✨ 运单详情打卡记录支持管理端编辑(含打卡状态)
新增 POST /waybill-manage/punch-record/save:按运单当前过程配置补录或修正
节点打卡与在途打卡,打卡状态也在可编辑之列。
- punched=false 即取消打卡:清除该节点全部打卡流水(时间轴上「未打卡」的判据
就是没有流水);在途节点没有节点 key 可定位单条,故清空该运单全部在途流水
- 节点以当前过程配置为准回写 key/name,前端带旧值也不会写歪
- 编辑按字段显式更新:updateById 会跳过 null 字段,清空地址/货量/照片保存后不生效
- 编辑不改 exception_flag(与异常处置表绑定);管理端补录 driver_id 留空
- 凭证图按 [{type,url}] 回写,避免展示层按下标回推类型时换序串标
- 补 8 个服务单测 + 3 个 MockMvc 端点绑定测试
This commit is contained in:
+8
@@ -60,6 +60,7 @@ import org.springblade.transport.pojo.vo.WaybillTrackVO;
|
||||
import org.springblade.transport.pojo.vo.WaybillPunchRecordsVO;
|
||||
import org.springblade.transport.pojo.vo.WaybillVO;
|
||||
import org.springblade.transport.pojo.dto.WaybillMileageRequest;
|
||||
import org.springblade.transport.pojo.dto.WaybillPunchRecordSaveRequest;
|
||||
import org.springblade.transport.service.IContractManageService;
|
||||
import org.springblade.transport.service.ICustomerArchiveService;
|
||||
import org.springblade.transport.service.IProjectApplyService;
|
||||
@@ -113,6 +114,13 @@ public class WaybillController extends BladeController {
|
||||
return R.data(waybillService.listPunchRecords(waybillId));
|
||||
}
|
||||
|
||||
@PostMapping("/punch-record/save")
|
||||
@ApiOperationSupport(order = 2)
|
||||
@Operation(summary = "编辑打卡记录", description = "按过程配置补录/修正节点或在途打卡;punched=false 表示取消打卡,清除该节点打卡流水")
|
||||
public R savePunchRecord(@RequestBody WaybillPunchRecordSaveRequest request) {
|
||||
return R.status(waybillService.savePunchRecord(request));
|
||||
}
|
||||
|
||||
@PostMapping("/locate")
|
||||
@ApiOperationSupport(order = 1)
|
||||
@Operation(summary = "车辆实时定位", description = "按运单绑定车牌调用 LBS_LOCATE")
|
||||
|
||||
+11
@@ -33,6 +33,7 @@ import org.springblade.transport.pojo.vo.WaybillTrackVO;
|
||||
import org.springblade.transport.pojo.vo.WaybillPunchRecordsVO;
|
||||
import org.springblade.transport.pojo.vo.WaybillVO;
|
||||
import org.springblade.transport.pojo.dto.WaybillMileageRequest;
|
||||
import org.springblade.transport.pojo.dto.WaybillPunchRecordSaveRequest;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -59,6 +60,16 @@ public interface IWaybillService extends BaseService<Waybill> {
|
||||
*/
|
||||
WaybillPunchRecordsVO listPunchRecords(Long waybillId);
|
||||
|
||||
/**
|
||||
* 管理端:编辑时间轴上的单条打卡记录(含打卡状态)
|
||||
* <p>
|
||||
* punched=false 时清除该节点(在途节点为该运单全部在途流水)的打卡流水。
|
||||
*
|
||||
* @param request 打卡记录编辑请求
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean savePunchRecord(WaybillPunchRecordSaveRequest request);
|
||||
|
||||
/**
|
||||
* 运单车辆实时定位(按运单绑定车牌调用 LBS)
|
||||
*
|
||||
|
||||
+274
@@ -28,9 +28,11 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.mp.base.BaseServiceImpl;
|
||||
import org.springblade.core.secure.utils.AuthUtil;
|
||||
import org.springblade.core.tenant.annotation.TenantIgnore;
|
||||
import org.springblade.core.tool.jackson.JsonUtil;
|
||||
import org.springblade.core.tool.utils.BeanUtil;
|
||||
import org.springblade.core.tool.utils.DateUtil;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.system.cache.UserCache;
|
||||
import org.springblade.system.pojo.entity.Dept;
|
||||
@@ -49,6 +51,7 @@ import org.springblade.transport.pojo.entity.Waybill;
|
||||
import org.springblade.transport.pojo.entity.WaybillEnroutePunch;
|
||||
import org.springblade.transport.pojo.entity.WaybillNodePunch;
|
||||
import org.springblade.transport.pojo.dto.WaybillMileageRequest;
|
||||
import org.springblade.transport.pojo.dto.WaybillPunchRecordSaveRequest;
|
||||
import org.springblade.transport.pojo.vo.BusinessRemoveResultVO;
|
||||
import org.springblade.transport.pojo.vo.LoadingManageVO;
|
||||
import org.springblade.transport.pojo.vo.WaybillLocateVO;
|
||||
@@ -97,6 +100,9 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
|
||||
|
||||
private static final String STATUS_DRAFT = "draft";
|
||||
|
||||
/** 打卡记录类型:在途打卡(其余一律按过程节点打卡处理) */
|
||||
private static final String ENROUTE_PUNCH_TYPE = "enroute";
|
||||
|
||||
@jakarta.annotation.Resource
|
||||
private ILoadingManageService loadingManageService;
|
||||
|
||||
@@ -483,6 +489,274 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
|
||||
return vo;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean savePunchRecord(WaybillPunchRecordSaveRequest request) {
|
||||
if (request == null || request.getWaybillId() == null) {
|
||||
throw new ServiceException("运单ID不能为空");
|
||||
}
|
||||
Waybill waybill = getById(request.getWaybillId());
|
||||
if (waybill == null || Objects.equals(waybill.getIsDeleted(), 1)) {
|
||||
throw new ServiceException("运单不存在");
|
||||
}
|
||||
// 只有显式传 false 才是取消打卡,未传按已打卡处理
|
||||
boolean punched = !Boolean.FALSE.equals(request.getPunched());
|
||||
if (ENROUTE_PUNCH_TYPE.equalsIgnoreCase(TransportBusinessSupport.trimToEmpty(request.getType()))) {
|
||||
return saveEnroutePunchRecord(waybill, request, punched);
|
||||
}
|
||||
return saveNodePunchRecord(waybill, request, punched);
|
||||
}
|
||||
|
||||
/**
|
||||
* 过程节点打卡:已打卡补录/修正,未打卡清除该节点流水。
|
||||
*/
|
||||
private boolean saveNodePunchRecord(Waybill waybill, WaybillPunchRecordSaveRequest request, boolean punched) {
|
||||
Long waybillId = waybill.getId();
|
||||
String processJson = resolveLiveProcessJson(waybill);
|
||||
Map<String, Object> nodeConfig = findEnabledNodeConfig(processJson, request.getNodeCode(), request.getNodeName());
|
||||
// 节点以过程配置为准:配置改过 key/name 后,前端带上来的可能是旧值
|
||||
String nodeCode = nodeConfig == null
|
||||
? TransportBusinessSupport.trimToEmpty(request.getNodeCode())
|
||||
: WaybillProcessSupport.nodeKey(nodeConfig);
|
||||
String nodeName = nodeConfig == null
|
||||
? TransportBusinessSupport.trimToEmpty(request.getNodeName())
|
||||
: WaybillProcessSupport.nodeName(nodeConfig);
|
||||
if (Func.isEmpty(nodeCode) && Func.isEmpty(nodeName)) {
|
||||
throw new ServiceException("过程节点不能为空");
|
||||
}
|
||||
List<WaybillNodePunch> rows = nodePunchRows(waybillId, nodeCode, nodeName);
|
||||
if (!punched) {
|
||||
if (!rows.isEmpty()) {
|
||||
waybillNodePunchMapper.deleteBatchIds(rows.stream().map(WaybillNodePunch::getId).toList());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
WaybillNodePunch punch = pickLatestNodePunch(rows, nodeCode, nodeName);
|
||||
boolean isNew = punch == null;
|
||||
if (isNew) {
|
||||
punch = new WaybillNodePunch();
|
||||
punch.setWaybillId(waybillId);
|
||||
punch.setWaybillNo(waybill.getWaybillNo());
|
||||
// 管理端补录没有打卡司机,driver_id 留空,不记到任何司机头上
|
||||
punch.setExceptionFlag(0);
|
||||
}
|
||||
if (Func.isNotEmpty(nodeCode)) {
|
||||
punch.setNodeCode(nodeCode);
|
||||
}
|
||||
if (Func.isNotEmpty(nodeName)) {
|
||||
punch.setNodeName(nodeName);
|
||||
}
|
||||
punch.setPunchTime(parsePunchTime(request.getPunchTime()));
|
||||
punch.setAddress(TransportBusinessSupport.trimToNull(request.getAddress()));
|
||||
punch.setLongitude(parseCoordinate(request.getLongitude(), "经度"));
|
||||
punch.setLatitude(parseCoordinate(request.getLatitude(), "纬度"));
|
||||
punch.setWeight(TransportBusinessSupport.trimToNull(request.getWeight()));
|
||||
punch.setVolume(TransportBusinessSupport.trimToNull(request.getVolume()));
|
||||
punch.setQuantity(TransportBusinessSupport.trimToNull(request.getQuantity()));
|
||||
punch.setRemark(TransportBusinessSupport.trimToNull(request.getRemark()));
|
||||
punch.setPhotos(encodePunchPhotos(request.getPhotos()));
|
||||
// 编辑不改 exception_flag:它与异常处置表的一致性由异常上报维护
|
||||
if (isNew) {
|
||||
waybillNodePunchMapper.insert(punch);
|
||||
} else {
|
||||
updateNodePunchFields(punch);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 显式按字段更新节点打卡。
|
||||
* <p>
|
||||
* 不能用 updateById:它按「非空才更新」生成 SQL,把地址/货量/照片清空时这些字段为 null,
|
||||
* 会被整条跳过——界面上删掉的内容保存后还在。
|
||||
*/
|
||||
private void updateNodePunchFields(WaybillNodePunch punch) {
|
||||
waybillNodePunchMapper.update(null, Wrappers.<WaybillNodePunch>lambdaUpdate()
|
||||
.eq(WaybillNodePunch::getId, punch.getId())
|
||||
.set(WaybillNodePunch::getNodeCode, punch.getNodeCode())
|
||||
.set(WaybillNodePunch::getNodeName, punch.getNodeName())
|
||||
.set(WaybillNodePunch::getPunchTime, punch.getPunchTime())
|
||||
.set(WaybillNodePunch::getAddress, punch.getAddress())
|
||||
.set(WaybillNodePunch::getLongitude, punch.getLongitude())
|
||||
.set(WaybillNodePunch::getLatitude, punch.getLatitude())
|
||||
.set(WaybillNodePunch::getWeight, punch.getWeight())
|
||||
.set(WaybillNodePunch::getVolume, punch.getVolume())
|
||||
.set(WaybillNodePunch::getQuantity, punch.getQuantity())
|
||||
.set(WaybillNodePunch::getRemark, punch.getRemark())
|
||||
.set(WaybillNodePunch::getPhotos, punch.getPhotos())
|
||||
// entity 传 null,MP 不会自动填充审计字段,这里补上
|
||||
.set(WaybillNodePunch::getUpdateTime, new Date())
|
||||
.set(WaybillNodePunch::getUpdateUser, AuthUtil.getUserId()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 在途打卡:时间轴上的在途节点只展示最新一条流水。
|
||||
*/
|
||||
private boolean saveEnroutePunchRecord(Waybill waybill, WaybillPunchRecordSaveRequest request, boolean punched) {
|
||||
Long waybillId = waybill.getId();
|
||||
List<WaybillEnroutePunch> rows = waybillEnroutePunchMapper.selectList(Wrappers.<WaybillEnroutePunch>lambdaQuery()
|
||||
.eq(WaybillEnroutePunch::getWaybillId, waybillId)
|
||||
.orderByAsc(WaybillEnroutePunch::getPunchTime)
|
||||
.orderByAsc(WaybillEnroutePunch::getId));
|
||||
if (!punched) {
|
||||
// 在途节点没有节点 key 可定位单条,取消打卡即清空该运单全部在途流水,
|
||||
// 否则更早的流水会顶上来继续显示已打卡
|
||||
if (!rows.isEmpty()) {
|
||||
waybillEnroutePunchMapper.deleteBatchIds(rows.stream().map(WaybillEnroutePunch::getId).toList());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
WaybillEnroutePunch punch = rows.isEmpty() ? null : rows.get(rows.size() - 1);
|
||||
boolean isNew = punch == null;
|
||||
if (isNew) {
|
||||
punch = new WaybillEnroutePunch();
|
||||
punch.setWaybillId(waybillId);
|
||||
punch.setWaybillNo(waybill.getWaybillNo());
|
||||
}
|
||||
punch.setPunchTime(parsePunchTime(request.getPunchTime()));
|
||||
punch.setAddress(TransportBusinessSupport.trimToNull(request.getAddress()));
|
||||
punch.setLongitude(parseCoordinate(request.getLongitude(), "经度"));
|
||||
punch.setLatitude(parseCoordinate(request.getLatitude(), "纬度"));
|
||||
// 在途打卡表只有一个照片字段(单张),多张只保留第一张
|
||||
punch.setPhoto(firstPunchPhotoUrl(request.getPhotos()));
|
||||
if (isNew) {
|
||||
waybillEnroutePunchMapper.insert(punch);
|
||||
} else {
|
||||
// 同节点打卡:显式 set,否则清空地址/照片保存后不生效
|
||||
waybillEnroutePunchMapper.update(null, Wrappers.<WaybillEnroutePunch>lambdaUpdate()
|
||||
.eq(WaybillEnroutePunch::getId, punch.getId())
|
||||
.set(WaybillEnroutePunch::getPunchTime, punch.getPunchTime())
|
||||
.set(WaybillEnroutePunch::getAddress, punch.getAddress())
|
||||
.set(WaybillEnroutePunch::getLongitude, punch.getLongitude())
|
||||
.set(WaybillEnroutePunch::getLatitude, punch.getLatitude())
|
||||
.set(WaybillEnroutePunch::getPhoto, punch.getPhoto())
|
||||
.set(WaybillEnroutePunch::getUpdateTime, new Date())
|
||||
.set(WaybillEnroutePunch::getUpdateUser, AuthUtil.getUserId()));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 该节点名下的全部打卡流水(含历史),按打卡时间升序。
|
||||
* <p>
|
||||
* 取消打卡要清空全部,只软删最新一条会让更早的历史流水顶上来继续显示已打卡。
|
||||
*/
|
||||
private List<WaybillNodePunch> nodePunchRows(Long waybillId, String nodeCode, String nodeName) {
|
||||
return waybillNodePunchMapper.selectList(Wrappers.<WaybillNodePunch>lambdaQuery()
|
||||
.eq(WaybillNodePunch::getWaybillId, waybillId)
|
||||
.orderByAsc(WaybillNodePunch::getPunchTime)
|
||||
.orderByAsc(WaybillNodePunch::getId))
|
||||
.stream()
|
||||
.filter(punch -> {
|
||||
String rowCode = TransportBusinessSupport.trimToEmpty(punch.getNodeCode());
|
||||
String rowName = TransportBusinessSupport.trimToEmpty(punch.getNodeName());
|
||||
if (Func.isNotEmpty(nodeCode) && nodeCode.equalsIgnoreCase(rowCode)) {
|
||||
return true;
|
||||
}
|
||||
return Func.isNotEmpty(nodeName) && nodeName.equals(rowName);
|
||||
})
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 取该节点最新一条流水,口径与展示层一致:nodeCode 命中优先,未命中才回退 nodeName。
|
||||
*/
|
||||
private WaybillNodePunch pickLatestNodePunch(List<WaybillNodePunch> rows, String nodeCode, String nodeName) {
|
||||
WaybillNodePunch byCode = null;
|
||||
WaybillNodePunch byName = null;
|
||||
for (WaybillNodePunch row : rows) {
|
||||
if (Func.isNotEmpty(nodeCode) && nodeCode.equalsIgnoreCase(TransportBusinessSupport.trimToEmpty(row.getNodeCode()))) {
|
||||
byCode = row;
|
||||
} else if (Func.isNotEmpty(nodeName) && nodeName.equals(TransportBusinessSupport.trimToEmpty(row.getNodeName()))) {
|
||||
byName = row;
|
||||
}
|
||||
}
|
||||
return byCode != null ? byCode : byName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 过程配置中启用(未启用时不参与打卡,补录无从校验)的节点配置;找不到返回 null。
|
||||
*/
|
||||
private Map<String, Object> findEnabledNodeConfig(String processJson, String nodeCode, String nodeName) {
|
||||
String code = TransportBusinessSupport.trimToEmpty(nodeCode);
|
||||
String name = TransportBusinessSupport.trimToEmpty(nodeName);
|
||||
List<Map<String, Object>> nodes = WaybillProcessSupport.listEnabledProcessNodes(processJson);
|
||||
if (nodes.isEmpty()) {
|
||||
nodes = WaybillProcessSupport.listDriverPunchNodes(processJson);
|
||||
}
|
||||
for (Map<String, Object> node : nodes) {
|
||||
if (Func.isNotEmpty(code) && code.equalsIgnoreCase(WaybillProcessSupport.nodeKey(node))) {
|
||||
return node;
|
||||
}
|
||||
if (Func.isNotEmpty(name) && name.equals(WaybillProcessSupport.nodeName(node))) {
|
||||
return node;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 打卡照片序列化:保留 [{type,url}] 结构,URL 为空的一律丢弃。
|
||||
*/
|
||||
private String encodePunchPhotos(List<WaybillPunchRecordSaveRequest.Photo> photos) {
|
||||
if (photos == null || photos.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
List<Map<String, String>> items = new ArrayList<>();
|
||||
for (WaybillPunchRecordSaveRequest.Photo photo : photos) {
|
||||
String url = photo == null ? "" : TransportBusinessSupport.trimToEmpty(photo.getUrl());
|
||||
if (Func.isEmpty(url)) {
|
||||
continue;
|
||||
}
|
||||
String type = photo == null ? "" : TransportBusinessSupport.trimToEmpty(photo.getType());
|
||||
Map<String, String> item = new LinkedHashMap<>();
|
||||
item.put("type", Func.isEmpty(type) ? ("凭证" + (items.size() + 1)) : type);
|
||||
item.put("url", url);
|
||||
items.add(item);
|
||||
}
|
||||
return items.isEmpty() ? null : JsonUtil.toJson(items);
|
||||
}
|
||||
|
||||
private String firstPunchPhotoUrl(List<WaybillPunchRecordSaveRequest.Photo> photos) {
|
||||
if (photos == null) {
|
||||
return null;
|
||||
}
|
||||
for (WaybillPunchRecordSaveRequest.Photo photo : photos) {
|
||||
String url = photo == null ? "" : TransportBusinessSupport.trimToEmpty(photo.getUrl());
|
||||
if (Func.isNotEmpty(url)) {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 打卡时间为空表示「此刻补录」;格式非法直接报错,不静默改成当前时间。
|
||||
*/
|
||||
private Date parsePunchTime(String text) {
|
||||
String value = TransportBusinessSupport.trimToEmpty(text);
|
||||
if (Func.isEmpty(value)) {
|
||||
return new Date();
|
||||
}
|
||||
try {
|
||||
return DateUtil.parse(value, DateUtil.PATTERN_DATETIME);
|
||||
} catch (Exception exception) {
|
||||
throw new ServiceException("打卡时间格式不正确,应为 yyyy-MM-dd HH:mm:ss");
|
||||
}
|
||||
}
|
||||
|
||||
private BigDecimal parseCoordinate(String text, String label) {
|
||||
String value = TransportBusinessSupport.trimToEmpty(text);
|
||||
if (Func.isEmpty(value)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return new BigDecimal(value);
|
||||
} catch (NumberFormatException exception) {
|
||||
throw new ServiceException(label + "格式不正确");
|
||||
}
|
||||
}
|
||||
|
||||
private WaybillNodePunch findLatestNodePunch(Map<String, WaybillNodePunch> index, String nodeCode, String nodeName) {
|
||||
if (index == null || index.isEmpty()) {
|
||||
return null;
|
||||
|
||||
Reference in New Issue
Block a user