Explorar el Código

会计报销 添加批量导入功能

徐滕 hace 2 semanas
padre
commit
959f35f52c

+ 453 - 0
jeeplus-modules/jeeplus-finance/src/main/java/com/jeeplus/finance/reimbursementApproval/approvalInfo/CODING_CONVENTIONS.md

@@ -0,0 +1,453 @@
+# 报销管理模块 — 代码规范与开发约定
+
+> 本文档基于现有 `jeeplus-finance` 模块中报销审批(reimbursementApproval)子模块的代码风格整理,
+> 新增代码(导入功能)须遵循以下规范。
+
+---
+
+## 1. 项目结构
+
+```
+jeeplus-finance/src/main/java/com/jeeplus/finance/reimbursementApproval/approvalInfo/
+├── controller/          # 控制器
+├── service/             # 业务逻辑
+│   └── dto/             # 数据传输对象
+├── domain/              # 数据库实体
+└── mapper/              # MyBatis Mapper
+    └── xml/             # Mapper XML
+
+jeeplus-finance/src/main/resources/
+└── mappings/            # Mapper XML(部分项目放此处)
+```
+
+**包命名**:`com.jeeplus.finance.reimbursementApproval.approvalInfo.{子包}`,子包名 camelCase。
+
+---
+
+## 2. Controller 规范
+
+### 2.1 类注解
+
+```java
+@RestController
+@Api(tags = "财务-报销申请")
+@RequestMapping(value = "/reimbursementApproval/info")
+public class CwReimbursementInfoController {
+```
+
+- `@RestController` — 必须
+- `@Api(tags = "中文模块名")` — Swagger 文档,用中文
+- `@RequestMapping(value = "...")` — 路径 camelCase
+
+### 2.2 依赖注入
+
+```java
+@Resource
+private CwReimbursementInfoService service;
+```
+
+- 使用 `@Resource`(JSR-250),**不用** `@Autowired`
+- 字段注入,不用构造器注入
+
+### 2.3 方法签名
+
+```java
+// 查询类 — GET,返回 ResponseEntity
+@ApiOperation(value = "列表查询")
+@GetMapping("/list")
+public ResponseEntity<IPage<RetureListDto>> list(Page<RetureListDto> page, QueryListDto dto)
+
+// 写入类 — POST,返回 ResponseEntity<String>
+@ApiOperation(value = "新增、修改")
+@PostMapping("/save")
+public ResponseEntity<String> save(@RequestBody SaveInfoDto dto) throws Exception
+
+// 导出类 — GET,返回 void,直接写 response
+@ApiOperation(value = "导出")
+@ApiLog(value = "财务报销数据", type = LogTypeEnum.EXPORT)
+@GetMapping("/exportFile")
+public void exportFile(QueryListDto dto, Page page, ExcelOptions options, HttpServletResponse response) throws Exception
+
+// 校验类 — 返回 Map<String, Object>
+@ApiOperation(value = "校验发票号")
+@GetMapping("/checkNumber")
+public Map<String, Object> checkNumber(@RequestParam String number)
+```
+
+**规则:**
+| 场景 | HTTP方法 | 返回类型 | 参数注解 |
+|---|---|---|---|
+| 分页查询 | GET | `ResponseEntity<IPage<T>>` | 无注解(自动绑定) |
+| 单条查询 | GET | `ResponseEntity<T>` | `@RequestParam` |
+| 新增/修改 | POST | `ResponseEntity<String>` | `@RequestBody` |
+| 删除 | GET | `ResponseEntity<String>` | `@RequestParam` |
+| 导出 | GET | `void` | 直接写 `HttpServletResponse` |
+| 导入 | POST | `ResponseEntity<T>` | `@RequestParam MultipartFile` |
+| 校验/简单操作 | GET/POST | `Map<String, Object>` | `@RequestParam` |
+
+### 2.4 返回值构造
+
+```java
+// 成功(带数据)
+return ResponseEntity.ok(iPage);
+
+// 成功(带自定义字段)
+return ResponseUtil.newInstance()
+    .add("businessTable", "cw_reimbursement_info")
+    .add("businessId", s.getId())
+    .add("no", s.getNo())
+    .ok("操作成功");
+
+// 校验结果(Map 方式)
+Map<String, Object> map = new HashMap<>();
+map.put("success", true);
+map.put("message", "操作成功");
+return map;
+```
+
+### 2.5 注释风格
+
+```java
+/**
+ * 报告号查询
+ * @param dto
+ * @return
+ */
+@ApiOperation(value = "报告号查询")
+@GetMapping("/reportNoList")
+public ResponseEntity<IPage<ReportNoDto>> reportNoList(Page<ReportNoDto> page, ReportNoDto dto) throws Exception {
+```
+
+- Javadoc 风格,**中文**描述
+- `@param` 和 `@return` 保留但内容可空
+- `@ApiOperation(value = "中文描述")` 必须加
+
+### 2.6 操作日志
+
+```java
+@ApiLog(value = "财务报销数据", type = LogTypeEnum.EXPORT)
+```
+
+- 导出、导入等关键操作加 `@ApiLog`
+- `LogTypeEnum` 选对应类型(EXPORT、IMPORT 等)
+
+---
+
+## 3. Service 规范
+
+### 3.1 类注解
+
+```java
+@Service
+public class CwReimbursementInfoService {
+```
+
+- 只用 `@Service`,不写接口
+- 不加 `@Transactional`(现有代码未使用)
+
+### 3.2 依赖注入
+
+```java
+@Resource
+private CwReimbursementInfoMapper infoMapper;
+
+@Resource
+private CwReimbursementDetailInfoMapper detailMapper;
+```
+
+- `@Resource` 字段注入
+- 多个 Mapper 直接注入
+
+### 3.3 方法签名
+
+```java
+public IPage<RetureListDto> list(Page<RetureListDto> page, QueryListDto dto, Boolean isExport) throws Exception
+```
+
+- 返回 Domain/DTO 对象,不包装
+- 统一 `throws Exception`
+- 分页参数 `Page` 放第一个
+
+### 3.4 查询构建
+
+```java
+// 基础条件由 QueryWrapperGenerator 生成
+QueryWrapper<ReportNoDto> queryWrapper = QueryWrapperGenerator.buildQueryCondition(dto, ReportNoDto.class);
+
+// 手动追加条件
+queryWrapper.eq("a.del_flag", 0);
+queryWrapper.eq("c.id", dto.getProjectId());
+
+// 复杂条件用 apply
+queryWrapper.apply("(b.project_id Like {0} OR b.project_name LIKE {1})",
+    "%" + dto.getProject() + "%", "%" + dto.getProject() + "%");
+```
+
+### 3.5 用户信息获取
+
+```java
+UserDTO userDTO = SpringUtil.getBean(IUserApi.class).getByToken(TokenProvider.getCurrentToken());
+```
+
+- 通过 `SpringUtil.getBean()` 获取 Feign 客户端
+- 通过 `TokenProvider.getCurrentToken()` 获取当前 token
+
+### 3.6 注释风格
+
+```java
+/**
+ * 报告号查询
+ */
+public IPage<ReportNoDto> getReportNoList(...) {
+
+/**
+ * a表是reimbursementInfo
+ * b表是reimbursementDetailInfo
+ * c表是sysUser
+ */
+```
+
+- 简短中文 Javadoc
+- 多表关联时注释表别名含义
+
+---
+
+## 4. DTO 规范
+
+### 4.1 类定义
+
+```java
+@Data
+public class SaveInfoDto extends BaseEntity {
+```
+
+- `@Data`(Lombok)— 必须
+- 需要继承 `BaseEntity` 时继承(有通用字段),否则不继承
+- 不加 `@ApiModel`
+
+### 4.2 字段注解
+
+```java
+/**
+ * 经办人id
+ */
+private String userId;
+
+@DateTimeFormat(pattern = "yyyy-MM-dd")
+@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
+private Date reimDate;
+
+@Excel(name = "报销编号", width = 30, orderNum = "1")
+private String no;
+
+@Excel(name = "报销金额", width = 30, orderNum = "11", type = 10)
+private String numberCount;
+```
+
+**规则:**
+- 字段注释用 Javadoc 风格,中文
+- 日期字段:`@DateTimeFormat` + `@JsonFormat` 双注解,timezone 固定 `GMT+8`
+- EasyPOI 导出字段:`@Excel(name = "中文名", width = 数字, orderNum = "序号")`
+- 数值类型 `type = 10`
+- 金额用 `String` 类型,不用 `BigDecimal`
+
+### 4.3 命名风格
+
+- camelCase:`userId`、`userName`、`reimDate`、`procInsId`
+- 允许缩写:`reim` = reimbursement,`procIns` = processInstance,`no` = number
+- 字段名与数据库列名一致(下划线转驼峰自动映射)
+
+---
+
+## 5. Domain 实体规范
+
+### 5.1 类定义
+
+```java
+@Data
+@TableName(value = "cw_reimbursement_info")
+public class CwReimbursementInfo extends BaseEntity {
+```
+
+- `@Data` + `@TableName(value = "表名")`
+- 继承 `BaseEntity`(提供 id、createBy、createTime、delFlag 等)
+- 不加 `@ApiModel`
+
+### 5.2 字段注解
+
+```java
+/**
+ * 经办人id
+ */
+private String userId;
+
+@DateTimeFormat(pattern = "yyyy-MM-dd")
+@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
+private Date reimDate;
+```
+
+- 不加 `@TableId`、`@TableField`(除非 `exist = false` 的关联字段)
+- 关联查询字段加 `@TableField(exist = false)`
+- 注释 Javadoc 风格,中文
+
+### 5.3 类型选择
+
+| 用途 | 类型 | 示例 |
+|---|---|---|
+| ID、编号 | `String` | `userId`、`no` |
+| 金额 | `String` | `number`、`numberCount` |
+| 日期 | `java.util.Date` | `reimDate` |
+| 计数 | `Integer` | `receiptNumber`、`days` |
+| 状态码 | `String` | `type`、`sourceType` |
+| 文本 | `String` | `content`、`remarks` |
+
+---
+
+## 6. Mapper 规范
+
+### 6.1 接口定义
+
+```java
+public interface CwReimbursementInfoMapper extends BaseMapper<CwReimbursementInfo> {
+
+    IPage<RetureListDto> findList(Page<RetureListDto> page, @Param(Constants.WRAPPER) QueryWrapper<RetureListDto> wrapper);
+
+    List<TreeUserDto> findOfficeList();
+
+    void updateStatusById(@Param("id") String id, @Param("type") String type);
+}
+```
+
+- 继承 `BaseMapper<T>`
+- 所有参数加 `@Param` 注解
+- QueryWrapper 参数用 `@Param(Constants.WRAPPER)`
+- 跨租户查询加 `@InterceptorIgnore(tenantLine = "true")`
+
+### 6.2 方法命名
+
+| 前缀 | 用途 | 示例 |
+|---|---|---|
+| `find*` | 复杂查询 | `findList`、`findProjectList` |
+| `get*` | 简单获取 | `getReportNoList`、`getById` |
+| `update*` | 更新 | `updateStatusById` |
+| `delete*` | 删除 | `deleteByIdAndNumber` |
+
+### 6.3 XML 规范
+
+```xml
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.jeeplus.finance.reimbursementApproval.approvalInfo.mapper.CwReimbursementInfoMapper">
+
+    <select id="findList" resultType="com.jeeplus.finance.reimbursementApproval.approvalInfo.service.dto.RetureListDto">
+        SELECT DISTINCT a.id,
+                        a.`no`,
+                        a.source_type
+        FROM cw_reimbursement_info a
+                 LEFT JOIN cw_reimbursement_detail_info b ON a.id = b.info_id AND b.del_flag = 0
+        ${ew.customSqlSegment}
+        ORDER BY a.update_time DESC
+    </select>
+
+</mapper>
+```
+
+**规则:**
+- namespace 用接口全限定名
+- SQL 关键字大写:`SELECT`、`FROM`、`WHERE`、`LEFT JOIN`
+- 表名小写下划线:`cw_reimbursement_info`
+- 表别名用小写字母:`a`、`b`、`c`
+- JOIN 条件包含 `del_flag = 0`
+- 动态条件用 `${ew.customSqlSegment}`(由 Java 端 QueryWrapper 生成)
+- 排序默认 `ORDER BY a.update_time DESC`
+
+---
+
+## 7. 通用规范
+
+### 7.1 Import 顺序
+
+```java
+// 1. 第三方库
+import cn.hutool.extra.spring.SpringUtil;
+import com.alibaba.fastjson.JSON;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+
+// 2. 框架公共
+import com.jeeplus.common.utils.ResponseUtil;
+
+// 3. 当前模块
+import com.jeeplus.finance.reimbursementApproval.approvalInfo.domain.*;
+import com.jeeplus.finance.reimbursementApproval.approvalInfo.service.dto.*;
+
+// 4. Feign 客户端
+import com.jeeplus.flowable.feign.IFlowableApi;
+
+// 5. Swagger
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+
+// 6. Apache Commons
+import org.apache.commons.lang3.StringUtils;
+
+// 7. Spring
+import org.springframework.web.bind.annotation.*;
+
+// 8. Java 标准库
+import javax.annotation.Resource;
+import java.util.*;
+```
+
+### 7.2 注释语言
+
+- **全部使用中文**
+- 类注释包含 `@author` 和 `@create` 日期
+- 方法注释简短描述功能
+- 字段注释 Javadoc 风格
+
+### 7.3 异常处理
+
+- 方法签名声明 `throws Exception`
+- 不在方法内部 try-catch(由全局异常处理器统一处理)
+- 校验失败用 `Map<String, Object>` 返回 `success: false` + `message`
+
+### 7.4 权限控制
+
+- 后端 Controller **不加** `@RequiresPermissions`(现有代码风格)
+- 权限由前端 `hasPermission()` 控制
+- 如需要加,格式:`@RequiresPermissions("cw:reimbursement:info:import")`
+
+### 7.5 字符串工具
+
+```java
+// 优先用项目自带工具类
+com.jeeplus.utils.StringUtils.isNotEmpty(value)
+com.jeeplus.utils.StringUtils.isEmpty(value)
+
+// 也可用 Apache Commons
+org.apache.commons.lang3.StringUtils.isNotBlank(value)
+```
+
+### 7.6 集合工具
+
+```java
+import org.apache.commons.collections4.CollectionUtils;
+
+if (CollectionUtils.isNotEmpty(list)) { ... }
+if (CollectionUtils.isEmpty(list)) { ... }
+```
+
+---
+
+## 8. 导入功能新增代码清单
+
+按以上规范,导入功能需要新增/修改的文件:
+
+| 文件 | 操作 | 说明 |
+|---|---|---|
+| `service/dto/ReimbursementImportDto.java` | 新增 | Excel 行数据映射,`@Data` + `@Excel` 注解 |
+| `service/dto/ReimbursementImportResult.java` | 新增 | 导入结果,`@Data` |
+| `service/CwReimbursementInfoService.java` | 修改 | 新增 `importReimbursement()` 方法 |
+| `controller/CwReimbursementInfoController.java` | 修改 | 新增 `importFile()`、`downloadImportError()`、`importTemplate()` 接口 |
+| `mapper/xml/CwReimbursementInfoMapper.xml` | 修改 | 新增项目匹配、报告号匹配、类别匹配的 SQL |
+| `resources/templates/报销导入模板.xlsx` | 新增 | 导入模板文件 |

