Files
hsm-database-certs-compare/需求文档.md
T
cheney fe28ddeaac feat: HSM main/slave certificate comparison with batch SQL generation
- compare usercert_tab by subject raw bytes + certtype
- generate single annotated CSV report
- generate merge-<timestamp>.sql in current directory for DBA execution
- preserve GBK DN bytes via ATTACH + INSERT OR REPLACE / UPDATE
- compatible with sqlite3 3.6.20 and JDK 1.8
2026-11-04 15:18:42 +08:00

578 lines
27 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# HSM 主备数据库证书比对工具 需求文档
| 项目 | 内容 |
| --- | --- |
| 文档版本 | v0.4 |
| 编写日期 | 2026-11-04 |
| 状态 | 已更新(批量 SQL 方案) |
| 关联文档 | `设计文档.md` |
| 测试环境 | 172.1.41.139(CentOS 6.5 / JDK 1.8 / SQLite) |
| 测试数据 | `/app/cert-compare/main.db`(4 条)、`/app/cert-compare/slave.db`(15 条) |
---
## 1. 背景
HSM 系统使用 SQLite3 数据库保存用户证书,证书保存在 `usercert_tab` 表中,生产规模约 3000 张。主、备两套系统各自维护一份数据库,长期运行后可能出现:
- 主库中存在、从库中不存在的证书(主库多);
- 从库中存在、主库中不存在的证书(从库多);
- 两边都存在同一张证书(同一 DN + 证书类型),但 SN、有效期、状态等字段不一致;
- 同一 DN 下存在多张证书(例如双证),不能简单地按 DN 去重。
主备库中的证书都是业务需要的,不存在“多余证书”的概念。比对的目的不是删除数据,而是:
1. 找出主库多、从库多、字段不一致三类差异;
2. 由业务人员在报表中逐条确认合并后采用“主”还是“备”;
3. 以主库为基础,把从库多出的证书补进来,并按人工标注处理冲突,最终形成完整可用的主库数据。
目前依赖人工执行 `sqlite3` 命令核对,存在以下问题:
- 表中 `subject`/`issuer` 实际存储的是 **GBK/GB18030 字节**,而 SQLite 头部声明为 UTF-8,直接用 UTF-8 工具读取会出现乱码甚至错误合并;
- 证书身份不是单纯的 DN,表主键为 `(subject, certtype)`;
- 3000 条数据人工比对不可行,且容易遗漏;
- 合并时如果经过 Java `String` 转换,会破坏原始 GBK 字节,导致写回的数据无法被原系统正确查询。
因此需要一个可在生产环境直接运行的 Java CLI 工具,支持交互式操作和脚本化调用,完成主备库证书比对,并依据人工标注生成在主库执行的批量 SQL 脚本。
---
## 2. 建设目标
1. 通过配置文件同时加载主库、从库两个 SQLite3 数据库。
2. 以主库为基准,准确识别三类差异:
- **主库多**:仅主库存在;
- **从库多**:仅从库存在;
- **不一致**:两边都有,但比较字段不同。
3. 提供 `set main/slave`、`compare`、`sql` 等交互命令。
4. `compare` 只输出**一份报表**,包含三类差异;汇总统计直接打印在控制台。
5. 报表预留“合并选择”字段,人工填写“主”或“备”。
6. `sql` 基于人工标注生成**在主库上执行的批量 SQL 脚本**:
- 以主库为基准;
- 从库多的证书全部补入;
- 不一致的证书按人工标注选择主库版本或从库版本;
- 脚本由 DBA 在主库执行,工具本身不直接写数据库;
- 从库仅以 `ATTACH` 只读引用,DN 中文按原始字节复制。
7. 不输出日志文件;运行信息、错误信息只打印到控制台。
8. 兼容生产环境:CentOS 6.5、glibc 2.12、JDK 1.8、无外网、无 Maven。
---
## 3. 范围
### 3.1 本期范围(In Scope)
- 仅处理 `usercert_tab` 表;
- 主库、从库两个数据库路径,以及 SQL 脚本输出目录的配置与切换;
- 证书比对与三类差异统计;
- 单份 CSV 报表(含人工合并选择列);
- 依据报表标注生成批量 SQL 脚本,由 DBA 在主库执行完成合并;
- 控制台汇总、错误提示、退出码;
- 交互式 Shell 与一次性命令两种使用方式;
- 在测试库上完成验收。
### 3.2 不在本期范围(Out of Scope)
- HSM 硬件、密钥、私钥的读取或操作;
- 证书签发、吊销、CRL、信任列表维护;
- `trustlist_tab`、`apetcert_tab`、`sym_key` 等其他表的合并(SQL 脚本只操作 `usercert_tab`);
- 修改现有数据库表结构或索引;
- Web UI、服务端常驻进程、定时任务;
- 日志文件、审计日志;
- 证书密码学有效性验证(签名链、私钥匹配等);
- 报表中除“主/备”之外的其他合并策略。
---
## 4. 术语
| 术语 | 说明 |
| --- | --- |
| 主库(main) | 基础数据库,合并时以它为基础 |
| 从库(slave) | 参与比对、提供差异证书的数据库 |
| 批量 SQL 脚本 | `sql` 生成的 `merge-<时间戳>.sql`,由 DBA 在主库执行完成合并 |
| DN / subject | 证书主体名称,表中为 `C=...,O=...,OU=...,CN=...` 形式的自定义顺序字符串,实际为 GBK 字节 |
| SN | 证书序列号,表中为十六进制字符串,可能带前导 0,逻辑上唯一 |
| certtype | 证书类型,表主键的一部分;同一 DN 可有多张不同 certtype 的证书(如双证) |
| 身份键 | 判断“同一张证书”的键:`subject 原始字节 + certtype` |
| 主库多 | 主库存在、从库不存在的证书 |
| 从库多 | 从库存在、主库不存在的证书 |
| 不一致 | 身份键相同,但 SN、有效期、状态等比较字段不同 |
| 一致 | 身份键相同且比较字段全部相同;只在汇总统计中计数,不进入报表 |
| 合并选择 | 报表中的人工标注列,取值“主”或“备” |
---
## 5. 用户与使用场景
| 角色 | 场景 |
| --- | --- |
| 运维人员 | 例行核对主备证书差异,导出报表 |
| 实施人员 | 数据迁移、容灾切换后核对并生成批量 SQL 脚本,交 DBA 在主库执行 |
| 研发/测试 | 定位双证、续期、状态差异问题 |
| 业务人员 | 在报表中逐条标注不一致证书采用主库还是从库版本 |
| 自动化脚本 | 执行 `compare` 生成报表,标注后执行 `sql` 生成批量脚本 |
---
## 6. 数据规则(已按测试库验证)
### 6.1 表结构
`usercert_tab` 主要字段:
| 字段 | 类型 | 说明 | 比对角色 |
| --- | --- | --- | --- |
| `organid` | varchar(32) | 机构标识 | 扩展字段 |
| `subject` | varchar(256) | DN,GBK/GB18030 字节 | **身份键**、展示字段 |
| `issuer` | varchar(256) | 颁发者,GBK/GB18030 字节 | 默认比较字段 |
| `sn` | varchar(64) | 证书序列号,十六进制字符串 | **唯一编号**、默认比较字段 |
| `pubkey_alg` | int | 公钥算法 | 扩展字段 |
| `pubkey` | varchar(1032) | 公钥 | 扩展字段(比较哈希) |
| `sign_alg` | int | 签名算法 | 扩展字段 |
| `signinfo` | varchar(512) | 签名信息 | 扩展字段(比较哈希) |
| `not_before` | varchar(14) | 生效时间 `yyyyMMddHHmmss` | 默认比较字段 |
| `not_after` | varchar(14) | 失效时间 `yyyyMMddHHmmss` | 默认比较字段 |
| `certfile` | varchar(2048) | PEM 证书内容 | 扩展字段(比较哈希) |
| `certtype` | smallint | 证书类型 | **身份键** |
| `opertime` | varchar(14) | 操作时间 | 扩展字段 |
| `stat` | smallint | 状态 | 默认比较字段 |
主键:`PRIMARY KEY (subject, certtype)`。
> 重要:不能只用 DN 判断同一张证书。测试库中 `sge` 机构同一 DN 存在 `certtype=0` 和 `certtype=2` 两张证书,若按 DN 去重会误判。
### 6.2 编码规则
- SQLite `PRAGMA encoding` 返回 `UTF-8`,但 `subject`、`issuer` 等字段实际写入的是 **GBK/GB18030 字节**;
- 使用 JDBC `getString()` 读取会把非法 UTF-8 字节替换为 `U+FFFD`,**严禁用于比对和回写**;
- 必须通过 `hex(col)` 或 `CAST(col AS BLOB)` 获取原始字节;
- 展示时按 GB18030 解码;解码失败时显示十六进制和转义字符;
- 合并写入必须使用 SQLite 内部 `ATTACH + INSERT ... SELECT`,避免 Java 字符串编码转换。
### 6.3 SN 规则
- 测试库中 SN 均为十六进制字符串,且与证书 DER 序列号一致;
- 存在前导 0,例如表中 `01971EE53B08`,DER 序列号为 `1971EE53B08`;
- 比对时默认归一化:去首尾空白、转大写、去前导 0;展示和写回保留原始值;
- SN 在单个库内逻辑唯一;跨库同 SN 视为同一序列号,但仍以身份键定位证书。
### 6.4 时间规则
- `not_before`、`not_after` 为 14 位 `yyyyMMddHHmmss` 本地时间字符串;
- 测试库全部记录与证书 DER 有效期一致;
- 比对按原始字符串比较,同时校验格式与 `not_before <= not_after`;
- 可选出报告:已过期、未生效、有效,按当前系统时间计算。
### 6.5 certtype 说明
测试库中观察到 `certtype` 取值 `0/1/2/4/5`,同一 DN 可出现多个值。本期不硬编码业务含义,报表按数值展示;后续可通过配置映射为中文名称。
---
## 7. 功能需求
### FR-01 配置加载
- 支持 UTF-8 编码的 `config.properties`;
- 至少包含:主库路径、从库路径、文本编码、DN 匹配方式、SN 归一化方式、报表目录;
- 命令行参数优先于配置文件;
- 配置文件不存在时使用内置默认值,但执行 `compare`/`sql` 前必须已有有效配置;
- 配置错误时给出明确中文提示。
**验收**:无参数启动进入交互模式;配置中的路径正确显示;路径错误时提示“文件不存在/不可读”,且不覆盖原配置。
### FR-02 交互式 Shell
- 启动后显示 `cert-compare>` 提示符;
- 支持命令:`set`、`status`、`compare`、`sql`、`help`、`quit`/`exit`;
- 命令大小写不敏感;
- 支持单引号/双引号包裹含空格的参数;
- 空行忽略;`Ctrl-D` 等同于 `quit`;
- `TERM=dumb` 或非 TTY 时不输出 ANSI 颜色控制字符;
- 单条命令失败不退出 Shell,仅打印错误。
**验收**:连续执行多条命令,错误后仍可继续;`quit` 正常退出并返回退出码 0。
### FR-03 set main
```text
set main <path>
```
- 校验文件存在、可读、是合法 SQLite 文件、包含 `usercert_tab` 表、必需字段齐全;
- 成功时输出:绝对路径、文件大小、证书总数;
- 仅保存路径,不长期持有数据库连接,避免锁库;
- 无参数执行 `set main` 时显示当前主库路径与状态。
### FR-04 set slave
同 FR-03,用于设置从库路径。
### FR-05 SQL 脚本输出位置
- 不提供单独的输出目录设置命令;
- `sql` 生成的脚本直接写入**运行时的当前工作目录**;
- 脚本文件名固定为 `merge-<yyyyMMdd-HHmmss>.sql`,每次生成新文件,不覆盖历史脚本;
- 启动脚本 `cert-compare.sh` 会先 `cd` 到程序目录,因此在服务器上脚本默认落在 `/app/cert-compare/`。
### FR-06 status
显示当前会话状态:
- 主库、从库的绝对路径、是否存在、文件大小、证书数量;
- 当前工作目录(即 SQL 脚本输出位置);
- 当前编码、DN 匹配方式、SN 归一化方式;
- 主库与从库表结构是否一致;
- 最近一次 `compare` 生成的报表路径;
- 最近一次 `sql` 生成的脚本路径。
### FR-07 compare 分类
对两库全部证书按身份键分类:
| 分类 | 判定 | 是否进入报表 |
| --- | --- | --- |
| 主库多(ONLY_MAIN) | 主库有、从库无 | 是 |
| 从库多(ONLY_SLAVE) | 从库有、主库无 | 是 |
| 不一致(DIFF) | 两边都有,至少一个默认比较字段不同 | 是 |
| 一致(EQUAL) | 两边都有,默认比较字段全部相同 | 否,仅汇总计数 |
默认比较字段:`sn`(归一化后)、`issuer`(原始字节)、`not_before`、`not_after`、`stat`。
`compare` 不做过滤、不做查询、不接收业务筛选参数;所有差异一次性写入同一份报表,由使用者在报表中自行过滤。
**验收**:测试库 `main.db`(4 条)与 `slave.db`(15 条)比对结果为:一致 0、主库多 1、从库多 12、不一致 3;报表共 16 条差异记录。
### FR-08 compare 报表
- 只生成**一份 CSV 报表**,不再拆分为多个文件;
- 文件名:`compare-report-<yyyyMMdd-HHmmss>.csv`;
- 控制台只打印汇总统计和报表路径,不打印逐条明细;
- CSV 使用 UTF-8 + BOM,保证 Excel 打开中文不乱码;
- 报表只包含主库多、从库多、不一致三类记录,不包含一致记录;
- 报表至少包含以下列:
| 列 | 说明 |
| --- | --- |
| 序号 | 报表内序号,便于人工沟通 |
| 类型 | 主库多 / 从库多 / 不一致 |
| DN | GB18030 解码后的 DN,便于阅读 |
| DN_HEX | 原始 DN 字节十六进制,带 `HEX:` 前缀,用于精确匹配 |
| CN | 从 DN 提取的 CN,仅展示 |
| CERTTYPE | 证书类型 |
| 主库SN / 从库SN | 两边 SN,不存在的一侧留空 |
| 主库生效 / 主库失效 | 主库有效期,不存在的一侧留空 |
| 从库生效 / 从库失效 | 从库有效期,不存在的一侧留空 |
| 主库状态 / 从库状态 | 两边 `stat`,不存在的一侧留空 |
| 差异字段 | 不一致时列出差异字段,如 `sn;not_after` |
| 合并选择 | **最后一列,人工填写:主 / 备** |
- `合并选择` 固定放在最后一列,便于 Excel 中直接拉到最右侧填写;
- 报表不再包含“备注”列,数据异常信息改由 `compare` 在控制台“提示”中输出;
- `DN_HEX` 统一加 `HEX:` 前缀,避免 Excel 把长十六进制转换成科学计数法;`sql` 读取时自动去掉前缀;
- 默认不输出 `certfile`、`pubkey`、`signinfo` 等大字段;
- 报表生成后,控制台打印报表绝对路径和汇总统计。
### FR-09 人工标注规则
- **主库多**:只有主库记录,`合并选择` 默认填 `主`,且只允许 `主`;
- **从库多**:只有从库记录,`合并选择` 默认填 `备`,且只允许 `备`;
- **不一致**:`合并选择` 默认留空,必须人工填写 `主` 或 `备`;
- 空白或非法值(非“主/备”)在 `sql` 时视为错误,拒绝生成脚本并提示具体行号;
- 人工只能修改 `合并选择` 列,不应修改 DN、DN_HEX、CERTTYPE 等定位列;
- 报表保存为 UTF-8 或 GBK/GB18030 均可,`sql` 读取时自动识别。
### FR-10 sql 生成计划
`sql` 读取已人工标注的报表,校验后生成批量 SQL 脚本,并先打印计划:
- 主库证书数、从库证书数;
- 一致、主库多、从库多、不一致数量;
- 写入记录数(从库多 + 不一致选备);
- 不一致中选择“主”的数量、选择“备”的数量;
- 执行后主库应有的记录数;
- 从库路径(脚本以 `ATTACH` 只读引用)与脚本输出路径;
- 控制台给出“备份主库 + `sqlite3 主库 < 脚本`”的执行命令。
`sql` 只写脚本文件,不连接主库执行、不修改任何数据库。
### FR-11 批量 SQL 脚本内容
- 脚本输出到运行时的当前工作目录,文件名 `merge-<yyyyMMdd-HHmmss>.sql`;
- 脚本本身不携带证书数据,而是 `ATTACH DATABASE '<从库>' AS cert_src` 后按身份键复制;
- **从库多**的证书:`INSERT OR REPLACE ... SELECT ... FROM cert_src.usercert_tab WHERE ...`;
- **不一致 + 选择主**:不产生语句,主库版本自然保留;
- **不一致 + 选择备**:用从库版本整行替换主库版本;
- **主库多**:不产生语句,主库版本自然保留;
- 所有语句按身份键(`hex(subject)` + `certtype`)定位,**不出现 DN 文本**,从库记录由 SQLite 内部按原始字节复制,DN 中的 GBK 中文不会被解码或重新编码;
- 脚本使用 `BEGIN IMMEDIATE` / `COMMIT` 包裹,可重复执行且结果一致(幂等);
- 脚本末尾输出 `select 'rows', count(*)` 与 `PRAGMA integrity_check` 供执行后自检;
- 脚本为 UTF-8(无 BOM),SQL 语句与字符串字面量全部为 ASCII,仅注释包含中文。
### FR-12 sql 报表校验
生成脚本前必须校验:
1. 报表存在、可解析、列头完整;
2. 报表中的差异记录与当前两库重新比对的结果一致(差异数量、身份键、类型);报表中的 SN/有效期等字段仅供人工阅读,脚本数据始终取自当前数据库;
3. 每条记录都有合法的“合并选择”;
4. 报表中的差异键集合、类型与当前比对一致;差异集合发生变化时视为报表过期,必须重新 `compare`。
校验失败时拒绝生成脚本,提示“报表已过期或标注不完整,请重新执行 compare 并标注”,不产生任何脚本。
### FR-13 脚本执行与结果校验
脚本由 DBA 在主库上执行:
```bash
cp -p /app/cert-compare/main.db /app/cert-compare/main.db.bak-$(date +%Y%m%d-%H%M%S)
sqlite3 /app/cert-compare/main.db < /app/cert-compare/merge-20261104-213307.sql
```
执行后应满足:
1. 脚本输出 `rows` 等于“主库数量 + 从库多数量”;
2. `PRAGMA integrity_check` 返回 `ok`;
3. 选择“主”的不一致记录保持主库版本;
4. 选择“备”的不一致记录等于从库版本;
5. 从库多记录全部存在,DN/SN/时间等字段与从库一致;
6. 从库文件不被修改(脚本只读引用);
7. 主库 DN 中文按原始字节写入,`hex(subject)` 与来源库一致。
### FR-14 错误与退出码
- 不生成任何日志文件;
- 运行信息、警告、错误只输出到控制台;
- `--debug` 时可在控制台输出详细堆栈;
- 退出码:
| 退出码 | 含义 |
| --- | --- |
| 0 | 成功(交互模式正常退出) |
| 1 | 未预期内部错误 |
| 2 | 参数/配置错误 |
| 4 | 数据库打开失败、表结构不兼容、完整性校验失败 |
| 5 | 报表过期、标注非法、SQL 脚本生成/写入失败 |
### FR-15 一次性命令模式
支持不进入交互 Shell,直接执行:
```bash
# 生成报表
java -Dfile.encoding=UTF-8 -jar cert-compare.jar compare \
--config /app/cert-compare/config.properties
# 人工标注报表后生成批量 SQL
java -Dfile.encoding=UTF-8 -jar cert-compare.jar sql \
--config /app/cert-compare/config.properties \
--report /app/cert-compare/reports/compare-report-20261104-213307-annotated.csv
# 由 DBA 在主库执行生成的脚本
sqlite3 /app/cert-compare/main.db < /app/cert-compare/merge-20261104-213307.sql
```
一次性模式与交互模式共用同一套比对、报表、SQL 生成引擎,结果必须一致。
### FR-16 help
- `help` 输出命令列表与示例;
- `help compare`、`help sql` 输出用法说明;
- 未知命令给出提示,不退出 Shell。
---
## 8. 非功能需求
| 编号 | 需求 |
| --- | --- |
| NFR-01 兼容性 | 可在 CentOS 6.5 / glibc 2.12 / JDK 1.8 上运行;无外网依赖;不要求服务器安装 Maven |
| NFR-02 性能 | 3000 张证书:`compare` ≤ 3 秒,`sql` ≤ 3 秒;生成的脚本可在 sqlite3 3.6.20 上执行;JVM 内存 ≤ 256MB |
| NFR-03 编码正确性 | DN/issuer 原始字节零损失;合并前后十六进制一致;脚本中不出现 DN 文本;禁止 `U+FFFD` 参与比较 |
| NFR-04 数据安全 | 工具默认只读源库;不直接修改任何数据库,只生成 SQL 脚本;不生成日志文件 |
| NFR-05 可靠性 | 脚本使用事务包裹且可重复执行;执行失败可按备份回滚;从库 SHA-256 不变 |
| NFR-06 易用性 | 中文提示;报表可直接用 Excel 标注;错误信息包含原因与处理建议 |
| NFR-07 可维护性 | 分层设计;核心逻辑有单元测试;不修改现有表结构 |
| NFR-08 可部署性 | 单个可执行 fat jar + 配置文件 + 启动脚本即可部署 |
| NFR-09 报表兼容性 | 报表读取兼容 UTF-8 BOM、UTF-8、GBK/GB18030 三种保存方式 |
---
## 9. 交互与输出示例
### 9.1 设置数据库
```text
cert-compare> set main /app/cert-compare/main.db
[OK] 主库: /app/cert-compare/main.db (459,776 字节, 4 张证书)
cert-compare> set slave /app/cert-compare/slave.db
[OK] 从库: /app/cert-compare/slave.db (459,776 字节, 15 张证书)
```
### 9.2 比对
```text
cert-compare> compare
主库: /app/cert-compare/main.db 证书数: 4
从库: /app/cert-compare/slave.db 证书数: 15
汇总:
一致 0
主库多 1
从库多 12
不一致 3
差异合计 16
报表:
/app/cert-compare/reports/compare-report-20261103-205500.csv
请按“合并选择”列标注:主 或 备,然后执行 sql 生成批量脚本。
```
### 9.3 报表示例(CSV)
```csv
序号,类型,DN,DN_HEX,CN,CERTTYPE,主库SN,从库SN,主库生效,主库失效,从库生效,从库失效,主库状态,从库状态,差异字段,合并选择
1,从库多,"C=CN,ST=北京,L=北京,O=信雅达,OU=双证,CN=172.1.40.188","HEX:433D434E2C...",172.1.40.188,0,,01971EE53B08,,,20250529160000,20260529160000,,0,,备
2,主库多,"C=cn,O=CFCA OCA1 SM2,OU=Local RA OCA1,OU=Business Customers,CN=041@...@陈超@...","HEX:433D636E2C...",041@...@陈超@...,1,1177482454,,20260806025132,20270806025132,,,,0,,主
14,不一致,CN=diff-sn-01,"HEX:434E3D646966662D736E2D3031",diff-sn-01,0,0A0B0C01,0A0B0C11,20260101000000,20301231235959,20260101000000,20301231235959,0,0,sn,主
15,不一致,CN=diff-stat-03,"HEX:434E3D646966662D737461742D3033",diff-stat-03,5,0A0B0C03,0A0B0C03,20260101000000,20301231235959,20260101000000,20301231235959,0,1,stat,主
16,不一致,CN=diff-time-02,"HEX:434E3D646966662D74696D652D3032",diff-time-02,2,0A0B0C02,0A0B0C02,20260101000000,20301231235959,20260101000000,20311231235959,0,0,not_after,备
```
### 9.4 生成批量 SQL
```text
cert-compare> sql --report /app/cert-compare/reports/compare-report-20261104-213307-annotated.csv
SQL 生成计划:
主库证书数 4
从库证书数 15
一致 0
主库多 1
从库多 12
不一致 3
写入(从库多+选备) 13
不一致选主 2
不一致选备 1
执行后主库记录数 16
从库(只读引用) /app/cert-compare/slave.db
SQL 文件 /app/cert-compare/merge-20261104-213307.sql
请在主库上执行(执行前先备份):
cp -p /app/cert-compare/main.db /app/cert-compare/main.db.bak-$(date +%Y%m%d-%H%M%S)
sqlite3 /app/cert-compare/main.db < /app/cert-compare/merge-20261104-213307.sql
执行后主库记录数应为 16 条;脚本末尾会输出 rows 与 integrity_check 结果。
```
执行脚本后:
```text
rows|16
ok
```
---
## 10. 验收标准
| 编号 | 场景 | 期望结果 |
| --- | --- | --- |
| AC-01 | `set main/slave` | 路径正确显示;主库 4 条、从库 15 条;当前目录可写 |
| AC-02 | `compare` | 一致 0、主库多 1、从库多 12、不一致 3;控制台只打印汇总 |
| AC-03 | 报表 | 只生成一份 CSV,含 16 条差异记录;“合并选择”为最后一列,无“备注”列 |
| AC-04 | 默认标注 | 主库多默认“主”,从库多默认“备”,不一致留空 |
| AC-05 | 标注后 sql | 生成 1 个 SQL 脚本;1 条 INSERT OR REPLACE(13 个身份键);无 UPDATE 语句 |
| AC-06 | 不一致选主 | 执行脚本后该身份键字段与主库一致 |
| AC-07 | 不一致选备 | 执行脚本后该身份键字段与从库一致 |
| AC-08 | 报表编码 | UTF-8 BOM 报表 Excel 打开中文正常;GBK 保存后 sql 仍可读取 |
| AC-09 | 报表过期 | 数据库变化后使用旧报表生成脚本,拒绝执行并提示重新 compare |
| AC-10 | 标注非法 | 不一致记录留空或填“其他”,生成脚本被拒绝并提示行号 |
| AC-11 | 同一 DN 不同 certtype | `sge` 的 certtype=0 与 2 均保留,不互相覆盖 |
| AC-12 | 字节保持 | 执行脚本后每条 `hex(subject)` 与来源库一致,`typeof(subject)='text'` |
| AC-13 | 源库只读 | 生成脚本前后两源文件 SHA-256 不变;执行后从库 SHA-256 不变 |
| AC-14 | 无日志文件 | 运行过程不生成 `*.log`,信息仅输出到控制台 |
| AC-15 | 3000 条模拟数据 | compare ≤ 3s,sql ≤ 3s,内存 ≤ 256MB |
| AC-16 | 脚本兼容性 | 脚本可在服务器 sqlite3 3.6.20 上一次执行成功,重复执行结果一致 |
---
## 11. 假设、约束与待确认事项
### 11.1 已确认
- `set main` / `set slave` 设置主库、从库路径;
- `sql` 生成的脚本直接写入运行时的当前工作目录;
- `sql` 只生成脚本,不直接修改任何数据库;脚本由 DBA 在主库执行;
- 脚本通过 `ATTACH` 从从库按原始字节复制,DN 中文不经过字符集转换;
- 证书身份键为 **DN + certtype**,SN 作为唯一编号参与比较;
- 主库多、从库多、不一致三类差异均进入同一份报表;
- 合并选择只有“主”“备”两种;
- 不生成日志文件。
### 11.2 假设
- `usercert_tab` 结构在生产与测试环境一致;
- 证书数量约 3000,单库文件规模在数十 MB 以内;
- 生产比对可在业务低峰或维护窗口执行,避免与 HSM 应用并发写冲突;
- 服务器 `/tmp` 可写且允许执行(sqlite-jdbc 需解压本地库);
- 业务人员使用 Excel 或文本编辑器修改报表的“合并选择”列。
### 11.3 待确认
1. `certtype` 各数值的业务含义(0/1/2/4/5),是否需要报表中文映射;
2. 生产环境是否允许停应用/只读挂载,或必须在线比对;
3. 是否需要同时核对 `trustlist_tab`、`apetcert_tab`(本期不做);
4. SQL 脚本文件命名与保留策略(当前为 `merge-<时间戳>.sql`,不覆盖历史脚本);
5. SN 前导 0 归一化是否与现网查询逻辑一致;
6. DN 是否需要忽略属性大小写/顺序的“近似匹配”(本期默认严格字节匹配);
7. 主库多/从库多的“合并选择”是否允许改成其他值(本期固定为主/备,确保不丢证书)。
---
## 12. 里程碑
| 阶段 | 内容 | 交付物 |
| --- | --- | --- |
| M1 | 环境验证、需求与设计 | 本文件、`设计文档.md` |
| M2 | 工程骨架、配置、set/status | 可启动的 CLI、配置加载、库校验 |
| M3 | compare 引擎与单份报表 | 三类差异、控制台汇总、CSV |
| M4 | 报表读取、标注校验、批量 SQL 生成 | SQL 脚本、脚本执行验证 |
| M5 | 测试与部署 | 单元/集成测试、3000 条压测、部署脚本、验收记录 |
---
## 13. 附:测试库现状摘要
| 项目 | main.db | slave.db |
| --- | --- | --- |
| 文件大小 | 459,776 字节 | 459,776 字节 |
| `usercert_tab` 记录数 | 4 | 15 |
| certtype 分布 | 0:1、1:1、2:1、5:1 | 0:5、2:3、4:1、5:6 |
| 重复身份键 | 无 | 无 |
| 同一 DN 多证书 | 无 | 有(`sge`,certtype 0/2) |
| 重复 SN | 无 | 无 |
| 日期格式异常 | 无 | 无 |
| 完整性检查 | ok | ok |
其中 3 条为专门构造的“不一致”测试数据(主备同 DN + certtype,SN/有效期/状态不同):
| DN | CERTTYPE | 差异 | 主库 | 从库 |
| --- | --- | --- | --- | --- |
| `CN=diff-sn-01` | 0 | SN | `0A0B0C01` | `0A0B0C11` |
| `CN=diff-time-02` | 2 | 失效时间 | `20301231235959` | `20311231235959` |
| `CN=diff-stat-03` | 5 | 状态 | `0` | `1` |
按本期规则,测试库的预期比对结论为:
- 主库多:**1 张**;
- 从库多:**12 张**;
- 不一致:**3 张**;
- 一致:0 张;
- 报表差异记录:16 条;
- 执行 SQL 后(示例标注:2 条选主、1 条选备):**16 张**。