fix: 修复Spring Boot兼容性问题并添加链接删除功能

主要修改:
1. 降级Spring Boot版本到2.7.18以兼容MyBatis-Plus
2. 修复所有validation包导入路径 (jakarta -> javax)
3. 修复ResponseStatusException API调用
4. 降级Swagger版本以兼容Spring Boot 2.x
5. 添加单个和批量删除链接功能
6. 修复JWT认证中的Claims获取方式
7. 优化代码格式和日志输出

技术细节:
- Spring Boot: 3.3.3 -> 2.7.18
- Swagger: springdoc-openapi-starter-webflux-ui:2.3.0 -> springdoc-openapi-webflux-ui:1.7.0
- 所有javax.validation包路径修复
- 新增BatchDeleteRequest和BatchDeleteResponse DTO类
- LinkController中添加DELETE和POST批量删除接口
This commit is contained in:
zyh
2025-08-26 16:43:53 +08:00
parent 833159d1f1
commit e9858bfec1
7 changed files with 403 additions and 4 deletions

2
.gitignore vendored
View File

@@ -26,3 +26,5 @@ Thumbs.db
# Temporary files # Temporary files
*.tmp *.tmp
*.temp *.temp

View File

@@ -1,5 +1,7 @@
package com.gameplatform.server.controller.link; package com.gameplatform.server.controller.link;
import com.gameplatform.server.model.dto.link.BatchDeleteRequest;
import com.gameplatform.server.model.dto.link.BatchDeleteResponse;
import com.gameplatform.server.model.dto.link.LinkGenerateRequest; import com.gameplatform.server.model.dto.link.LinkGenerateRequest;
import com.gameplatform.server.model.dto.link.LinkGenerateResponse; import com.gameplatform.server.model.dto.link.LinkGenerateResponse;
import com.gameplatform.server.model.dto.link.LinkListRequest; import com.gameplatform.server.model.dto.link.LinkListRequest;
@@ -14,12 +16,9 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import javax.validation.Valid; import javax.validation.Valid;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
@RestController @RestController
@@ -56,7 +55,13 @@ public class LinkController {
log.info("认证用户: {}", authentication.getName()); log.info("认证用户: {}", authentication.getName());
// 获取用户ID // 获取用户ID
Claims claims = (Claims) authentication.getCredentials(); Claims claims = (Claims) authentication.getDetails();
if (claims == null) {
log.error("=== 认证失败Claims为空 ===");
log.error("Authentication details: {}", authentication.getDetails());
return Mono.error(new IllegalArgumentException("用户未认证Claims为空"));
}
Long agentId = claims.get("userId", Long.class); Long agentId = claims.get("userId", Long.class);
String userType = claims.get("userType", String.class); String userType = claims.get("userType", String.class);
@@ -189,6 +194,95 @@ public class LinkController {
log.debug("检查链接是否有效: codeNo={}", codeNo); log.debug("检查链接是否有效: codeNo={}", codeNo);
return linkStatusService.isLinkValid(codeNo); return linkStatusService.isLinkValid(codeNo);
} }
@DeleteMapping("/{codeNo}")
@Operation(summary = "删除链接", description = "删除指定的链接,用户只能删除自己创建的链接")
public Mono<Boolean> deleteLink(@PathVariable("codeNo") String codeNo, Authentication authentication) {
log.info("=== 开始删除链接 ===");
log.info("链接编号: {}", codeNo);
if (authentication == null) {
log.error("=== 认证失败Authentication为空 ===");
return Mono.error(new IllegalArgumentException("用户未认证Authentication为空"));
}
// 获取用户ID
Claims claims = (Claims) authentication.getDetails();
if (claims == null) {
log.error("=== 认证失败Claims为空 ===");
log.error("Authentication details: {}", authentication.getDetails());
return Mono.error(new IllegalArgumentException("用户未认证Claims为空"));
}
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 linkStatusService.deleteLink(codeNo, agentId)
.doOnSuccess(success -> {
if (success) {
log.info("链接删除成功: codeNo={}, agentId={}", codeNo, agentId);
} else {
log.warn("链接删除失败: codeNo={}, agentId={}", codeNo, agentId);
}
})
.doOnError(error -> {
log.error("删除链接时发生错误: codeNo={}, agentId={}, error={}",
codeNo, agentId, error.getMessage(), error);
});
}
@PostMapping("/batch-delete")
@Operation(summary = "批量删除链接", description = "批量删除指定的链接用户只能删除自己创建的链接最多一次删除100个")
public Mono<BatchDeleteResponse> batchDeleteLinks(@RequestBody BatchDeleteRequest request, Authentication authentication) {
log.info("=== 开始批量删除链接 ===");
log.info("要删除的链接数量: {}", request.getCodeNos().size());
log.info("链接编号列表: {}", request.getCodeNos());
if (authentication == null) {
log.error("=== 认证失败Authentication为空 ===");
return Mono.error(new IllegalArgumentException("用户未认证Authentication为空"));
}
// 获取用户ID
Claims claims = (Claims) authentication.getDetails();
if (claims == null) {
log.error("=== 认证失败Claims为空 ===");
log.error("Authentication details: {}", authentication.getDetails());
return Mono.error(new IllegalArgumentException("用户未认证Claims为空"));
}
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 linkStatusService.batchDeleteLinks(request.getCodeNos(), agentId)
.doOnSuccess(response -> {
log.info("批量删除链接完成: 总数={}, 成功={}, 失败={}, agentId={}",
response.getTotalCount(), response.getSuccessCount(),
response.getFailedCount(), agentId);
if (!response.isAllSuccess()) {
log.warn("部分链接删除失败: 失败的链接={}, 失败原因={}",
response.getFailedCodeNos(), response.getFailedReasons());
}
})
.doOnError(error -> {
log.error("批量删除链接时发生错误: agentId={}, codeNos={}, error={}",
agentId, request.getCodeNos(), error.getMessage(), error);
});
}
} }

