From 0541ce0ac6fa10f2105fa5ae6d9aff869f30b8c0 Mon Sep 17 00:00:00 2001 From: wkc <978997012@qq.com> Date: Wed, 6 May 2026 18:02:19 +0800 Subject: [PATCH] =?UTF-8?q?=E8=AE=A1=E5=88=92:=20=E5=91=98=E5=B7=A5?= =?UTF-8?q?=E8=B5=84=E4=BA=A7=E5=AF=BC=E5=85=A5=E4=B8=8E=E5=AE=9E=E4=BD=93?= =?UTF-8?q?=E5=BA=93=E8=87=AA=E5=8A=A8=E8=A1=A5=E5=85=A5=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...utofill-fix-backend-implementation-plan.md | 1409 +++++++++++++++++ 1 file changed, 1409 insertions(+) create mode 100644 docs/plans/backend/2026-05-06-staff-asset-import-and-enterprise-autofill-fix-backend-implementation-plan.md diff --git a/docs/plans/backend/2026-05-06-staff-asset-import-and-enterprise-autofill-fix-backend-implementation-plan.md b/docs/plans/backend/2026-05-06-staff-asset-import-and-enterprise-autofill-fix-backend-implementation-plan.md new file mode 100644 index 00000000..27a5e031 --- /dev/null +++ b/docs/plans/backend/2026-05-06-staff-asset-import-and-enterprise-autofill-fix-backend-implementation-plan.md @@ -0,0 +1,1409 @@ +# Staff Asset Import And Enterprise Auto-Fill Fix Implementation Plan + +> **For implementers:** Follow this repository's `AGENTS.md`. Do not enable `using-superpowers` or subagents unless the user explicitly requests them. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Fix employee/family dual-Sheet asset imports so asset-only imports still work and same-file main rows can be used as owner context, then restore unified enterprise auto-fill for staff-family, credit-customer, intermediary, and supplier flows. + +**Architecture:** Keep the existing API paths, Excel templates, and two-task-ID response shape. Add internal execution-result/context methods to existing import services, then add backend orchestration methods that initialize the same task IDs but run main Sheet before asset Sheet when both contain data. Add one `EnterpriseAutoFillService` as the only internal missing-entity insertion service and call it from the four successful business write paths before relation/supplier data is inserted. + +**Tech Stack:** Java 21, Spring Boot 3, MyBatis Plus, RedisTemplate, EasyExcel, JUnit 5, Mockito, Maven. + +--- + +## Scope And Source Of Truth + +- Approved design: `docs/design/2026-05-06-staff-asset-import-and-enterprise-autofill-fix-design.md` +- Existing draft solution is stale in several places: `docs/plans/backend/2026-05-06-staff-asset-import-and-enterprise-autofill-fix-backend-solution.md` +- Use this implementation plan and the approved design as the current source of truth. +- Do not change frontend files for this implementation. +- Do not stage or commit unrelated dirty workspace files. +- Files under `ccdi-info-collection/src/test/` are local verification files in this repository because `*/src/test/` is ignored. Create or modify them while implementing TDD, but do not include them in commits unless the user explicitly asks to force-add tests with `git add -f`. +- After backend implementation, add a separate implementation record under `docs/reports/implementation/`. + +## File Structure + +- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/enums/EnterpriseSource.java` + - Add `SUPPLIER("SUPPLIER", "供应商")`. +- Create: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/support/EnterpriseAutoFillService.java` + - Internal missing-entity insert service. +- Create: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/support/EnterpriseAutoFillServiceTest.java` + - Unit tests for auto-fill insert, dedupe, risk rules, and duplicate-key behavior. +- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiBaseStaffService.java` + - Add dual-Sheet submit method returning `BaseStaffImportSubmitResultVO`. +- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiBaseStaffServiceImpl.java` + - Initialize staff/asset task IDs and call orchestrated backend import. +- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiBaseStaffImportServiceImpl.java` + - Extract reusable synchronous execution that returns success id cards and failure rows. +- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiBaseStaffAssetImportServiceImpl.java` + - Extract reusable synchronous execution that accepts extra owner mappings. +- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiBaseStaffController.java` + - Replace two independent submissions with the service dual-Sheet submit method. +- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiStaffFmyRelationService.java` + - Add dual-Sheet submit method returning `StaffFmyRelationImportSubmitResultVO`. +- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiStaffFmyRelationServiceImpl.java` + - Initialize relation/asset task IDs and call orchestrated backend import. +- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiStaffFmyRelationImportServiceImpl.java` + - Extract reusable synchronous execution that returns successful relation owner mappings. +- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiAssetInfoImportServiceImpl.java` + - Extract reusable synchronous execution that accepts extra owner mappings and preserves existing owner query conditions. +- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiStaffFmyRelationController.java` + - Replace two independent submissions with the service dual-Sheet submit method. +- Modify tests: + - `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/controller/CcdiEnumControllerTest.java` + - `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiBaseStaffDualImportServiceTest.java` + - `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiBaseStaffAssetImportServiceImplTest.java` + - `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiAssetInfoImportServiceImplTest.java` + - `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiStaffFmyRelationImportServiceImplTest.java` + - `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiStaffEnterpriseRelationServiceImplTest.java` + - `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiStaffEnterpriseRelationImportServiceImplTest.java` + - `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiIntermediaryServiceImplTest.java` + - `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiIntermediaryEnterpriseRelationImportServiceImplTest.java` + - `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiPurchaseTransactionFeatureContractTest.java` +- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiIntermediaryEnterpriseRelationAddDTO.java` + - Add `enterpriseName` for missing-entity auto-fill in manual intermediary relation API. +- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/excel/CcdiIntermediaryEnterpriseRelationExcel.java` + - Add `机构名称` column for missing-entity auto-fill in intermediary relation import. +- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/IntermediaryEnterpriseRelationImportFailureVO.java` + - Include `enterpriseName` in failure records if the import VO does not already expose it. +- Modify relation/supplier services: + - `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiStaffEnterpriseRelationServiceImpl.java` + - `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiStaffEnterpriseRelationImportServiceImpl.java` + - `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiCustEnterpriseRelationServiceImpl.java` + - `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiCustEnterpriseRelationImportServiceImpl.java` + - `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiIntermediaryServiceImpl.java` + - `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiIntermediaryEnterpriseRelationImportServiceImpl.java` + - `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiPurchaseTransactionServiceImpl.java` + - `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiPurchaseTransactionImportServiceImpl.java` +- Create: `docs/reports/implementation/2026-05-06-staff-asset-import-and-enterprise-autofill-fix-backend-implementation.md` + - Record final changed scope and verification. + +## Task 1: EnterpriseSource And Auto-Fill Service + +**Files:** +- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/enums/EnterpriseSource.java` +- Test: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/controller/CcdiEnumControllerTest.java` +- Create: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/support/EnterpriseAutoFillService.java` +- Test: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/support/EnterpriseAutoFillServiceTest.java` + +- [ ] **Step 1: Add failing enum test** + +In `CcdiEnumControllerTest#getEnterpriseSourceOptions_shouldReturnConfiguredOptions`, add: + +```java +assertTrue(data.stream() + .map(EnumOptionVO.class::cast) + .anyMatch(option -> + "SUPPLIER".equals(option.getValue()) + && "供应商".equals(option.getLabel()))); +``` + +Also add `import static org.junit.jupiter.api.Assertions.assertTrue;`. + +- [ ] **Step 2: Run enum test and verify it fails** + +Run: + +```bash +mvn -pl ccdi-info-collection -Dtest=CcdiEnumControllerTest test +``` + +Expected: FAIL because `SUPPLIER/供应商` is not present. + +- [ ] **Step 3: Add SUPPLIER enum** + +Modify `EnterpriseSource`: + +```java +SUPPLIER("SUPPLIER", "供应商"), +``` + +Place it after `INTERMEDIARY(...)` and before `BOTH(...)`. + +- [ ] **Step 4: Run enum test and verify it passes** + +Run: + +```bash +mvn -pl ccdi-info-collection -Dtest=CcdiEnumControllerTest test +``` + +Expected: PASS. + +- [ ] **Step 5: Write failing auto-fill service tests** + +Create `EnterpriseAutoFillServiceTest` with Mockito. Cover: + +```java +@Test +void ensureExistsBatch_shouldInsertMissingEnterpriseWithSourceAndNullRiskForSupplier() { + when(mapper.selectBatchIds(List.of("91330100MA27X12345"))).thenReturn(List.of()); + + service.ensureExistsBatch(List.of(new EnterpriseAutoFillService.EnterpriseFillItem( + "91330100MA27X12345", + "供应商A", + EnterpriseSource.SUPPLIER.getCode(), + DataSource.IMPORT.getCode(), + "tester" + ))); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + verify(mapper).insertBatch(captor.capture()); + CcdiEnterpriseBaseInfo entity = captor.getValue().get(0); + assertEquals("91330100MA27X12345", entity.getSocialCreditCode()); + assertEquals("供应商A", entity.getEnterpriseName()); + assertEquals("SUPPLIER", entity.getEntSource()); + assertEquals("IMPORT", entity.getDataSource()); + assertNull(entity.getRiskLevel()); + assertEquals("tester", entity.getCreatedBy()); + assertEquals("tester", entity.getUpdatedBy()); +} +``` + +Add tests for: + +- Existing entity returned by `selectBatchIds` is not inserted. +- Duplicate items in same batch insert once. +- `INTERMEDIARY` sets `riskLevel` to `"1"`. +- `EMP_RELATION` and `CREDIT_CUSTOMER` keep `riskLevel` null. +- `DuplicateKeyException` from batch insert falls back to per-row `selectById` and treats existing row as success. + +- [ ] **Step 6: Run auto-fill tests and verify they fail** + +Run: + +```bash +mvn -pl ccdi-info-collection -Dtest=EnterpriseAutoFillServiceTest test +``` + +Expected: FAIL because `EnterpriseAutoFillService` does not exist. + +- [ ] **Step 7: Implement EnterpriseAutoFillService** + +Create `EnterpriseAutoFillService`: + +```java +package com.ruoyi.info.collection.service.support; + +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.info.collection.domain.CcdiEnterpriseBaseInfo; +import com.ruoyi.info.collection.enums.EnterpriseSource; +import com.ruoyi.info.collection.mapper.CcdiEnterpriseBaseInfoMapper; +import jakarta.annotation.Resource; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +@Service +public class EnterpriseAutoFillService { + + @Resource + private CcdiEnterpriseBaseInfoMapper enterpriseBaseInfoMapper; + + public record EnterpriseFillItem( + String socialCreditCode, + String enterpriseName, + String entSource, + String dataSource, + String userName + ) { + } + + @Transactional + public void ensureExists(EnterpriseFillItem item) { + ensureExistsBatch(List.of(item)); + } + + @Transactional + public void ensureExistsBatch(List items) { + Map deduped = dedupe(items); + if (deduped.isEmpty()) { + return; + } + + List existing = enterpriseBaseInfoMapper.selectBatchIds(new ArrayList<>(deduped.keySet())); + Set existingCodes = existing.stream() + .map(CcdiEnterpriseBaseInfo::getSocialCreditCode) + .collect(Collectors.toSet()); + + List missing = deduped.entrySet().stream() + .filter(entry -> !existingCodes.contains(entry.getKey())) + .map(entry -> buildEntity(entry.getValue())) + .toList(); + if (missing.isEmpty()) { + return; + } + + try { + enterpriseBaseInfoMapper.insertBatch(missing); + } catch (DuplicateKeyException e) { + insertOneByOne(missing); + } + } + + private Map dedupe(List items) { + Map result = new LinkedHashMap<>(); + if (items == null) { + return result; + } + for (EnterpriseFillItem item : items) { + if (item == null) { + continue; + } + String code = StringUtils.trim(item.socialCreditCode()); + if (StringUtils.isEmpty(code)) { + continue; + } + result.putIfAbsent(code, new EnterpriseFillItem( + code, + StringUtils.trim(item.enterpriseName()), + item.entSource(), + item.dataSource(), + item.userName() + )); + } + return result; + } + + private CcdiEnterpriseBaseInfo buildEntity(EnterpriseFillItem item) { + CcdiEnterpriseBaseInfo entity = new CcdiEnterpriseBaseInfo(); + entity.setSocialCreditCode(item.socialCreditCode()); + entity.setEnterpriseName(item.enterpriseName()); + entity.setEntSource(item.entSource()); + entity.setDataSource(item.dataSource()); + entity.setRiskLevel(resolveRiskLevel(item.entSource())); + entity.setCreatedBy(item.userName()); + entity.setUpdatedBy(item.userName()); + return entity; + } + + private String resolveRiskLevel(String entSource) { + return EnterpriseSource.INTERMEDIARY.getCode().equals(entSource) ? "1" : null; + } + + private void insertOneByOne(List missing) { + for (CcdiEnterpriseBaseInfo entity : missing) { + if (enterpriseBaseInfoMapper.selectById(entity.getSocialCreditCode()) != null) { + continue; + } + try { + enterpriseBaseInfoMapper.insert(entity); + } catch (DuplicateKeyException ignored) { + // Concurrent insert won the race; treat as already existing. + } + } + } +} +``` + +- [ ] **Step 8: Run focused tests** + +Run: + +```bash +mvn -pl ccdi-info-collection -Dtest=CcdiEnumControllerTest,EnterpriseAutoFillServiceTest test +``` + +Expected: PASS. + +- [ ] **Step 9: Commit** + +```bash +git add \ + ccdi-info-collection/src/main/java/com/ruoyi/info/collection/enums/EnterpriseSource.java \ + ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/support/EnterpriseAutoFillService.java +git commit -m "新增实体库自动补入服务" +``` + +## Task 2: Employee Dual-Sheet Import Execution Context + +**Files:** +- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiBaseStaffImportServiceImpl.java` +- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiBaseStaffAssetImportServiceImpl.java` +- Test: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiBaseStaffDualImportServiceTest.java` +- Test: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiBaseStaffAssetImportServiceImplTest.java` + +- [ ] **Step 1: Write failing staff execution-result test** + +In `CcdiBaseStaffDualImportServiceTest`, add a test for a new method named `executeBaseStaffImport`: + +```java +@Test +void executeBaseStaffImport_shouldReturnSuccessIdCardsAndIgnoreFailureRowsForAssetContext() { + CcdiBaseStaffExcel valid = buildExcel(1001L, 10L, "11010519491231002X"); + CcdiBaseStaffExcel invalid = buildExcel(1002L, 99L, "320101199001010014"); + + when(baseStaffMapper.selectBatchIds(List.of(1001L, 1002L))).thenReturn(List.of()); + when(baseStaffMapper.selectList(any())).thenReturn(List.of()); + when(deptMapper.selectDeptById(10L)).thenReturn(buildDept(10L, "0", "0")); + when(deptMapper.selectDeptById(99L)).thenReturn(null); + when(redisTemplate.opsForHash()).thenReturn(hashOperations); + when(redisTemplate.opsForValue()).thenReturn(valueOperations); + + var result = service.executeBaseStaffImport(List.of(valid, invalid), "task-context", "tester"); + + assertEquals(Set.of("11010519491231002X"), result.successIdCards()); + assertEquals(1, result.failures().size()); + verify(baseStaffMapper).insertBatch(any()); +} +``` + +- [ ] **Step 2: Run test and verify it fails** + +Run: + +```bash +mvn -pl ccdi-info-collection -Dtest=CcdiBaseStaffDualImportServiceTest#executeBaseStaffImport_shouldReturnSuccessIdCardsAndIgnoreFailureRowsForAssetContext test +``` + +Expected: FAIL because `executeBaseStaffImport` does not exist. + +- [ ] **Step 3: Extract staff execution method** + +In `CcdiBaseStaffImportServiceImpl`: + +- Add a public record: + +```java +public record BaseStaffImportExecutionResult( + List successRecords, + List failures, + Set successIdCards +) { +} +``` + +- Add: + +```java +public BaseStaffImportExecutionResult executeBaseStaffImport(List excelList, String taskId, String userName) { + // Move validation, failure collection, insertBatch, failure Redis write, + // status update, and logging from importBaseStaffAsync here. + // Build successIdCards only from records successfully added to newRecords. +} +``` + +- Make `importBaseStaffAsync` delegate to: + +```java +executeBaseStaffImport(excelList, taskId, "系统"); +``` + +Do not put failed rows, duplicate rows, or rows rejected by dept/id-card validation into `successIdCards`. + +- [ ] **Step 4: Run staff tests** + +Run: + +```bash +mvn -pl ccdi-info-collection -Dtest=CcdiBaseStaffDualImportServiceTest,CcdiBaseStaffImportServiceImplTest test +``` + +Expected: PASS. + +- [ ] **Step 5: Write failing employee asset extra-owner test** + +In `CcdiBaseStaffAssetImportServiceImplTest`, add: + +```java +@Test +void executeAssetImport_shouldImportWhenOwnerOnlyExistsInCurrentStaffContext() { + CcdiBaseStaffAssetInfoExcel excel = buildExcel("11010519491231002X", "房产"); + when(redisTemplate.opsForHash()).thenReturn(hashOperations); + when(assetInfoMapper.selectOwnerCandidatesByBaseStaffIdCards(List.of("11010519491231002X"))).thenReturn(List.of()); + + service.executeAssetImport( + List.of(excel), + "task-current-owner", + "tester", + Map.of("11010519491231002X", Set.of("11010519491231002X")) + ); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + verify(assetInfoMapper).insertBatch(captor.capture()); + assertEquals("11010519491231002X", captor.getValue().get(0).getFamilyId()); +} +``` + +- [ ] **Step 6: Run test and verify it fails** + +Run: + +```bash +mvn -pl ccdi-info-collection -Dtest=CcdiBaseStaffAssetImportServiceImplTest#executeAssetImport_shouldImportWhenOwnerOnlyExistsInCurrentStaffContext test +``` + +Expected: FAIL because `executeAssetImport` does not exist. + +- [ ] **Step 7: Extract employee asset execution method** + +In `CcdiBaseStaffAssetImportServiceImpl`: + +- Add: + +```java +public void executeAssetImport( + List excelList, + String taskId, + String userName, + Map> extraOwnerMap +) { + // Move logic from importAssetInfoAsync here. +} +``` + +- Merge existing database owners with extra owner mappings: + +```java +Map> ownerMap = buildOwnerMap(personIds); +mergeOwnerMappings(ownerMap, extraOwnerMap); +``` + +- Keep `importAssetInfoAsync` delegating to: + +```java +executeAssetImport(excelList, taskId, userName, Map.of()); +``` + +- Do not require staff Sheet data; when `extraOwnerMap` is empty, behavior must match the current independent asset import. + +- [ ] **Step 8: Run employee import service tests** + +Run: + +```bash +mvn -pl ccdi-info-collection -Dtest=CcdiBaseStaffDualImportServiceTest,CcdiBaseStaffAssetImportServiceImplTest test +``` + +Expected: PASS. + +- [ ] **Step 9: Commit** + +```bash +git add \ + ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiBaseStaffImportServiceImpl.java \ + ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiBaseStaffAssetImportServiceImpl.java +git commit -m "重构员工导入执行上下文" +``` + +## Task 3: Employee Dual-Sheet Orchestration + +**Files:** +- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiBaseStaffService.java` +- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiBaseStaffServiceImpl.java` +- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiBaseStaffController.java` +- Test: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/controller/CcdiBaseStaffControllerTest.java` +- Test: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiBaseStaffServiceImplTest.java` + +- [ ] **Step 1: Write failing controller delegation test** + +In `CcdiBaseStaffControllerTest`, verify `/importData` delegates to `baseStaffService.importBaseStaffWithAssets(staffList, assetList)` and no longer calls asset service independently. Use mocked `EasyExcelUtil` only if current test pattern already does; otherwise add service-level tests first and keep controller assertion to response field shape. + +Expected result data for asset-only file: + +```java +BaseStaffImportSubmitResultVO result = new BaseStaffImportSubmitResultVO(); +result.setAssetTaskId("asset-task"); +result.setMessage("已提交员工资产信息导入任务"); +``` + +- [ ] **Step 2: Write failing service tests for task IDs** + +In `CcdiBaseStaffServiceImplTest`, add tests: + +- `importBaseStaffWithAssets_shouldReturnOnlyAssetTaskIdWhenStaffRowsEmpty` +- `importBaseStaffWithAssets_shouldReturnBothTaskIdsWhenBothSheetsHaveRows` + +Use mocks for `RedisTemplate.opsForHash()` and verify no `staffTaskId` is created for an empty staff list. + +- [ ] **Step 3: Run tests and verify failure** + +Run: + +```bash +mvn -pl ccdi-info-collection -Dtest=CcdiBaseStaffServiceImplTest,CcdiBaseStaffControllerTest test +``` + +Expected: FAIL because the service method does not exist and controller still submits independently. + +- [ ] **Step 4: Add service method contract** + +In `ICcdiBaseStaffService` add: + +```java +BaseStaffImportSubmitResultVO importBaseStaffWithAssets( + List staffList, + List assetList +); +``` + +Add imports for `CcdiBaseStaffAssetInfoExcel` and `BaseStaffImportSubmitResultVO`. + +- [ ] **Step 5: Implement task initialization and orchestration** + +In `CcdiBaseStaffServiceImpl`: + +- Inject: + +```java +@Resource +private CcdiBaseStaffImportServiceImpl baseStaffImportExecutionService; + +@Resource +private CcdiBaseStaffAssetImportServiceImpl baseStaffAssetImportExecutionService; + +@Async +public void importBaseStaffWithAssetsAsync(...) { ... } +``` + +If adding `@Async` on `CcdiBaseStaffServiceImpl` risks self-invocation, create a small new support service instead: + +`ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/support/BaseStaffDualSheetImportOrchestrator.java` + +Use that support service from `CcdiBaseStaffServiceImpl`. The support service should: + +1. Run `executeBaseStaffImport` only when `staffList` has rows. +2. Convert `successIdCards` to `Map>`. +3. Run `executeAssetImport` only when `assetList` has rows. +4. Pass an empty extra owner map when staff rows are empty. + +Task initialization rules: + +- Asset-only import creates only `assetTaskId`. +- Staff-only import creates only `staffTaskId`. +- Both create both IDs. +- If both lists are empty, throw `RuntimeException("至少需要一条数据")`. + +- [ ] **Step 6: Update controller** + +In `CcdiBaseStaffController#importData`, replace: + +```java +BaseStaffImportSubmitResultVO result = new BaseStaffImportSubmitResultVO(); +if (hasStaffRows) { + result.setStaffTaskId(baseStaffService.importBaseStaff(staffList)); +} +if (hasAssetRows) { + result.setAssetTaskId(baseStaffAssetImportService.importAssetInfo(assetList)); +} +``` + +with: + +```java +BaseStaffImportSubmitResultVO result = baseStaffService.importBaseStaffWithAssets(staffList, assetList); +``` + +Keep existing response text: + +```java +return AjaxResult.success("导入任务已提交,正在后台处理", result); +``` + +- [ ] **Step 7: Run employee dual-Sheet tests** + +Run: + +```bash +mvn -pl ccdi-info-collection -Dtest=CcdiBaseStaffServiceImplTest,CcdiBaseStaffControllerTest,CcdiBaseStaffDualImportServiceTest,CcdiBaseStaffAssetImportServiceImplTest test +``` + +Expected: PASS. + +- [ ] **Step 8: Commit** + +```bash +git add \ + ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiBaseStaffService.java \ + ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiBaseStaffServiceImpl.java \ + ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiBaseStaffController.java +git commit -m "修复员工双Sheet导入编排" +``` + +If you created `BaseStaffDualSheetImportOrchestrator.java`, include it in the commit. + +## Task 4: Family Relation Dual-Sheet Import Execution Context + +**Files:** +- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiStaffFmyRelationImportServiceImpl.java` +- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiAssetInfoImportServiceImpl.java` +- Test: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiStaffFmyRelationImportServiceImplTest.java` +- Test: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiAssetInfoImportServiceImplTest.java` + +- [ ] **Step 1: Write failing family relation execution-result test** + +In `CcdiStaffFmyRelationImportServiceImplTest`, add a Mockito-based test or split into a new Mockito extension test. Assert a new method `executeRelationImport` returns only successful relation owner mappings: + +```java +var result = service.executeRelationImport(List.of(validRelation, invalidRelation), "relation-task", "tester"); + +assertEquals(Set.of("320101199001010011"), result.ownerMap().get("320101199201010022")); +assertEquals(1, result.failures().size()); +``` + +The key is `relationCertNo`, and the owner value is the employee `personId`. + +- [ ] **Step 2: Run family relation test and verify failure** + +Run: + +```bash +mvn -pl ccdi-info-collection -Dtest=CcdiStaffFmyRelationImportServiceImplTest test +``` + +Expected: FAIL because `executeRelationImport` does not exist. + +- [ ] **Step 3: Extract relation execution method** + +In `CcdiStaffFmyRelationImportServiceImpl`: + +- Add record: + +```java +public record StaffFmyRelationImportExecutionResult( + List successRecords, + List failures, + Map> ownerMap +) { +} +``` + +- Add: + +```java +public StaffFmyRelationImportExecutionResult executeRelationImport( + List excelList, + String taskId, + String userName +) { + // Move current importRelationAsync body here. + // ownerMap key = relation.getRelationCertNo() + // ownerMap value includes relation.getPersonId() +} +``` + +- Make `importRelationAsync` delegate to `executeRelationImport`. + +- [ ] **Step 4: Write failing family asset extra-owner test** + +In `CcdiAssetInfoImportServiceImplTest`, add: + +```java +@Test +void executeAssetImport_shouldImportWhenOwnerOnlyExistsInCurrentRelationContext() { + CcdiAssetInfoExcel excel = buildExcel("320101199201010022", "房产"); + when(redisTemplate.opsForHash()).thenReturn(hashOperations); + when(assetInfoMapper.selectOwnerCandidatesByRelationCertNos(List.of("320101199201010022"))).thenReturn(List.of()); + + service.executeAssetImport( + List.of(excel), + "asset-task", + "tester", + Map.of("320101199201010022", Set.of("320101199001010011")) + ); + + ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); + verify(assetInfoMapper).insertBatch(captor.capture()); + assertEquals("320101199001010011", captor.getValue().get(0).getFamilyId()); + assertEquals("320101199201010022", captor.getValue().get(0).getPersonId()); +} +``` + +- [ ] **Step 5: Run family asset test and verify failure** + +Run: + +```bash +mvn -pl ccdi-info-collection -Dtest=CcdiAssetInfoImportServiceImplTest#executeAssetImport_shouldImportWhenOwnerOnlyExistsInCurrentRelationContext test +``` + +Expected: FAIL because `executeAssetImport` overload does not exist. + +- [ ] **Step 6: Extract family asset execution method** + +In `CcdiAssetInfoImportServiceImpl`: + +- Add: + +```java +public void executeAssetImport( + List excelList, + String taskId, + String userName, + Map> extraOwnerMap +) { + // Move current importAssetInfoAsync body here. +} +``` + +- Merge `extraOwnerMap` into the existing owner map after calling `selectOwnerCandidatesByRelationCertNos`. +- Do not add `status = 1` filtering to the database owner query. +- Keep owner ambiguity behavior: if merged owner set has more than one family id, fail with `亲属资产归属员工不唯一`. +- Make `importAssetInfoAsync` delegate with `Map.of()`. + +- [ ] **Step 7: Run family import tests** + +Run: + +```bash +mvn -pl ccdi-info-collection -Dtest=CcdiStaffFmyRelationImportServiceImplTest,CcdiAssetInfoImportServiceImplTest test +``` + +Expected: PASS. + +- [ ] **Step 8: Commit** + +```bash +git add \ + ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiStaffFmyRelationImportServiceImpl.java \ + ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiAssetInfoImportServiceImpl.java +git commit -m "重构亲属关系资产导入执行上下文" +``` + +## Task 5: Family Relation Dual-Sheet Orchestration + +**Files:** +- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiStaffFmyRelationService.java` +- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiStaffFmyRelationServiceImpl.java` +- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiStaffFmyRelationController.java` +- Test: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/controller/CcdiStaffFmyRelationControllerTest.java` +- Test: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiStaffFmyRelationServiceImplTest.java` + +- [ ] **Step 1: Write failing service tests** + +In `CcdiStaffFmyRelationServiceImplTest`, add: + +- `importRelationWithAssets_shouldReturnOnlyAssetTaskIdWhenRelationRowsEmpty` +- `importRelationWithAssets_shouldReturnBothTaskIdsWhenBothSheetsHaveRows` + +Assert relation-only, asset-only, and both-Sheet task ID behavior. In the asset-only test, assert no relation task is initialized. + +- [ ] **Step 2: Write failing controller delegation test** + +In `CcdiStaffFmyRelationControllerTest`, verify `importData` delegates to `relationService.importRelationWithAssets(relationList, assetList)` and returns `StaffFmyRelationImportSubmitResultVO`. + +- [ ] **Step 3: Run tests and verify failure** + +Run: + +```bash +mvn -pl ccdi-info-collection -Dtest=CcdiStaffFmyRelationServiceImplTest,CcdiStaffFmyRelationControllerTest test +``` + +Expected: FAIL because the service method does not exist and controller still submits independently. + +- [ ] **Step 4: Add service method contract** + +In `ICcdiStaffFmyRelationService` add: + +```java +StaffFmyRelationImportSubmitResultVO importRelationWithAssets( + List relationList, + List assetList +); +``` + +- [ ] **Step 5: Implement task initialization and orchestration** + +In `CcdiStaffFmyRelationServiceImpl`, either: + +- Add a small support orchestrator `StaffFmyRelationDualSheetImportOrchestrator`, or +- Inject the execution services and use an async proxy-safe pattern. + +The orchestrator must: + +1. Run relation import only when relation rows exist. +2. Build extra owner map from successful relation rows. +3. Run asset import only when asset rows exist. +4. Pass empty extra owner map when relation rows are empty. +5. Keep asset-only import as a normal path. + +- [ ] **Step 6: Update controller** + +In `CcdiStaffFmyRelationController#importData`, replace independent calls: + +```java +result.setRelationTaskId(relationService.importRelation(relationList)); +result.setAssetTaskId(assetInfoImportService.importAssetInfo(assetList)); +``` + +with: + +```java +StaffFmyRelationImportSubmitResultVO result = relationService.importRelationWithAssets(relationList, assetList); +``` + +- [ ] **Step 7: Run family dual-Sheet tests** + +Run: + +```bash +mvn -pl ccdi-info-collection -Dtest=CcdiStaffFmyRelationServiceImplTest,CcdiStaffFmyRelationControllerTest,CcdiStaffFmyRelationImportServiceImplTest,CcdiAssetInfoImportServiceImplTest test +``` + +Expected: PASS. + +- [ ] **Step 8: Commit** + +```bash +git add \ + ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiStaffFmyRelationService.java \ + ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiStaffFmyRelationServiceImpl.java \ + ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiStaffFmyRelationController.java +git commit -m "修复员工亲属双Sheet导入编排" +``` + +If you created `StaffFmyRelationDualSheetImportOrchestrator.java`, include it in the commit. + +## Task 6: Staff And Credit Enterprise Relation Auto-Fill + +**Files:** +- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiStaffEnterpriseRelationServiceImpl.java` +- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiStaffEnterpriseRelationImportServiceImpl.java` +- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiCustEnterpriseRelationServiceImpl.java` +- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiCustEnterpriseRelationImportServiceImpl.java` +- Test: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiStaffEnterpriseRelationServiceImplTest.java` +- Test: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiStaffEnterpriseRelationImportServiceImplTest.java` +- Add or modify tests for customer relation import/service. + +- [ ] **Step 1: Write failing staff manual auto-fill test** + +In `CcdiStaffEnterpriseRelationServiceImplTest#insertRelation_shouldAllowValidFamily`, mock `EnterpriseAutoFillService` and verify: + +```java +verify(enterpriseAutoFillService).ensureExists(argThat(item -> + "91310000123456789A".equals(item.socialCreditCode()) + && "测试企业".equals(item.enterpriseName()) + && "EMP_RELATION".equals(item.entSource()) + && "MANUAL".equals(item.dataSource()))); +``` + +- [ ] **Step 2: Write failing staff import auto-fill test** + +In `CcdiStaffEnterpriseRelationImportServiceImplTest`, add an async import test using mocks and assert `ensureExistsBatch` receives only the successful `newRecords`. + +- [ ] **Step 3: Write failing credit manual/import auto-fill tests** + +Add or extend customer relation service/import tests: + +- Manual insert calls `ensureExists` with `CREDIT_CUSTOMER/MANUAL`. +- Import calls `ensureExistsBatch` with only successful records and `CREDIT_CUSTOMER/IMPORT`. +- Duplicate DB or duplicate file rows do not enter auto-fill. + +- [ ] **Step 4: Run tests and verify failure** + +Run: + +```bash +mvn -pl ccdi-info-collection -Dtest=CcdiStaffEnterpriseRelationServiceImplTest,CcdiStaffEnterpriseRelationImportServiceImplTest,CcdiCustEnterpriseRelationServiceImplTest,CcdiCustEnterpriseRelationImportServiceImplTest test +``` + +Expected: FAIL because services do not call `EnterpriseAutoFillService`. + +- [ ] **Step 5: Implement staff manual auto-fill** + +In `CcdiStaffEnterpriseRelationServiceImpl` inject: + +```java +@Resource +private EnterpriseAutoFillService enterpriseAutoFillService; +``` + +After `existsByPersonIdAndSocialCreditCode` passes and before `relationMapper.insert(relation)`, call: + +```java +enterpriseAutoFillService.ensureExists(new EnterpriseAutoFillService.EnterpriseFillItem( + addDTO.getSocialCreditCode(), + addDTO.getEnterpriseName(), + EnterpriseSource.EMP_RELATION.getCode(), + DataSource.MANUAL.getCode(), + SecurityUtils.getUsername() +)); +``` + +- [ ] **Step 6: Implement staff import auto-fill** + +In `CcdiStaffEnterpriseRelationImportServiceImpl`, after `newRecords` are collected and before `saveBatch(newRecords, 500)`, call `ensureExistsBatch` with `EMP_RELATION/IMPORT/userName`. + +- [ ] **Step 7: Implement credit manual/import auto-fill** + +Use the same pattern: + +- `CcdiCustEnterpriseRelationServiceImpl`: `CREDIT_CUSTOMER/MANUAL` +- `CcdiCustEnterpriseRelationImportServiceImpl`: `CREDIT_CUSTOMER/IMPORT` + +Do not call auto-fill for failed import rows. + +- [ ] **Step 8: Run focused relation tests** + +Run: + +```bash +mvn -pl ccdi-info-collection -Dtest=CcdiStaffEnterpriseRelationServiceImplTest,CcdiStaffEnterpriseRelationImportServiceImplTest,CcdiCustEnterpriseRelationServiceImplTest,CcdiCustEnterpriseRelationImportServiceImplTest test +``` + +Expected: PASS. + +- [ ] **Step 9: Commit** + +```bash +git add \ + ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiStaffEnterpriseRelationServiceImpl.java \ + ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiStaffEnterpriseRelationImportServiceImpl.java \ + ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiCustEnterpriseRelationServiceImpl.java \ + ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiCustEnterpriseRelationImportServiceImpl.java +git commit -m "接入员工亲属和信贷客户实体自动补入" +``` + +If the customer test files do not exist yet, create them as local verification files only and do not stage them. + +## Task 7: Intermediary Enterprise Relation Auto-Fill + +**Files:** +- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiIntermediaryServiceImpl.java` +- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiIntermediaryEnterpriseRelationImportServiceImpl.java` +- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiIntermediaryEnterpriseRelationAddDTO.java` +- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/excel/CcdiIntermediaryEnterpriseRelationExcel.java` +- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/IntermediaryEnterpriseRelationImportFailureVO.java` +- Test: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiIntermediaryServiceImplTest.java` +- Test: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiIntermediaryEnterpriseRelationImportServiceImplTest.java` + +- [ ] **Step 1: Write failing intermediary enterprise-name contract tests** + +Add local tests proving the backend contract: + +- `CcdiIntermediaryEnterpriseRelationAddDTO` has `enterpriseName`. +- `CcdiIntermediaryEnterpriseRelationExcel` has `enterpriseName` with Excel header `机构名称`. +- Missing entity + blank `enterpriseName` fails instead of inserting an unnamed entity. +- Existing entity + blank `enterpriseName` still keeps current behavior because no auto-fill insert is needed. + +- [ ] **Step 2: Write failing manual relation test** + +In `CcdiIntermediaryServiceImplTest`, add: + +```java +@Test +void insertIntermediaryEnterpriseRelation_shouldAutoFillMissingEnterpriseBeforeInsert() { + CcdiBizIntermediary owner = new CcdiBizIntermediary(); + owner.setBizId("owner-biz"); + owner.setPersonSubType("本人"); + when(bizIntermediaryMapper.selectById("owner-biz")).thenReturn(owner); + when(enterpriseBaseInfoMapper.selectById("91330100MA27X12345")).thenReturn(null); + when(enterpriseRelationMapper.existsByIntermediaryBizIdAndSocialCreditCode("owner-biz", "91330100MA27X12345")).thenReturn(false); + when(enterpriseRelationMapper.insert(any())).thenReturn(1); + + CcdiIntermediaryEnterpriseRelationAddDTO addDTO = buildEnterpriseRelationAddDto(); + addDTO.setEnterpriseName("测试中介机构"); + service.insertIntermediaryEnterpriseRelation("owner-biz", addDTO); + + verify(enterpriseAutoFillService).ensureExists(argThat(item -> + "INTERMEDIARY".equals(item.entSource()) + && "MANUAL".equals(item.dataSource()) + && "测试中介机构".equals(item.enterpriseName()))); +} +``` + +- [ ] **Step 3: Write failing import relation test** + +Change `CcdiIntermediaryEnterpriseRelationImportServiceImplTest#importEnterpriseRelationAsync_shouldFailWhenEnterpriseDoesNotExist` into a success test: + +- Owner exists. +- Entity does not exist. +- Excel row has `enterpriseName`. +- Relation combination does not exist. +- `EnterpriseAutoFillService.ensureExistsBatch` is called with `INTERMEDIARY/IMPORT`. +- Relation insert succeeds. + +- [ ] **Step 4: Run tests and verify failure** + +Run: + +```bash +mvn -pl ccdi-info-collection -Dtest=CcdiIntermediaryServiceImplTest,CcdiIntermediaryEnterpriseRelationImportServiceImplTest test +``` + +Expected: FAIL because current code requires the enterprise to exist. + +- [ ] **Step 5: Add backend enterpriseName fields** + +In `CcdiIntermediaryEnterpriseRelationAddDTO`, add: + +```java +@Schema(description = "机构名称") +@Size(max = 200, message = "机构名称长度不能超过200个字符") +private String enterpriseName; +``` + +Do not mark it `@NotBlank` at DTO level, because existing-entity relations may still be created by social credit code alone. Missing-entity auto-fill must validate it at service level. + +In `CcdiIntermediaryEnterpriseRelationExcel`, add: + +```java +/** 机构名称 */ +@ExcelProperty(value = "机构名称", index = 2) +@ColumnWidth(30) +private String enterpriseName; +``` + +Shift `relationPersonPost` to index `3` and `remark` to index `4`. + +In `IntermediaryEnterpriseRelationImportFailureVO`, add `enterpriseName` if it is not already present so failed rows show the submitted name. + +- [ ] **Step 6: Replace manual existing-enterprise check** + +In `CcdiIntermediaryServiceImpl`: + +- Inject `EnterpriseAutoFillService`. +- Split `validateEnterpriseRelation` so it only checks owner and duplicate relation. +- Remove the `enterpriseBaseInfoMapper.selectById(socialCreditCode) == null` failure for insert. +- Add a helper that checks whether the entity exists. If it exists, do not call auto-fill and do not update it. If it does not exist, require `enterpriseName` and call auto-fill before `enterpriseRelationMapper.insert(relation)`: + +```java +private void ensureIntermediaryEnterpriseExists(CcdiIntermediaryEnterpriseRelationAddDTO addDTO) { + if (enterpriseBaseInfoMapper.selectById(addDTO.getSocialCreditCode()) != null) { + return; + } + if (StringUtils.isEmpty(addDTO.getEnterpriseName())) { + throw new RuntimeException("机构名称不能为空"); + } + enterpriseAutoFillService.ensureExists(new EnterpriseAutoFillService.EnterpriseFillItem( + addDTO.getSocialCreditCode(), + addDTO.getEnterpriseName(), + EnterpriseSource.INTERMEDIARY.getCode(), + DataSource.MANUAL.getCode(), + SecurityUtils.getUsername() + )); +} +``` + +- [ ] **Step 7: Replace import existing-enterprise check** + +In `CcdiIntermediaryEnterpriseRelationImportServiceImpl`: + +- Keep the existing enterprise code query only as `existingEnterpriseCodes` context, not as a missing-entity failure. +- If a row references a missing entity and `excel.getEnterpriseName()` is blank, fail that row with `机构名称不能为空`. +- If a row references an existing entity, do not update that entity and do not require `enterpriseName`. +- Build `EnterpriseFillItem` from successful Excel rows where the entity is missing and `enterpriseName` is present. +- Call `ensureExistsBatch` before `saveBatch(successRecords, 500)`. +- Keep owner missing, DB duplicate relation, and file duplicate relation failures unchanged. + +- [ ] **Step 8: Run intermediary tests** + +Run: + +```bash +mvn -pl ccdi-info-collection -Dtest=CcdiIntermediaryServiceImplTest,CcdiIntermediaryEnterpriseRelationImportServiceImplTest,EnterpriseAutoFillServiceTest test +``` + +Expected: PASS. + +- [ ] **Step 9: Commit** + +```bash +git add \ + ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiIntermediaryServiceImpl.java \ + ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiIntermediaryEnterpriseRelationImportServiceImpl.java \ + ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/dto/CcdiIntermediaryEnterpriseRelationAddDTO.java \ + ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/excel/CcdiIntermediaryEnterpriseRelationExcel.java \ + ccdi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/IntermediaryEnterpriseRelationImportFailureVO.java +git commit -m "接入中介实体自动补入" +``` + +## Task 8: Purchase Supplier Auto-Fill + +**Files:** +- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiPurchaseTransactionServiceImpl.java` +- Modify: `ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiPurchaseTransactionImportServiceImpl.java` +- Test: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiPurchaseTransactionFeatureContractTest.java` +- Add if useful: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiPurchaseTransactionServiceImplTest.java` +- Add if useful: `ccdi-info-collection/src/test/java/com/ruoyi/info/collection/service/CcdiPurchaseTransactionImportServiceImplTest.java` + +- [ ] **Step 1: Write failing manual supplier auto-fill test** + +Create focused service test if missing: + +- `insertTransaction`: `supplierUscc` non-empty and valid -> calls `ensureExistsBatch` with `SUPPLIER/MANUAL`. +- `updateTransaction`: `supplierUscc` non-empty and valid -> calls `ensureExistsBatch` with `SUPPLIER/MANUAL`. +- `supplierUscc` empty -> no auto-fill call for that supplier, but supplier is still saved if existing rules allow it. +- `supplierUscc` non-empty but invalid -> no auto-fill call for that supplier, but do not add a new manual-save failure condition. + +- [ ] **Step 2: Write failing import supplier auto-fill test** + +In import test: + +- Successful purchase with supplier `supplierUscc` non-empty -> auto-fill. +- Successful purchase with supplier `supplierUscc` empty -> no auto-fill, no failure because of empty code. +- Failed purchase -> supplier does not enter auto-fill. +- Invalid supplier USCC -> existing supplier validation fails and does not auto-fill. + +- [ ] **Step 3: Run supplier tests and verify failure** + +Run: + +```bash +mvn -pl ccdi-info-collection -Dtest=CcdiPurchaseTransactionFeatureContractTest,CcdiPurchaseTransactionServiceImplTest,CcdiPurchaseTransactionImportServiceImplTest test +``` + +Expected: FAIL because purchase services do not call auto-fill. + +- [ ] **Step 4: Implement manual supplier auto-fill** + +In `CcdiPurchaseTransactionServiceImpl` inject `EnterpriseAutoFillService`. + +Add one helper in the service and reuse it for add and edit: + +```java +private static final String SUPPLIER_USCC_PATTERN = "^[0-9A-HJ-NPQRTUWXY]{2}\\d{6}[0-9A-HJ-NPQRTUWXY]{10}$"; + +private boolean isValidSupplierUscc(String supplierUscc) { + return StringUtils.isNotEmpty(supplierUscc) && supplierUscc.matches(SUPPLIER_USCC_PATTERN); +} + +private void autoFillSuppliers(List supplierList, String dataSource, String userName) { + enterpriseAutoFillService.ensureExistsBatch(supplierList.stream() + .filter(item -> isValidSupplierUscc(item.getSupplierUscc())) + .map(item -> new EnterpriseAutoFillService.EnterpriseFillItem( + item.getSupplierUscc(), + item.getSupplierName(), + EnterpriseSource.SUPPLIER.getCode(), + dataSource, + userName + )) + .toList()); +} +``` + +Call it in both write paths: + +- `insertTransaction`: after `buildSupplierEntities(...)` and before `transactionMapper.insert(transaction)`. +- `updateTransaction`: after `buildSupplierEntities(...)` and before replacing supplier rows. + +Use `DataSource.MANUAL.getCode()` and `SecurityUtils.getUsername()` for both manual paths. Do not fail suppliers just because `supplierUscc` is empty or invalid; invalid codes are only excluded from entity auto-fill. + +- [ ] **Step 5: Implement import supplier auto-fill** + +In `CcdiPurchaseTransactionImportServiceImpl`, after `newSuppliers` is fully collected and before `saveBatch(newTransactions, 500)`: + +```java +autoFillSuppliers(newSuppliers, DataSource.IMPORT.getCode(), userName); +``` + +Use the same valid-USCC filter as manual save. This naturally excludes failed purchase rows because they never enter `newSuppliers`; invalid supplier USCC rows already fail existing import validation before they can be collected. + +- [ ] **Step 6: Run supplier tests** + +Run: + +```bash +mvn -pl ccdi-info-collection -Dtest=CcdiPurchaseTransactionFeatureContractTest,CcdiPurchaseTransactionServiceImplTest,CcdiPurchaseTransactionImportServiceImplTest test +``` + +Expected: PASS. + +- [ ] **Step 7: Commit** + +```bash +git add \ + ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiPurchaseTransactionServiceImpl.java \ + ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiPurchaseTransactionImportServiceImpl.java +git commit -m "接入招投标供应商实体自动补入" +``` + +Keep supplier tests as local verification files unless the user explicitly asks to force-add tests. + +## Task 9: Backend Regression And Implementation Record + +**Files:** +- Create: `docs/reports/implementation/2026-05-06-staff-asset-import-and-enterprise-autofill-fix-backend-implementation.md` + +- [ ] **Step 1: Run full backend focused regression** + +Run: + +```bash +mvn -pl ccdi-info-collection -Dtest=CcdiEnumControllerTest,EnterpriseAutoFillServiceTest,CcdiBaseStaffDualImportServiceTest,CcdiBaseStaffAssetImportServiceImplTest,CcdiBaseStaffServiceImplTest,CcdiBaseStaffControllerTest,CcdiStaffFmyRelationImportServiceImplTest,CcdiAssetInfoImportServiceImplTest,CcdiStaffFmyRelationServiceImplTest,CcdiStaffFmyRelationControllerTest,CcdiStaffEnterpriseRelationServiceImplTest,CcdiStaffEnterpriseRelationImportServiceImplTest,CcdiCustEnterpriseRelationServiceImplTest,CcdiCustEnterpriseRelationImportServiceImplTest,CcdiIntermediaryServiceImplTest,CcdiIntermediaryEnterpriseRelationImportServiceImplTest,CcdiPurchaseTransactionFeatureContractTest,CcdiPurchaseTransactionServiceImplTest,CcdiPurchaseTransactionImportServiceImplTest test +``` + +Expected: PASS. + +If Mockito fails with inline Byte Buddy self-attach on JDK 21, use the existing project workaround: + +- Add or verify `ccdi-info-collection/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker` contains `mock-maker-subclass`. +- Re-run the same command. + +- [ ] **Step 2: Run module compile** + +Run: + +```bash +mvn -pl ccdi-info-collection -am test -DskipTests +``` + +Expected: PASS compilation. + +- [ ] **Step 3: Run API-level verification after backend restart** + +Restart backend with the project script: + +```bash +sh bin/restart_java_backend.sh +``` + +Use `/login/test` to get token, download real templates, and verify: + +- `/ccdi/baseStaff/importTemplate` +- `/ccdi/baseStaff/importData` +- `/ccdi/baseStaff/importStatus/{taskId}` +- `/ccdi/baseStaff/importFailures/{taskId}` +- `/ccdi/staffFmyRelation/importTemplate` +- `/ccdi/staffFmyRelation/importData` +- `/ccdi/staffFmyRelation/importStatus/{taskId}` +- `/ccdi/staffFmyRelation/importFailures/{taskId}` +- `/ccdi/staffEnterpriseRelation` +- `/ccdi/staffEnterpriseRelation/importTemplate` +- `/ccdi/staffEnterpriseRelation/importData` +- `/ccdi/staffEnterpriseRelation/importStatus/{taskId}` +- `/ccdi/staffEnterpriseRelation/importFailures/{taskId}` +- `/ccdi/custEnterpriseRelation` +- `/ccdi/custEnterpriseRelation/importTemplate` +- `/ccdi/custEnterpriseRelation/importData` +- `/ccdi/custEnterpriseRelation/importStatus/{taskId}` +- `/ccdi/custEnterpriseRelation/importFailures/{taskId}` +- `/ccdi/intermediary/{bizId}/enterprise-relation` +- `/ccdi/intermediary/importEnterpriseRelationTemplate` +- `/ccdi/intermediary/importEnterpriseRelationData` +- `/ccdi/intermediary/importEnterpriseRelationStatus/{taskId}` +- `/ccdi/intermediary/importEnterpriseRelationFailures/{taskId}` +- `/ccdi/purchaseTransaction` +- `/ccdi/purchaseTransaction/importTemplate` +- `/ccdi/purchaseTransaction/importData` +- `/ccdi/purchaseTransaction/importStatus/{taskId}` +- `/ccdi/purchaseTransaction/importFailures/{taskId}` +- `/ccdi/enterpriseBaseInfo/list` + +Scenarios: + +- Employee asset-only import referencing an existing employee succeeds. +- Family asset-only import referencing an existing family relation succeeds. +- Employee + employee asset in same file succeeds. +- Family relation + family asset in same file succeeds. +- Failed main row does not provide owner context to asset row. +- Manual staff-family entity relation inserts missing entity with `ent_source=EMP_RELATION`, `data_source=MANUAL`, `risk_level IS NULL`. +- Staff-family entity relation import inserts missing entity with `ent_source=EMP_RELATION`, `data_source=IMPORT`, `risk_level IS NULL`; failed rows do not auto-fill. +- Manual credit-customer entity relation inserts missing entity with `ent_source=CREDIT_CUSTOMER`, `data_source=MANUAL`, `risk_level IS NULL`. +- Credit-customer entity relation import inserts missing entity with `ent_source=CREDIT_CUSTOMER`, `data_source=IMPORT`, `risk_level IS NULL`; failed rows do not auto-fill. +- Manual intermediary entity relation inserts missing entity when `enterpriseName` is present, with `ent_source=INTERMEDIARY`, `data_source=MANUAL`, `risk_level=1`. +- Intermediary entity relation import inserts missing entity when `机构名称` is present, with `ent_source=INTERMEDIARY`, `data_source=IMPORT`, `risk_level=1`; missing entity + blank `机构名称` fails that row. +- Manual purchase add and edit insert missing supplier entity with `ent_source=SUPPLIER`, `data_source=MANUAL`, `risk_level IS NULL`. +- Purchase import inserts missing supplier entity with `ent_source=SUPPLIER`, `data_source=IMPORT`, `risk_level IS NULL`; failed purchase or invalid supplier rows do not auto-fill. +- Existing entity rows for all four sources are not updated. Verify by seeding an existing `ccdi_enterprise_base_info` row with a sentinel `enterprise_name`, `ent_source`, `data_source`, and `risk_level`, then re-querying the same row after each manual/import path. +- Supplier with empty `supplierUscc` follows existing save/import validation but does not auto-fill. + +- [ ] **Step 4: Run real page verification** + +Use the browser-use skill on real business pages, not prototype pages: + +- 【员工信息维护】download template, upload employee + employee asset file, check task status, failure records, list/detail asset. +- 【员工亲属关系维护】download template, upload relation + family asset file, check task status, failure records, detail asset. +- 【实体库管理】query auto-filled entities by social credit code. + +- [ ] **Step 5: Clean test data** + +Delete this run's test data: + +- `ccdi_asset_info` +- `ccdi_base_staff` +- `ccdi_staff_fmy_relation` +- `ccdi_staff_enterprise_relation` +- `ccdi_cust_enterprise_relation` +- `ccdi_intermediary_enterprise_relation` +- `ccdi_purchase_transaction` +- `ccdi_purchase_transaction_supplier` +- `ccdi_enterprise_base_info` + +For SQL containing Chinese comments or values, write a SQL file and execute: + +```bash +bin/mysql_utf8_exec.sh +``` + +- [ ] **Step 6: Stop test-started processes** + +Stop backend/frontend processes started during verification. Do not stop unrelated user processes. + +- [ ] **Step 7: Write implementation record** + +Create `docs/reports/implementation/2026-05-06-staff-asset-import-and-enterprise-autofill-fix-backend-implementation.md`: + +```markdown +# 员工资产导入与实体库自动补入修复实施记录 + +## 修改内容 + +- 员工信息维护双 Sheet 导入改为后端顺序编排,保留两个任务 ID。 +- 员工亲属关系维护双 Sheet 导入改为后端顺序编排,保留两个任务 ID。 +- 只导资产 Sheet 时按数据库已有归属正常导入。 +- 新增统一实体库自动补入服务,接入员工亲属、信贷客户、中介、供应商四类业务。 + +## 影响范围 + +- 后端导入服务 +- 实体库来源枚举 +- 关联业务新增和导入链路 + +## 验证情况 + +- 单元测试: +- 编译: +- 接口验证: +- 页面验证: +- 数据清理: +``` + +- [ ] **Step 8: Commit implementation record and final changes** + +Check staged scope: + +```bash +git status --short +git diff --cached --name-status +``` + +Then commit only related files: + +```bash +git add +git commit -m "修复员工资产导入与实体库自动补入" +``` + +Do not use `git add -f` for local `src/test` verification files unless the user explicitly asks to commit tests. + +## Final Verification Checklist + +- [ ] Asset-only employee import succeeds when database employee exists. +- [ ] Asset-only family import succeeds when database family owner exists. +- [ ] Same-file employee + employee asset import succeeds. +- [ ] Same-file family relation + family asset import succeeds. +- [ ] Main Sheet failed row never becomes asset owner context. +- [ ] Two-task-ID response shape remains unchanged. +- [ ] `EnterpriseSource.SUPPLIER` appears in enum endpoint as `供应商`. +- [ ] Auto-fill inserts missing entities only. +- [ ] Auto-fill does not update existing entities. +- [ ] Staff-family manual add and import insert `ent_source=EMP_RELATION`, `data_source=MANUAL/IMPORT`, `risk_level=NULL`. +- [ ] Credit-customer manual add and import insert `ent_source=CREDIT_CUSTOMER`, `data_source=MANUAL/IMPORT`, `risk_level=NULL`. +- [ ] Intermediary manual add and import insert `ent_source=INTERMEDIARY`, `data_source=MANUAL/IMPORT`, `risk_level=1`. +- [ ] Supplier manual add, manual edit, and import insert `ent_source=SUPPLIER`, `data_source=MANUAL/IMPORT`, `risk_level=NULL`. +- [ ] Failed staff-family, credit-customer, intermediary, and supplier import rows do not auto-fill entities. +- [ ] Intermediary missing entity with blank `enterpriseName` / `机构名称` fails that row. +- [ ] Intermediary auto-fill inserts `risk_level=1`. +- [ ] Staff-family, credit-customer, and supplier auto-fill insert `risk_level=NULL`. +- [ ] Supplier empty or invalid `supplierUscc` does not auto-fill and does not become a new manual-save failure condition. +- [ ] Real business pages verified. +- [ ] Test data cleaned. +- [ ] Test-started processes stopped.