feat(content): 新增运营分类与剧单分类管理

This commit is contained in:
Zhou Weidong authored and trae-solo-agent committed 2026-10-22 22:17:58 +08:00
1 parent 758c7521bb
commit 7b360ddccd
26 files changed
+1106 -3

No files matched your search

@@ -0,0 +1,115 @@
// Author: fruit.com.cn
package com.fruitshorts.admin.controller;
import com.fruitshorts.common.pojo.PageResult;
import com.fruitshorts.common.pojo.Result;
import com.fruitshorts.content.service.DramaClassificationService;
import com.fruitshorts.content.vo.DramaBriefVO;
import com.fruitshorts.content.vo.DramaClassificationRespVO;
import com.fruitshorts.content.vo.DramaClassificationSaveReqVO;
import com.fruitshorts.content.vo.DramaIdsReqVO;
import com.fruitshorts.framework.log.OperationLog;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* 运营分类管理。分类与短剧类型(drama-categories)是两套独立概念,
* 分类可自定义增删,并关联任意多部短剧。
*/
@Tag(name = "后台-运营分类")
@RestController
@RequestMapping("/admin/drama-classifications")
@RequiredArgsConstructor
public class DramaClassificationController {
private final DramaClassificationService classificationService;
@Operation(summary = "分类列表(带关联数量)")
@GetMapping
@PreAuthorize("@ss.hasPermission('drama:list')")
public Result<List<DramaClassificationRespVO>> list() {
return Result.ok(classificationService.list());
}
@Operation(summary = "新建分类")
@PostMapping
@PreAuthorize("@ss.hasPermission('drama:create')")
@OperationLog(module = "内容管理", action = "新增分类")
public Result<Long> create(@Valid @RequestBody DramaClassificationSaveReqVO req) {
return Result.ok(classificationService.create(req));
}
@Operation(summary = "编辑分类")
@PutMapping("/{id}")
@PreAuthorize("@ss.hasPermission('drama:update')")
@OperationLog(module = "内容管理", action = "编辑分类")
public Result<Void> update(@PathVariable Long id,
@Valid @RequestBody DramaClassificationSaveReqVO req) {
classificationService.update(id, req);
return Result.ok();
}
@Operation(summary = "删除分类")
@DeleteMapping("/{id}")
@PreAuthorize("@ss.hasPermission('drama:delete')")
@OperationLog(module = "内容管理", action = "删除分类")
public Result<Void> delete(@PathVariable Long id) {
classificationService.delete(id);
return Result.ok();
}
@Operation(summary = "分类下已关联的短剧")
@GetMapping("/{id}/dramas")
@PreAuthorize("@ss.hasPermission('drama:list')")
public Result<PageResult<DramaBriefVO>> dramas(
@PathVariable Long id,
@RequestParam(required = false) String keyword,
@RequestParam(defaultValue = "1") int pageIndex,
@RequestParam(defaultValue = "10") int pageSize) {
return Result.ok(classificationService.pageDramas(id, keyword, pageIndex, pageSize));
}
@Operation(summary = "尚未关联到该分类的短剧")
@GetMapping("/{id}/dramas/candidates")
@PreAuthorize("@ss.hasPermission('drama:list')")
public Result<PageResult<DramaBriefVO>> candidates(
@PathVariable Long id,
@RequestParam(required = false) String keyword,
@RequestParam(defaultValue = "1") int pageIndex,
@RequestParam(defaultValue = "10") int pageSize) {
return Result.ok(classificationService.pageCandidates(id, keyword, pageIndex, pageSize));
}
@Operation(summary = "批量关联短剧")
@PostMapping("/{id}/dramas")
@PreAuthorize("@ss.hasPermission('drama:update')")
@OperationLog(module = "内容管理", action = "分类关联短剧")
public Result<Integer> bindDramas(@PathVariable Long id,
@Valid @RequestBody DramaIdsReqVO req) {
return Result.ok(classificationService.bindDramas(id, req.getDramaIds()));
}
@Operation(summary = "移除分类下的短剧")
@DeleteMapping("/{id}/dramas/{dramaId}")
@PreAuthorize("@ss.hasPermission('drama:update')")
@OperationLog(module = "内容管理", action = "分类移除短剧")
public Result<Void> unbindDrama(@PathVariable Long id, @PathVariable Long dramaId) {
classificationService.unbindDrama(id, dramaId);
return Result.ok();
}
}
@@ -0,0 +1,115 @@
// Author: fruit.com.cn
package com.fruitshorts.admin.controller;
import com.fruitshorts.common.pojo.PageResult;
import com.fruitshorts.common.pojo.Result;
import com.fruitshorts.content.service.DramaPlaylistCategoryService;
import com.fruitshorts.content.vo.DramaBriefVO;
import com.fruitshorts.content.vo.DramaIdsReqVO;
import com.fruitshorts.content.vo.DramaPlaylistCategoryRespVO;
import com.fruitshorts.content.vo.DramaPlaylistCategorySaveReqVO;
import com.fruitshorts.framework.log.OperationLog;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* 剧单分类管理:分类直接关联短剧,没有「剧单」这一层。
* 与「类型」「分类管理」均无关。
*/
@Tag(name = "后台-剧单分类")
@RestController
@RequestMapping("/admin/drama-playlist-categories")
@RequiredArgsConstructor
public class DramaPlaylistCategoryController {
private final DramaPlaylistCategoryService categoryService;
@Operation(summary = "剧单分类列表")
@GetMapping
@PreAuthorize("@ss.hasPermission('drama:list')")
public Result<List<DramaPlaylistCategoryRespVO>> list() {
return Result.ok(categoryService.list());
}
@Operation(summary = "新建剧单分类")
@PostMapping
@PreAuthorize("@ss.hasPermission('drama:create')")
@OperationLog(module = "内容管理", action = "新增剧单分类")
public Result<Long> create(@Valid @RequestBody DramaPlaylistCategorySaveReqVO req) {
return Result.ok(categoryService.create(req));
}
@Operation(summary = "编辑剧单分类")
@PutMapping("/{id}")
@PreAuthorize("@ss.hasPermission('drama:update')")
@OperationLog(module = "内容管理", action = "编辑剧单分类")
public Result<Void> update(@PathVariable Long id,
@Valid @RequestBody DramaPlaylistCategorySaveReqVO req) {
categoryService.update(id, req);
return Result.ok();
}
@Operation(summary = "删除剧单分类")
@DeleteMapping("/{id}")
@PreAuthorize("@ss.hasPermission('drama:delete')")
@OperationLog(module = "内容管理", action = "删除剧单分类")
public Result<Void> delete(@PathVariable Long id) {
categoryService.delete(id);
return Result.ok();
}
@Operation(summary = "分类下已关联的短剧")
@GetMapping("/{id}/dramas")
@PreAuthorize("@ss.hasPermission('drama:list')")
public Result<PageResult<DramaBriefVO>> dramas(
@PathVariable Long id,
@RequestParam(required = false) String keyword,
@RequestParam(defaultValue = "1") int pageIndex,
@RequestParam(defaultValue = "10") int pageSize) {
return Result.ok(categoryService.pageDramas(id, keyword, pageIndex, pageSize));
}
@Operation(summary = "尚未关联到该分类的短剧")
@GetMapping("/{id}/dramas/candidates")
@PreAuthorize("@ss.hasPermission('drama:list')")
public Result<PageResult<DramaBriefVO>> candidates(
@PathVariable Long id,
@RequestParam(required = false) String keyword,
@RequestParam(defaultValue = "1") int pageIndex,
@RequestParam(defaultValue = "10") int pageSize) {
return Result.ok(categoryService.pageCandidates(id, keyword, pageIndex, pageSize));
}
@Operation(summary = "批量关联短剧")
@PostMapping("/{id}/dramas")
@PreAuthorize("@ss.hasPermission('drama:update')")
@OperationLog(module = "内容管理", action = "剧单分类关联短剧")
public Result<Integer> bindDramas(@PathVariable Long id,
@Valid @RequestBody DramaIdsReqVO req) {
return Result.ok(categoryService.bindDramas(id, req.getDramaIds()));
}
@Operation(summary = "移除分类下的短剧")
@DeleteMapping("/{id}/dramas/{dramaId}")
@PreAuthorize("@ss.hasPermission('drama:update')")
@OperationLog(module = "内容管理", action = "剧单分类移除短剧")
public Result<Void> unbindDrama(@PathVariable Long id, @PathVariable Long dramaId) {
categoryService.unbindDrama(id, dramaId);
return Result.ok();
}
}
@@ -0,0 +1,24 @@
// Author: fruit.com.cn
package com.fruitshorts.content.dal.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
/** 运营分类:与短剧类型(drama_category)是两套独立概念。 */
@Data
@TableName("drama_classification")
public class DramaClassification {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private String name;
private Integer sort;
private Integer status;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
@@ -0,0 +1,22 @@
// Author: fruit.com.cn
package com.fruitshorts.content.dal.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
/** 分类与短剧的关联关系(多对多)。 */
@Data
@TableName("drama_classification_drama")
public class DramaClassificationDrama {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private Long classificationId;
private Long dramaId;
private LocalDateTime createdAt;
}
@@ -0,0 +1,26 @@
// Author: fruit.com.cn
package com.fruitshorts.content.dal.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
/** 剧单专属分类:只服务于剧单分组,与运营分类(drama_classification)无关。 */
@Data
@TableName("drama_playlist_category")
public class DramaPlaylistCategory {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private String name;
/** 分类描述,便于运营说明该分类的用途 */
private String description;
private Integer sort;
private Integer status;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
@@ -0,0 +1,22 @@
// Author: fruit.com.cn
package com.fruitshorts.content.dal.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
/** 剧单分类与短剧的关联关系(分类直接挂短剧,没有剧单层)。 */
@Data
@TableName("drama_playlist_category_drama")
public class DramaPlaylistCategoryDrama {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private Long categoryId;
private Long dramaId;
private LocalDateTime createdAt;
}
@@ -0,0 +1,9 @@
// Author: fruit.com.cn
package com.fruitshorts.content.dal.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.fruitshorts.content.dal.entity.DramaClassificationDrama;
public interface DramaClassificationDramaMapper extends BaseMapper<DramaClassificationDrama> {
}
@@ -0,0 +1,9 @@
// Author: fruit.com.cn
package com.fruitshorts.content.dal.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.fruitshorts.content.dal.entity.DramaClassification;
public interface DramaClassificationMapper extends BaseMapper<DramaClassification> {
}
@@ -0,0 +1,9 @@
// Author: fruit.com.cn
package com.fruitshorts.content.dal.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.fruitshorts.content.dal.entity.DramaPlaylistCategoryDrama;
public interface DramaPlaylistCategoryDramaMapper extends BaseMapper<DramaPlaylistCategoryDrama> {
}
@@ -0,0 +1,9 @@
// Author: fruit.com.cn
package com.fruitshorts.content.dal.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.fruitshorts.content.dal.entity.DramaPlaylistCategory;
public interface DramaPlaylistCategoryMapper extends BaseMapper<DramaPlaylistCategory> {
}
@@ -0,0 +1,204 @@
// Author: fruit.com.cn
package com.fruitshorts.content.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fruitshorts.common.exception.ErrorCode;
import com.fruitshorts.common.exception.ServiceException;
import com.fruitshorts.common.pojo.PageResult;
import com.fruitshorts.content.dal.entity.Drama;
import com.fruitshorts.content.dal.entity.DramaClassification;
import com.fruitshorts.content.dal.entity.DramaClassificationDrama;
import com.fruitshorts.content.dal.mapper.DramaClassificationDramaMapper;
import com.fruitshorts.content.dal.mapper.DramaClassificationMapper;
import com.fruitshorts.content.dal.mapper.DramaMapper;
import com.fruitshorts.content.vo.DramaBriefVO;
import com.fruitshorts.content.vo.DramaClassificationRespVO;
import com.fruitshorts.content.vo.DramaClassificationSaveReqVO;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 运营分类服务。分类是独立于「短剧类型(drama_category)」的一套概念,
* 由运营自定义增删,并通过关联表挂载任意多部短剧。
*/
@Service
@RequiredArgsConstructor
public class DramaClassificationService {
private final DramaClassificationMapper classificationMapper;
private final DramaClassificationDramaMapper classificationDramaMapper;
private final DramaMapper dramaMapper;
public List<DramaClassificationRespVO> list() {
Map<Long, Integer> counts = countDramas();
return classificationMapper.selectList(Wrappers.<DramaClassification>lambdaQuery()
.orderByAsc(DramaClassification::getSort)
.orderByDesc(DramaClassification::getId))
.stream().map(item -> {
DramaClassificationRespVO vo = new DramaClassificationRespVO();
vo.setId(item.getId());
vo.setName(item.getName());
vo.setSort(item.getSort());
vo.setStatus(item.getStatus());
vo.setUpdatedAt(item.getUpdatedAt());
vo.setDramaCount(counts.getOrDefault(item.getId(), 0));
return vo;
}).toList();
}
public Long create(DramaClassificationSaveReqVO req) {
ensureNameAvailable(req.getName(), null);
DramaClassification entity = new DramaClassification();
entity.setName(req.getName().trim());
Long count = classificationMapper.selectCount(null);
entity.setSort(req.getSort() == null
? (count == null ? 0 : count.intValue()) + 1
: req.getSort());
entity.setStatus(req.getStatus() == null ? 1 : req.getStatus());
classificationMapper.insert(entity);
return entity.getId();
}
public void update(Long id, DramaClassificationSaveReqVO req) {
DramaClassification entity = getExisting(id);
ensureNameAvailable(req.getName(), id);
entity.setName(req.getName().trim());
if (req.getSort() != null) {
entity.setSort(req.getSort());
}
if (req.getStatus() != null) {
entity.setStatus(req.getStatus());
}
classificationMapper.updateById(entity);
}
@Transactional(rollbackFor = Exception.class)
public void delete(Long id) {
getExisting(id);
classificationDramaMapper.delete(Wrappers.<DramaClassificationDrama>lambdaQuery()
.eq(DramaClassificationDrama::getClassificationId, id));
classificationMapper.deleteById(id);
}
/** 分类下已关联的短剧(分页)。 */
public PageResult<DramaBriefVO> pageDramas(Long id, String keyword,
int pageIndex, int pageSize) {
return pageDramas(id, keyword, pageIndex, pageSize, true);
}
/** 尚未关联到该分类的短剧(分页),供「添加短剧」多选。 */
public PageResult<DramaBriefVO> pageCandidates(Long id, String keyword,
int pageIndex, int pageSize) {
return pageDramas(id, keyword, pageIndex, pageSize, false);
}
/** 批量关联短剧;已关联的自动跳过。 */
@Transactional(rollbackFor = Exception.class)
public int bindDramas(Long id, List<Long> dramaIds) {
getExisting(id);
if (dramaIds == null || dramaIds.isEmpty()) {
return 0;
}
int added = 0;
for (Long dramaId : dramaIds) {
if (dramaId == null || dramaMapper.selectById(dramaId) == null) {
continue;
}
Long exists = classificationDramaMapper.selectCount(
Wrappers.<DramaClassificationDrama>lambdaQuery()
.eq(DramaClassificationDrama::getClassificationId, id)
.eq(DramaClassificationDrama::getDramaId, dramaId));
if (exists != null && exists > 0) {
continue;
}
DramaClassificationDrama relation = new DramaClassificationDrama();
relation.setClassificationId(id);
relation.setDramaId(dramaId);
classificationDramaMapper.insert(relation);
added++;
}
return added;
}
/** 移出分类;短剧本身不受影响。 */
public void unbindDrama(Long id, Long dramaId) {
getExisting(id);
classificationDramaMapper.delete(Wrappers.<DramaClassificationDrama>lambdaQuery()
.eq(DramaClassificationDrama::getClassificationId, id)
.eq(DramaClassificationDrama::getDramaId, dramaId));
}
private PageResult<DramaBriefVO> pageDramas(Long id, String keyword,
int pageIndex, int pageSize, boolean bound) {
getExisting(id);
Page<Drama> page = new Page<>(Math.max(1, pageIndex), Math.max(1, Math.min(pageSize, 100)));
LambdaQueryWrapper<Drama> wrapper = Wrappers.<Drama>lambdaQuery()
.like(StringUtils.hasText(keyword), Drama::getTitle, keyword)
.orderByDesc(Drama::getSort)
.orderByDesc(Drama::getId);
String subQuery = "SELECT drama_id FROM drama_classification_drama WHERE classification_id = " + id;
if (bound) {
wrapper.inSql(Drama::getId, subQuery);
} else {
wrapper.notInSql(Drama::getId, subQuery);
}
IPage<Drama> result = dramaMapper.selectPage(page, wrapper);
return new PageResult<>(result.getTotal(),
result.getRecords().stream().map(this::toBrief).toList());
}
private Map<Long, Integer> countDramas() {
Map<Long, Integer> counts = new HashMap<>();
classificationDramaMapper.selectMaps(new QueryWrapper<DramaClassificationDrama>()
.select("classification_id AS cid", "COUNT(*) AS cnt")
.groupBy("classification_id"))
.forEach(row -> {
Object cid = row.get("cid");
Object cnt = row.get("cnt");
if (cid instanceof Number c && cnt instanceof Number n) {
counts.put(c.longValue(), n.intValue());
}
});
return counts;
}
private DramaClassification getExisting(Long id) {
DramaClassification entity = id == null ? null : classificationMapper.selectById(id);
if (entity == null) {
throw new ServiceException(ErrorCode.DATA_NOT_FOUND, "分类不存在");
}
return entity;
}
private void ensureNameAvailable(String name, Long excludeId) {
Long count = classificationMapper.selectCount(Wrappers.<DramaClassification>lambdaQuery()
.eq(DramaClassification::getName, name.trim())
.ne(excludeId != null, DramaClassification::getId, excludeId));
if (count != null && count > 0) {
throw new ServiceException(ErrorCode.DATA_EXISTS, "分类名称已存在");
}
}
private DramaBriefVO toBrief(Drama drama) {
DramaBriefVO vo = new DramaBriefVO();
vo.setId(drama.getId());
vo.setTitle(drama.getTitle());
vo.setCover(drama.getCoverUrl());
vo.setStatus(drama.getStatus());
vo.setTotalEpisodes(drama.getTotalEpisodes());
vo.setSort(drama.getSort());
vo.setUpdatedAt(drama.getUpdatedAt());
return vo;
}
}
@@ -0,0 +1,208 @@
// Author: fruit.com.cn
package com.fruitshorts.content.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fruitshorts.common.exception.ErrorCode;
import com.fruitshorts.common.exception.ServiceException;
import com.fruitshorts.common.pojo.PageResult;
import com.fruitshorts.content.dal.entity.Drama;
import com.fruitshorts.content.dal.entity.DramaPlaylistCategory;
import com.fruitshorts.content.dal.entity.DramaPlaylistCategoryDrama;
import com.fruitshorts.content.dal.mapper.DramaMapper;
import com.fruitshorts.content.dal.mapper.DramaPlaylistCategoryDramaMapper;
import com.fruitshorts.content.dal.mapper.DramaPlaylistCategoryMapper;
import com.fruitshorts.content.vo.DramaBriefVO;
import com.fruitshorts.content.vo.DramaPlaylistCategoryRespVO;
import com.fruitshorts.content.vo.DramaPlaylistCategorySaveReqVO;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 剧单分类服务:分类直接关联短剧,没有「剧单」这一层。
* 这套分类与「类型」「分类管理」都不是同一套数据。
*/
@Service
@RequiredArgsConstructor
public class DramaPlaylistCategoryService {
private final DramaPlaylistCategoryMapper categoryMapper;
private final DramaPlaylistCategoryDramaMapper categoryDramaMapper;
private final DramaMapper dramaMapper;
public List<DramaPlaylistCategoryRespVO> list() {
Map<Long, Integer> counts = countDramas();
return categoryMapper.selectList(Wrappers.<DramaPlaylistCategory>lambdaQuery()
.orderByAsc(DramaPlaylistCategory::getSort)
.orderByDesc(DramaPlaylistCategory::getId))
.stream().map(item -> {
DramaPlaylistCategoryRespVO vo = new DramaPlaylistCategoryRespVO();
vo.setId(item.getId());
vo.setName(item.getName());
vo.setDescription(item.getDescription());
vo.setSort(item.getSort());
vo.setStatus(item.getStatus());
vo.setDramaCount(counts.getOrDefault(item.getId(), 0));
return vo;
}).toList();
}
public Long create(DramaPlaylistCategorySaveReqVO req) {
ensureNameAvailable(req.getName(), null);
DramaPlaylistCategory entity = new DramaPlaylistCategory();
entity.setName(req.getName().trim());
entity.setDescription(req.getDescription() == null ? "" : req.getDescription().trim());
Long count = categoryMapper.selectCount(null);
entity.setSort(req.getSort() == null
? (count == null ? 0 : count.intValue()) + 1
: req.getSort());
entity.setStatus(req.getStatus() == null ? 1 : req.getStatus());
categoryMapper.insert(entity);
return entity.getId();
}
public void update(Long id, DramaPlaylistCategorySaveReqVO req) {
DramaPlaylistCategory entity = getExisting(id);
ensureNameAvailable(req.getName(), id);
entity.setName(req.getName().trim());
if (req.getDescription() != null) {
entity.setDescription(req.getDescription().trim());
}
if (req.getSort() != null) {
entity.setSort(req.getSort());
}
if (req.getStatus() != null) {
entity.setStatus(req.getStatus());
}
categoryMapper.updateById(entity);
}
@Transactional(rollbackFor = Exception.class)
public void delete(Long id) {
getExisting(id);
categoryDramaMapper.delete(Wrappers.<DramaPlaylistCategoryDrama>lambdaQuery()
.eq(DramaPlaylistCategoryDrama::getCategoryId, id));
categoryMapper.deleteById(id);
}
/** 分类下已关联的短剧(分页)。 */
public PageResult<DramaBriefVO> pageDramas(Long id, String keyword,
int pageIndex, int pageSize) {
return pageDramas(id, keyword, pageIndex, pageSize, true);
}
/** 尚未关联到该分类的短剧(分页),供「选择短剧」多选。 */
public PageResult<DramaBriefVO> pageCandidates(Long id, String keyword,
int pageIndex, int pageSize) {
return pageDramas(id, keyword, pageIndex, pageSize, false);
}
/** 批量把短剧加入分类;已关联的自动跳过。 */
@Transactional(rollbackFor = Exception.class)
public int bindDramas(Long id, List<Long> dramaIds) {
getExisting(id);
if (dramaIds == null || dramaIds.isEmpty()) {
return 0;
}
int added = 0;
for (Long dramaId : dramaIds) {
if (dramaId == null || dramaMapper.selectById(dramaId) == null) {
continue;
}
Long exists = categoryDramaMapper.selectCount(
Wrappers.<DramaPlaylistCategoryDrama>lambdaQuery()
.eq(DramaPlaylistCategoryDrama::getCategoryId, id)
.eq(DramaPlaylistCategoryDrama::getDramaId, dramaId));
if (exists != null && exists > 0) {
continue;
}
DramaPlaylistCategoryDrama relation = new DramaPlaylistCategoryDrama();
relation.setCategoryId(id);
relation.setDramaId(dramaId);
categoryDramaMapper.insert(relation);
added++;
}
return added;
}
/** 把短剧移出分类;短剧本身不受影响。 */
public void unbindDrama(Long id, Long dramaId) {
getExisting(id);
categoryDramaMapper.delete(Wrappers.<DramaPlaylistCategoryDrama>lambdaQuery()
.eq(DramaPlaylistCategoryDrama::getCategoryId, id)
.eq(DramaPlaylistCategoryDrama::getDramaId, dramaId));
}
private PageResult<DramaBriefVO> pageDramas(Long id, String keyword,
int pageIndex, int pageSize, boolean bound) {
getExisting(id);
Page<Drama> page = new Page<>(Math.max(1, pageIndex), Math.max(1, Math.min(pageSize, 100)));
LambdaQueryWrapper<Drama> wrapper = Wrappers.<Drama>lambdaQuery()
.like(StringUtils.hasText(keyword), Drama::getTitle, keyword)
.orderByDesc(Drama::getSort)
.orderByDesc(Drama::getId);
String subQuery = "SELECT drama_id FROM drama_playlist_category_drama WHERE category_id = " + id;
if (bound) {
wrapper.inSql(Drama::getId, subQuery);
} else {
wrapper.notInSql(Drama::getId, subQuery);
}
IPage<Drama> result = dramaMapper.selectPage(page, wrapper);
return new PageResult<>(result.getTotal(),
result.getRecords().stream().map(this::toBrief).toList());
}
private Map<Long, Integer> countDramas() {
Map<Long, Integer> counts = new HashMap<>();
categoryDramaMapper.selectMaps(new QueryWrapper<DramaPlaylistCategoryDrama>()
.select("category_id AS cid", "COUNT(*) AS cnt")
.groupBy("category_id"))
.forEach(row -> {
Object cid = row.get("cid");
Object cnt = row.get("cnt");
if (cid instanceof Number c && cnt instanceof Number n) {
counts.put(c.longValue(), n.intValue());
}
});
return counts;
}
private DramaPlaylistCategory getExisting(Long id) {
DramaPlaylistCategory entity = id == null ? null : categoryMapper.selectById(id);
if (entity == null) {
throw new ServiceException(ErrorCode.DATA_NOT_FOUND, "剧单分类不存在");
}
return entity;
}
private void ensureNameAvailable(String name, Long excludeId) {
Long count = categoryMapper.selectCount(Wrappers.<DramaPlaylistCategory>lambdaQuery()
.eq(DramaPlaylistCategory::getName, name.trim())
.ne(excludeId != null, DramaPlaylistCategory::getId, excludeId));
if (count != null && count > 0) {
throw new ServiceException(ErrorCode.DATA_EXISTS, "剧单分类名称已存在");
}
}
private DramaBriefVO toBrief(Drama drama) {
DramaBriefVO vo = new DramaBriefVO();
vo.setId(drama.getId());
vo.setTitle(drama.getTitle());
vo.setCover(drama.getCoverUrl());
vo.setStatus(drama.getStatus());
vo.setTotalEpisodes(drama.getTotalEpisodes());
vo.setSort(drama.getSort());
vo.setUpdatedAt(drama.getUpdatedAt());
return vo;
}
}
@@ -0,0 +1,20 @@
// Author: fruit.com.cn
package com.fruitshorts.content.vo;
import lombok.Data;
import java.time.LocalDateTime;
/** 短剧精简信息:分类关联页列表用。 */
@Data
public class DramaBriefVO {
private Long id;
private String title;
private String cover;
private String status;
private Integer totalEpisodes;
private Integer sort;
private LocalDateTime updatedAt;
}
@@ -0,0 +1,19 @@
// Author: fruit.com.cn
package com.fruitshorts.content.vo;
import lombok.Data;
import java.time.LocalDateTime;
/** 分类列表项:带关联短剧数量。 */
@Data
public class DramaClassificationRespVO {
private Long id;
private String name;
private Integer sort;
private Integer status;
private Integer dramaCount;
private LocalDateTime updatedAt;
}
@@ -0,0 +1,17 @@
// Author: fruit.com.cn
package com.fruitshorts.content.vo;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/** 分类新增/编辑请求体。 */
@Data
public class DramaClassificationSaveReqVO {
@NotBlank(message = "请填写分类名称")
private String name;
private Integer sort;
private Integer status;
}
@@ -0,0 +1,16 @@
// Author: fruit.com.cn
package com.fruitshorts.content.vo;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
import java.util.List;
/** 批量操作短剧 id 的通用请求体。 */
@Data
public class DramaIdsReqVO {
@NotEmpty(message = "请选择短剧")
private List<Long> dramaIds;
}
@@ -0,0 +1,18 @@
// Author: fruit.com.cn
package com.fruitshorts.content.vo;
import lombok.Data;
/** 剧单分类列表项:带该分类下的短剧数量。 */
@Data
public class DramaPlaylistCategoryRespVO {
private Long id;
private String name;
private String description;
private Integer sort;
private Integer status;
/** 该分类下已关联的短剧数量 */
private Integer dramaCount;
}
@@ -0,0 +1,21 @@
// Author: fruit.com.cn
package com.fruitshorts.content.vo;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Data;
/** 剧单分类新增/编辑请求体。 */
@Data
public class DramaPlaylistCategorySaveReqVO {
@NotBlank(message = "请填写剧单分类名称")
private String name;
@Size(max = 255, message = "描述最多 255 个字符")
private String description;
private Integer sort;
private Integer status;
}
+12 -2
View File
@@ -57,6 +57,8 @@ INSERT INTO sys_menu (id, parent_id, title, route_path, route_name, component, r
(5, 3, '剧集管理', 'episodes', 'episodes', 'views/contentManagement/EpisodeManagementView', '', '', 'episode:list', 'MENU', 0, 0, 2),
(12, 3, '轮播图管理', 'banners', 'banners', 'views/contentManagement/BannerManagementView', '', '', 'banner:list', 'MENU', 0, 0, 3),
(13, 3, '类型管理', 'categories', 'categories', 'views/contentManagement/CategoryManagementView','', '', 'drama:list', 'MENU', 0, 0, 4),
(36, 3, '分类管理', 'category-dramas','category-dramas','views/contentManagement/CategoryDramaView', '', '', 'drama:list', 'MENU', 0, 0, 5),
(37, 3, '剧单管理', 'drama-playlists','drama-playlists','views/contentManagement/DramaPlaylistView', '', '', 'drama:list', 'MENU', 0, 0, 6),
(6, 0, '后台管理', '/system', 'System', '#', '/system/access', 'material-symbols:settings-rounded', 'system:role', 'CATALOG', 1, 0, 3),
(7, 6, '角色权限配置', 'access', 'access', 'views/permission/RoleManagementView', '', '', 'system:role', 'MENU', 0, 0, 1),
(8, 6, '管理员用户列表','admin-users','admin-users', 'views/userManagement/AdminUsersView', '', '', 'system:admin-user','MENU', 0, 0, 2),
@@ -77,9 +79,17 @@ ON CONFLICT (id) DO UPDATE SET parent_id = EXCLUDED.parent_id, title = EXCLUDED.
redirect = EXCLUDED.redirect, icon = EXCLUDED.icon, permission_code = EXCLUDED.permission_code,
type = EXCLUDED.type, always_show = EXCLUDED.always_show, affix = EXCLUDED.affix, sort = EXCLUDED.sort;
-- 转码设置菜单(挂在后台管理下,仅超级管理员可见)
INSERT INTO sys_menu (id, parent_id, title, route_path, route_name, component, redirect, icon,
permission_code, type, always_show, affix, sort) VALUES
(35, 6, '转码设置', 'transcode-settings', 'transcode-settings', 'views/systemManagement/TranscodeSettingsView', '', '', 'episode:upload', 'MENU', 0, 0, 7)
ON CONFLICT (id) DO UPDATE SET parent_id = EXCLUDED.parent_id, title = EXCLUDED.title,
route_path = EXCLUDED.route_path, route_name = EXCLUDED.route_name, component = EXCLUDED.component,
redirect = EXCLUDED.redirect, icon = EXCLUDED.icon, permission_code = EXCLUDED.permission_code,
type = EXCLUDED.type, always_show = EXCLUDED.always_show, affix = EXCLUDED.affix, sort = EXCLUDED.sort;
-- K23:二级菜单统一只显示文字,历史图标值也一并清空。
UPDATE sys_menu SET icon = '' WHERE type = 'MENU';
-- ---------- 角色-权限 ----------
INSERT INTO sys_role_permission (id, role_id, permission_id)
SELECT r.id * 1000000 + p.id, r.id, p.id FROM sys_role r CROSS JOIN sys_permission p WHERE r.code = 'super_admin'
@@ -112,7 +122,7 @@ ON CONFLICT DO NOTHING;
INSERT INTO sys_role_menu (id, role_id, menu_id)
SELECT r.id * 1000000 + m.id, r.id, m.id FROM sys_role r CROSS JOIN sys_menu m
WHERE r.code IN ('content_operator','content_reviewer','data_analyst') AND m.id IN (1,2,3,4,5,12,13)
WHERE r.code IN ('content_operator','content_reviewer','data_analyst') AND m.id IN (1,2,3,4,5,12,13,36,37)
ON CONFLICT DO NOTHING;
-- ---------- 管理员(密码均为 admin123) ----------
@@ -0,0 +1,21 @@
-- Author: fruit.com.cn
-- 后台「分类管理」菜单与分类-短剧关联页(幂等,仅提交文件,由发布流程执行)
INSERT INTO sys_menu (id, parent_id, title, route_path, route_name, component, redirect, icon,
permission_code, type, always_show, affix, sort) VALUES
(36, 3, '分类管理', 'category-dramas', 'category-dramas',
'views/contentManagement/CategoryDramaView', '', '',
'drama:list', 'MENU', 0, 0, 5)
ON CONFLICT (id) DO UPDATE SET parent_id = EXCLUDED.parent_id, title = EXCLUDED.title,
route_path = EXCLUDED.route_path, route_name = EXCLUDED.route_name,
component = EXCLUDED.component, redirect = EXCLUDED.redirect, icon = EXCLUDED.icon,
permission_code = EXCLUDED.permission_code, type = EXCLUDED.type,
always_show = EXCLUDED.always_show, affix = EXCLUDED.affix, sort = EXCLUDED.sort;
INSERT INTO sys_role_menu (id, role_id, menu_id)
SELECT r.id * 1000000 + m.id, r.id, m.id
FROM sys_role r
CROSS JOIN sys_menu m
WHERE r.code IN ('super_admin', 'content_operator', 'content_reviewer', 'data_analyst')
AND m.id = 36
ON CONFLICT DO NOTHING;
@@ -0,0 +1,31 @@
-- Author: fruit.com.cn
-- 运营分类(与「类型」drama_category 是两个概念):分类可自定义增删,并关联多部短剧
-- 幂等,仅提交文件,由发布流程执行
CREATE TABLE IF NOT EXISTS drama_classification (
id BIGINT PRIMARY KEY,
name VARCHAR(64) NOT NULL,
sort INTEGER NOT NULL DEFAULT 0,
status SMALLINT NOT NULL DEFAULT 1,
created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE UNIQUE INDEX IF NOT EXISTS uk_drama_classification_name ON drama_classification (name);
COMMENT ON TABLE drama_classification IS '运营分类;与短剧类型(drama_category)相互独立';
CREATE TABLE IF NOT EXISTS drama_classification_drama (
id BIGINT PRIMARY KEY,
classification_id BIGINT NOT NULL,
drama_id BIGINT NOT NULL,
created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE UNIQUE INDEX IF NOT EXISTS uk_classification_drama
ON drama_classification_drama (classification_id, drama_id);
CREATE INDEX IF NOT EXISTS idx_classification_drama_drama
ON drama_classification_drama (drama_id);
COMMENT ON TABLE drama_classification_drama IS '分类与短剧的关联关系';
DROP TRIGGER IF EXISTS trg_drama_classification_updated_at ON drama_classification;
CREATE TRIGGER trg_drama_classification_updated_at
BEFORE UPDATE ON drama_classification
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
@@ -0,0 +1,21 @@
-- Author: fruit.com.cn
-- 后台「剧单管理」菜单(幂等,仅提交文件,由发布流程执行)
INSERT INTO sys_menu (id, parent_id, title, route_path, route_name, component, redirect, icon,
permission_code, type, always_show, affix, sort) VALUES
(37, 3, '剧单管理', 'drama-playlists', 'drama-playlists',
'views/contentManagement/DramaPlaylistView', '', '',
'drama:list', 'MENU', 0, 0, 6)
ON CONFLICT (id) DO UPDATE SET parent_id = EXCLUDED.parent_id, title = EXCLUDED.title,
route_path = EXCLUDED.route_path, route_name = EXCLUDED.route_name,
component = EXCLUDED.component, redirect = EXCLUDED.redirect, icon = EXCLUDED.icon,
permission_code = EXCLUDED.permission_code, type = EXCLUDED.type,
always_show = EXCLUDED.always_show, affix = EXCLUDED.affix, sort = EXCLUDED.sort;
INSERT INTO sys_role_menu (id, role_id, menu_id)
SELECT r.id * 1000000 + m.id, r.id, m.id
FROM sys_role r
CROSS JOIN sys_menu m
WHERE r.code IN ('super_admin', 'content_operator', 'content_reviewer', 'data_analyst')
AND m.id = 37
ON CONFLICT DO NOTHING;
+31
View File
@@ -0,0 +1,31 @@
-- Author: fruit.com.cn
-- 剧单:运营编排的短剧集合,归属某个分类,并可关联多部短剧
-- 幂等,仅提交文件,由发布流程执行
CREATE TABLE IF NOT EXISTS drama_playlist (
id BIGINT PRIMARY KEY,
classification_id BIGINT NOT NULL DEFAULT 0,
name VARCHAR(64) NOT NULL,
sort INTEGER NOT NULL DEFAULT 0,
status SMALLINT NOT NULL DEFAULT 1,
created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE UNIQUE INDEX IF NOT EXISTS uk_drama_playlist_name ON drama_playlist (name);
CREATE INDEX IF NOT EXISTS idx_drama_playlist_classification ON drama_playlist (classification_id);
COMMENT ON TABLE drama_playlist IS '剧单;归属分类,可关联多部短剧';
CREATE TABLE IF NOT EXISTS drama_playlist_drama (
id BIGINT PRIMARY KEY,
playlist_id BIGINT NOT NULL,
drama_id BIGINT NOT NULL,
created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE UNIQUE INDEX IF NOT EXISTS uk_playlist_drama ON drama_playlist_drama (playlist_id, drama_id);
CREATE INDEX IF NOT EXISTS idx_playlist_drama_drama ON drama_playlist_drama (drama_id);
COMMENT ON TABLE drama_playlist_drama IS '剧单与短剧的关联关系';
DROP TRIGGER IF EXISTS trg_drama_playlist_updated_at ON drama_playlist;
CREATE TRIGGER trg_drama_playlist_updated_at
BEFORE UPDATE ON drama_playlist
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
@@ -0,0 +1,18 @@
-- Author: fruit.com.cn
-- 剧单分类直接关联短剧(不再有「剧单」这一层)
-- 幂等,仅提交文件,由发布流程执行
CREATE TABLE IF NOT EXISTS drama_playlist_category_drama (
id BIGINT PRIMARY KEY,
category_id BIGINT NOT NULL,
drama_id BIGINT NOT NULL,
created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE UNIQUE INDEX IF NOT EXISTS uk_playlist_category_drama
ON drama_playlist_category_drama (category_id, drama_id);
CREATE INDEX IF NOT EXISTS idx_playlist_category_drama_drama
ON drama_playlist_category_drama (drama_id);
COMMENT ON TABLE drama_playlist_category_drama IS '剧单分类与短剧的关联关系';
DROP TABLE IF EXISTS drama_playlist_drama;
DROP TABLE IF EXISTS drama_playlist;
@@ -0,0 +1,31 @@
-- Author: fruit.com.cn
-- 剧单专属分类(与「分类管理」的运营分类无关),并调整剧单归属字段
-- 幂等,仅提交文件,由发布流程执行
CREATE TABLE IF NOT EXISTS drama_playlist_category (
id BIGINT PRIMARY KEY,
name VARCHAR(64) NOT NULL,
sort INTEGER NOT NULL DEFAULT 0,
status SMALLINT NOT NULL DEFAULT 1,
created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE UNIQUE INDEX IF NOT EXISTS uk_drama_playlist_category_name
ON drama_playlist_category (name);
COMMENT ON TABLE drama_playlist_category IS '剧单专属分类;与运营分类(drama_classification)相互独立';
ALTER TABLE drama_playlist DROP COLUMN IF EXISTS classification_id;
ALTER TABLE drama_playlist ADD COLUMN IF NOT EXISTS playlist_category_id BIGINT NOT NULL DEFAULT 0;
CREATE INDEX IF NOT EXISTS idx_drama_playlist_category
ON drama_playlist (playlist_category_id);
COMMENT ON COLUMN drama_playlist.playlist_category_id IS '剧单分类;0 表示未分类';
DROP TRIGGER IF EXISTS trg_drama_playlist_category_updated_at ON drama_playlist_category;
CREATE TRIGGER trg_drama_playlist_category_updated_at
BEFORE UPDATE ON drama_playlist_category
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
-- Author: fruit.com.cn
-- 剧单分类增加描述字段(幂等)
ALTER TABLE drama_playlist_category ADD COLUMN IF NOT EXISTS description VARCHAR(255) NOT NULL DEFAULT '';
COMMENT ON COLUMN drama_playlist_category.description IS '分类描述';
+58 -1
View File
@@ -869,9 +869,64 @@ CREATE INDEX IF NOT EXISTS idx_audit_log_biz ON audit_log (biz_type, biz_id, cre
COMMENT ON TABLE audit_log IS '业务对象变更审计(支付、权益、定价等敏感操作)';
-- ============================================================
-- 九、updated_at 触发器(与已有表保持一致)
-- 八之二、运营分类(与短剧类型 drama_category 相互独立)
-- ============================================================
CREATE TABLE IF NOT EXISTS drama_classification (
id BIGINT PRIMARY KEY,
name VARCHAR(64) NOT NULL,
sort INTEGER NOT NULL DEFAULT 0,
status SMALLINT NOT NULL DEFAULT 1,
created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE UNIQUE INDEX IF NOT EXISTS uk_drama_classification_name ON drama_classification (name);
COMMENT ON TABLE drama_classification IS '运营分类;与短剧类型(drama_category)相互独立';
CREATE TABLE IF NOT EXISTS drama_classification_drama (
id BIGINT PRIMARY KEY,
classification_id BIGINT NOT NULL,
drama_id BIGINT NOT NULL,
created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE UNIQUE INDEX IF NOT EXISTS uk_classification_drama
ON drama_classification_drama (classification_id, drama_id);
CREATE INDEX IF NOT EXISTS idx_classification_drama_drama
ON drama_classification_drama (drama_id);
COMMENT ON TABLE drama_classification_drama IS '分类与短剧的关联关系';
-- ============================================================
-- 八之三、剧单(剧单有自己的一套分类,与运营分类无关)
-- ============================================================
CREATE TABLE IF NOT EXISTS drama_playlist_category (
id BIGINT PRIMARY KEY,
name VARCHAR(64) NOT NULL,
description VARCHAR(255) NOT NULL DEFAULT '',
sort INTEGER NOT NULL DEFAULT 0,
status SMALLINT NOT NULL DEFAULT 1,
created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE UNIQUE INDEX IF NOT EXISTS uk_drama_playlist_category_name
ON drama_playlist_category (name);
COMMENT ON TABLE drama_playlist_category IS '剧单专属分类;与运营分类(drama_classification)相互独立';
CREATE TABLE IF NOT EXISTS drama_playlist_category_drama (
id BIGINT PRIMARY KEY,
category_id BIGINT NOT NULL,
drama_id BIGINT NOT NULL,
created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE UNIQUE INDEX IF NOT EXISTS uk_playlist_category_drama
ON drama_playlist_category_drama (category_id, drama_id);
CREATE INDEX IF NOT EXISTS idx_playlist_category_drama_drama
ON drama_playlist_category_drama (drama_id);
COMMENT ON TABLE drama_playlist_category_drama IS '剧单分类与短剧的关联关系';
-- ============================================================
-- 九、updated_at 触发器(与已有表保持一致)
-- ============================================================
DO $$
DECLARE
t TEXT;
@@ -885,6 +940,8 @@ BEGIN
'reconcile_task', 'creator', 'creator_document', 'content_submission',
'creator_stat_daily', 'creator_settlement',
'transcode_task', 'media_rendition', 'media_thumbnail',
'drama_classification',
'drama_playlist_category',
'ops_notice', 'ops_daily_stat'
]
LOOP