Merge branch feat/app-version-and-c-end

This commit is contained in:
Zhou Weidong authored and trae-solo-agent committed 2026-10-23 16:45:55 +08:00
commit 5c20cf1ab9
11 files changed
+247 -4

No files matched your search

@@ -12,6 +12,8 @@ import com.fruitshorts.system.vo.AppVersionSaveReqVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Size;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
@@ -56,6 +58,17 @@ public class AppVersionAdminController {
return Result.ok(appVersionService.create(req));
}
@Operation(summary = "批量新建版本(一次事务)")
@PostMapping("/batch")
@PreAuthorize("@ss.hasPermission('app-version:create')")
@OperationLog(module = "版本管理", action = "批量新建版本")
public Result<java.util.List<Long>> batchCreate(
@NotEmpty(message = "请至少传一条")
@Size(max = 16, message = "单次不超过 16 条")
@Valid @RequestBody java.util.List<@Valid AppVersionSaveReqVO> reqs) {
return Result.ok(appVersionService.batchCreate(reqs));
}
@Operation(summary = "编辑版本")
@PutMapping("/{id}")
@PreAuthorize("@ss.hasPermission('app-version:update')")
@@ -6,12 +6,15 @@ import com.fruitshorts.app.service.AppCatalogService;
import com.fruitshorts.app.vo.AppCategoryVO;
import com.fruitshorts.app.vo.AppFeedVO;
import com.fruitshorts.app.vo.AppDramaCardVO;
import com.fruitshorts.app.vo.AppPlaylistDetailVO;
import com.fruitshorts.common.pojo.CursorResult;
import com.fruitshorts.common.pojo.PageResult;
import com.fruitshorts.common.pojo.Result;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@@ -56,4 +59,19 @@ public class AppCatalogController {
public Result<List<AppCategoryVO>> categories() {
return Result.ok(appCatalogService.categories());
}
@Operation(summary = "分类频道:按分类 ID 分页查短剧卡片")
@GetMapping("/categories/{id}/dramas")
public Result<PageResult<AppDramaCardVO>> categoryDramas(
@PathVariable Long id,
@RequestParam(defaultValue = "1") int pageIndex,
@RequestParam(defaultValue = "20") int pageSize) {
return Result.ok(appCatalogService.categoryDramas(id, pageIndex, pageSize));
}
@Operation(summary = "剧单详情:剧单基本信息 + 该剧单下已上线短剧卡片")
@GetMapping("/playlists/{id}")
public Result<AppPlaylistDetailVO> playlistDetail(@PathVariable Long id) {
return Result.ok(appCatalogService.playlistDetail(id));
}
}
@@ -8,14 +8,18 @@ import com.fruitshorts.app.vo.AppDramaCardVO;
import com.fruitshorts.app.vo.AppDramaDetailVO;
import com.fruitshorts.app.vo.AppEpisodeVO;
import com.fruitshorts.app.vo.AppFeedVO;
import com.fruitshorts.app.vo.AppPlaylistDetailVO;
import com.fruitshorts.app.vo.AppSectionVO;
import com.fruitshorts.common.pojo.CursorResult;
import com.fruitshorts.common.pojo.PageResult;
import com.fruitshorts.content.dal.entity.ContentBanner;
import com.fruitshorts.content.dal.entity.Drama;
import com.fruitshorts.content.dal.entity.DramaEpisode;
import com.fruitshorts.content.enums.BannerPosition;
import com.fruitshorts.content.service.BannerService;
import com.fruitshorts.content.service.DramaCategoryService;
import com.fruitshorts.content.service.DramaClassificationService;
import com.fruitshorts.content.service.DramaPlaylistCategoryService;
import com.fruitshorts.content.service.DramaService;
import com.fruitshorts.content.service.EpisodeService;
import com.fruitshorts.interaction.service.CommentService;
@@ -49,6 +53,8 @@ public class AppCatalogService {
private final EpisodeService episodeService;
private final BannerService bannerService;
private final DramaCategoryService dramaCategoryService;
private final DramaClassificationService dramaClassificationService;
private final DramaPlaylistCategoryService dramaPlaylistCategoryService;
private final PlaybackSigner playbackSigner;
private final CommentService commentService;
private final ThumbnailUrlResolver thumbnailUrlResolver;
@@ -69,13 +75,42 @@ public class AppCatalogService {
categorySections(DISCOVER_SECTION_SIZE, categoryNameMap()));
}
/** 分类频道:运营自定义的精选/新剧/榜单等(drama_classification)。 */
public List<AppCategoryVO> categories() {
return dramaCategoryService.listBasic().stream()
.filter(category -> category.getStatus() == null || category.getStatus() == 1)
.map(category -> new AppCategoryVO(category.getId(), category.getName()))
return dramaClassificationService.listEnabled().stream()
.map(c -> new AppCategoryVO(c.getId(), c.getName()))
.toList();
}
/** 分类频道:按分类分页查已上线短剧卡片(通过分类关联表过滤)。 */
public PageResult<AppDramaCardVO> categoryDramas(Long classificationId,
int pageIndex, int pageSize) {
dramaClassificationService.getEnabledById(classificationId); // 校验存在/启用
List<Long> allIds = dramaClassificationService.listDramaIdsByClassification(classificationId);
if (allIds.isEmpty()) {
return PageResult.empty();
}
// 只取已上线的,再按关联表原始顺序排
Map<Long, Integer> orderMap = new HashMap<>();
for (int i = 0; i < allIds.size(); i++) orderMap.put(allIds.get(i), i);
List<Drama> published = dramaService.listPublishedByIds(allIds);
List<Drama> ordered = published.stream()
.sorted((a, b) -> Integer.compare(
orderMap.getOrDefault(a.getId(), Integer.MAX_VALUE),
orderMap.getOrDefault(b.getId(), Integer.MAX_VALUE)))
.toList();
int safeIndex = Math.max(1, pageIndex);
int safeSize = Math.max(1, Math.min(pageSize, 50));
long total = ordered.size();
int fromIndex = (safeIndex - 1) * safeSize;
if (fromIndex >= ordered.size()) {
return new PageResult<>(total, List.of());
}
int toIndex = Math.min(fromIndex + safeSize, ordered.size());
List<Drama> pageRows = ordered.subList(fromIndex, toIndex);
return new PageResult<>(total, cards(pageRows, categoryNameMap()));
}
public CursorResult<AppDramaCardVO> feed(
String types, Long categoryId, Long cursor, Integer size) {
int pageSize = size == null ? DEFAULT_FEED_SIZE : Math.max(1, Math.min(size, MAX_FEED_SIZE));
@@ -137,6 +172,33 @@ public class AppCatalogService {
return detail;
}
/** C 端剧单详情:剧单基本信息 + 该剧单下所有已上线短剧卡片。 */
public AppPlaylistDetailVO playlistDetail(Long playlistId) {
var category = dramaPlaylistCategoryService.getEnabledById(playlistId);
List<Long> allIds = dramaPlaylistCategoryService.listDramaIdsByCategory(playlistId);
List<Drama> published = dramaService.listPublishedByIds(allIds);
// 保留关联表原始顺序:按 allIds 位置重新排序
Map<Long, Integer> orderMap = new HashMap<>();
for (int i = 0; i < allIds.size(); i++) {
orderMap.put(allIds.get(i), i);
}
List<Drama> ordered = published.stream()
.sorted((a, b) -> {
Integer ia = orderMap.getOrDefault(a.getId(), Integer.MAX_VALUE);
Integer ib = orderMap.getOrDefault(b.getId(), Integer.MAX_VALUE);
return Integer.compare(ia, ib);
})
.toList();
AppPlaylistDetailVO vo = new AppPlaylistDetailVO();
vo.setId(category.getId());
vo.setName(category.getName());
vo.setDescription(category.getDescription());
vo.setDramaCount(ordered.size());
vo.setList(cards(ordered, categoryNameMap()));
return vo;
}
private List<AppBannerVO> banners(String position) {
return bannerService.listActive(position).stream().map(this::toBanner).toList();
}
@@ -0,0 +1,19 @@
// Author: fruit.com.cn
package com.fruitshorts.app.vo;
import lombok.Data;
import java.util.List;
/** C 端剧单详情:基本信息 + 该剧单下已上线的短剧卡片。 */
@Data
public class AppPlaylistDetailVO {
private Long id;
private String name;
private String description;
/** 该剧单下已上线短剧的实际条数(被下架/下线的不计入) */
private Integer dramaCount;
private List<AppDramaCardVO> list;
}
@@ -8,6 +8,8 @@ import com.fruitshorts.content.dal.entity.Drama;
import com.fruitshorts.content.dal.entity.DramaEpisode;
import com.fruitshorts.content.service.BannerService;
import com.fruitshorts.content.service.DramaCategoryService;
import com.fruitshorts.content.service.DramaClassificationService;
import com.fruitshorts.content.service.DramaPlaylistCategoryService;
import com.fruitshorts.content.service.DramaService;
import com.fruitshorts.content.service.EpisodeService;
import com.fruitshorts.interaction.service.CommentService;
@@ -45,12 +47,15 @@ class AppCatalogServiceTest {
episodeService = mock(EpisodeService.class);
BannerService bannerService = mock(BannerService.class);
dramaCategoryService = mock(DramaCategoryService.class);
DramaClassificationService dramaClassificationService = mock(DramaClassificationService.class);
DramaPlaylistCategoryService dramaPlaylistCategoryService = mock(DramaPlaylistCategoryService.class);
PlaybackSigner playbackSigner = mock(PlaybackSigner.class);
thumbnailUrlResolver = mock(ThumbnailUrlResolver.class);
given(thumbnailUrlResolver.resolveAll(any())).willReturn(Map.of());
commentService = mock(CommentService.class);
catalogService = new AppCatalogService(dramaService, episodeService, bannerService,
dramaCategoryService, playbackSigner, commentService, thumbnailUrlResolver);
dramaCategoryService, dramaClassificationService, dramaPlaylistCategoryService,
playbackSigner, commentService, thumbnailUrlResolver);
}
@Test
@@ -38,6 +38,7 @@ public class SecurityConfig {
"/app/discover/**",
"/app/feed",
"/app/categories/**",
"/app/playlists/**",
"/app/banners/**",
"/app/dramas/**",
"/app/search",
@@ -27,6 +27,7 @@ import org.springframework.util.StringUtils;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 运营分类服务。分类是独立于「短剧类型(drama_category)」的一套概念,
@@ -190,6 +191,37 @@ public class DramaClassificationService {
}
}
// ========== C 端只读视图 ==========
/** C 端:只返回已启用(status=1)的分类,按 sort 升序。 */
public List<DramaClassification> listEnabled() {
return classificationMapper.selectList(Wrappers.<DramaClassification>lambdaQuery()
.eq(DramaClassification::getStatus, 1)
.orderByAsc(DramaClassification::getSort)
.orderByAsc(DramaClassification::getId));
}
/** C 端:校验分类存在且启用。 */
public DramaClassification getEnabledById(Long id) {
DramaClassification entity = getExisting(id);
if (entity.getStatus() != 1) {
throw new ServiceException(ErrorCode.DATA_NOT_FOUND, "分类不存在或已下架");
}
return entity;
}
/** C 端:分类下关联的全部 dramaId(按关联创建时间 desc)。 */
public List<Long> listDramaIdsByClassification(Long classificationId) {
return classificationDramaMapper.selectList(
Wrappers.<DramaClassificationDrama>lambdaQuery()
.eq(DramaClassificationDrama::getClassificationId, classificationId)
.orderByDesc(DramaClassificationDrama::getCreatedAt)
.orderByDesc(DramaClassificationDrama::getId))
.stream()
.map(DramaClassificationDrama::getDramaId)
.collect(Collectors.toList());
}
private DramaBriefVO toBrief(Drama drama) {
DramaBriefVO vo = new DramaBriefVO();
vo.setId(drama.getId());
@@ -27,6 +27,7 @@ import org.springframework.util.StringUtils;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 剧单分类服务:分类直接关联短剧,没有「剧单」这一层。
@@ -177,6 +178,26 @@ public class DramaPlaylistCategoryService {
return counts;
}
/** C 端用:只返回已启用(status=1)的剧单。 */
public DramaPlaylistCategory getEnabledById(Long id) {
DramaPlaylistCategory entity = getExisting(id);
if (entity.getStatus() == null || entity.getStatus() != 1) {
throw new ServiceException(ErrorCode.DATA_NOT_FOUND, "剧单不存在或已下架");
}
return entity;
}
/** C 端用:剧单下关联的全部 dramaId(按 created_at desc)。 */
public List<Long> listDramaIdsByCategory(Long categoryId) {
return categoryDramaMapper.selectList(Wrappers.<DramaPlaylistCategoryDrama>lambdaQuery()
.eq(DramaPlaylistCategoryDrama::getCategoryId, categoryId)
.orderByDesc(DramaPlaylistCategoryDrama::getCreatedAt)
.orderByDesc(DramaPlaylistCategoryDrama::getId))
.stream()
.map(DramaPlaylistCategoryDrama::getDramaId)
.collect(Collectors.toList());
}
private DramaPlaylistCategory getExisting(Long id) {
DramaPlaylistCategory entity = id == null ? null : categoryMapper.selectById(id);
if (entity == null) {
@@ -226,6 +226,22 @@ public class DramaService {
return dramaMapper.selectPage(page, wrapper).getRecords();
}
/** C 端分类频道:按分类分页查已上线短剧,含 total。 */
public PageResult<Drama> pagePublishedByCategory(Long categoryId, int pageIndex, int pageSize) {
int safeIndex = Math.max(1, pageIndex);
int safeSize = Math.max(1, Math.min(pageSize, 50));
Page<Drama> page = new Page<>(safeIndex, safeSize);
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)
.orderByDesc(Drama::getId);
IPage<Drama> result = dramaMapper.selectPage(page, wrapper);
return new PageResult<>(result.getTotal(), result.getRecords());
}
/** 已上线短剧,按上线时间倒序 */
public List<Drama> listPublishedLatest(long limit) {
Page<Drama> page = new Page<>(1, Math.max(1, limit));
@@ -64,6 +64,30 @@ public class AppVersionService {
return v.getId();
}
/** 批量新建:一次事务全插或全回滚。 */
@Transactional(rollbackFor = Exception.class)
public List<Long> batchCreate(List<AppVersionSaveReqVO> reqs) {
if (reqs == null || reqs.isEmpty()) {
throw new ServiceException(ErrorCode.BAD_REQUEST, "请至少传一条");
}
if (reqs.size() > 16) {
throw new ServiceException(ErrorCode.BAD_REQUEST, "单次不超过 16 条");
}
LocalDateTime now = LocalDateTime.now();
List<Long> ids = new java.util.ArrayList<>();
for (AppVersionSaveReqVO req : reqs) {
String platform = normalizePlatform(req.getPlatform());
checkDuplicate(platform, req.getVersionCode(), null);
AppVersion v = new AppVersion();
applyFields(v, req, platform);
v.setStatus(1);
v.setReleasedAt(now);
versionMapper.insert(v);
ids.add(v.getId());
}
return ids;
}
@Transactional(rollbackFor = Exception.class)
public void update(Long id, AppVersionSaveReqVO req) {
String platform = normalizePlatform(req.getPlatform());
+32
View File
@@ -0,0 +1,32 @@
-- Author: fruit.com.cn
-- 修正 App 版本管理的菜单与权限:
-- 1) 删除误加的 BUTTON 类型菜单 40-44(项目其他模块都只在 permission_code 里挂权限点,
-- 不在 sys_menu 里建按钮行,否则前端菜单渲染会多出无用条目)
-- 2) 在 sys_permission 里加 app-version 完整权限点
-- 3) 给超级管理员(role_id=1)授权这些权限点
-- 1) 删除多余的 BUTTON 菜单
DELETE FROM sys_role_menu WHERE menu_id IN (40, 41, 42, 43, 44);
DELETE FROM sys_menu WHERE id IN (40, 41, 42, 43, 44);
-- 2) 权限点(code 唯一约束兜底)
INSERT INTO sys_permission (id, code, name, group_name, type, sort) VALUES
(50000101, 'app-version:list', '版本管理-查看', 'App 版本', 'BUTTON', 1),
(50000102, 'app-version:create', '版本管理-新增', 'App 版本', 'BUTTON', 2),
(50000103, 'app-version:update', '版本管理-编辑', 'App 版本', 'BUTTON', 3),
(50000104, 'app-version:delete', '版本管理-删除', 'App 版本', 'BUTTON', 4)
ON CONFLICT (code) DO NOTHING;
-- 3) 超管授权(id 按现有 MAX+1 顺延)
INSERT INTO sys_role_permission (id, role_id, permission_id)
SELECT 50001, 1, p.id FROM sys_permission p WHERE p.code = 'app-version:list'
ON CONFLICT DO NOTHING;
INSERT INTO sys_role_permission (id, role_id, permission_id)
SELECT 50002, 1, p.id FROM sys_permission p WHERE p.code = 'app-version:create'
ON CONFLICT DO NOTHING;
INSERT INTO sys_role_permission (id, role_id, permission_id)
SELECT 50003, 1, p.id FROM sys_permission p WHERE p.code = 'app-version:update'
ON CONFLICT DO NOTHING;
INSERT INTO sys_role_permission (id, role_id, permission_id)
SELECT 50004, 1, p.id FROM sys_permission p WHERE p.code = 'app-version:delete'
ON CONFLICT DO NOTHING;