实现JWT身份认证机制,新增JWT认证过滤器和服务,更新链接生成接口以支持JWT验证,删除旧的用户控制器,添加JWT认证文档,增强错误处理和日志记录。

This commit is contained in:
zyh
2025-08-25 21:26:16 +08:00
parent 3f01d8590a
commit 7317866f98
54 changed files with 551 additions and 163 deletions

View File

@@ -0,0 +1,160 @@
# JWT认证使用说明
## 概述
系统已从手动传递操作者信息的头部参数改为使用JWTJSON Web Token进行身份认证。这种方式更加安全、标准化并且符合REST API的最佳实践。
## 认证流程
### 1. 用户登录获取JWT令牌
```http
POST /api/auth/login
Content-Type: application/json
{
"username": "your_username",
"password": "your_password"
}
```
响应示例:
```json
{
"token": "eyJhbGciOiJIUzI1NiJ9...",
"userType": "AGENT",
"userId": 123,
"username": "your_username"
}
```
### 2. 使用JWT令牌调用受保护的接口
在请求头中添加 `Authorization` 头:
```http
POST /api/link/generate
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
Content-Type: application/json
{
"times": 10,
"perTimeQuantity": 5
}
```
## 链接生成接口
### 接口地址
`POST /api/link/generate`
### 请求头
- `Authorization: Bearer {JWT_TOKEN}` - 必需JWT认证令牌
- `Content-Type: application/json` - 必需
### 请求参数
```json
{
"times": 10, // 本次打脚本的次数
"perTimeQuantity": 5 // 每次打的数量
}
```
### 响应示例
```json
{
"batchId": 456,
"deductPoints": 50,
"expireAt": "2024-01-15T16:30:00",
"codeNos": ["ABC12345", "DEF67890", ...]
}
```
## 权限控制
### 代理用户
- 只能为自己生成链接
- 必须检查积分余额
- 生成链接时扣除相应积分
### 管理员用户
- 只能为自己生成链接
- 跳过积分检查
- 不扣除积分
## 安全特性
1. **JWT令牌验证**每个请求都会验证JWT令牌的有效性
2. **自动过期**JWT令牌有自动过期时间默认30分钟
3. **用户身份验证**从JWT中自动提取用户ID、用户类型等信息
4. **权限控制**:基于用户类型进行不同的业务逻辑处理
## 错误处理
### 常见错误
1. **缺少认证令牌**
```json
{
"error": "用户未认证"
}
```
2. **令牌无效或过期**
```json
{
"error": "JWT token validation failed"
}
```
3. **权限不足**
```json
{
"error": "非法操作者类型"
}
```
## 技术实现
### 核心组件
1. **JwtService**JWT令牌的生成和解析
2. **JwtAuthenticationFilter**自动处理JWT认证的Web过滤器
3. **SecurityConfig**Spring Security配置定义哪些接口需要认证
4. **LinkController**使用Spring Security上下文获取用户信息
### 认证流程
```
请求 → JwtAuthenticationFilter → JWT解析 → 创建认证对象 → Spring Security上下文 → 控制器获取用户信息
```
## 迁移说明
### 从旧版本迁移
如果您之前使用的是手动传递操作者信息的头部参数:
**旧方式(已废弃):**
```http
X-Operator-Id: 123
X-Operator-Type: AGENT
```
**新方式:**
```http
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
```
### 兼容性
- 旧的头参数方式已被完全移除
- 所有链接相关接口现在都需要JWT认证
- 确保在调用接口前先获取有效的JWT令牌
## 最佳实践
1. **令牌管理**客户端应妥善保存JWT令牌并在过期前刷新
2. **安全传输**始终使用HTTPS传输JWT令牌
3. **错误处理**:客户端应处理认证失败的情况,引导用户重新登录
4. **日志记录**:系统会自动记录所有认证相关的操作日志

View File

@@ -1,92 +0,0 @@
package com.gameplatform.server.controller;
import com.gameplatform.server.model.dto.account.AccountCreateRequest;
import com.gameplatform.server.model.dto.account.AccountResponse;
import com.gameplatform.server.model.dto.account.AccountUpdateRequest;
import com.gameplatform.server.model.dto.common.PageResult;
import com.gameplatform.server.service.account.AccountService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Mono;
/**
* 用户接口控制器 - 基于UserAccount实体
* 提供用户账户的基本CRUD操作
*/
@RestController
@RequestMapping("/api/users")
@Tag(name = "用户账户管理", description = "用户账户的增删改查操作")
public class UserController {
private final AccountService accountService;
public UserController(AccountService accountService) {
this.accountService = accountService;
}
/**
* 根据ID获取用户账户信息
*/
@GetMapping("/{id}")
@Operation(summary = "获取用户详情", description = "根据用户ID获取用户详细信息")
public Mono<AccountResponse> getById(@Parameter(description = "用户ID") @PathVariable Long id) {
return accountService.get(id);
}
/**
* 分页查询用户列表
*/
@GetMapping
@Operation(summary = "获取用户列表", description = "分页获取用户列表,支持按用户类型、状态、关键词筛选")
public Mono<PageResult<AccountResponse>> list(
@Parameter(description = "用户类型ADMIN 或 AGENT") @RequestParam(value = "userType", required = false) String userType,
@Parameter(description = "账户状态ENABLED 或 DISABLED") @RequestParam(value = "status", required = false) String status,
@Parameter(description = "搜索关键词") @RequestParam(value = "keyword", required = false) String keyword,
@Parameter(description = "页码默认1") @RequestParam(value = "page", defaultValue = "1") Integer page,
@Parameter(description = "每页大小默认20") @RequestParam(value = "size", defaultValue = "20") Integer size
) {
return accountService.list(userType, status, keyword, page, size);
}
/**
* 创建新用户账户
*/
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
@Operation(summary = "创建用户", description = "创建新的代理用户账户")
public Mono<AccountResponse> create(@Valid @RequestBody AccountCreateRequest request) {
return accountService.create(request);
}
/**
* 更新用户账户信息
*/
@PutMapping("/{id}")
@Operation(summary = "更新用户", description = "更新用户账户信息")
public Mono<AccountResponse> update(@Parameter(description = "用户ID") @PathVariable Long id, @Valid @RequestBody AccountUpdateRequest request) {
return accountService.update(id, request);
}
/**
* 启用用户账户
*/
@PostMapping("/{id}/enable")
@ResponseStatus(HttpStatus.NO_CONTENT)
@Operation(summary = "启用用户", description = "启用指定用户账户")
public Mono<Void> enable(@Parameter(description = "用户ID") @PathVariable Long id) {
return accountService.setStatus(id, "ENABLED").then();
}
/**
* 禁用用户账户
*/
@PostMapping("/{id}/disable")
@ResponseStatus(HttpStatus.NO_CONTENT)
@Operation(summary = "禁用用户", description = "禁用指定用户账户")
public Mono<Void> disable(@Parameter(description = "用户ID") @PathVariable Long id) {
return accountService.setStatus(id, "DISABLED").then();
}
}

