1、新增总单

2、fix bug
This commit is contained in:
2026-08-11 23:50:37 +08:00
parent 4e7e354349
commit 7f31952bff
12 changed files with 617 additions and 0 deletions
@@ -0,0 +1,17 @@
package org.springblade.transport.pojo.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
import java.util.Map;
/**
* 多联总单调度请求。
*/
@Data
@Schema(description = "多联总单调度请求")
public class MasterOrderDispatchRequest {
private Long id;
private List<Map<String, Object>> dispatches;
}
@@ -0,0 +1,51 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
*/
package org.springblade.transport.pojo.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springblade.core.tenant.mp.TenantEntity;
import java.io.Serial;
import java.time.LocalDateTime;
/**
* 多联总单实体类
*
* @author Chill
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("blade_master_order")
@Schema(description = "多联总单")
public class MasterOrder extends TenantEntity {
@Serial
private static final long serialVersionUID = 1L;
private String masterNo;
private Long projectId;
private String projectName;
private Long contractId;
private String contractName;
private String customerName;
private String transportOrganizationType;
private String businessStatus;
private String departureName;
private String departureAddress;
private String departureContact;
private String departurePhone;
private String arrivalName;
private String arrivalAddress;
private String arrivalContact;
private String arrivalPhone;
private LocalDateTime planStartTime;
private LocalDateTime planEndTime;
private String routeJson;
private String goodsJson;
private String attachmentsJson;
private String remark;
}
@@ -48,6 +48,12 @@ public class TransportPlan extends TenantEntity {
@Schema(description = "计划单号") @Schema(description = "计划单号")
private String planNo; private String planNo;
@Schema(description = "多联总单号")
private String masterNo;
@Schema(description = "总单分段号")
private String relationNo;
@Schema(description = "计划名称") @Schema(description = "计划名称")
private String planName; private String planName;
@@ -0,0 +1,45 @@
/** BladeX Commercial License Agreement */
package org.springblade.transport.pojo.vo;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springblade.transport.pojo.entity.MasterOrder;
import java.io.Serial;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
/**
* 多联总单视图实体类
*
* @author Chill
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class MasterOrderVO extends MasterOrder {
@Serial
private static final long serialVersionUID = 1L;
@TableField(exist = false)
private String masterNos;
@TableField(exist = false)
private String cargoName;
@TableField(exist = false)
private String cargoType;
@TableField(exist = false)
private String contractNo;
@TableField(exist = false)
private List<Map<String, Object>> routes;
@TableField(exist = false)
private List<Map<String, Object>> goods;
@TableField(exist = false)
private List<Map<String, Object>> routeProgress;
@TableField(exist = false)
private BigDecimal totalQuantity;
@TableField(exist = false)
private String createUserName;
@TableField(exist = false)
private String updateUserName;
}
@@ -63,6 +63,10 @@ public class ProjectApplyVO extends ProjectApply {
@Schema(description = "审批状态名称") @Schema(description = "审批状态名称")
private String approvalStatusName; private String approvalStatusName;
@TableField(exist = false)
@Schema(description = "审批状态集合,多个状态使用逗号分隔")
private String approvalStatuses;
@TableField(exist = false) @TableField(exist = false)
@Schema(description = "生效类型名称") @Schema(description = "生效类型名称")
private String effectiveTypeName; private String effectiveTypeName;
@@ -0,0 +1,48 @@
/** BladeX Commercial License Agreement */
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.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import org.springblade.core.boot.ctrl.BladeController;
import org.springblade.core.excel.util.ExcelUtil;
import org.springblade.core.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.MasterOrderDispatchRequest;
import org.springblade.transport.pojo.vo.MasterOrderVO;
import org.springblade.transport.service.IMasterOrderService;
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;
/**
* 多联总单控制器
*
* @author Chill
*/
@RestController
@AllArgsConstructor
@PreAuth(menu = "master_order")
@RequestMapping("/master-order")
@Tag(name = "多联总单", description = "多联总单管理")
public class MasterOrderController extends BladeController {
private final IMasterOrderService masterOrderService;
@GetMapping("/detail") @ApiOperationSupport(order = 1) @Operation(summary = "详情") public R<MasterOrderVO> detail(@RequestParam Long id) { return R.data(masterOrderService.detail(id)); }
@GetMapping("/list") @ApiOperationSupport(order = 2) @Operation(summary = "分页") public R<IPage<MasterOrderVO>> list(MasterOrderVO query, Query page) { return R.data(masterOrderService.selectPage(Condition.getPage(page), query)); }
@PostMapping("/submit") @ApiOperationSupport(order = 3) @Operation(summary = "确认创建或编辑") public R<MasterOrderVO> submit(@RequestBody MasterOrderVO data) { return R.data(masterOrderService.submit(data, false)); }
@PostMapping("/draft") @ApiOperationSupport(order = 4) @Operation(summary = "暂存草稿") public R<MasterOrderVO> draft(@RequestBody MasterOrderVO data) { return R.data(masterOrderService.submit(data, true)); }
@PostMapping("/copy") @ApiOperationSupport(order = 5) @Operation(summary = "复制") public R<MasterOrderVO> copy(@RequestParam Long id) { return R.data(masterOrderService.copy(id)); }
@PostMapping("/remove") @ApiOperationSupport(order = 6) @Operation(summary = "删除") public R status(@RequestParam Long id) { return R.status(masterOrderService.removeMasterOrder(id)); }
@PostMapping("/close-dispatch") @ApiOperationSupport(order = 7) @Operation(summary = "关闭调度") public R closeDispatch(@RequestParam Long id) { return R.status(masterOrderService.closeDispatch(id)); }
@PostMapping("/dispatch") @ApiOperationSupport(order = 8) @Operation(summary = "确认调度") public R<MasterOrderVO> dispatch(@RequestBody MasterOrderDispatchRequest data) { return R.data(masterOrderService.dispatch(data)); }
@GetMapping("/export") @ApiOperationSupport(order = 9) @Operation(summary = "按运单导出") public void export(MasterOrderVO query, HttpServletResponse response) { ExcelUtil.export(response, "总单运单明细", "运单明细", masterOrderService.exportWaybills(query), MasterOrderVO.class); }
}
@@ -0,0 +1,13 @@
/** BladeX Commercial License Agreement */
package org.springblade.transport.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.springblade.transport.pojo.entity.MasterOrder;
/**
* 多联总单 Mapper 接口
*
* @author Chill
*/
public interface MasterOrderMapper extends BaseMapper<MasterOrder> {
}
@@ -0,0 +1,26 @@
/** BladeX Commercial License Agreement */
package org.springblade.transport.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import org.springblade.core.mp.base.BaseService;
import org.springblade.transport.pojo.dto.MasterOrderDispatchRequest;
import org.springblade.transport.pojo.entity.MasterOrder;
import org.springblade.transport.pojo.vo.MasterOrderVO;
import java.util.List;
/**
* 多联总单 服务类
*
* @author Chill
*/
public interface IMasterOrderService extends BaseService<MasterOrder> {
IPage<MasterOrderVO> selectPage(IPage<MasterOrder> page, MasterOrderVO query);
MasterOrderVO detail(Long id);
MasterOrderVO submit(MasterOrderVO masterOrder, boolean draft);
MasterOrderVO copy(Long id);
boolean removeMasterOrder(Long id);
boolean closeDispatch(Long id);
MasterOrderVO dispatch(MasterOrderDispatchRequest request);
List<MasterOrderVO> exportWaybills(MasterOrderVO query);
}
@@ -0,0 +1,366 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
*/
package org.springblade.transport.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
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.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.transport.mapper.MasterOrderMapper;
import org.springblade.transport.pojo.dto.MasterOrderDispatchRequest;
import org.springblade.transport.pojo.entity.MasterOrder;
import org.springblade.transport.pojo.entity.ContractManage;
import org.springblade.transport.pojo.entity.ProjectApply;
import org.springblade.transport.pojo.entity.TransportPlan;
import org.springblade.transport.pojo.entity.Waybill;
import org.springblade.transport.pojo.vo.MasterOrderVO;
import org.springblade.transport.service.IMasterOrderService;
import org.springblade.transport.service.IContractManageService;
import org.springblade.transport.service.IProjectApplyService;
import org.springblade.transport.service.ITransportPlanService;
import org.springblade.transport.service.IWaybillService;
import org.springblade.transport.support.TransportBusinessSupport;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* 多联总单 服务实现类
*
* @author Chill
*/
@Service
public class MasterOrderServiceImpl extends BaseServiceImpl<MasterOrderMapper, MasterOrder> implements IMasterOrderService {
private final IWaybillService waybillService;
private final ITransportPlanService transportPlanService;
private final IProjectApplyService projectApplyService;
private final IContractManageService contractManageService;
public MasterOrderServiceImpl(IWaybillService waybillService, ITransportPlanService transportPlanService, IProjectApplyService projectApplyService,
IContractManageService contractManageService) {
this.waybillService = waybillService;
this.transportPlanService = transportPlanService;
this.projectApplyService = projectApplyService;
this.contractManageService = contractManageService;
}
@Override
public IPage<MasterOrderVO> selectPage(IPage<MasterOrder> page, MasterOrderVO query) {
IPage<MasterOrder> records = page(page, buildQuery(query));
return records.convert(this::toVO);
}
@Override
public MasterOrderVO detail(Long id) {
MasterOrder masterOrder = getRequired(id);
return toVO(masterOrder);
}
@Override
@Transactional(rollbackFor = Exception.class)
public MasterOrderVO submit(MasterOrderVO request, boolean draft) {
boolean created = Func.isEmpty(request.getId());
MasterOrder target = created ? new MasterOrder() : getRequired(request.getId());
if (!created && ("dispatching".equals(target.getBusinessStatus()) || "completed".equals(target.getBusinessStatus()))) {
assertRestrictedEdit(target, request);
}
if (!created && "closed".equals(target.getBusinessStatus())) throw new ServiceException("调度关闭的总单不能编辑");
copyWritable(request, target);
if (created) target.setMasterNo(nextCode());
target.setTransportOrganizationType("多式联运");
target.setBusinessStatus(draft ? "draft" : ("dispatching".equals(target.getBusinessStatus()) ? "dispatching" : "waiting_dispatch"));
validate(target, draft);
saveOrUpdate(target);
return detail(target.getId());
}
@Override
@Transactional(rollbackFor = Exception.class)
public MasterOrderVO copy(Long id) {
MasterOrder source = getRequired(id);
MasterOrder target = new MasterOrder();
BeanUtil.copyProperties(source, target);
target.setId(null);
target.setMasterNo(nextCode());
target.setBusinessStatus("draft");
target.setStatus(1);
save(target);
return detail(target.getId());
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean removeMasterOrder(Long id) {
MasterOrder masterOrder = getRequired(id);
if (!"draft".equals(masterOrder.getBusinessStatus()) && !"waiting_dispatch".equals(masterOrder.getBusinessStatus())) {
throw new ServiceException("当前状态不允许删除");
}
return deleteLogic(Collections.singletonList(id));
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean closeDispatch(Long id) {
MasterOrder masterOrder = getRequired(id);
if (!"dispatching".equals(masterOrder.getBusinessStatus()) && !"completed".equals(masterOrder.getBusinessStatus())) {
throw new ServiceException("当前状态不允许关闭调度");
}
masterOrder.setBusinessStatus("closed");
return updateById(masterOrder);
}
@Override
@Transactional(rollbackFor = Exception.class)
public MasterOrderVO dispatch(MasterOrderDispatchRequest request) {
MasterOrder masterOrder = getRequired(request.getId());
if (!"waiting_dispatch".equals(masterOrder.getBusinessStatus()) && !"dispatching".equals(masterOrder.getBusinessStatus())) {
throw new ServiceException("当前状态不允许调度");
}
if (Func.isEmpty(request.getDispatches())) throw new ServiceException("请至少加入一条调度记录");
validateDispatches(masterOrder, request.getDispatches());
Map<String, List<Map<String, Object>>> planGroups = new LinkedHashMap<>();
Map<String, List<Map<String, Object>>> waybillGroups = new LinkedHashMap<>();
for (Map<String, Object> dispatch : request.getDispatches()) {
if ("计划单".equals(string(dispatch, "documentType"))) {
if (!string(dispatch, "transportType", "").contains("公路")) throw new ServiceException("仅公路运输可生成计划单");
planGroups.computeIfAbsent(string(dispatch, "batchNo", string(dispatch, "relationNo")), key -> new ArrayList<>()).add(dispatch);
} else {
waybillGroups.computeIfAbsent(waybillGroupKey(dispatch), key -> new ArrayList<>()).add(dispatch);
}
}
for (List<Map<String, Object>> dispatches : planGroups.values()) createTransportPlan(masterOrder, dispatches);
for (List<Map<String, Object>> dispatches : waybillGroups.values()) createWaybill(masterOrder, dispatches);
refreshStatus(masterOrder);
return detail(masterOrder.getId());
}
@Override
public List<MasterOrderVO> exportWaybills(MasterOrderVO query) {
List<MasterOrderVO> result = new ArrayList<>();
for (MasterOrder masterOrder : list(buildQuery(query))) {
for (Waybill waybill : waybillService.list(new LambdaQueryWrapper<Waybill>().eq(Waybill::getMasterNo, masterOrder.getMasterNo()))) {
MasterOrderVO row = toVO(masterOrder);
row.setCargoName(waybill.getCargoName());
row.setCargoType(waybill.getCargoType());
row.setMasterNo(waybill.getWaybillNo());
result.add(row);
}
}
return result;
}
private void createWaybill(MasterOrder masterOrder, List<Map<String, Object>> dispatches) {
Map<String, Object> dispatch = dispatches.get(0);
Waybill waybill = new Waybill();
waybill.setProjectId(masterOrder.getProjectId()); waybill.setProjectName(masterOrder.getProjectName());
waybill.setContractId(masterOrder.getContractId()); waybill.setContractName(masterOrder.getContractName());
waybill.setCustomerName(masterOrder.getCustomerName()); waybill.setMasterNo(masterOrder.getMasterNo());
waybill.setTransportType(string(dispatch, "transportType")); waybill.setCarrierType(string(dispatch, "carrierType"));
waybill.setCarrierName(string(dispatch, "carrierName")); waybill.setDriverName(string(dispatch, "driverName"));
waybill.setDriverPhone(string(dispatch, "driverPhone")); waybill.setVehicleNo(string(dispatch, "vehicleNo")); waybill.setTrailerVehicleNo(string(dispatch, "trailerVehicleNo"));
waybill.setEscortName(string(dispatch, "escortName")); waybill.setEscortPhone(string(dispatch, "escortPhone")); waybill.setMileage(decimal(dispatch, "mileage"));
waybill.setDepartureName(string(dispatch, "departureName")); waybill.setDepartureAddress(string(dispatch, "departureAddress"));
waybill.setArrivalName(string(dispatch, "arrivalName")); waybill.setArrivalAddress(string(dispatch, "arrivalAddress"));
waybill.setCargoName(joinGoodsField(dispatches, "cargoName")); waybill.setCargoType(joinGoodsField(dispatches, "cargoType"));
BigDecimal quantity = dispatches.stream().map(item -> decimal(item, "quantity")).reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal unitPrice = decimal(dispatch, "unitPrice");
BigDecimal freightTotal = dispatches.stream().map(item -> decimal(item, "quantity").multiply(decimal(item, "unitPrice"))).reduce(BigDecimal.ZERO, BigDecimal::add);
waybill.setQuantity(quantity); waybill.setQuantityUnit(string(dispatch, "quantityUnit", ""));
waybill.setEstimatedStartTime(date(dispatch, "estimatedStartTime")); waybill.setEstimatedEndTime(date(dispatch, "estimatedEndTime"));
waybill.setUnitPrice(unitPrice); waybill.setPriceUnit(string(dispatch, "priceUnit"));
waybill.setGoodsJson(JsonUtil.toJson(dispatches));
waybill.setFreightJson(JsonUtil.toJson(Map.of("weightTotal", quantity, "freightTotal", freightTotal))); waybill.setBusinessStatus("pending");
waybill.setTaskEntryMode(string(dispatch, "documentType", "运单")); waybill.setRelationNo(string(dispatch, "relationNo")); waybill.setCarrierJson(JsonUtil.toJson(dispatch));
waybillService.submit(waybill);
}
private void createTransportPlan(MasterOrder masterOrder, List<Map<String, Object>> dispatches) {
Map<String, Object> first = dispatches.get(0);
TransportPlan plan = new TransportPlan();
plan.setMasterNo(masterOrder.getMasterNo()); plan.setRelationNo(string(first, "segmentNo"));
plan.setPlanName(masterOrder.getMasterNo() + "-" + string(first, "segmentNo") + "运输计划");
plan.setProjectId(masterOrder.getProjectId()); plan.setProjectName(masterOrder.getProjectName());
plan.setContractId(masterOrder.getContractId()); plan.setContractName(masterOrder.getContractName()); plan.setCustomerName(masterOrder.getCustomerName());
plan.setTransportType(string(first, "transportType"));
plan.setDepartureName(string(first, "departureName")); plan.setDepartureAddress(string(first, "departureAddress"));
plan.setDepartureContact(string(first, "departureContact")); plan.setDeparturePhone(string(first, "departurePhone"));
plan.setArrivalName(string(first, "arrivalName")); plan.setArrivalAddress(string(first, "arrivalAddress"));
plan.setArrivalContact(string(first, "arrivalContact")); plan.setArrivalPhone(string(first, "arrivalPhone"));
plan.setPlanStartDate(date(first, "estimatedStartTime")); plan.setPlanEndDate(date(first, "estimatedEndTime"));
plan.setGoodsJson(JsonUtil.toJson(dispatches)); plan.setDataSource("多联总单调度"); plan.setBusinessStatus("waiting_dispatch");
plan.setRemark(string(first, "remark"));
transportPlanService.submit(plan);
}
private void refreshStatus(MasterOrder masterOrder) {
Map<String, BigDecimal> available = availableGoods(masterOrder);
List<Map<String, Object>> routes = parseArray(masterOrder.getRouteJson());
boolean completed = !routes.isEmpty() && !available.isEmpty() && routes.stream().allMatch(route -> isSegmentComplete(masterOrder.getMasterNo(), string(route, "segmentNo"), available));
masterOrder.setBusinessStatus(completed ? "completed" : "dispatching");
updateById(masterOrder);
}
private LambdaQueryWrapper<MasterOrder> buildQuery(MasterOrderVO query) {
LambdaQueryWrapper<MasterOrder> wrapper = new LambdaQueryWrapper<>();
if (Func.isNotEmpty(query.getMasterNos())) wrapper.in(MasterOrder::getMasterNo, Arrays.asList(query.getMasterNos().split(",")));
if (Func.isNotEmpty(query.getProjectName())) wrapper.like(MasterOrder::getProjectName, query.getProjectName());
if (Func.isNotEmpty(query.getCustomerName())) wrapper.like(MasterOrder::getCustomerName, query.getCustomerName());
if (Func.isNotEmpty(query.getBusinessStatus())) wrapper.eq(MasterOrder::getBusinessStatus, query.getBusinessStatus());
if (Func.isNotEmpty(query.getCargoName())) wrapper.like(MasterOrder::getGoodsJson, query.getCargoName());
if (Func.isNotEmpty(query.getCargoType())) wrapper.like(MasterOrder::getGoodsJson, query.getCargoType());
if (Func.isNotEmpty(query.getDepartureAddress())) wrapper.like(MasterOrder::getDepartureAddress, query.getDepartureAddress());
if (Func.isNotEmpty(query.getArrivalAddress())) wrapper.like(MasterOrder::getArrivalAddress, query.getArrivalAddress());
if (query.getPlanStartTime() != null) wrapper.ge(MasterOrder::getPlanStartTime, query.getPlanStartTime());
if (query.getPlanEndTime() != null) wrapper.le(MasterOrder::getPlanEndTime, query.getPlanEndTime());
return wrapper.orderByDesc(MasterOrder::getCreateTime);
}
private MasterOrderVO toVO(MasterOrder entity) {
MasterOrderVO vo = Objects.requireNonNull(BeanUtil.copyProperties(entity, MasterOrderVO.class));
ContractManage contract = resolveContract(entity);
vo.setContractNo(contract == null ? null : contract.getContractNo());
vo.setRoutes(parseArray(entity.getRouteJson())); vo.setGoods(parseArray(entity.getGoodsJson())); vo.setTotalQuantity(totalQuantity(vo.getGoods()));
vo.setRouteProgress(buildProgress(entity, vo.getRoutes()));
vo.setCreateUserName(UserCache.getUserRealName(entity.getCreateUser())); vo.setUpdateUserName(UserCache.getUserRealName(entity.getUpdateUser()));
return vo;
}
private ContractManage resolveContract(MasterOrder masterOrder) {
if (Func.isNotEmpty(masterOrder.getContractId())) {
ContractManage contract = contractManageService.getById(masterOrder.getContractId());
if (contract != null) return contract;
}
if (Func.isEmpty(masterOrder.getContractName())) return null;
LambdaQueryWrapper<ContractManage> query = new LambdaQueryWrapper<ContractManage>().eq(ContractManage::getContractName, masterOrder.getContractName());
if (Func.isNotEmpty(masterOrder.getProjectId())) query.eq(ContractManage::getProjectId, masterOrder.getProjectId());
return contractManageService.getOne(query, false);
}
private List<Map<String, Object>> buildProgress(MasterOrder masterOrder, List<Map<String, Object>> routes) {
List<Waybill> waybills = waybillService.list(new LambdaQueryWrapper<Waybill>().eq(Waybill::getMasterNo, masterOrder.getMasterNo()));
List<TransportPlan> plans = transportPlanService.list(new LambdaQueryWrapper<TransportPlan>().eq(TransportPlan::getMasterNo, masterOrder.getMasterNo()));
for (Map<String, Object> route : routes) {
String segmentNo = string(route, "segmentNo");
route.put("dispatchedQuantity", dispatchedQuantity(masterOrder.getMasterNo(), segmentNo));
route.put("dispatchedGoods", dispatchedGoods(masterOrder.getMasterNo(), segmentNo));
route.put("arrivedQuantity", waybills.stream().filter(item -> Objects.equals(segmentNo, item.getRelationNo()) && "completed".equals(item.getBusinessStatus())).map(Waybill::getQuantity).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add));
route.put("waybills", waybills.stream().filter(item -> Objects.equals(segmentNo, item.getRelationNo())).toList());
route.put("transportPlans", plans.stream().filter(item -> Objects.equals(segmentNo, item.getRelationNo())).toList());
}
return routes;
}
private void copyWritable(MasterOrderVO source, MasterOrder target) {
target.setProjectId(source.getProjectId()); target.setProjectName(source.getProjectName()); target.setContractId(source.getContractId()); target.setContractName(source.getContractName()); target.setCustomerName(source.getCustomerName());
target.setDepartureName(source.getDepartureName()); target.setDepartureAddress(source.getDepartureAddress()); target.setDepartureContact(source.getDepartureContact()); target.setDeparturePhone(source.getDeparturePhone());
target.setArrivalName(source.getArrivalName()); target.setArrivalAddress(source.getArrivalAddress()); target.setArrivalContact(source.getArrivalContact()); target.setArrivalPhone(source.getArrivalPhone());
target.setPlanStartTime(source.getPlanStartTime()); target.setPlanEndTime(source.getPlanEndTime()); target.setRouteJson(JsonUtil.toJson(source.getRoutes())); target.setGoodsJson(JsonUtil.toJson(source.getGoods())); target.setAttachmentsJson(source.getAttachmentsJson()); target.setRemark(source.getRemark());
}
private void assertRestrictedEdit(MasterOrder oldRecord, MasterOrderVO request) {
if (!Objects.equals(oldRecord.getProjectId(), request.getProjectId()) || !Objects.equals(oldRecord.getRouteJson(), JsonUtil.toJson(request.getRoutes()))) throw new ServiceException("调度中或调度完成的总单不能修改基本信息和路线");
}
private void validate(MasterOrder masterOrder, boolean draft) {
TransportBusinessSupport.validateRequired(masterOrder.getProjectName(), "项目不能为空");
if (Func.isEmpty(masterOrder.getProjectId())) throw new ServiceException("项目不能为空");
ProjectApply project = projectApplyService.getById(masterOrder.getProjectId());
if (project == null || (!"approved".equals(project.getApprovalStatus()) && !"change_approved".equals(project.getApprovalStatus()))) {
throw new ServiceException("仅审批通过或变更审批通过的项目可创建总单");
}
if (!draft) {
if (Func.isEmpty(masterOrder.getContractId())) throw new ServiceException("客户合同不能为空");
ContractManage contract = contractManageService.getById(masterOrder.getContractId());
if (contract == null || !Objects.equals(contract.getProjectId(), project.getId())) throw new ServiceException("客户合同不属于当前项目");
}
TransportBusinessSupport.validateRequired(masterOrder.getDepartureAddress(), "发货地址不能为空");
TransportBusinessSupport.validateRequired(masterOrder.getArrivalAddress(), "收货地址不能为空");
TransportBusinessSupport.validateLength(masterOrder.getRemark(), 200, "备注不能超过200字");
TransportBusinessSupport.validatePhone(masterOrder.getDeparturePhone(), "发货联系方式格式不正确"); TransportBusinessSupport.validatePhone(masterOrder.getArrivalPhone(), "收货联系方式格式不正确");
if (!draft && totalQuantity(parseArray(masterOrder.getGoodsJson())).compareTo(BigDecimal.ZERO) <= 0) throw new ServiceException("请至少维护一条数量大于0的货物");
}
private MasterOrder getRequired(Long id) { MasterOrder item = getById(id); if (item == null) throw new ServiceException("总单不存在"); return item; }
private List<Map<String, Object>> parseArray(String json) { if (Func.isEmpty(json)) return new ArrayList<>(); try { return JsonUtil.parse(json, List.class); } catch (Exception exception) { return new ArrayList<>(); } }
private void validateDispatches(MasterOrder masterOrder, List<Map<String, Object>> dispatches) {
Map<String, BigDecimal> available = availableGoods(masterOrder);
Map<String, Map<String, BigDecimal>> usedBySegment = new LinkedHashMap<>();
for (Map<String, Object> dispatch : dispatches) {
String segmentNo = string(dispatch, "segmentNo");
if (Func.isEmpty(segmentNo)) throw new ServiceException("调度分段不能为空");
String key = goodsKey(dispatch);
BigDecimal quantity = decimal(dispatch, "quantity");
if (quantity.compareTo(BigDecimal.ZERO) <= 0) throw new ServiceException("本次数量必须大于0");
if (!available.containsKey(key)) throw new ServiceException("调度货物必须来自总单配置");
LocalDate startDate = date(dispatch, "estimatedStartTime"); LocalDate endDate = date(dispatch, "estimatedEndTime");
if (startDate == null || endDate == null) throw new ServiceException("预计发货日期和预计完成日期不能为空");
if (startDate.isAfter(endDate)) throw new ServiceException("预计发货日期不能晚于预计完成日期");
if ("运单".equals(string(dispatch, "documentType", "运单"))) validateCarrier(dispatch);
Map<String, BigDecimal> used = usedBySegment.computeIfAbsent(segmentNo, value -> dispatchedGoods(masterOrder.getMasterNo(), value));
used.merge(key, quantity, BigDecimal::add);
if (used.get(key).compareTo(available.get(key)) > 0) throw new ServiceException("货物【" + string(dispatch, "cargoName") + "】的调度数量超过可调度数量");
}
}
private Map<String, BigDecimal> availableGoods(MasterOrder masterOrder) {
Map<String, BigDecimal> available = new LinkedHashMap<>();
for (Map<String, Object> goods : parseArray(masterOrder.getGoodsJson())) available.merge(goodsKey(goods), decimal(goods, "quantity"), BigDecimal::add);
return available;
}
private boolean isSegmentComplete(String masterNo, String segmentNo, Map<String, BigDecimal> available) {
Map<String, BigDecimal> dispatched = dispatchedGoods(masterNo, segmentNo);
return available.entrySet().stream().allMatch(item -> dispatched.getOrDefault(item.getKey(), BigDecimal.ZERO).compareTo(item.getValue()) >= 0);
}
private void validateCarrier(Map<String, Object> dispatch) {
String carrierType = string(dispatch, "carrierType", "承运商");
if (Func.isEmpty(string(dispatch, "vehicleNo"))) throw new ServiceException("运单车牌号不能为空");
if ("承运商".equals(carrierType)) {
if (Func.isEmpty(string(dispatch, "carrierName"))) throw new ServiceException("运单承运商不能为空");
return;
}
if (Func.isEmpty(string(dispatch, "driverName")) || Func.isEmpty(string(dispatch, "driverPhone")) || Func.isEmpty(string(dispatch, "trailerVehicleNo")) || Func.isEmpty(string(dispatch, "escortName")) || Func.isEmpty(string(dispatch, "escortPhone")) || Func.isEmpty(string(dispatch, "mileage")) || decimal(dispatch, "mileage").compareTo(BigDecimal.ZERO) < 0) throw new ServiceException("自运或网货平台的车辆与人员信息不完整");
}
private BigDecimal dispatchedQuantity(String masterNo, String segmentNo) {
return dispatchedGoods(masterNo, segmentNo).values().stream().reduce(BigDecimal.ZERO, BigDecimal::add);
}
private Map<String, BigDecimal> dispatchedGoods(String masterNo, String segmentNo) {
Map<String, BigDecimal> result = new LinkedHashMap<>();
LambdaQueryWrapper<Waybill> billQuery = new LambdaQueryWrapper<Waybill>().eq(Waybill::getMasterNo, masterNo);
if (Func.isNotEmpty(segmentNo)) billQuery.eq(Waybill::getRelationNo, segmentNo);
for (Waybill bill : waybillService.list(billQuery)) {
List<Map<String, Object>> goods = parseArray(bill.getGoodsJson());
if (goods.isEmpty()) goods = List.of(Map.of("cargoName", bill.getCargoName(), "cargoType", bill.getCargoType(), "quantity", bill.getQuantity()));
for (Map<String, Object> goodsItem : goods) result.merge(goodsKey(goodsItem), decimal(goodsItem, "quantity"), BigDecimal::add);
}
LambdaQueryWrapper<TransportPlan> planQuery = new LambdaQueryWrapper<TransportPlan>().eq(TransportPlan::getMasterNo, masterNo);
if (Func.isNotEmpty(segmentNo)) planQuery.eq(TransportPlan::getRelationNo, segmentNo);
for (TransportPlan plan : transportPlanService.list(planQuery)) for (Map<String, Object> goods : parseArray(plan.getGoodsJson())) result.merge(goodsKey(goods), decimal(goods, "quantity"), BigDecimal::add);
return result;
}
private BigDecimal totalQuantity(List<Map<String, Object>> goods) { return goods.stream().map(item -> decimal(item, "quantity")).reduce(BigDecimal.ZERO, BigDecimal::add); }
private String waybillGroupKey(Map<String, Object> dispatch) { return String.join("\u0000", string(dispatch, "segmentNo", ""), string(dispatch, "carrierType", ""), string(dispatch, "carrierName", ""), string(dispatch, "driverName", ""), string(dispatch, "vehicleNo", "")); }
private String joinGoodsField(List<Map<String, Object>> dispatches, String field) { return dispatches.stream().map(item -> string(item, field, "")).filter(Func::isNotEmpty).distinct().reduce((left, right) -> left + "" + right).orElse(""); }
private BigDecimal decimal(Map<String, Object> values, String key) { try { return new BigDecimal(string(values, key, "0")); } catch (Exception exception) { return BigDecimal.ZERO; } }
private String goodsKey(Map<String, Object> values) { return goodsKey(string(values, "cargoName"), string(values, "cargoType")); }
private String goodsKey(String cargoName, String cargoType) { return String.valueOf(cargoName) + "\u0000" + String.valueOf(cargoType); }
private LocalDate date(Map<String, Object> values, String key) { String value = string(values, key); if (Func.isEmpty(value)) return null; try { return LocalDate.parse(value.substring(0, 10)); } catch (Exception exception) { throw new ServiceException("日期格式不正确"); } }
private String string(Map<String, Object> values, String key) { return string(values, key, null); }
private String string(Map<String, Object> values, String key, String fallback) { Object value = values.get(key); return value == null ? fallback : String.valueOf(value); }
private synchronized String nextCode() { String prefix = "DL" + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE); long count = count(new LambdaQueryWrapper<MasterOrder>().likeRight(MasterOrder::getMasterNo, prefix)); return prefix + String.format("%04d", count + 1); }
}
@@ -47,6 +47,7 @@ import java.math.RoundingMode;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
@@ -277,6 +278,16 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl<ProjectApplyMapper,
.eq(Func.isNotEmpty(projectApply.getApprovalStatus()), ProjectApply::getApprovalStatus, projectApply.getApprovalStatus()) .eq(Func.isNotEmpty(projectApply.getApprovalStatus()), ProjectApply::getApprovalStatus, projectApply.getApprovalStatus())
.eq(Func.isNotEmpty(projectApply.getEffectiveType()), ProjectApply::getEffectiveType, projectApply.getEffectiveType()) .eq(Func.isNotEmpty(projectApply.getEffectiveType()), ProjectApply::getEffectiveType, projectApply.getEffectiveType())
.orderByDesc(ProjectApply::getCreateTime); .orderByDesc(ProjectApply::getCreateTime);
if (Func.isNotEmpty(projectApply.getApprovalStatuses())) {
List<String> approvalStatusList = Arrays.stream(projectApply.getApprovalStatuses().split(","))
.map(String::trim)
.filter(Func::isNotEmpty)
.distinct()
.toList();
if (!approvalStatusList.isEmpty()) {
queryWrapper.in(ProjectApply::getApprovalStatus, approvalStatusList);
}
}
if (Boolean.TRUE.equals(projectApply.getTemporaryCreditLimitSelectable())) { if (Boolean.TRUE.equals(projectApply.getTemporaryCreditLimitSelectable())) {
queryWrapper.eq(ProjectApply::getEffectiveType, EFFECTIVE_FORMAL) queryWrapper.eq(ProjectApply::getEffectiveType, EFFECTIVE_FORMAL)
.in(ProjectApply::getApprovalStatus, STATUS_APPROVED, "change_approved"); .in(ProjectApply::getApprovalStatus, STATUS_APPROVED, "change_approved");
@@ -0,0 +1,28 @@
CREATE TABLE IF NOT EXISTS `blade_master_order` (
`id` bigint NOT NULL COMMENT '主键', `tenant_id` varchar(12) DEFAULT '000000' COMMENT '租户ID',
`create_user` bigint DEFAULT NULL, `create_dept` bigint DEFAULT NULL, `create_time` datetime DEFAULT NULL,
`update_user` bigint DEFAULT NULL, `update_time` datetime DEFAULT NULL, `status` int DEFAULT 1,
`is_deleted` int DEFAULT 0, `master_no` varchar(64) NOT NULL COMMENT '总单号',
`project_id` bigint DEFAULT NULL, `project_name` varchar(100) DEFAULT NULL, `contract_id` bigint DEFAULT NULL,
`contract_name` varchar(100) DEFAULT NULL, `customer_name` varchar(100) DEFAULT NULL,
`transport_organization_type` varchar(32) NOT NULL DEFAULT '多式联运', `business_status` varchar(32) NOT NULL DEFAULT 'draft',
`departure_name` varchar(100) DEFAULT NULL, `departure_address` varchar(500) DEFAULT NULL, `departure_contact` varchar(100) DEFAULT NULL, `departure_phone` varchar(32) DEFAULT NULL,
`arrival_name` varchar(100) DEFAULT NULL, `arrival_address` varchar(500) DEFAULT NULL, `arrival_contact` varchar(100) DEFAULT NULL, `arrival_phone` varchar(32) DEFAULT NULL,
`plan_start_time` datetime DEFAULT NULL, `plan_end_time` datetime DEFAULT NULL, `route_json` text, `goods_json` text, `attachments_json` text, `remark` varchar(200) DEFAULT NULL,
PRIMARY KEY (`id`), UNIQUE KEY `uk_master_order_no` (`master_no`), KEY `idx_master_order_status` (`business_status`), KEY `idx_master_order_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='多联总单';
ALTER TABLE `blade_transport_plan`
ADD COLUMN `master_no` varchar(100) DEFAULT NULL COMMENT '多联总单号' AFTER `plan_no`,
ADD COLUMN `relation_no` varchar(100) DEFAULT NULL COMMENT '总单分段号' AFTER `master_no`;
INSERT IGNORE INTO `blade_menu` (`id`,`parent_id`,`code`,`name`,`alias`,`path`,`source`,`sort`,`category`,`action`,`is_open`,`component`,`remark`,`is_deleted`) VALUES
(2090000000000000800,2090000000000000000,'master_order','总单管理','master_order','/business/master-order','iconfont icon-caidanguanli',80,1,0,1,NULL,'',0),
(2090000000000000801,2090000000000000800,'master_order_view','查看','master_order_view','', '',1,2,0,1,NULL,'',0),
(2090000000000000802,2090000000000000800,'master_order_add','新增','master_order_add','', '',2,2,0,1,NULL,'',0),
(2090000000000000803,2090000000000000800,'master_order_edit','编辑','master_order_edit','', '',3,2,0,1,NULL,'',0),
(2090000000000000804,2090000000000000800,'master_order_delete','删除','master_order_delete','', '',4,2,0,1,NULL,'',0),
(2090000000000000805,2090000000000000800,'master_order_export','批量导出','master_order_export','', '',5,2,0,1,NULL,'',0),
(2090000000000000806,2090000000000000800,'master_order_copy','复制','master_order_copy','', '',6,2,0,1,NULL,'',0),
(2090000000000000807,2090000000000000800,'master_order_dispatch','调度','master_order_dispatch','', '',7,2,0,1,NULL,'',0),
(2090000000000000808,2090000000000000800,'master_order_close_dispatch','关闭调度','master_order_close_dispatch','', '',8,2,0,1,NULL,'',0);
+2
View File
@@ -165,6 +165,8 @@ CREATE TABLE `blade_transport_plan` (
`status` int(11) DEFAULT '1' COMMENT '状态', `status` int(11) DEFAULT '1' COMMENT '状态',
`is_deleted` int(11) DEFAULT '0' COMMENT '是否已删除', `is_deleted` int(11) DEFAULT '0' COMMENT '是否已删除',
`plan_no` varchar(100) DEFAULT NULL COMMENT '计划单号', `plan_no` varchar(100) DEFAULT NULL COMMENT '计划单号',
`master_no` varchar(100) DEFAULT NULL COMMENT '多联总单号',
`relation_no` varchar(100) DEFAULT NULL COMMENT '总单分段号',
`plan_name` varchar(100) DEFAULT NULL COMMENT '计划名称', `plan_name` varchar(100) DEFAULT NULL COMMENT '计划名称',
`project_id` bigint(20) DEFAULT NULL COMMENT '项目ID', `project_id` bigint(20) DEFAULT NULL COMMENT '项目ID',
`project_name` varchar(100) DEFAULT NULL COMMENT '项目', `project_name` varchar(100) DEFAULT NULL COMMENT '项目',