新增员工资产信息后端实施计划

This commit is contained in:
wkc
2026-03-12 16:33:07 +08:00
parent 606aab6bb4
commit bac3cf094e
22 changed files with 1825 additions and 2 deletions

View File

@@ -45,6 +45,13 @@
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>
<!-- 测试依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>

View File

@@ -0,0 +1,102 @@
package com.ruoyi.info.collection.controller;
import com.ruoyi.info.collection.domain.excel.CcdiAssetInfoExcel;
import com.ruoyi.info.collection.domain.vo.AssetImportFailureVO;
import com.ruoyi.info.collection.domain.vo.ImportResultVO;
import com.ruoyi.info.collection.domain.vo.ImportStatusVO;
import com.ruoyi.info.collection.service.ICcdiAssetInfoImportService;
import com.ruoyi.info.collection.utils.EasyExcelUtil;
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 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-12
*/
@Tag(name = "员工资产信息管理")
@RestController
@RequestMapping("/ccdi/assetInfo")
public class CcdiAssetInfoController extends BaseController {
@Resource
private ICcdiAssetInfoImportService assetInfoImportService;
/**
* 下载导入模板
*/
@Operation(summary = "下载资产导入模板")
@PostMapping("/importTemplate")
public void importTemplate(HttpServletResponse response) {
EasyExcelUtil.importTemplateWithDictDropdown(response, CcdiAssetInfoExcel.class, "员工资产信息");
}
/**
* 导入员工资产信息
*/
@Operation(summary = "导入员工资产信息")
@PreAuthorize("@ss.hasPermi('ccdi:baseStaff:import')")
@Log(title = "员工资产信息", businessType = BusinessType.IMPORT)
@PostMapping("/importData")
public AjaxResult importData(MultipartFile file) throws Exception {
List<CcdiAssetInfoExcel> list = EasyExcelUtil.importExcel(file.getInputStream(), CcdiAssetInfoExcel.class);
if (list == null || list.isEmpty()) {
return error("至少需要一条数据");
}
String taskId = assetInfoImportService.importAssetInfo(list);
ImportResultVO result = new ImportResultVO();
result.setTaskId(taskId);
result.setStatus("PROCESSING");
result.setMessage("导入任务已提交,正在后台处理");
return AjaxResult.success("导入任务已提交,正在后台处理", result);
}
/**
* 查询导入状态
*/
@Operation(summary = "查询员工资产导入状态")
@PreAuthorize("@ss.hasPermi('ccdi:baseStaff:import')")
@GetMapping("/importStatus/{taskId}")
public AjaxResult getImportStatus(@PathVariable String taskId) {
return success(assetInfoImportService.getImportStatus(taskId));
}
/**
* 查询导入失败记录
*/
@Operation(summary = "查询员工资产导入失败记录")
@PreAuthorize("@ss.hasPermi('ccdi:baseStaff:import')")
@GetMapping("/importFailures/{taskId}")
public TableDataInfo getImportFailures(
@PathVariable String taskId,
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize) {
List<AssetImportFailureVO> failures = assetInfoImportService.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

@@ -0,0 +1,83 @@
package com.ruoyi.info.collection.domain;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
* 员工资产信息对象 ccdi_asset_info
*
* @author ruoyi
* @date 2026-03-12
*/
@Data
@TableName("ccdi_asset_info")
public class CcdiAssetInfo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/** 资产ID */
@TableId(type = IdType.AUTO)
private Long assetId;
/** 归属员工身份证号 */
private String familyId;
/** 资产实际持有人身份证号 */
private String personId;
/** 资产大类 */
private String assetMainType;
/** 资产小类 */
private String assetSubType;
/** 资产名称 */
private String assetName;
/** 产权占比 */
private BigDecimal ownershipRatio;
/** 购买/评估日期 */
private Date purchaseEvalDate;
/** 资产原值 */
private BigDecimal originalValue;
/** 当前估值 */
private BigDecimal currentValue;
/** 估值截止日期 */
private Date valuationDate;
/** 资产状态 */
private String assetStatus;
/** 备注 */
private String remarks;
/** 创建者 */
@TableField(fill = FieldFill.INSERT)
private String createBy;
/** 创建时间 */
@TableField(fill = FieldFill.INSERT)
private Date createTime;
/** 更新者 */
@TableField(fill = FieldFill.INSERT_UPDATE)
private String updateBy;
/** 更新时间 */
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
}

View File

@@ -0,0 +1,86 @@
package com.ruoyi.info.collection.domain.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
* 员工资产信息DTO
*
* @author ruoyi
* @date 2026-03-12
*/
@Data
@Schema(description = "员工资产信息")
public class CcdiAssetInfoDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/** 资产实际持有人身份证号 */
@NotBlank(message = "资产实际持有人身份证号不能为空")
@Pattern(regexp = "^[1-9]\\d{5}(18|19|20)\\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\\d|3[01])\\d{3}[0-9Xx]$", message = "资产实际持有人身份证号格式不正确")
@Schema(description = "资产实际持有人身份证号")
private String personId;
/** 资产大类 */
@NotBlank(message = "资产大类不能为空")
@Size(max = 20, message = "资产大类长度不能超过20个字符")
@Schema(description = "资产大类")
private String assetMainType;
/** 资产小类 */
@NotBlank(message = "资产小类不能为空")
@Size(max = 50, message = "资产小类长度不能超过50个字符")
@Schema(description = "资产小类")
private String assetSubType;
/** 资产名称 */
@NotBlank(message = "资产名称不能为空")
@Size(max = 200, message = "资产名称长度不能超过200个字符")
@Schema(description = "资产名称")
private String assetName;
/** 产权占比 */
@Schema(description = "产权占比")
private BigDecimal ownershipRatio;
/** 购买/评估日期 */
@JsonFormat(pattern = "yyyy-MM-dd")
@Schema(description = "购买/评估日期")
private Date purchaseEvalDate;
/** 资产原值 */
@Schema(description = "资产原值")
private BigDecimal originalValue;
/** 当前估值 */
@NotNull(message = "当前估值不能为空")
@Schema(description = "当前估值")
private BigDecimal currentValue;
/** 估值截止日期 */
@JsonFormat(pattern = "yyyy-MM-dd")
@Schema(description = "估值截止日期")
private Date valuationDate;
/** 资产状态 */
@NotBlank(message = "资产状态不能为空")
@Size(max = 10, message = "资产状态长度不能超过10个字符")
@Schema(description = "资产状态")
private String assetStatus;
/** 备注 */
@Size(max = 500, message = "备注长度不能超过500个字符")
@Schema(description = "备注")
private String remarks;
}

View File

