petstore-backend/README.md

161 lines
7.4 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.

# 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` 冒认成客户
### 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 必填重复报告doingdone
- `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`并留存未解析预约/报告验证结果
- [ ] 已暴露凭据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