diff --git a/src/main/java/com/yunhai/userbase/controller/api/BaseController.java b/src/main/java/com/yunhai/userbase/controller/api/BaseController.java
index ee159c5..3cc1135 100644
--- a/src/main/java/com/yunhai/userbase/controller/api/BaseController.java
+++ b/src/main/java/com/yunhai/userbase/controller/api/BaseController.java
@@ -3,6 +3,7 @@ package com.yunhai.userbase.controller.api;
import com.yunhai.platform.utils.model.BoaUser;
import com.yunhai.platform.utils.enums.ClientEnum;
import com.yunhai.platform.utils.enums.IEnum;
+import com.yunhai.platform.apistd.ApiResponse;
import com.yunhai.userbase.config.Config;
import com.yunhai.userbase.enums.*;
import com.yunhai.userbase.utils.*;
@@ -104,19 +105,22 @@ public class BaseController {
* 上传成功后可直接用返回的 URL 访问,无需再改资源映射配置。
* 访问路径:POST {Config.API_PATH}/base/UploadFile(multipart/form-data)。
* 支持 type:image / file / icon / word(html 被 FileUtil 黑名单禁止上传)。
+ * 返回 {@link ApiResponse}({code,msg,data},data=文件 URL):
+ * String 返回类型会走 StringHttpMessageConverter,被 ApiResponseAdvice 跳过包装,
+ * 前端 Upload 组件拿到裸字符串无法按统一结构取 data。
*
* @param file 上传文件(必填,单文件上限 50MB,见 application.yml 的 multipart 配置)
* @param type 存储子目录类型,默认 image
* @return 文件对外可访问的 Web URL
*/
@PostMapping("UploadFile")
- public String uploadFile(@RequestParam("file") MultipartFile file,
- @RequestParam(value = "type", defaultValue = "image") String type) {
+ public ApiResponse uploadFile(@RequestParam("file") MultipartFile file,
+ @RequestParam(value = "type", defaultValue = "image") String type) {
if (!ALLOWED_UPLOAD_TYPES.contains(type)) {
throw new DException("不支持的上传类型: " + type + ",可选: image/file/icon/word");
}
// FileUtil.upload 内部已处理空文件校验、危险后缀黑名单、目录创建与访问 URL 拼接
- return FileUtil.upload(file, type);
+ return ApiResponse.success(FileUtil.upload(file, type));
}
/**
diff --git a/src/main/java/com/yunhai/userbase/model/groupauthz/UserPermissionUserVo.java b/src/main/java/com/yunhai/userbase/model/groupauthz/UserPermissionUserVo.java
index 4ec8fb0..b9a365e 100644
--- a/src/main/java/com/yunhai/userbase/model/groupauthz/UserPermissionUserVo.java
+++ b/src/main/java/com/yunhai/userbase/model/groupauthz/UserPermissionUserVo.java
@@ -20,6 +20,9 @@ public class UserPermissionUserVo {
/** 是否主归属:1=本职主学校/部门,0=兼职兼任。 */
private Integer isMain;
+ /** 该行对应的组ID(同一用户多组时每个组出一行,用于按组查看权限)。 */
+ private Integer groupId;
+
/** 用户所属部门/组织路径,如 /上海市仙霞高级中学/教师/行政。 */
private String groupPath;
}
diff --git a/src/main/java/com/yunhai/userbase/repository/SysUserRoleRelRepository.java b/src/main/java/com/yunhai/userbase/repository/SysUserRoleRelRepository.java
index ca4bccf..7da8e27 100644
--- a/src/main/java/com/yunhai/userbase/repository/SysUserRoleRelRepository.java
+++ b/src/main/java/com/yunhai/userbase/repository/SysUserRoleRelRepository.java
@@ -30,8 +30,15 @@ public interface SysUserRoleRelRepository extends JpaRepository findByUserIdAndSchoolIdAndStatus(Integer userId, Integer schoolId, StatusEnum status);
- /** 删除用户在某学校下的全部身份行(按 schoolId 全量覆盖保存用) */
- void deleteByUserIdAndSchoolId(Integer userId, Integer schoolId);
+ /**
+ * 删除用户在某学校下的全部身份行(按 schoolId 全量覆盖保存用)。
+ * 必须 bulk 立即发 SQL:派生 delete 只把删除动作入队,同一事务内随后 saveAll 的 INSERT
+ * 在刷新时会先于 DELETE 执行(Hibernate ActionQueue 顺序),与 uk(user_id, role_id, school_id)
+ * 撞唯一键(编辑提交原身份集即触发 Duplicate entry)。
+ */
+ @Modifying
+ @Query("DELETE FROM SysUserRoleRel r WHERE r.userId = :userId AND r.schoolId = :schoolId")
+ void deleteByUserIdAndSchoolId(@Param("userId") Integer userId, @Param("schoolId") Integer schoolId);
/** 删除用户全部身份行(级联删用户时清理,bulk delete 直接发 SQL) */
@Modifying
diff --git a/src/main/java/com/yunhai/userbase/service/impl/GroupAuthzService.java b/src/main/java/com/yunhai/userbase/service/impl/GroupAuthzService.java
index ada0fd3..56b9792 100644
--- a/src/main/java/com/yunhai/userbase/service/impl/GroupAuthzService.java
+++ b/src/main/java/com/yunhai/userbase/service/impl/GroupAuthzService.java
@@ -53,6 +53,7 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
+import java.util.stream.Collectors;
/**
* @author chenx
@@ -753,6 +754,7 @@ public class GroupAuthzService implements IGroupAuthzService {
vo.setAccount(user.getAccount());
vo.setRealName(user.getRealName());
vo.setIsMain(rel.getIsMain());
+ vo.setGroupId(rel.getGroupId());
vo.setGroupPath(groupPath);
result.add(vo);
}
@@ -774,30 +776,43 @@ public class GroupAuthzService implements IGroupAuthzService {
return Collections.emptyList();
}
- // 加载所有组,用于构建主归属路径
+ // 加载所有组,用于构建各组的组织路径
Map allGroupMap = buildAllGroupMap();
- // 加载用户与组关联,找到每个用户的主归属组
Set userIds = new LinkedHashSet<>();
for (SysUser user : users) {
userIds.add(user.getId());
}
- // 批量解析主归属组(isMain=1 取 id 最小,无主兜底 id 最小),一次 IN 查询,消灭全表扫描
- Map mainRelMap = mainGroupResolver.resolveByUserIds(new ArrayList<>(userIds));
+ // 同一用户在多个组时每个组出一行(各自组路径/主兼职标记),
+ // 不再只解析主归属组——否则多组用户只能看到主组一条数据
+ Map> relsByUser = sysUserGroupRelRepository
+ .findByUserIdIn(new ArrayList<>(userIds))
+ .stream()
+ .collect(Collectors.groupingBy(SysUserGroupRel::getUserId));
List result = new ArrayList<>();
for (SysUser user : users) {
- SysUserGroupRel mainRel = mainRelMap.get(user.getId());
- String path = "";
- if (mainRel != null && mainRel.getGroupId() != null) {
- path = buildGroupPath(mainRel.getGroupId(), allGroupMap);
+ List userRels = relsByUser.get(user.getId());
+ if (userRels == null || userRels.isEmpty()) {
+ // 无组关系的用户仍出一行(组路径空),保持可选中查看权限
+ UserPermissionUserVo vo = new UserPermissionUserVo();
+ vo.setId(user.getId());
+ vo.setAccount(user.getAccount());
+ vo.setRealName(user.getRealName());
+ vo.setGroupPath("");
+ result.add(vo);
+ continue;
+ }
+ for (SysUserGroupRel rel : userRels) {
+ UserPermissionUserVo vo = new UserPermissionUserVo();
+ vo.setId(user.getId());
+ vo.setAccount(user.getAccount());
+ vo.setRealName(user.getRealName());
+ vo.setIsMain(rel.getIsMain());
+ vo.setGroupId(rel.getGroupId());
+ vo.setGroupPath(rel.getGroupId() != null
+ ? buildGroupPath(rel.getGroupId(), allGroupMap) : "");
+ result.add(vo);
}
- UserPermissionUserVo vo = new UserPermissionUserVo();
- vo.setId(user.getId());
- vo.setAccount(user.getAccount());
- vo.setRealName(user.getRealName());
- vo.setIsMain(mainRel != null ? mainRel.getIsMain() : null);
- vo.setGroupPath(path);
- result.add(vo);
}
return result;
}
diff --git a/src/main/java/com/yunhai/userbase/service/impl/SysOapiLogServiceImpl.java b/src/main/java/com/yunhai/userbase/service/impl/SysOapiLogServiceImpl.java
index e6327c7..a70ac55 100644
--- a/src/main/java/com/yunhai/userbase/service/impl/SysOapiLogServiceImpl.java
+++ b/src/main/java/com/yunhai/userbase/service/impl/SysOapiLogServiceImpl.java
@@ -168,12 +168,18 @@ public class SysOapiLogServiceImpl implements ISysOapiLogService {
}
}
if (StringUtils.isNotBlank(query.getKeyword())) {
- String kw = "%" + query.getKeyword().trim() + "%";
- // 单输入框同时匹配:请求路径(requestUri) / 应用名(callerAppName) / appKey(callerAppKey)
- predicates.add(cb.or(
+ String trimmed = query.getKeyword().trim();
+ String kw = "%" + trimmed + "%";
+ // 单输入框同时匹配:请求路径(requestUri) / 应用名(callerAppName);
+ // 纯数字再按 appKey 数值等值匹配(cb.toString 会渲染成 CAST 表达式,MySQL 方言下 SQL 报错)
+ Predicate textMatch = cb.or(
cb.like(root.get("requestUri"), kw),
- cb.like(root.get("callerAppName"), kw),
- cb.like(cb.toString(root.get("callerAppKey")), kw)));
+ cb.like(root.get("callerAppName"), kw));
+ if (trimmed.matches("\\d+")) {
+ predicates.add(cb.or(textMatch, cb.equal(root.get("callerAppKey"), Integer.valueOf(trimmed))));
+ } else {
+ predicates.add(textMatch);
+ }
}
if (query.getStartTime() != null) {
predicates.add(cb.greaterThanOrEqualTo(root.get("createTime"), query.getStartTime()));
diff --git a/src/main/java/com/yunhai/userbase/service/impl/SysUserService.java b/src/main/java/com/yunhai/userbase/service/impl/SysUserService.java
index 5d7178f..67f7a9d 100644
--- a/src/main/java/com/yunhai/userbase/service/impl/SysUserService.java
+++ b/src/main/java/com/yunhai/userbase/service/impl/SysUserService.java
@@ -496,7 +496,7 @@ public class SysUserService implements ISysUserService {
}
/**
- * 按页批量带出各用户主校身份名(动态解析主身份,无 N+1)。
+ * 按页批量带出各用户身份名(动态解析主身份,无 N+1)。
* roles 列已停写(冻结历史值),列表「用户身份」列一律切此新字段展示。
*/
private Page attachMainIdentityNames(Page page) {
@@ -508,15 +508,32 @@ public class SysUserService implements ISysUserService {
.map(SysUserVo::getId)
.collect(Collectors.toList());
Map mainRelsByUser = resolveMainRoleRelsByUser(userIds);
+ // 全部正常态身份行按用户分组:列表列展示全部身份名(主身份在前),不只展示主身份
+ Map> roleRelsByUser = sysUserRoleRelRepository
+ .findByUserIdInAndStatus(userIds, StatusEnum.NORMAL)
+ .stream()
+ .collect(Collectors.groupingBy(SysUserRoleRel::getUserId));
for (SysUserVo vo : content) {
- SysUserRoleRel mainRel = mainRelsByUser.get(vo.getId());
- if (mainRel == null) {
+ List rels = roleRelsByUser.get(vo.getId());
+ if (rels == null || rels.isEmpty()) {
continue;
}
- SysRoleEnum e = IEnum.getByKey(SysRoleEnum.class, mainRel.getRoleId());
- String name = e != null ? e.getValue() : null;
- if (name != null) {
- vo.setIdentityNames(name);
+ SysUserRoleRel mainRel = mainRelsByUser.get(vo.getId());
+ String names = rels.stream()
+ .sorted(Comparator.comparingInt((SysUserRoleRel r) ->
+ mainRel != null && Objects.equals(r.getId(), mainRel.getId()) ? 0 : 1)
+ .thenComparingInt(SysUserRoleRel::getSortNo)
+ .thenComparingInt(SysUserRoleRel::getId))
+ .map(SysUserRoleRel::getRoleId)
+ .distinct()
+ .map(roleId -> {
+ SysRoleEnum e = IEnum.getByKey(SysRoleEnum.class, roleId);
+ return e != null ? e.getValue() : null;
+ })
+ .filter(Objects::nonNull)
+ .collect(Collectors.joining("、"));
+ if (!names.isEmpty()) {
+ vo.setIdentityNames(names);
}
}
return page;
diff --git a/src/main/java/com/yunhai/userbase/service/push/PushRecordService.java b/src/main/java/com/yunhai/userbase/service/push/PushRecordService.java
index c54ce9e..0c58931 100644
--- a/src/main/java/com/yunhai/userbase/service/push/PushRecordService.java
+++ b/src/main/java/com/yunhai/userbase/service/push/PushRecordService.java
@@ -27,7 +27,9 @@ import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Order;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
+import java.time.LocalDate;
import java.time.LocalDateTime;
+import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
@@ -164,10 +166,23 @@ public class PushRecordService {
}
private LocalDateTime parseStart(String s) {
- return LocalDateTime.parse(s + "T00:00:00", DateTimeFormatter.ISO_LOCAL_DATE_TIME);
+ return parseTime(s, LocalTime.MIN);
}
private LocalDateTime parseEnd(String s) {
- return LocalDateTime.parse(s + "T23:59:59", DateTimeFormatter.ISO_LOCAL_DATE_TIME);
+ return parseTime(s, LocalTime.of(23, 59, 59));
+ }
+
+ /**
+ * 兼容两种入参:yyyy-MM-dd(按日界补 00:00:00 / 23:59:59)与
+ * yyyy-MM-dd HH:mm:ss(前端日期选择器带时刻的完整格式,原样解析)。
+ * 前者按旧约定拼接解析,后者若仍走拼接会产生 "… HH:mm:ssT00:00:00" 解析异常。
+ */
+ private LocalDateTime parseTime(String s, LocalTime dayEdge) {
+ String v = s == null ? "" : s.trim();
+ if (v.length() == 10) {
+ return LocalDate.parse(v, DateTimeFormatter.ISO_LOCAL_DATE).atTime(dayEdge);
+ }
+ return LocalDateTime.parse(v, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}
}
diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml
index 20612de..91c0f5f 100644
--- a/src/main/resources/logback-spring.xml
+++ b/src/main/resources/logback-spring.xml
@@ -16,8 +16,8 @@
/app/logs/app.log
- /app/logs/app.%d{yyyy-MM-dd}.log
- 30
+ /app/logs/app.%d{yyyy-MM-dd}.log.zip
+ 60
5GB
diff --git a/src/test/java/com/yunhai/userbase/UserBaseApplicationTests.java b/src/test/java/com/yunhai/userbase/UserBaseApplicationTests.java
index 7683091..62922a2 100644
--- a/src/test/java/com/yunhai/userbase/UserBaseApplicationTests.java
+++ b/src/test/java/com/yunhai/userbase/UserBaseApplicationTests.java
@@ -1,4 +1,4 @@
-package com.yunhai.userprofile;
+package com.yunhai.userbase;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
diff --git a/src/test/java/com/yunhai/userbase/service/impl/LogSearchRegressionTest.java b/src/test/java/com/yunhai/userbase/service/impl/LogSearchRegressionTest.java
new file mode 100644
index 0000000..f9672fc
--- /dev/null
+++ b/src/test/java/com/yunhai/userbase/service/impl/LogSearchRegressionTest.java
@@ -0,0 +1,51 @@
+package com.yunhai.userbase.service.impl;
+
+import com.yunhai.userbase.model.BasePageQuery;
+import com.yunhai.userbase.model.oapilog.SysOapiLogQuery;
+import com.yunhai.userbase.model.push.PushRecordQuery;
+import com.yunhai.userbase.service.ISysOapiLogService;
+import com.yunhai.userbase.service.push.PushRecordService;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.data.domain.PageRequest;
+
+/**
+ * 日志搜索回归(真库执行 SQL,验证谓词语法与时间解析;空表也走完整 SQL):
+ * ① 开放接口日志关键字搜索:文本走 LIKE,纯数字走 appKey 数值等值——
+ * 修复前 cb.toString(callerAppKey) 渲染 CAST 表达式,MySQL 方言 SQL 报错;
+ * ② 推送日志时间搜索:兼容 yyyy-MM-dd 与 yyyy-MM-dd HH:mm:ss——
+ * 修复前端传带时刻格式时按旧约定拼接解析抛 DateTimeParseException。
+ */
+@SpringBootTest
+class LogSearchRegressionTest {
+
+ @Autowired
+ private ISysOapiLogService sysOapiLogService;
+ @Autowired
+ private PushRecordService pushRecordService;
+
+ @Test
+ void oapiLog_keywordSearch_textAndNumeric() {
+ BasePageQuery page = new BasePageQuery();
+ SysOapiLogQuery text = new SysOapiLogQuery();
+ text.setKeyword("user");
+ sysOapiLogService.pageQuery(text, page);
+
+ SysOapiLogQuery numeric = new SysOapiLogQuery();
+ numeric.setKeyword("100001");
+ sysOapiLogService.pageQuery(numeric, page);
+ }
+
+ @Test
+ void pushLog_timeSearch_bothFormats() {
+ PushRecordQuery q = new PushRecordQuery();
+ q.setStartTime("2026-09-01 00:00:00");
+ q.setEndTime("2026-09-01 23:59:59");
+ pushRecordService.query(q, PageRequest.of(0, 10));
+
+ q.setStartTime("2026-09-01");
+ q.setEndTime("2026-09-01");
+ pushRecordService.query(q, PageRequest.of(0, 10));
+ }
+}
diff --git a/src/test/java/com/yunhai/userbase/service/impl/SysUserIdentityRegressionTest.java b/src/test/java/com/yunhai/userbase/service/impl/SysUserIdentityRegressionTest.java
new file mode 100644
index 0000000..6b5c63d
--- /dev/null
+++ b/src/test/java/com/yunhai/userbase/service/impl/SysUserIdentityRegressionTest.java
@@ -0,0 +1,135 @@
+package com.yunhai.userbase.service.impl;
+
+import com.yunhai.userbase.entity.SysGroup;
+import com.yunhai.userbase.entity.SysUserRoleRel;
+import com.yunhai.userbase.enums.StatusEnum;
+import com.yunhai.userbase.model.groupauthz.UserPermissionUserVo;
+import com.yunhai.userbase.model.sysuser.SysUserQuery;
+import com.yunhai.userbase.model.sysuser.SysUserRequest;
+import com.yunhai.userbase.model.sysuser.SysUserVo;
+import com.yunhai.userbase.repository.SysGroupRepository;
+import com.yunhai.userbase.repository.SysUserRoleRelRepository;
+import com.yunhai.userbase.service.IGroupAuthzService;
+import com.yunhai.userbase.service.ISysUserService;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+/**
+ * 身份写入回归(真库集成,事务回滚不留数据):
+ * ① 新增教师+管理员双身份 → 两行都落库;
+ * ② 编辑提交原身份集 → 不再因「派生 delete 入队 + saveAll 刷新时 INSERT 先于 DELETE」
+ * 撞 uk(user_id, role_id, school_id)(修复:bulk 删除立即执行);
+ * ③ 编辑缩减身份 → 按 schoolId 全量覆盖;
+ * ④ 权限一览用户搜索 → 同一用户多组每个组出一行。
+ */
+@SpringBootTest
+@Transactional
+class SysUserIdentityRegressionTest {
+
+ @Autowired
+ private ISysUserService sysUserService;
+ @Autowired
+ private IGroupAuthzService groupAuthzService;
+ @Autowired
+ private SysGroupRepository sysGroupRepository;
+ @Autowired
+ private SysUserRoleRelRepository sysUserRoleRelRepository;
+
+ private SysUserRequest buildAddRequest(Integer groupId) {
+ SysUserRequest add = new SysUserRequest();
+ add.setAccount("regress_identity");
+ add.setPassword("Rk8pb2mW!");
+ add.setConfirmPassword("Rk8pb2mW!");
+ add.setRealName("回归用户");
+ add.setGender(0);
+ add.setGroupId(groupId);
+ add.setRoleIds(Arrays.asList(1, 7)); // 教师 + 管理员
+ return add;
+ }
+
+ private List roleIdsOf(Integer userId) {
+ return sysUserRoleRelRepository.findByUserIdAndStatus(userId, StatusEnum.NORMAL)
+ .stream().map(SysUserRoleRel::getRoleId).sorted().collect(Collectors.toList());
+ }
+
+ @Test
+ void addThenEdit_identities() {
+ SysGroup group = sysGroupRepository.findAll().stream().findFirst()
+ .orElseThrow(() -> new IllegalStateException("库中无组数据"));
+
+ Integer userId = sysUserService.add(buildAddRequest(group.getId()));
+ assertEquals(Arrays.asList(1, 7), roleIdsOf(userId), "新增教师+管理员双身份应两行都落库");
+
+ // 列表「用户身份」列应展示全部身份名(不只主身份),多身份按「、」连接
+ SysUserQuery query = new SysUserQuery();
+ query.setGroupId(group.getId());
+ Page page = sysUserService.list(query, PageRequest.of(0, 10));
+ SysUserVo mine = page.getContent().stream()
+ .filter(v -> userId.equals(v.getId())).findFirst().orElse(null);
+ assertNotNull(mine, "组列表应包含新建用户");
+ assertEquals("老师、管理员", mine.getIdentityNames(), "用户身份列应展示全部身份名");
+
+ Integer schoolId = sysUserRoleRelRepository.findByUserId(userId).stream()
+ .findFirst().map(SysUserRoleRel::getSchoolId).orElse(null);
+ assertNotNull(schoolId, "新增后身份行应带 schoolId");
+
+ // 编辑①:身份不变重复提交(前端编辑总是携带原值)——修复前此处 Duplicate entry 撞 uk
+ SysUserRequest keep = new SysUserRequest();
+ keep.setId(userId);
+ keep.setAccount("regress_identity");
+ keep.setRealName("回归用户改");
+ keep.setGender(0);
+ keep.setSchoolId(schoolId);
+ keep.setRoleIds(Arrays.asList(1, 7));
+ sysUserService.edit(userId, keep);
+ assertEquals(Arrays.asList(1, 7), roleIdsOf(userId), "编辑提交原身份集应原样保留");
+
+ // 编辑②:清掉管理员,仅剩教师——按 schoolId 全量覆盖
+ SysUserRequest shrink = new SysUserRequest();
+ shrink.setId(userId);
+ shrink.setAccount("regress_identity");
+ shrink.setRealName("回归用户改");
+ shrink.setGender(0);
+ shrink.setSchoolId(schoolId);
+ shrink.setRoleIds(Collections.singletonList(1));
+ sysUserService.edit(userId, shrink);
+ assertEquals(Collections.singletonList(1), roleIdsOf(userId), "编辑缩减身份应按 schoolId 全量覆盖");
+ }
+
+ @Test
+ void searchUsers_multiGroup_shouldReturnRowPerGroup() {
+ List groups = sysUserRepository_findTwoGroups();
+ Integer userId = sysUserService.add(buildAddRequest(groups.get(0).getId()));
+ // 调整到两个组(第二个组为主组)
+ sysUserService.changeGroups(Collections.singletonList(userId),
+ Arrays.asList(groups.get(0).getId(), groups.get(1).getId()), groups.get(1).getId());
+
+ List rows = groupAuthzService.searchUsers("regress_identity");
+ List mine = rows.stream()
+ .filter(v -> userId.equals(v.getId())).collect(Collectors.toList());
+ assertEquals(2, mine.size(), "同一用户在两个组应出两行");
+ assertEquals(1, mine.stream().filter(v -> Integer.valueOf(1).equals(v.getIsMain())).count(),
+ "两行中应恰有一行主归属");
+ mine.forEach(v -> assertNotNull(v.getGroupId(), "每行应带 groupId(按组查看权限)"));
+ }
+
+ private List sysUserRepository_findTwoGroups() {
+ List all = sysGroupRepository.findAll();
+ if (all.size() < 2) {
+ throw new IllegalStateException("库中组数据不足两个,无法覆盖多组场景");
+ }
+ return all.subList(0, 2);
+ }
+}