View File

@@ -3,18 +3,26 @@ package com.gameplatform.server.controller.link;
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.service.link.LinkGenerationService; import com.gameplatform.server.service.link.LinkGenerationService;
import io.jsonwebtoken.Claims;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import org.slf4j.Logger;
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.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
@RequestMapping("/api/link") @RequestMapping("/api/link")
@Tag(name = "链接管理", description = "生成链接与二维码代理") @Tag(name = "链接管理", description = "链接生成和管理相关接口")
public class LinkController { public class LinkController {
private static final Logger log = LoggerFactory.getLogger(LinkController.class);
private final LinkGenerationService linkGenerationService; private final LinkGenerationService linkGenerationService;
public LinkController(LinkGenerationService linkGenerationService) { public LinkController(LinkGenerationService linkGenerationService) {
@@ -22,27 +30,87 @@ public class LinkController {
} }
@PostMapping("/generate") @PostMapping("/generate")
@ResponseStatus(HttpStatus.CREATED) @ResponseStatus(HttpStatus.OK)
@Operation(summary = "生成链接批次", description = "times * perTimeQuantity = 目标点数;代理需扣点,管理员不扣") @Operation(summary = "生成链接批次", description = "生成指定数量的链接批次。所有用户(管理员和代理商)都只能为自己生成链接,代理用户生成链接时会扣除积分,管理员生成链接时不扣除积分")
public Mono<LinkGenerateResponse> generate(@Valid @RequestBody LinkGenerateRequest req, public Mono<LinkGenerateResponse> generateLinks(@Valid @RequestBody LinkGenerateRequest request,
@RequestHeader(value = "X-Operator-Id", required = false) Long operatorId, Authentication authentication) {
@RequestHeader(value = "X-Operator-Type", required = false) String operatorType) { log.info("=== 开始处理链接生成请求 ===");
// 暂时用两个 Header 传操作者信息;后续接入 JWT 解析 log.info("请求参数: times={}, perTimeQuantity={}",
Long targetAccountId = req.getAgentAccountId() != null ? req.getAgentAccountId() : operatorId; request.getTimes(), request.getPerTimeQuantity());
return linkGenerationService.generateLinks(operatorId, safeUpper(operatorType), targetAccountId,
defaultInt(req.getTimes(), 0), defaultInt(req.getPerTimeQuantity(), 0)) log.info("=== 开始检查认证信息 ===");
.map(r -> { log.info("Authentication: {}", authentication);
LinkGenerateResponse resp = new LinkGenerateResponse();
resp.setBatchId(r.getBatchId()); if (authentication == null) {
resp.setDeductPoints(r.getNeedPoints()); log.error("=== 认证失败Authentication为空 ===");
resp.setExpireAt(r.getExpireAt()); return Mono.error(new IllegalArgumentException("用户未认证Authentication为空"));
resp.setCodeNos(r.getCodeNos()); }
return resp;
log.info("Authentication获取成功: {}", authentication);
log.info("Authentication是否已认证: {}", authentication.isAuthenticated());
log.info("Authentication的principal: {}", authentication.getPrincipal());
log.info("Authentication的authorities: {}", authentication.getAuthorities());
log.info("Authentication的details: {}", authentication.getDetails());
if (!authentication.isAuthenticated()) {
log.error("=== 认证失败Authentication未通过验证 ===");
log.error("Authentication详情: {}", authentication);
return Mono.error(new IllegalArgumentException("用户未认证Authentication未通过验证"));
}
log.info("=== 认证验证通过 ===");
// 从认证对象中获取用户信息
log.info("开始解析Claims信息");
Claims claims = (Claims) authentication.getDetails();
if (claims == null) {
log.error("=== 认证失败Claims为空 ===");
log.error("Authentication details: {}", authentication.getDetails());
return Mono.error(new IllegalArgumentException("用户未认证Claims为空"));
}
log.info("Claims获取成功开始解析用户信息");
log.info("Claims内容: {}", claims);
log.info("Claims的subject: {}", claims.getSubject());
log.info("Claims的issuedAt: {}", claims.getIssuedAt());
log.info("Claims的expiration: {}", claims.getExpiration());
Long operatorId = claims.get("userId", Long.class);
String operatorType = claims.get("userType", String.class);
String username = claims.get("username", String.class);
log.info("解析出的用户信息 - operatorId: {}, operatorType: {}, username: {}",
operatorId, operatorType, username);
if (operatorId == null || operatorType == null) {
log.error("=== 认证失败:缺少必要的用户信息 ===");
log.error("operatorId: {}, operatorType: {}, username: {}", operatorId, operatorType, username);
log.error("Claims中所有键: {}", claims.keySet());
return Mono.error(new IllegalArgumentException("用户未认证:缺少必要的用户信息"));
}
log.info("=== 用户认证信息完整,开始处理业务逻辑 ===");
log.info("操作者信息: operatorId={}, operatorType={}, username={}",
operatorId, operatorType, username);
log.info("业务参数: times={}, perTimeQuantity={}",
request.getTimes(), request.getPerTimeQuantity());
return linkGenerationService.generateLinks(operatorId, operatorType,
request.getTimes(), request.getPerTimeQuantity())
.map(result -> {
log.info("链接生成成功batchId: {}, 扣除积分: {}, 过期时间: {}",
result.getBatchId(), result.getNeedPoints(), result.getExpireAt());
LinkGenerateResponse response = new LinkGenerateResponse();
response.setBatchId(result.getBatchId());
response.setDeductPoints(result.getNeedPoints());
response.setExpireAt(result.getExpireAt());
response.setCodeNos(result.getCodeNos());
return response;
})
.doOnError(error -> {
log.error("链接生成失败: {}", error.getMessage(), error);
}); });
} }
private static String safeUpper(String s) { return s == null ? null : s.toUpperCase(); }
private static int defaultInt(Integer v, int d) { return v == null ? d : v; }
} }

