1、新增小程序相关接口
2、调整OA
This commit is contained in:
+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("批量完成成功");
|
||||
|
||||
+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));
|
||||
}
|
||||
|
||||
}
|
||||
+6
@@ -68,6 +68,12 @@ public interface IDriverWaybillService {
|
||||
*/
|
||||
DriverWaybillCardVO detail(Long id);
|
||||
|
||||
/**
|
||||
* 按运单ID组装详情打卡数据(punchNodes / enrouteRecords),不校验当前登录人是否为该司机。
|
||||
* 供调度端 manage/detail 复用。
|
||||
*/
|
||||
DriverWaybillCardVO detailPunchSnapshot(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);
|
||||
|
||||
}
|
||||
+6
@@ -60,6 +60,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);
|
||||
|
||||
/**
|
||||
|
||||
+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) {
|
||||
|
||||
+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();
|
||||
}
|
||||
|
||||
}
|
||||
+11
-1
@@ -819,10 +819,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("仅待执行/进行中运单允许重新派单");
|
||||
|
||||
Reference in New Issue
Block a user