@@ -0,0 +1,218 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"91porn-server/common"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/common/stderr"
|
||||
"91porn-server/models/commod"
|
||||
"91porn-server/models/v/modulesectionmod"
|
||||
"91porn-server/models/v/modulevidmod"
|
||||
"91porn-server/models/v/tagmod"
|
||||
"91porn-server/models/v/vidmod"
|
||||
"91porn-server/models/v/vidtimeonlinemod"
|
||||
"91porn-server/web/service/video_media_service"
|
||||
"fmt"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type vidTimeOnlineInfo struct {
|
||||
VidId primitive.ObjectID `json:"vidId"`
|
||||
VidType string `json:"vidType"`
|
||||
SourceURL string `json:"sourceURL"`
|
||||
ReviewAccount string `json:"reviewAccount"`
|
||||
}
|
||||
|
||||
var vidTimeOnlineIsRuining = false
|
||||
|
||||
// 视频自动上架
|
||||
func VidTimeOnline() {
|
||||
common.Go(func() {
|
||||
log.Info("video time online start...")
|
||||
if vidTimeOnlineIsRuining {
|
||||
log.Info("video time online already executed")
|
||||
return
|
||||
}
|
||||
vidTimeOnlineIsRuining = true
|
||||
defer func() {
|
||||
vidTimeOnlineIsRuining = false
|
||||
}()
|
||||
log.Info("video time online execute")
|
||||
handleVidTimeOnline()
|
||||
log.Info("video time online end...")
|
||||
})
|
||||
}
|
||||
|
||||
func handleVidTimeOnline() {
|
||||
//每次取十条数据,定期上线
|
||||
vidInfs, err := vidtimeonlinemod.GetByPage(commod.Page{PageNumber: 1, PageSize: 100})
|
||||
if err != nil {
|
||||
log.Error("video time online eonlinemod.GetByPage err", log.E(err))
|
||||
return
|
||||
}
|
||||
if len(vidInfs) == 0 {
|
||||
log.Info("video time online vidInfs == 0")
|
||||
return
|
||||
}
|
||||
|
||||
//上架
|
||||
resIds := passVids(vidInfs)
|
||||
//从自动上架表中移除已经上架的帖子数据
|
||||
if len(resIds) > 0 {
|
||||
if err = vidtimeonlinemod.DeleteByVids(resIds); err != nil {
|
||||
log.Warn("video time online vidtimeonlinemod.DeleteByVids err", log.Any("err", err))
|
||||
return
|
||||
}
|
||||
}
|
||||
log.Info("video time online resIds", log.Any("resIds", resIds))
|
||||
}
|
||||
|
||||
// passVids 批量自动上架
|
||||
func passVids(vidIds []vidtimeonlinemod.VidTimeOnlineModel) (resIds []primitive.ObjectID) {
|
||||
var uploadRecord []vidmod.UploadMediaVideo
|
||||
var records []modulevidmod.SectionVideo
|
||||
for _, vid := range vidIds {
|
||||
//非图集才需要同步
|
||||
isCover := vid.VidType == vidmod.COVER || vid.VidType == vidmod.PIC
|
||||
log.Info("vid time online", log.Any("isCover", isCover), log.Any("vid", vid))
|
||||
// 查询视频信息
|
||||
videoInfo, err := vidmod.GetVideoInfo(vid.VidId.Hex())
|
||||
if err != nil {
|
||||
log.Warn("vid time online passVids vidmod.GetVideoInfo err,", log.Any("err", err), log.Any("vid", vid.VidId))
|
||||
return resIds
|
||||
}
|
||||
log.Info("vid time online", log.Any("status", videoInfo.Status), log.Any("MDSID", videoInfo.MDSID), log.Any("videoInfo.Filename", videoInfo.Filename))
|
||||
// 媒资库的不做任何校验,因为给过来的这部分数据的sourceID有问题
|
||||
if videoInfo.MDSID == "" && !isCover && !strings.Contains(videoInfo.Filename, "mzk_") {
|
||||
if code := SyncFileFromFs(vid.VidId.Hex()); code != stderr.UpLoadFileComplete {
|
||||
log.Warn("vid time online passVids syncFileFromFs err,", log.Any("code", code), log.Any("vid", vid.VidId))
|
||||
continue
|
||||
}
|
||||
}
|
||||
tagSort := make(bson.M, len(videoInfo.Tags))
|
||||
for _, v := range videoInfo.Tags {
|
||||
tagSort[v.Hex()] = 0
|
||||
}
|
||||
|
||||
if videoInfo.Status == 1 {
|
||||
resIds = append(resIds, videoInfo.ID)
|
||||
continue
|
||||
}
|
||||
if (videoInfo.NewsType == vidmod.SP || videoInfo.NewsType == vidmod.SHORT) && !strings.Contains(videoInfo.Filename, "mzk_") {
|
||||
var width, height int
|
||||
resolutions := strings.Split(videoInfo.Resolution, "*")
|
||||
if len(resolutions) >= 2 {
|
||||
width, _ = strconv.Atoi(resolutions[0])
|
||||
height, _ = strconv.Atoi(resolutions[1])
|
||||
}
|
||||
if len(resolutions) != 2 {
|
||||
resolutions = strings.Split(videoInfo.Resolution, "x")
|
||||
if len(resolutions) >= 2 {
|
||||
width, _ = strconv.Atoi(resolutions[0])
|
||||
height, _ = strconv.Atoi(resolutions[1])
|
||||
}
|
||||
}
|
||||
var tags string
|
||||
if len(videoInfo.Tags) > 0 {
|
||||
for _, v := range videoInfo.Tags {
|
||||
tagData, err := tagmod.FindOneTagByID(v)
|
||||
if err == nil {
|
||||
if tags != "" {
|
||||
tags += "," + tagData.TagName
|
||||
} else {
|
||||
tags += tagData.TagName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
uploadRecord = append(uploadRecord, vidmod.UploadMediaVideo{
|
||||
FsResourceId: videoInfo.SourceID,
|
||||
HashId: videoInfo.MD5,
|
||||
Title: videoInfo.Title,
|
||||
Length: int(videoInfo.PlayTime),
|
||||
FileSize: videoInfo.Size,
|
||||
M3u8Src: videoInfo.SourceURL,
|
||||
Height: height,
|
||||
Width: width,
|
||||
CoverImage: videoInfo.Cover,
|
||||
TagsText: tags,
|
||||
})
|
||||
}
|
||||
video, err := vidmod.PassVidsOnlineTime(vid.VidId, 1, "", vid.ReviewAccount, vid.OnlineTime, tagSort)
|
||||
if err != nil {
|
||||
log.Warn("vid time online passVids vidmod.PassVids err,", log.Any("err", err), log.Any("vid", video.ID))
|
||||
return resIds
|
||||
}
|
||||
if !vid.SectionId.IsZero() {
|
||||
// 获取专题
|
||||
section, err := modulesectionmod.GetBySectionByID(vid.SectionId)
|
||||
if err != nil {
|
||||
log.Error("vid time online modulesectionmod.GetBySectionByID fail", log.Any("vid.SectionId", vid.SectionId), log.E(err))
|
||||
continue
|
||||
}
|
||||
if !section.ID.IsZero() {
|
||||
// 判断当前专题下是否已经有该记录,没有则添加
|
||||
sectionVids, err := modulevidmod.GetBySectionIDAndVids(section.ID, []primitive.ObjectID{vid.VidId})
|
||||
if err != nil {
|
||||
log.Error("vid time online modulevidmod.GetBySectionIDAndVids( fail", log.Any("vid.SectionId", vid.SectionId), log.Any("vid.VidId", vid.VidId), log.E(err))
|
||||
continue
|
||||
}
|
||||
if len(sectionVids) == 0 {
|
||||
records = append(records, modulevidmod.SectionVideo{
|
||||
SectionID: section.ID,
|
||||
VideoID: vid.VidId,
|
||||
VideoReviewedAt: vid.OnlineTime,
|
||||
})
|
||||
}
|
||||
// 修改帖子的mId
|
||||
update := bson.M{}
|
||||
update["updatedAt"] = time.Now()
|
||||
update["mId"] = section.SubModuleID.Hex()
|
||||
vidmod.UpdateOneByID(vid.VidId, update)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
resIds = append(resIds, video.ID)
|
||||
}
|
||||
|
||||
if len(records) > 0 {
|
||||
err := modulevidmod.InsertMany(records)
|
||||
if err != nil {
|
||||
log.Error("vid time online AddVidTimeOnlineInfo moduleVidMod InsertMany error", log.Any("records", records), log.Any("err", err))
|
||||
return resIds
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
log.Info("vid time online", log.Any("uploadRecord", uploadRecord))
|
||||
// 处理上传媒资库
|
||||
if len(uploadRecord) > 0 {
|
||||
var err error
|
||||
var sourceIDs []string
|
||||
var errSourceIDs []string
|
||||
for _, info := range uploadRecord {
|
||||
if info.Length > 300 {
|
||||
err = video_media_service.UploadSkdVideoMedia(info)
|
||||
} else {
|
||||
err = video_media_service.UploadSkdShortVideoMedia(info)
|
||||
}
|
||||
if err != nil {
|
||||
errSourceIDs = append(errSourceIDs, info.FsResourceId)
|
||||
log.Info(fmt.Sprintf("vid time online UploadVideoMedia err%v;errSourceIDs%v", err, errSourceIDs))
|
||||
continue
|
||||
}
|
||||
sourceIDs = append(sourceIDs, info.FsResourceId)
|
||||
}
|
||||
if err != nil {
|
||||
log.Info(fmt.Sprintf("vid time online UploadVideoMedia err%v;errSourceIDs%v", err, errSourceIDs))
|
||||
return resIds
|
||||
}
|
||||
log.Info(fmt.Sprintf("vid time online UploadVideoMedia sucess sourceIDs%v", sourceIDs))
|
||||
}
|
||||
|
||||
return resIds
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package contentreviewser
|
||||
|
||||
import (
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/models/v/contentreviewmod"
|
||||
"91porn-server/models/v/mediacontentmod"
|
||||
"91porn-server/models/v/mediamod"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
// acgScanner ACG 扫描策略:每个 Media 可能产生 1 个 ACG_MEDIA + N 个 ACG_CONTENT 命中
|
||||
// 仅匹配 title / summary / 章节名 / 章节内容(不检测 tag)
|
||||
type acgScanner struct{}
|
||||
|
||||
func (s *acgScanner) CountTotal() (int64, error) {
|
||||
return mediamod.CountForReview()
|
||||
}
|
||||
|
||||
func (s *acgScanner) ScanBatch(taskID, lastID primitive.ObjectID, terms []string) (*ScanResult, error) {
|
||||
batch, err := mediamod.FindForReviewBatch(lastID, scanBatchSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(batch) == 0 {
|
||||
return &ScanResult{Done: true}, nil
|
||||
}
|
||||
|
||||
issues := make([]*contentreviewmod.ReviewIssue, 0)
|
||||
for _, m := range batch {
|
||||
issues = append(issues, collectMediaIssues(taskID, m, terms)...)
|
||||
}
|
||||
|
||||
return &ScanResult{
|
||||
Issues: issues,
|
||||
Checked: int64(len(batch)),
|
||||
NextID: batch[len(batch)-1].ID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// collectMediaIssues 检测一个 Media 自身及其全部 MediaContent
|
||||
func collectMediaIssues(taskID primitive.ObjectID, m *mediamod.Media, terms []string) []*contentreviewmod.ReviewIssue {
|
||||
out := make([]*contentreviewmod.ReviewIssue, 0)
|
||||
|
||||
mb := newIssueBuilder(taskID, contentreviewmod.IssueTargetACGMedia, m.ID).
|
||||
MatchTitle("标题", m.Title, terms).
|
||||
MatchContent("内容", m.Summary, terms)
|
||||
if mb.HasHits() {
|
||||
out = append(out, mb.Build())
|
||||
}
|
||||
|
||||
contents, err := mediacontentmod.QueryAllList(bson.M{"mediaId": m.ID, "isActive": true})
|
||||
if err != nil {
|
||||
log.Warn("query media contents fail", log.Any("mid", m.ID), log.E(err))
|
||||
return out
|
||||
}
|
||||
for _, c := range contents {
|
||||
cb := newIssueBuilder(taskID, contentreviewmod.IssueTargetACGContent, c.ID).
|
||||
WithMediaID(m.ID).
|
||||
MatchTitle("章节名", c.Name, terms).
|
||||
MatchContent("章节内容", c.Text, terms)
|
||||
if cb.HasHits() {
|
||||
out = append(out, cb.Build())
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package contentreviewser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"91porn-server/models/v/contentreviewmod"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
// issueBuilder 用 Builder 模式构建 ReviewIssue:
|
||||
// 通过链式调用 MatchXxx 收集各字段的命中,HasHits/Build 在所有匹配后调用
|
||||
type issueBuilder struct {
|
||||
issue *contentreviewmod.ReviewIssue
|
||||
hitSet map[string]struct{}
|
||||
descLines []string
|
||||
}
|
||||
|
||||
func newIssueBuilder(taskID primitive.ObjectID, targetType contentreviewmod.IssueTargetType, targetID primitive.ObjectID) *issueBuilder {
|
||||
return &issueBuilder{
|
||||
issue: &contentreviewmod.ReviewIssue{
|
||||
TaskID: taskID,
|
||||
TargetType: targetType,
|
||||
TargetID: targetID,
|
||||
},
|
||||
hitSet: make(map[string]struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// WithMediaID 设置关联的 MediaID(仅 ACG_CONTENT 需要)
|
||||
func (b *issueBuilder) WithMediaID(id primitive.ObjectID) *issueBuilder {
|
||||
b.issue.MediaID = id
|
||||
return b
|
||||
}
|
||||
|
||||
// MatchTitle 匹配标题字段;命中时存入高亮版本并累计描述
|
||||
// label 是描述文案中的字段名(如"标题"、"章节名")
|
||||
func (b *issueBuilder) MatchTitle(label, text string, terms []string) *issueBuilder {
|
||||
if hits, hl := matchAndHighlight(text, terms); len(hits) > 0 {
|
||||
b.issue.Title = hl
|
||||
b.addHits(label, hits)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *issueBuilder) MatchContent(label, text string, terms []string) *issueBuilder {
|
||||
if hits, hl := matchAndHighlight(text, terms); len(hits) > 0 {
|
||||
b.issue.Content = hl
|
||||
b.addHits(label, hits)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *issueBuilder) MatchTags(label, text string, terms []string) *issueBuilder {
|
||||
if hits, hl := matchAndHighlight(text, terms); len(hits) > 0 {
|
||||
b.issue.Tags = hl
|
||||
b.addHits(label, hits)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *issueBuilder) MatchRichText(label, text string, terms []string) *issueBuilder {
|
||||
if hits, hl := matchAndHighlight(text, terms); len(hits) > 0 {
|
||||
b.issue.RichText = hl
|
||||
b.addHits(label, hits)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// HasHits 是否至少有一个字段命中
|
||||
func (b *issueBuilder) HasHits() bool {
|
||||
return len(b.hitSet) > 0
|
||||
}
|
||||
|
||||
// Build 收尾:拼装 Description 与去重后的 HitWords
|
||||
func (b *issueBuilder) Build() *contentreviewmod.ReviewIssue {
|
||||
hits := make([]string, 0, len(b.hitSet))
|
||||
for h := range b.hitSet {
|
||||
hits = append(hits, h)
|
||||
}
|
||||
b.issue.HitWords = hits
|
||||
b.issue.Description = strings.Join(b.descLines, "\n")
|
||||
return b.issue
|
||||
}
|
||||
|
||||
func (b *issueBuilder) addHits(label string, hits []string) {
|
||||
b.descLines = append(b.descLines, fmt.Sprintf("%s命中 %s", label, strings.Join(hits, "、")))
|
||||
for _, h := range hits {
|
||||
b.hitSet[h] = struct{}{}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package contentreviewser
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"91porn-server/models/v/contentreviewmod"
|
||||
)
|
||||
|
||||
const (
|
||||
markPrefix = "<mark>"
|
||||
markSuffix = "</mark>"
|
||||
)
|
||||
|
||||
// allTerms 把快照展开为一组待匹配的词
|
||||
func allTerms(snaps []contentreviewmod.SensitiveWordSnap) []string {
|
||||
seen := make(map[string]struct{})
|
||||
out := make([]string, 0, len(snaps))
|
||||
for _, s := range snaps {
|
||||
if s.Word == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[s.Word]; ok {
|
||||
continue
|
||||
}
|
||||
seen[s.Word] = struct{}{}
|
||||
out = append(out, s.Word)
|
||||
}
|
||||
// 长词优先,避免短词先替换破坏长词的匹配
|
||||
sort.Slice(out, func(i, j int) bool {
|
||||
return len(out[i]) > len(out[j])
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
// findHits 返回 input 中命中的词(去重,保持长→短顺序)
|
||||
func findHits(input string, terms []string) []string {
|
||||
if input == "" || len(terms) == 0 {
|
||||
return nil
|
||||
}
|
||||
hits := make([]string, 0)
|
||||
seen := make(map[string]struct{})
|
||||
for _, t := range terms {
|
||||
if t == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[t]; ok {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(input, t) {
|
||||
seen[t] = struct{}{}
|
||||
hits = append(hits, t)
|
||||
}
|
||||
}
|
||||
return hits
|
||||
}
|
||||
|
||||
// highlight 用 <mark>...</mark> 包裹命中的词
|
||||
// hits 已按长度降序;为避免再次包裹已 mark 的内容,分段替换
|
||||
func highlight(input string, hits []string) string {
|
||||
if input == "" || len(hits) == 0 {
|
||||
return input
|
||||
}
|
||||
result := input
|
||||
for _, h := range hits {
|
||||
if h == "" {
|
||||
continue
|
||||
}
|
||||
result = replaceNotInMark(result, h)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// replaceNotInMark 在 input 中把 term 全部替换为 <mark>term</mark>,
|
||||
// 但跳过已经包在 <mark>...</mark> 内的片段(避免嵌套)
|
||||
func replaceNotInMark(input, term string) string {
|
||||
if term == "" {
|
||||
return input
|
||||
}
|
||||
var b strings.Builder
|
||||
i := 0
|
||||
for i < len(input) {
|
||||
// 已经在 mark 块里,整段原样输出
|
||||
if strings.HasPrefix(input[i:], markPrefix) {
|
||||
end := strings.Index(input[i:], markSuffix)
|
||||
if end < 0 {
|
||||
b.WriteString(input[i:])
|
||||
return b.String()
|
||||
}
|
||||
b.WriteString(input[i : i+end+len(markSuffix)])
|
||||
i += end + len(markSuffix)
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(input[i:], term) {
|
||||
b.WriteString(markPrefix)
|
||||
b.WriteString(term)
|
||||
b.WriteString(markSuffix)
|
||||
i += len(term)
|
||||
continue
|
||||
}
|
||||
b.WriteByte(input[i])
|
||||
i++
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// matchAndHighlight 在 input 上一步到位:返回命中词列表 + 高亮后的文本
|
||||
func matchAndHighlight(input string, terms []string) ([]string, string) {
|
||||
hits := findHits(input, terms)
|
||||
if len(hits) == 0 {
|
||||
return nil, input
|
||||
}
|
||||
return hits, highlight(input, hits)
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package contentreviewser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime/debug"
|
||||
"sync/atomic"
|
||||
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/models/v/contentreviewmod"
|
||||
"91porn-server/models/v/sensitivewordmod"
|
||||
"91porn-server/skd/service/export_task"
|
||||
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api"
|
||||
)
|
||||
|
||||
// tgChatID 内容审查任务完成通知群(与导出任务复用同一群)
|
||||
const tgChatID int64 = -1003399433452
|
||||
|
||||
var runningFlag int32 // 进程内互斥:0=空闲 1=运行中
|
||||
|
||||
// TryRunPendingTask 调度入口,由 skd 的 cron 定时调用
|
||||
// skd 单进程,仅需 atomic 互斥防止 cron tick 重入;每次只挑一个最早的任务处理
|
||||
// 取到 pending 视为首跑;取到心跳超时的 running 视为续跑,从 lastTargetId 继续
|
||||
func TryRunPendingTask() {
|
||||
if !atomic.CompareAndSwapInt32(&runningFlag, 0, 1) {
|
||||
return
|
||||
}
|
||||
defer atomic.StoreInt32(&runningFlag, 0)
|
||||
|
||||
task, err := contentreviewmod.PickNextTask()
|
||||
if err != nil {
|
||||
log.Warn("content review: pick next fail", log.E(err))
|
||||
return
|
||||
}
|
||||
if task == nil {
|
||||
return
|
||||
}
|
||||
if task.ID.IsZero() {
|
||||
return
|
||||
}
|
||||
isResume := task.Status == contentreviewmod.TaskStatusRunning
|
||||
if err := contentreviewmod.AcquireTask(task.ID, isResume); err != nil {
|
||||
log.Warn("content review: acquire task fail", log.Any("taskId", task.ID), log.E(err))
|
||||
return
|
||||
}
|
||||
if isResume {
|
||||
log.Info("content review: resume stale task",
|
||||
log.Any("taskId", task.ID), log.Any("lastTargetId", task.LastTargetID))
|
||||
}
|
||||
|
||||
executeTask(task)
|
||||
}
|
||||
|
||||
// executeTask 真正执行扫描
|
||||
func executeTask(task *contentreviewmod.ReviewTask) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
stack := string(debug.Stack())
|
||||
log.Error("content review task panic",
|
||||
log.Any("taskId", task.ID), log.Any("panic", r), log.Any("stack", stack))
|
||||
_ = contentreviewmod.MarkFailed(task.ID, fmt.Sprintf("panic: %v", r))
|
||||
}
|
||||
}()
|
||||
|
||||
log.Info("content review task start",
|
||||
log.Any("taskId", task.ID), log.Any("type", task.TaskType))
|
||||
|
||||
checked, issues, err := runScan(task)
|
||||
if err != nil {
|
||||
log.Error("content review task fail",
|
||||
log.Any("taskId", task.ID), log.E(err))
|
||||
_ = contentreviewmod.MarkFailed(task.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := contentreviewmod.MarkFinished(task.ID, checked, issues); err != nil {
|
||||
log.Error("content review mark finished fail", log.E(err))
|
||||
return
|
||||
}
|
||||
|
||||
// 发送 TG
|
||||
if sendTGNotify(task, checked, issues) {
|
||||
_ = contentreviewmod.MarkTgSent(task.ID)
|
||||
}
|
||||
|
||||
log.Info("content review task done",
|
||||
log.Any("taskId", task.ID), log.Any("checked", checked), log.Any("issues", issues))
|
||||
}
|
||||
|
||||
func sendTGNotify(task *contentreviewmod.ReviewTask, checked, issues int64) bool {
|
||||
if export_task.Bot == nil {
|
||||
log.Warn("content review: tg bot not initialized, skip notify")
|
||||
return false
|
||||
}
|
||||
typeName := "视频帖子"
|
||||
if task.TaskType == contentreviewmod.TaskTypeACG {
|
||||
typeName = "ACG"
|
||||
}
|
||||
operator := task.Operator
|
||||
if operator == "" {
|
||||
operator = "-"
|
||||
}
|
||||
text := fmt.Sprintf(
|
||||
"【%s 内容检测任务完成】\n操作人: %s\n类型: %s\n任务ID: %s\n总记录: %d\n已检测: %d\n命中: %d",
|
||||
export_task.GetProName(), operator, typeName, task.ID.Hex(), task.TotalCount, checked, issues,
|
||||
)
|
||||
if _, err := export_task.Bot.Send(tgbotapi.NewMessage(tgChatID, text)); err != nil {
|
||||
log.Warn("content review: send tg fail", log.Any("taskId", task.ID), log.E(err))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// BuildSnapshot 拉取当前启用的敏感词作为快照(web 创建任务时复用)
|
||||
func BuildSnapshot() ([]contentreviewmod.SensitiveWordSnap, error) {
|
||||
enabled := sensitivewordmod.StatusEnabled
|
||||
list, err := sensitivewordmod.FindAll(&sensitivewordmod.ListReq{Status: &enabled})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]contentreviewmod.SensitiveWordSnap, 0, len(list))
|
||||
for _, w := range list {
|
||||
out = append(out, contentreviewmod.SensitiveWordSnap{
|
||||
Word: w.Word,
|
||||
Category: w.Category,
|
||||
})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package contentreviewser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/models/v/contentreviewmod"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
const scanBatchSize = 100
|
||||
|
||||
// ScanResult 一批扫描的产出
|
||||
type ScanResult struct {
|
||||
Issues []*contentreviewmod.ReviewIssue // 本批生成的命中记录
|
||||
Checked int64 // 本批实际扫描的目标数量
|
||||
NextID primitive.ObjectID // 下一批的游标
|
||||
Done bool // true 表示已扫完,runScan 退出循环
|
||||
}
|
||||
|
||||
// Scanner 内容审查扫描策略;具体任务类型实现各自的 ScanBatch
|
||||
type Scanner interface {
|
||||
CountTotal() (int64, error)
|
||||
ScanBatch(taskID, lastID primitive.ObjectID, terms []string) (*ScanResult, error)
|
||||
}
|
||||
|
||||
// newScanner Strategy 工厂:根据任务类型返回具体扫描器
|
||||
func newScanner(taskType string) (Scanner, error) {
|
||||
switch taskType {
|
||||
case contentreviewmod.TaskTypeVideo:
|
||||
return &videoScanner{}, nil
|
||||
case contentreviewmod.TaskTypeACG:
|
||||
return &acgScanner{}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported task type: %s", taskType)
|
||||
}
|
||||
}
|
||||
|
||||
// runScan Template Method:把"分页游标 + 进度回写 + 批量插入"骨架抽出,差异由 Scanner 提供
|
||||
// 支持从 task.LastTargetID 断点续跑
|
||||
func runScan(task *contentreviewmod.ReviewTask) (int64, int64, error) {
|
||||
scanner, err := newScanner(task.TaskType)
|
||||
if err != nil {
|
||||
return task.CheckedCount, task.IssueCount, err
|
||||
}
|
||||
|
||||
terms := allTerms(task.SnapshotWords)
|
||||
|
||||
total := task.TotalCount
|
||||
if total <= 0 {
|
||||
t, err := scanner.CountTotal()
|
||||
if err != nil {
|
||||
return task.CheckedCount, task.IssueCount, fmt.Errorf("count total: %w", err)
|
||||
}
|
||||
total = t
|
||||
}
|
||||
// 同步到内存对象,便于 executeTask 拼 TG 消息时正确取值
|
||||
task.TotalCount = total
|
||||
|
||||
lastID := task.LastTargetID
|
||||
checked := task.CheckedCount
|
||||
issues := task.IssueCount
|
||||
|
||||
flushProgress(task.ID, lastID, checked, issues, total)
|
||||
|
||||
for {
|
||||
result, err := scanner.ScanBatch(task.ID, lastID, terms)
|
||||
if err != nil {
|
||||
return checked, issues, fmt.Errorf("scan batch: %w", err)
|
||||
}
|
||||
if result.Done {
|
||||
break
|
||||
}
|
||||
if len(result.Issues) > 0 {
|
||||
if err := contentreviewmod.InsertIssues(result.Issues); err != nil {
|
||||
log.Warn("insert issues fail", log.Any("count", len(result.Issues)), log.E(err))
|
||||
} else {
|
||||
issues += int64(len(result.Issues))
|
||||
}
|
||||
}
|
||||
checked += result.Checked
|
||||
lastID = result.NextID
|
||||
|
||||
flushProgress(task.ID, lastID, checked, issues, total)
|
||||
}
|
||||
return checked, issues, nil
|
||||
}
|
||||
|
||||
func flushProgress(id, lastID primitive.ObjectID, checked, issues, total int64) {
|
||||
if err := contentreviewmod.UpdateProgress(id, lastID, checked, issues, total); err != nil {
|
||||
log.Warn("update progress fail", log.E(err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package contentreviewser
|
||||
|
||||
import (
|
||||
"91porn-server/models/v/contentreviewmod"
|
||||
"91porn-server/models/v/vidmod"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
// videoScanner 视频帖子扫描策略;匹配 title / content / richText(不检测 tag)
|
||||
type videoScanner struct{}
|
||||
|
||||
func (s *videoScanner) CountTotal() (int64, error) {
|
||||
return vidmod.CountForReview()
|
||||
}
|
||||
|
||||
func (s *videoScanner) ScanBatch(taskID, lastID primitive.ObjectID, terms []string) (*ScanResult, error) {
|
||||
batch, err := vidmod.FindForReviewBatch(lastID, scanBatchSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(batch) == 0 {
|
||||
return &ScanResult{Done: true}, nil
|
||||
}
|
||||
|
||||
issues := make([]*contentreviewmod.ReviewIssue, 0, len(batch))
|
||||
for _, v := range batch {
|
||||
b := newIssueBuilder(taskID, contentreviewmod.IssueTargetVideo, v.ID).
|
||||
MatchTitle("标题", v.Title, terms).
|
||||
MatchContent("内容", v.Content, terms).
|
||||
MatchRichText("富文本", v.RichText, terms)
|
||||
if b.HasHits() {
|
||||
issues = append(issues, b.Build())
|
||||
}
|
||||
}
|
||||
|
||||
return &ScanResult{
|
||||
Issues: issues,
|
||||
Checked: int64(len(batch)),
|
||||
NextID: batch[len(batch)-1].ID,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"91porn-server/common"
|
||||
"91porn-server/common/constant/redisconst"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/common/redis"
|
||||
"91porn-server/common/usertruth"
|
||||
"91porn-server/models/l/registermod"
|
||||
"91porn-server/models/v/usermod"
|
||||
"91porn-server/skd/skdg"
|
||||
)
|
||||
|
||||
const (
|
||||
RestartSignal string = "RESTART"
|
||||
NotifyKeyPrefix string = "notify:user:new:register:"
|
||||
TrimPrefix string = "notify:"
|
||||
)
|
||||
|
||||
var MessageCh = make(chan *redis.Message, 100)
|
||||
var ChStatus = make(chan string)
|
||||
|
||||
// DoRegister
|
||||
func DoRegister() {
|
||||
common.Go(func() { Sub(redisconst.Db0ExpiredChannel) })
|
||||
common.Go(func() {
|
||||
for {
|
||||
select {
|
||||
case v, ok := <-MessageCh:
|
||||
if !ok {
|
||||
log.Warn("MessChan is nil waiting some second")
|
||||
}
|
||||
if v != nil {
|
||||
LoadNewRegister(v.Payload)
|
||||
}
|
||||
case v, ok := <-ChStatus:
|
||||
if !ok {
|
||||
log.Warn("Restart signal is not receive")
|
||||
}
|
||||
if v == RestartSignal {
|
||||
Sub(redisconst.Db0ExpiredChannel)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 接收过期的键 将信息存入到数据库中 只处理指定的过期键 notify:user:new:register
|
||||
func LoadNewRegister(expiredKey interface{}) {
|
||||
if v, ok := expiredKey.(string); ok {
|
||||
if strings.HasPrefix(v, NotifyKeyPrefix) {
|
||||
common.Go(func() {
|
||||
key := strings.TrimPrefix(v, TrimPrefix)
|
||||
uidstr := strings.TrimPrefix(v, NotifyKeyPrefix)
|
||||
uid, _ := strconv.ParseUint(uidstr, 10, 64)
|
||||
log.Info("Expired Key And Handle Data", log.Any("expiredKey", expiredKey), log.Any("needHandleKey", key), log.Any("uid", uid))
|
||||
handleRegisterData(key, uid)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleRegisterData(key string, uid uint64) {
|
||||
fileds := skdg.Redis.Hkeys(key)
|
||||
//获取IP
|
||||
ip, _ := skdg.Redis.Hget(key, "ip")
|
||||
if len(fileds) > 0 {
|
||||
values, err := skdg.Redis.HMget(key, fileds)
|
||||
if err != nil {
|
||||
log.Warn("[Method] HandleRegisterData HMget Data from redis error", log.Any("key", key), log.E(err))
|
||||
}
|
||||
qCountMap := make(map[string]int64)
|
||||
var qsArray []registermod.QueryStat
|
||||
rgister := ®istermod.NewRegisterLog{}
|
||||
for i := 0; i < len(fileds); i++ {
|
||||
if v, ok := values[i].(string); ok {
|
||||
cnt, err := strconv.ParseInt(v, 10, 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
qCountMap[fileds[i]] = cnt
|
||||
qs := registermod.QueryStat{QueryUrl: fileds[i], Count: cnt}
|
||||
qsArray = append(qsArray, qs)
|
||||
}
|
||||
}
|
||||
_, _ = skdg.Redis.Del(key)
|
||||
rgister.CreatedAt = time.Now()
|
||||
rgister.UID = uid
|
||||
rgister.Query = qsArray
|
||||
rgister.IP = ip
|
||||
_ = registermod.InsertRegisterLog(rgister)
|
||||
score := usertruth.Score(qCountMap)
|
||||
//为区别默认值0 将计算出来的得分为0的情况 设置为-1
|
||||
if score == 0 {
|
||||
score = -1
|
||||
}
|
||||
_, _ = usermod.Update(uid, usermod.UserSelector{TrueScore: &score})
|
||||
log.Info("[Method] HandleRegisterData success", log.Any("key", key), log.Any("trueScore", score), log.Any("uid", uid))
|
||||
}
|
||||
}
|
||||
|
||||
// 订阅
|
||||
func Sub(channel string) {
|
||||
subpush, err := skdg.Redis.Subscribe(channel)
|
||||
if err != nil {
|
||||
ChStatus <- RestartSignal
|
||||
log.Warn(fmt.Sprintf("skd sever sub channel %s err", channel), log.E(err))
|
||||
}
|
||||
ch := subpush.Channel()
|
||||
for msg := range ch {
|
||||
message := &redis.Message{Channel: msg.Channel, Payload: msg.Payload}
|
||||
MessageCh <- message
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package export_task
|
||||
|
||||
import (
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/models/v/advanceordermod"
|
||||
"91porn-server/models/v/export_task_mod"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
func ExecExportAdvanceOrderTask(task *export_task_mod.ExportTask) (total int64, err error) {
|
||||
arg := advanceordermod.QueryAllCond{}
|
||||
err = json.Unmarshal([]byte(task.Param), &arg)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var skip int64
|
||||
var size int64 = 100
|
||||
index := 1
|
||||
path := fmt.Sprintf("./temp/%v%v", "预售订单列表-", task.ID.Hex())
|
||||
zipName := fmt.Sprintf("%v_%v_预售订单列表_%v.zip", task.Admin, GetProName(), task.CreatedAt.Format("2006-01-02_15:04:05"))
|
||||
var records []*advanceordermod.AdvanceOrderExport
|
||||
excelList := []string{}
|
||||
for {
|
||||
fmt.Println("skip", skip, " now ", time.Now().Format("2006-01-02 15:04:05"))
|
||||
opt := options.Find().SetSkip(skip).SetLimit(size).SetSort(bson.M{"createdAt": -1})
|
||||
list, err := advanceordermod.QueryAllDocument(arg.Filter(), opt)
|
||||
if err != nil {
|
||||
log.Error("ExecExportAdvanceOrderTask advanceordermod.QueryAllDocument fail", log.E(err))
|
||||
return total, err
|
||||
}
|
||||
for _, v := range list {
|
||||
item := &advanceordermod.AdvanceOrderExport{
|
||||
ID: v.ID,
|
||||
UID: v.UID,
|
||||
AdvOid: v.AdvOid,
|
||||
BalOid: v.BalOid,
|
||||
ProductID: v.ProductID,
|
||||
TotalAmount: v.TotalAmount,
|
||||
AdvanceAmount: v.AdvanceAmount,
|
||||
BalanceAmount: v.BalanceAmount,
|
||||
StartTime: v.StartTime,
|
||||
EndTime: v.EndTime,
|
||||
CreatedAt: v.CreatedAt,
|
||||
}
|
||||
switch v.Status {
|
||||
case advanceordermod.AdvanceProcessing:
|
||||
item.Status = "预付中"
|
||||
case advanceordermod.AdvanceSUCCESS:
|
||||
item.Status = "预付成功"
|
||||
case advanceordermod.BalanceProcessing:
|
||||
item.Status = "尾款预付中"
|
||||
case advanceordermod.BalanceSUCCESS:
|
||||
item.Status = "尾款预付成功"
|
||||
}
|
||||
records = append(records, item)
|
||||
}
|
||||
|
||||
var fileName = fmt.Sprintf("预售订单列表_%v.xlsx", index)
|
||||
if len(records) >= maxDataNum {
|
||||
// 获取到的数据已经达到了单文件最大限制数据量
|
||||
filePath, err := SaveExcel(records, path, fileName)
|
||||
if err != nil {
|
||||
log.Error("ExecExportAdvanceOrderTask SaveExcel fail", log.E(err))
|
||||
return total, err
|
||||
}
|
||||
// 重新归0
|
||||
records = []*advanceordermod.AdvanceOrderExport{}
|
||||
index = index + 1
|
||||
excelList = append(excelList, filePath)
|
||||
fmt.Println("index:", index)
|
||||
total = total + int64(len(records))
|
||||
} else if len(list) < int(size) {
|
||||
if len(records) > 0 {
|
||||
// 数据库已经取不到更多的数据了
|
||||
filePath, err := SaveExcel(records, path, fileName)
|
||||
if err != nil {
|
||||
log.Error("ExecExportAdvanceOrderTask SaveExcel fail", log.E(err))
|
||||
return total, err
|
||||
}
|
||||
excelList = append(excelList, filePath)
|
||||
total = total + int64(len(records))
|
||||
}
|
||||
// 结束循环
|
||||
break
|
||||
}
|
||||
skip = skip + size
|
||||
}
|
||||
|
||||
// 发送到tg
|
||||
err = SendTg(task.Admin, excelList, zipName)
|
||||
if err != nil {
|
||||
log.Error("ExecExportVideoTask SendTg fail", log.E(err))
|
||||
return
|
||||
}
|
||||
return total, nil
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
package export_task
|
||||
|
||||
import (
|
||||
"91porn-server/common/file"
|
||||
"91porn-server/common/log"
|
||||
commonWorker "91porn-server/common/worker"
|
||||
"91porn-server/models/v/export_task_mod"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/xuri/excelize/v2"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
)
|
||||
|
||||
const (
|
||||
proName = "91PORN"
|
||||
tgToken = "5852134617:AAEO0_GPZiUJS_LeR5dM7byEfOpnRg4dGqU"
|
||||
maxDataNum = 20000 // 每个excel存储的数据量
|
||||
maxWorkerNum = 3
|
||||
maxSize = 1024 * 1024 * 20
|
||||
)
|
||||
|
||||
var Bot *tgbotapi.BotAPI
|
||||
var worker *commonWorker.Worker
|
||||
|
||||
func init() {
|
||||
bot, err := tgbotapi.NewBotAPI(tgToken)
|
||||
if err != nil || bot == nil {
|
||||
log.Fatal("初始化tgbot失败", log.E(err))
|
||||
}
|
||||
Bot = bot
|
||||
worker = commonWorker.NewWorker(maxWorkerNum)
|
||||
}
|
||||
|
||||
var execExportTaskRuining bool
|
||||
|
||||
func ExecExportTask() {
|
||||
//防止重入
|
||||
if execExportTaskRuining {
|
||||
return
|
||||
}
|
||||
execExportTaskRuining = true
|
||||
defer func() {
|
||||
execExportTaskRuining = false
|
||||
}()
|
||||
// 获取任务列表
|
||||
list, err := export_task_mod.GetTaskList()
|
||||
if err != nil {
|
||||
log.Error("ExecExportTask export_task_mod.GetTaskList fail", log.E(err))
|
||||
return
|
||||
}
|
||||
for _, task := range list {
|
||||
switch task.Type {
|
||||
case export_task_mod.ExportUserTask:
|
||||
CallExportFunc(task, ExecExportUserTask)
|
||||
case export_task_mod.ExportVidTask:
|
||||
CallExportFunc(task, ExecExportVideoTask)
|
||||
case export_task_mod.ExportAdvanceOrderTask:
|
||||
CallExportFunc(task, ExecExportAdvanceOrderTask)
|
||||
case export_task_mod.ExportProductHistoryTask:
|
||||
CallExportFunc(task, ExecExportProductHistoryTask)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func GetProName() string {
|
||||
//if skdg.Conf.Base.Env == "test" {
|
||||
// return "测试环境_" + proName
|
||||
//}
|
||||
return proName
|
||||
}
|
||||
|
||||
func CallExportFunc(task *export_task_mod.ExportTask, f func(t *export_task_mod.ExportTask) (int64, error)) {
|
||||
if task == nil {
|
||||
return
|
||||
}
|
||||
worker.Exec(func() {
|
||||
// 已经执行过了
|
||||
if task.Status != 0 {
|
||||
return
|
||||
}
|
||||
//key := fmt.Sprintf("ExecExportTask:%v", task.ID.Hex())
|
||||
//ok, err := skdg.Redis.SetNX(key, 1, time.Minute*30)
|
||||
//if err != nil {
|
||||
// log.Error("ExecExportTask fail", log.E(err))
|
||||
// return
|
||||
//}
|
||||
//if !ok {
|
||||
// log.Info("ExportTask already executed")
|
||||
// return
|
||||
//}
|
||||
//defer skdg.Redis.Del(key)
|
||||
start := time.Now().Unix()
|
||||
total, err := f(task)
|
||||
if err != nil {
|
||||
log.Error("CallExportFunc fail", log.Any("task", task), log.E(err))
|
||||
// 修改任务信息
|
||||
export_task_mod.UpdateTask(task.ID, bson.M{
|
||||
"status": 2,
|
||||
"reason": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
// 修改任务信息
|
||||
export_task_mod.UpdateTask(task.ID, bson.M{
|
||||
"status": 1,
|
||||
"time": time.Now().Unix() - start, // 耗时
|
||||
"total": total,
|
||||
})
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// SendTg 发送到tg
|
||||
func SendTg(admin string, excelList []string, zipFileName string) (err error) {
|
||||
exportPath, err := filepath.Abs("temp")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// 尝试创建
|
||||
file.MakeDir(exportPath)
|
||||
zipList, err := Zip(excelList, zipFileName)
|
||||
if err != nil {
|
||||
log.Error("SendTg Zip fail", log.Any("excelList", excelList), log.Any("zipFileName", zipFileName), log.E(err))
|
||||
return
|
||||
}
|
||||
// 测试环境不需要发送到tg群
|
||||
//if skdg.Conf.Base.Env == "test" {
|
||||
// return
|
||||
//}
|
||||
if len(zipList) == 0 {
|
||||
return
|
||||
}
|
||||
var chatId int64 = -1003399433452
|
||||
|
||||
content := fmt.Sprintf("%v总共有%v个压缩包,请查收", admin, len(zipList))
|
||||
_, err = Bot.Send(tgbotapi.NewMessage(chatId, content))
|
||||
if err != nil {
|
||||
log.Error("tgbot send fail", log.E(err))
|
||||
return errors.Wrap(err, "发送TG消息失败")
|
||||
}
|
||||
for _, zipName := range zipList {
|
||||
fileBytes, err := os.ReadFile("./temp/" + zipName)
|
||||
if err != nil {
|
||||
log.Error("ReadFile fail", log.Any("excelList", excelList), log.Any("zipFileName", zipFileName), log.E(err))
|
||||
return errors.Wrap(err, "打开压缩文件失败")
|
||||
}
|
||||
|
||||
// 创建要发送的文件
|
||||
tFile := tgbotapi.FileBytes{Name: zipName, Bytes: fileBytes}
|
||||
|
||||
//生成csv发送到tg群
|
||||
newDocument := tgbotapi.NewDocumentUpload(chatId, tFile)
|
||||
_, err = Bot.Send(newDocument)
|
||||
if err != nil {
|
||||
log.Error("tgbot send fail", log.E(err))
|
||||
return errors.Wrap(err, "发送TG消息失败")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Zip(filePathList []string, zipFileName string) (zipList []string, err error) {
|
||||
if len(filePathList) == 0 {
|
||||
return
|
||||
}
|
||||
var currentSize int64
|
||||
zipIndex := 1
|
||||
currentZipName := fmt.Sprintf("%v_%v.zip", zipFileName, zipIndex)
|
||||
args := []string{currentZipName}
|
||||
for k, filePath := range filePathList {
|
||||
// 获取文件大小
|
||||
stat, err := os.Stat(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
currentSize = currentSize + stat.Size()
|
||||
args = append(args, filePath)
|
||||
// 判断当前所有文件加起来是否已经超过20M,超过就直接压缩,否则就接着等待压缩
|
||||
if currentSize < maxSize && k != (len(filePathList)-1) {
|
||||
continue
|
||||
}
|
||||
cmd1 := exec.Command("zip", args...)
|
||||
out1, err := cmd1.CombinedOutput()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fmt.Println(string(out1))
|
||||
// 移动文件到目录里
|
||||
cmd2 := exec.Command("mv", currentZipName, "./temp")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out2, err := cmd2.CombinedOutput()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fmt.Println(string(out2))
|
||||
zipList = append(zipList, currentZipName)
|
||||
// 重置
|
||||
zipIndex = zipIndex + 1
|
||||
currentZipName = fmt.Sprintf("%v_%v.zip", zipFileName, zipIndex)
|
||||
args = []string{currentZipName}
|
||||
currentSize = 0
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// SaveExcel 保存数据到excel中
|
||||
func SaveExcel(records interface{}, path string, fileName string) (filePath string, err error) {
|
||||
|
||||
sheet := "sheet1"
|
||||
xlsx := excelize.NewFile() // new file
|
||||
index, _ := xlsx.NewSheet(sheet) // new sheet
|
||||
xlsx.SetActiveSheet(index) // set active (default) sheet
|
||||
t := reflect.TypeOf(records)
|
||||
if t.Kind() != reflect.Slice {
|
||||
panic("records must be slice")
|
||||
}
|
||||
|
||||
s := reflect.ValueOf(records)
|
||||
for i := 0; i < s.Len(); i++ {
|
||||
elem := s.Index(i).Interface()
|
||||
elemType := reflect.TypeOf(elem)
|
||||
elemValue := reflect.ValueOf(elem)
|
||||
if elemType.Kind() == reflect.Ptr {
|
||||
elemType = elemType.Elem()
|
||||
elemValue = elemValue.Elem()
|
||||
}
|
||||
if elemType.Kind() != reflect.Struct {
|
||||
panic("record in slice must be a struct")
|
||||
}
|
||||
k := 0
|
||||
for j := 0; j < elemType.NumField(); j++ {
|
||||
field := elemType.Field(j)
|
||||
tag := field.Tag.Get("xlsx")
|
||||
if tag == "-" {
|
||||
continue
|
||||
}
|
||||
if tag == "" {
|
||||
tag = field.Name
|
||||
}
|
||||
column, _ := excelize.ColumnNumberToName(k + 1)
|
||||
k++
|
||||
name := tag
|
||||
// 设置表头
|
||||
if i == 0 {
|
||||
_ = xlsx.SetCellValue(sheet, fmt.Sprintf("%s%d", column, i+1), name)
|
||||
}
|
||||
// 判断是否是时间类型
|
||||
|
||||
colValue := elemValue.Field(j).Interface()
|
||||
if colTime, ok := colValue.(time.Time); ok {
|
||||
colValue = colTime.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
// 设置内容
|
||||
_ = xlsx.SetCellValue(sheet, fmt.Sprintf("%s%d", column, i+2), colValue)
|
||||
}
|
||||
}
|
||||
// 保存的目录看是否存在不存在就创建
|
||||
_, err = os.Stat(path)
|
||||
if os.IsNotExist(err) {
|
||||
err = os.MkdirAll(path, os.ModePerm)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
filePath = path + "/" + fileName
|
||||
err = xlsx.SaveAs(filePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filePath, nil
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package export_task
|
||||
|
||||
import (
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/models/v/advanceordermod"
|
||||
"91porn-server/models/v/export_task_mod"
|
||||
"91porn-server/models/v/prdcthsomod"
|
||||
"91porn-server/models/v/productmod"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
func ExecExportProductHistoryTask(task *export_task_mod.ExportTask) (total int64, err error) {
|
||||
arg := prdcthsomod.ProductHistoryQueryReq{}
|
||||
err = json.Unmarshal([]byte(task.Param), &arg)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var skip int64
|
||||
var size int64 = 100
|
||||
index := 1
|
||||
path := fmt.Sprintf("./temp/%v%v", "会员卡购买记录-", task.ID.Hex())
|
||||
zipName := fmt.Sprintf("%v_%v_会员卡购买记录_%v.zip", task.Admin, GetProName(), task.CreatedAt.Format("2006-01-02_15:04:05"))
|
||||
var records []*prdcthsomod.ProductHistoryExport
|
||||
excelList := []string{}
|
||||
for {
|
||||
fmt.Println("skip", skip, " now ", time.Now().Format("2006-01-02 15:04:05"))
|
||||
opt := options.Find().SetSkip(skip).SetLimit(size).SetSort(bson.M{"createdAt": -1})
|
||||
list, err := prdcthsomod.FindList(arg.GetCond(), opt)
|
||||
if err != nil {
|
||||
log.Error("ExecExportProductHistoryTask prdcthsomod.FindList fail", log.E(err))
|
||||
return total, err
|
||||
}
|
||||
for _, v := range list {
|
||||
item := &prdcthsomod.ProductHistoryExport{
|
||||
ID: v.ID, // 交易订单号
|
||||
UID: v.UID, // 用户id
|
||||
ProductID: v.ProductID, // 会员卡id
|
||||
Name: v.Name, // 商品名字
|
||||
IsUpgrade: v.IsUpgrade, // 是否VIP升级
|
||||
Amount: v.Amount, // 花费的余额
|
||||
Income: v.Income, // 花费的收益
|
||||
SysType: v.SysType, // 设备系统类型 ios pc android
|
||||
CreatedAt: v.CreatedAt,
|
||||
}
|
||||
if v.ProductSnapShot != nil {
|
||||
item.Duration = v.ProductSnapShot.Duration
|
||||
item.VipLevel = v.ProductSnapShot.VipLevel
|
||||
item.OriginalPrice = v.ProductSnapShot.OriginalPrice
|
||||
if v.ProductSnapShot.DiscountedPriceIos != nil {
|
||||
item.DiscountedPriceIos = *v.ProductSnapShot.DiscountedPriceIos
|
||||
}
|
||||
if v.ProductSnapShot.DiscountedPriceAnd != nil {
|
||||
item.DiscountedPriceAnd = *v.ProductSnapShot.DiscountedPriceAnd
|
||||
}
|
||||
}
|
||||
switch v.ProductType {
|
||||
case productmod.VIP:
|
||||
item.ProductType = "会员卡"
|
||||
case productmod.AdvanceCard:
|
||||
item.ProductType = "预售卡"
|
||||
}
|
||||
switch v.AdvanceOrderStatus {
|
||||
case advanceordermod.AdvanceSUCCESS:
|
||||
item.AdvanceOrderStatus = "预付成功"
|
||||
case advanceordermod.BalanceSUCCESS:
|
||||
item.AdvanceOrderStatus = "尾款支付成功"
|
||||
}
|
||||
records = append(records, item)
|
||||
}
|
||||
|
||||
var fileName = fmt.Sprintf("预售订单列表_%v.xlsx", index)
|
||||
if len(records) >= maxDataNum {
|
||||
// 获取到的数据已经达到了单文件最大限制数据量
|
||||
filePath, err := SaveExcel(records, path, fileName)
|
||||
if err != nil {
|
||||
log.Error("ExecExportProductHistoryTask SaveExcel fail", log.E(err))
|
||||
return total, err
|
||||
}
|
||||
// 重新归0
|
||||
records = []*prdcthsomod.ProductHistoryExport{}
|
||||
index = index + 1
|
||||
excelList = append(excelList, filePath)
|
||||
fmt.Println("index:", index)
|
||||
total = total + int64(len(records))
|
||||
} else if len(list) < int(size) {
|
||||
if len(records) > 0 {
|
||||
// 数据库已经取不到更多的数据了
|
||||
filePath, err := SaveExcel(records, path, fileName)
|
||||
if err != nil {
|
||||
log.Error("ExecExportProductHistoryTask SaveExcel fail", log.E(err))
|
||||
return total, err
|
||||
}
|
||||
excelList = append(excelList, filePath)
|
||||
total = total + int64(len(records))
|
||||
}
|
||||
// 结束循环
|
||||
break
|
||||
}
|
||||
skip = skip + size
|
||||
}
|
||||
|
||||
// 发送到tg
|
||||
err = SendTg(task.Admin, excelList, zipName)
|
||||
if err != nil {
|
||||
log.Error("ExecExportProductHistoryTask SendTg fail", log.E(err))
|
||||
return
|
||||
}
|
||||
return total, nil
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
package export_task
|
||||
|
||||
import (
|
||||
"91porn-server/common"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/models/l/playlgmod"
|
||||
"91porn-server/models/v/export_task_mod"
|
||||
"91porn-server/models/v/usermod"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// ExecExportUserTask 执行用户导出任务
|
||||
func ExecExportUserTask(task *export_task_mod.ExportTask) (total int64, err error) {
|
||||
type userListReq struct {
|
||||
usermod.UserListSelector
|
||||
common.StandQuery
|
||||
IsPretendAcc *int `form:"isPretendAcc" json:"isPretendAcc"` //是否马甲账号
|
||||
}
|
||||
var arg userListReq
|
||||
err = json.Unmarshal([]byte(task.Param), &arg)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if arg.StartTime == nil || arg.EndTime == nil {
|
||||
return 0, errors.New("没有选择时间")
|
||||
}
|
||||
cond, _ := common.StandQueryMap(arg.StandQuery, arg.UserListSelector)
|
||||
//马甲账户
|
||||
if arg.IsPretendAcc != nil && *arg.IsPretendAcc == 1 {
|
||||
cond["devID"] = bson.M{"$regex": usermod.SystemDevIDPrex, "$options": "i"}
|
||||
}
|
||||
var skip int64
|
||||
var size int64 = 500
|
||||
records := []usermod.ExportUser{}
|
||||
|
||||
zipName := fmt.Sprintf("%v_%v_用户列表_%v.zip", task.Admin, GetProName(), task.CreatedAt.Format("2006-01-02_15:04:05"))
|
||||
index := 1
|
||||
|
||||
path := fmt.Sprintf("./temp/%v%v", "用户列表-", task.ID.Hex())
|
||||
excelList := []string{}
|
||||
// 最后一条记录的时间
|
||||
for {
|
||||
userList, err := usermod.ExportFindMany(cond, skip, size)
|
||||
if err != nil {
|
||||
log.Error("ExecExportUserTask fail", log.E(err))
|
||||
return total, err
|
||||
}
|
||||
videoCntMap := make(map[uint64]int)
|
||||
unDealVideoCntMap := make(map[uint64]int)
|
||||
// 针对91PORN做的优化,不然数据太多,导出数据文本太大,速度慢
|
||||
//if arg.IsPretendAcc != nil && *arg.IsPretendAcc == 1 {
|
||||
// videoCntMap, unDealVideoCntMap, _ = vidmod.GetVideosByUIDs(getUIDs(userList))
|
||||
//}
|
||||
eUsers := encodeUsers(userList, videoCntMap, unDealVideoCntMap)
|
||||
|
||||
records = append(records, eUsers...)
|
||||
var fileName = fmt.Sprintf("用户列表_%v.xlsx", index)
|
||||
// 最多容纳5000条
|
||||
if len(records) >= maxDataNum {
|
||||
// 直接写入excel
|
||||
filePath, err := SaveExcel(records, path, fileName)
|
||||
if err != nil {
|
||||
log.Error("ExecExportVideoTask SaveExcel fail", log.E(err))
|
||||
return total, err
|
||||
}
|
||||
// 重置
|
||||
records = []usermod.ExportUser{}
|
||||
index = index + 1
|
||||
excelList = append(excelList, filePath)
|
||||
total = total + int64(len(records))
|
||||
} else if len(userList) < int(size) {
|
||||
if len(records) > 0 {
|
||||
// 直接写入excel
|
||||
filePath, err := SaveExcel(records, path, fileName)
|
||||
if err != nil {
|
||||
log.Error("ExecExportVideoTask SaveExcel fail", log.E(err))
|
||||
return total, err
|
||||
}
|
||||
excelList = append(excelList, filePath)
|
||||
total = total + int64(len(records))
|
||||
}
|
||||
// 已经没有更多数据了,直接返回
|
||||
break
|
||||
}
|
||||
|
||||
skip = skip + size
|
||||
}
|
||||
// 发送到tg
|
||||
err = SendTg(task.Admin, excelList, zipName)
|
||||
if err != nil {
|
||||
log.Error("ExecExportUserTask SendTg fail", log.E(err))
|
||||
return
|
||||
}
|
||||
return total, nil
|
||||
}
|
||||
|
||||
func encodeUsers(users []*usermod.User, videoCntMap map[uint64]int, unDealVideoCntMap map[uint64]int) []usermod.ExportUser {
|
||||
usersLen := len(users)
|
||||
uids := make([]uint64, 0, usersLen)
|
||||
uInfos := make([]usermod.ExportUser, 0, usersLen)
|
||||
for _, u := range users {
|
||||
if u != nil {
|
||||
user := usermod.ExportUser{
|
||||
UID: u.UID,
|
||||
//DevID: u.DevID,
|
||||
DevType: u.DevType,
|
||||
RegisterIP: u.RegisterIP,
|
||||
Mobile: u.Mobile,
|
||||
//Gender: u.Gender,
|
||||
Channel: u.DistrictCode,
|
||||
//Name: u.Name,
|
||||
PromotionCode: u.PromCode,
|
||||
//Summary: u.Summary,
|
||||
//Region: u.Region,
|
||||
//Birthday: u.Birthday,
|
||||
//VipLevel: u.VipLevel,
|
||||
VipExpireDate: u.VipExpireDate,
|
||||
CreatedAt: u.CreatedAt,
|
||||
//MobileBindAt: u.MobileBindAt,
|
||||
//HasLocked: u.HasLocked,
|
||||
//HasBanned: u.HasBanned,
|
||||
//TotalVideoCnt: videoCntMap[u.UID],
|
||||
//UndealVideoCnt: unDealVideoCntMap[u.UID],
|
||||
LastVisitAt: u.LastVisitAt,
|
||||
}
|
||||
uInfos = append(uInfos, user)
|
||||
uids = append(uids, u.UID)
|
||||
}
|
||||
}
|
||||
wCnt := setArray2Map(calWatchedVideoCnt(uids))
|
||||
for i, u := range uInfos {
|
||||
uInfos[i].WatchCount = wCnt[u.UID]
|
||||
}
|
||||
return uInfos
|
||||
}
|
||||
|
||||
func getUIDs(users []*usermod.User) []uint64 {
|
||||
uids := make([]uint64, 0, len(users))
|
||||
for _, v := range users {
|
||||
if v == nil {
|
||||
continue
|
||||
}
|
||||
uids = append(uids, v.UID)
|
||||
}
|
||||
return uids
|
||||
}
|
||||
|
||||
func setArray2Map(datas []playlgmod.UIDCount) map[uint64]int {
|
||||
m := make(map[uint64]int)
|
||||
for _, d := range datas {
|
||||
m[d.UID] = d.Count
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func calWatchedVideoCnt(uids []uint64) []playlgmod.UIDCount {
|
||||
le := len(uids)
|
||||
if le < 1000 {
|
||||
datas, _ := playlgmod.HasWatchedVideoCnt(uids)
|
||||
return datas
|
||||
}
|
||||
//创建多个协程去拉取
|
||||
const goRoutineCnt = 10
|
||||
var wg sync.WaitGroup
|
||||
var mData [goRoutineCnt][]playlgmod.UIDCount
|
||||
size := le / goRoutineCnt
|
||||
wg.Add(goRoutineCnt)
|
||||
for i := 0; i < goRoutineCnt-1; i++ {
|
||||
common.GoParam(i, func(i int) {
|
||||
defer wg.Done()
|
||||
begin := i * size
|
||||
end := begin + size - 1
|
||||
mData[i], _ = playlgmod.HasWatchedVideoCnt(uids[begin:end])
|
||||
})
|
||||
}
|
||||
common.Go(func() {
|
||||
defer wg.Done()
|
||||
begin := (goRoutineCnt - 1) * size
|
||||
end := le - 1
|
||||
mData[goRoutineCnt-1], _ = playlgmod.HasWatchedVideoCnt(uids[begin:end])
|
||||
})
|
||||
wg.Wait()
|
||||
datas := []playlgmod.UIDCount{}
|
||||
for i := 0; i < goRoutineCnt; i++ {
|
||||
datas = append(datas, mData[i]...)
|
||||
}
|
||||
return datas
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
package export_task
|
||||
|
||||
import (
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/models/v/export_task_mod"
|
||||
"91porn-server/models/v/modulevidmod"
|
||||
"91porn-server/models/v/tagmod"
|
||||
"91porn-server/models/v/vidmod"
|
||||
"91porn-server/web/vidhelp"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"time"
|
||||
)
|
||||
|
||||
func ExecExportVideoTask(task *export_task_mod.ExportTask) (total int64, err error) {
|
||||
arg := vidmod.ListReq{}
|
||||
err = json.Unmarshal([]byte(task.Param), &arg)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
cond, err := assembleConditions(arg)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
secID, cond, _, err := dealVidListCond(cond)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var skip int64
|
||||
var size int64 = 100
|
||||
index := 1
|
||||
path := fmt.Sprintf("./temp/%v%v", "帖子列表-", task.ID.Hex())
|
||||
zipName := fmt.Sprintf("%v_%v_帖子列表_%v.zip", task.Admin, GetProName(), task.CreatedAt.Format("2006-01-02_15:04:05"))
|
||||
var records []*vidmod.WebVideo
|
||||
excelList := []string{}
|
||||
for {
|
||||
fmt.Println("skip", skip, " now ", time.Now().Format("2006-01-02 15:04:05"))
|
||||
list, err := vidmod.ExportFindMany(cond, skip, size)
|
||||
if err != nil {
|
||||
log.Error("ExecExportVideoTask vidmod.ExportFindMany fail", log.E(err))
|
||||
return total, err
|
||||
}
|
||||
infos, err := vidhelp.EncodeVideoInfo(list, secID)
|
||||
if err != nil {
|
||||
log.Error("ExecExportVideoTask vidhelp.EncodeVideoInfo fail", log.E(err))
|
||||
return total, err
|
||||
}
|
||||
records = append(records, infos...)
|
||||
var fileName = fmt.Sprintf("帖子列表_%v.xlsx", index)
|
||||
if len(records) >= maxDataNum {
|
||||
// 获取到的数据已经达到了单文件最大限制数据量
|
||||
filePath, err := SaveExcel(records, path, fileName)
|
||||
if err != nil {
|
||||
log.Error("ExecExportVideoTask SaveExcel fail", log.E(err))
|
||||
return total, err
|
||||
}
|
||||
// 重新归0
|
||||
records = []*vidmod.WebVideo{}
|
||||
index = index + 1
|
||||
excelList = append(excelList, filePath)
|
||||
fmt.Println("index:", index)
|
||||
total = total + int64(len(records))
|
||||
} else if len(list) < int(size) {
|
||||
if len(records) > 0 {
|
||||
// 数据库已经取不到更多的数据了
|
||||
filePath, err := SaveExcel(records, path, fileName)
|
||||
if err != nil {
|
||||
log.Error("ExecExportVideoTask SaveExcel fail", log.E(err))
|
||||
return total, err
|
||||
}
|
||||
excelList = append(excelList, filePath)
|
||||
total = total + int64(len(records))
|
||||
}
|
||||
// 结束循环
|
||||
break
|
||||
}
|
||||
skip = skip + size
|
||||
}
|
||||
|
||||
// 发送到tg
|
||||
err = SendTg(task.Admin, excelList, zipName)
|
||||
if err != nil {
|
||||
log.Error("ExecExportVideoTask SendTg fail", log.E(err))
|
||||
return
|
||||
}
|
||||
return total, nil
|
||||
}
|
||||
|
||||
func dealVidListCond(cond bson.M) (primitive.ObjectID, bson.M, []primitive.ObjectID, error) {
|
||||
var secID primitive.ObjectID
|
||||
var err error
|
||||
sectionID, ok := cond["sectionID"]
|
||||
if ok {
|
||||
secID, err = primitive.ObjectIDFromHex(sectionID.(string))
|
||||
if err != nil {
|
||||
log.Error("primitive.ObjectIDFromHex", log.Any("sectionID", sectionID), log.E(err))
|
||||
return secID, cond, nil, err
|
||||
}
|
||||
var flag bool
|
||||
isSorted, ok := cond["isSortedUnderModule"]
|
||||
if ok {
|
||||
flag = isSorted.(bool)
|
||||
}
|
||||
videoIDs, err := modulevidmod.GetBySectionID(secID, flag)
|
||||
if err != nil {
|
||||
return secID, cond, nil, err
|
||||
}
|
||||
cond["_id"] = bson.M{"$in": videoIDs}
|
||||
delete(cond, "sectionID")
|
||||
delete(cond, "isSortedUnderModule")
|
||||
return secID, cond, videoIDs, nil
|
||||
}
|
||||
delete(cond, "sectionID")
|
||||
delete(cond, "isSortedUnderModule")
|
||||
return secID, cond, nil, nil
|
||||
}
|
||||
|
||||
func assembleConditions(req vidmod.ListReq) (m map[string]interface{}, err error) {
|
||||
m = make(map[string]interface{})
|
||||
if req.ShowType != nil {
|
||||
m["showType"] = *req.ShowType
|
||||
}
|
||||
if req.Status != 4 {
|
||||
if req.Status == 7 {
|
||||
m["status"] = map[string]int{"$gt": 0}
|
||||
} else {
|
||||
m["status"] = req.Status
|
||||
}
|
||||
}
|
||||
if req.IsFree == 0 {
|
||||
m["coins"] = map[string]int{"$gt": 0}
|
||||
}
|
||||
if req.IsFree == 1 {
|
||||
m["coins"] = map[string]int{"$eq": 0}
|
||||
}
|
||||
if req.IsUserUp == 2 {
|
||||
m["publisherID"] = map[string]int{"$lt": 115000}
|
||||
}
|
||||
if req.IsUserUp == 1 {
|
||||
m["publisherID"] = map[string]int{"$gt": 115000}
|
||||
}
|
||||
if len(req.Title) != 0 {
|
||||
m["title"] = map[string]string{"$regex": req.Title, "$options": "i"}
|
||||
}
|
||||
if req.UID > 0 {
|
||||
m["publisherID"] = req.UID
|
||||
}
|
||||
if req.Chosen == 1 {
|
||||
m["chosen"] = true
|
||||
}
|
||||
if req.Chosen == 2 {
|
||||
m["chosen"] = false
|
||||
}
|
||||
if req.FreeArea == 1 {
|
||||
m["freeArea"] = true
|
||||
}
|
||||
if req.FreeArea == 2 {
|
||||
m["freeArea"] = false
|
||||
}
|
||||
if !req.End.IsZero() {
|
||||
m["createdAt"] = map[string]time.Time{"$gte": req.Start, "$lt": req.End}
|
||||
}
|
||||
if len(req.Tag) != 0 {
|
||||
id, err := tagmod.GetTagIDByName(req.Tag)
|
||||
if err != nil {
|
||||
return m, err
|
||||
}
|
||||
m["tags"] = id
|
||||
}
|
||||
if len(req.ID) != 0 {
|
||||
oid, err := primitive.ObjectIDFromHex(req.ID)
|
||||
if err != nil {
|
||||
return m, err
|
||||
}
|
||||
m["_id"] = oid
|
||||
}
|
||||
//通过初始价格判断是否是马甲账号
|
||||
if req.IsPretendAcc == 1 {
|
||||
m["coins"] = vidmod.PretendAccInitCoins
|
||||
}
|
||||
if req.IsPretendAcc == 2 {
|
||||
//i := make(map[string]int64)
|
||||
//i["$ne"] = vidmod.PretendAccInitCoins
|
||||
m["coins"] = map[string]int64{"$ne": vidmod.PretendAccInitCoins}
|
||||
}
|
||||
m["deleteAt"] = bson.M{"$exists": false}
|
||||
if req.NewsType != "" {
|
||||
m["newsType"] = req.NewsType
|
||||
}
|
||||
|
||||
if req.LiaoBaTop != nil {
|
||||
m["liaoBaTop"] = *req.LiaoBaTop
|
||||
}
|
||||
if req.SectionID != "" {
|
||||
m["sectionID"] = req.SectionID
|
||||
}
|
||||
m["isSortedUnderModule"] = req.IsSortedUnderModule
|
||||
if req.IsRecommended != nil {
|
||||
if *req.IsRecommended {
|
||||
m["recoWeight"] = bson.M{"$gte": 0}
|
||||
} else {
|
||||
m["recoWeight"] = bson.M{"$lt": 0}
|
||||
}
|
||||
}
|
||||
if req.Key == "likeRate" || req.Key == "purchaseRate" {
|
||||
m[req.Key] = req.Value
|
||||
}
|
||||
if req.IsHappinessPlazaTop != nil && *req.IsHappinessPlazaTop {
|
||||
if *req.IsHappinessPlazaTop {
|
||||
m["happinessPlazaTop"] = bson.M{"$gt": 0}
|
||||
} else {
|
||||
m["happinessPlazaTop"] = bson.M{"$lte": 0}
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"91porn-server/common"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/common/stderr"
|
||||
"91porn-server/models/v/chatrobotmod"
|
||||
"91porn-server/models/v/vidmod"
|
||||
|
||||
"github.com/robfig/cron/v3"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
)
|
||||
|
||||
const SingleQueryLimit = 5000
|
||||
|
||||
var VideoInfo *VideoInfoFaker
|
||||
|
||||
type VideoInfoFaker struct {
|
||||
once sync.Once
|
||||
limit int64 // 限制每次查询的数据量
|
||||
mu *sync.Mutex
|
||||
}
|
||||
|
||||
func newVideoInfoFaker() *VideoInfoFaker {
|
||||
return &VideoInfoFaker{
|
||||
once: sync.Once{},
|
||||
limit: SingleQueryLimit,
|
||||
mu: &sync.Mutex{},
|
||||
}
|
||||
}
|
||||
|
||||
// FullUpdate 单次更新,程序启动后执行一次
|
||||
func (f *VideoInfoFaker) FullUpdate() {
|
||||
f.once.Do(
|
||||
func() {
|
||||
f.mu.Lock()
|
||||
log.Debug("full update start...")
|
||||
common.Go(func() {
|
||||
defer f.mu.Unlock()
|
||||
start := time.Now()
|
||||
totalUpdate, err := f.fakeVideoInfo()
|
||||
log.Info("FullUpdate fihish",
|
||||
log.Any("time(s)", time.Since(start).Seconds()),
|
||||
log.Any("total", totalUpdate),
|
||||
log.E(err))
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// CronUpdate 定时更新
|
||||
func (f *VideoInfoFaker) CronUpdate() {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
start := time.Now()
|
||||
totalUpdate, err := f.fakeVideoInfo()
|
||||
log.Info("CronUpdate fihish",
|
||||
log.Any("time(s)", time.Since(start).Seconds()),
|
||||
log.Any("total", totalUpdate),
|
||||
log.E(err))
|
||||
}
|
||||
|
||||
func (f *VideoInfoFaker) RegisterCronJob(cron *cron.Cron) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
_, _ = cron.AddFunc("0 0 10 */1 * *", f.CronUpdate) // 每隔三天,早上10点,仅在周一到周六执行
|
||||
}
|
||||
|
||||
// FakeVideoInfo 更新视频相关数据
|
||||
func (f *VideoInfoFaker) fakeVideoInfo() (int64, error) {
|
||||
robotConf, err := chatrobotmod.GetRobotConfByType(chatrobotmod.Comment)
|
||||
if err != nil || robotConf.ID.IsZero() {
|
||||
return 0, err
|
||||
}
|
||||
if (robotConf.MaxFakeLikes <= 0 && robotConf.MaxFakePlayCount <= 0) ||
|
||||
(robotConf.MinFakeLikes >= robotConf.MaxFakeLikes &&
|
||||
robotConf.MinFakePlayCount >= robotConf.MaxFakePlayCount) {
|
||||
return 0, stderr.RobotInvalidConf
|
||||
}
|
||||
ticker := time.NewTicker(time.Second)
|
||||
defer ticker.Stop()
|
||||
pageSize, page, totalUpdate := f.limit, int64(0), int64(0)
|
||||
for pageSize == f.limit {
|
||||
videoList, err := vidmod.GetVideoListLtMinPlayLikeCount(int64(robotConf.MinFakeLikes),
|
||||
int64(robotConf.MinFakePlayCount), page*pageSize, pageSize)
|
||||
if err != nil {
|
||||
return totalUpdate, err
|
||||
}
|
||||
cnt := int64(0)
|
||||
for _, video := range videoList {
|
||||
set := bson.M{}
|
||||
if video.FakeLikeCount < robotConf.MinFakeLikes {
|
||||
destLike := common.RandInt(robotConf.MinFakeLikes, robotConf.MaxFakeLikes)
|
||||
set["fakeLikeCount"] = destLike
|
||||
}
|
||||
if video.FakePlayCount < robotConf.MinFakePlayCount {
|
||||
destPlay := common.RandInt(robotConf.MinFakePlayCount, robotConf.MaxFakePlayCount)
|
||||
video.FakePlayCount = destPlay
|
||||
set["fakePlayCount"] = destPlay
|
||||
}
|
||||
if set["fakePlayCount"] != 0 || set["fakeLikeCount"] != 0 {
|
||||
// log.Info("update", log.Any("set", set), log.Any("vid", video.ID), log.Any("origin like", video.FakeLikeCount), log.Any("origin play", video.PlayCount))
|
||||
res, err := vidmod.UpdateOneByID(video.ID, set)
|
||||
if err != nil {
|
||||
// 遇到错误继续执行
|
||||
log.Error("fakeVideoInfo UpdateOneByID", log.Any("videoID", video.ID), log.E(err))
|
||||
} else {
|
||||
cnt += res.ModifiedCount
|
||||
}
|
||||
}
|
||||
}
|
||||
page++
|
||||
pageSize = int64(len(videoList))
|
||||
totalUpdate += cnt
|
||||
log.Info("fake progress", log.Any("page", page), log.Any("total", totalUpdate))
|
||||
<-ticker.C
|
||||
}
|
||||
return totalUpdate, nil
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package service
|
||||
|
||||
func Init() {
|
||||
VideoInfo = newVideoInfoFaker()
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"91porn-server/common"
|
||||
"91porn-server/common/constant/redisconst"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/models/v/usermod"
|
||||
"91porn-server/skd/skdg"
|
||||
)
|
||||
|
||||
// 填充推广码
|
||||
func MakePromotionCode() {
|
||||
redisKey := redisconst.PromotionCodeKey()
|
||||
var low int64 = 5000
|
||||
total, err := skdg.Redis.SCard(redisKey)
|
||||
if err != nil {
|
||||
log.Warn("service MakePromotionCode SCard error", log.E(err), log.Any("total", total))
|
||||
return
|
||||
}
|
||||
if total < low {
|
||||
savePromotionCode(total)
|
||||
}
|
||||
}
|
||||
|
||||
func savePromotionCode(total int64) {
|
||||
var max int64 = 9999
|
||||
size := 100
|
||||
redisKey := redisconst.PromotionCodeKey()
|
||||
if total < max {
|
||||
promotionCodeCollection := make([]string, size)
|
||||
for i := 0; i < size; i++ {
|
||||
p, err := getPromotionCode()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
promotionCodeCollection[i] = p
|
||||
}
|
||||
count, err := skdg.Redis.SAdd(redisKey, promotionCodeCollection)
|
||||
if err != nil {
|
||||
log.Warn("service savePromotionCode SAdd error", log.E(err), log.Any("total", total))
|
||||
return
|
||||
}
|
||||
savePromotionCode(total + count)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取不重复的推广码
|
||||
func getPromotionCode() (string, error) {
|
||||
promotionCode := common.InvitePromotionCodeGenera()
|
||||
u, err := usermod.FindUserPromotionCode(promotionCode)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if u == nil {
|
||||
return promotionCode, nil
|
||||
}
|
||||
return getPromotionCode()
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"91porn-server/common"
|
||||
"91porn-server/common/httputil"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/common/stderr"
|
||||
"91porn-server/models/v/vidmod"
|
||||
"91porn-server/skd/skdg"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
// SyncFileFromFs 从文件服务器同步文件信息
|
||||
func SyncFileFromFs(vID string) (code stderr.Code) {
|
||||
v, _ := vidmod.GetVideoInfo(vID)
|
||||
resp, err := PullFileInfo(v.SourceID)
|
||||
if err != nil {
|
||||
return stderr.ErrConnectToFs
|
||||
}
|
||||
if resp.Code == stderr.UpLoadFileComplete {
|
||||
oid, _ := primitive.ObjectIDFromHex(resp.Data.ID)
|
||||
sourceID := resp.Data.ID
|
||||
playTime := resp.Data.PlayTime
|
||||
freeTime := vidmod.GetFreeTime(resp.Data.PlayTime)
|
||||
if v.FreeTime != 0 && freeTime != v.FreeTime {
|
||||
freeTime = v.FreeTime
|
||||
}
|
||||
seriesCover := v.SeriesCover
|
||||
resolution := strconv.FormatInt(int64(resp.Data.Width), 10) + "*" + strconv.FormatInt(int64(resp.Data.Height), 10)
|
||||
width := resp.Data.Width
|
||||
height := resp.Data.Height
|
||||
identifi := common.IdentifiVideoInfo(int64(resp.Data.Width), int64(resp.Data.Height))
|
||||
direction := string(identifi.Direction)
|
||||
quality := string(identifi.Quality)
|
||||
md5 := resp.Data.CheckSum
|
||||
actor := strings.Join(resp.Data.Actors, ",")
|
||||
size := resp.Data.Size
|
||||
filename := resp.Data.Filename
|
||||
via := resp.Data.Via
|
||||
ratio := resp.Data.Ratio
|
||||
update := vidmod.WebVideoUpdateDoc{
|
||||
ID: oid,
|
||||
SourceID: &sourceID,
|
||||
PlayTime: &playTime,
|
||||
FreeTime: &freeTime,
|
||||
SeriesCover: &seriesCover,
|
||||
Resolution: &resolution,
|
||||
Width: &width,
|
||||
Height: &height,
|
||||
MD5: &md5,
|
||||
Actor: &actor,
|
||||
Size: &size,
|
||||
Filename: &filename,
|
||||
Via: &via,
|
||||
Ratio: &ratio,
|
||||
Direction: &direction,
|
||||
Quality: &quality,
|
||||
}
|
||||
if v.Cover == "" {
|
||||
cover := resp.Data.FieldNameFs + "-1.jpg"
|
||||
update.Cover = &cover
|
||||
}
|
||||
if v.CoverThumb == "" {
|
||||
coverThumb := resp.Data.FieldNameFs + "-2.jpg"
|
||||
update.CoverThumb = &coverThumb
|
||||
}
|
||||
_, _ = vidmod.UpdateVideoResolutionPlayTime(v.ID, update)
|
||||
}
|
||||
return resp.Code
|
||||
}
|
||||
|
||||
// PullFileInfo 获取文件转码情况及详情
|
||||
func PullFileInfo(id string) (respBody vidmod.PuFinfoResp, err error) {
|
||||
client := httputil.Client()
|
||||
code, err := client.POSTWithJResp(&respBody, common.BindUrl(skdg.Conf.Url.PullFileInfo, id), nil, nil)
|
||||
log.Info("http method PullFileInfo response code ==>", log.Any("statusCode", code), log.Any("respCode", respBody.Code))
|
||||
if err != nil {
|
||||
log.Error("Pull FileInfo From Aws ", log.Any("Error", err))
|
||||
respBody.Code = stderr.ErrConnectToFs
|
||||
return
|
||||
}
|
||||
switch respBody.Data.Status {
|
||||
case vidmod.Merging, vidmod.MergeCompleted:
|
||||
respBody.Code = stderr.FileMerging
|
||||
case vidmod.MergeError:
|
||||
respBody.Code = stderr.ErrMergeFile
|
||||
case vidmod.Converting:
|
||||
respBody.Code = stderr.FileConverting
|
||||
case vidmod.ConvertError:
|
||||
respBody.Code = stderr.ErrConvertFile
|
||||
case vidmod.UploadLoadingToFs:
|
||||
respBody.Code = stderr.UploadLoadingToFs
|
||||
case vidmod.FileUploadError:
|
||||
respBody.Code = stderr.ErrUploadError
|
||||
case vidmod.ConvertCompleted, vidmod.Completed:
|
||||
respBody.Code = stderr.UpLoadFileComplete
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"91porn-server/common"
|
||||
"91porn-server/common/crypt"
|
||||
)
|
||||
|
||||
func GetTFUrl(tf string) string {
|
||||
if tf != "" {
|
||||
type tfStc struct {
|
||||
Url string `json:"url"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
var tfArr []tfStc
|
||||
_ = crypt.JSON2Struct(tf, &tfArr)
|
||||
if len(tfArr) == 0 {
|
||||
return tf
|
||||
}
|
||||
weight := 100 / len(tfArr)
|
||||
chioce := make([]common.Choice, len(tfArr))
|
||||
for i := range tfArr {
|
||||
chioce[i] = common.Choice{
|
||||
Weight: weight,
|
||||
Item: tfArr[i].Url,
|
||||
}
|
||||
}
|
||||
ch, _ := common.WeightedChoice(chioce)
|
||||
if v, ok := ch.Item.(string); ok {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return tf
|
||||
}
|
||||
Reference in New Issue
Block a user