已超出当天可抢订单数 ,停止抢单
This commit is contained in:
13
src/main/java/com/rj/dto/hxr/HxrFansApiContext.java
Normal file
13
src/main/java/com/rj/dto/hxr/HxrFansApiContext.java
Normal file
@@ -0,0 +1,13 @@
|
||||
package com.rj.dto.hxr;
|
||||
|
||||
/**
|
||||
* 调用 hxrd {@code /api/share/select} 粉丝列表 API 所需的运行时配置(由 {@code lb_third_integration_config} 解析而来)。
|
||||
*/
|
||||
public record HxrFansApiContext(
|
||||
String fansApiBaseUrl,
|
||||
int pageLimit,
|
||||
String origin,
|
||||
String referer,
|
||||
String token,
|
||||
String appStr) {
|
||||
}
|
||||
11
src/main/java/com/rj/dto/hxr/HxrLbFansPageData.java
Normal file
11
src/main/java/com/rj/dto/hxr/HxrLbFansPageData.java
Normal file
@@ -0,0 +1,11 @@
|
||||
package com.rj.dto.hxr;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/** hxrd {@code /api/share/select} 响应中的 {@code data} 节点。 */
|
||||
public record HxrLbFansPageData(
|
||||
List<Map<String, Object>> list,
|
||||
boolean hasmore,
|
||||
int lastPage) {
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.rj.dto.hxr;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.type.MapType;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 兼容第三方 {@code data} 两种形态:
|
||||
* <ul>
|
||||
* <li>对象:{@code {"list":[...],"hasmore":true,"last_page":13}}</li>
|
||||
* <li>数组:{@code [...]}(按每页条数推断是否还有下一页)</li>
|
||||
* </ul>
|
||||
*/
|
||||
public class HxrLbFansPageDataDeserializer extends JsonDeserializer<HxrLbFansPageData> {
|
||||
|
||||
private static final int DEFAULT_PAGE_SIZE = 10;
|
||||
|
||||
@Override
|
||||
public HxrLbFansPageData deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
|
||||
JsonNode node = p.getCodec().readTree(p);
|
||||
if (node == null || node.isNull()) {
|
||||
return emptyPage();
|
||||
}
|
||||
if (node.isArray()) {
|
||||
if (node.isEmpty()) {
|
||||
return emptyPage();
|
||||
}
|
||||
if (node.get(0).isTextual() || node.get(0).isNumber()) {
|
||||
return emptyPage();
|
||||
}
|
||||
List<Map<String, Object>> list = readMapList(ctxt, node);
|
||||
boolean hasMore = list.size() >= DEFAULT_PAGE_SIZE;
|
||||
int lastPage = hasMore ? Integer.MAX_VALUE : 1;
|
||||
return new HxrLbFansPageData(list, hasMore, lastPage);
|
||||
}
|
||||
JsonNode listNode = node.get("list");
|
||||
List<Map<String, Object>> list = listNode != null && listNode.isArray()
|
||||
? readMapList(ctxt, listNode)
|
||||
: List.of();
|
||||
boolean hasmore = node.path("hasmore").asBoolean(false);
|
||||
int lastPage = node.path("last_page").asInt(1);
|
||||
if (lastPage < 1) {
|
||||
lastPage = 1;
|
||||
}
|
||||
return new HxrLbFansPageData(list, hasmore, lastPage);
|
||||
}
|
||||
|
||||
private static List<Map<String, Object>> readMapList(DeserializationContext ctxt, JsonNode listNode)
|
||||
throws IOException {
|
||||
MapType mapType = ctxt.getTypeFactory().constructMapType(LinkedHashMap.class, String.class, Object.class);
|
||||
List<Map<String, Object>> list = new ArrayList<>();
|
||||
for (JsonNode item : listNode) {
|
||||
Map<String, Object> row = ctxt.readTreeAsValue(item, mapType);
|
||||
if (row != null) {
|
||||
list.add(row);
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private static HxrLbFansPageData emptyPage() {
|
||||
return new HxrLbFansPageData(List.of(), false, 1);
|
||||
}
|
||||
}
|
||||
10
src/main/java/com/rj/dto/hxr/HxrLbFansSelectResponse.java
Normal file
10
src/main/java/com/rj/dto/hxr/HxrLbFansSelectResponse.java
Normal file
@@ -0,0 +1,10 @@
|
||||
package com.rj.dto.hxr;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
|
||||
/** hxrd {@code /api/share/select} 顶层 JSON。 */
|
||||
public record HxrLbFansSelectResponse(
|
||||
int code,
|
||||
String msg,
|
||||
@JsonDeserialize(using = HxrLbFansPageDataDeserializer.class) HxrLbFansPageData data) {
|
||||
}
|
||||
170
src/main/java/com/rj/service/HxrAdminFansService.java
Normal file
170
src/main/java/com/rj/service/HxrAdminFansService.java
Normal file
@@ -0,0 +1,170 @@
|
||||
package com.rj.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
|
||||
import com.rj.dto.hxr.HxrFansApiContext;
|
||||
import com.rj.dto.hxr.HxrLbFansSelectResponse;
|
||||
import com.rj.util.HxrGoodsSignUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.security.SecureRandom;
|
||||
import java.time.Duration;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 调用 hxrd {@code GET /api/share/select} 分页拉取粉丝列表(请求头 {@code token} + {@code S/T/N} 鉴权)。
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class HxrAdminFansService {
|
||||
|
||||
public static final int FANS_PAGE_SIZE = 10;
|
||||
|
||||
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 String HEADER_TOKEN = "Token";
|
||||
|
||||
private static final ObjectMapper JSON = new ObjectMapper()
|
||||
.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)
|
||||
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
|
||||
/**
|
||||
* 分页拉取粉丝列表(使用租户 {@code lb_third_integration_config} 解析出的运行时配置)。
|
||||
*
|
||||
* @param page 页码,从 1 开始
|
||||
*/
|
||||
public Optional<HxrLbFansSelectResponse> fetchFansPage(int page, HxrFansApiContext ctx) throws Exception {
|
||||
if (ctx == null) {
|
||||
log.warn("粉丝 API 配置为空,跳过 /api/share/select");
|
||||
return Optional.empty();
|
||||
}
|
||||
String resolvedToken = ctx.token();
|
||||
if (resolvedToken == null || resolvedToken.isBlank()) {
|
||||
log.warn("未提供 token,跳过 /api/share/select");
|
||||
return Optional.empty();
|
||||
}
|
||||
String appStr = ctx.appStr();
|
||||
if (appStr == null || appStr.isBlank()) {
|
||||
log.warn("未配置 goodsApiAppStr,跳过 /api/share/select");
|
||||
return Optional.empty();
|
||||
}
|
||||
String fansApiBaseUrl = ctx.fansApiBaseUrl();
|
||||
if (fansApiBaseUrl == null || fansApiBaseUrl.isBlank()) {
|
||||
log.warn("未配置 fansApiBaseUrl,跳过 /api/share/select");
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
int pageLimit = ctx.pageLimit() > 0 ? ctx.pageLimit() : FANS_PAGE_SIZE;
|
||||
int safePage = Math.max(1, page);
|
||||
String uri = UriComponentsBuilder.fromUriString(fansApiBaseUrl)
|
||||
.replaceQueryParam("page", safePage)
|
||||
.replaceQueryParam("limit", pageLimit)
|
||||
.build()
|
||||
.encode()
|
||||
.toUriString();
|
||||
|
||||
long timestamp = System.currentTimeMillis() / 1000;
|
||||
String noncestr = randomNoncestr();
|
||||
Map<String, Object> signParams = new LinkedHashMap<>();
|
||||
signParams.put("page", safePage);
|
||||
signParams.put("limit", pageLimit);
|
||||
signParams.put("timestamp", timestamp);
|
||||
signParams.put("noncestr", noncestr);
|
||||
String sign = HxrGoodsSignUtil.computeSign(signParams, appStr.trim());
|
||||
|
||||
HttpClient client = HttpClient.newBuilder()
|
||||
.connectTimeout(Duration.ofSeconds(60))
|
||||
.followRedirects(HttpClient.Redirect.NORMAL)
|
||||
.build();
|
||||
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(uri))
|
||||
.timeout(Duration.ofSeconds(120))
|
||||
.header("Accept", "application/json,*/*")
|
||||
.header("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8")
|
||||
.header("Origin", ctx.origin())
|
||||
.header("Referer", ctx.referer())
|
||||
.header("User-Agent", USER_AGENT)
|
||||
.header(HEADER_TOKEN, resolvedToken)
|
||||
.header("S", sign)
|
||||
.header("T", String.valueOf(timestamp))
|
||||
.header("N", noncestr)
|
||||
.GET()
|
||||
.build();
|
||||
|
||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
int status = response.statusCode();
|
||||
if (status < 200 || status >= 300) {
|
||||
log.warn("hxr /api/share/select HTTP {} page={} bodyPrefix={}",
|
||||
status, safePage, abbreviate(response.body(), 400));
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
String bodyText = response.body();
|
||||
if (bodyText == null || bodyText.isBlank()) {
|
||||
log.warn("hxr /api/share/select empty body page={}", safePage);
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
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/share/select api code={} msg={} page={} bodyPrefix={}",
|
||||
code, msg, safePage, abbreviate(bodyText, 400));
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
JsonNode dataNode = root.get("data");
|
||||
if (isErrorPayload(dataNode)) {
|
||||
log.warn("hxr /api/share/select 返回异常 data={} page={} msg={}", dataNode, safePage, msg);
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
HxrLbFansSelectResponse body = JSON.treeToValue(root, HxrLbFansSelectResponse.class);
|
||||
return Optional.of(body);
|
||||
}
|
||||
|
||||
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 boolean isErrorPayload(JsonNode dataNode) {
|
||||
if (dataNode == null || dataNode.isNull()) {
|
||||
return false;
|
||||
}
|
||||
if (!dataNode.isArray() || dataNode.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return dataNode.get(0).isTextual() || dataNode.get(0).isNumber();
|
||||
}
|
||||
|
||||
private static String abbreviate(String s, int maxLen) {
|
||||
if (s == null) {
|
||||
return "";
|
||||
}
|
||||
return s.length() <= maxLen ? s : s.substring(0, maxLen) + "...";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
-- 升级脚本:lb_fan_management 增加优惠券与奖金字段
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
ALTER TABLE `lb_fan_management`
|
||||
ADD COLUMN `coupon` DECIMAL(12,3) NOT NULL DEFAULT 0.000 COMMENT '优惠券金额' AFTER `token`,
|
||||
ADD COLUMN `self_bonus` DECIMAL(12,3) NOT NULL DEFAULT 0.000 COMMENT '自购奖金' AFTER `coupon`,
|
||||
ADD COLUMN `share_bonus` DECIMAL(12,3) NOT NULL DEFAULT 0.000 COMMENT '分享奖金' AFTER `self_bonus`;
|
||||
Reference in New Issue
Block a user