797 lines
29 KiB
Go
797 lines
29 KiB
Go
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)
|
||
}
|