493 lines
15 KiB
Go
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
|
|
}
|