fix(order): 解决送水订单完成后同步商城订单状态及关联记录数回填问题

- 在送水订单完成、确认收货和超时自动确认收货时同步更新关联商城订单状态为已完成
- 添加updateShopOrderOrderStatusAfterTicketFinished方法处理商城订单状态同步逻辑
- 在CreditCompanyRecordCountService中优化SQL查询方式,避免相关子查询导致的性能问题
- 将批量更新的chunk大小从1000调整为500以提高稳定性
- 在导入功能中添加异常处理,确保即使回填失败也不会影响整体导入流程
- 当关联记录数回填失败时在响应消息中添加警告信息
This commit is contained in:
2026-02-10 12:52:27 +08:00
parent dd023bd2ca
commit 0fc914f47a
3 changed files with 110 additions and 9 deletions

View File

@@ -149,6 +149,7 @@ public class CreditCompanyController extends BaseController {
List<String> errorMessages = new ArrayList<>();
int insertedCount = 0;
Set<String> touchedMatchNames = new HashSet<>();
String refreshWarning = null;
try {
List<CreditCompanyImportParam> list = null;
@@ -324,13 +325,31 @@ public class CreditCompanyController extends BaseController {
}
}
}
creditCompanyRecordCountService.refreshAll(touchedCompanyIds);
try {
creditCompanyRecordCountService.refreshAll(touchedCompanyIds);
} catch (Exception ex) {
// 导入本身已经成功写入,回填计数字段失败不应导致整个导入失败(可后续单独重试刷新)。
String msg = ex.getMessage();
if (msg != null && msg.length() > 300) {
msg = msg.substring(0, 300) + "...";
}
refreshWarning = "关联记录数回填失败:" + (msg != null ? msg : ex.getClass().getSimpleName());
ex.printStackTrace();
}
}
if (errorMessages.isEmpty()) {
return success("成功入库" + insertedCount + "条数据", null);
String msg = "成功入库" + insertedCount + "条数据";
if (refreshWarning != null) {
msg = msg + "" + refreshWarning;
}
return success(msg, null);
} else {
return success("导入完成,入库" + insertedCount + "条,失败" + errorMessages.size() + "", errorMessages);
String msg = "导入完成,入库" + insertedCount + "条,失败" + errorMessages.size() + "";
if (refreshWarning != null) {
msg = msg + "" + refreshWarning;
}
return success(msg, errorMessages);
}
} catch (Exception e) {

View File

@@ -82,16 +82,24 @@ public class CreditCompanyRecordCountService {
}
// 表/字段名来自固定枚举,不允许外部传入,避免 SQL 注入。
//
// 这里不要用「UPDATE ... SET col=(SELECT COUNT... WHERE t.company_id=c.id)」的相关子查询:
// 如果关联表缺少 (company_id, deleted) 索引,会导致对每个 company_id 都进行一次全表扫描,极慢,
// 进而容易触发 JDBC/MySQL 侧的超时/断链Communications link failure
//
// 用聚合后再 JOIN 的方式:每个 chunk 只扫描一次关联表。
String sql = ""
+ "UPDATE credit_company c "
+ "SET " + type.getCompanyColumn() + " = ("
+ " SELECT COUNT(1) "
+ " FROM " + type.getSourceTable() + " t "
+ " WHERE t.company_id = c.id AND t.deleted = 0"
+ ") "
+ "LEFT JOIN ("
+ " SELECT company_id, COUNT(1) AS cnt "
+ " FROM " + type.getSourceTable() + " "
+ " WHERE deleted = 0 AND company_id IN (:ids) "
+ " GROUP BY company_id"
+ ") t ON t.company_id = c.id "
+ "SET c." + type.getCompanyColumn() + " = IFNULL(t.cnt, 0) "
+ "WHERE c.id IN (:ids)";
final int inChunkSize = 1000;
final int inChunkSize = 500;
for (int i = 0; i < ids.size(); i += inChunkSize) {
List<Integer> chunk = ids.subList(i, Math.min(ids.size(), i + inChunkSize));
namedJdbc.update(sql, new MapSqlParameterSource("ids", chunk));

View File

@@ -459,6 +459,8 @@ public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper,
if (ok) {
// 用户确认收货完成后触发配送员提成结算(幂等,重复调用不会重复入账)
settleRiderCommissionIfEligible(id, tenantId, false);
// 送水订单完成后,同步商城订单为“已完成”(orderStatus=1)
updateShopOrderOrderStatusAfterTicketFinished(id, tenantId, now);
return;
}
@@ -476,6 +478,7 @@ public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper,
if (order.getDeliveryStatus() != null && order.getDeliveryStatus() == DELIVERY_STATUS_FINISHED) {
// 幂等:重复确认收货视为成功
settleRiderCommissionIfEligible(id, tenantId, false);
updateShopOrderOrderStatusAfterTicketFinished(id, tenantId, LocalDateTime.now());
return;
}
throw new BusinessException("订单状态不允许确认收货");
@@ -534,6 +537,7 @@ public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper,
}
// 超时自动确认收货后,也按“完成”逻辑触发配送员提成结算(幂等)。
settleRiderCommissionIfEligible(id, tenantId, false);
updateShopOrderOrderStatusAfterTicketFinished(id, tenantId, nowFinal);
return true;
});
if (Boolean.TRUE.equals(ok)) {
@@ -546,6 +550,76 @@ public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper,
return confirmed;
}
private void updateShopOrderOrderStatusAfterTicketFinished(Integer ticketOrderId, Integer tenantId, LocalDateTime now) {
if (ticketOrderId == null || tenantId == null) {
return;
}
if (now == null) {
now = LocalDateTime.now();
}
// 找到关联水票的商城订单glt_user_ticket.orderId / orderNo
GltTicketOrder ticketOrder = this.lambdaQuery()
.select(GltTicketOrder::getId, GltTicketOrder::getUserTicketId)
.eq(GltTicketOrder::getId, ticketOrderId)
.eq(GltTicketOrder::getTenantId, tenantId)
.eq(GltTicketOrder::getDeleted, 0)
.last("limit 1")
.one();
if (ticketOrder == null || ticketOrder.getUserTicketId() == null) {
return;
}
GltUserTicket userTicket = gltUserTicketService.getOne(
new LambdaQueryWrapper<GltUserTicket>()
.eq(GltUserTicket::getTenantId, tenantId)
.eq(GltUserTicket::getDeleted, 0)
.eq(GltUserTicket::getId, ticketOrder.getUserTicketId())
.last("limit 1")
);
if (userTicket == null) {
return;
}
Integer shopOrderId = userTicket.getOrderId();
String shopOrderNo = userTicket.getOrderNo();
if (shopOrderId == null && !StringUtils.hasText(shopOrderNo)) {
return;
}
LambdaUpdateWrapper<ShopOrder> uw = new LambdaUpdateWrapper<ShopOrder>()
.eq(ShopOrder::getTenantId, tenantId)
.eq(ShopOrder::getDeleted, 0)
.and(w -> w.ne(ShopOrder::getOrderStatus, 1).or().isNull(ShopOrder::getOrderStatus))
.set(ShopOrder::getOrderStatus, 1)
.set(ShopOrder::getUpdateTime, now);
if (shopOrderId != null) {
uw.eq(ShopOrder::getOrderId, shopOrderId);
} else {
uw.eq(ShopOrder::getOrderNo, shopOrderNo);
}
boolean updated = shopOrderService.update(uw);
if (updated) {
return;
}
// 幂等:若已是 1则视为成功否则记录日志便于排查关联关系/数据缺失
LambdaQueryWrapper<ShopOrder> qw = new LambdaQueryWrapper<ShopOrder>()
.eq(ShopOrder::getTenantId, tenantId)
.eq(ShopOrder::getDeleted, 0)
.eq(ShopOrder::getOrderStatus, 1);
if (shopOrderId != null) {
qw.eq(ShopOrder::getOrderId, shopOrderId);
} else {
qw.eq(ShopOrder::getOrderNo, shopOrderNo);
}
if (shopOrderService.count(qw) <= 0) {
log.warn("送水订单完成但同步商城订单完成状态失败 - tenantId={}, ticketOrderId={}, shopOrderId={}, shopOrderNo={}",
tenantId, ticketOrderId, shopOrderId, shopOrderNo);
}
}
private void settleRiderCommissionIfEligible(Integer ticketOrderId, Integer tenantId, boolean requirePhoto) {
if (ticketOrderId == null || tenantId == null) {
return;