Files
ccdi/docs/plans/backend/2026-05-06-staff-asset-import-and-enterprise-autofill-fix-backend-implementation-plan.md

56 KiB

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 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:

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:

mvn -pl ccdi-info-collection -Dtest=CcdiEnumControllerTest test

Expected: FAIL because SUPPLIER/供应商 is not present.

  • Step 3: Add SUPPLIER enum

Modify EnterpriseSource:

SUPPLIER("SUPPLIER", "供应商"),

Place it after INTERMEDIARY(...) and before BOTH(...).

  • Step 4: Run enum test and verify it passes

Run:

mvn -pl ccdi-info-collection -Dtest=CcdiEnumControllerTest test

Expected: PASS.

  • Step 5: Write failing auto-fill service tests

Create EnterpriseAutoFillServiceTest with Mockito. Cover:

@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<List<CcdiEnterpriseBaseInfo>> 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" and still inserts when enterpriseName is null.

  • 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:

mvn -pl ccdi-info-collection -Dtest=EnterpriseAutoFillServiceTest test

Expected: FAIL because EnterpriseAutoFillService does not exist.

  • Step 7: Implement EnterpriseAutoFillService

Create EnterpriseAutoFillService:

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<EnterpriseFillItem> items) {
        Map<String, EnterpriseFillItem> deduped = dedupe(items);
        if (deduped.isEmpty()) {
            return;
        }

        List<CcdiEnterpriseBaseInfo> existing = enterpriseBaseInfoMapper.selectBatchIds(new ArrayList<>(deduped.keySet()));
        Set<String> existingCodes = existing.stream()
                .map(CcdiEnterpriseBaseInfo::getSocialCreditCode)
                .collect(Collectors.toSet());

        List<CcdiEnterpriseBaseInfo> 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<String, EnterpriseFillItem> dedupe(List<EnterpriseFillItem> items) {
        Map<String, EnterpriseFillItem> 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<CcdiEnterpriseBaseInfo> 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:

mvn -pl ccdi-info-collection -Dtest=CcdiEnumControllerTest,EnterpriseAutoFillServiceTest test

Expected: PASS.

  • Step 9: Commit
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:

@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:

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:
public record BaseStaffImportExecutionResult(
        List<CcdiBaseStaff> successRecords,
        List<ImportFailureVO> failures,
        Set<String> successIdCards
) {
}
  • Add:
public BaseStaffImportExecutionResult executeBaseStaffImport(List<CcdiBaseStaffExcel> 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:
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:

mvn -pl ccdi-info-collection -Dtest=CcdiBaseStaffDualImportServiceTest,CcdiBaseStaffImportServiceImplTest test

Expected: PASS.

  • Step 5: Write failing employee asset extra-owner test

In CcdiBaseStaffAssetImportServiceImplTest, add:

@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<List<CcdiAssetInfo>> 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:

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:
public void executeAssetImport(
        List<CcdiBaseStaffAssetInfoExcel> excelList,
        String taskId,
        String userName,
        Map<String, Set<String>> extraOwnerMap
) {
    // Move logic from importAssetInfoAsync here.
}
  • Merge existing database owners with extra owner mappings:
Map<String, Set<String>> ownerMap = buildOwnerMap(personIds);
mergeOwnerMappings(ownerMap, extraOwnerMap);
  • Keep importAssetInfoAsync delegating to:
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:

mvn -pl ccdi-info-collection -Dtest=CcdiBaseStaffDualImportServiceTest,CcdiBaseStaffAssetImportServiceImplTest test

Expected: PASS.

  • Step 9: Commit
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:

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:

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:

BaseStaffImportSubmitResultVO importBaseStaffWithAssets(
        List<CcdiBaseStaffExcel> staffList,
        List<CcdiBaseStaffAssetInfoExcel> assetList
);

Add imports for CcdiBaseStaffAssetInfoExcel and BaseStaffImportSubmitResultVO.

  • Step 5: Implement task initialization and orchestration

In CcdiBaseStaffServiceImpl:

  • Inject:
@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<String, Set<String>>.
  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:

BaseStaffImportSubmitResultVO result = new BaseStaffImportSubmitResultVO();
if (hasStaffRows) {
    result.setStaffTaskId(baseStaffService.importBaseStaff(staffList));
}
if (hasAssetRows) {
    result.setAssetTaskId(baseStaffAssetImportService.importAssetInfo(assetList));
}

with:

BaseStaffImportSubmitResultVO result = baseStaffService.importBaseStaffWithAssets(staffList, assetList);

Keep existing response text:

return AjaxResult.success("导入任务已提交,正在后台处理", result);
  • Step 7: Run employee dual-Sheet tests

Run:

mvn -pl ccdi-info-collection -Dtest=CcdiBaseStaffServiceImplTest,CcdiBaseStaffControllerTest,CcdiBaseStaffDualImportServiceTest,CcdiBaseStaffAssetImportServiceImplTest test

Expected: PASS.

  • Step 8: Commit
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:

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:

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:
public record StaffFmyRelationImportExecutionResult(
        List<CcdiStaffFmyRelation> successRecords,
        List<StaffFmyRelationImportFailureVO> failures,
        Map<String, Set<String>> ownerMap
) {
}
  • Add:
public StaffFmyRelationImportExecutionResult executeRelationImport(
        List<CcdiStaffFmyRelationExcel> 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:

@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<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());
}
  • Step 5: Run family asset test and verify failure

Run:

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:
public void executeAssetImport(
        List<CcdiAssetInfoExcel> excelList,
        String taskId,
        String userName,
        Map<String, Set<String>> 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:

mvn -pl ccdi-info-collection -Dtest=CcdiStaffFmyRelationImportServiceImplTest,CcdiAssetInfoImportServiceImplTest test

Expected: PASS.

  • Step 8: Commit
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:

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:

StaffFmyRelationImportSubmitResultVO importRelationWithAssets(
        List<CcdiStaffFmyRelationExcel> relationList,
        List<CcdiAssetInfoExcel> 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:

result.setRelationTaskId(relationService.importRelation(relationList));
result.setAssetTaskId(assetInfoImportService.importAssetInfo(assetList));

with:

StaffFmyRelationImportSubmitResultVO result = relationService.importRelationWithAssets(relationList, assetList);
  • Step 7: Run family dual-Sheet tests

Run:

mvn -pl ccdi-info-collection -Dtest=CcdiStaffFmyRelationServiceImplTest,CcdiStaffFmyRelationControllerTest,CcdiStaffFmyRelationImportServiceImplTest,CcdiAssetInfoImportServiceImplTest test

Expected: PASS.

  • Step 8: Commit
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:

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:

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:

@Resource
private EnterpriseAutoFillService enterpriseAutoFillService;

After existsByPersonIdAndSocialCreditCode passes and before relationMapper.insert(relation), call:

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:

mvn -pl ccdi-info-collection -Dtest=CcdiStaffEnterpriseRelationServiceImplTest,CcdiStaffEnterpriseRelationImportServiceImplTest,CcdiCustEnterpriseRelationServiceImplTest,CcdiCustEnterpriseRelationImportServiceImplTest test

Expected: PASS.

  • Step 9: Commit
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

  • 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 no-name auto-fill contract tests

Add local tests proving the backend contract:

  • Do not add enterpriseName to CcdiIntermediaryEnterpriseRelationAddDTO.

  • Do not add 机构名称 to CcdiIntermediaryEnterpriseRelationExcel.

  • Missing entity + no enterprise name succeeds after auto-fill.

  • The auto-filled EnterpriseFillItem.enterpriseName() is null.

  • Step 2: Write failing manual relation test

In CcdiIntermediaryServiceImplTest, add:

@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();
    service.insertIntermediaryEnterpriseRelation("owner-biz", addDTO);

    verify(enterpriseAutoFillService).ensureExists(argThat(item ->
            "INTERMEDIARY".equals(item.entSource())
                    && "MANUAL".equals(item.dataSource())
                    && item.enterpriseName() == null));
}
  • Step 3: Write failing import relation test

Change CcdiIntermediaryEnterpriseRelationImportServiceImplTest#importEnterpriseRelationAsync_shouldFailWhenEnterpriseDoesNotExist into a success test:

  • Owner exists.

  • Entity does not exist.

  • Excel row has no enterprise name column.

  • Relation combination does not exist.

  • EnterpriseAutoFillService.ensureExistsBatch is called with INTERMEDIARY/IMPORT and enterpriseName == null.

  • Relation insert succeeds.

  • Step 4: Run tests and verify failure

Run:

mvn -pl ccdi-info-collection -Dtest=CcdiIntermediaryServiceImplTest,CcdiIntermediaryEnterpriseRelationImportServiceImplTest test

Expected: FAIL because current code requires the enterprise to exist.

  • Step 5: Keep intermediary API and Excel contract unchanged

Do not modify these files for the intermediary no-name rule:

  • CcdiIntermediaryEnterpriseRelationAddDTO
  • CcdiIntermediaryEnterpriseRelationExcel
  • IntermediaryEnterpriseRelationImportFailureVO

The intermediary relation only provides socialCreditCode; missing entity auto-fill must insert a minimal entity with enterprise_name = NULL. The current initialization SQL already defines ccdi_enterprise_base_info.enterprise_name as nullable.

  • 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, call auto-fill with enterpriseName = null before enterpriseRelationMapper.insert(relation):
private void ensureIntermediaryEnterpriseExists(CcdiIntermediaryEnterpriseRelationAddDTO addDTO) {
    if (enterpriseBaseInfoMapper.selectById(addDTO.getSocialCreditCode()) != null) {
        return;
    }
    enterpriseAutoFillService.ensureExists(new EnterpriseAutoFillService.EnterpriseFillItem(
            addDTO.getSocialCreditCode(),
            null,
            EnterpriseSource.INTERMEDIARY.getCode(),
            DataSource.MANUAL.getCode(),
            SecurityUtils.getUsername()
    ));
}
  • Step 7: Replace import existing-enterprise check

In CcdiIntermediaryEnterpriseRelationImportServiceImpl:

  • Remove the existing enterprise code query from row failure validation; EnterpriseAutoFillService will check existing entities in batch and insert only missing ones.

  • If a row references a missing entity, do not fail just because the entity is missing.

  • If a row references an existing entity, do not update that entity.

  • Build EnterpriseFillItem from successful Excel rows with enterpriseName = null.

  • Call ensureExistsBatch before saveBatch(successRecords, 500).

  • Keep owner missing, DB duplicate relation, and file duplicate relation failures unchanged.

  • Step 8: Run intermediary tests

Run:

mvn -pl ccdi-info-collection -Dtest=CcdiIntermediaryServiceImplTest,CcdiIntermediaryEnterpriseRelationImportServiceImplTest,EnterpriseAutoFillServiceTest test

Expected: PASS.

  • Step 9: Commit
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
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:

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:

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<CcdiPurchaseTransactionSupplier> 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):

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:

mvn -pl ccdi-info-collection -Dtest=CcdiPurchaseTransactionFeatureContractTest,CcdiPurchaseTransactionServiceImplTest,CcdiPurchaseTransactionImportServiceImplTest test

Expected: PASS.

  • Step 7: Commit
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:

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:

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:

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 without requiring enterprise name, with enterprise_name IS NULL, ent_source=INTERMEDIARY, data_source=MANUAL, risk_level=1.

  • Intermediary entity relation import inserts missing entity without requiring enterprise name, with enterprise_name IS NULL, ent_source=INTERMEDIARY, data_source=IMPORT, risk_level=1.

  • 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:

bin/mysql_utf8_exec.sh <sql-file>
  • 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:

# 员工资产导入与实体库自动补入修复实施记录

## 修改内容

- 员工信息维护双 Sheet 导入改为后端顺序编排,保留两个任务 ID。
- 员工亲属关系维护双 Sheet 导入改为后端顺序编排,保留两个任务 ID。
- 只导资产 Sheet 时按数据库已有归属正常导入。
- 新增统一实体库自动补入服务,接入员工亲属、信贷客户、中介、供应商四类业务。

## 影响范围

- 后端导入服务
- 实体库来源枚举
- 关联业务新增和导入链路

## 验证情况

- 单元测试:
- 编译:
- 接口验证:
- 页面验证:
- 数据清理:
  • Step 8: Commit implementation record and final changes

Check staged scope:

git status --short
git diff --cached --name-status

Then commit only related files:

git add <related files>
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 enterprise_name=NULL, 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 does not require enterpriseName / 机构名称.
  • 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.