Files
huangguo_server/web/service/dramatopicser/topic.go
T
rootandClaude Opus 5 8679200f41 Initial commit
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 13:57:10 +08:00

493 lines
15 KiB
Go

package dramatopicser
import (
"errors"
"fmt"
"sort"
"strconv"
"strings"
"time"
"unicode/utf8"
"91porn-server/common/db"
"91porn-server/common/dramatopic"
"91porn-server/models/cache/sysconfdata"
"91porn-server/models/commod"
"91porn-server/models/v/mediamod"
"91porn-server/models/v/modulesectionmod"
"91porn-server/models/v/sysconfmod"
"91porn-server/web/webg"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
)
type ListRequest struct {
Name string `form:"name" json:"name"`
TopicType string `form:"topicType" json:"topicType" binding:"omitempty,oneof=SYSTEM CUSTOM"`
Status *int `form:"status" json:"status" binding:"omitempty,oneof=0 1"`
commod.Page
}
type Topic struct {
TopicID string `json:"topicId"`
Name string `json:"name"`
TopicType string `json:"topicType"`
SystemKey string `json:"systemKey"`
Status int `json:"status"`
Sort int `json:"sort"`
WorkCount int64 `json:"workCount"`
Editable bool `json:"editable"`
WorksEditable bool `json:"worksEditable"`
CreatedAt *time.Time `json:"createdAt"`
UpdatedAt *time.Time `json:"updatedAt"`
systemOrder int
}
type ListResponse struct {
Total int64 `json:"total"`
HasNext bool `json:"hasNext"`
List []Topic `json:"list"`
}
func (req ListRequest) List(now time.Time) (ListResponse, error) {
moduleID, ok, err := dramatopic.ModuleID(now)
if err != nil {
return ListResponse{}, err
}
sections := make([]modulesectionmod.Section, 0)
if ok {
sections, err = modulesectionmod.ListBySubModule(moduleID, 1000)
if err != nil {
return ListResponse{}, err
}
}
sectionIDs := make([]primitive.ObjectID, 0, len(sections))
for _, section := range sections {
sectionIDs = append(sectionIDs, section.ID)
}
counts, err := mediamod.CountActiveDramaBySectionIDs(sectionIDs)
if err != nil {
return ListResponse{}, err
}
activeTotal, err := mediamod.QueryAllCount(bson.M{"mediaType": mediamod.MediaTypeDrama, "status": 1, "isDelete": false})
if err != nil {
return ListResponse{}, err
}
all := make([]Topic, 0, 3+len(sections))
if req.TopicType == "" || req.TopicType == dramatopic.TypeSystem {
for _, system := range dramatopic.SystemTopics() {
if !matches(req, system.Name, 1) {
continue
}
all = append(all, Topic{
TopicID: system.ID, Name: system.Name, TopicType: dramatopic.TypeSystem,
SystemKey: system.SystemKey, Status: 1, Sort: system.Sort, WorkCount: activeTotal,
Editable: false, WorksEditable: false, systemOrder: system.TieOrder,
})
}
}
if req.TopicType == "" || req.TopicType == dramatopic.TypeCustom {
for _, section := range sections {
status := int(valueOrZeroUint8(section.Status))
if !matches(req, section.SectionName, status) {
continue
}
createdAt := section.CreatedAt
updatedAt := section.UpdatedAt
all = append(all, Topic{
TopicID: section.ID.Hex(), Name: section.SectionName, TopicType: dramatopic.TypeCustom,
Status: status, Sort: valueOrZeroInt(section.Sort), WorkCount: counts[section.ID],
Editable: true, WorksEditable: true, CreatedAt: &createdAt, UpdatedAt: &updatedAt,
})
}
}
sort.SliceStable(all, func(i, j int) bool {
if all[i].Sort != all[j].Sort {
return all[i].Sort > all[j].Sort
}
if all[i].systemOrder != all[j].systemOrder {
return all[i].systemOrder > all[j].systemOrder
}
if all[i].UpdatedAt == nil {
return false
}
if all[j].UpdatedAt == nil {
return true
}
return all[i].UpdatedAt.After(*all[j].UpdatedAt)
})
response := ListResponse{Total: int64(len(all)), List: []Topic{}}
start := int(req.Skip64())
if start >= len(all) {
return response, nil
}
end := start + int(req.PageSize)
if end > len(all) {
end = len(all)
}
response.HasNext = end < len(all)
response.List = all[start:end]
return response, nil
}
func matches(req ListRequest, name string, status int) bool {
if req.Status != nil && *req.Status != status {
return false
}
return req.Name == "" || strings.Contains(strings.ToLower(name), strings.ToLower(strings.TrimSpace(req.Name)))
}
type CreateRequest struct {
Name string `json:"name" binding:"required"`
Status *int `json:"status" binding:"omitempty,oneof=0 1"`
Sort *int `json:"sort" binding:"omitempty,min=0"`
}
func (req CreateRequest) Create(now time.Time) (primitive.ObjectID, error) {
name, err := validateName(req.Name)
if err != nil {
return primitive.NilObjectID, err
}
moduleID, ok, err := dramatopic.ModuleID(now)
if err != nil || !ok {
if err == nil {
err = errors.New("短剧专题模块未配置")
}
return primitive.NilObjectID, err
}
status := uint8(1)
if req.Status != nil {
status = uint8(*req.Status)
}
topicSort := 0
if req.Sort != nil {
topicSort = *req.Sort
}
id := primitive.NewObjectID()
err = modulesectionmod.InsertOne(modulesectionmod.Section{
ID: id, SectionName: name, SubModuleID: moduleID, Status: &status, Sort: &topicSort,
})
return id, err
}
type UpdateRequest struct {
TopicID string `json:"topicId" binding:"required"`
Name *string `json:"name"`
Status *int `json:"status" binding:"omitempty,oneof=0 1"`
Sort *int `json:"sort" binding:"omitempty,min=0"`
}
func (req UpdateRequest) Update(now time.Time) error {
topicID, moduleID, _, err := customTopic(req.TopicID, now)
if err != nil {
return err
}
fields := bson.M{}
if req.Name != nil {
name, nameErr := validateName(*req.Name)
if nameErr != nil {
return nameErr
}
fields["sectionName"] = name
}
if req.Status != nil {
fields["status"] = uint8(*req.Status)
}
if req.Sort != nil {
fields["sort"] = *req.Sort
}
if len(fields) == 0 {
return errors.New("至少需要修改一个字段")
}
return webg.VideoDB.Trans(func(t *db.MongoTool) error {
matched, updateErr := modulesectionmod.UpdateTopicFields(t, topicID, moduleID, fields)
if updateErr != nil {
return updateErr
}
if matched == 0 {
return errors.New("专题不存在")
}
if name, renamed := fields["sectionName"]; renamed {
if _, updateErr = mediamod.UpdateMany(t, bson.M{"sId": topicID}, bson.M{"$set": bson.M{"sectionName": name}}); updateErr != nil {
return updateErr
}
}
return nil
})
}
type DeleteRequest struct {
TopicID string `json:"topicId" binding:"required"`
}
func (req DeleteRequest) Delete(now time.Time) error {
topicID, moduleID, _, err := customTopic(req.TopicID, now)
if err != nil {
return err
}
return webg.VideoDB.Trans(func(t *db.MongoTool) error {
if _, updateErr := mediamod.UpdateMany(t, bson.M{"sId": topicID}, bson.M{"$set": bson.M{
"sId": primitive.NilObjectID, "sectionName": "", "sectionSort": 0,
}}); updateErr != nil {
return updateErr
}
deleted, deleteErr := modulesectionmod.DeleteTopic(t, topicID, moduleID)
if deleteErr != nil {
return deleteErr
}
if deleted != 1 {
return errors.New("自定义专题不存在")
}
return nil
})
}
type SortRequest struct {
Items []SortRequestItem `json:"items" binding:"required,min=1,max=1000,dive"`
}
type SortRequestItem struct {
TopicID string `json:"topicId" binding:"required"`
Sort int `json:"sort" binding:"min=0"`
}
func (req SortRequest) Update(now time.Time) error {
moduleID, moduleOK, err := dramatopic.ModuleID(now)
if err != nil {
return err
}
type target struct {
item SortRequestItem
system *dramatopic.SystemTopic
custom primitive.ObjectID
}
targets := make([]target, 0, len(req.Items))
seen := make(map[string]struct{}, len(req.Items))
systemTopics := make(map[string]dramatopic.SystemTopic, 3)
for _, system := range dramatopic.SystemTopics() {
systemTopics[system.ID] = system
}
for _, item := range req.Items {
if _, exists := seen[item.TopicID]; exists {
return errors.New("topicId不能重复")
}
seen[item.TopicID] = struct{}{}
if system, found := systemTopics[item.TopicID]; found {
topic := system
targets = append(targets, target{item: item, system: &topic})
continue
}
if !moduleOK {
return errors.New("短剧专题模块未配置")
}
id, parseErr := primitive.ObjectIDFromHex(item.TopicID)
if parseErr != nil {
return errors.New("无效的专题ID")
}
section, findErr := modulesectionmod.GetBySectionByID(id)
if findErr != nil || section.SubModuleID != moduleID || section.DeletedAt != nil {
return errors.New("自定义专题不存在")
}
targets = append(targets, target{item: item, custom: id})
}
sysconfmod.EnsureInitData()
for _, target := range targets {
if target.system != nil {
config, configErr := sysconfmod.GetByVCode(target.system.VCode)
if configErr != nil {
return configErr
}
if _, configErr = sysconfdata.UpdateData(nil, config.ID.Hex(), map[string]interface{}{
"value": strconv.Itoa(target.item.Sort), "updatedAt": time.Now(),
}); configErr != nil {
return configErr
}
continue
}
if _, updateErr := modulesectionmod.UpdateTopicFields(nil, target.custom, moduleID, bson.M{"sort": target.item.Sort}); updateErr != nil {
return updateErr
}
}
return nil
}
type UpdateWorksRequest struct {
TopicID string `json:"topicId" binding:"required"`
WorkIDs []string `json:"workIds" binding:"max=500,dive,required"`
}
type UpdateWorksResponse struct {
TopicID string `json:"topicId"`
WorkCount int `json:"workCount"`
}
type WorkConflict struct {
MediaID string `json:"mediaId"`
TopicID string `json:"topicId"`
TopicName string `json:"topicName"`
}
type WorksConflictError struct {
Message string `json:"message"`
Conflicts []WorkConflict `json:"conflicts"`
}
func (e *WorksConflictError) Error() string { return e.Message }
type topicWorkAssignment struct {
ID primitive.ObjectID
Sort int
}
func appendTopicWorkAssignments(
topicID primitive.ObjectID,
ids []primitive.ObjectID,
mediaMap map[primitive.ObjectID]mediamod.Media,
currentCount int64,
) ([]topicWorkAssignment, int) {
newIDs := make([]primitive.ObjectID, 0, len(ids))
for _, id := range ids {
if mediaMap[id].SID != topicID {
newIDs = append(newIDs, id)
}
}
total := int(currentCount) + len(newIDs)
assignments := make([]topicWorkAssignment, 0, len(newIDs))
for i, id := range newIDs {
assignments = append(assignments, topicWorkAssignment{ID: id, Sort: total - i})
}
return assignments, total
}
func (req UpdateWorksRequest) Update(now time.Time) (UpdateWorksResponse, error) {
topicID, _, section, err := customTopic(req.TopicID, now)
if err != nil {
return UpdateWorksResponse{}, err
}
ids := make([]primitive.ObjectID, 0, len(req.WorkIDs))
seen := make(map[primitive.ObjectID]struct{}, len(req.WorkIDs))
for _, raw := range req.WorkIDs {
id, parseErr := primitive.ObjectIDFromHex(raw)
if parseErr != nil {
return UpdateWorksResponse{}, errors.New("workIds包含无效ID")
}
if _, exists := seen[id]; exists {
return UpdateWorksResponse{}, errors.New("workIds不能重复")
}
seen[id] = struct{}{}
ids = append(ids, id)
}
medias, mediaMap, err := mediamod.GetListByIds(ids)
if err != nil {
return UpdateWorksResponse{}, err
}
if len(medias) != len(ids) {
return UpdateWorksResponse{}, errors.New("部分短剧不存在")
}
conflictTopicIDs := make([]primitive.ObjectID, 0)
for _, id := range ids {
media := mediaMap[id]
if media.MediaType != mediamod.MediaTypeDrama || media.IsDelete {
return UpdateWorksResponse{}, fmt.Errorf("作品%s不是有效短剧", id.Hex())
}
if !media.SID.IsZero() && media.SID != topicID {
conflictTopicIDs = append(conflictTopicIDs, media.SID)
}
}
if len(conflictTopicIDs) > 0 {
sections, _ := modulesectionmod.GetBySectionByIDs(conflictTopicIDs)
names := make(map[primitive.ObjectID]string, len(sections))
for _, item := range sections {
names[item.ID] = item.SectionName
}
conflict := &WorksConflictError{Message: "部分短剧已配置到其他专题", Conflicts: []WorkConflict{}}
for _, id := range ids {
media := mediaMap[id]
if !media.SID.IsZero() && media.SID != topicID {
conflict.Conflicts = append(conflict.Conflicts, WorkConflict{MediaID: id.Hex(), TopicID: media.SID.Hex(), TopicName: names[media.SID]})
}
}
return UpdateWorksResponse{}, conflict
}
currentCount, err := mediamod.QueryAllCount(bson.M{
"sId": topicID, "mediaType": mediamod.MediaTypeDrama, "isDelete": false,
})
if err != nil {
return UpdateWorksResponse{}, err
}
// 作品配置是增量追加;已在当前专题内的作品保持原顺序,避免单个追加覆盖整批历史配置。
assignments, total := appendTopicWorkAssignments(topicID, ids, mediaMap, currentCount)
if len(assignments) == 0 {
return UpdateWorksResponse{TopicID: req.TopicID, WorkCount: total}, nil
}
err = webg.VideoDB.Trans(func(t *db.MongoTool) error {
for _, assignment := range assignments {
matched, updateErr := mediamod.UpdateMany(t, bson.M{
"_id": assignment.ID, "mediaType": mediamod.MediaTypeDrama, "isDelete": false,
"$or": []bson.M{{"sId": primitive.NilObjectID}, {"sId": bson.M{"$exists": false}}},
}, bson.M{"$set": bson.M{
"sId": topicID, "sectionName": section.SectionName, "sectionSort": assignment.Sort,
}})
if updateErr != nil {
return updateErr
}
if matched != 1 {
return errors.New("专题作品并发冲突,请刷新后重试")
}
}
return nil
})
if err != nil {
return UpdateWorksResponse{}, err
}
return UpdateWorksResponse{TopicID: req.TopicID, WorkCount: total}, nil
}
func customTopic(rawID string, now time.Time) (primitive.ObjectID, primitive.ObjectID, modulesectionmod.Section, error) {
if _, system := dramatopic.FindSystem(rawID); system {
return primitive.NilObjectID, primitive.NilObjectID, modulesectionmod.Section{}, errors.New("系统专题不允许此操作")
}
id, err := primitive.ObjectIDFromHex(rawID)
if err != nil {
return primitive.NilObjectID, primitive.NilObjectID, modulesectionmod.Section{}, errors.New("无效的专题ID")
}
moduleID, ok, err := dramatopic.ModuleID(now)
if err != nil || !ok {
if err == nil {
err = errors.New("短剧专题模块未配置")
}
return primitive.NilObjectID, primitive.NilObjectID, modulesectionmod.Section{}, err
}
section, err := modulesectionmod.GetBySectionByID(id)
if err != nil || section.SubModuleID != moduleID || section.DeletedAt != nil {
if err == nil {
err = errors.New("自定义专题不存在")
}
return primitive.NilObjectID, primitive.NilObjectID, modulesectionmod.Section{}, err
}
return id, moduleID, section, nil
}
func validateName(raw string) (string, error) {
name := strings.TrimSpace(raw)
if count := utf8.RuneCountInString(name); count < 1 || count > 20 {
return "", errors.New("专题名称长度必须为1到20个字符")
}
return name, nil
}
func valueOrZeroInt(value *int) int {
if value == nil {
return 0
}
return *value
}
func valueOrZeroUint8(value *uint8) uint8 {
if value == nil {
return 0
}
return *value
}