deploy-ease-platform/backend/docs/团队管理功能开发指南.md
2025-10-25 17:49:22 +08:00

46 KiB
Raw Blame History

团队管理功能开发指南

📋 目标

实现技术团队管理功能,支持:

  • 产品线和团队的层级管理
  • 团队成员管理(支持跨部门)
  • 团队与应用的分支级别关联(一个团队负责多个应用的特定分支)
  • 团队-应用-环境的差异化配置

🎯 核心设计

关系层级

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

-- =====================================================================
-- 团队管理功能表结构 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

  - 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

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

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

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

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<Long> {

    @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

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<Long> {

    @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

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<Long> {

    @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

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<Long> {

    @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<String, Object> 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

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<Team, Long> {
    
    /**
     * 根据团队编码查询(忽略已删除)
     */
    Optional<Team> findByTeamCodeAndDeletedFalse(String teamCode);
    
    /**
     * 检查团队编码是否存在
     */
    boolean existsByTeamCodeAndDeletedFalse(String teamCode);
}

步骤 4.2: 创建团队成员 Repository

文件: src/main/java/com/qqchen/deploy/backend/deploy/repository/ITeamMemberRepository.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<TeamMember, Long> {
    
    /**
     * 根据团队ID查询成员列表
     */
    List<TeamMember> findByTeamIdAndDeletedFalse(Long teamId);
    
    /**
     * 根据用户ID查询所属团队
     */
    List<TeamMember> findByUserIdAndDeletedFalse(Long userId);
    
    /**
     * 检查用户是否在团队中
     */
    boolean existsByTeamIdAndUserIdAndDeletedFalse(Long teamId, Long userId);
    
    /**
     * 查询用户在团队中的记录
     */
    Optional<TeamMember> findByTeamIdAndUserIdAndDeletedFalse(Long teamId, Long userId);
}

步骤 4.3: 创建团队应用 Repository

文件: src/main/java/com/qqchen/deploy/backend/deploy/repository/ITeamApplicationRepository.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<TeamApplication, Long> {
    
    /**
     * 根据团队ID查询关联的应用
     */
    List<TeamApplication> findByTeamIdAndDeletedFalse(Long teamId);
    
    /**
     * 根据应用ID查询关联的团队
     */
    List<TeamApplication> findByApplicationIdAndDeletedFalse(Long applicationId);
    
    /**
     * 检查团队和应用的关联是否存在
     */
    boolean existsByTeamIdAndApplicationIdAndDeletedFalse(Long teamId, Long applicationId);
}

步骤 4.4: 创建应用环境配置 Repository

文件: src/main/java/com/qqchen/deploy/backend/deploy/repository/IApplicationEnvironmentConfigRepository.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<ApplicationEnvironmentConfig, Long> {
    
    /**
     * 根据应用ID查询环境配置
     */
    List<ApplicationEnvironmentConfig> findByApplicationIdAndDeletedFalse(Long applicationId);
    
    /**
     * 根据应用ID和环境ID查询默认配置团队ID为NULL
     */
    Optional<ApplicationEnvironmentConfig> findByApplicationIdAndEnvironmentIdAndTeamIdIsNullAndDeletedFalse(
        Long applicationId, Long environmentId);
    
    /**
     * 根据应用ID、环境ID和团队ID查询团队专属配置
     */
    Optional<ApplicationEnvironmentConfig> 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

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<TeamMemberDTO> members;

    @Schema(description = "关联应用列表")
    private List<Long> applicationIds;
}

步骤 5.2: 创建团队成员 DTO

文件: src/main/java/com/qqchen/deploy/backend/deploy/dto/TeamMemberDTO.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

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

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<Team, TeamDTO> {
}

步骤 6.2: 创建团队成员 Converter

文件: src/main/java/com/qqchen/deploy/backend/deploy/converter/TeamMemberConverter.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<TeamMember, TeamMemberDTO> {
}

Phase 7: Service 层

步骤 7.1: 创建团队 Service 接口

文件: src/main/java/com/qqchen/deploy/backend/deploy/service/ITeamService.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<Team, TeamDTO, TeamQuery, Long> {

    /**
     * 添加团队成员
     */
    void addMember(Long teamId, Long userId, String roleInTeam);

    /**
     * 移除团队成员
     */
    void removeMember(Long teamId, Long userId);

    /**
     * 获取团队成员列表
     */
    List<TeamMemberDTO> 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<String, Object> config);

    /**
     * 更新应用环境配置
     */
    void updateEnvironmentConfig(Long configId, Map<String, Object> config);

    /**
     * 删除应用环境配置
     */
    void deleteEnvironmentConfig(Long configId);
}

步骤 7.2: 创建团队 Service 实现

文件: src/main/java/com/qqchen/deploy/backend/deploy/service/impl/TeamServiceImpl.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<Team, TeamDTO, TeamQuery, Long> 
    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<TeamMemberDTO> getTeamMembers(Long teamId) {
        List<TeamMember> 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<TeamApplication> 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<TeamApplication> 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<String, Object> 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<String, Object> 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

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<Team, TeamDTO, Long, TeamQuery> {

    @Resource
    private ITeamService teamService;

    @Operation(summary = "添加团队成员")
    @PostMapping("/{teamId}/member")
    public Response<Void> 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<Void> 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<List<TeamMemberDTO>> getMembers(
        @Parameter(description = "团队ID") @PathVariable Long teamId
    ) {
        return Response.success(teamService.getTeamMembers(teamId));
    }

    @Operation(summary = "关联应用(支持分支模式)")
    @PostMapping("/{teamId}/application")
    public Response<Void> 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<Void> 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<Void> 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<String, Object> config
    ) {
        teamService.createEnvironmentConfig(applicationId, environmentId, teamId, config);
        return Response.success();
    }

    @Operation(summary = "更新应用环境配置")
    @PutMapping("/environment-config/{configId}")
    public Response<Void> updateEnvironmentConfig(
        @Parameter(description = "配置ID") @PathVariable Long configId,
        @Parameter(description = "配置内容") @RequestBody Map<String, Object> config
    ) {
        teamService.updateEnvironmentConfig(configId, config);
        return Response.success();
    }

    @Operation(summary = "删除应用环境配置")
    @DeleteMapping("/environment-config/{configId}")
    public Response<Void> deleteEnvironmentConfig(
        @Parameter(description = "配置ID") @PathVariable Long configId
    ) {
        teamService.deleteEnvironmentConfig(configId);
        return Response.success();
    }
}

Phase 9: 国际化消息

文件: src/main/resources/messages.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

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. 运行数据库迁移

# 启动应用Liquibase 会自动执行迁移脚本
mvn spring-boot:run

2. 编译代码

mvn clean compile

3. 运行测试

mvn test

4. 验证 API

访问 Swagger UI: http://localhost:8080/swagger-ui.html


检查清单

  • 数据库迁移脚本已创建并注册
  • 枚举类创建完成
  • 实体类创建完成
  • Repository 创建完成
  • DTO 和 Query 创建完成
  • Converter 创建完成
  • Service 接口和实现创建完成
  • Controller 创建完成
  • 国际化消息配置完成
  • 单元测试编写完成
  • 代码编译通过
  • API 测试通过

📌 注意事项

  1. 外键约束: 确保 sys_usersys_department 表存在
  2. 枚举值: 前端需要与后端枚举值保持一致
  3. 软删除: 所有删除操作使用软删除(deleted=1
  4. 事务管理: 涉及多表操作的方法需要加 @Transactional
  5. 日志记录: 关键操作需要记录详细日志

🎯 后续扩展

  1. 分支匹配算法优化: 支持更复杂的分支模式匹配(如正则表达式、优先级规则)
  2. 环境配置模板: 为不同环境类型提供配置模板,简化配置创建
  3. 配置继承机制: 支持团队配置继承默认配置,只需覆盖差异部分
  4. 部署权限控制: 基于团队-应用-分支-环境的细粒度权限管理
  5. 团队工作负载统计: 统计团队负责的应用数量、部署频率等指标
  6. 分支负责人自动通知: 当某分支有部署需求时,自动通知对应团队
  7. 产品线仪表盘: 展示产品线下所有团队的工作概况

📖 使用场景示例

场景1多团队协作开发同一应用

背景: user-service 应用由多个团队协作开发

  • 用户团队: 负责 feature/user-* 分支
  • 支付团队: 负责 feature/payment-* 分支
  • 核心团队: 负责 masterrelease/* 分支

配置:

-- 用户团队关联
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 在测试环境和生产环境,不同团队有不同的资源配置需求

配置:

-- 默认测试环境配置(所有团队共享)
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"}');

查询逻辑:

// 获取团队专属配置,如果不存在则使用默认配置
Optional<ApplicationEnvironmentConfig> teamConfig = 
    configRepository.findByApplicationIdAndEnvironmentIdAndTeamIdAndDeletedFalse(appId, envId, teamId);

if (teamConfig.isEmpty()) {
    teamConfig = configRepository.findByApplicationIdAndEnvironmentIdAndTeamIdIsNullAndDeletedFalse(appId, envId);
}