refactor: 后端标识统一为 FruitShorts,并落地 05 离线下载

为什么:
1) 产品已定名 FruitShorts,但后端仍是 reelshort 系列标识 —— Java 包 com.reelshort、
   模块名与 jar 名 reelshort-*、spring.application.name、配置前缀 reelshort.*,
   这些会出现在启动日志、命令行和 IDE 里,与产品名不一致。本次整体替换为 fruitshorts,
   数据库连接名上一轮已改名,保持不变。
2) 第一阶段下一批第 1 步(05 离线下载)要求把下载授权(设备绑定/有效期/吊销)、
   下载任务与断点续传、配额与过期清理、仅 Wi-Fi 规则做成完整接口,本提交一并落地。
3) 顺带修复:schema-phase1.sql 里 playback_ticket / order_event 仍按普通表定义唯一索引,
   表分区化之后重跑脚本必定报错(分区表的唯一索引必须包含分区键)。改为在本脚本里就
   按分区表定义,并验证脚本可重复执行。
This commit is contained in:
Vicky committed 2026-10-15 11:44:26 +08:00
1 parent a4aa749cc6
commit b3179fc836
251 files changed
+7261 -767

No files matched your search

@@ -0,0 +1,14 @@
package com.fruitshorts;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan("com.fruitshorts.**.dal.mapper")
public class FruitShortsApplication {
public static void main(String[] args) {
SpringApplication.run(FruitShortsApplication.class, args);
}
}
@@ -0,0 +1,83 @@
package com.fruitshorts.admin.controller;
import com.fruitshorts.common.pojo.PageResult;
import com.fruitshorts.common.pojo.Result;
import com.fruitshorts.framework.security.SecurityUtils;
import com.fruitshorts.system.service.AdminUserService;
import com.fruitshorts.framework.log.OperationLog;
import com.fruitshorts.system.vo.AdminUserPageReqVO;
import com.fruitshorts.system.vo.AdminUserRespVO;
import com.fruitshorts.system.vo.AdminUserSaveReqVO;
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.Map;
@RestController
@RequestMapping("/admin/admin-users")
@RequiredArgsConstructor
public class AdminUserController {
private final AdminUserService adminUserService;
@GetMapping("/page")
@PreAuthorize("@ss.hasPermission('system:admin-user')")
public Result<PageResult<AdminUserRespVO>> page(AdminUserPageReqVO req) {
return Result.ok(adminUserService.page(req));
}
@GetMapping("/{id}")
@PreAuthorize("@ss.hasPermission('system:admin-user')")
public Result<AdminUserRespVO> get(@PathVariable Long id) {
return Result.ok(adminUserService.get(id));
}
@PostMapping
@PreAuthorize("@ss.hasPermission('system:admin-user')")
@OperationLog(module = "后台管理", action = "新增管理员")
public Result<Long> create(@Valid @RequestBody AdminUserSaveReqVO req) {
return Result.ok(adminUserService.create(req));
}
@PutMapping("/{id}")
@PreAuthorize("@ss.hasPermission('system:admin-user')")
@OperationLog(module = "后台管理", action = "编辑管理员")
public Result<Void> update(@PathVariable Long id, @Valid @RequestBody AdminUserSaveReqVO req) {
adminUserService.update(id, req);
return Result.ok();
}
@DeleteMapping("/{id}")
@PreAuthorize("@ss.hasPermission('system:admin-user')")
@OperationLog(module = "后台管理", action = "删除管理员")
public Result<Void> delete(@PathVariable Long id) {
adminUserService.delete(id);
return Result.ok();
}
@PutMapping("/{id}/status")
@PreAuthorize("@ss.hasPermission('system:admin-user')")
@OperationLog(module = "后台管理", action = "修改管理员状态")
public Result<Void> updateStatus(@PathVariable Long id, @RequestParam Integer status) {
adminUserService.updateStatus(id, status);
return Result.ok();
}
@PutMapping("/{id}/password")
@PreAuthorize("@ss.hasPermission('system:admin-user')")
@OperationLog(module = "后台管理", action = "重置管理员密码")
public Result<Void> resetPassword(@PathVariable Long id, @RequestBody Map<String, String> body) {
adminUserService.resetPassword(id, body.get("password"));
return Result.ok();
}
}
@@ -0,0 +1,118 @@
package com.fruitshorts.admin.controller;
import com.fruitshorts.common.pojo.PageResult;
import com.fruitshorts.common.pojo.Result;
import com.fruitshorts.admin.vo.AppUserWatchHistoryVO;
import com.fruitshorts.content.dal.entity.Drama;
import com.fruitshorts.content.dal.entity.DramaEpisode;
import com.fruitshorts.content.enums.ContentStatus;
import com.fruitshorts.content.service.DramaService;
import com.fruitshorts.content.service.EpisodeService;
import com.fruitshorts.framework.log.OperationLog;
import com.fruitshorts.framework.security.SecurityUtils;
import com.fruitshorts.playback.dal.entity.WatchHistory;
import com.fruitshorts.playback.service.WatchProgressService;
import com.fruitshorts.user.dal.entity.AppUserBanRecord;
import com.fruitshorts.user.service.AppUserAdminService;
import com.fruitshorts.user.vo.AppUserQueryReqVO;
import com.fruitshorts.user.vo.AppUserAdminRespVO;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
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;
import java.util.Map;
import java.util.stream.Collectors;
/** 后台用户管理。 */
@RestController
@RequestMapping("/admin/app-users")
@RequiredArgsConstructor
public class AppUserAdminController {
private final AppUserAdminService appUserAdminService;
private final WatchProgressService watchProgressService;
private final DramaService dramaService;
private final EpisodeService episodeService;
@GetMapping("/page")
@PreAuthorize("@ss.hasPermission('app-user:list')")
public Result<PageResult<AppUserAdminRespVO>> page(AppUserQueryReqVO req) {
return Result.ok(appUserAdminService.page(req));
}
@GetMapping("/{id}")
@PreAuthorize("@ss.hasPermission('app-user:list')")
public Result<AppUserAdminRespVO> detail(@PathVariable Long id) {
return Result.ok(appUserAdminService.detail(id));
}
@GetMapping("/{id}/ban-records")
@PreAuthorize("@ss.hasPermission('app-user:list')")
public Result<List<AppUserBanRecord>> banRecords(@PathVariable Long id) {
return Result.ok(appUserAdminService.banRecords(id));
}
/** 观看流水明细:客服核对"用户说看了但没记录"这类问题时用,带上剧名与集号。 */
@GetMapping("/{id}/watch-history")
@PreAuthorize("@ss.hasPermission('app-user:list')")
public Result<List<AppUserWatchHistoryVO>> watchHistory(@PathVariable Long id,
@RequestParam(defaultValue = "50") int limit) {
List<WatchHistory> rows = watchProgressService.history(id, limit);
if (rows.isEmpty()) {
return Result.ok(List.of());
}
// 批量取剧名与集号:一屏 50 行逐条查库就是 100 次往返
Map<Long, Drama> dramas = dramaService
.listByIds(rows.stream().map(WatchHistory::getDramaId).distinct().toList())
.stream().collect(Collectors.toMap(Drama::getId, item -> item, (a, b) -> a));
Map<Long, DramaEpisode> episodes = episodeService
.listByIds(rows.stream().map(WatchHistory::getEpisodeId).distinct().toList())
.stream().collect(Collectors.toMap(DramaEpisode::getId, item -> item, (a, b) -> a));
return Result.ok(rows.stream().map(row -> toVo(row, dramas, episodes)).toList());
}
private AppUserWatchHistoryVO toVo(WatchHistory row, Map<Long, Drama> dramas,
Map<Long, DramaEpisode> episodes) {
AppUserWatchHistoryVO vo = new AppUserWatchHistoryVO();
vo.setId(row.getId());
vo.setDramaId(row.getDramaId());
vo.setEpisodeId(row.getEpisodeId());
vo.setPositionMs(row.getPositionMs());
vo.setDurationMs(row.getDurationMs());
vo.setSource(row.getSource());
vo.setDeviceId(row.getDeviceId());
vo.setWatchedAt(row.getWatchedAt());
long position = row.getPositionMs() == null ? 0L : row.getPositionMs();
long duration = row.getDurationMs() == null ? 0L : row.getDurationMs();
vo.setFinished(duration > 0 && position + 1000L >= duration);
Drama drama = dramas.get(row.getDramaId());
if (drama != null) {
vo.setDramaTitle(drama.getTitle());
vo.setDramaPublished(ContentStatus.PUBLISHED.getCode().equals(drama.getStatus()));
} else {
vo.setDramaPublished(false);
}
DramaEpisode episode = episodes.get(row.getEpisodeId());
if (episode != null) {
vo.setEpisodeNo(episode.getEpNo());
}
return vo;
}
/** 状态处置:normal/limited/banned。封禁会立即吊销该用户全部会话。 */
@PutMapping("/{id}/status")
@PreAuthorize("@ss.hasPermission('app-user:update')")
@OperationLog(module = "用户管理", action = "修改用户状态")
public Result<Void> updateStatus(@PathVariable Long id,
@RequestParam String status,
@RequestParam(required = false) String reason) {
appUserAdminService.updateStatus(id, status, reason, SecurityUtils.getUserId());
return Result.ok();
}
}
@@ -0,0 +1,61 @@
package com.fruitshorts.admin.controller;
import com.fruitshorts.common.pojo.Result;
import com.fruitshorts.framework.security.SecurityUtils;
import com.fruitshorts.framework.log.OperationLog;
import com.fruitshorts.framework.web.WebUtils;
import com.fruitshorts.system.service.AuthService;
import com.fruitshorts.system.vo.LoginReqVO;
import com.fruitshorts.system.vo.LoginRespVO;
import com.fruitshorts.system.vo.MenuVO;
import com.fruitshorts.system.vo.PasswordChangeReqVO;
import com.fruitshorts.system.vo.ProfileRespVO;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/admin/auth")
@RequiredArgsConstructor
public class AuthController {
private final AuthService authService;
@PostMapping("/login")
@OperationLog(module = "认证", action = "登录")
public Result<LoginRespVO> login(@Valid @RequestBody LoginReqVO req, HttpServletRequest request) {
return Result.ok(authService.login(req, WebUtils.getClientIp(request)));
}
/** 当前用户的菜单路由树,结构与前端 element-plus-admin 的动态路由一致。 */
@GetMapping("/menus")
public Result<List<MenuVO>> menus() {
return Result.ok(authService.listMenus(SecurityUtils.getUserId()));
}
@GetMapping("/profile")
public Result<ProfileRespVO> profile() {
return Result.ok(authService.profile(SecurityUtils.getUserId()));
}
/** JWT 无状态,登出由前端清除 token 即可。 */
@PostMapping("/logout")
@OperationLog(module = "认证", action = "退出登录")
public Result<Void> logout() {
return Result.ok();
}
@PostMapping("/password")
@OperationLog(module = "认证", action = "修改密码")
public Result<Void> changePassword(@Valid @RequestBody PasswordChangeReqVO req) {
authService.changePassword(SecurityUtils.getUserId(), req.getOldPassword(), req.getNewPassword());
return Result.ok();
}
}
@@ -0,0 +1,86 @@
package com.fruitshorts.admin.controller;
import com.fruitshorts.common.pojo.PageResult;
import com.fruitshorts.common.pojo.Result;
import com.fruitshorts.content.service.BannerService;
import com.fruitshorts.content.vo.BannerRespVO;
import com.fruitshorts.content.vo.BannerSaveReqVO;
import com.fruitshorts.framework.security.SecurityUtils;
import com.fruitshorts.framework.log.OperationLog;
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;
import java.util.Map;
@RestController
@RequestMapping("/admin/banners")
@RequiredArgsConstructor
public class BannerController {
private final BannerService bannerService;
@GetMapping("/page")
@PreAuthorize("@ss.hasPermission('banner:list')")
public Result<PageResult<BannerRespVO>> page(@RequestParam(required = false) String position,
@RequestParam(required = false) Integer status,
@RequestParam(defaultValue = "1") Integer pageIndex,
@RequestParam(defaultValue = "10") Integer pageSize) {
return Result.ok(bannerService.page(position, status, pageIndex, pageSize));
}
@GetMapping("/{id}")
@PreAuthorize("@ss.hasPermission('banner:list')")
public Result<BannerRespVO> get(@PathVariable Long id) {
return Result.ok(bannerService.get(id));
}
@PostMapping
@PreAuthorize("@ss.hasPermission('banner:create')")
@OperationLog(module = "内容管理", action = "新增轮播图")
public Result<Long> create(@Valid @RequestBody BannerSaveReqVO req) {
return Result.ok(bannerService.create(req, SecurityUtils.getUserId()));
}
@PutMapping("/{id}")
@PreAuthorize("@ss.hasPermission('banner:update')")
@OperationLog(module = "内容管理", action = "编辑轮播图")
public Result<Void> update(@PathVariable Long id, @Valid @RequestBody BannerSaveReqVO req) {
bannerService.update(id, req);
return Result.ok();
}
@DeleteMapping("/{id}")
@PreAuthorize("@ss.hasPermission('banner:delete')")
@OperationLog(module = "内容管理", action = "删除轮播图")
public Result<Void> delete(@PathVariable Long id) {
bannerService.delete(id);
return Result.ok();
}
@PutMapping("/{id}/status")
@PreAuthorize("@ss.hasPermission('banner:update')")
@OperationLog(module = "内容管理", action = "修改轮播图状态")
public Result<Void> updateStatus(@PathVariable Long id, @RequestParam Integer status) {
bannerService.updateStatus(id, status);
return Result.ok();
}
@PutMapping("/sort")
@PreAuthorize("@ss.hasPermission('banner:update')")
public Result<Void> sort(@RequestBody Map<String, List<Long>> body) {
bannerService.updateSort(body.getOrDefault("ids", List.of()));
return Result.ok();
}
}
@@ -0,0 +1,38 @@
package com.fruitshorts.admin.controller;
import com.fruitshorts.common.pojo.Result;
import com.fruitshorts.system.service.ConfigService;
import com.fruitshorts.framework.log.OperationLog;
import com.fruitshorts.system.vo.ConfigItemVO;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
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.RestController;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/admin/configs")
@RequiredArgsConstructor
public class ConfigController {
private final ConfigService configService;
@GetMapping
@PreAuthorize("@ss.hasAnyPermission('system:role','system:menu')")
public Result<List<ConfigItemVO>> list() {
return Result.ok(configService.list());
}
@PutMapping
@PreAuthorize("@ss.hasPermission('system:role')")
@OperationLog(module = "后台管理", action = "修改系统设置")
public Result<Void> update(@RequestBody Map<String, String> values) {
configService.update(values);
return Result.ok();
}
}
@@ -0,0 +1,40 @@
package com.fruitshorts.admin.controller;
import com.fruitshorts.common.pojo.Result;
import com.fruitshorts.content.enums.ContentStatus;
import com.fruitshorts.content.service.DramaCategoryService;
import com.fruitshorts.content.vo.DramaCategoryRespVO;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
/**
* 字典接口:题材、内容状态等下拉数据。
*/
@RestController
@RequestMapping("/admin/dicts")
@RequiredArgsConstructor
public class DictController {
private final DramaCategoryService dramaCategoryService;
@GetMapping("/drama-category")
@PreAuthorize("@ss.hasPermission('drama:list')")
public Result<List<DramaCategoryRespVO>> dramaCategories() {
return Result.ok(dramaCategoryService.list());
}
@GetMapping("/content-status")
@PreAuthorize("@ss.hasPermission('drama:list')")
public Result<List<Map<String, String>>> contentStatus() {
return Result.ok(Arrays.stream(ContentStatus.values())
.map(status -> Map.of("code", status.getCode(), "text", status.getText()))
.toList());
}
}
@@ -0,0 +1,56 @@
package com.fruitshorts.admin.controller;
import com.fruitshorts.common.pojo.PageResult;
import com.fruitshorts.common.pojo.Result;
import com.fruitshorts.framework.log.OperationLog;
import com.fruitshorts.playback.service.DownloadService;
import com.fruitshorts.playback.vo.DownloadLicenseRespVO;
import com.fruitshorts.playback.vo.DownloadQueryReqVO;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
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.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
/** 后台下载授权管理。 */
@RestController
@RequestMapping("/admin/downloads")
@RequiredArgsConstructor
public class DownloadAdminController {
private final DownloadService downloadService;
@GetMapping("/page")
@PreAuthorize("@ss.hasPermission('download:list')")
public Result<PageResult<DownloadLicenseRespVO>> page(DownloadQueryReqVO req) {
return Result.ok(downloadService.page(req));
}
@GetMapping("/stats")
@PreAuthorize("@ss.hasPermission('download:list')")
public Result<Map<String, Object>> stats() {
return Result.ok(downloadService.stats());
}
/** 吊销单条授权:用户侧下次联网即失效。 */
@PutMapping("/{id}/revoke")
@PreAuthorize("@ss.hasPermission('download:revoke')")
@OperationLog(module = "下载管理", action = "吊销下载授权")
public Result<Void> revoke(@PathVariable Long id) {
downloadService.adminRevoke(id);
return Result.ok();
}
/** 手动触发过期清理,正常情况下由定时任务每天执行。 */
@PostMapping("/cleanup")
@PreAuthorize("@ss.hasPermission('download:revoke')")
@OperationLog(module = "下载管理", action = "清理过期下载授权")
public Result<Integer> cleanup() {
return Result.ok(downloadService.expireLicenses());
}
}
@@ -0,0 +1,61 @@
package com.fruitshorts.admin.controller;
import com.fruitshorts.common.pojo.Result;
import com.fruitshorts.content.service.DramaCategoryService;
import com.fruitshorts.content.vo.DramaCategoryRespVO;
import com.fruitshorts.content.vo.DramaCategorySaveReqVO;
import com.fruitshorts.framework.security.SecurityUtils;
import com.fruitshorts.framework.log.OperationLog;
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.RestController;
import java.util.List;
/**
* 短剧类型管理。类型属于短剧域,权限沿用 drama:*,不再单开权限点。
*/
@RestController
@RequestMapping("/admin/drama-categories")
@RequiredArgsConstructor
public class DramaCategoryController {
private final DramaCategoryService dramaCategoryService;
@GetMapping
@PreAuthorize("@ss.hasPermission('drama:list')")
public Result<List<DramaCategoryRespVO>> list() {
return Result.ok(dramaCategoryService.list());
}
@PostMapping
@PreAuthorize("@ss.hasPermission('drama:create')")
@OperationLog(module = "内容管理", action = "新增短剧类型")
public Result<Long> create(@Valid @RequestBody DramaCategorySaveReqVO req) {
return Result.ok(dramaCategoryService.create(req));
}
@PutMapping("/{id}")
@PreAuthorize("@ss.hasPermission('drama:update')")
@OperationLog(module = "内容管理", action = "编辑短剧类型")
public Result<Void> update(@PathVariable Long id, @Valid @RequestBody DramaCategorySaveReqVO req) {
dramaCategoryService.update(id, req);
return Result.ok();
}
@DeleteMapping("/{id}")
@PreAuthorize("@ss.hasPermission('drama:delete')")
@OperationLog(module = "内容管理", action = "删除短剧类型")
public Result<Void> delete(@PathVariable Long id) {
dramaCategoryService.delete(id);
return Result.ok();
}
}
@@ -0,0 +1,120 @@
package com.fruitshorts.admin.controller;
import com.fruitshorts.common.pojo.PageResult;
import com.fruitshorts.common.pojo.Result;
import com.fruitshorts.content.dal.entity.DramaReviewRecord;
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 com.fruitshorts.content.vo.DramaStatusReqVO;
import com.fruitshorts.framework.security.SecurityUtils;
import com.fruitshorts.framework.log.OperationLog;
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.RestController;
import java.util.List;
@RestController
@RequestMapping("/admin/dramas")
@RequiredArgsConstructor
public class DramaController {
private final DramaService dramaService;
@GetMapping("/page")
@PreAuthorize("@ss.hasPermission('drama:list')")
public Result<PageResult<DramaRespVO>> page(DramaPageReqVO req) {
return Result.ok(dramaService.page(req));
}
@GetMapping("/{id}")
@PreAuthorize("@ss.hasPermission('drama:list')")
public Result<DramaRespVO> get(@PathVariable Long id) {
return Result.ok(dramaService.get(id));
}
@PostMapping
@PreAuthorize("@ss.hasPermission('drama:create')")
@OperationLog(module = "内容管理", action = "新建短剧")
public Result<Long> create(@Valid @RequestBody DramaSaveReqVO req) {
return Result.ok(dramaService.create(req, SecurityUtils.getUserId()));
}
@PutMapping("/{id}")
@PreAuthorize("@ss.hasPermission('drama:update')")
@OperationLog(module = "内容管理", action = "编辑短剧")
public Result<Void> update(@PathVariable Long id, @Valid @RequestBody DramaSaveReqVO req) {
dramaService.update(id, req);
return Result.ok();
}
@DeleteMapping("/{id}")
@PreAuthorize("@ss.hasPermission('drama:delete')")
@OperationLog(module = "内容管理", action = "删除短剧")
public Result<Void> delete(@PathVariable Long id) {
dramaService.delete(id);
return Result.ok();
}
/** 提交审核:内容运营即可操作。 */
@PostMapping("/{id}/submit")
@PreAuthorize("@ss.hasAnyPermission('drama:review','drama:update')")
@OperationLog(module = "内容管理", action = "提交审核")
public Result<Void> submit(@PathVariable Long id, @RequestBody(required = false) DramaStatusReqVO req) {
changeStatus(id, "SUBMIT", req);
return Result.ok();
}
@PostMapping("/{id}/approve")
@PreAuthorize("@ss.hasPermission('drama:review')")
@OperationLog(module = "内容管理", action = "审核通过")
public Result<Void> approve(@PathVariable Long id, @RequestBody(required = false) DramaStatusReqVO req) {
changeStatus(id, "APPROVE", req);
return Result.ok();
}
@PostMapping("/{id}/reject")
@PreAuthorize("@ss.hasPermission('drama:review')")
@OperationLog(module = "内容管理", action = "驳回短剧")
public Result<Void> reject(@PathVariable Long id, @RequestBody(required = false) DramaStatusReqVO req) {
changeStatus(id, "REJECT", req);
return Result.ok();
}
@PostMapping("/{id}/publish")
@PreAuthorize("@ss.hasPermission('drama:review')")
@OperationLog(module = "内容管理", action = "短剧上线")
public Result<Void> publish(@PathVariable Long id, @RequestBody(required = false) DramaStatusReqVO req) {
changeStatus(id, "PUBLISH", req);
return Result.ok();
}
@PostMapping("/{id}/offline")
@PreAuthorize("@ss.hasPermission('drama:review')")
@OperationLog(module = "内容管理", action = "短剧下线")
public Result<Void> offline(@PathVariable Long id, @RequestBody(required = false) DramaStatusReqVO req) {
changeStatus(id, "OFFLINE", req);
return Result.ok();
}
@GetMapping("/{id}/review-records")
@PreAuthorize("@ss.hasPermission('drama:list')")
public Result<List<DramaReviewRecord>> reviewRecords(@PathVariable Long id) {
return Result.ok(dramaService.listReviewRecords(id));
}
private void changeStatus(Long id, String action, DramaStatusReqVO req) {
dramaService.changeStatus(id, action, req == null ? null : req.getReason(),
SecurityUtils.getUserId(), SecurityUtils.getUsername());
}
}
@@ -0,0 +1,68 @@
package com.fruitshorts.admin.controller;
import com.fruitshorts.common.pojo.Result;
import com.fruitshorts.content.service.EpisodeService;
import com.fruitshorts.content.vo.EpisodeRespVO;
import com.fruitshorts.content.vo.EpisodeSaveReqVO;
import com.fruitshorts.content.vo.EpisodeSortReqVO;
import com.fruitshorts.framework.log.OperationLog;
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;
@RestController
@RequestMapping("/admin/episodes")
@RequiredArgsConstructor
public class EpisodeController {
private final EpisodeService episodeService;
@GetMapping
@PreAuthorize("@ss.hasPermission('episode:list')")
public Result<List<EpisodeRespVO>> list(@RequestParam Long dramaId,
@RequestParam(required = false) String status) {
return Result.ok(episodeService.listByDrama(dramaId, status));
}
@PostMapping
@PreAuthorize("@ss.hasPermission('episode:upload')")
@OperationLog(module = "内容管理", action = "新增剧集")
public Result<Long> create(@Valid @RequestBody EpisodeSaveReqVO req) {
return Result.ok(episodeService.create(req));
}
@PutMapping("/{id}")
@PreAuthorize("@ss.hasPermission('episode:upload')")
@OperationLog(module = "内容管理", action = "编辑剧集")
public Result<Void> update(@PathVariable Long id, @Valid @RequestBody EpisodeSaveReqVO req) {
episodeService.update(id, req);
return Result.ok();
}
@DeleteMapping("/{id}")
@PreAuthorize("@ss.hasPermission('episode:upload')")
@OperationLog(module = "内容管理", action = "删除剧集")
public Result<Void> delete(@PathVariable Long id) {
episodeService.delete(id);
return Result.ok();
}
@PutMapping("/sort")
@PreAuthorize("@ss.hasPermission('episode:sort')")
@OperationLog(module = "内容管理", action = "调整剧集顺序")
public Result<Void> sort(@Valid @RequestBody EpisodeSortReqVO req) {
episodeService.updateSort(req.getIds());
return Result.ok();
}
}
@@ -0,0 +1,58 @@
package com.fruitshorts.admin.controller;
import com.fruitshorts.common.pojo.Result;
import com.fruitshorts.framework.security.SecurityUtils;
import com.fruitshorts.media.service.MediaService;
import com.fruitshorts.media.vo.MediaAssetRespVO;
import com.fruitshorts.media.vo.UploadCompleteReqVO;
import com.fruitshorts.media.vo.UploadInitReqVO;
import com.fruitshorts.media.vo.UploadInitRespVO;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
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.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
/**
* 媒资上传(本地存储版):init 建会话 → chunk 传分片 → complete 合并出地址。
*/
@RestController
@RequestMapping("/admin/media")
@RequiredArgsConstructor
public class MediaController {
private final MediaService mediaService;
@PostMapping("/upload/init")
@PreAuthorize("@ss.hasAnyPermission('cover:upload','episode:upload')")
public Result<UploadInitRespVO> init(@Valid @RequestBody UploadInitReqVO req) {
return Result.ok(mediaService.init(req, SecurityUtils.getUserId()));
}
@PostMapping("/upload/chunk")
@PreAuthorize("@ss.hasAnyPermission('cover:upload','episode:upload')")
public Result<Integer> uploadChunk(@RequestParam String uploadId,
@RequestParam int index,
@RequestParam("file") MultipartFile file) {
mediaService.saveChunk(uploadId, index, file);
return Result.ok(index);
}
@PostMapping("/upload/complete")
@PreAuthorize("@ss.hasAnyPermission('cover:upload','episode:upload')")
public Result<MediaAssetRespVO> complete(@Valid @RequestBody UploadCompleteReqVO req) {
return Result.ok(mediaService.complete(req));
}
@GetMapping("/{id}")
@PreAuthorize("@ss.hasAnyPermission('cover:upload','episode:upload')")
public Result<MediaAssetRespVO> get(@PathVariable Long id) {
return Result.ok(mediaService.get(id));
}
}
@@ -0,0 +1,57 @@
package com.fruitshorts.admin.controller;
import com.fruitshorts.common.pojo.Result;
import com.fruitshorts.system.service.MenuService;
import com.fruitshorts.framework.log.OperationLog;
import com.fruitshorts.system.vo.MenuSaveReqVO;
import com.fruitshorts.system.vo.MenuVO;
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.RestController;
import java.util.List;
@RestController
@RequestMapping("/admin/menus")
@RequiredArgsConstructor
public class MenuController {
private final MenuService menuService;
@GetMapping("/tree")
@PreAuthorize("@ss.hasPermission('system:menu')")
public Result<List<MenuVO>> tree() {
return Result.ok(menuService.listAll());
}
@PostMapping
@PreAuthorize("@ss.hasPermission('system:menu')")
@OperationLog(module = "后台管理", action = "新增菜单")
public Result<Long> create(@Valid @RequestBody MenuSaveReqVO req) {
return Result.ok(menuService.create(req));
}
@PutMapping("/{id}")
@PreAuthorize("@ss.hasPermission('system:menu')")
@OperationLog(module = "后台管理", action = "编辑菜单")
public Result<Void> update(@PathVariable Long id, @Valid @RequestBody MenuSaveReqVO req) {
menuService.update(id, req);
return Result.ok();
}
@DeleteMapping("/{id}")
@PreAuthorize("@ss.hasPermission('system:menu')")
@OperationLog(module = "后台管理", action = "删除菜单")
public Result<Void> delete(@PathVariable Long id) {
menuService.delete(id);
return Result.ok();
}
}
@@ -0,0 +1,33 @@
package com.fruitshorts.admin.controller;
import com.fruitshorts.common.pojo.PageResult;
import com.fruitshorts.common.pojo.Result;
import com.fruitshorts.system.service.OperationLogService;
import com.fruitshorts.system.vo.OperationLogPageReqVO;
import com.fruitshorts.system.vo.OperationLogRespVO;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
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.RestController;
@RestController
@RequestMapping("/admin/operation-logs")
@RequiredArgsConstructor
public class OperationLogController {
private final OperationLogService operationLogService;
@GetMapping("/page")
@PreAuthorize("@ss.hasPermission('log:list')")
public Result<PageResult<OperationLogRespVO>> page(OperationLogPageReqVO req) {
return Result.ok(operationLogService.page(req));
}
@GetMapping("/{id}")
@PreAuthorize("@ss.hasPermission('log:list')")
public Result<OperationLogRespVO> get(@PathVariable Long id) {
return Result.ok(operationLogService.get(id));
}
}
@@ -0,0 +1,27 @@
package com.fruitshorts.admin.controller;
import com.fruitshorts.common.pojo.Result;
import com.fruitshorts.system.service.PermissionService;
import com.fruitshorts.system.vo.PermissionGroupVO;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/admin/permissions")
@RequiredArgsConstructor
public class PermissionController {
private final PermissionService permissionService;
/** 按分组返回权限点,直接喂给前端的角色权限配置页。 */
@GetMapping
@PreAuthorize("@ss.hasPermission('system:role')")
public Result<List<PermissionGroupVO>> groups() {
return Result.ok(permissionService.listGroups());
}
}
@@ -0,0 +1,57 @@
package com.fruitshorts.admin.controller;
import com.fruitshorts.common.pojo.Result;
import com.fruitshorts.system.service.RoleCategoryService;
import com.fruitshorts.framework.log.OperationLog;
import com.fruitshorts.system.vo.RoleCategoryRespVO;
import com.fruitshorts.system.vo.RoleCategorySaveReqVO;
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.RestController;
import java.util.List;
@RestController
@RequestMapping("/admin/role-categories")
@RequiredArgsConstructor
public class RoleCategoryController {
private final RoleCategoryService roleCategoryService;
@GetMapping
@PreAuthorize("@ss.hasPermission('system:role-category')")
public Result<List<RoleCategoryRespVO>> list() {
return Result.ok(roleCategoryService.list());
}
@PostMapping
@PreAuthorize("@ss.hasPermission('system:role-category')")
@OperationLog(module = "后台管理", action = "新增角色分类")
public Result<Long> create(@Valid @RequestBody RoleCategorySaveReqVO req) {
return Result.ok(roleCategoryService.create(req));
}
@PutMapping("/{id}")
@PreAuthorize("@ss.hasPermission('system:role-category')")
@OperationLog(module = "后台管理", action = "编辑角色分类")
public Result<Void> update(@PathVariable Long id, @Valid @RequestBody RoleCategorySaveReqVO req) {
roleCategoryService.update(id, req);
return Result.ok();
}
@DeleteMapping("/{id}")
@PreAuthorize("@ss.hasPermission('system:role-category')")
@OperationLog(module = "后台管理", action = "删除角色分类")
public Result<Void> delete(@PathVariable Long id) {
roleCategoryService.delete(id);
return Result.ok();
}
}
@@ -0,0 +1,88 @@
package com.fruitshorts.admin.controller;
import com.fruitshorts.common.pojo.PageResult;
import com.fruitshorts.common.pojo.Result;
import com.fruitshorts.system.service.RoleService;
import com.fruitshorts.framework.log.OperationLog;
import com.fruitshorts.system.vo.RolePermissionSaveReqVO;
import com.fruitshorts.system.vo.RoleRespVO;
import com.fruitshorts.system.vo.RoleSaveReqVO;
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;
@RestController
@RequestMapping("/admin/roles")
@RequiredArgsConstructor
public class RoleController {
private final RoleService roleService;
@GetMapping("/page")
@PreAuthorize("@ss.hasPermission('system:role')")
public Result<PageResult<RoleRespVO>> page(@RequestParam(required = false) String keyword,
@RequestParam(defaultValue = "1") Integer pageIndex,
@RequestParam(defaultValue = "10") Integer pageSize) {
return Result.ok(roleService.page(keyword, pageIndex, pageSize));
}
@GetMapping("/list")
public Result<List<RoleRespVO>> list() {
return Result.ok(roleService.listAll());
}
@GetMapping("/{id}")
@PreAuthorize("@ss.hasPermission('system:role')")
public Result<RoleRespVO> get(@PathVariable Long id) {
return Result.ok(roleService.get(id));
}
@PostMapping
@PreAuthorize("@ss.hasPermission('system:role')")
@OperationLog(module = "后台管理", action = "新增角色")
public Result<Long> create(@Valid @RequestBody RoleSaveReqVO req) {
return Result.ok(roleService.create(req));
}
@PutMapping("/{id}")
@PreAuthorize("@ss.hasPermission('system:role')")
@OperationLog(module = "后台管理", action = "编辑角色")
public Result<Void> update(@PathVariable Long id, @Valid @RequestBody RoleSaveReqVO req) {
roleService.update(id, req);
return Result.ok();
}
@DeleteMapping("/{id}")
@PreAuthorize("@ss.hasPermission('system:role')")
@OperationLog(module = "后台管理", action = "删除角色")
public Result<Void> delete(@PathVariable Long id) {
roleService.delete(id);
return Result.ok();
}
@GetMapping("/{id}/permissions")
@PreAuthorize("@ss.hasPermission('system:role')")
public Result<List<String>> permissions(@PathVariable Long id) {
return Result.ok(roleService.listCodesByRoleId(id));
}
@PutMapping("/{id}/permissions")
@PreAuthorize("@ss.hasPermission('system:role')")
@OperationLog(module = "后台管理", action = "修改角色权限")
public Result<Void> updatePermissions(@PathVariable Long id,
@Valid @RequestBody RolePermissionSaveReqVO req) {
roleService.updatePermissions(id, req.getPermissions());
return Result.ok();
}
}
@@ -0,0 +1,30 @@
package com.fruitshorts.admin.vo;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 后台观看流水行。
*
* <p>比原始流水多了剧名与集号:客服核对问题时看「闪婚后 · 第 3 集」比看
* 「dramaId=5 / episodeId=9」有用得多。剧已下架时仍然显示剧名,只标注状态。
*/
@Data
public class AppUserWatchHistoryVO {
private Long id;
private Long dramaId;
private String dramaTitle;
/** 短剧当前是否已上线,用于识别"用户说看了但剧已下线"的情况 */
private Boolean dramaPublished;
private Long episodeId;
private Integer episodeNo;
private Long positionMs;
private Long durationMs;
/** 是否已看完(进度到达片尾 1 秒内) */
private Boolean finished;
private String source;
private String deviceId;
private LocalDateTime watchedAt;
}
@@ -0,0 +1,53 @@
server:
port: 8080
spring:
application:
name: fruitshorts-admin-api
autoconfigure:
exclude: org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration
datasource:
driver-class-name: org.postgresql.Driver
url: jdbc:postgresql://127.0.0.1:5432/fruitshorts
username: ${DB_USER:fruitshorts}
password: ${DB_PASSWORD:fruitshorts}
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: Asia/Shanghai
servlet:
multipart:
# 分片上传,单个请求只需要容纳一个分片
max-file-size: 20MB
max-request-size: 20MB
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
global-config:
banner: false
db-config:
id-type: auto
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
fruitshorts:
media:
# 本地媒资目录(相对应用启动目录),通过 /files/** 访问
root-dir: ./data/uploads
public-prefix: /files
chunk-size: 5242880
max-file-size: 2147483648
jwt:
admin-secret: ${JWT_ADMIN_SECRET:fruitshorts-admin-dev-secret-change-me-in-production-32b}
admin-ttl-hours: 12
cors:
allowed-origins: http://localhost:3005,http://127.0.0.1:3005
springdoc:
swagger-ui:
path: /doc.html
logging:
level:
com.fruitshorts: debug