diff --git a/docs/api-phase1.md b/docs/api-phase1.md index 555ca54..86ef6f2 100644 --- a/docs/api-phase1.md +++ b/docs/api-phase1.md @@ -205,6 +205,10 @@ app.register.max-per-device-per-day > 卡片文案只放语言表:`titleLocales`、`synopsisLocales`、`types`、`tagLocales`。 > `tagColor` 是标签背景色。中文取各表的 `zh`。 > +> `tagCode` 是标签语义码(如 `NEW_ARRIVAL`),来自 `content_tag.code`。 +> 客户端按码判断板块(推荐页「上新」横滑),`tagLocales` 只用于展示; +> 码为空时客户端才退回按文案匹配。后台标签管理可配置该码。 +> > 卡片新增 `dramaType`:`ANIME` 表示漫剧,`LIVE` 表示真人剧。 > 客户端字段仍返回主内容类型;后台多选值保存在 `drama.drama_types`, > App 类型筛选按逗号列整词匹配,因此 `ANIME,LIVE` 多标签剧可被任一类型命中。 diff --git a/fruitshorts-app-api/src/main/java/com/fruitshorts/app/service/AppCatalogService.java b/fruitshorts-app-api/src/main/java/com/fruitshorts/app/service/AppCatalogService.java index 9249237..a61d415 100644 --- a/fruitshorts-app-api/src/main/java/com/fruitshorts/app/service/AppCatalogService.java +++ b/fruitshorts-app-api/src/main/java/com/fruitshorts/app/service/AppCatalogService.java @@ -411,6 +411,7 @@ public class AppCatalogService { ContentTag tag = tagOf(drama, tags); Map locales = tagLocales(tag); card.setTagLocales(locales); + card.setTagCode(tagCode(tag)); card.setTagColor(tag == null ? "" : tag.getBgColor()); } @@ -418,9 +419,18 @@ public class AppCatalogService { ContentTag tag = tagOf(drama, loadTags(List.of(drama))); Map locales = tagLocales(tag); detail.setTagLocales(locales); + detail.setTagCode(tagCode(tag)); detail.setTagColor(tag == null ? "" : tag.getBgColor()); } + /** 语义码直接下发,客户端据此判断板块;未配置时给空串,由客户端按文案兜底。 */ + private String tagCode(ContentTag tag) { + if (tag == null || tag.getCode() == null) { + return ""; + } + return tag.getCode().trim(); + } + private ContentTag tagOf(Drama drama, Map tags) { if (drama.getTagId() == null || drama.getTagId() <= 0L) { return null; diff --git a/fruitshorts-app-api/src/main/java/com/fruitshorts/app/vo/AppDramaCardVO.java b/fruitshorts-app-api/src/main/java/com/fruitshorts/app/vo/AppDramaCardVO.java index 819e888..aeceea3 100644 --- a/fruitshorts-app-api/src/main/java/com/fruitshorts/app/vo/AppDramaCardVO.java +++ b/fruitshorts-app-api/src/main/java/com/fruitshorts/app/vo/AppDramaCardVO.java @@ -20,6 +20,8 @@ public class AppDramaCardVO { private List> types; /** 标签语言表,key 为 zh / en / id。 */ private Map tagLocales; + /** 标签语义码,如 NEW_ARRIVAL;客户端按码判断板块,文案仅用于展示。 */ + private String tagCode; /** 标签角标背景色,#RRGGBB。 */ private String tagColor; private Integer totalEpisodes; diff --git a/fruitshorts-app-api/src/main/java/com/fruitshorts/app/vo/AppDramaDetailVO.java b/fruitshorts-app-api/src/main/java/com/fruitshorts/app/vo/AppDramaDetailVO.java index f4503e5..81c0da2 100644 --- a/fruitshorts-app-api/src/main/java/com/fruitshorts/app/vo/AppDramaDetailVO.java +++ b/fruitshorts-app-api/src/main/java/com/fruitshorts/app/vo/AppDramaDetailVO.java @@ -16,6 +16,8 @@ public class AppDramaDetailVO { private String cover; private List> types; private Map tagLocales; + /** 标签语义码,如 NEW_ARRIVAL;空表示未配置。 */ + private String tagCode; private String tagColor; private Integer totalEpisodes; private Integer freeEpisodes; diff --git a/fruitshorts-module-content/src/main/java/com/fruitshorts/content/dal/entity/ContentTag.java b/fruitshorts-module-content/src/main/java/com/fruitshorts/content/dal/entity/ContentTag.java index c70c3eb..697ea0b 100644 --- a/fruitshorts-module-content/src/main/java/com/fruitshorts/content/dal/entity/ContentTag.java +++ b/fruitshorts-module-content/src/main/java/com/fruitshorts/content/dal/entity/ContentTag.java @@ -24,6 +24,8 @@ public class ContentTag { private String nameLocales; /** 角标背景色,#RRGGBB。 */ private String bgColor; + /** 语义码,如 NEW_ARRIVAL;客户端按码判断板块,不解析文案。空表示未配置。 */ + private String code; private Integer sort; private Integer status; private LocalDateTime createdAt; diff --git a/fruitshorts-module-content/src/main/java/com/fruitshorts/content/service/ContentTagService.java b/fruitshorts-module-content/src/main/java/com/fruitshorts/content/service/ContentTagService.java index a2294e8..4b406b3 100644 --- a/fruitshorts-module-content/src/main/java/com/fruitshorts/content/service/ContentTagService.java +++ b/fruitshorts-module-content/src/main/java/com/fruitshorts/content/service/ContentTagService.java @@ -49,9 +49,12 @@ public class ContentTagService { public Long create(ContentTagSaveReqVO req) { Map locales = namesOf(req); ensureNameAvailable(locales.get(ContentLocaleTexts.ZH), null); + String code = normalizeCode(req.getCode()); + ensureCodeAvailable(code, null); ContentTag entity = new ContentTag(); writeNames(entity, locales); entity.setBgColor(normalizeColor(req.getBgColor())); + entity.setCode(code); Long count = tagMapper.selectCount(null); entity.setSort(req.getSort() == null ? (count == null ? 0 : count.intValue()) + 1 @@ -69,6 +72,11 @@ public class ContentTagService { if (req.getBgColor() != null) { entity.setBgColor(normalizeColor(req.getBgColor())); } + if (req.getCode() != null) { + String code = normalizeCode(req.getCode()); + ensureCodeAvailable(code, id); + entity.setCode(code); + } if (req.getSort() != null) { entity.setSort(req.getSort()); } @@ -115,6 +123,23 @@ public class ContentTagService { return color.trim().toUpperCase(); } + /** 语义码统一大写;空字符串表示不配置。 */ + private String normalizeCode(String code) { + return code == null ? "" : code.trim().toUpperCase(); + } + + private void ensureCodeAvailable(String code, Long excludeId) { + if (code.isEmpty()) { + return; + } + Long count = tagMapper.selectCount(Wrappers.lambdaQuery() + .eq(ContentTag::getCode, code) + .ne(excludeId != null, ContentTag::getId, excludeId)); + if (count != null && count > 0) { + throw new ServiceException(ErrorCode.DATA_EXISTS, "语义码已存在"); + } + } + private ContentTag getExisting(Long id) { ContentTag entity = id == null ? null : tagMapper.selectById(id); if (entity == null) { @@ -142,6 +167,7 @@ public class ContentTagService { vo.setNameId(ContentLocaleTexts.value(locales, ContentLocaleTexts.ID)); vo.setLocales(locales); vo.setBgColor(tag.getBgColor() == null || tag.getBgColor().isBlank() ? "#E5484D" : tag.getBgColor()); + vo.setCode(tag.getCode() == null ? "" : tag.getCode()); vo.setSort(tag.getSort()); vo.setStatus(tag.getStatus()); return vo; diff --git a/fruitshorts-module-content/src/main/java/com/fruitshorts/content/vo/ContentTagRespVO.java b/fruitshorts-module-content/src/main/java/com/fruitshorts/content/vo/ContentTagRespVO.java index ad8d039..a4164cd 100644 --- a/fruitshorts-module-content/src/main/java/com/fruitshorts/content/vo/ContentTagRespVO.java +++ b/fruitshorts-module-content/src/main/java/com/fruitshorts/content/vo/ContentTagRespVO.java @@ -14,6 +14,8 @@ public class ContentTagRespVO { private String nameId; private java.util.Map locales; private String bgColor; + /** 语义码,如 NEW_ARRIVAL;空表示未配置。 */ + private String code; private Integer sort; private Integer status; } diff --git a/fruitshorts-module-content/src/main/java/com/fruitshorts/content/vo/ContentTagSaveReqVO.java b/fruitshorts-module-content/src/main/java/com/fruitshorts/content/vo/ContentTagSaveReqVO.java index 08c4b1b..8895af1 100644 --- a/fruitshorts-module-content/src/main/java/com/fruitshorts/content/vo/ContentTagSaveReqVO.java +++ b/fruitshorts-module-content/src/main/java/com/fruitshorts/content/vo/ContentTagSaveReqVO.java @@ -21,6 +21,10 @@ public class ContentTagSaveReqVO { @Pattern(regexp = "^$|^#[0-9A-Fa-f]{6}$", message = "背景色格式应为 #RRGGBB") private String bgColor; + /** 语义码,如 NEW_ARRIVAL;客户端按码判断板块,留空表示按文案兜底。 */ + @Pattern(regexp = "^$|^[A-Z][A-Z0-9_]{0,31}$", message = "语义码需为大写字母、数字或下划线") + private String code; + private Integer sort; private Integer status; } diff --git a/sql/patch-2026-10-29-content-tag-code.sql b/sql/patch-2026-10-29-content-tag-code.sql new file mode 100644 index 0000000..9df0a1d --- /dev/null +++ b/sql/patch-2026-10-29-content-tag-code.sql @@ -0,0 +1,21 @@ +-- Author: fruit.com.cn +-- 标签语义码:客户端按稳定码判断板块归属(如「上新」横滑),文案可随意改。 + +ALTER TABLE content_tag ADD COLUMN IF NOT EXISTS code VARCHAR(32) NOT NULL DEFAULT ''; + +-- 同一个语义码只能给一个标签,空码不参与唯一性约束。 +CREATE UNIQUE INDEX IF NOT EXISTS uk_content_tag_code + ON content_tag (code) WHERE code <> ''; + +-- 已有标签回填语义码(幂等:只补空的)。 +UPDATE content_tag SET code = 'NEW_ARRIVAL' +WHERE code = '' AND (name = '上新' OR name_en = 'New Arrival'); + +UPDATE content_tag SET code = 'HOT' +WHERE code = '' AND (name = '热剧' OR name_en = 'Hot'); + +UPDATE content_tag SET code = 'NEW_RELEASE' +WHERE code = '' AND (name = '新剧' OR name_en = 'New Movie'); + +UPDATE content_tag SET code = 'TRENDING' +WHERE code = '' AND (name = '热播' OR name_en = 'Rebo');