完成员工与亲属资产导入后端拆分

This commit is contained in:
wkc
2026-03-13 16:29:04 +08:00
parent ee31f74aef
commit bda89202ba
17 changed files with 841 additions and 64 deletions

View File

@@ -28,12 +28,12 @@ import java.util.ArrayList;
import java.util.List;
/**
* 资产信息导入Controller
* 亲属资产信息导入Controller
*
* @author ruoyi
* @date 2026-03-12
*/
@Tag(name = "资产信息导入管理")
@Tag(name = "亲属资产信息导入管理")
@RestController
@RequestMapping("/ccdi/assetInfo")
public class CcdiAssetInfoController extends BaseController {
@@ -44,18 +44,18 @@ public class CcdiAssetInfoController extends BaseController {
/**
* 下载导入模板
*/
@Operation(summary = "下载资产导入模板")
@Operation(summary = "下载亲属资产导入模板")
@PostMapping("/importTemplate")
public void importTemplate(HttpServletResponse response) {
EasyExcelUtil.importTemplateWithDictDropdown(response, CcdiAssetInfoExcel.class, "资产信息");
EasyExcelUtil.importTemplateWithDictDropdown(response, CcdiAssetInfoExcel.class, "亲属资产信息");
}
/**
* 导入资产信息
* 导入亲属资产信息
*/
@Operation(summary = "导入资产信息")
@PreAuthorize("@ss.hasAnyPermi('ccdi:employee:import,ccdi:staffFmyRelation:import')")
@Log(title = "资产信息", businessType = BusinessType.IMPORT)
@Operation(summary = "导入亲属资产信息")
@PreAuthorize("@ss.hasPermi('ccdi:staffFmyRelation:import')")
@Log(title = "亲属资产信息", businessType = BusinessType.IMPORT)
@PostMapping("/importData")
public AjaxResult importData(MultipartFile file) throws Exception {
List<CcdiAssetInfoExcel> list = EasyExcelUtil.importExcel(file.getInputStream(), CcdiAssetInfoExcel.class);
@@ -74,8 +74,8 @@ public class CcdiAssetInfoController extends BaseController {
/**
* 查询导入状态
*/
@Operation(summary = "查询资产导入状态")
@PreAuthorize("@ss.hasAnyPermi('ccdi:employee:import,ccdi:staffFmyRelation:import')")
@Operation(summary = "查询亲属资产导入状态")
@PreAuthorize("@ss.hasPermi('ccdi:staffFmyRelation:import')")
@GetMapping("/importStatus/{taskId}")
public AjaxResult getImportStatus(@PathVariable String taskId) {
return success(assetInfoImportService.getImportStatus(taskId));
@@ -84,8 +84,8 @@ public class CcdiAssetInfoController extends BaseController {
/**
* 查询导入失败记录
*/
@Operation(summary = "查询资产导入失败记录")
@PreAuthorize("@ss.hasAnyPermi('ccdi:employee:import,ccdi:staffFmyRelation:import')")
@Operation(summary = "查询亲属资产导入失败记录")
@PreAuthorize("@ss.hasPermi('ccdi:staffFmyRelation:import')")
@GetMapping("/importFailures/{taskId}")
public TableDataInfo getImportFailures(
@PathVariable String taskId,

View File

@@ -0,0 +1,101 @@
package com.ruoyi.info.collection.controller;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.info.collection.domain.excel.CcdiBaseStaffAssetInfoExcel;
import com.ruoyi.info.collection.domain.vo.BaseStaffAssetImportFailureVO;
import com.ruoyi.info.collection.domain.vo.ImportResultVO;
import com.ruoyi.info.collection.service.ICcdiBaseStaffAssetImportService;
import com.ruoyi.info.collection.utils.EasyExcelUtil;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.util.ArrayList;
import java.util.List;
/**
* 员工资产信息导入Controller
*
* @author ruoyi
* @date 2026-03-13
*/
@Tag(name = "员工资产信息导入管理")
@RestController
@RequestMapping("/ccdi/baseStaff/asset")
public class CcdiBaseStaffAssetImportController extends BaseController {
@Resource
private ICcdiBaseStaffAssetImportService baseStaffAssetImportService;
/**
* 下载导入模板
*/
@Operation(summary = "下载员工资产导入模板")
@PostMapping("/importTemplate")
public void importTemplate(HttpServletResponse response) {
EasyExcelUtil.importTemplateWithDictDropdown(response, CcdiBaseStaffAssetInfoExcel.class, "员工资产信息");
}
/**
* 导入员工资产信息
*/
@Operation(summary = "导入员工资产信息")
@PreAuthorize("@ss.hasPermi('ccdi:employee:import')")
@Log(title = "员工资产信息", businessType = BusinessType.IMPORT)
@PostMapping("/importData")
public AjaxResult importData(MultipartFile file) throws Exception {
List<CcdiBaseStaffAssetInfoExcel> list = EasyExcelUtil.importExcel(file.getInputStream(), CcdiBaseStaffAssetInfoExcel.class);
if (list == null || list.isEmpty()) {
return warn("至少需要一条数据");
}
String taskId = baseStaffAssetImportService.importAssetInfo(list);
ImportResultVO result = new ImportResultVO();
result.setTaskId(taskId);
result.setStatus("PROCESSING");
result.setMessage("导入任务已提交,正在后台处理");
return AjaxResult.success("导入任务已提交,正在后台处理", result);
}
/**
* 查询导入状态
*/
@Operation(summary = "查询员工资产导入状态")
@PreAuthorize("@ss.hasPermi('ccdi:employee:import')")
@GetMapping("/importStatus/{taskId}")
public AjaxResult getImportStatus(@PathVariable String taskId) {
return success(baseStaffAssetImportService.getImportStatus(taskId));
}
/**
* 查询导入失败记录
*/
@Operation(summary = "查询员工资产导入失败记录")
@PreAuthorize("@ss.hasPermi('ccdi:employee:import')")
@GetMapping("/importFailures/{taskId}")
public TableDataInfo getImportFailures(
@PathVariable String taskId,
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize) {
List<BaseStaffAssetImportFailureVO> failures = baseStaffAssetImportService.getImportFailures(taskId);
int fromIndex = (pageNum - 1) * pageSize;
int toIndex = Math.min(fromIndex + pageSize, failures.size());
if (fromIndex >= failures.size()) {
return getDataTable(new ArrayList<>(), failures.size());
}
return getDataTable(failures.subList(fromIndex, toIndex), failures.size());
}
}

View File

@@ -24,8 +24,8 @@ public class CcdiAssetInfoExcel implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/** 资产实际持有人证件号 */
@ExcelProperty(value = "资产实际持有人证件号*", index = 0)
/** 亲属证件号 */
@ExcelProperty(value = "亲属证件号*", index = 0)
@ColumnWidth(22)
@Required
@TextFormat

View File

@@ -0,0 +1,89 @@
package com.ruoyi.info.collection.domain.excel;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import com.ruoyi.common.annotation.DictDropdown;
import com.ruoyi.common.annotation.Required;
import com.ruoyi.common.annotation.TextFormat;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
* 员工资产信息Excel导入导出对象
*
* @author ruoyi
* @date 2026-03-13
*/
@Data
public class CcdiBaseStaffAssetInfoExcel implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/** 员工身份证号 */
@ExcelProperty(value = "员工身份证号*", index = 0)
@ColumnWidth(22)
@Required
@TextFormat
private String personId;
/** 资产大类 */
@ExcelProperty(value = "资产大类*", index = 1)
@ColumnWidth(16)
@Required
private String assetMainType;
/** 资产小类 */
@ExcelProperty(value = "资产小类*", index = 2)
@ColumnWidth(18)
@Required
private String assetSubType;
/** 资产名称 */
@ExcelProperty(value = "资产名称*", index = 3)
@ColumnWidth(24)
@Required
private String assetName;
/** 产权占比 */
@ExcelProperty(value = "产权占比", index = 4)
@ColumnWidth(12)
private BigDecimal ownershipRatio;
/** 购买/评估日期 */
@ExcelProperty(value = "购买/评估日期", index = 5)
@ColumnWidth(16)
private Date purchaseEvalDate;
/** 资产原值 */
@ExcelProperty(value = "资产原值", index = 6)
@ColumnWidth(16)
private BigDecimal originalValue;
/** 当前估值 */
@ExcelProperty(value = "当前估值*", index = 7)
@ColumnWidth(16)
@Required
private BigDecimal currentValue;
/** 估值截止日期 */
@ExcelProperty(value = "估值截止日期", index = 8)
@ColumnWidth(16)
private Date valuationDate;
/** 资产状态 */
@ExcelProperty(value = "资产状态*", index = 9)
@ColumnWidth(14)
@DictDropdown(dictType = "ccdi_asset_status")
@Required
private String assetStatus;
/** 备注 */
@ExcelProperty(value = "备注", index = 10)
@ColumnWidth(28)
private String remarks;
}

View File

@@ -15,8 +15,8 @@ import java.math.BigDecimal;
@Schema(description = "亲属资产信息导入失败记录")
public class AssetImportFailureVO {
/** 关系人证件号 */
@Schema(description = "关系人证件号")
/** 亲属证件号 */
@Schema(description = "亲属证件号")
private String personId;
/** 资产大类 */

View File

@@ -0,0 +1,49 @@
package com.ruoyi.info.collection.domain.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
/**
* 员工资产信息导入失败记录VO
*
* @author ruoyi
* @date 2026-03-13
*/
@Data
@Schema(description = "员工资产信息导入失败记录")
public class BaseStaffAssetImportFailureVO {
/** 员工身份证号 */
@Schema(description = "员工身份证号")
private String personId;
/** 资产大类 */
@Schema(description = "资产大类")
private String assetMainType;
/** 资产小类 */
@Schema(description = "资产小类")
private String assetSubType;
/** 资产名称 */
@Schema(description = "资产名称")
private String assetName;
/** 产权占比 */
@Schema(description = "产权占比")
private BigDecimal ownershipRatio;
/** 当前估值 */
@Schema(description = "当前估值")
private BigDecimal currentValue;
/** 资产状态 */
@Schema(description = "资产状态")
private String assetStatus;
/** 错误信息 */
@Schema(description = "错误信息")
private String errorMessage;
}

View File

@@ -75,14 +75,6 @@ public interface CcdiAssetInfoMapper extends BaseMapper<CcdiAssetInfo> {
*/
int insertBatch(@Param("list") List<CcdiAssetInfo> list);
/**
* 按资产实际持有人证件号查询员工本人归属候选
*
* @param personIds 资产实际持有人证件号列表
* @return 归属映射
*/
List<Map<String, String>> selectOwnerCandidatesByPersonIds(@Param("personIds") List<String> personIds);
/**
* 按关系人证件号查询归属员工候选
*
@@ -90,4 +82,12 @@ public interface CcdiAssetInfoMapper extends BaseMapper<CcdiAssetInfo> {
* @return 归属映射
*/
List<Map<String, String>> selectOwnerCandidatesByRelationCertNos(@Param("relationCertNos") List<String> relationCertNos);
/**
* 按员工身份证号查询员工本人归属候选
*
* @param idCards 员工身份证号列表
* @return 归属映射
*/
List<Map<String, String>> selectOwnerCandidatesByBaseStaffIdCards(@Param("idCards") List<String> idCards);
}

View File

@@ -7,7 +7,7 @@ import com.ruoyi.info.collection.domain.vo.ImportStatusVO;
import java.util.List;
/**
* 员工资产信息异步导入 服务层
* 亲属资产信息异步导入 服务层
*
* @author ruoyi
* @date 2026-03-12
@@ -23,7 +23,7 @@ public interface ICcdiAssetInfoImportService {
String importAssetInfo(List<CcdiAssetInfoExcel> excelList);
/**
* 异步导入员工资产数据
* 异步导入亲属资产数据
*
* @param excelList Excel实体列表
* @param taskId 任务ID

View File

@@ -0,0 +1,49 @@
package com.ruoyi.info.collection.service;
import com.ruoyi.info.collection.domain.excel.CcdiBaseStaffAssetInfoExcel;
import com.ruoyi.info.collection.domain.vo.BaseStaffAssetImportFailureVO;
import com.ruoyi.info.collection.domain.vo.ImportStatusVO;
import java.util.List;
/**
* 员工资产信息异步导入 服务层
*
* @author ruoyi
* @date 2026-03-13
*/
public interface ICcdiBaseStaffAssetImportService {
/**
* 启动异步导入任务
*
* @param excelList Excel实体列表
* @return 任务ID
*/
String importAssetInfo(List<CcdiBaseStaffAssetInfoExcel> excelList);
/**
* 异步导入员工资产数据
*
* @param excelList Excel实体列表
* @param taskId 任务ID
* @param userName 用户名
*/
void importAssetInfoAsync(List<CcdiBaseStaffAssetInfoExcel> excelList, String taskId, String userName);
/**
* 查询导入状态
*
* @param taskId 任务ID
* @return 导入状态
*/
ImportStatusVO getImportStatus(String taskId);
/**
* 查询导入失败记录
*
* @param taskId 任务ID
* @return 失败记录列表
*/
List<BaseStaffAssetImportFailureVO> getImportFailures(String taskId);
}

View File

@@ -27,7 +27,6 @@ import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* 亲属资产信息异步导入服务层处理
@@ -97,10 +96,10 @@ public class CcdiAssetInfoImportServiceImpl implements ICcdiAssetInfoImportServi
validateExcel(excel);
Set<String> familyIds = ownerMap.get(excel.getPersonId());
if (familyIds == null || familyIds.isEmpty()) {
throw new RuntimeException("未找到资产归属员工");
throw new RuntimeException("未找到亲属资产归属员工");
}
if (familyIds.size() > 1) {
throw new RuntimeException("资产归属员工不唯一");
throw new RuntimeException("亲属资产归属员工不唯一");
}
CcdiAssetInfo assetInfo = new CcdiAssetInfo();
@@ -167,16 +166,7 @@ public class CcdiAssetInfoImportServiceImpl implements ICcdiAssetInfoImportServi
if (personIds == null || personIds.isEmpty()) {
return result;
}
List<Map<String, String>> selfMappings = assetInfoMapper.selectOwnerCandidatesByPersonIds(personIds);
mergeOwnerMappings(result, selfMappings);
Set<String> selfMatchedIds = result.keySet();
List<String> relationPersonIds = personIds.stream()
.filter(personId -> !selfMatchedIds.contains(personId))
.toList();
if (!relationPersonIds.isEmpty()) {
mergeOwnerMappings(result, assetInfoMapper.selectOwnerCandidatesByRelationCertNos(relationPersonIds));
}
mergeOwnerMappings(result, assetInfoMapper.selectOwnerCandidatesByRelationCertNos(personIds));
return result;
}
@@ -196,7 +186,7 @@ public class CcdiAssetInfoImportServiceImpl implements ICcdiAssetInfoImportServi
private void validateExcel(CcdiAssetInfoExcel excel) {
if (StringUtils.isEmpty(excel.getPersonId())) {
throw new RuntimeException("资产实际持有人证件号不能为空");
throw new RuntimeException("亲属证件号不能为空");
}
if (StringUtils.isEmpty(excel.getAssetMainType())) {
throw new RuntimeException("资产大类不能为空");

View File

@@ -0,0 +1,226 @@
package com.ruoyi.info.collection.service.impl;
import com.alibaba.fastjson2.JSON;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.info.collection.domain.CcdiAssetInfo;
import com.ruoyi.info.collection.domain.excel.CcdiBaseStaffAssetInfoExcel;
import com.ruoyi.info.collection.domain.vo.BaseStaffAssetImportFailureVO;
import com.ruoyi.info.collection.domain.vo.ImportResult;
import com.ruoyi.info.collection.domain.vo.ImportStatusVO;
import com.ruoyi.info.collection.mapper.CcdiAssetInfoMapper;
import com.ruoyi.info.collection.service.ICcdiBaseStaffAssetImportService;
import jakarta.annotation.Resource;
import org.springframework.beans.BeanUtils;
import org.springframework.context.annotation.Lazy;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* 员工资产信息异步导入服务层处理
*
* @author ruoyi
* @date 2026-03-13
*/
@Service
@EnableAsync
public class CcdiBaseStaffAssetImportServiceImpl implements ICcdiBaseStaffAssetImportService {
private static final String STATUS_KEY_PREFIX = "import:baseStaffAsset:";
@Resource
private CcdiAssetInfoMapper assetInfoMapper;
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Lazy
@Resource
private ICcdiBaseStaffAssetImportService baseStaffAssetImportService;
@Override
@Transactional
public String importAssetInfo(List<CcdiBaseStaffAssetInfoExcel> excelList) {
if (excelList == null || excelList.isEmpty()) {
throw new RuntimeException("至少需要一条数据");
}
String taskId = UUID.randomUUID().toString();
Map<String, Object> statusData = new HashMap<>();
statusData.put("taskId", taskId);
statusData.put("status", "PROCESSING");
statusData.put("totalCount", excelList.size());
statusData.put("successCount", 0);
statusData.put("failureCount", 0);
statusData.put("progress", 0);
statusData.put("startTime", System.currentTimeMillis());
statusData.put("message", "正在处理...");
String statusKey = STATUS_KEY_PREFIX + taskId;
redisTemplate.opsForHash().putAll(statusKey, statusData);
redisTemplate.expire(statusKey, 7, TimeUnit.DAYS);
baseStaffAssetImportService.importAssetInfoAsync(excelList, taskId, currentUserName());
return taskId;
}
@Override
@Async
@Transactional
public void importAssetInfoAsync(List<CcdiBaseStaffAssetInfoExcel> excelList, String taskId, String userName) {
List<CcdiAssetInfo> successList = new ArrayList<>();
List<BaseStaffAssetImportFailureVO> failures = new ArrayList<>();
List<String> personIds = excelList.stream()
.map(CcdiBaseStaffAssetInfoExcel::getPersonId)
.filter(StringUtils::isNotEmpty)
.distinct()
.toList();
Map<String, Set<String>> ownerMap = buildOwnerMap(personIds);
for (CcdiBaseStaffAssetInfoExcel excel : excelList) {
try {
validateExcel(excel);
Set<String> familyIds = ownerMap.get(excel.getPersonId());
if (familyIds == null || familyIds.isEmpty()) {
throw new RuntimeException("员工资产导入仅支持员工本人证件号");
}
CcdiAssetInfo assetInfo = new CcdiAssetInfo();
BeanUtils.copyProperties(excel, assetInfo);
assetInfo.setFamilyId(excel.getPersonId());
assetInfo.setPersonId(excel.getPersonId());
assetInfo.setCreateBy(userName);
assetInfo.setUpdateBy(userName);
successList.add(assetInfo);
} catch (Exception e) {
BaseStaffAssetImportFailureVO failureVO = new BaseStaffAssetImportFailureVO();
BeanUtils.copyProperties(excel, failureVO);
failureVO.setErrorMessage(e.getMessage());
failures.add(failureVO);
}
}
if (!successList.isEmpty()) {
assetInfoMapper.insertBatch(successList);
}
if (!failures.isEmpty()) {
redisTemplate.opsForValue().set(STATUS_KEY_PREFIX + taskId + ":failures", failures, 7, TimeUnit.DAYS);
}
ImportResult result = new ImportResult();
result.setTotalCount(excelList.size());
result.setSuccessCount(successList.size());
result.setFailureCount(failures.size());
updateImportStatus(taskId, failures.isEmpty() ? "SUCCESS" : "PARTIAL_SUCCESS", result);
}
@Override
public ImportStatusVO getImportStatus(String taskId) {
String key = STATUS_KEY_PREFIX + taskId;
if (Boolean.FALSE.equals(redisTemplate.hasKey(key))) {
throw new RuntimeException("任务不存在或已过期");
}
Map<Object, Object> statusMap = redisTemplate.opsForHash().entries(key);
ImportStatusVO statusVO = new ImportStatusVO();
statusVO.setTaskId((String) statusMap.get("taskId"));
statusVO.setStatus((String) statusMap.get("status"));
statusVO.setTotalCount((Integer) statusMap.get("totalCount"));
statusVO.setSuccessCount((Integer) statusMap.get("successCount"));
statusVO.setFailureCount((Integer) statusMap.get("failureCount"));
statusVO.setProgress((Integer) statusMap.get("progress"));
statusVO.setStartTime((Long) statusMap.get("startTime"));
statusVO.setEndTime((Long) statusMap.get("endTime"));
statusVO.setMessage((String) statusMap.get("message"));
return statusVO;
}
@Override
public List<BaseStaffAssetImportFailureVO> getImportFailures(String taskId) {
Object failuresObj = redisTemplate.opsForValue().get(STATUS_KEY_PREFIX + taskId + ":failures");
if (failuresObj == null) {
return List.of();
}
return JSON.parseArray(JSON.toJSONString(failuresObj), BaseStaffAssetImportFailureVO.class);
}
private Map<String, Set<String>> buildOwnerMap(List<String> personIds) {
Map<String, Set<String>> result = new LinkedHashMap<>();
if (personIds == null || personIds.isEmpty()) {
return result;
}
mergeOwnerMappings(result, assetInfoMapper.selectOwnerCandidatesByBaseStaffIdCards(personIds));
return result;
}
private void mergeOwnerMappings(Map<String, Set<String>> result, List<Map<String, String>> mappings) {
if (mappings == null) {
return;
}
for (Map<String, String> mapping : mappings) {
String personId = mapping.get("personId");
String familyId = mapping.get("familyId");
if (StringUtils.isEmpty(personId) || StringUtils.isEmpty(familyId)) {
continue;
}
result.computeIfAbsent(personId, key -> new java.util.LinkedHashSet<>()).add(familyId);
}
}
private void validateExcel(CcdiBaseStaffAssetInfoExcel excel) {
if (StringUtils.isEmpty(excel.getPersonId())) {
throw new RuntimeException("员工身份证号不能为空");
}
if (StringUtils.isEmpty(excel.getAssetMainType())) {
throw new RuntimeException("资产大类不能为空");
}
if (StringUtils.isEmpty(excel.getAssetSubType())) {
throw new RuntimeException("资产小类不能为空");
}
if (StringUtils.isEmpty(excel.getAssetName())) {
throw new RuntimeException("资产名称不能为空");
}
if (excel.getCurrentValue() == null) {
throw new RuntimeException("当前估值不能为空");
}
if (StringUtils.isEmpty(excel.getAssetStatus())) {
throw new RuntimeException("资产状态不能为空");
}
}
private void updateImportStatus(String taskId, String status, ImportResult result) {
Map<String, Object> statusData = new HashMap<>();
statusData.put("status", status);
statusData.put("successCount", result.getSuccessCount());
statusData.put("failureCount", result.getFailureCount());
statusData.put("progress", 100);
statusData.put("endTime", System.currentTimeMillis());
statusData.put("message", "SUCCESS".equals(status)
? "全部成功!共导入" + result.getTotalCount() + "条数据"
: "成功" + result.getSuccessCount() + "条,失败" + result.getFailureCount() + "");
redisTemplate.opsForHash().putAll(STATUS_KEY_PREFIX + taskId, statusData);
}
private String currentUserName() {
try {
return SecurityUtils.getUsername();
} catch (Exception e) {
return "system";
}
}
}

View File

@@ -77,17 +77,6 @@
</foreach>
</insert>
<select id="selectOwnerCandidatesByPersonIds" resultType="map">
SELECT
id_card AS personId,
id_card AS familyId
FROM ccdi_base_staff
WHERE id_card IN
<foreach collection="personIds" item="personId" open="(" separator="," close=")">
#{personId}
</foreach>
</select>
<select id="selectOwnerCandidatesByRelationCertNos" resultType="map">
SELECT
relation_cert_no AS personId,
@@ -100,4 +89,15 @@
</foreach>
</select>
<select id="selectOwnerCandidatesByBaseStaffIdCards" resultType="map">
SELECT
id_card AS personId,
id_card AS familyId
FROM ccdi_base_staff
WHERE id_card IN
<foreach collection="idCards" item="idCard" open="(" separator="," close=")">
#{idCard}
</foreach>
</select>
</mapper>

View File

@@ -109,11 +109,11 @@ class CcdiAssetInfoControllerTest {
}
@Test
void importTemplate_shouldUseGenericAssetTemplateName() {
void importTemplate_shouldUseFamilyAssetTemplateName() {
try (MockedStatic<EasyExcelUtil> mocked = mockStatic(EasyExcelUtil.class)) {
controller.importTemplate(null);
mocked.verify(() -> EasyExcelUtil.importTemplateWithDictDropdown(null, CcdiAssetInfoExcel.class, "资产信息"));
mocked.verify(() -> EasyExcelUtil.importTemplateWithDictDropdown(null, CcdiAssetInfoExcel.class, "亲属资产信息"));
}
}
}

View File

@@ -0,0 +1,119 @@
package com.ruoyi.info.collection.controller;
import com.ruoyi.common.constant.HttpStatus;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.info.collection.domain.excel.CcdiBaseStaffAssetInfoExcel;
import com.ruoyi.info.collection.domain.vo.BaseStaffAssetImportFailureVO;
import com.ruoyi.info.collection.domain.vo.ImportResultVO;
import com.ruoyi.info.collection.domain.vo.ImportStatusVO;
import com.ruoyi.info.collection.service.ICcdiBaseStaffAssetImportService;
import com.ruoyi.info.collection.utils.EasyExcelUtil;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.mock.web.MockMultipartFile;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class CcdiBaseStaffAssetImportControllerTest {
@InjectMocks
private CcdiBaseStaffAssetImportController controller;
@Mock
private ICcdiBaseStaffAssetImportService baseStaffAssetImportService;
@Test
void importData_shouldReturnWarnWhenExcelHasNoRows() throws Exception {
MockMultipartFile file = new MockMultipartFile(
"file",
"base-staff-asset-empty.xlsx",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"empty".getBytes(StandardCharsets.UTF_8)
);
try (MockedStatic<EasyExcelUtil> mocked = mockStatic(EasyExcelUtil.class)) {
mocked.when(() -> EasyExcelUtil.importExcel(any(InputStream.class), eq(CcdiBaseStaffAssetInfoExcel.class)))
.thenReturn(List.of());
AjaxResult result = controller.importData(file);
assertEquals(HttpStatus.WARN, result.get(AjaxResult.CODE_TAG));
assertEquals("至少需要一条数据", result.get(AjaxResult.MSG_TAG));
}
}
@Test
void importData_shouldReturnSuccessWhenTaskCreated() throws Exception {
MockMultipartFile file = new MockMultipartFile(
"file",
"base-staff-asset.xlsx",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"asset".getBytes(StandardCharsets.UTF_8)
);
CcdiBaseStaffAssetInfoExcel excel = new CcdiBaseStaffAssetInfoExcel();
excel.setPersonId("320101199001010011");
when(baseStaffAssetImportService.importAssetInfo(List.of(excel))).thenReturn("task-1");
try (MockedStatic<EasyExcelUtil> mocked = mockStatic(EasyExcelUtil.class)) {
mocked.when(() -> EasyExcelUtil.importExcel(any(InputStream.class), eq(CcdiBaseStaffAssetInfoExcel.class)))
.thenReturn(List.of(excel));
AjaxResult result = controller.importData(file);
assertEquals(HttpStatus.SUCCESS, result.get(AjaxResult.CODE_TAG));
assertEquals("导入任务已提交,正在后台处理", result.get(AjaxResult.MSG_TAG));
ImportResultVO data = (ImportResultVO) result.get(AjaxResult.DATA_TAG);
assertEquals("task-1", data.getTaskId());
}
}
@Test
void getImportStatus_shouldDelegateToImportService() {
ImportStatusVO statusVO = new ImportStatusVO();
statusVO.setTaskId("task-2");
when(baseStaffAssetImportService.getImportStatus("task-2")).thenReturn(statusVO);
AjaxResult result = controller.getImportStatus("task-2");
assertEquals(HttpStatus.SUCCESS, result.get(AjaxResult.CODE_TAG));
assertEquals(statusVO, result.get(AjaxResult.DATA_TAG));
}
@Test
void getImportFailures_shouldReturnPagedRows() {
BaseStaffAssetImportFailureVO failure1 = new BaseStaffAssetImportFailureVO();
failure1.setPersonId("A1");
BaseStaffAssetImportFailureVO failure2 = new BaseStaffAssetImportFailureVO();
failure2.setPersonId("A2");
when(baseStaffAssetImportService.getImportFailures("task-3")).thenReturn(List.of(failure1, failure2));
TableDataInfo result = controller.getImportFailures("task-3", 2, 1);
assertEquals(2, result.getTotal());
assertEquals(1, result.getRows().size());
assertEquals("A2", ((BaseStaffAssetImportFailureVO) result.getRows().get(0)).getPersonId());
}
@Test
void importTemplate_shouldUseBaseStaffAssetTemplateName() {
try (MockedStatic<EasyExcelUtil> mocked = mockStatic(EasyExcelUtil.class)) {
controller.importTemplate(null);
mocked.verify(() -> EasyExcelUtil.importTemplateWithDictDropdown(null, CcdiBaseStaffAssetInfoExcel.class, "员工资产信息"));
}
}
}

View File

@@ -97,19 +97,21 @@ class CcdiAssetInfoImportServiceImplTest {
}
@Test
void importAssetInfoAsync_shouldResolveFamilyIdFromEmployeeIdCardBeforeFamilyRelation() {
void importAssetInfoAsync_shouldFailWhenEmployeeIdCardIsUsedForFamilyAssetImport() {
CcdiAssetInfoExcel excel = buildExcel("320101199001010011", "房产");
when(redisTemplate.opsForHash()).thenReturn(hashOperations);
when(assetInfoMapper.selectOwnerCandidatesByPersonIds(List.of("320101199001010011")))
.thenReturn(List.of(owner("320101199001010011", "320101199001010011")));
when(redisTemplate.opsForValue()).thenReturn(valueOperations);
when(assetInfoMapper.selectOwnerCandidatesByRelationCertNos(List.of("320101199001010011")))
.thenReturn(List.of());
service.importAssetInfoAsync(List.of(excel), "task-self", "tester");
ArgumentCaptor<List<CcdiAssetInfo>> captor = ArgumentCaptor.forClass(List.class);
verify(assetInfoMapper).insertBatch(captor.capture());
assertEquals("320101199001010011", captor.getValue().get(0).getFamilyId());
assertEquals("320101199001010011", captor.getValue().get(0).getPersonId());
verify(assetInfoMapper, never()).selectOwnerCandidatesByRelationCertNos(any());
verify(assetInfoMapper, never()).insertBatch(any());
ArgumentCaptor<Object> failureCaptor = ArgumentCaptor.forClass(Object.class);
verify(valueOperations).set(eq("import:assetInfo:task-self:failures"), failureCaptor.capture(), eq(7L), eq(TimeUnit.DAYS));
AssetImportFailureVO failure = (AssetImportFailureVO) ((List<?>) failureCaptor.getValue()).get(0);
assertEquals("320101199001010011", failure.getPersonId());
assertTrue(failure.getErrorMessage().contains("未找到亲属资产归属员工"));
}
@Test
@@ -150,7 +152,7 @@ class CcdiAssetInfoImportServiceImplTest {
assertEquals(1, failures.size());
AssetImportFailureVO failure = (AssetImportFailureVO) failures.get(0);
assertEquals("320101199001010099", failure.getPersonId());
assertTrue(failure.getErrorMessage().contains("未找到资产归属员工"));
assertTrue(failure.getErrorMessage().contains("未找到亲属资产归属员工"));
}
@Test
@@ -170,7 +172,7 @@ class CcdiAssetInfoImportServiceImplTest {
ArgumentCaptor<Object> failureCaptor = ArgumentCaptor.forClass(Object.class);
verify(valueOperations).set(eq("import:assetInfo:task-4:failures"), failureCaptor.capture(), eq(7L), eq(TimeUnit.DAYS));
AssetImportFailureVO failure = (AssetImportFailureVO) ((List<?>) failureCaptor.getValue()).get(0);
assertTrue(failure.getErrorMessage().contains("资产归属员工不唯一"));
assertTrue(failure.getErrorMessage().contains("亲属资产归属员工不唯一"));
}
@Test
@@ -191,7 +193,7 @@ class CcdiAssetInfoImportServiceImplTest {
));
AssetImportFailureVO failureVO = new AssetImportFailureVO();
failureVO.setPersonId("320101199001010099");
failureVO.setErrorMessage("未找到资产归属员工");
failureVO.setErrorMessage("未找到亲属资产归属员工");
when(valueOperations.get("import:assetInfo:task-5:failures")).thenReturn(List.of(failureVO));
ImportStatusVO statusVO = service.getImportStatus("task-5");

View File

@@ -0,0 +1,145 @@
package com.ruoyi.info.collection.service;
import com.ruoyi.info.collection.domain.CcdiAssetInfo;
import com.ruoyi.info.collection.domain.excel.CcdiBaseStaffAssetInfoExcel;
import com.ruoyi.info.collection.domain.vo.BaseStaffAssetImportFailureVO;
import com.ruoyi.info.collection.domain.vo.ImportStatusVO;
import com.ruoyi.info.collection.mapper.CcdiAssetInfoMapper;
import com.ruoyi.info.collection.service.impl.CcdiBaseStaffAssetImportServiceImpl;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyMap;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class CcdiBaseStaffAssetImportServiceImplTest {
@InjectMocks
private CcdiBaseStaffAssetImportServiceImpl service;
@Mock
private CcdiAssetInfoMapper assetInfoMapper;
@Mock
private RedisTemplate<String, Object> redisTemplate;
@Mock
private ICcdiBaseStaffAssetImportService baseStaffAssetImportService;
@Mock
private HashOperations<String, Object, Object> hashOperations;
@Mock
private ValueOperations<String, Object> valueOperations;
@Test
void importAssetInfo_shouldUseDedicatedBaseStaffAssetTaskKeys() {
List<CcdiBaseStaffAssetInfoExcel> excelList = List.of(buildExcel("320101199001010011", "房产"));
when(redisTemplate.opsForHash()).thenReturn(hashOperations);
String taskId = service.importAssetInfo(excelList);
verify(hashOperations).putAll(eq("import:baseStaffAsset:" + taskId), anyMap());
verify(redisTemplate).expire("import:baseStaffAsset:" + taskId, 7, TimeUnit.DAYS);
verify(baseStaffAssetImportService).importAssetInfoAsync(eq(excelList), eq(taskId), any());
}
@Test
void importAssetInfoAsync_shouldImportWhenEmployeeIdCardExists() {
CcdiBaseStaffAssetInfoExcel excel = buildExcel("320101199001010011", "房产");
when(redisTemplate.opsForHash()).thenReturn(hashOperations);
when(assetInfoMapper.selectOwnerCandidatesByBaseStaffIdCards(List.of("320101199001010011")))
.thenReturn(List.of(owner("320101199001010011", "320101199001010011")));
service.importAssetInfoAsync(List.of(excel), "task-1", "tester");
ArgumentCaptor<List<CcdiAssetInfo>> captor = ArgumentCaptor.forClass(List.class);
verify(assetInfoMapper).insertBatch(captor.capture());
assertEquals("320101199001010011", captor.getValue().get(0).getFamilyId());
assertEquals("320101199001010011", captor.getValue().get(0).getPersonId());
}
@Test
void importAssetInfoAsync_shouldFailWhenFamilyCertificateIsUsed() {
CcdiBaseStaffAssetInfoExcel excel = buildExcel("320101199201010022", "车辆");
when(redisTemplate.opsForHash()).thenReturn(hashOperations);
when(redisTemplate.opsForValue()).thenReturn(valueOperations);
when(assetInfoMapper.selectOwnerCandidatesByBaseStaffIdCards(List.of("320101199201010022")))
.thenReturn(List.of());
service.importAssetInfoAsync(List.of(excel), "task-2", "tester");
verify(assetInfoMapper, never()).insertBatch(any());
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("320101199201010022", failure.getPersonId());
assertTrue(failure.getErrorMessage().contains("员工资产导入仅支持员工本人证件号"));
}
@Test
void getImportStatusAndFailures_shouldUseBaseStaffAssetPrefixes() {
when(redisTemplate.opsForHash()).thenReturn(hashOperations);
when(redisTemplate.opsForValue()).thenReturn(valueOperations);
when(redisTemplate.hasKey("import:baseStaffAsset:task-3")).thenReturn(true);
when(hashOperations.entries("import:baseStaffAsset:task-3")).thenReturn(Map.of(
"taskId", "task-3",
"status", "SUCCESS",
"totalCount", 1,
"successCount", 1,
"failureCount", 0,
"progress", 100,
"startTime", 1L,
"endTime", 2L,
"message", "全部成功"
));
BaseStaffAssetImportFailureVO failureVO = new BaseStaffAssetImportFailureVO();
failureVO.setPersonId("320101199001010099");
failureVO.setErrorMessage("员工资产导入仅支持员工本人证件号");
when(valueOperations.get("import:baseStaffAsset:task-3:failures")).thenReturn(List.of(failureVO));
ImportStatusVO statusVO = service.getImportStatus("task-3");
List<BaseStaffAssetImportFailureVO> failures = service.getImportFailures("task-3");
assertEquals("task-3", statusVO.getTaskId());
assertEquals("SUCCESS", statusVO.getStatus());
assertNotNull(failures);
assertEquals(1, failures.size());
assertEquals("320101199001010099", failures.get(0).getPersonId());
}
private CcdiBaseStaffAssetInfoExcel buildExcel(String personId, String assetMainType) {
CcdiBaseStaffAssetInfoExcel excel = new CcdiBaseStaffAssetInfoExcel();
excel.setPersonId(personId);
excel.setAssetMainType(assetMainType);
excel.setAssetSubType(assetMainType + "小类");
excel.setAssetName(assetMainType + "名称");
excel.setCurrentValue(new BigDecimal("100.00"));
excel.setAssetStatus("正常");
return excel;
}
private Map<String, String> owner(String personId, String familyId) {
return Map.of("personId", personId, "familyId", familyId);
}
}

View File

@@ -239,3 +239,10 @@
- 独立提交员工资产导入新增改动
- 独立提交亲属资产导入收敛改动
- 任一阶段出现回归,可按提交粒度回退
## 实现状态
- 2026-03-13 已完成后端拆分实现
- 已新增员工资产独立导入接口 `/ccdi/baseStaff/asset/*`
- 已将 `/ccdi/assetInfo/*` 收敛为亲属资产专用接口
- 已通过后端定向测试验证员工与亲属两套导入链路、模板名称和失败文案拆分生效