文档对齐 React 技术栈,并支持渐进式数据库迁移
将 README 与部署文档从仅 init 改为 001 基线 + 增量迁移约定,并补充 create/status 脚手架与序号校验。
This commit is contained in:
@@ -2,14 +2,16 @@
|
|||||||
|
|
||||||
快手自动发货与履约管理系统。
|
快手自动发货与履约管理系统。
|
||||||
|
|
||||||
项目采用前后端分离架构,用于接收订单、匹配快手履约规则、管理云卡资源,并提供后台页面处理平台配置、任务追踪和人工复核。
|
前后端分离架构:接收外部订单、按规则匹配发货平台、生成统一领取链接,并提供后台做平台配置、任务追踪和人工复核。
|
||||||
|
|
||||||
## 技术栈
|
## 技术栈
|
||||||
|
|
||||||
- 前端:Vue 3、Vite、Element Plus、TypeScript
|
| 层 | 技术 |
|
||||||
- 后端:Node.js、Express、PostgreSQL
|
| --- | --- |
|
||||||
- 网关:Caddy
|
| 前端 | React 19、Vite 8、Ant Design 6、React Router 7、TanStack Query、TypeScript |
|
||||||
- 本地与生产运行:Docker Compose
|
| 后端 | Node.js、Express 5、TypeScript、PostgreSQL、node-pg-migrate |
|
||||||
|
| 网关 | Caddy |
|
||||||
|
| 运行 | Docker Compose(开发 / 生产分离) |
|
||||||
|
|
||||||
## 目录结构
|
## 目录结构
|
||||||
|
|
||||||
@@ -17,80 +19,134 @@
|
|||||||
apps/
|
apps/
|
||||||
backend/ 后端 API、任务编排、平台对接、数据库迁移
|
backend/ 后端 API、任务编排、平台对接、数据库迁移
|
||||||
src/
|
src/
|
||||||
data/ 本地开发运行数据和平台配置
|
db/migrations/ SQL 迁移(001 基线 + 后续渐进增量)
|
||||||
frontend/ 前端后台、领取页和配置页面
|
data/ 本地运行数据与平台配置(敏感,默认不提交)
|
||||||
|
frontend/ React 管理后台 + 用户领取页
|
||||||
deploy/
|
deploy/
|
||||||
caddy/ Caddy 反向代理配置
|
caddy/ Caddy 反向代理
|
||||||
docker/ 前后端镜像 Dockerfile
|
docker/ 前后端 Dockerfile
|
||||||
docs/ 平台接口、迁移设计和部署资料
|
mac-dev.sh / ubuntu-deploy.sh
|
||||||
|
docs/ 平台接口、履约链路、部署说明
|
||||||
docker-compose.dev.yml 本地 Docker 开发环境
|
docker-compose.dev.yml 本地 Docker 开发环境
|
||||||
docker-compose.yml 生产/服务器 Compose
|
docker-compose.yml 生产 / 服务器 Compose
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 业务边界(简图)
|
||||||
|
|
||||||
|
```text
|
||||||
|
91 卡券进单
|
||||||
|
→ 本系统建单 / 匹配商品 / 创建履约任务
|
||||||
|
→ 返回统一 claimUrl
|
||||||
|
用户打开领取页
|
||||||
|
→ 可选:快手行业电子凭证自动核销
|
||||||
|
→ kuaishou-cloud / kuaishou-feifei / 人工 完成发货
|
||||||
|
后台
|
||||||
|
→ 订单任务、平台配置、审计、人工复核
|
||||||
|
```
|
||||||
|
|
||||||
|
更完整的链路说明见:`docs/履约配置/多发货平台与电子凭证整体链路.md`。
|
||||||
|
|
||||||
## 本地开发
|
## 本地开发
|
||||||
|
|
||||||
推荐直接使用 Docker,本地不需要额外维护 Node 和 PostgreSQL。
|
推荐直接用 Docker,本机不必单独装 Node / PostgreSQL。
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp .env.mac-docker.example .env
|
cp .env.mac-docker.example .env
|
||||||
docker compose -f docker-compose.dev.yml up -d --build
|
docker compose -f docker-compose.dev.yml up -d --build
|
||||||
```
|
```
|
||||||
|
|
||||||
启动后常用入口:
|
常用入口:
|
||||||
|
|
||||||
- 前端、后台、领取页:`http://localhost`
|
| 入口 | 地址 |
|
||||||
- 后端健康检查:`http://localhost/health`
|
| --- | --- |
|
||||||
- 后端 API 前缀:`http://localhost/api/v1/...`
|
| 前端 / 后台 / 领取页 | `http://localhost` |
|
||||||
|
| 后端健康检查 | `http://localhost/health` |
|
||||||
|
| 后端就绪检查 | `http://localhost/health/ready` |
|
||||||
|
| 后端 API 前缀 | `http://localhost/api/v1/...` |
|
||||||
|
|
||||||
查看容器状态:
|
常用命令:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose -f docker-compose.dev.yml ps
|
docker compose -f docker-compose.dev.yml ps
|
||||||
```
|
|
||||||
|
|
||||||
查看日志:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose -f docker-compose.dev.yml logs -f backend
|
docker compose -f docker-compose.dev.yml logs -f backend
|
||||||
docker compose -f docker-compose.dev.yml logs -f frontend
|
docker compose -f docker-compose.dev.yml logs -f frontend
|
||||||
|
docker compose -f docker-compose.dev.yml down
|
||||||
```
|
```
|
||||||
|
|
||||||
停止环境:
|
Mac 也可使用:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose -f docker-compose.dev.yml down
|
bash deploy/mac-dev.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
## 开发验证
|
## 开发验证
|
||||||
|
|
||||||
本项目以容器内验证为准。
|
以容器内执行为准:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose -f docker-compose.dev.yml exec -T backend npm test
|
docker compose -f docker-compose.dev.yml exec -T backend npm test
|
||||||
docker compose -f docker-compose.dev.yml exec -T backend npm run typecheck
|
docker compose -f docker-compose.dev.yml exec -T backend npm run typecheck
|
||||||
docker compose -f docker-compose.dev.yml exec -T backend npm run build
|
docker compose -f docker-compose.dev.yml exec -T backend npm run build
|
||||||
docker compose -f docker-compose.dev.yml exec -T frontend npm run typecheck
|
docker compose -f docker-compose.dev.yml exec -T frontend npm run typecheck
|
||||||
docker compose -f docker-compose.dev.yml exec -T frontend npm run lint
|
|
||||||
docker compose -f docker-compose.dev.yml exec -T frontend npm run build
|
docker compose -f docker-compose.dev.yml exec -T frontend npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
后端数据库迁移通常会在启动时自动执行;需要手动执行时:
|
## 数据库迁移
|
||||||
|
|
||||||
|
采用 **基线 + 渐进增量** 策略,由 `node-pg-migrate` 管理,记录表为 `schema_migrations`。
|
||||||
|
|
||||||
|
| 文件 | 含义 |
|
||||||
|
| --- | --- |
|
||||||
|
| `001_init.sql` | 空库基线,建立完整业务结构 |
|
||||||
|
| `002_xxx.sql` 起 | 后续结构变更的增量 SQL |
|
||||||
|
|
||||||
|
约定:
|
||||||
|
|
||||||
|
1. **不要修改**已经应用到共享/生产环境的历史迁移文件。
|
||||||
|
2. 结构变更一律新增迁移文件,不要回写 `001_init.sql`。
|
||||||
|
3. 空库按序号依次执行 `001 → 002 → …`;已有库只执行尚未记录的增量。
|
||||||
|
4. 后端启动时自动 `up`;也可手动执行。
|
||||||
|
|
||||||
|
常用命令(容器内):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# 查看本地文件与数据库对齐状态
|
||||||
|
docker compose -f docker-compose.dev.yml exec -T backend npm run db:migrate:status
|
||||||
|
|
||||||
|
# 手动执行未应用迁移
|
||||||
docker compose -f docker-compose.dev.yml exec -T backend npm run db:migrate
|
docker compose -f docker-compose.dev.yml exec -T backend npm run db:migrate
|
||||||
|
|
||||||
|
# 创建下一份增量迁移(示例)
|
||||||
|
docker compose -f docker-compose.dev.yml exec -T backend npm run db:migrate:create -- add_task_priority
|
||||||
```
|
```
|
||||||
|
|
||||||
当前数据库脚本只保留新库初始化结构,不维护历史测试库的升级兼容;切换到这版前请重建开发库或重置 Docker volume。
|
宿主机直连开发库(注意主机名):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd apps/backend
|
||||||
|
DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5432/order_site npm run db:migrate:status
|
||||||
|
DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5432/order_site npm run db:migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
本地需要清空数据重建库时(会丢开发数据):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.dev.yml down
|
||||||
|
docker volume rm order_site_postgres_dev_data
|
||||||
|
docker compose -f docker-compose.dev.yml up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
更多说明见 `docs/部署启动与数据库重建.md`。
|
||||||
|
|
||||||
## 环境变量
|
## 环境变量
|
||||||
|
|
||||||
本地开发从根目录 `.env` 读取,模板为:
|
本地:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp .env.mac-docker.example .env
|
cp .env.mac-docker.example .env
|
||||||
```
|
```
|
||||||
|
|
||||||
生产部署从 `.env.server.example` 复制:
|
生产:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp .env.server.example .env
|
cp .env.server.example .env
|
||||||
@@ -98,87 +154,78 @@ cp .env.server.example .env
|
|||||||
|
|
||||||
常用变量:
|
常用变量:
|
||||||
|
|
||||||
- `APP_DOMAIN` / `CADDY_SITE_ADDR`:Caddy 对外域名与监听地址
|
| 变量 | 说明 |
|
||||||
- `CLAIM_BASE_URL`:领取页完整地址
|
| --- | --- |
|
||||||
- `DATABASE_URL`:PostgreSQL 连接串
|
| `APP_DOMAIN` / `CADDY_SITE_ADDR` | 对外域名与 Caddy 监听地址 |
|
||||||
- `ADMIN_SESSION_SECRET`:后台登录态签名密钥
|
| `CLAIM_BASE_URL` | 领取页完整前缀,如 `https://域名/#/claim` |
|
||||||
- `ADMIN_DEFAULT_USERS_JSON`:默认后台用户
|
| `DATABASE_URL` | PostgreSQL 连接串(容器内主机名用 `postgres`) |
|
||||||
- `KAQUAN91_USER_ID` / `KAQUAN91_SECRET`:91 卡券开放接口凭据
|
| `ADMIN_SESSION_SECRET` | 后台登录态签名密钥 |
|
||||||
|
| `ADMIN_DEFAULT_USERS_JSON` | 默认后台用户 |
|
||||||
|
| `KAQUAN91_USER_ID` / `KAQUAN91_SECRET` | 91 卡券开放接口凭据 |
|
||||||
|
|
||||||
## 运行数据
|
## 运行数据
|
||||||
|
|
||||||
本地开发数据:
|
| 环境 | 路径 / Volume |
|
||||||
|
| --- | --- |
|
||||||
|
| 本地后端配置与日志 | `apps/backend/data` |
|
||||||
|
| 本地 PostgreSQL | Docker volume `postgres_dev_data` |
|
||||||
|
| 生产后端数据 | `apps/backend/data`(与本地路径一致) |
|
||||||
|
| 生产 PostgreSQL | Docker volume `postgres_data` |
|
||||||
|
| 生产 Caddy | Docker volume `caddy_data` |
|
||||||
|
|
||||||
- 后端配置与运行产物:`apps/backend/data`
|
注意:`apps/backend/data/*.json` 可能含账号、Cookie、token 等敏感信息,默认不进仓库;只保留 `*.example.json` 模板。
|
||||||
- PostgreSQL 数据:Docker volume `postgres_dev_data`
|
|
||||||
- 后端日志:`apps/backend/data/logs`
|
|
||||||
|
|
||||||
生产部署数据:
|
|
||||||
|
|
||||||
- 后端数据挂载到 `apps/backend/data`,与本地开发路径保持一致
|
|
||||||
- PostgreSQL 数据保存在 Docker volume `postgres_data`
|
|
||||||
- Caddy 数据保存在 Docker volume `caddy_data`
|
|
||||||
|
|
||||||
如果旧部署曾使用 `deploy/data/backend`,升级前请先把其中的 JSON 配置迁移到 `apps/backend/data`。
|
|
||||||
|
|
||||||
注意:`apps/backend/data/*.json` 中可能包含账号、Cookie、token、平台配置等敏感信息,默认不进入仓库;仓库只保留 `*.example.json` 模板。
|
|
||||||
|
|
||||||
## 生产部署
|
## 生产部署
|
||||||
|
|
||||||
在服务器上准备 `.env` 后启动:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp .env.server.example .env
|
cp .env.server.example .env
|
||||||
|
# 编辑真实配置后:
|
||||||
mkdir -p apps/backend/data
|
mkdir -p apps/backend/data
|
||||||
sudo chown -R 1001:1001 apps/backend/data
|
sudo chown -R 1001:1001 apps/backend/data
|
||||||
docker compose up -d --build
|
docker compose up -d --build
|
||||||
```
|
```
|
||||||
|
|
||||||
Ubuntu 服务器也可以使用一键部署脚本:
|
Ubuntu 一键脚本:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash deploy/ubuntu-deploy.sh
|
bash deploy/ubuntu-deploy.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
第一次执行如果没有 `.env`,脚本会从 `.env.server.example` 生成模板并停止;编辑真实配置后再次执行即可。脚本会自动检查 Docker、检查 `.env` 占位值、修正后端数据目录权限、构建启动服务并等待健康检查通过。
|
首次若无 `.env`,脚本会从示例生成模板并退出;填好后再执行一次即可。脚本会检查 Docker、占位配置、数据目录权限,构建启动并等待健康检查。
|
||||||
|
|
||||||
生产入口:
|
生产入口示例:
|
||||||
|
|
||||||
- 前端与领取页:`https://你的域名/`
|
- 前端与领取页:`https://你的域名/`
|
||||||
- 后端健康检查:`https://你的域名/health`
|
- 健康检查:`https://你的域名/health`
|
||||||
- 后端 API:`https://你的域名/api/v1/...`
|
- API:`https://你的域名/api/v1/...`
|
||||||
|
|
||||||
生产 Compose 会自动:
|
生产 Compose 会:启动 PostgreSQL → 后端自动迁移 → 初始化默认管理员 → 同步履约目录 → Caddy 暴露前端与 API。
|
||||||
|
|
||||||
- 启动 PostgreSQL
|
## 主要模块
|
||||||
- 启动后端并执行 migration
|
|
||||||
- 初始化默认后台管理员
|
|
||||||
- 同步履约目录
|
|
||||||
- 通过 Caddy 暴露前端和 API
|
|
||||||
|
|
||||||
## 主要业务模块
|
| 路径 | 职责 |
|
||||||
|
| --- | --- |
|
||||||
|
| `apps/backend/src/routes` | 开放接口 / 领取 / 后台 API |
|
||||||
|
| `apps/backend/src/services/order` | 订单入库、商品匹配、任务同步 |
|
||||||
|
| `apps/backend/src/services/fulfillment` | 履约路由与执行器(cloud / feifei / 人工) |
|
||||||
|
| `apps/backend/src/services/platforms` | 外部平台对接 |
|
||||||
|
| `apps/backend/src/services/admin` | 后台鉴权、读写、配置 |
|
||||||
|
| `apps/backend/src/repositories` | 数据访问 |
|
||||||
|
| `apps/backend/src/db/migrations` | 数据库迁移 |
|
||||||
|
| `apps/frontend/src/pages/admin` | 管理后台页面 |
|
||||||
|
| `apps/frontend/src/pages/claim` | 用户领取页 |
|
||||||
|
|
||||||
- `apps/backend/src/routes`:API 路由
|
## 提交前检查
|
||||||
- `apps/backend/src/services/order`:订单、商品匹配和履约任务编排
|
|
||||||
- `apps/backend/src/services/fulfillment`:快手 Cloud 履约编排
|
|
||||||
- `apps/backend/src/services/admin`:后台读写、权限、配置管理
|
|
||||||
- `apps/backend/src/repositories`:数据库访问
|
|
||||||
- `apps/frontend/src/pages/admin`:React 后台页面
|
|
||||||
- `apps/frontend/src/pages/claim`:React 用户领取页
|
|
||||||
- `apps/frontend-vue/src/views`:旧 Vue 前端保留目录
|
|
||||||
|
|
||||||
## 提交前检查清单
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose -f docker-compose.dev.yml exec -T backend npm test
|
docker compose -f docker-compose.dev.yml exec -T backend npm test
|
||||||
docker compose -f docker-compose.dev.yml exec -T backend npm run typecheck
|
docker compose -f docker-compose.dev.yml exec -T backend npm run typecheck
|
||||||
docker compose -f docker-compose.dev.yml exec -T frontend npm run typecheck
|
docker compose -f docker-compose.dev.yml exec -T frontend npm run typecheck
|
||||||
docker compose -f docker-compose.dev.yml exec -T frontend npm run lint
|
|
||||||
```
|
```
|
||||||
|
|
||||||
确认项:
|
确认:
|
||||||
|
|
||||||
- 没有提交真实账号、Cookie、token、手机号或生产配置
|
- 未提交真实账号、Cookie、token、手机号或生产配置
|
||||||
- `.env` 未被提交
|
- `.env` 未入库
|
||||||
- 数据库 migration 与代码一起提交
|
- **结构变更的 migration 与业务代码同 PR 提交**
|
||||||
- 领取链接相关改动已在 Docker 环境验证
|
- 领取链接相关改动已在 Docker 环境验证
|
||||||
|
|||||||
+84
-45
@@ -1,24 +1,17 @@
|
|||||||
# order-site-backend
|
# order-site-backend
|
||||||
|
|
||||||
快手轻量后端服务。
|
快手轻量后端:订单入库、履约任务编排、91 卡券 / kuaishou-cloud / kuaishou-feifei / 行业电子凭证对接、后台管理接口,以及数据库迁移。
|
||||||
|
|
||||||
后端负责订单入库、履约任务编排、kuaishou-lewan / 91 卡券 / 行业电子凭证配置、后台管理接口和数据库迁移。默认入口就是快手轻量后端。
|
|
||||||
|
|
||||||
## 快速启动
|
## 快速启动
|
||||||
|
|
||||||
先安装 Node 依赖:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install
|
npm install
|
||||||
```
|
|
||||||
|
|
||||||
复制并填写环境变量后启动:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cp ../../.env.mac-docker.example ../../.env
|
cp ../../.env.mac-docker.example ../../.env
|
||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
|
推荐优先用仓库根目录 Docker 开发环境(含 PostgreSQL 与 Caddy)。
|
||||||
|
|
||||||
生产构建:
|
生产构建:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -36,61 +29,107 @@ npm test
|
|||||||
|
|
||||||
## 配置
|
## 配置
|
||||||
|
|
||||||
后端采用工作区根目录 `.env` 做环境配置:
|
环境变量来自工作区根目录 `.env`:
|
||||||
|
|
||||||
- 通用默认值:`src/config/defaults.ts`
|
- 默认值:`src/config/defaults.ts`
|
||||||
- 本地/部署覆盖:工作区根目录 `.env`
|
- 覆盖:根目录 `.env`
|
||||||
|
|
||||||
常用配置:
|
常用项:
|
||||||
|
|
||||||
- 服务端口 `PORT`(Docker 部署中由根目录 `.env` 的 `BACKEND_PORT` 映射)
|
| 变量 | 说明 |
|
||||||
- 日志级别 `LOG_LEVEL=debug|info|warn|error`
|
| --- | --- |
|
||||||
- 数据库连接 `DATABASE_URL`
|
| `PORT` | 服务端口(Compose 中常由 `BACKEND_PORT` 映射) |
|
||||||
- 后台登录密钥 `ADMIN_SESSION_SECRET`
|
| `LOG_LEVEL` | `debug` / `info` / `warn` / `error` |
|
||||||
- 默认后台用户 `ADMIN_DEFAULT_USERS_JSON`
|
| `DATABASE_URL` | PostgreSQL 连接串 |
|
||||||
- 91 卡券开放接口凭据 `KAQUAN91_USER_ID` / `KAQUAN91_SECRET`
|
| `ADMIN_SESSION_SECRET` | 后台登录态签名密钥 |
|
||||||
|
| `ADMIN_DEFAULT_USERS_JSON` | 默认后台用户 |
|
||||||
|
| `KAQUAN91_USER_ID` / `KAQUAN91_SECRET` | 91 卡券凭据 |
|
||||||
|
|
||||||
## 接口
|
## 接口
|
||||||
|
|
||||||
- `GET /health`:健康检查
|
| 路径 | 说明 |
|
||||||
- `GET /health/live`:存活检查
|
| --- | --- |
|
||||||
- `GET /health/ready`:就绪检查
|
| `GET /health` | 健康检查 |
|
||||||
- `/api/v1/open/91/*`:91 卡券开放回调
|
| `GET /health/live` | 存活 |
|
||||||
- `/api/v1/claim/*`:领取页接口
|
| `GET /health/ready` | 就绪 |
|
||||||
- `/api/v1/admin/*`:后台管理接口
|
| `/api/v1/open/91/*` | 91 卡券开放接口 |
|
||||||
|
| `/api/v1/open/kuaishou-industry/*` | 快手行业电子凭证 |
|
||||||
|
| `/api/v1/open/kuaishou-feifei/*` | 发货平台 feifei 回调等 |
|
||||||
|
| `/api/v1/claim/*` | 领取页接口 |
|
||||||
|
| `/api/v1/admin/*` | 后台管理 |
|
||||||
|
| `/s/*` | 短链跳转 |
|
||||||
|
|
||||||
## 目录
|
## 目录
|
||||||
|
|
||||||
- `src/index.ts`:服务入口
|
| 路径 | 职责 |
|
||||||
- `src/app.ts`:Express 应用组装
|
| --- | --- |
|
||||||
- `src/routes`:API 路由
|
| `src/index.ts` | 服务入口 |
|
||||||
- `src/services/order`:订单、商品匹配和履约任务编排
|
| `src/app.ts` | Express 组装 |
|
||||||
- `src/services/fulfillment`:kuaishou-lewan 履约编排
|
| `src/routes` | API 路由 |
|
||||||
- `src/services/admin`:后台读写、权限、配置管理
|
| `src/services/order` | 订单与任务同步 |
|
||||||
- `src/repositories`:数据库访问
|
| `src/services/fulfillment` | 履约路由与执行器 |
|
||||||
- `src/db/migrations`:新库初始化脚本
|
| `src/services/platforms` | 外部平台实现 |
|
||||||
|
| `src/services/admin` | 后台读写与配置 |
|
||||||
|
| `src/repositories` | 数据库访问 |
|
||||||
|
| `src/db/migrations` | SQL 迁移 |
|
||||||
|
|
||||||
|
## 数据库迁移
|
||||||
|
|
||||||
|
策略:**001 基线 + 后续渐进增量**。
|
||||||
|
|
||||||
|
| 文件 | 作用 |
|
||||||
|
| --- | --- |
|
||||||
|
| `src/db/migrations/001_init.sql` | 空库完整基线结构 |
|
||||||
|
| `src/db/migrations/002_*.sql` 起 | 增量变更 |
|
||||||
|
|
||||||
|
规则:
|
||||||
|
|
||||||
|
1. 已应用的迁移文件禁止改内容。
|
||||||
|
2. 结构变更只新增文件,不回写 `001_init.sql`。
|
||||||
|
3. 启动时自动执行未应用迁移;记录在 `schema_migrations`。
|
||||||
|
4. 文件名必须为 `NNN_name.sql`(三位序号)。
|
||||||
|
|
||||||
|
命令:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 对齐状态
|
||||||
|
npm run db:migrate:status
|
||||||
|
|
||||||
|
# 手动 up
|
||||||
|
npm run db:migrate
|
||||||
|
|
||||||
|
# 创建下一份增量(生成 00N_xxx.sql 模板)
|
||||||
|
npm run db:migrate:create -- add_something
|
||||||
|
```
|
||||||
|
|
||||||
|
创建后编辑 SQL,与业务代码一并提交。Docker 开发环境示例:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f ../../docker-compose.dev.yml exec -T backend npm run db:migrate:create -- add_something
|
||||||
|
docker compose -f ../../docker-compose.dev.yml exec -T backend npm run db:migrate
|
||||||
|
```
|
||||||
|
|
||||||
## Docker 开发
|
## Docker 开发
|
||||||
|
|
||||||
开发版 Compose 会把源码挂载进容器:
|
开发 Compose 会挂载源码:
|
||||||
|
|
||||||
- `postgres` 容器提供开发库,后端通过 `DATABASE_URL` 连接
|
- `postgres` 提供开发库
|
||||||
- 后端容器启动时执行依赖安装和 `npm run dev`
|
- 后端启动时安装依赖并 `npm run dev`
|
||||||
- 平时改 `src/` 或 `.env`,一般不需要重建镜像
|
- 改 `src/` 或 `.env` 通常不必重建镜像
|
||||||
|
|
||||||
只有下面几类改动通常还需要重新构建 backend:
|
需要重建 backend 镜像的情况:
|
||||||
|
|
||||||
- `deploy/docker/backend*.Dockerfile`
|
- `deploy/docker/backend*.Dockerfile` 变更
|
||||||
- Node 版本或系统层依赖
|
- Node 版本或系统依赖变更
|
||||||
- `package.json` / `package-lock.json`
|
- `package.json` / `package-lock.json` 变更
|
||||||
|
|
||||||
## 运行产物
|
## 运行产物
|
||||||
|
|
||||||
运行日志默认保存在:
|
日志默认:
|
||||||
|
|
||||||
- `data/logs/app-YYYY-MM-DD.log`
|
- `data/logs/app-YYYY-MM-DD.log`
|
||||||
- `data/logs/integration-YYYY-MM-DD.log`
|
- `data/logs/integration-YYYY-MM-DD.log`
|
||||||
|
|
||||||
日志按天切分,默认自动清理 7 天前的旧日志。成功的 `/health`、`/health/live`、`/health/ready` 探活请求默认不写 access log。
|
按天切分,默认清理过期日志。成功的 `/health*` 探活默认不写 access log。
|
||||||
|
|
||||||
`data/*.json` 为本地运行配置,可能包含账号、Cookie、token 或平台配置,默认不提交;请复制 `data/*.example.json` 后填写真实值。
|
`data/*.json` 为本地运行配置,可能含敏感信息,默认不提交;请从 `*.example.json` 复制后填写。
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
"cleanup:dev-data": "tsx scripts/cleanup-dev-data.ts",
|
"cleanup:dev-data": "tsx scripts/cleanup-dev-data.ts",
|
||||||
"build": "tsc -p tsconfig.build.json && rm -rf dist/db/migrations && mkdir -p dist/db && cp -R src/db/migrations dist/db/migrations",
|
"build": "tsc -p tsconfig.build.json && rm -rf dist/db/migrations && mkdir -p dist/db && cp -R src/db/migrations dist/db/migrations",
|
||||||
"db:migrate": "tsx src/db/migrate.ts",
|
"db:migrate": "tsx src/db/migrate.ts",
|
||||||
|
"db:migrate:create": "tsx scripts/create-migration.ts",
|
||||||
|
"db:migrate:status": "tsx scripts/migration-status.ts",
|
||||||
"dev": "tsx watch --clear-screen=false src/index.ts",
|
"dev": "tsx watch --clear-screen=false src/index.ts",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"mock:claim": "tsx scripts/mock-kuaishou-cloud-claim.ts",
|
"mock:claim": "tsx scripts/mock-kuaishou-cloud-claim.ts",
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
/**
|
||||||
|
* 创建下一份渐进式数据库迁移文件。
|
||||||
|
*
|
||||||
|
* 用法:
|
||||||
|
* npm run db:migrate:create -- add_order_note
|
||||||
|
* npm run db:migrate:create -- "add order note"
|
||||||
|
*
|
||||||
|
* 规则:
|
||||||
|
* - 001_init.sql 是基线,只用于空库首次建表
|
||||||
|
* - 后续结构变更一律新增 002_xxx.sql / 003_xxx.sql ...
|
||||||
|
* - 已应用到任何环境的迁移文件禁止修改内容
|
||||||
|
*/
|
||||||
|
import fs from 'node:fs'
|
||||||
|
import path from 'node:path'
|
||||||
|
import process from 'node:process'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
|
||||||
|
const CURRENT_DIR = path.dirname(fileURLToPath(import.meta.url))
|
||||||
|
const MIGRATIONS_DIR = path.join(CURRENT_DIR, '../src/db/migrations')
|
||||||
|
const MIGRATION_FILE_PATTERN = /^(\d{3})_(.+)\.sql$/i
|
||||||
|
|
||||||
|
function slugify(input: string): string {
|
||||||
|
return input
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9]+/g, '_')
|
||||||
|
.replace(/^_+|_+$/g, '')
|
||||||
|
.replace(/_+/g, '_')
|
||||||
|
}
|
||||||
|
|
||||||
|
function listMigrationFiles(): string[] {
|
||||||
|
if (!fs.existsSync(MIGRATIONS_DIR)) {
|
||||||
|
throw new Error(`迁移目录不存在: ${MIGRATIONS_DIR}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fs
|
||||||
|
.readdirSync(MIGRATIONS_DIR)
|
||||||
|
.filter((name) => MIGRATION_FILE_PATTERN.test(name))
|
||||||
|
.sort((a, b) => a.localeCompare(b, 'en'))
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveNextVersion(files: string[]): number {
|
||||||
|
let maxVersion = 0
|
||||||
|
for (const file of files) {
|
||||||
|
const match = file.match(MIGRATION_FILE_PATTERN)
|
||||||
|
if (!match) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
maxVersion = Math.max(maxVersion, Number(match[1]))
|
||||||
|
}
|
||||||
|
return maxVersion + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTemplate(version: string, name: string): string {
|
||||||
|
const stamp = new Date().toISOString()
|
||||||
|
return `-- ${version}_${name}.sql
|
||||||
|
-- 创建时间: ${stamp}
|
||||||
|
-- 说明: TODO 填写本次结构变更目的
|
||||||
|
--
|
||||||
|
-- 渐进迁移约定:
|
||||||
|
-- 1. 只写增量 SQL(ALTER / CREATE INDEX / 数据回填等)
|
||||||
|
-- 2. 禁止修改已经提交并应用过的历史迁移文件
|
||||||
|
-- 3. 空库会按文件名顺序执行 001_init.sql → 后续增量迁移
|
||||||
|
-- 4. 线上/开发库启动时自动应用尚未记录在 schema_migrations 中的文件
|
||||||
|
|
||||||
|
-- 在此编写增量 SQL
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
const rawName = process.argv.slice(2).join(' ').trim()
|
||||||
|
if (!rawName || rawName.startsWith('-')) {
|
||||||
|
console.error('用法: npm run db:migrate:create -- <migration_name>')
|
||||||
|
console.error('示例: npm run db:migrate:create -- add_task_priority')
|
||||||
|
process.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = slugify(rawName)
|
||||||
|
if (!name) {
|
||||||
|
console.error('迁移名称无效,请使用英文/数字/下划线描述,例如 add_task_priority')
|
||||||
|
process.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = listMigrationFiles()
|
||||||
|
const nextVersion = resolveNextVersion(existing)
|
||||||
|
if (nextVersion > 999) {
|
||||||
|
console.error('迁移序号已超过 999,请调整命名策略')
|
||||||
|
process.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const version = String(nextVersion).padStart(3, '0')
|
||||||
|
const fileName = `${version}_${name}.sql`
|
||||||
|
const filePath = path.join(MIGRATIONS_DIR, fileName)
|
||||||
|
|
||||||
|
if (fs.existsSync(filePath)) {
|
||||||
|
console.error(`迁移文件已存在: ${fileName}`)
|
||||||
|
process.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const conflict = existing.find((item) => item.startsWith(`${version}_`))
|
||||||
|
if (conflict) {
|
||||||
|
console.error(`迁移序号冲突: ${version} 已被 ${conflict} 占用`)
|
||||||
|
process.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(filePath, buildTemplate(version, name), 'utf8')
|
||||||
|
console.info(`[db:migrate:create] 已创建 ${path.relative(process.cwd(), filePath)}`)
|
||||||
|
console.info('[db:migrate:create] 编辑 SQL 后提交;后端启动或 npm run db:migrate 会自动应用')
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
/**
|
||||||
|
* 查看本地迁移文件与数据库 schema_migrations 的对齐状态。
|
||||||
|
*
|
||||||
|
* 用法:
|
||||||
|
* npm run db:migrate:status
|
||||||
|
*/
|
||||||
|
import fs from 'node:fs'
|
||||||
|
import path from 'node:path'
|
||||||
|
import process from 'node:process'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
|
||||||
|
import { closeDb, query } from '../src/db/client.js'
|
||||||
|
|
||||||
|
const CURRENT_DIR = path.dirname(fileURLToPath(import.meta.url))
|
||||||
|
const MIGRATIONS_DIR = path.join(CURRENT_DIR, '../src/db/migrations')
|
||||||
|
const MIGRATION_FILE_PATTERN = /^(\d{3})_.+\.sql$/i
|
||||||
|
|
||||||
|
function listLocalMigrations(): string[] {
|
||||||
|
if (!fs.existsSync(MIGRATIONS_DIR)) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return fs
|
||||||
|
.readdirSync(MIGRATIONS_DIR)
|
||||||
|
.filter((name) => MIGRATION_FILE_PATTERN.test(name))
|
||||||
|
.sort((a, b) => a.localeCompare(b, 'en'))
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeMigrationName(value: unknown): string {
|
||||||
|
const raw = String(value || '').trim()
|
||||||
|
if (!raw) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
return raw.replace(/\.sql$/i, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const localFiles = listLocalMigrations()
|
||||||
|
const localNames = localFiles.map((file) => file.replace(/\.sql$/i, ''))
|
||||||
|
|
||||||
|
let appliedNames = new Set<string>()
|
||||||
|
try {
|
||||||
|
const result = await query<{ name: string | null; filename: string | null; run_on: string | null }>(
|
||||||
|
`
|
||||||
|
SELECT name, filename, run_on
|
||||||
|
FROM schema_migrations
|
||||||
|
ORDER BY COALESCE(filename, name), id
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
|
||||||
|
appliedNames = new Set(
|
||||||
|
result.rows
|
||||||
|
.map((row) => normalizeMigrationName(row.filename || row.name))
|
||||||
|
.filter(Boolean),
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error)
|
||||||
|
if (/schema_migrations/i.test(message) && /does not exist|不存在/i.test(message)) {
|
||||||
|
console.warn('[db:migrate:status] schema_migrations 尚不存在,视为尚未应用任何迁移')
|
||||||
|
} else {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pending = localNames.filter((name) => !appliedNames.has(name))
|
||||||
|
const orphaned = [...appliedNames].filter((name) => !localNames.includes(name))
|
||||||
|
|
||||||
|
console.info('[db:migrate:status] 本地迁移文件')
|
||||||
|
if (localFiles.length === 0) {
|
||||||
|
console.info(' (无)')
|
||||||
|
} else {
|
||||||
|
for (const file of localFiles) {
|
||||||
|
const name = file.replace(/\.sql$/i, '')
|
||||||
|
const mark = appliedNames.has(name) ? 'applied' : 'pending'
|
||||||
|
console.info(` [${mark}] ${file}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.info(`[db:migrate:status] 已应用 ${appliedNames.size} 个,待应用 ${pending.length} 个`)
|
||||||
|
if (pending.length > 0) {
|
||||||
|
console.info('[db:migrate:status] 待应用:')
|
||||||
|
for (const name of pending) {
|
||||||
|
console.info(` - ${name}.sql`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (orphaned.length > 0) {
|
||||||
|
console.warn('[db:migrate:status] 数据库中存在本地没有的迁移记录(可能是旧环境残留):')
|
||||||
|
for (const name of orphaned) {
|
||||||
|
console.warn(` - ${name}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(error)
|
||||||
|
process.exitCode = 1
|
||||||
|
})
|
||||||
|
.finally(async () => {
|
||||||
|
await closeDb()
|
||||||
|
})
|
||||||
@@ -1,3 +1,14 @@
|
|||||||
|
/**
|
||||||
|
* 数据库迁移入口。
|
||||||
|
*
|
||||||
|
* 策略:
|
||||||
|
* - 001_init.sql:空库基线,建立完整结构
|
||||||
|
* - 002_xxx.sql 起:渐进增量迁移,禁止改已应用历史文件
|
||||||
|
* - 启动时自动 up;也可用 npm run db:migrate 手动执行
|
||||||
|
* - 新建迁移:npm run db:migrate:create -- <name>
|
||||||
|
* - 查看状态:npm run db:migrate:status
|
||||||
|
*/
|
||||||
|
import fs from 'node:fs'
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
import process from 'node:process'
|
import process from 'node:process'
|
||||||
import { fileURLToPath } from 'node:url'
|
import { fileURLToPath } from 'node:url'
|
||||||
@@ -12,10 +23,14 @@ const CURRENT_DIR = path.dirname(fileURLToPath(import.meta.url))
|
|||||||
const MIGRATIONS_DIR = path.join(CURRENT_DIR, 'migrations')
|
const MIGRATIONS_DIR = path.join(CURRENT_DIR, 'migrations')
|
||||||
const MIGRATIONS_TABLE = 'schema_migrations'
|
const MIGRATIONS_TABLE = 'schema_migrations'
|
||||||
const SHORT_NUMERIC_PREFIX_MESSAGE = /^Can't determine timestamp for \d+$/
|
const SHORT_NUMERIC_PREFIX_MESSAGE = /^Can't determine timestamp for \d+$/
|
||||||
|
const MIGRATION_FILE_PATTERN = /^(\d{3})_.+\.sql$/i
|
||||||
|
|
||||||
export async function runDatabaseMigrations() {
|
export async function runDatabaseMigrations() {
|
||||||
|
assertMigrationFilesOrdered()
|
||||||
await ensureNodePgMigrateMetadataCompatibility()
|
await ensureNodePgMigrateMetadataCompatibility()
|
||||||
|
|
||||||
|
console.info(`[db:migrate] 迁移目录: ${MIGRATIONS_DIR}`)
|
||||||
|
|
||||||
await runner({
|
await runner({
|
||||||
databaseUrl: createMigrationDatabaseConfig(),
|
databaseUrl: createMigrationDatabaseConfig(),
|
||||||
dir: MIGRATIONS_DIR,
|
dir: MIGRATIONS_DIR,
|
||||||
@@ -45,6 +60,44 @@ export async function runDatabaseMigrations() {
|
|||||||
await syncLegacyMigrationColumns()
|
await syncLegacyMigrationColumns()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function assertMigrationFilesOrdered() {
|
||||||
|
if (!fs.existsSync(MIGRATIONS_DIR)) {
|
||||||
|
throw new Error(`迁移目录不存在: ${MIGRATIONS_DIR}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = fs
|
||||||
|
.readdirSync(MIGRATIONS_DIR)
|
||||||
|
.filter((name) => name.endsWith('.sql'))
|
||||||
|
.sort((a, b) => a.localeCompare(b, 'en'))
|
||||||
|
|
||||||
|
if (files.length === 0) {
|
||||||
|
throw new Error(`迁移目录为空: ${MIGRATIONS_DIR}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const invalid = files.filter((name) => !MIGRATION_FILE_PATTERN.test(name))
|
||||||
|
if (invalid.length > 0) {
|
||||||
|
throw new Error(
|
||||||
|
`迁移文件命名必须为 NNN_name.sql(三位序号),非法文件: ${invalid.join(', ')}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const versions = files.map((name) => Number(name.slice(0, 3)))
|
||||||
|
for (let index = 1; index < versions.length; index += 1) {
|
||||||
|
if (versions[index] < versions[index - 1]) {
|
||||||
|
throw new Error(`迁移序号乱序: ${files[index - 1]} 之后出现 ${files[index]}`)
|
||||||
|
}
|
||||||
|
if (versions[index] === versions[index - 1]) {
|
||||||
|
throw new Error(`迁移序号重复: ${files[index - 1]} 与 ${files[index]}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (files[0] !== '001_init.sql') {
|
||||||
|
console.warn(
|
||||||
|
`[db:migrate] 警告: 首个迁移不是 001_init.sql(当前为 ${files[0]}),请确认是否刻意调整基线`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function ensureNodePgMigrateMetadataCompatibility() {
|
async function ensureNodePgMigrateMetadataCompatibility() {
|
||||||
await query(`
|
await query(`
|
||||||
CREATE TABLE IF NOT EXISTS ${MIGRATIONS_TABLE} (
|
CREATE TABLE IF NOT EXISTS ${MIGRATIONS_TABLE} (
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
-- 初始化完整业务库结构。
|
-- 001_init.sql —— 空库基线迁移(完整业务库结构)。
|
||||||
-- 本项目当前不维护历史测试库的逐步升级链路;新环境只需要执行这一份 init 迁移。
|
--
|
||||||
|
-- 约定:
|
||||||
|
-- 1. 仅在空库首次迁移时执行本文件,建立全量基线结构。
|
||||||
|
-- 2. 后续结构变更不要改本文件,改为新增 002_xxx.sql / 003_xxx.sql 等渐进迁移。
|
||||||
|
-- 3. 新环境按文件名顺序执行:001_init → 后续增量;已有环境只应用尚未记录的增量。
|
||||||
|
-- 4. 已应用到任何共享/生产环境的迁移文件禁止修改内容。
|
||||||
|
|
||||||
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||||
|
|
||||||
|
|||||||
@@ -37,12 +37,13 @@ print_usage() {
|
|||||||
bash deploy/ubuntu-deploy.sh --reset-db
|
bash deploy/ubuntu-deploy.sh --reset-db
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
--reset-db 删除 Compose PostgreSQL 数据卷 postgres_data,并用当前 001_init.sql 重建数据库。
|
--reset-db 删除 Compose PostgreSQL 数据卷 postgres_data,再按 migrations 目录全量重建(001 基线 + 全部增量)。
|
||||||
--yes 跳过 --reset-db 的交互确认,也可使用环境变量 RESET_DB_CONFIRM=1。
|
--yes 跳过 --reset-db 的交互确认,也可使用环境变量 RESET_DB_CONFIRM=1。
|
||||||
-h, --help 显示帮助。
|
-h, --help 显示帮助。
|
||||||
|
|
||||||
注意:
|
注意:
|
||||||
--reset-db 会删除所有 PostgreSQL 数据,包括订单、任务、后台用户、审计日志和迁移记录。
|
--reset-db 会删除所有 PostgreSQL 数据,包括订单、任务、后台用户、审计日志和迁移记录。
|
||||||
|
日常结构变更请新增 002_xxx.sql 等渐进迁移,不要改已应用的历史迁移文件。
|
||||||
它不会删除 apps/backend/data 下的平台 JSON 配置,也不会删除 Caddy 证书卷。
|
它不会删除 apps/backend/data 下的平台 JSON 配置,也不会删除 Caddy 证书卷。
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|||||||
+100
-104
@@ -1,54 +1,89 @@
|
|||||||
# 部署启动与数据库重建
|
# 部署启动与数据库重建
|
||||||
|
|
||||||
本文记录本地开发、数据库重建和 Ubuntu 服务器部署的常用命令。
|
本文记录本地开发、数据库迁移、重建库和 Ubuntu 服务器部署的常用命令。
|
||||||
|
|
||||||
项目当前数据库迁移以新库初始化为主,主要结构在:
|
## 数据库迁移策略
|
||||||
|
|
||||||
|
项目使用 `node-pg-migrate`,迁移目录:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
apps/backend/src/db/migrations/001_init.sql
|
apps/backend/src/db/migrations/
|
||||||
|
001_init.sql 空库基线(完整结构)
|
||||||
|
002_xxx.sql 后续增量(示例命名)
|
||||||
|
003_xxx.sql
|
||||||
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
后端启动时会自动执行数据库迁移。迁移记录保存在数据库表 `schema_migrations`。
|
记录表:`schema_migrations`。
|
||||||
|
|
||||||
|
| 场景 | 行为 |
|
||||||
|
| --- | --- |
|
||||||
|
| 全新空库 | 按序号执行 `001_init` → 全部增量 |
|
||||||
|
| 已有库升级 | 只执行尚未记录的增量文件 |
|
||||||
|
| 改历史迁移文件内容 | **不会**自动重跑;已应用文件禁止修改 |
|
||||||
|
|
||||||
|
约定:
|
||||||
|
|
||||||
|
1. `001_init.sql` 只作为基线;上线后结构变更不要改它。
|
||||||
|
2. 每次 schema 变更新增 `NNN_name.sql`(三位序号)。
|
||||||
|
3. 迁移与业务代码同一变更一起提交。
|
||||||
|
4. 后端启动会自动 `up`;也可用命令手动执行。
|
||||||
|
|
||||||
|
### 开发常用命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查看本地文件与库内已应用记录
|
||||||
|
docker compose -f docker-compose.dev.yml exec -T backend npm run db:migrate:status
|
||||||
|
|
||||||
|
# 手动执行未应用迁移
|
||||||
|
docker compose -f docker-compose.dev.yml exec -T backend npm run db:migrate
|
||||||
|
|
||||||
|
# 创建下一份增量迁移模板
|
||||||
|
docker compose -f docker-compose.dev.yml exec -T backend npm run db:migrate:create -- add_column_example
|
||||||
|
```
|
||||||
|
|
||||||
|
宿主机直连(`postgres` 主机名仅在 Docker 网络内有效):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd apps/backend
|
||||||
|
DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5432/order_site npm run db:migrate:status
|
||||||
|
DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5432/order_site npm run db:migrate
|
||||||
|
```
|
||||||
|
|
||||||
## 本地开发启动
|
## 本地开发启动
|
||||||
|
|
||||||
首次启动:
|
首次:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp .env.mac-docker.example .env
|
cp .env.mac-docker.example .env
|
||||||
docker compose -f docker-compose.dev.yml up -d --build
|
docker compose -f docker-compose.dev.yml up -d --build
|
||||||
```
|
```
|
||||||
|
|
||||||
日常启动:
|
日常:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose -f docker-compose.dev.yml up -d
|
docker compose -f docker-compose.dev.yml up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
查看服务状态:
|
状态与日志:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose -f docker-compose.dev.yml ps
|
docker compose -f docker-compose.dev.yml ps
|
||||||
```
|
|
||||||
|
|
||||||
查看日志:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose -f docker-compose.dev.yml logs -f backend
|
docker compose -f docker-compose.dev.yml logs -f backend
|
||||||
docker compose -f docker-compose.dev.yml logs -f frontend
|
docker compose -f docker-compose.dev.yml logs -f frontend
|
||||||
```
|
```
|
||||||
|
|
||||||
本地访问入口:
|
本地入口:
|
||||||
|
|
||||||
- 前端、后台、领取页:`http://localhost`
|
- 前端、后台、领取页:`http://localhost`
|
||||||
- 后端健康检查:`http://localhost/health`
|
- 健康检查:`http://localhost/health`
|
||||||
- 后端就绪检查:`http://localhost/health/ready`
|
- 就绪检查:`http://localhost/health/ready`
|
||||||
|
|
||||||
## 本地重建数据库
|
## 本地重建数据库
|
||||||
|
|
||||||
如果迁移基线变化,或者需要清空本地测试数据,可以重建本地开发库。
|
仅在需要清空开发数据,或本地库与迁移历史严重不一致时使用。
|
||||||
|
|
||||||
只重建开发数据库,保留源码文件:
|
只重建开发库 volume:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose -f docker-compose.dev.yml down
|
docker compose -f docker-compose.dev.yml down
|
||||||
@@ -56,29 +91,33 @@ docker volume rm order_site_postgres_dev_data
|
|||||||
docker compose -f docker-compose.dev.yml up -d --build
|
docker compose -f docker-compose.dev.yml up -d --build
|
||||||
```
|
```
|
||||||
|
|
||||||
如果提示 volume 不存在,说明本地数据库 volume 名称可能不同,可以先查看:
|
volume 名称不确定时:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker volume ls | grep postgres_dev_data
|
docker volume ls | grep postgres_dev_data
|
||||||
```
|
```
|
||||||
|
|
||||||
也可以直接删除当前 dev compose 创建的所有 volume:
|
删除当前 dev compose 全部 volume(会重装 node_modules volume):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose -f docker-compose.dev.yml down -v
|
docker compose -f docker-compose.dev.yml down -v
|
||||||
docker compose -f docker-compose.dev.yml up -d --build
|
docker compose -f docker-compose.dev.yml up -d --build
|
||||||
```
|
```
|
||||||
|
|
||||||
注意:`down -v` 会同时删除开发环境里的 `postgres_dev_data`、`backend_node_modules`、`frontend_node_modules` 等 volume。下次启动会重新安装依赖。
|
确认迁移记录:
|
||||||
|
|
||||||
确认迁移结果:
|
```bash
|
||||||
|
docker compose -f docker-compose.dev.yml exec -T backend npm run db:migrate:status
|
||||||
|
```
|
||||||
|
|
||||||
|
或:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose -f docker-compose.dev.yml exec -T backend node --input-type=module -e '
|
docker compose -f docker-compose.dev.yml exec -T backend node --input-type=module -e '
|
||||||
const { query, closeDb } = await import("./src/db/client.ts");
|
const { query, closeDb } = await import("./src/db/client.ts");
|
||||||
try {
|
try {
|
||||||
const result = await query("SELECT filename FROM schema_migrations ORDER BY filename");
|
const result = await query("SELECT COALESCE(filename, name) AS name FROM schema_migrations ORDER BY COALESCE(filename, name)");
|
||||||
console.log(result.rows.map((row) => row.filename).join("\n"));
|
console.log(result.rows.map((row) => row.name).join("\n"));
|
||||||
} finally {
|
} finally {
|
||||||
await closeDb();
|
await closeDb();
|
||||||
}
|
}
|
||||||
@@ -87,39 +126,23 @@ try {
|
|||||||
|
|
||||||
## 本地验证
|
## 本地验证
|
||||||
|
|
||||||
后端:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose -f docker-compose.dev.yml exec -T backend npm test
|
docker compose -f docker-compose.dev.yml exec -T backend npm test
|
||||||
docker compose -f docker-compose.dev.yml exec -T backend npm run typecheck
|
docker compose -f docker-compose.dev.yml exec -T backend npm run typecheck
|
||||||
docker compose -f docker-compose.dev.yml exec -T backend npm run build
|
docker compose -f docker-compose.dev.yml exec -T backend npm run build
|
||||||
```
|
|
||||||
|
|
||||||
前端:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose -f docker-compose.dev.yml exec -T frontend npm run typecheck
|
docker compose -f docker-compose.dev.yml exec -T frontend npm run typecheck
|
||||||
docker compose -f docker-compose.dev.yml exec -T frontend npm run lint
|
|
||||||
docker compose -f docker-compose.dev.yml exec -T frontend npm run build
|
docker compose -f docker-compose.dev.yml exec -T frontend npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
手动执行迁移:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose -f docker-compose.dev.yml exec -T backend npm run db:migrate
|
|
||||||
```
|
|
||||||
|
|
||||||
## 服务器首次部署
|
## 服务器首次部署
|
||||||
|
|
||||||
服务器建议使用 Ubuntu + Docker Compose。
|
建议 Ubuntu + Docker Compose。
|
||||||
|
|
||||||
进入项目目录后执行:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp .env.server.example .env
|
cp .env.server.example .env
|
||||||
```
|
```
|
||||||
|
|
||||||
编辑 `.env`,至少替换这些值:
|
至少配置:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
CADDY_SITE_ADDR
|
CADDY_SITE_ADDR
|
||||||
@@ -130,66 +153,43 @@ ADMIN_SESSION_SECRET
|
|||||||
ADMIN_DEFAULT_USERS_JSON
|
ADMIN_DEFAULT_USERS_JSON
|
||||||
```
|
```
|
||||||
|
|
||||||
生产 Docker 部署时,`DATABASE_URL` 如果连接 compose 内置 PostgreSQL,主机名应使用 `postgres`:
|
容器内连接 Compose PostgreSQL 时,主机名用 `postgres`:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
DATABASE_URL=postgres://postgres:你的数据库密码@postgres:5432/order_site
|
DATABASE_URL=postgres://postgres:你的数据库密码@postgres:5432/order_site
|
||||||
```
|
```
|
||||||
|
|
||||||
不要写成:
|
不要写成容器内的 `127.0.0.1`(那会指向后端容器自己)。
|
||||||
|
|
||||||
```text
|
部署:
|
||||||
DATABASE_URL=postgres://postgres:你的数据库密码@127.0.0.1:5432/order_site
|
|
||||||
```
|
|
||||||
|
|
||||||
因为后端运行在容器内,容器内的 `127.0.0.1` 指向后端容器自己,不是 PostgreSQL 容器。
|
|
||||||
|
|
||||||
执行部署:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash deploy/ubuntu-deploy.sh
|
bash deploy/ubuntu-deploy.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
脚本会自动:
|
脚本会:检查 Docker、检查 `.env` 占位值、检查 `DATABASE_URL`、修正 `apps/backend/data` 权限、构建启动、等待健康检查、打印已应用迁移。
|
||||||
|
|
||||||
- 检查 Docker 和 Docker Compose
|
|
||||||
- 检查 `.env` 是否仍有占位值
|
|
||||||
- 检查容器部署时 `DATABASE_URL` 是否误写成本机地址
|
|
||||||
- 修正 `apps/backend/data` 目录权限
|
|
||||||
- 构建并启动服务
|
|
||||||
- 等待健康检查通过
|
|
||||||
- 打印已应用数据库迁移
|
|
||||||
|
|
||||||
部署完成后查看服务:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose ps
|
docker compose ps
|
||||||
```
|
|
||||||
|
|
||||||
查看后端日志:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose logs -f backend
|
docker compose logs -f backend
|
||||||
```
|
```
|
||||||
|
|
||||||
## 服务器日常更新
|
## 服务器日常更新
|
||||||
|
|
||||||
拉取或上传新代码后,在项目目录执行:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash deploy/ubuntu-deploy.sh
|
bash deploy/ubuntu-deploy.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
后端容器启动时会自动执行迁移,不需要单独手动跑迁移。
|
后端启动时自动执行未应用的增量迁移,一般不必单独跑迁移。
|
||||||
|
|
||||||
确认迁移记录:
|
确认:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose exec -T backend node --input-type=module -e '
|
docker compose exec -T backend node --input-type=module -e '
|
||||||
const { query, closeDb } = await import("./dist/db/client.js");
|
const { query, closeDb } = await import("./dist/db/client.js");
|
||||||
try {
|
try {
|
||||||
const result = await query("SELECT filename FROM schema_migrations ORDER BY filename");
|
const result = await query("SELECT COALESCE(filename, name) AS name FROM schema_migrations ORDER BY COALESCE(filename, name)");
|
||||||
console.log(result.rows.map((row) => row.filename).join("\n"));
|
console.log(result.rows.map((row) => row.name).join("\n"));
|
||||||
} finally {
|
} finally {
|
||||||
await closeDb();
|
await closeDb();
|
||||||
}
|
}
|
||||||
@@ -198,46 +198,38 @@ try {
|
|||||||
|
|
||||||
## 服务器重建数据库
|
## 服务器重建数据库
|
||||||
|
|
||||||
下面命令会删除生产数据库数据。只适合新开发、测试服务器或确认不需要保留数据的场景。
|
**会删除生产数据**。仅适合新环境、测试机或确认可丢数据的场景。
|
||||||
|
|
||||||
停止服务:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose down
|
docker compose down
|
||||||
```
|
|
||||||
|
|
||||||
只删除 PostgreSQL 数据 volume,保留 Caddy 证书和配置 volume:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker volume rm order_site_postgres_data
|
docker volume rm order_site_postgres_data
|
||||||
```
|
|
||||||
|
|
||||||
重新部署:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash deploy/ubuntu-deploy.sh
|
bash deploy/ubuntu-deploy.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
如果 volume 名称不确定,可以查看:
|
或:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash deploy/ubuntu-deploy.sh --reset-db
|
||||||
|
```
|
||||||
|
|
||||||
|
volume 名不确定时:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker volume ls | grep postgres_data
|
docker volume ls | grep postgres_data
|
||||||
```
|
```
|
||||||
|
|
||||||
不建议在生产服务器随手执行:
|
不要随手:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose down -v
|
docker compose down -v
|
||||||
```
|
```
|
||||||
|
|
||||||
因为它会同时删除 `postgres_data`、`caddy_data`、`caddy_config`。这会清空数据库,并可能让 Caddy 重新申请证书。
|
这会连同 `caddy_data` / `caddy_config` 一起删掉,可能触发证书重签。
|
||||||
|
|
||||||
## 常见问题
|
## 常见问题
|
||||||
|
|
||||||
### 后端一直未 ready
|
### 后端一直未 ready
|
||||||
|
|
||||||
查看诊断:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose ps
|
docker compose ps
|
||||||
docker compose logs --tail=120 backend
|
docker compose logs --tail=120 backend
|
||||||
@@ -246,23 +238,27 @@ docker compose logs --tail=120 backend
|
|||||||
常见原因:
|
常见原因:
|
||||||
|
|
||||||
- `.env` 仍有 `change-me` 占位值
|
- `.env` 仍有 `change-me` 占位值
|
||||||
- `DATABASE_URL` 密码和已有 PostgreSQL volume 初始化密码不一致
|
- `DATABASE_URL` 密码与已有 PostgreSQL volume 初始化密码不一致
|
||||||
- `DATABASE_URL` 在容器部署中误写成 `127.0.0.1`
|
- 容器部署里 `DATABASE_URL` 误写 `127.0.0.1`
|
||||||
- 后端迁移失败
|
- 某次增量迁移 SQL 失败
|
||||||
|
|
||||||
### 修改了 `001_init.sql` 但旧库没有变化
|
### 改了 `001_init.sql` 但旧库没变
|
||||||
|
|
||||||
`schema_migrations` 只记录迁移文件是否执行过。已经执行过 `001_init.sql` 的数据库,不会因为文件内容变化自动重跑。
|
已应用过的迁移不会因文件内容变更而重跑。正确做法是 **新增** `002_xxx.sql` 写增量。
|
||||||
|
|
||||||
新开发阶段如果不需要保留数据,直接重建数据库即可。
|
仅本地可丢数据时,才用重建 volume 的方式对齐。
|
||||||
|
|
||||||
|
### 如何新增字段 / 索引
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.dev.yml exec -T backend \
|
||||||
|
npm run db:migrate:create -- add_orders_note
|
||||||
|
|
||||||
|
# 编辑生成的 apps/backend/src/db/migrations/00N_add_orders_note.sql
|
||||||
|
docker compose -f docker-compose.dev.yml exec -T backend npm run db:migrate
|
||||||
|
docker compose -f docker-compose.dev.yml exec -T backend npm run db:migrate:status
|
||||||
|
```
|
||||||
|
|
||||||
### 宿主机手动跑迁移连不上 `postgres`
|
### 宿主机手动跑迁移连不上 `postgres`
|
||||||
|
|
||||||
`.env` 里的 `postgres` 主机名只在 Docker 网络里有效。宿主机直接跑迁移时需要临时使用宿主机地址:
|
`.env` 里的 `postgres` 只在 Docker 网络有效。宿主机请临时改为 `127.0.0.1` 并映射端口。
|
||||||
|
|
||||||
```bash
|
|
||||||
cd apps/backend
|
|
||||||
DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5432/order_site npm run db:migrate
|
|
||||||
```
|
|
||||||
|
|
||||||
容器内执行迁移时继续使用 `.env` 中的 `postgres` 服务名即可。
|
|
||||||
|
|||||||
Reference in New Issue
Block a user