feat: 新增设备互相通讯功能

feat: 新增通讯录页面
This commit is contained in:
Sakura-RanChen
2026-05-26 16:37:39 +08:00
parent 7f73dae1bf
commit ce68c83eea
48 changed files with 2361 additions and 7 deletions
@@ -188,4 +188,11 @@ public class RedisKeys {
return "ota:upload:count:" + username;
}
/**
* 设备通讯录缓存Key
*/
public static String getAddressBookKey() {
return "device:address_book:all";
}
}
@@ -9,6 +9,7 @@ import xiaozhi.common.constant.Constant;
import xiaozhi.common.redis.RedisKeys;
import xiaozhi.common.redis.RedisUtils;
import xiaozhi.modules.config.service.ConfigService;
import xiaozhi.modules.device.service.DeviceAddressBookService;
import xiaozhi.modules.sys.service.SysParamsService;
@Configuration
@@ -24,6 +25,9 @@ public class SystemInitConfig {
@Autowired
private RedisUtils redisUtils;
@Autowired
private DeviceAddressBookService deviceAddressBookService;
@PostConstruct
public void init() {
// 检查版本号
@@ -37,5 +41,8 @@ public class SystemInitConfig {
sysParamsService.initServerSecret();
configService.getConfig(false);
// 初始化设备通讯录缓存
deviceAddressBookService.refreshCache();
}
}
@@ -22,12 +22,15 @@ import xiaozhi.common.redis.RedisKeys;
import xiaozhi.common.redis.RedisUtils;
import xiaozhi.common.user.UserDetail;
import xiaozhi.common.utils.Result;
import xiaozhi.modules.device.dto.DeviceAddressBookAliasDTO;
import xiaozhi.modules.device.dto.DeviceAddressBookPermissionDTO;
import xiaozhi.modules.device.dto.DeviceManualAddDTO;
import xiaozhi.modules.device.dto.DeviceRegisterDTO;
import xiaozhi.modules.device.dto.DeviceToolsCallReqDTO;
import xiaozhi.modules.device.dto.DeviceUnBindDTO;
import xiaozhi.modules.device.dto.DeviceUpdateDTO;
import xiaozhi.modules.device.entity.DeviceEntity;
import xiaozhi.modules.device.service.DeviceAddressBookService;
import xiaozhi.modules.device.service.DeviceService;
import xiaozhi.modules.security.user.SecurityUser;
import xiaozhi.modules.sys.service.SysParamsService;
@@ -37,11 +40,13 @@ import xiaozhi.modules.sys.service.SysParamsService;
@RequestMapping("/device")
public class DeviceController {
private final DeviceService deviceService;
private final DeviceAddressBookService deviceAddressBookService;
private final RedisUtils redisUtils;
private final SysParamsService sysParamsService;
public DeviceController(DeviceService deviceService, RedisUtils redisUtils, SysParamsService sysParamsService) {
public DeviceController(DeviceService deviceService, DeviceAddressBookService deviceAddressBookService, RedisUtils redisUtils, SysParamsService sysParamsService) {
this.deviceService = deviceService;
this.deviceAddressBookService = deviceAddressBookService;
this.redisUtils = redisUtils;
this.sysParamsService = sysParamsService;
}
@@ -159,4 +164,37 @@ public class DeviceController {
response.setMsg("Tools called successfully");
return response.ok(result);
}
@GetMapping("/address-book/{macAddress}")
@Operation(summary = "获取设备通讯录")
@RequiresPermissions("sys:role:normal")
public Result<Object> getAddressBook(@PathVariable String macAddress) {
return new Result<Object>().ok(deviceAddressBookService.getAddressBookList(macAddress));
}
@GetMapping("/address-book/lookup")
@Operation(summary = "根据昵称查找目标设备")
public Result<Map<String, String>> lookupByNickname(String callerMac, String nickname) {
Map<String, String> result = deviceAddressBookService.lookupByNickname(callerMac, nickname);
if (result == null) {
return new Result<Map<String, String>>().error("未找到对应设备");
}
return new Result<Map<String, String>>().ok(result);
}
@PutMapping("/address-book/alias")
@Operation(summary = "更新设备通讯录别名")
@RequiresPermissions("sys:role:normal")
public Result<Void> updateAlias(@Valid @RequestBody DeviceAddressBookAliasDTO dto) {
deviceAddressBookService.saveOrUpdate(dto.getMacAddress(), dto.getTargetMac(), dto.getAlias(), null);
return new Result<Void>();
}
@PutMapping("/address-book/permission")
@Operation(summary = "更新设备通讯录权限")
@RequiresPermissions("sys:role:normal")
public Result<Void> updatePermission(@Valid @RequestBody DeviceAddressBookPermissionDTO dto) {
deviceAddressBookService.saveOrUpdate(dto.getMacAddress(), dto.getTargetMac(), null, dto.getHasPermission());
return new Result<Void>();
}
}
@@ -0,0 +1,29 @@
package xiaozhi.modules.device.dao;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import xiaozhi.modules.device.entity.DeviceAddressBookEntity;
@Mapper
public interface DeviceAddressBookDao extends BaseMapper<DeviceAddressBookEntity> {
/**
* 获取设备通讯录列表
*/
List<DeviceAddressBookEntity> getAddressBookList(@Param("macAddress") String macAddress);
/**
* 更新别名
*/
void updateAlias(@Param("macAddress") String macAddress, @Param("targetMac") String targetMac, @Param("alias") String alias);
/**
* 更新权限
*/
void updatePermission(@Param("macAddress") String macAddress, @Param("targetMac") String targetMac, @Param("hasPermission") Boolean hasPermission);
}
@@ -0,0 +1,21 @@
package xiaozhi.modules.device.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
@Schema(description = "更新设备通讯录别名")
public class DeviceAddressBookAliasDTO {
@NotBlank(message = "MAC地址不能为空")
@Schema(description = "本设备MAC地址")
private String macAddress;
@NotBlank(message = "目标MAC地址不能为空")
@Schema(description = "对方设备MAC地址")
private String targetMac;
@Schema(description = "我对对方的称呼")
private String alias;
}
@@ -0,0 +1,21 @@
package xiaozhi.modules.device.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
@Schema(description = "更新设备通讯录权限")
public class DeviceAddressBookPermissionDTO {
@NotBlank(message = "MAC地址不能为空")
@Schema(description = "本设备MAC地址")
private String macAddress;
@NotBlank(message = "目标MAC地址不能为空")
@Schema(description = "对方设备MAC地址")
private String targetMac;
@Schema(description = "是否有权限呼叫")
private Boolean hasPermission;
}
@@ -0,0 +1,49 @@
package xiaozhi.modules.device.entity;
import java.util.Date;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("ai_device_address_book")
@Schema(description = "设备通讯录")
public class DeviceAddressBookEntity {
@TableId(type = IdType.INPUT)
@Schema(description = "本设备MAC地址")
private String macAddress;
@Schema(description = "对方设备MAC地址")
private String targetMac;
@Schema(description = "我对对方的称呼")
private String alias;
@Schema(description = "是否有权限呼叫")
private Boolean hasPermission;
@TableField(fill = FieldFill.INSERT)
@Schema(description = "创建人")
private Long creator;
@TableField(fill = FieldFill.INSERT)
@Schema(description = "创建时间")
private Date createDate;
@TableField(fill = FieldFill.INSERT_UPDATE)
@Schema(description = "更新人")
private Long updater;
@TableField(fill = FieldFill.INSERT_UPDATE)
@Schema(description = "更新时间")
private Date updateDate;
}
@@ -0,0 +1,47 @@
package xiaozhi.modules.device.service;
import java.util.List;
import java.util.Map;
import xiaozhi.modules.device.entity.DeviceAddressBookEntity;
public interface DeviceAddressBookService {
/**
* 获取设备通讯录列表
*/
List<DeviceAddressBookEntity> getAddressBookList(String macAddress);
/**
* 获取所有设备的通讯录(全局缓存用)
*/
Map<String, Map<String, String>> getAllAddressBooks();
/**
* 更新别名
*/
void updateAlias(String macAddress, String targetMac, String alias);
/**
* 更新权限
*/
void updatePermission(String macAddress, String targetMac, Boolean hasPermission);
/**
* 添加或更新通讯录记录
*/
void saveOrUpdate(String macAddress, String targetMac, String alias, Boolean hasPermission);
/**
* 刷新通讯录缓存
*/
void refreshCache();
/**
* 根据昵称查找目标设备信息
* @param callerMac 主叫方MAC地址
* @param nickname 被叫方昵称
* @return {targetMac: 目标MAC, callerNickname: 目标如何称呼主叫方}
*/
Map<String, String> lookupByNickname(String callerMac, String nickname);
}
@@ -0,0 +1,147 @@
package xiaozhi.modules.device.service.impl;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import xiaozhi.common.redis.RedisKeys;
import xiaozhi.common.redis.RedisUtils;
import xiaozhi.modules.device.dao.DeviceAddressBookDao;
import xiaozhi.modules.device.entity.DeviceAddressBookEntity;
import xiaozhi.modules.device.service.DeviceAddressBookService;
@Service
public class DeviceAddressBookServiceImpl implements DeviceAddressBookService {
private final DeviceAddressBookDao deviceAddressBookDao;
private final RedisUtils redisUtils;
public DeviceAddressBookServiceImpl(DeviceAddressBookDao deviceAddressBookDao, RedisUtils redisUtils) {
this.deviceAddressBookDao = deviceAddressBookDao;
this.redisUtils = redisUtils;
}
@Override
public List<DeviceAddressBookEntity> getAddressBookList(String macAddress) {
return deviceAddressBookDao.getAddressBookList(macAddress);
}
@Override
@SuppressWarnings("unchecked")
public Map<String, Map<String, String>> getAllAddressBooks() {
Object cached = redisUtils.get(RedisKeys.getAddressBookKey());
if (cached != null) {
return (Map<String, Map<String, String>>) cached;
}
refreshCache();
return (Map<String, Map<String, String>>) redisUtils.get(RedisKeys.getAddressBookKey());
}
@Override
public Map<String, String> lookupByNickname(String callerMac, String nickname) {
Map<String, Map<String, String>> allBooks = getAllAddressBooks();
Map<String, String> callerBook = allBooks.get(callerMac.toLowerCase());
if (callerBook == null) {
return null;
}
// 从缓存获取 targetMac,格式: "mac|permission"
String targetMacWithPerm = callerBook.get(nickname);
if (targetMacWithPerm == null) {
return null;
}
// 解析 targetMac 和 permission
String[] parts = targetMacWithPerm.split("\\|");
String targetMac = parts[0];
boolean hasPermission = parts.length > 1 && "1".equals(parts[1]);
Map<String, String> targetBook = allBooks.get(targetMac.toLowerCase());
if (targetBook == null) {
return null;
}
// 检查双向关系:目标设备是否也添加了主叫方为联系人
String callerNickname = targetBook.get(callerMac.toLowerCase());
Map<String, String> result = new HashMap<>();
result.put("targetMac", targetMac);
result.put("callerNickname", callerNickname);
result.put("hasPermission", hasPermission ? "true" : "false");
return result;
}
@Override
public void refreshCache() {
Map<String, Map<String, String>> result = new HashMap<>();
List<DeviceAddressBookEntity> allRecords = deviceAddressBookDao.selectList(null);
Map<String, String> reverseMap = new HashMap<>();
Map<String, Boolean> permissionMap = new HashMap<>();
for (DeviceAddressBookEntity entity : allRecords) {
String macA = entity.getMacAddress().toLowerCase();
String macB = entity.getTargetMac().toLowerCase();
String alias = entity.getAlias();
Boolean hasPermission = entity.getHasPermission();
if (alias != null && !alias.isEmpty()) {
// 反向记录: (macB, macA) = B对A的称呼
reverseMap.put(macB + ":" + macA, alias);
}
// 记录 A 对 B 是否有权限呼叫
permissionMap.put(macA + ":" + macB, hasPermission != null && hasPermission);
}
for (DeviceAddressBookEntity entity : allRecords) {
String macA = entity.getMacAddress().toLowerCase();
String macB = entity.getTargetMac().toLowerCase();
String aliasAtoB = entity.getAlias();
result.computeIfAbsent(macA, k -> new HashMap<>());
result.computeIfAbsent(macB, k -> new HashMap<>());
if (aliasAtoB != null && !aliasAtoB.isEmpty()) {
// A对B的映射: nickname -> macB|permission
Boolean perm = entity.getHasPermission();
String permStr = (perm != null && perm) ? "1" : "0";
result.get(macA).put(aliasAtoB, macB + "|" + permStr);
}
// B对A的映射: macA -> nickname(查反向记录)
String aliasBtoA = reverseMap.get(macA + ":" + macB);
if (aliasBtoA != null && !aliasBtoA.isEmpty()) {
result.get(macB).put(macA, aliasBtoA);
}
}
redisUtils.set(RedisKeys.getAddressBookKey(), result);
}
@Override
public void updateAlias(String macAddress, String targetMac, String alias) {
deviceAddressBookDao.updateAlias(macAddress, targetMac, alias);
refreshCache();
}
@Override
public void updatePermission(String macAddress, String targetMac, Boolean hasPermission) {
deviceAddressBookDao.updatePermission(macAddress, targetMac, hasPermission);
refreshCache();
}
@Override
public void saveOrUpdate(String macAddress, String targetMac, String alias, Boolean hasPermission) {
QueryWrapper<DeviceAddressBookEntity> wrapper = new QueryWrapper<>();
wrapper.eq("mac_address", macAddress).eq("target_mac", targetMac);
DeviceAddressBookEntity record = deviceAddressBookDao.selectOne(wrapper);
if (record == null) {
DeviceAddressBookEntity entity = new DeviceAddressBookEntity();
entity.setMacAddress(macAddress);
entity.setTargetMac(targetMac);
entity.setAlias(alias);
entity.setHasPermission(hasPermission);
deviceAddressBookDao.insert(entity);
} else {
if (alias != null) {
deviceAddressBookDao.updateAlias(macAddress, targetMac, alias);
}
if (hasPermission != null) {
deviceAddressBookDao.updatePermission(macAddress, targetMac, hasPermission);
}
}
refreshCache();
}
}
@@ -87,6 +87,7 @@ public class ShiroConfig {
filterMap.put("/user/retrieve-password", "anon");
// 将config路径使用server服务过滤器
filterMap.put("/config/**", "server");
filterMap.put("/device/address-book/lookup", "server");
filterMap.put("/agent/chat-history/report", "server");
filterMap.put("/agent/chat-history/download/**", "anon");
filterMap.put("/agent/chat-summary/**", "server");
@@ -0,0 +1,31 @@
-- 新增呼叫设备工具配置
SET @data_exists = (SELECT COUNT(*) FROM ai_model_provider WHERE id = 'SYSTEM_PLUGIN_CALL_DEVICE');
SET @sql = IF(@data_exists = 0,
'INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `fields`, `sort`, `creator`, `create_date`, `updater`,
`update_date`) VALUES (''SYSTEM_PLUGIN_CALL_DEVICE'', ''Plugin'', ''call_device'', ''呼叫设备'', ''[{"key":"gateway_url","label":"网关地址","type":
"string","default":"http://127.0.0.1:8007"},{"key":"gateway_secret","label":"网关密钥","type":"string"}]'', 85, 1988490863118454785, ''2026-05-18
12:00:00'', 1988490863118454785, ''2026-05-18 12:00:00'')',
'SELECT ''data already exists, skip'' AS msg');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 更新系统菜单配置,增加联系人管理菜单
UPDATE sys_params
SET param_value = REPLACE(
param_value,
'"mcpAccessPoint":{"name":"feature.mcpAccessPoint.name","enabled":true,"description":"feature.mcpAccessPoint.description"}',
'"mcpAccessPoint":{"name":"feature.mcpAccessPoint.name","enabled":true,"description":"feature.mcpAccessPoint.description"},"addressBook":{"name":"feature.addressBook.name","enabled":false,"description":"feature.addressBook.description"}'
)
WHERE param_code = 'system-web.menu';
-- 创建设备通讯录表
CREATE TABLE IF NOT EXISTS `ai_device_address_book` (
`mac_address` VARCHAR(64) NOT NULL COMMENT '本设备MAC地址',
`target_mac` VARCHAR(64) NOT NULL COMMENT '对方设备MAC地址',
`alias` VARCHAR(64) DEFAULT NULL COMMENT '别名',
`has_permission` TINYINT(1) DEFAULT TRUE COMMENT '是否有权限呼叫',
`creator` BIGINT DEFAULT NULL COMMENT '创建人',
`create_date` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` BIGINT DEFAULT NULL COMMENT '更新人',
`update_date` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`mac_address`, `target_mac`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='设备通讯录表';
@@ -669,3 +669,10 @@ databaseChangeLog:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202605121509.sql
- changeSet:
id: 202605251425
author: RanChen
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202605251425.sql
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="xiaozhi.modules.device.dao.DeviceAddressBookDao">
<select id="getAddressBookList" resultType="xiaozhi.modules.device.entity.DeviceAddressBookEntity">
SELECT mac_address, target_mac, alias, has_permission, creator, create_date, updater, update_date
FROM ai_device_address_book
WHERE mac_address = #{macAddress}
ORDER BY update_date DESC
</select>
<update id="updateAlias">
UPDATE ai_device_address_book
SET alias = #{alias}, update_date = NOW()
WHERE mac_address = #{macAddress} AND target_mac = #{targetMac}
</update>
<update id="updatePermission">
UPDATE ai_device_address_book
SET has_permission = #{hasPermission}, update_date = NOW()
WHERE mac_address = #{macAddress} AND target_mac = #{targetMac}
</update>
<insert id="insert">
INSERT INTO ai_device_address_book (mac_address, target_mac, alias, has_permission, creator, create_date, updater, update_date)
VALUES (#{macAddress}, #{targetMac}, #{alias}, #{hasPermission}, #{creator}, NOW(), #{updater}, NOW())
</insert>
</mapper>