# SQL（字段 `sqlFile`，数组）

## 用户提供
- **SQL 全文**：每条以英文 `;` 结尾；注释以 `-- `（带空格）开头；确认是否涉及防火墙。
- 数据库类型 / 地址 / 库名可以不给——按下面的取值逻辑自己查。

---

# ⛔ 新增表结构 → 必须带上建表语句（DDL）

本次代码**新建了表**，`sqlContent` 里就**必须包含该表的 `CREATE TABLE`**。
漏了建表语句，上线时应用一跑就报 `Table doesn't exist`——这是 SQL 页签最常见的事故，务必主动检查。

**建表语句来源优先级：① 测试环境真实结构（`SHOW CREATE TABLE`）→ ② 仓库迁移脚本 → ③ 从代码生成**（详见第二节）。越靠近「应用真实跑的结构」越优先，能取到上层的就别自己生成。

## 一、怎么发现「新增了表」

扫本分支自己触碰的文件（见 [../reference/change-detection.md](../reference/change-detection.md)），命中任一信号就按新增表处理：

| 信号 | 例 |
|---|---|
| 新增实体类带表名注解 | `@TableName("t_xxx")`（MyBatis-Plus）、`@Table(name="t_xxx")`（JPA） |
| 新增 Mapper / DAO 接口 + 对应 XML | `XxxMapper.java` + `XxxMapper.xml` |
| Mapper XML / SQL 注解里出现的表名，在**已有**代码里从没出现过 | `select * from t_new_biz_log` |
| 新增迁移脚本本身就含 `CREATE TABLE` | `V20260821__create_t_xxx.sql` |
| 新增 Mongo 集合、Doris/TDengine 表等非关系型的建表/建集合 | `@Document("c_xxx")` |

**判断口径**：以「表名在本次改动之前的代码里是否出现过」为准（`git log -S "t_xxx" origin/master` 查这个表名是不是本次才引入的）。仍分不清就问用户。

## 二、建表语句从哪来

按优先级依次尝试：**① 测试环境真实结构 → ② 仓库迁移脚本 → ③ 从代码生成**。

### 1. 表已在测试环境建好 → `SHOW CREATE TABLE` 直接取原文（首选）

新增的表大概率开发已在测试库建过（手动建的，或部署时 Flyway 跑过）。表在库里，就以库里的为真——
这是应用真正跑通的结构，比任何生成都可靠，索引、字符集、注释一次带全。

```powershell
# 1) 先确认表在不在（datasource_id 用 -ListDatasources 列出后按库名匹配，库名依据见第四节「取值逻辑 ①」）
powershell -ExecutionPolicy Bypass -File scripts\query-db.ps1 -Environment test4 -Datasource <ds> `
    -Sql "SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_NAME='t_new_biz_log'"

# 2) 在（count>0）→ 直接取完整建表语句（中文注释完好，已实测）
powershell -ExecutionPolicy Bypass -File scripts\query-db.ps1 -Environment test4 -Datasource <ds> `
    -Sql "SHOW CREATE TABLE t_new_biz_log"
```

取到后做**三件后处理**再进 `sqlContent`：
- **剥掉 `AUTO_INCREMENT=<当前值>`**——那是测试环境的自增现值，带进生产会把自增起点带偏；
- **开头改成 `CREATE TABLE IF NOT EXISTS`**；
- 摘要里这条标 **「建表语句取自测试环境真实结构」**（属 `已自动填`，无需 DBA 重点复核）。

count=0（表没建）或 `DB_SKIP`（sql-console 不通）→ 软失败、不中断，继续走第 2、3 步。
> ⚠️ 这句必须走**本 skill 自带的** `query-db.ps1`（白名单版）；别用 log-analysis 的，它的黑名单正则 `\bCREATE\b` 会误拦。

### 2. 仓库里的迁移脚本（测试环境没建、仓库有 → 有就不用生成）