View File

@@ -6,6 +6,9 @@ import org.springframework.http.HttpStatus;
import org.springframework.web.bind.support.WebExchangeBindException; import org.springframework.web.bind.support.WebExchangeBindException;
import org.springframework.web.server.ResponseStatusException; import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.server.ServerWebInputException; import org.springframework.web.server.ServerWebInputException;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.AuthenticationException;
import jakarta.validation.ConstraintViolation; import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException; import jakarta.validation.ConstraintViolationException;
import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ExceptionHandler;
@@ -22,21 +25,45 @@ public class GlobalExceptionHandler {
@ExceptionHandler(IllegalArgumentException.class) @ExceptionHandler(IllegalArgumentException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST) @ResponseStatus(HttpStatus.BAD_REQUEST)
public Object handleBadRequest(IllegalArgumentException e) { public Object handleBadRequest(IllegalArgumentException e) {
log.info("400 BadRequest: {}", e.getMessage()); log.warn("400 BadRequest: {} - Stack: {}", e.getMessage(), getStackTrace(e));
return body(HttpStatus.BAD_REQUEST.value(), e.getMessage()); return body(HttpStatus.BAD_REQUEST.value(), e.getMessage());
} }
@ExceptionHandler(IllegalStateException.class) @ExceptionHandler(IllegalStateException.class)
@ResponseStatus(HttpStatus.FORBIDDEN) @ResponseStatus(HttpStatus.FORBIDDEN)
public Object handleForbidden(IllegalStateException e) { public Object handleForbidden(IllegalStateException e) {
log.info("403 Forbidden: {}", e.getMessage()); log.warn("403 Forbidden: {} - Stack: {}", e.getMessage(), getStackTrace(e));
return body(HttpStatus.FORBIDDEN.value(), e.getMessage()); return body(HttpStatus.FORBIDDEN.value(), e.getMessage());
} }
// 认证相关异常处理
@ExceptionHandler(AuthenticationException.class)
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public Object handleAuthenticationException(AuthenticationException e) {
log.warn("401 Authentication Failed: {} - Type: {} - Stack: {}",
e.getMessage(), e.getClass().getSimpleName(), getStackTrace(e));
return body(HttpStatus.UNAUTHORIZED.value(), "认证失败: " + e.getMessage());
}
@ExceptionHandler(BadCredentialsException.class)
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public Object handleBadCredentials(BadCredentialsException e) {
log.warn("401 Bad Credentials: {} - Stack: {}", e.getMessage(), getStackTrace(e));
return body(HttpStatus.UNAUTHORIZED.value(), "用户名或密码错误");
}
@ExceptionHandler(AccessDeniedException.class)
@ResponseStatus(HttpStatus.FORBIDDEN)
public Object handleAccessDenied(AccessDeniedException e) {
log.warn("403 Access Denied: {} - Stack: {}", e.getMessage(), getStackTrace(e));
return body(HttpStatus.FORBIDDEN.value(), "访问被拒绝: " + e.getMessage());
}
@ExceptionHandler(WebExchangeBindException.class) @ExceptionHandler(WebExchangeBindException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST) @ResponseStatus(HttpStatus.BAD_REQUEST)
public Object handleBindException(WebExchangeBindException e) { public Object handleBindException(WebExchangeBindException e) {
log.info("400 ValidationError: {}", e.getMessage()); log.warn("400 ValidationError: {} - Field errors: {} - Global errors: {}",
e.getMessage(), e.getFieldErrors().size(), e.getGlobalErrors().size());
var details = new java.util.LinkedHashMap<String, Object>(); var details = new java.util.LinkedHashMap<String, Object>();
e.getFieldErrors().forEach(fe -> details.put(fe.getField(), fe.getDefaultMessage())); e.getFieldErrors().forEach(fe -> details.put(fe.getField(), fe.getDefaultMessage()));
e.getGlobalErrors().forEach(ge -> details.put(ge.getObjectName(), ge.getDefaultMessage())); e.getGlobalErrors().forEach(ge -> details.put(ge.getObjectName(), ge.getDefaultMessage()));
@@ -46,7 +73,8 @@ public class GlobalExceptionHandler {
@ExceptionHandler(ConstraintViolationException.class) @ExceptionHandler(ConstraintViolationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST) @ResponseStatus(HttpStatus.BAD_REQUEST)
public Object handleConstraintViolation(ConstraintViolationException e) { public Object handleConstraintViolation(ConstraintViolationException e) {
log.info("400 ConstraintViolation: {}", e.getMessage()); log.warn("400 ConstraintViolation: {} - Violations: {}",
e.getMessage(), e.getConstraintViolations().size());
var details = new java.util.LinkedHashMap<String, Object>(); var details = new java.util.LinkedHashMap<String, Object>();
for (ConstraintViolation<?> v : e.getConstraintViolations()) { for (ConstraintViolation<?> v : e.getConstraintViolations()) {
details.put(String.valueOf(v.getPropertyPath()), v.getMessage()); details.put(String.valueOf(v.getPropertyPath()), v.getMessage());
@@ -57,21 +85,24 @@ public class GlobalExceptionHandler {
@ExceptionHandler(ServerWebInputException.class) @ExceptionHandler(ServerWebInputException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST) @ResponseStatus(HttpStatus.BAD_REQUEST)
public Object handleInput(ServerWebInputException e) { public Object handleInput(ServerWebInputException e) {
log.info("400 InputError: {}", e.getMessage()); log.warn("400 InputError: {} - Reason: {} - Stack: {}",
e.getMessage(), e.getReason(), getStackTrace(e));
return body(HttpStatus.BAD_REQUEST.value(), "请求解析失败: " + e.getReason()); return body(HttpStatus.BAD_REQUEST.value(), "请求解析失败: " + e.getReason());
} }
@ExceptionHandler(ResponseStatusException.class) @ExceptionHandler(ResponseStatusException.class)
public Object handleRse(ResponseStatusException e) { public Object handleRse(ResponseStatusException e) {
var status = e.getStatusCode(); var status = e.getStatusCode();
log.info("{} ResponseStatusException: {}", status, e.getReason()); log.warn("{} ResponseStatusException: {} - Stack: {}",
status, e.getReason(), getStackTrace(e));
return body(status.value(), e.getReason()); return body(status.value(), e.getReason());
} }
@ExceptionHandler(Exception.class) @ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Object handleOther(Exception e) { public Object handleOther(Exception e) {
log.error("500 InternalServerError", e); log.error("500 InternalServerError: {} - Type: {} - Stack: {}",
e.getMessage(), e.getClass().getSimpleName(), getStackTrace(e), e);
return body(HttpStatus.INTERNAL_SERVER_ERROR.value(), "服务器内部错误"); return body(HttpStatus.INTERNAL_SERVER_ERROR.value(), "服务器内部错误");
} }
@@ -91,4 +122,12 @@ public class GlobalExceptionHandler {
put("timestamp", Instant.now().toString()); put("timestamp", Instant.now().toString());
}}; }};
} }
private String getStackTrace(Exception e) {
StackTraceElement[] stackTrace = e.getStackTrace();
if (stackTrace.length > 0) {
return stackTrace[0].toString();
}
return "No stack trace available";
}
} }

View File

@@ -7,15 +7,11 @@ public class LinkGenerateRequest {
private Integer times; private Integer times;
@Schema(description = "每次打的数量", example = "5") @Schema(description = "每次打的数量", example = "5")
private Integer perTimeQuantity; private Integer perTimeQuantity;
@Schema(description = "为哪个代理账户生成(管理员可指定,代理可省略或为自己)")
private Long agentAccountId;
public Integer getTimes() { return times; } public Integer getTimes() { return times; }
public void setTimes(Integer times) { this.times = times; } public void setTimes(Integer times) { this.times = times; }
public Integer getPerTimeQuantity() { return perTimeQuantity; } public Integer getPerTimeQuantity() { return perTimeQuantity; }
public void setPerTimeQuantity(Integer perTimeQuantity) { this.perTimeQuantity = perTimeQuantity; } public void setPerTimeQuantity(Integer perTimeQuantity) { this.perTimeQuantity = perTimeQuantity; }
public Long getAgentAccountId() { return agentAccountId; }
public void setAgentAccountId(Long agentAccountId) { this.agentAccountId = agentAccountId; }
} }

View File

@@ -0,0 +1,123 @@
package com.gameplatform.server.security;
import io.jsonwebtoken.Claims;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextImpl;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
import java.util.Collections;
@Component
public class JwtAuthenticationFilter implements WebFilter {
private static final Logger log = LoggerFactory.getLogger(JwtAuthenticationFilter.class);
private final JwtService jwtService;
public JwtAuthenticationFilter(JwtService jwtService) {
this.jwtService = jwtService;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
String path = exchange.getRequest().getPath().value();
String method = exchange.getRequest().getMethod().name();
log.info("=== JWT过滤器开始处理请求 ===");
log.info("请求路径: {}, 请求方法: {}", path, method);
String authHeader = exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
log.info("Authorization头: {}", authHeader != null ? authHeader.substring(0, Math.min(20, authHeader.length())) + "..." : "null");
if (authHeader == null) {
log.info("未找到Authorization头跳过JWT认证继续处理请求");
return chain.filter(exchange);
}
if (!authHeader.startsWith("Bearer ")) {
log.warn("Authorization头格式无效期望格式: 'Bearer <token>',实际: {}", authHeader);
log.info("跳过JWT认证继续处理请求");
return chain.filter(exchange);
}
String token = authHeader.substring(7);
log.info("开始处理JWT tokentoken长度: {}", token.length());
log.debug("JWT token内容: {}", token);
try {
log.info("开始解析JWT token");
Claims claims = jwtService.parse(token);
log.info("JWT token解析成功");
Long userId = claims.get("userId", Long.class);
String userType = claims.get("userType", String.class);
String username = claims.get("username", String.class);
String subject = claims.getSubject();
log.info("JWT claims解析结果:");
log.info(" - subject: {}", subject);
log.info(" - userId: {}", userId);
log.info(" - userType: {}", userType);
log.info(" - username: {}", username);
log.info(" - issuedAt: {}", claims.getIssuedAt());
log.info(" - expiration: {}", claims.getExpiration());
log.info(" - 所有claims键: {}", claims.keySet());
if (userId == null || userType == null || username == null) {
log.warn("JWT token中缺少必要的claims信息");
log.warn(" - userId: {}", userId);
log.warn(" - userType: {}", userType);
log.warn(" - username: {}", username);
log.info("跳过JWT认证继续处理请求");
return chain.filter(exchange);
}
// 创建Spring Security的Authentication对象
log.info("开始创建Authentication对象");
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
username, // principal
null, // credentials (不需要密码)
Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + userType.toUpperCase()))
);
// 设置认证详情包含JWT claims信息
authentication.setDetails(claims);
log.info("Authentication对象创建成功: {}", authentication);
log.info("Authentication是否已认证: {}", authentication.isAuthenticated());
log.info("Authentication的principal: {}", authentication.getPrincipal());
log.info("Authentication的authorities: {}", authentication.getAuthorities());
log.info("Authentication的details: {}", authentication.getDetails());
// 创建安全上下文并设置认证信息
SecurityContext securityContext = new SecurityContextImpl(authentication);
log.info("=== JWT token验证成功设置安全上下文 ===");
log.info("用户: {} (ID: {}, 类型: {}) 在路径: {} 上JWT验证通过",
username, userId, userType, path);
// 将安全上下文设置到ReactiveSecurityContextHolder中
return chain.filter(exchange)
.contextWrite(ReactiveSecurityContextHolder.withSecurityContext(Mono.just(securityContext)));
} catch (Exception e) {
log.error("=== JWT token验证失败 ===");
log.error("请求路径: {}", path);
log.error("Authorization头: {}", authHeader);
log.error("错误详情: {}", e.getMessage(), e);
log.info("JWT认证失败继续处理请求未认证状态");
}
log.info("JWT过滤器处理完成继续处理请求");
return chain.filter(exchange);
}
}

