1
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
+256
-46
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user