Commit 52014d4ee2a9ef97a04e369a2bf7ffcf5679247e

Authored by zichun
1 parent 5babb654

plan: consolidate all config into config-vars.yaml (drop .env.local + DB guards)

- config-vars.yaml now holds全部 config incl. secrets (database/admin_init.password/secrets), committed
- delete env-local-template; setup-test-db reads config-vars.yaml; gitignore no longer ignores .env.local
- docs-07 single-store rule; db-init/db-design-gen read config-vars.yaml database: section
- drop test_db_allowed_hosts whitelist + schema-looks-like-test guard (trust config-vars.yaml)
skills/plan/db-design-gen/SKILL.md
@@ -47,7 +47,7 @@ allowed-tools: Read Write Edit Grep Glob AskUserQuestion @@ -47,7 +47,7 @@ allowed-tools: Read Write Edit Grep Glob AskUserQuestion
47 47
48 ### C. 渲染 docs/03 48 ### C. 渲染 docs/03
49 49
50 -1. 读取 `${CLAUDE_SKILL_DIR}/templates/docs-03-header-template.md`,填充 `schema_name`(从 `.env.local` 读 `DB_SCHEMA`,无则填 `【人工填写:DB_SCHEMA】`)、`er_overview`(纯文本 ER 概览),以及步骤 A0 确认的 `{{PK_CONVENTION}}` / `{{TENANT_COLS}}` / `{{COL_PREFIX_RULE}}`。 50 +1. 读取 `${CLAUDE_SKILL_DIR}/templates/docs-03-header-template.md`,填充 `schema_name`(从 `config-vars.yaml` 读 `database.schema`,无则填 `【人工填写:database.schema】`)、`er_overview`(纯文本 ER 概览),以及步骤 A0 确认的 `{{PK_CONVENTION}}` / `{{TENANT_COLS}}` / `{{COL_PREFIX_RULE}}`。
51 2. 渲染「表清单」:对每张表:读取并填充 `${CLAUDE_SKILL_DIR}/templates/docs-03-table-template.md`,其中标准列区块用步骤 A0 确认的 `{{PK_CONVENTION}}` / `{{TENANT_COLS}}` 展开(`TENANT_COLS` = 「无」时不输出租户列行)。 51 2. 渲染「表清单」:对每张表:读取并填充 `${CLAUDE_SKILL_DIR}/templates/docs-03-table-template.md`,其中标准列区块用步骤 A0 确认的 `{{PK_CONVENTION}}` / `{{TENANT_COLS}}` 展开(`TENANT_COLS` = 「无」时不输出租户列行)。
52 3. 写入 `docs/03-数据库设计文档.md`。 52 3. 写入 `docs/03-数据库设计文档.md`。
53 53
skills/plan/db-init/SKILL.md
1 --- 1 ---
2 name: db-init 2 name: db-init
3 -description: A4 DB 初始化——LLM 解析 docs/03-数据库设计文档.md → 生成 sql/migrations/V1__initial_schema.sql(DDL only,Flyway 初始 migration)→ 用 lib/validate-ddl.mjs 全量校验 DDL ↔ docs/03 一致性 → 验证 MySQL 连接 → 调 scripts/setup-test-db.mjs 复用三层防护并 DROP+CREATE 空库 → 用 lib/apply-ddl.mjs apply V1。 3 +description: A4 DB 初始化——LLM 解析 docs/03-数据库设计文档.md → 生成 sql/migrations/V1__initial_schema.sql(DDL only,Flyway 初始 migration)→ 用 lib/validate-ddl.mjs 全量校验 DDL ↔ docs/03 一致性 → 验证 MySQL 连接 → 调 scripts/setup-test-db.mjs DROP+CREATE 空库 → 用 lib/apply-ddl.mjs apply V1。
4 user-invocable: false 4 user-invocable: false
5 allowed-tools: Read Write Edit Glob Skill Bash(node *) 5 allowed-tools: Read Write Edit Glob Skill Bash(node *)
6 --- 6 ---
@@ -56,30 +56,39 @@ node "${CLAUDE_PLUGIN_ROOT}/lib/validate-ddl.mjs" \ @@ -56,30 +56,39 @@ node "${CLAUDE_PLUGIN_ROOT}/lib/validate-ddl.mjs" \
56 56
57 ### B. 数据库环境检查 57 ### B. 数据库环境检查
58 58
59 -用 `Glob` 确认 `.env.local` 存在(不存在 → 提示重跑 A2 `skeleton-gen` 并停下)。用 `Read` 逐行解析 `KEY=VALUE`(跳过空行 / `#` 注释,**不做 shell-source / 变量展开**),校验 `DB_HOST` / `DB_PORT` / `DB_USER` / `DB_PASSWORD` / `DB_SCHEMA` 5 项均非空——任一缺失 → 打印缺失字段并停下。 59 +用 `Glob` 确认 `config-vars.yaml` 存在(不存在 → 提示重跑 A1 `scope-lock` 并停下)。用 `Read` 读其 `database:` 段,校验 `host` / `port` / `user` / `password` / `schema` 5 项均非空且非 `【人工填写` 占位——任一缺失 → 打印缺失字段并停下。
60 60
61 -用解析出的值跑连通性自检。必须用 Node `spawnSync('mysql', args, {shell:false})`,不要把密码拼进 shell 命令;空密码也要传 `--password=`,避免 `mysql -p` 进入交互式等待。 61 +用解析出的值跑连通性自检。必须用 Node `spawnSync('mysql', args, {shell:false})`,不要把密码拼进 shell 命令;空密码也要传 `--password=`,避免 `mysql -p` 进入交互式等待。下面的 `node -e` 内联了与 `lib/yaml-config.mjs` 同规则的极简 YAML 读取(2 层 map + 标量),仅读 `database:` 段:
62 62
63 ```bash 63 ```bash
64 node -e ' 64 node -e '
65 const { spawnSync } = require("node:child_process"); 65 const { spawnSync } = require("node:child_process");
66 const { readFileSync } = require("node:fs"); 66 const { readFileSync } = require("node:fs");
67 -const env = {};  
68 -for (const raw of readFileSync(".env.local", "utf8").split(/\r?\n/)) {  
69 - const line = raw.trim();  
70 - if (!line || line.startsWith("#")) continue;  
71 - const eq = line.indexOf("=");  
72 - if (eq < 0) continue;  
73 - const key = line.slice(0, eq).trim();  
74 - let value = line.slice(eq + 1).trim();  
75 - if (value.length >= 2 && ((value[0] === "\"" && value.at(-1) === "\"") || (value[0] === "'"'"'" && value.at(-1) === "'"'"'"))) value = value.slice(1, -1);  
76 - env[key] = value; 67 +const SQ = String.fromCharCode(39), DQ = String.fromCharCode(34);
  68 +function parseScalar(raw) {
  69 + let s = String(raw).trim();
  70 + if (s === "" || s[0] === "#") return "";
  71 + const q = s[0];
  72 + if (q === SQ || q === DQ) { const e = s.indexOf(q, 1); if (e !== -1) return s.slice(1, e); }
  73 + const h = s.indexOf(" #"); if (h !== -1) s = s.slice(0, h).trim();
  74 + return s;
77 } 75 }
  76 +const cfg = {}; let sec = null;
  77 +for (const raw of readFileSync("config-vars.yaml", "utf8").split(/\r?\n/)) {
  78 + const t = raw.trim(); if (!t || t[0] === "#") continue;
  79 + const c = raw.indexOf(":"); if (c < 0) continue;
  80 + const k = raw.slice(0, c).trim(); if (!k) continue;
  81 + const ind = raw.length - raw.replace(/^\s+/, "").length;
  82 + const v = parseScalar(raw.slice(c + 1));
  83 + if (ind === 0) { if (v === "") { sec = {}; cfg[k] = sec; } else { cfg[k] = v; sec = null; } }
  84 + else if (sec) { sec[k] = v; }
  85 +}
  86 +const db = cfg.database || {};
