Files
FruitServer/README.md
T
Vicky b3179fc836 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 仍按普通表定义唯一索引,
   表分区化之后重跑脚本必定报错(分区表的唯一索引必须包含分区键)。改为在本脚本里就
   按分区表定义,并验证脚本可重复执行。
2026-10-15 11:44:26 +08:00

436 lines
22 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.
# 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` 启动 |
> `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)。
## 快速开始
服务有两个入口,可以只启动其中一个:
| 服务 | 模块 | 端口 | 面向 |
| --- | --- | --- | --- |
| 管理端 | `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` | 是否自动清理过期分区(默认只建不删) |
开发阶段签名校验默认关闭,是为了不打断正在联调的客户端。**上线前必须打开**,
否则片源地址可以被任意复用与盗链。
## 第一阶段实现进度
| 模块 | 状态 | 说明 |
| --- | --- | --- |
| 01 基础与交付准备 | 已实现 | 多模块骨架、接口契约、双密钥鉴权、分区维护 |
| 02 用户账号与登录 | 已实现 | 手机号/邮箱注册登录、验证码反刷、令牌轮换、资料、注销、封禁即时生效 |
| 03 内容发现与详情 | 已实现 | 首页/发现/分类/详情;补内容分级、可播区域、版权窗口、标签字段 |
| 04 播放器与播放控制 | 部分 | 播放票据 + 签名地址 + 进度已实现;多码率片源待转码 worker |
| 05 记录、收藏与下载 | 部分 | 进度、观看流水、收藏已实现;离线下载授权待做 |
| 06 评论、点赞与分享 | 待做 | 表已建,接口与实现待补 |
| 07 创作者中心 | 待做 | 表已建;转码任务与多码率产物记账已实现 |
| 08 平台运营后台 | 部分 | App 用户管理已实现;内容治理、订单、看板待做 |
| 09 分集收费与支付 | 待做 | 表已建;播放入口的权益判定端口已预留,接上即生效 |
| 10 质量、数据与上线 | 待做 | 压测、埋点、对账待做 |
## 默认账号
密码均为 `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` 格式化。
## 尚未实现
评论点赞分享、订单与钱包权益(IAP / Google Play Billing 票据校验、对账、退款回收)、
创作者中心、内容治理与经营看板、转码 worker(消费 `transcode_task`,产出多码率 HLS)、
Redis 缓存与限流、离线下载授权、埋点与数据看板。
这些模块的表结构、接口契约、状态机与权限点都已经冻结,见
[docs/api-phase1.md](docs/api-phase1.md) 里标注「下一批」的部分 —— 接下来是照契约填代码,
不会再有结构性改动。
## 常见问题
**编译报「找不到符号 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 的注解处理器配置保证这种情况下也能编译通过。