Files
rootandClaude Opus 4.7 79ac89b458 Initial commit
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-11-07 10:47:18 +08:00

336 lines
10 KiB
Go
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 playletchaptermod
import (
"context"
"fmt"
"strings"
"time"
"videoweb/pub/common/log"
"videoweb/pub/common/srvdef"
"videoweb/pub/mongos"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
var coll *mongos.Collection
const duplicatePlayletChapterMsg = "短剧剧集已存在"
// Init 初始化 playlet_chapter 集合和常用查询索引
func Init(ctx context.Context) {
coll = mongos.Shop.Coll(TN.PlayletChapter)
initIndex(ctx)
}
func initIndex(ctx context.Context) {
commonIndexes := []mongo.IndexModel{
{
Keys: bson.D{
{Key: FN.PlayletChapter.PlayletId, Value: 1},
{Key: FN.PlayletChapter.CurrentEpisode, Value: 1},
},
},
{
Keys: bson.D{
{Key: FN.PlayletChapter.PlayletId, Value: 1},
{Key: FN.PlayletChapter.CreatedAt, Value: -1},
},
},
{
Keys: bson.D{
{Key: FN.PlayletChapter.CreatedAt, Value: -1},
{Key: FN.PlayletChapter.Id, Value: -1},
},
},
{
Keys: bson.D{
{Key: FN.PlayletChapter.WatermarkStatus, Value: 1},
{Key: FN.PlayletChapter.CreatedAt, Value: -1},
},
},
{
Keys: bson.D{
{Key: FN.PlayletChapter.MediaSourceId, Value: 1},
},
Options: options.Index().
SetPartialFilterExpression(bson.M{
FN.PlayletChapter.MediaSourceId: bson.M{"$gt": ""},
}),
},
}
if _, err := coll.CreateIndex(ctx, commonIndexes); err != nil {
panic(fmt.Sprintf("(playletchaptermod:initIndex) Create Index failed! err:%+v:", err))
}
uniqueIndexes := []mongo.IndexModel{
{
// 反向键序避免和线上已有 playletId_1_currentEpisode_1 普通索引冲突,同时兜住同一短剧同一集数的并发重复写入。
Keys: bson.D{
{Key: FN.PlayletChapter.CurrentEpisode, Value: 1},
{Key: FN.PlayletChapter.PlayletId, Value: 1},
},
Options: options.Index().
SetName("uniq_playlet_chapter_episode").
SetUnique(true),
},
{
Keys: bson.D{
{Key: FN.PlayletChapter.PlayletId, Value: 1},
{Key: FN.PlayletChapter.MediaContentId, Value: 1},
},
Options: options.Index().
SetUnique(true).
SetPartialFilterExpression(bson.M{
FN.PlayletChapter.MediaContentId: bson.M{"$gt": 0},
}),
},
}
for _, index := range uniqueIndexes {
if _, err := coll.CreateIndex(ctx, []mongo.IndexModel{index}); err != nil {
if isPlayletChapterDuplicateIndexErr(err) {
log.Warn("短剧章节唯一索引创建失败,请先清理历史重复章节数据", log.E(err), log.Any("index", index.Keys))
continue
}
panic(fmt.Sprintf("(playletchaptermod:initIndex) Create Unique Index failed! err:%+v:", err))
}
}
}
// isPlayletChapterDuplicateIndexErr 判断唯一索引创建时是否命中历史重复数据。
func isPlayletChapterDuplicateIndexErr(err error) bool {
if err == nil {
return false
}
return mongo.IsDuplicateKeyError(err) || strings.Contains(err.Error(), "E11000 duplicate key error")
}
// InsertOnePlayletChapter 插入一条章节数据
func InsertOnePlayletChapter(ctx context.Context, doc *PlayletChapter) *srvdef.SrvErr {
if doc.Id == 0 {
doc.Id = mongos.Lid.PlayletChapter.Get()
}
now := time.Now()
if doc.CreatedAt.IsZero() {
doc.CreatedAt = now
}
doc.UpdatedAt = now
if _, err := coll.InsertOne(ctx, doc); err != nil {
if mongo.IsDuplicateKeyError(err) {
return duplicatePlayletChapterErr()
}
log.Warn("playletchaptermod:InsertOnePlayletChapter failed", log.Any("doc", doc), log.E(err))
return srvdef.New(srvdef.ErrDbInsertError, "", "")
}
return nil
}
// InsertManyPlayletChapter 批量插入章节数据
func InsertManyPlayletChapter(ctx context.Context, docs PlayletChapterList) *srvdef.SrvErr {
data := make(PlayletChapterList, 0, len(docs))
now := time.Now()
for _, doc := range docs {
if doc == nil {
continue
}
if doc.Id == 0 {
doc.Id = mongos.Lid.PlayletChapter.Get()
}
if doc.CreatedAt.IsZero() {
doc.CreatedAt = now
}
doc.UpdatedAt = now
data = append(data, doc)
}
if len(data) == 0 {
return nil
}
if _, err := coll.InsertMany(ctx, &data); err != nil {
if mongo.IsDuplicateKeyError(err) {
return duplicatePlayletChapterErr()
}
log.Warn("playletchaptermod:InsertManyPlayletChapter failed", log.Any("count", len(data)), log.E(err))
return srvdef.New(srvdef.ErrDbInsertError, "", "")
}
return nil
}
// duplicatePlayletChapterErr 返回章节唯一约束冲突的统一业务错误。
func duplicatePlayletChapterErr() *srvdef.SrvErr {
return srvdef.New(srvdef.ErrDbInputExist, duplicatePlayletChapterMsg, duplicatePlayletChapterMsg)
}
// DelOnePlayletChapter 删除一条章节数据
func DelOnePlayletChapter(ctx context.Context, filter bson.M) (int64, *srvdef.SrvErr) {
count, err := coll.DeleteOne(ctx, filter)
if err != nil {
log.Warn("playletchaptermod:DelOnePlayletChapter failed", log.E(err), log.Any("filter", filter))
return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
}
return count, nil
}
// DelManyPlayletChapter 按条件批量删除章节数据
func DelManyPlayletChapter(ctx context.Context, filter bson.M) (int64, *srvdef.SrvErr) {
count, err := coll.DeleteMany(ctx, filter)
if err != nil {
log.Warn("playletchaptermod:DelManyPlayletChapter failed", log.E(err), log.Any("filter", filter))
return 0, srvdef.New(srvdef.ErrDbDeleteError, "", "")
}
return count, nil
}
// FindOnePlayletChapter 查询一条章节数据
func FindOnePlayletChapter(ctx context.Context, filter bson.M, opts ...*options.FindOneOptions) (*PlayletChapter, *srvdef.SrvErr) {
var doc PlayletChapter
if err := coll.FindOneEx(ctx, &doc, filter, opts...); err != nil {
if err != mongo.ErrNoDocuments {
log.Warn("playletchaptermod:FindOnePlayletChapter failed", log.E(err), log.Any("filter", filter))
return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
}
return nil, srvdef.New(srvdef.ErrNoData, "", "")
}
return &doc, nil
}
type firstChapterRow struct {
PlayletId int64 `bson:"_id"`
ChapterId int64 `bson:"chapterId"`
}
// FirstChapterIDs 按集数取每部短剧第一集的章节 ID,排序与详情页选集列表一致。
func FirstChapterIDs(ctx context.Context, playletIDs []int64) (map[int64]int64, *srvdef.SrvErr) {
out := make(map[int64]int64, len(playletIDs))
if len(playletIDs) == 0 {
return out, nil
}
const batchSize = 500
for start := 0; start < len(playletIDs); start += batchSize {
end := start + batchSize
if end > len(playletIDs) {
end = len(playletIDs)
}
pipeline := []bson.M{
{"$match": bson.M{
FN.PlayletChapter.PlayletId: bson.M{"$in": playletIDs[start:end]},
FN.PlayletChapter.Status: StatusUp,
}},
{"$sort": bson.D{
{Key: FN.PlayletChapter.CurrentEpisode, Value: 1},
{Key: FN.PlayletChapter.Id, Value: 1},
}},
{"$group": bson.M{
"_id": "$" + FN.PlayletChapter.PlayletId,
"chapterId": bson.M{"$first": "$_id"},
}},
}
rows := make([]firstChapterRow, 0)
if err := coll.Aggregate(ctx, &rows, pipeline); err != nil {
log.Warn("playletchaptermod:FirstChapterIDs failed", log.E(err), log.Any("count", end-start))
return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
}
for _, row := range rows {
if row.PlayletId > 0 && row.ChapterId > 0 {
out[row.PlayletId] = row.ChapterId
}
}
}
return out, nil
}
// FindManyPlayletChapter 按条件查询多条章节数据
func FindManyPlayletChapter(ctx context.Context, filter bson.M, opts ...*options.FindOptions) (PlayletChapterList, *srvdef.SrvErr) {
data := make(PlayletChapterList, 0)
if err := coll.Find(ctx, &data, filter, opts...); err != nil {
log.Warn("playletchaptermod:FindManyPlayletChapter failed", log.E(err), log.Any("filter", filter))
return nil, srvdef.New(srvdef.ErrDbQueryError, "", "")
}
return data, nil
}
// CountPlayletChapter 统计满足条件的章节数量
func CountPlayletChapter(ctx context.Context, filter bson.M, opts ...*options.CountOptions) (int64, *srvdef.SrvErr) {
count, err := coll.Count(ctx, filter, opts...)
if err != nil {
log.Warn("playletchaptermod:CountPlayletChapter failed", log.E(err), log.Any("filter", filter))
return 0, srvdef.New(srvdef.ErrDbQueryError, "", "")
}
return count, nil
}
// UpdateOnePlayletChapter 更新一条章节数据
func UpdateOnePlayletChapter(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
result, err := coll.UpdateOne(ctx, filter, update)
if err != nil {
if mongo.IsDuplicateKeyError(err) {
return duplicatePlayletChapterErr()
}
log.Warn("playletchaptermod:UpdateOnePlayletChapter failed", log.E(err), log.Any("filter", filter))
return srvdef.New(srvdef.ErrDbUpdateError, "", "")
}
if result.MatchedCount != 1 {
return srvdef.New(srvdef.ErrNoData, "", "")
}
return nil
}
// IsLinkedMedia 是否短剧媒资:显式标记或已挂到剧集
func IsLinkedMedia(ctx context.Context, isPlaylet bool, ids ...string) bool {
if isPlaylet {
return true
}
return ExistsByMediaIDs(ctx, ids...)
}
// ExistsByMediaIDs 判断媒资 ID 是否属于短剧剧集(mediaSourceId / mediaTaskId)
func ExistsByMediaIDs(ctx context.Context, ids ...string) bool {
clean := uniqueNonEmptyStrings(ids)
if len(clean) == 0 {
return false
}
item, _ := FindOnePlayletChapter(ctx, bson.M{
"$or": []bson.M{
{FN.PlayletChapter.MediaSourceId: bson.M{"$in": clean}},
{FN.PlayletChapter.MediaTaskId: bson.M{"$in": clean}},
},
}, options.FindOne().SetProjection(bson.M{FN.PlayletChapter.Id: 1}))
return item != nil
}
func uniqueNonEmptyStrings(ids []string) []string {
if len(ids) == 0 {
return nil
}
seen := make(map[string]struct{}, len(ids))
out := make([]string, 0, len(ids))
for _, id := range ids {
id = strings.TrimSpace(id)
if id == "" {
continue
}
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
out = append(out, id)
}
return out
}
// UpdateManyPlayletChapter 批量更新章节数据
func UpdateManyPlayletChapter(ctx context.Context, filter bson.M, update bson.M) *srvdef.SrvErr {
result, err := coll.UpdateMany(ctx, filter, update)
if err != nil {
if mongo.IsDuplicateKeyError(err) {
return duplicatePlayletChapterErr()
}
log.Warn("批量更新短剧章节数据失败", log.E(err), log.Any("filter", filter))
return srvdef.New(srvdef.ErrDbUpdateError, "", "")
}
if result.MatchedCount == 0 {
return srvdef.New(srvdef.ErrNoData, "", "")
}
return nil
}