diff --git a/docs/plans/2026-03-11-project-detail-pull-bank-info-design.md b/docs/plans/2026-03-11-project-detail-pull-bank-info-design.md new file mode 100644 index 0000000..553dee5 --- /dev/null +++ b/docs/plans/2026-03-11-project-detail-pull-bank-info-design.md @@ -0,0 +1,390 @@ +# 项目详情拉取本行信息设计 + +## 概述 + +本次设计面向项目详情页“上传数据”菜单中的“拉取本行信息”能力。用户在页面点击按钮后,弹出录入弹窗,支持手动输入身份证号、上传身份证 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 idCards` + - `String startDate` + - `String endDate` + +- `CcdiIdCardParseVO` + - `List idCards` + - `Integer count` + +## 服务层设计 + +继续使用: + +- `ICcdiFileUploadService` +- `CcdiFileUploadServiceImpl` + +建议新增两个对外方法: + +1. `parseIdCardFile(MultipartFile file)` +- 读取首个 sheet 第一列 +- 忽略表头、空值、重复值 +- 统一执行身份证格式校验 + +2. `submitPullBankInfo(Long projectId, List 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`