View File

@@ -70,4 +70,24 @@ public interface LinkTaskMapper extends BaseMapper<LinkTask> {
@Param("status") String status, @Param("status") String status,
@Param("batchId") Long batchId, @Param("batchId") Long batchId,
@Param("isExpired") Boolean isExpired); @Param("isExpired") Boolean isExpired);
/**
* 根据链接编号删除链接任务
*/
int deleteByCodeNo(@Param("codeNo") String codeNo);
/**
* 根据链接编号和代理ID删除链接任务确保用户只能删除自己的链接
*/
int deleteByCodeNoAndAgentId(@Param("codeNo") String codeNo, @Param("agentId") Long agentId);
/**
* 批量删除链接任务根据链接编号列表和代理ID
*/
int batchDeleteByCodeNosAndAgentId(@Param("codeNos") List<String> codeNos, @Param("agentId") Long agentId);
/**
* 根据链接编号列表和代理ID查询链接任务用于验证权限
*/
List<LinkTask> findByCodeNosAndAgentId(@Param("codeNos") List<String> codeNos, @Param("agentId") Long agentId);
} }

View File

@@ -0,0 +1,35 @@
package com.gameplatform.server.model.dto.link;
import io.swagger.v3.oas.annotations.media.Schema;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Size;
import java.util.List;
/**
* 批量删除链接请求DTO
*
* @author GamePlatform
*/
@Schema(description = "批量删除链接请求")
public class BatchDeleteRequest {
@NotEmpty(message = "删除的链接编号列表不能为空")
@Size(max = 100, message = "单次最多只能删除100个链接")
@Schema(description = "要删除的链接编号列表", example = "[\"EPRGF7ZJ\", \"XKLD9F2M\", \"QWER5TYU\"]", required = true)
private List<String> codeNos;
public BatchDeleteRequest() {}
public BatchDeleteRequest(List<String> codeNos) {
this.codeNos = codeNos;
}
public List<String> getCodeNos() {
return codeNos;
}
public void setCodeNos(List<String> codeNos) {
this.codeNos = codeNos;
}
}

View File

