diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/NodeSubmitDTO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/NodeSubmitDTO.java
index 0ba2fba4..46720941 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/NodeSubmitDTO.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/NodeSubmitDTO.java
@@ -54,6 +54,15 @@ public class NodeSubmitDTO implements Serializable {
@Schema(description = "数量(件)")
private String quantity;
+ /**
+ * 里程(公里):只由末位打卡节点采集,运单计费规则含里程时必填。
+ *
+ * 用字符串接收——旧版小程序不传、新版按输入框传字符串,两种都要能进;
+ * 非末位节点收到该值时忽略,不报错。
+ */
+ @Schema(description = "里程(公里),末位打卡节点采集")
+ private String mileage;
+
@Schema(description = "备注")
private String remark;
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/WaybillPunchRecordSaveRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/WaybillPunchRecordSaveRequest.java
index 763f8f31..af45b2df 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/WaybillPunchRecordSaveRequest.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/WaybillPunchRecordSaveRequest.java
@@ -27,6 +27,7 @@ import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
+import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
@@ -84,6 +85,13 @@ public class WaybillPunchRecordSaveRequest implements Serializable {
@Schema(description = "数量(件)")
private String quantity;
+ /**
+ * 里程(公里):null 表示不改动运单里程;只在末位打卡节点上生效。
+ * 管理端为选填,留空即保持原值。
+ */
+ @Schema(description = "里程(公里),为空表示不改")
+ private BigDecimal mileage;
+
@Schema(description = "备注")
private String remark;
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverPunchNodeVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverPunchNodeVO.java
index eaeec543..7533ce2e 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverPunchNodeVO.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverPunchNodeVO.java
@@ -11,6 +11,7 @@ import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
+import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
@@ -94,6 +95,17 @@ public class DriverPunchNodeVO implements Serializable {
@Schema(description = "数量(件)")
private String quantity;
+ /**
+ * 本节点是否需要采集里程:末位可打卡节点、且运单计费规则含里程(应收或应付任一侧)。
+ *
+ * 判据与 finishOnPunch、自动完成同源的「末位」定义;客户端只认这个标记,不自己推末位。
+ */
+ @Schema(description = "本节点是否需要采集里程(末位可打卡节点且计费规则含里程)")
+ private Boolean needMileage;
+
+ @Schema(description = "运单里程(公里),needMileage 为真时用于回显")
+ private BigDecimal mileage;
+
@Schema(description = "已上传凭证图(已打卡回显)")
private List photos = new ArrayList<>();
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverWaybillTabCountsVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverWaybillTabCountsVO.java
index 3aa95f5e..79787cb7 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverWaybillTabCountsVO.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/DriverWaybillTabCountsVO.java
@@ -40,7 +40,7 @@ public class DriverWaybillTabCountsVO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
- @Schema(description = "全部(待接单+运输中+已完成)")
+ @Schema(description = "全部(待接单+运输中+待确认+已完成)")
private long all;
@Schema(description = "待接单")
@@ -49,6 +49,9 @@ public class DriverWaybillTabCountsVO implements Serializable {
@Schema(description = "进行中(运输中)")
private long doing;
+ @Schema(description = "待确认(待回单确认)")
+ private long confirm;
+
@Schema(description = "已完成")
private long done;
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TripTabCountsVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TripTabCountsVO.java
index 944c4ded..01eb22f7 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TripTabCountsVO.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TripTabCountsVO.java
@@ -40,7 +40,7 @@ public class TripTabCountsVO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
- @Schema(description = "全部(待接单+进行中+已完成,不含已取消)")
+ @Schema(description = "全部(待接单+进行中+待确认+已完成,不含已取消)")
private long all;
@Schema(description = "待接单")
@@ -49,6 +49,9 @@ public class TripTabCountsVO implements Serializable {
@Schema(description = "进行中")
private long doing;
+ @Schema(description = "待确认(待回单确认)")
+ private long confirm;
+
@Schema(description = "已完成")
private long done;
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillPunchRecordItemVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillPunchRecordItemVO.java
index 140d6bc8..62f089ba 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillPunchRecordItemVO.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillPunchRecordItemVO.java
@@ -9,6 +9,7 @@ import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
+import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
@@ -55,6 +56,18 @@ public class WaybillPunchRecordItemVO implements Serializable {
@Schema(description = "数量")
private String quantity;
+ /**
+ * 本条记录是否采集里程:末位打卡节点、且运单计费规则含里程。只有该条为真时管理端才展示里程项。
+ */
+ @Schema(description = "本条是否需要采集里程(末位打卡节点且计费规则含里程)")
+ private Boolean needMileage;
+
+ @Schema(description = "运单里程(公里),needMileage 为真时用于回显")
+ private BigDecimal mileage;
+
+ @Schema(description = "里程是否已冻结(运单已生成结算单,只能看不能改)")
+ private Boolean mileageLocked;
+
@Schema(description = "备注")
private String remark;
diff --git a/blade-service/blade-system/pom.xml b/blade-service/blade-system/pom.xml
index 81ac5a93..27c09ffa 100644
--- a/blade-service/blade-system/pom.xml
+++ b/blade-service/blade-system/pom.xml
@@ -94,6 +94,13 @@
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/MpUserServiceImpl.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/MpUserServiceImpl.java
index c93c8097..55e6556f 100644
--- a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/MpUserServiceImpl.java
+++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/MpUserServiceImpl.java
@@ -299,7 +299,7 @@ public class MpUserServiceImpl extends BaseServiceImpl imp
throw new ServiceException("该手机号已存在小程序用户");
}
encryptPasswordIfPresent(user);
- boolean saved = save(user) && submitScopes(user);
+ boolean saved = save(user) && submitScopes(user, true);
if (saved) {
MpUserCache.clear();
}
@@ -316,6 +316,20 @@ public class MpUserServiceImpl extends BaseServiceImpl imp
if (dbUser == null || Func.isEmpty(dbUser.getId())) {
throw new ServiceException("小程序用户不存在");
}
+ // 数据权限三字段以 null 表示调用方未提供(与 updateById 忽略 null 同口径),未提供的用库值回填。
+ // 判定与回填都必须在 prepare 之前:prepare 会把 range != 3 的 dataScopeDeptIds 置空,
+ // 之后就分不清「未提供」和「提供了空列表」。
+ // 不变量:只改人口字段的更新不得改变数据权限范围、层级与自定义组织。
+ boolean rebuildScopes = user.getDataScopeRange() != null || user.getDataScopeDeptIds() != null;
+ if (user.getDataScopeRange() == null) {
+ user.setDataScopeRange(dbUser.getDataScopeRange());
+ }
+ if (user.getDataLevelRange() == null) {
+ user.setDataLevelRange(dbUser.getDataLevelRange());
+ }
+ if (user.getDataScopeDeptIds() == null && Objects.equals(user.getDataScopeRange(), 3)) {
+ user.setDataScopeDeptIds(fillScopes(dbUser).getDataScopeDeptIds());
+ }
prepare(user);
// 未改手机号(前端脱敏未提交 / 仍为脱敏值)时沿用库中原值,避免把 138****8000 写回
if (Func.isBlank(user.getPhone()) || isMaskedPhone(user.getPhone())) {
@@ -331,7 +345,7 @@ public class MpUserServiceImpl extends BaseServiceImpl imp
}
// 后台编辑不改密码,避免空串覆盖
user.setPassword(null);
- boolean updated = updateById(user) && submitScopes(user);
+ boolean updated = updateById(user) && submitScopes(user, rebuildScopes);
if (updated) {
MpUserCache.clear();
}
@@ -552,9 +566,7 @@ public class MpUserServiceImpl extends BaseServiceImpl imp
if (Func.isBlank(user.getPhone())) {
throw new ServiceException("手机号不能为空");
}
- if (Func.isBlank(user.getDeptId())) {
- throw new ServiceException("请选择所属组织");
- }
+ // 账号组织只服务调度端数据权限:为空表示空集(不是全部组织),司机身份不需要它,故不作必填
if (Func.isBlank(user.getIdentities())) {
throw new ServiceException("小程序身份不能为空");
}
@@ -580,10 +592,13 @@ public class MpUserServiceImpl extends BaseServiceImpl imp
}
}
- private boolean submitScopes(MpUser user) {
+ private boolean submitScopes(MpUser user, boolean rebuild) {
if (user == null || Func.isEmpty(user.getId())) {
return false;
}
+ if (!rebuild) {
+ return true;
+ }
mpUserDataScopeMapper.delete(Wrappers.lambdaQuery().eq(MpUserDataScope::getUserId, user.getId()));
if (Objects.equals(user.getDataScopeRange(), 3) && Func.isNotEmpty(user.getDataScopeDeptIds())) {
user.getDataScopeDeptIds().stream().filter(Objects::nonNull).distinct().forEach(deptId -> {
diff --git a/blade-service/blade-system/src/test/java/org/springblade/system/service/impl/MpUserServiceImplTest.java b/blade-service/blade-system/src/test/java/org/springblade/system/service/impl/MpUserServiceImplTest.java
new file mode 100644
index 00000000..8ff82829
--- /dev/null
+++ b/blade-service/blade-system/src/test/java/org/springblade/system/service/impl/MpUserServiceImplTest.java
@@ -0,0 +1,411 @@
+package org.springblade.system.service.impl;
+
+import com.baomidou.mybatisplus.core.MybatisConfiguration;
+import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
+import org.apache.ibatis.builder.MapperBuilderAssistant;
+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.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.mockito.ArgumentCaptor;
+import org.mockito.MockedStatic;
+import org.springblade.core.log.exception.ServiceException;
+import org.springblade.system.cache.MpUserCache;
+import org.springblade.system.mapper.MpUserDataScopeMapper;
+import org.springblade.system.mapper.MpUserMapper;
+import org.springblade.system.pojo.entity.MpUser;
+import org.springblade.system.pojo.entity.MpUserDataScope;
+import org.springblade.system.pojo.vo.MpUserDataScopeVO;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.stream.Stream;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+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.ArgumentMatchers.anyBoolean;
+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;
+import static org.mockito.Mockito.when;
+
+/**
+ * 小程序用户身份与数据权限测试。
+ *
+ * 注入方式:被测类直接 new(唯一构造参数是 MpUserDataScopeMapper),
+ * {@code BaseServiceImpl.baseMapper} 用反射写入 MpUserMapper 的 mock,
+ * 与 blade-transport 的 DriverServiceImplTest 同一套路,不起 Spring 容器。
+ * {@code MpUserCache.clear()} 内部走 SpringUtil.getBean(CacheManager),无容器即抛空指针,
+ * 故每次调用前把该静态方法 mock 掉。
+ *
+ * 非法入参用例反射调用私有的 validate:updateUser 会先把手机号用库值回填、再补数据权限默认值,
+ * 空值走不到校验分支。
+ */
+class MpUserServiceImplTest {
+
+ private static final Long USER_ID = 7001L;
+ private static final Long SCOPE_DEPT_A = 1001L;
+ private static final Long SCOPE_DEPT_B = 1002L;
+ private static final String TENANT_ID = "000000";
+ private static final String PHONE = "15975348524";
+ private static final String DEPT_ID = "1123598817739906";
+
+ private MpUserMapper mpUserMapper;
+ private MpUserDataScopeMapper mpUserDataScopeMapper;
+ private MpUserServiceImpl service;
+
+ @BeforeAll
+ static void initTableInfo() {
+ // 构造 Lambda 条件需要 MyBatis-Plus 的实体元数据缓存
+ MybatisConfiguration configuration = new MybatisConfiguration();
+ MapperBuilderAssistant assistant = new MapperBuilderAssistant(configuration, "");
+ assistant.setCurrentNamespace("test");
+ TableInfoHelper.initTableInfo(assistant, MpUser.class);
+ TableInfoHelper.initTableInfo(assistant, MpUserDataScope.class);
+ }
+
+ @BeforeEach
+ void setUp() throws Exception {
+ mpUserMapper = mock(MpUserMapper.class);
+ mpUserDataScopeMapper = mock(MpUserDataScopeMapper.class);
+ service = new MpUserServiceImpl(mpUserDataScopeMapper);
+ inject("baseMapper", mpUserMapper);
+ }
+
+ // ------------------------------------------------------------ 账号组织不再必填
+
+ @ParameterizedTest(name = "{2}")
+ @MethodSource("identityCases")
+ @DisplayName("编辑小程序用户:司机 / 管理员 / 司机+管理员 × 有组织 / 无组织,六种组合全部放行")
+ void updateUserAcceptsEmptyDeptForEveryIdentity(String identities, String deptId, String label) {
+ when(mpUserMapper.selectById(USER_ID)).thenReturn(dbUser());
+ when(mpUserMapper.updateById(any(MpUser.class))).thenReturn(1);
+
+ MpUser user = new MpUser();
+ user.setId(USER_ID);
+ user.setTenantId(TENANT_ID);
+ user.setPhone(PHONE);
+ user.setIdentities(identities);
+ user.setDeptId(deptId);
+
+ assertTrue(updateUser(user), label + ":账号组织不是准入门槛");
+ }
+
+ static Stream identityCases() {
+ return Stream.of(
+ Arguments.of("driver", DEPT_ID, "司机 + 有组织"),
+ Arguments.of("driver", null, "司机 + 无组织"),
+ Arguments.of("admin", DEPT_ID, "管理员 + 有组织"),
+ Arguments.of("admin", null, "管理员 + 无组织"),
+ Arguments.of("driver,admin", DEPT_ID, "司机+管理员 + 有组织"),
+ Arguments.of("driver,admin", null, "司机+管理员 + 无组织"));
+ }
+
+ @Test
+ @DisplayName("validate 仍拦截:租户空、手机号空、身份空、数据权限范围与层级非法")
+ void validateStillRejectsMissingOrIllegalFields() {
+ assertValidateRejects(user -> user.setTenantId(null), "租户信息不能为空");
+ assertValidateRejects(user -> user.setPhone(null), "手机号不能为空");
+ assertValidateRejects(user -> user.setIdentities(null), "小程序身份不能为空");
+ assertValidateRejects(user -> user.setDataScopeRange(0), "数据权限范围不正确");
+ assertValidateRejects(user -> user.setDataScopeRange(4), "数据权限范围不正确");
+ assertValidateRejects(user -> user.setDataLevelRange(0), "数据层级范围不正确");
+ assertValidateRejects(user -> user.setDataLevelRange(3), "数据层级范围不正确");
+ }
+
+ // ------------------------------------------------------------ 只改人口字段不重置数据权限
+
+ @Test
+ @DisplayName("只改姓名:数据权限范围、层级与自定义组织全部不变,关联表一行都不动")
+ void updatePersonFieldsOnlyKeepsDataScope() {
+ when(mpUserMapper.selectById(USER_ID)).thenReturn(customScopeDbUser());
+ when(mpUserMapper.updateById(any(MpUser.class))).thenReturn(1);
+ when(mpUserDataScopeMapper.selectList(any())).thenReturn(scopes(SCOPE_DEPT_A, SCOPE_DEPT_B));
+
+ MpUser user = new MpUser();
+ user.setId(USER_ID);
+ user.setTenantId(TENANT_ID);
+ user.setName("张三");
+ user.setIdentities("admin");
+
+ assertTrue(updateUser(user));
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(MpUser.class);
+ verify(mpUserMapper).updateById(captor.capture());
+ assertEquals(3, captor.getValue().getDataScopeRange(), "未提供范围时沿用库里的 3-自定义");
+ assertEquals(2, captor.getValue().getDataLevelRange(), "未提供层级时沿用库里的 2-仅本级");
+ assertEquals(List.of(SCOPE_DEPT_A, SCOPE_DEPT_B), captor.getValue().getDataScopeDeptIds());
+ assertNull(captor.getValue().getDeptId(), "调用方不传组织时 updateById 忽略该列,不得写成空串");
+ verify(mpUserDataScopeMapper, never()).delete(any());
+ verify(mpUserDataScopeMapper, never()).insert(any(MpUserDataScope.class));
+ }
+
+ @Test
+ @DisplayName("显式传 range=2:范围改写为全部组织,自定义组织关联被清空")
+ void updateWithExplicitRangeRewritesScopes() {
+ when(mpUserMapper.selectById(USER_ID)).thenReturn(customScopeDbUser());
+ when(mpUserMapper.updateById(any(MpUser.class))).thenReturn(1);
+
+ MpUser user = new MpUser();
+ user.setId(USER_ID);
+ user.setTenantId(TENANT_ID);
+ user.setIdentities("admin");
+ user.setDataScopeRange(2);
+
+ assertTrue(updateUser(user));
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(MpUser.class);
+ verify(mpUserMapper).updateById(captor.capture());
+ assertEquals(2, captor.getValue().getDataScopeRange());
+ assertEquals(2, captor.getValue().getDataLevelRange(), "未提供层级时沿用库值");
+ assertTrue(captor.getValue().getDataScopeDeptIds().isEmpty(), "range != 3 时关联列表被清空");
+ verify(mpUserDataScopeMapper).delete(any());
+ verify(mpUserDataScopeMapper, never()).insert(any(MpUserDataScope.class));
+ }
+
+ @Test
+ @DisplayName("显式传 level=1:层级改写为仅本级,范围与自定义组织保持库值,关联表一行都不动")
+ void updateWithExplicitLevelRewritesLevelOnly() {
+ when(mpUserMapper.selectById(USER_ID)).thenReturn(customScopeDbUser());
+ when(mpUserMapper.updateById(any(MpUser.class))).thenReturn(1);
+ when(mpUserDataScopeMapper.selectList(any())).thenReturn(scopes(SCOPE_DEPT_A, SCOPE_DEPT_B));
+
+ MpUser user = new MpUser();
+ user.setId(USER_ID);
+ user.setTenantId(TENANT_ID);
+ user.setIdentities("admin");
+ user.setDataLevelRange(1);
+
+ assertTrue(updateUser(user));
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(MpUser.class);
+ verify(mpUserMapper).updateById(captor.capture());
+ assertEquals(1, captor.getValue().getDataLevelRange(), "显式传的层级要落库");
+ assertEquals(3, captor.getValue().getDataScopeRange(), "未传范围时沿用库里的 3-自定义");
+ assertEquals(List.of(SCOPE_DEPT_A, SCOPE_DEPT_B), captor.getValue().getDataScopeDeptIds());
+ verify(mpUserDataScopeMapper, never()).delete(any());
+ verify(mpUserDataScopeMapper, never()).insert(any(MpUserDataScope.class));
+ }
+
+ @Test
+ @DisplayName("只显式传自定义组织列表:关联按新列表重建,范围沿用库里的 3-自定义")
+ void updateWithExplicitDeptIdsRebuildsScopes() {
+ when(mpUserMapper.selectById(USER_ID)).thenReturn(customScopeDbUser());
+ when(mpUserMapper.updateById(any(MpUser.class))).thenReturn(1);
+
+ MpUser user = new MpUser();
+ user.setId(USER_ID);
+ user.setTenantId(TENANT_ID);
+ user.setIdentities("admin");
+ user.setDataScopeDeptIds(List.of(SCOPE_DEPT_A));
+
+ assertTrue(updateUser(user));
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(MpUser.class);
+ verify(mpUserMapper).updateById(captor.capture());
+ assertEquals(3, captor.getValue().getDataScopeRange(), "未提供范围时沿用库值");
+ assertEquals(List.of(SCOPE_DEPT_A), captor.getValue().getDataScopeDeptIds());
+ verify(mpUserDataScopeMapper).delete(any());
+ verify(mpUserDataScopeMapper).insert(any(MpUserDataScope.class));
+ }
+
+ @Test
+ @DisplayName("新建小程序用户:仍按提交的 range=3 重建自定义组织关联")
+ void submitStillRebuildsScopes() {
+ when(mpUserMapper.selectOne(any(), anyBoolean())).thenReturn(null);
+ when(mpUserMapper.insert(any(MpUser.class))).thenAnswer(invocation -> {
+ invocation.getArgument(0, MpUser.class).setId(USER_ID);
+ return 1;
+ });
+
+ MpUser user = new MpUser();
+ user.setTenantId(TENANT_ID);
+ user.setPhone(PHONE);
+ user.setIdentities("admin");
+ user.setDataScopeRange(3);
+ user.setDataScopeDeptIds(List.of(SCOPE_DEPT_A, SCOPE_DEPT_B));
+
+ try (MockedStatic ignored = mockStatic(MpUserCache.class)) {
+ assertTrue(service.submit(user));
+ }
+ verify(mpUserDataScopeMapper).delete(any());
+ verify(mpUserDataScopeMapper, times(2)).insert(any(MpUserDataScope.class));
+ }
+
+ // ------------------------------------------------------------ 注册路径不要求所属组织
+
+ @ParameterizedTest(name = "{1}")
+ @MethodSource("emptyDeptRegistrationCases")
+ @DisplayName("注册:司机 / 管理员 / 司机+管理员不选所属组织也能建号")
+ void submitAcceptsEmptyDeptForEveryIdentity(String identities, String label) {
+ when(mpUserMapper.selectOne(any(), anyBoolean())).thenReturn(null);
+ when(mpUserMapper.insert(any(MpUser.class))).thenAnswer(invocation -> {
+ invocation.getArgument(0, MpUser.class).setId(USER_ID);
+ return 1;
+ });
+
+ MpUser user = new MpUser();
+ user.setTenantId(TENANT_ID);
+ user.setPhone(PHONE);
+ user.setIdentities(identities);
+
+ try (MockedStatic ignored = mockStatic(MpUserCache.class)) {
+ assertTrue(service.submit(user), label + ":所属组织不是注册的准入门槛");
+ }
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(MpUser.class);
+ verify(mpUserMapper).insert(captor.capture());
+ assertNull(captor.getValue().getDeptId(), "未选组织时组织列保持 null,不写空串");
+ assertEquals(1, captor.getValue().getDataScopeRange(), "未选组织的账号按默认 1-仅所属组织落库");
+ }
+
+ static Stream emptyDeptRegistrationCases() {
+ return Stream.of(
+ Arguments.of("driver", "司机"),
+ Arguments.of("admin", "管理员"),
+ Arguments.of("driver,admin", "司机+管理员"));
+ }
+
+ // ------------------------------------------------------------ 空组织的解析结果是空集
+
+ @ParameterizedTest(name = "{2}")
+ @MethodSource("emptyScopeCases")
+ @DisplayName("数据权限解析:基础组织为空时得到空集,不退化成全部组织")
+ void resolveDataScopeIsEmptyWhenBaseDeptsEmpty(Integer dataScopeRange, Integer dataLevelRange, String label) {
+ MpUser user = dbUser();
+ user.setDeptId(null);
+ user.setDataScopeRange(dataScopeRange);
+ user.setDataLevelRange(dataLevelRange);
+ when(mpUserMapper.selectById(USER_ID)).thenReturn(user);
+ when(mpUserDataScopeMapper.selectList(any())).thenReturn(List.of());
+
+ MpUserDataScopeVO scope = service.resolveDataScope(USER_ID);
+
+ assertFalse(scope.isAllDept(), label + ":空集不是全部组织,allDept=true 表示不按组织过滤");
+ assertTrue(scope.getDeptIds().isEmpty());
+ }
+
+ static Stream emptyScopeCases() {
+ return Stream.of(
+ Arguments.of(null, null, "范围与层级都未设置"),
+ Arguments.of(1, 1, "1-仅所属组织 + 1-含下级,但没有组织"),
+ Arguments.of(3, 2, "3-自定义,但一条组织关联都没有"));
+ }
+
+ @Test
+ @DisplayName("数据权限解析:只有范围是 2-全部组织时才返回全部")
+ void resolveDataScopeIsAllOnlyForAllScope() {
+ MpUser user = dbUser();
+ user.setDeptId(null);
+ user.setDataScopeRange(2);
+ when(mpUserMapper.selectById(USER_ID)).thenReturn(user);
+
+ MpUserDataScopeVO scope = service.resolveDataScope(USER_ID);
+
+ assertTrue(scope.isAllDept());
+ }
+
+ // ------------------------------------------------------------ 夹具
+
+ /**
+ * updateUser 成功后会清缓存;缓存实现依赖 Spring 容器,无容器时静态 mock 掉
+ */
+ private boolean updateUser(MpUser user) {
+ try (MockedStatic ignored = mockStatic(MpUserCache.class)) {
+ return service.updateUser(user);
+ }
+ }
+
+ private MpUser dbUser() {
+ MpUser user = new MpUser();
+ user.setId(USER_ID);
+ user.setTenantId(TENANT_ID);
+ user.setPhone(PHONE);
+ user.setAccount(PHONE);
+ user.setName("张三");
+ user.setIdentities("driver");
+ user.setDeptId(DEPT_ID);
+ user.setDataScopeRange(1);
+ user.setDataLevelRange(1);
+ return user;
+ }
+
+ /**
+ * range=3(自定义)+ 仅本级,自定义组织由 mpUserDataScopeMapper.selectList 提供
+ */
+ private MpUser customScopeDbUser() {
+ MpUser user = dbUser();
+ user.setIdentities("admin");
+ user.setDataScopeRange(3);
+ user.setDataLevelRange(2);
+ return user;
+ }
+
+ private List scopes(Long... deptIds) {
+ return Stream.of(deptIds).map(deptId -> {
+ MpUserDataScope scope = new MpUserDataScope();
+ scope.setUserId(USER_ID);
+ scope.setDeptId(deptId);
+ return scope;
+ }).toList();
+ }
+
+ private MpUser validUser() {
+ MpUser user = new MpUser();
+ user.setId(USER_ID);
+ user.setTenantId(TENANT_ID);
+ user.setPhone(PHONE);
+ user.setIdentities("driver");
+ user.setDataScopeRange(1);
+ user.setDataLevelRange(1);
+ return user;
+ }
+
+ private void assertValidateRejects(Consumer mutation, String expectedMessage) {
+ MpUser user = validUser();
+ mutation.accept(user);
+ ServiceException exception = assertThrows(ServiceException.class, () -> invokeValidate(user));
+ assertEquals(expectedMessage, exception.getMessage());
+ }
+
+ private void invokeValidate(MpUser user) throws Exception {
+ Method validate = MpUserServiceImpl.class.getDeclaredMethod("validate", MpUser.class);
+ validate.setAccessible(true);
+ try {
+ validate.invoke(service, user);
+ } catch (InvocationTargetException exception) {
+ if (exception.getCause() instanceof RuntimeException runtimeException) {
+ throw runtimeException;
+ }
+ throw exception;
+ }
+ }
+
+ private void inject(String field, Object value) throws Exception {
+ Class> type = service.getClass();
+ while (type != null) {
+ try {
+ Field declared = type.getDeclaredField(field);
+ declared.setAccessible(true);
+ declared.set(service, value);
+ return;
+ } catch (NoSuchFieldException ignored) {
+ type = type.getSuperclass();
+ }
+ }
+ throw new IllegalStateException("未找到字段:" + field);
+ }
+
+}
diff --git a/blade-service/blade-transport/pom.xml b/blade-service/blade-transport/pom.xml
index 7f7e843b..02e9bab8 100644
--- a/blade-service/blade-transport/pom.xml
+++ b/blade-service/blade-transport/pom.xml
@@ -55,6 +55,9 @@
org.springframework.boot
spring-boot-starter-amqp
+
org.springframework.boot
spring-boot-starter-test
@@ -64,15 +67,6 @@
io.minio
minio
-
-
-
- org.springframework.boot
- spring-boot-starter-test
- test
-
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/TransportBusiConstant.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/TransportBusiConstant.java
index f27ce70d..93f278b8 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/TransportBusiConstant.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/TransportBusiConstant.java
@@ -61,6 +61,14 @@ public final class TransportBusiConstant {
/** 审批状态:已作废(终态)。 */
public static final String VOIDED = "voided";
+ /**
+ * 「未分配组织」筛选项的哨兵值,不是真实组织名。
+ *
+ * 前端 organization-search 混入、DriverMapper.xml 与 TransportVehicleMapper.xml 的
+ * organizationName 分支、以及两个导出查询都按这个字面量判定,四处必须同步修改。
+ */
+ public static final String UNASSIGNED_ORG_FILTER = "__UNASSIGNED_ORG__";
+
private TransportBusiConstant() {
}
}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/DriverController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/DriverController.java
index 89f336bc..e409446d 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/DriverController.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/DriverController.java
@@ -43,6 +43,7 @@ import org.springblade.core.secure.annotation.PreAuth;
import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.DateUtil;
import org.springblade.core.tool.utils.Func;
+import org.springblade.transport.constant.TransportBusiConstant;
import org.springblade.transport.excel.DriverExcel;
import org.springblade.transport.pojo.entity.Driver;
import org.springblade.transport.pojo.vo.DriverExpiryStatVO;
@@ -233,7 +234,11 @@ public class DriverController extends BladeController {
if (Func.isNotEmpty(driver.getDriverType())) {
queryWrapper.eq(Driver::getDriverType, driver.getDriverType());
}
- if (Func.isNotEmpty(driver.getOrganizationName())) {
+ // 「未分配组织」哨兵改判组织为空串:组织列 NOT NULL,无组织存空串。
+ // 哨兵字面量由 TransportBusiConstant.UNASSIGNED_ORG_FILTER 定义,前端与两个 Mapper XML 同步。
+ if (TransportBusiConstant.UNASSIGNED_ORG_FILTER.equals(driver.getOrganizationName())) {
+ queryWrapper.eq(Driver::getOrganizationName, "");
+ } else if (Func.isNotEmpty(driver.getOrganizationName())) {
queryWrapper.like(Driver::getOrganizationName, driver.getOrganizationName());
}
if (Func.isNotEmpty(driver.getStatus())) {
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/DriverWaybillController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/DriverWaybillController.java
index 313db1da..06eaf200 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/DriverWaybillController.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/DriverWaybillController.java
@@ -107,7 +107,7 @@ public class DriverWaybillController extends BladeController {
@GetMapping("/task/current")
@ApiOperationSupport(order = 21)
- @Operation(summary = "首页:当前运输中运次", description = "当前用户手机号可见的运输中运次(有配载单的一趟车算一个)最新一条")
+ @Operation(summary = "首页:当前运输中运次", description = "当前用户手机号可见的运输中或待确认(待回单确认)运次(有配载单的一趟车算一个)最新一条")
public R tripCurrentTask() {
return R.data(driverWaybillService.tripCurrentTask());
}
@@ -122,18 +122,18 @@ public class DriverWaybillController extends BladeController {
@GetMapping("/task/counts")
@ApiOperationSupport(order = 23)
- @Operation(summary = "运次 Tab 统计", description = "按运次统计:全部 = 待接单 + 进行中 + 已完成(不含已取消)")
+ @Operation(summary = "运次 Tab 统计", description = "按运次统计:全部 = 待接单 + 进行中 + 待确认 + 已完成(不含已取消);confirm 为待确认(待回单确认)")
public R tripCounts() {
return R.data(driverWaybillService.tripTabCounts());
}
@GetMapping("/task/page")
@ApiOperationSupport(order = 24)
- @Operation(summary = "运次分页列表", description = "一页即若干个运次;status:空=全部,0待接单,1运输中,2已完成;keyword 支持运单号/配载单号/项目/司机/车牌/起终点/地址")
+ @Operation(summary = "运次分页列表", description = "一页即若干个运次;status:空=全部,0待接单,1运输中(不含待回单确认),2已完成,4待确认(待回单确认);keyword 支持运单号/配载单号/项目/司机/车牌/起终点/地址")
public R> tripPage(
@Parameter(description = "当前页") @RequestParam(required = false) Integer current,
@Parameter(description = "每页运次数") @RequestParam(required = false) Integer size,
- @Parameter(description = "状态:0待接单/1运输中/2已完成,不传为全部") @RequestParam(required = false) String status,
+ @Parameter(description = "状态:0待接单/1运输中/2已完成/4待确认,不传为全部") @RequestParam(required = false) String status,
@Parameter(description = "关键字:运单号/配载单号/项目/司机/车牌/起终点/地址") @RequestParam(required = false) String keyword) {
Integer statusCode = parseStatus(status);
return R.data(driverWaybillService.tripPage(current, size, statusCode, keyword));
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ManageWaybillController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ManageWaybillController.java
index 81265fc4..ccab5d51 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ManageWaybillController.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ManageWaybillController.java
@@ -38,6 +38,7 @@ import org.springblade.transport.pojo.vo.AdminHomeVO;
import org.springblade.transport.pojo.vo.AdminVehicleOptionVO;
import org.springblade.transport.pojo.vo.AdminWaybillCardVO;
import org.springblade.transport.pojo.vo.AdminWaybillDetailVO;
+import org.springblade.transport.pojo.vo.WaybillLocateVO;
import org.springblade.transport.pojo.vo.WaybillPunchRecordsVO;
import org.springblade.transport.pojo.vo.WaybillTrackVO;
import org.springblade.transport.service.IManageWaybillService;
@@ -84,12 +85,12 @@ public class ManageWaybillController extends BladeController {
@GetMapping("/list")
@ApiOperationSupport(order = 3)
- @Operation(summary = "运单分页列表", description = "当前组织运单;status:0待接单/1运输中/2已完成;exception:exception/normal;transportType:common/load")
+ @Operation(summary = "运单分页列表", description = "当前组织运单;status:0待接单/1运输中(不含待回单确认)/2已完成/4待确认(待回单确认);exception:exception/normal;transportType:common/load")
public R> list(
@Parameter(description = "当前页") @RequestParam(required = false) Integer current,
@Parameter(description = "每页条数") @RequestParam(required = false) Integer size,
@Parameter(description = "关键字:运单号/配载单号/项目/司机/车牌/起终点/地址") @RequestParam(required = false) String keyword,
- @Parameter(description = "状态:0待接单/1运输中/2已完成,不传为全部") @RequestParam(required = false) String status,
+ @Parameter(description = "状态:0待接单/1运输中/2已完成/4待确认,不传为全部") @RequestParam(required = false) String status,
@Parameter(description = "异常:exception有异常/normal无异常") @RequestParam(required = false) String exception,
@Parameter(description = "运输组织:common普通/load配载") @RequestParam(required = false) String transportType,
@Parameter(description = "创建日起 YYYY-MM-DD") @RequestParam(required = false) String startDate,
@@ -108,7 +109,7 @@ public class ManageWaybillController extends BladeController {
@GetMapping("/pending")
@ApiOperationSupport(order = 5)
- @Operation(summary = "待处理运单", description = "待接单/运输中;needReassign=true 仅司机已拒单")
+ @Operation(summary = "待处理运单", description = "待接单/运输中/待确认;needReassign=true 仅司机已拒单")
public R> pending(
@Parameter(description = "当前页") @RequestParam(required = false) Integer current,
@Parameter(description = "每页条数") @RequestParam(required = false) Integer size,
@@ -170,4 +171,12 @@ public class ManageWaybillController extends BladeController {
return R.data(waybillService.trackVehicle(id, startDate, endDate));
}
+ @PostMapping("/locate")
+ @ApiOperationSupport(order = 12)
+ @Operation(summary = "车辆实时定位", description = "小程序调度端:仅需登录态,按运单绑定车牌查询 LBS 最新定位")
+ public R locate(
+ @Parameter(description = "运单ID", required = true) @RequestParam Long id) {
+ return R.data(waybillService.locateVehicle(id));
+ }
+
}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/TransportVehicleController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/TransportVehicleController.java
index 37596eb4..a321bdc3 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/TransportVehicleController.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/TransportVehicleController.java
@@ -44,6 +44,7 @@ import org.springblade.core.secure.annotation.PreAuth;
import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.DateUtil;
import org.springblade.core.tool.utils.Func;
+import org.springblade.transport.constant.TransportBusiConstant;
import org.springblade.transport.excel.TransportVehicleExcel;
import org.springblade.transport.pojo.entity.TransportVehicle;
import org.springblade.transport.pojo.vo.TransportVehicleExpiryStatVO;
@@ -226,7 +227,11 @@ public class TransportVehicleController extends BladeController {
if (Func.isNotEmpty(ids)) {
queryWrapper.in(TransportVehicle::getId, Func.toLongList(ids));
}
- if (Func.isNotEmpty(vehicle.getOrganizationName())) {
+ // 「未分配组织」哨兵改判组织为空串:组织列 NOT NULL,无组织存空串。
+ // 哨兵字面量由 TransportBusiConstant.UNASSIGNED_ORG_FILTER 定义,前端与两个 Mapper XML 同步。
+ if (TransportBusiConstant.UNASSIGNED_ORG_FILTER.equals(vehicle.getOrganizationName())) {
+ queryWrapper.eq(TransportVehicle::getOrganizationName, "");
+ } else if (Func.isNotEmpty(vehicle.getOrganizationName())) {
queryWrapper.like(TransportVehicle::getOrganizationName, vehicle.getOrganizationName());
}
if (Func.isNotEmpty(vehicle.getPlateNo())) {
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/DriverMapper.xml b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/DriverMapper.xml
index 89e87067..2f1b5e2a 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/DriverMapper.xml
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/DriverMapper.xml
@@ -170,8 +170,18 @@
AND driver_type = #{driver.driverType}
-
- AND organization_name LIKE #{organizationNameLike}
+
+
+
+ AND organization_name = ''
+
+
+
+ AND organization_name LIKE #{organizationNameLike}
+
+
AND status = #{driver.status}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/TransportVehicleMapper.xml b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/TransportVehicleMapper.xml
index 0faafb78..0d5857fd 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/TransportVehicleMapper.xml
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/TransportVehicleMapper.xml
@@ -117,8 +117,18 @@
is_deleted = 0
-
- AND organization_name LIKE #{organizationNameLike}
+
+
+
+ AND organization_name = ''
+
+
+
+ AND organization_name LIKE #{organizationNameLike}
+
+
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IReceivablePayableDetailService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IReceivablePayableDetailService.java
index 82592347..c472b14e 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IReceivablePayableDetailService.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IReceivablePayableDetailService.java
@@ -84,12 +84,29 @@ public interface IReceivablePayableDetailService extends BaseService waybillIds);
+ /**
+ * 同上,但按 {@code mileageOverrides} 覆盖运单里程后再算。
+ *
+ * 生成走独立事务(REQUIRES_NEW),读不到调用方事务里尚未提交的里程更新,
+ * 由调用方把本次刚写入的里程显式传进来,否则按里程计费会读到旧值或 null(算成 0 元)。
+ */
+ void generateForCompletedWaybills(List waybillIds, Map mileageOverrides);
+
/**
* 运单维护里程后,同步未挂结算单的应收应付明细里程;
* 对按里程、按吨·公里计费的费用行按新里程重新计算。
*/
void syncMileageFromWaybill(Long waybillId, BigDecimal mileage);
+ /**
+ * 运单的计费规则是否包含里程:客户合同(应收)与承运商合同(应付)任一侧命中里程类计费要素即为真。
+ *
+ * 判据与费用生成同源(同一份方案解析与规则命中),只读、不产生任何写操作;
+ * 无合同、方案不匹配、规则不命中一律返回 false。
+ *
+ */
+ boolean requiresMileage(Waybill waybill);
+
/**
* 批量导入完成运单后按合同系统计费模式生成应收、应付明细。
* 与导入事务共用同一事务,运单尚未提交,因此直接传入实体而非主键。
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IWaybillService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IWaybillService.java
index e87bc79a..53e50ad5 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IWaybillService.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IWaybillService.java
@@ -35,6 +35,7 @@ import org.springblade.transport.pojo.vo.WaybillVO;
import org.springblade.transport.pojo.dto.WaybillMileageRequest;
import org.springblade.transport.pojo.dto.WaybillPunchRecordSaveRequest;
+import java.math.BigDecimal;
import java.util.List;
/**
@@ -97,6 +98,16 @@ public interface IWaybillService extends BaseService {
WaybillVO copy(Long id);
boolean changeRoute(Waybill waybill);
boolean maintainMileage(WaybillMileageRequest request);
+
+ /**
+ * 只写运单里程(定向更新),用于打卡链路采集里程。
+ *
+ * 不做 maintainMileage 的「仅已完成 / 未生成结算单」前置——末位打卡发生在完成之前;
+ * 结算状态由调用方按各自入口的规则判定,也不触发费用重算,由调用方决定是否同步。
+ *
+ */
+ void savePunchMileage(Long waybillId, BigDecimal mileage);
+
boolean cancel(Long id);
boolean reassign(Waybill waybill);
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverCertServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverCertServiceImpl.java
index ca7ef54e..703119df 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverCertServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverCertServiceImpl.java
@@ -123,21 +123,6 @@ public class DriverCertServiceImpl implements IDriverCertService {
}
}
- /**
- * 建档前置条件:车辆行的 {@code organization_name} 是 NOT NULL 且无默认值,缺了这条 INSERT
- * 直接报「Field 'organization_name' doesn't have a default value」,用户看到的是一串英文异常。
- *
- * 只在**新建车辆**时要求:重新认证不会写这一列(它由管理端维护,属于内部管理归属),
- * 拿它拦住重新认证只会让司机无从下手。
- *
- * @param driver 当前司机
- */
- private void requireOrganizationForNewVehicle(Driver driver) {
- if (Func.isEmpty(driver.getOrganizationName())) {
- throw new ServiceException("司机档案缺少所属组织,请联系管理员补充后再提交");
- }
- }
-
/**
* 文本必填 + 长度校验
*
@@ -474,15 +459,13 @@ public class DriverCertServiceImpl implements IDriverCertService {
*/
private TransportVehicle createVehicleForDriver(Driver driver, VehicleCertSubmitRequest request,
String normalizedPlate) {
- // 内容校验已在 submitVehicleCert 里统一做过;这里只剩「这条 INSERT 需要什么」的前置条件
- requireOrganizationForNewVehicle(driver);
-
+ // 内容校验已在 submitVehicleCert 里统一做过
TransportVehicle vehicle = new TransportVehicle();
vehicle.setPlateNo(normalizedPlate);
- // 所属组织取司机档案:车辆表的该列 NOT NULL 且无默认值,不填这条 INSERT 直接报
- // 「Field 'organization_name' doesn't have a default value」(严格模式下必错)。
- // 车辆归到司机所属组织名下,管理端按组织查看时才看得到这台车。
- vehicle.setOrganizationName(driver.getOrganizationName());
+ // 所属组织是建档时对司机档案取的一次**快照**,不是活关联:司机之后补上组织也不回填已建的车辆。
+ // 司机档案没有组织时写空串——该列 NOT NULL 且无默认值,落 null 这条 INSERT 直接报
+ // 「Field 'organization_name' doesn't have a default value」。
+ vehicle.setOrganizationName(trimToEmpty(driver.getOrganizationName()));
vehicle.setOwnerName(request.getOwnerName());
vehicle.setPlateColor(request.getPlateColor());
vehicle.setVehicleType(request.getVehicleType());
@@ -571,4 +554,8 @@ public class DriverCertServiceImpl implements IDriverCertService {
return driver;
}
+ private String trimToEmpty(String value) {
+ return value == null ? "" : value.trim();
+ }
+
}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverServiceImpl.java
index 41964813..423bafec 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverServiceImpl.java
@@ -383,9 +383,6 @@ public class DriverServiceImpl extends BaseServiceImpl imp
if (Func.isEmpty(driver.getMobile())) {
throw new ServiceException("手机号不能为空");
}
- if (Func.isEmpty(driver.getOrganizationName())) {
- throw new ServiceException("所属组织不能为空");
- }
if (Func.isEmpty(driver.getEmergencyContactName())) {
throw new ServiceException("紧急联系人姓名不能为空");
}
@@ -475,11 +472,17 @@ public class DriverServiceImpl extends BaseServiceImpl imp
}
}
- // 部门可选:匹配到则写入小程序用户,匹配不到不阻断同步
+ // 按「组织名称」反查组织树 id:组织改名后这里必然落空——不报错、不掉数据,
+ // 只是该司机账号的权限不再随组织变化(新建账号落空时数据权限为空集,管理端看不到)。
+ // 组织为空时不做反查:DeptServiceImpl 会把空名称拆成空集合,MyBatis-Plus 的 inExpression
+ // 对空集合拼出 dept_name IN () 非法子句。此时 deptId 保持 null 而不是空串:updateUser 的
+ // updateById 忽略 null,账号原有的组织与数据权限得以保留;写成空串会把它们覆盖掉。
String deptId = null;
- R deptResult = sysClient.getDeptIds(tenantId, driver.getOrganizationName());
- if (deptResult != null && R.isSuccess(deptResult) && Func.isNotEmpty(deptResult.getData())) {
- deptId = deptResult.getData();
+ if (Func.isNotEmpty(driver.getOrganizationName())) {
+ R deptResult = sysClient.getDeptIds(tenantId, driver.getOrganizationName());
+ if (deptResult != null && R.isSuccess(deptResult) && Func.isNotEmpty(deptResult.getData())) {
+ deptId = deptResult.getData();
+ }
}
MpUser existing = findExistingMpUser(driver, tenantId, mobile);
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverWaybillServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverWaybillServiceImpl.java
index 428c4398..25b6bd4a 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverWaybillServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/DriverWaybillServiceImpl.java
@@ -52,6 +52,7 @@ import org.springblade.transport.pojo.entity.WaybillNodePunch;
import org.springblade.transport.service.ITransportVehicleService;
import org.springblade.transport.support.CertificationGate;
import org.springblade.transport.support.LiveProcessConfigSupport;
+import org.springblade.transport.support.TransportBusinessSupport;
import org.springblade.transport.support.VehiclePlateSupport;
import org.springblade.transport.support.WaybillCargoSupport;
import org.springblade.transport.pojo.vo.DriverEnrouteRecordVO;
@@ -70,6 +71,7 @@ import org.springblade.transport.pojo.vo.WaybillTrackVO;
import org.springblade.transport.service.IDriverService;
import org.springblade.transport.service.IDriverWaybillService;
import org.springblade.transport.service.IProcessConfigService;
+import org.springblade.transport.service.IReceivablePayableDetailService;
import org.springblade.transport.service.ITripQueryService;
import org.springblade.transport.service.IWaybillService;
import org.springblade.transport.support.WaybillProcessSupport;
@@ -147,6 +149,9 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
private static final int TRIP_STATUS_PENDING = 0;
private static final int TRIP_STATUS_RUNNING = 1;
+ /** 运次列表档位:4 待确认(待回单确认)。卡片状态码仍按 1 展示,只有列表 Tab 用它分档 */
+ private static final int TRIP_STATUS_PENDING_RETURN_CONFIRM = 4;
+
private final IWaybillService waybillService;
private final IDriverService driverService;
private final IMpUserClient mpUserClient;
@@ -157,6 +162,7 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
private final WaybillNodePunchMapper nodePunchMapper;
private final ExceptionDisposalMapper exceptionDisposalMapper;
private final ITripQueryService tripQueryService;
+ private final IReceivablePayableDetailService receivablePayableDetailService;
@Override
public DriverWaybillCardVO currentTask() {
@@ -277,7 +283,8 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
return null;
}
TripQueryVO query = TripQueryVO.forDriver(phone);
- query.setStatus(TRIP_STATUS_RUNNING);
+ // 待确认(待回单确认)的趟车司机仍需回到卡片上继续操作,与进行中并列作为当前任务
+ query.setStatuses(List.of(TRIP_STATUS_RUNNING, TRIP_STATUS_PENDING_RETURN_CONFIRM));
query.setCurrent(1);
query.setSize(1);
IPage page = tripQueryService.page(query);
@@ -313,6 +320,7 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
TripTabCountsVO tripCounts = tripQueryService.tabCounts(TripQueryVO.forDriver(phone));
counts.setPending(tripCounts.getPending());
counts.setDoing(tripCounts.getDoing());
+ counts.setConfirm(tripCounts.getConfirm());
counts.setDone(tripCounts.getDone());
counts.setAll(tripCounts.getAll());
return counts;
@@ -497,6 +505,16 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
: legacyNodePunchPhotos(dto.getPhotos(), WaybillProcessSupport.nodeStringList(nodeCfg, "voucherTypes"));
assertNodePunchPhotos(photoItems, nodeCfg, typedPhotoItems);
+ // 里程:末位可打卡节点 + 运单计费规则含里程(应收或应付任一侧)时必填。
+ // 必须在完成判定之前落库——应收应付在本次打卡触发的完成里生成,读到的得是新里程。
+ String submitNodeKey = WaybillProcessSupport.nodeKey(nodeCfg);
+ if (WaybillProcessSupport.isLastDriverPunchNode(processJson, submitNodeKey)
+ && receivablePayableDetailService.requiresMileage(waybill)) {
+ BigDecimal mileage = TransportBusinessSupport.requireMileage(dto.getMileage());
+ waybillService.savePunchMileage(waybill.getId(), mileage);
+ waybill.setMileage(mileage);
+ }
+
Date now = new Date();
WaybillNodePunch punch = new WaybillNodePunch();
punch.setWaybillId(waybill.getId());
@@ -1564,6 +1582,8 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
Map latestNodePunchByCode = latestNodePunchMap(waybill.getId());
// 未开始运输的运单在途卡仍下发,但不可提交:司机要看得到这张卡,才知道为什么不能打
String notStartedReason = WaybillProcessSupport.punchNotStartedReason(waybill.getBusinessStatus());
+ // 计费规则是否含里程与节点无关,整条运单算一次;放在循环外避免逐节点回查合同。
+ boolean lastNodeNeedsMileage = receivablePayableDetailService.requiresMileage(waybill);
for (Map cfg : punchConfigs) {
boolean isTransit = WaybillProcessSupport.isTransitNodePublic(cfg);
@@ -1622,9 +1642,15 @@ public class DriverWaybillServiceImpl implements IDriverWaybillService {
// 末位卡上打一下就完成这张运单:判据与下方 submitNode 的自动完成同一份
// (末位可打卡节点 + 到货/卸货/签收 + 回单不要求业务员确认),客户端只按它把按钮文案改成「完成运单」。
// 两处判据必须同源,否则会出现「按钮写着完成运单、打下去这张运单没完成」。
- vo.setFinishOnPunch(WaybillProcessSupport.isLastDriverPunchNode(processJson, vo.getKey())
+ boolean lastPunchNode = WaybillProcessSupport.isLastDriverPunchNode(processJson, vo.getKey());
+ vo.setFinishOnPunch(lastPunchNode
&& WaybillProcessSupport.isDeliveryNode(vo.getKey(), vo.getName())
&& WaybillProcessSupport.returnNeedsNoConfirmation(processJson));
+ // 里程同样只在末位卡采集:末位判定与上面同一个变量,避免两处各算一次走偏。
+ if (lastPunchNode && lastNodeNeedsMileage) {
+ vo.setNeedMileage(true);
+ vo.setMileage(waybill.getMileage());
+ }
if (done) {
if (punched.getPunchTime() != null) {
LocalDateTime ldt = punched.getPunchTime().toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ManageWaybillServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ManageWaybillServiceImpl.java
index 73fbe0f4..26684dad 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ManageWaybillServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ManageWaybillServiceImpl.java
@@ -236,8 +236,8 @@ public class ManageWaybillServiceImpl implements IManageWaybillService {
TripQueryVO query = TripQueryVO.forAdmin();
applyAdminDataScope(query);
query.setKeyword(keyword);
- // 待处理只要待接单与进行中两档;「未拒单」也是交给运次读服务按子单行筛
- query.setStatuses(List.of(0, 1));
+ // 待处理含待接单、进行中与待确认(待回单确认原本就算进行中,拆档后不能从待处理里消失)
+ query.setStatuses(List.of(0, 1, 4));
query.setExcludeRejectedTrips(Boolean.FALSE.equals(needReassign));
query.setCurrent(pageNo);
query.setSize(pageSize);
@@ -269,6 +269,19 @@ public class ManageWaybillServiceImpl implements IManageWaybillService {
return page;
}
+ /**
+ * 卡片展示用的原始业务状态:整趟车子单同一业务状态时取它,混着多种状态时为空,卡片按状态码展示。
+ *
+ * 待回单确认的趟车状态码仍是 1(运输中),只有这个字段能把它区分开。
+ */
+ private String uniqueChildBusinessStatus(List children) {
+ Set statuses = children.stream()
+ .map(TripVO.TripWaybillVO::getBusinessStatus)
+ .filter(Func::isNotBlank)
+ .collect(Collectors.toCollection(LinkedHashSet::new));
+ return statuses.size() == 1 ? statuses.iterator().next() : null;
+ }
+
/**
* 运次卡片:展示单号、整趟车起/经/终与站点数、货物汇总、计划时间都取运次字段。
* 旧字段 id / waybillNo 保留运单语义(未配载就是这张运单,配载取第一张可见子运单),
@@ -302,6 +315,7 @@ public class ManageWaybillServiceImpl implements IManageWaybillService {
card.setPlanTime(formatLocalDate(trip.getEstimatedStartDate()));
card.setPlanTimeEnd(formatLocalDate(trip.getEstimatedEndDate()));
card.setStatus(trip.getStatus());
+ card.setBusinessStatus(uniqueChildBusinessStatus(children));
card.setDriverName(Func.toStr(trip.getDriverName(), ""));
card.setVehicleNo(Func.toStr(trip.getVehicleNo(), ""));
card.setCreateTime(formatDateTime(trip.getSortTime()));
@@ -336,7 +350,11 @@ public class ManageWaybillServiceImpl implements IManageWaybillService {
return childIds.stream().map(this::parseId).filter(Objects::nonNull).anyMatch(exceptionWaybillIds::contains);
}
- /** 小程序 status → 运次状态:0待接单 / 1进行中 / 2已完成 */
+ /**
+ * 小程序 status → 运次可筛选档位:0待接单 / 1进行中 / 2已完成 / 3已取消 / 4待确认(待回单确认)。
+ *
+ * 1 不含待回单确认——待回单确认单独成档,卡片状态码仍是 1。
+ */
private Integer toTripStatus(String status) {
if (Func.isBlank(status)) {
return null;
@@ -346,6 +364,7 @@ public class ManageWaybillServiceImpl implements IManageWaybillService {
case "1", STATUS_RUNNING, "transporting", "doing" -> 1;
case "2", STATUS_COMPLETED, "done" -> 2;
case "3", STATUS_CANCELLED -> 3;
+ case "4", WaybillProcessSupport.STATUS_PENDING_RETURN_CONFIRM, "confirm" -> 4;
default -> null;
};
}
@@ -515,6 +534,7 @@ public class ManageWaybillServiceImpl implements IManageWaybillService {
detail.setId(head == null ? loading.getId() : head.getId());
detail.setWaybillNo(head == null ? loading.getLoadingNo() : Func.toStr(head.getWaybillNo(), loading.getLoadingNo()));
detail.setStatus(TripAggregation.appStatus(tripBusinessStatus(children)));
+ detail.setBusinessStatus(tripBusinessStatus(children));
detail.setTransportMode(Func.toStr(loading.getTransportType(), ""));
detail.setTransportType(toTransportTypeLabel(Func.toStr(loading.getTransportType(), "")));
detail.setTransportOrgType(TRANSPORT_LOAD);
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceivablePayableDetailServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceivablePayableDetailServiceImpl.java
index c9865f90..6bdbdcdd 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceivablePayableDetailServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceivablePayableDetailServiceImpl.java
@@ -200,6 +200,60 @@ public class ReceivablePayableDetailServiceImpl
return result;
}
+ @Override
+ public boolean requiresMileage(Waybill waybill) {
+ if (waybill == null) {
+ return false;
+ }
+ ContractManage receivableContract = Func.isEmpty(waybill.getContractId())
+ ? null : contractManageService.getById(waybill.getContractId());
+ if (hasMileageBillingRule(receivableContract, waybill)) {
+ return true;
+ }
+ ContractManage payableContract;
+ try {
+ payableContract = resolveCarrierContract(waybill);
+ } catch (ServiceException exception) {
+ // 承运商合同数据有问题时不在打卡页抛错:费用生成会带着明确文案拒绝,
+ // 打卡页只回答"这一趟要不要填里程",不该被合同脏数据卡住。
+ log.warn("判定运单是否含里程计费时承运商合同解析失败,按不含里程处理,waybillId:{},原因:{}",
+ waybill.getId(), exception.getMessage());
+ return false;
+ }
+ return hasMileageBillingRule(payableContract, waybill);
+ }
+
+ /**
+ * 这套合同命中的计费规则里,是否有里程类要素且规则本身命中该运单。
+ *
+ * 方案解析与规则命中都复用费用生成那两条私有方法,保证「打卡时要里程」与「结算时按里程算」同一口径。
+ */
+ private boolean hasMileageBillingRule(ContractManage contract, Waybill waybill) {
+ if (contract == null || waybill == null) {
+ return false;
+ }
+ Map plan = resolveDefaultBillingPlan(
+ parseList(contract.getBillingPlanJson()), waybill.getTransportType());
+ if (plan == null || !(plan.get("rules") instanceof List> rules)) {
+ return false;
+ }
+ for (Object value : rules) {
+ if (!(value instanceof Map, ?> raw)) {
+ continue;
+ }
+ Map rule = toRuleMap(raw);
+ if (!isMileageBasedElement(stringValue(rule, "billingElement", ""))) {
+ continue;
+ }
+ boolean matched = feeWaybills(rule, waybill).stream()
+ .anyMatch(feeWaybill -> matchesBillingRule(raw, rule, feeWaybill));
+ if (matched) {
+ return true;
+ }
+ }
+ return false;
+ }
+
@Override
@Transactional(rollbackFor = Exception.class)
public void syncMileageFromWaybill(Long waybillId, BigDecimal mileage) {
@@ -1052,7 +1106,25 @@ public class ReceivablePayableDetailServiceImpl
@Transactional(propagation = org.springframework.transaction.annotation.Propagation.REQUIRES_NEW,
rollbackFor = Exception.class)
public void generateForCompletedWaybills(List waybillIds) {
- generateAutomaticWaybillDetails(loadWaybills(waybillIds), true,
+ generateForCompletedWaybills(waybillIds, Map.of());
+ }
+
+ @Override
+ @Transactional(propagation = org.springframework.transaction.annotation.Propagation.REQUIRES_NEW,
+ rollbackFor = Exception.class)
+ public void generateForCompletedWaybills(List waybillIds, Map mileageOverrides) {
+ List waybills = loadWaybills(waybillIds);
+ // 独立事务读的是调用方提交前的数据:司机打卡刚写的里程在这里不可见,
+ // 不覆盖就会按 null 里程算出 0 元运费。
+ if (Func.isNotEmpty(mileageOverrides)) {
+ waybills.forEach(waybill -> {
+ BigDecimal mileage = mileageOverrides.get(waybill.getId());
+ if (mileage != null) {
+ waybill.setMileage(mileage);
+ }
+ });
+ }
+ generateAutomaticWaybillDetails(waybills, true,
this::resolveWaybillCarrierContract, AuthUtil.getUserId(), new Date());
}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportVehicleServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportVehicleServiceImpl.java
index d9e1c734..4e6e2c00 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportVehicleServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportVehicleServiceImpl.java
@@ -264,9 +264,6 @@ public class TransportVehicleServiceImpl extends BaseServiceImpl 0;
case RUNNING -> 1;
+ case PENDING_RETURN_CONFIRM -> 1;
case COMPLETED -> 2;
case CANCELLED -> 3;
default -> null;
};
}
+ /**
+ * 运次的列表档位:0 待接单 / 1 进行中 / 2 已完成 / 3 已取消 / 4 待确认(待回单确认)。
+ *
+ * 与 {@link #appStatus(String)} 的区别只在待回单确认:卡片状态仍按 1(运输中)展示,
+ * 列表档位单独成档,待回单确认不再落进行中。
+ */
+ static Integer tabOf(String status) {
+ return switch (Func.toStr(status, "")) {
+ case PENDING -> 0;
+ case RUNNING -> 1;
+ case COMPLETED -> 2;
+ case CANCELLED -> 3;
+ case PENDING_RETURN_CONFIRM -> 4;
+ default -> null;
+ };
+ }
+
/**
* 运次状态:终态优先——子单全为终态时有已完成取已完成、否则已取消;
- * 否则需接单且未接单取待接单,其余取运输中。
+ * 其次待接单档优先;再次有在跑的子单取运输中;只剩待回单确认的取待回单确认。
+ *
+ * 待回单确认不受接单需求影响:需要接单的趟车走到回单确认时司机已接过单,不再回退成待接单。
*
* 子单集合为空时无从判定,返回空。
*/
static String statusOf(List children, boolean requireAccept, boolean accepted) {
String status = classify(count(children));
- if (status == null || TERMINAL_STATUSES.contains(status)) {
+ if (status == null || TERMINAL_STATUSES.contains(status) || PENDING_RETURN_CONFIRM.equals(status)) {
return status;
}
if (!requireAccept) {
@@ -106,13 +126,15 @@ final class TripAggregation {
}
/**
- * 子单聚合结果:子单总数、终态数、已完成数、待接单档数
+ * 子单聚合结果:子单总数、终态数、已完成数、待接单档数、运输中数、待回单确认数
*/
static Counters count(List children) {
int total = 0;
int terminal = 0;
int completed = 0;
int pending = 0;
+ int running = 0;
+ int returnConfirm = 0;
for (Waybill child : children) {
String status = child == null ? null : child.getBusinessStatus();
total++;
@@ -125,10 +147,20 @@ final class TripAggregation {
if (status != null && PENDING_STATUSES.contains(status)) {
pending++;
}
+ if (RUNNING.equals(status)) {
+ running++;
+ }
+ if (PENDING_RETURN_CONFIRM.equals(status)) {
+ returnConfirm++;
+ }
}
- return new Counters(total, terminal, completed, pending);
+ return new Counters(total, terminal, completed, pending, running, returnConfirm);
}
+ /**
+ * 待接单档优先,其次运输中,最后只剩待回单确认——整趟车都打完卡等回单确认才单独成档,
+ * 混着在跑的子单时仍算运输中。
+ */
private static String classify(Counters counters) {
if (counters.total() == 0) {
return null;
@@ -136,7 +168,16 @@ final class TripAggregation {
if (counters.terminal() == counters.total()) {
return counters.completed() > 0 ? COMPLETED : CANCELLED;
}
- return counters.pending() > 0 ? PENDING : RUNNING;
+ if (counters.pending() > 0) {
+ return PENDING;
+ }
+ if (counters.running() > 0) {
+ return RUNNING;
+ }
+ if (counters.returnConfirm() > 0) {
+ return PENDING_RETURN_CONFIRM;
+ }
+ return RUNNING;
}
/**
@@ -169,7 +210,7 @@ final class TripAggregation {
}
/** 子单聚合计数 */
- record Counters(int total, int terminal, int completed, int pending) {
+ record Counters(int total, int terminal, int completed, int pending, int running, int returnConfirm) {
}
}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TripQueryServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TripQueryServiceImpl.java
index abf0fc92..56a2a5d3 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TripQueryServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TripQueryServiceImpl.java
@@ -128,9 +128,10 @@ public class TripQueryServiceImpl implements ITripQueryService {
if (!scoped(condition)) {
return counts;
}
- // 一次取数算清四档:分档口径与列表、卡片同一份推导,不再按库里的业务状态分批计数
+ // 一次取数算清各档:分档口径与列表、卡片同一份推导,不再按库里的业务状态分批计数
long pending = 0;
long doing = 0;
+ long confirm = 0;
long done = 0;
for (TripRows trip : trips(withoutTab(condition))) {
Integer tier = trip.tier();
@@ -141,14 +142,16 @@ public class TripQueryServiceImpl implements ITripQueryService {
case 0 -> pending++;
case 1 -> doing++;
case 2 -> done++;
+ case 4 -> confirm++;
default -> {
}
}
}
counts.setPending(pending);
counts.setDoing(doing);
+ counts.setConfirm(confirm);
counts.setDone(done);
- counts.setAll(pending + doing + done);
+ counts.setAll(pending + doing + confirm + done);
return counts;
}
@@ -205,7 +208,7 @@ public class TripQueryServiceImpl implements ITripQueryService {
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);
+ Integer tier = TripAggregation.tabOf(status);
if (tier == null || !tiers.contains(tier)) {
return;
}
@@ -361,7 +364,7 @@ public class TripQueryServiceImpl implements ITripQueryService {
}
/**
- * 要哪几档:多档优先,其次单档,都不传按「全部」=待接单/进行中/已完成(不含已取消)
+ * 要哪几档:多档优先,其次单档,都不传按「全部」=待接单/进行中/待确认/已完成(不含已取消)
*/
private Set tiers(TripQueryVO condition) {
if (Func.isNotEmpty(condition.getStatuses())) {
@@ -370,7 +373,7 @@ public class TripQueryServiceImpl implements ITripQueryService {
if (condition.getStatus() != null) {
return Set.of(condition.getStatus());
}
- return Set.of(0, 1, 2);
+ return Set.of(0, 1, 2, 4);
}
/**
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java
index be049adc..45a9ffa8 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java
@@ -421,6 +421,16 @@ public class WaybillServiceImpl extends BaseServiceImpl
if (processNodes.isEmpty()) {
processNodes = WaybillProcessSupport.listDriverPunchNodes(processJson);
}
+ // 里程只在末位打卡节点采集:判据与自动完成、司机端 needMileage 同源(司机可打卡节点的末位)。
+ List