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