From e37e8dfca6d1d35ca1c94e2f86b0434d96ecb520 Mon Sep 17 00:00:00 2001 From: bruce Date: Sun, 24 May 2026 21:12:14 +0800 Subject: [PATCH] =?UTF-8?q?feat(enum):=20=E7=BB=9F=E4=B8=80=E7=BB=93?= =?UTF-8?q?=E6=9E=84=E5=8C=96=E6=9E=9A=E4=B8=BE=E5=80=BC=E4=BC=A0=E8=BE=93?= =?UTF-8?q?=E4=B8=8E=E5=90=8C=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENT.md | 29 ++++ docs/ARCHITECTURE.md | 13 ++ ...-enum-value-transport-and-sys-enum-sync.md | 85 ++++++++++++ ...alue-transport-and-sys-enum-sync-design.md | 125 ++++++++++++++++++ frontend/src/api/ragDocuments.ts | 22 ++- frontend/src/pages/rag/RagDocumentsPage.vue | 23 ++-- .../rag/__tests__/RagDocumentsPage.spec.ts | 10 +- .../bruce/common/enums/CommonStatusEnum.java | 23 +++- .../bruce/common/enums/EnableStatusEnum.java | 23 +++- .../enums/PersistableSysEnumDefinition.java | 56 ++++++++ .../bruce/common/service/ISysEnumService.java | 2 + .../service/impl/SysEnumServiceImpl.java | 57 ++++++++ .../dto/request/RagDocumentParseRequest.java | 4 +- .../bruce/rag/enums/RagChunkStrategyEnum.java | 33 ++++- .../bruce/rag/enums/RagIndexStatusEnum.java | 24 +++- .../bruce/rag/enums/RagParseStatusEnum.java | 24 +++- .../impl/RagDocumentParseServiceImpl.java | 10 +- .../enumconfig/EnumDefinitionTests.java | 33 ++++- .../SysEnumComponentStructureTests.java | 8 ++ .../enumconfig/SysEnumDataInitTests.java | 58 +++----- .../SysEnumDefinitionSyncSupport.java | 90 +++++++++++++ .../SysEnumDefinitionSyncSupportTests.java | 98 ++++++++++++++ .../rag/RagDocumentParseServiceImplTests.java | 21 ++- 23 files changed, 793 insertions(+), 78 deletions(-) create mode 100644 docs/superpowers/plans/2026-05-24-enum-value-transport-and-sys-enum-sync.md create mode 100644 docs/superpowers/specs/2026-05-24-enum-value-transport-and-sys-enum-sync-design.md create mode 100644 src/main/java/com/bruce/common/enums/PersistableSysEnumDefinition.java create mode 100644 src/test/java/com/bruce/common/enumconfig/SysEnumDefinitionSyncSupport.java create mode 100644 src/test/java/com/bruce/common/enumconfig/SysEnumDefinitionSyncSupportTests.java diff --git a/AGENT.md b/AGENT.md index 53c6419..0654188 100644 --- a/AGENT.md +++ b/AGENT.md @@ -147,6 +147,35 @@ 6. OpenAPI 注解覆盖 所有 Controller、DTO 使用 `@Tag`、`@Operation`、`@Schema` 注解。 +## 4.1 代码注释约定 + +为方便后续多人协作和 Agent 接力阅读,新增以下约定: + +1. 新增或修改核心业务代码时,需要补充中文注释 + 注释优先覆盖类职责、关键方法、关键分支和重要参数含义,避免只写重复代码字面的无效注释。 + +2. 每次提交代码时,同步检查对应改动是否已经补齐中文注释 + 尤其是新引入的工厂、策略、服务编排、状态流转和复杂转换逻辑,默认需要有中文说明。 + +3. 注释以“帮助后来者快速理解设计意图”为目标 + 不追求注释数量,重点说明为什么这样做、边界是什么、哪些地方后续还会扩展。 + +## 4.2 结构化枚举约定 + +为保证前后端协议、代码定义和数据库配置一致,新增以下长期规则: + +1. 长期固定的结构化文本字段,统一采用枚举值传输 + 不再以字符串名称作为接口协议值,前后端统一传整型枚举值。 + +2. 这类枚举必须先定义为 Java 枚举类 + Java 枚举类作为单一事实来源,再派生前端常量和 `sys_enum` 配置。 + +3. 每次新增或修改结构化枚举时,必须同步纳入 `sys_enum` 初始化测试 + 通过统一测试入口按 `catalog + type` 先删后全量重建,避免数据库枚举配置漂移。 + +4. `catalog + type` 在枚举组层面必须唯一 + 一旦重复,会破坏枚举组重建语义,因此视为非法设计。 + ## 5. 数据与存储设计 ### 5.1 关系型数据库 diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 8a3a6f8..f935c1c 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -162,6 +162,19 @@ - **后端测试**:围绕结构约束的单元测试(Mapper/Service/Controller 继承体系、实体字段注解、方法签名验证)。 - **前端测试**:Vitest + @vue/test-utils,覆盖路由定义、布局组件、页面渲染、API 调用和 Long 类型解析。 +## 6.1 注释规范 + +- 新增或修改核心业务代码时,需补充中文注释,优先说明类职责、方法目的、关键判断和扩展边界。 +- 每次提交代码时,需要同步检查本次改动是否已经补齐对应中文注释,避免后续阅读只能靠反推代码语义。 +- 注释应聚焦设计意图和边界,不建议堆砌“变量赋值”“循环遍历”这类低价值说明。 + +## 6.2 结构化枚举规范 + +- 长期固定的结构化文本字段,统一采用整型枚举值作为前后端传输协议,不再直接传递字符串名称。 +- 后端 Java 枚举类是这类结构化枚举的单一事实来源,前端常量和 `sys_enum` 数据都基于它同步。 +- 新增或修改结构化枚举时,需要通过统一的枚举初始化测试按 `catalog + type` 先删后全量重建写入 `sys_enum`。 +- 不同枚举组之间的 `catalog + type` 必须唯一,否则会破坏枚举组重建的确定性。 + ## 7. 当前不足 - RAG 尚未进入"可检索链路",当前完成上传与解析,但未完成切片、向量化和召回。 diff --git a/docs/superpowers/plans/2026-05-24-enum-value-transport-and-sys-enum-sync.md b/docs/superpowers/plans/2026-05-24-enum-value-transport-and-sys-enum-sync.md new file mode 100644 index 0000000..c325220 --- /dev/null +++ b/docs/superpowers/plans/2026-05-24-enum-value-transport-and-sys-enum-sync.md @@ -0,0 +1,85 @@ +# Enum Value Transport And SysEnum Sync Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Unify long-lived structured text fields to use enum values for transport, and rebuild `sys_enum` from enum definitions through a delete-then-insert synchronization flow. + +**Architecture:** Add a shared enum definition contract in the backend, let existing enums implement it, and refactor enum initialization to rebuild each `catalog + type` group from code. Update the RAG parse request and frontend page to submit enum values instead of enum names. + +**Tech Stack:** Java 21, Spring Boot, MyBatis-Plus, JUnit 5, Vue 3, TypeScript, Vitest + +--- + +### Task 1: Lock Request Protocol With Failing Tests + +**Files:** +- Modify: `src/test/java/com/bruce/rag/RagDocumentParseServiceImplTests.java` +- Modify: `frontend/src/pages/rag/__tests__/RagDocumentsPage.spec.ts` + +- [ ] **Step 1: Write the failing test** +- [ ] **Step 2: Run targeted backend and frontend tests to verify they fail because the old string protocol is still in place** +- [ ] **Step 3: Update assertions to require integer enum values for `chunkStrategy`** +- [ ] **Step 4: Re-run targeted tests and keep them red until implementation exists** + +### Task 2: Implement Backend Enum Definition Contract + +**Files:** +- Create: `src/main/java/com/bruce/common/enums/PersistableSysEnumDefinition.java` +- Modify: `src/main/java/com/bruce/common/enums/EnableStatusEnum.java` +- Modify: `src/main/java/com/bruce/common/enums/CommonStatusEnum.java` +- Modify: `src/main/java/com/bruce/rag/enums/RagParseStatusEnum.java` +- Modify: `src/main/java/com/bruce/rag/enums/RagIndexStatusEnum.java` +- Modify: `src/main/java/com/bruce/rag/enums/RagChunkStrategyEnum.java` +- Modify: `src/test/java/com/bruce/common/enumconfig/EnumDefinitionTests.java` + +- [ ] **Step 1: Write or extend failing tests for enum metadata access and stable value lookup** +- [ ] **Step 2: Run backend enum tests to verify failure** +- [ ] **Step 3: Implement the shared enum definition contract and make existing enums implement it** +- [ ] **Step 4: Add `fromValue(Integer)` support where needed and rerun tests to green** + +### Task 3: Rebuild SysEnum Initialization Flow + +**Files:** +- Modify: `src/test/java/com/bruce/common/enumconfig/SysEnumDataInitTests.java` +- Create or Modify: `src/test/java/com/bruce/common/enumconfig/...` supporting tests as needed +- Modify: `src/main/java/com/bruce/common/service/ISysEnumService.java` +- Modify: `src/main/java/com/bruce/common/service/impl/SysEnumServiceImpl.java` + +- [ ] **Step 1: Write failing tests for duplicate `catalog + type`, duplicate value detection, and delete-then-insert rebuild behavior** +- [ ] **Step 2: Run targeted backend tests to verify failure** +- [ ] **Step 3: Add service support for removing and rebuilding a whole enum group** +- [ ] **Step 4: Refactor enum init test to register enum groups, validate uniqueness, delete old rows, and batch insert the new rows** +- [ ] **Step 5: Re-run targeted backend tests to green** + +### Task 4: Switch RAG Parse Request To Integer Enum Values + +**Files:** +- Modify: `src/main/java/com/bruce/rag/dto/request/RagDocumentParseRequest.java` +- Modify: `src/main/java/com/bruce/rag/parse/RagChunkCommand.java` +- Modify: `src/main/java/com/bruce/rag/service/impl/RagDocumentParseServiceImpl.java` +- Modify: `src/test/java/com/bruce/rag/RagDocumentParseServiceImplTests.java` + +- [ ] **Step 1: Confirm failing backend parse tests expect integer values** +- [ ] **Step 2: Change DTOs and validation logic to use enum values** +- [ ] **Step 3: Re-run targeted backend parse tests to green** + +### Task 5: Update Frontend To Use Enum Values + +**Files:** +- Modify: `frontend/src/api/ragDocuments.ts` +- Modify: `frontend/src/pages/rag/RagDocumentsPage.vue` +- Modify: `frontend/src/pages/rag/__tests__/RagDocumentsPage.spec.ts` + +- [ ] **Step 1: Confirm frontend parse request test is red for numeric enum values** +- [ ] **Step 2: Change API typing, page defaults, options, and comparisons to numeric enum values** +- [ ] **Step 3: Re-run targeted frontend tests to green** + +### Task 6: Record Project Convention + +**Files:** +- Modify: `AGENT.md` +- Modify: `docs/ARCHITECTURE.md` + +- [ ] **Step 1: Add the long-term convention that stable structured text uses enum values for transport** +- [ ] **Step 2: Add the rule that enum changes must be synchronized into `sys_enum` through the initialization test** +- [ ] **Step 3: Do a final focused verification run** diff --git a/docs/superpowers/specs/2026-05-24-enum-value-transport-and-sys-enum-sync-design.md b/docs/superpowers/specs/2026-05-24-enum-value-transport-and-sys-enum-sync-design.md new file mode 100644 index 0000000..4f96f04 --- /dev/null +++ b/docs/superpowers/specs/2026-05-24-enum-value-transport-and-sys-enum-sync-design.md @@ -0,0 +1,125 @@ +# 枚举值传输与 SysEnum 同步设计 + +## 1. 背景 + +当前系统中存在两类问题: + +1. 前后端针对长期固定的结构化字段,仍然传递字符串名称,例如 `chunkStrategy` 传 `"FIXED_LENGTH"`。 +2. `sys_enum` 初始化依赖测试类中逐条 `saveOrUpdate(...)`,新增或修改枚举时需要手工同步多处,且不会清理同一 `catalog/type` 下的历史脏数据。 + +这会导致协议冗余、前后端约束不统一,以及数据库枚举配置可能与代码定义漂移。 + +## 2. 目标 + +本次改造需要达成以下目标: + +- 长期固定的结构化文本字段,统一采用枚举值传输,不再传名称字符串。 +- 后端 Java 枚举成为结构化枚举的单一事实来源。 +- `sys_enum` 初始化机制支持按 `catalog + type` 分组,先删后全量重建。 +- 前端展示继续使用中文文案,但请求协议只传枚举值。 +- 新增或修改枚举后,只需改枚举类并运行统一测试,即可完成数据库同步。 + +## 3. 范围 + +本次纳入统一规范的枚举包括: + +- `EnableStatusEnum` +- `CommonStatusEnum` +- `RagParseStatusEnum` +- `RagIndexStatusEnum` +- `RagChunkStrategyEnum` + +本次同时把 `RagDocumentParseRequest.chunkStrategy` 从字符串协议改为数值协议。 + +## 4. 设计方案 + +### 4.1 后端枚举契约 + +新增一个统一的枚举定义接口,用于描述可同步到 `sys_enum` 的枚举项。接口提供: + +- `getCatalog()` +- `getType()` +- `getName()` +- `getValue()` +- `getStrvalue()` +- `getSort()` +- `getRemark()` + +上述五个现有枚举类统一实现该接口,使代码层直接具备落库所需信息。 + +### 4.2 枚举组唯一性 + +每一组枚举通过 `catalog + type` 唯一标识,例如: + +- `common / enable_status` +- `common / common_status` +- `rag / parse_status` +- `rag / index_status` +- `rag / chunk_strategy` + +系统要求不同枚举组之间 `catalog + type` 不能重复,否则无法安全执行“先删后全加”。 + +### 4.3 SysEnum 初始化机制 + +重写 `SysEnumDataInitTests` 的初始化方式: + +1. 收集所有需要同步的枚举组。 +2. 校验每个枚举组内部是否存在重复 `value`、重复 `sort`,以及不同组之间是否存在重复 `catalog + type`。 +3. 对每个枚举组先按 `catalog + type` 删除数据库中的旧枚举。 +4. 将当前代码定义的整组枚举全量写入 `sys_enum`。 + +这样可以保证数据库状态始终与当前代码一致,而不是增量叠加。 + +### 4.4 后端请求协议 + +`RagDocumentParseRequest.chunkStrategy` 改为 `Integer`,只接收枚举值。 + +同时为 `RagChunkStrategyEnum` 增加按值解析的方法,例如 `fromValue(Integer value)`,供服务层进行校验和转换。 + +`RagChunkCommand.chunkStrategy` 也同步改为 `Integer`,保持链路一致。 + +### 4.5 前端协议与展示 + +前端不再传字符串联合类型,而是改成数值枚举常量,例如: + +- `FIXED_LENGTH = 1` +- `DELIMITER = 5` + +页面中的单选项 `value` 使用数值,展示文案仍使用中文 `label`。提交请求时只传枚举值。 + +### 4.6 Agent 协作约定 + +在 `AGENT.md` 中新增长期规则: + +- 对长期固定的结构化文本字段,统一采用枚举值传输。 +- 枚举定义必须落在 Java 枚举类中。 +- 枚举变更需要同步纳入 `sys_enum` 初始化测试。 +- 每次新增或修改枚举后,需运行对应测试完成数据库同步。 + +## 5. 错误处理与边界 + +- 如果请求传入不存在的枚举值,后端直接抛出非法参数异常。 +- 如果某个枚举组定义了重复 `value` 或重复 `sort`,初始化测试直接失败。 +- 如果两个枚举组使用了相同的 `catalog + type`,初始化测试直接失败。 +- 如果前端传入旧字符串协议,后端不做兼容,统一按新协议处理。 + +## 6. 测试策略 + +后端: + +- 扩展 `EnumDefinitionTests`,验证关键枚举值稳定。 +- 为 `RagDocumentParseServiceImplTests` 增加数值协议断言和非法值校验。 +- 为新的 `sys_enum` 全量初始化逻辑增加单元测试,验证唯一性校验和重建行为。 + +前端: + +- 更新 `RagDocumentsPage.spec.ts`,断言解析请求提交数值枚举值。 +- 验证页面仍然展示中文切片名称。 + +## 7. 预期结果 + +改造完成后: + +- 前后端结构化字段协议更紧凑、更稳定。 +- 枚举定义、前端传值和数据库配置三者一致。 +- 新增枚举时有固定流程,不再依赖手工增量补数据。 diff --git a/frontend/src/api/ragDocuments.ts b/frontend/src/api/ragDocuments.ts index e18f4c6..be8f87b 100644 --- a/frontend/src/api/ragDocuments.ts +++ b/frontend/src/api/ragDocuments.ts @@ -47,13 +47,21 @@ export interface RagDocumentBatchUploadRequest { remark?: string; } -export type RagChunkStrategy = - | 'FIXED_LENGTH' - | 'PARAGRAPH' - | 'HEADING' - | 'TABLE_ROW' - | 'DELIMITER' - | 'SEMANTIC'; +/** + * RAG 切片策略枚举值。 + *

