添加Apache Commons Codec依赖以支持SHA-256和十六进制工具,并在application.yml中新增外部脚本配置及链接过期时间设置。同时,删除不再使用的类文件以清理项目结构。

This commit is contained in:
zyh
2025-08-24 20:46:35 +08:00
parent c3762f985e
commit 3f01d8590a
53 changed files with 428 additions and 10 deletions

View File

@@ -92,6 +92,13 @@
<version>2.3.0</version>
</dependency>
<!-- Apache Commons Codec for SHA-256 and hex utils -->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.16.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>

View File

@@ -0,0 +1,48 @@
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.service.link.LinkGenerationService;
import io.swagger.v3.oas.annotations.Operation;
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;
@RestController
@RequestMapping("/api/link")
@Tag(name = "链接管理", description = "生成链接与二维码代理")
public class LinkController {
private final LinkGenerationService linkGenerationService;
public LinkController(LinkGenerationService linkGenerationService) {
this.linkGenerationService = linkGenerationService;
}
@PostMapping("/generate")
@ResponseStatus(HttpStatus.CREATED)
@Operation(summary = "生成链接批次", description = "times * perTimeQuantity = 目标点数;代理需扣点,管理员不扣")
public Mono<LinkGenerateResponse> generate(@Valid @RequestBody LinkGenerateRequest req,
@RequestHeader(value = "X-Operator-Id", required = false) Long operatorId,
@RequestHeader(value = "X-Operator-Type", required = false) String operatorType) {
// 暂时用两个 Header 传操作者信息;后续接入 JWT 解析
Long targetAccountId = req.getAgentAccountId() != null ? req.getAgentAccountId() : operatorId;
return linkGenerationService.generateLinks(operatorId, safeUpper(operatorType), targetAccountId,
defaultInt(req.getTimes(), 0), defaultInt(req.getPerTimeQuantity(), 0))
.map(r -> {
LinkGenerateResponse resp = new LinkGenerateResponse();
resp.setBatchId(r.getBatchId());
resp.setDeductPoints(r.getNeedPoints());
resp.setExpireAt(r.getExpireAt());
resp.setCodeNos(r.getCodeNos());
return resp;
});
}
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

@@ -0,0 +1,41 @@
package com.gameplatform.server.controller.link;
import com.gameplatform.server.service.external.ScriptClient;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.CacheControl;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
import java.util.concurrent.TimeUnit;
@RestController
@RequestMapping("/api/link")
@Tag(name = "二维码代理", description = "转发脚本端的二维码图片,避免混合内容")
public class QrProxyController {
private final ScriptClient scriptClient;
public QrProxyController(ScriptClient scriptClient) {
this.scriptClient = scriptClient;
}
@GetMapping(value = "/{codeNo}/qr.png", produces = MediaType.IMAGE_PNG_VALUE)
@Operation(summary = "二维码图片代理")
public Mono<ResponseEntity<byte[]>> qr(@PathVariable("codeNo") String codeNo) {
String path = "/" + codeNo + "/二维码.png";
return scriptClient.getQrPng(path)
.map(bytes -> ResponseEntity.ok()
.contentType(MediaType.IMAGE_PNG)
.cacheControl(CacheControl.maxAge(30, TimeUnit.SECONDS).cachePublic())
.header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=qr.png")
.body(bytes));
}
}

View File

@@ -0,0 +1,21 @@
package com.gameplatform.server.model.dto.link;
import io.swagger.v3.oas.annotations.media.Schema;
public class LinkGenerateRequest {
@Schema(description = "本次打脚本的次数", example = "10")
private Integer times;
@Schema(description = "每次打的数量", example = "5")
private Integer perTimeQuantity;
@Schema(description = "为哪个代理账户生成(管理员可指定,代理可省略或为自己)")
private Long agentAccountId;
public Integer getTimes() { return times; }
public void setTimes(Integer times) { this.times = times; }
public Integer getPerTimeQuantity() { return 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,28 @@
package com.gameplatform.server.model.dto.link;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime;
import java.util.List;
public class LinkGenerateResponse {
@Schema(description = "批次ID")
private Long batchId;
@Schema(description = "扣点管理员为0")
private Long deductPoints;
@Schema(description = "过期时间")
private LocalDateTime expireAt;
@Schema(description = "生成的codeNo集合")
private List<String> codeNos;
public Long getBatchId() { return batchId; }
public void setBatchId(Long batchId) { this.batchId = batchId; }
public Long getDeductPoints() { return deductPoints; }
public void setDeductPoints(Long deductPoints) { this.deductPoints = deductPoints; }
public LocalDateTime getExpireAt() { return expireAt; }
public void setExpireAt(LocalDateTime expireAt) { this.expireAt = expireAt; }
public List<String> getCodeNos() { return codeNos; }
public void setCodeNos(List<String> codeNos) { this.codeNos = codeNos; }
}

View File

@@ -0,0 +1,60 @@
package com.gameplatform.server.service.external;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.ExchangeStrategies;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import java.time.Duration;
@Service
public class ScriptClient {
private static final Logger log = LoggerFactory.getLogger(ScriptClient.class);
private final WebClient webClient;
private final String baseUrl;
public ScriptClient(
@Value("${script.base-url}") String baseUrl,
@Value("${script.connect-timeout-ms:3000}") int connectTimeoutMs,
@Value("${script.read-timeout-ms:5000}") int readTimeoutMs
) {
this.baseUrl = baseUrl;
this.webClient = WebClient.builder()
.baseUrl(baseUrl)
.exchangeStrategies(ExchangeStrategies.builder()
.codecs(cfg -> cfg.defaultCodecs().maxInMemorySize(2 * 1024 * 1024))
.build())
.build();
if (log.isDebugEnabled()) {
log.debug("ScriptClient initialized baseUrl={}, connectTimeoutMs={}, readTimeoutMs={}", this.baseUrl, connectTimeoutMs, readTimeoutMs);
}
}
public Mono<byte[]> getQrPng(String path) {
// path example: /{codeNo}/二维码.png
return webClient.get()
.uri(path)
.accept(MediaType.IMAGE_PNG)
.retrieve()
.bodyToMono(byte[].class)
.timeout(Duration.ofSeconds(5))
.doOnError(e -> log.warn("ScriptClient.getQrPng error path={} err={}", path, e.toString()));
}
public Mono<String> getText(String path) {
return webClient.get()
.uri(path)
.accept(MediaType.TEXT_PLAIN)
.retrieve()
.bodyToMono(String.class)
.timeout(Duration.ofSeconds(5))
.doOnError(e -> log.warn("ScriptClient.getText error path={} err={}", path, e.toString()));
}
}

View File

@@ -0,0 +1,174 @@
package com.gameplatform.server.service.link;
import com.gameplatform.server.mapper.account.UserAccountMapper;
import com.gameplatform.server.mapper.agent.AgentPointsTxMapper;
import com.gameplatform.server.mapper.agent.LinkBatchMapper;
import com.gameplatform.server.mapper.agent.LinkTaskMapper;
import com.gameplatform.server.model.entity.account.UserAccount;
import com.gameplatform.server.model.entity.agent.AgentPointsTx;
import com.gameplatform.server.model.entity.agent.LinkBatch;
import com.gameplatform.server.model.entity.agent.LinkTask;
import org.apache.commons.codec.digest.DigestUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import java.security.SecureRandom;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Service
public class LinkGenerationService {
private static final Logger log = LoggerFactory.getLogger(LinkGenerationService.class);
private final UserAccountMapper userAccountMapper;
private final LinkBatchMapper linkBatchMapper;
private final LinkTaskMapper linkTaskMapper;
private final AgentPointsTxMapper agentPointsTxMapper;
private final int expireHours;
public LinkGenerationService(UserAccountMapper userAccountMapper,
LinkBatchMapper linkBatchMapper,
LinkTaskMapper linkTaskMapper,
AgentPointsTxMapper agentPointsTxMapper,
@Value("${link.expire-hours:2}") int expireHours) {
this.userAccountMapper = userAccountMapper;
this.linkBatchMapper = linkBatchMapper;
this.linkTaskMapper = linkTaskMapper;
this.agentPointsTxMapper = agentPointsTxMapper;
this.expireHours = expireHours;
}
@Transactional
public Mono<GenerateResult> generateLinks(Long operatorId, String operatorType,
Long targetAccountId,
int times, int perTimeQuantity) {
return Mono.fromCallable(() -> doGenerate(operatorId, operatorType, targetAccountId, times, perTimeQuantity))
.subscribeOn(Schedulers.boundedElastic());
}
private GenerateResult doGenerate(Long operatorId, String operatorType,
Long targetAccountId,
int times, int perTimeQuantity) {
if (times <= 0 || perTimeQuantity <= 0) {
throw new IllegalArgumentException("times 与 perTimeQuantity 必须为正整数");
}
UserAccount target = userAccountMapper.findById(targetAccountId);
if (target == null) {
throw new IllegalArgumentException("目标账户不存在");
}
boolean isAdminOperator = "ADMIN".equalsIgnoreCase(operatorType);
if (!isAdminOperator && !"AGENT".equalsIgnoreCase(operatorType)) {
throw new IllegalArgumentException("非法操作者类型");
}
if (!"AGENT".equalsIgnoreCase(target.getUserType())) {
throw new IllegalArgumentException("仅支持为代理账户生成链接");
}
long needPoints = (long) times * (long) perTimeQuantity;
if (log.isDebugEnabled()) {
log.debug("generateLinks operatorId={} operatorType={} targetAccountId={} times={} perTimeQuantity={} needPoints={} expireHours={}",
operatorId, operatorType, targetAccountId, times, perTimeQuantity, needPoints, expireHours);
}
if (!isAdminOperator) {
// 代理商自操作,需扣点判断
long balance = target.getPointsBalance() == null ? 0L : target.getPointsBalance();
if (balance < needPoints) {
throw new IllegalStateException("点数不足");
}
}
LinkBatch batch = new LinkBatch();
batch.setAgentId(target.getId());
batch.setQuantity(times);
batch.setTimes(times);
batch.setBatchSize(perTimeQuantity);
batch.setDeductPoints(needPoints);
batch.setOperatorId(operatorId);
linkBatchMapper.insert(batch);
LocalDateTime expireAt = LocalDateTime.now().plusHours(expireHours);
List<LinkTask> tasks = new ArrayList<>();
for (int i = 0; i < times; i++) {
LinkTask t = new LinkTask();
t.setBatchId(batch.getId());
t.setAgentId(target.getId());
t.setCodeNo(generateCodeNo());
t.setTokenHash(DigestUtils.sha256Hex(generateToken()));
t.setExpireAt(expireAt);
t.setStatus("NEW");
linkTaskMapper.insert(t);
tasks.add(t);
}
if (!isAdminOperator) {
// 扣点流水 + 账户余额
long before = target.getPointsBalance() == null ? 0L : target.getPointsBalance();
long after = before - needPoints;
AgentPointsTx tx = new AgentPointsTx();
tx.setAccountId(target.getId());
tx.setType("DEDUCT");
tx.setBeforePoints(before);
tx.setDeltaPoints(-needPoints);
tx.setAfterPoints(after);
tx.setReason("create_links");
tx.setRefId(batch.getId());
tx.setOperatorId(operatorId);
agentPointsTxMapper.insert(tx);
UserAccount patch = new UserAccount();
patch.setId(target.getId());
patch.setPointsBalance(after);
userAccountMapper.update(patch);
}
GenerateResult result = new GenerateResult();
result.setBatchId(batch.getId());
result.setNeedPoints(needPoints);
result.setExpireAt(expireAt);
result.setCodeNos(tasks.stream().map(LinkTask::getCodeNo).toList());
return result;
}
private static final String CODE_CHARS = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; // avoid confusing chars
private static final SecureRandom RANDOM = new SecureRandom();
private String generateCodeNo() {
StringBuilder sb = new StringBuilder(8);
for (int i = 0; i < 8; i++) {
sb.append(CODE_CHARS.charAt(RANDOM.nextInt(CODE_CHARS.length())));
}
return sb.toString();
}
private String generateToken() {
byte[] bytes = new byte[32];
RANDOM.nextBytes(bytes);
return org.apache.commons.codec.binary.Hex.encodeHexString(bytes);
}
public static class GenerateResult {
private Long batchId;
private Long needPoints;
private LocalDateTime expireAt;
private List<String> codeNos;
public Long getBatchId() { return batchId; }
public void setBatchId(Long batchId) { this.batchId = batchId; }
public Long getNeedPoints() { return needPoints; }
public void setNeedPoints(Long needPoints) { this.needPoints = needPoints; }
public LocalDateTime getExpireAt() { return expireAt; }
public void setExpireAt(LocalDateTime expireAt) { this.expireAt = expireAt; }
public List<String> getCodeNos() { return codeNos; }
public void setCodeNos(List<String> codeNos) { this.codeNos = codeNos; }
}
}

View File

@@ -53,3 +53,12 @@ springdoc:
disable-swagger-default-url: true
display-request-duration: true
packages-to-scan: com.gameplatform.server.controller
# 外部脚本端配置与链接过期时间
script:
base-url: "http://36.138.184.60:12345"
connect-timeout-ms: 3000
read-timeout-ms: 5000
link:
expire-hours: 2

View File

@@ -53,3 +53,12 @@ springdoc:
disable-swagger-default-url: true
display-request-duration: true
packages-to-scan: com.gameplatform.server.controller
# 外部脚本端配置与链接过期时间
script:
base-url: "http://36.138.184.60:12345"
connect-timeout-ms: 3000
read-timeout-ms: 5000
link:
expire-hours: 2

View File

@@ -1,34 +1,45 @@
com\gameplatform\server\mapper\agent\LinkTaskMapper.class
com\gameplatform\server\exception\GlobalExceptionHandler$2.class
com\gameplatform\server\model\entity\admin\Announcement.class
com\gameplatform\server\service\link\LinkGenerationService.class
com\gameplatform\server\mapper\agent\AgentPointsTxMapper.class
com\gameplatform\server\config\CorsConfig.class
com\gameplatform\server\exception\GlobalExceptionHandler.class
com\gameplatform\server\model\dto\common\PageResult.class
com\gameplatform\server\service\UserService.class
com\gameplatform\server\controller\auth\AuthController.class
com\gameplatform\server\model\entity\agent\AgentPointsTx.class
com\gameplatform\server\controller\admin\AccountController.class
com\gameplatform\server\GamePlatformServerApplication.class
com\gameplatform\server\model\dto\auth\LoginRequest.class
com\gameplatform\server\model\entity\account\UserAccount.class
com\gameplatform\server\mapper\admin\AnnouncementMapper.class
com\gameplatform\server\model\dto\auth\LoginResponse.class
com\gameplatform\server\model\entity\agent\LinkTask.class
com\gameplatform\server\model\dto\link\CreateLinkBatchResponse.class
com\gameplatform\server\service\auth\AuthService.class
com\gameplatform\server\config\SwaggerConfig.class
com\gameplatform\server\exception\GlobalExceptionHandler$1.class
com\gameplatform\server\mapper\account\UserAccountMapper.class
com\gameplatform\server\service\account\AccountService.class
com\gameplatform\server\model\dto\link\CreateLinkBatchRequest.class
com\gameplatform\server\model\dto\link\LinkStatisticsResponse.class
com\gameplatform\server\service\link\QrCodeProxyService$CachedImage.class
com\gameplatform\server\service\link\QrCodeProxyService.class
com\gameplatform\server\controller\auth\AuthController$1.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\controller\admin\AccountController.class
com\gameplatform\server\model\dto\auth\LoginRequest.class
com\gameplatform\server\model\entity\account\UserAccount.class
com\gameplatform\server\model\dto\auth\LoginResponse.class
com\gameplatform\server\model\entity\agent\LinkTask.class
com\gameplatform\server\exception\GlobalExceptionHandler$1.class
com\gameplatform\server\mapper\agent\LinkBatchMapper.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\model\entity\agent\LinkBatch.class
com\gameplatform\server\service\account\AccountService.class
com\gameplatform\server\controller\UserController.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\dto\account\AccountResponse.class
com\gameplatform\server\model\dto\account\AccountCreateRequest.class
com\gameplatform\server\config\ScheduleConfig.class
com\gameplatform\server\model\dto\account\AccountUpdateRequest.class
com\gameplatform\server\controller\auth\AuthController$1.class
com\gameplatform\server\model\dto\account\ResetPasswordRequest.class

View File

@@ -6,8 +6,10 @@ D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\service\acc
D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\service\auth\AuthService.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\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\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\config\SwaggerConfig.java
D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\model\dto\account\AccountResponse.java
@@ -17,13 +19,17 @@ D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\controller\
D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\security\JwtService.java
D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\config\CorsConfig.java
D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\model\dto\common\PageResult.java
D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\model\dto\link\LinkGenerateResponse.java
D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\GamePlatformServerApplication.java
D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\model\dto\link\LinkGenerateRequest.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\entity\admin\Announcement.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\controller\link\LinkController.java
D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\mapper\admin\OperationLogMapper.java
D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\service\link\LinkGenerationService.java
D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\exception\GlobalExceptionHandler.java
D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\model\entity\agent\LinkBatch.java
D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\mapper\account\UserAccountMapper.java

View File

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

View File

@@ -0,0 +1,2 @@
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