init: import web_huangguo2 server project

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-09-29 10:47:38 +08:00
co-authored by Claude Opus 4.7
commit a50c9c342e
668 changed files with 141008 additions and 0 deletions
+332
View File
@@ -0,0 +1,332 @@
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]}}},
{"$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
}
+72
View File
@@ -0,0 +1,72 @@
package playletchaptermod
// TagPlayletChapter 保存 playlet_chapter 集合字段名
type TagPlayletChapter struct {
Id string
PlayletId string
MediaContentId string
CurrentEpisode string
Title string
Md5 string
Hash string
Height string
Width string
Ratio string
VideoUrl string
WatermarkVideoUrl string
MediaSize string
MediaTime string
CountComment string
CountBuy string
CountBrowse string
CountCollect string
CountLike string
CountDisLike string
PayType string
SingleCoins string
WatermarkStatus string
MediaSourceId string
MediaTaskId string
CreatedAt string
UpdatedAt string
}
var FN struct {
PlayletChapter TagPlayletChapter
}
var TN struct {
PlayletChapter string
}
func init() {
FN.PlayletChapter.Id = "_id"
FN.PlayletChapter.PlayletId = "playletId"
FN.PlayletChapter.MediaContentId = "mediaContentId"
FN.PlayletChapter.CurrentEpisode = "currentEpisode"
FN.PlayletChapter.Title = "title"
FN.PlayletChapter.Md5 = "md5"
FN.PlayletChapter.Hash = "hash"
FN.PlayletChapter.Height = "height"
FN.PlayletChapter.Width = "width"
FN.PlayletChapter.Ratio = "ratio"
FN.PlayletChapter.VideoUrl = "videoUrl"
FN.PlayletChapter.WatermarkVideoUrl = "watermarkVideoUrl"
FN.PlayletChapter.MediaSize = "mediaSize"
FN.PlayletChapter.MediaTime = "mediaTime"
FN.PlayletChapter.CountComment = "countComment"
FN.PlayletChapter.CountBuy = "countBuy"
FN.PlayletChapter.CountBrowse = "countBrowse"
FN.PlayletChapter.CountCollect = "countCollect"
FN.PlayletChapter.CountLike = "countLike"
FN.PlayletChapter.CountDisLike = "countDisLike"
FN.PlayletChapter.PayType = "payType"
FN.PlayletChapter.SingleCoins = "singleCoins"
FN.PlayletChapter.WatermarkStatus = "watermarkStatus"
FN.PlayletChapter.MediaSourceId = "mediaSourceId"
FN.PlayletChapter.MediaTaskId = "mediaTaskId"
FN.PlayletChapter.CreatedAt = "createdAt"
FN.PlayletChapter.UpdatedAt = "updatedAt"
TN.PlayletChapter = "playlet_chapter"
}
+59
View File
@@ -0,0 +1,59 @@
package playletchaptermod
import "time"
// PayType 支付方式
type PayType int32
const (
PayTypeFree PayType = 0 // 免费
PayTypeCoin PayType = 1 // 金币
PayTypeVip PayType = 2 // VIP
)
// WatermarkStatus 水印状态
type WatermarkStatus int32
const (
WatermarkStatusPending WatermarkStatus = 0 // 未处理(待 mediafile/sync)
WatermarkStatusRegistered WatermarkStatus = 1 // 已登记(原片已登记,待 task/add)
WatermarkStatusProcessing WatermarkStatus = 2 // 处理中(已提交加水印任务,待回写)
WatermarkStatusDone WatermarkStatus = 3 // 已完成
WatermarkStatusFailed WatermarkStatus = 4 // 处理失败
)
// PlayletChapter @table: playlet_chapter
//
// PlayletChapter 连载短剧章节集合模型
type PlayletChapter struct {
Id int64 `json:"id" bson:"_id,omitempty"`
PlayletId int64 `json:"playletId" bson:"playletId"` // 短剧ID
MediaContentId uint `json:"mediaContentId" bson:"mediaContentId"` // 媒资中心章节ID
CurrentEpisode int64 `json:"currentEpisode" bson:"currentEpisode"` // 第几集
Title string `json:"title" bson:"title"` // 章节名
Md5 string `json:"md5" bson:"md5"` // 媒体md5
Hash string `json:"hash" bson:"hash"` // 媒体hash
Height int64 `json:"height" bson:"height"` // 高
Width int64 `json:"width" bson:"width"` // 宽
Ratio float64 `json:"ratio" bson:"ratio"` // 宽高比
VideoUrl string `json:"videoUrl" bson:"videoUrl"` // 原片视频地址(加水印后保持不变,方便换水印)
WatermarkVideoUrl string `json:"watermarkVideoUrl" bson:"watermarkVideoUrl"` // 加水印后的视频地址
MediaSize int64 `json:"mediaSize" bson:"mediaSize"` // 资源大小
MediaTime int64 `json:"mediaTime" bson:"mediaTime"` // 影片长度(秒)
CountComment int64 `json:"countComment" bson:"countComment"` // 评论数
CountBuy int64 `json:"countBuy" bson:"countBuy"` // 购买数
CountBrowse int64 `json:"countBrowse" bson:"countBrowse"` // 浏览数
CountCollect int64 `json:"countCollect" bson:"countCollect"` // 收藏数
CountLike int64 `json:"countLike" bson:"countLike"` // 喜欢数
CountDisLike int64 `json:"countDisLike" bson:"countDisLike"` // 不喜欢数
PayType PayType `json:"payType" bson:"payType"` // 支付方式 0-免费 1-金币 2-VIP
SingleCoins int64 `json:"singleCoins" bson:"singleCoins"` // 单集金币(支付方式为1-金币时候生效)
WatermarkStatus WatermarkStatus `json:"watermarkStatus" bson:"watermarkStatus"` // 水印状态 0-未处理 1-已登记 2-处理中 3-已完成 4-处理失败
MediaSourceId string `json:"mediaSourceId" bson:"mediaSourceId"` // media切片服原片ID
MediaTaskId string `json:"mediaTaskId" bson:"mediaTaskId"` // media切片服水印任务ID
CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 创建时间
UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"` // 修改时间
}
// PlayletChapterList 章节列表
type PlayletChapterList []*PlayletChapter