外部表模块 external.md 24 KB

外部表模块 external

本文引用的文件

  • external_tables.sql
  • external_table_field.sql
  • EntryRec.java
  • EntryRecMapper.java
  • EntryRecMapper.xml
  • ExternalEntryRecLoader.java
  • ExternalEntryRecCleaner.java
  • CleanFactory.java
  • StrategyType.java
  • StreamingEtlListener.java
  • StrConsts.java
  • SearchDoc.java
  • SearchDocIndexService.java

目录

  1. 引言
  2. 项目结构
  3. 核心组件
  4. 架构总览
  5. 详细组件分析
  6. 依赖关系分析
  7. 性能与同步策略
  8. 权限控制与案件隔离
  9. 常见问题排查
  10. 结论

引言

外部表模块 external 用于管理来自第三方或外部来源的数据,例如出入境记录、协居人员、同车违章、违法犯罪人员、在逃人员等。这些外部数据并不直接写入内部业务主表(如 person、call、trans、track),而是通过独立的 DuckDB 外部表存储,并通过字段映射、清洗加载流程、搜索索引和查询服务,最终与内部实体打通。

该模块的核心职责包括:

  • 定义外部表结构和字段映射;
  • 将外部 Excel/表格数据清洗为统一实体对象;
  • 将实体对象写入 DuckDB 外部表;
  • 将可检索内容写入 Elasticsearch 搜索索引;
  • 提供按身份证号、姓名、车牌号、案件等维度的查询能力;
  • 在查询侧按案件隔离并支持权限控制。

当前仓库中已实现的外部数据来源至少包括:

外部数据来源 对应外部表 说明
出入境证件信息 cert_info 人员出入境证件基础信息
出入境记录信息 entry_rec 人员出入口岸记录
机动车信息 vehicle_info 车辆登记信息
驾驶证信息 lic_info 驾驶人档案信息
机动车违章信息 veh_violation 车辆违法记录
同户人 household 同一户籍人员
同住址 co_resident 协居人员
同车违章 co_veh_viol 同车违法关联人员
违法犯罪人员信息 criminal 违法犯罪人员基础信息
在逃人员登记信息 fugitive 在逃人员登记
在逃人员撤销信息 fugi_revoke 在逃人员撤销记录
在逃同案人员 fugi_co 在逃同案人员
在逃同案撤销人员 fugi_co_revoke 在逃同案撤销人员

项目结构

外部表模块涉及 SQL 建表脚本、Java 实体、MyBatis Mapper、数据清洗器、数据加载器、工厂注册以及搜索索引模型。其组织方式不是单一包内集中,而是按职责拆分:

graph TB
    SQL["SQL 建表脚本<br/>external_tables.sql"] --> FieldMap["字段映射配置<br/>external_table_field.sql"]
    FieldMap --> Cleaner["数据清洗器<br/>ExternalEntryRecCleaner 等"]
    Cleaner --> Loader["数据加载器<br/>ExternalEntryRecLoader 等"]
    Loader --> DuckDB["DuckDB 外部表<br/>entry_rec 等"]
    Loader --> SearchDoc["搜索文档模型<br/>SearchDoc"]
    SearchDoc --> Index["Elasticsearch 索引<br/>zsjz_search"]
    Mapper["MyBatis Mapper<br/>EntryRecMapper"] --> DuckDB

图表来源

  • external_tables.sql:1-200
  • external_table_field.sql:1-80
  • ExternalEntryRecCleaner.java:1-14
  • ExternalEntryRecLoader.java:1-51
  • EntryRecMapper.java:1-12
  • SearchDoc.java:17-94

章节来源

  • external_tables.sql:1-430
  • external_table_field.sql:1-365
  • CleanFactory.java:15-166

核心组件

外部表结构

外部表使用 DuckDB 表结构定义,字段以字符串为主,便于从 Excel 或 CSV 导入后直接落库。目前已定义的表包括出入境证件、出入境记录、机动车、驾驶证、车辆违法、同户人、协居、同车违章、违法犯罪人员、在逃人员及其撤销和同案相关表。

