Files
FruitServer/README.md
T
Vicky a4aa749cc6 feat: 初始化短剧服务端(管理后台 API + 客户端 API)
Spring Boot 3.2 / MyBatis-Plus / PostgreSQL,7 个 Maven 模块;含认证鉴权、内容与媒资管理、操作日志,以及客户端 /app/** 接口。
2026-10-14 18:02:47 +08:00

321 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ReelShort 短剧服务(后端)
短剧服务的服务端,Spring Boot + PostgreSQL,目前实现管理后台接口(`/admin/**`)。
完整架构设计与迭代计划见 [docs/architecture-design.md](docs/architecture-design.md)。
| 项 | 版本 |
| --- | --- |
| 运行时 | JDK 21(LTS),编译目标 Java 17 |
| 框架 | Spring Boot 3.2.5、Spring Security 6、MyBatis-Plus 3.5.7 |
| 数据库 | PostgreSQL 18 |
| 构建 | Maven 3.10 多模块 |
## 目录结构
```
java/
├── reelshort-common/ 统一响应、分页、错误码、业务异常
├── reelshort-framework/ JWT 鉴权、权限校验、全局异常、MyBatis-Plus、CORS
├── reelshort-module-system/ 管理员、角色、权限、菜单、角色分类、配置、操作日志
├── reelshort-module-content/ 短剧、剧集、题材、审核发布
├── reelshort-admin-api/ 启动模块与 /admin/** 控制器
├── sql/ schema.sql 建表、data.sql 初始化数据
└── docs/ 架构设计文档
```
## 开发环境
本机环境由 Homebrew 统一管理(版本以本机实际安装为准):
| 组件 | 版本 | 说明 |
| --- | --- | --- |
| Homebrew | 7.0.9 | 远端指向清华镜像,见文末常见问题 |
| JDK | OpenJDK 21.0.12.1 | `brew install openjdk@21`,`JAVA_HOME` 已写入 `~/.zshrc` |
| Maven | 3.10.0 | `brew install maven`,命令为 `/opt/homebrew/bin/mvn` |
| PostgreSQL | 18.6 | `brew install postgresql@18`,`brew services start postgresql@18` 启动 |
> `postgresql@18` 是 keg-only,命令行工具在 `/opt/homebrew/opt/postgresql@18/bin`,
> 用之前把它加到 PATH:`export PATH="/opt/homebrew/opt/postgresql@18/bin:$PATH"`。
```sh
java -version # openjdk 21.0.12.1
mvn -v # Maven 3.10.0 / Java 21.0.12.1
```
说明:
- 工程编译目标为 Java 17(`maven.compiler.release=17`),在 JDK 21 上构建与运行。
- 父 POM 显式声明了注解处理器(`<proc>full</proc>` + `annotationProcessorPaths`):
JDK 23 起不再默认扫描 classpath 上的处理器,缺这段配置 Lombok 会失效。
- 旧的 JDK 18 仍留在 `/Library/Java/JavaVirtualMachines/jdk-18.0.2.jdk`,删除需要 sudo,
不影响使用(`java_home` 与 `JAVA_HOME` 都已指向 21)。
## 快速开始
服务有两个入口,可以只启动其中一个:
| 服务 | 模块 | 端口 | 面向 |
| --- | --- | --- | --- |
| 管理端 | `reelshort-admin-api` | 8080 | Vue 后台 |
| 客户端 | `reelshort-app-api` | 8081 | Flutter 客户端 |
### 1. 初始化数据库
```sh
export PATH="/opt/homebrew/opt/postgresql@18/bin:$PATH"
cd /Users/jianghaifeng/Desktop/ReelShort/FruitServer
psql -h 127.0.0.1 -U reelshort -d reelshort -f sql/schema.sql
psql -h 127.0.0.1 -U reelshort -d reelshort -f sql/data.sql
```
数据库名 `reelshort`,角色 `reelshort`(密码 `reelshort`),两个脚本都可重复执行。
首次搭建环境时先建角色与库:
```sh
psql -d postgres -c "CREATE ROLE reelshort LOGIN PASSWORD 'reelshort';"
psql -d postgres -c "CREATE DATABASE reelshort OWNER reelshort ENCODING 'UTF8';"
```
连接信息在
`reelshort-admin-api/src/main/resources/application.yml`,可用环境变量
`DB_USER` / `DB_PASSWORD` / `JWT_ADMIN_SECRET` 覆盖。
### 关于 PostgreSQL 的两点说明
- 自增列用 `GENERATED BY DEFAULT AS IDENTITY`;`sql/data.sql` 里用了显式 ID,
脚本末尾会把序列 `setval` 到当前最大值,新增数据不会主键冲突。
- PostgreSQL 没有 `ON UPDATE CURRENT_TIMESTAMP`,`updated_at` 由 `set_updated_at()`
触发器统一维护(见 `sql/schema.sql` 末尾),实体侧仍不参与更新该字段。
- 轮播图的展示位置列名叫 `slot`(`position` 在 PostgreSQL 里是关键字),
实体用 `@TableField("slot")` 映射,对外接口字段仍是 `position`,前端无需改动。
### 2. 构建
```sh
cd /Users/jianghaifeng/Desktop/ReelShort/FruitServer
mvn -DskipTests package
```
### 3. 启动
```sh
cd /Users/jianghaifeng/Desktop/ReelShort/FruitServer
java -jar reelshort-admin-api/target/reelshort-admin-api-1.0.0-SNAPSHOT.jar
java -jar reelshort-app-api/target/reelshort-app-api-1.0.0-SNAPSHOT.jar
```
启动后:服务地址 `http://localhost:8080`,接口文档 `http://localhost:8080/doc.html`。
## 默认账号
密码均为 `admin123`。
| 账号 | 角色 | 权限范围 |
| --- | --- | --- |
| admin | 超级管理员 | 全部 14 项权限 |
| content.editor01 | 内容运营 | 短剧、剧集的增删改与封面上传 |
| content.reviewer01 | 审核专员 | 查看内容、审核发布、数据报表 |
| data.analyst01 | 数据分析师 | 查看短剧列表与数据报表 |
## 接口一览
### 认证 `/admin/auth`
| 方法 | 路径 | 说明 |
| --- | --- | --- |
| POST | `/login` | 登录,返回 token 与权限码 |
| GET | `/menus` | 当前用户菜单路由树(前端动态路由) |
| GET | `/profile` | 当前登录人信息 |
| POST | `/logout` | 登出(JWT 无状态,前端清除 token 即可) |
| POST | `/password` | 修改自己的密码 |
### 后台管理
| 模块 | 接口 |
| --- | --- |
| 管理员 | `GET /admin/admin-users/page`、`POST /admin/admin-users`、`PUT\|DELETE /admin/admin-users/{id}`、`PUT /{id}/status`、`PUT /{id}/password` |
| 角色 | `GET /admin/roles/page`、`GET /admin/roles/list`、`POST /admin/roles`、`PUT\|DELETE /admin/roles/{id}`、`GET\|PUT /admin/roles/{id}/permissions` |
| 角色分类 | `GET\|POST /admin/role-categories`、`PUT\|DELETE /admin/role-categories/{id}` |
| 权限点 | `GET /admin/permissions`(按分组返回,供权限配置页使用) |
| 菜单 | `GET /admin/menus/tree`、`POST /admin/menus`、`PUT\|DELETE /admin/menus/{id}` |
| 系统配置 | `GET\|PUT /admin/configs` |
| 操作日志 | `GET /admin/operation-logs/page`(按操作人、模块、动作、结果、时间范围筛选)、`GET /admin/operation-logs/{id}` |
| 字典 | `GET /admin/dicts/drama-category`、`GET /admin/dicts/content-status` |
### 内容管理
| 模块 | 接口 |
| --- | --- |
| 短剧 | `GET /admin/dramas/page`、`GET /admin/dramas/{id}`、`POST /admin/dramas`、`PUT\|DELETE /admin/dramas/{id}` |
| 审核发布 | `POST /admin/dramas/{id}/submit`、`/approve`、`/reject`、`/publish`、`/offline` |
| 审核记录 | `GET /admin/dramas/{id}/review-records` |
| 剧集 | `GET /admin/episodes?dramaId=`、`POST /admin/episodes`、`PUT\|DELETE /admin/episodes/{id}`、`PUT /admin/episodes/sort` |
| 轮播图 | `GET /admin/banners/page`、`POST /admin/banners`、`PUT\|DELETE /admin/banners/{id}`、`PUT /admin/banners/{id}/status`、`PUT /admin/banners/sort` |
| 短剧类型 | `GET\|POST /admin/drama-categories`、`PUT\|DELETE /admin/drama-categories/{id}`(权限沿用 `drama:*`) |
### 媒资 `/admin/media`
| 方法 | 路径 | 说明 |
| --- | --- | --- |
| POST | `/upload/init` | 建立分片会话,返回 `uploadId`、`chunkSize`、`totalChunks` |
| POST | `/upload/chunk` | 上传单个分片(multipart:`uploadId`、`index`、`file`) |
| POST | `/upload/complete` | 合并分片,返回媒资(含可访问的 `url`) |
| GET | `/{id}` | 查询媒资 |
## 媒资存储(本地)
当前没有对象存储,文件直接落在本机磁盘:
| 项 | 默认值 | 说明 |
| --- | --- | --- |
| 存储目录 | `./data/uploads` | 配置项 `reelshort.media.root-dir`,按 `业务类型/年月/uuid.ext` 分目录 |
| 访问前缀 | `/files/**` | 配置项 `reelshort.media.public-prefix`,映射为静态资源 |
| 分片大小 | 5MB | `reelshort.media.chunk-size`,单文件上限 2GB |
| 请求上限 | 20MB | `spring.servlet.multipart`,只需容纳一个分片 |
要点:
- `/files/**` 在 `SecurityConfig` 里是公开路径,因为 `<img>` / `<video>` 无法携带请求头。
- 上传权限:封面用 `cover:upload`,视频用 `episode:upload`。
- `data/` 是运行时产物,不需要提交到版本库。
- 换成 OSS/S3 时只需替换 `MediaStorageService`(分片落盘、合并、生成访问地址三处),
`MediaService` 与前端接口都不用动。
## 统一约定
- 响应体 `{ code, message, data }`,成功 `code=0`。
- 分页请求 `pageIndex`(从 1 开始)、`pageSize`,响应 `{ total, list }`。
- 鉴权头 `Authorization: Bearer <token>`;未登录 `code=401`,无权限 `code=403`。
- 错误码见架构文档附录 A;业务错误如 `100000` 账号密码错误、`200003` 状态不允许该操作。
## 权限模型
## 客户端接口 `/app/**`
面向 Flutter 客户端,只暴露已上线内容,不含审核状态、操作日志等管理端字段。
| 方法 | 路径 | 说明 |
| --- | --- | --- |
| GET | `/app/home/feed` | 首页聚合:轮播(HOME_TOP)+ 分区(热门 / 新剧 / 各分类) |
| GET | `/app/discover/feed` | 发现页聚合:轮播(DISCOVER)+ 分类分区 |
| GET | `/app/categories` | 分类列表 |
| GET | `/app/dramas/{id}` | 详情,含剧集列表,一次请求即可渲染详情页 |
| GET | `/app/episodes/{id}/play` | 播放票据 |
约定:
- 只返回 `status = published` 的短剧与剧集,未上线按不存在处理。
- 图片与视频返回相对路径(`/files/...`),客户端拼上服务地址即可。
- 播放票据三种结果:`FREE`(返回 `playUrl`)、`COIN_REQUIRED`(返回 `unlockCoin`)、
`NO_MEDIA`(已上线但还没传视频)。
- `/app/**` 目前在安全配置里放行(浏览无需登录);手机号/邮箱登录与权益体系接入后再收紧。
性能(本机实测,预热后 30 次采样,目标 50ms):
| 接口 | 平均 | P95 | 最大 | 响应体 |
| --- | --- | --- | --- | --- |
| `/app/home/feed` | 13.6ms | 20.3ms | 23.4ms | 3.9KB |
| `/app/discover/feed` | 7.3ms | 8.1ms | 8.8ms | 1.7KB |
| `/app/categories` | 2.0ms | 2.1ms | 3.4ms | 0.2KB |
| `/app/dramas/{id}` | 2.8ms | 3.1ms | 3.5ms | 0.7KB |
| `/app/episodes/{id}/play` | 2.2ms | 2.6ms | 3.3ms | 0.2KB |
`SlowRequestLogger` 会对超过 50ms 的 `/app/**` 请求打 WARN 日志,
内容量增长或索引失效时能第一时间发现;首页聚合是查询最多的一档(分类 + 热门 + 新剧 + 各分类列表),
如果将来分类变多,优先给它加缓存。
## 权限模型
- 权限码随 JWT 下发(如 `drama:create`、`banner:list`),控制器用
`@PreAuthorize("@ss.hasPermission('drama:create')")` 校验。
- 菜单树由权限码派生:角色拥有哪些权限,就可见哪些菜单。
- 内容状态机:`draft → reviewing → approved → published → offline`。
草稿不能直接发布,已上线的短剧需先下线才能删除;每次流转写入审核记录与操作日志。
- 鉴权实时读库(`PermissionChecker` 通过 `PermissionProvider` 查当前用户的权限),
后台改了角色权限或菜单,**前端刷新一下即可生效,不用重新登录**。
前端在每次进入应用时会重新拉取 `GET /admin/auth/menus` 与 `GET /admin/auth/profile`。
## 操作日志
后台写操作由切面自动记录,**不需要在每个接口里手写**,在控制器方法上加注解即可:
```java
@OperationLog(module = "内容管理", action = "新增短剧类型")
public Result<Long> create(@Valid @RequestBody DramaCategorySaveReqVO req) { ... }
```
每次调用记录:操作人、操作时间、模块、动作、请求方式与地址、请求参数、请求体、
响应体、成功/失败、错误信息、耗时、IP。
约定:
- 只记写操作(POST/PUT/DELETE),GET 查询不入库,避免刷屏。
- 请求体与响应体超过 4000 字符会截断;密码、密钥、token 等字段自动替换成 `***`。
- 业务异常记 `FAIL` 并带错误信息;`@PreAuthorize` 拦下的越权访问由
`AccessDeniedRecorder` 单独记一条(模块=安全,动作=越权访问被拒绝)。
- 日志写入失败不影响业务(切面内兜底 try/catch)。
- 查看入口:后台管理 → 操作日志,权限码 `log:list`,默认只给超级管理员。
## 前端对接
管理后台 `../ReelShortWeb` 已完成全部对接,本地演示数据已删除:
| 页面 | 使用接口 |
| --- | --- |
| 登录 / 左侧菜单 | `POST /admin/auth/login`、`GET /admin/auth/menus` |
| 工作台 | `GET /admin/dramas/page`(统计与最近更新) |
| 短剧管理 | `/admin/dramas/**`、`/admin/dicts/drama-category`、审核发布动作 |
| 剧集管理 | `/admin/episodes/**`、`PUT /admin/episodes/sort` |
| 角色权限配置 | `/admin/roles/**`、`GET /admin/permissions`、`GET /admin/role-categories` |
| 管理员用户列表 | `/admin/admin-users/**` |
| 菜单管理 | `GET /admin/menus/tree` + 增删改 |
| 角色分类 | `/admin/role-categories/**` |
| 系统设置 | `GET\|PUT /admin/configs` |
前端侧改动:
1. `.env.base` 关闭 mock(`VITE_USE_MOCK=false`);
2. `vite.config.mts` 增加 `/admin` 代理到 `http://127.0.0.1:8080`;
3. `src/api/admin/*.ts` 封装全部管理端接口;
4. 请求头自动携带 `Authorization: Bearer <token>`;
5. `src/stores/rbac.js` 的 `can(code)` 改用登录返回的权限码,
原 `src/data/`、`src/permissions/` 下的演示数据已删除;
6. 表格状态列改用接口返回的 `status` / `statusText`,
大数值用 `src/utils/format.js` 的 `formatCount` 格式化。
## 尚未实现
视频转码(HLS 多码率)、C 端接口(`/app/**`)、金币与会员、单集解锁、支付与对账、
数据看板统计、Redis 缓存与限流。详见架构设计文档第 11 章。
## 常见问题
**编译报「找不到符号 getXxx / 无法推断类型参数」**
Lombok 注解处理器没跑起来。确认是在 JDK 21 下构建(`mvn -v` 应显示 Java 21),
父 POM 已声明 `annotationProcessorPaths`,若仍报错先 `mvn clean`。
**8080 端口被占用**
```sh
lsof -tiTCP:8080 -sTCP:LISTEN | xargs kill
```
**brew 连不上 GitHub**
本机访问 `github.com` 不通,brew 仓库已改用清华镜像:
```sh
git -C /opt/homebrew remote set-url origin https://mirrors.tuna.tsinghua.edu.cn/git/homebrew/brew.git
```
Bottle 走 USTC 镜像(环境变量 `HOMEBREW_BOTTLE_DOMAIN`)。
要换回官方源,把 `origin` 改回 `https://github.com/Homebrew/brew` 即可。
**临时切换 JDK**
```sh
export JAVA_HOME=$(/usr/libexec/java_home -v 21)
```
Homebrew 的 Maven 在 `JAVA_HOME` 未设置时会回退到它自带的 JDK(当前为 27),
父 POM 的注解处理器配置保证这种情况下也能编译通过。