feat(auth): U2 组织架构 Schema 扩展 org_type=5 岗位

V5 migration 新增岗位类型(5) + 种子岗位数据(维修工/维修组长), OrgTypeEnum 枚举, OrgService.list 增加 orgType 可选过滤. 满足 R5/R6, 解决 OQ-7/8
This commit is contained in:
ether 2026-07-02 01:16:30 +08:00
parent f3110d0ce6
commit 444381e831
8 changed files with 241 additions and 6 deletions

View File

@ -31,8 +31,9 @@ public class OrgController {
@GetMapping
public Result<List<OrgDTO>> list(@RequestParam(required = false) Long parentId,
@RequestParam(required = false) Integer status,
@RequestParam(required = false) Long projectId) {
return Result.success(orgService.list(parentId, status, projectId));
@RequestParam(required = false) Long projectId,
@RequestParam(required = false) Integer orgType) {
return Result.success(orgService.list(parentId, status, projectId, orgType));
}
/**

View File

@ -19,7 +19,7 @@ public class OrgDTO implements Serializable {
private String orgName;
/** 类型1公司 2部门 3小组 4项目部 */
/** 类型1公司 2部门 3小组 4项目部 5岗位 */
private Integer orgType;
private Long leaderId;

View File

@ -22,7 +22,7 @@ public class Org extends BaseEntity {
/** 组织名称 */
private String orgName;
/** 类型1公司 2部门 3小组 4项目部 */
/** 类型1公司 2部门 3小组 4项目部 5岗位 */
private Integer orgType;
/** 负责人用户ID */

View File

@ -0,0 +1,32 @@
package com.pms.auth.enums;
import lombok.Getter;
/**
* 组织类型枚举
*/
@Getter
public enum OrgTypeEnum {
COMPANY(1, "公司"),
DEPARTMENT(2, "部门"),
TEAM(3, "小组"),
PROJECT_DEPT(4, "项目部"),
POSITION(5, "岗位");
private final int code;
private final String desc;
OrgTypeEnum(int code, String desc) {
this.code = code;
this.desc = desc;
}
public static OrgTypeEnum fromCode(Integer code) {
if (code == null) return null;
for (OrgTypeEnum type : values()) {
if (type.code == code) return type;
}
return null;
}
}

View File

@ -12,8 +12,11 @@ public interface OrgService {
/**
* 组织列表树形
*
* @param orgType 组织类型过滤1公司 2部门 3小组 4项目部 5岗位null 表示不过滤
* 指定时返回扁平列表岗位等节点无根节点无法成树
*/
List<OrgDTO> list(Long parentId, Integer status, Long projectId);
List<OrgDTO> list(Long parentId, Integer status, Long projectId, Integer orgType);
/**
* 创建组织

View File

@ -30,7 +30,7 @@ public class OrgServiceImpl implements OrgService {
private final OrgMapper orgMapper;
@Override
public List<OrgDTO> list(Long parentId, Integer status, Long projectId) {
public List<OrgDTO> list(Long parentId, Integer status, Long projectId, Integer orgType) {
Long currentProjectId = projectId != null ? projectId : UserContext.getProjectId();
LambdaQueryWrapper<Org> wrapper = new LambdaQueryWrapper<>();
@ -38,6 +38,9 @@ public class OrgServiceImpl implements OrgService {
if (status != null) {
wrapper.eq(Org::getStatus, status);
}
if (orgType != null) {
wrapper.eq(Org::getOrgType, orgType);
}
if (currentProjectId != null) {
wrapper.and(w -> w.eq(Org::getProjectId, currentProjectId).or().isNull(Org::getProjectId));
}
@ -45,6 +48,11 @@ public class OrgServiceImpl implements OrgService {
List<Org> orgs = orgMapper.selectList(wrapper);
// 按组织类型过滤时返回扁平列表岗位等节点无 parent_id=0 根节点无法成树
if (orgType != null) {
return orgs.stream().map(this::toDTO).collect(Collectors.toList());
}
// 构建树
List<OrgDTO> tree = buildTree(orgs);

View File

@ -0,0 +1,19 @@
-- ========================================
-- V5: 扩展 t_org.org_type 枚举:新增 5=岗位 类型
-- 岗位挂在部门下t_project_user.org_id 指向 org_type=5 的节点
-- ========================================
-- 更新 org_type 列注释无需修改数据类型TINYINT 已足够)
ALTER TABLE `t_org` MODIFY COLUMN `org_type` TINYINT NOT NULL DEFAULT 1 COMMENT '类型1公司 2部门 3小组 4项目部 5岗位';
-- 种子岗位数据:在总部(id=1)下创建工程部,工程部下创建岗位
-- id=2,3,4 与 V1 种子数据(仅 t_org.id=1无冲突
-- 先创建工程部(如果不存在)
INSERT INTO `t_org` (`id`, `project_id`, `parent_id`, `org_code`, `org_name`, `org_type`, `leader_id`, `phone`, `sort`, `status`, `created_at`, `updated_at`, `created_by`, `updated_by`, `deleted`)
SELECT 2, NULL, 1, 'ENG', '工程部', 2, NULL, NULL, 1, 1, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0
WHERE NOT EXISTS (SELECT 1 FROM `t_org` WHERE `org_code` = 'ENG' AND `deleted` = 0);
-- 工程部下创建岗位节点
INSERT INTO `t_org` (`id`, `project_id`, `parent_id`, `org_code`, `org_name`, `org_type`, `leader_id`, `phone`, `sort`, `status`, `created_at`, `updated_at`, `created_by`, `updated_by`, `deleted`) VALUES
(3, NULL, 2, 'POS_REPAIR', '维修工', 5, NULL, NULL, 1, 1, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0),
(4, NULL, 2, 'POS_REPAIR_LEAD', '维修组长', 5, NULL, NULL, 2, 1, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000, NULL, NULL, 0);

View File

@ -0,0 +1,172 @@
package com.pms.auth.service;
import com.pms.auth.dto.OrgDTO;
import com.pms.auth.entity.Org;
import com.pms.auth.enums.OrgTypeEnum;
import com.pms.auth.mapper.OrgMapper;
import com.pms.auth.service.impl.OrgServiceImpl;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* 组织岗位org_type=5服务测试
* 使用 Mockito Mock OrgMapper验证 org_type 过滤与岗位种子数据查询
*/
@DisplayName("OrgService 岗位org_type=5测试")
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
class OrgServicePositionTest {
@Mock
private OrgMapper orgMapper;
@InjectMocks
private OrgServiceImpl orgService;
private Org buildOrg(Long id, Long parentId, String code, String name, int type, int sort) {
Org org = new Org();
org.setId(id);
org.setParentId(parentId);
org.setOrgCode(code);
org.setOrgName(name);
org.setOrgType(type);
org.setSort(sort);
org.setStatus(1);
org.setDeleted(0);
return org;
}
@Nested
@DisplayName("OrgTypeEnum 枚举测试")
class OrgTypeEnumTest {
@Test
@DisplayName("fromCode 正确映射全部类型(含 5=岗位)")
void fromCode_allTypes() {
assertThat(OrgTypeEnum.fromCode(1)).isEqualTo(OrgTypeEnum.COMPANY);
assertThat(OrgTypeEnum.fromCode(2)).isEqualTo(OrgTypeEnum.DEPARTMENT);
assertThat(OrgTypeEnum.fromCode(3)).isEqualTo(OrgTypeEnum.TEAM);
assertThat(OrgTypeEnum.fromCode(4)).isEqualTo(OrgTypeEnum.PROJECT_DEPT);
assertThat(OrgTypeEnum.fromCode(5)).isEqualTo(OrgTypeEnum.POSITION);
}
@Test
@DisplayName("POSITION 编码与描述正确")
void position_codeAndDesc() {
assertThat(OrgTypeEnum.POSITION.getCode()).isEqualTo(5);
assertThat(OrgTypeEnum.POSITION.getDesc()).isEqualTo("岗位");
}
@Test
@DisplayName("fromCode(null) 返回 null")
void fromCode_null_returnsNull() {
assertThat(OrgTypeEnum.fromCode(null)).isNull();
}
@Test
@DisplayName("fromCode 非法值0 或 >5返回 null")
void fromCode_invalid_returnsNull() {
assertThat(OrgTypeEnum.fromCode(0)).isNull();
assertThat(OrgTypeEnum.fromCode(6)).isNull();
assertThat(OrgTypeEnum.fromCode(-1)).isNull();
}
}
@Nested
@DisplayName("list 按 org_type 过滤测试")
class ListFilterTest {
@Test
@DisplayName("orgType=5 返回岗位扁平列表(含种子岗位名称)")
void list_orgType5_returnsPositionsFlat() {
Org pos1 = buildOrg(3L, 2L, "POS_REPAIR", "维修工", 5, 1);
Org pos2 = buildOrg(4L, 2L, "POS_REPAIR_LEAD", "维修组长", 5, 2);
when(orgMapper.selectList(any())).thenReturn(List.of(pos1, pos2));
List<OrgDTO> result = orgService.list(null, null, null, 5);
// 扁平列表岗位 parent_id=2 无根节点若走树形逻辑则返回空
// 返回 2 条证明走的是 orgType 过滤的扁平路径 DTO 映射保留了类型与名称
assertThat(result).hasSize(2);
assertThat(result).extracting(OrgDTO::getOrgType).containsOnly(5);
assertThat(result).extracting(OrgDTO::getOrgName)
.containsExactly("维修工", "维修组长");
// ponytail: wrapper.eq(Org::getOrgType, orgType) 为平凡一行不单独断言其 SQL
// MyBatis-Plus LambdaQueryWrapper 在无 Mybatis 上下文的纯单测中无法 getSqlSegment()
// 扁平列表行为已证明 orgType 分支被命中
verify(orgMapper).selectList(any());
}
@Test
@DisplayName("orgType=0非法返回空列表")
void list_orgType0_returnsEmpty() {
when(orgMapper.selectList(any())).thenReturn(List.of());
List<OrgDTO> result = orgService.list(null, null, null, 0);
assertThat(result).isEmpty();
}
@Test
@DisplayName("orgType=99超出范围返回空列表")
void list_orgType99_returnsEmpty() {
when(orgMapper.selectList(any())).thenReturn(List.of());
List<OrgDTO> result = orgService.list(null, null, null, 99);
assertThat(result).isEmpty();
}
@Test
@DisplayName("orgType=null 不过滤,返回树形结构")
void list_orgTypeNull_returnsTree() {
Org hq = buildOrg(1L, 0L, "HQ", "物业公司总部", 1, 1);
Org dept = buildOrg(2L, 1L, "ENG", "工程部", 2, 1);
when(orgMapper.selectList(any())).thenReturn(List.of(hq, dept));
List<OrgDTO> result = orgService.list(null, null, null, null);
// 树根为 HQ工程部为其子节点
assertThat(result).hasSize(1);
OrgDTO root = result.get(0);
assertThat(root.getOrgName()).isEqualTo("物业公司总部");
assertThat(root.getChildren()).hasSize(1);
assertThat(root.getChildren().get(0).getOrgName()).isEqualTo("工程部");
}
}
@Nested
@DisplayName("t_project_user.org_id 指向岗位节点测试")
class PositionLookupTest {
@Test
@DisplayName("通过 org_id 查询岗位节点返回正确岗位名称与类型")
void lookupPositionByOrgId_returnsCorrectName() {
// 模拟 t_project_user.org_id=3 指向岗位节点
Org position = buildOrg(3L, 2L, "POS_REPAIR", "维修工", 5, 1);
when(orgMapper.selectById(3L)).thenReturn(position);
Org org = orgMapper.selectById(3L);
assertThat(org).isNotNull();
assertThat(org.getOrgType()).isEqualTo(5);
assertThat(org.getOrgName()).isEqualTo("维修工");
assertThat(OrgTypeEnum.fromCode(org.getOrgType())).isEqualTo(OrgTypeEnum.POSITION);
}
}
}