+ 468 - 0
jeeplus-modules/jeeplus-finance/src/main/java/com/jeeplus/finance/reimbursementApproval/approvalInfo/REQUIREMENTS.md

@@ -0,0 +1,468 @@
+# 报销管理 — Excel 批量导入功能 需求文档
+
+> 版本:v1.0
+> 日期:2026-10-28
+> 模块:财务管理(会计)→ 报销管理
+> 关联页面:`/cw/reimbursementApproval/info/InfoList`(列表页)、`/cw/reimbursementApproval/info/NewReimbursementForm`(表单页)
+
+---
+
+## 一、需求概述
+
+在报销管理列表页(InfoList)新增「导入」功能,允许用户通过上传 Excel 文件批量创建报销单。导入生成的报销单为**暂存状态**,用户需在表单中逐条核对、上传数电发票 XML 后手动提交审批。
+
+---
+
+## 二、功能范围
+
+### 2.1 支持的报销类型
+
+| sourceType 值 | 类型 | 明细表 |
+|---|---|---|
+| 1 | 项目报销 | `cw_reimbursement_detail_info` |
+| 3 | 报告报销 | `cw_reimbursement_detail_info_report` |
+| 4 | 其他报销 | `cw_reimbursement_detail_info_other` |
+
+仅支持以上三种,其他类型(合同报销、采购报销、人事报销等)不在本次范围内。
+
+### 2.2 权限控制
+
+- 前端按钮权限标识:`cw:reimbursement:info:import`
+- 后端 Controller 不加 `@RequiresPermissions`(与现有代码风格一致),权限由前端控制
+- 权限在系统权限管理页面配置,可分配给不同角色
+
+---
+
+## 三、Excel 模板规范
+
+### 3.1 模板结构
+
+- 单 Sheet,Sheet 名:`导入数据`
+- 第 1 行:列标题(蓝色底白字)
+- 第 2 行:列说明(浅蓝底灰字,必填列浅黄底)
+- 第 3-7 行:示例数据(淡绿底,A 列带红色 `[示例]` 标记)
+- 第 8 行起:分隔线 + 填写提示
+- 冻结前两行
+
+### 3.2 列定义(共 13 列)
+
+| 序号 | 列名 | 字段 | 必填 | 类型 | 说明 |
+|---|---|---|---|---|---|
+| A | 是否单独报销 | aloneFlag | 是 | 下拉:是/同上 | 「是」= 本行独立成单;「同上」= 归入上一行所属报销单 |
+| B | 报销类型 | sourceType | 是 | 下拉:项目报销/报告报销/其他报销 | 对应 sourceType 值 1/3/4 |
+| C | 报销人 | userName | 否 | 文本 | 不填默认导入操作人;代他人报销时填写对方姓名 |
+| D | 报销类别 | typeName | 是 | 下拉(引用类别列表 Sheet) | 只能选最末级节点,如"租房及其他杂费" |
+| E | 项目编号 | projectNumber | 条件 | 文本 | 项目报销和报告报销必填,精确匹配 `cw_project_records.project_number` |
+| F | 报销详情 | projectName | 条件 | 文本 | 仅其他报销填写,自由文本 |
+| G | 报告号 | reportNo | 条件 | 文本 | 仅报告报销填写,格式如:苏兴会基字[2026]1号 |
+| H | 非数电发票(费用) | ordinaryNumber | 否 | 数字 | 金额(元) |
+| I | 其他(费用) | subsidy | 否 | 数字 | 金额(元) |
+| J | 汇总 | numberCount | 是 | 数字 | 费用合计,可手填或系统自动计算 |
+| K | 非数电发票(张数) | ordinaryReceiptNumber | 否 | 整数 | 票据张数 |
+| L | 内容 | content | 否 | 文本 | 报销内容文字说明 |
+| M | 出差天数 | days | 否 | 数字 | |
+
+**注:** 数电发票费用(number)不在模板中,由后台根据上传的 XML 文件自动计算。
+
+### 3.3 数据验证(下拉)
+
+| 列 | 验证方式 | 选项 |
+|---|---|---|
+| A 是否单独报销 | 内嵌列表 | 是, 同上 |
+| B 报销类型 | 内嵌列表 | 项目报销, 报告报销, 其他报销 |
+| D 报销类别 | 引用 Sheet2 | `类别列表!$A$2:$A$N`(N = 末级节点总数) |
+
+所有下拉均设置 `allow_blank = true`,空行不触发校验错误。
+
+### 3.4 类别列表 Sheet
+
+- Sheet 名:`类别列表`
+- A 列:报销类别最末级节点名称
+- 数据来源:`cw_reimbursement_type_info` 表中无子节点的记录(`level` 为最大值的记录)
+- 模板下载时由后端动态生成,确保与系统数据一致
+
+---
+
+## 四、业务逻辑
+
+### 4.1 分组规则
+
+按「是否单独报销」列将 Excel 行分组:
+
+```
+行1: 是   → 组1 开始
+行2: 同上  → 归入组1
+行3: 同上  → 归入组1
+行4: 是   → 组2 开始
+行5: 是   → 组3 开始
+```
+
+- 每组生成**一张**报销单(`cw_reimbursement_info`)
+- 组内每行生成**一条**明细记录
+- 全空行(所有列均为空)自动跳过,不参与分组
+
+### 4.2 同组一致性校验
+
+同一组内的以下字段必须与组内第一行完全一致,否则整组标记为校验失败:
+
+| 校验项 | 说明 |
+|---|---|
+| 报销类型 | 组内所有行的报销类型必须相同 |
+| 项目编号 | 项目报销和报告报销的组内,项目编号必须相同 |
+
+### 4.3 数据匹配逻辑
+
+#### 4.3.1 报销类别匹配
+
+- 用 Excel 中的类别名称去 `cw_reimbursement_type_info` 表匹配
+- 匹配条件:`name = #{typeName}` AND `del_flag = 0` AND 该节点无子节点(末级节点)
+- 匹配成功 → 获取 `id`(作为 typeId)和 `name`(作为 typeName)
+- 匹配失败 → 该行标记错误:「报销类别「XXX」不存在或不是末级节点」
+
+#### 4.3.2 项目编号匹配
+
+- 用 Excel 中的项目编号去 `cw_project_records` 表精确匹配
+- 匹配条件:`project_number = #{projectNumber}` AND `del_flag = 0`
+- 匹配成功 → 获取 `id`(作为 projectId)和 `project_name`(作为 projectName)
+- 匹配失败 → 该行标记错误:「项目编号「XXX」不存在」
+
+#### 4.3.3 报告号匹配(仅报告报销)
+
+- 用项目编号匹配到的 `cw_project_records.id`,关联查询报告号列表
+- 关联链路:`cw_project_records.id → cw_project_report.project_id → cw_project_report.id → cw_project_report_new_line.report_id → cw_project_report_new_line.report_no`
+- SQL:
+```sql
+SELECT report_no
+FROM cw_project_report_new_line a
+LEFT JOIN cw_project_report b ON a.report_id = b.id
+WHERE a.del_flag = 0 AND b.project_id = #{projectId}
+```
+- Excel 中的报告号必须在查询结果列表中
+- 匹配失败 → 该行标记错误:「报告号「XXX」不属于项目「YYY」」
+
+#### 4.3.4 报销人匹配
+
+- 用 Excel 中的报销人姓名去 `sys_user` 表匹配
+- 匹配条件:`name = #{userName}` AND `del_flag = 0`(受租户拦截器控制,只查当前租户)
+- 匹配结果处理:
+  - **0 条** → 标记错误:「报销人「XXX」不存在」
+  - **1 条** → 获取 `id`(作为 userId)、`name`(作为 userName)、`office_id`(作为 deptId)、部门名称(作为 deptName)
+  - **2 条及以上**(同名) → 标记错误:「报销人「XXX」存在重名,请手动录入后调整」
+- 不填 → 使用当前登录用户的信息
+
+### 4.4 金额计算
+
+- 汇总(numberCount)= 数电发票(number)+ 非数电发票(ordinaryNumber)+ 其他(subsidy)
+- 导入时数电发票费用(number)固定为 0(XML 未上传)
+- 如果 Excel 中汇总列为空,系统自动计算:`0 + ordinaryNumber + subsidy`
+- 如果 Excel 中汇总列有值,以填写值为准,但会校验是否等于三项之和,不等则标记警告(不阻断)
+
+### 4.5 票据张数计算
+
+- 汇总张数(receiptNumberCount)= 数电发票张数(receiptNumber)+ 非数电发票张数(ordinaryReceiptNumber)
+- 导入时数电发票张数(receiptNumber)固定为 0
+- receiptNumberCount = 0 + ordinaryReceiptNumber
+
+---
+
+## 五、数据写入
+
+### 5.1 主表(cw_reimbursement_info)
+
+每组生成一条主记录:
+
+| 字段 | 值来源 |
+|---|---|
+| id | UUID 生成 |
+| no | 调用 `IWorkAttachmentApi.genSerialNum()` 自动生成,业务编码 `101` |
+| userId / userName | 当前登录用户(经办人),通过 `TokenProvider` + `IUserApi` 获取 |
+| department | 当前用户所属部门 |
+| reimDate | 当天日期(`new Date()`) |
+| sourceType | Excel 中的报销类型(1/3/4) |
+| type | 固定 `'1'`(暂存) |
+| businessFlag | 固定 `'1'`(会计) |
+| reimbursementType | 固定 `'0'`(普通报销) |
+| remarks | 空(模板中无此列) |
+| createBy / createTime | 系统自动填充 |
+
+### 5.2 明细表
+
+根据 sourceType 写入不同的明细表:
+
+**sourceType = 1 → `cw_reimbursement_detail_info`**
+
+| 字段 | 值来源 |
+|---|---|
+| infoId | 主表 id |
+| userId / userName | 报销人(Excel 或默认当前用户) |
+| deptId / deptName | 报销人所属部门 |
+| typeId / typeName | 报销类别匹配结果 |
+| projectId / projectName | 项目编号匹配结果 |
+| reportNumber | 空 |
+| number | 固定 `'0'` |
+| ordinaryNumber | Excel 值,转 String |
+| subsidy | Excel 值,转 String |
+| numberCount | Excel 值或自动计算,转 String |
+| receiptNumber | 固定 `0` |
+| ordinaryReceiptNumber | Excel 值 |
+| receiptNumberCount | 自动计算 |
+| content | Excel 值 |
+| days | Excel 值 |
+| uniqueId | `UUID.randomUUID().toString()` |
+| reimbursementType | 固定 `'0'` |
+
+**sourceType = 3 → `cw_reimbursement_detail_info_report`**
+
+与 sourceType=1 相同,额外字段:
+- `reportId`:报告号匹配到的 `cw_project_report_new_line.id`
+
+**sourceType = 4 → `cw_reimbursement_detail_info_other`**
+
+与 sourceType=1 相同,区别:
+- `projectName` 字段存储的是「报销详情」自由文本(不是项目名)
+- 无 `projectId`、`reportNumber`、`reportId`
+
+### 5.3 不触发的流程
+
+- **不创建** Flowable 流程实例
+- **不发送** 审批通知
+- **不调用** 现有 `save()` 方法(直接操作 Mapper 插入)
+
+---
+
+## 六、校验与错误处理
+
+### 6.1 校验策略
+
+- **部分导入**:校验通过的组正常写入,校验失败的组跳过
+- **全部校验完成后**统一返回结果,不中途停止
+
+### 6.2 错误 Excel 生成
+
+校验失败时,后端生成一份新的 Excel 文件:
+
+- 包含原始数据的所有行
+- 错误行用**红色字体 + 红色底色**标注
+- 最后一列追加「错误原因」,说明具体错误(如:「第3行:项目编号「XXX」不存在」)
+- 文件临时存储在服务器,返回文件路径供前端下载
+- 用户修改后重新上传导入
+
+### 6.3 错误类型汇总
+
+| 错误类型 | 错误信息示例 |
+|---|---|
+| 报销类型非法 | 「第X行:报销类型必须为项目报销、报告报销或其他报销」 |
+| 报销类别不存在 | 「第X行:报销类别「XXX」不存在或不是末级节点」 |
+| 项目编号不存在 | 「第X行:项目编号「XXX」不存在」 |
+| 报告号不匹配 | 「第X行:报告号「XXX」不属于项目「YYY」」 |
+| 报销人不存在的 | 「第X行:报销人「XXX」不存在」 |
+| 报销人重名 | 「第X行:报销人「XXX」存在重名,请手动录入后调整」 |
+| 同组不一致 | 「第X行:同组内报销类型/项目编号与组内第一行不一致」 |
+| 报告报销缺项目编号 | 「第X行:报告报销必须填写项目编号」 |
+| 项目报销缺项目编号 | 「第X行:项目报销必须填写项目编号」 |
+| 其他报销缺报销详情 | 「第X行:其他报销必须填写报销详情」 |
+| 汇总金额不等 | 「第X行:汇总金额与各项之和不符(已按填写值写入)」 |
+
+---
+
+## 七、导入结果反馈
+
+### 7.1 前端弹窗
+
+导入完成后弹窗显示:
+
+```
+导入完成
+成功:X 条报销单(共 Y 条明细)
+失败:Z 条(共 W 行数据)
+
+[下载错误文件]  [关闭]
+```
+
+- 全部成功 → 只显示成功信息,无「下载错误文件」按钮
+- 全部失败 → 只显示失败信息 + 「下载错误文件」按钮
+- 部分成功 → 同时显示成功和失败信息 + 「下载错误文件」按钮
+
+### 7.2 成功后操作
+
+- 关闭导入弹窗
+- 刷新列表页数据(调用现有 `refreshList()` 方法)
+- **不自动跳转**到表单编辑页,用户自行在列表中查找
+
+### 7.3 失败后操作
+
+- 点击「下载错误文件」→ 下载标注错误的 Excel
+- 用户修改后重新上传
+- 导入弹窗保持打开,可重复操作
+
+---
+
+## 八、接口设计
+
+### 8.1 下载导入模板
+
+```
+GET /finance-server/reimbursementApproval/info/importTemplate
+```
+
+- 返回:Excel 文件流(`application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`)
+- 功能:动态生成模板,类别下拉列表从数据库实时查询
+
+### 8.2 导入 Excel
+
+```
+POST /finance-server/reimbursementApproval/info/importFile
+Content-Type: multipart/form-data
+参数:file(MultipartFile)
+```
+
+- 返回:`ReimbursementImportResult`
+```json
+{
+  "successCount": 3,
+  "failCount": 2,
+  "successDetailCount": 5,
+  "failDetailCount": 3,
+  "errorFilePath": "/tmp/import_error_xxx.xlsx",
+  "errorMessages": [
+    {"row": 3, "message": "项目编号「XXX」不存在"},
+    {"row": 5, "message": "报销人「XXX」存在重名"}
+  ]
+}
+```
+
+### 8.3 下载错误文件
+
+```
+GET /finance-server/reimbursementApproval/info/downloadImportError?filePath=/tmp/import_error_xxx.xlsx
+```
+
+- 返回:Excel 文件流
+- 下载后删除服务器临时文件
+
+---
+
+## 九、前端改动
+
+### 9.1 InfoList.vue
+
+- 工具栏新增「导入」按钮
+  - 位置:在「新建」按钮右侧
+  - 权限:`v-if="hasPermission('cw:reimbursement:info:import')"`
+  - 图标:`el-icon-upload2`
+- 引入 `ImportDialog` 组件
+- 点击按钮 → 打开导入弹窗
+
+### 9.2 ImportDialog.vue(新增组件)
+
+路径:`src/views/cw/reimbursementApproval/info/ImportDialog.vue`
+
+功能:
+1. **下载模板**按钮 → 调用 `importTemplate` 接口,浏览器下载
+2. **文件上传**区域
+   - 组件:`el-upload`
+   - 限制:`.xlsx`、`.xls` 格式
+   - 大小限制:10MB
+   - 手动上传(非自动)
+3. **开始导入**按钮
+   - 校验是否已选择文件
+   - 调用 `importFile` 接口
+   - 显示 loading 状态
+4. **结果展示**
+   - 成功/失败数量
+   - 错误明细列表(可展开)
+   - 「下载错误文件」按钮(有失败时显示)
+5. **关闭**按钮 → 关闭弹窗,触发父组件刷新列表
+
+### 9.3 ReimbursementApprovalService.js
+
+新增 3 个方法:
+
+```javascript
+importTemplate() {
+    return request({
+        url: prefix + "/reimbursementApproval/info/importTemplate",
+        method: "get",
+        responseType: "blob",
+    });
+}
+
+importFile(data) {
+    return request({
+        url: prefix + "/reimbursementApproval/info/importFile",
+        method: "post",
+        data: data,
+    });
+}
+
+downloadImportError(filePath) {
+    return request({
+        url: prefix + "/reimbursementApproval/info/downloadImportError",
+        method: "get",
+        params: { filePath: filePath },
+        responseType: "blob",
+    });
+}
+```
+
+---
+
+## 十、后端改动清单
+
+### 10.1 新增文件
+
+| 文件 | 路径 | 说明 |
+|---|---|---|
+| `ReimbursementImportDto.java` | `service/dto/` | Excel 行数据映射,`@Data` + `@Excel` 注解 |
+| `ReimbursementImportResult.java` | `service/dto/` | 导入结果 DTO |
+| `ImportDialog.vue` | `views/cw/reimbursementApproval/info/` | 导入弹窗组件 |
+| `报销导入模板.xlsx` | `resources/templates/` | 静态模板文件(类别列表动态填充) |
+
+### 10.2 修改文件
+
+| 文件 | 改动内容 |
+|---|---|
+| `CwReimbursementInfoController.java` | 新增 3 个接口方法:`importTemplate()`、`importFile()`、`downloadImportError()` |
+| `CwReimbursementInfoService.java` | 新增导入核心方法:`importReimbursement()`、`generateErrorExcel()` |
+| `CwReimbursementInfoMapper.java` | 新增查询方法:`findCategoryByName()`、`findProjectByNumber()`、`findReportNosByProjectId()`、`findUserByName()` |
+| `CwReimbursementInfoMapper.xml` | 新增对应 SQL |
+| `InfoList.vue` | 新增导入按钮 + 引入 ImportDialog 组件 |
+| `ReimbursementApprovalService.js` | 新增 3 个 API 方法 |
+
+### 10.3 不动的文件
+
+| 文件 | 原因 |
+|---|---|
+| `NewReimbursementForm.vue` | 导入生成暂存单据后用户手动打开编辑,走现有流程 |
+| `ReimbursementComponentTest.vue` | 同上 |
+| `CwReimbursementInfoService.save()` | 导入直接操作 Mapper 插入,不走 save 方法 |
+| 工作流相关代码 | 导入不触发流程 |
+| 现有明细表结构 | 直接往现有表插数据,不改表结构 |
+
+---
+
+## 十一、开发顺序
+
+1. **后端 DTO** → `ReimbursementImportDto.java`、`ReimbursementImportResult.java`
+2. **后端 Mapper** → 新增查询方法和 SQL(类别、项目、报告号、用户匹配)
+3. **后端 Service** → 导入核心逻辑(解析→分组→校验→写入→错误Excel)
+4. **后端 Controller** → 3 个接口
+5. **前端 API** → Service 新增 3 个方法
+6. **前端组件** → `ImportDialog.vue`
+7. **前端列表** → `InfoList.vue` 加按钮
+8. **模板文件** → 放到 resources 或动态生成
+
+---
+
+## 十二、验收标准
+
+1. 列表页显示「导入」按钮,有权限控制
+2. 点击下载模板 → 浏览器下载 Excel 文件,类别下拉列表数据正确
+3. 上传 Excel → 正确解析并分组
+4. 校验通过的数据 → 成功写入数据库,生成暂存状态的报销单
+5. 校验失败的数据 → 返回错误信息,可下载标注错误的 Excel
+6. 部分成功部分失败 → 成功的写入,失败的返回错误
+7. 空行 → 自动跳过,不报错
+8. 导入成功后 → 列表页刷新,显示新导入的暂存单据
+9. 暂存单据 → 用户打开后可正常编辑、上传发票、提交审批
+10. 同组一致性 → 组内报销类型、项目编号不一致时正确报错

