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 }