🔀 合并主干 master 到 dev

- 状态命名统一用 dev 的 pending_return_confirm:master 侧 pending_return 常量、字面量与文案分支一并改名,避免两套状态并存
- 汇票可选按 master 放开:去掉部门/收付款方限制,删除随之失效的 selectableBill 判定
- 结算导入模板按 master 重写:不预置费用项列、不再有结算金额列,导出签名收为两参
- 合入 master 的待确认回单批量完成、变更记录审批过滤、小程序数据权限(MpUserDataScope)与评分量化/客商档案改动
- 受影响的测试构造器补上 MpUserDataScopeSupport 依赖
This commit is contained in:
2026-10-24 16:37:36 +08:00
86 changed files with 2379 additions and 357 deletions
@@ -260,6 +260,17 @@ public class DeptController extends BladeController {
return R.data(deptService.listPlatformCompany());
}
/**
* 业务部门下拉(是否业务部门=是)
*/
@PreAuth(AuthConstant.PERMIT_ALL)
@GetMapping("/business-dept-select")
@ApiOperationSupport(order = 12)
@Operation(summary = "业务部门下拉", description = "返回是否业务部门=是的部门列表")
public R<List<Dept>> businessDeptSelect() {
return R.data(deptService.listBusinessDept());
}
/**
* 获取部门的主管信息
*/
@@ -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("小程序用户不存在");
}
@@ -29,10 +29,12 @@ import org.springblade.core.tool.api.R;
import org.springblade.system.pojo.dto.MpUserEnsureDTO;
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;
/**
@@ -54,6 +56,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) {
@@ -20,6 +20,7 @@
<result column="mnemonic_code" property="mnemonicCode"/>
<result column="carrier_customer_id" property="carrierCustomerId"/>
<result column="is_platform_company" property="isPlatformCompany"/>
<result column="is_business_dept" property="isBusinessDept"/>
<result column="sort" property="sort"/>
<result column="remark" property="remark"/>
<result column="status" property="status"/>
@@ -43,6 +44,7 @@
<result column="mnemonic_code" property="mnemonicCode"/>
<result column="carrier_customer_id" property="carrierCustomerId"/>
<result column="is_platform_company" property="isPlatformCompany"/>
<result column="is_business_dept" property="isBusinessDept"/>
<result column="sort" property="sort"/>
<result column="remark" property="remark"/>
<result column="status" property="status"/>
@@ -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> {
}
@@ -106,6 +106,13 @@ public interface IDeptService extends IService<Dept> {
*/
List<Dept> listPlatformCompany();
/**
* 业务部门下拉(是否业务部门=是)
*
* @return 业务部门列表
*/
List<Dept> listBusinessDept();
/**
* 根据部门名称精确匹配获取部门ID集合
*
@@ -27,6 +27,7 @@ import org.springblade.core.mp.base.BaseService;
import org.springblade.system.pojo.dto.MpUserEnsureDTO;
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;
@@ -80,6 +81,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);
/**
* 保存小程序用户
*/
@@ -337,6 +337,18 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
return list(queryWrapper);
}
@Override
public List<Dept> listBusinessDept() {
LambdaQueryWrapper<Dept> queryWrapper = Wrappers.<Dept>lambdaQuery()
.eq(Dept::getIsBusinessDept, 1)
.orderByAsc(Dept::getSort)
.orderByAsc(Dept::getId);
if (!AuthUtil.isAdministrator()) {
queryWrapper.eq(Dept::getTenantId, AuthUtil.getTenantId());
}
return list(queryWrapper);
}
@Override
public String getDeptIds(String tenantId, String deptNames) {
List<Dept> deptList = baseMapper.selectList(Wrappers.<Dept>query().lambda().eq(Dept::getTenantId, tenantId).in(Dept::getDeptName, Func.toStrList(deptNames)));
@@ -443,6 +455,7 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
rootDept.setIsDeleted(BladeConstant.DB_NOT_DELETED);
rootDept.setIsOa(0);
rootDept.setIsPlatformCompany(0);
rootDept.setIsBusinessDept(0);
rootDept.setSyncTime(new Date());
this.save(rootDept);
log.info("已创建 OA 同步集团顶级 id={} name={} code={}",
@@ -536,6 +549,7 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
newRoot.setIsDeleted(BladeConstant.DB_NOT_DELETED);
newRoot.setIsOa(0);
newRoot.setIsPlatformCompany(0);
newRoot.setIsBusinessDept(0);
newRoot.setSyncTime(new Date());
this.save(newRoot);
log.info("迁移创建新集团顶级 id={}(来自历史 id={})", newRootId, legacyRootId);
@@ -598,6 +612,7 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
externalDept.setIsDeleted(BladeConstant.DB_NOT_DELETED);
externalDept.setIsOa(0);
externalDept.setIsPlatformCompany(0);
externalDept.setIsBusinessDept(0);
externalDept.setSyncTime(new Date());
this.save(externalDept);
log.info("已创建外部组织顶级 id={}", externalDept.getId());
@@ -678,6 +693,9 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
if (dept.getIsPlatformCompany() == null) {
dept.setIsPlatformCompany(0);
}
if (dept.getIsBusinessDept() == null) {
dept.setIsBusinessDept(0);
}
// 排序未填时默认 1
if (dept.getSort() == null) {
dept.setSort(1);
@@ -804,6 +822,7 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
dept.setIsDeleted(BladeConstant.DB_NOT_DELETED);
dept.setIsOa(1);
dept.setIsPlatformCompany(0);
dept.setIsBusinessDept(0);
dept.setSyncTime(new Date());
boolean saved = save(dept);
if (saved) {
@@ -877,7 +896,9 @@ public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements ID
private void validateContractInitialSerial(Dept dept) {
Integer serial = dept.getContractInitialSerial();
if (serial == null) {
// -1 为空占位,未填写时按 null 落库,不自动保留 -1
if (serial == null || serial == -1) {
dept.setContractInitialSerial(null);
return;
}
if (serial < 1 || serial > 999) {
@@ -38,16 +38,21 @@ import org.springblade.core.tool.utils.Func;
import org.springblade.core.tool.utils.StringUtil;
import org.springblade.core.tool.utils.WebUtil;
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.dto.MpUserEnsureDTO;
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;
@@ -72,6 +77,19 @@ public class MpUserServiceImpl extends BaseServiceImpl<MpUserMapper, MpUser> imp
*/
private static final String TENANT_HEADER = "Tenant-Id";
/** 数据权限范围:仅所属组织 */
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)) {
@@ -213,6 +231,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) {
@@ -223,7 +299,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();
}
@@ -236,7 +312,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())) {
@@ -244,7 +331,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();
}
@@ -268,6 +355,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;
@@ -422,6 +510,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());
}
@@ -455,6 +552,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("小程序身份不能为空");
}
@@ -467,6 +567,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) {
@@ -485,4 +621,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("*");
}
}
@@ -935,7 +935,9 @@ public class OASyncServiceImpl implements IOASyncService {
.ne(Dept::getOaId, "")
.select(Dept::getId, Dept::getDeptCode, Dept::getDeptName, Dept::getFullName,
Dept::getOaModified, Dept::getParentId, Dept::getOaId, Dept::getBelongCompanyCode,
Dept::getParentCode, Dept::getOaSupSubComId));
Dept::getParentCode, Dept::getOaSupSubComId,
Dept::getContractCodePrefix, Dept::getContractInitialSerial,
Dept::getIsPlatformCompany, Dept::getIsBusinessDept));
Map<String, Dept> byOaId = new HashMap<>();
for (Dept dept : deptList) {
if (StringUtils.isNotBlank(dept.getOaId())) {
@@ -998,6 +1000,11 @@ public class OASyncServiceImpl implements IOASyncService {
} else {
dept.setId(DefaultIdentifierGenerator.getInstance().nextId(null));
}
// OA 不同步合同本地配置:保留已有值,并把 -1 空占位规范为 null,避免 ALWAYS 策略写回 -1/清空
preserveLocalContractFields(dept, existing);
// OA 不同步「是否平台公司 / 是否业务部门」:更新时强制保留本地已配置值,新增默认否
preserveLocalBizFlags(dept, existing);
normalizeContractInitialSerial(dept);
if (StringUtils.isNotBlank(dept.getDeptCode())) {
deptIdByCode.put(dept.getDeptCode(), dept.getId());
}
@@ -1058,6 +1065,55 @@ public class OASyncServiceImpl implements IOASyncService {
return new OrgSyncCount(syncedCount, skippedCount, notHandledList);
}
/**
* OA 同步对象不含合同本地配置,更新时保留库中已有合同编码前缀与初始流水号。
*/
private void preserveLocalContractFields(Dept incoming, Dept existing) {
if (incoming == null || existing == null) {
return;
}
if (StringUtils.isBlank(incoming.getContractCodePrefix())) {
incoming.setContractCodePrefix(existing.getContractCodePrefix());
}
Integer incomingSerial = incoming.getContractInitialSerial();
if (incomingSerial == null || incomingSerial == -1) {
incoming.setContractInitialSerial(existing.getContractInitialSerial());
}
}
/**
* OA 同步不携带本地业务标志:更新时保留已配置的「是否平台公司 / 是否业务部门」,新增默认否。
*/
private void preserveLocalBizFlags(Dept incoming, Dept existing) {
if (incoming == null) {
return;
}
if (existing != null) {
incoming.setIsPlatformCompany(existing.getIsPlatformCompany() != null ? existing.getIsPlatformCompany() : 0);
incoming.setIsBusinessDept(existing.getIsBusinessDept() != null ? existing.getIsBusinessDept() : 0);
return;
}
if (incoming.getIsPlatformCompany() == null) {
incoming.setIsPlatformCompany(0);
}
if (incoming.getIsBusinessDept() == null) {
incoming.setIsBusinessDept(0);
}
}
/**
* 合同初始流水号 -1 为空占位,同步落库前规范为 null。
*/
private void normalizeContractInitialSerial(Dept dept) {
if (dept == null) {
return;
}
Integer serial = dept.getContractInitialSerial();
if (serial != null && serial == -1) {
dept.setContractInitialSerial(null);
}
}
/**
* oaModified 一致则跳过更新;任一侧为空无法确认一致,执行更新以补齐字段。
* 组织编码已按新规则重算、父子挂载不一致、或名称字段与当前映射不一致时,即使 modified 相同也要更新。
@@ -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()));
}