2026-10-10-address-contact-backfill-realname.md 19 KB

地址联系人回填真实姓名 实现计划

面向 AI 代理的工作者: 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(- [ ])语法来跟踪进度。

目标: 用户保存商城收货地址或收货人时,若其 users.real_name 为空,则用联系人姓名自动回填真实姓名(幂等,仅首次生效)。

架构: 新增独立组件 RealNameBackfillService(依赖 UserMapper,提供 backfill(userId, contactName));在 UserAddressService.create/update 与 ConsigneeService.save(新增/编辑两支)保存成功后调用。回填失败 try-catch 静默,不阻塞地址保存主流程(风格参照 ConsigneeService.syncUserRegion())。零 schema 变更、零前端改动、无新接口。

技术栈: Java 8 / Spring Boot 2.7.18 / MyBatis-Plus / JUnit 5 + Mockito(纯单测,无 Spring 上下文、无 MySQL 连接)

规格: docs/superpowers/specs/2026-10-10-address-contact-backfill-realname-design.md

⚠️ 工作区约束: 当前工作区有与本任务无关的未提交改动(ProductRecommendationController.java、ProductRecommendationService.java、若干根目录临时文件等)。所有 git add 必须精确指定本任务文件,禁止 git add -A / git add .。

⚠️ 测试运行约束: cfc-backend/pom.xml:28 有 <skipTests>true</skipTests>,跑单测必须显式 -DskipTests=false,否则输出 Tests are skipped 是假通过。


文件结构

文件 动作 职责
cfc-backend/src/main/java/com/etotem/cfc/service/RealNameBackfillService.java 创建 真实姓名回填守卫:realName 空白才写入,幂等,失败静默
cfc-backend/src/main/java/com/etotem/cfc/service/UserAddressService.java 修改 create/update 保存成功后调用 backfill
cfc-backend/src/main/java/com/etotem/cfc/service/ConsigneeService.java 修改 save 新增/编辑两支保存成功后调用 backfill
cfc-backend/src/test/java/com/etotem/cfc/service/RealNameBackfillServiceTest.java 创建 回填守卫逻辑单测(8 用例)
cfc-backend/src/test/java/com/etotem/cfc/service/UserAddressServiceBackfillTest.java 创建 UserAddressService 接线单测(2 用例)
cfc-backend/src/test/java/com/etotem/cfc/service/ConsigneeServiceBackfillTest.java 创建 ConsigneeService 接线单测(2 用例)

关键顺序决策(任务 3): ConsigneeService.save 中 backfill 必须放在 syncUserRegion(...) 之后。syncUserRegion 在 realName 为空时回退用 nickname 匹配联系人姓名;若先回填 realName 会改变其匹配结果,破坏既有地区同步行为。放后面可完全保持既有行为不变。

任务分解:

  • 任务 1:RealNameBackfillService(TDD:测试→失败→实现→通过→commit)
  • 任务 2:UserAddressService 接线(TDD)
  • 任务 3:ConsigneeService 接线(TDD)
  • 任务 4:整体验证(compile + 三测试类 12 用例全绿)

任务 1:RealNameBackfillService 回填组件

文件:

  • 创建:cfc-backend/src/main/java/com/etotem/cfc/service/RealNameBackfillService.java
  • 测试:cfc-backend/src/test/java/com/etotem/cfc/service/RealNameBackfillServiceTest.java

  • [ ] 步骤 1:编写失败的测试

创建 cfc-backend/src/test/java/com/etotem/cfc/service/RealNameBackfillServiceTest.java:

package com.etotem.cfc.service;

import com.etotem.cfc.entity.User;
import com.etotem.cfc.mapper.UserMapper;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
class RealNameBackfillServiceTest {

    @Mock
    private UserMapper userMapper;

    @InjectMocks
    private RealNameBackfillService realNameBackfillService;

    @Test
    void backfill_真实姓名为null时回填联系人姓名() {
        User user = new User();
        user.setRealName(null);
        when(userMapper.selectById(1L)).thenReturn(user);

        realNameBackfillService.backfill(1L, "张三");

        assertEquals("张三", user.getRealName());
        verify(userMapper).updateById(user);
    }

    @Test
    void backfill_真实姓名为空白时回填联系人姓名() {
        User user = new User();
        user.setRealName("   ");
        when(userMapper.selectById(1L)).thenReturn(user);

        realNameBackfillService.backfill(1L, "李四");

        assertEquals("李四", user.getRealName());
        verify(userMapper).updateById(user);
    }

