package com.rj.service; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.rj.config.HxrAdminProperties; import com.rj.util.HxrGoodsSignUtil; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.security.SecureRandom; import java.time.Duration; import java.util.LinkedHashMap; import java.util.Map; /** * 调用 hxrd {@code POST /api/order/buy} 抢购(请求头 {@code token} + {@code S/T/N} 鉴权)。 */ @Slf4j @Service @RequiredArgsConstructor public class HxrAdminBuyService { private static final SecureRandom RANDOM = new SecureRandom(); private static final String USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36 Edg/148.0.0.0"; private static final ObjectMapper JSON = new ObjectMapper() .setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE); private final HxrAdminProperties properties; /** * @return {@code apiCode} 为 -1 表示 HTTP 或解析失败 */ public BuyApiResult buy(long goodsId, long sellerId, String token) throws Exception { String resolvedToken = resolveToken(token); if (resolvedToken == null) { return BuyApiResult.failure(-1, "未提供 token 且未配置 hxr.admin.goods-api-token"); } String appStr = properties.getGoodsApiAppStr(); if (appStr == null || appStr.isBlank()) { return BuyApiResult.failure(-1, "未配置 hxr.admin.goods-api-app-str"); } long timestamp = System.currentTimeMillis() / 1000; String noncestr = randomNoncestr(); Map signParams = new LinkedHashMap<>(); signParams.put("id", goodsId); signParams.put("seller_id", sellerId); signParams.put("timestamp", timestamp); signParams.put("noncestr", noncestr); String sign = HxrGoodsSignUtil.computeSign(signParams, appStr.trim()); Map body = new LinkedHashMap<>(); body.put("id", goodsId); body.put("seller_id", sellerId); String bodyJson = JSON.writeValueAsString(body); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(60)) .followRedirects(HttpClient.Redirect.NORMAL) .build(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(properties.getBuyApiUrl())) .timeout(Duration.ofSeconds(120)) .header("Accept", "application/json,*/*") .header("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8") .header("Content-Type", "application/json;charset=UTF-8") .header("Origin", properties.getGoodsApiOrigin()) .header("Referer", properties.getGoodsApiReferer()) .header("User-Agent", USER_AGENT) .header("token", resolvedToken) .header("S", sign) .header("T", String.valueOf(timestamp)) .header("N", noncestr) .POST(HttpRequest.BodyPublishers.ofString(bodyJson, StandardCharsets.UTF_8)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); int status = response.statusCode(); String bodyText = response.body(); if (status < 200 || status >= 300) { log.warn("hxr /api/order/buy HTTP {} id={} sellerId={} bodyPrefix={}", status, goodsId, sellerId, abbreviate(bodyText, 400)); return BuyApiResult.failure(-1, "HTTP " + status); } if (bodyText == null || bodyText.isBlank()) { log.warn("hxr /api/order/buy empty body id={} sellerId={}", goodsId, sellerId); return BuyApiResult.failure(-1, "响应为空"); } JsonNode root = JSON.readTree(bodyText); int code = root.path("code").asInt(-1); String msg = root.path("msg").asText(""); if (code != 0) { log.warn("hxr /api/order/buy api code={} msg={} id={} sellerId={}", code, msg, goodsId, sellerId); } return new BuyApiResult(code == 0, code, msg); } /** 优先使用入参 token,否则回退到配置 {@code goods-api-token}。 */ private String resolveToken(String token) { if (token != null && !token.isBlank()) { return token.trim(); } String configured = properties.getGoodsApiToken(); if (configured != null && !configured.isBlank()) { return configured.trim(); } return null; } public record BuyApiResult(boolean success, int apiCode, String apiMsg) { public static BuyApiResult failure(int code, String msg) { return new BuyApiResult(false, code, msg); } } private static String randomNoncestr() { String base36 = Long.toUnsignedString(Math.abs(RANDOM.nextLong()), 36); if (base36.length() >= 5) { return base36.substring(base36.length() - 5); } StringBuilder sb = new StringBuilder(base36); while (sb.length() < 5) { sb.append(Integer.toString(RANDOM.nextInt(36), 36)); } return sb.toString(); } private static String abbreviate(String s, int maxLen) { if (s == null) { return ""; } return s.length() <= maxLen ? s : s.substring(0, maxLen) + "..."; } }