Initial commit

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 13:57:10 +08:00
co-authored by Claude Opus 5
commit 8679200f41
1897 changed files with 257900 additions and 0 deletions
+218
View File
@@ -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
}
+68
View File
@@ -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{}{}
}
}
+114
View File
@@ -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)
}
+129
View File
@@ -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
}
+95
View File
@@ -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))
}
}
+42
View File
@@ -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
}
+119
View File
@@ -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 := &registermod.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
}
+282
View File
@@ -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
}
+192
View File
@@ -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
}
+219
View File
@@ -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
}
+122
View File
@@ -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
}
+5
View File
@@ -0,0 +1,5 @@
package service
func Init() {
VideoInfo = newVideoInfoFaker()
}
+58
View File
@@ -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()
}
+102
View File
@@ -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
}
+33
View File
@@ -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
}