@@ -9,6 +9,7 @@ import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
import java.util.List;
/**
* 员工信息新增 DTO
@@ -51,4 +52,7 @@ public class CcdiBaseStaffAddDTO implements Serializable {
/** 状态 */
@NotBlank(message = "状态不能为空")
private String status;
/** 资产信息列表 */
private List<CcdiAssetInfoDTO> assetInfoList;
}

View File

@@ -9,6 +9,7 @@ import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
import java.util.List;
/**
* 员工信息编辑 DTO
@@ -49,4 +50,7 @@ public class CcdiBaseStaffEditDTO implements Serializable {
/** 状态 */
private String status;
/** 资产信息列表 */
private List<CcdiAssetInfoDTO> assetInfoList;
}

View File

@@ -0,0 +1,85 @@
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.Required;
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-12
*/
@Data
public class CcdiAssetInfoExcel implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/** 资产实际持有人身份证号 */
@ExcelProperty(value = "资产实际持有人身份证号*", index = 0)
@ColumnWidth(22)
@Required
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)
@Required
private String assetStatus;
/** 备注 */
@ExcelProperty(value = "备注", index = 10)
@ColumnWidth(28)
private String remarks;
}

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-12
*/
@Data
@Schema(description = "员工资产信息导入失败记录")
public class AssetImportFailureVO {
/** 资产实际持有人身份证号 */
@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

@@ -0,0 +1,70 @@
package com.ruoyi.info.collection.domain.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
* 员工资产信息VO
*
* @author ruoyi
* @date 2026-03-12
*/
@Data
@Schema(description = "员工资产信息")
public class CcdiAssetInfoVO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/** 资产实际持有人身份证号 */
@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;
/** 购买/评估日期 */
@JsonFormat(pattern = "yyyy-MM-dd")
@Schema(description = "购买/评估日期")
private Date purchaseEvalDate;
/** 资产原值 */
@Schema(description = "资产原值")
private BigDecimal originalValue;
/** 当前估值 */
@Schema(description = "当前估值")
private BigDecimal currentValue;
/** 估值截止日期 */
@JsonFormat(pattern = "yyyy-MM-dd")
@Schema(description = "估值截止日期")
private Date valuationDate;
/** 资产状态 */
@Schema(description = "资产状态")
private String assetStatus;
/** 备注 */
@Schema(description = "备注")
private String remarks;
}

View File

@@ -5,6 +5,7 @@ import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
import java.util.List;
/**
* 员工信息 VO
@@ -56,4 +57,7 @@ public class CcdiBaseStaffVO implements Serializable {
/** 更新者 */
private String updateBy;
/** 资产信息列表 */
private List<CcdiAssetInfoVO> assetInfoList;
}

View File

@@ -0,0 +1,73 @@
package com.ruoyi.info.collection.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.info.collection.domain.CcdiAssetInfo;
import org.apache.ibatis.annotations.Param;
import java.util.List;
import java.util.Map;
/**
* 员工资产信息 数据层
*
* @author ruoyi
* @date 2026-03-12
*/
public interface CcdiAssetInfoMapper extends BaseMapper<CcdiAssetInfo> {
/**
* 按归属员工身份证号查询资产列表
*
* @param familyId 归属员工身份证号
* @return 资产列表
*/
List<CcdiAssetInfo> selectByFamilyId(@Param("familyId") String familyId);
/**
* 按归属员工身份证号删除资产
*
* @param familyId 归属员工身份证号
* @return 影响行数
*/
int deleteByFamilyId(@Param("familyId") String familyId);
/**
* 批量删除归属员工资产
*
* @param familyIds 归属员工身份证号列表
* @return 影响行数
*/
int deleteByFamilyIds(@Param("familyIds") List<String> familyIds);
/**
* 按资产实际持有人身份证号查询资产列表
*
* @param personId 资产实际持有人身份证号
* @return 资产列表
*/
List<CcdiAssetInfo> selectByPersonId(@Param("personId") String personId);
/**
* 批量插入资产数据
*
* @param list 资产列表
* @return 影响行数
*/
int insertBatch(@Param("list") List<CcdiAssetInfo> list);
/**
* 按员工身份证号查询归属信息
*
* @param personIds 资产实际持有人身份证号列表
* @return 归属映射
*/
List<Map<String, String>> selectOwnerByEmployeeIdCards(@Param("personIds") List<String> personIds);
/**
* 按员工家庭关系身份证号查询归属信息
*
* @param personIds 资产实际持有人身份证号列表
* @return 归属映射
*/
List<Map<String, String>> selectOwnerByFamilyRelationIdCards(@Param("personIds") List<String> personIds);
}

View File

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

View File

@@ -0,0 +1,47 @@
package com.ruoyi.info.collection.service;
import com.ruoyi.info.collection.domain.CcdiAssetInfo;
import com.ruoyi.info.collection.domain.dto.CcdiAssetInfoDTO;
import java.util.List;
/**
* 员工资产信息 服务层
*
* @author ruoyi
* @date 2026-03-12
*/
public interface ICcdiAssetInfoService {
/**
* 按归属员工身份证号查询资产列表
*
* @param familyId 归属员工身份证号
* @return 资产列表
*/
List<CcdiAssetInfo> selectByFamilyId(String familyId);
/**
* 按归属员工身份证号覆盖资产列表
*
* @param familyId 归属员工身份证号
* @param assetInfoList 资产列表
*/
void replaceByFamilyId(String familyId, List<CcdiAssetInfoDTO> assetInfoList);
/**
* 删除单个员工资产
*
* @param familyId 归属员工身份证号
* @return 影响行数
*/
int deleteByFamilyId(String familyId);
/**
* 批量删除员工资产
*
* @param familyIds 归属员工身份证号列表
* @return 影响行数
*/
int deleteByFamilyIds(List<String> familyIds);
}

View File

