Author SHA1 Message Date
Zhou Weidong 7dc4345060 feat: 印尼语敏感词库种子数据(id-ID 增量补全 78 词) 2026-10-17 09:36:07 +08:00
Zhou Weidong abea0a6c39 feat: 合规红线(提交审核接入敏感词检查 + 内容审核抽象 + 合规声明) 2026-10-17 03:03:51 +08:00
Zhou Weidong 9d92fa2a73 feat: C3 埋点事件上报(/app/events 匿名落库) 2026-10-17 02:55:46 +08:00
Zhou Weidong 952057ac51 feat: C2 Redis 接入 + 接口限流(RedisService + @RateLimit,验证码/登录防刷) 2026-10-17 02:49:10 +08:00
Zhou Weidong f03fcdede5 feat: B1 Google Play Billing 票据校验
- 新增 GooglePlayVerifier(调 Google Play Developer API 二次校验票据)
- StoreVerifier.verify 扩展 productId 参数,订单/控制器链路同步
- DisabledStoreVerifier 加条件装配,Google 启用后自动切换
- application.yml 增加 pay.google 配置(凭证经环境变量注入,默认关闭)
2026-10-17 02:35:36 +08:00
Zhou Weidong bbf46b3445 feat: 主键自增改雪花 ID(为分库分表预留)
- 52 个 entity 主键 @TableId 由 IdType.AUTO 改为 IdType.ASSIGN_ID(雪花)
- 双服务 application.yml 的 mybatis-plus id-type 由 auto 改为 assign_id
- schema 脚本主键去掉 GENERATED BY DEFAULT AS IDENTITY
2026-10-17 02:26:43 +08:00
Zhou Weidong 5f9483a86c 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 监控部署配置
2026-10-17 02:24:25 +08:00
Zhou Weidong efe8af6c9b docs: 补全接口契约遗漏,逐条展开系统管理与内容接口并补充 watch-history/creators 账号/media 详情 2026-10-16 10:23:25 +08:00
Zhou Weidong 66e6fc1555 feat: app-api 启用 Swagger 在线接口文档 (springdoc /doc.html) 2026-10-16 10:13:47 +08:00
Zhou Weidong 35be37c193 style: 全量补充作者标识 // Author: fruit.com.cn 2026-10-16 00:01:53 +08:00
372 changed files with 2923 additions and 146 deletions

No files matched your search

