From 2fd858c97e2c04012728bae809a555485fe31bcf Mon Sep 17 00:00:00 2001 From: lmk <1095689763@qq.com> Date: Sat, 18 Jul 2026 10:10:23 +0800 Subject: [PATCH] =?UTF-8?q?cid=E9=80=82=E9=85=8Dppgo=5Fjob?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 6 +- CLAUDE.md | 90 +++ config.yml | 5 +- consts/public/collections.go | 1 - .../dataengine/material_verify_controller.go | 100 ++- controller/internal/util.go | 42 ++ controller/yidun/content_check_controller.go | 23 +- controller/yidun/yidun_callback_controller.go | 112 ++-- controller/yidun/yidun_controller.go | 29 +- dao/dataengine/material_verify_log_dao.go | 48 +- .../tencent_content_check_log_dao.go | 17 +- dao/dataengine/tencent_image_dao.go | 16 + dao/dataengine/tencent_video_dao.go | 16 + main.go | 1 + .../entity/dataengine/material_verify_log.go | 101 +-- service/dataengine/material_verify_service.go | 261 +++++--- .../tencent_content_check_service.go | 189 ++++-- service/yidun/image_detection_service.go | 26 - service/yidun/video_detection_service.go | 606 +++--------------- ...terial_verify_log_add_risk_description.sql | 5 + 20 files changed, 810 insertions(+), 884 deletions(-) create mode 100644 CLAUDE.md delete mode 100644 consts/public/collections.go create mode 100644 controller/internal/util.go create mode 100644 sql/alter_material_verify_log_add_risk_description.sql diff --git a/.gitignore b/.gitignore index b8455be..73374e1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,6 @@ /.idea/* -/docs/* \ No newline at end of file +/docs/* + + +# 运行日志 +/resource/log/* \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..122bec6 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,90 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Build & Run + +```bash +# Build (Docker) +docker build -t cid . + +# Run locally (requires Go 1.26) +go mod download && go mod tidy +go run main.go + +# Build binary +go build -ldflags="-s -w" -o main ./main.go + +# Test +go test ./... +``` + +## GoFrame CLI (optional) + +```bash +# Install gf CLI +go install github.com/gogf/gf/v2/cmd/gf/v2@latest + +# Generate DAO/Model from DB +gf gen dao -c config.yml +``` + +## Project Overview + +CID is a **content moderation service** that submits advertising images/videos to Netease Yidun (易盾) for automated content review. It provides REST APIs, a scheduled batch-checking scheduler, and a frontend management UI. + +### Core Flow + +1. **Material sources**: `tencent_image` / `tencent_video` tables in the `dataengine` PostgreSQL database (external system) +2. **Detection**: Submit media to Yidun API for content moderation (async callback mode or sync polling mode) +3. **Result handling**: Yidun pushes results to callback endpoints, or the service polls Yidun for results +4. **Logging**: Results recorded in `material_verify_log` table (cid DB) with status backfilled to source tables + +### Architecture + +``` +controller/ HTTP handlers (GoFrame controller + ghttp.Request handlers) +├── yidun/ Content detection API (text/image/video submit, callback receive, result polling) +├── dataengine/ Material verification UI API (list, stats, manual verify, batch verify, export, callback) +service/ Business logic layer +├── yidun/ Yidun SDK wrappers (text/image/video detection, callback processing) +├── dataengine/ Material verification pipeline, content check scheduler, callback handling +dao/ Data access layer (GoFrame ORM + custom gfdb wrapper) +├── dataengine/ DAOs for tencent_image, tencent_video, material_verify_log, etc. +model/ Entity/DTO definitions +├── entity/dataengine/ DB entity structs with field constants +├── dto/yidun/ Request/response DTOs +consts/ Constants +├── dataengine/ Table names, check statuses, suggestion values +├── public/ Shared constants (currently empty) +resource/frontend/ Static frontend HTML (material-verify.html) +sql/ DDL and migration scripts +``` + +### Two Detection Modes + +Configured via `yidun.callback_mode` in config.yml: +- **Callback mode** (true): Yidun pushes results to `/yidun/callback/receiveImage` / `/yidun/callback/receiveVideo` after async detection completes. Requires a public-facing callback URL configured in `yidun.image.callback_url` / `yidun.video.callback_url`. +- **Polling mode** (false): After submitting content, the service immediately calls Yidun's sync image API (for images) or queries results via callback API (for video). Manual polling endpoints are also available. + +### Scheduled Content Check + +The `content_check` section in config.yml controls a background scheduler (`tencent_content_check_service.go`) that periodically fetches pending media from source tables and submits them for verification. Configurable via `batch_size`, `interval_seconds`, `image_enabled`, `video_enabled`, `scheduler_enabled`. + +## Key Design Decisions + +- **Snowflake IDs**: DAO `Create()` methods generate IDs client-side via Snowflake because GoFrame v2.10's pgsql driver doesn't support `RETURNING` / `LastInsertId`. +- **Dual database**: The `cid` DB holds verification logs; the `dataengine` DB holds source material tables. The `db.go` helper (`Model(tableName)`) selects the `dataEngine` DB group. +- **Mixed ORM**: Some DAOs use `g.DB("dataEngine").Model()` (standard GoFrame), others use `gfdb.DB(ctx, "cid").Model(ctx, ...)` (custom common library wrapper that passes context to DB operations). Both access the same underlying gdb engine. +- **Singleton pattern**: All controllers, services, and DAOs are package-level global singletons (e.g., `var MaterialVerify = new(MaterialVerifyService)`). +- **Context user**: Each controller method injects a hardcoded `beans.User` into context (`ctx = context.WithValue(ctx, "user", &beans.User{UserName: "admin", TenantId: 1})`). No real auth. +- **Verify status values**: `PENDING` → `SUBMITTING` → `VERIFIED` / `REJECTED` / `REVIEW`. Source tables use a different set: `PENDING` / `SUBMITTING` / `SUCCESS` / `FAILED` / `COMPLETED`. + +## API Routes + +Routes are registered in `main.go` via `http.RouteRegister()` (from the common library). Key endpoint groups: + +- **Yidun detection** (`controller/yidun`): POST `/yidun/detectText`, `/yidun/detectImage`, `/yidun/detectVideo` — submit content for detection +- **Yidun callback** (`controller/yidun`): POST `/yidun/callback/receiveImage`, `/yidun/callback/receiveVideo` — receive Yidun push results; POST `/yidun/callback/poll`, `/yidun/callback/pollImage`, `/yidun/callback/pollVideo` — manual polling +- **Material verify** (`controller/dataengine`): POST `/dataengine/listImage`, `/dataengine/listVideo`, `/dataengine/statsImage`, `/dataengine/statsVideo`, `/dataengine/manualVerifyImage`, `/dataengine/batchVerifyImage`, `/dataengine/exportRejected`, `/dataengine/imageCallback`, etc. +- **Content check** (`controller/yidun`): POST `/contentCheck/start`, `/contentCheck/stop`, `/contentCheck/status`, `/contentCheck/manualSubmitImageByID`, etc. diff --git a/config.yml b/config.yml index bba88ca..b6c200a 100644 --- a/config.yml +++ b/config.yml @@ -36,7 +36,7 @@ database: port: "15432" user: "postgres" pass: "Bjang09@686^*^" - name: "dataengine" + name: "engine" role: "master" maxIdle: "5" maxOpen: "20" @@ -119,4 +119,5 @@ content_check: # 是否启用视频检测 video_enabled: false # 定时任务执行间隔(秒) - interval_seconds: 30 \ No newline at end of file + interval_seconds: 30 + poll_interval: 60 diff --git a/consts/public/collections.go b/consts/public/collections.go deleted file mode 100644 index 9fd24e5..0000000 --- a/consts/public/collections.go +++ /dev/null @@ -1 +0,0 @@ -package public diff --git a/controller/dataengine/material_verify_controller.go b/controller/dataengine/material_verify_controller.go index 3616ef3..d75d71e 100644 --- a/controller/dataengine/material_verify_controller.go +++ b/controller/dataengine/material_verify_controller.go @@ -2,17 +2,15 @@ package dataengine import ( consts "cid/consts/dataengine" + internal "cid/controller/internal" dao "cid/dao/dataengine" entity "cid/model/entity/dataengine" serviceDataengine "cid/service/dataengine" "context" "fmt" - "strings" "time" - "gitea.redpowerfuture.com/red-future/common/beans" "github.com/gogf/gf/v2/frame/g" - "github.com/gogf/gf/v2/net/ghttp" ) // MaterialVerifyController 素材校验控制器 @@ -107,7 +105,7 @@ type BatchVerifyReq struct { // ListImage 图片素材列表 func (c *MaterialVerifyController) ListImage(ctx context.Context, req *ImageListReq) (res *ImageListRes, err error) { - ctx = context.WithValue(ctx, "user", &beans.User{UserName: "admin", TenantId: 1}) + ctx = internal.WithAdminUser(ctx) if req.Page == 0 { req.Page = 1 @@ -137,11 +135,20 @@ func (c *MaterialVerifyController) ListImage(ctx context.Context, req *ImageList // StatsImage 图片素材统计 func (c *MaterialVerifyController) StatsImage(ctx context.Context, req *ImageListReq) (res *StatsRes, err error) { - ctx = context.WithValue(ctx, "user", &beans.User{UserName: "admin", TenantId: 1}) + ctx = internal.WithAdminUser(ctx) // 使用实体中定义的正确状态值:PENDING=待校验, VERIFIED=校验通过, REJECTED=校验不通过 - pending, _ := dao.TencentImage.CountByStatus(ctx, entity.VerifyStatusPending) - verified, _ := dao.TencentImage.CountByStatus(ctx, entity.VerifyStatusVerified) - rejected, _ := dao.TencentImage.CountByStatus(ctx, entity.VerifyStatusRejected) + pending, err := dao.TencentImage.CountByStatus(ctx, entity.VerifyStatusPending) + if err != nil { + g.Log().Errorf(ctx, "统计待校验图片数量失败: %v", err) + } + verified, err := dao.TencentImage.CountByStatus(ctx, entity.VerifyStatusVerified) + if err != nil { + g.Log().Errorf(ctx, "统计已通过图片数量失败: %v", err) + } + rejected, err := dao.TencentImage.CountByStatus(ctx, entity.VerifyStatusRejected) + if err != nil { + g.Log().Errorf(ctx, "统计不通过图片数量失败: %v", err) + } return &StatsRes{ Pending: pending, @@ -156,7 +163,7 @@ func (c *MaterialVerifyController) StatsImage(ctx context.Context, req *ImageLis // ListVideo 视频素材列表 func (c *MaterialVerifyController) ListVideo(ctx context.Context, req *VideoListReq) (res *VideoListRes, err error) { - ctx = context.WithValue(ctx, "user", &beans.User{UserName: "admin", TenantId: 1}) + ctx = internal.WithAdminUser(ctx) if req.Page == 0 { req.Page = 1 @@ -186,11 +193,20 @@ func (c *MaterialVerifyController) ListVideo(ctx context.Context, req *VideoList // StatsVideo 视频素材统计 func (c *MaterialVerifyController) StatsVideo(ctx context.Context, req *VideoListReq) (res *StatsRes, err error) { - ctx = context.WithValue(ctx, "user", &beans.User{UserName: "admin", TenantId: 1}) + ctx = internal.WithAdminUser(ctx) // 使用实体中定义的正确状态值:PENDING=待校验, VERIFIED=校验通过, REJECTED=校验不通过 - pending, _ := dao.TencentVideo.CountByStatus(ctx, entity.VerifyStatusPending) - verified, _ := dao.TencentVideo.CountByStatus(ctx, entity.VerifyStatusVerified) - rejected, _ := dao.TencentVideo.CountByStatus(ctx, entity.VerifyStatusRejected) + pending, err := dao.TencentVideo.CountByStatus(ctx, entity.VerifyStatusPending) + if err != nil { + g.Log().Errorf(ctx, "统计待校验视频数量失败: %v", err) + } + verified, err := dao.TencentVideo.CountByStatus(ctx, entity.VerifyStatusVerified) + if err != nil { + g.Log().Errorf(ctx, "统计已通过视频数量失败: %v", err) + } + rejected, err := dao.TencentVideo.CountByStatus(ctx, entity.VerifyStatusRejected) + if err != nil { + g.Log().Errorf(ctx, "统计不通过视频数量失败: %v", err) + } return &StatsRes{ Pending: pending, @@ -211,7 +227,7 @@ type ListLogRes struct { // ListLog 日志列表 func (c *MaterialVerifyController) ListLog(ctx context.Context, req *LogListReq) (res *ListLogRes, err error) { - ctx = context.WithValue(ctx, "user", &beans.User{UserName: "admin", TenantId: 1}) + ctx = internal.WithAdminUser(ctx) if req.Page == 0 { req.Page = 1 @@ -258,7 +274,7 @@ type GetLogDetailReq struct { // GetLogDetail 日志详情 func (c *MaterialVerifyController) GetLogDetail(ctx context.Context, req *GetLogDetailReq) (res *LogDetailRes, err error) { - ctx = context.WithValue(ctx, "user", &beans.User{UserName: "admin", TenantId: 1}) + ctx = internal.WithAdminUser(ctx) log, err := serviceDataengine.MaterialVerify.GetLogByID(ctx, req.Id) if err != nil { @@ -297,7 +313,7 @@ type StatsLogRes struct { // StatsLog 日志统计 func (c *MaterialVerifyController) StatsLog(ctx context.Context, req *LogListReq) (res *StatsLogRes, err error) { - ctx = context.WithValue(ctx, "user", &beans.User{UserName: "admin", TenantId: 1}) + ctx = internal.WithAdminUser(ctx) stats, err := serviceDataengine.MaterialVerify.GetStats(ctx) if err != nil { return nil, err @@ -324,7 +340,7 @@ type ManualVerifyImageRes struct { // ManualVerifyImage 手动校验图片 func (c *MaterialVerifyController) ManualVerifyImage(ctx context.Context, req *ManualVerifyReq) (res *ManualVerifyImageRes, err error) { - ctx = context.WithValue(ctx, "user", &beans.User{UserName: "admin", TenantId: 1}) + ctx = internal.WithAdminUser(ctx) log, err := serviceDataengine.MaterialVerify.VerifyImageByID(ctx, req.MaterialID) if err != nil { @@ -340,7 +356,7 @@ func (c *MaterialVerifyController) ManualVerifyImage(ctx context.Context, req *M // ManualVerifyVideo 手动校验视频 func (c *MaterialVerifyController) ManualVerifyVideo(ctx context.Context, req *ManualVerifyReq) (res *ManualVerifyImageRes, err error) { - ctx = context.WithValue(ctx, "user", &beans.User{UserName: "admin", TenantId: 1}) + ctx = internal.WithAdminUser(ctx) log, err := serviceDataengine.MaterialVerify.VerifyVideoByID(ctx, req.MaterialID) if err != nil { @@ -368,7 +384,7 @@ type BatchVerifyRes struct { // BatchVerifyImage 批量校验图片 func (c *MaterialVerifyController) BatchVerifyImage(ctx context.Context, req *BatchVerifyReq) (res *BatchVerifyRes, err error) { - ctx = context.WithValue(ctx, "user", &beans.User{UserName: "admin", TenantId: 1}) + ctx = internal.WithAdminUser(ctx) if req.Limit <= 0 { req.Limit = 100 @@ -403,7 +419,7 @@ func (c *MaterialVerifyController) BatchVerifyImage(ctx context.Context, req *Ba // BatchVerifyVideo 批量校验视频 func (c *MaterialVerifyController) BatchVerifyVideo(ctx context.Context, req *BatchVerifyReq) (res *BatchVerifyRes, err error) { - ctx = context.WithValue(ctx, "user", &beans.User{UserName: "admin", TenantId: 1}) + ctx = internal.WithAdminUser(ctx) if req.Limit <= 0 { req.Limit = 100 @@ -456,7 +472,7 @@ type ListAccountsRes struct { // ListAccounts 获取所有启用的广告账户列表(用于前端下拉筛选) func (c *MaterialVerifyController) ListAccounts(ctx context.Context, req *ListAccountsReq) (res *ListAccountsRes, err error) { - ctx = context.WithValue(ctx, "user", &beans.User{UserName: "admin", TenantId: 1}) + ctx = internal.WithAdminUser(ctx) accounts, err := dao.TencentAccountRelation.GetAll(ctx) if err != nil { return nil, err @@ -506,7 +522,7 @@ type ExportRejectedRes struct { // ExportRejected 导出不通过的图片/视频数据(含失败原因) func (c *MaterialVerifyController) ExportRejected(ctx context.Context, req *ExportRejectedReq) (res *ExportRejectedRes, err error) { - ctx = context.WithValue(ctx, "user", &beans.User{UserName: "admin", TenantId: 1}) + ctx = internal.WithAdminUser(ctx) items, err := serviceDataengine.MaterialVerify.ExportRejectedData(ctx, req.MaterialType) if err != nil { @@ -546,32 +562,12 @@ type CallbackRes struct { Msg string `json:"msg"` } -// checkIP 校验请求IP是否在白名单内(context模式) -func checkIP(ctx context.Context) bool { - r := ghttp.RequestFromCtx(ctx) - if r == nil { - return true - } - allowedIPs := g.Cfg().MustGet(ctx, "yidun.callback_allowed_ips", "").String() - if allowedIPs == "" { - return true - } - clientIP := r.GetClientIp() - for _, ip := range strings.Split(allowedIPs, ",") { - if strings.TrimSpace(ip) == clientIP { - return true - } - } - g.Log().Warningf(ctx, "回调IP不在白名单中, clientIP=%s", clientIP) - return false -} - // ImageCallback 图片校验回调 func (c *MaterialVerifyController) ImageCallback(ctx context.Context, req *ImageCallbackReq) (res *CallbackRes, err error) { - if !checkIP(ctx) { + if !internal.CheckCallbackIP(ctx) { return &CallbackRes{Code: 403, Msg: "IP not allowed"}, nil } - ctx = context.WithValue(ctx, "user", &beans.User{UserName: "yidun_callback", TenantId: 1}) + ctx = internal.WithCallbackUser(ctx) if req.CallbackData == "" { return &CallbackRes{Code: 400, Msg: "callbackData不能为空"}, nil } @@ -581,15 +577,15 @@ func (c *MaterialVerifyController) ImageCallback(ctx context.Context, req *Image return &CallbackRes{Code: 500, Msg: err.Error()}, nil } - return &CallbackRes{Code: 200, Msg: "处理成功"}, nil + return &CallbackRes{Code: 0, Msg: "处理成功"}, nil } // VideoCallback 视频校验回调 func (c *MaterialVerifyController) VideoCallback(ctx context.Context, req *VideoCallbackReq) (res *CallbackRes, err error) { - if !checkIP(ctx) { + if !internal.CheckCallbackIP(ctx) { return &CallbackRes{Code: 403, Msg: "IP not allowed"}, nil } - ctx = context.WithValue(ctx, "user", &beans.User{UserName: "yidun_callback", TenantId: 1}) + ctx = internal.WithCallbackUser(ctx) if req.CallbackData == "" { return &CallbackRes{Code: 400, Msg: "callbackData不能为空"}, nil } @@ -599,7 +595,7 @@ func (c *MaterialVerifyController) VideoCallback(ctx context.Context, req *Video return &CallbackRes{Code: 500, Msg: err.Error()}, nil } - return &CallbackRes{Code: 200, Msg: "处理成功"}, nil + return &CallbackRes{Code: 0, Msg: "处理成功"}, nil } // ResultRes 结果查询响应 @@ -610,7 +606,7 @@ type ResultRes struct { // ImageResult 图片校验结果查询(轮询模式) func (c *MaterialVerifyController) ImageResult(ctx context.Context, req *TaskIDReq) (res *ResultRes, err error) { - ctx = context.WithValue(ctx, "user", &beans.User{UserName: "admin", TenantId: 1}) + ctx = internal.WithAdminUser(ctx) if req.TaskID == "" { return &ResultRes{Code: 400, Msg: "taskId不能为空"}, nil } @@ -620,12 +616,12 @@ func (c *MaterialVerifyController) ImageResult(ctx context.Context, req *TaskIDR return &ResultRes{Code: 500, Msg: err.Error()}, nil } - return &ResultRes{Code: 200, Msg: "处理成功"}, nil + return &ResultRes{Code: 0, Msg: "处理成功"}, nil } // VideoResult 视频校验结果查询(轮询模式) func (c *MaterialVerifyController) VideoResult(ctx context.Context, req *TaskIDReq) (res *ResultRes, err error) { - ctx = context.WithValue(ctx, "user", &beans.User{UserName: "admin", TenantId: 1}) + ctx = internal.WithAdminUser(ctx) if req.TaskID == "" { return &ResultRes{Code: 400, Msg: "taskId不能为空"}, nil } @@ -635,5 +631,5 @@ func (c *MaterialVerifyController) VideoResult(ctx context.Context, req *TaskIDR return &ResultRes{Code: 500, Msg: err.Error()}, nil } - return &ResultRes{Code: 200, Msg: "处理成功"}, nil + return &ResultRes{Code: 0, Msg: "处理成功"}, nil } diff --git a/controller/internal/util.go b/controller/internal/util.go new file mode 100644 index 0000000..a61f761 --- /dev/null +++ b/controller/internal/util.go @@ -0,0 +1,42 @@ +// Package internal 提供 controller 层的共享工具函数 +package internal + +import ( + "context" + "strings" + + "gitea.redpowerfuture.com/red-future/common/beans" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/net/ghttp" +) + +// WithAdminUser 在 context 中注入 admin 用户信息 +func WithAdminUser(ctx context.Context) context.Context { + return context.WithValue(ctx, "user", &beans.User{UserName: "admin", TenantId: 1}) +} + +// WithCallbackUser 在 context 中注入 yidun_callback 用户信息 +func WithCallbackUser(ctx context.Context) context.Context { + return context.WithValue(ctx, "user", &beans.User{UserName: "yidun_callback", TenantId: 1}) +} + +// CheckCallbackIP 校验回调请求 IP 是否在白名单内 +// 读取配置 yidun.callback_allowed_ips,若未配置则跳过校验 +func CheckCallbackIP(ctx context.Context) bool { + r := ghttp.RequestFromCtx(ctx) + if r == nil { + return true + } + allowedIPs := g.Cfg().MustGet(ctx, "yidun.callback_allowed_ips", "").String() + if allowedIPs == "" { + return true + } + clientIP := r.GetClientIp() + for _, ip := range strings.Split(allowedIPs, ",") { + if strings.TrimSpace(ip) == clientIP { + return true + } + } + g.Log().Warningf(ctx, "回调IP不在白名单中, clientIP=%s, allowedIPs=%s", clientIP, allowedIPs) + return false +} diff --git a/controller/yidun/content_check_controller.go b/controller/yidun/content_check_controller.go index 2f0e234..998d3ed 100644 --- a/controller/yidun/content_check_controller.go +++ b/controller/yidun/content_check_controller.go @@ -1,6 +1,7 @@ package yidun import ( + internal "cid/controller/internal" dto "cid/model/dto/yidun" serviceDataengine "cid/service/dataengine" "context" @@ -24,7 +25,7 @@ type StatusRes struct { // Start 启动送检服务 func (c *ContentCheckController) Start(ctx context.Context, req *dto.StartCheckReq) (res *beans.ResponseEmpty, err error) { - ctx = context.WithValue(ctx, "user", &beans.User{UserName: "admin", TenantId: 1}) + ctx = internal.WithAdminUser(ctx) if serviceDataengine.TencentContentCheck.IsRunning() { return &beans.ResponseEmpty{}, nil @@ -50,14 +51,14 @@ func (c *ContentCheckController) Start(ctx context.Context, req *dto.StartCheckR // Stop 停止送检服务 func (c *ContentCheckController) Stop(ctx context.Context, req *dto.EmptyReq) (res *beans.ResponseEmpty, err error) { - ctx = context.WithValue(ctx, "user", &beans.User{UserName: "admin", TenantId: 1}) + ctx = internal.WithAdminUser(ctx) serviceDataengine.TencentContentCheck.Stop(ctx) return } // Status 获取送检服务状态 func (c *ContentCheckController) Status(ctx context.Context, req *dto.EmptyReq) (res *StatusRes, err error) { - ctx = context.WithValue(ctx, "user", &beans.User{UserName: "admin", TenantId: 1}) + ctx = internal.WithAdminUser(ctx) res = &StatusRes{ Running: serviceDataengine.TencentContentCheck.IsRunning(), @@ -69,7 +70,7 @@ func (c *ContentCheckController) Status(ctx context.Context, req *dto.EmptyReq) // ProcessImageCallback 处理图片检测回调 func (c *ContentCheckController) ProcessImageCallback(ctx context.Context, req *dto.ProcessImageCallbackReq) (res *beans.ResponseEmpty, err error) { - ctx = context.WithValue(ctx, "user", &beans.User{UserName: "admin", TenantId: 1}) + ctx = internal.WithAdminUser(ctx) if req.CallbackData == "" { return nil, fmt.Errorf("callbackData不能为空") @@ -81,7 +82,7 @@ func (c *ContentCheckController) ProcessImageCallback(ctx context.Context, req * // ProcessVideoCallback 处理视频检测回调 func (c *ContentCheckController) ProcessVideoCallback(ctx context.Context, req *dto.ProcessVideoCallbackReq) (res *beans.ResponseEmpty, err error) { - ctx = context.WithValue(ctx, "user", &beans.User{UserName: "admin", TenantId: 1}) + ctx = internal.WithAdminUser(ctx) if req.CallbackData == "" { return nil, fmt.Errorf("callbackData不能为空") @@ -93,7 +94,7 @@ func (c *ContentCheckController) ProcessVideoCallback(ctx context.Context, req * // ProcessImageResult 查询并处理图片检测结果(轮询模式) func (c *ContentCheckController) ProcessImageResult(ctx context.Context, req *dto.ProcessImageResultReq) (res *beans.ResponseEmpty, err error) { - ctx = context.WithValue(ctx, "user", &beans.User{UserName: "admin", TenantId: 1}) + ctx = internal.WithAdminUser(ctx) if req.TaskID == "" { return nil, fmt.Errorf("taskId不能为空") @@ -105,7 +106,7 @@ func (c *ContentCheckController) ProcessImageResult(ctx context.Context, req *dt // ProcessVideoResult 查询并处理视频检测结果(轮询模式) func (c *ContentCheckController) ProcessVideoResult(ctx context.Context, req *dto.ProcessVideoResultReq) (res *beans.ResponseEmpty, err error) { - ctx = context.WithValue(ctx, "user", &beans.User{UserName: "admin", TenantId: 1}) + ctx = internal.WithAdminUser(ctx) if req.TaskID == "" { return nil, fmt.Errorf("taskId不能为空") @@ -117,7 +118,7 @@ func (c *ContentCheckController) ProcessVideoResult(ctx context.Context, req *dt // ManualSubmitImageByID 根据图片ID手动提交送检 func (c *ContentCheckController) ManualSubmitImageByID(ctx context.Context, req *dto.ManualSubmitImageByIDReq) (res *dto.ManualSubmitRes, err error) { - ctx = context.WithValue(ctx, "user", &beans.User{UserName: "admin", TenantId: 1}) + ctx = internal.WithAdminUser(ctx) result, err := serviceDataengine.TencentContentCheck.SubmitImageByID(ctx, req.ImageID) if err != nil { @@ -132,7 +133,7 @@ func (c *ContentCheckController) ManualSubmitImageByID(ctx context.Context, req // ManualSubmitVideoByID 根据视频ID手动提交送检 func (c *ContentCheckController) ManualSubmitVideoByID(ctx context.Context, req *dto.ManualSubmitVideoByIDReq) (res *dto.ManualSubmitRes, err error) { - ctx = context.WithValue(ctx, "user", &beans.User{UserName: "admin", TenantId: 1}) + ctx = internal.WithAdminUser(ctx) result, err := serviceDataengine.TencentContentCheck.SubmitVideoByID(ctx, req.VideoID) if err != nil { @@ -147,7 +148,7 @@ func (c *ContentCheckController) ManualSubmitVideoByID(ctx context.Context, req // GetImageCheckLogs 获取图片的送检日志 func (c *ContentCheckController) GetImageCheckLogs(ctx context.Context, req *dto.GetImageCheckLogsReq) (res *dto.GetCheckLogsRes, err error) { - ctx = context.WithValue(ctx, "user", &beans.User{UserName: "admin", TenantId: 1}) + ctx = internal.WithAdminUser(ctx) logs, err := serviceDataengine.TencentContentCallback.GetCheckLogsByImageID(ctx, req.ImageID) if err != nil { @@ -162,7 +163,7 @@ func (c *ContentCheckController) GetImageCheckLogs(ctx context.Context, req *dto // GetVideoCheckLogs 获取视频的送检日志 func (c *ContentCheckController) GetVideoCheckLogs(ctx context.Context, req *dto.GetVideoCheckLogsReq) (res *dto.GetCheckLogsRes, err error) { - ctx = context.WithValue(ctx, "user", &beans.User{UserName: "admin", TenantId: 1}) + ctx = internal.WithAdminUser(ctx) logs, err := serviceDataengine.TencentContentCallback.GetCheckLogsByVideoID(ctx, req.VideoID) if err != nil { diff --git a/controller/yidun/yidun_callback_controller.go b/controller/yidun/yidun_callback_controller.go index 9cb5b9c..6161a53 100644 --- a/controller/yidun/yidun_callback_controller.go +++ b/controller/yidun/yidun_callback_controller.go @@ -1,12 +1,10 @@ package yidun import ( + internal "cid/controller/internal" dataengineService "cid/service/dataengine" - "context" "fmt" - "strings" - "gitea.redpowerfuture.com/red-future/common/beans" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" ) @@ -42,13 +40,13 @@ type PollResult struct { // Body: callbackData={"antispam":{...}} func (c *YidunCallbackController) ReceiveImageCallback(r *ghttp.Request) { // IP 白名单校验 - if !checkCallbackIP(r) { + if !internal.CheckCallbackIP(r.Context()) { r.Response.WriteJson(CallbackResult{Code: 403, Msg: "IP not allowed"}) return } ctx := r.Context() - ctx = context.WithValue(ctx, "user", &beans.User{UserName: "yidun_callback", TenantId: 1}) + ctx = internal.WithCallbackUser(ctx) // 易盾推送的数据在请求体中 var callbackData string @@ -86,7 +84,7 @@ func (c *YidunCallbackController) ReceiveImageCallback(r *ghttp.Request) { return } - r.Response.WriteJson(CallbackResult{Code: 200, Msg: "success"}) + r.Response.WriteJson(CallbackResult{Code: 0, Msg: "success"}) } // ReceiveVideoCallback 接收易盾视频检测结果推送 @@ -94,13 +92,13 @@ func (c *YidunCallbackController) ReceiveImageCallback(r *ghttp.Request) { // Body: callbackData={"antispam":{...}} func (c *YidunCallbackController) ReceiveVideoCallback(r *ghttp.Request) { // IP 白名单校验 - if !checkCallbackIP(r) { + if !internal.CheckCallbackIP(r.Context()) { r.Response.WriteJson(CallbackResult{Code: 403, Msg: "IP not allowed"}) return } ctx := r.Context() - ctx = context.WithValue(ctx, "user", &beans.User{UserName: "yidun_callback", TenantId: 1}) + ctx = internal.WithCallbackUser(ctx) // 易盾推送的数据在请求体中 var callbackData string @@ -138,7 +136,7 @@ func (c *YidunCallbackController) ReceiveVideoCallback(r *ghttp.Request) { return } - r.Response.WriteJson(CallbackResult{Code: 200, Msg: "success"}) + r.Response.WriteJson(CallbackResult{Code: 0, Msg: "success"}) } // ============================================================================= @@ -149,35 +147,40 @@ func (c *YidunCallbackController) ReceiveVideoCallback(r *ghttp.Request) { // 格式: POST /yidun/callback/poll func (c *YidunCallbackController) PollAllResults(r *ghttp.Request) { ctx := r.Context() - ctx = context.WithValue(ctx, "user", &beans.User{UserName: "admin", TenantId: 1}) + ctx = internal.WithAdminUser(ctx) g.Log().Info(ctx, "开始轮询所有待查询的检测结果...") // 先获取待处理数量 - pendingCount, _ := dataengineService.MaterialVerify.GetPendingResultsCount(ctx) // 执行轮询 successCount, failCount, err := dataengineService.MaterialVerify.PollPendingResults(ctx) - result := PollResult{ - SuccessCount: successCount, - FailCount: failCount, - PendingCount: pendingCount - successCount, - } + // 轮询后再查一下剩余待处理的明细 + pendingItems, _ := dataengineService.MaterialVerify.GetPendingResultsDetail(ctx, 50) + + msg := fmt.Sprintf("✅ 成功处理 %d 条 | ❌ 失败 %d 条 | ⏳ 还剩 %d 条待处理", + successCount, failCount, len(pendingItems)) if err != nil { r.Response.WriteJson(CallbackResult{ Code: 500, - Msg: fmt.Sprintf("轮询完成但有错误: %v", err), - Data: result, + Msg: fmt.Sprintf("部分完成,但有错误: %v", err), + Data: g.Map{ + "summary": g.Map{"success": successCount, "fail": failCount, "pending": len(pendingItems)}, + "pending_detail": pendingItems, + }, }) return } r.Response.WriteJson(CallbackResult{ - Code: 200, - Msg: fmt.Sprintf("轮询完成,成功处理 %d 条,失败 %d 条", successCount, failCount), - Data: result, + Code: 0, + Msg: msg, + Data: g.Map{ + "summary": g.Map{"success": successCount, "fail": failCount, "pending": len(pendingItems)}, + "pending_detail": pendingItems, + }, }) } @@ -185,7 +188,7 @@ func (c *YidunCallbackController) PollAllResults(r *ghttp.Request) { // 格式: POST /yidun/callback/pollImage func (c *YidunCallbackController) PollImageResults(r *ghttp.Request) { ctx := r.Context() - ctx = context.WithValue(ctx, "user", &beans.User{UserName: "admin", TenantId: 1}) + ctx = internal.WithAdminUser(ctx) g.Log().Info(ctx, "开始轮询图片待查询的检测结果...") @@ -201,7 +204,7 @@ func (c *YidunCallbackController) PollImageResults(r *ghttp.Request) { } r.Response.WriteJson(CallbackResult{ - Code: 200, + Code: 0, Msg: fmt.Sprintf("轮询完成,成功处理 %d 条,失败 %d 条", successCount, failCount), Data: PollResult{SuccessCount: successCount, FailCount: failCount}, }) @@ -211,7 +214,7 @@ func (c *YidunCallbackController) PollImageResults(r *ghttp.Request) { // 格式: POST /yidun/callback/pollVideo func (c *YidunCallbackController) PollVideoResults(r *ghttp.Request) { ctx := r.Context() - ctx = context.WithValue(ctx, "user", &beans.User{UserName: "admin", TenantId: 1}) + ctx = internal.WithAdminUser(ctx) g.Log().Info(ctx, "开始轮询视频待查询的检测结果...") @@ -227,7 +230,7 @@ func (c *YidunCallbackController) PollVideoResults(r *ghttp.Request) { } r.Response.WriteJson(CallbackResult{ - Code: 200, + Code: 0, Msg: fmt.Sprintf("轮询完成,成功处理 %d 条,失败 %d 条", successCount, failCount), Data: PollResult{SuccessCount: successCount, FailCount: failCount}, }) @@ -237,7 +240,7 @@ func (c *YidunCallbackController) PollVideoResults(r *ghttp.Request) { // 格式: POST /yidun/callback/pollTask func (c *YidunCallbackController) PollByTaskID(r *ghttp.Request) { ctx := r.Context() - ctx = context.WithValue(ctx, "user", &beans.User{UserName: "admin", TenantId: 1}) + ctx = internal.WithAdminUser(ctx) taskID := r.Get("taskId", "").String() taskType := r.Get("type", "").String() // image 或 video @@ -268,14 +271,41 @@ func (c *YidunCallbackController) PollByTaskID(r *ghttp.Request) { return } - r.Response.WriteJson(CallbackResult{Code: 200, Msg: "查询并处理成功"}) + r.Response.WriteJson(CallbackResult{Code: 0, Msg: "查询并处理成功"}) +} + +// PendingListRes 待查询结果明细 +type PendingListRes struct { + Total int `json:"total"` + List []dataengineService.PendingResultItem `json:"list"` +} + +// GetPendingDetail 获取待查询结果的明细 +// 格式: GET /yidun/callback/pendingDetail +func (c *YidunCallbackController) GetPendingDetail(r *ghttp.Request) { + ctx := r.Context() + ctx = internal.WithAdminUser(ctx) + + items, err := dataengineService.MaterialVerify.GetPendingResultsDetail(ctx, 50) + if err != nil { + r.Response.WriteJson(CallbackResult{Code: 500, Msg: err.Error()}) + return + } + + r.Response.WriteJson(g.Map{ + "code": 0, + "data": PendingListRes{ + Total: len(items), + List: items, + }, + }) } // GetPendingCount 获取待查询结果的数量 // 格式: GET /yidun/callback/pendingCount func (c *YidunCallbackController) GetPendingCount(r *ghttp.Request) { ctx := r.Context() - ctx = context.WithValue(ctx, "user", &beans.User{UserName: "admin", TenantId: 1}) + ctx = internal.WithAdminUser(ctx) count, err := dataengineService.MaterialVerify.GetPendingResultsCount(ctx) if err != nil { @@ -284,7 +314,7 @@ func (c *YidunCallbackController) GetPendingCount(r *ghttp.Request) { } r.Response.WriteJson(g.Map{ - "code": 200, + "code": 0, "data": g.Map{ "pending_count": count, "description": "待查询结果的日志数量(状态为pending且有taskID)", @@ -300,7 +330,7 @@ func (c *YidunCallbackController) GetPendingCount(r *ghttp.Request) { // 格式: POST /yidun/callback/processImage func (c *YidunCallbackController) ProcessImageCallback(r *ghttp.Request) { ctx := r.Context() - ctx = context.WithValue(ctx, "user", &beans.User{UserName: "admin", TenantId: 1}) + ctx = internal.WithAdminUser(ctx) var req struct { CallbackData string `json:"callbackData" v:"required#回调数据不能为空"` @@ -317,14 +347,14 @@ func (c *YidunCallbackController) ProcessImageCallback(r *ghttp.Request) { return } - r.Response.WriteJson(CallbackResult{Code: 200, Msg: "success"}) + r.Response.WriteJson(CallbackResult{Code: 0, Msg: "success"}) } // ProcessVideoCallback 手动处理视频回调(兼容旧接口) // 格式: POST /yidun/callback/processVideo func (c *YidunCallbackController) ProcessVideoCallback(r *ghttp.Request) { ctx := r.Context() - ctx = context.WithValue(ctx, "user", &beans.User{UserName: "admin", TenantId: 1}) + ctx = internal.WithAdminUser(ctx) var req struct { CallbackData string `json:"callbackData" v:"required#回调数据不能为空"` @@ -341,7 +371,7 @@ func (c *YidunCallbackController) ProcessVideoCallback(r *ghttp.Request) { return } - r.Response.WriteJson(CallbackResult{Code: 200, Msg: "success"}) + r.Response.WriteJson(CallbackResult{Code: 0, Msg: "success"}) } // toString 转换interface{}为string @@ -351,19 +381,3 @@ func toString(v interface{}) string { } return "" } - -// checkCallbackIP 校验回调请求IP是否在白名单内 -func checkCallbackIP(r *ghttp.Request) bool { - allowedIPs := g.Cfg().MustGet(r.Context(), "yidun.callback_allowed_ips", "").String() - if allowedIPs == "" { - return true // 未配置白名单,跳过校验 - } - clientIP := r.GetClientIp() - for _, ip := range strings.Split(allowedIPs, ",") { - if strings.TrimSpace(ip) == clientIP { - return true - } - } - g.Log().Warningf(r.Context(), "回调IP不在白名单中, clientIP=%s, allowedIPs=%s", clientIP, allowedIPs) - return false -} diff --git a/controller/yidun/yidun_controller.go b/controller/yidun/yidun_controller.go index 88a91b5..e745818 100644 --- a/controller/yidun/yidun_controller.go +++ b/controller/yidun/yidun_controller.go @@ -1,10 +1,11 @@ package yidun import ( + internal "cid/controller/internal" + serviceDataengine "cid/service/dataengine" "cid/service/yidun" "context" - "gitea.redpowerfuture.com/red-future/common/beans" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/net/ghttp" "github.com/yidun/yidun-golang-sdk/yidun/service/antispam/text/v5/check/async/single" @@ -39,7 +40,7 @@ type DetectVideoReq struct { // DetectText 文本检测 func (c *yidunController) DetectText(ctx context.Context, req *DetectTextReq) (string, error) { - ctx = context.WithValue(ctx, "user", &beans.User{UserName: "admin", TenantId: 1}) + ctx = internal.WithAdminUser(ctx) businessId := g.Cfg().MustGet(ctx, "yidun.text.business_id").String() sdkReq := single.NewTextAsyncCheckRequest(businessId) @@ -57,13 +58,13 @@ func (c *yidunController) DetectText(ctx context.Context, req *DetectTextReq) (s // DetectImage 图片检测 func (c *yidunController) DetectImage(ctx context.Context, req *DetectImageReq) (*yidun.ImageSubmitResult, error) { - ctx = context.WithValue(ctx, "user", &beans.User{UserName: "admin", TenantId: 1}) + ctx = internal.WithAdminUser(ctx) return yidun.ImageDetection.DetectImage(ctx, req.ImageURL, req.DataID, req.CallbackURL) } // DetectVideo 视频检测 func (c *yidunController) DetectVideo(ctx context.Context, req *DetectVideoReq) (*yidun.VideoSubmitResult, error) { - ctx = context.WithValue(ctx, "user", &beans.User{UserName: "admin", TenantId: 1}) + ctx = internal.WithAdminUser(ctx) return yidun.VideoDetection.DetectVideo(ctx, req.VideoURL, req.DataID, req.CallbackURL) } @@ -76,13 +77,13 @@ type ImageCallbackResult struct { // ReceiveImageCallback 接收图片检测结果推送 func (c *yidunController) ReceiveImageCallback(r *ghttp.Request) { // IP 白名单校验 - if !checkCallbackIP(r) { + if !internal.CheckCallbackIP(r.Context()) { r.Response.WriteJson(ImageCallbackResult{Code: 403, Msg: "IP not allowed"}) return } ctx := r.Context() - ctx = context.WithValue(ctx, "user", &beans.User{UserName: "admin", TenantId: 1}) + ctx = internal.WithAdminUser(ctx) callbackData := r.GetForm("callbackData", "").String() if callbackData == "" { @@ -90,14 +91,14 @@ func (c *yidunController) ReceiveImageCallback(r *ghttp.Request) { return } - err := yidun.ImageDetection.ProcessImageCallback(ctx, callbackData) + err := serviceDataengine.MaterialVerify.ProcessImageCallback(ctx, callbackData) if err != nil { g.Log().Errorf(ctx, "处理图片检测回调失败: %v", err) r.Response.WriteJson(ImageCallbackResult{Code: 500, Msg: err.Error()}) return } - r.Response.WriteJson(ImageCallbackResult{Code: 200, Msg: "success"}) + r.Response.WriteJson(ImageCallbackResult{Code: 0, Msg: "success"}) } // VideoCallbackResult 视频检测回调响应 @@ -109,13 +110,13 @@ type VideoCallbackResult struct { // ReceiveVideoCallback 接收视频检测结果推送 func (c *yidunController) ReceiveVideoCallback(r *ghttp.Request) { // IP 白名单校验 - if !checkCallbackIP(r) { + if !internal.CheckCallbackIP(r.Context()) { r.Response.WriteJson(VideoCallbackResult{Code: 403, Msg: "IP not allowed"}) return } ctx := r.Context() - ctx = context.WithValue(ctx, "user", &beans.User{UserName: "admin", TenantId: 1}) + ctx = internal.WithAdminUser(ctx) callbackData := r.GetForm("callbackData", "").String() if callbackData == "" { @@ -123,20 +124,20 @@ func (c *yidunController) ReceiveVideoCallback(r *ghttp.Request) { return } - err := yidun.VideoDetection.ProcessVideoCallback(ctx, callbackData) + err := serviceDataengine.MaterialVerify.ProcessVideoCallback(ctx, callbackData) if err != nil { g.Log().Errorf(ctx, "处理视频检测回调失败: %v", err) r.Response.WriteJson(VideoCallbackResult{Code: 500, Msg: err.Error()}) return } - r.Response.WriteJson(VideoCallbackResult{Code: 200, Msg: "success"}) + r.Response.WriteJson(VideoCallbackResult{Code: 0, Msg: "success"}) } // GetVideoResult 获取视频检测结果 func (c *yidunController) GetVideoResult(r *ghttp.Request) { ctx := r.Context() - ctx = context.WithValue(ctx, "user", &beans.User{UserName: "admin", TenantId: 1}) + ctx = internal.WithAdminUser(ctx) taskId := r.Get("taskId", "").String() if taskId == "" { @@ -157,7 +158,7 @@ func (c *yidunController) GetVideoResult(r *ghttp.Request) { // GetImageResult 获取图片检测结果 func (c *yidunController) GetImageResult(r *ghttp.Request) { ctx := r.Context() - ctx = context.WithValue(ctx, "user", &beans.User{UserName: "admin", TenantId: 1}) + ctx = internal.WithAdminUser(ctx) taskId := r.Get("taskId", "").String() if taskId == "" { diff --git a/dao/dataengine/material_verify_log_dao.go b/dao/dataengine/material_verify_log_dao.go index 8757b99..d8c21a6 100644 --- a/dao/dataengine/material_verify_log_dao.go +++ b/dao/dataengine/material_verify_log_dao.go @@ -1,11 +1,9 @@ package dataengine import ( - consts "cid/consts/dataengine" daoEntity "cid/model/entity/dataengine" "context" - "gitea.redpowerfuture.com/red-future/common/db/gfdb" "github.com/bwmarrin/snowflake" "github.com/gogf/gf/v2/frame/g" ) @@ -51,7 +49,7 @@ func (d *MaterialVerifyLogDAO) Create(ctx context.Context, log *daoEntity.Materi // GetByID 根据ID获取日志 func (d *MaterialVerifyLogDAO) GetByID(ctx context.Context, id int64) (*daoEntity.MaterialVerifyLog, error) { var result daoEntity.MaterialVerifyLog - r, err := gfdb.DB(ctx, "cid").Model(ctx, MaterialVerifyLogTable). + r, err := g.DB("cid").Model(MaterialVerifyLogTable). Where(daoEntity.MaterialVerifyLogCols.Id, id). One() if err != nil { @@ -69,7 +67,7 @@ func (d *MaterialVerifyLogDAO) GetByID(ctx context.Context, id int64) (*daoEntit // GetByTaskID 根据任务ID获取日志 func (d *MaterialVerifyLogDAO) GetByTaskID(ctx context.Context, taskID string) (*daoEntity.MaterialVerifyLog, error) { var result daoEntity.MaterialVerifyLog - r, err := gfdb.DB(ctx, "cid").Model(ctx, MaterialVerifyLogTable). + r, err := g.DB("cid").Model(MaterialVerifyLogTable). Where(daoEntity.MaterialVerifyLogCols.TaskID, taskID). One() if err != nil { @@ -87,7 +85,7 @@ func (d *MaterialVerifyLogDAO) GetByTaskID(ctx context.Context, taskID string) ( // GetByMaterialID 根据素材ID获取日志列表 func (d *MaterialVerifyLogDAO) GetByMaterialID(ctx context.Context, materialID string) ([]daoEntity.MaterialVerifyLog, error) { var result []daoEntity.MaterialVerifyLog - r, err := gfdb.DB(ctx, "cid").Model(ctx, MaterialVerifyLogTable). + r, err := g.DB("cid").Model(MaterialVerifyLogTable). Where(daoEntity.MaterialVerifyLogCols.MaterialID, materialID). OrderDesc(daoEntity.MaterialVerifyLogCols.CreatedAt). All() @@ -103,7 +101,7 @@ func (d *MaterialVerifyLogDAO) GetByMaterialID(ctx context.Context, materialID s // GetBySource 根据来源获取日志 func (d *MaterialVerifyLogDAO) GetBySource(ctx context.Context, sourceTable string, sourceID int64) ([]daoEntity.MaterialVerifyLog, error) { var result []daoEntity.MaterialVerifyLog - r, err := gfdb.DB(ctx, "cid").Model(ctx, MaterialVerifyLogTable). + r, err := g.DB("cid").Model(MaterialVerifyLogTable). Where(daoEntity.MaterialVerifyLogCols.SourceTable, sourceTable). Where(daoEntity.MaterialVerifyLogCols.SourceID, sourceID). OrderDesc(daoEntity.MaterialVerifyLogCols.CreatedAt). @@ -119,7 +117,7 @@ func (d *MaterialVerifyLogDAO) GetBySource(ctx context.Context, sourceTable stri // UpdateVerifyResult 更新校验结果 func (d *MaterialVerifyLogDAO) UpdateVerifyResult(ctx context.Context, id int64, verifyStatus string, suggestion, label, resultType int, responseResult string, checkTime int64) error { - _, err := gfdb.DB(ctx, "cid").Model(ctx, MaterialVerifyLogTable). + _, err := g.DB("cid").Model(MaterialVerifyLogTable). Where(daoEntity.MaterialVerifyLogCols.Id, id). Data(g.Map{ daoEntity.MaterialVerifyLogCols.VerifyStatus: verifyStatus, @@ -138,7 +136,7 @@ func (d *MaterialVerifyLogDAO) UpdateVerifyResult(ctx context.Context, id int64, // UpdateError 更新错误信息 func (d *MaterialVerifyLogDAO) UpdateError(ctx context.Context, id int64, verifyStatus string, errorMsg string) error { - _, err := gfdb.DB(ctx, "cid").Model(ctx, MaterialVerifyLogTable). + _, err := g.DB("cid").Model(MaterialVerifyLogTable). Where(daoEntity.MaterialVerifyLogCols.Id, id). Data(g.Map{ daoEntity.MaterialVerifyLogCols.VerifyStatus: verifyStatus, @@ -153,7 +151,7 @@ func (d *MaterialVerifyLogDAO) UpdateError(ctx context.Context, id int64, verify // UpdateTaskID 更新任务ID func (d *MaterialVerifyLogDAO) UpdateTaskID(ctx context.Context, id int64, taskID string) error { - _, err := gfdb.DB(ctx, "cid").Model(ctx, MaterialVerifyLogTable). + _, err := g.DB("cid").Model(MaterialVerifyLogTable). Where(daoEntity.MaterialVerifyLogCols.Id, id). Data(g.Map{ daoEntity.MaterialVerifyLogCols.TaskID: taskID, @@ -166,7 +164,7 @@ func (d *MaterialVerifyLogDAO) UpdateTaskID(ctx context.Context, id int64, taskI // UpdateDuration 更新处理耗时 func (d *MaterialVerifyLogDAO) UpdateDuration(ctx context.Context, id int64, durationMs int64) error { - _, err := gfdb.DB(ctx, "cid").Model(ctx, MaterialVerifyLogTable). + _, err := g.DB("cid").Model(MaterialVerifyLogTable). Where(daoEntity.MaterialVerifyLogCols.Id, id). Data(g.Map{ daoEntity.MaterialVerifyLogCols.DurationMs: durationMs, @@ -179,7 +177,7 @@ func (d *MaterialVerifyLogDAO) UpdateDuration(ctx context.Context, id int64, dur // UpdateRequestParams 更新请求参数 func (d *MaterialVerifyLogDAO) UpdateRequestParams(ctx context.Context, id int64, requestParams string) error { - _, err := gfdb.DB(ctx, "cid").Model(ctx, MaterialVerifyLogTable). + _, err := g.DB("cid").Model(MaterialVerifyLogTable). Where(daoEntity.MaterialVerifyLogCols.Id, id). Data(g.Map{ daoEntity.MaterialVerifyLogCols.RequestParams: requestParams, @@ -193,7 +191,7 @@ func (d *MaterialVerifyLogDAO) UpdateRequestParams(ctx context.Context, id int64 // GetByCondition 根据条件分页查询 func (d *MaterialVerifyLogDAO) GetByCondition(ctx context.Context, condition map[string]interface{}, page, pageSize int) ([]daoEntity.MaterialVerifyLog, int, error) { var result []daoEntity.MaterialVerifyLog - m := gfdb.DB(ctx, "cid").Model(ctx, MaterialVerifyLogTable) + m := g.DB("cid").Model(MaterialVerifyLogTable) for k, v := range condition { m.Where(k, v) @@ -219,7 +217,7 @@ func (d *MaterialVerifyLogDAO) GetByCondition(ctx context.Context, condition map // CountByStatus 按状态统计 func (d *MaterialVerifyLogDAO) CountByStatus(ctx context.Context, verifyStatus string) (int, error) { - count, err := gfdb.DB(ctx, "cid").Model(ctx, MaterialVerifyLogTable). + count, err := g.DB("cid").Model(MaterialVerifyLogTable). Where(daoEntity.MaterialVerifyLogCols.VerifyStatus, verifyStatus). Count() if err != nil { @@ -259,8 +257,8 @@ func (d *MaterialVerifyLogDAO) GetStats(ctx context.Context) (map[string]int, er func (d *MaterialVerifyLogDAO) GetPendingResults(ctx context.Context, limit int) ([]daoEntity.MaterialVerifyLog, error) { var result []daoEntity.MaterialVerifyLog - r, err := gfdb.DB(ctx, "cid").Model(ctx, MaterialVerifyLogTable). - Where(daoEntity.MaterialVerifyLogCols.VerifyStatus, consts.CheckStatusPending). + r, err := g.DB("cid").Model(MaterialVerifyLogTable). + Where(daoEntity.MaterialVerifyLogCols.VerifyStatus, daoEntity.VerifyStatusPending). WhereNotNull(daoEntity.MaterialVerifyLogCols.TaskID). Where(daoEntity.MaterialVerifyLogCols.TaskID + " != ''"). OrderAsc(daoEntity.MaterialVerifyLogCols.CreatedAt). @@ -280,7 +278,7 @@ func (d *MaterialVerifyLogDAO) GetPendingResults(ctx context.Context, limit int) // GetLastRejectedLogByMaterialID 根据素材ID获取最后一条失败的校验日志 func (d *MaterialVerifyLogDAO) GetLastRejectedLogByMaterialID(ctx context.Context, materialID string, verifyStatus string) (*daoEntity.MaterialVerifyLog, error) { var result daoEntity.MaterialVerifyLog - r, err := gfdb.DB(ctx, "cid").Model(ctx, MaterialVerifyLogTable). + r, err := g.DB("cid").Model(MaterialVerifyLogTable). Where(daoEntity.MaterialVerifyLogCols.MaterialID, materialID). Where(daoEntity.MaterialVerifyLogCols.VerifyStatus, verifyStatus). OrderDesc(daoEntity.MaterialVerifyLogCols.CreatedAt). @@ -297,10 +295,24 @@ func (d *MaterialVerifyLogDAO) GetLastRejectedLogByMaterialID(ctx context.Contex return &result, nil } +// UpdateRiskDescription 更新风险描述 +func (d *MaterialVerifyLogDAO) UpdateRiskDescription(ctx context.Context, id int64, riskDescription string) error { + _, err := g.DB("cid").Model(MaterialVerifyLogTable). + Where(daoEntity.MaterialVerifyLogCols.Id, id). + Data(g.Map{ + daoEntity.MaterialVerifyLogCols.RiskDescription: riskDescription, + }).Update() + if err != nil { + g.Log().Errorf(ctx, "更新风险描述失败: %v", err) + return err + } + return nil +} + // CountPendingResults 统计待查询结果的数量 func (d *MaterialVerifyLogDAO) CountPendingResults(ctx context.Context) (int, error) { - count, err := gfdb.DB(ctx, "cid").Model(ctx, MaterialVerifyLogTable). - Where(daoEntity.MaterialVerifyLogCols.VerifyStatus, consts.CheckStatusPending). + count, err := g.DB("cid").Model(MaterialVerifyLogTable). + Where(daoEntity.MaterialVerifyLogCols.VerifyStatus, daoEntity.VerifyStatusPending). WhereNotNull(daoEntity.MaterialVerifyLogCols.TaskID). Where(daoEntity.MaterialVerifyLogCols.TaskID + " != ''"). Count() diff --git a/dao/dataengine/tencent_content_check_log_dao.go b/dao/dataengine/tencent_content_check_log_dao.go index c394904..15cead4 100644 --- a/dao/dataengine/tencent_content_check_log_dao.go +++ b/dao/dataengine/tencent_content_check_log_dao.go @@ -7,7 +7,6 @@ import ( "context" "encoding/json" - "gitea.redpowerfuture.com/red-future/common/db/gfdb" "github.com/bwmarrin/snowflake" "github.com/gogf/gf/v2/frame/g" ) @@ -53,7 +52,7 @@ func (d *TencentContentCheckLogDAO) Create(ctx context.Context, log *entity.Tenc // UpdateStatus 更新送检状态 func (d *TencentContentCheckLogDAO) UpdateStatus(ctx context.Context, id int64, status string, responseData string, failReason string) error { - _, err := gfdb.DB(ctx, "cid").Model(ctx, consts.TencentContentCheckLogTable). + _, err := g.DB("cid").Model(consts.TencentContentCheckLogTable). Where("id", id). Data(g.Map{ "status": status, @@ -65,7 +64,7 @@ func (d *TencentContentCheckLogDAO) UpdateStatus(ctx context.Context, id int64, // UpdateCheckResult 更新检测结果 func (d *TencentContentCheckLogDAO) UpdateCheckResult(ctx context.Context, id int64, suggestion, label, resultType int, checkTime int64) error { - _, err := gfdb.DB(ctx, "cid").Model(ctx, consts.TencentContentCheckLogTable). + _, err := g.DB("cid").Model(consts.TencentContentCheckLogTable). Where("id", id). Data(g.Map{ "status": consts.CheckStatusCompleted, @@ -80,7 +79,7 @@ func (d *TencentContentCheckLogDAO) UpdateCheckResult(ctx context.Context, id in // GetByID 根据ID获取日志 func (d *TencentContentCheckLogDAO) GetByID(ctx context.Context, id int64) (*entity.TencentContentCheckLog, error) { var result entity.TencentContentCheckLog - r, err := gfdb.DB(ctx, "cid").Model(ctx, consts.TencentContentCheckLogTable). + r, err := g.DB("cid").Model(consts.TencentContentCheckLogTable). Where("id", id). One() if err != nil { @@ -98,7 +97,7 @@ func (d *TencentContentCheckLogDAO) GetByID(ctx context.Context, id int64) (*ent // GetBySourceID 根据来源ID获取日志 func (d *TencentContentCheckLogDAO) GetBySourceID(ctx context.Context, sourceTable string, sourceID int64) ([]entity.TencentContentCheckLog, error) { var result []entity.TencentContentCheckLog - r, err := gfdb.DB(ctx, "cid").Model(ctx, consts.TencentContentCheckLogTable). + r, err := g.DB("cid").Model(consts.TencentContentCheckLogTable). Where("source_table", sourceTable). Where("source_id", sourceID). OrderDesc("created_at"). @@ -115,7 +114,7 @@ func (d *TencentContentCheckLogDAO) GetBySourceID(ctx context.Context, sourceTab // GetByTaskID 根据任务ID获取日志 func (d *TencentContentCheckLogDAO) GetByTaskID(ctx context.Context, taskID string) (*entity.TencentContentCheckLog, error) { var result entity.TencentContentCheckLog - r, err := gfdb.DB(ctx, "cid").Model(ctx, consts.TencentContentCheckLogTable). + r, err := g.DB("cid").Model(consts.TencentContentCheckLogTable). Where("task_id", taskID). One() if err != nil { @@ -133,7 +132,7 @@ func (d *TencentContentCheckLogDAO) GetByTaskID(ctx context.Context, taskID stri // ListByStatus 根据状态获取日志列表 func (d *TencentContentCheckLogDAO) ListByStatus(ctx context.Context, status string, page, pageSize int) ([]entity.TencentContentCheckLog, int, error) { var result []entity.TencentContentCheckLog - m := gfdb.DB(ctx, "cid").Model(ctx, consts.TencentContentCheckLogTable) + m := g.DB("cid").Model(consts.TencentContentCheckLogTable) if status != "" { m.Where("status", status) @@ -159,7 +158,7 @@ func (d *TencentContentCheckLogDAO) ListByStatus(ctx context.Context, status str // UpdateDuration 更新耗时 func (d *TencentContentCheckLogDAO) UpdateDuration(ctx context.Context, id int64, duration int64) error { - _, err := gfdb.DB(ctx, "cid").Model(ctx, consts.TencentContentCheckLogTable). + _, err := g.DB("cid").Model(consts.TencentContentCheckLogTable). Where("id", id). Data("duration", duration). Update() @@ -168,7 +167,7 @@ func (d *TencentContentCheckLogDAO) UpdateDuration(ctx context.Context, id int64 // UpdateTaskID 更新任务ID func (d *TencentContentCheckLogDAO) UpdateTaskID(ctx context.Context, id int64, taskID string) error { - _, err := gfdb.DB(ctx, "cid").Model(ctx, consts.TencentContentCheckLogTable). + _, err := g.DB("cid").Model(consts.TencentContentCheckLogTable). Where("id", id). Data("task_id", taskID). Update() diff --git a/dao/dataengine/tencent_image_dao.go b/dao/dataengine/tencent_image_dao.go index aa1db32..b80c2c9 100644 --- a/dao/dataengine/tencent_image_dao.go +++ b/dao/dataengine/tencent_image_dao.go @@ -121,6 +121,22 @@ func (d *TencentImageDAO) GetByCondition(ctx context.Context, condition map[stri return result, int(total), nil } +// ClaimPending 原子地尝试将图片从 PENDING 状态转为 SUBMITTING +// 返回 true 表示成功抢到处理权,false 表示已被其他进程处理 +func (d *TencentImageDAO) ClaimPending(ctx context.Context, id int64) (bool, error) { + result, err := Model(consts.TencentImageTable). + Where(entity.TencentImageCols.Id, id). + Where(entity.TencentImageCols.VerifyStatus, consts.CheckStatusPending). + Data(entity.TencentImageCols.VerifyStatus, consts.CheckStatusSubmitting). + Update() + if err != nil { + g.Log().Errorf(ctx, "原子认领图片送检失败: %v", err) + return false, err + } + affected, _ := result.RowsAffected() + return affected > 0, nil +} + // UpdateStatus 更新图片校验状态 func (d *TencentImageDAO) UpdateStatus(ctx context.Context, id int64, verifyStatus string) (int64, error) { result, err := Model(consts.TencentImageTable). diff --git a/dao/dataengine/tencent_video_dao.go b/dao/dataengine/tencent_video_dao.go index fb7615f..dad0ee1 100644 --- a/dao/dataengine/tencent_video_dao.go +++ b/dao/dataengine/tencent_video_dao.go @@ -121,6 +121,22 @@ func (d *TencentVideoDAO) GetByCondition(ctx context.Context, condition map[stri return result, int(total), nil } +// ClaimPending 原子地尝试将视频从 PENDING 状态转为 SUBMITTING +// 返回 true 表示成功抢到处理权,false 表示已被其他进程处理 +func (d *TencentVideoDAO) ClaimPending(ctx context.Context, id int64) (bool, error) { + result, err := Model(consts.TencentVideoTable). + Where(entity.TencentVideoCols.Id, id). + Where(entity.TencentVideoCols.VerifyStatus, consts.CheckStatusPending). + Data(entity.TencentVideoCols.VerifyStatus, consts.CheckStatusSubmitting). + Update() + if err != nil { + g.Log().Errorf(ctx, "原子认领视频送检失败: %v", err) + return false, err + } + affected, _ := result.RowsAffected() + return affected > 0, nil +} + // UpdateStatus 更新视频校验状态 func (d *TencentVideoDAO) UpdateStatus(ctx context.Context, id int64, verifyStatus string) (int64, error) { result, err := Model(consts.TencentVideoTable). diff --git a/main.go b/main.go index 03b2f39..2209ec9 100644 --- a/main.go +++ b/main.go @@ -126,6 +126,7 @@ func startContentCheckService(ctx context.Context) { ImageEnabled: g.Cfg().MustGet(ctx, "content_check.image_enabled", true).Bool(), VideoEnabled: g.Cfg().MustGet(ctx, "content_check.video_enabled", true).Bool(), IntervalSeconds: g.Cfg().MustGet(ctx, "content_check.interval_seconds", 30).Int(), + PollInterval: g.Cfg().MustGet(ctx, "content_check.poll_interval", 60).Int(), } serviceDataengine.TencentContentCheck.SetConfig(config) diff --git a/model/entity/dataengine/material_verify_log.go b/model/entity/dataengine/material_verify_log.go index de7aff1..609e360 100644 --- a/model/entity/dataengine/material_verify_log.go +++ b/model/entity/dataengine/material_verify_log.go @@ -8,22 +8,23 @@ import ( type MaterialVerifyLog struct { beans.SQLBaseDO `orm:",inherit"` // 业务字段 - TenantID int64 `orm:"tenant_id" json:"tenantId" description:"租户ID"` - MaterialType string `orm:"material_type" json:"materialType" description:"素材类型 IMAGE/VIDEO"` - MaterialID string `orm:"material_id" json:"materialId" description:"素材ID"` - SourceTable string `orm:"source_table" json:"sourceTable" description:"来源表"` - SourceID int64 `orm:"source_id" json:"sourceId" description:"原表主键ID"` - AccountID int64 `orm:"account_id" json:"accountId" description:"账户ID"` - TaskID string `orm:"task_id" json:"taskId" description:"易盾任务ID"` - RequestParams string `orm:"request_params" json:"requestParams" description:"请求入参"` - ResponseResult string `orm:"response_result" json:"responseResult" description:"响应出参"` - VerifyStatus string `orm:"verify_status" json:"verifyStatus" description:"校验状态"` - Suggestion int `orm:"suggestion" json:"suggestion" description:"处置建议"` - Label int `orm:"label" json:"label" description:"垃圾类型"` - ResultType int `orm:"result_type" json:"resultType" description:"结果类型"` - ErrorMsg string `orm:"error_msg" json:"errorMsg" description:"错误信息"` - CheckTime int64 `orm:"check_time" json:"checkTime" description:"审核时间戳"` - DurationMs int64 `orm:"duration_ms" json:"durationMs" description:"处理耗时(毫秒)"` + TenantID int64 `orm:"tenant_id" json:"tenantId" description:"租户ID"` + MaterialType string `orm:"material_type" json:"materialType" description:"素材类型 IMAGE/VIDEO"` + MaterialID string `orm:"material_id" json:"materialId" description:"素材ID"` + SourceTable string `orm:"source_table" json:"sourceTable" description:"来源表"` + SourceID int64 `orm:"source_id" json:"sourceId" description:"原表主键ID"` + AccountID int64 `orm:"account_id" json:"accountId" description:"账户ID"` + TaskID string `orm:"task_id" json:"taskId" description:"易盾任务ID"` + RequestParams string `orm:"request_params" json:"requestParams" description:"请求入参"` + ResponseResult string `orm:"response_result" json:"responseResult" description:"响应出参"` + VerifyStatus string `orm:"verify_status" json:"verifyStatus" description:"校验状态"` + Suggestion int `orm:"suggestion" json:"suggestion" description:"处置建议"` + Label int `orm:"label" json:"label" description:"垃圾类型"` + ResultType int `orm:"result_type" json:"resultType" description:"结果类型"` + ErrorMsg string `orm:"error_msg" json:"errorMsg" description:"错误信息"` + CheckTime int64 `orm:"check_time" json:"checkTime" description:"审核时间戳"` + DurationMs int64 `orm:"duration_ms" json:"durationMs" description:"处理耗时(毫秒)"` + RiskDescription string `orm:"risk_description" json:"riskDescription" description:"风险描述(易盾返回)"` // 扩展字段(用于展示) PreviewURL string `orm:"-" json:"previewUrl" description:"预览URL"` @@ -32,43 +33,45 @@ type MaterialVerifyLog struct { // MaterialVerifyLogCol 日志表字段定义 type MaterialVerifyLogCol struct { beans.SQLBaseCol - TenantID string - MaterialType string - MaterialID string - SourceTable string - SourceID string - AccountID string - TaskID string - RequestParams string - ResponseResult string - VerifyStatus string - Suggestion string - Label string - ResultType string - ErrorMsg string - CheckTime string - DurationMs string + TenantID string + MaterialType string + MaterialID string + SourceTable string + SourceID string + AccountID string + TaskID string + RequestParams string + ResponseResult string + VerifyStatus string + Suggestion string + Label string + ResultType string + ErrorMsg string + CheckTime string + DurationMs string + RiskDescription string } // MaterialVerifyLogCols 日志表字段常量 var MaterialVerifyLogCols = MaterialVerifyLogCol{ - SQLBaseCol: beans.DefSQLBaseCol, - TenantID: "tenant_id", - MaterialType: "material_type", - MaterialID: "material_id", - SourceTable: "source_table", - SourceID: "source_id", - AccountID: "account_id", - TaskID: "task_id", - RequestParams: "request_params", - ResponseResult: "response_result", - VerifyStatus: "verify_status", - Suggestion: "suggestion", - Label: "label", - ResultType: "result_type", - ErrorMsg: "error_msg", - CheckTime: "check_time", - DurationMs: "duration_ms", + SQLBaseCol: beans.DefSQLBaseCol, + TenantID: "tenant_id", + MaterialType: "material_type", + MaterialID: "material_id", + SourceTable: "source_table", + SourceID: "source_id", + AccountID: "account_id", + TaskID: "task_id", + RequestParams: "request_params", + ResponseResult: "response_result", + VerifyStatus: "verify_status", + Suggestion: "suggestion", + Label: "label", + ResultType: "result_type", + ErrorMsg: "error_msg", + CheckTime: "check_time", + DurationMs: "duration_ms", + RiskDescription: "risk_description", } // 素材类型常量 diff --git a/service/dataengine/material_verify_service.go b/service/dataengine/material_verify_service.go index 9ff9f6a..62fdac4 100644 --- a/service/dataengine/material_verify_service.go +++ b/service/dataengine/material_verify_service.go @@ -20,12 +20,6 @@ const ( PollBatchSize = 20 ) -// 状态常量 -const ( - // 原表状态 - 与 tencent_image/tencent_video 表的 status 字段对应 - StatusSubmitting = consts.CheckStatusSubmitting // 送检中 -) - // MaterialVerifyService 素材校验服务 type MaterialVerifyService struct{} @@ -55,31 +49,39 @@ func SuggestionToVerifyStatus(suggestion int) string { // ============================================================================= // VerifyImageByID 根据图片ID执行校验 +// 使用原子 CAS 防止并发重复送检 func (s *MaterialVerifyService) VerifyImageByID(ctx context.Context, imageID string) (*entity.MaterialVerifyLog, error) { image, err := dao.TencentImage.GetByImageID(ctx, imageID) if err != nil { - return nil, fmt.Errorf("unknown error: %w", err) + return nil, fmt.Errorf("查询图片数据失败, imageID=%s: %w", imageID, err) } if image == nil { return nil, fmt.Errorf("未找到图片数据, imageID=%s", imageID) } - // 幂等性检查:如果已在送检中,直接返回已有日志 - if image.VerifyStatus == consts.CheckStatusSubmitting { + // 原子 CAS:PENDING → SUBMITTING,确保只有第一个调用者能抢到处理权 + claimed, err := dao.TencentImage.ClaimPending(ctx, image.Id) + if err != nil { + return nil, fmt.Errorf("认领图片送检失败, imageID=%s: %w", imageID, err) + } + if !claimed { logs, err := dao.MaterialVerifyLog.GetByMaterialID(ctx, imageID) if err == nil && len(logs) > 0 { - g.Log().Infof(ctx, "图片已在送检中, imageID=%s, logId=%d", imageID, logs[0].Id) + g.Log().Infof(ctx, "图片已被其他进程送检, imageID=%s, logId=%d", imageID, logs[0].Id) return &logs[0], nil } + return nil, fmt.Errorf("图片正在送检中且无校验日志, imageID=%s", imageID) } log := s.createVerifyLog(ctx, entity.MaterialTypeImage, imageID, consts.SourceTableTencentImage, image.Id, image.AccountID) if log == nil { + dao.TencentImage.UpdateStatus(ctx, image.Id, entity.VerifyStatusPending) return nil, fmt.Errorf("创建校验日志失败") } err = s.submitImageCheck(ctx, image, log) if err != nil { + dao.TencentImage.UpdateStatus(ctx, image.Id, entity.VerifyStatusPending) return nil, err } @@ -113,13 +115,14 @@ func (s *MaterialVerifyService) submitImageCheck(ctx context.Context, image *ent 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("unknown error: %w", err) + return fmt.Errorf("图片异步检测提交失败, imageId=%s: %w", image.ImageID, err) } taskID = result.TaskID dao.MaterialVerifyLog.UpdateTaskID(ctx, log.Id, taskID) dao.MaterialVerifyLog.UpdateRequestParams(ctx, log.Id, string(requestParamsJSON)) - s.updateImageStatus(ctx, image.Id, StatusSubmitting) + + TencentContentCheck.writeAuditLog(ctx, consts.SourceTableTencentImage, image.Id, image.ImageID, image.PreviewURL, taskID, -1, 0, 0, "", duration) g.Log().Infof(ctx, "图片异步检测已提交, id=%d, imageId=%s, taskId=%s, duration=%dms", image.Id, image.ImageID, taskID, duration) @@ -130,7 +133,7 @@ func (s *MaterialVerifyService) submitImageCheck(ctx context.Context, image *ent 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("unknown error: %w", err) + return fmt.Errorf("图片同步检测失败, imageId=%s: %w", image.ImageID, err) } taskID = syncResult.TaskID @@ -143,6 +146,8 @@ func (s *MaterialVerifyService) submitImageCheck(ctx context.Context, image *ent syncResult.Suggestion, syncResult.Label, syncResult.ResultType, string(responseJSON), syncResult.CensorTime) s.updateImageStatus(ctx, image.Id, verifyStatus) + TencentContentCheck.writeAuditLog(ctx, consts.SourceTableTencentImage, image.Id, image.ImageID, image.PreviewURL, taskID, syncResult.Suggestion, syncResult.Label, syncResult.ResultType, string(responseJSON), duration) + 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) } @@ -155,31 +160,38 @@ func (s *MaterialVerifyService) submitImageCheck(ctx context.Context, image *ent // ============================================================================= // VerifyVideoByID 根据视频ID执行校验 +// 使用原子 CAS 防止并发重复送检 func (s *MaterialVerifyService) VerifyVideoByID(ctx context.Context, videoID string) (*entity.MaterialVerifyLog, error) { video, err := dao.TencentVideo.GetByVideoID(ctx, videoID) if err != nil { - return nil, fmt.Errorf("unknown error: %w", err) + return nil, fmt.Errorf("查询视频数据失败, videoID=%s: %w", videoID, err) } if video == nil { return nil, fmt.Errorf("未找到视频数据, videoID=%s", videoID) } - // 幂等性检查:如果已在送检中,直接返回已有日志 - if video.VerifyStatus == consts.CheckStatusSubmitting { + claimed, err := dao.TencentVideo.ClaimPending(ctx, video.Id) + if err != nil { + return nil, fmt.Errorf("认领视频送检失败, videoID=%s: %w", videoID, err) + } + if !claimed { logs, err := dao.MaterialVerifyLog.GetByMaterialID(ctx, videoID) if err == nil && len(logs) > 0 { - g.Log().Infof(ctx, "视频已在送检中, videoID=%s, logId=%d", videoID, logs[0].Id) + g.Log().Infof(ctx, "视频已被其他进程送检, videoID=%s, logId=%d", videoID, logs[0].Id) return &logs[0], nil } + return nil, fmt.Errorf("视频正在送检中且无校验日志, videoID=%s", videoID) } log := s.createVerifyLog(ctx, entity.MaterialTypeVideo, videoID, consts.SourceTableTencentVideo, video.Id, video.AccountID) if log == nil { + dao.TencentVideo.UpdateStatus(ctx, video.Id, entity.VerifyStatusPending) return nil, fmt.Errorf("创建校验日志失败") } err = s.submitVideoCheck(ctx, video, log) if err != nil { + dao.TencentVideo.UpdateStatus(ctx, video.Id, entity.VerifyStatusPending) return nil, err } @@ -211,21 +223,18 @@ func (s *MaterialVerifyService) submitVideoCheck(ctx context.Context, video *ent 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("unknown error: %w", err) + return fmt.Errorf("视频检测提交失败, videoId=%s: %w", video.VideoID, err) } dao.MaterialVerifyLog.UpdateTaskID(ctx, log.Id, result.TaskID) dao.MaterialVerifyLog.UpdateRequestParams(ctx, log.Id, string(requestParamsJSON)) - s.updateVideoStatus(ctx, video.Id, StatusSubmitting) if !callbackMode { - g.Log().Infof(ctx, "轮询模式:提交后立即查询结果, taskId=%s", result.TaskID) - 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, "轮询模式:视频检测已提交, taskId=%s, 请通过轮询接口获取结果", result.TaskID) } + TencentContentCheck.writeAuditLog(ctx, consts.SourceTableTencentVideo, video.Id, video.VideoID, video.PreviewURL, result.TaskID, -1, 0, 0, "", duration) + g.Log().Infof(ctx, "视频校验已提交, id=%d, videoId=%s, taskId=%s, duration=%dms", video.Id, video.VideoID, result.TaskID, duration) @@ -243,7 +252,7 @@ func (s *MaterialVerifyService) ProcessImageCallback(ctx context.Context, callba var callback yidunService.ImageCallbackData if err := json.Unmarshal([]byte(callbackData), &callback); err != nil { g.Log().Errorf(ctx, "解析图片回调数据失败: %v", err) - return fmt.Errorf("unknown error: %w", err) + return fmt.Errorf("解析图片回调数据失败: %w", err) } if callback.Antispam == nil { @@ -256,7 +265,7 @@ func (s *MaterialVerifyService) ProcessImageCallback(ctx context.Context, callba log, err := dao.MaterialVerifyLog.GetByTaskID(ctx, antispam.TaskId) if err != nil { - return fmt.Errorf("unknown error: %w", err) + return fmt.Errorf("查询图片校验日志失败, taskId=%s: %w", antispam.TaskId, err) } if log == nil { g.Log().Warningf(ctx, "未找到校验日志, taskId=%s", antispam.TaskId) @@ -268,13 +277,20 @@ func (s *MaterialVerifyService) ProcessImageCallback(ctx context.Context, callba err = dao.MaterialVerifyLog.UpdateVerifyResult(ctx, log.Id, verifyStatus, antispam.Suggestion, antispam.Label, antispam.ResultType, callbackData, antispam.CensorTime) if err != nil { - return fmt.Errorf("unknown error: %w", err) + return fmt.Errorf("更新图片校验日志结果失败, logId=%d: %w", log.Id, err) } if log.SourceTable == consts.SourceTableTencentImage { s.updateImageStatus(ctx, log.SourceID, verifyStatus) } + // 更新送检审计日志 + s.updateCheckLogResult(ctx, antispam.TaskId, antispam.Suggestion, antispam.Label, antispam.ResultType, callbackData) + // 提取风险描述 + if antispam.RiskDescription != "" { + _ = dao.MaterialVerifyLog.UpdateRiskDescription(ctx, log.Id, antispam.RiskDescription) + } + g.Log().Infof(ctx, "图片校验回调处理完成, taskId=%s, verifyStatus=%s, suggestion=%d", antispam.TaskId, verifyStatus, antispam.Suggestion) @@ -288,7 +304,7 @@ func (s *MaterialVerifyService) ProcessVideoCallback(ctx context.Context, callba var callback yidunService.VideoCallbackData if err := json.Unmarshal([]byte(callbackData), &callback); err != nil { g.Log().Errorf(ctx, "解析视频回调数据失败: %v", err) - return fmt.Errorf("unknown error: %w", err) + return fmt.Errorf("解析视频回调数据失败: %w", err) } if callback.Antispam == nil { @@ -301,7 +317,7 @@ func (s *MaterialVerifyService) ProcessVideoCallback(ctx context.Context, callba log, err := dao.MaterialVerifyLog.GetByTaskID(ctx, antispam.TaskID) if err != nil { - return fmt.Errorf("unknown error: %w", err) + return fmt.Errorf("查询视频校验日志失败, taskId=%s: %w", antispam.TaskID, err) } if log == nil { g.Log().Warningf(ctx, "未找到校验日志, taskId=%s", antispam.TaskID) @@ -318,23 +334,47 @@ func (s *MaterialVerifyService) ProcessVideoCallback(ctx context.Context, callba err = dao.MaterialVerifyLog.UpdateVerifyResult(ctx, log.Id, verifyStatus, antispam.Suggestion, antispam.Label, antispam.ResultType, callbackData, checkTime) if err != nil { - return fmt.Errorf("unknown error: %w", err) + return fmt.Errorf("更新视频校验日志结果失败, logId=%d: %w", log.Id, err) } if log.SourceTable == consts.SourceTableTencentVideo { s.updateVideoStatus(ctx, log.SourceID, verifyStatus) } + // 更新送检审计日志 + s.updateCheckLogResult(ctx, antispam.TaskID, antispam.Suggestion, antispam.Label, antispam.ResultType, callbackData) + // 提取风险描述 + if antispam.RiskDescription != "" { + _ = dao.MaterialVerifyLog.UpdateRiskDescription(ctx, log.Id, antispam.RiskDescription) + } + g.Log().Infof(ctx, "视频校验回调处理完成, taskId=%s, verifyStatus=%s, suggestion=%d", antispam.TaskID, verifyStatus, antispam.Suggestion) return nil } +// updateCheckLogResult 更新 tencent_content_check_log 的检测结果 +func (s *MaterialVerifyService) updateCheckLogResult(ctx context.Context, taskID string, suggestion, label, resultType int, responseData string) { + checkLog, err := dao.TencentContentCheckLog.GetByTaskID(ctx, taskID) + if err != nil { + g.Log().Warningf(ctx, "查询送检审计日志失败, taskId=%s: %v", taskID, err) + return + } + if checkLog == nil { + g.Log().Debugf(ctx, "送检审计日志不存在, taskId=%s(可能是通过 API 直接提交的)", taskID) + return + } + _ = dao.TencentContentCheckLog.UpdateCheckResult(ctx, checkLog.Id, suggestion, label, resultType, time.Now().UnixMilli()) +} + // ============================================================================= // 轮询模式处理 // ============================================================================= +// ErrResultPending 表示检测结果尚未就绪,非错误状态 +var ErrResultPending = fmt.Errorf("检测结果尚未就绪") + // 易盾检测状态常量 const ( YidunStatusNotStart = 0 // 未开始 @@ -344,9 +384,13 @@ const ( ) // ProcessImageResultByTask 根据任务ID处理图片结果(轮询模式) +// 返回 nil 表示结果已处理完成,返回 ErrResultPending 表示仍未就绪 func (s *MaterialVerifyService) ProcessImageResultByTask(ctx context.Context, taskID string) error { log, err := dao.MaterialVerifyLog.GetByTaskID(ctx, taskID) - if err != nil || log == nil { + if err != nil { + return fmt.Errorf("查询校验日志失败, taskId=%s: %w", taskID, err) + } + if log == nil { return fmt.Errorf("未找到校验日志, taskId=%s", taskID) } @@ -354,23 +398,23 @@ func (s *MaterialVerifyService) ProcessImageResultByTask(ctx context.Context, ta if err != nil { if err == yidunService.ErrImageResultNotFound || err == yidunService.ErrImageStillProcessing { g.Log().Infof(ctx, "图片检测结果未就绪, taskId=%s, 保持pending状态, err=%v", taskID, err) - return nil + return ErrResultPending } dao.MaterialVerifyLog.UpdateError(ctx, log.Id, entity.VerifyStatusPending, err.Error()) g.Log().Warningf(ctx, "图片检测查询失败(保持待检验), taskId=%s, error=%v", taskID, err) - return nil + return ErrResultPending } if result.Status == YidunStatusProcessing || result.Status == YidunStatusNotStart { g.Log().Infof(ctx, "图片检测仍在进行中, taskId=%s, status=%d, 保持pending状态", taskID, result.Status) - return nil + return ErrResultPending } if result.Status == YidunStatusFailed { dao.MaterialVerifyLog.UpdateError(ctx, log.Id, entity.VerifyStatusPending, fmt.Sprintf("易盾检测失败, status=%d", result.Status)) g.Log().Warningf(ctx, "图片检测失败(保持待检验), taskId=%s, status=%d", taskID, result.Status) - return nil + return ErrResultPending } verifyStatus := SuggestionToVerifyStatus(result.Suggestion) @@ -383,6 +427,14 @@ func (s *MaterialVerifyService) ProcessImageResultByTask(ctx context.Context, ta s.updateImageStatus(ctx, log.SourceID, verifyStatus) } + // 提取风险描述 + if result.Antispam != nil && result.Antispam.RiskDescription != nil { + _ = dao.MaterialVerifyLog.UpdateRiskDescription(ctx, log.Id, *result.Antispam.RiskDescription) + } + + // 更新送检审计日志 + s.updateCheckLogResult(ctx, taskID, result.Suggestion, result.Label, result.ResultType, string(responseJSON)) + g.Log().Infof(ctx, "图片检测结果更新成功, taskId=%s, status=%d, suggestion=%d, verifyStatus=%s", taskID, result.Status, result.Suggestion, verifyStatus) return nil @@ -391,7 +443,10 @@ func (s *MaterialVerifyService) ProcessImageResultByTask(ctx context.Context, ta // ProcessVideoResultByTask 根据任务ID处理视频结果(轮询模式) func (s *MaterialVerifyService) ProcessVideoResultByTask(ctx context.Context, taskID string) error { log, err := dao.MaterialVerifyLog.GetByTaskID(ctx, taskID) - if err != nil || log == nil { + if err != nil { + return fmt.Errorf("查询校验日志失败, taskId=%s: %w", taskID, err) + } + if log == nil { return fmt.Errorf("未找到校验日志, taskId=%s", taskID) } @@ -399,23 +454,23 @@ func (s *MaterialVerifyService) ProcessVideoResultByTask(ctx context.Context, ta if err != nil { if err == yidunService.ErrVideoResultNotFound || err == yidunService.ErrVideoStillProcessing { g.Log().Infof(ctx, "视频检测结果未就绪, taskId=%s, 保持pending状态, err=%v", taskID, err) - return nil + return ErrResultPending } dao.MaterialVerifyLog.UpdateError(ctx, log.Id, entity.VerifyStatusPending, err.Error()) g.Log().Warningf(ctx, "视频检测查询失败(保持待检验), taskId=%s, error=%v", taskID, err) - return nil + return ErrResultPending } if result.Status == YidunStatusProcessing || result.Status == YidunStatusNotStart { g.Log().Infof(ctx, "视频检测仍在进行中, taskId=%s, status=%d, 保持pending状态", taskID, result.Status) - return nil + return ErrResultPending } if result.Status == YidunStatusFailed { dao.MaterialVerifyLog.UpdateError(ctx, log.Id, entity.VerifyStatusPending, fmt.Sprintf("易盾检测失败, status=%d", result.Status)) g.Log().Warningf(ctx, "视频检测失败(保持待检验), taskId=%s, status=%d", taskID, result.Status) - return nil + return ErrResultPending } verifyStatus := SuggestionToVerifyStatus(result.Suggestion) @@ -428,6 +483,14 @@ func (s *MaterialVerifyService) ProcessVideoResultByTask(ctx context.Context, ta s.updateVideoStatus(ctx, log.SourceID, verifyStatus) } + // 提取风险描述 + if result.Antispam != nil && result.Antispam.RiskDescription != nil { + _ = dao.MaterialVerifyLog.UpdateRiskDescription(ctx, log.Id, *result.Antispam.RiskDescription) + } + + // 更新送检审计日志 + s.updateCheckLogResult(ctx, taskID, result.Suggestion, result.Label, result.ResultType, string(responseJSON)) + g.Log().Infof(ctx, "视频检测结果更新成功, taskId=%s, status=%d, suggestion=%d, verifyStatus=%s", taskID, result.Status, result.Suggestion, verifyStatus) return nil @@ -439,7 +502,6 @@ func (s *MaterialVerifyService) ProcessVideoResultByTask(ctx context.Context, ta // createVerifyLog 创建校验日志 func (s *MaterialVerifyService) createVerifyLog(ctx context.Context, materialType, materialID, sourceTable string, sourceID, accountID int64) *entity.MaterialVerifyLog { - // 从上下文提取租户ID var tenantID int64 if user := ctx.Value("user"); user != nil { if u, ok := user.(*beans.User); ok { @@ -467,7 +529,7 @@ func (s *MaterialVerifyService) createVerifyLog(ctx context.Context, materialTyp return log } -// updateImageStatus 更新图片状态(已记录日志则同步更新,失败仅记录日志不影响主流程) +// updateImageStatus 更新图片状态 func (s *MaterialVerifyService) updateImageStatus(ctx context.Context, imageID int64, verifyStatus string) { _, err := dao.TencentImage.UpdateStatus(ctx, imageID, verifyStatus) if err != nil { @@ -477,7 +539,7 @@ func (s *MaterialVerifyService) updateImageStatus(ctx context.Context, imageID i } } -// updateVideoStatus 更新视频状态(已记录日志则同步更新,失败仅记录日志不影响主流程) +// updateVideoStatus 更新视频状态 func (s *MaterialVerifyService) updateVideoStatus(ctx context.Context, videoID int64, verifyStatus string) { _, err := dao.TencentVideo.UpdateStatus(ctx, videoID, verifyStatus) if err != nil { @@ -515,10 +577,8 @@ func (s *MaterialVerifyService) GetStats(ctx context.Context) (map[string]int, e // 轮询模式 - 批量查询检测结果 // ============================================================================= -// PollPendingResults 轮询所有待查询结果的日志(手动触发) -// 返回处理成功的数量和错误信息 +// 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 @@ -538,7 +598,6 @@ func (s *MaterialVerifyService) PollPendingResults(ctx context.Context) (int, in for _, log := range logs { var err error - // 根据来源表判断调用哪个接口 if log.SourceTable == consts.SourceTableTencentImage { err = s.ProcessImageResultByTask(ctx, log.TaskID) } else if log.SourceTable == consts.SourceTableTencentVideo { @@ -548,7 +607,9 @@ func (s *MaterialVerifyService) PollPendingResults(ctx context.Context) (int, in continue } - if err != nil { + if err == ErrResultPending { + g.Log().Infof(ctx, "结果未就绪, logId=%d, taskId=%s", log.Id, log.TaskID) + } else if err != nil { failCount++ lastErr = err g.Log().Warningf(ctx, "处理结果失败, logId=%d, taskId=%s, error=%v", log.Id, log.TaskID, err) @@ -557,23 +618,20 @@ func (s *MaterialVerifyService) PollPendingResults(ctx context.Context) (int, in 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) + g.Log().Infof(ctx, "轮询完成, 成功=%d, 失败=%d, 未就绪=%d", successCount, failCount, len(logs)-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 { @@ -586,8 +644,6 @@ func (s *MaterialVerifyService) PollPendingResultsByType(ctx context.Context, so return 0, 0, nil } - g.Log().Infof(ctx, "开始轮询 %d 条待处理结果, sourceTable=%s", len(filteredLogs), sourceTable) - successCount := 0 failCount := 0 var lastErr error @@ -611,7 +667,6 @@ func (s *MaterialVerifyService) PollPendingResultsByType(ctx context.Context, so time.Sleep(100 * time.Millisecond) } - g.Log().Infof(ctx, "轮询完成, sourceTable=%s, 成功=%d, 失败=%d", sourceTable, successCount, failCount) return successCount, failCount, lastErr } @@ -631,16 +686,16 @@ func (s *MaterialVerifyService) PollPendingVideoResults(ctx context.Context) (in // 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"` // 检测时间(日志创建时间) + ID int64 `json:"id"` + MaterialID string `json:"materialId"` + AccountID int64 `json:"accountId"` + CorporationName string `json:"corporationName"` + PreviewURL string `json:"previewUrl"` + Description string `json:"description"` + ErrorMsg string `json:"errorMsg"` + MaterialType string `json:"materialType"` + ImageUsage string `json:"imageUsage,omitempty"` + CreatedAt string `json:"createdAt"` } // getFailureReason 获取失败原因 @@ -648,11 +703,9 @@ 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: "内容嫌疑(需人工审核)", @@ -662,7 +715,6 @@ func getFailureReason(log *entity.MaterialVerifyLog) string { 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 { @@ -677,7 +729,7 @@ func getFailureReason(log *entity.MaterialVerifyLog) string { const exportBatchSize = 1000 -// ExportRejectedData 导出不通过数据(分批加载,避免OOM) +// ExportRejectedData 导出不通过数据 func (s *MaterialVerifyService) ExportRejectedData(ctx context.Context, materialType string) ([]ExportRejectedItem, error) { var items []ExportRejectedItem @@ -694,15 +746,13 @@ func (s *MaterialVerifyService) ExportRejectedData(ctx context.Context, material condition := map[string]interface{}{ entity.TencentImageCols.VerifyStatus: entity.VerifyStatusRejected, } - page := 1 for { images, total, err := dao.TencentImage.GetByCondition(ctx, condition, page, exportBatchSize) if err != nil { g.Log().Errorf(ctx, "查询不通过图片失败: %v", err) - return nil, fmt.Errorf("unknown error: %w", err) + return nil, fmt.Errorf("查询不通过图片失败: %w", err) } - for _, img := range images { log, _ := dao.MaterialVerifyLog.GetLastRejectedLogByMaterialID(ctx, img.ImageID, entity.VerifyStatusRejected) var createdAtStr string @@ -710,19 +760,13 @@ func (s *MaterialVerifyService) ExportRejectedData(ctx context.Context, material createdAtStr = log.CreatedAt.Format("2006-01-02 15:04:05") } 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, + 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 page*exportBatchSize >= total { break } @@ -734,15 +778,13 @@ func (s *MaterialVerifyService) ExportRejectedData(ctx context.Context, material condition := map[string]interface{}{ entity.TencentVideoCols.VerifyStatus: entity.VerifyStatusRejected, } - page := 1 for { videos, total, err := dao.TencentVideo.GetByCondition(ctx, condition, page, exportBatchSize) if err != nil { g.Log().Errorf(ctx, "查询不通过视频失败: %v", err) - return nil, fmt.Errorf("unknown error: %w", err) + return nil, fmt.Errorf("查询不通过视频失败: %w", err) } - for _, vid := range videos { log, _ := dao.MaterialVerifyLog.GetLastRejectedLogByMaterialID(ctx, vid.VideoID, entity.VerifyStatusRejected) var createdAtStr string @@ -750,18 +792,12 @@ func (s *MaterialVerifyService) ExportRejectedData(ctx context.Context, material createdAtStr = log.CreatedAt.Format("2006-01-02 15:04:05") } 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, + 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, }) } - if page*exportBatchSize >= total { break } @@ -776,3 +812,36 @@ func (s *MaterialVerifyService) ExportRejectedData(ctx context.Context, material func (s *MaterialVerifyService) GetPendingResultsCount(ctx context.Context) (int, error) { return dao.MaterialVerifyLog.CountPendingResults(ctx) } + +// GetPendingResultsDetail 获取待查询结果的明细列表 +type PendingResultItem struct { + LogID int64 `json:"logId"` + MaterialID string `json:"materialId"` + MaterialType string `json:"materialType"` + SourceTable string `json:"sourceTable"` + TaskID string `json:"taskId"` + CreatedAt string `json:"createdAt"` +} + +func (s *MaterialVerifyService) GetPendingResultsDetail(ctx context.Context, limit int) ([]PendingResultItem, error) { + logs, err := dao.MaterialVerifyLog.GetPendingResults(ctx, limit) + if err != nil { + return nil, err + } + var items []PendingResultItem + for _, l := range logs { + createdAt := "" + if l.CreatedAt != nil { + createdAt = l.CreatedAt.Format("2006-01-02 15:04:05") + } + items = append(items, PendingResultItem{ + LogID: l.Id, + MaterialID: l.MaterialID, + MaterialType: l.MaterialType, + SourceTable: l.SourceTable, + TaskID: l.TaskID, + CreatedAt: createdAt, + }) + } + return items, nil +} diff --git a/service/dataengine/tencent_content_check_service.go b/service/dataengine/tencent_content_check_service.go index 4f3fe4c..57f3655 100644 --- a/service/dataengine/tencent_content_check_service.go +++ b/service/dataengine/tencent_content_check_service.go @@ -7,10 +7,12 @@ import ( yidunService "cid/service/yidun" "context" "encoding/json" + "sync" "time" "gitea.redpowerfuture.com/red-future/common/beans" "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/grpool" ) // ContentCheckConfig 送检配置 @@ -19,6 +21,7 @@ type ContentCheckConfig struct { ImageEnabled bool `json:"image_enabled"` VideoEnabled bool `json:"video_enabled"` IntervalSeconds int `json:"interval_seconds"` + PollInterval int `json:"poll_interval"` // 自动轮询检测结果间隔(秒) } // DefaultConfig 默认配置 @@ -27,12 +30,16 @@ var DefaultConfig = ContentCheckConfig{ ImageEnabled: true, VideoEnabled: true, IntervalSeconds: 30, + PollInterval: 60, } // TencentContentCheckService 腾讯内容送检服务 type TencentContentCheckService struct { + mu sync.RWMutex config ContentCheckConfig isRunning bool + pool *grpool.Pool + cancel context.CancelFunc } // TencentContentCheck 送检服务单例 @@ -42,47 +49,92 @@ var TencentContentCheck = &TencentContentCheckService{ // SetConfig 设置配置 func (s *TencentContentCheckService) SetConfig(config ContentCheckConfig) { + s.mu.Lock() s.config = config + s.mu.Unlock() } // Start 启动定时任务 func (s *TencentContentCheckService) Start(ctx context.Context) error { + s.mu.Lock() if s.isRunning { + s.mu.Unlock() g.Log().Info(ctx, "送检服务已在运行中,跳过启动") return nil } s.isRunning = true - g.Log().Infof(ctx, "启动内容送检服务,配置: batch_size=%d, interval=%ds, image=%v, video=%v", - s.config.BatchSize, s.config.IntervalSeconds, s.config.ImageEnabled, s.config.VideoEnabled) + config := s.config + s.pool = grpool.New(5) + s.mu.Unlock() - schedCtx := context.Background() - if user := ctx.Value("user"); user != nil { - schedCtx = context.WithValue(schedCtx, "user", user) + g.Log().Infof(ctx, "启动内容送检服务,配置: batch_size=%d, interval=%ds, poll=%ds, image=%v, video=%v", + config.BatchSize, config.IntervalSeconds, config.PollInterval, config.ImageEnabled, config.VideoEnabled) + + schedCtx, cancel := context.WithCancel(context.Background()) + s.cancel = cancel + + // 定时送检协程 + g.Go(schedCtx, func(ctx context.Context) { + ticker := time.NewTicker(time.Duration(config.IntervalSeconds) * time.Second) + defer ticker.Stop() + + // 启动时先执行一次 + _ = s.pool.Add(ctx, func(jobCtx context.Context) { + s.processAll(jobCtx) + }) + + for { + select { + case <-ticker.C: + _ = s.pool.Add(ctx, func(jobCtx context.Context) { + s.processAll(jobCtx) + }) + case <-ctx.Done(): + s.pool.Close() + return + } + } + }, nil) + + // 自动轮询检测结果协程(无论回调/轮询模式,都定期查一次未处理的结果) + pollInterval := config.PollInterval + if pollInterval <= 0 { + pollInterval = 60 } - go s.runScheduler(schedCtx) + g.Go(schedCtx, func(ctx context.Context) { + pollTicker := time.NewTicker(time.Duration(pollInterval) * time.Second) + defer pollTicker.Stop() + + g.Log().Infof(ctx, "启动自动轮询检测结果, 间隔=%ds", pollInterval) + + for { + select { + case <-pollTicker.C: + _, _, _ = MaterialVerify.PollPendingResults(ctx) + case <-ctx.Done(): + return + } + } + }, nil) + return nil } // Stop 停止定时任务 func (s *TencentContentCheckService) Stop(ctx context.Context) { - s.isRunning = false - g.Log().Info(ctx, "停止内容送检服务") -} - -// runScheduler 定时调度器 -func (s *TencentContentCheckService) runScheduler(ctx context.Context) { - ticker := time.NewTicker(time.Duration(s.config.IntervalSeconds) * time.Second) - defer ticker.Stop() - - s.processAll(ctx) - - for range ticker.C { - if !s.isRunning { - return - } - s.processAll(ctx) + s.mu.Lock() + if !s.isRunning { + s.mu.Unlock() + return } + s.isRunning = false + s.mu.Unlock() + + if s.cancel != nil { + s.cancel() + } + g.Log().Info(ctx, "停止内容送检服务") } // processAll 处理所有待送检数据 @@ -94,18 +146,33 @@ func (s *TencentContentCheckService) processAll(ctx context.Context) { var totalProcessed int - if s.config.ImageEnabled { - imageCount, _ := dao.TencentImage.CountPending(ctx) - if imageCount > 0 { - count, _ := s.processImages(ctx) + s.mu.RLock() + imageEnabled := s.config.ImageEnabled + videoEnabled := s.config.VideoEnabled + s.mu.RUnlock() + + if imageEnabled { + imageCount, err := dao.TencentImage.CountPending(ctx) + if err != nil { + g.Log().Errorf(ctx, "统计待送检图片数量失败: %v", err) + } else if imageCount > 0 { + count, procErr := s.processImages(ctx) + if procErr != nil { + g.Log().Errorf(ctx, "图片送检处理失败: %v", procErr) + } totalProcessed += count } } - if s.config.VideoEnabled { - videoCount, _ := dao.TencentVideo.CountPending(ctx) - if videoCount > 0 { - count, _ := s.processVideos(ctx) + if videoEnabled { + videoCount, err := dao.TencentVideo.CountPending(ctx) + if err != nil { + g.Log().Errorf(ctx, "统计待送检视频数量失败: %v", err) + } else if videoCount > 0 { + count, procErr := s.processVideos(ctx) + if procErr != nil { + g.Log().Errorf(ctx, "视频送检处理失败: %v", procErr) + } totalProcessed += count } } @@ -116,7 +183,11 @@ func (s *TencentContentCheckService) processAll(ctx context.Context) { // processImages 处理图片送检(统一走 MaterialVerify 系统) func (s *TencentContentCheckService) processImages(ctx context.Context) (int, error) { - images, err := dao.TencentImage.GetPendingList(ctx, s.config.BatchSize) + s.mu.RLock() + batchSize := s.config.BatchSize + s.mu.RUnlock() + + images, err := dao.TencentImage.GetPendingList(ctx, batchSize) if err != nil { g.Log().Errorf(ctx, "获取待送检图片失败: %v", err) return 0, err @@ -132,15 +203,12 @@ func (s *TencentContentCheckService) processImages(ctx context.Context) (int, er failedCount := 0 for _, img := range images { - // 统一走 MaterialVerify 系统提交(处理完整校验流程:日志→提交→状态反写) - mLog, err := MaterialVerify.VerifyImageByID(ctx, img.ImageID) + _, err := MaterialVerify.VerifyImageByID(ctx, img.ImageID) if err != nil { failedCount++ g.Log().Errorf(ctx, "图片送检失败, imageId=%s, error=%v", img.ImageID, err) } else { successCount++ - // 审计日志:同步写入 tencent_content_check_log - s.writeAuditLog(ctx, consts.SourceTableTencentImage, img.Id, img.ImageID, img.PreviewURL, mLog.TaskID) } time.Sleep(100 * time.Millisecond) @@ -152,7 +220,11 @@ func (s *TencentContentCheckService) processImages(ctx context.Context) (int, er // processVideos 处理视频送检(统一走 MaterialVerify 系统) func (s *TencentContentCheckService) processVideos(ctx context.Context) (int, error) { - videos, err := dao.TencentVideo.GetPendingList(ctx, s.config.BatchSize) + s.mu.RLock() + batchSize := s.config.BatchSize + s.mu.RUnlock() + + videos, err := dao.TencentVideo.GetPendingList(ctx, batchSize) if err != nil { g.Log().Errorf(ctx, "获取待送检视频失败: %v", err) return 0, err @@ -168,13 +240,12 @@ func (s *TencentContentCheckService) processVideos(ctx context.Context) (int, er failedCount := 0 for _, video := range videos { - mLog, err := MaterialVerify.VerifyVideoByID(ctx, video.VideoID) + _, err := MaterialVerify.VerifyVideoByID(ctx, video.VideoID) if err != nil { failedCount++ g.Log().Errorf(ctx, "视频送检失败, videoId=%s, error=%v", video.VideoID, err) } else { successCount++ - s.writeAuditLog(ctx, consts.SourceTableTencentVideo, video.Id, video.VideoID, video.PreviewURL, mLog.TaskID) } time.Sleep(100 * time.Millisecond) @@ -185,21 +256,32 @@ func (s *TencentContentCheckService) processVideos(ctx context.Context) (int, er } // writeAuditLog 写入审计日志(tencent_content_check_log) -func (s *TencentContentCheckService) writeAuditLog(ctx context.Context, sourceTable string, sourceID int64, mediaID string, mediaURL string, taskID string) { +// 当 suggestion<0 时记录为 SUBMITTING(已提交等待结果),否则记录为 COMPLETED(检测完成) +func (s *TencentContentCheckService) writeAuditLog(ctx context.Context, sourceTable string, sourceID int64, mediaID string, mediaURL string, taskID string, suggestion, label, resultType int, responseData string, duration int64) { requestParam := map[string]interface{}{ "media_id": mediaID, "url": mediaURL, } requestParamJSON, _ := json.Marshal(requestParam) + status := consts.CheckStatusSubmitting + if suggestion >= 0 { + status = consts.CheckStatusCompleted + } + log := &entity.TencentContentCheckLog{ SourceTable: sourceTable, SourceID: sourceID, RequestURL: "易盾内容安全检测接口", RequestParam: string(requestParamJSON), - Status: consts.CheckStatusSuccess, + Status: status, CheckTime: time.Now().UnixMilli(), TaskID: taskID, + Suggestion: suggestion, + Label: label, + ResultType: resultType, + ResponseData: responseData, + Duration: duration, } id, err := dao.TencentContentCheckLog.Create(ctx, log) @@ -220,7 +302,7 @@ func (s *TencentContentCheckService) SubmitImageByID(ctx context.Context, imageI image, err := dao.TencentImage.GetByImageID(ctx, imageID) if err == nil && image != nil { - s.writeAuditLog(ctx, consts.SourceTableTencentImage, image.Id, imageID, image.PreviewURL, mLog.TaskID) + s.writeAuditLog(ctx, consts.SourceTableTencentImage, image.Id, imageID, image.PreviewURL, mLog.TaskID, -1, 0, 0, "", 0) } return &yidunService.ImageSubmitResult{ @@ -237,7 +319,7 @@ func (s *TencentContentCheckService) SubmitVideoByID(ctx context.Context, videoI video, err := dao.TencentVideo.GetByVideoID(ctx, videoID) if err == nil && video != nil { - s.writeAuditLog(ctx, consts.SourceTableTencentVideo, video.Id, videoID, video.PreviewURL, mLog.TaskID) + s.writeAuditLog(ctx, consts.SourceTableTencentVideo, video.Id, videoID, video.PreviewURL, mLog.TaskID, -1, 0, 0, "", 0) } return &yidunService.VideoSubmitResult{ @@ -249,13 +331,24 @@ func (s *TencentContentCheckService) SubmitVideoByID(ctx context.Context, videoI func (s *TencentContentCheckService) GetPendingStats(ctx context.Context) map[string]int { stats := make(map[string]int) - if s.config.ImageEnabled { - count, _ := dao.TencentImage.CountPending(ctx) + s.mu.RLock() + imageEnabled := s.config.ImageEnabled + videoEnabled := s.config.VideoEnabled + s.mu.RUnlock() + + if imageEnabled { + count, err := dao.TencentImage.CountPending(ctx) + if err != nil { + g.Log().Errorf(ctx, "统计待送检图片数量失败: %v", err) + } stats["image_pending"] = count } - if s.config.VideoEnabled { - count, _ := dao.TencentVideo.CountPending(ctx) + if videoEnabled { + count, err := dao.TencentVideo.CountPending(ctx) + if err != nil { + g.Log().Errorf(ctx, "统计待送检视频数量失败: %v", err) + } stats["video_pending"] = count } @@ -264,10 +357,14 @@ func (s *TencentContentCheckService) GetPendingStats(ctx context.Context) map[st // IsRunning 获取运行状态 func (s *TencentContentCheckService) IsRunning() bool { + s.mu.RLock() + defer s.mu.RUnlock() return s.isRunning } // GetConfig 获取当前配置 func (s *TencentContentCheckService) GetConfig() ContentCheckConfig { + s.mu.RLock() + defer s.mu.RUnlock() return s.config } diff --git a/service/yidun/image_detection_service.go b/service/yidun/image_detection_service.go index 89692d7..8e2d661 100644 --- a/service/yidun/image_detection_service.go +++ b/service/yidun/image_detection_service.go @@ -2,7 +2,6 @@ package yidun import ( "context" - "encoding/json" "errors" "fmt" @@ -435,28 +434,3 @@ type StrategyVersionInfo struct { Label int `json:"label"` // 垃圾类别 Version string `json:"version"` // 版本号 } - -// ProcessImageCallback 处理图片检测回调(推送模式) -func (s *ImageDetectionService) ProcessImageCallback(ctx context.Context, callbackData string) error { - if callbackData == "" { - return fmt.Errorf("回调数据不能为空") - } - - var data ImageCallbackData - if err := json.Unmarshal([]byte(callbackData), &data); err != nil { - g.Log().Errorf(ctx, "解析回调数据失败: %v", err) - return fmt.Errorf("解析回调数据失败: %w", err) - } - - if data.Antispam == nil { - return fmt.Errorf("回调数据格式错误:缺少antispam字段") - } - - g.Log().Infof(ctx, "处理图片检测结果 - taskId: %s, suggestion: %d, resultType: %d", - data.Antispam.TaskId, data.Antispam.Suggestion, data.Antispam.ResultType) - - // TODO: 业务逻辑,如保存数据库、触发后续流程等 - // 可使用完整字段:data.Antispam.Labels, data.Antispam.CensorLabels, data.Antispam.Remark 等 - - return nil -} diff --git a/service/yidun/video_detection_service.go b/service/yidun/video_detection_service.go index ed36c8d..4a0f6d2 100644 --- a/service/yidun/video_detection_service.go +++ b/service/yidun/video_detection_service.go @@ -2,15 +2,14 @@ package yidun import ( "context" - "encoding/json" "errors" "fmt" "github.com/gogf/gf/v2/frame/g" audiocallback "github.com/yidun/yidun-golang-sdk/yidun/service/antispam/audio/callback/v4/response" videocallback "github.com/yidun/yidun-golang-sdk/yidun/service/antispam/video/callback/v4/response" - callbackrequest "github.com/yidun/yidun-golang-sdk/yidun/service/antispam/videosolution/callback/v2/request" callbackresponse "github.com/yidun/yidun-golang-sdk/yidun/service/antispam/videosolution/callback/v2/response" + queryrequest "github.com/yidun/yidun-golang-sdk/yidun/service/antispam/videosolution/query/v2/request" vsrequest "github.com/yidun/yidun-golang-sdk/yidun/service/antispam/videosolution/submit/v2/request" ) @@ -26,53 +25,39 @@ var ( // VideoSubmitResult 视频检测提交结果 type VideoSubmitResult struct { - TaskID string `json:"taskId"` // 任务ID - DataID string `json:"dataId"` // 数据ID - DealingCount int64 `json:"dealingCount"` // 缓冲池排队待处理数据量 + TaskID string `json:"taskId"` + DataID string `json:"dataId"` + DealingCount int64 `json:"dealingCount"` } -// DetectVideo 提交视频检测任务,返回完整响应 +// DetectVideo 提交视频检测任务 func (s *VideoDetectionService) DetectVideo(ctx context.Context, videoURL, dataID string, callbackURL string) (*VideoSubmitResult, error) { if DefaultClients == nil || DefaultClients.VideoClient == nil { return nil, fmt.Errorf("易盾视频检测客户端未初始化") } - if videoURL == "" { return nil, fmt.Errorf("视频URL不能为空") } g.Log().Infof(ctx, "视频检测任务提交, url: %s, dataID: %s", videoURL, dataID) - // 创建请求 req := vsrequest.NewVideoSolutionSubmitV2Req() req.SetURL(videoURL) req.SetDataID(dataID) req.SetUniqueKey(dataID) - - // 设置回调地址 if callbackURL != "" { req.SetCallbackURL(callbackURL) } - - // 设置子产品标识(视频解决方案必须) req.SetSubProduct("videoStream") - // 可选:设置IP(用于风险用户识别) - // req.SetIP("127.0.0.1") - - // 调用API response, err := DefaultClients.VideoClient.Submit(req) if err != nil { g.Log().Errorf(ctx, "视频检测提交HTTP错误: %v", err) return nil, fmt.Errorf("视频检测提交HTTP错误: %w", err) } - if response.GetCode() != 200 { g.Log().Errorf(ctx, "视频检测API错误: code=%d, msg=%s", response.GetCode(), response.GetMsg()) - // 根据错误码提供更详细的错误信息 errMsg := fmt.Sprintf("视频检测API错误: code=%d, msg=%s", response.GetCode(), response.GetMsg()) - - // 常见错误码说明 switch response.GetCode() { case 417: errMsg += " (可能原因: 视频URL无法访问或业务配置问题)" @@ -81,7 +66,7 @@ func (s *VideoDetectionService) DetectVideo(ctx context.Context, videoURL, dataI case 403: errMsg += " (可能原因: 鉴权失败,检查secretId和secretKey)" } - return nil, fmt.Errorf(errMsg) + return nil, fmt.Errorf("%s", errMsg) } result := &VideoSubmitResult{} @@ -96,38 +81,64 @@ func (s *VideoDetectionService) DetectVideo(ctx context.Context, videoURL, dataI result.DealingCount = *response.Result.DealingCount } } - g.Log().Infof(ctx, "视频检测任务提交成功, taskID: %s, dealingCount: %d", result.TaskID, result.DealingCount) return result, nil } // VideoResult 视频检测完整结果 -// 包含易盾返回的完整检测信息:文本、图片、音频、ASR等 type VideoResult struct { - TaskID string `json:"taskId"` // 任务ID - Status int `json:"status"` // 检测状态:0=未开始,1=检测中,2=检测完成 - Suggestion int `json:"suggestion"` // 处置建议:0=通过,1=嫌疑,2=不通过 - Label int `json:"label"` // 垃圾类型 - ResultType int `json:"resultType"` // 结果类型:1=机器结果,2=人审结果 - DataID string `json:"dataId"` // 数据ID - CensorTime int64 `json:"censorTime"` // 审核完成时间(毫秒) - Duration int64 `json:"duration"` // 视频时长(毫秒) + TaskID string `json:"taskId"` + Status int `json:"status"` // 0=未开始, 1=检测中, 2=检测成功, 3=检测失败 + Suggestion int `json:"suggestion"` // 0=通过, 1=嫌疑, 2=不通过 + Label int `json:"label"` // 违规类别 + ResultType int `json:"resultType"` // 1=机器结果, 2=人审结果 + DataID string `json:"dataId"` + CensorTime int64 `json:"censorTime"` + Duration int64 `json:"duration"` // 完整证据信息 - Antispam *callbackresponse.VideoSolutionAntispamCallbackV2Response `json:"antispam,omitempty"` // 反垃圾检测结果 - Language *audiocallback.AudioLanguageCallbackV4Response `json:"language,omitempty"` // 语言检测结果 - Voice *audiocallback.AudioVoiceCallbackV4Response `json:"voice,omitempty"` // 语音识别结果 - Asr *audiocallback.AudioAsrCallbackV4Response `json:"asr,omitempty"` // ASR语音转文字结果 - Ocr *videocallback.VideoCallbackOcrV4Response `json:"ocr,omitempty"` // OCR文字识别结果 - Discern *videocallback.VideoCallbackDiscernV4Response `json:"discern,omitempty"` // 画面识别结果 - Logo *videocallback.VideoCallbackLogoV4Response `json:"logo,omitempty"` // Logo识别结果 - Face *videocallback.VideoCallbackFaceV4Response `json:"face,omitempty"` // 人脸识别结果 - Aigc *videocallback.VideoCallbackAigcV4Response `json:"aigc,omitempty"` // AIGC识别结果 - Quality *callbackresponse.VideoSolutionQualityCallbackV2Response `json:"quality,omitempty"` // 音视频质量检测结果 + Antispam *callbackresponse.VideoSolutionAntispamCallbackV2Response `json:"antispam,omitempty"` + Language *audiocallback.AudioLanguageCallbackV4Response `json:"language,omitempty"` + Voice *audiocallback.AudioVoiceCallbackV4Response `json:"voice,omitempty"` + Asr *audiocallback.AudioAsrCallbackV4Response `json:"asr,omitempty"` + Ocr *videocallback.VideoCallbackOcrV4Response `json:"ocr,omitempty"` + Discern *videocallback.VideoCallbackDiscernV4Response `json:"discern,omitempty"` + Logo *videocallback.VideoCallbackLogoV4Response `json:"logo,omitempty"` + Face *videocallback.VideoCallbackFaceV4Response `json:"face,omitempty"` + Aigc *videocallback.VideoCallbackAigcV4Response `json:"aigc,omitempty"` + Quality *callbackresponse.VideoSolutionQualityCallbackV2Response `json:"quality,omitempty"` +} + +// queryCheckStatusToStatus 将易盾 checkStatus 映射为内部 Status +func queryCheckStatusToStatus(checkStatus int) int { + switch checkStatus { + case 0: + return 1 // Processing + case 1: + return 2 // Success + case 2: + return 3 // Failed + default: + return 0 // NotStart + } +} + +// queryResultToSuggestion 将易盾 result 映射为内部 suggestion +// result: 1=正常, 2=异常, 3=疑似 → suggestion: 0=通过, 1=嫌疑, 2=不通过 +func queryResultToSuggestion(result int) int { + switch result { + case 1: + return 0 // 通过 + case 2: + return 2 // 不通过 + case 3: + return 1 // 嫌疑 + default: + return 0 + } } // GetVideoResult 获取视频检测结果(轮询模式) -// 返回完整的检测详情,包括文本、图片、音频、ASR等 func (s *VideoDetectionService) GetVideoResult(ctx context.Context, taskID string) (*VideoResult, error) { if DefaultClients == nil || DefaultClients.VideoClient == nil { return nil, fmt.Errorf("易盾视频检测客户端未初始化") @@ -135,41 +146,44 @@ func (s *VideoDetectionService) GetVideoResult(ctx context.Context, taskID strin g.Log().Infof(ctx, "查询视频检测结果, taskID: %s", taskID) - req := callbackrequest.NewVideoSolutionCallbackV2Request() - req.SetYidunRequestId(taskID) - response, err := DefaultClients.VideoClient.Callback(req) + req := queryrequest.NewVideoSolutionQueryTaskV2Request() + req.SetTaskIds([]string{taskID}) + + response, err := DefaultClients.VideoClient.QueryTaskV2(req) if err != nil { g.Log().Errorf(ctx, "查询视频检测结果失败: %v", err) return nil, fmt.Errorf("查询视频检测结果失败: %w", err) } - if response.GetCode() != 200 { g.Log().Errorf(ctx, "查询视频检测结果API错误: code=%d, msg=%s", response.GetCode(), response.GetMsg()) return nil, fmt.Errorf("查询视频检测结果API错误: code=%d, msg=%s", response.GetCode(), response.GetMsg()) } - if response.Result == nil || len(*response.Result) == 0 { g.Log().Warningf(ctx, "未找到视频检测结果, taskID: %s", taskID) return nil, ErrVideoResultNotFound } - // 查找指定taskID的结果 for _, item := range *response.Result { - if item.Antispam != nil && item.Antispam.TaskID != nil && *item.Antispam.TaskID == taskID { - result := &VideoResult{ - Antispam: item.Antispam, - Language: item.Language, - Voice: item.Voice, - Asr: item.Asr, - Ocr: item.Ocr, - Discern: item.Discern, - Logo: item.Logo, - Face: item.Face, - Aigc: item.Aigc, - Quality: item.Quality, + if item.TaskID != nil && *item.TaskID == taskID { + // status: 0=检测完成/失败, 20=非7天内, 30=不存在, 40=检测中 + if item.Status != nil { + if *item.Status == 30 { + g.Log().Warningf(ctx, "视频检测结果不存在, taskID: %s", taskID) + return nil, ErrVideoResultNotFound + } + if *item.Status == 40 { + g.Log().Infof(ctx, "视频仍在检测中, taskID: %s", taskID) + return nil, ErrVideoStillProcessing + } } - result.TaskID = taskID + result := &VideoResult{TaskID: taskID} + if item.DataID != nil { + result.DataID = *item.DataID + } + if item.TaskID != nil { + result.TaskID = *item.TaskID + } if item.Antispam.Status != nil { result.Status = *item.Antispam.Status } @@ -192,6 +206,11 @@ func (s *VideoDetectionService) GetVideoResult(ctx context.Context, taskID strin result.Duration = *item.Antispam.Duration } + // 完整证据信息 + result.Antispam = &item.Antispam + + g.Log().Infof(ctx, "视频检测结果查询成功, taskID: %s, suggestion=%d, label=%d", + taskID, result.Suggestion, result.Label) return result, nil } } @@ -200,457 +219,24 @@ func (s *VideoDetectionService) GetVideoResult(ctx context.Context, taskID strin return nil, ErrVideoResultNotFound } -// ============================================================================= -// 视频检测结果推送模式(易盾主动回调) -// ============================================================================= - -// VideoCallbackData 推送模式回调数据完整结构 +// VideoCallbackData 推送模式回调数据 type VideoCallbackData struct { - Antispam *VideoCallbackAntispam `json:"antispam"` // 反垃圾检测结果 + Antispam *VideoCallbackAntispam `json:"antispam"` } +// VideoCallbackAntispam 视频回调反垃圾结果 type VideoCallbackAntispam struct { - TaskID string `json:"taskId"` // 任务ID - DataID string `json:"dataId"` // 客户数据ID - Callback string `json:"callback"` // 回调参数 - Suggestion int `json:"suggestion"` // 处置建议:0=通过,1=嫌疑,2=不通过 - Status int `json:"status"` // 检测状态:0=未开始,1=检测中,2=检测成功,3=检测失败 - ResultType int `json:"resultType"` // 结果类型:1=机器结果,2=人审结果 - CensorRound int `json:"censorRound"` // 人审轮次 - Censor string `json:"censor"` // 人审操作人 - CensorSource int `json:"censorSource"` // 审核来源:0=易盾人审,1=客户人审,2=易盾机审 - CheckTime int64 `json:"checkTime"` // 机器检测结束时间 - CensorTime int64 `json:"censorTime"` // 人工审核完成时间 - Duration int64 `json:"duration"` // 音视频时长(毫秒) - DurationMs int64 `json:"durationMs"` // 音频时长(毫秒) - Label int `json:"label"` // 一级垃圾类型 - SecondLabel string `json:"secondLabel"` // 二级垃圾类型 - ThirdLabel string `json:"thirdLabel"` // 三级垃圾类型 - RiskDescription string `json:"riskDescription"` // 风险描述 - PicCount int64 `json:"picCount"` // 截图数量 - CensorLabels []*VideoCensorLabel `json:"censorLabels"` // 审核标签 - CensorExtension *VideoCensorExtension `json:"censorExtension"` // 质检扩展结果 - Evidences *VideoCallbackEvidence `json:"evidences"` // 机器检测证据信息 - SolutionExtra *VideoSolutionExtra `json:"solutionExtra"` // 额外信息 - ReviewEvidences *VideoReviewEvidence `json:"reviewEvidences"` // 人审证据信息 -} - -// VideoCensorLabel 审核标签 -type VideoCensorLabel struct { - Code string `json:"code"` // 审核标签编码 - Desc string `json:"desc"` // 审核标签描述 - Name string `json:"name"` // 审核标签名称 - CustomCode string `json:"customCode"` // 自定义标签编码 - ParentLabelId string `json:"parentLabelId"` // 父标签ID - Depth int `json:"depth"` // 标签深度 -} - -// VideoCensorExtension 质检扩展结果 -type VideoCensorExtension struct { - QualityInspectionTaskId string `json:"qualityInspectionTaskId"` // 质检任务ID - QualityInspectionType int `json:"qualityInspectionType"` // 质检类型 - InspTaskCreateTime int64 `json:"inspTaskCreateTime"` // 质检任务创建时间 -} - -// VideoCallbackEvidence 机器检测证据信息 -type VideoCallbackEvidence struct { - Text *VideoTextEvidence `json:"text,omitempty"` // 文本证据 - Images *[]VideoImageEvidence `json:"images,omitempty"` // 图片证据 - Audio *VideoAudioEvidence `json:"audio,omitempty"` // 音频证据 - Video *VideoVideoEvidence `json:"video,omitempty"` // 视频证据 -} - -// VideoTextEvidence 文本证据 -type VideoTextEvidence struct { - TaskID string `json:"taskId"` // 任务ID - DataID string `json:"dataId"` // 数据ID - Suggestion int `json:"suggestion"` // 处置建议 - ResultType int `json:"resultType"` // 结果类型 - CensorType int `json:"censorType"` // 审核类型 - IsRelatedHit bool `json:"isRelatedHit"` // 是否关联命中 - Labels []*VideoTextLabel `json:"labels"` // 分类标签 -} - -// VideoTextLabel 文本分类标签 -type VideoTextLabel struct { - Label int `json:"label"` // 标签类型 - Level int `json:"level"` // 级别 - SubLabels []*VideoTextSubLabel `json:"subLabels"` // 二级分类 -} - -// VideoTextSubLabel 文本二级分类 -type VideoTextSubLabel struct { - SubLabel string `json:"subLabel"` // 二级分类 - SubLabelDepth int `json:"subLabelDepth"` // 细分类层级 - SecondLabel string `json:"secondLabel"` // 二级分类 - ThirdLabel string `json:"thirdLabel"` // 三级分类 - Details *VideoTextSubLabelDetail `json:"details"` // 命中详情 -} - -// VideoTextSubLabelDetail 文本二级分类命中详情 -type VideoTextSubLabelDetail struct { - Keywords *[]VideoTextKeyword `json:"keywords,omitempty"` // 敏感词 - LibInfos *[]VideoTextLibInfo `json:"libInfos,omitempty"` // 名单信息 - Anticheat *VideoTextAnticheat `json:"anticheat,omitempty"` // 反作弊 - HitInfos *[]VideoTextHitInfo `json:"hitInfos,omitempty"` // 其他命中 -} - -// VideoTextKeyword 敏感词信息 -type VideoTextKeyword struct { - Word string `json:"word"` // 敏感词 - StrategyGroupId int64 `json:"strategyGroupId"` // 策略组ID - StrategyGroupName string `json:"strategyGroupName"` // 策略组名称 -} - -// VideoTextLibInfo 名单信息 -type VideoTextLibInfo struct { - Type int `json:"type"` // 名单类型 - Entity string `json:"entity"` // 名单内容 - ReleaseTime int64 `json:"releaseTime"` // 释放时间 -} - -// VideoTextAnticheat 反作弊信息 -type VideoTextAnticheat struct { - HitType int `json:"hitType"` // 命中类型 -} - -// VideoTextHitInfo 其他命中信息 -type VideoTextHitInfo struct { - Value string `json:"value"` // 命中值 - Positions *[]VideoTextHitPosition `json:"positions"` // 命中位置 -} - -// VideoTextHitPosition 命中位置 -type VideoTextHitPosition struct { - FieldName string `json:"fieldName"` // 字段名 - StartPos int `json:"startPos"` // 开始位置 - EndPos int `json:"endPos"` // 结束位置 -} - -// VideoImageEvidence 图片证据 -type VideoImageEvidence struct { - Name string `json:"name"` // 图片名称 - DataID string `json:"dataId"` // 数据ID - Suggestion int `json:"suggestion"` // 处置建议 - ResultType int `json:"resultType"` // 结果类型 - Status int `json:"status"` // 状态 - CensorType int `json:"censorType"` // 审核类型 - Labels []*VideoImageLabel `json:"labels"` // 分类标签 - TaskId string `json:"taskId"` // 任务ID -} - -// VideoImageLabel 图片分类标签 -type VideoImageLabel struct { - Label int `json:"label"` // 标签类型 - Level int `json:"level"` // 级别 - Rate float32 `json:"rate"` // 置信度 - SubLabels []*VideoImageSubLabel `json:"subLabels"` // 二级分类 -} - -// VideoImageSubLabel 图片二级分类 -type VideoImageSubLabel struct { - SubLabel interface{} `json:"subLabel"` // 二级分类标签 - SubLabelDepth int `json:"subLabelDepth"` // 细分类层级 - SecondLabel string `json:"secondLabel"` // 二级分类 - ThirdLabel string `json:"thirdLabel"` // 三级分类 - HitStrategy int `json:"hitStrategy"` // 命中策略 - Rate float32 `json:"rate"` // 置信度 - Details *VideoImageSubDetail `json:"details"` // 命中详情 - Explain string `json:"explain"` // 解释 - IsLlmCheck bool `json:"isLlmCheck"` // 是否LLM命中 -} - -// VideoImageSubDetail 图片二级分类命中详情 -type VideoImageSubDetail struct { - Keywords *[]VideoImageHitInfo `json:"keywords,omitempty"` // 敏感词 - LibInfos *[]VideoImageLibInfo `json:"libInfos,omitempty"` // 名单信息 - HitInfos *[]VideoImageHitInfo `json:"hitInfos,omitempty"` // 其他命中 - Anticheat *VideoImageAnticheat `json:"anticheat,omitempty"` // 反作弊 - Llm *VideoImageLlm `json:"llm,omitempty"` // 大模型 -} - -// VideoImageHitInfo 图片命中信息 -type VideoImageHitInfo struct { - Type int `json:"type"` // 命中类型 - Word string `json:"word"` // 敏感词 - Entity string `json:"entity"` // 名单URL - HitCount int `json:"hitCount"` // 命中次数 - Value string `json:"value"` // 值 - Group string `json:"group"` // 分组 - X1 float32 `json:"x1"` // 坐标 - Y1 float32 `json:"y1"` // 坐标 - X2 float32 `json:"x2"` // 坐标 - Y2 float32 `json:"y2"` // 坐标 - StrategyGroupName string `json:"strategyGroupName"` // 策略组名称 - StrategyGroupId int64 `json:"strategyGroupId"` // 策略组ID -} - -// VideoImageLibInfo 图片名单信息 -type VideoImageLibInfo struct { - Type int `json:"type"` // 名单类型 - Entity string `json:"entity"` // 名单URL - HitCount int `json:"hitCount"` // 命中次数 - ReleaseTime int64 `json:"releaseTime"` // 释放时间 - StrategyGroupName string `json:"strategyGroupName"` // 策略组名称 - StrategyGroupId int64 `json:"strategyGroupId"` // 策略组ID -} - -// VideoImageAnticheat 反作弊信息 -type VideoImageAnticheat struct { - HitType int `json:"hitType"` // 命中类型 -} - -// VideoImageLlm 大模型信息 -type VideoImageLlm struct { - Keyword string `json:"keyword"` // 关键词 -} - -// VideoAudioEvidence 音频证据 -type VideoAudioEvidence struct { - TaskID string `json:"taskId"` // 任务ID - DataID string `json:"dataId"` // 数据ID - Status int `json:"status"` // 状态 - Suggestion int `json:"suggestion"` // 处置建议 - Label int `json:"label"` // 标签 - ResultType int `json:"resultType"` // 结果类型 - Callback string `json:"callback"` // 回调参数 - CensorSource int `json:"censorSource"` // 审核来源 - CensorTime int64 `json:"censorTime"` // 审核时间 - Duration int64 `json:"duration"` // 音频时长 - Segments []*VideoAudioSegment `json:"segments"` // 音频片段 -} - -// VideoAudioSegment 音频片段 -type VideoAudioSegment struct { - StartTime int `json:"startTime"` // 开始时间(秒) - EndTime int `json:"endTime"` // 结束时间(秒) - StartTimeMillis int64 `json:"startTimeMillis"` // 开始时间(毫秒) - EndTimeMillis int64 `json:"endTimeMillis"` // 结束时间(毫秒) - Content string `json:"content"` // 语音识别原文 - Type int `json:"type"` // 片段类型:0=语音识别,1=声纹检测 - LeaderName string `json:"leaderName"` // 声纹检测人名 - Labels []*VideoAudioLabel `json:"labels"` // 分类标签 - Url string `json:"url"` // 音频URL -} - -// VideoAudioLabel 音频分类标签 -type VideoAudioLabel struct { - Label int `json:"label"` // 标签类型 - Level int `json:"level"` // 级别 - SubLabels []*VideoAudioSubLabel `json:"subLabels"` // 二级分类 -} - -// VideoAudioSubLabel 音频二级分类 -type VideoAudioSubLabel struct { - SubLabel string `json:"subLabel"` // 细分类 - SubLabelDepth int `json:"subLabelDepth"` // 细分类层级 - SecondLabel string `json:"secondLabel"` // 二级分类 - ThirdLabel string `json:"thirdLabel"` // 三级分类 - SuggestionRiskLevel int `json:"suggestionRiskLevel"` // 嫌疑级别 - Rate float64 `json:"rate"` // 置信度 - RiskDescription string `json:"riskDescription"` // 风险描述 - Details *VideoAudioSubLabelDetail `json:"details"` // 命中详情 -} - -// VideoAudioSubLabelDetail 音频二级分类命中详情 -type VideoAudioSubLabelDetail struct { - HitInfos *[]VideoAudioHitInfo `json:"hitInfos,omitempty"` // 命中内容 - Keywords *[]VideoAudioKeyword `json:"keywords,omitempty"` // 自定义敏感词 - LibInfos *[]VideoAudioLibInfo `json:"libInfos,omitempty"` // 自定义名单 -} - -// VideoAudioHitInfo 音频命中内容 -type VideoAudioHitInfo struct { - Value string `json:"value"` // 命中敏感词或声纹检测分值 - StartTime int `json:"startTime"` // 开始时间 - EndTime int `json:"endTime"` // 结束时间 -} - -// VideoAudioKeyword 自定义敏感词 -type VideoAudioKeyword struct { - Word string `json:"word"` // 敏感词 - StrategyGroupName string `json:"strategyGroupName"` // 策略组名称 - StrategyGroupId int64 `json:"strategyGroupId"` // 策略组ID -} - -// VideoAudioLibInfo 自定义名单 -type VideoAudioLibInfo struct { - ListType int `json:"listType"` // 名单类型 - Entity string `json:"entity"` // 名单内容 -} - -// VideoVideoEvidence 视频证据 -type VideoVideoEvidence struct { - TaskID string `json:"taskId"` // 任务ID - DataID string `json:"dataId"` // 数据ID - Status int `json:"status"` // 状态 - Suggestion int `json:"suggestion"` // 处置建议 - ResultType int `json:"resultType"` // 结果类型 - CensorSource int `json:"censorSource"` // 审核来源 - CensorTime int64 `json:"censorTime"` // 审核时间 - Pictures []*VideoPicture `json:"pictures"` // 截图列表 -} - -// VideoPicture 截图信息 -type VideoPicture struct { - Type int `json:"type"` // 类型 - URL string `json:"url"` // 图片URL - StartTime int64 `json:"startTime"` // 开始时间 - EndTime int64 `json:"endTime"` // 结束时间 - Labels []*VideoPictureLabel `json:"labels"` // 分类标签 - CensorSource int `json:"censorSource"` // 审核来源 - FrontPics []*VideoRelatedPic `json:"frontPics"` // 关联前帧 - BackPics []*VideoRelatedPic `json:"backPics"` // 关联后帧 - PictureID string `json:"pictureId"` // 图片ID -} - -// VideoPictureLabel 截图分类标签 -type VideoPictureLabel struct { - Label int `json:"label"` // 标签类型 - Level int `json:"level"` // 级别 - Rate float32 `json:"rate"` // 置信度 - SubLabels []*VideoPictureSubLabel `json:"subLabels"` // 二级分类 -} - -// VideoPictureSubLabel 截图二级分类 -type VideoPictureSubLabel struct { - SubLabel interface{} `json:"subLabel"` // 二级分类标签 - SubLabelDepth int `json:"subLabelDepth"` // 细分类层级 - SecondLabel string `json:"secondLabel"` // 二级分类 - ThirdLabel string `json:"thirdLabel"` // 三级分类 - HitStrategy int `json:"hitStrategy"` // 命中策略 - Rate float32 `json:"rate"` // 置信度 - Details *VideoPictureSubDetail `json:"details"` // 命中详情 - Explain string `json:"explain"` // 解释 - IsLlmCheck bool `json:"isLlmCheck"` // 是否LLM命中 -} - -// VideoPictureSubDetail 截图二级分类命中详情 -type VideoPictureSubDetail struct { - Keywords *[]VideoPictureHitInfo `json:"keywords,omitempty"` // 敏感词 - LibInfos *[]VideoPictureLibInfo `json:"libInfos,omitempty"` // 名单信息 - HitInfos *[]VideoPictureHitInfo `json:"hitInfos,omitempty"` // 其他命中 - Anticheat *VideoPictureAnticheat `json:"anticheat,omitempty"` // 反作弊 - Llm *VideoPictureLlm `json:"llm,omitempty"` // 大模型 -} - -// VideoPictureHitInfo 截图命中信息 -type VideoPictureHitInfo struct { - Value string `json:"value"` // 命中值 - Group string `json:"group"` // 分组 - Type int `json:"type"` // 类型 - X1 float32 `json:"x1"` // 坐标 - Y1 float32 `json:"y1"` // 坐标 - X2 float32 `json:"x2"` // 坐标 - Y2 float32 `json:"y2"` // 坐标 -} - -// VideoPictureLibInfo 截图名单信息 -type VideoPictureLibInfo struct { - Type int `json:"type"` // 名单类型 - Entity string `json:"entity"` // 名单URL - HitCount int `json:"hitCount"` // 命中次数 - ReleaseTime int64 `json:"releaseTime"` // 释放时间 - StrategyGroupName string `json:"strategyGroupName"` // 策略组名称 - StrategyGroupId int64 `json:"strategyGroupId"` // 策略组ID -} - -// VideoPictureAnticheat 反作弊信息 -type VideoPictureAnticheat struct { - HitType int `json:"hitType"` // 命中类型 -} - -// VideoPictureLlm 大模型信息 -type VideoPictureLlm struct { - Keyword string `json:"keyword"` // 关键词 -} - -// VideoRelatedPic 关联截图 -type VideoRelatedPic struct { - URL string `json:"url"` // 图片URL -} - -// VideoSolutionExtra 额外信息 -type VideoSolutionExtra struct { - FailUnit *VideoFailUnit `json:"failUnit,omitempty"` // 失败单元 -} - -// VideoFailUnit 失败单元 -type VideoFailUnit struct { - Images *[]VideoImageFailUnit `json:"images,omitempty"` // 图片失败单元 - Audio *VideoTargetFailUnit `json:"audio,omitempty"` // 音频失败单元 - Video *VideoTargetFailUnit `json:"video,omitempty"` // 视频失败单元 -} - -// VideoImageFailUnit 图片失败单元 -type VideoImageFailUnit struct { - FailureReason int `json:"failureReason"` // 失败原因 - Name string `json:"name"` // 名称 -} - -// VideoTargetFailUnit 目标失败单元 -type VideoTargetFailUnit struct { - FailureReason int `json:"failureReason"` // 失败原因 -} - -// VideoReviewEvidence 人审证据信息 -type VideoReviewEvidence struct { - Description string `json:"description"` // 描述 - Detail string `json:"detail"` // 详情 - Texts *[]VideoReviewText `json:"texts"` // 文本证据 - Images *[]VideoReviewImage `json:"images"` // 图片证据 - Audios *[]VideoReviewAudio `json:"audios"` // 音频证据 - Videos *[]VideoReviewVideo `json:"videos"` // 视频证据 -} - -// VideoReviewText 人审文本证据 -type VideoReviewText struct { - Snippet string `json:"snippet"` // 片段 - Description string `json:"description"` // 描述 -} - -// VideoReviewImage 人审图片证据 -type VideoReviewImage struct { - URL string `json:"url"` // 图片URL - Description string `json:"description"` // 描述 -} - -// VideoReviewAudio 人审音频证据 -type VideoReviewAudio struct { - StartTime int64 `json:"startTime"` // 开始时间 - EndTime int64 `json:"endTime"` // 结束时间 - Description string `json:"description"` // 描述 -} - -// VideoReviewVideo 人审视频证据 -type VideoReviewVideo struct { - StartTime int64 `json:"startTime"` // 开始时间 - EndTime int64 `json:"endTime"` // 结束时间 - URL string `json:"url"` // 视频URL - Description string `json:"description"` // 描述 -} - -// ProcessVideoCallback 处理视频检测回调(推送模式) -func (s *VideoDetectionService) ProcessVideoCallback(ctx context.Context, callbackData string) error { - if callbackData == "" { - return fmt.Errorf("回调数据不能为空") - } - - var data VideoCallbackData - if err := json.Unmarshal([]byte(callbackData), &data); err != nil { - g.Log().Errorf(ctx, "解析视频回调数据失败: %v", err) - return fmt.Errorf("解析视频回调数据失败: %w", err) - } - - if data.Antispam == nil { - return fmt.Errorf("视频回调数据格式错误:缺少antispam字段") - } - - g.Log().Infof(ctx, "处理视频检测结果 - taskId: %s, suggestion: %d, resultType: %d, status: %d", - data.Antispam.TaskID, data.Antispam.Suggestion, data.Antispam.ResultType, data.Antispam.Status) - - // TODO: 业务逻辑,如保存数据库、触发后续流程等 - // 可使用完整字段:data.Antispam.Evidences, data.Antispam.CensorLabels, data.Antispam.Duration 等 - - return nil + TaskID string `json:"taskId"` + DataID string `json:"dataId"` + Suggestion int `json:"suggestion"` + Status int `json:"status"` + ResultType int `json:"resultType"` + Label int `json:"label"` + SecondLabel string `json:"secondLabel"` + ThirdLabel string `json:"thirdLabel"` + RiskDescription string `json:"riskDescription"` + CensorSource int `json:"censorSource"` + CensorTime int64 `json:"censorTime"` + CheckTime int64 `json:"checkTime"` + Duration int64 `json:"duration"` } diff --git a/sql/alter_material_verify_log_add_risk_description.sql b/sql/alter_material_verify_log_add_risk_description.sql new file mode 100644 index 0000000..9d9ad3f --- /dev/null +++ b/sql/alter_material_verify_log_add_risk_description.sql @@ -0,0 +1,5 @@ +-- 补充 material_verify_log 表的 risk_description 字段 +ALTER TABLE material_verify_log + ADD COLUMN IF NOT EXISTS risk_description TEXT DEFAULT '' NOT NULL; + +COMMENT ON COLUMN material_verify_log.risk_description IS '风险描述(易盾返回,如:广告法|涉嫌欺诈消费者)';