22 changed files with 1404 additions and 125 deletions
@ -1,14 +0,0 @@ |
|||
*.jar binary |
|||
*.zip binary |
|||
*.xz binary |
|||
*.png binary |
|||
*.jpg binary |
|||
*.ttf binary |
|||
*.woff2 binary |
|||
*.sql text eol=lf |
|||
*.java text eol=lf |
|||
*.xml text eol=lf |
|||
*.yml text eol=lf |
|||
*.md text eol=lf |
|||
*.ps1 text eol=crlf |
|||
*.cmd text eol=crlf |
|||
@ -1,45 +0,0 @@ |
|||
# 青核后端(内部完整配置版,2026-10-15) |
|||
|
|||
本包包含当前后端源码、可执行 backend.jar、实际云服务配置和 2026-10-14 数据库快照。包含真实凭据,仅供接手开发使用,请勿上传到 Gitee 或公开分发。没有前端源码及 AI 工作记录。 |
|||
|
|||
## 直接启动 |
|||
|
|||
1. 安装 JDK 17 或 JDK 21,确保命令行 `java -version` 能运行。也可设置 JAVA_HOME 指向 JDK 安装目录。 |
|||
2. 完整解压到普通文件夹,不要在 ZIP 预览里运行。双击 `Start-Backend.cmd`,等待出现 `Started CampusApplication`。窗口保持开启,Ctrl+C 停止。 |
|||
3. 访问 `http://127.0.0.1:8009/actuator/health`,预期 `{"status":"UP"}`。接口文档:`http://127.0.0.1:8009/swagger-ui.html`。API 前缀为 `/api`。 |
|||
|
|||
启动不需要 Maven,也不需要在 Windows 中逐个设置业务环境变量。MySQL、Redis、RabbitMQ 使用现有云服务,环信、OSS、OCR、短信、推送、VOD 的真实配置已写入 application.yml。需要能访问这些云服务的网络。 |
|||
|
|||
端口占用时可在解压目录运行: |
|||
|
|||
```powershell |
|||
powershell -NoProfile -ExecutionPolicy Bypass -File .\Start-Backend.ps1 -Port 8010 |
|||
``` |
|||
|
|||
## IDEA 开发 |
|||
|
|||
打开 `campus-server/pom.xml`,选择 JDK 17 或 21,等待 Maven 依赖下载完成,运行 `com.campus.CampusApplication`。工作目录设为 `campus-server`。`src/main/resources/application.yml` 已包含同样的实际配置,dev 配置的数据库地址也已同步,不需要复制环境变量。 |
|||
|
|||
修改源码后如需更新可执行包,安装 Maven 并在解压目录运行: |
|||
|
|||
```powershell |
|||
powershell -NoProfile -ExecutionPolicy Bypass -File .\Start-Backend.ps1 -Rebuild |
|||
``` |
|||
|
|||
根目录 application.yml 是脚本启动时的外部配置;源码 resources 内配置用于 IDEA 直接启动。修改配置时应同步两处,避免运行方式不同而使用旧值。 |
|||
|
|||
## 数据库及联调 |
|||
|
|||
默认连接原云数据库 buskbuddy,数据库连接及密码在 application.yml 的 spring.datasource 中。无需导入 SQL 或安装本地 MySQL、Redis、RabbitMQ。随包的数据库 ZIP 是 2026-10-14 的 80 表快照;只有需要建立独立本地数据库时才解压导入,不要将快照导回当前云库。 |
|||
|
|||
保留当前联调行为:万能验证码已开启,值为 `111111`;Flyway 关闭以兼容当前 MySQL 5.7;环信使用国内 2 区地址。VOD 回调签名保持原有的未启用状态,没有编造签名;管理员启动授权名单为空,表示不额外授予账号管理员权限。这两个可选设置不影响后端启动。 |
|||
|
|||
现有 OssStsService 实际采用直接 AK/SK 上传凭证,不调用 STS AssumeRole;已移除旧配置中不存在的 RAM 角色占位值。后续如要改成真正的 STS 临时凭证,需要创建角色并调整该服务,不属于本包的配置补齐。 |
|||
|
|||
## 本包验证 |
|||
|
|||
2026-10-15 在交付电脑上使用本包脚本及 JDK 21 实际启动通过,健康检查返回 UP,接口文档返回 HTTP 200。MySQL 查询、Redis 认证及 PING、RabbitMQ 认证、环信令牌获取、OSS 认证读取、OCR 测试图片识别均通过。短信签名与模板查询、推送应用配置查询、VOD 列表及转码模板查询也通过。 |
|||
|
|||
未发送真实短信、推送消息,未测试前端实际上传及 VOD 公网回调;同事电脑上的网络、JDK、客户端配置仍需按以上启动方法验证。 |
|||
|
|||
手机与电脑在同一局域网时,客户端填写电脑实际 IPv4 地址及 8009 端口,并允许 Java 通过系统防火墙。异地联调需要另行建立外网入口;本包不包含已经失效的临时网址。 |
|||
@ -0,0 +1,220 @@ |
|||
# 青核后端(qinghe-backend) |
|||
|
|||
青核是一个校园社交与服务平台,提供用户、动态、话题讨论、组局活动、好友与群聊、通知、认证和内容审核等能力。本仓库是青核项目的后端与工程协作仓库,不包含 App / Web 前端源码。 |
|||
|
|||
首次独立部署请阅读 [本地部署说明](方案分析与实现(md)文件/部署说明-本地部署.md),使用脱敏模板与专用启动器。下文普通 IDEA/JAR 启动用于已有安全配置的开发环境。 |
|||
|
|||
## 当前交付基线 |
|||
|
|||
| 项目 | 当前状态 | |
|||
| --- | --- | |
|||
| 后端模块 | `campus-server`,包根 `com.campus` | |
|||
| 默认端口 | `8009` | |
|||
| 业务接口前缀 | `/api` | |
|||
| 构建产物 | `campus-server/target/campus-server-1.0.0.jar` | |
|||
| 数据库迁移 | `campus-server/src/main/resources/db/migration/`;当前 Flyway 关闭,脚本不会自动执行 | |
|||
| 接口契约 | 运行时 Swagger / OpenAPI;内部专题接口资料由维护者单独提供 | |
|||
| 自动化测试 | 以当前源码执行 `mvn test` 或 `mvn clean verify` 的结果为准 | |
|||
| 部署与验收边界 | 见本仓库的本地部署说明;内部工作记录和开发规范不随源码公开 | |
|||
|
|||
2026-10-21 部署材料验证:`mvn -Pportable clean verify` 通过,496 用例 / 0 失败 / 0 错误 / 0 跳过;启动器离线检查及 Compose 静态校验通过。尚未执行 Docker 实机启动、全新空库或第三方服务验收,详细边界见本地部署说明。 |
|||
|
|||
## 仓库结构 |
|||
|
|||
```text |
|||
qinghe-backend/ |
|||
├─ campus-server/ Spring Boot 后端模块 |
|||
│ ├─ pom.xml Maven 构建配置 |
|||
│ ├─ .env.example 环境变量占位示例 |
|||
│ ├─ compose.local.yml 独立本地基础设施及可选后端容器 |
|||
│ ├─ Start-Local.ps1 独立配置启动器 |
|||
│ └─ src/ |
|||
│ ├─ main/java/com/campus/ Controller、Service、实体、持久层与配置 |
|||
│ ├─ main/resources/ 应用配置与 db/migration/ 迁移脚本 |
|||
│ └─ test/java/com/campus/ 后端测试 |
|||
├─ 方案分析与实现(md)文件/ |
|||
│ └─ 部署说明-本地部署.md 随源码提供的部署与验收说明 |
|||
└─ README.md 项目入口与启动、联调说明 |
|||
``` |
|||
|
|||
目录位置沿用现有工作区。其他内部方案、工作记录、`AGENTS.md`、真实应用配置和 `gitea-upload/` 保留在维护者本地,不随本次源码发布。 |
|||
|
|||
## 技术栈 |
|||
|
|||
- Java 8 编译目标,本地可使用 JDK 17 / 21 构建运行。 |
|||
- Spring Boot **2.7.18**。 |
|||
- Spring MVC、Spring Security、Spring Data JPA / Hibernate,使用 `javax.*`。 |
|||
- MySQL、Redis、RabbitMQ;Flyway 依赖已引入,当前配置关闭自动迁移。 |
|||
- 环信 IM,阿里云 OSS、VOD、推送、OCR 和短信。 |
|||
- Maven、JUnit 5、Mockito。 |
|||
- SpringDoc OpenAPI UI 1.7.0。 |
|||
|
|||
## 环境准备 |
|||
|
|||
开发机需要安装 JDK 和 Maven,并可在 PowerShell 中执行: |
|||
|
|||
```powershell |
|||
java -version |
|||
mvn -version |
|||
``` |
|||
|
|||
后端启动需要数据库、Redis、RabbitMQ、JWT 等配置,以及对应业务使用的第三方服务配置。使用团队通过安全渠道提供的配置或在运行环境中注入对应 Spring 配置项。不要将真实密码、JWT secret、Token 或云服务密钥写入 README、脚本、日志或 Git。 |
|||
|
|||
仓库存在两份应用配置: |
|||
|
|||
- `campus-server/src/main/resources/application.yml`:模块内的应用配置,IDEA 启动时加载,构建时打入 JAR。 |
|||
- 根目录 `application.yml`:外部应用配置;按下文从仓库根目录运行 JAR 时默认加载,同名配置项优先于 JAR 内配置。 |
|||
|
|||
两份配置涉及共同配置项时必须同步维护。启动前确认工作目录和实际连接环境;配置文件被 Git 忽略,拉取源码后需单独准备。 |
|||
|
|||
另提供不含真实凭据的 [application.example.yml](campus-server/src/main/resources/application.example.yml) 和 [.env.example](campus-server/.env.example)。不要覆盖现有真实配置;新部署者按部署说明复制私有 `.env` 并填写自己的配置。万能验证码在模板中默认关闭,未接入云服务时相关功能不可用。 |
|||
|
|||
## 基础设施准备 |
|||
|
|||
独立开发环境使用现有模块目录下的 [compose.local.yml](campus-server/compose.local.yml),提供 MySQL、Redis、RabbitMQ,不调整其他文件位置。先准备私有 `.env` 并启动 Docker Desktop,再在 `campus-server` 目录执行: |
|||
|
|||
```powershell |
|||
docker compose --env-file .env -f compose.local.yml up -d mysql redis rabbitmq |
|||
docker compose --env-file .env -f compose.local.yml ps |
|||
``` |
|||
|
|||
等待三个服务 healthy 后启动后端。宿主机默认端口分别为 `13306/16379/15672`,RabbitMQ 管理页端口为 `15673`。数据保留、首次初始化及可选后端容器步骤见本地部署说明。当前尚未完成 Docker 实机与全新空库验收。 |
|||
|
|||
## 启动后端 |
|||
|
|||
### 构建并验证 |
|||
|
|||
以下命令从仓库根目录执行,进入模块后完成编译、测试、打包与 Maven 验证: |
|||
|
|||
```powershell |
|||
cd .\campus-server |
|||
mvn clean verify |
|||
``` |
|||
|
|||
需要快速打包且已经完成对应测试时,可在 `campus-server` 目录执行: |
|||
|
|||
```powershell |
|||
mvn clean package -DskipTests |
|||
``` |
|||
|
|||
该命令跳过测试,不能作为测试通过的依据。构建产物为 `campus-server/target/campus-server-1.0.0.jar`。 |
|||
|
|||
独立部署/对外分发使用 `mvn -Pportable clean verify`,产物为 `target/portable/campus-server-1.0.0.jar`。该 profile 排除真实应用配置;普通开发产物可能包含本机配置,不应直接分发。独立启动器 [Start-Local.ps1](campus-server/Start-Local.ps1) 会采用该 profile 并显式加载脱敏模板。 |
|||
|
|||
### IDEA 启动 |
|||
|
|||
1. 打开 `campus-server/pom.xml`,等待 Maven 依赖加载完成。 |
|||
2. 选择 JDK 17 或 21,运行配置的工作目录设为 `campus-server`。 |
|||
3. 运行 `com.campus.CampusApplication`,等待日志出现 `Started CampusApplication`。 |
|||
|
|||
### 使用最新 JAR 启动 |
|||
|
|||
完成构建后,在 `campus-server` 目录执行: |
|||
|
|||
```powershell |
|||
java -jar .\target\campus-server-1.0.0.jar |
|||
``` |
|||
|
|||
需要使用根目录外部配置时,另开终端并从仓库根目录执行以下命令,选择一种方式启动即可: |
|||
|
|||
```powershell |
|||
java -jar .\campus-server\target\campus-server-1.0.0.jar |
|||
``` |
|||
|
|||
默认监听 `8009`。遇到端口占用先确认是否已有后端实例;需要另开实例时可在启动命令末尾添加 `--server.port=8010`,并相应调整联调地址。当前工作区采用上述源码或 JAR 启动方式。 |
|||
|
|||
### 启动验证 |
|||
|
|||
在另一个 PowerShell 窗口执行: |
|||
|
|||
```powershell |
|||
Invoke-RestMethod 'http://127.0.0.1:8009/actuator/health' |
|||
``` |
|||
|
|||
预期返回 `{"status":"UP"}`。接口文档入口为 [Swagger UI](http://127.0.0.1:8009/swagger-ui.html)。进入联调前应处理导致启动失败或健康状态异常的问题;第三方业务能力仍需按对应功能单独验收。 |
|||
|
|||
## 测试与接口校验 |
|||
|
|||
以下命令均在 `campus-server` 目录执行: |
|||
|
|||
```powershell |
|||
# 快速回归 |
|||
mvn test |
|||
|
|||
# 发布前完整验证 |
|||
mvn clean verify |
|||
|
|||
# 只运行指定测试类 |
|||
mvn -Dtest=GroupAnnouncementServiceTest test |
|||
``` |
|||
|
|||
测试报告位于 `campus-server/target/surefire-reports/`。验证记录需注明执行时间、代码版本、使用环境及结果。 |
|||
|
|||
启动后检查 [Swagger UI](http://127.0.0.1:8009/swagger-ui.html) 与 [运行时 OpenAPI](http://127.0.0.1:8009/v3/api-docs),并按功能对接文档核对路径、参数、鉴权、响应字段、状态码和错误文案。Markdown 接口清单是文档快照,发现与运行时不一致时需核对源码并同步更新。 |
|||
|
|||
## 接口文档与前端联调 |
|||
|
|||
随仓库提供的部署说明及运行时接口入口: |
|||
|
|||
| 文档 | 内容 | |
|||
| --- | --- | |
|||
| [本地部署说明](方案分析与实现(md)文件/部署说明-本地部署.md) | 配置、基础设施、数据库初始化、升级与验收 | |
|||
| [Swagger UI](http://127.0.0.1:8009/swagger-ui.html) | 后端启动后查看接口和参数 | |
|||
| [运行时 OpenAPI](http://127.0.0.1:8009/v3/api-docs) | 后端启动后获取接口契约 | |
|||
|
|||
其他专题接口清单与前端对接资料由维护者单独提供;需要联调某个功能时,先取得对应版本的契约说明。 |
|||
|
|||
本地联调的 API Base URL 为: |
|||
|
|||
```text |
|||
http://127.0.0.1:8009/api |
|||
``` |
|||
|
|||
接口清单写出的完整路径已包含 `/api`;拼接时避免重复添加。手机联调使用手机可访问的电脑地址和端口。使用内网穿透时,将前端与 Apifox 的 Base URL 统一替换为实际隧道地址并保留 `/api` 前缀。 |
|||
|
|||
需要认证的接口使用请求头: |
|||
|
|||
```http |
|||
access_token: <登录返回的令牌> |
|||
``` |
|||
|
|||
同名 Query 参数作为兜底方式,日常联调优先使用 Header。测试 Token、验证码和临时公网地址不要写入提交文件或公开文档。 |
|||
|
|||
接口统一返回 `{code,message,data}`;成功但没有内容时使用 `code=200, data=null`。分页参数从第 1 页开始,默认每页 20 条,上限 100 条;时间格式为 `yyyy-MM-dd HH:mm:ss`(GMT+8)。具体权限、参数、错误文案和状态码以对应功能文档为准。 |
|||
|
|||
## 数据库迁移与上线 |
|||
|
|||
当前配置为 `spring.flyway.enabled=false`,`db/migration/*.sql` **不会随启动自动执行**。上线某项功能前,阅读对应前端对接文档的迁移要求,核对目标数据库已执行的脚本,再人工执行所需迁移。 |
|||
|
|||
执行数据清理或包含 DDL 的脚本前先备份。MySQL DDL 可能隐式提交,不能假设整份脚本可以回滚。当前 Hibernate 使用 `ddl-auto: update`,它不能替代迁移脚本中的数据清理、外键及约束校验。 |
|||
|
|||
全新独立库与已有数据库升级的步骤不同,不能把历史迁移全部重跑。具体建表、唯一约束和外键核验流程见 [本地部署说明](方案分析与实现(md)文件/部署说明-本地部署.md) §6。 |
|||
|
|||
## 开发约定 |
|||
|
|||
仓库维护者另有内部开发规范。每个新功能按以下顺序交付: |
|||
|
|||
1. 核对需求、现有实现、实体、权限及接口语义,确认影响范围。 |
|||
2. 明确设计与兼容性要求,必要时在现有方案目录记录实现方案。 |
|||
3. 实现 DTO、Controller、Service、Repository 和必要迁移,遵循统一响应、异常、事务及防重复提交规则。 |
|||
4. 补齐单元测试,覆盖精确中文文案、权限、边界及失败时不落库;按风险补充集成验证。 |
|||
5. 执行 `mvn test`,发布前执行 `mvn clean verify`,并完成相应运行时接口和依赖验证。 |
|||
6. 在 `方案分析与实现(md)文件/` 交付 `前端对接-<功能名>.md`,说明兼容性、前端改造点、上线顺序及人工迁移要求。 |
|||
7. 记录本次改动、验证结果与未完成事项;内部工作台账由维护者单独保管。 |
|||
|
|||
根目录的 Markdown 入口为 `AGENTS.md` 和 `README.md`;实现方案、分析及前端对接文档统一保留在 `方案分析与实现(md)文件/`。参考其他项目时按本仓库实际结构填写文档,不据此调整现有文件位置。 |
|||
|
|||
提交前复核差异,使用精确文件清单加入版本控制。迁移 SQL 经复核后单独纳入,共享文件发生冲突时先核对双方改动及基线,再合并验证。 |
|||
|
|||
## 数据与安全边界 |
|||
|
|||
- 不提交含真实凭据的应用配置、Token、密钥、数据库快照、JAR、压缩包、日志和 `target/` 等产物。 |
|||
- 迁移 SQL 是受控例外:经复核后按文件精确加入版本控制。 |
|||
- 日志、错误响应和调试输出不记录密码、JWT、验证码及不必要的个人信息。 |
|||
- 测试与联调使用明确授权的环境和账号,不删除真实业务数据,不为联调放宽权限校验。 |
|||
|
|||
## 相关项目边界 |
|||
|
|||
本仓库只维护青核后端和后端文档。App / Web 前端源码位于其他仓库,所需前端改造通过对接文档交付;本文不假定前端项目在开发机上的具体路径。 |
|||
|
|||
`gitea-upload/` 为对外发布用的独立快照目录,已被主仓库忽略。主工作区的修改不会自动同步到该目录,发布时需单独核对源码、配置排除规则和文档。 |
|||
@ -1,4 +0,0 @@ |
|||
@echo off |
|||
cd /d "%~dp0" |
|||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0Start-Backend.ps1" |
|||
pause |
|||
@ -1,37 +0,0 @@ |
|||
[CmdletBinding()] |
|||
param([int]$Port = 8009, [switch]$Rebuild) |
|||
$ErrorActionPreference = 'Stop' |
|||
$javaExe = $null |
|||
if ($env:JAVA_HOME) { |
|||
$candidate = Join-Path $env:JAVA_HOME 'bin\java.exe' |
|||
if (Test-Path -LiteralPath $candidate) { $javaExe = $candidate } |
|||
} |
|||
if (-not $javaExe) { |
|||
$command = Get-Command java -ErrorAction SilentlyContinue |
|||
if (-not $command) { throw 'Install JDK 17 or 21 and add Java to PATH (or set JAVA_HOME).' } |
|||
$javaExe = $command.Source |
|||
} |
|||
$jar = Join-Path $PSScriptRoot 'backend.jar' |
|||
$configFile = Join-Path $PSScriptRoot 'application.yml' |
|||
if (-not (Test-Path -LiteralPath $configFile)) { throw 'application.yml must be next to this script.' } |
|||
if ($Rebuild) { |
|||
$maven = Get-Command mvn -ErrorAction Stop |
|||
Push-Location (Join-Path $PSScriptRoot 'campus-server') |
|||
try { |
|||
& $maven.Source clean package '-DskipTests' |
|||
if ($LASTEXITCODE -ne 0) { throw 'Maven build failed.' } |
|||
Copy-Item -LiteralPath 'target\campus-server-1.0.0.jar' -Destination $jar -Force |
|||
} finally { Pop-Location } |
|||
} |
|||
if (-not (Test-Path -LiteralPath $jar)) { throw 'backend.jar is missing; run with -Rebuild and Maven installed.' } |
|||
$occupied = Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue |
|||
if ($occupied) { throw "Port $Port is already in use. Stop the existing service or specify -Port 8010." } |
|||
Push-Location $PSScriptRoot |
|||
try { |
|||
Write-Host "API: http://127.0.0.1:$Port/api" |
|||
Write-Host "Health: http://127.0.0.1:$Port/actuator/health" |
|||
Write-Host 'Keep this window open. Press Ctrl+C to stop.' |
|||
$configLocation = 'file:' + $configFile.Replace('\', '/') |
|||
& $javaExe -jar $jar "--spring.config.additional-location=$configLocation" "--server.port=$Port" |
|||
if ($LASTEXITCODE -ne 0) { throw "Backend exited with code $LASTEXITCODE" } |
|||
} finally { Pop-Location } |
|||
@ -0,0 +1,9 @@ |
|||
** |
|||
!Dockerfile |
|||
!target/ |
|||
!target/portable/ |
|||
!target/portable/campus-server-1.0.0.jar |
|||
!src/ |
|||
!src/main/ |
|||
!src/main/resources/ |
|||
!src/main/resources/application.example.yml |
|||
@ -0,0 +1,51 @@ |
|||
# 复制成同目录 .env 后修改。仅包含示例值,不包含任何现有环境凭据。 |
|||
# Compose 会读取 --env-file;宿主机 Java 不会自动读取 .env,请用 Start-Local.ps1。 |
|||
# 值为单行原文,不加引号,不写行尾注释,不引用其他变量。 |
|||
COMPOSE_PROJECT_NAME=qinghe-local |
|||
DB_HOST=127.0.0.1 |
|||
DB_PORT=13306 |
|||
DB_NAME=qinghe_local |
|||
DB_USERNAME=qinghe |
|||
DB_PASSWORD=replace-me-db-password |
|||
MYSQL_ROOT_PASSWORD=replace-me-root-password |
|||
REDIS_HOST=127.0.0.1 |
|||
REDIS_PORT=16379 |
|||
REDIS_PASSWORD=replace-me-redis-password |
|||
RABBITMQ_HOST=127.0.0.1 |
|||
RABBITMQ_PORT=15672 |
|||
RABBITMQ_MANAGEMENT_PORT=15673 |
|||
RABBITMQ_USERNAME=qinghe |
|||
RABBITMQ_PASSWORD=replace-me-rabbitmq-password |
|||
JWT_SECRET=replace-me-with-a-random-secret-of-at-least-32-bytes |
|||
SERVER_PORT=8009 |
|||
# 首次独立空库用 update 建表;旧库升级先按文档审核迁移,验收后可用 validate。 |
|||
DB_DDL_AUTO=update |
|||
# 以下均为可选云服务配置;留空仅供受限的本地基础验证,不能使用对应云功能。 |
|||
OSS_ENDPOINT=http://127.0.0.1:19000 |
|||
OSS_ACCESS_KEY_ID= |
|||
OSS_ACCESS_KEY_SECRET= |
|||
OSS_BUCKET_NAME=qinghe-local-placeholder |
|||
OSS_DOMAIN=http://127.0.0.1:19000 |
|||
SMS_ACCESS_KEY_ID= |
|||
SMS_ACCESS_KEY_SECRET= |
|||
SMS_SIGN_NAME= |
|||
SMS_TEMPLATE_CODE= |
|||
SMS_REGION_ID=cn-hangzhou |
|||
PUSH_ENABLED=false |
|||
PUSH_ACCESS_KEY_ID= |
|||
PUSH_ACCESS_KEY_SECRET= |
|||
PUSH_ANDROID_APP_KEY= |
|||
PUSH_IOS_APP_KEY= |
|||
OCR_ACCESS_KEY_ID= |
|||
OCR_ACCESS_KEY_SECRET= |
|||
VOD_ACCESS_KEY_ID= |
|||
VOD_ACCESS_KEY_SECRET= |
|||
VOD_TRANSCODE_TEMPLATE_GROUP_ID= |
|||
VOD_CALLBACK_URL= |
|||
EASEMOB_APP_KEY= |
|||
EASEMOB_CLIENT_ID= |
|||
EASEMOB_CLIENT_SECRET= |
|||
EASEMOB_ORG_NAME= |
|||
EASEMOB_APP_NAME= |
|||
EASEMOB_BASE_URL=https://a1.easemob.com |
|||
DISCUSSION_ALLOWED_MEDIA_HOSTS=localhost,127.0.0.1 |
|||
@ -1,18 +1,17 @@ |
|||
# 指定基础镜像 |
|||
FROM swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/openjdk:8-jre |
|||
# 在 campus-server 目录先执行 mvn -Pportable clean verify。 |
|||
# 从软件源安装 FFmpeg,构建不再依赖开发机的离线压缩包。 |
|||
FROM eclipse-temurin:17-jre-jammy |
|||
|
|||
# 将本地已下载的 FFmpeg 静态版复制进镜像(无需网络) |
|||
COPY ffmpeg-release-amd64-static.tar.xz /tmp/ |
|||
RUN tar xf /tmp/ffmpeg-release-amd64-static.tar.xz -C /tmp \ |
|||
&& cp /tmp/ffmpeg-*-amd64-static/ffmpeg /usr/local/bin/ \ |
|||
&& cp /tmp/ffmpeg-*-amd64-static/ffprobe /usr/local/bin/ 2>/dev/null; true \ |
|||
&& chmod +x /usr/local/bin/ffmpeg \ |
|||
&& rm -rf /tmp/ffmpeg-* |
|||
RUN apt-get update \ |
|||
&& apt-get install -y --no-install-recommends ffmpeg fontconfig fonts-dejavu-core \ |
|||
&& rm -rf /var/lib/apt/lists/* \ |
|||
&& groupadd --system campus \ |
|||
&& useradd --system --gid campus --home-dir /app campus |
|||
|
|||
VOLUME /tmp |
|||
# 复制文件到容器(匹配实际的jar包名) |
|||
ADD target/campus-server-1.0.0.jar app.jar |
|||
# 申明需要暴露的端口 |
|||
WORKDIR /app |
|||
COPY target/portable/campus-server-1.0.0.jar /app/app.jar |
|||
COPY src/main/resources/application.example.yml /app/config/application.example.yml |
|||
|
|||
USER campus |
|||
EXPOSE 8009 |
|||
#配置容器启动后执行的命令 |
|||
ENTRYPOINT ["java","-Duser.timezone=GMT+08","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"] |
|||
ENTRYPOINT ["java", "-Duser.timezone=GMT+08", "-jar", "/app/app.jar", "--spring.config.location=file:/app/config/application.example.yml"] |
|||
|
|||
@ -0,0 +1,76 @@ |
|||
[CmdletBinding()] |
|||
param( |
|||
[string]$EnvFile = '.env', |
|||
[switch]$SkipBuild, |
|||
[switch]$CheckOnly |
|||
) |
|||
|
|||
$ErrorActionPreference = 'Stop' |
|||
# 仅载入此脚本允许的配置名,不把 .env 当成可执行脚本。 |
|||
$examplePath = Join-Path $PSScriptRoot '.env.example' |
|||
$allowedNames = @{} |
|||
foreach ($line in Get-Content -LiteralPath $examplePath -Encoding UTF8) { |
|||
if ($line -match '^([A-Z][A-Z0-9_]*)=') { $allowedNames[$Matches[1]] = $true } |
|||
} |
|||
$envPath = if ([IO.Path]::IsPathRooted($EnvFile)) { $EnvFile } else { Join-Path $PSScriptRoot $EnvFile } |
|||
if (-not (Test-Path -LiteralPath $envPath -PathType Leaf)) { |
|||
throw '缺少本地 .env;请从 .env.example 复制到 .env 并填写,不要覆盖现有文件。' |
|||
} |
|||
$settings = @{} |
|||
foreach ($line in Get-Content -LiteralPath $envPath -Encoding UTF8) { |
|||
if ([string]::IsNullOrWhiteSpace($line) -or $line.TrimStart().StartsWith('#')) { continue } |
|||
if ($line -notmatch '^([A-Z][A-Z0-9_]*)=(.*)$') { throw '环境文件格式错误:请使用 KEY=value,不使用 export 或引号。' } |
|||
$settingName = $Matches[1] |
|||
$settingValue = $Matches[2] |
|||
if (-not $allowedNames.ContainsKey($settingName)) { throw "环境文件包含未知配置名:$settingName" } |
|||
if ($settings.ContainsKey($settingName)) { throw "环境文件包含重复配置名:$settingName" } |
|||
if ($settingValue -match '^["'']' -or $settingValue -match '\s#' -or $settingValue.Contains('$')) { |
|||
throw "配置 $settingName 必须使用未加引号的单行原文,不能包含变量引用或行尾注释。" |
|||
} |
|||
$settings[$settingName] = $settingValue |
|||
} |
|||
foreach ($requiredName in @('DB_PASSWORD','MYSQL_ROOT_PASSWORD','REDIS_PASSWORD','RABBITMQ_PASSWORD','JWT_SECRET')) { |
|||
if (-not $settings.ContainsKey($requiredName) -or [string]::IsNullOrWhiteSpace($settings[$requiredName]) -or $settings[$requiredName].StartsWith('replace-me-')) { |
|||
throw "请为 $requiredName 设置自己的非示例值。" |
|||
} |
|||
} |
|||
if ([Text.Encoding]::UTF8.GetByteCount($settings['JWT_SECRET']) -lt 32) { throw 'JWT_SECRET 至少需要 32 字节。' } |
|||
if ($CheckOnly) { |
|||
Write-Host '本地配置格式与必填项检查通过(未连接任何服务、未构建或启动)。' |
|||
return |
|||
} |
|||
|
|||
$savedEnvironment = @{} |
|||
foreach ($settingName in $allowedNames.Keys) { |
|||
$savedEnvironment[$settingName] = [Environment]::GetEnvironmentVariable($settingName, 'Process') |
|||
} |
|||
Push-Location $PSScriptRoot |
|||
try { |
|||
# 清除继承的同名云凭据,只接受该 .env 中填写的值;空项回落到模板的占位值。 |
|||
foreach ($settingName in $allowedNames.Keys) { |
|||
$settingValue = if ($settings.ContainsKey($settingName) -and $settings[$settingName] -ne '') { $settings[$settingName] } else { $null } |
|||
if ($null -eq $settingValue) { |
|||
Remove-Item -LiteralPath "Env:$settingName" -ErrorAction SilentlyContinue |
|||
} else { |
|||
[Environment]::SetEnvironmentVariable($settingName, $settingValue, 'Process') |
|||
} |
|||
} |
|||
if (-not $SkipBuild) { |
|||
& mvn -Pportable clean verify |
|||
if ($LASTEXITCODE -ne 0) { throw '构建或测试失败,未启动后端。' } |
|||
} |
|||
$jarPath = Join-Path $PSScriptRoot 'target/portable/campus-server-1.0.0.jar' |
|||
if (-not (Test-Path -LiteralPath $jarPath -PathType Leaf)) { throw '缺少 portable JAR,请先执行 mvn -Pportable clean verify。' } |
|||
$configPath = (Join-Path $PSScriptRoot 'src/main/resources/application.example.yml').Replace('\', '/') |
|||
& java -Duser.timezone=GMT+08 -jar $jarPath "--spring.config.location=file:$configPath" |
|||
if ($LASTEXITCODE -ne 0) { throw '后端退出异常,请检查启动日志。' } |
|||
} finally { |
|||
foreach ($settingName in $allowedNames.Keys) { |
|||
if ($null -eq $savedEnvironment[$settingName]) { |
|||
Remove-Item -LiteralPath "Env:$settingName" -ErrorAction SilentlyContinue |
|||
} else { |
|||
[Environment]::SetEnvironmentVariable($settingName, $savedEnvironment[$settingName], 'Process') |
|||
} |
|||
} |
|||
Pop-Location |
|||
} |
|||
@ -0,0 +1,122 @@ |
|||
[CmdletBinding()] |
|||
param() |
|||
|
|||
# 仅做离线检查,不启动 Java、Maven 或 Docker,不连接数据库和云服务。 |
|||
$ErrorActionPreference = 'Stop' |
|||
$launcher = Join-Path $PSScriptRoot 'Start-Local.ps1' |
|||
$tokens = $null |
|||
$parseErrors = $null |
|||
[System.Management.Automation.Language.Parser]::ParseFile($launcher, [ref]$tokens, [ref]$parseErrors) | Out-Null |
|||
if ($parseErrors.Count) { throw 'Start-Local.ps1 语法检查失败。' } |
|||
$fixtureDirectory = Join-Path ([IO.Path]::GetTempPath()) ('qinghe-deployment-check-' + [Guid]::NewGuid().ToString('N')) |
|||
[void](New-Item -ItemType Directory -Path $fixtureDirectory) |
|||
$fixtureFile = Join-Path $fixtureDirectory '.env' |
|||
$utf8Bom = New-Object System.Text.UTF8Encoding($true) |
|||
$baseFixture = @' |
|||
DB_PASSWORD=test-db-value |
|||
MYSQL_ROOT_PASSWORD=test-root-value |
|||
REDIS_PASSWORD=test-redis-value |
|||
RABBITMQ_PASSWORD=test-rabbit-value |
|||
JWT_SECRET=test-only-secret-with-at-least-32-bytes-000 |
|||
'@ |
|||
$passedCases = 0 |
|||
|
|||
function Assert-ConfigurationCase { |
|||
param([string]$Name, [string]$Content, [bool]$ShouldPass, [string]$ExpectedMessage) |
|||
[IO.File]::WriteAllText($fixtureFile, $Content, $utf8Bom) |
|||
$didPass = $true |
|||
$failureMessage = '' |
|||
try { & $launcher -EnvFile $fixtureFile -CheckOnly } catch { |
|||
$didPass = $false |
|||
$failureMessage = $_.Exception.Message |
|||
} |
|||
if ($didPass -ne $ShouldPass) { throw "用例失败:$Name,结果不符合预期。" } |
|||
if (-not $ShouldPass -and -not $failureMessage.Contains($ExpectedMessage)) { |
|||
throw "用例失败:$Name,未得到预期的配置错误提示。" |
|||
} |
|||
Write-Host "PASS: $Name" |
|||
} |
|||
|
|||
try { |
|||
Assert-ConfigurationCase '合法配置' $baseFixture $true '' |
|||
$passedCases++ |
|||
Assert-ConfigurationCase '拒绝短 JWT secret' ($baseFixture.Replace('test-only-secret-with-at-least-32-bytes-000', 'short')) $false '至少需要 32 字节' |
|||
$passedCases++ |
|||
Assert-ConfigurationCase '拒绝未修改的示例密码' ($baseFixture.Replace('test-db-value', 'replace-me-db-password')) $false 'DB_PASSWORD' |
|||
$passedCases++ |
|||
Assert-ConfigurationCase '拒绝缺失密码' ($baseFixture.Replace('REDIS_PASSWORD=test-redis-value', 'REDIS_PASSWORD=')) $false 'REDIS_PASSWORD' |
|||
$passedCases++ |
|||
Assert-ConfigurationCase '拒绝重复变量' ($baseFixture + "`nDB_PASSWORD=second-value") $false '重复配置名' |
|||
$passedCases++ |
|||
Assert-ConfigurationCase '拒绝未知变量' ($baseFixture + "`nPATH=unknown") $false '未知配置名' |
|||
$passedCases++ |
|||
Assert-ConfigurationCase '拒绝脚本表达式' ($baseFixture + "`nOSS_DOMAIN=" + '$(Get-Process)') $false '不能包含变量引用' |
|||
$passedCases++ |
|||
Assert-ConfigurationCase '拒绝带引号的值' ($baseFixture + "`nOSS_DOMAIN=" + '"localhost"') $false '未加引号' |
|||
$passedCases++ |
|||
|
|||
$examplePath = Join-Path $PSScriptRoot '.env.example' |
|||
$exampleContent = [IO.File]::ReadAllText($examplePath) |
|||
Assert-ConfigurationCase '原始示例必须先填写' $exampleContent $false 'DB_PASSWORD' |
|||
$passedCases++ |
|||
|
|||
# 用局部 Java 替身检查启动参数及环境恢复,不启动 JVM 或访问任何服务。 |
|||
if (Test-Path -LiteralPath (Join-Path $PSScriptRoot 'target/portable/campus-server-1.0.0.jar')) { |
|||
$javaObservation = @{ Arguments = @(); DatabasePassword = $null } |
|||
function java { |
|||
$javaObservation.Arguments = @($args) |
|||
$javaObservation.DatabasePassword = [Environment]::GetEnvironmentVariable('DB_PASSWORD', 'Process') |
|||
$global:LASTEXITCODE = 0 |
|||
} |
|||
$previousPassword = [Environment]::GetEnvironmentVariable('DB_PASSWORD', 'Process') |
|||
try { |
|||
[Environment]::SetEnvironmentVariable('DB_PASSWORD', 'outer-test-sentinel', 'Process') |
|||
[IO.File]::WriteAllText($fixtureFile, $baseFixture, $utf8Bom) |
|||
& $launcher -EnvFile $fixtureFile -SkipBuild |
|||
if ($javaObservation.DatabasePassword -ne 'test-db-value') { throw '启动时未正确注入本地变量。' } |
|||
$locationArguments = @($javaObservation.Arguments | Where-Object { $_ -like '--spring.config.location=file:*application.example.yml' }) |
|||
if ($locationArguments.Count -ne 1) { throw '未显式指定独立模板。' } |
|||
if ([Environment]::GetEnvironmentVariable('DB_PASSWORD', 'Process') -ne 'outer-test-sentinel') { |
|||
throw '退出后没有恢复原进程环境变量。' |
|||
} |
|||
Write-Host 'PASS: 独立配置参数、环境注入及退出恢复(Java 替身,无外部连接)' |
|||
$passedCases++ |
|||
} finally { |
|||
if ($null -eq $previousPassword) { |
|||
Remove-Item -LiteralPath Env:DB_PASSWORD -ErrorAction SilentlyContinue |
|||
} else { |
|||
[Environment]::SetEnvironmentVariable('DB_PASSWORD', $previousPassword, 'Process') |
|||
} |
|||
Remove-Item -LiteralPath Function:\java |
|||
} |
|||
} |
|||
|
|||
$jarPath = Join-Path $PSScriptRoot 'target/portable/campus-server-1.0.0.jar' |
|||
if (Test-Path -LiteralPath $jarPath -PathType Leaf) { |
|||
Add-Type -AssemblyName System.IO.Compression.FileSystem |
|||
$archive = [IO.Compression.ZipFile]::OpenRead($jarPath) |
|||
try { |
|||
$privateEntries = @($archive.Entries | Where-Object { |
|||
$_.FullName -match '^BOOT-INF/classes/(.*/)?(application[^/]*\.(yml|yaml|properties)|\.env[^/]*)$' |
|||
}) |
|||
if ($privateEntries.Count) { throw '部署 JAR 混入应用配置或 .env 文件,禁止分发。' } |
|||
if (-not $archive.GetEntry('BOOT-INF/classes/com/campus/CampusApplication.class')) { |
|||
throw '部署 JAR 缺少应用主类。' |
|||
} |
|||
} finally { $archive.Dispose() } |
|||
Write-Host 'PASS: 部署 JAR 不含约定的私有应用配置,且存在主类' |
|||
$passedCases++ |
|||
} else { |
|||
Write-Host 'SKIP: 未找到 portable JAR,打包内容尚未检查。' |
|||
} |
|||
Write-Host "离线检查通过:$passedCases 项。未连接外部服务。" |
|||
} finally { |
|||
# 只清理本脚本刚创建、路径已核对的临时夹;不触及用户环境文件。 |
|||
$resolvedFixture = [IO.Path]::GetFullPath($fixtureDirectory) |
|||
$resolvedTemp = [IO.Path]::GetFullPath([IO.Path]::GetTempPath()).TrimEnd('\') + '\' |
|||
if (-not $resolvedFixture.StartsWith($resolvedTemp, [StringComparison]::OrdinalIgnoreCase) -or |
|||
-not ([IO.Path]::GetFileName($resolvedFixture)).StartsWith('qinghe-deployment-check-')) { |
|||
throw '临时检查目录超出预期路径,未清理。' |
|||
} |
|||
Remove-Item -LiteralPath $resolvedFixture -Recurse -Force |
|||
} |
|||
@ -0,0 +1,91 @@ |
|||
# 在 campus-server 目录执行;仅默认启动基础设施,backend 需显式启用 app profile。 |
|||
services: |
|||
mysql: |
|||
image: mysql:8.4 |
|||
environment: |
|||
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:?请先设置 MYSQL_ROOT_PASSWORD} |
|||
MYSQL_DATABASE: ${DB_NAME:-qinghe_local} |
|||
MYSQL_USER: ${DB_USERNAME:-qinghe} |
|||
MYSQL_PASSWORD: ${DB_PASSWORD:?请先设置 DB_PASSWORD} |
|||
TZ: Asia/Shanghai |
|||
command: ["--character-set-server=utf8mb4", "--collation-server=utf8mb4_unicode_ci"] |
|||
ports: |
|||
- "127.0.0.1:${DB_PORT:-13306}:3306" |
|||
volumes: |
|||
- mysql-data:/var/lib/mysql |
|||
healthcheck: |
|||
test: ["CMD-SHELL", "MYSQL_PWD=\"$$MYSQL_PASSWORD\" mysql --protocol=TCP -h 127.0.0.1 -u\"$$MYSQL_USER\" \"$$MYSQL_DATABASE\" -e 'SELECT 1' >/dev/null"] |
|||
interval: 10s |
|||
timeout: 5s |
|||
retries: 20 |
|||
start_period: 40s |
|||
redis: |
|||
image: redis:7.4-alpine |
|||
environment: |
|||
REDIS_PASSWORD: ${REDIS_PASSWORD:?请先设置 REDIS_PASSWORD} |
|||
command: ["sh", "-c", "exec redis-server --appendonly yes --requirepass \"$$REDIS_PASSWORD\""] |
|||
ports: |
|||
- "127.0.0.1:${REDIS_PORT:-16379}:6379" |
|||
volumes: |
|||
- redis-data:/data |
|||
healthcheck: |
|||
test: ["CMD-SHELL", "REDISCLI_AUTH=\"$$REDIS_PASSWORD\" redis-cli ping | grep -q PONG"] |
|||
interval: 10s |
|||
timeout: 5s |
|||
retries: 10 |
|||
rabbitmq: |
|||
image: rabbitmq:4.1-management |
|||
environment: |
|||
RABBITMQ_DEFAULT_USER: ${RABBITMQ_USERNAME:-qinghe} |
|||
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD:?请先设置 RABBITMQ_PASSWORD} |
|||
ports: |
|||
- "127.0.0.1:${RABBITMQ_PORT:-15672}:5672" |
|||
- "127.0.0.1:${RABBITMQ_MANAGEMENT_PORT:-15673}:15672" |
|||
volumes: |
|||
- rabbitmq-data:/var/lib/rabbitmq |
|||
healthcheck: |
|||
test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"] |
|||
interval: 10s |
|||
timeout: 10s |
|||
retries: 20 |
|||
start_period: 30s |
|||
backend: |
|||
profiles: ["app"] |
|||
build: |
|||
context: . |
|||
dockerfile: Dockerfile |
|||
env_file: |
|||
- .env |
|||
environment: |
|||
DB_HOST: mysql |
|||
DB_PORT: "3306" |
|||
REDIS_HOST: redis |
|||
REDIS_PORT: "6379" |
|||
RABBITMQ_HOST: rabbitmq |
|||
RABBITMQ_PORT: "5672" |
|||
SERVER_PORT: "8009" |
|||
JWT_SECRET: ${JWT_SECRET:?请先设置 JWT_SECRET} |
|||
# env_file 中空值不会触发 Spring 的默认值,这里统一回落到明确的本地占位值。 |
|||
OSS_ENDPOINT: ${OSS_ENDPOINT:-http://127.0.0.1:19000} |
|||
OSS_ACCESS_KEY_ID: ${OSS_ACCESS_KEY_ID:-your-local-placeholder} |
|||
OSS_ACCESS_KEY_SECRET: ${OSS_ACCESS_KEY_SECRET:-your-local-placeholder} |
|||
OSS_BUCKET_NAME: ${OSS_BUCKET_NAME:-qinghe-local-placeholder} |
|||
OSS_DOMAIN: ${OSS_DOMAIN:-http://127.0.0.1:19000} |
|||
SMS_ACCESS_KEY_ID: ${SMS_ACCESS_KEY_ID:-your-local-placeholder} |
|||
SMS_ACCESS_KEY_SECRET: ${SMS_ACCESS_KEY_SECRET:-your-local-placeholder} |
|||
SMS_SIGN_NAME: ${SMS_SIGN_NAME:-local-placeholder} |
|||
SMS_TEMPLATE_CODE: ${SMS_TEMPLATE_CODE:-local-placeholder} |
|||
SMS_REGION_ID: ${SMS_REGION_ID:-cn-hangzhou} |
|||
ports: |
|||
- "127.0.0.1:${SERVER_PORT:-8009}:8009" |
|||
depends_on: |
|||
mysql: |
|||
condition: service_healthy |
|||
redis: |
|||
condition: service_healthy |
|||
rabbitmq: |
|||
condition: service_healthy |
|||
volumes: |
|||
mysql-data: |
|||
redis-data: |
|||
rabbitmq-data: |
|||
@ -1,10 +0,0 @@ |
|||
{ |
|||
"folders": [ |
|||
{ |
|||
"path": "../../../../../.." |
|||
}, |
|||
{ |
|||
"path": "../../../../../../../collegeCli" |
|||
} |
|||
] |
|||
} |
|||
@ -0,0 +1,150 @@ |
|||
# 脱敏本地部署模板,不会自动加载;Start-Local.ps1 / Docker 显式指定此文件。 |
|||
# 保留此模板无真实凭据。真实值通过 .env -> 启动器 / Compose 环境变量注入。 |
|||
server: |
|||
address: 0.0.0.0 |
|||
port: ${SERVER_PORT:8009} |
|||
spring: |
|||
application: |
|||
name: campus-server |
|||
servlet: |
|||
multipart: |
|||
max-file-size: 100MB |
|||
max-request-size: 100MB |
|||
datasource: |
|||
url: jdbc:mysql://${DB_HOST:127.0.0.1}:${DB_PORT:13306}/${DB_NAME:qinghe_local}?useUnicode=true&characterEncoding=UTF-8&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai |
|||
username: ${DB_USERNAME:qinghe} |
|||
password: ${DB_PASSWORD} |
|||
driver-class-name: com.mysql.cj.jdbc.Driver |
|||
hikari: |
|||
maximum-pool-size: 10 |
|||
minimum-idle: 2 |
|||
connection-timeout: 30000 |
|||
jpa: |
|||
hibernate: |
|||
ddl-auto: ${DB_DDL_AUTO:update} |
|||
show-sql: false |
|||
properties: |
|||
hibernate: |
|||
dialect: org.hibernate.dialect.MySQL8Dialect |
|||
flyway: |
|||
enabled: false |
|||
jackson: |
|||
date-format: yyyy-MM-dd HH:mm:ss |
|||
time-zone: GMT+8 |
|||
serialization: |
|||
write-dates-as-timestamps: false |
|||
redis: |
|||
host: ${REDIS_HOST:127.0.0.1} |
|||
port: ${REDIS_PORT:16379} |
|||
password: ${REDIS_PASSWORD} |
|||
database: 0 |
|||
timeout: 10s |
|||
cache: |
|||
type: redis |
|||
redis: |
|||
time-to-live: 3600000 |
|||
cache-null-values: false |
|||
rabbitmq: |
|||
host: ${RABBITMQ_HOST:127.0.0.1} |
|||
port: ${RABBITMQ_PORT:15672} |
|||
username: ${RABBITMQ_USERNAME:qinghe} |
|||
password: ${RABBITMQ_PASSWORD} |
|||
virtual-host: / |
|||
publisher-confirm-type: correlated |
|||
publisher-returns: true |
|||
listener: |
|||
simple: |
|||
acknowledge-mode: manual |
|||
prefetch: 1 |
|||
retry: |
|||
enabled: true |
|||
max-attempts: 3 |
|||
initial-interval: 1000 |
|||
jwt: |
|||
secret: ${JWT_SECRET} |
|||
expiration: 86400000 |
|||
aliyun: |
|||
sms: |
|||
# 现有 @Value 无默认值,必须提供配置键;占位值不是可用的云服务凭据。 |
|||
access-key-id: ${SMS_ACCESS_KEY_ID:your-local-placeholder} |
|||
access-key-secret: ${SMS_ACCESS_KEY_SECRET:your-local-placeholder} |
|||
sign-name: ${SMS_SIGN_NAME:local-placeholder} |
|||
template-code: ${SMS_TEMPLATE_CODE:local-placeholder} |
|||
region-id: ${SMS_REGION_ID:cn-hangzhou} |
|||
oss: |
|||
# OssUtil 强依赖 OSS Bean,不能直接改成 false;本地占位不会提供存储功能。 |
|||
enabled: true |
|||
endpoint: ${OSS_ENDPOINT:http://127.0.0.1:19000} |
|||
access-key-id: ${OSS_ACCESS_KEY_ID:your-local-placeholder} |
|||
access-key-secret: ${OSS_ACCESS_KEY_SECRET:your-local-placeholder} |
|||
bucket-name: ${OSS_BUCKET_NAME:qinghe-local-placeholder} |
|||
domain: ${OSS_DOMAIN:http://127.0.0.1:19000} |
|||
max-file-size: 100 |
|||
allowed-file-types: |
|||
- jpg |
|||
- jpeg |
|||
- png |
|||
- gif |
|||
- webp |
|||
- pdf |
|||
- mp4 |
|||
push: |
|||
enabled: ${PUSH_ENABLED:false} |
|||
access-key-id: ${PUSH_ACCESS_KEY_ID:} |
|||
access-key-secret: ${PUSH_ACCESS_KEY_SECRET:} |
|||
region-id: cn-hangzhou |
|||
android-app-key: ${PUSH_ANDROID_APP_KEY:} |
|||
ios-app-key: ${PUSH_IOS_APP_KEY:} |
|||
ocr: |
|||
# enabled 字段并非统一禁用保障;无凭据时不要调用 OCR 接口。 |
|||
enabled: false |
|||
access-key-id: ${OCR_ACCESS_KEY_ID:} |
|||
access-key-secret: ${OCR_ACCESS_KEY_SECRET:} |
|||
region-id: cn-hangzhou |
|||
endpoint: ocr-api.cn-hangzhou.aliyuncs.com |
|||
connect-timeout: 10000 |
|||
read-timeout: 30000 |
|||
vod: |
|||
region-id: cn-shanghai |
|||
access-key-id: ${VOD_ACCESS_KEY_ID:} |
|||
access-key-secret: ${VOD_ACCESS_KEY_SECRET:} |
|||
transcode-template-group-id: ${VOD_TRANSCODE_TEMPLATE_GROUP_ID:} |
|||
callback-url: ${VOD_CALLBACK_URL:} |
|||
# 沿用现有协议,不编造回调签名校验。 |
|||
callback-auth: '' |
|||
easemob: |
|||
app-key: ${EASEMOB_APP_KEY:} |
|||
client-id: ${EASEMOB_CLIENT_ID:} |
|||
client-secret: ${EASEMOB_CLIENT_SECRET:} |
|||
org-name: ${EASEMOB_ORG_NAME:} |
|||
app-name: ${EASEMOB_APP_NAME:} |
|||
base-url: ${EASEMOB_BASE_URL:https://a1.easemob.com} |
|||
chat: |
|||
rest-send-forward-im: false |
|||
security: |
|||
captcha: |
|||
universal-enabled: false |
|||
management: |
|||
endpoints: |
|||
web: |
|||
exposure: |
|||
include: health,info |
|||
endpoint: |
|||
health: |
|||
show-details: never |
|||
springdoc: |
|||
api-docs: |
|||
path: /v3/api-docs |
|||
swagger-ui: |
|||
path: /swagger-ui.html |
|||
app: |
|||
admin: |
|||
bootstrap-phones: '' |
|||
click-guard: |
|||
window-ms: 2000 |
|||
topic: |
|||
auto-review: |
|||
enabled: false |
|||
backfill-apply: false |
|||
discussion: |
|||
allowed-media-hosts: ${DISCUSSION_ALLOWED_MEDIA_HOSTS:localhost,127.0.0.1} |
|||
@ -0,0 +1,61 @@ |
|||
-- 话题删除口径改为「软删 + 改名释放名称」后的一次性数据清理。 |
|||
-- |
|||
-- 背景:uk_topic_name 唯一索引包含软删行。历史上有若干话题是"只置 deleted=1、名字没改"的, |
|||
-- 它们的名字会一直被占用,用户无法重建同名话题(接口会返回「该名称曾被使用且已删除」)。 |
|||
-- 本脚本把所有 **已软删但名字未加后缀** 的话题改成 `原名_deleted_{id前8位}`,并同步改名 |
|||
-- 按名字关联的关注 / 访问 / 热度快照,以及别名,从而释放原名。 |
|||
-- |
|||
-- 注意:Flyway 关闭(spring.flyway.enabled=false),本脚本不会自动执行,需在目标库手工执行; |
|||
-- DDL/DML 混用会隐式提交,无法整体回滚,所以先做备份(第 0 步)。 |
|||
-- 注意:本脚本是幂等的:已经带 `_deleted_` 后缀的行会被跳过,可重复执行。 |
|||
-- 注意:只处理 deleted=1 的行,不影响任何在线话题、关注、访问数据。 |
|||
|
|||
-- ============ 0. 备份(回滚依据) ============ |
|||
CREATE TABLE IF NOT EXISTS topic_bak_v12 AS SELECT * FROM topic; |
|||
CREATE TABLE IF NOT EXISTS topic_follow_bak_v12 AS SELECT * FROM topic_follow; |
|||
CREATE TABLE IF NOT EXISTS topic_visit_bak_v12 AS SELECT * FROM topic_visit; |
|||
CREATE TABLE IF NOT EXISTS topic_hot_snapshot_bak_v12 AS SELECT * FROM topic_hot_snapshot; |
|||
CREATE TABLE IF NOT EXISTS topic_alias_bak_v12 AS SELECT * FROM topic_alias; |
|||
|
|||
-- ============ 1. 先改从属表(按旧名关联的三张) ============ |
|||
UPDATE topic_follow f |
|||
JOIN topic t ON t.name = f.topic_name |
|||
SET f.topic_name = CONCAT(LEFT(t.name, 80), '_deleted_', LEFT(t.id, 8)) |
|||
WHERE t.deleted = 1 AND INSTR(t.name, '_deleted_') = 0; |
|||
|
|||
UPDATE topic_visit v |
|||
JOIN topic t ON t.name = v.topic_name |
|||
SET v.topic_name = CONCAT(LEFT(t.name, 80), '_deleted_', LEFT(t.id, 8)) |
|||
WHERE t.deleted = 1 AND INSTR(t.name, '_deleted_') = 0; |
|||
|
|||
UPDATE topic_hot_snapshot s |
|||
JOIN topic t ON t.name = s.topic_name |
|||
SET s.topic_name = CONCAT(LEFT(t.name, 80), '_deleted_', LEFT(t.id, 8)) |
|||
WHERE t.deleted = 1 AND INSTR(t.name, '_deleted_') = 0; |
|||
|
|||
-- 别名按 topic_id 关联,统一加后缀(uk_topic_alias 同样包含软删行) |
|||
UPDATE topic_alias a |
|||
JOIN topic t ON t.id = a.topic_id |
|||
SET a.alias = CONCAT(a.alias, '_deleted_', LEFT(t.id, 8)) |
|||
WHERE t.deleted = 1 AND INSTR(t.name, '_deleted_') = 0; |
|||
|
|||
-- ============ 2. 再改话题自身 ============ |
|||
UPDATE topic |
|||
SET name = CONCAT(LEFT(name, 80), '_deleted_', LEFT(id, 8)) |
|||
WHERE deleted = 1 AND INSTR(name, '_deleted_') = 0; |
|||
|
|||
-- ============ 3. 校验 ============ |
|||
-- 3.1 不应再有"已删但名字未加后缀"的话题(期望 0) |
|||
SELECT 'soft_deleted_without_suffix' AS chk, COUNT(*) AS should_be_0 |
|||
FROM topic WHERE deleted = 1 AND INSTR(name, '_deleted_') = 0; |
|||
|
|||
-- 3.2 不应出现重名(期望空结果) |
|||
SELECT name, COUNT(*) AS cnt FROM topic GROUP BY name HAVING COUNT(*) > 1; |
|||
|
|||
-- 3.3 只读核对:已软删话题的新名字 |
|||
SELECT id, name, deleted FROM topic WHERE deleted = 1 ORDER BY updated_at DESC LIMIT 20; |
|||
|
|||
-- ============ 回滚说明 ============ |
|||
-- 若需回滚:先按 id 从 topic_bak_v12 / topic_follow_bak_v12 / topic_visit_bak_v12 / |
|||
-- topic_hot_snapshot_bak_v12 / topic_alias_bak_v12 还原对应行(按 id 匹配),再删除备份表。 |
|||
-- 注意:回滚会把原名重新占住,可能再次出现"无法重建同名话题"的问题。 |
|||
@ -0,0 +1,3 @@ |
|||
-- Flyway baseline marker for databases previously managed by Hibernate ddl-auto. |
|||
-- Future schema changes must be added as ordered V2+ migrations. |
|||
SELECT 1; |
|||
@ -0,0 +1,16 @@ |
|||
CREATE TABLE IF NOT EXISTS group_announcement ( |
|||
id VARCHAR(36) NOT NULL, |
|||
group_id VARCHAR(36) NOT NULL, |
|||
content TEXT NOT NULL, |
|||
publisher_id VARCHAR(36) NOT NULL, |
|||
publisher_nickname VARCHAR(50) NULL, |
|||
publisher_avatar VARCHAR(500) NULL, |
|||
published_at DATETIME(6) NOT NULL, |
|||
created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), |
|||
updated_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), |
|||
deleted TINYINT(1) NOT NULL DEFAULT 0, |
|||
PRIMARY KEY (id), |
|||
UNIQUE KEY uk_group_announcement_group_id (group_id), |
|||
CONSTRAINT fk_group_announcement_group |
|||
FOREIGN KEY (group_id) REFERENCES chat_group (id) ON DELETE CASCADE |
|||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; |
|||
@ -0,0 +1,133 @@ |
|||
-- 修复「组局活动连续点击加入/退出」遗留的脏数据: |
|||
-- 1) activity_participant 同一(活动,用户) 存在多行 CONFIRMED(并发重复报名) |
|||
-- 2) group_member 同一(群,用户) 存在多行(并发重复加群) |
|||
-- 3) activity.current_count 与 chat_group.member_count 漂移 |
|||
-- 并补上唯一约束,从数据库层面杜绝再次出现。 |
|||
-- |
|||
-- 注意:Flyway 当前关闭(spring.flyway.enabled=false),本脚本不会自动执行, |
|||
-- 需在目标库手工执行;请先在测试库执行并用文末校验语句确认结果。 |
|||
-- 注意:DDL(ALTER TABLE) 会隐式提交,脚本无法整体回滚,务必先做第 0 步备份。 |
|||
-- 注意:如果索引已由 Hibernate(ddl-auto=update) 建好,ALTER 会报 Duplicate key name, |
|||
-- 该报错可忽略,继续执行后续语句即可。 |
|||
|
|||
SET SESSION group_concat_max_len = 1000000; |
|||
|
|||
-- ============ 0. 备份(回滚依据) ============ |
|||
CREATE TABLE IF NOT EXISTS activity_participant_bak_20261016 AS SELECT * FROM activity_participant; |
|||
CREATE TABLE IF NOT EXISTS group_member_bak_20261016 AS SELECT * FROM group_member; |
|||
CREATE TABLE IF NOT EXISTS activity_bak_20261016 AS SELECT * FROM activity; |
|||
CREATE TABLE IF NOT EXISTS chat_group_bak_20261016 AS SELECT * FROM chat_group; |
|||
|
|||
-- ============ 1. activity_participant 去重 ============ |
|||
-- 同一 (activity_id, user_id) 只保留一行。 |
|||
-- 保留优先级:未删除行 -> CONFIRMED -> PENDING -> CANCELLED -> REJECTED -> REMOVED -> 其它; |
|||
-- 同优先级取 joined_at 最早、id 最小的那条。 |
|||
DELETE p FROM activity_participant p |
|||
JOIN ( |
|||
SELECT activity_id, |
|||
user_id, |
|||
SUBSTRING_INDEX( |
|||
GROUP_CONCAT(id ORDER BY deleted, |
|||
CASE status |
|||
WHEN 'CONFIRMED' THEN 0 |
|||
WHEN 'PENDING' THEN 1 |
|||
WHEN 'CANCELLED' THEN 2 |
|||
WHEN 'REJECTED' THEN 3 |
|||
WHEN 'REMOVED' THEN 4 |
|||
ELSE 5 |
|||
END, |
|||
joined_at, id), ',', 1) AS keep_id |
|||
FROM activity_participant |
|||
GROUP BY activity_id, user_id |
|||
) k ON k.activity_id = p.activity_id AND k.user_id = p.user_id |
|||
WHERE p.id <> k.keep_id; |
|||
|
|||
-- ============ 2. group_member 去重 ============ |
|||
-- 同一 (group_id, user_id) 只保留一行,优先保留权限最高的角色, |
|||
-- 避免误删群主/管理员记录:OWNER -> ADMIN -> MEMBER -> PENDING -> 其它。 |
|||
DELETE gm FROM group_member gm |
|||
JOIN ( |
|||
SELECT group_id, |
|||
user_id, |
|||
SUBSTRING_INDEX( |
|||
GROUP_CONCAT(id ORDER BY deleted, |
|||
CASE role |
|||
WHEN 'OWNER' THEN 0 |
|||
WHEN 'ADMIN' THEN 1 |
|||
WHEN 'MEMBER' THEN 2 |
|||
WHEN 'PENDING' THEN 3 |
|||
ELSE 4 |
|||
END, |
|||
join_time, id), ',', 1) AS keep_id |
|||
FROM group_member |
|||
GROUP BY group_id, user_id |
|||
) k ON k.group_id = gm.group_id AND k.user_id = gm.user_id |
|||
WHERE gm.id <> k.keep_id; |
|||
|
|||
-- ============ 3. 回算人数 ============ |
|||
-- activity.current_count 的口径(与代码保持一致): |
|||
-- CONFIRMED 报名行数 + Σ(报名单 quantity - 1),后者覆盖票档一次报多人。 |
|||
UPDATE activity a |
|||
SET a.current_count = ( |
|||
SELECT COUNT(*) |
|||
FROM activity_participant p |
|||
WHERE p.activity_id = a.id AND p.status = 'CONFIRMED' AND p.deleted = 0 |
|||
) + ( |
|||
SELECT COALESCE(SUM(r.quantity - 1), 0) |
|||
FROM activity_registration r |
|||
WHERE r.activity_id = a.id AND r.status = 'CONFIRMED' AND r.deleted = 0 |
|||
); |
|||
|
|||
-- 群成员数按成员行数回算。 |
|||
UPDATE chat_group g |
|||
SET g.member_count = ( |
|||
SELECT COUNT(*) |
|||
FROM group_member m |
|||
WHERE m.group_id = g.id AND m.deleted = 0 |
|||
); |
|||
|
|||
-- ============ 4. 唯一约束 ============ |
|||
ALTER TABLE activity_participant |
|||
ADD UNIQUE KEY uk_ap_activity_user (activity_id, user_id); |
|||
|
|||
ALTER TABLE group_member |
|||
ADD UNIQUE KEY uk_gm_group_user (group_id, user_id); |
|||
|
|||
-- ============ 5. 校验(都应为 0) ============ |
|||
-- 5.1 不应再有重复报名组合 |
|||
SELECT 'activity_participant_dup' AS chk, COUNT(*) AS should_be_0 FROM ( |
|||
SELECT activity_id, user_id FROM activity_participant |
|||
GROUP BY activity_id, user_id HAVING COUNT(*) > 1 |
|||
) d1; |
|||
|
|||
-- 5.2 不应再有重复群成员组合 |
|||
SELECT 'group_member_dup' AS chk, COUNT(*) AS should_be_0 FROM ( |
|||
SELECT group_id, user_id FROM group_member |
|||
GROUP BY group_id, user_id HAVING COUNT(*) > 1 |
|||
) d2; |
|||
|
|||
-- 5.3 活动人数应与口径一致 |
|||
SELECT 'activity_count_mismatch' AS chk, COUNT(*) AS should_be_0 FROM activity a |
|||
WHERE a.deleted = 0 |
|||
AND a.current_count <> ( |
|||
(SELECT COUNT(*) FROM activity_participant p |
|||
WHERE p.activity_id = a.id AND p.status = 'CONFIRMED' AND p.deleted = 0) |
|||
+ (SELECT COALESCE(SUM(r.quantity - 1), 0) FROM activity_registration r |
|||
WHERE r.activity_id = a.id AND r.status = 'CONFIRMED' AND r.deleted = 0) |
|||
); |
|||
|
|||
-- 5.4 群成员数应与成员行数一致 |
|||
SELECT 'group_member_count_mismatch' AS chk, COUNT(*) AS should_be_0 FROM chat_group g |
|||
WHERE g.deleted = 0 |
|||
AND g.member_count <> ( |
|||
SELECT COUNT(*) FROM group_member m WHERE m.group_id = g.id AND m.deleted = 0 |
|||
); |
|||
|
|||
-- ============ 回滚说明 ============ |
|||
-- 如需回滚去重与人数回算(索引需手工 DROP): |
|||
-- DROP INDEX uk_ap_activity_user ON activity_participant; |
|||
-- DROP INDEX uk_gm_group_user ON group_member; |
|||
-- DELETE FROM activity_participant; INSERT INTO activity_participant SELECT * FROM activity_participant_bak_20261016; |
|||
-- DELETE FROM group_member; INSERT INTO group_member SELECT * FROM group_member_bak_20261016; |
|||
-- UPDATE activity a JOIN activity_bak_20261016 b ON b.id = a.id SET a.current_count = b.current_count; |
|||
-- UPDATE chat_group g JOIN chat_group_bak_20261016 b ON b.id = g.id SET g.member_count = b.member_count; |
|||
@ -0,0 +1,101 @@ |
|||
-- ============================================================================ |
|||
-- V8:清理 SYSTEM 灌入的空壳话题(话题广场数据治理) |
|||
-- |
|||
-- ⚠️ 重要前提:先停掉外部灌数据任务! |
|||
-- 生产库 topic 表在两个时间点出现过 SYSTEM 数据: |
|||
-- 2026-09-28 12:05:00.465~.639(6 条英文标签) |
|||
-- 2026-09-30 17:05:00.395/.519(2 条中文标签) |
|||
-- 两批都精确落在「每小时第 5 分 00 秒」,且第二批只补了新增标签, |
|||
-- 说明存在一个"每小时 5 分执行、缺则补"的外部任务(直接写库,不走 API)。 |
|||
-- 只清理不停用该任务,下一个整点 5 分数据会被重新灌回。 |
|||
-- 定位方法见「方案分析与实现(md)文件/实现方案-话题广场数据治理.md」§1.10。 |
|||
-- |
|||
-- ⚠️ Flyway 当前 enabled: false,本脚本不会自动执行,必须人工在目标库执行。 |
|||
-- ⚠️ 备份段是 DDL(CREATE TABLE ... AS SELECT,隐式提交),必须单独先执行并确认成功, |
|||
-- 再执行清理段(清理段全是 DML,可包在事务里)。 |
|||
-- |
|||
-- 为什么必须物理删除而不是软删除: |
|||
-- topic 有唯一键 uk_topic_name,实体带 @Where(deleted = false)。 |
|||
-- 若置 deleted = 1,findByName 查不到该名字,但唯一键仍被占用, |
|||
-- 之后再创建同名话题会直接撞唯一键(500),名字被永久锁死。 |
|||
-- 因此这里使用 DELETE。 |
|||
-- ============================================================================ |
|||
|
|||
-- --------------------------------------------------------------------------- |
|||
-- 第一步:备份(单独执行,确认成功后继续) |
|||
-- --------------------------------------------------------------------------- |
|||
CREATE TABLE topic_bak_20261020 AS SELECT * FROM topic; |
|||
CREATE TABLE topic_follow_bak_20261020 AS SELECT * FROM topic_follow; |
|||
CREATE TABLE topic_visit_bak_20261020 AS SELECT * FROM topic_visit; |
|||
CREATE TABLE topic_alias_bak_20261020 AS SELECT * FROM topic_alias; |
|||
CREATE TABLE topic_hot_snapshot_bak_20261020 AS SELECT * FROM topic_hot_snapshot; |
|||
|
|||
-- --------------------------------------------------------------------------- |
|||
-- 第二步:影响面统计(把结果抄到工单/记录里,作为"删了多少"的证据) |
|||
-- 这些关注/访问记录会被一并删除,必须留档以便回答用户疑问。 |
|||
-- --------------------------------------------------------------------------- |
|||
SELECT 'topics' AS affected_table, COUNT(*) AS affected_rows |
|||
FROM topic WHERE created_by = 'SYSTEM' AND description IS NULL AND deleted = 0 |
|||
UNION ALL |
|||
SELECT 'topic_follow', COUNT(*) |
|||
FROM topic_follow f JOIN topic t ON t.name = f.topic_name |
|||
WHERE t.created_by = 'SYSTEM' AND t.description IS NULL AND t.deleted = 0 |
|||
UNION ALL |
|||
SELECT 'topic_visit', COUNT(*) |
|||
FROM topic_visit v JOIN topic t ON t.name = v.topic_name |
|||
WHERE t.created_by = 'SYSTEM' AND t.description IS NULL AND t.deleted = 0 |
|||
UNION ALL |
|||
SELECT 'topic_hot_snapshot', COUNT(*) |
|||
FROM topic_hot_snapshot s JOIN topic t ON t.name = s.topic_name |
|||
WHERE t.created_by = 'SYSTEM' AND t.description IS NULL AND t.deleted = 0 |
|||
UNION ALL |
|||
SELECT 'topic_alias', COUNT(*) |
|||
FROM topic_alias a JOIN topic t ON t.id = a.topic_id |
|||
WHERE t.created_by = 'SYSTEM' AND t.description IS NULL AND t.deleted = 0; |
|||
|
|||
-- --------------------------------------------------------------------------- |
|||
-- 第三步:清理关联数据(可包事务) |
|||
-- 关联方式:topic_follow / topic_visit / topic_hot_snapshot 按 name, |
|||
-- topic_alias 按 topic_id。 |
|||
-- --------------------------------------------------------------------------- |
|||
START TRANSACTION; |
|||
|
|||
DELETE f FROM topic_follow f |
|||
JOIN topic t ON t.name = f.topic_name |
|||
WHERE t.created_by = 'SYSTEM' AND t.description IS NULL AND t.deleted = 0; |
|||
|
|||
DELETE v FROM topic_visit v |
|||
JOIN topic t ON t.name = v.topic_name |
|||
WHERE t.created_by = 'SYSTEM' AND t.description IS NULL AND t.deleted = 0; |
|||
|
|||
DELETE s FROM topic_hot_snapshot s |
|||
JOIN topic t ON t.name = s.topic_name |
|||
WHERE t.created_by = 'SYSTEM' AND t.description IS NULL AND t.deleted = 0; |
|||
|
|||
DELETE a FROM topic_alias a |
|||
JOIN topic t ON t.id = a.topic_id |
|||
WHERE t.created_by = 'SYSTEM' AND t.description IS NULL AND t.deleted = 0; |
|||
|
|||
-- 话题本体(物理删除,释放 uk_topic_name) |
|||
DELETE FROM topic |
|||
WHERE created_by = 'SYSTEM' AND description IS NULL AND deleted = 0; |
|||
|
|||
COMMIT; |
|||
|
|||
-- --------------------------------------------------------------------------- |
|||
-- 第四步:校验(三条都必须为 0) |
|||
-- --------------------------------------------------------------------------- |
|||
SELECT COUNT(*) AS must_be_zero FROM topic |
|||
WHERE created_by = 'SYSTEM' AND description IS NULL; |
|||
|
|||
SELECT COUNT(*) AS orphan_follows FROM topic_follow f |
|||
WHERE NOT EXISTS (SELECT 1 FROM topic t WHERE t.name = f.topic_name); |
|||
|
|||
SELECT COUNT(*) AS orphan_visits FROM topic_visit v |
|||
WHERE NOT EXISTS (SELECT 1 FROM topic t WHERE t.name = v.topic_name); |
|||
|
|||
-- --------------------------------------------------------------------------- |
|||
-- 第五步(可选):清理后立刻创建 10~20 个正式话题 |
|||
-- 推荐用管理员账号调 POST /api/topics(管理员创建直通 APPROVED), |
|||
-- 不要用本脚本插数据,避免绕过校验、又产生新的空壳。 |
|||
-- --------------------------------------------------------------------------- |
|||
@ -0,0 +1,59 @@ |
|||
-- ============================================================================ |
|||
-- V9:话题热度口径切换(话题域指标),清理按旧口径产生的快照 |
|||
-- |
|||
-- 背景:话题热度原先由动态互动(点赞/评论/分享/浏览)聚合而来,解耦后 |
|||
-- 改为纯话题域指标:关注数、访问人数、近 7 天关注/访问增长、推荐/置顶加权。 |
|||
-- 若保留旧快照,新热度与旧快照算 delta 会得到巨大偏差(rising 榜失真)。 |
|||
-- |
|||
-- ⚠️ Flyway 当前 enabled: false,本脚本不会自动执行,必须人工在目标库执行。 |
|||
-- ⚠️ 执行顺序:先 V8(清空壳话题),再 V9(清快照)。 |
|||
-- ⚠️ 本脚本包含 DDL(CREATE TABLE ... AS SELECT),会隐式提交,无法整体回滚。 |
|||
-- ⚠️ 部分客户端默认「批处理模式:遇错继续执行」。这样即使备份失败,后面的 DELETE |
|||
-- 仍然会跑。执行前请确认备份语句真的成功(本脚本已改为先备份、再校验备份行数, |
|||
-- 校验不通过请立刻停止,不要执行 DELETE)。 |
|||
-- |
|||
-- 修订记录:2026-10-20 备份表由 topic_hot_snapshot_bak_20261020 改名为 |
|||
-- topic_hot_snapshot_bak_v9_20261020。原名字与 V8 撞名,导致 |
|||
-- 「Table already exists」而备份失败(V8 已建过同名表)。 |
|||
-- ============================================================================ |
|||
|
|||
-- --------------------------------------------------------------------------- |
|||
-- 第一步:备份(单独执行,必须确认成功) |
|||
-- --------------------------------------------------------------------------- |
|||
CREATE TABLE topic_hot_snapshot_bak_v9_20261020 AS SELECT * FROM topic_hot_snapshot; |
|||
|
|||
-- --------------------------------------------------------------------------- |
|||
-- 第二步:校验备份(行数必须与备份时一致;为 0 也正常,说明当时快照表是空的) |
|||
-- 如果这条报错或行数对不上,请立刻停止,不要执行第三步。 |
|||
-- --------------------------------------------------------------------------- |
|||
SELECT COUNT(*) AS backup_rows FROM topic_hot_snapshot_bak_v9_20261020; |
|||
|
|||
-- --------------------------------------------------------------------------- |
|||
-- 第三步:清空旧口径快照(旧数据按"动态数/动态互动"计算,不能与新口径混算) |
|||
-- 说明:快照是每小时重建的派生数据,清空不影响任何业务数据。 |
|||
-- --------------------------------------------------------------------------- |
|||
DELETE FROM topic_hot_snapshot; |
|||
|
|||
-- --------------------------------------------------------------------------- |
|||
-- 第四步:快照表新增话题域指标列 visit_count |
|||
-- 说明:应用启动时 Hibernate(ddl-auto=update)通常会自动补列; |
|||
-- 若不想依赖自动建表,请手工执行下面这行(已幂等化检查)。 |
|||
-- |
|||
-- 注意:moment_count 列保留但已废弃,代码固定写 0。 |
|||
-- 不 DROP 列的原因:该列 NOT NULL,DROP 属有风险 DDL,而本环境 |
|||
-- Flyway 关闭、迁移需人工执行,保留一列"永远是 0"更安全。 |
|||
-- --------------------------------------------------------------------------- |
|||
-- ALTER TABLE topic_hot_snapshot ADD COLUMN visit_count BIGINT NOT NULL DEFAULT 0; |
|||
|
|||
-- --------------------------------------------------------------------------- |
|||
-- 第五步:校验 |
|||
-- --------------------------------------------------------------------------- |
|||
SELECT COUNT(*) AS snapshots_after_cleanup FROM topic_hot_snapshot; -- 应为 0 |
|||
SHOW COLUMNS FROM topic_hot_snapshot LIKE 'visit_count'; -- 应存在 |
|||
SELECT COUNT(*) AS moment_count_column_kept FROM information_schema.columns |
|||
WHERE table_schema = DATABASE() AND table_name = 'topic_hot_snapshot' |
|||
AND column_name = 'moment_count'; -- 应为 1 |
|||
|
|||
-- 回滚(如需): |
|||
-- INSERT INTO topic_hot_snapshot SELECT * FROM topic_hot_snapshot_bak_v9_20261020; |
|||
-- 若当时 V8 也备份过(topic_hot_snapshot_bak_20261020),那两个备份都可作为恢复源。 |
|||
@ -0,0 +1,274 @@ |
|||
# 青核后端本地部署说明 |
|||
|
|||
本说明适用于下载源码后,在独立开发环境部署青核后端。不包含真实配置、真实账号或业务数据。源码与现有文档目录位置保持不变。 |
|||
|
|||
> 验证边界:本次提供模板、启动器、Compose 与部署流程;未完成 Docker 实机启动及全新 MySQL 空库业务验收。首次部署成功后仍需执行本文验收清单,不能把材料齐备等同于所有功能可用。 |
|||
|
|||
## 1. 文件与用途 |
|||
|
|||
| 文件 | 用途 | 后续是否适合上传 | |
|||
| --- | --- | --- | |
|||
| `README.md` | 项目入口 | 是 | |
|||
| `campus-server/.env.example` | 环境变量占位示例 | 是 | |
|||
| `campus-server/src/main/resources/application.example.yml` | 脱敏 Spring 配置模板 | 是 | |
|||
| `campus-server/Start-Local.ps1` | 在独立进程读取本地环境文件、构建与启动 | 是 | |
|||
| `campus-server/Test-LocalDeployment.ps1` | 离线验证启动器与部署 JAR 的配置排除 | 是 | |
|||
| `campus-server/compose.local.yml` | 独立 MySQL、Redis、RabbitMQ 与可选后端容器 | 是 | |
|||
| `campus-server/Dockerfile`、`.dockerignore` | 后端镜像构建与构建上下文控制 | 是 | |
|||
| `campus-server/pom.xml` 中 `portable` profile | 打包时排除真实应用配置,产物写入独立目录 | 是 | |
|||
| 本文及必要迁移脚本 | 初始化、升级和验收指导 | 审核后上传 | |
|||
| 本地 `.env`、现有两份真实 `application.yml` | 部署者的真实连接与凭据 | 否 | |
|||
|
|||
本文是随源码发布的部署材料,原目录位置不变。其他内部文档继续按原排除规则处理,不因发布本文而全部公开;真实应用配置与 `.env` 由部署者在本地准备。 |
|||
|
|||
## 2. 环境要求与限制 |
|||
|
|||
- JDK 17 或 21、Maven;项目编译目标仍为 Java 8,Spring Boot 2.7.18,业务代码使用 `javax.*`。 |
|||
- Windows PowerShell 5.1 或 PowerShell 7;容器部署另需已运行的 Docker Desktop Linux engine 和 Docker Compose。 |
|||
- 首次构建能访问 Maven 仓库;容器构建能访问镜像仓库与 Ubuntu 软件源。 |
|||
- 模板选择 MySQL 8.4、Redis 7.4、RabbitMQ 4.1 作为独立开发环境组合,尚未完成该组合的运行时兼容性验收;不代表现有共享环境的版本。 |
|||
- 默认基础设施端口仅绑定 `127.0.0.1`,避开常见端口;不使用现有云数据库。 |
|||
|
|||
| 服务 | 宿主机端口 | 容器端口 | |
|||
| --- | --- | --- | |
|||
| MySQL | 13306 | 3306 | |
|||
| Redis | 16379 | 6379 | |
|||
| RabbitMQ AMQP | 15672 | 5672 | |
|||
| RabbitMQ 管理页面 | 15673 | 15672 | |
|||
| 后端 | 8009 | 8009 | |
|||
|
|||
端口均可在本地 `.env` 修改。这里宿主机的 **15672 是 AMQP 端口**,管理页面是 `http://127.0.0.1:15673`。 |
|||
|
|||
## 3. 准备本地配置 |
|||
|
|||
以下命令从仓库根目录进入模块后执行: |
|||
|
|||
```powershell |
|||
cd .\campus-server |
|||
java -version |
|||
mvn -version |
|||
docker compose version |
|||
``` |
|||
|
|||
仅在 `.env` 尚不存在时复制模板: |
|||
|
|||
```powershell |
|||
if (-not (Test-Path -LiteralPath '.env')) { |
|||
Copy-Item -LiteralPath '.env.example' -Destination '.env' |
|||
} |
|||
``` |
|||
|
|||
编辑 `.env`,至少替换 `DB_PASSWORD`、`MYSQL_ROOT_PASSWORD`、`REDIS_PASSWORD`、`RABBITMQ_PASSWORD` 和 `JWT_SECRET` 的 `replace-me-...` 示例值。JWT secret 至少 32 字节,使用独立随机值。密码建议使用随机字母、数字组合;当前启动器要求未加引号的 `KEY=value`,不支持变量引用、行尾注释或值中的 `$`。 |
|||
|
|||
`DB_NAME=qinghe_local`、`DB_USERNAME=qinghe` 是本地示例名称;不得改为已有生产或共享业务数据库。数据库名及用户名建议只用字母、数字和下划线。环境变量来自本地 `.env`,不要填写到可上传模板中。 |
|||
|
|||
可先只检查本地文件格式,不连接服务: |
|||
|
|||
```powershell |
|||
powershell -NoProfile -ExecutionPolicy Bypass -File .\Start-Local.ps1 -CheckOnly |
|||
``` |
|||
|
|||
启动器按脚本目录解析 `.env`,不执行其内容,不打印配置值。它只接受 `.env.example` 列出的变量,运行结束后恢复原进程的同名环境变量。启动前显式指定 `application.example.yml`,避免默认加载你现有的两份真实配置。 |
|||
|
|||
`--spring.config.location` 会替换默认配置查找位置,但命令行、操作系统环境变量及 `SPRING_APPLICATION_JSON` 仍可覆盖属性。请在没有额外 `SPRING_*` 覆盖项的独立终端执行,不在现有生产运行环境复用本地模板。 |
|||
|
|||
## 4. 启动基础设施 |
|||
|
|||
所有命令在 `campus-server` 目录执行。先启动 Docker Desktop,再运行: |
|||
|
|||
```powershell |
|||
docker compose --env-file .env -f compose.local.yml config --quiet |
|||
docker compose --env-file .env -f compose.local.yml up -d mysql redis rabbitmq |
|||
docker compose --env-file .env -f compose.local.yml ps |
|||
``` |
|||
|
|||
等待三个服务全部 `healthy` 再启动后端。首次拉取镜像和初始化数据库需要时间。遇到错误时按服务查看日志,不把含敏感信息的完整配置输出粘贴到公共位置。 |
|||
|
|||
Compose 项目名默认为 `.env` 中的 `qinghe-local`,数据保存到该项目的命名卷。不要复用现有业务数据卷。已有卷的密码不会随 `.env` 修改而自动更新,MySQL/RabbitMQ 初始化变量主要用于首次创建数据目录;修改密码需在服务内同步处理。 |
|||
|
|||
宿主机终端中的同名环境变量优先于 Compose 的 `--env-file`,应使用干净终端,避免覆盖本地配置。Java 不会自动读取 `.env`,请使用下节启动器。 |
|||
|
|||
## 5. 构建并启动后端 |
|||
|
|||
### 5.1 推荐:基础设施容器 + 宿主机后端 |
|||
|
|||
在 `campus-server` 目录执行: |
|||
|
|||
```powershell |
|||
powershell -NoProfile -ExecutionPolicy Bypass -File .\Start-Local.ps1 |
|||
``` |
|||
|
|||
脚本先执行 `mvn -Pportable clean verify`,再使用 `target/portable/campus-server-1.0.0.jar` 与脱敏模板启动。构建或测试失败时不继续启动。保持窗口打开,使用 Ctrl+C 停止。 |
|||
|
|||
已完成构建且确认产物是当前源码时,可使用: |
|||
|
|||
```powershell |
|||
powershell -NoProfile -ExecutionPolicy Bypass -File .\Start-Local.ps1 -SkipBuild |
|||
``` |
|||
|
|||
`portable` 只在显式启用时生效,默认开发打包方式不变;它排除所有 `application*.yml/yaml/properties` 及 `.env*` 资源,不把本机真实配置带入部署 JAR。该配置只检查约定文件名,不代替对源码、其他资源及历史提交的凭据检查。不要分发普通开发方式打出的 JAR,它可能包含本机真实配置。 |
|||
|
|||
### 5.2 可选:后端也使用容器 |
|||
|
|||
先在宿主机构建部署专用 JAR: |
|||
|
|||
```powershell |
|||
mvn -Pportable clean verify |
|||
docker compose --env-file .env -f compose.local.yml --profile app up -d --build |
|||
docker compose --env-file .env -f compose.local.yml --profile app logs -f backend |
|||
``` |
|||
|
|||
不要同时启动宿主机后端和后端容器占用同一端口。后端容器使用 `mysql/redis/rabbitmq` 服务名及容器内部端口,Compose 会覆盖 `.env` 中这几项宿主机连接值。 |
|||
|
|||
Dockerfile 从软件源安装 FFmpeg 和基础字体,不依赖本机的 `ffmpeg-release-amd64-static.tar.xz`。镜像以非 root 用户运行,PDF 中文渲染如需额外字体应单独补齐并验收。构建上下文只允许部署 JAR 与脱敏模板,不发送真实应用配置或本地 `.env`。 |
|||
|
|||
## 6. 首次空库初始化与旧库升级 |
|||
|
|||
### 6.1 全新独立开发库 |
|||
|
|||
1. Compose 首次启动创建 `DB_NAME` 指定的空数据库与普通业务账号。 |
|||
2. 保持 `DB_DDL_AUTO=update`、Flyway 关闭,启动当前源码。Hibernate 按当前实体创建表,启动初始化器补入其支持的字典、礼物等预置数据;不包含真实用户、学校全量资料或历史业务数据。 |
|||
3. 检查启动日志中的 DDL/SQL 错误。Hibernate 可能记录 DDL 错误后继续启动,不能只看到 `Started CampusApplication` 就判定结构完整。 |
|||
4. 执行下方只读结构检查,并验收相关业务。 |
|||
5. 若缺少唯一约束或外键,停止对应业务验证,由维护者根据结构差异补齐后再次校验。 |
|||
|
|||
**不要在 Hibernate 已按当前实体建表后,再把 V1–V18 全部从头执行。** 现有迁移主要面向历史版本升级,部分 `ADD COLUMN` 与数据回填并不幂等;例如 V18 引用旧版 `raw_lines`,全新实体建出的表不一定存在旧字段。 |
|||
|
|||
V1 仅是 `SELECT 1` 基线标记,并非完整建库 SQL。当前没有经过验收的“单份 SQL 完整初始化全部表”基线,本次也未编造该基线。上述是独立开发库的建表与检查流程,不是生产部署验收结论。 |
|||
|
|||
可通过数据库客户端连接 `127.0.0.1:13306`,或在模块目录交互执行: |
|||
|
|||
```powershell |
|||
docker compose --env-file .env -f compose.local.yml exec mysql mysql -u qinghe -p qinghe_local |
|||
``` |
|||
|
|||
命令中的账号和数据库名需与 `.env` 一致,密码在交互提示中输入。 |
|||
|
|||
只读校验 SQL: |
|||
|
|||
```sql |
|||
SELECT DATABASE(), VERSION(); |
|||
SHOW TABLES; |
|||
SHOW CREATE TABLE `user`; |
|||
SHOW CREATE TABLE activity_participant; |
|||
SHOW CREATE TABLE group_member; |
|||
SHOW CREATE TABLE group_announcement; |
|||
SHOW CREATE TABLE group_invitation; |
|||
SHOW CREATE TABLE xuexin_report; |
|||
|
|||
SELECT table_name, index_name, |
|||
GROUP_CONCAT(column_name ORDER BY seq_in_index) AS columns_in_order |
|||
FROM information_schema.statistics |
|||
WHERE table_schema = DATABASE() AND non_unique = 0 |
|||
AND table_name IN ('user', 'activity_participant', 'group_member', |
|||
'group_announcement', 'group_invitation', 'xuexin_report') |
|||
GROUP BY table_name, index_name; |
|||
|
|||
SELECT table_name, constraint_name, referenced_table_name, delete_rule |
|||
FROM information_schema.referential_constraints |
|||
WHERE constraint_schema = DATABASE() |
|||
AND table_name IN ('group_announcement', 'group_invitation'); |
|||
``` |
|||
|
|||
重点确认手机号唯一、活动参与 `(activity_id,user_id)`、群成员 `(group_id,user_id)`、每群一条公告、邀请 `(group_id,invitee_id,pending_key)` 和认证 `active_marker` 唯一约束。公告与邀请到 `chat_group` 的外键应为 `ON DELETE CASCADE`;实体使用字符串 ID 时 Hibernate 不会自动替你建出这些关联外键。 |
|||
|
|||
如空库由 Hibernate 建表后外键缺失,先用 `SHOW CREATE TABLE` 比较关联列的类型、字符集和排序规则。**仅在新建的独立测试库、确认兼容且外键不存在时**,维护者可人工执行下列补充 DDL: |
|||
|
|||
```sql |
|||
ALTER TABLE group_announcement |
|||
ADD CONSTRAINT fk_ga_group |
|||
FOREIGN KEY (group_id) REFERENCES chat_group(id) ON DELETE CASCADE; |
|||
|
|||
ALTER TABLE group_invitation |
|||
ADD CONSTRAINT fk_gi_group |
|||
FOREIGN KEY (group_id) REFERENCES chat_group(id) ON DELETE CASCADE; |
|||
``` |
|||
|
|||
以上 DDL 不是幂等脚本,不允许对旧库或已有同类外键的库直接重复执行;本次未执行。独立库验收通过后可将 `DB_DDL_AUTO` 改为 `validate` 再重启检查,但 Hibernate validate 同样不能替代唯一索引和外键检查。 |
|||
|
|||
### 6.2 已有数据的数据库升级 |
|||
|
|||
先备份,在隔离副本核对已执行版本和表结构,阅读各脚本及功能对接说明,再人工执行确实缺少的迁移。不得套用空库建表步骤,也不要把 `update` 当成生产升级方案。 |
|||
|
|||
本次源码发布包含 V1–V18 的 18 份迁移。SQL 仍保留通用忽略规则,新增迁移须按文件精确审核后纳入版本控制;不能用数据库快照代替脱敏迁移,也不要修改已经执行的历史迁移。包含历史数据清理的 V3、V8、V9、V12 仅适用于对应升级场景,不应自动执行于新库或未经审核的业务库。 |
|||
|
|||
**是否需要人工迁移:** 新建隔离库需人工结构核验、按缺项补齐约束;旧库需按版本差异人工执行必要 SQL。Flyway 当前不会自动执行。 |
|||
|
|||
## 7. 云服务功能边界 |
|||
|
|||
| 功能 | 未配置真实云服务时的边界 | |
|||
| --- | --- | |
|||
| 数据库 / Redis / RabbitMQ | 必须连接本地独立实例,不能依赖云服务占位值 | |
|||
| OSS 文件上传 | 模板保留 OSS Bean,使用本地占位 endpoint;未提供可用存储服务,上传、材料存证等不可用 | |
|||
| 短信验证码 | 必填配置键已提供占位;发送短信不可用,万能验证码默认关闭 | |
|||
| 推送 | 模板默认关闭 | |
|||
| 环信 IM | 留空时初始化器会走现有降级;不能据此宣称群聊、消息同步全功能可用 | |
|||
| OCR / 学信网认证 | 需要真实 OCR 与 OSS,模板中的 OCR enabled 不是可靠的统一禁用保障;未配置时不要调用 | |
|||
| VOD / 视频 | 需要真实 VOD 权限、回调与可访问域名;本地 JAR 能启动不代表视频可用 | |
|||
|
|||
OSS 不能简单配置 `enabled=false`:现有 `OssUtil` 强制注入 `OSS` Bean,关闭后可能因依赖缺失启动失败。本次不修改业务依赖关系或引入模拟实现。 |
|||
|
|||
补充云凭据时只编辑私有 `.env`,核对地域、应用、bucket、回调域名和媒体域名白名单。不要把部署者自己的密钥写回模板。VOD 回调沿用当前协议,不新增虚构签名配置。 |
|||
|
|||
## 8. 验收、排错与停止 |
|||
|
|||
启动后在另一个终端检查: |
|||
|
|||
```powershell |
|||
Invoke-RestMethod 'http://127.0.0.1:8009/actuator/health' |
|||
``` |
|||
|
|||
预期 `status=UP`。Swagger 为 `http://127.0.0.1:8009/swagger-ui.html`,OpenAPI 为 `http://127.0.0.1:8009/v3/api-docs`,业务前缀 `/api`,认证头 `access_token`。端口变化时同步调整。 |
|||
|
|||
验收至少记录: |
|||
|
|||
- 构建及全量测试结果;运行版本和时间。 |
|||
- MySQL、Redis、RabbitMQ 健康状态及实际连接目标。 |
|||
- 表、唯一约束、外键及基础种子数据检查结果。 |
|||
- 一个匿名接口和一个经合法登录认证的业务接口;未接入短信时不声称登录全流程通过。 |
|||
- 每项云能力单独记录“未接入 / 验证通过 / 失败”,不以 UP 代替业务验收。 |
|||
|
|||
| 问题 | 检查方向 | |
|||
| --- | --- | |
|||
| Docker named pipe / daemon 连接失败 | Docker Desktop Linux engine 是否运行 | |
|||
| 连接拒绝、认证失败 | 服务是否 healthy、端口映射、密码和数据卷首次初始化状态 | |
|||
| 配置占位符无法解析 | 是否由启动器或 Compose 注入 `.env`,配置名是否拼写一致 | |
|||
| 缺少 OSS Bean | 是否误设 `aliyun.oss.enabled=false` | |
|||
| SQL 字段或约束错误 | 区分新库与旧库,检查实际结构;不要忽略所有 SQL 报错 | |
|||
| OPTIONS 401 | 当前有已知跨域预检缺口;本地服务启动不等于浏览器跨域可用,不通过关闭鉴权绕过 | |
|||
|
|||
停止本地容器并保留数据卷: |
|||
|
|||
```powershell |
|||
docker compose --env-file .env -f compose.local.yml --profile app down |
|||
``` |
|||
|
|||
不使用 `down -v` 作为常规停止方式,它会删除本地卷。本文不提供删除数据的快捷命令。 |
|||
|
|||
## 9. 发布范围与验证记录 |
|||
|
|||
可上传材料:模板、启动器及离线检查脚本、Compose、Dockerfile、`.dockerignore`、Maven 配置、README、本文及经复核的必要迁移。真实 `.env`、两份真实 `application.yml`、JAR、日志、数据库快照继续留本地。 |
|||
|
|||
创建新材料不自动取消内部资料的排除规则;发布时核对目标仓库、分支和最终文件清单,并检查 README 所需链接是否随文档一起发布。本次仅公开本文,其他内部方案、工作记录与开发规范不随源码发布。 |
|||
|
|||
2026-10-21 本次本地验证结果: |
|||
|
|||
- `mvn -Pportable clean verify`:BUILD SUCCESS;70 份报告,496 用例,0 失败 / 0 错误 / 0 跳过。 |
|||
- `Test-LocalDeployment.ps1`:Windows PowerShell 5.1 与 PowerShell 7 各 11 项离线检查通过,涵盖输入校验、独立配置参数、环境恢复及 JAR 排除私有配置。 |
|||
- Compose 结构与变量插值检查通过(未解析实际私有 `.env`),两份 YAML 语法解析通过。 |
|||
- 现有两份真实 `application.yml` 的 SHA-256 与改动前相同;没有创建或覆盖用户的 `.env`。 |
|||
- Docker Desktop 引擎未运行;未启动容器、未构建镜像、未连接共享数据库或云服务,空库和全业务验收仍待执行。 |
|||
|
|||
重复执行离线检查(模块目录): |
|||
|
|||
```powershell |
|||
powershell -NoProfile -ExecutionPolicy Bypass -File .\Test-LocalDeployment.ps1 |
|||
``` |
|||
|
|||
该脚本仅生成并清理自己的临时测试文件,使用 Java 替身验证启动参数,不启动后端;未生成 portable JAR 时,会跳过对应检查。 |
|||
|
|||
## 10. 官方参考 |
|||
|
|||
- [Spring Boot 2.7.18 外部配置](https://docs.spring.io/spring-boot/docs/2.7.18/reference/html/features.html#features.external-config.files):显式配置位置与默认查找规则。 |
|||
- [Docker Compose 环境变量插值](https://docs.docker.com/compose/how-tos/environment-variables/variable-interpolation/):`--env-file`、必填变量及宿主机环境优先级。 |
|||
- [Docker Compose 启动顺序](https://docs.docker.com/compose/how-tos/startup-order/):`service_healthy` 等待基础设施健康后再启动后端。 |
|||
Loading…
Reference in new issue