+32
View File
@@ -0,0 +1,32 @@
# Author: fruit.com.cn
# 监控组件编排:Prometheus + Grafana(Loki 日志后续接入)
services:
prometheus:
image: prom/prometheus:latest
container_name: fruitshorts-prometheus
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus-data:/prometheus
command:
- "--config.file=/etc/prometheus/prometheus.yml"
extra_hosts:
- "host.docker.internal:host-gateway"
restart: unless-stopped
grafana:
image: grafana/grafana:latest
container_name: fruitshorts-grafana
ports:
- "3000:3000"
volumes:
- grafana-data:/var/lib/grafana
environment:
- GF_SECURITY_ADMIN_USER=admin
- GF_SECURITY_ADMIN_PASSWORD=admin
restart: unless-stopped
volumes:
prometheus-data:
grafana-data:
+16
View File
@@ -0,0 +1,16 @@
# Author: fruit.com.cn
# Prometheus 抓取配置:抓取后端双服务的 actuator 指标
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: "fruitshorts-app-api"
metrics_path: "/actuator/prometheus"
static_configs:
- targets: ["host.docker.internal:8081"]
- job_name: "fruitshorts-admin-api"
metrics_path: "/actuator/prometheus"
static_configs:
- targets: ["host.docker.internal:8080"]
+84 -7
View File
@@ -219,24 +219,98 @@ POST /app/auth/register
### 2.1 认证与系统(已实现) ### 2.1 认证与系统(已实现)
`POST /admin/auth/login`、`GET /admin/auth/menus`、`GET /admin/auth/profile`、 **认证**
`POST /admin/auth/logout`、`POST /admin/auth/password`;
管理员、角色、权限、菜单、角色分类、系统配置、操作日志各自一套增删改查。 | 方法 | 路径 | 鉴权 | 说明 |
| --- | --- | --- | --- |
| POST | `/admin/auth/login` | 匿名 | 登录,返回 token |
| GET | `/admin/auth/menus` | 登录 | 当前用户菜单树 |
| GET | `/admin/auth/profile` | 登录 | 当前用户资料 |
| POST | `/admin/auth/logout` | 登录 | 登出 |
| POST | `/admin/auth/password` | 登录 | 修改密码 |
**系统管理(RBAC)**
| 资源 | 方法 | 路径 | 权限点 |
| --- | --- | --- | --- |
| 管理员 | GET | `/admin/admin-users/page` · `/{id}` | `system:admin-user` |
| 管理员 | POST | `/admin/admin-users` | `system:admin-user` |
| 管理员 | PUT | `/admin/admin-users/{id}` · `/{id}/status` · `/{id}/password` | `system:admin-user` |
| 管理员 | DELETE | `/admin/admin-users/{id}` | `system:admin-user` |
| 角色 | GET | `/admin/roles/page` · `/list` · `/{id}` · `/{id}/permissions` | `system:role`(`/list` 登录即可) |
| 角色 | POST | `/admin/roles` | `system:role` |
| 角色 | PUT | `/admin/roles/{id}` · `/{id}/permissions` | `system:role` |
| 角色 | DELETE | `/admin/roles/{id}` | `system:role` |
| 权限 | GET | `/admin/permissions` | `system:role` |
| 菜单 | GET | `/admin/menus/tree` | `system:menu` |
| 菜单 | POST · PUT · DELETE | `/admin/menus` · `/admin/menus/{id}` | `system:menu` |
| 角色分类 | GET · POST | `/admin/role-categories` | `system:role-category` |
| 角色分类 | PUT · DELETE | `/admin/role-categories/{id}` | `system:role-category` |
| 系统配置 | GET | `/admin/configs` | `system:role` / `system:menu` |
| 系统配置 | PUT | `/admin/configs` | `system:role` |
| 操作日志 | GET | `/admin/operation-logs/page` · `/{id}` | `log:list` |
| 字典 | GET | `/admin/dicts/drama-category` · `/content-status` | `drama:list` |
### 2.2 内容与媒资(已实现) ### 2.2 内容与媒资(已实现)
短剧(含是否热门、审核发布状态机)、剧集、分类、轮播图;媒资分片上传(init/chunk/complete)。
新增字段:短剧 `rating`(内容分级)、`regionCodes`(可播区域)、版权窗口、 新增字段:短剧 `rating`(内容分级)、`regionCodes`(可播区域)、版权窗口、
`creatorId`、`tags`、`trailerUrl`;剧集 `transcodeStatus`、`hlsUrl`、`previewSeconds`、字幕。 `creatorId`、`tags`、`trailerUrl`;剧集 `transcodeStatus`、`hlsUrl`、`previewSeconds`、字幕。
媒资上传补充(批 3 加固,后台与创作者共用同一套接口,未重写): **短剧(含审核发布状态机)**
| 方法 | 路径 | 权限点 | 说明 |
| --- | --- | --- | --- |
| GET | `/admin/dramas/page` | `drama:list` | 检索(含热门、状态筛选) |
| GET | `/admin/dramas/{id}` | `drama:list` | 详情 |
| POST | `/admin/dramas` | `drama:create` | 新建短剧 |
| PUT | `/admin/dramas/{id}` | `drama:update` | 编辑短剧 |
| DELETE | `/admin/dramas/{id}` | `drama:delete` | 删除短剧 |
| POST | `/admin/dramas/{id}/submit` | `drama:review` / `drama:update` | 提交审核 |
| POST | `/admin/dramas/{id}/approve` | `drama:review` | 审核通过 |
| POST | `/admin/dramas/{id}/reject` | `drama:review` | 驳回 |
| POST | `/admin/dramas/{id}/publish` | `drama:review` | 上线 |
| POST | `/admin/dramas/{id}/offline` | `drama:review` | 下线 |
| GET | `/admin/dramas/{id}/review-records` | `drama:list` | 审核记录 |
**剧集**
| 方法 | 路径 | 权限点 | 说明 |
| --- | --- | --- | --- |
| GET | `/admin/episodes?dramaId=` | `episode:list` | 某剧分集列表 |
| POST | `/admin/episodes` | `episode:upload` | 新增分集 |
| PUT | `/admin/episodes/{id}` | `episode:upload` | 编辑分集 |
| DELETE | `/admin/episodes/{id}` | `episode:upload` | 删除分集 |
| PUT | `/admin/episodes/sort` | `episode:sort` | 批量排序 |
**短剧类型**
| 方法 | 路径 | 权限点 | 说明 |
| --- | --- | --- | --- |
| GET | `/admin/drama-categories` | `drama:list` | 类型列表 |
| POST | `/admin/drama-categories` | `drama:create` | 新增类型 |
| PUT | `/admin/drama-categories/{id}` | `drama:update` | 编辑类型 |
| DELETE | `/admin/drama-categories/{id}` | `drama:delete` | 删除类型 |
**轮播图**
| 方法 | 路径 | 权限点 | 说明 |
| --- | --- | --- | --- |
| GET | `/admin/banners/page` · `/admin/banners/{id}` | `banner:list` | 检索 / 详情 |
| POST | `/admin/banners` | `banner:create` | 新增 |
| PUT | `/admin/banners/{id}` · `/{id}/status` · `/sort` | `banner:update` | 编辑 / 改状态 / 排序 |
| DELETE | `/admin/banners/{id}` | `banner:delete` | 删除 |
**媒资分片上传(后台与创作者共用)**
| 方法 | 路径 | 说明 | | 方法 | 路径 | 说明 |
| --- | --- | --- | | --- | --- | --- |
| POST | `/admin/media/upload/init` | 建上传会话(返回 `uploadId` / `chunkSize` / `totalChunks` / `objectKey`) | | POST | `/admin/media/upload/init` | 建上传会话(返回 `uploadId` / `chunkSize` / `totalChunks` / `objectKey`) |
| POST | `/admin/media/upload/chunk` | 传分片(`uploadId` + `index` + `file`),可重复传同一片 | | POST | `/admin/media/upload/chunk` | 传分片(`uploadId` + `index` + `file`),可重复传同一片 |
| GET | `/admin/media/upload/{uploadId}` | **新增**:断点续传回显(`receivedChunks` / `totalChunks` / `percent` / `status` / 完成后带 `url`) | | GET | `/admin/media/upload/{uploadId}` | 断点续传回显(`receivedChunks` / `totalChunks` / `percent` / `status` / 完成后带 `url`) |
| POST | `/admin/media/upload/complete` | 合并分片出媒资(幂等:同一 uploadId 重复调用返回同一产物) | | POST | `/admin/media/upload/complete` | 合并分片出媒资(幂等:同一 uploadId 重复调用返回同一产物) |
| GET | `/admin/media/{id}` | 媒资详情 |
> 媒资上传与详情权限点均为 `cover:upload` 或 `episode:upload`(`@ss.hasAnyPermission`)。
**上传会话归属**:`uploadId` 相当于上传凭证,`chunk` / `complete` / 进度查询都校验会话发起人, **上传会话归属**:`uploadId` 相当于上传凭证,`chunk` / `complete` / 进度查询都校验会话发起人,
别人拿到 `uploadId` 也塞不进分片(`{"code":403,"message":"无权操作该上传会话"}`)。 别人拿到 `uploadId` 也塞不进分片(`{"code":403,"message":"无权操作该上传会话"}`)。
@@ -248,6 +322,7 @@ POST /app/auth/register
| GET | `/admin/app-users/page` | `app-user:list` | 检索:keyword(uid/昵称/手机号/邮箱)、status、countryCode、registerSource | | GET | `/admin/app-users/page` | `app-user:list` | 检索:keyword(uid/昵称/手机号/邮箱)、status、countryCode、registerSource |
| GET | `/admin/app-users/{id}` | `app-user:list` | 详情 | | GET | `/admin/app-users/{id}` | `app-user:list` | 详情 |
| GET | `/admin/app-users/{id}/ban-records` | `app-user:list` | 封禁与申诉记录 | | GET | `/admin/app-users/{id}/ban-records` | `app-user:list` | 封禁与申诉记录 |
| GET | `/admin/app-users/{id}/watch-history` | `app-user:list` | 观看流水明细(带剧名/集号,客服核对用) |
| PUT | `/admin/app-users/{id}/status` | `app-user:update` | 改状态:normal/limited/banned | | PUT | `/admin/app-users/{id}/status` | `app-user:update` | 改状态:normal/limited/banned |
状态处置会**立即吊销该用户全部会话**并写入封禁记录,避免"封了还能用"。 状态处置会**立即吊销该用户全部会话**并写入封禁记录,避免"封了还能用"。
@@ -294,6 +369,9 @@ POST /app/auth/register
| --- | --- | --- | --- | | --- | --- | --- | --- |
| GET | `/admin/creators/page` `/admin/creators/{id}` `/{id}/documents` | `creator:list` | 创作者检索、详情、资质文件 | | GET | `/admin/creators/page` `/admin/creators/{id}` `/{id}/documents` | `creator:list` | 创作者检索、详情、资质文件 |
| PUT | `/admin/creators/{id}/audit` | `creator:audit` | 入驻审核:`APPROVE` / `REJECT`(必填原因+原因码)/ `FREEZE` | | PUT | `/admin/creators/{id}/audit` | `creator:audit` | 入驻审核:`APPROVE` / `REJECT`(必填原因+原因码)/ `FREEZE` |
| POST | `/admin/creators` | `creator:audit` | 手工录入创作者主体(C 端申请入驻未实现前的打通用口) |
| POST | `/admin/creators/{id}/account` | `creator:account` | 开通创作者账号(幂等) |
| POST | `/admin/creators/{id}/account/reset-password` | `creator:account` | 重置创作者账号密码(一次性明文返回) |
| GET | `/admin/submissions/page` `/admin/submissions/{id}` | `submission:list` | 内容提交检索与详情 | | GET | `/admin/submissions/page` `/admin/submissions/{id}` | `submission:list` | 内容提交检索与详情 |
| PUT | `/admin/submissions/{id}/audit` | `submission:audit` | 审核通过后按 `type` 落到短剧状态机(CREATE/UPDATE 留档、PUBLISH 上线、OFFLINE 下线) | | PUT | `/admin/submissions/{id}/audit` | `submission:audit` | 审核通过后按 `type` 落到短剧状态机(CREATE/UPDATE 留档、PUBLISH 上线、OFFLINE 下线) |
| GET · PUT | `/admin/prices` | `price:update` | 分集价格:整剧 `freeEpisodes`/`unlockCoin` + 逐集覆盖;改价立即影响播放与下载判定 | | GET · PUT | `/admin/prices` | `price:update` | 分集价格:整剧 `freeEpisodes`/`unlockCoin` + 逐集覆盖;改价立即影响播放与下载判定 |
@@ -348,7 +426,6 @@ POST /app/auth/register
| PUT | `/admin/prices` | 分集价格与免费集配置 | | PUT | `/admin/prices` | 分集价格与免费集配置 |
| GET | `/admin/dashboard/overview` | 经营大盘(用户/播放/收入/成本) | | GET | `/admin/dashboard/overview` | 经营大盘(用户/播放/收入/成本) |
| GET | `/admin/dashboard/transcode` | 转码队列与失败任务 | | GET | `/admin/dashboard/transcode` | 转码队列与失败任务 |
| POST | `/admin/notices` | 发布系统公告 |
### 2.9 后台前端页面(已实现) ### 2.9 后台前端页面(已实现)
+2
View File
@@ -1,4 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!-- Author: fruit.com.cn -->
<project xmlns="http://maven.apache.org/POM/4.0.0" <project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts; package com.fruitshorts;
import org.mybatis.spring.annotation.MapperScan; import org.mybatis.spring.annotation.MapperScan;
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts.admin.controller; package com.fruitshorts.admin.controller;
import com.fruitshorts.common.pojo.PageResult; import com.fruitshorts.common.pojo.PageResult;
@@ -8,6 +10,8 @@ import com.fruitshorts.framework.log.OperationLog;
import com.fruitshorts.system.vo.AdminUserPageReqVO; import com.fruitshorts.system.vo.AdminUserPageReqVO;
import com.fruitshorts.system.vo.AdminUserRespVO; import com.fruitshorts.system.vo.AdminUserRespVO;
import com.fruitshorts.system.vo.AdminUserSaveReqVO; 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 jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
@@ -23,6 +27,7 @@ import org.springframework.web.bind.annotation.RestController;
import java.util.Map; import java.util.Map;
@Tag(name = "管理员管理")
@RestController @RestController
@RequestMapping("/admin/admin-users") @RequestMapping("/admin/admin-users")
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -30,18 +35,21 @@ public class AdminUserController {
private final AdminUserService adminUserService; private final AdminUserService adminUserService;
@Operation(summary = "管理员分页检索")
@GetMapping("/page") @GetMapping("/page")
@PreAuthorize("@ss.hasPermission('system:admin-user')") @PreAuthorize("@ss.hasPermission('system:admin-user')")
public Result<PageResult<AdminUserRespVO>> page(AdminUserPageReqVO req) { public Result<PageResult<AdminUserRespVO>> page(AdminUserPageReqVO req) {
return Result.ok(adminUserService.page(req)); return Result.ok(adminUserService.page(req));
} }
@Operation(summary = "管理员详情")
@GetMapping("/{id}") @GetMapping("/{id}")
@PreAuthorize("@ss.hasPermission('system:admin-user')") @PreAuthorize("@ss.hasPermission('system:admin-user')")
public Result<AdminUserRespVO> get(@PathVariable Long id) { public Result<AdminUserRespVO> get(@PathVariable Long id) {
return Result.ok(adminUserService.get(id)); return Result.ok(adminUserService.get(id));
} }
@Operation(summary = "新增管理员")
@PostMapping @PostMapping
@PreAuthorize("@ss.hasPermission('system:admin-user')") @PreAuthorize("@ss.hasPermission('system:admin-user')")
@OperationLog(module = "后台管理", action = "新增管理员") @OperationLog(module = "后台管理", action = "新增管理员")
@@ -49,6 +57,7 @@ public class AdminUserController {
return Result.ok(adminUserService.create(req)); return Result.ok(adminUserService.create(req));
} }
@Operation(summary = "编辑管理员")
@PutMapping("/{id}") @PutMapping("/{id}")
@PreAuthorize("@ss.hasPermission('system:admin-user')") @PreAuthorize("@ss.hasPermission('system:admin-user')")
@OperationLog(module = "后台管理", action = "编辑管理员") @OperationLog(module = "后台管理", action = "编辑管理员")
@@ -57,6 +66,7 @@ public class AdminUserController {
return Result.ok(); return Result.ok();
} }
@Operation(summary = "删除管理员")
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
@PreAuthorize("@ss.hasPermission('system:admin-user')") @PreAuthorize("@ss.hasPermission('system:admin-user')")
@OperationLog(module = "后台管理", action = "删除管理员") @OperationLog(module = "后台管理", action = "删除管理员")
@@ -65,6 +75,7 @@ public class AdminUserController {
return Result.ok(); return Result.ok();
} }
@Operation(summary = "修改管理员状态")
@PutMapping("/{id}/status") @PutMapping("/{id}/status")
@PreAuthorize("@ss.hasPermission('system:admin-user')") @PreAuthorize("@ss.hasPermission('system:admin-user')")
@OperationLog(module = "后台管理", action = "修改管理员状态") @OperationLog(module = "后台管理", action = "修改管理员状态")
@@ -73,6 +84,7 @@ public class AdminUserController {
return Result.ok(); return Result.ok();
} }
@Operation(summary = "重置管理员密码")
@PutMapping("/{id}/password") @PutMapping("/{id}/password")
@PreAuthorize("@ss.hasPermission('system:admin-user')") @PreAuthorize("@ss.hasPermission('system:admin-user')")
@OperationLog(module = "后台管理", action = "重置管理员密码") @OperationLog(module = "后台管理", action = "重置管理员密码")
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts.admin.controller; package com.fruitshorts.admin.controller;
import com.fruitshorts.common.pojo.PageResult; import com.fruitshorts.common.pojo.PageResult;
@@ -16,6 +18,8 @@ import com.fruitshorts.user.dal.entity.AppUserBanRecord;
import com.fruitshorts.user.service.AppUserAdminService; import com.fruitshorts.user.service.AppUserAdminService;
import com.fruitshorts.user.vo.AppUserQueryReqVO; import com.fruitshorts.user.vo.AppUserQueryReqVO;
import com.fruitshorts.user.vo.AppUserAdminRespVO; 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 lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
@@ -30,6 +34,7 @@ import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** 后台用户管理。 */ /** 后台用户管理。 */
@Tag(name = "App 用户管理")
@RestController @RestController
@RequestMapping("/admin/app-users") @RequestMapping("/admin/app-users")
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -40,18 +45,21 @@ public class AppUserAdminController {
private final DramaService dramaService; private final DramaService dramaService;
private final EpisodeService episodeService; private final EpisodeService episodeService;
@Operation(summary = "用户分页检索(uid/昵称/手机号/邮箱/状态/国家/注册来源)")
@GetMapping("/page") @GetMapping("/page")
@PreAuthorize("@ss.hasPermission('app-user:list')") @PreAuthorize("@ss.hasPermission('app-user:list')")
public Result<PageResult<AppUserAdminRespVO>> page(AppUserQueryReqVO req) { public Result<PageResult<AppUserAdminRespVO>> page(AppUserQueryReqVO req) {
return Result.ok(appUserAdminService.page(req)); return Result.ok(appUserAdminService.page(req));
} }
@Operation(summary = "用户详情")
@GetMapping("/{id}") @GetMapping("/{id}")
@PreAuthorize("@ss.hasPermission('app-user:list')") @PreAuthorize("@ss.hasPermission('app-user:list')")
public Result<AppUserAdminRespVO> detail(@PathVariable Long id) { public Result<AppUserAdminRespVO> detail(@PathVariable Long id) {
return Result.ok(appUserAdminService.detail(id)); return Result.ok(appUserAdminService.detail(id));
} }
@Operation(summary = "封禁与申诉记录")
@GetMapping("/{id}/ban-records") @GetMapping("/{id}/ban-records")
@PreAuthorize("@ss.hasPermission('app-user:list')") @PreAuthorize("@ss.hasPermission('app-user:list')")
public Result<List<AppUserBanRecord>> banRecords(@PathVariable Long id) { public Result<List<AppUserBanRecord>> banRecords(@PathVariable Long id) {
@@ -59,6 +67,7 @@ public class AppUserAdminController {
} }
/** 观看流水明细:客服核对"用户说看了但没记录"这类问题时用,带上剧名与集号。 */ /** 观看流水明细:客服核对"用户说看了但没记录"这类问题时用,带上剧名与集号。 */
@Operation(summary = "观看流水明细(客服核对用,带剧名与集号)")
@GetMapping("/{id}/watch-history") @GetMapping("/{id}/watch-history")
@PreAuthorize("@ss.hasPermission('app-user:list')") @PreAuthorize("@ss.hasPermission('app-user:list')")
public Result<List<AppUserWatchHistoryVO>> watchHistory(@PathVariable Long id, public Result<List<AppUserWatchHistoryVO>> watchHistory(@PathVariable Long id,
@@ -106,6 +115,7 @@ public class AppUserAdminController {
} }
/** 状态处置:normal/limited/banned。封禁会立即吊销该用户全部会话。 */ /** 状态处置:normal/limited/banned。封禁会立即吊销该用户全部会话。 */
@Operation(summary = "修改用户状态(normal/limited/banned,封禁立即吊销全部会话)")
@PutMapping("/{id}/status") @PutMapping("/{id}/status")
@PreAuthorize("@ss.hasPermission('app-user:update')") @PreAuthorize("@ss.hasPermission('app-user:update')")
@OperationLog(module = "用户管理", action = "修改用户状态") @OperationLog(module = "用户管理", action = "修改用户状态")
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts.admin.controller; package com.fruitshorts.admin.controller;
import com.fruitshorts.common.pojo.Result; import com.fruitshorts.common.pojo.Result;
@@ -10,6 +12,8 @@ import com.fruitshorts.system.vo.LoginRespVO;
import com.fruitshorts.system.vo.MenuVO; import com.fruitshorts.system.vo.MenuVO;
import com.fruitshorts.system.vo.PasswordChangeReqVO; import com.fruitshorts.system.vo.PasswordChangeReqVO;
import com.fruitshorts.system.vo.ProfileRespVO; 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.servlet.http.HttpServletRequest;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@@ -21,6 +25,7 @@ import org.springframework.web.bind.annotation.RestController;
import java.util.List; import java.util.List;
@Tag(name = "后台认证")
@RestController @RestController
@RequestMapping("/admin/auth") @RequestMapping("/admin/auth")
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -28,6 +33,7 @@ public class AuthController {
private final AuthService authService; private final AuthService authService;
@Operation(summary = "后台登录,返回 token")
@PostMapping("/login") @PostMapping("/login")
@OperationLog(module = "认证", action = "登录") @OperationLog(module = "认证", action = "登录")
public Result<LoginRespVO> login(@Valid @RequestBody LoginReqVO req, HttpServletRequest request) { public Result<LoginRespVO> login(@Valid @RequestBody LoginReqVO req, HttpServletRequest request) {
@@ -35,23 +41,27 @@ public class AuthController {
} }
/** 当前用户的菜单路由树,结构与前端 element-plus-admin 的动态路由一致。 */ /** 当前用户的菜单路由树,结构与前端 element-plus-admin 的动态路由一致。 */
@Operation(summary = "当前用户菜单路由树")
@GetMapping("/menus") @GetMapping("/menus")
public Result<List<MenuVO>> menus() { public Result<List<MenuVO>> menus() {
return Result.ok(authService.listMenus(SecurityUtils.getUserId())); return Result.ok(authService.listMenus(SecurityUtils.getUserId()));
} }
@Operation(summary = "当前用户资料")
@GetMapping("/profile") @GetMapping("/profile")
public Result<ProfileRespVO> profile() { public Result<ProfileRespVO> profile() {
return Result.ok(authService.profile(SecurityUtils.getUserId())); return Result.ok(authService.profile(SecurityUtils.getUserId()));
} }
/** JWT 无状态,登出由前端清除 token 即可。 */ /** JWT 无状态,登出由前端清除 token 即可。 */
@Operation(summary = "登出(JWT 无状态,前端清除 token 即可)")
@PostMapping("/logout") @PostMapping("/logout")
@OperationLog(module = "认证", action = "退出登录") @OperationLog(module = "认证", action = "退出登录")
public Result<Void> logout() { public Result<Void> logout() {
return Result.ok(); return Result.ok();
} }
@Operation(summary = "修改密码")
@PostMapping("/password") @PostMapping("/password")
@OperationLog(module = "认证", action = "修改密码") @OperationLog(module = "认证", action = "修改密码")
public Result<Void> changePassword(@Valid @RequestBody PasswordChangeReqVO req) { public Result<Void> changePassword(@Valid @RequestBody PasswordChangeReqVO req) {
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts.admin.controller; package com.fruitshorts.admin.controller;
import com.fruitshorts.common.pojo.PageResult; import com.fruitshorts.common.pojo.PageResult;
@@ -7,6 +9,8 @@ import com.fruitshorts.content.vo.BannerRespVO;
import com.fruitshorts.content.vo.BannerSaveReqVO; import com.fruitshorts.content.vo.BannerSaveReqVO;
import com.fruitshorts.framework.security.SecurityUtils; import com.fruitshorts.framework.security.SecurityUtils;
import com.fruitshorts.framework.log.OperationLog; 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 jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
@@ -23,6 +27,7 @@ import org.springframework.web.bind.annotation.RestController;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@Tag(name = "轮播图管理")
@RestController @RestController
@RequestMapping("/admin/banners") @RequestMapping("/admin/banners")
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -30,6 +35,7 @@ public class BannerController {
private final BannerService bannerService; private final BannerService bannerService;
@Operation(summary = "轮播图分页检索(按位置/状态过滤)")
@GetMapping("/page") @GetMapping("/page")
@PreAuthorize("@ss.hasPermission('banner:list')") @PreAuthorize("@ss.hasPermission('banner:list')")
public Result<PageResult<BannerRespVO>> page(@RequestParam(required = false) String position, public Result<PageResult<BannerRespVO>> page(@RequestParam(required = false) String position,
@@ -39,12 +45,14 @@ public class BannerController {
return Result.ok(bannerService.page(position, status, pageIndex, pageSize)); return Result.ok(bannerService.page(position, status, pageIndex, pageSize));
} }
@Operation(summary = "轮播图详情")
@GetMapping("/{id}") @GetMapping("/{id}")
@PreAuthorize("@ss.hasPermission('banner:list')") @PreAuthorize("@ss.hasPermission('banner:list')")
public Result<BannerRespVO> get(@PathVariable Long id) { public Result<BannerRespVO> get(@PathVariable Long id) {
return Result.ok(bannerService.get(id)); return Result.ok(bannerService.get(id));
} }
@Operation(summary = "新增轮播图")
@PostMapping @PostMapping
@PreAuthorize("@ss.hasPermission('banner:create')") @PreAuthorize("@ss.hasPermission('banner:create')")
@OperationLog(module = "内容管理", action = "新增轮播图") @OperationLog(module = "内容管理", action = "新增轮播图")
@@ -52,6 +60,7 @@ public class BannerController {
return Result.ok(bannerService.create(req, SecurityUtils.getUserId())); return Result.ok(bannerService.create(req, SecurityUtils.getUserId()));
} }
@Operation(summary = "编辑轮播图")
@PutMapping("/{id}") @PutMapping("/{id}")
@PreAuthorize("@ss.hasPermission('banner:update')") @PreAuthorize("@ss.hasPermission('banner:update')")
@OperationLog(module = "内容管理", action = "编辑轮播图") @OperationLog(module = "内容管理", action = "编辑轮播图")
@@ -60,6 +69,7 @@ public class BannerController {
return Result.ok(); return Result.ok();
} }
@Operation(summary = "删除轮播图")
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
@PreAuthorize("@ss.hasPermission('banner:delete')") @PreAuthorize("@ss.hasPermission('banner:delete')")
@OperationLog(module = "内容管理", action = "删除轮播图") @OperationLog(module = "内容管理", action = "删除轮播图")
@@ -68,6 +78,7 @@ public class BannerController {
return Result.ok(); return Result.ok();
} }
@Operation(summary = "修改轮播图状态")
@PutMapping("/{id}/status") @PutMapping("/{id}/status")
@PreAuthorize("@ss.hasPermission('banner:update')") @PreAuthorize("@ss.hasPermission('banner:update')")
@OperationLog(module = "内容管理", action = "修改轮播图状态") @OperationLog(module = "内容管理", action = "修改轮播图状态")
@@ -76,6 +87,7 @@ public class BannerController {
return Result.ok(); return Result.ok();
} }
@Operation(summary = "调整轮播图排序")
@PutMapping("/sort") @PutMapping("/sort")
@PreAuthorize("@ss.hasPermission('banner:update')") @PreAuthorize("@ss.hasPermission('banner:update')")
public Result<Void> sort(@RequestBody Map<String, List<Long>> body) { public Result<Void> sort(@RequestBody Map<String, List<Long>> body) {
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts.admin.controller; package com.fruitshorts.admin.controller;
import com.fruitshorts.common.pojo.PageResult; import com.fruitshorts.common.pojo.PageResult;
@@ -9,6 +11,8 @@ import com.fruitshorts.interaction.service.CommentService;
import com.fruitshorts.interaction.vo.CommentAuditReqVO; import com.fruitshorts.interaction.vo.CommentAuditReqVO;
import com.fruitshorts.interaction.vo.CommentQueryReqVO; import com.fruitshorts.interaction.vo.CommentQueryReqVO;
import com.fruitshorts.interaction.vo.CommentRespVO; 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 jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
@@ -23,6 +27,7 @@ import org.springframework.web.bind.annotation.RestController;
import java.util.List; import java.util.List;
/** 后台评论审核。 */ /** 后台评论审核。 */
@Tag(name = "评论治理")
@RestController @RestController
@RequestMapping("/admin/comments") @RequestMapping("/admin/comments")
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -31,6 +36,7 @@ public class CommentAdminController {
private final CommentService commentService; private final CommentService commentService;
/** 检索评论:默认按状态过滤,`status=pending` 即审核队列。 */ /** 检索评论:默认按状态过滤,`status=pending` 即审核队列。 */
@Operation(summary = "评论检索(status=pending 即审核队列)")
@GetMapping("/page") @GetMapping("/page")
@PreAuthorize("@ss.hasPermission('comment:list')") @PreAuthorize("@ss.hasPermission('comment:list')")
public Result<PageResult<CommentRespVO>> page(CommentQueryReqVO req) { public Result<PageResult<CommentRespVO>> page(CommentQueryReqVO req) {
@@ -38,12 +44,14 @@ public class CommentAdminController {
} }
/** 审核留证:机审与人审的完整链路。 */ /** 审核留证:机审与人审的完整链路。 */
@Operation(summary = "审核留证(机审与人审完整链路)")
@GetMapping("/{id}/audit-records") @GetMapping("/{id}/audit-records")
@PreAuthorize("@ss.hasPermission('comment:list')") @PreAuthorize("@ss.hasPermission('comment:list')")
public Result<List<ContentAuditRecord>> auditRecords(@PathVariable Long id) { public Result<List<ContentAuditRecord>> auditRecords(@PathVariable Long id) {
return Result.ok(commentService.auditRecords(id)); return Result.ok(commentService.auditRecords(id));
} }
@Operation(summary = "审核评论(通过/驳回,带原因码与说明)")
@PutMapping("/{id}/audit") @PutMapping("/{id}/audit")
@PreAuthorize("@ss.hasPermission('comment:audit')") @PreAuthorize("@ss.hasPermission('comment:audit')")
@OperationLog(module = "内容治理", action = "审核评论") @OperationLog(module = "内容治理", action = "审核评论")
@@ -52,6 +60,7 @@ public class CommentAdminController {
return Result.ok(); return Result.ok();
} }
@Operation(summary = "删除评论")
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
@PreAuthorize("@ss.hasPermission('comment:audit')") @PreAuthorize("@ss.hasPermission('comment:audit')")
@OperationLog(module = "内容治理", action = "删除评论") @OperationLog(module = "内容治理", action = "删除评论")
@@ -1,9 +1,13 @@
// Author: fruit.com.cn
package com.fruitshorts.admin.controller; package com.fruitshorts.admin.controller;
import com.fruitshorts.common.pojo.Result; import com.fruitshorts.common.pojo.Result;
import com.fruitshorts.system.service.ConfigService; import com.fruitshorts.system.service.ConfigService;
import com.fruitshorts.framework.log.OperationLog; import com.fruitshorts.framework.log.OperationLog;
import com.fruitshorts.system.vo.ConfigItemVO; 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 lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
@@ -15,6 +19,7 @@ import org.springframework.web.bind.annotation.RestController;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@Tag(name = "系统配置")
@RestController @RestController
@RequestMapping("/admin/configs") @RequestMapping("/admin/configs")
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -22,12 +27,14 @@ public class ConfigController {
private final ConfigService configService; private final ConfigService configService;
@Operation(summary = "系统配置列表")
@GetMapping @GetMapping
@PreAuthorize("@ss.hasAnyPermission('system:role','system:menu')") @PreAuthorize("@ss.hasAnyPermission('system:role','system:menu')")
public Result<List<ConfigItemVO>> list() { public Result<List<ConfigItemVO>> list() {
return Result.ok(configService.list()); return Result.ok(configService.list());
} }
@Operation(summary = "批量修改系统配置")
@PutMapping @PutMapping
@PreAuthorize("@ss.hasPermission('system:role')") @PreAuthorize("@ss.hasPermission('system:role')")
@OperationLog(module = "后台管理", action = "修改系统设置") @OperationLog(module = "后台管理", action = "修改系统设置")
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts.admin.controller; package com.fruitshorts.admin.controller;
import com.fruitshorts.common.pojo.PageResult; import com.fruitshorts.common.pojo.PageResult;
@@ -13,6 +15,8 @@ import com.fruitshorts.creator.vo.CreatorAuditReqVO;
import com.fruitshorts.creator.vo.CreatorQueryReqVO; import com.fruitshorts.creator.vo.CreatorQueryReqVO;
import com.fruitshorts.framework.log.OperationLog; import com.fruitshorts.framework.log.OperationLog;
import com.fruitshorts.framework.security.SecurityUtils; 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 jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
@@ -27,6 +31,7 @@ import org.springframework.web.bind.annotation.RestController;
import java.util.List; import java.util.List;
/** 后台创作者:检索、详情、资质、入驻审核。 */ /** 后台创作者:检索、详情、资质、入驻审核。 */
@Tag(name = "创作者管理")
@RestController @RestController
@RequestMapping("/admin/creators") @RequestMapping("/admin/creators")
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -35,12 +40,14 @@ public class CreatorAdminController {
private final CreatorService creatorService; private final CreatorService creatorService;
private final CreatorAccountService creatorAccountService; private final CreatorAccountService creatorAccountService;
@Operation(summary = "创作者分页检索")
@GetMapping("/page") @GetMapping("/page")
@PreAuthorize("@ss.hasPermission('creator:list')") @PreAuthorize("@ss.hasPermission('creator:list')")
public Result<PageResult<Creator>> page(CreatorQueryReqVO req) { public Result<PageResult<Creator>> page(CreatorQueryReqVO req) {
return Result.ok(creatorService.page(req)); return Result.ok(creatorService.page(req));
} }
@Operation(summary = "创作者详情")
@GetMapping("/{id}") @GetMapping("/{id}")
@PreAuthorize("@ss.hasPermission('creator:list')") @PreAuthorize("@ss.hasPermission('creator:list')")
public Result<Creator> detail(@PathVariable Long id) { public Result<Creator> detail(@PathVariable Long id) {
@@ -48,12 +55,14 @@ public class CreatorAdminController {
} }
/** 资质文件:审核前要看证件原件。 */ /** 资质文件:审核前要看证件原件。 */
@Operation(summary = "资质文件(审核前查看证件原件)")
@GetMapping("/{id}/documents") @GetMapping("/{id}/documents")
@PreAuthorize("@ss.hasPermission('creator:list')") @PreAuthorize("@ss.hasPermission('creator:list')")
public Result<List<CreatorDocument>> documents(@PathVariable Long id) { public Result<List<CreatorDocument>> documents(@PathVariable Long id) {
return Result.ok(creatorService.documents(id)); return Result.ok(creatorService.documents(id));
} }
@Operation(summary = "入驻审核(通过/驳回/冻结)")
@PutMapping("/{id}/audit") @PutMapping("/{id}/audit")
@PreAuthorize("@ss.hasPermission('creator:audit')") @PreAuthorize("@ss.hasPermission('creator:audit')")
@OperationLog(module = "创作者管理", action = "入驻审核") @OperationLog(module = "创作者管理", action = "入驻审核")
@@ -62,6 +71,7 @@ public class CreatorAdminController {
} }
/** 手工录入创作者主体(C 端申请入驻还没实现,先用它打通链路)。 */ /** 手工录入创作者主体(C 端申请入驻还没实现,先用它打通链路)。 */
@Operation(summary = "手工录入创作者主体(C 端申请未实现前的打通用口)")
@PostMapping @PostMapping
@PreAuthorize("@ss.hasPermission('creator:audit')") @PreAuthorize("@ss.hasPermission('creator:audit')")
@OperationLog(module = "创作者管理", action = "新增创作者") @OperationLog(module = "创作者管理", action = "新增创作者")
@@ -70,6 +80,7 @@ public class CreatorAdminController {
} }
/** 开通创作者账号:仅审核通过可开;已开通则幂等返回,不重复建号。 */ /** 开通创作者账号:仅审核通过可开;已开通则幂等返回,不重复建号。 */
@Operation(summary = "开通创作者账号(仅审核通过可开,幂等)")
@PostMapping("/{id}/account") @PostMapping("/{id}/account")
@PreAuthorize("@ss.hasPermission('creator:account')") @PreAuthorize("@ss.hasPermission('creator:account')")
@OperationLog(module = "创作者管理", action = "开通创作者账号") @OperationLog(module = "创作者管理", action = "开通创作者账号")
@@ -79,6 +90,7 @@ public class CreatorAdminController {
} }
/** 重置创作者账号密码:一次性明文返回,旧密码立即失效。 */ /** 重置创作者账号密码:一次性明文返回,旧密码立即失效。 */
@Operation(summary = "重置创作者账号密码(一次性明文返回)")
@PostMapping("/{id}/account/reset-password") @PostMapping("/{id}/account/reset-password")
@PreAuthorize("@ss.hasPermission('creator:account')") @PreAuthorize("@ss.hasPermission('creator:account')")
@OperationLog(module = "创作者管理", action = "重置创作者密码") @OperationLog(module = "创作者管理", action = "重置创作者密码")
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts.admin.controller; package com.fruitshorts.admin.controller;
import com.fruitshorts.common.pojo.PageResult; import com.fruitshorts.common.pojo.PageResult;
@@ -9,6 +11,8 @@ import com.fruitshorts.creator.vo.CreatorCenterNoticeVO;
import com.fruitshorts.creator.vo.CreatorCenterProfileVO; import com.fruitshorts.creator.vo.CreatorCenterProfileVO;
import com.fruitshorts.creator.vo.CreatorCenterTargetVO; import com.fruitshorts.creator.vo.CreatorCenterTargetVO;
import com.fruitshorts.admin.service.CreatorDashboardService; 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 lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
@@ -26,6 +30,7 @@ import java.util.Map;
* 归属由 {@link CreatorCenterService#requireCurrentCreator()} 从登录态解析, * 归属由 {@link CreatorCenterService#requireCurrentCreator()} 从登录态解析,
* 查询强制带 `where creator_id = 当前创作者`;访问他人作品统一 403。 * 查询强制带 `where creator_id = 当前创作者`;访问他人作品统一 403。
*/ */
@Tag(name = "创作者中心")
@RestController @RestController
@RequestMapping("/admin/creator-center") @RequestMapping("/admin/creator-center")
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -35,6 +40,7 @@ public class CreatorCenterController {
private final CreatorDashboardService creatorDashboardService; private final CreatorDashboardService creatorDashboardService;
/** 当前创作者的主体信息与入驻状态。 */ /** 当前创作者的主体信息与入驻状态。 */
@Operation(summary = "当前创作者的主体信息与入驻状态")
@GetMapping("/profile") @GetMapping("/profile")
@PreAuthorize("@ss.hasPermission('creator-center:view')") @PreAuthorize("@ss.hasPermission('creator-center:view')")
public Result<CreatorCenterProfileVO> profile() { public Result<CreatorCenterProfileVO> profile() {
@@ -42,6 +48,7 @@ public class CreatorCenterController {
} }
/** 我的作品列表(含最近一次提交的审核进度)。 */ /** 我的作品列表(含最近一次提交的审核进度)。 */
@Operation(summary = "我的作品列表(含最近一次提交的审核进度)")
@GetMapping("/dramas/page") @GetMapping("/dramas/page")
@PreAuthorize("@ss.hasPermission('creator-center:view')") @PreAuthorize("@ss.hasPermission('creator-center:view')")
public Result<PageResult<CreatorCenterDramaVO>> pageMyDramas(CreatorCenterDramaQueryReqVO req) { public Result<PageResult<CreatorCenterDramaVO>> pageMyDramas(CreatorCenterDramaQueryReqVO req) {
@@ -49,6 +56,7 @@ public class CreatorCenterController {
} }
/** 我的作品详情:id 是别人的作品时返回 403,不区分"不存在"。 */ /** 我的作品详情:id 是别人的作品时返回 403,不区分"不存在"。 */
@Operation(summary = "我的作品详情(他人作品返回 403)")
@GetMapping("/dramas/{id}") @GetMapping("/dramas/{id}")
@PreAuthorize("@ss.hasPermission('creator-center:view')") @PreAuthorize("@ss.hasPermission('creator-center:view')")
public Result<CreatorCenterDramaVO> myDrama(@PathVariable Long id) { public Result<CreatorCenterDramaVO> myDrama(@PathVariable Long id) {
@@ -56,6 +64,7 @@ public class CreatorCenterController {
} }
/** 我的通知:入驻审核结果 + 我提交内容的审核结果。 */ /** 我的通知:入驻审核结果 + 我提交内容的审核结果。 */
@Operation(summary = "我的通知(入驻审核 + 内容审核结果)")
@GetMapping("/notices") @GetMapping("/notices")
@PreAuthorize("@ss.hasPermission('creator-center:view')") @PreAuthorize("@ss.hasPermission('creator-center:view')")
public Result<List<CreatorCenterNoticeVO>> notices() { public Result<List<CreatorCenterNoticeVO>> notices() {
@@ -63,6 +72,7 @@ public class CreatorCenterController {
} }
/** 我的数据看板:播放 / 完播 / 收藏 / 评论 / 解锁收入概览(只读,只含自己的作品)。 */ /** 我的数据看板:播放 / 完播 / 收藏 / 评论 / 解锁收入概览(只读,只含自己的作品)。 */
@Operation(summary = "我的数据看板(播放/完播/收藏/评论/解锁收入,只读)")
@GetMapping("/dashboard") @GetMapping("/dashboard")
@PreAuthorize("@ss.hasPermission('creator-center:stat:view')") @PreAuthorize("@ss.hasPermission('creator-center:stat:view')")
public Result<Map<String, Object>> dashboard() { public Result<Map<String, Object>> dashboard() {
@@ -73,6 +83,7 @@ public class CreatorCenterController {
* 代看目标下拉的数据源:只有带 `creator-center:impersonate` 的账号(第一阶段 = 超管)能调。 * 代看目标下拉的数据源:只有带 `creator-center:impersonate` 的账号(第一阶段 = 超管)能调。
* 前端拿它渲染顶部的「当前创作者」下拉;没权限的账号不显示下拉,也不会请求这个接口。 * 前端拿它渲染顶部的「当前创作者」下拉;没权限的账号不显示下拉,也不会请求这个接口。
*/ */
@Operation(summary = "代看目标下拉数据源(仅持 creator-center:impersonate 可调)")
@GetMapping("/impersonation/targets") @GetMapping("/impersonation/targets")
@PreAuthorize("@ss.hasPermission('creator-center:impersonate')") @PreAuthorize("@ss.hasPermission('creator-center:impersonate')")
public Result<List<CreatorCenterTargetVO>> impersonationTargets() { public Result<List<CreatorCenterTargetVO>> impersonationTargets() {
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts.admin.controller; package com.fruitshorts.admin.controller;
import com.fruitshorts.common.pojo.Result; import com.fruitshorts.common.pojo.Result;
@@ -12,6 +14,8 @@ import com.fruitshorts.creator.vo.CreatorDramaEditReqVO;
import com.fruitshorts.creator.vo.CreatorEpisodeReqVO; import com.fruitshorts.creator.vo.CreatorEpisodeReqVO;
import com.fruitshorts.creator.vo.CreatorSubmitReqVO; import com.fruitshorts.creator.vo.CreatorSubmitReqVO;
import com.fruitshorts.framework.log.OperationLog; 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 jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
@@ -32,6 +36,7 @@ import java.util.List;
* 与 {@link CreatorCenterController} 一样**不接收 creatorId**:归属由登录态解析, * 与 {@link CreatorCenterController} 一样**不接收 creatorId**:归属由登录态解析,
* 服务层强制 `where creator_id = 当前创作者`,改别人的剧或分集一律 403。 * 服务层强制 `where creator_id = 当前创作者`,改别人的剧或分集一律 403。
*/ */
@Tag(name = "创作者作品")
@RestController @RestController
@RequestMapping("/admin/creator-center") @RequestMapping("/admin/creator-center")
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -40,6 +45,7 @@ public class CreatorContentController {
private final CreatorContentService creatorContentService; private final CreatorContentService creatorContentService;
/** 新建草稿。 */ /** 新建草稿。 */
@Operation(summary = "新建作品草稿")
@PostMapping("/dramas") @PostMapping("/dramas")
@PreAuthorize("@ss.hasPermission('creator-center:drama:edit')") @PreAuthorize("@ss.hasPermission('creator-center:drama:edit')")
@OperationLog(module = "创作者中心", action = "新建作品") @OperationLog(module = "创作者中心", action = "新建作品")
@@ -48,6 +54,7 @@ public class CreatorContentController {
} }
/** 编辑作品信息(仅草稿 / 已下线可改)。 */ /** 编辑作品信息(仅草稿 / 已下线可改)。 */
@Operation(summary = "编辑作品信息(仅草稿/已下线可改)")
@PutMapping("/dramas/{id}") @PutMapping("/dramas/{id}")
@PreAuthorize("@ss.hasPermission('creator-center:drama:edit')") @PreAuthorize("@ss.hasPermission('creator-center:drama:edit')")
@OperationLog(module = "创作者中心", action = "编辑作品") @OperationLog(module = "创作者中心", action = "编辑作品")
@@ -57,6 +64,7 @@ public class CreatorContentController {
} }
/** 分集列表。 */ /** 分集列表。 */
@Operation(summary = "分集列表")
@GetMapping("/dramas/{id}/episodes") @GetMapping("/dramas/{id}/episodes")
@PreAuthorize("@ss.hasPermission('creator-center:view')") @PreAuthorize("@ss.hasPermission('creator-center:view')")
public Result<List<EpisodeRespVO>> listEpisodes(@PathVariable Long id) { public Result<List<EpisodeRespVO>> listEpisodes(@PathVariable Long id) {
@@ -64,6 +72,7 @@ public class CreatorContentController {
} }
/** 新增分集(集号不传自动排到最后)。 */ /** 新增分集(集号不传自动排到最后)。 */
@Operation(summary = "新增分集(不传集号自动排到最后)")
@PostMapping("/dramas/{id}/episodes") @PostMapping("/dramas/{id}/episodes")
@PreAuthorize("@ss.hasPermission('creator-center:drama:edit')") @PreAuthorize("@ss.hasPermission('creator-center:drama:edit')")
@OperationLog(module = "创作者中心", action = "新增分集") @OperationLog(module = "创作者中心", action = "新增分集")
@@ -72,6 +81,7 @@ public class CreatorContentController {
} }
/** 编辑分集(时长 / 封面 / 片源 / 排序)。 */ /** 编辑分集(时长 / 封面 / 片源 / 排序)。 */
@Operation(summary = "编辑分集(时长/封面/片源/排序)")
@PutMapping("/dramas/{id}/episodes/{episodeId}") @PutMapping("/dramas/{id}/episodes/{episodeId}")
@PreAuthorize("@ss.hasPermission('creator-center:drama:edit')") @PreAuthorize("@ss.hasPermission('creator-center:drama:edit')")
@OperationLog(module = "创作者中心", action = "编辑分集") @OperationLog(module = "创作者中心", action = "编辑分集")
@@ -82,6 +92,7 @@ public class CreatorContentController {
} }
/** 删除分集。 */ /** 删除分集。 */
@Operation(summary = "删除分集")
@DeleteMapping("/dramas/{id}/episodes/{episodeId}") @DeleteMapping("/dramas/{id}/episodes/{episodeId}")
@PreAuthorize("@ss.hasPermission('creator-center:drama:edit')") @PreAuthorize("@ss.hasPermission('creator-center:drama:edit')")
@OperationLog(module = "创作者中心", action = "删除分集") @OperationLog(module = "创作者中心", action = "删除分集")
@@ -91,6 +102,7 @@ public class CreatorContentController {
} }
/** 批量排序:body 里按新顺序给出分集 id。 */ /** 批量排序:body 里按新顺序给出分集 id。 */
@Operation(summary = "批量排序分集")
@PutMapping("/dramas/{id}/episodes/sort") @PutMapping("/dramas/{id}/episodes/sort")
@PreAuthorize("@ss.hasPermission('creator-center:drama:edit')") @PreAuthorize("@ss.hasPermission('creator-center:drama:edit')")
@OperationLog(module = "创作者中心", action = "调整分集顺序") @OperationLog(module = "创作者中心", action = "调整分集顺序")
@@ -100,6 +112,7 @@ public class CreatorContentController {
} }
/** 提交审核:CREATE / UPDATE / PUBLISH / OFFLINE。 */ /** 提交审核:CREATE / UPDATE / PUBLISH / OFFLINE。 */
@Operation(summary = "提交审核(CREATE/UPDATE/PUBLISH/OFFLINE)")
@PostMapping("/dramas/{id}/submit") @PostMapping("/dramas/{id}/submit")
@PreAuthorize("@ss.hasPermission('creator-center:drama:edit')") @PreAuthorize("@ss.hasPermission('creator-center:drama:edit')")
@OperationLog(module = "创作者中心", action = "提交审核") @OperationLog(module = "创作者中心", action = "提交审核")
@@ -108,6 +121,7 @@ public class CreatorContentController {
} }
/** 撤回自己的提交(撤回后作品退回草稿,可继续编辑)。 */ /** 撤回自己的提交(撤回后作品退回草稿,可继续编辑)。 */
@Operation(summary = "撤回提交(撤回后作品退回草稿)")
@PostMapping("/submissions/{id}/withdraw") @PostMapping("/submissions/{id}/withdraw")
@PreAuthorize("@ss.hasPermission('creator-center:drama:edit')") @PreAuthorize("@ss.hasPermission('creator-center:drama:edit')")
@OperationLog(module = "创作者中心", action = "撤回提交") @OperationLog(module = "创作者中心", action = "撤回提交")
@@ -116,6 +130,7 @@ public class CreatorContentController {
} }
/** 我的提交历史(审核进度)。 */ /** 我的提交历史(审核进度)。 */
@Operation(summary = "我的提交历史(审核进度)")
@GetMapping("/dramas/{id}/submissions") @GetMapping("/dramas/{id}/submissions")
@PreAuthorize("@ss.hasPermission('creator-center:view')") @PreAuthorize("@ss.hasPermission('creator-center:view')")
public Result<List<ContentSubmission>> listSubmissions(@PathVariable Long id) { public Result<List<ContentSubmission>> listSubmissions(@PathVariable Long id) {
@@ -123,6 +138,7 @@ public class CreatorContentController {
} }
/** 把上传好的视频产物挂到自己的分集上(产物必须是本人上传的)。 */ /** 把上传好的视频产物挂到自己的分集上(产物必须是本人上传的)。 */
@Operation(summary = "挂载分集视频产物(产物必须是本人上传)")
@PostMapping("/dramas/{id}/episodes/{episodeId}/attach") @PostMapping("/dramas/{id}/episodes/{episodeId}/attach")
@PreAuthorize("@ss.hasPermission('creator-center:upload')") @PreAuthorize("@ss.hasPermission('creator-center:upload')")
@OperationLog(module = "创作者中心", action = "挂载分集视频") @OperationLog(module = "创作者中心", action = "挂载分集视频")
@@ -133,6 +149,7 @@ public class CreatorContentController {
} }
/** 分集收费与试看秒数:读现状。 */ /** 分集收费与试看秒数:读现状。 */
@Operation(summary = "分集收费与试看秒数:读取")
@GetMapping("/dramas/{id}/prices") @GetMapping("/dramas/{id}/prices")
@PreAuthorize("@ss.hasPermission('creator-center:view')") @PreAuthorize("@ss.hasPermission('creator-center:view')")
public Result<PriceRespVO> priceDetail(@PathVariable Long id) { public Result<PriceRespVO> priceDetail(@PathVariable Long id) {
@@ -140,6 +157,7 @@ public class CreatorContentController {
} }
/** 分集收费与试看秒数:保存(复用后台同一套 PriceService,改完立即影响付费判定)。 */ /** 分集收费与试看秒数:保存(复用后台同一套 PriceService,改完立即影响付费判定)。 */
@Operation(summary = "分集收费与试看秒数:保存(改完立即影响付费判定)")
@PutMapping("/dramas/{id}/prices") @PutMapping("/dramas/{id}/prices")
@PreAuthorize("@ss.hasPermission('creator-center:price:update')") @PreAuthorize("@ss.hasPermission('creator-center:price:update')")
@OperationLog(module = "创作者中心", action = "配置分集价格") @OperationLog(module = "创作者中心", action = "配置分集价格")
@@ -1,7 +1,11 @@
// Author: fruit.com.cn
package com.fruitshorts.admin.controller; package com.fruitshorts.admin.controller;
import com.fruitshorts.admin.service.DashboardService; import com.fruitshorts.admin.service.DashboardService;
import com.fruitshorts.common.pojo.Result; 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 lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
@@ -16,6 +20,7 @@ import java.util.Map;
* 指标口径写在 DashboardService 的类注释与 docs/api-phase1.md 里; * 指标口径写在 DashboardService 的类注释与 docs/api-phase1.md 里;
* 取不到数据源的指标返回 null 并在 notes 里说明,不用 0 冒充。 * 取不到数据源的指标返回 null 并在 notes 里说明,不用 0 冒充。
*/ */
@Tag(name = "数据看板")
@RestController @RestController
@RequestMapping("/admin/dashboard") @RequestMapping("/admin/dashboard")
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -24,6 +29,7 @@ public class DashboardAdminController {
private final DashboardService dashboardService; private final DashboardService dashboardService;
/** 总览:用户 / 内容 / 播放 / 收入 / 成本。 */ /** 总览:用户 / 内容 / 播放 / 收入 / 成本。 */
@Operation(summary = "总览:用户/内容/播放/收入/成本")
@GetMapping("/overview") @GetMapping("/overview")
@PreAuthorize("@ss.hasPermission('dashboard:view')") @PreAuthorize("@ss.hasPermission('dashboard:view')")
public Result<Map<String, Object>> overview() { public Result<Map<String, Object>> overview() {
@@ -31,6 +37,7 @@ public class DashboardAdminController {
} }
/** 转码看板:队列积压与失败任务明细。 */ /** 转码看板:队列积压与失败任务明细。 */
@Operation(summary = "转码看板:队列积压与失败任务明细")
@GetMapping("/transcode") @GetMapping("/transcode")
@PreAuthorize("@ss.hasPermission('dashboard:view')") @PreAuthorize("@ss.hasPermission('dashboard:view')")
public Result<Map<String, Object>> transcode() { public Result<Map<String, Object>> transcode() {
@@ -1,9 +1,13 @@
// Author: fruit.com.cn
package com.fruitshorts.admin.controller; package com.fruitshorts.admin.controller;
import com.fruitshorts.common.pojo.Result; import com.fruitshorts.common.pojo.Result;
import com.fruitshorts.content.enums.ContentStatus; import com.fruitshorts.content.enums.ContentStatus;
import com.fruitshorts.content.service.DramaCategoryService; import com.fruitshorts.content.service.DramaCategoryService;
import com.fruitshorts.content.vo.DramaCategoryRespVO; 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 lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
@@ -17,6 +21,7 @@ import java.util.Map;
/** /**
* 字典接口:题材、内容状态等下拉数据。 * 字典接口:题材、内容状态等下拉数据。
*/ */
@Tag(name = "数据字典")
@RestController @RestController
@RequestMapping("/admin/dicts") @RequestMapping("/admin/dicts")
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -24,12 +29,14 @@ public class DictController {
private final DramaCategoryService dramaCategoryService; private final DramaCategoryService dramaCategoryService;
@Operation(summary = "题材字典(下拉数据)")
@GetMapping("/drama-category") @GetMapping("/drama-category")
@PreAuthorize("@ss.hasPermission('drama:list')") @PreAuthorize("@ss.hasPermission('drama:list')")
public Result<List<DramaCategoryRespVO>> dramaCategories() { public Result<List<DramaCategoryRespVO>> dramaCategories() {
return Result.ok(dramaCategoryService.list()); return Result.ok(dramaCategoryService.list());
} }
@Operation(summary = "内容状态字典")
@GetMapping("/content-status") @GetMapping("/content-status")
@PreAuthorize("@ss.hasPermission('drama:list')") @PreAuthorize("@ss.hasPermission('drama:list')")
public Result<List<Map<String, String>>> contentStatus() { public Result<List<Map<String, String>>> contentStatus() {
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts.admin.controller; package com.fruitshorts.admin.controller;
import com.fruitshorts.common.pojo.PageResult; import com.fruitshorts.common.pojo.PageResult;
@@ -6,6 +8,8 @@ import com.fruitshorts.framework.log.OperationLog;
import com.fruitshorts.playback.service.DownloadService; import com.fruitshorts.playback.service.DownloadService;
import com.fruitshorts.playback.vo.DownloadLicenseRespVO; import com.fruitshorts.playback.vo.DownloadLicenseRespVO;
import com.fruitshorts.playback.vo.DownloadQueryReqVO; 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 lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
@@ -18,6 +22,7 @@ import org.springframework.web.bind.annotation.RestController;
import java.util.Map; import java.util.Map;
/** 后台下载授权管理。 */ /** 后台下载授权管理。 */
@Tag(name = "下载管理")
@RestController @RestController
@RequestMapping("/admin/downloads") @RequestMapping("/admin/downloads")
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -25,12 +30,14 @@ public class DownloadAdminController {
private final DownloadService downloadService; private final DownloadService downloadService;
@Operation(summary = "下载授权分页检索(含剧名与集号)")
@GetMapping("/page") @GetMapping("/page")
@PreAuthorize("@ss.hasPermission('download:list')") @PreAuthorize("@ss.hasPermission('download:list')")
public Result<PageResult<DownloadLicenseRespVO>> page(DownloadQueryReqVO req) { public Result<PageResult<DownloadLicenseRespVO>> page(DownloadQueryReqVO req) {
return Result.ok(downloadService.page(req)); return Result.ok(downloadService.page(req));
} }
@Operation(summary = "授权与任务分布统计")
@GetMapping("/stats") @GetMapping("/stats")
@PreAuthorize("@ss.hasPermission('download:list')") @PreAuthorize("@ss.hasPermission('download:list')")
public Result<Map<String, Object>> stats() { public Result<Map<String, Object>> stats() {
@@ -38,6 +45,7 @@ public class DownloadAdminController {
} }
/** 吊销单条授权:用户侧下次联网即失效。 */ /** 吊销单条授权:用户侧下次联网即失效。 */
@Operation(summary = "吊销授权(用户侧下次联网即失效)")
@PutMapping("/{id}/revoke") @PutMapping("/{id}/revoke")
@PreAuthorize("@ss.hasPermission('download:revoke')") @PreAuthorize("@ss.hasPermission('download:revoke')")
@OperationLog(module = "下载管理", action = "吊销下载授权") @OperationLog(module = "下载管理", action = "吊销下载授权")
@@ -47,6 +55,7 @@ public class DownloadAdminController {
} }
/** 手动触发过期清理,正常情况下由定时任务每天执行。 */ /** 手动触发过期清理,正常情况下由定时任务每天执行。 */
@Operation(summary = "手动清理过期授权(默认每天凌晨自动执行)")
@PostMapping("/cleanup") @PostMapping("/cleanup")
@PreAuthorize("@ss.hasPermission('download:revoke')") @PreAuthorize("@ss.hasPermission('download:revoke')")
@OperationLog(module = "下载管理", action = "清理过期下载授权") @OperationLog(module = "下载管理", action = "清理过期下载授权")
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts.admin.controller; package com.fruitshorts.admin.controller;
import com.fruitshorts.common.pojo.Result; import com.fruitshorts.common.pojo.Result;
@@ -6,6 +8,8 @@ import com.fruitshorts.content.vo.DramaCategoryRespVO;
import com.fruitshorts.content.vo.DramaCategorySaveReqVO; import com.fruitshorts.content.vo.DramaCategorySaveReqVO;
import com.fruitshorts.framework.security.SecurityUtils; import com.fruitshorts.framework.security.SecurityUtils;
import com.fruitshorts.framework.log.OperationLog; 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 jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
@@ -23,6 +27,7 @@ import java.util.List;
/** /**
* 短剧类型管理。类型属于短剧域,权限沿用 drama:*,不再单开权限点。 * 短剧类型管理。类型属于短剧域,权限沿用 drama:*,不再单开权限点。
*/ */
@Tag(name = "短剧类型")
@RestController @RestController
@RequestMapping("/admin/drama-categories") @RequestMapping("/admin/drama-categories")
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -30,12 +35,14 @@ public class DramaCategoryController {
private final DramaCategoryService dramaCategoryService; private final DramaCategoryService dramaCategoryService;
@Operation(summary = "短剧类型列表")
@GetMapping @GetMapping
@PreAuthorize("@ss.hasPermission('drama:list')") @PreAuthorize("@ss.hasPermission('drama:list')")
public Result<List<DramaCategoryRespVO>> list() { public Result<List<DramaCategoryRespVO>> list() {
return Result.ok(dramaCategoryService.list()); return Result.ok(dramaCategoryService.list());
} }
@Operation(summary = "新增短剧类型")
@PostMapping @PostMapping
@PreAuthorize("@ss.hasPermission('drama:create')") @PreAuthorize("@ss.hasPermission('drama:create')")
@OperationLog(module = "内容管理", action = "新增短剧类型") @OperationLog(module = "内容管理", action = "新增短剧类型")
@@ -43,6 +50,7 @@ public class DramaCategoryController {
return Result.ok(dramaCategoryService.create(req)); return Result.ok(dramaCategoryService.create(req));
} }
@Operation(summary = "编辑短剧类型")
@PutMapping("/{id}") @PutMapping("/{id}")
@PreAuthorize("@ss.hasPermission('drama:update')") @PreAuthorize("@ss.hasPermission('drama:update')")
@OperationLog(module = "内容管理", action = "编辑短剧类型") @OperationLog(module = "内容管理", action = "编辑短剧类型")
@@ -51,6 +59,7 @@ public class DramaCategoryController {
return Result.ok(); return Result.ok();
} }
@Operation(summary = "删除短剧类型")
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
@PreAuthorize("@ss.hasPermission('drama:delete')") @PreAuthorize("@ss.hasPermission('drama:delete')")
@OperationLog(module = "内容管理", action = "删除短剧类型") @OperationLog(module = "内容管理", action = "删除短剧类型")
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts.admin.controller; package com.fruitshorts.admin.controller;
import com.fruitshorts.common.pojo.PageResult; import com.fruitshorts.common.pojo.PageResult;
@@ -10,6 +12,8 @@ import com.fruitshorts.content.vo.DramaSaveReqVO;
import com.fruitshorts.content.vo.DramaStatusReqVO; import com.fruitshorts.content.vo.DramaStatusReqVO;
import com.fruitshorts.framework.security.SecurityUtils; import com.fruitshorts.framework.security.SecurityUtils;
import com.fruitshorts.framework.log.OperationLog; 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 jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
@@ -24,6 +28,7 @@ import org.springframework.web.bind.annotation.RestController;
import java.util.List; import java.util.List;
@Tag(name = "短剧管理")
@RestController @RestController
@RequestMapping("/admin/dramas") @RequestMapping("/admin/dramas")
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -31,18 +36,21 @@ public class DramaController {
private final DramaService dramaService; private final DramaService dramaService;
@Operation(summary = "短剧分页检索(含热门、状态筛选)")
@GetMapping("/page") @GetMapping("/page")
@PreAuthorize("@ss.hasPermission('drama:list')") @PreAuthorize("@ss.hasPermission('drama:list')")
public Result<PageResult<DramaRespVO>> page(DramaPageReqVO req) { public Result<PageResult<DramaRespVO>> page(DramaPageReqVO req) {
return Result.ok(dramaService.page(req)); return Result.ok(dramaService.page(req));
} }
@Operation(summary = "短剧详情")
@GetMapping("/{id}") @GetMapping("/{id}")
@PreAuthorize("@ss.hasPermission('drama:list')") @PreAuthorize("@ss.hasPermission('drama:list')")
public Result<DramaRespVO> get(@PathVariable Long id) { public Result<DramaRespVO> get(@PathVariable Long id) {
return Result.ok(dramaService.get(id)); return Result.ok(dramaService.get(id));
} }
@Operation(summary = "新建短剧")
@PostMapping @PostMapping
@PreAuthorize("@ss.hasPermission('drama:create')") @PreAuthorize("@ss.hasPermission('drama:create')")
@OperationLog(module = "内容管理", action = "新建短剧") @OperationLog(module = "内容管理", action = "新建短剧")
@@ -50,6 +58,7 @@ public class DramaController {
return Result.ok(dramaService.create(req, SecurityUtils.getUserId())); return Result.ok(dramaService.create(req, SecurityUtils.getUserId()));
} }
@Operation(summary = "编辑短剧")
@PutMapping("/{id}") @PutMapping("/{id}")
@PreAuthorize("@ss.hasPermission('drama:update')") @PreAuthorize("@ss.hasPermission('drama:update')")
@OperationLog(module = "内容管理", action = "编辑短剧") @OperationLog(module = "内容管理", action = "编辑短剧")
@@ -58,6 +67,7 @@ public class DramaController {
return Result.ok(); return Result.ok();
} }
@Operation(summary = "删除短剧")
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
@PreAuthorize("@ss.hasPermission('drama:delete')") @PreAuthorize("@ss.hasPermission('drama:delete')")
@OperationLog(module = "内容管理", action = "删除短剧") @OperationLog(module = "内容管理", action = "删除短剧")
@@ -67,6 +77,7 @@ public class DramaController {
} }
/** 提交审核:内容运营即可操作。 */ /** 提交审核:内容运营即可操作。 */
@Operation(summary = "提交审核")
@PostMapping("/{id}/submit") @PostMapping("/{id}/submit")
@PreAuthorize("@ss.hasAnyPermission('drama:review','drama:update')") @PreAuthorize("@ss.hasAnyPermission('drama:review','drama:update')")
@OperationLog(module = "内容管理", action = "提交审核") @OperationLog(module = "内容管理", action = "提交审核")
@@ -75,6 +86,7 @@ public class DramaController {
return Result.ok(); return Result.ok();
} }
@Operation(summary = "审核通过")
@PostMapping("/{id}/approve") @PostMapping("/{id}/approve")
@PreAuthorize("@ss.hasPermission('drama:review')") @PreAuthorize("@ss.hasPermission('drama:review')")
@OperationLog(module = "内容管理", action = "审核通过") @OperationLog(module = "内容管理", action = "审核通过")
@@ -83,6 +95,7 @@ public class DramaController {
return Result.ok(); return Result.ok();
} }
@Operation(summary = "驳回短剧")
@PostMapping("/{id}/reject") @PostMapping("/{id}/reject")
@PreAuthorize("@ss.hasPermission('drama:review')") @PreAuthorize("@ss.hasPermission('drama:review')")
@OperationLog(module = "内容管理", action = "驳回短剧") @OperationLog(module = "内容管理", action = "驳回短剧")
@@ -91,6 +104,7 @@ public class DramaController {
return Result.ok(); return Result.ok();
} }
@Operation(summary = "短剧上线")
@PostMapping("/{id}/publish") @PostMapping("/{id}/publish")
@PreAuthorize("@ss.hasPermission('drama:review')") @PreAuthorize("@ss.hasPermission('drama:review')")
@OperationLog(module = "内容管理", action = "短剧上线") @OperationLog(module = "内容管理", action = "短剧上线")
@@ -99,6 +113,7 @@ public class DramaController {
return Result.ok(); return Result.ok();
} }
@Operation(summary = "短剧下线")
@PostMapping("/{id}/offline") @PostMapping("/{id}/offline")
@PreAuthorize("@ss.hasPermission('drama:review')") @PreAuthorize("@ss.hasPermission('drama:review')")
@OperationLog(module = "内容管理", action = "短剧下线") @OperationLog(module = "内容管理", action = "短剧下线")
@@ -107,6 +122,7 @@ public class DramaController {
return Result.ok(); return Result.ok();
} }
@Operation(summary = "审核记录")
@GetMapping("/{id}/review-records") @GetMapping("/{id}/review-records")
@PreAuthorize("@ss.hasPermission('drama:list')") @PreAuthorize("@ss.hasPermission('drama:list')")
public Result<List<DramaReviewRecord>> reviewRecords(@PathVariable Long id) { public Result<List<DramaReviewRecord>> reviewRecords(@PathVariable Long id) {
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts.admin.controller; package com.fruitshorts.admin.controller;
import com.fruitshorts.commerce.dal.entity.Entitlement; import com.fruitshorts.commerce.dal.entity.Entitlement;
@@ -6,6 +8,8 @@ import com.fruitshorts.commerce.vo.EntitlementGrantReqVO;
import com.fruitshorts.common.pojo.PageResult; import com.fruitshorts.common.pojo.PageResult;
import com.fruitshorts.common.pojo.Result; import com.fruitshorts.common.pojo.Result;
import com.fruitshorts.framework.log.OperationLog; 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 jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
@@ -19,6 +23,7 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
/** 后台权益:检索、人工补发、回收。 */ /** 后台权益:检索、人工补发、回收。 */
@Tag(name = "权益管理")
@RestController @RestController
@RequestMapping("/admin/entitlements") @RequestMapping("/admin/entitlements")
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -26,6 +31,7 @@ public class EntitlementAdminController {
private final EntitlementService entitlementService; private final EntitlementService entitlementService;
@Operation(summary = "权益分页检索")
@GetMapping("/page") @GetMapping("/page")
@PreAuthorize("@ss.hasPermission('entitlement:list')") @PreAuthorize("@ss.hasPermission('entitlement:list')")
public Result<PageResult<Entitlement>> page(@RequestParam(required = false) Long userId, public Result<PageResult<Entitlement>> page(@RequestParam(required = false) Long userId,
@@ -37,6 +43,7 @@ public class EntitlementAdminController {
} }
/** 客服补偿:直接给用户某集/整剧的播放权益。 */ /** 客服补偿:直接给用户某集/整剧的播放权益。 */
@Operation(summary = "人工补发权益(客服补偿)")
@PostMapping @PostMapping
@PreAuthorize("@ss.hasPermission('entitlement:update')") @PreAuthorize("@ss.hasPermission('entitlement:update')")
@OperationLog(module = "订单管理", action = "人工补发权益") @OperationLog(module = "订单管理", action = "人工补发权益")
@@ -48,6 +55,7 @@ public class EntitlementAdminController {
} }
/** 回收权益:退款、违规处置后用。 */ /** 回收权益:退款、违规处置后用。 */
@Operation(summary = "回收权益(退款/违规处置用,播放判定实时生效)")
@PutMapping("/{id}/revoke") @PutMapping("/{id}/revoke")
@PreAuthorize("@ss.hasPermission('entitlement:update')") @PreAuthorize("@ss.hasPermission('entitlement:update')")
@OperationLog(module = "订单管理", action = "回收权益") @OperationLog(module = "订单管理", action = "回收权益")
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts.admin.controller; package com.fruitshorts.admin.controller;
import com.fruitshorts.common.pojo.Result; import com.fruitshorts.common.pojo.Result;
@@ -6,6 +8,8 @@ import com.fruitshorts.content.vo.EpisodeRespVO;
import com.fruitshorts.content.vo.EpisodeSaveReqVO; import com.fruitshorts.content.vo.EpisodeSaveReqVO;
import com.fruitshorts.content.vo.EpisodeSortReqVO; import com.fruitshorts.content.vo.EpisodeSortReqVO;
import com.fruitshorts.framework.log.OperationLog; 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 jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
@@ -21,6 +25,7 @@ import org.springframework.web.bind.annotation.RestController;
import java.util.List; import java.util.List;
@Tag(name = "剧集管理")
@RestController @RestController
@RequestMapping("/admin/episodes") @RequestMapping("/admin/episodes")
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -28,6 +33,7 @@ public class EpisodeController {
private final EpisodeService episodeService; private final EpisodeService episodeService;
@Operation(summary = "某剧分集列表")
@GetMapping @GetMapping
@PreAuthorize("@ss.hasPermission('episode:list')") @PreAuthorize("@ss.hasPermission('episode:list')")
public Result<List<EpisodeRespVO>> list(@RequestParam Long dramaId, public Result<List<EpisodeRespVO>> list(@RequestParam Long dramaId,
@@ -35,6 +41,7 @@ public class EpisodeController {
return Result.ok(episodeService.listByDrama(dramaId, status)); return Result.ok(episodeService.listByDrama(dramaId, status));
} }
@Operation(summary = "新增分集")
@PostMapping @PostMapping
@PreAuthorize("@ss.hasPermission('episode:upload')") @PreAuthorize("@ss.hasPermission('episode:upload')")
@OperationLog(module = "内容管理", action = "新增剧集") @OperationLog(module = "内容管理", action = "新增剧集")
@@ -42,6 +49,7 @@ public class EpisodeController {
return Result.ok(episodeService.create(req)); return Result.ok(episodeService.create(req));
} }
@Operation(summary = "编辑分集")
@PutMapping("/{id}") @PutMapping("/{id}")
@PreAuthorize("@ss.hasPermission('episode:upload')") @PreAuthorize("@ss.hasPermission('episode:upload')")
@OperationLog(module = "内容管理", action = "编辑剧集") @OperationLog(module = "内容管理", action = "编辑剧集")
@@ -50,6 +58,7 @@ public class EpisodeController {
return Result.ok(); return Result.ok();
} }
@Operation(summary = "删除分集")
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
@PreAuthorize("@ss.hasPermission('episode:upload')") @PreAuthorize("@ss.hasPermission('episode:upload')")
@OperationLog(module = "内容管理", action = "删除剧集") @OperationLog(module = "内容管理", action = "删除剧集")
@@ -58,6 +67,7 @@ public class EpisodeController {
return Result.ok(); return Result.ok();
} }
@Operation(summary = "批量排序分集")
@PutMapping("/sort") @PutMapping("/sort")
@PreAuthorize("@ss.hasPermission('episode:sort')") @PreAuthorize("@ss.hasPermission('episode:sort')")
@OperationLog(module = "内容管理", action = "调整剧集顺序") @OperationLog(module = "内容管理", action = "调整剧集顺序")
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts.admin.controller; package com.fruitshorts.admin.controller;
import com.fruitshorts.common.pojo.Result; import com.fruitshorts.common.pojo.Result;
@@ -9,6 +11,8 @@ import com.fruitshorts.media.vo.UploadCompleteReqVO;
import com.fruitshorts.media.vo.UploadInitReqVO; import com.fruitshorts.media.vo.UploadInitReqVO;
import com.fruitshorts.media.vo.UploadInitRespVO; import com.fruitshorts.media.vo.UploadInitRespVO;
import com.fruitshorts.media.vo.UploadProgressRespVO; 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 jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
@@ -24,6 +28,7 @@ import org.springframework.web.multipart.MultipartFile;
/** /**
* 媒资上传(本地存储版):init 建会话 → chunk 传分片 → complete 合并出地址。 * 媒资上传(本地存储版):init 建会话 → chunk 传分片 → complete 合并出地址。
*/ */
@Tag(name = "媒资上传")
@RestController @RestController
@RequestMapping("/admin/media") @RequestMapping("/admin/media")
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -31,6 +36,7 @@ public class MediaController {
private final MediaService mediaService; private final MediaService mediaService;
@Operation(summary = "建上传会话(返回 uploadId / chunkSize / totalChunks)")
@PostMapping("/upload/init") @PostMapping("/upload/init")
@PreAuthorize("@ss.hasAnyPermission('cover:upload','episode:upload')") @PreAuthorize("@ss.hasAnyPermission('cover:upload','episode:upload')")
public Result<UploadInitRespVO> init(@Valid @RequestBody UploadInitReqVO req) { public Result<UploadInitRespVO> init(@Valid @RequestBody UploadInitReqVO req) {
@@ -39,6 +45,7 @@ public class MediaController {
return Result.ok(mediaService.init(req, SecurityUtils.getUserId())); return Result.ok(mediaService.init(req, SecurityUtils.getUserId()));
} }
@Operation(summary = "上传分片(可重复传同一片)")
@PostMapping("/upload/chunk") @PostMapping("/upload/chunk")
@PreAuthorize("@ss.hasAnyPermission('cover:upload','episode:upload')") @PreAuthorize("@ss.hasAnyPermission('cover:upload','episode:upload')")
public Result<Integer> uploadChunk(@RequestParam String uploadId, public Result<Integer> uploadChunk(@RequestParam String uploadId,
@@ -49,6 +56,7 @@ public class MediaController {
return Result.ok(index); return Result.ok(index);
} }
@Operation(summary = "合并分片出媒资(同一 uploadId 幂等)")
@PostMapping("/upload/complete") @PostMapping("/upload/complete")
@PreAuthorize("@ss.hasAnyPermission('cover:upload','episode:upload')") @PreAuthorize("@ss.hasAnyPermission('cover:upload','episode:upload')")
public Result<MediaAssetRespVO> complete(@Valid @RequestBody UploadCompleteReqVO req) { public Result<MediaAssetRespVO> complete(@Valid @RequestBody UploadCompleteReqVO req) {
@@ -57,12 +65,14 @@ public class MediaController {
} }
/** 断点续传:查上传会话进度(只能查自己发起的会话)。 */ /** 断点续传:查上传会话进度(只能查自己发起的会话)。 */
@Operation(summary = "断点续传进度查询(只能查自己发起的会话)")
@GetMapping("/upload/{uploadId}") @GetMapping("/upload/{uploadId}")
@PreAuthorize("@ss.hasAnyPermission('cover:upload','episode:upload')") @PreAuthorize("@ss.hasAnyPermission('cover:upload','episode:upload')")
public Result<UploadProgressRespVO> progress(@PathVariable String uploadId) { public Result<UploadProgressRespVO> progress(@PathVariable String uploadId) {
return Result.ok(mediaService.progress(uploadId, SecurityUtils.getUserId())); return Result.ok(mediaService.progress(uploadId, SecurityUtils.getUserId()));
} }
@Operation(summary = "媒资详情")
@GetMapping("/{id}") @GetMapping("/{id}")
@PreAuthorize("@ss.hasAnyPermission('cover:upload','episode:upload')") @PreAuthorize("@ss.hasAnyPermission('cover:upload','episode:upload')")
public Result<MediaAssetRespVO> get(@PathVariable Long id) { public Result<MediaAssetRespVO> get(@PathVariable Long id) {
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts.admin.controller; package com.fruitshorts.admin.controller;
import com.fruitshorts.common.pojo.Result; import com.fruitshorts.common.pojo.Result;
@@ -5,6 +7,8 @@ import com.fruitshorts.system.service.MenuService;
import com.fruitshorts.framework.log.OperationLog; import com.fruitshorts.framework.log.OperationLog;
import com.fruitshorts.system.vo.MenuSaveReqVO; import com.fruitshorts.system.vo.MenuSaveReqVO;
import com.fruitshorts.system.vo.MenuVO; 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 jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
@@ -19,6 +23,7 @@ import org.springframework.web.bind.annotation.RestController;
import java.util.List; import java.util.List;
@Tag(name = "菜单管理")
@RestController @RestController
@RequestMapping("/admin/menus") @RequestMapping("/admin/menus")
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -26,12 +31,14 @@ public class MenuController {
private final MenuService menuService; private final MenuService menuService;
@Operation(summary = "菜单树")
@GetMapping("/tree") @GetMapping("/tree")
@PreAuthorize("@ss.hasPermission('system:menu')") @PreAuthorize("@ss.hasPermission('system:menu')")
public Result<List<MenuVO>> tree() { public Result<List<MenuVO>> tree() {
return Result.ok(menuService.listAll()); return Result.ok(menuService.listAll());
} }
@Operation(summary = "新增菜单")
@PostMapping @PostMapping
@PreAuthorize("@ss.hasPermission('system:menu')") @PreAuthorize("@ss.hasPermission('system:menu')")
@OperationLog(module = "后台管理", action = "新增菜单") @OperationLog(module = "后台管理", action = "新增菜单")
@@ -39,6 +46,7 @@ public class MenuController {
return Result.ok(menuService.create(req)); return Result.ok(menuService.create(req));
} }
@Operation(summary = "编辑菜单")
@PutMapping("/{id}") @PutMapping("/{id}")
@PreAuthorize("@ss.hasPermission('system:menu')") @PreAuthorize("@ss.hasPermission('system:menu')")
@OperationLog(module = "后台管理", action = "编辑菜单") @OperationLog(module = "后台管理", action = "编辑菜单")
@@ -47,6 +55,7 @@ public class MenuController {
return Result.ok(); return Result.ok();
} }
@Operation(summary = "删除菜单")
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
@PreAuthorize("@ss.hasPermission('system:menu')") @PreAuthorize("@ss.hasPermission('system:menu')")
@OperationLog(module = "后台管理", action = "删除菜单") @OperationLog(module = "后台管理", action = "删除菜单")
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts.admin.controller; package com.fruitshorts.admin.controller;
import com.fruitshorts.common.pojo.PageResult; import com.fruitshorts.common.pojo.PageResult;
@@ -5,6 +7,8 @@ import com.fruitshorts.common.pojo.Result;
import com.fruitshorts.system.service.OperationLogService; import com.fruitshorts.system.service.OperationLogService;
import com.fruitshorts.system.vo.OperationLogPageReqVO; import com.fruitshorts.system.vo.OperationLogPageReqVO;
import com.fruitshorts.system.vo.OperationLogRespVO; 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 lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
@@ -12,6 +16,7 @@ import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
@Tag(name = "操作日志")
@RestController @RestController
@RequestMapping("/admin/operation-logs") @RequestMapping("/admin/operation-logs")
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -19,12 +24,14 @@ public class OperationLogController {
private final OperationLogService operationLogService; private final OperationLogService operationLogService;
@Operation(summary = "操作日志分页检索")
@GetMapping("/page") @GetMapping("/page")
@PreAuthorize("@ss.hasPermission('log:list')") @PreAuthorize("@ss.hasPermission('log:list')")
public Result<PageResult<OperationLogRespVO>> page(OperationLogPageReqVO req) { public Result<PageResult<OperationLogRespVO>> page(OperationLogPageReqVO req) {
return Result.ok(operationLogService.page(req)); return Result.ok(operationLogService.page(req));
} }
@Operation(summary = "操作日志详情")
@GetMapping("/{id}") @GetMapping("/{id}")
@PreAuthorize("@ss.hasPermission('log:list')") @PreAuthorize("@ss.hasPermission('log:list')")
public Result<OperationLogRespVO> get(@PathVariable Long id) { public Result<OperationLogRespVO> get(@PathVariable Long id) {
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts.admin.controller; package com.fruitshorts.admin.controller;
import com.fruitshorts.commerce.dal.entity.OrderEvent; import com.fruitshorts.commerce.dal.entity.OrderEvent;
@@ -8,6 +10,8 @@ import com.fruitshorts.common.pojo.PageResult;
import com.fruitshorts.common.pojo.Result; import com.fruitshorts.common.pojo.Result;
import com.fruitshorts.framework.log.OperationLog; import com.fruitshorts.framework.log.OperationLog;
import com.fruitshorts.framework.security.SecurityUtils; 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 lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
@@ -22,6 +26,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
/** 后台订单:检索、补发、退款回收。 */ /** 后台订单:检索、补发、退款回收。 */
@Tag(name = "订单管理")
@RestController @RestController
@RequestMapping("/admin/orders") @RequestMapping("/admin/orders")
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -29,12 +34,14 @@ public class OrderAdminController {
private final OrderService orderService; private final OrderService orderService;
@Operation(summary = "订单分页检索")
@GetMapping("/page") @GetMapping("/page")
@PreAuthorize("@ss.hasPermission('order:list')") @PreAuthorize("@ss.hasPermission('order:list')")
public Result<PageResult<Orders>> page(OrderQueryReqVO req) { public Result<PageResult<Orders>> page(OrderQueryReqVO req) {
return Result.ok(orderService.page(req)); return Result.ok(orderService.page(req));
} }
@Operation(summary = "订单详情")
@GetMapping("/{orderNo}") @GetMapping("/{orderNo}")
@PreAuthorize("@ss.hasPermission('order:list')") @PreAuthorize("@ss.hasPermission('order:list')")
public Result<Orders> detail(@PathVariable String orderNo) { public Result<Orders> detail(@PathVariable String orderNo) {
@@ -42,6 +49,7 @@ public class OrderAdminController {
} }
/** 订单状态流转流水:支付争议时的证据链。 */ /** 订单状态流转流水:支付争议时的证据链。 */
@Operation(summary = "订单状态流转流水(支付争议证据链)")
@GetMapping("/{orderNo}/events") @GetMapping("/{orderNo}/events")
@PreAuthorize("@ss.hasPermission('order:list')") @PreAuthorize("@ss.hasPermission('order:list')")
public Result<List<OrderEvent>> events(@PathVariable String orderNo) { public Result<List<OrderEvent>> events(@PathVariable String orderNo) {
@@ -49,6 +57,7 @@ public class OrderAdminController {
} }
/** 补发:已支付未交付的订单重新交付;已交付时幂等返回。 */ /** 补发:已支付未交付的订单重新交付;已交付时幂等返回。 */
@Operation(summary = "订单补发(已交付时幂等返回)")
@PostMapping("/{orderNo}/compensate") @PostMapping("/{orderNo}/compensate")
@PreAuthorize("@ss.hasPermission('order:update')") @PreAuthorize("@ss.hasPermission('order:update')")
@OperationLog(module = "订单管理", action = "补发订单") @OperationLog(module = "订单管理", action = "补发订单")
@@ -57,6 +66,7 @@ public class OrderAdminController {
} }
/** 退款/撤销:回收权益并扣回金币。 */ /** 退款/撤销:回收权益并扣回金币。 */
@Operation(summary = "订单退款/撤销(回收权益并扣回金币)")
@PostMapping("/{orderNo}/refund") @PostMapping("/{orderNo}/refund")
@PreAuthorize("@ss.hasPermission('order:update')") @PreAuthorize("@ss.hasPermission('order:update')")
@OperationLog(module = "订单管理", action = "订单退款") @OperationLog(module = "订单管理", action = "订单退款")
@@ -1,8 +1,12 @@
// Author: fruit.com.cn
package com.fruitshorts.admin.controller; package com.fruitshorts.admin.controller;
import com.fruitshorts.common.pojo.Result; import com.fruitshorts.common.pojo.Result;
import com.fruitshorts.system.service.PermissionService; import com.fruitshorts.system.service.PermissionService;
import com.fruitshorts.system.vo.PermissionGroupVO; 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 lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
@@ -11,6 +15,7 @@ import org.springframework.web.bind.annotation.RestController;
import java.util.List; import java.util.List;
@Tag(name = "权限管理")
@RestController @RestController
@RequestMapping("/admin/permissions") @RequestMapping("/admin/permissions")
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -19,6 +24,7 @@ public class PermissionController {
private final PermissionService permissionService; private final PermissionService permissionService;
/** 按分组返回权限点,直接喂给前端的角色权限配置页。 */ /** 按分组返回权限点,直接喂给前端的角色权限配置页。 */
@Operation(summary = "权限点分组列表(角色权限配置页用)")
@GetMapping @GetMapping
@PreAuthorize("@ss.hasPermission('system:role')") @PreAuthorize("@ss.hasPermission('system:role')")
public Result<List<PermissionGroupVO>> groups() { public Result<List<PermissionGroupVO>> groups() {
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts.admin.controller; package com.fruitshorts.admin.controller;
import com.fruitshorts.common.pojo.Result; import com.fruitshorts.common.pojo.Result;
@@ -5,6 +7,8 @@ import com.fruitshorts.content.service.PriceService;
import com.fruitshorts.content.vo.PriceRespVO; import com.fruitshorts.content.vo.PriceRespVO;
import com.fruitshorts.content.vo.PriceSaveReqVO; import com.fruitshorts.content.vo.PriceSaveReqVO;
import com.fruitshorts.framework.log.OperationLog; 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 lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.PutMapping;
@@ -15,6 +19,7 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
/** 后台分集价格与免费集配置。 */ /** 后台分集价格与免费集配置。 */
@Tag(name = "分集价格")
@RestController @RestController
@RequestMapping("/admin/prices") @RequestMapping("/admin/prices")
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -23,12 +28,14 @@ public class PriceAdminController {
private final PriceService priceService; private final PriceService priceService;
/** 当前价格配置:整剧默认 + 分集明细。 */ /** 当前价格配置:整剧默认 + 分集明细。 */
@Operation(summary = "当前价格配置:整剧默认 + 分集明细")
@GetMapping @GetMapping
@PreAuthorize("@ss.hasPermission('price:update')") @PreAuthorize("@ss.hasPermission('price:update')")
public Result<PriceRespVO> detail(@RequestParam Long dramaId) { public Result<PriceRespVO> detail(@RequestParam Long dramaId) {
return Result.ok(priceService.detail(dramaId)); return Result.ok(priceService.detail(dramaId));
} }
@Operation(summary = "保存价格配置(改价立即影响播放与下载判定)")
@PutMapping @PutMapping
@PreAuthorize("@ss.hasPermission('price:update')") @PreAuthorize("@ss.hasPermission('price:update')")
@OperationLog(module = "定价管理", action = "配置分集价格") @OperationLog(module = "定价管理", action = "配置分集价格")
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts.admin.controller; package com.fruitshorts.admin.controller;
import com.fruitshorts.commerce.dal.entity.ReconcileTask; import com.fruitshorts.commerce.dal.entity.ReconcileTask;
@@ -5,6 +7,8 @@ import com.fruitshorts.commerce.service.ReconcileService;
import com.fruitshorts.common.pojo.PageResult; import com.fruitshorts.common.pojo.PageResult;
import com.fruitshorts.common.pojo.Result; import com.fruitshorts.common.pojo.Result;
import com.fruitshorts.framework.log.OperationLog; 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 lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
@@ -16,6 +20,7 @@ import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDate; import java.time.LocalDate;
/** 后台对账:手动触发与历史查看。 */ /** 后台对账:手动触发与历史查看。 */
@Tag(name = "对账管理")
@RestController @RestController
@RequestMapping("/admin/reconcile") @RequestMapping("/admin/reconcile")
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -23,6 +28,7 @@ public class ReconcileAdminController {
private final ReconcileService reconcileService; private final ReconcileService reconcileService;
@Operation(summary = "对账历史分页")
@GetMapping("/page") @GetMapping("/page")
@PreAuthorize("@ss.hasPermission('order:list')") @PreAuthorize("@ss.hasPermission('order:list')")
public Result<PageResult<ReconcileTask>> page(@RequestParam(defaultValue = "1") Integer pageIndex, public Result<PageResult<ReconcileTask>> page(@RequestParam(defaultValue = "1") Integer pageIndex,
@@ -31,6 +37,7 @@ public class ReconcileAdminController {
} }
/** 手动对账:默认前一天,可指定日期与渠道。 */ /** 手动对账:默认前一天,可指定日期与渠道。 */
@Operation(summary = "手动对账(默认前一天,可指定日期与渠道)")
@PostMapping("/run") @PostMapping("/run")
@PreAuthorize("@ss.hasPermission('reconcile:run')") @PreAuthorize("@ss.hasPermission('reconcile:run')")
@OperationLog(module = "订单管理", action = "手动对账") @OperationLog(module = "订单管理", action = "手动对账")
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts.admin.controller; package com.fruitshorts.admin.controller;
import com.fruitshorts.common.pojo.PageResult; import com.fruitshorts.common.pojo.PageResult;
@@ -8,6 +10,8 @@ import com.fruitshorts.interaction.dal.entity.ContentReport;
import com.fruitshorts.interaction.service.ReportService; import com.fruitshorts.interaction.service.ReportService;
import com.fruitshorts.interaction.vo.ReportHandleReqVO; import com.fruitshorts.interaction.vo.ReportHandleReqVO;
import com.fruitshorts.interaction.vo.ReportQueryReqVO; 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 lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
@@ -18,6 +22,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
/** 后台举报工单。 */ /** 后台举报工单。 */
@Tag(name = "举报工单")
@RestController @RestController
@RequestMapping("/admin/reports") @RequestMapping("/admin/reports")
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -25,6 +30,7 @@ public class ReportAdminController {
private final ReportService reportService; private final ReportService reportService;
@Operation(summary = "举报工单检索(状态/类型/原因)")
@GetMapping("/page") @GetMapping("/page")
@PreAuthorize("@ss.hasPermission('report:list')") @PreAuthorize("@ss.hasPermission('report:list')")
public Result<PageResult<ContentReport>> page(ReportQueryReqVO req) { public Result<PageResult<ContentReport>> page(ReportQueryReqVO req) {
@@ -32,6 +38,7 @@ public class ReportAdminController {
} }
/** 处理工单:结论成立会联动驳回被举报评论,可选择同时封禁被举报用户。 */ /** 处理工单:结论成立会联动驳回被举报评论,可选择同时封禁被举报用户。 */
@Operation(summary = "处理工单(结论成立联动驳回被举报评论,可选择同时封禁用户)")
@PutMapping("/{id}/handle") @PutMapping("/{id}/handle")
@PreAuthorize("@ss.hasPermission('report:handle')") @PreAuthorize("@ss.hasPermission('report:handle')")
@OperationLog(module = "内容治理", action = "处理举报") @OperationLog(module = "内容治理", action = "处理举报")
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts.admin.controller; package com.fruitshorts.admin.controller;
import com.fruitshorts.common.pojo.Result; import com.fruitshorts.common.pojo.Result;
@@ -5,6 +7,8 @@ import com.fruitshorts.system.service.RoleCategoryService;
import com.fruitshorts.framework.log.OperationLog; import com.fruitshorts.framework.log.OperationLog;
import com.fruitshorts.system.vo.RoleCategoryRespVO; import com.fruitshorts.system.vo.RoleCategoryRespVO;
import com.fruitshorts.system.vo.RoleCategorySaveReqVO; 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 jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
@@ -19,6 +23,7 @@ import org.springframework.web.bind.annotation.RestController;
import java.util.List; import java.util.List;
@Tag(name = "角色分类")
@RestController @RestController
@RequestMapping("/admin/role-categories") @RequestMapping("/admin/role-categories")
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -26,12 +31,14 @@ public class RoleCategoryController {
private final RoleCategoryService roleCategoryService; private final RoleCategoryService roleCategoryService;
@Operation(summary = "角色分类列表")
@GetMapping @GetMapping
@PreAuthorize("@ss.hasPermission('system:role-category')") @PreAuthorize("@ss.hasPermission('system:role-category')")
public Result<List<RoleCategoryRespVO>> list() { public Result<List<RoleCategoryRespVO>> list() {
return Result.ok(roleCategoryService.list()); return Result.ok(roleCategoryService.list());
} }
@Operation(summary = "新增角色分类")
@PostMapping @PostMapping
@PreAuthorize("@ss.hasPermission('system:role-category')") @PreAuthorize("@ss.hasPermission('system:role-category')")
@OperationLog(module = "后台管理", action = "新增角色分类") @OperationLog(module = "后台管理", action = "新增角色分类")
@@ -39,6 +46,7 @@ public class RoleCategoryController {
return Result.ok(roleCategoryService.create(req)); return Result.ok(roleCategoryService.create(req));
} }
@Operation(summary = "编辑角色分类")
@PutMapping("/{id}") @PutMapping("/{id}")
@PreAuthorize("@ss.hasPermission('system:role-category')") @PreAuthorize("@ss.hasPermission('system:role-category')")
@OperationLog(module = "后台管理", action = "编辑角色分类") @OperationLog(module = "后台管理", action = "编辑角色分类")
@@ -47,6 +55,7 @@ public class RoleCategoryController {
return Result.ok(); return Result.ok();
} }
@Operation(summary = "删除角色分类")
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
@PreAuthorize("@ss.hasPermission('system:role-category')") @PreAuthorize("@ss.hasPermission('system:role-category')")
@OperationLog(module = "后台管理", action = "删除角色分类") @OperationLog(module = "后台管理", action = "删除角色分类")
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts.admin.controller; package com.fruitshorts.admin.controller;
import com.fruitshorts.common.pojo.PageResult; import com.fruitshorts.common.pojo.PageResult;
@@ -7,6 +9,8 @@ import com.fruitshorts.framework.log.OperationLog;
import com.fruitshorts.system.vo.RolePermissionSaveReqVO; import com.fruitshorts.system.vo.RolePermissionSaveReqVO;
import com.fruitshorts.system.vo.RoleRespVO; import com.fruitshorts.system.vo.RoleRespVO;
import com.fruitshorts.system.vo.RoleSaveReqVO; 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 jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
@@ -22,6 +26,7 @@ import org.springframework.web.bind.annotation.RestController;
import java.util.List; import java.util.List;
@Tag(name = "角色管理")
@RestController @RestController
@RequestMapping("/admin/roles") @RequestMapping("/admin/roles")
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -29,6 +34,7 @@ public class RoleController {
private final RoleService roleService; private final RoleService roleService;
@Operation(summary = "角色分页检索")
@GetMapping("/page") @GetMapping("/page")
@PreAuthorize("@ss.hasPermission('system:role')") @PreAuthorize("@ss.hasPermission('system:role')")
public Result<PageResult<RoleRespVO>> page(@RequestParam(required = false) String keyword, public Result<PageResult<RoleRespVO>> page(@RequestParam(required = false) String keyword,
@@ -37,17 +43,20 @@ public class RoleController {
return Result.ok(roleService.page(keyword, pageIndex, pageSize)); return Result.ok(roleService.page(keyword, pageIndex, pageSize));
} }
@Operation(summary = "角色全量列表")
@GetMapping("/list") @GetMapping("/list")
public Result<List<RoleRespVO>> list() { public Result<List<RoleRespVO>> list() {
return Result.ok(roleService.listAll()); return Result.ok(roleService.listAll());
} }
@Operation(summary = "角色详情")
@GetMapping("/{id}") @GetMapping("/{id}")
@PreAuthorize("@ss.hasPermission('system:role')") @PreAuthorize("@ss.hasPermission('system:role')")
public Result<RoleRespVO> get(@PathVariable Long id) { public Result<RoleRespVO> get(@PathVariable Long id) {
return Result.ok(roleService.get(id)); return Result.ok(roleService.get(id));
} }
@Operation(summary = "新增角色")
@PostMapping @PostMapping
@PreAuthorize("@ss.hasPermission('system:role')") @PreAuthorize("@ss.hasPermission('system:role')")
@OperationLog(module = "后台管理", action = "新增角色") @OperationLog(module = "后台管理", action = "新增角色")
@@ -55,6 +64,7 @@ public class RoleController {
return Result.ok(roleService.create(req)); return Result.ok(roleService.create(req));
} }
@Operation(summary = "编辑角色")
@PutMapping("/{id}") @PutMapping("/{id}")
@PreAuthorize("@ss.hasPermission('system:role')") @PreAuthorize("@ss.hasPermission('system:role')")
@OperationLog(module = "后台管理", action = "编辑角色") @OperationLog(module = "后台管理", action = "编辑角色")
@@ -63,6 +73,7 @@ public class RoleController {
return Result.ok(); return Result.ok();
} }
@Operation(summary = "删除角色")
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
@PreAuthorize("@ss.hasPermission('system:role')") @PreAuthorize("@ss.hasPermission('system:role')")
@OperationLog(module = "后台管理", action = "删除角色") @OperationLog(module = "后台管理", action = "删除角色")
@@ -71,12 +82,14 @@ public class RoleController {
return Result.ok(); return Result.ok();
} }
@Operation(summary = "查询角色权限码")
@GetMapping("/{id}/permissions") @GetMapping("/{id}/permissions")
@PreAuthorize("@ss.hasPermission('system:role')") @PreAuthorize("@ss.hasPermission('system:role')")
public Result<List<String>> permissions(@PathVariable Long id) { public Result<List<String>> permissions(@PathVariable Long id) {
return Result.ok(roleService.listCodesByRoleId(id)); return Result.ok(roleService.listCodesByRoleId(id));
} }
@Operation(summary = "保存角色权限")
@PutMapping("/{id}/permissions") @PutMapping("/{id}/permissions")
@PreAuthorize("@ss.hasPermission('system:role')") @PreAuthorize("@ss.hasPermission('system:role')")
@OperationLog(module = "后台管理", action = "修改角色权限") @OperationLog(module = "后台管理", action = "修改角色权限")
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts.admin.controller; package com.fruitshorts.admin.controller;
import com.fruitshorts.common.pojo.PageResult; import com.fruitshorts.common.pojo.PageResult;
@@ -6,6 +8,8 @@ import com.fruitshorts.framework.log.OperationLog;
import com.fruitshorts.interaction.dal.entity.SensitiveWord; import com.fruitshorts.interaction.dal.entity.SensitiveWord;
import com.fruitshorts.interaction.service.SensitiveWordService; import com.fruitshorts.interaction.service.SensitiveWordService;
import com.fruitshorts.interaction.vo.SensitiveWordReqVO; 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 jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
@@ -20,6 +24,7 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
/** 后台敏感词库。改动即时生效(服务端词库缓存会失效重建)。 */ /** 后台敏感词库。改动即时生效(服务端词库缓存会失效重建)。 */
@Tag(name = "敏感词")
@RestController @RestController
@RequestMapping("/admin/sensitive-words") @RequestMapping("/admin/sensitive-words")
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -27,6 +32,7 @@ public class SensitiveWordAdminController {
private final SensitiveWordService sensitiveWordService; private final SensitiveWordService sensitiveWordService;
@Operation(summary = "敏感词分页检索")
@GetMapping("/page") @GetMapping("/page")
@PreAuthorize("@ss.hasPermission('sensitive-word:list')") @PreAuthorize("@ss.hasPermission('sensitive-word:list')")
public Result<PageResult<SensitiveWord>> page(@RequestParam(required = false) String keyword, public Result<PageResult<SensitiveWord>> page(@RequestParam(required = false) String keyword,
@@ -37,6 +43,7 @@ public class SensitiveWordAdminController {
return Result.ok(sensitiveWordService.page(keyword, category, level, pageIndex, pageSize)); return Result.ok(sensitiveWordService.page(keyword, category, level, pageIndex, pageSize));
} }
@Operation(summary = "新增敏感词")
@PostMapping @PostMapping
@PreAuthorize("@ss.hasPermission('sensitive-word:update')") @PreAuthorize("@ss.hasPermission('sensitive-word:update')")
@OperationLog(module = "内容治理", action = "新增敏感词") @OperationLog(module = "内容治理", action = "新增敏感词")
@@ -44,6 +51,7 @@ public class SensitiveWordAdminController {
return Result.ok(sensitiveWordService.create(req)); return Result.ok(sensitiveWordService.create(req));
} }
@Operation(summary = "修改敏感词")
@PutMapping("/{id}") @PutMapping("/{id}")
@PreAuthorize("@ss.hasPermission('sensitive-word:update')") @PreAuthorize("@ss.hasPermission('sensitive-word:update')")
@OperationLog(module = "内容治理", action = "修改敏感词") @OperationLog(module = "内容治理", action = "修改敏感词")
@@ -52,6 +60,7 @@ public class SensitiveWordAdminController {
return Result.ok(); return Result.ok();
} }
@Operation(summary = "删除敏感词")
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
@PreAuthorize("@ss.hasPermission('sensitive-word:update')") @PreAuthorize("@ss.hasPermission('sensitive-word:update')")
@OperationLog(module = "内容治理", action = "删除敏感词") @OperationLog(module = "内容治理", action = "删除敏感词")
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts.admin.controller; package com.fruitshorts.admin.controller;
import com.fruitshorts.common.pojo.PageResult; import com.fruitshorts.common.pojo.PageResult;
@@ -8,6 +10,8 @@ import com.fruitshorts.creator.vo.SubmissionAuditReqVO;
import com.fruitshorts.creator.vo.SubmissionQueryReqVO; import com.fruitshorts.creator.vo.SubmissionQueryReqVO;
import com.fruitshorts.framework.log.OperationLog; import com.fruitshorts.framework.log.OperationLog;
import com.fruitshorts.framework.security.SecurityUtils; 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 lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
@@ -18,6 +22,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
/** 后台内容提交审核:检索、详情、审核(通过 / 驳回 / 撤回)。 */ /** 后台内容提交审核:检索、详情、审核(通过 / 驳回 / 撤回)。 */
@Tag(name = "内容提交审核")
@RestController @RestController
@RequestMapping("/admin/submissions") @RequestMapping("/admin/submissions")
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -25,18 +30,21 @@ public class SubmissionAdminController {
private final ContentSubmissionService submissionService; private final ContentSubmissionService submissionService;
@Operation(summary = "内容提交分页检索")
@GetMapping("/page") @GetMapping("/page")
@PreAuthorize("@ss.hasPermission('submission:list')") @PreAuthorize("@ss.hasPermission('submission:list')")
public Result<PageResult<ContentSubmission>> page(SubmissionQueryReqVO req) { public Result<PageResult<ContentSubmission>> page(SubmissionQueryReqVO req) {
return Result.ok(submissionService.page(req)); return Result.ok(submissionService.page(req));
} }
@Operation(summary = "内容提交详情")
@GetMapping("/{id}") @GetMapping("/{id}")
@PreAuthorize("@ss.hasPermission('submission:list')") @PreAuthorize("@ss.hasPermission('submission:list')")
public Result<ContentSubmission> detail(@PathVariable Long id) { public Result<ContentSubmission> detail(@PathVariable Long id) {
return Result.ok(submissionService.getExisting(id)); return Result.ok(submissionService.getExisting(id));
} }
@Operation(summary = "审核内容提交(通过后按类型落短剧状态机)")
@PutMapping("/{id}/audit") @PutMapping("/{id}/audit")
@PreAuthorize("@ss.hasPermission('submission:audit')") @PreAuthorize("@ss.hasPermission('submission:audit')")
@OperationLog(module = "内容审核", action = "提交审核") @OperationLog(module = "内容审核", action = "提交审核")
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts.admin.controller; package com.fruitshorts.admin.controller;
import com.fruitshorts.commerce.dal.entity.CoinLedger; import com.fruitshorts.commerce.dal.entity.CoinLedger;
@@ -9,6 +11,8 @@ import com.fruitshorts.common.pojo.Result;
import com.fruitshorts.framework.log.OperationLog; import com.fruitshorts.framework.log.OperationLog;
import com.fruitshorts.framework.security.SecurityUtils; import com.fruitshorts.framework.security.SecurityUtils;
import com.fruitshorts.user.dal.mapper.AppUserMapper; 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 jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
@@ -23,6 +27,7 @@ import org.springframework.web.bind.annotation.RestController;
import java.util.List; import java.util.List;
/** 后台钱包:人工补币/扣回,全部进流水且必须带幂等键。 */ /** 后台钱包:人工补币/扣回,全部进流水且必须带幂等键。 */
@Tag(name = "钱包管理")
@RestController @RestController
@RequestMapping("/admin/wallets") @RequestMapping("/admin/wallets")
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -31,6 +36,7 @@ public class WalletAdminController {
private final WalletService walletService; private final WalletService walletService;
private final AppUserMapper appUserMapper; private final AppUserMapper appUserMapper;
@Operation(summary = "金币流水查询")
@GetMapping("/{userId}/ledger") @GetMapping("/{userId}/ledger")
@PreAuthorize("@ss.hasPermission('order:list')") @PreAuthorize("@ss.hasPermission('order:list')")
public Result<List<CoinLedger>> ledger(@PathVariable Long userId, public Result<List<CoinLedger>> ledger(@PathVariable Long userId,
@@ -38,6 +44,7 @@ public class WalletAdminController {
return Result.ok(walletService.recentLedger(userId, limit)); return Result.ok(walletService.recentLedger(userId, limit));
} }
@Operation(summary = "人工补币/扣回(必须带幂等键,先校验用户存在)")
@PostMapping("/coins") @PostMapping("/coins")
@PreAuthorize("@ss.hasPermission('wallet:update')") @PreAuthorize("@ss.hasPermission('wallet:update')")
@OperationLog(module = "订单管理", action = "人工补币") @OperationLog(module = "订单管理", action = "人工补币")
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts.admin.service; package com.fruitshorts.admin.service;
import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.core.toolkit.Wrappers;
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts.admin.service; package com.fruitshorts.admin.service;
import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.core.toolkit.Wrappers;
@@ -1,5 +1,8 @@
// Author: fruit.com.cn
package com.fruitshorts.admin.vo; package com.fruitshorts.admin.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@@ -13,18 +16,30 @@ import java.time.LocalDateTime;
@Data @Data
public class AppUserWatchHistoryVO { public class AppUserWatchHistoryVO {
@Schema(description = "ID")
private Long id; private Long id;
@Schema(description = "短剧 ID")
private Long dramaId; private Long dramaId;
@Schema(description = "剧名")
private String dramaTitle; private String dramaTitle;
/** 短剧当前是否已上线,用于识别"用户说看了但剧已下线"的情况 */ /** 短剧当前是否已上线,用于识别"用户说看了但剧已下线"的情况 */
@Schema(description = "短剧当前是否已上线")
private Boolean dramaPublished; private Boolean dramaPublished;
@Schema(description = "剧集 ID")
private Long episodeId; private Long episodeId;
@Schema(description = "集号")
private Integer episodeNo; private Integer episodeNo;
@Schema(description = "播放进度(毫秒)")
private Long positionMs; private Long positionMs;
@Schema(description = "时长(毫秒)")
private Long durationMs; private Long durationMs;
/** 是否已看完(进度到达片尾 1 秒内) */ /** 是否已看完(进度到达片尾 1 秒内) */
@Schema(description = "是否已看完")
private Boolean finished; private Boolean finished;
@Schema(description = "来源")
private String source; private String source;
@Schema(description = "设备 ID")
private String deviceId; private String deviceId;
@Schema(description = "观看时间")
private LocalDateTime watchedAt; private LocalDateTime watchedAt;
} }
@@ -1,3 +1,5 @@
# Author: fruit.com.cn
server: server:
port: 8080 port: 8080
@@ -26,7 +28,7 @@ mybatis-plus:
global-config: global-config:
banner: false banner: false
db-config: db-config:
id-type: auto id-type: assign_id
logic-delete-field: deleted logic-delete-field: deleted
logic-delete-value: 1 logic-delete-value: 1
logic-not-delete-value: 0 logic-not-delete-value: 0
@@ -51,3 +53,15 @@ springdoc:
logging: logging:
level: level:
com.fruitshorts: debug com.fruitshorts: debug
management:
endpoints:
web:
exposure:
include: prometheus,health
endpoint:
health:
show-details: always
metrics:
tags:
application: ${spring.application.name}
+2
View File
@@ -1,4 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!-- Author: fruit.com.cn -->
<project xmlns="http://maven.apache.org/POM/4.0.0" <project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts.app; package com.fruitshorts.app;
import org.mybatis.spring.annotation.MapperScan; import org.mybatis.spring.annotation.MapperScan;
@@ -1,6 +1,9 @@
// Author: fruit.com.cn
package com.fruitshorts.app.controller; package com.fruitshorts.app.controller;
import com.fruitshorts.common.pojo.Result; import com.fruitshorts.common.pojo.Result;
import com.fruitshorts.framework.redis.RateLimit;
import com.fruitshorts.framework.security.SecurityUtils; import com.fruitshorts.framework.security.SecurityUtils;
import com.fruitshorts.framework.web.WebUtils; import com.fruitshorts.framework.web.WebUtils;
import com.fruitshorts.user.service.AppAuthService; import com.fruitshorts.user.service.AppAuthService;
@@ -11,6 +14,8 @@ import com.fruitshorts.user.vo.OtpSendReqVO;
import com.fruitshorts.user.vo.RefreshReqVO; import com.fruitshorts.user.vo.RefreshReqVO;
import com.fruitshorts.user.vo.RegisterReqVO; import com.fruitshorts.user.vo.RegisterReqVO;
import com.fruitshorts.user.vo.TokenRespVO; 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.servlet.http.HttpServletRequest;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@@ -20,6 +25,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
/** 客户端账号:验证码、注册、登录、刷新、登出。 */ /** 客户端账号:验证码、注册、登录、刷新、登出。 */
@Tag(name = "账号")
@RestController @RestController
@RequestMapping("/app/auth") @RequestMapping("/app/auth")
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -29,30 +35,37 @@ public class AppAuthController {
private final AppAuthService authService; private final AppAuthService authService;
/** 发送验证码。频控在服务端,客户端不需要自己限流。 */ /** 发送验证码。频控在服务端,客户端不需要自己限流。 */
@Operation(summary = "发送验证码(注册/登录/找回密码,服务端频控)")
@RateLimit(limit = 20, windowSeconds = 60, key = "otp")
@PostMapping("/otp") @PostMapping("/otp")
public Result<Void> sendOtp(@Valid @RequestBody OtpSendReqVO req, HttpServletRequest request) { public Result<Void> sendOtp(@Valid @RequestBody OtpSendReqVO req, HttpServletRequest request) {
otpService.send(req, WebUtils.getClientIp(request)); otpService.send(req, WebUtils.getClientIp(request));
return Result.ok(); return Result.ok();
} }
@Operation(summary = "注册账号(手机号/邮箱 + 验证码),返回令牌")
@PostMapping("/register") @PostMapping("/register")
public Result<TokenRespVO> register(@Valid @RequestBody RegisterReqVO req, public Result<TokenRespVO> register(@Valid @RequestBody RegisterReqVO req,
HttpServletRequest request) { HttpServletRequest request) {
return Result.ok(authService.register(req, WebUtils.getClientIp(request))); return Result.ok(authService.register(req, WebUtils.getClientIp(request)));
} }
@Operation(summary = "登录(验证码或密码),返回令牌")
@RateLimit(limit = 10, windowSeconds = 60, key = "login")
@PostMapping("/login") @PostMapping("/login")
public Result<TokenRespVO> login(@Valid @RequestBody LoginReqVO req, public Result<TokenRespVO> login(@Valid @RequestBody LoginReqVO req,
HttpServletRequest request) { HttpServletRequest request) {
return Result.ok(authService.login(req, WebUtils.getClientIp(request))); return Result.ok(authService.login(req, WebUtils.getClientIp(request)));
} }
@Operation(summary = "刷新令牌(refresh token 轮换)")
@PostMapping("/refresh") @PostMapping("/refresh")
public Result<TokenRespVO> refresh(@Valid @RequestBody RefreshReqVO req) { public Result<TokenRespVO> refresh(@Valid @RequestBody RefreshReqVO req) {
return Result.ok(authService.refresh(req.getRefreshToken())); return Result.ok(authService.refresh(req.getRefreshToken()));
} }
/** 登出:默认只吊销当前设备的会话,allDevices=true 时吊销全部。 */ /** 登出:默认只吊销当前设备的会话,allDevices=true 时吊销全部。 */
@Operation(summary = "登出(allDevices=true 时吊销全部设备会话)")
@PostMapping("/logout") @PostMapping("/logout")
public Result<Void> logout(@RequestBody(required = false) LogoutReqVO req) { public Result<Void> logout(@RequestBody(required = false) LogoutReqVO req) {
authService.logout(SecurityUtils.getUserIdOrNull(), req == null ? new LogoutReqVO() : req); authService.logout(SecurityUtils.getUserIdOrNull(), req == null ? new LogoutReqVO() : req);
@@ -1,9 +1,13 @@
// Author: fruit.com.cn
package com.fruitshorts.app.controller; package com.fruitshorts.app.controller;
import com.fruitshorts.app.service.AppCatalogService; import com.fruitshorts.app.service.AppCatalogService;
import com.fruitshorts.app.vo.AppCategoryVO; import com.fruitshorts.app.vo.AppCategoryVO;
import com.fruitshorts.app.vo.AppFeedVO; import com.fruitshorts.app.vo.AppFeedVO;
import com.fruitshorts.common.pojo.Result; 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 lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
@@ -14,6 +18,7 @@ import java.util.List;
/** /**
* 客户端目录接口:首页与发现页各一次请求拿全量聚合数据。 * 客户端目录接口:首页与发现页各一次请求拿全量聚合数据。
*/ */
@Tag(name = "内容浏览")
@RestController @RestController
@RequestMapping("/app") @RequestMapping("/app")
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -21,16 +26,19 @@ public class AppCatalogController {
private final AppCatalogService appCatalogService; private final AppCatalogService appCatalogService;
@Operation(summary = "首页聚合数据(轮播 + 分区卡片,卡片带预览集)")
@GetMapping("/home/feed") @GetMapping("/home/feed")
public Result<AppFeedVO> homeFeed() { public Result<AppFeedVO> homeFeed() {
return Result.ok(appCatalogService.homeFeed()); return Result.ok(appCatalogService.homeFeed());
} }
@Operation(summary = "发现页聚合数据")
@GetMapping("/discover/feed") @GetMapping("/discover/feed")
public Result<AppFeedVO> discoverFeed() { public Result<AppFeedVO> discoverFeed() {
return Result.ok(appCatalogService.discoverFeed()); return Result.ok(appCatalogService.discoverFeed());
} }
@Operation(summary = "分类列表")
@GetMapping("/categories") @GetMapping("/categories")
public Result<List<AppCategoryVO>> categories() { public Result<List<AppCategoryVO>> categories() {
return Result.ok(appCatalogService.categories()); return Result.ok(appCatalogService.categories());
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts.app.controller; package com.fruitshorts.app.controller;
import com.fruitshorts.common.pojo.CursorResult; import com.fruitshorts.common.pojo.CursorResult;
@@ -8,6 +10,8 @@ import com.fruitshorts.interaction.service.CommentService;
import com.fruitshorts.interaction.vo.CommentPublishReqVO; import com.fruitshorts.interaction.vo.CommentPublishReqVO;
import com.fruitshorts.interaction.vo.CommentQueryReqVO; import com.fruitshorts.interaction.vo.CommentQueryReqVO;
import com.fruitshorts.interaction.vo.CommentRespVO; 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.servlet.http.HttpServletRequest;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@@ -27,6 +31,7 @@ import org.springframework.web.bind.annotation.RestController;
* 不传 `rootId` 时返回顶层评论,传了则返回该楼层的回复。 * 不传 `rootId` 时返回顶层评论,传了则返回该楼层的回复。
* 列表允许匿名浏览;发布与删除需要登录。 * 列表允许匿名浏览;发布与删除需要登录。
*/ */
@Tag(name = "评论")
@RestController @RestController
@RequestMapping("/app/comments") @RequestMapping("/app/comments")
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -34,6 +39,7 @@ public class AppCommentController {
private final CommentService commentService; private final CommentService commentService;
@Operation(summary = "发表评论或回复(敏感词处置后按策略发布或转待审)")
@PostMapping @PostMapping
public Result<CommentRespVO> publish(@Valid @RequestBody CommentPublishReqVO req, public Result<CommentRespVO> publish(@Valid @RequestBody CommentPublishReqVO req,
HttpServletRequest request) { HttpServletRequest request) {
@@ -41,6 +47,7 @@ public class AppCommentController {
WebUtils.getClientIp(request), request.getHeader("X-Device-Id"))); WebUtils.getClientIp(request), request.getHeader("X-Device-Id")));
} }
@Operation(summary = "评论列表(游标翻页;不传 rootId 为顶层评论,传了为楼层回复)")
@GetMapping @GetMapping
public Result<CursorResult<CommentRespVO>> page(@RequestParam Long dramaId, public Result<CursorResult<CommentRespVO>> page(@RequestParam Long dramaId,
@RequestParam(required = false) Long episodeId, @RequestParam(required = false) Long episodeId,
@@ -56,6 +63,7 @@ public class AppCommentController {
return Result.ok(commentService.page(SecurityUtils.getUserIdOrNull(), query)); return Result.ok(commentService.page(SecurityUtils.getUserIdOrNull(), query));
} }
@Operation(summary = "删除自己的评论(逻辑删除)")
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
public Result<Void> delete(@PathVariable Long id) { public Result<Void> delete(@PathVariable Long id) {
commentService.delete(SecurityUtils.getAppUserId(), id); commentService.delete(SecurityUtils.getAppUserId(), id);
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts.app.controller; package com.fruitshorts.app.controller;
import com.fruitshorts.commerce.dal.entity.Orders; import com.fruitshorts.commerce.dal.entity.Orders;
@@ -6,6 +8,8 @@ import com.fruitshorts.commerce.service.UnlockService;
import com.fruitshorts.commerce.service.WalletService; import com.fruitshorts.commerce.service.WalletService;
import com.fruitshorts.common.pojo.Result; import com.fruitshorts.common.pojo.Result;
import com.fruitshorts.framework.security.SecurityUtils; 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 lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
@@ -24,6 +28,7 @@ import java.util.Map;
* 金币解锁是第一阶段真实的收费方式;真实商店购买走 {@code /app/orders/verify}, * 金币解锁是第一阶段真实的收费方式;真实商店购买走 {@code /app/orders/verify},
* 本轮只预留接口(商店未接入时返回明确业务提示,不会产生假订单)。 * 本轮只预留接口(商店未接入时返回明确业务提示,不会产生假订单)。
*/ */
@Tag(name = "金币与订单")
@RestController @RestController
@RequestMapping("/app") @RequestMapping("/app")
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -34,6 +39,7 @@ public class AppCommerceController {
private final OrderService orderService; private final OrderService orderService;
/** 我的钱包:余额 + 最近流水,客户端一处渲染。 */ /** 我的钱包:余额 + 最近流水,客户端一处渲染。 */
@Operation(summary = "我的钱包:金币余额 + 最近流水")
@GetMapping("/wallet") @GetMapping("/wallet")
public Result<Map<String, Object>> wallet() { public Result<Map<String, Object>> wallet() {
Long userId = SecurityUtils.getAppUserId(); Long userId = SecurityUtils.getAppUserId();
@@ -44,20 +50,25 @@ public class AppCommerceController {
} }
/** 金币解锁分集:幂等,重复点击不会重复扣币。 */ /** 金币解锁分集:幂等,重复点击不会重复扣币。 */
@Operation(summary = "金币解锁分集(幂等,重复调用不重复扣币)")
@PostMapping("/episodes/{episodeId}/unlock") @PostMapping("/episodes/{episodeId}/unlock")
public Result<Map<String, Object>> unlock(@PathVariable Long episodeId) { public Result<Map<String, Object>> unlock(@PathVariable Long episodeId) {
return Result.ok(unlockService.unlock(SecurityUtils.getAppUserId(), episodeId)); return Result.ok(unlockService.unlock(SecurityUtils.getAppUserId(), episodeId));
} }
@Operation(summary = "我的订单列表")
@GetMapping("/orders") @GetMapping("/orders")
public Result<List<Orders>> orders() { public Result<List<Orders>> orders() {
return Result.ok(orderService.listByUser(SecurityUtils.getAppUserId())); return Result.ok(orderService.listByUser(SecurityUtils.getAppUserId()));
} }
/** 提交商店票据。第一阶段商店未接入,会返回明确的"尚未配置"。 */ /** 提交商店票据。第一阶段商店未接入,会返回明确的"尚未配置"。 */
@Operation(summary = "提交商店票据(IAP/Google Play;商店未接入时返回明确业务提示)")
@PostMapping("/orders/verify") @PostMapping("/orders/verify")
public Result<Orders> verify(@RequestBody Map<String, String> body) { public Result<Orders> verify(@RequestBody Map<String, String> body) {
return Result.ok(orderService.verifyAndDeliver(SecurityUtils.getAppUserId(), return Result.ok(orderService.verifyAndDeliver(SecurityUtils.getAppUserId(),
body.getOrDefault("platform", ""), body.getOrDefault("receipt", ""))); body.getOrDefault("platform", ""),
body.getOrDefault("productId", ""),
body.getOrDefault("receipt", "")));
} }
} }
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts.app.controller; package com.fruitshorts.app.controller;
import com.fruitshorts.common.pojo.Result; import com.fruitshorts.common.pojo.Result;
@@ -8,6 +10,8 @@ import com.fruitshorts.playback.vo.DownloadLicenseRespVO;
import com.fruitshorts.playback.vo.DownloadPolicyRespVO; import com.fruitshorts.playback.vo.DownloadPolicyRespVO;
import com.fruitshorts.playback.vo.DownloadProgressReqVO; import com.fruitshorts.playback.vo.DownloadProgressReqVO;
import com.fruitshorts.playback.vo.DownloadSettingReqVO; 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.servlet.http.HttpServletRequest;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@@ -29,6 +33,7 @@ import java.util.List;
* <p>服务端只发授权与地址,分片由客户端向 CDN 拉取;进度上报用于断点续传与跨端展示。 * <p>服务端只发授权与地址,分片由客户端向 CDN 拉取;进度上报用于断点续传与跨端展示。
* 设备标识优先取请求体,其次取请求头 X-Device-Id。 * 设备标识优先取请求体,其次取请求头 X-Device-Id。
*/ */
@Tag(name = "离线下载")
@RestController @RestController
@RequestMapping("/app/downloads") @RequestMapping("/app/downloads")
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -37,6 +42,7 @@ public class AppDownloadController {
private final DownloadService downloadService; private final DownloadService downloadService;
/** 申请下载授权(同一剧集重复申请幂等,只刷新有效期)。 */ /** 申请下载授权(同一剧集重复申请幂等,只刷新有效期)。 */
@Operation(summary = "申请下载授权(同剧集重复申请幂等,只刷新有效期)")
@PostMapping @PostMapping
public Result<DownloadLicenseRespVO> grant(@Valid @RequestBody DownloadGrantReqVO req, public Result<DownloadLicenseRespVO> grant(@Valid @RequestBody DownloadGrantReqVO req,
HttpServletRequest request) { HttpServletRequest request) {
@@ -44,17 +50,20 @@ public class AppDownloadController {
request.getHeader("X-Device-Id"))); request.getHeader("X-Device-Id")));
} }
@Operation(summary = "下载列表(授权 + 任务进度,可直接渲染)")
@GetMapping @GetMapping
public Result<List<DownloadLicenseRespVO>> list(@RequestParam(required = false) String deviceId) { public Result<List<DownloadLicenseRespVO>> list(@RequestParam(required = false) String deviceId) {
return Result.ok(downloadService.list(SecurityUtils.getAppUserId(), deviceId)); return Result.ok(downloadService.list(SecurityUtils.getAppUserId(), deviceId));
} }
@Operation(summary = "下载策略与配额(单设备上限/授权天数/仅 Wi-Fi/清晰度上限)")
@GetMapping("/policy") @GetMapping("/policy")
public Result<DownloadPolicyRespVO> policy(@RequestParam(required = false) String deviceId) { public Result<DownloadPolicyRespVO> policy(@RequestParam(required = false) String deviceId) {
return Result.ok(downloadService.policy(SecurityUtils.getAppUserId(), deviceId)); return Result.ok(downloadService.policy(SecurityUtils.getAppUserId(), deviceId));
} }
/** 仅 Wi-Fi 下载、清晰度上限等偏好。 */ /** 仅 Wi-Fi 下载、清晰度上限等偏好。 */
@Operation(summary = "更新下载偏好(仅 Wi-Fi 下载、最高清晰度)")
@PutMapping("/settings") @PutMapping("/settings")
public Result<Void> updateSettings(@RequestBody DownloadSettingReqVO req) { public Result<Void> updateSettings(@RequestBody DownloadSettingReqVO req) {
downloadService.updateSettings(SecurityUtils.getAppUserId(), req); downloadService.updateSettings(SecurityUtils.getAppUserId(), req);
@@ -62,6 +71,7 @@ public class AppDownloadController {
} }
/** 断点续传前重新取一次地址:授权不变,只有地址是短时效的。 */ /** 断点续传前重新取一次地址:授权不变,只有地址是短时效的。 */
@Operation(summary = "断点续传前重新获取下载地址(授权不变,地址短时效)")
@GetMapping("/{episodeId}/url") @GetMapping("/{episodeId}/url")
public Result<DownloadLicenseRespVO> refreshUrl(@PathVariable Long episodeId, public Result<DownloadLicenseRespVO> refreshUrl(@PathVariable Long episodeId,
HttpServletRequest request) { HttpServletRequest request) {
@@ -69,6 +79,7 @@ public class AppDownloadController {
request.getHeader("X-Device-Id"))); request.getHeader("X-Device-Id")));
} }
@Operation(summary = "上报下载进度(用于断点续传与跨端展示)")
@PostMapping("/{episodeId}/progress") @PostMapping("/{episodeId}/progress")
public Result<DownloadLicenseRespVO> reportProgress(@PathVariable Long episodeId, public Result<DownloadLicenseRespVO> reportProgress(@PathVariable Long episodeId,
@RequestBody DownloadProgressReqVO req, @RequestBody DownloadProgressReqVO req,
@@ -77,6 +88,7 @@ public class AppDownloadController {
request.getHeader("X-Device-Id"))); request.getHeader("X-Device-Id")));
} }
@Operation(summary = "删除单条下载(吊销授权)")
@DeleteMapping("/{episodeId}") @DeleteMapping("/{episodeId}")
public Result<Void> revoke(@PathVariable Long episodeId, public Result<Void> revoke(@PathVariable Long episodeId,
@RequestParam(required = false) String deviceId, @RequestParam(required = false) String deviceId,
@@ -87,6 +99,7 @@ public class AppDownloadController {
} }
/** 清空当前设备的全部下载授权。 */ /** 清空当前设备的全部下载授权。 */
@Operation(summary = "清空当前设备全部下载(返回清除条数)")
@DeleteMapping @DeleteMapping
public Result<Integer> clear(@RequestParam(required = false) String deviceId, public Result<Integer> clear(@RequestParam(required = false) String deviceId,
HttpServletRequest request) { HttpServletRequest request) {
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts.app.controller; package com.fruitshorts.app.controller;
import com.fruitshorts.app.service.AppCatalogService; import com.fruitshorts.app.service.AppCatalogService;
@@ -7,6 +9,8 @@ import com.fruitshorts.framework.security.SecurityUtils;
import com.fruitshorts.framework.web.WebUtils; import com.fruitshorts.framework.web.WebUtils;
import com.fruitshorts.playback.service.PlaybackService; import com.fruitshorts.playback.service.PlaybackService;
import com.fruitshorts.playback.vo.PlayRespVO; 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 jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
@@ -14,6 +18,7 @@ import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
@Tag(name = "短剧与播放")
@RestController @RestController
@RequestMapping("/app") @RequestMapping("/app")
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -23,6 +28,7 @@ public class AppDramaController {
private final PlaybackService playbackService; private final PlaybackService playbackService;
/** 详情:包含剧集列表,客户端进详情页一次请求即可渲染 */ /** 详情:包含剧集列表,客户端进详情页一次请求即可渲染 */
@Operation(summary = "短剧详情(含剧集列表,免费集带已签名播放地址)")
@GetMapping("/dramas/{id}") @GetMapping("/dramas/{id}")
public Result<AppDramaDetailVO> detail(@PathVariable Long id) { public Result<AppDramaDetailVO> detail(@PathVariable Long id) {
return Result.ok(appCatalogService.dramaDetail(id)); return Result.ok(appCatalogService.dramaDetail(id));
@@ -32,6 +38,7 @@ public class AppDramaController {
* 播放票据:服务端判定权益后签发短时效播放地址。 * 播放票据:服务端判定权益后签发短时效播放地址。
* 未登录也能播免费集;付费集未解锁时只返回需要多少金币。 * 未登录也能播免费集;付费集未解锁时只返回需要多少金币。
*/ */
@Operation(summary = "签发播放票据与签名地址(服务端判定权益:免费/已购/需解锁/无片源)")
@GetMapping("/episodes/{id}/play") @GetMapping("/episodes/{id}/play")
public Result<PlayRespVO> play(@PathVariable Long id, HttpServletRequest request) { public Result<PlayRespVO> play(@PathVariable Long id, HttpServletRequest request) {
return Result.ok(playbackService.play( return Result.ok(playbackService.play(
@@ -0,0 +1,35 @@
// Author: fruit.com.cn
package com.fruitshorts.app.controller;
import com.fruitshorts.app.service.AppEventService;
import com.fruitshorts.app.vo.AppEventReqVO;
import com.fruitshorts.common.pojo.Result;
import com.fruitshorts.framework.security.SecurityUtils;
import com.fruitshorts.framework.web.WebUtils;
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;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/** 客户端埋点上报。 */
@Tag(name = "埋点")
@RestController
@RequestMapping("/app")
@RequiredArgsConstructor
public class AppEventController {
private final AppEventService eventService;
@Operation(summary = "埋点事件批量上报")
@PostMapping("/events")
public Result<Void> report(@Valid @RequestBody AppEventReqVO req, HttpServletRequest request) {
eventService.save(req, SecurityUtils.getUserIdOrNull(), WebUtils.getClientIp(request));
return Result.ok();
}
}
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts.app.controller; package com.fruitshorts.app.controller;
import com.fruitshorts.app.service.AppCatalogService; import com.fruitshorts.app.service.AppCatalogService;
@@ -6,6 +8,8 @@ import com.fruitshorts.common.pojo.Result;
import com.fruitshorts.framework.security.SecurityUtils; import com.fruitshorts.framework.security.SecurityUtils;
import com.fruitshorts.playback.service.FavoriteService; import com.fruitshorts.playback.service.FavoriteService;
import com.fruitshorts.playback.vo.FavoriteToggleReqVO; 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 jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.DeleteMapping;
@@ -19,6 +23,7 @@ import org.springframework.web.bind.annotation.RestController;
import java.util.List; import java.util.List;
/** 收藏。 */ /** 收藏。 */
@Tag(name = "收藏")
@RestController @RestController
@RequestMapping("/app/favorites") @RequestMapping("/app/favorites")
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -28,18 +33,21 @@ public class AppFavoriteController {
private final AppCatalogService appCatalogService; private final AppCatalogService appCatalogService;
/** 切换收藏,返回切换后的状态:true 为已收藏。 */ /** 切换收藏,返回切换后的状态:true 为已收藏。 */
@Operation(summary = "切换收藏状态(返回切换后的结果)")
@PostMapping("/toggle") @PostMapping("/toggle")
public Result<Boolean> toggle(@Valid @RequestBody FavoriteToggleReqVO req) { public Result<Boolean> toggle(@Valid @RequestBody FavoriteToggleReqVO req) {
return Result.ok(favoriteService.toggle(SecurityUtils.getAppUserId(), req.getDramaId())); return Result.ok(favoriteService.toggle(SecurityUtils.getAppUserId(), req.getDramaId()));
} }
/** 收藏列表,卡片结构与首页一致。 */ /** 收藏列表,卡片结构与首页一致。 */
@Operation(summary = "收藏列表(卡片结构与首页一致)")
@GetMapping @GetMapping
public Result<List<AppDramaCardVO>> list() { public Result<List<AppDramaCardVO>> list() {
return Result.ok(appCatalogService.toCards( return Result.ok(appCatalogService.toCards(
favoriteService.listDramas(SecurityUtils.getAppUserId()))); favoriteService.listDramas(SecurityUtils.getAppUserId())));
} }
@Operation(summary = "取消收藏")
@DeleteMapping("/{dramaId}") @DeleteMapping("/{dramaId}")
public Result<Void> remove(@PathVariable Long dramaId) { public Result<Void> remove(@PathVariable Long dramaId) {
favoriteService.remove(SecurityUtils.getAppUserId(), dramaId); favoriteService.remove(SecurityUtils.getAppUserId(), dramaId);
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts.app.controller; package com.fruitshorts.app.controller;
import com.fruitshorts.common.pojo.Result; import com.fruitshorts.common.pojo.Result;
@@ -7,6 +9,8 @@ import com.fruitshorts.interaction.service.ReportService;
import com.fruitshorts.interaction.vo.LikeToggleReqVO; import com.fruitshorts.interaction.vo.LikeToggleReqVO;
import com.fruitshorts.interaction.vo.LikeToggleRespVO; import com.fruitshorts.interaction.vo.LikeToggleRespVO;
import com.fruitshorts.interaction.vo.ReportCreateReqVO; 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 jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
@@ -15,6 +19,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
/** 点赞与举报。 */ /** 点赞与举报。 */
@Tag(name = "点赞与举报")
@RestController @RestController
@RequestMapping("/app") @RequestMapping("/app")
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -24,12 +29,14 @@ public class AppInteractionController {
private final ReportService reportService; private final ReportService reportService;
/** 点赞/取消点赞,返回切换后的状态与最新计数。 */ /** 点赞/取消点赞,返回切换后的状态与最新计数。 */
@Operation(summary = "点赞/取消点赞(支持短剧与评论),返回切换后状态与计数")
@PostMapping("/likes/toggle") @PostMapping("/likes/toggle")
public Result<LikeToggleRespVO> toggleLike(@Valid @RequestBody LikeToggleReqVO req) { public Result<LikeToggleRespVO> toggleLike(@Valid @RequestBody LikeToggleReqVO req) {
return Result.ok(likeService.toggle(SecurityUtils.getAppUserId(), req)); return Result.ok(likeService.toggle(SecurityUtils.getAppUserId(), req));
} }
/** 提交举报:同一用户对同一目标只保留一条待处理工单。 */ /** 提交举报:同一用户对同一目标只保留一条待处理工单。 */
@Operation(summary = "提交举报(评论/用户/短剧,同一目标仅保留一条待处理工单)")
@PostMapping("/reports") @PostMapping("/reports")
public Result<Long> report(@Valid @RequestBody ReportCreateReqVO req) { public Result<Long> report(@Valid @RequestBody ReportCreateReqVO req) {
return Result.ok(reportService.create(SecurityUtils.getAppUserId(), req)); return Result.ok(reportService.create(SecurityUtils.getAppUserId(), req));
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts.app.controller; package com.fruitshorts.app.controller;
import com.fruitshorts.common.pojo.Result; import com.fruitshorts.common.pojo.Result;
@@ -7,6 +9,8 @@ import com.fruitshorts.user.service.AppUserService;
import com.fruitshorts.user.vo.AppUserRespVO; import com.fruitshorts.user.vo.AppUserRespVO;
import com.fruitshorts.user.vo.DeactivateReqVO; import com.fruitshorts.user.vo.DeactivateReqVO;
import com.fruitshorts.user.vo.ProfileUpdateReqVO; 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 jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
@@ -21,6 +25,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
/** 客户端个人中心:资料、设备、注销、数据导出。 */ /** 客户端个人中心:资料、设备、注销、数据导出。 */
@Tag(name = "个人中心")
@RestController @RestController
@RequestMapping("/app/me") @RequestMapping("/app/me")
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -28,33 +33,39 @@ public class AppMeController {
private final AppUserService userService; private final AppUserService userService;
@Operation(summary = "获取当前用户资料")
@GetMapping @GetMapping
public Result<AppUserRespVO> me() { public Result<AppUserRespVO> me() {
return Result.ok(userService.me(SecurityUtils.getAppUserId())); return Result.ok(userService.me(SecurityUtils.getAppUserId()));
} }
@Operation(summary = "修改资料(昵称/头像/性别/生日/语言/时区/区域)")
@PutMapping @PutMapping
public Result<AppUserRespVO> update(@Valid @RequestBody ProfileUpdateReqVO req) { public Result<AppUserRespVO> update(@Valid @RequestBody ProfileUpdateReqVO req) {
return Result.ok(userService.updateProfile(SecurityUtils.getAppUserId(), req)); return Result.ok(userService.updateProfile(SecurityUtils.getAppUserId(), req));
} }
@Operation(summary = "登录设备列表")
@GetMapping("/devices") @GetMapping("/devices")
public Result<List<AppUserDevice>> devices() { public Result<List<AppUserDevice>> devices() {
return Result.ok(userService.devices(SecurityUtils.getAppUserId())); return Result.ok(userService.devices(SecurityUtils.getAppUserId()));
} }
/** 商店与隐私法规要求的数据导出,返回账号侧数据;内容侧数据由客户端自行下载。 */ /** 商店与隐私法规要求的数据导出,返回账号侧数据;内容侧数据由客户端自行下载。 */
@Operation(summary = "数据导出(商店与隐私法规要求,返回账号侧数据)")
@GetMapping("/export") @GetMapping("/export")
public Result<Map<String, Object>> export() { public Result<Map<String, Object>> export() {
return Result.ok(userService.exportAccount(SecurityUtils.getAppUserId())); return Result.ok(userService.exportAccount(SecurityUtils.getAppUserId()));
} }
/** 申请注销:立即登出全部设备,进入冷静期,期间可撤销。 */ /** 申请注销:立即登出全部设备,进入冷静期,期间可撤销。 */
@Operation(summary = "申请注销(进入冷静期并登出全部设备,期间可撤销)")
@PostMapping("/deactivate") @PostMapping("/deactivate")
public Result<LocalDateTime> deactivate(@RequestBody(required = false) DeactivateReqVO req) { public Result<LocalDateTime> deactivate(@RequestBody(required = false) DeactivateReqVO req) {
return Result.ok(userService.requestDeactivation(SecurityUtils.getAppUserId(), req)); return Result.ok(userService.requestDeactivation(SecurityUtils.getAppUserId(), req));
} }
@Operation(summary = "撤销注销申请")
@PostMapping("/deactivate/cancel") @PostMapping("/deactivate/cancel")
public Result<Void> cancelDeactivate() { public Result<Void> cancelDeactivate() {
userService.cancelDeactivation(SecurityUtils.getAppUserId()); userService.cancelDeactivation(SecurityUtils.getAppUserId());
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts.app.controller; package com.fruitshorts.app.controller;
import com.fruitshorts.common.pojo.Result; import com.fruitshorts.common.pojo.Result;
@@ -5,6 +7,8 @@ import com.fruitshorts.framework.security.SecurityUtils;
import com.fruitshorts.playback.service.WatchProgressService; import com.fruitshorts.playback.service.WatchProgressService;
import com.fruitshorts.playback.vo.ProgressRespVO; import com.fruitshorts.playback.vo.ProgressRespVO;
import com.fruitshorts.playback.vo.ProgressSaveReqVO; 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 jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.DeleteMapping;
@@ -19,6 +23,7 @@ import org.springframework.web.bind.annotation.RestController;
import java.util.List; import java.util.List;
/** 观看进度与观看记录。 */ /** 观看进度与观看记录。 */
@Tag(name = "观看记录")
@RestController @RestController
@RequestMapping("/app/watch") @RequestMapping("/app/watch")
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -27,28 +32,33 @@ public class AppWatchController {
private final WatchProgressService progressService; private final WatchProgressService progressService;
/** 上报进度:客户端每 10 秒一次,切集时立即上报一次。 */ /** 上报进度:客户端每 10 秒一次,切集时立即上报一次。 */
@Operation(summary = "上报观看进度(建议每 10 秒一次,切集时补报)")
@PostMapping("/progress") @PostMapping("/progress")
public Result<ProgressRespVO> save(@Valid @RequestBody ProgressSaveReqVO req) { public Result<ProgressRespVO> save(@Valid @RequestBody ProgressSaveReqVO req) {
return Result.ok(progressService.save(SecurityUtils.getAppUserId(), req)); return Result.ok(progressService.save(SecurityUtils.getAppUserId(), req));
} }
/** 某部剧的续看位置,进详情页时用它决定"续看第 N 集"。 */ /** 某部剧的续看位置,进详情页时用它决定"续看第 N 集"。 */
@Operation(summary = "查询某部剧的续看位置")
@GetMapping("/progress") @GetMapping("/progress")
public Result<ProgressRespVO> ofDrama(@RequestParam Long dramaId) { public Result<ProgressRespVO> ofDrama(@RequestParam Long dramaId) {
return Result.ok(progressService.ofDrama(SecurityUtils.getAppUserId(), dramaId)); return Result.ok(progressService.ofDrama(SecurityUtils.getAppUserId(), dramaId));
} }
@Operation(summary = "最近观看列表")
@GetMapping("/recent") @GetMapping("/recent")
public Result<List<ProgressRespVO>> recent(@RequestParam(defaultValue = "20") int limit) { public Result<List<ProgressRespVO>> recent(@RequestParam(defaultValue = "20") int limit) {
return Result.ok(progressService.recent(SecurityUtils.getAppUserId(), limit)); return Result.ok(progressService.recent(SecurityUtils.getAppUserId(), limit));
} }
@Operation(summary = "删除某部剧的观看记录")
@DeleteMapping("/progress/{dramaId}") @DeleteMapping("/progress/{dramaId}")
public Result<Void> remove(@PathVariable Long dramaId) { public Result<Void> remove(@PathVariable Long dramaId) {
progressService.remove(SecurityUtils.getAppUserId(), dramaId); progressService.remove(SecurityUtils.getAppUserId(), dramaId);
return Result.ok(); return Result.ok();
} }
@Operation(summary = "清空全部观看记录")
@DeleteMapping("/progress") @DeleteMapping("/progress")
public Result<Void> clear() { public Result<Void> clear() {
progressService.clear(SecurityUtils.getAppUserId()); progressService.clear(SecurityUtils.getAppUserId());
@@ -0,0 +1,33 @@
// Author: fruit.com.cn
package com.fruitshorts.app.dal.entity;
import com.baomidou.mybatisplus.annotation.FieldStrategy;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
/** 埋点事件落库记录,为后续分析/漏斗预留原始数据。 */
@Data
@TableName("app_event_log")
public class AppEventLog {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private String eventName;
private LocalDateTime eventTime;
private Long userId;
private String deviceId;
private String sessionId;
private String page;
private String properties;
private String clientIp;
private LocalDateTime createdAt;
@TableField(updateStrategy = FieldStrategy.NEVER)
private LocalDateTime updatedAt;
}
@@ -0,0 +1,10 @@
// Author: fruit.com.cn
package com.fruitshorts.app.dal.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.fruitshorts.app.dal.entity.AppEventLog;
/** 埋点事件 Mapper。 */
public interface AppEventLogMapper extends BaseMapper<AppEventLog> {
}
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts.app.service; package com.fruitshorts.app.service;
import com.fruitshorts.app.vo.AppBannerVO; import com.fruitshorts.app.vo.AppBannerVO;
@@ -166,10 +168,7 @@ public class AppCatalogService {
vo.setNumber(episode.getEpNo()); vo.setNumber(episode.getEpNo());
vo.setTitle(episode.getTitle()); vo.setTitle(episode.getTitle());
vo.setDurationSec(episode.getDurationSec()); 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; boolean free = episode.getIsFree() != null && episode.getIsFree() == 1;
vo.setIsFree(free);
vo.setLocked(!free); vo.setLocked(!free);
vo.setUnlockCoin(episode.getUnlockCoin()); vo.setUnlockCoin(episode.getUnlockCoin());
if (free) { if (free) {
@@ -0,0 +1,40 @@
// Author: fruit.com.cn
package com.fruitshorts.app.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fruitshorts.app.dal.entity.AppEventLog;
import com.fruitshorts.app.dal.mapper.AppEventLogMapper;
import com.fruitshorts.app.vo.AppEventReqVO;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
/** 埋点事件接收:批量落库,事件名与属性暂不校验、透传,口径后续与运营对齐。 */
@Service
@RequiredArgsConstructor
public class AppEventService {
private final AppEventLogMapper eventLogMapper;
private final ObjectMapper objectMapper;
@SneakyThrows
public void save(AppEventReqVO req, Long userId, String clientIp) {
for (AppEventReqVO.EventItem item : req.getEvents()) {
AppEventLog log = new AppEventLog();
log.setEventName(item.getEventName());
log.setEventTime(item.getEventTime() == null ? LocalDateTime.now() : item.getEventTime());
log.setUserId(userId);
log.setDeviceId(req.getDeviceId());
log.setSessionId(req.getSessionId());
log.setPage(item.getPage());
log.setProperties(item.getProperties() == null ? null
: objectMapper.writeValueAsString(item.getProperties()));
log.setClientIp(clientIp);
log.setCreatedAt(LocalDateTime.now());
eventLogMapper.insert(log);
}
}
}
@@ -1,14 +1,22 @@
// Author: fruit.com.cn
package com.fruitshorts.app.vo; package com.fruitshorts.app.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
@Data @Data
public class AppBannerVO { public class AppBannerVO {
@Schema(description = "轮播图 ID")
private Long id; private Long id;
@Schema(description = "图片地址")
private String imageUrl; private String imageUrl;
/** NONE / DRAMA / URL */
@Schema(description = "跳转类型:NONE / DRAMA / URL")
private String linkType; private String linkType;
/** 短剧 ID 或跳转地址 */
@Schema(description = "跳转目标:短剧 ID 或跳转地址")
private String linkValue; private String linkValue;
} }
@@ -1,5 +1,8 @@
// Author: fruit.com.cn
package com.fruitshorts.app.vo; package com.fruitshorts.app.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
@@ -7,6 +10,9 @@ import lombok.Data;
@AllArgsConstructor @AllArgsConstructor
public class AppCategoryVO { public class AppCategoryVO {
@Schema(description = "分类 ID")
private Long id; private Long id;
@Schema(description = "分类名称")
private String name; private String name;
} }
@@ -1,23 +1,41 @@
// Author: fruit.com.cn
package com.fruitshorts.app.vo; package com.fruitshorts.app.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
/** 列表卡片,用于首页/发现页/分类分区 */ /** 列表卡片,用于首页/发现页/分类分区 */
@Data @Data
public class AppDramaCardVO { public class AppDramaCardVO {
@Schema(description = "短剧 ID")
private Long id; private Long id;
@Schema(description = "短剧标题")
private String title; private String title;
@Schema(description = "封面地址")
private String cover; private String cover;
@Schema(description = "简介")
private String synopsis; private String synopsis;
@Schema(description = "题材")
private String genre; private String genre;
@Schema(description = "分类名称")
private String category; private String category;
@Schema(description = "总集数")
private Integer totalEpisodes; private Integer totalEpisodes;
@Schema(description = "是否热门")
private Boolean isHot; private Boolean isHot;
@Schema(description = "播放量")
private Long viewCount; private Long viewCount;
/**
* 首个可播免费集。首页竖屏播放需要在列表阶段就能起播, @Schema(description = "首个可播免费集;首页竖屏播放可直接起播,省去再请求详情/播放地址的往返")
* 带上这一集可以省掉「点开→请求详情→再请求播放地址」的往返。
*/
private AppEpisodeVO preview; private AppEpisodeVO preview;
} }
@@ -1,5 +1,8 @@
// Author: fruit.com.cn
package com.fruitshorts.app.vo; package com.fruitshorts.app.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import java.util.List; import java.util.List;
@@ -7,16 +10,39 @@ import java.util.List;
@Data @Data
public class AppDramaDetailVO { public class AppDramaDetailVO {
@Schema(description = "短剧 ID")
private Long id; private Long id;
@Schema(description = "短剧标题")
private String title; private String title;
@Schema(description = "简介")
private String synopsis; private String synopsis;
@Schema(description = "封面地址")
private String cover; private String cover;
@Schema(description = "题材")
private String genre; private String genre;
@Schema(description = "分类名称")
private String category; private String category;
@Schema(description = "总集数")
private Integer totalEpisodes; private Integer totalEpisodes;
@Schema(description = "免费集数")
private Integer freeEpisodes; private Integer freeEpisodes;
@Schema(description = "单集解锁所需金币数")
private Integer unlockCoin; private Integer unlockCoin;
@Schema(description = "是否热门")
private Boolean isHot; private Boolean isHot;
@Schema(description = "播放量")
private Long viewCount; private Long viewCount;
@Schema(description = "剧集列表")
private List<AppEpisodeVO> episodes; private List<AppEpisodeVO> episodes;
} }
@@ -1,23 +1,31 @@
// Author: fruit.com.cn
package com.fruitshorts.app.vo; package com.fruitshorts.app.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
@Data @Data
public class AppEpisodeVO { public class AppEpisodeVO {
@Schema(description = "剧集 ID")
private Long id; private Long id;
@Schema(description = "集号(第几集)")
private Integer number; private Integer number;
@Schema(description = "剧集标题")
private String title; private String title;
/** 展示用时长 mm:ss */
private String duration; @Schema(description = "时长(秒)")
private Integer durationSec; private Integer durationSec;
private Boolean isFree;
/** true 表示需要解锁后才能播放 */ @Schema(description = "是否锁定,true 表示需要解锁后才能播放")
private Boolean locked; private Boolean locked;
@Schema(description = "解锁所需金币数")
private Integer unlockCoin; private Integer unlockCoin;
/**
* 可播放时的地址;锁定或不含片源时为空。 @Schema(description = "可播放地址;锁定或无片源时为空")
* 详情页拿到就能直接播,不必按集再请求一次播放票据。
*/
private String playUrl; private String playUrl;
} }
@@ -0,0 +1,48 @@
// Author: fruit.com.cn
package com.fruitshorts.app.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
/** 埋点事件批量上报请求。 */
@Data
public class AppEventReqVO {
@Schema(description = "设备ID")
private String deviceId;
@Schema(description = "会话ID")
private String sessionId;
@Schema(description = "事件列表")
@NotEmpty(message = "事件列表不能为空")
@Size(max = 50, message = "单次最多上报 50 条事件")
@Valid
private List<EventItem> events;
@Data
public static class EventItem {
@Schema(description = "事件名,如 playback_start/playback_end/pay_success")
@NotBlank(message = "事件名不能为空")
private String eventName;
@Schema(description = "事件发生时间,缺省用服务端接收时间")
private LocalDateTime eventTime;
@Schema(description = "页面/场景标识")
private String page;
@Schema(description = "事件属性(key-value)")
private Map<String, Object> properties;
}
}
@@ -1,5 +1,8 @@
// Author: fruit.com.cn
package com.fruitshorts.app.vo; package com.fruitshorts.app.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
@@ -10,6 +13,9 @@ import java.util.List;
@AllArgsConstructor @AllArgsConstructor
public class AppFeedVO { public class AppFeedVO {
@Schema(description = "轮播图列表")
private List<AppBannerVO> banners; private List<AppBannerVO> banners;
@Schema(description = "分区列表")
private List<AppSectionVO> sections; private List<AppSectionVO> sections;
} }
@@ -1,5 +1,8 @@
// Author: fruit.com.cn
package com.fruitshorts.app.vo; package com.fruitshorts.app.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
@@ -10,8 +13,12 @@ import java.util.List;
@AllArgsConstructor @AllArgsConstructor
public class AppSectionVO { public class AppSectionVO {
/** 分区标识:hot / new / category-{id},客户端可据此做埋点与样式 */ @Schema(description = "分区标识:hot / new / category-{id},客户端据此做埋点与样式")
private String code; private String code;
@Schema(description = "分区标题")
private String title; private String title;
@Schema(description = "分区内短剧列表")
private List<AppDramaCardVO> dramas; private List<AppDramaCardVO> dramas;
} }
@@ -1,3 +1,5 @@
# Author: fruit.com.cn
server: server:
port: 8081 port: 8081
@@ -14,6 +16,10 @@ spring:
jackson: jackson:
date-format: yyyy-MM-dd HH:mm:ss date-format: yyyy-MM-dd HH:mm:ss
time-zone: Asia/Shanghai time-zone: Asia/Shanghai
data:
redis:
host: ${REDIS_HOST:127.0.0.1}
port: ${REDIS_PORT:6379}
mybatis-plus: mybatis-plus:
configuration: configuration:
@@ -21,11 +27,17 @@ mybatis-plus:
global-config: global-config:
banner: false banner: false
db-config: db-config:
id-type: auto id-type: assign_id
logic-delete-field: deleted logic-delete-field: deleted
logic-delete-value: 1 logic-delete-value: 1
logic-not-delete-value: 0 logic-not-delete-value: 0
springdoc:
swagger-ui:
path: /doc.html
api-docs:
path: /v3/api-docs
fruitshorts: fruitshorts:
interaction: interaction:
# MACHINE_FIRST:机审通过即发布,命中 REVIEW 级敏感词才转人工 # MACHINE_FIRST:机审通过即发布,命中 REVIEW 级敏感词才转人工
@@ -49,7 +61,25 @@ fruitshorts:
admin-ttl-hours: 12 admin-ttl-hours: 12
cors: cors:
allowed-origins: "*" allowed-origins: "*"
pay:
google:
# 接入 Google Play Billing 后置 true,并注入服务账号 JSON
enabled: false
package-name: com.nontonki
service-account-json: ${GOOGLE_PLAY_SERVICE_ACCOUNT_JSON:}
logging: logging:
level: level:
com.fruitshorts: info com.fruitshorts: info
management:
endpoints:
web:
exposure:
include: prometheus,health
endpoint:
health:
show-details: always
metrics:
tags:
application: ${spring.application.name}
+2
View File
@@ -1,4 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!-- Author: fruit.com.cn -->
<project xmlns="http://maven.apache.org/POM/4.0.0" <project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts.common.exception; package com.fruitshorts.common.exception;
import lombok.Getter; import lombok.Getter;
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts.common.exception; package com.fruitshorts.common.exception;
import lombok.Getter; import lombok.Getter;
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts.common.pojo; package com.fruitshorts.common.pojo;
import lombok.Getter; import lombok.Getter;
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts.common.pojo; package com.fruitshorts.common.pojo;
import lombok.Data; import lombok.Data;
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts.common.pojo; package com.fruitshorts.common.pojo;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts.common.pojo; package com.fruitshorts.common.pojo;
import com.fruitshorts.common.exception.ErrorCode; import com.fruitshorts.common.exception.ErrorCode;
+16
View File
@@ -1,4 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!-- Author: fruit.com.cn -->
<project xmlns="http://maven.apache.org/POM/4.0.0" <project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
@@ -64,5 +66,19 @@
<groupId>org.springdoc</groupId> <groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency> </dependency>
<!-- 监控:暴露 actuator 指标供 Prometheus 抓取 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies> </dependencies>
</project> </project>
@@ -0,0 +1,27 @@
// Author: fruit.com.cn
package com.fruitshorts.framework.compliance;
/**
* 平台合规声明(法律红线,不可删除或修改)。
*
* <p>本软件「水果短剧(FruitShorts)」为正规短剧平台,仅用于承载、分发符合法律法规
* 与公序良俗的短剧内容。平台严禁且不提供任何用于上传、传播、存储色情、淫秽、露骨或
* 违法犯罪内容的功能。任何运营方利用本软件从事违法犯罪行为的,其法律责任由运营方独立
* 承担,与开发者无任何法律关系。
*/
public final class ComplianceNotice {
private ComplianceNotice() {
}
/** 平台定位与合法用途声明。 */
public static final String PLATFORM_STATEMENT =
"水果短剧(FruitShorts)为正规短剧平台,仅用于合法合规的短剧内容运营,"
+ "严禁用于任何违法犯罪活动或色情、淫秽、露骨内容的传播。";
/** 内容合规红线:用户生成内容发布前必须通过敏感词自动检查。 */
public static final String CONTENT_RED_LINE =
"所有用户生成内容必须通过敏感词自动检查(BLOCK 级直接拦截),"
+ "命中待审词必须进入人工审核,禁止任何违规内容上线。";
}
@@ -0,0 +1,31 @@
// Author: fruit.com.cn
package com.fruitshorts.framework.compliance;
/**
* 图片/视频内容审核抽象(露骨内容、违规画面检测)。
* 第一阶段默认未接入第三方,接入时新增实现类即可,业务侧不感知。
*/
public interface ContentModerationService {
/** 审核结论:通过 / 拦截 / 转人工。 */
enum Verdict {
PASS, BLOCK, REVIEW
}
/**
* 审核图片。
*
* @param imageUrl 图片地址
* @return 审核结论
*/
Verdict checkImage(String imageUrl);
/**
* 审核视频(实现方负责抽帧)。
*
* @param videoUrl 视频地址
* @return 审核结论
*/
Verdict checkVideo(String videoUrl);
}
@@ -0,0 +1,20 @@
// Author: fruit.com.cn
package com.fruitshorts.framework.compliance;
/**
* 默认内容审核实现:未配置第三方审核服务时一律放行,交由运营方人工审核兜底。
* 接入第三方(如阿里云内容安全 / Google Cloud Vision SafeSearch)后新增实现类并替换装配。
*/
public class DisabledContentModeration implements ContentModerationService {
@Override
public Verdict checkImage(String imageUrl) {
return Verdict.PASS;
}
@Override
public Verdict checkVideo(String videoUrl) {
return Verdict.PASS;
}
}
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts.framework.config; package com.fruitshorts.framework.config;
import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.annotation.DbType;
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts.framework.config; package com.fruitshorts.framework.config;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts.framework.log; package com.fruitshorts.framework.log;
import com.fruitshorts.framework.security.LoginUser; import com.fruitshorts.framework.security.LoginUser;
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts.framework.log; package com.fruitshorts.framework.log;
import java.lang.annotation.ElementType; import java.lang.annotation.ElementType;
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts.framework.log; package com.fruitshorts.framework.log;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts.framework.log; package com.fruitshorts.framework.log;
import lombok.Builder; import lombok.Builder;
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts.framework.log; package com.fruitshorts.framework.log;
/** /**
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts.framework.partition; package com.fruitshorts.framework.partition;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts.framework.partition; package com.fruitshorts.framework.partition;
import lombok.Data; import lombok.Data;
@@ -0,0 +1,25 @@
// Author: fruit.com.cn
package com.fruitshorts.framework.redis;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 接口级固定窗口限流。基于 Redis INCR,key 按客户端 IP 维度隔离。
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
/** 窗口内允许的最大请求次数。 */
int limit() default 10;
/** 窗口时长(秒)。 */
int windowSeconds() default 60;
/** 限流维度前缀,默认取方法名。 */
String key() default "";
}
@@ -0,0 +1,54 @@
// Author: fruit.com.cn
package com.fruitshorts.framework.redis;
import com.fruitshorts.common.exception.ErrorCode;
import com.fruitshorts.common.exception.ServiceException;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.time.Duration;
/**
* 固定窗口限流切面:每次命中注解先 INCR,超阈值即拒绝。
*/
@Aspect
@Component
@RequiredArgsConstructor
public class RateLimitAspect {
private final RedisService redisService;
@Before("@annotation(rateLimit)")
public void check(RateLimit rateLimit) {
long count = redisService.increment(
buildKey(rateLimit), Duration.ofSeconds(rateLimit.windowSeconds()));
if (count > rateLimit.limit()) {
throw new ServiceException(ErrorCode.TOO_MANY_REQUESTS, "请求过于频繁,请稍后再试");
}
}
private String buildKey(RateLimit rateLimit) {
String scope = rateLimit.key().isBlank() ? "default" : rateLimit.key();
return "rate:" + scope + ":" + clientIp();
}
private String clientIp() {
ServletRequestAttributes attrs =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attrs == null) {
return "unknown";
}
HttpServletRequest request = attrs.getRequest();
String forwarded = request.getHeader("X-Forwarded-For");
if (forwarded != null && !forwarded.isBlank()) {
return forwarded.split(",")[0].trim();
}
return request.getRemoteAddr();
}
}
@@ -0,0 +1,45 @@
// Author: fruit.com.cn
package com.fruitshorts.framework.redis;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.time.Duration;
/**
* Redis 字符串操作封装,供缓存与限流复用。
* 统一在这里兜底过期策略,业务侧不直接接触 RedisTemplate。
*/
@Component
@RequiredArgsConstructor
public class RedisService {
private final StringRedisTemplate redisTemplate;
public void set(String key, String value, Duration ttl) {
redisTemplate.opsForValue().set(key, value, ttl);
}
public String get(String key) {
return redisTemplate.opsForValue().get(key);
}
public void delete(String key) {
redisTemplate.delete(key);
}
/**
* 固定窗口计数:自增并保证首次写入时设置过期时间。
* 用于接口限流,返回当前窗口内的累计次数。
*/
public long increment(String key, Duration window) {
Long count = redisTemplate.opsForValue().increment(key);
long value = count == null ? 1 : count;
if (value == 1) {
redisTemplate.expire(key, window);
}
return value;
}
}
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts.framework.security; package com.fruitshorts.framework.security;
/** /**
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts.framework.security; package com.fruitshorts.framework.security;
import com.fruitshorts.common.exception.ErrorCode; import com.fruitshorts.common.exception.ErrorCode;
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts.framework.security; package com.fruitshorts.framework.security;
import jakarta.servlet.FilterChain; import jakarta.servlet.FilterChain;
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts.framework.security; package com.fruitshorts.framework.security;
import lombok.Data; import lombok.Data;
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts.framework.security; package com.fruitshorts.framework.security;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts.framework.security; package com.fruitshorts.framework.security;
import java.util.List; import java.util.List;
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts.framework.security; package com.fruitshorts.framework.security;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts.framework.security; package com.fruitshorts.framework.security;
import java.util.List; import java.util.List;
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts.framework.security; package com.fruitshorts.framework.security;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts.framework.security; package com.fruitshorts.framework.security;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@@ -39,6 +41,8 @@ public class SecurityConfig {
"/app/dramas/**", "/app/dramas/**",
"/app/episodes/*/play", "/app/episodes/*/play",
"/app/config/**", "/app/config/**",
/* 埋点上报:游客浏览也要记录,匿名可访问 */
"/app/events",
/* 媒资:封面与视频用 <img>/<video> 直接访问,无法带请求头。 /* 媒资:封面与视频用 <img>/<video> 直接访问,无法带请求头。
片源的防盗链由 fruitshorts.media.signature-required 控制。 */ 片源的防盗链由 fruitshorts.media.signature-required 控制。 */
"/files/**", "/files/**",
@@ -46,7 +50,8 @@ public class SecurityConfig {
"/swagger-ui/**", "/swagger-ui/**",
"/swagger-ui.html", "/swagger-ui.html",
"/v3/api-docs/**", "/v3/api-docs/**",
"/actuator/health" "/actuator/health",
"/actuator/prometheus"
}; };
private final JwtAuthenticationFilter jwtAuthenticationFilter; private final JwtAuthenticationFilter jwtAuthenticationFilter;
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts.framework.security; package com.fruitshorts.framework.security;
import com.fruitshorts.common.exception.ErrorCode; import com.fruitshorts.common.exception.ErrorCode;
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts.framework.web; package com.fruitshorts.framework.web;
import com.fruitshorts.common.exception.ErrorCode; import com.fruitshorts.common.exception.ErrorCode;
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
package com.fruitshorts.framework.web; package com.fruitshorts.framework.web;
import jakarta.servlet.FilterChain; import jakarta.servlet.FilterChain;
Loaded 100 of 372 files, more files were not shown because too many files have changed in this diff. Show more