`db/migration`、`db/changelog`、`src/main/resources/sql`、任意 `*.sql` 里已有该表的 `CREATE TABLE`
→ **直接取原文，一个字都不要改**。找的时候按表名全仓搜，别只看本次改动的文件。
> 若第 1 步已从库里取到、且与脚本不一致：**以库里的为准**（那是应用真正跑通的结构），并把差异点告诉用户。

### 3. 没有现成脚本 → 从代码生成
**依据三样东西：① 实体类　② Mapper XML　③ 同库其他表的既有 DDL。** 不需要连测试环境。

#### ① 列的来源（三处合并，取并集）

| 来源 | 取什么 |
|---|---|
| 实体类字段 | 字段名 → 下划线列名（`createTime` → `create_time`）；`@TableField("xxx")` / `@Column(name=)` 显式指定时以注解为准；`@TableField(exist=false)` 的字段**不建列** |
| Mapper XML `<resultMap>` | `<id column=>` / `<result column=>` 的 column 值——这是最可靠的列名来源，优先于字段名推导 |
| XML 里的 SQL 语句 | `insert` 的列清单、`where` 条件里出现的列——用来兜底补漏，也用来定索引 |

三处不一致（如实体有字段但 resultMap 没有）→ **以 resultMap 为准**，并在 DDL 里对多出来的列加注释标出。

#### ② 类型与长度

Java 类型 → SQL 类型的映射：

| Java | MySQL |
|---|---|
| `String` | `varchar(n)`（n 见下）；明确是长文本/JSON → `text` / `json` |
| `Integer` / `int` | `int` |
| `Long` / `long` | `bigint(20)` |
| `BigDecimal` | `decimal(p,s)`——金额默认 `decimal(20,2)`，费率类 `decimal(10,6)` |
| `Boolean` | `tinyint(1)` |
| `Date` / `LocalDateTime` | `datetime` |
| `LocalDate` | `date` |
| `byte[]` | `blob` |
| 枚举 | 按存储形式：存 code 用 `varchar(n)`，存序号用 `tinyint` |

**长度按这个顺序定，不要直接拍脑袋**：

1. **注解里有就用注解**——`@Column(length=64)`、`@Size(max=64)`、`@Length(max=64)`、`@ApiModelProperty` 里写的长度；
2. **同库其他表的同名列**——`user_id`、`create_by`、`remark`、`status` 这类列，全库通常一个长度，照抄；
3. **同类语义的常见约定**——id/编号 `varchar(32)`、名称 `varchar(64)`、备注 `varchar(255)`、原因/描述 `varchar(500)`；
4. 以上都定不了 → 用一个保守值，并在该行末尾加注释 `-- 长度待确认`。

#### ③ 主键

- `@TableId(type = IdType.AUTO)` → `bigint(20) NOT NULL AUTO_INCREMENT`
- `@TableId(type = IdType.ASSIGN_ID)`（雪花）→ `bigint(20) NOT NULL`（字段是 `String` 时 `varchar(32) NOT NULL`）
- `@TableId` 无 type / JPA `@GeneratedValue` → 照抄**同库其他表主键的写法**
- 一律 `PRIMARY KEY (id)`

#### ④ 公共字段照抄同库其他表 —— 用参考表脚本，别凭印象

`create_time` / `update_time` / `create_by` / `update_by` / `del_flag` / `is_deleted` / `version` / `tenant_id` 这些
**不要自己定义**——拉同库任意一张既有表的真实结构，把类型、默认值、注释**原样搬过来**，保证全库一致。
（`@TableLogic`、`@Version`、`@TableField(fill = FieldFill.INSERT)` 能告诉你哪些字段属于这类。）

**怎么拉参考表**（只读，走内网 sql-console，无需数据库驱动、无需从 Nacos 解密码）：

