Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27e689c771 | ||
|
|
5ce6de1e3b | ||
|
|
c6fcfcc6ea | ||
|
|
fafadfc6ec | ||
|
|
84aa0e17dd | ||
|
|
37c9a141d3 | ||
|
|
6833e01e1f |
No files matched your search
@@ -74,6 +74,30 @@ POST /app/auth/register
|
||||
}
|
||||
```
|
||||
|
||||
**邮箱 + 密码注册(C 端第一批口径)**
|
||||
|
||||
```json
|
||||
POST /app/auth/register
|
||||
{
|
||||
"targetType": "EMAIL", "target": "User@Example.com ", "password": "nonton2026",
|
||||
"deviceId": "d-1", "countryCode": "ID", "agreementVersion": "2026-10-01"
|
||||
}
|
||||
```
|
||||
|
||||
| 规则 | 口径 |
|
||||
| --- | --- |
|
||||
| `targetType=EMAIL` | **`code` 可以不传**(邮箱+密码直接注册);传了就走一次验证码校验(邮箱验证能力保留) |
|
||||
| `targetType=PHONE` | 维持原状:`code` **必填** |
|
||||
| 邮箱规范化 | 去空格 + 转小写(`batch1@Example.com ` → `batch1@example.com`);格式不合法直接 `400 邮箱格式不正确` |
|
||||
| 密码强度 | 8–64 位,且**同时包含字母与数字**(不强制特殊字符:低端机输入成本高,拦机器注册靠频控) |
|
||||
| 注册频控 | 同 IP 24h ≤ **5** 个、同设备 24h ≤ **3** 个,超出返回 `429`(`TOO_MANY_REQUESTS`) |
|
||||
| 邮箱验证状态 | 免码注册时 `app_user_credential.verified = 0`(保留"待验证"),**不阻塞登录**;只有**验证码登录**会把 `verified` 置 1,密码登录不改变该状态 |
|
||||
|
||||
频控口径落库在 `app_user.register_ip` / `register_device_id` 上(按 24h 计数,不新建计数表)。
|
||||
**残留风险**:IP 取自 `X-Forwarded-For`、设备号由客户端上报,都能伪造,只能挡脚本批量;
|
||||
真要防住需要接图形/行为验证码或风控服务,本阶段未做。验证码通道目前是 `LogOtpSender`(只写日志不发信),
|
||||
所以邮箱**可达性**未验证 —— 这也是"不阻塞注册"的前提。
|
||||
|
||||
响应(登录、刷新同结构):
|
||||
|
||||
```json
|
||||
|
||||
@@ -5,7 +5,48 @@ package com.fruitshorts;
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
|
||||
import io.swagger.v3.oas.annotations.info.Info;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
@OpenAPIDefinition(
|
||||
info = @Info(
|
||||
title = "FruitShorts 运营后台接口(/admin/**)",
|
||||
version = "1.0.0",
|
||||
description = "接口契约以 FruitServer/docs/api-phase1.md 为准"
|
||||
),
|
||||
tags = {
|
||||
@Tag(name = "后台-管理员账号", description = "后台管理员账号管理"),
|
||||
@Tag(name = "后台-App 用户", description = "App 用户查询与处置"),
|
||||
@Tag(name = "后台-登录与个人", description = "管理端登录、菜单、个人资料与改密"),
|
||||
@Tag(name = "后台-轮播图管理", description = "轮播图维护与排序"),
|
||||
@Tag(name = "后台-评论审核", description = "评论检索与审核"),
|
||||
@Tag(name = "后台-系统配置", description = "系统参数配置"),
|
||||
@Tag(name = "后台-创作者管理", description = "创作者入驻审核与账号开通"),
|
||||
@Tag(name = "创作者中心", description = "创作者工作台(作品、通知、看板)"),
|
||||
@Tag(name = "创作者中心-内容与收费", description = "创作者内容编辑、提交审核与分集收费"),
|
||||
@Tag(name = "后台-数据看板", description = "运营大盘与转码看板"),
|
||||
@Tag(name = "后台-字典", description = "分类与状态字典"),
|
||||
@Tag(name = "后台-离线下载管理", description = "下载授权查询、吊销与清理"),
|
||||
@Tag(name = "后台-短剧分类", description = "短剧分类维护"),
|
||||
@Tag(name = "后台-短剧管理", description = "短剧 CRUD 与审核发布流转"),
|
||||
@Tag(name = "后台-权益管理", description = "权益补发与回收"),
|
||||
@Tag(name = "后台-剧集管理", description = "剧集 CRUD 与排序"),
|
||||
@Tag(name = "后台-媒资管理", description = "分片上传与媒资详情"),
|
||||
@Tag(name = "后台-菜单管理", description = "菜单树维护"),
|
||||
@Tag(name = "后台-操作日志", description = "操作日志检索"),
|
||||
@Tag(name = "后台-订单管理", description = "订单检索、补发与退款"),
|
||||
@Tag(name = "后台-权限点", description = "权限点查询"),
|
||||
@Tag(name = "后台-分集收费", description = "分集价格配置"),
|
||||
@Tag(name = "后台-对账", description = "对账任务与手动对账"),
|
||||
@Tag(name = "后台-举报工单", description = "举报工单处理"),
|
||||
@Tag(name = "后台-角色分类", description = "角色分类维护"),
|
||||
@Tag(name = "后台-角色管理", description = "角色与角色权限"),
|
||||
@Tag(name = "后台-敏感词库", description = "敏感词维护"),
|
||||
@Tag(name = "后台-内容提交审核", description = "创作者内容提交审核"),
|
||||
@Tag(name = "后台-金币钱包", description = "金币流水与人工补币"),
|
||||
}
|
||||
)
|
||||
@SpringBootApplication
|
||||
@MapperScan("com.fruitshorts.**.dal.mapper")
|
||||
public class FruitShortsApplication {
|
||||
|
||||
+10
@@ -10,6 +10,8 @@ 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 io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
@@ -25,6 +27,7 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Tag(name = "后台-管理员账号")
|
||||
@RestController
|
||||
@RequestMapping("/admin/admin-users")
|
||||
@RequiredArgsConstructor
|
||||
@@ -32,18 +35,21 @@ public class AdminUserController {
|
||||
|
||||
private final AdminUserService adminUserService;
|
||||
|
||||
@Operation(summary = "管理员分页查询")
|
||||
@GetMapping("/page")
|
||||
@PreAuthorize("@ss.hasPermission('system:admin-user')")
|
||||
public Result<PageResult<AdminUserRespVO>> page(AdminUserPageReqVO req) {
|
||||
return Result.ok(adminUserService.page(req));
|
||||
}
|
||||
|
||||
@Operation(summary = "管理员详情")
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("@ss.hasPermission('system:admin-user')")
|
||||
public Result<AdminUserRespVO> get(@PathVariable Long id) {
|
||||
return Result.ok(adminUserService.get(id));
|
||||
}
|
||||
|
||||
@Operation(summary = "新建管理员")
|
||||
@PostMapping
|
||||
@PreAuthorize("@ss.hasPermission('system:admin-user')")
|
||||
@OperationLog(module = "后台管理", action = "新增管理员")
|
||||
@@ -51,6 +57,7 @@ public class AdminUserController {
|
||||
return Result.ok(adminUserService.create(req));
|
||||
}
|
||||
|
||||
@Operation(summary = "编辑管理员")
|
||||
@PutMapping("/{id}")
|
||||
@PreAuthorize("@ss.hasPermission('system:admin-user')")
|
||||
@OperationLog(module = "后台管理", action = "编辑管理员")
|
||||
@@ -59,6 +66,7 @@ public class AdminUserController {
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@Operation(summary = "删除管理员")
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("@ss.hasPermission('system:admin-user')")
|
||||
@OperationLog(module = "后台管理", action = "删除管理员")
|
||||
@@ -67,6 +75,7 @@ public class AdminUserController {
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@Operation(summary = "启用 / 停用管理员")
|
||||
@PutMapping("/{id}/status")
|
||||
@PreAuthorize("@ss.hasPermission('system:admin-user')")
|
||||
@OperationLog(module = "后台管理", action = "修改管理员状态")
|
||||
@@ -75,6 +84,7 @@ public class AdminUserController {
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@Operation(summary = "重置管理员密码")
|
||||
@PutMapping("/{id}/password")
|
||||
@PreAuthorize("@ss.hasPermission('system:admin-user')")
|
||||
@OperationLog(module = "后台管理", action = "重置管理员密码")
|
||||
|
||||
+8
@@ -18,6 +18,8 @@ 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 io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -32,6 +34,7 @@ import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/** 后台用户管理。 */
|
||||
@Tag(name = "后台-App 用户")
|
||||
@RestController
|
||||
@RequestMapping("/admin/app-users")
|
||||
@RequiredArgsConstructor
|
||||
@@ -42,18 +45,21 @@ public class AppUserAdminController {
|
||||
private final DramaService dramaService;
|
||||
private final EpisodeService episodeService;
|
||||
|
||||
@Operation(summary = "App 用户分页查询")
|
||||
@GetMapping("/page")
|
||||
@PreAuthorize("@ss.hasPermission('app-user:list')")
|
||||
public Result<PageResult<AppUserAdminRespVO>> page(AppUserQueryReqVO req) {
|
||||
return Result.ok(appUserAdminService.page(req));
|
||||
}
|
||||
|
||||
@Operation(summary = "App 用户详情")
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("@ss.hasPermission('app-user:list')")
|
||||
public Result<AppUserAdminRespVO> detail(@PathVariable Long id) {
|
||||
return Result.ok(appUserAdminService.detail(id));
|
||||
}
|
||||
|
||||
@Operation(summary = "封禁 / 处置记录")
|
||||
@GetMapping("/{id}/ban-records")
|
||||
@PreAuthorize("@ss.hasPermission('app-user:list')")
|
||||
public Result<List<AppUserBanRecord>> banRecords(@PathVariable Long id) {
|
||||
@@ -61,6 +67,7 @@ public class AppUserAdminController {
|
||||
}
|
||||
|
||||
/** 观看流水明细:客服核对"用户说看了但没记录"这类问题时用,带上剧名与集号。 */
|
||||
@Operation(summary = "观看流水明细(客服核对用)")
|
||||
@GetMapping("/{id}/watch-history")
|
||||
@PreAuthorize("@ss.hasPermission('app-user:list')")
|
||||
public Result<List<AppUserWatchHistoryVO>> watchHistory(@PathVariable Long id,
|
||||
@@ -108,6 +115,7 @@ public class AppUserAdminController {
|
||||
}
|
||||
|
||||
/** 状态处置:normal/limited/banned。封禁会立即吊销该用户全部会话。 */
|
||||
@Operation(summary = "处置用户状态(封禁会立即吊销全部会话)")
|
||||
@PutMapping("/{id}/status")
|
||||
@PreAuthorize("@ss.hasPermission('app-user:update')")
|
||||
@OperationLog(module = "用户管理", action = "修改用户状态")
|
||||
|
||||
+8
@@ -12,6 +12,8 @@ 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 io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -23,6 +25,7 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Tag(name = "后台-登录与个人")
|
||||
@RestController
|
||||
@RequestMapping("/admin/auth")
|
||||
@RequiredArgsConstructor
|
||||
@@ -30,6 +33,7 @@ public class AuthController {
|
||||
|
||||
private final AuthService authService;
|
||||
|
||||
@Operation(summary = "管理端登录")
|
||||
@PostMapping("/login")
|
||||
@OperationLog(module = "认证", action = "登录")
|
||||
public Result<LoginRespVO> login(@Valid @RequestBody LoginReqVO req, HttpServletRequest request) {
|
||||
@@ -37,23 +41,27 @@ public class AuthController {
|
||||
}
|
||||
|
||||
/** 当前用户的菜单路由树,结构与前端 element-plus-admin 的动态路由一致。 */
|
||||
@Operation(summary = "当前用户菜单路由树")
|
||||
@GetMapping("/menus")
|
||||
public Result<List<MenuVO>> menus() {
|
||||
return Result.ok(authService.listMenus(SecurityUtils.getUserId()));
|
||||
}
|
||||
|
||||
@Operation(summary = "当前用户资料")
|
||||
@GetMapping("/profile")
|
||||
public Result<ProfileRespVO> profile() {
|
||||
return Result.ok(authService.profile(SecurityUtils.getUserId()));
|
||||
}
|
||||
|
||||
/** JWT 无状态,登出由前端清除 token 即可。 */
|
||||
@Operation(summary = "登出")
|
||||
@PostMapping("/logout")
|
||||
@OperationLog(module = "认证", action = "退出登录")
|
||||
public Result<Void> logout() {
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@Operation(summary = "修改自己的登录密码")
|
||||
@PostMapping("/password")
|
||||
@OperationLog(module = "认证", action = "修改密码")
|
||||
public Result<Void> changePassword(@Valid @RequestBody PasswordChangeReqVO req) {
|
||||
|
||||
+10
@@ -9,6 +9,8 @@ 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 io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
@@ -25,6 +27,7 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Tag(name = "后台-轮播图管理")
|
||||
@RestController
|
||||
@RequestMapping("/admin/banners")
|
||||
@RequiredArgsConstructor
|
||||
@@ -32,6 +35,7 @@ public class BannerController {
|
||||
|
||||
private final BannerService bannerService;
|
||||
|
||||
@Operation(summary = "轮播图分页查询")
|
||||
@GetMapping("/page")
|
||||
@PreAuthorize("@ss.hasPermission('banner:list')")
|
||||
public Result<PageResult<BannerRespVO>> page(@RequestParam(required = false) String position,
|
||||
@@ -41,12 +45,14 @@ public class BannerController {
|
||||
return Result.ok(bannerService.page(position, status, pageIndex, pageSize));
|
||||
}
|
||||
|
||||
@Operation(summary = "轮播图详情")
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("@ss.hasPermission('banner:list')")
|
||||
public Result<BannerRespVO> get(@PathVariable Long id) {
|
||||
return Result.ok(bannerService.get(id));
|
||||
}
|
||||
|
||||
@Operation(summary = "新建轮播图")
|
||||
@PostMapping
|
||||
@PreAuthorize("@ss.hasPermission('banner:create')")
|
||||
@OperationLog(module = "内容管理", action = "新增轮播图")
|
||||
@@ -54,6 +60,7 @@ public class BannerController {
|
||||
return Result.ok(bannerService.create(req, SecurityUtils.getUserId()));
|
||||
}
|
||||
|
||||
@Operation(summary = "编辑轮播图")
|
||||
@PutMapping("/{id}")
|
||||
@PreAuthorize("@ss.hasPermission('banner:update')")
|
||||
@OperationLog(module = "内容管理", action = "编辑轮播图")
|
||||
@@ -62,6 +69,7 @@ public class BannerController {
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@Operation(summary = "删除轮播图")
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("@ss.hasPermission('banner:delete')")
|
||||
@OperationLog(module = "内容管理", action = "删除轮播图")
|
||||
@@ -70,6 +78,7 @@ public class BannerController {
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@Operation(summary = "上架 / 下架轮播图")
|
||||
@PutMapping("/{id}/status")
|
||||
@PreAuthorize("@ss.hasPermission('banner:update')")
|
||||
@OperationLog(module = "内容管理", action = "修改轮播图状态")
|
||||
@@ -78,6 +87,7 @@ public class BannerController {
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@Operation(summary = "调整轮播排序")
|
||||
@PutMapping("/sort")
|
||||
@PreAuthorize("@ss.hasPermission('banner:update')")
|
||||
public Result<Void> sort(@RequestBody Map<String, List<Long>> body) {
|
||||
|
||||
+7
@@ -11,6 +11,8 @@ import com.fruitshorts.interaction.service.CommentService;
|
||||
import com.fruitshorts.interaction.vo.CommentAuditReqVO;
|
||||
import com.fruitshorts.interaction.vo.CommentQueryReqVO;
|
||||
import com.fruitshorts.interaction.vo.CommentRespVO;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
@@ -25,6 +27,7 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
import java.util.List;
|
||||
|
||||
/** 后台评论审核。 */
|
||||
@Tag(name = "后台-评论审核")
|
||||
@RestController
|
||||
@RequestMapping("/admin/comments")
|
||||
@RequiredArgsConstructor
|
||||
@@ -33,6 +36,7 @@ public class CommentAdminController {
|
||||
private final CommentService commentService;
|
||||
|
||||
/** 检索评论:默认按状态过滤,`status=pending` 即审核队列。 */
|
||||
@Operation(summary = "评论检索(默认按状态过滤,待审队列)")
|
||||
@GetMapping("/page")
|
||||
@PreAuthorize("@ss.hasPermission('comment:list')")
|
||||
public Result<PageResult<CommentRespVO>> page(CommentQueryReqVO req) {
|
||||
@@ -40,12 +44,14 @@ public class CommentAdminController {
|
||||
}
|
||||
|
||||
/** 审核留证:机审与人审的完整链路。 */
|
||||
@Operation(summary = "评论审核留证(机审 + 人审链路)")
|
||||
@GetMapping("/{id}/audit-records")
|
||||
@PreAuthorize("@ss.hasPermission('comment:list')")
|
||||
public Result<List<ContentAuditRecord>> auditRecords(@PathVariable Long id) {
|
||||
return Result.ok(commentService.auditRecords(id));
|
||||
}
|
||||
|
||||
@Operation(summary = "审核评论(通过 / 驳回)")
|
||||
@PutMapping("/{id}/audit")
|
||||
@PreAuthorize("@ss.hasPermission('comment:audit')")
|
||||
@OperationLog(module = "内容治理", action = "审核评论")
|
||||
@@ -54,6 +60,7 @@ public class CommentAdminController {
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@Operation(summary = "删除评论")
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("@ss.hasPermission('comment:audit')")
|
||||
@OperationLog(module = "内容治理", action = "删除评论")
|
||||
|
||||
+5
@@ -6,6 +6,8 @@ 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 io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -17,6 +19,7 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Tag(name = "后台-系统配置")
|
||||
@RestController
|
||||
@RequestMapping("/admin/configs")
|
||||
@RequiredArgsConstructor
|
||||
@@ -24,12 +27,14 @@ public class ConfigController {
|
||||
|
||||
private final ConfigService configService;
|
||||
|
||||
@Operation(summary = "系统配置列表")
|
||||
@GetMapping
|
||||
@PreAuthorize("@ss.hasAnyPermission('system:role','system:menu')")
|
||||
public Result<List<ConfigItemVO>> list() {
|
||||
return Result.ok(configService.list());
|
||||
}
|
||||
|
||||
@Operation(summary = "保存系统配置")
|
||||
@PutMapping
|
||||
@PreAuthorize("@ss.hasPermission('system:role')")
|
||||
@OperationLog(module = "后台管理", action = "修改系统设置")
|
||||
|
||||
+10
@@ -15,6 +15,8 @@ import com.fruitshorts.creator.vo.CreatorAuditReqVO;
|
||||
import com.fruitshorts.creator.vo.CreatorQueryReqVO;
|
||||
import com.fruitshorts.framework.log.OperationLog;
|
||||
import com.fruitshorts.framework.security.SecurityUtils;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
@@ -29,6 +31,7 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
import java.util.List;
|
||||
|
||||
/** 后台创作者:检索、详情、资质、入驻审核。 */
|
||||
@Tag(name = "后台-创作者管理")
|
||||
@RestController
|
||||
@RequestMapping("/admin/creators")
|
||||
@RequiredArgsConstructor
|
||||
@@ -37,12 +40,14 @@ public class CreatorAdminController {
|
||||
private final CreatorService creatorService;
|
||||
private final CreatorAccountService creatorAccountService;
|
||||
|
||||
@Operation(summary = "创作者分页查询")
|
||||
@GetMapping("/page")
|
||||
@PreAuthorize("@ss.hasPermission('creator:list')")
|
||||
public Result<PageResult<Creator>> page(CreatorQueryReqVO req) {
|
||||
return Result.ok(creatorService.page(req));
|
||||
}
|
||||
|
||||
@Operation(summary = "创作者详情")
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("@ss.hasPermission('creator:list')")
|
||||
public Result<Creator> detail(@PathVariable Long id) {
|
||||
@@ -50,12 +55,14 @@ public class CreatorAdminController {
|
||||
}
|
||||
|
||||
/** 资质文件:审核前要看证件原件。 */
|
||||
@Operation(summary = "创作者资质文件")
|
||||
@GetMapping("/{id}/documents")
|
||||
@PreAuthorize("@ss.hasPermission('creator:list')")
|
||||
public Result<List<CreatorDocument>> documents(@PathVariable Long id) {
|
||||
return Result.ok(creatorService.documents(id));
|
||||
}
|
||||
|
||||
@Operation(summary = "创作者入驻审核")
|
||||
@PutMapping("/{id}/audit")
|
||||
@PreAuthorize("@ss.hasPermission('creator:audit')")
|
||||
@OperationLog(module = "创作者管理", action = "入驻审核")
|
||||
@@ -64,6 +71,7 @@ public class CreatorAdminController {
|
||||
}
|
||||
|
||||
/** 手工录入创作者主体(C 端申请入驻还没实现,先用它打通链路)。 */
|
||||
@Operation(summary = "手工录入创作者主体")
|
||||
@PostMapping
|
||||
@PreAuthorize("@ss.hasPermission('creator:audit')")
|
||||
@OperationLog(module = "创作者管理", action = "新增创作者")
|
||||
@@ -72,6 +80,7 @@ public class CreatorAdminController {
|
||||
}
|
||||
|
||||
/** 开通创作者账号:仅审核通过可开;已开通则幂等返回,不重复建号。 */
|
||||
@Operation(summary = "开通创作者账号(仅审核通过可开通,幂等)")
|
||||
@PostMapping("/{id}/account")
|
||||
@PreAuthorize("@ss.hasPermission('creator:account')")
|
||||
@OperationLog(module = "创作者管理", action = "开通创作者账号")
|
||||
@@ -81,6 +90,7 @@ public class CreatorAdminController {
|
||||
}
|
||||
|
||||
/** 重置创作者账号密码:一次性明文返回,旧密码立即失效。 */
|
||||
@Operation(summary = "重置创作者账号密码(一次性明文返回)")
|
||||
@PostMapping("/{id}/account/reset-password")
|
||||
@PreAuthorize("@ss.hasPermission('creator:account')")
|
||||
@OperationLog(module = "创作者管理", action = "重置创作者密码")
|
||||
|
||||
+9
@@ -11,6 +11,8 @@ import com.fruitshorts.creator.vo.CreatorCenterNoticeVO;
|
||||
import com.fruitshorts.creator.vo.CreatorCenterProfileVO;
|
||||
import com.fruitshorts.creator.vo.CreatorCenterTargetVO;
|
||||
import com.fruitshorts.admin.service.CreatorDashboardService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -28,6 +30,7 @@ import java.util.Map;
|
||||
* 归属由 {@link CreatorCenterService#requireCurrentCreator()} 从登录态解析,
|
||||
* 查询强制带 `where creator_id = 当前创作者`;访问他人作品统一 403。
|
||||
*/
|
||||
@Tag(name = "创作者中心")
|
||||
@RestController
|
||||
@RequestMapping("/admin/creator-center")
|
||||
@RequiredArgsConstructor
|
||||
@@ -37,6 +40,7 @@ public class CreatorCenterController {
|
||||
private final CreatorDashboardService creatorDashboardService;
|
||||
|
||||
/** 当前创作者的主体信息与入驻状态。 */
|
||||
@Operation(summary = "我的创作者资料与入驻状态")
|
||||
@GetMapping("/profile")
|
||||
@PreAuthorize("@ss.hasPermission('creator-center:view')")
|
||||
public Result<CreatorCenterProfileVO> profile() {
|
||||
@@ -44,6 +48,7 @@ public class CreatorCenterController {
|
||||
}
|
||||
|
||||
/** 我的作品列表(含最近一次提交的审核进度)。 */
|
||||
@Operation(summary = "我的作品列表(含最近一次审核进度)")
|
||||
@GetMapping("/dramas/page")
|
||||
@PreAuthorize("@ss.hasPermission('creator-center:view')")
|
||||
public Result<PageResult<CreatorCenterDramaVO>> pageMyDramas(CreatorCenterDramaQueryReqVO req) {
|
||||
@@ -51,6 +56,7 @@ public class CreatorCenterController {
|
||||
}
|
||||
|
||||
/** 我的作品详情:id 是别人的作品时返回 403,不区分"不存在"。 */
|
||||
@Operation(summary = "我的作品详情")
|
||||
@GetMapping("/dramas/{id}")
|
||||
@PreAuthorize("@ss.hasPermission('creator-center:view')")
|
||||
public Result<CreatorCenterDramaVO> myDrama(@PathVariable Long id) {
|
||||
@@ -58,6 +64,7 @@ public class CreatorCenterController {
|
||||
}
|
||||
|
||||
/** 我的通知:入驻审核结果 + 我提交内容的审核结果。 */
|
||||
@Operation(summary = "我的通知(入驻与内容审核结果)")
|
||||
@GetMapping("/notices")
|
||||
@PreAuthorize("@ss.hasPermission('creator-center:view')")
|
||||
public Result<List<CreatorCenterNoticeVO>> notices() {
|
||||
@@ -65,6 +72,7 @@ public class CreatorCenterController {
|
||||
}
|
||||
|
||||
/** 我的数据看板:播放 / 完播 / 收藏 / 评论 / 解锁收入概览(只读,只含自己的作品)。 */
|
||||
@Operation(summary = "我的数据看板(播放 / 完播 / 收藏 / 评论 / 解锁收入)")
|
||||
@GetMapping("/dashboard")
|
||||
@PreAuthorize("@ss.hasPermission('creator-center:stat:view')")
|
||||
public Result<Map<String, Object>> dashboard() {
|
||||
@@ -75,6 +83,7 @@ public class CreatorCenterController {
|
||||
* 代看目标下拉的数据源:只有带 `creator-center:impersonate` 的账号(第一阶段 = 超管)能调。
|
||||
* 前端拿它渲染顶部的「当前创作者」下拉;没权限的账号不显示下拉,也不会请求这个接口。
|
||||
*/
|
||||
@Operation(summary = "代看可选创作者列表")
|
||||
@GetMapping("/impersonation/targets")
|
||||
@PreAuthorize("@ss.hasPermission('creator-center:impersonate')")
|
||||
public Result<List<CreatorCenterTargetVO>> impersonationTargets() {
|
||||
|
||||
+16
@@ -14,6 +14,8 @@ import com.fruitshorts.creator.vo.CreatorDramaEditReqVO;
|
||||
import com.fruitshorts.creator.vo.CreatorEpisodeReqVO;
|
||||
import com.fruitshorts.creator.vo.CreatorSubmitReqVO;
|
||||
import com.fruitshorts.framework.log.OperationLog;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
@@ -34,6 +36,7 @@ import java.util.List;
|
||||
* 与 {@link CreatorCenterController} 一样**不接收 creatorId**:归属由登录态解析,
|
||||
* 服务层强制 `where creator_id = 当前创作者`,改别人的剧或分集一律 403。
|
||||
*/
|
||||
@Tag(name = "创作者中心-内容与收费")
|
||||
@RestController
|
||||
@RequestMapping("/admin/creator-center")
|
||||
@RequiredArgsConstructor
|
||||
@@ -42,6 +45,7 @@ public class CreatorContentController {
|
||||
private final CreatorContentService creatorContentService;
|
||||
|
||||
/** 新建草稿。 */
|
||||
@Operation(summary = "新建作品草稿")
|
||||
@PostMapping("/dramas")
|
||||
@PreAuthorize("@ss.hasPermission('creator-center:drama:edit')")
|
||||
@OperationLog(module = "创作者中心", action = "新建作品")
|
||||
@@ -50,6 +54,7 @@ public class CreatorContentController {
|
||||
}
|
||||
|
||||
/** 编辑作品信息(仅草稿 / 已下线可改)。 */
|
||||
@Operation(summary = "编辑作品信息(仅草稿 / 已下线可改)")
|
||||
@PutMapping("/dramas/{id}")
|
||||
@PreAuthorize("@ss.hasPermission('creator-center:drama:edit')")
|
||||
@OperationLog(module = "创作者中心", action = "编辑作品")
|
||||
@@ -59,6 +64,7 @@ public class CreatorContentController {
|
||||
}
|
||||
|
||||
/** 分集列表。 */
|
||||
@Operation(summary = "分集列表")
|
||||
@GetMapping("/dramas/{id}/episodes")
|
||||
@PreAuthorize("@ss.hasPermission('creator-center:view')")
|
||||
public Result<List<EpisodeRespVO>> listEpisodes(@PathVariable Long id) {
|
||||
@@ -66,6 +72,7 @@ public class CreatorContentController {
|
||||
}
|
||||
|
||||
/** 新增分集(集号不传自动排到最后)。 */
|
||||
@Operation(summary = "新增分集")
|
||||
@PostMapping("/dramas/{id}/episodes")
|
||||
@PreAuthorize("@ss.hasPermission('creator-center:drama:edit')")
|
||||
@OperationLog(module = "创作者中心", action = "新增分集")
|
||||
@@ -74,6 +81,7 @@ public class CreatorContentController {
|
||||
}
|
||||
|
||||
/** 编辑分集(时长 / 封面 / 片源 / 排序)。 */
|
||||
@Operation(summary = "编辑分集(时长 / 封面 / 片源)")
|
||||
@PutMapping("/dramas/{id}/episodes/{episodeId}")
|
||||
@PreAuthorize("@ss.hasPermission('creator-center:drama:edit')")
|
||||
@OperationLog(module = "创作者中心", action = "编辑分集")
|
||||
@@ -84,6 +92,7 @@ public class CreatorContentController {
|
||||
}
|
||||
|
||||
/** 删除分集。 */
|
||||
@Operation(summary = "删除分集")
|
||||
@DeleteMapping("/dramas/{id}/episodes/{episodeId}")
|
||||
@PreAuthorize("@ss.hasPermission('creator-center:drama:edit')")
|
||||
@OperationLog(module = "创作者中心", action = "删除分集")
|
||||
@@ -93,6 +102,7 @@ public class CreatorContentController {
|
||||
}
|
||||
|
||||
/** 批量排序:body 里按新顺序给出分集 id。 */
|
||||
@Operation(summary = "分集批量排序")
|
||||
@PutMapping("/dramas/{id}/episodes/sort")
|
||||
@PreAuthorize("@ss.hasPermission('creator-center:drama:edit')")
|
||||
@OperationLog(module = "创作者中心", action = "调整分集顺序")
|
||||
@@ -102,6 +112,7 @@ public class CreatorContentController {
|
||||
}
|
||||
|
||||
/** 提交审核:CREATE / UPDATE / PUBLISH / OFFLINE。 */
|
||||
@Operation(summary = "提交审核(CREATE / UPDATE / PUBLISH / OFFLINE)")
|
||||
@PostMapping("/dramas/{id}/submit")
|
||||
@PreAuthorize("@ss.hasPermission('creator-center:drama:edit')")
|
||||
@OperationLog(module = "创作者中心", action = "提交审核")
|
||||
@@ -110,6 +121,7 @@ public class CreatorContentController {
|
||||
}
|
||||
|
||||
/** 撤回自己的提交(撤回后作品退回草稿,可继续编辑)。 */
|
||||
@Operation(summary = "撤回自己的提交")
|
||||
@PostMapping("/submissions/{id}/withdraw")
|
||||
@PreAuthorize("@ss.hasPermission('creator-center:drama:edit')")
|
||||
@OperationLog(module = "创作者中心", action = "撤回提交")
|
||||
@@ -118,6 +130,7 @@ public class CreatorContentController {
|
||||
}
|
||||
|
||||
/** 我的提交历史(审核进度)。 */
|
||||
@Operation(summary = "我的提交历史")
|
||||
@GetMapping("/dramas/{id}/submissions")
|
||||
@PreAuthorize("@ss.hasPermission('creator-center:view')")
|
||||
public Result<List<ContentSubmission>> listSubmissions(@PathVariable Long id) {
|
||||
@@ -125,6 +138,7 @@ public class CreatorContentController {
|
||||
}
|
||||
|
||||
/** 把上传好的视频产物挂到自己的分集上(产物必须是本人上传的)。 */
|
||||
@Operation(summary = "把上传产物挂到自己的分集上")
|
||||
@PostMapping("/dramas/{id}/episodes/{episodeId}/attach")
|
||||
@PreAuthorize("@ss.hasPermission('creator-center:upload')")
|
||||
@OperationLog(module = "创作者中心", action = "挂载分集视频")
|
||||
@@ -135,6 +149,7 @@ public class CreatorContentController {
|
||||
}
|
||||
|
||||
/** 分集收费与试看秒数:读现状。 */
|
||||
@Operation(summary = "分集收费与试看秒数(读)")
|
||||
@GetMapping("/dramas/{id}/prices")
|
||||
@PreAuthorize("@ss.hasPermission('creator-center:view')")
|
||||
public Result<PriceRespVO> priceDetail(@PathVariable Long id) {
|
||||
@@ -142,6 +157,7 @@ public class CreatorContentController {
|
||||
}
|
||||
|
||||
/** 分集收费与试看秒数:保存(复用后台同一套 PriceService,改完立即影响付费判定)。 */
|
||||
@Operation(summary = "分集收费与试看秒数(保存)")
|
||||
@PutMapping("/dramas/{id}/prices")
|
||||
@PreAuthorize("@ss.hasPermission('creator-center:price:update')")
|
||||
@OperationLog(module = "创作者中心", action = "配置分集价格")
|
||||
|
||||
+5
@@ -4,6 +4,8 @@ package com.fruitshorts.admin.controller;
|
||||
|
||||
import com.fruitshorts.admin.service.DashboardService;
|
||||
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.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -18,6 +20,7 @@ import java.util.Map;
|
||||
* 指标口径写在 DashboardService 的类注释与 docs/api-phase1.md 里;
|
||||
* 取不到数据源的指标返回 null 并在 notes 里说明,不用 0 冒充。
|
||||
*/
|
||||
@Tag(name = "后台-数据看板")
|
||||
@RestController
|
||||
@RequestMapping("/admin/dashboard")
|
||||
@RequiredArgsConstructor
|
||||
@@ -26,6 +29,7 @@ public class DashboardAdminController {
|
||||
private final DashboardService dashboardService;
|
||||
|
||||
/** 总览:用户 / 内容 / 播放 / 收入 / 成本。 */
|
||||
@Operation(summary = "运营大盘总览(用户 / 内容 / 播放 / 收入 / 成本)")
|
||||
@GetMapping("/overview")
|
||||
@PreAuthorize("@ss.hasPermission('dashboard:view')")
|
||||
public Result<Map<String, Object>> overview() {
|
||||
@@ -33,6 +37,7 @@ public class DashboardAdminController {
|
||||
}
|
||||
|
||||
/** 转码看板:队列积压与失败任务明细。 */
|
||||
@Operation(summary = "转码看板(队列积压与失败任务)")
|
||||
@GetMapping("/transcode")
|
||||
@PreAuthorize("@ss.hasPermission('dashboard:view')")
|
||||
public Result<Map<String, Object>> transcode() {
|
||||
|
||||
+5
@@ -6,6 +6,8 @@ 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 io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -19,6 +21,7 @@ import java.util.Map;
|
||||
/**
|
||||
* 字典接口:题材、内容状态等下拉数据。
|
||||
*/
|
||||
@Tag(name = "后台-字典")
|
||||
@RestController
|
||||
@RequestMapping("/admin/dicts")
|
||||
@RequiredArgsConstructor
|
||||
@@ -26,12 +29,14 @@ public class DictController {
|
||||
|
||||
private final DramaCategoryService dramaCategoryService;
|
||||
|
||||
@Operation(summary = "短剧分类字典")
|
||||
@GetMapping("/drama-category")
|
||||
@PreAuthorize("@ss.hasPermission('drama:list')")
|
||||
public Result<List<DramaCategoryRespVO>> dramaCategories() {
|
||||
return Result.ok(dramaCategoryService.list());
|
||||
}
|
||||
|
||||
@Operation(summary = "内容状态字典")
|
||||
@GetMapping("/content-status")
|
||||
@PreAuthorize("@ss.hasPermission('drama:list')")
|
||||
public Result<List<Map<String, String>>> contentStatus() {
|
||||
|
||||
+7
@@ -8,6 +8,8 @@ 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 io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -20,6 +22,7 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
import java.util.Map;
|
||||
|
||||
/** 后台下载授权管理。 */
|
||||
@Tag(name = "后台-离线下载管理")
|
||||
@RestController
|
||||
@RequestMapping("/admin/downloads")
|
||||
@RequiredArgsConstructor
|
||||
@@ -27,12 +30,14 @@ public class DownloadAdminController {
|
||||
|
||||
private final DownloadService downloadService;
|
||||
|
||||
@Operation(summary = "下载授权分页查询")
|
||||
@GetMapping("/page")
|
||||
@PreAuthorize("@ss.hasPermission('download:list')")
|
||||
public Result<PageResult<DownloadLicenseRespVO>> page(DownloadQueryReqVO req) {
|
||||
return Result.ok(downloadService.page(req));
|
||||
}
|
||||
|
||||
@Operation(summary = "下载统计")
|
||||
@GetMapping("/stats")
|
||||
@PreAuthorize("@ss.hasPermission('download:list')")
|
||||
public Result<Map<String, Object>> stats() {
|
||||
@@ -40,6 +45,7 @@ public class DownloadAdminController {
|
||||
}
|
||||
|
||||
/** 吊销单条授权:用户侧下次联网即失效。 */
|
||||
@Operation(summary = "吊销单条下载授权")
|
||||
@PutMapping("/{id}/revoke")
|
||||
@PreAuthorize("@ss.hasPermission('download:revoke')")
|
||||
@OperationLog(module = "下载管理", action = "吊销下载授权")
|
||||
@@ -49,6 +55,7 @@ public class DownloadAdminController {
|
||||
}
|
||||
|
||||
/** 手动触发过期清理,正常情况下由定时任务每天执行。 */
|
||||
@Operation(summary = "手动触发过期授权清理")
|
||||
@PostMapping("/cleanup")
|
||||
@PreAuthorize("@ss.hasPermission('download:revoke')")
|
||||
@OperationLog(module = "下载管理", action = "清理过期下载授权")
|
||||
|
||||
+7
@@ -8,6 +8,8 @@ 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 io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
@@ -25,6 +27,7 @@ import java.util.List;
|
||||
/**
|
||||
* 短剧类型管理。类型属于短剧域,权限沿用 drama:*,不再单开权限点。
|
||||
*/
|
||||
@Tag(name = "后台-短剧分类")
|
||||
@RestController
|
||||
@RequestMapping("/admin/drama-categories")
|
||||
@RequiredArgsConstructor
|
||||
@@ -32,12 +35,14 @@ public class DramaCategoryController {
|
||||
|
||||
private final DramaCategoryService dramaCategoryService;
|
||||
|
||||
@Operation(summary = "分类列表")
|
||||
@GetMapping
|
||||
@PreAuthorize("@ss.hasPermission('drama:list')")
|
||||
public Result<List<DramaCategoryRespVO>> list() {
|
||||
return Result.ok(dramaCategoryService.list());
|
||||
}
|
||||
|
||||
@Operation(summary = "新建分类")
|
||||
@PostMapping
|
||||
@PreAuthorize("@ss.hasPermission('drama:create')")
|
||||
@OperationLog(module = "内容管理", action = "新增短剧类型")
|
||||
@@ -45,6 +50,7 @@ public class DramaCategoryController {
|
||||
return Result.ok(dramaCategoryService.create(req));
|
||||
}
|
||||
|
||||
@Operation(summary = "编辑分类")
|
||||
@PutMapping("/{id}")
|
||||
@PreAuthorize("@ss.hasPermission('drama:update')")
|
||||
@OperationLog(module = "内容管理", action = "编辑短剧类型")
|
||||
@@ -53,6 +59,7 @@ public class DramaCategoryController {
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@Operation(summary = "删除分类")
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("@ss.hasPermission('drama:delete')")
|
||||
@OperationLog(module = "内容管理", action = "删除短剧类型")
|
||||
|
||||
+14
@@ -12,6 +12,8 @@ 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 io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
@@ -26,6 +28,7 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Tag(name = "后台-短剧管理")
|
||||
@RestController
|
||||
@RequestMapping("/admin/dramas")
|
||||
@RequiredArgsConstructor
|
||||
@@ -33,18 +36,21 @@ public class DramaController {
|
||||
|
||||
private final DramaService dramaService;
|
||||
|
||||
@Operation(summary = "短剧分页查询")
|
||||
@GetMapping("/page")
|
||||
@PreAuthorize("@ss.hasPermission('drama:list')")
|
||||
public Result<PageResult<DramaRespVO>> page(DramaPageReqVO req) {
|
||||
return Result.ok(dramaService.page(req));
|
||||
}
|
||||
|
||||
@Operation(summary = "短剧详情")
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("@ss.hasPermission('drama:list')")
|
||||
public Result<DramaRespVO> get(@PathVariable Long id) {
|
||||
return Result.ok(dramaService.get(id));
|
||||
}
|
||||
|
||||
@Operation(summary = "新建短剧")
|
||||
@PostMapping
|
||||
@PreAuthorize("@ss.hasPermission('drama:create')")
|
||||
@OperationLog(module = "内容管理", action = "新建短剧")
|
||||
@@ -52,6 +58,7 @@ public class DramaController {
|
||||
return Result.ok(dramaService.create(req, SecurityUtils.getUserId()));
|
||||
}
|
||||
|
||||
@Operation(summary = "编辑短剧")
|
||||
@PutMapping("/{id}")
|
||||
@PreAuthorize("@ss.hasPermission('drama:update')")
|
||||
@OperationLog(module = "内容管理", action = "编辑短剧")
|
||||
@@ -60,6 +67,7 @@ public class DramaController {
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@Operation(summary = "删除短剧")
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("@ss.hasPermission('drama:delete')")
|
||||
@OperationLog(module = "内容管理", action = "删除短剧")
|
||||
@@ -69,6 +77,7 @@ public class DramaController {
|
||||
}
|
||||
|
||||
/** 提交审核:内容运营即可操作。 */
|
||||
@Operation(summary = "提交审核")
|
||||
@PostMapping("/{id}/submit")
|
||||
@PreAuthorize("@ss.hasAnyPermission('drama:review','drama:update')")
|
||||
@OperationLog(module = "内容管理", action = "提交审核")
|
||||
@@ -77,6 +86,7 @@ public class DramaController {
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@Operation(summary = "审核通过")
|
||||
@PostMapping("/{id}/approve")
|
||||
@PreAuthorize("@ss.hasPermission('drama:review')")
|
||||
@OperationLog(module = "内容管理", action = "审核通过")
|
||||
@@ -85,6 +95,7 @@ public class DramaController {
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@Operation(summary = "审核驳回")
|
||||
@PostMapping("/{id}/reject")
|
||||
@PreAuthorize("@ss.hasPermission('drama:review')")
|
||||
@OperationLog(module = "内容管理", action = "驳回短剧")
|
||||
@@ -93,6 +104,7 @@ public class DramaController {
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@Operation(summary = "上线")
|
||||
@PostMapping("/{id}/publish")
|
||||
@PreAuthorize("@ss.hasPermission('drama:review')")
|
||||
@OperationLog(module = "内容管理", action = "短剧上线")
|
||||
@@ -101,6 +113,7 @@ public class DramaController {
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@Operation(summary = "下线")
|
||||
@PostMapping("/{id}/offline")
|
||||
@PreAuthorize("@ss.hasPermission('drama:review')")
|
||||
@OperationLog(module = "内容管理", action = "短剧下线")
|
||||
@@ -109,6 +122,7 @@ public class DramaController {
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@Operation(summary = "审核记录")
|
||||
@GetMapping("/{id}/review-records")
|
||||
@PreAuthorize("@ss.hasPermission('drama:list')")
|
||||
public Result<List<DramaReviewRecord>> reviewRecords(@PathVariable Long id) {
|
||||
|
||||
+6
@@ -8,6 +8,8 @@ import com.fruitshorts.commerce.vo.EntitlementGrantReqVO;
|
||||
import com.fruitshorts.common.pojo.PageResult;
|
||||
import com.fruitshorts.common.pojo.Result;
|
||||
import com.fruitshorts.framework.log.OperationLog;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
@@ -21,6 +23,7 @@ import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/** 后台权益:检索、人工补发、回收。 */
|
||||
@Tag(name = "后台-权益管理")
|
||||
@RestController
|
||||
@RequestMapping("/admin/entitlements")
|
||||
@RequiredArgsConstructor
|
||||
@@ -28,6 +31,7 @@ public class EntitlementAdminController {
|
||||
|
||||
private final EntitlementService entitlementService;
|
||||
|
||||
@Operation(summary = "权益分页查询")
|
||||
@GetMapping("/page")
|
||||
@PreAuthorize("@ss.hasPermission('entitlement:list')")
|
||||
public Result<PageResult<Entitlement>> page(@RequestParam(required = false) Long userId,
|
||||
@@ -39,6 +43,7 @@ public class EntitlementAdminController {
|
||||
}
|
||||
|
||||
/** 客服补偿:直接给用户某集/整剧的播放权益。 */
|
||||
@Operation(summary = "补发权益(客服补偿)")
|
||||
@PostMapping
|
||||
@PreAuthorize("@ss.hasPermission('entitlement:update')")
|
||||
@OperationLog(module = "订单管理", action = "人工补发权益")
|
||||
@@ -50,6 +55,7 @@ public class EntitlementAdminController {
|
||||
}
|
||||
|
||||
/** 回收权益:退款、违规处置后用。 */
|
||||
@Operation(summary = "回收权益(退款 / 违规处置)")
|
||||
@PutMapping("/{id}/revoke")
|
||||
@PreAuthorize("@ss.hasPermission('entitlement:update')")
|
||||
@OperationLog(module = "订单管理", action = "回收权益")
|
||||
|
||||
+8
@@ -8,6 +8,8 @@ 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 io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
@@ -23,6 +25,7 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Tag(name = "后台-剧集管理")
|
||||
@RestController
|
||||
@RequestMapping("/admin/episodes")
|
||||
@RequiredArgsConstructor
|
||||
@@ -30,6 +33,7 @@ public class EpisodeController {
|
||||
|
||||
private final EpisodeService episodeService;
|
||||
|
||||
@Operation(summary = "剧集列表")
|
||||
@GetMapping
|
||||
@PreAuthorize("@ss.hasPermission('episode:list')")
|
||||
public Result<List<EpisodeRespVO>> list(@RequestParam Long dramaId,
|
||||
@@ -37,6 +41,7 @@ public class EpisodeController {
|
||||
return Result.ok(episodeService.listByDrama(dramaId, status));
|
||||
}
|
||||
|
||||
@Operation(summary = "新建剧集")
|
||||
@PostMapping
|
||||
@PreAuthorize("@ss.hasPermission('episode:upload')")
|
||||
@OperationLog(module = "内容管理", action = "新增剧集")
|
||||
@@ -44,6 +49,7 @@ public class EpisodeController {
|
||||
return Result.ok(episodeService.create(req));
|
||||
}
|
||||
|
||||
@Operation(summary = "编辑剧集")
|
||||
@PutMapping("/{id}")
|
||||
@PreAuthorize("@ss.hasPermission('episode:upload')")
|
||||
@OperationLog(module = "内容管理", action = "编辑剧集")
|
||||
@@ -52,6 +58,7 @@ public class EpisodeController {
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@Operation(summary = "删除剧集")
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("@ss.hasPermission('episode:upload')")
|
||||
@OperationLog(module = "内容管理", action = "删除剧集")
|
||||
@@ -60,6 +67,7 @@ public class EpisodeController {
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@Operation(summary = "剧集排序")
|
||||
@PutMapping("/sort")
|
||||
@PreAuthorize("@ss.hasPermission('episode:sort')")
|
||||
@OperationLog(module = "内容管理", action = "调整剧集顺序")
|
||||
|
||||
+8
@@ -11,6 +11,8 @@ import com.fruitshorts.media.vo.UploadCompleteReqVO;
|
||||
import com.fruitshorts.media.vo.UploadInitReqVO;
|
||||
import com.fruitshorts.media.vo.UploadInitRespVO;
|
||||
import com.fruitshorts.media.vo.UploadProgressRespVO;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
@@ -26,6 +28,7 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
/**
|
||||
* 媒资上传(本地存储版):init 建会话 → chunk 传分片 → complete 合并出地址。
|
||||
*/
|
||||
@Tag(name = "后台-媒资管理")
|
||||
@RestController
|
||||
@RequestMapping("/admin/media")
|
||||
@RequiredArgsConstructor
|
||||
@@ -33,6 +36,7 @@ public class MediaController {
|
||||
|
||||
private final MediaService mediaService;
|
||||
|
||||
@Operation(summary = "初始化分片上传")
|
||||
@PostMapping("/upload/init")
|
||||
@PreAuthorize("@ss.hasAnyPermission('cover:upload','episode:upload')")
|
||||
public Result<UploadInitRespVO> init(@Valid @RequestBody UploadInitReqVO req) {
|
||||
@@ -41,6 +45,7 @@ public class MediaController {
|
||||
return Result.ok(mediaService.init(req, SecurityUtils.getUserId()));
|
||||
}
|
||||
|
||||
@Operation(summary = "上传分片")
|
||||
@PostMapping("/upload/chunk")
|
||||
@PreAuthorize("@ss.hasAnyPermission('cover:upload','episode:upload')")
|
||||
public Result<Integer> uploadChunk(@RequestParam String uploadId,
|
||||
@@ -51,6 +56,7 @@ public class MediaController {
|
||||
return Result.ok(index);
|
||||
}
|
||||
|
||||
@Operation(summary = "完成上传")
|
||||
@PostMapping("/upload/complete")
|
||||
@PreAuthorize("@ss.hasAnyPermission('cover:upload','episode:upload')")
|
||||
public Result<MediaAssetRespVO> complete(@Valid @RequestBody UploadCompleteReqVO req) {
|
||||
@@ -59,12 +65,14 @@ public class MediaController {
|
||||
}
|
||||
|
||||
/** 断点续传:查上传会话进度(只能查自己发起的会话)。 */
|
||||
@Operation(summary = "查询上传进度(断点续传)")
|
||||
@GetMapping("/upload/{uploadId}")
|
||||
@PreAuthorize("@ss.hasAnyPermission('cover:upload','episode:upload')")
|
||||
public Result<UploadProgressRespVO> progress(@PathVariable String uploadId) {
|
||||
return Result.ok(mediaService.progress(uploadId, SecurityUtils.getUserId()));
|
||||
}
|
||||
|
||||
@Operation(summary = "媒资详情")
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("@ss.hasAnyPermission('cover:upload','episode:upload')")
|
||||
public Result<MediaAssetRespVO> get(@PathVariable Long id) {
|
||||
|
||||
+7
@@ -7,6 +7,8 @@ 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 io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
@@ -21,6 +23,7 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Tag(name = "后台-菜单管理")
|
||||
@RestController
|
||||
@RequestMapping("/admin/menus")
|
||||
@RequiredArgsConstructor
|
||||
@@ -28,12 +31,14 @@ public class MenuController {
|
||||
|
||||
private final MenuService menuService;
|
||||
|
||||
@Operation(summary = "菜单树")
|
||||
@GetMapping("/tree")
|
||||
@PreAuthorize("@ss.hasPermission('system:menu')")
|
||||
public Result<List<MenuVO>> tree() {
|
||||
return Result.ok(menuService.listAll());
|
||||
}
|
||||
|
||||
@Operation(summary = "新建菜单")
|
||||
@PostMapping
|
||||
@PreAuthorize("@ss.hasPermission('system:menu')")
|
||||
@OperationLog(module = "后台管理", action = "新增菜单")
|
||||
@@ -41,6 +46,7 @@ public class MenuController {
|
||||
return Result.ok(menuService.create(req));
|
||||
}
|
||||
|
||||
@Operation(summary = "编辑菜单")
|
||||
@PutMapping("/{id}")
|
||||
@PreAuthorize("@ss.hasPermission('system:menu')")
|
||||
@OperationLog(module = "后台管理", action = "编辑菜单")
|
||||
@@ -49,6 +55,7 @@ public class MenuController {
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@Operation(summary = "删除菜单")
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("@ss.hasPermission('system:menu')")
|
||||
@OperationLog(module = "后台管理", action = "删除菜单")
|
||||
|
||||
+5
@@ -7,6 +7,8 @@ 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 io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -14,6 +16,7 @@ import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@Tag(name = "后台-操作日志")
|
||||
@RestController
|
||||
@RequestMapping("/admin/operation-logs")
|
||||
@RequiredArgsConstructor
|
||||
@@ -21,12 +24,14 @@ public class OperationLogController {
|
||||
|
||||
private final OperationLogService operationLogService;
|
||||
|
||||
@Operation(summary = "操作日志分页查询")
|
||||
@GetMapping("/page")
|
||||
@PreAuthorize("@ss.hasPermission('log:list')")
|
||||
public Result<PageResult<OperationLogRespVO>> page(OperationLogPageReqVO req) {
|
||||
return Result.ok(operationLogService.page(req));
|
||||
}
|
||||
|
||||
@Operation(summary = "操作日志详情")
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("@ss.hasPermission('log:list')")
|
||||
public Result<OperationLogRespVO> get(@PathVariable Long id) {
|
||||
|
||||
+8
@@ -10,6 +10,8 @@ import com.fruitshorts.common.pojo.PageResult;
|
||||
import com.fruitshorts.common.pojo.Result;
|
||||
import com.fruitshorts.framework.log.OperationLog;
|
||||
import com.fruitshorts.framework.security.SecurityUtils;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -24,6 +26,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/** 后台订单:检索、补发、退款回收。 */
|
||||
@Tag(name = "后台-订单管理")
|
||||
@RestController
|
||||
@RequestMapping("/admin/orders")
|
||||
@RequiredArgsConstructor
|
||||
@@ -31,12 +34,14 @@ public class OrderAdminController {
|
||||
|
||||
private final OrderService orderService;
|
||||
|
||||
@Operation(summary = "订单分页查询")
|
||||
@GetMapping("/page")
|
||||
@PreAuthorize("@ss.hasPermission('order:list')")
|
||||
public Result<PageResult<Orders>> page(OrderQueryReqVO req) {
|
||||
return Result.ok(orderService.page(req));
|
||||
}
|
||||
|
||||
@Operation(summary = "订单详情")
|
||||
@GetMapping("/{orderNo}")
|
||||
@PreAuthorize("@ss.hasPermission('order:list')")
|
||||
public Result<Orders> detail(@PathVariable String orderNo) {
|
||||
@@ -44,6 +49,7 @@ public class OrderAdminController {
|
||||
}
|
||||
|
||||
/** 订单状态流转流水:支付争议时的证据链。 */
|
||||
@Operation(summary = "订单状态流转流水")
|
||||
@GetMapping("/{orderNo}/events")
|
||||
@PreAuthorize("@ss.hasPermission('order:list')")
|
||||
public Result<List<OrderEvent>> events(@PathVariable String orderNo) {
|
||||
@@ -51,6 +57,7 @@ public class OrderAdminController {
|
||||
}
|
||||
|
||||
/** 补发:已支付未交付的订单重新交付;已交付时幂等返回。 */
|
||||
@Operation(summary = "补发订单(已支付未交付重新交付,幂等)")
|
||||
@PostMapping("/{orderNo}/compensate")
|
||||
@PreAuthorize("@ss.hasPermission('order:update')")
|
||||
@OperationLog(module = "订单管理", action = "补发订单")
|
||||
@@ -59,6 +66,7 @@ public class OrderAdminController {
|
||||
}
|
||||
|
||||
/** 退款/撤销:回收权益并扣回金币。 */
|
||||
@Operation(summary = "退款 / 撤销(回收权益并扣回金币)")
|
||||
@PostMapping("/{orderNo}/refund")
|
||||
@PreAuthorize("@ss.hasPermission('order:update')")
|
||||
@OperationLog(module = "订单管理", action = "订单退款")
|
||||
|
||||
+4
@@ -5,6 +5,8 @@ package com.fruitshorts.admin.controller;
|
||||
import com.fruitshorts.common.pojo.Result;
|
||||
import com.fruitshorts.system.service.PermissionService;
|
||||
import com.fruitshorts.system.vo.PermissionGroupVO;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -13,6 +15,7 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Tag(name = "后台-权限点")
|
||||
@RestController
|
||||
@RequestMapping("/admin/permissions")
|
||||
@RequiredArgsConstructor
|
||||
@@ -21,6 +24,7 @@ public class PermissionController {
|
||||
private final PermissionService permissionService;
|
||||
|
||||
/** 按分组返回权限点,直接喂给前端的角色权限配置页。 */
|
||||
@Operation(summary = "权限点列表(按分组返回)")
|
||||
@GetMapping
|
||||
@PreAuthorize("@ss.hasPermission('system:role')")
|
||||
public Result<List<PermissionGroupVO>> groups() {
|
||||
|
||||
+5
@@ -7,6 +7,8 @@ import com.fruitshorts.content.service.PriceService;
|
||||
import com.fruitshorts.content.vo.PriceRespVO;
|
||||
import com.fruitshorts.content.vo.PriceSaveReqVO;
|
||||
import com.fruitshorts.framework.log.OperationLog;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
@@ -17,6 +19,7 @@ import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/** 后台分集价格与免费集配置。 */
|
||||
@Tag(name = "后台-分集收费")
|
||||
@RestController
|
||||
@RequestMapping("/admin/prices")
|
||||
@RequiredArgsConstructor
|
||||
@@ -25,12 +28,14 @@ public class PriceAdminController {
|
||||
private final PriceService priceService;
|
||||
|
||||
/** 当前价格配置:整剧默认 + 分集明细。 */
|
||||
@Operation(summary = "分集价格配置(整剧默认 + 分集明细)")
|
||||
@GetMapping
|
||||
@PreAuthorize("@ss.hasPermission('price:update')")
|
||||
public Result<PriceRespVO> detail(@RequestParam Long dramaId) {
|
||||
return Result.ok(priceService.detail(dramaId));
|
||||
}
|
||||
|
||||
@Operation(summary = "保存分集价格配置")
|
||||
@PutMapping
|
||||
@PreAuthorize("@ss.hasPermission('price:update')")
|
||||
@OperationLog(module = "定价管理", action = "配置分集价格")
|
||||
|
||||
+5
@@ -7,6 +7,8 @@ import com.fruitshorts.commerce.service.ReconcileService;
|
||||
import com.fruitshorts.common.pojo.PageResult;
|
||||
import com.fruitshorts.common.pojo.Result;
|
||||
import com.fruitshorts.framework.log.OperationLog;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -18,6 +20,7 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
import java.time.LocalDate;
|
||||
|
||||
/** 后台对账:手动触发与历史查看。 */
|
||||
@Tag(name = "后台-对账")
|
||||
@RestController
|
||||
@RequestMapping("/admin/reconcile")
|
||||
@RequiredArgsConstructor
|
||||
@@ -25,6 +28,7 @@ public class ReconcileAdminController {
|
||||
|
||||
private final ReconcileService reconcileService;
|
||||
|
||||
@Operation(summary = "对账任务列表")
|
||||
@GetMapping("/page")
|
||||
@PreAuthorize("@ss.hasPermission('order:list')")
|
||||
public Result<PageResult<ReconcileTask>> page(@RequestParam(defaultValue = "1") Integer pageIndex,
|
||||
@@ -33,6 +37,7 @@ public class ReconcileAdminController {
|
||||
}
|
||||
|
||||
/** 手动对账:默认前一天,可指定日期与渠道。 */
|
||||
@Operation(summary = "手动触发对账")
|
||||
@PostMapping("/run")
|
||||
@PreAuthorize("@ss.hasPermission('reconcile:run')")
|
||||
@OperationLog(module = "订单管理", action = "手动对账")
|
||||
|
||||
+5
@@ -10,6 +10,8 @@ import com.fruitshorts.interaction.dal.entity.ContentReport;
|
||||
import com.fruitshorts.interaction.service.ReportService;
|
||||
import com.fruitshorts.interaction.vo.ReportHandleReqVO;
|
||||
import com.fruitshorts.interaction.vo.ReportQueryReqVO;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -20,6 +22,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/** 后台举报工单。 */
|
||||
@Tag(name = "后台-举报工单")
|
||||
@RestController
|
||||
@RequestMapping("/admin/reports")
|
||||
@RequiredArgsConstructor
|
||||
@@ -27,6 +30,7 @@ public class ReportAdminController {
|
||||
|
||||
private final ReportService reportService;
|
||||
|
||||
@Operation(summary = "举报工单分页查询")
|
||||
@GetMapping("/page")
|
||||
@PreAuthorize("@ss.hasPermission('report:list')")
|
||||
public Result<PageResult<ContentReport>> page(ReportQueryReqVO req) {
|
||||
@@ -34,6 +38,7 @@ public class ReportAdminController {
|
||||
}
|
||||
|
||||
/** 处理工单:结论成立会联动驳回被举报评论,可选择同时封禁被举报用户。 */
|
||||
@Operation(summary = "处理举报工单(可联动驳回评论与封禁用户)")
|
||||
@PutMapping("/{id}/handle")
|
||||
@PreAuthorize("@ss.hasPermission('report:handle')")
|
||||
@OperationLog(module = "内容治理", action = "处理举报")
|
||||
|
||||
+7
@@ -7,6 +7,8 @@ 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 io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
@@ -21,6 +23,7 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Tag(name = "后台-角色分类")
|
||||
@RestController
|
||||
@RequestMapping("/admin/role-categories")
|
||||
@RequiredArgsConstructor
|
||||
@@ -28,12 +31,14 @@ public class RoleCategoryController {
|
||||
|
||||
private final RoleCategoryService roleCategoryService;
|
||||
|
||||
@Operation(summary = "角色分类列表")
|
||||
@GetMapping
|
||||
@PreAuthorize("@ss.hasPermission('system:role-category')")
|
||||
public Result<List<RoleCategoryRespVO>> list() {
|
||||
return Result.ok(roleCategoryService.list());
|
||||
}
|
||||
|
||||
@Operation(summary = "新建角色分类")
|
||||
@PostMapping
|
||||
@PreAuthorize("@ss.hasPermission('system:role-category')")
|
||||
@OperationLog(module = "后台管理", action = "新增角色分类")
|
||||
@@ -41,6 +46,7 @@ public class RoleCategoryController {
|
||||
return Result.ok(roleCategoryService.create(req));
|
||||
}
|
||||
|
||||
@Operation(summary = "编辑角色分类")
|
||||
@PutMapping("/{id}")
|
||||
@PreAuthorize("@ss.hasPermission('system:role-category')")
|
||||
@OperationLog(module = "后台管理", action = "编辑角色分类")
|
||||
@@ -49,6 +55,7 @@ public class RoleCategoryController {
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@Operation(summary = "删除角色分类")
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("@ss.hasPermission('system:role-category')")
|
||||
@OperationLog(module = "后台管理", action = "删除角色分类")
|
||||
|
||||
+11
@@ -9,6 +9,8 @@ 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 io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
@@ -24,6 +26,7 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Tag(name = "后台-角色管理")
|
||||
@RestController
|
||||
@RequestMapping("/admin/roles")
|
||||
@RequiredArgsConstructor
|
||||
@@ -31,6 +34,7 @@ public class RoleController {
|
||||
|
||||
private final RoleService roleService;
|
||||
|
||||
@Operation(summary = "角色分页查询")
|
||||
@GetMapping("/page")
|
||||
@PreAuthorize("@ss.hasPermission('system:role')")
|
||||
public Result<PageResult<RoleRespVO>> page(@RequestParam(required = false) String keyword,
|
||||
@@ -39,17 +43,20 @@ public class RoleController {
|
||||
return Result.ok(roleService.page(keyword, pageIndex, pageSize));
|
||||
}
|
||||
|
||||
@Operation(summary = "角色下拉列表")
|
||||
@GetMapping("/list")
|
||||
public Result<List<RoleRespVO>> list() {
|
||||
return Result.ok(roleService.listAll());
|
||||
}
|
||||
|
||||
@Operation(summary = "角色详情")
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("@ss.hasPermission('system:role')")
|
||||
public Result<RoleRespVO> get(@PathVariable Long id) {
|
||||
return Result.ok(roleService.get(id));
|
||||
}
|
||||
|
||||
@Operation(summary = "新建角色")
|
||||
@PostMapping
|
||||
@PreAuthorize("@ss.hasPermission('system:role')")
|
||||
@OperationLog(module = "后台管理", action = "新增角色")
|
||||
@@ -57,6 +64,7 @@ public class RoleController {
|
||||
return Result.ok(roleService.create(req));
|
||||
}
|
||||
|
||||
@Operation(summary = "编辑角色")
|
||||
@PutMapping("/{id}")
|
||||
@PreAuthorize("@ss.hasPermission('system:role')")
|
||||
@OperationLog(module = "后台管理", action = "编辑角色")
|
||||
@@ -65,6 +73,7 @@ public class RoleController {
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@Operation(summary = "删除角色")
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("@ss.hasPermission('system:role')")
|
||||
@OperationLog(module = "后台管理", action = "删除角色")
|
||||
@@ -73,12 +82,14 @@ public class RoleController {
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@Operation(summary = "角色已授权限")
|
||||
@GetMapping("/{id}/permissions")
|
||||
@PreAuthorize("@ss.hasPermission('system:role')")
|
||||
public Result<List<String>> permissions(@PathVariable Long id) {
|
||||
return Result.ok(roleService.listCodesByRoleId(id));
|
||||
}
|
||||
|
||||
@Operation(summary = "保存角色权限")
|
||||
@PutMapping("/{id}/permissions")
|
||||
@PreAuthorize("@ss.hasPermission('system:role')")
|
||||
@OperationLog(module = "后台管理", action = "修改角色权限")
|
||||
|
||||
+7
@@ -8,6 +8,8 @@ import com.fruitshorts.framework.log.OperationLog;
|
||||
import com.fruitshorts.interaction.dal.entity.SensitiveWord;
|
||||
import com.fruitshorts.interaction.service.SensitiveWordService;
|
||||
import com.fruitshorts.interaction.vo.SensitiveWordReqVO;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
@@ -22,6 +24,7 @@ import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/** 后台敏感词库。改动即时生效(服务端词库缓存会失效重建)。 */
|
||||
@Tag(name = "后台-敏感词库")
|
||||
@RestController
|
||||
@RequestMapping("/admin/sensitive-words")
|
||||
@RequiredArgsConstructor
|
||||
@@ -29,6 +32,7 @@ public class SensitiveWordAdminController {
|
||||
|
||||
private final SensitiveWordService sensitiveWordService;
|
||||
|
||||
@Operation(summary = "敏感词分页查询")
|
||||
@GetMapping("/page")
|
||||
@PreAuthorize("@ss.hasPermission('sensitive-word:list')")
|
||||
public Result<PageResult<SensitiveWord>> page(@RequestParam(required = false) String keyword,
|
||||
@@ -39,6 +43,7 @@ public class SensitiveWordAdminController {
|
||||
return Result.ok(sensitiveWordService.page(keyword, category, level, pageIndex, pageSize));
|
||||
}
|
||||
|
||||
@Operation(summary = "新增敏感词")
|
||||
@PostMapping
|
||||
@PreAuthorize("@ss.hasPermission('sensitive-word:update')")
|
||||
@OperationLog(module = "内容治理", action = "新增敏感词")
|
||||
@@ -46,6 +51,7 @@ public class SensitiveWordAdminController {
|
||||
return Result.ok(sensitiveWordService.create(req));
|
||||
}
|
||||
|
||||
@Operation(summary = "编辑敏感词")
|
||||
@PutMapping("/{id}")
|
||||
@PreAuthorize("@ss.hasPermission('sensitive-word:update')")
|
||||
@OperationLog(module = "内容治理", action = "修改敏感词")
|
||||
@@ -54,6 +60,7 @@ public class SensitiveWordAdminController {
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@Operation(summary = "删除敏感词")
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("@ss.hasPermission('sensitive-word:update')")
|
||||
@OperationLog(module = "内容治理", action = "删除敏感词")
|
||||
|
||||
+6
@@ -10,6 +10,8 @@ import com.fruitshorts.creator.vo.SubmissionAuditReqVO;
|
||||
import com.fruitshorts.creator.vo.SubmissionQueryReqVO;
|
||||
import com.fruitshorts.framework.log.OperationLog;
|
||||
import com.fruitshorts.framework.security.SecurityUtils;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -20,6 +22,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/** 后台内容提交审核:检索、详情、审核(通过 / 驳回 / 撤回)。 */
|
||||
@Tag(name = "后台-内容提交审核")
|
||||
@RestController
|
||||
@RequestMapping("/admin/submissions")
|
||||
@RequiredArgsConstructor
|
||||
@@ -27,18 +30,21 @@ public class SubmissionAdminController {
|
||||
|
||||
private final ContentSubmissionService submissionService;
|
||||
|
||||
@Operation(summary = "内容提交分页查询")
|
||||
@GetMapping("/page")
|
||||
@PreAuthorize("@ss.hasPermission('submission:list')")
|
||||
public Result<PageResult<ContentSubmission>> page(SubmissionQueryReqVO req) {
|
||||
return Result.ok(submissionService.page(req));
|
||||
}
|
||||
|
||||
@Operation(summary = "内容提交详情")
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("@ss.hasPermission('submission:list')")
|
||||
public Result<ContentSubmission> detail(@PathVariable Long id) {
|
||||
return Result.ok(submissionService.getExisting(id));
|
||||
}
|
||||
|
||||
@Operation(summary = "内容提交审核")
|
||||
@PutMapping("/{id}/audit")
|
||||
@PreAuthorize("@ss.hasPermission('submission:audit')")
|
||||
@OperationLog(module = "内容审核", action = "提交审核")
|
||||
|
||||
+5
@@ -11,6 +11,8 @@ import com.fruitshorts.common.pojo.Result;
|
||||
import com.fruitshorts.framework.log.OperationLog;
|
||||
import com.fruitshorts.framework.security.SecurityUtils;
|
||||
import com.fruitshorts.user.dal.mapper.AppUserMapper;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
@@ -25,6 +27,7 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
import java.util.List;
|
||||
|
||||
/** 后台钱包:人工补币/扣回,全部进流水且必须带幂等键。 */
|
||||
@Tag(name = "后台-金币钱包")
|
||||
@RestController
|
||||
@RequestMapping("/admin/wallets")
|
||||
@RequiredArgsConstructor
|
||||
@@ -33,6 +36,7 @@ public class WalletAdminController {
|
||||
private final WalletService walletService;
|
||||
private final AppUserMapper appUserMapper;
|
||||
|
||||
@Operation(summary = "金币流水")
|
||||
@GetMapping("/{userId}/ledger")
|
||||
@PreAuthorize("@ss.hasPermission('order:list')")
|
||||
public Result<List<CoinLedger>> ledger(@PathVariable Long userId,
|
||||
@@ -40,6 +44,7 @@ public class WalletAdminController {
|
||||
return Result.ok(walletService.recentLedger(userId, limit));
|
||||
}
|
||||
|
||||
@Operation(summary = "人工补币")
|
||||
@PostMapping("/coins")
|
||||
@PreAuthorize("@ss.hasPermission('wallet:update')")
|
||||
@OperationLog(module = "订单管理", action = "人工补币")
|
||||
|
||||
@@ -28,10 +28,15 @@ mybatis-plus:
|
||||
global-config:
|
||||
banner: false
|
||||
db-config:
|
||||
id-type: auto
|
||||
# 主键由应用侧雪花算法生成(S0),数据库不再自增
|
||||
id-type: assign_id
|
||||
logic-delete-field: deleted
|
||||
logic-delete-value: 1
|
||||
logic-not-delete-value: 0
|
||||
sequence:
|
||||
# 单机开发用固定值;上多实例前必须改成按实例分配(环境变量注入),否则会撞 ID
|
||||
worker-id: 1
|
||||
datacenter-id: 1
|
||||
|
||||
fruitshorts:
|
||||
media:
|
||||
|
||||
@@ -41,6 +41,12 @@
|
||||
<groupId>com.fruitshorts</groupId>
|
||||
<artifactId>fruitshorts-module-commerce</artifactId>
|
||||
</dependency>
|
||||
<!-- 契约测试用:MockMvc + Mockito + JUnit5,test scope,不进运行态依赖 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@@ -5,12 +5,34 @@ package com.fruitshorts.app;
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
|
||||
import io.swagger.v3.oas.annotations.info.Info;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
* 客户端接口服务,只暴露 /app/** 与静态媒资 /files/**。
|
||||
* 与 fruitshorts-admin-api 共用同一套领域模块和数据库,但入口、鉴权和限流策略独立。
|
||||
*/
|
||||
/* 显式指定扫描根包:本类在 com.fruitshorts.app 下,默认扫不到 content/system/framework 模块的 Bean */
|
||||
@OpenAPIDefinition(
|
||||
info = @Info(
|
||||
title = "FruitShorts 客户端接口(/app/**)",
|
||||
version = "1.0.0",
|
||||
description = "接口契约以 FruitServer/docs/api-phase1.md 为准"
|
||||
),
|
||||
tags = {
|
||||
@Tag(name = "客户端-账号", description = "验证码、注册、登录、令牌刷新与登出"),
|
||||
@Tag(name = "客户端-内容浏览", description = "首页 / 发现 / 分类聚合接口"),
|
||||
@Tag(name = "客户端-评论", description = "评论发布、列表与删除"),
|
||||
@Tag(name = "客户端-金币与订单", description = "钱包、金币解锁与订单票据"),
|
||||
@Tag(name = "客户端-离线下载", description = "下载授权、下载偏好与断点续传"),
|
||||
@Tag(name = "客户端-短剧与播放", description = "短剧详情与播放票据"),
|
||||
@Tag(name = "客户端-收藏", description = "收藏切换与列表"),
|
||||
@Tag(name = "客户端-点赞与举报", description = "点赞与举报提报"),
|
||||
@Tag(name = "客户端-个人中心", description = "资料、设备、数据导出与注销"),
|
||||
@Tag(name = "客户端-观看进度", description = "观看进度上报与续看位置"),
|
||||
}
|
||||
)
|
||||
@SpringBootApplication(scanBasePackages = "com.fruitshorts")
|
||||
@MapperScan("com.fruitshorts.**.dal.mapper")
|
||||
public class AppApiApplication {
|
||||
|
||||
+8
@@ -13,6 +13,8 @@ import com.fruitshorts.user.vo.OtpSendReqVO;
|
||||
import com.fruitshorts.user.vo.RefreshReqVO;
|
||||
import com.fruitshorts.user.vo.RegisterReqVO;
|
||||
import com.fruitshorts.user.vo.TokenRespVO;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -22,6 +24,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/** 客户端账号:验证码、注册、登录、刷新、登出。 */
|
||||
@Tag(name = "客户端-账号")
|
||||
@RestController
|
||||
@RequestMapping("/app/auth")
|
||||
@RequiredArgsConstructor
|
||||
@@ -31,30 +34,35 @@ public class AppAuthController {
|
||||
private final AppAuthService authService;
|
||||
|
||||
/** 发送验证码。频控在服务端,客户端不需要自己限流。 */
|
||||
@Operation(summary = "发送验证码(频控在服务端)")
|
||||
@PostMapping("/otp")
|
||||
public Result<Void> sendOtp(@Valid @RequestBody OtpSendReqVO req, HttpServletRequest request) {
|
||||
otpService.send(req, WebUtils.getClientIp(request));
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@Operation(summary = "注册账号(邮箱免验证码 / 手机号需验证码)")
|
||||
@PostMapping("/register")
|
||||
public Result<TokenRespVO> register(@Valid @RequestBody RegisterReqVO req,
|
||||
HttpServletRequest request) {
|
||||
return Result.ok(authService.register(req, WebUtils.getClientIp(request)));
|
||||
}
|
||||
|
||||
@Operation(summary = "登录(验证码或密码)")
|
||||
@PostMapping("/login")
|
||||
public Result<TokenRespVO> login(@Valid @RequestBody LoginReqVO req,
|
||||
HttpServletRequest request) {
|
||||
return Result.ok(authService.login(req, WebUtils.getClientIp(request)));
|
||||
}
|
||||
|
||||
@Operation(summary = "刷新令牌(旧 refresh token 立即失效)")
|
||||
@PostMapping("/refresh")
|
||||
public Result<TokenRespVO> refresh(@Valid @RequestBody RefreshReqVO req) {
|
||||
return Result.ok(authService.refresh(req.getRefreshToken()));
|
||||
}
|
||||
|
||||
/** 登出:默认只吊销当前设备的会话,allDevices=true 时吊销全部。 */
|
||||
@Operation(summary = "登出(默认当前设备,allDevices 吊销全部)")
|
||||
@PostMapping("/logout")
|
||||
public Result<Void> logout(@RequestBody(required = false) LogoutReqVO req) {
|
||||
authService.logout(SecurityUtils.getUserIdOrNull(), req == null ? new LogoutReqVO() : req);
|
||||
|
||||
+6
@@ -6,6 +6,8 @@ import com.fruitshorts.app.service.AppCatalogService;
|
||||
import com.fruitshorts.app.vo.AppCategoryVO;
|
||||
import com.fruitshorts.app.vo.AppFeedVO;
|
||||
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.RequestMapping;
|
||||
@@ -16,6 +18,7 @@ import java.util.List;
|
||||
/**
|
||||
* 客户端目录接口:首页与发现页各一次请求拿全量聚合数据。
|
||||
*/
|
||||
@Tag(name = "客户端-内容浏览")
|
||||
@RestController
|
||||
@RequestMapping("/app")
|
||||
@RequiredArgsConstructor
|
||||
@@ -23,16 +26,19 @@ public class AppCatalogController {
|
||||
|
||||
private final AppCatalogService appCatalogService;
|
||||
|
||||
@Operation(summary = "首页聚合:轮播 + 分区,一次请求渲染整页")
|
||||
@GetMapping("/home/feed")
|
||||
public Result<AppFeedVO> homeFeed() {
|
||||
return Result.ok(appCatalogService.homeFeed());
|
||||
}
|
||||
|
||||
@Operation(summary = "发现页聚合:分区列表")
|
||||
@GetMapping("/discover/feed")
|
||||
public Result<AppFeedVO> discoverFeed() {
|
||||
return Result.ok(appCatalogService.discoverFeed());
|
||||
}
|
||||
|
||||
@Operation(summary = "分类列表")
|
||||
@GetMapping("/categories")
|
||||
public Result<List<AppCategoryVO>> categories() {
|
||||
return Result.ok(appCatalogService.categories());
|
||||
|
||||
+6
@@ -10,6 +10,8 @@ import com.fruitshorts.interaction.service.CommentService;
|
||||
import com.fruitshorts.interaction.vo.CommentPublishReqVO;
|
||||
import com.fruitshorts.interaction.vo.CommentQueryReqVO;
|
||||
import com.fruitshorts.interaction.vo.CommentRespVO;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -29,6 +31,7 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
* 不传 `rootId` 时返回顶层评论,传了则返回该楼层的回复。
|
||||
* 列表允许匿名浏览;发布与删除需要登录。
|
||||
*/
|
||||
@Tag(name = "客户端-评论")
|
||||
@RestController
|
||||
@RequestMapping("/app/comments")
|
||||
@RequiredArgsConstructor
|
||||
@@ -36,6 +39,7 @@ public class AppCommentController {
|
||||
|
||||
private final CommentService commentService;
|
||||
|
||||
@Operation(summary = "发布评论")
|
||||
@PostMapping
|
||||
public Result<CommentRespVO> publish(@Valid @RequestBody CommentPublishReqVO req,
|
||||
HttpServletRequest request) {
|
||||
@@ -43,6 +47,7 @@ public class AppCommentController {
|
||||
WebUtils.getClientIp(request), request.getHeader("X-Device-Id")));
|
||||
}
|
||||
|
||||
@Operation(summary = "评论列表(游标翻页,可匿名浏览)")
|
||||
@GetMapping
|
||||
public Result<CursorResult<CommentRespVO>> page(@RequestParam Long dramaId,
|
||||
@RequestParam(required = false) Long episodeId,
|
||||
@@ -58,6 +63,7 @@ public class AppCommentController {
|
||||
return Result.ok(commentService.page(SecurityUtils.getUserIdOrNull(), query));
|
||||
}
|
||||
|
||||
@Operation(summary = "删除自己的评论")
|
||||
@DeleteMapping("/{id}")
|
||||
public Result<Void> delete(@PathVariable Long id) {
|
||||
commentService.delete(SecurityUtils.getAppUserId(), id);
|
||||
|
||||
+7
@@ -8,6 +8,8 @@ import com.fruitshorts.commerce.service.UnlockService;
|
||||
import com.fruitshorts.commerce.service.WalletService;
|
||||
import com.fruitshorts.common.pojo.Result;
|
||||
import com.fruitshorts.framework.security.SecurityUtils;
|
||||
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;
|
||||
@@ -26,6 +28,7 @@ import java.util.Map;
|
||||
* 金币解锁是第一阶段真实的收费方式;真实商店购买走 {@code /app/orders/verify},
|
||||
* 本轮只预留接口(商店未接入时返回明确业务提示,不会产生假订单)。
|
||||
*/
|
||||
@Tag(name = "客户端-金币与订单")
|
||||
@RestController
|
||||
@RequestMapping("/app")
|
||||
@RequiredArgsConstructor
|
||||
@@ -36,6 +39,7 @@ public class AppCommerceController {
|
||||
private final OrderService orderService;
|
||||
|
||||
/** 我的钱包:余额 + 最近流水,客户端一处渲染。 */
|
||||
@Operation(summary = "我的钱包(余额 + 最近流水)")
|
||||
@GetMapping("/wallet")
|
||||
public Result<Map<String, Object>> wallet() {
|
||||
Long userId = SecurityUtils.getAppUserId();
|
||||
@@ -46,17 +50,20 @@ public class AppCommerceController {
|
||||
}
|
||||
|
||||
/** 金币解锁分集:幂等,重复点击不会重复扣币。 */
|
||||
@Operation(summary = "金币解锁分集(幂等,重复点击不重复扣币)")
|
||||
@PostMapping("/episodes/{episodeId}/unlock")
|
||||
public Result<Map<String, Object>> unlock(@PathVariable Long episodeId) {
|
||||
return Result.ok(unlockService.unlock(SecurityUtils.getAppUserId(), episodeId));
|
||||
}
|
||||
|
||||
@Operation(summary = "我的订单列表")
|
||||
@GetMapping("/orders")
|
||||
public Result<List<Orders>> orders() {
|
||||
return Result.ok(orderService.listByUser(SecurityUtils.getAppUserId()));
|
||||
}
|
||||
|
||||
/** 提交商店票据。第一阶段商店未接入,会返回明确的"尚未配置"。 */
|
||||
@Operation(summary = "提交商店票据(第一阶段商店未接入,返回明确提示)")
|
||||
@PostMapping("/orders/verify")
|
||||
public Result<Orders> verify(@RequestBody Map<String, String> body) {
|
||||
return Result.ok(orderService.verifyAndDeliver(SecurityUtils.getAppUserId(),
|
||||
|
||||
+11
@@ -10,6 +10,8 @@ import com.fruitshorts.playback.vo.DownloadLicenseRespVO;
|
||||
import com.fruitshorts.playback.vo.DownloadPolicyRespVO;
|
||||
import com.fruitshorts.playback.vo.DownloadProgressReqVO;
|
||||
import com.fruitshorts.playback.vo.DownloadSettingReqVO;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -31,6 +33,7 @@ import java.util.List;
|
||||
* <p>服务端只发授权与地址,分片由客户端向 CDN 拉取;进度上报用于断点续传与跨端展示。
|
||||
* 设备标识优先取请求体,其次取请求头 X-Device-Id。
|
||||
*/
|
||||
@Tag(name = "客户端-离线下载")
|
||||
@RestController
|
||||
@RequestMapping("/app/downloads")
|
||||
@RequiredArgsConstructor
|
||||
@@ -39,6 +42,7 @@ public class AppDownloadController {
|
||||
private final DownloadService downloadService;
|
||||
|
||||
/** 申请下载授权(同一剧集重复申请幂等,只刷新有效期)。 */
|
||||
@Operation(summary = "申请下载授权(同一剧集幂等,只刷新有效期)")
|
||||
@PostMapping
|
||||
public Result<DownloadLicenseRespVO> grant(@Valid @RequestBody DownloadGrantReqVO req,
|
||||
HttpServletRequest request) {
|
||||
@@ -46,17 +50,20 @@ public class AppDownloadController {
|
||||
request.getHeader("X-Device-Id")));
|
||||
}
|
||||
|
||||
@Operation(summary = "我的下载授权列表")
|
||||
@GetMapping
|
||||
public Result<List<DownloadLicenseRespVO>> list(@RequestParam(required = false) String deviceId) {
|
||||
return Result.ok(downloadService.list(SecurityUtils.getAppUserId(), deviceId));
|
||||
}
|
||||
|
||||
@Operation(summary = "下载规则(设备上限 / 仅 Wi-Fi / 清晰度上限)")
|
||||
@GetMapping("/policy")
|
||||
public Result<DownloadPolicyRespVO> policy(@RequestParam(required = false) String deviceId) {
|
||||
return Result.ok(downloadService.policy(SecurityUtils.getAppUserId(), deviceId));
|
||||
}
|
||||
|
||||
/** 仅 Wi-Fi 下载、清晰度上限等偏好。 */
|
||||
@Operation(summary = "更新下载偏好设置")
|
||||
@PutMapping("/settings")
|
||||
public Result<Void> updateSettings(@RequestBody DownloadSettingReqVO req) {
|
||||
downloadService.updateSettings(SecurityUtils.getAppUserId(), req);
|
||||
@@ -64,6 +71,7 @@ public class AppDownloadController {
|
||||
}
|
||||
|
||||
/** 断点续传前重新取一次地址:授权不变,只有地址是短时效的。 */
|
||||
@Operation(summary = "重新获取下载地址(断点续传前刷新短时效地址)")
|
||||
@GetMapping("/{episodeId}/url")
|
||||
public Result<DownloadLicenseRespVO> refreshUrl(@PathVariable Long episodeId,
|
||||
HttpServletRequest request) {
|
||||
@@ -71,6 +79,7 @@ public class AppDownloadController {
|
||||
request.getHeader("X-Device-Id")));
|
||||
}
|
||||
|
||||
@Operation(summary = "上报下载进度")
|
||||
@PostMapping("/{episodeId}/progress")
|
||||
public Result<DownloadLicenseRespVO> reportProgress(@PathVariable Long episodeId,
|
||||
@RequestBody DownloadProgressReqVO req,
|
||||
@@ -79,6 +88,7 @@ public class AppDownloadController {
|
||||
request.getHeader("X-Device-Id")));
|
||||
}
|
||||
|
||||
@Operation(summary = "吊销某集的下载授权")
|
||||
@DeleteMapping("/{episodeId}")
|
||||
public Result<Void> revoke(@PathVariable Long episodeId,
|
||||
@RequestParam(required = false) String deviceId,
|
||||
@@ -89,6 +99,7 @@ public class AppDownloadController {
|
||||
}
|
||||
|
||||
/** 清空当前设备的全部下载授权。 */
|
||||
@Operation(summary = "清空当前设备的全部下载授权")
|
||||
@DeleteMapping
|
||||
public Result<Integer> clear(@RequestParam(required = false) String deviceId,
|
||||
HttpServletRequest request) {
|
||||
|
||||
+5
@@ -9,6 +9,8 @@ import com.fruitshorts.framework.security.SecurityUtils;
|
||||
import com.fruitshorts.framework.web.WebUtils;
|
||||
import com.fruitshorts.playback.service.PlaybackService;
|
||||
import com.fruitshorts.playback.vo.PlayRespVO;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -16,6 +18,7 @@ import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@Tag(name = "客户端-短剧与播放")
|
||||
@RestController
|
||||
@RequestMapping("/app")
|
||||
@RequiredArgsConstructor
|
||||
@@ -25,6 +28,7 @@ public class AppDramaController {
|
||||
private final PlaybackService playbackService;
|
||||
|
||||
/** 详情:包含剧集列表,客户端进详情页一次请求即可渲染 */
|
||||
@Operation(summary = "短剧详情(含剧集列表,进详情页一次请求即可渲染)")
|
||||
@GetMapping("/dramas/{id}")
|
||||
public Result<AppDramaDetailVO> detail(@PathVariable Long id) {
|
||||
return Result.ok(appCatalogService.dramaDetail(id));
|
||||
@@ -34,6 +38,7 @@ public class AppDramaController {
|
||||
* 播放票据:服务端判定权益后签发短时效播放地址。
|
||||
* 未登录也能播免费集;付费集未解锁时只返回需要多少金币。
|
||||
*/
|
||||
@Operation(summary = "播放票据:判定权益后签发短时效播放地址")
|
||||
@GetMapping("/episodes/{id}/play")
|
||||
public Result<PlayRespVO> play(@PathVariable Long id, HttpServletRequest request) {
|
||||
return Result.ok(playbackService.play(
|
||||
|
||||
+6
@@ -8,6 +8,8 @@ import com.fruitshorts.common.pojo.Result;
|
||||
import com.fruitshorts.framework.security.SecurityUtils;
|
||||
import com.fruitshorts.playback.service.FavoriteService;
|
||||
import com.fruitshorts.playback.vo.FavoriteToggleReqVO;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
@@ -21,6 +23,7 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
import java.util.List;
|
||||
|
||||
/** 收藏。 */
|
||||
@Tag(name = "客户端-收藏")
|
||||
@RestController
|
||||
@RequestMapping("/app/favorites")
|
||||
@RequiredArgsConstructor
|
||||
@@ -30,18 +33,21 @@ public class AppFavoriteController {
|
||||
private final AppCatalogService appCatalogService;
|
||||
|
||||
/** 切换收藏,返回切换后的状态:true 为已收藏。 */
|
||||
@Operation(summary = "切换收藏(返回切换后的状态)")
|
||||
@PostMapping("/toggle")
|
||||
public Result<Boolean> toggle(@Valid @RequestBody FavoriteToggleReqVO req) {
|
||||
return Result.ok(favoriteService.toggle(SecurityUtils.getAppUserId(), req.getDramaId()));
|
||||
}
|
||||
|
||||
/** 收藏列表,卡片结构与首页一致。 */
|
||||
@Operation(summary = "我的收藏列表")
|
||||
@GetMapping
|
||||
public Result<List<AppDramaCardVO>> list() {
|
||||
return Result.ok(appCatalogService.toCards(
|
||||
favoriteService.listDramas(SecurityUtils.getAppUserId())));
|
||||
}
|
||||
|
||||
@Operation(summary = "取消收藏")
|
||||
@DeleteMapping("/{dramaId}")
|
||||
public Result<Void> remove(@PathVariable Long dramaId) {
|
||||
favoriteService.remove(SecurityUtils.getAppUserId(), dramaId);
|
||||
|
||||
+5
@@ -9,6 +9,8 @@ import com.fruitshorts.interaction.service.ReportService;
|
||||
import com.fruitshorts.interaction.vo.LikeToggleReqVO;
|
||||
import com.fruitshorts.interaction.vo.LikeToggleRespVO;
|
||||
import com.fruitshorts.interaction.vo.ReportCreateReqVO;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
@@ -17,6 +19,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/** 点赞与举报。 */
|
||||
@Tag(name = "客户端-点赞与举报")
|
||||
@RestController
|
||||
@RequestMapping("/app")
|
||||
@RequiredArgsConstructor
|
||||
@@ -26,12 +29,14 @@ public class AppInteractionController {
|
||||
private final ReportService reportService;
|
||||
|
||||
/** 点赞/取消点赞,返回切换后的状态与最新计数。 */
|
||||
@Operation(summary = "点赞 / 取消点赞")
|
||||
@PostMapping("/likes/toggle")
|
||||
public Result<LikeToggleRespVO> toggleLike(@Valid @RequestBody LikeToggleReqVO req) {
|
||||
return Result.ok(likeService.toggle(SecurityUtils.getAppUserId(), req));
|
||||
}
|
||||
|
||||
/** 提交举报:同一用户对同一目标只保留一条待处理工单。 */
|
||||
@Operation(summary = "提交举报(同一目标只保留一条待处理工单)")
|
||||
@PostMapping("/reports")
|
||||
public Result<Long> report(@Valid @RequestBody ReportCreateReqVO req) {
|
||||
return Result.ok(reportService.create(SecurityUtils.getAppUserId(), req));
|
||||
|
||||
@@ -9,6 +9,8 @@ import com.fruitshorts.user.service.AppUserService;
|
||||
import com.fruitshorts.user.vo.AppUserRespVO;
|
||||
import com.fruitshorts.user.vo.DeactivateReqVO;
|
||||
import com.fruitshorts.user.vo.ProfileUpdateReqVO;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -23,6 +25,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/** 客户端个人中心:资料、设备、注销、数据导出。 */
|
||||
@Tag(name = "客户端-个人中心")
|
||||
@RestController
|
||||
@RequestMapping("/app/me")
|
||||
@RequiredArgsConstructor
|
||||
@@ -30,33 +33,39 @@ public class AppMeController {
|
||||
|
||||
private final AppUserService userService;
|
||||
|
||||
@Operation(summary = "我的资料")
|
||||
@GetMapping
|
||||
public Result<AppUserRespVO> me() {
|
||||
return Result.ok(userService.me(SecurityUtils.getAppUserId()));
|
||||
}
|
||||
|
||||
@Operation(summary = "修改资料(昵称 / 头像 / 语言等)")
|
||||
@PutMapping
|
||||
public Result<AppUserRespVO> update(@Valid @RequestBody ProfileUpdateReqVO req) {
|
||||
return Result.ok(userService.updateProfile(SecurityUtils.getAppUserId(), req));
|
||||
}
|
||||
|
||||
@Operation(summary = "我的登录设备列表")
|
||||
@GetMapping("/devices")
|
||||
public Result<List<AppUserDevice>> devices() {
|
||||
return Result.ok(userService.devices(SecurityUtils.getAppUserId()));
|
||||
}
|
||||
|
||||
/** 商店与隐私法规要求的数据导出,返回账号侧数据;内容侧数据由客户端自行下载。 */
|
||||
@Operation(summary = "数据导出(合规要求)")
|
||||
@GetMapping("/export")
|
||||
public Result<Map<String, Object>> export() {
|
||||
return Result.ok(userService.exportAccount(SecurityUtils.getAppUserId()));
|
||||
}
|
||||
|
||||
/** 申请注销:立即登出全部设备,进入冷静期,期间可撤销。 */
|
||||
@Operation(summary = "申请注销(进入冷静期并登出全部设备)")
|
||||
@PostMapping("/deactivate")
|
||||
public Result<LocalDateTime> deactivate(@RequestBody(required = false) DeactivateReqVO req) {
|
||||
return Result.ok(userService.requestDeactivation(SecurityUtils.getAppUserId(), req));
|
||||
}
|
||||
|
||||
@Operation(summary = "撤销注销申请")
|
||||
@PostMapping("/deactivate/cancel")
|
||||
public Result<Void> cancelDeactivate() {
|
||||
userService.cancelDeactivation(SecurityUtils.getAppUserId());
|
||||
|
||||
+8
@@ -7,6 +7,8 @@ import com.fruitshorts.framework.security.SecurityUtils;
|
||||
import com.fruitshorts.playback.service.WatchProgressService;
|
||||
import com.fruitshorts.playback.vo.ProgressRespVO;
|
||||
import com.fruitshorts.playback.vo.ProgressSaveReqVO;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
@@ -21,6 +23,7 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
import java.util.List;
|
||||
|
||||
/** 观看进度与观看记录。 */
|
||||
@Tag(name = "客户端-观看进度")
|
||||
@RestController
|
||||
@RequestMapping("/app/watch")
|
||||
@RequiredArgsConstructor
|
||||
@@ -29,28 +32,33 @@ public class AppWatchController {
|
||||
private final WatchProgressService progressService;
|
||||
|
||||
/** 上报进度:客户端每 10 秒一次,切集时立即上报一次。 */
|
||||
@Operation(summary = "上报观看进度(客户端每 10 秒一次)")
|
||||
@PostMapping("/progress")
|
||||
public Result<ProgressRespVO> save(@Valid @RequestBody ProgressSaveReqVO req) {
|
||||
return Result.ok(progressService.save(SecurityUtils.getAppUserId(), req));
|
||||
}
|
||||
|
||||
/** 某部剧的续看位置,进详情页时用它决定"续看第 N 集"。 */
|
||||
@Operation(summary = "某部剧的续看位置")
|
||||
@GetMapping("/progress")
|
||||
public Result<ProgressRespVO> ofDrama(@RequestParam Long dramaId) {
|
||||
return Result.ok(progressService.ofDrama(SecurityUtils.getAppUserId(), dramaId));
|
||||
}
|
||||
|
||||
@Operation(summary = "最近观看列表")
|
||||
@GetMapping("/recent")
|
||||
public Result<List<ProgressRespVO>> recent(@RequestParam(defaultValue = "20") int limit) {
|
||||
return Result.ok(progressService.recent(SecurityUtils.getAppUserId(), limit));
|
||||
}
|
||||
|
||||
@Operation(summary = "删除某部剧的观看记录")
|
||||
@DeleteMapping("/progress/{dramaId}")
|
||||
public Result<Void> remove(@PathVariable Long dramaId) {
|
||||
progressService.remove(SecurityUtils.getAppUserId(), dramaId);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@Operation(summary = "清空观看记录")
|
||||
@DeleteMapping("/progress")
|
||||
public Result<Void> clear() {
|
||||
progressService.clear(SecurityUtils.getAppUserId());
|
||||
|
||||
@@ -23,11 +23,17 @@ mybatis-plus:
|
||||
global-config:
|
||||
banner: false
|
||||
db-config:
|
||||
id-type: auto
|
||||
# 主键由应用侧雪花算法生成(S0),数据库不再自增
|
||||
id-type: assign_id
|
||||
logic-delete-field: deleted
|
||||
logic-delete-value: 1
|
||||
logic-not-delete-value: 0
|
||||
sequence:
|
||||
# 单机开发用固定值;上多实例前必须改成按实例分配(环境变量注入),否则会撞 ID
|
||||
worker-id: 1
|
||||
datacenter-id: 1
|
||||
|
||||
# 接口文档:/doc.html 打开 Swagger UI,/v3/api-docs 是机器可读契约(前端联调按它写)
|
||||
springdoc:
|
||||
swagger-ui:
|
||||
path: /doc.html
|
||||
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
// Author: fruit.com.cn
|
||||
package com.fruitshorts.app.controller;
|
||||
|
||||
import com.fruitshorts.app.support.ControllerContractSupport;
|
||||
import com.fruitshorts.common.exception.ErrorCode;
|
||||
import com.fruitshorts.common.exception.ServiceException;
|
||||
import com.fruitshorts.user.service.AppAuthService;
|
||||
import com.fruitshorts.user.service.AppOtpService;
|
||||
import com.fruitshorts.user.vo.TokenRespVO;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.nullValue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
/**
|
||||
* `/app/auth/**` 的接入层契约:路由、请求体绑定、统一响应包 `{code,message,data}`、错误码映射。
|
||||
*
|
||||
* <p>业务规则(免码、密码强度、频控)在 {@code AppAuthServiceTest} / {@code AppOtpServiceTest} 里用真服务验证,
|
||||
* 这里只钉"接口长什么样"。
|
||||
*/
|
||||
class AppAuthControllerContractTest {
|
||||
|
||||
private AppOtpService otpService;
|
||||
private AppAuthService authService;
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
otpService = mock(AppOtpService.class);
|
||||
authService = mock(AppAuthService.class);
|
||||
mockMvc = ControllerContractSupport.mockMvcOf(new AppAuthController(otpService, authService));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_return_access_token_when_email_register_succeeds() throws Exception {
|
||||
given(authService.register(any(), any())).willReturn(tokens("access-1", "refresh-1"));
|
||||
|
||||
mockMvc.perform(post("/app/auth/register")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"targetType\":\"EMAIL\",\"target\":\"a@b.com\",\"password\":\"abcd1234\"}"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.message").value("ok"))
|
||||
.andExpect(jsonPath("$.data.accessToken").value("access-1"))
|
||||
.andExpect(jsonPath("$.data.tokenType").value("Bearer"))
|
||||
.andExpect(jsonPath("$.data.newUser").value(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_return_data_exists_code_when_email_is_already_registered() throws Exception {
|
||||
given(authService.register(any(), any())).willThrow(
|
||||
new ServiceException(ErrorCode.DATA_EXISTS, "该账号已注册,请直接登录"));
|
||||
|
||||
mockMvc.perform(post("/app/auth/register")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"targetType\":\"EMAIL\",\"target\":\"a@b.com\",\"password\":\"abcd1234\"}"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(ErrorCode.DATA_EXISTS.getCode()))
|
||||
.andExpect(jsonPath("$.message").value("该账号已注册,请直接登录"))
|
||||
.andExpect(jsonPath("$.data").value(nullValue()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_map_otp_required_error_when_phone_register_has_no_code() throws Exception {
|
||||
given(authService.register(any(), any())).willThrow(
|
||||
new ServiceException(ErrorCode.BAD_REQUEST, "请先获取验证码"));
|
||||
|
||||
mockMvc.perform(post("/app/auth/register")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"targetType\":\"PHONE\",\"target\":\"08123456789\"}"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(ErrorCode.BAD_REQUEST.getCode()))
|
||||
.andExpect(jsonPath("$.message").value("请先获取验证码"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_return_bad_request_code_when_register_body_has_no_target() throws Exception {
|
||||
mockMvc.perform(post("/app/auth/register")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"targetType\":\"EMAIL\"}"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(ErrorCode.BAD_REQUEST.getCode()))
|
||||
.andExpect(jsonPath("$.message").value(containsString("请输入手机号或邮箱")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_return_access_token_when_password_login_succeeds() throws Exception {
|
||||
given(authService.login(any(), any())).willReturn(tokens("access-2", "refresh-2"));
|
||||
|
||||
mockMvc.perform(post("/app/auth/login")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"targetType\":\"EMAIL\",\"target\":\"a@b.com\",\"password\":\"abcd1234\"}"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.accessToken").value("access-2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_return_login_failed_code_when_password_is_wrong() throws Exception {
|
||||
given(authService.login(any(), any())).willThrow(new ServiceException(ErrorCode.LOGIN_FAILED));
|
||||
|
||||
mockMvc.perform(post("/app/auth/login")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"targetType\":\"EMAIL\",\"target\":\"a@b.com\",\"password\":\"wrongpass1\"}"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(ErrorCode.LOGIN_FAILED.getCode()))
|
||||
.andExpect(jsonPath("$.message").value("账号或密码错误"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_return_ok_envelope_when_otp_send_succeeds() throws Exception {
|
||||
mockMvc.perform(post("/app/auth/otp")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"scene\":\"REGISTER\",\"targetType\":\"PHONE\",\"target\":\"08123456789\",\"deviceId\":\"d-1\"}"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data").value(nullValue()));
|
||||
|
||||
verify(otpService).send(any(), any());
|
||||
}
|
||||
|
||||
private TokenRespVO tokens(String accessToken, String refreshToken) {
|
||||
TokenRespVO vo = new TokenRespVO();
|
||||
vo.setAccessToken(accessToken);
|
||||
vo.setRefreshToken(refreshToken);
|
||||
vo.setExpiresIn(7200);
|
||||
vo.setNewUser(true);
|
||||
return vo;
|
||||
}
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
// Author: fruit.com.cn
|
||||
package com.fruitshorts.app.controller;
|
||||
|
||||
import com.fruitshorts.app.service.AppCatalogService;
|
||||
import com.fruitshorts.app.support.ControllerContractSupport;
|
||||
import com.fruitshorts.app.vo.AppBannerVO;
|
||||
import com.fruitshorts.app.vo.AppCategoryVO;
|
||||
import com.fruitshorts.app.vo.AppDramaDetailVO;
|
||||
import com.fruitshorts.app.vo.AppFeedVO;
|
||||
import com.fruitshorts.common.exception.ErrorCode;
|
||||
import com.fruitshorts.common.exception.ServiceException;
|
||||
import com.fruitshorts.playback.service.PlaybackService;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.hamcrest.Matchers.nullValue;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
/**
|
||||
* 目录接口的接入层契约:`/app/home/feed`、`/app/discover/feed`、`/app/categories`、`/app/dramas/{id}`。
|
||||
*
|
||||
* <p>关注三件事:路由通不通、路径参数有没有绑对、响应壳是不是 `{code,message,data}`。
|
||||
*/
|
||||
class AppCatalogControllerContractTest {
|
||||
|
||||
private AppCatalogService catalogService;
|
||||
private MockMvc catalogMvc;
|
||||
private MockMvc dramaMvc;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
catalogService = mock(AppCatalogService.class);
|
||||
catalogMvc = ControllerContractSupport.mockMvcOf(new AppCatalogController(catalogService));
|
||||
dramaMvc = ControllerContractSupport.mockMvcOf(
|
||||
new AppDramaController(catalogService, mock(PlaybackService.class)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_return_banners_and_sections_when_home_feed_is_requested() throws Exception {
|
||||
AppBannerVO banner = new AppBannerVO();
|
||||
banner.setId(9L);
|
||||
banner.setImageUrl("/files/banner/9.png");
|
||||
banner.setLinkType("DRAMA");
|
||||
banner.setLinkValue("12");
|
||||
AppFeedVO feed = new AppFeedVO(List.of(banner), List.of());
|
||||
given(catalogService.homeFeed()).willReturn(feed);
|
||||
|
||||
catalogMvc.perform(get("/app/home/feed"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.banners[0].id").value(9))
|
||||
.andExpect(jsonPath("$.data.banners[0].imageUrl").value("/files/banner/9.png"))
|
||||
.andExpect(jsonPath("$.data.banners[0].linkType").value("DRAMA"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_return_sections_when_discover_feed_is_requested() throws Exception {
|
||||
AppFeedVO feed = new AppFeedVO(List.of(), List.of());
|
||||
given(catalogService.discoverFeed()).willReturn(feed);
|
||||
|
||||
catalogMvc.perform(get("/app/discover/feed"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.sections").isArray());
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_return_category_list_when_categories_are_requested() throws Exception {
|
||||
AppCategoryVO category = new AppCategoryVO(3L, "都市");
|
||||
given(catalogService.categories()).willReturn(List.of(category));
|
||||
|
||||
catalogMvc.perform(get("/app/categories"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data[0].id").value(3))
|
||||
.andExpect(jsonPath("$.data[0].name").value("都市"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_bind_path_variable_when_drama_detail_is_requested() throws Exception {
|
||||
AppDramaDetailVO detail = new AppDramaDetailVO();
|
||||
detail.setId(42L);
|
||||
detail.setTitle("命运的相遇");
|
||||
detail.setEpisodes(List.of());
|
||||
given(catalogService.dramaDetail(42L)).willReturn(detail);
|
||||
|
||||
dramaMvc.perform(get("/app/dramas/42"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.id").value(42))
|
||||
.andExpect(jsonPath("$.data.title").value("命运的相遇"));
|
||||
|
||||
verify(catalogService).dramaDetail(42L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_return_data_not_found_code_when_drama_does_not_exist() throws Exception {
|
||||
given(catalogService.dramaDetail(404L))
|
||||
.willThrow(new ServiceException(ErrorCode.DATA_NOT_FOUND, "短剧不存在"));
|
||||
|
||||
dramaMvc.perform(get("/app/dramas/404"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(ErrorCode.DATA_NOT_FOUND.getCode()))
|
||||
.andExpect(jsonPath("$.data").value(nullValue()));
|
||||
}
|
||||
}
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
// Author: fruit.com.cn
|
||||
package com.fruitshorts.app.controller;
|
||||
|
||||
import com.fruitshorts.app.service.AppCatalogService;
|
||||
import com.fruitshorts.app.support.ControllerContractSupport;
|
||||
import com.fruitshorts.playback.service.PlaybackService;
|
||||
import com.fruitshorts.playback.vo.PlayRespVO;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
/**
|
||||
* 播放票据接口的接入层契约:`/app/episodes/{id}/play` 的免费/付费两种形态与参数透传。
|
||||
*
|
||||
* <p>免费 / 付费的**判定规则**在 {@code PlaybackServiceTest} 里用真服务验证;
|
||||
* 这里钉的是"下发给客户端的字段长什么样"(客户端 Flutter 依赖这些字段名)。
|
||||
*/
|
||||
class AppPlaybackContractTest {
|
||||
|
||||
private PlaybackService playbackService;
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
playbackService = mock(PlaybackService.class);
|
||||
mockMvc = ControllerContractSupport.mockMvcOf(
|
||||
new AppDramaController(mock(AppCatalogService.class), playbackService));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_return_free_access_when_episode_is_free() throws Exception {
|
||||
given(playbackService.play(isNull(), eq(11L), any(), any()))
|
||||
.willReturn(ticket(11L, true, PlaybackService.ACCESS_FREE, "/files/ep.mp4?s=1"));
|
||||
|
||||
mockMvc.perform(get("/app/episodes/11/play").header("X-Device-Id", "device-9"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.episodeId").value(11))
|
||||
.andExpect(jsonPath("$.data.accessible").value(true))
|
||||
.andExpect(jsonPath("$.data.accessType").value(PlaybackService.ACCESS_FREE))
|
||||
.andExpect(jsonPath("$.data.playUrl").value("/files/ep.mp4?s=1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_return_coin_required_when_episode_is_locked_and_not_entitled() throws Exception {
|
||||
PlayRespVO locked = ticket(12L, false, PlaybackService.ACCESS_COIN_REQUIRED, "");
|
||||
locked.setUnlockCoin(60);
|
||||
given(playbackService.play(isNull(), eq(12L), any(), any())).willReturn(locked);
|
||||
|
||||
mockMvc.perform(get("/app/episodes/12/play"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.accessible").value(false))
|
||||
.andExpect(jsonPath("$.data.accessType").value(PlaybackService.ACCESS_COIN_REQUIRED))
|
||||
.andExpect(jsonPath("$.data.playUrl").value(""))
|
||||
.andExpect(jsonPath("$.data.unlockCoin").value(60));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_pass_episode_id_and_device_header_when_play_is_requested() throws Exception {
|
||||
given(playbackService.play(isNull(), eq(13L), eq("device-9"), any()))
|
||||
.willReturn(ticket(13L, true, PlaybackService.ACCESS_FREE, "/files/ep13.mp4?s=1"));
|
||||
|
||||
mockMvc.perform(get("/app/episodes/13/play").header("X-Device-Id", "device-9"))
|
||||
.andExpect(status().isOk());
|
||||
|
||||
verify(playbackService).play(isNull(), eq(13L), eq("device-9"), any());
|
||||
}
|
||||
|
||||
private PlayRespVO ticket(Long episodeId, boolean accessible, String accessType, String playUrl) {
|
||||
PlayRespVO vo = new PlayRespVO();
|
||||
vo.setEpisodeId(episodeId);
|
||||
vo.setDramaId(7L);
|
||||
vo.setNumber(1);
|
||||
vo.setTitle("第 1 集");
|
||||
vo.setDurationSec(120);
|
||||
vo.setAccessible(accessible);
|
||||
vo.setAccessType(accessType);
|
||||
vo.setPlayUrl(playUrl);
|
||||
vo.setQualities(List.of("AUTO"));
|
||||
vo.setUnlockCoin(0);
|
||||
vo.setPreviewSeconds(0);
|
||||
return vo;
|
||||
}
|
||||
}
|
||||
+320
@@ -0,0 +1,320 @@
|
||||
// Author: fruit.com.cn
|
||||
package com.fruitshorts.app.service;
|
||||
|
||||
import com.fruitshorts.common.exception.ErrorCode;
|
||||
import com.fruitshorts.common.exception.ServiceException;
|
||||
import com.fruitshorts.framework.security.JwtTokenProvider;
|
||||
import com.fruitshorts.user.dal.entity.AppUser;
|
||||
import com.fruitshorts.user.dal.entity.AppUserCredential;
|
||||
import com.fruitshorts.user.dal.entity.AppUserOtp;
|
||||
import com.fruitshorts.user.dal.entity.AppUserSession;
|
||||
import com.fruitshorts.user.dal.mapper.AppUserConsentMapper;
|
||||
import com.fruitshorts.user.dal.mapper.AppUserCredentialMapper;
|
||||
import com.fruitshorts.user.dal.mapper.AppUserDeviceMapper;
|
||||
import com.fruitshorts.user.dal.mapper.AppUserMapper;
|
||||
import com.fruitshorts.user.dal.mapper.AppUserOtpMapper;
|
||||
import com.fruitshorts.user.dal.mapper.AppUserSessionMapper;
|
||||
import com.fruitshorts.user.enums.AppUserStatus;
|
||||
import com.fruitshorts.user.service.AppAuthService;
|
||||
import com.fruitshorts.user.service.AppOtpService;
|
||||
import com.fruitshorts.user.service.CredentialHasher;
|
||||
import com.fruitshorts.user.service.OtpSender;
|
||||
import com.fruitshorts.user.vo.LoginReqVO;
|
||||
import com.fruitshorts.user.vo.RegisterReqVO;
|
||||
import com.fruitshorts.user.vo.TokenRespVO;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* C 端注册 / 登录的**业务规则**测试:真 `AppAuthService` + 替身 Mapper,不连库。
|
||||
*
|
||||
* <p>覆盖几件接入层测不到的事:EMAIL 免码到底走没走验证码、密码强度、注册频控、
|
||||
* 账号重复、密码比对与封禁判断。验证码本身的校验规则在 `AppOtpServiceTest`。
|
||||
*/
|
||||
class AppAuthServiceTest {
|
||||
|
||||
private AppUserMapper userMapper;
|
||||
private AppUserCredentialMapper credentialMapper;
|
||||
private AppUserSessionMapper sessionMapper;
|
||||
private AppUserConsentMapper consentMapper;
|
||||
private AppUserDeviceMapper deviceMapper;
|
||||
private AppOtpService otpService;
|
||||
private CredentialHasher hasher;
|
||||
private JwtTokenProvider tokenProvider;
|
||||
private PasswordEncoder passwordEncoder;
|
||||
private AppAuthService authService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
userMapper = mock(AppUserMapper.class);
|
||||
credentialMapper = mock(AppUserCredentialMapper.class);
|
||||
sessionMapper = mock(AppUserSessionMapper.class);
|
||||
consentMapper = mock(AppUserConsentMapper.class);
|
||||
deviceMapper = mock(AppUserDeviceMapper.class);
|
||||
otpService = mock(AppOtpService.class);
|
||||
hasher = mock(CredentialHasher.class);
|
||||
tokenProvider = mock(JwtTokenProvider.class);
|
||||
passwordEncoder = mock(PasswordEncoder.class);
|
||||
authService = new AppAuthService(userMapper, credentialMapper, sessionMapper, consentMapper,
|
||||
deviceMapper, otpService, hasher, tokenProvider, passwordEncoder);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_register_without_otp_when_target_type_is_email() {
|
||||
stubHappyRegister();
|
||||
AtomicReference<AppUserCredential> saved = captureCredential();
|
||||
|
||||
TokenRespVO resp = authService.register(emailRegister("abcd1234", null), "203.0.113.8");
|
||||
|
||||
assertThat(resp.getAccessToken()).isEqualTo("access-1");
|
||||
assertThat(resp.getRefreshToken()).isEqualTo("refresh-1");
|
||||
assertThat(resp.isNewUser()).isTrue();
|
||||
// 免码注册不等于"已验证":邮箱可达性没验证过,所以先记 0,等日后点验证链接再置 1
|
||||
assertThat(saved.get().getVerified()).isZero();
|
||||
verify(otpService, never()).consume(anyString(), anyString(), anyString(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_consume_otp_when_email_register_carries_a_code() {
|
||||
stubHappyRegister();
|
||||
|
||||
authService.register(emailRegister("abcd1234", "654321"), "203.0.113.8");
|
||||
|
||||
verify(otpService).consume(eq("REGISTER"), eq("EMAIL"), eq("a@b.com"), eq("654321"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_consume_otp_when_target_type_is_phone() {
|
||||
stubHappyRegister();
|
||||
|
||||
authService.register(phoneRegister("+628123456789", "654321"), "203.0.113.8");
|
||||
|
||||
verify(otpService).consume(eq("REGISTER"), eq("PHONE"), eq("+628123456789"), eq("654321"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_reject_phone_register_when_no_code_was_requested() {
|
||||
AppAuthService serviceWithRealOtp = serviceWithRealOtpService(null);
|
||||
|
||||
assertThatThrownBy(() -> serviceWithRealOtp.register(phoneRegister("+628123456789", ""), "203.0.113.8"))
|
||||
.isInstanceOf(ServiceException.class)
|
||||
.hasMessage("请先获取验证码");
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_reject_phone_register_when_code_is_wrong() {
|
||||
AppUserOtp issued = new AppUserOtp();
|
||||
issued.setCodeHash("hash-of-654321");
|
||||
issued.setExpireAt(LocalDateTime.now().plusMinutes(5));
|
||||
issued.setFailCount(0);
|
||||
AppAuthService serviceWithRealOtp = serviceWithRealOtpService(issued);
|
||||
|
||||
assertThatThrownBy(() -> serviceWithRealOtp.register(phoneRegister("+628123456789", "000000"), "203.0.113.8"))
|
||||
.isInstanceOf(ServiceException.class)
|
||||
.hasMessage("验证码不正确");
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_reject_email_register_when_password_is_weak() {
|
||||
RegisterReqVO req = emailRegister("abcdefgh", null);
|
||||
|
||||
assertThatThrownBy(() -> authService.register(req, "203.0.113.8"))
|
||||
.isInstanceOf(ServiceException.class)
|
||||
.hasMessageContaining("密码需同时包含字母和数字");
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_reject_email_register_when_format_is_invalid() {
|
||||
RegisterReqVO req = emailRegister("abcd1234", null);
|
||||
req.setTarget("not-an-email");
|
||||
|
||||
assertThatThrownBy(() -> authService.register(req, "203.0.113.8"))
|
||||
.isInstanceOf(ServiceException.class)
|
||||
.hasMessage("邮箱格式不正确");
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_return_data_exists_when_email_is_already_registered() {
|
||||
given(userMapper.selectCount(any())).willReturn(0L);
|
||||
AppUserCredential existing = new AppUserCredential();
|
||||
existing.setUserId(1001L);
|
||||
given(credentialMapper.selectOne(any())).willReturn(existing);
|
||||
|
||||
assertThatThrownBy(() -> authService.register(emailRegister("abcd1234", null), "203.0.113.8"))
|
||||
.isInstanceOf(ServiceException.class)
|
||||
.satisfies(ex -> assertThat(((ServiceException) ex).getCode())
|
||||
.isEqualTo(ErrorCode.DATA_EXISTS.getCode()))
|
||||
.hasMessage("该账号已注册,请直接登录");
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_reject_register_when_ip_reaches_the_daily_limit() {
|
||||
given(userMapper.selectCount(any())).willReturn(5L);
|
||||
|
||||
assertThatThrownBy(() -> authService.register(emailRegister("abcd1234", null), "203.0.113.8"))
|
||||
.isInstanceOf(ServiceException.class)
|
||||
.satisfies(ex -> assertThat(((ServiceException) ex).getCode())
|
||||
.isEqualTo(ErrorCode.TOO_MANY_REQUESTS.getCode()))
|
||||
.hasMessageContaining("当前网络注册过于频繁");
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_return_tokens_when_password_matches() {
|
||||
stubLogin(credential("bcrypt-hash"), user(AppUserStatus.NORMAL));
|
||||
given(passwordEncoder.matches("abcd1234", "bcrypt-hash")).willReturn(true);
|
||||
|
||||
TokenRespVO resp = authService.login(emailLogin("abcd1234"), "203.0.113.8");
|
||||
|
||||
assertThat(resp.getAccessToken()).isEqualTo("access-1");
|
||||
assertThat(resp.getUser().getUid()).isEqualTo("UAB12CD");
|
||||
verify(sessionMapper).insert(any(AppUserSession.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_reject_login_when_password_does_not_match() {
|
||||
stubLogin(credential("bcrypt-hash"), user(AppUserStatus.NORMAL));
|
||||
given(passwordEncoder.matches("wrongpass1", "bcrypt-hash")).willReturn(false);
|
||||
|
||||
assertThatThrownBy(() -> authService.login(emailLogin("wrongpass1"), "203.0.113.8"))
|
||||
.isInstanceOf(ServiceException.class)
|
||||
.satisfies(ex -> assertThat(((ServiceException) ex).getCode())
|
||||
.isEqualTo(ErrorCode.LOGIN_FAILED.getCode()))
|
||||
.hasMessage("账号或密码错误");
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_reject_login_when_account_does_not_exist() {
|
||||
given(credentialMapper.selectOne(any())).willReturn(null);
|
||||
|
||||
assertThatThrownBy(() -> authService.login(emailLogin("abcd1234"), "203.0.113.8"))
|
||||
.isInstanceOf(ServiceException.class)
|
||||
.satisfies(ex -> assertThat(((ServiceException) ex).getCode())
|
||||
.isEqualTo(ErrorCode.LOGIN_FAILED.getCode()))
|
||||
.hasMessage("账号不存在,请先注册");
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_reject_login_when_account_is_banned() {
|
||||
stubLogin(credential("bcrypt-hash"), user(AppUserStatus.BANNED));
|
||||
given(passwordEncoder.matches("abcd1234", "bcrypt-hash")).willReturn(true);
|
||||
|
||||
assertThatThrownBy(() -> authService.login(emailLogin("abcd1234"), "203.0.113.8"))
|
||||
.isInstanceOf(ServiceException.class)
|
||||
.satisfies(ex -> assertThat(((ServiceException) ex).getCode())
|
||||
.isEqualTo(ErrorCode.ACCOUNT_DISABLED.getCode()));
|
||||
}
|
||||
|
||||
private void stubHappyRegister() {
|
||||
given(userMapper.selectCount(any())).willReturn(0L);
|
||||
given(credentialMapper.selectOne(any())).willReturn(null);
|
||||
given(hasher.newUid()).willReturn("UAB12CD");
|
||||
given(hasher.newRefreshToken()).willReturn("refresh-1");
|
||||
given(hasher.sha256(anyString())).willReturn("hash-1");
|
||||
given(passwordEncoder.encode(anyString())).willReturn("bcrypt-hash");
|
||||
given(tokenProvider.createAppToken(any(), anyString(), any())).willReturn("access-1");
|
||||
given(tokenProvider.accessTtlSeconds()).willReturn(7200L);
|
||||
given(tokenProvider.appRefreshTtl()).willReturn(Duration.ofDays(30));
|
||||
AtomicReference<AppUser> inserted = new AtomicReference<>();
|
||||
given(userMapper.insert(any(AppUser.class))).willAnswer(invocation -> {
|
||||
AppUser saved = invocation.getArgument(0);
|
||||
saved.setId(1001L);
|
||||
inserted.set(saved);
|
||||
return 1;
|
||||
});
|
||||
given(userMapper.selectById(any())).willAnswer(invocation -> inserted.get());
|
||||
}
|
||||
|
||||
private AtomicReference<AppUserCredential> captureCredential() {
|
||||
AtomicReference<AppUserCredential> saved = new AtomicReference<>();
|
||||
given(credentialMapper.insert(any(AppUserCredential.class))).willAnswer(invocation -> {
|
||||
saved.set(invocation.getArgument(0));
|
||||
return 1;
|
||||
});
|
||||
return saved;
|
||||
}
|
||||
|
||||
private void stubLogin(AppUserCredential credential, AppUser user) {
|
||||
given(credentialMapper.selectOne(any())).willReturn(credential);
|
||||
given(userMapper.selectById(any())).willReturn(user);
|
||||
given(hasher.newRefreshToken()).willReturn("refresh-1");
|
||||
given(hasher.sha256(anyString())).willReturn("hash-1");
|
||||
given(tokenProvider.createAppToken(any(), anyString(), any())).willReturn("access-1");
|
||||
given(tokenProvider.accessTtlSeconds()).willReturn(7200L);
|
||||
given(tokenProvider.appRefreshTtl()).willReturn(Duration.ofDays(30));
|
||||
}
|
||||
|
||||
private AppAuthService serviceWithRealOtpService(AppUserOtp issued) {
|
||||
AppUserOtpMapper otpMapper = mock(AppUserOtpMapper.class);
|
||||
CredentialHasher otpHasher = mock(CredentialHasher.class);
|
||||
given(otpMapper.selectOne(any())).willReturn(issued);
|
||||
given(otpHasher.hashOtp(any())).willReturn("hash-of-something-else");
|
||||
AppOtpService realOtpService = new AppOtpService(otpMapper, mock(OtpSender.class), otpHasher);
|
||||
return new AppAuthService(userMapper, credentialMapper, sessionMapper, consentMapper,
|
||||
deviceMapper, realOtpService, hasher, tokenProvider, passwordEncoder);
|
||||
}
|
||||
|
||||
private RegisterReqVO emailRegister(String password, String code) {
|
||||
RegisterReqVO req = new RegisterReqVO();
|
||||
req.setTargetType("EMAIL");
|
||||
req.setTarget("a@b.com");
|
||||
req.setPassword(password);
|
||||
req.setCode(code);
|
||||
req.setDeviceId("device-9");
|
||||
req.setAgreementVersion("2026-10-01");
|
||||
return req;
|
||||
}
|
||||
|
||||
private RegisterReqVO phoneRegister(String target, String code) {
|
||||
RegisterReqVO req = new RegisterReqVO();
|
||||
req.setTargetType("PHONE");
|
||||
req.setTarget(target);
|
||||
req.setCode(code);
|
||||
req.setDeviceId("device-9");
|
||||
req.setAgreementVersion("2026-10-01");
|
||||
return req;
|
||||
}
|
||||
|
||||
private LoginReqVO emailLogin(String password) {
|
||||
LoginReqVO req = new LoginReqVO();
|
||||
req.setTargetType("EMAIL");
|
||||
req.setTarget("a@b.com");
|
||||
req.setPassword(password);
|
||||
req.setDeviceId("device-9");
|
||||
return req;
|
||||
}
|
||||
|
||||
private AppUserCredential credential(String passwordHash) {
|
||||
AppUserCredential credential = new AppUserCredential();
|
||||
credential.setId(1L);
|
||||
credential.setUserId(1001L);
|
||||
credential.setType("EMAIL");
|
||||
credential.setIdentifier("a@b.com");
|
||||
credential.setPasswordHash(passwordHash);
|
||||
return credential;
|
||||
}
|
||||
|
||||
private AppUser user(String status) {
|
||||
AppUser user = new AppUser();
|
||||
user.setId(1001L);
|
||||
user.setUid("UAB12CD");
|
||||
user.setNickname("UserAB12CD");
|
||||
user.setStatus(status);
|
||||
user.setCountryCode("ID");
|
||||
return user;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
// Author: fruit.com.cn
|
||||
package com.fruitshorts.app.service;
|
||||
|
||||
import com.fruitshorts.common.exception.ErrorCode;
|
||||
import com.fruitshorts.common.exception.ServiceException;
|
||||
import com.fruitshorts.user.dal.entity.AppUserOtp;
|
||||
import com.fruitshorts.user.dal.mapper.AppUserOtpMapper;
|
||||
import com.fruitshorts.user.service.AppOtpService;
|
||||
import com.fruitshorts.user.service.CredentialHasher;
|
||||
import com.fruitshorts.user.service.OtpSender;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* 验证码**校验规则**测试:真 `AppOtpService` + 替身 Mapper。
|
||||
*
|
||||
* <p>注册/登录链路"PHONE 必须带验证码"的拒绝对外表现,靠这里的真规则 + `AppAuthServiceTest`
|
||||
* 里的"必须调用 consume"两条一起钉住,避免两边都用替身导致互相假装。
|
||||
*/
|
||||
class AppOtpServiceTest {
|
||||
|
||||
private AppUserOtpMapper otpMapper;
|
||||
private CredentialHasher hasher;
|
||||
private AppOtpService otpService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
otpMapper = mock(AppUserOtpMapper.class);
|
||||
hasher = mock(CredentialHasher.class);
|
||||
otpService = new AppOtpService(otpMapper, mock(OtpSender.class), hasher);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_reject_consume_when_no_code_was_requested() {
|
||||
given(otpMapper.selectOne(any())).willReturn(null);
|
||||
|
||||
assertThatThrownBy(() -> otpService.consume("REGISTER", "PHONE", "+628123456789", "123456"))
|
||||
.isInstanceOf(ServiceException.class)
|
||||
.satisfies(ex -> assertThat(((ServiceException) ex).getCode())
|
||||
.isEqualTo(ErrorCode.BAD_REQUEST.getCode()))
|
||||
.hasMessage("请先获取验证码");
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_reject_consume_when_code_is_wrong() {
|
||||
AppUserOtp issued = issuedOtp("hash-of-123456");
|
||||
given(otpMapper.selectOne(any())).willReturn(issued);
|
||||
given(hasher.hashOtp("000000")).willReturn("hash-of-000000");
|
||||
|
||||
assertThatThrownBy(() -> otpService.consume("REGISTER", "PHONE", "+628123456789", "000000"))
|
||||
.isInstanceOf(ServiceException.class)
|
||||
.hasMessage("验证码不正确");
|
||||
// 失败要计数,连续错 5 次就作废(防爆破)
|
||||
assertThat(issued.getFailCount()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_reject_consume_when_code_is_expired() {
|
||||
AppUserOtp issued = issuedOtp("hash-of-123456");
|
||||
issued.setExpireAt(LocalDateTime.now().minusMinutes(1));
|
||||
given(otpMapper.selectOne(any())).willReturn(issued);
|
||||
|
||||
assertThatThrownBy(() -> otpService.consume("REGISTER", "PHONE", "+628123456789", "123456"))
|
||||
.isInstanceOf(ServiceException.class)
|
||||
.hasMessage("验证码已过期");
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_mark_code_used_when_code_matches() {
|
||||
AppUserOtp issued = issuedOtp("hash-of-123456");
|
||||
AtomicReference<AppUserOtp> updated = new AtomicReference<>();
|
||||
given(otpMapper.selectOne(any())).willReturn(issued);
|
||||
given(hasher.hashOtp("123456")).willReturn("hash-of-123456");
|
||||
given(otpMapper.updateById(any(AppUserOtp.class))).willAnswer(invocation -> {
|
||||
updated.set(invocation.getArgument(0));
|
||||
return 1;
|
||||
});
|
||||
|
||||
otpService.consume("REGISTER", "PHONE", "+628123456789", "123456");
|
||||
|
||||
assertThat(updated.get().getUsedAt()).isNotNull();
|
||||
}
|
||||
|
||||
private AppUserOtp issuedOtp(String codeHash) {
|
||||
AppUserOtp otp = new AppUserOtp();
|
||||
otp.setId(1L);
|
||||
otp.setScene("REGISTER");
|
||||
otp.setTargetType("PHONE");
|
||||
otp.setTarget("+628123456789");
|
||||
otp.setCodeHash(codeHash);
|
||||
otp.setExpireAt(LocalDateTime.now().plusMinutes(5));
|
||||
otp.setFailCount(0);
|
||||
return otp;
|
||||
}
|
||||
}
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
// Author: fruit.com.cn
|
||||
package com.fruitshorts.app.service;
|
||||
|
||||
import com.fruitshorts.content.dal.entity.Drama;
|
||||
import com.fruitshorts.content.dal.entity.DramaEpisode;
|
||||
import com.fruitshorts.content.service.DramaService;
|
||||
import com.fruitshorts.content.service.EpisodeService;
|
||||
import com.fruitshorts.media.service.PlaybackSigner;
|
||||
import com.fruitshorts.media.service.TranscodeService;
|
||||
import com.fruitshorts.playback.dal.entity.PlaybackTicket;
|
||||
import com.fruitshorts.playback.dal.mapper.PlaybackTicketMapper;
|
||||
import com.fruitshorts.playback.service.EntitlementPort;
|
||||
import com.fruitshorts.playback.service.PlaybackService;
|
||||
import com.fruitshorts.playback.vo.PlayRespVO;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* 播放票据的**判定规则**测试:真 `PlaybackService` + 替身依赖,不连库。
|
||||
*
|
||||
* <p>两条关键分支(接入层只验证字段形态,判定在这里):免费集一律给签名地址;
|
||||
* 付费集在没权益时**不给地址**,只回 `COIN_REQUIRED` 与所需金币。
|
||||
*/
|
||||
class PlaybackServiceTest {
|
||||
|
||||
private EpisodeService episodeService;
|
||||
private DramaService dramaService;
|
||||
private PlaybackTicketMapper ticketMapper;
|
||||
private PlaybackSigner signer;
|
||||
private EntitlementPort entitlementPort;
|
||||
private TranscodeService transcodeService;
|
||||
private PlaybackService playbackService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
episodeService = mock(EpisodeService.class);
|
||||
dramaService = mock(DramaService.class);
|
||||
ticketMapper = mock(PlaybackTicketMapper.class);
|
||||
signer = mock(PlaybackSigner.class);
|
||||
entitlementPort = mock(EntitlementPort.class);
|
||||
transcodeService = mock(TranscodeService.class);
|
||||
playbackService = new PlaybackService(episodeService, dramaService, ticketMapper, signer,
|
||||
entitlementPort, transcodeService);
|
||||
given(dramaService.getPublished(anyLong())).willReturn(drama());
|
||||
given(transcodeService.listRenditions(anyLong())).willReturn(List.of());
|
||||
given(signer.sign(anyString())).willReturn("/files/episode/1.mp4?e=1&s=sig");
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_return_free_access_when_episode_is_free() {
|
||||
given(episodeService.getPublished(11L)).willReturn(episode(11L, 1, 0, "/files/episode/1.mp4"));
|
||||
|
||||
PlayRespVO resp = playbackService.play(null, 11L, "device-9", "203.0.113.8");
|
||||
|
||||
assertThat(resp.getAccessible()).isTrue();
|
||||
assertThat(resp.getAccessType()).isEqualTo(PlaybackService.ACCESS_FREE);
|
||||
assertThat(resp.getPlayUrl()).isEqualTo("/files/episode/1.mp4?e=1&s=sig");
|
||||
// 未登录不记票据(票据是"已登录用户"的签发审计)
|
||||
verify(ticketMapper, never()).insert(any(PlaybackTicket.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_return_coin_required_when_episode_is_locked_and_not_entitled() {
|
||||
given(episodeService.getPublished(12L)).willReturn(episode(12L, 0, 60, "/files/episode/2.mp4"));
|
||||
given(entitlementPort.isUnlocked(anyLong(), anyLong(), anyLong())).willReturn(false);
|
||||
|
||||
PlayRespVO resp = playbackService.play(1001L, 12L, "device-9", "203.0.113.8");
|
||||
|
||||
assertThat(resp.getAccessible()).isFalse();
|
||||
assertThat(resp.getAccessType()).isEqualTo(PlaybackService.ACCESS_COIN_REQUIRED);
|
||||
assertThat(resp.getPlayUrl()).isEmpty();
|
||||
assertThat(resp.getUnlockCoin()).isEqualTo(60);
|
||||
verify(ticketMapper, never()).insert(any(PlaybackTicket.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_return_entitled_access_when_entitlement_port_confirms_unlock() {
|
||||
given(episodeService.getPublished(13L)).willReturn(episode(13L, 0, 60, "/files/episode/3.mp4"));
|
||||
given(entitlementPort.isUnlocked(eq(1001L), anyLong(), eq(13L))).willReturn(true);
|
||||
|
||||
PlayRespVO resp = playbackService.play(1001L, 13L, "device-9", "203.0.113.8");
|
||||
|
||||
assertThat(resp.getAccessible()).isTrue();
|
||||
assertThat(resp.getAccessType()).isEqualTo(PlaybackService.ACCESS_ENTITLED);
|
||||
assertThat(resp.getPlayUrl()).isEqualTo("/files/episode/1.mp4?e=1&s=sig");
|
||||
verify(ticketMapper).insert(any(PlaybackTicket.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_return_no_media_when_episode_has_no_source() {
|
||||
given(episodeService.getPublished(14L)).willReturn(episode(14L, 1, 0, ""));
|
||||
|
||||
PlayRespVO resp = playbackService.play(isNull(), 14L, "device-9", "203.0.113.8");
|
||||
|
||||
assertThat(resp.getAccessible()).isFalse();
|
||||
assertThat(resp.getAccessType()).isEqualTo(PlaybackService.ACCESS_NO_MEDIA);
|
||||
assertThat(resp.getPlayUrl()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_prefer_hls_url_when_the_episode_has_multi_bitrate_renditions() {
|
||||
DramaEpisode episode = episode(15L, 1, 0, "/files/episode/origin.mp4");
|
||||
episode.setHlsUrl("/files/episode/15/master.m3u8");
|
||||
given(episodeService.getPublished(15L)).willReturn(episode);
|
||||
|
||||
PlayRespVO resp = playbackService.play(null, 15L, "device-9", "203.0.113.8");
|
||||
|
||||
assertThat(resp.getPlayUrl()).isEqualTo("/files/episode/1.mp4?e=1&s=sig");
|
||||
verify(signer).sign("/files/episode/15/master.m3u8");
|
||||
}
|
||||
|
||||
private DramaEpisode episode(Long id, int isFree, int unlockCoin, String videoUrl) {
|
||||
DramaEpisode episode = new DramaEpisode();
|
||||
episode.setId(id);
|
||||
episode.setDramaId(7L);
|
||||
episode.setEpNo(1);
|
||||
episode.setTitle("第 1 集");
|
||||
episode.setDurationSec(120);
|
||||
episode.setIsFree(isFree);
|
||||
episode.setUnlockCoin(unlockCoin);
|
||||
episode.setPreviewSeconds(0);
|
||||
episode.setVideoUrl(videoUrl);
|
||||
episode.setStatus("published");
|
||||
return episode;
|
||||
}
|
||||
|
||||
private Drama drama() {
|
||||
Drama drama = new Drama();
|
||||
drama.setId(7L);
|
||||
drama.setTitle("命运的相遇");
|
||||
return drama;
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
// Author: fruit.com.cn
|
||||
package com.fruitshorts.app.support;
|
||||
|
||||
import com.fruitshorts.framework.log.AccessDeniedRecorder;
|
||||
import com.fruitshorts.framework.web.GlobalExceptionHandler;
|
||||
import org.mockito.Mockito;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
|
||||
/**
|
||||
* 控制器契约测试的公共装配:真控制器 + 真全局异常处理器 + 真校验器,**不启 Spring 容器**。
|
||||
*
|
||||
* <p>为什么不用 {@code @WebMvcTest}:{@code AppApiApplication} 上挂着
|
||||
* {@code @MapperScan("com.fruitshorts.**.dal.mapper")},而切片测试不装配 DataSource / MyBatis,
|
||||
* 扫描出的 Mapper 工厂会因为缺少 {@code SqlSessionFactory} 直接报错;要绕开就得再加一套测试专用的
|
||||
* 引导配置。这里的 standaloneSetup 同样只装 Web 层(路由、参数绑定、消息转换、异常处理),
|
||||
* 覆盖面不变而更轻、更稳。
|
||||
*/
|
||||
public final class ControllerContractSupport {
|
||||
|
||||
private ControllerContractSupport() {
|
||||
}
|
||||
|
||||
/** 组装一个只含指定控制器与全局异常处理的 MockMvc。 */
|
||||
@SuppressWarnings("unchecked")
|
||||
public static MockMvc mockMvcOf(Object controller) {
|
||||
AccessDeniedRecorder recorder = new AccessDeniedRecorder(Mockito.mock(ObjectProvider.class));
|
||||
return MockMvcBuilders.standaloneSetup(controller)
|
||||
.setControllerAdvice(new GlobalExceptionHandler(recorder))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -14,7 +14,7 @@ import java.time.LocalDateTime;
|
||||
@TableName("coin_ledger")
|
||||
public class CoinLedger {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private Long userId;
|
||||
/** 正数入账、负数出账 */
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ import java.time.LocalDateTime;
|
||||
@TableName("entitlement")
|
||||
public class Entitlement {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private Long userId;
|
||||
private Long dramaId;
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ import java.time.LocalDateTime;
|
||||
@TableName("order_event")
|
||||
public class OrderEvent {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private String orderNo;
|
||||
private String event;
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ import java.time.LocalDateTime;
|
||||
@TableName("orders")
|
||||
public class Orders {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private String orderNo;
|
||||
private Long userId;
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ import java.time.LocalDateTime;
|
||||
@TableName("product")
|
||||
public class Product {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private String code;
|
||||
private String name;
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ import java.time.LocalDateTime;
|
||||
@TableName("reconcile_task")
|
||||
public class ReconcileTask {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private LocalDate bizDate;
|
||||
private String platform;
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ import java.time.LocalDateTime;
|
||||
@TableName("store_receipt")
|
||||
public class StoreReceipt {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private String platform;
|
||||
private String transactionId;
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ import java.time.LocalDateTime;
|
||||
@TableName("wallet")
|
||||
public class Wallet {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private Long userId;
|
||||
private Long coinBalance;
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ import java.time.LocalDateTime;
|
||||
@TableName("content_banner")
|
||||
public class ContentBanner {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private String imageUrl;
|
||||
/** 展示位置,列名为 slot(position 在 PostgreSQL 里是关键字) */
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ import java.time.LocalDateTime;
|
||||
@TableName("drama")
|
||||
public class Drama {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private String code;
|
||||
private String title;
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ import lombok.Data;
|
||||
@TableName("drama_category")
|
||||
public class DramaCategory {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private String name;
|
||||
private Integer sort;
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ import java.time.LocalDateTime;
|
||||
@TableName("drama_episode")
|
||||
public class DramaEpisode {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private Long dramaId;
|
||||
private Integer epNo;
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ import java.time.LocalDateTime;
|
||||
@TableName("drama_review_record")
|
||||
public class DramaReviewRecord {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private String bizType;
|
||||
private Long bizId;
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ import java.time.LocalDateTime;
|
||||
@TableName("content_submission")
|
||||
public class ContentSubmission {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private Long creatorId;
|
||||
private Long dramaId;
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ import java.time.LocalDateTime;
|
||||
@TableName("creator")
|
||||
public class Creator {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
/** 关联 C 端账号 app_user.id */
|
||||
private Long userId;
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ import java.time.LocalDateTime;
|
||||
@TableName("creator_document")
|
||||
public class CreatorDocument {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private Long creatorId;
|
||||
/** ID_CARD / BUSINESS_LICENSE / CONTRACT */
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ import java.time.LocalDateTime;
|
||||
@TableName("content_audit_record")
|
||||
public class ContentAuditRecord {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
/** COMMENT/DRAMA/AVATAR/CREATOR */
|
||||
private String bizType;
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ import java.time.LocalDateTime;
|
||||
@TableName("content_comment")
|
||||
public class ContentComment {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private Long dramaId;
|
||||
private Long episodeId;
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ import java.time.LocalDateTime;
|
||||
@TableName("content_like")
|
||||
public class ContentLike {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private Long userId;
|
||||
/** DRAMA/EPISODE/COMMENT */
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ import java.time.LocalDateTime;
|
||||
@TableName("content_report")
|
||||
public class ContentReport {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private Long reporterId;
|
||||
/** COMMENT/USER/DRAMA */
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ import java.time.LocalDateTime;
|
||||
@TableName("content_share")
|
||||
public class ContentShare {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private Long userId;
|
||||
private Long dramaId;
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ import java.time.LocalDateTime;
|
||||
@TableName("sensitive_word")
|
||||
public class SensitiveWord {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private String word;
|
||||
/** POLITICS/PORN/ABUSE/AD/RELIGION */
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ import java.time.LocalDateTime;
|
||||
@TableName("user_blocklist")
|
||||
public class UserBlocklist {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private Long userId;
|
||||
private Long blockedUserId;
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ import java.time.LocalDateTime;
|
||||
@TableName("media_asset")
|
||||
public class MediaAsset {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private String bizType;
|
||||
private String fileName;
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ import java.time.LocalDateTime;
|
||||
@TableName("media_rendition")
|
||||
public class MediaRendition {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private Long mediaAssetId;
|
||||
private Long dramaId;
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ import java.time.LocalDateTime;
|
||||
@TableName("media_upload_task")
|
||||
public class MediaUploadTask {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private String uploadId;
|
||||
private String bizType;
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ import java.time.LocalDateTime;
|
||||
@TableName("transcode_task")
|
||||
public class TranscodeTask {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private Long mediaAssetId;
|
||||
private Long dramaId;
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ import java.time.LocalDateTime;
|
||||
@TableName("download_license")
|
||||
public class DownloadLicense {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private Long userId;
|
||||
private Long dramaId;
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ import java.time.LocalDateTime;
|
||||
@TableName("download_task")
|
||||
public class DownloadTask {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private Long userId;
|
||||
private String deviceId;
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ import java.time.LocalDateTime;
|
||||
@TableName("playback_ticket")
|
||||
public class PlaybackTicket {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private String ticketNo;
|
||||
private Long userId;
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ import java.time.LocalDateTime;
|
||||
@TableName("user_favorite")
|
||||
public class UserFavorite {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private Long userId;
|
||||
private Long dramaId;
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ import java.time.LocalDateTime;
|
||||
@TableName("watch_history")
|
||||
public class WatchHistory {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private Long userId;
|
||||
private Long dramaId;
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ import java.time.LocalDateTime;
|
||||
@TableName("watch_progress")
|
||||
public class WatchProgress {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private Long userId;
|
||||
private Long dramaId;
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ import java.time.LocalDateTime;
|
||||
@TableName("sys_admin_user")
|
||||
public class AdminUser {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private String username;
|
||||
private String password;
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ import lombok.NoArgsConstructor;
|
||||
@TableName("sys_admin_user_role")
|
||||
public class AdminUserRole {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private Long userId;
|
||||
private Long roleId;
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ import java.time.LocalDateTime;
|
||||
@TableName("audit_log")
|
||||
public class AuditLog {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
/** ORDER / ENTITLEMENT / USER / CREATOR / PRICE */
|
||||
private String bizType;
|
||||
|
||||
@@ -15,7 +15,7 @@ import java.time.LocalDateTime;
|
||||
@TableName("sys_menu")
|
||||
public class Menu {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private Long parentId;
|
||||
private String title;
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ import java.time.LocalDateTime;
|
||||
@TableName("sys_operation_log")
|
||||
public class OperationLog {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private Long adminUserId;
|
||||
private String username;
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ import lombok.Data;
|
||||
@TableName("sys_permission")
|
||||
public class Permission {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private String code;
|
||||
private String name;
|
||||
|
||||
@@ -16,7 +16,7 @@ import java.time.LocalDateTime;
|
||||
@TableName("sys_role")
|
||||
public class Role {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private String name;
|
||||
private String code;
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ import java.time.LocalDateTime;
|
||||
@TableName("sys_role_category")
|
||||
public class RoleCategory {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private String name;
|
||||
private String description;
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ import lombok.NoArgsConstructor;
|
||||
@TableName("sys_role_menu")
|
||||
public class RoleMenu {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private Long roleId;
|
||||
private Long menuId;
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ import lombok.NoArgsConstructor;
|
||||
@TableName("sys_role_permission")
|
||||
public class RolePermission {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private Long roleId;
|
||||
private Long permissionId;
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ import lombok.Data;
|
||||
@TableName("sys_config")
|
||||
public class SysConfig {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private String configKey;
|
||||
private String configValue;
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ import java.time.LocalDateTime;
|
||||
@TableName("app_account_deletion")
|
||||
public class AppAccountDeletion {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private Long userId;
|
||||
/** pending/cancelled/completed */
|
||||
|
||||
@@ -18,7 +18,7 @@ import java.time.LocalDateTime;
|
||||
@TableName("app_user")
|
||||
public class AppUser {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
/** 对外用户标识,接口只暴露 uid,不暴露自增 ID */
|
||||
private String uid;
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ import java.time.LocalDateTime;
|
||||
@TableName("app_user_ban_record")
|
||||
public class AppUserBanRecord {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private Long userId;
|
||||
/** BAN/LIMIT/MUTE */
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ import java.time.LocalDateTime;
|
||||
@TableName("app_user_consent")
|
||||
public class AppUserConsent {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private Long userId;
|
||||
private String scene;
|
||||
|
||||
Loaded 100 of 113 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user