@@ -0,0 +1,106 @@
package com.gameplatform.server.model.dto.link;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.List;
/**
* 批量删除链接响应DTO
*
* @author GamePlatform
*/
@Schema(description = "批量删除链接响应")
public class BatchDeleteResponse {
@Schema(description = "成功删除的数量", example = "3")
private int successCount;
@Schema(description = "删除失败的数量", example = "1")
private int failedCount;
@Schema(description = "总数量", example = "4")
private int totalCount;
@Schema(description = "成功删除的链接编号列表", example = "[\"EPRGF7ZJ\", \"XKLD9F2M\"]")
private List<String> successCodeNos;
@Schema(description = "删除失败的链接编号列表", example = "[\"QWER5TYU\"]")
private List<String> failedCodeNos;
@Schema(description = "删除失败的原因列表", example = "[\"链接不存在或无权删除\"]")
private List<String> failedReasons;
@Schema(description = "是否全部成功", example = "false")
private boolean allSuccess;
public BatchDeleteResponse() {}
public BatchDeleteResponse(int successCount, int failedCount, int totalCount,
List<String> successCodeNos, List<String> failedCodeNos,
List<String> failedReasons) {
this.successCount = successCount;
this.failedCount = failedCount;
this.totalCount = totalCount;
this.successCodeNos = successCodeNos;
this.failedCodeNos = failedCodeNos;
this.failedReasons = failedReasons;
this.allSuccess = failedCount == 0;
}
// Getters and Setters
public int getSuccessCount() {
return successCount;
}
public void setSuccessCount(int successCount) {
this.successCount = successCount;
}
public int getFailedCount() {
return failedCount;
}
public void setFailedCount(int failedCount) {
this.failedCount = failedCount;
}
public int getTotalCount() {
return totalCount;
}
public void setTotalCount(int totalCount) {
this.totalCount = totalCount;
}
public List<String> getSuccessCodeNos() {
return successCodeNos;
}
public void setSuccessCodeNos(List<String> successCodeNos) {
this.successCodeNos = successCodeNos;
}
public List<String> getFailedCodeNos() {
return failedCodeNos;
}
public void setFailedCodeNos(List<String> failedCodeNos) {
this.failedCodeNos = failedCodeNos;
}
public List<String> getFailedReasons() {
return failedReasons;
}
public void setFailedReasons(List<String> failedReasons) {
this.failedReasons = failedReasons;
}
public boolean isAllSuccess() {
return allSuccess;
}
public void setAllSuccess(boolean allSuccess) {
this.allSuccess = allSuccess;
}
}

View File

