Merge remote-tracking branch 'origin/master' into feature/payment-apply-bfi-sync
# Conflicts: # blade-common/src/main/java/org/springblade/common/config/BladeCommonConfiguration.java # blade-service-api/blade-system-api/src/main/java/org/springblade/system/feign/ISysClient.java # blade-service-api/blade-system-api/src/main/java/org/springblade/system/feign/ISysClientFallback.java # blade-service/blade-system/src/main/java/org/springblade/system/feign/SysClient.java # blade-service/blade-transport/pom.xml # blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/CustomerArchiveMapper.xml # blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java # blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/PaymentApplicationWrapper.java # blade-service/blade-transport/src/main/resources/application-dev.yml # blade-third-party-api/pom.xml # doc/nacos/blade-dev.yaml # doc/nacos/blade-prod.yaml # doc/nacos/third-party-api.yaml # pom.xml
This commit is contained in:
+6
@@ -6,14 +6,20 @@ package org.springblade.transport.config;
|
||||
|
||||
import io.minio.MinioClient;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* 凭证图片 MinIO 客户端配置。
|
||||
* <p>
|
||||
* 连接参数由 Nacos 的 file.storage.minio 配置提供。
|
||||
* 未配置 {@code file.storage.minio.endpoint} 时不注册该客户端,
|
||||
* 以免凭证上传功能缺失配置导致整个 blade-transport 服务无法启动;
|
||||
* 此时凭证相关接口会在调用时给出明确提示,而非启动即失败。
|
||||
*/
|
||||
@Configuration
|
||||
@ConditionalOnProperty(prefix = "file.storage.minio", name = "endpoint")
|
||||
public class VoucherMinioConfig {
|
||||
|
||||
@Bean
|
||||
|
||||
+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.controller;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.core.mp.support.Condition;
|
||||
import org.springblade.core.mp.support.Query;
|
||||
import org.springblade.core.secure.annotation.PreAuth;
|
||||
import org.springblade.core.secure.constant.AuthConstant;
|
||||
import org.springblade.core.tenant.annotation.TenantIgnore;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.transport.pojo.vo.CustomerArchiveVO;
|
||||
import org.springblade.transport.pojo.vo.CustomerChangeRecordVO;
|
||||
import org.springblade.transport.service.ICustomerArchiveService;
|
||||
import org.springblade.transport.service.impl.CustomerArchivePublicProcessService;
|
||||
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.Map;
|
||||
|
||||
/**
|
||||
* 客商档案公开查看 控制器
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@AllArgsConstructor
|
||||
@TenantIgnore
|
||||
@PreAuth(AuthConstant.PERMIT_ALL)
|
||||
@RequestMapping("/customer-archive/public")
|
||||
@Tag(name = "客商档案公开查看", description = "客商档案公开查看")
|
||||
public class CustomerArchivePublicController {
|
||||
|
||||
private final ICustomerArchiveService customerArchiveService;
|
||||
private final CustomerArchivePublicProcessService customerArchivePublicProcessService;
|
||||
|
||||
/**
|
||||
* 公开详情
|
||||
*/
|
||||
@GetMapping("/detail")
|
||||
@ApiOperationSupport(order = 1)
|
||||
@Operation(summary = "公开详情", description = "传入id,无需登录")
|
||||
public R<CustomerArchiveVO> detail(@Parameter(description = "主键", required = true) @RequestParam Long id) {
|
||||
return R.data(customerArchiveService.publicDetail(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 公开变更记录分页
|
||||
*/
|
||||
@GetMapping("/change-record/list")
|
||||
@ApiOperationSupport(order = 2)
|
||||
@Operation(summary = "公开变更记录分页", description = "传入客商ID,无需登录")
|
||||
public R<IPage<CustomerChangeRecordVO>> changeRecordList(
|
||||
@Parameter(description = "客商ID", required = true) @RequestParam Long customerId, Query query) {
|
||||
return R.data(customerArchiveService.publicChangeRecordPage(Condition.getPage(query), customerId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 公开接收流程页 postMessage 数据
|
||||
*/
|
||||
@PostMapping("/process-message")
|
||||
@ApiOperationSupport(order = 3)
|
||||
@Operation(summary = "公开接收流程消息", description = "无需登录,立即查询当前节点,5秒后查询流程实例详情并同步客商")
|
||||
public R processMessage(@RequestBody Map<String, Object> body) {
|
||||
log.info("客商公开页收到流程消息:{}", JSON.toJSONString(body));
|
||||
customerArchivePublicProcessService.handleProcessMessage(body);
|
||||
return R.success("ok");
|
||||
}
|
||||
|
||||
}
|
||||
+4
-8
@@ -30,7 +30,6 @@ import lombok.AllArgsConstructor;
|
||||
import org.springblade.core.boot.ctrl.BladeController;
|
||||
import org.springblade.core.mp.support.Condition;
|
||||
import org.springblade.core.mp.support.Query;
|
||||
import org.springblade.core.secure.annotation.PreAuth;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.transport.pojo.dto.ExceptionDisposalFollowRequest;
|
||||
import org.springblade.transport.pojo.entity.ExceptionDisposal;
|
||||
@@ -48,7 +47,7 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
* <p>
|
||||
* 对外路径:{@code /api/blade-transport/exception-disposal/**}
|
||||
* 同时兼容未去前缀直连 {@code /blade-transport/exception-disposal/**}。
|
||||
* 司机上报(submit)/ 列表 / 详情仅需登录态;跟进与完成保留菜单鉴权。
|
||||
* 列表 / 详情 / 上报 / 跟进 / 完成均仅需登录态(小程序调度端与司机端共用)。
|
||||
*/
|
||||
@RestController
|
||||
@AllArgsConstructor
|
||||
@@ -80,27 +79,24 @@ public class ExceptionDisposalController extends BladeController {
|
||||
}
|
||||
|
||||
@PostMapping("/follow")
|
||||
@PreAuth(menu = "exception_disposal")
|
||||
@ApiOperationSupport(order = 4)
|
||||
@Operation(summary = "异常跟进")
|
||||
@Operation(summary = "异常跟进", description = "调度端跟进;仅需登录态")
|
||||
public R follow(@RequestBody ExceptionDisposalFollowRequest request) {
|
||||
exceptionDisposalService.follow(request);
|
||||
return R.success("跟进成功");
|
||||
}
|
||||
|
||||
@PostMapping("/complete")
|
||||
@PreAuth(menu = "exception_disposal")
|
||||
@ApiOperationSupport(order = 5)
|
||||
@Operation(summary = "完成异常")
|
||||
@Operation(summary = "完成异常", description = "调度端结案;仅需登录态")
|
||||
public R complete(@RequestBody ExceptionDisposalFollowRequest request) {
|
||||
exceptionDisposalService.complete(request.getId());
|
||||
return R.success("完成成功");
|
||||
}
|
||||
|
||||
@PostMapping("/batch-complete")
|
||||
@PreAuth(menu = "exception_disposal")
|
||||
@ApiOperationSupport(order = 6)
|
||||
@Operation(summary = "批量完成异常")
|
||||
@Operation(summary = "批量完成异常", description = "调度端批量结案;仅需登录态")
|
||||
public R batchComplete(@RequestParam String ids) {
|
||||
exceptionDisposalService.batchComplete(ids);
|
||||
return R.success("批量完成成功");
|
||||
|
||||
+10
-3
@@ -119,22 +119,29 @@ public class LoadingManageController extends BladeController {
|
||||
return R.status(loadingManageService.changeRoute(loadingManage));
|
||||
}
|
||||
|
||||
@PostMapping("/cancel")
|
||||
@PostMapping("/start")
|
||||
@ApiOperationSupport(order = 10)
|
||||
@Operation(summary = "改为进行中", description = "传入id")
|
||||
public R start(@Parameter(description = "主键", required = true) @RequestParam Long id) {
|
||||
return R.status(loadingManageService.start(id));
|
||||
}
|
||||
|
||||
@PostMapping("/cancel")
|
||||
@ApiOperationSupport(order = 11)
|
||||
@Operation(summary = "取消", description = "传入id")
|
||||
public R cancel(@Parameter(description = "主键", required = true) @RequestParam Long id) {
|
||||
return R.status(loadingManageService.cancel(id));
|
||||
}
|
||||
|
||||
@PostMapping("/complete")
|
||||
@ApiOperationSupport(order = 11)
|
||||
@ApiOperationSupport(order = 12)
|
||||
@Operation(summary = "完成", description = "传入id")
|
||||
public R complete(@Parameter(description = "主键", required = true) @RequestParam Long id) {
|
||||
return R.status(loadingManageService.complete(id));
|
||||
}
|
||||
|
||||
@PostMapping("/batch-complete")
|
||||
@ApiOperationSupport(order = 12)
|
||||
@ApiOperationSupport(order = 13)
|
||||
@Operation(summary = "批量完成", description = "传入ids")
|
||||
public R<BusinessRemoveResultVO> batchComplete(@Parameter(description = "主键集合", required = true) @RequestParam String ids) {
|
||||
return R.data(loadingManageService.batchComplete(ids));
|
||||
|
||||
+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>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springblade.core.boot.ctrl.BladeController;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.transport.pojo.entity.Waybill;
|
||||
import org.springblade.transport.pojo.vo.AdminDriverOptionVO;
|
||||
import org.springblade.transport.pojo.vo.AdminHomeStatsVO;
|
||||
import org.springblade.transport.pojo.vo.AdminHomeVO;
|
||||
import org.springblade.transport.pojo.vo.AdminVehicleOptionVO;
|
||||
import org.springblade.transport.pojo.vo.AdminWaybillCardVO;
|
||||
import org.springblade.transport.pojo.vo.AdminWaybillDetailVO;
|
||||
import org.springblade.transport.service.IManageWaybillService;
|
||||
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.List;
|
||||
|
||||
/**
|
||||
* 调度端运单(小程序管理端)
|
||||
* <p>
|
||||
* 对外完整路径:{@code /api/blade-transport/waybill/manage/**}
|
||||
* (网关 StripPrefix 去掉 {@code blade-transport} 后落入 {@code /waybill/manage/**})。
|
||||
* 同时兼容未去前缀直连({@code /blade-transport/waybill/manage/**})。
|
||||
* 仅需登录态,不挂管理端菜单鉴权。
|
||||
*/
|
||||
@RestController
|
||||
@AllArgsConstructor
|
||||
@RequestMapping({"/waybill/manage", "/blade-transport/waybill/manage"})
|
||||
@Tag(name = "调度端运单", description = "小程序调度端首页统计与运单列表")
|
||||
public class ManageWaybillController extends BladeController {
|
||||
|
||||
private final IManageWaybillService manageWaybillService;
|
||||
|
||||
@GetMapping("/stats")
|
||||
@ApiOperationSupport(order = 1)
|
||||
@Operation(summary = "运单状态统计", description = "待接单=pending,运输中=running,已完成=completed;租户内不过滤组织(小程序调度账号组织常与运单不一致);在途异常=异常处置状态≠已完成")
|
||||
public R<AdminHomeStatsVO> stats() {
|
||||
return R.data(manageWaybillService.stats());
|
||||
}
|
||||
|
||||
@GetMapping("/home")
|
||||
@ApiOperationSupport(order = 2)
|
||||
@Operation(summary = "首页聚合", description = "统计 + 异常/风险角标 + 待处理事项(异常处置≠已完成)+ 当前用户名")
|
||||
public R<AdminHomeVO> home() {
|
||||
return R.data(manageWaybillService.home());
|
||||
}
|
||||
|
||||
@GetMapping("/list")
|
||||
@ApiOperationSupport(order = 3)
|
||||
@Operation(summary = "运单分页列表", description = "当前组织运单;status:0待接单/1运输中/2已完成;exception:exception/normal;transportType:common/load")
|
||||
public R<IPage<AdminWaybillCardVO>> list(
|
||||
@Parameter(description = "当前页") @RequestParam(required = false) Integer current,
|
||||
@Parameter(description = "每页条数") @RequestParam(required = false) Integer size,
|
||||
@Parameter(description = "关键字:运单号/司机/车牌") @RequestParam(required = false) String keyword,
|
||||
@Parameter(description = "状态:0待接单/1运输中/2已完成,不传为全部") @RequestParam(required = false) String status,
|
||||
@Parameter(description = "异常:exception有异常/normal无异常") @RequestParam(required = false) String exception,
|
||||
@Parameter(description = "运输组织:common普通/load配载") @RequestParam(required = false) String transportType,
|
||||
@Parameter(description = "创建日起 YYYY-MM-DD") @RequestParam(required = false) String startDate,
|
||||
@Parameter(description = "创建日止 YYYY-MM-DD") @RequestParam(required = false) String endDate) {
|
||||
return R.data(manageWaybillService.pageList(
|
||||
current, size, keyword, status, exception, transportType, startDate, endDate));
|
||||
}
|
||||
|
||||
@GetMapping("/detail")
|
||||
@ApiOperationSupport(order = 4)
|
||||
@Operation(summary = "运单详情", description = "调度端查看运单详情(含 punchNodes / enrouteRecords),不校验司机归属与组织;字段对齐小程序 pages/waybill/detail")
|
||||
public R<AdminWaybillDetailVO> detail(
|
||||
@Parameter(description = "运单ID", required = true) @RequestParam Long id) {
|
||||
return R.data(manageWaybillService.detail(id));
|
||||
}
|
||||
|
||||
@GetMapping("/pending")
|
||||
@ApiOperationSupport(order = 5)
|
||||
@Operation(summary = "待处理运单", description = "待接单/运输中;needReassign=true 仅司机已拒单")
|
||||
public R<IPage<AdminWaybillCardVO>> pending(
|
||||
@Parameter(description = "当前页") @RequestParam(required = false) Integer current,
|
||||
@Parameter(description = "每页条数") @RequestParam(required = false) Integer size,
|
||||
@Parameter(description = "关键字:运单号/司机/车牌") @RequestParam(required = false) String keyword,
|
||||
@Parameter(description = "是否需重新派单") @RequestParam(required = false) Boolean needReassign) {
|
||||
return R.data(manageWaybillService.pendingList(current, size, keyword, needReassign));
|
||||
}
|
||||
|
||||
@PostMapping("/reassign")
|
||||
@ApiOperationSupport(order = 6)
|
||||
@Operation(summary = "重新派单", description = "小程序调度端:跳过组织校验,仅需登录态;传入运单ID及新司机、手机号、车牌")
|
||||
public R reassign(@RequestBody Waybill waybill) {
|
||||
return R.status(manageWaybillService.reassign(
|
||||
waybill.getId(),
|
||||
waybill.getDriverId(),
|
||||
waybill.getDriverName(),
|
||||
waybill.getDriverPhone(),
|
||||
waybill.getVehicleNo()));
|
||||
}
|
||||
|
||||
@GetMapping("/driver-search")
|
||||
@ApiOperationSupport(order = 7)
|
||||
@Operation(summary = "搜索司机", description = "按姓名/手机号模糊搜索,供重新派单选用")
|
||||
public R<List<AdminDriverOptionVO>> driverSearch(
|
||||
@Parameter(description = "关键字") @RequestParam(required = false) String keyword) {
|
||||
return R.data(manageWaybillService.searchDrivers(keyword));
|
||||
}
|
||||
|
||||
@GetMapping("/vehicle-search")
|
||||
@ApiOperationSupport(order = 8)
|
||||
@Operation(summary = "搜索车牌", description = "按车牌模糊搜索(来自司机绑定车牌)")
|
||||
public R<List<AdminVehicleOptionVO>> vehicleSearch(
|
||||
@Parameter(description = "关键字") @RequestParam(required = false) String keyword) {
|
||||
return R.data(manageWaybillService.searchVehicles(keyword));
|
||||
}
|
||||
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
package org.springblade.transport.controller;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.core.secure.annotation.PreAuth;
|
||||
import org.springblade.core.secure.constant.AuthConstant;
|
||||
import org.springblade.core.tenant.annotation.TenantIgnore;
|
||||
import org.springblade.core.tool.api.R;
|
||||
import org.springblade.transport.mk.MkProcessMessageService;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
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.Map;
|
||||
|
||||
/**
|
||||
* MK 业务流程公开查看
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@AllArgsConstructor
|
||||
@TenantIgnore
|
||||
@PreAuth(AuthConstant.PERMIT_ALL)
|
||||
@RequestMapping("/mk-process/public")
|
||||
@Tag(name = "MK业务流程公开查看", description = "MK业务流程公开查看")
|
||||
public class MkProcessPublicController {
|
||||
|
||||
private final MkProcessMessageService mkProcessMessageService;
|
||||
|
||||
@GetMapping("/{bizType}/detail")
|
||||
@ApiOperationSupport(order = 1)
|
||||
@Operation(summary = "公开详情", description = "无需登录")
|
||||
public R detail(@PathVariable String bizType,
|
||||
@Parameter(description = "主键", required = true) @RequestParam Long id) {
|
||||
return R.data(mkProcessMessageService.handler(bizType).publicDetail(id));
|
||||
}
|
||||
|
||||
@PostMapping("/{bizType}/process-message")
|
||||
@ApiOperationSupport(order = 2)
|
||||
@Operation(summary = "公开接收流程消息", description = "无需登录,立即查询当前节点,5秒后查询流程实例详情并同步业务")
|
||||
public R processMessage(@PathVariable String bizType, @RequestBody Map<String, Object> body) {
|
||||
log.info("公开页收到流程消息 bizType={} body={}", bizType, JSON.toJSONString(body));
|
||||
mkProcessMessageService.handleProcessMessage(bizType, body);
|
||||
return R.success("ok");
|
||||
}
|
||||
}
|
||||
+20
-5
@@ -52,6 +52,7 @@ import org.springblade.transport.pojo.entity.Waybill;
|
||||
import org.springblade.transport.pojo.vo.BusinessRemoveResultVO;
|
||||
import org.springblade.transport.pojo.vo.ProcessConfigVO;
|
||||
import org.springblade.transport.service.IProcessConfigService;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
@@ -83,20 +84,34 @@ public class ProcessConfigController extends BladeController {
|
||||
private final VoucherFileMapper voucherFileMapper;
|
||||
private final VoucherImageMapper voucherImageMapper;
|
||||
private final VoucherManageMapper voucherManageMapper;
|
||||
private final MinioClient minioClient;
|
||||
private final ObjectProvider<MinioClient> minioClientProvider;
|
||||
@Value("${file.storage.minio.bucket-name:${minio.bucket-name:}}")
|
||||
private String minioBucketName;
|
||||
|
||||
public ProcessConfigController(IProcessConfigService processConfigService, WaybillMapper waybillMapper,
|
||||
VoucherFileMapper voucherFileMapper, VoucherImageMapper voucherImageMapper,
|
||||
VoucherManageMapper voucherManageMapper,
|
||||
MinioClient minioClient) {
|
||||
ObjectProvider<MinioClient> minioClientProvider) {
|
||||
this.processConfigService = processConfigService;
|
||||
this.waybillMapper = waybillMapper;
|
||||
this.voucherFileMapper = voucherFileMapper;
|
||||
this.voucherImageMapper = voucherImageMapper;
|
||||
this.voucherManageMapper = voucherManageMapper;
|
||||
this.minioClient = minioClient;
|
||||
this.minioClientProvider = minioClientProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 MinIO 客户端。
|
||||
* <p>
|
||||
* 未配置 file.storage.minio.endpoint 时该客户端不会被注册,
|
||||
* 此时凭证预览地址无法生成,抛出明确提示而非启动即失败。
|
||||
*/
|
||||
private MinioClient minioClient() {
|
||||
MinioClient minioClient = minioClientProvider.getIfAvailable();
|
||||
if (minioClient == null) {
|
||||
throw new IllegalStateException("Nacos 未配置 file.storage.minio.endpoint,凭证文件功能不可用");
|
||||
}
|
||||
return minioClient;
|
||||
}
|
||||
|
||||
@GetMapping("/detail")
|
||||
@@ -147,7 +162,7 @@ public class ProcessConfigController extends BladeController {
|
||||
result.put("waybillNo", image.getWaybillNo());
|
||||
result.put("objectKey", image.getObjectKey());
|
||||
try {
|
||||
result.put("url", minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
|
||||
result.put("url", minioClient().getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
|
||||
.method(Method.GET).bucket(minioBucketName).object(image.getObjectKey())
|
||||
.expiry(1, TimeUnit.HOURS).build()));
|
||||
} catch (Exception e) {
|
||||
@@ -270,7 +285,7 @@ public class ProcessConfigController extends BladeController {
|
||||
result.put("objectKey", image.objectKey());
|
||||
result.put("matched", image.matched());
|
||||
try {
|
||||
result.put("url", minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
|
||||
result.put("url", minioClient().getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
|
||||
.method(Method.GET).bucket(minioBucketName).object(image.objectKey())
|
||||
.expiry(1, TimeUnit.HOURS).build()));
|
||||
} catch (Exception exception) {
|
||||
|
||||
+1
-1
@@ -105,7 +105,7 @@ public class ProjectApplyController extends BladeController {
|
||||
@ApiOperationSupport(order = 4)
|
||||
@Operation(summary = "新增或修改", description = "传入projectApply")
|
||||
public R submit(@RequestBody ProjectApply projectApply) {
|
||||
return R.status(projectApplyService.submit(projectApply));
|
||||
return projectApplyService.submit(projectApply) ? R.data(projectApply) : R.fail("保存失败");
|
||||
}
|
||||
|
||||
@PostMapping("/submit-approval")
|
||||
|
||||
+20
-1
@@ -53,6 +53,8 @@ import org.springblade.transport.pojo.dto.WaybillImportBatchRequest;
|
||||
import org.springblade.transport.pojo.vo.BusinessRemoveResultVO;
|
||||
import org.springblade.transport.pojo.vo.LoadingManageVO;
|
||||
import org.springblade.transport.pojo.vo.WaybillImportBatchVO;
|
||||
import org.springblade.transport.pojo.vo.WaybillLocateVO;
|
||||
import org.springblade.transport.pojo.vo.WaybillTrackVO;
|
||||
import org.springblade.transport.pojo.vo.WaybillPunchRecordsVO;
|
||||
import org.springblade.transport.pojo.vo.WaybillVO;
|
||||
import org.springblade.transport.pojo.dto.WaybillMileageRequest;
|
||||
@@ -109,6 +111,23 @@ public class WaybillController extends BladeController {
|
||||
return R.data(waybillService.listPunchRecords(waybillId));
|
||||
}
|
||||
|
||||
@PostMapping("/locate")
|
||||
@ApiOperationSupport(order = 1)
|
||||
@Operation(summary = "车辆实时定位", description = "按运单绑定车牌调用 LBS_LOCATE")
|
||||
public R<WaybillLocateVO> locate(@Parameter(description = "运单ID", required = true) @RequestParam Long id) {
|
||||
return R.data(waybillService.locateVehicle(id));
|
||||
}
|
||||
|
||||
@PostMapping("/track")
|
||||
@ApiOperationSupport(order = 1)
|
||||
@Operation(summary = "车辆历史轨迹", description = "按运单绑定车牌 + 日期区间调用 LBS_TRACK")
|
||||
public R<WaybillTrackVO> track(
|
||||
@Parameter(description = "运单ID", required = true) @RequestParam Long id,
|
||||
@Parameter(description = "开始日期 YYYY-MM-DD", required = true) @RequestParam String startDate,
|
||||
@Parameter(description = "结束日期 YYYY-MM-DD", required = true) @RequestParam String endDate) {
|
||||
return R.data(waybillService.trackVehicle(id, startDate, endDate));
|
||||
}
|
||||
|
||||
@GetMapping("/list")
|
||||
@ApiOperationSupport(order = 2)
|
||||
@Operation(summary = "分页", description = "传入waybill")
|
||||
@@ -194,7 +213,7 @@ public class WaybillController extends BladeController {
|
||||
@ApiOperationSupport(order = 9)
|
||||
@Operation(summary = "新增或修改", description = "传入waybill")
|
||||
public R submit(@RequestBody Waybill waybill) {
|
||||
return R.status(waybillService.submit(waybill));
|
||||
return waybillService.submit(waybill) ? R.data(waybill) : R.fail("保存失败");
|
||||
}
|
||||
|
||||
@PostMapping("/save-draft")
|
||||
|
||||
+2
-3
@@ -36,7 +36,6 @@ import lombok.Data;
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
* 事故记录 Excel
|
||||
@@ -60,8 +59,8 @@ public class AccidentRecordExcel implements Serializable {
|
||||
@ExcelProperty("*车牌号/船号")
|
||||
private String vehicleNo;
|
||||
|
||||
@ExcelProperty("*事故发生日期")
|
||||
private LocalDate accidentDate;
|
||||
@ExcelProperty(value = "*事故发生日期", converter = org.springblade.common.excel.LenientDateStringConverter.class)
|
||||
private String accidentDate;
|
||||
|
||||
@ExcelProperty("事故发生地点")
|
||||
private String accidentLocation;
|
||||
|
||||
+3
-3
@@ -37,7 +37,7 @@ import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 事故记录导出 Excel
|
||||
@@ -83,14 +83,14 @@ public class AccidentRecordExportExcel implements Serializable {
|
||||
|
||||
@ExcelProperty("创建时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createTime;
|
||||
private Date createTime;
|
||||
|
||||
@ExcelProperty("更新人")
|
||||
private String updateUserName;
|
||||
|
||||
@ExcelProperty("更新时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime updateTime;
|
||||
private Date updateTime;
|
||||
|
||||
@ExcelProperty("备注")
|
||||
private String remark;
|
||||
|
||||
+4
-5
@@ -35,7 +35,6 @@ import lombok.Data;
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
* 年检记录 Excel
|
||||
@@ -59,11 +58,11 @@ public class AnnualInspectionRecordExcel implements Serializable {
|
||||
@ExcelProperty("*车牌号/船号")
|
||||
private String vehicleNo;
|
||||
|
||||
@ExcelProperty("*检测评定日期")
|
||||
private LocalDate inspectionAssessmentDate;
|
||||
@ExcelProperty(value = "*检测评定日期", converter = org.springblade.common.excel.LenientDateStringConverter.class)
|
||||
private String inspectionAssessmentDate;
|
||||
|
||||
@ExcelProperty("*有效期截止日")
|
||||
private LocalDate validUntilDate;
|
||||
@ExcelProperty(value = "*有效期截止日", converter = org.springblade.common.excel.LenientDateStringConverter.class)
|
||||
private String validUntilDate;
|
||||
|
||||
@ExcelProperty("*车辆技术等级")
|
||||
private String vehicleTechnicalLevel;
|
||||
|
||||
+3
-3
@@ -36,7 +36,7 @@ import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 年检记录导出 Excel
|
||||
@@ -83,14 +83,14 @@ public class AnnualInspectionRecordExportExcel implements Serializable {
|
||||
|
||||
@ExcelProperty("创建时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createTime;
|
||||
private Date createTime;
|
||||
|
||||
@ExcelProperty("更新人")
|
||||
private String updateUserName;
|
||||
|
||||
@ExcelProperty("更新时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime updateTime;
|
||||
private Date updateTime;
|
||||
|
||||
@ExcelProperty("备注")
|
||||
private String remark;
|
||||
|
||||
+99
-96
@@ -1,96 +1,99 @@
|
||||
/**
|
||||
* 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.excel;
|
||||
|
||||
import cn.idev.excel.annotation.ExcelIgnore;
|
||||
import cn.idev.excel.annotation.ExcelProperty;
|
||||
import cn.idev.excel.annotation.write.style.ColumnWidth;
|
||||
import cn.idev.excel.annotation.write.style.ContentRowHeight;
|
||||
import cn.idev.excel.annotation.write.style.HeadRowHeight;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 常用地址导出 Excel
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Data
|
||||
@ColumnWidth(18)
|
||||
@HeadRowHeight(20)
|
||||
@ContentRowHeight(18)
|
||||
public class CommonAddressExportExcel implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@ExcelIgnore
|
||||
private Long id;
|
||||
|
||||
@ExcelProperty("地址名称")
|
||||
private String addressName;
|
||||
|
||||
@ExcelProperty("地址编号")
|
||||
private String addressCode;
|
||||
|
||||
@ExcelProperty("类型")
|
||||
private String addressType;
|
||||
|
||||
@ExcelProperty("站点编码")
|
||||
private String siteCodeDisplay;
|
||||
|
||||
@ExcelProperty("详细地址")
|
||||
private String detailAddress;
|
||||
|
||||
@ExcelProperty("经度")
|
||||
private BigDecimal longitude;
|
||||
|
||||
@ExcelProperty("纬度")
|
||||
private BigDecimal latitude;
|
||||
|
||||
@ExcelProperty("行政区划")
|
||||
private String regionName;
|
||||
|
||||
@ExcelProperty("联系人")
|
||||
private String contactName;
|
||||
|
||||
@ExcelProperty("联系方式")
|
||||
private String contactPhone;
|
||||
|
||||
@ExcelProperty("组织")
|
||||
private String deptName;
|
||||
|
||||
@ExcelProperty("备注")
|
||||
private String remark;
|
||||
|
||||
@ExcelProperty("更新时间")
|
||||
private Date updateTime;
|
||||
|
||||
@ExcelProperty("创建时间")
|
||||
private Date createTime;
|
||||
|
||||
}
|
||||
/**
|
||||
* 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.excel;
|
||||
|
||||
import cn.idev.excel.annotation.ExcelIgnore;
|
||||
import cn.idev.excel.annotation.ExcelProperty;
|
||||
import cn.idev.excel.annotation.format.DateTimeFormat;
|
||||
import cn.idev.excel.annotation.write.style.ColumnWidth;
|
||||
import cn.idev.excel.annotation.write.style.ContentRowHeight;
|
||||
import cn.idev.excel.annotation.write.style.HeadRowHeight;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 常用地址导出 Excel
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Data
|
||||
@ColumnWidth(18)
|
||||
@HeadRowHeight(20)
|
||||
@ContentRowHeight(18)
|
||||
public class CommonAddressExportExcel implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@ExcelIgnore
|
||||
private Long id;
|
||||
|
||||
@ExcelProperty("地址名称")
|
||||
private String addressName;
|
||||
|
||||
@ExcelProperty("地址编号")
|
||||
private String addressCode;
|
||||
|
||||
@ExcelProperty("类型")
|
||||
private String addressType;
|
||||
|
||||
@ExcelProperty("站点编码")
|
||||
private String siteCodeDisplay;
|
||||
|
||||
@ExcelProperty("详细地址")
|
||||
private String detailAddress;
|
||||
|
||||
@ExcelProperty("经度")
|
||||
private BigDecimal longitude;
|
||||
|
||||
@ExcelProperty("纬度")
|
||||
private BigDecimal latitude;
|
||||
|
||||
@ExcelProperty("行政区划")
|
||||
private String regionName;
|
||||
|
||||
@ExcelProperty("联系人")
|
||||
private String contactName;
|
||||
|
||||
@ExcelProperty("联系方式")
|
||||
private String contactPhone;
|
||||
|
||||
@ExcelProperty("组织")
|
||||
private String deptName;
|
||||
|
||||
@ExcelProperty("备注")
|
||||
private String remark;
|
||||
|
||||
@ExcelProperty("更新时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private Date updateTime;
|
||||
|
||||
@ExcelProperty("创建时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private Date createTime;
|
||||
|
||||
}
|
||||
|
||||
+104
-101
@@ -1,101 +1,104 @@
|
||||
/**
|
||||
* 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.excel;
|
||||
|
||||
import cn.idev.excel.annotation.ExcelProperty;
|
||||
import cn.idev.excel.annotation.format.NumberFormat;
|
||||
import cn.idev.excel.annotation.write.style.ColumnWidth;
|
||||
import cn.idev.excel.annotation.write.style.ContentRowHeight;
|
||||
import cn.idev.excel.annotation.write.style.HeadRowHeight;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 常用货物导出 Excel
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Data
|
||||
@ColumnWidth(24)
|
||||
@HeadRowHeight(20)
|
||||
@ContentRowHeight(18)
|
||||
public class CommonCargoExportExcel implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@ExcelProperty("货物名称")
|
||||
private String cargoName;
|
||||
|
||||
@ExcelProperty("货物编号后缀")
|
||||
private String cargoCodeSuffix;
|
||||
|
||||
@ExcelProperty("一级货物类型")
|
||||
private String firstCargoTypeName;
|
||||
|
||||
@ExcelProperty("二级货物类型")
|
||||
private String secondCargoTypeName;
|
||||
|
||||
@ExcelProperty("二级货物类型编码")
|
||||
private String secondCargoTypeCode;
|
||||
|
||||
@ExcelProperty("包装")
|
||||
private String packageType;
|
||||
|
||||
@ExcelProperty("品牌")
|
||||
private String brand;
|
||||
|
||||
@ExcelProperty("规格")
|
||||
private String specification;
|
||||
|
||||
@ExcelProperty("型号")
|
||||
private String model;
|
||||
|
||||
@ExcelProperty("单价")
|
||||
@NumberFormat("0.00")
|
||||
private BigDecimal cargoValue;
|
||||
|
||||
@ExcelProperty("计价单位")
|
||||
private String priceUnit;
|
||||
|
||||
@ExcelProperty("说明")
|
||||
private String descriptionOne;
|
||||
|
||||
@ExcelProperty("备注")
|
||||
private String remark;
|
||||
|
||||
@ExcelProperty("组织")
|
||||
private String deptName;
|
||||
|
||||
@ExcelProperty("更新时间")
|
||||
private Date updateTime;
|
||||
|
||||
@ExcelProperty("创建时间")
|
||||
private Date createTime;
|
||||
|
||||
}
|
||||
/**
|
||||
* 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.excel;
|
||||
|
||||
import cn.idev.excel.annotation.ExcelProperty;
|
||||
import cn.idev.excel.annotation.format.DateTimeFormat;
|
||||
import cn.idev.excel.annotation.format.NumberFormat;
|
||||
import cn.idev.excel.annotation.write.style.ColumnWidth;
|
||||
import cn.idev.excel.annotation.write.style.ContentRowHeight;
|
||||
import cn.idev.excel.annotation.write.style.HeadRowHeight;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 常用货物导出 Excel
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Data
|
||||
@ColumnWidth(24)
|
||||
@HeadRowHeight(20)
|
||||
@ContentRowHeight(18)
|
||||
public class CommonCargoExportExcel implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@ExcelProperty("货物名称")
|
||||
private String cargoName;
|
||||
|
||||
@ExcelProperty("货物编号后缀")
|
||||
private String cargoCodeSuffix;
|
||||
|
||||
@ExcelProperty("一级货物类型")
|
||||
private String firstCargoTypeName;
|
||||
|
||||
@ExcelProperty("二级货物类型")
|
||||
private String secondCargoTypeName;
|
||||
|
||||
@ExcelProperty("二级货物类型编码")
|
||||
private String secondCargoTypeCode;
|
||||
|
||||
@ExcelProperty("包装")
|
||||
private String packageType;
|
||||
|
||||
@ExcelProperty("品牌")
|
||||
private String brand;
|
||||
|
||||
@ExcelProperty("规格")
|
||||
private String specification;
|
||||
|
||||
@ExcelProperty("型号")
|
||||
private String model;
|
||||
|
||||
@ExcelProperty("单价")
|
||||
@NumberFormat("0.00")
|
||||
private BigDecimal cargoValue;
|
||||
|
||||
@ExcelProperty("计价单位")
|
||||
private String priceUnit;
|
||||
|
||||
@ExcelProperty("说明")
|
||||
private String descriptionOne;
|
||||
|
||||
@ExcelProperty("备注")
|
||||
private String remark;
|
||||
|
||||
@ExcelProperty("组织")
|
||||
private String deptName;
|
||||
|
||||
@ExcelProperty("更新时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private Date updateTime;
|
||||
|
||||
@ExcelProperty("创建时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private Date createTime;
|
||||
|
||||
}
|
||||
|
||||
+85
-82
@@ -1,82 +1,85 @@
|
||||
/**
|
||||
* 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.excel;
|
||||
|
||||
import cn.idev.excel.annotation.ExcelProperty;
|
||||
import cn.idev.excel.annotation.write.style.ColumnWidth;
|
||||
import cn.idev.excel.annotation.write.style.ContentRowHeight;
|
||||
import cn.idev.excel.annotation.write.style.HeadRowHeight;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 常用线路导出 Excel
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Data
|
||||
@ColumnWidth(22)
|
||||
@HeadRowHeight(20)
|
||||
@ContentRowHeight(18)
|
||||
public class CommonRouteExportExcel implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@ExcelProperty("线路编号")
|
||||
private String routeCode;
|
||||
@ExcelProperty("线路名称")
|
||||
private String routeName;
|
||||
@ExcelProperty("发货地")
|
||||
private String departureName;
|
||||
@ExcelProperty("发货地址")
|
||||
private String departureAddress;
|
||||
@ExcelProperty("发货联系人")
|
||||
private String departureContact;
|
||||
@ExcelProperty("发货联系方式")
|
||||
private String departurePhone;
|
||||
@ExcelProperty("收货地")
|
||||
private String arrivalName;
|
||||
@ExcelProperty("收货地址")
|
||||
private String arrivalAddress;
|
||||
@ExcelProperty("收货联系人")
|
||||
private String arrivalContact;
|
||||
@ExcelProperty("收货联系方式")
|
||||
private String arrivalPhone;
|
||||
@ExcelProperty("所属组织")
|
||||
private String deptName;
|
||||
@ExcelProperty("备注")
|
||||
private String remark;
|
||||
@ExcelProperty("创建人")
|
||||
private String createUserName;
|
||||
@ExcelProperty("更新人")
|
||||
private String updateUserName;
|
||||
@ExcelProperty("更新时间")
|
||||
private Date updateTime;
|
||||
@ExcelProperty("创建时间")
|
||||
private Date createTime;
|
||||
|
||||
}
|
||||
/**
|
||||
* 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.excel;
|
||||
|
||||
import cn.idev.excel.annotation.ExcelProperty;
|
||||
import cn.idev.excel.annotation.format.DateTimeFormat;
|
||||
import cn.idev.excel.annotation.write.style.ColumnWidth;
|
||||
import cn.idev.excel.annotation.write.style.ContentRowHeight;
|
||||
import cn.idev.excel.annotation.write.style.HeadRowHeight;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 常用线路导出 Excel
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Data
|
||||
@ColumnWidth(22)
|
||||
@HeadRowHeight(20)
|
||||
@ContentRowHeight(18)
|
||||
public class CommonRouteExportExcel implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@ExcelProperty("线路编号")
|
||||
private String routeCode;
|
||||
@ExcelProperty("线路名称")
|
||||
private String routeName;
|
||||
@ExcelProperty("发货地")
|
||||
private String departureName;
|
||||
@ExcelProperty("发货地址")
|
||||
private String departureAddress;
|
||||
@ExcelProperty("发货联系人")
|
||||
private String departureContact;
|
||||
@ExcelProperty("发货联系方式")
|
||||
private String departurePhone;
|
||||
@ExcelProperty("收货地")
|
||||
private String arrivalName;
|
||||
@ExcelProperty("收货地址")
|
||||
private String arrivalAddress;
|
||||
@ExcelProperty("收货联系人")
|
||||
private String arrivalContact;
|
||||
@ExcelProperty("收货联系方式")
|
||||
private String arrivalPhone;
|
||||
@ExcelProperty("所属组织")
|
||||
private String deptName;
|
||||
@ExcelProperty("备注")
|
||||
private String remark;
|
||||
@ExcelProperty("创建人")
|
||||
private String createUserName;
|
||||
@ExcelProperty("更新人")
|
||||
private String updateUserName;
|
||||
@ExcelProperty("更新时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private Date updateTime;
|
||||
@ExcelProperty("创建时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private Date createTime;
|
||||
|
||||
}
|
||||
|
||||
+121
-118
@@ -1,118 +1,121 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.excel;
|
||||
|
||||
import cn.idev.excel.annotation.ExcelProperty;
|
||||
import cn.idev.excel.annotation.format.NumberFormat;
|
||||
import cn.idev.excel.annotation.write.style.ColumnWidth;
|
||||
import cn.idev.excel.annotation.write.style.ContentRowHeight;
|
||||
import cn.idev.excel.annotation.write.style.HeadRowHeight;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 合同管理 Excel
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Data
|
||||
@ColumnWidth(22)
|
||||
@HeadRowHeight(20)
|
||||
@ContentRowHeight(18)
|
||||
public class ContractManageExcel implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@ExcelProperty("合同编号")
|
||||
private String contractNo;
|
||||
@ExcelProperty("合同名称")
|
||||
private String contractName;
|
||||
@ExcelProperty("所属项目")
|
||||
private String projectName;
|
||||
@ExcelProperty("所属组织")
|
||||
private String organizationName;
|
||||
@ExcelProperty("合同类别")
|
||||
private String contractCategory;
|
||||
@ExcelProperty("签约类型")
|
||||
private String signType;
|
||||
@ExcelProperty("甲方")
|
||||
private String partyA;
|
||||
@ExcelProperty("乙方")
|
||||
private String partyB;
|
||||
@ExcelProperty("开始日期")
|
||||
private LocalDate startDate;
|
||||
@ExcelProperty("结束日期")
|
||||
private LocalDate endDate;
|
||||
@ExcelProperty("临时效力起")
|
||||
private LocalDate temporaryStartDate;
|
||||
@ExcelProperty("临时效力止")
|
||||
private LocalDate temporaryEndDate;
|
||||
@ExcelProperty("经办人")
|
||||
private String handlerUserName;
|
||||
@ExcelProperty("合同阶段")
|
||||
private String contractStage;
|
||||
@ExcelProperty("审核状态")
|
||||
private String approvalStatus;
|
||||
@ExcelProperty("归档状态")
|
||||
private String archiveStatus;
|
||||
@ExcelProperty("结算币种")
|
||||
private String settlementCurrency;
|
||||
@ExcelProperty("结算方式")
|
||||
private String settlementMode;
|
||||
@ExcelProperty("开票周期(天)")
|
||||
private Integer invoiceCycle;
|
||||
@ExcelProperty("一式份数")
|
||||
private Integer copyCount;
|
||||
@ExcelProperty("回款账期(天)")
|
||||
private Integer paymentDays;
|
||||
@ExcelProperty("合同金额")
|
||||
@NumberFormat("0.00")
|
||||
private BigDecimal contractAmount;
|
||||
@ExcelProperty("是否范本")
|
||||
private Integer templateFlag;
|
||||
@ExcelProperty("原件合同编号")
|
||||
private String originalContractNo;
|
||||
@ExcelProperty("是否电子章")
|
||||
private Integer electronicSealFlag;
|
||||
@ExcelProperty("当前节点")
|
||||
private String currentNode;
|
||||
@ExcelProperty("当前处理人")
|
||||
private String currentProcessor;
|
||||
@ExcelProperty("备注")
|
||||
private String remark;
|
||||
@ExcelProperty("创建人")
|
||||
private String createUserName;
|
||||
@ExcelProperty("更新人")
|
||||
private String updateUserName;
|
||||
@ExcelProperty("创建时间")
|
||||
private LocalDateTime createTime;
|
||||
@ExcelProperty("更新时间")
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
}
|
||||
/**
|
||||
* 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.excel;
|
||||
|
||||
import cn.idev.excel.annotation.ExcelProperty;
|
||||
import cn.idev.excel.annotation.format.DateTimeFormat;
|
||||
import cn.idev.excel.annotation.format.NumberFormat;
|
||||
import cn.idev.excel.annotation.write.style.ColumnWidth;
|
||||
import cn.idev.excel.annotation.write.style.ContentRowHeight;
|
||||
import cn.idev.excel.annotation.write.style.HeadRowHeight;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 合同管理 Excel
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Data
|
||||
@ColumnWidth(22)
|
||||
@HeadRowHeight(20)
|
||||
@ContentRowHeight(18)
|
||||
public class ContractManageExcel implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@ExcelProperty("合同编号")
|
||||
private String contractNo;
|
||||
@ExcelProperty("合同名称")
|
||||
private String contractName;
|
||||
@ExcelProperty("所属项目")
|
||||
private String projectName;
|
||||
@ExcelProperty("所属组织")
|
||||
private String organizationName;
|
||||
@ExcelProperty("合同类别")
|
||||
private String contractCategory;
|
||||
@ExcelProperty("签约类型")
|
||||
private String signType;
|
||||
@ExcelProperty("甲方")
|
||||
private String partyA;
|
||||
@ExcelProperty("乙方")
|
||||
private String partyB;
|
||||
@ExcelProperty("开始日期")
|
||||
private LocalDate startDate;
|
||||
@ExcelProperty("结束日期")
|
||||
private LocalDate endDate;
|
||||
@ExcelProperty("临时效力起")
|
||||
private LocalDate temporaryStartDate;
|
||||
@ExcelProperty("临时效力止")
|
||||
private LocalDate temporaryEndDate;
|
||||
@ExcelProperty("经办人")
|
||||
private String handlerUserName;
|
||||
@ExcelProperty("合同阶段")
|
||||
private String contractStage;
|
||||
@ExcelProperty("审核状态")
|
||||
private String approvalStatus;
|
||||
@ExcelProperty("归档状态")
|
||||
private String archiveStatus;
|
||||
@ExcelProperty("结算币种")
|
||||
private String settlementCurrency;
|
||||
@ExcelProperty("结算方式")
|
||||
private String settlementMode;
|
||||
@ExcelProperty("开票周期(天)")
|
||||
private Integer invoiceCycle;
|
||||
@ExcelProperty("一式份数")
|
||||
private Integer copyCount;
|
||||
@ExcelProperty("回款账期(天)")
|
||||
private Integer paymentDays;
|
||||
@ExcelProperty("合同金额")
|
||||
@NumberFormat("0.00")
|
||||
private BigDecimal contractAmount;
|
||||
@ExcelProperty("是否范本")
|
||||
private Integer templateFlag;
|
||||
@ExcelProperty("原件合同编号")
|
||||
private String originalContractNo;
|
||||
@ExcelProperty("是否电子章")
|
||||
private Integer electronicSealFlag;
|
||||
@ExcelProperty("当前节点")
|
||||
private String currentNode;
|
||||
@ExcelProperty("当前处理人")
|
||||
private String currentProcessor;
|
||||
@ExcelProperty("备注")
|
||||
private String remark;
|
||||
@ExcelProperty("创建人")
|
||||
private String createUserName;
|
||||
@ExcelProperty("更新人")
|
||||
private String updateUserName;
|
||||
@ExcelProperty("创建时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private Date createTime;
|
||||
@ExcelProperty("更新时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private Date updateTime;
|
||||
|
||||
}
|
||||
|
||||
+3
@@ -58,6 +58,9 @@ public class CustomerArchiveExcel implements Serializable {
|
||||
private String fullName;
|
||||
|
||||
@ExcelProperty("客商类型")
|
||||
private String customerKind;
|
||||
|
||||
@ExcelProperty("客商分类")
|
||||
private String customerType;
|
||||
|
||||
@ExcelProperty("客商性质")
|
||||
|
||||
+1
-3
@@ -7,12 +7,10 @@ package org.springblade.transport.excel;
|
||||
|
||||
import cn.idev.excel.annotation.ExcelIgnore;
|
||||
import cn.idev.excel.annotation.ExcelProperty;
|
||||
import cn.idev.excel.annotation.format.DateTimeFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
* 设备台账 Excel
|
||||
@@ -29,7 +27,7 @@ public class EquipmentLedgerExcel implements Serializable {
|
||||
@ExcelProperty("设备品牌") private String equipmentBrand;
|
||||
@ExcelProperty("设备类型") private String equipmentType;
|
||||
@ExcelProperty("规格型号") private String specificationModel;
|
||||
@ExcelProperty("出厂日期") @DateTimeFormat("yyyy-MM-dd") private LocalDate factoryDate;
|
||||
@ExcelProperty(value = "出厂日期", converter = org.springblade.common.excel.LenientDateStringConverter.class) private String factoryDate;
|
||||
@ExcelProperty("备注") private String remark;
|
||||
@ExcelIgnore private String errorMessage;
|
||||
}
|
||||
|
||||
+3
-2
@@ -11,6 +11,7 @@ import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* ETC记录导出 Excel
|
||||
@@ -50,14 +51,14 @@ public class EtcRecordExportExcel implements Serializable {
|
||||
|
||||
@ExcelProperty("创建时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createTime;
|
||||
private Date createTime;
|
||||
|
||||
@ExcelProperty("更新人")
|
||||
private String updateUserName;
|
||||
|
||||
@ExcelProperty("更新时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime updateTime;
|
||||
private Date updateTime;
|
||||
|
||||
@ExcelProperty("备注")
|
||||
private String remark;
|
||||
|
||||
+6
-7
@@ -35,7 +35,6 @@ import lombok.Data;
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
* 保险记录 Excel
|
||||
@@ -68,11 +67,11 @@ public class InsuranceRecordExcel implements Serializable {
|
||||
@ExcelProperty("*保单号")
|
||||
private String policyNo;
|
||||
|
||||
@ExcelProperty("*开始日期")
|
||||
private LocalDate startDate;
|
||||
@ExcelProperty(value = "*开始日期", converter = org.springblade.common.excel.LenientDateStringConverter.class)
|
||||
private String startDate;
|
||||
|
||||
@ExcelProperty("*结束日期")
|
||||
private LocalDate endDate;
|
||||
@ExcelProperty(value = "*结束日期", converter = org.springblade.common.excel.LenientDateStringConverter.class)
|
||||
private String endDate;
|
||||
|
||||
@ExcelProperty("保额")
|
||||
private BigDecimal insuredAmount;
|
||||
@@ -83,8 +82,8 @@ public class InsuranceRecordExcel implements Serializable {
|
||||
@ExcelProperty("发票号")
|
||||
private String invoiceNo;
|
||||
|
||||
@ExcelProperty("开票日期")
|
||||
private LocalDate invoiceDate;
|
||||
@ExcelProperty(value = "开票日期", converter = org.springblade.common.excel.LenientDateStringConverter.class)
|
||||
private String invoiceDate;
|
||||
|
||||
@ExcelProperty("备注")
|
||||
private String remark;
|
||||
|
||||
+3
-3
@@ -36,7 +36,7 @@ import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 保险记录导出 Excel
|
||||
@@ -83,14 +83,14 @@ public class InsuranceRecordExportExcel implements Serializable {
|
||||
|
||||
@ExcelProperty("创建时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createTime;
|
||||
private Date createTime;
|
||||
|
||||
@ExcelProperty("更新人")
|
||||
private String updateUserName;
|
||||
|
||||
@ExcelProperty("更新时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime updateTime;
|
||||
private Date updateTime;
|
||||
|
||||
@ExcelProperty("备注")
|
||||
private String remark;
|
||||
|
||||
+11
-15
@@ -33,22 +33,20 @@ import cn.idev.excel.metadata.data.WriteCellData;
|
||||
import cn.idev.excel.metadata.property.ExcelContentProperty;
|
||||
import cn.idev.excel.util.DateUtils;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeParseException;
|
||||
|
||||
/**
|
||||
* 保养记录日期转换器,兼容日期文本、日期时间文本和 Excel 数值日期。
|
||||
* <p>
|
||||
* 文本解析委托公共宽容解析器(支持 2026-8-2、2026/8/2 等写法,口径见根工作区
|
||||
* docs/import-spec.md);无法识别的文本按既有行为抛出转换异常,
|
||||
* 文案带原值,与「日期格式无法识别」口径一致。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
public class MaintenancePlanDateTimeConverter implements Converter<LocalDateTime> {
|
||||
|
||||
private static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
|
||||
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
private static final DateTimeFormatter DATE_TIME_MINUTE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
|
||||
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT);
|
||||
|
||||
@Override
|
||||
public Class<?> supportJavaTypeKey() {
|
||||
@@ -71,16 +69,14 @@ public class MaintenancePlanDateTimeConverter implements Converter<LocalDateTime
|
||||
if (value == null || value.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
String normalizedValue = value.trim();
|
||||
try {
|
||||
return LocalDateTime.parse(normalizedValue, DATE_TIME_FORMATTER);
|
||||
} catch (DateTimeParseException ignored) {
|
||||
try {
|
||||
return LocalDateTime.parse(normalizedValue, DATE_TIME_MINUTE_FORMATTER);
|
||||
} catch (DateTimeParseException ignoredMinute) {
|
||||
return LocalDate.parse(normalizedValue, DATE_FORMATTER).atStartOfDay();
|
||||
}
|
||||
LocalDateTime parsed = org.springblade.common.excel.LenientDateParser.parseDateTimeOrNull(value);
|
||||
if (parsed != null) {
|
||||
return parsed;
|
||||
}
|
||||
// 无法识别的文本:与既有行为一致抛出转换异常(ExcelDataConvertException 是 RuntimeException,
|
||||
// 会带上本文案一路抛到 Controller,由全局异常处理返回给前端提示)。
|
||||
throw new cn.idev.excel.exception.ExcelDataConvertException(-1, -1, cellData, contentProperty,
|
||||
value.trim() + " 日期格式无法识别");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+3
-3
@@ -11,7 +11,7 @@ import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 车辆保养记录导出 Excel
|
||||
@@ -26,12 +26,12 @@ public class MaintenancePlanExportExcel extends MaintenancePlanExcel {
|
||||
|
||||
@ExcelProperty(value = "创建时间", index = 12)
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createTime;
|
||||
private Date createTime;
|
||||
|
||||
@ExcelProperty(value = "更新人", index = 13)
|
||||
private String updateUserName;
|
||||
|
||||
@ExcelProperty(value = "更新时间", index = 14)
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime updateTime;
|
||||
private Date updateTime;
|
||||
}
|
||||
|
||||
+5
-4
@@ -38,6 +38,7 @@ import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 车辆维修记录 Excel
|
||||
@@ -95,16 +96,16 @@ public class MaintenanceRecordExcel implements Serializable {
|
||||
@NumberFormat("0.00")
|
||||
private BigDecimal mileage;
|
||||
|
||||
@ExcelProperty(value = "创建时间", converter = MaintenancePlanDateTimeConverter.class)
|
||||
@ExcelProperty("创建时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createTime;
|
||||
private Date createTime;
|
||||
|
||||
@ExcelProperty("更新人")
|
||||
private String updateUserName;
|
||||
|
||||
@ExcelProperty(value = "更新时间", converter = MaintenancePlanDateTimeConverter.class)
|
||||
@ExcelProperty("更新时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime updateTime;
|
||||
private Date updateTime;
|
||||
|
||||
@ExcelProperty("里程单位")
|
||||
private String mileageUnit;
|
||||
|
||||
-4
@@ -25,7 +25,6 @@
|
||||
*/
|
||||
package org.springblade.transport.excel;
|
||||
|
||||
import cn.idev.excel.annotation.ExcelProperty;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
@@ -42,7 +41,4 @@ public class MaintenanceRecordExportExcel extends MaintenanceRecordExcel {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@ExcelProperty(value = "导出失败原因", index = 17)
|
||||
private String errorMessage;
|
||||
|
||||
}
|
||||
|
||||
+3
-3
@@ -36,7 +36,7 @@ import lombok.Data;
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 里程记录导出 Excel
|
||||
@@ -72,14 +72,14 @@ public class MileageRecordExportExcel implements Serializable {
|
||||
|
||||
@ExcelProperty("创建时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createTime;
|
||||
private Date createTime;
|
||||
|
||||
@ExcelProperty("更新人")
|
||||
private String updateUserName;
|
||||
|
||||
@ExcelProperty("更新时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime updateTime;
|
||||
private Date updateTime;
|
||||
|
||||
@ExcelProperty("备注")
|
||||
private String remark;
|
||||
|
||||
+3
-2
@@ -11,6 +11,7 @@ import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 油电记录导出 Excel
|
||||
@@ -61,14 +62,14 @@ public class OilElectricRecordExportExcel implements Serializable {
|
||||
|
||||
@ExcelProperty("创建时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createTime;
|
||||
private Date createTime;
|
||||
|
||||
@ExcelProperty("更新人")
|
||||
private String updateUserName;
|
||||
|
||||
@ExcelProperty("更新时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime updateTime;
|
||||
private Date updateTime;
|
||||
|
||||
@ExcelProperty("备注")
|
||||
private String remark;
|
||||
|
||||
+2
-3
@@ -16,7 +16,6 @@ import lombok.Data;
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
* 其他费用记录 Excel
|
||||
@@ -40,8 +39,8 @@ public class OtherExpenseRecordExcel implements Serializable {
|
||||
@ExcelProperty("*车牌号/船号")
|
||||
private String vehicleNo;
|
||||
|
||||
@ExcelProperty("*费用日期")
|
||||
private LocalDate expenseDate;
|
||||
@ExcelProperty(value = "*费用日期", converter = org.springblade.common.excel.LenientDateStringConverter.class)
|
||||
private String expenseDate;
|
||||
|
||||
@ExcelProperty("*费用类型")
|
||||
private String expenseType;
|
||||
|
||||
+3
-3
@@ -11,7 +11,7 @@ import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 其他费用记录导出 Excel
|
||||
@@ -43,14 +43,14 @@ public class OtherExpenseRecordExportExcel implements Serializable {
|
||||
|
||||
@ExcelProperty("创建时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createTime;
|
||||
private Date createTime;
|
||||
|
||||
@ExcelProperty("更新人")
|
||||
private String updateUserName;
|
||||
|
||||
@ExcelProperty("更新时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime updateTime;
|
||||
private Date updateTime;
|
||||
|
||||
@ExcelProperty("备注")
|
||||
private String remark;
|
||||
|
||||
+79
-76
@@ -1,76 +1,79 @@
|
||||
/**
|
||||
* 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.excel;
|
||||
|
||||
import cn.idev.excel.annotation.ExcelProperty;
|
||||
import cn.idev.excel.annotation.write.style.ColumnWidth;
|
||||
import cn.idev.excel.annotation.write.style.ContentRowHeight;
|
||||
import cn.idev.excel.annotation.write.style.HeadRowHeight;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 过程配置导出 Excel
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Data
|
||||
@ColumnWidth(22)
|
||||
@HeadRowHeight(20)
|
||||
@ContentRowHeight(18)
|
||||
public class ProcessConfigExportExcel implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@ExcelProperty("配置编号")
|
||||
private String configCode;
|
||||
@ExcelProperty("配置名称")
|
||||
private String configName;
|
||||
@ExcelProperty("项目ID集合")
|
||||
private String projectIds;
|
||||
@ExcelProperty("项目")
|
||||
private String projectNames;
|
||||
@ExcelProperty("包含过程节点")
|
||||
private String includedNodes;
|
||||
@ExcelProperty("默认后台完成运输天数")
|
||||
private Integer defaultFinishDays;
|
||||
@ExcelProperty("状态")
|
||||
private Integer status;
|
||||
@ExcelProperty("所属组织")
|
||||
private String deptName;
|
||||
@ExcelProperty("备注")
|
||||
private String remark;
|
||||
@ExcelProperty("创建人")
|
||||
private String createUserName;
|
||||
@ExcelProperty("更新人")
|
||||
private String updateUserName;
|
||||
@ExcelProperty("更新时间")
|
||||
private Date updateTime;
|
||||
@ExcelProperty("创建时间")
|
||||
private Date createTime;
|
||||
|
||||
}
|
||||
/**
|
||||
* 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.excel;
|
||||
|
||||
import cn.idev.excel.annotation.ExcelProperty;
|
||||
import cn.idev.excel.annotation.format.DateTimeFormat;
|
||||
import cn.idev.excel.annotation.write.style.ColumnWidth;
|
||||
import cn.idev.excel.annotation.write.style.ContentRowHeight;
|
||||
import cn.idev.excel.annotation.write.style.HeadRowHeight;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 过程配置导出 Excel
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Data
|
||||
@ColumnWidth(22)
|
||||
@HeadRowHeight(20)
|
||||
@ContentRowHeight(18)
|
||||
public class ProcessConfigExportExcel implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@ExcelProperty("配置编号")
|
||||
private String configCode;
|
||||
@ExcelProperty("配置名称")
|
||||
private String configName;
|
||||
@ExcelProperty("项目ID集合")
|
||||
private String projectIds;
|
||||
@ExcelProperty("项目")
|
||||
private String projectNames;
|
||||
@ExcelProperty("包含过程节点")
|
||||
private String includedNodes;
|
||||
@ExcelProperty("默认后台完成运输天数")
|
||||
private Integer defaultFinishDays;
|
||||
@ExcelProperty("状态")
|
||||
private Integer status;
|
||||
@ExcelProperty("所属组织")
|
||||
private String deptName;
|
||||
@ExcelProperty("备注")
|
||||
private String remark;
|
||||
@ExcelProperty("创建人")
|
||||
private String createUserName;
|
||||
@ExcelProperty("更新人")
|
||||
private String updateUserName;
|
||||
@ExcelProperty("更新时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private Date updateTime;
|
||||
@ExcelProperty("创建时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private Date createTime;
|
||||
|
||||
}
|
||||
|
||||
+6
-2
@@ -23,6 +23,7 @@
|
||||
package org.springblade.transport.excel;
|
||||
|
||||
import cn.idev.excel.annotation.ExcelProperty;
|
||||
import cn.idev.excel.annotation.format.DateTimeFormat;
|
||||
import cn.idev.excel.annotation.write.style.ColumnWidth;
|
||||
import cn.idev.excel.annotation.write.style.ContentRowHeight;
|
||||
import cn.idev.excel.annotation.write.style.HeadRowHeight;
|
||||
@@ -33,6 +34,7 @@ import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 项目立项 Excel
|
||||
@@ -123,8 +125,10 @@ public class ProjectApplyExcel implements Serializable {
|
||||
@ExcelProperty("更新人")
|
||||
private String updateUserName;
|
||||
@ExcelProperty("创建时间")
|
||||
private LocalDateTime createTime;
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private Date createTime;
|
||||
@ExcelProperty("更新时间")
|
||||
private LocalDateTime updateTime;
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private Date updateTime;
|
||||
|
||||
}
|
||||
|
||||
+6
-3
@@ -23,6 +23,7 @@
|
||||
package org.springblade.transport.excel;
|
||||
|
||||
import cn.idev.excel.annotation.ExcelProperty;
|
||||
import cn.idev.excel.annotation.format.DateTimeFormat;
|
||||
import cn.idev.excel.annotation.write.style.ColumnWidth;
|
||||
import cn.idev.excel.annotation.write.style.ContentRowHeight;
|
||||
import cn.idev.excel.annotation.write.style.HeadRowHeight;
|
||||
@@ -30,7 +31,7 @@ import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 发货模板 Excel
|
||||
@@ -59,8 +60,10 @@ public class ShippingTemplateExcel implements Serializable {
|
||||
@ExcelProperty("备注")
|
||||
private String remark;
|
||||
@ExcelProperty("更新时间")
|
||||
private LocalDateTime updateTime;
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private Date updateTime;
|
||||
@ExcelProperty("创建时间")
|
||||
private LocalDateTime createTime;
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private Date createTime;
|
||||
|
||||
}
|
||||
|
||||
+6
-3
@@ -23,6 +23,7 @@
|
||||
package org.springblade.transport.excel;
|
||||
|
||||
import cn.idev.excel.annotation.ExcelProperty;
|
||||
import cn.idev.excel.annotation.format.DateTimeFormat;
|
||||
import cn.idev.excel.annotation.write.style.ColumnWidth;
|
||||
import cn.idev.excel.annotation.write.style.ContentRowHeight;
|
||||
import cn.idev.excel.annotation.write.style.HeadRowHeight;
|
||||
@@ -32,7 +33,7 @@ import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 临时额度申请 Excel
|
||||
@@ -81,8 +82,10 @@ public class TemporaryCreditLimitExcel implements Serializable {
|
||||
@ExcelProperty("更新人")
|
||||
private String updateUserName;
|
||||
@ExcelProperty("创建时间")
|
||||
private LocalDateTime createTime;
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private Date createTime;
|
||||
@ExcelProperty("更新时间")
|
||||
private LocalDateTime updateTime;
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private Date updateTime;
|
||||
|
||||
}
|
||||
|
||||
+4
@@ -38,6 +38,10 @@ import java.time.format.DateTimeFormatter;
|
||||
|
||||
/**
|
||||
* 换胎日期文本转换器,保留非法日期原值并兼容 Excel 数值日期。
|
||||
* <p>
|
||||
* 数值日期序列号转 ISO 文本,文本原样透传;文本的宽容解析由服务层委托
|
||||
* {@link org.springblade.common.excel.LenientDateParser} 完成(支持 2026-8-2 等写法,
|
||||
* 口径见根工作区 docs/import-spec.md)。
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
|
||||
+3
-3
@@ -37,7 +37,7 @@ import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 换胎记录导出 Excel
|
||||
@@ -76,14 +76,14 @@ public class TireReplacementRecordExportExcel implements Serializable {
|
||||
|
||||
@ExcelProperty("创建时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createTime;
|
||||
private Date createTime;
|
||||
|
||||
@ExcelProperty("更新人")
|
||||
private String updateUserName;
|
||||
|
||||
@ExcelProperty("更新时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime updateTime;
|
||||
private Date updateTime;
|
||||
|
||||
@ExcelProperty("备注")
|
||||
private String remark;
|
||||
|
||||
+3
-3
@@ -28,7 +28,7 @@ import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 变更记录导出 Excel
|
||||
@@ -57,14 +57,14 @@ public class TransportChangeRecordExportExcel implements Serializable {
|
||||
|
||||
@ExcelProperty("创建时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createTime;
|
||||
private Date createTime;
|
||||
|
||||
@ExcelProperty("更新人")
|
||||
private String updateUserName;
|
||||
|
||||
@ExcelProperty("更新时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime updateTime;
|
||||
private Date updateTime;
|
||||
|
||||
@ExcelProperty("备注")
|
||||
private String remark;
|
||||
|
||||
+6
-3
@@ -23,6 +23,7 @@
|
||||
package org.springblade.transport.excel;
|
||||
|
||||
import cn.idev.excel.annotation.ExcelProperty;
|
||||
import cn.idev.excel.annotation.format.DateTimeFormat;
|
||||
import cn.idev.excel.annotation.write.style.ColumnWidth;
|
||||
import cn.idev.excel.annotation.write.style.ContentRowHeight;
|
||||
import cn.idev.excel.annotation.write.style.HeadRowHeight;
|
||||
@@ -31,7 +32,7 @@ import lombok.Data;
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 运输计划 Excel
|
||||
@@ -92,8 +93,10 @@ public class TransportPlanExcel implements Serializable {
|
||||
@ExcelProperty("更新人")
|
||||
private String updateUserName;
|
||||
@ExcelProperty("创建时间")
|
||||
private LocalDateTime createTime;
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private Date createTime;
|
||||
@ExcelProperty("更新时间")
|
||||
private LocalDateTime updateTime;
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private Date updateTime;
|
||||
|
||||
}
|
||||
|
||||
+3
-2
@@ -37,6 +37,7 @@ import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 违章记录 Excel
|
||||
@@ -95,14 +96,14 @@ public class ViolationRecordExcel implements Serializable {
|
||||
|
||||
@ExcelProperty("创建时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createTime;
|
||||
private Date createTime;
|
||||
|
||||
@ExcelProperty("更新人")
|
||||
private String updateUserName;
|
||||
|
||||
@ExcelProperty("更新时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime updateTime;
|
||||
private Date updateTime;
|
||||
|
||||
@ExcelIgnore
|
||||
private String errorMessage;
|
||||
|
||||
+5
-2
@@ -60,8 +60,11 @@ public class ViolationRecordImportExcel implements Serializable {
|
||||
@ExcelProperty("*驾驶人")
|
||||
private String driverName;
|
||||
|
||||
@ExcelProperty("*类型/事项")
|
||||
private String violationTypeOrItem;
|
||||
@ExcelProperty("*类型")
|
||||
private String violationType;
|
||||
|
||||
@ExcelProperty("*事项")
|
||||
private String violationItem;
|
||||
|
||||
@ExcelProperty(value = "*时间", converter = MaintenancePlanDateTimeConverter.class)
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
|
||||
+163
-163
@@ -1,163 +1,163 @@
|
||||
/**
|
||||
* 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.excel;
|
||||
|
||||
import cn.idev.excel.annotation.ExcelProperty;
|
||||
import cn.idev.excel.annotation.ExcelIgnore;
|
||||
import cn.idev.excel.annotation.format.DateTimeFormat;
|
||||
import cn.idev.excel.annotation.write.style.ColumnWidth;
|
||||
import cn.idev.excel.annotation.write.style.ContentRowHeight;
|
||||
import cn.idev.excel.annotation.write.style.HeadRowHeight;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 运单管理 Excel
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Data
|
||||
@ColumnWidth(22)
|
||||
@HeadRowHeight(20)
|
||||
@ContentRowHeight(18)
|
||||
public class WaybillExcel implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@ExcelProperty("运单号")
|
||||
private String waybillNo;
|
||||
@ExcelProperty("项目")
|
||||
private String projectName;
|
||||
@ExcelProperty("客户合同")
|
||||
private String contractName;
|
||||
@ExcelProperty("客户名称")
|
||||
private String customerName;
|
||||
@ExcelProperty("运输类型")
|
||||
private String transportType;
|
||||
@ExcelProperty("货物名称")
|
||||
private String cargoName;
|
||||
@ExcelProperty("货物类型")
|
||||
private String cargoType;
|
||||
@ExcelProperty("规格")
|
||||
private String specification;
|
||||
@ExcelProperty("型号")
|
||||
private String model;
|
||||
@ExcelProperty("数量")
|
||||
private BigDecimal quantity;
|
||||
@ExcelProperty("数量单位")
|
||||
private String quantityUnit;
|
||||
@ExcelProperty("发货地")
|
||||
private String departureName;
|
||||
@ExcelProperty("发货地址")
|
||||
private String departureAddress;
|
||||
@ExcelProperty("发货联系人")
|
||||
private String departureContact;
|
||||
@ExcelProperty("发货联系方式")
|
||||
private String departurePhone;
|
||||
@ExcelProperty("收货地")
|
||||
private String arrivalName;
|
||||
@ExcelProperty("收货地址")
|
||||
private String arrivalAddress;
|
||||
@ExcelProperty("收货联系人")
|
||||
private String arrivalContact;
|
||||
@ExcelProperty("收货联系方式")
|
||||
private String arrivalPhone;
|
||||
@ExcelProperty("任务录入模式")
|
||||
private String taskEntryMode;
|
||||
@ExcelProperty("承运类型")
|
||||
private String carrierType;
|
||||
@ExcelProperty("承运商名称")
|
||||
private String carrierName;
|
||||
@ExcelProperty("司机姓名")
|
||||
private String driverName;
|
||||
@ExcelProperty("司机手机号")
|
||||
private String driverPhone;
|
||||
@ExcelProperty("车/船/航班/班列号")
|
||||
private String vehicleNo;
|
||||
@ExcelProperty("挂车车牌号")
|
||||
private String trailerVehicleNo;
|
||||
@ExcelProperty("押运人")
|
||||
private String escortName;
|
||||
@ExcelProperty("押运人手机号")
|
||||
private String escortPhone;
|
||||
@ExcelProperty("里程(km)")
|
||||
private BigDecimal mileage;
|
||||
@ExcelProperty("预计发货日期")
|
||||
private LocalDate estimatedStartTime;
|
||||
@ExcelProperty("预计完成日期")
|
||||
private LocalDate estimatedEndTime;
|
||||
@ExcelProperty("单价")
|
||||
private BigDecimal unitPrice;
|
||||
@ExcelProperty("计价单位")
|
||||
private String priceUnit;
|
||||
@ExcelProperty("其他费用合计")
|
||||
private BigDecimal otherFeeTotal;
|
||||
@ExcelProperty("任务备注")
|
||||
private String taskRemark;
|
||||
@ExcelProperty("原始单号")
|
||||
private String originalNo;
|
||||
@ExcelProperty("业务状态")
|
||||
private String businessStatus;
|
||||
@ExcelProperty("数据来源")
|
||||
private String dataSource;
|
||||
@ExcelProperty("开始日期")
|
||||
private LocalDate startDate;
|
||||
@ExcelProperty("结束日期")
|
||||
private LocalDate endDate;
|
||||
@ExcelProperty("计划名称")
|
||||
private String planName;
|
||||
@ExcelProperty("多联总单")
|
||||
private String masterNo;
|
||||
@ExcelProperty("配载单号")
|
||||
private String loadingNo;
|
||||
@ExcelProperty("运单批次号")
|
||||
private String batchNo;
|
||||
@ExcelProperty("关联单号")
|
||||
private String relationNo;
|
||||
@ExcelProperty("当前过程节点")
|
||||
private String currentProcessNode;
|
||||
@ExcelProperty("所属组织")
|
||||
private String deptName;
|
||||
@ExcelProperty("备注")
|
||||
private String remark;
|
||||
@ExcelProperty("创建人")
|
||||
private String createUserName;
|
||||
@ExcelProperty("更新人")
|
||||
private String updateUserName;
|
||||
@ExcelProperty("创建时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private Date createTime;
|
||||
@ExcelProperty("更新时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private Date updateTime;
|
||||
|
||||
@ExcelIgnore
|
||||
private String errorMessage;
|
||||
|
||||
}
|
||||
/**
|
||||
* 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.excel;
|
||||
|
||||
import cn.idev.excel.annotation.ExcelProperty;
|
||||
import cn.idev.excel.annotation.ExcelIgnore;
|
||||
import cn.idev.excel.annotation.format.DateTimeFormat;
|
||||
import cn.idev.excel.annotation.write.style.ColumnWidth;
|
||||
import cn.idev.excel.annotation.write.style.ContentRowHeight;
|
||||
import cn.idev.excel.annotation.write.style.HeadRowHeight;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 运单管理 Excel
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Data
|
||||
@ColumnWidth(22)
|
||||
@HeadRowHeight(20)
|
||||
@ContentRowHeight(18)
|
||||
public class WaybillExcel implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@ExcelProperty("运单号")
|
||||
private String waybillNo;
|
||||
@ExcelProperty("项目")
|
||||
private String projectName;
|
||||
@ExcelProperty("客户合同")
|
||||
private String contractName;
|
||||
@ExcelProperty("客户名称")
|
||||
private String customerName;
|
||||
@ExcelProperty("运输类型")
|
||||
private String transportType;
|
||||
@ExcelProperty("货物名称")
|
||||
private String cargoName;
|
||||
@ExcelProperty("货物类型")
|
||||
private String cargoType;
|
||||
@ExcelProperty("规格")
|
||||
private String specification;
|
||||
@ExcelProperty("型号")
|
||||
private String model;
|
||||
@ExcelProperty("数量")
|
||||
private BigDecimal quantity;
|
||||
@ExcelProperty("数量单位")
|
||||
private String quantityUnit;
|
||||
@ExcelProperty("发货地")
|
||||
private String departureName;
|
||||
@ExcelProperty("发货地址")
|
||||
private String departureAddress;
|
||||
@ExcelProperty("发货联系人")
|
||||
private String departureContact;
|
||||
@ExcelProperty("发货联系方式")
|
||||
private String departurePhone;
|
||||
@ExcelProperty("收货地")
|
||||
private String arrivalName;
|
||||
@ExcelProperty("收货地址")
|
||||
private String arrivalAddress;
|
||||
@ExcelProperty("收货联系人")
|
||||
private String arrivalContact;
|
||||
@ExcelProperty("收货联系方式")
|
||||
private String arrivalPhone;
|
||||
@ExcelProperty("任务录入模式")
|
||||
private String taskEntryMode;
|
||||
@ExcelProperty("承运类型")
|
||||
private String carrierType;
|
||||
@ExcelProperty("承运商名称")
|
||||
private String carrierName;
|
||||
@ExcelProperty("司机姓名")
|
||||
private String driverName;
|
||||
@ExcelProperty("司机手机号")
|
||||
private String driverPhone;
|
||||
@ExcelProperty("车/船/航班/班列号")
|
||||
private String vehicleNo;
|
||||
@ExcelProperty("挂车车牌号")
|
||||
private String trailerVehicleNo;
|
||||
@ExcelProperty("押运人")
|
||||
private String escortName;
|
||||
@ExcelProperty("押运人手机号")
|
||||
private String escortPhone;
|
||||
@ExcelProperty("里程(km)")
|
||||
private BigDecimal mileage;
|
||||
@ExcelProperty(value = "预计发货日期", converter = org.springblade.common.excel.LenientDateStringConverter.class)
|
||||
private String estimatedStartTime;
|
||||
@ExcelProperty(value = "预计完成日期", converter = org.springblade.common.excel.LenientDateStringConverter.class)
|
||||
private String estimatedEndTime;
|
||||
@ExcelProperty("单价")
|
||||
private BigDecimal unitPrice;
|
||||
@ExcelProperty("计价单位")
|
||||
private String priceUnit;
|
||||
@ExcelProperty("其他费用合计")
|
||||
private BigDecimal otherFeeTotal;
|
||||
@ExcelProperty("任务备注")
|
||||
private String taskRemark;
|
||||
@ExcelProperty("原始单号")
|
||||
private String originalNo;
|
||||
@ExcelProperty("业务状态")
|
||||
private String businessStatus;
|
||||
@ExcelProperty("数据来源")
|
||||
private String dataSource;
|
||||
@ExcelProperty(value = "开始日期", converter = org.springblade.common.excel.LenientDateStringConverter.class)
|
||||
private String startDate;
|
||||
@ExcelProperty(value = "结束日期", converter = org.springblade.common.excel.LenientDateStringConverter.class)
|
||||
private String endDate;
|
||||
@ExcelProperty("计划名称")
|
||||
private String planName;
|
||||
@ExcelProperty("多联总单")
|
||||
private String masterNo;
|
||||
@ExcelProperty("配载单号")
|
||||
private String loadingNo;
|
||||
@ExcelProperty("运单批次号")
|
||||
private String batchNo;
|
||||
@ExcelProperty("关联单号")
|
||||
private String relationNo;
|
||||
@ExcelProperty("当前过程节点")
|
||||
private String currentProcessNode;
|
||||
@ExcelProperty("所属组织")
|
||||
private String deptName;
|
||||
@ExcelProperty("备注")
|
||||
private String remark;
|
||||
@ExcelProperty("创建人")
|
||||
private String createUserName;
|
||||
@ExcelProperty("更新人")
|
||||
private String updateUserName;
|
||||
@ExcelProperty("创建时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private Date createTime;
|
||||
@ExcelProperty("更新时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private Date updateTime;
|
||||
|
||||
@ExcelIgnore
|
||||
private String errorMessage;
|
||||
|
||||
}
|
||||
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package org.springblade.transport.feign;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springblade.core.secure.annotation.PreAuth;
|
||||
import org.springblade.core.secure.constant.AuthConstant;
|
||||
import org.springblade.core.tenant.annotation.TenantIgnore;
|
||||
import org.springblade.core.tool.api.FR;
|
||||
import org.springblade.transport.pojo.dto.CustomerProcessNodeSyncDTO;
|
||||
import org.springblade.transport.service.ICustomerArchiveService;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* 客商档案 Feign实现
|
||||
*/
|
||||
@Hidden
|
||||
@RestController
|
||||
@AllArgsConstructor
|
||||
public class CustomerArchiveClient implements ICustomerArchiveClient {
|
||||
|
||||
private final ICustomerArchiveService customerArchiveService;
|
||||
|
||||
@TenantIgnore
|
||||
@PreAuth(AuthConstant.PERMIT_ALL)
|
||||
@PostMapping(SYNC_PROCESS_NODE)
|
||||
@Override
|
||||
public FR<Boolean> syncProcessNode(@RequestBody CustomerProcessNodeSyncDTO param) {
|
||||
if (param == null) {
|
||||
return FR.data(false);
|
||||
}
|
||||
if (param.getProcessInfo() != null) {
|
||||
return FR.data(customerArchiveService.syncProcessNodeFromProcessInfo(param.getId(), param.getProcessInfo()));
|
||||
}
|
||||
return FR.data(customerArchiveService.syncProcessNode(
|
||||
param.getId(), param.getCurrentNode(), param.getCurrentProcessor(), param.getApprovalStatus()));
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package org.springblade.transport.feign;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springblade.core.secure.annotation.PreAuth;
|
||||
import org.springblade.core.secure.constant.AuthConstant;
|
||||
import org.springblade.core.tenant.annotation.TenantIgnore;
|
||||
import org.springblade.core.tool.api.FR;
|
||||
import org.springblade.transport.mk.IMkProcessBizHandler;
|
||||
import org.springblade.transport.mk.MkProcessMessageService;
|
||||
import org.springblade.transport.pojo.dto.MkProcessSyncDTO;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* MK 业务流程同步 Feign 实现
|
||||
*/
|
||||
@Hidden
|
||||
@RestController
|
||||
@AllArgsConstructor
|
||||
public class MkProcessClient implements IMkProcessClient {
|
||||
|
||||
private final MkProcessMessageService mkProcessMessageService;
|
||||
|
||||
@TenantIgnore
|
||||
@PreAuth(AuthConstant.PERMIT_ALL)
|
||||
@PostMapping(APPLY)
|
||||
@Override
|
||||
public FR<Boolean> apply(@RequestBody MkProcessSyncDTO param) {
|
||||
if (param == null || param.getId() == null) {
|
||||
return FR.data(false);
|
||||
}
|
||||
IMkProcessBizHandler handler = mkProcessMessageService.handler(param.getBizType());
|
||||
String action = param.getAction() == null ? "sync" : param.getAction();
|
||||
return switch (action) {
|
||||
case "approve" -> FR.data(handler.approveFromProcess(param.getId(), param.getProcessorName()));
|
||||
case "reject" -> FR.data(handler.rejectFromProcess(param.getId(), param.getProcessorName()));
|
||||
default -> FR.data(handler.syncFromProcessInfo(param.getId(), param.getProcessInfo()));
|
||||
};
|
||||
}
|
||||
}
|
||||
+15
@@ -15,6 +15,7 @@
|
||||
<result column="customer_code" property="customerCode"/>
|
||||
<result column="short_name" property="shortName"/>
|
||||
<result column="full_name" property="fullName"/>
|
||||
<result column="customer_kind" property="customerKind"/>
|
||||
<result column="customer_nature" property="customerNature"/>
|
||||
<result column="guangxi_top100" property="guangxiTop100"/>
|
||||
<result column="unified_credit_code" property="unifiedCreditCode"/>
|
||||
@@ -46,6 +47,7 @@
|
||||
<result column="current_node" property="currentNode"/>
|
||||
<result column="current_processor" property="currentProcessor"/>
|
||||
<result column="approved_time" property="approvedTime"/>
|
||||
<result column="review_status" property="reviewStatus"/>
|
||||
<result column="kingdee_status" property="kingdeeStatus"/>
|
||||
<result column="kingdee_fail_reason" property="kingdeeFailReason"/>
|
||||
<result column="kingdee_sync_time" property="kingdeeSyncTime"/>
|
||||
@@ -65,6 +67,7 @@
|
||||
customer_code,
|
||||
short_name,
|
||||
full_name,
|
||||
customer_kind,
|
||||
customer_nature,
|
||||
guangxi_top100,
|
||||
unified_credit_code,
|
||||
@@ -99,6 +102,15 @@
|
||||
kingdee_status,
|
||||
kingdee_fail_reason,
|
||||
kingdee_sync_time
|
||||
approved_time,
|
||||
(
|
||||
SELECT s.review_status
|
||||
FROM blade_customer_credit_score s
|
||||
WHERE s.customer_id = blade_customer_archive.id
|
||||
AND (s.is_deleted = 0 OR s.is_deleted IS NULL)
|
||||
ORDER BY s.score_date DESC, s.update_time DESC, s.id DESC
|
||||
LIMIT 1
|
||||
) AS review_status
|
||||
FROM blade_customer_archive
|
||||
WHERE is_deleted = 0
|
||||
<if test="userId != null">
|
||||
@@ -125,6 +137,9 @@
|
||||
<bind name="creditCodeLike" value="'%' + customer.unifiedCreditCode + '%'"/>
|
||||
AND unified_credit_code LIKE #{creditCodeLike}
|
||||
</if>
|
||||
<if test="customer.customerKind != null and customer.customerKind != ''">
|
||||
AND customer_kind = #{customer.customerKind}
|
||||
</if>
|
||||
<if test="customer.customerNature != null and customer.customerNature != ''">
|
||||
AND customer_nature = #{customer.customerNature}
|
||||
</if>
|
||||
|
||||
+28
@@ -22,9 +22,15 @@
|
||||
*/
|
||||
package org.springblade.transport.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.springblade.transport.pojo.entity.ProjectApply;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 项目立项 Mapper 接口
|
||||
*
|
||||
@@ -32,4 +38,26 @@ import org.springblade.transport.pojo.entity.ProjectApply;
|
||||
*/
|
||||
public interface ProjectApplyMapper extends BaseMapper<ProjectApply> {
|
||||
|
||||
/**
|
||||
* 按租户读取业务字典项,供未登录公开页回显下拉文案
|
||||
*/
|
||||
@InterceptorIgnore(tenantLine = "true")
|
||||
@Select("""
|
||||
SELECT dict_key AS dictKey, dict_value AS dictValue
|
||||
FROM blade_dict_biz
|
||||
WHERE is_deleted = 0 AND parent_id <> 0 AND tenant_id = #{tenantId} AND code = #{code}
|
||||
""")
|
||||
List<Map<String, Object>> selectDictBizItems(@Param("tenantId") String tenantId, @Param("code") String code);
|
||||
|
||||
/**
|
||||
* 读取系统字典项,供未登录公开页回显下拉文案
|
||||
*/
|
||||
@InterceptorIgnore(tenantLine = "true")
|
||||
@Select("""
|
||||
SELECT dict_key AS dictKey, dict_value AS dictValue
|
||||
FROM blade_dict
|
||||
WHERE is_deleted = 0 AND parent_id <> 0 AND code = #{code}
|
||||
""")
|
||||
List<Map<String, Object>> selectDictItems(@Param("code") String code);
|
||||
|
||||
}
|
||||
|
||||
+2
-2
@@ -28,8 +28,8 @@
|
||||
d.is_deleted
|
||||
FROM blade_transport_vehicle_dispatch d
|
||||
LEFT JOIN blade_transport_vehicle v
|
||||
ON v.plate_no = d.plate_no
|
||||
AND v.tenant_id = d.tenant_id
|
||||
ON v.plate_no COLLATE utf8mb4_general_ci = d.plate_no
|
||||
AND v.tenant_id COLLATE utf8mb4_general_ci = d.tenant_id
|
||||
AND v.is_deleted = 0
|
||||
WHERE d.is_deleted = 0
|
||||
<if test="dispatch.applicationNo != null and dispatch.applicationNo != ''">
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package org.springblade.transport.mk;
|
||||
|
||||
/**
|
||||
* MK 业务流程处理器
|
||||
*/
|
||||
public interface IMkProcessBizHandler {
|
||||
|
||||
String bizType();
|
||||
|
||||
Object publicDetail(Long id);
|
||||
|
||||
boolean syncFromProcessInfo(Long id, Object processInfo);
|
||||
|
||||
boolean approveFromProcess(Long id, String processorName);
|
||||
|
||||
boolean rejectFromProcess(Long id, String processorName);
|
||||
}
|
||||
+439
@@ -0,0 +1,439 @@
|
||||
package org.springblade.transport.mk;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springblade.core.tenant.annotation.TenantIgnore;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.transport.pojo.entity.ContractManage;
|
||||
import org.springblade.transport.pojo.entity.FormalSettlement;
|
||||
import org.springblade.transport.pojo.entity.PaymentApplication;
|
||||
import org.springblade.transport.pojo.entity.PreSettlement;
|
||||
import org.springblade.transport.pojo.entity.ProjectApply;
|
||||
import org.springblade.transport.pojo.entity.Waybill;
|
||||
import org.springblade.transport.service.IContractManageService;
|
||||
import org.springblade.transport.service.ICustomerArchiveService;
|
||||
import org.springblade.transport.service.IFormalSettlementService;
|
||||
import org.springblade.transport.service.IPaymentApplicationService;
|
||||
import org.springblade.transport.service.IPreSettlementService;
|
||||
import org.springblade.transport.service.IProjectApplyService;
|
||||
import org.springblade.transport.service.IWaybillService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 各业务 MK 流程处理器
|
||||
*/
|
||||
public final class MkProcessBizHandlers {
|
||||
|
||||
private MkProcessBizHandlers() {
|
||||
}
|
||||
|
||||
@Component
|
||||
@TenantIgnore
|
||||
@RequiredArgsConstructor
|
||||
public static class CustomerHandler implements IMkProcessBizHandler {
|
||||
private final ICustomerArchiveService service;
|
||||
|
||||
@Override
|
||||
public String bizType() {
|
||||
return "customer-archive";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object publicDetail(Long id) {
|
||||
return service.publicDetail(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean syncFromProcessInfo(Long id, Object processInfo) {
|
||||
return service.syncProcessNodeFromProcessInfo(id, processInfo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean approveFromProcess(Long id, String processorName) {
|
||||
return service.approveFromProcess(id, processorName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean rejectFromProcess(Long id, String processorName) {
|
||||
return service.rejectFromProcess(id, processorName);
|
||||
}
|
||||
}
|
||||
|
||||
@Component
|
||||
@TenantIgnore
|
||||
@RequiredArgsConstructor
|
||||
public static class ProjectHandler implements IMkProcessBizHandler {
|
||||
private final IProjectApplyService service;
|
||||
|
||||
@Override
|
||||
public String bizType() {
|
||||
return "project-apply";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object publicDetail(Long id) {
|
||||
return service.publicDetail(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean syncFromProcessInfo(Long id, Object processInfo) {
|
||||
ProjectApply entity = service.getById(id);
|
||||
if (entity == null) {
|
||||
return false;
|
||||
}
|
||||
String node = MkProcessNodeHelper.currentNode(processInfo);
|
||||
String processor = MkProcessNodeHelper.currentProcessor(processInfo);
|
||||
if (Func.isEmpty(node) && Func.isEmpty(processor)) {
|
||||
return false;
|
||||
}
|
||||
if (Func.isNotEmpty(node)) {
|
||||
entity.setCurrentNode(node);
|
||||
}
|
||||
if (Func.isNotEmpty(processor)) {
|
||||
entity.setCurrentProcessor(processor);
|
||||
}
|
||||
if (!"change_reviewing".equals(entity.getApprovalStatus())) {
|
||||
entity.setApprovalStatus("reviewing");
|
||||
}
|
||||
return service.updateById(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean approveFromProcess(Long id, String processorName) {
|
||||
ProjectApply entity = service.getById(id);
|
||||
if (entity == null || "approved".equals(entity.getApprovalStatus())
|
||||
|| "change_approved".equals(entity.getApprovalStatus())) {
|
||||
return entity != null;
|
||||
}
|
||||
boolean change = "change_reviewing".equals(entity.getApprovalStatus());
|
||||
entity.setApprovalStatus(change ? "change_approved" : "approved");
|
||||
entity.setEffectiveType("formal");
|
||||
entity.setCurrentNode("审核通过");
|
||||
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
|
||||
entity.setApprovedTime(LocalDateTime.now());
|
||||
return service.updateById(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean rejectFromProcess(Long id, String processorName) {
|
||||
ProjectApply entity = service.getById(id);
|
||||
if (entity == null || "rejected".equals(entity.getApprovalStatus())
|
||||
|| "change_rejected".equals(entity.getApprovalStatus())) {
|
||||
return entity != null;
|
||||
}
|
||||
boolean change = "change_reviewing".equals(entity.getApprovalStatus());
|
||||
entity.setApprovalStatus(change ? "change_rejected" : "rejected");
|
||||
entity.setCurrentNode("审核不通过");
|
||||
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
|
||||
return service.updateById(entity);
|
||||
}
|
||||
}
|
||||
|
||||
@Component
|
||||
@TenantIgnore
|
||||
@RequiredArgsConstructor
|
||||
public static class ContractHandler implements IMkProcessBizHandler {
|
||||
private final IContractManageService service;
|
||||
|
||||
@Override
|
||||
public String bizType() {
|
||||
return "contract-manage";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object publicDetail(Long id) {
|
||||
return service.publicDetail(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean syncFromProcessInfo(Long id, Object processInfo) {
|
||||
ContractManage entity = service.getById(id);
|
||||
if (entity == null) {
|
||||
return false;
|
||||
}
|
||||
String node = MkProcessNodeHelper.currentNode(processInfo);
|
||||
String processor = MkProcessNodeHelper.currentProcessor(processInfo);
|
||||
if (Func.isEmpty(node) && Func.isEmpty(processor)) {
|
||||
return false;
|
||||
}
|
||||
if (Func.isNotEmpty(node)) {
|
||||
entity.setCurrentNode(node);
|
||||
}
|
||||
if (Func.isNotEmpty(processor)) {
|
||||
entity.setCurrentProcessor(processor);
|
||||
}
|
||||
if (!"change_reviewing".equals(entity.getApprovalStatus())) {
|
||||
entity.setApprovalStatus("reviewing");
|
||||
}
|
||||
return service.updateById(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean approveFromProcess(Long id, String processorName) {
|
||||
ContractManage entity = service.getById(id);
|
||||
if (entity == null || "approved".equals(entity.getApprovalStatus())
|
||||
|| "change_approved".equals(entity.getApprovalStatus())) {
|
||||
return entity != null;
|
||||
}
|
||||
boolean change = "change_reviewing".equals(entity.getApprovalStatus());
|
||||
entity.setApprovalStatus(change ? "change_approved" : "approved");
|
||||
entity.setCurrentNode("审核通过");
|
||||
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
|
||||
return service.updateById(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean rejectFromProcess(Long id, String processorName) {
|
||||
ContractManage entity = service.getById(id);
|
||||
if (entity == null || "rejected".equals(entity.getApprovalStatus())
|
||||
|| "change_rejected".equals(entity.getApprovalStatus())) {
|
||||
return entity != null;
|
||||
}
|
||||
boolean change = "change_reviewing".equals(entity.getApprovalStatus());
|
||||
entity.setApprovalStatus(change ? "change_rejected" : "rejected");
|
||||
entity.setCurrentNode("审核不通过");
|
||||
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
|
||||
return service.updateById(entity);
|
||||
}
|
||||
}
|
||||
|
||||
@Component
|
||||
@TenantIgnore
|
||||
@RequiredArgsConstructor
|
||||
public static class WaybillHandler implements IMkProcessBizHandler {
|
||||
private final IWaybillService service;
|
||||
|
||||
@Override
|
||||
public String bizType() {
|
||||
return "waybill-manage";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object publicDetail(Long id) {
|
||||
return service.publicDetail(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean syncFromProcessInfo(Long id, Object processInfo) {
|
||||
Waybill entity = service.getById(id);
|
||||
if (entity == null) {
|
||||
return false;
|
||||
}
|
||||
String node = MkProcessNodeHelper.currentNode(processInfo);
|
||||
if (Func.isEmpty(node)) {
|
||||
return false;
|
||||
}
|
||||
entity.setCurrentProcessNode(node);
|
||||
return service.updateById(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean approveFromProcess(Long id, String processorName) {
|
||||
Waybill entity = service.getById(id);
|
||||
if (entity == null) {
|
||||
return false;
|
||||
}
|
||||
entity.setCurrentProcessNode("审核通过");
|
||||
return service.updateById(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean rejectFromProcess(Long id, String processorName) {
|
||||
Waybill entity = service.getById(id);
|
||||
if (entity == null) {
|
||||
return false;
|
||||
}
|
||||
entity.setCurrentProcessNode("审核不通过");
|
||||
return service.updateById(entity);
|
||||
}
|
||||
}
|
||||
|
||||
@Component
|
||||
@TenantIgnore
|
||||
@RequiredArgsConstructor
|
||||
public static class PreSettlementHandler implements IMkProcessBizHandler {
|
||||
private final IPreSettlementService service;
|
||||
|
||||
@Override
|
||||
public String bizType() {
|
||||
return "pre-settlement";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object publicDetail(Long id) {
|
||||
return service.detail(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean syncFromProcessInfo(Long id, Object processInfo) {
|
||||
PreSettlement entity = service.getById(id);
|
||||
if (entity == null) {
|
||||
return false;
|
||||
}
|
||||
String node = MkProcessNodeHelper.currentNode(processInfo);
|
||||
String processor = MkProcessNodeHelper.currentProcessor(processInfo);
|
||||
if (Func.isEmpty(node) && Func.isEmpty(processor)) {
|
||||
return false;
|
||||
}
|
||||
if (Func.isNotEmpty(node)) {
|
||||
entity.setCurrentNode(node);
|
||||
}
|
||||
if (Func.isNotEmpty(processor)) {
|
||||
entity.setCurrentProcessor(processor);
|
||||
}
|
||||
entity.setApprovalStatus("reviewing");
|
||||
return service.updateById(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean approveFromProcess(Long id, String processorName) {
|
||||
PreSettlement entity = service.getById(id);
|
||||
if (entity == null || "approved".equals(entity.getApprovalStatus())) {
|
||||
return entity != null;
|
||||
}
|
||||
entity.setApprovalStatus("approved");
|
||||
entity.setCurrentNode("审核通过");
|
||||
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
|
||||
entity.setApprovedTime(LocalDateTime.now());
|
||||
return service.updateById(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean rejectFromProcess(Long id, String processorName) {
|
||||
PreSettlement entity = service.getById(id);
|
||||
if (entity == null || "returned".equals(entity.getApprovalStatus())) {
|
||||
return entity != null;
|
||||
}
|
||||
entity.setApprovalStatus("returned");
|
||||
entity.setCurrentNode("审核不通过");
|
||||
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
|
||||
return service.updateById(entity);
|
||||
}
|
||||
}
|
||||
|
||||
@Component
|
||||
@TenantIgnore
|
||||
@RequiredArgsConstructor
|
||||
public static class FormalSettlementHandler implements IMkProcessBizHandler {
|
||||
private final IFormalSettlementService service;
|
||||
|
||||
@Override
|
||||
public String bizType() {
|
||||
return "formal-settlement";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object publicDetail(Long id) {
|
||||
return service.detail(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean syncFromProcessInfo(Long id, Object processInfo) {
|
||||
FormalSettlement entity = service.getById(id);
|
||||
if (entity == null) {
|
||||
return false;
|
||||
}
|
||||
String node = MkProcessNodeHelper.currentNode(processInfo);
|
||||
String processor = MkProcessNodeHelper.currentProcessor(processInfo);
|
||||
if (Func.isEmpty(node) && Func.isEmpty(processor)) {
|
||||
return false;
|
||||
}
|
||||
if (Func.isNotEmpty(node)) {
|
||||
entity.setCurrentNode(node);
|
||||
}
|
||||
if (Func.isNotEmpty(processor)) {
|
||||
entity.setCurrentProcessor(processor);
|
||||
}
|
||||
entity.setApprovalStatus("reviewing");
|
||||
return service.updateById(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean approveFromProcess(Long id, String processorName) {
|
||||
FormalSettlement entity = service.getById(id);
|
||||
if (entity == null || "approved".equals(entity.getApprovalStatus())) {
|
||||
return entity != null;
|
||||
}
|
||||
entity.setApprovalStatus("approved");
|
||||
entity.setCurrentNode("审核通过");
|
||||
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
|
||||
entity.setApprovedTime(LocalDateTime.now());
|
||||
return service.updateById(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean rejectFromProcess(Long id, String processorName) {
|
||||
FormalSettlement entity = service.getById(id);
|
||||
if (entity == null || "returned".equals(entity.getApprovalStatus())) {
|
||||
return entity != null;
|
||||
}
|
||||
entity.setApprovalStatus("returned");
|
||||
entity.setCurrentNode("审核不通过");
|
||||
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
|
||||
return service.updateById(entity);
|
||||
}
|
||||
}
|
||||
|
||||
@Component
|
||||
@TenantIgnore
|
||||
@RequiredArgsConstructor
|
||||
public static class PaymentHandler implements IMkProcessBizHandler {
|
||||
private final IPaymentApplicationService service;
|
||||
|
||||
@Override
|
||||
public String bizType() {
|
||||
return "payment-application";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object publicDetail(Long id) {
|
||||
return service.detail(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean syncFromProcessInfo(Long id, Object processInfo) {
|
||||
PaymentApplication entity = service.getById(id);
|
||||
if (entity == null) {
|
||||
return false;
|
||||
}
|
||||
String node = MkProcessNodeHelper.currentNode(processInfo);
|
||||
String processor = MkProcessNodeHelper.currentProcessor(processInfo);
|
||||
if (Func.isEmpty(node) && Func.isEmpty(processor)) {
|
||||
return false;
|
||||
}
|
||||
if (Func.isNotEmpty(node)) {
|
||||
entity.setCurrentNode(node);
|
||||
}
|
||||
if (Func.isNotEmpty(processor)) {
|
||||
entity.setCurrentProcessor(processor);
|
||||
}
|
||||
entity.setApprovalStatus("reviewing");
|
||||
return service.updateById(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean approveFromProcess(Long id, String processorName) {
|
||||
PaymentApplication entity = service.getById(id);
|
||||
if (entity == null || "approved".equals(entity.getApprovalStatus())) {
|
||||
return entity != null;
|
||||
}
|
||||
entity.setApprovalStatus("approved");
|
||||
entity.setCurrentNode("审核通过");
|
||||
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
|
||||
return service.updateById(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean rejectFromProcess(Long id, String processorName) {
|
||||
PaymentApplication entity = service.getById(id);
|
||||
if (entity == null || "returned".equals(entity.getApprovalStatus())) {
|
||||
return entity != null;
|
||||
}
|
||||
entity.setApprovalStatus("returned");
|
||||
entity.setCurrentNode("审核不通过");
|
||||
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
|
||||
return service.updateById(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
package org.springblade.transport.mk;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.core.tenant.annotation.TenantIgnore;
|
||||
import org.springblade.core.tool.api.FR;
|
||||
import org.springblade.core.tool.utils.StringUtil;
|
||||
import org.springblade.process.feign.IBusinessProcessClient;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 公开页流程消息:当前节点立即查询,流程实例详情延迟 5 秒查询后同步业务状态。
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@TenantIgnore
|
||||
@RequiredArgsConstructor
|
||||
public class MkProcessMessageService {
|
||||
|
||||
private static final long PROCESS_INFO_DELAY_SECONDS = 5L;
|
||||
|
||||
private final IBusinessProcessClient businessProcessClient;
|
||||
private final List<IMkProcessBizHandler> handlers;
|
||||
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2, runnable -> {
|
||||
Thread thread = new Thread(runnable, "mk-process-info-" + THREAD_INDEX.incrementAndGet());
|
||||
thread.setDaemon(true);
|
||||
return thread;
|
||||
});
|
||||
|
||||
private static final AtomicInteger THREAD_INDEX = new AtomicInteger();
|
||||
|
||||
@PreDestroy
|
||||
public void shutdown() {
|
||||
scheduler.shutdown();
|
||||
}
|
||||
|
||||
public IMkProcessBizHandler handler(String bizType) {
|
||||
Map<String, IMkProcessBizHandler> mapping = handlers.stream()
|
||||
.collect(Collectors.toMap(IMkProcessBizHandler::bizType, Function.identity(), (a, b) -> a));
|
||||
IMkProcessBizHandler handler = mapping.get(bizType);
|
||||
if (handler == null) {
|
||||
throw new IllegalArgumentException("不支持的MK业务类型:" + bizType);
|
||||
}
|
||||
return handler;
|
||||
}
|
||||
|
||||
public void handleProcessMessage(String bizType, Map<String, Object> body) {
|
||||
IMkProcessBizHandler handler = handler(bizType);
|
||||
Map<String, Object> formValues = asMap(body == null ? null : body.get("formValues"));
|
||||
String processId = firstText(formValues, "processId");
|
||||
if (StringUtil.isBlank(processId) && body != null) {
|
||||
processId = firstText(body, "processId");
|
||||
}
|
||||
String loginName = firstText(formValues, "mkLoginName", "loginName");
|
||||
if (StringUtil.isBlank(processId)) {
|
||||
log.warn("公开页流程消息未找到 processId,跳过查询当前节点 bizType={}", bizType);
|
||||
return;
|
||||
}
|
||||
String bizIdText = firstText(asMap(body == null ? null : body.get("formData")), "id", "formInstanceId");
|
||||
queryCurrentNodes(bizType, processId, loginName);
|
||||
scheduleProcessInfo(handler, bizType, processId, loginName, bizIdText, formValues);
|
||||
}
|
||||
|
||||
private void queryCurrentNodes(String bizType, String processId, String loginName) {
|
||||
try {
|
||||
FR<Object> result = businessProcessClient.getCurrentNodes(processId, loginName);
|
||||
Object nodeData = result == null ? null : result.getData();
|
||||
log.info("公开页流程消息当前节点详情 bizType={} processId={} loginName={} result={}",
|
||||
bizType, processId, loginName, JSON.toJSONString(nodeData));
|
||||
} catch (Exception e) {
|
||||
log.error("公开页查询当前节点失败 bizType={} processId={} loginName={}", bizType, processId, loginName, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void scheduleProcessInfo(IMkProcessBizHandler handler, String bizType, String processId,
|
||||
String loginName, String bizIdText, Map<String, Object> formValues) {
|
||||
Map<String, Object> formValuesCopy = new HashMap<>(formValues == null ? Map.of() : formValues);
|
||||
log.info("公开页将在{}秒后查询流程实例详情 bizType={} processId={}", PROCESS_INFO_DELAY_SECONDS, bizType, processId);
|
||||
scheduler.schedule(
|
||||
() -> queryProcessInfo(handler, bizType, processId, loginName, bizIdText, formValuesCopy),
|
||||
PROCESS_INFO_DELAY_SECONDS,
|
||||
TimeUnit.SECONDS
|
||||
);
|
||||
}
|
||||
|
||||
private void queryProcessInfo(IMkProcessBizHandler handler, String bizType, String processId,
|
||||
String loginName, String bizIdText, Map<String, Object> formValues) {
|
||||
try {
|
||||
FR<Object> processInfoResult = businessProcessClient.getProcessInfo(processId, loginName);
|
||||
Object processInfo = processInfoResult == null ? null : processInfoResult.getData();
|
||||
log.info("公开页流程消息流程实例详情 bizType={} processId={} loginName={} result={}",
|
||||
bizType, processId, loginName, JSON.toJSONString(processInfo));
|
||||
if (isProcessFinished(processInfo)) {
|
||||
applyProcessResult(handler, bizType, bizIdText, processId, formValues, true);
|
||||
} else if (isProcessRejected(processInfo)) {
|
||||
applyProcessResult(handler, bizType, bizIdText, processId, formValues, false);
|
||||
} else if (StringUtil.isNotBlank(bizIdText)) {
|
||||
try {
|
||||
handler.syncFromProcessInfo(Long.valueOf(bizIdText), processInfo);
|
||||
} catch (NumberFormatException e) {
|
||||
log.warn("公开页流程消息业务id格式不正确 bizType={} id={}", bizType, bizIdText);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("公开页查询流程实例详情失败 bizType={} processId={} loginName={}", bizType, processId, loginName, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isProcessFinished(Object processInfo) {
|
||||
return "30".equals(firstText(asMap(processInfo), "fdProcessStatus"));
|
||||
}
|
||||
|
||||
private static boolean isProcessRejected(Object processInfo) {
|
||||
Map<String, Object> info = asMap(processInfo);
|
||||
if (!"20".equals(firstText(info, "fdProcessStatus"))) {
|
||||
return false;
|
||||
}
|
||||
if (hasItems(info.get("currentHandlers"))) {
|
||||
return false;
|
||||
}
|
||||
return !hasItems(asMap(info.get("fdTaskInfo")).get("handlerInfos"));
|
||||
}
|
||||
|
||||
private static boolean hasItems(Object value) {
|
||||
return value instanceof Collection<?> collection && !collection.isEmpty();
|
||||
}
|
||||
|
||||
private void applyProcessResult(IMkProcessBizHandler handler, String bizType, String bizIdText,
|
||||
String processId, Map<String, Object> formValues, boolean approved) {
|
||||
String action = approved ? "审核通过" : "审核驳回";
|
||||
if (StringUtil.isBlank(bizIdText)) {
|
||||
log.warn("流程{}但未找到业务id,跳过同步 bizType={} processId={}", action, bizType, processId);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
String processorName = firstText(formValues, "mkUserName", "mkLoginName");
|
||||
Long bizId = Long.valueOf(bizIdText);
|
||||
if (approved) {
|
||||
handler.approveFromProcess(bizId, processorName);
|
||||
} else {
|
||||
handler.rejectFromProcess(bizId, processorName);
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
log.warn("流程{}但业务id格式不正确 bizType={} id={}", action, bizType, bizIdText);
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<String, Object> asMap(Object value) {
|
||||
if (!(value instanceof Map<?, ?> map)) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
map.forEach((key, nested) -> {
|
||||
if (key != null) {
|
||||
result.put(String.valueOf(key), nested);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
private static String firstText(Map<String, Object> source, String... keys) {
|
||||
if (source == null || keys == null) {
|
||||
return null;
|
||||
}
|
||||
for (String key : keys) {
|
||||
Object value = source.get(key);
|
||||
if (value == null) {
|
||||
continue;
|
||||
}
|
||||
String text = String.valueOf(value).trim();
|
||||
if (StringUtil.isNotBlank(text) && !"null".equalsIgnoreCase(text)) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
package org.springblade.transport.mk;
|
||||
|
||||
import org.springblade.core.tool.jackson.JsonUtil;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 解析 MK 流程实例详情中的当前节点、当前处理人
|
||||
*/
|
||||
public final class MkProcessNodeHelper {
|
||||
|
||||
private MkProcessNodeHelper() {
|
||||
}
|
||||
|
||||
public static String currentProcessor(Object processInfo) {
|
||||
return joinDistinct(extractTexts(asMap(processInfo).get("currentHandlers"), "fdName", "name"), "、");
|
||||
}
|
||||
|
||||
public static String currentNode(Object processInfo) {
|
||||
Map<String, Object> taskInfo = asMap(asMap(processInfo).get("fdTaskInfo"));
|
||||
return joinDistinct(extractTexts(taskInfo.get("handlerInfos"), "nodeName"), "、");
|
||||
}
|
||||
|
||||
public static String text(String value, String fallback) {
|
||||
return Func.isEmpty(value) ? fallback : value.trim();
|
||||
}
|
||||
|
||||
public static Map<String, Object> asMap(Object value) {
|
||||
if (value instanceof Map<?, ?> map) {
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
map.forEach((key, nested) -> {
|
||||
if (key != null) {
|
||||
result.put(String.valueOf(key), nested);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
if (value == null) {
|
||||
return Map.of();
|
||||
}
|
||||
try {
|
||||
Map<String, Object> parsed = JsonUtil.toMap(JsonUtil.toJson(value));
|
||||
return parsed == null ? Map.of() : parsed;
|
||||
} catch (Exception ignored) {
|
||||
return Map.of();
|
||||
}
|
||||
}
|
||||
|
||||
private static List<String> extractTexts(Object listObj, String... keys) {
|
||||
List<String> result = new ArrayList<>();
|
||||
if (!(listObj instanceof Collection<?> collection) || keys == null) {
|
||||
return result;
|
||||
}
|
||||
for (Object item : collection) {
|
||||
Map<String, Object> map = asMap(item);
|
||||
for (String key : keys) {
|
||||
String text = Func.toStr(map.get(key), "").trim();
|
||||
if (Func.isNotEmpty(text)) {
|
||||
result.add(text);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static String joinDistinct(List<String> values, String delimiter) {
|
||||
return values.stream().filter(Func::isNotEmpty).distinct().collect(Collectors.joining(delimiter));
|
||||
}
|
||||
}
|
||||
+8
@@ -40,6 +40,14 @@ public interface IContractManageService extends BaseService<ContractManage> {
|
||||
|
||||
IPage<ContractManageVO> selectContractManagePage(IPage<ContractManage> page, ContractManageVO contractManage);
|
||||
ContractManageVO detail(Long id);
|
||||
|
||||
/**
|
||||
* 公开详情,不校验当前用户所属组织
|
||||
*
|
||||
* @param id 主键
|
||||
* @return 合同详情
|
||||
*/
|
||||
ContractManageVO publicDetail(Long id);
|
||||
boolean saveDraft(ContractManage contractManage);
|
||||
boolean submit(ContractManage contractManage);
|
||||
boolean toTemporary(Long id);
|
||||
|
||||
+64
@@ -66,6 +66,23 @@ public interface ICustomerArchiveService extends BaseService<CustomerArchive> {
|
||||
*/
|
||||
CustomerArchiveVO detail(Long id);
|
||||
|
||||
/**
|
||||
* 公开查看详情(不校验登录态与数据权限)
|
||||
*
|
||||
* @param id 主键
|
||||
* @return 客商档案详情
|
||||
*/
|
||||
CustomerArchiveVO publicDetail(Long id);
|
||||
|
||||
/**
|
||||
* 公开查看变更记录分页(不校验登录态与数据权限)
|
||||
*
|
||||
* @param page 分页参数
|
||||
* @param customerId 客商ID
|
||||
* @return 变更记录分页
|
||||
*/
|
||||
IPage<CustomerChangeRecordVO> publicChangeRecordPage(IPage<CustomerChangeRecordVO> page, Long customerId);
|
||||
|
||||
/**
|
||||
* 新增或修改客商档案
|
||||
* <p>仅当 {@code customer.recordChange = true}(前端点「提交」)时写入变更记录;「保存」不落变更记录。</p>
|
||||
@@ -83,6 +100,53 @@ public interface ICustomerArchiveService extends BaseService<CustomerArchive> {
|
||||
*/
|
||||
boolean submitApproval(Long id);
|
||||
|
||||
/**
|
||||
* 按 MK 当前节点同步客商当前节点、当前处理人和审批状态
|
||||
*
|
||||
* @param id 客商ID
|
||||
* @param currentNode 当前节点
|
||||
* @param currentProcessor 当前处理人
|
||||
* @param approvalStatus 审批状态,可为空,为空时保持为审核中
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean syncProcessNode(Long id, String currentNode, String currentProcessor, String approvalStatus);
|
||||
|
||||
/**
|
||||
* 按 MK getCurrentNodes 返回结果同步客商当前节点
|
||||
*
|
||||
* @param id 客商ID
|
||||
* @param currentNodes MK 当前节点详情
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean syncProcessNodeFromMk(Long id, Object currentNodes);
|
||||
|
||||
/**
|
||||
* 按 MK 流程实例详情回写当前节点、当前处理人
|
||||
*
|
||||
* @param id 客商ID
|
||||
* @param processInfo MK getProcessInfo 返回数据
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean syncProcessNodeFromProcessInfo(Long id, Object processInfo);
|
||||
|
||||
/**
|
||||
* MK 流程结束(fdProcessStatus=30)时将客商置为审核通过
|
||||
*
|
||||
* @param id 客商ID
|
||||
* @param processorName 当前处理人,可为空
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean approveFromProcess(Long id, String processorName);
|
||||
|
||||
/**
|
||||
* MK 流程状态为 20 时将客商置为审核驳回
|
||||
*
|
||||
* @param id 客商ID
|
||||
* @param processorName 当前处理人,可为空
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean rejectFromProcess(Long id, String processorName);
|
||||
|
||||
/**
|
||||
* 撤回审批并恢复草稿状态
|
||||
*
|
||||
|
||||
+6
@@ -68,6 +68,12 @@ public interface IDriverWaybillService {
|
||||
*/
|
||||
DriverWaybillCardVO detail(Long id);
|
||||
|
||||
/**
|
||||
* 按运单ID组装详情打卡数据(punchNodes / enrouteRecords),不校验当前登录人是否为该司机。
|
||||
* 供调度端 manage/detail 复用。
|
||||
*/
|
||||
DriverWaybillCardVO detailPunchSnapshot(Long id);
|
||||
|
||||
/**
|
||||
* 司机确认接单:过程配置要求接单且尚未接单时,写入接单记录并将运单改为进行中。
|
||||
*/
|
||||
|
||||
+2
@@ -50,6 +50,8 @@ public interface ILoadingManageService extends BaseService<LoadingManage> {
|
||||
|
||||
boolean changeRoute(LoadingManage loadingManage);
|
||||
|
||||
boolean start(Long id);
|
||||
|
||||
boolean cancel(Long id);
|
||||
|
||||
boolean complete(Long id);
|
||||
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import org.springblade.transport.pojo.vo.AdminDriverOptionVO;
|
||||
import org.springblade.transport.pojo.vo.AdminHomeStatsVO;
|
||||
import org.springblade.transport.pojo.vo.AdminHomeVO;
|
||||
import org.springblade.transport.pojo.vo.AdminVehicleOptionVO;
|
||||
import org.springblade.transport.pojo.vo.AdminWaybillCardVO;
|
||||
import org.springblade.transport.pojo.vo.AdminWaybillDetailVO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 调度端(小程序管理端)运单首页服务
|
||||
*/
|
||||
public interface IManageWaybillService {
|
||||
|
||||
/**
|
||||
* 运单状态统计:运输中 / 待接单 / 在途异常 / 已完成
|
||||
*/
|
||||
AdminHomeStatsVO stats();
|
||||
|
||||
/**
|
||||
* 首页聚合:统计 + 角标 + 待处理事项(异常处置≠已完成)+ 用户名
|
||||
*/
|
||||
AdminHomeVO home();
|
||||
|
||||
/**
|
||||
* 调度端运单分页列表
|
||||
*
|
||||
* @param current 页码
|
||||
* @param size 每页条数
|
||||
* @param keyword 运单号/司机/车牌
|
||||
* @param status 0待接单/1运输中/2已完成,空=全部
|
||||
* @param exception exception有异常 / normal无异常 / 空=全部
|
||||
* @param transportType common普通 / load配载 / 空=全部
|
||||
* @param startDate 创建日起 YYYY-MM-DD
|
||||
* @param endDate 创建日止 YYYY-MM-DD
|
||||
*/
|
||||
IPage<AdminWaybillCardVO> pageList(Integer current, Integer size, String keyword, String status,
|
||||
String exception, String transportType, String startDate, String endDate);
|
||||
|
||||
/**
|
||||
* 调度端运单详情(不校验司机归属)
|
||||
*/
|
||||
AdminWaybillDetailVO detail(Long id);
|
||||
|
||||
/**
|
||||
* 待处理运单(待接单 / 运输中;可筛需重新派单)
|
||||
*/
|
||||
IPage<AdminWaybillCardVO> pendingList(Integer current, Integer size, String keyword, Boolean needReassign);
|
||||
|
||||
/**
|
||||
* 重新派单:跳过管理端部门校验,仅需登录态(司机、手机号、车牌)
|
||||
*/
|
||||
boolean reassign(Long id, Long driverId, String driverName, String driverPhone, String vehicleNo);
|
||||
|
||||
/**
|
||||
* 搜索司机(姓名/手机号)
|
||||
*/
|
||||
List<AdminDriverOptionVO> searchDrivers(String keyword);
|
||||
|
||||
/**
|
||||
* 搜索车牌(来自司机绑定车牌)
|
||||
*/
|
||||
List<AdminVehicleOptionVO> searchVehicles(String keyword);
|
||||
|
||||
}
|
||||
+8
@@ -40,6 +40,14 @@ public interface IProjectApplyService extends BaseService<ProjectApply> {
|
||||
|
||||
IPage<ProjectApplyVO> selectProjectApplyPage(IPage<ProjectApply> page, ProjectApplyVO projectApply);
|
||||
ProjectApplyVO detail(Long id);
|
||||
|
||||
/**
|
||||
* 公开详情,不校验当前用户所属组织
|
||||
*
|
||||
* @param id 主键
|
||||
* @return 项目立项详情
|
||||
*/
|
||||
ProjectApplyVO publicDetail(Long id);
|
||||
Map<String, Integer> fundRiskStats(ProjectApplyVO projectApply);
|
||||
Map<String, Object> changeRecordDetail(Long id, Integer recordIndex);
|
||||
boolean saveDraft(ProjectApply projectApply);
|
||||
|
||||
+7
@@ -37,6 +37,7 @@ import org.springblade.transport.pojo.vo.ReceivablePayableCargoFeeVO;
|
||||
import org.springblade.transport.pojo.vo.ReceivablePayableDetailVO;
|
||||
import org.springblade.transport.pojo.vo.ReceivablePayableFeeDetailVO;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Collection;
|
||||
@@ -83,6 +84,12 @@ public interface IReceivablePayableDetailService extends BaseService<ReceivableP
|
||||
/** 完成运单后按合同系统计费模式自动生成应收、应付明细。 */
|
||||
void generateForCompletedWaybills(List<Long> waybillIds);
|
||||
|
||||
/**
|
||||
* 运单维护里程后,同步未挂结算单的应收应付明细里程;
|
||||
* 对按里程、按吨·公里计费的费用行按新里程重新计算。
|
||||
*/
|
||||
void syncMileageFromWaybill(Long waybillId, BigDecimal mileage);
|
||||
|
||||
/**
|
||||
* 批量导入完成运单后按合同系统计费模式生成应收、应付明细。
|
||||
* <p>与导入事务共用同一事务,运单尚未提交,因此直接传入实体而非主键。</p>
|
||||
|
||||
+34
@@ -28,6 +28,8 @@ import org.springblade.transport.excel.WaybillExcel;
|
||||
import org.springblade.transport.pojo.entity.Waybill;
|
||||
import org.springblade.transport.pojo.vo.BusinessRemoveResultVO;
|
||||
import org.springblade.transport.pojo.vo.LoadingManageVO;
|
||||
import org.springblade.transport.pojo.vo.WaybillLocateVO;
|
||||
import org.springblade.transport.pojo.vo.WaybillTrackVO;
|
||||
import org.springblade.transport.pojo.vo.WaybillPunchRecordsVO;
|
||||
import org.springblade.transport.pojo.vo.WaybillVO;
|
||||
import org.springblade.transport.pojo.dto.WaybillMileageRequest;
|
||||
@@ -44,11 +46,37 @@ public interface IWaybillService extends BaseService<Waybill> {
|
||||
IPage<WaybillVO> selectWaybillPage(IPage<Waybill> page, WaybillVO waybill);
|
||||
WaybillVO detail(Long id);
|
||||
|
||||
/**
|
||||
* 公开详情,不校验当前用户所属组织
|
||||
*
|
||||
* @param id 主键
|
||||
* @return 运单详情
|
||||
*/
|
||||
WaybillVO publicDetail(Long id);
|
||||
|
||||
/**
|
||||
* 管理端:运单打卡记录 + 司机上传凭证图(label=节点-凭证类型)
|
||||
*/
|
||||
WaybillPunchRecordsVO listPunchRecords(Long waybillId);
|
||||
|
||||
/**
|
||||
* 运单车辆实时定位(按运单绑定车牌调用 LBS)
|
||||
*
|
||||
* @param id 运单ID
|
||||
* @return 定位结果
|
||||
*/
|
||||
WaybillLocateVO locateVehicle(Long id);
|
||||
|
||||
/**
|
||||
* 运单历史轨迹回放(按运单绑定车牌 + 日期区间调用 LBS)
|
||||
*
|
||||
* @param id 运单ID
|
||||
* @param startDate 开始日期 YYYY-MM-DD
|
||||
* @param endDate 结束日期 YYYY-MM-DD
|
||||
* @return 轨迹结果
|
||||
*/
|
||||
WaybillTrackVO trackVehicle(Long id, String startDate, String endDate);
|
||||
|
||||
Waybill syncDriverAcceptState(Waybill waybill);
|
||||
boolean submit(Waybill waybill);
|
||||
boolean saveDraft(Waybill waybill);
|
||||
@@ -60,6 +88,12 @@ public interface IWaybillService extends BaseService<Waybill> {
|
||||
boolean maintainMileage(WaybillMileageRequest request);
|
||||
boolean cancel(Long id);
|
||||
boolean reassign(Waybill waybill);
|
||||
|
||||
/**
|
||||
* 小程序调度端重新派单:跳过管理端部门校验,其余逻辑与 {@link #reassign(Waybill)} 一致。
|
||||
*/
|
||||
boolean reassignWithoutDeptCheck(Waybill waybill);
|
||||
|
||||
boolean complete(Long id);
|
||||
|
||||
/**
|
||||
|
||||
+1
@@ -94,6 +94,7 @@ public class AccidentRecordServiceImpl extends BaseServiceImpl<AccidentRecordMap
|
||||
AccidentRecordExcel excel = data.get(index);
|
||||
try {
|
||||
AccidentRecord accidentRecord = Objects.requireNonNull(BeanUtil.copyProperties(excel, AccidentRecord.class));
|
||||
accidentRecord.setAccidentDate(org.springblade.common.excel.LenientDateParser.parseDate(excel.getAccidentDate(), "事故发生日期"));
|
||||
prepare(accidentRecord);
|
||||
List<String> validationErrors = validateImportAccidentRecord(accidentRecord);
|
||||
if (Func.isNotEmpty(validationErrors)) {
|
||||
|
||||
+2
@@ -113,6 +113,8 @@ public class AnnualInspectionRecordServiceImpl extends BaseServiceImpl<AnnualIns
|
||||
AnnualInspectionRecordExcel excel = data.get(index);
|
||||
try {
|
||||
AnnualInspectionRecord annualInspectionRecord = Objects.requireNonNull(BeanUtil.copyProperties(excel, AnnualInspectionRecord.class));
|
||||
annualInspectionRecord.setInspectionAssessmentDate(org.springblade.common.excel.LenientDateParser.parseDate(excel.getInspectionAssessmentDate(), "检测评定日期"));
|
||||
annualInspectionRecord.setValidUntilDate(org.springblade.common.excel.LenientDateParser.parseDate(excel.getValidUntilDate(), "有效期截止日"));
|
||||
prepare(annualInspectionRecord);
|
||||
List<String> validationErrors = validateImportAnnualInspectionRecord(annualInspectionRecord);
|
||||
if (Func.isNotEmpty(validationErrors)) {
|
||||
|
||||
+14
@@ -28,6 +28,7 @@ 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.secure.utils.AuthUtil;
|
||||
import org.springblade.core.tenant.annotation.TenantIgnore;
|
||||
import org.springblade.core.tool.jackson.JsonUtil;
|
||||
import org.springblade.core.tool.utils.BeanUtil;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
@@ -97,6 +98,15 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
|
||||
return contractManageVO;
|
||||
}
|
||||
|
||||
@Override
|
||||
@TenantIgnore
|
||||
public ContractManageVO publicDetail(Long id) {
|
||||
ContractManageVO contractManageVO = ContractManageWrapper.build().entityVO(loadExists(id));
|
||||
normalizeOptionalIntegerFields(contractManageVO);
|
||||
contractManageVO.setReadonly(true);
|
||||
return contractManageVO;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean saveDraft(ContractManage contractManage) {
|
||||
@@ -647,6 +657,10 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
|
||||
if (contractManage.getInvoiceCycle() != null && contractManage.getInvoiceCycle() <= 0) {
|
||||
contractManage.setInvoiceCycle(null);
|
||||
}
|
||||
BigDecimal contractAmount = contractManage.getContractAmount();
|
||||
if (contractAmount != null && contractAmount.compareTo(BigDecimal.valueOf(-1)) == 0) {
|
||||
contractManage.setContractAmount(null);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateDraft(ContractManage contractManage) {
|
||||
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
*/
|
||||
package org.springblade.transport.service.impl;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.core.tenant.annotation.TenantIgnore;
|
||||
import org.springblade.core.tool.api.FR;
|
||||
import org.springblade.core.tool.utils.StringUtil;
|
||||
import org.springblade.process.feign.IBusinessProcessClient;
|
||||
import org.springblade.transport.service.ICustomerArchiveService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* 客商公开页流程消息处理:当前节点立即查询,流程实例详情延迟查询。
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@TenantIgnore
|
||||
@RequiredArgsConstructor
|
||||
public class CustomerArchivePublicProcessService {
|
||||
|
||||
private static final long PROCESS_INFO_DELAY_SECONDS = 5L;
|
||||
|
||||
private final ICustomerArchiveService customerArchiveService;
|
||||
private final IBusinessProcessClient businessProcessClient;
|
||||
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2, runnable -> {
|
||||
Thread thread = new Thread(runnable, "customer-archive-process-info-" + THREAD_INDEX.incrementAndGet());
|
||||
thread.setDaemon(true);
|
||||
return thread;
|
||||
});
|
||||
|
||||
private static final AtomicInteger THREAD_INDEX = new AtomicInteger();
|
||||
|
||||
@PreDestroy
|
||||
public void shutdown() {
|
||||
scheduler.shutdown();
|
||||
}
|
||||
|
||||
public void handleProcessMessage(Map<String, Object> body) {
|
||||
Map<String, Object> formValues = asMap(body == null ? null : body.get("formValues"));
|
||||
String processId = firstText(formValues, "processId");
|
||||
if (StringUtil.isBlank(processId) && body != null) {
|
||||
processId = firstText(body, "processId");
|
||||
}
|
||||
String loginName = firstText(formValues, "mkLoginName", "loginName");
|
||||
if (StringUtil.isBlank(processId)) {
|
||||
log.warn("客商公开页流程消息未找到 processId,跳过查询当前节点");
|
||||
return;
|
||||
}
|
||||
String customerIdText = firstText(asMap(body == null ? null : body.get("formData")), "id");
|
||||
queryCurrentNodes(processId, loginName);
|
||||
scheduleProcessInfo(processId, loginName, customerIdText, formValues);
|
||||
}
|
||||
|
||||
private void queryCurrentNodes(String processId, String loginName) {
|
||||
try {
|
||||
FR<Object> result = businessProcessClient.getCurrentNodes(processId, loginName);
|
||||
Object nodeData = result == null ? null : result.getData();
|
||||
log.info("客商公开页流程消息当前节点详情 processId={} loginName={} result={}",
|
||||
processId, loginName, JSON.toJSONString(nodeData));
|
||||
} catch (Exception e) {
|
||||
log.error("客商公开页查询当前节点失败 processId={} loginName={}", processId, loginName, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void scheduleProcessInfo(String processId, String loginName, String customerIdText,
|
||||
Map<String, Object> formValues) {
|
||||
Map<String, Object> formValuesCopy = new HashMap<>(formValues == null ? Map.of() : formValues);
|
||||
log.info("客商公开页将在{}秒后查询流程实例详情 processId={}", PROCESS_INFO_DELAY_SECONDS, processId);
|
||||
scheduler.schedule(
|
||||
() -> queryProcessInfo(processId, loginName, customerIdText, formValuesCopy),
|
||||
PROCESS_INFO_DELAY_SECONDS,
|
||||
TimeUnit.SECONDS
|
||||
);
|
||||
}
|
||||
|
||||
private void queryProcessInfo(String processId, String loginName, String customerIdText,
|
||||
Map<String, Object> formValues) {
|
||||
try {
|
||||
FR<Object> processInfoResult = businessProcessClient.getProcessInfo(processId, loginName);
|
||||
Object processInfo = processInfoResult == null ? null : processInfoResult.getData();
|
||||
log.info("客商公开页流程消息流程实例详情 processId={} loginName={} result={}",
|
||||
processId, loginName, JSON.toJSONString(processInfo));
|
||||
if (isProcessFinished(processInfo)) {
|
||||
applyProcessResult(customerIdText, processId, formValues, true);
|
||||
} else if (isProcessRejected(processInfo)) {
|
||||
applyProcessResult(customerIdText, processId, formValues, false);
|
||||
} else if (StringUtil.isNotBlank(customerIdText)) {
|
||||
try {
|
||||
customerArchiveService.syncProcessNodeFromProcessInfo(Long.valueOf(customerIdText), processInfo);
|
||||
} catch (NumberFormatException e) {
|
||||
log.warn("客商公开页流程消息客商id格式不正确:{}", customerIdText);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("客商公开页查询流程实例详情失败 processId={} loginName={}", processId, loginName, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isProcessFinished(Object processInfo) {
|
||||
return "30".equals(firstText(asMap(processInfo), "fdProcessStatus"));
|
||||
}
|
||||
|
||||
private static boolean isProcessRejected(Object processInfo) {
|
||||
Map<String, Object> info = asMap(processInfo);
|
||||
if (!"20".equals(firstText(info, "fdProcessStatus"))) {
|
||||
return false;
|
||||
}
|
||||
if (hasItems(info.get("currentHandlers"))) {
|
||||
return false;
|
||||
}
|
||||
return !hasItems(asMap(info.get("fdTaskInfo")).get("handlerInfos"));
|
||||
}
|
||||
|
||||
private static boolean hasItems(Object value) {
|
||||
return value instanceof Collection<?> collection && !collection.isEmpty();
|
||||
}
|
||||
|
||||
private void applyProcessResult(String customerIdText, String processId, Map<String, Object> formValues,
|
||||
boolean approved) {
|
||||
String action = approved ? "审核通过" : "审核驳回";
|
||||
if (StringUtil.isBlank(customerIdText)) {
|
||||
log.warn("流程{}但未找到客商id,跳过同步 processId={}", action, processId);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
String processorName = firstText(formValues, "mkUserName", "mkLoginName");
|
||||
Long customerId = Long.valueOf(customerIdText);
|
||||
if (approved) {
|
||||
customerArchiveService.approveFromProcess(customerId, processorName);
|
||||
} else {
|
||||
customerArchiveService.rejectFromProcess(customerId, processorName);
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
log.warn("流程{}但客商id格式不正确:{}", action, customerIdText);
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<String, Object> asMap(Object value) {
|
||||
if (!(value instanceof Map<?, ?> map)) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
map.forEach((key, nested) -> {
|
||||
if (key != null) {
|
||||
result.put(String.valueOf(key), nested);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
private static String firstText(Map<String, Object> source, String... keys) {
|
||||
if (source == null || keys == null) {
|
||||
return null;
|
||||
}
|
||||
for (String key : keys) {
|
||||
Object value = source.get(key);
|
||||
if (value == null) {
|
||||
continue;
|
||||
}
|
||||
String text = String.valueOf(value).trim();
|
||||
if (StringUtil.isNotBlank(text) && !"null".equalsIgnoreCase(text)) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+334
-37
@@ -34,6 +34,7 @@ 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.tenant.annotation.TenantIgnore;
|
||||
import org.springblade.core.tool.jackson.JsonUtil;
|
||||
import org.springblade.core.tool.utils.BeanUtil;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
@@ -95,6 +96,7 @@ import java.math.RoundingMode;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
@@ -112,6 +114,7 @@ import java.util.stream.Stream;
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@@ -225,31 +228,28 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
|
||||
throw new ServiceException("客商ID不能为空");
|
||||
}
|
||||
ensureCustomerAccessible(customerId);
|
||||
IPage<CustomerChangeRecord> recordPage = changeRecordMapper.selectPage(
|
||||
new Page<>(page.getCurrent(), page.getSize()),
|
||||
Wrappers.<CustomerChangeRecord>lambdaQuery()
|
||||
.eq(CustomerChangeRecord::getCustomerId, customerId)
|
||||
.eq(CustomerChangeRecord::getIsDeleted, 0)
|
||||
.orderByDesc(CustomerChangeRecord::getChangeTime));
|
||||
page.setTotal(recordPage.getTotal());
|
||||
return page.setRecords(recordPage.getRecords().stream()
|
||||
.map(record -> Objects.requireNonNull(BeanUtil.copyProperties(record, CustomerChangeRecordVO.class)))
|
||||
.toList());
|
||||
return queryChangeRecordPage(page, customerId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@TenantIgnore
|
||||
public IPage<CustomerChangeRecordVO> publicChangeRecordPage(IPage<CustomerChangeRecordVO> page, Long customerId) {
|
||||
if (Func.isEmpty(customerId)) {
|
||||
throw new ServiceException("客商ID不能为空");
|
||||
}
|
||||
getExistingCustomer(customerId);
|
||||
return queryChangeRecordPage(page, customerId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CustomerArchiveVO detail(Long id) {
|
||||
if (Func.isEmpty(id)) {
|
||||
throw new ServiceException("主键不能为空");
|
||||
}
|
||||
CustomerArchive customer = ensureCustomerAccessible(id);
|
||||
CustomerArchiveVO detail = Objects.requireNonNull(BeanUtil.copyProperties(customer, CustomerArchiveVO.class));
|
||||
detail.setContacts(loadContacts(id));
|
||||
detail.setReceiptAccounts(loadReceiptAccounts(id));
|
||||
detail.setInvoices(loadInvoices(id));
|
||||
detail.setScores(loadScores(id));
|
||||
fillFundUseRisk(List.of(detail));
|
||||
return detail;
|
||||
return buildDetail(ensureCustomerAccessible(id));
|
||||
}
|
||||
|
||||
@Override
|
||||
@TenantIgnore
|
||||
public CustomerArchiveVO publicDetail(Long id) {
|
||||
return buildDetail(getExistingCustomer(id));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -286,16 +286,157 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
|
||||
CustomerArchiveVO detail = detail(id);
|
||||
prepare(detail);
|
||||
validateBase(detail);
|
||||
validateScoreForApproval(detail);
|
||||
validateApproval(detail);
|
||||
CustomerArchive before = Objects.requireNonNull(BeanUtil.copyProperties(detail, CustomerArchive.class));
|
||||
CustomerArchive after = copyCustomer(before);
|
||||
after.setApprovalStatus(APPROVAL_REVIEWING);
|
||||
after.setCurrentNode("客商准入审批");
|
||||
after.setCurrentProcessor("待处理");
|
||||
if (Func.isEmpty(after.getCurrentNode()) || Objects.equals(after.getCurrentNode(), "草稿")) {
|
||||
after.setCurrentNode("客商准入审批");
|
||||
after.setCurrentProcessor("待处理");
|
||||
}
|
||||
addChangeRecord(id, "提交客商准入审批", before, after);
|
||||
return updateById(after);
|
||||
}
|
||||
|
||||
@Override
|
||||
@TenantIgnore
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean syncProcessNode(Long id, String currentNode, String currentProcessor, String approvalStatus) {
|
||||
if (Func.isEmpty(id)) {
|
||||
log.warn("同步客商当前节点失败,客商ID为空");
|
||||
return false;
|
||||
}
|
||||
CustomerArchive customer = getById(id);
|
||||
if (customer == null || Objects.equals(customer.getIsDeleted(), 1)) {
|
||||
log.warn("同步客商当前节点失败,客商不存在 id={}", id);
|
||||
return false;
|
||||
}
|
||||
String nextStatus = Func.isEmpty(approvalStatus) ? APPROVAL_REVIEWING : approvalStatus;
|
||||
boolean updated = this.lambdaUpdate()
|
||||
.eq(CustomerArchive::getId, id)
|
||||
.set(Func.isNotEmpty(currentNode), CustomerArchive::getCurrentNode, currentNode)
|
||||
.set(Func.isNotEmpty(currentProcessor), CustomerArchive::getCurrentProcessor, currentProcessor)
|
||||
.set(Func.isNotEmpty(nextStatus), CustomerArchive::getApprovalStatus, nextStatus)
|
||||
.update();
|
||||
log.info("同步客商当前节点 id={} currentNode={} currentProcessor={} approvalStatus={} result={}",
|
||||
id, currentNode, currentProcessor, nextStatus, updated);
|
||||
return updated;
|
||||
}
|
||||
|
||||
@Override
|
||||
@TenantIgnore
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean syncProcessNodeFromMk(Long id, Object currentNodes) {
|
||||
if (Func.isEmpty(id) || currentNodes == null) {
|
||||
return false;
|
||||
}
|
||||
List<Map<String, Object>> nodes = normalizeMkNodeList(currentNodes);
|
||||
if (nodes.isEmpty()) {
|
||||
log.warn("同步客商当前节点跳过,节点详情为空 id={}", id);
|
||||
return false;
|
||||
}
|
||||
List<String> nodeNames = new ArrayList<>();
|
||||
List<String> handlerNames = new ArrayList<>();
|
||||
for (Map<String, Object> node : nodes) {
|
||||
String nodeName = Func.toStr(node.get("nodeName"), "").trim();
|
||||
if (Func.isNotEmpty(nodeName)) {
|
||||
nodeNames.add(nodeName);
|
||||
}
|
||||
Object handlers = node.get("nodeHandlers");
|
||||
if (!(handlers instanceof Collection<?> handlerList)) {
|
||||
continue;
|
||||
}
|
||||
for (Object handler : handlerList) {
|
||||
Map<String, Object> handlerMap = asStringObjectMap(handler);
|
||||
String handlerName = Func.toStr(handlerMap.get("handlerName"), "").trim();
|
||||
if (Func.isNotEmpty(handlerName)) {
|
||||
handlerNames.add(handlerName);
|
||||
}
|
||||
}
|
||||
}
|
||||
return syncProcessNode(id, joinDistinct(nodeNames), joinDistinct(handlerNames), APPROVAL_REVIEWING);
|
||||
}
|
||||
|
||||
@Override
|
||||
@TenantIgnore
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean syncProcessNodeFromProcessInfo(Long id, Object processInfo) {
|
||||
if (Func.isEmpty(id) || processInfo == null) {
|
||||
return false;
|
||||
}
|
||||
Map<String, Object> info = asStringObjectMap(processInfo);
|
||||
if (info.isEmpty()) {
|
||||
log.warn("同步客商当前节点跳过,流程实例详情为空 id={}", id);
|
||||
return false;
|
||||
}
|
||||
String currentProcessor = joinDistinct(extractTexts(info.get("currentHandlers"), "fdName", "name"), "、");
|
||||
Map<String, Object> taskInfo = asStringObjectMap(info.get("fdTaskInfo"));
|
||||
String currentNode = joinDistinct(extractTexts(taskInfo.get("handlerInfos"), "nodeName"), "、");
|
||||
if (Func.isEmpty(currentNode) && Func.isEmpty(currentProcessor)) {
|
||||
log.warn("同步客商当前节点跳过,未解析到节点名称或处理人 id={}", id);
|
||||
return false;
|
||||
}
|
||||
return syncProcessNode(id, currentNode, currentProcessor, APPROVAL_REVIEWING);
|
||||
}
|
||||
|
||||
@Override
|
||||
@TenantIgnore
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean approveFromProcess(Long id, String processorName) {
|
||||
if (Func.isEmpty(id)) {
|
||||
log.warn("流程结束审核通过失败,客商ID为空");
|
||||
return false;
|
||||
}
|
||||
CustomerArchive before = getById(id);
|
||||
if (before == null || Objects.equals(before.getIsDeleted(), 1)) {
|
||||
log.warn("流程结束审核通过失败,客商不存在 id={}", id);
|
||||
return false;
|
||||
}
|
||||
if (Objects.equals(before.getApprovalStatus(), APPROVAL_APPROVED)) {
|
||||
log.info("流程结束审核通过跳过,客商已是审核通过 id={}", id);
|
||||
return true;
|
||||
}
|
||||
validateReviewCompleted(buildDetail(before));
|
||||
CustomerArchive after = copyCustomer(before);
|
||||
after.setAccessType(ACCESS_FORMAL);
|
||||
after.setApprovalStatus(APPROVAL_APPROVED);
|
||||
after.setCurrentNode("审核通过");
|
||||
after.setCurrentProcessor(Func.isEmpty(processorName) ? "系统" : processorName.trim());
|
||||
after.setApprovedTime(LocalDateTime.now());
|
||||
addChangeRecord(id, "客商准入审核通过", before, after);
|
||||
boolean updated = updateById(after);
|
||||
log.info("流程结束审核通过 id={} processor={} result={}", id, after.getCurrentProcessor(), updated);
|
||||
return updated;
|
||||
}
|
||||
|
||||
@Override
|
||||
@TenantIgnore
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean rejectFromProcess(Long id, String processorName) {
|
||||
if (Func.isEmpty(id)) {
|
||||
log.warn("流程审核驳回失败,客商ID为空");
|
||||
return false;
|
||||
}
|
||||
CustomerArchive before = getById(id);
|
||||
if (before == null || Objects.equals(before.getIsDeleted(), 1)) {
|
||||
log.warn("流程审核驳回失败,客商不存在 id={}", id);
|
||||
return false;
|
||||
}
|
||||
if (Objects.equals(before.getApprovalStatus(), APPROVAL_REJECTED)) {
|
||||
log.info("流程审核驳回跳过,客商已是审核不通过 id={}", id);
|
||||
return true;
|
||||
}
|
||||
CustomerArchive after = copyCustomer(before);
|
||||
after.setApprovalStatus(APPROVAL_REJECTED);
|
||||
after.setCurrentNode("审核不通过");
|
||||
after.setCurrentProcessor(Func.isEmpty(processorName) ? "系统" : processorName.trim());
|
||||
addChangeRecord(id, "客商准入审核不通过", before, after);
|
||||
boolean updated = updateById(after);
|
||||
log.info("流程审核驳回 id={} processor={} result={}", id, after.getCurrentProcessor(), updated);
|
||||
return updated;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean withdrawApproval(Long id) {
|
||||
@@ -317,6 +458,7 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
|
||||
CustomerArchiveVO detail = detail(id);
|
||||
prepare(detail);
|
||||
validateBase(detail);
|
||||
validateReviewCompleted(detail);
|
||||
// 临时客商审核通过时由流程转为正式客商,不要求补齐正式客商材料。
|
||||
validateApproval(detail, Objects.equals(detail.getAccessType(), ACCESS_TEMPORARY));
|
||||
CustomerArchive before = Objects.requireNonNull(BeanUtil.copyProperties(detail, CustomerArchive.class));
|
||||
@@ -741,6 +883,7 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
|
||||
private void prepare(CustomerArchiveVO customer) {
|
||||
customer.setShortName(trimToNull(customer.getShortName()));
|
||||
customer.setFullName(trimToEmpty(customer.getFullName()));
|
||||
customer.setCustomerKind(normalizeCustomerKind(customer.getCustomerKind()));
|
||||
customer.setCustomerNature(trimToEmpty(customer.getCustomerNature()));
|
||||
customer.setGuangxiTop100(normalizeYesNoFlag(customer.getGuangxiTop100()));
|
||||
customer.setUnifiedCreditCode(trimToEmpty(customer.getUnifiedCreditCode()));
|
||||
@@ -780,6 +923,12 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
|
||||
if (Func.isEmpty(customer.getFullName())) {
|
||||
throw new ServiceException("客商全称不能为空");
|
||||
}
|
||||
if (Func.isEmpty(customer.getCustomerKind())) {
|
||||
throw new ServiceException("客商类型不能为空");
|
||||
}
|
||||
if (!"external".equals(customer.getCustomerKind()) && !"internal".equals(customer.getCustomerKind())) {
|
||||
throw new ServiceException("客商类型只能是外部客商或内部组织");
|
||||
}
|
||||
if (Func.isEmpty(customer.getCustomerNature())) {
|
||||
throw new ServiceException("客商性质不能为空");
|
||||
}
|
||||
@@ -790,7 +939,7 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
|
||||
throw new ServiceException("统一社会信用代码必须为18位大写字母或数字");
|
||||
}
|
||||
if (Func.isEmpty(customer.getCustomerType())) {
|
||||
throw new ServiceException("客商类型不能为空");
|
||||
throw new ServiceException("客商分类不能为空");
|
||||
}
|
||||
validateCustomerType(customer.getCustomerType());
|
||||
if (Func.isEmpty(customer.getLegalPerson())) {
|
||||
@@ -832,7 +981,7 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
|
||||
.filter(Func::isNotEmpty)
|
||||
.toList();
|
||||
if (Func.isEmpty(typeList)) {
|
||||
throw new ServiceException("客商类型不能为空");
|
||||
throw new ServiceException("客商分类不能为空");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -840,16 +989,30 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
|
||||
validateApproval(customer, false);
|
||||
}
|
||||
|
||||
private void validateReviewCompleted(CustomerArchiveVO customer) {
|
||||
List<CustomerCreditScoreVO> scores = customer.getScores() == null ? List.of() : customer.getScores();
|
||||
boolean reviewCompleted = scores.size() == 1 && "已完成".equals(scores.get(0).getReviewStatus());
|
||||
if (!reviewCompleted) {
|
||||
throw new ServiceException("请先完成复评");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateScoreForApproval(CustomerArchiveVO customer) {
|
||||
List<CustomerCreditScoreVO> scores = customer.getScores() == null ? List.of() : customer.getScores();
|
||||
if (scores.size() > 1) {
|
||||
throw new ServiceException("评分记录只能有一条");
|
||||
}
|
||||
boolean selfCompleted = scores.size() == 1 && "已完成".equals(scores.get(0).getSelfStatus());
|
||||
if (!selfCompleted) {
|
||||
throw new ServiceException("提交审批前必须完成自评");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateApproval(CustomerArchiveVO customer, boolean temporaryToFormal) {
|
||||
if (Objects.equals(customer.getAccessType(), ACCESS_FORMAL) && !temporaryToFormal) {
|
||||
if (Func.isEmpty(customer.getQualificationAttachments())) {
|
||||
throw new ServiceException("正式客商提交审核前必须上传客商材料");
|
||||
}
|
||||
boolean hasCompletedScore = customer.getScores().stream()
|
||||
.anyMatch(score -> "已完成".equals(score.getSelfStatus()) || "已完成".equals(score.getReviewStatus()) || Func.isNotEmpty(score.getFinalScore()));
|
||||
if (!hasCompletedScore) {
|
||||
throw new ServiceException("正式客商提交审核前必须完成信用评分");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -881,10 +1044,8 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
|
||||
insertContacts(customerId, customer.getContacts());
|
||||
insertReceiptAccounts(customerId, customer.getReceiptAccounts());
|
||||
insertInvoices(customerId, customer);
|
||||
if (Func.isNotEmpty(customer.getScores())) {
|
||||
deleteScores(List.of(customerId));
|
||||
insertScores(customerId, customer.getScores());
|
||||
}
|
||||
deleteScores(List.of(customerId));
|
||||
insertScores(customerId, customer.getScores());
|
||||
}
|
||||
|
||||
private void insertContacts(Long customerId, List<CustomerContactVO> contacts) {
|
||||
@@ -905,6 +1066,23 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
|
||||
if (Func.isEmpty(accountVO.getAccountName()) && Func.isEmpty(accountVO.getBankAccount())) {
|
||||
continue;
|
||||
}
|
||||
if (Func.isEmpty(accountVO.getAccountName())) {
|
||||
throw new ServiceException("收款单位名称不能为空");
|
||||
}
|
||||
if (Func.isEmpty(accountVO.getAccountHolderName())) {
|
||||
throw new ServiceException("开户人姓名不能为空");
|
||||
}
|
||||
if (Func.isEmpty(accountVO.getBankName())) {
|
||||
throw new ServiceException("开户行不能为空");
|
||||
}
|
||||
if (Func.isEmpty(accountVO.getCnapsCode())) {
|
||||
throw new ServiceException("联行号不能为空");
|
||||
}
|
||||
if (Func.isEmpty(accountVO.getBankAccount())) {
|
||||
throw new ServiceException("收款账号不能为空");
|
||||
}
|
||||
validateLength(accountVO.getCnapsCode(), 32, "联行号最多32个字符");
|
||||
validateLength(accountVO.getRemark(), 200, "收款信息备注最多200个字符");
|
||||
CustomerReceiptAccount account = Objects.requireNonNull(BeanUtil.copyProperties(accountVO, CustomerReceiptAccount.class));
|
||||
account.setId(IdWorker.getId());
|
||||
account.setCustomerId(customerId);
|
||||
@@ -1041,7 +1219,7 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
|
||||
}
|
||||
for (int detailIndex = 0; detailIndex < details.size(); detailIndex++) {
|
||||
CustomerCreditScoreDetailVO detailVO = details.get(detailIndex);
|
||||
if (Func.isEmpty(detailVO.getScore())) {
|
||||
if (detailVO.getScore() == null) {
|
||||
detailVO.setScore(resolveFullScore(detailVO.getOptionsJson()));
|
||||
}
|
||||
CustomerCreditScoreDetail detail = Objects.requireNonNull(BeanUtil.copyProperties(detailVO, CustomerCreditScoreDetail.class));
|
||||
@@ -1205,6 +1383,24 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
|
||||
throw new ServiceException("是否广西百强只能选择是或否");
|
||||
}
|
||||
|
||||
private String normalizeCustomerKind(String customerKind) {
|
||||
String value = trimToEmpty(customerKind);
|
||||
if (Func.isEmpty(value) || "外部客商".equals(value) || "external".equals(value)) {
|
||||
return "external";
|
||||
}
|
||||
if ("内部组织".equals(value) || "internal".equals(value)) {
|
||||
return "internal";
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private String customerKindName(String customerKind) {
|
||||
if ("internal".equals(customerKind) || "内部组织".equals(customerKind)) {
|
||||
return "内部组织";
|
||||
}
|
||||
return "外部客商";
|
||||
}
|
||||
|
||||
private Object parseScoreAttachments(String value) {
|
||||
if (Func.isEmpty(value)) {
|
||||
return new ArrayList<>();
|
||||
@@ -1425,11 +1621,43 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
|
||||
return CUSTOMER_CODE_PREFIX + String.format("%06d", nextNumber);
|
||||
}
|
||||
|
||||
private CustomerArchive ensureCustomerAccessible(Long id) {
|
||||
private IPage<CustomerChangeRecordVO> queryChangeRecordPage(IPage<CustomerChangeRecordVO> page, Long customerId) {
|
||||
IPage<CustomerChangeRecord> recordPage = changeRecordMapper.selectPage(
|
||||
new Page<>(page.getCurrent(), page.getSize()),
|
||||
Wrappers.<CustomerChangeRecord>lambdaQuery()
|
||||
.eq(CustomerChangeRecord::getCustomerId, customerId)
|
||||
.eq(CustomerChangeRecord::getIsDeleted, 0)
|
||||
.orderByDesc(CustomerChangeRecord::getChangeTime));
|
||||
page.setTotal(recordPage.getTotal());
|
||||
return page.setRecords(recordPage.getRecords().stream()
|
||||
.map(record -> Objects.requireNonNull(BeanUtil.copyProperties(record, CustomerChangeRecordVO.class)))
|
||||
.toList());
|
||||
}
|
||||
|
||||
private CustomerArchiveVO buildDetail(CustomerArchive customer) {
|
||||
CustomerArchiveVO detail = Objects.requireNonNull(BeanUtil.copyProperties(customer, CustomerArchiveVO.class));
|
||||
Long id = customer.getId();
|
||||
detail.setContacts(loadContacts(id));
|
||||
detail.setReceiptAccounts(loadReceiptAccounts(id));
|
||||
detail.setInvoices(loadInvoices(id));
|
||||
detail.setScores(loadScores(id));
|
||||
fillFundUseRisk(List.of(detail));
|
||||
return detail;
|
||||
}
|
||||
|
||||
private CustomerArchive getExistingCustomer(Long id) {
|
||||
if (Func.isEmpty(id)) {
|
||||
throw new ServiceException("主键不能为空");
|
||||
}
|
||||
CustomerArchive customer = getById(id);
|
||||
if (Func.isEmpty(customer) || Objects.equals(customer.getIsDeleted(), 1)) {
|
||||
throw new ServiceException("客商档案不存在");
|
||||
}
|
||||
return customer;
|
||||
}
|
||||
|
||||
private CustomerArchive ensureCustomerAccessible(Long id) {
|
||||
CustomerArchive customer = getExistingCustomer(id);
|
||||
if (baseMapper.selectCustomerPermissionCount(id, AuthUtil.getUserId()) == 0) {
|
||||
throw new ServiceException("无权访问该客商档案");
|
||||
}
|
||||
@@ -1440,6 +1668,73 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
|
||||
return Objects.requireNonNull(BeanUtil.copyProperties(customer, CustomerArchive.class));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private List<Map<String, Object>> normalizeMkNodeList(Object currentNodes) {
|
||||
Object source = currentNodes;
|
||||
if (source instanceof String text && Func.isNotEmpty(text.trim())) {
|
||||
source = JsonUtil.parse(text, List.class);
|
||||
}
|
||||
if (source instanceof Collection<?> collection) {
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
for (Object item : collection) {
|
||||
Map<String, Object> map = asStringObjectMap(item);
|
||||
if (!map.isEmpty()) {
|
||||
result.add(map);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
Map<String, Object> single = asStringObjectMap(source);
|
||||
return single.isEmpty() ? List.of() : List.of(single);
|
||||
}
|
||||
|
||||
private Map<String, Object> asStringObjectMap(Object value) {
|
||||
if (value instanceof Map<?, ?> map) {
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
map.forEach((key, nested) -> {
|
||||
if (key != null) {
|
||||
result.put(String.valueOf(key), nested);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
if (value == null) {
|
||||
return Map.of();
|
||||
}
|
||||
try {
|
||||
Map<String, Object> parsed = JsonUtil.toMap(JsonUtil.toJson(value));
|
||||
return parsed == null ? Map.of() : parsed;
|
||||
} catch (Exception ignored) {
|
||||
return Map.of();
|
||||
}
|
||||
}
|
||||
|
||||
private String joinDistinct(List<String> values) {
|
||||
return joinDistinct(values, ",");
|
||||
}
|
||||
|
||||
private String joinDistinct(List<String> values, String delimiter) {
|
||||
return values.stream().filter(Func::isNotEmpty).distinct().collect(Collectors.joining(delimiter));
|
||||
}
|
||||
|
||||
private List<String> extractTexts(Object listObj, String... keys) {
|
||||
List<String> result = new ArrayList<>();
|
||||
if (!(listObj instanceof Collection<?> collection) || keys == null) {
|
||||
return result;
|
||||
}
|
||||
for (Object item : collection) {
|
||||
Map<String, Object> map = asStringObjectMap(item);
|
||||
for (String key : keys) {
|
||||
String text = Func.toStr(map.get(key), "").trim();
|
||||
if (Func.isNotEmpty(text)) {
|
||||
result.add(text);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void addChangeRecord(Long customerId, String content, CustomerArchive before, CustomerArchive after) {
|
||||
CustomerChangeRecord record = buildChangeRecord(customerId, content, before, after);
|
||||
if (record != null) {
|
||||
@@ -1614,10 +1909,11 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
|
||||
snapshot.put("客商编号", customer.getCustomerCode());
|
||||
snapshot.put("客商简称", customer.getShortName());
|
||||
snapshot.put("客商全称", customer.getFullName());
|
||||
snapshot.put("客商类型(内外部)", customer.getCustomerKind());
|
||||
snapshot.put("客商性质", customer.getCustomerNature());
|
||||
snapshot.put("是否广西百强", customer.getGuangxiTop100());
|
||||
snapshot.put("统一社会信用代码", customer.getUnifiedCreditCode());
|
||||
snapshot.put("客商类型", customer.getCustomerType());
|
||||
snapshot.put("客商分类", customer.getCustomerType());
|
||||
snapshot.put("所属项目", customer.getProjectName());
|
||||
snapshot.put("注册/实际经营地址", customer.getRegisteredAddress());
|
||||
snapshot.put("注册地址行政区划", customer.getRegisteredRegionName());
|
||||
@@ -1716,6 +2012,7 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
|
||||
excel.setCustomerCode(customer.getCustomerCode());
|
||||
excel.setShortName(customer.getShortName());
|
||||
excel.setFullName(customer.getFullName());
|
||||
excel.setCustomerKind(customerKindName(customer.getCustomerKind()));
|
||||
excel.setCustomerType(customer.getCustomerType());
|
||||
excel.setCustomerNature(customer.getCustomerNature());
|
||||
excel.setUnifiedCreditCode(customer.getUnifiedCreditCode());
|
||||
|
||||
+72
-2
@@ -225,6 +225,18 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
return toCard(normalizeAcceptStatus(waybill), true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DriverWaybillCardVO detailPunchSnapshot(Long id) {
|
||||
if (id == null) {
|
||||
throw new ServiceException("运单ID不能为空");
|
||||
}
|
||||
Waybill waybill = waybillService.getById(id);
|
||||
if (waybill == null || Objects.equals(waybill.getIsDeleted(), 1)) {
|
||||
throw new ServiceException("运单不存在");
|
||||
}
|
||||
return toCard(normalizeAcceptStatus(waybill), true);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public DriverEnrouteRecordVO submitEnroute(EnrouteSubmitDTO dto) {
|
||||
@@ -610,6 +622,19 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
card.setAcceptStatus(waybill.getDriverAcceptStatus());
|
||||
card.setRejectReason(waybill.getDriverRejectReason());
|
||||
|
||||
// 详情页字段(列表也可带上,体积很小)
|
||||
String cargoName = Func.toStr(waybill.getCargoName(), "");
|
||||
String weightText = card.getWeight();
|
||||
card.setCargoName(cargoName);
|
||||
card.setPickupAddress(card.getFromAddress());
|
||||
card.setUnloadAddress(card.getToAddress());
|
||||
card.setCargoQuantity(weightText);
|
||||
card.setTotalWeight(weightText);
|
||||
card.setTransportType(toTransportTypeLabel(waybill.getTransportType()));
|
||||
card.setPlanShipTime(formatLocalDateYmd(waybill.getEstimatedStartTime()));
|
||||
card.setPlanFinishTime(formatLocalDateYmd(waybill.getEstimatedEndTime()));
|
||||
card.setRemark(Func.toStr(waybill.getRemark(), ""));
|
||||
|
||||
if (withEnrouteRecords) {
|
||||
Date lastPunchAt = findLastPunchTime(waybill.getId());
|
||||
WaybillProcessSupport.TransitCheckinDecision transit = WaybillProcessSupport.evaluateTransitCheckin(
|
||||
@@ -623,6 +648,8 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
card.setTransitTimeEnd(transit.timeEnd());
|
||||
card.setEnrouteRecords(listEnrouteRecords(waybill.getId()));
|
||||
card.setPunchNodes(buildPunchNodes(waybill, transit, processJson));
|
||||
card.setRoutePoints(buildSimpleRoutePoints(waybill));
|
||||
card.setProcessJson(processJson);
|
||||
} else {
|
||||
// 列表/首页:只解析过程配置是否启用在途打卡,不做频次/时段与落库查询
|
||||
boolean punchEnabled = WaybillProcessSupport.isTransitPunchEnabled(processJson);
|
||||
@@ -634,18 +661,61 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
return card;
|
||||
}
|
||||
|
||||
private List<DriverWaybillCardVO.DriverRoutePointVO> buildSimpleRoutePoints(Waybill waybill) {
|
||||
DriverWaybillCardVO.DriverRoutePointVO load = new DriverWaybillCardVO.DriverRoutePointVO();
|
||||
load.setName(Func.toStr(waybill.getDepartureName(), "装货点"));
|
||||
load.setAddress(Func.toStr(waybill.getDepartureAddress(), load.getName()));
|
||||
load.setStatus("pending");
|
||||
DriverWaybillCardVO.DriverRoutePointVO unload = new DriverWaybillCardVO.DriverRoutePointVO();
|
||||
unload.setName(Func.toStr(waybill.getArrivalName(), "卸货点"));
|
||||
unload.setAddress(Func.toStr(waybill.getArrivalAddress(), unload.getName()));
|
||||
unload.setStatus("pending");
|
||||
return List.of(load, unload);
|
||||
}
|
||||
|
||||
private String toTransportTypeLabel(String transportType) {
|
||||
if (Func.isBlank(transportType)) {
|
||||
return "";
|
||||
}
|
||||
String t = transportType.trim().toLowerCase();
|
||||
return switch (t) {
|
||||
case "road", "gl" -> "公路运输";
|
||||
case "railway", "rail" -> "铁路运输";
|
||||
case "river", "water", "waterway" -> "水路运输";
|
||||
case "air", "aviation" -> "航空运输";
|
||||
default -> transportType;
|
||||
};
|
||||
}
|
||||
|
||||
private String formatLocalDateYmd(LocalDate date) {
|
||||
if (date == null) {
|
||||
return "";
|
||||
}
|
||||
return date.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 动态获取项目启用中的过程配置节点 JSON;无则回退运单快照 processJson。
|
||||
* 优先用项目启用中的过程配置;若动态配置无打卡节点,回退运单快照 processJson,
|
||||
* 避免项目配置改坏后司机端打卡页空白。
|
||||
*/
|
||||
private String resolveProcessJson(Waybill waybill) {
|
||||
if (waybill == null) {
|
||||
return null;
|
||||
}
|
||||
String snapshot = waybill.getProcessJson();
|
||||
String live = loadLiveProcessConfigJson(waybill.getProjectId());
|
||||
if (Func.isNotEmpty(live)) {
|
||||
if (!WaybillProcessSupport.listDriverPunchNodes(live).isEmpty()) {
|
||||
return live;
|
||||
}
|
||||
// 动态配置存在但无可打卡节点:仍回退快照
|
||||
if (Func.isNotEmpty(snapshot)
|
||||
&& !WaybillProcessSupport.listDriverPunchNodes(snapshot).isEmpty()) {
|
||||
return snapshot;
|
||||
}
|
||||
return live;
|
||||
}
|
||||
return waybill.getProcessJson();
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
private String loadLiveProcessConfigJson(Long projectId) {
|
||||
|
||||
+1
@@ -91,6 +91,7 @@ public class EquipmentLedgerServiceImpl extends BaseServiceImpl<EquipmentLedgerM
|
||||
EquipmentLedgerExcel excel = data.get(index);
|
||||
try {
|
||||
EquipmentLedger equipmentLedger = Objects.requireNonNull(BeanUtil.copyProperties(excel, EquipmentLedger.class));
|
||||
equipmentLedger.setFactoryDate(org.springblade.common.excel.LenientDateParser.parseDate(excel.getFactoryDate(), "出厂日期"));
|
||||
prepare(equipmentLedger);
|
||||
if (equipmentLedger.getId() == null && Func.isEmpty(equipmentLedger.getEquipmentCode())) {
|
||||
equipmentLedger.setEquipmentCode(nextEquipmentCode(importEquipmentCodes));
|
||||
|
||||
+3
@@ -119,6 +119,9 @@ public class InsuranceRecordServiceImpl extends BaseServiceImpl<InsuranceRecordM
|
||||
InsuranceRecordExcel excel = data.get(index);
|
||||
try {
|
||||
InsuranceRecord insuranceRecord = Objects.requireNonNull(BeanUtil.copyProperties(excel, InsuranceRecord.class));
|
||||
insuranceRecord.setStartDate(org.springblade.common.excel.LenientDateParser.parseDate(excel.getStartDate(), "开始日期"));
|
||||
insuranceRecord.setEndDate(org.springblade.common.excel.LenientDateParser.parseDate(excel.getEndDate(), "结束日期"));
|
||||
insuranceRecord.setInvoiceDate(org.springblade.common.excel.LenientDateParser.parseDate(excel.getInvoiceDate(), "开票日期"));
|
||||
prepare(insuranceRecord);
|
||||
List<String> validationErrors = validateImportInsuranceRecord(insuranceRecord);
|
||||
if (Func.isNotEmpty(insuranceRecord.getVehicleType()) && Func.isNotEmpty(insuranceRecord.getInsuranceType()) && Func.isNotEmpty(insuranceRecord.getPolicyNo())) {
|
||||
|
||||
+160
-5
@@ -29,6 +29,7 @@ import org.springblade.transport.service.IContractManageService;
|
||||
import org.springblade.transport.service.ILoadingManageService;
|
||||
import org.springblade.transport.service.IReceivablePayableDetailService;
|
||||
import org.springblade.transport.support.TransportBusinessSupport;
|
||||
import org.springblade.transport.support.WaybillProcessSupport;
|
||||
import org.springblade.transport.wrapper.LoadingManageWrapper;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -40,8 +41,10 @@ import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
@@ -76,6 +79,7 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
|
||||
IPage<LoadingManage> entityPage = page(page, buildQuery(loadingManage));
|
||||
IPage<LoadingManageVO> voPage = LoadingManageWrapper.build().pageVO(entityPage);
|
||||
voPage.getRecords().forEach(this::fillReadonly);
|
||||
fillDriverRejected(voPage.getRecords());
|
||||
return voPage;
|
||||
}
|
||||
|
||||
@@ -83,6 +87,7 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
|
||||
public LoadingManageVO detail(Long id) {
|
||||
LoadingManageVO loadingManageVO = LoadingManageWrapper.build().entityVO(loadExists(id));
|
||||
fillReadonly(loadingManageVO);
|
||||
fillDriverRejected(List.of(loadingManageVO));
|
||||
return loadingManageVO;
|
||||
}
|
||||
|
||||
@@ -104,24 +109,33 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean saveDraft(LoadingManage loadingManage) {
|
||||
prepare(loadingManage);
|
||||
validate(loadingManage, true);
|
||||
// 暂存不做任何业务校验,允许空运单、缺字段直接落草稿
|
||||
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();
|
||||
}
|
||||
}
|
||||
prepareCreateOrUpdate(loadingManage);
|
||||
loadingManage.setBusinessStatus(STATUS_DRAFT);
|
||||
loadingManage.setBusinessStatus(businessStatus);
|
||||
if (Func.isEmpty(loadingManage.getLoadingNo())) {
|
||||
loadingManage.setLoadingNo(nextCode());
|
||||
}
|
||||
validateWaybillsAvailable(loadingManage, waybillIds(loadingManage.getWaybillIdsJson()));
|
||||
validateCarrierContract(loadingManage, false);
|
||||
boolean result = saveOrUpdate(loadingManage);
|
||||
syncAssociatedWaybills(loadingManage, STATUS_DRAFT, oldWaybillIds);
|
||||
// 草稿仅预占运单号,不把运单业务状态改成 draft;正式确认时再完整同步为待执行
|
||||
if (STATUS_DRAFT.equals(businessStatus)) {
|
||||
syncDraftWaybillReservation(loadingManage, oldWaybillIds);
|
||||
} else {
|
||||
syncAssociatedWaybills(loadingManage, businessStatus, oldWaybillIds);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -139,6 +153,7 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
|
||||
loadingManage.setDeptName(oldRecord.getDeptName());
|
||||
}
|
||||
prepareCreateOrUpdate(loadingManage);
|
||||
// 确认提交:草稿/新建统一生成正式配载单(待执行)
|
||||
loadingManage.setBusinessStatus(STATUS_PENDING);
|
||||
if (Func.isEmpty(loadingManage.getLoadingNo())) {
|
||||
loadingManage.setLoadingNo(nextCode());
|
||||
@@ -164,6 +179,7 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
|
||||
throw new ServiceException("配载标识号已存在:" + normalizedLoadingNo);
|
||||
}
|
||||
Waybill first = waybills.get(0);
|
||||
Waybill last = waybills.get(waybills.size() - 1);
|
||||
LoadingManage loadingManage = new LoadingManage();
|
||||
loadingManage.setLoadingNo(normalizedLoadingNo);
|
||||
loadingManage.setLoadingSubNos(waybills.stream()
|
||||
@@ -187,7 +203,8 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
|
||||
loadingManage.setCarrierName(first.getCarrierName());
|
||||
loadingManage.setCarrierContractId(first.getCarrierContractId());
|
||||
loadingManage.setDepartureAddress(first.getDepartureAddress());
|
||||
loadingManage.setArrivalAddress(first.getArrivalAddress());
|
||||
loadingManage.setTransitAddress(buildImportedRouteTransitAddress(waybills));
|
||||
loadingManage.setArrivalAddress(last.getArrivalAddress());
|
||||
loadingManage.setOriginalNo(first.getOriginalNo());
|
||||
loadingManage.setDataSource("批量导入");
|
||||
loadingManage.setStartDate(first.getStartDate());
|
||||
@@ -212,6 +229,39 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
|
||||
.set(Waybill::getLoadingNo, normalizedLoadingNo));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据导入运单构建配载单的途经地:
|
||||
* 组内运单按导入顺序串联路线,上一票的到货地址与下一票的发货地址相同(中途卸货点)时只保留一个点,
|
||||
* 最终形成“首票发货地 → 途经点 → 末票到货地”。
|
||||
*/
|
||||
private String buildImportedRouteTransitAddress(List<Waybill> waybills) {
|
||||
// 按顺序收集全部节点:首票发货地、每票到货地;节点与相邻前一点相同则重合跳过
|
||||
List<String> nodes = new ArrayList<>();
|
||||
for (Waybill waybill : waybills) {
|
||||
String departure = TransportBusinessSupport.trimToNull(waybill.getDepartureAddress());
|
||||
String arrival = TransportBusinessSupport.trimToNull(waybill.getArrivalAddress());
|
||||
appendRouteNode(nodes, departure);
|
||||
appendRouteNode(nodes, arrival);
|
||||
}
|
||||
// 途经点 = 去掉首尾(首票发货地、末票到货地)后的中间节点
|
||||
List<String> transitNodes = nodes.size() > 2 ? nodes.subList(1, nodes.size() - 1) : List.of();
|
||||
if (Func.isEmpty(transitNodes)) {
|
||||
return null;
|
||||
}
|
||||
return String.join(" - ", transitNodes);
|
||||
}
|
||||
|
||||
private void appendRouteNode(List<String> nodes, String address) {
|
||||
if (Func.isEmpty(address)) {
|
||||
return;
|
||||
}
|
||||
if (!nodes.isEmpty() && nodes.get(nodes.size() - 1).equals(address)) {
|
||||
// 与上一节点相同视为同一地点,重合不重复
|
||||
return;
|
||||
}
|
||||
nodes.add(address);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public BusinessRemoveResultVO removeLoadingManage(String ids) {
|
||||
@@ -321,6 +371,9 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
|
||||
if (!Objects.equals(oldRecord.getBusinessStatus(), STATUS_PENDING)) {
|
||||
throw new ServiceException("仅待执行状态允许重新派单");
|
||||
}
|
||||
if (!hasDriverRejectedWaybill(oldRecord)) {
|
||||
throw new ServiceException("仅司机拒绝接单后允许重新派单");
|
||||
}
|
||||
prepare(loadingManage);
|
||||
validate(loadingManage, false);
|
||||
List<Long> oldWaybillIds = waybillIds(oldRecord.getWaybillIdsJson());
|
||||
@@ -332,6 +385,7 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
|
||||
validateCarrierContract(loadingManage, true);
|
||||
boolean result = updateById(loadingManage);
|
||||
syncAssociatedWaybills(loadingManage, STATUS_PENDING, oldWaybillIds);
|
||||
clearAssociatedWaybillAcceptRecords(loadingManage);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -351,6 +405,19 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
|
||||
return updateById(oldRecord);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean start(Long id) {
|
||||
LoadingManage loadingManage = loadEditable(id, true);
|
||||
if (!Objects.equals(loadingManage.getBusinessStatus(), STATUS_PENDING)) {
|
||||
throw new ServiceException("仅待执行状态允许改为进行中");
|
||||
}
|
||||
loadingManage.setBusinessStatus(STATUS_RUNNING);
|
||||
boolean result = updateById(loadingManage);
|
||||
syncAssociatedWaybills(loadingManage, STATUS_RUNNING, new ArrayList<>());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean cancel(Long id) {
|
||||
@@ -554,6 +621,11 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
|
||||
loadingManage.setRouteJson(TransportBusinessSupport.trimToNull(loadingManage.getRouteJson()));
|
||||
loadingManage.setTaskInfoJson(TransportBusinessSupport.trimToNull(loadingManage.getTaskInfoJson()));
|
||||
loadingManage.setDeptName(TransportBusinessSupport.trimToNull(loadingManage.getDeptName()));
|
||||
// 未填写里程禁止落库 -1 占位值,统一存 null
|
||||
if (loadingManage.getMileage() != null
|
||||
&& loadingManage.getMileage().compareTo(BigDecimal.valueOf(-1)) == 0) {
|
||||
loadingManage.setMileage(null);
|
||||
}
|
||||
if (Func.isEmpty(loadingManage.getDeptId())) {
|
||||
Dept dept = TransportBusinessSupport.currentDept("配载管理");
|
||||
loadingManage.setDeptId(dept.getId());
|
||||
@@ -751,6 +823,25 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
|
||||
.set(Waybill::getCurrentProcessNode, loadingManage.getCurrentProcessNode()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 草稿阶段仅预占运单(写入 loadingNo),不改写运单业务状态。
|
||||
* 避免暂存把运单打成 draft,导致确认生成正式配载单时状态异常。
|
||||
*/
|
||||
private void syncDraftWaybillReservation(LoadingManage loadingManage, List<Long> oldWaybillIds) {
|
||||
List<Long> newWaybillIds = waybillIds(loadingManage.getWaybillIdsJson());
|
||||
if (Func.isNotEmpty(oldWaybillIds)) {
|
||||
List<Long> removedIds = new ArrayList<>(oldWaybillIds);
|
||||
removedIds.removeAll(newWaybillIds);
|
||||
releaseAssociatedWaybills(removedIds, STATUS_PENDING);
|
||||
}
|
||||
if (Func.isEmpty(newWaybillIds)) {
|
||||
return;
|
||||
}
|
||||
waybillMapper.update(null, Wrappers.<Waybill>lambdaUpdate()
|
||||
.in(Waybill::getId, newWaybillIds)
|
||||
.set(Waybill::getLoadingNo, loadingManage.getLoadingNo()));
|
||||
}
|
||||
|
||||
private void releaseAssociatedWaybills(List<Long> waybillIds, String status) {
|
||||
if (Func.isEmpty(waybillIds)) {
|
||||
return;
|
||||
@@ -817,6 +908,70 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
|
||||
// 保持与现有业务模块一致,包装类已完成主字段映射,这里预留兼容入口。
|
||||
}
|
||||
|
||||
private void fillDriverRejected(List<LoadingManageVO> records) {
|
||||
if (Func.isEmpty(records)) {
|
||||
return;
|
||||
}
|
||||
List<String> loadingNos = records.stream()
|
||||
.map(LoadingManage::getLoadingNo)
|
||||
.filter(Func::isNotEmpty)
|
||||
.distinct()
|
||||
.toList();
|
||||
Set<String> rejectedLoadingNos = new HashSet<>();
|
||||
if (Func.isNotEmpty(loadingNos)) {
|
||||
rejectedLoadingNos.addAll(waybillMapper.selectList(Wrappers.<Waybill>lambdaQuery()
|
||||
.select(Waybill::getLoadingNo)
|
||||
.in(Waybill::getLoadingNo, loadingNos)
|
||||
.eq(Waybill::getIsDeleted, 0)
|
||||
.eq(Waybill::getDriverAcceptStatus, WaybillProcessSupport.ACCEPT_REJECTED))
|
||||
.stream()
|
||||
.map(Waybill::getLoadingNo)
|
||||
.filter(Func::isNotEmpty)
|
||||
.collect(Collectors.toSet()));
|
||||
}
|
||||
for (LoadingManageVO record : records) {
|
||||
record.setDriverRejected(rejectedLoadingNos.contains(record.getLoadingNo()));
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasDriverRejectedWaybill(LoadingManage loadingManage) {
|
||||
if (Func.isNotEmpty(loadingManage.getLoadingNo())) {
|
||||
Long count = waybillMapper.selectCount(Wrappers.<Waybill>lambdaQuery()
|
||||
.eq(Waybill::getLoadingNo, loadingManage.getLoadingNo())
|
||||
.eq(Waybill::getIsDeleted, 0)
|
||||
.eq(Waybill::getDriverAcceptStatus, WaybillProcessSupport.ACCEPT_REJECTED));
|
||||
if (count != null && count > 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
List<Long> ids = waybillIds(loadingManage.getWaybillIdsJson());
|
||||
if (Func.isEmpty(ids)) {
|
||||
return false;
|
||||
}
|
||||
Long count = waybillMapper.selectCount(Wrappers.<Waybill>lambdaQuery()
|
||||
.in(Waybill::getId, ids)
|
||||
.eq(Waybill::getIsDeleted, 0)
|
||||
.eq(Waybill::getDriverAcceptStatus, WaybillProcessSupport.ACCEPT_REJECTED));
|
||||
return count != null && count > 0;
|
||||
}
|
||||
|
||||
private void clearAssociatedWaybillAcceptRecords(LoadingManage loadingManage) {
|
||||
List<Long> ids = waybillIds(loadingManage.getWaybillIdsJson());
|
||||
var update = Wrappers.<Waybill>lambdaUpdate()
|
||||
.set(Waybill::getDriverAcceptStatus, WaybillProcessSupport.ACCEPT_PENDING)
|
||||
.set(Waybill::getDriverAcceptTime, null)
|
||||
.set(Waybill::getDriverAcceptDriverId, null)
|
||||
.set(Waybill::getDriverRejectTime, null)
|
||||
.set(Waybill::getDriverRejectReason, null);
|
||||
if (Func.isNotEmpty(ids)) {
|
||||
waybillMapper.update(null, update.in(Waybill::getId, ids));
|
||||
return;
|
||||
}
|
||||
if (Func.isNotEmpty(loadingManage.getLoadingNo())) {
|
||||
waybillMapper.update(null, update.eq(Waybill::getLoadingNo, loadingManage.getLoadingNo()));
|
||||
}
|
||||
}
|
||||
|
||||
private String formatMileage(BigDecimal mileage) {
|
||||
if (mileage == null) {
|
||||
return null;
|
||||
|
||||
+642
@@ -0,0 +1,642 @@
|
||||
/**
|
||||
* 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 com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springblade.core.secure.utils.AuthUtil;
|
||||
import org.springblade.core.tool.utils.DateUtil;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.system.cache.UserCache;
|
||||
import org.springblade.transport.pojo.entity.Driver;
|
||||
import org.springblade.transport.pojo.entity.ExceptionDisposal;
|
||||
import org.springblade.transport.pojo.entity.RiskDisposal;
|
||||
import org.springblade.transport.pojo.entity.Waybill;
|
||||
import org.springblade.transport.pojo.vo.AdminDriverOptionVO;
|
||||
import org.springblade.transport.pojo.vo.AdminHomeBadgesVO;
|
||||
import org.springblade.transport.pojo.vo.AdminHomeStatsVO;
|
||||
import org.springblade.transport.pojo.vo.AdminHomeVO;
|
||||
import org.springblade.transport.pojo.vo.AdminTodoItemVO;
|
||||
import org.springblade.transport.pojo.vo.AdminVehicleOptionVO;
|
||||
import org.springblade.transport.pojo.vo.AdminWaybillCardVO;
|
||||
import org.springblade.transport.pojo.vo.AdminWaybillDetailVO;
|
||||
import org.springblade.transport.pojo.vo.DriverWaybillCardVO;
|
||||
import org.springblade.transport.service.IDriverService;
|
||||
import org.springblade.transport.service.IDriverWaybillService;
|
||||
import org.springblade.transport.service.IExceptionDisposalService;
|
||||
import org.springblade.transport.service.IManageWaybillService;
|
||||
import org.springblade.transport.service.IRiskDisposalService;
|
||||
import org.springblade.transport.service.IWaybillService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 调度端:运单状态统计 + 异常/风险角标 + 待处理事项 + 运单列表
|
||||
* <p>
|
||||
* 小程序调度账号(如「小程序管理」)组织常与运单业务组织不一致,故不做 dept 过滤,
|
||||
* 仅依赖租户隔离,口径接近后台 {@code /waybill-manage/list?allDept=1}。
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ManageWaybillServiceImpl implements IManageWaybillService {
|
||||
|
||||
private static final String STATUS_PENDING = "pending";
|
||||
private static final String STATUS_RUNNING = "running";
|
||||
private static final String STATUS_COMPLETED = "completed";
|
||||
private static final String STATUS_CANCELLED = "cancelled";
|
||||
private static final String ACCEPT_REJECTED = "rejected";
|
||||
|
||||
private static final String DISPOSAL_PENDING = "pending";
|
||||
private static final String DISPOSAL_PROCESSING = "processing";
|
||||
private static final String RISK_PENDING = "pending";
|
||||
|
||||
private static final String EXCEPTION_YES = "exception";
|
||||
private static final String EXCEPTION_NO = "normal";
|
||||
private static final String TRANSPORT_COMMON = "common";
|
||||
private static final String TRANSPORT_LOAD = "load";
|
||||
|
||||
private static final int FEED_LIMIT = 20;
|
||||
private static final int DEFAULT_PAGE_SIZE = 10;
|
||||
private static final int MAX_PAGE_SIZE = 50;
|
||||
|
||||
private static final DateTimeFormatter DATE_MD = DateTimeFormatter.ofPattern("MM-dd");
|
||||
private static final DateTimeFormatter DATE_YMD = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||
|
||||
private final IWaybillService waybillService;
|
||||
private final IDriverService driverService;
|
||||
private final IExceptionDisposalService exceptionDisposalService;
|
||||
private final IRiskDisposalService riskDisposalService;
|
||||
private final IDriverWaybillService driverWaybillService;
|
||||
|
||||
@Override
|
||||
public AdminHomeStatsVO stats() {
|
||||
AdminHomeStatsVO vo = new AdminHomeStatsVO();
|
||||
vo.setPendingAccept(countWaybillByStatus(STATUS_PENDING));
|
||||
vo.setTransporting(countWaybillByStatus(STATUS_RUNNING));
|
||||
vo.setCompleted(countWaybillByStatus(STATUS_COMPLETED));
|
||||
vo.setException(countIncompleteExceptions());
|
||||
return vo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AdminHomeVO home() {
|
||||
AdminHomeVO home = new AdminHomeVO();
|
||||
home.setUserName(resolveUserName());
|
||||
home.setStats(stats());
|
||||
|
||||
AdminHomeBadgesVO badges = new AdminHomeBadgesVO();
|
||||
badges.setException(home.getStats().getException());
|
||||
badges.setRisk(countPendingRisks());
|
||||
home.setBadges(badges);
|
||||
home.setFeed(buildExceptionFeed());
|
||||
return home;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IPage<AdminWaybillCardVO> pageList(Integer current, Integer size, String keyword, String status,
|
||||
String exception, String transportType, String startDate, String endDate) {
|
||||
int pageNo = current == null || current < 1 ? 1 : current;
|
||||
int pageSize = size == null || size < 1 ? DEFAULT_PAGE_SIZE : Math.min(size, MAX_PAGE_SIZE);
|
||||
|
||||
Set<Long> exceptionWaybillIds = loadIncompleteExceptionWaybillIds();
|
||||
if (EXCEPTION_YES.equals(exception) && exceptionWaybillIds.isEmpty()) {
|
||||
Page<AdminWaybillCardVO> emptyVo = new Page<>(pageNo, pageSize, 0);
|
||||
emptyVo.setRecords(List.of());
|
||||
return emptyVo;
|
||||
}
|
||||
|
||||
LambdaQueryWrapper<Waybill> wrapper = scopedWaybillQuery();
|
||||
applyStatusFilter(wrapper, status);
|
||||
applyExceptionFilter(wrapper, exception, exceptionWaybillIds);
|
||||
applyTransportTypeFilter(wrapper, transportType);
|
||||
applyKeywordFilter(wrapper, keyword);
|
||||
applyCreateTimeFilter(wrapper, startDate, endDate);
|
||||
wrapper.orderByDesc(Waybill::getCreateTime);
|
||||
|
||||
IPage<Waybill> entityPage = waybillService.page(new Page<>(pageNo, pageSize), wrapper);
|
||||
List<Long> pageIds = entityPage.getRecords().stream()
|
||||
.map(Waybill::getId)
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
Set<Long> pageExceptionIds = pageIds.isEmpty()
|
||||
? Collections.emptySet()
|
||||
: exceptionWaybillIds.stream().filter(pageIds::contains).collect(Collectors.toSet());
|
||||
|
||||
Page<AdminWaybillCardVO> voPage = new Page<>(entityPage.getCurrent(), entityPage.getSize(), entityPage.getTotal());
|
||||
voPage.setRecords(entityPage.getRecords().stream()
|
||||
.map(w -> toCard(w, pageExceptionIds.contains(w.getId())))
|
||||
.toList());
|
||||
return voPage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AdminWaybillDetailVO detail(Long id) {
|
||||
if (id == null) {
|
||||
throw new org.springblade.core.log.exception.ServiceException("运单ID不能为空");
|
||||
}
|
||||
Waybill waybill = waybillService.getById(id);
|
||||
if (waybill == null || Objects.equals(waybill.getIsDeleted(), 1)) {
|
||||
throw new org.springblade.core.log.exception.ServiceException("运单不存在");
|
||||
}
|
||||
boolean hasException = false;
|
||||
Long exceptionId = null;
|
||||
ExceptionDisposal latest = exceptionDisposalService.getOne(Wrappers.<ExceptionDisposal>lambdaQuery()
|
||||
.eq(ExceptionDisposal::getWaybillId, id)
|
||||
.in(ExceptionDisposal::getDisposalStatus, DISPOSAL_PENDING, DISPOSAL_PROCESSING)
|
||||
.orderByDesc(ExceptionDisposal::getReportTime)
|
||||
.orderByDesc(ExceptionDisposal::getCreateTime)
|
||||
.last("LIMIT 1"));
|
||||
if (latest != null) {
|
||||
hasException = true;
|
||||
exceptionId = latest.getId();
|
||||
}
|
||||
return toDetail(waybill, hasException, exceptionId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IPage<AdminWaybillCardVO> pendingList(Integer current, Integer size, String keyword, Boolean needReassign) {
|
||||
int pageNo = current == null || current < 1 ? 1 : current;
|
||||
int pageSize = size == null || size < 1 ? DEFAULT_PAGE_SIZE : Math.min(size, MAX_PAGE_SIZE);
|
||||
|
||||
LambdaQueryWrapper<Waybill> wrapper = scopedWaybillQuery()
|
||||
.in(Waybill::getBusinessStatus, STATUS_PENDING, STATUS_RUNNING);
|
||||
if (Boolean.TRUE.equals(needReassign)) {
|
||||
wrapper.eq(Waybill::getDriverAcceptStatus, ACCEPT_REJECTED);
|
||||
} else if (Boolean.FALSE.equals(needReassign)) {
|
||||
wrapper.and(w -> w.isNull(Waybill::getDriverAcceptStatus)
|
||||
.or().ne(Waybill::getDriverAcceptStatus, ACCEPT_REJECTED));
|
||||
}
|
||||
applyKeywordFilter(wrapper, keyword);
|
||||
wrapper.orderByDesc(Waybill::getUpdateTime).orderByDesc(Waybill::getCreateTime);
|
||||
|
||||
IPage<Waybill> entityPage = waybillService.page(new Page<>(pageNo, pageSize), wrapper);
|
||||
Set<Long> exceptionWaybillIds = loadIncompleteExceptionWaybillIds();
|
||||
List<Long> pageIds = entityPage.getRecords().stream()
|
||||
.map(Waybill::getId)
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
Set<Long> pageExceptionIds = pageIds.isEmpty()
|
||||
? Collections.emptySet()
|
||||
: exceptionWaybillIds.stream().filter(pageIds::contains).collect(Collectors.toSet());
|
||||
|
||||
Page<AdminWaybillCardVO> voPage = new Page<>(entityPage.getCurrent(), entityPage.getSize(), entityPage.getTotal());
|
||||
voPage.setRecords(entityPage.getRecords().stream()
|
||||
.map(w -> toCard(w, pageExceptionIds.contains(w.getId())))
|
||||
.toList());
|
||||
return voPage;
|
||||
}
|
||||
|
||||
private AdminWaybillDetailVO toDetail(Waybill waybill, boolean hasException, Long exceptionId) {
|
||||
AdminWaybillDetailVO detail = new AdminWaybillDetailVO();
|
||||
detail.setId(waybill.getId());
|
||||
detail.setWaybillNo(waybill.getWaybillNo());
|
||||
detail.setStatus(toAppStatus(waybill.getBusinessStatus()));
|
||||
|
||||
String mode = Func.toStr(waybill.getTransportType(), "");
|
||||
detail.setTransportMode(mode);
|
||||
detail.setTransportType(toTransportTypeLabel(mode));
|
||||
detail.setTransportOrgType(Func.isNotBlank(waybill.getLoadingNo()) ? TRANSPORT_LOAD : TRANSPORT_COMMON);
|
||||
|
||||
detail.setFromName(formatPlaceName(waybill.getDepartureName(), mode));
|
||||
detail.setToName(formatPlaceName(waybill.getArrivalName(), mode));
|
||||
String fromAddr = Func.toStr(waybill.getDepartureAddress(), Func.toStr(waybill.getDepartureName(), ""));
|
||||
String toAddr = Func.toStr(waybill.getArrivalAddress(), Func.toStr(waybill.getArrivalName(), ""));
|
||||
detail.setFromAddress(fromAddr);
|
||||
detail.setToAddress(toAddr);
|
||||
detail.setPickupAddress(fromAddr);
|
||||
detail.setUnloadAddress(toAddr);
|
||||
|
||||
String cargo = Func.toStr(waybill.getCargoName(), "");
|
||||
String weight = formatWeight(waybill.getQuantity(), waybill.getQuantityUnit());
|
||||
detail.setCargoName(cargo);
|
||||
detail.setCargoQuantity(weight);
|
||||
detail.setWeight(weight);
|
||||
detail.setTotalWeight(weight);
|
||||
|
||||
detail.setPlanShipTime(formatLocalDate(waybill.getEstimatedStartTime()));
|
||||
detail.setPlanFinishTime(formatLocalDate(waybill.getEstimatedEndTime()));
|
||||
detail.setCarrierName(Func.toStr(waybill.getCarrierName(), ""));
|
||||
detail.setDriverName(Func.toStr(waybill.getDriverName(), ""));
|
||||
detail.setDriverPhone(Func.toStr(waybill.getDriverPhone(), ""));
|
||||
detail.setVehicleNo(Func.toStr(waybill.getVehicleNo(), ""));
|
||||
detail.setRemark(Func.toStr(waybill.getRemark(), ""));
|
||||
detail.setHasException(hasException);
|
||||
detail.setExceptionId(exceptionId);
|
||||
detail.setNeedReassign(ACCEPT_REJECTED.equals(waybill.getDriverAcceptStatus()));
|
||||
detail.setAcceptStatus(Func.toStr(waybill.getDriverAcceptStatus(), ""));
|
||||
detail.setRejectReason(Func.toStr(waybill.getDriverRejectReason(), ""));
|
||||
detail.setDriverId(waybill.getDriverId());
|
||||
|
||||
AdminWaybillDetailVO.AdminRoutePointVO load = new AdminWaybillDetailVO.AdminRoutePointVO();
|
||||
load.setName(Func.toStr(waybill.getDepartureName(), "装货点"));
|
||||
load.setAddress(fromAddr);
|
||||
load.setStatus("pending");
|
||||
AdminWaybillDetailVO.AdminRoutePointVO unload = new AdminWaybillDetailVO.AdminRoutePointVO();
|
||||
unload.setName(Func.toStr(waybill.getArrivalName(), "卸货点"));
|
||||
unload.setAddress(toAddr);
|
||||
unload.setStatus("pending");
|
||||
detail.setRoutePoints(List.of(load, unload));
|
||||
|
||||
// 复用司机端打卡组装:过程节点 + 途打卡记录(调度端只读展示)
|
||||
DriverWaybillCardVO punch = driverWaybillService.detailPunchSnapshot(waybill.getId());
|
||||
if (punch != null) {
|
||||
detail.setPunchNodes(punch.getPunchNodes());
|
||||
detail.setEnrouteRecords(punch.getEnrouteRecords());
|
||||
if (punch.getRoutePoints() != null && !punch.getRoutePoints().isEmpty()) {
|
||||
detail.setRoutePoints(punch.getRoutePoints().stream().map(p -> {
|
||||
AdminWaybillDetailVO.AdminRoutePointVO rp = new AdminWaybillDetailVO.AdminRoutePointVO();
|
||||
rp.setName(p.getName());
|
||||
rp.setAddress(p.getAddress());
|
||||
rp.setStatus(p.getStatus());
|
||||
return rp;
|
||||
}).toList());
|
||||
}
|
||||
}
|
||||
return detail;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean reassign(Long id, Long driverId, String driverName, String driverPhone, String vehicleNo) {
|
||||
Waybill request = new Waybill();
|
||||
request.setId(id);
|
||||
request.setDriverId(driverId);
|
||||
request.setDriverName(driverName);
|
||||
request.setDriverPhone(driverPhone);
|
||||
request.setVehicleNo(vehicleNo);
|
||||
return waybillService.reassignWithoutDeptCheck(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AdminDriverOptionVO> searchDrivers(String keyword) {
|
||||
String key = Func.toStr(keyword, "").trim();
|
||||
LambdaQueryWrapper<Driver> wrapper = Wrappers.<Driver>lambdaQuery()
|
||||
.eq(Driver::getIsDeleted, 0)
|
||||
.orderByDesc(Driver::getUpdateTime)
|
||||
.last("LIMIT 20");
|
||||
if (Func.isNotBlank(key)) {
|
||||
wrapper.and(w -> w.like(Driver::getDriverName, key).or().like(Driver::getMobile, key));
|
||||
}
|
||||
return driverService.list(wrapper).stream().map(d -> {
|
||||
AdminDriverOptionVO vo = new AdminDriverOptionVO();
|
||||
vo.setId(d.getId());
|
||||
vo.setName(Func.toStr(d.getDriverName(), ""));
|
||||
vo.setPhone(Func.toStr(d.getMobile(), ""));
|
||||
vo.setVehicleNo(Func.toStr(d.getDrivingVehicle(), ""));
|
||||
return vo;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@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)
|
||||
.last("LIMIT 30");
|
||||
if (Func.isNotBlank(key)) {
|
||||
wrapper.like(Driver::getDrivingVehicle, 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)) {
|
||||
continue;
|
||||
}
|
||||
AdminVehicleOptionVO vo = new AdminVehicleOptionVO();
|
||||
vo.setVehicleNo(plate);
|
||||
vo.setDriverName(Func.toStr(d.getDriverName(), ""));
|
||||
map.put(plate, vo);
|
||||
}
|
||||
return new java.util.ArrayList<>(map.values());
|
||||
}
|
||||
|
||||
/** 运输方式字典值 → 展示文案 */
|
||||
private String toTransportTypeLabel(String transportType) {
|
||||
if (Func.isBlank(transportType)) {
|
||||
return "";
|
||||
}
|
||||
String t = transportType.trim().toLowerCase();
|
||||
return switch (t) {
|
||||
case "road", "gl" -> "公路运输";
|
||||
case "railway", "rail" -> "铁路运输";
|
||||
case "river", "water", "waterway" -> "水路运输";
|
||||
case "air", "aviation" -> "航空运输";
|
||||
default -> transportType;
|
||||
};
|
||||
}
|
||||
|
||||
private long countWaybillByStatus(String status) {
|
||||
return waybillService.count(Wrappers.<Waybill>lambdaQuery()
|
||||
.eq(Waybill::getBusinessStatus, status));
|
||||
}
|
||||
|
||||
/**
|
||||
* 小程序调度端不做组织过滤。
|
||||
* 「小程序管理」等账号 JWT/档案 dept 常与运单业务组织不一致,按 dept 过滤会导致统计全 0;
|
||||
* 与后台 allDept=1 一致,仅依赖租户隔离(MyBatis-Plus TenantLine)。
|
||||
*/
|
||||
private LambdaQueryWrapper<Waybill> scopedWaybillQuery() {
|
||||
return Wrappers.<Waybill>lambdaQuery();
|
||||
}
|
||||
|
||||
private void applyStatusFilter(LambdaQueryWrapper<Waybill> wrapper, String status) {
|
||||
String businessStatus = toBusinessStatus(status);
|
||||
if (Func.isNotBlank(businessStatus)) {
|
||||
wrapper.eq(Waybill::getBusinessStatus, businessStatus);
|
||||
}
|
||||
}
|
||||
|
||||
private void applyExceptionFilter(LambdaQueryWrapper<Waybill> wrapper, String exception, Set<Long> exceptionWaybillIds) {
|
||||
if (EXCEPTION_YES.equals(exception)) {
|
||||
wrapper.in(Waybill::getId, exceptionWaybillIds);
|
||||
} else if (EXCEPTION_NO.equals(exception) && !exceptionWaybillIds.isEmpty()) {
|
||||
wrapper.notIn(Waybill::getId, exceptionWaybillIds);
|
||||
}
|
||||
}
|
||||
|
||||
private void applyTransportTypeFilter(LambdaQueryWrapper<Waybill> wrapper, String transportType) {
|
||||
if (TRANSPORT_LOAD.equals(transportType)) {
|
||||
wrapper.isNotNull(Waybill::getLoadingNo).ne(Waybill::getLoadingNo, "");
|
||||
} else if (TRANSPORT_COMMON.equals(transportType)) {
|
||||
wrapper.and(w -> w.isNull(Waybill::getLoadingNo).or().eq(Waybill::getLoadingNo, ""));
|
||||
}
|
||||
}
|
||||
|
||||
private void applyKeywordFilter(LambdaQueryWrapper<Waybill> wrapper, String keyword) {
|
||||
if (Func.isBlank(keyword)) {
|
||||
return;
|
||||
}
|
||||
String key = keyword.trim();
|
||||
wrapper.and(w -> w.like(Waybill::getWaybillNo, key)
|
||||
.or().like(Waybill::getDriverName, key)
|
||||
.or().like(Waybill::getVehicleNo, key));
|
||||
}
|
||||
|
||||
private void applyCreateTimeFilter(LambdaQueryWrapper<Waybill> wrapper, String startDate, String endDate) {
|
||||
if (Func.isNotBlank(startDate)) {
|
||||
Date start = DateUtil.parse(startDate.trim() + " 00:00:00", DateUtil.PATTERN_DATETIME);
|
||||
if (start != null) {
|
||||
wrapper.ge(Waybill::getCreateTime, start);
|
||||
}
|
||||
}
|
||||
if (Func.isNotBlank(endDate)) {
|
||||
Date end = DateUtil.parse(endDate.trim() + " 23:59:59", DateUtil.PATTERN_DATETIME);
|
||||
if (end != null) {
|
||||
wrapper.le(Waybill::getCreateTime, end);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 小程序 status → 后端 businessStatus */
|
||||
private String toBusinessStatus(String status) {
|
||||
if (Func.isBlank(status)) {
|
||||
return null;
|
||||
}
|
||||
return switch (status.trim()) {
|
||||
case "0", STATUS_PENDING -> STATUS_PENDING;
|
||||
case "1", STATUS_RUNNING, "transporting", "doing" -> STATUS_RUNNING;
|
||||
case "2", STATUS_COMPLETED, "done" -> STATUS_COMPLETED;
|
||||
case "3", STATUS_CANCELLED -> STATUS_CANCELLED;
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
private Set<Long> loadIncompleteExceptionWaybillIds() {
|
||||
List<ExceptionDisposal> list = exceptionDisposalService.list(Wrappers.<ExceptionDisposal>lambdaQuery()
|
||||
.select(ExceptionDisposal::getWaybillId)
|
||||
.in(ExceptionDisposal::getDisposalStatus, DISPOSAL_PENDING, DISPOSAL_PROCESSING)
|
||||
.isNotNull(ExceptionDisposal::getWaybillId));
|
||||
Set<Long> ids = new HashSet<>();
|
||||
for (ExceptionDisposal item : list) {
|
||||
if (item.getWaybillId() != null) {
|
||||
ids.add(item.getWaybillId());
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
private AdminWaybillCardVO toCard(Waybill waybill, boolean hasException) {
|
||||
AdminWaybillCardVO card = new AdminWaybillCardVO();
|
||||
card.setId(waybill.getId());
|
||||
card.setWaybillNo(waybill.getWaybillNo());
|
||||
String mode = Func.toStr(waybill.getTransportType(), "");
|
||||
card.setTransportMode(mode);
|
||||
card.setFromName(formatPlaceName(waybill.getDepartureName(), mode));
|
||||
card.setToName(formatPlaceName(waybill.getArrivalName(), mode));
|
||||
card.setCargo(Func.toStr(waybill.getCargoName(), ""));
|
||||
card.setWeight(formatWeight(waybill.getQuantity(), waybill.getQuantityUnit()));
|
||||
card.setPlanTime(formatLocalDate(waybill.getEstimatedStartTime()));
|
||||
card.setPlanTimeEnd(formatLocalDate(waybill.getEstimatedEndTime()));
|
||||
card.setStatus(toAppStatus(waybill.getBusinessStatus()));
|
||||
card.setCarrierName(Func.toStr(waybill.getCarrierName(), ""));
|
||||
card.setDriverName(Func.toStr(waybill.getDriverName(), ""));
|
||||
card.setVehicleNo(Func.toStr(waybill.getVehicleNo(), ""));
|
||||
card.setHasException(hasException);
|
||||
card.setTransportType(Func.isNotBlank(waybill.getLoadingNo()) ? TRANSPORT_LOAD : TRANSPORT_COMMON);
|
||||
card.setCreateTime(formatDateTime(waybill.getCreateTime()));
|
||||
card.setNeedReassign(ACCEPT_REJECTED.equals(waybill.getDriverAcceptStatus()));
|
||||
card.setBuyerPaid(false);
|
||||
return card;
|
||||
}
|
||||
|
||||
/**
|
||||
* 公路运输:起/终仅展示市县(去掉省/自治区);其它运输方式原样返回。
|
||||
*/
|
||||
private String formatPlaceName(String name, String transportType) {
|
||||
String raw = Func.toStr(name, "").trim();
|
||||
if (Func.isBlank(raw) || !isRoadTransport(transportType)) {
|
||||
return raw;
|
||||
}
|
||||
return toCityCounty(raw);
|
||||
}
|
||||
|
||||
private boolean isRoadTransport(String transportType) {
|
||||
if (Func.isBlank(transportType)) {
|
||||
return false;
|
||||
}
|
||||
String t = transportType.trim().toLowerCase();
|
||||
return t.contains("road") || transportType.contains("公路") || transportType.contains("道路") || "gl".equals(t);
|
||||
}
|
||||
|
||||
/** 去掉省级前缀,保留「市 + 区/县/旗」 */
|
||||
private String toCityCounty(String name) {
|
||||
String s = name.replaceFirst("^.+?(省|自治区|特别行政区)", "");
|
||||
if (Func.isBlank(s)) {
|
||||
s = name;
|
||||
}
|
||||
java.util.regex.Matcher city = java.util.regex.Pattern
|
||||
.compile("^(.+?市)(.+?(?:区|县|旗|市))?")
|
||||
.matcher(s);
|
||||
if (city.find()) {
|
||||
return Func.toStr(city.group(1), "") + Func.toStr(city.group(2), "");
|
||||
}
|
||||
java.util.regex.Matcher prefecture = java.util.regex.Pattern
|
||||
.compile("^(.+?(?:州|盟|地区))(.+?(?:区|县|旗|市))?")
|
||||
.matcher(s);
|
||||
if (prefecture.find()) {
|
||||
return Func.toStr(prefecture.group(1), "") + Func.toStr(prefecture.group(2), "");
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
private Integer toAppStatus(String businessStatus) {
|
||||
if (Func.isBlank(businessStatus)) {
|
||||
return null;
|
||||
}
|
||||
return switch (businessStatus) {
|
||||
case STATUS_PENDING, "waiting_dispatch", "dispatching" -> 0;
|
||||
case STATUS_RUNNING -> 1;
|
||||
case STATUS_COMPLETED -> 2;
|
||||
case STATUS_CANCELLED -> 3;
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
private String formatWeight(BigDecimal quantity, String unit) {
|
||||
if (quantity == null) {
|
||||
return "";
|
||||
}
|
||||
String qty = quantity.stripTrailingZeros().toPlainString();
|
||||
return Func.isBlank(unit) ? qty : qty + unit;
|
||||
}
|
||||
|
||||
private String formatLocalDate(LocalDate date) {
|
||||
if (date == null) {
|
||||
return "";
|
||||
}
|
||||
return date.format(DATE_YMD);
|
||||
}
|
||||
|
||||
private String formatDateTime(Date date) {
|
||||
if (date == null) {
|
||||
return "";
|
||||
}
|
||||
return DateUtil.format(date, DateUtil.PATTERN_DATETIME);
|
||||
}
|
||||
|
||||
private long countIncompleteExceptions() {
|
||||
return exceptionDisposalService.count(Wrappers.<ExceptionDisposal>lambdaQuery()
|
||||
.in(ExceptionDisposal::getDisposalStatus, DISPOSAL_PENDING, DISPOSAL_PROCESSING));
|
||||
}
|
||||
|
||||
private long countPendingRisks() {
|
||||
return riskDisposalService.count(Wrappers.<RiskDisposal>lambdaQuery()
|
||||
.eq(RiskDisposal::getDisposalStatus, RISK_PENDING));
|
||||
}
|
||||
|
||||
private List<AdminTodoItemVO> buildExceptionFeed() {
|
||||
List<ExceptionDisposal> list = exceptionDisposalService.list(Wrappers.<ExceptionDisposal>lambdaQuery()
|
||||
.in(ExceptionDisposal::getDisposalStatus, DISPOSAL_PENDING, DISPOSAL_PROCESSING)
|
||||
.orderByDesc(ExceptionDisposal::getReportTime)
|
||||
.last("LIMIT " + FEED_LIMIT));
|
||||
return list.stream().map(this::toTodoItem).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private AdminTodoItemVO toTodoItem(ExceptionDisposal disposal) {
|
||||
AdminTodoItemVO item = new AdminTodoItemVO();
|
||||
item.setId(disposal.getId());
|
||||
item.setType("exception");
|
||||
item.setTitle("异常待处置");
|
||||
item.setTimeAgo(formatTimeAgo(disposal.getReportTime() != null
|
||||
? disposal.getReportTime()
|
||||
: toLocalDateTime(disposal.getCreateTime())));
|
||||
item.setDesc(buildExceptionDesc(disposal));
|
||||
item.setWaybillNo(Func.toStr(disposal.getWaybillNo(), ""));
|
||||
item.setActionLabel("立即处置");
|
||||
String status = Func.toStr(disposal.getDisposalStatus(), DISPOSAL_PENDING);
|
||||
item.setTargetUrl("/subpackages/admin/exception?status=" + status);
|
||||
return item;
|
||||
}
|
||||
|
||||
private String buildExceptionDesc(ExceptionDisposal disposal) {
|
||||
String reporter = Func.toStr(disposal.getReporterName(), "司机");
|
||||
String type = Func.toStr(disposal.getExceptionType(), "异常");
|
||||
String reason = Func.isNotBlank(disposal.getExceptionReason())
|
||||
? disposal.getExceptionReason()
|
||||
: Func.toStr(disposal.getReportDescription(), "");
|
||||
if (Func.isBlank(reason)) {
|
||||
return reporter + "上报" + type;
|
||||
}
|
||||
String text = reporter + "上报" + type + ":" + reason.trim();
|
||||
return text.length() > 80 ? text.substring(0, 80) + "…" : text;
|
||||
}
|
||||
|
||||
private String resolveUserName() {
|
||||
String realName = UserCache.getUserRealName(AuthUtil.getUserId());
|
||||
if (Func.isNotBlank(realName)) {
|
||||
return realName;
|
||||
}
|
||||
return Func.toStr(AuthUtil.getUserName(), "");
|
||||
}
|
||||
|
||||
private String formatTimeAgo(LocalDateTime time) {
|
||||
if (time == null) {
|
||||
return "";
|
||||
}
|
||||
Duration duration = Duration.between(time, LocalDateTime.now());
|
||||
if (duration.isNegative()) {
|
||||
duration = Duration.ZERO;
|
||||
}
|
||||
long minutes = duration.toMinutes();
|
||||
if (minutes < 1) {
|
||||
return "刚刚";
|
||||
}
|
||||
if (minutes < 60) {
|
||||
return minutes + "分钟";
|
||||
}
|
||||
long hours = duration.toHours();
|
||||
if (hours < 24) {
|
||||
return hours + "小时";
|
||||
}
|
||||
long days = duration.toDays();
|
||||
if (days < 30) {
|
||||
return days + "天";
|
||||
}
|
||||
return time.format(DATE_MD);
|
||||
}
|
||||
|
||||
private LocalDateTime toLocalDateTime(Date date) {
|
||||
if (date == null) {
|
||||
return null;
|
||||
}
|
||||
return date.toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDateTime();
|
||||
}
|
||||
|
||||
}
|
||||
+1
-1
@@ -572,6 +572,6 @@ public class MasterOrderServiceImpl extends BaseServiceImpl<MasterOrderMapper, M
|
||||
private synchronized String nextCode() {
|
||||
String prefix = "DL-" + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE) + "-";
|
||||
int serial = baseMapper.selectMaxSerial(prefix) + 1;
|
||||
return prefix + String.format("%04d", serial);
|
||||
return prefix + String.format("%03d", serial);
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -75,6 +75,7 @@ public class OtherExpenseRecordServiceImpl extends BaseServiceImpl<OtherExpenseR
|
||||
OtherExpenseRecordExcel excel = data.get(index);
|
||||
try {
|
||||
OtherExpenseRecord otherExpenseRecord = Objects.requireNonNull(BeanUtil.copyProperties(excel, OtherExpenseRecord.class));
|
||||
otherExpenseRecord.setExpenseDate(org.springblade.common.excel.LenientDateParser.parseDate(excel.getExpenseDate(), "费用日期"));
|
||||
otherExpenseRecord.setDataSource("批量导入");
|
||||
prepare(otherExpenseRecord);
|
||||
List<String> validationErrors = validateImportOtherExpenseRecord(otherExpenseRecord);
|
||||
|
||||
+57
-21
@@ -182,15 +182,26 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
||||
public PaymentApplicationReferenceAmountVO referenceAmount(String paymentType, Long referenceId, Long excludeId) {
|
||||
if (referenceId == null) throw new ServiceException("结算单不能为空");
|
||||
BigDecimal settlementAmount;
|
||||
boolean preSettlementReference = false;
|
||||
if (TransportBusiConstant.PAYMENT_TYPE_SETTLEMENT.equals(paymentType)) {
|
||||
FormalSettlement settlement = formalSettlementMapper.selectById(referenceId);
|
||||
if (settlement == null || Objects.equals(settlement.getIsDeleted(), 1)) {
|
||||
throw new ServiceException("正式结算单不存在");
|
||||
FormalSettlement formalSettlement = formalSettlementMapper.selectById(referenceId);
|
||||
if (formalSettlement != null && !Objects.equals(formalSettlement.getIsDeleted(), 1)) {
|
||||
if (!TransportBusiConstant.APPROVED.equals(formalSettlement.getApprovalStatus())) {
|
||||
throw new ServiceException("只能选择审批通过、未作废的正式结算单");
|
||||
}
|
||||
settlementAmount = money(formalSettlement.getSettlementAmount());
|
||||
} else {
|
||||
// 尾款付款也允许关联预结算单
|
||||
PreSettlement preSettlement = preSettlementMapper.selectById(referenceId);
|
||||
if (preSettlement == null || Objects.equals(preSettlement.getIsDeleted(), 1)) {
|
||||
throw new ServiceException("结算单不存在");
|
||||
}
|
||||
if (!TransportBusiConstant.APPROVED.equals(preSettlement.getApprovalStatus())) {
|
||||
throw new ServiceException("只能选择审批通过、未作废的预结算单");
|
||||
}
|
||||
settlementAmount = money(preSettlement.getSettlementAmount());
|
||||
preSettlementReference = true;
|
||||
}
|
||||
if (!TransportBusiConstant.APPROVED.equals(settlement.getApprovalStatus())) {
|
||||
throw new ServiceException("只能选择审批通过、未作废的正式结算单");
|
||||
}
|
||||
settlementAmount = money(settlement.getSettlementAmount());
|
||||
} else if ("progress_advance".equals(paymentType)) {
|
||||
PreSettlement settlement = preSettlementMapper.selectById(referenceId);
|
||||
if (settlement == null || Objects.equals(settlement.getIsDeleted(), 1)) {
|
||||
@@ -200,10 +211,13 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
||||
throw new ServiceException("只能选择审批通过、未作废的预结算单");
|
||||
}
|
||||
settlementAmount = money(settlement.getSettlementAmount());
|
||||
preSettlementReference = true;
|
||||
} else {
|
||||
throw new ServiceException("付款类型不支持关联结算单");
|
||||
}
|
||||
BigDecimal cumulativeAppliedAmount = cumulativeAppliedAmount(paymentType, referenceId, excludeId);
|
||||
BigDecimal cumulativeAppliedAmount = preSettlementReference
|
||||
? cumulativePreSettlementApplied(referenceId, excludeId)
|
||||
: cumulativeFormalSettlementApplied(referenceId, excludeId);
|
||||
PaymentApplicationReferenceAmountVO amount = new PaymentApplicationReferenceAmountVO();
|
||||
amount.setSettlementAmount(settlementAmount);
|
||||
amount.setCumulativeAppliedAmount(cumulativeAppliedAmount);
|
||||
@@ -297,6 +311,9 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
||||
if (!List.of(TransportBusiConstant.DRAFT, TransportBusiConstant.RETURNED).contains(entity.getApprovalStatus())) {
|
||||
throw new ServiceException("当前状态不允许提交");
|
||||
}
|
||||
if (entity.getAppliedAmount() == null || entity.getAppliedAmount().compareTo(BigDecimal.ZERO) <= 0) {
|
||||
throw new ServiceException("申请付款金额必须大于0");
|
||||
}
|
||||
validateNoTailPayment(entity);
|
||||
validateProjectAdvanceContract(entity);
|
||||
validateSelectedBill(entity, false);
|
||||
@@ -1155,7 +1172,8 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
||||
}
|
||||
|
||||
private void syncPreSettlementAdvance(PaymentApplication entity) {
|
||||
if (!"progress_advance".equals(entity.getPaymentType()) || entity.getPreSettlementId() == null) return;
|
||||
if (entity.getPreSettlementId() == null) return;
|
||||
if (!List.of("progress_advance", "settlement_payment").contains(entity.getPaymentType())) return;
|
||||
PreSettlementAdvance advance = preSettlementAdvanceMapper.selectList(
|
||||
Wrappers.<PreSettlementAdvance>lambdaQuery()
|
||||
.eq(PreSettlementAdvance::getPreSettlementId, entity.getPreSettlementId())
|
||||
@@ -1279,13 +1297,15 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
||||
if (Func.isEmpty(request.getPaymentMethod())) throw new ServiceException("付款方式不能为空");
|
||||
if (isBillPayment(request.getPaymentMethod()) && request.getBillLedgerId() == null) throw new ServiceException("汇票付款必须选择汇票台账");
|
||||
if (request.getPaymentRatio() != null && (request.getPaymentRatio().compareTo(BigDecimal.ZERO) < 0 || request.getPaymentRatio().compareTo(BigDecimal.valueOf(100)) > 0)) throw new ServiceException("付款比例必须在0-100之间");
|
||||
if (request.getAppliedAmount() == null || request.getAppliedAmount().compareTo(BigDecimal.ZERO) < 0) throw new ServiceException("申请付款金额不能小于0");
|
||||
if (request.getAppliedAmount() == null || request.getAppliedAmount().compareTo(BigDecimal.ZERO) <= 0) throw new ServiceException("申请付款金额必须大于0");
|
||||
if (!"project_advance".equals(request.getPaymentType()) && request.getSettlementId() == null
|
||||
&& request.getPreSettlementId() == null && Func.isEmpty(request.getSettlementIds())) throw new ServiceException("非项目预付必须关联结算单");
|
||||
if ("project_advance".equals(request.getPaymentType()) && request.getProjectId() == null) throw new ServiceException("项目预付必须选择所属项目");
|
||||
if ("progress_advance".equals(request.getPaymentType()) && request.getPreSettlementId() == null) throw new ServiceException("进度预付必须关联预结算单");
|
||||
if (TransportBusiConstant.PAYMENT_TYPE_SETTLEMENT.equals(request.getPaymentType()) && request.getSettlementId() == null
|
||||
&& Func.isEmpty(request.getSettlementIds())) throw new ServiceException("结算付款必须关联正式结算单");
|
||||
&& Func.isEmpty(request.getSettlementIds()) && request.getPreSettlementId() == null) {
|
||||
throw new ServiceException("尾款付款必须关联正式结算单或预结算单");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateInvoice(PaymentApplicationInvoiceRequest invoice) {
|
||||
@@ -1351,12 +1371,12 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
||||
if (source == null) throw new ServiceException("正式结算单不存在");
|
||||
if (!TransportBusiConstant.APPROVED.equals(source.getApprovalStatus())) throw new ServiceException("只能选择审批通过、未作废的正式结算单");
|
||||
entity.setSettlementId(source.getId()); entity.setSettlementNo(source.getFormalSettlementNo()); entity.setPreSettlementId(null); entity.setPreSettlementNo(null);
|
||||
entity.setProjectId(source.getProjectId()); entity.setProjectName(source.getProjectName()); entity.setDeptId(source.getDeptId()); entity.setDeptName(source.getDeptName()); entity.setContractId(source.getContractId()); entity.setContractNo(source.getContractNo()); entity.setContractName(source.getContractName()); entity.setPayerName(source.getPayerName()); entity.setPayeeName(source.getPayeeName()); entity.setSettlementAmount(source.getSettlementAmount()); entity.setPayableAmount(money(source.getSettlementAmount()).subtract(cumulativeAppliedAmount(request.getPaymentType(), source.getId(), entity.getId()))); entity.setBillType("正式结算单");
|
||||
entity.setProjectId(source.getProjectId()); entity.setProjectName(source.getProjectName()); entity.setDeptId(source.getDeptId()); entity.setDeptName(source.getDeptName()); entity.setContractId(source.getContractId()); entity.setContractNo(source.getContractNo()); entity.setContractName(source.getContractName()); entity.setPayerName(source.getPayerName()); entity.setPayeeName(source.getPayeeName()); entity.setSettlementAmount(source.getSettlementAmount()); entity.setPayableAmount(money(source.getSettlementAmount()).subtract(cumulativeFormalSettlementApplied(source.getId(), entity.getId()))); entity.setBillType("正式结算单");
|
||||
} else if (request.getPreSettlementId() != null) {
|
||||
PreSettlement source = preSettlementMapper.selectById(request.getPreSettlementId());
|
||||
if (source == null) throw new ServiceException("预结算单不存在");
|
||||
if (!TransportBusiConstant.APPROVED.equals(source.getApprovalStatus())) throw new ServiceException("只能选择审批通过、未作废的预结算单");
|
||||
entity.setPreSettlementId(source.getId()); entity.setPreSettlementNo(source.getPreSettlementNo()); entity.setSettlementId(null); entity.setSettlementNo(null); entity.setProjectId(source.getProjectId()); entity.setProjectName(source.getProjectName()); entity.setDeptId(source.getDeptId()); entity.setDeptName(source.getDeptName()); entity.setContractId(source.getContractId()); entity.setContractNo(source.getContractNo()); entity.setContractName(source.getContractName()); entity.setPayerName(source.getPayerName()); entity.setPayeeName(source.getPayeeName()); entity.setSettlementAmount(source.getSettlementAmount()); entity.setPayableAmount(money(source.getSettlementAmount()).subtract(cumulativeAppliedAmount(request.getPaymentType(), source.getId(), entity.getId()))); entity.setBillType("预结算单");
|
||||
entity.setPreSettlementId(source.getId()); entity.setPreSettlementNo(source.getPreSettlementNo()); entity.setSettlementId(null); entity.setSettlementNo(null); entity.setProjectId(source.getProjectId()); entity.setProjectName(source.getProjectName()); entity.setDeptId(source.getDeptId()); entity.setDeptName(source.getDeptName()); entity.setContractId(source.getContractId()); entity.setContractNo(source.getContractNo()); entity.setContractName(source.getContractName()); entity.setPayerName(source.getPayerName()); entity.setPayeeName(source.getPayeeName()); entity.setSettlementAmount(source.getSettlementAmount()); entity.setPayableAmount(money(source.getSettlementAmount()).subtract(cumulativePreSettlementApplied(source.getId(), entity.getId()))); entity.setBillType("预结算单");
|
||||
} else {
|
||||
entity.setProjectId(request.getProjectId()); entity.setProjectName(request.getProjectName()); entity.setDeptId(request.getDeptId()); entity.setDeptName(request.getDeptName()); entity.setContractId(request.getContractId()); entity.setContractNo(request.getContractNo()); entity.setContractName(request.getContractName()); entity.setPayerName(request.getPayerName()); entity.setPayeeName(request.getPayeeName()); entity.setSettlementAmount(request.getSettlementAmount()); entity.setPayableAmount(request.getPayableAmount()); entity.setBillType(request.getBillType());
|
||||
}
|
||||
@@ -1370,6 +1390,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
||||
List<Long> ids = request.getSettlementIds() == null ? List.of() : request.getSettlementIds();
|
||||
if (ids.isEmpty() && request.getSettlementId() != null) ids = List.of(request.getSettlementId());
|
||||
ids = ids.stream().filter(Objects::nonNull).distinct().toList();
|
||||
if (ids.isEmpty()) return List.of();
|
||||
List<FormalSettlement> settlements = ids.stream().map(id -> formalSettlementMapper.selectById(id)).toList();
|
||||
if (settlements.stream().anyMatch(item -> item == null || Objects.equals(item.getIsDeleted(), 1)
|
||||
|| !TransportBusiConstant.APPROVED.equals(item.getApprovalStatus()) || !"payable".equals(item.getSettlementType()))) {
|
||||
@@ -1391,7 +1412,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
||||
BigDecimal settlementAmount = settlements.stream().map(FormalSettlement::getSettlementAmount)
|
||||
.map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
BigDecimal payableAmount = settlements.stream()
|
||||
.map(item -> money(item.getSettlementAmount()).subtract(cumulativeAppliedAmount(TransportBusiConstant.PAYMENT_TYPE_SETTLEMENT, item.getId(), entity.getId())).max(BigDecimal.ZERO))
|
||||
.map(item -> money(item.getSettlementAmount()).subtract(cumulativeFormalSettlementApplied(item.getId(), entity.getId())).max(BigDecimal.ZERO))
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
if (money(entity.getAppliedAmount()).compareTo(payableAmount) > 0) throw new ServiceException("申请付款金额不能超过所选结算单可付款金额合计");
|
||||
FormalSettlement first = settlements.get(0);
|
||||
@@ -1406,14 +1427,14 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
||||
settlementRelationMapper.delete(Wrappers.<PaymentApplicationSettlement>lambdaQuery()
|
||||
.eq(PaymentApplicationSettlement::getPaymentApplicationId, entity.getId()));
|
||||
BigDecimal totalBase = settlements.stream().map(item -> money(item.getSettlementAmount()).subtract(
|
||||
cumulativeAppliedAmount(TransportBusiConstant.PAYMENT_TYPE_SETTLEMENT, item.getId(), entity.getId())).max(BigDecimal.ZERO))
|
||||
cumulativeFormalSettlementApplied(item.getId(), entity.getId())).max(BigDecimal.ZERO))
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
BigDecimal remainingApplied = money(entity.getAppliedAmount());
|
||||
BigDecimal remainingPaid = money(entity.getPaidAmount());
|
||||
for (int index = 0; index < settlements.size(); index++) {
|
||||
FormalSettlement settlement = settlements.get(index);
|
||||
BigDecimal base = money(settlement.getSettlementAmount()).subtract(
|
||||
cumulativeAppliedAmount(TransportBusiConstant.PAYMENT_TYPE_SETTLEMENT, settlement.getId(), entity.getId())).max(BigDecimal.ZERO);
|
||||
cumulativeFormalSettlementApplied(settlement.getId(), entity.getId())).max(BigDecimal.ZERO);
|
||||
BigDecimal applied = index == settlements.size() - 1 ? remainingApplied
|
||||
: money(entity.getAppliedAmount()).multiply(base).divide(totalBase, 2, RoundingMode.DOWN);
|
||||
BigDecimal paid = index == settlements.size() - 1 ? remainingPaid
|
||||
@@ -1468,11 +1489,26 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
|
||||
}
|
||||
|
||||
private BigDecimal cumulativeAppliedAmount(String paymentType, Long referenceId, Long excludeId) {
|
||||
LambdaQueryWrapper<PaymentApplication> wrapper = Wrappers.<PaymentApplication>lambdaQuery()
|
||||
.eq(TransportBusiConstant.PAYMENT_TYPE_SETTLEMENT.equals(paymentType), PaymentApplication::getSettlementId, referenceId)
|
||||
.eq("progress_advance".equals(paymentType), PaymentApplication::getPreSettlementId, referenceId)
|
||||
.ne(excludeId != null, PaymentApplication::getId, excludeId);
|
||||
return sumApplied(wrapper);
|
||||
if ("progress_advance".equals(paymentType)) {
|
||||
return cumulativePreSettlementApplied(referenceId, excludeId);
|
||||
}
|
||||
if ("settlement_payment".equals(paymentType)) {
|
||||
return cumulativeFormalSettlementApplied(referenceId, excludeId);
|
||||
}
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
private BigDecimal cumulativeFormalSettlementApplied(Long settlementId, Long excludeId) {
|
||||
return sumApplied(Wrappers.<PaymentApplication>lambdaQuery()
|
||||
.eq(PaymentApplication::getSettlementId, settlementId)
|
||||
.ne(excludeId != null, PaymentApplication::getId, excludeId));
|
||||
}
|
||||
|
||||
private BigDecimal cumulativePreSettlementApplied(Long preSettlementId, Long excludeId) {
|
||||
// 进度预付、尾款付款关联同一预结算单时累计占用可付款额度
|
||||
return sumApplied(Wrappers.<PaymentApplication>lambdaQuery()
|
||||
.eq(PaymentApplication::getPreSettlementId, preSettlementId)
|
||||
.ne(excludeId != null, PaymentApplication::getId, excludeId));
|
||||
}
|
||||
|
||||
private void fillBillLedger(PaymentApplication entity, PaymentApplicationSaveRequest request) {
|
||||
|
||||
+1
-1
@@ -599,7 +599,7 @@ public class PreSettlementServiceImpl extends BaseServiceImpl<PreSettlementMappe
|
||||
if (Func.isEmpty(request.getRows())) {
|
||||
throw new ServiceException("请填写需要调整的费用行");
|
||||
}
|
||||
String changeReason = requiredText(limitRemark(request.getChangeReason(), 200), "调整原因");
|
||||
String changeReason = limitRemark(request.getChangeReason(), 200);
|
||||
Map<Long, PreSettlementDetailFee> existingMap = detailFees(detail.getId()).stream()
|
||||
.collect(Collectors.toMap(PreSettlementDetailFee::getId, Function.identity()));
|
||||
if (request.getRows().size() != existingMap.size()) {
|
||||
|
||||
+70
-3
@@ -26,9 +26,11 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
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.tenant.annotation.TenantIgnore;
|
||||
import org.springblade.core.tool.jackson.JsonUtil;
|
||||
import org.springblade.core.tool.utils.BeanUtil;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
@@ -70,6 +72,7 @@ import java.util.stream.Collectors;
|
||||
* @author Chill
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class ProjectApplyServiceImpl extends BaseServiceImpl<ProjectApplyMapper, ProjectApply> implements IProjectApplyService {
|
||||
|
||||
@@ -109,6 +112,16 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl<ProjectApplyMapper,
|
||||
return projectApplyVO;
|
||||
}
|
||||
|
||||
@Override
|
||||
@TenantIgnore
|
||||
public ProjectApplyVO publicDetail(Long id) {
|
||||
ProjectApplyVO projectApplyVO = ProjectApplyWrapper.build().entityVO(loadExists(id));
|
||||
normalizeOptionalFields(projectApplyVO);
|
||||
projectApplyVO.setReadonly(true);
|
||||
fillPublicDictOptions(projectApplyVO);
|
||||
return projectApplyVO;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Integer> fundRiskStats(ProjectApplyVO projectApply) {
|
||||
projectApply.setFundUseRisk(null);
|
||||
@@ -244,14 +257,12 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl<ProjectApplyMapper,
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean saveChange(ProjectApplyVO projectApply) {
|
||||
// 变更草稿仅更新业务字段,不生成变更记录;变更记录在 submitChange 时落库
|
||||
ProjectApply oldRecord = loadChangeable(projectApply.getId());
|
||||
Map<String, Object> beforeData = buildProjectChangeSnapshot(oldRecord);
|
||||
prepare(projectApply);
|
||||
copyChangeFields(oldRecord, projectApply);
|
||||
validateSubmit(oldRecord);
|
||||
validateChangeLength(oldRecord);
|
||||
Map<String, Object> afterData = buildProjectChangeSnapshot(oldRecord);
|
||||
appendProjectChangeRecord(oldRecord, CHANGE_TYPE_PROJECT, oldRecord.getChangeReason(), "saved", "已保存", beforeData, afterData);
|
||||
return updateById(oldRecord);
|
||||
}
|
||||
|
||||
@@ -878,6 +889,62 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl<ProjectApplyMapper,
|
||||
projectApplyVO.setReadonly(!canCurrentUserOperate(projectApplyVO));
|
||||
}
|
||||
|
||||
/**
|
||||
* 公开页没有登录态,下拉字典接口无法访问,这里按项目租户把文案一并返回。
|
||||
*/
|
||||
private void fillPublicDictOptions(ProjectApplyVO projectApplyVO) {
|
||||
try {
|
||||
projectApplyVO.setCargoTypeOptions(loadSystemDictOptions("type_of_goods"));
|
||||
projectApplyVO.setTransportTypeOptions(loadBizDictOptions(projectApplyVO.getTenantId(), "transport_type"));
|
||||
projectApplyVO.setSettlementModeOptions(loadBizDictOptions(projectApplyVO.getTenantId(), "settle_method"));
|
||||
} catch (Exception exception) {
|
||||
log.warn("公开项目详情字典加载失败, id={}", projectApplyVO.getId(), exception);
|
||||
}
|
||||
}
|
||||
|
||||
private List<Map<String, String>> loadSystemDictOptions(String code) {
|
||||
return toDictOptions(baseMapper.selectDictItems(code));
|
||||
}
|
||||
|
||||
private List<Map<String, String>> loadBizDictOptions(String tenantId, String code) {
|
||||
if (Func.isEmpty(tenantId)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
return toDictOptions(baseMapper.selectDictBizItems(tenantId, code));
|
||||
}
|
||||
|
||||
private List<Map<String, String>> toDictOptions(List<Map<String, Object>> rows) {
|
||||
List<Map<String, String>> options = new ArrayList<>();
|
||||
if (rows == null) {
|
||||
return options;
|
||||
}
|
||||
for (Map<String, Object> row : rows) {
|
||||
String dictKey = dictColumn(row, "dictKey", "dictkey", "dict_key");
|
||||
if (Func.isEmpty(dictKey) || "-1".equals(dictKey)) {
|
||||
continue;
|
||||
}
|
||||
String dictValue = dictColumn(row, "dictValue", "dictvalue", "dict_value");
|
||||
Map<String, String> option = new LinkedHashMap<>();
|
||||
option.put("value", dictKey);
|
||||
option.put("label", Func.isEmpty(dictValue) ? dictKey : dictValue);
|
||||
options.add(option);
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
private String dictColumn(Map<String, Object> row, String... names) {
|
||||
if (row == null) {
|
||||
return "";
|
||||
}
|
||||
for (String name : names) {
|
||||
Object value = row.get(name);
|
||||
if (value != null && Func.isNotEmpty(String.valueOf(value))) {
|
||||
return String.valueOf(value);
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private boolean canCurrentUserOperate(ProjectApply projectApply) {
|
||||
if (AuthUtil.isAdministrator()) {
|
||||
return true;
|
||||
|
||||
+381
-20
@@ -198,6 +198,209 @@ public class ReceivablePayableDetailServiceImpl
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void syncMileageFromWaybill(Long waybillId, BigDecimal mileage) {
|
||||
if (waybillId == null || mileage == null) {
|
||||
return;
|
||||
}
|
||||
Waybill waybill = waybillService.getById(waybillId);
|
||||
if (waybill == null || Objects.equals(waybill.getIsDeleted(), 1)) {
|
||||
return;
|
||||
}
|
||||
BigDecimal normalizedMileage = normalizeGeneratedMileage(mileage);
|
||||
Set<Long> detailIds = new LinkedHashSet<>();
|
||||
list(Wrappers.<ReceivablePayableDetail>lambdaQuery()
|
||||
.eq(ReceivablePayableDetail::getIsDeleted, 0)
|
||||
.eq(ReceivablePayableDetail::getWaybillId, waybillId)
|
||||
.eq(ReceivablePayableDetail::getSettlementStatus, "pending")
|
||||
.and(wrapper -> wrapper.isNull(ReceivablePayableDetail::getPreSettlementNo)
|
||||
.or().eq(ReceivablePayableDetail::getPreSettlementNo, ""))
|
||||
.and(wrapper -> wrapper.isNull(ReceivablePayableDetail::getFormalSettlementNo)
|
||||
.or().eq(ReceivablePayableDetail::getFormalSettlementNo, "")))
|
||||
.forEach(detail -> detailIds.add(detail.getId()));
|
||||
List<ReceivablePayableCargoFee> relatedFees = cargoFeeMapper.selectList(
|
||||
Wrappers.<ReceivablePayableCargoFee>lambdaQuery()
|
||||
.eq(ReceivablePayableCargoFee::getIsDeleted, 0)
|
||||
.eq(ReceivablePayableCargoFee::getWaybillId, waybillId));
|
||||
relatedFees.stream()
|
||||
.map(ReceivablePayableCargoFee::getDetailId)
|
||||
.filter(Objects::nonNull)
|
||||
.forEach(detailIds::add);
|
||||
if (detailIds.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
for (Long detailId : detailIds) {
|
||||
ReceivablePayableDetail detail = getById(detailId);
|
||||
if (detail == null || Objects.equals(detail.getIsDeleted(), 1)
|
||||
|| !"pending".equals(detail.getSettlementStatus())
|
||||
|| Func.isNotEmpty(detail.getPreSettlementNo())
|
||||
|| Func.isNotEmpty(detail.getFormalSettlementNo())) {
|
||||
continue;
|
||||
}
|
||||
syncDetailMileage(detail, waybill, normalizedMileage);
|
||||
}
|
||||
}
|
||||
|
||||
private void syncDetailMileage(ReceivablePayableDetail detail, Waybill waybill, BigDecimal mileage) {
|
||||
List<ReceivablePayableCargoFee> rows = activeCargoFees(detail.getId());
|
||||
if (rows.isEmpty()) {
|
||||
if (Objects.equals(detail.getWaybillId(), waybill.getId())) {
|
||||
detail.setMileage(mileage);
|
||||
updateById(detail);
|
||||
}
|
||||
return;
|
||||
}
|
||||
List<String> changes = new ArrayList<>();
|
||||
boolean feeChanged = false;
|
||||
for (ReceivablePayableCargoFee fee : rows) {
|
||||
boolean related = Objects.equals(fee.getWaybillId(), waybill.getId())
|
||||
|| (fee.getWaybillId() == null && Objects.equals(detail.getWaybillId(), waybill.getId()));
|
||||
if (!related) {
|
||||
continue;
|
||||
}
|
||||
BigDecimal oldMileage = normalizeGeneratedMileage(fee.getMileage());
|
||||
fee.setMileage(mileage);
|
||||
if (isMileageBasedElement(fee.getBillingFactor()) && !isManualFee(fee)
|
||||
&& recalculateMileageBasedFee(detail, fee, waybill, oldMileage)) {
|
||||
changes.add("【运单维护里程】按" + fee.getBillingFactor() + "重新计算费用");
|
||||
}
|
||||
cargoFeeMapper.updateById(fee);
|
||||
feeChanged = true;
|
||||
appendChange(changes, "里程", oldMileage, mileage);
|
||||
}
|
||||
if (!feeChanged && !Objects.equals(detail.getWaybillId(), waybill.getId())) {
|
||||
return;
|
||||
}
|
||||
refreshAdjustedDetail(detail, activeCargoFees(detail.getId()));
|
||||
if (Objects.equals(detail.getWaybillId(), waybill.getId())) {
|
||||
detail.setMileage(mileage);
|
||||
updateById(detail);
|
||||
}
|
||||
if (!changes.isEmpty()) {
|
||||
saveChangeRecord(detail, String.join(";", changes), "运单维护里程同步", "0001");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 按最新运单里程重算「按里程 / 按吨·公里」费用行。
|
||||
*/
|
||||
private boolean recalculateMileageBasedFee(ReceivablePayableDetail detail, ReceivablePayableCargoFee fee,
|
||||
Waybill sourceWaybill, BigDecimal previousMileage) {
|
||||
Waybill calcWaybill = waybillForMileageRecalc(sourceWaybill, fee, previousMileage);
|
||||
List<Map<String, Object>> rules = parseList(fee.getBillingRulesJson());
|
||||
if (rules.isEmpty()) {
|
||||
ContractManage contract = contractManageService.getById(detail.getContractId());
|
||||
if (contract != null) {
|
||||
rules = matchingAdjustedRules(contract, fee, calcWaybill);
|
||||
if (!rules.isEmpty()) {
|
||||
fee.setBillingRulesJson(JsonUtil.toJson(rules));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (rules.isEmpty()) {
|
||||
log.warn("运单维护里程重算跳过:未找到计费规则,detailId={}, feeId={}", detail.getId(), fee.getId());
|
||||
return false;
|
||||
}
|
||||
Map<String, BigDecimal> calculatedAmounts = new LinkedHashMap<>();
|
||||
BigDecimal calculatedFreight = BigDecimal.ZERO;
|
||||
boolean freightRuleMatched = false;
|
||||
Map<String, Object> measureRule = null;
|
||||
for (Map<String, Object> rule : rules) {
|
||||
BigDecimal amount = calculateRule(rule, calcWaybill);
|
||||
if (amount == null) {
|
||||
continue;
|
||||
}
|
||||
String feeItem = stringValue(rule, "feeItem", "费用");
|
||||
calculatedAmounts.merge(feeItem, amount, BigDecimal::add);
|
||||
if (measureRule == null || isFreightRule(rule)) {
|
||||
measureRule = rule;
|
||||
}
|
||||
if (isFreightRule(rule)) {
|
||||
calculatedFreight = calculatedFreight.add(amount);
|
||||
freightRuleMatched = true;
|
||||
}
|
||||
}
|
||||
if (calculatedAmounts.isEmpty() || measureRule == null) {
|
||||
log.warn("运单维护里程重算跳过:计费规则无法试算,detailId={}, feeId={}", detail.getId(), fee.getId());
|
||||
return false;
|
||||
}
|
||||
Map<String, BigDecimal> feeItems = new LinkedHashMap<>();
|
||||
parseMap(fee.getFeeItemsJson()).forEach((name, value) -> feeItems.put(name, decimal(value)));
|
||||
calculatedAmounts.forEach(feeItems::put);
|
||||
fee.setTransportQuantity(resolveStoredTransportQuantity(measureRule, calcWaybill));
|
||||
fee.setUnitPrice(resolveCalculatedUnitPrice(measureRule, calcWaybill));
|
||||
fee.setBillingFactor(stringValue(measureRule, "billingElement", fee.getBillingFactor()));
|
||||
fee.setBillingType(stringValue(measureRule, "billingType", fee.getBillingType()));
|
||||
fee.setPriceUnit(stringValue(measureRule, "billingUnit", fee.getPriceUnit()));
|
||||
if (freightRuleMatched) {
|
||||
fee.setFreightAmount(calculatedFreight);
|
||||
}
|
||||
fee.setFeeItemsJson(JsonUtil.toJson(feeItems));
|
||||
BigDecimal afterAmount = adjustedAfterAmount(money(fee.getFreightAmount()), feeItems);
|
||||
fee.setAdjustAmount(afterAmount.subtract(money(fee.getOriginalAmount())));
|
||||
fee.setAfterAmount(afterAmount);
|
||||
return true;
|
||||
}
|
||||
|
||||
private Waybill waybillForMileageRecalc(Waybill sourceWaybill, ReceivablePayableCargoFee fee,
|
||||
BigDecimal previousMileage) {
|
||||
Waybill calcWaybill = Objects.requireNonNull(BeanUtil.copyProperties(sourceWaybill, Waybill.class));
|
||||
calcWaybill.setMileage(sourceWaybill.getMileage());
|
||||
String element = fee.getBillingFactor();
|
||||
if (!"按吨·公里".equals(element) && !"按重量".equals(element)
|
||||
&& !"按数量".equals(element) && !"按体积".equals(element)) {
|
||||
return calcWaybill;
|
||||
}
|
||||
List<Map<String, Object>> matchedGoods = parseList(sourceWaybill.getGoodsJson()).stream()
|
||||
.filter(goods -> matchesFeeCargo(goods, fee))
|
||||
.toList();
|
||||
if (!matchedGoods.isEmpty()) {
|
||||
Map<String, Object> goods = matchedGoods.get(0);
|
||||
calcWaybill.setGoodsJson(JsonUtil.toJson(List.of(goods)));
|
||||
calcWaybill.setCargoName(stringValue(goods, "cargoName", fee.getCargoName()));
|
||||
calcWaybill.setCargoType(stringValue(goods, "cargoType", fee.getCargoType()));
|
||||
calcWaybill.setSpecification(stringValue(goods, "specification", fee.getSpecification()));
|
||||
calcWaybill.setModel(stringValue(goods, "model", fee.getModel()));
|
||||
calcWaybill.setQuantity(decimal(goods.get("quantity")));
|
||||
calcWaybill.setQuantityUnit(stringValue(goods, "quantityUnit", fee.getQuantityUnit()));
|
||||
return calcWaybill;
|
||||
}
|
||||
Map<String, Object> goods = new LinkedHashMap<>();
|
||||
goods.put("cargoName", fee.getCargoName());
|
||||
goods.put("cargoType", fee.getCargoType());
|
||||
goods.put("specification", fee.getSpecification());
|
||||
goods.put("model", fee.getModel());
|
||||
goods.put("quantityUnit", fee.getQuantityUnit());
|
||||
BigDecimal quantity = money(fee.getTransportQuantity());
|
||||
if ("按吨·公里".equals(element)) {
|
||||
BigDecimal baseMileage = money(previousMileage);
|
||||
if (baseMileage.signum() > 0) {
|
||||
quantity = quantity.divide(baseMileage, 8, RoundingMode.HALF_UP);
|
||||
}
|
||||
}
|
||||
goods.put("quantity", quantity);
|
||||
if ("按体积".equals(element)) {
|
||||
goods.put("volume", quantity);
|
||||
}
|
||||
calcWaybill.setGoodsJson(JsonUtil.toJson(List.of(goods)));
|
||||
calcWaybill.setCargoName(fee.getCargoName());
|
||||
calcWaybill.setCargoType(fee.getCargoType());
|
||||
calcWaybill.setSpecification(fee.getSpecification());
|
||||
calcWaybill.setModel(fee.getModel());
|
||||
calcWaybill.setQuantity(quantity);
|
||||
calcWaybill.setQuantityUnit(fee.getQuantityUnit());
|
||||
return calcWaybill;
|
||||
}
|
||||
|
||||
private boolean matchesFeeCargo(Map<String, Object> goods, ReceivablePayableCargoFee fee) {
|
||||
return Objects.equals(stringValue(goods, "cargoName", ""), Objects.toString(fee.getCargoName(), ""))
|
||||
&& Objects.equals(stringValue(goods, "cargoType", ""), Objects.toString(fee.getCargoType(), ""))
|
||||
&& Objects.equals(stringValue(goods, "specification", ""), Objects.toString(fee.getSpecification(), ""))
|
||||
&& Objects.equals(stringValue(goods, "model", ""), Objects.toString(fee.getModel(), ""))
|
||||
&& Objects.equals(stringValue(goods, "quantityUnit", ""), Objects.toString(fee.getQuantityUnit(), ""));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReceivablePayableFeeDetailVO feeDetail(Long id) {
|
||||
ReceivablePayableDetail detail = getExisting(id);
|
||||
@@ -893,11 +1096,94 @@ public class ReceivablePayableDetailServiceImpl
|
||||
Map<String, Object> plan = resolveDefaultBillingPlan(plans, waybill.getTransportType());
|
||||
if (plan == null) return null;
|
||||
if (!(plan.get("rules") instanceof List<?> rules)) return null;
|
||||
boolean matched = rules.stream().anyMatch(value -> value instanceof Map<?, ?> raw && matchesRule(raw, waybill));
|
||||
boolean matched = rules.stream().anyMatch(value -> {
|
||||
if (!(value instanceof Map<?, ?> raw)) return false;
|
||||
Map<String, Object> rule = toRuleMap(raw);
|
||||
return feeWaybills(rule, waybill).stream()
|
||||
.anyMatch(feeWaybill -> matchesBillingRule(raw, rule, feeWaybill));
|
||||
});
|
||||
if (!matched) return null;
|
||||
return "__matched__";
|
||||
}
|
||||
|
||||
private Map<String, Object> toRuleMap(Map<?, ?> raw) {
|
||||
Map<String, Object> rule = new LinkedHashMap<>();
|
||||
raw.forEach((key, item) -> rule.put(String.valueOf(key), item));
|
||||
return rule;
|
||||
}
|
||||
|
||||
/**
|
||||
* 费用生成规则命中:
|
||||
* 1. 货物需满足匹配规则(始发/目的/货类/货名)
|
||||
* 2. 普通计费要素还要求货物数量单位与计费单位一致
|
||||
* 3. 按里程/按公里、按吨·公里/按吨公里、按车辆、固定金额不依赖货物数量单位,不校验数量单位
|
||||
*/
|
||||
private boolean matchesBillingRule(Map<?, ?> raw, Map<String, Object> rule, Waybill feeWaybill) {
|
||||
if (!matchesRule(raw, feeWaybill)) {
|
||||
return false;
|
||||
}
|
||||
String element = stringValue(rule, "billingElement", "");
|
||||
if (skipsQuantityUnitMatch(element)) {
|
||||
return true;
|
||||
}
|
||||
return matchesBillingUnit(rule, feeWaybill);
|
||||
}
|
||||
|
||||
private boolean isMileageBasedElement(String element) {
|
||||
String normalized = normalizeBillingElement(element);
|
||||
return "按里程".equals(normalized)
|
||||
|| "按公里".equals(normalized)
|
||||
|| "按吨公里".equals(normalized);
|
||||
}
|
||||
|
||||
/**
|
||||
* 不计货物数量单位的计费要素:里程类按运单里程计算;按车辆/固定金额按整单(量=1)计费。
|
||||
*/
|
||||
private boolean skipsQuantityUnitMatch(String element) {
|
||||
String normalized = normalizeBillingElement(element);
|
||||
return isMileageBasedElement(normalized)
|
||||
|| "按车辆".equals(normalized)
|
||||
|| "固定金额(整单一口价)".equals(normalized)
|
||||
|| "固定金额".equals(normalized);
|
||||
}
|
||||
|
||||
/** 统一计费要素文案,兼容「按公里 / 按吨公里 / 按吨·公里」等写法 */
|
||||
private String normalizeBillingElement(String element) {
|
||||
if (element == null) {
|
||||
return "";
|
||||
}
|
||||
return element.trim()
|
||||
.replace("·", "")
|
||||
.replace(".", "")
|
||||
.replace(".", "")
|
||||
.replace(" ", "");
|
||||
}
|
||||
|
||||
private boolean matchesBillingUnit(Map<String, Object> rule, Waybill feeWaybill) {
|
||||
String billingUnit = stringValue(rule, "billingUnit", "").trim();
|
||||
if (billingUnit.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
String quantityUnit = resolveQuantityUnit(feeWaybill);
|
||||
if (quantityUnit.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return billingUnit.equals(quantityUnit);
|
||||
}
|
||||
|
||||
private String resolveQuantityUnit(Waybill feeWaybill) {
|
||||
String quantityUnit = feeWaybill.getQuantityUnit() == null ? "" : feeWaybill.getQuantityUnit().trim();
|
||||
if (!quantityUnit.isEmpty()) {
|
||||
return quantityUnit;
|
||||
}
|
||||
List<Map<String, Object>> goods = parseList(feeWaybill.getGoodsJson());
|
||||
return goods.stream()
|
||||
.map(item -> stringValue(item, "quantityUnit", "").trim())
|
||||
.filter(Func::isNotEmpty)
|
||||
.findFirst()
|
||||
.orElse("");
|
||||
}
|
||||
|
||||
private boolean matchesRule(Map<?, ?> raw, Waybill waybill) {
|
||||
Object conditionValue = raw.get("matchCondition");
|
||||
if (!(conditionValue instanceof Map<?, ?> condition) || !hasConfiguredMatchCondition(condition)) return true;
|
||||
@@ -1180,7 +1466,7 @@ public class ReceivablePayableDetailServiceImpl
|
||||
detail.setTransportType(waybill.getTransportType());
|
||||
detail.setCargoName(waybill.getCargoName());
|
||||
detail.setCargoType(waybill.getCargoType());
|
||||
detail.setTransportQuantity(waybill.getQuantity());
|
||||
detail.setTransportQuantity(resolveWaybillTransportQuantity(waybill));
|
||||
detail.setQuantityUnit(waybill.getQuantityUnit());
|
||||
detail.setMileage(normalizeGeneratedMileage(waybill.getMileage()));
|
||||
detail.setBatchNo(waybill.getBatchNo());
|
||||
@@ -1194,6 +1480,21 @@ public class ReceivablePayableDetailServiceImpl
|
||||
return detail;
|
||||
}
|
||||
|
||||
/**
|
||||
* 多货物运单优先按 goodsJson 数量合计作为运输总量,避免主表 quantity 只落了首行。
|
||||
*/
|
||||
private BigDecimal resolveWaybillTransportQuantity(Waybill waybill) {
|
||||
List<Map<String, Object>> goods = parseList(waybill.getGoodsJson());
|
||||
if (goods.size() > 1) {
|
||||
BigDecimal total = goods.stream().map(item -> decimal(item.get("quantity")))
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
if (total.signum() > 0) {
|
||||
return total;
|
||||
}
|
||||
}
|
||||
return money(waybill.getQuantity());
|
||||
}
|
||||
|
||||
private ReceivablePayableDetail buildMasterOrderDetail(MasterOrder masterOrder, ContractManage contract,
|
||||
List<Waybill> masterGoods, String settlementType,
|
||||
List<ReceivablePayableCargoFee> fees, BigDecimal unitPrice) {
|
||||
@@ -1390,10 +1691,10 @@ public class ReceivablePayableDetailServiceImpl
|
||||
Set<List<String>> freightBillingCargoKeys = new LinkedHashSet<>();
|
||||
for (Object value : (List<?>) plan.get("rules")) {
|
||||
if (!(value instanceof Map<?, ?> raw)) continue;
|
||||
Map<String, Object> rule = new LinkedHashMap<>();
|
||||
raw.forEach((key, item) -> rule.put(String.valueOf(key), item));
|
||||
Map<String, Object> rule = toRuleMap(raw);
|
||||
for (Waybill feeWaybill : feeWaybills(rule, waybill)) {
|
||||
if (matchOnly && !matchesRule(raw, feeWaybill)) continue;
|
||||
// 统一生成:匹配规则内货物 + 数量单位一致;按里程/按吨·公里跳过单位匹配
|
||||
if (!matchesBillingRule(raw, rule, feeWaybill)) continue;
|
||||
BigDecimal amount = calculateRule(rule, feeWaybill);
|
||||
if (amount == null) continue;
|
||||
String feeItem = stringValue(rule, "feeItem", "费用");
|
||||
@@ -1449,13 +1750,25 @@ public class ReceivablePayableDetailServiceImpl
|
||||
|
||||
private void fillCalculatedBillingFields(ReceivablePayableCargoFee fee, Waybill feeWaybill,
|
||||
Map<String, Object> rule) {
|
||||
fee.setBillingFactor(stringValue(rule, "billingElement", ""));
|
||||
String element = stringValue(rule, "billingElement", "");
|
||||
fee.setBillingFactor(element);
|
||||
fee.setBillingType(stringValue(rule, "billingType", ""));
|
||||
fee.setTransportQuantity(measure(rule, feeWaybill));
|
||||
fee.setTransportQuantity(resolveStoredTransportQuantity(rule, feeWaybill));
|
||||
fee.setPriceUnit(stringValue(rule, "billingUnit", feeWaybill.getPriceUnit()));
|
||||
fee.setUnitPrice(resolveCalculatedUnitPrice(rule, feeWaybill));
|
||||
}
|
||||
|
||||
/**
|
||||
* 费用行运输量展示值:按车辆/固定金额的计费量仅试算用(为 1),不回写到运输量字段。
|
||||
*/
|
||||
private BigDecimal resolveStoredTransportQuantity(Map<String, Object> rule, Waybill waybill) {
|
||||
String element = stringValue(rule, "billingElement", "");
|
||||
if ("按车辆".equals(element) || "固定金额(整单一口价)".equals(element)) {
|
||||
return null;
|
||||
}
|
||||
return measure(rule, waybill);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析费用行展示用的实际命中单价。
|
||||
* 区间计费的规则默认单价仅用于兜底,费用行应展示当前计费量命中的区间单价。
|
||||
@@ -1548,22 +1861,68 @@ public class ReceivablePayableDetailServiceImpl
|
||||
|
||||
/**
|
||||
* 解析合同生效计费方案。
|
||||
* 合同存在勾选默认的计费方案时仅在默认方案内匹配计算,不再回退其他方案;
|
||||
* 全部方案均未勾选默认时仅使用最新添加的计费方案。
|
||||
* 仅使用与运单运输方式一致的计费方案;无匹配方案时返回 null,不生成应收应付。
|
||||
* 匹配方案中存在默认方案时取最新默认方案;否则取最新匹配方案。
|
||||
*/
|
||||
private Map<String, Object> resolveDefaultBillingPlan(List<Map<String, Object>> plans, String transportType) {
|
||||
if (plans.isEmpty()) return null;
|
||||
List<Map<String, Object>> defaultPlans = plans.stream().filter(this::isDefaultPlan).toList();
|
||||
if (defaultPlans.isEmpty()) return plans.get(plans.size() - 1);
|
||||
Optional<Map<String, Object>> matchedDefaultPlan = defaultPlans.stream()
|
||||
.filter(plan -> !isBlank(plan.get("transportMode")))
|
||||
.filter(plan -> matchesCondition(plan.get("transportMode"), transportType))
|
||||
.findFirst();
|
||||
if (matchedDefaultPlan.isPresent()) return matchedDefaultPlan.get();
|
||||
return defaultPlans.stream()
|
||||
.filter(plan -> isBlank(plan.get("transportMode")))
|
||||
.findFirst()
|
||||
.orElseGet(() -> defaultPlans.get(defaultPlans.size() - 1));
|
||||
List<Map<String, Object>> matchedPlans = plans.stream()
|
||||
.filter(plan -> matchesTransportMode(plan.get("transportMode"), transportType))
|
||||
.toList();
|
||||
if (matchedPlans.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
List<Map<String, Object>> defaultPlans = matchedPlans.stream().filter(this::isDefaultPlan).toList();
|
||||
if (!defaultPlans.isEmpty()) {
|
||||
return defaultPlans.get(defaultPlans.size() - 1);
|
||||
}
|
||||
return matchedPlans.get(matchedPlans.size() - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计费方案运输方式与运单运输方式比对。
|
||||
* 统一归一化为字典键后精确比较,避免 railway 被子串 road 误匹配。
|
||||
*/
|
||||
private boolean matchesTransportMode(Object expected, String actual) {
|
||||
if (isBlank(expected) || Func.isBlank(actual)) {
|
||||
return false;
|
||||
}
|
||||
if (expected instanceof List<?> values) {
|
||||
return values.stream().anyMatch(value -> matchesTransportMode(value, actual));
|
||||
}
|
||||
String expectedKey = normalizeTransportModeKey(String.valueOf(expected));
|
||||
String actualKey = normalizeTransportModeKey(actual);
|
||||
if (expectedKey.isEmpty() || actualKey.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return expectedKey.equals(actualKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将运输方式字典键/中文名归一化为统一键:road / railway / river / air。
|
||||
* 须先判断铁路再判断公路,避免 railway 被识别为 road。
|
||||
*/
|
||||
private String normalizeTransportModeKey(String value) {
|
||||
if (value == null) {
|
||||
return "";
|
||||
}
|
||||
String text = value.trim().toLowerCase();
|
||||
if (text.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
if ("railway".equals(text) || "rail".equals(text) || text.contains("铁路")) {
|
||||
return "railway";
|
||||
}
|
||||
if ("road".equals(text) || "gl".equals(text) || text.contains("公路") || text.contains("道路")) {
|
||||
return "road";
|
||||
}
|
||||
if ("river".equals(text) || "water".equals(text) || text.contains("水路") || text.contains("水运")) {
|
||||
return "river";
|
||||
}
|
||||
if ("air".equals(text) || "aviation".equals(text) || text.contains("航空") || text.contains("空运")) {
|
||||
return "air";
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
private BigDecimal calculateRule(Map<String, Object> rule, Waybill waybill) {
|
||||
@@ -1638,7 +1997,9 @@ public class ReceivablePayableDetailServiceImpl
|
||||
return switch (element) {
|
||||
case "按体积" -> goods.stream().map(this::goodsVolume).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
case "按车辆", "固定金额(整单一口价)" -> BigDecimal.ONE;
|
||||
// 按里程:计费量=运单里程;固定单价/区间单价/阶梯等均基于该计费量
|
||||
case "按里程" -> money(waybill.getMileage());
|
||||
// 按吨·公里:计费量=货物重量数量 × 运单里程
|
||||
case "按吨·公里" -> goodsQuantity(goods).multiply(money(waybill.getMileage()));
|
||||
case "按数量" -> goodsQuantity(goods);
|
||||
default -> goodsQuantity(goods);
|
||||
|
||||
+3
-6
@@ -43,8 +43,6 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
@@ -64,7 +62,6 @@ public class TireReplacementRecordServiceImpl extends BaseServiceImpl<TireReplac
|
||||
private static final int REMARK_MAX_LENGTH = 500;
|
||||
private static final int ATTACHMENTS_MAX_LENGTH = 1000;
|
||||
private static final int MONEY_SCALE = 2;
|
||||
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE;
|
||||
|
||||
@Override
|
||||
public IPage<TireReplacementRecordVO> selectTireReplacementRecordPage(IPage<TireReplacementRecordVO> page, TireReplacementRecordVO tireReplacementRecord) {
|
||||
@@ -144,9 +141,9 @@ public class TireReplacementRecordServiceImpl extends BaseServiceImpl<TireReplac
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return LocalDate.parse(normalizedValue, DATE_FORMATTER);
|
||||
} catch (DateTimeParseException exception) {
|
||||
validationErrors.add("换胎时间格式不正确,请使用yyyy-MM-dd格式并填写有效日期");
|
||||
return org.springblade.common.excel.LenientDateParser.parseDate(normalizedValue, "换胎时间");
|
||||
} catch (Exception exception) {
|
||||
validationErrors.add(exception.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
+20
-7
@@ -416,9 +416,9 @@ public class TransportPlanServiceImpl extends BaseServiceImpl<TransportPlanMappe
|
||||
// 2. 校验运输类型枚举值
|
||||
String transportType = trimToEmpty(excel.getTransportType());
|
||||
if (Func.isNotEmpty(transportType)) {
|
||||
List<String> validTransportTypes = List.of("公路整车", "公路配载/零担", "铁路运输", "水路运输", "跨境海运", "航空运输");
|
||||
List<String> validTransportTypes = List.of("公路运输", "铁路运输", "水路运输", "航空运输");
|
||||
if (!validTransportTypes.contains(transportType)) {
|
||||
errors.add("运输类型需系统枚举值(公路整车、公路配载/零担、铁路运输、水路运输、跨境海运、航空运输)");
|
||||
errors.add("运输类型需系统枚举值(公路运输、铁路运输、水路运输、航空运输)");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -510,11 +510,12 @@ public class TransportPlanServiceImpl extends BaseServiceImpl<TransportPlanMappe
|
||||
if (Func.isEmpty(value)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return LocalDate.parse(value.trim(), DateTimeFormatter.ISO_LOCAL_DATE);
|
||||
} catch (Exception exception) {
|
||||
throw new ServiceException(fieldName + "格式必须为 YYYY-MM-DD");
|
||||
// 宽容解析:接受 2026-8-2、2026/8/2 等写法(口径见根工作区 docs/import-spec.md)。
|
||||
LocalDate date = org.springblade.common.excel.LenientDateParser.parseDateLenientlyOrNull(value);
|
||||
if (date == null) {
|
||||
throw new ServiceException(fieldName + " 日期格式无法识别:" + value.trim());
|
||||
}
|
||||
return date;
|
||||
}
|
||||
|
||||
private void validateImportLength(String value, int maxLength, String fieldName) {
|
||||
@@ -717,6 +718,18 @@ public class TransportPlanServiceImpl extends BaseServiceImpl<TransportPlanMappe
|
||||
if (Func.isNotEmpty(transportPlan.getRemark())) {
|
||||
queryWrapper.like(TransportPlan::getRemark, transportPlan.getRemark());
|
||||
}
|
||||
if (transportPlan.getPlanStartDateStart() != null) {
|
||||
queryWrapper.ge(TransportPlan::getPlanStartDate, transportPlan.getPlanStartDateStart());
|
||||
}
|
||||
if (transportPlan.getPlanStartDateEnd() != null) {
|
||||
queryWrapper.le(TransportPlan::getPlanStartDate, transportPlan.getPlanStartDateEnd());
|
||||
}
|
||||
if (transportPlan.getCreateTimeStart() != null) {
|
||||
queryWrapper.ge(TransportPlan::getCreateTime, transportPlan.getCreateTimeStart());
|
||||
}
|
||||
if (transportPlan.getCreateTimeEnd() != null) {
|
||||
queryWrapper.le(TransportPlan::getCreateTime, transportPlan.getCreateTimeEnd());
|
||||
}
|
||||
queryWrapper.orderByDesc(TransportPlan::getCreateTime);
|
||||
return queryWrapper;
|
||||
}
|
||||
@@ -800,7 +813,7 @@ public class TransportPlanServiceImpl extends BaseServiceImpl<TransportPlanMappe
|
||||
}
|
||||
|
||||
private boolean shouldSkipDelete(TransportPlan transportPlan) {
|
||||
return false;
|
||||
return !"draft".equals(transportPlan.getBusinessStatus());
|
||||
}
|
||||
|
||||
private synchronized String nextCode() {
|
||||
|
||||
+5
-8
@@ -60,7 +60,6 @@ import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
@@ -1346,14 +1345,12 @@ public class TransportReconciliationServiceImpl
|
||||
|
||||
private LocalDateTime parseTimeNullable(String value, String field) {
|
||||
if (Func.isEmpty(value)) return null;
|
||||
for (String pattern : List.of("yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "yyyy-M-d HH:mm:ss", "yyyy-M-d HH:mm",
|
||||
"yyyy/MM/dd HH:mm:ss", "yyyy/MM/dd HH:mm", "yyyy/M/d HH:mm:ss", "yyyy/M/d HH:mm")) {
|
||||
try { return LocalDateTime.parse(value.trim(), DateTimeFormatter.ofPattern(pattern)); } catch (DateTimeParseException ignored) { }
|
||||
// 宽容解析:统一委托公共解析器,接受 2026-8-2 12:3 等写法(口径见根工作区 docs/import-spec.md)。
|
||||
LocalDateTime result = org.springblade.common.excel.LenientDateParser.parseDateTimeOrNull(value);
|
||||
if (result == null) {
|
||||
throw new ServiceException(field + " 日期格式无法识别:" + value.trim());
|
||||
}
|
||||
for (String pattern : List.of("yyyy-MM-dd", "yyyy-M-d", "yyyy/MM/dd", "yyyy/M/d")) {
|
||||
try { return LocalDate.parse(value.trim(), DateTimeFormatter.ofPattern(pattern)).atStartOfDay(); } catch (DateTimeParseException ignored) { }
|
||||
}
|
||||
throw new ServiceException(field + "格式应为yyyy-MM-dd HH:mm:ss或yyyy-MM-dd");
|
||||
return result;
|
||||
}
|
||||
|
||||
private String matchKey(TransportReconciliationInternal row) {
|
||||
|
||||
+326
-310
@@ -1,310 +1,326 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.mp.base.BaseServiceImpl;
|
||||
import org.springblade.core.tool.utils.BeanUtil;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.system.cache.UserCache;
|
||||
import org.springblade.transport.excel.ViolationRecordExcel;
|
||||
import org.springblade.transport.excel.ViolationRecordImportExcel;
|
||||
import org.springblade.transport.mapper.ViolationRecordMapper;
|
||||
import org.springblade.transport.pojo.entity.ViolationRecord;
|
||||
import org.springblade.transport.pojo.vo.ViolationRecordVO;
|
||||
import org.springblade.transport.service.IViolationRecordService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 违章记录 服务实现类
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Service
|
||||
public class ViolationRecordServiceImpl extends BaseServiceImpl<ViolationRecordMapper, ViolationRecord> implements IViolationRecordService {
|
||||
|
||||
private static final int VEHICLE_NO_MAX_LENGTH = 30;
|
||||
private static final int DRIVER_NAME_MAX_LENGTH = 20;
|
||||
private static final int TYPE_MAX_LENGTH = 50;
|
||||
private static final int ITEM_MAX_LENGTH = 100;
|
||||
private static final int LOCATION_MAX_LENGTH = 100;
|
||||
private static final int PENALTY_UNIT_MAX_LENGTH = 50;
|
||||
private static final int DESCRIPTION_MAX_LENGTH = 500;
|
||||
private static final int RESULT_MAX_LENGTH = 500;
|
||||
private static final int ATTACHMENTS_MAX_LENGTH = 16000;
|
||||
private static final int MAX_DEDUCT_POINTS = 15;
|
||||
private static final String PROCESSED = "已处理";
|
||||
private static final String UNPROCESSED = "未处理";
|
||||
|
||||
@Override
|
||||
public IPage<ViolationRecordVO> selectViolationRecordPage(IPage<ViolationRecordVO> page, ViolationRecordVO violationRecord) {
|
||||
List<ViolationRecordVO> records = baseMapper.selectViolationRecordPage(page, violationRecord);
|
||||
records.forEach(record -> record.setUpdateUserName(UserCache.getUserRealName(record.getUpdateUser())));
|
||||
return page.setRecords(records);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean submit(ViolationRecord violationRecord) {
|
||||
prepare(violationRecord);
|
||||
validate(violationRecord);
|
||||
validateVehicleTypeImmutable(violationRecord);
|
||||
return saveOrUpdate(violationRecord);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public List<ViolationRecordImportExcel> importViolationRecord(List<ViolationRecordImportExcel> data) {
|
||||
if (Func.isEmpty(data)) {
|
||||
throw new ServiceException("导入数据不能为空");
|
||||
}
|
||||
List<ViolationRecordImportExcel> errorList = new ArrayList<>();
|
||||
List<ViolationRecord> violationRecordList = new ArrayList<>();
|
||||
for (int index = 0; index < data.size(); index++) {
|
||||
ViolationRecordImportExcel excel = data.get(index);
|
||||
try {
|
||||
ViolationRecord violationRecord = Objects.requireNonNull(BeanUtil.copyProperties(excel, ViolationRecord.class));
|
||||
if ("船舶".equals(trimToEmpty(excel.getVehicleType()))) {
|
||||
violationRecord.setViolationItem(excel.getViolationTypeOrItem());
|
||||
} else {
|
||||
violationRecord.setViolationType(excel.getViolationTypeOrItem());
|
||||
}
|
||||
prepare(violationRecord);
|
||||
List<String> validationErrors = validateImportViolationRecord(violationRecord);
|
||||
if (Func.isNotEmpty(validationErrors)) {
|
||||
excel.setErrorMessage(org.springblade.common.excel.ImportFailureExcelUtil.formatErrorMessage(validationErrors));
|
||||
errorList.add(excel);
|
||||
continue;
|
||||
}
|
||||
validateVehicleTypeImmutable(violationRecord);
|
||||
violationRecordList.add(violationRecord);
|
||||
} catch (Exception exception) {
|
||||
String message = exception instanceof ServiceException ? exception.getMessage() : "导入失败";
|
||||
excel.setErrorMessage(org.springblade.common.excel.ImportFailureExcelUtil.formatErrorMessage(List.of(message)));
|
||||
errorList.add(excel);
|
||||
}
|
||||
}
|
||||
if (Func.isNotEmpty(errorList)) {
|
||||
org.springframework.transaction.interceptor.TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
|
||||
return errorList;
|
||||
}
|
||||
for (ViolationRecord violationRecord : violationRecordList) {
|
||||
if (!save(violationRecord)) {
|
||||
throw new ServiceException("违章记录保存失败");
|
||||
}
|
||||
}
|
||||
return errorList;
|
||||
}
|
||||
|
||||
private List<String> validateImportViolationRecord(ViolationRecord violationRecord) {
|
||||
List<String> validationErrors = new ArrayList<>();
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(violationRecord.getVehicleType()), "车船类型不能为空");
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(violationRecord.getVehicleType()) && !"车辆".equals(violationRecord.getVehicleType()) && !"船舶".equals(violationRecord.getVehicleType()), "车船类型不正确");
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(violationRecord.getVehicleNo()), "车牌号/船号不能为空");
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(violationRecord.getDriverName()), "驾驶人/船长不能为空");
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, "车辆".equals(violationRecord.getVehicleType()) && Func.isEmpty(violationRecord.getViolationType()), "类型不能为空");
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, "船舶".equals(violationRecord.getVehicleType()) && Func.isEmpty(violationRecord.getViolationItem()), "事项不能为空");
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(violationRecord.getViolationTime()), "时间不能为空");
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(violationRecord.getViolationTime()) && violationRecord.getViolationTime().isAfter(LocalDateTime.now()), "时间不能超过当前时间");
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(violationRecord.getLocation()), "地址不能为空");
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(violationRecord.getProcessStatus()), "状态不能为空");
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(violationRecord.getProcessStatus()) && !PROCESSED.equals(violationRecord.getProcessStatus()) && !UNPROCESSED.equals(violationRecord.getProcessStatus()), "状态值不正确");
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(violationRecord.getProcessDescription()), "过程描述不能为空");
|
||||
addImportNonNegativeError(validationErrors, violationRecord.getFineAmount(), "被罚金额");
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(violationRecord.getDeductPoints()) && (violationRecord.getDeductPoints() < 0 || violationRecord.getDeductPoints() > MAX_DEDUCT_POINTS), "被扣分数范围为0-15分");
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, violationRecord.getVehicleNo(), VEHICLE_NO_MAX_LENGTH, "车牌号/船号不能超过30字");
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, violationRecord.getDriverName(), DRIVER_NAME_MAX_LENGTH, "驾驶人/船长不能超过20字");
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, violationRecord.getViolationType(), TYPE_MAX_LENGTH, "类型不能超过50字");
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, violationRecord.getViolationItem(), ITEM_MAX_LENGTH, "事项不能超过100字");
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, violationRecord.getLocation(), LOCATION_MAX_LENGTH, "地址不能超过100字");
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, violationRecord.getPenaltyUnit(), PENALTY_UNIT_MAX_LENGTH, "被罚单位不能超过50字");
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, violationRecord.getProcessDescription(), DESCRIPTION_MAX_LENGTH, "过程描述不能超过500字");
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, violationRecord.getProcessResult(), RESULT_MAX_LENGTH, "处理结果不能超过500字");
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, violationRecord.getAttachments(), ATTACHMENTS_MAX_LENGTH, "附件数据不能超过16000字");
|
||||
return validationErrors;
|
||||
}
|
||||
|
||||
private void addImportNonNegativeError(List<String> validationErrors, BigDecimal value, String fieldName) {
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(value) && value.compareTo(BigDecimal.ZERO) < 0, fieldName + "不能小于0");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ViolationRecordExcel> exportViolationRecord(Wrapper<ViolationRecord> queryWrapper) {
|
||||
return list(queryWrapper).stream().map(violationRecord -> {
|
||||
ViolationRecordExcel excel = Objects.requireNonNull(BeanUtil.copyProperties(violationRecord, ViolationRecordExcel.class));
|
||||
excel.setFineAmount(nonNegative(violationRecord.getFineAmount()));
|
||||
excel.setDeductPoints(validDeductPoints(violationRecord.getDeductPoints()));
|
||||
excel.setUpdateUserName(UserCache.getUserRealName(violationRecord.getUpdateUser()));
|
||||
return excel;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
private void prepare(ViolationRecord violationRecord) {
|
||||
violationRecord.setVehicleType(normalizeVehicleType(violationRecord.getVehicleType()));
|
||||
violationRecord.setVehicleNo(trimToEmpty(violationRecord.getVehicleNo()).toUpperCase());
|
||||
violationRecord.setDriverName(trimToEmpty(violationRecord.getDriverName()));
|
||||
violationRecord.setViolationType(trimToNull(violationRecord.getViolationType()));
|
||||
violationRecord.setViolationItem(trimToNull(violationRecord.getViolationItem()));
|
||||
violationRecord.setLocation(trimToEmpty(violationRecord.getLocation()));
|
||||
violationRecord.setPenaltyUnit(trimToNull(violationRecord.getPenaltyUnit()));
|
||||
violationRecord.setProcessStatus(normalizeProcessStatus(violationRecord.getProcessStatus()));
|
||||
violationRecord.setProcessDescription(trimToEmpty(violationRecord.getProcessDescription()));
|
||||
violationRecord.setProcessResult(trimToNull(violationRecord.getProcessResult()));
|
||||
violationRecord.setAttachments(trimToNull(violationRecord.getAttachments()));
|
||||
if ("车辆".equals(violationRecord.getVehicleType())) {
|
||||
violationRecord.setViolationItem(null);
|
||||
} else {
|
||||
violationRecord.setViolationType(null);
|
||||
}
|
||||
if (UNPROCESSED.equals(violationRecord.getProcessStatus())) {
|
||||
violationRecord.setProcessResult(null);
|
||||
}
|
||||
}
|
||||
|
||||
private void validate(ViolationRecord violationRecord) {
|
||||
if (Func.isEmpty(violationRecord.getVehicleType())) {
|
||||
throw new ServiceException("车船类型不能为空");
|
||||
}
|
||||
if (!"车辆".equals(violationRecord.getVehicleType()) && !"船舶".equals(violationRecord.getVehicleType())) {
|
||||
throw new ServiceException("车船类型不正确");
|
||||
}
|
||||
if (Func.isEmpty(violationRecord.getVehicleNo())) {
|
||||
throw new ServiceException("车牌号/船号不能为空");
|
||||
}
|
||||
if (Func.isEmpty(violationRecord.getDriverName())) {
|
||||
throw new ServiceException("驾驶人/船长不能为空");
|
||||
}
|
||||
if ("车辆".equals(violationRecord.getVehicleType()) && Func.isEmpty(violationRecord.getViolationType())) {
|
||||
throw new ServiceException("类型不能为空");
|
||||
}
|
||||
if ("船舶".equals(violationRecord.getVehicleType()) && Func.isEmpty(violationRecord.getViolationItem())) {
|
||||
throw new ServiceException("事项不能为空");
|
||||
}
|
||||
if (Func.isEmpty(violationRecord.getViolationTime())) {
|
||||
throw new ServiceException("时间不能为空");
|
||||
}
|
||||
if (violationRecord.getViolationTime().isAfter(LocalDateTime.now())) {
|
||||
throw new ServiceException("时间不能超过当前时间");
|
||||
}
|
||||
if (Func.isEmpty(violationRecord.getLocation())) {
|
||||
throw new ServiceException("地址不能为空");
|
||||
}
|
||||
if (Func.isEmpty(violationRecord.getProcessStatus())) {
|
||||
throw new ServiceException("状态不能为空");
|
||||
}
|
||||
if (!PROCESSED.equals(violationRecord.getProcessStatus()) && !UNPROCESSED.equals(violationRecord.getProcessStatus())) {
|
||||
throw new ServiceException("状态值不正确");
|
||||
}
|
||||
if (Func.isEmpty(violationRecord.getProcessDescription())) {
|
||||
throw new ServiceException("过程描述不能为空");
|
||||
}
|
||||
validateNonNegative(violationRecord.getFineAmount(), "被罚金额不能小于0");
|
||||
validateDeductPoints(violationRecord.getDeductPoints());
|
||||
validateLength(violationRecord.getVehicleNo(), VEHICLE_NO_MAX_LENGTH, "车牌号/船号不能超过30字");
|
||||
validateLength(violationRecord.getDriverName(), DRIVER_NAME_MAX_LENGTH, "驾驶人/船长不能超过20字");
|
||||
validateLength(violationRecord.getViolationType(), TYPE_MAX_LENGTH, "类型不能超过50字");
|
||||
validateLength(violationRecord.getViolationItem(), ITEM_MAX_LENGTH, "事项不能超过100字");
|
||||
validateLength(violationRecord.getLocation(), LOCATION_MAX_LENGTH, "地址不能超过100字");
|
||||
validateLength(violationRecord.getPenaltyUnit(), PENALTY_UNIT_MAX_LENGTH, "被罚单位不能超过50字");
|
||||
validateLength(violationRecord.getProcessDescription(), DESCRIPTION_MAX_LENGTH, "过程描述不能超过500字");
|
||||
validateLength(violationRecord.getProcessResult(), RESULT_MAX_LENGTH, "处理结果不能超过500字");
|
||||
validateLength(violationRecord.getAttachments(), ATTACHMENTS_MAX_LENGTH, "附件数据不能超过16000字");
|
||||
}
|
||||
|
||||
private void validateVehicleTypeImmutable(ViolationRecord violationRecord) {
|
||||
if (Func.isEmpty(violationRecord.getId())) {
|
||||
return;
|
||||
}
|
||||
ViolationRecord oldRecord = getById(violationRecord.getId());
|
||||
if (oldRecord != null && Func.isNotEmpty(oldRecord.getVehicleType()) && !oldRecord.getVehicleType().equals(violationRecord.getVehicleType())) {
|
||||
throw new ServiceException("车船类型保存后不可修改");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateLength(String value, int maxLength, String message) {
|
||||
if (Func.isNotEmpty(value) && value.length() > maxLength) {
|
||||
throw new ServiceException(message);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateNonNegative(BigDecimal value, String message) {
|
||||
if (Func.isNotEmpty(value) && value.compareTo(BigDecimal.ZERO) < 0) {
|
||||
throw new ServiceException(message);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateDeductPoints(Integer value) {
|
||||
if (Func.isNotEmpty(value) && (value < 0 || value > MAX_DEDUCT_POINTS)) {
|
||||
throw new ServiceException("被扣分数范围为0-15分");
|
||||
}
|
||||
}
|
||||
|
||||
private BigDecimal nonNegative(BigDecimal value) {
|
||||
if (Func.isEmpty(value) || value.compareTo(BigDecimal.ZERO) >= 0) {
|
||||
return value;
|
||||
}
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
private Integer validDeductPoints(Integer value) {
|
||||
if (Func.isEmpty(value)) {
|
||||
return value;
|
||||
}
|
||||
if (value < 0) {
|
||||
return 0;
|
||||
}
|
||||
return Math.min(value, MAX_DEDUCT_POINTS);
|
||||
}
|
||||
|
||||
private String normalizeVehicleType(String vehicleType) {
|
||||
String value = trimToEmpty(vehicleType);
|
||||
return value.isEmpty() ? "车辆" : value;
|
||||
}
|
||||
|
||||
private String normalizeProcessStatus(String processStatus) {
|
||||
String value = trimToEmpty(processStatus);
|
||||
return value.isEmpty() ? UNPROCESSED : value;
|
||||
}
|
||||
|
||||
private String trimToEmpty(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
|
||||
private String trimToNull(String value) {
|
||||
String trimValue = trimToEmpty(value);
|
||||
return trimValue.isEmpty() ? null : trimValue;
|
||||
}
|
||||
|
||||
}
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* Author: Chill Zhuang (bladejava@qq.com)
|
||||
*/
|
||||
package org.springblade.transport.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.mp.base.BaseServiceImpl;
|
||||
import org.springblade.core.tool.utils.BeanUtil;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.system.cache.UserCache;
|
||||
import org.springblade.transport.excel.ViolationRecordExcel;
|
||||
import org.springblade.transport.excel.ViolationRecordImportExcel;
|
||||
import org.springblade.transport.mapper.ViolationRecordMapper;
|
||||
import org.springblade.transport.pojo.entity.ViolationRecord;
|
||||
import org.springblade.transport.pojo.vo.ViolationRecordVO;
|
||||
import org.springblade.transport.service.IViolationRecordService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 违章记录 服务实现类
|
||||
*
|
||||
* @author Chill
|
||||
*/
|
||||
@Service
|
||||
public class ViolationRecordServiceImpl extends BaseServiceImpl<ViolationRecordMapper, ViolationRecord> implements IViolationRecordService {
|
||||
|
||||
private static final int VEHICLE_NO_MAX_LENGTH = 30;
|
||||
private static final int DRIVER_NAME_MAX_LENGTH = 20;
|
||||
private static final int TYPE_MAX_LENGTH = 50;
|
||||
private static final int ITEM_MAX_LENGTH = 100;
|
||||
private static final int LOCATION_MAX_LENGTH = 100;
|
||||
private static final int PENALTY_UNIT_MAX_LENGTH = 50;
|
||||
private static final int DESCRIPTION_MAX_LENGTH = 500;
|
||||
private static final int RESULT_MAX_LENGTH = 500;
|
||||
private static final int ATTACHMENTS_MAX_LENGTH = 16000;
|
||||
private static final int MAX_DEDUCT_POINTS = 15;
|
||||
private static final String PROCESSED = "已处理";
|
||||
private static final String UNPROCESSED = "未处理";
|
||||
private static final String CAR = "车辆";
|
||||
private static final String SHIP = "船舶";
|
||||
|
||||
@Override
|
||||
public IPage<ViolationRecordVO> selectViolationRecordPage(IPage<ViolationRecordVO> page, ViolationRecordVO violationRecord) {
|
||||
List<ViolationRecordVO> records = baseMapper.selectViolationRecordPage(page, violationRecord);
|
||||
records.forEach(record -> record.setUpdateUserName(UserCache.getUserRealName(record.getUpdateUser())));
|
||||
return page.setRecords(records);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean submit(ViolationRecord violationRecord) {
|
||||
prepare(violationRecord);
|
||||
validate(violationRecord);
|
||||
validateVehicleTypeImmutable(violationRecord);
|
||||
clearIrrelevantField(violationRecord);
|
||||
return saveOrUpdate(violationRecord);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public List<ViolationRecordImportExcel> importViolationRecord(List<ViolationRecordImportExcel> data) {
|
||||
if (Func.isEmpty(data)) {
|
||||
throw new ServiceException("导入数据不能为空");
|
||||
}
|
||||
List<ViolationRecordImportExcel> errorList = new ArrayList<>();
|
||||
List<ViolationRecord> violationRecordList = new ArrayList<>();
|
||||
for (int index = 0; index < data.size(); index++) {
|
||||
ViolationRecordImportExcel excel = data.get(index);
|
||||
try {
|
||||
ViolationRecord violationRecord = Objects.requireNonNull(BeanUtil.copyProperties(excel, ViolationRecord.class));
|
||||
prepare(violationRecord);
|
||||
List<String> validationErrors = validateImportViolationRecord(violationRecord);
|
||||
if (Func.isNotEmpty(validationErrors)) {
|
||||
excel.setErrorMessage(org.springblade.common.excel.ImportFailureExcelUtil.formatErrorMessage(validationErrors));
|
||||
errorList.add(excel);
|
||||
continue;
|
||||
}
|
||||
validateVehicleTypeImmutable(violationRecord);
|
||||
clearIrrelevantField(violationRecord);
|
||||
violationRecordList.add(violationRecord);
|
||||
} catch (Exception exception) {
|
||||
String message = exception instanceof ServiceException ? exception.getMessage() : "导入失败";
|
||||
excel.setErrorMessage(org.springblade.common.excel.ImportFailureExcelUtil.formatErrorMessage(List.of(message)));
|
||||
errorList.add(excel);
|
||||
}
|
||||
}
|
||||
if (Func.isNotEmpty(errorList)) {
|
||||
org.springframework.transaction.interceptor.TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
|
||||
return errorList;
|
||||
}
|
||||
for (ViolationRecord violationRecord : violationRecordList) {
|
||||
if (!save(violationRecord)) {
|
||||
throw new ServiceException("违章记录保存失败");
|
||||
}
|
||||
}
|
||||
return errorList;
|
||||
}
|
||||
|
||||
private List<String> validateImportViolationRecord(ViolationRecord violationRecord) {
|
||||
List<String> validationErrors = new ArrayList<>();
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(violationRecord.getVehicleType()), "车船类型不能为空");
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(violationRecord.getVehicleType()) && !"车辆".equals(violationRecord.getVehicleType()) && !"船舶".equals(violationRecord.getVehicleType()), "车船类型不正确");
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(violationRecord.getVehicleNo()), "车牌号/船号不能为空");
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(violationRecord.getDriverName()), "驾驶人/船长不能为空");
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, "车辆".equals(violationRecord.getVehicleType()) && Func.isEmpty(violationRecord.getViolationType()), "类型不能为空");
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, "船舶".equals(violationRecord.getVehicleType()) && Func.isEmpty(violationRecord.getViolationItem()), "事项不能为空");
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, SHIP.equals(violationRecord.getVehicleType()) && Func.isNotEmpty(violationRecord.getViolationType()), "船舶不适用于类型,该列应留空");
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, CAR.equals(violationRecord.getVehicleType()) && Func.isNotEmpty(violationRecord.getViolationItem()), "车辆不适用于事项,该列应留空");
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(violationRecord.getViolationTime()), "时间不能为空");
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(violationRecord.getViolationTime()) && violationRecord.getViolationTime().isAfter(LocalDateTime.now()), "时间不能超过当前时间");
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(violationRecord.getLocation()), "地址不能为空");
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(violationRecord.getProcessStatus()), "状态不能为空");
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(violationRecord.getProcessStatus()) && !PROCESSED.equals(violationRecord.getProcessStatus()) && !UNPROCESSED.equals(violationRecord.getProcessStatus()), "状态值不正确");
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isEmpty(violationRecord.getProcessDescription()), "过程描述不能为空");
|
||||
addImportNonNegativeError(validationErrors, violationRecord.getFineAmount(), "被罚金额");
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(violationRecord.getDeductPoints()) && (violationRecord.getDeductPoints() < 0 || violationRecord.getDeductPoints() > MAX_DEDUCT_POINTS), "被扣分数范围为0-15分");
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, violationRecord.getVehicleNo(), VEHICLE_NO_MAX_LENGTH, "车牌号/船号不能超过30字");
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, violationRecord.getDriverName(), DRIVER_NAME_MAX_LENGTH, "驾驶人/船长不能超过20字");
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, violationRecord.getViolationType(), TYPE_MAX_LENGTH, "类型不能超过50字");
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, violationRecord.getViolationItem(), ITEM_MAX_LENGTH, "事项不能超过100字");
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, violationRecord.getLocation(), LOCATION_MAX_LENGTH, "地址不能超过100字");
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, violationRecord.getPenaltyUnit(), PENALTY_UNIT_MAX_LENGTH, "被罚单位不能超过50字");
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, violationRecord.getProcessDescription(), DESCRIPTION_MAX_LENGTH, "过程描述不能超过500字");
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, violationRecord.getProcessResult(), RESULT_MAX_LENGTH, "处理结果不能超过500字");
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.addLengthValidationError(validationErrors, violationRecord.getAttachments(), ATTACHMENTS_MAX_LENGTH, "附件数据不能超过16000字");
|
||||
return validationErrors;
|
||||
}
|
||||
|
||||
private void addImportNonNegativeError(List<String> validationErrors, BigDecimal value, String fieldName) {
|
||||
org.springblade.common.excel.ImportFailureExcelUtil.addValidationError(validationErrors, Func.isNotEmpty(value) && value.compareTo(BigDecimal.ZERO) < 0, fieldName + "不能小于0");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ViolationRecordExcel> exportViolationRecord(Wrapper<ViolationRecord> queryWrapper) {
|
||||
return list(queryWrapper).stream().map(violationRecord -> {
|
||||
ViolationRecordExcel excel = Objects.requireNonNull(BeanUtil.copyProperties(violationRecord, ViolationRecordExcel.class));
|
||||
excel.setFineAmount(nonNegative(violationRecord.getFineAmount()));
|
||||
excel.setDeductPoints(validDeductPoints(violationRecord.getDeductPoints()));
|
||||
excel.setUpdateUserName(UserCache.getUserRealName(violationRecord.getUpdateUser()));
|
||||
return excel;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
private void prepare(ViolationRecord violationRecord) {
|
||||
violationRecord.setVehicleType(normalizeVehicleType(violationRecord.getVehicleType()));
|
||||
violationRecord.setVehicleNo(trimToEmpty(violationRecord.getVehicleNo()).toUpperCase());
|
||||
violationRecord.setDriverName(trimToEmpty(violationRecord.getDriverName()));
|
||||
violationRecord.setViolationType(trimToNull(violationRecord.getViolationType()));
|
||||
violationRecord.setViolationItem(trimToNull(violationRecord.getViolationItem()));
|
||||
violationRecord.setLocation(trimToEmpty(violationRecord.getLocation()));
|
||||
violationRecord.setPenaltyUnit(trimToNull(violationRecord.getPenaltyUnit()));
|
||||
violationRecord.setProcessStatus(normalizeProcessStatus(violationRecord.getProcessStatus()));
|
||||
violationRecord.setProcessDescription(trimToEmpty(violationRecord.getProcessDescription()));
|
||||
violationRecord.setProcessResult(trimToNull(violationRecord.getProcessResult()));
|
||||
violationRecord.setAttachments(trimToNull(violationRecord.getAttachments()));
|
||||
if (UNPROCESSED.equals(violationRecord.getProcessStatus())) {
|
||||
violationRecord.setProcessResult(null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空与车船类型不匹配的对侧字段
|
||||
* <p>
|
||||
* 必须在 validate 之后执行:校验需要看到用户填了什么,
|
||||
* 若提前清空,误填的内容会被静默丢弃,用户无从察觉。
|
||||
*/
|
||||
private void clearIrrelevantField(ViolationRecord violationRecord) {
|
||||
if (CAR.equals(violationRecord.getVehicleType())) {
|
||||
violationRecord.setViolationItem(null);
|
||||
} else {
|
||||
violationRecord.setViolationType(null);
|
||||
}
|
||||
}
|
||||
|
||||
private void validate(ViolationRecord violationRecord) {
|
||||
if (Func.isEmpty(violationRecord.getVehicleType())) {
|
||||
throw new ServiceException("车船类型不能为空");
|
||||
}
|
||||
if (!CAR.equals(violationRecord.getVehicleType()) && !SHIP.equals(violationRecord.getVehicleType())) {
|
||||
throw new ServiceException("车船类型不正确");
|
||||
}
|
||||
if (Func.isEmpty(violationRecord.getVehicleNo())) {
|
||||
throw new ServiceException("车牌号/船号不能为空");
|
||||
}
|
||||
if (Func.isEmpty(violationRecord.getDriverName())) {
|
||||
throw new ServiceException("驾驶人/船长不能为空");
|
||||
}
|
||||
if (CAR.equals(violationRecord.getVehicleType()) && Func.isEmpty(violationRecord.getViolationType())) {
|
||||
throw new ServiceException("类型不能为空");
|
||||
}
|
||||
if (SHIP.equals(violationRecord.getVehicleType()) && Func.isEmpty(violationRecord.getViolationItem())) {
|
||||
throw new ServiceException("事项不能为空");
|
||||
}
|
||||
if (SHIP.equals(violationRecord.getVehicleType()) && Func.isNotEmpty(violationRecord.getViolationType())) {
|
||||
throw new ServiceException("船舶不适用于类型,该列应留空");
|
||||
}
|
||||
if (CAR.equals(violationRecord.getVehicleType()) && Func.isNotEmpty(violationRecord.getViolationItem())) {
|
||||
throw new ServiceException("车辆不适用于事项,该列应留空");
|
||||
}
|
||||
if (Func.isEmpty(violationRecord.getViolationTime())) {
|
||||
throw new ServiceException("时间不能为空");
|
||||
}
|
||||
if (violationRecord.getViolationTime().isAfter(LocalDateTime.now())) {
|
||||
throw new ServiceException("时间不能超过当前时间");
|
||||
}
|
||||
if (Func.isEmpty(violationRecord.getLocation())) {
|
||||
throw new ServiceException("地址不能为空");
|
||||
}
|
||||
if (Func.isEmpty(violationRecord.getProcessStatus())) {
|
||||
throw new ServiceException("状态不能为空");
|
||||
}
|
||||
if (!PROCESSED.equals(violationRecord.getProcessStatus()) && !UNPROCESSED.equals(violationRecord.getProcessStatus())) {
|
||||
throw new ServiceException("状态值不正确");
|
||||
}
|
||||
if (Func.isEmpty(violationRecord.getProcessDescription())) {
|
||||
throw new ServiceException("过程描述不能为空");
|
||||
}
|
||||
validateNonNegative(violationRecord.getFineAmount(), "被罚金额不能小于0");
|
||||
validateDeductPoints(violationRecord.getDeductPoints());
|
||||
validateLength(violationRecord.getVehicleNo(), VEHICLE_NO_MAX_LENGTH, "车牌号/船号不能超过30字");
|
||||
validateLength(violationRecord.getDriverName(), DRIVER_NAME_MAX_LENGTH, "驾驶人/船长不能超过20字");
|
||||
validateLength(violationRecord.getViolationType(), TYPE_MAX_LENGTH, "类型不能超过50字");
|
||||
validateLength(violationRecord.getViolationItem(), ITEM_MAX_LENGTH, "事项不能超过100字");
|
||||
validateLength(violationRecord.getLocation(), LOCATION_MAX_LENGTH, "地址不能超过100字");
|
||||
validateLength(violationRecord.getPenaltyUnit(), PENALTY_UNIT_MAX_LENGTH, "被罚单位不能超过50字");
|
||||
validateLength(violationRecord.getProcessDescription(), DESCRIPTION_MAX_LENGTH, "过程描述不能超过500字");
|
||||
validateLength(violationRecord.getProcessResult(), RESULT_MAX_LENGTH, "处理结果不能超过500字");
|
||||
validateLength(violationRecord.getAttachments(), ATTACHMENTS_MAX_LENGTH, "附件数据不能超过16000字");
|
||||
}
|
||||
|
||||
private void validateVehicleTypeImmutable(ViolationRecord violationRecord) {
|
||||
if (Func.isEmpty(violationRecord.getId())) {
|
||||
return;
|
||||
}
|
||||
ViolationRecord oldRecord = getById(violationRecord.getId());
|
||||
if (oldRecord != null && Func.isNotEmpty(oldRecord.getVehicleType()) && !oldRecord.getVehicleType().equals(violationRecord.getVehicleType())) {
|
||||
throw new ServiceException("车船类型保存后不可修改");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateLength(String value, int maxLength, String message) {
|
||||
if (Func.isNotEmpty(value) && value.length() > maxLength) {
|
||||
throw new ServiceException(message);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateNonNegative(BigDecimal value, String message) {
|
||||
if (Func.isNotEmpty(value) && value.compareTo(BigDecimal.ZERO) < 0) {
|
||||
throw new ServiceException(message);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateDeductPoints(Integer value) {
|
||||
if (Func.isNotEmpty(value) && (value < 0 || value > MAX_DEDUCT_POINTS)) {
|
||||
throw new ServiceException("被扣分数范围为0-15分");
|
||||
}
|
||||
}
|
||||
|
||||
private BigDecimal nonNegative(BigDecimal value) {
|
||||
if (Func.isEmpty(value) || value.compareTo(BigDecimal.ZERO) >= 0) {
|
||||
return value;
|
||||
}
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
private Integer validDeductPoints(Integer value) {
|
||||
if (Func.isEmpty(value)) {
|
||||
return value;
|
||||
}
|
||||
if (value < 0) {
|
||||
return 0;
|
||||
}
|
||||
return Math.min(value, MAX_DEDUCT_POINTS);
|
||||
}
|
||||
|
||||
private String normalizeVehicleType(String vehicleType) {
|
||||
String value = trimToEmpty(vehicleType);
|
||||
return value.isEmpty() ? CAR : value;
|
||||
}
|
||||
|
||||
private String normalizeProcessStatus(String processStatus) {
|
||||
String value = trimToEmpty(processStatus);
|
||||
return value.isEmpty() ? UNPROCESSED : value;
|
||||
}
|
||||
|
||||
private String trimToEmpty(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
|
||||
private String trimToNull(String value) {
|
||||
String trimValue = trimToEmpty(value);
|
||||
return trimValue.isEmpty() ? null : trimValue;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+25
-10
@@ -42,6 +42,7 @@ import io.minio.PutObjectArgs;
|
||||
import io.minio.RemoveObjectArgs;
|
||||
import io.minio.http.Method;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
|
||||
import java.time.format.DateTimeFormatter;
|
||||
@@ -79,14 +80,14 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
|
||||
private final VoucherImageMapper voucherImageMapper;
|
||||
private final VoucherFileMapper voucherFileMapper;
|
||||
private final ApplicationEventPublisher eventPublisher;
|
||||
private final MinioClient minioClient;
|
||||
private final ObjectProvider<MinioClient> minioClientProvider;
|
||||
private final String minioBucketName;
|
||||
private final String minioRootDirectory;
|
||||
|
||||
public VoucherManageServiceImpl(VoucherWaybillBatchMapper voucherWaybillBatchMapper, IProjectApplyService projectApplyService,
|
||||
IWaybillService waybillService,
|
||||
VoucherImageMapper voucherImageMapper, VoucherFileMapper voucherFileMapper,
|
||||
ApplicationEventPublisher eventPublisher, MinioClient minioClient,
|
||||
ApplicationEventPublisher eventPublisher, ObjectProvider<MinioClient> minioClientProvider,
|
||||
@Value("${file.storage.minio.bucket-name:${minio.bucket-name:}}") String minioBucketName,
|
||||
@Value("${file.storage.minio.root-directory:${minio.root-directory:}}") String minioRootDirectory) {
|
||||
this.voucherWaybillBatchMapper = voucherWaybillBatchMapper;
|
||||
@@ -95,7 +96,7 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
|
||||
this.voucherImageMapper = voucherImageMapper;
|
||||
this.voucherFileMapper = voucherFileMapper;
|
||||
this.eventPublisher = eventPublisher;
|
||||
this.minioClient = minioClient;
|
||||
this.minioClientProvider = minioClientProvider;
|
||||
this.minioBucketName = minioBucketName;
|
||||
this.minioRootDirectory = minioRootDirectory;
|
||||
}
|
||||
@@ -251,7 +252,7 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
|
||||
List<String> candidateObjectKeys = resolvedObjectKey.equals(objectKey)
|
||||
? List.of(objectKey) : List.of(resolvedObjectKey, objectKey);
|
||||
for (String candidateObjectKey : candidateObjectKeys) {
|
||||
try (InputStream source = minioClient.getObject(GetObjectArgs.builder().bucket(minioBucketName).object(candidateObjectKey).build());
|
||||
try (InputStream source = minioClient().getObject(GetObjectArgs.builder().bucket(minioBucketName).object(candidateObjectKey).build());
|
||||
OutputStream target = Files.newOutputStream(archivePath)) {
|
||||
source.transferTo(target);
|
||||
return archivePath;
|
||||
@@ -427,7 +428,7 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
|
||||
String waybillNo = matchedWaybill == null ? "unmatched" : safePathPart(matchedWaybill.getWaybillNo());
|
||||
String objectKey = buildObjectKey(voucher.getId(), waybillNo, plateNo, entryName);
|
||||
String contentType = contentType(fileName);
|
||||
minioClient.putObject(PutObjectArgs.builder().bucket(minioBucketName).object(objectKey)
|
||||
minioClient().putObject(PutObjectArgs.builder().bucket(minioBucketName).object(objectKey)
|
||||
.stream(zipInputStream, entry.getSize(), 10 * 1024 * 1024)
|
||||
.contentType(contentType).build());
|
||||
VoucherFile file = new VoucherFile();
|
||||
@@ -490,7 +491,7 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
|
||||
String waybillNo = matchedWaybill == null ? "unmatched" : safePathPart(matchedWaybill.getWaybillNo());
|
||||
String objectKey = buildObjectKey(voucher.getId(), waybillNo, plateNo, entryName);
|
||||
String contentType = contentType(fileName);
|
||||
minioClient.putObject(PutObjectArgs.builder().bucket(minioBucketName).object(objectKey)
|
||||
minioClient().putObject(PutObjectArgs.builder().bucket(minioBucketName).object(objectKey)
|
||||
.stream(zipInputStream, entry.getSize(), 10 * 1024 * 1024)
|
||||
.contentType(contentType).build());
|
||||
VoucherFile file = new VoucherFile();
|
||||
@@ -537,7 +538,7 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
|
||||
String entryName = plateNo + "/" + System.currentTimeMillis() + "_" + fileName;
|
||||
String objectKey = buildObjectKey(voucher.getId(), matchedWaybill == null ? "unmatched" : safePathPart(matchedWaybill.getWaybillNo()), plateNo, entryName);
|
||||
try (InputStream inputStream = imageFile.getInputStream()) {
|
||||
minioClient.putObject(PutObjectArgs.builder().bucket(minioBucketName).object(objectKey)
|
||||
minioClient().putObject(PutObjectArgs.builder().bucket(minioBucketName).object(objectKey)
|
||||
.stream(inputStream, imageFile.getSize(), 10 * 1024 * 1024)
|
||||
.contentType(Func.isEmpty(imageFile.getContentType()) ? contentType(fileName) : imageFile.getContentType()).build());
|
||||
}
|
||||
@@ -606,7 +607,7 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
|
||||
return Func.isEmpty(normalizedPlateNo) ? "未识别车牌" : normalizedPlateNo;
|
||||
}
|
||||
|
||||
private void deleteObjectQuietly(String objectKey) { try { minioClient.removeObject(RemoveObjectArgs.builder().bucket(minioBucketName).object(objectKey).build()); } catch (Exception exception) { log.warn("删除凭证对象失败 objectKey={}", objectKey, exception); } }
|
||||
private void deleteObjectQuietly(String objectKey) { try { minioClient().removeObject(RemoveObjectArgs.builder().bucket(minioBucketName).object(objectKey).build()); } catch (Exception exception) { log.warn("删除凭证对象失败 objectKey={}", objectKey, exception); } }
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@@ -838,7 +839,7 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
|
||||
String objectKey = buildObjectKey(voucher.getId(), waybillNo,
|
||||
Func.isEmpty(plateNo) ? "root" : plateNo, entryName);
|
||||
String contentType = contentType(fileName);
|
||||
minioClient.putObject(PutObjectArgs.builder().bucket(minioBucketName).object(objectKey)
|
||||
minioClient().putObject(PutObjectArgs.builder().bucket(minioBucketName).object(objectKey)
|
||||
.stream(zipInputStream, entry.getSize(), 10 * 1024 * 1024)
|
||||
.contentType(contentType).build());
|
||||
VoucherFile file = new VoucherFile();
|
||||
@@ -1052,6 +1053,20 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 MinIO 客户端。
|
||||
* <p>
|
||||
* 未配置 file.storage.minio.endpoint 时该客户端不会被注册,
|
||||
* 此处给出与 {@link #validateMinioConfig()} 一致的明确提示。
|
||||
*/
|
||||
private MinioClient minioClient() {
|
||||
MinioClient minioClient = minioClientProvider.getIfAvailable();
|
||||
if (minioClient == null) {
|
||||
throw new ServiceException("Nacos 未配置 file.storage.minio.endpoint,凭证文件功能不可用");
|
||||
}
|
||||
return minioClient;
|
||||
}
|
||||
|
||||
private String buildObjectKey(Long voucherId, String waybillNo, String plateNo, String entryName) {
|
||||
String objectKey = voucherId + "/" + waybillNo + "/" + safePathPart(plateNo) + "/" + safeArchivePath(entryName);
|
||||
if (Func.isEmpty(minioRootDirectory)) {
|
||||
@@ -1127,7 +1142,7 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
|
||||
|
||||
private String buildVoucherFileUrl(Long fileId, String objectKey) {
|
||||
try {
|
||||
return minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
|
||||
return minioClient().getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
|
||||
.method(Method.GET).bucket(minioBucketName).object(objectKey).expiry(1, java.util.concurrent.TimeUnit.HOURS).build());
|
||||
} catch (Exception exception) {
|
||||
log.warn("生成凭证文件预览地址失败 voucherFileId={}, objectKey={}", fileId, objectKey, exception);
|
||||
|
||||
+152
-33
@@ -19,6 +19,7 @@ import org.springblade.core.tool.utils.BeanUtil;
|
||||
import org.springblade.core.tool.utils.DateUtil;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.core.tool.utils.WebUtil;
|
||||
import org.springblade.core.tool.jackson.JsonUtil;
|
||||
import org.springblade.system.cache.DictCache;
|
||||
import org.springblade.system.cache.DictBizCache;
|
||||
import org.springblade.system.cache.UserCache;
|
||||
@@ -51,10 +52,10 @@ import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
@@ -68,7 +69,6 @@ import java.util.stream.Collectors;
|
||||
@RequiredArgsConstructor
|
||||
public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImportBatchMapper, WaybillImportBatch> implements IWaybillImportBatchService {
|
||||
|
||||
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
/** 批量导入状态仅保留草稿与导入完成两种。 */
|
||||
private static final String STATUS_DRAFT = "draft";
|
||||
private static final String STATUS_COMPLETED = "completed";
|
||||
@@ -184,20 +184,36 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
|
||||
|
||||
List<Map<String, Object>> rows = Func.isEmpty(request.getRows()) ? List.of() : request.getRows();
|
||||
|
||||
// 逐行构建运单(未落库),构建失败时报出对应行号
|
||||
List<Waybill> rowWaybills = new ArrayList<>();
|
||||
for (int index = 0; index < rows.size(); index++) {
|
||||
try {
|
||||
rowWaybills.add(buildWaybill(rows.get(index), batch, request.getCarrierContractId(), draft));
|
||||
} catch (Exception exception) {
|
||||
throw new ServiceException("第" + (index + 1) + "行" + (draft ? "保存" : "导入") + "失败:" + exception.getMessage());
|
||||
}
|
||||
}
|
||||
// 同一运单标识号的多货物行合并为一条运单,各行转为货物明细
|
||||
Map<Integer, Waybill> waybillByRow = new HashMap<>();
|
||||
Set<Integer> skipRows = new HashSet<>();
|
||||
mergeMultiCargoRows(rows, rowWaybills, waybillByRow, skipRows);
|
||||
|
||||
List<Waybill> waybills = new ArrayList<>();
|
||||
Map<String, List<Waybill>> loadingWaybills = new TreeMap<>();
|
||||
for (int index = 0; index < rows.size(); index++) {
|
||||
if (skipRows.contains(index)) continue;
|
||||
Waybill waybill = waybillByRow.getOrDefault(index, rowWaybills.get(index));
|
||||
// 配载标识号必须取自行数据:运单落库(prepareForSave)会把新建运单的 loadingNo 置空
|
||||
String loadingIdentifier = stringValue(rows.get(index), "loadingIdentifier", "配载标识号");
|
||||
try {
|
||||
String loadingIdentifier = stringValue(rows.get(index), "loadingIdentifier", "配载标识号");
|
||||
Waybill waybill = buildWaybill(rows.get(index), batch, request.getCarrierContractId(), draft);
|
||||
if (draft) waybillService.saveDraft(waybill); else waybillService.submit(waybill);
|
||||
waybills.add(waybill);
|
||||
if (!draft && IMPORT_TYPE_WAYBILL.equals(batch.getImportType()) && Func.isNotEmpty(loadingIdentifier)) {
|
||||
loadingWaybills.computeIfAbsent(loadingIdentifier, key -> new ArrayList<>()).add(waybill);
|
||||
}
|
||||
} catch (Exception exception) {
|
||||
throw new ServiceException("第" + (index + 1) + "行" + (draft ? "保存" : "导入") + "失败:" + exception.getMessage());
|
||||
}
|
||||
waybills.add(waybill);
|
||||
if (!draft && IMPORT_TYPE_WAYBILL.equals(batch.getImportType()) && Func.isNotEmpty(loadingIdentifier)) {
|
||||
loadingWaybills.computeIfAbsent(loadingIdentifier, key -> new ArrayList<>()).add(waybill);
|
||||
}
|
||||
}
|
||||
loadingWaybills.forEach(loadingManageService::createFromImportedWaybills);
|
||||
batch.setWaybillCount(waybills.size());
|
||||
@@ -218,6 +234,69 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将同一运单标识号的多货物行合并为一条运单:
|
||||
* 首行作为主行(数值字段累加各行),后续行转为货物明细追加到主行货物信息中;
|
||||
* 被 merge 的行记入 skipRows,落库时跳过。
|
||||
*/
|
||||
private void mergeMultiCargoRows(List<Map<String, Object>> rows, List<Waybill> rowWaybills,
|
||||
Map<Integer, Waybill> waybillByRow, Set<Integer> skipRows) {
|
||||
Map<String, List<Integer>> groups = new LinkedHashMap<>();
|
||||
for (int index = 0; index < rowWaybills.size(); index++) {
|
||||
String relationNo = rowWaybills.get(index).getRelationNo();
|
||||
if (Func.isEmpty(relationNo)) continue;
|
||||
groups.computeIfAbsent(relationNo, key -> new ArrayList<>()).add(index);
|
||||
}
|
||||
for (List<Integer> groupRows : groups.values()) {
|
||||
if (groupRows.size() <= 1) continue;
|
||||
Waybill primary = rowWaybills.get(groupRows.get(0));
|
||||
List<Map<String, Object>> goodsList = new ArrayList<>();
|
||||
for (Integer rowIndex : groupRows) {
|
||||
Waybill current = rowWaybills.get(rowIndex);
|
||||
if (!rowIndex.equals(groupRows.get(0))) {
|
||||
// 主行字段保留首行值,数量等数值字段累加同组其余各行
|
||||
mergeNumericFields(primary, current);
|
||||
skipRows.add(rowIndex);
|
||||
waybillByRow.remove(rowIndex);
|
||||
}
|
||||
goodsList.add(buildGoodsItem(rows.get(rowIndex), current));
|
||||
}
|
||||
primary.setGoodsJson(JsonUtil.toJson(goodsList));
|
||||
}
|
||||
}
|
||||
|
||||
/** 合并多货物行时累加数量、其他费用等可加数值字段;为空的字段跳过。 */
|
||||
private void mergeNumericFields(Waybill primary, Waybill current) {
|
||||
primary.setQuantity(sumNullable(primary.getQuantity(), current.getQuantity()));
|
||||
primary.setOtherFeeTotal(sumNullable(primary.getOtherFeeTotal(), current.getOtherFeeTotal()));
|
||||
}
|
||||
|
||||
private BigDecimal sumNullable(BigDecimal first, BigDecimal second) {
|
||||
if (first == null) return second;
|
||||
if (second == null) return first;
|
||||
return first.add(second);
|
||||
}
|
||||
|
||||
/** 由导入行构建一条货物明细,字段与运单详情货物编辑结构保持一致。 */
|
||||
private Map<String, Object> buildGoodsItem(Map<String, Object> row, Waybill waybill) {
|
||||
Map<String, Object> goods = new LinkedHashMap<>();
|
||||
putIfNotEmpty(goods, "cargoName", waybill.getCargoName());
|
||||
putIfNotEmpty(goods, "cargoType", waybill.getCargoType());
|
||||
putIfNotEmpty(goods, "specification", waybill.getSpecification());
|
||||
putIfNotEmpty(goods, "model", waybill.getModel());
|
||||
putIfNotEmpty(goods, "packageType", stringValue(row, "packageType", "包装"));
|
||||
if (waybill.getQuantity() != null) {
|
||||
goods.put("quantity", waybill.getQuantity().toPlainString());
|
||||
}
|
||||
putIfNotEmpty(goods, "quantityUnit", waybill.getQuantityUnit());
|
||||
putIfNotEmpty(goods, "remark", waybill.getRemark());
|
||||
return goods;
|
||||
}
|
||||
|
||||
private void putIfNotEmpty(Map<String, Object> goods, String key, String value) {
|
||||
if (Func.isNotEmpty(value)) goods.put(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IPage<WaybillImportBatchVO> page(IPage<WaybillImportBatch> page, WaybillImportBatchRequest request) {
|
||||
LambdaQueryWrapper<WaybillImportBatch> queryWrapper = Wrappers.<WaybillImportBatch>lambdaQuery()
|
||||
@@ -386,11 +465,12 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
|
||||
throw new ServiceException(fieldName + "不能为空");
|
||||
}
|
||||
String text = String.valueOf(value).trim();
|
||||
try {
|
||||
return text.length() == 10 ? LocalDate.parse(text) : LocalDate.parse(text, DATE_TIME_FORMATTER);
|
||||
} catch (DateTimeParseException exception) {
|
||||
throw new ServiceException(fieldName + "格式必须为 YYYY-MM-DD HH:mm:ss");
|
||||
// 宽容解析:接受 2026-8-2、2026-9-1 8:0:0 等写法(口径见根工作区 docs/import-spec.md)。
|
||||
LocalDate date = org.springblade.common.excel.LenientDateParser.parseDateLenientlyOrNull(text);
|
||||
if (date == null) {
|
||||
throw new ServiceException(fieldName + " 日期格式无法识别:" + text);
|
||||
}
|
||||
return date;
|
||||
}
|
||||
|
||||
private BigDecimal defaultQuantity(BigDecimal quantity) {
|
||||
@@ -523,6 +603,9 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
|
||||
// 16. 同一运单标识号校验
|
||||
validateWaybillIdentifier(row, i, waybillIdentifierMap, rows, errors);
|
||||
|
||||
// 17. 同一运单标识号组内一致性校验(地址/配载标识号/数量单位,多货物合并的前提)
|
||||
validateWaybillIdentifierConsistency(row, i, waybillIdentifierMap, rows, errors);
|
||||
|
||||
if (!errors.isEmpty()) {
|
||||
errorMap.put(i, String.join("; ", errors));
|
||||
}
|
||||
@@ -571,21 +654,13 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
|
||||
if (!isRoadTransport(transportType)) {
|
||||
return;
|
||||
}
|
||||
if (Func.isEmpty(vehicleNo)) {
|
||||
errors.add("公路运输时车牌号/航班号/船号/班列号不能为空");
|
||||
return;
|
||||
}
|
||||
if (vehicleNo.length() < 7) {
|
||||
errors.add("车牌号长度不能少于7位");
|
||||
}
|
||||
if (vehicleNo.length() > 8) {
|
||||
errors.add("车牌号长度不能超过8位");
|
||||
}
|
||||
if (!VEHICLE_PROVINCE_PATTERN.matcher(vehicleNo).lookingAt()) {
|
||||
errors.add("车牌号首位必须是省份简称,第二位必须是英文字母");
|
||||
}
|
||||
if (!VEHICLE_NO_PATTERN.matcher(vehicleNo).matches()) {
|
||||
errors.add("车牌号格式不正确,应为首位省份简称、次位英文字母、总长度7或8位");
|
||||
// 必填、长度与格式统一为一条简短原因,避免多条规则叠加导致失败原因过长;
|
||||
// 文案保留“车牌号”字样,导入失败明细仍能据此定位到车牌号列并标红。
|
||||
// VEHICLE_NO_PATTERN 已约束总长度为 7 或 8 位,故不再单独判断长度。
|
||||
if (Func.isEmpty(vehicleNo)
|
||||
|| !VEHICLE_PROVINCE_PATTERN.matcher(vehicleNo).lookingAt()
|
||||
|| !VEHICLE_NO_PATTERN.matcher(vehicleNo).matches()) {
|
||||
errors.add("车牌号校验不通过");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -826,18 +901,62 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 同一运单标识号组内的发货地址、到货地址、数量单位、配载标识号必须一致:
|
||||
* 多货物行会合并为一条运单,上述字段不一致时无法合并(数量累加也要求单位相同)。
|
||||
*/
|
||||
private void validateWaybillIdentifierConsistency(Map<String, Object> row, int rowIndex,
|
||||
Map<String, List<Integer>> waybillIdentifierMap, List<Map<String, Object>> allRows, List<String> errors) {
|
||||
String waybillIdentifier = stringValue(row, "waybillIdentifier", "同一运单标识号");
|
||||
if (Func.isEmpty(waybillIdentifier)) {
|
||||
return;
|
||||
}
|
||||
List<Integer> sameIdentifierRows = waybillIdentifierMap.get(waybillIdentifier);
|
||||
if (sameIdentifierRows == null || sameIdentifierRows.size() <= 1) {
|
||||
return;
|
||||
}
|
||||
compareGroupValue(rowIndex, sameIdentifierRows, allRows, "departureAddress", "发货地址", errors);
|
||||
compareGroupValue(rowIndex, sameIdentifierRows, allRows, "arrivalAddress", "到货地址", errors);
|
||||
compareGroupValue(rowIndex, sameIdentifierRows, allRows, "quantityUnit", "数量单位", errors);
|
||||
compareGroupValue(rowIndex, sameIdentifierRows, allRows, "loadingIdentifier", "配载标识号", errors);
|
||||
}
|
||||
|
||||
/** 比较同一运单标识号组内其余行的字段值,不一致时记录错误;空值不参与比较。 */
|
||||
private void compareGroupValue(int rowIndex, List<Integer> sameIdentifierRows, List<Map<String, Object>> allRows,
|
||||
String field, String fieldName, List<String> errors) {
|
||||
String currentValue = normalizeCompareValue(stringValue(allRows.get(rowIndex), field, fieldName));
|
||||
if (Func.isEmpty(currentValue)) {
|
||||
return;
|
||||
}
|
||||
for (Integer otherRowIndex : sameIdentifierRows) {
|
||||
if (otherRowIndex <= rowIndex) {
|
||||
continue;
|
||||
}
|
||||
String otherValue = normalizeCompareValue(stringValue(allRows.get(otherRowIndex), field, fieldName));
|
||||
if (Func.isNotEmpty(otherValue) && !currentValue.equals(otherValue)) {
|
||||
errors.add("同一运单标识号下," + fieldName + "必须一致");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String normalizeCompareValue(String value) {
|
||||
return Func.isEmpty(value) ? null : value.replaceAll("\\s+", "");
|
||||
}
|
||||
|
||||
private LocalDate parseDateForValidation(Object value, String fieldName, List<String> errors) {
|
||||
if (value == null || String.valueOf(value).isBlank()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String text = String.valueOf(value).trim();
|
||||
try {
|
||||
return text.length() == 10 ? LocalDate.parse(text) : LocalDate.parse(text, DATE_TIME_FORMATTER);
|
||||
} catch (DateTimeParseException exception) {
|
||||
errors.add(fieldName + "格式必须为日期格式(YYYY-MM-DD 或 YYYY-MM-DD HH:mm:ss)");
|
||||
// 宽容解析:接受 2026-8-2、2026-9-1 8:0:0 等写法(口径见根工作区 docs/import-spec.md)。
|
||||
LocalDate date = org.springblade.common.excel.LenientDateParser.parseDateLenientlyOrNull(text);
|
||||
if (date == null) {
|
||||
errors.add(fieldName + " 日期格式无法识别:" + text);
|
||||
return null;
|
||||
}
|
||||
return date;
|
||||
}
|
||||
|
||||
private Map<String, String> loadTransportTypeOptions() {
|
||||
@@ -861,8 +980,8 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
|
||||
// 字典读取失败时使用默认值。
|
||||
}
|
||||
if (options.isEmpty()) {
|
||||
List<String> defaults = List.of("公路运输", "铁路运输", "水路运输", "航空运输",
|
||||
"公路整车", "公路配载/零担", "铁路整车", "铁路零担", "水路", "航空", "多式联运", "管道运输", "其他");
|
||||
// 运输方式仅支持公路、铁路、水路、航空四种
|
||||
List<String> defaults = List.of("公路运输", "铁路运输", "水路运输", "航空运输");
|
||||
defaults.forEach(value -> options.put(value, value));
|
||||
}
|
||||
return options;
|
||||
|
||||
+337
-9
@@ -25,17 +25,19 @@ package org.springblade.transport.service.impl;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.mp.base.BaseServiceImpl;
|
||||
import org.springblade.core.tenant.annotation.TenantIgnore;
|
||||
import org.springblade.core.tool.jackson.JsonUtil;
|
||||
import org.springblade.core.tool.utils.BeanUtil;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.system.cache.UserCache;
|
||||
import org.springblade.system.pojo.entity.Dept;
|
||||
import org.springblade.thirdparty.lbs.feign.ILbsClient;
|
||||
import org.springblade.thirdparty.lbs.pojo.dto.LbsLocateRequest;
|
||||
import org.springblade.thirdparty.lbs.pojo.vo.LbsLocateResponse;
|
||||
import org.springblade.transport.excel.WaybillExcel;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Arrays;
|
||||
import org.springblade.transport.mapper.WaybillEnroutePunchMapper;
|
||||
import org.springblade.transport.mapper.WaybillMapper;
|
||||
import org.springblade.transport.mapper.WaybillNodePunchMapper;
|
||||
@@ -49,6 +51,8 @@ import org.springblade.transport.pojo.entity.WaybillNodePunch;
|
||||
import org.springblade.transport.pojo.dto.WaybillMileageRequest;
|
||||
import org.springblade.transport.pojo.vo.BusinessRemoveResultVO;
|
||||
import org.springblade.transport.pojo.vo.LoadingManageVO;
|
||||
import org.springblade.transport.pojo.vo.WaybillLocateVO;
|
||||
import org.springblade.transport.pojo.vo.WaybillTrackVO;
|
||||
import org.springblade.transport.pojo.vo.WaybillPunchPhotoVO;
|
||||
import org.springblade.transport.pojo.vo.WaybillPunchRecordItemVO;
|
||||
import org.springblade.transport.pojo.vo.WaybillPunchRecordsVO;
|
||||
@@ -67,7 +71,11 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
@@ -111,6 +119,9 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
|
||||
@jakarta.annotation.Resource
|
||||
private WaybillEnroutePunchMapper waybillEnroutePunchMapper;
|
||||
|
||||
@jakarta.annotation.Resource
|
||||
private ILbsClient lbsClient;
|
||||
|
||||
@Override
|
||||
public IPage<WaybillVO> selectWaybillPage(IPage<Waybill> page, WaybillVO waybill) {
|
||||
IPage<Waybill> entityPage = page(page, buildQuery(waybill));
|
||||
@@ -128,6 +139,233 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
@TenantIgnore
|
||||
public WaybillVO publicDetail(Long id) {
|
||||
if (Func.isEmpty(id)) {
|
||||
throw new ServiceException("主键不能为空");
|
||||
}
|
||||
Waybill waybill = getById(id);
|
||||
if (Func.isEmpty(waybill) || Objects.equals(waybill.getIsDeleted(), 1)) {
|
||||
throw new ServiceException("运单管理不存在");
|
||||
}
|
||||
WaybillVO waybillVO = WaybillWrapper.build().entityVO(waybill);
|
||||
waybillVO.setReadonly(true);
|
||||
fillCustomerNameFromContract(waybillVO);
|
||||
return waybillVO;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WaybillLocateVO locateVehicle(Long id) {
|
||||
Waybill waybill = requireWaybillWithVehicleNo(id, "无法实时定位");
|
||||
String vehicleNo = waybill.getVehicleNo().trim();
|
||||
LbsLocateResponse response = invokeLbs(id, vehicleNo, new LbsLocateRequest(vehicleNo), false, "实时定位");
|
||||
List<Map<String, Object>> pointMaps = extractLbsPointMaps(response);
|
||||
if (pointMaps.isEmpty()) {
|
||||
throw new ServiceException("暂无车辆实时定位数据");
|
||||
}
|
||||
return buildLocateVO(id, vehicleNo, pointMaps.get(0));
|
||||
}
|
||||
|
||||
@Override
|
||||
public WaybillTrackVO trackVehicle(Long id, String startDate, String endDate) {
|
||||
Waybill waybill = requireWaybillWithVehicleNo(id, "无法查询历史轨迹");
|
||||
String vehicleNo = waybill.getVehicleNo().trim();
|
||||
String normalizedStart = normalizeTrackDate(startDate, "开始日期");
|
||||
String normalizedEnd = normalizeTrackDate(endDate, "结束日期");
|
||||
if (LocalDate.parse(normalizedStart).isAfter(LocalDate.parse(normalizedEnd))) {
|
||||
throw new ServiceException("开始日期不能晚于结束日期");
|
||||
}
|
||||
LbsLocateResponse response = invokeLbs(id, vehicleNo,
|
||||
new LbsLocateRequest(vehicleNo, normalizedStart, normalizedEnd), true, "历史轨迹");
|
||||
List<Map<String, Object>> pointMaps = extractLbsPointMaps(response);
|
||||
WaybillTrackVO trackVO = new WaybillTrackVO();
|
||||
trackVO.setWaybillId(id);
|
||||
trackVO.setVehicleNo(vehicleNo);
|
||||
trackVO.setStartDate(normalizedStart);
|
||||
trackVO.setEndDate(normalizedEnd);
|
||||
List<WaybillTrackVO.WaybillTrackPointVO> points = new ArrayList<>();
|
||||
for (Map<String, Object> pointMap : pointMaps) {
|
||||
WaybillTrackVO.WaybillTrackPointVO point = buildTrackPoint(vehicleNo, pointMap);
|
||||
if (point.getLongitude() != null && point.getLatitude() != null) {
|
||||
points.add(point);
|
||||
}
|
||||
}
|
||||
trackVO.setPoints(points);
|
||||
trackVO.setTotal(points.size());
|
||||
return trackVO;
|
||||
}
|
||||
|
||||
private Waybill requireWaybillWithVehicleNo(Long id, String actionTip) {
|
||||
if (id == null) {
|
||||
throw new ServiceException("运单ID不能为空");
|
||||
}
|
||||
Waybill waybill = getById(id);
|
||||
if (waybill == null || Objects.equals(waybill.getIsDeleted(), 1)) {
|
||||
throw new ServiceException("运单不存在");
|
||||
}
|
||||
String vehicleNo = Func.toStr(waybill.getVehicleNo(), "").trim();
|
||||
if (Func.isEmpty(vehicleNo)) {
|
||||
throw new ServiceException("运单未绑定车牌号," + actionTip);
|
||||
}
|
||||
waybill.setVehicleNo(vehicleNo);
|
||||
return waybill;
|
||||
}
|
||||
|
||||
private LbsLocateResponse invokeLbs(Long waybillId, String vehicleNo, LbsLocateRequest request,
|
||||
boolean trackMode, String scene) {
|
||||
LbsLocateResponse response;
|
||||
try {
|
||||
response = trackMode ? lbsClient.track(request) : lbsClient.locate(request);
|
||||
} catch (Exception exception) {
|
||||
log.error("调用LBS{}失败 waybillId={}, vehicleNo={}", scene, waybillId, vehicleNo, exception);
|
||||
throw new ServiceException("调用车辆" + scene + "接口失败");
|
||||
}
|
||||
if (response == null || !response.isSuccess()) {
|
||||
String errorMessage = response == null ? "车辆" + scene + "无返回" : response.errorMessage();
|
||||
throw new ServiceException(errorMessage);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
private String normalizeTrackDate(String dateText, String fieldName) {
|
||||
if (Func.isEmpty(dateText)) {
|
||||
throw new ServiceException(fieldName + "不能为空");
|
||||
}
|
||||
String normalized = dateText.trim();
|
||||
try {
|
||||
return LocalDate.parse(normalized, DateTimeFormatter.ISO_LOCAL_DATE).toString();
|
||||
} catch (DateTimeParseException exception) {
|
||||
throw new ServiceException(fieldName + "格式必须为YYYY-MM-DD");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取 LBS 轨迹点:优先 list,其次 obj,再兼容 data / data.list / data.obj
|
||||
*/
|
||||
private List<Map<String, Object>> extractLbsPointMaps(LbsLocateResponse response) {
|
||||
List<Map<String, Object>> pointMaps = new ArrayList<>();
|
||||
if (response == null) {
|
||||
return pointMaps;
|
||||
}
|
||||
appendLbsNodes(pointMaps, response.getList());
|
||||
if (!pointMaps.isEmpty()) {
|
||||
return pointMaps;
|
||||
}
|
||||
appendLbsNodes(pointMaps, response.getObj());
|
||||
if (!pointMaps.isEmpty()) {
|
||||
return pointMaps;
|
||||
}
|
||||
JsonNode dataNode = response.getData();
|
||||
if (dataNode != null && !dataNode.isNull()) {
|
||||
if (dataNode.isObject() && dataNode.has("list")) {
|
||||
appendLbsNodes(pointMaps, dataNode.get("list"));
|
||||
if (!pointMaps.isEmpty()) {
|
||||
return pointMaps;
|
||||
}
|
||||
}
|
||||
if (dataNode.isObject() && dataNode.has("obj")) {
|
||||
appendLbsNodes(pointMaps, dataNode.get("obj"));
|
||||
if (!pointMaps.isEmpty()) {
|
||||
return pointMaps;
|
||||
}
|
||||
}
|
||||
appendLbsNodes(pointMaps, dataNode);
|
||||
}
|
||||
return pointMaps;
|
||||
}
|
||||
|
||||
private void appendLbsNodes(List<Map<String, Object>> pointMaps, JsonNode node) {
|
||||
if (node == null || node.isNull()) {
|
||||
return;
|
||||
}
|
||||
if (node.isArray()) {
|
||||
for (JsonNode item : node) {
|
||||
if (item != null && item.isObject()) {
|
||||
pointMaps.add(JsonUtil.toMap(item.toString()));
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (node.isObject()) {
|
||||
pointMaps.add(JsonUtil.toMap(node.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 LBS 单点数据归一化为实时定位结果
|
||||
*/
|
||||
private WaybillLocateVO buildLocateVO(Long waybillId, String vehicleNo, Map<String, Object> pointMap) {
|
||||
WaybillLocateVO locateVO = new WaybillLocateVO();
|
||||
locateVO.setWaybillId(waybillId);
|
||||
locateVO.setVehicleNo(vehicleNo);
|
||||
locateVO.setRawData(pointMap);
|
||||
fillPointFields(locateVO, pointMap);
|
||||
return locateVO;
|
||||
}
|
||||
|
||||
private WaybillTrackVO.WaybillTrackPointVO buildTrackPoint(String vehicleNo, Map<String, Object> pointMap) {
|
||||
WaybillTrackVO.WaybillTrackPointVO point = new WaybillTrackVO.WaybillTrackPointVO();
|
||||
point.setVehicleNo(vehicleNo);
|
||||
fillPointFields(point, pointMap);
|
||||
return point;
|
||||
}
|
||||
|
||||
private void fillPointFields(WaybillLocateVO target, Map<String, Object> pointMap) {
|
||||
target.setLongitude(firstDecimal(pointMap, "longitude", "lon", "lng", "x", "jd", "Longitude", "LON", "LNG", "JD", "X"));
|
||||
target.setLatitude(firstDecimal(pointMap, "latitude", "lat", "y", "wd", "Latitude", "LAT", "WD", "Y"));
|
||||
target.setAddress(firstText(pointMap, "address", "addr", "adr", "wz", "Address", "ADDR", "ADR"));
|
||||
target.setLocateTime(firstText(pointMap, "utc", "locationTime", "pos_time", "locateTime", "gpsTime", "gpstime", "time", "gpszdsj", "GPSTime", "Time"));
|
||||
target.setSpeed(firstText(pointMap, "spd", "speed", "v", "sd", "Speed", "SD", "V"));
|
||||
target.setDirection(firstText(pointMap, "direct", "direction", "h", "fx", "Direction", "FX", "course", "H"));
|
||||
String responseVehicleNo = firstText(pointMap, "vno", "cph", "vehicleNo", "plateNo", "CPH", "VNO");
|
||||
if (Func.isNotEmpty(responseVehicleNo)) {
|
||||
target.setVehicleNo(responseVehicleNo);
|
||||
}
|
||||
}
|
||||
|
||||
private void fillPointFields(WaybillTrackVO.WaybillTrackPointVO target, Map<String, Object> pointMap) {
|
||||
target.setLongitude(firstDecimal(pointMap, "longitude", "lon", "lng", "x", "jd", "Longitude", "LON", "LNG", "JD", "X"));
|
||||
target.setLatitude(firstDecimal(pointMap, "latitude", "lat", "y", "wd", "Latitude", "LAT", "WD", "Y"));
|
||||
target.setAddress(firstText(pointMap, "address", "addr", "adr", "wz", "Address", "ADDR", "ADR"));
|
||||
target.setLocateTime(firstText(pointMap, "utc", "locationTime", "pos_time", "locateTime", "gpsTime", "gpstime", "time", "gpszdsj", "GPSTime", "Time"));
|
||||
target.setSpeed(firstText(pointMap, "spd", "speed", "v", "sd", "Speed", "SD", "V"));
|
||||
target.setDirection(firstText(pointMap, "direct", "direction", "h", "fx", "Direction", "FX", "course", "H"));
|
||||
String responseVehicleNo = firstText(pointMap, "vno", "cph", "vehicleNo", "plateNo", "CPH", "VNO");
|
||||
if (Func.isNotEmpty(responseVehicleNo)) {
|
||||
target.setVehicleNo(responseVehicleNo);
|
||||
}
|
||||
}
|
||||
|
||||
private BigDecimal firstDecimal(Map<String, Object> rawData, String... keys) {
|
||||
String text = firstText(rawData, keys);
|
||||
if (Func.isEmpty(text)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return new BigDecimal(text.trim());
|
||||
} catch (NumberFormatException exception) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String firstText(Map<String, Object> rawData, String... keys) {
|
||||
if (rawData == null || rawData.isEmpty() || keys == null) {
|
||||
return null;
|
||||
}
|
||||
for (String key : keys) {
|
||||
Object value = rawData.get(key);
|
||||
if (value == null) {
|
||||
continue;
|
||||
}
|
||||
String text = String.valueOf(value).trim();
|
||||
if (Func.isNotEmpty(text) && !"null".equalsIgnoreCase(text)) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WaybillPunchRecordsVO listPunchRecords(Long waybillId) {
|
||||
WaybillPunchRecordsVO vo = new WaybillPunchRecordsVO();
|
||||
@@ -446,9 +684,15 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean submit(Waybill waybill) {
|
||||
boolean created = Func.isEmpty(waybill.getId());
|
||||
prepareForSave(waybill);
|
||||
validate(waybill);
|
||||
return saveOrUpdate(waybill);
|
||||
boolean result = saveOrUpdate(waybill);
|
||||
// 复制后确认提交为已完成时,按合同计费规则生成应收应付
|
||||
if (result && created) {
|
||||
generateReceivablePayableIfCompleted(waybill);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -475,8 +719,13 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
|
||||
}
|
||||
} else {
|
||||
waybill.setLoadingNo(null);
|
||||
waybill.setMasterNo(null);
|
||||
waybill.setMileageRemark(null);
|
||||
}
|
||||
// 新建若已带业务状态(如复制提交)则在过程校正后回写,保证与原运单一致
|
||||
String preservedBusinessStatus = created
|
||||
? TransportBusinessSupport.trimToNull(waybill.getBusinessStatus())
|
||||
: null;
|
||||
// 无过程快照时按项目回填;再按接单设置决定 pending / running
|
||||
fillProjectProcessConfig(waybill);
|
||||
prepare(waybill);
|
||||
@@ -486,6 +735,9 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
|
||||
clearDriverAcceptRecord(waybill);
|
||||
}
|
||||
applyDriverAcceptBusinessStatus(waybill);
|
||||
if (Func.isNotEmpty(preservedBusinessStatus)) {
|
||||
waybill.setBusinessStatus(preservedBusinessStatus);
|
||||
}
|
||||
fillCustomerName(waybill);
|
||||
if (created && Func.isEmpty(waybill.getWaybillNo())) {
|
||||
waybill.setWaybillNo(nextCode());
|
||||
@@ -656,7 +908,13 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
|
||||
WaybillExcel excel = data.get(index);
|
||||
try {
|
||||
Waybill waybill = Objects.requireNonNull(BeanUtil.copyProperties(excel, Waybill.class));
|
||||
waybill.setEstimatedStartTime(org.springblade.common.excel.LenientDateParser.parseDate(excel.getEstimatedStartTime(), "预计发货日期"));
|
||||
waybill.setEstimatedEndTime(org.springblade.common.excel.LenientDateParser.parseDate(excel.getEstimatedEndTime(), "预计完成日期"));
|
||||
waybill.setStartDate(org.springblade.common.excel.LenientDateParser.parseDate(excel.getStartDate(), "开始日期"));
|
||||
waybill.setEndDate(org.springblade.common.excel.LenientDateParser.parseDate(excel.getEndDate(), "结束日期"));
|
||||
waybill.setDataSource("批量导入");
|
||||
waybill.setCreateTime(null);
|
||||
waybill.setUpdateTime(null);
|
||||
waybill.setCarrierJson(buildImportCarrierJson(waybill));
|
||||
submit(waybill);
|
||||
} catch (Exception exception) {
|
||||
@@ -717,13 +975,12 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
|
||||
target.setOtherFeeTotal(source.getOtherFeeTotal());
|
||||
target.setTaskRemark(source.getTaskRemark());
|
||||
target.setOriginalNo(source.getOriginalNo());
|
||||
target.setBusinessStatus(source.getBusinessStatus());
|
||||
target.setDataSource(source.getDataSource());
|
||||
target.setStartDate(source.getStartDate());
|
||||
target.setEndDate(source.getEndDate());
|
||||
target.setPlanId(source.getPlanId());
|
||||
target.setPlanName(source.getPlanName());
|
||||
target.setMasterNo(source.getMasterNo());
|
||||
target.setMasterNo(null);
|
||||
target.setLoadingNo(null);
|
||||
target.setBatchNo(source.getBatchNo());
|
||||
target.setRelationNo(source.getRelationNo());
|
||||
@@ -736,17 +993,38 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
|
||||
target.setFreightJson(source.getFreightJson());
|
||||
target.setAttachmentsJson(source.getAttachmentsJson());
|
||||
target.setRemark(source.getRemark());
|
||||
target.setBusinessStatus("pending");
|
||||
// 复制后业务状态与原运单保持一致
|
||||
String sourceBusinessStatus = source.getBusinessStatus();
|
||||
target.setBusinessStatus(sourceBusinessStatus);
|
||||
target.setWaybillNo(nextCode());
|
||||
clearDriverAcceptRecord(target);
|
||||
fillProjectProcessConfig(target);
|
||||
prepare(target);
|
||||
applyDriverAcceptBusinessStatus(target);
|
||||
if (Func.isNotEmpty(sourceBusinessStatus)) {
|
||||
target.setBusinessStatus(sourceBusinessStatus);
|
||||
}
|
||||
validate(target);
|
||||
save(target);
|
||||
// 原运单已完成时,复制后按合同系统计费规则生成应收应付明细
|
||||
generateReceivablePayableIfCompleted(target);
|
||||
return detail(target.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 新建/复制运单若业务状态为已完成,按合同系统计费模式生成应收、应付明细。
|
||||
* <p>使用同事务入口,避免 REQUIRES_NEW 读不到尚未提交的新运单。</p>
|
||||
*/
|
||||
private void generateReceivablePayableIfCompleted(Waybill waybill) {
|
||||
if (waybill == null || Func.isEmpty(waybill.getId())) {
|
||||
return;
|
||||
}
|
||||
if (!"completed".equals(waybill.getBusinessStatus())) {
|
||||
return;
|
||||
}
|
||||
receivablePayableDetailService.generateForImportedWaybills(List.of(waybill));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean changeRoute(Waybill waybill) {
|
||||
@@ -789,7 +1067,11 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
|
||||
TransportBusinessSupport.validateLength(mileageRemark, 200, "里程维护备注不能超过200字");
|
||||
waybill.setMileage(mileage);
|
||||
waybill.setMileageRemark(mileageRemark);
|
||||
return updateById(waybill);
|
||||
boolean updated = updateById(waybill);
|
||||
if (updated) {
|
||||
receivablePayableDetailService.syncMileageFromWaybill(waybill.getId(), mileage);
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -807,10 +1089,20 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean reassign(Waybill request) {
|
||||
return doReassign(request, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean reassignWithoutDeptCheck(Waybill request) {
|
||||
return doReassign(request, false);
|
||||
}
|
||||
|
||||
private boolean doReassign(Waybill request, boolean checkDept) {
|
||||
if (request == null || Func.isEmpty(request.getId())) {
|
||||
throw new ServiceException("运单ID不能为空");
|
||||
}
|
||||
Waybill waybill = loadEditable(request.getId(), true);
|
||||
Waybill waybill = loadEditable(request.getId(), checkDept);
|
||||
assertNotLoaded(waybill);
|
||||
if (!"pending".equals(waybill.getBusinessStatus()) && !"running".equals(waybill.getBusinessStatus())) {
|
||||
throw new ServiceException("仅待执行/进行中运单允许重新派单");
|
||||
@@ -1066,6 +1358,8 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
|
||||
if (isSentinelMinusOne(waybill.getQuantity())) waybill.setQuantity(null);
|
||||
if (isSentinelMinusOne(waybill.getMileage())) waybill.setMileage(null);
|
||||
if (isSentinelMinusOne(waybill.getUnitPrice())) waybill.setUnitPrice(null);
|
||||
if (isSentinelMinusOne(waybill.getOtherFeeTotal())) waybill.setOtherFeeTotal(null);
|
||||
syncQuantityFromGoodsJson(waybill);
|
||||
waybill.setWaybillNo(TransportBusinessSupport.trimToNull(waybill.getWaybillNo()));
|
||||
waybill.setProjectName(TransportBusinessSupport.trimToNull(waybill.getProjectName()));
|
||||
waybill.setContractName(TransportBusinessSupport.trimToNull(waybill.getContractName()));
|
||||
@@ -1133,6 +1427,40 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
|
||||
return value != null && value.compareTo(BigDecimal.ONE.negate()) == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 完整录入多货物时,运单主表 quantity 按 goodsJson 数量合计回写,避免只落首行数量。
|
||||
*/
|
||||
private void syncQuantityFromGoodsJson(Waybill waybill) {
|
||||
if (waybill == null || Func.isEmpty(waybill.getGoodsJson())) {
|
||||
return;
|
||||
}
|
||||
List<Map> goodsList;
|
||||
try {
|
||||
goodsList = JsonUtil.parseArray(waybill.getGoodsJson(), Map.class);
|
||||
} catch (Exception exception) {
|
||||
return;
|
||||
}
|
||||
if (Func.isEmpty(goodsList) || goodsList.size() <= 1) {
|
||||
return;
|
||||
}
|
||||
BigDecimal total = BigDecimal.ZERO;
|
||||
boolean hasQuantity = false;
|
||||
for (Map goods : goodsList) {
|
||||
if (goods == null || goods.get("quantity") == null) {
|
||||
continue;
|
||||
}
|
||||
BigDecimal quantity = new BigDecimal(String.valueOf(goods.get("quantity")).trim());
|
||||
if (isSentinelMinusOne(quantity)) {
|
||||
continue;
|
||||
}
|
||||
hasQuantity = true;
|
||||
total = total.add(quantity);
|
||||
}
|
||||
if (hasQuantity) {
|
||||
waybill.setQuantity(total);
|
||||
}
|
||||
}
|
||||
|
||||
private void validate(Waybill waybill) {
|
||||
TransportBusinessSupport.validateRequired(waybill.getProjectName(), "项目不能为空");
|
||||
TransportBusinessSupport.validateRequired(waybill.getContractName(), "客户合同不能为空");
|
||||
|
||||
+5
@@ -27,6 +27,7 @@ import org.springblade.core.tool.utils.BeanUtil;
|
||||
import org.springblade.transport.pojo.entity.ContractManage;
|
||||
import org.springblade.transport.pojo.vo.ContractManageVO;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
@@ -47,6 +48,10 @@ public class ContractManageWrapper extends BaseEntityWrapper<ContractManage, Con
|
||||
contractManageVO.setUpdateUserName(org.springblade.system.cache.UserCache.getUserRealName(contractManage.getUpdateUser()));
|
||||
contractManageVO.setContractStageName(stageName(contractManage.getContractStage()));
|
||||
contractManageVO.setApprovalStatusName(statusName(contractManage.getApprovalStatus()));
|
||||
if (contractManageVO.getContractAmount() != null
|
||||
&& contractManageVO.getContractAmount().compareTo(BigDecimal.valueOf(-1)) == 0) {
|
||||
contractManageVO.setContractAmount(null);
|
||||
}
|
||||
return contractManageVO;
|
||||
}
|
||||
|
||||
|
||||
+6
@@ -12,6 +12,7 @@ import org.springblade.system.cache.UserCache;
|
||||
import org.springblade.transport.pojo.entity.LoadingManage;
|
||||
import org.springblade.transport.pojo.vo.LoadingManageVO;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
@@ -33,6 +34,11 @@ public class LoadingManageWrapper extends BaseEntityWrapper<LoadingManage, Loadi
|
||||
Long currentDeptId = Func.firstLong(AuthUtil.getDeptId());
|
||||
loadingManageVO.setReadonly(currentDeptId != null && !Objects.equals(loadingManage.getDeptId(), currentDeptId));
|
||||
loadingManageVO.setBusinessStatusName(businessStatusName(loadingManage.getBusinessStatus()));
|
||||
// 未维护里程的占位值 -1 对外展示为空
|
||||
if (loadingManageVO.getMileage() != null
|
||||
&& loadingManageVO.getMileage().compareTo(BigDecimal.valueOf(-1)) == 0) {
|
||||
loadingManageVO.setMileage(null);
|
||||
}
|
||||
return loadingManageVO;
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -40,7 +40,7 @@ public class PaymentApplicationWrapper extends BaseEntityWrapper<PaymentApplicat
|
||||
vo.setPaymentTypeName(switch (entity.getPaymentType() == null ? "" : entity.getPaymentType()) {
|
||||
case "project_advance" -> "项目预付";
|
||||
case "progress_advance" -> "进度预付";
|
||||
case TransportBusiConstant.PAYMENT_TYPE_SETTLEMENT -> "结算付款";
|
||||
case TransportBusiConstant.PAYMENT_TYPE_SETTLEMENT -> "尾款付款";
|
||||
default -> entity.getPaymentType();
|
||||
});
|
||||
vo.setApprovalStatusName(switch (entity.getApprovalStatus() == null ? "" : entity.getApprovalStatus()) {
|
||||
|
||||
+15
@@ -32,6 +32,7 @@ import org.springblade.transport.pojo.vo.WaybillVO;
|
||||
import org.springblade.transport.support.TransportBusinessSupport;
|
||||
import org.springblade.transport.support.WaybillProcessSupport;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
@@ -58,9 +59,23 @@ public class WaybillWrapper extends BaseEntityWrapper<Waybill, WaybillVO> {
|
||||
waybillVO.setBusinessStatus(displayStatus);
|
||||
waybillVO.setBusinessStatusName(businessStatusName(displayStatus));
|
||||
waybillVO.setRequireAccept(WaybillProcessSupport.requiresDriverAcceptConfirmation(waybill.getProcessJson()));
|
||||
// 未维护里程等占位值 -1 对外展示为空
|
||||
if (isSentinelMinusOne(waybillVO.getMileage())) {
|
||||
waybillVO.setMileage(null);
|
||||
}
|
||||
if (isSentinelMinusOne(waybillVO.getQuantity())) {
|
||||
waybillVO.setQuantity(null);
|
||||
}
|
||||
if (isSentinelMinusOne(waybillVO.getUnitPrice())) {
|
||||
waybillVO.setUnitPrice(null);
|
||||
}
|
||||
return waybillVO;
|
||||
}
|
||||
|
||||
private static boolean isSentinelMinusOne(BigDecimal value) {
|
||||
return value != null && value.compareTo(BigDecimal.valueOf(-1)) == 0;
|
||||
}
|
||||
|
||||
public static String businessStatusName(String status) {
|
||||
if (status == null) {
|
||||
return "未知";
|
||||
|
||||
@@ -1,2 +1,23 @@
|
||||
# blade-transport 开发环境配置
|
||||
# 数据源等公共配置已在 Nacos 的 blade-dev.yaml 中统一管理
|
||||
#服务器端口
|
||||
server:
|
||||
port: 8110
|
||||
|
||||
#数据源配置
|
||||
spring:
|
||||
datasource:
|
||||
url: ${blade.datasource.dev.url}
|
||||
username: ${blade.datasource.dev.username}
|
||||
password: ${blade.datasource.dev.password}
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
# 本机多网卡时避免注册 169.254.* 导致网关 Host is down
|
||||
ip: ${NACOS_DISCOVERY_IP:127.0.0.1}
|
||||
|
||||
# LBS 车辆实时定位(Authorization 与 OA 人员接口一致)
|
||||
thirdParty:
|
||||
lbs:
|
||||
baseUrl: ${LBS_BASE_URL:http://172.16.204.83:38000}
|
||||
locateUrl: ${LBS_LOCATE_URL:/gwzh/LBS/LBS_LOCATE}
|
||||
trackUrl: ${LBS_TRACK_URL:/gwzh/LBS/LBS_TRACK}
|
||||
authorization: ${LBS_AUTHORIZATION:${OA_AUTHORIZATION:${IAM_SSO_AUTHORIZATION:Z3d6aF90bXMtOVJJU0RVN1U6YW0yYkcwWnBJZ0RQZmtrSjNaZkZjUDBSaGFuSEtxQng=}}}
|
||||
|
||||
Reference in New Issue
Block a user