78 const args = [ 87 const args = [
79 - `--host=${env.DB_HOST}`,  
80 - `--port=${env.DB_PORT || "3306"}`,  
81 - `--user=${env.DB_USER}`,  
82 - `--password=${env.DB_PASSWORD || ""}`, 88 + `--host=${db.host}`,
  89 + `--port=${db.port || "3306"}`,
  90 + `--user=${db.user}`,
  91 + `--password=${db.password || ""}`,
83 "-e", 92 "-e",
84 "SELECT 1;" 93 "SELECT 1;"
85 ]; 94 ];
@@ -90,7 +99,7 @@ process.exit(r.status === null ? 1 : r.status); @@ -90,7 +99,7 @@ process.exit(r.status === null ? 1 : r.status);
90 99
91 成功 → 进入步骤 C;失败 → 打印具体错误(认证 / 主机不可达 / 端口拒接)并停下。 100 成功 → 进入步骤 C;失败 → 打印具体错误(认证 / 主机不可达 / 端口拒接)并停下。
92 101
93 -勾选:` - [ ] .env.local 凭据已验证(mysql -e "SELECT 1" OK)` 102 +勾选:` - [ ] config-vars.yaml DB 凭据已验证(mysql -e "SELECT 1" OK)`
94 103
95 ### C. 自动导入 MySQL 104 ### C. 自动导入 MySQL
96 105
@@ -102,10 +111,10 @@ node scripts/setup-test-db.mjs @@ -102,10 +111,10 @@ node scripts/setup-test-db.mjs
102 111
103 #### C.2 把 V1 灌入已清空的 schema 112 #### C.2 把 V1 灌入已清空的 schema
104 113
105 -调 `${CLAUDE_PLUGIN_ROOT}/lib/apply-ddl.mjs`:它用纯 JS 解析 `.env.local`(**不** shell-source,消除注入),复用 host 白名单 + schema 名安全闸,再经 mysql2 把 DDL 灌入 schema。 114 +调 `${CLAUDE_PLUGIN_ROOT}/lib/apply-ddl.mjs`:它用纯 JS 解析 `config-vars.yaml` 的 `database:` 段(**不** shell-source,消除注入),再经 mysql2 把 DDL 灌入 schema。
106 115
107 ```bash 116 ```bash
108 -node "${CLAUDE_PLUGIN_ROOT}/lib/apply-ddl.mjs" .env.local sql/migrations/V1__initial_schema.sql 117 +node "${CLAUDE_PLUGIN_ROOT}/lib/apply-ddl.mjs" config-vars.yaml sql/migrations/V1__initial_schema.sql
109 ``` 118 ```
110 119
111 退出码与处理: 120 退出码与处理:
@@ -113,7 +122,7 @@ node &quot;${CLAUDE_PLUGIN_ROOT}/lib/apply-ddl.mjs&quot; .env.local sql/migrations/V1__ini @@ -113,7 +122,7 @@ node &quot;${CLAUDE_PLUGIN_ROOT}/lib/apply-ddl.mjs&quot; .env.local sql/migrations/V1__ini
113 - `1` → 失败:打印 stderr 并停下 122 - `1` → 失败:打印 stderr 并停下
114 - `2` → 用法错(路径找不到),打印路径并停下 123 - `2` → 用法错(路径找不到),打印路径并停下
115 124
116 -勾选:` - [ ] setup-test-db.mjs 防护通过 + DROP+CREATE + apply V1 已执行` 125 +勾选:` - [ ] setup-test-db.mjs DROP+CREATE + apply V1 已执行`
117 126
118 ### D. 勾选 docs/08 进度 + 进入 A5 127 ### D. 勾选 docs/08 进度 + 进入 A5
119 128
@@ -126,7 +135,8 @@ node &quot;${CLAUDE_PLUGIN_ROOT}/lib/apply-ddl.mjs&quot; .env.local sql/migrations/V1__ini @@ -126,7 +135,8 @@ node &quot;${CLAUDE_PLUGIN_ROOT}/lib/apply-ddl.mjs&quot; .env.local sql/migrations/V1__ini
126 ## 参考 135 ## 参考
127 136
128 - `${CLAUDE_PLUGIN_ROOT}/lib/validate-ddl.mjs`(A.3 docs/03 ↔ V1.sql 5 维一致性校验,跨平台纯 Node) 137 - `${CLAUDE_PLUGIN_ROOT}/lib/validate-ddl.mjs`(A.3 docs/03 ↔ V1.sql 5 维一致性校验,跨平台纯 Node)
129 -- `${CLAUDE_PLUGIN_ROOT}/lib/apply-ddl.mjs`(C.2 安全解析 .env.local + mysql2 灌入 DDL,不 shell-source) 138 +- `${CLAUDE_PLUGIN_ROOT}/lib/apply-ddl.mjs`(C.2 安全解析 config-vars.yaml 的 database: 段 + mysql2 灌入 DDL,不 shell-source)
  139 +- `${CLAUDE_PLUGIN_ROOT}/lib/yaml-config.mjs`(apply-ddl 依赖的极简 YAML 读取)
