增加抢单接口

This commit is contained in:
2026-05-28 18:45:12 +08:00
parent b0494ff5b6
commit 412aa1f8d8
7 changed files with 304 additions and 1 deletions

View File

@@ -65,6 +65,11 @@ public class HxrAdminProperties {
*/
private String goodsApiUrl = "https://hxrdhoutai.hxrdsm.cn/api/order/goods?page=1&limit=20";
/**
* 抢购 API{@code POST /api/order/buy})。
*/
private String buyApiUrl = "https://hxrdhoutai.hxrdsm.cn/api/order/buy";
/**
* 货品列表 API 请求头 {@code token}。
*/

View File

@@ -1,5 +1,6 @@
package com.rj.controller;
import com.rj.dto.LbGoodsRushBuyRequest;
import com.rj.entity.LbGoods;
import com.rj.service.ILbGoodsService;
import io.swagger.v3.oas.annotations.Operation;
@@ -126,4 +127,28 @@ public class LbGoodsController {
}
return ResponseEntity.badRequest().body(result);
}
@PostMapping("/rush-buy")
@Operation(
summary = "抢购货品",
description =
"按 idList 查询 lb_goods再使用 token 调用 hxrd POST /api/order/buybody: id、seller_id"
+ "成功笔数达到 maxBuyCount 后停止继续请求;需配置 hxr.admin.goods-api-app-str")
public ResponseEntity<Map<String, Object>> rushBuy(
@Parameter(description = "token、maxBuyCount、idList", required = true)
@RequestBody LbGoodsRushBuyRequest request) {
Map<String, Object> result = lbGoodsService.rushBuy(
request.getToken(), request.getMaxBuyCount(), request.getIdList());
Boolean success = (Boolean) result.get("success");
if (success != null && success) {
return ResponseEntity.ok(result);
}
if (result.get("message") != null
&& (result.get("message").toString().contains("不能为空")
|| result.get("message").toString().contains("必须大于0")
|| result.get("message").toString().contains("无有效id"))) {
return ResponseEntity.badRequest().body(result);
}
return ResponseEntity.ok(result);
}
}

View File

@@ -0,0 +1,23 @@
package com.rj.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
/**
* 抢购请求:按货品 id 列表查询 {@code lb_goods},再调用 hxrd {@code /api/order/buy}。
*/
@Data
@Schema(description = "LB 货品抢购请求")
public class LbGoodsRushBuyRequest {
@Schema(description = "hxrd 接口请求头 token", requiredMode = Schema.RequiredMode.REQUIRED)
private String token;
@Schema(description = "最多成功抢购笔数(达到后停止继续请求)", requiredMode = Schema.RequiredMode.REQUIRED)
private Integer maxBuyCount;
@Schema(description = "货品 id 列表(对应 lb_goods.id", requiredMode = Schema.RequiredMode.REQUIRED)
private List<Long> idList;
}

View File

@@ -0,0 +1,136 @@
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 {
if (token == null || token.isBlank()) {
return BuyApiResult.failure(-1, "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<String, Object> 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<String, Object> 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", token.trim())
.header("S", sign)
.header("T", String.valueOf(timestamp))
.header("N", noncestr)
.POST(HttpRequest.BodyPublishers.ofString(bodyJson, StandardCharsets.UTF_8))
.build();
HttpResponse<String> 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);
}
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) + "...";
}
}

View File

