面向 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 会改变其匹配结果,破坏既有地区同步行为。放后面可完全保持既有行为不变。
任务分解:
RealNameBackfillService(TDD:测试→失败→实现→通过→commit)UserAddressService 接线(TDD)ConsigneeService 接线(TDD)文件:
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());
}
}
运行(PowerShell,仓库根目录):
cd cfc-backend
mvn test -DskipTests=false -Dtest=RealNameBackfillServiceTest
预期:COMPILATION ERROR — 找不到符号 RealNameBackfillService(类不存在,这就是本任务的红灯)。
创建 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());
}
}
}
运行(同步骤 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 真实姓名回填组件"
文件:
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)。
修改 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): 商城地址保存后回填用户真实姓名"
文件:
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)。
修改 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): 收货人保存后回填用户真实姓名"
文件: 无新改动,仅验证。
[ ] 步骤 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 同一守卫逻辑。