package dataengine import ( consts "cid/consts/dataengine" dao "cid/dao/dataengine" entity "cid/model/entity/dataengine" yidunService "cid/service/yidun" "context" "encoding/json" "fmt" "time" "github.com/gogf/gf/v2/frame/g" ) // 轮询配置常量 const ( // PollBatchSize 每次轮询处理数量 PollBatchSize = 20 ) // 状态常量 const ( // 原表状态 - 与 tencent_image/tencent_video 表的 status 字段对应 StatusSubmitting = consts.CheckStatusSubmitting // 送检中 ) // MaterialVerifyService 素材校验服务 type MaterialVerifyService struct{} // MaterialVerify 校验服务单例 var MaterialVerify = new(MaterialVerifyService) // ============================================================================= // 校验状态转换 // ============================================================================= // SuggestionToVerifyStatus 根据易盾处置建议转换为校验状态 func SuggestionToVerifyStatus(suggestion int) string { switch suggestion { case consts.SuggestionPass: return entity.VerifyStatusVerified // 通过 case consts.SuggestionReview: return entity.VerifyStatusPending // 嫌疑,需要人工审核,暂不更新状态 case consts.SuggestionBlock: return entity.VerifyStatusRejected // 不通过 default: return entity.VerifyStatusPending } } // ============================================================================= // 图片校验 // ============================================================================= // VerifyImageByID 根据图片ID执行校验 func (s *MaterialVerifyService) VerifyImageByID(ctx context.Context, imageID string) (*entity.MaterialVerifyLog, error) { // 1. 获取图片数据 image, err := dao.TencentImage.GetByImageID(ctx, imageID) if err != nil { return nil, fmt.Errorf("查询图片数据失败: %w", err) } if image == nil { return nil, fmt.Errorf("未找到图片数据, imageID=%s", imageID) } // 2. 创建校验日志 log := s.createVerifyLog(ctx, entity.MaterialTypeImage, imageID, consts.SourceTableTencentImage, image.Id, image.AccountID) if log == nil { return nil, fmt.Errorf("创建校验日志失败") } // 3. 执行校验 err = s.submitImageCheck(ctx, image, log) if err != nil { return nil, err } return log, nil } // submitImageCheck 提交图片校验 func (s *MaterialVerifyService) submitImageCheck(ctx context.Context, image *entity.TencentImage, log *entity.MaterialVerifyLog) error { startTime := time.Now() // 获取回调模式开关 callbackMode := g.Cfg().MustGet(ctx, "yidun.callback_mode").Bool() // 构建请求参数 requestParams := map[string]interface{}{ "imageURL": image.PreviewURL, "dataID": image.ImageID, } requestParamsJSON, _ := json.Marshal(requestParams) var ( taskID string duration int64 ) if callbackMode { // 回调模式:使用异步检测,易盾处理完成后会回调 callbackURL := g.Cfg().MustGet(ctx, "yidun.image.callback_url").String() requestParams["callbackURL"] = callbackURL result, err := yidunService.ImageDetection.DetectImage(ctx, image.PreviewURL, image.ImageID, callbackURL) duration = time.Since(startTime).Milliseconds() if err != nil { dao.MaterialVerifyLog.UpdateError(ctx, log.Id, entity.VerifyStatusPending, err.Error()) dao.MaterialVerifyLog.UpdateDuration(ctx, log.Id, duration) g.Log().Warningf(ctx, "图片异步检测失败(保持待检验), id=%d, imageId=%s, error=%v", image.Id, image.ImageID, err) return fmt.Errorf("图片异步检测失败: %w", err) } taskID = result.TaskID // 保存任务ID和请求参数 dao.MaterialVerifyLog.UpdateTaskID(ctx, log.Id, taskID) dao.MaterialVerifyLog.UpdateRequestParams(ctx, log.Id, string(requestParamsJSON)) // 更新原表状态为 submitting(等待回调) s.updateImageStatus(ctx, image.Id, StatusSubmitting) g.Log().Infof(ctx, "图片异步检测已提交, id=%d, imageId=%s, taskId=%s, duration=%dms", image.Id, image.ImageID, taskID, duration) } else { // 轮询模式:使用同步检测,直接返回结果 syncResult, err := yidunService.ImageDetection.DetectImageSync(ctx, image.PreviewURL, image.ImageID) duration = time.Since(startTime).Milliseconds() if err != nil { dao.MaterialVerifyLog.UpdateError(ctx, log.Id, entity.VerifyStatusPending, err.Error()) dao.MaterialVerifyLog.UpdateDuration(ctx, log.Id, duration) g.Log().Warningf(ctx, "图片同步检测失败(保持待检验), id=%d, imageId=%s, error=%v", image.Id, image.ImageID, err) return fmt.Errorf("图片同步检测失败: %w", err) } taskID = syncResult.TaskID // 保存任务ID和请求参数 dao.MaterialVerifyLog.UpdateTaskID(ctx, log.Id, taskID) dao.MaterialVerifyLog.UpdateRequestParams(ctx, log.Id, string(requestParamsJSON)) // 根据同步结果更新状态 verifyStatus := SuggestionToVerifyStatus(syncResult.Suggestion) responseJSON, _ := json.Marshal(syncResult) dao.MaterialVerifyLog.UpdateVerifyResult(ctx, log.Id, verifyStatus, syncResult.Suggestion, syncResult.Label, syncResult.ResultType, string(responseJSON), syncResult.CensorTime) s.updateImageStatus(ctx, image.Id, verifyStatus) g.Log().Infof(ctx, "图片同步检测完成, id=%d, imageId=%s, taskId=%s, suggestion=%d, verifyStatus=%s, duration=%dms", image.Id, image.ImageID, taskID, syncResult.Suggestion, verifyStatus, duration) } return nil } // ============================================================================= // 视频校验 // ============================================================================= // VerifyVideoByID 根据视频ID执行校验 func (s *MaterialVerifyService) VerifyVideoByID(ctx context.Context, videoID string) (*entity.MaterialVerifyLog, error) { // 1. 获取视频数据 video, err := dao.TencentVideo.GetByVideoID(ctx, videoID) if err != nil { return nil, fmt.Errorf("查询视频数据失败: %w", err) } if video == nil { return nil, fmt.Errorf("未找到视频数据, videoID=%s", videoID) } // 2. 创建校验日志 log := s.createVerifyLog(ctx, entity.MaterialTypeVideo, videoID, consts.SourceTableTencentVideo, video.Id, video.AccountID) if log == nil { return nil, fmt.Errorf("创建校验日志失败") } // 3. 执行校验 err = s.submitVideoCheck(ctx, video, log) if err != nil { return nil, err } return log, nil } // submitVideoCheck 提交视频校验 func (s *MaterialVerifyService) submitVideoCheck(ctx context.Context, video *entity.TencentVideo, log *entity.MaterialVerifyLog) error { startTime := time.Now() // 获取回调模式开关 callbackMode := g.Cfg().MustGet(ctx, "yidun.callback_mode").Bool() // 根据开关决定回调地址 var callbackURL string if callbackMode { callbackURL = g.Cfg().MustGet(ctx, "yidun.video.callback_url").String() } // 构建请求参数 requestParams := map[string]interface{}{ "videoURL": video.PreviewURL, "dataID": video.VideoID, "callbackURL": callbackURL, } requestParamsJSON, _ := json.Marshal(requestParams) // 调用易盾视频检测 result, err := yidunService.VideoDetection.DetectVideo(ctx, video.PreviewURL, video.VideoID, callbackURL) duration := time.Since(startTime).Milliseconds() if err != nil { // 调用易盾接口失败(如额度用光、网络错误、超时等),不更新状态,保持待检验 // 只有易盾明确返回检测结果且suggestion=BLOCK时才标记为失败 dao.MaterialVerifyLog.UpdateError(ctx, log.Id, entity.VerifyStatusPending, err.Error()) dao.MaterialVerifyLog.UpdateDuration(ctx, log.Id, duration) g.Log().Warningf(ctx, "视频校验接口调用失败(保持待检验), id=%d, videoId=%s, error=%v", video.Id, video.VideoID, err) return fmt.Errorf("视频校验调用失败: %w", err) } // 保存任务ID和请求参数 dao.MaterialVerifyLog.UpdateTaskID(ctx, log.Id, result.TaskID) dao.MaterialVerifyLog.UpdateRequestParams(ctx, log.Id, string(requestParamsJSON)) // 更新原表状态为 submitting s.updateVideoStatus(ctx, video.Id, StatusSubmitting) // 轮询模式(无回调):提交后立即尝试查询检测结果 if !callbackMode { g.Log().Infof(ctx, "轮询模式:提交后立即查询结果, taskId=%s", result.TaskID) // 等待500ms让易盾有时间处理 time.Sleep(500 * time.Millisecond) if err := s.ProcessVideoResultByTask(ctx, result.TaskID); err != nil { g.Log().Warningf(ctx, "提交后立即查询结果失败(不影响状态,后续轮询继续), taskId=%s, error=%v", result.TaskID, err) } } g.Log().Infof(ctx, "视频校验已提交, id=%d, videoId=%s, taskId=%s, duration=%dms", video.Id, video.VideoID, result.TaskID, duration) return nil } // ============================================================================= // 回调处理 // ============================================================================= // ProcessImageCallback 处理图片校验回调 func (s *MaterialVerifyService) ProcessImageCallback(ctx context.Context, callbackData string) error { g.Log().Infof(ctx, "处理图片校验回调, data: %s", callbackData) var callback yidunService.ImageCallbackData if err := json.Unmarshal([]byte(callbackData), &callback); err != nil { g.Log().Errorf(ctx, "解析图片回调数据失败: %v", err) return fmt.Errorf("解析回调数据失败: %w", err) } if callback.Antispam == nil { return fmt.Errorf("回调数据格式错误:缺少antispam字段") } antispam := callback.Antispam g.Log().Infof(ctx, "处理图片校验结果 - taskId: %s, suggestion: %d, resultType: %d", antispam.TaskId, antispam.Suggestion, antispam.ResultType) // 根据 taskId 查找校验日志 log, err := dao.MaterialVerifyLog.GetByTaskID(ctx, antispam.TaskId) if err != nil { return fmt.Errorf("查找校验日志失败: %w", err) } if log == nil { g.Log().Warningf(ctx, "未找到校验日志, taskId=%s", antispam.TaskId) return nil } // 构建响应结果 responseResult := callbackData // 根据 suggestion 确定校验状态 verifyStatus := SuggestionToVerifyStatus(antispam.Suggestion) // 更新日志 err = dao.MaterialVerifyLog.UpdateVerifyResult(ctx, log.Id, verifyStatus, antispam.Suggestion, antispam.Label, antispam.ResultType, responseResult, antispam.CensorTime) if err != nil { return fmt.Errorf("更新校验日志失败: %w", err) } // 更新原表状态(图片回调只处理图片来源) if log.SourceTable == consts.SourceTableTencentImage { s.updateImageStatus(ctx, log.SourceID, verifyStatus) } g.Log().Infof(ctx, "图片校验回调处理完成, taskId=%s, verifyStatus=%s, suggestion=%d", antispam.TaskId, verifyStatus, antispam.Suggestion) return nil } // ProcessVideoCallback 处理视频校验回调 func (s *MaterialVerifyService) ProcessVideoCallback(ctx context.Context, callbackData string) error { g.Log().Infof(ctx, "处理视频校验回调, data: %s", callbackData) var callback yidunService.VideoCallbackData if err := json.Unmarshal([]byte(callbackData), &callback); err != nil { g.Log().Errorf(ctx, "解析视频回调数据失败: %v", err) return fmt.Errorf("解析回调数据失败: %w", err) } if callback.Antispam == nil { return fmt.Errorf("视频回调数据格式错误:缺少antispam字段") } antispam := callback.Antispam g.Log().Infof(ctx, "处理视频校验结果 - taskId: %s, suggestion: %d, resultType: %d", antispam.TaskID, antispam.Suggestion, antispam.ResultType) // 根据 taskId 查找校验日志 log, err := dao.MaterialVerifyLog.GetByTaskID(ctx, antispam.TaskID) if err != nil { return fmt.Errorf("查找校验日志失败: %w", err) } if log == nil { g.Log().Warningf(ctx, "未找到校验日志, taskId=%s", antispam.TaskID) return nil } // 构建响应结果 responseResult := callbackData // 根据 suggestion 确定校验状态 verifyStatus := SuggestionToVerifyStatus(antispam.Suggestion) // 审核时间 checkTime := antispam.CensorTime if checkTime == 0 { checkTime = antispam.CheckTime } // 更新日志 err = dao.MaterialVerifyLog.UpdateVerifyResult(ctx, log.Id, verifyStatus, antispam.Suggestion, antispam.Label, antispam.ResultType, responseResult, checkTime) if err != nil { return fmt.Errorf("更新校验日志失败: %w", err) } // 更新原表状态(视频回调只处理视频来源) if log.SourceTable == consts.SourceTableTencentVideo { s.updateVideoStatus(ctx, log.SourceID, verifyStatus) } g.Log().Infof(ctx, "视频校验回调处理完成, taskId=%s, verifyStatus=%s, suggestion=%d", antispam.TaskID, verifyStatus, antispam.Suggestion) return nil } // ============================================================================= // 轮询模式处理 // ============================================================================= // 易盾检测状态常量 const ( YidunStatusNotStart = 0 // 未开始 YidunStatusProcessing = 1 // 检测中 YidunStatusSuccess = 2 // 检测成功 YidunStatusFailed = 3 // 检测失败 ) // ProcessImageResultByTask 根据任务ID处理图片结果(轮询模式) func (s *MaterialVerifyService) ProcessImageResultByTask(ctx context.Context, taskID string) error { log, err := dao.MaterialVerifyLog.GetByTaskID(ctx, taskID) if err != nil || log == nil { return fmt.Errorf("未找到校验日志, taskId=%s", taskID) } result, err := yidunService.ImageDetection.GetImageResult(ctx, taskID) if err != nil { // 判断是否是未找到结果或仍在检测中的错误 if err == yidunService.ErrImageResultNotFound || err == yidunService.ErrImageStillProcessing { // 未获取到结果(任务不存在或仍在处理),不更新状态,保持等待下次轮询 g.Log().Infof(ctx, "图片检测结果未就绪, taskId=%s, 保持pending状态, err=%v", taskID, err) return nil } // 其他错误(如额度用光、网络错误、API错误等),不更新状态,保持待检验 // 只有易盾明确返回suggestion=BLOCK时才标记为失败 dao.MaterialVerifyLog.UpdateError(ctx, log.Id, entity.VerifyStatusPending, err.Error()) g.Log().Warningf(ctx, "图片检测查询失败(保持待检验), taskId=%s, error=%v", taskID, err) return nil // 返回nil避免日志被反复处理,但保持pending状态 } // 判断检测状态 if result.Status == YidunStatusProcessing || result.Status == YidunStatusNotStart { // 检测仍在进行中,保持pending状态 g.Log().Infof(ctx, "图片检测仍在进行中, taskId=%s, status=%d, 保持pending状态", taskID, result.Status) return nil } if result.Status == YidunStatusFailed { // 易盾检测失败(如额度用光、服务端错误等),不更新状态,保持待检验 // 只有易盾明确返回suggestion=BLOCK时才标记为失败 errMsg := fmt.Sprintf("易盾检测失败, status=%d", result.Status) dao.MaterialVerifyLog.UpdateError(ctx, log.Id, entity.VerifyStatusPending, errMsg) g.Log().Warningf(ctx, "图片检测失败(保持待检验), taskId=%s, status=%d", taskID, result.Status) return nil } // status == YidunStatusSuccess,检测成功,根据suggestion更新状态 verifyStatus := SuggestionToVerifyStatus(result.Suggestion) responseJSON, _ := json.Marshal(result) dao.MaterialVerifyLog.UpdateVerifyResult(ctx, log.Id, verifyStatus, result.Suggestion, result.Label, result.ResultType, string(responseJSON), result.CensorTime) if log.SourceTable == consts.SourceTableTencentImage { s.updateImageStatus(ctx, log.SourceID, verifyStatus) } g.Log().Infof(ctx, "图片检测结果更新成功, taskId=%s, status=%d, suggestion=%d, verifyStatus=%s", taskID, result.Status, result.Suggestion, verifyStatus) return nil } // ProcessVideoResultByTask 根据任务ID处理视频结果(轮询模式) func (s *MaterialVerifyService) ProcessVideoResultByTask(ctx context.Context, taskID string) error { log, err := dao.MaterialVerifyLog.GetByTaskID(ctx, taskID) if err != nil || log == nil { return fmt.Errorf("未找到校验日志, taskId=%s", taskID) } result, err := yidunService.VideoDetection.GetVideoResult(ctx, taskID) if err != nil { // 判断是否是未找到结果或仍在检测中的错误 if err == yidunService.ErrVideoResultNotFound || err == yidunService.ErrVideoStillProcessing { // 未获取到结果(任务不存在或仍在处理),不更新状态,保持等待下次轮询 g.Log().Infof(ctx, "视频检测结果未就绪, taskId=%s, 保持pending状态, err=%v", taskID, err) return nil } // 其他错误(如额度用光、网络错误、API错误等),不更新状态,保持待检验 // 只有易盾明确返回suggestion=BLOCK时才标记为失败 dao.MaterialVerifyLog.UpdateError(ctx, log.Id, entity.VerifyStatusPending, err.Error()) g.Log().Warningf(ctx, "视频检测查询失败(保持待检验), taskId=%s, error=%v", taskID, err) return nil // 返回nil避免日志被反复处理,但保持pending状态 } // 判断检测状态 if result.Status == YidunStatusProcessing || result.Status == YidunStatusNotStart { // 检测仍在进行中,保持pending状态 g.Log().Infof(ctx, "视频检测仍在进行中, taskId=%s, status=%d, 保持pending状态", taskID, result.Status) return nil } if result.Status == YidunStatusFailed { // 易盾检测失败(如额度用光、服务端错误等),不更新状态,保持待检验 // 只有易盾明确返回suggestion=BLOCK时才标记为失败 errMsg := fmt.Sprintf("易盾检测失败, status=%d", result.Status) dao.MaterialVerifyLog.UpdateError(ctx, log.Id, entity.VerifyStatusPending, errMsg) g.Log().Warningf(ctx, "视频检测失败(保持待检验), taskId=%s, status=%d", taskID, result.Status) return nil } // status == YidunStatusSuccess,检测成功,根据suggestion更新状态 verifyStatus := SuggestionToVerifyStatus(result.Suggestion) responseJSON, _ := json.Marshal(result) dao.MaterialVerifyLog.UpdateVerifyResult(ctx, log.Id, verifyStatus, result.Suggestion, result.Label, result.ResultType, string(responseJSON), result.CensorTime) if log.SourceTable == consts.SourceTableTencentVideo { s.updateVideoStatus(ctx, log.SourceID, verifyStatus) } g.Log().Infof(ctx, "视频检测结果更新成功, taskId=%s, status=%d, suggestion=%d, verifyStatus=%s", taskID, result.Status, result.Suggestion, verifyStatus) return nil } // ============================================================================= // 辅助方法 // ============================================================================= // createVerifyLog 创建校验日志 func (s *MaterialVerifyService) createVerifyLog(ctx context.Context, materialType, materialID, sourceTable string, sourceID, accountID int64) *entity.MaterialVerifyLog { log := &entity.MaterialVerifyLog{ TenantID: 0, MaterialType: materialType, MaterialID: materialID, SourceTable: sourceTable, SourceID: sourceID, AccountID: accountID, VerifyStatus: entity.VerifyStatusPending, } id, err := dao.MaterialVerifyLog.Create(ctx, log) if err != nil { g.Log().Errorf(ctx, "创建校验日志失败: %v", err) return nil } log.Id = id return log } // updateImageStatus 更新图片状态 func (s *MaterialVerifyService) updateImageStatus(ctx context.Context, imageID int64, verifyStatus string) { _, err := dao.TencentImage.UpdateStatus(ctx, imageID, verifyStatus) if err != nil { g.Log().Errorf(ctx, "更新图片状态失败: %v", err) } else { g.Log().Infof(ctx, "更新图片状态成功, imageID=%d, status=%s", imageID, verifyStatus) } } // updateVideoStatus 更新视频状态 func (s *MaterialVerifyService) updateVideoStatus(ctx context.Context, videoID int64, verifyStatus string) { _, err := dao.TencentVideo.UpdateStatus(ctx, videoID, verifyStatus) if err != nil { g.Log().Errorf(ctx, "更新视频状态失败: %v", err) } else { g.Log().Infof(ctx, "更新视频状态成功, videoID=%d, status=%s", videoID, verifyStatus) } } // ============================================================================= // 查询接口 // ============================================================================= // GetLogByID 根据ID获取日志 func (s *MaterialVerifyService) GetLogByID(ctx context.Context, id int64) (*entity.MaterialVerifyLog, error) { return dao.MaterialVerifyLog.GetByID(ctx, id) } // GetLogsByMaterialID 根据素材ID获取日志列表 func (s *MaterialVerifyService) GetLogsByMaterialID(ctx context.Context, materialID string) ([]entity.MaterialVerifyLog, error) { return dao.MaterialVerifyLog.GetByMaterialID(ctx, materialID) } // GetLogsByCondition 条件查询日志 func (s *MaterialVerifyService) GetLogsByCondition(ctx context.Context, condition map[string]interface{}, page, pageSize int) ([]entity.MaterialVerifyLog, int, error) { return dao.MaterialVerifyLog.GetByCondition(ctx, condition, page, pageSize) } // GetStats 获取统计信息 func (s *MaterialVerifyService) GetStats(ctx context.Context) (map[string]int, error) { return dao.MaterialVerifyLog.GetStats(ctx) } // ============================================================================= // 轮询模式 - 批量查询检测结果 // ============================================================================= // PollPendingResults 轮询所有待查询结果的日志(手动触发) // 返回处理成功的数量和错误信息 func (s *MaterialVerifyService) PollPendingResults(ctx context.Context) (int, int, error) { // 获取待查询的日志 logs, err := dao.MaterialVerifyLog.GetPendingResults(ctx, PollBatchSize) if err != nil { return 0, 0, err } if len(logs) == 0 { g.Log().Infof(ctx, "没有待查询结果的日志") return 0, 0, nil } g.Log().Infof(ctx, "开始轮询 %d 条待处理结果", len(logs)) successCount := 0 failCount := 0 var lastErr error for _, log := range logs { var err error // 根据来源表判断调用哪个接口 if log.SourceTable == consts.SourceTableTencentImage { err = s.ProcessImageResultByTask(ctx, log.TaskID) } else if log.SourceTable == consts.SourceTableTencentVideo { err = s.ProcessVideoResultByTask(ctx, log.TaskID) } else { g.Log().Warningf(ctx, "未知的来源表: %s, logId=%d", log.SourceTable, log.Id) continue } if err != nil { failCount++ lastErr = err g.Log().Warningf(ctx, "处理结果失败, logId=%d, taskId=%s, error=%v", log.Id, log.TaskID, err) } else { successCount++ g.Log().Infof(ctx, "处理结果成功, logId=%d, taskId=%s", log.Id, log.TaskID) } // 避免请求过快 time.Sleep(100 * time.Millisecond) } g.Log().Infof(ctx, "轮询完成, 成功=%d, 失败=%d", successCount, failCount) return successCount, failCount, lastErr } // PollPendingResultsByType 按类型轮询待查询结果的日志 func (s *MaterialVerifyService) PollPendingResultsByType(ctx context.Context, sourceTable string) (int, int, error) { // 获取待查询的日志 logs, err := dao.MaterialVerifyLog.GetPendingResults(ctx, PollBatchSize) if err != nil { return 0, 0, err } // 过滤指定类型 var filteredLogs []entity.MaterialVerifyLog for _, log := range logs { if log.SourceTable == sourceTable { filteredLogs = append(filteredLogs, log) } } if len(filteredLogs) == 0 { g.Log().Infof(ctx, "没有待查询结果的日志, sourceTable=%s", sourceTable) return 0, 0, nil } g.Log().Infof(ctx, "开始轮询 %d 条待处理结果, sourceTable=%s", len(filteredLogs), sourceTable) successCount := 0 failCount := 0 var lastErr error for _, log := range filteredLogs { var err error if sourceTable == consts.SourceTableTencentImage { err = s.ProcessImageResultByTask(ctx, log.TaskID) } else if sourceTable == consts.SourceTableTencentVideo { err = s.ProcessVideoResultByTask(ctx, log.TaskID) } if err != nil { failCount++ lastErr = err } else { successCount++ } time.Sleep(100 * time.Millisecond) } g.Log().Infof(ctx, "轮询完成, sourceTable=%s, 成功=%d, 失败=%d", sourceTable, successCount, failCount) return successCount, failCount, lastErr } // PollPendingImageResults 轮询图片待查询结果 func (s *MaterialVerifyService) PollPendingImageResults(ctx context.Context) (int, int, error) { return s.PollPendingResultsByType(ctx, consts.SourceTableTencentImage) } // PollPendingVideoResults 轮询视频待查询结果 func (s *MaterialVerifyService) PollPendingVideoResults(ctx context.Context) (int, int, error) { return s.PollPendingResultsByType(ctx, consts.SourceTableTencentVideo) } // ============================================================================= // 导出服务 - 不通过数据导出 // ============================================================================= // ExportRejectedItem 导出的不通过数据项 type ExportRejectedItem struct { ID int64 `json:"id"` // 素材表主键ID MaterialID string `json:"materialId"` // 素材ID(imageId/videoId) AccountID int64 `json:"accountId"` // 账户ID CorporationName string `json:"corporationName"` // 公司名称 PreviewURL string `json:"previewUrl"` // 预览URL Description string `json:"description"` // 描述 ErrorMsg string `json:"errorMsg"` // 失败原因(最后一条失败日志的error_msg) MaterialType string `json:"materialType"` // 素材类型 IMAGE/VIDEO ImageUsage string `json:"imageUsage"` // 图片用途(仅图片) CreatedAt string `json:"createdAt"` // 检测时间(日志创建时间) } // getFailureReason 获取失败原因 func getFailureReason(log *entity.MaterialVerifyLog) string { if log == nil { return "无校验日志" } // 优先使用 error_msg if log.ErrorMsg != "" { return log.ErrorMsg } // 根据 suggestion 和 label 生成原因 reasonMap := map[int]string{ 0: "内容检测通过", 1: "内容嫌疑(需人工审核)", 2: "内容不通过", } suggestionText := reasonMap[log.Suggestion] if suggestionText == "" { suggestionText = fmt.Sprintf("未知(suggestion=%d)", log.Suggestion) } // 如果有 response_result,尝试提取更多信息 if log.ResponseResult != "" { var resultMap map[string]interface{} if err := json.Unmarshal([]byte(log.ResponseResult), &resultMap); err == nil { if labels, ok := resultMap["labels"]; ok { return fmt.Sprintf("%s (labels: %v)", suggestionText, labels) } } return suggestionText } return suggestionText } // ExportRejectedData 导出不通过数据 func (s *MaterialVerifyService) ExportRejectedData(ctx context.Context, materialType string) ([]ExportRejectedItem, error) { var items []ExportRejectedItem // 加载账户名称映射 accountMap := make(map[int64]string) if accounts, err := dao.TencentAccountRelation.GetAll(ctx); err == nil { for _, acc := range accounts { if acc.CorporationName != "" { accountMap[acc.AccountID] = acc.CorporationName } } } // 处理图片 if materialType == "" || materialType == entity.MaterialTypeImage { condition := map[string]interface{}{ entity.TencentImageCols.VerifyStatus: entity.VerifyStatusRejected, } images, total, err := dao.TencentImage.GetByCondition(ctx, condition, 1, 100000) if err != nil { g.Log().Errorf(ctx, "查询不通过图片失败: %v", err) return nil, fmt.Errorf("查询不通过图片失败: %w", err) } g.Log().Infof(ctx, "导出不通过图片: total=%d, got=%d", total, len(images)) for _, img := range images { // 查询最后一条失败的校验日志 log, _ := dao.MaterialVerifyLog.GetLastRejectedLogByMaterialID(ctx, img.ImageID, entity.VerifyStatusRejected) var createdAtStr string if log != nil && log.CreatedAt != nil { createdAtStr = log.CreatedAt.Format("Y-m-d H:i:s") } items = append(items, ExportRejectedItem{ ID: img.Id, MaterialID: img.ImageID, AccountID: img.AccountID, CorporationName: accountMap[img.AccountID], PreviewURL: img.PreviewURL, Description: img.Description, ErrorMsg: getFailureReason(log), MaterialType: entity.MaterialTypeImage, ImageUsage: img.ImageUsage, CreatedAt: createdAtStr, }) } } // 处理视频 if materialType == "" || materialType == entity.MaterialTypeVideo { condition := map[string]interface{}{ entity.TencentVideoCols.VerifyStatus: entity.VerifyStatusRejected, } videos, total, err := dao.TencentVideo.GetByCondition(ctx, condition, 1, 100000) if err != nil { g.Log().Errorf(ctx, "查询不通过视频失败: %v", err) return nil, fmt.Errorf("查询不通过视频失败: %w", err) } g.Log().Infof(ctx, "导出不通过视频: total=%d, got=%d", total, len(videos)) for _, vid := range videos { // 查询最后一条失败的校验日志 log, _ := dao.MaterialVerifyLog.GetLastRejectedLogByMaterialID(ctx, vid.VideoID, entity.VerifyStatusRejected) var createdAtStr string if log != nil && log.CreatedAt != nil { createdAtStr = log.CreatedAt.Format("Y-m-d H:i:s") } items = append(items, ExportRejectedItem{ ID: vid.Id, MaterialID: vid.VideoID, AccountID: vid.AccountID, CorporationName: accountMap[vid.AccountID], PreviewURL: vid.PreviewURL, Description: vid.Description, ErrorMsg: getFailureReason(log), MaterialType: entity.MaterialTypeVideo, CreatedAt: createdAtStr, }) } } return items, nil } // GetPendingResultsCount 获取待查询结果的数量 func (s *MaterialVerifyService) GetPendingResultsCount(ctx context.Context) (int, error) { return dao.MaterialVerifyLog.CountPendingResults(ctx) }