diff --git a/backend/frontend.rules b/backend/frontend.rules new file mode 100644 index 00000000..0b5435da --- /dev/null +++ b/backend/frontend.rules @@ -0,0 +1,291 @@ +# 前端接口对接文档 + +## 通用说明 + +### 1. 接口响应格式 + +所有接口统一返回以下格式: +```typescript +interface Response { + success: boolean; // 请求是否成功 + code: number; // 状态码,200表示成功,其他表示失败 + message: string; // 提示信息 + data?: T; // 响应数据,可选 +} +``` + +### 2. 分页请求参数 +```typescript +interface PageQuery { + pageNum: number; // 页码,从1开始 + pageSize: number; // 每页大小 + sortField?: string; // 排序字段,可选 + sortOrder?: 'asc' | 'desc'; // 排序方式,可选 +} +``` + +### 3. 分页响应格式 +```typescript +interface PageResponse { + content: T[]; // 数据列表 + totalElements: number;// 总记录数 + totalPages: number; // 总页数 + size: number; // 每页大小 + number: number; // 当前页码(从0开始) + first: boolean; // 是否第一页 + last: boolean; // 是否最后一页 + empty: boolean; // 是否为空 +} +``` + +## 通用接口 + +### 1. 基础CRUD接口 +所有实体都支持以下基础操作接口: + +#### 1.1 分页查询 +```typescript +GET /api/v1/{module}/page + +请求参数:PageQuery & { + // 其他查询条件,根据具体模块定义 +} + +响应结果:Response> +``` + +#### 1.2 列表查询 +```typescript +GET /api/v1/{module}/list + +请求参数:{ + // 查询条件,根据具体模块定义 +} + +响应结果:Response +``` + +#### 1.3 获取详情 +```typescript +GET /api/v1/{module}/{id} + +响应结果:Response +``` + +#### 1.4 创建 +```typescript +POST /api/v1/{module} + +请求参数:{ + // 创建参数,根据具体模块定义 +} + +响应结果:Response +``` + +#### 1.5 更新 +```typescript +PUT /api/v1/{module}/{id} + +请求参数:{ + // 更新参数,根据具体模块定义 +} + +响应结果:Response +``` + +#### 1.6 删除 +```typescript +DELETE /api/v1/{module}/{id} + +响应结果:Response +``` + +#### 1.7 批量删除 +```typescript +DELETE /api/v1/{module}/batch + +请求参数:{ + ids: number[]; // ID列表 +} + +响应结果:Response +``` + +#### 1.8 导出数据 +```typescript +GET /api/v1/{module}/export + +请求参数:{ + // 查询条件,根据具体模块定义 +} + +响应结果:二进制文件流 +``` + +### 2. 树形结构接口 +对于树形结构的数据(如部门、菜单等),还支持以下接口: + +#### 2.1 获取树形数据 +```typescript +GET /api/v1/{module}/tree + +响应结果:Response + +interface TreeNode { + id: number; + parentId: number | null; + children: TreeNode[]; + // 其他字段根据具体模块定义 +} +``` + +### 3. 状态管理接口 +对于需要状态管理的数据(如租户、用户等),还支持以下接口: + +#### 3.1 获取状态 +```typescript +GET /api/v1/{module}/{id}/enabled + +响应结果:Response +``` + +#### 3.2 更新状态 +```typescript +PUT /api/v1/{module}/{id}/enabled + +请求参数: +enabled=true // 是否启用 + +响应结果:Response +``` + +## 错误处理 + +### 1. 错误码说明 +- 200:成功 +- 400:请求参数错误 +- 401:未认证 +- 403:无权限 +- 404:资源不存在 +- 500:服务器内部错误 + +### 2. 错误响应示例 +```json +{ + "success": false, + "code": 400, + "message": "请求参数错误", + "data": { + "field": "name", + "message": "名称不能为空" + } +} +``` + +## 接口调用示例 + +### TypeScript 示例 +```typescript +// 定义接口返回类型 +interface User { + id: number; + username: string; + enabled: boolean; +} + +// 分页查询 +async function getUserList(query: PageQuery & { username?: string }) { + const response = await axios.get>>('/api/v1/user/page', { + params: query + }); + return response.data; +} + +// 创建用户 +async function createUser(user: Omit) { + const response = await axios.post>('/api/v1/user', user); + return response.data; +} + +// 更新状态 +async function updateUserStatus(id: number, enabled: boolean) { + const response = await axios.put>(`/api/v1/user/${id}/enabled`, null, { + params: { enabled } + }); + return response.data; +} +``` + +### Vue3 + TypeScript 示例 +```typescript +// 在组件中使用 +import { ref, onMounted } from 'vue'; + +export default defineComponent({ + setup() { + const userList = ref([]); + const total = ref(0); + const loading = ref(false); + + const queryList = async (query: PageQuery) => { + try { + loading.value = true; + const response = await getUserList(query); + if (response.success) { + userList.value = response.data.content; + total.value = response.data.totalElements; + } + } finally { + loading.value = false; + } + }; + + onMounted(() => { + queryList({ + pageNum: 1, + pageSize: 10 + }); + }); + + return { + userList, + total, + loading, + queryList + }; + } +}); +``` + +## 注意事项 + +1. 请求头要求 +```typescript +{ + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ${token}' // JWT认证token +} +``` + +2. 日期时间格式 +- 请求参数:使用ISO 8601格式(YYYY-MM-DDTHH:mm:ss.sssZ) +- 响应数据:统一返回ISO 8601格式 + +3. 文件上传 +- 使用multipart/form-data格式 +- 文件大小限制:10MB + +4. 接口版本 +- 所有接口统一使用v1版本 +- URL格式:/api/v1/{module}/{resource} + +5. 安全性 +- 所有接口都需要JWT认证 +- Token过期时间:2小时 +- 需要定期刷新Token + +6. 错误处理 +- 统一使用axios拦截器处理错误 +- 401错误跳转到登录页 +- 其他错误统一提示 \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/converter/RepositoryConverter.java b/backend/src/main/java/com/qqchen/deploy/backend/converter/RepositoryConverter.java new file mode 100644 index 00000000..2ba989ef --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/converter/RepositoryConverter.java @@ -0,0 +1,51 @@ +package com.qqchen.deploy.backend.converter; + +import com.qqchen.deploy.backend.entity.RepositoryGroup; +import com.qqchen.deploy.backend.entity.RepositoryProject; +import com.qqchen.deploy.backend.entity.RepositoryBranch; +import com.qqchen.deploy.backend.model.dto.RepositoryGroupDTO; +import com.qqchen.deploy.backend.model.dto.RepositoryProjectDTO; +import com.qqchen.deploy.backend.model.dto.RepositoryBranchDTO; +import com.qqchen.deploy.backend.framework.converter.BaseConverter; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +/** + * 仓库相关的对象转换器 + * + * @author QQchen + * @version 1.0 + */ +@Mapper(config = BaseConverter.class) +public interface RepositoryConverter { + + /** + * 仓库组实体转DTO + */ + RepositoryGroupDTO toDto(RepositoryGroup entity); + + /** + * 仓库组DTO转实体 + */ + RepositoryGroup toEntity(RepositoryGroupDTO dto); + + /** + * 仓库项目实体转DTO + */ + RepositoryProjectDTO toDto(RepositoryProject entity); + + /** + * 仓库项目DTO转实体 + */ + RepositoryProject toEntity(RepositoryProjectDTO dto); + + /** + * 仓库分支实体转DTO + */ + RepositoryBranchDTO toDto(RepositoryBranch entity); + + /** + * 仓库分支DTO转实体 + */ + RepositoryBranch toEntity(RepositoryBranchDTO dto); +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/entity/RepositoryBranch.java b/backend/src/main/java/com/qqchen/deploy/backend/entity/RepositoryBranch.java index 0f71352b..98d7ff48 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/entity/RepositoryBranch.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/entity/RepositoryBranch.java @@ -16,8 +16,8 @@ import java.time.LocalDateTime; @LogicDelete public class RepositoryBranch extends Entity { - @Column(name = "repository_id", nullable = false) - private Long repositoryId; + @Column(name = "external_system_id") + private Long externalSystemId; @Column(name = "project_id", nullable = false) private Long projectId; @@ -37,7 +37,8 @@ public class RepositoryBranch extends Entity { @Column(name = "commit_date") private LocalDateTime commitDate; - private Boolean protected_ = false; + @Column(name = "is_protected") + private Boolean isProtected = false; @Column(name = "developers_can_push") private Boolean developersCanPush = true; @@ -48,8 +49,8 @@ public class RepositoryBranch extends Entity { @Column(name = "can_push") private Boolean canPush = true; - @Column(name = "default_branch") - private Boolean defaultBranch = false; + @Column(name = "is_default_branch") + private Boolean isDefaultBranch = false; @Column(name = "web_url") private String webUrl; diff --git a/backend/src/main/java/com/qqchen/deploy/backend/entity/RepositoryConfig.java b/backend/src/main/java/com/qqchen/deploy/backend/entity/RepositoryConfig.java deleted file mode 100644 index a0b481f7..00000000 --- a/backend/src/main/java/com/qqchen/deploy/backend/entity/RepositoryConfig.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.qqchen.deploy.backend.entity; - -import com.qqchen.deploy.backend.framework.annotation.LogicDelete; -import com.qqchen.deploy.backend.framework.domain.Entity; -import jakarta.persistence.Column; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.Table; -import lombok.Data; -import lombok.EqualsAndHashCode; - -import java.time.LocalDateTime; - - -@Data -@EqualsAndHashCode(callSuper = true) -@jakarta.persistence.Entity -@Table(name = "deploy_repository_config") -@LogicDelete -public class RepositoryConfig extends Entity { - - @Column(nullable = false) - private String name; - - @Column(nullable = false) - private String url; - - @Column(name = "access_token", nullable = false) - private String accessToken; - - @Column(name = "api_version") - private String apiVersion; - - private Integer sort; - - private String remark; - - @Column(name = "last_sync_time") - private LocalDateTime lastSyncTime; - - @Column(name = "last_sync_status") - @Enumerated(EnumType.STRING) - private RepositorySyncHistory.SyncStatus lastSyncStatus; -} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/entity/RepositoryGroup.java b/backend/src/main/java/com/qqchen/deploy/backend/entity/RepositoryGroup.java index 66f227a9..4044cad3 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/entity/RepositoryGroup.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/entity/RepositoryGroup.java @@ -7,16 +7,21 @@ import jakarta.persistence.Table; import lombok.Data; import lombok.EqualsAndHashCode; - +/** + * 仓库组实体 + * + * @author QQchen + * @version 1.0 + */ @Data @EqualsAndHashCode(callSuper = true) @jakarta.persistence.Entity -@Table(name = "deploy_repository_group") +@Table(name = "deploy_repo_group") @LogicDelete public class RepositoryGroup extends Entity { - @Column(name = "repository_id", nullable = false) - private Long repositoryId; + @Column(name = "external_system_id") + private Long externalSystemId; @Column(name = "group_id", nullable = false) private Long groupId; diff --git a/backend/src/main/java/com/qqchen/deploy/backend/entity/RepositoryProject.java b/backend/src/main/java/com/qqchen/deploy/backend/entity/RepositoryProject.java index 5d73055a..64e5a3f5 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/entity/RepositoryProject.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/entity/RepositoryProject.java @@ -16,8 +16,8 @@ import java.time.LocalDateTime; @LogicDelete public class RepositoryProject extends Entity { - @Column(name = "repository_id", nullable = false) - private Long repositoryId; + @Column(name = "external_system_id") + private Long externalSystemId; @Column(name = "project_id", nullable = false) private Long projectId; @@ -35,8 +35,8 @@ public class RepositoryProject extends Entity { @Column(name = "group_id") private Long groupId; - @Column(name = "default_branch") - private String defaultBranch; + @Column(name = "is_default_branch") + private String isDefaultBranch; @Column(name = "web_url") private String webUrl; diff --git a/backend/src/main/java/com/qqchen/deploy/backend/framework/enums/ResponseCode.java b/backend/src/main/java/com/qqchen/deploy/backend/framework/enums/ResponseCode.java index 992a8a37..8636f068 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/framework/enums/ResponseCode.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/framework/enums/ResponseCode.java @@ -80,7 +80,20 @@ public enum ResponseCode { /** * Git系统Token必填 */ - EXTERNAL_SYSTEM_GIT_TOKEN_REQUIRED(2502, "Git系统必须提供Token"); + EXTERNAL_SYSTEM_GIT_TOKEN_REQUIRED(2502, "Git系统必须提供Token"), + + // 仓库相关错误码 (2600-2699) + REPOSITORY_GROUP_NOT_FOUND(2600, "repository.group.not.found"), + REPOSITORY_GROUP_NAME_EXISTS(2601, "repository.group.name.exists"), + REPOSITORY_GROUP_PATH_EXISTS(2602, "repository.group.path.exists"), + REPOSITORY_PROJECT_NOT_FOUND(2610, "repository.project.not.found"), + REPOSITORY_PROJECT_NAME_EXISTS(2611, "repository.project.name.exists"), + REPOSITORY_PROJECT_PATH_EXISTS(2612, "repository.project.path.exists"), + REPOSITORY_BRANCH_NOT_FOUND(2620, "repository.branch.not.found"), + REPOSITORY_BRANCH_NAME_EXISTS(2621, "repository.branch.name.exists"), + REPOSITORY_SYNC_IN_PROGRESS(2630, "repository.sync.in.progress"), + REPOSITORY_SYNC_FAILED(2631, "repository.sync.failed"), + REPOSITORY_SYNC_HISTORY_NOT_FOUND(2632, "repository.sync.history.not.found"); private final int code; private final String messageKey; // 国际化消息key diff --git a/backend/src/main/java/com/qqchen/deploy/backend/model/dto/RepositoryBranchDTO.java b/backend/src/main/java/com/qqchen/deploy/backend/model/dto/RepositoryBranchDTO.java new file mode 100644 index 00000000..3ed16ab5 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/model/dto/RepositoryBranchDTO.java @@ -0,0 +1,64 @@ +package com.qqchen.deploy.backend.model.dto; + +import com.qqchen.deploy.backend.framework.dto.BaseDTO; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.LocalDateTime; + +/** + * 仓库分支DTO + * + * @author QQchen + * @version 1.0 + */ +@Data +@Schema(description = "仓库分支DTO") +@EqualsAndHashCode(callSuper = true) +public class RepositoryBranchDTO extends BaseDTO { + + @Schema(description = "外部系统ID") + @NotNull(message = "外部系统ID不能为空") + private Long externalSystemId; + + @Schema(description = "所属项目ID") + @NotNull(message = "项目ID不能为空") + private Long projectId; + + @Schema(description = "分支名称") + @NotBlank(message = "分支名称不能为空") + private String name; + + @Schema(description = "是否为默认分支") + private Boolean isDefaultBranch = false; + + @Schema(description = "是否受保护") + private Boolean isProtected = false; + + @Schema(description = "是否可推送") + private Boolean canPush = true; + + @Schema(description = "开发者是否可推送") + private Boolean developersCanPush = true; + + @Schema(description = "开发者是否可合并") + private Boolean developersCanMerge = true; + + @Schema(description = "最新提交ID") + private String commitId; + + @Schema(description = "最新提交信息") + private String commitMessage; + + @Schema(description = "最新提交作者") + private String commitAuthor; + + @Schema(description = "最新提交时间") + private LocalDateTime commitDate; + + @Schema(description = "网页URL") + private String webUrl; +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/model/dto/RepositoryGroupDTO.java b/backend/src/main/java/com/qqchen/deploy/backend/model/dto/RepositoryGroupDTO.java new file mode 100644 index 00000000..807fa8ee --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/model/dto/RepositoryGroupDTO.java @@ -0,0 +1,57 @@ +package com.qqchen.deploy.backend.model.dto; + +import com.qqchen.deploy.backend.framework.dto.BaseDTO; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 仓库组DTO + * + * @author QQchen + * @version 1.0 + */ +@Data +@Schema(description = "仓库组DTO") +@EqualsAndHashCode(callSuper = true) +public class RepositoryGroupDTO extends BaseDTO { + + @Schema(description = "外部系统ID") + @NotNull(message = "外部系统ID不能为空") + private Long externalSystemId; + + @Schema(description = "外部系统中的组ID") + @NotNull(message = "组ID不能为空") + private Long groupId; + + @Schema(description = "仓库组名称") + @NotBlank(message = "仓库组名称不能为空") + private String name; + + @Schema(description = "仓库组路径") + @NotBlank(message = "仓库组路径不能为空") + private String path; + + @Schema(description = "仓库组描述") + private String description; + + @Schema(description = "可见性:private-私有,internal-内部,public-公开") + private String visibility; + + @Schema(description = "父级仓库组ID") + private Long parentId; + + @Schema(description = "网页URL") + private String webUrl; + + @Schema(description = "头像URL") + private String avatarUrl; + + @Schema(description = "是否启用") + private Boolean enabled = true; + + @Schema(description = "排序号") + private Integer sort = 0; +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/model/dto/RepositoryProjectDTO.java b/backend/src/main/java/com/qqchen/deploy/backend/model/dto/RepositoryProjectDTO.java new file mode 100644 index 00000000..62539956 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/model/dto/RepositoryProjectDTO.java @@ -0,0 +1,68 @@ +package com.qqchen.deploy.backend.model.dto; + +import com.qqchen.deploy.backend.framework.dto.BaseDTO; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.LocalDateTime; + +/** + * 仓库项目DTO + * + * @author QQchen + * @version 1.0 + */ +@Data +@Schema(description = "仓库项目DTO") +@EqualsAndHashCode(callSuper = true) +public class RepositoryProjectDTO extends BaseDTO { + + @Schema(description = "外部系统ID") + @NotNull(message = "外部系统ID不能为空") + private Long externalSystemId; + + @Schema(description = "外部系统中的项目ID") + @NotNull(message = "项目ID不能为空") + private Long projectId; + + @Schema(description = "项目名称") + @NotBlank(message = "项目名称不能为空") + private String name; + + @Schema(description = "项目路径") + @NotBlank(message = "项目路径不能为空") + private String path; + + @Schema(description = "项目描述") + private String description; + + @Schema(description = "可见性:private-私有,internal-内部,public-公开") + private String visibility; + + @Schema(description = "所属仓库组ID") + private Long groupId; + + @Schema(description = "默认分支") + private String defaultBranch; + + @Schema(description = "网页URL") + private String webUrl; + + @Schema(description = "SSH克隆地址") + private String sshUrl; + + @Schema(description = "HTTP克隆地址") + private String httpUrl; + + @Schema(description = "最后活动时间") + private LocalDateTime lastActivityAt; + + @Schema(description = "是否启用") + private Boolean enabled = true; + + @Schema(description = "排序号") + private Integer sort = 0; +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/model/dto/RepositorySyncStatusDTO.java b/backend/src/main/java/com/qqchen/deploy/backend/model/dto/RepositorySyncStatusDTO.java new file mode 100644 index 00000000..1d37f5ac --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/model/dto/RepositorySyncStatusDTO.java @@ -0,0 +1,54 @@ +package com.qqchen.deploy.backend.model.dto; + +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; + +/** + * 仓库同步状态DTO + * 用于查询单个同步任务的详细状态 + * + * @author QQchen + * @version 1.0 + */ +@Data +@Schema(description = "仓库同步状态DTO") +@EqualsAndHashCode(callSuper = true) +public class RepositorySyncStatusDTO extends BaseDTO { + + @Schema(description = "外部系统ID") + private Long externalSystemId; + + @Schema(description = "外部系统名称") + private String externalSystemName; + + @Schema(description = "同步开始时间") + private LocalDateTime startTime; + + @Schema(description = "同步结束时间") + private LocalDateTime endTime; + + @Schema(description = "同步状态:PENDING-等待中,RUNNING-运行中,SUCCESS-成功,FAILED-失败") + private String status; + + @Schema(description = "错误信息") + private String errorMessage; + + @Schema(description = "同步的组数量") + private Integer groupCount; + + @Schema(description = "同步的项目数量") + private Integer projectCount; + + @Schema(description = "同步的分支数量") + private Integer branchCount; + + @Schema(description = "同步耗时(秒)") + private Long durationSeconds; + + @Schema(description = "同步详细日志") + private String syncLog; +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/model/dto/RepositorySyncTaskDTO.java b/backend/src/main/java/com/qqchen/deploy/backend/model/dto/RepositorySyncTaskDTO.java new file mode 100644 index 00000000..1f7d16cc --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/model/dto/RepositorySyncTaskDTO.java @@ -0,0 +1,51 @@ +package com.qqchen.deploy.backend.model.dto; + +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; + +/** + * 仓库同步任务DTO + * 用于展示正在运行的同步任务信息 + * + * @author QQchen + * @version 1.0 + */ +@Data +@Schema(description = "仓库同步任务DTO") +@EqualsAndHashCode(callSuper = true) +public class RepositorySyncTaskDTO extends BaseDTO { + + @Schema(description = "外部系统ID") + private Long externalSystemId; + + @Schema(description = "外部系统名称") + private String externalSystemName; + + @Schema(description = "同步开始时间") + private LocalDateTime startTime; + + @Schema(description = "同步状态:PENDING-等待中,RUNNING-运行中,SUCCESS-成功,FAILED-失败") + private String status; + + @Schema(description = "错误信息") + private String errorMessage; + + @Schema(description = "是否正在运行") + private Boolean isRunning; + + @Schema(description = "已同步的组数量") + private Integer syncedGroupCount; + + @Schema(description = "已同步的项目数量") + private Integer syncedProjectCount; + + @Schema(description = "已同步的分支数量") + private Integer syncedBranchCount; + + @Schema(description = "预计剩余时间(秒)") + private Long estimatedRemainingSeconds; +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/repository/IRepositoryBranchRepository.java b/backend/src/main/java/com/qqchen/deploy/backend/repository/IRepositoryBranchRepository.java index edf18fdf..78c3f28e 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/repository/IRepositoryBranchRepository.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/repository/IRepositoryBranchRepository.java @@ -7,11 +7,17 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; +import java.util.List; +import java.util.Optional; + @Repository public interface IRepositoryBranchRepository extends IBaseRepository { @Modifying @Transactional - @Query("DELETE FROM RepositoryBranch b WHERE b.repositoryId = :repositoryId") - void deleteByRepositoryId(Long repositoryId); + void deleteByExternalSystemId(Long externalSystemId); + + Optional findByExternalSystemIdAndProjectIdAndName(Long externalSystemId, Long projectId, String name); + + List findByExternalSystemIdAndProjectIdAndDeletedFalse(Long externalSystemId, Long projectId); } \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/repository/IRepositoryConfigRepository.java b/backend/src/main/java/com/qqchen/deploy/backend/repository/IRepositoryConfigRepository.java deleted file mode 100644 index 4b099c8e..00000000 --- a/backend/src/main/java/com/qqchen/deploy/backend/repository/IRepositoryConfigRepository.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.qqchen.deploy.backend.repository; - - -import com.qqchen.deploy.backend.framework.repository.IBaseRepository; -import com.qqchen.deploy.backend.entity.RepositoryConfig; -import org.springframework.stereotype.Repository; - -import java.util.List; - -@Repository -public interface IRepositoryConfigRepository extends IBaseRepository { - List findByDeletedFalseOrderBySort(); - boolean existsByNameAndDeletedFalse(String name); -} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/repository/IRepositoryGroupRepository.java b/backend/src/main/java/com/qqchen/deploy/backend/repository/IRepositoryGroupRepository.java index ca58fc84..30e07377 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/repository/IRepositoryGroupRepository.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/repository/IRepositoryGroupRepository.java @@ -7,6 +7,7 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; +import java.util.List; import java.util.Optional; @Repository @@ -14,8 +15,9 @@ public interface IRepositoryGroupRepository extends IBaseRepository findByRepositoryIdAndGroupId(Long repositoryId, Long groupId); + void deleteByExternalSystemId(Long externalSystemId); + + Optional findByExternalSystemIdAndGroupId(Long externalSystemId, Long groupId); + + List findByExternalSystemIdAndDeletedFalseOrderBySortAsc(Long externalSystemId); } \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/repository/IRepositoryProjectRepository.java b/backend/src/main/java/com/qqchen/deploy/backend/repository/IRepositoryProjectRepository.java index 670c289d..a992f90d 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/repository/IRepositoryProjectRepository.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/repository/IRepositoryProjectRepository.java @@ -7,6 +7,7 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; +import java.util.List; import java.util.Optional; @Repository @@ -14,8 +15,9 @@ public interface IRepositoryProjectRepository extends IBaseRepository findByRepositoryIdAndProjectId(Long repositoryId, Long projectId); + void deleteByExternalSystemId(Long externalSystemId); + + Optional findByExternalSystemIdAndProjectId(Long externalSystemId, Long projectId); + + List findByExternalSystemIdAndGroupIdAndDeletedFalseOrderBySortAsc(Long externalSystemId, Long groupId); } \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/service/IRepositoryVersionControlService.java b/backend/src/main/java/com/qqchen/deploy/backend/service/IRepositoryVersionControlService.java new file mode 100644 index 00000000..3f57cec1 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/service/IRepositoryVersionControlService.java @@ -0,0 +1,132 @@ +package com.qqchen.deploy.backend.service; + +import com.qqchen.deploy.backend.entity.RepositoryGroup; +import com.qqchen.deploy.backend.entity.RepositoryProject; +import com.qqchen.deploy.backend.entity.RepositoryBranch; +import com.qqchen.deploy.backend.model.dto.RepositoryGroupDTO; +import com.qqchen.deploy.backend.model.dto.RepositoryProjectDTO; +import com.qqchen.deploy.backend.model.dto.RepositoryBranchDTO; +import com.qqchen.deploy.backend.model.dto.RepositorySyncStatusDTO; +import com.qqchen.deploy.backend.model.dto.RepositorySyncTaskDTO; + +import java.util.List; + +/** + * 版本控制仓库服务接口 + * 提供代码仓库的组织结构、项目和分支的管理功能,以及与外部版本控制系统的数据同步功能 + * + * @author QQchen + * @version 1.0 + */ +public interface IRepositoryVersionControlService { + + /** + * 获取指定外部系统的仓库组列表 + * + * @param externalSystemId 外部系统ID + * @return 仓库组列表,按排序号升序排列 + */ + List listGroups(Long externalSystemId); + + /** + * 获取指定的仓库组信息 + * + * @param externalSystemId 外部系统ID + * @param groupId 仓库组ID + * @return 仓库组信息 + * @throws com.qqchen.deploy.backend.framework.exception.BusinessException 当仓库组不存在时抛出异常 + */ + RepositoryGroupDTO getGroup(Long externalSystemId, Long groupId); + + /** + * 同步指定外部系统的仓库组数据 + * + * @param externalSystemId 外部系统ID + */ + void syncGroups(Long externalSystemId); + + /** + * 获取指定仓库组下的项目列表 + * + * @param externalSystemId 外部系统ID + * @param groupId 仓库组ID + * @return 项目列表,按排序号升序排列 + */ + List listProjects(Long externalSystemId, Long groupId); + + /** + * 获取指定的项目信息 + * + * @param externalSystemId 外部系统ID + * @param projectId 项目ID + * @return 项目信息 + * @throws com.qqchen.deploy.backend.framework.exception.BusinessException 当项目不存在时抛出异常 + */ + RepositoryProjectDTO getProject(Long externalSystemId, Long projectId); + + /** + * 同步指定仓库组下的项目数据 + * + * @param externalSystemId 外部系统ID + * @param groupId 仓库组ID + */ + void syncProjects(Long externalSystemId, Long groupId); + + /** + * 获取指定项目的分支列表 + * + * @param externalSystemId 外部系统ID + * @param projectId 项目ID + * @return 分支列表 + */ + List listBranches(Long externalSystemId, Long projectId); + + /** + * 获取指定的分支信息 + * + * @param externalSystemId 外部系统ID + * @param projectId 项目ID + * @param branchName 分支名称 + * @return 分支信息 + * @throws com.qqchen.deploy.backend.framework.exception.BusinessException 当分支不存在时抛出异常 + */ + RepositoryBranchDTO getBranch(Long externalSystemId, Long projectId, String branchName); + + /** + * 同步指定项目的分支数据 + * + * @param externalSystemId 外部系统ID + * @param projectId 项目ID + */ + void syncBranches(Long externalSystemId, Long projectId); + + /** + * 同步指定外部系统的所有数据(同步组、项目、分支) + * + * @param externalSystemId 外部系统ID + */ + void syncAll(Long externalSystemId); + + /** + * 异步同步指定外部系统的所有数据 + * + * @param externalSystemId 外部系统ID + * @return 同步任务ID + */ + Long asyncSyncAll(Long externalSystemId); + + /** + * 获取同步任务状态 + * + * @param historyId 同步历史ID + * @return 同步状态信息 + */ + RepositorySyncStatusDTO getSyncStatus(Long historyId); + + /** + * 获取正在运行的同步任务列表 + * + * @return 运行中的同步任务列表 + */ + List getRunningSyncs(); +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/service/impl/RepositoryVersionControlServiceImpl.java b/backend/src/main/java/com/qqchen/deploy/backend/service/impl/RepositoryVersionControlServiceImpl.java new file mode 100644 index 00000000..73472ba3 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/service/impl/RepositoryVersionControlServiceImpl.java @@ -0,0 +1,145 @@ +package com.qqchen.deploy.backend.service.impl; + +import com.qqchen.deploy.backend.entity.RepositoryGroup; +import com.qqchen.deploy.backend.entity.RepositoryProject; +import com.qqchen.deploy.backend.entity.RepositoryBranch; +import com.qqchen.deploy.backend.framework.annotation.ServiceType; +import com.qqchen.deploy.backend.framework.enums.ResponseCode; +import com.qqchen.deploy.backend.framework.exception.BusinessException; +import com.qqchen.deploy.backend.model.dto.RepositoryGroupDTO; +import com.qqchen.deploy.backend.model.dto.RepositoryProjectDTO; +import com.qqchen.deploy.backend.model.dto.RepositoryBranchDTO; +import com.qqchen.deploy.backend.model.dto.RepositorySyncStatusDTO; +import com.qqchen.deploy.backend.model.dto.RepositorySyncTaskDTO; +import com.qqchen.deploy.backend.repository.IRepositoryGroupRepository; +import com.qqchen.deploy.backend.repository.IRepositoryProjectRepository; +import com.qqchen.deploy.backend.repository.IRepositoryBranchRepository; +import com.qqchen.deploy.backend.service.IRepositoryVersionControlService; +import com.qqchen.deploy.backend.converter.RepositoryConverter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * 版本控制仓库服务实现类 + * 用于管理代码仓库的组织结构、项目和分支,支持与外部版本控制系统(如GitLab)的数据同步 + * + * @author QQchen + * @version 1.0 + */ +@Slf4j +@Service +@ServiceType(ServiceType.Type.DATABASE) +@RequiredArgsConstructor +public class RepositoryVersionControlServiceImpl implements IRepositoryVersionControlService { + + private final IRepositoryGroupRepository groupRepository; + private final IRepositoryProjectRepository projectRepository; + private final IRepositoryBranchRepository branchRepository; + private final RepositoryConverter repositoryConverter; + + @Override + public List listGroups(Long externalSystemId) { + log.debug("获取仓库组列表, externalSystemId: {}", externalSystemId); + return groupRepository.findByExternalSystemIdAndDeletedFalseOrderBySortAsc(externalSystemId) + .stream() + .map(repositoryConverter::toDto) + .collect(Collectors.toList()); + } + + @Override + public RepositoryGroupDTO getGroup(Long externalSystemId, Long groupId) { + log.debug("获取仓库组信息, externalSystemId: {}, groupId: {}", externalSystemId, groupId); + RepositoryGroup group = groupRepository.findByExternalSystemIdAndGroupId(externalSystemId, groupId) + .orElseThrow(() -> new BusinessException(ResponseCode.REPOSITORY_GROUP_NOT_FOUND)); + return repositoryConverter.toDto(group); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void syncGroups(Long externalSystemId) { + log.info("开始同步仓库组数据, externalSystemId: {}", externalSystemId); + // TODO: 实现仓库组同步逻辑 + } + + @Override + public List listProjects(Long externalSystemId, Long groupId) { + log.debug("获取项目列表, externalSystemId: {}, groupId: {}", externalSystemId, groupId); + return projectRepository.findByExternalSystemIdAndGroupIdAndDeletedFalseOrderBySortAsc(externalSystemId, groupId) + .stream() + .map(repositoryConverter::toDto) + .collect(Collectors.toList()); + } + + @Override + public RepositoryProjectDTO getProject(Long externalSystemId, Long projectId) { + log.debug("获取项目信息, externalSystemId: {}, projectId: {}", externalSystemId, projectId); + RepositoryProject project = projectRepository.findByExternalSystemIdAndProjectId(externalSystemId, projectId) + .orElseThrow(() -> new BusinessException(ResponseCode.REPOSITORY_PROJECT_NOT_FOUND)); + return repositoryConverter.toDto(project); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void syncProjects(Long externalSystemId, Long groupId) { + log.info("开始同步项目数据, externalSystemId: {}, groupId: {}", externalSystemId, groupId); + // TODO: 实现项目同步逻辑 + } + + @Override + public List listBranches(Long externalSystemId, Long projectId) { + log.debug("获取分支列表, externalSystemId: {}, projectId: {}", externalSystemId, projectId); + return branchRepository.findByExternalSystemIdAndProjectIdAndDeletedFalse(externalSystemId, projectId) + .stream() + .map(repositoryConverter::toDto) + .collect(Collectors.toList()); + } + + @Override + public RepositoryBranchDTO getBranch(Long externalSystemId, Long projectId, String branchName) { + log.debug("获取分支信息, externalSystemId: {}, projectId: {}, branchName: {}", + externalSystemId, projectId, branchName); + RepositoryBranch branch = branchRepository.findByExternalSystemIdAndProjectIdAndName(externalSystemId, projectId, branchName) + .orElseThrow(() -> new BusinessException(ResponseCode.REPOSITORY_BRANCH_NOT_FOUND)); + return repositoryConverter.toDto(branch); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void syncBranches(Long externalSystemId, Long projectId) { + log.info("开始同步分支数据, externalSystemId: {}, projectId: {}", externalSystemId, projectId); + // TODO: 实现分支同步逻辑 + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void syncAll(Long externalSystemId) { + log.info("开始全量同步数据, externalSystemId: {}", externalSystemId); + // TODO: 实现全量同步逻辑 + } + + @Override + public Long asyncSyncAll(Long externalSystemId) { + log.info("开始异步全量同步数据, externalSystemId: {}", externalSystemId); + // TODO: 实现异步全量同步逻辑 + return null; + } + + @Override + public RepositorySyncStatusDTO getSyncStatus(Long historyId) { + log.debug("获取同步状态, historyId: {}", historyId); + // TODO: 实现获取同步状态逻辑 + return null; + } + + @Override + public List getRunningSyncs() { + log.debug("获取运行中的同步任务列表"); + // TODO: 实现获取运行中的同步任务列表逻辑 + return null; + } +} \ No newline at end of file diff --git a/backend/src/main/resources/db/migration/V1.0.0__init_schema.sql b/backend/src/main/resources/db/migration/V1.0.0__init_schema.sql index 8fe0c5e9..6fb0c843 100644 --- a/backend/src/main/resources/db/migration/V1.0.0__init_schema.sql +++ b/backend/src/main/resources/db/migration/V1.0.0__init_schema.sql @@ -145,7 +145,7 @@ CREATE TABLE sys_role_tag ( version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', name VARCHAR(50) NOT NULL COMMENT '标签名称', - color VARCHAR(20) NULL COMMENT '标签��色(十六进制颜色码)' + color VARCHAR(20) NULL COMMENT '标签���十六进制颜色码)' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色标签表'; -- 角色标签关联表 @@ -261,4 +261,119 @@ CREATE TABLE sys_external_system ( CONSTRAINT UK_external_system_name UNIQUE (name), CONSTRAINT UK_external_system_type_url UNIQUE (type, url) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='外部系统表'; + +-- 代码仓库组表 +CREATE TABLE deploy_repo_group ( + -- 基础字段 + id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID', + 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 '是否删除:0-未删除,1-已删除', + + -- 业务字段 + name VARCHAR(100) NOT NULL COMMENT '仓库组名称', + description VARCHAR(500) NULL COMMENT '仓库组描述', + group_id BIGINT NOT NULL COMMENT '外部系统中的组ID', + parent_id BIGINT NULL COMMENT '父级仓库组ID', + path VARCHAR(200) NOT NULL COMMENT '仓库组路径', + external_system_id BIGINT NOT NULL COMMENT '外部系统ID', + avatar_url VARCHAR(255) NULL COMMENT '头像URL', + web_url VARCHAR(255) NULL COMMENT '网页URL', + visibility ENUM('private', 'internal', 'public') NOT NULL DEFAULT 'private' COMMENT '可见性:private-私有,internal-内部,public-公开', + sort INT DEFAULT 0 COMMENT '排序号', + + -- 索引 + INDEX idx_external_system_group_id (external_system_id, group_id) COMMENT '外部系统组ID索引', + INDEX idx_parent_id (parent_id) COMMENT '父级ID索引', + INDEX idx_path (path) COMMENT '路径索引', + UNIQUE INDEX uk_external_system_group_path (external_system_id, path) COMMENT '外部系统下路径唯一', + + -- 外键约束 + CONSTRAINT fk_group_parent FOREIGN KEY (parent_id) + REFERENCES deploy_repo_group (id), + CONSTRAINT fk_group_external_system FOREIGN KEY (external_system_id) + REFERENCES sys_external_system (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='代码仓库组表'; + +-- 代码仓库项目表 +CREATE TABLE deploy_repo_project ( + -- 基础字段 + id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID', + 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 '是否删除:0-未删除,1-已删除', + + -- 业务字段 + name VARCHAR(100) NOT NULL COMMENT '项目名称', + description VARCHAR(500) NULL COMMENT '项目描述', + project_id BIGINT NOT NULL COMMENT '外部系统中的项目ID', + group_id BIGINT NULL COMMENT '所属仓库组ID', + path VARCHAR(200) NOT NULL COMMENT '项目路径', + external_system_id BIGINT NOT NULL COMMENT '外部系统ID', + is_default_branch VARCHAR(100) NULL COMMENT '默认分支', + is_enabled BIT NOT NULL DEFAULT 1 COMMENT '是否启用:0-禁用,1-启用', + visibility ENUM('private', 'internal', 'public') NOT NULL DEFAULT 'private' COMMENT '可见性:private-私有,internal-内部,public-公开', + http_url VARCHAR(255) NULL COMMENT 'HTTP克隆地址', + ssh_url VARCHAR(255) NULL COMMENT 'SSH克隆地址', + web_url VARCHAR(255) NULL COMMENT '网页URL', + last_activity_at DATETIME(6) NULL COMMENT '最后活动时间', + sort INT DEFAULT 0 COMMENT '排序号', + + -- 索引 + INDEX idx_external_system_project_id (external_system_id, project_id) COMMENT '外部系统项目ID索引', + INDEX idx_group_id (group_id) COMMENT '组ID索引', + INDEX idx_path (path) COMMENT '路径索引', + UNIQUE INDEX uk_external_system_project_path (external_system_id, path) COMMENT '外部系统下路径唯一', + + -- 外键约束 + CONSTRAINT fk_project_group FOREIGN KEY (group_id) + REFERENCES deploy_repo_group (id), + CONSTRAINT fk_project_external_system FOREIGN KEY (external_system_id) + REFERENCES sys_external_system (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='代码仓库项目表'; + +-- 代码仓库分支表 +CREATE TABLE deploy_repo_branch ( + -- 基础字段 + id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID', + 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 '是否删除:0-未删除,1-已删除', + + -- 业务字段 + name VARCHAR(100) NOT NULL COMMENT '分支名称', + project_id BIGINT NOT NULL COMMENT '所属项目ID', + external_system_id BIGINT NOT NULL COMMENT '外部系统ID', + is_default_branch BIT DEFAULT 0 COMMENT '是否为默认分支:0-否,1-是', + is_protected BIT DEFAULT 0 COMMENT '是否受保护:0-否,1-是', + can_push BIT DEFAULT 1 COMMENT '是否可推送:0-否,1-是', + developers_can_push BIT DEFAULT 1 COMMENT '开发者是否可推送:0-否,1-是', + developers_can_merge BIT DEFAULT 1 COMMENT '开发者是否可合并:0-否,1-是', + commit_id VARCHAR(64) NULL COMMENT '最新提交ID', + commit_message TEXT NULL COMMENT '最新提交信息', + commit_author VARCHAR(100) NULL COMMENT '最新提交作者', + commit_date DATETIME(6) NULL COMMENT '最新提交时间', + web_url VARCHAR(255) NULL COMMENT '网页URL', + + -- 索引 + INDEX idx_project_id (project_id) COMMENT '项目ID索引', + INDEX idx_external_system_id (external_system_id) COMMENT '外部系统ID索引', + INDEX idx_name (name) COMMENT '分支名称索引', + UNIQUE INDEX uk_project_branch_name (project_id, name) COMMENT '项目下分支名称唯一', + + -- 外键约束 + CONSTRAINT fk_branch_project FOREIGN KEY (project_id) + REFERENCES deploy_repo_project (id), + CONSTRAINT fk_branch_external_system FOREIGN KEY (external_system_id) + REFERENCES sys_external_system (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='代码仓库分支表'; \ No newline at end of file diff --git a/backend/src/main/resources/messages.properties b/backend/src/main/resources/messages.properties index 8c293b60..a916d4a8 100644 --- a/backend/src/main/resources/messages.properties +++ b/backend/src/main/resources/messages.properties @@ -74,4 +74,17 @@ external.system.type.not.supported=不支持的系统类型 # Git系统相关错误 external.system.git.auth.type.error=Git系统只支持Token认证 -external.system.git.token.required=Git系统必须提供Token \ No newline at end of file +external.system.git.token.required=Git系统必须提供Token + +# 仓库相关错误消息 +repository.group.not.found=仓库组不存在 +repository.group.name.exists=仓库组名称"{0}"已存在 +repository.group.path.exists=仓库组路径"{0}"已存在 +repository.project.not.found=仓库项目不存在 +repository.project.name.exists=仓库项目名称"{0}"已存在 +repository.project.path.exists=仓库项目路径"{0}"已存在 +repository.branch.not.found=分支不存在 +repository.branch.name.exists=分支名称"{0}"已存在 +repository.sync.in.progress=仓库同步正在进行中 +repository.sync.failed=仓库同步失败:{0} +repository.sync.history.not.found=同步历史记录不存在 \ No newline at end of file