View File

@@ -27,9 +27,14 @@ public class JwtService {
byte[] bytes = secret.length() < 32 ? (secret + "_pad_to_32_chars_secret_key_value").getBytes() : secret.getBytes(); byte[] bytes = secret.length() < 32 ? (secret + "_pad_to_32_chars_secret_key_value").getBytes() : secret.getBytes();
this.key = Keys.hmacShaKeyFor(bytes); this.key = Keys.hmacShaKeyFor(bytes);
this.accessTokenMinutes = accessTokenMinutes; this.accessTokenMinutes = accessTokenMinutes;
log.info("JWT服务初始化完成 - 密钥长度: {} bytes, token过期时间: {} 分钟", bytes.length, accessTokenMinutes);
} }
public String generateToken(String subject, String userType, Long userId, String username, Map<String, Object> extra) { public String generateToken(String subject, String userType, Long userId, String username, Map<String, Object> extra) {
log.info("=== 开始生成JWT token ===");
log.info("生成参数: subject={}, userType={}, userId={}, username={}, extra={}",
subject, userType, userId, username, extra);
Instant now = Instant.now(); Instant now = Instant.now();
var builder = Jwts.builder() var builder = Jwts.builder()
.setSubject(subject) .setSubject(subject)
@@ -41,14 +46,44 @@ public class JwtService {
if (extra != null) { if (extra != null) {
extra.forEach(builder::claim); extra.forEach(builder::claim);
} }
log.info("JWT builder配置完成开始签名");
String token = builder.signWith(key, SignatureAlgorithm.HS256).compact(); String token = builder.signWith(key, SignatureAlgorithm.HS256).compact();
if (log.isDebugEnabled()) {
log.debug("JWT generated subject={}, userType={}, userId={}, username={} expInMin={}", subject, userType, userId, username, accessTokenMinutes); log.info("=== JWT token生成成功 ===");
} log.info("token长度: {} 字符", token.length());
log.info("过期时间: {} 分钟后", accessTokenMinutes);
log.info("完整token: {}", token);
return token; return token;
} }
public io.jsonwebtoken.Claims parse(String token) { public io.jsonwebtoken.Claims parse(String token) {
return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody(); log.info("=== 开始解析JWT token ===");
log.info("token长度: {} 字符", token.length());
log.debug("完整token: {}", token);
try {
log.info("开始验证JWT签名");
io.jsonwebtoken.Claims claims = Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
log.info("=== JWT token解析成功 ===");
log.info("解析结果:");
log.info(" - subject: {}", claims.getSubject());
log.info(" - issuedAt: {}", claims.getIssuedAt());
log.info(" - expiration: {}", claims.getExpiration());
log.info(" - 所有claims: {}", claims);
return claims;
} catch (Exception e) {
log.error("=== JWT token解析失败 ===");
log.error("token: {}", token);
log.error("错误详情: {}", e.getMessage(), e);
throw e;
}
} }
} }

