# 征信维护设计文档

## 1. 背景

当前仓库已经具备两项征信解析基础能力：

- `ccdi-lsfx` 已提供 `POST /lsfx/credit/parse` 征信解析联调接口，可接收 HTML 并返回 `lx_header`、`lx_debt`、`lx_publictype`
- `lsfx-mock-server` 已支持从征信 HTML 中解析员工姓名和身份证号，能够稳定返回本地联调 payload

本次需求是在“信息维护”菜单下新增“征信维护”菜单，支持批量导入员工征信 HTML 文件并提交上传。系统需要调用现有征信解析接口完成数据解析，并把返回结果批量落库。页面同时要展示当前已维护的员工征信信息，并支持删除现有征信数据。

## 2. 已确认约束

- 菜单挂在“信息维护”下，页面为独立菜单，不挂到员工维护弹窗内
- 页面主列表按员工维度展示摘要，一行一个员工
- 批量上传按员工维度原子化处理，一个员工失败不影响其他员工
- 负债明细表名固定为 `ccdi_debts_info`
- 负债信息按照 `assets/征信解析/ccdi_debts_info.xlsx` 设计
- 负面信息按照接口字段单独设计一张表
- 每个员工只保留最新的征信信息
- 列表操作列支持删除现有征信信息
- 不新增征信主表，主列表通过明细表实时聚合

## 3. 目标

- 在“信息维护”下新增“征信维护”菜单与页面
- 支持一次上传多个 `.html/.htm` 征信文件
- 上传后逐文件调用征信解析接口
- 按员工身份证号归户，并按“最新征信优先”规则覆盖写入
- 在页面展示员工维度的征信摘要列表
- 在详情中展示负债明细和负面信息
- 支持按员工删除当前已维护的征信数据

## 4. 非目标

- 不改造现有 `POST /lsfx/credit/parse` 的协议语义
- 不新增异步任务、轮询状态页或上传历史模块
- 不保存多版本征信记录
- 不保存原始 HTML 文件或原始 payload JSON
- 不增加说明书之外的补丁性兜底方案

## 5. 方案对比

### 5.1 方案 A：三张业务表，增加征信主表

新增征信主表保存员工最新摘要，再配合负债表和负面信息表。

优点：

- 列表查询简单
- 聚合逻辑更少

缺点：

- 引入额外主表，超出本次最短路径
- 同一份摘要数据会与明细形成重复维护

### 5.2 方案 B：仅保留两张业务表，列表实时聚合

只建设 `ccdi_debts_info` 与负面信息表，主列表按员工维度实时聚合展示。

优点：

- 满足“负债表固定命名”和“负面信息单独建表”
- 不引入额外主表，路径最短
- 删除和覆盖逻辑仍然清晰，按员工清空两张表即可

缺点：

- 列表查询需要做员工维度聚合

### 5.3 方案 C：两张业务表加原始 payload 存档

在方案 B 基础上额外保存原始 payload 或原始 HTML。

优点：

- 排查解析问题时信息更全

缺点：

- 增加冗余与维护成本
- 超出当前需求

## 6. 最终方案

采用方案 B：仅新增 `ccdi_debts_info` 和 `ccdi_credit_negative_info` 两张业务表，列表按员工维度实时聚合，不新增征信主表。

选择原因：

- 满足“每个员工只保留最新征信信息”的要求
- 满足“负债信息按 Excel 设计、负面信息单独建表”的要求
- 不额外增加主表，保持最短路径实现
- 删除逻辑直接按员工清理两张业务表，语义明确

## 7. 总体架构

业务链路如下：

`征信维护页面 -> 征信维护业务接口 -> 征信维护服务 -> /lsfx/credit/parse -> 征信解析服务`

职责拆分如下：

- 征信维护页面
  - 选择多个 HTML 文件并提交上传
  - 展示员工维度征信摘要列表
  - 打开详情弹窗查看负债明细与负面信息
  - 删除员工当前征信数据

