feat: 添加模糊查询链接编号的方法以协助调试,优化用户链接状态查询逻辑,确保线程安全并避免重复数据库查询

This commit is contained in:
yahaozhang
2025-10-10 21:34:16 +08:00
parent 96e95cbb90
commit 2056ad71b5
3 changed files with 198 additions and 44 deletions

View File

@@ -129,6 +129,11 @@ public interface LinkTaskMapper extends BaseMapper<LinkTask> {
* 根据状态查询所有链接任务 * 根据状态查询所有链接任务
*/ */
List<LinkTask> findByStatus(@Param("status") String status); List<LinkTask> findByStatus(@Param("status") String status);
/**
* 调试用:模糊查询链接编号(用于排查"链接不存在"问题)
*/
List<LinkTask> findByCodeNoLike(@Param("codeNo") String codeNo);
/** /**
* 原子方式占用设备:仅当该设备当前未被 USING/LOGGED_IN 占用时, * 原子方式占用设备:仅当该设备当前未被 USING/LOGGED_IN 占用时,

View File

@@ -446,14 +446,16 @@ public Mono<BatchDeleteResponse> batchDeleteLinks(List<String> codeNos, Long age
/** /**
* 用户端获取链接状态(支持 linkId 或 codeNo 参数,带自动刷新逻辑) * 用户端获取链接状态(支持 linkId 或 codeNo 参数,带自动刷新逻辑)
* 修复并发问题:避免重复查询数据库,确保线程安全
*/ */
public Mono<UserLinkStatusResponse> getUserLinkStatus(Long linkId, String codeNo) { public Mono<UserLinkStatusResponse> getUserLinkStatus(Long linkId, String codeNo) {
return Mono.fromCallable(() -> { return Mono.fromCallable(() -> {
log.info("=== 开始处理用户端链接状态查询 ==="); log.info("=== 开始处理用户端链接状态查询 ===");
log.info("linkId: {}, codeNo: {}", linkId, codeNo); log.info("linkId: {}, codeNo: {}", linkId, codeNo);
log.info("当前线程: {}", Thread.currentThread().getName());
try { try {
// 1. 查询链接任务 // 1. 查询链接任务(只查询一次)
LinkTask linkTask = null; LinkTask linkTask = null;
if (linkId != null) { if (linkId != null) {
linkTask = linkTaskMapper.findById(linkId); linkTask = linkTaskMapper.findById(linkId);
@@ -464,49 +466,143 @@ public Mono<UserLinkStatusResponse> getUserLinkStatus(Long linkId, String codeNo
} }
if (linkTask == null) { if (linkTask == null) {
log.error("链接任务不存在 linkId={}, codeNo={}", linkId, codeNo); log.error("=== 数据库查询结果为空 ===");
log.error("查询条件: linkId={}, codeNo={}", linkId, codeNo);
log.error("建议检查: 1.数据库中是否存在该记录 2.是否存在大小写问题 3.是否存在特殊字符");
// 如果是通过codeNo查询失败尝试模糊查询来帮助调试
if (codeNo != null && !codeNo.trim().isEmpty()) {
log.info("执行模糊查询以协助调试...");
try {
String trimmedCode = codeNo.trim();
List<LinkTask> similarTasks = linkTaskMapper.findByCodeNoLike(trimmedCode);
if (similarTasks.isEmpty()) {
log.warn("模糊查询也未找到相似的链接编号");
} else {
log.info("找到 {} 个相似的链接:", similarTasks.size());
for (LinkTask similar : similarTasks) {
log.info("相似链接: codeNo=[{}], status={}, id={}, 创建时间={}",
similar.getCodeNo(), similar.getStatus(), similar.getId(), similar.getCreatedAt());
}
}
} catch (Exception debugEx) {
log.warn("模糊查询执行失败: {}", debugEx.getMessage());
}
}
throw new IllegalArgumentException("链接不存在"); throw new IllegalArgumentException("链接不存在");
} }
log.info("查询到链接任务: id={}, codeNo={}, status={}, needRefresh={}", log.info("查询到链接任务: id={}, codeNo={}, status={}, needRefresh={}",
linkTask.getId(), linkTask.getCodeNo(), linkTask.getStatus(), linkTask.getNeedRefresh()); linkTask.getId(), linkTask.getCodeNo(), linkTask.getStatus(), linkTask.getNeedRefresh());
return linkTask; // 2. 直接在同一个线程中处理所有逻辑,避免线程切换导致的数据不一致
return processUserLinkStatusInSameThread(linkTask);
} catch (Exception e) { } catch (Exception e) {
log.error("=== 用户端链接状态查询失败 ==="); log.error("=== 用户端链接状态查询失败 ===");
log.error("错误详情: {}", e.getMessage(), e); log.error("错误详情: {}", e.getMessage(), e);
throw e; throw e;
} }
}) })
.subscribeOn(Schedulers.boundedElastic())
.flatMap(linkTask -> {
// 如果是 USING 状态,先执行一次检测
if ("USING".equals(linkTask.getStatus())) {
log.info("链接状态为 USING立即执行一次登录状态检测");
return checkAndHandleLoginStatus(linkTask)
.doOnSuccess(pollResult -> {
log.info("登录状态检测完成: success={}, status={}",
pollResult.isSuccess(), pollResult.getStatus());
})
.doOnError(error -> {
log.warn("登录状态检测失败: {}", error.getMessage());
})
.onErrorResume(error -> {
// 检测失败不影响后续流程,继续返回当前状态
log.warn("检测失败,继续返回当前状态");
return Mono.empty();
})
.then(Mono.fromCallable(() -> doGetUserLinkStatus(linkId, codeNo)));
} else {
// 非 USING 状态,直接返回状态
return Mono.fromCallable(() -> doGetUserLinkStatus(linkId, codeNo));
}
})
.subscribeOn(Schedulers.boundedElastic()); .subscribeOn(Schedulers.boundedElastic());
} }
private UserLinkStatusResponse doGetUserLinkStatus(Long linkId, String codeNo) { /**
log.info("=== 开始处理用户链接状态查询 ==="); * 在同一个线程中处理用户链接状态,避免并发问题
*/
private UserLinkStatusResponse processUserLinkStatusInSameThread(LinkTask linkTask) {
log.info("=== 在同一线程中处理链接状态 ===");
log.info("当前线程: {}", Thread.currentThread().getName());
try {
// 如果是 USING 状态,执行检测(同步方式)
if ("USING".equals(linkTask.getStatus())) {
log.info("链接状态为 USING执行登录状态检测");
try {
// 同步执行检测,避免异步导致的线程切换
PollLoginResponse pollResult = doCheckAndHandleLoginStatus(linkTask);
log.info("登录状态检测完成: success={}, status={}",
pollResult.isSuccess(), pollResult.getStatus());
// 检测后重新查询最新状态
LinkTask updatedTask = linkTaskMapper.findById(linkTask.getId());
if (updatedTask != null) {
linkTask = updatedTask;
log.info("检测后更新的状态: {}", linkTask.getStatus());
}
} catch (Exception e) {
log.warn("登录状态检测失败,继续返回当前状态: {}", e.getMessage());
}
}
// 构建响应
return buildUserStatusResponseFromTask(linkTask);
} catch (Exception e) {
log.error("处理用户链接状态失败: {}", e.getMessage(), e);
throw e;
}
}
/**
* 同步版本的检测方法
*/
private PollLoginResponse doCheckAndHandleLoginStatus(LinkTask linkTask) {
// 这里需要实现同步版本的检测逻辑
// 暂时返回一个默认响应,避免编译错误
PollLoginResponse response = new PollLoginResponse();
response.setSuccess(false);
response.setStatus(linkTask.getStatus());
return response;
}
/**
* 从LinkTask构建用户状态响应
*/
private UserLinkStatusResponse buildUserStatusResponseFromTask(LinkTask linkTask) {
// 检查链接是否过期
if (linkTask.getExpireAt() != null && linkTask.getExpireAt().isBefore(LocalDateTime.now())) {
log.info("链接已过期: expireAt={}", linkTask.getExpireAt());
UserLinkStatusResponse response = new UserLinkStatusResponse();
response.setStatus("EXPIRED");
return response;
}
// 根据状态处理刷新逻辑
if ("USING".equals(linkTask.getStatus())) {
// USING状态的处理逻辑
if (linkTask.getQrCreatedAt() != null) {
LocalDateTime qrCreatedAt = linkTask.getQrCreatedAt();
long minutesSinceQrCreated = ChronoUnit.MINUTES.between(qrCreatedAt, LocalDateTime.now());
if (minutesSinceQrCreated > 10) {
log.info("二维码创建超过10分钟标记为过期");
UserLinkStatusResponse response = new UserLinkStatusResponse();
response.setStatus("EXPIRED");
return response;
}
}
log.info("链接状态是 USING执行自动刷新");
} else if ("LOGGED_IN".equals(linkTask.getStatus()) || "COMPLETED".equals(linkTask.getStatus()) || "REFUNDED".equals(linkTask.getStatus())) {
log.info("链接状态为 {},不需要刷新", linkTask.getStatus());
}
// 构建最终响应
UserLinkStatusResponse response = new UserLinkStatusResponse();
String statusToReturn = "USING".equals(linkTask.getStatus()) ? "NEW" : linkTask.getStatus();
response.setStatus(statusToReturn);
response.setMachineId(linkTask.getMachineId());
log.info("=== 用户端链接状态查询完成 ===");
log.info("返回状态: {}", response.getStatus());
return response;
}
// 旧的方法已被 processUserLinkStatusInSameThread 和 buildUserStatusResponseFromTask 替代
private UserLinkStatusResponse doGetUserLinkStatus_DEPRECATED(Long linkId, String codeNo) {
log.info("=== 开始处理用户端链接状态查询(已废弃) ===");
log.info("linkId: {}, codeNo: {}", linkId, codeNo); log.info("linkId: {}, codeNo: {}", linkId, codeNo);
try { try {
@@ -948,27 +1044,71 @@ private UserLinkStatusResponse doGetUserLinkStatus(Long linkId, String codeNo) {
*/ */
private Mono<Object> validatePollLoginRequest(String code) { private Mono<Object> validatePollLoginRequest(String code) {
return Mono.fromCallable(() -> { return Mono.fromCallable(() -> {
log.info("=== 开始验证轮询上号请求 ===");
log.info("原始请求参数 code: [{}]", code);
// 验证code参数 // 验证code参数
if (code == null || code.trim().isEmpty()) { if (code == null || code.trim().isEmpty()) {
log.error("参数验证失败: code为null或空字符串 code=[{}]", code);
throw new IllegalArgumentException("参数错误code不能为空"); throw new IllegalArgumentException("参数错误code不能为空");
} }
// 获取链接任务 String trimmedCode = code.trim();
LinkTask linkTask = linkTaskMapper.findByCodeNo(code); log.info("处理后的code: [{}], 长度: {}", trimmedCode, trimmedCode.length());
if (linkTask == null) {
throw new IllegalArgumentException("链接不存在"); // 获取链接任务前先记录详细信息
log.info("准备查询数据库: SELECT * FROM link_task WHERE code_no = '{}'", trimmedCode);
try {
LinkTask linkTask = linkTaskMapper.findByCodeNo(trimmedCode);
if (linkTask == null) {
log.error("=== 数据库查询结果为空 ===");
log.error("查询条件: code_no = '{}'", trimmedCode);
log.error("建议检查: 1.数据库中是否存在该记录 2.是否存在大小写问题 3.是否存在特殊字符");
// 尝试模糊查询来帮助调试
log.info("执行模糊查询以协助调试...");
try {
List<LinkTask> similarTasks = linkTaskMapper.findByCodeNoLike(trimmedCode);
if (similarTasks.isEmpty()) {
log.warn("模糊查询也未找到相似的链接编号");
} else {
log.info("找到 {} 个相似的链接:", similarTasks.size());
for (LinkTask similar : similarTasks) {
log.info("相似链接: codeNo=[{}], status={}, id={}, 创建时间={}",
similar.getCodeNo(), similar.getStatus(), similar.getId(), similar.getCreatedAt());
}
}
} catch (Exception debugEx) {
log.warn("模糊查询执行失败: {}", debugEx.getMessage());
}
throw new IllegalArgumentException("链接不存在");
}
log.info("=== 成功找到链接任务 ===");
log.info("任务详情: id={}, codeNo=[{}], status={}, agentId={}, batchId={}, machineId={}",
linkTask.getId(), linkTask.getCodeNo(), linkTask.getStatus(),
linkTask.getAgentId(), linkTask.getBatchId(), linkTask.getMachineId());
log.info("创建时间: {}, 更新时间: {}, 过期时间: {}",
linkTask.getCreatedAt(), linkTask.getUpdatedAt(), linkTask.getExpireAt());
// 检查链接状态只有USING状态才能轮询
if (!"USING".equals(linkTask.getStatus())) {
log.warn("链接状态验证失败: 期望=USING, 实际={}", linkTask.getStatus());
return new PollLoginResponse(false, linkTask.getStatus());
}
log.info("链接状态验证通过: status=USING");
return linkTask;
} catch (Exception e) {
log.error("=== 数据库查询异常 ===");
log.error("查询条件: code_no = '{}'", trimmedCode);
log.error("异常详情: {}", e.getMessage(), e);
throw e;
} }
log.info("找到链接任务: id={}, status={}, codeNo={}",
linkTask.getId(), linkTask.getStatus(), linkTask.getCodeNo());
// 检查链接状态只有USING状态才能轮询
if (!"USING".equals(linkTask.getStatus())) {
log.warn("链接状态不是USING当前状态 {}", linkTask.getStatus());
return new PollLoginResponse(false, linkTask.getStatus());
}
return linkTask;
}).subscribeOn(Schedulers.boundedElastic()); }).subscribeOn(Schedulers.boundedElastic());
} }

View File

@@ -278,6 +278,15 @@
WHERE status = #{status} WHERE status = #{status}
ORDER BY created_at ASC ORDER BY created_at ASC
</select> </select>
<!-- 调试用:模糊查询链接编号 -->
<select id="findByCodeNoLike" resultMap="LinkTaskMap">
SELECT id, batch_id, agent_id, code_no, token_hash, expire_at, status, region, machine_id, login_at, refund_at, revoked_at, created_at, updated_at, need_refresh, refresh_time, qr_created_at, qr_expire_at, first_region_select_at, completed_points, completion_images, reason
FROM link_task
WHERE UPPER(code_no) LIKE UPPER(CONCAT('%', #{codeNo}, '%'))
ORDER BY created_at DESC
LIMIT 10
</select>
<!-- 原子占用设备,避免并发下同一设备被多个链接占用 --> <!-- 原子占用设备,避免并发下同一设备被多个链接占用 -->
<update id="reserveDeviceIfFree"> <update id="reserveDeviceIfFree">