提交资产导入拆分当前进展

This commit is contained in:
wkc
2026-03-13 16:07:51 +08:00
parent 80b2f1b39a
commit ee31f74aef
10 changed files with 794 additions and 23 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.hasPermi('ccdi:staffFmyRelation:import')")
@Log(title = "亲属资产信息", businessType = BusinessType.IMPORT)
@Operation(summary = "导入资产信息")
@PreAuthorize("@ss.hasAnyPermi('ccdi:employee:import,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.hasPermi('ccdi:staffFmyRelation:import')")
@Operation(summary = "查询资产导入状态")
@PreAuthorize("@ss.hasAnyPermi('ccdi:employee:import,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.hasPermi('ccdi:staffFmyRelation:import')")
@Operation(summary = "查询资产导入失败记录")
@PreAuthorize("@ss.hasAnyPermi('ccdi:employee:import,ccdi:staffFmyRelation:import')")
@GetMapping("/importFailures/{taskId}")
public TableDataInfo getImportFailures(
@PathVariable String taskId,

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

@@ -75,6 +75,14 @@ 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);
/**
* 按关系人证件号查询归属员工候选
*

View File

@@ -97,10 +97,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();
@@ -164,7 +164,19 @@ public class CcdiAssetInfoImportServiceImpl implements ICcdiAssetInfoImportServi
private Map<String, Set<String>> buildOwnerMap(List<String> personIds) {
Map<String, Set<String>> result = new LinkedHashMap<>();
mergeOwnerMappings(result, assetInfoMapper.selectOwnerCandidatesByRelationCertNos(personIds));
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));
}
return result;
}
@@ -184,7 +196,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

@@ -77,6 +77,17 @@
</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,

View File

@@ -109,11 +109,11 @@ class CcdiAssetInfoControllerTest {
}
@Test
void importTemplate_shouldUseRelativeAssetTemplateName() {
void importTemplate_shouldUseGenericAssetTemplateName() {
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

@@ -96,6 +96,22 @@ class CcdiAssetInfoImportServiceImplTest {
assertEquals("320101199001010011", captor.getValue().get(0).getPersonId());
}
@Test
void importAssetInfoAsync_shouldResolveFamilyIdFromEmployeeIdCardBeforeFamilyRelation() {
CcdiAssetInfoExcel excel = buildExcel("320101199001010011", "房产");
when(redisTemplate.opsForHash()).thenReturn(hashOperations);
when(assetInfoMapper.selectOwnerCandidatesByPersonIds(List.of("320101199001010011")))
.thenReturn(List.of(owner("320101199001010011", "320101199001010011")));
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());
}
@Test
void importAssetInfoAsync_shouldResolveFamilyIdFromFamilyRelationIdCard() {
CcdiAssetInfoExcel excel = buildExcel("320101199201010022", "车辆");
@@ -134,7 +150,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
@@ -154,7 +170,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
@@ -175,7 +191,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,253 @@
# 员工资产导入与亲属资产导入拆分后端实施计划
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** 将员工资产导入与亲属资产导入拆成两套独立后端接口与服务,确保员工页仅导入本人资产、亲属页仅导入亲属资产。
**Architecture:** 保留现有亲属资产导入控制器与服务作为“亲属专用导入链路”新增一套员工资产专用导入控制器、Excel 模型、失败记录 VO 与异步导入服务。两套链路分别使用不同权限、模板和归属匹配规则,不再共享“本人/亲属兜底识别”逻辑。
**Tech Stack:** Java 21, Spring Boot 3, MyBatis Plus, Redis, EasyExcel, JUnit 5, Mockito
---
### Task 1: 固化拆分后的导入规则测试
**Files:**
- Modify: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiAssetInfoImportServiceImplTest.java`
- Create: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiBaseStaffAssetImportServiceImplTest.java`
- Modify: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/controller/CcdiAssetInfoControllerTest.java`
- Create: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/controller/CcdiBaseStaffAssetImportControllerTest.java`
**Step 1: 写亲属资产导入失败测试**
-`CcdiAssetInfoImportServiceImplTest` 增加用例:
- 员工本人身份证号导入时失败
- 失败文案为亲属资产专用文案
-`CcdiAssetInfoControllerTest` 校验模板标题为“亲属资产信息”
**Step 2: 运行测试确认当前失败**
Run:
```bash
mvn test -pl ccdi-info-collection -am "-Dtest=CcdiAssetInfoImportServiceImplTest,CcdiAssetInfoControllerTest" "-Dsurefire.failIfNoSpecifiedTests=false"
```
Expected:
- 新增断言失败
- 失败原因是当前实现仍允许员工本人资产命中
**Step 3: 写员工资产导入测试**
- 新增 `CcdiBaseStaffAssetImportServiceImplTest`
- 覆盖以下场景:
- 员工本人身份证号导入成功
- 亲属证件号导入失败
- Redis key 使用员工资产独立前缀
- 新增 `CcdiBaseStaffAssetImportControllerTest`
- 覆盖模板标题、任务创建、状态与失败记录查询
**Step 4: 再次运行测试确认失败点准确**
Run:
```bash
mvn test -pl ccdi-info-collection -am "-Dtest=CcdiBaseStaffAssetImportServiceImplTest,CcdiBaseStaffAssetImportControllerTest" "-Dsurefire.failIfNoSpecifiedTests=false"
```
Expected:
- 因类与实现尚不存在而失败
**Step 5: 提交测试脚手架**
```bash
git add ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiAssetInfoImportServiceImplTest.java ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiBaseStaffAssetImportServiceImplTest.java ccdi-info-collection/src/test/java/com/ruoyi/info/collection/controller/CcdiAssetInfoControllerTest.java ccdi-info-collection/src/test/java/com/ruoyi/info/collection/controller/CcdiBaseStaffAssetImportControllerTest.java
git commit -m "补充资产导入拆分后端失败测试"
```
### Task 2: 收敛亲属资产导入为亲属专用链路
**Files:**
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiAssetInfoController.java`
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiAssetInfoImportServiceImpl.java`
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/excel/CcdiAssetInfoExcel.java`
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/AssetImportFailureVO.java`
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/mapper/CcdiAssetInfoMapper.java`
- Modify: `ccdi-info-collection/src/main/resources/mapper/info/collection/CcdiAssetInfoMapper.xml`
**Step 1: 最小实现修改**
-`CcdiAssetInfoImportServiceImpl` 的归属匹配改为只调用 `selectOwnerCandidatesByRelationCertNos`
- 删除员工本人兜底逻辑
- 恢复亲属资产专用错误文案
- 将 Excel 首列表头改为“亲属证件号*”
- 将 controller 标题、swagger 文案、日志标题改回亲属资产专用表述
- 权限仅保留 `ccdi:staffFmyRelation:import`
**Step 2: 运行亲属资产相关测试**
Run:
```bash
mvn test -pl ccdi-info-collection -am "-Dtest=CcdiAssetInfoImportServiceImplTest,CcdiAssetInfoControllerTest" "-Dsurefire.failIfNoSpecifiedTests=false"
```
Expected:
- 亲属资产测试全部通过
**Step 3: 检查无多余员工导入逻辑残留**
Run:
```bash
git grep -n "selectOwnerCandidatesByPersonIds|hasAnyPermi('ccdi:employee:import" -- "ccdi-info-collection/src/main/java/**/*.java" "ccdi-info-collection/src/main/resources/**/*.xml"
```
Expected:
- `CcdiAssetInfo*` 相关文件不再包含员工资产导入特有逻辑
**Step 4: 提交**
```bash
git add ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiAssetInfoController.java ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiAssetInfoImportServiceImpl.java ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/excel/CcdiAssetInfoExcel.java ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/AssetImportFailureVO.java ccdi-info-collection/src/main/java/com/ruoyi/info/collection/mapper/CcdiAssetInfoMapper.java ccdi-info-collection/src/main/resources/mapper/info/collection/CcdiAssetInfoMapper.xml
git commit -m "收敛亲属资产导入为亲属专用逻辑"
```
### Task 3: 新增员工资产导入后端接口与模型
**Files:**
- Create: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiBaseStaffAssetImportController.java`
- Create: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiBaseStaffAssetImportService.java`
- Create: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiBaseStaffAssetImportServiceImpl.java`
- Create: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/excel/CcdiBaseStaffAssetInfoExcel.java`
- Create: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/BaseStaffAssetImportFailureVO.java`
**Step 1: 写最小接口定义**
- 定义员工资产导入 service 接口:
- `importAssetInfo`
- `importAssetInfoAsync`
- `getImportStatus`
- `getImportFailures`
**Step 2: 写 controller 最小实现**
- 提供以下接口:
- `POST /ccdi/baseStaff/asset/importTemplate`
- `POST /ccdi/baseStaff/asset/importData`
- `GET /ccdi/baseStaff/asset/importStatus/{taskId}`
- `GET /ccdi/baseStaff/asset/importFailures/{taskId}`
- 权限使用 `ccdi:employee:import`
**Step 3: 写 Excel 与失败记录模型**
- `CcdiBaseStaffAssetInfoExcel` 首列使用“员工身份证号*”
- `BaseStaffAssetImportFailureVO` 字段与员工资产模板保持一致
**Step 4: 运行员工资产 controller 测试**
Run:
```bash
mvn test -pl ccdi-info-collection -am "-Dtest=CcdiBaseStaffAssetImportControllerTest" "-Dsurefire.failIfNoSpecifiedTests=false"
```
Expected:
- controller 测试通过
**Step 5: 提交**
```bash
git add ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiBaseStaffAssetImportController.java ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiBaseStaffAssetImportService.java ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiBaseStaffAssetImportServiceImpl.java ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/excel/CcdiBaseStaffAssetInfoExcel.java ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/BaseStaffAssetImportFailureVO.java
git commit -m "新增员工资产导入后端接口"
```
### Task 4: 实现员工资产归属匹配与异步导入
**Files:**
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiBaseStaffAssetImportServiceImpl.java`
- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/mapper/CcdiAssetInfoMapper.java`
- Modify: `ccdi-info-collection/src/main/resources/mapper/info/collection/CcdiAssetInfoMapper.xml`
**Step 1: 写最小匹配实现**
- 在 mapper 中新增 `selectOwnerCandidatesByBaseStaffIdCards`
- SQL 只查询 `ccdi_base_staff.id_card`
- service 只按员工身份证号匹配
- 不查亲属关系表
**Step 2: 写导入成功逻辑**
- 复制 Excel 到 `CcdiAssetInfo`
- 强制 `familyId = personId = 员工身份证号`
- 使用独立 Redis 前缀,例如 `import:baseStaffAsset:`
**Step 3: 写失败逻辑**
- 未命中员工表时报错
- 失败文案使用员工资产专用文案
**Step 4: 运行员工资产 service 测试**
Run:
```bash
mvn test -pl ccdi-info-collection -am "-Dtest=CcdiBaseStaffAssetImportServiceImplTest" "-Dsurefire.failIfNoSpecifiedTests=false"
```
Expected:
- 员工资产 service 测试通过
**Step 5: 提交**
```bash
git add ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiBaseStaffAssetImportServiceImpl.java ccdi-info-collection/src/main/java/com/ruoyi/info/collection/mapper/CcdiAssetInfoMapper.java ccdi-info-collection/src/main/resources/mapper/info/collection/CcdiAssetInfoMapper.xml
git commit -m "实现员工资产导入归属匹配"
```
### Task 5: 执行回归验证
**Files:**
- Modify: `docs/plans/2026-03-13-employee-family-asset-import-split-design.md`
**Step 1: 运行后端定向测试**
Run:
```bash
mvn test -pl ccdi-info-collection -am "-Dtest=CcdiAssetInfoImportServiceImplTest,CcdiAssetInfoControllerTest,CcdiBaseStaffAssetImportServiceImplTest,CcdiBaseStaffAssetImportControllerTest" "-Dsurefire.failIfNoSpecifiedTests=false"
```
Expected:
- 相关测试全部通过
**Step 2: 做源码断言检查**
Run:
```bash
git grep -n "/ccdi/baseStaff/asset|/ccdi/assetInfo" -- "ccdi-info-collection/src/main/java/**/*.java"
```
Expected:
- 员工与亲属两套接口都存在
- 路由职责清晰
**Step 3: 更新设计文档的实现状态说明**
- 在设计文档末尾补充“已完成实现验证”的简短说明
**Step 4: 提交**
```bash
git add docs/plans/2026-03-13-employee-family-asset-import-split-design.md
git commit -m "完成资产导入拆分后端验证"
```

