已超出当天可抢订单数 ,停止抢单
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