# 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 set slave set main | set slave # 无参数:显示当前值 status compare sql [--report ] 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 ` | 指定已标注的报表;缺省使用本次会话最近一次 compare 的报表,若没有则使用报表目录中最新的 `compare-report-*.csv` | `sql` 不写数据库、不改源库,只在当前工作目录生成 `merge-.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 3. 从 slave 读取投影记录 -> List 4. 分别构建 Map> 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:`/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-.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 1.8 1.8 UTF-8 org.xerial sqlite-jdbc 3.36.0.3 junit junit 4.13.2 test ``` 打包: ```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`,完成真实库只读比对与脚本执行演练。