feat: 添加用户端链接状态查询接口及自动刷新逻辑

主要修改:
1. 在LinkController中新增获取用户链接状态的接口,支持通过linkId或codeNo查询。
2. 在LinkStatusService中实现用户链接状态查询逻辑,包含自动刷新和二维码更新功能。
3. 更新LinkTask实体,添加needRefresh、refreshTime、qrCreatedAt和qrExpireAt字段以支持新功能。
4. 在ScriptClient中新增检查空闲设备、选区、刷新、检查上号状态等操作的实现。
5. 更新SecurityConfig,允许用户端获取链接状态接口公开访问。

技术细节:
- 新增UserLinkStatusResponse DTO以支持用户链接状态的返回格式。
- 通过脚本端接口实现链接状态的自动刷新和二维码信息更新。
This commit is contained in:
zyh
2025-08-26 18:07:44 +08:00
parent e9858bfec1
commit 3847250c2b
13 changed files with 1106 additions and 18 deletions

View File

@@ -58,6 +58,102 @@ public class JwtService {
return token;
}
/**
* 生成链接code用于用户端访问链接
*/
public String generateLinkCode(Long linkId, String codeNo, int expireHours) {
log.info("=== 开始生成链接code ===");
log.info("生成参数: linkId={}, codeNo={}, expireHours={}", linkId, codeNo, expireHours);
Instant now = Instant.now();
var builder = Jwts.builder()
.setSubject("link_access")
.setIssuedAt(Date.from(now))
.setExpiration(Date.from(now.plus(expireHours, ChronoUnit.HOURS)))
.claim("linkId", linkId)
.claim("codeNo", codeNo)
.claim("type", "link_access");
String code = builder.signWith(key, SignatureAlgorithm.HS256).compact();
log.info("=== 链接code生成成功 ===");
log.info("code长度: {} 字符", code.length());
log.info("过期时间: {} 小时后", expireHours);
return code;
}
/**
* 解析链接code获取链接信息
*/
public LinkCodeInfo parseLinkCode(String code) {
log.debug("=== 开始解析链接code ===");
log.debug("code长度: {} 字符", code.length());
try {
io.jsonwebtoken.Claims claims = Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(code)
.getBody();
// 检查是否是链接类型的token
String type = claims.get("type", String.class);
if (!"link_access".equals(type)) {
throw new IllegalArgumentException("无效的链接code类型");
}
Long linkId = claims.get("linkId", Long.class);
String codeNo = claims.get("codeNo", String.class);
if (linkId == null || codeNo == null) {
throw new IllegalArgumentException("链接code格式错误");
}
LinkCodeInfo info = new LinkCodeInfo();
info.setLinkId(linkId);
info.setCodeNo(codeNo);
info.setIssuedAt(claims.getIssuedAt());
info.setExpireAt(claims.getExpiration());
log.debug("=== 链接code解析成功 ===");
log.debug("linkId: {}, codeNo: {}", linkId, codeNo);
return info;
} catch (Exception e) {
log.warn("=== 链接code解析失败 ===");
log.warn("code: {}", code);
log.warn("错误详情: {}", e.getMessage());
throw new IllegalArgumentException("无效的链接code", e);
}
}
/**
* 链接code信息
*/
public static class LinkCodeInfo {
private Long linkId;
private String codeNo;
private Date issuedAt;
private Date expireAt;
public Long getLinkId() { return linkId; }
public void setLinkId(Long linkId) { this.linkId = linkId; }
public String getCodeNo() { return codeNo; }
public void setCodeNo(String codeNo) { this.codeNo = codeNo; }
public Date getIssuedAt() { return issuedAt; }
public void setIssuedAt(Date issuedAt) { this.issuedAt = issuedAt; }
public Date getExpireAt() { return expireAt; }
public void setExpireAt(Date expireAt) { this.expireAt = expireAt; }
public boolean isExpired() {
return expireAt != null && new Date().after(expireAt);
}
}
public io.jsonwebtoken.Claims parse(String token) {
log.info("=== 开始解析JWT token ===");
log.info("token长度: {} 字符", token.length());

View File

@@ -41,7 +41,8 @@ public class SecurityConfig {
.pathMatchers("/actuator/**").permitAll()
.pathMatchers(HttpMethod.POST, "/api/auth/login").permitAll()
.pathMatchers(HttpMethod.GET, "/api/auth/me").permitAll()
.pathMatchers("/api/link/**").authenticated() // 链接接口需要认证
.pathMatchers(HttpMethod.GET, "/api/link/status").permitAll() // 用户端获取链接状态接口,公开访问
.pathMatchers("/api/link/**").authenticated() // 其他链接接口需要认证
.anyExchange().permitAll() // 其他接口后续再收紧
)
// 关键将JWT过滤器集成到Security过滤链中放在AUTHENTICATION位置
@@ -59,6 +60,7 @@ public class SecurityConfig {
log.info(" * /actuator/** -> 允许所有");
log.info(" * POST /api/auth/login -> 允许所有");
log.info(" * GET /api/auth/me -> 允许所有");
log.info(" * GET /api/link/status -> 允许所有 (用户端公开接口)");
log.info(" * /api/link/** -> 需要认证");
log.info(" * 其他路径 -> 允许所有");