feat(client): 实现账号设备试用期管理功能

- 新增设备试用期过期时间字段及管理接口
- 实现试用期状态检查与过期提醒逻辑
- 支持账号类型区分试用与付费用户
- 添加设备注册时自动设置3天试用期- 实现VIP状态刷新与过期类型判断
-优化账号列表查询支持按客户端用户名过滤
- 更新客户端设备管理支持试用期控制- 完善登录流程支持试用期状态提示
-修复设备离线通知缺少用户名参数问题
- 调整账号默认设置清除逻辑关联客户端用户名
This commit is contained in:
2025-10-17 14:17:02 +08:00
parent 132299c4b7
commit 6e1b4d00de
18 changed files with 348 additions and 129 deletions

View File

@@ -32,7 +32,7 @@ import com.ruoyi.system.domain.SysCache;
public class CacheController
{
@Autowired
private RedisTemplate<String, String> redisTemplate;
private RedisTemplate<Object, Object> redisTemplate;
private final static List<SysCache> caches = new ArrayList<SysCache>();
{
@@ -80,15 +80,16 @@ public class CacheController
@GetMapping("/getKeys/{cacheName}")
public AjaxResult getCacheKeys(@PathVariable String cacheName)
{
Set<String> cacheKeys = redisTemplate.keys(cacheName + "*");
return AjaxResult.success(new TreeSet<>(cacheKeys));
Set<Object> cacheKeys = redisTemplate.keys(cacheName + "*");
return AjaxResult.success(cacheKeys != null ? new TreeSet<>(cacheKeys.stream().map(String::valueOf).collect(java.util.stream.Collectors.toSet())) : new TreeSet<>());
}
@PreAuthorize("@ss.hasPermi('monitor:cache:list')")
@GetMapping("/getValue/{cacheName}/{cacheKey}")
public AjaxResult getCacheValue(@PathVariable String cacheName, @PathVariable String cacheKey)
{
String cacheValue = redisTemplate.opsForValue().get(cacheKey);
Object cacheValueObj = redisTemplate.opsForValue().get(cacheKey);
String cacheValue = cacheValueObj != null ? cacheValueObj.toString() : null;
SysCache sysCache = new SysCache(cacheName, cacheKey, cacheValue);
return AjaxResult.success(sysCache);
}
@@ -97,8 +98,10 @@ public class CacheController
@DeleteMapping("/clearCacheName/{cacheName}")
public AjaxResult clearCacheName(@PathVariable String cacheName)
{
Collection<String> cacheKeys = redisTemplate.keys(cacheName + "*");
redisTemplate.delete(cacheKeys);
Set<Object> cacheKeys = redisTemplate.keys(cacheName + "*");
if (cacheKeys != null && !cacheKeys.isEmpty()) {
redisTemplate.delete(cacheKeys);
}
return AjaxResult.success();
}
@@ -114,8 +117,10 @@ public class CacheController
@DeleteMapping("/clearCacheAll")
public AjaxResult clearCacheAll()
{
Collection<String> cacheKeys = redisTemplate.keys("*");
redisTemplate.delete(cacheKeys);
Set<Object> cacheKeys = redisTemplate.keys("*");
if (cacheKeys != null && !cacheKeys.isEmpty()) {
redisTemplate.delete(cacheKeys);
}
return AjaxResult.success();
}
}

View File

@@ -149,7 +149,6 @@ public class ClientAccountController extends BaseController {
if (userDevice >= deviceLimit) {
return AjaxResult.error("设备数量已达上限(" + deviceLimit + "个),请先移除其他设备");
}
String token = Jwts.builder()
.setHeaderParam("kid", jwtRsaKeyService.getKeyId())
.setSubject(username)
@@ -161,15 +160,24 @@ public class ClientAccountController extends BaseController {
.signWith(SignatureAlgorithm.RS256, jwtRsaKeyService.getPrivateKey())
.compact();
// 检查设备试用期仅对trial账号生效
boolean deviceTrialExpired = false;
if ("trial".equals(account.getAccountType())) {
ClientDevice device = clientDeviceMapper.selectByDeviceIdAndUsername(clientId, username);
deviceTrialExpired = device != null
&& device.getTrialExpireTime() != null
&& new Date().after(device.getTrialExpireTime());
}
Map<String, Object> data = new HashMap<>();
data.put("token", token);
data.put("permissions", account.getPermissions());
data.put("accountName", account.getAccountName());
data.put("expireTime", account.getExpireTime());
data.put("accountType", account.getAccountType());
data.put("deviceTrialExpired", deviceTrialExpired);
return AjaxResult.success(data);
}
/**
* 验证token
*/
@@ -181,17 +189,35 @@ public class ClientAccountController extends BaseController {
.parseClaimsJws(token)
.getBody();
String username = (String) claims.get("sub");
String clientId = (String) claims.get("clientId");
ClientAccount account = clientAccountService.selectClientAccountByUsername(username);
if (account == null || !"0".equals(account.getStatus())) {
return AjaxResult.error("token无效");
}
// 付费账号到期自动降级为试用账号
if ("paid".equals(account.getAccountType()) && account.getExpireTime() != null && new Date().after(account.getExpireTime())) {
account.setAccountType("trial");
clientAccountService.updateClientAccount(account);
}
// 检查设备试用期仅对trial账号生效
boolean deviceTrialExpired = false;
if ("trial".equals(account.getAccountType())) {
ClientDevice device = clientDeviceMapper.selectByDeviceIdAndUsername(clientId, username);
deviceTrialExpired = device != null
&& device.getTrialExpireTime() != null
&& new Date().after(device.getTrialExpireTime());
}
Map<String, Object> result = new HashMap<>();
result.put("username", username);
result.put("permissions", account.getPermissions());
result.put("accountName", account.getAccountName());
result.put("expireTime", account.getExpireTime());
result.put("accountType", account.getAccountType());
result.put("deviceTrialExpired", deviceTrialExpired);
return AjaxResult.success(result);
}
@@ -217,7 +243,7 @@ public class ClientAccountController extends BaseController {
/**
* 客户端账号注册
* 新设备注册送3天VIP同一设备ID重复注册不赠送
* 新账号注册送3天VIP试用期
*/
@PostMapping("/register")
public AjaxResult register(@RequestBody Map<String, String> registerData) {
@@ -232,16 +258,8 @@ public class ClientAccountController extends BaseController {
clientAccount.setStatus("0");
clientAccount.setPermissions("{\"amazon\":true,\"rakuten\":true,\"zebra\":true}");
clientAccount.setPassword(passwordEncoder.encode(password));
// 新设备赠送3天VIP
ClientDevice existingDevice = clientDeviceMapper.selectByDeviceId(deviceId);
int vipDays = (existingDevice == null) ? 3 : 0;
if (vipDays > 0) {
clientAccount.setExpireTime(new Date(System.currentTimeMillis() + vipDays * 24L * 60 * 60 * 1000));
} else {
clientAccount.setExpireTime(new Date());
}
clientAccount.setAccountType("trial");
clientAccount.setExpireTime(new Date(System.currentTimeMillis() + 3 * 24L * 60 * 60 * 1000));
int result = clientAccountService.insertClientAccount(clientAccount);
if (result <= 0) {
@@ -263,6 +281,8 @@ public class ClientAccountController extends BaseController {
data.put("permissions", clientAccount.getPermissions());
data.put("accountName", clientAccount.getAccountName());
data.put("expireTime", clientAccount.getExpireTime());
data.put("accountType", clientAccount.getAccountType());
data.put("deviceTrialExpired", false);
return AjaxResult.success(data);
}
@@ -300,6 +320,7 @@ public class ClientAccountController extends BaseController {
Date newExpireTime = cal.getTime();
account.setExpireTime(newExpireTime);
account.setAccountType("paid");
account.setUpdateBy(getUsername());
clientAccountService.updateClientAccount(account);

View File

@@ -97,32 +97,33 @@ public class ClientDeviceController {
*/
@PostMapping("/register")
public AjaxResult register(@RequestBody ClientDevice device, HttpServletRequest request) {
ClientDevice exists = clientDeviceMapper.selectByDeviceId(device.getDeviceId());
String ip = IpUtils.getIpAddr(request);
String username = device.getUsername();
String deviceId = device.getDeviceId();
String os = device.getOs();
String deviceName = username + "@" + ip + " (" + os + ")";
ClientDevice exists = clientDeviceMapper.selectByDeviceIdAndUsername(deviceId, username);
if (exists == null) {
// 检查设备数量限制
try {
checkDeviceLimit(device.getUsername(), device.getDeviceId());
checkDeviceLimit(username, deviceId);
} catch (RuntimeException e) {
return AjaxResult.error(e.getMessage());
}
device.setIp(ip);
device.setStatus("online");
device.setLastActiveAt(new java.util.Date());
device.setTrialExpireTime(new java.util.Date(System.currentTimeMillis() + 3 * 24L * 60 * 60 * 1000));
device.setName(deviceName);
clientDeviceMapper.insert(device);
} else {
exists.setUsername(device.getUsername());
exists.setName(deviceName);
exists.setOs(device.getOs());
exists.setOs(os);
exists.setStatus("online");
exists.setIp(ip);
exists.setLocation(device.getLocation());
exists.setLastActiveAt(new java.util.Date());
clientDeviceMapper.updateByDeviceId(exists);
clientDeviceMapper.updateByDeviceIdAndUsername(exists);
}
return AjaxResult.success();
}
@@ -137,6 +138,15 @@ public class ClientDeviceController {
clientDeviceMapper.updateByDeviceId(device);
return AjaxResult.success();
}
/**
* 修改设备试用期过期时间
*/
@PostMapping("/updateExpire")
public AjaxResult updateExpire(@RequestBody ClientDevice device) {
clientDeviceMapper.updateByDeviceIdAndUsername(device);
return AjaxResult.success();
}
/**
* 移除设备
* 根据 deviceId 删除设备绑定记录。
@@ -144,19 +154,20 @@ public class ClientDeviceController {
@PostMapping("/remove")
public AjaxResult remove(@RequestBody Map<String, String> body) {
String deviceId = body.get("deviceId");
String username = body.get("username");
if (deviceId == null || deviceId.isEmpty()) {
return AjaxResult.error("deviceId不能为空");
}
ClientDevice exists = clientDeviceMapper.selectByDeviceId(deviceId);
ClientDevice exists = clientDeviceMapper.selectByDeviceIdAndUsername(deviceId, username);
if (exists == null) {
return AjaxResult.success();
}
if (!"removed".equals(exists.getStatus())) {
exists.setStatus("removed");
exists.setLastActiveAt(new java.util.Date());
clientDeviceMapper.updateByDeviceId(exists);
sseHubService.sendEvent(exists.getUsername(), deviceId, "DEVICE_REMOVED", "{}");
sseHubService.disconnectDevice(exists.getUsername(), deviceId);
clientDeviceMapper.updateByDeviceIdAndUsername(exists);
sseHubService.sendEvent(username, deviceId, "DEVICE_REMOVED", "{}");
sseHubService.disconnectDevice(username, deviceId);
}
return AjaxResult.success();
}
@@ -167,11 +178,13 @@ public class ClientDeviceController {
@PostMapping("/offline")
public AjaxResult offline(@RequestBody Map<String, String> body) {
String deviceId = body.get("deviceId");
ClientDevice device = clientDeviceMapper.selectByDeviceId(deviceId);
String username = body.get("username");
ClientDevice device = clientDeviceMapper.selectByDeviceIdAndUsername(deviceId, username);
if (device != null) {
device.setStatus("offline");
device.setLastActiveAt(new java.util.Date());
clientDeviceMapper.updateByDeviceId(device);
clientDeviceMapper.updateByDeviceIdAndUsername(device);
}
return AjaxResult.success();
}
@@ -182,38 +195,37 @@ public class ClientDeviceController {
*/
@PostMapping("/heartbeat")
public AjaxResult heartbeat(@RequestBody ClientDevice device, HttpServletRequest request) {
ClientDevice exists = clientDeviceMapper.selectByDeviceId(device.getDeviceId());
String ip = IpUtils.getIpAddr(request);
String username = device.getUsername();
String deviceId = device.getDeviceId();
String os = device.getOs();
String deviceName = username + "@" + ip + " (" + os + ")";
ClientDevice exists = clientDeviceMapper.selectByDeviceIdAndUsername(deviceId, username);
if (exists == null) {
// 新设备注册
device.setIp(ip);
device.setStatus("online");
device.setLastActiveAt(new java.util.Date());
device.setTrialExpireTime(new java.util.Date(System.currentTimeMillis() + 3 * 24L * 60 * 60 * 1000));
device.setName(deviceName);
clientDeviceMapper.insert(device);
} else if ("removed".equals(exists.getStatus())) {
// 被移除设备重新激活
exists.setUsername(device.getUsername());
exists.setName(deviceName);
exists.setOs(device.getOs());
exists.setOs(os);
exists.setStatus("online");
exists.setIp(ip);
exists.setLocation(device.getLocation());
exists.setLastActiveAt(new java.util.Date());
clientDeviceMapper.updateByDeviceId(exists);
clientDeviceMapper.updateByDeviceIdAndUsername(exists);
} else {
// 已存在设备更新
exists.setUsername(device.getUsername());
exists.setStatus("online");
exists.setIp(ip);
exists.setLastActiveAt(new java.util.Date());
exists.setName(deviceName);
clientDeviceMapper.updateByDeviceId(exists);
clientDeviceMapper.updateByDeviceIdAndUsername(exists);
}
return AjaxResult.success();
}

View File

@@ -1,6 +1,7 @@
package com.ruoyi.web.controller.tool;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import com.ruoyi.common.annotation.Anonymous;
@@ -10,9 +11,7 @@ import com.ruoyi.system.domain.BanmaAccount;
import com.ruoyi.system.service.IBanmaAccountService;
/**
* 斑马账号管理(数据库版,极简接口):
* - 仅负责账号与 Token 的存取
* - 不参与登录/刷新与数据采集,客户端自行处理
* 斑马账号管理
*/
@RestController
@RequestMapping("/tool/banma")
@@ -22,47 +21,36 @@ public class BanmaOrderController extends BaseController {
@Autowired
private IBanmaAccountService accountService;
/**
* 查询账号列表(
*/
@GetMapping("/accounts")
public R<?> listAccounts() {
List<BanmaAccount> list = accountService.listSimple();
return R.ok(list);
public R<?> listAccounts(String name) {
return R.ok(accountService.listSimple(name));
}
/**
* 新增或编辑账号(含设为默认)
*/
@PostMapping("/accounts")
public R<?> saveAccount(@RequestBody BanmaAccount body) {
Long id = accountService.saveOrUpdate(body);
boolean ok = false;
try { ok = accountService.refreshToken(id); } catch (Exception ignore) {}
return ok ? R.ok(Map.of("id", id)) : R.fail("账号或密码错误无法获取Token");
public R<?> saveAccount(@RequestBody BanmaAccount body, String name) {
if (body.getId() == null && accountService.validateAndGetToken(body.getUsername(), body.getPassword()) == null) {
throw new RuntimeException("账号或密码错误");
}
Long id = accountService.saveOrUpdate(body, name);
accountService.refreshToken(id);
return R.ok(Map.of("id", id));
}
/**
* 删除账号
*/
@DeleteMapping("/accounts/{id}")
public R<?> remove(@PathVariable Long id) {
accountService.remove(id);
return R.ok();
}
/** 手动刷新单个账号 Token */
@PostMapping("/accounts/{id}/refresh-token")
public R<?> refreshOne(@PathVariable Long id) {
accountService.refreshToken(id);
return R.ok();
}
/** 手动刷新全部启用账号 Token */
@PostMapping("/refresh-all")
public R<?> refreshAll() {
accountService.refreshAllTokens();
accountService.listSimple().forEach(account -> accountService.refreshToken(account.getId()));
return R.ok();
}
}
}