# 项目详情拉取本行信息设计

## 概述

本次设计面向项目详情页“上传数据”菜单中的“拉取本行信息”能力。用户在页面点击按钮后，弹出录入弹窗，支持手动输入身份证号、上传身份证 Excel 文件自动解析回填、选择时间跨度，然后提交后台异步拉取本行流水。

后端以项目现有“文件上传记录 + 线程池 + 落本地流水表”的链路为基础实现，不再新增第二套独立任务体系。每个身份证对应一条文件上传记录和一个线程任务，先调用流水分析平台“拉取行内流水”接口获取 `logId`，再复用现有“解析状态轮询 -> 获取文件上传状态 -> 获取流水列表并入库”的后半段处理链路。

## 已确认范围

- 页面入口保留在 `ruoyi-ui/src/views/ccdiProject/components/detail/UploadData.vue`
- 点击“拉取本行信息”后弹出表单弹窗，不再使用简单确认框
- 弹窗字段包括：
  - 证件号码文本域
  - 身份证文件上传
  - 时间跨度
- 身份证文件解析规则：
  - 读取首个 sheet
  - 只读取第一列
  - 忽略表头
  - 忽略空行
  - 忽略重复值
- 上传身份证文件后立即自动解析，并将结果回填到文本域
- 文本域与文件解析结果合并后按输入顺序去重
- 文件上传记录表中的 `uploadUser` 使用 `SecurityUtils.getUserName()`
- 调用流水分析平台 `fetchInnerFlow` 时的 `uploadUserId` 使用 `SecurityUtils.getUserId()`
- 创建上传记录时，先将身份证号写入 `accountNos` 作为主体账号
- 每个身份证使用一个线程处理
- 在调用“获取单个文件上传后的状态”接口时，根据返回值中的文件名更新文件上传记录
- 获取 `logId` 之后的处理步骤与现有“导入流水文件”方法保持一致

## 方案对比

### 方案一：在现有文件上传服务中扩展并抽取公共流水线

- 继续使用 `CcdiFileUploadController`、`ICcdiFileUploadService`、`CcdiFileUploadServiceImpl`
- 新增身份证文件解析接口和拉取本行信息提交接口
- 将现有“文件上传成功拿到 logId 后”的处理逻辑抽成公共方法，供文件上传和本行拉取共同复用

优点：

- 与现有上传记录列表、线程池、状态轮询、流水入库逻辑完全一致
- 复用度最高，后续维护成本最低
- 记录表、状态统计、页面轮询无需新增体系

缺点：

- 需要对现有 `CcdiFileUploadServiceImpl` 做一次中等规模重构

### 方案二：新增独立的本行拉取服务

- 新建独立 Controller 和 Service
- 仅在最后复用“获取流水列表并入库”的局部能力

优点：

- 对现有文件上传代码侵入较小

缺点：

- 会出现两套高度相似的任务调度和状态回写逻辑
- 后续容易出现功能漂移和修复不一致

### 方案三：在 Controller 中直接串接已有逻辑

- Controller 直接完成记录插入、线程调度和接口调用

优点：

- 早期开发速度快

缺点：

- Controller 职责过重
- 不利于测试和后续扩展

## 选型

采用方案一：在现有文件上传服务中扩展，并抽取“拿到 `logId` 后的公共处理流水线”。

该方案最符合当前项目已经存在的上传记录表、线程池和流水落库架构，可以保证文件上传和本行拉取在状态、错误处理和数据口径上保持一致。

## 前端交互设计

页面文件仍为 `ruoyi-ui/src/views/ccdiProject/components/detail/UploadData.vue`。

### 弹窗结构

新增“拉取本行信息”弹窗，包含以下控件：

1. 证件号码文本域
- 占位提示：支持逗号、中文逗号、换行分隔
- 用于展示最终待提交的身份证集合

2. 身份证文件上传
- 仅支持 `.xlsx`、`.xls`
- 选中文件后立即自动调用后端解析接口
- 解析成功后，把有效身份证集合合并回填到文本域

3. 时间跨度
- 开始日期
- 结束日期
- 提交时必填

### 前端交互规则

1. 用户选择身份证文件后：
- 立即上传到解析接口
- 前端显示“正在解析身份证文件”
- 解析成功后：
  - 将文件解析出的身份证集合与文本域当前内容合并
  - 去重后回填到文本域
  - 提示解析成功及有效条数