    @Test
    void backfill_联系人姓名去除首尾空白后回填() {
        User user = new User();
        user.setRealName(null);
        when(userMapper.selectById(1L)).thenReturn(user);

        realNameBackfillService.backfill(1L, "  王五  ");

        assertEquals("王五", user.getRealName());
        verify(userMapper).updateById(user);
    }

    @Test
    void backfill_真实姓名已有值时不覆盖() {
        User user = new User();
        user.setRealName("原有姓名");
        when(userMapper.selectById(1L)).thenReturn(user);

        realNameBackfillService.backfill(1L, "张三");

        assertEquals("原有姓名", user.getRealName());
        verify(userMapper, never()).updateById(any());
    }

    @Test
    void backfill_联系人姓名为null时跳过() {
        realNameBackfillService.backfill(1L, null);

        verifyNoInteractions(userMapper);
    }

    @Test
    void backfill_联系人姓名为空白时跳过() {
        realNameBackfillService.backfill(1L, "   ");

        verifyNoInteractions(userMapper);
    }

    @Test
    void backfill_userId为null时跳过() {
        realNameBackfillService.backfill(null, "张三");

        verifyNoInteractions(userMapper);
    }

    @Test
    void backfill_用户不存在时跳过() {
        when(userMapper.selectById(99L)).thenReturn(null);

        realNameBackfillService.backfill(99L, "张三");

        verify(userMapper, never()).updateById(any());
    }
}
  • 步骤 2:运行测试验证失败

运行(PowerShell,仓库根目录):

cd cfc-backend
mvn test -DskipTests=false -Dtest=RealNameBackfillServiceTest

预期:COMPILATION ERROR — 找不到符号 RealNameBackfillService(类不存在,这就是本任务的红灯)。

  • 步骤 3:编写最少实现代码

创建 cfc-backend/src/main/java/com/etotem/cfc/service/RealNameBackfillService.java:

package com.etotem.cfc.service;

import com.etotem.cfc.entity.User;
import com.etotem.cfc.mapper.UserMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

/**
 * 真实姓名回填:用户第一次输入地址/收货人时,若 users.real_name 尚未填写,
 * 用联系人姓名回填(规格:docs/superpowers/specs/2026-10-10-address-contact-backfill-realname-design.md)。
 * 幂等:回填一次后 realName 非空,后续保存不再触发。
 * 风格参照 ConsigneeService.syncUserRegion():失败静默,不阻塞主流程。
 */
@Service
public class RealNameBackfillService {

    private static final Logger log = LoggerFactory.getLogger(RealNameBackfillService.class);

    @Resource
    private UserMapper userMapper;

    /**
     * 若 userId 对应用户的 realName 为空白,则用 contactName(去首尾空白后)回填。
     * contactName 空白、userId 为空、用户不存在时直接跳过。
     */
    public void backfill(Long userId, String contactName) {
        try {
            if (userId == null) return;
            if (contactName == null || contactName.trim().isEmpty()) return;
            User user = userMapper.selectById(userId);
            if (user == null) return;
            String realName = user.getRealName();
            if (realName != null && !realName.trim().isEmpty()) return;
            user.setRealName(contactName.trim());
            userMapper.updateById(user);
        } catch (Exception e) {
            log.warn("[RealNameBackfill] 回填失败 userId={} : {}", userId, e.getMessage());
        }
    }
}
  • 步骤 4:运行测试验证通过

运行(同步骤 2):

mvn test -DskipTests=false -Dtest=RealNameBackfillServiceTest

预期:Tests run: 8, Failures: 0, Errors: 0, Skipped: 0,BUILD SUCCESS。

  • [ ] 步骤 5:Commit

    git add cfc-backend/src/main/java/com/etotem/cfc/service/RealNameBackfillService.java cfc-backend/src/test/java/com/etotem/cfc/service/RealNameBackfillServiceTest.java
    git commit -m "feat(realname): 新增 RealNameBackfillService 真实姓名回填组件"
    

任务 2:UserAddressService 接线

文件:

  • 修改:cfc-backend/src/main/java/com/etotem/cfc/service/UserAddressService.java(字段区 17-18 行、create 37-49 行、update 51-63 行)
  • 测试:cfc-backend/src/test/java/com/etotem/cfc/service/UserAddressServiceBackfillTest.java

  • [ ] 步骤 1:编写失败的测试

创建 cfc-backend/src/test/java/com/etotem/cfc/service/UserAddressServiceBackfillTest.java:

package com.etotem.cfc.service;

import com.etotem.cfc.entity.UserAddress;
import com.etotem.cfc.mapper.UserAddressMapper;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
class UserAddressServiceBackfillTest {

