|
|
@@ -1,5 +1,7 @@
|
|
|
package com.jeeplus.finance.reimbursementApproval.approvalInfo.service;
|
|
|
|
|
|
+import cn.afterturn.easypoi.excel.ExcelImportUtil;
|
|
|
+import cn.afterturn.easypoi.excel.entity.ImportParams;
|
|
|
import cn.hutool.core.collection.CollectionUtil;
|
|
|
import cn.hutool.core.date.DateUtil;
|
|
|
import cn.hutool.core.util.ObjectUtil;
|
|
|
@@ -37,11 +39,16 @@ import com.jeeplus.sys.service.dto.UserDTO;
|
|
|
//import com.jeeplus.sys.utils.UserUtils;
|
|
|
import org.apache.commons.compress.utils.Lists;
|
|
|
import org.apache.commons.lang3.StringUtils;
|
|
|
+import org.apache.poi.ss.usermodel.*;
|
|
|
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
|
|
import org.springframework.beans.BeanUtils;
|
|
|
import org.springframework.stereotype.Service;
|
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
|
+import org.springframework.web.multipart.MultipartFile;
|
|
|
|
|
|
import javax.annotation.Resource;
|
|
|
+import java.io.File;
|
|
|
+import java.io.FileOutputStream;
|
|
|
import java.text.SimpleDateFormat;
|
|
|
import java.time.LocalDate;
|
|
|
import java.time.format.DateTimeFormatter;
|
|
|
@@ -2078,4 +2085,621 @@ public class CwReimbursementInfoService {
|
|
|
String createUserId = infoMapper.getReimbursementCreateUserIdByProcessInstanceId(id);
|
|
|
return createUserId;
|
|
|
}
|
|
|
+
|
|
|
+ // ==================== 导入功能 ====================
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 查询所有末级报销类别
|
|
|
+ * @return 类别列表
|
|
|
+ */
|
|
|
+ public List<Map<String, Object>> findAllLeafCategories() {
|
|
|
+ return infoMapper.findAllLeafCategories();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 报销批量导入
|
|
|
+ * @param file Excel文件
|
|
|
+ * @param currentUser 当前登录用户
|
|
|
+ * @return 导入结果
|
|
|
+ */
|
|
|
+ public ReimbursementImportResult importReimbursement(MultipartFile file, UserDTO currentUser) throws Exception {
|
|
|
+ ReimbursementImportResult result = new ReimbursementImportResult();
|
|
|
+ List<Map<String, Object>> errorMessages = new ArrayList<>();
|
|
|
+ // 原始行索引(从0开始) → 错误信息
|
|
|
+ Map<Integer, String> rowErrorMap = new LinkedHashMap<>();
|
|
|
+
|
|
|
+ // 使用传入的当前用户信息
|
|
|
+ String currentUserId = currentUser.getId();
|
|
|
+ String currentUserName = currentUser.getName();
|
|
|
+ String companyId = currentUser.getCompanyDTO().getId();
|
|
|
+ String currentToken = TokenProvider.getCurrentToken();
|
|
|
+
|
|
|
+ // 业务编码(2026年后用101)
|
|
|
+ Date targetDate = DateUtil.parseDate("2026-01-01");
|
|
|
+ String bizCode = DateUtil.date().after(targetDate) ? "101" : "5";
|
|
|
+
|
|
|
+ // 当前用户部门(经办人部门)
|
|
|
+ String currentUserDeptId = "";
|
|
|
+ String currentUserDeptName = "";
|
|
|
+ if (currentUser.getOfficeDTO() != null && StringUtils.isNotBlank(currentUser.getOfficeDTO().getId())) {
|
|
|
+ currentUserDeptId = currentUser.getOfficeDTO().getId();
|
|
|
+ // 优先从DTO直接取名称
|
|
|
+ currentUserDeptName = currentUser.getOfficeDTO().getName();
|
|
|
+ // 兜底:通过API查询
|
|
|
+ if (StringUtils.isBlank(currentUserDeptName)) {
|
|
|
+ OfficeDTO officeDTO = SpringUtil.getBean(IOfficeApi.class).getOfficeById(currentUserDeptId);
|
|
|
+ if (officeDTO != null) {
|
|
|
+ currentUserDeptName = officeDTO.getName();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 1. 解析Excel(titleRows=0不跳过标题, headRows=1用第1行列头匹配, 从第2行开始读数据)
|
|
|
+ ImportParams params = new ImportParams();
|
|
|
+ params.setTitleRows(0); // 第1行就是列头,不跳过
|
|
|
+ params.setHeadRows(1); // 第1行作为列头匹配,从第2行开始读取数据
|
|
|
+ List<ReimbursementImportDto> rows = ExcelImportUtil.importExcel(file.getInputStream(), ReimbursementImportDto.class, params);
|
|
|
+
|
|
|
+ if (rows == null || rows.isEmpty()) {
|
|
|
+ throw new RuntimeException("Excel文件为空或格式不正确");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 分组:遍历所有行,按「是否单独报销」分组,跳过示例行和无效行
|
|
|
+ List<List<Map<String, Object>>> groups = new ArrayList<>();
|
|
|
+ for (int i = 0; i < rows.size(); i++) {
|
|
|
+ ReimbursementImportDto dto = rows.get(i);
|
|
|
+ String aloneFlag = safeStr(dto.getAloneFlag());
|
|
|
+ String sourceTypeName = safeStr(dto.getSourceTypeName());
|
|
|
+ String userName = safeStr(dto.getUserName());
|
|
|
+ String typeName = safeStr(dto.getTypeName());
|
|
|
+
|
|
|
+ // 跳过模板描述行(A列含"请删除"或"填写提示"等提示文字)
|
|
|
+ if (aloneFlag.contains("请删除") || aloneFlag.contains("填写提示")) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ // 跳过模板示例行(A列含[示例]标记)
|
|
|
+ if (aloneFlag.contains("[示例]")) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ // 跳过全空行(包括分隔线、提示行等)
|
|
|
+ if (StringUtils.isBlank(aloneFlag) && StringUtils.isBlank(sourceTypeName)
|
|
|
+ && StringUtils.isBlank(userName) && StringUtils.isBlank(typeName)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ // 只处理「是否单独报销」为"是"或"同上"的合法数据行
|
|
|
+ if (!"是".equals(aloneFlag) && !"同上".equals(aloneFlag)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ Map<String, Object> row = new HashMap<>();
|
|
|
+ row.put("originalIndex", i);
|
|
|
+ row.put("excelRowNum", i + 2); // 实际Excel行号(1-based,跳过列头行,索引0对应第2行)
|
|
|
+ row.put("dto", dto);
|
|
|
+ row.put("sourceTypeName", sourceTypeName);
|
|
|
+ row.put("userName", userName);
|
|
|
+ row.put("typeName", typeName);
|
|
|
+
|
|
|
+ if ("是".equals(aloneFlag) || groups.isEmpty()) {
|
|
|
+ // 新建一组
|
|
|
+ List<Map<String, Object>> group = new ArrayList<>();
|
|
|
+ group.add(row);
|
|
|
+ groups.add(group);
|
|
|
+ } else if ("同上".equals(aloneFlag)) {
|
|
|
+ // 归入上一组
|
|
|
+ groups.get(groups.size() - 1).add(row);
|
|
|
+ }
|
|
|
+ // 其他值(空或非法)的行已被空行检查跳过或后续校验捕获
|
|
|
+ }
|
|
|
+
|
|
|
+ if (groups.isEmpty()) {
|
|
|
+ throw new RuntimeException("未找到有效的数据行,请确认已删除模板示例行并填入实际数据");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 逐组校验
|
|
|
+ int successCount = 0, failCount = 0, successDetailCount = 0, failDetailCount = 0;
|
|
|
+
|
|
|
+ for (List<Map<String, Object>> group : groups) {
|
|
|
+ List<String> groupErrors = new ArrayList<>();
|
|
|
+
|
|
|
+ // 3.1 组内一致性校验(与第一行比较)
|
|
|
+ Map<String, Object> firstRow = group.get(0);
|
|
|
+ String firstSourceTypeName = (String) firstRow.get("sourceTypeName");
|
|
|
+ String firstProjectNumber = safeStr(((ReimbursementImportDto) firstRow.get("dto")).getProjectNumber());
|
|
|
+
|
|
|
+ for (int r = 1; r < group.size(); r++) {
|
|
|
+ Map<String, Object> row = group.get(r);
|
|
|
+ ReimbursementImportDto dto = (ReimbursementImportDto) row.get("dto");
|
|
|
+ int rowNum = (int) row.get("excelRowNum");
|
|
|
+
|
|
|
+ if (!firstSourceTypeName.equals(row.get("sourceTypeName"))) {
|
|
|
+ String err = "第" + rowNum + "行:同组内报销类型与第一行不一致";
|
|
|
+ groupErrors.add(err);
|
|
|
+ rowErrorMap.put((int) row.get("originalIndex"), err);
|
|
|
+ }
|
|
|
+ if (("项目报销".equals(firstSourceTypeName) || "报告报销".equals(firstSourceTypeName))
|
|
|
+ && !firstProjectNumber.equals(safeStr(dto.getProjectNumber()))) {
|
|
|
+ String err = "第" + rowNum + "行:同组内项目编号与第一行不一致";
|
|
|
+ groupErrors.add(err);
|
|
|
+ rowErrorMap.put((int) row.get("originalIndex"), err);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3.2 逐行数据匹配校验
|
|
|
+ for (Map<String, Object> row : group) {
|
|
|
+ ReimbursementImportDto dto = (ReimbursementImportDto) row.get("dto");
|
|
|
+ int rowNum = (int) row.get("excelRowNum");
|
|
|
+ int originalIndex = (int) row.get("originalIndex");
|
|
|
+ String stn = (String) row.get("sourceTypeName");
|
|
|
+ List<String> rowErrors = new ArrayList<>();
|
|
|
+
|
|
|
+ // 报销类型校验
|
|
|
+ if (!"项目报销".equals(stn) && !"报告报销".equals(stn) && !"其他报销".equals(stn)) {
|
|
|
+ rowErrors.add("第" + rowNum + "行:报销类型必须为项目报销、报告报销或其他报销");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 报销类别匹配
|
|
|
+ Map<String, Object> category = null;
|
|
|
+ if (StringUtils.isNotBlank((String) row.get("typeName"))) {
|
|
|
+ category = infoMapper.findCategoryByName((String) row.get("typeName"));
|
|
|
+ if (category == null) {
|
|
|
+ rowErrors.add("第" + rowNum + "行:报销类别「" + row.get("typeName") + "」不存在或不是末级节点");
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ rowErrors.add("第" + rowNum + "行:报销类别不能为空");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 项目编号匹配(项目报销和报告报销必填)
|
|
|
+ Map<String, Object> project = null;
|
|
|
+ if ("项目报销".equals(stn) || "报告报销".equals(stn)) {
|
|
|
+ if (StringUtils.isNotBlank(safeStr(dto.getProjectNumber()))) {
|
|
|
+ project = infoMapper.findProjectByNumber(safeStr(dto.getProjectNumber()));
|
|
|
+ if (project == null) {
|
|
|
+ rowErrors.add("第" + rowNum + "行:项目编号「" + dto.getProjectNumber() + "」不存在");
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ rowErrors.add("第" + rowNum + "行:" + stn + "必须填写项目编号");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 报告号匹配(仅报告报销)
|
|
|
+ String reportLineId = null;
|
|
|
+ if ("报告报销".equals(stn) && project != null) {
|
|
|
+ if (StringUtils.isNotBlank(safeStr(dto.getReportNo()))) {
|
|
|
+ List<Map<String, Object>> reportList = infoMapper.findReportNosByProjectId(project.get("id").toString());
|
|
|
+ boolean found = false;
|
|
|
+ for (Map<String, Object> rpt : reportList) {
|
|
|
+ if (dto.getReportNo().equals(rpt.get("report_no"))) {
|
|
|
+ reportLineId = rpt.get("line_id").toString();
|
|
|
+ found = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (!found) {
|
|
|
+ rowErrors.add("第" + rowNum + "行:报告号「" + dto.getReportNo() + "」不属于该项目");
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ rowErrors.add("第" + rowNum + "行:报告报销必须填写报告号");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 报销人匹配
|
|
|
+ String userId = currentUserId;
|
|
|
+ String userNameVal = currentUserName;
|
|
|
+ String deptId = currentUserDeptId;
|
|
|
+ String deptName = currentUserDeptName;
|
|
|
+ if (StringUtils.isNotBlank((String) row.get("userName"))) {
|
|
|
+ List<Map<String, Object>> users = infoMapper.findUserByName((String) row.get("userName"));
|
|
|
+ if (users == null || users.isEmpty()) {
|
|
|
+ rowErrors.add("第" + rowNum + "行:报销人「" + row.get("userName") + "」不存在");
|
|
|
+ } else if (users.size() > 1) {
|
|
|
+ rowErrors.add("第" + rowNum + "行:报销人「" + row.get("userName") + "」存在重名,请手动录入后调整");
|
|
|
+ } else {
|
|
|
+ Map<String, Object> u = users.get(0);
|
|
|
+ userId = u.get("id").toString();
|
|
|
+ userNameVal = u.get("name").toString();
|
|
|
+ deptId = u.get("office_id") != null ? u.get("office_id").toString() : "";
|
|
|
+ deptName = u.get("office_name") != null ? u.get("office_name").toString() : "";
|
|
|
+ }
|
|
|
+ }
|
|
|
+ row.put("matchedUserId", userId);
|
|
|
+ row.put("matchedUserName", userNameVal);
|
|
|
+ row.put("matchedDeptId", deptId);
|
|
|
+ row.put("matchedDeptName", deptName);
|
|
|
+
|
|
|
+ // 其他报销:报销详情必填
|
|
|
+ if ("其他报销".equals(stn) && StringUtils.isBlank(safeStr(dto.getProjectName()))) {
|
|
|
+ rowErrors.add("第" + rowNum + "行:其他报销必须填写报销详情");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 金额处理
|
|
|
+ String ordinaryNumber = safeStr(dto.getOrdinaryNumber());
|
|
|
+ String subsidy = safeStr(dto.getSubsidy());
|
|
|
+ String numberCount = safeStr(dto.getNumberCount());
|
|
|
+ if (StringUtils.isBlank(numberCount)) {
|
|
|
+ numberCount = calcSum(ordinaryNumber, subsidy);
|
|
|
+ }
|
|
|
+ row.put("ordinaryNumber", ordinaryNumber);
|
|
|
+ row.put("subsidy", subsidy);
|
|
|
+ row.put("numberCount", numberCount);
|
|
|
+
|
|
|
+ // 张数处理
|
|
|
+ int ordinaryReceiptNumber = dto.getOrdinaryReceiptNumber() != null ? dto.getOrdinaryReceiptNumber() : 0;
|
|
|
+ row.put("ordinaryReceiptNumber", ordinaryReceiptNumber);
|
|
|
+ row.put("receiptNumberCount", ordinaryReceiptNumber);
|
|
|
+
|
|
|
+ // 存储匹配结果
|
|
|
+ row.put("category", category);
|
|
|
+ row.put("project", project);
|
|
|
+ row.put("reportLineId", reportLineId);
|
|
|
+
|
|
|
+ if (!rowErrors.isEmpty()) {
|
|
|
+ for (String err : rowErrors) {
|
|
|
+ groupErrors.add(err);
|
|
|
+ }
|
|
|
+ // 只记录第一条错误到Excel标注
|
|
|
+ if (!rowErrorMap.containsKey(originalIndex)) {
|
|
|
+ rowErrorMap.put(originalIndex, rowErrors.get(0));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 4. 根据校验结果写入数据库或标记失败
|
|
|
+ if (groupErrors.isEmpty()) {
|
|
|
+ // 校验通过,写入数据库
|
|
|
+ try {
|
|
|
+ writeGroupToDb(group, bizCode, companyId, currentToken, currentUserId, currentUserName, currentUserDeptName);
|
|
|
+ successCount++;
|
|
|
+ successDetailCount += group.size();
|
|
|
+ } catch (Exception e) {
|
|
|
+ failCount++;
|
|
|
+ failDetailCount += group.size();
|
|
|
+ for (Map<String, Object> row : group) {
|
|
|
+ int originalIndex = (int) row.get("originalIndex");
|
|
|
+ if (!rowErrorMap.containsKey(originalIndex)) {
|
|
|
+ rowErrorMap.put(originalIndex, "第" + row.get("excelRowNum") + "行:写入失败 - " + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ errorMessages.add(createErrorEntry(
|
|
|
+ (int) group.get(0).get("excelRowNum"),
|
|
|
+ "写入失败: " + e.getMessage()));
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ failCount++;
|
|
|
+ failDetailCount += group.size();
|
|
|
+ for (String err : groupErrors) {
|
|
|
+ errorMessages.add(createErrorEntry(0, err));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 5. 如果有失败,生成错误Excel
|
|
|
+ if (!rowErrorMap.isEmpty()) {
|
|
|
+ String errorPath = generateErrorExcel(file, rowErrorMap);
|
|
|
+ result.setErrorFilePath(errorPath);
|
|
|
+ }
|
|
|
+
|
|
|
+ result.setSuccessCount(successCount);
|
|
|
+ result.setFailCount(failCount);
|
|
|
+ result.setSuccessDetailCount(successDetailCount);
|
|
|
+ result.setFailDetailCount(failDetailCount);
|
|
|
+ result.setErrorMessages(errorMessages);
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 将校验通过的一组数据写入数据库
|
|
|
+ */
|
|
|
+ private void writeGroupToDb(List<Map<String, Object>> group, String bizCode,
|
|
|
+ String companyId, String currentToken, String currentUserId,
|
|
|
+ String currentUserName, String currentDeptName) throws Exception {
|
|
|
+ // 生成主记录ID和编号
|
|
|
+ String infoId = UUID.randomUUID().toString().replace("-", "");
|
|
|
+ String no = SpringUtil.getBean(IWorkAttachmentApi.class)
|
|
|
+ .genSerialNum(companyId, bizCode, currentToken);
|
|
|
+
|
|
|
+ // 取第一行确定sourceType
|
|
|
+ ReimbursementImportDto firstDto = (ReimbursementImportDto) group.get(0).get("dto");
|
|
|
+ String sourceTypeName = (String) group.get(0).get("sourceTypeName");
|
|
|
+ String sourceType;
|
|
|
+ switch (sourceTypeName) {
|
|
|
+ case "项目报销": sourceType = "1"; break;
|
|
|
+ case "报告报销": sourceType = "3"; break;
|
|
|
+ default: sourceType = "4"; break;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 写入主表 cw_reimbursement_info(经办人 = 当前登录人)
|
|
|
+ CwReimbursementInfo info = new CwReimbursementInfo();
|
|
|
+ info.setId(infoId);
|
|
|
+ info.setNo(no);
|
|
|
+ info.setUserId(currentUserId);
|
|
|
+ info.setUserName(currentUserName);
|
|
|
+ info.setDepartment(currentDeptName);
|
|
|
+ info.setReimDate(new Date());
|
|
|
+ info.setSourceType(sourceType);
|
|
|
+ info.setType("1"); // 暂存
|
|
|
+ info.setBusinessFlag("1"); // 会计
|
|
|
+ info.setReimbursementType("0"); // 普通报销
|
|
|
+ info.setCreateById(currentUserId);
|
|
|
+ info.setCreateTime(new Date());
|
|
|
+ info.setUpdateById(currentUserId);
|
|
|
+ info.setUpdateTime(new Date());
|
|
|
+ info.setDelFlag(0);
|
|
|
+ infoMapper.insert(info);
|
|
|
+
|
|
|
+ // 写入明细记录
|
|
|
+ for (Map<String, Object> row : group) {
|
|
|
+ ReimbursementImportDto dto = (ReimbursementImportDto) row.get("dto");
|
|
|
+ String userId = (String) row.get("matchedUserId");
|
|
|
+ String userName = (String) row.get("matchedUserName");
|
|
|
+ String deptId = (String) row.get("matchedDeptId");
|
|
|
+ String deptName = (String) row.get("matchedDeptName");
|
|
|
+ String ordinaryNumber = (String) row.get("ordinaryNumber");
|
|
|
+ String subsidy = (String) row.get("subsidy");
|
|
|
+ String numberCount = (String) row.get("numberCount");
|
|
|
+ int ordinaryReceiptNumber = (int) row.get("ordinaryReceiptNumber");
|
|
|
+ int receiptNumberCount = (int) row.get("receiptNumberCount");
|
|
|
+ Map<String, Object> category = (Map<String, Object>) row.get("category");
|
|
|
+ Map<String, Object> project = (Map<String, Object>) row.get("project");
|
|
|
+
|
|
|
+ String detailId = UUID.randomUUID().toString().replace("-", "");
|
|
|
+ String typeId = category != null ? category.get("id").toString() : "";
|
|
|
+ String typeName = category != null ? category.get("name").toString() : "";
|
|
|
+
|
|
|
+ switch (sourceType) {
|
|
|
+ case "1": {
|
|
|
+ // 项目报销 → cw_reimbursement_detail_info
|
|
|
+ CwReimbursementDetailInfo detail = new CwReimbursementDetailInfo();
|
|
|
+ detail.setId(detailId);
|
|
|
+ detail.setInfoId(infoId);
|
|
|
+ detail.setUserId(userId);
|
|
|
+ detail.setUserName(userName);
|
|
|
+ detail.setDeptId(deptId);
|
|
|
+ detail.setDeptName(deptName);
|
|
|
+ detail.setTypeId(typeId);
|
|
|
+ detail.setTypeName(typeName);
|
|
|
+ detail.setProjectId(project != null ? project.get("id").toString() : "");
|
|
|
+ detail.setProjectName(project != null ? project.get("project_name").toString() : "");
|
|
|
+ detail.setNumber("0");
|
|
|
+ detail.setOrdinaryNumber(ordinaryNumber);
|
|
|
+ detail.setSubsidy(subsidy);
|
|
|
+ detail.setNumberCount(numberCount);
|
|
|
+ detail.setReceiptNumber(0);
|
|
|
+ detail.setOrdinaryReceiptNumber(ordinaryReceiptNumber);
|
|
|
+ detail.setReceiptNumberCount(receiptNumberCount);
|
|
|
+ detail.setContent(safeStr(dto.getContent()));
|
|
|
+ detail.setDays(dto.getDays());
|
|
|
+ detail.setUniqueId(UUID.randomUUID().toString().replace("-", ""));
|
|
|
+ detail.setReimbursementType("0");
|
|
|
+ detail.setCreateById(currentUserId);
|
|
|
+ detail.setCreateTime(new Date());
|
|
|
+ detail.setUpdateById(currentUserId);
|
|
|
+ detail.setUpdateTime(new Date());
|
|
|
+ detail.setDelFlag(0);
|
|
|
+ detailInfoMapper.insert(detail);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ case "3": {
|
|
|
+ // 报告报销 → cw_reimbursement_detail_info_report
|
|
|
+ CwReimbursementDetailInfoReport detail = new CwReimbursementDetailInfoReport();
|
|
|
+ detail.setId(detailId);
|
|
|
+ detail.setInfoId(infoId);
|
|
|
+ detail.setUserId(userId);
|
|
|
+ detail.setUserName(userName);
|
|
|
+ detail.setDeptId(deptId);
|
|
|
+ detail.setDeptName(deptName);
|
|
|
+ detail.setTypeId(typeId);
|
|
|
+ detail.setTypeName(typeName);
|
|
|
+ detail.setProjectId(project != null ? project.get("id").toString() : "");
|
|
|
+ detail.setReportId(row.get("reportLineId") != null ? row.get("reportLineId").toString() : "");
|
|
|
+ detail.setReportNumber(safeStr(dto.getReportNo()));
|
|
|
+ detail.setNumber("0");
|
|
|
+ detail.setOrdinaryNumber(ordinaryNumber);
|
|
|
+ detail.setSubsidy(subsidy);
|
|
|
+ detail.setNumberCount(numberCount);
|
|
|
+ detail.setReceiptNumber(0);
|
|
|
+ detail.setOrdinaryReceiptNumber(ordinaryReceiptNumber);
|
|
|
+ detail.setReceiptNumberCount(receiptNumberCount);
|
|
|
+ detail.setContent(safeStr(dto.getContent()));
|
|
|
+ detail.setDays(dto.getDays());
|
|
|
+ detail.setUniqueId(UUID.randomUUID().toString().replace("-", ""));
|
|
|
+ detail.setReimbursementType("0");
|
|
|
+ detail.setCreateById(currentUserId);
|
|
|
+ detail.setCreateTime(new Date());
|
|
|
+ detail.setUpdateById(currentUserId);
|
|
|
+ detail.setUpdateTime(new Date());
|
|
|
+ detail.setDelFlag(0);
|
|
|
+ cwReimbursementDetailInfoReportMapper.insert(detail);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ default: {
|
|
|
+ // 其他报销 → cw_reimbursement_detail_info_other
|
|
|
+ CwReimbursementDetailInfoOther detail = new CwReimbursementDetailInfoOther();
|
|
|
+ detail.setId(detailId);
|
|
|
+ detail.setInfoId(infoId);
|
|
|
+ detail.setUserId(userId);
|
|
|
+ detail.setUserName(userName);
|
|
|
+ detail.setDeptId(deptId);
|
|
|
+ detail.setDeptName(deptName);
|
|
|
+ detail.setTypeId(typeId);
|
|
|
+ detail.setTypeName(typeName);
|
|
|
+ detail.setProjectName(safeStr(dto.getProjectName())); // 报销详情(自由文本)
|
|
|
+ detail.setNumber("0");
|
|
|
+ detail.setOrdinaryNumber(ordinaryNumber);
|
|
|
+ detail.setSubsidy(subsidy);
|
|
|
+ detail.setNumberCount(numberCount);
|
|
|
+ detail.setReceiptNumber(0);
|
|
|
+ detail.setOrdinaryReceiptNumber(ordinaryReceiptNumber);
|
|
|
+ detail.setReceiptNumberCount(receiptNumberCount);
|
|
|
+ detail.setContent(safeStr(dto.getContent()));
|
|
|
+ detail.setDays(dto.getDays());
|
|
|
+ detail.setUniqueId(UUID.randomUUID().toString().replace("-", ""));
|
|
|
+ detail.setReimbursementType("0");
|
|
|
+ detail.setCreateById(currentUserId);
|
|
|
+ detail.setCreateTime(new Date());
|
|
|
+ detail.setUpdateById(currentUserId);
|
|
|
+ detail.setUpdateTime(new Date());
|
|
|
+ detail.setDelFlag(0);
|
|
|
+ cwReimbursementDetailInfoOtherMapper.insert(detail);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成错误Excel文件
|
|
|
+ * @param originalFile 原始上传文件
|
|
|
+ * @param rowErrorMap 行索引→错误信息
|
|
|
+ * @return 临时文件路径
|
|
|
+ */
|
|
|
+ private String generateErrorExcel(MultipartFile originalFile, Map<Integer, String> rowErrorMap) throws Exception {
|
|
|
+ Workbook workbook = WorkbookFactory.create(originalFile.getInputStream());
|
|
|
+ Sheet sheet = workbook.getSheetAt(0);
|
|
|
+
|
|
|
+ // 错误原因列放在第N列(0-based,13列数据之后 = 第14列即N列)
|
|
|
+ int errorColIndex = 13;
|
|
|
+
|
|
|
+ // 创建错误样式:绿色底色(与模板示例行一致)+ 红色字体
|
|
|
+ CellStyle errorStyle = workbook.createCellStyle();
|
|
|
+ Font errorFont = workbook.createFont();
|
|
|
+ errorFont.setColor(IndexedColors.RED.getIndex());
|
|
|
+ errorStyle.setFont(errorFont);
|
|
|
+ errorStyle.setFillForegroundColor(IndexedColors.LIGHT_GREEN.getIndex());
|
|
|
+ errorStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
|
|
|
+
|
|
|
+ // 写错误列表头
|
|
|
+ Row headerRow = sheet.getRow(0); // 第1行是列头行(0-based)
|
|
|
+ if (headerRow != null) {
|
|
|
+ Cell errorHeaderCell = headerRow.createCell(errorColIndex);
|
|
|
+ errorHeaderCell.setCellValue("错误原因");
|
|
|
+ errorHeaderCell.setCellStyle(errorStyle);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 逐行标注错误(数据从第2行开始,0-based,跳过第0行列头)
|
|
|
+ for (Map.Entry<Integer, String> entry : rowErrorMap.entrySet()) {
|
|
|
+ int dataRowIndex = entry.getKey(); // 数据列表中的索引(0-based)
|
|
|
+ int excelRowNum = dataRowIndex + 1; // 对应Sheet中的行号(0-based,跳过第0行列头)
|
|
|
+ Row dataRow = sheet.getRow(excelRowNum);
|
|
|
+ if (dataRow == null) {
|
|
|
+ dataRow = sheet.createRow(excelRowNum);
|
|
|
+ }
|
|
|
+ // 整行标红
|
|
|
+ for (int c = 0; c <= errorColIndex; c++) {
|
|
|
+ Cell cell = dataRow.getCell(c);
|
|
|
+ if (cell == null) {
|
|
|
+ cell = dataRow.createCell(c);
|
|
|
+ }
|
|
|
+ cell.setCellStyle(errorStyle);
|
|
|
+ }
|
|
|
+ // 写错误原因
|
|
|
+ Cell errorCell = dataRow.getCell(errorColIndex);
|
|
|
+ if (errorCell == null) {
|
|
|
+ errorCell = dataRow.createCell(errorColIndex);
|
|
|
+ }
|
|
|
+ errorCell.setCellValue(entry.getValue());
|
|
|
+ }
|
|
|
+
|
|
|
+ // 保存临时文件
|
|
|
+ String tempPath = System.getProperty("java.io.tmpdir") + File.separator
|
|
|
+ + "import_error_" + System.currentTimeMillis() + ".xlsx";
|
|
|
+ try (FileOutputStream fos = new FileOutputStream(tempPath)) {
|
|
|
+ workbook.write(fos);
|
|
|
+ }
|
|
|
+ workbook.close();
|
|
|
+ return tempPath;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 构建行数据Map
|
|
|
+ */
|
|
|
+ private Map<String, Object> buildRowMap(int index, ReimbursementImportDto dto) {
|
|
|
+ Map<String, Object> row = new HashMap<>();
|
|
|
+ row.put("originalIndex", index);
|
|
|
+ row.put("excelRowNum", index + 2);
|
|
|
+ row.put("dto", dto);
|
|
|
+ row.put("sourceTypeName", safeStr(dto.getSourceTypeName()));
|
|
|
+ row.put("userName", safeStr(dto.getUserName()));
|
|
|
+ row.put("typeName", safeStr(dto.getTypeName()));
|
|
|
+ return row;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 判断行是否全空
|
|
|
+ */
|
|
|
+ private boolean isRowEmpty(ReimbursementImportDto dto) {
|
|
|
+ return StringUtils.isBlank(safeStr(dto.getAloneFlag()))
|
|
|
+ && StringUtils.isBlank(safeStr(dto.getSourceTypeName()))
|
|
|
+ && StringUtils.isBlank(safeStr(dto.getUserName()))
|
|
|
+ && StringUtils.isBlank(safeStr(dto.getTypeName()))
|
|
|
+ && StringUtils.isBlank(safeStr(dto.getProjectNumber()))
|
|
|
+ && StringUtils.isBlank(safeStr(dto.getProjectName()))
|
|
|
+ && StringUtils.isBlank(safeStr(dto.getReportNo()))
|
|
|
+ && StringUtils.isBlank(safeStr(dto.getOrdinaryNumber()))
|
|
|
+ && StringUtils.isBlank(safeStr(dto.getSubsidy()))
|
|
|
+ && StringUtils.isBlank(safeStr(dto.getNumberCount()))
|
|
|
+ && dto.getOrdinaryReceiptNumber() == null
|
|
|
+ && StringUtils.isBlank(safeStr(dto.getContent()))
|
|
|
+ && dto.getDays() == null;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 安全转字符串
|
|
|
+ */
|
|
|
+ private String safeStr(Object obj) {
|
|
|
+ if (obj == null) return "";
|
|
|
+ String s = obj.toString().trim();
|
|
|
+ // EasyPOI可能将数字读为Double(如 "123.0"),去掉末尾的.0
|
|
|
+ if (s.endsWith(".0")) {
|
|
|
+ try {
|
|
|
+ Double.parseDouble(s);
|
|
|
+ return s.substring(0, s.length() - 2);
|
|
|
+ } catch (NumberFormatException ignored) {}
|
|
|
+ }
|
|
|
+ return s;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 计算汇总金额
|
|
|
+ */
|
|
|
+ private String calcSum(String ordinaryNumber, String subsidy) {
|
|
|
+ double ordinary = parseDouble(ordinaryNumber);
|
|
|
+ double subsidyVal = parseDouble(subsidy);
|
|
|
+ double sum = ordinary + subsidyVal;
|
|
|
+ if (sum == 0) return "0";
|
|
|
+ return String.valueOf(sum);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 安全解析double
|
|
|
+ */
|
|
|
+ private double parseDouble(String str) {
|
|
|
+ if (StringUtils.isBlank(str)) return 0;
|
|
|
+ try {
|
|
|
+ return Double.parseDouble(str);
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 创建错误信息条目
|
|
|
+ */
|
|
|
+ private Map<String, Object> createErrorEntry(int row, String message) {
|
|
|
+ Map<String, Object> entry = new HashMap<>();
|
|
|
+ entry.put("row", row);
|
|
|
+ entry.put("message", message);
|
|
|
+ return entry;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 列索引转Excel列字母(0→A, 1→B, ...)
|
|
|
+ */
|
|
|
+ private String getColumnLetter(int colIndex) {
|
|
|
+ StringBuilder sb = new StringBuilder();
|
|
|
+ while (colIndex >= 0) {
|
|
|
+ sb.insert(0, (char) ('A' + colIndex % 26));
|
|
|
+ colIndex = colIndex / 26 - 1;
|
|
|
+ }
|
|
|
+ return sb.toString();
|
|
|
+ }
|
|
|
}
|