✨ 小程序调度端运单列表、待处理与统计按组织收口
原先三个取数入口都不带组织条件,租户内两个组织的运单混在同一张表里。「小程序管理」 账号与运单业务组织不一致确实是事实,但那是账号挂在一个已软删部门上的脏数据症状, 不是「不该按组织过滤」的理由。 现在与后台运单列表同一条规则:普通用户只出自己所属组织,超级管理员不过滤, 小程序端不提供「全部组织」开关;详情与重新派单照旧不按组织收口。 组织条件由调用方算好、经 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:
+3
@@ -53,6 +53,9 @@ public class TripQueryVO implements Serializable {
|
||||
@Schema(description = "是否不按司机收口(管理端)")
|
||||
private boolean allDrivers;
|
||||
|
||||
@Schema(description = "可见组织 ID 集合:非空时只出这些组织的运单行,空集合表示不按组织过滤")
|
||||
private List<Long> deptIds;
|
||||
|
||||
@Schema(description = "状态:0待接单/1进行中/2已完成/3已取消,不传为全部(不含已取消)")
|
||||
private Integer status;
|
||||
|
||||
|
||||
+1
-1
@@ -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());
|
||||
}
|
||||
|
||||
+27
-6
@@ -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);
|
||||
}
|
||||
|
||||
+15
@@ -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());
|
||||
|
||||
+16
@@ -5,12 +5,15 @@ import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import org.apache.ibatis.builder.MapperBuilderAssistant;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.ArgumentMatchers;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.springblade.core.secure.utils.AuthUtil;
|
||||
import org.springblade.transport.pojo.entity.ExceptionDisposal;
|
||||
import org.springblade.transport.pojo.entity.Waybill;
|
||||
import org.springblade.transport.pojo.vo.AdminTodoItemVO;
|
||||
@@ -34,6 +37,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@@ -52,10 +56,13 @@ class ManageAdminHomeFeedTest {
|
||||
private static final Long EXCEPTION_ID = 8801L;
|
||||
private static final String REJECTED_WAYBILL_NO = "YD-20261010-0001";
|
||||
private static final String REJECTED_LOADING_NO = "PZ-20261017-001";
|
||||
private static final String ROLE_ORDINARY = "yewu";
|
||||
private static final String DEPT_ID = "1123598813738675201";
|
||||
|
||||
private ManageWaybillServiceImpl service;
|
||||
private IWaybillService waybillService;
|
||||
private IExceptionDisposalService exceptionService;
|
||||
private MockedStatic<AuthUtil> authUtil;
|
||||
|
||||
@BeforeAll
|
||||
static void initTableInfo() {
|
||||
@@ -82,6 +89,15 @@ class ManageAdminHomeFeedTest {
|
||||
.thenReturn(new ArrayList<>());
|
||||
when(waybillService.list(ArgumentMatchers.<Wrapper<Waybill>>any()))
|
||||
.thenReturn(new ArrayList<>());
|
||||
// 重派待办的库层查询要带可见组织;没有请求上下文时按普通用户 + 本组织给出
|
||||
authUtil = mockStatic(AuthUtil.class);
|
||||
authUtil.when(AuthUtil::getUserRole).thenReturn(ROLE_ORDINARY);
|
||||
authUtil.when(AuthUtil::getDeptId).thenReturn(DEPT_ID);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
authUtil.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+16
@@ -6,13 +6,16 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import org.apache.ibatis.builder.MapperBuilderAssistant;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.ArgumentMatchers;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.secure.utils.AuthUtil;
|
||||
import org.springblade.transport.pojo.entity.ExceptionDisposal;
|
||||
import org.springblade.transport.pojo.entity.LoadingManage;
|
||||
import org.springblade.transport.pojo.entity.Waybill;
|
||||
@@ -41,6 +44,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
@@ -58,12 +62,15 @@ class ManageTripListDetailTest {
|
||||
private static final Long LOADING_ID = 2111350784070275074L;
|
||||
private static final Long CHILD_A = 2111350784070275123L;
|
||||
private static final Long CHILD_B = 2102780956493299713L;
|
||||
private static final String ROLE_ORDINARY = "yewu";
|
||||
private static final String DEPT_ID = "1123598813738675201";
|
||||
|
||||
private ManageWaybillServiceImpl service;
|
||||
private IWaybillService waybillService;
|
||||
private IExceptionDisposalService exceptionDisposalService;
|
||||
private ITripQueryService tripQueryService;
|
||||
private ILoadingManageService loadingManageService;
|
||||
private MockedStatic<AuthUtil> authUtil;
|
||||
private IDriverWaybillService driverWaybillService;
|
||||
|
||||
private static java.time.LocalDate toLocalDate(Date date) {
|
||||
@@ -98,6 +105,15 @@ class ManageTripListDetailTest {
|
||||
when(exceptionDisposalService.list(ArgumentMatchers.<Wrapper<ExceptionDisposal>>any()))
|
||||
.thenReturn(new ArrayList<>());
|
||||
when(waybillService.list(ArgumentMatchers.<Wrapper<Waybill>>any())).thenReturn(new ArrayList<>());
|
||||
// 列表与待处理都要先算出可见组织;没有请求上下文时按普通用户 + 本组织给出
|
||||
authUtil = mockStatic(AuthUtil.class);
|
||||
authUtil.when(AuthUtil::getUserRole).thenReturn(ROLE_ORDINARY);
|
||||
authUtil.when(AuthUtil::getDeptId).thenReturn(DEPT_ID);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
authUtil.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+226
@@ -0,0 +1,226 @@
|
||||
package org.springblade.transport.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import org.apache.ibatis.builder.MapperBuilderAssistant;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.ArgumentMatchers;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.secure.utils.AuthUtil;
|
||||
import org.springblade.transport.pojo.entity.ExceptionDisposal;
|
||||
import org.springblade.transport.pojo.entity.Waybill;
|
||||
import org.springblade.transport.pojo.vo.TripQueryVO;
|
||||
import org.springblade.transport.pojo.vo.TripTabCountsVO;
|
||||
import org.springblade.transport.pojo.vo.TripVO;
|
||||
import org.springblade.transport.service.IDriverService;
|
||||
import org.springblade.transport.service.IDriverWaybillService;
|
||||
import org.springblade.transport.service.IExceptionDisposalService;
|
||||
import org.springblade.transport.service.ILoadingManageService;
|
||||
import org.springblade.transport.service.IRiskDisposalService;
|
||||
import org.springblade.transport.service.ITripQueryService;
|
||||
import org.springblade.transport.service.IWaybillService;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.atLeastOnce;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* 调度端的组织可见范围:普通用户只出自己所属组织,超级管理员不过滤。
|
||||
* <p>
|
||||
* 三个取数入口必须同一条规则——列表、待处理、首页三档统计各走一次查询,漏一处就会出现
|
||||
* 「统计说 46 张、列表只出 18 张」这种对不上的数;待重新派单另走库层分组查询,也要单独带上。
|
||||
* <p>
|
||||
* 组织取不到时报错而不是放行:放行等于把全租户的运单发给一个没有组织的账号。
|
||||
*/
|
||||
class ManageWaybillOrgScopeTest {
|
||||
|
||||
private static final String DEPT_ID = "1123598813738675202";
|
||||
private static final Long DEPT = 1123598813738675202L;
|
||||
private static final String ROLE_ORDINARY = "yewu";
|
||||
private static final String ROLE_ADMINISTRATOR = "administrator";
|
||||
|
||||
private ManageWaybillServiceImpl service;
|
||||
private IWaybillService waybillService;
|
||||
private ITripQueryService tripQueryService;
|
||||
private MockedStatic<AuthUtil> authUtil;
|
||||
|
||||
@BeforeAll
|
||||
static void initTableInfo() {
|
||||
MybatisConfiguration configuration = new MybatisConfiguration();
|
||||
MapperBuilderAssistant assistant = new MapperBuilderAssistant(configuration, "");
|
||||
assistant.setCurrentNamespace("test");
|
||||
TableInfoHelper.initTableInfo(assistant, Waybill.class);
|
||||
TableInfoHelper.initTableInfo(assistant, ExceptionDisposal.class);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
waybillService = mock(IWaybillService.class);
|
||||
tripQueryService = mock(ITripQueryService.class);
|
||||
IExceptionDisposalService exceptionDisposalService = mock(IExceptionDisposalService.class);
|
||||
service = new ManageWaybillServiceImpl(
|
||||
waybillService,
|
||||
mock(IDriverService.class),
|
||||
exceptionDisposalService,
|
||||
mock(IRiskDisposalService.class),
|
||||
mock(IDriverWaybillService.class),
|
||||
tripQueryService,
|
||||
mock(ILoadingManageService.class));
|
||||
when(exceptionDisposalService.list(ArgumentMatchers.<Wrapper<ExceptionDisposal>>any()))
|
||||
.thenReturn(new ArrayList<>());
|
||||
when(waybillService.list(ArgumentMatchers.<Wrapper<Waybill>>any())).thenReturn(new ArrayList<>());
|
||||
// 待重派那次分页查询回它收到的 Page(总数从入参上读,回 null 会空指针)
|
||||
when(waybillService.page(any(), any())).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
// 运次读服务的两次取数:本类只看传给它的条件,行集合给空页即可
|
||||
when(tripQueryService.page(any())).thenReturn(emptyTripPage());
|
||||
when(tripQueryService.tabCounts(any())).thenReturn(new TripTabCountsVO());
|
||||
authUtil = mockStatic(AuthUtil.class);
|
||||
asOrdinaryUser();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
authUtil.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("普通用户:列表把当前组织交给运次读服务,行级收口落在库层")
|
||||
void pageListScopesCurrentDept() {
|
||||
service.pageList(1, 10, null, null, null, null, null, null);
|
||||
|
||||
TripQueryVO query = capturedPageQuery();
|
||||
assertEquals(List.of(DEPT), query.getDeptIds(), "列表只出本组织");
|
||||
assertTrue(query.isAllDrivers(), "组织收口不影响司机收口口径");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("普通用户:待处理列表与列表同一条组织规则")
|
||||
void pendingListScopesCurrentDept() {
|
||||
service.pendingList(1, 10, null, false);
|
||||
|
||||
assertEquals(List.of(DEPT), capturedPageQuery().getDeptIds());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("普通用户:首页三档统计与列表同一口径,否则统计与列表的数对不上")
|
||||
void statsScopesCurrentDept() {
|
||||
service.stats();
|
||||
|
||||
ArgumentCaptor<TripQueryVO> captor = ArgumentCaptor.forClass(TripQueryVO.class);
|
||||
verify(tripQueryService, atLeastOnce()).tabCounts(captor.capture());
|
||||
assertEquals(List.of(DEPT), captor.getValue().getDeptIds());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("超级管理员:三处都不带组织,看全租户")
|
||||
void administratorSeesEveryDept() {
|
||||
asAdministrator();
|
||||
|
||||
service.pageList(1, 10, null, null, null, null, null, null);
|
||||
ArgumentCaptor<TripQueryVO> pageCaptor = ArgumentCaptor.forClass(TripQueryVO.class);
|
||||
verify(tripQueryService).page(pageCaptor.capture());
|
||||
assertTrue(pageCaptor.getValue().getDeptIds().isEmpty(), "超管不过滤组织");
|
||||
|
||||
service.stats();
|
||||
ArgumentCaptor<TripQueryVO> statsCaptor = ArgumentCaptor.forClass(TripQueryVO.class);
|
||||
verify(tripQueryService, atLeastOnce()).tabCounts(statsCaptor.capture());
|
||||
assertTrue(statsCaptor.getValue().getDeptIds().isEmpty(), "超管的统计也不过过滤组织");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("待重新派单:库层分组查询也带组织,否则重派列表与角标会出别的组织的单")
|
||||
void reassignQueryScopesCurrentDept() {
|
||||
service.pendingList(1, 10, null, true);
|
||||
|
||||
ArgumentCaptor<Wrapper<Waybill>> captor = ArgumentCaptor.forClass(Wrapper.class);
|
||||
verify(waybillService).page(any(), captor.capture());
|
||||
QueryWrapper<Waybill> wrapper = (QueryWrapper<Waybill>) captor.getValue();
|
||||
String sql = String.valueOf(wrapper.getTargetSql());
|
||||
assertTrue(sql.contains("dept_id"), "重派查询要带上组织:" + sql);
|
||||
Set<Object> values = new HashSet<>(wrapper.getParamNameValuePairs().values());
|
||||
assertTrue(values.contains(DEPT), "组织值要作为参数下发:" + values);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("待重新派单:超管的库层分组查询不带组织")
|
||||
void reassignQuerySkipsScopeForAdministrator() {
|
||||
asAdministrator();
|
||||
|
||||
service.pendingList(1, 10, null, true);
|
||||
|
||||
ArgumentCaptor<Wrapper<Waybill>> captor = ArgumentCaptor.forClass(Wrapper.class);
|
||||
verify(waybillService).page(any(), captor.capture());
|
||||
String sql = String.valueOf(captor.getValue().getTargetSql());
|
||||
assertFalse(sql.contains("dept_id"), "超管不按组织收口:" + sql);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("详情不按组织收口:按主键进入,不带组织条件")
|
||||
void detailIsNotScoped() {
|
||||
when(tripQueryService.detail(any())).thenThrow(new ServiceException("运次不存在或无权操作"));
|
||||
when(waybillService.getById(any())).thenReturn(null);
|
||||
assertThrows(ServiceException.class, () -> service.detail("7201"));
|
||||
|
||||
ArgumentCaptor<TripQueryVO> captor = ArgumentCaptor.forClass(TripQueryVO.class);
|
||||
verify(tripQueryService).detail(captor.capture());
|
||||
assertTrue(captor.getValue().getDeptIds() == null || captor.getValue().getDeptIds().isEmpty(),
|
||||
"详情与后台同口径:不按组织收口");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("组织取不到时报错,不退回不带组织条件")
|
||||
void blankDeptFailsClosed() {
|
||||
authUtil.when(AuthUtil::getDeptId).thenReturn("");
|
||||
|
||||
assertThrows(ServiceException.class,
|
||||
() -> service.pageList(1, 10, null, null, null, null, null, null),
|
||||
"没有组织的账号不能拿到全租户运单");
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ 夹具
|
||||
|
||||
private IPage<TripVO> emptyTripPage() {
|
||||
Page<TripVO> page = new Page<>(1, 10, 0);
|
||||
page.setRecords(List.of());
|
||||
return page;
|
||||
}
|
||||
|
||||
private TripQueryVO capturedPageQuery() {
|
||||
ArgumentCaptor<TripQueryVO> captor = ArgumentCaptor.forClass(TripQueryVO.class);
|
||||
verify(tripQueryService).page(captor.capture());
|
||||
return captor.getValue();
|
||||
}
|
||||
|
||||
private void asOrdinaryUser() {
|
||||
authUtil.when(AuthUtil::getUserRole).thenReturn(ROLE_ORDINARY);
|
||||
authUtil.when(AuthUtil::getDeptId).thenReturn(DEPT_ID);
|
||||
}
|
||||
|
||||
private void asAdministrator() {
|
||||
authUtil.when(AuthUtil::getUserRole).thenReturn(ROLE_ADMINISTRATOR);
|
||||
authUtil.when(AuthUtil::getDeptId).thenReturn(DEPT_ID);
|
||||
}
|
||||
|
||||
}
|
||||
+38
@@ -24,7 +24,9 @@ import org.springblade.transport.service.IProcessConfigService;
|
||||
import org.springblade.transport.service.IWaybillService;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
@@ -116,6 +118,42 @@ class TripQueryServiceImplTest {
|
||||
assertFalse(select.contains(String.valueOf((Object) null)) && select.contains("*"), "不得整行拖回:" + select);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("组织收口给了就落在库层运单行上,分档与分页都由缩过的行集合算")
|
||||
void deptScopeIsAppliedOnRows() {
|
||||
TripQueryVO query = TripQueryVO.forDriver(PHONE);
|
||||
query.setDeptIds(List.of(1123598813738675202L, 1123598813738675201L));
|
||||
|
||||
service.page(query);
|
||||
|
||||
String sql = capturedRowSql();
|
||||
assertTrue(sql.contains("dept_id IN"), "组织收口要下发到库层:" + sql);
|
||||
Set<Object> values = new HashSet<>(capturedRowQuery().getParamNameValuePairs().values());
|
||||
assertTrue(values.contains(1123598813738675202L) && values.contains(1123598813738675201L),
|
||||
"组织值按参数下发:" + values);
|
||||
assertTrue(capturedRowSql().contains("driver_phone"), "组织收口不顶替司机收口:两种收口同时生效");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("统计四档时组织收口不能丢:去掉的只有 Tab 条件,其余条件照搬")
|
||||
void tabCountsKeepsDeptScope() {
|
||||
TripQueryVO query = TripQueryVO.forAdmin();
|
||||
query.setDeptIds(List.of(1123598813738675202L));
|
||||
|
||||
service.tabCounts(query);
|
||||
|
||||
assertTrue(capturedRowSql().contains("dept_id IN"),
|
||||
"统计与列表必须是同一份可见范围,否则首页三张卡与列表的数对不上:" + capturedRowSql());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("组织收口缺省时不下发组织条件:超管与司机端都按不传处理")
|
||||
void deptScopeIsOptional() {
|
||||
service.page(TripQueryVO.forDriver(PHONE));
|
||||
|
||||
assertFalse(capturedRowSql().contains("dept_id"), "不给组织集合即不过滤:" + capturedRowSql());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Tab 在服务层按推导分档过滤,每张卡的 status 与该档一致")
|
||||
void tabFilterHappensOnDerivedTier() {
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
-- ----------------------------
|
||||
-- 调度端运单列表按组织过滤:dept_id 缺失的存量运单订正回它所属的组织
|
||||
-- 写业务数据。执行前必须先建备份表(第 0 节),执行后必须跑第 2 节校验。可重复执行。
|
||||
-- ----------------------------
|
||||
--
|
||||
-- 为什么要订正:
|
||||
-- 调度端(小程序管理端)运单列表的可见范围是「本组织」,超管看全租户。
|
||||
-- dept_id 为 NULL 或 -1 的运单不属于任何组织,普通用户再也看不到它们。
|
||||
-- 本机库实测 42 张:34 张的 dept_id = -1、8 张为 NULL,全部是 2026-10-15 / 10-16
|
||||
-- 由 admin 在 1123598813738675201 下建的(-1 是客户端把「未选组织」当 -1 传上来的哨兵值,
|
||||
-- 服务端写入前的兜底只判空、拦不住负数)。
|
||||
--
|
||||
-- 处置口径:取该运单的 create_dept(创建人所属组织)作为它的组织。
|
||||
-- 42 张的 create_dept 都是 1123598813738675201,与它们的业务归属一致;
|
||||
-- 按 create_dept 取而不是写死这个 id,其它环境执行时同样成立。
|
||||
-- dept_name 取组织表当前的名字:新建运单写的就是这个名字,不跟随存量行里的历史短名
|
||||
-- (「桂物物流集团」这类短名是历史写法,本次只补缺失值,不复位已存在的值)。
|
||||
-- 只动 dept_id 为空或非正数的行,且要求 create_dept 为正数:重复执行不再改动任何行。
|
||||
-- update_time 不在 SET 里,保留原值。
|
||||
-- ----------------------------
|
||||
-- 0. 备份(必须在订正之前执行,仅首次需要)
|
||||
-- ----------------------------
|
||||
-- CREATE TABLE blade_waybill_dept_bak AS
|
||||
-- SELECT id, waybill_no, dept_id, dept_name, create_dept, update_time
|
||||
-- FROM blade_waybill
|
||||
-- WHERE is_deleted = 0 AND (dept_id IS NULL OR dept_id <= 0);
|
||||
|
||||
START TRANSACTION;
|
||||
|
||||
-- ----------------------------
|
||||
-- 1. 订正组织
|
||||
-- ----------------------------
|
||||
UPDATE `blade_waybill`
|
||||
SET `dept_id` = `create_dept`,
|
||||
`dept_name` = (SELECT `dept_name` FROM `blade_dept` WHERE `id` = `blade_waybill`.`create_dept` LIMIT 1)
|
||||
WHERE `is_deleted` = 0
|
||||
AND (`dept_id` IS NULL OR `dept_id` <= 0)
|
||||
AND `create_dept` IS NOT NULL
|
||||
AND `create_dept` > 0;
|
||||
|
||||
COMMIT;
|
||||
|
||||
-- ----------------------------
|
||||
-- 2. 校验(执行后必看,两个都必须返回 0)
|
||||
-- ----------------------------
|
||||
-- -- 仍不属于任何组织的可见运单
|
||||
-- SELECT COUNT(*) FROM blade_waybill
|
||||
-- WHERE is_deleted = 0 AND (dept_id IS NULL OR dept_id <= 0) AND create_dept > 0;
|
||||
--
|
||||
-- -- 本次订正过的行,组织 ID 与组织名都必须跟组织表一致
|
||||
-- SELECT COUNT(*) FROM blade_waybill w
|
||||
-- JOIN blade_waybill_dept_bak b ON b.id = w.id
|
||||
-- JOIN blade_dept d ON d.id = w.dept_id
|
||||
-- WHERE w.dept_id <> w.create_dept OR w.dept_name <> d.dept_name;
|
||||
|
||||
-- ----------------------------
|
||||
-- 3. 回退(订正后发现问题时执行)
|
||||
-- ----------------------------
|
||||
-- UPDATE blade_waybill w
|
||||
-- JOIN blade_waybill_dept_bak b ON b.id = w.id
|
||||
-- SET w.dept_id = b.dept_id, w.dept_name = b.dept_name;
|
||||
Reference in New Issue
Block a user