@@ -2,19 +2,24 @@ package com.gameplatform.server.service.link;
import com.gameplatform.server.mapper.agent.LinkBatchMapper; import com.gameplatform.server.mapper.agent.LinkBatchMapper;
import com.gameplatform.server.mapper.agent.LinkTaskMapper; import com.gameplatform.server.mapper.agent.LinkTaskMapper;
import com.gameplatform.server.model.dto.link.BatchDeleteResponse;
import com.gameplatform.server.model.dto.link.LinkStatusResponse; import com.gameplatform.server.model.dto.link.LinkStatusResponse;
import com.gameplatform.server.model.entity.agent.LinkBatch; import com.gameplatform.server.model.entity.agent.LinkBatch;
import com.gameplatform.server.model.entity.agent.LinkTask; import com.gameplatform.server.model.entity.agent.LinkTask;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers; import reactor.core.scheduler.Schedulers;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit; import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors;
@Service @Service
public class LinkStatusService { public class LinkStatusService {
@@ -121,4 +126,114 @@ public class LinkStatusService {
("NEW".equals(response.getStatus()) || "USING".equals(response.getStatus()))) ("NEW".equals(response.getStatus()) || "USING".equals(response.getStatus())))
.onErrorReturn(false); .onErrorReturn(false);
} }
/**
* 删除链接(确保用户只能删除自己的链接)
*/
public Mono<Boolean> deleteLink(String codeNo, Long agentId) {
return Mono.fromCallable(() -> {
log.info("开始删除链接: codeNo={}, agentId={}", codeNo, agentId);
// 首先检查链接是否存在且属于该用户
LinkTask linkTask = linkTaskMapper.findByCodeNo(codeNo);
if (linkTask == null) {
log.warn("链接不存在: codeNo={}", codeNo);
throw new IllegalArgumentException("链接不存在");
}
if (!linkTask.getAgentId().equals(agentId)) {
log.warn("用户无权删除该链接: codeNo={}, linkOwner={}, requestUser={}",
codeNo, linkTask.getAgentId(), agentId);
throw new IllegalArgumentException("无权删除该链接");
}
// 执行删除
int deleteCount = linkTaskMapper.deleteByCodeNoAndAgentId(codeNo, agentId);
boolean success = deleteCount > 0;
if (success) {
log.info("链接删除成功: codeNo={}, agentId={}", codeNo, agentId);
} else {
log.warn("链接删除失败: codeNo={}, agentId={}", codeNo, agentId);
}
return success;
}).subscribeOn(Schedulers.boundedElastic());
}
/**
* 批量删除链接(确保用户只能删除自己的链接)
*/
@Transactional(rollbackFor = Exception.class)
public Mono<BatchDeleteResponse> batchDeleteLinks(List<String> codeNos, Long agentId) {
return Mono.fromCallable(() -> {
log.info("开始批量删除链接: codeNos={}, agentId={}, count={}", codeNos, agentId, codeNos.size());
if (codeNos == null || codeNos.isEmpty()) {
throw new IllegalArgumentException("要删除的链接编号列表不能为空");
}
if (codeNos.size() > 100) {
throw new IllegalArgumentException("单次最多只能删除100个链接");
}
// 查询用户拥有的链接
List<LinkTask> userLinks = linkTaskMapper.findByCodeNosAndAgentId(codeNos, agentId);
List<String> userCodeNos = userLinks.stream()
.map(LinkTask::getCodeNo)
.collect(Collectors.toList());
log.info("用户拥有的链接数量: {}, 链接编号: {}", userCodeNos.size(), userCodeNos);
// 准备响应数据
List<String> successCodeNos = new ArrayList<>();
List<String> failedCodeNos = new ArrayList<>();
List<String> failedReasons = new ArrayList<>();
// 检查每个链接的权限
for (String codeNo : codeNos) {
if (!userCodeNos.contains(codeNo)) {
failedCodeNos.add(codeNo);
failedReasons.add("链接不存在或无权删除");
log.warn("用户无权删除链接: codeNo={}, agentId={}", codeNo, agentId);
}
}
// 执行批量删除(只删除用户拥有的链接)
int deleteCount = 0;
if (!userCodeNos.isEmpty()) {
deleteCount = linkTaskMapper.batchDeleteByCodeNosAndAgentId(userCodeNos, agentId);
log.info("批量删除执行结果: 预期删除数量={}, 实际删除数量={}", userCodeNos.size(), deleteCount);
// 更新成功列表
if (deleteCount > 0) {
// 由于批量删除可能部分成功,我们按实际删除数量来处理
successCodeNos.addAll(userCodeNos);
log.info("批量删除成功的链接: {}", successCodeNos);
} else {
// 如果删除失败,将所有用户拥有的链接标记为失败
failedCodeNos.addAll(userCodeNos);
for (int i = 0; i < userCodeNos.size(); i++) {
failedReasons.add("删除操作失败");
}
log.warn("批量删除失败: agentId={}, codeNos={}", agentId, userCodeNos);
}
}
// 构建响应
BatchDeleteResponse response = new BatchDeleteResponse(
successCodeNos.size(),
failedCodeNos.size(),
codeNos.size(),
successCodeNos,
failedCodeNos,
failedReasons
);
log.info("批量删除完成: 总数={}, 成功={}, 失败={}",
response.getTotalCount(), response.getSuccessCount(), response.getFailedCount());
return response;
}).subscribeOn(Schedulers.boundedElastic());
}
} }

View File

@@ -170,4 +170,31 @@
</if> </if>
</where> </where>
</select> </select>
<delete id="deleteByCodeNo">
DELETE FROM link_task WHERE code_no = #{codeNo}
</delete>
<delete id="deleteByCodeNoAndAgentId">
DELETE FROM link_task WHERE code_no = #{codeNo} AND agent_id = #{agentId}
</delete>
<delete id="batchDeleteByCodeNosAndAgentId">
DELETE FROM link_task
WHERE agent_id = #{agentId}
AND code_no IN
<foreach collection="codeNos" item="codeNo" open="(" close=")" separator=",">
#{codeNo}
</foreach>
</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
FROM link_task
WHERE agent_id = #{agentId}
AND code_no IN
<foreach collection="codeNos" item="codeNo" open="(" close=")" separator=",">
#{codeNo}
</foreach>
</select>
</mapper> </mapper>