SHA256
137 lines
5.7 KiB
Java
137 lines
5.7 KiB
Java
package ru.shine.promo.controller;
|
|
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.LoggerFactory;
|
|
import org.springframework.http.HttpStatus;
|
|
import org.springframework.http.ResponseEntity;
|
|
import org.springframework.web.bind.annotation.GetMapping;
|
|
import org.springframework.web.bind.annotation.PostMapping;
|
|
import org.springframework.web.bind.annotation.RequestBody;
|
|
import org.springframework.web.bind.annotation.RestController;
|
|
import ru.shine.promo.config.AppProperties;
|
|
import ru.shine.promo.dto.PromoRequest;
|
|
import ru.shine.promo.dto.PromoResponse;
|
|
import ru.shine.promo.service.PromoCodeService;
|
|
import ru.shine.promo.service.PromoException;
|
|
import ru.shine.promo.service.PromoTransferService;
|
|
import ru.shine.promo.service.UsedPromoStorageService;
|
|
|
|
import java.util.Map;
|
|
|
|
@RestController
|
|
public class PromoApiController {
|
|
|
|
private static final Logger log = LoggerFactory.getLogger(PromoApiController.class);
|
|
|
|
private final AppProperties appProperties;
|
|
private final PromoCodeService promoCodeService;
|
|
private final PromoTransferService promoTransferService;
|
|
private final UsedPromoStorageService usedPromoStorageService;
|
|
|
|
public PromoApiController(
|
|
AppProperties appProperties,
|
|
PromoCodeService promoCodeService,
|
|
PromoTransferService promoTransferService,
|
|
UsedPromoStorageService usedPromoStorageService
|
|
) {
|
|
this.appProperties = appProperties;
|
|
this.promoCodeService = promoCodeService;
|
|
this.promoTransferService = promoTransferService;
|
|
this.usedPromoStorageService = usedPromoStorageService;
|
|
}
|
|
|
|
@PostMapping("/api/promo/top-up")
|
|
public ResponseEntity<PromoResponse> topUp(@RequestBody PromoRequest request) {
|
|
String wallet = trimToEmpty(request == null ? null : request.getWallet());
|
|
String name = trimToEmpty(request == null ? null : request.getName());
|
|
String promoCode = promoCodeService.normalizePromoCode(request == null ? null : request.getPromoCode());
|
|
|
|
if (wallet.isEmpty()) {
|
|
return error(HttpStatus.BAD_REQUEST, "Пустой wallet");
|
|
}
|
|
if (!promoTransferService.isSolanaWalletValid(wallet)) {
|
|
return error(HttpStatus.BAD_REQUEST, "Неверный формат Solana-адреса");
|
|
}
|
|
if (name.isEmpty()) {
|
|
return error(HttpStatus.BAD_REQUEST, "Пустое имя");
|
|
}
|
|
if (name.length() < 2 || name.length() > 120) {
|
|
return error(HttpStatus.BAD_REQUEST, "Имя должно быть длиной от 2 до 120 символов");
|
|
}
|
|
if (promoCode.isEmpty()) {
|
|
return error(HttpStatus.BAD_REQUEST, "Пустой промокод");
|
|
}
|
|
boolean eternalPromo = promoCodeService.isEternalPromoCode(promoCode);
|
|
if (!eternalPromo && !promoCodeService.isPromoCodeFormatValid(promoCode)) {
|
|
return error(HttpStatus.BAD_REQUEST, "Неверный формат промокода");
|
|
}
|
|
|
|
try {
|
|
if (!eternalPromo && !promoCodeService.promoCodeExists(promoCode)) {
|
|
return error(HttpStatus.BAD_REQUEST, "Промокод не найден");
|
|
}
|
|
|
|
String signature = usedPromoStorageService.executeLocked(() -> {
|
|
if (!eternalPromo && usedPromoStorageService.isPromoUsed(promoCode)) {
|
|
throw new PromoException(HttpStatus.CONFLICT, "Промокод уже использован");
|
|
}
|
|
|
|
String txSignature = promoTransferService.sendPromoTransfer(wallet);
|
|
// Для вечного промокода ведём только лог использования, но не блокируем повторное применение.
|
|
usedPromoStorageService.appendUsedPromo(promoCode, wallet, name, txSignature);
|
|
return txSignature;
|
|
});
|
|
|
|
String explorerUrl = promoTransferService.buildExplorerUrl(signature);
|
|
String amount = appProperties.getPromoTransferAmountSol().stripTrailingZeros().toPlainString();
|
|
|
|
log.info(
|
|
"Promo top-up success: wallet={}, name={}, promoCode={}, signature={}",
|
|
wallet,
|
|
name,
|
|
promoCode,
|
|
signature
|
|
);
|
|
|
|
PromoResponse response = PromoResponse.success(
|
|
"Тестовое пополнение выполнено",
|
|
wallet,
|
|
name,
|
|
amount,
|
|
signature,
|
|
explorerUrl
|
|
);
|
|
return ResponseEntity.ok(response);
|
|
} catch (PromoException e) {
|
|
if (e.getStatus().is5xxServerError()) {
|
|
log.error("Top-up failed: {}", e.getMessage(), e);
|
|
} else {
|
|
log.warn("Top-up rejected: {}", e.getMessage());
|
|
}
|
|
return error(e.getStatus(), e.getMessage());
|
|
} catch (Exception e) {
|
|
log.error("Unexpected top-up error", e);
|
|
return error(HttpStatus.INTERNAL_SERVER_ERROR, "Внутренняя ошибка сервера");
|
|
}
|
|
}
|
|
|
|
@GetMapping("/health")
|
|
public Map<String, String> health() {
|
|
return Map.of(
|
|
"status", "ok",
|
|
"app", "SHiNE-promo-solana-devnet"
|
|
);
|
|
}
|
|
|
|
private ResponseEntity<PromoResponse> error(HttpStatus status, String message) {
|
|
return ResponseEntity.status(status).body(PromoResponse.error(message));
|
|
}
|
|
|
|
private String trimToEmpty(String value) {
|
|
if (value == null) {
|
|
return "";
|
|
}
|
|
return value.trim().replaceAll("\\s{2,}", " ");
|
|
}
|
|
}
|