@@ -0,0 +1,492 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user