@@ -3,6 +3,7 @@ package com.rj.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.rj.entity.LbGoods;
import java.util.List;
import java.util.Map;
public interface ILbGoodsService extends IService<LbGoods> {
@@ -32,4 +33,9 @@ public interface ILbGoodsService extends IService<LbGoods> {
* 分页调用第三方 {@code /api/order/goods},解析后 upsert 到 {@code lb_goods}。
*/
Map<String, Object> syncFromHxrGoods(String tenantId);
/**
* 按 id 列表查询 {@code lb_goods},使用 token 调用 hxrd {@code /api/order/buy} 抢购。
*/
Map<String, Object> rushBuy(String token, Integer maxBuyCount, List<Long> idList);
}

View File

@@ -7,6 +7,7 @@ import com.rj.dto.hxr.HxrLbGoodsPageData;
import com.rj.dto.hxr.HxrLbGoodsSelectResponse;
import com.rj.entity.LbGoods;
import com.rj.mapper.LbGoodsMapper;
import com.rj.service.HxrAdminBuyService;
import com.rj.service.HxrAdminGoodsService;
import com.rj.service.ILbGoodsService;
import com.rj.tenant.TenantContextHolder;
@@ -35,6 +36,8 @@ public class LbGoodsServiceImpl extends ServiceImpl<LbGoodsMapper, LbGoods> impl
private final HxrAdminGoodsService hxrAdminGoodsService;
private final HxrAdminBuyService hxrAdminBuyService;
@Override
public Map<String, Object> add(LbGoods entity) {
Map<String, Object> result = new HashMap<>();
@@ -371,6 +374,111 @@ public class LbGoodsServiceImpl extends ServiceImpl<LbGoodsMapper, LbGoods> impl
}
}
@Override
public Map<String, Object> rushBuy(String token, Integer maxBuyCount, List<Long> idList) {
Map<String, Object> result = new HashMap<>();
try {
if (token == null || token.isBlank()) {
result.put("success", false);
result.put("message", "token不能为空");
return result;
}
if (maxBuyCount == null || maxBuyCount <= 0) {
result.put("success", false);
result.put("message", "maxBuyCount必须大于0");
return result;
}
if (idList == null || idList.isEmpty()) {
result.put("success", false);
result.put("message", "idList不能为空");
return result;
}
List<Long> distinctIds = idList.stream()
.filter(id -> id != null)
.distinct()
.toList();
if (distinctIds.isEmpty()) {
result.put("success", false);
result.put("message", "idList无有效id");
return result;
}
List<LbGoods> found = this.listByIds(distinctIds);
Map<Long, LbGoods> goodsById = new LinkedHashMap<>();
for (LbGoods goods : found) {
if (goods.getId() != null) {
goodsById.put(goods.getId(), goods);
}
}
List<Map<String, Object>> details = new ArrayList<>();
int successCount = 0;
int failCount = 0;
boolean stoppedByMax = false;
for (Long goodsId : idList) {
if (goodsId == null) {
continue;
}
if (successCount >= maxBuyCount) {
stoppedByMax = true;
break;
}
Map<String, Object> item = new LinkedHashMap<>();
item.put("id", goodsId);
LbGoods goods = goodsById.get(goodsId);
if (goods == null) {
item.put("success", false);
item.put("message", "lb_goods中未找到该货品");
details.add(item);
failCount++;
continue;
}
if (goods.getSellerId() == null) {
item.put("success", false);
item.put("message", "seller_id为空无法抢购");
details.add(item);
failCount++;
continue;
}
item.put("sellerId", goods.getSellerId());
item.put("title", goods.getTitle());
HxrAdminBuyService.BuyApiResult buyResult =
hxrAdminBuyService.buy(goodsId, goods.getSellerId(), token);
item.put("apiCode", buyResult.apiCode());
item.put("message", buyResult.apiMsg());
item.put("success", buyResult.success());
details.add(item);
if (buyResult.success()) {
successCount++;
} else {
failCount++;
}
}
result.put("success", successCount > 0);
result.put("message", successCount > 0
? "抢购完成,成功 " + successCount + " 笔,失败 " + failCount + ""
: "抢购未成功");
result.put("successCount", successCount);
result.put("failCount", failCount);
result.put("stoppedByMax", stoppedByMax);
result.put("details", details);
return result;
} catch (Exception e) {
log.error("rushBuy failed", e);
result.put("success", false);
result.put("message", "抢购异常:" + e.getMessage());
return result;
}
}
private static void applyDefaults(LbGoods entity) {
if (entity.getPrice() == null) {
entity.setPrice(BigDecimal.ZERO);

View File

@@ -23,7 +23,7 @@ hxr:
cookie: PHPSID=f3710baf6381da418aa832660ccb9d08
phpsid: "f3710baf6381da418aa832660ccb9d08"
# 货品同步 /api/order/goodsLbGoodsController#sync-from-hxr
goods-api-token: "e176f641-4425-4930-bab3-3cfdc2a8fd2a"
goods-api-token: "75d9a643-ea07-4f0a-afff-2f8bbcd3f3f2"
goods-api-app-str: "ssniQQ3UP2Vr8mXwaugssgaOLzQo0cX5"
goods-api-origin: "https://hxrdweb.hxrdsm.cn"
goods-api-referer: "https://hxrdweb.hxrdsm.cn/"