7 changed files with 688 additions and 0 deletions
@ -0,0 +1,65 @@ |
|||
package com.gxwebsoft.shop.service; |
|||
|
|||
import com.gxwebsoft.shop.entity.ShopOrder; |
|||
|
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 订单取消服务接口 |
|||
* |
|||
* @author WebSoft |
|||
* @since 2025-01-26 |
|||
*/ |
|||
public interface OrderCancelService { |
|||
|
|||
/** |
|||
* 取消单个订单 |
|||
* |
|||
* @param order 订单对象 |
|||
* @return 是否取消成功 |
|||
*/ |
|||
boolean cancelOrder(ShopOrder order); |
|||
|
|||
/** |
|||
* 批量取消订单 |
|||
* |
|||
* @param orders 订单列表 |
|||
* @return 成功取消的订单数量 |
|||
*/ |
|||
int batchCancelOrders(List<ShopOrder> orders); |
|||
|
|||
/** |
|||
* 查找超时的待付款订单 |
|||
* |
|||
* @param timeoutMinutes 超时时间(分钟) |
|||
* @param batchSize 批量大小 |
|||
* @return 超时订单列表 |
|||
*/ |
|||
List<ShopOrder> findExpiredUnpaidOrders(Integer timeoutMinutes, Integer batchSize); |
|||
|
|||
/** |
|||
* 查找指定租户的超时订单 |
|||
* |
|||
* @param tenantId 租户ID |
|||
* @param timeoutMinutes 超时时间(分钟) |
|||
* @param batchSize 批量大小 |
|||
* @return 超时订单列表 |
|||
*/ |
|||
List<ShopOrder> findExpiredUnpaidOrdersByTenant(Integer tenantId, Integer timeoutMinutes, Integer batchSize); |
|||
|
|||
/** |
|||
* 回退订单库存 |
|||
* |
|||
* @param order 订单对象 |
|||
* @return 是否回退成功 |
|||
*/ |
|||
boolean restoreOrderStock(ShopOrder order); |
|||
|
|||
/** |
|||
* 退还订单优惠券 |
|||
* |
|||
* @param order 订单对象 |
|||
* @return 是否退还成功 |
|||
*/ |
|||
boolean returnOrderCoupon(ShopOrder order); |
|||
} |
@ -0,0 +1,226 @@ |
|||
package com.gxwebsoft.shop.service.impl; |
|||
|
|||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
|||
import com.gxwebsoft.shop.entity.*; |
|||
import com.gxwebsoft.shop.service.*; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.stereotype.Service; |
|||
import org.springframework.transaction.annotation.Transactional; |
|||
|
|||
import java.time.LocalDateTime; |
|||
import java.util.ArrayList; |
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 订单取消服务实现 |
|||
* |
|||
* @author WebSoft |
|||
* @since 2025-01-26 |
|||
*/ |
|||
@Slf4j |
|||
@Service |
|||
public class OrderCancelServiceImpl implements OrderCancelService { |
|||
|
|||
@Autowired |
|||
private ShopOrderService shopOrderService; |
|||
|
|||
@Autowired |
|||
private ShopOrderGoodsService shopOrderGoodsService; |
|||
|
|||
@Autowired |
|||
private ShopGoodsService shopGoodsService; |
|||
|
|||
@Autowired |
|||
private ShopGoodsSkuService shopGoodsSkuService; |
|||
|
|||
@Autowired |
|||
private CouponStatusService couponStatusService; |
|||
|
|||
@Override |
|||
@Transactional(rollbackFor = Exception.class) |
|||
public boolean cancelOrder(ShopOrder order) { |
|||
try { |
|||
log.info("开始取消订单,订单号:{},订单ID:{}", order.getOrderNo(), order.getOrderId()); |
|||
|
|||
// 1. 检查订单状态
|
|||
if (order.getPayStatus() != null && order.getPayStatus()) { |
|||
log.warn("订单已支付,无法取消,订单号:{}", order.getOrderNo()); |
|||
return false; |
|||
} |
|||
|
|||
if (order.getOrderStatus() != null && order.getOrderStatus() != 0) { |
|||
log.warn("订单状态不是待支付,无法取消,订单号:{},当前状态:{}", order.getOrderNo(), order.getOrderStatus()); |
|||
return false; |
|||
} |
|||
|
|||
// 2. 更新订单状态为已取消
|
|||
order.setOrderStatus(2); // 2表示已取消
|
|||
order.setCancelTime(LocalDateTime.now()); |
|||
order.setCancelReason("系统自动取消(超时未支付)"); |
|||
|
|||
boolean updateSuccess = shopOrderService.updateById(order); |
|||
if (!updateSuccess) { |
|||
log.error("更新订单状态失败,订单号:{}", order.getOrderNo()); |
|||
return false; |
|||
} |
|||
|
|||
// 3. 回退库存
|
|||
boolean stockRestored = restoreOrderStock(order); |
|||
if (!stockRestored) { |
|||
log.error("回退库存失败,订单号:{}", order.getOrderNo()); |
|||
// 注意:这里不直接返回false,因为订单状态已经更新,需要记录错误但继续处理
|
|||
} |
|||
|
|||
// 4. 退还优惠券
|
|||
boolean couponReturned = returnOrderCoupon(order); |
|||
if (!couponReturned) { |
|||
log.error("退还优惠券失败,订单号:{}", order.getOrderNo()); |
|||
// 同样不直接返回false
|
|||
} |
|||
|
|||
log.info("订单取消成功,订单号:{},库存回退:{},优惠券退还:{}", |
|||
order.getOrderNo(), stockRestored, couponReturned); |
|||
return true; |
|||
|
|||
} catch (Exception e) { |
|||
log.error("取消订单失败,订单号:{}", order.getOrderNo(), e); |
|||
throw e; // 重新抛出异常,触发事务回滚
|
|||
} |
|||
} |
|||
|
|||
@Override |
|||
@Transactional(rollbackFor = Exception.class) |
|||
public int batchCancelOrders(List<ShopOrder> orders) { |
|||
if (orders == null || orders.isEmpty()) { |
|||
return 0; |
|||
} |
|||
|
|||
int successCount = 0; |
|||
for (ShopOrder order : orders) { |
|||
try { |
|||
if (cancelOrder(order)) { |
|||
successCount++; |
|||
} |
|||
} catch (Exception e) { |
|||
log.error("批量取消订单时发生错误,订单号:{}", order.getOrderNo(), e); |
|||
// 继续处理下一个订单
|
|||
} |
|||
} |
|||
|
|||
log.info("批量取消订单完成,总数:{},成功:{}", orders.size(), successCount); |
|||
return successCount; |
|||
} |
|||
|
|||
@Override |
|||
public List<ShopOrder> findExpiredUnpaidOrders(Integer timeoutMinutes, Integer batchSize) { |
|||
LocalDateTime expireTime = LocalDateTime.now().minusMinutes(timeoutMinutes); |
|||
|
|||
LambdaQueryWrapper<ShopOrder> queryWrapper = new LambdaQueryWrapper<ShopOrder>() |
|||
.eq(ShopOrder::getPayStatus, false) // 未支付
|
|||
.eq(ShopOrder::getOrderStatus, 0) // 待支付状态
|
|||
.lt(ShopOrder::getCreateTime, expireTime) // 创建时间小于过期时间
|
|||
.orderByAsc(ShopOrder::getCreateTime) |
|||
.last("LIMIT " + batchSize); |
|||
|
|||
return shopOrderService.list(queryWrapper); |
|||
} |
|||
|
|||
@Override |
|||
public List<ShopOrder> findExpiredUnpaidOrdersByTenant(Integer tenantId, Integer timeoutMinutes, Integer batchSize) { |
|||
LocalDateTime expireTime = LocalDateTime.now().minusMinutes(timeoutMinutes); |
|||
|
|||
LambdaQueryWrapper<ShopOrder> queryWrapper = new LambdaQueryWrapper<ShopOrder>() |
|||
.eq(ShopOrder::getTenantId, tenantId) |
|||
.eq(ShopOrder::getPayStatus, false) // 未支付
|
|||
.eq(ShopOrder::getOrderStatus, 0) // 待支付状态
|
|||
.lt(ShopOrder::getCreateTime, expireTime) // 创建时间小于过期时间
|
|||
.orderByAsc(ShopOrder::getCreateTime) |
|||
.last("LIMIT " + batchSize); |
|||
|
|||
return shopOrderService.list(queryWrapper); |
|||
} |
|||
|
|||
@Override |
|||
public boolean restoreOrderStock(ShopOrder order) { |
|||
try { |
|||
// 获取订单商品信息
|
|||
List<ShopOrderGoods> orderGoods = shopOrderGoodsService.list( |
|||
new LambdaQueryWrapper<ShopOrderGoods>() |
|||
.eq(ShopOrderGoods::getOrderId, order.getOrderId()) |
|||
); |
|||
|
|||
if (orderGoods == null || orderGoods.isEmpty()) { |
|||
log.warn("订单没有商品信息,订单号:{}", order.getOrderNo()); |
|||
return true; // 没有商品信息也算成功
|
|||
} |
|||
|
|||
for (ShopOrderGoods orderGood : orderGoods) { |
|||
if (orderGood.getSkuId() != null && orderGood.getSkuId() > 0) { |
|||
// 多规格商品,恢复SKU库存
|
|||
restoreSkuStock(orderGood); |
|||
} else { |
|||
// 单规格商品,恢复商品库存
|
|||
restoreGoodsStock(orderGood); |
|||
} |
|||
} |
|||
|
|||
log.info("订单库存回退成功,订单号:{},商品数量:{}", order.getOrderNo(), orderGoods.size()); |
|||
return true; |
|||
|
|||
} catch (Exception e) { |
|||
log.error("回退订单库存失败,订单号:{}", order.getOrderNo(), e); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public boolean returnOrderCoupon(ShopOrder order) { |
|||
try { |
|||
if (order.getCouponId() == null || order.getCouponId() <= 0) { |
|||
log.debug("订单未使用优惠券,订单号:{}", order.getOrderNo()); |
|||
return true; // 没有使用优惠券也算成功
|
|||
} |
|||
|
|||
boolean success = couponStatusService.returnCoupon(order.getOrderId()); |
|||
if (success) { |
|||
log.info("订单优惠券退还成功,订单号:{},优惠券ID:{}", order.getOrderNo(), order.getCouponId()); |
|||
} else { |
|||
log.warn("订单优惠券退还失败,订单号:{},优惠券ID:{}", order.getOrderNo(), order.getCouponId()); |
|||
} |
|||
return success; |
|||
|
|||
} catch (Exception e) { |
|||
log.error("退还订单优惠券失败,订单号:{}", order.getOrderNo(), e); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 恢复SKU库存 |
|||
*/ |
|||
private void restoreSkuStock(ShopOrderGoods orderGoods) { |
|||
ShopGoodsSku sku = shopGoodsSkuService.getById(orderGoods.getSkuId()); |
|||
if (sku != null) { |
|||
int newStock = (sku.getStock() != null ? sku.getStock() : 0) + orderGoods.getTotalNum(); |
|||
sku.setStock(newStock); |
|||
shopGoodsSkuService.updateById(sku); |
|||
log.debug("恢复SKU库存 - SKU ID:{},恢复数量:{},当前库存:{}", |
|||
orderGoods.getSkuId(), orderGoods.getTotalNum(), newStock); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 恢复商品库存 |
|||
*/ |
|||
private void restoreGoodsStock(ShopOrderGoods orderGoods) { |
|||
ShopGoods goods = shopGoodsService.getById(orderGoods.getGoodsId()); |
|||
if (goods != null) { |
|||
int newStock = (goods.getStock() != null ? goods.getStock() : 0) + orderGoods.getTotalNum(); |
|||
goods.setStock(newStock); |
|||
shopGoodsService.updateById(goods); |
|||
log.debug("恢复商品库存 - 商品ID:{},恢复数量:{},当前库存:{}", |
|||
orderGoods.getGoodsId(), orderGoods.getTotalNum(), newStock); |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,193 @@ |
|||
package com.gxwebsoft.shop.task; |
|||
|
|||
import com.gxwebsoft.shop.config.OrderConfigProperties; |
|||
import com.gxwebsoft.shop.entity.ShopOrder; |
|||
import com.gxwebsoft.shop.service.OrderCancelService; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.beans.factory.annotation.Value; |
|||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
|||
import org.springframework.scheduling.annotation.Scheduled; |
|||
import org.springframework.stereotype.Component; |
|||
|
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 订单自动取消定时任务 |
|||
* |
|||
* @author WebSoft |
|||
* @since 2025-01-26 |
|||
*/ |
|||
@Slf4j |
|||
@Component |
|||
@ConditionalOnProperty(prefix = "shop.order.auto-cancel", name = "enabled", havingValue = "true", matchIfMissing = true) |
|||
public class OrderAutoCancelTask { |
|||
|
|||
@Autowired |
|||
private OrderCancelService orderCancelService; |
|||
|
|||
@Autowired |
|||
private OrderConfigProperties orderConfig; |
|||
|
|||
@Value("${spring.profiles.active:dev}") |
|||
private String activeProfile; |
|||
|
|||
/** |
|||
* 自动取消超时订单 |
|||
* 生产环境:每5分钟执行一次 |
|||
* 开发环境:每1分钟执行一次(便于测试) |
|||
*/ |
|||
@Scheduled(cron = "${shop.order.auto-cancel.cron:0 */5 * * * ?}") |
|||
public void cancelExpiredOrders() { |
|||
if (!orderConfig.getAutoCancel().isEnabled()) { |
|||
log.debug("订单自动取消功能已禁用"); |
|||
return; |
|||
} |
|||
|
|||
log.info("开始执行订单自动取消任务..."); |
|||
|
|||
try { |
|||
long startTime = System.currentTimeMillis(); |
|||
int totalCancelledCount = 0; |
|||
|
|||
// 处理默认配置的订单
|
|||
int defaultCancelledCount = processDefaultTimeoutOrders(); |
|||
totalCancelledCount += defaultCancelledCount; |
|||
|
|||
// 处理租户特殊配置的订单
|
|||
int tenantCancelledCount = processTenantSpecificOrders(); |
|||
totalCancelledCount += tenantCancelledCount; |
|||
|
|||
long endTime = System.currentTimeMillis(); |
|||
long duration = endTime - startTime; |
|||
|
|||
log.info("订单自动取消任务完成,总取消数量: {},默认配置: {},租户配置: {},耗时: {}ms", |
|||
totalCancelledCount, defaultCancelledCount, tenantCancelledCount, duration); |
|||
|
|||
// 开发环境输出更详细的日志
|
|||
if ("dev".equals(activeProfile)) { |
|||
log.debug("开发环境 - 订单自动取消详情: 总共取消{}个订单", totalCancelledCount); |
|||
} |
|||
|
|||
} catch (Exception e) { |
|||
log.error("订单自动取消任务执行失败", e); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 处理使用默认超时配置的订单 |
|||
*/ |
|||
private int processDefaultTimeoutOrders() { |
|||
try { |
|||
Integer defaultTimeout = orderConfig.getAutoCancel().getDefaultTimeoutMinutes(); |
|||
Integer batchSize = orderConfig.getAutoCancel().getBatchSize(); |
|||
|
|||
log.debug("处理默认超时订单,超时时间: {}分钟,批量大小: {}", defaultTimeout, batchSize); |
|||
|
|||
List<ShopOrder> expiredOrders = orderCancelService.findExpiredUnpaidOrders(defaultTimeout, batchSize); |
|||
|
|||
if (expiredOrders.isEmpty()) { |
|||
log.debug("没有找到使用默认配置的超时订单"); |
|||
return 0; |
|||
} |
|||
|
|||
// 过滤掉有特殊租户配置的订单
|
|||
List<ShopOrder> ordersToCancel = filterOrdersWithoutTenantConfig(expiredOrders); |
|||
|
|||
if (ordersToCancel.isEmpty()) { |
|||
log.debug("过滤后没有需要使用默认配置取消的订单"); |
|||
return 0; |
|||
} |
|||
|
|||
int cancelledCount = orderCancelService.batchCancelOrders(ordersToCancel); |
|||
log.info("默认配置取消订单完成,找到: {}个,过滤后: {}个,成功取消: {}个", |
|||
expiredOrders.size(), ordersToCancel.size(), cancelledCount); |
|||
|
|||
return cancelledCount; |
|||
|
|||
} catch (Exception e) { |
|||
log.error("处理默认超时订单失败", e); |
|||
return 0; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 处理租户特殊配置的订单 |
|||
*/ |
|||
private int processTenantSpecificOrders() { |
|||
try { |
|||
List<OrderConfigProperties.TenantCancelConfig> tenantConfigs = orderConfig.getAutoCancel().getTenantConfigs(); |
|||
if (tenantConfigs == null || tenantConfigs.isEmpty()) { |
|||
log.debug("没有配置租户特殊超时规则"); |
|||
return 0; |
|||
} |
|||
|
|||
int totalCancelledCount = 0; |
|||
Integer batchSize = orderConfig.getAutoCancel().getBatchSize(); |
|||
|
|||
for (OrderConfigProperties.TenantCancelConfig tenantConfig : tenantConfigs) { |
|||
if (!tenantConfig.isEnabled()) { |
|||
log.debug("租户{}的自动取消功能已禁用", tenantConfig.getTenantId()); |
|||
continue; |
|||
} |
|||
|
|||
log.debug("处理租户{}的超时订单,超时时间: {}分钟", |
|||
tenantConfig.getTenantId(), tenantConfig.getTimeoutMinutes()); |
|||
|
|||
List<ShopOrder> tenantExpiredOrders = orderCancelService.findExpiredUnpaidOrdersByTenant( |
|||
tenantConfig.getTenantId(), tenantConfig.getTimeoutMinutes(), batchSize); |
|||
|
|||
if (!tenantExpiredOrders.isEmpty()) { |
|||
int cancelledCount = orderCancelService.batchCancelOrders(tenantExpiredOrders); |
|||
totalCancelledCount += cancelledCount; |
|||
|
|||
log.info("租户{}取消订单完成,找到: {}个,成功取消: {}个", |
|||
tenantConfig.getTenantId(), tenantExpiredOrders.size(), cancelledCount); |
|||
} |
|||
} |
|||
|
|||
return totalCancelledCount; |
|||
|
|||
} catch (Exception e) { |
|||
log.error("处理租户特殊配置订单失败", e); |
|||
return 0; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 过滤掉有租户特殊配置的订单 |
|||
*/ |
|||
private List<ShopOrder> filterOrdersWithoutTenantConfig(List<ShopOrder> orders) { |
|||
List<OrderConfigProperties.TenantCancelConfig> tenantConfigs = orderConfig.getAutoCancel().getTenantConfigs(); |
|||
if (tenantConfigs == null || tenantConfigs.isEmpty()) { |
|||
return orders; |
|||
} |
|||
|
|||
return orders.stream() |
|||
.filter(order -> { |
|||
// 检查该订单的租户是否有特殊配置
|
|||
return tenantConfigs.stream() |
|||
.noneMatch(config -> config.isEnabled() && config.getTenantId().equals(order.getTenantId())); |
|||
}) |
|||
.collect(java.util.stream.Collectors.toList()); |
|||
} |
|||
|
|||
/** |
|||
* 手动触发订单自动取消任务(用于测试) |
|||
*/ |
|||
public void manualCancelExpiredOrders() { |
|||
log.info("手动触发订单自动取消任务..."); |
|||
cancelExpiredOrders(); |
|||
} |
|||
|
|||
/** |
|||
* 获取任务状态信息 |
|||
*/ |
|||
public String getTaskStatus() { |
|||
return String.format("订单自动取消任务状态 - 启用: %s, 默认超时: %d分钟, 检查间隔: %d分钟, 批量大小: %d", |
|||
orderConfig.getAutoCancel().isEnabled(), |
|||
orderConfig.getAutoCancel().getDefaultTimeoutMinutes(), |
|||
orderConfig.getAutoCancel().getCheckIntervalMinutes(), |
|||
orderConfig.getAutoCancel().getBatchSize()); |
|||
} |
|||
} |
Loading…
Reference in new issue