Files
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

1273 lines
48 KiB
Markdown
Raw Permalink 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. 设计概述
工具采用命令行 + 交互式 Shell 的形态,读取主库、从库中的 `usercert_tab`,以 `(subject 原始字节, certtype)` 为身份键进行比对,输出**一份包含三类差异的 CSV 报表**:
- 主库多(仅主库存在);
- 从库多(仅从库存在);
- 不一致(两边都有,比较字段不同)。
报表中预留“合并选择”列,由业务人员填写“主”或“备”。`sql` 读取标注后的报表,生成在主库执行的批量 SQL 脚本:
- 以主库为基准;
- 从库多的证书全部补入;
- 不一致的证书按标注选择主库或从库版本;
- 主库多的证书自然保留;
- 工具只生成脚本,由 DBA 在主库执行;执行前备份,从库只读引用。
设计重点:
1. **编码安全**:全程以原始字节处理 `subject`/`issuer`,禁止使用 JDBC `getString()` 读取 GBK 字段;
2. **身份键正确**:DN + certtype,避免双证场景误判;
3. **报表驱动合并**:合并决策来自人工标注,不在代码里写死主库优先;
4. **字节级脚本**:`ATTACH` 从库后由 SQLite 内部复制行,脚本中只出现 `hex(subject)`,不出现 DN 文本;
5. **非破坏性**:工具不写任何数据库;脚本使用事务,失败可回滚,执行前备份主库;
6. **无日志文件**:运行信息、错误只输出到控制台;
7. **老环境兼容**:JDK 8 字节码、sqlite-jdbc 3.36.0.3、无外网、无 Maven,脚本兼容 sqlite3 3.6.20。
---
## 2. 环境验证报告
验证时间:2026-11-03,验证账号:`root@172.1.41.139`。以下结论均为实际执行结果。
### 2.1 服务器基础环境
| 项目 | 实测结果 |
| --- | --- |
| 操作系统 | Red Hat Enterprise Linux Server release 6.5 (Santiago) |
| 内核 | Linux 2.6.32-431.el6.x86_64 |
| 架构 | x86_64 |
| glibc | 2.12 |
| CPU | 8 核 |
| 内存 | 7.8 GB 总量,约 7.1 GB 可用 |
| 磁盘 | `/` 117 GB,剩余 63 GB |
| 时区/时间 | CST(UTC+8) |
| 语言环境 | `LANG=zh_CN.UTF-8` |
| `/app/cert-compare` | root 可读写,验证通过 |
| `/tmp` | 可写,sqlite-jdbc 可解压并加载本地库 |
### 2.2 Java 与构建工具
| 项目 | 实测结果 |
| --- | --- |
| `java -version` | 1.8.0_152(HotSpot 64-Bit) |
| `javac -version` | 1.8.0_152 |
| 默认 `file.encoding` | UTF-8 |
| `sun.jnu.encoding` | UTF-8 |
| Maven / Gradle | 服务器**未安装** |
| 本地构建环境 | JDK 8 + Maven 3.8.5 可用 |
**结论**:服务器只需 JRE 8 即可运行;在本地构建 Java 8 目标字节码的 fat jar 后上传部署。
### 2.3 SQLite 与 JDBC 驱动
| 项目 | 实测结果 |
| --- | --- |
| `sqlite3 --version` | 3.6.20(CentOS 6.5 自带,较旧) |
| `PRAGMA journal_mode` | `delete`(非 WAL) |
| `PRAGMA encoding` | `UTF-8`(仅文件头声明,见 2.5) |
| `PRAGMA integrity_check` | main.db / slave.db 均为 `ok` |
| sqlite-jdbc 3.36.0.3 | 在服务器加载成功,读写正常,**选用** |
| sqlite-jdbc 3.8.11.2 | 可运行,但版本过旧,不选 |
| sqlite-jdbc 3.45.3.0 | 启动报 `NoClassDefFoundError: org/slf4j/LoggerFactory`,需额外 slf4j 依赖,不选 |
| 内置 SQLite 版本(3.36.0.3) | 3.36.0 |
| `VACUUM INTO`(JDBC) | 3.36.0 支持,但本方案已不使用 |
| `VACUUM INTO`(系统 CLI) | 3.6.20 不支持,报 `near "INTO": syntax error` |
**结论**:使用 `org.xerial:sqlite-jdbc:3.36.0.3`,其原生库可在 glibc 2.12 上运行;生成的脚本不得使用 3.6.20 不支持的新语法(CTE、UPSERT、`VACUUM INTO`、row-value 等)。
### 2.4 数据库与数据特征
| 项目 | main.db | slave.db |
| --- | --- | --- |
| 文件大小 | 459,776 字节 | 459,776 字节 |
| 表数量 | 13 | 13 |
| `usercert_tab` 记录数 | 4 | 15 |
| certtype 分布 | 0:1、1:1、2:1、5:1 | 0:5、2:3、4:1、5:6 |
| 不一致测试数据 | 3 组 | 3 组 |
| 重复身份键 `(subject,certtype)` | 无 | 无 |
| 同一 DN 多证书 | 无 | 有:`sge` 的 certtype=0 与 2 |
| 重复 SN | 无 | 无 |
| 日期格式异常 | 无 | 无 |
| 完整性检查 | ok | ok |
| 文件占用 | 无进程持有 | 无进程持有 |
`usercert_tab` 主键:
```sql
PRIMARY KEY (`subject`, `certtype`)
```
关键字段:`organid, subject, issuer, sn, pubkey_alg, pubkey, sign_alg, signinfo, not_before, not_after, certfile, certtype, opertime, stat`。
### 2.5 编码验证(关键)
**现象**:SQLite 头部声明 UTF-8,但 `subject`/`issuer` 实际是 GBK 字节。
以 main.db 唯一记录为例:
```text
JDBC getString("subject") 长度:109(包含替换字符 U+FFFD)
CAST(subject AS BLOB) 长度:110 字节
原始十六进制:... 40 B3 C2 B3 AC 40 ...
GB18030 解码:... @陈超@ ...
```
- `B3 C2 B3 AC` 是“陈超”的 GBK 编码;
- 直接用 `getString()` 会把 `B3 C2` 等非法 UTF-8 字节替换为 `U+FFFD`,导致不同 DN 可能被错误合并;
- 通过 `CAST(subject AS BLOB)` / `hex(subject)` 获取原始字节后,按 GB18030 解码显示完全正确。
**设计结论**:
- 比对、键计算全部使用原始字节;
- 仅在展示和 CSV 输出时按 GB18030 解码;
- 数据写入由 SQLite 内部完成,不经过 Java `String`。
### 2.6 批量 SQL 机制验证
主备 DN 以 GBK/GB18030 字节存储在 TEXT 列中,直接 `getString()` 会产生 `U+FFFD`。因此合并改为生成由 DBA 在主库执行的 SQL 脚本;脚本以 `ATTACH` 引用从库并按 `hex(subject)` + `certtype` 定位,复制过程完全在 SQLite 内部完成。
早期在 main.db 副本上的机制验证结果:
- `ATTACH DATABASE ... AS cert_src` 成功;
- `INSERT OR REPLACE ... SELECT ... FROM cert_src.usercert_tab` 成功;
- 插入前后 `hex(subject)` 完全一致;
- `typeof(subject)` 保持 `text`,不是 `blob`;
- 事务回滚后记录数恢复,回滚正常;
- 说明 `ATTACH + INSERT ... SELECT` 可在不接触 Java 字符串的情况下保持 GBK 字节和字段类型。
服务器端(sqlite3 3.6.20)对生成脚本的执行验证:
```text
rows|16
ok
```
- 服务器实际脚本(`dn.match=EXACT`)为 1 条 `INSERT OR REPLACE ... SELECT`,覆盖 13 个身份键;
- `dn.match=TRIM` 且主备 DN 字节不同的场景由单元测试覆盖,生成 `UPDATE ... SET col = (SELECT ...)`;
- 重复执行后仍为 16 条(幂等);
- 执行前后 main.db / slave.db 的 SHA-256 不变;
- “陈超”记录 `hex(subject)` 与主库逐字节一致(221 字节),`iconv` 解码中文正常。
**设计结论**:`sql` 只生成脚本,不直接写数据库;脚本使用 `ATTACH + INSERT OR REPLACE / UPDATE`,由 SQLite 内部按原始字节复制,从而彻底规避 Java 侧编码问题。此前的 `VACUUM INTO` 快照方案已取消。
### 2.7 字段与证书一致性验证
使用 `cryptography` 解析原有 13 条记录的 PEM 证书,与表字段对比:
| 校验项 | 结果 |
| --- | --- |
| 表 `sn` 与 DER 序列号 | 13/13 一致(含前导 0 归一化后) |
| 表 `not_before` 与证书生效时间 | 13/13 一致 |
| 表 `not_after` 与证书失效时间 | 13/13 一致 |
| 表 `subject` 与证书主体 | 逻辑一致,但表内顺序为 `C..CN`,PEM 为 `CN..C` |
| PEM 主体字符编码 | 个别证书内部主体字符串不是合法 UTF-8,工具显示替换符 |
**设计结论**:
- 以表字段为唯一比对依据,不从 PEM 反推 DN/SN;
- 表内 DN 顺序与 RFC4514 输出不同,不做 RFC 规范化,默认按原始字节比较;
- 不提供证书密码学验证功能。
### 2.8 环境验证结论
| 结论 | 说明 |
| --- | --- |
| 可实施 | Java 8 + sqlite-jdbc 3.36.0.3 + GBK 字节处理方案已在目标服务器验证 |
| 主要风险 | 编码误用、生产库并发写、报表人工编辑错误、DN 近似匹配、certtype 业务含义未知 |
| 构建方式 | 本地 Maven 构建 fat jar,上传服务器运行;服务器不安装 Maven |
| 测试数据 | 主库 4 条、从库 15 条;预期主库多 1、从库多 12、不一致 3 |
---
## 3. 总体架构
```text
+----------------------------------------------------------+
| cert-compare.sh |
| java -Dfile.encoding=UTF-8 -Xmx256m -jar cert-compare.jar|
+----------------------------+-----------------------------+
|
+--------v---------+
| CertCompareApp |
| 启动/模式选择 |
+--------+---------+
|
+---------+---------+
| |
+------v-------+ +-------v--------+
| Shell | | 一次性命令 |
| 交互循环 | | compare / sql |
+------+-------+ +-------+--------+
| |
+---------+---------+
|
+--------v---------+
| Operations |
| compare / sql |
+--------+---------+
|
+------------------+-------------------+
| |
+------v-------+ +-------v--------+
| Compare | | ReportReader |
| Service | | ReportValidator|
+------+-------+ +-------+--------+
| |
+------v-------+ +-------v--------+
| ReportWriter | | SqlScript |
| 单份 CSV | | Service |
+------+-------+ +-------+--------+
| |
| +-------v--------+
| | merge-*.sql |
| | 由 DBA 执行 |
| +----------------+
| |
+------------------+-------------------+
|
+--------v---------+
| DbManager |
| sqlite-jdbc |
+--------+---------+
|
+----------+----------+
| |
+-----v-----+ +-----v-----+
| main.db | | slave.db |
| (只读) | | (只读) |
+-----------+ +-----------+
```
分层职责:
| 层 | 职责 |
| --- | --- |
| CLI | 交互循环、命令分词、参数解析、帮助、退出码 |
| Operations | 组织 compare / sql 流程,控制台输出 |
| Service | 比对、报表生成、报表读取与校验、批量 SQL 生成 |
| Dao | 数据库打开、Schema 校验、只读查询 |
| Report | 单份 CSV 写入、编码识别、CSV 解析、标注校验 |
| Util | GB18030、十六进制、路径、时间 |
---
## 4. 模块与类设计
包名:`com.sunyard.certcompare`。实际源码结构:
```text
src/main/java/com/sunyard/certcompare/
CertCompareApp.java main 入口,交互/一次性模式
AppConfig.java 配置模型与默认值
Operations.java compare / sql 公共流程
UserException.java 业务异常 + 退出码
cli/
Shell.java 交互循环与命令分发
CommandTokenizer.java 引号感知分词
compare/
CompareService.java 比对主流程(分类、字段差异、异常提示)
db/
DbManager.java 连接、Schema 校验、hex 读取、SQL 字面量
model/
CertRecord.java 投影记录(DN/issuer 为十六进制原始字节)
CertKey.java 身份键(subjectHex + certtype)
CompareCategory.java 一致/主库多/从库多/不一致
CompareResult.java 分类结果与统计
CompareRow.java 报表行数据
FieldDiff.java 字段差异
MergeChoice.java 人工选择:主/备
report/
Csv.java RFC4180 转义与解析
ReportWriter.java 单份 CSV 报表
ReportReader.java 报表读取、编码识别
ReportRow.java 报表行
ReportData.java 报表数据
ReportValidator.java 报表过期与标注校验
sql/
SqlPlan.java 生成计划与统计
SqlScriptService.java 批量 SQL 脚本生成
util/
Text.java GB18030 解码、hex 工具、CN 提取
SnNormalizer.java SN 归一化
DateUtil.java 14 位时间校验
```
### 4.1 核心类职责
| 类 | 职责 |
| --- | --- |
| `CertCompareApp` | 解析全局参数(`--config`、`--debug`),选择交互/一次性模式 |
| `AppConfig` | 读取 UTF-8 properties;路径解析;默认值;校验编码与匹配方式 |
| `Operations` | compare / sql 流程编排、控制台汇总与执行提示 |
| `DbManager` | 建立只读连接、`busy_timeout`、Schema 校验、`hex()` 投影读取 |
| `CertKey` | 不可变身份键,`equals/hashCode` 基于字节十六进制 + certtype |
| `SnNormalizer` | 去空白、大写、去前导 0;保留原始值用于输出 |
| `CompareService` | 读取两库、构造 Map、分类、生成结果与提示 |
| `ReportWriter` | 生成单份 CSV,预填默认合并选择 |
| `ReportReader` | 识别 UTF-8/GB18030、解析 CSV、读取标注 |
| `ReportValidator` | 校验报表与当前比对结果一致、标注合法 |
| `SqlScriptService` | 生成 ATTACH + INSERT OR REPLACE / UPDATE 的批量脚本 |
| `SqlPlan` | 生成计划统计与控制台展示 |
---
## 5. 配置设计
### 5.1 配置文件示例
`config.properties`(UTF-8):
```properties
# 数据库路径
db.main=/app/cert-compare/main.db
db.slave=/app/cert-compare/slave.db
# 文本编码:subject/issuer 实际为 GBK/GB18030 字节
db.text.encoding=GB18030
# DN 匹配:EXACT(默认,按原始字节)| TRIM(去除整体首尾空白)
dn.match=EXACT
# SN 归一化:HEX(大写+去前导0)| EXACT
sn.normalize=HEX
# 报表
report.dir=./reports
report.csv.bom=true
```
批量 SQL 脚本不通过配置指定目录,直接写入运行时的当前工作目录。
### 5.2 优先级
```text
命令行参数 > 配置文件 > 内置默认值
```
### 5.3 配置校验
- 主库、从库必须存在、是普通文件、可读、是合法 SQLite 文件;
- SQL 脚本写入当前工作目录;目录不可写时生成失败并提示;
- 编码必须是 JVM 支持的字符集(默认 GB18030);
- `dn.match`、`sn.normalize` 仅接受枚举值;
- 报表目录不存在时自动创建,创建失败则报错;
- 配置错误退出码 2,交互模式仅提示不退出;
- 不生成任何日志文件,不读取日志配置。
---
## 6. CLI 设计
### 6.1 启动方式
```bash
# 交互模式
java -Dfile.encoding=UTF-8 -jar cert-compare.jar
java -Dfile.encoding=UTF-8 -jar cert-compare.jar --config /app/cert-compare/config.properties
# 一次性模式
java -Dfile.encoding=UTF-8 -jar cert-compare.jar compare --config /app/cert-compare/config.properties
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
```
### 6.2 交互命令语法
```text
set main <path>
set slave <path>
set main | set slave # 无参数:显示当前值
status
compare
sql [--report <path>]
help [command]
quit | exit
```
### 6.3 命令行为
| 命令 | 行为 |
| --- | --- |
| `set main` | 校验并设置主库,打印大小/数量 |
| `set slave` | 校验并设置从库 |
| `status` | 显示主库/从库、当前工作目录(SQL 输出位置)、数量、结构兼容性、最近报表/脚本 |
| `compare` | 比对、生成单份报表、控制台打印汇总 |
| `sql` | 读取标注报表,校验后生成批量 SQL 脚本并打印执行命令 |
| `help` | 命令/参数帮助 |
| `quit` | 退出,返回 0 |
交互实现:
- `BufferedReader` 读取标准输入;
- `CommandTokenizer` 支持 `"..."`、`'...'` 和反斜杠转义;
- 命令名大小写不敏感;
- `TERM=dumb` 时不使用 ANSI 颜色;
- 每条命令独立捕获异常,打印错误后继续;
- 不长期持有数据库连接,命令结束后关闭,避免锁库;
- 不写日志文件。
### 6.4 compare 参数
`compare` 不接受业务筛选、查询、过滤参数。所有差异一次性进入同一份报表,由使用者在报表中自行过滤。
### 6.5 sql 参数
| 参数 | 说明 |
| --- | --- |
| `--report <path>` | 指定已标注的报表;缺省使用本次会话最近一次 compare 的报表,若没有则使用报表目录中最新的 `compare-report-*.csv` |
`sql` 不写数据库、不改源库,只在当前工作目录生成 `merge-<yyyyMMdd-HHmmss>.sql`,因此不需要确认参数。
### 6.6 退出码
| 码 | 含义 |
| --- | --- |
| 0 | 成功 |
| 1 | 未预期内部错误 |
| 2 | 参数/配置错误 |
| 4 | 数据库/结构/完整性错误 |
| 5 | 报表过期、标注非法、SQL 脚本生成/写入失败 |
---
## 7. 数据访问与编码设计
### 7.1 连接
```text
jdbc:sqlite:/app/cert-compare/main.db
```
连接后执行:
```sql
PRAGMA busy_timeout = 5000;
```
说明:
- sqlite-jdbc 3.36.0.3 内部 SQLite 3.36.0,仅用于只读查询;
- 源库默认不执行任何写操作;
- `compare` 可设置 `PRAGMA query_only = ON` 进一步防误写;
- 若生产库被占用,返回明确的 `SQLITE_BUSY` 提示,建议维护窗口执行。
### 7.2 读取投影
默认比较不读取大字段:
```sql
SELECT rowid,
hex(subject) AS subject_hex,
hex(issuer) AS issuer_hex,
hex(organid) AS organid_hex,
sn, pubkey_alg, sign_alg,
not_before, not_after, certtype, opertime, stat
FROM usercert_tab
```
约束:
- 不查询 `subject`/`issuer` 的字符串形态;
- 不使用 `getString()` 读取上述字段;
- 生成脚本时只读取 `hex(subject)`,数据复制在 SQLite 内部完成,不经过 Java 字符串。
### 7.3 编码工具
```java
public final class Gb18030 {
public static final Charset CHARSET = Charset.forName("GB18030");
public static String decode(byte[] raw) {
if (raw == null) return "";
return new String(raw, CHARSET);
}
public static byte[] encode(String text) {
return text.getBytes(CHARSET);
}
}
```
展示策略:
- 正常:GB18030 解码;
- 异常:输出 `hex=...` 与可打印 ASCII,避免抛出;
- 控制台/CSV 统一 UTF-8 输出;
- CSV 首部写入 BOM `\uFEFF`。
### 7.4 身份键
```java
final class CertKey {
final String subjectHex; // 大写十六进制,原始字节
final int certtype;
}
```
键字符串形式:`subjectHex + "|" + certtype`。
- 默认 `dn.match=EXACT`:`subjectHex` 完全一致;
- `TRIM`:仅去除 subject 整体首尾的 ASCII 空白后再取十六进制;
- 不做大小写归一化、不做属性顺序重排、不做 RFC4514 规范化;
- 报表中的 `DN_HEX` 加 `HEX:` 前缀,读取时去掉;键计算始终用裸十六进制。
### 7.5 SN 归一化
```text
raw = trim(sn)
normalized = raw.toUpperCase(Locale.ROOT)
while (normalized.length() > 1 && normalized.charAt(0) == '0') remove first char
```
- 仅当 `sn.normalize=HEX` 时启用;
- 非十六进制字符时退化为 `trim + uppercase` 并记录控制台警告;
- 展示与 CSV 均使用原始 `sn`。
### 7.6 时间校验
- 长度必须为 14;
- 必须全部为数字;
- 可按 `yyyyMMddHHmmss` 解析;
- `not_before <= not_after`;
- 校验失败标记 `INVALID_DATE`,在 `compare` 控制台“提示”中列出(HEX + CERTTYPE),不阻止 compare;`sql` 仍按报表标注生成脚本,并在控制台提示该记录风险。
---
## 8. 比对设计
### 8.1 流程
```text
1. 校验配置与两库结构
2. 从 main 读取投影记录 -> List<CertRecord>
3. 从 slave 读取投影记录 -> List<CertRecord>
4. 分别构建 Map<CertKey, List<CertRecord>>
5. 检测库内重复键、重复 SN、非法日期(诊断信息)
6. 遍历两库键的并集:
- 仅 main -> ONLY_MAIN(主库多)
- 仅 slave -> ONLY_SLAVE(从库多)
- 两边都有 -> DiffEngine 比较
无差异 -> EQUAL(仅计数)
有差异 -> DIFF(不一致)
7. 汇总统计
8. 控制台打印汇总;仅将 ONLY_MAIN/ONLY_SLAVE/DIFF 写入一份 CSV
```
### 8.2 分类矩阵
| main | slave | 分类 | 是否进报表 | 默认合并选择 |
| --- | --- | --- | --- | --- |
| 有 | 无 | ONLY_MAIN(主库多) | 是 | 主 |
| 无 | 有 | ONLY_SLAVE(从库多) | 是 | 备 |
| 有 | 有,字段相同 | EQUAL(一致) | 否 | - |
| 有 | 有,字段不同 | DIFF(不一致) | 是 | 空,待人工填写 |
| 库内重复键 | - | 诊断告警 | 按实际分类 | - |
| 日期非法 | - | 诊断告警 | 按实际分类 | - |
### 8.3 字段比较
默认比较字段:
| 字段 | 比较方式 |
| --- | --- |
| `sn` | 归一化后字符串相等 |
| `issuer` | 原始字节相等 |
| `not_before` | 原始字符串相等 |
| `not_after` | 原始字符串相等 |
| `stat` | 整数相等 |
`opertime`、`organid`、公钥、证书 PEM 等不参与默认比较,避免操作时间等元数据造成大量误报。
`FieldDiff` 输出:字段名、主库值、从库值。二进制字段输出十六进制或哈希,避免控制台/报表乱码。
### 8.4 数据异常
| 异常 | 判定 | 处理 |
| --- | --- | --- |
| `DUPLICATE_KEY` | 同一库内身份键出现多次 | 理论上被主键阻止;如出现,直接报错 |
| `DUPLICATE_SN` | 同一库内归一化 SN 重复但身份键不同 | 控制台“提示”告警,不阻止生成脚本 |
| `INVALID_DATE` | 非 14 位/非法时间/起止倒置 | 控制台“提示”告警,仍按标注生成脚本 |
| `EMPTY_DN` / `EMPTY_SN` | 空值 | 控制台“提示”告警 |
| `DECODE_ERROR` | GB18030 解码异常 | 展示十六进制;不影响字节比对 |
### 8.5 复杂度
- 时间:O(n + m),n/m 为两库记录数;
- 空间:O(n + m),默认不加载大字段;
- 3000 条规模下内存占用预计 < 20 MB(不含 JVM 基础开销)。
---
## 9. 报表设计
### 9.1 文件
- 每次 `compare` 生成一份 CSV:`<report.dir>/compare-report-yyyyMMdd-HHmmss.csv`;
- 不生成 `slave-extra`、`main-missing`、`field-diff`、`summary` 等额外文件;
- 汇总信息只打印到控制台;
- 报表包含三类差异记录,不包含一致记录;
- 若两库完全一致,生成只有表头的空报表,仍打印汇总。
### 9.2 列定义
| 列 | 说明 |
| --- | --- |
| 序号 | 报表内序号 |
| 类型 | 主库多 / 从库多 / 不一致 |
| DN | GB18030 解码后的展示值 |
| DN_HEX | `HEX:` + 原始字节大写十六进制 |
| CN | 从 DN 提取的 CN,仅展示 |
| CERTTYPE | 证书类型 |
| 主库SN / 从库SN | 两边 SN,不存在的一侧留空 |
| 主库生效 / 主库失效 | 主库有效期,不存在的一侧留空 |
| 从库生效 / 从库失效 | 从库有效期,不存在的一侧留空 |
| 主库状态 / 从库状态 | 两边 `stat`,不存在的一侧留空 |
| 差异字段 | 不一致时列出,如 `sn;not_after` |
| 合并选择 | **最后一列,人工填写:主 / 备** |
- `合并选择` 固定放在最后一列,方便在 Excel 中直接向右填写;
- 不设“备注”列;日期非法、有效期倒置、SN/DN 为空、重复 SN 等异常改在 `compare` 控制台“提示”中输出;
- 读取端按表头列名定位,不依赖列顺序,因此列顺序调整不影响旧报表读取。
### 9.3 默认标注
| 类型 | 默认值 | 允许值 |
| --- | --- | --- |
| 主库多 | 主 | 仅主 |
| 从库多 | 备 | 仅备 |
| 不一致 | 空 | 主 / 备 |
默认标注让业务人员只需要处理“不一致”行;主库多、从库多已按唯一可行来源预填。
### 9.4 CSV 规格
- 编码:UTF-8 + BOM;
- 分隔符:逗号;
- 换行:`\n`;
- 字段内逗号、引号、换行按 RFC4180 转义;
- `DN_HEX` 加 `HEX:` 前缀,防止 Excel 将长十六进制转为科学计数法;
- `DN` 用双引号包裹(包含逗号);
- 报表生成后控制台打印绝对路径。
### 9.5 人工编辑建议
- 只修改最后一列“合并选择”,不要改动 DN、DN_HEX、CERTTYPE、类型等定位列;
- Excel 保存时选择“CSV UTF-8(逗号分隔)”,或直接保存为 GBK CSV;
- 不要增删行、不要调整列顺序;
- 修改后用 `sql` 校验;校验失败会指出具体行号/列名。
### 9.6 报表读取兼容性
`CompareReportReader` 按以下顺序识别编码:
1. UTF-8 BOM;
2. 严格 UTF-8 解码;
3. 失败则按 GB18030 解码(兼容 Excel 另存为 ANSI/GBK)。
CSV 解析器支持:
- 引号包裹字段;
- 字段内逗号、换行;
- 双引号转义 `""`;
- 空字段;
- 表头前可有空行(容错)。
### 9.7 报表一致性校验
`sql` 不信任报表中的业务字段,只信任“合并选择”。生成脚本前重新执行一次 compare,并校验:
1. 报表行集合与当前差异键集合完全相同;
2. 每行的“类型”与当前分类一致;
3. 主库多、从库多的身份键分别只在一侧存在;不一致行的身份键两边都存在;
4. 报表中的 SN、有效期等字段仅供人工阅读,脚本数据始终取自当前数据库,不取自报表文本;
5. 无重复行、无缺失行、无额外行;
6. 每条记录标注合法。
任一不满足即判定报表过期或被修改,拒绝生成脚本并提示重新 `compare`。
---
## 10. 批量 SQL 设计
### 10.1 前置条件
- 主库、从库均已通过 Schema 校验;
- 两库必需字段一致;
- 报表存在且通过 9.7 校验;
- 报表中所有“不一致”行已填写“主”或“备”;
- 当前工作目录可写(脚本直接写入当前目录);
- 脚本执行前由 DBA 备份主库(脚本头注释中给出命令)。
### 10.2 合并语义
| 报表类型 | 合并选择 | 脚本动作 |
| --- | --- | --- |
| 主库多 | 主 | 无语句(主库已有) |
| 从库多 | 备 | `INSERT OR REPLACE ... SELECT` |
| 不一致(主备 DN 字节相同) | 主 | 无语句(保留主库版本) |
| 不一致(主备 DN 字节相同) | 备 | `INSERT OR REPLACE ... SELECT`(整行替换) |
| 不一致(主备 DN 字节不同,TRIM) | 主 | 无语句(保留主库版本) |
| 不一致(主备 DN 字节不同,TRIM) | 备 | `UPDATE ... SET col = (SELECT ...)`(按主库字节定位) |
| 一致 | - | 不在报表中;主库已有 |
结果:执行后主库 = 原主库全部证书 + 从库多证书;不一致记录按标注取其一;不删除任何证书。
### 10.3 生成步骤
```text
1. 计算 CompareResult
2. 读取报表并校验标注(9.7)
3. 分流:
- 从库多 → INSERT 集合
- 不一致 + 选备 + DN 字节相同 → INSERT 集合
- 不一致 + 选备 + DN 字节不同 → UPDATE 集合
- 不一致 + 选主 / 主库多 → 不需要语句
4. 生成 SqlPlan(统计 + 输出路径)
5. 在当前工作目录创建 merge-<yyyyMMdd-HHmmss>.sql(UTF-8 无 BOM)
6. 写注释头:主从路径、执行方式、字节保持说明、统计
7. ATTACH DATABASE '<从库>' AS cert_src
8. BEGIN IMMEDIATE
9. 每 2000 个身份键一组,生成 INSERT OR REPLACE ... SELECT
10. 每条 TRIM 场景生成一条 UPDATE
11. COMMIT
12. DETACH DATABASE cert_src
13. SELECT 'rows', count(*) + PRAGMA integrity_check 自检
14. 控制台打印计划与 DBA 执行命令
```
说明:
- 身份键 = `hex(subject)` + `certtype`,脚本中只出现十六进制,不出现 DN 文本;
- 主备 DN 字节相同才用 `INSERT OR REPLACE` 整行替换;
- 主备 DN 字节不同(`dn.match=TRIM`)时改用 `UPDATE`:按主库字节定位,按从库字节取新值,避免“按从库字节删不到主库行”导致丢数据;
- 单条 `INSERT` 最多 2000 个身份键(`KEYS_PER_STATEMENT`),避免 SQL 文本过长;
- 脚本使用 `BEGIN IMMEDIATE` / `COMMIT` 包裹,失败可整体回滚;
- 报表文本中的 SN、有效期等只用于人工阅读,脚本数据始终取自当前数据库。
### 10.4 关键 SQL
包装:
```sql
PRAGMA foreign_keys = OFF;
ATTACH DATABASE '/app/cert-compare/slave.db' AS cert_src;
BEGIN IMMEDIATE;
```
从库多 / 不一致选备(主备 DN 字节相同):
```sql
INSERT OR REPLACE INTO usercert_tab
(organid, subject, issuer, sn, pubkey_alg, pubkey, sign_alg, signinfo,
not_before, not_after, certfile, certtype, opertime, stat)
SELECT organid, subject, issuer, sn, pubkey_alg, pubkey, sign_alg, signinfo,
not_before, not_after, certfile, certtype, opertime, stat
FROM cert_src.usercert_tab
WHERE (hex(subject) = '434E3D646966662D736E2D3031' AND certtype = 0)
OR (hex(subject) = 'B3C2B3AC' AND certtype = 2);
```
不一致选备(主备 DN 字节不同,`dn.match=TRIM`):
```sql
UPDATE usercert_tab SET
organid = (SELECT organid FROM cert_src.usercert_tab WHERE hex(subject) = '<从库DN_HEX>' AND certtype = 1),
...
stat = (SELECT stat FROM cert_src.usercert_tab WHERE hex(subject) = '<从库DN_HEX>' AND certtype = 1)
WHERE hex(subject) = '<主库DN_HEX>' AND certtype = 1;
```
末尾自检:
```sql
COMMIT;
DETACH DATABASE cert_src;
SELECT 'rows' AS item, count(*) AS value FROM usercert_tab;
PRAGMA integrity_check;
```
注意:
- `subject` 为 TEXT,SQLite 默认 BINARY 比较,即按原始字节比较;
- 使用 `hex()` 作为身份键,避免 BLOB/TEXT 类型比较陷阱;
- 不使用 `COLLATE NOCASE`,避免改变 DN 语义;
- 若从库存在库内重复键,主键冲突会让事务失败并回滚;
- SQL 语句与字符串字面量全部为 ASCII,仅注释包含中文;文件为 UTF-8 无 BOM。
### 10.5 生成计划
控制台示例:
```text
SQL 生成计划:
主库证书数 4
从库证书数 15
一致 0
主库多 1
从库多 12
不一致 3
写入(从库多+选备) 13
不一致选主 2
不一致选备 1
执行后主库记录数 16
从库(只读引用) /app/cert-compare/slave.db
SQL 文件 /app/cert-compare/merge-20261104-213307.sql
```
### 10.6 执行与结果校验
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
```
| 校验项 | 方法 |
| --- | --- |
| 脚本退出码 | `sqlite3` 返回 0 |
| 文件完整性 | `PRAGMA integrity_check` = ok |
| 总数 | `SELECT 'rows'` = `mainCount + count(从库多)` |
| 从库多已插入 | 按身份键检查存在,且关键字段与从库一致 |
| 不一致选主 | 关键字段与主库一致 |
| 不一致选备 | 关键字段与从库一致 |
| 主库记录未变 | 未在选择“备”集合中的主库记录保持原值 |
| 从库未变 | 执行前后从库 SHA-256 一致 |
| 字节保持 | `hex(subject)` 与来源库一致,`typeof(subject) = 'text'` |
### 10.7 备份、回滚与脚本保留
- 工具只生成脚本,不打开、不修改任何数据库文件;
- 执行前必须备份主库,脚本头注释给出 `cp -p` 备份命令;
- 脚本使用 `BEGIN IMMEDIATE` / `COMMIT`,中途报错时事务不提交;
- 需要回滚时用备份覆盖主库:`cp -p main.db.bak-<时间戳> main.db`;
- 历史脚本保留在当前工作目录,不自动删除、不覆盖(同名时追加 `-2`、`-3`);
- 脚本文件权限遵循系统 umask。
### 10.8 幂等与重复执行
脚本是幂等的:源库相同、标注相同时重复执行结果一致。
- `INSERT OR REPLACE` 按主键 `(subject, certtype)` 覆盖,不会累积重复记录;
- `UPDATE` 按主键定位并覆盖字段,重复执行结果不变;
- 已执行过的脚本再次执行仍得到同一记录集(实测两次执行后均为 16 条);
- 若两次执行之间源库或标注发生变化,需重新 `compare` 并生成新脚本。
---
## 11. 控制台输出、错误与退出码
### 11.1 compare 输出
```text
主库: /app/cert-compare/main.db 证书数: 4
从库: /app/cert-compare/slave.db 证书数: 15
汇总:
一致 0
主库多 1
从库多 12
不一致 3
差异合计 16
报表:
/app/cert-compare/reports/compare-report-20261104-213307.csv
请按“合并选择”列标注:主 或 备,然后执行 sql 生成批量脚本。
```
### 11.2 sql 输出
```text
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 结果。
```
### 11.3 异常分类
| 异常 | 提示 | 退出码 |
| --- | --- | --- |
| 参数缺失/未知参数 | 用法 + 示例 | 2 |
| 配置文件错误 | 具体键与原因 | 2 |
| 文件不存在/不可读 | 路径 + 权限建议 | 2/4 |
| 非 SQLite 文件 | 文件头校验失败 | 4 |
| 表/字段缺失 | 缺失项列表 | 4 |
| 数据库被占用 | SQLITE_BUSY + 建议维护窗口 | 4 |
| 完整性校验失败 | 具体结果 | 4 |
| 报表过期/标注非法 | 行号 + 期望值 + 重新 compare 建议 | 5 |
| SQL 脚本生成/写入失败 | 路径 + 磁盘空间 + 权限建议 | 5 |
### 11.4 日志
- **不生成日志文件**;
- 不创建 `logs/` 目录;
- 运行信息、警告、错误只输出到控制台(stdout/stderr);
- `--debug` 时在控制台输出异常堆栈;
- 不输出证书 PEM、公钥、签名等敏感内容。
### 11.5 安全
- 工具默认只读源库,不直接修改任何数据库;
- 合并由 DBA 执行生成的 SQL 脚本完成,执行前须备份主库;
- 脚本中的路径、十六进制字面量均经过单引号转义;
- 脚本不包含 DN 文本,仅包含 `hex(subject)`;
- 不在控制台/报表中输出证书大字段。
---
## 12. 性能与容量
| 项目 | 设计目标 | 依据 |
| --- | --- | --- |
| 3000 条 compare | ≤ 3s | 两次全表扫描 + 内存 Map |
| 3000 条 sql | ≤ 3s | 只生成脚本,不连接数据库写入 |
| 生成脚本数据量 | 3000 条 | 每 2000 个身份键一组 INSERT,约数百 KB |
| 内存 | ≤ 256MB | 默认不加载 certfile/pubkey |
| 磁盘 | 主库大小 + 脚本大小 | 工具不生成数据库副本 |
| 报表 | 3000 行以内 | 仅差异记录,通常远小于全量 |
| 脚本执行(人工) | 视数据量,3000 条约数秒 | `ATTACH` + 集合 SQL,SQLite 内部复制 |
优化点:
- 单次 SELECT 读取全部投影字段;
- 使用 `HashMap`,初始容量按记录数预估;
- 脚本使用集合 SQL(一条语句覆盖多个身份键),不逐行往返;
- 报表流式写入,不拼接大字符串;
- 报表读取一次性载入内存(差异通常远小于 3000);
- 脚本生成不打开数据库,避免锁库与一致性开销。
---
## 13. 测试设计
### 13.1 测试分层
| 层级 | 内容 |
| --- | --- |
| 单元测试 | 编码、SN 归一化、时间校验、身份键、DiffEngine、CSV 转义/解析、SQL 脚本生成 |
| 集成测试 | 复制测试库到临时目录,执行 compare/标注/sql,并执行生成的脚本 |
| 回归测试 | GBK 十六进制、报表过期、非法标注、脚本幂等性、源库哈希不变 |
| 性能测试 | 生成 3000 条含中文 DN 的数据 |
### 13.2 测试夹具
| 夹具 | 构造 | 预期 |
| --- | --- | --- |
| A 现有测试库 | main 4 条 / slave 15 条 | 主库多 1、从库多 12、不一致 3、一致 0;报表 16 行 |
| B 完全一致 | slave 复制 main | 报表只有表头;脚本无写入语句 |
| C 双证 | 同一 DN,certtype 0/2 | 视为两张证书,不合并、不误报 |
| D 字段差异 | 同 DN+certtype,SN/时间/状态不同 | 分类不一致;选主/选备分别验证 |
| E 报表异常 | 空标注、非法标注、旧报表、缺行、多行 | sql 拒绝并提示行号/原因 |
| F 3000 条 | 生成中文 DN、分布 certtype/状态 | 性能达标;执行后字节不变 |
| G 编码回归 | 含“陈超”“北京”“信雅达”等 GBK 字符 | `hex(subject)` 执行前后一致 |
| H 无日志 | 执行完整流程 | 目录中无 `*.log`、无 `logs/` |
### 13.3 关键断言
1. `hex(subject)` 执行脚本前后逐条一致;
2. `typeof(subject) = 'text'`;
3. 生成脚本前后 main.db / slave.db 的 SHA-256 不变;
4. 执行后总数 = 主库数 + 从库多;
5. 选主的不一致记录等于主库版本,选备的等于从库版本;
6. 相同源库与标注重复执行脚本,结果行数与字段保持一致;
7. 脚本正文(除注释)全部为 ASCII,不含 DN 文本;
8. 报表 UTF-8 BOM 正确,Excel 打开无乱码;
9. GBK 保存的报表可被读取并通过校验;
10. 脚本可在 sqlite3 3.6.20 上一次执行成功;
11. 不生成日志文件,不产生 `.tmp` 文件。
### 13.4 手工验收命令
```bash
cd /app/cert-compare
./cert-compare.sh
# set main /app/cert-compare/main.db
# set slave /app/cert-compare/slave.db
# compare
# (编辑报表“合并选择”列)
# sql --report reports/compare-report-....csv
# 由 DBA 备份后执行(脚本生成在当前目录)
cp -p main.db main.db.bak-$(date +%Y%m%d-%H%M%S)
sqlite3 main.db < merge-*.sql
```
---
## 14. 构建与部署
### 14.1 本地构建
`pom.xml` 关键配置:
```xml
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.36.0.3</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>
```
打包:
```bash
mvn clean package
# 产物:target/cert-compare.jar(shade fat jar)
```
要求:
- `maven-shade-plugin` 合并依赖并设置 `Main-Class`;
- 保留 `META-INF/services/java.sql.Driver`;
- 不压缩/不破坏 sqlite-jdbc 原生库资源;
- 字节码目标 1.8;
- jar 内包含 `org.sqlite` 原生库。
### 14.2 部署目录
```text
/app/cert-compare/
cert-compare.jar
config.properties
cert-compare.sh
reports/ # compare 报表
merge-*.sql # 由 sql 生成到当前目录(启动脚本已 cd 到此目录)
main.db # 现有
slave.db # 现有
main.db.bak-* # 执行脚本前由 DBA 备份
```
启动脚本:
```sh
#!/bin/sh
cd "$(dirname "$0")" || exit 1
exec java -Dfile.encoding=UTF-8 -Xmx256m -jar cert-compare.jar "$@"
```
### 14.3 部署步骤
```bash
# 本地构建后上传
scp target/cert-compare.jar root@172.1.41.139:/app/cert-compare/
scp config.properties root@172.1.41.139:/app/cert-compare/
scp scripts/cert-compare.sh root@172.1.41.139:/app/cert-compare/
ssh root@172.1.41.139
cd /app/cert-compare
chmod +x cert-compare.sh
./cert-compare.sh
```
### 14.4 运行注意
- sqlite-jdbc 会把原生库解压到 `java.io.tmpdir`,当前 `/tmp` 可写且可执行;
- 若生产环境 `/tmp` 挂载 `noexec`,需通过 `-Dorg.sqlite.tmpdir=/app/cert-compare/tmp` 指定可执行目录;
- 生成的脚本只使用 sqlite3 3.6.20 已支持的语法(`ATTACH`、`INSERT OR REPLACE ... SELECT`、`BEGIN IMMEDIATE`、`DETACH`、`PRAGMA integrity_check`);
- 生产执行脚本建议在维护窗口进行,或确认 HSM 应用无写入;
- 执行脚本前必须用 `cp -p` 备份主库;
- 不创建日志目录/日志文件。
### 14.5 部署与实测结果(2026-11-04)
| 项目 | 结果 |
| --- | --- |
| 构建产物 | `target/cert-compare.jar`,9,799,848 字节,Java 8 字节码(major 52) |
| 单元测试 | 12 个用例全部通过(含 `SqlScriptTest` 2 个) |
| 部署路径 | `/app/cert-compare/{cert-compare.jar,config.properties,cert-compare.sh,reports/}` |
| 服务器运行 | JDK 1.8.0_152,sqlite-jdbc 3.36.0.3 原生库加载正常 |
| 测试数据 | main.db 4 条、slave.db 15 条(新增 3 组不一致) |
| compare | 一致 0、主库多 1、从库多 12、不一致 3、差异合计 16 |
| 报表列 | 无“备注”列;“合并选择”为最后一列 |
| 脚本输出位置 | 在 `/tmp/cert-restore-test` 下运行,脚本生成到该当前目录(`merge-20261104-215550.sql`,4,154 字节) |
| sql | 1 条 INSERT(13 个键);从库多 12 + 不一致选备 1 |
| 脚本执行 | 在 main.db 副本上以 sqlite3 3.6.20 执行,输出 `rows|16`、`ok` |
| 中文恢复 | 从库 2 条中文 DN(“北京 / 信雅达”)恢复后逐字段一致,`hex(subject)` 完全相同 |
| 中文解码 | 恢复库中文 DN 经 GB18030→UTF-8 解码为 `C=CN,ST=北京,L=北京,O=信雅达,...` |
| 主库中文保持 | 主库“陈超”记录执行脚本后 `hex(subject)`、SN 均未变化 |
| 选主校验 | `CN=diff-sn-01` 保留主库 SN `0A0B0C01`;`CN=diff-stat-03` 保留主库 `stat=0` |
| 选备校验 | `CN=diff-time-02` 使用从库失效时间 `20311231235959` |
| 字节保持 | 执行后“陈超”记录 `hex(subject)` 与主库完全一致(221 字节),`typeof(subject)=text`,iconv 解码正常 |
| 源库只读 | 生成/执行前后 main.db、slave.db SHA-256 均不变 |
| 幂等 | 同一脚本重复执行两次,记录数均为 16,`integrity_check=ok` |
| 报表兼容 | UTF-8 BOM 与 GB18030 保存的报表均可读取并通过校验 |
| 旧报表兼容 | 含“备注”、合并选择在中间的旧报表仍可读取并生成脚本 |
| 异常校验 | 空标注/非法标注/缺失报表均拒绝,退出码 5 |
| 交互模式 | `set main`、`set slave`、`status`、`sql --report`、`quit` 正常,退出码 0 |
| 日志 | 未生成 `*.log`,未创建 `logs/`,无 `.tmp` 残留 |
测试数据脚本:`scripts/test-data-main.sql`、`scripts/test-data-slave.sql`;执行前已备份为 `main.db.bak-*`、`slave.db.bak-*`。
---
## 15. 风险与对策
| 风险 | 影响 | 对策 |
| --- | --- | --- |
| GBK 字段被按 UTF-8 读取 | DN 乱码、错误匹配 | 强制 `hex` 读取;脚本走 SQL 内部复制;编码回归测试 |
| 报表被 Excel 改坏 | 标注丢失、DN_HEX 变科学计数法 | `HEX:` 前缀;重新 compare;严格校验报表与当前库一致 |
| 报表保存为 GBK | 读取乱码 | 读取器自动识别 UTF-8/GB18030 |
| 报表过期 | 脚本内容与当前库不符 | sql 前重新 compare 并逐项校验,失败即拒绝 |
| 不一致行未标注 | 无法决定版本 | 默认留空;sql 拒绝并提示行号 |
| 标注写错(主/备) | 选错证书版本 | 计划中展示选主/选备数量;执行后校验字段与所选源一致 |
| 脚本执行出错 | 主库部分变更 | `BEGIN IMMEDIATE` / `COMMIT` 事务;执行前备份;失败按备份回滚 |
| 生产库正在写入 | 脚本执行锁等待、`SQLITE_BUSY` | 维护窗口;脚本使用 `BEGIN IMMEDIATE` |
| 从库路径变化 | 脚本 ATTACH 失败 | 生成脚本时写入绝对路径;执行前确认从库未移动 |
| 仅按 DN 比对 | 双证被误判 | 身份键加入 certtype |
| DN 属性大小写/空格差异 | 逻辑同一 DN 被当作两张 | 默认严格匹配,预留 TRIM;报表自行过滤 |
| TRIM 场景主备 DN 字节不同 | 按从库字节删除会丢数据 | 改用 `UPDATE`,按主库字节定位 |
| SN 前导 0 差异 | 误报不一致 | 默认 HEX 归一化,展示保留原值 |
| certtype 含义未知 | 报表可读性差 | 本期显示数值,预留映射配置 |
| 单条 SQL 过长 | sqlite3 解析慢或失败 | 每 2000 个身份键拆分一条语句 |
| 测试数据仅 19 条(主 4 + 从 15) | 性能/边界覆盖不足 | 构造 3000 条及异常夹具 |
| `/tmp` noexec | 原生库加载失败 | 支持 `org.sqlite.tmpdir` |
---
## 16. 待确认事项
1. `certtype` 取值 0/1/2/4/5 的业务名称,是否需要在报表中显示中文;
2. 生产环境是否允许维护窗口/停写,或必须在线只读;
3. SQL 脚本命名(当前 `merge-<时间戳>.sql`)与历史脚本保留策略;
4. 主库多/从库多的“合并选择”是否允许改成空(跳过);本期按“所有证书都需要”固定为主/备;
5. SN 前导 0 归一化是否与现网业务查询完全一致;
6. 是否需要 DN 近似匹配(大小写、属性顺序)作为可选模式;
7. 报表是否需要 XLSX 格式(当前 CSV,Excel 可直接编辑)。
---
## 17. 附录
### 17.1 表结构(测试库)
```sql
CREATE TABLE "usercert_tab" (
`organid` varchar(32) NOT NULL,
`subject` varchar(256) NOT NULL,
`issuer` varchar(256) NOT NULL,
`sn` varchar(64) NOT NULL,
`pubkey_alg` int(4) NOT NULL,
`pubkey` varchar(1032) NOT NULL,
`sign_alg` int(4) NOT NULL,
`signinfo` varchar(512) NOT NULL,
`not_before` varchar(14) NOT NULL,
`not_after` varchar(14) NOT NULL,
`certfile` varchar(2048) NOT NULL,
`certtype` smallint(4) NOT NULL,
`opertime` varchar(14) NOT NULL,
`stat` smallint(4) NOT NULL,
PRIMARY KEY (`subject`, `certtype`)
);
```
### 17.2 测试库分类预期
| 分类 | 数量 | 说明 |
| --- | --- | --- |
| ONLY_MAIN(主库多) | 1 | 主库“陈超”证书不在从库 |
| ONLY_SLAVE(从库多) | 12 | 从库原有 12 张证书不在主库 |
| DIFF(不一致) | 3 | 3 组构造数据:SN、失效时间、状态各一组 |
| EQUAL(一致) | 0 | 无相同身份键且字段全同 |
| 报表记录 | 16 | 1 + 12 + 3 |
| 执行脚本后主库总数 | 16 | 4(主库)+ 12(从库多) |
### 17.3 已验证的关键命令
```bash
# 版本与环境
java -version
sqlite3 --version
cat /etc/redhat-release
# 完整性
sqlite3 main.db 'pragma integrity_check;'
sqlite3 slave.db 'pragma integrity_check;'
# 编码证据
sqlite3 main.db "select hex(subject) from usercert_tab;"
# JDBC 兼容性(服务器)
java -cp '.:sqlite-jdbc-3.36.0.3.jar' JdbcProbe /app/cert-compare/main.db
# 生成脚本并在主库副本上执行
./cert-compare.sh sql --report reports/compare-report-....csv
cp -p main.db test-main.db
sqlite3 test-main.db < merge-....sql
sqlite3 test-main.db "select count(*) from usercert_tab; pragma integrity_check;"
```
### 17.4 环境验证证据摘要
| 证据 | 结果 |
| --- | --- |
| JDBC 读取原始字节 | `hex=...B3C2B3AC...`,GB18030 解码为“陈超” |
| JDBC `getString` | 长度 109,含 `U+FFFD`,不可用于比对 |
| ATTACH INSERT SELECT | 插入后 `hex` 相同、`typeof=text` |
| 事务回滚 | 记录数恢复,回滚成功 |
| 脚本执行(sqlite3 3.6.20) | `rows|16`、`ok`,重复执行仍为 16 条 |
| sqlite-jdbc 3.36 | 在 glibc 2.12 加载成功 |
| sqlite-jdbc 3.45 | 缺 slf4j,不选 |
| 证书字段一致性 | 13/13 SN、有效期与 DER 一致 |
---
## 18. 后续实施顺序
1. 搭建 Maven 工程与 fat jar 构建;
2. 实现配置、数据库校验、`set`/`status`;
3. 实现字节级 DAO 与身份键、compare 引擎;
4. 实现单份 CSV 报表(含“合并选择”列);
5. 实现报表读取、编码识别、标注校验;
6. 实现 SQL 生成计划、`ATTACH + INSERT OR REPLACE / UPDATE` 脚本;
7. 编写测试夹具(含 3000 条生成器)并验收;
8. 部署到 `/app/cert-compare`,完成真实库只读比对与脚本执行演练。