```powershell
# 1. 找库对应的 datasource_id（返回 id/name/module/database，按库名匹配）
powershell -ExecutionPolicy Bypass -File scripts\query-db.ps1 -Environment test4 -ListDatasources

# 2. 拉一张同库既有表的完整结构，直接照抄
powershell -ExecutionPolicy Bypass -File scripts\table-struct.ps1 `
    -Environment test4 -Datasource centerAccount -Table t_acc_cancel_apply
```

[../scripts/table-struct.ps1](../scripts/table-struct.ps1) 会拼出可照抄的 `CREATE TABLE`，覆盖本节要用的全部信息：

```sql
-- 参考表：test4 / centerAccount / t_acc_cancel_apply （只读导出，用于照抄字段与索引风格）
CREATE TABLE `t_acc_cancel_apply` (
  `ID`          bigint(19)    NOT NULL COMMENT '主键',
  `NOTES`       varchar(1000) DEFAULT NULL COMMENT '备注',
  `CREATE_ID`   bigint(19)    DEFAULT NULL COMMENT '创建人',
  `CREATE_TIME` datetime      DEFAULT NULL COMMENT '创建时间',
  `UPDATE_ID`   bigint(19)    DEFAULT NULL COMMENT '更新人',
  `UPDATE_TIME` datetime      DEFAULT NULL COMMENT '更新时间',
  PRIMARY KEY (`ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
```

**参考表怎么选**：优先取**同库、同业务前缀**的表（如新表 `t_biz_log` → 参考 `t_biz_*`）；没有就取同库任意一张最近维护的表。
从它身上抄四样：**公共字段写法**（本例可见列名是全大写、时间字段用 `datetime`、人员字段用 `bigint(19)`）、**同名列的长度**、**索引命名风格**、**引擎/字符集/排序规则**（本例是 `utf8` 而非 `utf8mb4`——**照库里的实际值抄，别套用通用默认**）。

> ⚠️ 别用 `SHOW CREATE TABLE` 走 log-analysis 的 `query-db.ps1`：它的黑名单正则 `\bCREATE\b` 会把这句当破坏性语句误拦。
> 本 skill 自带的 `query-db.ps1` 改成了**首关键字白名单**（`SELECT`/`SHOW`/`DESC`/`DESCRIBE`/`EXPLAIN`），
> 放行结构查询、拦住 `UPDATE`/`CREATE`/`DROP`/多语句拼接/注释绕过（六项已实测），服务端另有 `sql_ddl` 权限门兜底。
> `SHOW CREATE TABLE` 走本 skill 脚本已实测通过：客户端白名单与服务端 `sql_ddl` 门均放行，中文注释完好返回
> （早期出现 `???` 是 PS 5.1 `Invoke-RestMethod` 按 Latin-1 解码响应所致，已在脚本里改为取原始字节按 UTF-8 解码）。

**连不上怎么办**：`query-db.ps1` 软失败（输出 `===DB_SKIP===` 后 `exit 0`），**不中断录制流程**——回到「按注解 → 按常见约定」定长度，并把该行标 `-- 长度待确认`，摘要里标 `建议核对`。

#### ⑤ 索引（从 XML 推，标注为建议）

- XML 里 `where` 用到的列、`selectByXxx` / `getByXxx` 方法名里的列 → **建普通索引**
- 业务上唯一的（编号、单号、code）→ `UNIQUE KEY`
- 外键列（`xxx_id`）→ 建索引
- 多列组合查询 → 按查询顺序建联合索引

生成的索引一律写成 `KEY idx_列名 (列名) -- 依据：XxxMapper.xml selectByOrderNo`，把依据写进注释，方便复核。

#### ⑥ 表级属性照抄同库其他表

引擎、字符集、排序规则**不要用自己的默认值**，打开同库任一张既有表的 DDL 抄下来，典型是：
`ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci`。
表注释取实体类的类注释 / `@ApiModel(description=)`。

#### ⑦ 生成结果必须标注

生成的 DDL **顶部加一行来源说明**，不确定的地方逐行标注，交用户/DBA 复核：

```sql
-- ⚠️ 本建表语句由代码生成（依据：XxxEntity.java + XxxMapper.xml + 同库 t_order 的既有 DDL），请复核
CREATE TABLE IF NOT EXISTS `t_new_biz_log` (
  `id`          bigint(20)   NOT NULL COMMENT '主键',
  `order_no`    varchar(32)  NOT NULL COMMENT '订单号',
  `biz_type`    varchar(32)  DEFAULT NULL COMMENT '业务类型',
  `remark`      varchar(255) DEFAULT NULL COMMENT '备注',        -- 长度待确认
  `create_time` datetime     DEFAULT NULL COMMENT '创建时间',     -- 照抄 t_order
  `update_time` datetime     DEFAULT NULL COMMENT '更新时间',     -- 照抄 t_order
  `del_flag`    tinyint(1)   DEFAULT '0' COMMENT '删除标记',      -- 照抄 t_order
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_order_no` (`order_no`),                        -- 依据：XxxMapper.xml selectByOrderNo
  KEY `idx_biz_type` (`biz_type`)                               -- 依据：XxxMapper.xml listByBizType
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='业务日志';
```

最终摘要里这条要标 **「建表语句已自动生成，建议 DBA 复核」**，不能当成已核实的内容交出去。

#### ⑧ 实在生成不了才问用户

实体类和 XML 都找不到（比如表名只出现在一句手写 SQL 里、纯 JDBC 代码）→ 把已掌握的线索列出来问用户要建表语句，不要凭表名硬造。

### 4. 生成完的可选校验
走到本步说明第 1 步查过表不在测试环境。若生成后表又被建好（开发手动建 / 部署跑出来了），拉真实结构和生成件比对：

```powershell
powershell -ExecutionPolicy Bypass -File scripts\table-struct.ps1 -Environment test4 -Datasource <ds> -Table t_new_biz_log
```

有差异**以库里的为准**（那是应用真正跑通的结构），并把差异点告诉用户。
> 另有 [../scripts/dump_ddl.py](../scripts/dump_ddl.py)（需 `pymysql` + 从 Nacos 取库密码）走直连数据库，作用相同；
> 优先用 `table-struct.ps1`——无需 Python、无需数据库密码。

## 三、DDL 要带全

从迁移脚本或 `SHOW CREATE TABLE` 取到的原文要**完整保留**，别只截 `CREATE TABLE` 主体：

- 主键、唯一键、**索引**（漏索引 = 上线后慢查询）
- 字符集与排序规则（`DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci`）
- 字段注释与表注释
- 初始化数据（字典表、状态表的 `INSERT`）——有就一起带上

建议加 `IF NOT EXISTS` 或在前面写一行 `-- ` 注释说明这是新表，便于 DBA 复核。

## 四、语句顺序：DDL 在前，DML 在后

同一条 `sqlContent` 里混有建表和数据变更时，**建表/改表语句必须排在前面**，否则后面的 `INSERT`/`UPDATE` 会因为表不存在而失败：

```sql
-- 1. 新增表 t_new_biz_log
CREATE TABLE IF NOT EXISTS t_new_biz_log ( ... ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='业务日志';
-- 2. 初始化字典数据
INSERT INTO t_dict (code, name) VALUES ('BIZ_LOG', '业务日志');
-- 3. 已有表加字段
ALTER TABLE t_order ADD COLUMN log_id BIGINT NULL COMMENT '日志id';
```

## 五、顺带检查：新增表通常还要配防火墙

新表如果在**本服务此前没连过的库**上，那不只是 SQL 页签的事——按 [firewall.md](firewall.md) 的触发判据，还要配一条到该库的放行规则。同库新增表则不用。

---

# 数据库类型 / 地址 / 库名 · 取值逻辑

## 一、原理

三个下拉是一棵**三级字典树**，逐级联动。每个下拉选中的是「字典节点 id」（**不是名字**），保存报文里存的就是这三个 id。

```
数据库类型 databaseTypeId
   └─(往下取子节点)→ 数据库地址 databaseAddrId
         └─(往下取子节点)→ 数据库库名 databaseNameId
```

**父级一变，子级作废重取。**

## 二、接口

| 级别 | 取选项的接口 | 取哪个字段 |
|---|---|---|
| ① 数据库类型 | `GET /zqyl-devops-base-api/base/data/getDictionary?type=FEEDBACK_TYPE` | 每项 `dicId`→`databaseTypeId`，`dicName`=显示名 |
| ② 数据库地址 | `GET /zqyl-devops-base-api/base/data/getDictionaryById?id={databaseTypeId}` | `dicId`→`databaseAddrId` |
| ③ 数据库库名 | `GET /zqyl-devops-base-api/base/data/getDictionaryById?id={databaseAddrId}` | `dicId`→`databaseNameId` |

②③ 是同一个 `getDictionaryById?id={上级id}`，逐级往下取子节点。都是 **GET（只读）**。

返回元素形如 `{"dicId":"6","dicName":"MySQL","dicCode":"MySQL","parentId":null}`。

## 三、一级字典 id（固定，直接用）

| 数据库类型 | typeId |
|---|---|
| Oracle | `5` |
| MySQL | `6` |
| TDengine | `3719232322302640624` |
| MongoDB | `3722025703533707760` |
| Doris | `3744652204355617552` |
| Redis | `3829440114744885780` |

二级（地址）、三级（库名）用上面 ②③ 接口自查。

## 四、每一级选哪个（接口只给候选，选择依据在这里）

上面的接口只负责**列出候选**，具体选哪一个按下面来：

### ① 数据库类型 —— 按实际代码写

依据来自**代码**，不是猜的。按「本地 → Nacos」找到 datasource 配置，再从 **jdbc url** 判定：

**a. 定位配置文件**
本地找不到连接串时（很常见，连接串通常不入库），去 `bootstrap.properties` / `bootstrap.yml` 的
`spring.cloud.nacos.config.ext-config[*].data-id` 里挑**含 `database` / `datasource` / `mysql` / `oracle` / `db` 关键字**的项，
记下它的 `data-id` 和 `group`；没有 `ext-config` 就再搜 `shared-dataids` / `extension-configs`。

**b. 拉配置**（namespace 用项目 `spring.cloud.nacos.config.namespace` 的值，未必等于测试环境名）
```bash
curl -s "http://{nacos-addr}/nacos/v1/cs/configs?show=all&dataId={dataId}&group={group}&tenant={ns}&namespaceId={ns}"
```

**c. 解析** —— 关注这些 key：

| 目标 | 常见 key |
|---|---|
| url | `spring.datasource.druid.url` / `spring.datasource.url` |
| user | `spring.datasource.druid.username` / `spring.datasource.username` |
| Mongo | `spring.data.mongodb.uri` / `spring.data.mongodb.database` |

**同一文件没有 url 就继续拉同 group 下其它 dataId**，别因为第一个文件没有就放弃。

**d. 判定类型**：dataId 或 url 含 `oracle` → Oracle；含 `mysql` → MySQL；`mongodb://` → MongoDB；其余同理。

**e. 顺带拿到库名** —— jdbc url 里的库名可以用来**交叉验证第 ③ 级选得对不对**：
- `jdbc:mysql://host:port/{db}?...`
- `jdbc:oracle:thin:@//host:port/{service}` ／ `@host:port:{sid}` ／ `@host:port/{service}`
- `mongodb://user:pwd@host:port/{db}`

一次交付同时改多种库 → 拆成多条 `sqlFile` 条目，一条一个库。

### ② 地址 + ③ 库名 —— 查该服务的历史交付单，抄历史值
地址字典里几十个条目，光看名字选不出来。**看这个服务以前是怎么配的**：

```bash
# 按 serverName 查该服务全部历史交付单（onlineTime 留空 = 不限时间）
GET /zqyl-devops-deliver-api/deliver/queryDeliverList
    ?page=1&pageRow=20&jiraCode=&projectName=&projectCode=
    &onlineTime=&serverName={SERVER_NAME}&configtypes=&deliverStatus=

# 对每个返回行的 id 取详情，读 sqlFile[*] 里的 id
GET /zqyl-devops-deliver-api/deliver/getDeliverDetail?deliverId={id}
```

- 历史各单**一致** → 直接沿用（最常见）。
- 历史**不一致** → 取与本次「数据库类型 + 目标表」匹配的那条；仍分不清就问用户，别赌。
- 该服务**一条历史单都没有** → 见下面第五节。

抄的时候 `databaseAddr`/`databaseAddrId`、`databaseName`/`databaseNameId` **成对使用、不要拆**；
抄完用 ②③ 接口回验一遍：id 确实存在，且父子关系对得上（`databaseNameId` 必须出现在 `getDictionaryById?id={databaseAddrId}` 的返回里）。

> `configtypes` 参数留空、拿到结果后自己过滤出有 `sqlFile` 的单即可（传 `configtypes=sqlFile` 是否真生效未验证，别依赖）。

## 五、⛔ 新库 / 历史没配过 → 停下来问用户

如果这个库（地址或库名）在该服务历史交付单里**从来没出现过**，说明是新库：

- **先保留**——`sqlFile` 该条**先不填地址和库名**（或整条先不加），SQL 全文可以先备好；
- **不要**自己去 `getDictionaryById` 的候选列表里挑一个看着像的填进去；
- **明确告诉用户**「这是新库，历史没配过，地址/库名等你指示」，然后停在这里等用户答复。

## 六、保存报文字段

```json
"sqlFile": [
  { "databaseTypeId": "6",
    "databaseAddrId": "<②查到的id>",
    "databaseNameId": "<③查到的id>",
    "sqlContent": "-- 变更说明\nUPDATE t SET a=1 WHERE id=2;",
    "dataFrom": 1 }
]
```

- `dataFrom`：`1`=手工录入（可编辑）／`2`=只读展示。
- SQL 规范：每条以 `;` 结尾；注释以 `-- `（带空格）开头。
- 三个 id 是必填项；同时带上对应的 `databaseType`/`databaseAddr`/`databaseName` 名字字段更稳妥（页面回显用）。
- 新增条目**不要带 `id`**，服务端会生成；改已有条目才保留其 `id`。
- 同一个库的多条语句：既可合并进一条的 `sqlContent`，也可拆成多条条目（页面显示成多张卡片）——按用户要求来，默认合并。

## 七、完整示例（MySQL → fc_mysql → fc_integration）

```
databaseTypeId = 6                       # getDictionary?type=FEEDBACK_TYPE 里 MySQL
databaseAddrId = 3819712382112891664     # getDictionaryById?id=6 里 fc_mysql
databaseNameId = 3819712845969359632     # getDictionaryById?id=3819712382112891664 里 fc_integration
```

同一个 `fc_mysql` 地址下还有 `fc_channel` / `fc_gateway` / `fc_credit` / `fc_payment` / `fc_basic_data` / `fc_file`——
**同地址不同库，只差第三级**，所以第三级必须按第四节的依据选，不能凭名字像就填。

已查证的服务实例：`fc-payment` 三张历史交付单（创建人 张三 / 李四 / 王五）的 `sqlFile` 库配置完全一致，
均为 `MySQL(6) → fc_mysql(3819712382112891664) → fc_payment(3819712820199555856)`。

---

## 可自动读
仓库有迁移脚本（Flyway / `*.sql` / `db/migration` / `src/main/resources/sql`）就抽 SQL 全文。
