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

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

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

```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<CcdiBaseStaff> successRecords,
        List<ImportFailureVO> failures,
        Set<String> successIdCards
) {
}
```

- Add:

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

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

```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<CcdiBaseStaffAssetInfoExcel> excelList,
        String taskId,
        String userName,
        Map<String, Set<String>> extraOwnerMap
) {
    // Move logic from importAssetInfoAsync here.
}
```

- Merge existing database owners with extra owner mappings:

```java
Map<String, Set<String>> 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<CcdiBaseStaffExcel> staffList,
        List<CcdiBaseStaffAssetInfoExcel> 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<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:

```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<CcdiStaffFmyRelation> successRecords,
        List<StaffFmyRelationImportFailureVO> failures,
        Map<String, Set<String>> ownerMap
) {
}
```

- Add:

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

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

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

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

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

```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();
    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:

```bash
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)`:

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

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

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

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

```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 <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.
