This commit is contained in:
zyh
2025-08-26 15:18:14 +08:00
parent 599ec0a36b
commit d3fe8fda7d
77 changed files with 1149 additions and 60 deletions

View File

@@ -2,8 +2,11 @@ package com.gameplatform.server.controller.link;
import com.gameplatform.server.model.dto.link.LinkGenerateRequest;
import com.gameplatform.server.model.dto.link.LinkGenerateResponse;
import com.gameplatform.server.model.dto.link.LinkListRequest;
import com.gameplatform.server.model.dto.link.LinkListResponse;
import com.gameplatform.server.model.dto.link.LinkStatusResponse;
import com.gameplatform.server.service.link.LinkGenerationService;
import com.gameplatform.server.service.link.LinkListService;
import com.gameplatform.server.service.link.LinkStatusService;
import io.jsonwebtoken.Claims;
import io.swagger.v3.oas.annotations.Operation;
@@ -27,10 +30,51 @@ public class LinkController {
private final LinkGenerationService linkGenerationService;
private final LinkStatusService linkStatusService;
private final LinkListService linkListService;
public LinkController(LinkGenerationService linkGenerationService, LinkStatusService linkStatusService) {
public LinkController(LinkGenerationService linkGenerationService,
LinkStatusService linkStatusService,
LinkListService linkListService) {
this.linkGenerationService = linkGenerationService;
this.linkStatusService = linkStatusService;
this.linkListService = linkListService;
}
@GetMapping("/list")
@Operation(summary = "查询链接列表", description = "分页查询用户生成的链接列表支持按状态、批次ID等条件过滤和排序")
public Mono<LinkListResponse> getLinkList(@Valid LinkListRequest request, Authentication authentication) {
log.info("=== 开始查询链接列表 ===");
log.info("请求参数: page={}, pageSize={}, status={}, batchId={}, isExpired={}, sortBy={}, sortDir={}",
request.getPage(), request.getPageSize(), request.getStatus(),
request.getBatchId(), request.getIsExpired(), request.getSortBy(), request.getSortDir());
if (authentication == null) {
log.error("=== 认证失败Authentication为空 ===");
return Mono.error(new IllegalArgumentException("用户未认证Authentication为空"));
}
log.info("认证用户: {}", authentication.getName());
// 获取用户ID
Claims claims = (Claims) authentication.getCredentials();
Long agentId = claims.get("userId", Long.class);
String userType = claims.get("userType", String.class);
log.info("用户信息: agentId={}, userType={}", agentId, userType);
if (agentId == null) {
log.error("=== 无法获取用户ID ===");
return Mono.error(new IllegalArgumentException("无法获取用户ID"));
}
return linkListService.getLinkList(agentId, request)
.doOnSuccess(response -> {
log.info("链接列表查询成功: 总数={}, 当前页={}, 总页数={}",
response.getTotal(), response.getPage(), response.getTotalPages());
})
.doOnError(error -> {
log.error("链接列表查询失败: agentId={}, error={}", agentId, error.getMessage(), error);
});
}
@PostMapping("/generate")

View File

@@ -20,4 +20,9 @@ public interface LinkBatchMapper {
@Param("offset") int offset);
long countAll();
/**
* 根据ID列表查询批次信息
*/
List<LinkBatch> findByIds(@Param("ids") List<Long> ids);
}

View File

@@ -47,4 +47,24 @@ public interface LinkTaskMapper {
List<LinkTask> findExpiredTasks(@Param("expireTime") LocalDateTime expireTime,
@Param("size") int size);
/**
* 分页查询代理的链接任务(支持条件过滤和排序)
*/
List<LinkTask> findLinkTasksWithConditions(@Param("agentId") Long agentId,
@Param("status") String status,
@Param("batchId") Long batchId,
@Param("isExpired") Boolean isExpired,
@Param("sortBy") String sortBy,
@Param("sortDir") String sortDir,
@Param("offset") int offset,
@Param("size") int size);
/**
* 统计满足条件的链接任务数量
*/
long countLinkTasksWithConditions(@Param("agentId") Long agentId,
@Param("status") String status,
@Param("batchId") Long batchId,
@Param("isExpired") Boolean isExpired);
}

View File

@@ -0,0 +1,14 @@
package com.gameplatform.server.mapper.agent;
import com.mybatisflex.core.BaseMapper;
import com.gameplatform.server.model.entity.agent.LinkBatch;
import org.apache.ibatis.annotations.Mapper;
/**
* MyBatis-Flex Mapper for LinkBatch
* 提供高性能的查询构建器和自动生成的基础CRUD操作
*/
@Mapper
public interface LinkBatchFlexMapper extends BaseMapper<LinkBatch> {
// MyBatis-Flex 会自动提供完整的CRUD操作
}

View File

@@ -0,0 +1,29 @@
package com.gameplatform.server.mapper.agent;
import com.mybatisflex.core.BaseMapper;
import com.gameplatform.server.model.entity.agent.LinkTask;
import org.apache.ibatis.annotations.Mapper;
/**
* MyBatis-Flex Mapper for LinkTask
* 提供高性能的查询构建器和自动生成的基础CRUD操作
*/
@Mapper
public interface LinkTaskFlexMapper extends BaseMapper<LinkTask> {
// MyBatis-Flex 会自动提供以下方法:
// - selectById
// - selectByMap
// - selectByCondition
// - selectListByCondition
// - selectCountByCondition
// - selectPageByCondition
// - insert
// - insertBatch
// - update
// - updateByCondition
// - delete
// - deleteByCondition
// 等等...
// 如果需要自定义 SQL可以在这里添加方法并在 XML 文件中实现
}

View File

@@ -0,0 +1,64 @@
package com.gameplatform.server.model.dto.link;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 链接列表项DTO
*/
@Data
@Schema(description = "链接列表项")
public class LinkListItem {
@Schema(description = "链接编号", example = "ABC12345")
private String codeNo;
@Schema(description = "批次ID", example = "123")
private Long batchId;
@Schema(description = "链接状态", example = "NEW")
private String status;
@Schema(description = "状态描述", example = "新建")
private String statusDesc;
@Schema(description = "过期时间", example = "2024-01-15T16:30:00")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime expireAt;
@Schema(description = "是否已过期", example = "false")
private Boolean isExpired;
@Schema(description = "剩余时间(秒)", example = "3600")
private Long remainingSeconds;
@Schema(description = "每次奖励数量", example = "50")
private Integer quantity;
@Schema(description = "执行次数", example = "5")
private Integer times;
@Schema(description = "总奖励点数", example = "250")
private Integer totalPoints;
@Schema(description = "分配区域", example = "Q")
private String region;
@Schema(description = "机器ID", example = "MACHINE001")
private String machineId;
@Schema(description = "登录时间", example = "2024-01-15T14:30:00")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime loginAt;
@Schema(description = "创建时间", example = "2024-01-15T12:00:00")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createdAt;
@Schema(description = "更新时间", example = "2024-01-15T12:00:00")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updatedAt;
}

View File

@@ -0,0 +1,38 @@
package com.gameplatform.server.model.dto.link;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import lombok.Data;
/**
* 链接列表查询请求DTO
*/
@Data
@Schema(description = "链接列表查询请求")
public class LinkListRequest {
@Schema(description = "页码从1开始", example = "1")
@Min(value = 1, message = "页码必须大于0")
private Integer page = 1;
@Schema(description = "每页大小", example = "20")
@Min(value = 1, message = "每页大小必须大于0")
@Max(value = 100, message = "每页大小不能超过100")
private Integer pageSize = 20;
@Schema(description = "链接状态过滤", example = "NEW")
private String status;
@Schema(description = "批次ID过滤", example = "123")
private Long batchId;
@Schema(description = "是否过期过滤", example = "false")
private Boolean isExpired;
@Schema(description = "排序字段", example = "createdAt", allowableValues = {"createdAt", "updatedAt", "expireAt"})
private String sortBy = "createdAt";
@Schema(description = "排序方向", example = "DESC", allowableValues = {"ASC", "DESC"})
private String sortDir = "DESC";
}

View File

@@ -0,0 +1,35 @@
package com.gameplatform.server.model.dto.link;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
/**
* 链接列表响应DTO
*/
@Data
@Schema(description = "链接列表响应")
public class LinkListResponse {
@Schema(description = "链接列表")
private List<LinkListItem> items;
@Schema(description = "总记录数", example = "150")
private Long total;
@Schema(description = "当前页码", example = "1")
private Integer page;
@Schema(description = "每页大小", example = "20")
private Integer pageSize;
@Schema(description = "总页数", example = "8")
private Integer totalPages;
@Schema(description = "是否有下一页", example = "true")
private Boolean hasNext;
@Schema(description = "是否有上一页", example = "false")
private Boolean hasPrev;
}

View File

@@ -98,4 +98,3 @@ public class LinkStatusResponse {
public LocalDateTime getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
}

View File

@@ -0,0 +1,198 @@
package com.gameplatform.server.service.link;
import com.gameplatform.server.mapper.agent.LinkBatchMapper;
import com.gameplatform.server.mapper.agent.LinkTaskMapper;
import com.gameplatform.server.model.dto.link.LinkListItem;
import com.gameplatform.server.model.dto.link.LinkListRequest;
import com.gameplatform.server.model.dto.link.LinkListResponse;
import com.gameplatform.server.model.entity.agent.LinkBatch;
import com.gameplatform.server.model.entity.agent.LinkTask;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 链接列表查询服务
*/
@Service
public class LinkListService {
private static final Logger log = LoggerFactory.getLogger(LinkListService.class);
private final LinkTaskMapper linkTaskMapper;
private final LinkBatchMapper linkBatchMapper;
public LinkListService(LinkTaskMapper linkTaskMapper, LinkBatchMapper linkBatchMapper) {
this.linkTaskMapper = linkTaskMapper;
this.linkBatchMapper = linkBatchMapper;
}
/**
* 分页查询链接列表
*/
public Mono<LinkListResponse> getLinkList(Long agentId, LinkListRequest request) {
return Mono.fromCallable(() -> {
log.info("=== 开始查询链接列表 ===");
log.info("代理ID: {}, 请求参数: {}", agentId, request);
// 计算偏移量
int offset = (request.getPage() - 1) * request.getPageSize();
// 验证排序参数
validateSortParams(request);
// 查询链接任务列表
List<LinkTask> linkTasks = linkTaskMapper.findLinkTasksWithConditions(
agentId,
request.getStatus(),
request.getBatchId(),
request.getIsExpired(),
request.getSortBy(),
request.getSortDir(),
offset,
request.getPageSize()
);
// 查询总数
long total = linkTaskMapper.countLinkTasksWithConditions(
agentId,
request.getStatus(),
request.getBatchId(),
request.getIsExpired()
);
log.info("查询到 {} 条记录,总数: {}", linkTasks.size(), total);
// 获取所有相关的批次信息
Map<Long, LinkBatch> batchMap = getBatchMap(linkTasks);
// 转换为DTO
List<LinkListItem> items = linkTasks.stream()
.map(task -> convertToListItem(task, batchMap.get(task.getBatchId())))
.collect(Collectors.toList());
// 构建响应
LinkListResponse response = new LinkListResponse();
response.setItems(items);
response.setTotal(total);
response.setPage(request.getPage());
response.setPageSize(request.getPageSize());
response.setTotalPages((int) Math.ceil((double) total / request.getPageSize()));
response.setHasNext(request.getPage() * request.getPageSize() < total);
response.setHasPrev(request.getPage() > 1);
log.info("链接列表查询完成: 当前页={}, 每页大小={}, 总页数={}",
response.getPage(), response.getPageSize(), response.getTotalPages());
return response;
});
}
/**
* 验证排序参数
*/
private void validateSortParams(LinkListRequest request) {
String sortBy = request.getSortBy();
if (sortBy != null && !List.of("createdAt", "updatedAt", "expireAt").contains(sortBy)) {
request.setSortBy("createdAt");
log.warn("无效的排序字段 '{}', 使用默认值 'createdAt'", sortBy);
}
String sortDir = request.getSortDir();
if (sortDir != null && !List.of("ASC", "DESC").contains(sortDir.toUpperCase())) {
request.setSortDir("DESC");
log.warn("无效的排序方向 '{}', 使用默认值 'DESC'", sortDir);
} else if (sortDir != null) {
request.setSortDir(sortDir.toUpperCase());
}
}
/**
* 获取批次信息映射
*/
private Map<Long, LinkBatch> getBatchMap(List<LinkTask> linkTasks) {
if (linkTasks.isEmpty()) {
return Map.of();
}
List<Long> batchIds = linkTasks.stream()
.map(LinkTask::getBatchId)
.distinct()
.collect(Collectors.toList());
try {
List<LinkBatch> batches = linkBatchMapper.findByIds(batchIds);
return batches.stream()
.collect(Collectors.toMap(LinkBatch::getId, batch -> batch));
} catch (Exception e) {
log.error("获取批次信息失败: {}", e.getMessage(), e);
return Map.of();
}
}
/**
* 转换为列表项DTO
*/
private LinkListItem convertToListItem(LinkTask task, LinkBatch batch) {
LinkListItem item = new LinkListItem();
// 基本信息
item.setCodeNo(task.getCodeNo());
item.setBatchId(task.getBatchId());
item.setStatus(task.getStatus());
item.setStatusDesc(getStatusDesc(task.getStatus()));
item.setExpireAt(task.getExpireAt());
item.setRegion(task.getRegion());
item.setMachineId(task.getMachineId());
item.setLoginAt(task.getLoginAt());
item.setCreatedAt(task.getCreatedAt());
item.setUpdatedAt(task.getUpdatedAt());
// 计算是否过期和剩余时间
LocalDateTime now = LocalDateTime.now();
boolean isExpired = task.getExpireAt().isBefore(now);
item.setIsExpired(isExpired);
if (isExpired) {
item.setRemainingSeconds(0L);
} else {
long remainingSeconds = ChronoUnit.SECONDS.between(now, task.getExpireAt());
item.setRemainingSeconds(Math.max(0, remainingSeconds));
}
// 从批次信息中获取任务详情
if (batch != null) {
item.setQuantity(batch.getQuantity());
item.setTimes(batch.getTimes());
item.setTotalPoints(batch.getQuantity() * batch.getTimes());
} else {
// 如果批次信息不存在,设置默认值
log.warn("批次信息不存在: batchId={}", task.getBatchId());
item.setQuantity(0);
item.setTimes(0);
item.setTotalPoints(0);
}
return item;
}
/**
* 获取状态描述
*/
private String getStatusDesc(String status) {
return switch (status) {
case "NEW" -> "新建";
case "USING" -> "使用中";
case "LOGGED_IN" -> "已登录";
case "REFUNDED" -> "已退款";
case "EXPIRED" -> "已过期";
default -> "未知状态";
};
}
}

View File

@@ -122,4 +122,3 @@ public class LinkStatusService {
.onErrorReturn(false);
}
}

