已超出当天可抢订单数 ,停止抢单

This commit is contained in:
2026-06-03 23:10:18 +08:00
parent 4cf8122659
commit 0a13ce32ac
8 changed files with 465 additions and 6 deletions

View File

@@ -11,6 +11,7 @@ import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
@@ -68,6 +69,18 @@ public class LbFanManagement implements Serializable {
@Schema(description = "登录 token")
private String token;
@TableField("coupon")
@Schema(description = "优惠券金额")
private BigDecimal coupon;
@TableField("self_bonus")
@Schema(description = "自购奖金")
private BigDecimal selfBonus;
@TableField("share_bonus")
@Schema(description = "分享奖金")
private BigDecimal shareBonus;
@TableField("created_at")
@Schema(description = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")

View File

@@ -37,6 +37,9 @@ public class HxrAdminBuyService {
/** 与 hxrd 前端 DevTools 一致,使用 {@code Token} 而非小写 {@code token}。 */
private static final String HEADER_TOKEN = "Token";
/** 外部系统返回此文案时表示该账号当日可抢订单数已满,应停止继续抢购。 */
public static final String MSG_DAILY_LIMIT_EXCEEDED = "已超出当天可抢订单数";
private static final ObjectMapper JSON = new ObjectMapper()
.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
@@ -148,6 +151,10 @@ public class HxrAdminBuyService {
log.warn("hxr /api/order/buy 鉴权失败常见原因Token 与浏览器不一致或已过期、goodsApiAppStr 错误;"
+ "请在 rush-buy 请求体传入浏览器 DevTools 中 Token 头的值,并核对 lb_third_integration_config.goods_api_token / goods_api_app_str");
}
if (MSG_DAILY_LIMIT_EXCEEDED.equals(msg)) {
log.warn("hxr /api/order/buy 当日可抢订单数已满,停止该账号继续抢购 id={} sellerId={} tokenPrefix={}",
goodsId, sellerId, abbreviate(resolvedToken, 8));
}
}
return new BuyApiResult(code == 0, code, msg);
}
@@ -169,6 +176,11 @@ public class HxrAdminBuyService {
public static BuyApiResult failure(int code, String msg) {
return new BuyApiResult(false, code, msg);
}
/** {@code code=1} 且 msg 为「已超出当天可抢订单数」时,调用方应停止该账号后续抢购。 */
public boolean dailyLimitExceeded() {
return apiCode == 1 && MSG_DAILY_LIMIT_EXCEEDED.equals(apiMsg);
}
}
private static String randomNoncestr() {

View File

@@ -59,7 +59,7 @@ public class HxrAdminUserLoginService {
String noncestr = randomNoncestr();
Map<String, Object> body = new LinkedHashMap<>();
body.put("mobile", mobile.trim());
body.put("account", mobile.trim());
body.put("password", password.trim());
String bodyJson = JSON.writeValueAsString(body);
@@ -76,7 +76,7 @@ public class HxrAdminUserLoginService {
String appStr = ctx.appStr();
if (StringUtils.hasText(appStr)) {
Map<String, Object> signParams = new LinkedHashMap<>();
signParams.put("mobile", mobile.trim());
signParams.put("account", mobile.trim());
signParams.put("password", password.trim());
signParams.put("timestamp", timestamp);
signParams.put("noncestr", noncestr);

View File

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.extension.service.IService;
import com.rj.dto.LbThirdIntegrationCredentialUpdateRequest;
import com.rj.dto.hxr.HxrAdminOrderApiContext;
import com.rj.dto.hxr.HxrAdminUserApiContext;
import com.rj.dto.hxr.HxrFansApiContext;
import com.rj.dto.hxr.HxrGoodsApiContext;
import com.rj.dto.hxr.HxrUserLoginApiContext;
import com.rj.entity.LbThirdIntegrationConfig;
@@ -60,4 +61,10 @@ public interface ILbThirdIntegrationConfigService extends IService<LbThirdIntegr
* 按租户 id 解析用户登录 API 运行时配置login_api_path、Origin、Referer、appStr
*/
Optional<HxrUserLoginApiContext> resolveUserLoginApiContext(String tenantId);
/**
* 按租户 id 解析粉丝列表 API 运行时配置fans_api_path、fans_page_limit、Origin、Referer、appStr
* {@code token} 为登录用户 token必填。
*/
Optional<HxrFansApiContext> resolveFansApiContext(String tenantId, String token);
}

View File

@@ -4,9 +4,13 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.rj.dto.LbFanSimulateLoginRequest;
import com.rj.dto.hxr.HxrFansApiContext;
import com.rj.dto.hxr.HxrLbFansPageData;
import com.rj.dto.hxr.HxrLbFansSelectResponse;
import com.rj.dto.hxr.HxrUserLoginApiContext;
import com.rj.entity.LbFanManagement;
import com.rj.mapper.LbFanManagementMapper;
import com.rj.service.HxrAdminFansService;
import com.rj.service.HxrAdminUserLoginService;
import com.rj.service.ILbFanManagementService;
import com.rj.service.ILbThirdIntegrationConfigService;
@@ -16,6 +20,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
@@ -40,6 +45,9 @@ public class LbFanManagementServiceImpl
@Autowired
private HxrAdminUserLoginService hxrAdminUserLoginService;
@Autowired
private HxrAdminFansService hxrAdminFansService;
private static final String DEFAULT_SIMULATE_LOGIN_PASSWORD = "123456";
private static final DateTimeFormatter DATE_TIME_FORMATTER =
@@ -67,6 +75,15 @@ public class LbFanManagementServiceImpl
if (entity.getShareNum() == null) {
entity.setShareNum(0);
}
if (entity.getCoupon() == null) {
entity.setCoupon(BigDecimal.ZERO);
}
if (entity.getSelfBonus() == null) {
entity.setSelfBonus(BigDecimal.ZERO);
}
if (entity.getShareBonus() == null) {
entity.setShareBonus(BigDecimal.ZERO);
}
if (entity.getCreatedAt() == null) {
entity.setCreatedAt(LocalDateTime.now());
}
@@ -288,6 +305,18 @@ public class LbFanManagementServiceImpl
item.put("parsed", loginResult.parsed());
if (loginResult.success()) {
successCount++;
Map<String, Object> userinfo = extractUserinfo(loginResult.parsed());
if (userinfo != null) {
Map<String, Object> saveResult =
saveFanFromLoginUserinfo(userinfo, tenantId);
item.putAll(saveResult);
if (Boolean.TRUE.equals(saveResult.get("saved"))) {
item.putAll(fetchAndSaveFansFromThirdParty(userinfo, tenantId));
}
} else {
item.put("saved", false);
item.put("saveMsg", "响应中无 data.userinfo");
}
} else {
failCount++;
}
@@ -408,4 +437,350 @@ public class LbFanManagementServiceImpl
return null;
}
}
@SuppressWarnings("unchecked")
private static Map<String, Object> extractUserinfo(Object parsed) {
if (!(parsed instanceof Map<?, ?> root)) {
return null;
}
Object dataObj = root.get("data");
if (!(dataObj instanceof Map<?, ?> data)) {
return null;
}
Object userinfoObj = data.get("userinfo");
if (!(userinfoObj instanceof Map<?, ?>)) {
return null;
}
return (Map<String, Object>) userinfoObj;
}
private Map<String, Object> saveFanFromLoginUserinfo(Map<String, Object> userinfo, String tenantId) {
Map<String, Object> saveResult = new LinkedHashMap<>();
Long fanId = parseLong(userinfo.get("id"));
if (fanId == null) {
saveResult.put("saved", false);
saveResult.put("saveMsg", "userinfo.id 为空");
return saveResult;
}
LbFanManagement entity = new LbFanManagement();
entity.setTenantId(tenantId);
entity.setId(fanId);
entity.setIdTenantId(buildIdTenantId(fanId, tenantId));
entity.setAvatar(parseString(userinfo.get("avatar")));
entity.setNickname(parseString(userinfo.get("nickname")));
entity.setMobile(parseString(userinfo.get("mobile")));
entity.setToken(parseString(userinfo.get("token")));
entity.setCoupon(parseDecimal(userinfo.get("coupon")));
entity.setSelfBonus(parseDecimal(userinfo.get("self_bonus")));
entity.setShareBonus(parseDecimal(userinfo.get("share_bonus")));
trimStringFields(entity);
LbFanManagement existing = this.getById(entity.getIdTenantId());
if (existing != null) {
entity.setCreatedAt(existing.getCreatedAt());
entity.setShareNum(existing.getShareNum());
entity.setPName(existing.getPName());
entity.setPid(existing.getPid());
entity.setPPhone(existing.getPPhone());
if (entity.getCoupon() == null) {
entity.setCoupon(existing.getCoupon());
}
if (entity.getSelfBonus() == null) {
entity.setSelfBonus(existing.getSelfBonus());
}
if (entity.getShareBonus() == null) {
entity.setShareBonus(existing.getShareBonus());
}
boolean ok = this.updateById(entity);
saveResult.put("saved", ok);
saveResult.put("saveMsg", ok ? "已更新" : "更新失败");
if (ok) {
saveResult.put("fan", this.getById(entity.getIdTenantId()));
}
return saveResult;
}
if (entity.getShareNum() == null) {
entity.setShareNum(0);
}
if (entity.getCoupon() == null) {
entity.setCoupon(BigDecimal.ZERO);
}
if (entity.getSelfBonus() == null) {
entity.setSelfBonus(BigDecimal.ZERO);
}
if (entity.getShareBonus() == null) {
entity.setShareBonus(BigDecimal.ZERO);
}
entity.setCreatedAt(LocalDateTime.now());
try {
boolean ok = this.save(entity);
saveResult.put("saved", ok);
saveResult.put("saveMsg", ok ? "已新增" : "新增失败");
if (ok) {
saveResult.put("fan", entity);
}
} catch (DuplicateKeyException e) {
log.warn("粉丝模拟登录入库重复键, idTenantId={}", entity.getIdTenantId());
saveResult.put("saved", false);
saveResult.put("saveMsg", "唯一ID已存在" + entity.getIdTenantId());
}
return saveResult;
}
/**
* 登录用户保存成功后,分页调用第三方粉丝列表 API 并入库。
*/
private Map<String, Object> fetchAndSaveFansFromThirdParty(Map<String, Object> parentUserinfo,
String tenantId) {
Map<String, Object> result = new LinkedHashMap<>();
String token = parseString(parentUserinfo.get("token"));
if (token == null) {
result.put("fansFetchSuccess", false);
result.put("fansFetchMsg", "userinfo.token 为空,无法拉取粉丝");
result.put("fansSaved", 0);
return result;
}
Optional<HxrFansApiContext> ctxOpt =
lbThirdIntegrationConfigService.resolveFansApiContext(tenantId, token);
if (ctxOpt.isEmpty()) {
result.put("fansFetchSuccess", false);
result.put("fansFetchMsg",
"未找到粉丝 API 配置或配置未启用、URL/凭证不完整(请检查 lb_third_integration_config");
result.put("fansSaved", 0);
return result;
}
HxrFansApiContext ctx = ctxOpt.get();
Long parentId = parseLong(parentUserinfo.get("id"));
String parentName = parseString(parentUserinfo.get("nickname"));
String parentPhone = parseString(parentUserinfo.get("mobile"));
int page = 1;
int totalSaved = 0;
int totalFetched = 0;
HxrLbFansPageData firstPageData = null;
try {
while (true) {
Optional<HxrLbFansSelectResponse> opt =
hxrAdminFansService.fetchFansPage(page, ctx);
if (opt.isEmpty()) {
if (page == 1) {
result.put("fansFetchSuccess", false);
result.put("fansFetchMsg",
"未拉取到粉丝(请检查 lb_third_integration_config 中 token、appStr、URL 或网络)");
result.put("fansSaved", 0);
result.put("fansApiUrl", ctx.fansApiBaseUrl());
return result;
}
result.put("fansFetchSuccess", false);
result.put("fansFetchMsg", "" + page + " 页拉取失败,已成功保存前 "
+ (page - 1) + " 页共 " + totalSaved + " 条粉丝");
result.put("fansSaved", totalSaved);
result.put("fansFetched", totalFetched);
if (firstPageData != null) {
result.put("lastPage", firstPageData.lastPage());
}
result.put("fansPages", page - 1);
result.put("fansApiUrl", ctx.fansApiBaseUrl());
return result;
}
HxrLbFansSelectResponse body = opt.get();
HxrLbFansPageData pageData = body.data();
if (firstPageData == null && pageData != null) {
firstPageData = pageData;
}
List<Map<String, Object>> rows = pageData != null ? pageData.list() : null;
if (rows == null || rows.isEmpty()) {
break;
}
totalFetched += rows.size();
for (Map<String, Object> row : rows) {
Map<String, Object> rowSave = saveFanFromShareRow(
row, tenantId, parentId, parentName, parentPhone);
if (Boolean.TRUE.equals(rowSave.get("saved"))) {
totalSaved++;
}
}
boolean hasMore = pageData != null && pageData.hasmore();
int lastPage = pageData != null ? pageData.lastPage() : page;
if (!hasMore || page >= lastPage) {
break;
}
page++;
}
result.put("fansFetchSuccess", true);
result.put("fansFetchMsg", totalSaved == 0 ? "接口成功,无粉丝数据" : "粉丝拉取完成");
result.put("fansSaved", totalSaved);
result.put("fansFetched", totalFetched);
result.put("lastPage", firstPageData != null ? firstPageData.lastPage() : page);
result.put("fansPages", page);
result.put("fansApiUrl", ctx.fansApiBaseUrl());
return result;
} catch (Exception e) {
log.warn("拉取粉丝异常 tenantId={} parentId={}", tenantId, parentId, e);
result.put("fansFetchSuccess", false);
result.put("fansFetchMsg", "拉取粉丝异常:" + e.getMessage());
result.put("fansSaved", totalSaved);
result.put("fansFetched", totalFetched);
if (firstPageData != null) {
result.put("lastPage", firstPageData.lastPage());
}
result.put("fansPages", page > 1 ? page - 1 : 0);
result.put("fansApiUrl", ctx.fansApiBaseUrl());
return result;
}
}
private Map<String, Object> saveFanFromShareRow(Map<String, Object> row,
String tenantId,
Long parentId,
String parentName,
String parentPhone) {
Map<String, Object> saveResult = new LinkedHashMap<>();
Long fanId = parseLong(row.get("id"));
if (fanId == null) {
saveResult.put("saved", false);
saveResult.put("saveMsg", "粉丝 id 为空");
return saveResult;
}
LbFanManagement entity = new LbFanManagement();
entity.setTenantId(tenantId);
entity.setId(fanId);
entity.setIdTenantId(buildIdTenantId(fanId, tenantId));
entity.setAvatar(parseString(row.get("avatar")));
entity.setNickname(parseString(row.get("nickname")));
entity.setMobile(parseString(row.get("mobile")));
entity.setPid(parentId);
entity.setPName(parentName);
entity.setPPhone(parentPhone);
entity.setCoupon(parseDecimal(row.get("coupon")));
entity.setSelfBonus(parseDecimal(row.get("self_bonus")));
entity.setShareBonus(parseDecimal(row.get("share_bonus")));
Integer shareNum = parseInteger(row.get("share_num"));
if (shareNum != null) {
entity.setShareNum(shareNum);
}
trimStringFields(entity);
LbFanManagement existing = this.getById(entity.getIdTenantId());
if (existing != null) {
entity.setCreatedAt(existing.getCreatedAt());
if (entity.getShareNum() == null) {
entity.setShareNum(existing.getShareNum());
}
if (entity.getCoupon() == null) {
entity.setCoupon(existing.getCoupon());
}
if (entity.getSelfBonus() == null) {
entity.setSelfBonus(existing.getSelfBonus());
}
if (entity.getShareBonus() == null) {
entity.setShareBonus(existing.getShareBonus());
}
if (entity.getToken() == null) {
entity.setToken(existing.getToken());
}
boolean ok = this.updateById(entity);
saveResult.put("saved", ok);
saveResult.put("saveMsg", ok ? "已更新" : "更新失败");
return saveResult;
}
if (entity.getShareNum() == null) {
entity.setShareNum(0);
}
if (entity.getCoupon() == null) {
entity.setCoupon(BigDecimal.ZERO);
}
if (entity.getSelfBonus() == null) {
entity.setSelfBonus(BigDecimal.ZERO);
}
if (entity.getShareBonus() == null) {
entity.setShareBonus(BigDecimal.ZERO);
}
entity.setCreatedAt(LocalDateTime.now());
try {
boolean ok = this.save(entity);
saveResult.put("saved", ok);
saveResult.put("saveMsg", ok ? "已新增" : "新增失败");
} catch (DuplicateKeyException e) {
log.warn("粉丝拉取入库重复键, idTenantId={}", entity.getIdTenantId());
saveResult.put("saved", false);
saveResult.put("saveMsg", "唯一ID已存在" + entity.getIdTenantId());
}
return saveResult;
}
private static Integer parseInteger(Object value) {
if (value == null) {
return null;
}
if (value instanceof Number number) {
return number.intValue();
}
String text = value.toString().trim();
if (text.isEmpty()) {
return null;
}
try {
return Integer.parseInt(text);
} catch (NumberFormatException e) {
return null;
}
}
private static Long parseLong(Object value) {
if (value == null) {
return null;
}
if (value instanceof Number number) {
return number.longValue();
}
String text = value.toString().trim();
if (text.isEmpty()) {
return null;
}
try {
return Long.parseLong(text);
} catch (NumberFormatException e) {
return null;
}
}
private static String parseString(Object value) {
if (value == null) {
return null;
}
String text = value.toString().trim();
return text.isEmpty() ? null : text;
}
private static BigDecimal parseDecimal(Object value) {
if (value == null) {
return null;
}
if (value instanceof BigDecimal decimal) {
return decimal;
}
if (value instanceof Number number) {
return BigDecimal.valueOf(number.doubleValue());
}
String text = value.toString().trim();
if (text.isEmpty()) {
return null;
}
try {
return new BigDecimal(text);
} catch (NumberFormatException e) {
return null;
}
}
}

View File

@@ -34,7 +34,7 @@ import java.util.Optional;
@RequiredArgsConstructor
public class LbGoodsServiceImpl extends ServiceImpl<LbGoodsMapper, LbGoods> implements ILbGoodsService {
private static final BigDecimal RUSH_BUY_MIN_TOTAL_MONEY = new BigDecimal("26000");
private static final BigDecimal RUSH_BUY_MIN_TOTAL_MONEY = new BigDecimal("25000");
private static final DateTimeFormatter DATETIME_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@@ -569,6 +569,7 @@ public class LbGoodsServiceImpl extends ServiceImpl<LbGoodsMapper, LbGoods> impl
int attemptCount = 0;
boolean stoppedByMax = false;
boolean stoppedByMaxAttempts = false;
boolean stoppedByDailyLimit = false;
for (LbGoods goods : goodsList) {
if (successCount >= maxBuyCount) {
@@ -605,19 +606,35 @@ public class LbGoodsServiceImpl extends ServiceImpl<LbGoodsMapper, LbGoods> impl
successCount++;
} else {
failCount++;
if (buyResult.dailyLimitExceeded()) {
stoppedByDailyLimit = true;
if (accountTag.isEmpty()) {
log.info("当日可抢订单数已满,停止继续抢购");
} else {
log.info("{} 当日可抢订单数已满,停止继续抢购", accountTag);
}
break;
}
}
}
result.put("success", successCount > 0);
result.put("message", successCount > 0
? "抢购完成,成功 " + successCount + " 笔,失败 " + failCount + ""
: "抢购成功");
if (stoppedByDailyLimit) {
result.put("message", successCount > 0
? "当日可抢订单数已满,已停止抢购成功 " + successCount + " 笔,失败 " + failCount + ""
: HxrAdminBuyService.MSG_DAILY_LIMIT_EXCEEDED);
} else {
result.put("message", successCount > 0
? "抢购完成,成功 " + successCount + " 笔,失败 " + failCount + ""
: "抢购未成功");
}
result.put("successCount", successCount);
result.put("failCount", failCount);
result.put("attemptCount", attemptCount);
result.put("maxAttempts", maxAttempts);
result.put("stoppedByMax", stoppedByMax);
result.put("stoppedByMaxAttempts", stoppedByMaxAttempts);
result.put("stoppedByDailyLimit", stoppedByDailyLimit);
result.put("details", details);
if (accountTag.isEmpty()) {
log.info("抢购结束success={}message={}", successCount > 0, result.get("message"));

View File

@@ -8,6 +8,7 @@ import com.rj.common.LbThirdIntegrationConstants;
import com.rj.dto.LbThirdIntegrationCredentialUpdateRequest;
import com.rj.dto.hxr.HxrAdminOrderApiContext;
import com.rj.dto.hxr.HxrAdminUserApiContext;
import com.rj.dto.hxr.HxrFansApiContext;
import com.rj.dto.hxr.HxrGoodsApiContext;
import com.rj.dto.hxr.HxrUserLoginApiContext;
import com.rj.entity.LbThirdIntegrationConfig;
@@ -397,6 +398,37 @@ public class LbThirdIntegrationConfigServiceImpl
}
}
@Override
public Optional<HxrFansApiContext> resolveFansApiContext(String tenantId, String token) {
if (!StringUtils.hasText(tenantId) || !StringUtils.hasText(token)) {
return Optional.empty();
}
LbThirdIntegrationConfig config = getByTenantIdOrNull(tenantId.trim());
if (config == null) {
return Optional.empty();
}
if (config.getEnabled() == null || config.getEnabled() != 1) {
return Optional.empty();
}
String appStr = config.getGoodsApiAppStr();
if (!StringUtils.hasText(appStr)) {
return Optional.empty();
}
try {
return Optional.of(new HxrFansApiContext(
LbThirdIntegrationConfigUtil.resolveFansApiBaseUrl(config),
LbThirdIntegrationConfigUtil.resolveFansPageLimit(config),
LbThirdIntegrationConfigUtil.resolveGoodsApiOrigin(config),
LbThirdIntegrationConfigUtil.resolveGoodsApiReferer(config),
token.trim(),
appStr.trim()));
} catch (IllegalStateException e) {
return Optional.empty();
}
}
@Override
public Optional<HxrAdminOrderApiContext> resolveAdminOrderApiContext(String tenantId) {
if (!StringUtils.hasText(tenantId)) {

View File

@@ -13,6 +13,9 @@ CREATE TABLE `lb_fan_management` (
`pid` BIGINT NULL DEFAULT NULL COMMENT '父ID',
`p_phone` VARCHAR(32) NULL DEFAULT NULL COMMENT '父手机号',
`token` VARCHAR(512) NULL DEFAULT NULL COMMENT '登录 token',
`coupon` DECIMAL(12,3) NOT NULL DEFAULT 0.000 COMMENT '优惠券金额',
`self_bonus` DECIMAL(12,3) NOT NULL DEFAULT 0.000 COMMENT '自购奖金',
`share_bonus` DECIMAL(12,3) NOT NULL DEFAULT 0.000 COMMENT '分享奖金',
`created_at` DATETIME NULL DEFAULT NULL COMMENT '创建时间',
`share_num` INT NOT NULL DEFAULT 0 COMMENT '分享次数',
PRIMARY KEY (`id_tenant_id`) USING BTREE,