添加Apache Commons Codec依赖以支持SHA-256和十六进制工具,并在application.yml中新增外部脚本配置及链接过期时间设置。同时,删除不再使用的类文件以清理项目结构。
This commit is contained in:
@@ -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; }
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
|
||||
60
src/main/java/com/gameplatform/server/service/external/ScriptClient.java
vendored
Normal file
60
src/main/java/com/gameplatform/server/service/external/ScriptClient.java
vendored
Normal 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()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user