员工亲属实体关联

This commit is contained in:
wkc
2026-04-24 13:29:13 +08:00
parent b7db711906
commit aa08ab4711
51 changed files with 2070 additions and 845 deletions

View File

@@ -4,8 +4,10 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.info.collection.domain.dto.CcdiBaseStaffAddDTO;
import com.ruoyi.info.collection.domain.dto.CcdiBaseStaffEditDTO;
import com.ruoyi.info.collection.domain.dto.CcdiBaseStaffQueryDTO;
import com.ruoyi.info.collection.domain.excel.CcdiBaseStaffAssetInfoExcel;
import com.ruoyi.info.collection.domain.excel.CcdiBaseStaffExcel;
import com.ruoyi.info.collection.domain.vo.*;
import com.ruoyi.info.collection.service.ICcdiBaseStaffAssetImportService;
import com.ruoyi.info.collection.service.ICcdiBaseStaffImportService;
import com.ruoyi.info.collection.service.ICcdiBaseStaffService;
import com.ruoyi.info.collection.utils.EasyExcelUtil;
@@ -45,6 +47,9 @@ public class CcdiBaseStaffController extends BaseController {
@Resource
private ICcdiBaseStaffImportService importAsyncService;
@Resource
private ICcdiBaseStaffAssetImportService baseStaffAssetImportService;
/**
* 查询员工列表
*/
@@ -120,7 +125,14 @@ public class CcdiBaseStaffController extends BaseController {
@Operation(summary = "下载导入模板")
@PostMapping("/importTemplate")
public void importTemplate(HttpServletResponse response) {
EasyExcelUtil.importTemplateWithDictDropdown(response, CcdiBaseStaffExcel.class, "员工信息");
EasyExcelUtil.importTemplateWithDictDropdown(
response,
CcdiBaseStaffExcel.class,
"员工信息",
CcdiBaseStaffAssetInfoExcel.class,
"员工资产信息",
"员工信息维护导入模板"
);
}
/**
@@ -130,21 +142,33 @@ public class CcdiBaseStaffController extends BaseController {
@PreAuthorize("@ss.hasPermi('ccdi:baseStaff:import')")
@Log(title = "员工信息", businessType = BusinessType.IMPORT)
@PostMapping("/importData")
public AjaxResult importData(MultipartFile file, boolean updateSupport) throws Exception {
List<CcdiBaseStaffExcel> list = EasyExcelUtil.importExcel(file.getInputStream(), CcdiBaseStaffExcel.class);
public AjaxResult importData(MultipartFile file) throws Exception {
List<CcdiBaseStaffExcel> staffList = EasyExcelUtil.importExcel(
file.getInputStream(),
CcdiBaseStaffExcel.class,
"员工信息"
);
List<CcdiBaseStaffAssetInfoExcel> assetList = EasyExcelUtil.importExcel(
file.getInputStream(),
CcdiBaseStaffAssetInfoExcel.class,
"员工资产信息"
);
if (list == null || list.isEmpty()) {
boolean hasStaffRows = staffList != null && !staffList.isEmpty();
boolean hasAssetRows = assetList != null && !assetList.isEmpty();
if (!hasStaffRows && !hasAssetRows) {
return error("至少需要一条数据");
}
// 提交异步任务
String taskId = baseStaffService.importBaseStaff(list, updateSupport);
// 立即返回,不等待后台任务完成
ImportResultVO result = new ImportResultVO();
result.setTaskId(taskId);
result.setStatus("PROCESSING");
result.setMessage("导入任务已提交,正在后台处理");
BaseStaffImportSubmitResultVO result = new BaseStaffImportSubmitResultVO();
if (hasStaffRows) {
result.setStaffTaskId(baseStaffService.importBaseStaff(staffList));
}
if (hasAssetRows) {
result.setAssetTaskId(baseStaffAssetImportService.importAssetInfo(assetList));
}
result.setMessage(buildImportSubmitMessage(hasStaffRows, hasAssetRows));
return AjaxResult.success("导入任务已提交,正在后台处理", result);
}
@@ -190,4 +214,14 @@ public class CcdiBaseStaffController extends BaseController {
return getDataTable(pageData, failures.size());
}
private String buildImportSubmitMessage(boolean hasStaffRows, boolean hasAssetRows) {
if (hasStaffRows && hasAssetRows) {
return "已提交员工信息和员工资产信息导入任务";
}
if (hasStaffRows) {
return "已提交员工信息导入任务";
}
return "已提交员工资产信息导入任务";
}
}

View File

@@ -4,11 +4,13 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.info.collection.domain.dto.CcdiStaffFmyRelationAddDTO;
import com.ruoyi.info.collection.domain.dto.CcdiStaffFmyRelationEditDTO;
import com.ruoyi.info.collection.domain.dto.CcdiStaffFmyRelationQueryDTO;
import com.ruoyi.info.collection.domain.excel.CcdiAssetInfoExcel;
import com.ruoyi.info.collection.domain.excel.CcdiStaffFmyRelationExcel;
import com.ruoyi.info.collection.domain.vo.CcdiStaffFmyRelationVO;
import com.ruoyi.info.collection.domain.vo.ImportResultVO;
import com.ruoyi.info.collection.domain.vo.ImportStatusVO;
import com.ruoyi.info.collection.domain.vo.StaffFmyRelationImportFailureVO;
import com.ruoyi.info.collection.domain.vo.StaffFmyRelationImportSubmitResultVO;
import com.ruoyi.info.collection.service.ICcdiAssetInfoImportService;
import com.ruoyi.info.collection.service.ICcdiStaffFmyRelationImportService;
import com.ruoyi.info.collection.service.ICcdiStaffFmyRelationService;
import com.ruoyi.info.collection.utils.EasyExcelUtil;
@@ -49,6 +51,9 @@ public class CcdiStaffFmyRelationController extends BaseController {
@Resource
private ICcdiStaffFmyRelationImportService relationImportService;
@Resource
private ICcdiAssetInfoImportService assetInfoImportService;
/**
* 查询员工亲属关系列表
*/
@@ -115,7 +120,14 @@ public class CcdiStaffFmyRelationController extends BaseController {
@Operation(summary = "下载导入模板")
@PostMapping("/importTemplate")
public void importTemplate(HttpServletResponse response) {
EasyExcelUtil.importTemplateWithDictDropdown(response, CcdiStaffFmyRelationExcel.class, "员工亲属关系信息");
EasyExcelUtil.importTemplateWithDictDropdown(
response,
CcdiStaffFmyRelationExcel.class,
"员工亲属关系信息",
CcdiAssetInfoExcel.class,
"亲属资产信息",
"员工亲属关系维护导入模板"
);
}
/**
@@ -127,20 +139,32 @@ public class CcdiStaffFmyRelationController extends BaseController {
@Log(title = "员工亲属关系", businessType = BusinessType.IMPORT)
@PostMapping("/importData")
public AjaxResult importData(@Parameter(description = "导入文件") MultipartFile file) throws Exception {
List<CcdiStaffFmyRelationExcel> list = EasyExcelUtil.importExcel(file.getInputStream(), CcdiStaffFmyRelationExcel.class);
List<CcdiStaffFmyRelationExcel> relationList = EasyExcelUtil.importExcel(
file.getInputStream(),
CcdiStaffFmyRelationExcel.class,
"员工亲属关系信息"
);
List<CcdiAssetInfoExcel> assetList = EasyExcelUtil.importExcel(
file.getInputStream(),
CcdiAssetInfoExcel.class,
"亲属资产信息"
);
if (list == null || list.isEmpty()) {
boolean hasRelationRows = relationList != null && !relationList.isEmpty();
boolean hasAssetRows = assetList != null && !assetList.isEmpty();
if (!hasRelationRows && !hasAssetRows) {
return error("至少需要一条数据");
}
// 提交异步任务
String taskId = relationService.importRelation(list);
// 立即返回,不等待后台任务完成
ImportResultVO result = new ImportResultVO();
result.setTaskId(taskId);
result.setStatus("PROCESSING");
result.setMessage("导入任务已提交,正在后台处理");
StaffFmyRelationImportSubmitResultVO result = new StaffFmyRelationImportSubmitResultVO();
if (hasRelationRows) {
result.setRelationTaskId(relationService.importRelation(relationList));
}
if (hasAssetRows) {
result.setAssetTaskId(assetInfoImportService.importAssetInfo(assetList));
}
result.setMessage(buildImportSubmitMessage(hasRelationRows, hasAssetRows));
return AjaxResult.success("导入任务已提交,正在后台处理", result);
}
@@ -186,4 +210,14 @@ public class CcdiStaffFmyRelationController extends BaseController {
return getDataTable(pageData, failures.size());
}
private String buildImportSubmitMessage(boolean hasRelationRows, boolean hasAssetRows) {
if (hasRelationRows && hasAssetRows) {
return "已提交员工亲属关系和亲属资产信息导入任务";
}
if (hasRelationRows) {
return "已提交员工亲属关系导入任务";
}
return "已提交亲属资产信息导入任务";
}
}

View File

@@ -2,8 +2,6 @@ package com.ruoyi.info.collection.domain.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.io.Serial;
@@ -20,29 +18,19 @@ public class CcdiPurchaseTransactionSupplierDTO implements Serializable {
private static final long serialVersionUID = 1L;
@NotBlank(message = "供应商名称不能为空")
@Size(max = 200, message = "供应商名称长度不能超过200个字符")
@Schema(description = "供应商名称")
private String supplierName;
@Pattern(
regexp = "^$|^[0-9A-HJ-NPQRTUWXY]{2}\\d{6}[0-9A-HJ-NPQRTUWXY]{10}$",
message = "供应商统一信用代码格式不正确"
)
@NotBlank(message = "供应商统一信用代码不能为空")
@Schema(description = "供应商统一信用代码")
private String supplierUscc;
@Size(max = 50, message = "供应商联系人长度不能超过50个字符")
@Schema(description = "供应商联系人")
private String contactPerson;
@Pattern(
regexp = "^$|^1[3-9]\\d{9}$|^0\\d{2,3}-?\\d{7,8}$",
message = "供应商联系电话格式不正确"
)
@Schema(description = "供应商联系电话")
private String contactPhone;
@Size(max = 50, message = "供应商银行账户长度不能超过50个字符")
@Schema(description = "供应商银行账户")
private String supplierBankAccount;

View File

@@ -15,6 +15,14 @@ import java.math.BigDecimal;
@Schema(description = "亲属资产信息导入失败记录")
public class AssetImportFailureVO {
/** Sheet名称 */
@Schema(description = "Sheet名称")
private String sheetName;
/** Excel行号 */
@Schema(description = "Excel行号")
private Integer rowNum;
/** 亲属证件号 */
@Schema(description = "亲属证件号")
private String personId;

View File

@@ -15,6 +15,14 @@ import java.math.BigDecimal;
@Schema(description = "员工资产信息导入失败记录")
public class BaseStaffAssetImportFailureVO {
/** Sheet名称 */
@Schema(description = "Sheet名称")
private String sheetName;
/** Excel行号 */
@Schema(description = "Excel行号")
private Integer rowNum;
/** 员工身份证号 */
@Schema(description = "员工身份证号")
private String personId;

View File

@@ -0,0 +1,23 @@
package com.ruoyi.info.collection.domain.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 员工双Sheet导入提交结果
*
* @author ruoyi
*/
@Data
@Schema(description = "员工双Sheet导入提交结果")
public class BaseStaffImportSubmitResultVO {
@Schema(description = "员工信息导入任务ID")
private String staffTaskId;
@Schema(description = "员工资产信息导入任务ID")
private String assetTaskId;
@Schema(description = "提交说明")
private String message;
}

View File

@@ -14,8 +14,14 @@ import java.math.BigDecimal;
@Schema(description = "导入失败记录")
public class ImportFailureVO {
@Schema(description = "Sheet名称")
private String sheetName;
@Schema(description = "Excel行号")
private Integer rowNum;
@Schema(description = "柜员号")
private Long employeeId;
private Long staffId;
@Schema(description = "姓名")
private String name;

View File

@@ -15,6 +15,14 @@ import java.math.BigDecimal;
@Schema(description = "招投标信息导入失败记录")
public class PurchaseTransactionImportFailureVO {
/** 失败来源Sheet */
@Schema(description = "失败来源Sheet")
private String sheetName;
/** 失败行号 */
@Schema(description = "失败行号")
private String sheetRowNum;
/** 采购事项ID */
@Schema(description = "采购事项ID")
private String purchaseId;

View File

@@ -15,6 +15,14 @@ import java.math.BigDecimal;
@Schema(description = "员工亲属关系信息导入失败记录")
public class StaffFmyRelationImportFailureVO {
/** Sheet名称 */
@Schema(description = "Sheet名称")
private String sheetName;
/** Excel行号 */
@Schema(description = "Excel行号")
private Integer rowNum;
/** 员工身份证号 */
@Schema(description = "员工身份证号")
private String personId;

View File

@@ -0,0 +1,24 @@
package com.ruoyi.info.collection.domain.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 员工亲属关系双Sheet导入提交结果
*
* @author ruoyi
* @date 2026-04-22
*/
@Data
@Schema(description = "员工亲属关系双Sheet导入提交结果")
public class StaffFmyRelationImportSubmitResultVO {
@Schema(description = "员工亲属关系导入任务ID")
private String relationTaskId;
@Schema(description = "亲属资产信息导入任务ID")
private String assetTaskId;
@Schema(description = "提交结果提示")
private String message;
}

View File

@@ -15,10 +15,9 @@ public interface ICcdiBaseStaffImportService {
/**
* 异步导入员工数据
*
* @param excelList Excel数据列表
* @param isUpdateSupport 是否更新已存在的数据
* @param excelList Excel数据列表
*/
void importBaseStaffAsync(List<CcdiBaseStaffExcel> excelList, Boolean isUpdateSupport, String taskId);
void importBaseStaffAsync(List<CcdiBaseStaffExcel> excelList, String taskId);
/**
* 查询导入状态

View File

@@ -78,11 +78,10 @@ public interface ICcdiBaseStaffService {
/**
* 导入员工数据
*
* @param excelList Excel实体列表
* @param isUpdateSupport 是否更新支持
* @param excelList Excel实体列表
* @return 结果
*/
String importBaseStaff(List<CcdiBaseStaffExcel> excelList, Boolean isUpdateSupport);
String importBaseStaff(List<CcdiBaseStaffExcel> excelList);
/**
* 查询员工下拉列表

View File

@@ -39,6 +39,8 @@ import java.util.concurrent.TimeUnit;
public class CcdiAssetInfoImportServiceImpl implements ICcdiAssetInfoImportService {
private static final String STATUS_KEY_PREFIX = "import:assetInfo:";
private static final String SHEET_NAME = "亲属资产信息";
private static final int EXCEL_DATA_START_ROW = 2;
@Resource
private CcdiAssetInfoMapper assetInfoMapper;
@@ -91,7 +93,8 @@ public class CcdiAssetInfoImportServiceImpl implements ICcdiAssetInfoImportServi
Map<String, Set<String>> ownerMap = buildOwnerMap(personIds);
for (CcdiAssetInfoExcel excel : excelList) {
for (int i = 0; i < excelList.size(); i++) {
CcdiAssetInfoExcel excel = excelList.get(i);
try {
validateExcel(excel);
Set<String> familyIds = ownerMap.get(excel.getPersonId());
@@ -111,6 +114,8 @@ public class CcdiAssetInfoImportServiceImpl implements ICcdiAssetInfoImportServi
} catch (Exception e) {
AssetImportFailureVO failureVO = new AssetImportFailureVO();
BeanUtils.copyProperties(excel, failureVO);
failureVO.setSheetName(SHEET_NAME);
failureVO.setRowNum(i + EXCEL_DATA_START_ROW);
failureVO.setErrorMessage(e.getMessage());
failures.add(failureVO);
}

View File

@@ -1,6 +1,7 @@
package com.ruoyi.info.collection.service.impl;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.info.collection.domain.CcdiAssetInfo;
@@ -90,14 +91,24 @@ public class CcdiBaseStaffAssetImportServiceImpl implements ICcdiBaseStaffAssetI
.toList();
Map<String, Set<String>> ownerMap = buildOwnerMap(personIds);
Set<String> existingAssetKeys = buildExistingAssetKeys(personIds);
Set<String> importedAssetKeys = new java.util.LinkedHashSet<>();
for (CcdiBaseStaffAssetInfoExcel excel : excelList) {
for (int i = 0; i < excelList.size(); i++) {
CcdiBaseStaffAssetInfoExcel excel = excelList.get(i);
try {
validateExcel(excel);
Set<String> familyIds = ownerMap.get(excel.getPersonId());
if (familyIds == null || familyIds.isEmpty()) {
throw new RuntimeException("员工资产导入仅支持员工本人证件号");
}
String assetKey = buildAssetKey(excel.getPersonId(), excel.getAssetMainType(), excel.getAssetSubType(), excel.getAssetName());
if (existingAssetKeys.contains(assetKey)) {
throw new RuntimeException("资产记录已存在");
}
if (!importedAssetKeys.add(assetKey)) {
throw new RuntimeException("资产记录在导入文件中重复");
}
CcdiAssetInfo assetInfo = new CcdiAssetInfo();
BeanUtils.copyProperties(excel, assetInfo);
@@ -109,6 +120,8 @@ public class CcdiBaseStaffAssetImportServiceImpl implements ICcdiBaseStaffAssetI
} catch (Exception e) {
BaseStaffAssetImportFailureVO failureVO = new BaseStaffAssetImportFailureVO();
BeanUtils.copyProperties(excel, failureVO);
failureVO.setSheetName("员工资产信息");
failureVO.setRowNum(i + 2);
failureVO.setErrorMessage(e.getMessage());
failures.add(failureVO);
}
@@ -168,6 +181,18 @@ public class CcdiBaseStaffAssetImportServiceImpl implements ICcdiBaseStaffAssetI
return result;
}
private Set<String> buildExistingAssetKeys(List<String> personIds) {
if (personIds == null || personIds.isEmpty()) {
return Set.of();
}
LambdaQueryWrapper<CcdiAssetInfo> wrapper = new LambdaQueryWrapper<>();
wrapper.in(CcdiAssetInfo::getPersonId, personIds);
return assetInfoMapper.selectList(wrapper).stream()
.filter(asset -> StringUtils.equals(asset.getFamilyId(), asset.getPersonId()))
.map(asset -> buildAssetKey(asset.getPersonId(), asset.getAssetMainType(), asset.getAssetSubType(), asset.getAssetName()))
.collect(java.util.stream.Collectors.toCollection(java.util.LinkedHashSet::new));
}
private void mergeOwnerMappings(Map<String, Set<String>> result, List<Map<String, String>> mappings) {
if (mappings == null) {
return;
@@ -203,6 +228,14 @@ public class CcdiBaseStaffAssetImportServiceImpl implements ICcdiBaseStaffAssetI
}
}
private String buildAssetKey(String personId, String assetMainType, String assetSubType, String assetName) {
return String.join("|",
StringUtils.nvl(personId, ""),
StringUtils.nvl(assetMainType, ""),
StringUtils.nvl(assetSubType, ""),
StringUtils.nvl(assetName, ""));
}
private void updateImportStatus(String taskId, String status, ImportResult result) {
Map<String, Object> statusData = new HashMap<>();
statusData.put("status", status);

View File

@@ -2,6 +2,7 @@ package com.ruoyi.info.collection.service.impl;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.ruoyi.common.core.domain.entity.SysDept;
import com.ruoyi.info.collection.domain.CcdiBaseStaff;
import com.ruoyi.info.collection.domain.dto.CcdiBaseStaffAddDTO;
import com.ruoyi.info.collection.domain.excel.CcdiBaseStaffExcel;
@@ -13,6 +14,7 @@ import com.ruoyi.info.collection.service.ICcdiBaseStaffImportService;
import com.ruoyi.info.collection.utils.ImportLogUtils;
import com.ruoyi.common.utils.IdCardUtil;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.mapper.SysDeptMapper;
import jakarta.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -43,16 +45,18 @@ public class CcdiBaseStaffImportServiceImpl implements ICcdiBaseStaffImportServi
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource
private SysDeptMapper deptMapper;
@Override
@Async
public void importBaseStaffAsync(List<CcdiBaseStaffExcel> excelList, Boolean isUpdateSupport, String taskId) {
public void importBaseStaffAsync(List<CcdiBaseStaffExcel> excelList, String taskId) {
long startTime = System.currentTimeMillis();
// 记录导入开始
ImportLogUtils.logImportStart(log, taskId, "员工基础信息", excelList.size(), "系统");
List<CcdiBaseStaff> newRecords = new ArrayList<>();
List<CcdiBaseStaff> updateRecords = new ArrayList<>();
List<ImportFailureVO> failures = new ArrayList<>();
// 批量查询已存在的员工ID和身份证号
@@ -75,13 +79,12 @@ public class CcdiBaseStaffImportServiceImpl implements ICcdiBaseStaffImportServi
CcdiBaseStaffAddDTO addDTO = new CcdiBaseStaffAddDTO();
BeanUtils.copyProperties(excel, addDTO);
// 验证数据(支持更新模式)
validateStaffData(addDTO, isUpdateSupport, existingIds, existingIdCards);
validateStaffData(addDTO, existingIds, existingIdCards);
CcdiBaseStaff staff = new CcdiBaseStaff();
BeanUtils.copyProperties(excel, staff);
// 统一检查Excel内重复(更新和新增两个分支都需要检查)
// 统一检查Excel内重复
if (processedStaffIds.contains(excel.getStaffId())) {
throw new RuntimeException(String.format("员工ID[%d]在导入文件中重复,已跳过此条记录", excel.getStaffId()));
}
@@ -90,20 +93,7 @@ public class CcdiBaseStaffImportServiceImpl implements ICcdiBaseStaffImportServi
throw new RuntimeException(String.format("身份证号[%s]在导入文件中重复,已跳过此条记录", excel.getIdCard()));
}
// 检查员工ID是否在数据库中已存在
if (existingIds.contains(excel.getStaffId())) {
// 员工ID已存在于数据库
if (!isUpdateSupport) {
throw new RuntimeException("员工ID已存在且未启用更新支持");
}
// 通过检查,添加到更新列表
updateRecords.add(staff);
} else {
// 员工ID不存在,添加到新增列表
newRecords.add(staff);
}
newRecords.add(staff);
// 统一标记为已处理(只有成功添加到列表后才会执行到这里)
if (excel.getStaffId() != null) {
@@ -115,11 +105,13 @@ public class CcdiBaseStaffImportServiceImpl implements ICcdiBaseStaffImportServi
// 记录进度
ImportLogUtils.logProgress(log, taskId, i + 1, excelList.size(),
newRecords.size() + updateRecords.size(), failures.size());
newRecords.size(), failures.size());
} catch (Exception e) {
ImportFailureVO failure = new ImportFailureVO();
BeanUtils.copyProperties(excel, failure);
failure.setSheetName("员工信息");
failure.setRowNum(i + 2);
failure.setErrorMessage(e.getMessage());
failures.add(failure);
@@ -137,13 +129,6 @@ public class CcdiBaseStaffImportServiceImpl implements ICcdiBaseStaffImportServi
saveBatch(newRecords, 500);
}
// 批量更新已有数据(先删除再插入)
if (!updateRecords.isEmpty() && isUpdateSupport) {
ImportLogUtils.logBatchOperationStart(log, taskId, "更新",
(updateRecords.size() + 499) / 500, 500);
baseStaffMapper.insertOrUpdateBatch(updateRecords);
}
// 保存失败记录到Redis
if (!failures.isEmpty()) {
try {
@@ -157,7 +142,7 @@ public class CcdiBaseStaffImportServiceImpl implements ICcdiBaseStaffImportServi
ImportResult result = new ImportResult();
result.setTotalCount(excelList.size());
result.setSuccessCount(newRecords.size() + updateRecords.size());
result.setSuccessCount(newRecords.size());
result.setFailureCount(failures.size());
// 更新最终状态
@@ -299,11 +284,10 @@ public class CcdiBaseStaffImportServiceImpl implements ICcdiBaseStaffImportServi
* 验证员工数据
*
* @param addDTO 新增DTO
* @param isUpdateSupport 是否支持更新
* @param existingIds 已存在的员工ID集合(导入场景使用,传null表示单条新增)
* @param existingIdCards 已存在的身份证号集合(导入场景使用,传null表示单条新增)
*/
public void validateStaffData(CcdiBaseStaffAddDTO addDTO, Boolean isUpdateSupport, Set<Long> existingIds, Set<String> existingIdCards) {
public void validateStaffData(CcdiBaseStaffAddDTO addDTO, Set<Long> existingIds, Set<String> existingIdCards) {
// 验证必填字段
if (StringUtils.isEmpty(addDTO.getName())) {
throw new RuntimeException("姓名不能为空");
@@ -326,6 +310,7 @@ public class CcdiBaseStaffImportServiceImpl implements ICcdiBaseStaffImportServi
if (StringUtils.isEmpty(addDTO.getStatus())) {
throw new RuntimeException("状态不能为空");
}
validateDeptId(addDTO.getDeptId());
// 验证身份证号格式
String idCardError = IdCardUtil.getErrorMessage(addDTO.getIdCard());
@@ -347,12 +332,11 @@ public class CcdiBaseStaffImportServiceImpl implements ICcdiBaseStaffImportServi
throw new RuntimeException("该身份证号已存在");
}
} else {
// 导入场景:如果员工ID不存在,才检查身份证号唯一性
if (!existingIds.contains(addDTO.getStaffId())) {
// 使用批量查询的结果检查身份证号唯一性
if (existingIdCards != null && existingIdCards.contains(addDTO.getIdCard())) {
throw new RuntimeException("该身份证号已存在");
}
if (existingIds.contains(addDTO.getStaffId())) {
throw new RuntimeException("该员工ID已存在");
}
if (existingIdCards != null && existingIdCards.contains(addDTO.getIdCard())) {
throw new RuntimeException("该身份证号已存在");
}
}
@@ -378,4 +362,11 @@ public class CcdiBaseStaffImportServiceImpl implements ICcdiBaseStaffImportServi
throw new RuntimeException(fieldLabel + "最多保留2位小数");
}
}
private void validateDeptId(Long deptId) {
SysDept dept = deptMapper.selectDeptById(deptId);
if (dept == null || !"0".equals(dept.getStatus()) || !"0".equals(dept.getDelFlag())) {
throw new RuntimeException(String.format("所属部门ID[%d]不存在或已停用/删除,请检查机构号", deptId));
}
}
}

View File

@@ -211,13 +211,12 @@ public class CcdiBaseStaffServiceImpl implements ICcdiBaseStaffService {
/**
* 导入员工数据
*
* @param excelList Excel实体列表
* @param isUpdateSupport 是否更新支持
* @param excelList Excel实体列表
* @return 结果
*/
@Override
@Transactional
public String importBaseStaff(List<CcdiBaseStaffExcel> excelList, Boolean isUpdateSupport) {
public String importBaseStaff(List<CcdiBaseStaffExcel> excelList) {
String taskId = UUID.randomUUID().toString();
long startTime = System.currentTimeMillis();
@@ -236,7 +235,7 @@ public class CcdiBaseStaffServiceImpl implements ICcdiBaseStaffService {
redisTemplate.opsForHash().putAll(statusKey, statusData);
redisTemplate.expire(statusKey, 7, java.util.concurrent.TimeUnit.DAYS);
importAsyncService.importBaseStaffAsync(excelList, isUpdateSupport, taskId);
importAsyncService.importBaseStaffAsync(excelList, taskId);
return taskId;
}

View File

@@ -40,6 +40,9 @@ import java.util.stream.Collectors;
public class CcdiPurchaseTransactionImportServiceImpl implements ICcdiPurchaseTransactionImportService {
private static final Logger log = LoggerFactory.getLogger(CcdiPurchaseTransactionImportServiceImpl.class);
private static final String MAIN_SHEET_NAME = "招投标主信息";
private static final String SUPPLIER_SHEET_NAME = "供应商明细";
private static final int EXCEL_DATA_START_ROW = 2;
@Resource
private CcdiPurchaseTransactionMapper transactionMapper;
@@ -62,6 +65,8 @@ public class CcdiPurchaseTransactionImportServiceImpl implements ICcdiPurchaseTr
long startTime = System.currentTimeMillis();
List<CcdiPurchaseTransactionExcel> safeMainList = mainExcelList == null ? List.of() : mainExcelList;
List<CcdiPurchaseTransactionSupplierExcel> safeSupplierList = supplierExcelList == null ? List.of() : supplierExcelList;
List<MainImportRow> indexedMainRows = buildMainImportRows(safeMainList);
List<SupplierImportRow> indexedSupplierRows = buildSupplierImportRows(safeSupplierList);
int totalCount = countImportUnits(safeMainList, safeSupplierList);
// 记录导入开始
@@ -76,17 +81,17 @@ public class CcdiPurchaseTransactionImportServiceImpl implements ICcdiPurchaseTr
Set<String> existingIds = getExistingPurchaseIds(safeMainList);
ImportLogUtils.logBatchQueryComplete(log, taskId, "采购事项ID", existingIds.size());
Map<String, List<CcdiPurchaseTransactionExcel>> mainGroupMap = safeMainList.stream()
.filter(item -> StringUtils.isNotEmpty(item.getPurchaseId()))
Map<String, List<MainImportRow>> mainGroupMap = indexedMainRows.stream()
.filter(item -> StringUtils.isNotEmpty(item.data().getPurchaseId()))
.collect(Collectors.groupingBy(
CcdiPurchaseTransactionExcel::getPurchaseId,
item -> item.data().getPurchaseId(),
LinkedHashMap::new,
Collectors.toList()
));
Map<String, List<CcdiPurchaseTransactionSupplierExcel>> supplierGroupMap = safeSupplierList.stream()
.filter(item -> StringUtils.isNotEmpty(item.getPurchaseId()))
Map<String, List<SupplierImportRow>> supplierGroupMap = indexedSupplierRows.stream()
.filter(item -> StringUtils.isNotEmpty(item.data().getPurchaseId()))
.collect(Collectors.groupingBy(
CcdiPurchaseTransactionSupplierExcel::getPurchaseId,
item -> item.data().getPurchaseId(),
LinkedHashMap::new,
Collectors.toList()
));
@@ -94,34 +99,53 @@ public class CcdiPurchaseTransactionImportServiceImpl implements ICcdiPurchaseTr
purchaseIds.addAll(mainGroupMap.keySet());
purchaseIds.addAll(supplierGroupMap.keySet());
for (CcdiPurchaseTransactionSupplierExcel supplierExcel : safeSupplierList) {
if (StringUtils.isEmpty(supplierExcel.getPurchaseId())) {
failures.add(buildFailure(null, null, "供应商明细Sheet中的采购事项ID不能为空"));
for (SupplierImportRow supplierExcel : indexedSupplierRows) {
if (StringUtils.isEmpty(supplierExcel.data().getPurchaseId())) {
failures.add(buildFailure(
null,
null,
SUPPLIER_SHEET_NAME,
String.valueOf(supplierExcel.sheetRowNum()),
"供应商明细Sheet中的采购事项ID不能为空"
));
}
}
int index = 0;
for (String purchaseId : purchaseIds) {
index++;
List<CcdiPurchaseTransactionExcel> mainRows = mainGroupMap.getOrDefault(purchaseId, List.of());
List<CcdiPurchaseTransactionSupplierExcel> supplierRows = supplierGroupMap.getOrDefault(purchaseId, List.of());
List<MainImportRow> mainRows = mainGroupMap.getOrDefault(purchaseId, List.of());
List<SupplierImportRow> supplierRows = supplierGroupMap.getOrDefault(purchaseId, List.of());
try {
if (existingIds.contains(purchaseId)) {
throw new RuntimeException(String.format("采购事项ID[%s]已存在,请勿重复导入", purchaseId));
throw buildValidationException(
MAIN_SHEET_NAME,
extractMainRowNums(mainRows),
String.format("采购事项ID[%s]已存在,请勿重复导入", purchaseId)
);
}
if (mainRows.isEmpty()) {
throw new RuntimeException(String.format("采购事项ID[%s]缺少招投标主信息", purchaseId));
throw buildValidationException(
SUPPLIER_SHEET_NAME,
extractSupplierRowNums(supplierRows),
String.format("采购事项ID[%s]缺少招投标主信息", purchaseId)
);
}
if (mainRows.size() > 1) {
throw new RuntimeException(String.format("采购事项ID[%s]在招投标主信息Sheet中重复", purchaseId));
throw buildValidationException(
MAIN_SHEET_NAME,
extractMainRowNums(mainRows),
String.format("采购事项ID[%s]在招投标主信息Sheet中重复", purchaseId)
);
}
CcdiPurchaseTransactionExcel mainExcel = mainRows.getFirst();
MainImportRow mainRow = mainRows.getFirst();
CcdiPurchaseTransactionExcel mainExcel = mainRow.data();
CcdiPurchaseTransactionAddDTO addDTO = new CcdiPurchaseTransactionAddDTO();
BeanUtils.copyProperties(mainExcel, addDTO);
validateTransactionData(addDTO);
validateTransactionData(addDTO, mainRow.sheetRowNum());
List<CcdiPurchaseTransactionSupplier> suppliers = buildSupplierEntities(purchaseId, supplierRows, userName);
CcdiPurchaseTransaction transaction = new CcdiPurchaseTransaction();
@@ -137,8 +161,16 @@ public class CcdiPurchaseTransactionImportServiceImpl implements ICcdiPurchaseTr
newTransactions.size(), failures.size());
} catch (Exception e) {
CcdiPurchaseTransactionExcel mainExcel = mainRows.isEmpty() ? null : mainRows.getFirst();
failures.add(buildFailure(mainExcel, purchaseId, e.getMessage()));
MainImportRow mainRow = mainRows.isEmpty() ? null : mainRows.getFirst();
CcdiPurchaseTransactionExcel mainExcel = mainRow == null ? null : mainRow.data();
FailureMeta failureMeta = resolveFailureMeta(e, mainRows, supplierRows);
failures.add(buildFailure(
mainExcel,
purchaseId,
failureMeta.sheetName(),
failureMeta.sheetRowNum(),
e.getMessage()
));
// 记录验证失败日志
String keyData = String.format("采购事项ID=%s, 采购类别=%s, 标的物=%s",
@@ -301,97 +333,99 @@ public class CcdiPurchaseTransactionImportServiceImpl implements ICcdiPurchaseTr
*
* @param addDTO 新增DTO
*/
private void validateTransactionData(CcdiPurchaseTransactionAddDTO addDTO) {
private void validateTransactionData(CcdiPurchaseTransactionAddDTO addDTO, int sheetRowNum) {
// 验证必填字段
if (StringUtils.isEmpty(addDTO.getPurchaseId())) {
throw new RuntimeException("采购事项ID不能为空");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "采购事项ID不能为空");
}
if (StringUtils.isEmpty(addDTO.getPurchaseCategory())) {
throw new RuntimeException("采购类别不能为空");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "采购类别不能为空");
}
if (StringUtils.isEmpty(addDTO.getSubjectName())) {
throw new RuntimeException("标的物名称不能为空");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "标的物名称不能为空");
}
if (addDTO.getPurchaseQty() == null) {
throw new RuntimeException("采购数量不能为空");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "采购数量不能为空");
}
if (addDTO.getBudgetAmount() == null) {
throw new RuntimeException("预算金额不能为空");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "预算金额不能为空");
}
if (StringUtils.isEmpty(addDTO.getPurchaseMethod())) {
throw new RuntimeException("采购方式不能为空");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "采购方式不能为空");
}
if (addDTO.getApplyDate() == null) {
throw new RuntimeException("采购申请日期不能为空");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "采购申请日期不能为空");
}
if (StringUtils.isEmpty(addDTO.getApplicantId())) {
throw new RuntimeException("申请人工号不能为空");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "申请人工号不能为空");
}
if (StringUtils.isEmpty(addDTO.getApplicantName())) {
throw new RuntimeException("申请人姓名不能为空");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "申请人姓名不能为空");
}
if (StringUtils.isEmpty(addDTO.getApplyDepartment())) {
throw new RuntimeException("申请部门不能为空");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "申请部门不能为空");
}
// 验证工号格式7位数字
if (!addDTO.getApplicantId().matches("^\\d{7}$")) {
throw new RuntimeException("申请人工号必须为7位数字");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "申请人工号必须为7位数字");
}
if (StringUtils.isNotEmpty(addDTO.getPurchaseLeaderId()) && !addDTO.getPurchaseLeaderId().matches("^\\d{7}$")) {
throw new RuntimeException("采购负责人工号必须为7位数字");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "采购负责人工号必须为7位数字");
}
// 验证金额非负
if (addDTO.getPurchaseQty().compareTo(BigDecimal.ZERO) <= 0) {
throw new RuntimeException("采购数量必须大于0");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "采购数量必须大于0");
}
if (addDTO.getBudgetAmount().compareTo(BigDecimal.ZERO) <= 0) {
throw new RuntimeException("预算金额必须大于0");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "预算金额必须大于0");
}
if (addDTO.getBidAmount() != null && addDTO.getBidAmount().compareTo(BigDecimal.ZERO) <= 0) {
throw new RuntimeException("中标金额必须大于0");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "中标金额必须大于0");
}
if (addDTO.getActualAmount() != null && addDTO.getActualAmount().compareTo(BigDecimal.ZERO) <= 0) {
throw new RuntimeException("实际采购金额必须大于0");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "实际采购金额必须大于0");
}
if (addDTO.getContractAmount() != null && addDTO.getContractAmount().compareTo(BigDecimal.ZERO) <= 0) {
throw new RuntimeException("合同金额必须大于0");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "合同金额必须大于0");
}
if (addDTO.getSettlementAmount() != null && addDTO.getSettlementAmount().compareTo(BigDecimal.ZERO) <= 0) {
throw new RuntimeException("结算金额必须大于0");
throw buildValidationException(MAIN_SHEET_NAME, List.of(sheetRowNum), "结算金额必须大于0");
}
}
private List<CcdiPurchaseTransactionSupplier> buildSupplierEntities(
String purchaseId,
List<CcdiPurchaseTransactionSupplierExcel> supplierRows,
List<SupplierImportRow> supplierRows,
String userName
) {
List<CcdiPurchaseTransactionSupplierExcel> normalizedRows = supplierRows == null
List<SupplierImportRow> normalizedRows = supplierRows == null
? List.of()
: supplierRows.stream()
.filter(Objects::nonNull)
.filter(this::hasAnySupplierValue)
.filter(item -> hasAnySupplierValue(item.data()))
.toList();
long winnerCount = normalizedRows.stream()
.filter(item -> parseIsBidWinner(item.getIsBidWinner()) == 1)
.count();
if (winnerCount > 1) {
throw new RuntimeException(String.format("采购事项ID[%s]存在多条中标供应商", purchaseId));
}
Set<String> duplicateSupplierKeys = new LinkedHashSet<>();
List<Integer> winnerRowNums = new ArrayList<>();
Map<String, Integer> supplierKeyRowMap = new LinkedHashMap<>();
List<CcdiPurchaseTransactionSupplier> result = new ArrayList<>();
for (int i = 0; i < normalizedRows.size(); i++) {
CcdiPurchaseTransactionSupplierExcel supplierRow = normalizedRows.get(i);
validateSupplierRow(supplierRow);
for (SupplierImportRow supplierImportRow : normalizedRows) {
CcdiPurchaseTransactionSupplierExcel supplierRow = supplierImportRow.data();
int isBidWinner = validateSupplierRow(supplierImportRow);
if (isBidWinner == 1) {
winnerRowNums.add(supplierImportRow.sheetRowNum());
}
String duplicateKey = StringUtils.trimToEmpty(supplierRow.getSupplierName()) + "|"
+ StringUtils.trimToEmpty(supplierRow.getSupplierUscc());
if (!duplicateSupplierKeys.add(duplicateKey)) {
throw new RuntimeException(String.format("采购事项ID[%s]存在重复供应商", purchaseId));
Integer firstRowNum = supplierKeyRowMap.putIfAbsent(duplicateKey, supplierImportRow.sheetRowNum());
if (firstRowNum != null) {
throw buildValidationException(
SUPPLIER_SHEET_NAME,
List.of(firstRowNum, supplierImportRow.sheetRowNum()),
String.format("采购事项ID[%s]存在重复供应商", purchaseId)
);
}
CcdiPurchaseTransactionSupplier supplier = new CcdiPurchaseTransactionSupplier();
@@ -401,37 +435,45 @@ public class CcdiPurchaseTransactionImportServiceImpl implements ICcdiPurchaseTr
supplier.setContactPerson(StringUtils.trimToNull(supplierRow.getContactPerson()));
supplier.setContactPhone(StringUtils.trimToNull(supplierRow.getContactPhone()));
supplier.setSupplierBankAccount(StringUtils.trimToNull(supplierRow.getSupplierBankAccount()));
supplier.setIsBidWinner(parseIsBidWinner(supplierRow.getIsBidWinner()));
supplier.setSortOrder(supplierRow.getSortOrder() == null ? i + 1 : supplierRow.getSortOrder());
supplier.setIsBidWinner(isBidWinner);
supplier.setSortOrder(supplierRow.getSortOrder() == null ? result.size() + 1 : supplierRow.getSortOrder());
supplier.setCreatedBy(userName);
supplier.setUpdatedBy(userName);
result.add(supplier);
}
if (winnerRowNums.size() > 1) {
throw buildValidationException(
SUPPLIER_SHEET_NAME,
winnerRowNums,
String.format("采购事项ID[%s]存在多条中标供应商", purchaseId)
);
}
return result;
}
private void validateSupplierRow(CcdiPurchaseTransactionSupplierExcel supplierRow) {
private int validateSupplierRow(SupplierImportRow supplierImportRow) {
CcdiPurchaseTransactionSupplierExcel supplierRow = supplierImportRow.data();
if (StringUtils.isEmpty(supplierRow.getSupplierName())) {
throw new RuntimeException("供应商名称不能为空");
throw buildValidationException(SUPPLIER_SHEET_NAME, List.of(supplierImportRow.sheetRowNum()), "供应商名称不能为空");
}
if (StringUtils.length(supplierRow.getSupplierName()) > 200) {
throw new RuntimeException("供应商名称长度不能超过200个字符");
throw buildValidationException(SUPPLIER_SHEET_NAME, List.of(supplierImportRow.sheetRowNum()), "供应商名称长度不能超过200个字符");
}
if (StringUtils.length(supplierRow.getContactPerson()) > 50) {
throw new RuntimeException("供应商联系人长度不能超过50个字符");
throw buildValidationException(SUPPLIER_SHEET_NAME, List.of(supplierImportRow.sheetRowNum()), "供应商联系人长度不能超过50个字符");
}
if (StringUtils.length(supplierRow.getSupplierBankAccount()) > 50) {
throw new RuntimeException("供应商银行账户长度不能超过50个字符");
throw buildValidationException(SUPPLIER_SHEET_NAME, List.of(supplierImportRow.sheetRowNum()), "供应商银行账户长度不能超过50个字符");
}
if (StringUtils.isNotEmpty(supplierRow.getContactPhone())
&& !supplierRow.getContactPhone().matches("^1[3-9]\\d{9}$|^0\\d{2,3}-?\\d{7,8}$")) {
throw new RuntimeException("供应商联系电话格式不正确");
throw buildValidationException(SUPPLIER_SHEET_NAME, List.of(supplierImportRow.sheetRowNum()), "供应商联系电话格式不正确");
}
if (StringUtils.isNotEmpty(supplierRow.getSupplierUscc())
&& !supplierRow.getSupplierUscc().matches("^[0-9A-HJ-NPQRTUWXY]{2}\\d{6}[0-9A-HJ-NPQRTUWXY]{10}$")) {
throw new RuntimeException("供应商统一信用代码格式不正确");
throw buildValidationException(SUPPLIER_SHEET_NAME, List.of(supplierImportRow.sheetRowNum()), "供应商统一信用代码格式不正确");
}
parseIsBidWinner(supplierRow.getIsBidWinner());
return parseIsBidWinner(supplierRow.getIsBidWinner(), supplierImportRow.sheetRowNum());
}
private boolean hasAnySupplierValue(CcdiPurchaseTransactionSupplierExcel supplierRow) {
@@ -445,7 +487,7 @@ public class CcdiPurchaseTransactionImportServiceImpl implements ICcdiPurchaseTr
|| supplierRow.getSortOrder() != null;
}
private int parseIsBidWinner(String rawValue) {
private int parseIsBidWinner(String rawValue, Integer sheetRowNum) {
if (StringUtils.isEmpty(rawValue)) {
return 0;
}
@@ -458,7 +500,11 @@ public class CcdiPurchaseTransactionImportServiceImpl implements ICcdiPurchaseTr
|| "FALSE".equalsIgnoreCase(normalized)) {
return 0;
}
throw new RuntimeException("是否中标仅支持填写“是/否”或“1/0”");
throw buildValidationException(
SUPPLIER_SHEET_NAME,
sheetRowNum == null ? List.of() : List.of(sheetRowNum),
"是否中标仅支持填写“是/否”或“1/0”"
);
}
private void fillWinnerSummary(
@@ -487,12 +533,16 @@ public class CcdiPurchaseTransactionImportServiceImpl implements ICcdiPurchaseTr
private PurchaseTransactionImportFailureVO buildFailure(
CcdiPurchaseTransactionExcel mainExcel,
String purchaseId,
String sheetName,
String sheetRowNum,
String errorMessage
) {
PurchaseTransactionImportFailureVO failure = new PurchaseTransactionImportFailureVO();
if (mainExcel != null) {
BeanUtils.copyProperties(mainExcel, failure);
}
failure.setSheetName(sheetName);
failure.setSheetRowNum(sheetRowNum);
if (StringUtils.isNotEmpty(purchaseId)) {
failure.setPurchaseId(purchaseId);
}
@@ -519,4 +569,87 @@ public class CcdiPurchaseTransactionImportServiceImpl implements ICcdiPurchaseTr
);
return purchaseIds.size();
}
private List<MainImportRow> buildMainImportRows(List<CcdiPurchaseTransactionExcel> mainExcelList) {
List<MainImportRow> rows = new ArrayList<>();
for (int i = 0; i < mainExcelList.size(); i++) {
rows.add(new MainImportRow(mainExcelList.get(i), i + EXCEL_DATA_START_ROW));
}
return rows;
}
private List<SupplierImportRow> buildSupplierImportRows(List<CcdiPurchaseTransactionSupplierExcel> supplierExcelList) {
List<SupplierImportRow> rows = new ArrayList<>();
for (int i = 0; i < supplierExcelList.size(); i++) {
rows.add(new SupplierImportRow(supplierExcelList.get(i), i + EXCEL_DATA_START_ROW));
}
return rows;
}
private List<Integer> extractMainRowNums(List<MainImportRow> rows) {
return rows.stream().map(MainImportRow::sheetRowNum).toList();
}
private List<Integer> extractSupplierRowNums(List<SupplierImportRow> rows) {
return rows.stream().map(SupplierImportRow::sheetRowNum).toList();
}
private ImportValidationException buildValidationException(String sheetName, List<Integer> rowNums, String message) {
return new ImportValidationException(sheetName, formatSheetRowNum(rowNums), message);
}
private FailureMeta resolveFailureMeta(
Exception exception,
List<MainImportRow> mainRows,
List<SupplierImportRow> supplierRows
) {
if (exception instanceof ImportValidationException validationException) {
return new FailureMeta(validationException.getSheetName(), validationException.getSheetRowNum());
}
if (!mainRows.isEmpty()) {
return new FailureMeta(MAIN_SHEET_NAME, formatSheetRowNum(extractMainRowNums(mainRows)));
}
if (!supplierRows.isEmpty()) {
return new FailureMeta(SUPPLIER_SHEET_NAME, formatSheetRowNum(extractSupplierRowNums(supplierRows)));
}
return new FailureMeta("", "");
}
private String formatSheetRowNum(List<Integer> rowNums) {
if (rowNums == null || rowNums.isEmpty()) {
return "";
}
return rowNums.stream()
.filter(Objects::nonNull)
.distinct()
.sorted()
.map(String::valueOf)
.collect(Collectors.joining(""));
}
private record MainImportRow(CcdiPurchaseTransactionExcel data, int sheetRowNum) {}
private record SupplierImportRow(CcdiPurchaseTransactionSupplierExcel data, int sheetRowNum) {}
private record FailureMeta(String sheetName, String sheetRowNum) {}
private static class ImportValidationException extends RuntimeException {
private final String sheetName;
private final String sheetRowNum;
private ImportValidationException(String sheetName, String sheetRowNum, String message) {
super(message);
this.sheetName = sheetName;
this.sheetRowNum = sheetRowNum;
}
public String getSheetName() {
return sheetName;
}
public String getSheetRowNum() {
return sheetRowNum;
}
}
}

View File

@@ -41,6 +41,8 @@ import java.util.stream.Collectors;
public class CcdiStaffFmyRelationImportServiceImpl implements ICcdiStaffFmyRelationImportService {
private static final Logger log = LoggerFactory.getLogger(CcdiStaffFmyRelationImportServiceImpl.class);
private static final String SHEET_NAME = "员工亲属关系信息";
private static final int EXCEL_DATA_START_ROW = 2;
@Resource
private CcdiStaffFmyRelationMapper relationMapper;
@@ -168,6 +170,8 @@ public class CcdiStaffFmyRelationImportServiceImpl implements ICcdiStaffFmyRelat
} catch (Exception e) {
StaffFmyRelationImportFailureVO failure = new StaffFmyRelationImportFailureVO();
BeanUtils.copyProperties(excel, failure);
failure.setSheetName(SHEET_NAME);
failure.setRowNum(i + EXCEL_DATA_START_ROW);
failure.setErrorMessage(e.getMessage());
failures.add(failure);

View File

@@ -96,8 +96,12 @@ class CcdiAssetInfoControllerTest {
@Test
void getImportFailures_shouldReturnPagedRows() {
AssetImportFailureVO failure1 = new AssetImportFailureVO();
failure1.setSheetName("亲属资产信息");
failure1.setRowNum(2);
failure1.setPersonId("A1");
AssetImportFailureVO failure2 = new AssetImportFailureVO();
failure2.setSheetName("亲属资产信息");
failure2.setRowNum(3);
failure2.setPersonId("A2");
when(assetInfoImportService.getImportFailures("task-3")).thenReturn(List.of(failure1, failure2));
@@ -105,7 +109,10 @@ class CcdiAssetInfoControllerTest {
assertEquals(2, result.getTotal());
assertEquals(1, result.getRows().size());
assertEquals("A2", ((AssetImportFailureVO) result.getRows().get(0)).getPersonId());
AssetImportFailureVO row = (AssetImportFailureVO) result.getRows().get(0);
assertEquals("亲属资产信息", row.getSheetName());
assertEquals(3, row.getRowNum());
assertEquals("A2", row.getPersonId());
}
@Test

View File

@@ -93,10 +93,39 @@ class CcdiBaseStaffAssetImportServiceImplTest {
ArgumentCaptor<Object> failureCaptor = ArgumentCaptor.forClass(Object.class);
verify(valueOperations).set(eq("import:baseStaffAsset:task-2:failures"), failureCaptor.capture(), eq(7L), eq(TimeUnit.DAYS));
BaseStaffAssetImportFailureVO failure = (BaseStaffAssetImportFailureVO) ((List<?>) failureCaptor.getValue()).get(0);
assertEquals("员工资产信息", failure.getSheetName());
assertEquals(2, failure.getRowNum());
assertEquals("320101199201010022", failure.getPersonId());
assertTrue(failure.getErrorMessage().contains("员工资产导入仅支持员工本人证件号"));
}
@Test
void importAssetInfoAsync_shouldFailWhenAssetAlreadyExists() {
CcdiBaseStaffAssetInfoExcel excel = buildExcel("320101199001010011", "房产");
when(redisTemplate.opsForHash()).thenReturn(hashOperations);
when(redisTemplate.opsForValue()).thenReturn(valueOperations);
when(assetInfoMapper.selectOwnerCandidatesByBaseStaffIdCards(List.of("320101199001010011")))
.thenReturn(List.of(owner("320101199001010011", "320101199001010011")));
CcdiAssetInfo existing = new CcdiAssetInfo();
existing.setFamilyId("320101199001010011");
existing.setPersonId("320101199001010011");
existing.setAssetMainType("房产");
existing.setAssetSubType("房产小类");
existing.setAssetName("房产名称");
when(assetInfoMapper.selectList(any())).thenReturn(List.of(existing));
service.importAssetInfoAsync(List.of(excel), "task-duplicate", "tester");
verify(assetInfoMapper, never()).insertBatch(any());
ArgumentCaptor<Object> failureCaptor = ArgumentCaptor.forClass(Object.class);
verify(valueOperations).set(eq("import:baseStaffAsset:task-duplicate:failures"), failureCaptor.capture(), eq(7L), eq(TimeUnit.DAYS));
BaseStaffAssetImportFailureVO failure = (BaseStaffAssetImportFailureVO) ((List<?>) failureCaptor.getValue()).get(0);
assertEquals("员工资产信息", failure.getSheetName());
assertEquals(2, failure.getRowNum());
assertTrue(failure.getErrorMessage().contains("资产记录已存在"));
}
@Test
void getImportStatusAndFailures_shouldUseBaseStaffAssetPrefixes() {
when(redisTemplate.opsForHash()).thenReturn(hashOperations);

View File

@@ -1,8 +1,14 @@
package com.ruoyi.info.collection.service;
import com.ruoyi.common.core.domain.entity.SysDept;
import com.ruoyi.info.collection.domain.dto.CcdiBaseStaffAddDTO;
import com.ruoyi.info.collection.service.impl.CcdiBaseStaffImportServiceImpl;
import com.ruoyi.system.mapper.SysDeptMapper;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.math.BigDecimal;
import java.util.Collections;
@@ -11,60 +17,109 @@ import java.util.Set;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class CcdiBaseStaffImportServiceImplTest {
private final CcdiBaseStaffImportServiceImpl service = new CcdiBaseStaffImportServiceImpl();
@InjectMocks
private CcdiBaseStaffImportServiceImpl service;
@Mock
private SysDeptMapper deptMapper;
@Test
void validateStaffData_shouldAllowEmptyAnnualIncome() {
assertDoesNotThrow(() -> service.validateStaffData(buildDto(null), false, Collections.emptySet(), Collections.emptySet()));
mockNormalDept();
assertDoesNotThrow(() -> service.validateStaffData(buildDto(null), Collections.emptySet(), Collections.emptySet()));
}
@Test
void validateStaffData_shouldAllowZeroAndTwoDecimalAnnualIncome() {
assertDoesNotThrow(() -> service.validateStaffData(buildDto(new BigDecimal("0.00")), false, Collections.emptySet(), Collections.emptySet()));
assertDoesNotThrow(() -> service.validateStaffData(buildDto(new BigDecimal("12345.67")), false, Collections.emptySet(), Collections.emptySet()));
mockNormalDept();
assertDoesNotThrow(() -> service.validateStaffData(buildDto(new BigDecimal("0.00")), Collections.emptySet(), Collections.emptySet()));
assertDoesNotThrow(() -> service.validateStaffData(buildDto(new BigDecimal("12345.67")), Collections.emptySet(), Collections.emptySet()));
}
@Test
void validateStaffData_shouldAllowPartyMemberValuesZeroAndOne() {
mockNormalDept();
CcdiBaseStaffAddDTO nonPartyMember = buildDto(null);
nonPartyMember.setPartyMember(0);
CcdiBaseStaffAddDTO partyMember = buildDto(null);
partyMember.setPartyMember(1);
assertDoesNotThrow(() -> service.validateStaffData(nonPartyMember, false, Collections.emptySet(), Collections.emptySet()));
assertDoesNotThrow(() -> service.validateStaffData(partyMember, false, Collections.emptySet(), Collections.emptySet()));
assertDoesNotThrow(() -> service.validateStaffData(nonPartyMember, Collections.emptySet(), Collections.emptySet()));
assertDoesNotThrow(() -> service.validateStaffData(partyMember, Collections.emptySet(), Collections.emptySet()));
}
@Test
void validateStaffData_shouldRejectInvalidPartyMemberValue() {
mockNormalDept();
CcdiBaseStaffAddDTO dto = buildDto(null);
dto.setPartyMember(2);
RuntimeException exception = assertThrows(RuntimeException.class,
() -> service.validateStaffData(dto, false, Set.of(), Set.of()));
() -> service.validateStaffData(dto, Set.of(), Set.of()));
assertEquals("是否党员只能填写'0'或'1'", exception.getMessage());
}
@Test
void validateStaffData_shouldRejectNegativeAnnualIncome() {
mockNormalDept();
RuntimeException exception = assertThrows(RuntimeException.class,
() -> service.validateStaffData(buildDto(new BigDecimal("-1.00")), false, Set.of(), Set.of()));
() -> service.validateStaffData(buildDto(new BigDecimal("-1.00")), Set.of(), Set.of()));
assertEquals("年收入不能为负数", exception.getMessage());
}
@Test
void validateStaffData_shouldRejectAnnualIncomeWithMoreThanTwoDecimals() {
mockNormalDept();
RuntimeException exception = assertThrows(RuntimeException.class,
() -> service.validateStaffData(buildDto(new BigDecimal("12.345")), false, Set.of(), Set.of()));
() -> service.validateStaffData(buildDto(new BigDecimal("12.345")), Set.of(), Set.of()));
assertEquals("年收入最多保留2位小数", exception.getMessage());
}
@Test
void validateStaffData_shouldAllowWhenDeptIsNormalAndNotDeleted() {
mockNormalDept();
assertDoesNotThrow(() -> service.validateStaffData(buildDto(null), Set.of(), Set.of()));
}
@Test
void validateStaffData_shouldRejectWhenDeptDoesNotExist() {
when(deptMapper.selectDeptById(10L)).thenReturn(null);
RuntimeException exception = assertThrows(RuntimeException.class,
() -> service.validateStaffData(buildDto(null), Set.of(), Set.of()));
assertEquals("所属部门ID[10]不存在或已停用/删除,请检查机构号", exception.getMessage());
}
@Test
void validateStaffData_shouldRejectWhenDeptIsDisabled() {
when(deptMapper.selectDeptById(10L)).thenReturn(buildDept("1", "0"));
RuntimeException exception = assertThrows(RuntimeException.class,
() -> service.validateStaffData(buildDto(null), Set.of(), Set.of()));
assertEquals("所属部门ID[10]不存在或已停用/删除,请检查机构号", exception.getMessage());
}
@Test
void validateStaffData_shouldRejectWhenDeptIsDeleted() {
when(deptMapper.selectDeptById(10L)).thenReturn(buildDept("0", "2"));
RuntimeException exception = assertThrows(RuntimeException.class,
() -> service.validateStaffData(buildDto(null), Set.of(), Set.of()));
assertEquals("所属部门ID[10]不存在或已停用/删除,请检查机构号", exception.getMessage());
}
private CcdiBaseStaffAddDTO buildDto(BigDecimal annualIncome) {
CcdiBaseStaffAddDTO dto = new CcdiBaseStaffAddDTO();
dto.setName("张三");
@@ -77,4 +132,17 @@ class CcdiBaseStaffImportServiceImplTest {
dto.setAnnualIncome(annualIncome);
return dto;
}
private SysDept buildDept(String status, String delFlag) {
SysDept dept = new SysDept();
dept.setDeptId(10L);
dept.setDeptName("测试部门");
dept.setStatus(status);
dept.setDelFlag(delFlag);
return dept;
}
private void mockNormalDept() {
lenient().when(deptMapper.selectDeptById(10L)).thenReturn(buildDept("0", "0"));
}
}