View File

@@ -44,4 +44,13 @@
<select id="countAll" resultType="long">
SELECT COUNT(1) FROM link_batch
</select>
<select id="findByIds" resultMap="LinkBatchMap">
SELECT id, agent_id, quantity, times, operator_id, created_at
FROM link_batch
WHERE id IN
<foreach item="id" collection="ids" open="(" separator="," close=")">
#{id}
</foreach>
</select>
</mapper>

View File

@@ -110,4 +110,64 @@
ORDER BY expire_at ASC
LIMIT #{size}
</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
FROM link_task
<where>
agent_id = #{agentId}
<if test="status != null and status != ''">
AND status = #{status}
</if>
<if test="batchId != null">
AND batch_id = #{batchId}
</if>
<if test="isExpired != null">
<choose>
<when test="isExpired == true">
AND expire_at &lt;= NOW()
</when>
<otherwise>
AND expire_at &gt; NOW()
</otherwise>
</choose>
</if>
</where>
<choose>
<when test="sortBy == 'expireAt'">
ORDER BY expire_at ${sortDir}
</when>
<when test="sortBy == 'updatedAt'">
ORDER BY updated_at ${sortDir}
</when>
<otherwise>
ORDER BY created_at ${sortDir}
</otherwise>
</choose>
LIMIT #{size} OFFSET #{offset}
</select>
<select id="countLinkTasksWithConditions" resultType="long">
SELECT COUNT(1)
FROM link_task
<where>
agent_id = #{agentId}
<if test="status != null and status != ''">
AND status = #{status}
</if>
<if test="batchId != null">
AND batch_id = #{batchId}
</if>
<if test="isExpired != null">
<choose>
<when test="isExpired == true">
AND expire_at &lt;= NOW()
</when>
<otherwise>
AND expire_at &gt; NOW()
</otherwise>
</choose>
</if>
</where>
</select>
</mapper>