    @Mock
    private UserAddressMapper userAddressMapper;

    @Mock
    private RealNameBackfillService realNameBackfillService;

    @InjectMocks
    private UserAddressService userAddressService;

    @Test
    void create_保存成功后以收货人姓名回填真实姓名() {
        UserAddress address = new UserAddress();
        address.setReceiverName("张三");
        address.setIsDefault(0);

        userAddressService.create(address, 1L);

        verify(userAddressMapper).insert(address);
        verify(realNameBackfillService).backfill(1L, "张三");
    }

    @Test
    void update_保存成功后以收货人姓名回填真实姓名() {
        UserAddress existing = new UserAddress();
        existing.setId(10L);
        existing.setUserId(1L);
        when(userAddressMapper.selectOne(any())).thenReturn(existing);

        UserAddress address = new UserAddress();
        address.setId(10L);
        address.setReceiverName("李四");
        address.setIsDefault(0);

        userAddressService.update(address, 1L);

        verify(userAddressMapper).updateById(address);
        verify(realNameBackfillService).backfill(1L, "李四");
    }
}

说明:update 走到 updateById 前会经 get() → SortUtil.applySort(SortContext 无值时为 no-op,单测安全)。

  • [ ] 步骤 2:运行测试验证失败

    mvn test -DskipTests=false -Dtest=UserAddressServiceBackfillTest
    

预期:Tests run: 2, Failures: 2, Errors: 0 — 两个用例均因 verify 失败:Wanted but not invoked: realNameBackfillService.backfill(...)(此时 UserAddressService 还没有回填调用;Mockito 未注入不存在的字段,verify 的是 mock 对象本身,断言"未发生"。Mockito 的 WantedButNotInvoked 是 AssertionError 子类,故计入 Failures)。

  • 步骤 3:编写最少实现代码

修改 UserAddressService.java 三处:

字段区(18 行后)新增:

    @Resource
    private RealNameBackfillService realNameBackfillService;

create 方法(48 行 userAddressMapper.insert(address); 之后)追加一行:

        userAddressMapper.insert(address);
        realNameBackfillService.backfill(userId, address.getReceiverName());

update 方法(62 行 userAddressMapper.updateById(address); 之后)追加一行:

        userAddressMapper.updateById(address);
        realNameBackfillService.backfill(userId, address.getReceiverName());

RealNameBackfillService 同包,无需新增 import。

  • [ ] 步骤 4:运行测试验证通过

    mvn test -DskipTests=false -Dtest=UserAddressServiceBackfillTest
    

预期:Tests run: 2, Failures: 0, Errors: 0,BUILD SUCCESS。

  • [ ] 步骤 5:Commit

    git add cfc-backend/src/main/java/com/etotem/cfc/service/UserAddressService.java cfc-backend/src/test/java/com/etotem/cfc/service/UserAddressServiceBackfillTest.java
    git commit -m "feat(realname): 商城地址保存后回填用户真实姓名"
    

任务 3:ConsigneeService 接线

文件:

  • 修改:cfc-backend/src/main/java/com/etotem/cfc/service/ConsigneeService.java(字段区 18-22 行、save 41-76 行)
  • 测试:cfc-backend/src/test/java/com/etotem/cfc/service/ConsigneeServiceBackfillTest.java

  • [ ] 步骤 1:编写失败的测试

创建 cfc-backend/src/test/java/com/etotem/cfc/service/ConsigneeServiceBackfillTest.java:

package com.etotem.cfc.service;

import com.etotem.cfc.entity.Consignee;
import com.etotem.cfc.mapper.ConsigneeMapper;
import com.etotem.cfc.mapper.UserMapper;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
class ConsigneeServiceBackfillTest {

    @Mock
    private ConsigneeMapper consigneeMapper;

    @Mock
    private UserMapper userMapper;

    @Mock
    private RealNameBackfillService realNameBackfillService;

    @InjectMocks
    private ConsigneeService consigneeService;

    @Test
    void save_新增分支保存成功后以联系人姓名回填真实姓名() {
        Consignee consignee = new Consignee();
        consignee.setUserId(1L);
        consignee.setName("张三");
        consignee.setIsDefault(0);

        consigneeService.save(consignee);

        verify(consigneeMapper).insert(consignee);
        verify(realNameBackfillService).backfill(1L, "张三");
    }

