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

14 KiB
Raw Blame History

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

前端侧改动:

  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 端口被占用

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 的注解处理器配置保证这种情况下也能编译通过。