- 征信维护业务接口
  - 对外提供上传、列表、详情、删除接口
  - 不直接把前端耦合到 `ccdi-lsfx`

- 征信维护服务
  - 逐文件调用征信解析接口
  - 根据员工身份证号归户
  - 校验“是否为最新征信”
  - 单员工事务覆盖写入两张业务表

- `ccdi-lsfx`
  - 保持现有征信解析客户端能力
  - 继续作为外部征信解析接入边界

## 8. 数据模型设计

### 8.1 负债明细表 `ccdi_debts_info`

一条记录表示一条负债明细，字段按 `assets/征信解析/ccdi_debts_info.xlsx` 设计：

- `debt_id`：`BIGINT` 主键，自增
- `person_id`：`VARCHAR(18)`，员工身份证号
- `person_name`：`VARCHAR(100)`，员工姓名
- `query_date`：`DATE`，征信查询日期
- `debt_main_type`：`VARCHAR(50)`，负债大类
- `debt_sub_type`：`VARCHAR(50)`，负债小类
- `creditor_type`：`VARCHAR(50)`，债权人类型
- `debt_name`：`VARCHAR(100)`，负债名称
- `principal_balance`：`DECIMAL(18,2)`，负债本金余额
- `debt_total_amount`：`DECIMAL(18,2)`，负债总额
- `debt_status`：`VARCHAR(20)`，负债状态
- `create_by`：`VARCHAR(64)`
- `create_time`：`DATETIME`
- `update_by`：`VARCHAR(64)`
- `update_time`：`DATETIME`

建议索引：

- `idx_person_id(person_id)`
- `idx_query_date(query_date)`
- `idx_person_query_date(person_id, query_date)`

### 8.2 负面信息表 `ccdi_credit_negative_info`

一条记录表示一个员工当前最新征信对应的负面信息汇总：

- `negative_id`：`BIGINT` 主键，自增
- `person_id`：`VARCHAR(18)`，员工身份证号
- `person_name`：`VARCHAR(100)`，员工姓名
- `query_date`：`DATE`，征信查询日期
- `civil_cnt`：`INT`，民事案件笔数
- `enforce_cnt`：`INT`，强制执行笔数
- `adm_cnt`：`INT`，行政处罚笔数
- `civil_lmt`：`DECIMAL(18,2)`，民事案件金额
- `enforce_lmt`：`DECIMAL(18,2)`，强制执行金额
- `adm_lmt`：`DECIMAL(18,2)`，行政处罚金额
- `create_by`：`VARCHAR(64)`
- `create_time`：`DATETIME`
- `update_by`：`VARCHAR(64)`
- `update_time`：`DATETIME`

建议索引：

- `uk_person_id(person_id)` 唯一索引

## 9. 解析字段映射设计

### 9.1 归户字段

从解析结果 `lx_header` 中读取：

- `query_cert_no` -> 员工身份证号 `person_id`
- `query_cust_name` -> 员工姓名 `person_name`
- `report_time` -> 征信查询日期 `query_date`

`query_cert_no` 作为员工唯一归户标识，并校验该身份证号必须存在于 `ccdi_base_staff`。

### 9.2 `lx_debt` 到 `ccdi_debts_info` 的映射

`lx_debt` 当前为 7 组固定字段，每组映射成一条负债明细：

| 接口前缀 | 负债大类 | 负债小类 | 债权人类型 | 负债名称 |
| --- | --- | --- | --- | --- |
| `uncle_bank_house` | 银行 | 住房贷款 | 银行 | 未结清银行住房贷款 |
| `uncle_bank_car` | 银行 | 汽车贷款 | 银行 | 未结清银行汽车贷款 |
| `uncle_bank_manage` | 银行 | 经营贷款 | 银行 | 未结清银行经营贷款 |
| `uncle_bank_consume` | 银行 | 消费贷款 | 银行 | 未结清银行消费贷款 |
| `uncle_bank_other` | 银行 | 其他贷款 | 银行 | 未结清银行其他贷款 |
| `uncle_not_bank` | 非银 | 非银行贷款 | 非银 | 未结清非银行贷款 |
| `uncle_credit_card` | 银行 | 信用卡 | 银行 | 未结清信用卡 |

