diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/MasterOrderDispatchRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/MasterOrderDispatchRequest.java new file mode 100644 index 0000000..a6e31d1 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/MasterOrderDispatchRequest.java @@ -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> dispatches; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/MasterOrder.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/MasterOrder.java new file mode 100644 index 0000000..186aeb1 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/MasterOrder.java @@ -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; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/TransportPlan.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/TransportPlan.java index 934682b..4462cba 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/TransportPlan.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/TransportPlan.java @@ -48,6 +48,12 @@ public class TransportPlan extends TenantEntity { @Schema(description = "计划单号") private String planNo; + @Schema(description = "多联总单号") + private String masterNo; + + @Schema(description = "总单分段号") + private String relationNo; + @Schema(description = "计划名称") private String planName; diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/MasterOrderVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/MasterOrderVO.java new file mode 100644 index 0000000..76bda0c --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/MasterOrderVO.java @@ -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> routes; + @TableField(exist = false) + private List> goods; + @TableField(exist = false) + private List> routeProgress; + @TableField(exist = false) + private BigDecimal totalQuantity; + @TableField(exist = false) + private String createUserName; + @TableField(exist = false) + private String updateUserName; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ProjectApplyVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ProjectApplyVO.java index 96559ad..8431d07 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ProjectApplyVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ProjectApplyVO.java @@ -63,6 +63,10 @@ public class ProjectApplyVO extends ProjectApply { @Schema(description = "审批状态名称") private String approvalStatusName; + @TableField(exist = false) + @Schema(description = "审批状态集合,多个状态使用逗号分隔") + private String approvalStatuses; + @TableField(exist = false) @Schema(description = "生效类型名称") private String effectiveTypeName; diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/MasterOrderController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/MasterOrderController.java new file mode 100644 index 0000000..8b958e7 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/MasterOrderController.java @@ -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 detail(@RequestParam Long id) { return R.data(masterOrderService.detail(id)); } + @GetMapping("/list") @ApiOperationSupport(order = 2) @Operation(summary = "分页") public R> list(MasterOrderVO query, Query page) { return R.data(masterOrderService.selectPage(Condition.getPage(page), query)); } + @PostMapping("/submit") @ApiOperationSupport(order = 3) @Operation(summary = "确认创建或编辑") public R submit(@RequestBody MasterOrderVO data) { return R.data(masterOrderService.submit(data, false)); } + @PostMapping("/draft") @ApiOperationSupport(order = 4) @Operation(summary = "暂存草稿") public R draft(@RequestBody MasterOrderVO data) { return R.data(masterOrderService.submit(data, true)); } + @PostMapping("/copy") @ApiOperationSupport(order = 5) @Operation(summary = "复制") public R 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 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); } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/MasterOrderMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/MasterOrderMapper.java new file mode 100644 index 0000000..0b7862f --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/MasterOrderMapper.java @@ -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 { +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IMasterOrderService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IMasterOrderService.java new file mode 100644 index 0000000..db8963e --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IMasterOrderService.java @@ -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 { + IPage selectPage(IPage 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 exportWaybills(MasterOrderVO query); +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MasterOrderServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MasterOrderServiceImpl.java new file mode 100644 index 0000000..60b5f6e --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/MasterOrderServiceImpl.java @@ -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 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 selectPage(IPage page, MasterOrderVO query) { + IPage 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>> planGroups = new LinkedHashMap<>(); + Map>> waybillGroups = new LinkedHashMap<>(); + for (Map 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> dispatches : planGroups.values()) createTransportPlan(masterOrder, dispatches); + for (List> dispatches : waybillGroups.values()) createWaybill(masterOrder, dispatches); + refreshStatus(masterOrder); + return detail(masterOrder.getId()); + } + + @Override + public List exportWaybills(MasterOrderVO query) { + List result = new ArrayList<>(); + for (MasterOrder masterOrder : list(buildQuery(query))) { + for (Waybill waybill : waybillService.list(new LambdaQueryWrapper().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> dispatches) { + Map 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> dispatches) { + Map 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 available = availableGoods(masterOrder); + List> 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 buildQuery(MasterOrderVO query) { + LambdaQueryWrapper 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 query = new LambdaQueryWrapper().eq(ContractManage::getContractName, masterOrder.getContractName()); + if (Func.isNotEmpty(masterOrder.getProjectId())) query.eq(ContractManage::getProjectId, masterOrder.getProjectId()); + return contractManageService.getOne(query, false); + } + + private List> buildProgress(MasterOrder masterOrder, List> routes) { + List waybills = waybillService.list(new LambdaQueryWrapper().eq(Waybill::getMasterNo, masterOrder.getMasterNo())); + List plans = transportPlanService.list(new LambdaQueryWrapper().eq(TransportPlan::getMasterNo, masterOrder.getMasterNo())); + for (Map 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> 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> dispatches) { + Map available = availableGoods(masterOrder); + Map> usedBySegment = new LinkedHashMap<>(); + for (Map 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 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 availableGoods(MasterOrder masterOrder) { + Map available = new LinkedHashMap<>(); + for (Map goods : parseArray(masterOrder.getGoodsJson())) available.merge(goodsKey(goods), decimal(goods, "quantity"), BigDecimal::add); + return available; + } + private boolean isSegmentComplete(String masterNo, String segmentNo, Map available) { + Map dispatched = dispatchedGoods(masterNo, segmentNo); + return available.entrySet().stream().allMatch(item -> dispatched.getOrDefault(item.getKey(), BigDecimal.ZERO).compareTo(item.getValue()) >= 0); + } + private void validateCarrier(Map 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 dispatchedGoods(String masterNo, String segmentNo) { + Map result = new LinkedHashMap<>(); + LambdaQueryWrapper billQuery = new LambdaQueryWrapper().eq(Waybill::getMasterNo, masterNo); + if (Func.isNotEmpty(segmentNo)) billQuery.eq(Waybill::getRelationNo, segmentNo); + for (Waybill bill : waybillService.list(billQuery)) { + List> goods = parseArray(bill.getGoodsJson()); + if (goods.isEmpty()) goods = List.of(Map.of("cargoName", bill.getCargoName(), "cargoType", bill.getCargoType(), "quantity", bill.getQuantity())); + for (Map goodsItem : goods) result.merge(goodsKey(goodsItem), decimal(goodsItem, "quantity"), BigDecimal::add); + } + LambdaQueryWrapper planQuery = new LambdaQueryWrapper().eq(TransportPlan::getMasterNo, masterNo); + if (Func.isNotEmpty(segmentNo)) planQuery.eq(TransportPlan::getRelationNo, segmentNo); + for (TransportPlan plan : transportPlanService.list(planQuery)) for (Map goods : parseArray(plan.getGoodsJson())) result.merge(goodsKey(goods), decimal(goods, "quantity"), BigDecimal::add); + return result; + } + private BigDecimal totalQuantity(List> goods) { return goods.stream().map(item -> decimal(item, "quantity")).reduce(BigDecimal.ZERO, BigDecimal::add); } + private String waybillGroupKey(Map dispatch) { return String.join("\u0000", string(dispatch, "segmentNo", ""), string(dispatch, "carrierType", ""), string(dispatch, "carrierName", ""), string(dispatch, "driverName", ""), string(dispatch, "vehicleNo", "")); } + private String joinGoodsField(List> 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 values, String key) { try { return new BigDecimal(string(values, key, "0")); } catch (Exception exception) { return BigDecimal.ZERO; } } + private String goodsKey(Map 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 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 values, String key) { return string(values, key, null); } + private String string(Map 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().likeRight(MasterOrder::getMasterNo, prefix)); return prefix + String.format("%04d", count + 1); } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ProjectApplyServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ProjectApplyServiceImpl.java index 95907d4..dfa503a 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ProjectApplyServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ProjectApplyServiceImpl.java @@ -47,6 +47,7 @@ import java.math.RoundingMode; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; +import java.util.Arrays; import java.util.List; import java.util.Objects; @@ -277,6 +278,16 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl 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())) { queryWrapper.eq(ProjectApply::getEffectiveType, EFFECTIVE_FORMAL) .in(ProjectApply::getApprovalStatus, STATUS_APPROVED, "change_approved"); diff --git a/doc/sql/transport/blade_master_order_20260811.sql b/doc/sql/transport/blade_master_order_20260811.sql new file mode 100644 index 0000000..f575c6f --- /dev/null +++ b/doc/sql/transport/blade_master_order_20260811.sql @@ -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); diff --git a/doc/sql/transport/blade_tms_business.sql b/doc/sql/transport/blade_tms_business.sql index 8d771ac..9c3509f 100644 --- a/doc/sql/transport/blade_tms_business.sql +++ b/doc/sql/transport/blade_tms_business.sql @@ -165,6 +165,8 @@ CREATE TABLE `blade_transport_plan` ( `status` int(11) DEFAULT '1' COMMENT '状态', `is_deleted` int(11) DEFAULT '0' 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 '计划名称', `project_id` bigint(20) DEFAULT NULL COMMENT '项目ID', `project_name` varchar(100) DEFAULT NULL COMMENT '项目',