130 - `docs/03-数据库设计文档.md`(DDL 翻译输入,SSoT) 140 - `docs/03-数据库设计文档.md`(DDL 翻译输入,SSoT)
131 -- `.env.local`(DB 凭据 141 +- `config-vars.yaml`(DB 凭据,A1 产出
132 - 产物:`sql/migrations/V1__initial_schema.sql`(由 Flyway 在 Spring Boot 启动时验证 / apply) 142 - 产物:`sql/migrations/V1__initial_schema.sql`(由 Flyway 在 Spring Boot 启动时验证 / apply)
skills/plan/scope-lock/templates/config-vars-template.yaml
1 -# config-vars.yaml — non-sensitive project config; rules live in docs/07.  
2 -# Sensitive values go in .env.local; only list key names under secrets_ref. 1 +# config-vars.yaml — 项目全部配置(含敏感凭据)。随项目提交,内部 git 传播。
  2 +# 工具脚本(apply-ddl / setup-test-db)运行时按 2 层 map 解析此文件。
  3 +# 值含 : / # / 空格 / $ / 引号等特殊字符时,用单引号包裹整个值:password: 'p@ss: w0rd#1'
3 4
4 backend: 5 backend:
5 - base_package: 【人工填写:后端根包名 / 命名空间,如 com.acme.erp】 6 + base_package: com.xly.erp
6 http_port: 【人工填写:后端 HTTP 端口,默认 8080】 7 http_port: 【人工填写:后端 HTTP 端口,默认 8080】
7 8
8 frontend: 9 frontend:
9 - pkg_name: 【人工填写:前端包名,如 acme-erp-web】 10 + pkg_name: xly-erp-web
10 dev_port: 【人工填写:前端开发服务器端口,默认 5173】 11 dev_port: 【人工填写:前端开发服务器端口,默认 5173】
11 12
  13 +database:
  14 + host: 【人工填写:MySQL host,推荐 localhost】
  15 + port: 【人工填写:MySQL port,默认 3306】
  16 + user: 【人工填写:开发账号名】
  17 + password: 【人工填写:对应密码,含特殊字符时用单引号包裹】
  18 + schema: 【人工填写:schema 名,推荐含 test/_dev/_local,例如 erp_dev】
  19 +
12 admin_init: 20 admin_init:
13 - username: 【人工填写:超级管理员初始账号,如 admin】  
14 - # 初始密码属敏感 → 见 .env.local 的 ADMIN_INIT_PASSWORD 21 + username: admin
  22 + password: 666666
15 23
16 -secrets_ref:  
17 - - DB_PASSWORD # 数据库密码  
18 - - JWT_SECRET # JWT / 令牌签名密钥  
19 - # - REDIS_PASSWORD # 缓存 / 会话(用 Redis 时)  
20 - # - ADMIN_INIT_PASSWORD # 超级管理员初始密码(有初始账号时)  
21 - # - OSS_ACCESS_KEY_SECRET / SMS_API_SECRET ... # 第三方凭证按需添加 24 +secrets:
  25 + jwt_secret: 【人工填写:JWT 签名密钥,256+ bit 随机串】
  26 + # 项目专属凭据按需取消注释 / 追加,直接填真实值:
  27 + # redis_password: 【人工填写:Redis 密码(用 Redis 时)】
  28 + # oss_access_key_secret: 【人工填写:对象存储密钥】
  29 + # sms_api_secret: 【人工填写:短信网关密钥】
skills/plan/skeleton-gen/templates/docs-07-env-template.md
@@ -6,14 +6,15 @@ @@ -6,14 +6,15 @@
6 6
7 ## 三、配置与凭据规则 7 ## 三、配置与凭据规则
8 8
9 -项目配置分两处存放,**本文档只记规则、不记具体值**: 9 +项目**全部配置**(含敏感凭据)统一存放在仓库根 `config-vars.yaml`,结构化 YAML,随项目提交(内部 git 传播)。**本文档只记规则、不记具体值**:
10 10
11 -- **非敏感、项目级配置**(根包名 / 命名空间、应用端口、前端包名、管理员初始账号等)→ 仓库根 `config-vars.yaml`,结构化 YAML,随项目提交。  
12 -- **敏感凭据**(数据库密码、JWT / 签名密钥、Redis 密码、第三方 key/secret、管理员初始密码等)→ 仓库根 `.env.local`,入 `.gitignore`,**不提交**;`config-vars.yaml` 末尾 `secrets_ref` 只登记键名作引用。 11 +- **非敏感、项目级配置**(根包名 / 命名空间、应用端口、前端包名、管理员初始账号等)→ `config-vars.yaml` 对应段。
  12 +- **敏感凭据**(数据库密码、JWT / 签名密钥、Redis 密码、第三方 key/secret、管理员初始密码等)→ `config-vars.yaml` 的 `database` / `admin_init.password` / `secrets` 段,直接填真实值。
13 13
14 规则: 14 规则:
15 - 根包名 / 命名空间一经在 `config-vars.yaml` 锁定,全项目复用,不得各模块各写。 15 - 根包名 / 命名空间一经在 `config-vars.yaml` 锁定,全项目复用,不得各模块各写。
16 - 端口遵循 § 二 约定;调整时改 `config-vars.yaml`,本文档不写具体端口。 16 - 端口遵循 § 二 约定;调整时改 `config-vars.yaml`,本文档不写具体端口。
17 -- 任何敏感值不得出现在 `config-vars.yaml`、docs、源码或日志中——只允许出现在 `.env.local`。 17 +- 任何配置值(含敏感值)只允许出现在 `config-vars.yaml`,不得散落在 docs、源码或日志中。
  18 +- 工具脚本(apply-ddl / setup-test-db)运行时按 2 层 map 解析 `config-vars.yaml`;值含特殊字符时用单引号包裹。
18 19
19 ## 四、常用命令 20 ## 四、常用命令
skills/plan/skeleton-gen/templates/env-local-template deleted
1 -# Local dev credentials — gitignored.  
2 -# Quote values containing $/space/!/backtick with single quotes: DB_PASSWORD='p@ss$w0rd!'  
3 -  
4 -DB_HOST=【人工填写:MySQL host,推荐 localhost】  
5 -DB_PORT=【人工填写:MySQL port,默认 3306】  
6 -DB_USER=【人工填写:开发账号名】  
7 -DB_PASSWORD=【人工填写:对应密码,含特殊字符时用单引号包裹】  
8 -DB_SCHEMA=【人工填写:schema 名,推荐含 test/_dev/_local,例如 erp_dev】  
9 -JWT_SECRET=【人工填写:JWT 签名密钥,256+ bit 随机串】  
10 -  
11 -# 可选:额外允许 DROP CREATE 的远程 host(空格或逗号分隔)。  
12 -TEST_DB_ALLOWED_HOSTS=  
skills/plan/skeleton-gen/templates/gitignore-append-template
1 # ==== ERP 插件推荐忽略项(skeleton-gen 追加) ==== 1 # ==== ERP 插件推荐忽略项(skeleton-gen 追加) ====
2 -# 本地运行时配置(含真实凭据,严禁入库)  
3 -.env.local  
4 -.env.*.local 2 +# 注:项目配置(含凭据)统一在 config-vars.yaml,随项目提交(内部 git 传播),不在此忽略。
5 3
6 # Java / Maven 4 # Java / Maven
7 target/ 5 target/
skills/plan/skeleton-gen/templates/scripts-setup-test-db-template.mjs
1 #!/usr/bin/env node 1 #!/usr/bin/env node
2 -// scripts/setup-test-db.mjs — DROP + CREATE 测试库。 2 +// scripts/setup-test-db.mjs — DROP + CREATE 测试库。
3 // 由 coding.mjs 的 test-gate 调用;schema 由 Flyway 在 Spring Boot 启动时重放。 3 // 由 coding.mjs 的 test-gate 调用;schema 由 Flyway 在 Spring Boot 启动时重放。
4 -// 只允许本地 host(或 TEST_DB_ALLOWED_HOSTS 白名单内的 host)+ 测试库名(含 test/_dev/_local/_ci)。 4 +// DB 凭据从仓库根 config-vars.yaml 的 database: 段读取(host / schema 完全信任该文件,无额外校验)。
5 5
6 import { spawnSync } from 'node:child_process' 6 import { spawnSync } from 'node:child_process'
7 import { existsSync, readFileSync } from 'node:fs' 7 import { existsSync, readFileSync } from 'node:fs'
@@ -9,86 +9,70 @@ import { dirname, join } from &#39;node:path&#39; @@ -9,86 +9,70 @@ import { dirname, join } from &#39;node:path&#39;
9 import { fileURLToPath } from 'node:url' 9 import { fileURLToPath } from 'node:url'
10 10
11 const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url)) 11 const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url))
12 -const ENV_FILE = join(SCRIPT_DIR, '..', '.env.local') 12 +const CONFIG_FILE = join(SCRIPT_DIR, '..', 'config-vars.yaml')
13 13
14 -function parseEnv(text) {  
15 - const env = {}  
16 - for (const rawLine of text.split(/\r?\n/)) {  
17 - const line = rawLine.trim()  
18 - if (line === '' || line.startsWith('#')) continue  
19 - const eq = line.indexOf('=')  
20 - if (eq === -1) continue  
21 - const key = line.slice(0, eq).trim()  
22 - if (!key) continue  
23 - let value = line.slice(eq + 1).trim()  
24 - if (  
25 - value.length >= 2 &&  
26 - ((value.startsWith("'") && value.endsWith("'")) ||  
27 - (value.startsWith('"') && value.endsWith('"')))  
28 - ) {  
29 - value = value.slice(1, -1) 14 +// 极简 YAML 读取(2 层 map + 标量;与插件 lib/yaml-config.mjs 同规则,内联以免运行时依赖)。
  15 +function parseScalar(raw) {
  16 + let s = String(raw).trim()
  17 + if (s === '' || s[0] === '#') return ''
  18 + const q = s[0]
  19 + if (q === '"' || q === "'") {
  20 + const end = s.indexOf(q, 1)
  21 + if (end !== -1) return s.slice(1, end)
  22 + }
  23 + const hash = s.indexOf(' #')
  24 + if (hash !== -1) s = s.slice(0, hash).trim()
  25 + return s
  26 +}
  27 +function parseYamlConfig(text) {
  28 + const root = {}
  29 + let section = null
  30 + for (const rawLine of text.split('\n')) {
  31 + const line = rawLine.replace(/\r$/, '')
  32 + const trimmed = line.trim()
  33 + if (trimmed === '' || trimmed[0] === '#') continue
  34 + const colon = line.indexOf(':')
  35 + if (colon === -1) continue
  36 + const key = line.slice(0, colon).trim()
  37 + if (key === '') continue
  38 + const indent = line.length - line.replace(/^\s+/, '').length
  39 + const value = parseScalar(line.slice(colon + 1))
  40 + if (indent === 0) {
  41 + if (value === '') {
  42 + section = {}
  43 + root[key] = section
  44 + } else {
  45 + root[key] = value
  46 + section = null
  47 + }
  48 + } else if (section) {
  49 + section[key] = value
  50 + } else {
  51 + root[key] = value
30 } 52 }
31 - env[key] = value  
32 } 53 }
33 - return env 54 + return root
34 } 55 }
35 56
36 -if (!existsSync(ENV_FILE)) {  
37 - console.error(`[setup-test-db] .env.local 不存在(${ENV_FILE})`) 57 +if (!existsSync(CONFIG_FILE)) {
  58 + console.error(`[setup-test-db] config-vars.yaml 不存在(${CONFIG_FILE})`)
38 process.exit(1) 59 process.exit(1)
39 } 60 }
40 61
41 -const env = parseEnv(readFileSync(ENV_FILE, 'utf8')) 62 +const db = parseYamlConfig(readFileSync(CONFIG_FILE, 'utf8')).database || {}
42 63
43 -const DB_HOST = env.DB_HOST ?? ''  
44 -const DB_PORT = env.DB_PORT ?? '3306'  
45 -const DB_USER = env.DB_USER ?? ''  
46 -const DB_PASSWORD = env.DB_PASSWORD ?? ''  
47 -const DB_SCHEMA = env.DB_SCHEMA ?? '' 64 +const DB_HOST = db.host ?? ''
  65 +const DB_PORT = db.port ?? '3306'
  66 +const DB_USER = db.user ?? ''
  67 +const DB_PASSWORD = db.password ?? ''
  68 +const DB_SCHEMA = db.schema ?? ''
