feat(app): 应用设置·应用管理——flw_app 白名单 + 学校池同步 + defSave 归属应用校验

- flw_app(sch,app_key uk):学校池(UserBase list-by-school,学校组单点已开通、clientType=null 不过滤端)
  diff 同步——新增即 enabled=1、改名静默刷新、池外保留;UserBase 不可达降级本地行
- /admin/app/list(触发同步)/ /admin/app/enable;defSave 显式 appKey 须 ∈ 启用集合(不传不拦,升版本继承不受影响)
- SQL 备案:flw_app 建表 + UserBase 菜单 107 新增/104 下线(人工执行)
- 单测 +8;E2E 13/13(local-debug/four-topics-c-e2e.py)
- 关联 SDK 修复:BoaApp.createTime @JsonFormat 对齐 UserBase 时间契约(BasePlatform 单独 commit,Nexus -73)
This commit is contained in:
chenxing
2026-10-10 19:17:38 +08:00
parent 92619508f0
commit d6b7a68ec3
10 changed files with 454 additions and 1 deletions
@@ -50,6 +50,7 @@ public class FlowAdminController {
private final FlowDiagramService diagramService;
private final com.yunhai.flowcenter.service.DiagramImageService imageService;
private final com.fasterxml.jackson.databind.ObjectMapper objectMapper;
private final com.yunhai.flowcenter.service.FlowAppAdminService appAdminService;
// ==================== 流程定义 ====================
@@ -66,6 +67,27 @@ public class FlowAdminController {
return defAdminService.distinctAppKeys(adminGuard.requireAdmin(user, null));
}
// ==================== 应用设置(2026-10-10 四议题) ====================
/**
* 应用管理列表:本校"可对接流程"白名单(flw_app)。附带一次学校池静默同步
* (UserBase list-by-school;不可达降级本地行)。开通过程在 UserBase 管理端做。
*/
@GetMapping("/app/list")
public List<com.yunhai.flowcenter.entity.FlwApp> appList(BoaUser user) {
return appAdminService.list(adminGuard.requireAdmin(user, null));
}
/** 开关某应用是否对接流程(停用后 defSave 归属应用校验拦截新引用) */
@PostMapping("/app/enable")
public void appEnable(BoaUser user,
@org.springframework.web.bind.annotation.RequestBody Map<String, Object> body) {
Long schId = adminGuard.requireAdmin(user, null);
appAdminService.enable(schId,
body.get("id") == null ? null : Long.valueOf(String.valueOf(body.get("id"))),
Boolean.parseBoolean(String.valueOf(body.get("enabled"))));
}
@GetMapping("/def/get")
public FlwDef defGet(BoaUser user, @RequestParam("id") Long id) {
return defAdminService.get(adminGuard.requireAdmin(user, null), id);
@@ -0,0 +1,45 @@
package com.yunhai.flowcenter.entity;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Index;
import javax.persistence.Table;
import javax.persistence.UniqueConstraint;
/**
* 流程对接应用(2026-10-10 四议题·应用设置):本校"哪些应用可对接流程"的白名单。
* 行来源=应用设置页从学校池(UserBase /oapi/app/list-by-school,学校组单点已开通)同步,
* 同步即 enabled=1;管理员可停用(停用后 defSave 的归属应用校验拦截新引用)。
* app_name 是学校池快照(含学校别名),每次同步静默刷新。
*/
@Getter
@Setter
@Entity
@Table(name = "flw_app",
uniqueConstraints = @UniqueConstraint(name = "uk_flw_app_sch_key", columnNames = {"sch_id", "app_key"}),
indexes = @Index(name = "idx_flw_app_sch_enabled", columnList = "sch_id, enabled"))
public class FlwApp extends BaseEntity {
/** 租户键 */
@Column(name = "sch_id", nullable = false)
private Long schId;
/** 子应用 appKey(UserBase application_info.app_key 的字符串形态,与 flw_def.app_key 同口径) */
@Column(name = "app_key", nullable = false, length = 64)
private String appKey;
/** 应用名快照(学校池返回值,含学校别名;同步时刷新) */
@Column(name = "app_name", nullable = false, length = 128)
private String appName;
/** 1=对接流程(白名单内);0=停用(defSave 校验拦截) */
@Column(name = "enabled", nullable = false)
private Integer enabled = 1;
/** 展示排序(学校池 orderNumber 快照,可空=0) */
@Column(name = "sort_no")
private Integer sortNo;
}
@@ -0,0 +1,19 @@
package com.yunhai.flowcenter.repository;
import com.yunhai.flowcenter.entity.FlwApp;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.List;
public interface FlwAppRepository extends JpaRepository<FlwApp, Long> {
List<FlwApp> findBySchIdOrderBySortNoAscIdAsc(Long schId);
List<FlwApp> findBySchIdAndEnabledOrderBySortNoAscIdAsc(Long schId, Integer enabled);
/** defSave 归属应用白名单校验:本校启用集合是否包含该 appKey */
@Query("select count(a) > 0 from FlwApp a where a.schId = :schId and a.appKey = :appKey and a.enabled = 1")
boolean isEnabledApp(@Param("schId") Long schId, @Param("appKey") String appKey);
}
@@ -0,0 +1,109 @@
package com.yunhai.flowcenter.service;
import com.yunhai.flowcenter.entity.FlwApp;
import com.yunhai.flowcenter.repository.FlwAppRepository;
import com.yunhai.platform.openapi.base.OpenApiAppClient;
import com.yunhai.platform.utils.model.BoaApp;
import com.yunhai.platform.utils.model.BoaAppType;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 应用设置·应用管理(2026-10-10 四议题):flw_app 白名单的同步与开关。
*
* <p><b>源池口径(拍板 D7)</b>:UserBase {@code /oapi/app/list-by-school}——学校组单点已开通
* (sys_group_app_rel,不并子组/角色模块),clientType 传 null 不过滤端(拍板 D3,避免漏纯移动端应用)。
* 同步即入白名单 enabled=1(拍板 D2:只做最优实现,不做存量 appKey 兜底——不在学校池的应用
* 进不了白名单,defSave 引用会被拦,属拍板接受的后果);池内应用名变化静默刷新(学校别名);
* 学校池已移除的本地行保留(enabled 交管理员处置)。
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class FlowAppAdminService {
private final FlwAppRepository appRepository;
private final OpenApiAppClient appClient;
/**
* 列表(管理端应用设置页)。附带一次静默学校池同步:UserBase 不可达/学校未开通任何应用时
* 降级只返回本地行(首次使用前 flw_app 为空表是正常态,管理员可先在 UserBase 侧开通)。
*/
public List<FlwApp> list(Long schId) {
try {
syncFromSchoolPool(schId);
} catch (Exception e) {
log.warn("[flow-center] 学校池同步失败,降级返回本地应用行:sch={} {}", schId, e.toString());
}
return appRepository.findBySchIdOrderBySortNoAscIdAsc(schId);
}
/** 开关某应用是否对接流程(停用后 defSave 归属应用校验拦截新引用;存量定义不受影响) */
@Transactional
public FlwApp enable(Long schId, Long id, boolean enabled) {
FlwApp app = appRepository.findById(id)
.orElseThrow(() -> new com.yunhai.platform.apistd.BizException(
com.yunhai.platform.utils.exception.ErrorCode.PARAM_ERROR, "应用行不存在:" + id));
if (!app.getSchId().equals(schId)) {
throw new com.yunhai.platform.apistd.BizException(
com.yunhai.platform.utils.exception.ErrorCode.FORBIDDEN, "应用行不属于该学校");
}
app.setEnabled(enabled ? 1 : 0);
return appRepository.save(app);
}
/**
* 学校池 → flw_app diff 同步(新增 enabled=1 / 改名静默刷新 / 池外保留)。
* 幂等:按 (sch_id, app_key) 唯一键 upsert。
*/
@Transactional
public void syncFromSchoolPool(Long schId) {
List<BoaAppType> pool = appClient.getSchoolApp(schId.intValue(), null);
Map<String, BoaApp> poolApps = new HashMap<>();
for (BoaAppType type : pool) {
for (BoaApp app : type.getApps()) {
if (app.getAppKey() != null) {
poolApps.put(String.valueOf(app.getAppKey()), app);
}
}
}
Map<String, FlwApp> local = new HashMap<>();
for (FlwApp row : appRepository.findBySchIdOrderBySortNoAscIdAsc(schId)) {
local.put(row.getAppKey(), row);
}
int inserted = 0;
int refreshed = 0;
for (Map.Entry<String, BoaApp> e : poolApps.entrySet()) {
BoaApp poolApp = e.getValue();
FlwApp row = local.get(e.getKey());
if (row == null) {
row = new FlwApp();
row.setSchId(schId);
row.setAppKey(e.getKey());
row.setAppName(poolApp.getName() == null || poolApp.getName().isEmpty()
? e.getKey() : poolApp.getName());
row.setEnabled(1);
row.setSortNo(poolApp.getOrderNumber() == null ? 0 : poolApp.getOrderNumber());
appRepository.save(row);
inserted++;
} else if (poolApp.getName() != null && !poolApp.getName().isEmpty()
&& !poolApp.getName().equals(row.getAppName())) {
// 学校别名变化:静默刷新名称快照(开关不动)
row.setAppName(poolApp.getName());
row.setSortNo(poolApp.getOrderNumber() == null ? row.getSortNo() : poolApp.getOrderNumber());
appRepository.save(row);
refreshed++;
}
}
if (inserted > 0 || refreshed > 0) {
log.info("[flow-center] 学校池同步:sch={} 池内 {} 应用,新增 {} 行,刷新名 {} 行",
schId, poolApps.size(), inserted, refreshed);
}
}
}
@@ -36,6 +36,7 @@ public class FlowDefAdminService {
private final FlwAssigneeMapRepository mapRepository;
private final com.yunhai.flowcenter.repository.FlwRouterRepository routerRepository;
private final com.yunhai.flowcenter.repository.FlwCallbackRegRepository regRepository;
private final com.yunhai.flowcenter.repository.FlwAppRepository appRepository;
private final FlowEngine flowEngine;
private final ObjectMapper objectMapper;
@@ -79,6 +80,12 @@ public class FlowDefAdminService {
@Transactional
public FlwDef save(Long schId, Long id, String code, String name, String contentJson, String baseGuid, String appKey) {
String cleanAppKey = (appKey == null || appKey.trim().isEmpty()) ? null : appKey.trim();
// 归属应用白名单(2026-10-10 应用设置):显式传入非空时必须 ∈ flw_app(sch, enabled=1);
// 不传(升版本继承源线)不拦。不在学校池/未开启的应用由此拦下(拍板 D2:不做存量兜底)
if (cleanAppKey != null && !appRepository.isEnabledApp(schId, cleanAppKey)) {
throw new BizException(ErrorCode.PARAM_ERROR,
"归属应用未开启或不存在(" + cleanAppKey + "):请在 流程管理 > 应用设置 中开启后重试");
}
// JSON 先解析再落库(语法错误不进库)
FlowDefModel parsed = FlowDefModel.parse(objectMapper, contentJson);
if (code == null || code.trim().isEmpty() || name == null || name.trim().isEmpty()) {
@@ -0,0 +1,15 @@
-- 2026-10-10 四议题·应用设置:流程对接应用白名单表
-- ddl-auto=update 会自动建表;本脚本为生产备案(生产建议停自动 DDL 后手工执行)
CREATE TABLE IF NOT EXISTS `flw_app` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`sch_id` BIGINT NOT NULL COMMENT '租户键',
`app_key` VARCHAR(64) NOT NULL COMMENT '子应用 appKey(UserBase application_info.app_key 字符串形态)',
`app_name` VARCHAR(128) NOT NULL COMMENT '应用名快照(学校池返回值,含学校别名;同步刷新)',
`enabled` INT NOT NULL DEFAULT 1 COMMENT '1=对接流程(白名单);0=停用(defSave 校验拦截)',
`sort_no` INT NULL COMMENT '展示排序(学校池 orderNumber 快照)',
`create_time` DATETIME(6) NULL,
`update_time` DATETIME(6) NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_flw_app_sch_key` (`sch_id`, `app_key`),
KEY `idx_flw_app_sch_enabled` (`sch_id`, `enabled`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '流程对接应用白名单(应用设置)';
@@ -0,0 +1,56 @@
-- =====================================================================
-- flow-center 四议题(2026-10-10):UserBase app_module 变更
-- ① 新增 107「应用设置」(/admin/app-setting,应用管理 + 科室管理两个 tab)
-- ② 下线 104「办理人映射」(/admin/maps 由 应用设置-科室管理 承载,Part E 上线时执行 ②段)
-- 纪律:AI 不代跑,dev / prod 由人工执行;MySQL 5.7 兼容;幂等可重复执行。
-- 前置:flow-center 前端(含 AppSetting.vue 路由)已发布,否则菜单点进去 404。
-- =====================================================================
-- 步骤 0:校验身份链(app_module.app_id 存的是 application_info 主键,非 app_key)
SELECT id AS app_id_pk, app_key, name
FROM application_info
WHERE id = 100011 OR app_key = 100019;
-- 期望同一行(id=100011 且 app_key=100019);不同则把 @app_id 改成 app_key=100019 那行的 id。
SET @app_id = 100011;
SET @operator = 0;
-- ---------------------------------------------------------------------
-- ① 新增 107 应用设置(幂等:不存在才插;path 与 routes.ts 逐字一致)
-- ---------------------------------------------------------------------
INSERT INTO app_module
(app_id, parent_id, name, custom_id, path, icon, level, sort_no, create_user, create_time)
SELECT @app_id, NULL, '应用设置', '107', '/admin/app-setting', NULL, 1, 107, @operator, NOW()
WHERE NOT EXISTS (
SELECT 1 FROM app_module m WHERE m.app_id = @app_id AND m.custom_id = '107'
);
-- ①授权(角色路径,@role_id 换成实际选中的启用角色;见 flow-center接入UserBase菜单.sql §3.1 查询)
SET @role_id = 0; -- ← 改这里
INSERT INTO sys_app_role_module_rel (role_id, module_id, create_user, create_time)
SELECT @role_id, m.id, @operator, NOW()
FROM app_module m
WHERE m.app_id = @app_id AND m.custom_id = '107'
AND NOT EXISTS (SELECT 1 FROM sys_app_role_module_rel r
WHERE r.role_id = @role_id AND r.module_id = m.id);
-- ---------------------------------------------------------------------
-- ② 下线 104 办理人映射(Part E 科室管理上线时执行;先删授权再删模块行)
-- 如需保留回滚能力,可改为 UPDATE app_module SET sort_no = 9999 隐藏(菜单仍下发)。
-- ---------------------------------------------------------------------
-- DELETE r FROM sys_app_role_module_rel r
-- JOIN app_module m ON m.id = r.module_id
-- WHERE m.app_id = @app_id AND m.custom_id = '104';
-- DELETE p FROM sys_group_module_perm p
-- JOIN app_module m ON m.id = p.module_id
-- WHERE m.app_id = @app_id AND m.custom_id = '104';
-- DELETE m FROM app_module m
-- WHERE m.app_id = @app_id AND m.custom_id = '104';
-- ---------------------------------------------------------------------
-- 验收
-- ---------------------------------------------------------------------
SELECT id, app_id, custom_id, name, path, level, sort_no
FROM app_module
WHERE app_id = @app_id
ORDER BY CAST(custom_id AS UNSIGNED);
@@ -0,0 +1,155 @@
package com.yunhai.flowcenter.service;
import com.yunhai.flowcenter.entity.FlwApp;
import com.yunhai.flowcenter.repository.FlwAppRepository;
import com.yunhai.platform.openapi.base.OpenApiAppClient;
import com.yunhai.platform.utils.model.BoaApp;
import com.yunhai.platform.utils.model.BoaAppType;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* 应用设置·应用管理单测(2026-10-10):学校池 diff 同步(新增 enabled=1/改名刷新/池外保留)、
* UserBase 不可达降级、开关与跨校守卫。口径拍板:D2 只做最优实现(不做存量 appKey 兜底)、
* D3 clientType=null 不过滤端、D7 学校组单点池。
*/
class FlowAppAdminServiceTest {
private static final Long SCH = 990001L;
private FlwAppRepository appRepository;
private OpenApiAppClient appClient;
private FlowAppAdminService service;
@BeforeEach
void setUp() {
appRepository = mock(FlwAppRepository.class);
appClient = mock(OpenApiAppClient.class);
service = new FlowAppAdminService(appRepository, appClient);
when(appRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
}
private BoaAppType type(BoaApp... apps) {
BoaAppType t = new BoaAppType();
t.setId(2);
t.setName("办公管理");
t.setApps(Arrays.asList(apps));
return t;
}
private BoaApp app(Integer appKey, String name, Integer orderNumber) {
BoaApp a = new BoaApp();
a.setAppKey(appKey);
a.setName(name);
a.setOrderNumber(orderNumber);
return a;
}
private FlwApp row(String appKey, String name, int enabled) {
FlwApp r = new FlwApp();
r.setId((long) appKey.hashCode());
r.setSchId(SCH);
r.setAppKey(appKey);
r.setAppName(name);
r.setEnabled(enabled);
return r;
}
@Test
void sync_池内新应用_插入且默认启用() {
when(appClient.getSchoolApp(990001, null)).thenReturn(Collections.singletonList(
type(app(100002, "通知公告", 1))));
when(appRepository.findBySchIdOrderBySortNoAscIdAsc(SCH)).thenReturn(Collections.emptyList());
service.syncFromSchoolPool(SCH);
ArgumentCaptor<FlwApp> cap = ArgumentCaptor.forClass(FlwApp.class);
verify(appRepository, times(1)).save(cap.capture());
assertEquals("100002", cap.getValue().getAppKey());
assertEquals("通知公告", cap.getValue().getAppName());
assertEquals(1, cap.getValue().getEnabled());
}
@Test
void sync_改名静默刷新_开关不动_池外行保留() {
FlwApp renamed = row("100002", "通知公告", 1);
FlwApp untouchedEnabled = row("100008", "请假", 0); // 池外且已停用:保留不动
FlwApp untouchedName = row("100010", "旧名", 1); // 池外:名称也不动
when(appClient.getSchoolApp(990001, null)).thenReturn(Collections.singletonList(
type(app(100002, "通知公告(校内别名)", 9))));
when(appRepository.findBySchIdOrderBySortNoAscIdAsc(SCH))
.thenReturn(Arrays.asList(renamed, untouchedEnabled, untouchedName));
service.syncFromSchoolPool(SCH);
verify(appRepository, times(1)).save(any(FlwApp.class));
assertEquals("通知公告(校内别名)", renamed.getAppName());
assertEquals(1, renamed.getEnabled());
assertEquals("旧名", untouchedName.getAppName());
assertEquals(0, untouchedEnabled.getEnabled());
}
@Test
void list_UserBase不可达_降级返回本地行() {
when(appClient.getSchoolApp(any(), any())).thenThrow(new RuntimeException("connect timeout"));
when(appRepository.findBySchIdOrderBySortNoAscIdAsc(SCH))
.thenReturn(Collections.singletonList(row("100002", "通知公告", 1)));
List<FlwApp> rows = service.list(SCH);
assertEquals(1, rows.size());
assertEquals("100002", rows.get(0).getAppKey());
}
@Test
void list_正常链路_先同步再返回() {
when(appClient.getSchoolApp(990001, null)).thenReturn(Collections.emptyList());
when(appRepository.findBySchIdOrderBySortNoAscIdAsc(SCH)).thenReturn(Collections.emptyList());
assertEquals(0, service.list(SCH).size());
verify(appClient, times(1)).getSchoolApp(990001, null);
}
@Test
void enable_切换与守卫() {
FlwApp r = row("100002", "通知公告", 1);
when(appRepository.findById(r.getId())).thenReturn(java.util.Optional.of(r));
service.enable(SCH, r.getId(), false);
assertEquals(0, r.getEnabled());
service.enable(SCH, r.getId(), true);
assertEquals(1, r.getEnabled());
FlwApp other = row("100002", "x", 1);
other.setSchId(2L);
when(appRepository.findById(other.getId())).thenReturn(java.util.Optional.of(other));
assertThrows(com.yunhai.platform.apistd.BizException.class, () -> service.enable(SCH, other.getId(), true));
}
@Test
void sync_池为空_零写入() {
when(appClient.getSchoolApp(990001, null)).thenReturn(Collections.emptyList());
when(appRepository.findBySchIdOrderBySortNoAscIdAsc(SCH)).thenReturn(Collections.emptyList());
service.syncFromSchoolPool(SCH);
verify(appRepository, never()).save(any());
assertTrue(service.list(SCH).isEmpty());
}
}
@@ -37,18 +37,23 @@ class FlowDefAdminServiceSaveTest {
private static final String JSON = "{\"nodes\":[],\"edges\":[]}";
private FlwDefRepository defRepository;
private com.yunhai.flowcenter.repository.FlwAppRepository appRepository;
private FlowDefAdminService service;
@BeforeEach
void setUp() {
defRepository = mock(FlwDefRepository.class);
appRepository = mock(com.yunhai.flowcenter.repository.FlwAppRepository.class);
service = new FlowDefAdminService(defRepository,
mock(FlwAssigneeMapRepository.class),
mock(FlwRouterRepository.class),
mock(com.yunhai.flowcenter.repository.FlwCallbackRegRepository.class),
appRepository,
mock(FlowEngine.class),
new ObjectMapper());
when(defRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
// 归属应用白名单:非空 appKey 一律放行(校验拦截语义由专门用例覆盖)
when(appRepository.isEnabledApp(any(), any())).thenReturn(true);
}
private FlwDef def(long id, String flowGuid, String code, int version, int status) {
@@ -168,4 +173,23 @@ class FlowDefAdminServiceSaveTest {
() -> service.save(SCH, 9L, "work-leave", "请假", JSON, null, null));
assertTrue(e.getMessage().contains("保存冲突"));
}
@Test
void appKey不在启用白名单_保存被拦() {
when(appRepository.isEnabledApp(SCH, "999999")).thenReturn(false);
BizException e = assertThrows(BizException.class,
() -> service.save(SCH, null, "work-leave", "请假", JSON, null, "999999"));
assertTrue(e.getMessage().contains("应用设置"));
}
@Test
void appKey不传_白名单校验不触发() {
// 升版本继承源线(cleanAppKey=null)不查白名单——存量线升版本不受切换影响
when(defRepository.findById(9L)).thenReturn(Optional.of(def(9L, "G", "work-leave", 4, FlwEnums.DefStatus.PUBLISHED.getCode())));
when(defRepository.maxVersionByGuid(SCH, "G")).thenReturn(4);
when(defRepository.findFirstBySchIdAndFlowGuidOrderByVersionDesc(SCH, "G")).thenReturn(Optional.empty());
FlwDef saved = service.save(SCH, 9L, "work-leave", "请假", JSON, null, null);
org.mockito.Mockito.verify(appRepository, org.mockito.Mockito.never()).isEnabledApp(any(), any());
assertNull(saved.getAppKey());
}
}
@@ -483,7 +483,8 @@ class FlowDiagramServiceTest {
mock(com.yunhai.flowcenter.service.FlowRouterAdminService.class),
mock(FlwInstanceRepository.class), diagramService,
mock(com.yunhai.flowcenter.service.DiagramImageService.class),
new ObjectMapper());
new ObjectMapper(),
mock(com.yunhai.flowcenter.service.FlowAppAdminService.class));
}
private BoaUser user(int id, Integer type, Integer schId, Integer... roleIds) {