View File

@@ -0,0 +1,241 @@
# 员工资产导入与亲属资产导入拆分设计
## 背景
当前员工信息维护页 [ccdiBaseStaff/index.vue](/D:/ccdi/ccdi/ruoyi-ui/src/views/ccdiBaseStaff/index.vue) 与员工亲属关系维护页 [ccdiStaffFmyRelation/index.vue](/D:/ccdi/ccdi/ruoyi-ui/src/views/ccdiStaffFmyRelation/index.vue) 共用了 `/ccdi/assetInfo/*` 资产导入接口,导致两类业务边界混淆:
- 员工页的“导入资产信息”应只导入员工本人资产
- 亲属页的“导入亲属资产信息”应只导入员工亲属资产
- 当前共享接口无法同时满足这两条规则,模板、权限、失败文案也容易串用
用户已确认:
- 保留员工页“导入资产信息”按钮
- 员工资产导入与亲属资产导入必须彻底拆分
- 员工亲属资产导入功能只能导入员工亲属的资产信息,不能更新员工的
## 目标
- 将员工资产导入与亲属资产导入拆成两条独立链路
- 员工页只支持员工本人资产导入
- 亲属页只支持亲属资产导入
- 模板、接口、权限、失败提示、任务状态缓存全部分离
## 非目标
- 不改造员工资产手工维护功能
- 不改造亲属资产手工维护功能
- 不新增独立资产菜单页面
- 不调整 `ccdi_asset_info` 表结构
## 现状
当前共用资产导入能力集中在:
- 前端 API[ccdiAssetInfo.js](/D:/ccdi/ccdi/ruoyi-ui/src/api/ccdiAssetInfo.js)
- 员工页调用点:[ccdiBaseStaff/index.vue](/D:/ccdi/ccdi/ruoyi-ui/src/views/ccdiBaseStaff/index.vue)
- 亲属页调用点:[ccdiStaffFmyRelation/index.vue](/D:/ccdi/ccdi/ruoyi-ui/src/views/ccdiStaffFmyRelation/index.vue)
- 后端控制器:[CcdiAssetInfoController.java](/D:/ccdi/ccdi/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiAssetInfoController.java)
- 后端导入服务:[CcdiAssetInfoImportServiceImpl.java](/D:/ccdi/ccdi/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiAssetInfoImportServiceImpl.java)
问题点:
- 员工页与亲属页共用同一上传地址 `/ccdi/assetInfo/importData`
- 共用同一模板下载地址 `/ccdi/assetInfo/importTemplate`
- 共用同一任务状态与失败记录查询入口
- 共用同一导入匹配规则,无法表达“员工页仅员工本人、亲属页仅亲属”
## 方案对比
### 方案一:员工资产导入、亲属资产导入完全拆分
- 员工页新增一套独立导入接口
- 亲属页保留现有 `/ccdi/assetInfo/*`
- 两边各自使用独立模板、权限、校验和失败文案
优点:
- 业务边界最清晰
- 后续维护风险最低
- 模板与前端交互不再串用
缺点:
- 需要新增一套员工资产导入 controller/service/api
### 方案二:继续共用接口,通过 `mode` 区分
- 前端调用同一接口
- 后端通过 `mode=employee/family` 分支处理
优点:
- 代码新增较少
缺点:
- 控制器和 service 内分支复杂
- 模板、权限、提示文案仍容易混淆
- 后续扩展时回归风险高
### 方案三:仅修前端入口文案
优点:
- 改动最小
缺点:
- 业务问题未解决
- 实际导入规则仍然混乱
## 最终方案
采用方案一:员工资产导入与亲属资产导入完全拆分。
### 员工资产导入
- 页面入口:员工信息维护页
- 独立接口:
- `POST /ccdi/baseStaff/asset/importTemplate`
- `POST /ccdi/baseStaff/asset/importData`
- `GET /ccdi/baseStaff/asset/importStatus/{taskId}`
- `GET /ccdi/baseStaff/asset/importFailures/{taskId}`
- 独立前端 API 文件:`ruoyi-ui/src/api/ccdiBaseStaffAsset.js`
- 仅允许导入员工本人资产
- 模板第一列要求填写员工身份证号
- 导入成功后强制写入:
- `family_id = 员工身份证号`
- `person_id = 员工身份证号`
### 亲属资产导入
- 页面入口:员工亲属关系维护页
- 保留现有接口:
- `POST /ccdi/assetInfo/importTemplate`
- `POST /ccdi/assetInfo/importData`
- `GET /ccdi/assetInfo/importStatus/{taskId}`
- `GET /ccdi/assetInfo/importFailures/{taskId}`
- 仅允许导入员工亲属资产
- 模板第一列要求填写亲属证件号
- 导入成功后强制写入:
- `family_id = 关联员工证件号`
- `person_id = 亲属证件号`
## 后端设计
### 新增员工资产导入控制面
新增:
- `controller/CcdiBaseStaffAssetImportController.java`
- `service/ICcdiBaseStaffAssetImportService.java`
- `service/impl/CcdiBaseStaffAssetImportServiceImpl.java`
- `domain/excel/CcdiBaseStaffAssetInfoExcel.java`
- `domain/vo/BaseStaffAssetImportFailureVO.java`
员工资产导入匹配规则:
- 仅根据 `ccdi_base_staff.id_card` 匹配
- 若未匹配到员工,导入失败
- 不再兜底匹配亲属关系表
- 若命中员工,则写入 `family_id = person_id = id_card`
亲属资产导入规则调整:
- [CcdiAssetInfoImportServiceImpl.java](/D:/ccdi/ccdi/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiAssetInfoImportServiceImpl.java) 只保留亲属资产逻辑
- 仅根据 `ccdi_staff_fmy_relation.relation_cert_no` 匹配
- 不再匹配员工本人身份证号
### 权限设计
- 员工资产导入接口:`ccdi:employee:import`
- 亲属资产导入接口:`ccdi:staffFmyRelation:import`
禁止再使用同时兼容两个权限的写法,以免接口语义再次混淆。
## 前端设计
### 员工页
[ccdiBaseStaff/index.vue](/D:/ccdi/ccdi/ruoyi-ui/src/views/ccdiBaseStaff/index.vue) 改为:
- 上传地址改为员工资产专用接口
- 模板下载改为员工资产模板
- 任务状态和失败记录查询改为员工资产专用接口
- 提示文案明确为“员工资产数据导入”
### 亲属页
[ccdiStaffFmyRelation/index.vue](/D:/ccdi/ccdi/ruoyi-ui/src/views/ccdiStaffFmyRelation/index.vue) 保持:
- 上传地址仍为 `/ccdi/assetInfo/importData`
- 模板下载仍为亲属资产模板
- 文案继续强调“亲属资产”
## 模板设计
员工资产模板:
- 文件名:`员工资产信息模板_xxx.xlsx`
- 首列表头:`员工身份证号*`
亲属资产模板:
- 文件名:`亲属资产信息模板_xxx.xlsx`
- 首列表头:`亲属证件号*`
## 校验规则
### 员工资产导入
- 员工身份证号不能为空
- 资产必填字段不能为空
- 员工身份证号必须存在于 `ccdi_base_staff.id_card`
- 若填写的是亲属证件号或其他未命中的证件号,直接失败
建议失败文案:
- `未找到员工资产归属员工`
- 或更明确的 `员工资产导入仅支持员工本人证件号`
### 亲属资产导入
- 亲属证件号不能为空
- 资产必填字段不能为空
- 亲属证件号必须存在于 `ccdi_staff_fmy_relation.relation_cert_no`
- 若填写员工本人身份证号且不存在亲属关系映射,直接失败
- 若同一亲属证件号匹配多个员工关系,失败并提示归属不唯一
## 测试要求
后端:
- 员工资产导入:员工本人证件号成功
- 员工资产导入:亲属证件号失败
- 亲属资产导入:亲属证件号成功
- 亲属资产导入:员工本人证件号失败
- 两套模板标题和首列表头不同
- 两套接口权限分别正确
前端:
- 员工页使用员工资产专用上传地址
- 亲属页继续使用 `/ccdi/assetInfo/*`
- 员工页下载员工资产模板
- 亲属页下载亲属资产模板
- 员工页和亲属页的失败记录弹窗文案不混淆
## 风险与回滚
风险:
- 当前仓库内已有共享资产导入代码,拆分时容易遗漏某一处调用
- 若只拆后端不拆前端,页面会继续指向旧接口
- 若模板文案未同步,用户仍可能误用模板
回滚策略:
- 独立提交员工资产导入新增改动
- 独立提交亲属资产导入收敛改动
- 任一阶段出现回归,可按提交粒度回退

