增加抢单接口
This commit is contained in:
@@ -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}。
|
||||
*/
|
||||
|
||||
@@ -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/buy(body: 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);
|
||||
}
|
||||
}
|
||||
|
||||
23
src/main/java/com/rj/dto/LbGoodsRushBuyRequest.java
Normal file
23
src/main/java/com/rj/dto/LbGoodsRushBuyRequest.java
Normal 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;
|
||||
}
|
||||
136
src/main/java/com/rj/service/HxrAdminBuyService.java
Normal file
136
src/main/java/com/rj/service/HxrAdminBuyService.java
Normal 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) + "...";
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -23,7 +23,7 @@ hxr:
|
||||
cookie: PHPSID=f3710baf6381da418aa832660ccb9d08
|
||||
phpsid: "f3710baf6381da418aa832660ccb9d08"
|
||||
# 货品同步 /api/order/goods(LbGoodsController#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/"
|
||||
|
||||
Reference in New Issue
Block a user