字段映射规则：

- `*_bal` -> `principal_balance`
- `*_lmt` -> `debt_total_amount`
- `*_state` -> `debt_status`

落库过滤规则：

- 当某组 `*_state` 为 `-9999` 时，表示不存在该类型负债，不生成明细记录
- 当某组 `principal_balance`、`debt_total_amount` 都为空或 `0`，且 `debt_status` 为空时，不生成明细记录
- 其余情况生成一条明细

### 9.3 `lx_publictype` 到 `ccdi_credit_negative_info` 的映射

按接口字段直接映射：

- `civil_cnt` -> `civil_cnt`
- `enforce_cnt` -> `enforce_cnt`
- `adm_cnt` -> `adm_cnt`
- `civil_lmt` -> `civil_lmt`
- `enforce_lmt` -> `enforce_lmt`
- `adm_lmt` -> `adm_lmt`

负面风险字段值为 `-9999` 或 `-9999.0` 时，表示不存在对应风险类型；次数按 `0` 处理，金额按空值处理。

## 10. 最新征信判定与覆盖策略

系统只保留员工最新征信，规则如下：

- 以 `person_id` 作为员工唯一标识
- 以 `query_date` 作为征信最新时间判定字段
- 员工上传新文件时：
  - 若库中无现有征信数据，直接写入
  - 若新 `query_date` 大于等于现有 `query_date`，覆盖旧数据
  - 若新 `query_date` 小于现有 `query_date`，该员工本次文件判定为失败，不覆盖

单员工覆盖写入步骤：

1. 删除 `ccdi_debts_info` 中该员工旧记录
2. 删除 `ccdi_credit_negative_info` 中该员工旧记录
3. 插入本次解析后的最新负债明细
4. 插入本次解析后的最新负面信息

以上 4 步置于同一事务中，保证员工维度原子化。

## 11. 页面设计

### 11.1 菜单

在“信息维护”下新增菜单：

- 菜单名称：`征信维护`
- 路由：`creditInfo`
- 组件：`ccdiCreditInfo/index`
- 权限前缀：`ccdi:creditInfo:*`

### 11.2 顶部操作区

- 批量上传征信 HTML 按钮
- 查询条件：
  - 员工姓名
  - 柜员号
  - 身份证号
  - 是否已维护征信

上传弹窗要求：

- 仅允许选择 `.html/.htm`
- 支持一次选择多个文件
- 提交后调用业务上传接口
- 接口返回后直接展示成功/失败汇总及失败清单

### 11.3 主列表

主列表按员工一行展示，字段建议：

- 员工姓名
- 柜员号
- 身份证号
- 所属部门
- 最近征信查询日期
- 负债笔数
- 负债总额
- 民事案件笔数
- 强制执行笔数
- 行政处罚笔数
- 操作

聚合规则：

- 基础信息来自 `ccdi_base_staff`
- 最近征信查询日期、负债笔数、负债总额来自 `ccdi_debts_info`
- 三类负面计数来自 `ccdi_credit_negative_info`
- 任一业务表存在数据，即视为“已维护征信”

### 11.4 详情弹窗

详情弹窗分为两部分：

- 摘要区域
  - 员工姓名
  - 身份证号
  - 征信查询日期
  - 负债总额
  - 负债笔数
  - 民事案件笔数
  - 强制执行笔数
  - 行政处罚笔数

- 明细区域
  - 负债信息表格
  - 负面信息汇总卡片

负债表格字段：

- 负债大类
- 负债小类
- 债权人类型
- 负债名称
- 负债本金余额
- 负债总额
- 负债状态

负面信息展示字段：

- 民事案件笔数 / 金额
- 强制执行笔数 / 金额
- 行政处罚笔数 / 金额

### 11.5 删除交互

列表点击删除时，提示：