+ * 前后端统一传递枚举值,不再传递字符串名称。 + */ +export const RAG_CHUNK_STRATEGY = { + FIXED_LENGTH: 1, + PARAGRAPH: 2, + HEADING: 3, + TABLE_ROW: 4, + DELIMITER: 5, + SEMANTIC: 6, +} as const; + +export type RagChunkStrategy = (typeof RAG_CHUNK_STRATEGY)[keyof typeof RAG_CHUNK_STRATEGY]; export interface RagDocumentParseRequest { documentIds: string[]; diff --git a/frontend/src/pages/rag/RagDocumentsPage.vue b/frontend/src/pages/rag/RagDocumentsPage.vue index a722b9d..5056914 100644 --- a/frontend/src/pages/rag/RagDocumentsPage.vue +++ b/frontend/src/pages/rag/RagDocumentsPage.vue @@ -10,6 +10,7 @@ import { parseRagDocuments, queryRagDocuments, saveRagDocument, + RAG_CHUNK_STRATEGY, type RagChunkStrategy, type RagDocument, } from '@/api/ragDocuments'; @@ -49,19 +50,19 @@ const editForm = reactive({ const parseForm = reactive({ documentIds: [] as string[], - chunkStrategy: 'FIXED_LENGTH' as RagChunkStrategy, + chunkStrategy: RAG_CHUNK_STRATEGY.FIXED_LENGTH as RagChunkStrategy, chunkSize: 800, chunkOverlap: 120, delimiter: '。', }); const chunkStrategyOptions: Array<{ label: string; value: RagChunkStrategy; description: string }> = [ - { label: '固定长度切片', value: 'FIXED_LENGTH', description: '按指定长度和重叠长度切分通用文本' }, - { label: '按段落切片', value: 'PARAGRAPH', description: '按空行、自然段落边界切分' }, - { label: '按标题层级切片', value: 'HEADING', description: '按标题和章节层级组织内容' }, - { label: '按表格行切片', value: 'TABLE_ROW', description: '适合 Excel 表格和结构化明细数据' }, - { label: '按分隔符切片', value: 'DELIMITER', description: '按句号、换行符或自定义分隔符切分' }, - { label: '语义切片', value: 'SEMANTIC', description: '后续结合语义边界或模型能力切分' }, + { label: '固定长度切片', value: RAG_CHUNK_STRATEGY.FIXED_LENGTH, description: '按指定长度和重叠长度切分通用文本' }, + { label: '按段落切片', value: RAG_CHUNK_STRATEGY.PARAGRAPH, description: '按空行、自然段落边界切分' }, + { label: '按标题层级切片', value: RAG_CHUNK_STRATEGY.HEADING, description: '按标题和章节层级组织内容' }, + { label: '按表格行切片', value: RAG_CHUNK_STRATEGY.TABLE_ROW, description: '适合 Excel 表格和结构化明细数据' }, + { label: '按分隔符切片', value: RAG_CHUNK_STRATEGY.DELIMITER, description: '按句号、换行符或自定义分隔符切分' }, + { label: '语义切片', value: RAG_CHUNK_STRATEGY.SEMANTIC, description: '后续结合语义边界或模型能力切分' }, ]; const filteredRows = computed(() => { @@ -138,7 +139,7 @@ function openParseDialog(rows: RagDocument[]) { return; } parseForm.documentIds = ids; - parseForm.chunkStrategy = 'FIXED_LENGTH'; + parseForm.chunkStrategy = RAG_CHUNK_STRATEGY.FIXED_LENGTH; parseForm.chunkSize = 800; parseForm.chunkOverlap = 120; parseForm.delimiter = '。'; @@ -467,13 +468,13 @@ onMounted(() => { - + - + - + diff --git a/frontend/src/pages/rag/__tests__/RagDocumentsPage.spec.ts b/frontend/src/pages/rag/__tests__/RagDocumentsPage.spec.ts index 9d5fc17..6f11c81 100644 --- a/frontend/src/pages/rag/__tests__/RagDocumentsPage.spec.ts +++ b/frontend/src/pages/rag/__tests__/RagDocumentsPage.spec.ts @@ -29,6 +29,14 @@ vi.mock('@/api/ragStores', () => ({ vi.mock('@/api/ragDocuments', () => ({ SOURCE_TYPE_RAG: 'RAG', + RAG_CHUNK_STRATEGY: { + FIXED_LENGTH: 1, + PARAGRAPH: 2, + HEADING: 3, + TABLE_ROW: 4, + DELIMITER: 5, + SEMANTIC: 6, + }, queryRagDocuments: vi.fn((query?: { storeId?: string }) => { const rows = [ { @@ -178,7 +186,7 @@ describe('RagDocumentsPage', () => { expect(parseRagDocuments).toHaveBeenCalledWith({ documentIds: ['11'], - chunkStrategy: 'FIXED_LENGTH', + chunkStrategy: 1, chunkSize: 800, chunkOverlap: 120, delimiter: '。', diff --git a/src/main/java/com/bruce/common/enums/CommonStatusEnum.java b/src/main/java/com/bruce/common/enums/CommonStatusEnum.java index 0883a0f..bae993f 100644 --- a/src/main/java/com/bruce/common/enums/CommonStatusEnum.java +++ b/src/main/java/com/bruce/common/enums/CommonStatusEnum.java @@ -5,7 +5,7 @@ import lombok.Getter; @Getter @AllArgsConstructor -public enum CommonStatusEnum { +public enum CommonStatusEnum implements PersistableSysEnumDefinition { DISABLED(0, "禁用"), ENABLED(1, "启用"), @@ -14,7 +14,28 @@ public enum CommonStatusEnum { COMPLETED(4, "已完成"), FAILED(5, "失败"); + private static final String CATALOG = "common"; + + private static final String TYPE = "common_status"; + + private static final String REMARK = "通用状态"; + private final Integer value; private final String label; + + @Override + public String getCatalog() { + return CATALOG; + } + + @Override + public String getType() { + return TYPE; + } + + @Override + public String getRemark() { + return REMARK; + } } diff --git a/src/main/java/com/bruce/common/enums/EnableStatusEnum.java b/src/main/java/com/bruce/common/enums/EnableStatusEnum.java index 9fc0be5..98f6725 100644 --- a/src/main/java/com/bruce/common/enums/EnableStatusEnum.java +++ b/src/main/java/com/bruce/common/enums/EnableStatusEnum.java @@ -5,12 +5,33 @@ import lombok.Getter; @Getter @AllArgsConstructor -public enum EnableStatusEnum { +public enum EnableStatusEnum implements PersistableSysEnumDefinition { DISABLED(0, "禁用"), ENABLED(1, "启用"); + private static final String CATALOG = "common"; + + private static final String TYPE = "enable_status"; + + private static final String REMARK = "通用启用状态"; + private final Integer value; private final String label; + + @Override + public String getCatalog() { + return CATALOG; + } + + @Override + public String getType() { + return TYPE; + } + + @Override + public String getRemark() { + return REMARK; + } } diff --git a/src/main/java/com/bruce/common/enums/PersistableSysEnumDefinition.java b/src/main/java/com/bruce/common/enums/PersistableSysEnumDefinition.java new file mode 100644 index 0000000..2aa1502 --- /dev/null +++ b/src/main/java/com/bruce/common/enums/PersistableSysEnumDefinition.java @@ -0,0 +1,56 @@ +package com.bruce.common.enums; + +/** + * 可同步到 sys_enum 的枚举定义契约。 + *

+ * 长期固定的结构化文本字段统一通过该契约描述, + * 便于前后端传值、数据库初始化和后续协作保持同一事实来源。 + */ +public interface PersistableSysEnumDefinition { + + /** + * 枚举所属模块目录,例如 common、rag。 + */ + String getCatalog(); + + /** + * 枚举所属类型,同一 catalog/type 视为同一个枚举组。 + */ + String getType(); + + /** + * 落库到 sys_enum.name 的展示名称。 + */ + default String getName() { + return getLabel(); + } + + /** + * 枚举整型值,前后端协议统一传该值。 + */ + Integer getValue(); + + /** + * 可选的字符串值,当前大多数业务枚举不使用。 + */ + default String getStrvalue() { + return null; + } + + /** + * 排序值,默认与枚举值保持一致。 + */ + default Integer getSort() { + return getValue(); + } + + /** + * sys_enum.remark 中的说明文本。 + */ + String getRemark(); + + /** + * 枚举显示文案,通常与 name 保持一致。 + */ + String getLabel(); +} diff --git a/src/main/java/com/bruce/common/service/ISysEnumService.java b/src/main/java/com/bruce/common/service/ISysEnumService.java index 7bff5b8..ca760b2 100644 --- a/src/main/java/com/bruce/common/service/ISysEnumService.java +++ b/src/main/java/com/bruce/common/service/ISysEnumService.java @@ -25,4 +25,6 @@ public interface ISysEnumService extends IService { boolean saveOrUpdate(SysEnumSaveRequest request); boolean batchSave(SysEnumBatchSaveRequest request); + + boolean replaceByCatalogAndType(String catalog, String type, List items); } diff --git a/src/main/java/com/bruce/common/service/impl/SysEnumServiceImpl.java b/src/main/java/com/bruce/common/service/impl/SysEnumServiceImpl.java index 525ff31..b7759fa 100644 --- a/src/main/java/com/bruce/common/service/impl/SysEnumServiceImpl.java +++ b/src/main/java/com/bruce/common/service/impl/SysEnumServiceImpl.java @@ -10,6 +10,7 @@ import com.bruce.common.dto.response.SysEnumResponse; import com.bruce.common.mapper.SysEnumMapper; import com.bruce.common.service.ISysEnumService; import lombok.extern.slf4j.Slf4j; +import org.springframework.transaction.annotation.Transactional; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; @@ -137,6 +138,24 @@ public class SysEnumServiceImpl extends ServiceImpl impl return result; } + @Override + @Transactional(rollbackFor = Exception.class) + public boolean replaceByCatalogAndType(String catalog, String type, List items) { + log.info("SysEnumServiceImpl.replaceByCatalogAndType start, catalog={}, type={}, itemCount={}", + catalog, type, items == null ? 0 : items.size()); + validateReplaceByCatalogAndTypeRequest(catalog, type, items); + + boolean removed = lambdaUpdate() + .eq(SysEnum::getCatalog, catalog) + .eq(SysEnum::getType, type) + .remove(); + boolean saved = saveBatch(items); + boolean result = removed || saved; + log.info("SysEnumServiceImpl.replaceByCatalogAndType success, catalog={}, type={}, removed={}, saved={}, result={}", + catalog, type, removed, saved, result); + return result; + } + public void validateBatchSaveRequest(SysEnumBatchSaveRequest request, List existingEnums) { log.info("SysEnumServiceImpl.validateBatchSaveRequest start"); if (request == null) { @@ -183,6 +202,44 @@ public class SysEnumServiceImpl extends ServiceImpl impl request.getCatalog(), request.getType(), requestValues.size(), existingValues.size()); } + private void validateReplaceByCatalogAndTypeRequest(String catalog, String type, List items) { + if (!StringUtils.hasText(catalog)) { + throw new IllegalArgumentException("模块目录不能为空"); + } + if (!StringUtils.hasText(type)) { + throw new IllegalArgumentException("枚举类型不能为空"); + } + if (items == null || items.isEmpty()) { + throw new IllegalArgumentException("枚举项不能为空"); + } + + Set values = new HashSet<>(); + Set sorts = new HashSet<>(); + for (SysEnum item : items) { + if (item == null) { + throw new IllegalArgumentException("枚举项不能为空"); + } + if (!catalog.equals(item.getCatalog()) || !type.equals(item.getType())) { + throw new IllegalArgumentException("替换的枚举项 catalog/type 必须与目标分组一致"); + } + if (!StringUtils.hasText(item.getName())) { + throw new IllegalArgumentException("枚举名称不能为空"); + } + if (item.getValue() == null) { + throw new IllegalArgumentException("枚举整型值不能为空"); + } + if (!values.add(item.getValue())) { + throw new IllegalArgumentException("枚举值重复: " + item.getValue()); + } + if (item.getSort() == null) { + throw new IllegalArgumentException("枚举排序不能为空"); + } + if (!sorts.add(item.getSort())) { + throw new IllegalArgumentException("枚举排序重复: " + item.getSort()); + } + } + } + private List toResponses(List enums) { return enums.stream() .map(SysEnumResponse::fromEntity) diff --git a/src/main/java/com/bruce/rag/dto/request/RagDocumentParseRequest.java b/src/main/java/com/bruce/rag/dto/request/RagDocumentParseRequest.java index 8fc93c0..e29a0c8 100644 --- a/src/main/java/com/bruce/rag/dto/request/RagDocumentParseRequest.java +++ b/src/main/java/com/bruce/rag/dto/request/RagDocumentParseRequest.java @@ -12,8 +12,8 @@ public class RagDocumentParseRequest { @Schema(description = "文档ID列表") private List documentIds; - @Schema(description = "切片方式") - private String chunkStrategy; + @Schema(description = "切片方式枚举值") + private Integer chunkStrategy; @Schema(description = "切片长度") private Integer chunkSize; diff --git a/src/main/java/com/bruce/rag/enums/RagChunkStrategyEnum.java b/src/main/java/com/bruce/rag/enums/RagChunkStrategyEnum.java index f514b06..21c378f 100644 --- a/src/main/java/com/bruce/rag/enums/RagChunkStrategyEnum.java +++ b/src/main/java/com/bruce/rag/enums/RagChunkStrategyEnum.java @@ -1,11 +1,14 @@ package com.bruce.rag.enums; +import com.bruce.common.enums.PersistableSysEnumDefinition; import lombok.AllArgsConstructor; import lombok.Getter; +import java.util.Arrays; + @Getter @AllArgsConstructor -public enum RagChunkStrategyEnum { +public enum RagChunkStrategyEnum implements PersistableSysEnumDefinition { FIXED_LENGTH(1, "固定长度切片"), PARAGRAPH(2, "按段落切片"), @@ -14,7 +17,35 @@ public enum RagChunkStrategyEnum { DELIMITER(5, "按分隔符切片"), SEMANTIC(6, "语义切片"); + private static final String CATALOG = "rag"; + + private static final String TYPE = "chunk_strategy"; + + private static final String REMARK = "RAG文档切片方式"; + private final Integer value; private final String label; + + public static RagChunkStrategyEnum fromValue(Integer value) { + return Arrays.stream(values()) + .filter(item -> item.getValue().equals(value)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("不支持的切片方式: " + value)); + } + + @Override + public String getCatalog() { + return CATALOG; + } + + @Override + public String getType() { + return TYPE; + } + + @Override + public String getRemark() { + return REMARK; + } } diff --git a/src/main/java/com/bruce/rag/enums/RagIndexStatusEnum.java b/src/main/java/com/bruce/rag/enums/RagIndexStatusEnum.java index e7337c2..980c289 100644 --- a/src/main/java/com/bruce/rag/enums/RagIndexStatusEnum.java +++ b/src/main/java/com/bruce/rag/enums/RagIndexStatusEnum.java @@ -1,18 +1,40 @@ package com.bruce.rag.enums; +import com.bruce.common.enums.PersistableSysEnumDefinition; import lombok.AllArgsConstructor; import lombok.Getter; @Getter @AllArgsConstructor -public enum RagIndexStatusEnum { +public enum RagIndexStatusEnum implements PersistableSysEnumDefinition { PENDING(1, "待索引"), INDEXING(2, "索引中"), INDEXED(3, "已索引"), FAILED(4, "索引失败"); + private static final String CATALOG = "rag"; + + private static final String TYPE = "index_status"; + + private static final String REMARK = "RAG文档索引状态"; + private final Integer value; private final String label; + + @Override + public String getCatalog() { + return CATALOG; + } + + @Override + public String getType() { + return TYPE; + } + + @Override + public String getRemark() { + return REMARK; + } } diff --git a/src/main/java/com/bruce/rag/enums/RagParseStatusEnum.java b/src/main/java/com/bruce/rag/enums/RagParseStatusEnum.java index f15485c..15ee95c 100644 --- a/src/main/java/com/bruce/rag/enums/RagParseStatusEnum.java +++ b/src/main/java/com/bruce/rag/enums/RagParseStatusEnum.java @@ -1,18 +1,40 @@ package com.bruce.rag.enums; +import com.bruce.common.enums.PersistableSysEnumDefinition; import lombok.AllArgsConstructor; import lombok.Getter; @Getter @AllArgsConstructor -public enum RagParseStatusEnum { +public enum RagParseStatusEnum implements PersistableSysEnumDefinition { UPLOADED(1, "已上传"), PARSING(2, "解析中"), PARSED(3, "已解析"), FAILED(4, "解析失败"); + private static final String CATALOG = "rag"; + + private static final String TYPE = "parse_status"; + + private static final String REMARK = "RAG文档解析状态"; + private final Integer value; private final String label; + + @Override + public String getCatalog() { + return CATALOG; + } + + @Override + public String getType() { + return TYPE; + } + + @Override + public String getRemark() { + return REMARK; + } } diff --git a/src/main/java/com/bruce/rag/service/impl/RagDocumentParseServiceImpl.java b/src/main/java/com/bruce/rag/service/impl/RagDocumentParseServiceImpl.java index df60746..a588f9e 100644 --- a/src/main/java/com/bruce/rag/service/impl/RagDocumentParseServiceImpl.java +++ b/src/main/java/com/bruce/rag/service/impl/RagDocumentParseServiceImpl.java @@ -22,10 +22,7 @@ import org.springframework.util.StringUtils; import java.nio.file.Files; import java.nio.file.Path; -import java.util.Arrays; import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; @Slf4j @Service @@ -95,12 +92,7 @@ public class RagDocumentParseServiceImpl implements IRagDocumentParseService { if (request.getDocumentIds() == null || request.getDocumentIds().isEmpty()) { throw new IllegalArgumentException("文档ID列表不能为空"); } - Set strategies = Arrays.stream(RagChunkStrategyEnum.values()) - .map(Enum::name) - .collect(Collectors.toSet()); - if (request.getChunkStrategy() == null || !strategies.contains(request.getChunkStrategy())) { - throw new IllegalArgumentException("不支持的切片方式: " + request.getChunkStrategy()); - } + RagChunkStrategyEnum.fromValue(request.getChunkStrategy()); } private DocumentParseContext buildParseContext(RagDocument document, SysAttachment attachment) { diff --git a/src/test/java/com/bruce/common/enumconfig/EnumDefinitionTests.java b/src/test/java/com/bruce/common/enumconfig/EnumDefinitionTests.java index 6c119c3..e824d30 100644 --- a/src/test/java/com/bruce/common/enumconfig/EnumDefinitionTests.java +++ b/src/test/java/com/bruce/common/enumconfig/EnumDefinitionTests.java @@ -2,12 +2,14 @@ package com.bruce.common.enumconfig; import com.bruce.common.enums.CommonStatusEnum; import com.bruce.common.enums.EnableStatusEnum; -import com.bruce.rag.enums.RagIndexStatusEnum; +import com.bruce.common.enums.PersistableSysEnumDefinition; import com.bruce.rag.enums.RagChunkStrategyEnum; +import com.bruce.rag.enums.RagIndexStatusEnum; import com.bruce.rag.enums.RagParseStatusEnum; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; class EnumDefinitionTests { @@ -46,4 +48,33 @@ class EnumDefinitionTests { assertEquals("按分隔符切片", RagChunkStrategyEnum.DELIMITER.getLabel()); assertEquals("语义切片", RagChunkStrategyEnum.SEMANTIC.getLabel()); } + + @Test + void enumsShouldExposeStableSysEnumMetadata() { + PersistableSysEnumDefinition chunkStrategy = RagChunkStrategyEnum.DELIMITER; + PersistableSysEnumDefinition parseStatus = RagParseStatusEnum.PARSED; + PersistableSysEnumDefinition enableStatus = EnableStatusEnum.ENABLED; + + assertEquals("rag", chunkStrategy.getCatalog()); + assertEquals("chunk_strategy", chunkStrategy.getType()); + assertEquals("按分隔符切片", chunkStrategy.getName()); + assertEquals(5, chunkStrategy.getValue()); + assertEquals(5, chunkStrategy.getSort()); + assertEquals("RAG文档切片方式", chunkStrategy.getRemark()); + + assertEquals("rag", parseStatus.getCatalog()); + assertEquals("parse_status", parseStatus.getType()); + assertEquals("已解析", parseStatus.getName()); + + assertEquals("common", enableStatus.getCatalog()); + assertEquals("enable_status", enableStatus.getType()); + assertEquals("启用", enableStatus.getName()); + } + + @Test + void ragChunkStrategyShouldResolveByIntegerValue() { + assertEquals(RagChunkStrategyEnum.FIXED_LENGTH, RagChunkStrategyEnum.fromValue(1)); + assertEquals(RagChunkStrategyEnum.DELIMITER, RagChunkStrategyEnum.fromValue(5)); + assertThrows(IllegalArgumentException.class, () -> RagChunkStrategyEnum.fromValue(999)); + } } diff --git a/src/test/java/com/bruce/common/enumconfig/SysEnumComponentStructureTests.java b/src/test/java/com/bruce/common/enumconfig/SysEnumComponentStructureTests.java index 02e5a2c..ff55856 100644 --- a/src/test/java/com/bruce/common/enumconfig/SysEnumComponentStructureTests.java +++ b/src/test/java/com/bruce/common/enumconfig/SysEnumComponentStructureTests.java @@ -104,4 +104,12 @@ class SysEnumComponentStructureTests { assertNotNull(initMethod); } + + @Test + void sysEnumServiceShouldExposeReplaceByCatalogAndType() throws NoSuchMethodException { + Method replaceMethod = ISysEnumService.class.getMethod("replaceByCatalogAndType", String.class, String.class, List.class); + + assertNotNull(replaceMethod); + assertEquals(boolean.class, replaceMethod.getReturnType()); + } } diff --git a/src/test/java/com/bruce/common/enumconfig/SysEnumDataInitTests.java b/src/test/java/com/bruce/common/enumconfig/SysEnumDataInitTests.java index 72a0cf9..be8a3ee 100644 --- a/src/test/java/com/bruce/common/enumconfig/SysEnumDataInitTests.java +++ b/src/test/java/com/bruce/common/enumconfig/SysEnumDataInitTests.java @@ -2,6 +2,7 @@ package com.bruce.common.enumconfig; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.bruce.common.domain.entity.SysEnum; +import com.bruce.common.enums.PersistableSysEnumDefinition; import com.bruce.common.enums.CommonStatusEnum; import com.bruce.common.enums.EnableStatusEnum; import com.bruce.common.service.ISysEnumService; @@ -13,6 +14,8 @@ import org.junit.jupiter.api.condition.EnabledIfSystemProperty; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import java.util.List; + @SpringBootTest @EnabledIfSystemProperty(named = "runEnumInit", matches = "true") class SysEnumDataInitTests { @@ -22,49 +25,22 @@ class SysEnumDataInitTests { @Test public void initDefaultEnums() { - saveOrUpdate("common", "enable_status", EnableStatusEnum.DISABLED.getLabel(), EnableStatusEnum.DISABLED.getValue(), 0, "通用启用状态"); - saveOrUpdate("common", "enable_status", EnableStatusEnum.ENABLED.getLabel(), EnableStatusEnum.ENABLED.getValue(), 1, "通用启用状态"); + List groups = List.of( + buildGroup(EnableStatusEnum.values()), + buildGroup(CommonStatusEnum.values()), + buildGroup(RagParseStatusEnum.values()), + buildGroup(RagIndexStatusEnum.values()), + buildGroup(RagChunkStrategyEnum.values()) + ); + SysEnumDefinitionSyncSupport.validateUniqueGroupKeys(groups); - saveOrUpdate("common", "common_status", CommonStatusEnum.DISABLED.getLabel(), CommonStatusEnum.DISABLED.getValue(), 0, "通用状态"); - saveOrUpdate("common", "common_status", CommonStatusEnum.ENABLED.getLabel(), CommonStatusEnum.ENABLED.getValue(), 1, "通用状态"); - saveOrUpdate("common", "common_status", CommonStatusEnum.DRAFT.getLabel(), CommonStatusEnum.DRAFT.getValue(), 2, "通用状态"); - saveOrUpdate("common", "common_status", CommonStatusEnum.PROCESSING.getLabel(), CommonStatusEnum.PROCESSING.getValue(), 3, "通用状态"); - saveOrUpdate("common", "common_status", CommonStatusEnum.COMPLETED.getLabel(), CommonStatusEnum.COMPLETED.getValue(), 4, "通用状态"); - saveOrUpdate("common", "common_status", CommonStatusEnum.FAILED.getLabel(), CommonStatusEnum.FAILED.getValue(), 5, "通用状态"); - - saveOrUpdate("rag", "parse_status", RagParseStatusEnum.UPLOADED.getLabel(), RagParseStatusEnum.UPLOADED.getValue(), 1, "RAG文档解析状态"); - saveOrUpdate("rag", "parse_status", RagParseStatusEnum.PARSING.getLabel(), RagParseStatusEnum.PARSING.getValue(), 2, "RAG文档解析状态"); - saveOrUpdate("rag", "parse_status", RagParseStatusEnum.PARSED.getLabel(), RagParseStatusEnum.PARSED.getValue(), 3, "RAG文档解析状态"); - saveOrUpdate("rag", "parse_status", RagParseStatusEnum.FAILED.getLabel(), RagParseStatusEnum.FAILED.getValue(), 4, "RAG文档解析状态"); - - saveOrUpdate("rag", "index_status", RagIndexStatusEnum.PENDING.getLabel(), RagIndexStatusEnum.PENDING.getValue(), 1, "RAG文档索引状态"); - saveOrUpdate("rag", "index_status", RagIndexStatusEnum.INDEXING.getLabel(), RagIndexStatusEnum.INDEXING.getValue(), 2, "RAG文档索引状态"); - saveOrUpdate("rag", "index_status", RagIndexStatusEnum.INDEXED.getLabel(), RagIndexStatusEnum.INDEXED.getValue(), 3, "RAG文档索引状态"); - saveOrUpdate("rag", "index_status", RagIndexStatusEnum.FAILED.getLabel(), RagIndexStatusEnum.FAILED.getValue(), 4, "RAG文档索引状态"); - - saveOrUpdate("rag", "chunk_strategy", RagChunkStrategyEnum.FIXED_LENGTH.getLabel(), RagChunkStrategyEnum.FIXED_LENGTH.getValue(), 1, "RAG文档切片方式"); - saveOrUpdate("rag", "chunk_strategy", RagChunkStrategyEnum.PARAGRAPH.getLabel(), RagChunkStrategyEnum.PARAGRAPH.getValue(), 2, "RAG文档切片方式"); - saveOrUpdate("rag", "chunk_strategy", RagChunkStrategyEnum.HEADING.getLabel(), RagChunkStrategyEnum.HEADING.getValue(), 3, "RAG文档切片方式"); - saveOrUpdate("rag", "chunk_strategy", RagChunkStrategyEnum.TABLE_ROW.getLabel(), RagChunkStrategyEnum.TABLE_ROW.getValue(), 4, "RAG文档切片方式"); - saveOrUpdate("rag", "chunk_strategy", RagChunkStrategyEnum.DELIMITER.getLabel(), RagChunkStrategyEnum.DELIMITER.getValue(), 5, "RAG文档切片方式"); - saveOrUpdate("rag", "chunk_strategy", RagChunkStrategyEnum.SEMANTIC.getLabel(), RagChunkStrategyEnum.SEMANTIC.getValue(), 6, "RAG文档切片方式"); + for (SysEnumDefinitionSyncSupport.EnumGroup group : groups) { + List rows = SysEnumDefinitionSyncSupport.toEntities(group); + sysEnumService.replaceByCatalogAndType(group.catalog(), group.type(), rows); + } } - private void saveOrUpdate(String catalog, String type, String name, Integer value, Integer sort, String remark) { - SysEnum sysEnum = sysEnumService.getOne(new LambdaQueryWrapper() - .eq(SysEnum::getCatalog, catalog) - .eq(SysEnum::getType, type) - .eq(SysEnum::getName, name)); - if (sysEnum == null) { - sysEnum = new SysEnum(); - } - sysEnum.setCatalog(catalog); - sysEnum.setType(type); - sysEnum.setName(name); - sysEnum.setValue(value); - sysEnum.setStrvalue(null); - sysEnum.setSort(sort); - sysEnum.setRemark(remark); - sysEnumService.saveOrUpdate(sysEnum); + private SysEnumDefinitionSyncSupport.EnumGroup buildGroup(PersistableSysEnumDefinition[] definitions) { + return SysEnumDefinitionSyncSupport.groupOf(List.of(definitions)); } } diff --git a/src/test/java/com/bruce/common/enumconfig/SysEnumDefinitionSyncSupport.java b/src/test/java/com/bruce/common/enumconfig/SysEnumDefinitionSyncSupport.java new file mode 100644 index 0000000..2dad2c9 --- /dev/null +++ b/src/test/java/com/bruce/common/enumconfig/SysEnumDefinitionSyncSupport.java @@ -0,0 +1,90 @@ +package com.bruce.common.enumconfig; + +import com.bruce.common.domain.entity.SysEnum; +import com.bruce.common.enums.PersistableSysEnumDefinition; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * sys_enum 初始化测试的辅助工具。 + *

+ * 该类只服务于测试入口,用于把代码里的枚举定义组装成可落库的数据结构, + * 并在真正写库前完成组级唯一性校验。 + */ +final class SysEnumDefinitionSyncSupport { + + private SysEnumDefinitionSyncSupport() { + } + + static EnumGroup groupOf(List definitions) { + if (definitions == null || definitions.isEmpty()) { + throw new IllegalArgumentException("枚举定义不能为空"); + } + PersistableSysEnumDefinition first = definitions.getFirst(); + validateGroupMembers(first, definitions); + validateUniqueValuesAndSorts(first, definitions); + return new EnumGroup(first.getCatalog(), first.getType(), List.copyOf(definitions)); + } + + static void validateUniqueGroupKeys(List groups) { + Set keys = new HashSet<>(); + for (EnumGroup group : groups) { + String key = group.catalog() + "/" + group.type(); + if (!keys.add(key)) { + throw new IllegalArgumentException("存在重复的枚举分组: " + key); + } + } + } + + static List toEntities(EnumGroup group) { + return group.definitions().stream() + .map(item -> { + SysEnum sysEnum = new SysEnum(); + sysEnum.setCatalog(group.catalog()); + sysEnum.setType(group.type()); + sysEnum.setName(item.getName()); + sysEnum.setValue(item.getValue()); + sysEnum.setStrvalue(item.getStrvalue()); + sysEnum.setSort(item.getSort()); + sysEnum.setRemark(item.getRemark()); + return sysEnum; + }) + .toList(); + } + + private static void validateGroupMembers( + PersistableSysEnumDefinition first, + List definitions + ) { + for (PersistableSysEnumDefinition item : definitions) { + if (!first.getCatalog().equals(item.getCatalog()) || !first.getType().equals(item.getType())) { + throw new IllegalArgumentException("同一枚举组中的 catalog/type 必须一致"); + } + } + } + + private static void validateUniqueValuesAndSorts( + PersistableSysEnumDefinition first, + List definitions + ) { + Set values = new HashSet<>(); + Set sorts = new HashSet<>(); + for (PersistableSysEnumDefinition item : definitions) { + if (!values.add(item.getValue())) { + throw new IllegalArgumentException("枚举值重复: " + first.getCatalog() + "/" + first.getType() + "/" + item.getValue()); + } + if (!sorts.add(item.getSort())) { + throw new IllegalArgumentException("枚举排序重复: " + first.getCatalog() + "/" + first.getType() + "/" + item.getSort()); + } + } + } + + record EnumGroup( + String catalog, + String type, + List definitions + ) { + } +} diff --git a/src/test/java/com/bruce/common/enumconfig/SysEnumDefinitionSyncSupportTests.java b/src/test/java/com/bruce/common/enumconfig/SysEnumDefinitionSyncSupportTests.java new file mode 100644 index 0000000..9da09ec --- /dev/null +++ b/src/test/java/com/bruce/common/enumconfig/SysEnumDefinitionSyncSupportTests.java @@ -0,0 +1,98 @@ +package com.bruce.common.enumconfig; + +import com.bruce.common.domain.entity.SysEnum; +import com.bruce.common.enums.EnableStatusEnum; +import com.bruce.common.enums.PersistableSysEnumDefinition; +import com.bruce.rag.enums.RagChunkStrategyEnum; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class SysEnumDefinitionSyncSupportTests { + + @Test + void shouldRejectDuplicateCatalogAndTypeAcrossGroups() { + SysEnumDefinitionSyncSupport.EnumGroup left = SysEnumDefinitionSyncSupport.groupOf( + List.of(EnableStatusEnum.DISABLED, EnableStatusEnum.ENABLED) + ); + SysEnumDefinitionSyncSupport.EnumGroup right = SysEnumDefinitionSyncSupport.groupOf( + List.of( + new FakeEnumDefinition("common", "enable_status", "草稿", 2, 2, "通用启用状态") + ) + ); + + assertThrows( + IllegalArgumentException.class, + () -> SysEnumDefinitionSyncSupport.validateUniqueGroupKeys(List.of(left, right)) + ); + } + + @Test + void shouldBuildSysEnumRowsFromDefinitions() { + SysEnumDefinitionSyncSupport.EnumGroup group = SysEnumDefinitionSyncSupport.groupOf( + List.of(RagChunkStrategyEnum.FIXED_LENGTH, RagChunkStrategyEnum.DELIMITER) + ); + + List rows = SysEnumDefinitionSyncSupport.toEntities(group); + + assertEquals(2, rows.size()); + assertEquals("rag", rows.get(0).getCatalog()); + assertEquals("chunk_strategy", rows.get(0).getType()); + assertEquals("固定长度切片", rows.get(0).getName()); + assertEquals(1, rows.get(0).getValue()); + assertEquals(5, rows.get(1).getValue()); + } + + private record FakeEnumDefinition( + String catalog, + String type, + String name, + Integer value, + Integer sort, + String remark + ) implements PersistableSysEnumDefinition { + + @Override + public String getCatalog() { + return catalog; + } + + @Override + public String getType() { + return type; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getLabel() { + return name; + } + + @Override + public Integer getValue() { + return value; + } + + @Override + public String getStrvalue() { + return null; + } + + @Override + public Integer getSort() { + return sort; + } + + @Override + public String getRemark() { + return remark; + } + } +} diff --git a/src/test/java/com/bruce/rag/RagDocumentParseServiceImplTests.java b/src/test/java/com/bruce/rag/RagDocumentParseServiceImplTests.java index f660c3f..50ee560 100644 --- a/src/test/java/com/bruce/rag/RagDocumentParseServiceImplTests.java +++ b/src/test/java/com/bruce/rag/RagDocumentParseServiceImplTests.java @@ -10,6 +10,7 @@ import com.bruce.common.service.ISysAttachmentService; import com.bruce.rag.dto.request.RagDocumentParseRequest; import com.bruce.rag.dto.response.RagDocumentParseResponse; import com.bruce.rag.entity.RagDocument; +import com.bruce.rag.enums.RagChunkStrategyEnum; import com.bruce.rag.enums.RagParseStatusEnum; import com.bruce.rag.service.IRagDocumentService; import com.bruce.rag.service.impl.RagDocumentParseServiceImpl; @@ -26,6 +27,7 @@ import java.util.List; import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.verify; @@ -121,7 +123,7 @@ class RagDocumentParseServiceImplTests { ); RagDocumentParseRequest request = new RagDocumentParseRequest(); request.setDocumentIds(List.of(1002L)); - request.setChunkStrategy("DELIMITER"); + request.setChunkStrategy(RagChunkStrategyEnum.DELIMITER.getValue()); request.setDelimiter("。"); when(ragDocumentService.getById(1002L)).thenReturn(document); @@ -135,6 +137,23 @@ class RagDocumentParseServiceImplTests { assertEquals(RagParseStatusEnum.PARSED.name(), responses.getFirst().getParseStatus()); } + @Test + void parseShouldRejectUnknownChunkStrategyValue() { + AttachmentProperties attachmentProperties = new AttachmentProperties(); + attachmentProperties.setBasePath(tempDir.toString()); + RagDocumentParseServiceImpl service = new RagDocumentParseServiceImpl( + ragDocumentService, + sysAttachmentService, + attachmentProperties, + new DocumentParserFactory(List.of(new FixedDocumentParser("batch profiles"))) + ); + RagDocumentParseRequest request = new RagDocumentParseRequest(); + request.setDocumentIds(List.of(1002L)); + request.setChunkStrategy(999); + + assertThrows(IllegalArgumentException.class, () -> service.parse(request)); + } + private static class FixedDocumentParser implements DocumentParser { private final String text;