refactor: 简化FAQ导入结果获取逻辑

将导入结果字段合并到FAQImportProgress结构体中,当状态为completed时自动填充。删除了独立的GetLastFAQImportResult接口和相关方法,客户端改为通过进度接口获取已完成任务的结果,并使用localStorage记录最后完成的任务ID。
This commit is contained in:
wizardchen
2026-01-14 17:09:19 +08:00
parent f6aab2bc86
commit 84a4ec62de
8 changed files with 85 additions and 146 deletions
+8 -66
View File
@@ -358,6 +358,7 @@ type FAQFailedEntry struct {
}
// FAQImportProgress represents the progress of an async FAQ import task.
// When Status is "completed", the result fields (SkippedCount, ImportMode, ImportedAt, DisplayStatus, ProcessingTime) are populated.
type FAQImportProgress struct {
TaskID string `json:"task_id"`
KBID string `json:"kb_id"`
@@ -368,6 +369,7 @@ type FAQImportProgress struct {
Processed int `json:"processed"`
SuccessCount int `json:"success_count"`
FailedCount int `json:"failed_count"`
SkippedCount int `json:"skipped_count,omitempty"`
FailedEntries []FAQFailedEntry `json:"failed_entries,omitempty"`
FailedEntriesURL string `json:"failed_entries_url,omitempty"` // CSV download URL when too many failures
Message string `json:"message"`
@@ -375,6 +377,12 @@ type FAQImportProgress struct {
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
DryRun bool `json:"dry_run,omitempty"` // Whether this is a dry run validation
// Result fields (populated when Status == "completed")
ImportMode string `json:"import_mode,omitempty"`
ImportedAt time.Time `json:"imported_at,omitempty"`
DisplayStatus string `json:"display_status,omitempty"`
ProcessingTime int64 `json:"processing_time,omitempty"`
}
// FAQImportProgressResponse wraps the FAQ import progress response.
@@ -400,69 +408,3 @@ func (c *Client) GetFAQImportProgress(ctx context.Context, taskID string) (*FAQI
}
return response.Data, nil
}
// FAQImportResult stores statistics of the last FAQ import.
// It matches server response from GET /knowledge-bases/{id}/faq/import/last-result.
type FAQImportResult struct {
TotalEntries int `json:"total_entries"`
SuccessCount int `json:"success_count"`
FailedCount int `json:"failed_count"`
SkippedCount int `json:"skipped_count"`
ImportMode string `json:"import_mode"`
ImportedAt time.Time `json:"imported_at"`
TaskID string `json:"task_id"`
FailedEntriesURL string `json:"failed_entries_url,omitempty"`
DisplayStatus string `json:"display_status"`
ProcessingTime int64 `json:"processing_time"`
}
type faqImportResultResponse struct {
Success bool `json:"success"`
Data *FAQImportResult `json:"data"`
Message string `json:"message,omitempty"`
Code string `json:"code,omitempty"`
}
type updateLastFAQImportResultDisplayStatusRequest struct {
DisplayStatus string `json:"display_status"`
}
// GetLastFAQImportResult retrieves the last FAQ import result summary for a knowledge base.
func (c *Client) GetLastFAQImportResult(ctx context.Context, knowledgeBaseID string) (*FAQImportResult, error) {
path := fmt.Sprintf("/api/v1/knowledge-bases/%s/faq/import/last-result", knowledgeBaseID)
resp, err := c.doRequest(ctx, http.MethodGet, path, nil, nil)
if err != nil {
return nil, err
}
var response faqImportResultResponse
if err := parseResponse(resp, &response); err != nil {
return nil, err
}
return response.Data, nil
}
// UpdateLastFAQImportResultDisplayStatus updates the display status (open/close) of the last FAQ import result.
func (c *Client) UpdateLastFAQImportResultDisplayStatus(ctx context.Context, knowledgeBaseID string, displayStatus string) error {
path := fmt.Sprintf("/api/v1/knowledge-bases/%s/faq/import/last-result/display", knowledgeBaseID)
resp, err := c.doRequest(ctx, http.MethodPut, path, &updateLastFAQImportResultDisplayStatusRequest{DisplayStatus: displayStatus}, nil)
if err != nil {
return err
}
var response faqSimpleResponse
return parseResponse(resp, &response)
}
// GetFAQImportResult is kept for backward compatibility; it now calls GetLastFAQImportResult.
func (c *Client) GetFAQImportResult(ctx context.Context, knowledgeBaseID string) (*FAQImportResult, error) {
return c.GetLastFAQImportResult(ctx, knowledgeBaseID)
}
// UpdateFAQImportResultDisplayStatus is kept for backward compatibility; it now calls UpdateLastFAQImportResultDisplayStatus.
func (c *Client) UpdateFAQImportResultDisplayStatus(ctx context.Context, knowledgeBaseID string, displayStatus string) error {
return c.UpdateLastFAQImportResultDisplayStatus(ctx, knowledgeBaseID, displayStatus)
}
-4
View File
@@ -232,10 +232,6 @@ export function getFAQImportProgress(taskId: string) {
return get(`/api/v1/faq/import/progress/${taskId}`);
}
export function getFAQImportResult(knowledgeBaseId: string) {
return get(`/api/v1/knowledge-bases/${knowledgeBaseId}/faq/import/last-result`);
}
export function updateFAQImportResultDisplayStatus(knowledgeBaseId: string, displayStatus: 'open' | 'close') {
return put(`/api/v1/knowledge-bases/${knowledgeBaseId}/faq/import/last-result/display`, {
display_status: displayStatus
@@ -1316,7 +1316,6 @@ import {
getKnowledgeBaseById,
listKnowledgeBases,
getFAQImportProgress,
getFAQImportResult,
updateFAQImportResultDisplayStatus,
} from '@/api/knowledge-base'
import * as XLSX from 'xlsx'
@@ -2451,6 +2450,10 @@ const startPolling = (taskId: string) => {
if (status === 'success' || status === 'failed') {
stopPolling()
if (status === 'success') {
// 保存已完成的 taskId 用于后续加载结果
if (importState.taskId) {
saveLastCompletedTaskId(importState.taskId)
}
MessagePlugin.success(t('knowledgeEditor.faqImport.importSuccess'))
// 清除筛选条件,确保用户能看到所有新导入的数据
selectedTagId.value = ''
@@ -2588,13 +2591,61 @@ const restoreImportTask = async () => {
}
}
// localStorage key for last completed task
const getLastCompletedTaskKey = () => {
return `faq_import_last_completed_${props.kbId}`
}
const saveLastCompletedTaskId = (taskId: string) => {
if (!props.kbId) return
try {
localStorage.setItem(getLastCompletedTaskKey(), taskId)
} catch (error) {
console.error('Failed to save last completed taskId:', error)
}
}
const getLastCompletedTaskId = (): string | null => {
if (!props.kbId) return null
try {
return localStorage.getItem(getLastCompletedTaskKey())
} catch (error) {
return null
}
}
// 加载持久化的导入结果统计
const loadImportResult = async () => {
if (!props.kbId) return
const lastTaskId = getLastCompletedTaskId()
if (!lastTaskId) {
importResult.value = null
return
}
try {
const res: any = await getFAQImportResult(props.kbId)
if (res?.data) {
importResult.value = res.data
const res: any = await getFAQImportProgress(lastTaskId)
const data = res?.data
if (data && data.status === 'completed') {
// 检查后端返回的 display_status,如果是 close 则不显示
if (data.display_status === 'close') {
importResult.value = null
return
}
// Map progress fields to importResult format
importResult.value = {
total_entries: data.total,
success_count: data.success_count,
failed_count: data.failed_count,
skipped_count: data.skipped_count || 0,
import_mode: data.import_mode || 'append',
imported_at: data.imported_at,
task_id: data.task_id,
failed_entries_url: data.failed_entries_url,
display_status: data.display_status || 'open',
processing_time: data.processing_time || 0,
}
} else {
importResult.value = null
}
+13 -30
View File
@@ -6095,40 +6095,23 @@ func (s *knowledgeService) GetFAQImportProgress(ctx context.Context, taskID stri
if err := json.Unmarshal(data, &progress); err != nil {
return nil, fmt.Errorf("failed to unmarshal FAQ import progress: %w", err)
}
return &progress, nil
}
// GetLastFAQImportResult retrieves the latest FAQ import result for a knowledge base
func (s *knowledgeService) GetLastFAQImportResult(ctx context.Context, kbID string) (*types.FAQImportResult, error) {
// 获取当前租户ID
tenantID := ctx.Value(types.TenantIDContextKey).(uint64)
// 查找FAQ类型的knowledge
knowledgeList, err := s.repo.ListKnowledgeByKnowledgeBaseID(ctx, tenantID, kbID)
if err != nil {
return nil, fmt.Errorf("failed to list knowledge: %w", err)
}
// 查找FAQ类型的knowledge
var faqKnowledge *types.Knowledge
for _, k := range knowledgeList {
if k.Type == types.KnowledgeTypeFAQ {
faqKnowledge = k
break
// If task is completed, enrich with persisted result fields from database
if progress.Status == types.FAQImportStatusCompleted && progress.KnowledgeID != "" {
tenantID := ctx.Value(types.TenantIDContextKey).(uint64)
knowledge, err := s.repo.GetKnowledgeByID(ctx, tenantID, progress.KnowledgeID)
if err == nil && knowledge != nil {
if result, err := knowledge.GetLastFAQImportResult(); err == nil && result != nil {
progress.SkippedCount = result.SkippedCount
progress.ImportMode = result.ImportMode
progress.ImportedAt = result.ImportedAt
progress.DisplayStatus = result.DisplayStatus
progress.ProcessingTime = result.ProcessingTime
}
}
}
if faqKnowledge == nil {
return nil, werrors.NewNotFoundError("FAQ knowledge not found in this knowledge base")
}
// 解析导入结果
result, err := faqKnowledge.GetLastFAQImportResult()
if err != nil {
return nil, fmt.Errorf("failed to parse FAQ import result: %w", err)
}
return result, nil
return &progress, nil
}
// UpdateLastFAQImportResultDisplayStatus updates the display status of FAQ import result
-38
View File
@@ -423,44 +423,6 @@ func (h *FAQHandler) GetImportProgress(c *gin.Context) {
})
}
// GetLastImportResult godoc
// @Summary 获取FAQ最后一次导入结果统计
// @Description 获取FAQ知识库的最后一次导入结果统计信息
// @Tags FAQ管理
// @Accept json
// @Produce json
// @Param id path string true "知识库ID"
// @Success 200 {object} map[string]interface{} "导入结果统计"
// @Failure 404 {object} errors.AppError "知识库不存在或无导入记录"
// @Security Bearer
// @Security ApiKeyAuth
// @Router /knowledge-bases/{id}/faq/import/last-result [get]
func (h *FAQHandler) GetLastImportResult(c *gin.Context) {
ctx := c.Request.Context()
kbID := secutils.SanitizeForLog(c.Param("id"))
result, err := h.knowledgeService.GetLastFAQImportResult(ctx, kbID)
if err != nil {
logger.ErrorWithFields(ctx, err, nil)
c.Error(err)
return
}
if result == nil {
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": nil,
"message": "暂无导入记录",
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": result,
})
}
// updateLastFAQImportResultDisplayStatusRequest is the request payload for UpdateLastImportResultDisplayStatus
type updateLastFAQImportResultDisplayStatusRequest struct {
DisplayStatus string `json:"display_status" binding:"required,oneof=open close"`
+1 -2
View File
@@ -194,8 +194,7 @@ func RegisterFAQRoutes(r *gin.RouterGroup, handler *handler.FAQHandler) {
faq.PUT("/entries/tags", handler.UpdateEntryTagBatch)
faq.DELETE("/entries", handler.DeleteEntries)
faq.POST("/search", handler.SearchFAQ)
// FAQ import result API
faq.GET("/import/last-result", handler.GetLastImportResult)
// FAQ import result display status
faq.PUT("/import/last-result/display", handler.UpdateLastImportResultDisplayStatus)
}
// FAQ import progress route (outside of knowledge-base scope)
+8
View File
@@ -293,6 +293,7 @@ const (
)
// FAQImportProgress represents the progress of an FAQ import task stored in Redis
// When Status is "completed", the result fields (SkippedCount, ImportMode, ImportedAt, DisplayStatus, ProcessingTime) are populated.
type FAQImportProgress struct {
TaskID string `json:"task_id"` // UUID for the import task
KBID string `json:"kb_id"` // Knowledge Base ID
@@ -303,6 +304,7 @@ type FAQImportProgress struct {
Processed int `json:"processed"` // Entries processed so far
SuccessCount int `json:"success_count"` // 成功导入/验证通过的条目数
FailedCount int `json:"failed_count"` // 失败的条目数
SkippedCount int `json:"skipped_count,omitempty"` // 跳过的条目数(如重复等)
FailedEntries []FAQFailedEntry `json:"failed_entries,omitempty"` // 失败条目详情(少量时直接返回)
FailedEntriesURL string `json:"failed_entries_url,omitempty"` // 失败条目CSV下载URL(大量时返回URL)
Message string `json:"message"` // Status message
@@ -310,6 +312,12 @@ type FAQImportProgress struct {
CreatedAt int64 `json:"created_at"` // Task creation timestamp
UpdatedAt int64 `json:"updated_at"` // Last update timestamp
DryRun bool `json:"dry_run,omitempty"` // 是否为 dry run 模式
// Result fields (populated when Status == "completed")
ImportMode string `json:"import_mode,omitempty"` // 导入模式:append 或 replace
ImportedAt time.Time `json:"imported_at,omitempty"` // 导入完成时间
DisplayStatus string `json:"display_status,omitempty"` // 显示状态:open 或 close
ProcessingTime int64 `json:"processing_time,omitempty"` // 处理耗时(毫秒)
}
// FAQImportMetadata 存储在Knowledge.Metadata中的FAQ导入任务信息
-2
View File
@@ -126,8 +126,6 @@ type KnowledgeService interface {
SaveKBCloneProgress(ctx context.Context, progress *types.KBCloneProgress) error
// GetFAQImportProgress retrieves the progress of an FAQ import task
GetFAQImportProgress(ctx context.Context, taskID string) (*types.FAQImportProgress, error)
// GetLastFAQImportResult retrieves the latest FAQ import result for a knowledge base
GetLastFAQImportResult(ctx context.Context, kbID string) (*types.FAQImportResult, error)
// UpdateLastFAQImportResultDisplayStatus updates the display status of FAQ import result
UpdateLastFAQImportResultDisplayStatus(ctx context.Context, kbID string, displayStatus string) error
// SearchKnowledge searches knowledge items by keyword across the tenant.