Files
FruitServer/README.md
T
jianghaifeng 5609b42dcd docs: 对齐契约 §3 与 README 文档索引的进度口径
按实测修正 4 处状态描述,消除"已完成/未完成"口径不一致:

- api-phase1.md §3「契约与实现状态对照」:
  04 多码率为「转码 worker(C1)已产出」;06 分享埋点标「待 C3」;
  09 由"下一批"改为「Google Play 票据校验闭环已实现,Apple IAP 只留端口」;
  10 由"压测与埋点 下一批"改为「C1 已实现,C2/C3/压测 未做」;
  并补最后更新时间与「未完成清单见 README」的指向
- README.md 文档索引:api-phase1 的描述由"标注已实现 / 下一批"
  改为"含「契约与实现状态对照」"

为什么改:契约里的状态表过期会直接误导排期与联调判断,属最容易踩的坑。
2026-10-16 15:32:05 +08:00

506 lines
29 KiB
Markdown
Raw Permalink 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.
# FruitShorts 短剧服务(后端)
短剧服务的服务端,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) | 早期架构设计文档 |
| 项 | 版本 |
| --- | --- |
| 运行时 | JDK 21(LTS),编译目标 Java 17 |
| 框架 | Spring Boot 3.2.5、Spring Security 6、MyBatis-Plus 3.5.7 |
| 数据库 | PostgreSQL 18 |
| 构建 | Maven 3.10 多模块 |
## 目录结构
```
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、后端设计、接口清单
```
## 开发环境
本机环境由 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` 启动 |
| ffmpeg | 9.0.2(Homebrew 当前稳定版) | `brew install ffmpeg`;**只有转码 worker 需要**,两个在线入口(8080/8081)不依赖它 |
> `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
ffmpeg -version # 转码 worker 用;没装也能跑在线服务,只是转码任务会一直排队
```
> **ffmpeg 只被 `fruitshorts-transcode-worker` 使用**:`backend-design-phase1.md` §8.5 要求转码与在线服务隔离,
> 所以 app-api / admin-api 里 `fruitshorts.media.transcode.worker-enabled` 默认 false,转码要单独起 worker 进程
> (`java -jar fruitshorts-transcode-worker/target/fruitshorts-transcode-worker-1.0.0-SNAPSHOT.jar`)。
说明:
- 工程编译目标为 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)。
## 快速开始
服务有两个入口,可以只启动其中一个:
| 服务 | 模块 | 端口 | 面向 |
| --- | --- | --- | --- |
| 管理端 | `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 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
```
| 脚本 | 作用 |
| --- | --- |
| `schema.sql` / `data.sql` | 系统域与内容域的基础表与初始化数据 |
| `schema-phase1.sql` | 第一阶段增量表:用户、播放记录、互动、交易、创作者、转码、运营 |
| `partition-maintenance.sql` | 分区函数 + 把高写入表转成分区 + 预建未来 3 个月分区 |
| `data-phase1.sql` | 新增权限点、超管授权、占位商品 |
四个脚本都幂等可重复执行,**顺序不要颠倒**:分区转换要求目标表是空的。
数据库名 `fruitshorts`,角色 `fruitshorts`(密码 `fruitshorts`)。
首次搭建环境时先建角色与库:
```sh
psql -d postgres -c "CREATE ROLE fruitshorts LOGIN PASSWORD 'fruitshorts';"
psql -d postgres -c "CREATE DATABASE fruitshorts OWNER fruitshorts ENCODING 'UTF8';"
```
连接信息在
`fruitshorts-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`,前端无需改动。
### 大表分区与保留策略
**为什么要一开始就分区**:`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
cd /Users/jianghaifeng/Desktop/ReelShort/FruitServer
mvn -DskipTests package
```
### 3. 启动
```sh
cd /Users/jianghaifeng/Desktop/ReelShort/FruitServer
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` | 是否自动清理过期分区(默认只建不删) |
开发阶段签名校验默认关闭,是为了不打断正在联调的客户端。**上线前必须打开**,
否则片源地址可以被任意复用与盗链。
## 第一阶段实现进度
> 最后更新:2026-10-16(后端 `develop` 分支 `4418a84`)。**未完成清单见文末「尚未完成」。**
| 模块 | 状态 | 说明 |
| --- | --- | --- |
| 01 基础与交付准备 | 已实现 | 多模块骨架、接口契约、双密钥鉴权、分区维护;**雪花主键(S0)**、**Swagger 在线文档 `/doc.html`(S1)** |
| 02 用户账号与登录 | 已实现 | 手机号/邮箱注册登录、验证码反刷、令牌轮换、资料、注销、封禁即时生效 |
| 03 内容发现与详情 | 已实现 | 首页/发现/分类/详情;内容分级、可播区域、版权窗口、标签字段 |
| 04 播放器与播放控制 | 已实现 | 播放票据 + 签名地址 + 进度;多码率 HLS 由转码 worker 产出(见 10) |
| 05 记录、收藏与下载 | 已实现 | 进度、观看流水、收藏、离线下载授权与任务 |
| 06 评论、点赞与分享 | 已实现 | 评论(游标翻页/审核/删除)、点赞、举报、敏感词;**分享埋点待 C3** |
| 07 创作者中心 | 已实现 | **在后台管理(Web 端 `ReelShortWeb`)**:工作台、内容编辑、分片上传与断点续传、提交审核与上下架、分集收费与试看、数据看板;不含提现/结算。**App 客户端不提供创作者中心**(口径 2026-10-16) |
| 08 平台运营后台 | 已实现 | 用户管理、内容治理、订单与权益、创作者与内容审核、数据看板 + 四个后台页面 |
| 09 分集收费与支付 | 部分 | **Google Play 票据校验闭环已实现**(校验→建单→发权益、幂等与防重放、恢复购买、退款拒付回收、对账);**Apple IAP 只留端口未实现**(印尼 iOS 占比低,文档口径为"预留") |
| 10 质量、数据与上线 | 部分 | **转码 worker(C1)已实现**;**Redis 缓存与限流(C2)、埋点(C3)、压测未做** |
### 已完成批次(按提交顺序)
| 批次 | 内容 | 提交 |
| --- | --- | --- |
| S0 | 主键改雪花(58 表 / 52 实体 / 存量库幂等迁移脚本) | `37c9a14` `84aa0e1` |
| S1 | 接口文档在线化(39 个 Controller / 172 个接口中文注解 + `/doc.html`) | `c6fcfcc` |
| 契约补遗 | 补全接口契约遗漏(系统管理 / 内容 / watch-history / creators 账号 / media 详情) | `efe8af6` |
| M2 | 后端契约测试补齐(**0 → 37 条**) | `fafadfc` |
| B1-1 | Google Play 票据校验闭环(校验→建单→发权益) | `db897d5` |
| B1-2 | 恢复购买 / 退款拒付回收 / 对账真实比对 + Apple 端口预留 | `1dc97b4` |
| C1 | 转码 worker(独立进程消费 `transcode_task`,四档 HLS + 封面) | `9f76302` |
| D1 | 部署就绪(配置外置 + prod fail-fast + 自包含 Compose + 冒烟/回滚) | `b567365` |
| D1-fix | 冒烟脚本与 prod Swagger 开关解耦 | `cbb9961` |
| D2 | 部署文档按 specs 约束补全(新增 `DEPLOY-CHECKLIST.md`) | `7dd4b15` |
| 部署修复 | PG18 数据卷挂载路径 + worker 的 prod 密钥/CORS 映射(PR #1) | `8b46e53` `4c4f643` |
| E1 | 补 5 个 C 端接口(`/app/creator/apply`、`/app/config`、`/app/notices`) | `14b5e41` |
| 契约修正 | 移除 `/app/creator/works`、`/app/creator/stats` 的 C 端定义 | `bb21920` |
| E1-rollback | 撤除上述两条接口对应代码(创作者中心归 Web 端) | `4418a84` |
**门禁现状**:`mvn test` **91 条全过**(app-api 49 / commerce 27 / worker 12 / admin-api 3)。
> 客户端仓库 `ReelShort`(Flutter)另有已完成批次:网络层换 `dio`(`1853e18`)、客户端改名 `nontonki → FruitShorts` + 印尼语 `id-ID` 文案(`61454e3`)。
## 默认账号
密码均为 `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` | 配置项 `fruitshorts.media.root-dir`,按 `业务类型/年月/uuid.ext` 分目录 |
| 访问前缀 | `/files/**` | 配置项 `fruitshorts.media.public-prefix`,映射为静态资源 |
| 分片大小 | 5MB | `fruitshorts.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 客户端,只暴露已上线内容,不含审核状态、操作日志等管理端字段。
**完整接口清单(含请求/响应示例与错误码)见 [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` | 播放票据:服务端判定权益后签发**短时效签名地址** |
| 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/...`),客户端拼上服务地址即可;
播放地址会带 `?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):
| 接口 | 平均 | 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` 格式化。
## 尚未完成
> 最后更新:2026-10-16。完成后请同步本表与 [docs/api-phase1.md](docs/api-phase1.md)。
### 一、后端研发(剩余)
| # | 项 | 内容 | 依据 |
| --- | --- | --- | --- |
| 1 | **C2 Redis 缓存与限流** | 首页/发现/分类等热点接口缓存(Cache-Aside)+ 验证码/注册/登录限流;生产 Redis 规格 `4C16G 主从 2~3 哨兵` | 任务清单 C2、`17-风险评估` R-10、`10-安全方案` §三 |
| 2 | **C3 埋点** | 播放/支付/转化事件 + 数据字典对齐(含 06 的分享埋点) | 任务清单 C3、`api-phase1` §3 |
| 3 | **Apple IAP 票据校验** | 目前只留端口与枚举,未做真实校验(印尼 iOS 占比低,文档口径为"预留") | `api-phase1` §1.8 |
| 4 | **压测** | 目标 `P95 < 50ms`(预热后);容量口径文档里有两套("1 万并发"与"2 万并发 + 3000 QPS"),需定稿 | `09-测试计划` §五、`13/14-容量` |
| 5 | **对账数据源** | 结算金额/手续费/税只能从 Play Console 财务报告取(需报表权限);当前 `storeAmountSum` 恒为 `null` + `notes` 说明 | B1-2 待确认 |
| 6 | **退款口径完善** | 部分退款按比例回收、退款实时性(当前由每日对账 + 手动驱动,未接 RTDN 推送) | B1-2 待确认 |
### 二、上线前必补(P0 / Go-No-Go:非研发,但阻塞上线)
| # | 项 | 当前状态 | 目标 |
| --- | --- | --- | --- |
| 1 | 生产密钥集中托管 + 轮换 | 仅 `.env`(权限 600),无托管、无轮换流程 | 密钥集中托管、最小权限、可轮换(`10-安全方案` §五) |
| 2 | 安全专项测试 | 未做 | 越权/注入/敏感数据专项,高危上线前清零(`12-上线检查清单` §三) |
| 3 | 备份与恢复演练 | 仅手动 `pg_dump`,且与数据库**同机** | **RPO ≤ 5min / RTO ≤ 30min**;全量 + WAL 归档 + 加密 + 异地 + 季度演练(`08-备份恢复`) |
| 4 | 监控告警接入 | 未部署,仅日志 + `SlowRequestLogger` 兜底 | Prometheus / Grafana / Loki + P0/P1 告警规则(`07-监控告警`;**需先加 actuator 指标端点**) |
| 5 | HTTPS + 域名 + 反代 | 无 | 管理端 / 客户端 API / 媒资**分别域名**,HTTPS 强制(`04-部署架构` §四、`10-安全方案` §二) |
| 6 | 媒资迁 OSS/S3 + CDN | 本地磁盘 `./data/uploads` | 生产必须换 OSS/S3 + CDN(`08-备份恢复` §六、`12-上线检查清单` §四) |
### 三、基础设施
- **CI/CD 流水线**:当前手动跑门禁,未接流水线(`06-CI-CD方案`)
- **多实例 / 高可用**:当前单点部署;文档要求应用 `8C16G × 3~4`、转码 worker `8C16G × 2~3`,生产用 K8s(`14-生产资源规划`)
- **短信 / 邮件真实通道**:开发走 log,生产待接通道(`05-中间件清单`)
- **分区保留策略**:`partition.retention-enabled=false`,待合规确认后打开(`03-数据架构`)
- **`watch_history` 异步落库**:Kafka 异步通道为 Phase 2 预留(`13-容量与扩展规划` §四)
- 备份存储位置与成本待定(`08-备份恢复` §六)
### 四、不在后端范围(另有分工)
- **客户端 Flutter(`ReelShort`)**:A2~A6 与后端联调、B2 Google Play Billing 购买流程、秒开基线(需低端安卓设备);网络层 `dio`、改名与印尼语文案已完成
- **Web 后台(`ReelShortWeb`)**:由另一位同事侧负责
### 五、文档待同步(供文档维护人)
`docs/api-phase1.md` 与 specs `docs/` 下仍有 20+ 处**过期陈述**(例:`api-phase1` §3 仍把 09 支付标"下一批";specs `17-风险评估` 的 R-01/R-03/R-04/R-09 仍标"未开始")。
**遇到冲突以本表与本仓库 `api-phase1.md` 正文为准。**
> **已作废的方案**:曾提议新建 `fruitshorts-module-analytics` 收敛"两份创作者看板"。
> 因 2026-10-16 口径变更(**创作者中心由 Web 端承载**)撤除了 App 侧 `/app/creator/works`、`/app/creator/stats`,
> **两份看板问题已从根上消失,该方案不再需要**。
## 常见问题
**编译报「找不到符号 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 的注解处理器配置保证这种情况下也能编译通过。