@@ -0,0 +1,227 @@
package com.ruoyi.info.collection.service.impl;
import com.alibaba.fastjson2.JSON;
import com.ruoyi.info.collection.domain.CcdiAssetInfo;
import com.ruoyi.info.collection.domain.excel.CcdiAssetInfoExcel;
import com.ruoyi.info.collection.domain.vo.AssetImportFailureVO;
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.ICcdiAssetInfoImportService;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
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;
import java.util.stream.Collectors;
/**
* 员工资产信息异步导入服务层处理
*
* @author ruoyi
* @date 2026-03-12
*/
@Service
@EnableAsync
public class CcdiAssetInfoImportServiceImpl implements ICcdiAssetInfoImportService {
private static final String STATUS_KEY_PREFIX = "import:assetInfo:";
@Resource
private CcdiAssetInfoMapper assetInfoMapper;
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Lazy
@Resource
private ICcdiAssetInfoImportService assetInfoImportService;
@Override
@Transactional
public String importAssetInfo(List<CcdiAssetInfoExcel> 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);
assetInfoImportService.importAssetInfoAsync(excelList, taskId, currentUserName());
return taskId;
}
@Override
@Async
@Transactional
public void importAssetInfoAsync(List<CcdiAssetInfoExcel> excelList, String taskId, String userName) {
List<CcdiAssetInfo> successList = new ArrayList<>();
List<AssetImportFailureVO> failures = new ArrayList<>();
List<String> personIds = excelList.stream()
.map(CcdiAssetInfoExcel::getPersonId)
.filter(StringUtils::isNotEmpty)
.distinct()
.toList();
Map<String, Set<String>> ownerMap = buildOwnerMap(personIds);
for (CcdiAssetInfoExcel excel : excelList) {
try {
validateExcel(excel);
Set<String> familyIds = ownerMap.get(excel.getPersonId());
if (familyIds == null || familyIds.isEmpty()) {
throw new RuntimeException("未找到资产归属员工");
}
if (familyIds.size() > 1) {
throw new RuntimeException("资产归属员工不唯一");
}
CcdiAssetInfo assetInfo = new CcdiAssetInfo();
BeanUtils.copyProperties(excel, assetInfo);
assetInfo.setFamilyId(familyIds.iterator().next());
assetInfo.setCreateBy(userName);
assetInfo.setUpdateBy(userName);
successList.add(assetInfo);
} catch (Exception e) {
AssetImportFailureVO failureVO = new AssetImportFailureVO();
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<AssetImportFailureVO> 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), AssetImportFailureVO.class);
}
private Map<String, Set<String>> buildOwnerMap(List<String> personIds) {
Map<String, Set<String>> result = new LinkedHashMap<>();
mergeOwnerMappings(result, assetInfoMapper.selectOwnerByEmployeeIdCards(personIds));
mergeOwnerMappings(result, assetInfoMapper.selectOwnerByFamilyRelationIdCards(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(CcdiAssetInfoExcel 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

@@ -0,0 +1,84 @@
package com.ruoyi.info.collection.service.impl;
import com.ruoyi.info.collection.domain.CcdiAssetInfo;
import com.ruoyi.info.collection.domain.dto.CcdiAssetInfoDTO;
import com.ruoyi.info.collection.mapper.CcdiAssetInfoMapper;
import com.ruoyi.info.collection.service.ICcdiAssetInfoService;
import com.ruoyi.common.utils.StringUtils;
import jakarta.annotation.Resource;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Collections;
import java.util.List;
/**
* 员工资产信息 服务层处理
*
* @author ruoyi
* @date 2026-03-12
*/
@Service
public class CcdiAssetInfoServiceImpl implements ICcdiAssetInfoService {
@Resource
private CcdiAssetInfoMapper assetInfoMapper;
@Override
public List<CcdiAssetInfo> selectByFamilyId(String familyId) {
return assetInfoMapper.selectByFamilyId(familyId);
}
@Override
@Transactional
public void replaceByFamilyId(String familyId, List<CcdiAssetInfoDTO> assetInfoList) {
assetInfoMapper.deleteByFamilyId(familyId);
if (assetInfoList == null || assetInfoList.isEmpty()) {
return;
}
List<CcdiAssetInfo> saveList = assetInfoList.stream()
.filter(item -> !isEmptyRow(item))
.map(item -> toEntity(familyId, item))
.toList();
if (!saveList.isEmpty()) {
assetInfoMapper.insertBatch(saveList);
}
}
@Override
public int deleteByFamilyId(String familyId) {
return assetInfoMapper.deleteByFamilyId(familyId);
}
@Override
public int deleteByFamilyIds(List<String> familyIds) {
if (familyIds == null || familyIds.isEmpty()) {
return 0;
}
return assetInfoMapper.deleteByFamilyIds(familyIds);
}
private CcdiAssetInfo toEntity(String familyId, CcdiAssetInfoDTO dto) {
CcdiAssetInfo assetInfo = new CcdiAssetInfo();
BeanUtils.copyProperties(dto, assetInfo);
assetInfo.setFamilyId(familyId);
return assetInfo;
}
private boolean isEmptyRow(CcdiAssetInfoDTO dto) {
return StringUtils.isEmpty(dto.getPersonId())
&& StringUtils.isEmpty(dto.getAssetMainType())
&& StringUtils.isEmpty(dto.getAssetSubType())
&& StringUtils.isEmpty(dto.getAssetName())
&& dto.getCurrentValue() == null
&& StringUtils.isEmpty(dto.getAssetStatus())
&& dto.getOwnershipRatio() == null
&& dto.getPurchaseEvalDate() == null
&& dto.getOriginalValue() == null
&& dto.getValuationDate() == null
&& StringUtils.isEmpty(dto.getRemarks());
}
}

View File

@@ -7,10 +7,12 @@ 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.CcdiBaseStaffExcel;
import com.ruoyi.info.collection.domain.vo.CcdiAssetInfoVO;
import com.ruoyi.info.collection.domain.vo.CcdiBaseStaffOptionVO;
import com.ruoyi.info.collection.domain.vo.CcdiBaseStaffVO;
import com.ruoyi.info.collection.enums.EmployeeStatus;
import com.ruoyi.info.collection.mapper.CcdiBaseStaffMapper;
import com.ruoyi.info.collection.service.ICcdiAssetInfoService;
import com.ruoyi.info.collection.service.ICcdiBaseStaffImportService;
import com.ruoyi.info.collection.service.ICcdiBaseStaffService;
import com.ruoyi.common.utils.StringUtils;
@@ -40,6 +42,9 @@ public class CcdiBaseStaffServiceImpl implements ICcdiBaseStaffService {
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource
private ICcdiAssetInfoService assetInfoService;
/**
* 查询员工列表
*
@@ -104,7 +109,15 @@ public class CcdiBaseStaffServiceImpl implements ICcdiBaseStaffService {
@Override
public CcdiBaseStaffVO selectBaseStaffById(Long staffId) {
CcdiBaseStaff staff = baseStaffMapper.selectById(staffId);
return convertToVO(staff);
CcdiBaseStaffVO vo = convertToVO(staff);
if (staff != null) {
vo.setAssetInfoList(assetInfoService.selectByFamilyId(staff.getIdCard()).stream().map(asset -> {
CcdiAssetInfoVO assetInfoVO = new CcdiAssetInfoVO();
BeanUtils.copyProperties(asset, assetInfoVO);
return assetInfoVO;
}).toList());
}
return vo;
}
/**
@@ -131,6 +144,7 @@ public class CcdiBaseStaffServiceImpl implements ICcdiBaseStaffService {
CcdiBaseStaff staff = new CcdiBaseStaff();
BeanUtils.copyProperties(addDTO, staff);
int result = baseStaffMapper.insert(staff);
assetInfoService.replaceByFamilyId(addDTO.getIdCard(), addDTO.getAssetInfoList());
return result;
}
@@ -144,6 +158,11 @@ public class CcdiBaseStaffServiceImpl implements ICcdiBaseStaffService {
@Override
@Transactional
public int updateBaseStaff(CcdiBaseStaffEditDTO editDTO) {
CcdiBaseStaff existing = baseStaffMapper.selectById(editDTO.getStaffId());
if (existing == null) {
throw new RuntimeException("员工不存在");
}
// 检查身份证号唯一性(排除自己)
if (StringUtils.isNotEmpty(editDTO.getIdCard())) {
LambdaQueryWrapper<CcdiBaseStaff> wrapper = new LambdaQueryWrapper<>();
@@ -158,6 +177,11 @@ public class CcdiBaseStaffServiceImpl implements ICcdiBaseStaffService {
BeanUtils.copyProperties(editDTO, staff);
int result = baseStaffMapper.updateById(staff);
if (!StringUtils.equals(existing.getIdCard(), editDTO.getIdCard())) {
assetInfoService.deleteByFamilyId(existing.getIdCard());
}
assetInfoService.replaceByFamilyId(editDTO.getIdCard(), editDTO.getAssetInfoList());
return result;
}
@@ -170,7 +194,13 @@ public class CcdiBaseStaffServiceImpl implements ICcdiBaseStaffService {
@Override
@Transactional
public int deleteBaseStaffByIds(Long[] staffIds) {
return baseStaffMapper.deleteBatchIds(List.of(staffIds));
List<Long> idList = List.of(staffIds);
List<String> familyIds = baseStaffMapper.selectBatchIds(idList).stream()
.map(CcdiBaseStaff::getIdCard)
.filter(StringUtils::isNotEmpty)
.toList();
assetInfoService.deleteByFamilyIds(familyIds);
return baseStaffMapper.deleteBatchIds(idList);
}
/**

View File

@@ -0,0 +1,96 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.info.collection.mapper.CcdiAssetInfoMapper">
<resultMap id="CcdiAssetInfoResultMap" type="com.ruoyi.info.collection.domain.CcdiAssetInfo">
<id property="assetId" column="asset_id"/>
<result property="familyId" column="family_id"/>
<result property="personId" column="person_id"/>
<result property="assetMainType" column="asset_main_type"/>
<result property="assetSubType" column="asset_sub_type"/>
<result property="assetName" column="asset_name"/>
<result property="ownershipRatio" column="ownership_ratio"/>
<result property="purchaseEvalDate" column="purchase_eval_date"/>
<result property="originalValue" column="original_value"/>
<result property="currentValue" column="current_value"/>
<result property="valuationDate" column="valuation_date"/>
<result property="assetStatus" column="asset_status"/>
<result property="remarks" column="remarks"/>
<result property="createBy" column="create_by"/>
<result property="createTime" column="create_time"/>
<result property="updateBy" column="update_by"/>
<result property="updateTime" column="update_time"/>
</resultMap>
<select id="selectByFamilyId" resultMap="CcdiAssetInfoResultMap">
SELECT
asset_id, family_id, person_id, asset_main_type, asset_sub_type, asset_name,
ownership_ratio, purchase_eval_date, original_value, current_value,
valuation_date, asset_status, remarks, create_by, create_time, update_by, update_time
FROM ccdi_asset_info
WHERE family_id = #{familyId}
ORDER BY create_time DESC, asset_id DESC
</select>
<select id="selectByPersonId" resultMap="CcdiAssetInfoResultMap">
SELECT
asset_id, family_id, person_id, asset_main_type, asset_sub_type, asset_name,
ownership_ratio, purchase_eval_date, original_value, current_value,
valuation_date, asset_status, remarks, create_by, create_time, update_by, update_time
FROM ccdi_asset_info
WHERE person_id = #{personId}
ORDER BY create_time DESC, asset_id DESC
</select>
<delete id="deleteByFamilyId">
DELETE FROM ccdi_asset_info
WHERE family_id = #{familyId}
</delete>
<delete id="deleteByFamilyIds">
DELETE FROM ccdi_asset_info
WHERE family_id IN
<foreach collection="familyIds" item="familyId" open="(" separator="," close=")">
#{familyId}
</foreach>
</delete>
<insert id="insertBatch">
INSERT INTO ccdi_asset_info
(family_id, person_id, asset_main_type, asset_sub_type, asset_name,
ownership_ratio, purchase_eval_date, original_value, current_value,
valuation_date, asset_status, remarks, create_by, create_time, update_by, update_time)
VALUES
<foreach collection="list" item="item" separator=",">
(#{item.familyId}, #{item.personId}, #{item.assetMainType}, #{item.assetSubType}, #{item.assetName},
#{item.ownershipRatio}, #{item.purchaseEvalDate}, #{item.originalValue}, #{item.currentValue},
#{item.valuationDate}, #{item.assetStatus}, #{item.remarks}, #{item.createBy}, NOW(), #{item.updateBy}, NOW())
</foreach>
</insert>
<select id="selectOwnerByEmployeeIdCards" 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="selectOwnerByFamilyRelationIdCards" resultType="map">
SELECT
relation_cert_no AS personId,
person_id AS familyId
FROM ccdi_staff_fmy_relation
WHERE is_emp_family = 1
AND relation_cert_no IN
<foreach collection="personIds" item="personId" open="(" separator="," close=")">
#{personId}
</foreach>
</select>
</mapper>

View File

@@ -0,0 +1,175 @@
package com.ruoyi.info.collection.mapper;
import com.ruoyi.info.collection.domain.CcdiAssetInfo;
import org.apache.ibatis.builder.xml.XMLMapperBuilder;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.Environment;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.scripting.xmltags.XMLLanguageDriver;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
import org.apache.ibatis.type.TypeAliasRegistry;
import org.junit.jupiter.api.Test;
import javax.sql.DataSource;
import java.io.InputStream;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class CcdiAssetInfoMapperTest {
private static final String RESOURCE = "mapper/info/collection/CcdiAssetInfoMapper.xml";
@Test
void selectByFamilyId_shouldFilterByFamilyId() throws Exception {
MappedStatement mappedStatement = loadMappedStatement(
"com.ruoyi.info.collection.mapper.CcdiAssetInfoMapper.selectByFamilyId");
String sql = renderSql(mappedStatement, Map.of("familyId", "320101199001010011"));
assertTrue(sql.contains("FROM ccdi_asset_info"), sql);
assertTrue(sql.contains("WHERE family_id = ?"), sql);
}
@Test
void selectByPersonId_shouldFilterByPersonId() throws Exception {
MappedStatement mappedStatement = loadMappedStatement(
"com.ruoyi.info.collection.mapper.CcdiAssetInfoMapper.selectByPersonId");
String sql = renderSql(mappedStatement, Map.of("personId", "320101199201010022"));
assertTrue(sql.contains("FROM ccdi_asset_info"), sql);
assertTrue(sql.contains("WHERE person_id = ?"), sql);
}
@Test
void deleteByFamilyIds_shouldRenderInClause() throws Exception {
MappedStatement mappedStatement = loadMappedStatement(
"com.ruoyi.info.collection.mapper.CcdiAssetInfoMapper.deleteByFamilyIds");
String sql = renderSql(mappedStatement, Map.of("familyIds", List.of("A", "B")));
assertTrue(sql.contains("DELETE FROM ccdi_asset_info"), sql);
assertTrue(sql.contains("family_id IN"), sql);
assertFalse(sql.contains("IN ()"), sql);
}
@Test
void insertBatch_shouldIncludeAllBusinessColumns() throws Exception {
MappedStatement mappedStatement = loadMappedStatement(
"com.ruoyi.info.collection.mapper.CcdiAssetInfoMapper.insertBatch");
CcdiAssetInfo assetInfo = new CcdiAssetInfo();
assetInfo.setFamilyId("320101199001010011");
assetInfo.setPersonId("320101199201010022");
assetInfo.setAssetMainType("车辆");
assetInfo.setAssetSubType("小汽车");
assetInfo.setAssetName("家庭车辆");
assetInfo.setCurrentValue(new BigDecimal("100000.00"));
assetInfo.setAssetStatus("正常");
String sql = renderSql(mappedStatement, Map.of("list", List.of(assetInfo)));
assertTrue(sql.contains("INSERT INTO ccdi_asset_info"), sql);
assertTrue(sql.contains("family_id"), sql);
assertTrue(sql.contains("person_id"), sql);
assertTrue(sql.contains("asset_main_type"), sql);
assertTrue(sql.contains("current_value"), sql);
assertTrue(sql.contains("asset_status"), sql);
}
@Test
void ownerLookupQueries_shouldResolveFromEmployeeAndFamilyRelation() throws Exception {
MappedStatement employeeStatement = loadMappedStatement(
"com.ruoyi.info.collection.mapper.CcdiAssetInfoMapper.selectOwnerByEmployeeIdCards");
MappedStatement familyStatement = loadMappedStatement(
"com.ruoyi.info.collection.mapper.CcdiAssetInfoMapper.selectOwnerByFamilyRelationIdCards");
String employeeSql = renderSql(employeeStatement, Map.of("personIds", List.of("A")));
String familySql = renderSql(familyStatement, Map.of("personIds", List.of("B")));
assertTrue(employeeSql.contains("FROM ccdi_base_staff"), employeeSql);
assertTrue(employeeSql.contains("id_card AS personId"), employeeSql);
assertTrue(employeeSql.contains("id_card AS familyId"), employeeSql);
assertTrue(familySql.contains("FROM ccdi_staff_fmy_relation"), familySql);
assertTrue(familySql.contains("relation_cert_no AS personId"), familySql);
assertTrue(familySql.contains("person_id AS familyId"), familySql);
assertTrue(familySql.contains("is_emp_family = 1"), familySql);
}
private MappedStatement loadMappedStatement(String statementId) throws Exception {
Configuration configuration = new Configuration();
configuration.setEnvironment(new Environment("test", new JdbcTransactionFactory(), new NoOpDataSource()));
registerTypeAliases(configuration.getTypeAliasRegistry());
configuration.getLanguageRegistry().register(XMLLanguageDriver.class);
configuration.addMapper(CcdiAssetInfoMapper.class);
try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(RESOURCE)) {
XMLMapperBuilder xmlMapperBuilder =
new XMLMapperBuilder(inputStream, configuration, RESOURCE, configuration.getSqlFragments());
xmlMapperBuilder.parse();
}
return configuration.getMappedStatement(statementId);
}
private String renderSql(MappedStatement mappedStatement, Map<String, Object> params) {
BoundSql boundSql = mappedStatement.getBoundSql(new HashMap<>(params));
return boundSql.getSql().replaceAll("\\s+", " ").trim();
}
private void registerTypeAliases(TypeAliasRegistry typeAliasRegistry) {
typeAliasRegistry.registerAlias("map", Map.class);
}
private static class NoOpDataSource implements DataSource {
@Override
public java.sql.Connection getConnection() {
throw new UnsupportedOperationException("Not required for SQL rendering tests");
}
@Override
public java.sql.Connection getConnection(String username, String password) {
throw new UnsupportedOperationException("Not required for SQL rendering tests");
}
@Override
public java.io.PrintWriter getLogWriter() {
return null;
}
@Override
public void setLogWriter(java.io.PrintWriter out) {
}
@Override
public void setLoginTimeout(int seconds) {
}
@Override
public int getLoginTimeout() {
return 0;
}
@Override
public java.util.logging.Logger getParentLogger() {
return java.util.logging.Logger.getGlobal();
}
@Override
public <T> T unwrap(Class<T> iface) {
throw new UnsupportedOperationException("Not supported");
}
@Override
public boolean isWrapperFor(Class<?> iface) {
return false;
}
}
}

View File

@@ -0,0 +1,213 @@
package com.ruoyi.info.collection.service;
import com.ruoyi.info.collection.domain.CcdiAssetInfo;
import com.ruoyi.info.collection.domain.excel.CcdiAssetInfoExcel;
import com.ruoyi.info.collection.domain.vo.AssetImportFailureVO;
import com.ruoyi.info.collection.domain.vo.ImportStatusVO;
import com.ruoyi.info.collection.mapper.CcdiAssetInfoMapper;
import com.ruoyi.info.collection.service.impl.CcdiAssetInfoImportServiceImpl;
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.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
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.anyLong;
import static org.mockito.ArgumentMatchers.anyMap;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.startsWith;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class CcdiAssetInfoImportServiceImplTest {
@InjectMocks
private CcdiAssetInfoImportServiceImpl service;
@Mock
private CcdiAssetInfoMapper assetInfoMapper;
@Mock
private RedisTemplate<String, Object> redisTemplate;
@Mock
private ICcdiAssetInfoImportService assetInfoImportService;
@Mock
private HashOperations<String, Object, Object> hashOperations;
@Mock
private ValueOperations<String, Object> valueOperations;
@Test
void assetInfoExcel_shouldExcludeAssetIdAndFamilyId() {
Set<String> fieldNames = Arrays.stream(CcdiAssetInfoExcel.class.getDeclaredFields())
.map(field -> field.getName())
.collect(Collectors.toSet());
assertFalse(fieldNames.contains("assetId"));
assertFalse(fieldNames.contains("familyId"));
assertTrue(fieldNames.contains("personId"));
}
@Test
void importAssetInfo_shouldUseDedicatedAssetTaskKeys() {
List<CcdiAssetInfoExcel> excelList = List.of(buildExcel("320101199001010011", "房产"));
when(redisTemplate.opsForHash()).thenReturn(hashOperations);
String taskId = service.importAssetInfo(excelList);
verify(hashOperations).putAll(eq("import:assetInfo:" + taskId), anyMap());
verify(redisTemplate).expire("import:assetInfo:" + taskId, 7, TimeUnit.DAYS);
verify(assetInfoImportService).importAssetInfoAsync(eq(excelList), eq(taskId), any());
}
@Test
void importAssetInfoAsync_shouldResolveFamilyIdFromEmployeeIdCard() {
CcdiAssetInfoExcel excel = buildExcel("320101199001010011", "房产");
when(redisTemplate.opsForHash()).thenReturn(hashOperations);
when(assetInfoMapper.selectOwnerByEmployeeIdCards(List.of("320101199001010011")))
.thenReturn(List.of(owner("320101199001010011", "320101199001010011")));
when(assetInfoMapper.selectOwnerByFamilyRelationIdCards(List.of("320101199001010011")))
.thenReturn(List.of());
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_shouldResolveFamilyIdFromFamilyRelationIdCard() {
CcdiAssetInfoExcel excel = buildExcel("320101199201010022", "车辆");
when(redisTemplate.opsForHash()).thenReturn(hashOperations);
when(assetInfoMapper.selectOwnerByEmployeeIdCards(List.of("320101199201010022")))
.thenReturn(List.of());
when(assetInfoMapper.selectOwnerByFamilyRelationIdCards(List.of("320101199201010022")))
.thenReturn(List.of(owner("320101199201010022", "320101199001010011")));
service.importAssetInfoAsync(List.of(excel), "task-2", "tester");
ArgumentCaptor<List<CcdiAssetInfo>> captor = ArgumentCaptor.forClass(List.class);
verify(assetInfoMapper).insertBatch(captor.capture());
assertEquals("320101199001010011", captor.getValue().get(0).getFamilyId());
assertEquals("320101199201010022", captor.getValue().get(0).getPersonId());
}
@Test
void importAssetInfoAsync_shouldStoreFailureRowsOnlyForBadRecords() {
CcdiAssetInfoExcel good = buildExcel("320101199001010011", "房产");
CcdiAssetInfoExcel bad = buildExcel("320101199001010099", "车辆");
when(redisTemplate.opsForHash()).thenReturn(hashOperations);
when(redisTemplate.opsForValue()).thenReturn(valueOperations);
when(assetInfoMapper.selectOwnerByEmployeeIdCards(List.of("320101199001010011", "320101199001010099")))
.thenReturn(List.of(owner("320101199001010011", "320101199001010011")));
when(assetInfoMapper.selectOwnerByFamilyRelationIdCards(List.of("320101199001010011", "320101199001010099")))
.thenReturn(List.of());
service.importAssetInfoAsync(List.of(good, bad), "task-3", "tester");
ArgumentCaptor<List<CcdiAssetInfo>> insertCaptor = ArgumentCaptor.forClass(List.class);
verify(assetInfoMapper).insertBatch(insertCaptor.capture());
assertEquals(1, insertCaptor.getValue().size());
assertEquals("320101199001010011", insertCaptor.getValue().get(0).getFamilyId());
ArgumentCaptor<Object> failureCaptor = ArgumentCaptor.forClass(Object.class);
verify(valueOperations).set(eq("import:assetInfo:task-3:failures"), failureCaptor.capture(), eq(7L), eq(TimeUnit.DAYS));
List<?> failures = (List<?>) failureCaptor.getValue();
assertEquals(1, failures.size());
AssetImportFailureVO failure = (AssetImportFailureVO) failures.get(0);
assertEquals("320101199001010099", failure.getPersonId());
assertTrue(failure.getErrorMessage().contains("未找到资产归属员工"));
}
@Test
void importAssetInfoAsync_shouldFailWhenOwnerIsAmbiguous() {
CcdiAssetInfoExcel excel = buildExcel("320101199201010022", "车辆");
when(redisTemplate.opsForHash()).thenReturn(hashOperations);
when(redisTemplate.opsForValue()).thenReturn(valueOperations);
when(assetInfoMapper.selectOwnerByEmployeeIdCards(List.of("320101199201010022")))
.thenReturn(List.of(
owner("320101199201010022", "320101199001010011"),
owner("320101199201010022", "320101199001010033")
));
when(assetInfoMapper.selectOwnerByFamilyRelationIdCards(List.of("320101199201010022")))
.thenReturn(List.of());
service.importAssetInfoAsync(List.of(excel), "task-4", "tester");
verify(assetInfoMapper, never()).insertBatch(any());
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("资产归属员工不唯一"));
}
@Test
void getImportStatusAndFailures_shouldUseAssetPrefixes() {
when(redisTemplate.opsForHash()).thenReturn(hashOperations);
when(redisTemplate.opsForValue()).thenReturn(valueOperations);
when(redisTemplate.hasKey("import:assetInfo:task-5")).thenReturn(true);
when(hashOperations.entries("import:assetInfo:task-5")).thenReturn(Map.of(
"taskId", "task-5",
"status", "SUCCESS",
"totalCount", 1,
"successCount", 1,
"failureCount", 0,
"progress", 100,
"startTime", 1L,
"endTime", 2L,
"message", "全部成功"
));
AssetImportFailureVO failureVO = new AssetImportFailureVO();
failureVO.setPersonId("320101199001010099");
failureVO.setErrorMessage("未找到资产归属员工");
when(valueOperations.get("import:assetInfo:task-5:failures")).thenReturn(List.of(failureVO));
ImportStatusVO statusVO = service.getImportStatus("task-5");
List<AssetImportFailureVO> failures = service.getImportFailures("task-5");
assertEquals("task-5", statusVO.getTaskId());
assertEquals("SUCCESS", statusVO.getStatus());
assertNotNull(failures);
assertEquals(1, failures.size());
assertEquals("320101199001010099", failures.get(0).getPersonId());
}
private CcdiAssetInfoExcel buildExcel(String personId, String assetMainType) {
CcdiAssetInfoExcel excel = new CcdiAssetInfoExcel();
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

@@ -0,0 +1,105 @@
package com.ruoyi.info.collection.service;
import com.ruoyi.info.collection.domain.CcdiAssetInfo;
import com.ruoyi.info.collection.domain.dto.CcdiAssetInfoDTO;
import com.ruoyi.info.collection.mapper.CcdiAssetInfoMapper;
import com.ruoyi.info.collection.service.impl.CcdiAssetInfoServiceImpl;
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 java.math.BigDecimal;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.mockito.ArgumentMatchers.anyList;
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 CcdiAssetInfoServiceImplTest {
@InjectMocks
private CcdiAssetInfoServiceImpl service;
@Mock
private CcdiAssetInfoMapper assetInfoMapper;
@Test
void selectByFamilyId_shouldReturnMapperResult() {
List<CcdiAssetInfo> expected = List.of(new CcdiAssetInfo());
when(assetInfoMapper.selectByFamilyId("320101199001010011")).thenReturn(expected);
List<CcdiAssetInfo> result = service.selectByFamilyId("320101199001010011");
assertSame(expected, result);
}
@Test
void replaceByFamilyId_shouldDeleteThenInsertNormalizedRows() {
CcdiAssetInfoDTO selfOwnedAsset = buildDto("320101199001010011", "房产");
CcdiAssetInfoDTO familyOwnedAsset = buildDto("320101199201010022", "车辆");
service.replaceByFamilyId("320101199001010011", List.of(selfOwnedAsset, familyOwnedAsset));
verify(assetInfoMapper).deleteByFamilyId("320101199001010011");
ArgumentCaptor<List<CcdiAssetInfo>> captor = ArgumentCaptor.forClass(List.class);
verify(assetInfoMapper).insertBatch(captor.capture());
List<CcdiAssetInfo> savedList = captor.getValue();
assertEquals(2, savedList.size());
assertEquals("320101199001010011", savedList.get(0).getFamilyId());
assertEquals("320101199001010011", savedList.get(0).getPersonId());
assertEquals("320101199001010011", savedList.get(1).getFamilyId());
assertEquals("320101199201010022", savedList.get(1).getPersonId());
assertEquals("房产", savedList.get(0).getAssetMainType());
assertEquals("车辆", savedList.get(1).getAssetMainType());
}
@Test
void replaceByFamilyId_shouldIgnoreEmptyRows() {
CcdiAssetInfoDTO emptyRow = new CcdiAssetInfoDTO();
service.replaceByFamilyId("320101199001010011", List.of(emptyRow));
verify(assetInfoMapper).deleteByFamilyId("320101199001010011");
verify(assetInfoMapper, never()).insertBatch(anyList());
}
@Test
void deleteByFamilyId_shouldDelegateToMapper() {
when(assetInfoMapper.deleteByFamilyId("320101199001010011")).thenReturn(1);
int result = service.deleteByFamilyId("320101199001010011");
assertEquals(1, result);
}
@Test
void deleteByFamilyIds_shouldDelegateToMapper() {
List<String> familyIds = List.of("320101199001010011", "320101199001010033");
when(assetInfoMapper.deleteByFamilyIds(familyIds)).thenReturn(2);
int result = service.deleteByFamilyIds(familyIds);
assertEquals(2, result);
verify(assetInfoMapper).deleteByFamilyIds(eq(familyIds));
}
private CcdiAssetInfoDTO buildDto(String personId, String assetMainType) {
CcdiAssetInfoDTO dto = new CcdiAssetInfoDTO();
dto.setPersonId(personId);
dto.setAssetMainType(assetMainType);
dto.setAssetSubType(assetMainType + "小类");
dto.setAssetName(assetMainType + "名称");
dto.setCurrentValue(new BigDecimal("100.00"));
dto.setAssetStatus("正常");
return dto;
}
}

View File

@@ -0,0 +1,45 @@
package com.ruoyi.info.collection.service;
import com.ruoyi.info.collection.domain.dto.CcdiAssetInfoDTO;
import com.ruoyi.info.collection.domain.dto.CcdiBaseStaffAddDTO;
import com.ruoyi.info.collection.domain.dto.CcdiBaseStaffEditDTO;
import com.ruoyi.info.collection.domain.vo.CcdiAssetInfoVO;
import com.ruoyi.info.collection.domain.vo.CcdiBaseStaffVO;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertSame;
class CcdiBaseStaffServiceAssetAggregationTest {
@Test
void addDto_shouldExposeAssetInfoList() {
CcdiBaseStaffAddDTO addDTO = new CcdiBaseStaffAddDTO();
List<CcdiAssetInfoDTO> assetInfoList = List.of(new CcdiAssetInfoDTO());
addDTO.setAssetInfoList(assetInfoList);
assertSame(assetInfoList, addDTO.getAssetInfoList());
}
@Test
void editDto_shouldExposeAssetInfoList() {
CcdiBaseStaffEditDTO editDTO = new CcdiBaseStaffEditDTO();
List<CcdiAssetInfoDTO> assetInfoList = List.of(new CcdiAssetInfoDTO());
editDTO.setAssetInfoList(assetInfoList);
assertSame(assetInfoList, editDTO.getAssetInfoList());
}
@Test
void staffVo_shouldExposeAssetInfoList() {
CcdiBaseStaffVO staffVO = new CcdiBaseStaffVO();
List<CcdiAssetInfoVO> assetInfoList = List.of(new CcdiAssetInfoVO());
staffVO.setAssetInfoList(assetInfoList);
assertSame(assetInfoList, staffVO.getAssetInfoList());
}
}

View File

@@ -0,0 +1,185 @@
package com.ruoyi.info.collection.service;
import com.ruoyi.info.collection.domain.CcdiAssetInfo;
import com.ruoyi.info.collection.domain.CcdiBaseStaff;
import com.ruoyi.info.collection.domain.dto.CcdiAssetInfoDTO;
import com.ruoyi.info.collection.domain.dto.CcdiBaseStaffAddDTO;
import com.ruoyi.info.collection.domain.dto.CcdiBaseStaffEditDTO;
import com.ruoyi.info.collection.domain.vo.CcdiBaseStaffVO;
import com.ruoyi.info.collection.mapper.CcdiBaseStaffMapper;
import com.ruoyi.info.collection.service.impl.CcdiBaseStaffServiceImpl;
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.RedisTemplate;
import java.math.BigDecimal;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.mockito.ArgumentMatchers.any;
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 CcdiBaseStaffServiceImplTest {
@InjectMocks
private CcdiBaseStaffServiceImpl service;
@Mock
private CcdiBaseStaffMapper baseStaffMapper;
@Mock
private ICcdiBaseStaffImportService importAsyncService;
@Mock
private RedisTemplate<String, Object> redisTemplate;
@Mock
private ICcdiAssetInfoService assetInfoService;
@Test
void insertBaseStaff_shouldPersistEmployeeThenReplaceAssetsUsingEmployeeIdCard() {
CcdiBaseStaffAddDTO addDTO = new CcdiBaseStaffAddDTO();
addDTO.setStaffId(1001L);
addDTO.setName("张三");
addDTO.setDeptId(10L);
addDTO.setIdCard("320101199001010011");
addDTO.setPhone("13812345678");
addDTO.setStatus("0");
addDTO.setAssetInfoList(List.of(
buildAssetDto("320101199001010011", "房产"),
buildAssetDto("320101199201010022", "车辆")
));
when(baseStaffMapper.selectById(1001L)).thenReturn(null);
when(baseStaffMapper.selectCount(any())).thenReturn(0L);
when(baseStaffMapper.insert(any(CcdiBaseStaff.class))).thenReturn(1);
int result = service.insertBaseStaff(addDTO);
assertEquals(1, result);
ArgumentCaptor<List<CcdiAssetInfoDTO>> captor = ArgumentCaptor.forClass(List.class);
verify(assetInfoService).replaceByFamilyId(eq("320101199001010011"), captor.capture());
List<CcdiAssetInfoDTO> savedAssets = captor.getValue();
assertEquals(2, savedAssets.size());
assertEquals("320101199001010011", savedAssets.get(0).getPersonId());
assertEquals("320101199201010022", savedAssets.get(1).getPersonId());
}
@Test
void updateBaseStaff_shouldReplaceAssetsForCurrentIdCard() {
CcdiBaseStaff existing = new CcdiBaseStaff();
existing.setStaffId(1001L);
existing.setIdCard("320101199001010011");
CcdiBaseStaffEditDTO editDTO = new CcdiBaseStaffEditDTO();
editDTO.setStaffId(1001L);
editDTO.setDeptId(10L);
editDTO.setName("张三");
editDTO.setIdCard("320101199001010011");
editDTO.setPhone("13812345678");
editDTO.setStatus("0");
editDTO.setAssetInfoList(List.of(buildAssetDto("320101199201010022", "车辆")));
when(baseStaffMapper.selectById(1001L)).thenReturn(existing);
when(baseStaffMapper.selectCount(any())).thenReturn(0L);
when(baseStaffMapper.updateById(any(CcdiBaseStaff.class))).thenReturn(1);
int result = service.updateBaseStaff(editDTO);
assertEquals(1, result);
verify(assetInfoService, never()).deleteByFamilyId("320101199001010011");
verify(assetInfoService).replaceByFamilyId("320101199001010011", editDTO.getAssetInfoList());
}
@Test
void updateBaseStaff_shouldDeleteOldAssetsWhenIdCardChanges() {
CcdiBaseStaff existing = new CcdiBaseStaff();
existing.setStaffId(1001L);
existing.setIdCard("320101199001010099");
CcdiBaseStaffEditDTO editDTO = new CcdiBaseStaffEditDTO();
editDTO.setStaffId(1001L);
editDTO.setDeptId(10L);
editDTO.setName("张三");
editDTO.setIdCard("320101199001010011");
editDTO.setPhone("13812345678");
editDTO.setStatus("0");
editDTO.setAssetInfoList(List.of(buildAssetDto("320101199201010022", "车辆")));
when(baseStaffMapper.selectById(1001L)).thenReturn(existing);
when(baseStaffMapper.selectCount(any())).thenReturn(0L);
when(baseStaffMapper.updateById(any(CcdiBaseStaff.class))).thenReturn(1);
service.updateBaseStaff(editDTO);
verify(assetInfoService).deleteByFamilyId("320101199001010099");
verify(assetInfoService).replaceByFamilyId("320101199001010011", editDTO.getAssetInfoList());
}
@Test
void selectBaseStaffById_shouldReturnAssetInfoList() {
CcdiBaseStaff staff = new CcdiBaseStaff();
staff.setStaffId(1001L);
staff.setName("张三");
staff.setIdCard("320101199001010011");
staff.setStatus("0");
CcdiAssetInfo assetInfo = new CcdiAssetInfo();
assetInfo.setFamilyId("320101199001010011");
assetInfo.setPersonId("320101199201010022");
assetInfo.setAssetMainType("车辆");
assetInfo.setAssetSubType("小汽车");
assetInfo.setAssetName("家庭车辆");
assetInfo.setCurrentValue(new BigDecimal("100000.00"));
assetInfo.setAssetStatus("正常");
when(baseStaffMapper.selectById(1001L)).thenReturn(staff);
when(assetInfoService.selectByFamilyId("320101199001010011")).thenReturn(List.of(assetInfo));
CcdiBaseStaffVO result = service.selectBaseStaffById(1001L);
assertNotNull(result.getAssetInfoList());
assertEquals(1, result.getAssetInfoList().size());
assertEquals("320101199201010022", result.getAssetInfoList().get(0).getPersonId());
assertEquals("车辆", result.getAssetInfoList().get(0).getAssetMainType());
}
@Test
void deleteBaseStaffByIds_shouldCascadeDeleteAssets() {
CcdiBaseStaff staff1 = new CcdiBaseStaff();
staff1.setStaffId(1001L);
staff1.setIdCard("320101199001010011");
CcdiBaseStaff staff2 = new CcdiBaseStaff();
staff2.setStaffId(1002L);
staff2.setIdCard("320101199001010022");
when(baseStaffMapper.selectBatchIds(List.of(1001L, 1002L))).thenReturn(List.of(staff1, staff2));
when(baseStaffMapper.deleteBatchIds(List.of(1001L, 1002L))).thenReturn(2);
int result = service.deleteBaseStaffByIds(new Long[]{1001L, 1002L});
assertEquals(2, result);
verify(assetInfoService).deleteByFamilyIds(List.of("320101199001010011", "320101199001010022"));
}
private CcdiAssetInfoDTO buildAssetDto(String personId, String assetMainType) {
CcdiAssetInfoDTO dto = new CcdiAssetInfoDTO();
dto.setPersonId(personId);
dto.setAssetMainType(assetMainType);
dto.setAssetSubType(assetMainType + "小类");
dto.setAssetName(assetMainType + "名称");
dto.setCurrentValue(new BigDecimal("100.00"));
dto.setAssetStatus("正常");
return dto;
}
}