♻️ 运次分档改由服务层只读推导,不再由库层判据决定
库层只负责取回收口与筛选命中的可见运单行,分组、分档、筛选、排序与分页 全部在服务层算完再做,Tab 与卡片由同一份推导得出;接单需求改取项目生效中 的过程配置(与单票路径同口径,仅写回内存不落库),否则快照缺接单节点的 运次会被误判成进行中。筛选只决定哪些运次在范围内,分档永远看整趟车, 避免筛选缩小子单集合后把运输中的车算进已完成。SQL 分档渲染与重复的分组 键声明一并删除,管理端两类筛选合成一条取数路径。
This commit is contained in:
+6
-3
@@ -31,12 +31,15 @@ import org.springblade.transport.pojo.vo.TripVO;
|
||||
* 运次读服务
|
||||
* <p>
|
||||
* 「一卡一运次」的取数、状态聚合、接单需求合并与分页只在这里实现一次,司机端与管理端列表都调它,
|
||||
* 各自只做字段裁剪。聚合全程只读,不在读路径上写业务状态。
|
||||
* 各自只做字段裁剪。聚合全程只读,不在读路径上写业务状态——分档只按过程配置与接单态推导,
|
||||
* 不按库里的业务状态,也不借读路径做惰性校正。
|
||||
*/
|
||||
public interface ITripQueryService {
|
||||
|
||||
/**
|
||||
* 运次分页:一页就是若干个运次(有配载单的一趟车算一个),筛选、排序与分页都在库层按运次完成。
|
||||
* 运次分页:一页就是若干个运次(有配载单的一趟车算一个)。
|
||||
* <p>
|
||||
* 库层只取回收口与行级筛选命中的运单行,分组、分档、筛选、排序与分页都在服务层算完再做。
|
||||
*
|
||||
* @param query 查询条件,收口由 driverPhone 或 allDrivers 给出
|
||||
* @return 运次分页
|
||||
@@ -44,7 +47,7 @@ public interface ITripQueryService {
|
||||
IPage<TripVO> page(TripQueryVO query);
|
||||
|
||||
/**
|
||||
* 运次 Tab 统计:全部 / 待接单 / 进行中 / 已完成
|
||||
* 运次 Tab 统计:全部 / 待接单 / 进行中 / 已完成,分档口径与 page 完全一致
|
||||
*
|
||||
* @param query 查询条件,口径同 page
|
||||
* @return 运次计数
|
||||
|
||||
+32
-182
@@ -120,24 +120,15 @@ public class ManageWaybillServiceImpl implements IManageWaybillService {
|
||||
private static final int DEFAULT_PAGE_SIZE = 10;
|
||||
private static final int MAX_PAGE_SIZE = 50;
|
||||
|
||||
/**
|
||||
* 库层分组键:有配载按配载单号成组,未配载每张运单自成一组(W 前缀避免与配载单号撞键)。
|
||||
* 与运次读服务同一个表达式,两处的「运次」才是同一个东西。
|
||||
*/
|
||||
private static final String SQL_TRIP_KEY = "IFNULL(NULLIF(loading_no, ''), CONCAT('W', id))";
|
||||
/** 待重新派单的排序键:运次内子单被拒的时刻取最大值 */
|
||||
private static final String SQL_MAX_REJECT_TIME = "MAX(driver_reject_time)";
|
||||
/** 待重新派单的排序键:运次内子单创建时刻取最大值(与运次列表同一排序口径) */
|
||||
private static final String SQL_MAX_CREATE_TIME = "MAX(create_time)";
|
||||
/** 关键字在子单上匹配的字段:与运次读服务同一份口径 */
|
||||
private static final String[] KEYWORD_COLUMNS = {
|
||||
"k.waybill_no", "k.loading_no", "k.departure_name", "k.arrival_name",
|
||||
"k.departure_address", "k.arrival_address", "k.vehicle_no"
|
||||
/** 关键字在子单上匹配的字段:与运次读服务同一份口径,待重新派单列表按它筛 */
|
||||
private static final String[] REASSIGN_REASSIGN_KEYWORD_COLUMNS = {
|
||||
"waybill_no", "loading_no", "departure_name", "arrival_name",
|
||||
"departure_address", "arrival_address", "vehicle_no"
|
||||
};
|
||||
/** 已拒子单命中的运次键:待重新派单的判据(已拒单 + 未取消未完成),与首页待办同源 */
|
||||
private static final String SQL_REJECTED_TRIP_KEYS = "SELECT IFNULL(NULLIF(r.loading_no, ''), CONCAT('W', r.id))"
|
||||
+ " FROM blade_waybill r WHERE r.is_deleted = 0 AND r.driver_accept_status = '" + ACCEPT_REJECTED + "'"
|
||||
+ " AND r.business_status IN ('" + STATUS_PENDING + "', '" + STATUS_RUNNING + "')";
|
||||
|
||||
private static final DateTimeFormatter DATE_MD = DateTimeFormatter.ofPattern("MM-dd");
|
||||
private static final DateTimeFormatter DATE_YMD = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||
@@ -188,20 +179,20 @@ public class ManageWaybillServiceImpl implements IManageWaybillService {
|
||||
return emptyCardPage(pageNo, pageSize);
|
||||
}
|
||||
|
||||
// 组织类型 / 异常 / 创建时间都是子单行级条件,与状态、关键字、分页一起交给运次读服务,
|
||||
// 一次取数取完整趟车:分组、分档、筛选、排序与分页都在它那边算完
|
||||
TripQueryVO query = TripQueryVO.forAdmin();
|
||||
query.setStatus(toTripStatus(status));
|
||||
query.setKeyword(keyword);
|
||||
|
||||
// 运次读服务只认状态、关键字与分页;组织类型 / 异常 / 创建时间这些筛选它表达不了,
|
||||
// 走库层分组取这一页的运次主键与总数,再逐条交给它取整趟车(状态与货物汇总仍出自同一份实现)
|
||||
boolean filtered = Func.isNotBlank(transportType) || Func.isNotBlank(exception)
|
||||
|| Func.isNotBlank(startDate) || Func.isNotBlank(endDate);
|
||||
if (!filtered) {
|
||||
IPage<TripVO> trips = tripQueryService.page(tripPageQuery(query, pageNo, pageSize));
|
||||
return tripCardPage(trips.getRecords(), pageNo, pageSize, trips.getTotal(), exceptionWaybillIds);
|
||||
}
|
||||
return groupedTripPage(filteredTripKeyQuery(query, transportType, exception, exceptionWaybillIds,
|
||||
parseDayStart(startDate), parseDayEnd(endDate)), pageNo, pageSize, exceptionWaybillIds);
|
||||
query.setTransportOrgType(transportType);
|
||||
query.setException(exception);
|
||||
query.setExceptionWaybillIds(exceptionWaybillIds);
|
||||
query.setCreatedFrom(parseDayStart(startDate));
|
||||
query.setCreatedTo(parseDayEnd(endDate));
|
||||
query.setCurrent(pageNo);
|
||||
query.setSize(pageSize);
|
||||
IPage<TripVO> trips = tripQueryService.page(query);
|
||||
return tripCardPage(trips.getRecords(), pageNo, pageSize, trips.getTotal(), exceptionWaybillIds);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -239,140 +230,13 @@ public class ManageWaybillServiceImpl implements IManageWaybillService {
|
||||
Set<Long> exceptionWaybillIds = loadIncompleteExceptionWaybillIds();
|
||||
TripQueryVO query = TripQueryVO.forAdmin();
|
||||
query.setKeyword(keyword);
|
||||
return groupedTripPage(pendingTripKeyQuery(query, needReassign), pageNo, pageSize, exceptionWaybillIds);
|
||||
}
|
||||
|
||||
/* ===========================================================================
|
||||
* 运次取数:列表 / 待处理都走共享运次读服务,本类只做字段裁剪与调度端独有的筛选
|
||||
* =========================================================================*/
|
||||
|
||||
private TripQueryVO tripPageQuery(TripQueryVO base, int pageNo, int pageSize) {
|
||||
TripQueryVO query = new TripQueryVO();
|
||||
query.setAllDrivers(base.isAllDrivers());
|
||||
query.setDriverPhone(base.getDriverPhone());
|
||||
query.setStatus(base.getStatus());
|
||||
query.setKeyword(base.getKeyword());
|
||||
// 待处理只要待接单与进行中两档;「未拒单」也是交给运次读服务按子单行筛
|
||||
query.setStatuses(List.of(0, 1));
|
||||
query.setExcludeRejectedTrips(Boolean.FALSE.equals(needReassign));
|
||||
query.setCurrent(pageNo);
|
||||
query.setSize(pageSize);
|
||||
return query;
|
||||
}
|
||||
|
||||
/**
|
||||
* 库层按运次分组取这一页的运次主键与总数,再逐条向运次读服务取整趟车。
|
||||
* <p>
|
||||
* 为什么不是直接把筛选交给运次读服务:它只认状态、关键字与分页,没有筛选钩子;
|
||||
* 而按运次在服务端过滤必须先扫完命中的运次才能给出列表口径的总数,几千个运次的租户下要十几秒。
|
||||
* 这里只把「它表达不了的筛选」落到库层,分组键与 Tab 判据都复用它的那份声明,
|
||||
* 卡片上的状态、接单需求、站点与货物汇总仍由运次读服务下发。
|
||||
*/
|
||||
private Page<AdminWaybillCardVO> groupedTripPage(QueryWrapper<Waybill> keyQuery, int pageNo, int pageSize,
|
||||
Set<Long> exceptionWaybillIds) {
|
||||
Page<Waybill> rows = new Page<>(pageNo, pageSize);
|
||||
waybillService.page(rows, keyQuery);
|
||||
// 分组行只带运次键:配载运次要换成配载单主键,运次读服务才认这个主键
|
||||
Set<String> loadingNos = rows.getRecords().stream().map(Waybill::getLoadingNo).filter(Func::isNotEmpty)
|
||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
Map<String, LoadingManage> loadings = loadingsByNo(loadingNos);
|
||||
List<TripVO> trips = new ArrayList<>(rows.getRecords().size());
|
||||
for (Waybill row : rows.getRecords()) {
|
||||
Long tripId = Func.isEmpty(row.getLoadingNo())
|
||||
? row.getId()
|
||||
: loadingId(loadings.get(row.getLoadingNo()), row.getId());
|
||||
TripVO trip = tripId == null ? null : visibleTrip(String.valueOf(tripId));
|
||||
if (trip != null) {
|
||||
// 运次在两次查询之间被拒单/释放:这一页少一张卡,不拿没有子单的运次凑数
|
||||
trips.add(trip);
|
||||
}
|
||||
}
|
||||
return tripCardPage(trips, pageNo, pageSize, rows.getTotal(), exceptionWaybillIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* 筛选用的库层运次查询:分组键、可见口径与 Tab 判据都与运次读服务同源
|
||||
*/
|
||||
private QueryWrapper<Waybill> filteredTripKeyQuery(TripQueryVO base, String transportType, String exception,
|
||||
Set<Long> exceptionWaybillIds, Date createdFrom, Date createdTo) {
|
||||
QueryWrapper<Waybill> wrapper = visibleTripRows();
|
||||
applyTransportOrgTypeFilter(wrapper, transportType);
|
||||
applyExceptionFilter(wrapper, exception, exceptionWaybillIds);
|
||||
applyCreatedRangeFilter(wrapper, createdFrom, createdTo);
|
||||
applyTripKeywordFilter(wrapper, base.getKeyword());
|
||||
wrapper.groupBy(SQL_TRIP_KEY);
|
||||
wrapper.having(TripAggregation.tabPredicate(base.getStatus()));
|
||||
wrapper.orderByDesc(SQL_MAX_CREATE_TIME);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* 待处理列表的库层运次查询:待接单 + 进行中两档,可选「未拒单」。
|
||||
* 已拒子单的运次键子查询与首页待办同源,整单拒绝后这趟车不再算待处理。
|
||||
*/
|
||||
private QueryWrapper<Waybill> pendingTripKeyQuery(TripQueryVO base, Boolean needReassign) {
|
||||
QueryWrapper<Waybill> wrapper = visibleTripRows();
|
||||
applyTripKeywordFilter(wrapper, base.getKeyword());
|
||||
if (Boolean.FALSE.equals(needReassign)) {
|
||||
wrapper.apply(SQL_TRIP_KEY + " NOT IN (" + SQL_REJECTED_TRIP_KEYS + ")");
|
||||
}
|
||||
wrapper.groupBy(SQL_TRIP_KEY);
|
||||
wrapper.having("(" + TripAggregation.tabPredicate(0) + " OR " + TripAggregation.tabPredicate(1) + ")");
|
||||
wrapper.orderByDesc(SQL_MAX_CREATE_TIME);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
/** 可见运次行:与运次读服务的可见口径同一条(已拒子单不进运次,草稿不成运次) */
|
||||
private QueryWrapper<Waybill> visibleTripRows() {
|
||||
QueryWrapper<Waybill> wrapper = Wrappers.query();
|
||||
wrapper.select("MIN(id) AS id", "MIN(NULLIF(loading_no, '')) AS loading_no", SQL_MAX_CREATE_TIME + " AS create_time");
|
||||
wrapper.apply("(driver_accept_status IS NULL OR driver_accept_status <> {0})", ACCEPT_REJECTED);
|
||||
wrapper.in("business_status", TripAggregation.CHILD_STATUSES);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
private void applyTransportOrgTypeFilter(QueryWrapper<Waybill> wrapper, String transportType) {
|
||||
if (TRANSPORT_LOAD.equals(transportType)) {
|
||||
wrapper.apply("IFNULL(loading_no, '') <> ''");
|
||||
} else if (TRANSPORT_COMMON.equals(transportType)) {
|
||||
wrapper.apply("IFNULL(loading_no, '') = ''");
|
||||
}
|
||||
}
|
||||
|
||||
/** 异常筛选按运单命中:整趟车任一子单有未完成异常即命中(与旧运单级筛选同判据) */
|
||||
private void applyExceptionFilter(QueryWrapper<Waybill> wrapper, String exception, Set<Long> exceptionWaybillIds) {
|
||||
if (exceptionWaybillIds.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (EXCEPTION_YES.equals(exception)) {
|
||||
wrapper.in("id", exceptionWaybillIds);
|
||||
} else if (EXCEPTION_NO.equals(exception)) {
|
||||
wrapper.notIn("id", exceptionWaybillIds);
|
||||
}
|
||||
}
|
||||
|
||||
private void applyCreatedRangeFilter(QueryWrapper<Waybill> wrapper, Date createdFrom, Date createdTo) {
|
||||
if (createdFrom != null) {
|
||||
wrapper.ge("create_time", createdFrom);
|
||||
}
|
||||
if (createdTo != null) {
|
||||
wrapper.le("create_time", createdTo);
|
||||
}
|
||||
}
|
||||
|
||||
/** 关键字与运次读服务同一口径:任一子单或配载单号命中即整趟车命中 */
|
||||
private void applyTripKeywordFilter(QueryWrapper<Waybill> wrapper, String keyword) {
|
||||
if (Func.isBlank(keyword)) {
|
||||
return;
|
||||
}
|
||||
Object[] params = new Object[KEYWORD_COLUMNS.length];
|
||||
StringBuilder matched = new StringBuilder();
|
||||
for (int i = 0; i < KEYWORD_COLUMNS.length; i++) {
|
||||
if (i > 0) {
|
||||
matched.append(" OR ");
|
||||
}
|
||||
matched.append(KEYWORD_COLUMNS[i]).append(" LIKE {").append(i).append('}');
|
||||
params[i] = "%" + keyword.trim() + "%";
|
||||
}
|
||||
wrapper.apply(SQL_TRIP_KEY + " IN (SELECT IFNULL(NULLIF(k.loading_no, ''), CONCAT('W', k.id))"
|
||||
+ " FROM blade_waybill k WHERE k.is_deleted = 0 AND (" + matched + "))", params);
|
||||
IPage<TripVO> trips = tripQueryService.page(query);
|
||||
return tripCardPage(trips.getRecords(), pageNo, pageSize, trips.getTotal(), exceptionWaybillIds);
|
||||
}
|
||||
|
||||
private Page<AdminWaybillCardVO> emptyCardPage(int pageNo, int pageSize) {
|
||||
@@ -472,21 +336,12 @@ public class ManageWaybillServiceImpl implements IManageWaybillService {
|
||||
if (Func.isEmpty(tripIds)) {
|
||||
return Set.of();
|
||||
}
|
||||
// 占位符逐个列出:分组键是 SQL 表达式,用不了 in(column, values),而 {0} 传集合会被当成单个参数序列化
|
||||
List<String> keys = new ArrayList<>(tripIds);
|
||||
Object[] params = new Object[keys.size()];
|
||||
StringBuilder placeholders = new StringBuilder();
|
||||
for (int i = 0; i < keys.size(); i++) {
|
||||
if (i > 0) {
|
||||
placeholders.append(", ");
|
||||
}
|
||||
placeholders.append("{").append(i).append("}");
|
||||
params[i] = keys.get(i);
|
||||
}
|
||||
List<Waybill> rejected = waybillService.list(pendingReassignQuery()
|
||||
.select(Waybill::getId, Waybill::getLoadingNo)
|
||||
.apply(SQL_TRIP_KEY + " IN (" + placeholders + ")", params));
|
||||
return rejected.stream().map(this::tripKeyOf).collect(Collectors.toSet());
|
||||
// 被拒子单只有几十行量级:一次取回后在服务层换算运次键,比在库层按分组键表达式反查更省事
|
||||
return waybillService.list(pendingReassignQuery().select(Waybill::getId, Waybill::getLoadingNo))
|
||||
.stream()
|
||||
.map(this::tripKeyOf)
|
||||
.filter(tripIds::contains)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
private boolean hasRejectedChild(String tripId) {
|
||||
@@ -540,20 +395,14 @@ public class ManageWaybillServiceImpl implements IManageWaybillService {
|
||||
}
|
||||
}
|
||||
|
||||
/** 库层分组用的运次键,与 SQL_TRIP_KEY 同一个表达式 */
|
||||
/** 运次的键:配载取配载单号,未配载取 W + 运单主键,与运次读服务同一个口径 */
|
||||
private String tripKeyOf(Waybill waybill) {
|
||||
if (waybill == null) {
|
||||
return null;
|
||||
}
|
||||
return Func.isEmpty(waybill.getLoadingNo()) ? "W" + waybill.getId() : waybill.getLoadingNo();
|
||||
return waybill == null ? null : TripAggregation.keyOf(waybill.getLoadingNo(), waybill.getId());
|
||||
}
|
||||
|
||||
/** 运次的键:配载取配载单号,未配载取 W + 运单主键,与 SQL_TRIP_KEY 同一个表达式 */
|
||||
/** 运次的键:同上,运次卡片取运次主键换算 */
|
||||
private String tripKeyOf(TripVO trip) {
|
||||
if (trip == null) {
|
||||
return null;
|
||||
}
|
||||
return Func.isEmpty(trip.getLoadingNo()) ? "W" + trip.getId() : trip.getLoadingNo();
|
||||
return trip == null ? null : TripAggregation.keyOf(trip.getLoadingNo(), parseId(trip.getId()));
|
||||
}
|
||||
|
||||
/* ===========================================================================
|
||||
@@ -830,6 +679,7 @@ public class ManageWaybillServiceImpl implements IManageWaybillService {
|
||||
* 展示用字段一律包聚合函数取组内代表值,既满足 ONLY_FULL_GROUP_BY,也让一条待办自带它要显示的信息。
|
||||
*/
|
||||
private QueryWrapper<Waybill> reassignGroupQuery() {
|
||||
// 分组键只在 TripAggregation 声明一次:库层分组与服务层分组的「运次」必须是同一个东西
|
||||
QueryWrapper<Waybill> wrapper = Wrappers.query();
|
||||
wrapper.select("MIN(id) AS id", "MIN(NULLIF(loading_no, '')) AS loading_no", "MIN(waybill_no) AS waybill_no",
|
||||
"MIN(transport_type) AS transport_type", "MIN(business_status) AS business_status",
|
||||
@@ -841,7 +691,7 @@ public class ManageWaybillServiceImpl implements IManageWaybillService {
|
||||
SQL_MAX_REJECT_TIME + " AS driver_reject_time", SQL_MAX_CREATE_TIME + " AS create_time");
|
||||
wrapper.eq("driver_accept_status", ACCEPT_REJECTED);
|
||||
wrapper.in("business_status", STATUS_PENDING, STATUS_RUNNING);
|
||||
wrapper.groupBy(SQL_TRIP_KEY);
|
||||
wrapper.groupBy(TripAggregation.SQL_TRIP_KEY);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
|
||||
+15
-40
@@ -27,16 +27,14 @@ import org.springblade.transport.pojo.entity.Waybill;
|
||||
import org.springblade.transport.support.WaybillProcessSupport;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 运次聚合口径:状态四档 + 接单需求 OR
|
||||
* 运次聚合口径:四档状态 + 接单需求 OR + 运次键
|
||||
* <p>
|
||||
* 四档判据有两种渲染:库层聚合条件用于 Tab 过滤与分页(配载段按配载单号分组,先筛后排序再分页),
|
||||
* Java 判定用于摆上一页的运次给状态。两种渲染必须同档。
|
||||
* 四档与接单需求都按子运单的过程配置与接单态在这里推导,Tab 过滤与卡片状态用同一份推导,
|
||||
* 库层不再按已落库的业务状态分档——业务状态靠读取时的惰性校正维持,与过程配置不一致是常态。
|
||||
* <p>
|
||||
* 接单需求要解析过程配置,库层表达不出来,只能在本页复核:库里落的是业务状态,
|
||||
* 而「需接单未接」与「无需接单」两种情况在库里可能都是待接单档。
|
||||
* 运次键与它的库层表达式只在这里声明一次:库层分组(重新派单待办)与服务层分组必须是同一个键。
|
||||
*/
|
||||
final class TripAggregation {
|
||||
|
||||
@@ -60,40 +58,21 @@ final class TripAggregation {
|
||||
PENDING, WAITING_DISPATCH, DISPATCHING, RUNNING, COMPLETED, CANCELLED
|
||||
);
|
||||
|
||||
/** 库层聚合计数 */
|
||||
private static final String SQL_TOTAL = "COUNT(*)";
|
||||
private static final String SQL_TERMINAL = "SUM(business_status IN (" + quoted(TERMINAL_STATUSES) + "))";
|
||||
private static final String SQL_COMPLETED = "SUM(business_status = '" + COMPLETED + "')";
|
||||
private static final String SQL_PENDING = "SUM(business_status IN (" + quoted(PENDING_STATUSES) + "))";
|
||||
private static final String SQL_ALL_TERMINAL = SQL_TERMINAL + " = " + SQL_TOTAL;
|
||||
private static final String SQL_NOT_ALL_TERMINAL = SQL_TERMINAL + " < " + SQL_TOTAL;
|
||||
private static final String SQL_ANY_COMPLETED = SQL_COMPLETED + " > 0";
|
||||
private static final String SQL_NO_COMPLETED = SQL_COMPLETED + " = 0";
|
||||
private static final String SQL_ANY_PENDING = SQL_PENDING + " > 0";
|
||||
private static final String SQL_NO_PENDING = SQL_PENDING + " = 0";
|
||||
/** 待重新派单的口径:子单被拒且这趟车还没结束 */
|
||||
static final List<String> REASSIGN_STATUSES = List.of(PENDING, RUNNING);
|
||||
|
||||
/** 四档的库层判据:0 待接单 / 1 运输中 / 2 已完成 / 3 已取消 */
|
||||
private static final String SQL_PENDING_TIER = SQL_ANY_PENDING + " AND " + SQL_NOT_ALL_TERMINAL;
|
||||
private static final String SQL_RUNNING_TIER = SQL_NO_PENDING + " AND " + SQL_NOT_ALL_TERMINAL;
|
||||
private static final String SQL_COMPLETED_TIER = SQL_ANY_COMPLETED + " AND " + SQL_ALL_TERMINAL;
|
||||
private static final String SQL_CANCELLED_TIER = SQL_NO_COMPLETED + " AND " + SQL_ALL_TERMINAL;
|
||||
/**
|
||||
* 运次键的库层表达式:有配载按配载单号成组,未配载每张运单自成一组(W 前缀避免与配载单号撞键)。
|
||||
* 服务层换 {@link #keyOf(String, Long)} 算同一个键。
|
||||
*/
|
||||
static final String SQL_TRIP_KEY = "IFNULL(NULLIF(loading_no, ''), CONCAT('W', id))";
|
||||
|
||||
private TripAggregation() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Tab 过滤条件(库层 HAVING)
|
||||
* <p>
|
||||
* 0 待接单 / 1 进行中 / 2 已完成 / 3 已取消;其余(含不传)按「全部」= 0/1/2,不含已取消。
|
||||
*/
|
||||
static String tabPredicate(Integer tab) {
|
||||
return switch (tab == null ? -1 : tab) {
|
||||
case 0 -> SQL_PENDING_TIER;
|
||||
case 1 -> SQL_RUNNING_TIER;
|
||||
case 2 -> SQL_COMPLETED_TIER;
|
||||
case 3 -> SQL_CANCELLED_TIER;
|
||||
default -> "(" + SQL_PENDING_TIER + " OR " + SQL_RUNNING_TIER + " OR " + SQL_COMPLETED_TIER + ")";
|
||||
};
|
||||
/** 运次键:与 {@link #SQL_TRIP_KEY} 同一个口径 */
|
||||
static String keyOf(String loadingNo, Long id) {
|
||||
return Func.isEmpty(loadingNo) ? "W" + id : loadingNo;
|
||||
}
|
||||
|
||||
/** 运次的四档状态:0 待接单 / 1 运输中 / 2 已完成 / 3 已取消 */
|
||||
@@ -125,7 +104,7 @@ final class TripAggregation {
|
||||
}
|
||||
|
||||
/**
|
||||
* 库层聚合结果:子单总数、终态数、已完成数、待接单档数
|
||||
* 子单聚合结果:子单总数、终态数、已完成数、待接单档数
|
||||
*/
|
||||
static Counters count(List<Waybill> children) {
|
||||
int total = 0;
|
||||
@@ -187,10 +166,6 @@ final class TripAggregation {
|
||||
return anyPending ? WaybillProcessSupport.ACCEPT_PENDING : null;
|
||||
}
|
||||
|
||||
private static String quoted(List<String> statuses) {
|
||||
return statuses.stream().map(status -> "'" + status + "'").collect(Collectors.joining(", "));
|
||||
}
|
||||
|
||||
/** 子单聚合计数 */
|
||||
record Counters(int total, int terminal, int completed, int pending) {
|
||||
}
|
||||
|
||||
+301
-79
@@ -22,7 +22,7 @@
|
||||
*/
|
||||
package org.springblade.transport.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
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;
|
||||
@@ -30,12 +30,14 @@ import lombok.RequiredArgsConstructor;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.tool.utils.Func;
|
||||
import org.springblade.transport.pojo.entity.LoadingManage;
|
||||
import org.springblade.transport.pojo.entity.ProcessConfig;
|
||||
import org.springblade.transport.pojo.entity.Waybill;
|
||||
import org.springblade.transport.pojo.vo.TripQueryVO;
|
||||
import org.springblade.transport.pojo.vo.TripStationVO;
|
||||
import org.springblade.transport.pojo.vo.TripTabCountsVO;
|
||||
import org.springblade.transport.pojo.vo.TripVO;
|
||||
import org.springblade.transport.service.ILoadingManageService;
|
||||
import org.springblade.transport.service.IProcessConfigService;
|
||||
import org.springblade.transport.service.ITripQueryService;
|
||||
import org.springblade.transport.service.IWaybillService;
|
||||
import org.springblade.transport.support.WaybillProcessSupport;
|
||||
@@ -43,6 +45,7 @@ import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
@@ -57,8 +60,12 @@ import java.util.stream.Collectors;
|
||||
/**
|
||||
* 运次读服务实现
|
||||
* <p>
|
||||
* 取数两段,段内都是批量查询:段一在库层按运次分组聚合,一次查询完成筛选、排序与分页;
|
||||
* 段二按这一页的运次主键与配载单号批量取配载单与子运单。
|
||||
* 取数两段,段内都是批量查询:段一一次取回收口与行级筛选命中的可见运单行,分组、分档、排序与分页
|
||||
* 都在服务层算完再做;段二按这一页运次的配载单号批量取配载单。子运单不再回查——段一拿回来的行就是它们。
|
||||
* <p>
|
||||
* 分档只按过程配置与接单态推导,与卡片状态同一份口径:库里的业务状态靠读取时的惰性校正维持,
|
||||
* 拿它分档会让「归在待接单的运次、卡片显示运输中」。分页因此必须在过滤后的完整运次序列上做,
|
||||
* 先分页再过滤会把该在第一页的运次挤到后面几页。
|
||||
* <p>
|
||||
* 可见运次=派给该司机的未配载运单(每张自成运次),加上配载单派给该司机、且至少有一张可见子运单的运次。
|
||||
* 司机侧可见范围与运单列表同口径:司机手机号命中且未被该司机拒单;子单里已拒单的不进运次,
|
||||
@@ -75,19 +82,22 @@ public class TripQueryServiceImpl implements ITripQueryService {
|
||||
private static final String TRANSPORT_LOAD = "load";
|
||||
private static final String TRANSPORT_COMMON = "common";
|
||||
|
||||
/** 库层分组键:有配载按配载单号成组,未配载每张运单自成一组(W 前缀避免与配载单号撞键) */
|
||||
private static final String SQL_TRIP_KEY = "IFNULL(NULLIF(loading_no, ''), CONCAT('W', id))";
|
||||
/** 库层排序键:子单 create_time 的最大值 */
|
||||
private static final String SQL_MAX_CREATE_TIME = "MAX(create_time)";
|
||||
/** 未配载运次的键前缀:与 TripAggregation.SQL_TRIP_KEY 的 W 前缀同一个口径 */
|
||||
private static final String LONE_KEY_PREFIX = "W";
|
||||
|
||||
/** 异常筛选的取值:命中未完成异常的子单 */
|
||||
private static final String EXCEPTION_YES = "exception";
|
||||
private static final String EXCEPTION_NO = "normal";
|
||||
|
||||
/** 关键字在子单上匹配的字段 */
|
||||
private static final String[] KEYWORD_COLUMNS = {
|
||||
"k.waybill_no", "k.loading_no", "k.departure_name", "k.arrival_name",
|
||||
"k.departure_address", "k.arrival_address", "k.vehicle_no"
|
||||
"waybill_no", "loading_no", "departure_name", "arrival_name",
|
||||
"departure_address", "arrival_address", "vehicle_no"
|
||||
};
|
||||
|
||||
private final IWaybillService waybillService;
|
||||
private final ILoadingManageService loadingManageService;
|
||||
private final IProcessConfigService processConfigService;
|
||||
|
||||
@Override
|
||||
public IPage<TripVO> page(TripQueryVO query) {
|
||||
@@ -98,10 +108,11 @@ public class TripQueryServiceImpl implements ITripQueryService {
|
||||
// 收口缺失时返回空页,不能退回「不按司机收口」:那等于把别人的运次发出去
|
||||
return new Page<>(pageNo, pageSize);
|
||||
}
|
||||
Page<Waybill> rows = new Page<>(pageNo, pageSize);
|
||||
waybillService.page(rows, tripQuery(condition));
|
||||
Page<TripVO> page = new Page<>(pageNo, pageSize, rows.getTotal());
|
||||
page.setRecords(assemble(rows.getRecords()));
|
||||
List<TripRows> trips = trips(condition);
|
||||
Page<TripVO> page = new Page<>(pageNo, pageSize, trips.size());
|
||||
int from = Math.min((pageNo - 1) * pageSize, trips.size());
|
||||
int to = Math.min(from + pageSize, trips.size());
|
||||
page.setRecords(assemble(trips.subList(from, to)));
|
||||
return page;
|
||||
}
|
||||
|
||||
@@ -112,9 +123,23 @@ public class TripQueryServiceImpl implements ITripQueryService {
|
||||
if (!scoped(condition)) {
|
||||
return counts;
|
||||
}
|
||||
long pending = countTrips(condition, 0);
|
||||
long doing = countTrips(condition, 1);
|
||||
long done = countTrips(condition, 2);
|
||||
// 一次取数算清四档:分档口径与列表、卡片同一份推导,不再按库里的业务状态分批计数
|
||||
long pending = 0;
|
||||
long doing = 0;
|
||||
long done = 0;
|
||||
for (TripRows trip : trips(withoutTab(condition))) {
|
||||
Integer tier = trip.tier();
|
||||
if (tier == null) {
|
||||
continue;
|
||||
}
|
||||
switch (tier) {
|
||||
case 0 -> pending++;
|
||||
case 1 -> doing++;
|
||||
case 2 -> done++;
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
counts.setPending(pending);
|
||||
counts.setDoing(doing);
|
||||
counts.setDone(done);
|
||||
@@ -147,33 +172,206 @@ public class TripQueryServiceImpl implements ITripQueryService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 单个 Tab 的运次数:只走库层的分组计数,不取子单、不做接单态复核
|
||||
* 全部可见运次:一次取回运单行后在服务层分组、分档、筛选并按排序键倒序
|
||||
* <p>
|
||||
* 分档用过程配置与接单态推导,于是 Tab 与卡片必然同档;行级筛选(组织类型 / 异常 / 创建时间)落在库层,
|
||||
* 命中的行同时决定这一趟车的子单集合与分档。
|
||||
*/
|
||||
private long countTrips(TripQueryVO condition, int status) {
|
||||
Page<Waybill> rows = new Page<>(1, 1);
|
||||
waybillService.page(rows, tripQuery(withStatus(condition, status)));
|
||||
return rows.getTotal();
|
||||
private List<TripRows> trips(TripQueryVO condition) {
|
||||
Map<String, List<Waybill>> grouped = new LinkedHashMap<>();
|
||||
for (Waybill row : visibleRows(condition)) {
|
||||
grouped.computeIfAbsent(TripAggregation.keyOf(row.getLoadingNo(), row.getId()),
|
||||
ignored -> new ArrayList<>()).add(row);
|
||||
}
|
||||
Set<String> excluded = condition.isExcludeRejectedTrips() ? rejectedTripKeys() : Set.of();
|
||||
Set<Integer> tiers = tiers(condition);
|
||||
List<TripRows> trips = new ArrayList<>(grouped.size());
|
||||
grouped.forEach((key, rows) -> {
|
||||
if (excluded.contains(key)) {
|
||||
return;
|
||||
}
|
||||
List<Waybill> children = sortedChildren(rows);
|
||||
if (!inScope(children, condition)) {
|
||||
return;
|
||||
}
|
||||
boolean requireAccept = TripAggregation.requiresAccept(children);
|
||||
boolean accepted = WaybillProcessSupport.ACCEPT_ACCEPTED.equals(TripAggregation.acceptStatus(children));
|
||||
String status = TripAggregation.statusOf(children, requireAccept, accepted);
|
||||
Integer tier = TripAggregation.appStatus(status);
|
||||
if (tier == null || !tiers.contains(tier)) {
|
||||
return;
|
||||
}
|
||||
trips.add(new TripRows(key, rows.get(0).getLoadingNo(), children, status, tier, maxCreateTime(children)));
|
||||
});
|
||||
// 排序键是子单 create_time 的最大值,倒序;键相同再按运次键定序,免得同分页里顺序漂移
|
||||
trips.sort(Comparator.comparing(TripRows::sortTime, Comparator.nullsLast(Comparator.reverseOrder()))
|
||||
.thenComparing(TripRows::key));
|
||||
return trips;
|
||||
}
|
||||
|
||||
/** 子单按创建时刻升序:站点拼装与货物汇总都按这个顺序 */
|
||||
private List<Waybill> sortedChildren(List<Waybill> rows) {
|
||||
List<Waybill> children = new ArrayList<>(rows);
|
||||
children.sort(Comparator.comparing(Waybill::getCreateTime, Comparator.nullsLast(Comparator.naturalOrder())));
|
||||
return children;
|
||||
}
|
||||
|
||||
/**
|
||||
* 库层运次查询:按运次分组,一次完成筛选、排序与分页
|
||||
* 段一:一次取回该收口与行级筛选命中的可见运单行
|
||||
* <p>
|
||||
* 分组键与排序键都在 SQL 里,Tab 过滤是分组后的聚合条件(HAVING)——先分页再在服务端过滤会让
|
||||
* 本该在第一页的运次漏到后面几页。
|
||||
* 只 select 拼装与分档用得上的列:运单表上有几张大 JSON 列,整行拖回不值得。
|
||||
* 可见口径与运单列表同一条:状态进运次的才取,被该司机拒单的不取。
|
||||
*/
|
||||
private QueryWrapper<Waybill> tripQuery(TripQueryVO condition) {
|
||||
QueryWrapper<Waybill> wrapper = Wrappers.query();
|
||||
wrapper.select("MIN(id) AS id", "MIN(NULLIF(loading_no, '')) AS loading_no",
|
||||
SQL_MAX_CREATE_TIME + " AS create_time");
|
||||
wrapper.apply("(driver_accept_status IS NULL OR driver_accept_status <> {0})",
|
||||
WaybillProcessSupport.ACCEPT_REJECTED);
|
||||
wrapper.in("business_status", TripAggregation.CHILD_STATUSES);
|
||||
private List<Waybill> visibleRows(TripQueryVO condition) {
|
||||
LambdaQueryWrapper<Waybill> wrapper = Wrappers.<Waybill>lambdaQuery()
|
||||
.select(Waybill::getId, Waybill::getLoadingNo, Waybill::getWaybillNo, Waybill::getBusinessStatus,
|
||||
Waybill::getDriverAcceptStatus, Waybill::getProcessJson, Waybill::getProjectId, Waybill::getProjectName,
|
||||
Waybill::getCargoName, Waybill::getCargoType, Waybill::getQuantity, Waybill::getQuantityUnit,
|
||||
Waybill::getDepartureName, Waybill::getDepartureAddress, Waybill::getArrivalName,
|
||||
Waybill::getArrivalAddress, Waybill::getDriverName, Waybill::getDriverPhone, Waybill::getVehicleNo,
|
||||
Waybill::getCustomerName, Waybill::getTransportType, Waybill::getTaskRemark, Waybill::getCreateTime)
|
||||
.in(Waybill::getBusinessStatus, TripAggregation.CHILD_STATUSES)
|
||||
.and(visible -> visible.isNull(Waybill::getDriverAcceptStatus)
|
||||
.or().ne(Waybill::getDriverAcceptStatus, WaybillProcessSupport.ACCEPT_REJECTED));
|
||||
applyScope(wrapper, condition);
|
||||
applyOrgTypeFilter(wrapper, condition);
|
||||
applyKeyword(wrapper, condition.getKeyword());
|
||||
wrapper.groupBy(SQL_TRIP_KEY);
|
||||
wrapper.having(TripAggregation.tabPredicate(condition.getStatus()));
|
||||
wrapper.orderByDesc(SQL_MAX_CREATE_TIME);
|
||||
return wrapper;
|
||||
List<Waybill> rows = waybillService.list(wrapper);
|
||||
applyLiveProcessConfig(rows);
|
||||
return rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* 运输组织类型按子单行筛:它在同一趟车里必然是同一个值(配载的每张子单带同一个配载单号,
|
||||
* 未配载的每张自成一趟车),所以这一层不会把一趟车拆开。
|
||||
*/
|
||||
private void applyOrgTypeFilter(LambdaQueryWrapper<Waybill> wrapper, TripQueryVO condition) {
|
||||
if (TRANSPORT_LOAD.equals(condition.getTransportOrgType())) {
|
||||
wrapper.apply("IFNULL(loading_no, '') <> ''");
|
||||
} else if (TRANSPORT_COMMON.equals(condition.getTransportOrgType())) {
|
||||
wrapper.apply("IFNULL(loading_no, '') = ''");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 这趟车在不在筛选范围内:异常按「任一子单命中」,创建时间按子单 create_time 的最大值(运次列表的排序键,
|
||||
* 也是调度端卡片上显示的创建时间)。两个判据都按整趟车的子单集合算——先按筛选把子单集合缩小再分档,
|
||||
* 会让同一趟车归在某一档、卡片却显示另一档。
|
||||
* <p>
|
||||
* 筛选只决定这趟车在不在范围内,分档仍看它完整的可见子单集合。
|
||||
*/
|
||||
private boolean inScope(List<Waybill> children, TripQueryVO condition) {
|
||||
Set<Long> exceptionIds = condition.getExceptionWaybillIds();
|
||||
if (Func.isNotEmpty(exceptionIds)) {
|
||||
boolean hit = children.stream().map(Waybill::getId).anyMatch(exceptionIds::contains);
|
||||
if (EXCEPTION_YES.equals(condition.getException()) && !hit) {
|
||||
return false;
|
||||
}
|
||||
if (EXCEPTION_NO.equals(condition.getException()) && hit) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Date created = maxCreateTime(children);
|
||||
if (condition.getCreatedFrom() != null
|
||||
&& (created == null || created.before(condition.getCreatedFrom()))) {
|
||||
return false;
|
||||
}
|
||||
return condition.getCreatedTo() == null
|
||||
|| (created != null && !created.after(condition.getCreatedTo()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 按单票路径的口径把生效中的过程配置写回内存里的行:项目启用中的配置优先,它没有可打卡节点时回退运单快照。
|
||||
* <p>
|
||||
* 只改内存里的对象,分档与卡片下发都读它,不落库;生效配置一次取回后按项目复用,不逐条回查。
|
||||
*/
|
||||
private void applyLiveProcessConfig(List<Waybill> rows) {
|
||||
if (Func.isEmpty(rows)) {
|
||||
return;
|
||||
}
|
||||
Set<Long> projectIds = rows.stream().map(Waybill::getProjectId).filter(Objects::nonNull)
|
||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
if (projectIds.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Map<Long, String> live = liveProcessConfigs(projectIds);
|
||||
if (live.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
for (Waybill row : rows) {
|
||||
String liveJson = live.get(row.getProjectId());
|
||||
if (Func.isNotEmpty(liveJson)) {
|
||||
row.setProcessJson(resolveProcessJson(liveJson, row.getProcessJson()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 生效配置与快照二选一:生效配置没有可打卡节点时回退快照,两边都取不到节点就按生效配置来 */
|
||||
private String resolveProcessJson(String liveJson, String snapshot) {
|
||||
if (WaybillProcessSupport.listDriverPunchNodes(liveJson).isEmpty()
|
||||
&& Func.isNotEmpty(snapshot)
|
||||
&& !WaybillProcessSupport.listDriverPunchNodes(snapshot).isEmpty()) {
|
||||
return snapshot;
|
||||
}
|
||||
return liveJson;
|
||||
}
|
||||
|
||||
/** 生效中的过程配置:项目 → 节点配置 JSON,取最近更新的一条 */
|
||||
private Map<Long, String> liveProcessConfigs(Set<Long> projectIds) {
|
||||
List<ProcessConfig> configs = processConfigService.list(Wrappers.<ProcessConfig>lambdaQuery()
|
||||
.eq(ProcessConfig::getStatus, 1)
|
||||
.eq(ProcessConfig::getIsDeleted, 0)
|
||||
.orderByDesc(ProcessConfig::getUpdateTime)
|
||||
.orderByDesc(ProcessConfig::getCreateTime));
|
||||
Map<Long, String> live = new LinkedHashMap<>();
|
||||
for (Long projectId : projectIds) {
|
||||
if (projectId == null) {
|
||||
continue;
|
||||
}
|
||||
String projectIdStr = String.valueOf(projectId);
|
||||
for (ProcessConfig config : configs) {
|
||||
if (Func.isEmpty(config.getNodeConfigJson())
|
||||
|| !containsProjectId(config.getProjectIds(), projectIdStr)) {
|
||||
continue;
|
||||
}
|
||||
live.put(projectId, config.getNodeConfigJson());
|
||||
break;
|
||||
}
|
||||
}
|
||||
return live;
|
||||
}
|
||||
|
||||
private boolean containsProjectId(String projectIds, String projectId) {
|
||||
if (Func.isEmpty(projectIds) || Func.isEmpty(projectId)) {
|
||||
return false;
|
||||
}
|
||||
return Arrays.stream(projectIds.split(",")).map(String::trim).anyMatch(projectId::equals);
|
||||
}
|
||||
|
||||
/**
|
||||
* 有子单被拒、且这趟车还没结束的运次键:整单拒绝后这趟车不再算待处理
|
||||
*/
|
||||
private Set<String> rejectedTripKeys() {
|
||||
return waybillService.list(Wrappers.<Waybill>lambdaQuery()
|
||||
.select(Waybill::getId, Waybill::getLoadingNo)
|
||||
.eq(Waybill::getDriverAcceptStatus, WaybillProcessSupport.ACCEPT_REJECTED)
|
||||
.in(Waybill::getBusinessStatus, TripAggregation.REASSIGN_STATUSES))
|
||||
.stream()
|
||||
.map(row -> TripAggregation.keyOf(row.getLoadingNo(), row.getId()))
|
||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
}
|
||||
|
||||
/**
|
||||
* 要哪几档:多档优先,其次单档,都不传按「全部」=待接单/进行中/已完成(不含已取消)
|
||||
*/
|
||||
private Set<Integer> tiers(TripQueryVO condition) {
|
||||
if (Func.isNotEmpty(condition.getStatuses())) {
|
||||
return new LinkedHashSet<>(condition.getStatuses());
|
||||
}
|
||||
if (condition.getStatus() != null) {
|
||||
return Set.of(condition.getStatus());
|
||||
}
|
||||
return Set.of(0, 1, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -181,7 +379,7 @@ public class TripQueryServiceImpl implements ITripQueryService {
|
||||
* <p>
|
||||
* 手机号允许带空格,与运单列表的收口判据一致。
|
||||
*/
|
||||
private void applyScope(QueryWrapper<Waybill> wrapper, TripQueryVO condition) {
|
||||
private void applyScope(LambdaQueryWrapper<Waybill> wrapper, TripQueryVO condition) {
|
||||
if (condition.isAllDrivers()) {
|
||||
return;
|
||||
}
|
||||
@@ -191,20 +389,48 @@ public class TripQueryServiceImpl implements ITripQueryService {
|
||||
+ " WHERE lm.is_deleted = 0 AND lm.driver_phone = {0}))", phone)
|
||||
.or(lone -> lone
|
||||
.apply("IFNULL(loading_no, '') = ''")
|
||||
.and(row -> row.eq("driver_phone", phone)
|
||||
.and(row -> row.eq(Waybill::getDriverPhone, phone)
|
||||
.or().apply("REPLACE(IFNULL(driver_phone, ''), ' ', '') = {0}", phone))));
|
||||
}
|
||||
|
||||
/**
|
||||
* 关键字按运次匹配:任一子单或配载单号命中即整趟车命中。
|
||||
* <p>
|
||||
* 关键字不缩小这一组的子单集合,否则状态与排序会按命中的那一部分子单算。
|
||||
* 先按关键字查出命中的运次键,再要求这一行的所属运次在其中——关键字不缩小分组内的子单集合,
|
||||
* 否则状态与排序会按命中的那一部分子单算。
|
||||
*/
|
||||
private void applyKeyword(QueryWrapper<Waybill> wrapper, String keyword) {
|
||||
private void applyKeyword(LambdaQueryWrapper<Waybill> wrapper, String keyword) {
|
||||
if (Func.isEmpty(keyword)) {
|
||||
return;
|
||||
}
|
||||
String like = "%" + keyword.trim() + "%";
|
||||
Set<String> keys = keywordTripKeys(keyword.trim());
|
||||
if (keys.isEmpty()) {
|
||||
wrapper.apply("1 = 0");
|
||||
return;
|
||||
}
|
||||
Set<String> loadingNos = keys.stream().filter(key -> !key.startsWith(LONE_KEY_PREFIX))
|
||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
Set<Long> loneIds = keys.stream().filter(key -> key.startsWith(LONE_KEY_PREFIX))
|
||||
.map(key -> key.substring(LONE_KEY_PREFIX.length()))
|
||||
.map(this::tripId)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
wrapper.and(matched -> {
|
||||
if (!loadingNos.isEmpty()) {
|
||||
matched.in(Waybill::getLoadingNo, loadingNos);
|
||||
}
|
||||
if (!loneIds.isEmpty()) {
|
||||
if (!loadingNos.isEmpty()) {
|
||||
matched.or();
|
||||
}
|
||||
matched.apply("IFNULL(loading_no, '') = ''").in(Waybill::getId, loneIds);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** 关键字命中的运次键:在子单上按关键字找一遍,再换算成运次键 */
|
||||
private Set<String> keywordTripKeys(String keyword) {
|
||||
String like = "%" + keyword + "%";
|
||||
Object[] params = new Object[KEYWORD_COLUMNS.length];
|
||||
StringBuilder matched = new StringBuilder();
|
||||
for (int i = 0; i < KEYWORD_COLUMNS.length; i++) {
|
||||
@@ -214,58 +440,43 @@ public class TripQueryServiceImpl implements ITripQueryService {
|
||||
matched.append(KEYWORD_COLUMNS[i]).append(" LIKE {").append(i).append('}');
|
||||
params[i] = like;
|
||||
}
|
||||
wrapper.apply(SQL_TRIP_KEY + " IN (SELECT IFNULL(NULLIF(k.loading_no, ''), CONCAT('W', k.id))"
|
||||
+ " FROM blade_waybill k WHERE k.is_deleted = 0 AND (" + matched + "))", params);
|
||||
return waybillService.list(Wrappers.<Waybill>lambdaQuery()
|
||||
.select(Waybill::getId, Waybill::getLoadingNo)
|
||||
.apply("(" + matched + ")", params))
|
||||
.stream()
|
||||
.map(row -> TripAggregation.keyOf(row.getLoadingNo(), row.getId()))
|
||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
}
|
||||
|
||||
/**
|
||||
* 把这一页的运次行组装成运次:先批量取配载单与子运单,再逐行拼装
|
||||
* 把这一页的运次组装成运次:先批量取配载单,再逐趟拼装
|
||||
* <p>
|
||||
* 页里的行只带分组键与排序键,子运单要按主键/配载单号回查一次:未配载的运次就是那张运单本身,
|
||||
* 状态、接单需求与货物都得从那行真身来,不能拿分组行凑。
|
||||
* 子运单就是段一取回来的那些行,不再回查:回查会拿回未经行级筛选的子单,
|
||||
* 卡片的状态与货物汇总就会与分档、与筛选结果对不上。
|
||||
*/
|
||||
private List<TripVO> assemble(List<Waybill> rows) {
|
||||
if (Func.isEmpty(rows)) {
|
||||
private List<TripVO> assemble(List<TripRows> trips) {
|
||||
if (Func.isEmpty(trips)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
Set<String> loadingNos = rows.stream()
|
||||
.map(Waybill::getLoadingNo)
|
||||
Set<String> loadingNos = trips.stream()
|
||||
.map(TripRows::loadingNo)
|
||||
.filter(Func::isNotEmpty)
|
||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
Set<Long> loneIds = rows.stream()
|
||||
.filter(row -> Func.isEmpty(row.getLoadingNo()))
|
||||
.map(Waybill::getId)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
Map<String, LoadingManage> loadings = loadLoadings(loadingNos);
|
||||
Map<String, List<Waybill>> children = loadChildren(loadingNos, loneIds);
|
||||
Map<Long, Waybill> loneWaybills = new LinkedHashMap<>();
|
||||
children.values().forEach(group -> group.forEach(child -> loneWaybills.put(child.getId(), child)));
|
||||
List<TripVO> trips = new ArrayList<>(rows.size());
|
||||
for (Waybill row : rows) {
|
||||
String loadingNo = Func.toStr(row.getLoadingNo(), "");
|
||||
if (Func.isEmpty(loadingNo)) {
|
||||
Waybill waybill = loneWaybills.get(row.getId());
|
||||
if (waybill == null) {
|
||||
// 运单在两次查询之间被删或被拒单:这一页不给出没有子单的运次
|
||||
continue;
|
||||
}
|
||||
trips.add(waybillTrip(waybill, false));
|
||||
List<TripVO> result = new ArrayList<>(trips.size());
|
||||
for (TripRows trip : trips) {
|
||||
if (Func.isEmpty(trip.loadingNo())) {
|
||||
result.add(waybillTrip(trip.children().get(0), false));
|
||||
continue;
|
||||
}
|
||||
LoadingManage loading = loadings.get(loadingNo);
|
||||
LoadingManage loading = loadings.get(trip.loadingNo());
|
||||
if (loading == null) {
|
||||
// 配载单被并发删除:不给出没有主键的运次
|
||||
continue;
|
||||
}
|
||||
List<Waybill> group = children.getOrDefault(loadingNo, List.of());
|
||||
if (group.isEmpty()) {
|
||||
// 子单全部被拒单:整趟车对司机不可见,与详情口径一致
|
||||
continue;
|
||||
}
|
||||
trips.add(loadingTrip(loading, group, false));
|
||||
result.add(loadingTrip(loading, trip.children(), false));
|
||||
}
|
||||
return trips;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -370,7 +581,7 @@ public class TripQueryServiceImpl implements ITripQueryService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 聚合出运次的状态、接单需求、接单态与排序键
|
||||
* 聚合出运次的状态、接单需求、接单态与排序键:与分档用同一份推导,Tab 与卡片因此必然同档
|
||||
*/
|
||||
private void fillAggregation(TripVO trip, List<Waybill> children) {
|
||||
String acceptStatus = TripAggregation.acceptStatus(children);
|
||||
@@ -469,7 +680,7 @@ public class TripQueryServiceImpl implements ITripQueryService {
|
||||
return child;
|
||||
}
|
||||
|
||||
/** 排序键:子单 create_time 的最大值,与库层排序用的是同一个聚合 */
|
||||
/** 排序键:子单 create_time 的最大值 */
|
||||
private Date maxCreateTime(List<Waybill> children) {
|
||||
return children.stream()
|
||||
.map(Waybill::getCreateTime)
|
||||
@@ -532,12 +743,18 @@ public class TripQueryServiceImpl implements ITripQueryService {
|
||||
return query == null ? new TripQueryVO() : query;
|
||||
}
|
||||
|
||||
private TripQueryVO withStatus(TripQueryVO source, Integer status) {
|
||||
/** 去掉 Tab 条件、其余照搬:统计四档时不按某一档收窄 */
|
||||
private TripQueryVO withoutTab(TripQueryVO source) {
|
||||
TripQueryVO copy = new TripQueryVO();
|
||||
copy.setDriverPhone(source.getDriverPhone());
|
||||
copy.setAllDrivers(source.isAllDrivers());
|
||||
copy.setKeyword(source.getKeyword());
|
||||
copy.setStatus(status);
|
||||
copy.setTransportOrgType(source.getTransportOrgType());
|
||||
copy.setException(source.getException());
|
||||
copy.setExceptionWaybillIds(source.getExceptionWaybillIds());
|
||||
copy.setCreatedFrom(source.getCreatedFrom());
|
||||
copy.setCreatedTo(source.getCreatedTo());
|
||||
copy.setExcludeRejectedTrips(source.isExcludeRejectedTrips());
|
||||
return copy;
|
||||
}
|
||||
|
||||
@@ -558,6 +775,11 @@ public class TripQueryServiceImpl implements ITripQueryService {
|
||||
return id == null ? null : String.valueOf(id);
|
||||
}
|
||||
|
||||
/** 一趟车:运次键、子单行(段一取回的、含行级筛选后的)、派生状态与排序键 */
|
||||
private record TripRows(String key, String loadingNo, List<Waybill> children, String status, Integer tier,
|
||||
Date sortTime) {
|
||||
}
|
||||
|
||||
private int pageNo(Integer current) {
|
||||
return current == null || current < 1 ? 1 : current;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user