内容库版本分支管理功能技术文档
1. 概述
1.1 功能目标
基于现有的内容库版本管理架构,扩展实现类似Git的分支管理功能,支持多分支并行开发、分支合并、冲突解决等核心功能。
1.2 设计原则
核心思想:在数据源和版本中间增加一个"分支"层级
原本:数据源->版本->内容
改造完之后:数据源->分支->版本->内容
基于现有数据结构,最小化改动
支持多分支并行开发
提供完整的版本历史追踪
支持分支合并和冲突解决
2. 现有架构分析
2.1 核心数据结构
2.1.1 ContentVersion(版本表)
java
@Document(collection = "cms_version")
public class ContentVersion {
private String id; // 版本ID
private Integer datasourceId; // 数据源ID
private Integer version; // 版本号(递增)
private VersionSourceTypeEnum sourceType; // 版本来源类型
private VersionCompletionEnum completion; // 版本完成度
private Boolean isRelease; // 是否为正式版本
private String hash; // 7位短hash
private ContentVersionStatusEnum status; // 状态:ACTIVE/ARCHIVED
private LocalDateTime createTime; // 创建时间
private Long creatorId; // 创建人ID
}2.1.2 Content(内容表)
java
@Document(collection = "cms_content")
public class Content {
private String id; // 内容ID
private Integer datasourceId; // 数据源ID
private Version version; // 版本信息
private String key; // 内容唯一标识
private ContentStatusEnum status; // 内容状态
private Source source; // 源文本信息
private List<Translation> translations; // 翻译列表
private Metadata metadata; // 元数据
private Integer isDeleted; // 是否删除
// ... 其他字段
}2.2 现有版本管理机制
- 版本类型:INIT(初始化)、SYNC(同步)、UPDATE(更新)、ORDER_DELIVERY(订单交付)
- 版本状态:ACTIVE(活跃)、ARCHIVED(归档)
- 版本完成度:PART(部分完成)、FULL(全部完成)
- 版本限制:最多保留3个活跃的正式版本
3. 分支管理功能设计
3.1 数据结构扩展
3.1.1 新增分支表(ContentBranch)
java
@Document(collection = "cms_branch")
public class ContentBranch {
@Id
private String id; // 分支ID
private Integer datasourceId; // 数据源ID
private String name; // 分支名称
private String description; // 分支描述
private String baseVersionId; // 基于的版本ID
private String currentVersionId; // 当前版本ID
@Enumerated(EnumType.STRING)
private BranchStatusEnum status; // 分支状态:ACTIVE/MERGED/DELETED
private Boolean isDefault; // 是否为默认分支
private LocalDateTime createTime; // 创建时间
private LocalDateTime updateTime; // 更新时间
private Long creatorId; // 创建人ID
private Long updaterId; // 更新人ID
}3.1.2 扩展ContentVersion表
java
// 在现有ContentVersion基础上新增字段
public class ContentVersion {
// ... 现有字段
private String branchId; // 所属分支ID
private List<String> mergeFromBranchIds; // 合并来源分支ID列表
private MergeStatusEnum mergeStatus; // 合并状态:NONE/MERGING/MERGED/CONFLICT
private String mergeConflictInfo; // 合并冲突信息(JSON格式,也就是将检测冲突的结果保存起来)
}3.1.3 扩展DataSource表
java
// 在现有DataSource基础上新增字段
@Entity
@Table(name = "t_cms_datasource")
public class DataSource {
// ... 现有字段
/**
* 当前选中的分支ID
*/
private String currentBranchId;
// ... 其他现有字段
}3.1.4 新增枚举类型
java
// 分支状态枚举
public enum BranchStatusEnum {
ACTIVE, // 活跃
MERGED, // 已合并
DELETED // 已删除
}
// 合并状态枚举
public enum MergeStatusEnum {
NONE, // 无合并
MERGING, // 合并中
MERGED, // 已合并
CONFLICT // 冲突
}3.2 分支管理核心功能
3.2.1 分支创建
java
public class BranchService {
/**
* 创建新分支
*/
public ContentBranch createBranch(CreateBranchParam param) {
// 1. 验证基础版本是否存在
ContentVersion baseVersion = contentSupport.getVersionById(param.getBaseVersionId());
if (baseVersion == null) {
throw new BusinessException(ExceptionEnum.VERSION_NOT_FOUND);
}
// 2. 验证分支名称唯一性
if (isBranchNameExists(param.getDatasourceId(), param.getName())) {
throw new BusinessException(ExceptionEnum.BRANCH_NAME_EXISTS);
}
// 3. 创建分支记录
ContentBranch branch = new ContentBranch();
branch.setDatasourceId(param.getDatasourceId());
branch.setName(param.getName());
branch.setDescription(param.getDescription());
branch.setBaseVersionId(param.getBaseVersionId());
branch.setCurrentVersionId(param.getBaseVersionId());
branch.setStatus(BranchStatusEnum.ACTIVE);
branch.setIsDefault(false);
branch.setIsProtected(false);
branch.setCreateTime(LocalDateTime.now());
branch.setCreatorId(param.getUserId());
return branchRepository.save(branch);
}
}3.2.2 分支切换(更新数据源表t_cms_datasource的currentBranchId字段)
java
/**
* 切换到指定分支
*/
public void switchBranch(Integer datasourceId, String branchId) {
// 1. 验证分支存在且活跃
ContentBranch branch = branchRepository.findById(branchId)
.orElseThrow(() -> new BusinessException(ExceptionEnum.BRANCH_NOT_FOUND));
if (!BranchStatusEnum.ACTIVE.equals(branch.getStatus())) {
throw new BusinessException(ExceptionEnum.BRANCH_NOT_ACTIVE);
}
// 2. 更新数据源的当前分支
DataSource dataSource = datasourceRepository.findById(datasourceId)
.orElseThrow(() -> new BusinessException(ExceptionEnum.DATASOURCE_NOT_FOUND));
dataSource.setCurrentBranchId(branchId);
dataSource.setGmtModified(LocalDateTime.now());
datasourceRepository.save(dataSource);
}3.2.3 分支合并(需不需要用颜色区分更新、新增、删除)
3.2.3.1 合并冲突定义
冲突类型定义: 在内容库分支管理中,合并冲突主要发生在以下情况:
1. 内容冲突(Content Conflict)
- 定义:两个分支对同一个内容项(相同的key)进行了不同的修改
- 触发条件:
- 源分支和目标分支都包含相同key的内容
- 两个分支的内容在以下任一字段存在差异:
source.text(源文本)translations(翻译内容)metadata(元数据)dynamicFields(动态字段)
2. 结构冲突(Structural Conflict)
以下三种情况算不算冲突?待确定
- 定义:两个分支对内容结构进行了不兼容的修改
- 触发条件:
- 源分支删除了目标分支中仍存在的内容
- 源分支和目标分支对同一内容的翻译语言配置不同
- 源分支和目标分支的元数据结构不一致
结构冲突示例:
示例1:内容删除冲突
json
// 目标分支(main)中的内容
{
"key": "welcome_message",
"source": {
"text": "Welcome to our application",
"language": "en-US"
},
"translations": [
{
"language": "zh-CN",
"text": "欢迎使用我们的应用",
"status": "TRANSLATED"
}
]
}
// 源分支(feature/cleanup)中删除了该内容
// 结果:结构冲突 - 目标分支存在内容,源分支已删除示例2:翻译语言配置冲突
json
// 目标分支(main)中的内容
{
"key": "product_description",
"source": {
"text": "High quality product",
"language": "en-US"
},
"translations": [
{
"language": "zh-CN",
"text": "高质量产品",
"status": "TRANSLATED"
},
{
"language": "ja-JP",
"text": "高品質製品",
"status": "TRANSLATED"
}
]
}
// 源分支(feature/internationalization)中的内容
{
"key": "product_description",
"source": {
"text": "High quality product",
"language": "en-US"
},
"translations": [
{
"language": "zh-CN",
"text": "高质量产品",
"status": "TRANSLATED"
},
{
"language": "ko-KR",
"text": "고품질 제품",
"status": "TRANSLATED"
}
]
}
// 结果:结构冲突 - 翻译语言配置不一致(ja-JP vs ko-KR)示例3:元数据结构冲突
json
// 目标分支(main)中的内容
{
"key": "user_guide",
"source": {
"text": "User guide content",
"language": "en-US"
},
"metadata": {
"ueKey": "user_guide",
"ueNamespace": "ui",
"sheetName": "UserGuide",
"category": "documentation"
}
}
// 源分支(feature/metadata-enhancement)中的内容
{
"key": "user_guide",
"source": {
"text": "User guide content",
"language": "en-US"
},
"metadata": {
"ueKey": "user_guide",
"ueNamespace": "ui",
"sheetName": "UserGuide",
"priority": "high",
"tags": ["guide", "user"]
}
}
// 结果:结构冲突 - 元数据字段结构不一致(category vs priority+tags)3.2.3.2 冲突检测规则
冲突检测算法:
java
/**
* 冲突检测规则定义
*/
public class ConflictDetectionRules {
/**
* 检测两个内容是否存在冲突
*/
public static boolean hasConflict(Content sourceContent, Content targetContent) {
// 1. 源文本冲突检测
if (hasSourceTextConflict(sourceContent, targetContent)) {
return true;
}
// 2. 翻译内容冲突检测
if (hasTranslationConflict(sourceContent, targetContent)) {
return true;
}
// 3. 元数据冲突检测
if (hasMetadataConflict(sourceContent, targetContent)) {
return true;
}
// 4. 动态字段冲突检测
if (hasDynamicFieldsConflict(sourceContent, targetContent)) {
return true;
}
return false;
}
/**
* 源文本冲突检测
*/
private static boolean hasSourceTextConflict(Content source, Content target) {
String sourceText = source.getSource().getText();
String targetText = target.getSource().getText();
return !Objects.equals(sourceText, targetText);
}
/**
* 翻译内容冲突检测
*/
private static boolean hasTranslationConflict(Content source, Content target) {
List<Content.Translation> sourceTranslations = source.getTranslations();
List<Content.Translation> targetTranslations = target.getTranslations();
// 检查翻译语言是否一致
Set<String> sourceLanguages = sourceTranslations.stream()
.map(Content.Translation::getLanguage)
.collect(Collectors.toSet());
Set<String> targetLanguages = targetTranslations.stream()
.map(Content.Translation::getLanguage)
.collect(Collectors.toSet());
if (!sourceLanguages.equals(targetLanguages)) {
return true;
}
// 检查相同语言的翻译内容是否一致
for (Content.Translation sourceTrans : sourceTranslations) {
Content.Translation targetTrans = targetTranslations.stream()
.filter(t -> t.getLanguage().equals(sourceTrans.getLanguage()))
.findFirst()
.orElse(null);
if (targetTrans != null && !Objects.equals(sourceTrans.getText(), targetTrans.getText())) {
return true;
}
}
return false;
}
/**
* 元数据冲突检测
*/
private static boolean hasMetadataConflict(Content source, Content target) {
Content.Metadata sourceMetadata = source.getMetadata();
Content.Metadata targetMetadata = target.getMetadata();
// 检查关键元数据字段
return !Objects.equals(sourceMetadata.getUeKey(), targetMetadata.getUeKey()) ||
!Objects.equals(sourceMetadata.getUeNamespace(), targetMetadata.getUeNamespace()) ||
!Objects.equals(sourceMetadata.getSheetName(), targetMetadata.getSheetName());
}
/**
* 动态字段冲突检测
*/
private static boolean hasDynamicFieldsConflict(Content source, Content target) {
Map<String, Object> sourceFields = source.getDynamicFields();
Map<String, Object> targetFields = target.getDynamicFields();
if (sourceFields == null && targetFields == null) {
return false;
}
if (sourceFields == null || targetFields == null) {
return true;
}
return !sourceFields.equals(targetFields);
}
}3.2.3.3 冲突解决策略
冲突解决选项:
- 保留源分支内容(KEEP_SOURCE):使用源分支的完整内容
- 保留目标分支内容(KEEP_TARGET):使用目标分支的完整内容
- 手动合并(MANUAL_MERGE):用户手动选择合并策略(难实现!)暂不实现
- 智能合并(SMART_MERGE):系统自动选择最优合并策略 暂不实现
智能合并规则:
- 如果只有一个分支修改了某个字段,使用修改后的值
- 如果两个分支都修改了同一字段,优先使用目标分支的值
- 对于翻译内容,合并所有语言的翻译,冲突时使用目标分支的值
3.2.3.4 分支合并实现
java
/**
* 合并分支
*/
public MergeResult mergeBranch(MergeBranchParam param) {
// 1. 验证源分支和目标分支
ContentBranch sourceBranch = validateBranch(param.getSourceBranchId());
ContentBranch targetBranch = validateBranch(param.getTargetBranchId());
// 2. 检查是否有冲突
List<ContentConflict> conflicts = detectConflicts(sourceBranch, targetBranch);
if (!conflicts.isEmpty()) {
// 存在冲突,返回冲突信息
return MergeResult.conflict(conflicts);
}
// 3. 执行合并
return executeMerge(sourceBranch, targetBranch, param.getUserId());
}
/**
* 检测冲突
*/
private List<ContentConflict> detectConflicts(ContentBranch sourceBranch, ContentBranch targetBranch) {
List<ContentConflict> conflicts = new ArrayList<>();
// 获取两个分支的最新内容
List<Content> sourceContents = contentSupport.getContentListByBranch(sourceBranch.getId());
List<Content> targetContents = contentSupport.getContentListByBranch(targetBranch.getId());
// 构建key到content的映射
Map<String, Content> sourceMap = sourceContents.stream()
.collect(Collectors.toMap(Content::getKey, c -> c));
Map<String, Content> targetMap = targetContents.stream()
.collect(Collectors.toMap(Content::getKey, c -> c));
// 检测冲突
for (String key : sourceMap.keySet()) {
if (targetMap.containsKey(key)) {
Content sourceContent = sourceMap.get(key);
Content targetContent = targetMap.get(key);
// 使用冲突检测规则检查是否有冲突
if (ConflictDetectionRules.hasConflict(sourceContent, targetContent)) {
conflicts.add(new ContentConflict(key, sourceContent, targetContent));
}
}
}
return conflicts;
}
/**
* 执行合并
*/
private MergeResult executeMerge(ContentBranch sourceBranch, ContentBranch targetBranch, Long userId) {
// 1. 创建新的合并版本
ContentVersion mergeVersion = createMergeVersion(sourceBranch, targetBranch, userId);
// 2. 合并内容
List<Content> mergedContents = mergeContents(sourceBranch, targetBranch, mergeVersion);
// 3. 保存合并结果
contentSupport.saveContents(
targetBranch.getDatasourceId(),
null,
null,
TaskTypeEnum.MERGE,
VersionCompletionEnum.FULL,
true,
mergedContents,
null,
null,
userId,
null
);
return MergeResult.success(mergeVersion.getId(), mergedContents.size());
}3.2.4 冲突解决
java
/**
* 解决合并冲突
*/
public void resolveConflict(ResolveConflictParam param) {
// 1. 验证冲突是否存在
ContentConflict conflict = conflictRepository.findById(param.getConflictId())
.orElseThrow(() -> new BusinessException(ExceptionEnum.CONFLICT_NOT_FOUND));
// 2. 根据用户选择解决冲突
Content resolvedContent = resolveConflictContent(conflict, param.getResolution());
// 3. 更新内容
contentSupport.updateContent(resolvedContent);
// 4. 标记冲突为已解决
conflict.setStatus(ConflictStatusEnum.RESOLVED);
conflict.setResolvedBy(param.getUserId());
conflict.setResolvedAt(LocalDateTime.now());
conflictRepository.save(conflict);
}
/**
* 根据解决策略解析冲突内容
*/
private Content resolveConflictContent(ContentConflict conflict, ConflictResolution resolution) {
switch (resolution.getType()) {
case KEEP_SOURCE:
return conflict.getSourceContent();
case KEEP_TARGET:
return conflict.getTargetContent();
case MANUAL_MERGE:
return resolution.getCustomContent();
case SMART_MERGE:
return smartMergeContent(conflict.getSourceContent(), conflict.getTargetContent());
default:
throw new BusinessException(ExceptionEnum.INVALID_RESOLUTION_TYPE);
}
}
/**
* 智能合并内容
*/
private Content smartMergeContent(Content source, Content target) {
Content merged = new Content();
// 复制基础字段
merged.setId(target.getId());
merged.setKey(target.getKey());
merged.setDatasourceId(target.getDatasourceId());
// 智能合并源文本
merged.setSource(smartMergeSource(source.getSource(), target.getSource()));
// 智能合并翻译
merged.setTranslations(smartMergeTranslations(source.getTranslations(), target.getTranslations()));
// 智能合并元数据
merged.setMetadata(smartMergeMetadata(source.getMetadata(), target.getMetadata()));
// 智能合并动态字段
merged.setDynamicFields(smartMergeDynamicFields(source.getDynamicFields(), target.getDynamicFields()));
return merged;
}3.3 数据迁移策略(尽量不迁移,舍弃掉之前所有数据)
3.3.1 现有数据迁移
java
/**
* 迁移现有数据到分支管理
*/
public void migrateToBranchManagement(Integer datasourceId) {
// 1. 创建默认分支
ContentBranch defaultBranch = createDefaultBranch(datasourceId);
// 2. 为现有版本添加分支信息
List<ContentVersion> versions = contentSupport.getAllVersions(datasourceId);
for (ContentVersion version : versions) {
version.setBranchId(defaultBranch.getId());
version.setMergeStatus(MergeStatusEnum.NONE);
contentSupport.updateVersion(version);
}
// 3. 为现有内容添加分支信息
List<Content> contents = contentSupport.getAllContents(datasourceId);
for (Content content : contents) {
// 内容的分支信息通过version.branchId获取
contentSupport.updateContent(content);
}
// 4. 更新数据源的当前分支
DataSource dataSource = datasourceRepository.findById(datasourceId)
.orElseThrow(() -> new BusinessException(ExceptionEnum.DATASOURCE_NOT_FOUND));
dataSource.setCurrentBranchId(defaultBranch.getId());
dataSource.setGmtModified(LocalDateTime.now());
datasourceRepository.save(dataSource);
}4. API接口设计
4.1 分支管理接口
4.1.1 创建分支
接口地址: POST /api/branches
请求参数:
json
{
"datasourceId": 123,
"name": "feature/new-ui",
"description": "新UI功能开发分支",
"baseVersionId": "version_001",
"userId": 1001
}响应结果:
json
{
"code": 200,
"message": "success",
"data": {
"id": "branch_001",
"datasourceId": 123,
"name": "feature/new-ui",
"description": "新UI功能开发分支",
"baseVersionId": "version_001",
"currentVersionId": "version_001",
"status": "ACTIVE",
"isDefault": false,
"isProtected": false,
"createTime": "2024-01-01T10:00:00Z",
"updateTime": "2024-01-01T10:00:00Z",
"creatorId": 1001,
"updaterId": 1001
}
}4.1.2 获取分支列表
接口地址: GET /api/branches?datasourceId=123
请求参数:
datasourceId: 123响应结果:
json
{
"code": 200,
"message": "success",
"data": [
{
"id": "branch_001",
"name": "main",
"description": "主分支",
"status": "ACTIVE",
"isDefault": true,
"isProtected": true,
"currentVersionId": "version_005",
"createTime": "2024-01-01T10:00:00Z",
"creatorName": "张三",
"contentCount": 150
},
{
"id": "branch_002",
"name": "feature/new-ui",
"description": "新UI功能开发分支",
"status": "ACTIVE",
"isDefault": false,
"isProtected": false,
"currentVersionId": "version_003",
"createTime": "2024-01-02T14:30:00Z",
"creatorName": "李四",
"contentCount": 25
}
]
}4.1.3 切换分支
接口地址: POST /api/branches/switch
请求参数:
json
{
"datasourceId": 123,
"branchId": "branch_002"
}响应结果:
json
{
"code": 200,
"message": "success",
"data": null
}4.1.4 合并分支
接口地址: POST /api/branches/merge
请求参数:
json
{
"sourceBranchId": "branch_002",
"targetBranchId": "branch_001",
"mergeStrategy": "AUTO",
"userId": 1001
}响应结果(无冲突):
json
{
"code": 200,
"message": "success",
"data": {
"mergeId": "merge_001",
"status": "SUCCESS",
"mergedVersionId": "version_006",
"mergedContentCount": 25,
"conflictCount": 0,
"createTime": "2024-01-03T16:45:00Z"
}
}响应结果(有冲突):
json
{
"code": 200,
"message": "success",
"data": {
"mergeId": "merge_002",
"status": "CONFLICT",
"conflictCount": 3,
"conflicts": [
{
"id": "conflict_001",
"contentKey": "welcome_message",
"sourceContent": {
"id": "content_001",
"key": "welcome_message",
"source": {
"text": "Welcome to our application",
"language": "en-US"
},
"translations": [
{
"language": "zh-CN",
"text": "欢迎使用我们的应用",
"status": "TRANSLATED"
}
]
},
"targetContent": {
"id": "content_002",
"key": "welcome_message",
"source": {
"text": "Welcome to our new application",
"language": "en-US"
},
"translations": [
{
"language": "zh-CN",
"text": "欢迎使用我们的新应用",
"status": "TRANSLATED"
}
]
}
}
],
"createTime": "2024-01-03T16:45:00Z"
}
}4.1.5 解决冲突
接口地址: POST /api/branches/conflicts/resolve
请求参数:
json
{
"conflictId": "conflict_001",
"resolution": {
"type": "KEEP_SOURCE",
"customContent": null
},
"userId": 1001
}响应结果:
json
{
"code": 200,
"message": "success",
"data": null
}5. 数据库设计
5.1 新增集合
5.1.1 cms_branch(分支表)
javascript
{
"_id": "branch_id",
"datasourceId": 123,
"name": "feature/new-ui",
"description": "新UI功能开发分支",
"baseVersionId": "version_001",
"currentVersionId": "version_005",
"status": "ACTIVE",
"isDefault": false,
"isProtected": false,
"createTime": "2024-01-01T10:00:00Z",
"updateTime": "2024-01-01T10:00:00Z",
"creatorId": 1001,
"updaterId": 1001
}5.1.2 cms_conflict(冲突表)
javascript
{
"_id": "conflict_id",
"mergeId": "merge_001",
"contentKey": "welcome_message",
"sourceContent": { /* 源分支内容 */ },
"targetContent": { /* 目标分支内容 */ },
"status": "PENDING",
"createTime": "2024-01-01T10:00:00Z",
"resolvedBy": null,
"resolvedAt": null
}5.2 索引设计
javascript
// cms_branch 索引
db.cms_branch.createIndex({"datasourceId": 1, "name": 1}, {unique: true})
db.cms_branch.createIndex({"datasourceId": 1, "status": 1})
db.cms_branch.createIndex({"isDefault": 1})
// cms_version 新增索引
db.cms_version.createIndex({"branchId": 1, "version": -1})
db.cms_version.createIndex({"mergeFromBranchIds": 1})
// cms_conflict 索引
db.cms_conflict.createIndex({"mergeId": 1})
db.cms_conflict.createIndex({"status": 1})6. 部署和迁移
6.1 部署步骤
数据库迁移
- 创建新的集合:
cms_branch、cms_conflict - 为现有集合添加新字段
- 创建必要的索引
- 创建新的集合:
代码部署
- 部署新的分支管理功能代码
- 运行数据迁移脚本
- 验证功能正常
功能开关
- 使用功能开关控制分支管理功能的启用
- 逐步迁移现有数据源
6.2 数据迁移脚本
java
@Component
public class BranchMigrationService {
@EventListener(ApplicationReadyEvent.class)
public void migrateExistingData() {
log.info("开始迁移现有数据到分支管理系统");
// 获取所有数据源
List<DataSource> dataSources = datasourceSupport.getAllDataSources();
for (DataSource dataSource : dataSources) {
try {
migrateToBranchManagement(dataSource.getId());
log.info("数据源 {} 迁移完成", dataSource.getId());
} catch (Exception e) {
log.error("数据源 {} 迁移失败: {}", dataSource.getId(), e.getMessage());
}
}
log.info("数据迁移完成");
}
}