merge: 合并 K93 短剧类型多选
This commit is contained in:
commit
33c1172da5
13 files changed
+665
-50
No files matched your search
+15
-3
@@ -181,10 +181,13 @@ app.register.max-per-device-per-day
|
||||
> `viewCount` 热度仍取整部剧。首页、发现、分类、收藏、搜索等列表卡片结构一致。
|
||||
>
|
||||
> 卡片新增 `dramaType`:`ANIME` 表示漫剧,`LIVE` 表示真人剧。
|
||||
> 客户端字段仍返回主内容类型;后台多选值保存在 `drama.drama_types`,
|
||||
> App 类型筛选按逗号列整词匹配,因此 `ANIME,LIVE` 多标签剧可被任一类型命中。
|
||||
|
||||
#### 推荐类型流(`GET /app/feed`)
|
||||
|
||||
- `types` 可选,逗号分隔,支持 `ANIME`、`LIVE` 或多选 `ANIME,LIVE`;不传或空值返回全部已上线内容。
|
||||
过滤按 `drama_types` 整词匹配,多标签剧可被多个类型同时命中。
|
||||
- `cursor` 可选,上一页返回的 `nextCursor`;`size` 默认 20,最大 50。
|
||||
- 排序:`recommend_weight DESC NULLS LAST, view_count DESC, id DESC`。
|
||||
- 返回 `Result<CursorResult<AppDramaCardVO>>`,卡片字段与首页一致,包含互动计数、`preview`、`language`、`dramaType`。
|
||||
@@ -527,10 +530,10 @@ GET /app/notices?pageIndex=1&pageSize=20
|
||||
|
||||
| 方法 | 路径 | 权限点 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| GET | `/admin/dramas/page` | `drama:list` | 检索(含热门、状态筛选) |
|
||||
| GET | `/admin/dramas/page` | `drama:list` | 检索(含热门、状态、`dramaTypes[]`、`categoryIds[]` 多选筛选) |
|
||||
| GET | `/admin/dramas/{id}` | `drama:list` | 详情 |
|
||||
| POST | `/admin/dramas` | `drama:create` | 新建短剧 |
|
||||
| PUT | `/admin/dramas/{id}` | `drama:update` | 编辑短剧 |
|
||||
| POST | `/admin/dramas` | `drama:create` | 新建短剧;优先接收 `dramaTypes[]` / `categoryIds[]` |
|
||||
| PUT | `/admin/dramas/{id}` | `drama:update` | 编辑短剧;优先接收 `dramaTypes[]` / `categoryIds[]` |
|
||||
| DELETE | `/admin/dramas/{id}` | `drama:delete` | 删除短剧 |
|
||||
| POST | `/admin/dramas/{id}/submit` | `drama:review` / `drama:update` | 提交审核 |
|
||||
| POST | `/admin/dramas/{id}/approve` | `drama:review` | 审核通过 |
|
||||
@@ -539,6 +542,15 @@ GET /app/notices?pageIndex=1&pageSize=20
|
||||
| POST | `/admin/dramas/{id}/offline` | `drama:review` | 下线 |
|
||||
| GET | `/admin/dramas/{id}/review-records` | `drama:list` | 审核记录 |
|
||||
|
||||
多选字段说明:
|
||||
|
||||
- 保存优先使用 `dramaTypes[]` / `categoryIds[]`,去重并保持提交顺序;
|
||||
`drama_type` / `category_id` 写第一个值,供旧客户端与旧接口兼容。
|
||||
- 仅传 `dramaType` / `categoryId` 时保持原单值行为。
|
||||
- 列表响应返回 `dramaTypes[]`、`categoryIds[]` 与按选中顺序映射的 `categoryNames[]`;
|
||||
分类名缺失时回退 `genre`。
|
||||
- 后台与 App 过滤均使用 `(',' || column || ',') LIKE '%,value,%'` 整词匹配。
|
||||
|
||||
**剧集**
|
||||
|
||||
| 方法 | 路径 | 权限点 | 说明 |
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
# K93 短剧类型与内容类型支持多选
|
||||
|
||||
## 目标
|
||||
|
||||
- 后台短剧基础信息中的「类型」「内容类型」改为至少选一项的多选。
|
||||
- 后台列表筛选、列表展示支持多分类与多内容类型。
|
||||
- App 分类分区和内容类型 feed 使用多标签字段做整词匹配。
|
||||
- 单值字段继续保留,兼容创作者中心、旧接口和现有 App 卡片。
|
||||
|
||||
## 分支
|
||||
|
||||
- FruitServer:`feat/k93-drama-multi-tag`,从 `origin/develop` 创建。
|
||||
- ReelShortWeb:`feat/k93-drama-multi-tag-ui`,从 `origin/develop` 创建。
|
||||
|
||||
## A. FruitServer
|
||||
|
||||
### A1 数据库
|
||||
|
||||
- 新增 `sql/migrate-k93-drama-multi-tag.sql`:
|
||||
- `drama.drama_types VARCHAR(32) NOT NULL DEFAULT ''`
|
||||
- `drama.category_ids VARCHAR(255) NOT NULL DEFAULT ''`
|
||||
- 从现有 `drama_type / category_id` 幂等回填。
|
||||
- 同步 `sql/schema.sql` 的 drama 建表字段。
|
||||
- 只提交迁移文件,不执行数据库。
|
||||
|
||||
### A2 实体与 VO
|
||||
|
||||
- `Drama`:增加 `dramaTypes/categoryIds` 逗号字符串。
|
||||
- `DramaSaveReqVO`:
|
||||
- 新增 `List<String> dramaTypes`、`List<Long> categoryIds`,列表优先。
|
||||
- 保留 `dramaType/categoryId` 单值兜底。
|
||||
- `DramaPageReqVO`:增加 `List<Long> categoryIds`,保留 `categoryId`。
|
||||
- `DramaRespVO`:增加 `dramaTypes/categoryIds/categoryNames`,旧单值字段语义不变。
|
||||
|
||||
### A3 保存规则
|
||||
|
||||
- 内容类型列表:normalize、去重、保持提交顺序;主值写第一个,完整列表写 `drama_types`。
|
||||
- 分类列表:过滤 null/非正数、去重、保持提交顺序;主值写第一个或 0,完整列表写 `category_ids`。
|
||||
- 列表为空时回退单值;单值也没有时保留当前值。
|
||||
- 创建时内容类型最终为空则默认 LIVE。
|
||||
- 非法内容类型继续返回 BAD_REQUEST。
|
||||
|
||||
### A4 查询规则
|
||||
|
||||
统一使用整词匹配:
|
||||
|
||||
```sql
|
||||
(',' || column || ',') LIKE '%,value,%'
|
||||
```
|
||||
|
||||
- 后台分页:
|
||||
- `dramaTypes` 非空:`drama_types` token 匹配。
|
||||
- `categoryIds` 非空:`category_ids` token 匹配。
|
||||
- `categoryIds` 为空:回退 `category_id` 等值。
|
||||
- App `listPublishedFeed`:`drama_types` token 匹配。
|
||||
- App feed 游标:marker 的解析后 `dramaTypes` 与任一筛选类型相交才允许继续。
|
||||
- App `listPublished(categoryId)`:`category_ids` token 匹配,使多分类剧出现在每个分类分区。
|
||||
|
||||
### A5 响应装配
|
||||
|
||||
- 解析 `drama_types/category_ids`,空值回退单值。
|
||||
- `categoryNames` 按 categoryIds 顺序映射分类名。
|
||||
- 分类名缺失时回退 `genre`。
|
||||
- 现有 `dramaType/categoryId/category` 保持旧语义。
|
||||
|
||||
### A6 文档与测试
|
||||
|
||||
- 更新 `docs/api-phase1.md` 的后台保存/分页字段、App feed 和分类说明。
|
||||
- 增加服务测试覆盖:
|
||||
- 多类型/多分类去重、顺序与主值写回。
|
||||
- 单值兜底行为不变。
|
||||
- 后台分页 token 条件。
|
||||
- feed 多标签 marker 可被 ANIME/LIVE 命中。
|
||||
- App 分类分区 token 条件。
|
||||
- 响应多值解析和分类名顺序。
|
||||
- 门禁:`mvn test` 全模块通过。
|
||||
|
||||
## B. ReelShortWeb
|
||||
|
||||
### B1 类型
|
||||
|
||||
- `DramaItem` 增加 `dramaTypes/categoryIds/categoryNames`。
|
||||
- `DramaSaveData` 增加 `dramaTypes/categoryIds`。
|
||||
- `DramaPageParams` 增加 `categoryIds`。
|
||||
|
||||
### B2 表单
|
||||
|
||||
- `form.categoryIds: number[]`,默认空数组。
|
||||
- `form.dramaTypes: string[]`,创建默认 `['LIVE']`。
|
||||
- 两个下拉均使用 multiple、collapse-tags、collapse-tags-tooltip、filterable。
|
||||
- 校验至少 1 项。
|
||||
- 编辑时优先数组,缺失回退单值。
|
||||
- 提交数组;`genre` 使用所选分类名以 ` / ` 拼接。
|
||||
|
||||
### B3 筛选与表格
|
||||
|
||||
- 分类筛选改为 `categoryIds: number[]`。
|
||||
- 内容类型筛选改为 `dramaTypes: string[]`。
|
||||
- 查询传数组,重置为空数组。
|
||||
- 类型列显示 `categoryNames.join(' / ')`,回退 `genre`。
|
||||
- 内容类型列逐个渲染标签,缺失时回退 `dramaType`。
|
||||
|
||||
### B4 门禁
|
||||
|
||||
- `pnpm ts:check`
|
||||
- `pnpm build:dev`
|
||||
- 对修改文件执行定向 ESLint
|
||||
|
||||
## 风险与说明
|
||||
|
||||
1. 新代码依赖迁移后的 `drama_types/category_ids` 列,发布顺序必须先执行迁移再启动新版本。
|
||||
2. 逗号列仅存储受控内容类型和数字 id,不允许用户输入;查询值均来自校验后的数组。
|
||||
3. 主值继续保留,降低旧客户端、创作者中心和旧接口的兼容风险。
|
||||
4. 本次不改 specs 与 Flutter 客户端,不执行数据库迁移,不操作真机。
|
||||
|
||||
## Git
|
||||
|
||||
- FruitServer:`feat(content): 短剧类型与内容类型支持多选`
|
||||
- ReelShortWeb:`feat(admin): 短剧类型与内容类型多选`
|
||||
- 两仓库均 `--no-ff` 合回 `develop` 并 push。
|
||||
- 作者:`Zhou Weidong <502333560@qq.com>`;禁止 `git add -A`。
|
||||
@@ -23947,3 +23947,59 @@ dev 构建保留既有 `element-plus` circular chunk 警告,不影响成功退
|
||||
门禁已独立完成。
|
||||
- 本任务无数据库迁移;未修改 specs、Flutter 客户端、K91 评论列表与观看记录接口;
|
||||
未安装或操作真机,等待用户验收。
|
||||
|
||||
## 2026-10-21 K93 短剧类型与内容类型多选
|
||||
|
||||
- 已完整读取 K93 `docs/plans/INBOX.md` 及项目协作、编码、Git 规范。
|
||||
- 已创建 FruitServer `feat/k93-drama-multi-tag` 与 ReelShortWeb
|
||||
`feat/k93-drama-multi-tag-ui`,均基于最新 `origin/develop`。
|
||||
- 已先写计划 `docs/plans/2026-10-21-K93-drama-multi-tag.md`,明确双列存储、
|
||||
主值兼容、token 整词匹配、feed 游标与 Web 多选交互。
|
||||
- 本任务迁移仅写文件,不执行数据库;不改 specs 与 Flutter 客户端。
|
||||
- 已完成 A 主体代码:`Drama` / 保存、分页、响应 VO 增加双列与数组字段;
|
||||
`DramaService.apply` 支持多值去重、顺序保留和单值兜底;响应装配分类名顺序。
|
||||
- 后台分页、App feed 与 App 分类列表已改用逗号列整词匹配;feed 游标会校验 marker
|
||||
的多内容类型是否与筛选类型相交。
|
||||
- 已新增迁移 `sql/migrate-k93-drama-multi-tag.sql` 并同步 `sql/schema.sql`,迁移未执行;
|
||||
`docs/api-phase1.md` 已补多选保存、过滤与响应说明。
|
||||
- 已新增 `DramaServiceMultiTagTest`,覆盖多值去重/顺序/主值、单值兼容、后台 token 条件、
|
||||
feed 多标签游标、App 分类 token 条件与响应分类名顺序。
|
||||
- K93 定向后端测试已通过:
|
||||
|
||||
```text
|
||||
$ JAVA_HOME=<jdk21> mvn -pl fruitshorts-app-api -am \
|
||||
-Dtest=DramaServiceMultiTagTest,DramaServiceK88Test,DramaServiceFeedTest \
|
||||
-Dsurefire.failIfNoSpecifiedTests=false test
|
||||
[INFO] fruitshorts-app-api ... SUCCESS
|
||||
[INFO] Tests run: 12, Failures: 0, Errors: 0, Skipped: 0
|
||||
[INFO] BUILD SUCCESS
|
||||
```
|
||||
|
||||
解读:多值保存、单值兼容、后台分页、feed 多标签游标和分类分区匹配均通过。
|
||||
- 已完成 B:Web 类型与分页参数扩展;表单两个下拉改为多选并至少 1 项;编辑优先回填数组,
|
||||
缺失回退单值;筛选传数组;表格显示多分类名与多个内容类型标签。
|
||||
- ReelShortWeb `pnpm ts:check`、定向 ESLint 已通过;dev 构建已通过:
|
||||
|
||||
```text
|
||||
$ pnpm build:dev
|
||||
> fruitshorts-admin@1.0.0 build:dev
|
||||
> VITE_CJS_IGNORE_WARNING=1 vite build --mode dev
|
||||
Build successful. Please see dist-dev directory
|
||||
```
|
||||
|
||||
解读:多选表单、筛选与展示通过类型检查、Lint 和构建;构建仅有既有 circular chunk 警告。
|
||||
- 规范复核发现 `DramaService` 增至 618 行,超过 500 行红线;已将多标签存储、解析与
|
||||
Wrapper token 条件抽到同包 `DramaMultiTagSupport`,`DramaService` 回落至 440 行,
|
||||
新工具类 204 行。重构后定向测试再次通过:12 项,`BUILD SUCCESS`。
|
||||
- FruitServer 全模块门禁已通过:
|
||||
|
||||
```text
|
||||
$ JAVA_HOME=<jdk21> mvn test
|
||||
[INFO] fruitshorts-admin-api .............................. SUCCESS
|
||||
[INFO] fruitshorts-app-api ................................ SUCCESS
|
||||
[INFO] fruitshorts-transcode-worker ....................... SUCCESS
|
||||
[INFO] BUILD SUCCESS
|
||||
[INFO] Total time: 15.119 s
|
||||
```
|
||||
|
||||
解读:全部 14 个 Reactor 模块通过;app-api 共 95 项测试全过,含新增多标签测试。
|
||||
+1
-1
@@ -71,7 +71,7 @@ class DramaServiceFeedTest {
|
||||
.contains("view_count")
|
||||
.contains("id");
|
||||
assertThat(query.getParamNameValuePairs().values())
|
||||
.contains("published", "ANIME", 30, 1200L, 9L);
|
||||
.contains("published", "%,ANIME,%", 30, 1200L, 9L);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+192
@@ -0,0 +1,192 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
package com.fruitshorts.app.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.fruitshorts.content.dal.entity.Drama;
|
||||
import com.fruitshorts.content.dal.mapper.DramaEpisodeMapper;
|
||||
import com.fruitshorts.content.dal.mapper.DramaMapper;
|
||||
import com.fruitshorts.content.dal.mapper.DramaReviewRecordMapper;
|
||||
import com.fruitshorts.content.service.DramaCategoryService;
|
||||
import com.fruitshorts.content.service.DramaService;
|
||||
import com.fruitshorts.content.vo.DramaPageReqVO;
|
||||
import com.fruitshorts.content.vo.DramaRespVO;
|
||||
import com.fruitshorts.content.vo.DramaSaveReqVO;
|
||||
import org.apache.ibatis.builder.MapperBuilderAssistant;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/** K93 短剧分类与内容类型多选。 */
|
||||
class DramaServiceMultiTagTest {
|
||||
|
||||
private DramaMapper dramaMapper;
|
||||
private DramaCategoryService categoryService;
|
||||
private DramaService dramaService;
|
||||
|
||||
@BeforeAll
|
||||
static void initTableInfo() {
|
||||
TableInfoHelper.initTableInfo(
|
||||
new MapperBuilderAssistant(new MybatisConfiguration(), ""), Drama.class);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
dramaMapper = mock(DramaMapper.class);
|
||||
categoryService = mock(DramaCategoryService.class);
|
||||
dramaService = new DramaService(
|
||||
dramaMapper,
|
||||
mock(DramaEpisodeMapper.class),
|
||||
mock(DramaReviewRecordMapper.class),
|
||||
categoryService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_save_distinct_multi_tags_and_write_first_value_as_primary() {
|
||||
DramaSaveReqVO req = new DramaSaveReqVO();
|
||||
req.setTitle("多标签短剧");
|
||||
req.setDramaTypes(List.of("live", "ANIME", "LIVE", " anime "));
|
||||
req.setCategoryIds(Arrays.asList(3L, 1L, 3L, 0L, null));
|
||||
req.setGenre("类型三 / 类型一");
|
||||
|
||||
dramaService.create(req, 1L);
|
||||
|
||||
ArgumentCaptor<Drama> captor = ArgumentCaptor.forClass(Drama.class);
|
||||
verify(dramaMapper).insert(captor.capture());
|
||||
Drama created = captor.getValue();
|
||||
assertThat(created.getDramaType()).isEqualTo("LIVE");
|
||||
assertThat(created.getDramaTypes()).isEqualTo("LIVE,ANIME");
|
||||
assertThat(created.getCategoryId()).isEqualTo(3L);
|
||||
assertThat(created.getCategoryIds()).isEqualTo("3,1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_keep_single_value_behavior_when_lists_are_absent() {
|
||||
Drama existing = drama(7L);
|
||||
existing.setDramaType("ANIME");
|
||||
existing.setDramaTypes("ANIME");
|
||||
existing.setCategoryId(2L);
|
||||
existing.setCategoryIds("2");
|
||||
given(dramaMapper.selectById(7L)).willReturn(existing);
|
||||
DramaSaveReqVO req = new DramaSaveReqVO();
|
||||
req.setTitle("单值更新");
|
||||
req.setDramaType("live");
|
||||
req.setCategoryId(4L);
|
||||
|
||||
dramaService.update(7L, req);
|
||||
|
||||
ArgumentCaptor<Drama> captor = ArgumentCaptor.forClass(Drama.class);
|
||||
verify(dramaMapper).updateById(captor.capture());
|
||||
assertThat(captor.getValue().getDramaType()).isEqualTo("LIVE");
|
||||
assertThat(captor.getValue().getDramaTypes()).isEqualTo("LIVE");
|
||||
assertThat(captor.getValue().getCategoryId()).isEqualTo(4L);
|
||||
assertThat(captor.getValue().getCategoryIds()).isEqualTo("4");
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void should_apply_token_filters_for_admin_page_multi_tags() {
|
||||
Page<Drama> page = new Page<>(1, 10);
|
||||
page.setRecords(List.of());
|
||||
given(dramaMapper.selectPage(any(Page.class), any())).willReturn(page);
|
||||
DramaPageReqVO req = new DramaPageReqVO();
|
||||
req.setDramaTypes(List.of("ANIME", "LIVE"));
|
||||
req.setCategoryIds(List.of(2L, 3L));
|
||||
ArgumentCaptor<LambdaQueryWrapper<Drama>> captor =
|
||||
ArgumentCaptor.forClass(LambdaQueryWrapper.class);
|
||||
|
||||
dramaService.page(req);
|
||||
|
||||
verify(dramaMapper).selectPage(any(), captor.capture());
|
||||
assertThat(captor.getValue().getSqlSegment())
|
||||
.contains("drama_types")
|
||||
.contains("category_ids")
|
||||
.contains("LIKE");
|
||||
assertThat(captor.getValue().getParamNameValuePairs().values())
|
||||
.contains("%,ANIME,%", "%,LIVE,%", "%,2,%", "%,3,%");
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void should_allow_multi_tag_cursor_marker_for_each_filter_type() {
|
||||
Drama marker = drama(9L);
|
||||
marker.setDramaType("ANIME");
|
||||
marker.setDramaTypes("ANIME,LIVE");
|
||||
marker.setRecommendWeight(30);
|
||||
marker.setViewCount(1200L);
|
||||
given(dramaMapper.selectById(9L)).willReturn(marker);
|
||||
given(dramaMapper.selectList(any())).willReturn(List.of());
|
||||
ArgumentCaptor<QueryWrapper<Drama>> captor = ArgumentCaptor.forClass(QueryWrapper.class);
|
||||
|
||||
dramaService.listPublishedFeed(List.of("ANIME"), 9L, 5);
|
||||
dramaService.listPublishedFeed(List.of("LIVE"), 9L, 5);
|
||||
|
||||
verify(dramaMapper, times(2)).selectList(captor.capture());
|
||||
assertThat(captor.getAllValues()).allSatisfy(query ->
|
||||
assertThat(query.getSqlSegment())
|
||||
.contains("drama_types")
|
||||
.contains("LIKE"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void should_match_multi_category_when_loading_app_category_section() {
|
||||
Page<Drama> page = new Page<>(1, 10);
|
||||
page.setRecords(List.of());
|
||||
given(dramaMapper.selectPage(any(Page.class), any())).willReturn(page);
|
||||
ArgumentCaptor<LambdaQueryWrapper<Drama>> captor =
|
||||
ArgumentCaptor.forClass(LambdaQueryWrapper.class);
|
||||
|
||||
dramaService.listPublished(3L, 10);
|
||||
|
||||
verify(dramaMapper).selectPage(any(), captor.capture());
|
||||
assertThat(captor.getValue().getSqlSegment())
|
||||
.contains("category_ids")
|
||||
.contains("LIKE");
|
||||
assertThat(captor.getValue().getParamNameValuePairs().values()).contains("%,3,%");
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_parse_multi_values_and_keep_category_name_order_in_response() {
|
||||
Drama drama = drama(7L);
|
||||
drama.setDramaType("LIVE");
|
||||
drama.setDramaTypes("LIVE,ANIME");
|
||||
drama.setCategoryId(3L);
|
||||
drama.setCategoryIds("3,1");
|
||||
drama.setGenre("Fallback Genre");
|
||||
given(dramaMapper.selectById(7L)).willReturn(drama);
|
||||
given(categoryService.nameMap()).willReturn(Map.of(1L, "Type A", 3L, "Type B"));
|
||||
|
||||
DramaRespVO vo = dramaService.get(7L);
|
||||
|
||||
assertThat(vo.getDramaTypes()).containsExactly("LIVE", "ANIME");
|
||||
assertThat(vo.getCategoryIds()).containsExactly(3L, 1L);
|
||||
assertThat(vo.getCategoryNames()).containsExactly("Type B", "Type A");
|
||||
assertThat(vo.getDramaType()).isEqualTo("LIVE");
|
||||
assertThat(vo.getCategoryId()).isEqualTo(3L);
|
||||
}
|
||||
|
||||
private Drama drama(Long id) {
|
||||
Drama drama = new Drama();
|
||||
drama.setId(id);
|
||||
drama.setStatus("published");
|
||||
drama.setViewCount(0L);
|
||||
return drama;
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,8 @@ public class Drama {
|
||||
private String title;
|
||||
private String subtitle;
|
||||
private Long categoryId;
|
||||
/** 分类多选,逗号分隔的 category_id,category_id 保留第一个 */
|
||||
private String categoryIds;
|
||||
private String genre;
|
||||
private String synopsis;
|
||||
private String coverUrl;
|
||||
@@ -29,6 +31,8 @@ public class Drama {
|
||||
private String language;
|
||||
/** 内容类型:ANIME=漫剧,LIVE=真人剧 */
|
||||
private String dramaType;
|
||||
/** 内容类型多选,逗号分隔(ANIME/LIVE),drama_type 保留第一个 */
|
||||
private String dramaTypes;
|
||||
private Integer totalEpisodes;
|
||||
private Integer freeEpisodes;
|
||||
private Integer unlockCoin;
|
||||
|
||||
+204
@@ -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.fruitshorts.common.exception.ErrorCode;
|
||||
import com.fruitshorts.common.exception.ServiceException;
|
||||
import com.fruitshorts.content.dal.entity.Drama;
|
||||
import com.fruitshorts.content.vo.DramaSaveReqVO;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/** 短剧分类与内容类型多选值的存储、解析和整词过滤。 */
|
||||
final class DramaMultiTagSupport {
|
||||
|
||||
static final String DEFAULT_DRAMA_TYPE = "LIVE";
|
||||
private static final Set<String> DRAMA_TYPES = Set.of("ANIME", "LIVE");
|
||||
|
||||
private DramaMultiTagSupport() {
|
||||
}
|
||||
|
||||
static void applyDramaTypes(Drama drama, DramaSaveReqVO req) {
|
||||
List<String> values = normalizeDramaTypes(req.getDramaTypes());
|
||||
if (values.isEmpty() && StringUtils.hasText(req.getDramaType())) {
|
||||
values = List.of(normalizeDramaType(req.getDramaType()));
|
||||
}
|
||||
if (!values.isEmpty()) {
|
||||
drama.setDramaType(values.get(0));
|
||||
drama.setDramaTypes(String.join(",", values));
|
||||
return;
|
||||
}
|
||||
if (!StringUtils.hasText(drama.getDramaTypes())
|
||||
&& !StringUtils.hasText(drama.getDramaType())) {
|
||||
drama.setDramaType(DEFAULT_DRAMA_TYPE);
|
||||
drama.setDramaTypes(DEFAULT_DRAMA_TYPE);
|
||||
}
|
||||
}
|
||||
|
||||
static void applyCategoryIds(Drama drama, DramaSaveReqVO req) {
|
||||
List<Long> values = normalizeCategoryIds(req.getCategoryIds());
|
||||
if (values.isEmpty() && req.getCategoryId() != null && req.getCategoryId() > 0L) {
|
||||
values = List.of(req.getCategoryId());
|
||||
}
|
||||
if (!values.isEmpty()) {
|
||||
drama.setCategoryId(values.get(0));
|
||||
drama.setCategoryIds(String.join(",", values.stream().map(String::valueOf).toList()));
|
||||
return;
|
||||
}
|
||||
if (drama.getCategoryId() == null) {
|
||||
drama.setCategoryId(0L);
|
||||
}
|
||||
if (drama.getCategoryIds() == null) {
|
||||
drama.setCategoryIds("");
|
||||
}
|
||||
}
|
||||
|
||||
static void applyDramaTypeTokens(LambdaQueryWrapper<Drama> wrapper,
|
||||
List<String> dramaTypes) {
|
||||
wrapper.and(nested -> {
|
||||
for (int index = 0; index < dramaTypes.size(); index++) {
|
||||
if (index > 0) {
|
||||
nested.or();
|
||||
}
|
||||
nested.apply("(',' || drama_types || ',') LIKE {0}",
|
||||
tokenPattern(dramaTypes.get(index)));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static void applyCategoryIdTokens(LambdaQueryWrapper<Drama> wrapper,
|
||||
List<Long> categoryIds) {
|
||||
wrapper.and(nested -> {
|
||||
for (int index = 0; index < categoryIds.size(); index++) {
|
||||
if (index > 0) {
|
||||
nested.or();
|
||||
}
|
||||
nested.apply("(',' || category_ids || ',') LIKE {0}",
|
||||
tokenPattern(categoryIds.get(index)));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static void applyDramaTypeTokens(QueryWrapper<Drama> wrapper,
|
||||
List<String> dramaTypes) {
|
||||
wrapper.and(nested -> {
|
||||
for (int index = 0; index < dramaTypes.size(); index++) {
|
||||
if (index > 0) {
|
||||
nested.or();
|
||||
}
|
||||
nested.apply("(',' || drama_types || ',') LIKE {0}",
|
||||
tokenPattern(dramaTypes.get(index)));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static List<String> parseDramaTypes(Drama drama) {
|
||||
List<String> values = parseStringTokens(drama.getDramaTypes()).stream()
|
||||
.map(DramaMultiTagSupport::normalizeDramaType)
|
||||
.distinct()
|
||||
.toList();
|
||||
if (!values.isEmpty()) {
|
||||
return values;
|
||||
}
|
||||
if (StringUtils.hasText(drama.getDramaType())) {
|
||||
return List.of(normalizeDramaType(drama.getDramaType()));
|
||||
}
|
||||
return List.of(DEFAULT_DRAMA_TYPE);
|
||||
}
|
||||
|
||||
static List<Long> parseCategoryIds(Drama drama) {
|
||||
List<Long> values = parseLongTokens(drama.getCategoryIds());
|
||||
if (values.isEmpty() && drama.getCategoryId() != null && drama.getCategoryId() > 0L) {
|
||||
return List.of(drama.getCategoryId());
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
static List<String> categoryNames(Drama drama, List<Long> categoryIds,
|
||||
Map<Long, String> names) {
|
||||
List<String> values = categoryIds.stream()
|
||||
.map(names::get)
|
||||
.filter(StringUtils::hasText)
|
||||
.toList();
|
||||
if (values.isEmpty() && StringUtils.hasText(drama.getGenre())) {
|
||||
return List.of(drama.getGenre());
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
static List<String> normalizeDramaTypes(List<String> dramaTypes) {
|
||||
if (dramaTypes == null || dramaTypes.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
return dramaTypes.stream()
|
||||
.filter(StringUtils::hasText)
|
||||
.map(DramaMultiTagSupport::normalizeDramaType)
|
||||
.distinct()
|
||||
.toList();
|
||||
}
|
||||
|
||||
static List<Long> normalizeCategoryIds(List<Long> categoryIds) {
|
||||
if (categoryIds == null || categoryIds.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
return categoryIds.stream()
|
||||
.filter(id -> id != null && id > 0L)
|
||||
.distinct()
|
||||
.toList();
|
||||
}
|
||||
|
||||
private static String normalizeDramaType(String dramaType) {
|
||||
String normalized = dramaType == null ? "" : dramaType.trim().toUpperCase(Locale.ROOT);
|
||||
if (!DRAMA_TYPES.contains(normalized)) {
|
||||
throw new ServiceException(ErrorCode.BAD_REQUEST, "内容类型仅支持 ANIME 或 LIVE");
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private static List<String> parseStringTokens(String value) {
|
||||
if (!StringUtils.hasText(value)) {
|
||||
return List.of();
|
||||
}
|
||||
List<String> result = new ArrayList<>();
|
||||
for (String token : value.split(",")) {
|
||||
String trimmed = token.trim();
|
||||
if (StringUtils.hasText(trimmed) && !result.contains(trimmed)) {
|
||||
result.add(trimmed);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static List<Long> parseLongTokens(String value) {
|
||||
if (!StringUtils.hasText(value)) {
|
||||
return List.of();
|
||||
}
|
||||
List<Long> result = new ArrayList<>();
|
||||
for (String token : value.split(",")) {
|
||||
Long parsed = parseLongValue(token);
|
||||
if (parsed != null && parsed > 0L && !result.contains(parsed)) {
|
||||
result.add(parsed);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Long parseLongValue(String value) {
|
||||
try {
|
||||
return Long.parseLong(value.trim());
|
||||
} catch (NumberFormatException ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static String tokenPattern(Object value) {
|
||||
return "%," + value + ",%";
|
||||
}
|
||||
}
|
||||
+42
-46
@@ -3,6 +3,7 @@
|
||||
package com.fruitshorts.content.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
@@ -28,7 +29,6 @@ import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
@@ -37,8 +37,6 @@ import java.util.Set;
|
||||
public class DramaService {
|
||||
|
||||
private static final DateTimeFormatter CODE_DATE = DateTimeFormatter.ofPattern("yyyyMMdd");
|
||||
private static final String DEFAULT_DRAMA_TYPE = "LIVE";
|
||||
private static final Set<String> DRAMA_TYPES = Set.of("ANIME", "LIVE");
|
||||
|
||||
private final DramaMapper dramaMapper;
|
||||
private final DramaEpisodeMapper dramaEpisodeMapper;
|
||||
@@ -46,16 +44,23 @@ public class DramaService {
|
||||
private final DramaCategoryService dramaCategoryService;
|
||||
|
||||
public PageResult<DramaRespVO> page(DramaPageReqVO req) {
|
||||
List<String> dramaTypes = normalizeDramaTypes(req.getDramaTypes());
|
||||
List<String> dramaTypes = DramaMultiTagSupport.normalizeDramaTypes(req.getDramaTypes());
|
||||
List<Long> categoryIds = DramaMultiTagSupport.normalizeCategoryIds(req.getCategoryIds());
|
||||
Page<Drama> page = new Page<>(req.getSafePageIndex(), req.getSafePageSize());
|
||||
IPage<Drama> result = dramaMapper.selectPage(page, Wrappers.<Drama>lambdaQuery()
|
||||
LambdaQueryWrapper<Drama> wrapper = Wrappers.<Drama>lambdaQuery()
|
||||
.like(StringUtils.hasText(req.getKeyword()), Drama::getTitle, req.getKeyword())
|
||||
.eq(StringUtils.hasText(req.getStatus()), Drama::getStatus, req.getStatus())
|
||||
.eq(req.getCategoryId() != null, Drama::getCategoryId, req.getCategoryId())
|
||||
.eq(req.getIsHot() != null, Drama::getIsHot, Boolean.TRUE.equals(req.getIsHot()) ? 1 : 0)
|
||||
.in(!dramaTypes.isEmpty(), Drama::getDramaType, dramaTypes)
|
||||
.orderByDesc(Drama::getSort)
|
||||
.orderByDesc(Drama::getId));
|
||||
.eq(req.getIsHot() != null, Drama::getIsHot, Boolean.TRUE.equals(req.getIsHot()) ? 1 : 0);
|
||||
if (!dramaTypes.isEmpty()) {
|
||||
DramaMultiTagSupport.applyDramaTypeTokens(wrapper, dramaTypes);
|
||||
}
|
||||
if (!categoryIds.isEmpty()) {
|
||||
DramaMultiTagSupport.applyCategoryIdTokens(wrapper, categoryIds);
|
||||
} else {
|
||||
wrapper.eq(req.getCategoryId() != null, Drama::getCategoryId, req.getCategoryId());
|
||||
}
|
||||
wrapper.orderByDesc(Drama::getSort).orderByDesc(Drama::getId);
|
||||
IPage<Drama> result = dramaMapper.selectPage(page, wrapper);
|
||||
return new PageResult<>(result.getTotal(), toRespList(result.getRecords()));
|
||||
}
|
||||
|
||||
@@ -210,11 +215,13 @@ public class DramaService {
|
||||
/** 已上线短剧,按运营排序 + 播放量 */
|
||||
public List<Drama> listPublished(Long categoryId, long limit) {
|
||||
Page<Drama> page = new Page<>(1, Math.max(1, limit));
|
||||
return dramaMapper.selectPage(page, Wrappers.<Drama>lambdaQuery()
|
||||
.eq(Drama::getStatus, ContentStatus.PUBLISHED.getCode())
|
||||
.eq(categoryId != null && categoryId > 0, Drama::getCategoryId, categoryId)
|
||||
.orderByDesc(Drama::getSort)
|
||||
.orderByDesc(Drama::getViewCount)).getRecords();
|
||||
LambdaQueryWrapper<Drama> wrapper = Wrappers.<Drama>lambdaQuery()
|
||||
.eq(Drama::getStatus, ContentStatus.PUBLISHED.getCode());
|
||||
if (categoryId != null && categoryId > 0) {
|
||||
DramaMultiTagSupport.applyCategoryIdTokens(wrapper, List.of(categoryId));
|
||||
}
|
||||
wrapper.orderByDesc(Drama::getSort).orderByDesc(Drama::getViewCount);
|
||||
return dramaMapper.selectPage(page, wrapper).getRecords();
|
||||
}
|
||||
|
||||
/** 已上线短剧,按上线时间倒序 */
|
||||
@@ -231,17 +238,19 @@ public class DramaService {
|
||||
* cursor 使用上一页最后一条 ID,服务端反查排序键后做 keyset 查询。
|
||||
*/
|
||||
public List<Drama> listPublishedFeed(List<String> dramaTypes, Long cursor, int limit) {
|
||||
List<String> normalizedTypes = normalizeDramaTypes(dramaTypes);
|
||||
List<String> normalizedTypes = DramaMultiTagSupport.normalizeDramaTypes(dramaTypes);
|
||||
QueryWrapper<Drama> query = new QueryWrapper<>();
|
||||
query.eq("status", ContentStatus.PUBLISHED.getCode());
|
||||
if (!normalizedTypes.isEmpty()) {
|
||||
query.in("drama_type", normalizedTypes);
|
||||
DramaMultiTagSupport.applyDramaTypeTokens(query, normalizedTypes);
|
||||
}
|
||||
if (cursor != null) {
|
||||
Drama marker = dramaMapper.selectById(cursor);
|
||||
if (marker == null
|
||||
|| !ContentStatus.PUBLISHED.getCode().equals(marker.getStatus())
|
||||
|| (!normalizedTypes.isEmpty() && !normalizedTypes.contains(marker.getDramaType()))) {
|
||||
|| (!normalizedTypes.isEmpty()
|
||||
&& DramaMultiTagSupport.parseDramaTypes(marker).stream()
|
||||
.noneMatch(normalizedTypes::contains))) {
|
||||
return List.of();
|
||||
}
|
||||
applyFeedCursor(query, marker);
|
||||
@@ -333,18 +342,14 @@ public class DramaService {
|
||||
private void apply(Drama drama, DramaSaveReqVO req) {
|
||||
drama.setTitle(req.getTitle());
|
||||
drama.setSubtitle(req.getSubtitle() == null ? "" : req.getSubtitle());
|
||||
drama.setCategoryId(req.getCategoryId() == null ? 0L : req.getCategoryId());
|
||||
DramaMultiTagSupport.applyCategoryIds(drama, req);
|
||||
drama.setGenre(req.getGenre() == null ? "" : req.getGenre());
|
||||
drama.setSynopsis(req.getDescription() == null ? "" : req.getDescription());
|
||||
drama.setCoverUrl(req.getCover() == null ? "" : req.getCover());
|
||||
drama.setTrailerUrl(req.getTrailerUrl() == null ? "" : req.getTrailerUrl());
|
||||
drama.setHorizontalCoverUrl(drama.getHorizontalCoverUrl() == null ? "" : drama.getHorizontalCoverUrl());
|
||||
drama.setLanguage(req.getLanguage() == null ? "zh-CN" : req.getLanguage());
|
||||
if (StringUtils.hasText(req.getDramaType())) {
|
||||
drama.setDramaType(normalizeDramaType(req.getDramaType()));
|
||||
} else if (!StringUtils.hasText(drama.getDramaType())) {
|
||||
drama.setDramaType(DEFAULT_DRAMA_TYPE);
|
||||
}
|
||||
DramaMultiTagSupport.applyDramaTypes(drama, req);
|
||||
drama.setFreeEpisodes(req.getFreeEpisodes() == null ? 3 : req.getFreeEpisodes());
|
||||
drama.setUnlockCoin(req.getUnlockCoin() == null ? 0 : req.getUnlockCoin());
|
||||
drama.setIsHot(Boolean.TRUE.equals(req.getIsHot()) ? 1 : 0);
|
||||
@@ -363,19 +368,29 @@ public class DramaService {
|
||||
}
|
||||
Map<Long, String> categories = dramaCategoryService.nameMap();
|
||||
return dramas.stream().map(drama -> {
|
||||
List<Long> categoryIds = DramaMultiTagSupport.parseCategoryIds(drama);
|
||||
Long primaryCategoryId = categoryIds.isEmpty()
|
||||
? (drama.getCategoryId() == null ? 0L : drama.getCategoryId())
|
||||
: categoryIds.get(0);
|
||||
List<String> categoryNames =
|
||||
DramaMultiTagSupport.categoryNames(drama, categoryIds, categories);
|
||||
List<String> dramaTypes = DramaMultiTagSupport.parseDramaTypes(drama);
|
||||
DramaRespVO vo = new DramaRespVO();
|
||||
vo.setId(drama.getId());
|
||||
vo.setCode(drama.getCode());
|
||||
vo.setTitle(drama.getTitle());
|
||||
vo.setSubtitle(drama.getSubtitle());
|
||||
vo.setCategoryId(drama.getCategoryId());
|
||||
vo.setCategory(categories.getOrDefault(drama.getCategoryId(), ""));
|
||||
vo.setCategoryId(primaryCategoryId);
|
||||
vo.setCategoryIds(categoryIds);
|
||||
vo.setCategoryNames(categoryNames);
|
||||
vo.setCategory(categories.getOrDefault(primaryCategoryId, ""));
|
||||
vo.setGenre(drama.getGenre());
|
||||
vo.setDescription(drama.getSynopsis());
|
||||
vo.setCover(drama.getCoverUrl());
|
||||
vo.setTrailerUrl(drama.getTrailerUrl());
|
||||
vo.setLanguage(drama.getLanguage());
|
||||
vo.setDramaType(drama.getDramaType());
|
||||
vo.setDramaType(dramaTypes.get(0));
|
||||
vo.setDramaTypes(dramaTypes);
|
||||
vo.setEpisodes(drama.getTotalEpisodes());
|
||||
vo.setFreeEpisodes(drama.getFreeEpisodes());
|
||||
vo.setUnlockCoin(drama.getUnlockCoin());
|
||||
@@ -412,25 +427,6 @@ public class DramaService {
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> normalizeDramaTypes(List<String> dramaTypes) {
|
||||
if (dramaTypes == null || dramaTypes.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
return dramaTypes.stream()
|
||||
.filter(StringUtils::hasText)
|
||||
.map(this::normalizeDramaType)
|
||||
.distinct()
|
||||
.toList();
|
||||
}
|
||||
|
||||
private String normalizeDramaType(String dramaType) {
|
||||
String normalized = dramaType == null ? "" : dramaType.trim().toUpperCase(Locale.ROOT);
|
||||
if (!DRAMA_TYPES.contains(normalized)) {
|
||||
throw new ServiceException(ErrorCode.BAD_REQUEST, "内容类型仅支持 ANIME 或 LIVE");
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private Long nonNegativeOrZero(Long value, String fieldName) {
|
||||
return value == null ? 0L : nonNegative(value, fieldName);
|
||||
}
|
||||
|
||||
+2
@@ -16,6 +16,8 @@ public class DramaPageReqVO extends PageParam {
|
||||
private String keyword;
|
||||
private String status;
|
||||
private Long categoryId;
|
||||
/** 分类多选,空时回退 categoryId */
|
||||
private List<Long> categoryIds;
|
||||
/** 是否只看热门 */
|
||||
private Boolean isHot;
|
||||
/** 内容类型多选:ANIME/LIVE,空=全部 */
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 短剧列表/详情响应,字段名与后台页面表格列一致。
|
||||
@@ -18,6 +19,8 @@ public class DramaRespVO {
|
||||
private String title;
|
||||
private String subtitle;
|
||||
private Long categoryId;
|
||||
private List<Long> categoryIds;
|
||||
private List<String> categoryNames;
|
||||
private String category;
|
||||
private String genre;
|
||||
private String description;
|
||||
@@ -26,6 +29,7 @@ public class DramaRespVO {
|
||||
private String trailerUrl;
|
||||
private String language;
|
||||
private String dramaType;
|
||||
private List<String> dramaTypes;
|
||||
private Integer episodes;
|
||||
private Integer freeEpisodes;
|
||||
private Integer unlockCoin;
|
||||
|
||||
+6
@@ -7,6 +7,8 @@ import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.PositiveOrZero;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class DramaSaveReqVO {
|
||||
|
||||
@@ -15,6 +17,8 @@ public class DramaSaveReqVO {
|
||||
|
||||
private String subtitle;
|
||||
private Long categoryId;
|
||||
/** 后台多选优先,创作者中心可继续只传 categoryId */
|
||||
private List<Long> categoryIds;
|
||||
private String genre;
|
||||
private String description;
|
||||
private String cover;
|
||||
@@ -23,6 +27,8 @@ public class DramaSaveReqVO {
|
||||
private String language;
|
||||
@Pattern(regexp = "ANIME|LIVE", message = "内容类型仅支持 ANIME 或 LIVE")
|
||||
private String dramaType;
|
||||
/** 后台多选优先,创作者中心可继续只传 dramaType */
|
||||
private List<String> dramaTypes;
|
||||
private Integer freeEpisodes;
|
||||
private Integer unlockCoin;
|
||||
@PositiveOrZero(message = "收藏数不能小于 0")
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
-- Author: fruit.com.cn
|
||||
|
||||
-- K93 短剧分类与内容类型多选(幂等,仅提交文件,由发布流程执行)
|
||||
|
||||
ALTER TABLE drama ADD COLUMN IF NOT EXISTS drama_types VARCHAR(32) NOT NULL DEFAULT '';
|
||||
COMMENT ON COLUMN drama.drama_types IS '内容类型多选,逗号分隔(ANIME/LIVE),drama_type 保留第一个';
|
||||
|
||||
ALTER TABLE drama ADD COLUMN IF NOT EXISTS category_ids VARCHAR(255) NOT NULL DEFAULT '';
|
||||
COMMENT ON COLUMN drama.category_ids IS
|
||||
'短剧分类多选,逗号分隔的 category_id,category_id 保留第一个';
|
||||
|
||||
UPDATE drama SET drama_types = drama_type
|
||||
WHERE (drama_types IS NULL OR drama_types = '') AND drama_type IS NOT NULL AND drama_type <> '';
|
||||
|
||||
UPDATE drama SET category_ids = category_id::text
|
||||
WHERE (category_ids IS NULL OR category_ids = '') AND category_id IS NOT NULL AND category_id > 0;
|
||||
@@ -164,12 +164,14 @@ CREATE TABLE IF NOT EXISTS drama (
|
||||
title VARCHAR(128) NOT NULL,
|
||||
subtitle VARCHAR(128) NOT NULL DEFAULT '',
|
||||
category_id BIGINT NOT NULL DEFAULT 0,
|
||||
category_ids VARCHAR(255) NOT NULL DEFAULT '',
|
||||
genre VARCHAR(64) NOT NULL DEFAULT '',
|
||||
synopsis VARCHAR(1000) NOT NULL DEFAULT '',
|
||||
cover_url VARCHAR(512) NOT NULL DEFAULT '',
|
||||
horizontal_cover_url VARCHAR(512) NOT NULL DEFAULT '',
|
||||
language VARCHAR(16) NOT NULL DEFAULT 'zh-CN',
|
||||
drama_type VARCHAR(16) NOT NULL DEFAULT 'LIVE',
|
||||
drama_types VARCHAR(32) NOT NULL DEFAULT '',
|
||||
total_episodes INT NOT NULL DEFAULT 0,
|
||||
free_episodes INT NOT NULL DEFAULT 3,
|
||||
unlock_coin INT NOT NULL DEFAULT 0,
|
||||
|
||||
Reference in new issue
Block a user