feat(phase1): 完成 JPA 实体类和 Repository 层实现

- 创建 3 个 JPA 实体类:DiagnosisRecord、CaseLibrary、ApiDocument
- 创建 3 个 Repository 接口,实现基础 CRUD 和自定义查询方法
- 实现 DiagnosisRecordRepository 单元测试(6 个测试全部通过)
- 修复 Hibernate schema 验证问题(枚举类型使用 VARCHAR)
- 更新 application.yml,添加完整的数据库和 Redis 配置
- 更新 OpenSpec tasks.md,标记已完成任务(12/33)

测试结果:
- DiagnosisRecordRepositoryTest: 6/6 通过
- 编译成功,无错误

Progress: 12/33 tasks completed
This commit is contained in:
zhuyongxin
2026-06-23 14:30:35 +08:00
parent 60be51f4a5
commit 1de1e98ef8
11 changed files with 1151 additions and 9 deletions
@@ -0,0 +1,95 @@
package org.example.domain.entity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import com.superbiz.agent.domain.enums.FaultCategory;
import java.time.LocalDateTime;
/**
* API 文档元数据实体
* 对应表: api_document
*/
@Entity
@Table(name = "api_document", indexes = {
@Index(name = "idx_doc_id", columnList = "doc_id"),
@Index(name = "idx_fault_source", columnList = "fault_source"),
@Index(name = "idx_status", columnList = "status"),
@Index(name = "idx_created_at", columnList = "created_at")
}, uniqueConstraints = {
@UniqueConstraint(name = "uk_file_hash", columnNames = "file_hash")
})
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ApiDocument {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "doc_id", unique = true, nullable = false, length = 64)
private String docId;
// 文档分类
@Enumerated(EnumType.STRING)
@Column(name = "fault_category", length = 32, columnDefinition = "VARCHAR(32)")
private FaultCategory faultCategory = FaultCategory.EXTERNAL_API;
@Column(name = "fault_source", length = 128)
private String faultSource;
@Column(name = "api_name", length = 128)
private String apiName;
@Column(name = "version", length = 32)
private String version = "v1.0";
// 文件信息
@Column(name = "file_name", nullable = false, length = 256)
private String fileName;
@Column(name = "file_path", length = 512)
private String filePath;
@Column(name = "file_hash", length = 64)
private String fileHash;
@Column(name = "file_size")
private Long fileSize;
// 索引状态
@Column(name = "status", length = 16)
private String status = "PENDING";
@Column(name = "chunk_count")
private Integer chunkCount = 0;
@Column(name = "error_message", columnDefinition = "TEXT")
private String errorMessage;
// 时间字段
@Column(name = "indexed_at")
private LocalDateTime indexedAt;
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
}
@@ -0,0 +1,95 @@
package org.example.domain.entity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import com.superbiz.agent.domain.enums.FaultCategory;
import com.superbiz.agent.domain.enums.SourceType;
import java.time.LocalDateTime;
/**
* 案例库实体
* 对应表: case_library
*/
@Entity
@Table(name = "case_library", indexes = {
@Index(name = "idx_fault_category", columnList = "fault_category"),
@Index(name = "idx_error_code", columnList = "error_code"),
@Index(name = "idx_fault_source", columnList = "fault_source"),
@Index(name = "idx_diagnosis_id", columnList = "diagnosis_id"),
@Index(name = "idx_reference_count", columnList = "reference_count"),
@Index(name = "idx_created_at", columnList = "created_at")
})
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CaseLibrary {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "case_id", unique = true, nullable = false, length = 64)
private String caseId;
// 来源关联
@Column(name = "diagnosis_id", length = 64)
private String diagnosisId;
@Enumerated(EnumType.STRING)
@Column(name = "source_type", length = 16, columnDefinition = "VARCHAR(16)")
private SourceType sourceType = SourceType.AUTO;
// 案例分类
@Enumerated(EnumType.STRING)
@Column(name = "fault_category", length = 32, columnDefinition = "VARCHAR(32)")
private FaultCategory faultCategory;
@Column(name = "fault_source", length = 128)
private String faultSource;
@Column(name = "fault_target", length = 256)
private String faultTarget;
@Column(name = "error_code", length = 64)
private String errorCode;
// 案例内容
@Column(name = "title", nullable = false, length = 256)
private String title;
@Column(name = "root_cause", nullable = false, columnDefinition = "TEXT")
private String rootCause;
@Column(name = "solution", nullable = false, columnDefinition = "TEXT")
private String solution;
// 简单统计
@Column(name = "reference_count")
private Integer referenceCount = 0;
// 元数据
@Column(name = "created_by", length = 64)
private String createdBy;
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
}
@@ -0,0 +1,128 @@
package org.example.domain.entity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import com.superbiz.agent.domain.enums.DiagnosisStatus;
import com.superbiz.agent.domain.enums.FaultCategory;
import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.type.SqlTypes;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
/**
* 诊断记录实体
* 对应表: diagnosis_record
*/
@Entity
@Table(name = "diagnosis_record", indexes = {
@Index(name = "idx_business_id", columnList = "business_id"),
@Index(name = "idx_trace_id", columnList = "trace_id"),
@Index(name = "idx_session_id", columnList = "session_id"),
@Index(name = "idx_fault_category", columnList = "fault_category"),
@Index(name = "idx_error_code", columnList = "error_code"),
@Index(name = "idx_created_at", columnList = "created_at"),
@Index(name = "idx_status", columnList = "status")
})
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DiagnosisRecord {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "diagnosis_id", unique = true, nullable = false, length = 64)
private String diagnosisId;
// 关联信息
@Column(name = "session_id", length = 64)
private String sessionId;
@Column(name = "business_id", length = 128)
private String businessId;
@Column(name = "trace_id", length = 64)
private String traceId;
// 故障分类
@Enumerated(EnumType.STRING)
@Column(name = "fault_category", length = 32, columnDefinition = "VARCHAR(32)")
private FaultCategory faultCategory;
@Column(name = "fault_source", length = 128)
private String faultSource;
@Column(name = "fault_target", length = 256)
private String faultTarget;
// 错误信息
@Column(name = "error_code", length = 64)
private String errorCode;
@Column(name = "error_message", columnDefinition = "TEXT")
private String errorMessage;
@Column(name = "stack_trace", columnDefinition = "TEXT")
private String stackTrace;
// 诊断结果
@Column(name = "problem_type", length = 32)
private String problemType;
@Column(name = "root_cause", columnDefinition = "TEXT")
private String rootCause;
@Column(name = "solution", columnDefinition = "TEXT")
private String solution;
@Column(name = "report_markdown", columnDefinition = "TEXT")
private String reportMarkdown;
// 评估指标
@Enumerated(EnumType.STRING)
@Column(name = "status", length = 16, columnDefinition = "VARCHAR(16)")
private DiagnosisStatus status = DiagnosisStatus.PENDING;
@Column(name = "confidence")
private Integer confidence;
@Column(name = "duration")
private Integer duration;
// 用户反馈
@Column(name = "feedback", length = 16)
private String feedback;
// 调试字段 - JSON 类型
@JdbcTypeCode(SqlTypes.JSON)
@Column(name = "tool_calls", columnDefinition = "JSON")
private List<Map<String, Object>> toolCalls;
// 元数据
@Column(name = "created_by", length = 64)
private String createdBy;
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
}
@@ -0,0 +1,63 @@
package org.example.repository;
import com.superbiz.agent.domain.enums.FaultCategory;
import org.example.domain.entity.ApiDocument;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
/**
* API 文档 Repository
*/
@Repository
public interface ApiDocumentRepository extends JpaRepository<ApiDocument, Long> {
/**
* 根据文档ID查询
*/
Optional<ApiDocument> findByDocId(String docId);
/**
* 根据文件hash查询(去重)
*/
Optional<ApiDocument> findByFileHash(String fileHash);
/**
* 根据故障源查询
*/
List<ApiDocument> findByFaultSource(String faultSource);
/**
* 根据故障类别和故障源查询
*/
List<ApiDocument> findByFaultCategoryAndFaultSource(FaultCategory category, String faultSource);
/**
* 根据状态查询
*/
List<ApiDocument> findByStatus(String status);
/**
* 根据状态查询(分页)
*/
Page<ApiDocument> findByStatus(String status, Pageable pageable);
/**
* 查询已索引的文档
*/
List<ApiDocument> findByStatusAndChunkCountGreaterThan(String status, Integer chunkCount);
/**
* 模糊搜索文件名
*/
Page<ApiDocument> findByFileNameContaining(String keyword, Pageable pageable);
/**
* 根据故障源模糊搜索(分页)
*/
Page<ApiDocument> findByFaultSourceContaining(String keyword, Pageable pageable);
}
@@ -0,0 +1,70 @@
package org.example.repository;
import com.superbiz.agent.domain.enums.FaultCategory;
import com.superbiz.agent.domain.enums.SourceType;
import org.example.domain.entity.CaseLibrary;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
/**
* 案例库 Repository
*/
@Repository
public interface CaseLibraryRepository extends JpaRepository<CaseLibrary, Long> {
/**
* 根据案例ID查询
*/
Optional<CaseLibrary> findByCaseId(String caseId);
/**
* 根据诊断ID查询
*/
Optional<CaseLibrary> findByDiagnosisId(String diagnosisId);
/**
* 根据故障类别查询
*/
List<CaseLibrary> findByFaultCategory(FaultCategory category);
/**
* 根据错误码查询
*/
List<CaseLibrary> findByErrorCode(String errorCode);
/**
* 根据故障类别和错误码查询
*/
List<CaseLibrary> findByFaultCategoryAndErrorCode(FaultCategory category, String errorCode);
/**
* 根据故障类别、故障源和错误码查询(精确匹配)
*/
List<CaseLibrary> findByFaultCategoryAndFaultSourceAndErrorCode(
FaultCategory category, String faultSource, String errorCode);
/**
* 根据来源类型查询(分页)
*/
Page<CaseLibrary> findBySourceType(SourceType sourceType, Pageable pageable);
/**
* 查询热门案例(按引用次数排序)
*/
List<CaseLibrary> findTop10ByOrderByReferenceCountDesc();
/**
* 根据故障类别查询热门案例
*/
List<CaseLibrary> findTop5ByFaultCategoryOrderByReferenceCountDesc(FaultCategory category);
/**
* 模糊搜索标题
*/
Page<CaseLibrary> findByTitleContaining(String keyword, Pageable pageable);
}
@@ -0,0 +1,73 @@
package org.example.repository;
import com.superbiz.agent.domain.enums.DiagnosisStatus;
import com.superbiz.agent.domain.enums.FaultCategory;
import org.example.domain.entity.DiagnosisRecord;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
/**
* 诊断记录 Repository
*/
@Repository
public interface DiagnosisRecordRepository extends JpaRepository<DiagnosisRecord, Long> {
/**
* 根据诊断ID查询
*/
Optional<DiagnosisRecord> findByDiagnosisId(String diagnosisId);
/**
* 根据业务ID查询
*/
Optional<DiagnosisRecord> findByBusinessId(String businessId);
/**
* 根据链路追踪ID查询
*/
Optional<DiagnosisRecord> findByTraceId(String traceId);
/**
* 根据会话ID查询所有记录
*/
List<DiagnosisRecord> findBySessionId(String sessionId);
/**
* 根据故障类别和错误码查询
*/
List<DiagnosisRecord> findByFaultCategoryAndErrorCode(FaultCategory category, String errorCode);
/**
* 根据故障类别、故障源和错误码查询
*/
List<DiagnosisRecord> findByFaultCategoryAndFaultSourceAndErrorCode(
FaultCategory category, String faultSource, String errorCode);
/**
* 根据状态查询
*/
List<DiagnosisRecord> findByStatus(DiagnosisStatus status);
/**
* 根据时间范围查询(分页)
*/
Page<DiagnosisRecord> findByCreatedAtBetween(
LocalDateTime start, LocalDateTime end, Pageable pageable);
/**
* 根据故障类别和时间范围查询(分页)
*/
Page<DiagnosisRecord> findByFaultCategoryAndCreatedAtBetween(
FaultCategory category, LocalDateTime start, LocalDateTime end, Pageable pageable);
/**
* 查询有用反馈的高置信度记录(用于生成案例)
*/
List<DiagnosisRecord> findByFeedbackAndConfidenceGreaterThanEqual(String feedback, Integer confidence);
}
+2 -2
View File
@@ -70,8 +70,8 @@ spring:
data:
redis:
host: 119.29.78.52
port: 6379
password: ''
port: 33308
password: '!Fucker123..'
database: 0
timeout: 3000
lettuce:
@@ -0,0 +1,174 @@
package org.example.repository;
import com.superbiz.agent.domain.enums.DiagnosisStatus;
import com.superbiz.agent.domain.enums.FaultCategory;
import org.example.domain.entity.DiagnosisRecord;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.test.context.TestPropertySource;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.*;
/**
* DiagnosisRecordRepository 单元测试
*/
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@TestPropertySource(properties = {
"spring.flyway.enabled=true",
"spring.jpa.hibernate.ddl-auto=validate",
"spring.jpa.show-sql=true"
})
class DiagnosisRecordRepositoryTest {
@Autowired
private DiagnosisRecordRepository repository;
@Test
void testSaveAndFindById() {
// 创建测试数据
DiagnosisRecord record = DiagnosisRecord.builder()
.diagnosisId(UUID.randomUUID().toString())
.sessionId("session-001")
.businessId("order-12345")
.traceId("trace-abc123")
.faultCategory(FaultCategory.EXTERNAL_API)
.faultSource("广东")
.faultTarget("http://api.example.com/query")
.errorCode("40003")
.errorMessage("接口超时")
.status(DiagnosisStatus.SUCCESS)
.confidence(85)
.duration(1500)
.build();
// 保存
DiagnosisRecord saved = repository.save(record);
assertNotNull(saved.getId());
assertNotNull(saved.getCreatedAt());
System.out.println("✓ 保存成功,ID: " + saved.getId());
// 查询
Optional<DiagnosisRecord> found = repository.findById(saved.getId());
assertTrue(found.isPresent());
assertEquals("order-12345", found.get().getBusinessId());
System.out.println("✓ 根据 ID 查询成功");
}
@Test
void testFindByDiagnosisId() {
String diagnosisId = UUID.randomUUID().toString();
DiagnosisRecord record = DiagnosisRecord.builder()
.diagnosisId(diagnosisId)
.businessId("order-test-001")
.faultCategory(FaultCategory.DATABASE)
.status(DiagnosisStatus.PENDING)
.build();
repository.save(record);
Optional<DiagnosisRecord> found = repository.findByDiagnosisId(diagnosisId);
assertTrue(found.isPresent());
assertEquals(diagnosisId, found.get().getDiagnosisId());
System.out.println("✓ 根据 diagnosisId 查询成功");
}
@Test
void testFindByFaultCategoryAndErrorCode() {
// 创建测试数据
DiagnosisRecord record1 = DiagnosisRecord.builder()
.diagnosisId(UUID.randomUUID().toString())
.faultCategory(FaultCategory.EXTERNAL_API)
.errorCode("40003")
.status(DiagnosisStatus.SUCCESS)
.build();
DiagnosisRecord record2 = DiagnosisRecord.builder()
.diagnosisId(UUID.randomUUID().toString())
.faultCategory(FaultCategory.EXTERNAL_API)
.errorCode("40003")
.status(DiagnosisStatus.FAILED)
.build();
repository.save(record1);
repository.save(record2);
// 查询
List<DiagnosisRecord> results = repository.findByFaultCategoryAndErrorCode(
FaultCategory.EXTERNAL_API, "40003");
assertFalse(results.isEmpty());
assertTrue(results.size() >= 2);
System.out.println("✓ 根据故障类别和错误码查询成功,找到 " + results.size() + " 条记录");
}
@Test
void testFindByStatus() {
DiagnosisRecord record = DiagnosisRecord.builder()
.diagnosisId(UUID.randomUUID().toString())
.status(DiagnosisStatus.RUNNING)
.faultCategory(FaultCategory.CACHE)
.build();
repository.save(record);
List<DiagnosisRecord> results = repository.findByStatus(DiagnosisStatus.RUNNING);
assertFalse(results.isEmpty());
System.out.println("✓ 根据状态查询成功,找到 " + results.size() + " 条 RUNNING 记录");
}
@Test
void testUpdateRecord() {
// 创建并保存
DiagnosisRecord record = DiagnosisRecord.builder()
.diagnosisId(UUID.randomUUID().toString())
.status(DiagnosisStatus.PENDING)
.confidence(0)
.build();
DiagnosisRecord saved = repository.save(record);
Long id = saved.getId();
// 更新
saved.setStatus(DiagnosisStatus.SUCCESS);
saved.setConfidence(90);
saved.setRootCause("接口超时导致");
saved.setSolution("增加重试机制");
repository.save(saved);
// 验证更新
Optional<DiagnosisRecord> updated = repository.findById(id);
assertTrue(updated.isPresent());
assertEquals(DiagnosisStatus.SUCCESS, updated.get().getStatus());
assertEquals(90, updated.get().getConfidence());
assertNotNull(updated.get().getUpdatedAt());
System.out.println("✓ 更新记录成功");
}
@Test
void testDeleteRecord() {
DiagnosisRecord record = DiagnosisRecord.builder()
.diagnosisId(UUID.randomUUID().toString())
.status(DiagnosisStatus.PENDING)
.build();
DiagnosisRecord saved = repository.save(record);
Long id = saved.getId();
// 删除
repository.deleteById(id);
// 验证删除
Optional<DiagnosisRecord> deleted = repository.findById(id);
assertFalse(deleted.isPresent());
System.out.println("✓ 删除记录成功");
}
}