# 团队管理功能开发指南 ## 📋 目标 实现技术团队管理功能,支持: - 产品线和团队的层级管理 - 团队成员管理(支持跨部门) - 团队与应用的分支级别关联(一个团队负责多个应用的特定分支) - 团队-应用-环境的差异化配置 ## 🎯 核心设计 ### 关系层级 ``` deploy_product_line (产品线) ↓ (1:N) deploy_team (开发团队) ↓ (M:N with branch_pattern) deploy_team_application (团队-应用关联,支持分支模式) ↓ (关联到) deploy_application (应用) ↓ (M:N with team-specific config) deploy_application_environment_config (应用-环境配置,支持团队差异化) ↓ deploy_environment (环境) ``` ### 关键特性 1. **产品线管理**: `deploy_project_group` 重命名为 `deploy_product_line`,更清晰表达业务含义 2. **分支级别关联**: 团队可以负责应用的特定分支(如 `feature/*`, `release/*`) 3. **差异化配置**: 同一应用在同一环境,不同团队可以有不同的配置(如资源限制、环境变量) ## 🎯 开发步骤 ### Phase 1: 数据库设计和迁移 #### 步骤 1.1: 创建数据库迁移脚本 **文件**: `src/main/resources/db/changelog/changes/v1.0.2-team-management.sql` ```sql -- ===================================================================== -- 团队管理功能表结构 v2.0 -- 关键改动: -- 1. deploy_project_group -> deploy_product_line(重命名) -- 2. deploy_team_application 增加 branch_pattern(支持分支级别管理) -- 3. 新增 deploy_application_environment_config(支持团队差异化配置) -- ===================================================================== -- ===================================================================== -- 第一步:重命名产品线表 -- ===================================================================== -- 重命名表名 RENAME TABLE deploy_project_group TO deploy_product_line; -- 重命名字段(保持语义一致) ALTER TABLE deploy_product_line CHANGE COLUMN project_group_code product_line_code VARCHAR(50) NOT NULL COMMENT '产品线编码', CHANGE COLUMN project_group_name product_line_name VARCHAR(100) NOT NULL COMMENT '产品线名称', CHANGE COLUMN project_group_desc product_line_desc VARCHAR(255) NULL COMMENT '产品线描述'; -- 更新注释 ALTER TABLE deploy_product_line COMMENT='产品线表(原项目组表)'; -- ===================================================================== -- 第二步:创建团队表 -- ===================================================================== -- 1. 技术团队表 CREATE TABLE deploy_team ( id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID', -- 基本信息 team_code VARCHAR(50) NOT NULL COMMENT '团队编码(如:USER_SERVICE_TEAM)', team_name VARCHAR(100) NOT NULL COMMENT '团队名称(如:用户服务团队)', team_desc VARCHAR(500) NULL COMMENT '团队描述', -- 产品线关联 product_line_id BIGINT NOT NULL COMMENT '所属产品线ID', -- 团队负责人 leader_id BIGINT NULL COMMENT '团队负责人ID', enabled BIT NOT NULL DEFAULT 1 COMMENT '是否启用', sort INT NOT NULL DEFAULT 0 COMMENT '排序号', -- 基础字段 create_by VARCHAR(100) NULL COMMENT '创建人', create_time DATETIME(6) NULL COMMENT '创建时间', update_by VARCHAR(100) NULL COMMENT '更新人', update_time DATETIME(6) NULL COMMENT '更新时间', version INT NOT NULL DEFAULT 1 COMMENT '版本号', deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除', UNIQUE INDEX uk_team_code (team_code), INDEX idx_leader_id (leader_id), INDEX idx_product_line_id (product_line_id), CONSTRAINT fk_team_leader FOREIGN KEY (leader_id) REFERENCES sys_user (id), CONSTRAINT fk_team_product_line FOREIGN KEY (product_line_id) REFERENCES deploy_product_line (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='技术团队表'; -- 2. 团队成员表 CREATE TABLE deploy_team_member ( id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID', team_id BIGINT NOT NULL COMMENT '团队ID', user_id BIGINT NOT NULL COMMENT '用户ID', join_time DATETIME(6) NULL COMMENT '加入时间', -- 基础字段 create_by VARCHAR(100) NULL COMMENT '创建人', create_time DATETIME(6) NULL COMMENT '创建时间', update_by VARCHAR(100) NULL COMMENT '更新人', update_time DATETIME(6) NULL COMMENT '更新时间', version INT NOT NULL DEFAULT 1 COMMENT '版本号', deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除', UNIQUE INDEX uk_team_user (team_id, user_id), INDEX idx_user_id (user_id), CONSTRAINT fk_team_member_team FOREIGN KEY (team_id) REFERENCES deploy_team (id), CONSTRAINT fk_team_member_user FOREIGN KEY (user_id) REFERENCES sys_user (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='团队成员表'; -- 3. 团队应用关联表(支持分支模式) CREATE TABLE deploy_team_application ( id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID', team_id BIGINT NOT NULL COMMENT '团队ID', application_id BIGINT NOT NULL COMMENT '应用ID', -- ✅ 新增:分支模式匹配 branch_pattern VARCHAR(255) NULL COMMENT '负责的分支模式(如:feature/*、release/*、master、develop)', responsibility VARCHAR(50) NOT NULL DEFAULT 'PRIMARY' COMMENT '职责类型:PRIMARY-主要负责,SECONDARY-协作支持', -- 基础字段 create_by VARCHAR(100) NULL COMMENT '创建人', create_time DATETIME(6) NULL COMMENT '创建时间', update_by VARCHAR(100) NULL COMMENT '更新人', update_time DATETIME(6) NULL COMMENT '更新时间', version INT NOT NULL DEFAULT 1 COMMENT '版本号', deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除', UNIQUE INDEX uk_team_app_branch (team_id, application_id, branch_pattern), INDEX idx_application_id (application_id), CONSTRAINT fk_team_app_team FOREIGN KEY (team_id) REFERENCES deploy_team (id), CONSTRAINT fk_team_app_application FOREIGN KEY (application_id) REFERENCES deploy_application (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='团队应用关联表(支持分支级别管理)'; -- 4. 应用环境配置表(支持团队差异化配置) CREATE TABLE deploy_application_environment_config ( id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID', application_id BIGINT NOT NULL COMMENT '应用ID', environment_id BIGINT NOT NULL COMMENT '环境ID', team_id BIGINT NULL COMMENT '团队ID(为NULL时表示默认配置,非NULL时表示团队专属配置)', -- 配置内容(JSON格式) config JSON NULL COMMENT '环境配置(如:资源限制、环境变量、部署参数等)', enabled BIT NOT NULL DEFAULT 1 COMMENT '是否启用', -- 基础字段 create_by VARCHAR(100) NULL COMMENT '创建人', create_time DATETIME(6) NULL COMMENT '创建时间', update_by VARCHAR(100) NULL COMMENT '更新人', update_time DATETIME(6) NULL COMMENT '更新时间', version INT NOT NULL DEFAULT 1 COMMENT '版本号', deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除', UNIQUE INDEX uk_app_env_team (application_id, environment_id, team_id), INDEX idx_environment_id (environment_id), INDEX idx_team_id (team_id), CONSTRAINT fk_app_env_config_application FOREIGN KEY (application_id) REFERENCES deploy_application (id), CONSTRAINT fk_app_env_config_environment FOREIGN KEY (environment_id) REFERENCES deploy_environment (id), CONSTRAINT fk_app_env_config_team FOREIGN KEY (team_id) REFERENCES deploy_team (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='应用环境配置表(支持团队差异化配置)'; -- ===================================================================== -- 第三步:清理旧表 -- ===================================================================== -- 删除旧的产品线环境关联表 DROP TABLE IF EXISTS deploy_project_group_environment; -- ===================================================================== -- 第四步:调整 deploy_application 表 -- ===================================================================== -- 更新产品线关联(如果存在旧的 project_group_id 字段) ALTER TABLE deploy_application DROP FOREIGN KEY IF EXISTS fk_application_project_group; -- 删除冗余字段 ALTER TABLE deploy_application DROP COLUMN IF EXISTS project_group_id, DROP COLUMN IF EXISTS repo_group_id, DROP COLUMN IF EXISTS external_system_id; -- ===================================================================== -- 第五步:优化 deploy_log 表 -- ===================================================================== ALTER TABLE deploy_log DROP COLUMN IF EXISTS form_variables, DROP COLUMN IF EXISTS deploy_variables, ADD COLUMN deploy_branch VARCHAR(200) NULL COMMENT '部署分支', ADD COLUMN team_id BIGINT NULL COMMENT '部署团队ID', ADD COLUMN deploy_status VARCHAR(50) NULL COMMENT '部署状态:SUCCESS,FAILED,IN_PROGRESS', ADD COLUMN start_time DATETIME(6) NULL COMMENT '开始时间', ADD COLUMN end_time DATETIME(6) NULL COMMENT '结束时间', ADD COLUMN error_message TEXT NULL COMMENT '错误信息', ADD INDEX idx_team_id (team_id); ``` #### 步骤 1.2: 注册迁移脚本 **文件**: `src/main/resources/db/changelog/db.changelog-master.yaml` ```yaml - changeSet: id: v1.0.2-team-management author: qqchen runOnChange: false failOnError: true comment: "团队管理功能表结构" sqlFile: path: db/changelog/changes/v1.0.2-team-management.sql stripComments: false splitStatements: true endDelimiter: ";" rollback: - sql: "DROP TABLE IF EXISTS deploy_team_environment;" - sql: "DROP TABLE IF EXISTS deploy_team_application;" - sql: "DROP TABLE IF EXISTS deploy_team_member;" - sql: "DROP TABLE IF EXISTS deploy_team;" ``` --- ### Phase 2: 枚举类创建 #### 步骤 2.1: 创建团队类型枚举 **文件**: `src/main/java/com/qqchen/deploy/backend/deploy/enums/TeamTypeEnum.java` ```java package com.qqchen.deploy.backend.deploy.enums; import lombok.Getter; /** * 团队类型枚举 */ @Getter public enum TeamTypeEnum { BACKEND("后端团队"), FRONTEND("前端团队"), FULLSTACK("全栈团队"), OPS("运维团队"), QA("测试团队"); private final String description; TeamTypeEnum(String description) { this.description = description; } } ``` #### 步骤 2.2: 创建团队角色枚举 **文件**: `src/main/java/com/qqchen/deploy/backend/deploy/enums/TeamRoleEnum.java` ```java package com.qqchen.deploy.backend.deploy.enums; import lombok.Getter; /** * 团队角色枚举 */ @Getter public enum TeamRoleEnum { LEADER("技术负责人"), BACKEND_DEVELOPER("后端开发"), FRONTEND_DEVELOPER("前端开发"), TESTER("测试工程师"), OPS("运维工程师"), ARCHITECT("架构师"); private final String description; TeamRoleEnum(String description) { this.description = description; } } ``` #### 步骤 2.3: 创建团队职责枚举 **文件**: `src/main/java/com/qqchen/deploy/backend/deploy/enums/TeamResponsibilityEnum.java` ```java package com.qqchen.deploy.backend.deploy.enums; import lombok.Getter; /** * 团队应用职责类型枚举 */ @Getter public enum TeamResponsibilityEnum { PRIMARY("主要负责"), SECONDARY("协作支持"); private final String description; TeamResponsibilityEnum(String description) { this.description = description; } } ``` --- ### Phase 3: 实体类创建 #### 步骤 3.1: 创建团队实体 **文件**: `src/main/java/com/qqchen/deploy/backend/deploy/entity/Team.java` ```java package com.qqchen.deploy.backend.deploy.entity; import com.qqchen.deploy.backend.deploy.enums.TeamTypeEnum; import com.qqchen.deploy.backend.framework.domain.Entity; import jakarta.persistence.*; import lombok.Data; import lombok.EqualsAndHashCode; /** * 技术团队实体 */ @Data @EqualsAndHashCode(callSuper = true) @jakarta.persistence.Entity @Table(name = "deploy_team") public class Team extends Entity { @Column(name = "team_code", nullable = false, length = 50) private String teamCode; @Column(name = "team_name", nullable = false, length = 100) private String teamName; @Column(name = "team_desc", length = 500) private String teamDesc; @Column(name = "product_line_id", nullable = false) private Long productLineId; @Column(name = "leader_id") private Long leaderId; @Column(name = "enabled", nullable = false) private Boolean enabled = true; @Column(name = "sort", nullable = false) private Integer sort = 0; } ``` #### 步骤 3.2: 创建团队成员实体 **文件**: `src/main/java/com/qqchen/deploy/backend/deploy/entity/TeamMember.java` ```java package com.qqchen.deploy.backend.deploy.entity; import com.qqchen.deploy.backend.deploy.enums.TeamRoleEnum; import com.qqchen.deploy.backend.framework.domain.Entity; import jakarta.persistence.*; import lombok.Data; import lombok.EqualsAndHashCode; import java.time.LocalDateTime; /** * 团队成员实体 */ @Data @EqualsAndHashCode(callSuper = true) @jakarta.persistence.Entity @Table(name = "deploy_team_member") public class TeamMember extends Entity { @Column(name = "team_id", nullable = false) private Long teamId; @Column(name = "user_id", nullable = false) private Long userId; @Enumerated(EnumType.STRING) @Column(name = "role_in_team", nullable = false, length = 50) private TeamRoleEnum roleInTeam; @Column(name = "join_time") private LocalDateTime joinTime; } ``` #### 步骤 3.3: 创建团队应用关联实体 **文件**: `src/main/java/com/qqchen/deploy/backend/deploy/entity/TeamApplication.java` ```java package com.qqchen.deploy.backend.deploy.entity; import com.qqchen.deploy.backend.deploy.enums.TeamResponsibilityEnum; import com.qqchen.deploy.backend.framework.domain.Entity; import jakarta.persistence.*; import lombok.Data; import lombok.EqualsAndHashCode; /** * 团队应用关联实体(支持分支级别管理) */ @Data @EqualsAndHashCode(callSuper = true) @jakarta.persistence.Entity @Table(name = "deploy_team_application") public class TeamApplication extends Entity { @Column(name = "team_id", nullable = false) private Long teamId; @Column(name = "application_id", nullable = false) private Long applicationId; @Column(name = "branch_pattern", length = 255) private String branchPattern; @Enumerated(EnumType.STRING) @Column(name = "responsibility", nullable = false, length = 50) private TeamResponsibilityEnum responsibility = TeamResponsibilityEnum.PRIMARY; } ``` #### 步骤 3.4: 创建应用环境配置实体 **文件**: `src/main/java/com/qqchen/deploy/backend/deploy/entity/ApplicationEnvironmentConfig.java` ```java package com.qqchen.deploy.backend.deploy.entity; import com.qqchen.deploy.backend.framework.domain.Entity; import com.vladmihalcea.hibernate.type.json.JsonType; import jakarta.persistence.*; import lombok.Data; import lombok.EqualsAndHashCode; import org.hibernate.annotations.Type; import java.util.Map; /** * 应用环境配置实体(支持团队差异化配置) */ @Data @EqualsAndHashCode(callSuper = true) @jakarta.persistence.Entity @Table(name = "deploy_application_environment_config") public class ApplicationEnvironmentConfig extends Entity { @Column(name = "application_id", nullable = false) private Long applicationId; @Column(name = "environment_id", nullable = false) private Long environmentId; @Column(name = "team_id") private Long teamId; @Type(JsonType.class) @Column(name = "config", columnDefinition = "json") private Map config; @Column(name = "enabled", nullable = false) private Boolean enabled = true; } ``` --- ### Phase 4: Repository 层 #### 步骤 4.1: 创建团队 Repository **文件**: `src/main/java/com/qqchen/deploy/backend/deploy/repository/ITeamRepository.java` ```java package com.qqchen.deploy.backend.deploy.repository; import com.qqchen.deploy.backend.deploy.entity.Team; import com.qqchen.deploy.backend.framework.repository.IBaseRepository; import org.springframework.stereotype.Repository; import java.util.Optional; @Repository public interface ITeamRepository extends IBaseRepository { /** * 根据团队编码查询(忽略已删除) */ Optional findByTeamCodeAndDeletedFalse(String teamCode); /** * 检查团队编码是否存在 */ boolean existsByTeamCodeAndDeletedFalse(String teamCode); } ``` #### 步骤 4.2: 创建团队成员 Repository **文件**: `src/main/java/com/qqchen/deploy/backend/deploy/repository/ITeamMemberRepository.java` ```java package com.qqchen.deploy.backend.deploy.repository; import com.qqchen.deploy.backend.deploy.entity.TeamMember; import com.qqchen.deploy.backend.framework.repository.IBaseRepository; import org.springframework.stereotype.Repository; import java.util.List; import java.util.Optional; @Repository public interface ITeamMemberRepository extends IBaseRepository { /** * 根据团队ID查询成员列表 */ List findByTeamIdAndDeletedFalse(Long teamId); /** * 根据用户ID查询所属团队 */ List findByUserIdAndDeletedFalse(Long userId); /** * 检查用户是否在团队中 */ boolean existsByTeamIdAndUserIdAndDeletedFalse(Long teamId, Long userId); /** * 查询用户在团队中的记录 */ Optional findByTeamIdAndUserIdAndDeletedFalse(Long teamId, Long userId); } ``` #### 步骤 4.3: 创建团队应用 Repository **文件**: `src/main/java/com/qqchen/deploy/backend/deploy/repository/ITeamApplicationRepository.java` ```java package com.qqchen.deploy.backend.deploy.repository; import com.qqchen.deploy.backend.deploy.entity.TeamApplication; import com.qqchen.deploy.backend.framework.repository.IBaseRepository; import org.springframework.stereotype.Repository; import java.util.List; @Repository public interface ITeamApplicationRepository extends IBaseRepository { /** * 根据团队ID查询关联的应用 */ List findByTeamIdAndDeletedFalse(Long teamId); /** * 根据应用ID查询关联的团队 */ List findByApplicationIdAndDeletedFalse(Long applicationId); /** * 检查团队和应用的关联是否存在 */ boolean existsByTeamIdAndApplicationIdAndDeletedFalse(Long teamId, Long applicationId); } ``` #### 步骤 4.4: 创建应用环境配置 Repository **文件**: `src/main/java/com/qqchen/deploy/backend/deploy/repository/IApplicationEnvironmentConfigRepository.java` ```java package com.qqchen.deploy.backend.deploy.repository; import com.qqchen.deploy.backend.deploy.entity.ApplicationEnvironmentConfig; import com.qqchen.deploy.backend.framework.repository.IBaseRepository; import org.springframework.stereotype.Repository; import java.util.List; import java.util.Optional; @Repository public interface IApplicationEnvironmentConfigRepository extends IBaseRepository { /** * 根据应用ID查询环境配置 */ List findByApplicationIdAndDeletedFalse(Long applicationId); /** * 根据应用ID和环境ID查询默认配置(团队ID为NULL) */ Optional findByApplicationIdAndEnvironmentIdAndTeamIdIsNullAndDeletedFalse( Long applicationId, Long environmentId); /** * 根据应用ID、环境ID和团队ID查询团队专属配置 */ Optional findByApplicationIdAndEnvironmentIdAndTeamIdAndDeletedFalse( Long applicationId, Long environmentId, Long teamId); /** * 检查配置是否存在 */ boolean existsByApplicationIdAndEnvironmentIdAndTeamIdAndDeletedFalse( Long applicationId, Long environmentId, Long teamId); } ``` --- ### Phase 5: DTO 和 Query #### 步骤 5.1: 创建团队 DTO **文件**: `src/main/java/com/qqchen/deploy/backend/deploy/dto/TeamDTO.java` ```java package com.qqchen.deploy.backend.deploy.dto; import com.qqchen.deploy.backend.deploy.enums.TeamTypeEnum; import com.qqchen.deploy.backend.framework.dto.BaseDTO; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import java.util.List; @Data @EqualsAndHashCode(callSuper = true) @Schema(description = "团队信息") public class TeamDTO extends BaseDTO { @Schema(description = "团队编码", example = "USER_SERVICE_TEAM") private String teamCode; @Schema(description = "团队名称", example = "用户服务团队") private String teamName; @Schema(description = "团队描述") private String teamDesc; @Schema(description = "所属产品线ID") private Long productLineId; @Schema(description = "负责人ID") private Long leaderId; @Schema(description = "是否启用") private Boolean enabled; @Schema(description = "排序号") private Integer sort; // 扩展字段(用于展示) @Schema(description = "产品线名称") private String productLineName; @Schema(description = "团队成员列表") private List members; @Schema(description = "关联应用列表") private List applicationIds; } ``` #### 步骤 5.2: 创建团队成员 DTO **文件**: `src/main/java/com/qqchen/deploy/backend/deploy/dto/TeamMemberDTO.java` ```java package com.qqchen.deploy.backend.deploy.dto; import com.qqchen.deploy.backend.deploy.enums.TeamRoleEnum; import com.qqchen.deploy.backend.framework.dto.BaseDTO; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; import java.time.LocalDateTime; @Data @EqualsAndHashCode(callSuper = true) @Schema(description = "团队成员信息") public class TeamMemberDTO extends BaseDTO { @Schema(description = "团队ID") private Long teamId; @Schema(description = "用户ID") private Long userId; @Schema(description = "团队角色") private TeamRoleEnum roleInTeam; @Schema(description = "加入时间") private LocalDateTime joinTime; // 扩展字段 @Schema(description = "用户名") private String username; @Schema(description = "真实姓名") private String realName; @Schema(description = "邮箱") private String email; } ``` #### 步骤 5.3: 创建查询对象 **文件**: `src/main/java/com/qqchen/deploy/backend/deploy/query/TeamQuery.java` ```java package com.qqchen.deploy.backend.deploy.query; import com.qqchen.deploy.backend.deploy.enums.TeamTypeEnum; import com.qqchen.deploy.backend.framework.query.BaseQuery; import com.qqchen.deploy.backend.framework.query.QueryField; import com.qqchen.deploy.backend.framework.query.QueryType; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; @Data @EqualsAndHashCode(callSuper = true) @Schema(description = "团队查询条件") public class TeamQuery extends BaseQuery { @QueryField(field = "teamName", type = QueryType.LIKE) @Schema(description = "团队名称(模糊查询)") private String teamName; @QueryField(field = "teamCode", type = QueryType.LIKE) @Schema(description = "团队编码(模糊查询)") private String teamCode; @QueryField(field = "productLineId") @Schema(description = "产品线ID") private Long productLineId; @QueryField(field = "leaderId") @Schema(description = "负责人ID") private Long leaderId; @QueryField(field = "enabled") @Schema(description = "是否启用") private Boolean enabled; } ``` --- ### Phase 6: Converter #### 步骤 6.1: 创建团队 Converter **文件**: `src/main/java/com/qqchen/deploy/backend/deploy/converter/TeamConverter.java` ```java package com.qqchen.deploy.backend.deploy.converter; import com.qqchen.deploy.backend.deploy.dto.TeamDTO; import com.qqchen.deploy.backend.deploy.entity.Team; import com.qqchen.deploy.backend.framework.converter.BaseConverter; import org.mapstruct.Mapper; @Mapper(config = BaseConverter.class) public interface TeamConverter extends BaseConverter { } ``` #### 步骤 6.2: 创建团队成员 Converter **文件**: `src/main/java/com/qqchen/deploy/backend/deploy/converter/TeamMemberConverter.java` ```java package com.qqchen.deploy.backend.deploy.converter; import com.qqchen.deploy.backend.deploy.dto.TeamMemberDTO; import com.qqchen.deploy.backend.deploy.entity.TeamMember; import com.qqchen.deploy.backend.framework.converter.BaseConverter; import org.mapstruct.Mapper; @Mapper(config = BaseConverter.class) public interface TeamMemberConverter extends BaseConverter { } ``` --- ### Phase 7: Service 层 #### 步骤 7.1: 创建团队 Service 接口 **文件**: `src/main/java/com/qqchen/deploy/backend/deploy/service/ITeamService.java` ```java package com.qqchen.deploy.backend.deploy.service; import com.qqchen.deploy.backend.deploy.dto.TeamDTO; import com.qqchen.deploy.backend.deploy.dto.TeamMemberDTO; import com.qqchen.deploy.backend.deploy.entity.Team; import com.qqchen.deploy.backend.deploy.query.TeamQuery; import com.qqchen.deploy.backend.framework.service.IBaseService; import java.util.List; public interface ITeamService extends IBaseService { /** * 添加团队成员 */ void addMember(Long teamId, Long userId, String roleInTeam); /** * 移除团队成员 */ void removeMember(Long teamId, Long userId); /** * 获取团队成员列表 */ List getTeamMembers(Long teamId); /** * 关联应用(支持分支模式) * @param teamId 团队ID * @param applicationId 应用ID * @param branchPattern 分支模式(如:feature/*、release/*、master) * @param responsibility 职责类型 */ void bindApplication(Long teamId, Long applicationId, String branchPattern, String responsibility); /** * 解绑应用 */ void unbindApplication(Long teamId, Long applicationId, String branchPattern); /** * 创建应用环境配置 * @param applicationId 应用ID * @param environmentId 环境ID * @param teamId 团队ID(可选,NULL表示默认配置) * @param config 配置内容 */ void createEnvironmentConfig(Long applicationId, Long environmentId, Long teamId, Map config); /** * 更新应用环境配置 */ void updateEnvironmentConfig(Long configId, Map config); /** * 删除应用环境配置 */ void deleteEnvironmentConfig(Long configId); } ``` #### 步骤 7.2: 创建团队 Service 实现 **文件**: `src/main/java/com/qqchen/deploy/backend/deploy/service/impl/TeamServiceImpl.java` ```java package com.qqchen.deploy.backend.deploy.service.impl; import com.qqchen.deploy.backend.deploy.converter.TeamConverter; import com.qqchen.deploy.backend.deploy.converter.TeamMemberConverter; import com.qqchen.deploy.backend.deploy.dto.TeamDTO; import com.qqchen.deploy.backend.deploy.dto.TeamMemberDTO; import com.qqchen.deploy.backend.deploy.entity.*; import com.qqchen.deploy.backend.deploy.enums.TeamResponsibilityEnum; import com.qqchen.deploy.backend.deploy.enums.TeamRoleEnum; import com.qqchen.deploy.backend.deploy.query.TeamQuery; import com.qqchen.deploy.backend.deploy.repository.*; import com.qqchen.deploy.backend.deploy.service.ITeamService; import com.qqchen.deploy.backend.framework.enums.ResponseCode; import com.qqchen.deploy.backend.framework.exception.BusinessException; import com.qqchen.deploy.backend.framework.service.impl.BaseServiceImpl; import com.qqchen.deploy.backend.system.repository.IUserRepository; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; import java.util.List; import java.util.stream.Collectors; @Slf4j @Service public class TeamServiceImpl extends BaseServiceImpl implements ITeamService { @Resource private ITeamRepository teamRepository; @Resource private ITeamMemberRepository teamMemberRepository; @Resource private ITeamApplicationRepository teamApplicationRepository; @Resource private IApplicationEnvironmentConfigRepository applicationEnvironmentConfigRepository; @Resource private IUserRepository userRepository; @Resource private TeamConverter teamConverter; @Resource private TeamMemberConverter teamMemberConverter; @Override @Transactional public TeamDTO create(TeamDTO dto) { // 检查团队编码唯一性 if (teamRepository.existsByTeamCodeAndDeletedFalse(dto.getTeamCode())) { throw new BusinessException(ResponseCode.SYSTEM_ERROR, new Object[]{"团队编码已存在: " + dto.getTeamCode()}); } return super.create(dto); } @Override @Transactional public void addMember(Long teamId, Long userId, String roleInTeam) { // 检查团队是否存在 teamRepository.findById(teamId) .orElseThrow(() -> new BusinessException(ResponseCode.SYSTEM_ERROR, new Object[]{"团队不存在"})); // 检查用户是否存在 userRepository.findById(userId) .orElseThrow(() -> new BusinessException(ResponseCode.SYSTEM_ERROR, new Object[]{"用户不存在"})); // 检查是否已经是成员 if (teamMemberRepository.existsByTeamIdAndUserIdAndDeletedFalse(teamId, userId)) { throw new BusinessException(ResponseCode.SYSTEM_ERROR, new Object[]{"用户已经是团队成员"}); } // 创建成员记录 TeamMember member = new TeamMember(); member.setTeamId(teamId); member.setUserId(userId); member.setRoleInTeam(TeamRoleEnum.valueOf(roleInTeam)); member.setJoinTime(LocalDateTime.now()); teamMemberRepository.save(member); log.info("添加团队成员: teamId={}, userId={}, role={}", teamId, userId, roleInTeam); } @Override @Transactional public void removeMember(Long teamId, Long userId) { TeamMember member = teamMemberRepository.findByTeamIdAndUserIdAndDeletedFalse(teamId, userId) .orElseThrow(() -> new BusinessException(ResponseCode.SYSTEM_ERROR, new Object[]{"用户不是团队成员"})); member.setDeleted(true); teamMemberRepository.save(member); log.info("移除团队成员: teamId={}, userId={}", teamId, userId); } @Override public List getTeamMembers(Long teamId) { List members = teamMemberRepository.findByTeamIdAndDeletedFalse(teamId); return members.stream() .map(teamMemberConverter::toDto) .collect(Collectors.toList()); } @Override @Transactional public void bindApplication(Long teamId, Long applicationId, String branchPattern, String responsibility) { // 检查团队-应用-分支的组合是否已存在 List existingBindings = teamApplicationRepository .findByTeamIdAndDeletedFalse(teamId).stream() .filter(ta -> ta.getApplicationId().equals(applicationId) && Objects.equals(ta.getBranchPattern(), branchPattern)) .collect(Collectors.toList()); if (!existingBindings.isEmpty()) { throw new BusinessException(ResponseCode.SYSTEM_ERROR, new Object[]{"团队已关联该应用的指定分支模式"}); } TeamApplication teamApp = new TeamApplication(); teamApp.setTeamId(teamId); teamApp.setApplicationId(applicationId); teamApp.setBranchPattern(branchPattern); teamApp.setResponsibility(TeamResponsibilityEnum.valueOf(responsibility)); teamApplicationRepository.save(teamApp); log.info("团队关联应用: teamId={}, applicationId={}, branchPattern={}, responsibility={}", teamId, applicationId, branchPattern, responsibility); } @Override @Transactional public void unbindApplication(Long teamId, Long applicationId, String branchPattern) { List teamApps = teamApplicationRepository .findByTeamIdAndDeletedFalse(teamId).stream() .filter(ta -> ta.getApplicationId().equals(applicationId) && Objects.equals(ta.getBranchPattern(), branchPattern)) .collect(Collectors.toList()); teamApps.forEach(ta -> { ta.setDeleted(true); teamApplicationRepository.save(ta); }); log.info("团队解绑应用: teamId={}, applicationId={}, branchPattern={}", teamId, applicationId, branchPattern); } @Override @Transactional public void createEnvironmentConfig(Long applicationId, Long environmentId, Long teamId, Map config) { // 检查配置是否已存在 if (applicationEnvironmentConfigRepository.existsByApplicationIdAndEnvironmentIdAndTeamIdAndDeletedFalse( applicationId, environmentId, teamId)) { throw new BusinessException(ResponseCode.SYSTEM_ERROR, new Object[]{"该应用环境配置已存在"}); } ApplicationEnvironmentConfig envConfig = new ApplicationEnvironmentConfig(); envConfig.setApplicationId(applicationId); envConfig.setEnvironmentId(environmentId); envConfig.setTeamId(teamId); envConfig.setConfig(config); envConfig.setEnabled(true); applicationEnvironmentConfigRepository.save(envConfig); log.info("创建环境配置: applicationId={}, environmentId={}, teamId={}", applicationId, environmentId, teamId); } @Override @Transactional public void updateEnvironmentConfig(Long configId, Map config) { ApplicationEnvironmentConfig envConfig = applicationEnvironmentConfigRepository.findById(configId) .orElseThrow(() -> new BusinessException(ResponseCode.SYSTEM_ERROR, new Object[]{"环境配置不存在"})); envConfig.setConfig(config); applicationEnvironmentConfigRepository.save(envConfig); log.info("更新环境配置: configId={}", configId); } @Override @Transactional public void deleteEnvironmentConfig(Long configId) { ApplicationEnvironmentConfig envConfig = applicationEnvironmentConfigRepository.findById(configId) .orElseThrow(() -> new BusinessException(ResponseCode.SYSTEM_ERROR, new Object[]{"环境配置不存在"})); envConfig.setDeleted(true); applicationEnvironmentConfigRepository.save(envConfig); log.info("删除环境配置: configId={}", configId); } } ``` --- ### Phase 8: Controller 层 #### 步骤 8.1: 创建团队管理 Controller **文件**: `src/main/java/com/qqchen/deploy/backend/deploy/api/TeamApiController.java` ```java package com.qqchen.deploy.backend.deploy.api; import com.qqchen.deploy.backend.deploy.dto.TeamDTO; import com.qqchen.deploy.backend.deploy.dto.TeamMemberDTO; import com.qqchen.deploy.backend.deploy.entity.Team; import com.qqchen.deploy.backend.deploy.query.TeamQuery; import com.qqchen.deploy.backend.deploy.service.ITeamService; import com.qqchen.deploy.backend.framework.api.Response; import com.qqchen.deploy.backend.framework.controller.BaseController; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.*; import java.util.List; @Slf4j @RestController @RequestMapping("/api/v1/team") @Tag(name = "团队管理", description = "技术团队管理相关接口") public class TeamApiController extends BaseController { @Resource private ITeamService teamService; @Operation(summary = "添加团队成员") @PostMapping("/{teamId}/member") public Response addMember( @Parameter(description = "团队ID") @PathVariable Long teamId, @Parameter(description = "用户ID") @RequestParam Long userId, @Parameter(description = "团队角色") @RequestParam String roleInTeam ) { teamService.addMember(teamId, userId, roleInTeam); return Response.success(); } @Operation(summary = "移除团队成员") @DeleteMapping("/{teamId}/member/{userId}") public Response removeMember( @Parameter(description = "团队ID") @PathVariable Long teamId, @Parameter(description = "用户ID") @PathVariable Long userId ) { teamService.removeMember(teamId, userId); return Response.success(); } @Operation(summary = "获取团队成员列表") @GetMapping("/{teamId}/members") public Response> getMembers( @Parameter(description = "团队ID") @PathVariable Long teamId ) { return Response.success(teamService.getTeamMembers(teamId)); } @Operation(summary = "关联应用(支持分支模式)") @PostMapping("/{teamId}/application") public Response bindApplication( @Parameter(description = "团队ID") @PathVariable Long teamId, @Parameter(description = "应用ID") @RequestParam Long applicationId, @Parameter(description = "分支模式(如:feature/*、release/*、master)") @RequestParam(required = false) String branchPattern, @Parameter(description = "职责类型") @RequestParam String responsibility ) { teamService.bindApplication(teamId, applicationId, branchPattern, responsibility); return Response.success(); } @Operation(summary = "解绑应用") @DeleteMapping("/{teamId}/application/{applicationId}") public Response unbindApplication( @Parameter(description = "团队ID") @PathVariable Long teamId, @Parameter(description = "应用ID") @PathVariable Long applicationId, @Parameter(description = "分支模式") @RequestParam(required = false) String branchPattern ) { teamService.unbindApplication(teamId, applicationId, branchPattern); return Response.success(); } @Operation(summary = "创建应用环境配置") @PostMapping("/environment-config") public Response createEnvironmentConfig( @Parameter(description = "应用ID") @RequestParam Long applicationId, @Parameter(description = "环境ID") @RequestParam Long environmentId, @Parameter(description = "团队ID(可选,NULL表示默认配置)") @RequestParam(required = false) Long teamId, @Parameter(description = "配置内容") @RequestBody Map config ) { teamService.createEnvironmentConfig(applicationId, environmentId, teamId, config); return Response.success(); } @Operation(summary = "更新应用环境配置") @PutMapping("/environment-config/{configId}") public Response updateEnvironmentConfig( @Parameter(description = "配置ID") @PathVariable Long configId, @Parameter(description = "配置内容") @RequestBody Map config ) { teamService.updateEnvironmentConfig(configId, config); return Response.success(); } @Operation(summary = "删除应用环境配置") @DeleteMapping("/environment-config/{configId}") public Response deleteEnvironmentConfig( @Parameter(description = "配置ID") @PathVariable Long configId ) { teamService.deleteEnvironmentConfig(configId); return Response.success(); } } ``` --- ### Phase 9: 国际化消息 **文件**: `src/main/resources/messages.properties` ```properties # 团队管理相关错误码 team.code.exists=团队编码{0}已存在 team.not.found=团队不存在 team.member.exists=用户已经是团队成员 team.member.not.found=用户不是团队成员 team.application.bound=团队已关联该应用 team.environment.granted=团队已拥有该环境访问权限 ``` --- ### Phase 10: 测试 #### 步骤 10.1: 单元测试 **文件**: `src/test/java/com/qqchen/deploy/backend/deploy/service/TeamServiceTest.java` ```java package com.qqchen.deploy.backend.deploy.service; import com.qqchen.deploy.backend.deploy.dto.TeamDTO; import com.qqchen.deploy.backend.deploy.enums.TeamTypeEnum; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.transaction.annotation.Transactional; import static org.junit.jupiter.api.Assertions.*; @SpringBootTest @Transactional class TeamServiceTest { @Autowired private ITeamService teamService; @Test void testCreateTeam() { TeamDTO dto = new TeamDTO(); dto.setTeamCode("TEST_TEAM"); dto.setTeamName("测试团队"); dto.setTeamType(TeamTypeEnum.FULLSTACK); dto.setBusinessDomain("测试域"); dto.setEnabled(true); TeamDTO created = teamService.create(dto); assertNotNull(created.getId()); assertEquals("TEST_TEAM", created.getTeamCode()); } @Test void testAddMember() { // 准备数据 TeamDTO team = createTestTeam(); Long userId = 1L; // 假设存在的用户ID // 添加成员 teamService.addMember(team.getId(), userId, "DEVELOPER"); // 验证 var members = teamService.getTeamMembers(team.getId()); assertEquals(1, members.size()); } private TeamDTO createTestTeam() { TeamDTO dto = new TeamDTO(); dto.setTeamCode("TEST_" + System.currentTimeMillis()); dto.setTeamName("测试团队"); dto.setTeamType(TeamTypeEnum.BACKEND); dto.setEnabled(true); return teamService.create(dto); } } ``` --- ## 🚀 执行步骤 ### 1. 运行数据库迁移 ```bash # 启动应用,Liquibase 会自动执行迁移脚本 mvn spring-boot:run ``` ### 2. 编译代码 ```bash mvn clean compile ``` ### 3. 运行测试 ```bash mvn test ``` ### 4. 验证 API 访问 Swagger UI: `http://localhost:8080/swagger-ui.html` --- ## ✅ 检查清单 - [ ] 数据库迁移脚本已创建并注册 - [ ] 枚举类创建完成 - [ ] 实体类创建完成 - [ ] Repository 创建完成 - [ ] DTO 和 Query 创建完成 - [ ] Converter 创建完成 - [ ] Service 接口和实现创建完成 - [ ] Controller 创建完成 - [ ] 国际化消息配置完成 - [ ] 单元测试编写完成 - [ ] 代码编译通过 - [ ] API 测试通过 --- ## 📌 注意事项 1. **外键约束**: 确保 `sys_user` 和 `sys_department` 表存在 2. **枚举值**: 前端需要与后端枚举值保持一致 3. **软删除**: 所有删除操作使用软删除(`deleted=1`) 4. **事务管理**: 涉及多表操作的方法需要加 `@Transactional` 5. **日志记录**: 关键操作需要记录详细日志 --- ## 🎯 后续扩展 1. **分支匹配算法优化**: 支持更复杂的分支模式匹配(如正则表达式、优先级规则) 2. **环境配置模板**: 为不同环境类型提供配置模板,简化配置创建 3. **配置继承机制**: 支持团队配置继承默认配置,只需覆盖差异部分 4. **部署权限控制**: 基于团队-应用-分支-环境的细粒度权限管理 5. **团队工作负载统计**: 统计团队负责的应用数量、部署频率等指标 6. **分支负责人自动通知**: 当某分支有部署需求时,自动通知对应团队 7. **产品线仪表盘**: 展示产品线下所有团队的工作概况 --- ## 📖 使用场景示例 ### 场景1:多团队协作开发同一应用 **背景**: `user-service` 应用由多个团队协作开发 - **用户团队**: 负责 `feature/user-*` 分支 - **支付团队**: 负责 `feature/payment-*` 分支 - **核心团队**: 负责 `master` 和 `release/*` 分支 **配置**: ```sql -- 用户团队关联 INSERT INTO deploy_team_application (team_id, application_id, branch_pattern, responsibility) VALUES (1, 100, 'feature/user-*', 'PRIMARY'); -- 支付团队关联 INSERT INTO deploy_team_application (team_id, application_id, branch_pattern, responsibility) VALUES (2, 100, 'feature/payment-*', 'PRIMARY'); -- 核心团队关联 INSERT INTO deploy_team_application (team_id, application_id, branch_pattern, responsibility) VALUES (3, 100, 'master', 'PRIMARY'); INSERT INTO deploy_team_application (team_id, application_id, branch_pattern, responsibility) VALUES (3, 100, 'release/*', 'PRIMARY'); ``` ### 场景2:团队在不同环境使用不同配置 **背景**: `order-service` 在测试环境和生产环境,不同团队有不同的资源配置需求 **配置**: ```sql -- 默认测试环境配置(所有团队共享) INSERT INTO deploy_application_environment_config (application_id, environment_id, team_id, config) VALUES (200, 1, NULL, '{"replicas": 2, "memory": "512Mi"}'); -- 团队A在测试环境的专属配置(需要更多资源) INSERT INTO deploy_application_environment_config (application_id, environment_id, team_id, config) VALUES (200, 1, 10, '{"replicas": 3, "memory": "1Gi", "debug": true}'); -- 生产环境默认配置 INSERT INTO deploy_application_environment_config (application_id, environment_id, team_id, config) VALUES (200, 2, NULL, '{"replicas": 5, "memory": "2Gi", "cpu": "1000m"}'); ``` **查询逻辑**: ```java // 获取团队专属配置,如果不存在则使用默认配置 Optional teamConfig = configRepository.findByApplicationIdAndEnvironmentIdAndTeamIdAndDeletedFalse(appId, envId, teamId); if (teamConfig.isEmpty()) { teamConfig = configRepository.findByApplicationIdAndEnvironmentIdAndTeamIdIsNullAndDeletedFalse(appId, envId); } ```