以出入境记录表为例,关键字段包括:

字段名 含义 用途
id 主键 行唯一标识
fileId 文件标识 关联外部文件来源
sheetId 工作表标识 区分不同工作表
cid 身份证号 与内部人员匹配的关键维度
certNo 证件号码 辅助身份匹配
ioDate 出入日期 时间维度查询
ioTime 出入时间 精确时间查询
port 出入口岸 地点维度查询
vehicle 交通工具 与交通轨迹关联
photo 照片 附件或展示用途

章节来源

  • external_tables.sql:1-200

字段映射结构

字段映射由 table_info 和 table_field 两类元数据组成。虽然仓库未给出这两张表的完整 DDL,但从 external_table_field.sql 可见其作用:

  • table_info 表示一张外部表模板,包含中文名、英文名、分类、是否内部表、是否有物理表等;
  • table_field 表示该模板下的字段,包含中文名称、英文字段名、字段类型、是否必填、排序等。

示例中所有外部数据源都被归类为“公安数据库”,且标记为有内部表、有物理表。这意味着这些外部数据不仅会被清洗入库,还会进入系统的标准化处理链路。

章节来源

  • external_table_field.sql:1-365

实体映射 EntryRec

EntryRec 是出入境记录的外部实体模型,继承自 Cleaned,并使用 MyBatis Plus 的注解映射到 entry_rec 表。它把外部列名映射为 Java 字段,并提供一个 str() 方法,将多个字段拼接成搜索文本。

Java 字段 外部表字段 说明
personName personName 姓名
certType certType 证件类别
certNo certNo 证件号码
gender gender 性别
nation nation 国籍
visaType visaType 签证种类
cid cid 身份证号
birth birth 出生日期
ioDate ioDate 出入日期
ioTime ioTime 出入时间
port port 出入口岸
transport transport 交通方式
vehicle vehicle 交通工具
visaNo visaNo 签证号码
country country 前往或归来国家
photo photo 照片

str() 方法把所有可搜索字段用特殊分隔符连接,供搜索索引使用。

章节来源

  • EntryRec.java:1-138

数据访问层 EntryRecMapper

EntryRecMapper 是一个简单的 MyBatis Plus Mapper,指向外部数据源。它使用了动态数据源注解,目标数据源键为从常量类中读取的从库键。XML 映射文件为空,表示使用默认 CRUD。

这个设计表明:外部表通常不需要复杂业务 SQL,主要走标准增删改查;复杂查询可能由上层服务组合或通过搜索索引完成。

章节来源

  • EntryRecMapper.java:1-12
  • EntryRecMapper.xml:1-5
  • StrConsts.java:5-8

架构总览

外部表模块的整体流程如下:

sequenceDiagram
    participant User as "用户或任务"
    participant ETL as "流式 ETL 监听器"
    participant Factory as "清洗工厂"
    participant Cleaner as "外部数据清洗器"
    participant Loader as "外部数据加载器"
    participant DuckDB as "DuckDB 外部表"
    participant Search as "搜索索引服务"
    
    User->>ETL: "上传外部表格"
    ETL->>Factory: "根据主模板 ID 创建清洗器和加载器"
    Factory-->>ETL: "返回 ExternalEntryRecCleaner / ExternalEntryRecLoader"
    ETL->>Cleaner: "解析并清洗一行外部数据"
    Cleaner-->>Loader: "输出 EntryRec 实体"
    Loader->>DuckDB: "写入 entry_rec 外部表"
    Loader->>Search: "写入搜索文档"
    Search-->>User: "可被后续查询检索"

图表来源

  • StreamingEtlListener.java:289-302
  • CleanFactory.java:120-166
  • ExternalEntryRecCleaner.java:1-14
  • ExternalEntryRecLoader.java:10-51
  • SearchDocIndexService.java:101-136

