diff --git a/YIDUN_API_DOCUMENTATION.md b/YIDUN_API_DOCUMENTATION.md index 6f8147b..8a64eab 100644 --- a/YIDUN_API_DOCUMENTATION.md +++ b/YIDUN_API_DOCUMENTATION.md @@ -4,11 +4,14 @@ - [概述](#概述) - [配置说明](#配置说明) +- [模式切换](#模式切换) - [API接口列表](#api接口列表) - [文本检测](#1-文本检测) - [图片检测](#2-图片检测) - [视频检测](#3-视频检测) +- [回调与轮询接口](#回调与轮询接口) - [错误码说明](#错误码说明) +- [常见问题](#常见问题) --- @@ -27,7 +30,7 @@ ### 服务地址 ``` -http://localhost:3002 +http://localhost:3001 ``` --- @@ -38,38 +41,100 @@ http://localhost:3002 ```yaml yidun: - # 音视频检测配置 + # 回调模式开关: true=使用回调模式(需要公网地址), false=使用轮询模式 + callback_mode: false + + # 视频检测配置 video: - business_id: "YOUR_VIDEO_BUSINESS_ID" secret_id: "f58a38341ca6227014df7c3bf0e6f16f" secret_key: "526aa631ba5d518aedeb70b5a3b67371" - + callback_url: "http://your-domain.com:3001/yidun/callback/receiveVideo" + # 图片检测配置 image: - business_id: "YOUR_IMAGE_BUSINESS_ID" + business_id: "your_image_business_id" secret_id: "9a82f90bfec61eb40d1c95605b894817" secret_key: "f73a78954417a3713c36ec2d14eb2b5f" - + callback_url: "http://your-domain.com:3001/yidun/callback/receiveImage" + # 文本检测配置 text: business_id: "YOUR_TEXT_BUSINESS_ID" secret_id: "YOUR_TEXT_SECRET_ID" secret_key: "YOUR_TEXT_SECRET_KEY" + +# 内容送检定时任务配置 +content_check: + batch_size: 10 # 每批处理数量 + image_enabled: true # 是否启用图片检测 + video_enabled: true # 是否启用视频检测 + interval_seconds: 30 # 定时任务执行间隔(秒) +``` + +--- + +## 模式切换 + +### 轮询模式(无公网地址) + +适用于开发测试环境或没有公网地址的场景。 + +```yaml +yidun: + callback_mode: false # 使用轮询模式 +``` + +**工作流程**: +``` +定时任务 → 提交检测 → 保存taskId → 手动轮询 → 获取结果 → 更新状态 +``` + +### 回调模式(有公网地址) + +适用于生产环境,需要配置公网可访问的回调地址。 + +```yaml +yidun: + callback_mode: true # 使用回调模式 + image: + callback_url: "http://your-public-domain.com:3001/yidun/callback/receiveImage" + video: + callback_url: "http://your-public-domain.com:3001/yidun/callback/receiveVideo" +``` + +**工作流程**: +``` +定时任务 → 提交检测 → 易盾检测完成 → 易盾推送结果 → 自动更新状态 ``` --- ## API接口列表 +### 送检接口 + | 接口名称 | 请求方法 | 路径 | 说明 | |---------|---------|------|------| | 文本检测提交 | POST | `/yidun/detect-text` | 提交文本进行异步检测 | | 图片检测提交 | POST | `/yidun/detect-image` | 提交图片进行异步检测 | | 视频检测提交 | POST | `/yidun/detect-video` | 提交视频进行检测 | -| 图片结果查询 | POST | `/yidun/GetImageResult` | 查询图片检测结果(轮询模式) | -| 图片检测回调 | POST | `/yidun/receive-image-callback` | 接收图片检测结果推送(推送模式) | -| 视频结果查询 | POST | `/yidun/GetVideoResult` | 查询视频检测结果(轮询模式) | -| 视频检测回调 | POST | `/yidun/receive-video-callback` | 接收视频检测结果推送(推送模式) | + +### 回调模式接口 + +| 接口名称 | 请求方法 | 路径 | 说明 | +|---------|---------|------|------| +| 接收图片回调 | POST | `/yidun/callback/receiveImage` | 接收易盾图片检测结果推送 | +| 接收视频回调 | POST | `/yidun/callback/receiveVideo` | 接收易盾视频检测结果推送 | + +### 轮询模式接口 + +| 接口名称 | 请求方法 | 路径 | 说明 | +|---------|---------|------|------| +| 轮询所有结果 | POST | `/yidun/callback/poll` | 轮询所有待处理结果(图片+视频) | +| 轮询图片结果 | POST | `/yidun/callback/pollImage` | 仅轮询图片待处理结果 | +| 轮询视频结果 | POST | `/yidun/callback/pollVideo` | 仅轮询视频待处理结果 | +| 查询单个结果 | POST | `/yidun/callback/pollTask` | 根据taskId查询单个结果 | +| 获取待处理数量 | GET | `/yidun/callback/pendingCount` | 查看待处理结果数量 | **注意**:检测结果是异步的,提交接口只返回 `task_id`,需要通过回调或轮询获取结果。 @@ -814,6 +879,197 @@ callbackData={"antispam":{...}}&signature=xxx&secretId=xxx --- +## 回调与轮询接口 + +### 轮询模式接口详解 + +#### 轮询所有待处理结果 + +``` +POST /yidun/callback/poll +``` + +**说明**:轮询所有图片和视频的待处理检测结果 + +**响应示例**: +```json +{ + "code": 200, + "msg": "轮询完成,成功处理 5 条,失败 1 条", + "data": { + "success_count": 5, + "fail_count": 1, + "pending_count": 10 + } +} +``` + +#### 获取待处理数量 + +``` +GET /yidun/callback/pendingCount +``` + +**响应示例**: +```json +{ + "code": 200, + "data": { + "pending_count": 15, + "description": "待查询结果的日志数量(状态为pending且有taskID)" + } +} +``` + +#### 查询单个结果 + +``` +POST /yidun/callback/pollTask?taskId=xxx&type=image +``` + +**参数说明**: + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| taskId | string | 是 | 易盾任务ID | +| type | string | 否 | 类型:`image`/`video`,不填则自动识别 | + +**响应示例**: +```json +{ + "code": 200, + "msg": "查询并处理成功" +} +``` + +--- + +## 使用示例 + +### 轮询模式使用流程 + +```bash +# 1. 查看待处理数量 +curl http://localhost:3001/yidun/callback/pendingCount + +# 2. 执行轮询(所有) +curl -X POST http://localhost:3001/yidun/callback/poll + +# 3. 或分别轮询 +curl -X POST http://localhost:3001/yidun/callback/pollImage +curl -X POST http://localhost:3001/yidun/callback/pollVideo + +# 4. 查询单个结果 +curl -X POST "http://localhost:3001/yidun/callback/pollTask?taskId=abc123&type=image" +``` + +### 配置定时轮询 + +```bash +# 每分钟轮询一次 +*/1 * * * * curl -X POST http://localhost:3001/yidun/callback/poll + +# 或更保守的频率(每5分钟) +*/5 * * * * curl -X POST http://localhost:3001/yidun/callback/poll +``` + +--- + +## 状态说明 + +### 检测状态转换 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 送检前 │ +│ tencent_image/tencent_video: status = "pending" │ +└─────────────────────────────────────────────────────────────┘ + ↓ + ↓ 定时任务/手动送检 + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ 提交检测 │ +│ material_verify_log: verify_status = "PENDING" │ +│ material_verify_log: task_id = "易盾任务ID" │ +│ tencent_xxx: status = "submitting" │ +└─────────────────────────────────────────────────────────────┘ + ↓ + ↓ 等待检测完成 + ↓ (回调模式)或(轮询查询) + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ 检测完成 │ +│ material_verify_log: verify_status = "VERIFIED" (通过) │ +│ 或 "REJECTED" (不通过) │ +│ 或 "PENDING" (嫌疑,需人工审核) │ +│ tencent_xxx: status = "VERIFIED"/"REJECTED"/"PENDING" │ +└─────────────────────────────────────────────────────────────┘ +``` + +### 状态值说明 + +| 状态值 | 说明 | 触发条件 | +|--------|------|----------| +| `pending` / `PENDING` | 待检测 | 素材创建时 | +| `submitting` | 送检中 | 提交至易盾检测 | +| `VERIFIED` | 校验通过 | 易盾建议通过(suggestion=0)| +| `REJECTED` | 校验不通过 | 易盾建议不通过(suggestion=2)或检测失败 | +| `PENDING` (人审) | 嫌疑待审 | 易盾建议人工审核(suggestion=1)| + +### 易盾处置建议对照 + +| suggestion值 | 说明 | 系统状态 | +|-------------|------|----------| +| 0 | 通过 | VERIFIED | +| 1 | 嫌疑,需人工审核 | PENDING | +| 2 | 不通过 | REJECTED | + +--- + +## 常见问题 + +### Q1: 轮询后待处理数量没有减少? + +**可能原因**: +1. 检测仍在进行中(易盾尚未返回结果) +2. 上次查询出错,状态已更新为 REJECTED + +**排查方法**: +```bash +# 查看日志 +grep "检测仍在进行中" resource/log/server/cid.log + +# 查看数据库状态 +SELECT id, task_id, verify_status FROM material_verify_log WHERE task_id = 'xxx'; +``` + +### Q2: 如何判断是图片还是视频? + +```bash +# 方法1:查询日志 +SELECT id, material_type, task_id, verify_status FROM material_verify_log WHERE task_id = 'xxx'; + +# 方法2:调用接口指定类型 +curl -X POST "http://localhost:3001/yidun/callback/pollTask?taskId=xxx&type=image" +``` + +### Q3: 回调模式收不到回调? + +**排查步骤**: +1. 确认公网地址可访问:`curl http://your-domain.com:3001/yidun/callback/receiveImage` +2. 确认易盾控制台配置了正确的回调地址 +3. 检查服务器防火墙/安全组是否开放端口 +4. 查看日志确认请求是否到达:`grep "收到易盾" resource/log/server/cid.log` + +### Q4: 状态不一致? + +如果 `material_verify_log` 和 `tencent_image/video` 表状态不一致,检查: +1. 代码执行过程中是否有报错 +2. 是否有并发更新导致覆盖 +3. 日志表和原表更新是否在同一事务中 + +--- + ## 错误码说明 | code | 说明 | @@ -872,4 +1128,4 @@ func detectVideo() (string, error) { --- -**最后更新**: 2026-05-07 +**最后更新**: 2026-05-14 diff --git a/cid b/cid new file mode 100755 index 0000000..57062be Binary files /dev/null and b/cid differ diff --git a/config.yml b/config.yml index 1a160de..604c07e 100644 --- a/config.yml +++ b/config.yml @@ -13,10 +13,10 @@ rate: database: default: - type: "pgsql" - host: "localhost" - port: "5432" + host: "116.204.74.41" + port: "15432" user: "postgres" - pass: "123456" + pass: "Bjang09@686^*^" name: "cid" role: "master" maxIdle: "5" @@ -29,6 +29,25 @@ database: updatedAt: "updated_at" deletedAt: "deleted_at" timeMaintainDisabled: false + # data-engine 数据库配置(用于存放 tencent_image, tencent_video 等送检表) + dataEngine: + - type: "pgsql" + host: "116.204.74.41" + port: "15432" + user: "postgres" + pass: "Bjang09@686^*^" + name: "dataengine" + role: "master" + maxIdle: "5" + maxOpen: "20" + maxLifetime: "60s" + charset: "utf8mb4" + debug: true + dryRun: false + createdAt: "created_at" + updatedAt: "updated_at" + deletedAt: "deleted_at" + timeMaintainDisabled: false redis: # 集群模式配置方法 @@ -49,13 +68,20 @@ jaeger: #链路追踪 addr: 116.204.74.41:4318 yidun: - # 音视频检测配置 + # 回调模式开关: true=使用回调模式(需要公网地址), false=使用轮询模式 + callback_mode: false + + # 视频检测配置 video: + business_id: "YD00256761935486" secret_id: "f58a38341ca6227014df7c3bf0e6f16f" secret_key: "526aa631ba5d518aedeb70b5a3b67371" region: "cn-hangzhou" protocol: "https" max_retry_count: 3 + # 易盾回调地址(用于接收检测结果推送) + # 替换为实际可访问的地址 + callback_url: "http://your-domain.com:3001/yidun/callback/receiveVideo" # 图片检测配置 image: @@ -65,6 +91,9 @@ yidun: region: "cn-hangzhou" protocol: "https" max_retry_count: 3 + # 易盾回调地址(用于接收检测结果推送) + # 替换为实际可访问的地址 + callback_url: "http://your-domain.com:3001/yidun/callback/receiveImage" # 文本检测配置(如需要请补充) text: @@ -73,4 +102,20 @@ yidun: secret_key: "YOUR_TEXT_SECRET_KEY" region: "cn-hangzhou" protocol: "https" - max_retry_count: 3 \ No newline at end of file + max_retry_count: 3 + # 易盾回调地址(用于接收检测结果推送) + # 替换为实际可访问的地址 + callback_url: "http://your-domain.com:3001/yidun/callback/receiveText" + +# 内容送检定时任务配置 +content_check: + # 是否启动定时送检任务(true=启动定时任务自动送检,false=不启动,仅通过API手动送检) + scheduler_enabled: false + # 每批处理数量 + batch_size: 10 + # 是否启用图片检测 + image_enabled: false + # 是否启用视频检测 + video_enabled: false + # 定时任务执行间隔(秒) + interval_seconds: 30 \ No newline at end of file diff --git a/consts/dataengine/check_status.go b/consts/dataengine/check_status.go new file mode 100644 index 0000000..e88ebfb --- /dev/null +++ b/consts/dataengine/check_status.go @@ -0,0 +1,22 @@ +package dataengine + +// 送检状态常量 +const ( + // SourceTable 来源表标识 + SourceTableTencentImage = "tencent_image" + SourceTableTencentVideo = "tencent_video" + + // CheckStatus 送检状态 + CheckStatusPending = "PENDING" // 待送检 + CheckStatusSubmitting = "SUBMITTING" // 送检中 + CheckStatusSuccess = "SUCCESS" // 送检成功 + CheckStatusFailed = "FAILED" // 送检失败 + CheckStatusCompleted = "COMPLETED" // 检测完成 +) + +// Suggestion 处置建议 +const ( + SuggestionPass = 0 // 通过 + SuggestionReview = 1 // 嫌疑,需人工审核 + SuggestionBlock = 2 // 不通过 +) diff --git a/consts/dataengine/table.go b/consts/dataengine/table.go new file mode 100644 index 0000000..531062f --- /dev/null +++ b/consts/dataengine/table.go @@ -0,0 +1,8 @@ +package dataengine + +// PostgreSQL表名常量 +const ( + TencentImageTable = "tencent_image" // 图片送检表 + TencentVideoTable = "tencent_video" // 视频送检表 + TencentContentCheckLogTable = "tencent_content_check_log" // 送检日志表 +) diff --git a/controller/dataengine/material_verify_controller.go b/controller/dataengine/material_verify_controller.go new file mode 100644 index 0000000..c6b4559 --- /dev/null +++ b/controller/dataengine/material_verify_controller.go @@ -0,0 +1,509 @@ +package dataengine + +import ( + consts "cid/consts/dataengine" + dao "cid/dao/dataengine" + entity "cid/model/entity/dataengine" + serviceDataengine "cid/service/dataengine" + "context" + "fmt" + "time" + + "github.com/gogf/gf/v2/frame/g" +) + +// MaterialVerifyController 素材校验控制器 +type MaterialVerifyController struct{} + +// MaterialVerify 控制器单例 +var MaterialVerify = new(MaterialVerifyController) + +// ============================================================================= +// 请求/响应结构体 +// ============================================================================= + +// ImageListReq 图片列表请求 +type ImageListReq struct { + Status string `json:"status"` + AccountID int64 `json:"accountId"` + Page int `json:"page"` + PageSize int `json:"pageSize"` + StartTime int64 `json:"startTime"` + EndTime int64 `json:"endTime"` +} + +// ImageListRes 图片列表响应 +type ImageListRes struct { + List interface{} `json:"list"` + Total int `json:"total"` +} + +// StatsRes 统计响应 +type StatsRes struct { + Pending int `json:"pending"` + Verified int `json:"verified"` + Rejected int `json:"rejected"` +} + +// VideoListReq 视频列表请求 +type VideoListReq struct { + Status string `json:"status"` + AccountID int64 `json:"accountId"` + Page int `json:"page"` + PageSize int `json:"pageSize"` + StartTime int64 `json:"startTime"` + EndTime int64 `json:"endTime"` +} + +// VideoListRes 视频列表响应 +type VideoListRes struct { + List interface{} `json:"list"` + Total int `json:"total"` +} + +// LogListReq 日志列表请求 +type LogListReq struct { + MaterialType string `json:"materialType"` + MaterialID string `json:"materialId"` + VerifyStatus string `json:"verifyStatus"` + AccountID int64 `json:"accountId"` + Page int `json:"page"` + PageSize int `json:"pageSize"` + StartTime int64 `json:"startTime"` + EndTime int64 `json:"endTime"` +} + +// ManualVerifyReq 手动校验请求 +type ManualVerifyReq struct { + MaterialID string `json:"materialId" v:"required#素材ID不能为空"` +} + +// TaskIDReq 任务ID请求 +type TaskIDReq struct { + TaskID string `json:"taskId" v:"required#任务ID不能为空"` +} + +// ImageCallbackReq 图片回调请求 +type ImageCallbackReq struct { + CallbackData string `json:"callbackData"` +} + +// VideoCallbackReq 视频回调请求 +type VideoCallbackReq struct { + CallbackData string `json:"callbackData"` +} + +// BatchVerifyReq 批量校验请求 +type BatchVerifyReq struct { + Limit int `json:"limit"` +} + +// ============================================================================= +// 图片素材接口 +// ============================================================================= + +// ListImage 图片素材列表 +func (c *MaterialVerifyController) ListImage(ctx context.Context, req *ImageListReq) (res *ImageListRes, err error) { + if req.Page == 0 { + req.Page = 1 + } + if req.PageSize == 0 { + req.PageSize = 20 + } + + condition := make(map[string]interface{}) + if req.Status != "" { + condition[entity.TencentImageCols.VerifyStatus] = req.Status + } + if req.AccountID > 0 { + condition[entity.TencentImageCols.AccountID] = req.AccountID + } + + data, total, err := dao.TencentImage.GetByCondition(ctx, condition, req.Page, req.PageSize) + if err != nil { + return nil, err + } + + return &ImageListRes{ + List: data, + Total: total, + }, nil +} + +// StatsImage 图片素材统计 +func (c *MaterialVerifyController) StatsImage(ctx context.Context, req *ImageListReq) (res *StatsRes, err error) { + // 使用实体中定义的正确状态值:PENDING=待校验, VERIFIED=校验通过, REJECTED=校验不通过 + pending, _ := dao.TencentImage.CountByStatus(ctx, entity.VerifyStatusPending) + verified, _ := dao.TencentImage.CountByStatus(ctx, entity.VerifyStatusVerified) + rejected, _ := dao.TencentImage.CountByStatus(ctx, entity.VerifyStatusRejected) + + return &StatsRes{ + Pending: pending, + Verified: verified, + Rejected: rejected, + }, nil +} + +// ============================================================================= +// 视频素材接口 +// ============================================================================= + +// ListVideo 视频素材列表 +func (c *MaterialVerifyController) ListVideo(ctx context.Context, req *VideoListReq) (res *VideoListRes, err error) { + if req.Page == 0 { + req.Page = 1 + } + if req.PageSize == 0 { + req.PageSize = 20 + } + + condition := make(map[string]interface{}) + if req.Status != "" { + condition[entity.TencentVideoCols.VerifyStatus] = req.Status + } + if req.AccountID > 0 { + condition[entity.TencentVideoCols.AccountID] = req.AccountID + } + + data, total, err := dao.TencentVideo.GetByCondition(ctx, condition, req.Page, req.PageSize) + if err != nil { + return nil, err + } + + return &VideoListRes{ + List: data, + Total: total, + }, nil +} + +// StatsVideo 视频素材统计 +func (c *MaterialVerifyController) StatsVideo(ctx context.Context, req *VideoListReq) (res *StatsRes, err error) { + // 使用实体中定义的正确状态值:PENDING=待校验, VERIFIED=校验通过, REJECTED=校验不通过 + pending, _ := dao.TencentVideo.CountByStatus(ctx, entity.VerifyStatusPending) + verified, _ := dao.TencentVideo.CountByStatus(ctx, entity.VerifyStatusVerified) + rejected, _ := dao.TencentVideo.CountByStatus(ctx, entity.VerifyStatusRejected) + + return &StatsRes{ + Pending: pending, + Verified: verified, + Rejected: rejected, + }, nil +} + +// ============================================================================= +// 校验日志接口 +// ============================================================================= + +// ListLogRes 日志列表响应 +type ListLogRes struct { + List interface{} `json:"list"` + Total int `json:"total"` +} + +// ListLog 日志列表 +func (c *MaterialVerifyController) ListLog(ctx context.Context, req *LogListReq) (res *ListLogRes, err error) { + if req.Page == 0 { + req.Page = 1 + } + if req.PageSize == 0 { + req.PageSize = 20 + } + + condition := make(map[string]interface{}) + if req.MaterialType != "" { + condition[entity.MaterialVerifyLogCols.MaterialType] = req.MaterialType + } + if req.MaterialID != "" { + condition[entity.MaterialVerifyLogCols.MaterialID] = req.MaterialID + } + if req.VerifyStatus != "" { + condition[entity.MaterialVerifyLogCols.VerifyStatus] = req.VerifyStatus + } + if req.AccountID > 0 { + condition[entity.MaterialVerifyLogCols.AccountID] = req.AccountID + } + + data, total, err := serviceDataengine.MaterialVerify.GetLogsByCondition(ctx, condition, req.Page, req.PageSize) + if err != nil { + return nil, err + } + + return &ListLogRes{ + List: data, + Total: total, + }, nil +} + +// LogDetailRes 日志详情响应 +type LogDetailRes struct { + *entity.MaterialVerifyLog + PreviewURL string `json:"previewURL"` +} + +// GetLogDetailReq 日志详情请求 +type GetLogDetailReq struct { + Id int64 `json:"id" v:"required#日志ID不能为空"` +} + +// GetLogDetail 日志详情 +func (c *MaterialVerifyController) GetLogDetail(ctx context.Context, req *GetLogDetailReq) (res *LogDetailRes, err error) { + log, err := serviceDataengine.MaterialVerify.GetLogByID(ctx, req.Id) + if err != nil { + return nil, err + } + if log == nil { + return nil, fmt.Errorf("日志不存在") + } + + // 获取来源数据预览 + res = &LogDetailRes{ + MaterialVerifyLog: log, + } + if log.SourceTable == consts.SourceTableTencentImage { + image, _ := dao.TencentImage.GetByID(ctx, log.SourceID) + if image != nil { + res.PreviewURL = image.PreviewURL + } + } else if log.SourceTable == consts.SourceTableTencentVideo { + video, _ := dao.TencentVideo.GetByID(ctx, log.SourceID) + if video != nil { + res.PreviewURL = video.PreviewURL + } + } + + return +} + +// StatsLogRes 日志统计响应 +type StatsLogRes struct { + Total int `json:"total"` + Pending int `json:"pending"` + Verified int `json:"verified"` + Rejected int `json:"rejected"` +} + +// StatsLog 日志统计 +func (c *MaterialVerifyController) StatsLog(ctx context.Context, req *LogListReq) (res *StatsLogRes, err error) { + stats, err := serviceDataengine.MaterialVerify.GetStats(ctx) + if err != nil { + return nil, err + } + + return &StatsLogRes{ + Total: stats["total"], + Pending: stats["pending"], + Verified: stats["verified"], + Rejected: stats["rejected"], + }, nil +} + +// ============================================================================= +// 手动校验接口 +// ============================================================================= + +// ManualVerifyImageRes 手动校验响应 +type ManualVerifyImageRes struct { + Id int64 `json:"id"` + TaskID string `json:"taskId"` + SourceID string `json:"sourceId"` +} + +// ManualVerifyImage 手动校验图片 +func (c *MaterialVerifyController) ManualVerifyImage(ctx context.Context, req *ManualVerifyReq) (res *ManualVerifyImageRes, err error) { + log, err := serviceDataengine.MaterialVerify.VerifyImageByID(ctx, req.MaterialID) + if err != nil { + return nil, err + } + + return &ManualVerifyImageRes{ + Id: log.Id, + TaskID: log.TaskID, + SourceID: fmt.Sprintf("%d", log.SourceID), + }, nil +} + +// ManualVerifyVideo 手动校验视频 +func (c *MaterialVerifyController) ManualVerifyVideo(ctx context.Context, req *ManualVerifyReq) (res *ManualVerifyImageRes, err error) { + log, err := serviceDataengine.MaterialVerify.VerifyVideoByID(ctx, req.MaterialID) + if err != nil { + return nil, err + } + + return &ManualVerifyImageRes{ + Id: log.Id, + TaskID: log.TaskID, + SourceID: fmt.Sprintf("%d", log.SourceID), + }, nil +} + +// ============================================================================= +// 批量校验接口 +// ============================================================================= + +// BatchVerifyRes 批量校验响应 +type BatchVerifyRes struct { + Success int `json:"success"` + Fail int `json:"fail"` + Total int `json:"total"` + Message string `json:"message"` +} + +// BatchVerifyImage 批量校验图片 +func (c *MaterialVerifyController) BatchVerifyImage(ctx context.Context, req *BatchVerifyReq) (res *BatchVerifyRes, err error) { + if req.Limit <= 0 { + req.Limit = 100 + } + + images, err := dao.TencentImage.GetPendingList(ctx, req.Limit) + if err != nil { + return nil, err + } + + successCount := 0 + failCount := 0 + + for _, image := range images { + log, err := serviceDataengine.MaterialVerify.VerifyImageByID(ctx, image.ImageID) + if err != nil { + failCount++ + g.Log().Errorf(ctx, "图片校验失败: %s, error: %v", image.ImageID, err) + } else { + successCount++ + g.Log().Infof(ctx, "图片校验已提交: %s, logId: %d", image.ImageID, log.Id) + } + time.Sleep(100 * time.Millisecond) + } + + // 等待易盾处理,然后自动查询结果 + msg := fmt.Sprintf("批量校验完成,成功: %d,失败: %d", successCount, failCount) + if successCount > 0 { + g.Log().Infof(ctx, "提交完成,等待2秒后自动查询结果...") + time.Sleep(2 * time.Second) + pollSuccess, pollFail, _ := serviceDataengine.MaterialVerify.PollPendingResults(ctx) + msg = fmt.Sprintf("批量校验完成,提交成功: %d,提交失败: %d,自动查询成功: %d,未就绪: %d", + successCount, failCount, pollSuccess, pollFail) + } + + return &BatchVerifyRes{ + Success: successCount, + Fail: failCount, + Total: len(images), + Message: msg, + }, nil +} + +// BatchVerifyVideo 批量校验视频 +func (c *MaterialVerifyController) BatchVerifyVideo(ctx context.Context, req *BatchVerifyReq) (res *BatchVerifyRes, err error) { + if req.Limit <= 0 { + req.Limit = 100 + } + + videos, err := dao.TencentVideo.GetPendingList(ctx, req.Limit) + if err != nil { + return nil, err + } + + successCount := 0 + failCount := 0 + + for _, video := range videos { + log, err := serviceDataengine.MaterialVerify.VerifyVideoByID(ctx, video.VideoID) + if err != nil { + failCount++ + g.Log().Errorf(ctx, "视频校验失败: %s, error: %v", video.VideoID, err) + } else { + successCount++ + g.Log().Infof(ctx, "视频校验已提交: %s, logId: %d", video.VideoID, log.Id) + } + time.Sleep(100 * time.Millisecond) + } + + // 等待易盾处理,然后自动查询结果 + msg := fmt.Sprintf("批量校验完成,成功: %d,失败: %d", successCount, failCount) + if successCount > 0 { + g.Log().Infof(ctx, "提交完成,等待2秒后自动查询结果...") + time.Sleep(2 * time.Second) + pollSuccess, pollFail, _ := serviceDataengine.MaterialVerify.PollPendingResults(ctx) + msg = fmt.Sprintf("批量校验完成,提交成功: %d,提交失败: %d,自动查询成功: %d,未就绪: %d", + successCount, failCount, pollSuccess, pollFail) + } + + return &BatchVerifyRes{ + Success: successCount, + Fail: failCount, + Total: len(videos), + Message: msg, + }, nil +} + +// ============================================================================= +// 回调处理接口 +// ============================================================================= + +// CallbackRes 回调响应 +type CallbackRes struct { + Code int `json:"code"` + Msg string `json:"msg"` +} + +// ImageCallback 图片校验回调 +func (c *MaterialVerifyController) ImageCallback(ctx context.Context, req *ImageCallbackReq) (res *CallbackRes, err error) { + if req.CallbackData == "" { + return &CallbackRes{Code: 400, Msg: "callbackData不能为空"}, nil + } + + err = serviceDataengine.MaterialVerify.ProcessImageCallback(ctx, req.CallbackData) + if err != nil { + return &CallbackRes{Code: 500, Msg: err.Error()}, nil + } + + return &CallbackRes{Code: 200, Msg: "处理成功"}, nil +} + +// VideoCallback 视频校验回调 +func (c *MaterialVerifyController) VideoCallback(ctx context.Context, req *VideoCallbackReq) (res *CallbackRes, err error) { + if req.CallbackData == "" { + return &CallbackRes{Code: 400, Msg: "callbackData不能为空"}, nil + } + + err = serviceDataengine.MaterialVerify.ProcessVideoCallback(ctx, req.CallbackData) + if err != nil { + return &CallbackRes{Code: 500, Msg: err.Error()}, nil + } + + return &CallbackRes{Code: 200, Msg: "处理成功"}, nil +} + +// ResultRes 结果查询响应 +type ResultRes struct { + Code int `json:"code"` + Msg string `json:"msg"` +} + +// ImageResult 图片校验结果查询(轮询模式) +func (c *MaterialVerifyController) ImageResult(ctx context.Context, req *TaskIDReq) (res *ResultRes, err error) { + if req.TaskID == "" { + return &ResultRes{Code: 400, Msg: "taskId不能为空"}, nil + } + + err = serviceDataengine.MaterialVerify.ProcessImageResultByTask(ctx, req.TaskID) + if err != nil { + return &ResultRes{Code: 500, Msg: err.Error()}, nil + } + + return &ResultRes{Code: 200, Msg: "处理成功"}, nil +} + +// VideoResult 视频校验结果查询(轮询模式) +func (c *MaterialVerifyController) VideoResult(ctx context.Context, req *TaskIDReq) (res *ResultRes, err error) { + if req.TaskID == "" { + return &ResultRes{Code: 400, Msg: "taskId不能为空"}, nil + } + + err = serviceDataengine.MaterialVerify.ProcessVideoResultByTask(ctx, req.TaskID) + if err != nil { + return &ResultRes{Code: 500, Msg: err.Error()}, nil + } + + return &ResultRes{Code: 200, Msg: "处理成功"}, nil +} diff --git a/controller/yidun/content_check_controller.go b/controller/yidun/content_check_controller.go new file mode 100644 index 0000000..e6e97a4 --- /dev/null +++ b/controller/yidun/content_check_controller.go @@ -0,0 +1,175 @@ +package yidun + +import ( + dto "cid/model/dto/yidun" + serviceDataengine "cid/service/dataengine" + "context" + + "gitea.com/red-future/common/beans" +) + +// ContentCheckController 内容送检控制器 +type ContentCheckController struct{} + +// ContentCheck 内容送检控制器单例 +var ContentCheck = new(ContentCheckController) + +// StatusRes 状态响应 +type StatusRes struct { + Running bool `json:"running"` + Config serviceDataengine.ContentCheckConfig `json:"config"` + PendingStats map[string]int `json:"pending_stats"` +} + +// 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"}) + + if serviceDataengine.TencentContentCheck.IsRunning() { + return &beans.ResponseEmpty{}, nil + } + + // 如果有配置参数,更新配置 + if req.BatchSize > 0 || req.IntervalSeconds > 0 { + config := serviceDataengine.ContentCheckConfig{ + BatchSize: req.BatchSize, + ImageEnabled: req.ImageEnabled, + VideoEnabled: req.VideoEnabled, + IntervalSeconds: req.IntervalSeconds, + } + serviceDataengine.TencentContentCheck.SetConfig(config) + } + + err = serviceDataengine.TencentContentCheck.Start(ctx) + if err != nil { + return nil, err + } + return &beans.ResponseEmpty{}, nil +} + +// 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"}) + 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"}) + + res = &StatusRes{ + Running: serviceDataengine.TencentContentCheck.IsRunning(), + Config: serviceDataengine.TencentContentCheck.GetConfig(), + PendingStats: serviceDataengine.TencentContentCheck.GetPendingStats(ctx), + } + return +} + +// 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"}) + + if req.CallbackData == "" { + return nil, err + } + + err = serviceDataengine.TencentContentCallback.ProcessImageCallback(ctx, req.CallbackData) + return +} + +// 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"}) + + if req.CallbackData == "" { + return nil, err + } + + err = serviceDataengine.TencentContentCallback.ProcessVideoCallback(ctx, req.CallbackData) + return +} + +// 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"}) + + if req.TaskID == "" { + return nil, err + } + + err = serviceDataengine.TencentContentCallback.ProcessImageResult(ctx, req.TaskID) + return +} + +// 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"}) + + if req.TaskID == "" { + return nil, err + } + + err = serviceDataengine.TencentContentCallback.ProcessVideoResult(ctx, req.TaskID) + return +} + +// 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"}) + + result, err := serviceDataengine.TencentContentCheck.SubmitImageByID(ctx, req.ImageID) + if err != nil { + return nil, err + } + + res = &dto.ManualSubmitRes{ + TaskID: result.TaskID, + } + return +} + +// 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"}) + + result, err := serviceDataengine.TencentContentCheck.SubmitVideoByID(ctx, req.VideoID) + if err != nil { + return nil, err + } + + res = &dto.ManualSubmitRes{ + TaskID: result.TaskID, + } + return +} + +// 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"}) + + logs, err := serviceDataengine.TencentContentCallback.GetCheckLogsByImageID(ctx, req.ImageID) + if err != nil { + return nil, err + } + + res = &dto.GetCheckLogsRes{ + List: logs, + } + return +} + +// 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"}) + + logs, err := serviceDataengine.TencentContentCallback.GetCheckLogsByVideoID(ctx, req.VideoID) + if err != nil { + return nil, err + } + + res = &dto.GetCheckLogsRes{ + List: logs, + } + return +} diff --git a/controller/yidun/yidun_callback_controller.go b/controller/yidun/yidun_callback_controller.go new file mode 100644 index 0000000..1c3591e --- /dev/null +++ b/controller/yidun/yidun_callback_controller.go @@ -0,0 +1,340 @@ +package yidun + +import ( + dataengineService "cid/service/dataengine" + "context" + "fmt" + + "gitea.com/red-future/common/beans" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/net/ghttp" +) + +// YidunCallbackController 易盾回调控制器 +// 用于接收易盾检测结果的主动推送或手动轮询查询 +type YidunCallbackController struct{} + +// YidunCallback 易盾回调控制器单例 +var YidunCallback = new(YidunCallbackController) + +// CallbackResult 通用回调响应 +type CallbackResult struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data interface{} `json:"data,omitempty"` +} + +// PollResult 轮询结果 +type PollResult struct { + SuccessCount int `json:"success_count"` + FailCount int `json:"fail_count"` + PendingCount int `json:"pending_count"` +} + +// ============================================================================= +// 易盾主动推送模式回调接口 +// 易盾会在检测完成后主动 POST 数据到这些接口 +// ============================================================================= + +// ReceiveImageCallback 接收易盾图片检测结果推送 +// 易盾回调格式: POST /yidun/callback/receiveImage +// Body: callbackData={"antispam":{...}} +func (c *YidunCallbackController) ReceiveImageCallback(r *ghttp.Request) { + ctx := r.Context() + ctx = context.WithValue(ctx, "user", &beans.User{UserName: "yidun_callback"}) + + // 易盾推送的数据在请求体中 + var callbackData string + + // 尝试从表单数据获取 + callbackData = r.GetForm("callbackData", "").String() + if callbackData == "" { + // 尝试从请求体JSON获取 + var reqBody map[string]interface{} + if err := r.Parse(&reqBody); err == nil { + if v, ok := reqBody["callbackData"]; ok { + callbackData = toString(v) + } + } + } + + // 尝试直接从请求体获取原始数据 + if callbackData == "" { + callbackData = string(r.GetBody()) + } + + if callbackData == "" { + g.Log().Warningf(ctx, "图片回调数据为空") + r.Response.WriteJson(CallbackResult{Code: 400, Msg: "callbackData不能为空"}) + return + } + + g.Log().Infof(ctx, "收到易盾图片回调, data长度: %d", len(callbackData)) + + // 处理回调 - 更新 material_verify_log 和 tencent_image 表 + err := dataengineService.MaterialVerify.ProcessImageCallback(ctx, callbackData) + if err != nil { + g.Log().Errorf(ctx, "处理易盾图片回调失败: %v", err) + r.Response.WriteJson(CallbackResult{Code: 500, Msg: err.Error()}) + return + } + + r.Response.WriteJson(CallbackResult{Code: 200, Msg: "success"}) +} + +// ReceiveVideoCallback 接收易盾视频检测结果推送 +// 易盾回调格式: POST /yidun/callback/receiveVideo +// Body: callbackData={"antispam":{...}} +func (c *YidunCallbackController) ReceiveVideoCallback(r *ghttp.Request) { + ctx := r.Context() + ctx = context.WithValue(ctx, "user", &beans.User{UserName: "yidun_callback"}) + + // 易盾推送的数据在请求体中 + var callbackData string + + // 尝试从表单数据获取 + callbackData = r.GetForm("callbackData", "").String() + if callbackData == "" { + // 尝试从请求体JSON获取 + var reqBody map[string]interface{} + if err := r.Parse(&reqBody); err == nil { + if v, ok := reqBody["callbackData"]; ok { + callbackData = toString(v) + } + } + } + + // 尝试直接从请求体获取原始数据 + if callbackData == "" { + callbackData = string(r.GetBody()) + } + + if callbackData == "" { + g.Log().Warningf(ctx, "视频回调数据为空") + r.Response.WriteJson(CallbackResult{Code: 400, Msg: "callbackData不能为空"}) + return + } + + g.Log().Infof(ctx, "收到易盾视频回调, data长度: %d", len(callbackData)) + + // 处理回调 - 更新 material_verify_log 和 tencent_video 表 + err := dataengineService.MaterialVerify.ProcessVideoCallback(ctx, callbackData) + if err != nil { + g.Log().Errorf(ctx, "处理易盾视频回调失败: %v", err) + r.Response.WriteJson(CallbackResult{Code: 500, Msg: err.Error()}) + return + } + + r.Response.WriteJson(CallbackResult{Code: 200, Msg: "success"}) +} + +// ============================================================================= +// 轮询模式 - 手动查询检测结果 +// ============================================================================= + +// PollAllResults 轮询所有待查询的检测结果(图片+视频) +// 格式: POST /yidun/callback/poll +func (c *YidunCallbackController) PollAllResults(r *ghttp.Request) { + ctx := r.Context() + ctx = context.WithValue(ctx, "user", &beans.User{UserName: "admin"}) + + 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, + } + + if err != nil { + r.Response.WriteJson(CallbackResult{ + Code: 500, + Msg: fmt.Sprintf("轮询完成但有错误: %v", err), + Data: result, + }) + return + } + + r.Response.WriteJson(CallbackResult{ + Code: 200, + Msg: fmt.Sprintf("轮询完成,成功处理 %d 条,失败 %d 条", successCount, failCount), + Data: result, + }) +} + +// PollImageResults 轮询图片待查询的检测结果 +// 格式: POST /yidun/callback/pollImage +func (c *YidunCallbackController) PollImageResults(r *ghttp.Request) { + ctx := r.Context() + ctx = context.WithValue(ctx, "user", &beans.User{UserName: "admin"}) + + g.Log().Info(ctx, "开始轮询图片待查询的检测结果...") + + successCount, failCount, err := dataengineService.MaterialVerify.PollPendingImageResults(ctx) + + if err != nil { + r.Response.WriteJson(CallbackResult{ + Code: 500, + Msg: fmt.Sprintf("轮询失败: %v", err), + Data: PollResult{SuccessCount: successCount, FailCount: failCount}, + }) + return + } + + r.Response.WriteJson(CallbackResult{ + Code: 200, + Msg: fmt.Sprintf("轮询完成,成功处理 %d 条,失败 %d 条", successCount, failCount), + Data: PollResult{SuccessCount: successCount, FailCount: failCount}, + }) +} + +// PollVideoResults 轮询视频待查询的检测结果 +// 格式: POST /yidun/callback/pollVideo +func (c *YidunCallbackController) PollVideoResults(r *ghttp.Request) { + ctx := r.Context() + ctx = context.WithValue(ctx, "user", &beans.User{UserName: "admin"}) + + g.Log().Info(ctx, "开始轮询视频待查询的检测结果...") + + successCount, failCount, err := dataengineService.MaterialVerify.PollPendingVideoResults(ctx) + + if err != nil { + r.Response.WriteJson(CallbackResult{ + Code: 500, + Msg: fmt.Sprintf("轮询失败: %v", err), + Data: PollResult{SuccessCount: successCount, FailCount: failCount}, + }) + return + } + + r.Response.WriteJson(CallbackResult{ + Code: 200, + Msg: fmt.Sprintf("轮询完成,成功处理 %d 条,失败 %d 条", successCount, failCount), + Data: PollResult{SuccessCount: successCount, FailCount: failCount}, + }) +} + +// PollByTaskID 根据任务ID查询单个检测结果 +// 格式: POST /yidun/callback/pollTask +func (c *YidunCallbackController) PollByTaskID(r *ghttp.Request) { + ctx := r.Context() + ctx = context.WithValue(ctx, "user", &beans.User{UserName: "admin"}) + + taskID := r.Get("taskId", "").String() + taskType := r.Get("type", "").String() // image 或 video + + if taskID == "" { + r.Response.WriteJson(CallbackResult{Code: 400, Msg: "taskId不能为空"}) + return + } + + g.Log().Infof(ctx, "查询单个检测结果, taskId=%s, type=%s", taskID, taskType) + + var err error + if taskType == "video" || taskType == "" { + // 尝试视频 + err = dataengineService.MaterialVerify.ProcessVideoResultByTask(ctx, taskID) + if err != nil { + // 如果失败且没有指定类型,尝试图片 + if taskType == "" { + err = dataengineService.MaterialVerify.ProcessImageResultByTask(ctx, taskID) + } + } + } else if taskType == "image" { + err = dataengineService.MaterialVerify.ProcessImageResultByTask(ctx, taskID) + } + + if err != nil { + r.Response.WriteJson(CallbackResult{Code: 500, Msg: err.Error()}) + return + } + + r.Response.WriteJson(CallbackResult{Code: 200, Msg: "查询并处理成功"}) +} + +// GetPendingCount 获取待查询结果的数量 +// 格式: GET /yidun/callback/pendingCount +func (c *YidunCallbackController) GetPendingCount(r *ghttp.Request) { + ctx := r.Context() + ctx = context.WithValue(ctx, "user", &beans.User{UserName: "admin"}) + + count, err := dataengineService.MaterialVerify.GetPendingResultsCount(ctx) + if err != nil { + r.Response.WriteJson(CallbackResult{Code: 500, Msg: err.Error()}) + return + } + + r.Response.WriteJson(g.Map{ + "code": 200, + "data": g.Map{ + "pending_count": count, + "description": "待查询结果的日志数量(状态为pending且有taskID)", + }, + }) +} + +// ============================================================================= +// 兼容旧接口(手动触发回调处理) +// ============================================================================= + +// ProcessImageCallback 手动处理图片回调(兼容旧接口) +// 格式: POST /yidun/callback/processImage +func (c *YidunCallbackController) ProcessImageCallback(r *ghttp.Request) { + ctx := r.Context() + ctx = context.WithValue(ctx, "user", &beans.User{UserName: "admin"}) + + var req struct { + CallbackData string `json:"callbackData" v:"required#回调数据不能为空"` + } + if err := r.Parse(&req); err != nil { + r.Response.WriteJson(CallbackResult{Code: 400, Msg: err.Error()}) + return + } + + err := dataengineService.MaterialVerify.ProcessImageCallback(ctx, req.CallbackData) + if err != nil { + g.Log().Errorf(ctx, "处理图片回调失败: %v", err) + r.Response.WriteJson(CallbackResult{Code: 500, Msg: err.Error()}) + return + } + + r.Response.WriteJson(CallbackResult{Code: 200, 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"}) + + var req struct { + CallbackData string `json:"callbackData" v:"required#回调数据不能为空"` + } + if err := r.Parse(&req); err != nil { + r.Response.WriteJson(CallbackResult{Code: 400, Msg: err.Error()}) + return + } + + err := dataengineService.MaterialVerify.ProcessVideoCallback(ctx, req.CallbackData) + if err != nil { + g.Log().Errorf(ctx, "处理视频回调失败: %v", err) + r.Response.WriteJson(CallbackResult{Code: 500, Msg: err.Error()}) + return + } + + r.Response.WriteJson(CallbackResult{Code: 200, Msg: "success"}) +} + +// toString 转换interface{}为string +func toString(v interface{}) string { + if s, ok := v.(string); ok { + return s + } + return "" +} diff --git a/dao/dataengine/db.go b/dao/dataengine/db.go new file mode 100644 index 0000000..88f06ad --- /dev/null +++ b/dao/dataengine/db.go @@ -0,0 +1,12 @@ +package dataengine + +import ( + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// Model 获取 dataEngine 数据库的 Model(GoFrame ORM) +// 配置文件中 dataEngine 对应的实际数据库名是 dataengine +func Model(tableName string) *gdb.Model { + return g.DB("dataEngine").Model(tableName) +} diff --git a/dao/dataengine/material_verify_log_dao.go b/dao/dataengine/material_verify_log_dao.go new file mode 100644 index 0000000..e531360 --- /dev/null +++ b/dao/dataengine/material_verify_log_dao.go @@ -0,0 +1,287 @@ +package dataengine + +import ( + consts "cid/consts/dataengine" + daoEntity "cid/model/entity/dataengine" + "context" + + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +// MaterialVerifyLogDAO 素材校验日志数据访问层 +type MaterialVerifyLogDAO struct{} + +// MaterialVerifyLog DAO单例 +var MaterialVerifyLog = new(MaterialVerifyLogDAO) + +// TableName 表名 +const MaterialVerifyLogTable = "material_verify_log" + +// Create 创建校验日志 +func (d *MaterialVerifyLogDAO) Create(ctx context.Context, log *daoEntity.MaterialVerifyLog) (int64, error) { + // 构建插入数据,排除主键Id(让数据库自增) + data := g.Map{ + "tenant_id": log.TenantID, + "material_type": log.MaterialType, + "material_id": log.MaterialID, + "source_table": log.SourceTable, + "source_id": log.SourceID, + "account_id": log.AccountID, + "verify_status": log.VerifyStatus, + "created_at": gtime.Now(), + } + result, err := g.DB("default").Model(MaterialVerifyLogTable).Data(data).Insert() + if err != nil { + g.Log().Errorf(ctx, "创建校验日志失败: %v", err) + return 0, err + } + id, _ := result.LastInsertId() + return id, nil +} + +// GetByID 根据ID获取日志 +func (d *MaterialVerifyLogDAO) GetByID(ctx context.Context, id int64) (*daoEntity.MaterialVerifyLog, error) { + var result daoEntity.MaterialVerifyLog + r, err := g.DB("default").Model(MaterialVerifyLogTable). + Where(daoEntity.MaterialVerifyLogCols.Id, id). + One() + if err != nil { + return nil, err + } + if r.IsEmpty() { + return nil, nil + } + if err = r.Struct(&result); err != nil { + return nil, err + } + return &result, nil +} + +// GetByTaskID 根据任务ID获取日志 +func (d *MaterialVerifyLogDAO) GetByTaskID(ctx context.Context, taskID string) (*daoEntity.MaterialVerifyLog, error) { + var result daoEntity.MaterialVerifyLog + r, err := g.DB("default").Model(MaterialVerifyLogTable). + Where(daoEntity.MaterialVerifyLogCols.TaskID, taskID). + One() + if err != nil { + return nil, err + } + if r.IsEmpty() { + return nil, nil + } + if err = r.Struct(&result); err != nil { + return nil, err + } + return &result, nil +} + +// GetByMaterialID 根据素材ID获取日志列表 +func (d *MaterialVerifyLogDAO) GetByMaterialID(ctx context.Context, materialID string) ([]daoEntity.MaterialVerifyLog, error) { + var result []daoEntity.MaterialVerifyLog + r, err := g.DB("default").Model(MaterialVerifyLogTable). + Where(daoEntity.MaterialVerifyLogCols.MaterialID, materialID). + OrderDesc(daoEntity.MaterialVerifyLogCols.CreatedAt). + All() + if err != nil { + return nil, err + } + if err = r.Structs(&result); err != nil { + return nil, err + } + return result, nil +} + +// GetBySource 根据来源获取日志 +func (d *MaterialVerifyLogDAO) GetBySource(ctx context.Context, sourceTable string, sourceID int64) ([]daoEntity.MaterialVerifyLog, error) { + var result []daoEntity.MaterialVerifyLog + r, err := g.DB("default").Model(MaterialVerifyLogTable). + Where(daoEntity.MaterialVerifyLogCols.SourceTable, sourceTable). + Where(daoEntity.MaterialVerifyLogCols.SourceID, sourceID). + OrderDesc(daoEntity.MaterialVerifyLogCols.CreatedAt). + All() + if err != nil { + return nil, err + } + if err = r.Structs(&result); err != nil { + return nil, err + } + return result, nil +} + +// UpdateVerifyResult 更新校验结果 +func (d *MaterialVerifyLogDAO) UpdateVerifyResult(ctx context.Context, id int64, verifyStatus string, suggestion, label, resultType int, responseResult string, checkTime int64) error { + _, err := g.DB("default").Model(MaterialVerifyLogTable). + Where(daoEntity.MaterialVerifyLogCols.Id, id). + Data(map[string]interface{}{ + daoEntity.MaterialVerifyLogCols.VerifyStatus: verifyStatus, + daoEntity.MaterialVerifyLogCols.Suggestion: suggestion, + daoEntity.MaterialVerifyLogCols.Label: label, + daoEntity.MaterialVerifyLogCols.ResultType: resultType, + daoEntity.MaterialVerifyLogCols.ResponseResult: responseResult, + daoEntity.MaterialVerifyLogCols.CheckTime: checkTime, + }).Update() + if err != nil { + g.Log().Errorf(ctx, "更新校验日志结果失败: %v", err) + return err + } + return nil +} + +// UpdateError 更新错误信息 +func (d *MaterialVerifyLogDAO) UpdateError(ctx context.Context, id int64, verifyStatus string, errorMsg string) error { + _, err := g.DB("default").Model(MaterialVerifyLogTable). + Where(daoEntity.MaterialVerifyLogCols.Id, id). + Data(map[string]interface{}{ + daoEntity.MaterialVerifyLogCols.VerifyStatus: verifyStatus, + daoEntity.MaterialVerifyLogCols.ErrorMsg: errorMsg, + }).Update() + if err != nil { + g.Log().Errorf(ctx, "更新校验日志错误失败: %v", err) + return err + } + return nil +} + +// UpdateTaskID 更新任务ID +func (d *MaterialVerifyLogDAO) UpdateTaskID(ctx context.Context, id int64, taskID string) error { + _, err := g.DB("default").Model(MaterialVerifyLogTable). + Where(daoEntity.MaterialVerifyLogCols.Id, id). + Data(map[string]interface{}{ + daoEntity.MaterialVerifyLogCols.TaskID: taskID, + }).Update() + if err != nil { + return err + } + return nil +} + +// UpdateDuration 更新处理耗时 +func (d *MaterialVerifyLogDAO) UpdateDuration(ctx context.Context, id int64, durationMs int64) error { + _, err := g.DB("default").Model(MaterialVerifyLogTable). + Where(daoEntity.MaterialVerifyLogCols.Id, id). + Data(map[string]interface{}{ + daoEntity.MaterialVerifyLogCols.DurationMs: durationMs, + }).Update() + if err != nil { + return err + } + return nil +} + +// UpdateRequestParams 更新请求参数 +func (d *MaterialVerifyLogDAO) UpdateRequestParams(ctx context.Context, id int64, requestParams string) error { + _, err := g.DB("default").Model(MaterialVerifyLogTable). + Where(daoEntity.MaterialVerifyLogCols.Id, id). + Data(map[string]interface{}{ + daoEntity.MaterialVerifyLogCols.RequestParams: requestParams, + }).Update() + if err != nil { + return err + } + return nil +} + +// GetByCondition 根据条件分页查询 +func (d *MaterialVerifyLogDAO) GetByCondition(ctx context.Context, condition map[string]interface{}, page, pageSize int) ([]daoEntity.MaterialVerifyLog, int, error) { + var result []daoEntity.MaterialVerifyLog + model := g.DB("default").Model(MaterialVerifyLogTable) + + for k, v := range condition { + model = model.Where(k, v) + } + + total, err := model.Count() + if err != nil { + return nil, 0, err + } + + r, err := model. + OrderDesc(daoEntity.MaterialVerifyLogCols.CreatedAt). + Page(page, pageSize). + All() + if err != nil { + return nil, 0, err + } + if err = r.Structs(&result); err != nil { + return nil, 0, err + } + return result, int(total), nil +} + +// CountByStatus 按状态统计 +func (d *MaterialVerifyLogDAO) CountByStatus(ctx context.Context, verifyStatus string) (int, error) { + count, err := g.DB("default").Model(MaterialVerifyLogTable). + Where(daoEntity.MaterialVerifyLogCols.VerifyStatus, verifyStatus). + Count() + if err != nil { + return 0, err + } + return int(count), nil +} + +// GetStats 获取统计信息 +func (d *MaterialVerifyLogDAO) GetStats(ctx context.Context) (map[string]int, error) { + stats := make(map[string]int) + + // 使用实体中定义的正确状态值:PENDING=待校验, VERIFIED=校验通过, REJECTED=校验不通过 + statuses := []struct { + statusKey string + statusVal string + }{ + {"pending", daoEntity.VerifyStatusPending}, + {"verified", daoEntity.VerifyStatusVerified}, + {"rejected", daoEntity.VerifyStatusRejected}, + } + + var totalCount int + for _, item := range statuses { + count, err := d.CountByStatus(ctx, item.statusVal) + if err != nil { + continue + } + stats[item.statusKey] = count + totalCount += count + } + + // 添加总计 + stats["total"] = totalCount + + return stats, nil +} + +// GetPendingResults 获取待查询结果的日志(状态为submitting且有taskID) +func (d *MaterialVerifyLogDAO) GetPendingResults(ctx context.Context, limit int) ([]daoEntity.MaterialVerifyLog, error) { + var result []daoEntity.MaterialVerifyLog + + // 查询状态为 pending 且有 task_id 的记录 + r, err := g.DB("default").Model(MaterialVerifyLogTable). + Where(daoEntity.MaterialVerifyLogCols.VerifyStatus, consts.CheckStatusPending). + WhereNotNull(daoEntity.MaterialVerifyLogCols.TaskID). + Where(daoEntity.MaterialVerifyLogCols.TaskID + " != ''"). + OrderAsc(daoEntity.MaterialVerifyLogCols.CreatedAt). + Limit(limit). + All() + if err != nil { + g.Log().Errorf(ctx, "查询待处理结果日志失败: %v", err) + return nil, err + } + if err = r.Structs(&result); err != nil { + g.Log().Errorf(ctx, "转换待处理结果日志失败: %v", err) + return nil, err + } + return result, nil +} + +// CountPendingResults 统计待查询结果的数量 +func (d *MaterialVerifyLogDAO) CountPendingResults(ctx context.Context) (int, error) { + count, err := g.DB("default").Model(MaterialVerifyLogTable). + Where(daoEntity.MaterialVerifyLogCols.VerifyStatus, consts.CheckStatusPending). + WhereNotNull(daoEntity.MaterialVerifyLogCols.TaskID). + Where(daoEntity.MaterialVerifyLogCols.TaskID + " != ''"). + Count() + if err != nil { + return 0, err + } + return int(count), nil +} diff --git a/dao/dataengine/tencent_content_check_log_dao.go b/dao/dataengine/tencent_content_check_log_dao.go new file mode 100644 index 0000000..58c68ad --- /dev/null +++ b/dao/dataengine/tencent_content_check_log_dao.go @@ -0,0 +1,180 @@ +package dataengine + +import ( + consts "cid/consts/dataengine" + entity "cid/model/entity/dataengine" + yidunService "cid/service/yidun" + "context" + "encoding/json" + + "github.com/gogf/gf/v2/frame/g" +) + +// TencentContentCheckLogDAO 送检日志数据访问层 +type TencentContentCheckLogDAO struct{} + +// TencentContentCheckLog 日志DAO单例 +var TencentContentCheckLog = new(TencentContentCheckLogDAO) + +// Create 创建送检日志 +func (d *TencentContentCheckLogDAO) Create(ctx context.Context, log *entity.TencentContentCheckLog) (int64, error) { + r, err := g.DB("default").Model(consts.TencentContentCheckLogTable).Data(log).Insert() + if err != nil { + g.Log().Errorf(ctx, "创建送检日志失败: %v", err) + return 0, err + } + id, _ := r.LastInsertId() + return id, nil +} + +// UpdateStatus 更新送检状态 +func (d *TencentContentCheckLogDAO) UpdateStatus(ctx context.Context, id int64, status string, responseData string, failReason string) error { + _, err := g.DB("default").Model(consts.TencentContentCheckLogTable). + Where("id", id). + Data(map[string]interface{}{ + "status": status, + "response_data": responseData, + "fail_reason": failReason, + }).Update() + return err +} + +// UpdateCheckResult 更新检测结果 +func (d *TencentContentCheckLogDAO) UpdateCheckResult(ctx context.Context, id int64, suggestion, label, resultType int, checkTime int64) error { + _, err := g.DB("default").Model(consts.TencentContentCheckLogTable). + Where("id", id). + Data(map[string]interface{}{ + "status": consts.CheckStatusCompleted, + "suggestion": suggestion, + "label": label, + "result_type": resultType, + "check_time": checkTime, + }).Update() + return err +} + +// GetByID 根据ID获取日志 +func (d *TencentContentCheckLogDAO) GetByID(ctx context.Context, id int64) (*entity.TencentContentCheckLog, error) { + var result entity.TencentContentCheckLog + r, err := g.DB("default").Model(consts.TencentContentCheckLogTable). + Where("id", id). + One() + if err != nil { + return nil, err + } + if r.IsEmpty() { + return nil, nil + } + if err = r.Struct(&result); err != nil { + return nil, err + } + return &result, nil +} + +// GetBySourceID 根据来源ID获取日志 +func (d *TencentContentCheckLogDAO) GetBySourceID(ctx context.Context, sourceTable string, sourceID int64) ([]entity.TencentContentCheckLog, error) { + var result []entity.TencentContentCheckLog + r, err := g.DB("default").Model(consts.TencentContentCheckLogTable). + Where("source_table", sourceTable). + Where("source_id", sourceID). + OrderDesc("created_at"). + All() + if err != nil { + return nil, err + } + if err = r.Structs(&result); err != nil { + return nil, err + } + return result, nil +} + +// GetByTaskID 根据任务ID获取日志 +func (d *TencentContentCheckLogDAO) GetByTaskID(ctx context.Context, taskID string) (*entity.TencentContentCheckLog, error) { + var result entity.TencentContentCheckLog + r, err := g.DB("default").Model(consts.TencentContentCheckLogTable). + Where("task_id", taskID). + One() + if err != nil { + return nil, err + } + if r.IsEmpty() { + return nil, nil + } + if err = r.Struct(&result); err != nil { + return nil, err + } + return &result, nil +} + +// ListByStatus 根据状态获取日志列表 +func (d *TencentContentCheckLogDAO) ListByStatus(ctx context.Context, status string, page, pageSize int) ([]entity.TencentContentCheckLog, int, error) { + var result []entity.TencentContentCheckLog + model := g.DB("default").Model(consts.TencentContentCheckLogTable) + + if status != "" { + model = model.Where("status", status) + } + + total, err := model.Count() + if err != nil { + return nil, 0, err + } + + r, err := model. + OrderDesc("created_at"). + Page(page, pageSize). + All() + if err != nil { + return nil, 0, err + } + if err = r.Structs(&result); err != nil { + return nil, 0, err + } + return result, int(total), nil +} + +// UpdateDuration 更新耗时 +func (d *TencentContentCheckLogDAO) UpdateDuration(ctx context.Context, id int64, duration int64) error { + _, err := g.DB("default").Model(consts.TencentContentCheckLogTable). + Where("id", id). + Data("duration", duration). + Update() + return err +} + +// UpdateTaskID 更新任务ID +func (d *TencentContentCheckLogDAO) UpdateTaskID(ctx context.Context, id int64, taskID string) error { + _, err := g.DB("default").Model(consts.TencentContentCheckLogTable). + Where("id", id). + Data("task_id", taskID). + Update() + return err +} + +// GetSubmitResult 获取图片提交结果 +func (d *TencentContentCheckLogDAO) GetImageSubmitResult(ctx context.Context, id int64) (*yidunService.ImageSubmitResult, error) { + log, err := d.GetByID(ctx, id) + if err != nil || log == nil { + return nil, err + } + + var result yidunService.ImageSubmitResult + if err := json.Unmarshal([]byte(log.ResponseData), &result); err != nil { + return nil, err + } + return &result, nil +} + +// GetVideoSubmitResult 获取视频提交结果 +func (d *TencentContentCheckLogDAO) GetVideoSubmitResult(ctx context.Context, id int64) (*yidunService.VideoSubmitResult, error) { + log, err := d.GetByID(ctx, id) + if err != nil || log == nil { + return nil, err + } + + var result yidunService.VideoSubmitResult + if err := json.Unmarshal([]byte(log.ResponseData), &result); err != nil { + return nil, err + } + return &result, nil +} diff --git a/dao/dataengine/tencent_image_dao.go b/dao/dataengine/tencent_image_dao.go new file mode 100644 index 0000000..aa1db32 --- /dev/null +++ b/dao/dataengine/tencent_image_dao.go @@ -0,0 +1,136 @@ +package dataengine + +import ( + consts "cid/consts/dataengine" + entity "cid/model/entity/dataengine" + "context" + + "github.com/gogf/gf/v2/frame/g" +) + +// TencentImageDAO 图片素材数据访问层 +type TencentImageDAO struct{} + +// TencentImage 图片DAO单例 +var TencentImage = new(TencentImageDAO) + +// GetPendingList 获取待送检数据列表 +func (d *TencentImageDAO) GetPendingList(ctx context.Context, limit int) ([]entity.TencentImage, error) { + var result []entity.TencentImage + r, err := Model(consts.TencentImageTable). + Where(entity.TencentImageCols.VerifyStatus, consts.CheckStatusPending). + WhereNull("deleted_at"). + OrderAsc("created_time"). + Limit(limit). + All() + if err != nil { + g.Log().Errorf(ctx, "查询待送检图片数据失败: %v", err) + return nil, err + } + if err = r.Structs(&result); err != nil { + g.Log().Errorf(ctx, "转换待送检图片数据失败: %v", err) + return nil, err + } + return result, nil +} + +// GetByImageID 根据图片ID获取数据 +func (d *TencentImageDAO) GetByImageID(ctx context.Context, imageID string) (*entity.TencentImage, error) { + var result entity.TencentImage + r, err := Model(consts.TencentImageTable). + Where(entity.TencentImageCols.ImageID, imageID). + One() + if err != nil { + return nil, err + } + if r.IsEmpty() { + return nil, nil + } + if err = r.Struct(&result); err != nil { + return nil, err + } + return &result, nil +} + +// GetByID 根据ID获取数据 +func (d *TencentImageDAO) GetByID(ctx context.Context, id int64) (*entity.TencentImage, error) { + var result entity.TencentImage + r, err := Model(consts.TencentImageTable). + Where(entity.TencentImageCols.Id, id). + One() + if err != nil { + return nil, err + } + if r.IsEmpty() { + return nil, nil + } + if err = r.Struct(&result); err != nil { + return nil, err + } + return &result, nil +} + +// CountPending 统计待送检数量 +func (d *TencentImageDAO) CountPending(ctx context.Context) (int, error) { + count, err := Model(consts.TencentImageTable). + Where(entity.TencentImageCols.VerifyStatus, consts.CheckStatusPending). + WhereNull("deleted_at"). + Count() + if err != nil { + g.Log().Errorf(ctx, "统计待送检图片数量失败: %v", err) + return 0, err + } + return int(count), nil +} + +// CountByStatus 根据状态统计数量 +func (d *TencentImageDAO) CountByStatus(ctx context.Context, status string) (int, error) { + count, err := Model(consts.TencentImageTable). + Where(entity.TencentImageCols.VerifyStatus, status). + Count() + if err != nil { + return 0, err + } + return int(count), nil +} + +// GetByCondition 根据条件获取数据列表 +func (d *TencentImageDAO) GetByCondition(ctx context.Context, condition map[string]interface{}, page, pageSize int) ([]entity.TencentImage, int, error) { + var result []entity.TencentImage + model := Model(consts.TencentImageTable) + + for k, v := range condition { + model = model.Where(k, v) + } + + total, err := model.Count() + if err != nil { + return nil, 0, err + } + + r, err := model. + OrderDesc(entity.TencentImageCols.CreatedTime). + Page(page, pageSize). + All() + if err != nil { + return nil, 0, err + } + if err = r.Structs(&result); err != nil { + return nil, 0, err + } + return result, int(total), nil +} + +// UpdateStatus 更新图片校验状态 +func (d *TencentImageDAO) UpdateStatus(ctx context.Context, id int64, verifyStatus string) (int64, error) { + result, err := Model(consts.TencentImageTable). + Where(entity.TencentImageCols.Id, id). + Data(entity.TencentImageCols.VerifyStatus, verifyStatus). + Update() + if err != nil { + g.Log().Errorf(ctx, "更新图片校验状态失败: %v", err) + return 0, err + } + affected, _ := result.RowsAffected() + return affected, nil +} diff --git a/dao/dataengine/tencent_video_dao.go b/dao/dataengine/tencent_video_dao.go new file mode 100644 index 0000000..fb7615f --- /dev/null +++ b/dao/dataengine/tencent_video_dao.go @@ -0,0 +1,136 @@ +package dataengine + +import ( + consts "cid/consts/dataengine" + entity "cid/model/entity/dataengine" + "context" + + "github.com/gogf/gf/v2/frame/g" +) + +// TencentVideoDAO 视频素材数据访问层 +type TencentVideoDAO struct{} + +// TencentVideo 视频DAO单例 +var TencentVideo = new(TencentVideoDAO) + +// GetPendingList 获取待送检数据列表 +func (d *TencentVideoDAO) GetPendingList(ctx context.Context, limit int) ([]entity.TencentVideo, error) { + var result []entity.TencentVideo + r, err := Model(consts.TencentVideoTable). + Where(entity.TencentVideoCols.VerifyStatus, consts.CheckStatusPending). + WhereNull("deleted_at"). + OrderAsc("created_time"). + Limit(limit). + All() + if err != nil { + g.Log().Errorf(ctx, "查询待送检视频数据失败: %v", err) + return nil, err + } + if err = r.Structs(&result); err != nil { + g.Log().Errorf(ctx, "转换待送检视频数据失败: %v", err) + return nil, err + } + return result, nil +} + +// GetByVideoID 根据视频ID获取数据 +func (d *TencentVideoDAO) GetByVideoID(ctx context.Context, videoID string) (*entity.TencentVideo, error) { + var result entity.TencentVideo + r, err := Model(consts.TencentVideoTable). + Where(entity.TencentVideoCols.VideoID, videoID). + One() + if err != nil { + return nil, err + } + if r.IsEmpty() { + return nil, nil + } + if err = r.Struct(&result); err != nil { + return nil, err + } + return &result, nil +} + +// GetByID 根据ID获取数据 +func (d *TencentVideoDAO) GetByID(ctx context.Context, id int64) (*entity.TencentVideo, error) { + var result entity.TencentVideo + r, err := Model(consts.TencentVideoTable). + Where(entity.TencentVideoCols.Id, id). + One() + if err != nil { + return nil, err + } + if r.IsEmpty() { + return nil, nil + } + if err = r.Struct(&result); err != nil { + return nil, err + } + return &result, nil +} + +// CountPending 统计待送检数量 +func (d *TencentVideoDAO) CountPending(ctx context.Context) (int, error) { + count, err := Model(consts.TencentVideoTable). + Where(entity.TencentVideoCols.VerifyStatus, consts.CheckStatusPending). + WhereNull("deleted_at"). + Count() + if err != nil { + g.Log().Errorf(ctx, "统计待送检视频数量失败: %v", err) + return 0, err + } + return int(count), nil +} + +// CountByStatus 根据状态统计数量 +func (d *TencentVideoDAO) CountByStatus(ctx context.Context, status string) (int, error) { + count, err := Model(consts.TencentVideoTable). + Where(entity.TencentVideoCols.VerifyStatus, status). + Count() + if err != nil { + return 0, err + } + return int(count), nil +} + +// GetByCondition 根据条件获取数据列表 +func (d *TencentVideoDAO) GetByCondition(ctx context.Context, condition map[string]interface{}, page, pageSize int) ([]entity.TencentVideo, int, error) { + var result []entity.TencentVideo + model := Model(consts.TencentVideoTable) + + for k, v := range condition { + model = model.Where(k, v) + } + + total, err := model.Count() + if err != nil { + return nil, 0, err + } + + r, err := model. + OrderDesc(entity.TencentVideoCols.CreatedTime). + Page(page, pageSize). + All() + if err != nil { + return nil, 0, err + } + if err = r.Structs(&result); err != nil { + return nil, 0, err + } + return result, int(total), nil +} + +// UpdateStatus 更新视频校验状态 +func (d *TencentVideoDAO) UpdateStatus(ctx context.Context, id int64, verifyStatus string) (int64, error) { + result, err := Model(consts.TencentVideoTable). + Where(entity.TencentVideoCols.Id, id). + Data(entity.TencentVideoCols.VerifyStatus, verifyStatus). + Update() + if err != nil { + g.Log().Errorf(ctx, "更新视频校验状态失败: %v", err) + return 0, err + } + affected, _ := result.RowsAffected() + return affected, nil +} diff --git a/go.mod b/go.mod index 810a974..ca1a063 100644 --- a/go.mod +++ b/go.mod @@ -4,11 +4,10 @@ go 1.26.0 require ( gitea.com/red-future/common v0.0.18 - github.com/gogf/gf/contrib/drivers/mysql/v2 v2.9.5 + github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.10.0 github.com/gogf/gf/contrib/nosql/redis/v2 v2.9.5 - github.com/gogf/gf/v2 v2.9.5 + github.com/gogf/gf/v2 v2.10.0 github.com/yidun/yidun-golang-sdk v1.0.38 - golang.org/x/net v0.47.0 ) replace gitea.com/red-future/common => ../common @@ -24,13 +23,12 @@ require ( github.com/dgraph-io/ristretto v0.1.1 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dustin/go-humanize v1.0.1 // indirect - github.com/emirpasic/gods v1.18.1 // indirect + github.com/emirpasic/gods/v2 v2.0.0-alpha // indirect github.com/fatih/color v1.18.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-ego/gse v1.0.2 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-sql-driver/mysql v1.7.1 // indirect github.com/gogf/gf/contrib/registry/consul/v2 v2.9.5 // indirect github.com/gogf/gf/contrib/trace/otlphttp/v2 v2.9.5 // indirect github.com/gogo/protobuf v1.3.2 // indirect @@ -54,6 +52,7 @@ require ( github.com/hashicorp/golang-lru v1.0.2 // indirect github.com/hashicorp/serf v0.10.1 // indirect github.com/klauspost/compress v1.18.0 // indirect + github.com/lib/pq v1.10.9 // indirect github.com/magiconair/properties v1.8.10 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -83,6 +82,7 @@ require ( go.opentelemetry.io/otel/trace v1.38.0 // indirect go.opentelemetry.io/proto/otlp v1.7.1 // indirect golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect + golang.org/x/net v0.47.0 // indirect golang.org/x/sys v0.38.0 // indirect golang.org/x/text v0.31.0 // indirect google.golang.org/appengine v1.6.7 // indirect diff --git a/go.sum b/go.sum index 7567df0..8a13be0 100644 --- a/go.sum +++ b/go.sum @@ -50,8 +50,8 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cu github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= -github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/emirpasic/gods/v2 v2.0.0-alpha h1:dwFlh8pBg1VMOXWGipNMRt8v96dKAIvBehtCt6OtunU= +github.com/emirpasic/gods/v2 v2.0.0-alpha/go.mod h1:W0y4M2dtBB9U5z3YlghmpuUhiaZT2h6yoeE+C1sCp6A= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -74,19 +74,17 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= -github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/gogf/gf/contrib/drivers/mysql/v2 v2.9.5 h1:0+ZBYhi4sqwxXwL+hIBpp06a7G4m5nmjskQ3NNb8qYc= -github.com/gogf/gf/contrib/drivers/mysql/v2 v2.9.5/go.mod h1:vyB7J/uJcLCrHD5lfFBzxhEEMkePIRzfhd33EcsuLa0= +github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.10.0 h1:39+jbTenm7KBj4hO2C8ANAxVHpX/7OuRDs1VcGC9ylA= +github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.10.0/go.mod h1:B0s0fVzn0W220E8UTpSGzrrGKsop5KcB90twBeLCiz0= github.com/gogf/gf/contrib/nosql/redis/v2 v2.9.5 h1:Ku7p3CvGchxC7zPSgArf/tZs2w9Yb8tS/gH5ADN+p9g= github.com/gogf/gf/contrib/nosql/redis/v2 v2.9.5/go.mod h1:cjy18NsSLZQf5zaLAzuo7B2gr8GGjCTWDTEPY7T+6FI= github.com/gogf/gf/contrib/registry/consul/v2 v2.9.5 h1:eUqwJ/qNH8lJ6yssiqskazgp1ACQuNU6zXlLOZVuXTQ= github.com/gogf/gf/contrib/registry/consul/v2 v2.9.5/go.mod h1:sjQyMry9+0POYZCA6lHXBxO77WoNKkruJpRB4xKqk5k= github.com/gogf/gf/contrib/trace/otlphttp/v2 v2.9.5 h1:tHUEZYB5GTqEYYVDYnlGobf1xISARKDE4KHVlgjwTec= github.com/gogf/gf/contrib/trace/otlphttp/v2 v2.9.5/go.mod h1:cfzTn2HS9RDX8f5pUVkbGxUWcSosouqfNQ1G6cY0V88= -github.com/gogf/gf/v2 v2.9.5 h1:1scfOdHbMP854oQaiLejl+eL+c4xfuvtWmmZiDJxbKs= -github.com/gogf/gf/v2 v2.9.5/go.mod h1:VUb5eyJKpvW77O/dXsbbLNO/Kjrg0UycIiq0lRiBjjo= +github.com/gogf/gf/v2 v2.10.0 h1:rzDROlyqGMe/eM6dCalSR8dZOuMIdLhmxKSH1DGhbFs= +github.com/gogf/gf/v2 v2.10.0/go.mod h1:Svl1N+E8G/QshU2DUbh/3J/AJauqCgUnxHurXWR4Qx0= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= @@ -198,6 +196,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= diff --git a/main.go b/main.go index 6ab55db..1651578 100644 --- a/main.go +++ b/main.go @@ -3,41 +3,145 @@ package main import ( "cid/controller/app" "cid/controller/data" + "cid/controller/dataengine" "cid/controller/mapping" + "cid/controller/yidun" controllerYidun "cid/controller/yidun" + serviceDataengine "cid/service/dataengine" serviceYidun "cid/service/yidun" + "context" "fmt" + "os" + "path/filepath" + "time" _ "gitea.com/red-future/common/consul" "gitea.com/red-future/common/http" "gitea.com/red-future/common/jaeger" - _ "github.com/gogf/gf/contrib/drivers/mysql/v2" + _ "github.com/gogf/gf/contrib/drivers/pgsql/v2" _ "github.com/gogf/gf/contrib/nosql/redis/v2" - "golang.org/x/net/context" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/net/ghttp" ) func main() { ctx := context.Background() defer jaeger.ShutDown(ctx) + // 设置时区为东八区 + loc, err := time.LoadLocation("Asia/Shanghai") + if err == nil { + time.Local = loc + } + // 关键:设置 PGTZ 环境变量,lib/pq 驱动在连接 pg 时会自动设置 session timezone + // 确保从数据库读取 TIMESTAMPTZ 时返回的是东八区时间,gtime.Time 序列化输出北京时间 + os.Setenv("PGTZ", "Asia/Shanghai") + // 初始化易盾客户端 if err := serviceYidun.InitYidunClients(ctx); err != nil { panic(fmt.Sprintf("初始化易盾客户端失败: %v", err)) } + g.Log().Info(ctx, "易盾客户端初始化成功") + + // 启动内容送检定时任务 + startContentCheckService(ctx) + + // 获取前端目录 + frontendDir := getFrontendDir() + + // 注册前端静态文件路由(在使用 http.Httpserver 之前) + registerFrontendRoutes(frontendDir) + + // 注册 API 路由并启动服务器 http.RouteRegister([]interface{}{ - // 平台管理 data.Platform, - // 接口管理 data.ApiInterface, - // 数据获取 data.DataFetch, - // 数据映射 mapping.DataMapping, - // 应用管理 app.Application, - // 易盾内容安全 controllerYidun.YidunController, + controllerYidun.YidunCallback, + yidun.ContentCheck, + dataengine.MaterialVerify, }) + + // 打印前端访问地址 + port := g.Cfg().MustGet(ctx, "server.address", ":3001").String() + g.Log().Info(ctx, "============================================") + g.Log().Infof(ctx, "🌐 前端访问地址: http://localhost%s", port) + g.Log().Info(ctx, "============================================") + select {} } + +// getFrontendDir 获取前端目录路径 +func getFrontendDir() string { + execPath, _ := os.Executable() + execDir := filepath.Dir(execPath) + frontendDir := filepath.Join(execDir, "resource", "frontend") + + if _, err := os.Stat(frontendDir); os.IsNotExist(err) { + cwd, _ := os.Getwd() + frontendDir = filepath.Join(cwd, "resource", "frontend") + } + + return frontendDir +} + +// registerFrontendRoutes 注册前端静态文件路由 +func registerFrontendRoutes(frontendDir string) { + if _, err := os.Stat(frontendDir); os.IsNotExist(err) { + g.Log().Warningf(context.Background(), "前端目录不存在: %s", frontendDir) + return + } + + s := http.Httpserver + + // 静态资源路由 + s.BindHandler("/frontend/{file}", func(r *ghttp.Request) { + file := r.Get("file").String() + filePath := filepath.Join(frontendDir, file) + if _, err := os.Stat(filePath); err == nil { + r.Response.ServeFile(filePath) + } else { + r.Response.WriteStatus(404) + } + }) + + // 首页/主入口 + s.BindHandler("/", func(r *ghttp.Request) { + indexFile := filepath.Join(frontendDir, "material-verify.html") + if _, err := os.Stat(indexFile); err == nil { + r.Response.ServeFile(indexFile) + } else { + r.Response.Write("
前端页面未找到
") + } + }) +} + +// startContentCheckService 启动内容送检服务 +func startContentCheckService(ctx context.Context) { + // 检查是否启用定时送检任务 + schedulerEnabled := g.Cfg().MustGet(ctx, "content_check.scheduler_enabled", true).Bool() + if !schedulerEnabled { + g.Log().Info(ctx, "定时送检任务已禁用(scheduler_enabled=false),仅支持API手动送检") + return + } + + // 配置送检服务参数 + config := serviceDataengine.ContentCheckConfig{ + BatchSize: g.Cfg().MustGet(ctx, "content_check.batch_size", 10).Int(), + 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(), + } + serviceDataengine.TencentContentCheck.SetConfig(config) + + // 启动服务 + if err := serviceDataengine.TencentContentCheck.Start(ctx); err != nil { + g.Log().Errorf(ctx, "启动内容送检服务失败: %v", err) + } else { + g.Log().Info(ctx, "内容送检服务启动成功") + } +} diff --git a/model/dto/yidun/content_check_dto.go b/model/dto/yidun/content_check_dto.go new file mode 100644 index 0000000..b468e15 --- /dev/null +++ b/model/dto/yidun/content_check_dto.go @@ -0,0 +1,70 @@ +package yidun + +// ContentCheckConfig 送检配置 +type ContentCheckConfig struct { + BatchSize int `json:"batch_size"` + ImageEnabled bool `json:"image_enabled"` + VideoEnabled bool `json:"video_enabled"` + IntervalSeconds int `json:"interval_seconds"` +} + +// StartCheckReq 启动送检服务请求 +type StartCheckReq struct { + BatchSize int `json:"batch_size"` + IntervalSeconds int `json:"interval_seconds"` + ImageEnabled bool `json:"image_enabled"` + VideoEnabled bool `json:"video_enabled"` +} + +// EmptyReq 空请求 +type EmptyReq struct{} + +// ProcessImageCallbackReq 处理图片回调请求 +type ProcessImageCallbackReq struct { + CallbackData string `json:"callbackData"` +} + +// ProcessVideoCallbackReq 处理视频回调请求 +type ProcessVideoCallbackReq struct { + CallbackData string `json:"callbackData"` +} + +// ProcessImageResultReq 查询图片检测结果请求 +type ProcessImageResultReq struct { + TaskID string `json:"taskId"` +} + +// ProcessVideoResultReq 查询视频检测结果请求 +type ProcessVideoResultReq struct { + TaskID string `json:"taskId"` +} + +// ManualSubmitImageByIDReq 手动提交图片送检请求 +type ManualSubmitImageByIDReq struct { + ImageID string `json:"image_id" v:"required#图片ID不能为空"` +} + +// ManualSubmitVideoByIDReq 手动提交视频送检请求 +type ManualSubmitVideoByIDReq struct { + VideoID string `json:"video_id" v:"required#视频ID不能为空"` +} + +// ManualSubmitRes 手动提交响应 +type ManualSubmitRes struct { + TaskID string `json:"taskId"` +} + +// GetImageCheckLogsReq 获取图片送检日志请求 +type GetImageCheckLogsReq struct { + ImageID string `json:"image_id" v:"required#图片ID不能为空"` +} + +// GetVideoCheckLogsReq 获取视频送检日志请求 +type GetVideoCheckLogsReq struct { + VideoID string `json:"video_id" v:"required#视频ID不能为空"` +} + +// GetCheckLogsRes 获取送检日志响应 +type GetCheckLogsRes struct { + List interface{} `json:"list"` +} diff --git a/model/entity/dataengine/material_verify_log.go b/model/entity/dataengine/material_verify_log.go new file mode 100644 index 0000000..58d1d0a --- /dev/null +++ b/model/entity/dataengine/material_verify_log.go @@ -0,0 +1,85 @@ +package dataengine + +import ( + "gitea.com/red-future/common/beans" +) + +// MaterialVerifyLog 素材校验日志实体 +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:"处理耗时(毫秒)"` + + // 扩展字段(用于展示) + PreviewURL string `orm:"-" json:"previewUrl" description:"预览URL"` +} + +// 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 +} + +// 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", +} + +// 素材类型常量 +const ( + MaterialTypeImage = "IMAGE" + MaterialTypeVideo = "VIDEO" +) + +// 校验状态常量 +const ( + VerifyStatusPending = "PENDING" // 待校验 + VerifyStatusVerified = "VERIFIED" // 校验通过 + VerifyStatusRejected = "REJECTED" // 校验不通过 +) diff --git a/model/entity/dataengine/tencent_content_check_log.go b/model/entity/dataengine/tencent_content_check_log.go new file mode 100644 index 0000000..075fa40 --- /dev/null +++ b/model/entity/dataengine/tencent_content_check_log.go @@ -0,0 +1,62 @@ +package dataengine + +import ( + "gitea.com/red-future/common/beans" +) + +// TencentContentCheckLog 送检日志实体(来源:data-engine.tencent_content_check_log) +type TencentContentCheckLog struct { + beans.SQLBaseDO `orm:",inherit"` + // 来源标识 + SourceTable string `orm:"source_table" json:"sourceTable" description:"来源表标识:tencent_image/tencent_video"` + SourceID int64 `orm:"source_id" json:"sourceId" description:"原数据ID(关联业务表数据)"` + // 送检信息 + RequestURL string `orm:"request_url" json:"requestUrl" description:"送检请求路径(接口地址)"` + RequestParam string `orm:"request_param" json:"requestParam" description:"送检入参(完整请求参数,JSON格式)"` + ResponseData string `orm:"response_data" json:"responseData" description:"送检出参(完整接口返回结果,JSON格式)"` + Status string `orm:"status" json:"status" description:"送检状态:pending-待送检, submitting-送检中, success-送检成功, failed-送检失败"` + CheckTime int64 `orm:"check_time" json:"checkTime" description:"送检时间(时间戳,毫秒)"` + FailReason string `orm:"fail_reason" json:"failReason" description:"失败原因(可选,记录接口报错信息)"` + TaskID string `orm:"task_id" json:"taskId" description:"易盾返回的任务ID"` + // 检测结果 + Suggestion int `orm:"suggestion" json:"suggestion" description:"检测结果建议:0-通过,1-嫌疑,2-不通过"` + Label int `orm:"label" json:"label" description:"检测标签"` + ResultType int `orm:"result_type" json:"resultType" description:"结果类型:1-机器结果,2-人审结果"` + Duration int64 `orm:"duration" json:"duration" description:"送检耗时(毫秒)"` +} + +// TencentContentCheckLogCol 送检日志表字段定义 +type TencentContentCheckLogCol struct { + beans.SQLBaseCol + SourceTable string + SourceID string + RequestURL string + RequestParam string + ResponseData string + Status string + CheckTime string + FailReason string + TaskID string + Suggestion string + Label string + ResultType string + Duration string +} + +// TencentContentCheckLogCols 送检日志表字段常量 +var TencentContentCheckLogCols = TencentContentCheckLogCol{ + SQLBaseCol: beans.DefSQLBaseCol, + SourceTable: "source_table", + SourceID: "source_id", + RequestURL: "request_url", + RequestParam: "request_param", + ResponseData: "response_data", + Status: "status", + CheckTime: "check_time", + FailReason: "fail_reason", + TaskID: "task_id", + Suggestion: "suggestion", + Label: "label", + ResultType: "result_type", + Duration: "duration", +} diff --git a/model/entity/dataengine/tencent_image.go b/model/entity/dataengine/tencent_image.go new file mode 100644 index 0000000..6e506f9 --- /dev/null +++ b/model/entity/dataengine/tencent_image.go @@ -0,0 +1,120 @@ +package dataengine + +import ( + "gitea.com/red-future/common/beans" +) + +// TencentImage 图片素材实体(来源:data-engine.tencent_image) +type TencentImage struct { + beans.SQLBaseDO `orm:",inherit"` + // 业务字段 - 匹配现有表结构 + ImageID string `orm:"image_id" json:"imageId" description:"图片ID"` + AccountID int64 `orm:"account_id" json:"accountId" description:"账户ID"` + Width int `orm:"width" json:"width" description:"宽度"` + Height int `orm:"height" json:"height" description:"高度"` + FileSize int64 `orm:"file_size" json:"fileSize" description:"文件大小"` + Type string `orm:"type" json:"type" description:"图片类型"` + Signature string `orm:"signature" json:"signature" description:"签名"` + Description string `orm:"description" json:"description" description:"描述"` + SourceSignature string `orm:"source_signature" json:"sourceSignature" description:"源签名"` + PreviewURL string `orm:"preview_url" json:"previewUrl" description:"预览URL"` + ThumbPreviewURL string `orm:"thumb_preview_url" json:"thumbPreviewUrl" description:"缩略图URL"` + SourceType string `orm:"source_type" json:"sourceType" description:"来源类型"` + ImageUsage string `orm:"image_usage" json:"imageUsage" description:"图片用途"` + CreatedTime int64 `orm:"created_time" json:"createdTime" description:"创建时间戳"` + LastModifiedTime int64 `orm:"last_modified_time" json:"lastModifiedTime" description:"最后修改时间戳"` + ProductCatalogID int64 `orm:"product_catalog_id" json:"productCatalogId" description:"产品目录ID"` + ProductOuterID string `orm:"product_outer_id" json:"productOuterId" description:"产品外部ID"` + SourceReferenceID string `orm:"source_reference_id" json:"sourceReferenceId" description:"源引用ID"` + OwnerAccountID string `orm:"owner_account_id" json:"ownerAccountId" description:"所有者账户ID"` + VerifyStatus string `orm:"verify_status" json:"verifyStatus" description:"审核状态"` + SampleAspectRatio string `orm:"sample_aspect_ratio" json:"sampleAspectRatio" description:"示例宽高比"` + SourceMaterialID string `orm:"source_material_id" json:"sourceMaterialId" description:"源素材ID"` + NewSourceType string `orm:"new_source_type" json:"newSourceType" description:"新来源类型"` + FirstPublicationStatus string `orm:"first_publication_status" json:"firstPublicationStatus" description:"首次发布状态"` + QualityStatus string `orm:"quality_status" json:"qualityStatus" description:"质量状态"` + SimilarityStatus string `orm:"similarity_status" json:"similarityStatus" description:"相似度状态"` + UserAigcStatus string `orm:"user_aigc_status" json:"userAigcStatus" description:"用户AIGC状态"` + SystemAigcStatus string `orm:"system_aigc_status" json:"systemAigcStatus" description:"系统AIGC状态"` + AigcSource string `orm:"aigc_source" json:"aigcSource" description:"AIGC来源"` + AigcFlag string `orm:"aigc_flag" json:"aigcFlag" description:"AIGC标志"` + MuseAigcVersion int `orm:"muse_aigc_version" json:"museAigcVersion" description:"Muse AIGC版本"` + AigcType int `orm:"aigc_type" json:"aigcType" description:"AIGC类型"` + + // 内容检测相关字段(扩展字段,用于存储检测结果) + // 注意:如果表中没有这些字段,需要通过 content_check_log 表来存储检测结果 +} + +// TencentImageCol 图片素材表字段定义 +type TencentImageCol struct { + beans.SQLBaseCol + ImageID string + AccountID string + Width string + Height string + FileSize string + Type string + Signature string + Description string + SourceSignature string + PreviewURL string + ThumbPreviewURL string + SourceType string + ImageUsage string + CreatedTime string + LastModifiedTime string + ProductCatalogID string + ProductOuterID string + SourceReferenceID string + OwnerAccountID string + VerifyStatus string + SampleAspectRatio string + SourceMaterialID string + NewSourceType string + FirstPublicationStatus string + QualityStatus string + SimilarityStatus string + UserAigcStatus string + SystemAigcStatus string + AigcSource string + AigcFlag string + MuseAigcVersion string + AigcType string +} + +// TencentImageCols 图片素材表字段常量 +var TencentImageCols = TencentImageCol{ + SQLBaseCol: beans.DefSQLBaseCol, + ImageID: "image_id", + AccountID: "account_id", + Width: "width", + Height: "height", + FileSize: "file_size", + Type: "type", + Signature: "signature", + Description: "description", + SourceSignature: "source_signature", + PreviewURL: "preview_url", + ThumbPreviewURL: "thumb_preview_url", + SourceType: "source_type", + ImageUsage: "image_usage", + CreatedTime: "created_time", + LastModifiedTime: "last_modified_time", + ProductCatalogID: "product_catalog_id", + ProductOuterID: "product_outer_id", + SourceReferenceID: "source_reference_id", + OwnerAccountID: "owner_account_id", + VerifyStatus: "verify_status", + SampleAspectRatio: "sample_aspect_ratio", + SourceMaterialID: "source_material_id", + NewSourceType: "new_source_type", + FirstPublicationStatus: "first_publication_status", + QualityStatus: "quality_status", + SimilarityStatus: "similarity_status", + UserAigcStatus: "user_aigc_status", + SystemAigcStatus: "system_aigc_status", + AigcSource: "aigc_source", + AigcFlag: "aigc_flag", + MuseAigcVersion: "muse_aigc_version", + AigcType: "aigc_type", +} diff --git a/model/entity/dataengine/tencent_video.go b/model/entity/dataengine/tencent_video.go new file mode 100644 index 0000000..1c298ca --- /dev/null +++ b/model/entity/dataengine/tencent_video.go @@ -0,0 +1,159 @@ +package dataengine + +import ( + "gitea.com/red-future/common/beans" +) + +// TencentVideo 视频素材实体(来源:data-engine.tencent_video) +type TencentVideo struct { + beans.SQLBaseDO `orm:",inherit"` + // 业务字段 - 匹配现有表结构 + VideoID string `orm:"video_id" json:"videoId" description:"视频ID"` + AccountID int64 `orm:"account_id" json:"accountId" description:"账户ID"` + Width int `orm:"width" json:"width" description:"宽度"` + Height int `orm:"height" json:"height" description:"高度"` + VideoFrames int `orm:"video_frames" json:"videoFrames" description:"视频帧数"` + VideoFps int `orm:"video_fps" json:"videoFps" description:"帧率"` + VideoCodec string `orm:"video_codec" json:"videoCodec" description:"视频编码"` + VideoBitRate int64 `orm:"video_bit_rate" json:"videoBitRate" description:"视频码率"` + AudioCodec string `orm:"audio_codec" json:"audioCodec" description:"音频编码"` + AudioBitRate int64 `orm:"audio_bit_rate" json:"audioBitRate" description:"音频码率"` + FileSize int64 `orm:"file_size" json:"fileSize" description:"文件大小"` + Type string `orm:"type" json:"type" description:"媒体类型"` + Signature string `orm:"signature" json:"signature" description:"签名"` + SystemStatus string `orm:"system_status" json:"systemStatus" description:"系统状态"` + Description string `orm:"description" json:"description" description:"描述"` + PreviewURL string `orm:"preview_url" json:"previewUrl" description:"预览URL"` + KeyFrameImageURL string `orm:"key_frame_image_url" json:"keyFrameImageUrl" description:"关键帧图片URL"` + CreatedTime int64 `orm:"created_time" json:"createdTime" description:"创建时间戳"` + LastModifiedTime int64 `orm:"last_modified_time" json:"lastModifiedTime" description:"最后修改时间戳"` + VideoProfileName string `orm:"video_profile_name" json:"videoProfileName" description:"视频配置名称"` + AudioSampleRate int `orm:"audio_sample_rate" json:"audioSampleRate" description:"音频采样率"` + MaxKeyframeInterval int `orm:"max_keyframe_interval" json:"maxKeyframeInterval" description:"最大关键帧间隔"` + MinKeyframeInterval int `orm:"min_keyframe_interval" json:"minKeyframeInterval" description:"最小关键帧间隔"` + SampleAspectRatio string `orm:"sample_aspect_ratio" json:"sampleAspectRatio" description:"示例宽高比"` + AudioProfileName string `orm:"audio_profile_name" json:"audioProfileName" description:"音频配置名称"` + ScanType string `orm:"scan_type" json:"scanType" description:"扫描类型"` + ImageDurationMs int64 `orm:"image_duration_millisecond" json:"imageDurationMs" description:"图片时长(毫秒)"` + AudioDurationMs int64 `orm:"audio_duration_millisecond" json:"audioDurationMs" description:"音频时长(毫秒)"` + SourceType string `orm:"source_type" json:"sourceType" description:"来源类型"` + ProductCatalogID string `orm:"product_catalog_id" json:"productCatalogId" description:"产品目录ID"` + ProductOuterID string `orm:"product_outer_id" json:"productOuterId" description:"产品外部ID"` + SourceReferenceID string `orm:"source_reference_id" json:"sourceReferenceId" description:"源引用ID"` + OwnerAccountID string `orm:"owner_account_id" json:"ownerAccountId" description:"所有者账户ID"` + VerifyStatus string `orm:"verify_status" json:"verifyStatus" description:"审核状态"` + SourceMaterialID string `orm:"source_material_id" json:"sourceMaterialId" description:"源素材ID"` + NewSourceType string `orm:"new_source_type" json:"newSourceType" description:"新来源类型"` + AigcType int `orm:"aigc_type" json:"aigcType" description:"AIGC类型"` + FirstPublicationStatus string `orm:"first_publication_status" json:"firstPublicationStatus" description:"首次发布状态"` + QualityStatus string `orm:"quality_status" json:"qualityStatus" description:"质量状态"` + CoverID string `orm:"cover_id" json:"coverId" description:"封面ID"` + SimilarityStatus string `orm:"similarity_status" json:"similarityStatus" description:"相似度状态"` + UserAigcStatus string `orm:"user_aigc_status" json:"userAigcStatus" description:"用户AIGC状态"` + SystemAigcStatus string `orm:"system_aigc_status" json:"systemAigcStatus" description:"系统AIGC状态"` + AigcSource string `orm:"aigc_source" json:"aigcSource" description:"AIGC来源"` + AigcFlag string `orm:"aigc_flag" json:"aigcFlag" description:"AIGC标志"` + MuseAigcVersion int `orm:"muse_aigc_version" json:"museAigcVersion" description:"Muse AIGC版本"` +} + +// TencentVideoCol 视频素材表字段定义 +type TencentVideoCol struct { + beans.SQLBaseCol + VideoID string + AccountID string + Width string + Height string + VideoFrames string + VideoFps string + VideoCodec string + VideoBitRate string + AudioCodec string + AudioBitRate string + FileSize string + Type string + Signature string + SystemStatus string + Description string + PreviewURL string + KeyFrameImageURL string + CreatedTime string + LastModifiedTime string + VideoProfileName string + AudioSampleRate string + MaxKeyframeInterval string + MinKeyframeInterval string + SampleAspectRatio string + AudioProfileName string + ScanType string + ImageDurationMs string + AudioDurationMs string + SourceType string + ProductCatalogID string + ProductOuterID string + SourceReferenceID string + OwnerAccountID string + VerifyStatus string + SourceMaterialID string + NewSourceType string + AigcType string + FirstPublicationStatus string + QualityStatus string + CoverID string + SimilarityStatus string + UserAigcStatus string + SystemAigcStatus string + AigcSource string + AigcFlag string + MuseAigcVersion string +} + +// TencentVideoCols 视频素材表字段常量 +var TencentVideoCols = TencentVideoCol{ + SQLBaseCol: beans.DefSQLBaseCol, + VideoID: "video_id", + AccountID: "account_id", + Width: "width", + Height: "height", + VideoFrames: "video_frames", + VideoFps: "video_fps", + VideoCodec: "video_codec", + VideoBitRate: "video_bit_rate", + AudioCodec: "audio_codec", + AudioBitRate: "audio_bit_rate", + FileSize: "file_size", + Type: "type", + Signature: "signature", + SystemStatus: "system_status", + Description: "description", + PreviewURL: "preview_url", + KeyFrameImageURL: "key_frame_image_url", + CreatedTime: "created_time", + LastModifiedTime: "last_modified_time", + VideoProfileName: "video_profile_name", + AudioSampleRate: "audio_sample_rate", + MaxKeyframeInterval: "max_keyframe_interval", + MinKeyframeInterval: "min_keyframe_interval", + SampleAspectRatio: "sample_aspect_ratio", + AudioProfileName: "audio_profile_name", + ScanType: "scan_type", + ImageDurationMs: "image_duration_millisecond", + AudioDurationMs: "audio_duration_millisecond", + SourceType: "source_type", + ProductCatalogID: "product_catalog_id", + ProductOuterID: "product_outer_id", + SourceReferenceID: "source_reference_id", + OwnerAccountID: "owner_account_id", + VerifyStatus: "verify_status", + SourceMaterialID: "source_material_id", + NewSourceType: "new_source_type", + AigcType: "aigc_type", + FirstPublicationStatus: "first_publication_status", + QualityStatus: "quality_status", + CoverID: "cover_id", + SimilarityStatus: "similarity_status", + UserAigcStatus: "user_aigc_status", + SystemAigcStatus: "system_aigc_status", + AigcSource: "aigc_source", + AigcFlag: "aigc_flag", + MuseAigcVersion: "muse_aigc_version", +} diff --git a/resource/frontend/material-verify.html b/resource/frontend/material-verify.html new file mode 100644 index 0000000..f62e8d0 --- /dev/null +++ b/resource/frontend/material-verify.html @@ -0,0 +1,965 @@ + + + + + +腾讯图片/视频素材自动校验系统 - 基于易盾内容安全检测
+{{ currentLog.requestParams || '无' }}
+ {{ currentLog.responseResult || '无' }}
+ {{ currentLog.errorMsg }}
+ 无+