This commit is contained in:
2025-10-10 10:06:56 +08:00
parent 4fbe51d625
commit 6f22c9bffd
37 changed files with 2176 additions and 1183 deletions

View File

@@ -54,6 +54,8 @@ public class ClientAccountController extends BaseController {
private JwtRsaKeyService jwtRsaKeyService;
@Autowired
private SseHubService sseHubService;
@Autowired
private ClientDeviceMapper clientDeviceMapper;
/**
* 查询账号列表
@@ -137,9 +139,6 @@ public class ClientAccountController extends BaseController {
if (!"0".equals(account.getStatus())) {
return AjaxResult.error("账号已被停用");
}
if (account.getExpireTime() != null && account.getExpireTime().before(new Date())) {
return AjaxResult.error("账号已过期");
}
String clientId = loginData.get("clientId");
String accessToken = Jwts.builder()
.setHeaderParam("kid", jwtRsaKeyService.getKeyId())
@@ -151,7 +150,6 @@ public class ClientAccountController extends BaseController {
.claim("clientId", clientId)
.signWith(SignatureAlgorithm.RS256, jwtRsaKeyService.getPrivateKey())
.compact();
Map<String, Object> result = new HashMap<>();
result.put("accessToken", accessToken);
result.put("permissions", account.getPermissions());
@@ -181,6 +179,15 @@ public class ClientAccountController extends BaseController {
result.put("username", username);
result.put("permissions", account.getPermissions());
result.put("accountName", account.getAccountName());
result.put("expireTime", account.getExpireTime());
// 计算VIP状态
if (account.getExpireTime() != null) {
boolean isExpired = account.getExpireTime().before(new Date());
result.put("isVip", !isExpired);
} else {
result.put("isVip", false);
}
return AjaxResult.success("验证成功", result);
}
@@ -203,36 +210,53 @@ public class ClientAccountController extends BaseController {
/**
* 客户端账号注册
* 新设备注册送3天VIP同一设备ID重复注册不赠送
*/
@PostMapping("/register")
public AjaxResult register(@RequestBody ClientAccount clientAccount) {
if (StringUtils.isEmpty(clientAccount.getUsername()) || StringUtils.isEmpty(clientAccount.getPassword())) {
return AjaxResult.error("用户名和密码不能为空");
}
if (clientAccount.getPassword().length() < 6) {
return AjaxResult.error("密码长度不能少于6位");
}
if (clientAccountService.selectClientAccountByUsername(clientAccount.getUsername()) != null) {
return AjaxResult.error("用户名已存在");
}
public AjaxResult register(@RequestBody Map<String, String> registerData) {
String username = registerData.get("username");
String password = registerData.get("password");
String deviceId = registerData.get("deviceId");
ClientAccount clientAccount = new ClientAccount();
clientAccount.setUsername(username);
clientAccount.setAccountName(username);
clientAccount.setCreateBy("system");
clientAccount.setStatus("0");
clientAccount.setPermissions("{\"amazon\":true,\"rakuten\":true,\"zebra\":true}");
clientAccount.setPassword(passwordEncoder.encode(clientAccount.getPassword()));
if (clientAccount.getExpireTime() == null) {
Date expireDate = new Date(System.currentTimeMillis() + 90L * 24 * 60 * 60 * 1000);
clientAccount.setExpireTime(expireDate);
clientAccount.setPassword(passwordEncoder.encode(password));
// 检查设备ID是否已注册过赠送VIP逻辑
boolean isNewDevice = true;
if (!StringUtils.isEmpty(deviceId)) {
ClientDevice existingDevice = clientDeviceMapper.selectByDeviceId(deviceId);
isNewDevice = (existingDevice == null);
}
int vipDays;
if (isNewDevice) {
vipDays = 3;
} else {
vipDays = 0; // 立即过期,需要续费
}
if (vipDays > 0) {
Date expireDate = new Date(System.currentTimeMillis() + vipDays * 24L * 60 * 60 * 1000);
clientAccount.setExpireTime(expireDate);
} else {
clientAccount.setExpireTime(new Date());
}
int result = clientAccountService.insertClientAccount(clientAccount);
if (result <= 0) {
return AjaxResult.error("注册失败");
}
String accessToken = Jwts.builder()
.setHeaderParam("kid", jwtRsaKeyService.getKeyId())
.setSubject(clientAccount.getUsername())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + JWT_EXPIRATION))
.claim("accountId", clientAccount.getId())
.claim("clientId", deviceId)
.signWith(SignatureAlgorithm.RS256, jwtRsaKeyService.getPrivateKey())
.compact();
@@ -256,5 +280,43 @@ public class ClientAccountController extends BaseController {
return AjaxResult.success(account == null);
}
/**
* 续费账号
*/
@PreAuthorize("@ss.hasPermi('monitor:account:edit')")
@Log(title = "账号续费", businessType = BusinessType.UPDATE)
@PostMapping("/renew")
public AjaxResult renew(@RequestBody Map<String, Object> data) {
Long accountId = Long.valueOf(data.get("accountId").toString());
Integer days = Integer.valueOf(data.get("days").toString());
ClientAccount account = clientAccountService.selectClientAccountById(accountId);
if (account == null) {
return AjaxResult.error("账号不存在");
}
java.util.Calendar cal = java.util.Calendar.getInstance();
if (account.getExpireTime() != null && account.getExpireTime().after(new Date())) {
cal.setTime(account.getExpireTime());
} else {
cal.setTime(new Date());
}
cal.add(java.util.Calendar.DAY_OF_MONTH, days);
Date newExpireTime = cal.getTime();
account.setExpireTime(newExpireTime);
account.setUpdateBy(getUsername());
clientAccountService.updateClientAccount(account);
// 通过SSE推送续费通知给该账号的所有在线设备
try {
sseHubService.sendEventToAllDevices(account.getUsername(), "VIP_RENEWED",
"{\"expireTime\":\"" + newExpireTime + "\"}");
} catch (Exception e) {
// SSE推送失败不影响续费操作
}
return AjaxResult.success("续费成功,新的过期时间:" + newExpireTime);
}
}

View File

@@ -4,6 +4,8 @@ import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.utils.ip.IpUtils;
import com.ruoyi.system.domain.ClientDevice;
import com.ruoyi.system.mapper.ClientDeviceMapper;
import com.ruoyi.system.mapper.ClientAccountMapper;
import com.ruoyi.system.domain.ClientAccount;
import com.ruoyi.web.sse.SseHubService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@@ -19,9 +21,28 @@ public class ClientDeviceController {
@Autowired
private ClientDeviceMapper clientDeviceMapper;
@Autowired
private ClientAccountMapper clientAccountMapper;
@Autowired
private SseHubService sseHubService;
private static final int DEFAULT_LIMIT = 3;
/**
* 获取账号的设备数量限制
*
* @param username 用户名
* @return 设备数量限制,如果账号不存在或未配置则返回默认值
*/
private int getDeviceLimit(String username) {
if (username == null || username.isEmpty()) {
return DEFAULT_LIMIT;
}
ClientAccount account = clientAccountMapper.selectClientAccountByUsername(username);
if (account == null || account.getDeviceLimit() == null) {
return DEFAULT_LIMIT;
}
return account.getDeviceLimit();
}
/**
* 查询设备配额与已使用数量
*
@@ -35,8 +56,9 @@ public class ClientDeviceController {
for (ClientDevice d : all) {
if (!"removed".equals(d.getStatus())) used++;
}
int limit = getDeviceLimit(username);
Map<String, Object> map = new HashMap<>();
map.put("limit", DEFAULT_LIMIT);
map.put("limit", limit);
map.put("used", used);
return AjaxResult.success(map);
}
@@ -55,26 +77,26 @@ public class ClientDeviceController {
return AjaxResult.success(active);
}
/**
* 设备注册(幂等)
*
* 根据 deviceId 判断:
* - 不存在:插入新记录(检查设备数量限制)
* - 已存在:更新设备信息
* 设备注册
*/
@PostMapping("/register")
public AjaxResult register(@RequestBody ClientDevice device, HttpServletRequest request) {
ClientDevice exists = clientDeviceMapper.selectByDeviceId(device.getDeviceId());
String ip = IpUtils.getIpAddr(request);
String deviceName = request.getHeader("X-Client-User") + "@" + ip + " (" + request.getHeader("X-Client-OS") + ")";
// 从请求体读取用户名和操作系统,构建设备名称
String username = device.getUsername();
String os = device.getOs();
String deviceName = username + "@" + ip + " (" + os + ")";
if (exists == null) {
// 检查设备数量限制
int deviceLimit = getDeviceLimit(device.getUsername());
List<ClientDevice> userDevices = clientDeviceMapper.selectByUsername(device.getUsername());
int activeDeviceCount = 0;
for (ClientDevice d : userDevices) {
if (!"removed".equals(d.getStatus())) activeDeviceCount++;
}
if (activeDeviceCount >= DEFAULT_LIMIT) {
return AjaxResult.error("设备数量已达上限(" + DEFAULT_LIMIT + "个),请先移除其他设备");
if (activeDeviceCount >= deviceLimit) {
return AjaxResult.error("设备数量已达上限(" + deviceLimit + "个),请先移除其他设备");
}
device.setIp(ip);
device.setStatus("online");
@@ -139,7 +161,6 @@ public class ClientDeviceController {
public AjaxResult offline(@RequestBody Map<String, String> body) {
String deviceId = body.get("deviceId");
if (deviceId == null) return AjaxResult.error("deviceId不能为空");
ClientDevice device = clientDeviceMapper.selectByDeviceId(deviceId);
if (device != null) {
device.setStatus("offline");
@@ -157,16 +178,20 @@ public class ClientDeviceController {
public AjaxResult heartbeat(@RequestBody ClientDevice device, HttpServletRequest request) {
ClientDevice exists = clientDeviceMapper.selectByDeviceId(device.getDeviceId());
String ip = IpUtils.getIpAddr(request);
String deviceName = request.getHeader("X-Client-User") + "@" + ip + " (" + request.getHeader("X-Client-OS") + ")";
// 从请求体读取用户名和操作系统,构建设备名称
String username = device.getUsername() ;
String os = device.getOs();
String deviceName = username + "@" + ip + " (" + os + ")";
if (exists == null) {
// 检查设备数量限制
int deviceLimit = getDeviceLimit(device.getUsername());
List<ClientDevice> userDevices = clientDeviceMapper.selectByUsername(device.getUsername());
int activeDeviceCount = 0;
for (ClientDevice d : userDevices) {
if (!"removed".equals(d.getStatus())) activeDeviceCount++;
}
if (activeDeviceCount >= DEFAULT_LIMIT) {
return AjaxResult.error("设备数量已达上限(" + DEFAULT_LIMIT + "个),请先移除其他设备");
if (activeDeviceCount >= deviceLimit) {
return AjaxResult.error("设备数量已达上限(" + deviceLimit + "个),请先移除其他设备");
}
device.setIp(ip);
device.setStatus("online");

View File

@@ -92,6 +92,28 @@ public class SseHubService {
}
}
/**
* 向指定账号的所有设备推送消息
*/
public void sendEventToAllDevices(String username, String type, String message) {
if (username == null || username.isEmpty()) return;
// 遍历所有会话找到匹配的username
String prefix = username + ":";
sessionEmitters.forEach((key, emitter) -> {
if (key.startsWith(prefix)) {
try {
String data = message != null ? message : "{}";
String eventData = "{\"type\":\"" + type + "\",\"message\":" + escapeJson(data) + "}";
emitter.send(SseEmitter.event().data(eventData));
} catch (IOException e) {
sessionEmitters.remove(key);
try { emitter.complete(); } catch (Exception ignored) {}
}
}
});
}
/**
* 更新设备状态
*/

View File

@@ -0,0 +1,21 @@
package com.ruoyi.web.task;
import com.ruoyi.system.service.IBanmaAccountService;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
@Component
public class BanmaTokenRefreshTask {
@Resource
private IBanmaAccountService banmaAccountService;
// 每两天凌晨3点
@Scheduled(cron = "0 0 3 */2 * ?")
public void refreshAllTokens() {
banmaAccountService.refreshAllTokens();
}
}

View File

@@ -0,0 +1,44 @@
package com.ruoyi.web.task;
import com.ruoyi.system.domain.ClientDevice;
import com.ruoyi.system.mapper.ClientDeviceMapper;
import com.ruoyi.web.sse.SseHubService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 设备心跳定时任务
*
* @author ruoyi
*/
@Component
public class DeviceHeartbeatTask {
@Autowired
private ClientDeviceMapper clientDeviceMapper;
@Autowired
private SseHubService sseHubService;
/**
* 每30秒发送一次ping保持SSE连接活跃
*/
@Scheduled(fixedRate = 30000)
public void sendHeartbeatPing() {
List<ClientDevice> onlineDevices = clientDeviceMapper.selectOnlineDevices();
for (ClientDevice device : onlineDevices) {
sseHubService.sendPing(device.getUsername(), device.getDeviceId());
}
}
/**
* 每2分钟清理一次过期设备
*/
@Scheduled(fixedRate = 120000)
public void cleanExpiredDevices() {
clientDeviceMapper.updateExpiredDevicesOffline();
}
}