document_service.go 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. package service
  2. import (
  3. "context"
  4. "errors"
  5. "log"
  6. "strconv"
  7. "github.com/2930134478/AI-CS/backend/models"
  8. "github.com/2930134478/AI-CS/backend/repository"
  9. "github.com/2930134478/AI-CS/backend/service/rag"
  10. )
  11. // DocumentService 文档管理服务
  12. type DocumentService struct {
  13. docRepo *repository.DocumentRepository
  14. kbRepo *repository.KnowledgeBaseRepository
  15. documentEmbeddingService *rag.DocumentEmbeddingService
  16. retrievalService *rag.RetrievalService
  17. }
  18. // NewDocumentService 创建文档服务实例
  19. func NewDocumentService(
  20. docRepo *repository.DocumentRepository,
  21. kbRepo *repository.KnowledgeBaseRepository,
  22. documentEmbeddingService *rag.DocumentEmbeddingService,
  23. retrievalService *rag.RetrievalService,
  24. ) *DocumentService {
  25. return &DocumentService{
  26. docRepo: docRepo,
  27. kbRepo: kbRepo,
  28. documentEmbeddingService: documentEmbeddingService,
  29. retrievalService: retrievalService,
  30. }
  31. }
  32. // CreateDocument 创建文档
  33. func (s *DocumentService) CreateDocument(input CreateDocumentInput) (*DocumentSummary, error) {
  34. // 验证知识库是否存在
  35. _, err := s.kbRepo.GetByID(input.KnowledgeBaseID)
  36. if err != nil {
  37. return nil, errors.New("知识库不存在")
  38. }
  39. if input.Title == "" {
  40. return nil, errors.New("文档标题不能为空")
  41. }
  42. if input.Content == "" {
  43. return nil, errors.New("文档内容不能为空")
  44. }
  45. docType := input.Type
  46. if docType == "" {
  47. docType = "document"
  48. }
  49. status := input.Status
  50. if status == "" {
  51. status = "draft"
  52. }
  53. doc := &models.Document{
  54. KnowledgeBaseID: input.KnowledgeBaseID,
  55. Title: input.Title,
  56. Content: input.Content,
  57. Summary: input.Summary,
  58. Type: docType,
  59. Status: status,
  60. EmbeddingStatus: "pending",
  61. }
  62. if err := s.docRepo.Create(doc); err != nil {
  63. return nil, err
  64. }
  65. // 新建文档后自动异步向量化,状态见文档列表的「向量状态」;日志关键字 [文档向量化]
  66. go s.embedDocumentAsync(context.Background(), doc.ID, doc.KnowledgeBaseID, doc.Content)
  67. return s.toSummary(doc), nil
  68. }
  69. // embedDocumentAsync 异步向量化文档(新建/更新文档后触发)
  70. func (s *DocumentService) embedDocumentAsync(ctx context.Context, docID uint, kbID uint, content string) {
  71. defer func() {
  72. if r := recover(); r != nil {
  73. log.Printf("[文档向量化] panic doc_id=%d: %v", docID, r)
  74. _ = s.docRepo.UpdateEmbeddingStatus(docID, "failed")
  75. }
  76. }()
  77. log.Printf("[文档向量化] 开始 doc_id=%d kb_id=%d content_len=%d", docID, kbID, len([]rune(content)))
  78. if err := s.docRepo.UpdateEmbeddingStatus(docID, "processing"); err != nil {
  79. log.Printf("[文档向量化] doc_id=%d 更新 processing 失败: %v", docID, err)
  80. return
  81. }
  82. err := s.documentEmbeddingService.EmbedDocument(ctx, docID, kbID, content)
  83. if err != nil {
  84. log.Printf("[文档向量化] doc_id=%d 失败: %v", docID, err)
  85. _ = s.docRepo.UpdateEmbeddingStatus(docID, "failed")
  86. return
  87. }
  88. if err := s.docRepo.UpdateEmbeddingStatus(docID, "completed"); err != nil {
  89. log.Printf("[文档向量化] doc_id=%d 更新 completed 失败: %v", docID, err)
  90. return
  91. }
  92. log.Printf("[文档向量化] 完成 doc_id=%d", docID)
  93. }
  94. // GetDocument 获取文档详情
  95. func (s *DocumentService) GetDocument(id uint) (*DocumentSummary, error) {
  96. doc, err := s.docRepo.GetByID(id)
  97. if err != nil {
  98. return nil, err
  99. }
  100. return s.toSummary(doc), nil
  101. }
  102. // ListDocuments 获取文档列表
  103. func (s *DocumentService) ListDocuments(knowledgeBaseID uint, page, pageSize int, keyword string, status string) (*DocumentListResult, error) {
  104. if page < 1 {
  105. page = 1
  106. }
  107. if pageSize < 1 {
  108. pageSize = 20
  109. }
  110. docs, total, err := s.docRepo.GetByKnowledgeBaseID(knowledgeBaseID, page, pageSize, keyword, status)
  111. if err != nil {
  112. return nil, err
  113. }
  114. summaries := make([]DocumentSummary, len(docs))
  115. for i, doc := range docs {
  116. summaries[i] = *s.toSummary(&doc)
  117. }
  118. totalPage := int((total + int64(pageSize) - 1) / int64(pageSize))
  119. return &DocumentListResult{
  120. Documents: summaries,
  121. Total: total,
  122. Page: page,
  123. PageSize: pageSize,
  124. TotalPage: totalPage,
  125. }, nil
  126. }
  127. // UpdateDocument 更新文档
  128. func (s *DocumentService) UpdateDocument(id uint, input UpdateDocumentInput) (*DocumentSummary, error) {
  129. doc, err := s.docRepo.GetByID(id)
  130. if err != nil {
  131. return nil, err
  132. }
  133. needReembed := false
  134. if input.Title != nil {
  135. doc.Title = *input.Title
  136. }
  137. if input.Content != nil {
  138. doc.Content = *input.Content
  139. needReembed = true // 内容变化需要重新向量化
  140. }
  141. if input.Summary != nil {
  142. doc.Summary = *input.Summary
  143. }
  144. if input.Type != nil {
  145. doc.Type = *input.Type
  146. }
  147. if input.Status != nil {
  148. doc.Status = *input.Status
  149. }
  150. if err := s.docRepo.Update(doc); err != nil {
  151. return nil, err
  152. }
  153. // 如果内容变化,重新向量化
  154. if needReembed {
  155. doc.EmbeddingStatus = "pending"
  156. s.docRepo.Update(doc)
  157. go s.embedDocumentAsync(context.Background(), doc.ID, doc.KnowledgeBaseID, doc.Content)
  158. }
  159. return s.toSummary(doc), nil
  160. }
  161. // DeleteDocument 删除文档
  162. func (s *DocumentService) DeleteDocument(id uint) error {
  163. _, err := s.docRepo.GetByID(id)
  164. if err != nil {
  165. return err
  166. }
  167. // 删除向量
  168. if err := s.documentEmbeddingService.DeleteDocumentEmbedding(context.Background(), id); err != nil {
  169. // 记录错误但不阻止删除
  170. }
  171. // 删除文档
  172. return s.docRepo.Delete(id)
  173. }
  174. // UpdateDocumentStatus 更新文档状态
  175. func (s *DocumentService) UpdateDocumentStatus(id uint, status string) error {
  176. return s.docRepo.UpdateStatus(id, status)
  177. }
  178. // PublishDocument 发布文档
  179. func (s *DocumentService) PublishDocument(id uint) error {
  180. return s.UpdateDocumentStatus(id, "published")
  181. }
  182. // UnpublishDocument 取消发布文档
  183. func (s *DocumentService) UnpublishDocument(id uint) error {
  184. return s.UpdateDocumentStatus(id, "draft")
  185. }
  186. // SearchDocuments 向量检索文档
  187. func (s *DocumentService) SearchDocuments(query string, topK int, knowledgeBaseID *uint) ([]DocumentSummary, error) {
  188. results, err := s.retrievalService.Retrieve(context.Background(), query, topK, knowledgeBaseID)
  189. if err != nil {
  190. return nil, err
  191. }
  192. // 获取文档 ID
  193. docIDs := make([]uint, 0, len(results))
  194. for _, result := range results {
  195. // 将 document_id 字符串转换为 uint
  196. docID, err := strconv.ParseUint(result.DocumentID, 10, 64)
  197. if err == nil {
  198. docIDs = append(docIDs, uint(docID))
  199. }
  200. }
  201. // 查询文档详情
  202. if len(docIDs) > 0 {
  203. docs, err := s.docRepo.GetByIDs(docIDs)
  204. if err == nil {
  205. // 保持检索结果的顺序
  206. docMap := make(map[uint]*models.Document)
  207. for i := range docs {
  208. docMap[docs[i].ID] = &docs[i]
  209. }
  210. summaries := make([]DocumentSummary, 0, len(docIDs))
  211. for _, docID := range docIDs {
  212. if doc, ok := docMap[docID]; ok {
  213. summaries = append(summaries, *s.toSummary(doc))
  214. }
  215. }
  216. return summaries, nil
  217. }
  218. }
  219. return []DocumentSummary{}, nil
  220. }
  221. // toSummary 转换为摘要
  222. func (s *DocumentService) toSummary(doc *models.Document) *DocumentSummary {
  223. return &DocumentSummary{
  224. ID: doc.ID,
  225. KnowledgeBaseID: doc.KnowledgeBaseID,
  226. Title: doc.Title,
  227. Content: doc.Content,
  228. Summary: doc.Summary,
  229. Type: doc.Type,
  230. Status: doc.Status,
  231. EmbeddingStatus: doc.EmbeddingStatus,
  232. CreatedAt: doc.CreatedAt,
  233. UpdatedAt: doc.UpdatedAt,
  234. }
  235. }