✨ 小程序调度端运单列表、待处理与统计按组织收口

原先三个取数入口都不带组织条件,租户内两个组织的运单混在同一张表里。「小程序管理」
账号与运单业务组织不一致确实是事实,但那是账号挂在一个已软删部门上的脏数据症状,
不是「不该按组织过滤」的理由。

现在与后台运单列表同一条规则:普通用户只出自己所属组织,超级管理员不过滤,
小程序端不提供「全部组织」开关;详情与重新派单照旧不按组织收口。
组织条件由调用方算好、经 TripQueryVO.deptIds 落到运次读服务的运单行上,
列表、待处理与首页三档统计因此共用同一条可见范围;「待重新派单」另走库层分组查询,
单独带上,首页重派角标与待办 feed 随之一起收口。

运次读服务的分档统计走一份「去掉 Tab 条件」的副本,该副本必须照搬 deptIds,
否则统计会退回全租户、与列表的数对不上(已由用例钉住)。

存量 42 张 dept_id 为 -1 或 NULL 的运单不属于任何组织,收紧后普通用户再也看不到,
另订正到各自的创建组织,见 doc/sql/transport/blade_waybill_dept_backfill_20261018.sql。

新增 ManageWaybillOrgScopeTest(8 项),TripQueryServiceImplTest 补 3 项;
blade-transport 全量 376 项用例通过。
This commit is contained in:
2026-10-18 11:22:04 +08:00
parent 1720f9192f
commit 7d951d5b04
9 changed files with 403 additions and 7 deletions
@@ -69,7 +69,7 @@ public class ManageWaybillController extends BladeController {
@GetMapping("/stats")
@ApiOperationSupport(order = 1)
@Operation(summary = "运单状态统计", description = "待接单=pending,运输中=running,已完成=completed;租户内不过滤组织(小程序调度账号组织常与运单不一致);在途异常=异常处置状态≠已完成")
@Operation(summary = "运单状态统计", description = "待接单=pending,运输中=running,已完成=completed;只统计本组织的运单(超级管理员不过滤组织);在途异常=异常处置状态≠已完成")
public R<AdminHomeStatsVO> stats() {
return R.data(manageWaybillService.stats());
}
@@ -59,6 +59,7 @@ import org.springblade.transport.service.IManageWaybillService;
import org.springblade.transport.service.IRiskDisposalService;
import org.springblade.transport.service.ITripQueryService;
import org.springblade.transport.service.IWaybillService;
import org.springblade.transport.support.TransportBusinessSupport;
import org.springblade.transport.support.WaybillProcessSupport;
import org.springframework.stereotype.Service;
@@ -140,7 +141,9 @@ public class ManageWaybillServiceImpl implements IManageWaybillService {
public AdminHomeStatsVO stats() {
AdminHomeStatsVO vo = new AdminHomeStatsVO();
// 三档状态按运次计:有配载单的一趟车算一个,与列表和司机端同一份聚合
TripTabCountsVO tripCounts = tripQueryService.tabCounts(TripQueryVO.forAdmin());
TripQueryVO query = TripQueryVO.forAdmin();
query.setDeptIds(manageableDeptIds());
TripTabCountsVO tripCounts = tripQueryService.tabCounts(query);
vo.setPendingAccept(tripCounts.getPending());
vo.setTransporting(tripCounts.getDoing());
vo.setCompleted(tripCounts.getDone());
@@ -177,6 +180,7 @@ public class ManageWaybillServiceImpl implements IManageWaybillService {
// 组织类型 / 异常 / 创建时间都是子单行级条件,与状态、关键字、分页一起交给运次读服务,
// 一次取数取完整趟车:分组、分档、筛选、排序与分页都在它那边算完
TripQueryVO query = TripQueryVO.forAdmin();
query.setDeptIds(manageableDeptIds());
query.setStatus(toTripStatus(status));
query.setKeyword(keyword);
query.setTransportOrgType(transportType);
@@ -224,6 +228,7 @@ public class ManageWaybillServiceImpl implements IManageWaybillService {
Set<Long> exceptionWaybillIds = loadIncompleteExceptionWaybillIds();
TripQueryVO query = TripQueryVO.forAdmin();
query.setDeptIds(manageableDeptIds());
query.setKeyword(keyword);
// 待处理只要待接单与进行中两档;「未拒单」也是交给运次读服务按子单行筛
query.setStatuses(List.of(0, 1));
@@ -686,6 +691,11 @@ 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);
// 这条查询不经运次读服务,组织收口得自己带上,否则待重派列表与首页重派角标会出别的组织的单
List<Long> deptIds = manageableDeptIds();
if (Func.isNotEmpty(deptIds)) {
wrapper.in("dept_id", deptIds);
}
wrapper.groupBy(TripAggregation.SQL_TRIP_KEY);
return wrapper;
}
@@ -895,11 +905,22 @@ public class ManageWaybillServiceImpl implements IManageWaybillService {
}
/**
* 小程序调度端不做组织过滤。
* 「小程序管理」等账号 JWT/档案 dept 常与运单业务组织不一致,按 dept 过滤会导致统计全 0;
* 与后台 allDept=1 一致,仅依赖租户隔离(MyBatis-Plus TenantLine)。
* 调度端可见组织:普通用户(含各级角色)只出自己所属组织,超级管理员不过滤。
* <p>
* 与后台运单列表同一条规则({@link TransportBusinessSupport#resolveDeptFilterIds}),
* 小程序端不提供「全部组织」开关,故两个入参固定传空;详情与重新派单不按组织收口。
*/
private LambdaQueryWrapper<Waybill> scopedWaybillQuery() {
private List<Long> manageableDeptIds() {
return TransportBusinessSupport.resolveDeptFilterIds(null, null, "运单管理");
}
/**
* 已拒子单的查询底:只有租户与逻辑删除(MyBatis-Plus 自动补),不带组织。
* <p>
* 它服务的是「这个运次里有没有被拒的子单」,可见范围由调用方先定好——列表传的运次已经收过组织,
* 详情按主键进入不收组织。这里再按组织收一次,详情里别的组织的运次会丢掉重派标记。
*/
private LambdaQueryWrapper<Waybill> rejectedChildQuery() {
return Wrappers.<Waybill>lambdaQuery();
}
@@ -1039,7 +1060,7 @@ public class ManageWaybillServiceImpl implements IManageWaybillService {
}
private LambdaQueryWrapper<Waybill> pendingReassignQuery() {
return scopedWaybillQuery()
return rejectedChildQuery()
.eq(Waybill::getDriverAcceptStatus, ACCEPT_REJECTED)
.in(Waybill::getBusinessStatus, STATUS_PENDING, STATUS_RUNNING);
}
@@ -236,6 +236,7 @@ public class TripQueryServiceImpl implements ITripQueryService {
.and(visible -> visible.isNull(Waybill::getDriverAcceptStatus)
.or().ne(Waybill::getDriverAcceptStatus, WaybillProcessSupport.ACCEPT_REJECTED));
applyScope(wrapper, condition);
applyDeptScope(wrapper, condition);
applyOrgTypeFilter(wrapper, condition);
applyKeyword(wrapper, condition.getKeyword());
List<Waybill> rows = waybillService.list(wrapper);
@@ -243,6 +244,19 @@ public class TripQueryServiceImpl implements ITripQueryService {
return rows;
}
/**
* 组织收口:条件里给了组织 ID 集合时只出这些组织的运单行,空集合表示不过滤。
* <p>
* 与司机手机号收口一样落在库层的运单行上:分档、分页、总数都由它缩过的行集合算出来。
* 读服务不自己解析当前用户的组织——它同时服务司机端与管理端,可见范围由调用方算好再传。
*/
private void applyDeptScope(LambdaQueryWrapper<Waybill> wrapper, TripQueryVO condition) {
if (Func.isEmpty(condition.getDeptIds())) {
return;
}
wrapper.in(Waybill::getDeptId, condition.getDeptIds());
}
/**
* 运输组织类型按子单行筛:它在同一趟车里必然是同一个值(配载的每张子单带同一个配载单号,
* 未配载的每张自成一趟车),所以这一层不会把一趟车拆开。
@@ -709,6 +723,7 @@ public class TripQueryServiceImpl implements ITripQueryService {
TripQueryVO copy = new TripQueryVO();
copy.setDriverPhone(source.getDriverPhone());
copy.setAllDrivers(source.isAllDrivers());
copy.setDeptIds(source.getDeptIds());
copy.setKeyword(source.getKeyword());
copy.setTransportOrgType(source.getTransportOrgType());
copy.setException(source.getException());