Compare commits

..

No commits in common. "f10c36d9fa9b5a9f13e47aa196c5c6ae3acaad2f" and "151324b4138a3352835e42cf198cfd6388e34d3b" have entirely different histories.

12 changed files with 4717 additions and 10535 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
// Code generated by protoc-gen-go-triple. DO NOT EDIT.
// versions:
// - protoc-gen-go-triple v1.0.8
// - protoc v3.21.1
// - protoc v6.32.0--rc2
// source: pb/fiee/cast.proto
package cast
@ -52,8 +52,6 @@ type CastClient interface {
UpdateWorkPlatformInfo(ctx context.Context, in *UpdateWorkPlatformInfoReq, opts ...grpc_go.CallOption) (*UpdateWorkPlatformInfoResp, common.ErrorWithAttachment)
UpdateWorkPublishLog(ctx context.Context, in *UpdateWorkPublishLogReq, opts ...grpc_go.CallOption) (*emptypb.Empty, common.ErrorWithAttachment)
RefreshWorkList(ctx context.Context, in *RefreshWorkListReq, opts ...grpc_go.CallOption) (*RefreshWorkListResp, common.ErrorWithAttachment)
WorkResource(ctx context.Context, in *WorkResourceReq, opts ...grpc_go.CallOption) (*WorkResourceResp, common.ErrorWithAttachment)
UpdateWorkResource(ctx context.Context, in *UpdateWorkResourceReq, opts ...grpc_go.CallOption) (*UpdateWorkResourceResp, common.ErrorWithAttachment)
OAuthAccount(ctx context.Context, in *OAuthAccountReq, opts ...grpc_go.CallOption) (*OAuthAccountResp, common.ErrorWithAttachment)
OAuthAccountV2(ctx context.Context, in *OAuthAccountV2Req, opts ...grpc_go.CallOption) (*OAuthAccountV2Resp, common.ErrorWithAttachment)
OAuthCodeToToken(ctx context.Context, in *OAuthCodeToTokenReq, opts ...grpc_go.CallOption) (*OAuthCodeToTokenResp, common.ErrorWithAttachment)
@ -162,8 +160,6 @@ type CastClientImpl struct {
UpdateWorkPlatformInfo func(ctx context.Context, in *UpdateWorkPlatformInfoReq) (*UpdateWorkPlatformInfoResp, error)
UpdateWorkPublishLog func(ctx context.Context, in *UpdateWorkPublishLogReq) (*emptypb.Empty, error)
RefreshWorkList func(ctx context.Context, in *RefreshWorkListReq) (*RefreshWorkListResp, error)
WorkResource func(ctx context.Context, in *WorkResourceReq) (*WorkResourceResp, error)
UpdateWorkResource func(ctx context.Context, in *UpdateWorkResourceReq) (*UpdateWorkResourceResp, error)
OAuthAccount func(ctx context.Context, in *OAuthAccountReq) (*OAuthAccountResp, error)
OAuthAccountV2 func(ctx context.Context, in *OAuthAccountV2Req) (*OAuthAccountV2Resp, error)
OAuthCodeToToken func(ctx context.Context, in *OAuthCodeToTokenReq) (*OAuthCodeToTokenResp, error)
@ -379,18 +375,6 @@ func (c *castClient) RefreshWorkList(ctx context.Context, in *RefreshWorkListReq
return out, c.cc.Invoke(ctx, "/"+interfaceKey+"/RefreshWorkList", in, out)
}
func (c *castClient) WorkResource(ctx context.Context, in *WorkResourceReq, opts ...grpc_go.CallOption) (*WorkResourceResp, common.ErrorWithAttachment) {
out := new(WorkResourceResp)
interfaceKey := ctx.Value(constant.InterfaceKey).(string)
return out, c.cc.Invoke(ctx, "/"+interfaceKey+"/WorkResource", in, out)
}
func (c *castClient) UpdateWorkResource(ctx context.Context, in *UpdateWorkResourceReq, opts ...grpc_go.CallOption) (*UpdateWorkResourceResp, common.ErrorWithAttachment) {
out := new(UpdateWorkResourceResp)
interfaceKey := ctx.Value(constant.InterfaceKey).(string)
return out, c.cc.Invoke(ctx, "/"+interfaceKey+"/UpdateWorkResource", in, out)
}
func (c *castClient) OAuthAccount(ctx context.Context, in *OAuthAccountReq, opts ...grpc_go.CallOption) (*OAuthAccountResp, common.ErrorWithAttachment) {
out := new(OAuthAccountResp)
interfaceKey := ctx.Value(constant.InterfaceKey).(string)
@ -796,8 +780,6 @@ type CastServer interface {
UpdateWorkPlatformInfo(context.Context, *UpdateWorkPlatformInfoReq) (*UpdateWorkPlatformInfoResp, error)
UpdateWorkPublishLog(context.Context, *UpdateWorkPublishLogReq) (*emptypb.Empty, error)
RefreshWorkList(context.Context, *RefreshWorkListReq) (*RefreshWorkListResp, error)
WorkResource(context.Context, *WorkResourceReq) (*WorkResourceResp, error)
UpdateWorkResource(context.Context, *UpdateWorkResourceReq) (*UpdateWorkResourceResp, error)
OAuthAccount(context.Context, *OAuthAccountReq) (*OAuthAccountResp, error)
OAuthAccountV2(context.Context, *OAuthAccountV2Req) (*OAuthAccountV2Resp, error)
OAuthCodeToToken(context.Context, *OAuthCodeToTokenReq) (*OAuthCodeToTokenResp, error)
@ -953,12 +935,6 @@ func (UnimplementedCastServer) UpdateWorkPublishLog(context.Context, *UpdateWork
func (UnimplementedCastServer) RefreshWorkList(context.Context, *RefreshWorkListReq) (*RefreshWorkListResp, error) {
return nil, status.Errorf(codes.Unimplemented, "method RefreshWorkList not implemented")
}
func (UnimplementedCastServer) WorkResource(context.Context, *WorkResourceReq) (*WorkResourceResp, error) {
return nil, status.Errorf(codes.Unimplemented, "method WorkResource not implemented")
}
func (UnimplementedCastServer) UpdateWorkResource(context.Context, *UpdateWorkResourceReq) (*UpdateWorkResourceResp, error) {
return nil, status.Errorf(codes.Unimplemented, "method UpdateWorkResource not implemented")
}
func (UnimplementedCastServer) OAuthAccount(context.Context, *OAuthAccountReq) (*OAuthAccountResp, error) {
return nil, status.Errorf(codes.Unimplemented, "method OAuthAccount not implemented")
}
@ -1843,64 +1819,6 @@ func _Cast_RefreshWorkList_Handler(srv interface{}, ctx context.Context, dec fun
return interceptor(ctx, in, info, handler)
}
func _Cast_WorkResource_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc_go.UnaryServerInterceptor) (interface{}, error) {
in := new(WorkResourceReq)
if err := dec(in); err != nil {
return nil, err
}
base := srv.(dubbo3.Dubbo3GrpcService)
args := []interface{}{}
args = append(args, in)
md, _ := metadata.FromIncomingContext(ctx)
invAttachment := make(map[string]interface{}, len(md))
for k, v := range md {
invAttachment[k] = v
}
invo := invocation.NewRPCInvocation("WorkResource", args, invAttachment)
if interceptor == nil {
result := base.XXX_GetProxyImpl().Invoke(ctx, invo)
return result, result.Error()
}
info := &grpc_go.UnaryServerInfo{
Server: srv,
FullMethod: ctx.Value("XXX_TRIPLE_GO_INTERFACE_NAME").(string),
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
result := base.XXX_GetProxyImpl().Invoke(ctx, invo)
return result, result.Error()
}
return interceptor(ctx, in, info, handler)
}
func _Cast_UpdateWorkResource_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc_go.UnaryServerInterceptor) (interface{}, error) {
in := new(UpdateWorkResourceReq)
if err := dec(in); err != nil {
return nil, err
}
base := srv.(dubbo3.Dubbo3GrpcService)
args := []interface{}{}
args = append(args, in)
md, _ := metadata.FromIncomingContext(ctx)
invAttachment := make(map[string]interface{}, len(md))
for k, v := range md {
invAttachment[k] = v
}
invo := invocation.NewRPCInvocation("UpdateWorkResource", args, invAttachment)
if interceptor == nil {
result := base.XXX_GetProxyImpl().Invoke(ctx, invo)
return result, result.Error()
}
info := &grpc_go.UnaryServerInfo{
Server: srv,
FullMethod: ctx.Value("XXX_TRIPLE_GO_INTERFACE_NAME").(string),
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
result := base.XXX_GetProxyImpl().Invoke(ctx, invo)
return result, result.Error()
}
return interceptor(ctx, in, info, handler)
}
func _Cast_OAuthAccount_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc_go.UnaryServerInterceptor) (interface{}, error) {
in := new(OAuthAccountReq)
if err := dec(in); err != nil {
@ -3827,14 +3745,6 @@ var Cast_ServiceDesc = grpc_go.ServiceDesc{
MethodName: "RefreshWorkList",
Handler: _Cast_RefreshWorkList_Handler,
},
{
MethodName: "WorkResource",
Handler: _Cast_WorkResource_Handler,
},
{
MethodName: "UpdateWorkResource",
Handler: _Cast_UpdateWorkResource_Handler,
},
{
MethodName: "OAuthAccount",
Handler: _Cast_OAuthAccount_Handler,

Binary file not shown.

View File

@ -5,7 +5,6 @@ import (
"encoding/json"
"errors"
"fmt"
"fonchain-fiee/api/aryshare"
"fonchain-fiee/api/bundle"
"fonchain-fiee/api/cast"
"fonchain-fiee/pkg/cache"
@ -17,7 +16,6 @@ import (
"log"
"math/rand"
"strconv"
"strings"
"time"
"github.com/go-redis/redis"
@ -30,7 +28,7 @@ func InitTasks() error {
err := cm.AddTask("refreshWorkApprovalStatus", "0 */1 * * * *", RefreshWorkApprovalStatusTask)
err = cm.AddTask("artistAutoConfirm", "0 */1 * * * *", ArtistAutoConfirmTask)
err = cm.AddTask("refreshPublishStatus", "0 */1 * * * *", PublishTask)
err = cm.AddTask("scheduledPublish", "0 */10 * * * *", ScheduledPublishTask)
err = cm.AddTask("scheduledPublish", "0 */1 * * * *", ScheduledPublishTask)
err = cm.AddTask("artistAutoConfirmAnalysis", "0 */1 * * * *", ArtistAutoConfirmAnalysisTask)
err = cm.AddTask("refreshWorkAnalysisApprovalStatus", "0 */1 * * * *", RefreshWorkAnalysisApprovalStatusTask)
@ -42,9 +40,6 @@ func InitTasks() error {
if err != nil {
log.Printf("添加定时任务失败: %v", err)
}
// 每2分钟执行一次标签观看次数更新任务
// err = cm.AddTask("updateCastTagWatchCount", "0 */1 * * * *", UpdateCastTagWatchCountTask)
cm.Start()
// 启动队列消费者
@ -285,7 +280,7 @@ func PublishTask() {
func ScheduledPublishTask() {
// 加上锁
lockKey := "scheduled_publish:lock"
reply := cache.RedisClient.SetNX(lockKey, time.Now().Format("2006-01-02 15:04:05"), 2*time.Hour)
reply := cache.RedisClient.SetNX(lockKey, time.Now().Format("2006-01-02 15:04:05"), 1*time.Hour)
if !reply.Val() {
zap.L().Warn("定时发布任务正在被其他实例处理")
return
@ -315,12 +310,9 @@ func ScheduledPublishTask() {
const batchSize = 8 // 每批发布8个与PublishWork的workerCount保持一致
zap.L().Info("发现定时发布任务", zap.Int("total_count", len(workList)))
zap.L().Info("发现定时发布任务 列表", zap.Any("workList", workList))
now := float64(time.Now().Unix())
publishBatch := make([]string, 0, batchSize)
// 使用 map 进行去重,防止同一个 work_uuid 被多次添加
publishedUuids := make(map[string]bool)
// 遍历所有数据根据score判断处理
for _, item := range workList {
@ -343,32 +335,11 @@ func ScheduledPublishTask() {
continue
}
// 先从 Redis 中删除,采用"先删后处理"的策略,防止重复处理
removed, delErr := cache.RedisClient.ZRem(modelCast.ScheduledPublishQueueKey, workUuid).Result()
if delErr != nil {
zap.L().Error("删除定时发布任务失败", zap.Error(delErr), zap.String("work_uuid", workUuid))
continue // 删除失败则跳过,避免重复处理
}
if removed == 0 {
zap.L().Warn("定时发布任务已被删除,跳过",
zap.String("work_uuid", workUuid))
continue // 已被其他实例删除,跳过
}
// 检查是否已经在当前批次中(去重)
if publishedUuids[workUuid] {
zap.L().Warn("发现重复的定时发布任务,跳过",
zap.String("work_uuid", workUuid))
continue
}
// score大于等于当前时间添加到待发布批次
zap.L().Info("添加到发布批次",
zap.String("work_uuid", workUuid),
zap.Float64("score", score))
publishBatch = append(publishBatch, workUuid)
publishedUuids[workUuid] = true
publishCount++
// 当批次达到指定大小时,批量发布
if len(publishBatch) >= batchSize {
@ -376,6 +347,15 @@ func ScheduledPublishTask() {
_ = serverCast.PublishWork(context.Background(), &cast.PublishReq{
WorkUuids: publishBatch,
})
// 批量删除已发布的任务
for _, uuid := range publishBatch {
removed, delErr := cache.RedisClient.ZRem(modelCast.ScheduledPublishQueueKey, uuid).Result()
if delErr != nil {
zap.L().Error("删除定时发布任务失败", zap.Error(delErr), zap.String("work_uuid", uuid))
} else if removed > 0 {
publishCount++
}
}
zap.L().Info("批次发布完成", zap.Int("published", len(publishBatch)))
// 清空批次,准备下一批
publishBatch = make([]string, 0, batchSize)
@ -388,6 +368,15 @@ func ScheduledPublishTask() {
_ = serverCast.PublishWork(context.Background(), &cast.PublishReq{
WorkUuids: publishBatch,
})
// 批量删除已发布的任务
for _, uuid := range publishBatch {
removed, delErr := cache.RedisClient.ZRem(modelCast.ScheduledPublishQueueKey, uuid).Result()
if delErr != nil {
zap.L().Error("删除定时发布任务失败", zap.Error(delErr), zap.String("work_uuid", uuid))
} else if removed > 0 {
publishCount++
}
}
zap.L().Info("剩余批次发布完成", zap.Int("published", len(publishBatch)))
}
@ -578,131 +567,3 @@ func AyrshareMetricsCollectorTask() {
func RefreshArtistOrderTask() {
_, _ = service.CastProvider.Tools(context.Background(), &cast.ToolsReq{Action: "artistOrderInfo"})
}
// UpdateCastTagWatchCountTask 更新标签观看次数的定时任务每5分钟执行一次
func UpdateCastTagWatchCountTask() {
ctx := context.Background()
// 计算两天前的00:00:00
now := time.Now()
twoDaysAgo := now.AddDate(0, 0, -2)
createdAtStart := time.Date(twoDaysAgo.Year(), twoDaysAgo.Month(), twoDaysAgo.Day(), 0, 0, 0, 0, twoDaysAgo.Location())
createdAtEnd := now
// 格式化时间字符串2026-01-01 00:00:00
createdAtStartStr := createdAtStart.Format("2006-01-02 15:04:05")
createdAtEndStr := createdAtEnd.Format("2006-01-02 15:04:05")
// 调用 ListCastTags 接口,筛选 IsWatchCountCalled = 2 的数据
listReq := &cast.ListCastTagsReq{
CreatedAtStart: createdAtStartStr,
CreatedAtEnd: createdAtEndStr,
IsWatchCountCalled: 2, // 2表示未调用
Page: 1,
PageSize: 10,
}
listResp, err := service.CastProvider.ListCastTags(ctx, listReq)
if err != nil {
zap.L().Error("获取标签列表失败", zap.Error(err))
return
}
if listResp.Data == nil || len(listResp.Data) == 0 {
return
}
zap.L().Info("获取到需要更新的标签", zap.Int("count", len(listResp.Data)))
// 获取有效的 profileKey
profileKey, err := serverCast.GetValidProfileKey(ctx, []uint32{1})
if err != nil {
zap.L().Error("获取有效profileKey失败", zap.Error(err))
return
}
// 准备批量更新的数据
updateData := make([]*cast.CastTagInfo, 0, len(listResp.Data))
// 遍历每个标签,调用 RecommendHashtags 接口
for _, tag := range listResp.Data {
if tag.HashTag == "" {
zap.L().Warn("标签HashTag为空跳过", zap.String("uuid", tag.Uuid))
// 即使HashTag为空也要更新IsWatchCountCalled为1
updateData = append(updateData, &cast.CastTagInfo{
Uuid: tag.Uuid,
WatchCount: 1,
IsWatchCountCalled: 1,
})
continue
}
// 调用 RecommendHashtags 接口
recommendReq := &aryshare.RecommendHashtagsRequest{
Keyword: tag.HashTag,
ProfileKey: profileKey,
}
recommendResp, err := service.AyrshareProvider.RecommendHashtags(ctx, recommendReq)
if err != nil {
zap.L().Error("调用RecommendHashtags接口失败",
zap.String("hashTag", tag.HashTag),
zap.String("uuid", tag.Uuid),
zap.Error(err))
// 调用失败时将WatchCount更新为1IsWatchCountCalled更新为1
updateData = append(updateData, &cast.CastTagInfo{
Uuid: tag.Uuid,
WatchCount: 1,
IsWatchCountCalled: 1,
})
continue
}
// 对比返回结果,查找完全一致的标签
var matchedViewCount int64 = 0
if recommendResp.Recommendations != nil {
for _, recommendation := range recommendResp.Recommendations {
// 完全一致匹配(不区分大小写)
if strings.EqualFold(recommendation.Name, tag.HashTag) {
matchedViewCount = recommendation.ViewCount
break
}
}
}
// 根据匹配结果更新WatchCount
var watchCount int32 = 1
if matchedViewCount > 0 {
watchCount = int32(matchedViewCount)
}
// 添加到更新列表
updateData = append(updateData, &cast.CastTagInfo{
Uuid: tag.Uuid,
WatchCount: watchCount,
IsWatchCountCalled: 1,
})
zap.L().Debug("处理标签完成",
zap.String("hashTag", tag.HashTag),
zap.String("uuid", tag.Uuid),
zap.Int64("matchedViewCount", matchedViewCount),
zap.Int32("watchCount", watchCount))
}
// 如果没有需要更新的数据,直接返回
if len(updateData) == 0 {
return
}
// 批量更新标签
batchUpdateReq := &cast.BatchUpdateCastTagsReq{
Data: updateData,
}
_, err = service.CastProvider.BatchUpdateCastTags(ctx, batchUpdateReq)
if err != nil {
zap.L().Error("批量更新标签失败", zap.Error(err), zap.Int("count", len(updateData)))
return
}
}

View File

@ -81,18 +81,6 @@ func MediaRouter(r *gin.RouterGroup) {
prompt.POST("delete", serviceCast.DeletePrompt)
}
tag := auth.Group("tag")
{
tag.POST("update", serviceCast.UpdateCastTag)
tag.POST("list", serviceCast.ListCastTags)
tag.POST("import-batch", serviceCast.ImportTagBatch)
tag.POST("recalculate-quote-count", serviceCast.RecalculateCastTagQuoteCount)
// tag.POST("auto-hashtags", serviceCast.AutoHashtags)
// 这两个接口需要关闭ins通过facebook授权
// tag.POST("recommend-hashtags", serviceCast.RecommendHashtags)
// tag.POST("search-hashtags", serviceCast.SearchHashtags)
}
//AI 生图
aiNoAuth := noAuth.Group("ai")
{

View File

@ -1,40 +0,0 @@
package cast
import (
"context"
"fonchain-fiee/api/cast"
"fonchain-fiee/pkg/service"
)
func CheckImage(workUuid string) error {
detailResp, err := service.CastProvider.WorkDetail(context.Background(), &cast.WorkDetailReq{WorkUuid: workUuid})
if err != nil {
return err
}
if detailResp.CoverUrl == "" {
return nil
}
if detailResp.WorkCategory == 1 {
return nil
}
resourceResp, err := service.CastProvider.WorkResource(context.Background(), &cast.WorkResourceReq{WorkUuid: workUuid})
if err != nil {
return err
}
if detailResp.CoverUrl == resourceResp.OldCoverUrl {
return nil
}
mediaUrls, err := ProcessImg([]string{detailResp.CoverUrl})
if err != nil {
return err
}
if len(mediaUrls) > 0 {
_, _ = service.CastProvider.UpdateWorkResource(context.Background(), &cast.UpdateWorkResourceReq{
Uuid: "",
WorkUuid: detailResp.WorkUuid,
OldCoverUrl: detailResp.CoverUrl,
NewCoverUrl: mediaUrls[0],
})
}
return nil
}

View File

@ -1,440 +0,0 @@
package cast
import (
"context"
"errors"
"fmt"
"fonchain-fiee/api/aryshare"
"fonchain-fiee/api/cast"
"fonchain-fiee/cmd/config"
"fonchain-fiee/pkg/model/login"
"fonchain-fiee/pkg/service"
"fonchain-fiee/pkg/utils"
"math/rand"
"path/filepath"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/xuri/excelize/v2"
"go.uber.org/zap"
"google.golang.org/protobuf/types/known/emptypb"
)
// UpdateCastTag 更新话题标签
func UpdateCastTag(ctx *gin.Context) {
var req *cast.UpdateCastTagReq
var err error
if err = ctx.ShouldBind(&req); err != nil {
service.Error(ctx, err)
return
}
newCtx := NewCtxWithUserInfo(ctx)
resp, err := service.CastProvider.UpdateCastTag(newCtx, req)
if err != nil {
service.Error(ctx, err)
return
}
service.Success(ctx, resp)
return
}
// ListCastTags 获取话题标签列表
func ListCastTags(ctx *gin.Context) {
var req *cast.ListCastTagsReq
var err error
if err = ctx.ShouldBind(&req); err != nil {
service.Error(ctx, err)
return
}
newCtx := NewCtxWithUserInfo(ctx)
resp, err := service.CastProvider.ListCastTags(newCtx, req)
if err != nil {
service.Error(ctx, err)
return
}
service.Success(ctx, resp)
return
}
// ImportTagBatch 批量导入话题标签
func ImportTagBatch(ctx *gin.Context) {
// 接收form表单的Excel保存到本地进行解析
excelFile, err := ctx.FormFile("file")
if err != nil {
service.Error(ctx, err)
return
}
tempDir := "./runtime/tag"
_, err = utils.CheckDirPath(tempDir, true)
fileName := time.Now().Format("20060102150405") + "_in_tag.xlsx"
excelPath := filepath.Join(tempDir, fileName)
if err = ctx.SaveUploadedFile(excelFile, excelPath); err != nil {
service.Error(ctx, err)
return
}
// 读取Excel中的数据
excelData, err := excelize.OpenFile(excelPath)
if err != nil {
service.Error(ctx, err)
return
}
defer excelData.Close()
// 解析Excel中的数据
rows, err := excelData.GetRows("Sheet1")
if err != nil {
service.Error(ctx, err)
return
}
req := cast.UpdateCastTagBatchReq{
Data: make([]*cast.CastTagInfo, 0),
}
userInfo := login.GetUserInfoFromC(ctx)
for line, row := range rows {
if line == 0 {
continue
}
temp := cast.CastTagInfo{
CreatorUuid: fmt.Sprint(userInfo.ID),
CreatorName: userInfo.Name,
Source: 1, // 固定来源:人工导入
Status: 1, // 固定状态:有效
}
// 解析Excel列A-话题标签B-备注
if len(row) > 0 {
temp.HashTag = strings.TrimSpace(row[0])
}
if len(row) > 1 {
temp.Remark = strings.TrimSpace(row[1])
}
zap.L().Info("ImportTagBatch row", zap.Int("line", line), zap.Strings("row", row))
// 验证必填字段:话题标签不能为空
if utils.CleanString(temp.HashTag) == "" {
temp.Remark = "必填项未填"
req.Data = append(req.Data, &temp)
continue
}
req.Data = append(req.Data, &temp)
}
newCtx := NewCtxWithUserInfo(ctx)
resp, _err := service.CastProvider.UpdateCastTagBatch(newCtx, &req)
if _err != nil {
service.Error(ctx, _err)
return
}
// 打开模板 写入resp 返回的数据
templatePath := "./data/话题标签导入模板.xlsx"
template, err := excelize.OpenFile(templatePath)
if err != nil {
service.Error(ctx, err)
return
}
defer template.Close()
var urlResult string
if resp.FailCount != 0 {
rowIndex := 2 // 从第2行开始写入第1行是表头
for _, v := range resp.Data {
if v.Success {
continue
}
// 写入失败的数据到Excel模板只写入话题标签和备注两列
template.SetCellValue("Sheet1", fmt.Sprintf("A%d", rowIndex), v.HashTag)
template.SetCellValue("Sheet1", fmt.Sprintf("B%d", rowIndex), v.Remark)
rowIndex++
}
resultFilename := strings.Replace(fileName, "_in_tag.xlsx", "_out_tag.xlsx", -1)
resultPath := fmt.Sprintf("./runtime/tag/%s", resultFilename)
if err = template.SaveAs(resultPath); err != nil {
service.Error(ctx, err)
return
}
urlHost := config.AppConfig.System.FieeHost
urlResult = fmt.Sprintf("%s/api/fiee/static/tag/%s", urlHost, resultFilename)
}
service.Success(ctx, map[string]interface{}{
"successCount": resp.SuccessCount,
"failCount": resp.FailCount,
"resultUrl": urlResult,
})
return
}
// RecalculateCastTagQuoteCount 重新计算话题标签引用数量
func RecalculateCastTagQuoteCount(ctx *gin.Context) {
newCtx := NewCtxWithUserInfo(ctx)
// 创建空的请求对象
req := &emptypb.Empty{}
resp, err := service.CastProvider.RecalculateCastTagQuoteCount(newCtx, req)
if err != nil {
service.Error(ctx, err)
return
}
service.Success(ctx, resp)
return
}
// findNewTags 对比两次标签,找出新增的标签
func findNewTags(beforeTags, afterTags []string) []string {
// 将 beforeTags 转换为 map方便查找
beforeMap := make(map[string]bool)
for _, tag := range beforeTags {
cleanTag := strings.TrimSpace(tag)
if cleanTag != "" {
beforeMap[cleanTag] = true
}
}
// 找出 afterTags 中不在 beforeTags 中的标签
newTags := make([]string, 0)
for _, tag := range afterTags {
cleanTag := strings.TrimSpace(tag)
if cleanTag != "" && !beforeMap[cleanTag] {
newTags = append(newTags, cleanTag)
}
}
return newTags
}
func GetValidProfileKey(ctx context.Context, platformIDs []uint32) (string, error) {
if len(platformIDs) == 0 {
platformIDs = []uint32{1, 2, 3, 5}
}
profileKeys, err := service.CastProvider.GetArtistAyrShareInfoByPlatformIDs(ctx, &cast.GetArtistAyrShareInfoByPlatformIDsReq{
PlatformIDs: platformIDs,
Page: 1,
PageSize: 20,
})
if err != nil {
zap.L().Error("GetArtistAyrShareInfoByPlatformIDs failed", zap.Error(err))
return "", errors.New("获取有效profileKey失败")
}
if len(profileKeys.Data) == 0 {
return "", errors.New("当前没有有效的profileKey")
}
// 过滤出所有非空的 profileKey
validProfileKeys := make([]string, 0)
for _, item := range profileKeys.Data {
if item.ProfileKey != "" {
validProfileKeys = append(validProfileKeys, item.ProfileKey)
}
}
if len(validProfileKeys) == 0 {
return "", errors.New("profileKey为空")
}
// 从有效的 profileKey 中随机选择一个
randIndex := rand.Intn(len(validProfileKeys))
return validProfileKeys[randIndex], nil
}
// SaveTagsToDatabase 将标签保存到数据库
func SaveTagsToDatabase(ctx *gin.Context, tags []string, source uint32) error {
if len(tags) == 0 {
return nil
}
// 获取用户信息
userInfo := login.GetUserInfoFromC(ctx)
newCtx := NewCtxWithUserInfo(ctx)
// 构建批量导入请求
req := cast.UpdateCastTagBatchReq{
Data: make([]*cast.CastTagInfo, 0, len(tags)),
}
for _, tag := range tags {
tagInfo := &cast.CastTagInfo{
HashTag: tag,
CreatorUuid: fmt.Sprint(userInfo.ID),
CreatorName: userInfo.Name,
Source: source, // 4: 自动标签(从内容中自动提取)
Status: 1, // 1: 有效
}
req.Data = append(req.Data, tagInfo)
}
// 调用批量导入接口
_, err := service.CastProvider.UpdateCastTagBatch(newCtx, &req)
if err != nil {
err = errors.New("标签保存到数据库失败")
zap.L().Error("SaveTagsToDatabase UpdateCastTagBatch failed", zap.Error(err))
return err
}
zap.L().Info("SaveTagsToDatabase success", zap.Int("tagCount", len(tags)), zap.Strings("tags", tags), zap.Uint32("source", source))
return nil
}
func GenerateAutoHashtags(ctx context.Context, post string, max int32, position, language string) (*aryshare.AutoHashtagsResponse, []string, bool, error) {
// 验证帖子内容
if post == "" {
return nil, nil, false, errors.New("帖子内容不能为空")
}
// post 的长度不能超过1000个字符
if len(post) > 1000 {
return nil, nil, false, errors.New("自动生成标签的帖子内容不能超过1000个字符")
}
// 提取生成前的标签
beforeTags := utils.ExtractTags(post)
zap.L().Info("GenerateAutoHashtags beforeTags", zap.Strings("beforeTags", beforeTags))
// 如果标签数量已经达到或超过5个不需要生成
if len(beforeTags) >= 5 {
return nil, nil, false, nil
}
// 设置默认值
if position == "" {
position = "end"
}
if language == "" {
language = "zh"
}
// 如果 max 为0则根据现有标签数自动计算确保总数为5
if max == 0 {
max = int32(5 - len(beforeTags))
}
// 如果此时 max 小于等于0则直接返回
if max <= 0 {
return nil, nil, false, nil
}
profileKey, err := GetValidProfileKey(ctx, []uint32{1, 2, 3, 5})
if err != nil {
return nil, nil, false, err
}
// 构建请求
req := &aryshare.AutoHashtagsRequest{
Post: post,
Max: max,
Position: position,
Language: language,
ProfileKey: profileKey,
}
// 调用 Ayrshare 的 AutoHashtags 接口
resp, err := service.AyrshareProvider.AutoHashtags(ctx, req)
if err != nil {
zap.L().Error("AutoHashtags failed", zap.Error(err))
return nil, nil, false, errors.New("自动生成标签失败")
}
if resp.Post == "" {
return nil, nil, false, errors.New("自动生成标签返回的帖子内容为空")
}
// 去掉自动标签返回的帖子内容多余的引号
resp.Post = utils.CleanAutoHashtagsQuote(resp.Post)
// 提取生成后的标签
afterTags := utils.ExtractTags(resp.Post)
zap.L().Info("GenerateAutoHashtags afterTags", zap.Strings("afterTags", afterTags))
// 对比两次标签,找出新增的标签
newTags := findNewTags(beforeTags, afterTags)
return resp, newTags, true, nil
}
// AutoHashtags 自动生成标签
func AutoHashtags(ctx *gin.Context) {
var req *aryshare.AutoHashtagsRequest
var err error
if err = ctx.ShouldBind(&req); err != nil {
service.Error(ctx, err)
return
}
// 调用核心逻辑生成标签
resp, newTags, needMore, err := GenerateAutoHashtags(
context.Background(),
req.Post,
req.Max,
req.Position,
req.Language,
)
if err != nil {
service.Error(ctx, err)
return
}
// 如果标签已满5个直接返回
if !needMore {
service.Success(ctx, map[string]interface{}{
"message": "当前帖子的标签已经有5个了",
})
return
}
// 保存新增的标签到数据库Source 设置为 4自动标签
if len(newTags) > 0 {
if err = SaveTagsToDatabase(ctx, newTags, 4); err != nil {
zap.L().Error("SaveTagsToDatabase failed", zap.Error(err), zap.Strings("newTags", newTags))
err = errors.New("标签保存到数据库失败")
service.Error(ctx, err)
return
}
}
service.Success(ctx, resp)
return
}
// RecommendHashtags 推荐标签
func RecommendHashtags(ctx *gin.Context) {
var req *aryshare.RecommendHashtagsRequest
var err error
if err = ctx.ShouldBind(&req); err != nil {
service.Error(ctx, err)
return
}
profileKey, err := GetValidProfileKey(context.Background(), []uint32{1})
if err != nil {
service.Error(ctx, err)
return
}
req.ProfileKey = profileKey
resp, err := service.AyrshareProvider.RecommendHashtags(context.Background(), req)
if err != nil {
fmt.Println("err", err)
zap.L().Error("RecommendHashtags failed", zap.Error(err))
err = errors.New("推荐标签失败")
service.Error(ctx, err)
return
}
service.Success(ctx, resp)
return
}
// SearchHashtags 搜索标签
func SearchHashtags(ctx *gin.Context) {
var req *aryshare.SearchHashtagsRequest
var err error
if err = ctx.ShouldBind(&req); err != nil {
service.Error(ctx, err)
return
}
profileKey, err := GetValidProfileKey(context.Background(), []uint32{3})
if err != nil {
service.Error(ctx, err)
return
}
req.ProfileKey = profileKey
resp, err := service.AyrshareProvider.SearchHashtags(context.Background(), req)
if err != nil {
fmt.Println("err", err)
err = errors.New("获取热门话题标签失败")
zap.L().Error("SearchHashtags failed", zap.Error(err))
service.Error(ctx, err)
return
}
service.Success(ctx, resp)
return
}

View File

@ -17,13 +17,6 @@ import (
func Test(ctx *gin.Context) {
action := ctx.PostForm("action")
if action == "" {
workUuid := ctx.PostForm("workUuid")
err := CheckImage(workUuid)
if err != nil {
service.Error(ctx, err)
return
}
service.Success(ctx, nil)
return
}

View File

@ -122,14 +122,6 @@ func UpdateWorkImageCore(ctx *gin.Context, req *cast.UpdateWorkImageReq) (*cast.
//if _, err = CheckUserBundleBalance(int32(artistID), modelCast.BalanceTypeImageValue); err != nil {
// return nil, err
//}
// 处理内容中的标签:提取、验证并批量导入,以及自动生成标签
content, err := processContentAndAutoTags(ctx, req.Content)
if err != nil {
return nil, err
}
// 将自动生成标签后的内容更新到请求中
req.Content = content
newCtx := NewCtxWithUserInfo(ctx)
req.Source = 1
resp, err := service.CastProvider.UpdateWorkImage(newCtx, req)
@ -179,100 +171,6 @@ func UpdateWorkImage(ctx *gin.Context) {
return
}
func processContentTags(ctx *gin.Context, content string) error {
// 如果内容为空,直接返回
if content == "" {
return nil
}
// 提取标签
tags := utils.ExtractTags(content)
if len(tags) == 0 {
return nil
}
// 第一步:检查标签格式(验证标签不为空且有效)
validTags := make([]string, 0, len(tags))
for _, tag := range tags {
// 去除空白字符后检查
cleanTag := strings.TrimSpace(tag)
if cleanTag != "" {
validTags = append(validTags, cleanTag)
}
}
// 如果没有有效标签,直接返回
if len(validTags) == 0 {
return nil
}
// 第二步:检查是否有重复的标签
tagMap := make(map[string]bool)
for _, tag := range validTags {
tagLower := strings.ToLower(tag)
if tagMap[tagLower] {
return errors.New("帖子标签不能重复")
}
tagMap[tagLower] = true
}
// 第三步检查标签数量是否超过5个
if len(validTags) > 5 {
return errors.New("帖子标签数量不能超过5个")
}
fmt.Println("validTags", validTags)
// 第四步:调用 SaveTagsToDatabase 函数批量导入标签Source 设置为 3推荐标签
if err := SaveTagsToDatabase(ctx, validTags, 3); err != nil {
zap.L().Error("processContentTags SaveTagsToDatabase failed", zap.Error(err))
return errors.New("批量导入标签失败")
}
zap.L().Info("processContentTags success", zap.Int("tagCount", len(validTags)), zap.Strings("tags", validTags))
return nil
}
// processContentAndAutoTags 处理内容标签并自动生成标签
func processContentAndAutoTags(ctx *gin.Context, content string) (string, error) {
// 如果内容为空,直接返回
if content == "" {
return "", nil
}
// 处理内容中的标签:提取、验证并批量导入
if err := processContentTags(ctx, content); err != nil {
return content, err
}
// 处理完内容标签后,自动生成标签并存入数据库
resp, newTags, needMore, err := GenerateAutoHashtags(
context.Background(),
content,
0, // max 为0时自动计算
"", // position 使用默认值
"", // language 使用默认值
)
if err != nil {
return content, err
}
if resp == nil {
return content, nil
}
// 保存新生成的标签到数据库
if needMore && len(newTags) > 0 {
if saveErr := SaveTagsToDatabase(ctx, newTags, 4); saveErr != nil {
zap.L().Error("processContentAndAutoTags SaveTagsToDatabase failed", zap.Error(saveErr))
return content, errors.New("自动生成标签保存到数据库失败")
}
}
// 检查一下 resp.Post 是否为空
if resp.Post == "" {
return content, nil
}
return resp.Post, nil
}
// UpdateWorkVideoCore 更新作品视频的核心逻辑,可以被其他函数复用
func UpdateWorkVideoCore(ctx *gin.Context, req *cast.UpdateWorkVideoReq) (*cast.UpdateWorkVideoResp, error) {
var infoResp *accountFiee.UserInfoResponse
@ -352,15 +250,6 @@ func UpdateWorkVideoCore(ctx *gin.Context, req *cast.UpdateWorkVideoReq) (*cast.
req.ArtistPhone = infoResp.TelNum
req.ArtistPhoneAreaCode = infoResp.TelAreaCode
req.ArtistSubNum = infoResp.SubNum
// 处理内容中的标签:提取、验证并批量导入,以及自动生成标签
fmt.Println("UpdateWorkVideoCore: req.Content=", req.Content, "req.Title=", req.Title)
content, err := processContentAndAutoTags(ctx, req.Content)
if err != nil {
return nil, err
}
// 将自动生成标签后的内容更新到请求中
req.Content = content
newCtx := NewCtxWithUserInfo(ctx)
req.Source = 1
resp, err := service.CastProvider.UpdateWorkVideo(newCtx, req)
@ -659,8 +548,6 @@ func PublishWork(ctx context.Context, req *cast.PublishReq) error {
func PostAS(ctx context.Context, workUuid string) error {
var err error
//检查封面
_ = CheckImage(workUuid)
_, err = service.CastProvider.Publish(ctx, &cast.PublishReq{WorkUuids: []string{workUuid}})
if err != nil {
zap.L().Error("Publish err", zap.String("workUuid", workUuid), zap.Error(err))
@ -1001,6 +888,7 @@ func RePublish(ctx *gin.Context) {
service.Error(ctx, err)
return
}
service.Success(ctx, req)
return
}
@ -1526,15 +1414,6 @@ func ImportWorkBatch(ctx *gin.Context) {
req.ImageWorks = append(req.ImageWorks, temp)
break
}
// 处理内容中的标签:提取、验证并批量导入,以及自动生成标签
processedContent, err := processContentAndAutoTags(ctx, temp.Content)
if err != nil {
temp.Remark = fmt.Sprintf("%s", err.Error())
req.ImageWorks = append(req.ImageWorks, temp)
break
}
// 将处理后的内容更新到 temp.Content
temp.Content = processedContent
}
}
for i := 10; i <= 20; i++ {

View File

@ -297,36 +297,35 @@ func (p *BatchProcessor) submitTask(req *excelData) error {
if !tiktokFound {
return fmt.Errorf("未找到匹配的TikTok账号: %s", req.TikTok)
}
if req.Instagram != "" {
// 获取 Instagram 自媒体账号
accountListIns, err := service.CastProvider.MediaUserList(context.Background(), &apiCast.MediaUserListReq{
ArtistVal: req.ArtistName,
PlatformID: 3,
Page: 1,
PageSize: 10,
ArtistUuid: strconv.FormatUint(list.UserList[0].Id, 10),
})
if err != nil {
return fmt.Errorf("获取 Instagram 账号失败: %s", err.Error())
}
if accountListIns == nil || len(accountListIns.Data) == 0 {
return fmt.Errorf("ins自媒体账号数量为0")
}
// 查找 Instagram 账号
insFound := false
for _, user := range accountListIns.Data {
if user.PlatformUserName == req.Instagram {
req.MediaAccountNames = append(req.MediaAccountNames, user.PlatformUserName)
req.MediaAccountUuids = append(req.MediaAccountUuids, user.MediaAccountUuid)
insFound = true
break
}
}
if !insFound {
return fmt.Errorf("未找到匹配的Instagram账号: %s", req.Instagram)
}
// 获取 Instagram 自媒体账号
accountListIns, err := service.CastProvider.MediaUserList(context.Background(), &apiCast.MediaUserListReq{
ArtistVal: req.ArtistName,
PlatformID: 3,
Page: 1,
PageSize: 10,
ArtistUuid: strconv.FormatUint(list.UserList[0].Id, 10),
})
if err != nil {
return fmt.Errorf("获取 Instagram 账号失败: %s", err.Error())
}
if accountListIns == nil || len(accountListIns.Data) == 0 {
return fmt.Errorf("ins自媒体账号数量为0")
}
// 查找 Instagram 账号
insFound := false
for _, user := range accountListIns.Data {
if user.PlatformUserName == req.Instagram {
req.MediaAccountNames = append(req.MediaAccountNames, user.PlatformUserName)
req.MediaAccountUuids = append(req.MediaAccountUuids, user.MediaAccountUuid)
insFound = true
break
}
}
if !insFound {
return fmt.Errorf("未找到匹配的Instagram账号: %s", req.Instagram)
}
// 获取 Bluesky 自媒体账号
accountListBlueSky, err := service.CastProvider.MediaUserList(context.Background(), &apiCast.MediaUserListReq{
ArtistVal: req.ArtistName,

View File

@ -1,9 +1,6 @@
package utils
import (
"regexp"
"strings"
)
import "strings"
// CleanString 移除所有空白字符
func CleanString(s string) string {
@ -21,33 +18,3 @@ func TruncateString(s string, maxLen int) string {
}
return string(runes[:maxLen])
}
// ExtractTags 从文本中提取标签,标签以 # 开头,后面不能直接跟空格
func ExtractTags(s string) []string {
if len(s) == 0 {
return []string{}
}
re := regexp.MustCompile(`#[^\s#\p{P}]+`)
matches := re.FindAllString(s, -1)
tags := make([]string, 0, len(matches))
for _, match := range matches {
// 去掉开头的 #,只保留标签内容
tag := match[1:]
if len(tag) > 0 {
tags = append(tags, tag)
}
}
return tags
}
// 去掉自动标签里面多余的引号
func CleanAutoHashtagsQuote(input string) string {
if input == "" {
return ""
}
cleaned := strings.ReplaceAll(input, "\\\"", "")
cleaned = strings.ReplaceAll(cleaned, "\"", "")
return cleaned
}