feat: 接口字段补 @Schema 中文说明 + 精简冗余字段 + 接入 Prometheus 监控

- 全部 VO 字段补 @Schema(description) 中文说明,在线文档字段可读
- AppEpisodeVO 精简 duration/isFree 冗余字段(保留 durationSec/locked)
- framework 引入 actuator + micrometer-registry-prometheus
- SecurityConfig 放行 /actuator/prometheus,双服务配置 management 暴露指标
- 新增 deploy/docker-compose.yml + prometheus.yml 监控部署配置
This commit is contained in:
Zhou Weidong committed 2026-10-17 02:24:25 +08:00
1 parent efe8af6c9b
commit 5f9483a86c
140 files changed
+1357 -19

No files matched your search

@@ -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=true 时吊销全部设备会话)")
@PostMapping("/logout")
public Result<Void> logout(@RequestBody(required = false) LogoutReqVO req) {
authService.logout(SecurityUtils.getUserIdOrNull(), req == null ? new LogoutReqVO() : req);
@@ -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());
@@ -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 = "评论列表(游标翻页;不传 rootId 为顶层评论,传了为楼层回复)")
@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);
@@ -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 = "提交商店票据(IAP/Google Play;商店未接入时返回明确业务提示)")
@PostMapping("/orders/verify")
public Result<Orders> verify(@RequestBody Map<String, String> body) {
return Result.ok(orderService.verifyAndDeliver(SecurityUtils.getAppUserId(),
@@ -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 = "更新下载偏好(仅 Wi-Fi 下载、最高清晰度)")
@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) {
@@ -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(
@@ -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);
@@ -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());
@@ -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());
@@ -168,10 +168,7 @@ public class AppCatalogService {
vo.setNumber(episode.getEpNo());
vo.setTitle(episode.getTitle());
vo.setDurationSec(episode.getDurationSec());
int seconds = episode.getDurationSec() == null ? 0 : episode.getDurationSec();
vo.setDuration(String.format("%02d:%02d", seconds / 60, seconds % 60));
boolean free = episode.getIsFree() != null && episode.getIsFree() == 1;
vo.setIsFree(free);
vo.setLocked(!free);
vo.setUnlockCoin(episode.getUnlockCoin());
if (free) {
@@ -2,15 +2,21 @@
package com.fruitshorts.app.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class AppBannerVO {
@Schema(description = "轮播图 ID")
private Long id;
@Schema(description = "图片地址")
private String imageUrl;
/** NONE / DRAMA / URL */
@Schema(description = "跳转类型:NONE / DRAMA / URL")
private String linkType;
/** 短剧 ID 或跳转地址 */
@Schema(description = "跳转目标:短剧 ID 或跳转地址")
private String linkValue;
}
@@ -2,6 +2,7 @@
package com.fruitshorts.app.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
@@ -9,6 +10,9 @@ import lombok.Data;
@AllArgsConstructor
public class AppCategoryVO {
@Schema(description = "分类 ID")
private Long id;
@Schema(description = "分类名称")
private String name;
}
@@ -2,24 +2,40 @@
package com.fruitshorts.app.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/** 列表卡片,用于首页/发现页/分类分区 */
@Data
public class AppDramaCardVO {
@Schema(description = "短剧 ID")
private Long id;
@Schema(description = "短剧标题")
private String title;
@Schema(description = "封面地址")
private String cover;
@Schema(description = "简介")
private String synopsis;
@Schema(description = "题材")
private String genre;
@Schema(description = "分类名称")
private String category;
@Schema(description = "总集数")
private Integer totalEpisodes;
@Schema(description = "是否热门")
private Boolean isHot;
@Schema(description = "播放量")
private Long viewCount;
/**
* 首个可播免费集。首页竖屏播放需要在列表阶段就能起播,
* 带上这一集可以省掉「点开→请求详情→再请求播放地址」的往返。
*/
@Schema(description = "首个可播免费集;首页竖屏播放可直接起播,省去再请求详情/播放地址的往返")
private AppEpisodeVO preview;
}
@@ -2,6 +2,7 @@
package com.fruitshorts.app.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@@ -9,16 +10,39 @@ import java.util.List;
@Data
public class AppDramaDetailVO {
@Schema(description = "短剧 ID")
private Long id;
@Schema(description = "短剧标题")
private String title;
@Schema(description = "简介")
private String synopsis;
@Schema(description = "封面地址")
private String cover;
@Schema(description = "题材")
private String genre;
@Schema(description = "分类名称")
private String category;
@Schema(description = "总集数")
private Integer totalEpisodes;
@Schema(description = "免费集数")
private Integer freeEpisodes;
@Schema(description = "单集解锁所需金币数")
private Integer unlockCoin;
@Schema(description = "是否热门")
private Boolean isHot;
@Schema(description = "播放量")
private Long viewCount;
@Schema(description = "剧集列表")
private List<AppEpisodeVO> episodes;
}
@@ -2,24 +2,30 @@
package com.fruitshorts.app.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class AppEpisodeVO {
@Schema(description = "剧集 ID")
private Long id;
@Schema(description = "集号(第几集)")
private Integer number;
@Schema(description = "剧集标题")
private String title;
/** 展示用时长 mm:ss */
private String duration;
@Schema(description = "时长(秒)")
private Integer durationSec;
private Boolean isFree;
/** true 表示需要解锁后才能播放 */
@Schema(description = "是否锁定,true 表示需要解锁后才能播放")
private Boolean locked;
@Schema(description = "解锁所需金币数")
private Integer unlockCoin;
/**
* 可播放时的地址;锁定或不含片源时为空。
* 详情页拿到就能直接播,不必按集再请求一次播放票据。
*/
@Schema(description = "可播放地址;锁定或无片源时为空")
private String playUrl;
}
@@ -2,6 +2,7 @@
package com.fruitshorts.app.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
@@ -12,6 +13,9 @@ import java.util.List;
@AllArgsConstructor
public class AppFeedVO {
@Schema(description = "轮播图列表")
private List<AppBannerVO> banners;
@Schema(description = "分区列表")
private List<AppSectionVO> sections;
}
@@ -2,6 +2,7 @@
package com.fruitshorts.app.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
@@ -12,8 +13,12 @@ import java.util.List;
@AllArgsConstructor
public class AppSectionVO {
/** 分区标识:hot / new / category-{id},客户端可据此做埋点与样式 */
@Schema(description = "分区标识:hot / new / category-{id},客户端据此做埋点与样式")
private String code;
@Schema(description = "分区标题")
private String title;
@Schema(description = "分区内短剧列表")
private List<AppDramaCardVO> dramas;
}
@@ -61,3 +61,15 @@ fruitshorts:
logging:
level:
com.fruitshorts: info
management:
endpoints:
web:
exposure:
include: prometheus,health
endpoint:
health:
show-details: always
metrics:
tags:
application: ${spring.application.name}