From 83b21444076691cdfec9b8814d88d50c0e7cd3e5 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Thu, 22 Oct 2026 10:59:21 +0800 Subject: [PATCH] =?UTF-8?q?:sparkles:=20=E5=B0=8F=E7=A8=8B=E5=BA=8F?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E6=94=AF=E6=8C=81=E7=BB=84=E7=BB=87=E4=B8=8E?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E6=9D=83=E9=99=90=EF=BC=8C=E8=B0=83=E5=BA=A6?= =?UTF-8?q?=E7=AB=AF=E6=8C=89=E9=A1=B9=E7=9B=AE=E4=B8=9A=E5=8A=A1=E9=83=A8?= =?UTF-8?q?=E9=97=A8=E6=94=B6=E5=8F=A3=E8=BF=90=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- .../transport/pojo/vo/TripQueryVO.java | 3 + .../system/feign/IMpUserClient.java | 8 + .../system/pojo/entity/MpUser.java | 20 +- .../system/pojo/entity/MpUserDataScope.java | 44 +++++ .../system/pojo/vo/MpUserDataScopeVO.java | 91 +++++++++ .../springblade/system/pojo/vo/MpUserVO.java | 7 + .../system/controller/MpUserController.java | 3 +- .../system/feign/MpUserClient.java | 8 + .../system/mapper/MpUserDataScopeMapper.java | 21 ++ .../system/service/IMpUserService.java | 13 ++ .../service/impl/MpUserServiceImpl.java | 154 ++++++++++++++- .../system/wrapper/MpUserWrapper.java | 4 + .../impl/ExceptionDisposalServiceImpl.java | 8 +- .../impl/ManageWaybillServiceImpl.java | 124 +++++++++--- .../service/impl/RiskDisposalServiceImpl.java | 13 +- .../service/impl/TripQueryServiceImpl.java | 14 ++ .../support/MpUserDataScopeSupport.java | 184 ++++++++++++++++++ .../impl/ExceptionDisposalListQueryTest.java | 5 +- .../impl/ExceptionDisposalReportGateTest.java | 3 +- .../service/impl/ManageAdminHomeFeedTest.java | 3 +- .../impl/ManageTripListDetailTest.java | 3 +- .../impl/ManageWaybillOrgScopeTest.java | 6 +- doc/sql/transport/blade_mp_user_20261012.sql | 2 + .../blade_mp_user_data_scope_20261021.sql | 12 ++ 24 files changed, 717 insertions(+), 36 deletions(-) create mode 100644 blade-service-api/blade-user-api/src/main/java/org/springblade/system/pojo/entity/MpUserDataScope.java create mode 100644 blade-service-api/blade-user-api/src/main/java/org/springblade/system/pojo/vo/MpUserDataScopeVO.java create mode 100644 blade-service/blade-system/src/main/java/org/springblade/system/mapper/MpUserDataScopeMapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/support/MpUserDataScopeSupport.java create mode 100644 doc/sql/transport/blade_mp_user_data_scope_20261021.sql diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TripQueryVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TripQueryVO.java index 5027ada9..0fd0b218 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TripQueryVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/TripQueryVO.java @@ -56,6 +56,9 @@ public class TripQueryVO implements Serializable { @Schema(description = "可见组织 ID 集合:非空时只出这些组织的运单行,空集合表示不按组织过滤") private List deptIds; + @Schema(description = "可见项目 ID 集合:非空时只出这些项目的运单行,空集合表示不按项目过滤(小程序调度端优先用此口径)") + private List projectIds; + @Schema(description = "状态:0待接单/1进行中/2已完成/3已取消,不传为全部(不含已取消)") private Integer status; diff --git a/blade-service-api/blade-user-api/src/main/java/org/springblade/system/feign/IMpUserClient.java b/blade-service-api/blade-user-api/src/main/java/org/springblade/system/feign/IMpUserClient.java index 0a0a6e4b..ad462f4d 100644 --- a/blade-service-api/blade-user-api/src/main/java/org/springblade/system/feign/IMpUserClient.java +++ b/blade-service-api/blade-user-api/src/main/java/org/springblade/system/feign/IMpUserClient.java @@ -26,6 +26,7 @@ import org.springblade.core.launch.constant.AppConstant; import org.springblade.core.tool.api.R; import org.springblade.system.pojo.entity.MpUser; import org.springblade.system.pojo.entity.MpUserInfo; +import org.springblade.system.pojo.vo.MpUserDataScopeVO; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; @@ -53,6 +54,7 @@ public interface IMpUserClient { String UPDATE_USER = API_PREFIX + "/update-user"; String BIND_OPENID = API_PREFIX + "/bind-openid"; String SET_PASSWORD = API_PREFIX + "/set-password"; + String RESOLVE_DATA_SCOPE = API_PREFIX + "/resolve-data-scope"; /** * 按主键查询 @@ -60,6 +62,12 @@ public interface IMpUserClient { @GetMapping(USER_BY_ID) R userById(@RequestParam("userId") Long userId); + /** + * 解析小程序用户可见组织(所属组织 + 数据权限范围 + 数据层级范围) + */ + @GetMapping(RESOLVE_DATA_SCOPE) + R resolveDataScope(@RequestParam("userId") Long userId); + /** * 按租户 + 手机号查询 */ diff --git a/blade-service-api/blade-user-api/src/main/java/org/springblade/system/pojo/entity/MpUser.java b/blade-service-api/blade-user-api/src/main/java/org/springblade/system/pojo/entity/MpUser.java index 6d613811..11896b43 100644 --- a/blade-service-api/blade-user-api/src/main/java/org/springblade/system/pojo/entity/MpUser.java +++ b/blade-service-api/blade-user-api/src/main/java/org/springblade/system/pojo/entity/MpUser.java @@ -25,6 +25,7 @@ */ package org.springblade.system.pojo.entity; +import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableName; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -35,6 +36,7 @@ import org.springblade.core.tool.sensitive.SensitiveType; import java.io.Serial; import java.util.Date; +import java.util.List; /** * 小程序用户实体(仅用于小程序登录,与 blade_user 分离) @@ -113,10 +115,26 @@ public class MpUser extends TenantEntity { @Schema(description = "小程序身份") private String identities; /** - * 部门id + * 部门id(多选,逗号分隔) */ @Schema(description = "部门id") private String deptId; + /** + * 数据权限范围:1-仅所属组织,2-全部组织,3-自定义 + */ + @Schema(description = "数据权限范围") + private Integer dataScopeRange; + /** + * 数据层级范围:1-包含下级,2-仅本级 + */ + @Schema(description = "数据层级范围") + private Integer dataLevelRange; + /** + * 自定义数据权限组织ID列表(dataScopeRange=3 时生效) + */ + @TableField(exist = false) + @Schema(description = "自定义数据权限组织ID") + private List dataScopeDeptIds; /** * 岗位id */ diff --git a/blade-service-api/blade-user-api/src/main/java/org/springblade/system/pojo/entity/MpUserDataScope.java b/blade-service-api/blade-user-api/src/main/java/org/springblade/system/pojo/entity/MpUserDataScope.java new file mode 100644 index 00000000..1115694d --- /dev/null +++ b/blade-service-api/blade-user-api/src/main/java/org/springblade/system/pojo/entity/MpUserDataScope.java @@ -0,0 +1,44 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.system.pojo.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 小程序用户组织数据权限 + * + * @author Chill + */ +@Data +@TableName("blade_mp_user_data_scope") +@Schema(description = "小程序用户组织数据权限") +public class MpUserDataScope implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.ASSIGN_ID) + @Schema(description = "主键") + private Long id; + + @Schema(description = "小程序用户主键") + private Long userId; + + @Schema(description = "组织主键") + private Long deptId; + +} diff --git a/blade-service-api/blade-user-api/src/main/java/org/springblade/system/pojo/vo/MpUserDataScopeVO.java b/blade-service-api/blade-user-api/src/main/java/org/springblade/system/pojo/vo/MpUserDataScopeVO.java new file mode 100644 index 00000000..6d9f38bf --- /dev/null +++ b/blade-service-api/blade-user-api/src/main/java/org/springblade/system/pojo/vo/MpUserDataScopeVO.java @@ -0,0 +1,91 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.system.pojo.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +/** + * 小程序用户数据权限解析结果 + *

+ * allDept=true 表示全部组织(查询不过滤);否则按 deptIds 收口,空列表表示无可见组织。 + * + * @author Chill + */ +@Data +@Schema(description = "小程序用户数据权限") +public class MpUserDataScopeVO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 是否全部组织(不过滤) + */ + @Schema(description = "是否全部组织") + private boolean allDept; + + /** + * 可见组织 ID;allDept=false 时生效,空表示无权限 + */ + @Schema(description = "可见组织ID") + private List deptIds = new ArrayList<>(); + + /** + * 全部组织 + */ + public static MpUserDataScopeVO all() { + MpUserDataScopeVO scope = new MpUserDataScopeVO(); + scope.setAllDept(true); + return scope; + } + + /** + * 无可见组织 + */ + public static MpUserDataScopeVO empty() { + MpUserDataScopeVO scope = new MpUserDataScopeVO(); + scope.setAllDept(false); + scope.setDeptIds(new ArrayList<>()); + return scope; + } + + /** + * 指定可见组织 + */ + public static MpUserDataScopeVO of(List deptIds) { + MpUserDataScopeVO scope = new MpUserDataScopeVO(); + scope.setAllDept(false); + scope.setDeptIds(deptIds == null ? new ArrayList<>() : new ArrayList<>(deptIds)); + return scope; + } + +} diff --git a/blade-service-api/blade-user-api/src/main/java/org/springblade/system/pojo/vo/MpUserVO.java b/blade-service-api/blade-user-api/src/main/java/org/springblade/system/pojo/vo/MpUserVO.java index c2ced763..f65620ee 100644 --- a/blade-service-api/blade-user-api/src/main/java/org/springblade/system/pojo/vo/MpUserVO.java +++ b/blade-service-api/blade-user-api/src/main/java/org/springblade/system/pojo/vo/MpUserVO.java @@ -42,6 +42,13 @@ public class MpUserVO extends MpUser { @Serial private static final long serialVersionUID = 1L; + /** + * 所属组织名称(逗号分隔) + */ + @TableField(exist = false) + @Schema(description = "所属组织名称") + private String deptName; + /** * 身份名称(逗号分隔中文) */ diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/controller/MpUserController.java b/blade-service/blade-system/src/main/java/org/springblade/system/controller/MpUserController.java index b4c84f8e..435ba1d3 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/controller/MpUserController.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/controller/MpUserController.java @@ -71,7 +71,8 @@ public class MpUserController extends BladeController { @ApiOperationSupport(order = 1) @Operation(summary = "详情", description = "传入id") public R detail(MpUser mpUser) { - MpUser detail = mpUserService.getOne(Condition.getQueryWrapper(mpUser)); + Long id = mpUser == null ? null : mpUser.getId(); + MpUser detail = id == null ? null : mpUserService.detail(id); if (detail == null) { return R.fail("小程序用户不存在"); } diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/feign/MpUserClient.java b/blade-service/blade-system/src/main/java/org/springblade/system/feign/MpUserClient.java index 4d8eabfe..4a4becd4 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/feign/MpUserClient.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/feign/MpUserClient.java @@ -28,10 +28,12 @@ import org.springblade.core.tenant.annotation.NonDS; import org.springblade.core.tool.api.R; import org.springblade.system.pojo.entity.MpUser; import org.springblade.system.pojo.entity.MpUserInfo; +import org.springblade.system.pojo.vo.MpUserDataScopeVO; import org.springblade.system.service.IMpUserService; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; /** @@ -53,6 +55,12 @@ public class MpUserClient implements IMpUserClient { return R.data(mpUserService.getById(userId)); } + @Override + @GetMapping(RESOLVE_DATA_SCOPE) + public R resolveDataScope(@RequestParam("userId") Long userId) { + return R.data(mpUserService.resolveDataScope(userId)); + } + @Override @GetMapping(USER_BY_PHONE) public R userByPhone(String tenantId, String phone) { diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/mapper/MpUserDataScopeMapper.java b/blade-service/blade-system/src/main/java/org/springblade/system/mapper/MpUserDataScopeMapper.java new file mode 100644 index 00000000..80704440 --- /dev/null +++ b/blade-service/blade-system/src/main/java/org/springblade/system/mapper/MpUserDataScopeMapper.java @@ -0,0 +1,21 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.system.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.springblade.system.pojo.entity.MpUserDataScope; + +/** + * 小程序用户组织数据权限 Mapper + * + * @author Chill + */ +public interface MpUserDataScopeMapper extends BaseMapper { +} diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/IMpUserService.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/IMpUserService.java index c0294ced..e6102c4d 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/service/IMpUserService.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/IMpUserService.java @@ -26,6 +26,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage; import org.springblade.core.mp.base.BaseService; import org.springblade.system.pojo.entity.MpUser; import org.springblade.system.pojo.entity.MpUserInfo; +import org.springblade.system.pojo.vo.MpUserDataScopeVO; import java.util.List; import java.util.Map; @@ -67,6 +68,18 @@ public interface IMpUserService extends BaseService { */ IPage selectMpUserPage(IPage page, MpUser query); + /** + * 详情(含自定义数据权限组织) + */ + MpUser detail(Long id); + + /** + * 解析小程序用户可见组织(所属组织 + 数据权限范围 + 数据层级范围) + *

+ * allDept=true 表示全部组织;否则 deptIds 为可见组织,空列表表示无权限。 + */ + MpUserDataScopeVO resolveDataScope(Long userId); + /** * 保存小程序用户 */ 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 93f744ba..fdd83446 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 @@ -35,15 +35,20 @@ import org.springblade.core.tool.utils.DigestUtil; import org.springblade.core.tool.utils.Func; import org.springblade.core.tool.utils.StringUtil; import org.springblade.system.cache.MpUserCache; +import org.springblade.system.cache.SysCache; +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.entity.MpUserInfo; import org.springblade.system.pojo.enums.MpUserIdentity; +import org.springblade.system.pojo.vo.MpUserDataScopeVO; import org.springblade.system.service.IMpUserService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; @@ -57,6 +62,19 @@ import java.util.Objects; @RequiredArgsConstructor public class MpUserServiceImpl extends BaseServiceImpl implements IMpUserService { + /** 数据权限范围:仅所属组织 */ + private static final int DATA_SCOPE_OWN = 1; + /** 数据权限范围:全部组织 */ + private static final int DATA_SCOPE_ALL = 2; + /** 数据权限范围:自定义 */ + private static final int DATA_SCOPE_CUSTOM = 3; + /** 数据层级范围:包含下级 */ + private static final int DATA_LEVEL_INCLUDE_CHILDREN = 1; + /** 数据层级范围:仅本级 */ + private static final int DATA_LEVEL_CURRENT_ONLY = 2; + + private final MpUserDataScopeMapper mpUserDataScopeMapper; + @Override public MpUser getByPhone(String tenantId, String phone) { if (Func.isBlank(tenantId) || Func.isBlank(phone)) { @@ -112,6 +130,64 @@ public class MpUserServiceImpl extends BaseServiceImpl imp .orderByDesc(MpUser::getCreateTime)); } + @Override + public MpUser detail(Long id) { + if (Func.isEmpty(id)) { + return null; + } + return fillScopes(getById(id)); + } + + @Override + public MpUserDataScopeVO resolveDataScope(Long userId) { + if (Func.isEmpty(userId) || userId <= 0) { + return MpUserDataScopeVO.empty(); + } + MpUser user = getById(userId); + if (user == null || Func.isEmpty(user.getId())) { + return MpUserDataScopeVO.empty(); + } + Integer dataScopeRange = user.getDataScopeRange() == null ? DATA_SCOPE_OWN : user.getDataScopeRange(); + Integer dataLevelRange = user.getDataLevelRange() == null ? DATA_LEVEL_INCLUDE_CHILDREN : user.getDataLevelRange(); + if (Objects.equals(dataScopeRange, DATA_SCOPE_ALL)) { + return MpUserDataScopeVO.all(); + } + List baseDeptIds; + if (Objects.equals(dataScopeRange, DATA_SCOPE_CUSTOM)) { + baseDeptIds = mpUserDataScopeMapper.selectList(Wrappers.lambdaQuery() + .eq(MpUserDataScope::getUserId, userId)) + .stream() + .map(MpUserDataScope::getDeptId) + .filter(Objects::nonNull) + .distinct() + .toList(); + } else { + // 仅所属组织 + baseDeptIds = Func.toLongList(user.getDeptId()); + } + if (Func.isEmpty(baseDeptIds)) { + return MpUserDataScopeVO.empty(); + } + boolean includeChildren = !Objects.equals(dataLevelRange, DATA_LEVEL_CURRENT_ONLY); + LinkedHashSet allowedDeptIds = new LinkedHashSet<>(); + for (Long deptId : baseDeptIds) { + if (Func.isEmpty(deptId) || deptId <= 0) { + continue; + } + if (includeChildren) { + List childIds = SysCache.getDeptChildIds(deptId); + if (Func.isNotEmpty(childIds)) { + allowedDeptIds.addAll(childIds); + } else { + allowedDeptIds.add(deptId); + } + } else { + allowedDeptIds.add(deptId); + } + } + return MpUserDataScopeVO.of(List.copyOf(allowedDeptIds)); + } + @Override @Transactional(rollbackFor = Exception.class) public boolean submit(MpUser user) { @@ -122,7 +198,7 @@ public class MpUserServiceImpl extends BaseServiceImpl imp throw new ServiceException("该手机号已存在小程序用户"); } encryptPasswordIfPresent(user); - boolean saved = save(user); + boolean saved = save(user) && submitScopes(user); if (saved) { MpUserCache.clear(); } @@ -135,7 +211,18 @@ public class MpUserServiceImpl extends BaseServiceImpl imp if (user == null || Func.isEmpty(user.getId())) { throw new ServiceException("小程序用户主键不能为空"); } + MpUser dbUser = getById(user.getId()); + if (dbUser == null || Func.isEmpty(dbUser.getId())) { + throw new ServiceException("小程序用户不存在"); + } prepare(user); + // 未改手机号(前端脱敏未提交 / 仍为脱敏值)时沿用库中原值,避免把 138****8000 写回 + if (Func.isBlank(user.getPhone()) || isMaskedPhone(user.getPhone())) { + user.setPhone(dbUser.getPhone()); + } + if (Func.isBlank(user.getEmail()) || isMaskedEmail(user.getEmail())) { + user.setEmail(dbUser.getEmail()); + } validate(user); MpUser existed = getByPhone(user.getTenantId(), user.getPhone()); if (existed != null && Func.isNotEmpty(existed.getId()) && !existed.getId().equals(user.getId())) { @@ -143,7 +230,7 @@ public class MpUserServiceImpl extends BaseServiceImpl imp } // 后台编辑不改密码,避免空串覆盖 user.setPassword(null); - boolean updated = updateById(user); + boolean updated = updateById(user) && submitScopes(user); if (updated) { MpUserCache.clear(); } @@ -167,6 +254,7 @@ public class MpUserServiceImpl extends BaseServiceImpl imp } boolean removed = deleteLogic(ids); if (removed) { + mpUserDataScopeMapper.delete(Wrappers.lambdaQuery().in(MpUserDataScope::getUserId, ids)); MpUserCache.clear(); } return removed; @@ -314,6 +402,15 @@ public class MpUserServiceImpl extends BaseServiceImpl imp user.setPostId(trimToNull(user.getPostId())); user.setRemark(trimToNull(user.getRemark())); user.setIdentities(normalizeIdentities(user.getIdentities())); + if (user.getDataScopeRange() == null) { + user.setDataScopeRange(1); + } + if (user.getDataLevelRange() == null) { + user.setDataLevelRange(1); + } + if (!Objects.equals(user.getDataScopeRange(), 3)) { + user.setDataScopeDeptIds(List.of()); + } if (Func.isBlank(user.getAccount()) && Func.isNotBlank(user.getPhone())) { user.setAccount(user.getPhone()); } @@ -347,6 +444,9 @@ 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("小程序身份不能为空"); } @@ -359,6 +459,42 @@ public class MpUserServiceImpl extends BaseServiceImpl imp throw new ServiceException("不支持的小程序身份:" + code); } } + Integer dataScopeRange = user.getDataScopeRange(); + if (dataScopeRange == null || (dataScopeRange != 1 && dataScopeRange != 2 && dataScopeRange != 3)) { + throw new ServiceException("数据权限范围不正确"); + } + Integer dataLevelRange = user.getDataLevelRange(); + if (dataLevelRange == null || (dataLevelRange != 1 && dataLevelRange != 2)) { + throw new ServiceException("数据层级范围不正确"); + } + if (Objects.equals(dataScopeRange, 3) && Func.isEmpty(user.getDataScopeDeptIds())) { + throw new ServiceException("请选择自定义数据权限组织"); + } + } + + private boolean submitScopes(MpUser user) { + if (user == null || Func.isEmpty(user.getId())) { + return false; + } + 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 -> { + MpUserDataScope scope = new MpUserDataScope(); + scope.setUserId(user.getId()); + scope.setDeptId(deptId); + mpUserDataScopeMapper.insert(scope); + }); + } + return true; + } + + private MpUser fillScopes(MpUser user) { + if (user == null || Func.isEmpty(user.getId())) { + return user; + } + user.setDataScopeDeptIds(mpUserDataScopeMapper.selectList(Wrappers.lambdaQuery() + .eq(MpUserDataScope::getUserId, user.getId())).stream().map(MpUserDataScope::getDeptId).toList()); + return user; } private String normalizeIdentities(String identities) { @@ -377,4 +513,18 @@ public class MpUserServiceImpl extends BaseServiceImpl imp return trimValue.isEmpty() ? null : trimValue; } + /** + * 判断是否为前端脱敏手机号(如 138****8000),不可直接落库。 + */ + private boolean isMaskedPhone(String phone) { + return Func.isNotBlank(phone) && phone.contains("*"); + } + + /** + * 判断是否为前端脱敏邮箱。 + */ + private boolean isMaskedEmail(String email) { + return Func.isNotBlank(email) && email.contains("*"); + } + } diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/wrapper/MpUserWrapper.java b/blade-service/blade-system/src/main/java/org/springblade/system/wrapper/MpUserWrapper.java index 2022dcd7..1b50e295 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/wrapper/MpUserWrapper.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/wrapper/MpUserWrapper.java @@ -26,6 +26,7 @@ import org.springblade.core.mp.support.BaseEntityWrapper; import org.springblade.core.tool.utils.BeanUtil; import org.springblade.core.tool.utils.Func; import org.springblade.system.cache.DictCache; +import org.springblade.system.cache.SysCache; import org.springblade.system.cache.UserCache; import org.springblade.system.pojo.entity.MpUser; import org.springblade.system.pojo.enums.DictEnum; @@ -53,6 +54,9 @@ public class MpUserWrapper extends BaseEntityWrapper { // 密码哈希不下发前端 mpUserVO.setPassword(null); mpUserVO.setIdentitiesName(resolveIdentitiesName(mpUser.getIdentities())); + if (Func.isNotBlank(mpUser.getDeptId())) { + mpUserVO.setDeptName(Func.join(SysCache.getDeptNames(mpUser.getDeptId()))); + } if (mpUser.getSex() != null) { mpUserVO.setSexName(DictCache.getValue(DictEnum.SEX, mpUser.getSex())); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ExceptionDisposalServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ExceptionDisposalServiceImpl.java index 575ce831..3d82894b 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ExceptionDisposalServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ExceptionDisposalServiceImpl.java @@ -44,6 +44,7 @@ import org.springblade.transport.pojo.vo.ExceptionDisposalVO; import org.springblade.transport.service.IDriverWaybillService; import org.springblade.transport.service.IExceptionDisposalService; import org.springblade.transport.service.IWaybillService; +import org.springblade.transport.support.MpUserDataScopeSupport; import org.springblade.transport.support.WaybillProcessSupport; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -79,13 +80,16 @@ public class ExceptionDisposalServiceImpl private final ExceptionDisposalFollowRecordMapper followRecordMapper; private final IWaybillService waybillService; private final IDriverWaybillService driverWaybillService; + private final MpUserDataScopeSupport mpUserDataScopeSupport; public ExceptionDisposalServiceImpl(ExceptionDisposalFollowRecordMapper followRecordMapper, IWaybillService waybillService, - IDriverWaybillService driverWaybillService) { + IDriverWaybillService driverWaybillService, + MpUserDataScopeSupport mpUserDataScopeSupport) { this.followRecordMapper = followRecordMapper; this.waybillService = waybillService; this.driverWaybillService = driverWaybillService; + this.mpUserDataScopeSupport = mpUserDataScopeSupport; } @Override @@ -409,6 +413,8 @@ public class ExceptionDisposalServiceImpl .like(Func.isNotEmpty(query.getCarrierName()), ExceptionDisposal::getCarrierName, query.getCarrierName()) .eq(Func.isNotEmpty(query.getExceptionType()), ExceptionDisposal::getExceptionType, query.getExceptionType()) .eq(Func.isNotEmpty(query.getDisposalStatus()), ExceptionDisposal::getDisposalStatus, query.getDisposalStatus()); + // 调度端列表按「权限组织 → 项目业务部门 → 项目」收口;司机端按运单查自己的异常时不套此过滤 + mpUserDataScopeSupport.applyProjectScopeIfMpDispatcher(wrapper, ExceptionDisposal::getProjectId); return wrapper.orderByDesc(ExceptionDisposal::getCreateTime); } 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 4b1e5c93..1ad8e2e0 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 @@ -33,6 +33,7 @@ import org.springblade.core.secure.utils.AuthUtil; import org.springblade.core.tool.utils.DateUtil; import org.springblade.core.tool.utils.Func; import org.springblade.system.cache.UserDisplayCache; +import org.springblade.system.utils.MpUserAuth; import org.springblade.transport.pojo.entity.Driver; import org.springblade.transport.pojo.entity.ExceptionDisposal; import org.springblade.transport.pojo.entity.LoadingManage; @@ -59,6 +60,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.MpUserDataScopeSupport; import org.springblade.transport.support.TransportBusinessSupport; import org.springblade.transport.support.WaybillCargoSupport; import org.springblade.transport.support.WaybillProcessSupport; @@ -89,8 +91,8 @@ import java.util.stream.Collectors; * 列表与详情与司机端同粒度:取数、状态聚合、接单需求合并都调共享运次读服务, * 本类只做字段裁剪与调度端独有的筛选(组织类型 / 异常 / 创建时间 / 待重新派单)。 *

- * 小程序调度账号(如「小程序管理」)组织常与运单业务组织不一致,故不做 dept 过滤, - * 仅依赖租户隔离,口径接近后台 {@code /waybill-manage/list?allDept=1}。 + * 小程序调度端:所属组织 + 数据权限范围 + 数据层级范围 → 业务部门在范围内的项目 → 再取这些项目的运单; + * 异常/风险与运单同一项目口径。系统用户仍按「所属组织 / 超管不过滤」。 */ @Service @RequiredArgsConstructor @@ -137,13 +139,14 @@ public class ManageWaybillServiceImpl implements IManageWaybillService { private final IDriverWaybillService driverWaybillService; private final ITripQueryService tripQueryService; private final ILoadingManageService loadingManageService; + private final MpUserDataScopeSupport mpUserDataScopeSupport; @Override public AdminHomeStatsVO stats() { AdminHomeStatsVO vo = new AdminHomeStatsVO(); // 三档状态按运次计:有配载单的一趟车算一个,与列表和司机端同一份聚合 TripQueryVO query = TripQueryVO.forAdmin(); - query.setDeptIds(manageableDeptIds()); + applyAdminDataScope(query); TripTabCountsVO tripCounts = tripQueryService.tabCounts(query); vo.setPendingAccept(tripCounts.getPending()); vo.setTransporting(tripCounts.getDoing()); @@ -181,7 +184,7 @@ public class ManageWaybillServiceImpl implements IManageWaybillService { // 组织类型 / 异常 / 创建时间都是子单行级条件,与状态、关键字、分页一起交给运次读服务, // 一次取数取完整趟车:分组、分档、筛选、排序与分页都在它那边算完 TripQueryVO query = TripQueryVO.forAdmin(); - query.setDeptIds(manageableDeptIds()); + applyAdminDataScope(query); query.setStatus(toTripStatus(status)); query.setKeyword(keyword); query.setTransportOrgType(transportType); @@ -229,7 +232,7 @@ public class ManageWaybillServiceImpl implements IManageWaybillService { Set exceptionWaybillIds = loadIncompleteExceptionWaybillIds(); TripQueryVO query = TripQueryVO.forAdmin(); - query.setDeptIds(manageableDeptIds()); + applyAdminDataScope(query); query.setKeyword(keyword); // 待处理只要待接单与进行中两档;「未拒单」也是交给运次读服务按子单行筛 query.setStatuses(List.of(0, 1)); @@ -667,11 +670,8 @@ 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 deptIds = manageableDeptIds(); - if (Func.isNotEmpty(deptIds)) { - wrapper.in("dept_id", deptIds); - } + // 这条查询不经运次读服务,数据权限收口得自己带上,否则待重派列表与首页重派角标会越权 + applyAdminDataScope(wrapper); wrapper.groupBy(TripAggregation.SQL_TRIP_KEY); return wrapper; } @@ -884,10 +884,50 @@ public class ManageWaybillServiceImpl implements IManageWaybillService { } /** - * 调度端可见组织:普通用户(含各级角色)只出自己所属组织,超级管理员不过滤。 - *

- * 小程序不校验「所属组织不能为空」:账号没有组织时不按组织收口,避免首页、待处理等接口直接报错。 - * 详情与重新派单本身不走这里。 + * 调度端数据权限落到运次查询条件:小程序按可见项目,系统用户按所属组织。 + */ + private void applyAdminDataScope(TripQueryVO query) { + if (query == null) { + return; + } + if (MpUserAuth.isMpUser()) { + query.setProjectIds(manageableProjectIds()); + return; + } + query.setDeptIds(manageableDeptIds()); + } + + /** + * 待重派等直查运单表时的数据权限:小程序按 project_id,系统用户按 dept_id。 + */ + private void applyAdminDataScope(QueryWrapper wrapper) { + if (wrapper == null) { + return; + } + if (MpUserAuth.isMpUser()) { + List projectIds = manageableProjectIds(); + if (Func.isNotEmpty(projectIds)) { + wrapper.in("project_id", projectIds); + } + return; + } + List deptIds = manageableDeptIds(); + if (Func.isNotEmpty(deptIds)) { + wrapper.in("dept_id", deptIds); + } + } + + /** + * 小程序调度端可见项目:所属组织 + 数据权限范围 + 数据层级范围 → 业务部门在范围内的项目。 + * 空列表不过滤;[-1] 表示无权限。 + */ + private List manageableProjectIds() { + List projectIds = mpUserDataScopeSupport.resolveVisibleProjectIdsForQuery(); + return projectIds == null ? List.of() : projectIds; + } + + /** + * 系统用户可见组织:普通账号只出所属组织,超管不过滤;组织为空不拦截。 */ private List manageableDeptIds() { if (TransportBusinessSupport.isAdministrator()) { @@ -911,10 +951,12 @@ public class ManageWaybillServiceImpl implements IManageWaybillService { } private Set loadIncompleteExceptionWaybillIds() { - List list = exceptionDisposalService.list(Wrappers.lambdaQuery() + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery() .select(ExceptionDisposal::getWaybillId) .in(ExceptionDisposal::getDisposalStatus, DISPOSAL_PENDING, DISPOSAL_PROCESSING) - .isNotNull(ExceptionDisposal::getWaybillId)); + .isNotNull(ExceptionDisposal::getWaybillId); + applyExceptionDeptScope(wrapper); + List list = exceptionDisposalService.list(wrapper); Set ids = new HashSet<>(); for (ExceptionDisposal item : list) { if (item.getWaybillId() != null) { @@ -924,6 +966,38 @@ public class ManageWaybillServiceImpl implements IManageWaybillService { return ids; } + /** 异常与运单同一套项目口径(小程序);系统用户仍按组织 */ + private void applyExceptionDeptScope(LambdaQueryWrapper wrapper) { + if (MpUserAuth.isMpUser()) { + List projectIds = manageableProjectIds(); + if (Func.isNotEmpty(projectIds)) { + wrapper.in(ExceptionDisposal::getProjectId, projectIds); + } + return; + } + List deptIds = manageableDeptIds(); + if (Func.isNotEmpty(deptIds)) { + wrapper.in(ExceptionDisposal::getDeptId, deptIds); + } + } + + /** 风险无项目字段:小程序按关联运单的项目收口;系统用户按组织 */ + private void applyRiskDeptScope(LambdaQueryWrapper wrapper) { + if (MpUserAuth.isMpUser()) { + List projectIds = manageableProjectIds(); + if (Func.isNotEmpty(projectIds)) { + String inList = projectIds.stream().map(String::valueOf).collect(Collectors.joining(",")); + wrapper.inSql(RiskDisposal::getWaybillId, + "SELECT id FROM blade_waybill WHERE is_deleted = 0 AND project_id IN (" + inList + ")"); + } + return; + } + List deptIds = manageableDeptIds(); + if (Func.isNotEmpty(deptIds)) { + wrapper.in(RiskDisposal::getDeptId, deptIds); + } + } + private AdminWaybillCardVO toCard(Waybill waybill, boolean hasException) { AdminWaybillCardVO card = new AdminWaybillCardVO(); card.setId(waybill.getId()); @@ -1051,13 +1125,17 @@ public class ManageWaybillServiceImpl implements IManageWaybillService { } private long countIncompleteExceptions() { - return exceptionDisposalService.count(Wrappers.lambdaQuery() - .in(ExceptionDisposal::getDisposalStatus, DISPOSAL_PENDING, DISPOSAL_PROCESSING)); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery() + .in(ExceptionDisposal::getDisposalStatus, DISPOSAL_PENDING, DISPOSAL_PROCESSING); + applyExceptionDeptScope(wrapper); + return exceptionDisposalService.count(wrapper); } private long countPendingRisks() { - return riskDisposalService.count(Wrappers.lambdaQuery() - .eq(RiskDisposal::getDisposalStatus, RISK_PENDING)); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery() + .eq(RiskDisposal::getDisposalStatus, RISK_PENDING); + applyRiskDeptScope(wrapper); + return riskDisposalService.count(wrapper); } /** @@ -1099,10 +1177,12 @@ public class ManageWaybillServiceImpl implements IManageWaybillService { } private List buildExceptionFeed() { - List list = exceptionDisposalService.list(Wrappers.lambdaQuery() + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery() .in(ExceptionDisposal::getDisposalStatus, DISPOSAL_PENDING, DISPOSAL_PROCESSING) .orderByDesc(ExceptionDisposal::getReportTime) - .last("LIMIT " + FEED_LIMIT)); + .last("LIMIT " + FEED_LIMIT); + applyExceptionDeptScope(wrapper); + List list = exceptionDisposalService.list(wrapper); return list.stream() .map(disposal -> new TimedTodo(toTodoItem(disposal), exceptionTimeOf(disposal))) .collect(Collectors.toList()); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/RiskDisposalServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/RiskDisposalServiceImpl.java index 869cdfc0..bc323eb4 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/RiskDisposalServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/RiskDisposalServiceImpl.java @@ -3,9 +3,9 @@ package org.springblade.transport.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import lombok.RequiredArgsConstructor; import org.springblade.core.log.exception.ServiceException; import org.springblade.core.mp.base.BaseServiceImpl; -import org.springblade.core.secure.utils.AuthUtil; import org.springblade.core.tool.utils.BeanUtil; import org.springblade.core.tool.utils.Func; import org.springblade.system.cache.UserCache; @@ -16,6 +16,7 @@ import org.springblade.transport.pojo.entity.RiskDisposal; import org.springblade.transport.pojo.vo.RiskDisposalBatchResultVO; import org.springblade.transport.pojo.vo.RiskDisposalVO; import org.springblade.transport.service.IRiskDisposalService; +import org.springblade.transport.support.MpUserDataScopeSupport; import org.springblade.transport.support.TransportBusinessSupport; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -25,12 +26,15 @@ import java.util.List; import java.util.Objects; @Service +@RequiredArgsConstructor public class RiskDisposalServiceImpl extends BaseServiceImpl implements IRiskDisposalService { private static final String STATUS_PENDING = "pending"; private static final String STATUS_PROCESSED = "processed"; private static final String STATUS_IGNORED = "ignored"; + private final MpUserDataScopeSupport mpUserDataScopeSupport; + @Override public IPage selectPage(IPage page, RiskDisposalVO query) { IPage entityPage = page(page, buildQuery(query)); @@ -84,15 +88,16 @@ public class RiskDisposalServiceImpl extends BaseServiceImpl buildQuery(RiskDisposalVO query) { - return Wrappers.lambdaQuery() + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery() .eq(RiskDisposal::getIsDeleted, 0) .like(Func.isNotEmpty(query.getRiskNo()), RiskDisposal::getRiskNo, query.getRiskNo()) .like(Func.isNotEmpty(query.getRuleName()), RiskDisposal::getRuleName, query.getRuleName()) .like(Func.isNotEmpty(query.getWaybillNo()), RiskDisposal::getWaybillNo, query.getWaybillNo()) .like(Func.isNotEmpty(query.getProjectName()), RiskDisposal::getProjectName, query.getProjectName()) .eq(Func.isNotEmpty(query.getRiskLevel()), RiskDisposal::getRiskLevel, query.getRiskLevel()) - .eq(Func.isNotEmpty(query.getDisposalStatus()), RiskDisposal::getDisposalStatus, query.getDisposalStatus()) - .orderByDesc(RiskDisposal::getCreateTime); + .eq(Func.isNotEmpty(query.getDisposalStatus()), RiskDisposal::getDisposalStatus, query.getDisposalStatus()); + mpUserDataScopeSupport.applyWaybillProjectScopeIfMpDispatcher(wrapper, RiskDisposal::getWaybillId); + return wrapper.orderByDesc(RiskDisposal::getCreateTime); } private RiskDisposalVO toVO(RiskDisposal entity) { 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 3a3de257..8dfe420f 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 @@ -244,6 +244,7 @@ public class TripQueryServiceImpl implements ITripQueryService { .or().ne(Waybill::getDriverAcceptStatus, WaybillProcessSupport.ACCEPT_REJECTED)); applyScope(wrapper, condition); applyDeptScope(wrapper, condition); + applyProjectScope(wrapper, condition); applyOrgTypeFilter(wrapper, condition); applyKeyword(wrapper, condition.getKeyword()); List rows = waybillService.list(wrapper); @@ -264,6 +265,18 @@ public class TripQueryServiceImpl implements ITripQueryService { wrapper.in(Waybill::getDeptId, condition.getDeptIds()); } + /** + * 项目收口:条件里给了项目 ID 集合时只出这些项目的运单行,空集合表示不过滤。 + *

+ * 小程序调度端先按用户数据权限解析可见项目(业务部门在范围内),再把项目 ID 传进来。 + */ + private void applyProjectScope(LambdaQueryWrapper wrapper, TripQueryVO condition) { + if (Func.isEmpty(condition.getProjectIds())) { + return; + } + wrapper.in(Waybill::getProjectId, condition.getProjectIds()); + } + /** * 运输组织类型按子单行筛:它在同一趟车里必然是同一个值(配载的每张子单带同一个配载单号, * 未配载的每张自成一趟车),所以这一层不会把一趟车拆开。 @@ -803,6 +816,7 @@ public class TripQueryServiceImpl implements ITripQueryService { copy.setDriverPhone(source.getDriverPhone()); copy.setAllDrivers(source.isAllDrivers()); copy.setDeptIds(source.getDeptIds()); + copy.setProjectIds(source.getProjectIds()); copy.setKeyword(source.getKeyword()); copy.setTransportOrgType(source.getTransportOrgType()); copy.setException(source.getException()); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/MpUserDataScopeSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/MpUserDataScopeSupport.java new file mode 100644 index 00000000..8336292a --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/MpUserDataScopeSupport.java @@ -0,0 +1,184 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.support; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.core.toolkit.support.SFunction; +import lombok.RequiredArgsConstructor; +import org.springblade.core.secure.utils.AuthUtil; +import org.springblade.core.tool.api.R; +import org.springblade.core.tool.utils.Func; +import org.springblade.system.feign.IMpUserClient; +import org.springblade.system.pojo.enums.MpUserIdentity; +import org.springblade.system.pojo.vo.MpUserDataScopeVO; +import org.springblade.system.utils.MpUserAuth; +import org.springblade.transport.pojo.entity.ProjectApply; +import org.springblade.transport.service.IProjectApplyService; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * 小程序调度端数据权限。 + *

+ * 口径:所属组织 + 数据权限范围 + 数据层级范围 → 可见组织 → + * 业务部门落在可见组织内的项目 → 再按这些项目收口运单/异常/风险。 + * + * @author Chill + */ +@Component +@RequiredArgsConstructor +public class MpUserDataScopeSupport { + + /** + * 无权限时的占位 ID:查询 in(-1) 保证查不到数据(空列表在运次查询里表示不过滤)。 + */ + private static final Long NONE_ID = -1L; + + private final IMpUserClient mpUserClient; + private final IProjectApplyService projectApplyService; + + /** + * 当前登录是否为小程序调度端用户(含 admin 身份) + */ + public boolean isMpDispatcher() { + return MpUserAuth.isMpUser() + && MpUserIdentity.has(AuthUtil.getUserRole(), MpUserIdentity.ADMIN); + } + + /** + * 解析当前小程序用户可见组织;非小程序用户返回 null(调用方走原逻辑)。 + */ + public MpUserDataScopeVO resolveCurrentMpUserScope() { + if (!MpUserAuth.isMpUser()) { + return null; + } + return resolveByUserId(AuthUtil.getUserId()); + } + + /** + * 解析指定小程序用户可见组织;Feign 失败时按无权限处理。 + */ + public MpUserDataScopeVO resolveByUserId(Long userId) { + if (Func.isEmpty(userId) || userId <= 0) { + return MpUserDataScopeVO.empty(); + } + R result = mpUserClient.resolveDataScope(userId); + if (result == null || !result.isSuccess() || result.getData() == null) { + return MpUserDataScopeVO.empty(); + } + return result.getData(); + } + + /** + * 转为查询用的组织 ID 约定:空=不过滤;非空=in;无权限=[-1]。 + */ + public List toQueryDeptIds(MpUserDataScopeVO scope) { + if (scope == null || scope.isAllDept()) { + return List.of(); + } + if (Func.isEmpty(scope.getDeptIds())) { + return List.of(NONE_ID); + } + return scope.getDeptIds(); + } + + /** + * 解析当前小程序用户可见项目 ID(业务部门落在数据权限组织范围内)。 + *

    + *
  • 空列表 = 全部组织 / 不过滤
  • + *
  • 非空 = 仅这些项目
  • + *
  • 无权限或范围内无项目 = [-1]
  • + *
+ * 非小程序用户返回 null。 + */ + public List resolveVisibleProjectIdsForQuery() { + MpUserDataScopeVO scope = resolveCurrentMpUserScope(); + if (scope == null) { + return null; + } + return resolveProjectIds(scope); + } + + /** + * 按数据权限结果解析可见项目 ID。 + */ + public List resolveProjectIds(MpUserDataScopeVO scope) { + if (scope == null || scope.isAllDept()) { + return List.of(); + } + List deptIds = scope.getDeptIds(); + if (Func.isEmpty(deptIds)) { + return List.of(NONE_ID); + } + List projectList = projectApplyService.list(Wrappers.lambdaQuery() + .select(ProjectApply::getId) + .in(ProjectApply::getBusinessDeptId, deptIds)); + if (Func.isEmpty(projectList)) { + return List.of(NONE_ID); + } + List projectIds = projectList.stream() + .map(ProjectApply::getId) + .filter(Objects::nonNull) + .distinct() + .collect(Collectors.toList()); + return Func.isEmpty(projectIds) ? List.of(NONE_ID) : projectIds; + } + + /** + * 当前为小程序调度用户时,按可见项目写入 project 列;非调度用户不改动。 + */ + public void applyProjectScopeIfMpDispatcher(LambdaQueryWrapper wrapper, SFunction projectColumn) { + if (!isMpDispatcher() || wrapper == null || projectColumn == null) { + return; + } + List projectIds = resolveVisibleProjectIdsForQuery(); + if (Func.isEmpty(projectIds)) { + return; + } + wrapper.in(projectColumn, projectIds); + } + + /** + * 风险等无 projectId 字段时:按关联运单的项目收口。 + */ + public void applyWaybillProjectScopeIfMpDispatcher(LambdaQueryWrapper wrapper, SFunction waybillIdColumn) { + if (!isMpDispatcher() || wrapper == null || waybillIdColumn == null) { + return; + } + List projectIds = resolveVisibleProjectIdsForQuery(); + if (Func.isEmpty(projectIds)) { + return; + } + String inList = projectIds.stream().map(String::valueOf).collect(Collectors.joining(",")); + wrapper.inSql(waybillIdColumn, + "SELECT id FROM blade_waybill WHERE is_deleted = 0 AND project_id IN (" + inList + ")"); + } + +} diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ExceptionDisposalListQueryTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ExceptionDisposalListQueryTest.java index 9d0dd4a1..75eca29d 100644 --- a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ExceptionDisposalListQueryTest.java +++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ExceptionDisposalListQueryTest.java @@ -52,10 +52,13 @@ class ExceptionDisposalListQueryTest { @BeforeEach void setUp() throws Exception { disposalMapper = mock(ExceptionDisposalMapper.class); + org.springblade.transport.support.MpUserDataScopeSupport scopeSupport = + mock(org.springblade.transport.support.MpUserDataScopeSupport.class); service = new ExceptionDisposalServiceImpl( mock(ExceptionDisposalFollowRecordMapper.class), mock(IWaybillService.class), - mock(IDriverWaybillService.class)); + mock(IDriverWaybillService.class), + scopeSupport); inject("baseMapper", disposalMapper); when(disposalMapper.selectPage(any(), any())).thenReturn(new Page(1, 10)); } diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ExceptionDisposalReportGateTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ExceptionDisposalReportGateTest.java index 4a64ae2a..6dc72185 100644 --- a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ExceptionDisposalReportGateTest.java +++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ExceptionDisposalReportGateTest.java @@ -71,7 +71,8 @@ class ExceptionDisposalReportGateTest { waybillService = mock(IWaybillService.class); driverWaybillService = mock(IDriverWaybillService.class); service = new ExceptionDisposalServiceImpl( - mock(ExceptionDisposalFollowRecordMapper.class), waybillService, driverWaybillService); + mock(ExceptionDisposalFollowRecordMapper.class), waybillService, driverWaybillService, + mock(org.springblade.transport.support.MpUserDataScopeSupport.class)); inject("baseMapper", disposalMapper); when(waybillService.getById(WAYBILL_ID)).thenReturn(waybill()); diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ManageAdminHomeFeedTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ManageAdminHomeFeedTest.java index 3b37513b..a0228fcd 100644 --- a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ManageAdminHomeFeedTest.java +++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ManageAdminHomeFeedTest.java @@ -84,7 +84,8 @@ class ManageAdminHomeFeedTest { mock(IRiskDisposalService.class), mock(IDriverWaybillService.class), mock(ITripQueryService.class), - mock(ILoadingManageService.class)); + mock(ILoadingManageService.class), + mock(org.springblade.transport.support.MpUserDataScopeSupport.class)); when(exceptionService.list(ArgumentMatchers.>any())) .thenReturn(new ArrayList<>()); when(waybillService.list(ArgumentMatchers.>any())) diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ManageTripListDetailTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ManageTripListDetailTest.java index b5771621..8f3f26d6 100644 --- a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ManageTripListDetailTest.java +++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ManageTripListDetailTest.java @@ -101,7 +101,8 @@ class ManageTripListDetailTest { mock(IRiskDisposalService.class), driverWaybillService, tripQueryService, - loadingManageService); + loadingManageService, + mock(org.springblade.transport.support.MpUserDataScopeSupport.class)); when(exceptionDisposalService.list(ArgumentMatchers.>any())) .thenReturn(new ArrayList<>()); when(waybillService.list(ArgumentMatchers.>any())).thenReturn(new ArrayList<>()); diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ManageWaybillOrgScopeTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ManageWaybillOrgScopeTest.java index 95346246..ad8d8b48 100644 --- a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ManageWaybillOrgScopeTest.java +++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/ManageWaybillOrgScopeTest.java @@ -29,6 +29,7 @@ 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 org.springblade.transport.support.MpUserDataScopeSupport; import java.util.ArrayList; import java.util.HashSet; @@ -80,6 +81,8 @@ class ManageWaybillOrgScopeTest { waybillService = mock(IWaybillService.class); tripQueryService = mock(ITripQueryService.class); IExceptionDisposalService exceptionDisposalService = mock(IExceptionDisposalService.class); + MpUserDataScopeSupport mpUserDataScopeSupport = mock(MpUserDataScopeSupport.class); + when(mpUserDataScopeSupport.resolveVisibleProjectIdsForQuery()).thenReturn(null); service = new ManageWaybillServiceImpl( waybillService, mock(IDriverService.class), @@ -87,7 +90,8 @@ class ManageWaybillOrgScopeTest { mock(IRiskDisposalService.class), mock(IDriverWaybillService.class), tripQueryService, - mock(ILoadingManageService.class)); + mock(ILoadingManageService.class), + mpUserDataScopeSupport); when(exceptionDisposalService.list(ArgumentMatchers.>any())) .thenReturn(new ArrayList<>()); when(waybillService.list(ArgumentMatchers.>any())).thenReturn(new ArrayList<>()); diff --git a/doc/sql/transport/blade_mp_user_20261012.sql b/doc/sql/transport/blade_mp_user_20261012.sql index 9d34d49c..32e2d94a 100644 --- a/doc/sql/transport/blade_mp_user_20261012.sql +++ b/doc/sql/transport/blade_mp_user_20261012.sql @@ -18,6 +18,8 @@ CREATE TABLE IF NOT EXISTS `blade_mp_user` ( `openid` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信小程序 openid', `identities` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '小程序身份,逗号分隔:driver,admin', `dept_id` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '部门id', + `data_scope_range` tinyint NOT NULL DEFAULT 1 COMMENT '数据权限范围:1所属组织,2全部组织,3自定义', + `data_level_range` tinyint NOT NULL DEFAULT 1 COMMENT '数据层级范围:1包含下级,2仅本级', `post_id` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '岗位id', `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', `create_user` bigint NULL DEFAULT NULL COMMENT '创建人', diff --git a/doc/sql/transport/blade_mp_user_data_scope_20261021.sql b/doc/sql/transport/blade_mp_user_data_scope_20261021.sql new file mode 100644 index 00000000..5eba53c1 --- /dev/null +++ b/doc/sql/transport/blade_mp_user_data_scope_20261021.sql @@ -0,0 +1,12 @@ +-- 小程序用户:数据权限范围、数据层级范围、自定义组织数据权限 +ALTER TABLE `blade_mp_user` + ADD COLUMN `data_scope_range` tinyint NOT NULL DEFAULT 1 COMMENT '数据权限范围:1所属组织,2全部组织,3自定义' AFTER `dept_id`, + ADD COLUMN `data_level_range` tinyint NOT NULL DEFAULT 1 COMMENT '数据层级范围:1包含下级,2仅本级' AFTER `data_scope_range`; + +CREATE TABLE IF NOT EXISTS `blade_mp_user_data_scope` ( + `id` bigint NOT NULL COMMENT '主键', + `user_id` bigint NOT NULL COMMENT '小程序用户主键', + `dept_id` bigint NOT NULL COMMENT '组织主键', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `uk_blade_mp_user_data_scope_user_dept` (`user_id`, `dept_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='小程序用户组织数据权限';