refactor: 后端标识统一为 FruitShorts,并落地 05 离线下载

为什么:
1) 产品已定名 FruitShorts,但后端仍是 reelshort 系列标识 —— Java 包 com.reelshort、
   模块名与 jar 名 reelshort-*、spring.application.name、配置前缀 reelshort.*,
   这些会出现在启动日志、命令行和 IDE 里,与产品名不一致。本次整体替换为 fruitshorts,
   数据库连接名上一轮已改名,保持不变。
2) 第一阶段下一批第 1 步(05 离线下载)要求把下载授权(设备绑定/有效期/吊销)、
   下载任务与断点续传、配额与过期清理、仅 Wi-Fi 规则做成完整接口,本提交一并落地。
3) 顺带修复:schema-phase1.sql 里 playback_ticket / order_event 仍按普通表定义唯一索引,
   表分区化之后重跑脚本必定报错(分区表的唯一索引必须包含分区键)。改为在本脚本里就
   按分区表定义,并验证脚本可重复执行。
This commit is contained in:
Vicky committed 2026-10-15 11:44:26 +08:00
1 parent a4aa749cc6
commit b3179fc836
251 files changed
+7261 -767

No files matched your search

+148 -33
View File
@@ -1,7 +1,14 @@
# ReelShort 短剧服务(后端)
# FruitShorts 短剧服务(后端)
短剧服务的服务端,Spring Boot + PostgreSQL,目前实现管理后台接口(`/admin/**`)。
完整架构设计与迭代计划见 [docs/architecture-design.md](docs/architecture-design.md)。
短剧服务的服务端,Spring Boot + PostgreSQL,对外两个入口:
管理后台 `/admin/**`(8080)与客户端 `/app/**`(8081)。
| 文档 | 内容 |
| --- | --- |
| [docs/prd-phase1.md](docs/prd-phase1.md) | 第一阶段 PRD:范围、验收口径、里程碑 |
| [docs/backend-design-phase1.md](docs/backend-design-phase1.md) | 后端设计:1 万并发观看的容量推导、成本、部署 |
| [docs/api-phase1.md](docs/api-phase1.md) | **接口清单:后台管理 + 客户端,标注已实现 / 下一批** |
| [docs/architecture-design.md](docs/architecture-design.md) | 早期架构设计文档 |
| 项 | 版本 |
| --- | --- |
@@ -13,14 +20,18 @@
## 目录结构
```
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/ 架构设计文档
FruitServer/
├── fruitshorts-common/ 统一响应、分页、错误码、业务异常
├── fruitshorts-framework/ JWT 鉴权(管理端/客户端双密钥)、权限校验、全局异常、分区维护、MyBatis-Plus、CORS
├── fruitshorts-module-system/ 管理员、角色、权限、菜单、角色分类、配置、操作日志
├── fruitshorts-module-content/ 短剧、剧集、题材、轮播、审核发布
├── fruitshorts-module-media/ 分片上传、媒资、播放地址签名、转码任务与多码率产物
├── fruitshorts-module-user/ C 端账号:验证码、注册登录、会话、资料、注销、风控
├── fruitshorts-module-playback/ 播放票据、观看进度与流水、收藏、下载授权
├── fruitshorts-admin-api/ 启动模块与 /admin/** 控制器(8080)
├── fruitshorts-app-api/ 启动模块与 /app/** 控制器(8081)
├── sql/ 建表、初始化数据、分区维护(见下)
└── docs/ PRD、后端设计、接口清单
```
## 开发环境
@@ -56,28 +67,40 @@ mvn -v # Maven 3.10.0 / Java 21.0.12.1
| 服务 | 模块 | 端口 | 面向 |
| --- | --- | --- | --- |
| 管理端 | `reelshort-admin-api` | 8080 | Vue 后台 |
| 客户端 | `reelshort-app-api` | 8081 | Flutter 客户端 |
| 管理端 | `fruitshorts-admin-api` | 8080 | Vue 后台 |
| 客户端 | `fruitshorts-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
psql -h 127.0.0.1 -U fruitshorts -d fruitshorts -f sql/schema.sql
psql -h 127.0.0.1 -U fruitshorts -d fruitshorts -f sql/data.sql
psql -h 127.0.0.1 -U fruitshorts -d fruitshorts -f sql/schema-phase1.sql
psql -h 127.0.0.1 -U fruitshorts -d fruitshorts -f sql/partition-maintenance.sql
psql -h 127.0.0.1 -U fruitshorts -d fruitshorts -f sql/data-phase1.sql
```
数据库名 `reelshort`,角色 `reelshort`(密码 `reelshort`),两个脚本都可重复执行。
| 脚本 | 作用 |
| --- | --- |
| `schema.sql` / `data.sql` | 系统域与内容域的基础表与初始化数据 |
| `schema-phase1.sql` | 第一阶段增量表:用户、播放记录、互动、交易、创作者、转码、运营 |
| `partition-maintenance.sql` | 分区函数 + 把高写入表转成分区 + 预建未来 3 个月分区 |
| `data-phase1.sql` | 新增权限点、超管授权、占位商品 |
四个脚本都幂等可重复执行,**顺序不要颠倒**:分区转换要求目标表是空的。
数据库名 `fruitshorts`,角色 `fruitshorts`(密码 `fruitshorts`)。
首次搭建环境时先建角色与库:
```sh
psql -d postgres -c "CREATE ROLE reelshort LOGIN PASSWORD 'reelshort';"
psql -d postgres -c "CREATE DATABASE reelshort OWNER reelshort ENCODING 'UTF8';"
psql -d postgres -c "CREATE ROLE fruitshorts LOGIN PASSWORD 'fruitshorts';"
psql -d postgres -c "CREATE DATABASE fruitshorts OWNER fruitshorts ENCODING 'UTF8';"
```
连接信息在
`reelshort-admin-api/src/main/resources/application.yml`,可用环境变量
`fruitshorts-admin-api/src/main/resources/application.yml`,可用环境变量
`DB_USER` / `DB_PASSWORD` / `JWT_ADMIN_SECRET` 覆盖。
### 关于 PostgreSQL 的两点说明
@@ -89,6 +112,41 @@ psql -d postgres -c "CREATE DATABASE reelshort OWNER reelshort ENCODING 'UTF8';"
- 轮播图的展示位置列名叫 `slot`(`position` 在 PostgreSQL 里是关键字),
实体用 `@TableField("slot")` 映射,对外接口字段仍是 `position`,前端无需改动。
### 大表分区与保留策略
**为什么要一开始就分区**:`watch_history`(观看流水)、`playback_ticket`(播放票据)、
`order_event`(订单流水)都是「每次行为写一行」的表。按 1 万并发观看估算,观看流水约
300 万行/天、播放票据约 360 万行/天,一年就是十亿级。这类表后补分区的代价很高
(要在维护窗口里整表重建),所以第一阶段就按 `watched_at` / `created_at` 做按月声明式分区。
**光建表没用**:声明式分区不会自动长分区,只建 default 分区的话数据会全堆进 default,
分区等于白做。所以有两层保障:
1. `sql/partition-maintenance.sql` 里的 `ensure_monthly_partitions(table, from, months)`
负责按月建分区,执行脚本时会建好当前月 + 未来 3 个月;
2. `fruitshorts-framework` 的 `PartitionMaintainer` 在**启动时**和**每天 03:17** 各调一次,
跨月时即使服务停机一周也不会漏建。
验证分区是否真的生效(应落在 `watch_history_2026xx` 而不是 `_default`):
```sh
psql -h 127.0.0.1 -U fruitshorts -d fruitshorts -c \
"select tableoid::regclass as 分区, count(*) from watch_history group by 1;"
```
**保留策略默认只建不删**。删数据不可逆,第一阶段先解决"写不爆",等保留期与合规确认后
再把 `fruitshorts.partition.retention-enabled` 打开;各表保留月数写在 `PartitionMaintainer`
的 `TABLES` 常量里(观看流水 24 个月、播放票据 6 个月、订单流水 36 个月)。手工清理:
```sql
SELECT drop_partitions_before('watch_history', CURRENT_DATE - INTERVAL '24 months');
```
分区表有两条 PostgreSQL 限制要注意:唯一索引必须包含分区键,所以 `playback_ticket`
的唯一索引是 `(ticket_no, created_at)`、主键是 `(id, created_at)`。
`coin_ledger` 故意**不分区** —— 它靠 `idempotent_key` 全局唯一保证不重复发币,
这个唯一性比分区更重要。
### 2. 构建
```sh
@@ -100,12 +158,46 @@ mvn -DskipTests package
```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
java -jar fruitshorts-admin-api/target/fruitshorts-admin-api-1.0.0-SNAPSHOT.jar
java -jar fruitshorts-app-api/target/fruitshorts-app-api-1.0.0-SNAPSHOT.jar
```
启动后:服务地址 `http://localhost:8080`,接口文档 `http://localhost:8080/doc.html`。
## 关键配置
| 配置项 | 默认值 | 说明 |
| --- | --- | --- |
| `fruitshorts.jwt.admin-secret` | 开发默认值 | 管理端签名密钥,**生产必须用 `JWT_ADMIN_SECRET` 注入** |
| `fruitshorts.jwt.app-secret` | 开发默认值 | 客户端签名密钥,与管理端物理隔离,两边 token 不能互换 |
| `fruitshorts.jwt.app-access-ttl-minutes` | 120 | 客户端 access token 有效期 |
| `fruitshorts.jwt.app-refresh-ttl-days` | 30 | refresh token 有效期,每次刷新轮换 |
| `fruitshorts.media.signature-secret` | 开发默认值 | 播放地址签名密钥,生产必须注入 |
| `fruitshorts.media.signature-ttl-minutes` | 15 | 播放地址有效期 |
| `fruitshorts.media.signature-required` | `false` | **生产必须打开**;打开后 `/files/episode/**` 无有效签名返回 403 |
| `fruitshorts.otp.sender` | `log` | 验证码通道,开发打日志;接入短信/邮件时实现 `OtpSender` 并改这里 |
| `fruitshorts.partition.enabled` | `true` | 是否自动维护分区 |
| `fruitshorts.partition.months-ahead` | 3 | 预建未来几个月分区 |
| `fruitshorts.partition.retention-enabled` | `false` | 是否自动清理过期分区(默认只建不删) |
开发阶段签名校验默认关闭,是为了不打断正在联调的客户端。**上线前必须打开**,
否则片源地址可以被任意复用与盗链。
## 第一阶段实现进度
| 模块 | 状态 | 说明 |
| --- | --- | --- |
| 01 基础与交付准备 | 已实现 | 多模块骨架、接口契约、双密钥鉴权、分区维护 |
| 02 用户账号与登录 | 已实现 | 手机号/邮箱注册登录、验证码反刷、令牌轮换、资料、注销、封禁即时生效 |
| 03 内容发现与详情 | 已实现 | 首页/发现/分类/详情;补内容分级、可播区域、版权窗口、标签字段 |
| 04 播放器与播放控制 | 部分 | 播放票据 + 签名地址 + 进度已实现;多码率片源待转码 worker |
| 05 记录、收藏与下载 | 部分 | 进度、观看流水、收藏已实现;离线下载授权待做 |
| 06 评论、点赞与分享 | 待做 | 表已建,接口与实现待补 |
| 07 创作者中心 | 待做 | 表已建;转码任务与多码率产物记账已实现 |
| 08 平台运营后台 | 部分 | App 用户管理已实现;内容治理、订单、看板待做 |
| 09 分集收费与支付 | 待做 | 表已建;播放入口的权益判定端口已预留,接上即生效 |
| 10 质量、数据与上线 | 待做 | 压测、埋点、对账待做 |
## 默认账号
密码均为 `admin123`。
@@ -168,9 +260,9 @@ java -jar reelshort-app-api/target/reelshort-app-api-1.0.0-SNAPSHOT.jar
| 项 | 默认值 | 说明 |
| --- | --- | --- |
| 存储目录 | `./data/uploads` | 配置项 `reelshort.media.root-dir`,按 `业务类型/年月/uuid.ext` 分目录 |
| 访问前缀 | `/files/**` | 配置项 `reelshort.media.public-prefix`,映射为静态资源 |
| 分片大小 | 5MB | `reelshort.media.chunk-size`,单文件上限 2GB |
| 存储目录 | `./data/uploads` | 配置项 `fruitshorts.media.root-dir`,按 `业务类型/年月/uuid.ext` 分目录 |
| 访问前缀 | `/files/**` | 配置项 `fruitshorts.media.public-prefix`,映射为静态资源 |
| 分片大小 | 5MB | `fruitshorts.media.chunk-size`,单文件上限 2GB |
| 请求上限 | 20MB | `spring.servlet.multipart`,只需容纳一个分片 |
要点:
@@ -188,27 +280,45 @@ java -jar reelshort-app-api/target/reelshort-app-api-1.0.0-SNAPSHOT.jar
- 鉴权头 `Authorization: Bearer <token>`;未登录 `code=401`,无权限 `code=403`。
- 错误码见架构文档附录 A;业务错误如 `100000` 账号密码错误、`200003` 状态不允许该操作。
## 权限模型
## 客户端接口 `/app/**`
面向 Flutter 客户端,只暴露已上线内容,不含审核状态、操作日志等管理端字段。
**完整接口清单(含请求/响应示例与错误码)见 [docs/api-phase1.md](docs/api-phase1.md)。**
| 方法 | 路径 | 说明 |
| --- | --- | --- |
| POST | `/app/auth/otp` | 发送验证码(REGISTER/LOGIN/RESET_PWD/BIND) |
| POST | `/app/auth/register` | 手机号/邮箱注册,返回令牌 |
| POST | `/app/auth/login` | 验证码或密码登录 |
| POST | `/app/auth/refresh` | 刷新令牌(refresh token 每次轮换) |
| POST | `/app/auth/logout` | 登出,`allDevices=true` 吊销全部会话 |
| GET/PUT | `/app/me` | 我的资料 |
| GET | `/app/me/devices` | 登录设备 |
| GET | `/app/me/export` | 数据导出(隐私合规) |
| POST | `/app/me/deactivate` `/cancel` | 申请/撤销注销(15 天冷静期) |
| GET | `/app/home/feed` | 首页聚合:轮播(HOME_TOP)+ 分区(热门 / 新剧 / 各分类) |
| GET | `/app/discover/feed` | 发现页聚合:轮播(DISCOVER)+ 分类分区 |
| GET | `/app/categories` | 分类列表 |
| GET | `/app/dramas/{id}` | 详情,含剧集列表,一次请求即可渲染详情页 |
| GET | `/app/episodes/{id}/play` | 播放票据 |
| GET | `/app/episodes/{id}/play` | 播放票据:服务端判定权益后签发**短时效签名地址** |
| POST | `/app/watch/progress` | 上报观看进度(建议每 10s 一次) |
| GET | `/app/watch/progress?dramaId=` | 某剧续看位置 |
| GET | `/app/watch/recent` | 最近观看 |
| DELETE | `/app/watch/progress[/{dramaId}]` | 删除 / 清空记录 |
| POST | `/app/favorites/toggle` | 收藏切换 |
| GET | `/app/favorites` | 收藏列表(卡片结构与首页一致) |
约定:
- 只返回 `status = published` 的短剧与剧集,未上线按不存在处理。
- 图片与视频返回相对路径(`/files/...`),客户端拼上服务地址即可。
- 播放票据三种结果:`FREE`(返回 `playUrl`)、`COIN_REQUIRED`(返回 `unlockCoin`)、
`NO_MEDIA`(已上线但还没传视频)。
- `/app/**` 目前在安全配置里放行(浏览无需登录);手机号/邮箱登录与权益体系接入后再收紧。
- 图片与视频返回相对路径(`/files/...`),客户端拼上服务地址即可;
播放地址会带 `?e=&s=` 签名参数,客户端**原样使用、不要自己拼接**。
- 播放票据四种结果:`FREE`、`ENTITLED`(已购权益)、`COIN_REQUIRED`(返回 `unlockCoin`)、
`NO_MEDIA`(已上线但还没传视频或未转码完成)。
- 鉴权边界:只有浏览类接口(`/app/auth/**`、`/app/home|discover|categories|dramas`、
`/app/episodes/*/play`)允许匿名,其余 `/app/**` 一律需要客户端 token;
管理端 token 打到客户端接口会被拒(两边密钥不同)。
- 需要登录的接口建议带 `X-Device-Id`,用于下载授权、并发播放限制与风控。
性能(本机实测,预热后 30 次采样,目标 50ms):
@@ -285,8 +395,13 @@ public Result<Long> create(@Valid @RequestBody DramaCategorySaveReqVO req) { ...
## 尚未实现
视频转码(HLS 多码率)、C 端接口(`/app/**`)、金币与会员、单集解锁、支付与对账、
数据看板统计、Redis 缓存与限流。详见架构设计文档第 11 章。
评论点赞分享、订单与钱包权益(IAP / Google Play Billing 票据校验、对账、退款回收)、
创作者中心、内容治理与经营看板、转码 worker(消费 `transcode_task`,产出多码率 HLS)、
Redis 缓存与限流、离线下载授权、埋点与数据看板。
这些模块的表结构、接口契约、状态机与权限点都已经冻结,见
[docs/api-phase1.md](docs/api-phase1.md) 里标注「下一批」的部分 —— 接下来是照契约填代码,
不会再有结构性改动。
## 常见问题