- 解析失败后：
  - 保留文本域现有内容
  - 显示明确错误提示

2. 用户点击“确认拉取”时：
- 前端先对文本域内容做本地拆分和去重
- 校验身份证集合非空、日期范围完整
- 调用正式提交接口
- 提交成功后关闭弹窗，刷新上传记录列表和统计，并开启现有轮询

3. 页面上传记录列表无需新增新页面：
- 本行拉取创建的记录与文件上传记录共用同一列表
- 状态、上传时间、上传人展示口径保持一致

## 后端接口设计

接口统一放在 `CcdiFileUploadController` 下。

### 1. 解析身份证文件

- 路径：`POST /ccdi/file-upload/parse-id-card-file`
- 请求类型：`multipart/form-data`
- 入参：
  - `file`
- 返回：
  - `idCards`
  - `count`

用途：

- 供弹窗选择文件后即时解析
- 只负责读取、去重、校验身份证，不创建任务

### 2. 提交拉取本行信息任务

- 路径：`POST /ccdi/file-upload/pull-bank-info`
- 请求类型：`application/json`
- 入参：
  - `projectId`
  - `idCards`
  - `startDate`
  - `endDate`
- 返回：
  - `batchId`

用途：

- 正式提交本行拉取任务
- 一次请求可提交多个身份证

## DTO / VO 设计

建议新增：

- `CcdiPullBankInfoSubmitDTO`
  - `Long projectId`
  - `List<String> idCards`
  - `String startDate`
  - `String endDate`

- `CcdiIdCardParseVO`
  - `List<String> idCards`
  - `Integer count`

## 服务层设计

继续使用：

- `ICcdiFileUploadService`
- `CcdiFileUploadServiceImpl`

建议新增两个对外方法：

1. `parseIdCardFile(MultipartFile file)`
- 读取首个 sheet 第一列
- 忽略表头、空值、重复值
- 统一执行身份证格式校验

2. `submitPullBankInfo(Long projectId, List<String> idCards, String startDate, String endDate, Long userId, String username)`
- 校验项目和日期
- 插入上传记录
- 在事务提交后调度线程池任务

## 核心数据流设计

### 一、身份证文件解析

1. Controller 接收 Excel 文件
2. Service 使用 EasyExcel 读取首个 sheet 第一列
3. 将单元格内容转成字符串并清理空白
4. 忽略首行表头
5. 使用 `LinkedHashSet` 去重并保序
6. 对每个值执行身份证格式校验
7. 返回有效身份证集合

### 二、正式提交任务

1. 校验 `projectId`
2. 查询项目，获取 `lsfxProjectId`
3. 校验 `startDate`、`endDate`
4. 校验身份证集合非空
5. 为每个身份证创建一条 `ccdi_file_upload_record`
   - `projectId = 当前项目`
   - `lsfxProjectId = 项目关联流水分析ID`
   - `fileStatus = uploading`
   - `fileName = 身份证号`
   - `accountNos = 身份证号`
   - `uploadUser = SecurityUtils.getUserName()`
   - `uploadTime = 当前时间`
6. 批量插入记录
7. 在事务提交后启动调度线程

### 三、线程处理单个身份证

每个身份证一个线程，使用现有 `fileUploadExecutor`。

单线程处理步骤：

1. 调用 `fetchInnerFlow`
   - `groupId = lsfxProjectId`
   - `customerNo = 身份证号`
   - `dataChannelCode = ZJRCU`
   - `requestDateId = 当天 yyyyMMdd`
   - `dataStartDateId = 开始日期 yyyyMMdd`
   - `dataEndDateId = 结束日期 yyyyMMdd`
   - `uploadUserId = SecurityUtils.getUserId()`

2. 从响应中获取唯一 `logId`

3. 进入公共处理流水线
   - 更新记录 `logId`
   - 更新状态为 `parsing`
   - 轮询解析状态
   - 调用“获取单个文件上传状态”接口
   - 读取文件名字段，优先使用 `uploadFileName`，取不到则回退 `downloadFileName`
   - 将文件名回写到 `ccdi_file_upload_record.file_name`
   - 提取主体名称、主体账号
   - 调用“获取流水列表”接口分页拉取并入库
   - 成功后更新状态为 `parsed_success`
   - 失败时更新状态为 `parsed_failed`

## 公共流水线重构设计

