1) 排序(管理端 GET /AppList):未选应用类型(前端「全部」)统一按 id 正序,最新创建的在最后。
- SUPER:去掉 @SortDefault(sort="sortNo"),改 categoryId==null ? Sort.by(ASC,"id") : Sort.unsorted();
类别视图排序改由 Specification 内嵌 orderBy(NULL 沉底 + sortNo + id)——NULL 沉底须 case
表达式,Sort 表达不了,故该分支必须传 unsorted,否则 Pageable 排序会整体覆盖 spec 的 orderBy。
- SCHOOL:listSchoolApps 按视图分叉,全部视图走新增 appIdOrder(),选中类型仍走生效排序
(override sortNo 优先、空回落主表、并列 id 升序),学校拖拽排序管理不回归。
- 口径:sort_no 是「类型内」序号,跨类型打平无全局意义(服务内注释 + 2026-07-22 记忆)。
2) 基石过滤(oapi GET /oapi/app/list):AppCategoryService.listBoaAppType 补 AppNature.isBaseApp
过滤,UserBase 自身(appKey<=100000)与 app_nature=BASE(UserHome / 移动门户)不进用户端应用列表。
根因:findUserAuthorizedAppIds 超管分支返回全量,而该读链全程无基石判定——即
《消息卡片直批与跳转基址统一方案-20260928》§4.2「一开四关」漏掉的第 6 个触点(现收口为「关」)。
未动 findUserAuthorizedAppIds(被 /oapi/app/authorized-codes 跳转判权共用,改了会连带把
「跳到基石应用」判无权限);未动 manage 端口径(仅 isUserbaseSelf,保留基石供重置密钥)。
测试:AppCategoryServiceBaseAppFilterTest(3 用例)+ SchoolAppOverrideServiceListSchoolAppsTest(5 用例)
This commit is contained in:
@@ -30,7 +30,6 @@ import com.yunhai.userbase.service.resolve.AccessScopeService;
|
||||
import com.yunhai.userbase.utils.PageUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.web.SortDefault;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@@ -195,8 +194,14 @@ public class AppSettingsController {
|
||||
|
||||
@GetMapping("/AppList")
|
||||
public BasePageResult<ApplicationInfoVo> appList(ApplicationInfoQuery query, BasePageQuery pageQuery,
|
||||
@SortDefault(sort = "sortNo", direction = Sort.Direction.ASC) Sort sort,
|
||||
@RequestParam(value = "schoolId", required = false) Integer schoolId) {
|
||||
// 排序必须带唯一兜底键:同键行 MySQL 不保证稳定序,加删一行即整组洗牌(应用列表乱序的根因)。
|
||||
// 全部(未选类别)= id 升序——sort_no 是「类型内」序号,跨类型打平无全局意义;
|
||||
// 选中类别 = 未排序(NULL)沉底 + sortNo 升序 + id 兜底——由 Specification 内嵌 orderBy 实现
|
||||
// (NULL 沉底须 case 表达式,Sort 只走属性解析表达不了;此处传 unsorted,否则 Pageable 排序会覆盖 spec 的 orderBy)
|
||||
Sort sort = query.getCategoryId() == null
|
||||
? Sort.by(Sort.Direction.ASC, "id")
|
||||
: Sort.unsorted();
|
||||
if (isSchoolLevel()) {
|
||||
// 学校:仅本校已开通应用,override 合并 名称/描述/图标/排序(多校默认并集,可指定单校)
|
||||
AccessScope scope = accessScopeService.resolveCurrentScope();
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.yunhai.platform.utils.model.BoaApp;
|
||||
import com.yunhai.platform.utils.model.BoaAppType;
|
||||
import com.yunhai.platform.utils.model.BoaRoleInfo;
|
||||
import com.yunhai.userbase.config.CacheNames;
|
||||
import com.yunhai.userbase.constant.AppNature;
|
||||
import com.yunhai.userbase.entity.AppCategory;
|
||||
import com.yunhai.userbase.entity.ApplicationInfo;
|
||||
import com.yunhai.userbase.entity.SysUserGroupRel;
|
||||
@@ -220,6 +221,14 @@ public class AppCategoryService implements IAppCategoryService {
|
||||
type.setName(category.getName());
|
||||
List<BoaApp> boaApps = new ArrayList<>();
|
||||
for (ApplicationInfo app : appMap.getOrDefault(category.getId(), Collections.emptyList())) {
|
||||
// 基石应用过滤(口径单点 = AppNature.isBaseApp,与管理端 ①②③⑤ 同侧):
|
||||
// UserBase 自身 / app_nature=BASE(UserHome、移动门户)不参与用户端应用列表,
|
||||
// 否则超管(findUserAuthorizedAppIds 返回全量)会把基石带进门户 /apps。
|
||||
// 与 manage 端列表(ApplicationInfoService.select,仅同步 isUserbaseSelf 语义、
|
||||
// 保留基石供重置密钥)口径不同,勿合并。
|
||||
if (AppNature.isBaseApp(app.getAppKey(), app.getAppNature())) {
|
||||
continue;
|
||||
}
|
||||
// 用户权限过滤
|
||||
if (!authorizedAppIds.contains(app.getId())) {
|
||||
continue;
|
||||
|
||||
@@ -294,6 +294,15 @@ public class ApplicationInfoService implements IApplicationInfoService {
|
||||
predicates.add(cb.equal(root.get("categoryId"), query.getCategoryId()));
|
||||
}
|
||||
}
|
||||
// 类别视图的排序只能内嵌在 spec(NULL 沉底须 case 表达式,Pageable 的 Sort 表达不了)。
|
||||
// 生效前提:控制器对该分支传 Sort.unsorted(),否则 Pageable 排序会在此之后 cq.orderBy 整体覆盖;
|
||||
// count 查询不受影响(Spring Data 会剥掉 spec 带入的 orderBy)
|
||||
if (query != null && query.getCategoryId() != null) {
|
||||
cq.orderBy(
|
||||
cb.asc(cb.selectCase().when(cb.isNull(root.get("sortNo")), 1).otherwise(0)),
|
||||
cb.asc(root.get("sortNo")),
|
||||
cb.asc(root.get("id")));
|
||||
}
|
||||
return cq.where(predicates.toArray(new Predicate[predicates.size()])).getRestriction();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -194,6 +194,15 @@ public class SchoolAppOverrideService {
|
||||
return vo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 「全部」视图(未选应用类型)排序:按 id 正序 —— 创建顺序,最新创建的在最后。
|
||||
* 与 SUPER 分支同口径(AppSettingsController:「全部」= Sort.by(ASC,"id")):
|
||||
* sort_no 是「类型内」序号,跨类型打平无全局意义,故「全部」不参与生效排序。
|
||||
*/
|
||||
private Comparator<ApplicationInfoVo> appIdOrder() {
|
||||
return Comparator.comparing(ApplicationInfoVo::getId, Comparator.nullsLast(Integer::compareTo));
|
||||
}
|
||||
|
||||
/** 生效排序:override sortNo 优先,空沿用主表(空视为999),并列按 id 升序 */
|
||||
private Comparator<ApplicationInfoVo> effectiveAppOrder(Map<Integer, SysSchoolAppOverride> overrides) {
|
||||
return (a, b) -> {
|
||||
@@ -243,9 +252,12 @@ public class SchoolAppOverrideService {
|
||||
filtered.add(app);
|
||||
}
|
||||
Map<Integer, SysSchoolAppOverride> overrides = appOverridesFor(schools);
|
||||
// 「全部」视图(未选应用类型)按 id 正序——与 SUPER 分支同口径(AppSettingsController:
|
||||
// 全部 = Sort.by(ASC,"id"))。sort_no 是「类型内」序号,跨类型打平无全局意义,
|
||||
// 故「全部」不参与生效排序;选中类型仍走 override/主表生效排序(拖拽结果可见)。
|
||||
List<ApplicationInfoVo> vos = filtered.stream()
|
||||
.map(app -> mergeAppVo(app, overrides))
|
||||
.sorted(effectiveAppOrder(overrides))
|
||||
.sorted(categoryId == null ? appIdOrder() : effectiveAppOrder(overrides))
|
||||
.collect(Collectors.toList());
|
||||
return pageInMemory(vos, pageable);
|
||||
}
|
||||
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
package com.yunhai.userbase.service.impl;
|
||||
|
||||
import com.yunhai.platform.utils.model.BoaApp;
|
||||
import com.yunhai.platform.utils.model.BoaAppType;
|
||||
import com.yunhai.userbase.entity.AppCategory;
|
||||
import com.yunhai.userbase.entity.ApplicationInfo;
|
||||
import com.yunhai.userbase.repository.AppCategoryRepository;
|
||||
import com.yunhai.userbase.repository.ApplicationInfoRepository;
|
||||
import com.yunhai.userbase.repository.SysUserGroupRelRepository;
|
||||
import com.yunhai.userbase.service.IGroupAuthzService;
|
||||
import com.yunhai.userbase.service.ISysGroupService;
|
||||
import com.yunhai.userbase.service.resolve.MainGroupResolver;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyList;
|
||||
import static org.mockito.ArgumentMatchers.anyMap;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* AppCategoryService.listBoaAppType 基石应用过滤单元测试(2026-10-08 修复)。<br>
|
||||
* 契约:用户端应用列表(门户 /apps)不出现基石应用 ——
|
||||
* <ul>
|
||||
* <li>UserBase 自身(appKey ≤ 100000)被排除(超管分支 findUserAuthorizedAppIds 返回全量时的兜底);</li>
|
||||
* <li>app_nature=BASE(UserHome / 移动门户)被排除;</li>
|
||||
* <li>BUSINESS 应用保留;</li>
|
||||
* <li>无基石行时输出与改动前逐项等价(不误伤)。</li>
|
||||
* </ul>
|
||||
*/
|
||||
class AppCategoryServiceBaseAppFilterTest {
|
||||
|
||||
@InjectMocks
|
||||
private AppCategoryService appCategoryService;
|
||||
|
||||
@Mock
|
||||
private AppCategoryRepository appCategoryRepository;
|
||||
|
||||
@Mock
|
||||
private ApplicationInfoRepository applicationInfoRepository;
|
||||
|
||||
@Mock
|
||||
private IGroupAuthzService groupAuthzService;
|
||||
|
||||
@Mock
|
||||
private SchoolAppOverrideService schoolAppOverrideService;
|
||||
|
||||
@Mock
|
||||
private IdentityService identityService;
|
||||
|
||||
@Mock
|
||||
private MainGroupResolver mainGroupResolver;
|
||||
|
||||
@Mock
|
||||
private ISysGroupService sysGroupService;
|
||||
|
||||
@Mock
|
||||
private SysUserGroupRelRepository sysUserGroupRelRepository;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
MockitoAnnotations.openMocks(this);
|
||||
AppCategory category = new AppCategory();
|
||||
category.setId(10);
|
||||
category.setName("办公管理");
|
||||
category.setSortNo(1);
|
||||
when(appCategoryRepository.findAllByOrderBySortNoAsc()).thenReturn(Collections.singletonList(category));
|
||||
// 无学校语境(平台账号):identity 无、主归属无 → applyAlias 不合并,直接看构建结果
|
||||
when(identityService.getCurrent(1)).thenReturn(null);
|
||||
when(mainGroupResolver.resolve(1)).thenReturn(Optional.empty());
|
||||
// 合并口透传第一参数(本测试只断言过滤后的构建结果)
|
||||
when(schoolAppOverrideService.applyAlias(anyList(), anyMap(), any()))
|
||||
.thenAnswer(inv -> inv.getArgument(0));
|
||||
}
|
||||
|
||||
/** 基石三类:UserBase 自身(appKey=100000)/ app_nature=BASE 均排除,BUSINESS 保留 */
|
||||
@Test
|
||||
void baseApps_excluded_businessKept() {
|
||||
List<ApplicationInfo> apps = Arrays.asList(
|
||||
app(1, 100000, null, "用户中心"),
|
||||
app(2, 100001, "BASE", "用户之家"),
|
||||
app(3, 100002, "BUSINESS", "通知公告"));
|
||||
when(applicationInfoRepository.findAll()).thenReturn(apps);
|
||||
// 超管分支:授权集合为全量(基石也在内)——正是 bug1 的触发条件
|
||||
when(groupAuthzService.findUserAuthorizedAppIds(1)).thenReturn(Arrays.asList(1, 2, 3));
|
||||
|
||||
List<BoaAppType> result = appCategoryService.listBoaAppType(1, null, null);
|
||||
|
||||
assertEquals(1, result.size());
|
||||
List<BoaApp> kept = result.get(0).getApps();
|
||||
assertEquals(1, kept.size());
|
||||
assertEquals(Integer.valueOf(3), kept.get(0).getId());
|
||||
assertEquals("通知公告", kept.get(0).getName());
|
||||
}
|
||||
|
||||
/** 无基石行时过滤逻辑零影响:全部 BUSINESS 应用按原顺序保留 */
|
||||
@Test
|
||||
void noBaseApps_allKeptUnchanged() {
|
||||
when(applicationInfoRepository.findAll()).thenReturn(Arrays.asList(
|
||||
app(3, 100002, "BUSINESS", "通知公告"),
|
||||
app(4, null, null, "老应用")));
|
||||
when(groupAuthzService.findUserAuthorizedAppIds(1)).thenReturn(Arrays.asList(3, 4));
|
||||
|
||||
List<BoaApp> kept = appCategoryService.listBoaAppType(1, null, null).get(0).getApps();
|
||||
|
||||
assertEquals(2, kept.size());
|
||||
assertEquals(Integer.valueOf(3), kept.get(0).getId());
|
||||
assertEquals(Integer.valueOf(4), kept.get(1).getId());
|
||||
}
|
||||
|
||||
/** 无权限 / 无客户端命中的应用原本就被挡(回归:基石过滤不改变这两条既有口径) */
|
||||
@Test
|
||||
void unauthorizedBaseApp_stillNoneKept() {
|
||||
when(applicationInfoRepository.findAll())
|
||||
.thenReturn(new ArrayList<>(Collections.singletonList(app(2, 100001, "BASE", "用户之家"))));
|
||||
when(groupAuthzService.findUserAuthorizedAppIds(1)).thenReturn(Collections.singletonList(2));
|
||||
|
||||
assertEquals(0, appCategoryService.listBoaAppType(1, null, null).get(0).getApps().size());
|
||||
}
|
||||
|
||||
private static ApplicationInfo app(Integer id, Integer appKey, String appNature, String name) {
|
||||
ApplicationInfo app = new ApplicationInfo();
|
||||
app.setId(id);
|
||||
app.setAppKey(appKey);
|
||||
app.setAppNature(appNature);
|
||||
app.setName(name);
|
||||
app.setCategoryId(10);
|
||||
app.setSortNo(id);
|
||||
return app;
|
||||
}
|
||||
}
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
package com.yunhai.userbase.service.impl;
|
||||
|
||||
import com.yunhai.userbase.entity.ApplicationInfo;
|
||||
import com.yunhai.userbase.entity.SysGroupAppRel;
|
||||
import com.yunhai.userbase.entity.SysSchoolAppOverride;
|
||||
import com.yunhai.userbase.model.application.ApplicationInfoQuery;
|
||||
import com.yunhai.userbase.model.application.ApplicationInfoVo;
|
||||
import com.yunhai.userbase.repository.AppCategoryRepository;
|
||||
import com.yunhai.userbase.repository.ApplicationInfoRepository;
|
||||
import com.yunhai.userbase.repository.SysGroupAppRelRepository;
|
||||
import com.yunhai.userbase.repository.SysGroupRepository;
|
||||
import com.yunhai.userbase.repository.SysSchoolAppOverrideRepository;
|
||||
import com.yunhai.userbase.repository.SysSchoolCategoryOverrideRepository;
|
||||
import com.yunhai.userbase.service.resolve.AccessScope;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
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.mockito.ArgumentMatchers.anyIterable;
|
||||
import static org.mockito.ArgumentMatchers.anyList;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* SchoolAppOverrideService.listSchoolApps 视图分叉排序单元测试(2026-10-08 修复)。<br>
|
||||
* 契约(学校管理员应用列表):
|
||||
* <ul>
|
||||
* <li>「全部」视图(categoryId=null)→ 按 id 正序(与 SUPER 分支 Sort.by(ASC,"id") 同口径),
|
||||
* 最新创建的在最后,主表/覆盖 sort_no 均不影响;</li>
|
||||
* <li>选中应用类型 → 仍按生效排序(override sortNo 优先,空回落主表,并列 id 升序),拖拽结果不回归。</li>
|
||||
* </ul>
|
||||
*/
|
||||
class SchoolAppOverrideServiceListSchoolAppsTest {
|
||||
|
||||
private static final Integer SCHOOL_ID = 7;
|
||||
private static final Integer CAT_A = 10;
|
||||
|
||||
@InjectMocks
|
||||
private SchoolAppOverrideService service;
|
||||
|
||||
@Mock
|
||||
private SysSchoolAppOverrideRepository appOverrideRepository;
|
||||
|
||||
@Mock
|
||||
private SysSchoolCategoryOverrideRepository categoryOverrideRepository;
|
||||
|
||||
@Mock
|
||||
private SysGroupAppRelRepository sysGroupAppRelRepository;
|
||||
|
||||
@Mock
|
||||
private ApplicationInfoRepository applicationInfoRepository;
|
||||
|
||||
@Mock
|
||||
private AppCategoryRepository appCategoryRepository;
|
||||
|
||||
@Mock
|
||||
private SysGroupRepository sysGroupRepository;
|
||||
|
||||
private final AccessScope scope = AccessScope.school(Collections.singletonList(SCHOOL_ID));
|
||||
|
||||
private final Pageable pageable = PageRequest.of(0, 20);
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
MockitoAnnotations.openMocks(this);
|
||||
when(appOverrideRepository.findBySchoolIdIn(anyList())).thenReturn(Collections.emptyList());
|
||||
when(sysGroupAppRelRepository.findByGroupIdIn(anyList()))
|
||||
.thenReturn(Arrays.asList(rel(1), rel(2), rel(3)));
|
||||
// 主表 sortNo 与 id 反序(id 升序 = 3 的 sortNo 最小),两种视图结果必然不同,便于断言分叉生效
|
||||
// 注意 findAllById 的入参是 openedAppIds 产出的 Set,须用 anyIterable 匹配(anyList 不匹配 Set)
|
||||
when(applicationInfoRepository.findAllById(anyIterable())).thenReturn(Arrays.asList(
|
||||
app(1, CAT_A, "应用一", 3),
|
||||
app(2, CAT_A, "应用二", 2),
|
||||
app(3, CAT_A, "应用三", 1)));
|
||||
}
|
||||
|
||||
/** 「全部」视图:按 id 正序,主表 sortNo 不参与 */
|
||||
@Test
|
||||
void allView_orderedByIdAsc() {
|
||||
List<Integer> ids = idsOf(service.listSchoolApps(new ApplicationInfoQuery(), pageable, scope, null));
|
||||
assertEquals(Arrays.asList(1, 2, 3), ids);
|
||||
}
|
||||
|
||||
/** 「全部」视图:有覆盖 sort_no 也不参与(sticky 排序不影响全部视图) */
|
||||
@Test
|
||||
void allView_ignoresOverrideSortNo() {
|
||||
when(appOverrideRepository.findBySchoolIdIn(anyList()))
|
||||
.thenReturn(new ArrayList<>(Collections.singletonList(override(2, 0))));
|
||||
|
||||
List<Integer> ids = idsOf(service.listSchoolApps(new ApplicationInfoQuery(), pageable, scope, null));
|
||||
assertEquals(Arrays.asList(1, 2, 3), ids);
|
||||
}
|
||||
|
||||
/** 选中应用类型:仍按主表生效排序(sortNo 升序) */
|
||||
@Test
|
||||
void categoryView_orderedByEffectiveSortNo() {
|
||||
List<Integer> ids = idsOf(service.listSchoolApps(queryOfCategory(CAT_A), pageable, scope, null));
|
||||
assertEquals(Arrays.asList(3, 2, 1), ids);
|
||||
}
|
||||
|
||||
/** 选中应用类型:override sort_no 优先于主表(排序管理结果不回归) */
|
||||
@Test
|
||||
void categoryView_overrideSortNoWins() {
|
||||
when(appOverrideRepository.findBySchoolIdIn(anyList()))
|
||||
.thenReturn(new ArrayList<>(Collections.singletonList(override(2, 0))));
|
||||
|
||||
List<Integer> ids = idsOf(service.listSchoolApps(queryOfCategory(CAT_A), pageable, scope, null));
|
||||
assertEquals(Arrays.asList(2, 3, 1), ids);
|
||||
}
|
||||
|
||||
/** 关键词过滤仍生效,且「全部」视图内按 id 正序 */
|
||||
@Test
|
||||
void allView_keywordFilterKept() {
|
||||
ApplicationInfoQuery query = new ApplicationInfoQuery();
|
||||
query.setName("应用三");
|
||||
|
||||
List<Integer> ids = idsOf(service.listSchoolApps(query, pageable, scope, null));
|
||||
assertEquals(Collections.singletonList(3), ids);
|
||||
}
|
||||
|
||||
// ===================== 辅助 =====================
|
||||
|
||||
private static List<Integer> idsOf(Page<ApplicationInfoVo> page) {
|
||||
return page.getContent().stream().map(ApplicationInfoVo::getId).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private static ApplicationInfoQuery queryOfCategory(Integer categoryId) {
|
||||
ApplicationInfoQuery query = new ApplicationInfoQuery();
|
||||
query.setCategoryId(categoryId);
|
||||
return query;
|
||||
}
|
||||
|
||||
private static SysGroupAppRel rel(Integer appId) {
|
||||
SysGroupAppRel rel = new SysGroupAppRel();
|
||||
rel.setGroupId(SCHOOL_ID);
|
||||
rel.setAppId(appId);
|
||||
return rel;
|
||||
}
|
||||
|
||||
private static SysSchoolAppOverride override(Integer appId, Integer sortNo) {
|
||||
SysSchoolAppOverride o = new SysSchoolAppOverride();
|
||||
o.setSchoolId(SCHOOL_ID);
|
||||
o.setAppId(appId);
|
||||
o.setSortNo(sortNo);
|
||||
return o;
|
||||
}
|
||||
|
||||
private static ApplicationInfo app(Integer id, Integer categoryId, String name, Integer sortNo) {
|
||||
ApplicationInfo app = new ApplicationInfo();
|
||||
app.setId(id);
|
||||
app.setCategoryId(categoryId);
|
||||
app.setName(name);
|
||||
app.setSortNo(sortNo);
|
||||
return app;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user