yidun送检功能
This commit is contained in:
@@ -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
|
||||
|
||||
55
config.yml
55
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
|
||||
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
|
||||
22
consts/dataengine/check_status.go
Normal file
22
consts/dataengine/check_status.go
Normal file
@@ -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 // 不通过
|
||||
)
|
||||
8
consts/dataengine/table.go
Normal file
8
consts/dataengine/table.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package dataengine
|
||||
|
||||
// PostgreSQL表名常量
|
||||
const (
|
||||
TencentImageTable = "tencent_image" // 图片送检表
|
||||
TencentVideoTable = "tencent_video" // 视频送检表
|
||||
TencentContentCheckLogTable = "tencent_content_check_log" // 送检日志表
|
||||
)
|
||||
509
controller/dataengine/material_verify_controller.go
Normal file
509
controller/dataengine/material_verify_controller.go
Normal file
@@ -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
|
||||
}
|
||||
175
controller/yidun/content_check_controller.go
Normal file
175
controller/yidun/content_check_controller.go
Normal file
@@ -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
|
||||
}
|
||||
340
controller/yidun/yidun_callback_controller.go
Normal file
340
controller/yidun/yidun_callback_controller.go
Normal file
@@ -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 ""
|
||||
}
|
||||
12
dao/dataengine/db.go
Normal file
12
dao/dataengine/db.go
Normal file
@@ -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)
|
||||
}
|
||||
287
dao/dataengine/material_verify_log_dao.go
Normal file
287
dao/dataengine/material_verify_log_dao.go
Normal file
@@ -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
|
||||
}
|
||||
180
dao/dataengine/tencent_content_check_log_dao.go
Normal file
180
dao/dataengine/tencent_content_check_log_dao.go
Normal file
@@ -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
|
||||
}
|
||||
136
dao/dataengine/tencent_image_dao.go
Normal file
136
dao/dataengine/tencent_image_dao.go
Normal file
@@ -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
|
||||
}
|
||||
136
dao/dataengine/tencent_video_dao.go
Normal file
136
dao/dataengine/tencent_video_dao.go
Normal file
@@ -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
|
||||
}
|
||||
10
go.mod
10
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
|
||||
|
||||
16
go.sum
16
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=
|
||||
|
||||
120
main.go
120
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("<html><body><h1>CID Backend Service</h1><p>前端页面未找到</p></body></html>")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 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, "内容送检服务启动成功")
|
||||
}
|
||||
}
|
||||
|
||||
70
model/dto/yidun/content_check_dto.go
Normal file
70
model/dto/yidun/content_check_dto.go
Normal file
@@ -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"`
|
||||
}
|
||||
85
model/entity/dataengine/material_verify_log.go
Normal file
85
model/entity/dataengine/material_verify_log.go
Normal file
@@ -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" // 校验不通过
|
||||
)
|
||||
62
model/entity/dataengine/tencent_content_check_log.go
Normal file
62
model/entity/dataengine/tencent_content_check_log.go
Normal file
@@ -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",
|
||||
}
|
||||
120
model/entity/dataengine/tencent_image.go
Normal file
120
model/entity/dataengine/tencent_image.go
Normal file
@@ -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",
|
||||
}
|
||||
159
model/entity/dataengine/tencent_video.go
Normal file
159
model/entity/dataengine/tencent_video.go
Normal file
@@ -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",
|
||||
}
|
||||
965
resource/frontend/material-verify.html
Normal file
965
resource/frontend/material-verify.html
Normal file
@@ -0,0 +1,965 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>素材送检状态管理</title>
|
||||
<link rel="stylesheet" href="https://unpkg.com/element-ui@2.15.14/lib/theme-chalk/index.css">
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
background: #f5f7fa;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.app-container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
.header {
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 12px rgba(0,0,0,0.1);
|
||||
}
|
||||
.header h1 {
|
||||
color: #409EFF;
|
||||
font-size: 24px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.header p {
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
}
|
||||
.stats-card {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.stat-item {
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
box-shadow: 0 2px 12px rgba(0,0,0,0.1);
|
||||
}
|
||||
.stat-item.pending { border-left: 4px solid #E6A23C; }
|
||||
.stat-item.verified { border-left: 4px solid #67C23A; }
|
||||
.stat-item.rejected { border-left: 4px solid #F56C6C; }
|
||||
.stat-value {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
}
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.stat-breakdown {
|
||||
margin-top: 12px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px dashed #ebeef5;
|
||||
}
|
||||
.stat-breakdown-item {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 13px;
|
||||
line-height: 24px;
|
||||
}
|
||||
.stat-breakdown-item .type-tag {
|
||||
display: inline-block;
|
||||
padding: 1px 8px;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
min-width: 28px;
|
||||
}
|
||||
.stat-breakdown-item .type-tag.image {
|
||||
background: #ecf5ff;
|
||||
color: #409EFF;
|
||||
}
|
||||
.stat-breakdown-item .type-tag.video {
|
||||
background: #fdf6ec;
|
||||
color: #E6A23C;
|
||||
}
|
||||
.stat-breakdown-item .num {
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
}
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 12px rgba(0,0,0,0.1);
|
||||
}
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.filter-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.filter-item label {
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
}
|
||||
.action-bar {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.table-status {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.status-pending { background: #FDF6EC; color: #E6A23C; }
|
||||
.status-submitting { background: #E8F4FD; color: #409EFF; }
|
||||
.status-verified { background: #F0F9EB; color: #67C23A; }
|
||||
.status-rejected { background: #FEF0F0; color: #F56C6C; }
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
text-align: right;
|
||||
}
|
||||
.log-detail {
|
||||
background: #f5f7fa;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.log-detail pre {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
font-family: Monaco, Consolas, monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
.media-preview {
|
||||
max-width: 200px;
|
||||
max-height: 150px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.video-preview {
|
||||
width: 200px;
|
||||
height: 120px;
|
||||
background: #000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
.tab-container {
|
||||
margin-top: 20px;
|
||||
}
|
||||
.description-text {
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<div class="app-container">
|
||||
<!-- 头部 -->
|
||||
<div class="header">
|
||||
<h1>素材送检状态管理</h1>
|
||||
<p>腾讯图片/视频素材自动校验系统 - 基于易盾内容安全检测</p>
|
||||
</div>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<div class="stats-card">
|
||||
<div class="stat-item pending">
|
||||
<div class="stat-value">{{ imageStats.pending + videoStats.pending || 0 }}</div>
|
||||
<div class="stat-label">待校验</div>
|
||||
<div class="stat-breakdown">
|
||||
<div class="stat-breakdown-item">
|
||||
<span class="type-tag image">图</span>
|
||||
<span>图片: </span>
|
||||
<span class="num">{{ imageStats.pending || 0 }}</span>
|
||||
</div>
|
||||
<div class="stat-breakdown-item">
|
||||
<span class="type-tag video">视</span>
|
||||
<span>视频: </span>
|
||||
<span class="num">{{ videoStats.pending || 0 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-item verified">
|
||||
<div class="stat-value">{{ imageStats.verified + videoStats.verified || 0 }}</div>
|
||||
<div class="stat-label">校验通过</div>
|
||||
<div class="stat-breakdown">
|
||||
<div class="stat-breakdown-item">
|
||||
<span class="type-tag image">图</span>
|
||||
<span>图片: </span>
|
||||
<span class="num">{{ imageStats.verified || 0 }}</span>
|
||||
</div>
|
||||
<div class="stat-breakdown-item">
|
||||
<span class="type-tag video">视</span>
|
||||
<span>视频: </span>
|
||||
<span class="num">{{ videoStats.verified || 0 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-item rejected">
|
||||
<div class="stat-value">{{ imageStats.rejected + videoStats.rejected || 0 }}</div>
|
||||
<div class="stat-label">校验不通过</div>
|
||||
<div class="stat-breakdown">
|
||||
<div class="stat-breakdown-item">
|
||||
<span class="type-tag image">图</span>
|
||||
<span>图片: </span>
|
||||
<span class="num">{{ imageStats.rejected || 0 }}</span>
|
||||
</div>
|
||||
<div class="stat-breakdown-item">
|
||||
<span class="type-tag video">视</span>
|
||||
<span>视频: </span>
|
||||
<span class="num">{{ videoStats.rejected || 0 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
|
||||
<el-tab-pane label="图片素材" name="image">
|
||||
<div class="card">
|
||||
<!-- 筛选 -->
|
||||
<div class="filter-bar">
|
||||
<div class="filter-item">
|
||||
<label>状态:</label>
|
||||
<el-select v-model="imageFilters.status" placeholder="全部" clearable style="width: 120px;">
|
||||
<el-option label="全部" value=""></el-option>
|
||||
<el-option label="待校验" value="PENDING"></el-option>
|
||||
<el-option label="送检中" value="SUBMITTING"></el-option>
|
||||
<el-option label="校验通过" value="VERIFIED"></el-option>
|
||||
<el-option label="校验不通过" value="REJECTED"></el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="filter-item">
|
||||
<label>账户ID:</label>
|
||||
<el-input v-model="imageFilters.accountId" placeholder="账户ID" style="width: 120px;" clearable @keyup.enter.native="searchImage"></el-input>
|
||||
</div>
|
||||
<el-button type="primary" @click="searchImage">搜索</el-button>
|
||||
<el-button @click="resetImageFilter">重置</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="action-bar">
|
||||
<el-button type="success" @click="batchVerifyImage" :loading="batchLoading">
|
||||
批量校验图片
|
||||
</el-button>
|
||||
<el-button type="primary" plain @click="pollImageResults" :loading="pollLoading">
|
||||
<i class="el-icon-refresh"></i> 刷新检测结果
|
||||
</el-button>
|
||||
<el-button type="warning" plain @click="exportImageUrls">
|
||||
<i class="el-icon-download"></i> 导出
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 表格 -->
|
||||
<el-table :data="imageList" border style="width: 100%;" v-loading="imageLoading">
|
||||
<el-table-column prop="id" label="ID" width="80"></el-table-column>
|
||||
<el-table-column prop="imageId" label="图片ID" width="180"></el-table-column>
|
||||
<el-table-column prop="accountId" label="账户ID" width="100"></el-table-column>
|
||||
<el-table-column prop="imageUsage" label="用途" width="100"></el-table-column>
|
||||
<el-table-column label="预览" width="120">
|
||||
<template slot-scope="scope">
|
||||
<img v-if="scope.row.previewUrl" :src="scope.row.previewUrl" class="media-preview" @click="previewMedia(scope.row.previewUrl, 'image')">
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="verifyStatus" label="校验状态" width="110">
|
||||
<template slot-scope="scope">
|
||||
<span :class="'table-status status-' + (scope.row.verifyStatus || 'pending').toLowerCase()">
|
||||
{{ getStatusText(scope.row.verifyStatus) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="description" label="描述" min-width="150">
|
||||
<template slot-scope="scope">
|
||||
<span class="description-text" :title="scope.row.description">{{ scope.row.description || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="text" @click="showLogs('IMAGE', scope.row.imageId)">查看日志</el-button>
|
||||
<el-button size="mini" type="text" @click="verifyImage(scope.row.imageId)">送检</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination">
|
||||
<el-pagination
|
||||
@current-change="handleImagePageChange"
|
||||
:current-page="imagePage"
|
||||
:page-size="imagePageSize"
|
||||
layout="total, prev, pager, next"
|
||||
:total="imageTotal">
|
||||
</el-pagination>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="视频素材" name="video">
|
||||
<div class="card">
|
||||
<!-- 筛选 -->
|
||||
<div class="filter-bar">
|
||||
<div class="filter-item">
|
||||
<label>状态:</label>
|
||||
<el-select v-model="videoFilters.status" placeholder="全部" clearable style="width: 120px;">
|
||||
<el-option label="全部" value=""></el-option>
|
||||
<el-option label="待校验" value="PENDING"></el-option>
|
||||
<el-option label="送检中" value="SUBMITTING"></el-option>
|
||||
<el-option label="校验通过" value="VERIFIED"></el-option>
|
||||
<el-option label="校验不通过" value="REJECTED"></el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="filter-item">
|
||||
<label>账户ID:</label>
|
||||
<el-input v-model="videoFilters.accountId" placeholder="账户ID" style="width: 120px;" clearable @keyup.enter.native="searchVideo"></el-input>
|
||||
</div>
|
||||
<el-button type="primary" @click="searchVideo">搜索</el-button>
|
||||
<el-button @click="resetVideoFilter">重置</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="action-bar">
|
||||
<el-button type="success" @click="batchVerifyVideo" :loading="batchLoading">
|
||||
批量校验视频
|
||||
</el-button>
|
||||
<el-button type="primary" plain @click="pollVideoResults" :loading="pollLoading">
|
||||
<i class="el-icon-refresh"></i> 刷新检测结果
|
||||
</el-button>
|
||||
<el-button type="warning" plain @click="exportVideoUrls">
|
||||
<i class="el-icon-download"></i> 导出
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 表格 -->
|
||||
<el-table :data="videoList" border style="width: 100%;" v-loading="videoLoading">
|
||||
<el-table-column prop="id" label="ID" width="80"></el-table-column>
|
||||
<el-table-column prop="videoId" label="视频ID" width="180"></el-table-column>
|
||||
<el-table-column prop="accountId" label="账户ID" width="100"></el-table-column>
|
||||
<el-table-column label="预览" width="120">
|
||||
<template slot-scope="scope">
|
||||
<div class="video-preview" @click="previewMedia(scope.row.previewUrl, 'video')">
|
||||
<i class="el-icon-video-play" style="font-size: 32px;"></i>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="verifyStatus" label="校验状态" width="110">
|
||||
<template slot-scope="scope">
|
||||
<span :class="'table-status status-' + (scope.row.verifyStatus || 'pending').toLowerCase()">
|
||||
{{ getStatusText(scope.row.verifyStatus) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="description" label="描述" min-width="150">
|
||||
<template slot-scope="scope">
|
||||
<span class="description-text" :title="scope.row.description">{{ scope.row.description || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="text" @click="showLogs('VIDEO', scope.row.videoId)">查看日志</el-button>
|
||||
<el-button size="mini" type="text" @click="verifyVideo(scope.row.videoId)">送检</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination">
|
||||
<el-pagination
|
||||
@current-change="handleVideoPageChange"
|
||||
:current-page="videoPage"
|
||||
:page-size="videoPageSize"
|
||||
layout="total, prev, pager, next"
|
||||
:total="videoTotal">
|
||||
</el-pagination>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="校验日志" name="log">
|
||||
<div class="card">
|
||||
<!-- 筛选 -->
|
||||
<div class="filter-bar">
|
||||
<div class="filter-item">
|
||||
<label>素材类型:</label>
|
||||
<el-select v-model="logFilters.materialType" placeholder="全部" clearable style="width: 120px;">
|
||||
<el-option label="全部" value=""></el-option>
|
||||
<el-option label="图片" value="IMAGE"></el-option>
|
||||
<el-option label="视频" value="VIDEO"></el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="filter-item">
|
||||
<label>校验状态:</label>
|
||||
<el-select v-model="logFilters.verifyStatus" placeholder="全部" clearable style="width: 120px;">
|
||||
<el-option label="全部" value=""></el-option>
|
||||
<el-option label="待校验" value="PENDING"></el-option>
|
||||
<el-option label="校验通过" value="VERIFIED"></el-option>
|
||||
<el-option label="校验不通过" value="REJECTED"></el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="filter-item">
|
||||
<label>素材ID:</label>
|
||||
<el-input v-model="logFilters.materialId" placeholder="素材ID" style="width: 150px;" clearable></el-input>
|
||||
</div>
|
||||
<el-button type="primary" @click="searchLog">搜索</el-button>
|
||||
<el-button @click="resetLogFilter">重置</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 表格 -->
|
||||
<el-table :data="logList" border style="width: 100%;" v-loading="logLoading">
|
||||
<el-table-column prop="id" label="ID" width="80"></el-table-column>
|
||||
<el-table-column prop="materialType" label="类型" width="80">
|
||||
<template slot-scope="scope">
|
||||
{{ scope.row.materialType === 'IMAGE' ? '图片' : '视频' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="materialId" label="素材ID" width="180"></el-table-column>
|
||||
<el-table-column prop="accountId" label="账户ID" width="100"></el-table-column>
|
||||
<el-table-column prop="taskId" label="任务ID" width="180"></el-table-column>
|
||||
<el-table-column prop="verifyStatus" label="校验状态" width="110">
|
||||
<template slot-scope="scope">
|
||||
<span :class="'table-status status-' + (scope.row.verifyStatus || 'pending').toLowerCase()">
|
||||
{{ getStatusText(scope.row.verifyStatus) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="suggestion" label="建议" width="80">
|
||||
<template slot-scope="scope">
|
||||
{{ getSuggestionText(scope.row.suggestion) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createdAt" label="创建时间" width="160">
|
||||
<template slot-scope="scope">
|
||||
{{ formatTime(scope.row.createdAt) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="text" @click="showLogDetail(scope.row)">详情</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination">
|
||||
<el-pagination
|
||||
@current-change="handleLogPageChange"
|
||||
:current-page="logPage"
|
||||
:page-size="logPageSize"
|
||||
layout="total, prev, pager, next"
|
||||
:total="logTotal">
|
||||
</el-pagination>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
|
||||
<!-- 日志详情对话框 -->
|
||||
<el-dialog title="校验日志详情" :visible.sync="logDialogVisible" width="800px">
|
||||
<div v-if="currentLog">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="日志ID">{{ currentLog.id }}</el-descriptions-item>
|
||||
<el-descriptions-item label="素材类型">{{ currentLog.materialType === 'IMAGE' ? '图片' : '视频' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="素材ID">{{ currentLog.materialId }}</el-descriptions-item>
|
||||
<el-descriptions-item label="账户ID">{{ currentLog.accountId }}</el-descriptions-item>
|
||||
<el-descriptions-item label="任务ID">{{ currentLog.taskId || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="校验状态">
|
||||
<span :class="'table-status status-' + (currentLog.verifyStatus || 'pending').toLowerCase()">
|
||||
{{ getStatusText(currentLog.verifyStatus) }}
|
||||
</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="处置建议">{{ getSuggestionText(currentLog.suggestion) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">{{ formatTime(currentLog.createdAt) }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<h4 style="margin-top: 20px;">请求参数:</h4>
|
||||
<div class="log-detail">
|
||||
<pre>{{ currentLog.requestParams || '无' }}</pre>
|
||||
</div>
|
||||
|
||||
<h4>响应结果:</h4>
|
||||
<div class="log-detail">
|
||||
<pre>{{ currentLog.responseResult || '无' }}</pre>
|
||||
</div>
|
||||
|
||||
<h4>错误信息:</h4>
|
||||
<div class="log-detail" v-if="currentLog.errorMsg">
|
||||
<pre style="color: #F56C6C;">{{ currentLog.errorMsg }}</pre>
|
||||
</div>
|
||||
<div class="log-detail" v-else>
|
||||
<pre>无</pre>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 预览对话框 -->
|
||||
<el-dialog title="媒体预览" :visible.sync="previewVisible" width="60%">
|
||||
<div style="text-align: center;">
|
||||
<img v-if="previewType === 'image'" :src="previewUrl" style="max-width: 100%;">
|
||||
<video v-if="previewType === 'video'" :src="previewUrl" controls style="max-width: 100%;"></video>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 日志列表对话框 -->
|
||||
<el-dialog title="校验日志" :visible.sync="logsDialogVisible" width="900px">
|
||||
<el-table :data="materialLogs" border style="width: 100%;" size="small">
|
||||
<el-table-column prop="id" label="ID" width="80"></el-table-column>
|
||||
<el-table-column prop="taskId" label="任务ID" width="180"></el-table-column>
|
||||
<el-table-column prop="verifyStatus" label="状态" width="100">
|
||||
<template slot-scope="scope">
|
||||
<span :class="'table-status status-' + (scope.row.verifyStatus || 'pending').toLowerCase()">
|
||||
{{ getStatusText(scope.row.verifyStatus) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="suggestion" label="建议" width="80">
|
||||
<template slot-scope="scope">
|
||||
{{ getSuggestionText(scope.row.suggestion) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="errorMsg" label="错误信息" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column prop="createdAt" label="时间" width="160">
|
||||
<template slot-scope="scope">
|
||||
{{ formatTime(scope.row.createdAt) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-dialog>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.14/dist/vue.js"></script>
|
||||
<script src="https://unpkg.com/element-ui@2.15.14/lib/index.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/axios@0.27.2/dist/axios.min.js"></script>
|
||||
<script>
|
||||
// API 基础地址
|
||||
const API_BASE = 'http://localhost:3001';
|
||||
|
||||
new Vue({
|
||||
el: '#app',
|
||||
data: {
|
||||
activeTab: 'image',
|
||||
// 轮询加载
|
||||
pollLoading: false,
|
||||
// 图片统计
|
||||
imageStats: { pending: 0, verified: 0, rejected: 0 },
|
||||
// 视频统计
|
||||
videoStats: { pending: 0, verified: 0, rejected: 0 },
|
||||
|
||||
// 图片
|
||||
imageList: [],
|
||||
imageLoading: false,
|
||||
imagePage: 1,
|
||||
imagePageSize: 20,
|
||||
imageTotal: 0,
|
||||
imageFilters: { status: '', accountId: '' },
|
||||
|
||||
// 视频
|
||||
videoList: [],
|
||||
videoLoading: false,
|
||||
videoPage: 1,
|
||||
videoPageSize: 20,
|
||||
videoTotal: 0,
|
||||
videoFilters: { status: '', accountId: '' },
|
||||
|
||||
// 日志
|
||||
logList: [],
|
||||
logLoading: false,
|
||||
logPage: 1,
|
||||
logPageSize: 20,
|
||||
logTotal: 0,
|
||||
logFilters: { materialType: '', verifyStatus: '', materialId: '' },
|
||||
|
||||
// 对话框
|
||||
logDialogVisible: false,
|
||||
previewVisible: false,
|
||||
logsDialogVisible: false,
|
||||
currentLog: null,
|
||||
previewUrl: '',
|
||||
previewType: 'image',
|
||||
materialLogs: [],
|
||||
batchLoading: false
|
||||
},
|
||||
mounted() {
|
||||
this.loadStats();
|
||||
this.loadImageList();
|
||||
},
|
||||
methods: {
|
||||
// API 请求
|
||||
apiGet(url, params = {}) {
|
||||
return axios.get(API_BASE + url, { params });
|
||||
},
|
||||
apiPost(url, data = {}) {
|
||||
return axios.post(API_BASE + url, data);
|
||||
},
|
||||
|
||||
// 加载统计(图片和视频分开存储)
|
||||
loadStats() {
|
||||
Promise.all([
|
||||
this.apiGet('/material/verify/controller/stats-image'),
|
||||
this.apiGet('/material/verify/controller/stats-video')
|
||||
]).then(results => {
|
||||
const imgStats = results[0].data.data || {};
|
||||
const vidStats = results[1].data.data || {};
|
||||
this.imageStats = {
|
||||
pending: imgStats.pending || 0,
|
||||
verified: imgStats.verified || 0,
|
||||
rejected: imgStats.rejected || 0
|
||||
};
|
||||
this.videoStats = {
|
||||
pending: vidStats.pending || 0,
|
||||
verified: vidStats.verified || 0,
|
||||
rejected: vidStats.rejected || 0
|
||||
};
|
||||
}).catch(err => {
|
||||
console.error('加载统计失败', err);
|
||||
});
|
||||
},
|
||||
|
||||
// 图片列表
|
||||
loadImageList() {
|
||||
this.imageLoading = true;
|
||||
const params = {
|
||||
page: this.imagePage,
|
||||
pageSize: this.imagePageSize,
|
||||
status: this.imageFilters.status,
|
||||
accountId: this.imageFilters.accountId
|
||||
};
|
||||
this.apiGet('/material/verify/controller/list-image', params).then(res => {
|
||||
this.imageList = res.data.data?.list || [];
|
||||
this.imageTotal = res.data.data?.total || 0;
|
||||
}).catch(err => {
|
||||
this.$message.error('加载图片列表失败');
|
||||
console.error(err);
|
||||
}).finally(() => {
|
||||
this.imageLoading = false;
|
||||
});
|
||||
},
|
||||
|
||||
// 视频列表
|
||||
loadVideoList() {
|
||||
this.videoLoading = true;
|
||||
const params = {
|
||||
page: this.videoPage,
|
||||
pageSize: this.videoPageSize,
|
||||
status: this.videoFilters.status,
|
||||
accountId: this.videoFilters.accountId
|
||||
};
|
||||
this.apiGet('/material/verify/controller/list-video', params).then(res => {
|
||||
this.videoList = res.data.data?.list || [];
|
||||
this.videoTotal = res.data.data?.total || 0;
|
||||
}).catch(err => {
|
||||
this.$message.error('加载视频列表失败');
|
||||
console.error(err);
|
||||
}).finally(() => {
|
||||
this.videoLoading = false;
|
||||
});
|
||||
},
|
||||
|
||||
// 日志列表
|
||||
loadLogList() {
|
||||
this.logLoading = true;
|
||||
const params = {
|
||||
page: this.logPage,
|
||||
pageSize: this.logPageSize,
|
||||
materialType: this.logFilters.materialType,
|
||||
verifyStatus: this.logFilters.verifyStatus,
|
||||
materialId: this.logFilters.materialId
|
||||
};
|
||||
this.apiGet('/material/verify/controller/list-log', params).then(res => {
|
||||
this.logList = res.data.data?.list || [];
|
||||
this.logTotal = res.data.data?.total || 0;
|
||||
}).catch(err => {
|
||||
this.$message.error('加载日志列表失败');
|
||||
console.error(err);
|
||||
}).finally(() => {
|
||||
this.logLoading = false;
|
||||
});
|
||||
},
|
||||
|
||||
// Tab 切换
|
||||
handleTabClick(tab) {
|
||||
if (tab.name === 'image') {
|
||||
this.loadImageList();
|
||||
} else if (tab.name === 'video') {
|
||||
this.loadVideoList();
|
||||
} else if (tab.name === 'log') {
|
||||
this.loadLogList();
|
||||
}
|
||||
},
|
||||
|
||||
// 搜索
|
||||
searchImage() {
|
||||
this.imagePage = 1;
|
||||
this.loadImageList();
|
||||
},
|
||||
searchVideo() {
|
||||
this.videoPage = 1;
|
||||
this.loadVideoList();
|
||||
},
|
||||
searchLog() {
|
||||
this.logPage = 1;
|
||||
this.loadLogList();
|
||||
},
|
||||
|
||||
// 重置筛选
|
||||
resetImageFilter() {
|
||||
this.imageFilters = { status: '', accountId: '' };
|
||||
this.searchImage();
|
||||
},
|
||||
resetVideoFilter() {
|
||||
this.videoFilters = { status: '', accountId: '' };
|
||||
this.searchVideo();
|
||||
},
|
||||
resetLogFilter() {
|
||||
this.logFilters = { materialType: '', verifyStatus: '', materialId: '' };
|
||||
this.searchLog();
|
||||
},
|
||||
|
||||
// 分页
|
||||
handleImagePageChange(page) {
|
||||
this.imagePage = page;
|
||||
this.loadImageList();
|
||||
},
|
||||
handleVideoPageChange(page) {
|
||||
this.videoPage = page;
|
||||
this.loadVideoList();
|
||||
},
|
||||
handleLogPageChange(page) {
|
||||
this.logPage = page;
|
||||
this.loadLogList();
|
||||
},
|
||||
|
||||
// 手动送检
|
||||
verifyImage(imageId) {
|
||||
this.$confirm('确认提交图片 ' + imageId + ' 进行校验?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
this.apiPost('/material/verify/controller/manual-verify-image', { materialId: imageId }).then(res => {
|
||||
this.$message.success('提交成功');
|
||||
this.loadImageList();
|
||||
this.loadStats();
|
||||
}).catch(err => {
|
||||
this.$message.error('提交失败: ' + (err.response?.data?.msg || err.message));
|
||||
});
|
||||
}).catch(() => {});
|
||||
},
|
||||
verifyVideo(videoId) {
|
||||
this.$confirm('确认提交视频 ' + videoId + ' 进行校验?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
this.apiPost('/material/verify/controller/manual-verify-video', { materialId: videoId }).then(res => {
|
||||
this.$message.success('提交成功');
|
||||
this.loadVideoList();
|
||||
this.loadStats();
|
||||
}).catch(err => {
|
||||
this.$message.error('提交失败: ' + (err.response?.data?.msg || err.message));
|
||||
});
|
||||
}).catch(() => {});
|
||||
},
|
||||
|
||||
// 刷新检测结果(轮询)
|
||||
pollImageResults() {
|
||||
this.pollLoading = true;
|
||||
this.apiPost('/yidun/callback/controller/poll-image-results').then(res => {
|
||||
const d = res.data;
|
||||
this.$message.success(d.msg || '刷新完成');
|
||||
this.loadImageList();
|
||||
this.loadStats();
|
||||
}).catch(err => {
|
||||
this.$message.error('刷新失败: ' + (err.response?.data?.msg || err.message));
|
||||
}).finally(() => {
|
||||
this.pollLoading = false;
|
||||
});
|
||||
},
|
||||
pollVideoResults() {
|
||||
this.pollLoading = true;
|
||||
this.apiPost('/yidun/callback/controller/poll-video-results').then(res => {
|
||||
const d = res.data;
|
||||
this.$message.success(d.msg || '刷新完成');
|
||||
this.loadVideoList();
|
||||
this.loadStats();
|
||||
}).catch(err => {
|
||||
this.$message.error('刷新失败: ' + (err.response?.data?.msg || err.message));
|
||||
}).finally(() => {
|
||||
this.pollLoading = false;
|
||||
});
|
||||
},
|
||||
|
||||
// 导出
|
||||
exportImageUrls() {
|
||||
if (!this.imageList.length) {
|
||||
this.$message.warning('没有可导出的图片');
|
||||
return;
|
||||
}
|
||||
const rows = [['ID', '图片ID', '账户ID', '用途', '预览URL', '状态', '描述']];
|
||||
this.imageList.forEach(item => {
|
||||
rows.push([
|
||||
item.id, item.imageId, item.accountId, item.imageUsage,
|
||||
item.previewUrl || '-',
|
||||
this.getStatusText(item.verifyStatus), item.description || '-'
|
||||
]);
|
||||
});
|
||||
this.downloadCsv('图片列表.csv', rows);
|
||||
},
|
||||
exportVideoUrls() {
|
||||
if (!this.videoList.length) {
|
||||
this.$message.warning('没有可导出的视频');
|
||||
return;
|
||||
}
|
||||
const rows = [['ID', '视频ID', '账户ID', '预览URL', '状态', '描述']];
|
||||
this.videoList.forEach(item => {
|
||||
rows.push([
|
||||
item.id, item.videoId, item.accountId,
|
||||
item.previewUrl || '-',
|
||||
this.getStatusText(item.verifyStatus), item.description || '-'
|
||||
]);
|
||||
});
|
||||
this.downloadCsv('视频列表.csv', rows);
|
||||
},
|
||||
downloadCsv(filename, rows) {
|
||||
const csv = rows.map(r => r.map(v => `"${String(v).replace(/"/g, '""')}"`).join(',')).join('\n');
|
||||
const blob = new Blob(['\uFEFF' + csv], { type: 'text/csv;charset=utf-8;' });
|
||||
const link = document.createElement('a');
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(link.href);
|
||||
},
|
||||
|
||||
// 批量送检
|
||||
batchVerifyImage() {
|
||||
this.$confirm('确认批量校验待处理的图片?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
this.batchLoading = true;
|
||||
this.apiPost('/material/verify/controller/batch-verify-image', {}).then(res => {
|
||||
this.$message.success(res.data.msg || '批量校验完成');
|
||||
this.loadImageList();
|
||||
this.loadStats();
|
||||
}).catch(err => {
|
||||
this.$message.error('批量校验失败: ' + (err.response?.data?.msg || err.message));
|
||||
}).finally(() => {
|
||||
this.batchLoading = false;
|
||||
});
|
||||
}).catch(() => {});
|
||||
},
|
||||
batchVerifyVideo() {
|
||||
this.$confirm('确认批量校验待处理的视频?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
this.batchLoading = true;
|
||||
this.apiPost('/material/verify/controller/batch-verify-video', {}).then(res => {
|
||||
this.$message.success(res.data.msg || '批量校验完成');
|
||||
this.loadVideoList();
|
||||
this.loadStats();
|
||||
}).catch(err => {
|
||||
this.$message.error('批量校验失败: ' + (err.response?.data?.msg || err.message));
|
||||
}).finally(() => {
|
||||
this.batchLoading = false;
|
||||
});
|
||||
}).catch(() => {});
|
||||
},
|
||||
|
||||
// 查看日志
|
||||
showLogs(materialType, materialId) {
|
||||
this.apiGet('/material/verify/controller/list-log', {
|
||||
materialType: materialType,
|
||||
materialId: materialId,
|
||||
pageSize: 100
|
||||
}).then(res => {
|
||||
// 后端返回格式为 { list: [...], total: xxx }
|
||||
this.materialLogs = (res.data.data && res.data.data.list) || [];
|
||||
this.logsDialogVisible = true;
|
||||
}).catch(err => {
|
||||
this.$message.error('加载日志失败');
|
||||
});
|
||||
},
|
||||
|
||||
// 日志详情
|
||||
showLogDetail(log) {
|
||||
this.currentLog = log;
|
||||
this.logDialogVisible = true;
|
||||
},
|
||||
|
||||
// 预览
|
||||
previewMedia(url, type) {
|
||||
if (!url) {
|
||||
this.$message.warning('无预览地址');
|
||||
return;
|
||||
}
|
||||
this.previewUrl = url;
|
||||
this.previewType = type;
|
||||
this.previewVisible = true;
|
||||
},
|
||||
|
||||
// 工具方法
|
||||
getStatusText(status) {
|
||||
const map = {
|
||||
'PENDING': '待校验',
|
||||
'SUBMITTING': '送检中',
|
||||
'VERIFIED': '校验通过',
|
||||
'REJECTED': '校验不通过'
|
||||
};
|
||||
return map[status] || status || '待校验';
|
||||
},
|
||||
getSuggestionText(suggestion) {
|
||||
const map = {
|
||||
0: '通过',
|
||||
1: '嫌疑',
|
||||
2: '不通过'
|
||||
};
|
||||
return map[suggestion] || '-';
|
||||
},
|
||||
formatTime(timeStr) {
|
||||
if (!timeStr) return '-';
|
||||
// 后端 gf gtime.Time 输出北京时间但不含时区(如 "2026-05-15 09:35:07")
|
||||
// 手动补上 +08:00 时区,new Date() 才能正确解析
|
||||
if (typeof timeStr === 'string' && !/Z|[+-]\d{2}:\d{2}$/i.test(timeStr)) {
|
||||
timeStr = timeStr.replace(' ', 'T') + '+08:00';
|
||||
}
|
||||
const date = new Date(timeStr);
|
||||
if (isNaN(date.getTime())) return timeStr;
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric', month: '2-digit', day: '2-digit',
|
||||
hour: '2-digit', minute: '2-digit', second: '2-digit'
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
662
service/dataengine/material_verify_service.go
Normal file
662
service/dataengine/material_verify_service.go
Normal file
@@ -0,0 +1,662 @@
|
||||
package dataengine
|
||||
|
||||
import (
|
||||
consts "cid/consts/dataengine"
|
||||
dao "cid/dao/dataengine"
|
||||
entity "cid/model/entity/dataengine"
|
||||
yidunService "cid/service/yidun"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
// 轮询配置常量
|
||||
const (
|
||||
// PollBatchSize 每次轮询处理数量
|
||||
PollBatchSize = 20
|
||||
)
|
||||
|
||||
// 状态常量
|
||||
const (
|
||||
// 原表状态 - 与 tencent_image/tencent_video 表的 status 字段对应
|
||||
StatusSubmitting = consts.CheckStatusSubmitting // 送检中
|
||||
)
|
||||
|
||||
// MaterialVerifyService 素材校验服务
|
||||
type MaterialVerifyService struct{}
|
||||
|
||||
// MaterialVerify 校验服务单例
|
||||
var MaterialVerify = new(MaterialVerifyService)
|
||||
|
||||
// =============================================================================
|
||||
// 校验状态转换
|
||||
// =============================================================================
|
||||
|
||||
// SuggestionToVerifyStatus 根据易盾处置建议转换为校验状态
|
||||
func SuggestionToVerifyStatus(suggestion int) string {
|
||||
switch suggestion {
|
||||
case consts.SuggestionPass:
|
||||
return entity.VerifyStatusVerified // 通过
|
||||
case consts.SuggestionReview:
|
||||
return entity.VerifyStatusPending // 嫌疑,需要人工审核,暂不更新状态
|
||||
case consts.SuggestionBlock:
|
||||
return entity.VerifyStatusRejected // 不通过
|
||||
default:
|
||||
return entity.VerifyStatusPending
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 图片校验
|
||||
// =============================================================================
|
||||
|
||||
// VerifyImageByID 根据图片ID执行校验
|
||||
func (s *MaterialVerifyService) VerifyImageByID(ctx context.Context, imageID string) (*entity.MaterialVerifyLog, error) {
|
||||
// 1. 获取图片数据
|
||||
image, err := dao.TencentImage.GetByImageID(ctx, imageID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询图片数据失败: %w", err)
|
||||
}
|
||||
if image == nil {
|
||||
return nil, fmt.Errorf("未找到图片数据, imageID=%s", imageID)
|
||||
}
|
||||
|
||||
// 2. 创建校验日志
|
||||
log := s.createVerifyLog(ctx, entity.MaterialTypeImage, imageID, consts.SourceTableTencentImage, image.Id, image.AccountID)
|
||||
if log == nil {
|
||||
return nil, fmt.Errorf("创建校验日志失败")
|
||||
}
|
||||
|
||||
// 3. 执行校验
|
||||
err = s.submitImageCheck(ctx, image, log)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return log, nil
|
||||
}
|
||||
|
||||
// submitImageCheck 提交图片校验
|
||||
func (s *MaterialVerifyService) submitImageCheck(ctx context.Context, image *entity.TencentImage, log *entity.MaterialVerifyLog) error {
|
||||
startTime := time.Now()
|
||||
|
||||
// 获取回调模式开关
|
||||
callbackMode := g.Cfg().MustGet(ctx, "yidun.callback_mode").Bool()
|
||||
|
||||
// 构建请求参数
|
||||
requestParams := map[string]interface{}{
|
||||
"imageURL": image.PreviewURL,
|
||||
"dataID": image.ImageID,
|
||||
}
|
||||
requestParamsJSON, _ := json.Marshal(requestParams)
|
||||
|
||||
var (
|
||||
taskID string
|
||||
duration int64
|
||||
)
|
||||
|
||||
if callbackMode {
|
||||
// 回调模式:使用异步检测,易盾处理完成后会回调
|
||||
callbackURL := g.Cfg().MustGet(ctx, "yidun.image.callback_url").String()
|
||||
requestParams["callbackURL"] = callbackURL
|
||||
|
||||
result, err := yidunService.ImageDetection.DetectImage(ctx, image.PreviewURL, image.ImageID, callbackURL)
|
||||
duration = time.Since(startTime).Milliseconds()
|
||||
if err != nil {
|
||||
dao.MaterialVerifyLog.UpdateError(ctx, log.Id, entity.VerifyStatusPending, err.Error())
|
||||
dao.MaterialVerifyLog.UpdateDuration(ctx, log.Id, duration)
|
||||
g.Log().Warningf(ctx, "图片异步检测失败(保持待检验), id=%d, imageId=%s, error=%v", image.Id, image.ImageID, err)
|
||||
return fmt.Errorf("图片异步检测失败: %w", err)
|
||||
}
|
||||
taskID = result.TaskID
|
||||
|
||||
// 保存任务ID和请求参数
|
||||
dao.MaterialVerifyLog.UpdateTaskID(ctx, log.Id, taskID)
|
||||
dao.MaterialVerifyLog.UpdateRequestParams(ctx, log.Id, string(requestParamsJSON))
|
||||
// 更新原表状态为 submitting(等待回调)
|
||||
s.updateImageStatus(ctx, image.Id, StatusSubmitting)
|
||||
|
||||
g.Log().Infof(ctx, "图片异步检测已提交, id=%d, imageId=%s, taskId=%s, duration=%dms",
|
||||
image.Id, image.ImageID, taskID, duration)
|
||||
} else {
|
||||
// 轮询模式:使用同步检测,直接返回结果
|
||||
syncResult, err := yidunService.ImageDetection.DetectImageSync(ctx, image.PreviewURL, image.ImageID)
|
||||
duration = time.Since(startTime).Milliseconds()
|
||||
if err != nil {
|
||||
dao.MaterialVerifyLog.UpdateError(ctx, log.Id, entity.VerifyStatusPending, err.Error())
|
||||
dao.MaterialVerifyLog.UpdateDuration(ctx, log.Id, duration)
|
||||
g.Log().Warningf(ctx, "图片同步检测失败(保持待检验), id=%d, imageId=%s, error=%v", image.Id, image.ImageID, err)
|
||||
return fmt.Errorf("图片同步检测失败: %w", err)
|
||||
}
|
||||
taskID = syncResult.TaskID
|
||||
|
||||
// 保存任务ID和请求参数
|
||||
dao.MaterialVerifyLog.UpdateTaskID(ctx, log.Id, taskID)
|
||||
dao.MaterialVerifyLog.UpdateRequestParams(ctx, log.Id, string(requestParamsJSON))
|
||||
|
||||
// 根据同步结果更新状态
|
||||
verifyStatus := SuggestionToVerifyStatus(syncResult.Suggestion)
|
||||
responseJSON, _ := json.Marshal(syncResult)
|
||||
dao.MaterialVerifyLog.UpdateVerifyResult(ctx, log.Id, verifyStatus,
|
||||
syncResult.Suggestion, syncResult.Label, syncResult.ResultType, string(responseJSON), syncResult.CensorTime)
|
||||
s.updateImageStatus(ctx, image.Id, verifyStatus)
|
||||
|
||||
g.Log().Infof(ctx, "图片同步检测完成, id=%d, imageId=%s, taskId=%s, suggestion=%d, verifyStatus=%s, duration=%dms",
|
||||
image.Id, image.ImageID, taskID, syncResult.Suggestion, verifyStatus, duration)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 视频校验
|
||||
// =============================================================================
|
||||
|
||||
// VerifyVideoByID 根据视频ID执行校验
|
||||
func (s *MaterialVerifyService) VerifyVideoByID(ctx context.Context, videoID string) (*entity.MaterialVerifyLog, error) {
|
||||
// 1. 获取视频数据
|
||||
video, err := dao.TencentVideo.GetByVideoID(ctx, videoID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询视频数据失败: %w", err)
|
||||
}
|
||||
if video == nil {
|
||||
return nil, fmt.Errorf("未找到视频数据, videoID=%s", videoID)
|
||||
}
|
||||
|
||||
// 2. 创建校验日志
|
||||
log := s.createVerifyLog(ctx, entity.MaterialTypeVideo, videoID, consts.SourceTableTencentVideo, video.Id, video.AccountID)
|
||||
if log == nil {
|
||||
return nil, fmt.Errorf("创建校验日志失败")
|
||||
}
|
||||
|
||||
// 3. 执行校验
|
||||
err = s.submitVideoCheck(ctx, video, log)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return log, nil
|
||||
}
|
||||
|
||||
// submitVideoCheck 提交视频校验
|
||||
func (s *MaterialVerifyService) submitVideoCheck(ctx context.Context, video *entity.TencentVideo, log *entity.MaterialVerifyLog) error {
|
||||
startTime := time.Now()
|
||||
|
||||
// 获取回调模式开关
|
||||
callbackMode := g.Cfg().MustGet(ctx, "yidun.callback_mode").Bool()
|
||||
|
||||
// 根据开关决定回调地址
|
||||
var callbackURL string
|
||||
if callbackMode {
|
||||
callbackURL = g.Cfg().MustGet(ctx, "yidun.video.callback_url").String()
|
||||
}
|
||||
|
||||
// 构建请求参数
|
||||
requestParams := map[string]interface{}{
|
||||
"videoURL": video.PreviewURL,
|
||||
"dataID": video.VideoID,
|
||||
"callbackURL": callbackURL,
|
||||
}
|
||||
requestParamsJSON, _ := json.Marshal(requestParams)
|
||||
|
||||
// 调用易盾视频检测
|
||||
result, err := yidunService.VideoDetection.DetectVideo(ctx, video.PreviewURL, video.VideoID, callbackURL)
|
||||
duration := time.Since(startTime).Milliseconds()
|
||||
|
||||
if err != nil {
|
||||
// 调用易盾接口失败(如额度用光、网络错误、超时等),不更新状态,保持待检验
|
||||
// 只有易盾明确返回检测结果且suggestion=BLOCK时才标记为失败
|
||||
dao.MaterialVerifyLog.UpdateError(ctx, log.Id, entity.VerifyStatusPending, err.Error())
|
||||
dao.MaterialVerifyLog.UpdateDuration(ctx, log.Id, duration)
|
||||
g.Log().Warningf(ctx, "视频校验接口调用失败(保持待检验), id=%d, videoId=%s, error=%v", video.Id, video.VideoID, err)
|
||||
return fmt.Errorf("视频校验调用失败: %w", err)
|
||||
}
|
||||
|
||||
// 保存任务ID和请求参数
|
||||
dao.MaterialVerifyLog.UpdateTaskID(ctx, log.Id, result.TaskID)
|
||||
dao.MaterialVerifyLog.UpdateRequestParams(ctx, log.Id, string(requestParamsJSON))
|
||||
|
||||
// 更新原表状态为 submitting
|
||||
s.updateVideoStatus(ctx, video.Id, StatusSubmitting)
|
||||
|
||||
// 轮询模式(无回调):提交后立即尝试查询检测结果
|
||||
if !callbackMode {
|
||||
g.Log().Infof(ctx, "轮询模式:提交后立即查询结果, taskId=%s", result.TaskID)
|
||||
// 等待500ms让易盾有时间处理
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
if err := s.ProcessVideoResultByTask(ctx, result.TaskID); err != nil {
|
||||
g.Log().Warningf(ctx, "提交后立即查询结果失败(不影响状态,后续轮询继续), taskId=%s, error=%v", result.TaskID, err)
|
||||
}
|
||||
}
|
||||
|
||||
g.Log().Infof(ctx, "视频校验已提交, id=%d, videoId=%s, taskId=%s, duration=%dms",
|
||||
video.Id, video.VideoID, result.TaskID, duration)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 回调处理
|
||||
// =============================================================================
|
||||
|
||||
// ProcessImageCallback 处理图片校验回调
|
||||
func (s *MaterialVerifyService) ProcessImageCallback(ctx context.Context, callbackData string) error {
|
||||
g.Log().Infof(ctx, "处理图片校验回调, data: %s", callbackData)
|
||||
|
||||
var callback yidunService.ImageCallbackData
|
||||
if err := json.Unmarshal([]byte(callbackData), &callback); err != nil {
|
||||
g.Log().Errorf(ctx, "解析图片回调数据失败: %v", err)
|
||||
return fmt.Errorf("解析回调数据失败: %w", err)
|
||||
}
|
||||
|
||||
if callback.Antispam == nil {
|
||||
return fmt.Errorf("回调数据格式错误:缺少antispam字段")
|
||||
}
|
||||
|
||||
antispam := callback.Antispam
|
||||
g.Log().Infof(ctx, "处理图片校验结果 - taskId: %s, suggestion: %d, resultType: %d",
|
||||
antispam.TaskId, antispam.Suggestion, antispam.ResultType)
|
||||
|
||||
// 根据 taskId 查找校验日志
|
||||
log, err := dao.MaterialVerifyLog.GetByTaskID(ctx, antispam.TaskId)
|
||||
if err != nil {
|
||||
return fmt.Errorf("查找校验日志失败: %w", err)
|
||||
}
|
||||
if log == nil {
|
||||
g.Log().Warningf(ctx, "未找到校验日志, taskId=%s", antispam.TaskId)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 构建响应结果
|
||||
responseResult := callbackData
|
||||
|
||||
// 根据 suggestion 确定校验状态
|
||||
verifyStatus := SuggestionToVerifyStatus(antispam.Suggestion)
|
||||
|
||||
// 更新日志
|
||||
err = dao.MaterialVerifyLog.UpdateVerifyResult(ctx, log.Id, verifyStatus,
|
||||
antispam.Suggestion, antispam.Label, antispam.ResultType, responseResult, antispam.CensorTime)
|
||||
if err != nil {
|
||||
return fmt.Errorf("更新校验日志失败: %w", err)
|
||||
}
|
||||
|
||||
// 更新原表状态(图片回调只处理图片来源)
|
||||
if log.SourceTable == consts.SourceTableTencentImage {
|
||||
s.updateImageStatus(ctx, log.SourceID, verifyStatus)
|
||||
}
|
||||
|
||||
g.Log().Infof(ctx, "图片校验回调处理完成, taskId=%s, verifyStatus=%s, suggestion=%d",
|
||||
antispam.TaskId, verifyStatus, antispam.Suggestion)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ProcessVideoCallback 处理视频校验回调
|
||||
func (s *MaterialVerifyService) ProcessVideoCallback(ctx context.Context, callbackData string) error {
|
||||
g.Log().Infof(ctx, "处理视频校验回调, data: %s", callbackData)
|
||||
|
||||
var callback yidunService.VideoCallbackData
|
||||
if err := json.Unmarshal([]byte(callbackData), &callback); err != nil {
|
||||
g.Log().Errorf(ctx, "解析视频回调数据失败: %v", err)
|
||||
return fmt.Errorf("解析回调数据失败: %w", err)
|
||||
}
|
||||
|
||||
if callback.Antispam == nil {
|
||||
return fmt.Errorf("视频回调数据格式错误:缺少antispam字段")
|
||||
}
|
||||
|
||||
antispam := callback.Antispam
|
||||
g.Log().Infof(ctx, "处理视频校验结果 - taskId: %s, suggestion: %d, resultType: %d",
|
||||
antispam.TaskID, antispam.Suggestion, antispam.ResultType)
|
||||
|
||||
// 根据 taskId 查找校验日志
|
||||
log, err := dao.MaterialVerifyLog.GetByTaskID(ctx, antispam.TaskID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("查找校验日志失败: %w", err)
|
||||
}
|
||||
if log == nil {
|
||||
g.Log().Warningf(ctx, "未找到校验日志, taskId=%s", antispam.TaskID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 构建响应结果
|
||||
responseResult := callbackData
|
||||
|
||||
// 根据 suggestion 确定校验状态
|
||||
verifyStatus := SuggestionToVerifyStatus(antispam.Suggestion)
|
||||
|
||||
// 审核时间
|
||||
checkTime := antispam.CensorTime
|
||||
if checkTime == 0 {
|
||||
checkTime = antispam.CheckTime
|
||||
}
|
||||
|
||||
// 更新日志
|
||||
err = dao.MaterialVerifyLog.UpdateVerifyResult(ctx, log.Id, verifyStatus,
|
||||
antispam.Suggestion, antispam.Label, antispam.ResultType, responseResult, checkTime)
|
||||
if err != nil {
|
||||
return fmt.Errorf("更新校验日志失败: %w", err)
|
||||
}
|
||||
|
||||
// 更新原表状态(视频回调只处理视频来源)
|
||||
if log.SourceTable == consts.SourceTableTencentVideo {
|
||||
s.updateVideoStatus(ctx, log.SourceID, verifyStatus)
|
||||
}
|
||||
|
||||
g.Log().Infof(ctx, "视频校验回调处理完成, taskId=%s, verifyStatus=%s, suggestion=%d",
|
||||
antispam.TaskID, verifyStatus, antispam.Suggestion)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 轮询模式处理
|
||||
// =============================================================================
|
||||
|
||||
// 易盾检测状态常量
|
||||
const (
|
||||
YidunStatusNotStart = 0 // 未开始
|
||||
YidunStatusProcessing = 1 // 检测中
|
||||
YidunStatusSuccess = 2 // 检测成功
|
||||
YidunStatusFailed = 3 // 检测失败
|
||||
)
|
||||
|
||||
// ProcessImageResultByTask 根据任务ID处理图片结果(轮询模式)
|
||||
func (s *MaterialVerifyService) ProcessImageResultByTask(ctx context.Context, taskID string) error {
|
||||
log, err := dao.MaterialVerifyLog.GetByTaskID(ctx, taskID)
|
||||
if err != nil || log == nil {
|
||||
return fmt.Errorf("未找到校验日志, taskId=%s", taskID)
|
||||
}
|
||||
|
||||
result, err := yidunService.ImageDetection.GetImageResult(ctx, taskID)
|
||||
if err != nil {
|
||||
// 判断是否是未找到结果或仍在检测中的错误
|
||||
if err == yidunService.ErrImageResultNotFound || err == yidunService.ErrImageStillProcessing {
|
||||
// 未获取到结果(任务不存在或仍在处理),不更新状态,保持等待下次轮询
|
||||
g.Log().Infof(ctx, "图片检测结果未就绪, taskId=%s, 保持pending状态, err=%v", taskID, err)
|
||||
return nil
|
||||
}
|
||||
// 其他错误(如额度用光、网络错误、API错误等),不更新状态,保持待检验
|
||||
// 只有易盾明确返回suggestion=BLOCK时才标记为失败
|
||||
dao.MaterialVerifyLog.UpdateError(ctx, log.Id, entity.VerifyStatusPending, err.Error())
|
||||
g.Log().Warningf(ctx, "图片检测查询失败(保持待检验), taskId=%s, error=%v", taskID, err)
|
||||
return nil // 返回nil避免日志被反复处理,但保持pending状态
|
||||
}
|
||||
|
||||
// 判断检测状态
|
||||
if result.Status == YidunStatusProcessing || result.Status == YidunStatusNotStart {
|
||||
// 检测仍在进行中,保持pending状态
|
||||
g.Log().Infof(ctx, "图片检测仍在进行中, taskId=%s, status=%d, 保持pending状态", taskID, result.Status)
|
||||
return nil
|
||||
}
|
||||
|
||||
if result.Status == YidunStatusFailed {
|
||||
// 易盾检测失败(如额度用光、服务端错误等),不更新状态,保持待检验
|
||||
// 只有易盾明确返回suggestion=BLOCK时才标记为失败
|
||||
errMsg := fmt.Sprintf("易盾检测失败, status=%d", result.Status)
|
||||
dao.MaterialVerifyLog.UpdateError(ctx, log.Id, entity.VerifyStatusPending, errMsg)
|
||||
g.Log().Warningf(ctx, "图片检测失败(保持待检验), taskId=%s, status=%d", taskID, result.Status)
|
||||
return nil
|
||||
}
|
||||
|
||||
// status == YidunStatusSuccess,检测成功,根据suggestion更新状态
|
||||
verifyStatus := SuggestionToVerifyStatus(result.Suggestion)
|
||||
responseJSON, _ := json.Marshal(result)
|
||||
|
||||
dao.MaterialVerifyLog.UpdateVerifyResult(ctx, log.Id, verifyStatus,
|
||||
result.Suggestion, result.Label, result.ResultType, string(responseJSON), result.CensorTime)
|
||||
|
||||
if log.SourceTable == consts.SourceTableTencentImage {
|
||||
s.updateImageStatus(ctx, log.SourceID, verifyStatus)
|
||||
}
|
||||
|
||||
g.Log().Infof(ctx, "图片检测结果更新成功, taskId=%s, status=%d, suggestion=%d, verifyStatus=%s",
|
||||
taskID, result.Status, result.Suggestion, verifyStatus)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ProcessVideoResultByTask 根据任务ID处理视频结果(轮询模式)
|
||||
func (s *MaterialVerifyService) ProcessVideoResultByTask(ctx context.Context, taskID string) error {
|
||||
log, err := dao.MaterialVerifyLog.GetByTaskID(ctx, taskID)
|
||||
if err != nil || log == nil {
|
||||
return fmt.Errorf("未找到校验日志, taskId=%s", taskID)
|
||||
}
|
||||
|
||||
result, err := yidunService.VideoDetection.GetVideoResult(ctx, taskID)
|
||||
if err != nil {
|
||||
// 判断是否是未找到结果或仍在检测中的错误
|
||||
if err == yidunService.ErrVideoResultNotFound || err == yidunService.ErrVideoStillProcessing {
|
||||
// 未获取到结果(任务不存在或仍在处理),不更新状态,保持等待下次轮询
|
||||
g.Log().Infof(ctx, "视频检测结果未就绪, taskId=%s, 保持pending状态, err=%v", taskID, err)
|
||||
return nil
|
||||
}
|
||||
// 其他错误(如额度用光、网络错误、API错误等),不更新状态,保持待检验
|
||||
// 只有易盾明确返回suggestion=BLOCK时才标记为失败
|
||||
dao.MaterialVerifyLog.UpdateError(ctx, log.Id, entity.VerifyStatusPending, err.Error())
|
||||
g.Log().Warningf(ctx, "视频检测查询失败(保持待检验), taskId=%s, error=%v", taskID, err)
|
||||
return nil // 返回nil避免日志被反复处理,但保持pending状态
|
||||
}
|
||||
|
||||
// 判断检测状态
|
||||
if result.Status == YidunStatusProcessing || result.Status == YidunStatusNotStart {
|
||||
// 检测仍在进行中,保持pending状态
|
||||
g.Log().Infof(ctx, "视频检测仍在进行中, taskId=%s, status=%d, 保持pending状态", taskID, result.Status)
|
||||
return nil
|
||||
}
|
||||
|
||||
if result.Status == YidunStatusFailed {
|
||||
// 易盾检测失败(如额度用光、服务端错误等),不更新状态,保持待检验
|
||||
// 只有易盾明确返回suggestion=BLOCK时才标记为失败
|
||||
errMsg := fmt.Sprintf("易盾检测失败, status=%d", result.Status)
|
||||
dao.MaterialVerifyLog.UpdateError(ctx, log.Id, entity.VerifyStatusPending, errMsg)
|
||||
g.Log().Warningf(ctx, "视频检测失败(保持待检验), taskId=%s, status=%d", taskID, result.Status)
|
||||
return nil
|
||||
}
|
||||
|
||||
// status == YidunStatusSuccess,检测成功,根据suggestion更新状态
|
||||
verifyStatus := SuggestionToVerifyStatus(result.Suggestion)
|
||||
responseJSON, _ := json.Marshal(result)
|
||||
|
||||
dao.MaterialVerifyLog.UpdateVerifyResult(ctx, log.Id, verifyStatus,
|
||||
result.Suggestion, result.Label, result.ResultType, string(responseJSON), result.CensorTime)
|
||||
|
||||
if log.SourceTable == consts.SourceTableTencentVideo {
|
||||
s.updateVideoStatus(ctx, log.SourceID, verifyStatus)
|
||||
}
|
||||
|
||||
g.Log().Infof(ctx, "视频检测结果更新成功, taskId=%s, status=%d, suggestion=%d, verifyStatus=%s",
|
||||
taskID, result.Status, result.Suggestion, verifyStatus)
|
||||
return nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 辅助方法
|
||||
// =============================================================================
|
||||
|
||||
// createVerifyLog 创建校验日志
|
||||
func (s *MaterialVerifyService) createVerifyLog(ctx context.Context, materialType, materialID, sourceTable string, sourceID, accountID int64) *entity.MaterialVerifyLog {
|
||||
log := &entity.MaterialVerifyLog{
|
||||
TenantID: 0,
|
||||
MaterialType: materialType,
|
||||
MaterialID: materialID,
|
||||
SourceTable: sourceTable,
|
||||
SourceID: sourceID,
|
||||
AccountID: accountID,
|
||||
VerifyStatus: entity.VerifyStatusPending,
|
||||
}
|
||||
|
||||
id, err := dao.MaterialVerifyLog.Create(ctx, log)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "创建校验日志失败: %v", err)
|
||||
return nil
|
||||
}
|
||||
log.Id = id
|
||||
|
||||
return log
|
||||
}
|
||||
|
||||
// updateImageStatus 更新图片状态
|
||||
func (s *MaterialVerifyService) updateImageStatus(ctx context.Context, imageID int64, verifyStatus string) {
|
||||
_, err := dao.TencentImage.UpdateStatus(ctx, imageID, verifyStatus)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "更新图片状态失败: %v", err)
|
||||
} else {
|
||||
g.Log().Infof(ctx, "更新图片状态成功, imageID=%d, status=%s", imageID, verifyStatus)
|
||||
}
|
||||
}
|
||||
|
||||
// updateVideoStatus 更新视频状态
|
||||
func (s *MaterialVerifyService) updateVideoStatus(ctx context.Context, videoID int64, verifyStatus string) {
|
||||
_, err := dao.TencentVideo.UpdateStatus(ctx, videoID, verifyStatus)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "更新视频状态失败: %v", err)
|
||||
} else {
|
||||
g.Log().Infof(ctx, "更新视频状态成功, videoID=%d, status=%s", videoID, verifyStatus)
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 查询接口
|
||||
// =============================================================================
|
||||
|
||||
// GetLogByID 根据ID获取日志
|
||||
func (s *MaterialVerifyService) GetLogByID(ctx context.Context, id int64) (*entity.MaterialVerifyLog, error) {
|
||||
return dao.MaterialVerifyLog.GetByID(ctx, id)
|
||||
}
|
||||
|
||||
// GetLogsByMaterialID 根据素材ID获取日志列表
|
||||
func (s *MaterialVerifyService) GetLogsByMaterialID(ctx context.Context, materialID string) ([]entity.MaterialVerifyLog, error) {
|
||||
return dao.MaterialVerifyLog.GetByMaterialID(ctx, materialID)
|
||||
}
|
||||
|
||||
// GetLogsByCondition 条件查询日志
|
||||
func (s *MaterialVerifyService) GetLogsByCondition(ctx context.Context, condition map[string]interface{}, page, pageSize int) ([]entity.MaterialVerifyLog, int, error) {
|
||||
return dao.MaterialVerifyLog.GetByCondition(ctx, condition, page, pageSize)
|
||||
}
|
||||
|
||||
// GetStats 获取统计信息
|
||||
func (s *MaterialVerifyService) GetStats(ctx context.Context) (map[string]int, error) {
|
||||
return dao.MaterialVerifyLog.GetStats(ctx)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 轮询模式 - 批量查询检测结果
|
||||
// =============================================================================
|
||||
|
||||
// PollPendingResults 轮询所有待查询结果的日志(手动触发)
|
||||
// 返回处理成功的数量和错误信息
|
||||
func (s *MaterialVerifyService) PollPendingResults(ctx context.Context) (int, int, error) {
|
||||
// 获取待查询的日志
|
||||
logs, err := dao.MaterialVerifyLog.GetPendingResults(ctx, PollBatchSize)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
if len(logs) == 0 {
|
||||
g.Log().Infof(ctx, "没有待查询结果的日志")
|
||||
return 0, 0, nil
|
||||
}
|
||||
|
||||
g.Log().Infof(ctx, "开始轮询 %d 条待处理结果", len(logs))
|
||||
|
||||
successCount := 0
|
||||
failCount := 0
|
||||
var lastErr error
|
||||
|
||||
for _, log := range logs {
|
||||
var err error
|
||||
|
||||
// 根据来源表判断调用哪个接口
|
||||
if log.SourceTable == consts.SourceTableTencentImage {
|
||||
err = s.ProcessImageResultByTask(ctx, log.TaskID)
|
||||
} else if log.SourceTable == consts.SourceTableTencentVideo {
|
||||
err = s.ProcessVideoResultByTask(ctx, log.TaskID)
|
||||
} else {
|
||||
g.Log().Warningf(ctx, "未知的来源表: %s, logId=%d", log.SourceTable, log.Id)
|
||||
continue
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
failCount++
|
||||
lastErr = err
|
||||
g.Log().Warningf(ctx, "处理结果失败, logId=%d, taskId=%s, error=%v", log.Id, log.TaskID, err)
|
||||
} else {
|
||||
successCount++
|
||||
g.Log().Infof(ctx, "处理结果成功, logId=%d, taskId=%s", log.Id, log.TaskID)
|
||||
}
|
||||
|
||||
// 避免请求过快
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
g.Log().Infof(ctx, "轮询完成, 成功=%d, 失败=%d", successCount, failCount)
|
||||
return successCount, failCount, lastErr
|
||||
}
|
||||
|
||||
// PollPendingResultsByType 按类型轮询待查询结果的日志
|
||||
func (s *MaterialVerifyService) PollPendingResultsByType(ctx context.Context, sourceTable string) (int, int, error) {
|
||||
// 获取待查询的日志
|
||||
logs, err := dao.MaterialVerifyLog.GetPendingResults(ctx, PollBatchSize)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
// 过滤指定类型
|
||||
var filteredLogs []entity.MaterialVerifyLog
|
||||
for _, log := range logs {
|
||||
if log.SourceTable == sourceTable {
|
||||
filteredLogs = append(filteredLogs, log)
|
||||
}
|
||||
}
|
||||
|
||||
if len(filteredLogs) == 0 {
|
||||
g.Log().Infof(ctx, "没有待查询结果的日志, sourceTable=%s", sourceTable)
|
||||
return 0, 0, nil
|
||||
}
|
||||
|
||||
g.Log().Infof(ctx, "开始轮询 %d 条待处理结果, sourceTable=%s", len(filteredLogs), sourceTable)
|
||||
|
||||
successCount := 0
|
||||
failCount := 0
|
||||
var lastErr error
|
||||
|
||||
for _, log := range filteredLogs {
|
||||
var err error
|
||||
|
||||
if sourceTable == consts.SourceTableTencentImage {
|
||||
err = s.ProcessImageResultByTask(ctx, log.TaskID)
|
||||
} else if sourceTable == consts.SourceTableTencentVideo {
|
||||
err = s.ProcessVideoResultByTask(ctx, log.TaskID)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
failCount++
|
||||
lastErr = err
|
||||
} else {
|
||||
successCount++
|
||||
}
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
g.Log().Infof(ctx, "轮询完成, sourceTable=%s, 成功=%d, 失败=%d", sourceTable, successCount, failCount)
|
||||
return successCount, failCount, lastErr
|
||||
}
|
||||
|
||||
// PollPendingImageResults 轮询图片待查询结果
|
||||
func (s *MaterialVerifyService) PollPendingImageResults(ctx context.Context) (int, int, error) {
|
||||
return s.PollPendingResultsByType(ctx, consts.SourceTableTencentImage)
|
||||
}
|
||||
|
||||
// PollPendingVideoResults 轮询视频待查询结果
|
||||
func (s *MaterialVerifyService) PollPendingVideoResults(ctx context.Context) (int, int, error) {
|
||||
return s.PollPendingResultsByType(ctx, consts.SourceTableTencentVideo)
|
||||
}
|
||||
|
||||
// GetPendingResultsCount 获取待查询结果的数量
|
||||
func (s *MaterialVerifyService) GetPendingResultsCount(ctx context.Context) (int, error) {
|
||||
return dao.MaterialVerifyLog.CountPendingResults(ctx)
|
||||
}
|
||||
190
service/dataengine/tencent_content_callback_service.go
Normal file
190
service/dataengine/tencent_content_callback_service.go
Normal file
@@ -0,0 +1,190 @@
|
||||
package dataengine
|
||||
|
||||
import (
|
||||
consts "cid/consts/dataengine"
|
||||
dao "cid/dao/dataengine"
|
||||
entity "cid/model/entity/dataengine"
|
||||
yidunService "cid/service/yidun"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
// TencentContentCallbackService 腾讯内容检测回调处理服务
|
||||
type TencentContentCallbackService struct{}
|
||||
|
||||
// TencentContentCallback 回调处理服务单例
|
||||
var TencentContentCallback = new(TencentContentCallbackService)
|
||||
|
||||
// ProcessImageCallback 处理图片检测回调
|
||||
func (s *TencentContentCallbackService) ProcessImageCallback(ctx context.Context, callbackData string) error {
|
||||
g.Log().Infof(ctx, "处理图片检测回调, data: %s", callbackData)
|
||||
|
||||
var callback yidunService.ImageCallbackData
|
||||
if err := json.Unmarshal([]byte(callbackData), &callback); err != nil {
|
||||
g.Log().Errorf(ctx, "解析图片回调数据失败: %v", err)
|
||||
return fmt.Errorf("解析回调数据失败: %w", err)
|
||||
}
|
||||
|
||||
if callback.Antispam == nil {
|
||||
return fmt.Errorf("回调数据格式错误:缺少antispam字段")
|
||||
}
|
||||
|
||||
antispam := callback.Antispam
|
||||
g.Log().Infof(ctx, "处理图片检测结果 - taskId: %s, suggestion: %d, resultType: %d",
|
||||
antispam.TaskId, antispam.Suggestion, antispam.ResultType)
|
||||
|
||||
// 根据 taskId 查找送检日志
|
||||
log, err := dao.TencentContentCheckLog.GetByTaskID(ctx, antispam.TaskId)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "查找送检日志失败, taskId=%s: %v", antispam.TaskId, err)
|
||||
return fmt.Errorf("查找送检日志失败: %w", err)
|
||||
}
|
||||
|
||||
if log == nil {
|
||||
g.Log().Warningf(ctx, "未找到送检日志, taskId=%s", antispam.TaskId)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 更新送检日志
|
||||
checkTime := antispam.CensorTime
|
||||
|
||||
err = dao.TencentContentCheckLog.UpdateCheckResult(ctx, log.Id,
|
||||
antispam.Suggestion, antispam.Label, antispam.ResultType, checkTime)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "更新送检日志检测结果失败: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
g.Log().Infof(ctx, "图片检测回调处理完成, taskId=%s, suggestion=%d", antispam.TaskId, antispam.Suggestion)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ProcessVideoCallback 处理视频检测回调
|
||||
func (s *TencentContentCallbackService) ProcessVideoCallback(ctx context.Context, callbackData string) error {
|
||||
g.Log().Infof(ctx, "处理视频检测回调, data: %s", callbackData)
|
||||
|
||||
var callback yidunService.VideoCallbackData
|
||||
if err := json.Unmarshal([]byte(callbackData), &callback); err != nil {
|
||||
g.Log().Errorf(ctx, "解析视频回调数据失败: %v", err)
|
||||
return fmt.Errorf("解析回调数据失败: %w", err)
|
||||
}
|
||||
|
||||
if callback.Antispam == nil {
|
||||
return fmt.Errorf("回调数据格式错误:缺少antispam字段")
|
||||
}
|
||||
|
||||
antispam := callback.Antispam
|
||||
g.Log().Infof(ctx, "处理视频检测结果 - taskId: %s, suggestion: %d, resultType: %d, censorSource: %d",
|
||||
antispam.TaskID, antispam.Suggestion, antispam.ResultType, antispam.CensorSource)
|
||||
|
||||
// 根据 taskId 查找送检日志
|
||||
log, err := dao.TencentContentCheckLog.GetByTaskID(ctx, antispam.TaskID)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "查找送检日志失败, taskId=%s: %v", antispam.TaskID, err)
|
||||
return fmt.Errorf("查找送检日志失败: %w", err)
|
||||
}
|
||||
|
||||
if log == nil {
|
||||
g.Log().Warningf(ctx, "未找到送检日志, taskId=%s", antispam.TaskID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 更新送检日志
|
||||
checkTime := antispam.CensorTime
|
||||
if checkTime == 0 {
|
||||
checkTime = antispam.CheckTime
|
||||
}
|
||||
|
||||
err = dao.TencentContentCheckLog.UpdateCheckResult(ctx, log.Id,
|
||||
antispam.Suggestion, antispam.Label, antispam.ResultType, checkTime)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "更新送检日志检测结果失败: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
g.Log().Infof(ctx, "视频检测回调处理完成, taskId=%s, suggestion=%d", antispam.TaskID, antispam.Suggestion)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ProcessImageResult 手动处理图片检测结果(轮询模式)
|
||||
func (s *TencentContentCallbackService) ProcessImageResult(ctx context.Context, taskID string) error {
|
||||
g.Log().Infof(ctx, "查询图片检测结果, taskId: %s", taskID)
|
||||
|
||||
// 查找送检日志
|
||||
log, err := dao.TencentContentCheckLog.GetByTaskID(ctx, taskID)
|
||||
if err != nil || log == nil {
|
||||
return fmt.Errorf("未找到送检日志, taskId=%s", taskID)
|
||||
}
|
||||
|
||||
// 调用易盾查询结果
|
||||
result, err := yidunService.ImageDetection.GetImageResult(ctx, taskID)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "查询图片检测结果失败: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新日志
|
||||
err = dao.TencentContentCheckLog.UpdateCheckResult(ctx, log.Id,
|
||||
result.Suggestion, result.Label, result.ResultType, result.CensorTime)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "更新送检日志检测结果失败: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
g.Log().Infof(ctx, "图片检测结果处理完成, taskId=%s, suggestion=%d", taskID, result.Suggestion)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ProcessVideoResult 手动处理视频检测结果(轮询模式)
|
||||
func (s *TencentContentCallbackService) ProcessVideoResult(ctx context.Context, taskID string) error {
|
||||
g.Log().Infof(ctx, "查询视频检测结果, taskId: %s", taskID)
|
||||
|
||||
// 查找送检日志
|
||||
log, err := dao.TencentContentCheckLog.GetByTaskID(ctx, taskID)
|
||||
if err != nil || log == nil {
|
||||
return fmt.Errorf("未找到送检日志, taskId=%s", taskID)
|
||||
}
|
||||
|
||||
// 调用易盾查询结果
|
||||
result, err := yidunService.VideoDetection.GetVideoResult(ctx, taskID)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "查询视频检测结果失败: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新日志
|
||||
err = dao.TencentContentCheckLog.UpdateCheckResult(ctx, log.Id,
|
||||
result.Suggestion, result.Label, result.ResultType, result.CensorTime)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "更新送检日志检测结果失败: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
g.Log().Infof(ctx, "视频检测结果处理完成, taskId=%s, suggestion=%d", taskID, result.Suggestion)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCheckLogsByImageID 根据图片ID获取送检日志
|
||||
func (s *TencentContentCallbackService) GetCheckLogsByImageID(ctx context.Context, imageID string) ([]entity.TencentContentCheckLog, error) {
|
||||
// 先获取图片数据
|
||||
image, err := dao.TencentImage.GetByImageID(ctx, imageID)
|
||||
if err != nil || image == nil {
|
||||
return nil, fmt.Errorf("未找到图片数据")
|
||||
}
|
||||
|
||||
return dao.TencentContentCheckLog.GetBySourceID(ctx, consts.SourceTableTencentImage, image.Id)
|
||||
}
|
||||
|
||||
// GetCheckLogsByVideoID 根据视频ID获取送检日志
|
||||
func (s *TencentContentCallbackService) GetCheckLogsByVideoID(ctx context.Context, videoID string) ([]entity.TencentContentCheckLog, error) {
|
||||
// 先获取视频数据
|
||||
video, err := dao.TencentVideo.GetByVideoID(ctx, videoID)
|
||||
if err != nil || video == nil {
|
||||
return nil, fmt.Errorf("未找到视频数据")
|
||||
}
|
||||
|
||||
return dao.TencentContentCheckLog.GetBySourceID(ctx, consts.SourceTableTencentVideo, video.Id)
|
||||
}
|
||||
390
service/dataengine/tencent_content_check_service.go
Normal file
390
service/dataengine/tencent_content_check_service.go
Normal file
@@ -0,0 +1,390 @@
|
||||
package dataengine
|
||||
|
||||
import (
|
||||
consts "cid/consts/dataengine"
|
||||
dao "cid/dao/dataengine"
|
||||
entity "cid/model/entity/dataengine"
|
||||
yidunService "cid/service/yidun"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"gitea.com/red-future/common/beans"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
// ContentCheckConfig 送检配置
|
||||
type ContentCheckConfig struct {
|
||||
// 每批处理数量
|
||||
BatchSize int `json:"batch_size"`
|
||||
// 图片检测启用
|
||||
ImageEnabled bool `json:"image_enabled"`
|
||||
// 视频检测启用
|
||||
VideoEnabled bool `json:"video_enabled"`
|
||||
// 定时任务间隔(秒)
|
||||
IntervalSeconds int `json:"interval_seconds"`
|
||||
}
|
||||
|
||||
// DefaultConfig 默认配置
|
||||
var DefaultConfig = ContentCheckConfig{
|
||||
BatchSize: 10,
|
||||
ImageEnabled: true,
|
||||
VideoEnabled: true,
|
||||
IntervalSeconds: 30,
|
||||
}
|
||||
|
||||
// TencentContentCheckService 腾讯内容送检服务
|
||||
type TencentContentCheckService struct {
|
||||
config ContentCheckConfig
|
||||
isRunning bool
|
||||
}
|
||||
|
||||
// TencentContentCheck 送检服务单例
|
||||
var TencentContentCheck = &TencentContentCheckService{
|
||||
config: DefaultConfig,
|
||||
}
|
||||
|
||||
// SetConfig 设置配置
|
||||
func (s *TencentContentCheckService) SetConfig(config ContentCheckConfig) {
|
||||
s.config = config
|
||||
}
|
||||
|
||||
// Start 启动定时任务
|
||||
func (s *TencentContentCheckService) Start(ctx context.Context) error {
|
||||
if s.isRunning {
|
||||
g.Log().Info(ctx, "送检服务已在运行中,跳过启动")
|
||||
return nil
|
||||
}
|
||||
|
||||
s.isRunning = true
|
||||
g.Log().Infof(ctx, "启动内容送检服务,配置: batch_size=%d, interval=%ds, image=%v, video=%v",
|
||||
s.config.BatchSize, s.config.IntervalSeconds, s.config.ImageEnabled, s.config.VideoEnabled)
|
||||
|
||||
go s.runScheduler(ctx)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop 停止定时任务
|
||||
func (s *TencentContentCheckService) Stop(ctx context.Context) {
|
||||
s.isRunning = false
|
||||
g.Log().Info(ctx, "停止内容送检服务")
|
||||
}
|
||||
|
||||
// runScheduler 定时调度器
|
||||
func (s *TencentContentCheckService) runScheduler(ctx context.Context) {
|
||||
ticker := time.NewTicker(time.Duration(s.config.IntervalSeconds) * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
// 启动时先执行一次
|
||||
s.processAll(ctx)
|
||||
|
||||
for s.isRunning {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
s.processAll(ctx)
|
||||
case <-ctx.Done():
|
||||
s.isRunning = false
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// processAll 处理所有待送检数据
|
||||
func (s *TencentContentCheckService) processAll(ctx context.Context) {
|
||||
// 添加系统用户上下文,绕过gfdb租户验证
|
||||
ctx = context.WithValue(ctx, "user", &beans.User{UserName: "system", TenantId: 1})
|
||||
|
||||
startTime := time.Now()
|
||||
g.Log().Info(ctx, "开始处理待送检数据...")
|
||||
|
||||
var totalProcessed int
|
||||
|
||||
// 处理图片
|
||||
if s.config.ImageEnabled {
|
||||
imageCount, _ := dao.TencentImage.CountPending(ctx)
|
||||
if imageCount > 0 {
|
||||
count, _ := s.processImages(ctx)
|
||||
totalProcessed += count
|
||||
}
|
||||
}
|
||||
|
||||
// 处理视频
|
||||
if s.config.VideoEnabled {
|
||||
videoCount, _ := dao.TencentVideo.CountPending(ctx)
|
||||
if videoCount > 0 {
|
||||
count, _ := s.processVideos(ctx)
|
||||
totalProcessed += count
|
||||
}
|
||||
}
|
||||
|
||||
duration := time.Since(startTime).Milliseconds()
|
||||
g.Log().Infof(ctx, "处理完成,共处理 %d 条数据,耗时 %dms", totalProcessed, duration)
|
||||
}
|
||||
|
||||
// processImages 处理图片送检
|
||||
func (s *TencentContentCheckService) processImages(ctx context.Context) (int, error) {
|
||||
// 获取待送检图片
|
||||
images, err := dao.TencentImage.GetPendingList(ctx, s.config.BatchSize)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "获取待送检图片失败: %v", err)
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if len(images) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
g.Log().Infof(ctx, "开始送检 %d 张图片", len(images))
|
||||
|
||||
successCount := 0
|
||||
failedCount := 0
|
||||
|
||||
for _, img := range images {
|
||||
// 创建送检日志
|
||||
log := s.createCheckLog(ctx, consts.SourceTableTencentImage, img.Id, img.ImageID, img.PreviewURL)
|
||||
|
||||
// 提交送检
|
||||
err := s.submitImageCheck(ctx, &img, log)
|
||||
if err != nil {
|
||||
failedCount++
|
||||
// 更新日志为失败
|
||||
if log != nil {
|
||||
dao.TencentContentCheckLog.UpdateStatus(ctx, log.Id, consts.CheckStatusFailed, "", err.Error())
|
||||
}
|
||||
} else {
|
||||
successCount++
|
||||
}
|
||||
|
||||
// 避免请求过快
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
g.Log().Infof(ctx, "图片送检完成,成功: %d,失败: %d", successCount, failedCount)
|
||||
return len(images), nil
|
||||
}
|
||||
|
||||
// processVideos 处理视频送检
|
||||
func (s *TencentContentCheckService) processVideos(ctx context.Context) (int, error) {
|
||||
// 获取待送检视频
|
||||
videos, err := dao.TencentVideo.GetPendingList(ctx, s.config.BatchSize)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "获取待送检视频失败: %v", err)
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if len(videos) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
g.Log().Infof(ctx, "开始送检 %d 个视频", len(videos))
|
||||
|
||||
successCount := 0
|
||||
failedCount := 0
|
||||
|
||||
for _, video := range videos {
|
||||
// 创建送检日志
|
||||
log := s.createCheckLog(ctx, consts.SourceTableTencentVideo, video.Id, video.VideoID, video.PreviewURL)
|
||||
|
||||
// 提交送检
|
||||
err := s.submitVideoCheck(ctx, &video, log)
|
||||
if err != nil {
|
||||
failedCount++
|
||||
// 更新日志为失败
|
||||
if log != nil {
|
||||
dao.TencentContentCheckLog.UpdateStatus(ctx, log.Id, consts.CheckStatusFailed, "", err.Error())
|
||||
}
|
||||
} else {
|
||||
successCount++
|
||||
}
|
||||
|
||||
// 避免请求过快
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
g.Log().Infof(ctx, "视频送检完成,成功: %d,失败: %d", successCount, failedCount)
|
||||
return len(videos), nil
|
||||
}
|
||||
|
||||
// createCheckLog 创建送检日志
|
||||
func (s *TencentContentCheckService) createCheckLog(ctx context.Context, sourceTable string, sourceID int64, mediaID string, mediaURL string) *entity.TencentContentCheckLog {
|
||||
requestParam := map[string]interface{}{
|
||||
"media_id": mediaID,
|
||||
"url": mediaURL,
|
||||
}
|
||||
requestParamJSON, _ := json.Marshal(requestParam)
|
||||
|
||||
log := &entity.TencentContentCheckLog{
|
||||
SourceTable: sourceTable,
|
||||
SourceID: sourceID,
|
||||
RequestURL: "易盾内容安全检测接口",
|
||||
RequestParam: string(requestParamJSON),
|
||||
Status: consts.CheckStatusPending,
|
||||
CheckTime: time.Now().UnixMilli(),
|
||||
}
|
||||
|
||||
id, err := dao.TencentContentCheckLog.Create(ctx, log)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "创建送检日志失败: %v", err)
|
||||
return nil
|
||||
}
|
||||
log.Id = id
|
||||
|
||||
g.Log().Debugf(ctx, "创建送检日志成功, id=%d, sourceTable=%s, sourceID=%d", id, sourceTable, sourceID)
|
||||
return log
|
||||
}
|
||||
|
||||
// submitImageCheck 提交图片送检
|
||||
func (s *TencentContentCheckService) submitImageCheck(ctx context.Context, image *entity.TencentImage, log *entity.TencentContentCheckLog) error {
|
||||
startTime := time.Now()
|
||||
|
||||
// 更新日志状态为送检中
|
||||
if log != nil {
|
||||
dao.TencentContentCheckLog.UpdateStatus(ctx, log.Id, consts.CheckStatusSubmitting, "", "")
|
||||
}
|
||||
|
||||
// 获取回调地址
|
||||
callbackURL := g.Cfg().MustGet(ctx, "yidun.image.callback_url").String()
|
||||
|
||||
// 调用易盾图片检测
|
||||
result, err := yidunService.ImageDetection.DetectImage(ctx, image.PreviewURL, image.ImageID, callbackURL)
|
||||
|
||||
duration := time.Since(startTime).Milliseconds()
|
||||
|
||||
// 更新日志
|
||||
if log != nil {
|
||||
if err != nil {
|
||||
dao.TencentContentCheckLog.UpdateDuration(ctx, log.Id, duration)
|
||||
dao.TencentContentCheckLog.UpdateStatus(ctx, log.Id, consts.CheckStatusFailed, "", err.Error())
|
||||
g.Log().Errorf(ctx, "图片送检失败, id=%d, url=%s, error=%v", image.Id, image.PreviewURL, err)
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新日志和图片状态
|
||||
responseData, _ := json.Marshal(result)
|
||||
dao.TencentContentCheckLog.UpdateStatus(ctx, log.Id, consts.CheckStatusSuccess, string(responseData), "")
|
||||
dao.TencentContentCheckLog.UpdateTaskID(ctx, log.Id, result.TaskID)
|
||||
dao.TencentContentCheckLog.UpdateDuration(ctx, log.Id, duration)
|
||||
}
|
||||
|
||||
g.Log().Infof(ctx, "图片送检成功, id=%d, imageId=%s, taskId=%s", image.Id, image.ImageID, result.TaskID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// submitVideoCheck 提交视频送检
|
||||
func (s *TencentContentCheckService) submitVideoCheck(ctx context.Context, video *entity.TencentVideo, log *entity.TencentContentCheckLog) error {
|
||||
startTime := time.Now()
|
||||
|
||||
// 更新日志状态为送检中
|
||||
if log != nil {
|
||||
dao.TencentContentCheckLog.UpdateStatus(ctx, log.Id, consts.CheckStatusSubmitting, "", "")
|
||||
}
|
||||
|
||||
// 获取回调地址
|
||||
callbackURL := g.Cfg().MustGet(ctx, "yidun.video.callback_url").String()
|
||||
|
||||
// 调用易盾视频检测
|
||||
result, err := yidunService.VideoDetection.DetectVideo(ctx, video.PreviewURL, video.VideoID, callbackURL)
|
||||
|
||||
duration := time.Since(startTime).Milliseconds()
|
||||
|
||||
// 更新日志
|
||||
if log != nil {
|
||||
if err != nil {
|
||||
dao.TencentContentCheckLog.UpdateDuration(ctx, log.Id, duration)
|
||||
dao.TencentContentCheckLog.UpdateStatus(ctx, log.Id, consts.CheckStatusFailed, "", err.Error())
|
||||
g.Log().Errorf(ctx, "视频送检失败, id=%d, url=%s, error=%v", video.Id, video.PreviewURL, err)
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新日志和视频状态
|
||||
responseData, _ := json.Marshal(result)
|
||||
dao.TencentContentCheckLog.UpdateStatus(ctx, log.Id, consts.CheckStatusSuccess, string(responseData), "")
|
||||
dao.TencentContentCheckLog.UpdateTaskID(ctx, log.Id, result.TaskID)
|
||||
dao.TencentContentCheckLog.UpdateDuration(ctx, log.Id, duration)
|
||||
}
|
||||
|
||||
g.Log().Infof(ctx, "视频送检成功, id=%d, videoId=%s, taskId=%s", video.Id, video.VideoID, result.TaskID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SubmitImageByID 根据图片ID手动提交送检
|
||||
func (s *TencentContentCheckService) SubmitImageByID(ctx context.Context, imageID string) (*yidunService.ImageSubmitResult, error) {
|
||||
// 根据图片ID获取数据
|
||||
image, err := dao.TencentImage.GetByImageID(ctx, imageID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询图片数据失败: %w", err)
|
||||
}
|
||||
if image == nil {
|
||||
return nil, fmt.Errorf("未找到图片数据, imageID=%s", imageID)
|
||||
}
|
||||
|
||||
// 创建送检日志
|
||||
log := s.createCheckLog(ctx, consts.SourceTableTencentImage, image.Id, image.ImageID, image.PreviewURL)
|
||||
if log == nil {
|
||||
return nil, fmt.Errorf("创建送检日志失败")
|
||||
}
|
||||
|
||||
// 提交送检
|
||||
err = s.submitImageCheck(ctx, image, log)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取送检结果
|
||||
return dao.TencentContentCheckLog.GetImageSubmitResult(ctx, log.Id)
|
||||
}
|
||||
|
||||
// SubmitVideoByID 根据视频ID手动提交送检
|
||||
func (s *TencentContentCheckService) SubmitVideoByID(ctx context.Context, videoID string) (*yidunService.VideoSubmitResult, error) {
|
||||
// 根据视频ID获取数据
|
||||
video, err := dao.TencentVideo.GetByVideoID(ctx, videoID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询视频数据失败: %w", err)
|
||||
}
|
||||
if video == nil {
|
||||
return nil, fmt.Errorf("未找到视频数据, videoID=%s", videoID)
|
||||
}
|
||||
|
||||
// 创建送检日志
|
||||
log := s.createCheckLog(ctx, consts.SourceTableTencentVideo, video.Id, video.VideoID, video.PreviewURL)
|
||||
if log == nil {
|
||||
return nil, fmt.Errorf("创建送检日志失败")
|
||||
}
|
||||
|
||||
// 提交送检
|
||||
err = s.submitVideoCheck(ctx, video, log)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取送检结果
|
||||
return dao.TencentContentCheckLog.GetVideoSubmitResult(ctx, log.Id)
|
||||
}
|
||||
|
||||
// GetPendingStats 获取待送检统计
|
||||
func (s *TencentContentCheckService) GetPendingStats(ctx context.Context) map[string]int {
|
||||
stats := make(map[string]int)
|
||||
|
||||
if s.config.ImageEnabled {
|
||||
count, _ := dao.TencentImage.CountPending(ctx)
|
||||
stats["image_pending"] = count
|
||||
}
|
||||
|
||||
if s.config.VideoEnabled {
|
||||
count, _ := dao.TencentVideo.CountPending(ctx)
|
||||
stats["video_pending"] = count
|
||||
}
|
||||
|
||||
return stats
|
||||
}
|
||||
|
||||
// IsRunning 获取运行状态
|
||||
func (s *TencentContentCheckService) IsRunning() bool {
|
||||
return s.isRunning
|
||||
}
|
||||
|
||||
// GetConfig 获取当前配置
|
||||
func (s *TencentContentCheckService) GetConfig() ContentCheckConfig {
|
||||
return s.config
|
||||
}
|
||||
@@ -92,6 +92,79 @@ func (s *ImageDetectionService) DetectImage(ctx context.Context, imageURL, dataI
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// DetectImageSync 同步检测图片,提交并直接返回检测结果
|
||||
// 适用于轮询模式(无回调),提交时实时返回结果,无需额外查询
|
||||
func (s *ImageDetectionService) DetectImageSync(ctx context.Context, imageURL, dataID string) (*ImageResult, error) {
|
||||
if DefaultClients == nil || DefaultClients.ImageClient == nil {
|
||||
return nil, fmt.Errorf("易盾图片检测客户端未初始化")
|
||||
}
|
||||
|
||||
if imageURL == "" {
|
||||
return nil, fmt.Errorf("图片URL不能为空")
|
||||
}
|
||||
|
||||
businessId := g.Cfg().MustGet(ctx, "yidun.image.business_id").String()
|
||||
g.Log().Infof(ctx, "图片同步检测, url: %s, business_id: %s", imageURL, businessId)
|
||||
|
||||
// 创建同步检测请求
|
||||
request := check.NewImageV5CheckRequest(businessId)
|
||||
imageBean := check.NewImageBeanRequest()
|
||||
imageBean.SetData(imageURL)
|
||||
imageBean.SetName(dataID)
|
||||
imageBean.SetType(1) // 1: 图片URL
|
||||
request.SetImages([]check.ImageBeanRequest{*imageBean})
|
||||
|
||||
// 调用同步检测API
|
||||
response, err := DefaultClients.ImageClient.ImageSyncCheck(request)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "图片同步检测失败: %v", err)
|
||||
return nil, fmt.Errorf("图片同步检测失败: %w", err)
|
||||
}
|
||||
|
||||
if response.GetCode() != 200 {
|
||||
g.Log().Errorf(ctx, "图片同步检测API错误: code=%d, msg=%s", response.GetCode(), response.GetMsg())
|
||||
return nil, fmt.Errorf("图片同步检测API错误: code=%d, msg=%s", response.GetCode(), response.GetMsg())
|
||||
}
|
||||
|
||||
if response.Result == nil || len(*response.Result) == 0 {
|
||||
g.Log().Warningf(ctx, "图片同步检测结果为空, url: %s", imageURL)
|
||||
return nil, ErrImageResultNotFound
|
||||
}
|
||||
|
||||
// 提取结果
|
||||
detail := (*response.Result)[0]
|
||||
result := &ImageResult{
|
||||
TaskID: dataID,
|
||||
Name: dataID,
|
||||
Url: imageURL,
|
||||
}
|
||||
if detail.Antispam != nil {
|
||||
if detail.Antispam.TaskId != nil {
|
||||
result.TaskID = *detail.Antispam.TaskId
|
||||
}
|
||||
if detail.Antispam.Status != nil {
|
||||
result.Status = *detail.Antispam.Status
|
||||
}
|
||||
if detail.Antispam.Suggestion != nil {
|
||||
result.Suggestion = *detail.Antispam.Suggestion
|
||||
}
|
||||
if detail.Antispam.Label != nil {
|
||||
result.Label = *detail.Antispam.Label
|
||||
}
|
||||
if detail.Antispam.ResultType != nil {
|
||||
result.ResultType = *detail.Antispam.ResultType
|
||||
}
|
||||
if detail.Antispam.CensorTime != nil {
|
||||
result.CensorTime = *detail.Antispam.CensorTime
|
||||
}
|
||||
result.Antispam = detail.Antispam
|
||||
}
|
||||
|
||||
g.Log().Infof(ctx, "图片同步检测完成, taskID: %s, suggestion: %d, status: %d",
|
||||
result.TaskID, result.Suggestion, result.Status)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ImageResult 图片检测完整结果
|
||||
type ImageResult struct {
|
||||
TaskID string `json:"taskId"` // 任务ID
|
||||
@@ -144,7 +217,7 @@ func (s *ImageDetectionService) GetImageResult(ctx context.Context, taskID strin
|
||||
|
||||
if response.Result == nil || len(*response.Result) == 0 {
|
||||
g.Log().Warningf(ctx, "未找到图片检测结果, taskID: %s", taskID)
|
||||
return nil, ErrImageStillProcessing
|
||||
return nil, ErrImageResultNotFound
|
||||
}
|
||||
|
||||
// 查找指定taskID的结果
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
audiocallback "github.com/yidun/yidun-golang-sdk/yidun/service/antispam/audio/callback/v4/response"
|
||||
@@ -13,7 +12,6 @@ import (
|
||||
callbackrequest "github.com/yidun/yidun-golang-sdk/yidun/service/antispam/videosolution/callback/v2/request"
|
||||
callbackresponse "github.com/yidun/yidun-golang-sdk/yidun/service/antispam/videosolution/callback/v2/response"
|
||||
vsrequest "github.com/yidun/yidun-golang-sdk/yidun/service/antispam/videosolution/submit/v2/request"
|
||||
submitresponse "github.com/yidun/yidun-golang-sdk/yidun/service/antispam/videosolution/submit/v2/response"
|
||||
)
|
||||
|
||||
// VideoDetectionService 视频检测服务
|
||||
@@ -43,27 +41,47 @@ func (s *VideoDetectionService) DetectVideo(ctx context.Context, videoURL, dataI
|
||||
return nil, fmt.Errorf("视频URL不能为空")
|
||||
}
|
||||
|
||||
g.Log().Infof(ctx, "视频检测任务提交, url: %s", videoURL)
|
||||
g.Log().Infof(ctx, "视频检测任务提交, url: %s, dataID: %s", videoURL, dataID)
|
||||
|
||||
// 创建请求
|
||||
req := vsrequest.NewVideoSolutionSubmitV2Req()
|
||||
req.SetURL(videoURL)
|
||||
req.SetDataID(dataID)
|
||||
req.SetUniqueKey(dataID)
|
||||
|
||||
// 设置回调地址
|
||||
if callbackURL != "" {
|
||||
req.SetCallbackURL(callbackURL)
|
||||
}
|
||||
|
||||
// 设置子产品标识(视频解决方案必须)
|
||||
req.SetSubProduct("videoStream")
|
||||
|
||||
// 可选:设置IP(用于风险用户识别)
|
||||
// req.SetIP("127.0.0.1")
|
||||
|
||||
// 调用API
|
||||
response, err := DefaultClients.VideoClient.Submit(req)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "视频检测提交失败: %v", err)
|
||||
return nil, fmt.Errorf("视频检测提交失败: %w", err)
|
||||
g.Log().Errorf(ctx, "视频检测提交HTTP错误: %v", err)
|
||||
return nil, fmt.Errorf("视频检测提交HTTP错误: %w", err)
|
||||
}
|
||||
|
||||
if response.GetCode() != 200 {
|
||||
g.Log().Errorf(ctx, "视频检测API错误: code=%d, msg=%s", response.GetCode(), response.GetMsg())
|
||||
return nil, fmt.Errorf("视频检测API错误: code=%d, msg=%s", response.GetCode(), response.GetMsg())
|
||||
// 根据错误码提供更详细的错误信息
|
||||
errMsg := fmt.Sprintf("视频检测API错误: code=%d, msg=%s", response.GetCode(), response.GetMsg())
|
||||
|
||||
// 常见错误码说明
|
||||
switch response.GetCode() {
|
||||
case 417:
|
||||
errMsg += " (可能原因: 视频URL无法访问或业务配置问题)"
|
||||
case 400:
|
||||
errMsg += " (可能原因: 请求参数错误)"
|
||||
case 403:
|
||||
errMsg += " (可能原因: 鉴权失败,检查secretId和secretKey)"
|
||||
}
|
||||
return nil, fmt.Errorf(errMsg)
|
||||
}
|
||||
|
||||
result := &VideoSubmitResult{}
|
||||
@@ -132,7 +150,7 @@ func (s *VideoDetectionService) GetVideoResult(ctx context.Context, taskID strin
|
||||
|
||||
if response.Result == nil || len(*response.Result) == 0 {
|
||||
g.Log().Warningf(ctx, "未找到视频检测结果, taskID: %s", taskID)
|
||||
return nil, ErrVideoStillProcessing
|
||||
return nil, ErrVideoResultNotFound
|
||||
}
|
||||
|
||||
// 查找指定taskID的结果
|
||||
@@ -182,82 +200,6 @@ func (s *VideoDetectionService) GetVideoResult(ctx context.Context, taskID strin
|
||||
return nil, ErrVideoResultNotFound
|
||||
}
|
||||
|
||||
// PollVideoResult 轮询获取视频检测结果
|
||||
func (s *VideoDetectionService) PollVideoResult(ctx context.Context, taskID string, interval, maxWait time.Duration) (*VideoResult, error) {
|
||||
if interval <= 0 {
|
||||
interval = 1 * time.Second
|
||||
}
|
||||
if maxWait <= 0 {
|
||||
maxWait = 5 * time.Minute
|
||||
}
|
||||
|
||||
startTime := time.Now()
|
||||
g.Log().Infof(ctx, "开始轮询视频检测结果, taskID: %s, interval: %v, maxWait: %v", taskID, interval, maxWait)
|
||||
|
||||
for time.Since(startTime) < maxWait {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, fmt.Errorf("轮询取消: %w", ctx.Err())
|
||||
default:
|
||||
}
|
||||
|
||||
result, err := s.GetVideoResult(ctx, taskID)
|
||||
if err == nil {
|
||||
g.Log().Infof(ctx, "轮询成功, taskID: %s, elapsed: %v", taskID, time.Since(startTime))
|
||||
return result, nil
|
||||
}
|
||||
|
||||
if errors.Is(err, ErrVideoResultNotFound) {
|
||||
g.Log().Infof(ctx, "视频仍在检测中,继续轮询, taskID: %s, elapsed: %v", taskID, time.Since(startTime))
|
||||
} else {
|
||||
g.Log().Warningf(ctx, "查询出错,继续重试, taskID: %s, error: %v", taskID, err)
|
||||
}
|
||||
|
||||
time.Sleep(interval)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("轮询超时,已等待 %v", time.Since(startTime))
|
||||
}
|
||||
|
||||
// GetSDKSubmitResponse 获取SDK原始提交响应(包含完整易盾返回信息)
|
||||
func (s *VideoDetectionService) GetSDKSubmitResponse(ctx context.Context, videoURL, dataID string, callbackURL string) (*submitresponse.VideoSolutionSubmitV2Response, error) {
|
||||
if DefaultClients == nil || DefaultClients.VideoClient == nil {
|
||||
return nil, fmt.Errorf("易盾视频检测客户端未初始化")
|
||||
}
|
||||
|
||||
req := vsrequest.NewVideoSolutionSubmitV2Req()
|
||||
req.SetURL(videoURL)
|
||||
req.SetDataID(dataID)
|
||||
req.SetUniqueKey(dataID)
|
||||
if callbackURL != "" {
|
||||
req.SetCallbackURL(callbackURL)
|
||||
}
|
||||
|
||||
response, err := DefaultClients.VideoClient.Submit(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// GetSDKCallbackResponse 获取SDK原始回调响应(包含完整易盾返回信息)
|
||||
func (s *VideoDetectionService) GetSDKCallbackResponse(ctx context.Context, taskID string) (*callbackresponse.VideoSolutionCallbackV2Response, error) {
|
||||
if DefaultClients == nil || DefaultClients.VideoClient == nil {
|
||||
return nil, fmt.Errorf("易盾视频检测客户端未初始化")
|
||||
}
|
||||
|
||||
req := callbackrequest.NewVideoSolutionCallbackV2Request()
|
||||
req.SetYidunRequestId(taskID)
|
||||
|
||||
response, err := DefaultClients.VideoClient.Callback(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 视频检测结果推送模式(易盾主动回调)
|
||||
// =============================================================================
|
||||
|
||||
40
sql/material_verify_log.sql
Normal file
40
sql/material_verify_log.sql
Normal file
@@ -0,0 +1,40 @@
|
||||
-- =============================================
|
||||
-- 素材校验日志表 (material_verify_log) - cid数据库
|
||||
-- =============================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS material_verify_log
|
||||
(
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id BIGINT DEFAULT 0,
|
||||
material_type VARCHAR(20) NOT NULL,
|
||||
material_id VARCHAR(100) NOT NULL,
|
||||
source_table VARCHAR(50) NOT NULL,
|
||||
source_id BIGINT NOT NULL,
|
||||
account_id BIGINT NOT NULL,
|
||||
task_id VARCHAR(100),
|
||||
request_params TEXT,
|
||||
response_result TEXT,
|
||||
verify_status VARCHAR(20) DEFAULT 'PENDING' NOT NULL,
|
||||
suggestion INT DEFAULT -1,
|
||||
label INT DEFAULT -1,
|
||||
result_type INT DEFAULT -1,
|
||||
error_msg TEXT,
|
||||
check_time BIGINT,
|
||||
duration_ms BIGINT DEFAULT 0,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by VARCHAR(100) DEFAULT 'system'
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_material_verify_log_material_id ON material_verify_log(material_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_material_verify_log_source ON material_verify_log(source_table, source_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_material_verify_log_status ON material_verify_log(verify_status);
|
||||
CREATE INDEX IF NOT EXISTS idx_material_verify_log_account ON material_verify_log(account_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_material_verify_log_task ON material_verify_log(task_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_material_verify_log_created ON material_verify_log(created_at);
|
||||
|
||||
COMMENT ON TABLE material_verify_log IS '素材校验日志表';
|
||||
COMMENT ON COLUMN material_verify_log.material_type IS '素材类型 IMAGE/VIDEO';
|
||||
COMMENT ON COLUMN material_verify_log.material_id IS '素材ID(image_id/video_id)';
|
||||
COMMENT ON COLUMN material_verify_log.verify_status IS '校验状态: PENDING=待校验, VERIFIED=通过, REJECTED=不通过';
|
||||
COMMENT ON COLUMN material_verify_log.suggestion IS '易盾处置建议: 0=通过, 1=嫌疑, 2=不通过';
|
||||
COMMENT ON COLUMN material_verify_log.error_msg IS '错误信息';
|
||||
44
sql/tencent_content_check_log.sql
Normal file
44
sql/tencent_content_check_log.sql
Normal file
@@ -0,0 +1,44 @@
|
||||
-- =============================================
|
||||
-- 送检日志表 (tencent_content_check_log) - cid数据库
|
||||
-- =============================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "tencent_content_check_log" (
|
||||
"id" BIGSERIAL PRIMARY KEY,
|
||||
"source_table" VARCHAR(64) NOT NULL DEFAULT '',
|
||||
"source_id" BIGINT NOT NULL DEFAULT 0,
|
||||
"request_url" VARCHAR(512) NOT NULL DEFAULT '',
|
||||
"request_param" TEXT NOT NULL DEFAULT '',
|
||||
"response_data" TEXT NOT NULL DEFAULT '',
|
||||
"status" VARCHAR(32) NOT NULL DEFAULT 'pending',
|
||||
"check_time" BIGINT NOT NULL DEFAULT 0,
|
||||
"fail_reason" TEXT NOT NULL DEFAULT '',
|
||||
"task_id" VARCHAR(128) NOT NULL DEFAULT '',
|
||||
"suggestion" INTEGER NOT NULL DEFAULT 0,
|
||||
"label" INTEGER NOT NULL DEFAULT 0,
|
||||
"result_type" INTEGER NOT NULL DEFAULT 0,
|
||||
"duration" BIGINT NOT NULL DEFAULT 0,
|
||||
"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"deleted_at" TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "idx_content_check_log_source" ON "tencent_content_check_log" ("source_table", "source_id");
|
||||
CREATE INDEX IF NOT EXISTS "idx_content_check_log_task_id" ON "tencent_content_check_log" ("task_id");
|
||||
CREATE INDEX IF NOT EXISTS "idx_content_check_log_status" ON "tencent_content_check_log" ("status");
|
||||
CREATE INDEX IF NOT EXISTS "idx_content_check_log_check_time" ON "tencent_content_check_log" ("check_time");
|
||||
CREATE INDEX IF NOT EXISTS "idx_content_check_log_created_at" ON "tencent_content_check_log" ("created_at");
|
||||
|
||||
COMMENT ON TABLE "tencent_content_check_log" IS '送检日志表';
|
||||
COMMENT ON COLUMN "tencent_content_check_log"."source_table" IS '来源表标识:tencent_image/tencent_video';
|
||||
COMMENT ON COLUMN "tencent_content_check_log"."source_id" IS '原数据ID(关联业务表数据)';
|
||||
COMMENT ON COLUMN "tencent_content_check_log"."request_url" IS '送检请求路径(接口地址)';
|
||||
COMMENT ON COLUMN "tencent_content_check_log"."request_param" IS '送检入参(完整请求参数,JSON格式)';
|
||||
COMMENT ON COLUMN "tencent_content_check_log"."response_data" IS '送检出参(完整接口返回结果,JSON格式)';
|
||||
COMMENT ON COLUMN "tencent_content_check_log"."status" IS '送检状态:pending-待送检, submitting-送检中, success-送检成功, failed-送检失败, completed-检测完成';
|
||||
COMMENT ON COLUMN "tencent_content_check_log"."check_time" IS '送检时间(时间戳,毫秒)';
|
||||
COMMENT ON COLUMN "tencent_content_check_log"."fail_reason" IS '失败原因(可选,记录接口报错信息)';
|
||||
COMMENT ON COLUMN "tencent_content_check_log"."task_id" IS '易盾返回的任务ID';
|
||||
COMMENT ON COLUMN "tencent_content_check_log"."suggestion" IS '检测结果建议:0-通过,1-嫌疑,2-不通过';
|
||||
COMMENT ON COLUMN "tencent_content_check_log"."label" IS '检测标签';
|
||||
COMMENT ON COLUMN "tencent_content_check_log"."result_type" IS '结果类型:1-机器结果,2-人审结果';
|
||||
COMMENT ON COLUMN "tencent_content_check_log"."duration" IS '送检耗时(毫秒)';
|
||||
Reference in New Issue
Block a user