+ 91 - 0
jeeplus-modules/jeeplus-finance/src/main/java/com/jeeplus/finance/reimbursementApproval/approvalInfo/service/dto/ReimbursementImportDto.java

@@ -0,0 +1,91 @@
+package com.jeeplus.finance.reimbursementApproval.approvalInfo.service.dto;
+
+import cn.afterturn.easypoi.excel.annotation.Excel;
+import lombok.Data;
+
+/**
+ * 报销导入Excel行数据映射
+ * @author: CDB
+ * @create: 2026-10-28
+ **/
+@Data
+public class ReimbursementImportDto {
+
+    /**
+     * 是否单独报销(是/同上)
+     */
+    @Excel(name = "是否单独报销", orderNum = "1")
+    private String aloneFlag;
+
+    /**
+     * 报销类型(项目报销/报告报销/其他报销)
+     */
+    @Excel(name = "报销类型", orderNum = "2")
+    private String sourceTypeName;
+
+    /**
+     * 报销人姓名
+     */
+    @Excel(name = "报销人", orderNum = "3")
+    private String userName;
+
+    /**
+     * 报销类别名称
+     */
+    @Excel(name = "报销类别", orderNum = "4")
+    private String typeName;
+
+    /**
+     * 项目编号
+     */
+    @Excel(name = "项目编号", orderNum = "5")
+    private String projectNumber;
+
+    /**
+     * 报告号(仅报告报销)
+     */
+    @Excel(name = "报告号", orderNum = "6")
+    private String reportNo;
+
+    /**
+     * 报销详情(仅其他报销)
+     */
+    @Excel(name = "报销详情", orderNum = "7")
+    private String projectName;
+
+    /**
+     * 非数电发票费用
+     */
+    @Excel(name = "非数电发票(费用)", orderNum = "8", type = 10)
+    private String ordinaryNumber;
+
+    /**
+     * 其他费用
+     */
+    @Excel(name = "其他(费用)", orderNum = "9", type = 10)
+    private String subsidy;
+
+    /**
+     * 汇总
+     */
+    @Excel(name = "汇总", orderNum = "10", type = 10)
+    private String numberCount;
+
+    /**
+     * 非数电发票张数
+     */
+    @Excel(name = "非数电发票(张数)", orderNum = "11", type = 10)
+    private Integer ordinaryReceiptNumber;
+
+    /**
+     * 内容
+     */
+    @Excel(name = "内容", orderNum = "12")
+    private String content;
+
+    /**
+     * 出差天数
+     */
+    @Excel(name = "出差天数", orderNum = "13", type = 10)
+    private Integer days;
+}

+ 45 - 0
jeeplus-modules/jeeplus-finance/src/main/java/com/jeeplus/finance/reimbursementApproval/approvalInfo/service/dto/ReimbursementImportResult.java

@@ -0,0 +1,45 @@
+package com.jeeplus.finance.reimbursementApproval.approvalInfo.service.dto;
+
+import lombok.Data;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 报销导入结果
+ * @author: CDB
+ * @create: 2026-10-28
+ **/
+@Data
+public class ReimbursementImportResult {
+
+    /**
+     * 成功报销单数
+     */
+    private int successCount;
+
+    /**
+     * 失败报销单数
+     */
+    private int failCount;
+
+    /**
+     * 成功明细条数
+     */
+    private int successDetailCount;
+
+    /**
+     * 失败明细条数
+     */
+    private int failDetailCount;
+
+    /**
+     * 错误Excel临时文件路径(失败时返回)
+     */
+    private String errorFilePath;
+
+    /**
+     * 错误明细列表
+     */
+    private List<Map<String, Object>> errorMessages;
+}

BIN
jeeplus-modules/jeeplus-finance/src/main/resources/dot/报销导入模板.xlsx