当前 `CcdiFileUploadServiceImpl` 中，`processFileAsync` 同时承担“上传文件并拿到 `logId`”和“拿到 `logId` 后继续处理”两段职责。

本次建议拆成两段：

1. 文件来源阶段
- 上传文件拿到 `logId`
- 或者拉取本行信息拿到 `logId`

2. 公共处理阶段
- 接收 `projectId`、`lsfxProjectId`、`record`、`logId`
- 负责后续统一处理

拆分后：

- 现有 `processFileAsync` 仍然保留，但只负责文件上传到平台并获得 `logId`
- 新增 `processPullBankInfoAsync` 负责调用 `fetchInnerFlow` 并获得 `logId`
- 两者统一调用新的公共处理方法，例如：
  - `processRecordAfterLogIdReady(...)`

## 文件上传记录表回写规则

### 记录初始化

- `fileName`：先写身份证号占位
- `accountNos`：写身份证号
- `enterpriseNames`：初始为空

### 状态接口返回后

- 若状态接口返回 `uploadFileName`，更新到 `fileName`
- 若 `uploadFileName` 为空但 `downloadFileName` 不为空，回写 `downloadFileName`
- `enterpriseNameList` 存在时更新 `enterpriseNames`
- `accountNoList` 存在时更新 `accountNos`

## 异常处理

### 提交阶段异常

以下场景直接拦截，不进入异步任务：

- 项目不存在
- 项目未绑定 `lsfxProjectId`
- 身份证集合为空
- 开始日期或结束日期为空
- 开始日期大于结束日期

### 文件解析异常

- 文件为空
- 文件格式不是 Excel
- 首个 sheet 第一列没有有效身份证
- 存在非法身份证号

解析异常直接返回错误，不覆盖前端已有输入值。

### 异步执行异常

每个身份证单独处理，单条失败不影响其他条目：

- `fetchInnerFlow` 失败：当前记录标记 `parsed_failed`
- 轮询超时：当前记录标记 `parsed_failed`
- 获取状态失败：当前记录标记 `parsed_failed`
- 获取流水失败：清理该 `logId` 已入库流水后标记 `parsed_failed`

错误信息统一落入 `error_message`，并延续现有超长错误截断规则。

## 测试设计

### 后端测试

重点新增以下测试：

1. 身份证文件解析测试
- 读取首个 sheet 第一列
- 忽略表头、空行、重复值
- 非法身份证时返回失败

2. 提交任务测试
- 为每个身份证插入一条上传记录
- 初始化 `accountNos` 为身份证号
- `uploadUser` 正确记录当前用户名

3. 公共流水线复用测试
- `fetchInnerFlow` 成功后能进入公共处理链路
- 状态接口返回文件名时能正确回写到记录表
- 流水入库失败时能清理已写入数据

4. Controller 测试
- 解析接口成功/失败
- 提交接口成功/失败

### 前端验证

- 选择身份证文件后自动解析并回填
- 手输身份证与文件解析结果正确合并去重
- 日期必填校验生效
- 提交成功后弹窗关闭并刷新列表
- 正在执行的记录可通过现有轮询刷新状态

## 验收标准

### 功能验收

- 上传数据页面可打开“拉取本行信息”弹窗
- 身份证 Excel 上传后能自动解析并回填输入框
- 提交后每个身份证都创建一条上传记录
- 每个身份证走一个线程处理
- 状态接口返回文件名后，记录列表能展示更新后的文件名
- 成功记录能拉取流水并入库
- 失败记录不会影响其他身份证继续执行

### 技术验收

- 复用现有 `fileUploadExecutor`
- 复用现有上传记录列表、统计和轮询机制
- Controller 使用 Swagger 注释
- Service 中公共处理逻辑不重复实现两套
- 后端测试覆盖解析、提交、公共流水线复用

## 风险与约束

1. 身份证文件模板不固定
- 首版只按“首个 sheet 第一列”解析
- 如后续存在多模板，再扩展模板识别

2. `fetchInnerFlow` 返回值只提供 `logId`
- 必须严格复用后续状态接口和流水接口，不能只看首个响应判断成功

3. 线程池容量与批量身份证数量存在上限
- 沿用现有线程池拒绝重试机制
- 超量场景下记录单条失败，不阻断整批任务

4. 文件名依赖状态接口返回
- 需要兼容 `uploadFileName` 为空的场景，并回退 `downloadFileName`