View File

@@ -1,37 +1,107 @@
package com.gameplatform.server.security; package com.gameplatform.server.security;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.server.SecurityWebFilterChain; import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.context.ServerSecurityContextRepository;
import org.springframework.security.web.server.context.WebSessionServerSecurityContextRepository;
import reactor.core.publisher.Mono;
@Configuration @Configuration
@EnableWebFluxSecurity @EnableWebFluxSecurity
public class SecurityConfig { public class SecurityConfig {
private static final Logger log = LoggerFactory.getLogger(SecurityConfig.class);
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean @Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
return http log.info("=== 开始配置Spring Security安全链 ===");
SecurityWebFilterChain chain = http
.csrf(ServerHttpSecurity.CsrfSpec::disable) .csrf(ServerHttpSecurity.CsrfSpec::disable)
.cors(cors -> {}) .cors(cors -> {})
.httpBasic(ServerHttpSecurity.HttpBasicSpec::disable) .httpBasic(ServerHttpSecurity.HttpBasicSpec::disable)
.formLogin(ServerHttpSecurity.FormLoginSpec::disable) .formLogin(ServerHttpSecurity.FormLoginSpec::disable)
.securityContextRepository(securityContextRepository())
.authenticationManager(authenticationManager())
.authorizeExchange(ex -> ex .authorizeExchange(ex -> ex
.pathMatchers("/actuator/**").permitAll() .pathMatchers("/actuator/**").permitAll()
.pathMatchers(HttpMethod.POST, "/api/auth/login").permitAll() .pathMatchers(HttpMethod.POST, "/api/auth/login").permitAll()
.pathMatchers(HttpMethod.GET, "/api/auth/me").permitAll() .pathMatchers(HttpMethod.GET, "/api/auth/me").permitAll()
.pathMatchers("/api/link/**").authenticated() // 链接接口需要认证
.anyExchange().permitAll() // 其他接口后续再收紧 .anyExchange().permitAll() // 其他接口后续再收紧
) )
// 关键将JWT过滤器集成到Security过滤链中放在AUTHENTICATION位置
.addFilterAt(jwtAuthenticationFilter, SecurityWebFiltersOrder.AUTHENTICATION)
.build(); .build();
log.info("=== Spring Security安全链配置完成 ===");
log.info("安全配置详情:");
log.info(" - CSRF: 已禁用");
log.info(" - CORS: 已启用");
log.info(" - HTTP Basic: 已禁用");
log.info(" - Form Login: 已禁用");
log.info(" - JWT过滤器: 已集成到Security链中 (AUTHENTICATION位置)");
log.info(" - 路径权限配置:");
log.info(" * /actuator/** -> 允许所有");
log.info(" * POST /api/auth/login -> 允许所有");
log.info(" * GET /api/auth/me -> 允许所有");
log.info(" * /api/link/** -> 需要认证");
log.info(" * 其他路径 -> 允许所有");
return chain;
}
@Bean
public ReactiveAuthenticationManager authenticationManager() {
log.info("创建JWT认证管理器");
return authentication -> {
log.info("=== JWT认证管理器开始处理认证 ===");
log.info("认证对象: {}", authentication);
if (authentication instanceof UsernamePasswordAuthenticationToken) {
UsernamePasswordAuthenticationToken auth = (UsernamePasswordAuthenticationToken) authentication;
log.info("处理UsernamePasswordAuthenticationToken认证");
log.info("Principal: {}", auth.getPrincipal());
log.info("Credentials: {}", auth.getCredentials());
log.info("Authorities: {}", auth.getAuthorities());
log.info("Details: {}", auth.getDetails());
log.info("是否已认证: {}", auth.isAuthenticated());
// 如果已经通过JWT过滤器认证直接返回
if (auth.isAuthenticated()) {
log.info("认证对象已经通过验证,返回成功");
return Mono.just(auth);
}
}
log.info("认证对象未通过验证,返回失败");
return Mono.empty();
};
}
@Bean
public ServerSecurityContextRepository securityContextRepository() {
log.info("创建安全上下文仓库");
return new WebSessionServerSecurityContextRepository();
} }
@Bean @Bean
public PasswordEncoder passwordEncoder() { public PasswordEncoder passwordEncoder() {
log.info("创建BCrypt密码编码器");
return new BCryptPasswordEncoder(); return new BCryptPasswordEncoder();
} }
} }

