Files
rootandClaude Opus 5 8679200f41 Initial commit
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 13:57:10 +08:00

609 lines
19 KiB
Go
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package mediamod
import (
"91porn-server/models"
"encoding/json"
"errors"
"fmt"
"time"
"91porn-server/common/db"
"91porn-server/common/log"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
const table = models.Media
func coll(t *db.MongoTool) *db.MongoTool {
if t == nil {
return mdb.Coll(table)
}
return t.Coll(table)
}
func initIndex() {
many := []mongo.IndexModel{
{
Keys: bson.D{{Key: "lsjId", Value: 1}},
},
{
Keys: bson.D{{Key: "sId", Value: 1}, {Key: "sortCode", Value: -1}, {Key: "createdAt", Value: -1}},
},
{
Keys: bson.D{{Key: "sId", Value: 1}, {Key: "sectionSort", Value: -1}, {Key: "createdAt", Value: -1}},
},
{
Keys: bson.D{{Key: "tags", Value: 1}},
}, {
Keys: bson.D{{Key: "tags", Value: 1}, {"isDelete", 1}, {"status", 1}, {"hot", -1}, {"createdAt", -1}},
}, {
Keys: bson.D{{Key: "tags", Value: 1}, {"isDelete", 1}, {"status", 1}, {"countCollect", -1}, {"createdAt", -1}},
}, {
Keys: bson.D{{Key: "tags", Value: 1}, {"isDelete", 1}, {"status", 1}, {"countBrowse", -1}, {"createdAt", -1}},
}, {
Keys: bson.D{{Key: "tags", Value: 1}, {"isDelete", 1}, {"status", 1}, {"createdAt", -1}},
},
{
Keys: bson.D{{Key: "kind", Value: 1}},
},
{
Keys: bson.D{{Key: "isActive", Value: 1}},
},
{
Keys: bson.D{{Key: "specialSubjectID", Value: 1}},
},
{
Keys: bson.D{{Key: "updatedAt", Value: -1}},
},
{
Keys: bson.D{{Key: "createdAt", Value: -1}},
},
{
Keys: bson.D{{Key: "mediaContentId", Value: 1}},
},
{
Keys: bson.D{{Key: "title", Value: 1}},
}, { // 年度排行需要
Keys: bson.D{{Key: "mediaType", Value: 1}, {Key: "status", Value: 1}, {"isDelete", 1}, {"countBrowse", -1}, {Key: "createdAt", Value: -1}},
}, {
Keys: bson.D{{Key: "sId", Value: 1}, {Key: "status", Value: 1}, {"isDelete", 1}, {Key: "createdAt", Value: -1}},
}, {
Keys: bson.D{{Key: "sId", Value: 1}, {Key: "status", Value: 1}, {"isDelete", 1}, {Key: "countLike", Value: -1}, {Key: "createdAt", Value: -1}},
}, {
Keys: bson.D{{Key: "sId", Value: 1}, {Key: "status", Value: 1}, {"isDelete", 1}, {Key: "countCollect", Value: -1}, {Key: "createdAt", Value: -1}},
}, {
Keys: bson.D{{Key: "sId", Value: 1}, {Key: "status", Value: 1}, {"isDelete", 1}, {Key: "countBrowse", Value: -1}, {Key: "createdAt", Value: -1}},
}, {
Keys: bson.D{{Key: "sId", Value: 1}, {Key: "status", Value: 1}, {"isDelete", 1}, {Key: "hot", Value: -1}, {Key: "createdAt", Value: -1}},
},
// 首页优化
{
Keys: bson.D{{Key: "mId", Value: 1}, {Key: "status", Value: 1}, {"isDelete", 1}, {Key: "createdAt", Value: -1}},
}, {
Keys: bson.D{{Key: "mId", Value: 1}, {Key: "status", Value: 1}, {"isDelete", 1}, {Key: "countLike", Value: -1}, {Key: "createdAt", Value: -1}},
}, {
Keys: bson.D{{Key: "mId", Value: 1}, {Key: "status", Value: 1}, {"isDelete", 1}, {Key: "countBrowse", Value: -1}, {Key: "createdAt", Value: -1}},
}, {
Keys: bson.D{{Key: "mId", Value: 1}, {Key: "status", Value: 1}, {"isDelete", 1}, {Key: "countCollect", Value: -1}, {Key: "createdAt", Value: -1}},
}, {
Keys: bson.D{{Key: "mId", Value: 1}, {Key: "status", Value: 1}, {"isDelete", 1}, {Key: "hot", Value: -1}, {Key: "createdAt", Value: -1}},
},
{
Keys: bson.D{{Key: "mId", Value: 1}, {Key: "sortCode", Value: -1}, {Key: "createdAt", Value: -1}},
},
{
Keys: bson.D{
{Key: "mId", Value: 1},
{Key: "status", Value: 1},
{Key: "isDelete", Value: 1},
{Key: "latestPublishedAt", Value: -1},
{Key: "contentUpdateTime", Value: -1},
{Key: "createdAt", Value: -1},
{Key: "_id", Value: -1},
},
},
}
if _, err := coll(nil).CreateIndex(many); err != nil {
panic(fmt.Sprintf("%s model set index err ==>[%+v]", table, err))
}
}
// Init 初始化索引
func Init() {
mdb = db.Init(table)
initIndex()
}
// GetList 获取列表
func GetList(cond bson.M, skip, limit int64, sort bson.D) (res []*Media, hasNext bool, err error) {
opts := options.Find()
if len(sort) > 0 {
opts.SetSort(sort)
}
opts = opts.SetSort(sort).SetSkip(skip).SetLimit(limit + 1)
if err = coll(nil).Find(&res, cond, opts); err != nil {
return
}
// 判断下一页
if len(res) > int(limit) {
hasNext = true
res = res[:limit]
}
return
}
// QueryAllList 分页查询文档
func QueryAllList(filter primitive.M, opts ...*options.FindOptions) (out []*Media, err error) {
if err = coll(nil).Find(&out, filter, opts...); err != nil {
log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryAllList", table, "Find", err),
log.Any("filter", filter),
log.Any("opts", opts),
)
return nil, err
}
return
}
// QueryAllCount 查询文档条目数
func QueryAllCount(filter primitive.M) (int64, error) {
if count, err := coll(nil).Count(filter); err != nil {
log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryAllCount", table, "Count", err),
log.Any("filter", filter),
)
return 0, err
} else {
return count, nil
}
}
// GetLatestIDForESSync 固定本轮同步的 ID 上界,仅读取已有主键索引。
func GetLatestIDForESSync() (primitive.ObjectID, error) {
var data []*Media
opts := options.Find().SetSort(bson.D{{Key: "_id", Value: -1}}).
SetProjection(bson.M{"_id": 1}).SetHint("_id_").SetLimit(1).SetMaxTime(30 * time.Second)
if err := coll(nil).Find(&data, bson.M{}, opts); err != nil {
return primitive.NilObjectID, err
}
if len(data) == 0 {
return primitive.NilObjectID, nil
}
return data[0].ID, nil
}
// GetListForESSync 不限制更新时间上界,防止浏览等持续更新使记录逃出同步窗口。
// 使用稳定的 ID 游标,避免修改 updateTime 导致 skip 分页漏数据。
func GetListForESSync(since time.Time, after, maxID primitive.ObjectID, size int) (data []*Media, err error) {
query := bson.M{
"updateTime": bson.M{"$gte": since},
"_id": bson.M{"$gt": after, "$lte": maxID},
}
opts := options.Find().SetSort(bson.D{{Key: "_id", Value: 1}}).
SetHint("_id_").SetLimit(int64(size)).SetMaxTime(30 * time.Second)
err = coll(nil).Find(&data, query, opts)
return
}
var MediaNotFound = errors.New("media not found")
// GetInfoByMediaCenterId 通过mediaCenterId获取详细信息
func GetInfoByMediaCenterId(mediaCenterId uint) (Media, error) {
v := Media{}
if err := coll(nil).FindOne(&v, bson.M{"mediaCenterId": mediaCenterId, "isDelete": false}); err != nil {
log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetInfoByMediaCenterId", table, "FindOne", err),
log.Any("mediaCenterId", mediaCenterId),
)
return v, err
}
if v.ID.IsZero() {
return v, MediaNotFound
}
return v, nil
}
// GetInfoByTitle 通过标题获取详细信息
func GetInfoByTitle(title string) (Media, error) {
v := Media{}
if err := coll(nil).FindOne(&v, bson.M{"title": title, "isDelete": false}); err != nil {
log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetInfoByTitle", table, "FindOne", err),
log.Any("title", title),
)
return v, err
}
if v.ID.IsZero() {
return v, MediaNotFound
}
return v, nil
}
// GetInfo 通过id获取详细信息
func GetInfo(id primitive.ObjectID) (Media, error) {
v := Media{}
if err := coll(nil).FindOne(&v, bson.M{"_id": id}); err != nil {
log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetInfo", table, "FindOne", err),
log.Any("id", id),
)
return v, err
}
if v.ID.IsZero() {
return v, errors.New("record not found")
}
return v, nil
}
func GetListByIds(ids []primitive.ObjectID) (list []Media, res map[primitive.ObjectID]Media, err error) {
if err := coll(nil).Find(&list, bson.M{"_id": bson.M{"$in": ids}}); err != nil {
log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "GetListByIds", table, "Find", err),
log.Any("ids", ids),
)
return nil, nil, err
}
res = make(map[primitive.ObjectID]Media)
for _, v := range list {
res[v.ID] = v
}
return list, res, nil
}
// IncreaseCountLikeByIDs 更新媒体点赞数
func IncreaseCountLikeByIDs(value int, ids ...primitive.ObjectID) (err error) {
var query = bson.M{}
if len(ids) == 1 {
query["_id"] = ids[0]
} else {
query["_id"] = bson.M{"$in": ids}
}
if _, err = coll(nil).UpdateOne(query, bson.M{"$inc": bson.M{"countLike": value}}); err != nil {
log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IncreaseLikeCountByID", table, "UpdateOne", err),
log.Any("id", ids),
log.Any("value", value),
)
return
}
return
}
// IncreaseCountShareByID 累计短剧真实分享次数,供推荐互动分计算。
func IncreaseCountShareByID(id primitive.ObjectID, value int) error {
_, err := coll(nil).UpdateOne(bson.M{"_id": id, "mediaType": MediaTypeDrama}, bson.M{"$inc": bson.M{"countShare": value}})
return err
}
// IncrCommentCountByID 更新媒体评论数
func IncrCommentCountByID(id primitive.ObjectID, value int) (err error) {
if _, err = coll(nil).UpdateOne(bson.M{"_id": id}, bson.M{"$inc": bson.M{"countComment": value}}); err != nil {
log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IncrCommentCountByID", table, "UpdateOne", err),
log.Any("id", id),
log.Any("value", value),
)
return
}
return
}
// QueryMediaByID 查询文档
func QueryMediaByID(filter primitive.M) (out *Media, err error) {
if err = coll(nil).FindOne(&out, filter); err != nil {
log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryAllList", table, "Find", err),
log.Any("filter", filter),
)
return nil, err
}
return
}
// QueryMediaByCond 查询文档
func QueryMediaByCond(filter primitive.M, opts *options.FindOptions) (out []*Media, err error) {
if err = coll(nil).Find(&out, filter, opts); err != nil {
log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "QueryAllList", table, "Find", err),
log.Any("filter", filter),
)
return nil, err
}
return
}
// Insert 插入记录
func Insert(t *db.MongoTool, d Media) (data primitive.ObjectID, err error) {
normalizeLatestPublishedAt(&d)
result, err := coll(t).InsertOne(&d)
if err != nil {
log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "InsertOne", err))
return
}
byteID, err := json.Marshal(result.InsertedID)
if err != nil {
log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "Marshal", err))
return
}
if err = data.UnmarshalJSON(byteID); err != nil {
log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Insert", table, "UnmarshalJSON", err))
return
}
return
}
func InsertMany(c []Media) error {
for i := range c {
normalizeLatestPublishedAt(&c[i])
}
if _, err := coll(nil).InsertMany(c); err != nil {
log.Warn(fmt.Sprintf("METHOD-%s==> Model %s %s fail error:%+v:", "InsertMany", table, "InsertMany", err))
return err
}
return nil
}
func normalizeLatestPublishedAt(media *Media) {
if media == nil || media.Status != 1 || !media.LatestPublishedAt.IsZero() {
return
}
switch {
case !media.ContentUpdateTime.IsZero():
media.LatestPublishedAt = media.ContentUpdateTime
case !media.UpdateTime.IsZero():
media.LatestPublishedAt = media.UpdateTime
case !media.CreatedAt.IsZero():
media.LatestPublishedAt = media.CreatedAt
default:
media.LatestPublishedAt = time.Now()
}
}
// UpdateByID 根据id更新数据
func UpdateByID(t *db.MongoTool, id primitive.ObjectID, data map[string]interface{}) (int64, error) {
cond := bson.M{"_id": id}
return update(t, cond, data)
}
// UpdateByIDS 根据ids更新数据
func UpdateByIDS(t *db.MongoTool, ids []primitive.ObjectID, data map[string]interface{}) (int64, error) {
cond := bson.M{"_id": bson.M{"$in": ids}}
return update(t, cond, data)
}
// SectionVideosBySectionID 根据专题获取视频列表
func SectionVideosBySectionID(sectionID primitive.ObjectID, opts *options.FindOptions) ([]*Media, error) {
var out []*Media
return out, coll(nil).Find(&out, bson.M{"sId": sectionID, "isDelete": false, "status": 1}, opts)
}
type SectionWorkCount struct {
SectionID primitive.ObjectID `bson:"_id"`
Count int64 `bson:"count"`
}
// CountActiveDramaBySectionIDs groups all visible short dramas for one bounded topic page.
func CountActiveDramaBySectionIDs(sectionIDs []primitive.ObjectID) (map[primitive.ObjectID]int64, error) {
counts := make(map[primitive.ObjectID]int64, len(sectionIDs))
if len(sectionIDs) == 0 {
return counts, nil
}
var rows []SectionWorkCount
err := coll(nil).Aggregate(&rows, []bson.M{
{"$match": bson.M{"sId": bson.M{"$in": sectionIDs}, "mediaType": MediaTypeDrama, "status": 1, "isDelete": false}},
{"$group": bson.M{"_id": "$sId", "count": bson.M{"$sum": 1}}},
})
if err != nil {
return nil, err
}
for _, row := range rows {
counts[row.SectionID] = row.Count
}
return counts, nil
}
// ListActiveDramaByInteractionScore computes the full-history ranking in MongoDB
// and only returns the requested page. The formula is:
// likes + collections*2 + comments*3 + shares*5.
func ListActiveDramaByInteractionScore(skip, limit int64) ([]*Media, error) {
if skip < 0 {
skip = 0
}
if limit <= 0 || limit > 101 {
limit = 21
}
var list []*Media
pipeline := []bson.M{
{"$match": bson.M{"mediaType": MediaTypeDrama, "status": 1, "isDelete": false}},
{"$addFields": bson.M{"_dramaInteractionScore": bson.M{"$add": []any{
bson.M{"$ifNull": []any{"$countLike", 0}},
bson.M{"$multiply": []any{bson.M{"$ifNull": []any{"$countCollect", 0}}, 2}},
bson.M{"$multiply": []any{bson.M{"$ifNull": []any{"$countComment", 0}}, 3}},
bson.M{"$multiply": []any{bson.M{"$ifNull": []any{"$countShare", 0}}, 5}},
}}}},
{"$sort": bson.D{{Key: "_dramaInteractionScore", Value: -1}, {Key: "latestPublishedAt", Value: -1}, {Key: "_id", Value: -1}}},
{"$skip": skip},
{"$limit": limit},
{"$project": bson.M{"_dramaInteractionScore": 0}},
}
err := coll(nil).Aggregate(&list, pipeline, options.Aggregate().SetAllowDiskUse(true))
return list, err
}
// update 更新数据
func update(t *db.MongoTool, cond primitive.M, data map[string]interface{}) (int64, error) {
result, err := coll(t).UpdateMany(cond, bson.M{"$set": bson.M(data)})
if err != nil {
log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Update", table, "UpdateMany", err),
log.Any("cond", cond),
log.Any("update", data),
)
return 0, err
}
return result.ModifiedCount, nil
}
// DeleteByID 删除数据
func DeleteByID(t *db.MongoTool, id primitive.ObjectID) error {
_, err := coll(t).UpdateOne(bson.M{"_id": id}, bson.M{"$set": bson.M{"isDelete": true}})
if err != nil {
if err == mongo.ErrNoDocuments {
return nil
} else {
log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "DeleteByID", table, "DeleteMany", err), log.Any("id", id))
}
}
return err
}
// IncCollectCount 收藏次数增加
func IncCollectCount(id ObjectID, inc int) error {
query := bson.M{"_id": id}
update := bson.M{"$set": bson.M{"updateTime": time.Now()}, "$inc": bson.M{"countCollect": inc}}
_, err := coll(nil).UpdateOne(query, update)
if err != nil {
log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IncCollectCount", table, "UpdateOne", err),
log.Any("id", id),
)
return err
}
return nil
}
// IncContentPurchasesCount 子集购买次数增加
func IncContentPurchasesCount(id ObjectID, inc int) error {
query := bson.M{"_id": id}
update := bson.M{"$set": bson.M{"updateTime": time.Now()}, "$inc": bson.M{"countContentPurchases": inc}}
_, err := coll(nil).UpdateOne(query, update)
if err != nil {
log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IncPurchasesCount", table, "UpdateOne", err),
log.Any("id", id),
)
return err
}
return nil
}
// IncPurchasesCount 购买次数增加
func IncPurchasesCount(id ObjectID, inc int) error {
return IncPurchasesCountWithTool(nil, id, inc)
}
func IncPurchasesCountWithTool(t *db.MongoTool, id ObjectID, inc int) error {
query := bson.M{"_id": id}
update := bson.M{"$set": bson.M{"updateTime": time.Now()}, "$inc": bson.M{"countPurchases": inc}}
_, err := coll(t).UpdateOne(query, update)
if err != nil {
log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IncPurchasesCount", table, "UpdateOne", err),
log.Any("id", id),
)
return err
}
return nil
}
// IncBrowseCount 浏览次数增加
func IncBrowseCount(id ObjectID, inc int) error {
query := bson.M{"_id": id}
update := bson.M{"$set": bson.M{"updateTime": time.Now()}, "$inc": bson.M{"countBrowse": inc}}
_, err := coll(nil).UpdateOne(query, update)
if err != nil {
log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "IncBrowseCount", table, "UpdateOne", err),
log.Any("id", id),
)
return err
}
return nil
}
func Bulk(models []mongo.WriteModel) error {
_, err := coll(nil).Bulk(models, options.BulkWrite().SetOrdered(false))
return err
}
func GetListByLsjIds(lsjIds []string) ([]Media, error) {
filter := bson.M{
"lsjId": bson.M{"$in": lsjIds},
}
var ms []Media
return ms, coll(nil).Find(&ms, filter)
}
func GetListByLsjId(lsjId string) (Media, error) {
filter := bson.M{
"lsjId": lsjId,
}
var ms Media
return ms, coll(nil).FindOne(&ms, filter)
}
// UpdateMany 根据ids更新数据
func UpdateMany(t *db.MongoTool, filter bson.M, data bson.M) (int64, error) {
result, err := coll(t).UpdateMany(filter, data)
if err != nil {
log.Error(fmt.Sprintf("[METHOD-%s]==> Model %s %s fail error:%+v:", "Update", table, "UpdateMany", err),
log.Any("cond", filter),
log.Any("update", data),
)
return 0, err
}
return result.ModifiedCount, nil
}
// FindForReviewBatch 按 _id 游标分页扫描上架 Media,用于内容审查任务
// 仅扫描 status=1(上架)
func FindForReviewBatch(lastID primitive.ObjectID, limit int64) ([]*Media, error) {
cond := bson.M{"status": 1}
if !lastID.IsZero() {
cond["_id"] = bson.M{"$gt": lastID}
}
opts := options.Find().
SetSort(bson.D{{Key: "_id", Value: 1}}).
SetLimit(limit)
var list []*Media
if err := coll(nil).Find(&list, cond, opts); err != nil {
return nil, err
}
return list, nil
}
// CountForReview 上架 Media 总数;条件与 FindForReviewBatch 保持一致
func CountForReview() (int64, error) {
return coll(nil).Count(bson.M{"status": 1})
}
// UpdateForReview 内容审查通过后回写文本字段
// title 对应 Media.Titlecontent 对应 Media.Summary
func UpdateForReview(id primitive.ObjectID, title, content string) error {
set := bson.M{}
if title != "" {
set["title"] = title
}
if content != "" {
set["summary"] = content
}
if len(set) == 0 {
return nil
}
set["updateTime"] = time.Now()
_, err := coll(nil).UpdateOne(bson.M{"_id": id}, bson.M{"$set": set})
return err
}
// OffShelfManyForReview 内容审查命中后批量下架 Mediastatus -> 0
// 仅当前 status=1 的会被改动
func OffShelfManyForReview(ids []primitive.ObjectID) error {
if len(ids) == 0 {
return nil
}
_, err := coll(nil).UpdateMany(
bson.M{"_id": bson.M{"$in": ids}, "status": 1},
bson.M{"$set": bson.M{"status": 0, "updateTime": time.Now()}},
)
return err
}