✨ 打卡照片按类型最多传 9 张,照片列加宽为 text
- 节点打卡提交新增 photoItems(带显式类型),photos 原样保留供旧版小程序按下标回推 - 打卡照片是每个打卡节点必有的类型,不由过程配置决定,与各凭证类型各自最多 9 张 - 新版提交要求每个类型至少 1 张;旧版仍只要求至少 1 张,避免打断未升级的小程序 - 管理端订正只校验每类上限,不校验必填 - photos 由 varchar(2000) 改 text:36 张约 4000 字符,旧列宽约 17 张即溢出 - 基线建表脚本补 COLLATE=utf8mb4_0900_ai_ci(与本机库实际排序规则一致)
This commit is contained in:
+105
-21
@@ -38,6 +38,7 @@ import org.springblade.transport.mapper.WaybillEnroutePunchMapper;
|
||||
import org.springblade.transport.mapper.WaybillNodePunchMapper;
|
||||
import org.springblade.transport.pojo.dto.EnrouteSubmitDTO;
|
||||
import org.springblade.transport.pojo.dto.NodeSubmitDTO;
|
||||
import org.springblade.transport.pojo.dto.PunchPhotoDTO;
|
||||
import org.springblade.transport.pojo.entity.Driver;
|
||||
import org.springblade.transport.pojo.entity.ExceptionDisposal;
|
||||
import org.springblade.transport.pojo.entity.TransportVehicle;
|
||||
@@ -323,7 +324,13 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
}
|
||||
assertNodeNotBlocked(processJson, waybill.getId(), WaybillProcessSupport.nodeKey(nodeCfg));
|
||||
assertPrerequisitePunched(processJson, waybill.getId(), nodeCfg);
|
||||
assertPunchPhoto(dto.getPhotos());
|
||||
// photoItems 是新版小程序带的类型化照片;photos 是旧版仍在提交的裸 URL 列表,
|
||||
// 类型只能按下标回推过程配置的凭证类型。两者并存是为了不强制升级小程序。
|
||||
boolean typedPhotoItems = dto.getPhotoItems() != null && !dto.getPhotoItems().isEmpty();
|
||||
List<Map<String, String>> photoItems = typedPhotoItems
|
||||
? typedNodePunchPhotos(dto.getPhotoItems(), WaybillProcessSupport.nodePhotoTypes(nodeCfg))
|
||||
: legacyNodePunchPhotos(dto.getPhotos(), WaybillProcessSupport.nodeStringList(nodeCfg, "voucherTypes"));
|
||||
assertNodePunchPhotos(photoItems, nodeCfg, typedPhotoItems);
|
||||
|
||||
Date now = new Date();
|
||||
WaybillNodePunch punch = new WaybillNodePunch();
|
||||
@@ -342,24 +349,8 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
}
|
||||
punch.setAddress(Func.toStr(dto.getLocation().getAddress(), "").trim());
|
||||
}
|
||||
if (dto.getPhotos() != null && !dto.getPhotos().isEmpty()) {
|
||||
List<String> urls = dto.getPhotos().stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(String::trim)
|
||||
.filter(Func::isNotEmpty)
|
||||
.collect(Collectors.toList());
|
||||
if (!urls.isEmpty()) {
|
||||
List<String> types = WaybillProcessSupport.nodeStringList(nodeCfg, "voucherTypes");
|
||||
List<Map<String, String>> photoItems = new ArrayList<>();
|
||||
for (int i = 0; i < urls.size(); i++) {
|
||||
Map<String, String> item = new LinkedHashMap<>();
|
||||
String type = i < types.size() ? types.get(i) : ("凭证" + (i + 1));
|
||||
item.put("type", type);
|
||||
item.put("url", urls.get(i));
|
||||
photoItems.add(item);
|
||||
}
|
||||
punch.setPhotos(JsonUtil.toJson(photoItems));
|
||||
}
|
||||
if (!photoItems.isEmpty()) {
|
||||
punch.setPhotos(JsonUtil.toJson(photoItems));
|
||||
}
|
||||
punch.setWeight(trimOrNull(dto.getWeight()));
|
||||
punch.setVolume(trimOrNull(dto.getVolume()));
|
||||
@@ -916,9 +907,9 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 断言打卡照片已上传。
|
||||
* 断言打卡照片已上传(在途链路)。
|
||||
* <p>
|
||||
* 照片是打卡的必填项(每个节点至少 1 张),此处拦的是绕过客户端直接调接口的情况
|
||||
* 在途只有一张照片、不带类型,故只判有没有;此处拦的是绕过客户端直接调接口的情况
|
||||
* ——前端的必填提示不构成约束。
|
||||
*/
|
||||
private void assertPunchPhoto(List<String> photos) {
|
||||
@@ -931,6 +922,99 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 节点打卡的显式类型照片:类型由提交方给出,超出该节点取值域的直接拒绝。
|
||||
* <p>
|
||||
* 取值域是固定的「打卡照片」加过程配置勾选的凭证类型;类型是照片分组的唯一依据,
|
||||
* 放进来路不明的类型会让回显分组错乱。
|
||||
*/
|
||||
private List<Map<String, String>> typedNodePunchPhotos(List<PunchPhotoDTO> items, List<String> allowedTypes) {
|
||||
List<Map<String, String>> out = new ArrayList<>();
|
||||
for (PunchPhotoDTO item : items) {
|
||||
if (item == null) {
|
||||
continue;
|
||||
}
|
||||
String url = Func.toStr(item.getUrl(), "").trim();
|
||||
if (Func.isEmpty(url)) {
|
||||
continue;
|
||||
}
|
||||
String type = Func.toStr(item.getType(), "").trim();
|
||||
if (Func.isEmpty(type)) {
|
||||
throw new ServiceException("照片类型不能为空");
|
||||
}
|
||||
if (!allowedTypes.contains(type)) {
|
||||
throw new ServiceException("照片类型「" + type + "」不属于该节点");
|
||||
}
|
||||
out.add(nodePunchPhotoItem(type, url));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 节点打卡的旧版照片:裸 URL 列表,类型按下标取该节点的凭证类型,超出补「凭证N」。
|
||||
*/
|
||||
private List<Map<String, String>> legacyNodePunchPhotos(List<String> photos, List<String> voucherTypes) {
|
||||
List<Map<String, String>> out = new ArrayList<>();
|
||||
if (photos == null || photos.isEmpty()) {
|
||||
return out;
|
||||
}
|
||||
List<String> urls = photos.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(String::trim)
|
||||
.filter(Func::isNotEmpty)
|
||||
.collect(Collectors.toList());
|
||||
for (int i = 0; i < urls.size(); i++) {
|
||||
String type = i < voucherTypes.size() ? voucherTypes.get(i) : ("凭证" + (i + 1));
|
||||
out.add(nodePunchPhotoItem(type, urls.get(i)));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private Map<String, String> nodePunchPhotoItem(String type, String url) {
|
||||
Map<String, String> item = new LinkedHashMap<>();
|
||||
item.put("type", type);
|
||||
item.put("url", url);
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
* 断言节点打卡照片满足类型口径。
|
||||
* <p>
|
||||
* 每个类型至多 9 张,按类型各自计数,不是节点合计。
|
||||
* 新版提交(photoItems)还需每个类型至少 1 张,含固定的「打卡照片」;
|
||||
* 旧版只提交裸 URL 列表,类型按下标回推、表达不出「打卡照片」,
|
||||
* 对它套用「每个类型至少 1 张」会让所有未升级的小程序提交失败,故旧版仍只要求至少有 1 张。
|
||||
*/
|
||||
private void assertNodePunchPhotos(List<Map<String, String>> photoItems, Map<String, Object> nodeCfg,
|
||||
boolean typedPhotoItems) {
|
||||
Map<String, Integer> countByType = countPunchPhotoByType(photoItems);
|
||||
for (Map.Entry<String, Integer> entry : countByType.entrySet()) {
|
||||
if (entry.getValue() > WaybillProcessSupport.MAX_PHOTOS_PER_TYPE) {
|
||||
throw new ServiceException("「" + entry.getKey() + "」最多上传 "
|
||||
+ WaybillProcessSupport.MAX_PHOTOS_PER_TYPE + " 张照片");
|
||||
}
|
||||
}
|
||||
if (!typedPhotoItems) {
|
||||
if (countByType.isEmpty()) {
|
||||
throw new ServiceException("请上传打卡照片");
|
||||
}
|
||||
return;
|
||||
}
|
||||
for (String type : WaybillProcessSupport.nodePhotoTypes(nodeCfg)) {
|
||||
if (countByType.getOrDefault(type, 0) < 1) {
|
||||
throw new ServiceException("「" + type + "」至少上传 1 张照片");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, Integer> countPunchPhotoByType(List<Map<String, String>> photoItems) {
|
||||
Map<String, Integer> countByType = new LinkedHashMap<>();
|
||||
for (Map<String, String> item : photoItems) {
|
||||
countByType.merge(item.get("type"), 1, Integer::sum);
|
||||
}
|
||||
return countByType;
|
||||
}
|
||||
|
||||
/**
|
||||
* 断言该运单没有未了结的异常。
|
||||
* <p>
|
||||
|
||||
+28
-7
@@ -582,7 +582,9 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
|
||||
punch.setVolume(TransportBusinessSupport.trimToNull(request.getVolume()));
|
||||
punch.setQuantity(TransportBusinessSupport.trimToNull(request.getQuantity()));
|
||||
punch.setRemark(TransportBusinessSupport.trimToNull(request.getRemark()));
|
||||
punch.setPhotos(encodePunchPhotos(request.getPhotos()));
|
||||
List<Map<String, String>> photoItems = punchPhotoItems(request.getPhotos());
|
||||
assertPunchPhotoTypeLimit(photoItems);
|
||||
punch.setPhotos(photoItems.isEmpty() ? null : JsonUtil.toJson(photoItems));
|
||||
// 编辑不改 exception_flag:它与异常处置表的一致性由异常上报维护
|
||||
if (isNew) {
|
||||
waybillNodePunchMapper.insert(punch);
|
||||
@@ -724,13 +726,13 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
|
||||
}
|
||||
|
||||
/**
|
||||
* 打卡照片序列化:保留 [{type,url}] 结构,URL 为空的一律丢弃。
|
||||
* 打卡照片规范成 [{type,url}]:保留原结构,URL 为空的一律丢弃,类型为空按序号补「凭证N」。
|
||||
*/
|
||||
private String encodePunchPhotos(List<WaybillPunchRecordSaveRequest.Photo> photos) {
|
||||
if (photos == null || photos.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
private List<Map<String, String>> punchPhotoItems(List<WaybillPunchRecordSaveRequest.Photo> photos) {
|
||||
List<Map<String, String>> items = new ArrayList<>();
|
||||
if (photos == null || photos.isEmpty()) {
|
||||
return items;
|
||||
}
|
||||
for (WaybillPunchRecordSaveRequest.Photo photo : photos) {
|
||||
String url = photo == null ? "" : TransportBusinessSupport.trimToEmpty(photo.getUrl());
|
||||
if (Func.isEmpty(url)) {
|
||||
@@ -742,7 +744,26 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
|
||||
item.put("url", url);
|
||||
items.add(item);
|
||||
}
|
||||
return items.isEmpty() ? null : JsonUtil.toJson(items);
|
||||
return items;
|
||||
}
|
||||
|
||||
/**
|
||||
* 断言打卡照片每种类型都不超过单类上限。
|
||||
* <p>
|
||||
* 上限按类型各自计数,不是节点合计:一个节点可以同时有「打卡照片」与多类凭证,各自最多 9 张。
|
||||
* 订正不校验必填——订正的是已经发生的事实,不是重新走一遍流程;必填只约束司机提交。
|
||||
*/
|
||||
private void assertPunchPhotoTypeLimit(List<Map<String, String>> photoItems) {
|
||||
Map<String, Integer> countByType = new LinkedHashMap<>();
|
||||
for (Map<String, String> item : photoItems) {
|
||||
countByType.merge(item.get("type"), 1, Integer::sum);
|
||||
}
|
||||
for (Map.Entry<String, Integer> entry : countByType.entrySet()) {
|
||||
if (entry.getValue() > WaybillProcessSupport.MAX_PHOTOS_PER_TYPE) {
|
||||
throw new ServiceException("「" + entry.getKey() + "」最多上传 "
|
||||
+ WaybillProcessSupport.MAX_PHOTOS_PER_TYPE + " 张照片");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String firstPunchPhotoUrl(List<WaybillPunchRecordSaveRequest.Photo> photos) {
|
||||
|
||||
+21
@@ -60,6 +60,10 @@ public final class WaybillProcessSupport {
|
||||
private static final String DISPOSAL_REVOKED = "revoked";
|
||||
/** 异常处置终态状态码集合:不在此集合内即未了结(processing 也在未了结之列) */
|
||||
public static final List<String> SETTLED_DISPOSAL_STATUSES = List.of(DISPOSAL_COMPLETED, DISPOSAL_REVOKED);
|
||||
/** 每个打卡节点必有的照片类型,不由过程配置的凭证类型决定 */
|
||||
public static final String PUNCH_PHOTO_TYPE = "打卡照片";
|
||||
/** 单个照片类型的张数上限;打卡照片与每个凭证类型各自独立计数,不是节点合计 */
|
||||
public static final int MAX_PHOTOS_PER_TYPE = 9;
|
||||
private static final DateTimeFormatter HM = DateTimeFormatter.ofPattern("H:mm");
|
||||
private static final DateTimeFormatter HM_PADDED = DateTimeFormatter.ofPattern("HH:mm");
|
||||
|
||||
@@ -309,6 +313,23 @@ public final class WaybillProcessSupport {
|
||||
return isTruthy(node.get("uploadVoucher"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 该节点的照片类型取值域:固定的「打卡照片」加过程配置勾选的凭证类型。
|
||||
* <p>
|
||||
* 打卡照片是每个打卡节点必有的照片类型,不由过程配置决定,与各凭证类型并列且互不替代。
|
||||
* 类型是照片分组的唯一依据,提交上来的类型必须落在这个取值域内,否则回显分组会错乱。
|
||||
*/
|
||||
public static List<String> nodePhotoTypes(Map<String, Object> node) {
|
||||
List<String> types = new ArrayList<>();
|
||||
types.add(PUNCH_PHOTO_TYPE);
|
||||
for (String voucherType : nodeStringList(node, "voucherTypes")) {
|
||||
if (!types.contains(voucherType)) {
|
||||
types.add(voucherType);
|
||||
}
|
||||
}
|
||||
return types;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static List<String> nodeStringList(Map<String, Object> node, String field) {
|
||||
Object raw = node.get(field);
|
||||
|
||||
Reference in New Issue
Block a user