175 lines
8.3 KiB
Markdown
175 lines
8.3 KiB
Markdown
# petstore-backend
|
||
|
||
宠小它 · 智慧宠物门店服务系统后端(Spring Boot 3.2 + JPA + MySQL)。
|
||
|
||
## 环境准备
|
||
|
||
### 必备
|
||
|
||
- JDK 17
|
||
- Maven 3.8+
|
||
- MySQL 5.7+ / 8.x
|
||
- FFmpeg + ffprobe(服务回顾短片生成;可选,未安装时成片功能不可用但不影响主流程)
|
||
|
||
### 环境变量
|
||
|
||
| 变量 | 必填 | 默认 | 说明 |
|
||
|------|------|------|------|
|
||
| `MYSQL_URL` | 否 | `jdbc:mysql://127.0.0.1:3306/petstore?...` | 数据库 JDBC URL |
|
||
| `MYSQL_USERNAME` | 否 | `root` | 数据库用户名 |
|
||
| `MYSQL_PASSWORD` | **是** | 空 | 数据库密码 |
|
||
| `WECHAT_APPID` | 生产必填 | 空 | 微信小程序 AppID |
|
||
| `WECHAT_APPSECRET` | 生产必填 | 空 | 微信小程序 AppSecret |
|
||
| `WECHAT_REDIRECT_URI` | 否 | `http://localhost:8080/api/wechat/callback` | 微信回调 |
|
||
| `PETSTORE_SESSION_SECRET` | **生产必填** | `dev-change-me` | HMAC session token 签名密钥;改密会使所有已签发 token 立即失效 |
|
||
| `PETSTORE_SESSION_TTL_SECONDS` | 否 | `604800`(7 天) | session token 有效期 |
|
||
| `APP_BASE_URL` | 生产必填 | `http://localhost:8080` | 后端对外可访问 base URL(用于生成媒体绝对 URL) |
|
||
| `UPLOAD_PATH` | 否 | `/www/petstore/uploads` | 上传目录绝对路径 |
|
||
| `HIGHLIGHT_FFMPEG` | 否 | `ffmpeg` | ffmpeg 可执行路径 |
|
||
| `HIGHLIGHT_FFPROBE` | 否 | `ffprobe` | ffprobe 可执行路径 |
|
||
| `HIGHLIGHT_MAX_DURATION_SEC` | 否 | `300` | 成片最大总秒数 |
|
||
| `HIGHLIGHT_BGM_PATH` | 否 | 空 | 商用授权 BGM 绝对路径;留空不混音 |
|
||
| `SMS_UNIVERSAL_CODE` | **生产必须为空** | `123456`(dev)/ 空(production) | 演示万能验证码;production profile 自动关闭 |
|
||
| `JPA_DDL_AUTO` | 否 | `update`(dev)/ `validate`(production) | JPA DDL 模式 |
|
||
| `JPA_SHOW_SQL` | 否 | `false` | SQL 日志 |
|
||
| `LOG_LEVEL_PETSTORE` | 否 | `info` | `com.petstore` 包日志级别 |
|
||
|
||
> ⚠️ **密钥轮换**:`application.yml` 历史版本曾提交过真实 DB 密码与微信 AppSecret。这些凭据已在仓库历史中暴露,**必须按安全流程轮换**:改 DB 密码、重置微信 AppSecret、更换 `PETSTORE_SESSION_SECRET`。
|
||
|
||
## 本地开发
|
||
|
||
```bash
|
||
# 1. 准备本地 MySQL,创建数据库
|
||
mysql -uroot -p -e "CREATE DATABASE petstore DEFAULT CHARSET utf8mb4"
|
||
|
||
# 2. 配置环境变量(或复制 application-example.yml 为 application.yml 并填写)
|
||
export MYSQL_PASSWORD=your_local_password
|
||
|
||
# 3. 启动
|
||
mvn spring-boot:run
|
||
|
||
# 4. 验证
|
||
curl http://localhost:8080/api/store/list
|
||
```
|
||
|
||
## 数据库迁移
|
||
|
||
⚠️ **生产库变更必须走 SQL/Flyway/Liquibase 或人工审核 SQL,禁止依赖 `ddl-auto:update` 自动改表**。
|
||
|
||
### P0 稳定化批次迁移要点
|
||
|
||
1. **`uk_report_appointment` 唯一约束上线前需清理重复 `appointment_id`**:
|
||
```sql
|
||
-- 检查是否存在重复
|
||
SELECT appointment_id, COUNT(*) c FROM t_report WHERE deleted = 0 GROUP BY appointment_id HAVING c > 1;
|
||
-- 清理:保留最新一条,其余软删
|
||
-- (具体清理 SQL 由 DBA 按数据情况编写)
|
||
```
|
||
清理后再启动应用,让 JPA `ddl-auto:update` 创建唯一约束;否则应用启动会失败。
|
||
|
||
2. **历史图片 URL 修复**(旧版启动期自动执行,现已移除):
|
||
```sql
|
||
UPDATE t_report SET before_photo = REPLACE(before_photo, 'http://localhost:8080/2026/', '/api/upload/image/2026/') WHERE before_photo LIKE 'http://localhost:8080/2026/%';
|
||
UPDATE t_report SET after_photo = REPLACE(after_photo, 'http://localhost:8080/2026/', '/api/upload/image/2026/') WHERE after_photo LIKE 'http://localhost:8080/2026/%';
|
||
```
|
||
仅需在生产库执行一次;dev 库若无历史数据可跳过。
|
||
|
||
3. **`t_report.appointment_id` 列**:P0 口径要求报告必须绑定预约,但历史 DDL 曾将该列改为 `NULL`。新环境由 JPA 实体 `@UniqueConstraint` 创建约束;老环境若该列允许 NULL 且无重复,可直接加唯一约束。
|
||
|
||
4. **预约/报告身份字段拆分**:升级到 2026-08-01 之后的版本前,生产库必须先执行:
|
||
```bash
|
||
mysql -h <host> -u <user> -p petstore < db/migrations/20260801_split_service_identity.sql
|
||
```
|
||
脚本新增 `customer_user_id`、`created_by_user_id`、`author_staff_id` 并回填可确定的历史数据。末尾三个验证查询必须留档;`unresolved_assisted_appointments` 非 0 时需人工确认,禁止把旧员工 `user_id` 冒认成客户。
|
||
|
||
5. **门店客户主档**:身份拆分验证完成后,紧接着执行:
|
||
```bash
|
||
mysql -h <host> -u <user> -p petstore < db/migrations/20260801_create_store_customer.sql
|
||
```
|
||
脚本创建 `t_store_customer`,从预约和报告留资回填本店客户关系。末尾 `appointments_without_store_customer`、`leads_without_store_customer`、`store_customers_linked_to_non_customer` 必须全部为 0。
|
||
|
||
6. **业务事件落库**:客户主档验证完成后执行:
|
||
```bash
|
||
mysql -h <host> -u <user> -p petstore < db/migrations/20260801_create_business_event.sql
|
||
```
|
||
脚本创建不可变 `t_business_event` 并回填可确认的历史事实。末尾四个验证计数必须全部为 0;报告打开和服务开始没有可靠历史数据,不做猜测性回填。
|
||
|
||
### production profile
|
||
|
||
```bash
|
||
java -jar petstore-backend.jar --spring.profiles.active=production
|
||
```
|
||
|
||
production profile 下:
|
||
- `ddl-auto=validate`(只校验,不改表)
|
||
- `show-sql=false`
|
||
- `SMS_UNIVERSAL_CODE` 默认空(关闭万能验证码)
|
||
|
||
## 测试
|
||
|
||
```bash
|
||
mvn test
|
||
```
|
||
|
||
最小测试基线(P0 稳定化批次):
|
||
- `SessionTokenServiceTest`:签发/校验/篡改/过期
|
||
- `ReportServiceTest`:appointmentId 必填、重复报告、doing→done
|
||
- `ReportControllerTest`:公开字段裁剪、照片必填业务码、重复报告业务码
|
||
- `AppointmentServiceTest`:状态机非法迁移
|
||
|
||
## RC 发布门禁
|
||
|
||
发布前必须依次执行并全部通过:
|
||
|
||
```bash
|
||
# 1. FFmpeg 可用
|
||
ffmpeg -version
|
||
ffprobe -version
|
||
|
||
# 2. 上传资源可访问(替换 <known-file> 为已上传的测试文件)
|
||
curl -I "$APP_BASE_URL/api/upload/image/<known-file>"
|
||
# 期望:HTTP/1.1 200
|
||
|
||
# 3. 后端测试
|
||
mvn -f backend/pom.xml test
|
||
# 期望:BUILD SUCCESS,且不再输出 "No tests to run"
|
||
|
||
# 4. 前端构建(见 frontend/README.md)
|
||
npm --prefix frontend install
|
||
npm --prefix frontend run build:h5
|
||
npm --prefix frontend run build:mp-weixin
|
||
```
|
||
|
||
### RC 检查项
|
||
|
||
- [ ] `APP_BASE_URL` 已配置且后端可生成可访问媒体 URL
|
||
- [ ] `PETSTORE_SESSION_SECRET` 已通过环境变量覆盖(非 `dev-change-me`)
|
||
- [ ] `MYSQL_PASSWORD` / `WECHAT_APPSECRET` 等敏感项通过环境变量注入,未硬编码
|
||
- [ ] `HIGHLIGHT_FFMPEG` / `HIGHLIGHT_FFPROBE` 指向可用二进制
|
||
- [ ] `HIGHLIGHT_MAX_DURATION_SEC` / `HIGHLIGHT_BGM_PATH` 按运营配置
|
||
- [ ] 上传目录 `UPLOAD_PATH` 存在且进程有读写权限
|
||
- [ ] Nginx `client_max_body_size` ≥ 200MB(与 `spring.servlet.multipart.max-file-size` 一致)
|
||
- [ ] 启动使用 `--spring.profiles.active=production`
|
||
- [ ] `uk_report_appointment` 唯一约束上线前已清理重复数据
|
||
- [ ] 历史图片 URL 已执行一次性修复 SQL
|
||
- [ ] 已执行 `20260801_split_service_identity.sql`,并留存未解析预约/报告验证结果
|
||
- [ ] 已按顺序执行 `20260801_create_store_customer.sql`,三项主档验证计数均为 0
|
||
- [ ] 已执行 `20260801_create_business_event.sql`,四项事件回填验证计数均为 0
|
||
- [ ] 已暴露凭据(DB 密码、微信 AppSecret)已轮换
|
||
|
||
## 安全说明
|
||
|
||
- **最小鉴权上下文**:HMAC session token(`SessionTokenService`)+ `AuthInterceptor`,不引入完整 Spring Security。
|
||
- **公开接口白名单**:见 `AuthInterceptor.PUBLIC_PATTERNS`;其余 `/api/**` 要求 `Authorization: Bearer <token>`。
|
||
- **跨店/跨用户边界**:所有 protected 接口从 `CurrentUserContext` 派生 `userId/storeId/role`,不信任请求体里的身份字段。
|
||
- **公开报告页**:`GET /api/report/get?token=` 不返回 top-level `userId/storeId/reportToken`;日志只记 token SHA-256 前 8 位 hash。
|
||
- **回访池脱敏**:`/api/report/leads` 不返回完整 `wechatOpenid/wechatUnionid`,只返回 `wechatBound` + 脱敏 id。
|
||
- **上传路径保护**:`FileController` 归一化路径 + 扩展名/MIME 校验,拒绝 `..` 逃逸。
|
||
|
||
## 参与贡献
|
||
|
||
1. Fork 本仓库
|
||
2. 新建 `feat_xxx` 分支
|
||
3. 提交代码(使用 conventional commits:`feat` / `fix` / `chore` / `docs` / `refactor` / `test`)
|
||
4. 新建 Pull Request
|