通讯录没起名时不再取对方设备的名字 (#3473)

* 别把对方给设备起的名字抄进自己的通讯录

通讯录里没起名的时候,以前会跑去设备表查目标设备的名字。那是设备主人在智控台上自己起的,
直接抄进调用方的通讯录,等于把别人的昵称漏了出去。现在改成一律用 MAC 尾号称呼,
只有目标是自己那台设备时,才用自己设备表里的名字。补了单测,覆盖对方已起名、目标是自己、
显式起名、目标没登记这几种情况。

* 取自己的设备名改用不会撞行的查法

设备表里同一个 MAC 可能有两行只差大小写,一次只准取一条的查法撞上就抛异常,保存通讯录整个接口带崩。改成按归属查的那一种,按 id 取第一条。

补齐自己那台设备在设备表里没起名时退回尾号称呼的用例,另有几条用例的名字跟实际测的东西对不上,一并改掉。
This commit is contained in:
hrz authored and GitHub committed 2026-10-29 11:56:48 +08:00
1 parent 4dbed0ab1d
commit 942ac2fe12
2 files changed
+197 -7

No files matched your search

@@ -95,6 +95,10 @@ public class DeviceAddressBookServiceImpl implements DeviceAddressBookService {
callerNickname = targetBook.get(bookKey(callerMac));
}
if (StringUtils.isBlank(callerNickname)) {
// 主叫显示名取的是主叫方自己那台设备在设备表里的名字,不是对方的。
// 对方那边给主叫方起的称呼在上面的 targetBook 里已经查过。
// 产品上按「来电显示」的口径处理,所以这里不套通讯录那套「不抄对方私产」的限制
// 这个接口没有登录用户上下文,归属校验那个查法在这里会返回空,只能用不校验归属的查法
DeviceEntity callerDevice = deviceService.getDeviceByMacAddress(callerMac);
if (callerDevice != null) {
callerNickname = callerDevice.getAlias();
@@ -212,11 +216,18 @@ public class DeviceAddressBookServiceImpl implements DeviceAddressBookService {
DeviceAddressBookEntity entity = new DeviceAddressBookEntity();
entity.setMacAddress(macAddress);
entity.setTargetMac(targetMac);
// 如果 alias 为空,就默认使用设备名称;对方设备尚未登记时退回尾号称呼
// 调用方没起名就退回尾号称呼。对方设备在智控台上起的名字是对方的私产,
// 抄进调用方的通讯录等于把别人的昵称漏出去,所以别人的设备一律不去设备表取名
if (StringUtils.isBlank(alias)) {
DeviceEntity targetDevice = deviceService.getDeviceByMacAddress(targetMac);
if (targetDevice != null) {
alias = targetDevice.getAlias();
// 只有给自己那台设备起名时才用设备表里的名字,那本来就是调用方自己的
if (isOwnDevice(macAddress, targetMac)) {
// 用带归属校验的那个查法。设备表里同一个 MAC 可能有两行只差大小写,
// 一次只准取一条的那种查法撞上这种数据会抛 TooManyResultsException,
// 整个保存接口带崩;按 id 取第一条,撞行也不报错
DeviceEntity ownDevice = deviceService.getOwnedDeviceByMacAddress(macAddress);
if (ownDevice != null && StringUtils.isNotBlank(ownDevice.getAlias())) {
alias = ownDevice.getAlias();
}
}
// 设备登记了但用户没起过名,alias 建表就是 null,这是默认状态
// 不是异常。这时候落个 null 进去,缓存刷新会把整条跳过,
@@ -304,6 +315,16 @@ public class DeviceAddressBookServiceImpl implements DeviceAddressBookService {
return macAddress == null ? null : macAddress.toLowerCase();
}
/**
* 目标设备是不是调用方自己那台
*
* <p>
* MAC 不区分大小写,比较前先按通讯录的键归一化,跟别处的比较口径保持一致
*/
private static boolean isOwnDevice(String macAddress, String targetMac) {
return bookKey(macAddress) != null && bookKey(macAddress).equals(bookKey(targetMac));
}
private String formatMacAsDeviceName(String mac) {
if (StringUtils.isBlank(mac) || mac.length() < 2) {
return mac;
@@ -1,5 +1,6 @@
package xiaozhi.modules.device.service.impl;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -21,6 +22,7 @@ import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.ibatis.exceptions.TooManyResultsException;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.MockedStatic;
@@ -75,7 +77,7 @@ class DeviceAddressBookServiceImplTest {
addressBookDao, redisUtils, deviceService, sysParamsService);
when(addressBookDao.selectOne(any())).thenReturn(null);
when(addressBookDao.selectList(null)).thenReturn(List.of());
when(deviceService.getDeviceByMacAddress(OTHER_MAC)).thenReturn(null);
when(deviceService.getOwnedDeviceByMacAddress(anyString())).thenReturn(null);
service.saveOrUpdate(MAC, OTHER_MAC, null, true);
@@ -85,10 +87,11 @@ class DeviceAddressBookServiceImplTest {
assertNotNull(entity.getAlias());
assertEquals("尾号为66的设备", entity.getAlias());
assertTrue(entity.getHasPermission());
verify(deviceService, never()).getOwnedDeviceByMacAddress(anyString());
}
@Test
void blankAliasOnRegisteredTargetWithoutNicknameFallsBackToMacSuffix() {
void blankAliasOnAnotherDeviceFallsBackToMacSuffixWithoutAskingTheDeviceTable() {
DeviceAddressBookDao addressBookDao = mock(DeviceAddressBookDao.class);
RedisUtils redisUtils = mock(RedisUtils.class);
DeviceService deviceService = mock(DeviceService.class);
@@ -101,6 +104,7 @@ class DeviceAddressBookServiceImplTest {
registered.setMacAddress(OTHER_MAC);
registered.setAlias(null);
when(deviceService.getDeviceByMacAddress(OTHER_MAC)).thenReturn(registered);
when(deviceService.getOwnedDeviceByMacAddress(OTHER_MAC)).thenReturn(registered);
service.saveOrUpdate(MAC, OTHER_MAC, null, true);
@@ -108,6 +112,171 @@ class DeviceAddressBookServiceImplTest {
verify(addressBookDao).insertAddressBook(entityCaptor.capture());
assertEquals("尾号为66的设备", entityCaptor.getValue().getAlias(),
"登记了但没起名的设备落个空名,缓存刷新会跳过这条,对方永远呼不过来");
verify(deviceService, never()).getDeviceByMacAddress(anyString());
verify(deviceService, never()).getOwnedDeviceByMacAddress(anyString());
}
@Test
void blankAliasOnRegisteredTargetDoesNotTakeOverTheirNickname() {
DeviceAddressBookDao addressBookDao = mock(DeviceAddressBookDao.class);
RedisUtils redisUtils = mock(RedisUtils.class);
DeviceService deviceService = mock(DeviceService.class);
SysParamsService sysParamsService = mock(SysParamsService.class);
DeviceAddressBookServiceImpl service = new DeviceAddressBookServiceImpl(
addressBookDao, redisUtils, deviceService, sysParamsService);
when(addressBookDao.selectOne(any())).thenReturn(null);
when(addressBookDao.selectList(null)).thenReturn(List.of());
DeviceEntity theirs = new DeviceEntity();
theirs.setMacAddress(OTHER_MAC);
theirs.setAlias("老王的音响");
when(deviceService.getDeviceByMacAddress(OTHER_MAC)).thenReturn(theirs);
when(deviceService.getOwnedDeviceByMacAddress(OTHER_MAC)).thenReturn(theirs);
service.saveOrUpdate(MAC, OTHER_MAC, null, true);
ArgumentCaptor<DeviceAddressBookEntity> entityCaptor = ArgumentCaptor.forClass(DeviceAddressBookEntity.class);
verify(addressBookDao).insertAddressBook(entityCaptor.capture());
assertEquals("尾号为66的设备", entityCaptor.getValue().getAlias(),
"对方在智控台上给这台设备起的名字被抄进了调用方的通讯录");
verify(deviceService, never()).getDeviceByMacAddress(anyString());
verify(deviceService, never()).getOwnedDeviceByMacAddress(anyString());
}
@Test
void blankAliasOnOwnDeviceUsesTheNameFromItsOwnDeviceRow() {
DeviceAddressBookDao addressBookDao = mock(DeviceAddressBookDao.class);
RedisUtils redisUtils = mock(RedisUtils.class);
DeviceService deviceService = mock(DeviceService.class);
SysParamsService sysParamsService = mock(SysParamsService.class);
DeviceAddressBookServiceImpl service = new DeviceAddressBookServiceImpl(
addressBookDao, redisUtils, deviceService, sysParamsService);
when(addressBookDao.selectOne(any())).thenReturn(null);
when(addressBookDao.selectList(null)).thenReturn(List.of());
DeviceEntity mine = new DeviceEntity();
mine.setMacAddress(MAC);
mine.setAlias("我的书房");
when(deviceService.getOwnedDeviceByMacAddress(MAC)).thenReturn(mine);
service.saveOrUpdate(MAC, MAC, null, true);
ArgumentCaptor<DeviceAddressBookEntity> entityCaptor = ArgumentCaptor.forClass(DeviceAddressBookEntity.class);
verify(addressBookDao).insertAddressBook(entityCaptor.capture());
assertEquals("我的书房", entityCaptor.getValue().getAlias(),
"给自己那台设备起名,用的就是自己设备表里的名字");
}
@Test
void ownDeviceWithoutAliasInDeviceTableFallsBackToMacSuffix() {
DeviceAddressBookDao addressBookDao = mock(DeviceAddressBookDao.class);
RedisUtils redisUtils = mock(RedisUtils.class);
DeviceService deviceService = mock(DeviceService.class);
SysParamsService sysParamsService = mock(SysParamsService.class);
DeviceAddressBookServiceImpl service = new DeviceAddressBookServiceImpl(
addressBookDao, redisUtils, deviceService, sysParamsService);
when(addressBookDao.selectOne(any())).thenReturn(null);
when(addressBookDao.selectList(null)).thenReturn(List.of());
DeviceEntity mine = new DeviceEntity();
mine.setMacAddress(MAC);
mine.setAlias(null);
when(deviceService.getOwnedDeviceByMacAddress(MAC)).thenReturn(mine);
service.saveOrUpdate(MAC, MAC, null, true);
ArgumentCaptor<DeviceAddressBookEntity> entityCaptor = ArgumentCaptor.forClass(DeviceAddressBookEntity.class);
verify(addressBookDao).insertAddressBook(entityCaptor.capture());
assertEquals("尾号为55的设备", entityCaptor.getValue().getAlias(),
"自己的设备在设备表里也没起名,落个空名的话缓存刷新会跳过这条");
}
@Test
void duplicateDeviceRowsDifferingOnlyInCaseDoNotBreakNaming() {
DeviceAddressBookDao addressBookDao = mock(DeviceAddressBookDao.class);
RedisUtils redisUtils = mock(RedisUtils.class);
DeviceService deviceService = mock(DeviceService.class);
SysParamsService sysParamsService = mock(SysParamsService.class);
DeviceAddressBookServiceImpl service = new DeviceAddressBookServiceImpl(
addressBookDao, redisUtils, deviceService, sysParamsService);
when(addressBookDao.selectOne(any())).thenReturn(null);
when(addressBookDao.selectList(null)).thenReturn(List.of());
// 设备表里同一个 MAC 有两行只差大小写,一次只准取一条的那种查法会直接抛异常
when(deviceService.getDeviceByMacAddress(anyString()))
.thenThrow(new TooManyResultsException("selectOne 撞上两行只差大小写的数据"));
DeviceEntity first = new DeviceEntity();
first.setMacAddress(MAC);
first.setAlias("我的书房");
when(deviceService.getOwnedDeviceByMacAddress(MAC)).thenReturn(first);
assertDoesNotThrow(() -> service.saveOrUpdate(MAC, MAC, null, true),
"同 MAC 有两行只差大小写时,取名不能把整个接口带崩");
ArgumentCaptor<DeviceAddressBookEntity> entityCaptor = ArgumentCaptor.forClass(DeviceAddressBookEntity.class);
verify(addressBookDao).insertAddressBook(entityCaptor.capture());
assertEquals("我的书房", entityCaptor.getValue().getAlias(),
"撞行时按 id 取第一条那条的名字");
}
@Test
void ownDeviceIsRecognizedWhateverCaseTheAddressIsWrittenIn() {
DeviceAddressBookDao addressBookDao = mock(DeviceAddressBookDao.class);
RedisUtils redisUtils = mock(RedisUtils.class);
DeviceService deviceService = mock(DeviceService.class);
SysParamsService sysParamsService = mock(SysParamsService.class);
DeviceAddressBookServiceImpl service = new DeviceAddressBookServiceImpl(
addressBookDao, redisUtils, deviceService, sysParamsService);
when(addressBookDao.selectOne(any())).thenReturn(null);
when(addressBookDao.selectList(null)).thenReturn(List.of());
DeviceEntity mine = new DeviceEntity();
mine.setMacAddress(HEX_MAC);
mine.setAlias("我的书房");
when(deviceService.getOwnedDeviceByMacAddress(HEX_MAC)).thenReturn(mine);
service.saveOrUpdate(HEX_MAC, HEX_MAC.toUpperCase(), null, true);
ArgumentCaptor<DeviceAddressBookEntity> entityCaptor = ArgumentCaptor.forClass(DeviceAddressBookEntity.class);
verify(addressBookDao).insertAddressBook(entityCaptor.capture());
assertEquals("我的书房", entityCaptor.getValue().getAlias(),
"同一个地址写成大小写两种样子,就被当成别人的设备了");
}
@Test
void explicitAliasIsKeptAsIs() {
DeviceAddressBookDao addressBookDao = mock(DeviceAddressBookDao.class);
RedisUtils redisUtils = mock(RedisUtils.class);
DeviceService deviceService = mock(DeviceService.class);
SysParamsService sysParamsService = mock(SysParamsService.class);
DeviceAddressBookServiceImpl service = new DeviceAddressBookServiceImpl(
addressBookDao, redisUtils, deviceService, sysParamsService);
when(addressBookDao.selectOne(any())).thenReturn(null);
when(addressBookDao.selectList(null)).thenReturn(List.of());
service.saveOrUpdate(MAC, OTHER_MAC, "客厅", true);
ArgumentCaptor<DeviceAddressBookEntity> entityCaptor = ArgumentCaptor.forClass(DeviceAddressBookEntity.class);
verify(addressBookDao).insertAddressBook(entityCaptor.capture());
assertEquals("客厅", entityCaptor.getValue().getAlias(),
"自己给别人的设备起名是通讯录的本意,不能被兜底逻辑改掉");
verify(deviceService, never()).getDeviceByMacAddress(anyString());
verify(deviceService, never()).getOwnedDeviceByMacAddress(anyString());
}
@Test
void blankAliasOnUnregisteredTargetDoesNotBlowUp() {
DeviceAddressBookDao addressBookDao = mock(DeviceAddressBookDao.class);
RedisUtils redisUtils = mock(RedisUtils.class);
DeviceService deviceService = mock(DeviceService.class);
SysParamsService sysParamsService = mock(SysParamsService.class);
DeviceAddressBookServiceImpl service = new DeviceAddressBookServiceImpl(
addressBookDao, redisUtils, deviceService, sysParamsService);
when(addressBookDao.selectOne(any())).thenReturn(null);
when(addressBookDao.selectList(null)).thenReturn(List.of());
when(deviceService.getOwnedDeviceByMacAddress(anyString())).thenReturn(null);
assertDoesNotThrow(() -> service.saveOrUpdate(MAC, OTHER_MAC, null, true),
"目标设备没登记时接口得正常返回,不能抛异常");
ArgumentCaptor<DeviceAddressBookEntity> entityCaptor = ArgumentCaptor.forClass(DeviceAddressBookEntity.class);
verify(addressBookDao).insertAddressBook(entityCaptor.capture());
assertEquals("尾号为66的设备", entityCaptor.getValue().getAlias());
}
@Test
@@ -325,7 +494,7 @@ class DeviceAddressBookServiceImplTest {
DeviceService deviceService = mock(DeviceService.class);
when(addressBookDao.selectOne(any())).thenReturn(null);
when(addressBookDao.selectList(null)).thenReturn(List.of());
when(deviceService.getDeviceByMacAddress(anyString())).thenReturn(null);
when(deviceService.getOwnedDeviceByMacAddress(anyString())).thenReturn(null);
DeviceAddressBookServiceImpl service = new DeviceAddressBookServiceImpl(
addressBookDao, redisUtils, deviceService, mock(SysParamsService.class));