详细组件分析

外部表注册机制

外部表并不是靠 Java 注解自动发现,而是通过枚举值、SQL 元数据和工厂静态注册共同完成。

枚举注册 StrategyType

StrategyType 中为每个外部数据类型分配了数字编码,并与 SQL 中的 main_id 对应。例如:

枚举 编码 中文名称
CERT_INFO 51 出入境证件信息
ENTRY_REC 52 出入境记录信息
VEHICLE_INFO 53 机动车信息
LIC_INFO 54 驾驶证信息
VEH_VIOLATION 55 机动车违章信息
HOUSEHOLD 56 同户人
CO_RESIDENT 57 同住址
CO_VEH_VIOL 58 同车违章
CRIMINAL 59 违法犯罪人员信息
FUGITIVE 60 在逃人员登记信息
FUGI_REVOKE 61 在逃人员撤销信息
FUGI_CO 62 在逃同案人员
FUGI_CO_REVOKE 63 在逃同案撤销人员

SQL 元数据注册

external_table_field.sql 中通过插入 table_info 和 table_field 完成外部表模板注册。每条外部表模板都包含:

  • id:模板主键;
  • main_id:与枚举编码一致;
  • table_name_cn:中文表名;
  • table_name_en:英文表名;
  • classify:数据来源分类;
  • has_inner:是否内部表;
  • has_table:是否有物理表。

工厂注册 CleanFactory

CleanFactory 在静态块中把所有外部数据类型绑定到具体的清洗器和加载器。新增外部数据源时,需要同时注册:

  1. 清洗器;
  2. 加载器;
  3. StrategyType 枚举;
  4. SQL 模板和字段映射;
  5. 可选的 Java 实体和 Mapper。

章节来源

  • StrategyType.java:47-59
  • external_table_field.sql:1-365
  • CleanFactory.java:15-166

数据清洗与加载流程

外部数据清洗分为两个阶段:

  1. 清洗阶段:将原始行数据转换为统一的 Java 实体;
  2. 加载阶段:将实体写入 DuckDB 外部表,并构建搜索文档。

以出入境记录为例:

flowchart TD
    Start["开始处理外部行"] --> CreateEntity["创建 EntryRec 实体"]
    CreateEntity --> MapFields["映射外部字段到实体属性"]
    MapFields --> Validate{"字段是否有效"}
    Validate -->|否| Skip["跳过或记录脏数据"]
    Validate -->|是| WriteDuckDB["写入 DuckDB 外部表"]
    WriteDuckDB --> BuildDoc["构建搜索文档"]
    BuildDoc --> AddIndex["批量写入 Elasticsearch"]
    AddIndex --> End["结束"]
    Skip --> End

图表来源

  • ExternalEntryRecCleaner.java:1-14
  • ExternalEntryRecLoader.java:10-51
  • SearchDocIndexService.java:101-136

清洗器 ExternalEntryRecCleaner

ExternalEntryRecCleaner 继承自外部数据清洗抽象类,只负责创建 EntryRec 实体。具体字段解析逻辑由父类或上游解析器完成。这种设计让每种外部表只需要一个轻量级清洗器。

加载器 ExternalEntryRecLoader

ExternalEntryRecLoader 继承自通用数据加载器,负责:

  • 调用 DuckDB 追加器逐列写入 entry_rec 表;
  • 将实体转换为搜索文档;
  • 通过搜索索引服务写入 Elasticsearch。

加载器中显式写入的列包括主键、文件标识、工作表标识、姓名、证件信息、身份信息、时间、口岸、交通工具和国家等。

章节来源

  • ExternalEntryRecCleaner.java:1-14
  • ExternalEntryRecLoader.java:10-51

外部数据与内部实体的打通

外部表本身不直接替代 person、call、trans、track,而是作为补充视图和关联维度。打通方式包括:

  • 通过身份证号 cid、姓名、证件号与内部人员匹配;
  • 通过车牌号、驾驶证号与内部车辆、交易、轨迹数据关联;
  • 通过协居、同户、同车违章建立间接关系;
  • 通过搜索索引统一检索,再在上层服务中与内部实体聚合。