View File

@@ -0,0 +1,230 @@
# 员工资产导入与亲属资产导入拆分前端实施计划
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** 将员工页与亲属页的资产导入前端交互彻底拆开,确保员工页只走员工资产导入接口,亲属页只走亲属资产导入接口。
**Architecture:** 保留亲属页现有资产导入状态管理,新增员工资产专用 API 封装并改造员工页上传地址、模板下载、状态轮询与失败记录查询。两边继续复用现有异步导入交互样式,但数据源和文案完全隔离。
**Tech Stack:** Vue 2, Element UI, Axios request wrapper, Node 静态契约测试
---
### Task 1: 固化前端拆分契约测试
**Files:**
- Modify: `ruoyi-ui/tests/unit/employee-asset-api-contract.test.js`
- Modify: `ruoyi-ui/tests/unit/employee-asset-import-ui.test.js`
- Modify: `ruoyi-ui/tests/unit/staff-family-asset-api-contract.test.js`
- Modify: `ruoyi-ui/tests/unit/staff-family-asset-detail-import-ui.test.js`
**Step 1: 写员工页失败测试**
- 断言员工页不再引用 `/ccdi/assetInfo/importData`
- 断言员工页引用新的员工资产 API 文件或路由
- 断言模板文案为“员工资产模板”
**Step 2: 运行员工页静态测试确认失败**
Run:
```bash
node tests/unit/employee-asset-api-contract.test.js
node tests/unit/employee-asset-import-ui.test.js
```
Expected:
- 至少一个断言失败
- 失败原因是员工页仍指向旧接口
**Step 3: 写亲属页保护性测试**
- 断言亲属页继续使用 `/ccdi/assetInfo/*`
- 断言亲属页模板文案仍为“亲属资产”
**Step 4: 运行亲属页测试确认当前仍通过**
Run:
```bash
node tests/unit/staff-family-asset-api-contract.test.js
node tests/unit/staff-family-asset-detail-import-ui.test.js
```
Expected:
- 现有亲属页测试通过
**Step 5: 提交**
```bash
git add ruoyi-ui/tests/unit/employee-asset-api-contract.test.js ruoyi-ui/tests/unit/employee-asset-import-ui.test.js ruoyi-ui/tests/unit/staff-family-asset-api-contract.test.js ruoyi-ui/tests/unit/staff-family-asset-detail-import-ui.test.js
git commit -m "补充资产导入拆分前端失败测试"
```
### Task 2: 新增员工资产导入 API 封装
**Files:**
- Create: `ruoyi-ui/src/api/ccdiBaseStaffAsset.js`
- Modify: `ruoyi-ui/tests/unit/employee-asset-api-contract.test.js`
**Step 1: 写最小 API 文件**
导出以下方法:
```javascript
export function importBaseStaffAssetTemplate() {}
export function importBaseStaffAssetData(data) {}
export function getBaseStaffAssetImportStatus(taskId) {}
export function getBaseStaffAssetImportFailures(taskId, pageNum, pageSize) {}
```
**Step 2: 接入员工专用路由**
- `/ccdi/baseStaff/asset/importTemplate`
- `/ccdi/baseStaff/asset/importData`
- `/ccdi/baseStaff/asset/importStatus/`
- `/ccdi/baseStaff/asset/importFailures/`
**Step 3: 运行员工资产 API 静态测试**
Run:
```bash
node tests/unit/employee-asset-api-contract.test.js
```
Expected:
- API 契约测试通过
**Step 4: 提交**
```bash
git add ruoyi-ui/src/api/ccdiBaseStaffAsset.js ruoyi-ui/tests/unit/employee-asset-api-contract.test.js
git commit -m "新增员工资产导入前端接口"
```
### Task 3: 改造员工页导入交互
**Files:**
- Modify: `ruoyi-ui/src/views/ccdiBaseStaff/index.vue`
- Modify: `ruoyi-ui/tests/unit/employee-asset-import-ui.test.js`
**Step 1: 替换员工页上传地址**
- `assetUpload.url` 改为 `/ccdi/baseStaff/asset/importData`
- 模板下载改为 `/ccdi/baseStaff/asset/importTemplate`
- 状态查询与失败记录改为员工资产专用 API
**Step 2: 保持状态隔离**
- 保留 `assetUpload``assetPollingTimer``assetCurrentTaskId`
- 仅替换其数据来源
- 不改动普通员工导入状态
**Step 3: 调整员工页提示文案**
- 提示用户仅支持员工本人资产导入
- 下载链接文字明确为“下载员工资产模板”
**Step 4: 运行员工页静态测试**
Run:
```bash
node tests/unit/employee-asset-import-ui.test.js
```
Expected:
- 测试通过
**Step 5: 提交**
```bash
git add ruoyi-ui/src/views/ccdiBaseStaff/index.vue ruoyi-ui/tests/unit/employee-asset-import-ui.test.js
git commit -m "切换员工页资产导入到专用接口"
```
### Task 4: 保护亲属页导入交互不回归
**Files:**
- Modify: `ruoyi-ui/src/views/ccdiStaffFmyRelation/index.vue`
- Modify: `ruoyi-ui/tests/unit/staff-family-asset-api-contract.test.js`
- Modify: `ruoyi-ui/tests/unit/staff-family-asset-detail-import-ui.test.js`
**Step 1: 检查亲属页调用**
- 确认亲属页仍使用 `/ccdi/assetInfo/importData`
- 确认模板下载仍使用 `/ccdi/assetInfo/importTemplate`
- 确认提示文案仍强调“亲属资产”
**Step 2: 如有必要补充只读修正**
- 若之前误改了亲属页文案或下载文件名,恢复为亲属专用表达
**Step 3: 运行亲属页静态测试**
Run:
```bash
node tests/unit/staff-family-asset-api-contract.test.js
node tests/unit/staff-family-asset-detail-import-ui.test.js
```
Expected:
- 测试通过
**Step 4: 提交**
```bash
git add ruoyi-ui/src/views/ccdiStaffFmyRelation/index.vue ruoyi-ui/tests/unit/staff-family-asset-api-contract.test.js ruoyi-ui/tests/unit/staff-family-asset-detail-import-ui.test.js
git commit -m "保护亲属页资产导入交互不回归"
```
### Task 5: 执行前端回归验证
**Files:**
- Modify: `docs/plans/2026-03-13-employee-family-asset-import-split-design.md`
**Step 1: 运行全部相关静态测试**
Run:
```bash
node tests/unit/employee-asset-api-contract.test.js
node tests/unit/employee-asset-import-ui.test.js
node tests/unit/staff-family-asset-api-contract.test.js
node tests/unit/staff-family-asset-detail-import-ui.test.js
```
Expected:
- 四个测试全部通过
**Step 2: 做源码检查**
Run:
```bash
git grep -n "/ccdi/baseStaff/asset|/ccdi/assetInfo" -- "ruoyi-ui/src/views/**/*.vue" "ruoyi-ui/src/api/*.js"
```
Expected:
- 员工页仅指向 `/ccdi/baseStaff/asset/*`
- 亲属页仅指向 `/ccdi/assetInfo/*`
**Step 3: 更新设计文档实现状态**
- 在设计文档末尾补充前端已完成拆分验证说明
**Step 4: 提交**
```bash
git add docs/plans/2026-03-13-employee-family-asset-import-split-design.md
git commit -m "完成资产导入拆分前端验证"
```