feat(device): 实现设备与账号绑定管理机制

- 引入 ClientAccountDevice 表管理设备与账号绑定关系
- 重构设备注册逻辑,支持多账号绑定同一设备
- 新增设备配额检查,基于账号维度限制设备数量
-优化设备移除逻辑,仅解除绑定而非物理删除- 改进设备列表查询,通过账号ID关联获取设备信息
- 更新心跳任务,支持向设备绑定的所有账号发送心跳
- 调整设备API参数,增加username字段用于权限校验
-修复HTTP请求编码问题,统一使用UTF-8字符集
- 增强错误处理,携带错误码信息便于前端识别
- 移除设备表中的username字段,解耦设备与用户名关联
This commit is contained in:
2025-10-22 09:51:55 +08:00
parent 901d67d2dc
commit 17b6a7b9f9
29 changed files with 589 additions and 277 deletions

View File

@@ -0,0 +1,61 @@
package com.ruoyi.system.domain;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.common.core.domain.BaseEntity;
import java.util.Date;
/**
* 客户端账号-设备关联对象 client_account_device
*/
public class ClientAccountDevice extends BaseEntity {
private static final long serialVersionUID = 1L;
private Long id;
private Long accountId;
private String deviceId;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date bindTime;
private String status; // active/removed
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getAccountId() {
return accountId;
}
public void setAccountId(Long accountId) {
this.accountId = accountId;
}
public String getDeviceId() {
return deviceId;
}
public void setDeviceId(String deviceId) {
this.deviceId = deviceId;
}
public Date getBindTime() {
return bindTime;
}
public void setBindTime(Date bindTime) {
this.bindTime = bindTime;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
}

View File

@@ -13,8 +13,6 @@ public class ClientDevice extends BaseEntity {
private static final long serialVersionUID = 1L;
private Long id;
@Excel(name = "用户名")
private String username;
@Excel(name = "设备ID")
private String deviceId;
@Excel(name = "设备名")
@@ -36,8 +34,6 @@ public class ClientDevice extends BaseEntity {
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getDeviceId() { return deviceId; }
public void setDeviceId(String deviceId) { this.deviceId = deviceId; }
public String getName() { return name; }

View File

@@ -0,0 +1,48 @@
package com.ruoyi.system.mapper;
import com.ruoyi.system.domain.ClientAccountDevice;
import com.ruoyi.system.domain.ClientDevice;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 客户端账号-设备关联Mapper接口
*/
public interface ClientAccountDeviceMapper {
/**
* 根据账号ID查询已绑定的设备列表
*/
List<ClientDevice> selectDevicesByAccountId(@Param("accountId") Long accountId);
/**
* 根据设备ID查询绑定的账号ID列表
*/
List<Long> selectAccountIdsByDeviceId(@Param("deviceId") String deviceId);
/**
* 查询账号绑定的设备数量(不包括已移除的)
*/
int countActiveDevicesByAccountId(@Param("accountId") Long accountId);
/**
* 检查账号和设备是否已绑定
*/
ClientAccountDevice selectByAccountIdAndDeviceId(@Param("accountId") Long accountId, @Param("deviceId") String deviceId);
/**
* 插入账号-设备绑定
*/
int insert(ClientAccountDevice binding);
/**
* 更新绑定状态
*/
int updateStatus(@Param("accountId") Long accountId, @Param("deviceId") String deviceId, @Param("status") String status);
/**
* 删除绑定
*/
int delete(@Param("accountId") Long accountId, @Param("deviceId") String deviceId);
}

View File

@@ -7,14 +7,10 @@ import java.util.List;
public interface ClientDeviceMapper {
ClientDevice selectByDeviceId(@Param("deviceId") String deviceId);
ClientDevice selectByDeviceIdAndUsername(@Param("deviceId") String deviceId, @Param("username") String username);
List<ClientDevice> selectByUsername(@Param("username") String username);
List<ClientDevice> selectOnlineDevices();
int insert(ClientDevice device);
int updateByDeviceId(ClientDevice device);
int updateByDeviceIdAndUsername(ClientDevice device);
int deleteByDeviceId(@Param("deviceId") String deviceId);
int countByUsername(@Param("username") String username);
}

View File

@@ -10,7 +10,9 @@ import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.web.client.RestTemplate;
import com.ruoyi.system.domain.BanmaAccount;
import com.ruoyi.system.domain.ClientAccount;
import com.ruoyi.system.mapper.BanmaAccountMapper;
import com.ruoyi.system.mapper.ClientAccountMapper;
import com.ruoyi.system.service.IBanmaAccountService;
/**
@@ -21,9 +23,21 @@ public class BanmaAccountServiceImpl implements IBanmaAccountService {
@Autowired
private BanmaAccountMapper mapper;
@Autowired
private ClientAccountMapper clientAccountMapper;
private final RestTemplate restTemplate = new RestTemplate();
private static final String LOGIN_URL = "https://banma365.cn/api/login";
private int getAccountLimit(String clientUsername) {
if (clientUsername == null) return 3;
ClientAccount client = clientAccountMapper.selectClientAccountByUsername(clientUsername);
if (client == null) return 1;
if ("paid".equals(client.getAccountType()) && client.getExpireTime() != null && new Date().before(client.getExpireTime())) {
return 3;
}
return 1;
}
@Override
public List<BanmaAccount> listSimple() {
return listSimple(null);
@@ -51,6 +65,17 @@ public class BanmaAccountServiceImpl implements IBanmaAccountService {
entity.setClientUsername(clientUsername);
}
// 新增时检查数量限制
if (entity.getId() == null && entity.getClientUsername() != null) {
int limit = getAccountLimit(entity.getClientUsername());
BanmaAccount query = new BanmaAccount();
query.setClientUsername(entity.getClientUsername());
int count = mapper.selectList(query).size();
if (count >= limit) {
throw new RuntimeException("账号数量已达上限(" + limit + "个),请升级订阅或删除其他账号");
}
}
if (entity.getId() == null) {
mapper.insert(entity);
} else {

View File

@@ -0,0 +1,87 @@
<?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="com.ruoyi.system.mapper.ClientAccountDeviceMapper">
<resultMap id="ClientAccountDeviceMap" type="com.ruoyi.system.domain.ClientAccountDevice">
<id property="id" column="id"/>
<result property="accountId" column="account_id"/>
<result property="deviceId" column="device_id"/>
<result property="bindTime" column="bind_time"/>
<result property="status" column="status"/>
<result property="createTime" column="create_time"/>
<result property="updateTime" column="update_time"/>
</resultMap>
<resultMap id="ClientDeviceResultMap" type="com.ruoyi.system.domain.ClientDevice">
<id property="id" column="id"/>
<result property="deviceId" column="device_id"/>
<result property="name" column="name"/>
<result property="os" column="os"/>
<result property="status" column="status"/>
<result property="ip" column="ip"/>
<result property="location" column="location"/>
<result property="lastActiveAt" column="last_active_at"/>
<result property="trialExpireTime" column="trial_expire_time"/>
<result property="createTime" column="create_time"/>
<result property="updateTime" column="update_time"/>
</resultMap>
<!-- 根据账号ID查询已绑定的设备列表 -->
<select id="selectDevicesByAccountId" resultMap="ClientDeviceResultMap">
SELECT
d.id, d.device_id, d.name, d.os, d.status, d.ip, d.location,
d.last_active_at, d.trial_expire_time, d.create_time, d.update_time
FROM client_device d
INNER JOIN client_account_device ad ON d.device_id = ad.device_id
WHERE ad.account_id = #{accountId}
AND ad.status = 'active'
ORDER BY d.last_active_at DESC
</select>
<!-- 根据设备ID查询绑定的账号ID列表 -->
<select id="selectAccountIdsByDeviceId" resultType="java.lang.Long">
SELECT account_id
FROM client_account_device
WHERE device_id = #{deviceId}
AND status = 'active'
</select>
<!-- 查询账号绑定的设备数量(不包括已移除的) -->
<select id="countActiveDevicesByAccountId" resultType="int">
SELECT COUNT(1)
FROM client_account_device
WHERE account_id = #{accountId}
AND status = 'active'
</select>
<!-- 检查账号和设备是否已绑定 -->
<select id="selectByAccountIdAndDeviceId" resultMap="ClientAccountDeviceMap">
SELECT *
FROM client_account_device
WHERE account_id = #{accountId}
AND device_id = #{deviceId}
</select>
<!-- 插入账号-设备绑定 -->
<insert id="insert" parameterType="com.ruoyi.system.domain.ClientAccountDevice" useGeneratedKeys="true" keyProperty="id">
INSERT INTO client_account_device(account_id, device_id, bind_time, status, create_time, update_time)
VALUES(#{accountId}, #{deviceId}, #{bindTime}, #{status}, now(), now())
</insert>
<!-- 更新绑定状态 -->
<update id="updateStatus">
UPDATE client_account_device
SET status = #{status}, update_time = now()
WHERE account_id = #{accountId}
AND device_id = #{deviceId}
</update>
<!-- 删除绑定 -->
<delete id="delete">
DELETE FROM client_account_device
WHERE account_id = #{accountId}
AND device_id = #{deviceId}
</delete>
</mapper>

View File

@@ -3,7 +3,6 @@
<mapper namespace="com.ruoyi.system.mapper.ClientDeviceMapper">
<resultMap id="ClientDeviceMap" type="com.ruoyi.system.domain.ClientDevice">
<id property="id" column="id"/>
<result property="username" column="username"/>
<result property="deviceId" column="device_id"/>
<result property="name" column="name"/>
<result property="os" column="os"/>
@@ -17,38 +16,16 @@
</resultMap>
<select id="selectByDeviceId" resultMap="ClientDeviceMap">
select * from client_device where device_id = #{deviceId}
</select>
<select id="selectByDeviceIdAndUsername" resultMap="ClientDeviceMap">
select * from client_device where device_id = #{deviceId} and username = #{username}
</select>
<select id="selectByUsername" resultMap="ClientDeviceMap">
select * from client_device where username = #{username} and status != 'removed' order by update_time desc
SELECT * FROM client_device WHERE device_id = #{deviceId}
</select>
<insert id="insert" parameterType="com.ruoyi.system.domain.ClientDevice" useGeneratedKeys="true" keyProperty="id">
insert into client_device(username, device_id, name, os, status, ip, location, last_active_at, trial_expire_time, create_time, update_time)
values(#{username}, #{deviceId}, #{name}, #{os}, #{status}, #{ip}, #{location}, #{lastActiveAt}, #{trialExpireTime}, now(), now())
INSERT INTO client_device(device_id, name, os, status, ip, location, last_active_at, trial_expire_time, create_time, update_time)
VALUES(#{deviceId}, #{name}, #{os}, #{status}, #{ip}, #{location}, #{lastActiveAt}, #{trialExpireTime}, now(), now())
</insert>
<update id="updateByDeviceId" parameterType="com.ruoyi.system.domain.ClientDevice">
update client_device
set username = #{username},
name = #{name},
os = #{os},
status = #{status},
ip = #{ip},
location = #{location},
last_active_at = #{lastActiveAt},
trial_expire_time = #{trialExpireTime},
update_time = now()
where device_id = #{deviceId}
</update>
<update id="updateByDeviceIdAndUsername" parameterType="com.ruoyi.system.domain.ClientDevice">
update client_device
UPDATE client_device
<set>
<if test="name != null">name = #{name},</if>
<if test="os != null">os = #{os},</if>
@@ -59,19 +36,15 @@
<if test="trialExpireTime != null">trial_expire_time = #{trialExpireTime},</if>
update_time = now()
</set>
where device_id = #{deviceId} and username = #{username}
WHERE device_id = #{deviceId}
</update>
<delete id="deleteByDeviceId">
delete from client_device where device_id = #{deviceId}
DELETE FROM client_device WHERE device_id = #{deviceId}
</delete>
<select id="countByUsername" resultType="int">
select count(1) from client_device where username = #{username}
</select>
<select id="selectOnlineDevices" resultMap="ClientDeviceMap">
select * from client_device where status = 'online' order by last_active_at desc
SELECT * FROM client_device WHERE status = 'online' ORDER BY last_active_at DESC
</select>
</mapper>

View File

@@ -169,21 +169,22 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<select id="selectClientInfoList" parameterType="ClientInfo" resultMap="ClientInfoResult">
select
d.device_id as client_id,
d.username,
a.username,
d.os as os_name,
d.ip as ip_address,
d.last_active_at as last_active_time,
d.create_time as auth_time,
ad.bind_time as auth_time,
CASE WHEN d.status = 'online' THEN '1' ELSE '0' END as online,
a.account_name as hostname,
'' as app_version,
'' as os_version,
'' as java_version
from client_device d
left join client_account a on d.username COLLATE utf8mb4_unicode_ci = a.username
left join client_account_device ad on d.device_id = ad.device_id and ad.status = 'active'
left join client_account a on ad.account_id = a.id
<where>
<if test="clientId != null and clientId != ''">AND d.device_id like concat('%', #{clientId}, '%')</if>
<if test="username != null and username != ''">AND d.username like concat('%', #{username}, '%')</if>
<if test="username != null and username != ''">AND a.username like concat('%', #{username}, '%')</if>
<if test="osName != null and osName != ''">AND d.os like concat('%', #{osName}, '%')</if>
<if test="online != null and online != ''">
AND d.status =