当前代码中,EntryRec 没有直接写 person 表,而是写入 entry_rec 外部表,并生成搜索文档。因此,“打通”更多体现在查询和分析层,而不是强制覆盖内部主数据。

章节来源

  • EntryRec.java:1-138
  • SearchDoc.java:17-94

依赖关系分析

classDiagram
    class StrategyType {
        +CERT_INFO
        +ENTRY_REC
        +VEHICLE_INFO
        +LIC_INFO
        +VEH_VIOLATION
        +HOUSEHOLD
        +CO_RESIDENT
        +CO_VEH_VIOL
        +CRIMINAL
        +FUGITIVE
        +FUGI_REVOKE
        +FUGI_CO
        +FUGI_CO_REVOKE
        +fromCode(code)
    }

    class CleanFactory {
        -DATA_CLEANER_MAP
        -DATA_LOADER_MAP
        +createDataCleaner(strategyType, funcRegx, currentSheet)
        +createDataLoader(strategyType)
        +registerDataCleaner(strategyType, supplier)
        +registerDataLoader(strategyType, supplier)
    }

    class ExternalEntryRecCleaner {
        +createEntity()
    }

    class ExternalEntryRecLoader {
        +doWrite(entity)
    }

    class EntryRec {
        +str()
    }

    class EntryRecMapper

    class SearchDoc

    CleanFactory --> StrategyType : "按枚举选择策略"
    CleanFactory --> ExternalEntryRecCleaner : "注册清洗器"
    CleanFactory --> ExternalEntryRecLoader : "注册加载器"
    ExternalEntryRecCleaner --> EntryRec : "创建实体"
    ExternalEntryRecLoader --> EntryRec : "消费实体"
    EntryRecMapper --> EntryRec : "映射外部表"
    ExternalEntryRecLoader --> SearchDoc : "构建搜索文档"

图表来源

  • StrategyType.java:47-59
  • CleanFactory.java:15-166
  • ExternalEntryRecCleaner.java:1-14
  • ExternalEntryRecLoader.java:10-51
  • EntryRec.java:1-138
  • EntryRecMapper.java:1-12
  • SearchDoc.java:17-94

章节来源

  • CleanFactory.java:15-166
  • StrategyType.java:47-59

性能与同步策略

写入性能

ExternalEntryRecLoader 使用 DuckDB 追加器逐列写入,并在每次写入后构建搜索文档。搜索文档写入由 SearchDocIndexService 做批量缓冲,避免频繁远程索引更新。

建议:

  • 对大表外部数据优先使用批量写入;
  • 避免在加载器中执行复杂 SQL;
  • 对图片等大字段仅保存路径或引用,不直接入库;
  • 对高频查询字段建立合适索引;
  • 对非实时查询场景,优先使用 Elasticsearch 检索,再按需回查 DuckDB。

同步策略

外部数据同步应遵循以下原则:

  1. 幂等写入:相同 fileId、sheetId、id 的记录应可重复导入而不产生重复数据;
  2. 增量同步:优先按更新时间或批次号增量导入;
  3. 失败重试:单行失败不应中断整批导入;
  4. 审计日志:记录每批文件的来源、行数、成功数、失败数;
  5. 一致性校验:定期比对外部源与 DuckDB 外部表行数、关键字段哈希。

脏数据清洗

外部数据常见脏数据包括:

  • 空身份证号;
  • 身份证号格式错误;
  • 姓名与身份证号不匹配;
  • 日期字段不可解析;
  • 地址字段缺失或异常;
  • 照片字段为空或无效路径。

建议清洗策略:

  • 必填字段为空时丢弃或标记脏数据;
  • 身份证号长度、校验位校验;
  • 日期字段标准化;
  • 姓名、证件号去空格、全角半角统一;
  • 保留原始值和清洗后值,便于回溯;
  • 提供脏数据导出和修复入口。