View File

@@ -46,46 +46,43 @@ public class LinkGenerationService {
@Transactional @Transactional
public Mono<GenerateResult> generateLinks(Long operatorId, String operatorType, public Mono<GenerateResult> generateLinks(Long operatorId, String operatorType,
Long targetAccountId,
int times, int perTimeQuantity) { int times, int perTimeQuantity) {
return Mono.fromCallable(() -> doGenerate(operatorId, operatorType, targetAccountId, times, perTimeQuantity)) return Mono.fromCallable(() -> doGenerate(operatorId, operatorType, times, perTimeQuantity))
.subscribeOn(Schedulers.boundedElastic()); .subscribeOn(Schedulers.boundedElastic());
} }
private GenerateResult doGenerate(Long operatorId, String operatorType, private GenerateResult doGenerate(Long operatorId, String operatorType,
Long targetAccountId,
int times, int perTimeQuantity) { int times, int perTimeQuantity) {
if (times <= 0 || perTimeQuantity <= 0) { if (times <= 0 || perTimeQuantity <= 0) {
throw new IllegalArgumentException("times 与 perTimeQuantity 必须为正整数"); throw new IllegalArgumentException("times 与 perTimeQuantity 必须为正整数");
} }
UserAccount target = userAccountMapper.findById(targetAccountId); // 获取操作者账户信息
if (target == null) { UserAccount operator = userAccountMapper.findById(operatorId);
throw new IllegalArgumentException("目标账户不存在"); if (operator == null) {
throw new IllegalArgumentException("操作者账户不存在");
} }
boolean isAdminOperator = "ADMIN".equalsIgnoreCase(operatorType); boolean isAdminOperator = "ADMIN".equalsIgnoreCase(operatorType);
if (!isAdminOperator && !"AGENT".equalsIgnoreCase(operatorType)) { if (!isAdminOperator && !"AGENT".equalsIgnoreCase(operatorType)) {
throw new IllegalArgumentException("非法操作者类型"); throw new IllegalArgumentException("非法操作者类型");
} }
if (!"AGENT".equalsIgnoreCase(target.getUserType())) {
throw new IllegalArgumentException("仅支持为代理账户生成链接");
}
long needPoints = (long) times * (long) perTimeQuantity; long needPoints = (long) times * (long) perTimeQuantity;
if (log.isDebugEnabled()) { if (log.isDebugEnabled()) {
log.debug("generateLinks operatorId={} operatorType={} targetAccountId={} times={} perTimeQuantity={} needPoints={} expireHours={}", log.debug("generateLinks operatorId={} operatorType={} times={} perTimeQuantity={} needPoints={} expireHours={}",
operatorId, operatorType, targetAccountId, times, perTimeQuantity, needPoints, expireHours); operatorId, operatorType, times, perTimeQuantity, needPoints, expireHours);
} }
if (!isAdminOperator) { if (!isAdminOperator) {
// 代理商自操作,需扣点判断 // 代理商自操作,需扣点判断
long balance = target.getPointsBalance() == null ? 0L : target.getPointsBalance(); long balance = operator.getPointsBalance() == null ? 0L : operator.getPointsBalance();
if (balance < needPoints) { if (balance < needPoints) {
throw new IllegalStateException("点数不足"); throw new IllegalStateException("点数不足");
} }
} }
LinkBatch batch = new LinkBatch(); LinkBatch batch = new LinkBatch();
batch.setAgentId(target.getId()); batch.setAgentId(operator.getId());
batch.setQuantity(times); batch.setQuantity(times);
batch.setTimes(times); batch.setTimes(times);
batch.setBatchSize(perTimeQuantity); batch.setBatchSize(perTimeQuantity);
@@ -98,7 +95,7 @@ public class LinkGenerationService {
for (int i = 0; i < times; i++) { for (int i = 0; i < times; i++) {
LinkTask t = new LinkTask(); LinkTask t = new LinkTask();
t.setBatchId(batch.getId()); t.setBatchId(batch.getId());
t.setAgentId(target.getId()); t.setAgentId(operator.getId());
t.setCodeNo(generateCodeNo()); t.setCodeNo(generateCodeNo());
t.setTokenHash(DigestUtils.sha256Hex(generateToken())); t.setTokenHash(DigestUtils.sha256Hex(generateToken()));
t.setExpireAt(expireAt); t.setExpireAt(expireAt);
@@ -109,11 +106,11 @@ public class LinkGenerationService {
if (!isAdminOperator) { if (!isAdminOperator) {
// 扣点流水 + 账户余额 // 扣点流水 + 账户余额
long before = target.getPointsBalance() == null ? 0L : target.getPointsBalance(); long before = operator.getPointsBalance() == null ? 0L : operator.getPointsBalance();
long after = before - needPoints; long after = before - needPoints;
AgentPointsTx tx = new AgentPointsTx(); AgentPointsTx tx = new AgentPointsTx();
tx.setAccountId(target.getId()); tx.setAccountId(operator.getId());
tx.setType("DEDUCT"); tx.setType("DEDUCT");
tx.setBeforePoints(before); tx.setBeforePoints(before);
tx.setDeltaPoints(-needPoints); tx.setDeltaPoints(-needPoints);
@@ -124,7 +121,7 @@ public class LinkGenerationService {
agentPointsTxMapper.insert(tx); agentPointsTxMapper.insert(tx);
UserAccount patch = new UserAccount(); UserAccount patch = new UserAccount();
patch.setId(target.getId()); patch.setId(operator.getId());
patch.setPointsBalance(after); patch.setPointsBalance(after);
userAccountMapper.update(patch); userAccountMapper.update(patch);
} }

View File

@@ -8,38 +8,34 @@ com\gameplatform\server\model\dto\common\PageResult.class
com\gameplatform\server\service\UserService.class com\gameplatform\server\service\UserService.class
com\gameplatform\server\controller\auth\AuthController.class com\gameplatform\server\controller\auth\AuthController.class
com\gameplatform\server\GamePlatformServerApplication.class com\gameplatform\server\GamePlatformServerApplication.class
com\gameplatform\server\service\link\LinkGenerationService$GenerateResult.class
com\gameplatform\server\mapper\admin\AnnouncementMapper.class com\gameplatform\server\mapper\admin\AnnouncementMapper.class
com\gameplatform\server\model\dto\link\CreateLinkBatchResponse.class
com\gameplatform\server\service\auth\AuthService.class com\gameplatform\server\service\auth\AuthService.class
com\gameplatform\server\config\SwaggerConfig.class com\gameplatform\server\config\SwaggerConfig.class
com\gameplatform\server\mapper\account\UserAccountMapper.class com\gameplatform\server\mapper\account\UserAccountMapper.class
com\gameplatform\server\service\account\AccountService.class com\gameplatform\server\service\account\AccountService.class
com\gameplatform\server\model\dto\link\CreateLinkBatchRequest.class com\gameplatform\server\security\JwtAuthenticationFilter.class
com\gameplatform\server\model\dto\link\LinkStatisticsResponse.class com\gameplatform\server\service\external\ScriptClient.class
com\gameplatform\server\service\link\QrCodeProxyService$CachedImage.class com\gameplatform\server\model\dto\link\LinkGenerateResponse.class
com\gameplatform\server\service\link\QrCodeProxyService.class
com\gameplatform\server\controller\auth\AuthController$1.class com\gameplatform\server\controller\auth\AuthController$1.class
com\gameplatform\server\model\dto\link\LinkGenerateRequest.class
com\gameplatform\server\model\entity\admin\Announcement.class com\gameplatform\server\model\entity\admin\Announcement.class
com\gameplatform\server\controller\link\LinkTaskController.class
com\gameplatform\server\model\dto\link\LinkTaskResponse.class
com\gameplatform\server\model\entity\agent\AgentPointsTx.class com\gameplatform\server\model\entity\agent\AgentPointsTx.class
com\gameplatform\server\controller\admin\AccountController.class com\gameplatform\server\controller\admin\AccountController.class
com\gameplatform\server\model\dto\auth\LoginRequest.class com\gameplatform\server\model\dto\auth\LoginRequest.class
com\gameplatform\server\model\entity\account\UserAccount.class com\gameplatform\server\model\entity\account\UserAccount.class
com\gameplatform\server\model\dto\auth\LoginResponse.class com\gameplatform\server\model\dto\auth\LoginResponse.class
com\gameplatform\server\model\entity\agent\LinkTask.class com\gameplatform\server\model\entity\agent\LinkTask.class
com\gameplatform\server\controller\link\LinkController.class
com\gameplatform\server\exception\GlobalExceptionHandler$1.class com\gameplatform\server\exception\GlobalExceptionHandler$1.class
com\gameplatform\server\mapper\agent\LinkBatchMapper.class com\gameplatform\server\mapper\agent\LinkBatchMapper.class
com\gameplatform\server\security\JwtService.class com\gameplatform\server\security\JwtService.class
com\gameplatform\server\model\dto\link\LinkTaskQueryRequest.class
com\gameplatform\server\mapper\admin\OperationLogMapper.class com\gameplatform\server\mapper\admin\OperationLogMapper.class
com\gameplatform\server\model\entity\agent\LinkBatch.class com\gameplatform\server\model\entity\agent\LinkBatch.class
com\gameplatform\server\controller\UserController.class
com\gameplatform\server\security\SecurityConfig.class com\gameplatform\server\security\SecurityConfig.class
com\gameplatform\server\service\link\LinkTaskService.class
com\gameplatform\server\model\entity\admin\OperationLog.class com\gameplatform\server\model\entity\admin\OperationLog.class
com\gameplatform\server\model\dto\account\AccountResponse.class com\gameplatform\server\model\dto\account\AccountResponse.class
com\gameplatform\server\model\dto\account\AccountCreateRequest.class com\gameplatform\server\model\dto\account\AccountCreateRequest.class
com\gameplatform\server\config\ScheduleConfig.class com\gameplatform\server\controller\link\QrProxyController.class
com\gameplatform\server\model\dto\account\AccountUpdateRequest.class com\gameplatform\server\model\dto\account\AccountUpdateRequest.class
com\gameplatform\server\model\dto\account\ResetPasswordRequest.class com\gameplatform\server\model\dto\account\ResetPasswordRequest.class

View File

@@ -7,7 +7,6 @@ D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\service\aut
D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\model\dto\auth\LoginRequest.java D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\model\dto\auth\LoginRequest.java
D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\controller\admin\AccountController.java D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\controller\admin\AccountController.java
D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\service\external\ScriptClient.java D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\service\external\ScriptClient.java
D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\controller\UserController.java
D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\mapper\agent\LinkBatchMapper.java D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\mapper\agent\LinkBatchMapper.java
D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\controller\link\QrProxyController.java D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\controller\link\QrProxyController.java
D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\mapper\agent\LinkTaskMapper.java D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\mapper\agent\LinkTaskMapper.java
@@ -25,6 +24,7 @@ D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\model\dto\l
D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\model\entity\agent\LinkTask.java D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\model\entity\agent\LinkTask.java
D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\model\dto\account\AccountUpdateRequest.java D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\model\dto\account\AccountUpdateRequest.java
D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\model\entity\admin\Announcement.java D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\model\entity\admin\Announcement.java
D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\security\JwtAuthenticationFilter.java
D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\model\entity\agent\AgentPointsTx.java D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\model\entity\agent\AgentPointsTx.java
D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\model\dto\auth\LoginResponse.java D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\model\dto\auth\LoginResponse.java
D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\controller\link\LinkController.java D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\controller\link\LinkController.java

View File

@@ -1,2 +0,0 @@
com\gameplatform\server\service\link\LinkGenerationServiceTest.class
com\gameplatform\server\service\link\LinkTaskServiceTest.class

View File

@@ -1,2 +0,0 @@
D:\project\gamePlatform\server\src\test\java\com\gameplatform\server\service\link\LinkTaskServiceTest.java
D:\project\gamePlatform\server\src\test\java\com\gameplatform\server\service\link\LinkGenerationServiceTest.java