feat: 更新数据库结构和链接任务逻辑

主要修改:
1. 更新`game.sql`文件,添加`system_config`表并调整多个表的`ENGINE`和`AUTO_INCREMENT`设置。
2. 在`LinkTask`实体中新增`completedPoints`字段,更新状态字段以包含`COMPLETED`状态。
3. 在`LinkTaskMapper`中新增根据设备ID和状态查询链接任务的方法。
4. 在`LinkStatusService`中更新状态描述映射,增加对`COMPLETED`状态的处理。
5. 在`DeviceStatusService`和`ScriptClient`中新增解析设备状态的方法,支持检查设备是否完成游戏。

技术细节:
- 通过数据库结构的更新,增强了系统的配置管理和链接任务的状态处理能力。
- 新增的功能支持更灵活的设备状态监控和任务管理。
This commit is contained in:
zyh
2025-08-27 16:00:43 +08:00
parent bb4136b4ab
commit c6e8953960
16 changed files with 620 additions and 55 deletions

View File

@@ -3,9 +3,11 @@ package com.gameplatform.server;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@MapperScan("com.gameplatform.server.mapper")
@EnableScheduling
public class GamePlatformServerApplication {
public static void main(String[] args) {
SpringApplication.run(GamePlatformServerApplication.class, args);

View File

@@ -90,4 +90,9 @@ public interface LinkTaskMapper extends BaseMapper<LinkTask> {
* 根据链接编号列表和代理ID查询链接任务用于验证权限
*/
List<LinkTask> findByCodeNosAndAgentId(@Param("codeNos") List<String> codeNos, @Param("agentId") Long agentId);
/**
* 根据设备ID和状态查询链接任务
*/
List<LinkTask> findByMachineIdAndStatus(@Param("machineId") String machineId, @Param("status") String status);
}

View File

@@ -13,7 +13,7 @@ public class LinkStatusResponse {
@Schema(description = "批次ID", example = "123")
private Long batchId;
@Schema(description = "链接状态", example = "NEW", allowableValues = {"NEW", "USING", "LOGGED_IN", "REFUNDED", "EXPIRED"})
@Schema(description = "链接状态", example = "NEW", allowableValues = {"NEW", "USING", "LOGGED_IN", "COMPLETED", "REFUNDED", "EXPIRED"})
private String status;
@Schema(description = "链接状态描述", example = "新建")

View File

@@ -5,7 +5,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "用户端链接状态响应")
public class UserLinkStatusResponse {
@Schema(description = "链接状态", example = "NEW", allowableValues = {"NEW", "USING", "LOGGED_IN", "REFUNDED", "EXPIRED"})
@Schema(description = "链接状态", example = "NEW", allowableValues = {"NEW", "USING", "LOGGED_IN", "COMPLETED", "REFUNDED", "EXPIRED"})
private String status;
// Getter and Setter

View File

@@ -25,7 +25,7 @@ public class LinkTask {
@TableField("expire_at")
private LocalDateTime expireAt;
private String status; // NEW | USING | LOGGED_IN | REFUNDED | EXPIRED
private String status; // NEW | USING | LOGGED_IN | COMPLETED | REFUNDED | EXPIRED
private String region; // Q | V
@@ -61,6 +61,9 @@ public class LinkTask {
@TableField("first_region_select_at")
private LocalDateTime firstRegionSelectAt;
@TableField("completed_points")
private Integer completedPoints;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
@@ -118,4 +121,7 @@ public class LinkTask {
public LocalDateTime getFirstRegionSelectAt() { return firstRegionSelectAt; }
public void setFirstRegionSelectAt(LocalDateTime firstRegionSelectAt) { this.firstRegionSelectAt = firstRegionSelectAt; }
public Integer getCompletedPoints() { return completedPoints; }
public void setCompletedPoints(Integer completedPoints) { this.completedPoints = completedPoints; }
}

View File

@@ -0,0 +1,158 @@
package com.gameplatform.server.service.device;
import com.gameplatform.server.mapper.agent.LinkTaskMapper;
import com.gameplatform.server.model.entity.agent.LinkTask;
import com.gameplatform.server.service.external.ScriptClient;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
@Service
@Slf4j
public class DeviceStatusCheckService {
private final ScriptClient scriptClient;
private final LinkTaskMapper linkTaskMapper;
public DeviceStatusCheckService(ScriptClient scriptClient, LinkTaskMapper linkTaskMapper) {
this.scriptClient = scriptClient;
this.linkTaskMapper = linkTaskMapper;
}
/**
* 检查设备状态并更新相关链接任务
* @param machineId 设备ID
* @param reason 检查原因(定时检查 或 选区请求)
*/
@Transactional
public void checkDeviceStatusAndUpdateTasks(String machineId, String reason) {
log.info("=== 开始检查设备状态 ===");
log.info("设备ID: {}, 检查原因: {}", machineId, reason);
try {
// 1. 获取设备状态
Map<String, Object> deviceStatus = scriptClient.getDeviceStatus(machineId).block();
if (deviceStatus == null || deviceStatus.isEmpty()) {
log.warn("获取设备状态失败设备ID: {}", machineId);
return;
}
// 2. 解析设备状态
DeviceStatusInfo statusInfo = parseDeviceStatus(deviceStatus);
log.info("设备状态解析结果: {}", statusInfo);
// 3. 如果设备空闲检查是否有使用该设备的LOGGED_IN状态链接
if (statusInfo.isIdle()) {
log.info("设备 {} 处于空闲状态,检查相关链接任务", machineId);
updateCompletedTasks(machineId, statusInfo.getPoints());
} else {
log.info("设备 {} 不是空闲状态: {}", machineId, statusInfo.getStatus());
}
} catch (Exception e) {
log.error("检查设备状态时发生异常设备ID: {}", machineId, e);
}
log.info("=== 设备状态检查完成 ===");
}
/**
* 解析设备状态响应
*/
private DeviceStatusInfo parseDeviceStatus(Map<String, Object> deviceStatus) {
DeviceStatusInfo info = new DeviceStatusInfo();
try {
// 解析 f0 (点数)
Object f0Obj = deviceStatus.get("f0");
if (f0Obj instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> f0 = (Map<String, Object>) f0Obj;
if (f0.get("val") != null) {
String pointsStr = f0.get("val").toString();
info.setPoints(Integer.parseInt(pointsStr));
}
}
// 解析 f1 (状态)
Object f1Obj = deviceStatus.get("f1");
if (f1Obj instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> f1 = (Map<String, Object>) f1Obj;
if (f1.get("val") != null) {
String status = f1.get("val").toString();
info.setStatus(status);
info.setIdle("空闲".equals(status));
}
}
} catch (Exception e) {
log.error("解析设备状态时发生异常", e);
}
return info;
}
/**
* 更新已完成的任务
*/
private void updateCompletedTasks(String machineId, Integer points) {
// 查找使用该设备且状态为LOGGED_IN的链接任务
List<LinkTask> loggedInTasks = linkTaskMapper.findByMachineIdAndStatus(machineId, "LOGGED_IN");
if (loggedInTasks.isEmpty()) {
log.info("设备 {} 没有处于LOGGED_IN状态的链接任务", machineId);
return;
}
log.info("找到 {} 个使用设备 {} 且状态为LOGGED_IN的链接任务", loggedInTasks.size(), machineId);
// 更新所有相关任务为COMPLETED状态
for (LinkTask task : loggedInTasks) {
try {
task.setStatus("COMPLETED");
task.setCompletedPoints(points);
task.setUpdatedAt(LocalDateTime.now());
int updated = linkTaskMapper.updateById(task);
if (updated > 0) {
log.info("链接任务 {} (代码: {}) 已标记为完成,完成点数: {}",
task.getId(), task.getCodeNo(), points);
} else {
log.warn("更新链接任务 {} 失败", task.getId());
}
} catch (Exception e) {
log.error("更新链接任务 {} 时发生异常", task.getId(), e);
}
}
}
/**
* 设备状态信息
*/
private static class DeviceStatusInfo {
private String status;
private Integer points;
private boolean idle;
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public Integer getPoints() { return points; }
public void setPoints(Integer points) { this.points = points; }
public boolean isIdle() { return idle; }
public void setIdle(boolean idle) { this.idle = idle; }
@Override
public String toString() {
return String.format("DeviceStatusInfo{status='%s', points=%d, idle=%s}",
status, points, idle);
}
}
}

View File

@@ -129,6 +129,65 @@ public class DeviceStatusService {
private boolean isDeviceAvailable(String val) {
return "空闲".equals(val);
}
/**
* 解析特定设备的状态信息(用于检查是否完成游戏)
* @param jsonResponse JSON响应字符串
* @param machineId 设备ID
* @return 包含f0(点数)和f1(状态)的Map
*/
public Map<String, Object> parseDeviceStatusForMachine(String jsonResponse, String machineId) {
Map<String, Object> result = new HashMap<>();
try {
log.debug("解析设备 {} 的状态信息", machineId);
JsonNode rootNode = objectMapper.readTree(jsonResponse);
// 查找指定设备的信息
JsonNode deviceNode = rootNode.get(machineId);
if (deviceNode == null) {
log.warn("未找到设备 {} 的状态信息", machineId);
return result;
}
// 解析f0(点数)和f1(状态)等信息
String val = deviceNode.get("val").asText();
String time = deviceNode.get("time").asText();
// 构建f0和f1格式的返回数据
Map<String, Object> f0Info = new HashMap<>();
Map<String, Object> f1Info = new HashMap<>();
// 假设val包含点数信息f1包含状态信息
// 根据实际API响应格式调整
if (val.matches("\\d+")) {
// 如果val是数字则认为是点数
f0Info.put("val", val);
f0Info.put("time", time);
result.put("f0", f0Info);
// 状态信息需要额外获取,这里先设置默认值
f1Info.put("val", "空闲"); // 如果能获取到点数,可能表示空闲
f1Info.put("time", time);
result.put("f1", f1Info);
} else {
// 如果val不是数字可能是状态信息
f1Info.put("val", val);
f1Info.put("time", time);
result.put("f1", f1Info);
}
log.debug("设备 {} 状态解析完成: {}", machineId, result);
} catch (JsonProcessingException e) {
log.error("解析设备 {} 状态JSON失败: {}", machineId, e.getMessage(), e);
} catch (Exception e) {
log.error("解析设备 {} 状态时发生异常", machineId, e);
}
return result;
}
/**
* 按系列分组空闲设备

View File

@@ -280,6 +280,33 @@ public class ScriptClient {
.doOnSuccess(result -> log.info("保存总次数接口调用成功: url={}, result={}", url, result))
.doOnError(e -> log.warn("保存总次数接口调用失败: times={}, error={}", times, e.toString()));
}
/**
* 获取特定设备的状态信息(用于检查是否完成游戏)
* @param machineId 设备ID
* @return 设备状态信息的Map包含f0(点数)和f1(状态)等信息
*/
public Mono<java.util.Map<String, Object>> getDeviceStatus(String machineId) {
String url = "http://36.138.184.60:1234/yijianwan_netfile/readAllMsg?文件名=判断分数";
log.debug("获取设备状态: 设备={}, url={}", machineId, url);
return webClient.get()
.uri(url)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(String.class)
.timeout(Duration.ofSeconds(10))
.map(jsonResponse -> {
// 解析JSON响应提取指定设备的状态信息
return deviceStatusService.parseDeviceStatusForMachine(jsonResponse, machineId);
})
.doOnSuccess(deviceStatus -> {
log.debug("获取设备状态成功: 设备={}, 状态={}", machineId, deviceStatus);
})
.doOnError(e -> {
log.warn("获取设备状态失败: 设备={}, 错误={}", machineId, e.toString());
});
}
}

View File

@@ -190,6 +190,7 @@ public class LinkListService {
case "NEW" -> "新建";
case "USING" -> "使用中";
case "LOGGED_IN" -> "已登录";
case "COMPLETED" -> "已完成";
case "REFUNDED" -> "已退款";
case "EXPIRED" -> "已过期";
default -> "未知状态";

View File

@@ -13,6 +13,7 @@ import com.gameplatform.server.model.entity.agent.LinkTask;
import com.gameplatform.server.service.external.ScriptClient;
import com.gameplatform.server.service.device.DeviceCodeMappingService;
import com.gameplatform.server.service.device.DeviceStatusCheckService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
@@ -37,6 +38,7 @@ public class LinkStatusService {
private final LinkBatchMapper linkBatchMapper;
private final ScriptClient scriptClient;
private final DeviceCodeMappingService deviceCodeMappingService;
private final DeviceStatusCheckService deviceStatusCheckService;
// 状态描述映射
@@ -45,17 +47,19 @@ public class LinkStatusService {
STATUS_DESC_MAP.put("NEW", "新建");
STATUS_DESC_MAP.put("USING", "使用中");
STATUS_DESC_MAP.put("LOGGED_IN", "已登录");
STATUS_DESC_MAP.put("COMPLETED", "已完成");
STATUS_DESC_MAP.put("REFUNDED", "已退款");
STATUS_DESC_MAP.put("EXPIRED", "已过期");
}
public LinkStatusService(LinkTaskMapper linkTaskMapper, LinkBatchMapper linkBatchMapper,
ScriptClient scriptClient, DeviceCodeMappingService deviceCodeMappingService) {
ScriptClient scriptClient, DeviceCodeMappingService deviceCodeMappingService,
DeviceStatusCheckService deviceStatusCheckService) {
this.linkTaskMapper = linkTaskMapper;
this.linkBatchMapper = linkBatchMapper;
this.scriptClient = scriptClient;
this.deviceCodeMappingService = deviceCodeMappingService;
this.deviceStatusCheckService = deviceStatusCheckService;
}
/**
@@ -312,8 +316,8 @@ public class LinkStatusService {
// 如果未超过10分钟执行自动刷新
log.info("链接状态是USING执行自动刷新");
performAutoRefresh(linkTask);
} else if ("LOGGED_IN".equals(linkTask.getStatus()) || "REFUNDED".equals(linkTask.getStatus())) {
// 已上号或已退款状态,不需要刷新
} else if ("LOGGED_IN".equals(linkTask.getStatus()) || "COMPLETED".equals(linkTask.getStatus()) || "REFUNDED".equals(linkTask.getStatus())) {
// 已上号、已完成或已退款状态,不需要刷新
log.info("链接状态为 {},不需要刷新", linkTask.getStatus());
}
@@ -447,6 +451,15 @@ public class LinkStatusService {
log.info("从空闲设备列表中选择设备: {}", selectedDevice);
log.info("设备选择详情: 可用设备总数={}, 选择了第一个设备={}",
deviceStatus.getAvailableDevices().size(), selectedDevice);
// 7.5. 检查该设备是否有之前的LOGGED_IN状态链接任务需要完成
try {
log.info("检查设备 {} 是否有需要完成的链接任务", selectedDevice);
deviceStatusCheckService.checkDeviceStatusAndUpdateTasks(selectedDevice, "选区请求");
} catch (Exception e) {
log.warn("检查设备状态时发生异常,继续选区流程: {}", e.getMessage());
// 不影响选区流程,只记录警告日志
}
// 8. 调用保存总次数接口
try {

View File

@@ -0,0 +1,67 @@
package com.gameplatform.server.task;
import com.gameplatform.server.model.dto.device.DeviceStatusResponse;
import com.gameplatform.server.service.device.DeviceStatusCheckService;
import com.gameplatform.server.service.external.ScriptClient;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 设备状态检查定时任务
*/
@Component
@Slf4j
public class DeviceStatusCheckTask {
private final ScriptClient scriptClient;
private final DeviceStatusCheckService deviceStatusCheckService;
public DeviceStatusCheckTask(ScriptClient scriptClient, DeviceStatusCheckService deviceStatusCheckService) {
this.scriptClient = scriptClient;
this.deviceStatusCheckService = deviceStatusCheckService;
}
/**
* 每分钟检查一次空闲设备,并更新相关链接任务状态
*/
@Scheduled(fixedRate = 60000) // 每60秒执行一次
public void checkIdleDevicesAndUpdateTasks() {
log.debug("=== 开始定时检查空闲设备 ===");
try {
// 1. 获取所有设备状态
DeviceStatusResponse deviceStatus = scriptClient.checkAvailableDeviceStatus().block();
if (deviceStatus == null) {
log.warn("获取设备状态失败,跳过本次检查");
return;
}
List<String> availableDevices = deviceStatus.getAvailableDevices();
if (availableDevices.isEmpty()) {
log.debug("当前没有空闲设备");
return;
}
log.info("发现 {} 个空闲设备: {}", availableDevices.size(), availableDevices);
// 2. 对每个空闲设备检查是否有相关的LOGGED_IN状态链接任务
for (String deviceId : availableDevices) {
try {
deviceStatusCheckService.checkDeviceStatusAndUpdateTasks(deviceId, "定时检查");
} catch (Exception e) {
log.error("检查设备 {} 状态时发生异常", deviceId, e);
// 继续检查下一个设备,不因为一个设备出错而中断整个流程
}
}
} catch (Exception e) {
log.error("定时检查空闲设备时发生异常", e);
}
log.debug("=== 定时检查空闲设备完成 ===");
}
}

View File

@@ -21,24 +21,25 @@
<result property="qrCreatedAt" column="qr_created_at" />
<result property="qrExpireAt" column="qr_expire_at" />
<result property="firstRegionSelectAt" column="first_region_select_at" />
<result property="completedPoints" column="completed_points" />
</resultMap>
<select id="findById" parameterType="long" 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
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
FROM link_task
WHERE id = #{id}
LIMIT 1
</select>
<select id="findByCodeNo" parameterType="string" 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
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
FROM link_task
WHERE code_no = #{codeNo}
LIMIT 1
</select>
<select id="findByTokenHash" parameterType="string" 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
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
FROM link_task
WHERE token_hash = #{tokenHash}
LIMIT 1
@@ -78,7 +79,7 @@
</update>
<select id="findByAgentId" 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
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
FROM link_task
WHERE agent_id = #{agentId}
ORDER BY created_at DESC
@@ -90,7 +91,7 @@
</select>
<select id="findByAgentIdAndStatus" 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
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
FROM link_task
WHERE agent_id = #{agentId} AND status = #{status}
ORDER BY created_at DESC
@@ -102,7 +103,7 @@
</select>
<select id="findByBatchId" 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
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
FROM link_task
WHERE batch_id = #{batchId}
ORDER BY created_at DESC
@@ -114,7 +115,7 @@
</select>
<select id="findExpiredTasks" 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
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
FROM link_task
WHERE expire_at &lt;= #{expireTime} AND status IN ('NEW', 'USING')
ORDER BY expire_at ASC
@@ -122,7 +123,7 @@
</select>
<select id="findLinkTasksWithConditions" 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
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
FROM link_task
<where>
agent_id = #{agentId}
@@ -199,7 +200,7 @@
</delete>
<select id="findByCodeNosAndAgentId" 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
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, completed_points
FROM link_task
WHERE agent_id = #{agentId}
AND code_no IN
@@ -207,4 +208,10 @@
#{codeNo}
</foreach>
</select>
<select id="findByMachineIdAndStatus" 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, completed_points
FROM link_task
WHERE machine_id = #{machineId} AND status = #{status}
</select>
</mapper>