    @Test
    void save_编辑分支保存成功后以联系人姓名回填真实姓名() {
        Consignee existing = new Consignee();
        existing.setId(10L);
        existing.setUserId(1L);
        when(consigneeMapper.selectById(10L)).thenReturn(existing);

        Consignee consignee = new Consignee();
        consignee.setId(10L);
        consignee.setName("李四");
        consignee.setIsDefault(0);

        consigneeService.save(consignee);

        verify(consigneeMapper).updateById(consignee);
        verify(realNameBackfillService).backfill(1L, "李四");
    }
}

说明:save 内部 syncUserRegion 会调 userMapper.selectById(mock 默认返回 null → 其内部 early-return),@Mock UserMapper 仅为满足字段注入。

  • [ ] 步骤 2:运行测试验证失败

    mvn test -DskipTests=false -Dtest=ConsigneeServiceBackfillTest
    

预期:Tests run: 2, Failures: 2, Errors: 0 — Wanted but not invoked: realNameBackfillService.backfill(...)(WantedButNotInvoked 是 AssertionError 子类,计入 Failures)。

  • 步骤 3:编写最少实现代码

修改 ConsigneeService.java 三处:

字段区(22 行后)新增:

    @Resource
    private RealNameBackfillService realNameBackfillService;

save 编辑分支(61 行 syncUserRegion(existing.getUserId(), consignee); 之后、return 之前)追加:

            syncUserRegion(existing.getUserId(), consignee);
            // 回填写在 syncUserRegion 之后:保持既有同步逻辑看到回填前的 realName,行为不变
            realNameBackfillService.backfill(existing.getUserId(), consignee.getName());
            return Result.success(consignee.getId());

save 新增分支(73 行 syncUserRegion(consignee.getUserId(), consignee); 之后、return 之前)追加:

            syncUserRegion(consignee.getUserId(), consignee);
            realNameBackfillService.backfill(consignee.getUserId(), consignee.getName());
            return Result.success(consignee.getId());

RealNameBackfillService 同包,无需新增 import。

  • [ ] 步骤 4:运行测试验证通过

    mvn test -DskipTests=false -Dtest=ConsigneeServiceBackfillTest
    

预期:Tests run: 2, Failures: 0, Errors: 0,BUILD SUCCESS。

  • [ ] 步骤 5:Commit

    git add cfc-backend/src/main/java/com/etotem/cfc/service/ConsigneeService.java cfc-backend/src/test/java/com/etotem/cfc/service/ConsigneeServiceBackfillTest.java
    git commit -m "feat(realname): 收货人保存后回填用户真实姓名"
    

任务 4:整体验证

文件: 无新改动,仅验证。

  • [ ] 步骤 1:全量编译

    cd cfc-backend
    mvn clean compile
    

预期:BUILD SUCCESS,无编译错误。

  • [ ] 步骤 2:运行本任务全部 12 个单测

    mvn test -DskipTests=false -Dtest="RealNameBackfillServiceTest,UserAddressServiceBackfillTest,ConsigneeServiceBackfillTest"
    

预期:Tests run: 12, Failures: 0, Errors: 0, Skipped: 0,BUILD SUCCESS。

  • [ ] 步骤 3:核对提交记录

    git log --oneline -4
    

预期:看到 3 个 feat 提交 + 之前的 docs 提交,且 git status 中本任务涉及的 6 个文件全部干净(无关的既有改动仍在,属正常)。


测试矩阵 ↔ 用例映射(规格覆盖度自证)

规格测试要求 承载用例
realName 为空 + 创建地址 → 回填 UserAddressServiceBackfillTest.create_保存成功后以收货人姓名回填真实姓名
realName 为空 + 编辑地址 → 回填 UserAddressServiceBackfillTest.update_保存成功后以收货人姓名回填真实姓名
realName 已有 → 不覆盖 RealNameBackfillServiceTest.backfill_真实姓名已有值时不覆盖
联系人空白 → 跳过 RealNameBackfillServiceTest.backfill_联系人姓名为null时跳过 / backfill_联系人姓名为空白时跳过
ConsigneeService 新增、编辑两支 ConsigneeServiceBackfillTest 两个用例
空白 realName(空串/纯空格)也回填 RealNameBackfillServiceTest.backfill_真实姓名为空白时回填联系人姓名
trim 后回填 RealNameBackfillServiceTest.backfill_联系人姓名去除首尾空白后回填
防御路径(userId null / 用户不存在) backfill_userId为null时跳过 / backfill_用户不存在时跳过

守卫逻辑集中在 RealNameBackfillServiceTest(8 用例),两个接线测试只验证"保存成功后以正确参数调用 backfill"——合起来完整覆盖规格的测试矩阵,避免在接线层重复 mock 同一守卫逻辑。