Spring Boot 3.2 / MyBatis-Plus / PostgreSQL,7 个 Maven 模块;含认证鉴权、内容与媒资管理、操作日志,以及客户端 /app/** 接口。
ReelShort 短剧服务(后端)
短剧服务的服务端,Spring Boot + PostgreSQL,目前实现管理后台接口(/admin/**)。
完整架构设计与迭代计划见 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"。
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. 初始化数据库
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),两个脚本都可重复执行。
首次搭建环境时先建角色与库:
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. 构建
cd /Users/jianghaifeng/Desktop/ReelShort/FruitServer
mvn -DskipTests package
3. 启动
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。
操作日志
后台写操作由切面自动记录,不需要在每个接口里手写,在控制器方法上加注解即可:
@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 |
前端侧改动:
.env.base关闭 mock(VITE_USE_MOCK=false);vite.config.mts增加/admin代理到http://127.0.0.1:8080;src/api/admin/*.ts封装全部管理端接口;- 请求头自动携带
Authorization: Bearer <token>; src/stores/rbac.js的can(code)改用登录返回的权限码, 原src/data/、src/permissions/下的演示数据已删除;- 表格状态列改用接口返回的
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 端口被占用
lsof -tiTCP:8080 -sTCP:LISTEN | xargs kill
brew 连不上 GitHub
本机访问 github.com 不通,brew 仓库已改用清华镜像:
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
export JAVA_HOME=$(/usr/libexec/java_home -v 21)
Homebrew 的 Maven 在 JAVA_HOME 未设置时会回退到它自带的 JDK(当前为 27),
父 POM 的注解处理器配置保证这种情况下也能编译通过。