diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelMidServiceImpl.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelMidServiceImpl.java
index 0458dafa..b8a99801 100644
--- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelMidServiceImpl.java
+++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelMidServiceImpl.java
@@ -2855,9 +2855,40 @@ public class SmHotelMidServiceImpl implements SmHotelMidService {
*
* 唯一键 uk_product_date=(tenant_id, product_id, stay_date) 不含 del_flag,必须物理删。
* 删除区间须覆盖本次将写入的日期(含回调报文里的历史日),不能写死 LocalDate.now()。
+ *
+ * 全量详情多酒店并行时,同租户唯一索引上易出现死锁,失败自动重试几次。
*/
private int persistDailies(SmHotelEntity hotel, List drafts, LocalDate fromInclusive,
LocalDate toExclusive) {
+ final int maxAttempts = 4;
+ for (int attempt = 1; attempt <= maxAttempts; attempt++) {
+ try {
+ return persistDailiesOnce(hotel, drafts, fromInclusive, toExclusive);
+ }
+ catch (Exception e) {
+ if (!isMysqlDeadlock(e) || attempt >= maxAttempts) {
+ throw e instanceof RuntimeException ? (RuntimeException) e
+ : new IllegalStateException(e.getMessage(), e);
+ }
+ long sleepMs = 80L * attempt + (long) (Math.random() * 120);
+ log.warn("|酒店日历落库|死锁重试|hotelId={}|code={}|attempt={}/{}|sleepMs={}|{}",
+ hotel == null ? null : hotel.getId(),
+ hotel == null ? null : hotel.getHotelCode(), attempt, maxAttempts, sleepMs,
+ StrUtil.maxLength(e.getMessage(), 160));
+ try {
+ Thread.sleep(sleepMs);
+ }
+ catch (InterruptedException ie) {
+ Thread.currentThread().interrupt();
+ throw new IllegalStateException("日历落库重试被中断", ie);
+ }
+ }
+ }
+ return 0;
+ }
+
+ private int persistDailiesOnce(SmHotelEntity hotel, List drafts, LocalDate fromInclusive,
+ LocalDate toExclusive) {
LocalDate delFrom = fromInclusive;
LocalDate delTo = toExclusive;
if (delFrom == null || delTo == null || !delFrom.isBefore(delTo)) {
@@ -2933,6 +2964,23 @@ public class SmHotelMidServiceImpl implements SmHotelMidService {
return toInsert.size();
}
+ /** 并发拉详情时,日历表 uk_product_date 易出现 InnoDB 死锁 */
+ private static boolean isMysqlDeadlock(Throwable e) {
+ Throwable cur = e;
+ while (cur != null) {
+ String msg = cur.getMessage();
+ if (msg != null && (msg.contains("Deadlock") || msg.contains("try restarting transaction"))) {
+ return true;
+ }
+ String name = cur.getClass().getName();
+ if (name.contains("MySQLTransactionRollbackException") || name.contains("DeadlockLoser")) {
+ return true;
+ }
+ cur = cur.getCause();
+ }
+ return false;
+ }
+
private Map loadRoomMap(Long hotelId) {
List rows = roomMapper.selectList(Wrappers.lambdaQuery()
.eq(SmHotelRoomEntity::getHotelId, hotelId));
diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelSyncServiceImpl.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelSyncServiceImpl.java
index 111bac76..84987a7b 100644
--- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelSyncServiceImpl.java
+++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelSyncServiceImpl.java
@@ -2023,8 +2023,20 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService {
return;
}
String code = StrUtil.blankToDefault(h.getHotelCode(), String.valueOf(h.getId()));
- String name = StrUtil.blankToDefault(h.getHotelName(), "-");
- failLines.add(code + "|" + name + "|" + StrUtil.maxLength(StrUtil.blankToDefault(reason, "未知错误"), 300));
+ String name = StrUtil.blankToDefault(h.getHotelName(), "-")
+ .replace('|', '/')
+ .replace('\r', ' ')
+ .replace('\n', ' ');
+ String raw = StrUtil.blankToDefault(reason, "未知错误");
+ // MyBatis 堆栈含换行会把导出 Excel 拆成多行;死锁写成短文案便于重试
+ String safe;
+ if (raw.contains("Deadlock") || raw.contains("try restarting transaction")) {
+ safe = "日历落库死锁(可点失败重试)";
+ }
+ else {
+ safe = raw.replace('\r', ' ').replace('\n', ' ').replace('|', '/');
+ }
+ failLines.add(code + "|" + name + "|" + StrUtil.maxLength(safe, 300));
}
private static void applyFailDetailOnly(SmHotelSyncLogEntity done, List failLines, int fail) {