yidun送检新增账户下拉和导出优化
This commit is contained in:
@@ -5,4 +5,5 @@ const (
|
||||
TencentImageTable = "tencent_image" // 图片送检表
|
||||
TencentVideoTable = "tencent_video" // 视频送检表
|
||||
TencentContentCheckLogTable = "tencent_content_check_log" // 送检日志表
|
||||
TencentAccountRelationTable = "tencent_account_relation" // 腾讯广告账户关系表
|
||||
)
|
||||
|
||||
@@ -436,6 +436,103 @@ func (c *MaterialVerifyController) BatchVerifyVideo(ctx context.Context, req *Ba
|
||||
}, nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 账户列表接口
|
||||
// =============================================================================
|
||||
|
||||
// ListAccountsReq 账户列表请求
|
||||
type ListAccountsReq struct{}
|
||||
|
||||
// AccountItem 账户列表项
|
||||
type AccountItem struct {
|
||||
AccountID int64 `json:"accountId"`
|
||||
CorporationName string `json:"corporationName"`
|
||||
}
|
||||
|
||||
// ListAccountsRes 账户列表响应
|
||||
type ListAccountsRes struct {
|
||||
List []AccountItem `json:"list"`
|
||||
}
|
||||
|
||||
// ListAccounts 获取所有启用的广告账户列表(用于前端下拉筛选)
|
||||
func (c *MaterialVerifyController) ListAccounts(ctx context.Context, req *ListAccountsReq) (res *ListAccountsRes, err error) {
|
||||
accounts, err := dao.TencentAccountRelation.GetAll(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var items []AccountItem
|
||||
for _, acc := range accounts {
|
||||
items = append(items, AccountItem{
|
||||
AccountID: acc.AccountID,
|
||||
CorporationName: acc.CorporationName,
|
||||
})
|
||||
}
|
||||
|
||||
return &ListAccountsRes{
|
||||
List: items,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 导出接口 - 不通过数据
|
||||
// =============================================================================
|
||||
|
||||
// ExportRejectedReq 导出不通过数据请求
|
||||
type ExportRejectedReq struct {
|
||||
MaterialType string `json:"materialType"` // IMAGE/VIDEO,为空则导出全部
|
||||
}
|
||||
|
||||
// ExportRejectedItem 导出的不通过数据项
|
||||
type ExportRejectedItem struct {
|
||||
ID int64 `json:"id"`
|
||||
MaterialID string `json:"materialId"`
|
||||
AccountID int64 `json:"accountId"`
|
||||
CorporationName string `json:"corporationName"`
|
||||
PreviewURL string `json:"previewUrl"`
|
||||
Description string `json:"description"`
|
||||
ErrorMsg string `json:"errorMsg"`
|
||||
MaterialType string `json:"materialType"`
|
||||
ImageUsage string `json:"imageUsage,omitempty"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
}
|
||||
|
||||
// ExportRejectedRes 导出不通过数据响应
|
||||
type ExportRejectedRes struct {
|
||||
Items []ExportRejectedItem `json:"items"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
// ExportRejected 导出不通过的图片/视频数据(含失败原因)
|
||||
func (c *MaterialVerifyController) ExportRejected(ctx context.Context, req *ExportRejectedReq) (res *ExportRejectedRes, err error) {
|
||||
items, err := serviceDataengine.MaterialVerify.ExportRejectedData(ctx, req.MaterialType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 转换为响应结构
|
||||
var respItems []ExportRejectedItem
|
||||
for _, item := range items {
|
||||
respItems = append(respItems, ExportRejectedItem{
|
||||
ID: item.ID,
|
||||
MaterialID: item.MaterialID,
|
||||
AccountID: item.AccountID,
|
||||
CorporationName: item.CorporationName,
|
||||
PreviewURL: item.PreviewURL,
|
||||
Description: item.Description,
|
||||
ErrorMsg: item.ErrorMsg,
|
||||
MaterialType: item.MaterialType,
|
||||
ImageUsage: item.ImageUsage,
|
||||
CreatedAt: item.CreatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
return &ExportRejectedRes{
|
||||
Items: respItems,
|
||||
Total: len(respItems),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 回调处理接口
|
||||
// =============================================================================
|
||||
|
||||
@@ -273,6 +273,26 @@ func (d *MaterialVerifyLogDAO) GetPendingResults(ctx context.Context, limit int)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetLastRejectedLogByMaterialID 根据素材ID获取最后一条失败的校验日志
|
||||
func (d *MaterialVerifyLogDAO) GetLastRejectedLogByMaterialID(ctx context.Context, materialID string, verifyStatus string) (*daoEntity.MaterialVerifyLog, error) {
|
||||
var result daoEntity.MaterialVerifyLog
|
||||
r, err := g.DB("default").Model(MaterialVerifyLogTable).
|
||||
Where(daoEntity.MaterialVerifyLogCols.MaterialID, materialID).
|
||||
Where(daoEntity.MaterialVerifyLogCols.VerifyStatus, verifyStatus).
|
||||
OrderDesc(daoEntity.MaterialVerifyLogCols.CreatedAt).
|
||||
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
|
||||
}
|
||||
|
||||
// CountPendingResults 统计待查询结果的数量
|
||||
func (d *MaterialVerifyLogDAO) CountPendingResults(ctx context.Context) (int, error) {
|
||||
count, err := g.DB("default").Model(MaterialVerifyLogTable).
|
||||
|
||||
33
dao/dataengine/tencent_account_relation_dao.go
Normal file
33
dao/dataengine/tencent_account_relation_dao.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package dataengine
|
||||
|
||||
import (
|
||||
consts "cid/consts/dataengine"
|
||||
entity "cid/model/entity/dataengine"
|
||||
"context"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
// TencentAccountRelationDAO 腾讯广告账户关系数据访问层
|
||||
type TencentAccountRelationDAO struct{}
|
||||
|
||||
// TencentAccountRelation DAO单例
|
||||
var TencentAccountRelation = new(TencentAccountRelationDAO)
|
||||
|
||||
// GetAll 获取所有启用的账户列表
|
||||
func (d *TencentAccountRelationDAO) GetAll(ctx context.Context) ([]entity.TencentAccountRelation, error) {
|
||||
var result []entity.TencentAccountRelation
|
||||
r, err := Model(consts.TencentAccountRelationTable).
|
||||
WhereNull("deleted_at").
|
||||
OrderAsc(entity.TencentAccountRelationCols.AccountID).
|
||||
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
|
||||
}
|
||||
27
model/entity/dataengine/tencent_account_relation.go
Normal file
27
model/entity/dataengine/tencent_account_relation.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package dataengine
|
||||
|
||||
import (
|
||||
"gitea.com/red-future/common/beans"
|
||||
)
|
||||
|
||||
// TencentAccountRelation 腾讯广告账户关系实体(来源:data-engine.tencent_account_relation)
|
||||
type TencentAccountRelation struct {
|
||||
beans.SQLBaseDO `orm:",inherit"`
|
||||
// 业务字段
|
||||
AccountID int64 `orm:"account_id" json:"accountId" description:"账户ID"`
|
||||
CorporationName string `orm:"corporation_name" json:"corporationName" description:"公司名称"`
|
||||
}
|
||||
|
||||
// TencentAccountRelationCol 账户关系表字段定义
|
||||
type TencentAccountRelationCol struct {
|
||||
beans.SQLBaseCol
|
||||
AccountID string
|
||||
CorporationName string
|
||||
}
|
||||
|
||||
// TencentAccountRelationCols 账户关系表字段常量
|
||||
var TencentAccountRelationCols = TencentAccountRelationCol{
|
||||
SQLBaseCol: beans.DefSQLBaseCol,
|
||||
AccountID: "account_id",
|
||||
CorporationName: "corporation_name",
|
||||
}
|
||||
@@ -255,8 +255,15 @@
|
||||
</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>
|
||||
<label>账户:</label>
|
||||
<el-select v-model="imageFilters.accountId" placeholder="全部账户" clearable filterable style="width: 200px;" @change="searchImage">
|
||||
<el-option
|
||||
v-for="item in accounts"
|
||||
:key="item.accountId"
|
||||
:label="item.accountId + ' - ' + item.corporationName"
|
||||
:value="item.accountId">
|
||||
</el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
<el-button type="primary" @click="searchImage">搜索</el-button>
|
||||
<el-button @click="resetImageFilter">重置</el-button>
|
||||
@@ -334,8 +341,15 @@
|
||||
</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>
|
||||
<label>账户:</label>
|
||||
<el-select v-model="videoFilters.accountId" placeholder="全部账户" clearable filterable style="width: 200px;" @change="searchVideo">
|
||||
<el-option
|
||||
v-for="item in accounts"
|
||||
:key="item.accountId"
|
||||
:label="item.accountId + ' - ' + item.corporationName"
|
||||
:value="item.accountId">
|
||||
</el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
<el-button type="primary" @click="searchVideo">搜索</el-button>
|
||||
<el-button @click="resetVideoFilter">重置</el-button>
|
||||
@@ -601,11 +615,14 @@
|
||||
previewUrl: '',
|
||||
previewType: 'image',
|
||||
materialLogs: [],
|
||||
batchLoading: false
|
||||
batchLoading: false,
|
||||
// 账户列表(下拉筛选用)
|
||||
accounts: []
|
||||
},
|
||||
mounted() {
|
||||
this.loadStats();
|
||||
this.loadImageList();
|
||||
this.loadAccounts();
|
||||
},
|
||||
methods: {
|
||||
// API 请求
|
||||
@@ -639,6 +656,21 @@
|
||||
});
|
||||
},
|
||||
|
||||
// 加载账户列表(下拉筛选用)
|
||||
loadAccounts() {
|
||||
this.apiPost('/material/verify/controller/list-accounts', {}).then(res => {
|
||||
this.accounts = res.data.data?.list || [];
|
||||
}).catch(err => {
|
||||
console.error('加载账户列表失败', err);
|
||||
});
|
||||
},
|
||||
|
||||
// 获取账户名称(显示用)
|
||||
getAccountName(accountId) {
|
||||
const account = this.accounts.find(a => a.accountId === accountId);
|
||||
return account ? account.corporationName : '';
|
||||
},
|
||||
|
||||
// 图片列表
|
||||
loadImageList() {
|
||||
this.imageLoading = true;
|
||||
@@ -813,36 +845,50 @@
|
||||
});
|
||||
},
|
||||
|
||||
// 导出
|
||||
// 导出(不通过数据,含失败原因)
|
||||
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.apiPost('/material/verify/controller/export-rejected', { materialType: 'IMAGE' }).then(res => {
|
||||
const items = res.data.data?.items || [];
|
||||
if (!items.length) {
|
||||
this.$message.warning('没有不通过的图片数据');
|
||||
return;
|
||||
}
|
||||
const rows = [['序号', '账户(名称)', '预览URL', '描述', '失败原因', '检测时间']];
|
||||
items.forEach((item, index) => {
|
||||
const accountLabel = item.corporationName ? item.accountId + ' - ' + item.corporationName : item.accountId;
|
||||
rows.push([
|
||||
index + 1, accountLabel,
|
||||
item.previewUrl || '-', item.description || '-',
|
||||
item.errorMsg || '-', item.createdAt || '-'
|
||||
]);
|
||||
});
|
||||
this.downloadCsv('图片不通过数据.csv', rows);
|
||||
this.$message.success('导出成功,共 ' + items.length + ' 条');
|
||||
}).catch(err => {
|
||||
this.$message.error('导出失败: ' + (err.response?.data?.msg || err.message));
|
||||
});
|
||||
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.apiPost('/material/verify/controller/export-rejected', { materialType: 'VIDEO' }).then(res => {
|
||||
const items = res.data.data?.items || [];
|
||||
if (!items.length) {
|
||||
this.$message.warning('没有不通过的视频数据');
|
||||
return;
|
||||
}
|
||||
const rows = [['序号', '账户(名称)', '预览URL', '描述', '失败原因', '检测时间']];
|
||||
items.forEach((item, index) => {
|
||||
const accountLabel = item.corporationName ? item.accountId + ' - '+ item.corporationName : item.accountId;
|
||||
rows.push([
|
||||
index + 1, accountLabel,
|
||||
item.previewUrl || '-', item.description || '-',
|
||||
item.errorMsg || '-', item.createdAt || '-'
|
||||
]);
|
||||
});
|
||||
this.downloadCsv('视频不通过数据.csv', rows);
|
||||
this.$message.success('导出成功,共 ' + items.length + ' 条');
|
||||
}).catch(err => {
|
||||
this.$message.error('导出失败: ' + (err.response?.data?.msg || err.message));
|
||||
});
|
||||
this.downloadCsv('视频列表.csv', rows);
|
||||
},
|
||||
downloadCsv(filename, rows) {
|
||||
const csv = rows.map(r => r.map(v => `"${String(v).replace(/"/g, '""')}"`).join(',')).join('\n');
|
||||
|
||||
@@ -656,6 +656,140 @@ func (s *MaterialVerifyService) PollPendingVideoResults(ctx context.Context) (in
|
||||
return s.PollPendingResultsByType(ctx, consts.SourceTableTencentVideo)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 导出服务 - 不通过数据导出
|
||||
// =============================================================================
|
||||
|
||||
// ExportRejectedItem 导出的不通过数据项
|
||||
type ExportRejectedItem struct {
|
||||
ID int64 `json:"id"` // 素材表主键ID
|
||||
MaterialID string `json:"materialId"` // 素材ID(imageId/videoId)
|
||||
AccountID int64 `json:"accountId"` // 账户ID
|
||||
CorporationName string `json:"corporationName"` // 公司名称
|
||||
PreviewURL string `json:"previewUrl"` // 预览URL
|
||||
Description string `json:"description"` // 描述
|
||||
ErrorMsg string `json:"errorMsg"` // 失败原因(最后一条失败日志的error_msg)
|
||||
MaterialType string `json:"materialType"` // 素材类型 IMAGE/VIDEO
|
||||
ImageUsage string `json:"imageUsage"` // 图片用途(仅图片)
|
||||
CreatedAt string `json:"createdAt"` // 检测时间(日志创建时间)
|
||||
}
|
||||
|
||||
// getFailureReason 获取失败原因
|
||||
func getFailureReason(log *entity.MaterialVerifyLog) string {
|
||||
if log == nil {
|
||||
return "无校验日志"
|
||||
}
|
||||
// 优先使用 error_msg
|
||||
if log.ErrorMsg != "" {
|
||||
return log.ErrorMsg
|
||||
}
|
||||
// 根据 suggestion 和 label 生成原因
|
||||
reasonMap := map[int]string{
|
||||
0: "内容检测通过",
|
||||
1: "内容嫌疑(需人工审核)",
|
||||
2: "内容不通过",
|
||||
}
|
||||
suggestionText := reasonMap[log.Suggestion]
|
||||
if suggestionText == "" {
|
||||
suggestionText = fmt.Sprintf("未知(suggestion=%d)", log.Suggestion)
|
||||
}
|
||||
// 如果有 response_result,尝试提取更多信息
|
||||
if log.ResponseResult != "" {
|
||||
var resultMap map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(log.ResponseResult), &resultMap); err == nil {
|
||||
if labels, ok := resultMap["labels"]; ok {
|
||||
return fmt.Sprintf("%s (labels: %v)", suggestionText, labels)
|
||||
}
|
||||
}
|
||||
return suggestionText
|
||||
}
|
||||
return suggestionText
|
||||
}
|
||||
|
||||
// ExportRejectedData 导出不通过数据
|
||||
func (s *MaterialVerifyService) ExportRejectedData(ctx context.Context, materialType string) ([]ExportRejectedItem, error) {
|
||||
var items []ExportRejectedItem
|
||||
|
||||
// 加载账户名称映射
|
||||
accountMap := make(map[int64]string)
|
||||
if accounts, err := dao.TencentAccountRelation.GetAll(ctx); err == nil {
|
||||
for _, acc := range accounts {
|
||||
if acc.CorporationName != "" {
|
||||
accountMap[acc.AccountID] = acc.CorporationName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理图片
|
||||
if materialType == "" || materialType == entity.MaterialTypeImage {
|
||||
condition := map[string]interface{}{
|
||||
entity.TencentImageCols.VerifyStatus: entity.VerifyStatusRejected,
|
||||
}
|
||||
images, total, err := dao.TencentImage.GetByCondition(ctx, condition, 1, 100000)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "查询不通过图片失败: %v", err)
|
||||
return nil, fmt.Errorf("查询不通过图片失败: %w", err)
|
||||
}
|
||||
g.Log().Infof(ctx, "导出不通过图片: total=%d, got=%d", total, len(images))
|
||||
|
||||
for _, img := range images {
|
||||
// 查询最后一条失败的校验日志
|
||||
log, _ := dao.MaterialVerifyLog.GetLastRejectedLogByMaterialID(ctx, img.ImageID, entity.VerifyStatusRejected)
|
||||
var createdAtStr string
|
||||
if log != nil && log.CreatedAt != nil {
|
||||
createdAtStr = log.CreatedAt.Format("Y-m-d H:i:s")
|
||||
}
|
||||
items = append(items, ExportRejectedItem{
|
||||
ID: img.Id,
|
||||
MaterialID: img.ImageID,
|
||||
AccountID: img.AccountID,
|
||||
CorporationName: accountMap[img.AccountID],
|
||||
PreviewURL: img.PreviewURL,
|
||||
Description: img.Description,
|
||||
ErrorMsg: getFailureReason(log),
|
||||
MaterialType: entity.MaterialTypeImage,
|
||||
ImageUsage: img.ImageUsage,
|
||||
CreatedAt: createdAtStr,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 处理视频
|
||||
if materialType == "" || materialType == entity.MaterialTypeVideo {
|
||||
condition := map[string]interface{}{
|
||||
entity.TencentVideoCols.VerifyStatus: entity.VerifyStatusRejected,
|
||||
}
|
||||
videos, total, err := dao.TencentVideo.GetByCondition(ctx, condition, 1, 100000)
|
||||
if err != nil {
|
||||
g.Log().Errorf(ctx, "查询不通过视频失败: %v", err)
|
||||
return nil, fmt.Errorf("查询不通过视频失败: %w", err)
|
||||
}
|
||||
g.Log().Infof(ctx, "导出不通过视频: total=%d, got=%d", total, len(videos))
|
||||
|
||||
for _, vid := range videos {
|
||||
// 查询最后一条失败的校验日志
|
||||
log, _ := dao.MaterialVerifyLog.GetLastRejectedLogByMaterialID(ctx, vid.VideoID, entity.VerifyStatusRejected)
|
||||
var createdAtStr string
|
||||
if log != nil && log.CreatedAt != nil {
|
||||
createdAtStr = log.CreatedAt.Format("Y-m-d H:i:s")
|
||||
}
|
||||
items = append(items, ExportRejectedItem{
|
||||
ID: vid.Id,
|
||||
MaterialID: vid.VideoID,
|
||||
AccountID: vid.AccountID,
|
||||
CorporationName: accountMap[vid.AccountID],
|
||||
PreviewURL: vid.PreviewURL,
|
||||
Description: vid.Description,
|
||||
ErrorMsg: getFailureReason(log),
|
||||
MaterialType: entity.MaterialTypeVideo,
|
||||
CreatedAt: createdAtStr,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// GetPendingResultsCount 获取待查询结果的数量
|
||||
func (s *MaterialVerifyService) GetPendingResultsCount(ctx context.Context) (int, error) {
|
||||
return dao.MaterialVerifyLog.CountPendingResults(ctx)
|
||||
|
||||
Reference in New Issue
Block a user