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 }