48 69
49 if (!/^\d+$/.test(DB_PORT) || Number(DB_PORT) <= 0 || Number(DB_PORT) > 65535) { 70 if (!/^\d+$/.test(DB_PORT) || Number(DB_PORT) <= 0 || Number(DB_PORT) > 65535) {
50 - console.error(`[setup-test-db] DB_PORT 非法: ${DB_PORT}(必须是 1..65535 的整数)`)  
51 - process.exit(1)  
52 -}  
53 -  
54 -if (!/^[A-Za-z0-9_]+$/.test(DB_SCHEMA)) {  
55 - console.error(`[setup-test-db] DB_SCHEMA 只能包含字母、数字、下划线,当前为: ${DB_SCHEMA}`)  
56 - process.exit(1)  
57 -}  
58 -  
59 -// 防护 1:默认只允许本地 host(localhost / 127.0.0.1 / ::1)。  
60 -// 额外允许的远程 host 在 .env.local 的 TEST_DB_ALLOWED_HOSTS 中(空格或逗号分隔)。  
61 -const extraHosts = (env.TEST_DB_ALLOWED_HOSTS ?? '')  
62 - .split(/[\s,]+/)  
63 - .filter(Boolean)  
64 -const allowedHosts = ['localhost', '127.0.0.1', '::1', ...extraHosts]  
65 -if (!allowedHosts.includes(DB_HOST)) {  
66 - console.error(`[setup-test-db] 拒绝在非白名单 host (${DB_HOST}) 上执行 DROP DATABASE`)  
67 - console.error(` 当前白名单:${allowedHosts.join(' ')}`)  
68 - console.error(' 加入 host:在 .env.local 追加 TEST_DB_ALLOWED_HOSTS="<host1> <host2>"')  
69 - process.exit(1)  
70 -}  
71 -  
72 -// 防护 2:schema 名需像测试/开发库(含 test / _dev / _local / _ci),否则拒绝。  
73 -const schemaLooksLikeTest =  
74 - /test/.test(DB_SCHEMA) || /_dev$/.test(DB_SCHEMA) || /_local$/.test(DB_SCHEMA) || /_ci$/.test(DB_SCHEMA)  
75 -if (!schemaLooksLikeTest) {  
76 - console.error(  
77 - `[setup-test-db] schema '${DB_SCHEMA}' 不像测试库(期望命名含 test / _dev / _local / _ci)`  
78 - ) 71 + console.error(`[setup-test-db] database.port 非法: ${DB_PORT}(必须是 1..65535 的整数)`)
79 process.exit(1) 72 process.exit(1)
80 } 73 }
81 74
82 console.log(`[setup-test-db] 即将 DROP + CREATE \`${DB_SCHEMA}\` on ${DB_HOST}:${DB_PORT}`) 75 console.log(`[setup-test-db] 即将 DROP + CREATE \`${DB_SCHEMA}\` on ${DB_HOST}:${DB_PORT}`)
83 -if (!['localhost', '127.0.0.1', '::1'].includes(DB_HOST)) {  
84 - console.log(  
85 - '[setup-test-db] 目标是 **远程** host(已在 TEST_DB_ALLOWED_HOSTS 白名单中,每次 test.mjs 都会 DROP)'  
86 - )  
87 - console.log(`[setup-test-db] 当前白名单: ${allowedHosts.join(' ')}`)  
88 - console.log(  
89 - '[setup-test-db] 若不希望每次自动 DROP,从 .env.local 的 TEST_DB_ALLOWED_HOSTS 删掉此 host'  
90 - )  
91 -}  
92 76
93 const sql = 77 const sql =
94 `DROP DATABASE IF EXISTS \`${DB_SCHEMA}\`; ` + 78 `DROP DATABASE IF EXISTS \`${DB_SCHEMA}\`; ` +