This commit is contained in:
van
2026-09-29 14:55:53 +08:00
parent 4e4def7f72
commit 459540819d
5 changed files with 585 additions and 46 deletions
@@ -71,4 +71,19 @@ public class JarvisGoofishProperties {
/** 与 defaultShipExpressCode 配套的展示名称 */
private String defaultShipExpressName = "日日顺";
/**
* 两次发货接口调用的最小间隔(毫秒)。开放平台对连续发货会返回「操作频繁」。
*/
private long shipMinIntervalMs = 2000L;
/**
* 发货遇到「操作频繁」、超时等可恢复错误时的额外重试次数(不含首次)。
*/
private int shipRetryTimes = 3;
/**
* 发货重试基础等待(毫秒)。第 n 次重试等待 n × 该值。
*/
private long shipRetryDelayMs = 2000L;
}
@@ -29,6 +29,7 @@ import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/**
* 闲管家:推送/拉单后的落库、详情、关联京东单、同步运单、发货
*/
@@ -39,6 +40,15 @@ public class GoofishOrderPipeline {
private static final String REDIS_WAYBILL_KEY_PREFIX = "logistics:waybill:order:";
/** F-闲鱼等京东单:运单可按第三方单号=闲鱼单号写入 Redis,见 LogisticsServiceImpl 镜像写入 */
public static final String REDIS_WAYBILL_GOOFISH_ORDER_PREFIX = "logistics:waybill:goofish:";
/** 同一闲鱼单发货互斥,避免定时扫描与京东运单回调同时打出发货 */
private static final String REDIS_SHIP_ORDER_LOCK_PREFIX = "goofish:ship:lock:";
/** 全局发货槽:同一时刻只允许一笔发货请求在途,结束后再保留最小间隔 */
private static final String REDIS_SHIP_GLOBAL_LOCK = "goofish:ship:global";
/** 发货扫描互斥,避免拉单后扫描与定时扫描叠在一起 */
private static final String REDIS_SHIP_BATCH_LOCK = "goofish:ship:batch";
/** 可恢复失败后的短冷却,避免同一轮扫描立刻再打一次 */
private static final String REDIS_SHIP_COOLDOWN_PREFIX = "goofish:ship:cooldown:";
private static final long SHIP_FAIL_COOLDOWN_SECONDS = 30L;
@Resource
private ErpGoofishOrderMapper erpGoofishOrderMapper;
@@ -565,34 +575,7 @@ public class GoofishOrderPipeline {
ship.setWaybillNo(waybill.trim());
ship.setExpressCode(expressCode);
ship.setExpressName(expressName);
String resp = ship.getResponseBody();
JSONObject r = JSON.parseObject(resp);
if (r != null && r.getIntValue("code") == 0) {
ErpGoofishOrder ok = new ErpGoofishOrder();
ok.setId(row.getId());
ok.setShipStatus(1);
ok.setShipTime(DateUtils.getNowDate());
ok.setShipExpressCode(expressCode);
ok.setShipError(null);
ok.setUpdateTime(DateUtils.getNowDate());
erpGoofishOrderMapper.update(ok);
row.setShipStatus(1);
if (goofishOrderChangeLogger != null) {
goofishOrderChangeLogger.append(row.getId(), row.getAppKey(), row.getOrderNo(),
GoofishOrderChangeLogger.TYPE_SHIP, "AUTO_SHIP",
"发货成功,运单 " + waybill.trim()
+ ",订单状态 " + GoofishStatusLabels.orderStatusHuman(row.getOrderStatus())
+ ",退款状态 " + GoofishStatusLabels.refundStatusHuman(row.getRefundStatus()));
}
} else {
String msg = r != null ? r.getString("msg") : "unknown";
patchShipError(row, msg != null ? msg : "发货接口返回失败");
if (goofishOrderChangeLogger != null) {
goofishOrderChangeLogger.append(row.getId(), row.getAppKey(), row.getOrderNo(),
GoofishOrderChangeLogger.TYPE_SHIP, "AUTO_SHIP",
"发货失败:" + (row.getShipError() != null ? row.getShipError() : msg));
}
}
submitShip(row, ship, expressCode, waybill.trim(), manualRetry);
} catch (Exception ex) {
patchShipError(row, ex.getMessage());
if (goofishOrderChangeLogger != null) {
@@ -604,6 +587,220 @@ public class GoofishOrderPipeline {
}
}
/**
* 同一订单只允许一路发货;全局再串行调用开放平台,遇到「操作频繁」等可恢复错误按间隔重试。
*/
private void submitShip(ErpGoofishOrder row, OrderShipRequest ship, String expressCode, String waybill,
boolean manualRetry) {
if (!manualRetry && inShipCooldown(row.getId())) {
log.info("闲管家发货冷却中,跳过本轮 orderNo={}", row.getOrderNo());
return;
}
String orderLock = REDIS_SHIP_ORDER_LOCK_PREFIX + row.getId();
Boolean got = stringRedisTemplate.opsForValue().setIfAbsent(orderLock, "1", 180, TimeUnit.SECONDS);
if (!Boolean.TRUE.equals(got)) {
log.info("闲管家发货跳过:该单正在发货 orderNo={}", row.getOrderNo());
return;
}
try {
ErpGoofishOrder fresh = erpGoofishOrderMapper.selectById(row.getId());
if (fresh != null && fresh.getShipStatus() != null && fresh.getShipStatus() == 1) {
row.setShipStatus(1);
return;
}
int maxAttempts = Math.max(1, goofishProperties.getShipRetryTimes() + 1);
String lastMsg = null;
for (int attempt = 1; attempt <= maxAttempts; attempt++) {
String resp;
try {
resp = callShipSerialized(ship);
} catch (Exception ex) {
lastMsg = ex.getMessage() == null ? ex.toString() : ex.getMessage();
log.warn("闲管家发货调用异常 orderNo={} attempt={}/{} {}", row.getOrderNo(), attempt, maxAttempts, lastMsg);
if (attempt < maxAttempts && sleepBeforeShipRetry(attempt)) {
continue;
}
patchShipError(row, lastMsg);
appendShipLog(row, "发货异常:" + (row.getShipError() != null ? row.getShipError() : lastMsg));
armShipCooldown(row.getId());
return;
}
JSONObject r;
try {
r = StringUtils.isEmpty(resp) ? null : JSON.parseObject(resp);
} catch (Exception parseEx) {
r = null;
}
if (r != null && r.getIntValue("code") == 0) {
markShipSuccess(row, expressCode, waybill);
return;
}
lastMsg = shipFailureMessage(r, resp);
if (isAlreadyShippedMessage(lastMsg)) {
log.info("闲管家发货接口表示已发货,按成功处理 orderNo={} msg={}", row.getOrderNo(), lastMsg);
markShipSuccess(row, expressCode, waybill);
return;
}
if (attempt < maxAttempts && isRetryableShipFailure(r, resp, lastMsg)) {
log.warn("闲管家发货将重试 orderNo={} attempt={}/{} msg={}", row.getOrderNo(), attempt, maxAttempts, lastMsg);
if (sleepBeforeShipRetry(attempt)) {
continue;
}
}
patchShipError(row, lastMsg);
appendShipLog(row, "发货失败:" + (row.getShipError() != null ? row.getShipError() : lastMsg));
if (isRetryableShipFailure(r, resp, lastMsg)) {
armShipCooldown(row.getId());
}
return;
}
} finally {
try {
stringRedisTemplate.delete(orderLock);
} catch (Exception e) {
log.debug("释放闲管家发货锁失败 orderNo={} {}", row.getOrderNo(), e.getMessage());
}
}
}
/**
* 全局只放行一笔发货请求。调用结束后把锁缩短为最小间隔,下一笔必须等这段时间过去。
*/
private String callShipSerialized(OrderShipRequest ship) {
long intervalMs = goofishProperties.getShipMinIntervalMs();
long deadline = System.currentTimeMillis() + 90_000L;
boolean locked = false;
while (System.currentTimeMillis() < deadline) {
Boolean ok = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_SHIP_GLOBAL_LOCK, "1", 45, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(ok)) {
locked = true;
break;
}
if (!sleepQuiet(250L)) {
throw new IllegalStateException("发货排队被中断");
}
}
if (!locked) {
throw new IllegalStateException("发货排队超时,避免并发触发操作频繁");
}
try {
return ship.getResponseBody();
} finally {
try {
if (intervalMs <= 0) {
stringRedisTemplate.delete(REDIS_SHIP_GLOBAL_LOCK);
} else {
long seconds = Math.max(1L, (intervalMs + 999L) / 1000L);
stringRedisTemplate.expire(REDIS_SHIP_GLOBAL_LOCK, seconds, TimeUnit.SECONDS);
}
} catch (Exception e) {
try {
stringRedisTemplate.delete(REDIS_SHIP_GLOBAL_LOCK);
} catch (Exception ignore) {
log.debug("释放闲管家发货全局锁失败 {}", e.getMessage());
}
}
}
}
private void markShipSuccess(ErpGoofishOrder row, String expressCode, String waybill) {
ErpGoofishOrder ok = new ErpGoofishOrder();
ok.setId(row.getId());
ok.setShipStatus(1);
ok.setShipTime(DateUtils.getNowDate());
ok.setShipExpressCode(expressCode);
ok.setShipError("");
ok.setUpdateTime(DateUtils.getNowDate());
erpGoofishOrderMapper.update(ok);
row.setShipStatus(1);
row.setShipError(null);
appendShipLog(row, "发货成功,运单 " + waybill
+ ",订单状态 " + GoofishStatusLabels.orderStatusHuman(row.getOrderStatus())
+ ",退款状态 " + GoofishStatusLabels.refundStatusHuman(row.getRefundStatus()));
}
private void appendShipLog(ErpGoofishOrder row, String message) {
if (goofishOrderChangeLogger == null || row == null) {
return;
}
goofishOrderChangeLogger.append(row.getId(), row.getAppKey(), row.getOrderNo(),
GoofishOrderChangeLogger.TYPE_SHIP, "AUTO_SHIP", message);
}
private static String shipFailureMessage(JSONObject r, String resp) {
if (r != null && StringUtils.isNotEmpty(r.getString("msg"))) {
return r.getString("msg");
}
if (StringUtils.isEmpty(resp)) {
return "发货接口无响应";
}
return "发货接口返回失败";
}
private static boolean isAlreadyShippedMessage(String msg) {
return StringUtils.isNotEmpty(msg) && (msg.contains("已发货") || msg.contains("重复发货"));
}
private static boolean isRetryableShipFailure(JSONObject r, String resp, String msg) {
if (r == null || StringUtils.isEmpty(resp)) {
return true;
}
return isRetryableShipMessage(msg);
}
private static boolean isRetryableShipMessage(String msg) {
if (StringUtils.isEmpty(msg)) {
return true;
}
String m = msg.toLowerCase();
return m.contains("频繁") || m.contains("稍后") || m.contains("繁忙")
|| m.contains("限流") || m.contains("超时")
|| m.contains("too many") || m.contains("rate limit") || m.contains("timeout");
}
private boolean sleepBeforeShipRetry(int attempt) {
long base = Math.max(200L, goofishProperties.getShipRetryDelayMs());
long delay = base * Math.max(1, attempt);
log.info("闲管家发货重试等待 {} ms", delay);
return sleepQuiet(delay);
}
private void armShipCooldown(Long orderId) {
if (orderId == null) {
return;
}
try {
stringRedisTemplate.opsForValue().set(REDIS_SHIP_COOLDOWN_PREFIX + orderId, "1",
SHIP_FAIL_COOLDOWN_SECONDS, TimeUnit.SECONDS);
} catch (Exception e) {
log.debug("写入闲管家发货冷却失败 id={} {}", orderId, e.getMessage());
}
}
private boolean inShipCooldown(Long orderId) {
if (orderId == null) {
return false;
}
try {
return Boolean.TRUE.equals(stringRedisTemplate.hasKey(REDIS_SHIP_COOLDOWN_PREFIX + orderId));
} catch (Exception e) {
return false;
}
}
private static boolean sleepQuiet(long millis) {
if (millis <= 0) {
return true;
}
try {
Thread.sleep(millis);
return true;
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
return false;
}
}
private void patchShipError(ErpGoofishOrder row, String err) {
ErpGoofishOrder p = new ErpGoofishOrder();
p.setId(row.getId());
@@ -1197,30 +1394,43 @@ public class GoofishOrderPipeline {
}
public int syncWaybillAndTryShipBatch(int limit) {
List<Integer> statuses = resolveAutoShipOrderStatuses();
List<ErpGoofishOrder> rows = erpGoofishOrderMapper.selectPendingShip(statuses, limit);
if (rows == null) {
Boolean got = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_SHIP_BATCH_LOCK, "1", 10, TimeUnit.MINUTES);
if (!Boolean.TRUE.equals(got)) {
log.info("闲管家发货扫描已在进行,跳过本轮");
return 0;
}
int n = 0;
for (ErpGoofishOrder row : rows) {
ErpGoofishOrder full = erpGoofishOrderMapper.selectById(row.getId());
if (full == null) {
continue;
try {
List<Integer> statuses = resolveAutoShipOrderStatuses();
List<ErpGoofishOrder> rows = erpGoofishOrderMapper.selectPendingShip(statuses, limit);
if (rows == null) {
return 0;
}
tryLinkJdOrder(full);
syncWaybillFromRedis(full);
if ((full.getRefundStatus() == null || full.getRefundStatus() == 0)
&& statuses.contains(full.getOrderStatus())
&& StringUtils.isEmpty(full.getLocalWaybillNo())
&& StringUtils.isEmpty(full.getDetailWaybillNo())) {
refreshDetail(full);
int n = 0;
for (ErpGoofishOrder row : rows) {
ErpGoofishOrder full = erpGoofishOrderMapper.selectById(row.getId());
if (full == null) {
continue;
}
tryLinkJdOrder(full);
syncWaybillFromRedis(full);
if ((full.getRefundStatus() == null || full.getRefundStatus() == 0)
&& statuses.contains(full.getOrderStatus())
&& StringUtils.isEmpty(full.getLocalWaybillNo())
&& StringUtils.isEmpty(full.getDetailWaybillNo())) {
refreshDetail(full);
syncWaybillFromRedis(full);
}
tryAutoShip(full);
n++;
}
return n;
} finally {
try {
stringRedisTemplate.delete(REDIS_SHIP_BATCH_LOCK);
} catch (Exception e) {
log.debug("释放闲管家发货扫描锁失败 {}", e.getMessage());
}
tryAutoShip(full);
n++;
}
return n;
}
}