✨ 小程序用户支持组织与数据权限,调度端按项目业务部门收口运单
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+3
@@ -56,6 +56,9 @@ public class TripQueryVO implements Serializable {
|
||||
@Schema(description = "可见组织 ID 集合:非空时只出这些组织的运单行,空集合表示不按组织过滤")
|
||||
private List<Long> deptIds;
|
||||
|
||||
@Schema(description = "可见项目 ID 集合:非空时只出这些项目的运单行,空集合表示不按项目过滤(小程序调度端优先用此口径)")
|
||||
private List<Long> projectIds;
|
||||
|
||||
@Schema(description = "状态:0待接单/1进行中/2已完成/3已取消,不传为全部(不含已取消)")
|
||||
private Integer status;
|
||||
|
||||
|
||||
+8
@@ -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<MpUser> userById(@RequestParam("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 解析小程序用户可见组织(所属组织 + 数据权限范围 + 数据层级范围)
|
||||
*/
|
||||
@GetMapping(RESOLVE_DATA_SCOPE)
|
||||
R<MpUserDataScopeVO> resolveDataScope(@RequestParam("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 按租户 + 手机号查询
|
||||
*/
|
||||
|
||||
+19
-1
@@ -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<Long> dataScopeDeptIds;
|
||||
/**
|
||||
* 岗位id
|
||||
*/
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 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;
|
||||
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* 小程序用户数据权限解析结果
|
||||
* <p>
|
||||
* 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<Long> 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<Long> deptIds) {
|
||||
MpUserDataScopeVO scope = new MpUserDataScopeVO();
|
||||
scope.setAllDept(false);
|
||||
scope.setDeptIds(deptIds == null ? new ArrayList<>() : new ArrayList<>(deptIds));
|
||||
return scope;
|
||||
}
|
||||
|
||||
}
|
||||
+7
@@ -42,6 +42,13 @@ public class MpUserVO extends MpUser {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 所属组织名称(逗号分隔)
|
||||
*/
|
||||
@TableField(exist = false)
|
||||
@Schema(description = "所属组织名称")
|
||||
private String deptName;
|
||||
|
||||
/**
|
||||
* 身份名称(逗号分隔中文)
|
||||
*/
|
||||
|
||||
+2
-1
@@ -71,7 +71,8 @@ public class MpUserController extends BladeController {
|
||||
@ApiOperationSupport(order = 1)
|
||||
@Operation(summary = "详情", description = "传入id")
|
||||
public R<MpUserVO> 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("小程序用户不存在");
|
||||
}
|
||||
|
||||
+8
@@ -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<MpUserDataScopeVO> resolveDataScope(@RequestParam("userId") Long userId) {
|
||||
return R.data(mpUserService.resolveDataScope(userId));
|
||||
}
|
||||
|
||||
@Override
|
||||
@GetMapping(USER_BY_PHONE)
|
||||
public R<MpUser> userByPhone(String tenantId, String phone) {
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 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<MpUserDataScope> {
|
||||
}
|
||||
+13
@@ -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<MpUser> {
|
||||
*/
|
||||
IPage<MpUser> selectMpUserPage(IPage<MpUser> page, MpUser query);
|
||||
|
||||
/**
|
||||
* 详情(含自定义数据权限组织)
|
||||
*/
|
||||
MpUser detail(Long id);
|
||||
|
||||
/**
|
||||
* 解析小程序用户可见组织(所属组织 + 数据权限范围 + 数据层级范围)
|
||||
* <p>
|
||||
* allDept=true 表示全部组织;否则 deptIds 为可见组织,空列表表示无权限。
|
||||
*/
|
||||
MpUserDataScopeVO resolveDataScope(Long userId);
|
||||
|
||||
/**
|
||||
* 保存小程序用户
|
||||
*/
|
||||
|
||||
+152
-2
@@ -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<MpUserMapper, MpUser> 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<MpUserMapper, MpUser> 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<Long> baseDeptIds;
|
||||
if (Objects.equals(dataScopeRange, DATA_SCOPE_CUSTOM)) {
|
||||
baseDeptIds = mpUserDataScopeMapper.selectList(Wrappers.<MpUserDataScope>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<Long> allowedDeptIds = new LinkedHashSet<>();
|
||||
for (Long deptId : baseDeptIds) {
|
||||
if (Func.isEmpty(deptId) || deptId <= 0) {
|
||||
continue;
|
||||
}
|
||||
if (includeChildren) {
|
||||
List<Long> 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<MpUserMapper, MpUser> 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<MpUserMapper, MpUser> 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<MpUserMapper, MpUser> 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<MpUserMapper, MpUser> imp
|
||||
}
|
||||
boolean removed = deleteLogic(ids);
|
||||
if (removed) {
|
||||
mpUserDataScopeMapper.delete(Wrappers.<MpUserDataScope>lambdaQuery().in(MpUserDataScope::getUserId, ids));
|
||||
MpUserCache.clear();
|
||||
}
|
||||
return removed;
|
||||
@@ -314,6 +402,15 @@ public class MpUserServiceImpl extends BaseServiceImpl<MpUserMapper, MpUser> 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<MpUserMapper, MpUser> 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<MpUserMapper, MpUser> 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.<MpUserDataScope>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.<MpUserDataScope>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<MpUserMapper, MpUser> 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("*");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+4
@@ -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<MpUser, MpUserVO> {
|
||||
// 密码哈希不下发前端
|
||||
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()));
|
||||
}
|
||||
|
||||
+7
-1
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
+102
-22
@@ -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;
|
||||
* 列表与详情与司机端同粒度:取数、状态聚合、接单需求合并都调共享运次读服务,
|
||||
* 本类只做字段裁剪与调度端独有的筛选(组织类型 / 异常 / 创建时间 / 待重新派单)。
|
||||
* <p>
|
||||
* 小程序调度账号(如「小程序管理」)组织常与运单业务组织不一致,故不做 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<Long> 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<Long> 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 {
|
||||
}
|
||||
|
||||
/**
|
||||
* 调度端可见组织:普通用户(含各级角色)只出自己所属组织,超级管理员不过滤。
|
||||
* <p>
|
||||
* 小程序不校验「所属组织不能为空」:账号没有组织时不按组织收口,避免首页、待处理等接口直接报错。
|
||||
* 详情与重新派单本身不走这里。
|
||||
* 调度端数据权限落到运次查询条件:小程序按可见项目,系统用户按所属组织。
|
||||
*/
|
||||
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<Waybill> wrapper) {
|
||||
if (wrapper == null) {
|
||||
return;
|
||||
}
|
||||
if (MpUserAuth.isMpUser()) {
|
||||
List<Long> projectIds = manageableProjectIds();
|
||||
if (Func.isNotEmpty(projectIds)) {
|
||||
wrapper.in("project_id", projectIds);
|
||||
}
|
||||
return;
|
||||
}
|
||||
List<Long> deptIds = manageableDeptIds();
|
||||
if (Func.isNotEmpty(deptIds)) {
|
||||
wrapper.in("dept_id", deptIds);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 小程序调度端可见项目:所属组织 + 数据权限范围 + 数据层级范围 → 业务部门在范围内的项目。
|
||||
* 空列表不过滤;[-1] 表示无权限。
|
||||
*/
|
||||
private List<Long> manageableProjectIds() {
|
||||
List<Long> projectIds = mpUserDataScopeSupport.resolveVisibleProjectIdsForQuery();
|
||||
return projectIds == null ? List.of() : projectIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统用户可见组织:普通账号只出所属组织,超管不过滤;组织为空不拦截。
|
||||
*/
|
||||
private List<Long> manageableDeptIds() {
|
||||
if (TransportBusinessSupport.isAdministrator()) {
|
||||
@@ -911,10 +951,12 @@ public class ManageWaybillServiceImpl implements IManageWaybillService {
|
||||
}
|
||||
|
||||
private Set<Long> loadIncompleteExceptionWaybillIds() {
|
||||
List<ExceptionDisposal> list = exceptionDisposalService.list(Wrappers.<ExceptionDisposal>lambdaQuery()
|
||||
LambdaQueryWrapper<ExceptionDisposal> wrapper = Wrappers.<ExceptionDisposal>lambdaQuery()
|
||||
.select(ExceptionDisposal::getWaybillId)
|
||||
.in(ExceptionDisposal::getDisposalStatus, DISPOSAL_PENDING, DISPOSAL_PROCESSING)
|
||||
.isNotNull(ExceptionDisposal::getWaybillId));
|
||||
.isNotNull(ExceptionDisposal::getWaybillId);
|
||||
applyExceptionDeptScope(wrapper);
|
||||
List<ExceptionDisposal> list = exceptionDisposalService.list(wrapper);
|
||||
Set<Long> 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<ExceptionDisposal> wrapper) {
|
||||
if (MpUserAuth.isMpUser()) {
|
||||
List<Long> projectIds = manageableProjectIds();
|
||||
if (Func.isNotEmpty(projectIds)) {
|
||||
wrapper.in(ExceptionDisposal::getProjectId, projectIds);
|
||||
}
|
||||
return;
|
||||
}
|
||||
List<Long> deptIds = manageableDeptIds();
|
||||
if (Func.isNotEmpty(deptIds)) {
|
||||
wrapper.in(ExceptionDisposal::getDeptId, deptIds);
|
||||
}
|
||||
}
|
||||
|
||||
/** 风险无项目字段:小程序按关联运单的项目收口;系统用户按组织 */
|
||||
private void applyRiskDeptScope(LambdaQueryWrapper<RiskDisposal> wrapper) {
|
||||
if (MpUserAuth.isMpUser()) {
|
||||
List<Long> 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<Long> 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.<ExceptionDisposal>lambdaQuery()
|
||||
.in(ExceptionDisposal::getDisposalStatus, DISPOSAL_PENDING, DISPOSAL_PROCESSING));
|
||||
LambdaQueryWrapper<ExceptionDisposal> wrapper = Wrappers.<ExceptionDisposal>lambdaQuery()
|
||||
.in(ExceptionDisposal::getDisposalStatus, DISPOSAL_PENDING, DISPOSAL_PROCESSING);
|
||||
applyExceptionDeptScope(wrapper);
|
||||
return exceptionDisposalService.count(wrapper);
|
||||
}
|
||||
|
||||
private long countPendingRisks() {
|
||||
return riskDisposalService.count(Wrappers.<RiskDisposal>lambdaQuery()
|
||||
.eq(RiskDisposal::getDisposalStatus, RISK_PENDING));
|
||||
LambdaQueryWrapper<RiskDisposal> wrapper = Wrappers.<RiskDisposal>lambdaQuery()
|
||||
.eq(RiskDisposal::getDisposalStatus, RISK_PENDING);
|
||||
applyRiskDeptScope(wrapper);
|
||||
return riskDisposalService.count(wrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1099,10 +1177,12 @@ public class ManageWaybillServiceImpl implements IManageWaybillService {
|
||||
}
|
||||
|
||||
private List<TimedTodo> buildExceptionFeed() {
|
||||
List<ExceptionDisposal> list = exceptionDisposalService.list(Wrappers.<ExceptionDisposal>lambdaQuery()
|
||||
LambdaQueryWrapper<ExceptionDisposal> wrapper = Wrappers.<ExceptionDisposal>lambdaQuery()
|
||||
.in(ExceptionDisposal::getDisposalStatus, DISPOSAL_PENDING, DISPOSAL_PROCESSING)
|
||||
.orderByDesc(ExceptionDisposal::getReportTime)
|
||||
.last("LIMIT " + FEED_LIMIT));
|
||||
.last("LIMIT " + FEED_LIMIT);
|
||||
applyExceptionDeptScope(wrapper);
|
||||
List<ExceptionDisposal> list = exceptionDisposalService.list(wrapper);
|
||||
return list.stream()
|
||||
.map(disposal -> new TimedTodo(toTodoItem(disposal), exceptionTimeOf(disposal)))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
+9
-4
@@ -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<RiskDisposalMapper, RiskDisposal> 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<RiskDisposalVO> selectPage(IPage<RiskDisposal> page, RiskDisposalVO query) {
|
||||
IPage<RiskDisposal> entityPage = page(page, buildQuery(query));
|
||||
@@ -84,15 +88,16 @@ public class RiskDisposalServiceImpl extends BaseServiceImpl<RiskDisposalMapper,
|
||||
}
|
||||
|
||||
private LambdaQueryWrapper<RiskDisposal> buildQuery(RiskDisposalVO query) {
|
||||
return Wrappers.<RiskDisposal>lambdaQuery()
|
||||
LambdaQueryWrapper<RiskDisposal> wrapper = Wrappers.<RiskDisposal>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) {
|
||||
|
||||
+14
@@ -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<Waybill> rows = waybillService.list(wrapper);
|
||||
@@ -264,6 +265,18 @@ public class TripQueryServiceImpl implements ITripQueryService {
|
||||
wrapper.in(Waybill::getDeptId, condition.getDeptIds());
|
||||
}
|
||||
|
||||
/**
|
||||
* 项目收口:条件里给了项目 ID 集合时只出这些项目的运单行,空集合表示不过滤。
|
||||
* <p>
|
||||
* 小程序调度端先按用户数据权限解析可见项目(业务部门在范围内),再把项目 ID 传进来。
|
||||
*/
|
||||
private void applyProjectScope(LambdaQueryWrapper<Waybill> 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());
|
||||
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
* <p>
|
||||
* Use of this software is governed by the Commercial License Agreement
|
||||
* obtained after purchasing a license from BladeX.
|
||||
* <p>
|
||||
* 1. This software is for development use only under a valid license
|
||||
* from BladeX.
|
||||
* <p>
|
||||
* 2. Redistribution of this software's source code to any third party
|
||||
* without a commercial license is strictly prohibited.
|
||||
* <p>
|
||||
* 3. Licensees may copyright their own code but cannot use segments
|
||||
* from this software for such purposes. Copyright of this software
|
||||
* remains with BladeX.
|
||||
* <p>
|
||||
* Using this software signifies agreement to this License, and the software
|
||||
* must not be used for illegal purposes.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
|
||||
* not liable for any claims arising from secondary or illegal development.
|
||||
* <p>
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* 小程序调度端数据权限。
|
||||
* <p>
|
||||
* 口径:所属组织 + 数据权限范围 + 数据层级范围 → 可见组织 →
|
||||
* 业务部门落在可见组织内的项目 → 再按这些项目收口运单/异常/风险。
|
||||
*
|
||||
* @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<MpUserDataScopeVO> result = mpUserClient.resolveDataScope(userId);
|
||||
if (result == null || !result.isSuccess() || result.getData() == null) {
|
||||
return MpUserDataScopeVO.empty();
|
||||
}
|
||||
return result.getData();
|
||||
}
|
||||
|
||||
/**
|
||||
* 转为查询用的组织 ID 约定:空=不过滤;非空=in;无权限=[-1]。
|
||||
*/
|
||||
public List<Long> 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(业务部门落在数据权限组织范围内)。
|
||||
* <ul>
|
||||
* <li>空列表 = 全部组织 / 不过滤</li>
|
||||
* <li>非空 = 仅这些项目</li>
|
||||
* <li>无权限或范围内无项目 = [-1]</li>
|
||||
* </ul>
|
||||
* 非小程序用户返回 null。
|
||||
*/
|
||||
public List<Long> resolveVisibleProjectIdsForQuery() {
|
||||
MpUserDataScopeVO scope = resolveCurrentMpUserScope();
|
||||
if (scope == null) {
|
||||
return null;
|
||||
}
|
||||
return resolveProjectIds(scope);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按数据权限结果解析可见项目 ID。
|
||||
*/
|
||||
public List<Long> resolveProjectIds(MpUserDataScopeVO scope) {
|
||||
if (scope == null || scope.isAllDept()) {
|
||||
return List.of();
|
||||
}
|
||||
List<Long> deptIds = scope.getDeptIds();
|
||||
if (Func.isEmpty(deptIds)) {
|
||||
return List.of(NONE_ID);
|
||||
}
|
||||
List<ProjectApply> projectList = projectApplyService.list(Wrappers.<ProjectApply>lambdaQuery()
|
||||
.select(ProjectApply::getId)
|
||||
.in(ProjectApply::getBusinessDeptId, deptIds));
|
||||
if (Func.isEmpty(projectList)) {
|
||||
return List.of(NONE_ID);
|
||||
}
|
||||
List<Long> projectIds = projectList.stream()
|
||||
.map(ProjectApply::getId)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
return Func.isEmpty(projectIds) ? List.of(NONE_ID) : projectIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前为小程序调度用户时,按可见项目写入 project 列;非调度用户不改动。
|
||||
*/
|
||||
public <T> void applyProjectScopeIfMpDispatcher(LambdaQueryWrapper<T> wrapper, SFunction<T, ?> projectColumn) {
|
||||
if (!isMpDispatcher() || wrapper == null || projectColumn == null) {
|
||||
return;
|
||||
}
|
||||
List<Long> projectIds = resolveVisibleProjectIdsForQuery();
|
||||
if (Func.isEmpty(projectIds)) {
|
||||
return;
|
||||
}
|
||||
wrapper.in(projectColumn, projectIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* 风险等无 projectId 字段时:按关联运单的项目收口。
|
||||
*/
|
||||
public <T> void applyWaybillProjectScopeIfMpDispatcher(LambdaQueryWrapper<T> wrapper, SFunction<T, ?> waybillIdColumn) {
|
||||
if (!isMpDispatcher() || wrapper == null || waybillIdColumn == null) {
|
||||
return;
|
||||
}
|
||||
List<Long> 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 + ")");
|
||||
}
|
||||
|
||||
}
|
||||
+4
-1
@@ -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<ExceptionDisposal>(1, 10));
|
||||
}
|
||||
|
||||
+2
-1
@@ -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());
|
||||
|
||||
+2
-1
@@ -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.<Wrapper<ExceptionDisposal>>any()))
|
||||
.thenReturn(new ArrayList<>());
|
||||
when(waybillService.list(ArgumentMatchers.<Wrapper<Waybill>>any()))
|
||||
|
||||
+2
-1
@@ -101,7 +101,8 @@ class ManageTripListDetailTest {
|
||||
mock(IRiskDisposalService.class),
|
||||
driverWaybillService,
|
||||
tripQueryService,
|
||||
loadingManageService);
|
||||
loadingManageService,
|
||||
mock(org.springblade.transport.support.MpUserDataScopeSupport.class));
|
||||
when(exceptionDisposalService.list(ArgumentMatchers.<Wrapper<ExceptionDisposal>>any()))
|
||||
.thenReturn(new ArrayList<>());
|
||||
when(waybillService.list(ArgumentMatchers.<Wrapper<Waybill>>any())).thenReturn(new ArrayList<>());
|
||||
|
||||
+5
-1
@@ -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.<Wrapper<ExceptionDisposal>>any()))
|
||||
.thenReturn(new ArrayList<>());
|
||||
when(waybillService.list(ArgumentMatchers.<Wrapper<Waybill>>any())).thenReturn(new ArrayList<>());
|
||||
|
||||
@@ -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 '创建人',
|
||||
|
||||
@@ -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='小程序用户组织数据权限';
|
||||
Reference in New Issue
Block a user