字段对齐

字段对齐应分三层:

  1. 表级对齐:通过 table_info 明确数据来源、分类、是否内部表;
  2. 字段级对齐:通过 table_field 明确中英文字段名、类型、排序;
  3. 业务级对齐:在清洗器或服务层将外部字段映射到内部标准字段,例如将外部 cid 映射为内部人员身份证号。

章节来源

  • ExternalEntryRecLoader.java:10-51
  • SearchDocIndexService.java:101-136
  • external_table_field.sql:1-365

权限控制与案件隔离

搜索索引的案件隔离

搜索文档模型 SearchDoc 明确强调:所有案件共用单一索引,但通过 caseId 强制注入;查询和删除时必须携带 caseId 过滤。文档 _id 由服务端组装,通常为 caseId|tableName|id。

这意味着外部表数据虽然是共享外部数据,但在系统查询侧应按案件隔离,防止跨案件泄露。

查询隔离

推荐查询模式:

  • 先通过身份证号、姓名、车牌号等条件搜索 SearchDoc;
  • 从搜索结果中提取 caseId;
  • 查询 DuckDB 外部表时,也带上 caseId 过滤;
  • 若需要跨案件汇总,应在更高权限的服务层进行,并记录审计日志。

权限控制建议

当前仓库证据显示搜索索引和服务层强调 caseId 隔离,但没有看到完整的角色权限控制实现。建议补充:

  • 按用户角色控制能否查看外部表详情;
  • 按案件负责人控制能否查看协居、违法犯罪、在逃人员等敏感数据;
  • 对外部表导出增加审批和脱敏;
  • 对敏感字段(照片、身份证、地址)做访问控制和脱敏展示。

章节来源

  • SearchDoc.java:17-94
  • SearchDocIndexService.java:101-179

常见问题排查

无法找到清洗或加载策略

如果报错提示未找到某个策略类型,通常原因包括:

  • StrategyType 中缺少对应枚举;
  • CleanFactory 中没有注册对应的清洗器或加载器;
  • SQL 中 main_id 与枚举编码不一致;
  • 新外部表没有添加 table_info 和 table_field。

章节来源

  • CleanFactory.java:120-166
  • StrategyType.java:47-59

外部表数据未出现在搜索结果

可能原因:

  • 加载器未写入搜索文档;
  • 搜索索引服务未初始化索引;
  • 批量缓冲区未刷新;
  • 查询时未携带正确的 caseId;
  • 搜索关键词未覆盖 EntryRec.str() 生成的字段。

章节来源

  • ExternalEntryRecLoader.java:35-51
  • SearchDocIndexService.java:89-136

外部表字段缺失或错位

可能原因:

  • table_field 中字段顺序与实际 Excel 列不一致;
  • 字段中文名与解析器期望不一致;
  • 外部表结构变更未同步到 SQL 和元数据;
  • 清洗器未正确创建实体。

章节来源

  • external_table_field.sql:1-365
  • ExternalEntryRecCleaner.java:1-14

结论

外部表模块 external 是一个围绕外部来源数据构建的数据接入与分析支撑层。它通过 SQL 表结构定义、字段映射、清洗加载器、DuckDB 外部表和 Elasticsearch 搜索索引,把协居、车辆违法、违法犯罪、在逃人员等外部数据纳入系统可用范围。

该模块的设计优点在于:

  • 外部表与内部主表解耦;
  • 通过枚举和工厂统一管理外部数据类型;
  • 通过搜索索引实现多源数据统一检索;
  • 通过 caseId 实现案件级查询隔离。

未来改进重点应放在:

  • 完善权限控制和数据脱敏;
  • 增强外部数据质量校验和脏数据处理;
  • 优化大表同步性能和一致性保障;
  • 提供更清晰的字段对齐和业务映射规则;
  • 加强外部表与内部 person、call、trans、track 的关联分析和可视化。