petstore-backend/README.md
2026-08-02 00:15:00 +08:00

10 KiB
Raw Blame History

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 6048007 天) 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 生产必须为空 123456dev/ 空production 演示万能验证码production profile 自动关闭
SPRING_JPA_HIBERNATE_DDL_AUTO validateproduction 生产只能为 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 稳定化批次迁移要点

  1. 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 创建唯一约束;否则应用启动会失败。

  2. 历史图片 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 库若无历史数据可跳过。

  3. t_report.appointment_idP0 口径要求报告必须绑定预约,但历史 DDL 曾将该列改为 NULL。新环境由 JPA 实体 @UniqueConstraint 创建约束;老环境若该列允许 NULL 且无重复,可直接加唯一约束。

  4. 预约/报告身份字段拆分:升级到 2026-08-01 之后的版本前,生产库必须先执行:

    mysql -h <host> -u <user> -p petstore < db/migrations/20260801_split_service_identity.sql
    

    脚本新增 customer_user_idcreated_by_user_idauthor_staff_id 并回填可确定的历史数据。末尾三个验证查询必须留档;unresolved_assisted_appointments 非 0 时需人工确认,禁止把旧员工 user_id 冒认成客户。

  5. 门店客户主档:身份拆分验证完成后,紧接着执行:

    mysql -h <host> -u <user> -p petstore < db/migrations/20260801_create_store_customer.sql
    

    脚本创建 t_store_customer,从预约和报告留资回填本店客户关系。末尾 appointments_without_store_customerleads_without_store_customerstore_customers_linked_to_non_customer 必须全部为 0。

  6. 业务事件落库:客户主档验证完成后执行:

    mysql -h <host> -u <user> -p petstore < db/migrations/20260801_create_business_event.sql
    

    脚本创建不可变 t_business_event 并回填可确认的历史事实。末尾四个验证计数必须全部为 0报告打开和服务开始没有可靠历史数据不做猜测性回填。

  7. 真实预约容量:业务事件验证完成后执行:

    mysql -h <host> -u <user> -p petstore < db/migrations/20260801_create_booking_capacity.sql
    

    脚本新增门店并发容量、服务时长、预约时长快照和连续排班占用。末尾容量、时长和快照验证计数必须全部为 0。

  8. 报告显式确认发送状态:预约容量验证完成后执行:

    mysql -h <host> -u <user> -p petstore < db/migrations/20260801_create_report_send_status.sql
    

    脚本把历史报告统一标记为 unknown,迁移后的新报告默认 unsent;不得推测历史发送事实。末尾三项状态与回执一致性验证必须全部为 0。

production profile

java -jar petstore-backend.jar --spring.profiles.active=production

production profile 下:

  • ddl-auto=validate(只校验,不改表)
  • show-sql=false
  • SMS_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 和 18 项只读数据不变量检查,通过后退出。它不会执行迁移,也不会修复数据。

测试

mvn test

最小测试基线P0 稳定化批次):

  • SessionTokenServiceTest:签发/校验/篡改/过期
  • ReportServiceTestappointmentId 必填、重复报告、doing→done
  • ReportControllerTest:公开字段裁剪、照片必填业务码、重复报告业务码
  • 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 已配置且后端可生成可访问媒体 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 ≥ 200MBspring.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
  • 已执行 20260801_create_report_send_status.sql,历史 unknown 与三项回执一致性验证均为 0
  • CORS_ALLOWED_ORIGINS 仅包含本次发布的显式 HTTPS Web 源
  • deploy/release-preflight.sh 已以最终环境变量通过并留存输出
  • readiness 与上线后只读冒烟全部通过
  • 已暴露凭据DB 密码、微信 AppSecret已轮换

安全说明

  • 最小鉴权上下文HMAC session tokenSessionTokenService+ 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 commitsfeat / fix / chore / docs / refactor / test
  4. 新建 Pull Request