Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c0b561b6cb | |||
| 1dbeaa7209 | |||
| f1e3976c95 | |||
| 998a44431d | |||
| 488df0b1ee |
@@ -46,9 +46,13 @@
|
||||
├── docs/
|
||||
│ └── API.md # 前端对接接口文档
|
||||
├── deploy/
|
||||
│ ├── env/
|
||||
│ │ ├── test.env.example # 测试环境变量示例,不包含真实密码
|
||||
│ │ └── production.env.example # 生产环境变量示例,不包含真实密码
|
||||
│ └── server/
|
||||
│ ├── create-env.sh # 在服务器生成真实 .env 和 .env.production
|
||||
│ └── docker-compose.mysql.yml # 服务器 MySQL Compose 模板
|
||||
│ ├── create-env.sh # 在服务器生成测试/生产真实环境变量
|
||||
│ ├── docker-compose.mysql.test.yml # 测试 MySQL Compose 模板
|
||||
│ └── docker-compose.mysql.production.yml # 生产 MySQL Compose 模板
|
||||
├── migrations/ # 数据库迁移 SQL
|
||||
│ ├── 001_initial_schema.sql # 创建基础表结构
|
||||
│ ├── 002_seed_demo_data.sql # 初始化演示门店和角色
|
||||
@@ -95,7 +99,8 @@
|
||||
| `AGENTS.md` | Agent 工具读取的入口文件,当前通过 `@RTK.md` 引入项目规则。 |
|
||||
| `RTK.md` | 本项目的协作规则,例如使用中文说明、保持分层、改目录时同步 README。 |
|
||||
| `docs/API.md` | 面向前端对接的完整接口文档,包含认证、权限、字段约束、示例请求响应和错误码。 |
|
||||
| `deploy/server/` | 服务器部署辅助文件。`create-env.sh` 在服务器本地生成真实环境变量,`docker-compose.mysql.yml` 用于启动服务器 MySQL。 |
|
||||
| `deploy/env/` | 测试/生产环境变量示例,只给字段结构,不提交真实密码。 |
|
||||
| `deploy/server/` | 服务器部署辅助文件。`create-env.sh` 在服务器本地生成测试/生产真实环境变量,Compose 模板分别启动测试和生产 MySQL。 |
|
||||
| `migrations/` | 数据库迁移目录。所有建表、改表、初始化基础数据的 SQL 都放在这里。 |
|
||||
| `src/app.ts` | 创建 Fastify 应用,注册路由,处理健康检查和全局错误。 |
|
||||
| `src/server.ts` | 真正启动 HTTP 服务,监听端口,并处理优雅停机。 |
|
||||
@@ -227,60 +232,82 @@ curl http://localhost:3500/health
|
||||
|
||||
服务器部署时不要直接使用本地 `.env.development`,也不要把本机 `.env.*` 打进部署包。真实密码只应该在服务器本地生成和保存。
|
||||
|
||||
当前服务器按测试环境和生产环境拆分,默认路径如下:
|
||||
|
||||
| 环境 | 应用目录 | 默认端口 | 数据目录 |
|
||||
| --- | --- | --- | --- |
|
||||
| 测试环境 | `/srv/www/test/access-manage` | `3501` | `/srv/data/test/access-manage/mysql` |
|
||||
| 生产环境 | `/srv/www/production/access-manage` | `3500` | `/srv/data/production/access-manage/mysql` |
|
||||
|
||||
1. 生成服务器真实环境变量:
|
||||
|
||||
```bash
|
||||
cd /srv/www/access-manage
|
||||
bash deploy/server/create-env.sh /srv/www/access-manage
|
||||
cd /srv/www/test/access-manage
|
||||
bash deploy/server/create-env.sh /srv/www/test/access-manage test
|
||||
|
||||
cd /srv/www/production/access-manage
|
||||
bash deploy/server/create-env.sh /srv/www/production/access-manage production
|
||||
```
|
||||
|
||||
这个脚本会生成两个不提交到仓库的文件:
|
||||
这个脚本会生成不提交到仓库的文件:
|
||||
|
||||
```text
|
||||
/srv/www/access-manage/.env
|
||||
/srv/www/access-manage/.env.production
|
||||
/srv/www/test/access-manage/.env.test.mysql
|
||||
/srv/www/test/access-manage/.env.test
|
||||
/srv/www/production/access-manage/.env.production.mysql
|
||||
/srv/www/production/access-manage/.env.production
|
||||
```
|
||||
|
||||
其中 `.env` 给 MySQL 容器使用,`.env.production` 给 Node 后端使用。如果 `.env` 已经存在,脚本会读取现有 `MYSQL_PASSWORD`,只补 `.env.production`;如果 `.env.production` 已经存在,脚本会拒绝覆盖。
|
||||
其中 `.env.*.mysql` 给 MySQL 容器使用,`.env.test` / `.env.production` 给 Node 后端使用。如果应用 env 已存在,脚本会拒绝覆盖。
|
||||
|
||||
2. 启动服务器 MySQL:
|
||||
|
||||
```bash
|
||||
docker-compose -f deploy/server/docker-compose.mysql.yml up -d mysql
|
||||
cd /srv/www/test/access-manage
|
||||
docker-compose --env-file .env.test.mysql -f deploy/server/docker-compose.mysql.test.yml up -d mysql
|
||||
|
||||
cd /srv/www/production/access-manage
|
||||
docker-compose --env-file .env.production.mysql -f deploy/server/docker-compose.mysql.production.yml up -d mysql
|
||||
```
|
||||
|
||||
服务器模板会把 MySQL 数据持久化到:
|
||||
服务器模板会把 MySQL 数据分别持久化到:
|
||||
|
||||
```text
|
||||
/srv/data/access-manage/mysql
|
||||
/srv/data/test/access-manage/mysql
|
||||
/srv/data/production/access-manage/mysql
|
||||
```
|
||||
|
||||
当前模板默认后端通过服务器本机端口连接 MySQL:
|
||||
|
||||
```env
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3307
|
||||
DB_PORT=3308 # 测试
|
||||
DB_PORT=3307 # 生产
|
||||
```
|
||||
|
||||
3. 安装生产依赖、迁移、启动:
|
||||
|
||||
```bash
|
||||
pnpm install --prod --frozen-lockfile
|
||||
pnpm db:migrate:test
|
||||
pnpm start:test
|
||||
|
||||
pnpm db:migrate:prod
|
||||
pnpm start:prod
|
||||
```
|
||||
|
||||
`pnpm build:dev` 和 `pnpm build:pro` 只生成编译后的 `dist/` 目录。服务器上不需要再执行 `pnpm build`。
|
||||
`pnpm build:dev`、`pnpm build:test` 和 `pnpm build:pro` 只生成编译后的 `dist/` 目录。服务器上不需要再执行 `pnpm build`。
|
||||
|
||||
4. 健康检查:
|
||||
|
||||
```bash
|
||||
curl http://127.0.0.1:3500/health
|
||||
curl http://127.0.0.1:3501/health # 测试
|
||||
curl http://127.0.0.1:3500/health # 生产
|
||||
```
|
||||
|
||||
只有返回里出现 `database: "up"`,才代表后端服务和 MySQL 都连通。
|
||||
|
||||
如果手动压缩部署,至少需要包含 `dist/`、`migrations/`、`deploy/`、`package.json` 和 `pnpm-lock.yaml`。不要把本机 `.env.*`、`node_modules/`、`src/`、`tsconfig.json` 和 `.git/` 打进部署包。
|
||||
如果手动压缩部署,至少需要包含 `dist/`、`migrations/`、`package.json` 和 `pnpm-lock.yaml`。不要把本机 `.env.*`、`node_modules/`、`src/`、`tsconfig.json` 和 `.git/` 打进部署包。
|
||||
|
||||
可以在本机执行:
|
||||
|
||||
@@ -288,7 +315,11 @@ curl http://127.0.0.1:3500/health
|
||||
pnpm build:dev
|
||||
```
|
||||
|
||||
命令执行完成后会得到最新的 `dist/`。如果需要压缩上传,由部署者手动选择要打包的文件;正式生产构建时可以执行 `pnpm build:pro`。
|
||||
命令执行完成后会得到最新的 `dist/`。如果需要压缩上传,由部署者手动选择要打包的文件;测试构建可执行 `pnpm build:test`,正式生产构建时执行 `pnpm build:pro`。
|
||||
|
||||
## 环境构建约定
|
||||
|
||||
仓库只维护项目自身的构建脚本和环境变量示例,不维护流水线文件。测试、生产的触发规则和部署路径由外部 CI/CD 平台配置。
|
||||
|
||||
## package.json 脚本说明
|
||||
|
||||
@@ -299,11 +330,14 @@ pnpm build:dev
|
||||
| `pnpm dev` | 使用现有 `.env.development` 启动开发服务,并通过 `tsx watch` 监听代码变化。 | 日常开发接口时使用。 |
|
||||
| `pnpm build` | 使用 `tsc` 编译 TypeScript,输出到 `dist/`。 | 准备运行编译产物或发布前验证时使用。 |
|
||||
| `pnpm build:dev` | 清空旧 `dist/`,再执行 `pnpm build` 生成新的 `dist/`。 | 准备开发/测试服务器部署产物时使用。 |
|
||||
| `pnpm build:pro` | 清空旧 `dist/`,再执行 `pnpm build` 生成新的 `dist/`。 | 准备正式生产部署产物时使用。 |
|
||||
| `pnpm build:test` | 清空旧 `dist/`,再执行 `pnpm build` 生成新的 `dist/`。 | 准备测试环境部署产物时使用。 |
|
||||
| `pnpm build:pro` | 清空旧 `dist/`,再执行 `pnpm build` 生成新的 `dist/`。 | 准备生产环境部署产物时使用。 |
|
||||
| `pnpm start` | 使用现有 `.env.development` 运行 `dist/server.js`。 | 已经执行过 `pnpm build` 后,用编译产物启动服务。 |
|
||||
| `pnpm start:test` | 使用 `.env.test` 运行 `dist/server.js`。 | 服务器测试环境启动编译产物时使用。 |
|
||||
| `pnpm start:prod` | 使用 `.env.production` 运行 `dist/server.js`。 | 服务器生产环境启动编译产物时使用。 |
|
||||
| `pnpm typecheck` | 执行 `tsc --noEmit`,只检查类型,不生成文件。 | 改 TypeScript 代码后快速确认类型是否正确。 |
|
||||
| `pnpm db:migrate` | 使用现有 `.env.development` 运行 `src/db/migrate.ts`,按顺序执行 `migrations/*.sql`。 | 第一次启动项目、拉到新迁移、改数据库结构后使用。 |
|
||||
| `pnpm db:migrate:test` | 使用 `.env.test` 运行 `dist/db/migrate.js`,按顺序执行 `migrations/*.sql`。 | 服务器测试环境建表、升级表结构或初始化基础数据时使用。 |
|
||||
| `pnpm db:migrate:prod` | 使用 `.env.production` 运行 `dist/db/migrate.js`,按顺序执行 `migrations/*.sql`。 | 服务器生产环境建表、升级表结构或初始化基础数据时使用。 |
|
||||
| `pnpm db:shell` | 进入 Docker 容器里的 MySQL 命令行。 | 需要手动查看表结构或查询数据时使用。 |
|
||||
| `pnpm mysql:up` | 启动本地 MySQL 容器。 | 开发前先启动数据库。 |
|
||||
|
||||
Vendored
+14
@@ -0,0 +1,14 @@
|
||||
NODE_ENV=production
|
||||
APP_ENV=production
|
||||
APP_ENV_LABEL=生产环境
|
||||
PORT=3500
|
||||
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3307
|
||||
DB_USER=access_user
|
||||
DB_PASSWORD=replace-with-production-password
|
||||
DB_NAME=access_manage
|
||||
DB_CONNECTION_LIMIT=10
|
||||
|
||||
JWT_SECRET=replace-with-at-least-32-characters-production-secret
|
||||
JWT_EXPIRES_IN=2h
|
||||
Vendored
+14
@@ -0,0 +1,14 @@
|
||||
NODE_ENV=test
|
||||
APP_ENV=test
|
||||
APP_ENV_LABEL=测试环境
|
||||
PORT=3501
|
||||
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3308
|
||||
DB_USER=access_user
|
||||
DB_PASSWORD=replace-with-test-password
|
||||
DB_NAME=access_manage_test
|
||||
DB_CONNECTION_LIMIT=10
|
||||
|
||||
JWT_SECRET=replace-with-at-least-32-characters-test-secret
|
||||
JWT_EXPIRES_IN=2h
|
||||
@@ -2,11 +2,33 @@
|
||||
set -euo pipefail
|
||||
|
||||
target_dir="${1:-$(pwd)}"
|
||||
mysql_env="${target_dir}/.env"
|
||||
app_env="${target_dir}/.env.production"
|
||||
deploy_env="${2:-production}"
|
||||
|
||||
case "${deploy_env}" in
|
||||
test)
|
||||
mysql_env="${target_dir}/.env.test.mysql"
|
||||
app_env="${target_dir}/.env.test"
|
||||
node_env="test"
|
||||
app_port="${ACCESS_MANAGE_TEST_PORT:-3501}"
|
||||
mysql_port="${ACCESS_MANAGE_TEST_DB_PORT:-3308}"
|
||||
mysql_database="${ACCESS_MANAGE_TEST_DB_NAME:-access_manage_test}"
|
||||
;;
|
||||
production)
|
||||
mysql_env="${target_dir}/.env.production.mysql"
|
||||
app_env="${target_dir}/.env.production"
|
||||
node_env="production"
|
||||
app_port="${ACCESS_MANAGE_PRODUCTION_PORT:-3500}"
|
||||
mysql_port="${ACCESS_MANAGE_PRODUCTION_DB_PORT:-3307}"
|
||||
mysql_database="${ACCESS_MANAGE_PRODUCTION_DB_NAME:-access_manage}"
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 [target_dir] [test|production]" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ -e "${app_env}" ]]; then
|
||||
echo "Refuse to overwrite existing .env.production in ${target_dir}" >&2
|
||||
echo "Refuse to overwrite existing $(basename "${app_env}") in ${target_dir}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -22,7 +44,7 @@ if [[ -e "${mysql_env}" ]]; then
|
||||
# shellcheck disable=SC1090
|
||||
source "${mysql_env}"
|
||||
|
||||
mysql_database="${MYSQL_DATABASE:-access_manage}"
|
||||
mysql_database="${MYSQL_DATABASE:-${mysql_database}}"
|
||||
mysql_user="${MYSQL_USER:-access_user}"
|
||||
mysql_password="${MYSQL_PASSWORD:-}"
|
||||
|
||||
@@ -34,7 +56,6 @@ if [[ -e "${mysql_env}" ]]; then
|
||||
echo "Found existing ${mysql_env}; only creating ${app_env}"
|
||||
else
|
||||
mysql_root_password="root_$(openssl rand -hex 24)"
|
||||
mysql_database="access_manage"
|
||||
mysql_user="access_user"
|
||||
mysql_password="app_$(openssl rand -hex 24)"
|
||||
|
||||
@@ -43,6 +64,7 @@ MYSQL_ROOT_PASSWORD=${mysql_root_password}
|
||||
MYSQL_DATABASE=${mysql_database}
|
||||
MYSQL_USER=${mysql_user}
|
||||
MYSQL_PASSWORD=${mysql_password}
|
||||
MYSQL_HOST_PORT=${mysql_port}
|
||||
EOF
|
||||
|
||||
echo "Created ${mysql_env}"
|
||||
@@ -51,11 +73,13 @@ fi
|
||||
jwt_secret="$(openssl rand -hex 48)"
|
||||
|
||||
cat > "${app_env}" <<EOF
|
||||
NODE_ENV=production
|
||||
PORT=3500
|
||||
NODE_ENV=${node_env}
|
||||
APP_ENV=${node_env}
|
||||
APP_ENV_LABEL=$([[ "${deploy_env}" == "test" ]] && echo "测试环境" || echo "生产环境")
|
||||
PORT=${app_port}
|
||||
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3307
|
||||
DB_PORT=${mysql_port}
|
||||
DB_USER=${mysql_user}
|
||||
DB_PASSWORD=${mysql_password}
|
||||
DB_NAME=${mysql_database}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:8.4
|
||||
container_name: access-manage-mysql-production
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
|
||||
MYSQL_DATABASE: ${MYSQL_DATABASE}
|
||||
MYSQL_USER: ${MYSQL_USER}
|
||||
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
|
||||
TZ: Asia/Shanghai
|
||||
ports:
|
||||
- "${MYSQL_HOST_PORT:-3307}:3306"
|
||||
volumes:
|
||||
- /srv/data/production/access-manage/mysql:/var/lib/mysql
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -p$${MYSQL_ROOT_PASSWORD} --silent"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 20
|
||||
@@ -0,0 +1,22 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:8.4
|
||||
container_name: access-manage-mysql-test
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
|
||||
MYSQL_DATABASE: ${MYSQL_DATABASE}
|
||||
MYSQL_USER: ${MYSQL_USER}
|
||||
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
|
||||
TZ: Asia/Shanghai
|
||||
ports:
|
||||
- "${MYSQL_HOST_PORT:-3308}:3306"
|
||||
volumes:
|
||||
- /srv/data/test/access-manage/mysql:/var/lib/mysql
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -p$${MYSQL_ROOT_PASSWORD} --silent"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 20
|
||||
+4
-2
@@ -7,11 +7,14 @@
|
||||
"dev": "DOTENV_CONFIG_PATH=.env.development tsx watch src/server.ts",
|
||||
"build": "tsc",
|
||||
"build:dev": "rm -rf dist && pnpm build",
|
||||
"build:test": "rm -rf dist && pnpm build",
|
||||
"build:pro": "rm -rf dist && pnpm build",
|
||||
"start": "DOTENV_CONFIG_PATH=.env.development node dist/server.js",
|
||||
"start:test": "DOTENV_CONFIG_PATH=.env.test node dist/server.js",
|
||||
"start:prod": "DOTENV_CONFIG_PATH=.env.production node dist/server.js",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"db:migrate": "DOTENV_CONFIG_PATH=.env.development tsx src/db/migrate.ts",
|
||||
"db:migrate:test": "DOTENV_CONFIG_PATH=.env.test node dist/db/migrate.js",
|
||||
"db:migrate:prod": "DOTENV_CONFIG_PATH=.env.production node dist/db/migrate.js",
|
||||
"db:shell": "docker compose exec mysql mysql -uaccess_user -paccess_pass access_manage",
|
||||
"mysql:up": "docker compose up -d mysql",
|
||||
@@ -36,6 +39,5 @@
|
||||
"@types/node": "^25.9.1",
|
||||
"tsx": "^4.22.3",
|
||||
"typescript": "^6.0.3"
|
||||
},
|
||||
"packageManager": "pnpm@11.5.0"
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,2 +1,2 @@
|
||||
allowBuilds:
|
||||
esbuild: true
|
||||
onlyBuiltDependencies:
|
||||
- esbuild
|
||||
|
||||
@@ -55,6 +55,8 @@ export function createApp() {
|
||||
|
||||
return ok({
|
||||
status: "ok",
|
||||
environment: env.APP_ENV,
|
||||
environmentLabel: env.APP_ENV_LABEL,
|
||||
database: "up",
|
||||
now: new Date().toISOString(),
|
||||
});
|
||||
|
||||
+7
-1
@@ -7,6 +7,8 @@ const envSchema = z.object({
|
||||
NODE_ENV: z
|
||||
.enum(["local", "development", "test", "production"])
|
||||
.default("development"),
|
||||
APP_ENV: z.enum(["local", "development", "test", "production"]).optional(),
|
||||
APP_ENV_LABEL: z.string().min(1).optional(),
|
||||
PORT: z.coerce.number().int().positive().default(3000),
|
||||
|
||||
DB_HOST: z.string().min(1),
|
||||
@@ -30,4 +32,8 @@ if (!result.success) {
|
||||
}
|
||||
|
||||
// 其他模块只从 env 读取已校验过的值,不再直接访问 process.env。
|
||||
export const env = result.data;
|
||||
export const env = {
|
||||
...result.data,
|
||||
APP_ENV: result.data.APP_ENV ?? result.data.NODE_ENV,
|
||||
APP_ENV_LABEL: result.data.APP_ENV_LABEL ?? result.data.NODE_ENV
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user