10 KiB
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 |
微信回调;生产必须为真实 HTTPS 地址 |
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) |
CORS_ALLOWED_ORIGINS |
生产必填 | 本地开发源 | 逗号分隔的显式 HTTPS Web 源;禁止 *、localhost 和占位域名 |
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 自动关闭 |
SPRING_JPA_HIBERNATE_DDL_AUTO |
否 | validate(production) |
生产只能为 validate;禁止自动改表 |
SPRING_JPA_SHOW_SQL |
否 | false |
生产 SQL 日志开关,必须为 false |
LOG_LEVEL_PETSTORE |
否 | info |
com.petstore 包日志级别 |
⚠️ 密钥轮换:
application.yml历史版本曾提交过真实 DB 密码与微信 AppSecret。这些凭据已在仓库历史中暴露,必须按安全流程轮换:改 DB 密码、重置微信 AppSecret、更换PETSTORE_SESSION_SECRET。
本地开发
# 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 稳定化批次迁移要点
-
uk_report_appointment唯一约束上线前需清理重复appointment_id:-- 检查是否存在重复 SELECT appointment_id, COUNT(*) c FROM t_report WHERE deleted = 0 GROUP BY appointment_id HAVING c > 1; -- 清理:保留最新一条,其余软删 -- (具体清理 SQL 由 DBA 按数据情况编写)清理后再启动应用,让 JPA
ddl-auto:update创建唯一约束;否则应用启动会失败。 -
历史图片 URL 修复(旧版启动期自动执行,现已移除):
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 库若无历史数据可跳过。
-
t_report.appointment_id列:P0 口径要求报告必须绑定预约,但历史 DDL 曾将该列改为NULL。新环境由 JPA 实体@UniqueConstraint创建约束;老环境若该列允许 NULL 且无重复,可直接加唯一约束。 -
预约/报告身份字段拆分:升级到 2026-08-01 之后的版本前,生产库必须先执行:
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冒认成客户。 -
门店客户主档:身份拆分验证完成后,紧接着执行:
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。 -
业务事件落库:客户主档验证完成后执行:
mysql -h <host> -u <user> -p petstore < db/migrations/20260801_create_business_event.sql脚本创建不可变
t_business_event并回填可确认的历史事实。末尾四个验证计数必须全部为 0;报告打开和服务开始没有可靠历史数据,不做猜测性回填。 -
真实预约容量:业务事件验证完成后执行:
mysql -h <host> -u <user> -p petstore < db/migrations/20260801_create_booking_capacity.sql脚本新增门店并发容量、服务时长、预约时长快照和连续排班占用。末尾容量、时长和快照验证计数必须全部为 0。
production profile
java -jar petstore-backend.jar --spring.profiles.active=production
production profile 下:
ddl-auto=validate(只校验,不改表)show-sql=falseSMS_UNIVERSAL_CODE默认空(关闭万能验证码)- 不执行开发期默认服务初始化,启动和预检不会写入业务数据
/actuator/health/liveness与/actuator/health/readiness提供编排探针;响应不暴露内部细节
生产只读预检
完成备份和四个迁移后,以最终生产环境变量运行:
chmod +x deploy/release-preflight.sh deploy/production-smoke.sh
BUILD_FIRST=1 deploy/release-preflight.sh
脚本先校验生产配置、上传目录、Java 与 FFmpeg,再启动无 Web 的 production profile,执行 JPA schema validate 和 15 项只读数据不变量检查,通过后退出。它不会执行迁移,也不会修复数据。
测试
mvn test
最小测试基线(P0 稳定化批次):
SessionTokenServiceTest:签发/校验/篡改/过期ReportServiceTest:appointmentId 必填、重复报告、doing→doneReportControllerTest:公开字段裁剪、照片必填业务码、重复报告业务码AppointmentServiceTest:状态机非法迁移
RC 发布门禁
发布前必须依次执行并全部通过:
# 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
# 5. 上线后只读冒烟
API_ORIGIN=https://<api-domain> backend/deploy/production-smoke.sh
RC 检查项
APP_BASE_URL已配置且后端可生成可访问媒体 URLPETSTORE_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 - 已执行
20260801_create_booking_capacity.sql,容量与时长验证计数均为 0 CORS_ALLOWED_ORIGINS仅包含本次发布的显式 HTTPS Web 源deploy/release-preflight.sh已以最终环境变量通过并留存输出- readiness 与上线后只读冒烟全部通过
- 已暴露凭据(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-leveluserId/storeId/reportToken;日志只记 token SHA-256 前 8 位 hash。 - 回访池脱敏:
/api/report/leads不返回完整wechatOpenid/wechatUnionid,只返回wechatBound+ 脱敏 id。 - 上传路径保护:
FileController归一化路径 + 扩展名/MIME 校验,拒绝..逃逸。
参与贡献
- Fork 本仓库
- 新建
feat_xxx分支 - 提交代码(使用 conventional commits:
feat/fix/chore/docs/refactor/test) - 新建 Pull Request