增加抢单接口
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";
|
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}。
|
* 货品列表 API 请求头 {@code token}。
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.rj.controller;
|
package com.rj.controller;
|
||||||
|
|
||||||
|
import com.rj.dto.LbGoodsRushBuyRequest;
|
||||||
import com.rj.entity.LbGoods;
|
import com.rj.entity.LbGoods;
|
||||||
import com.rj.service.ILbGoodsService;
|
import com.rj.service.ILbGoodsService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
@@ -126,4 +127,28 @@ public class LbGoodsController {
|
|||||||
}
|
}
|
||||||
return ResponseEntity.badRequest().body(result);
|
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.baomidou.mybatisplus.extension.service.IService;
|
||||||
import com.rj.entity.LbGoods;
|
import com.rj.entity.LbGoods;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
public interface ILbGoodsService extends IService<LbGoods> {
|
public interface ILbGoodsService extends IService<LbGoods> {
|
||||||
@@ -32,4 +33,9 @@ public interface ILbGoodsService extends IService<LbGoods> {
|
|||||||
* 分页调用第三方 {@code /api/order/goods},解析后 upsert 到 {@code lb_goods}。
|
* 分页调用第三方 {@code /api/order/goods},解析后 upsert 到 {@code lb_goods}。
|
||||||
*/
|
*/
|
||||||
Map<String, Object> syncFromHxrGoods(String tenantId);
|
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.dto.hxr.HxrLbGoodsSelectResponse;
|
||||||
import com.rj.entity.LbGoods;
|
import com.rj.entity.LbGoods;
|
||||||
import com.rj.mapper.LbGoodsMapper;
|
import com.rj.mapper.LbGoodsMapper;
|
||||||
|
import com.rj.service.HxrAdminBuyService;
|
||||||
import com.rj.service.HxrAdminGoodsService;
|
import com.rj.service.HxrAdminGoodsService;
|
||||||
import com.rj.service.ILbGoodsService;
|
import com.rj.service.ILbGoodsService;
|
||||||
import com.rj.tenant.TenantContextHolder;
|
import com.rj.tenant.TenantContextHolder;
|
||||||
@@ -35,6 +36,8 @@ public class LbGoodsServiceImpl extends ServiceImpl<LbGoodsMapper, LbGoods> impl
|
|||||||
|
|
||||||
private final HxrAdminGoodsService hxrAdminGoodsService;
|
private final HxrAdminGoodsService hxrAdminGoodsService;
|
||||||
|
|
||||||
|
private final HxrAdminBuyService hxrAdminBuyService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Map<String, Object> add(LbGoods entity) {
|
public Map<String, Object> add(LbGoods entity) {
|
||||||
Map<String, Object> result = new HashMap<>();
|
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) {
|
private static void applyDefaults(LbGoods entity) {
|
||||||
if (entity.getPrice() == null) {
|
if (entity.getPrice() == null) {
|
||||||
entity.setPrice(BigDecimal.ZERO);
|
entity.setPrice(BigDecimal.ZERO);
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ hxr:
|
|||||||
cookie: PHPSID=f3710baf6381da418aa832660ccb9d08
|
cookie: PHPSID=f3710baf6381da418aa832660ccb9d08
|
||||||
phpsid: "f3710baf6381da418aa832660ccb9d08"
|
phpsid: "f3710baf6381da418aa832660ccb9d08"
|
||||||
# 货品同步 /api/order/goods(LbGoodsController#sync-from-hxr)
|
# 货品同步 /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-app-str: "ssniQQ3UP2Vr8mXwaugssgaOLzQo0cX5"
|
||||||
goods-api-origin: "https://hxrdweb.hxrdsm.cn"
|
goods-api-origin: "https://hxrdweb.hxrdsm.cn"
|
||||||
goods-api-referer: "https://hxrdweb.hxrdsm.cn/"
|
goods-api-referer: "https://hxrdweb.hxrdsm.cn/"
|
||||||
|
|||||||
Reference in New Issue
Block a user