`确认删除该员工当前已维护的征信信息吗？`

确认后删除该员工在两张业务表中的全部数据。

## 12. 接口设计

新增业务接口，前端不直接调用 `/lsfx/credit/parse`：

### 12.1 批量上传

- 方法：`POST`
- 路径：`/ccdi/creditInfo/upload`
- 入参：多个 HTML 文件
- 返回：
  - 总文件数
  - 成功数
  - 失败数
  - 失败明细列表

失败明细建议字段：

- `fileName`
- `personId`
- `personName`
- `reason`

### 12.2 列表分页查询

- 方法：`GET`
- 路径：`/ccdi/creditInfo/list`
- 功能：分页查询员工征信摘要列表

### 12.3 详情查询

- 方法：`GET`
- 路径：`/ccdi/creditInfo/{personId}`
- 功能：查询员工征信摘要、负债明细和负面信息

### 12.4 删除

- 方法：`DELETE`
- 路径：`/ccdi/creditInfo/{personId}`
- 功能：删除员工当前征信数据

## 13. 后端设计

建议改动集中在 `ccdi-info-collection` 模块，新增独立征信维护资源。

建议新增：

- `controller/CcdiCreditInfoController.java`
- `service/ICcdiCreditInfoService.java`
- `service/impl/CcdiCreditInfoServiceImpl.java`
- `domain/CcdiCreditNegativeInfo.java`
- `domain/dto/CreditInfoUploadResultDTO.java`
- `domain/vo/CreditInfoListVO.java`
- `domain/vo/CreditInfoDetailVO.java`
- `mapper/CcdiCreditNegativeInfoMapper.java`
- `mapper/CcdiCreditInfoQueryMapper.java`
- `resources/mapper/...`

职责说明：

- `CcdiCreditInfoController`
  - 暴露上传、列表、详情、删除接口

- `CcdiCreditInfoService`
  - 协调解析、归户、最新记录校验、覆盖写入

- `CcdiCreditNegativeInfoMapper`
  - 维护负面信息表增删查

- `CcdiCreditInfoQueryMapper`
  - 承担员工维度列表聚合查询和详情聚合查询

## 14. 异常处理

上传过程中逐文件处理，失败不影响其他员工成功：

- 文件后缀非法：失败原因为“文件格式不支持”
- 解析接口调用失败：失败原因为“征信解析失败”
- 解析成功但身份证号为空：失败原因为“未解析到员工身份证号”
- 身份证号未匹配到员工：失败原因为“未匹配到员工信息”
- 上传征信日期早于当前最新记录：失败原因为“上传征信日期早于当前已维护最新记录”
- 员工覆盖写入事务失败：失败原因为“征信数据保存失败”

删除接口按幂等处理：

- 即使员工当前无征信数据，也返回删除成功

## 15. 测试方案

### 15.1 后端测试

- 上传服务测试
  - 单文件解析成功后正确写入两张业务表
  - 同员工新日期文件覆盖旧数据
  - 同员工旧日期文件被拒绝覆盖
  - 一个员工失败不影响其他员工成功
  - 删除后两张表数据均被清空

- 聚合查询测试
  - 只有负债信息
  - 只有负面信息
  - 两者都有
  - 两者都没有
  - 分页下聚合字段正确

- 控制器测试
  - 批量上传成功
  - 混合成功/失败上传
  - 详情查询成功/无数据
  - 删除接口幂等

### 15.2 前端验证

- 上传多个 HTML 文件并查看结果提示
- 列表按员工维度展示聚合摘要
- 详情弹窗正确显示负债表和负面信息
- 删除后该员工征信摘要即时消失
- 前端生产构建通过

## 16. 文档与脚本产出要求

本次实施阶段需同步补充：

- 后端实施计划：`docs/plans/backend/`
- 前端实施计划：`docs/plans/frontend/`
- SQL 脚本：`sql/` 或 `sql/migration/`
- 实施记录：`docs/reports/implementation/`

文档落点需遵循仓库目录规范，避免写入错误目录。
