自动取消订单任务
This commit is contained in:
@@ -33,6 +33,11 @@ public class OrderConfigProperties {
|
|||||||
*/
|
*/
|
||||||
private DefaultConfig defaultConfig = new DefaultConfig();
|
private DefaultConfig defaultConfig = new DefaultConfig();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订单自动取消配置
|
||||||
|
*/
|
||||||
|
private AutoCancel autoCancel = new AutoCancel();
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public static class TestAccount {
|
public static class TestAccount {
|
||||||
/**
|
/**
|
||||||
@@ -112,6 +117,71 @@ public class OrderConfigProperties {
|
|||||||
testAccount.getPhoneNumbers().contains(phone);
|
testAccount.getPhoneNumbers().contains(phone);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class AutoCancel {
|
||||||
|
/**
|
||||||
|
* 是否启用自动取消功能
|
||||||
|
*/
|
||||||
|
private boolean enabled = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认超时时间(分钟)
|
||||||
|
*/
|
||||||
|
private Integer defaultTimeoutMinutes = 30;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 定时任务检查间隔(分钟)
|
||||||
|
*/
|
||||||
|
private Integer checkIntervalMinutes = 5;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量处理大小
|
||||||
|
*/
|
||||||
|
private Integer batchSize = 100;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 租户特殊配置
|
||||||
|
*/
|
||||||
|
private List<TenantCancelConfig> tenantConfigs;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class TenantCancelConfig {
|
||||||
|
/**
|
||||||
|
* 租户ID
|
||||||
|
*/
|
||||||
|
private Integer tenantId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 租户名称
|
||||||
|
*/
|
||||||
|
private String tenantName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 超时时间(分钟)
|
||||||
|
*/
|
||||||
|
private Integer timeoutMinutes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否启用
|
||||||
|
*/
|
||||||
|
private boolean enabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定租户的超时时间
|
||||||
|
*/
|
||||||
|
public Integer getTimeoutMinutes(Integer tenantId) {
|
||||||
|
if (autoCancel.getTenantConfigs() != null) {
|
||||||
|
for (TenantCancelConfig config : autoCancel.getTenantConfigs()) {
|
||||||
|
if (config.isEnabled() && config.getTenantId().equals(tenantId)) {
|
||||||
|
return config.getTimeoutMinutes();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return autoCancel.getDefaultTimeoutMinutes();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取租户规则
|
* 获取租户规则
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import com.gxwebsoft.common.system.entity.Payment;
|
|||||||
import com.gxwebsoft.shop.service.ShopOrderGoodsService;
|
import com.gxwebsoft.shop.service.ShopOrderGoodsService;
|
||||||
import com.gxwebsoft.shop.service.ShopOrderService;
|
import com.gxwebsoft.shop.service.ShopOrderService;
|
||||||
import com.gxwebsoft.shop.service.OrderBusinessService;
|
import com.gxwebsoft.shop.service.OrderBusinessService;
|
||||||
|
import com.gxwebsoft.shop.service.OrderCancelService;
|
||||||
|
import com.gxwebsoft.shop.task.OrderAutoCancelTask;
|
||||||
import com.gxwebsoft.shop.entity.ShopOrder;
|
import com.gxwebsoft.shop.entity.ShopOrder;
|
||||||
import com.gxwebsoft.shop.param.ShopOrderParam;
|
import com.gxwebsoft.shop.param.ShopOrderParam;
|
||||||
import com.gxwebsoft.shop.dto.OrderCreateRequest;
|
import com.gxwebsoft.shop.dto.OrderCreateRequest;
|
||||||
@@ -36,6 +38,8 @@ import org.slf4j.Logger;
|
|||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
@@ -63,6 +67,10 @@ public class ShopOrderController extends BaseController {
|
|||||||
@Resource
|
@Resource
|
||||||
private OrderBusinessService orderBusinessService;
|
private OrderBusinessService orderBusinessService;
|
||||||
@Resource
|
@Resource
|
||||||
|
private OrderCancelService orderCancelService;
|
||||||
|
@Resource
|
||||||
|
private OrderAutoCancelTask orderAutoCancelTask;
|
||||||
|
@Resource
|
||||||
private RedisUtil redisUtil;
|
private RedisUtil redisUtil;
|
||||||
@Resource
|
@Resource
|
||||||
private ConfigProperties conf;
|
private ConfigProperties conf;
|
||||||
@@ -214,6 +222,74 @@ public class ShopOrderController extends BaseController {
|
|||||||
return success(shopOrderService.total());
|
return success(shopOrderService.total());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "取消订单")
|
||||||
|
@PutMapping("/cancel/{id}")
|
||||||
|
public ApiResult<?> cancelOrder(@PathVariable("id") Integer id) {
|
||||||
|
try {
|
||||||
|
User loginUser = getLoginUser();
|
||||||
|
if (loginUser == null) {
|
||||||
|
return fail("用户未登录");
|
||||||
|
}
|
||||||
|
|
||||||
|
ShopOrder order = shopOrderService.getById(id);
|
||||||
|
if (order == null) {
|
||||||
|
return fail("订单不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查订单是否属于当前用户(非管理员用户)
|
||||||
|
if (!loginUser.getUserId().equals(order.getUserId()) &&
|
||||||
|
!hasOrderCancelAuthority()) {
|
||||||
|
return fail("无权限取消此订单");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查订单状态
|
||||||
|
if (order.getPayStatus() != null && order.getPayStatus()) {
|
||||||
|
return fail("订单已支付,无法取消");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (order.getOrderStatus() != null && order.getOrderStatus() != 0) {
|
||||||
|
return fail("订单状态不允许取消");
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean success = orderCancelService.cancelOrder(order);
|
||||||
|
if (success) {
|
||||||
|
return success("订单取消成功");
|
||||||
|
} else {
|
||||||
|
return fail("订单取消失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("取消订单失败,订单ID:{}", id, e);
|
||||||
|
return fail("取消订单失败:" + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("hasAuthority('shop:shopOrder:manage')")
|
||||||
|
@Operation(summary = "手动触发订单自动取消任务(管理员)")
|
||||||
|
@PostMapping("/auto-cancel/trigger")
|
||||||
|
public ApiResult<?> triggerAutoCancelTask() {
|
||||||
|
try {
|
||||||
|
orderAutoCancelTask.manualCancelExpiredOrders();
|
||||||
|
return success("自动取消任务已触发");
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("触发自动取消任务失败", e);
|
||||||
|
return fail("触发失败:" + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("hasAuthority('shop:shopOrder:manage')")
|
||||||
|
@Operation(summary = "获取自动取消任务状态(管理员)")
|
||||||
|
@GetMapping("/auto-cancel/status")
|
||||||
|
public ApiResult<?> getAutoCancelTaskStatus() {
|
||||||
|
try {
|
||||||
|
String status = orderAutoCancelTask.getTaskStatus();
|
||||||
|
return success(status);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("获取自动取消任务状态失败", e);
|
||||||
|
return fail("获取状态失败:" + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Schema(description = "异步通知11")
|
@Schema(description = "异步通知11")
|
||||||
@PostMapping("/notify/{tenantId}")
|
@PostMapping("/notify/{tenantId}")
|
||||||
public String wxNotify(@RequestHeader Map<String, String> header, @RequestBody String body, @PathVariable("tenantId") Integer tenantId) {
|
public String wxNotify(@RequestHeader Map<String, String> header, @RequestBody String body, @PathVariable("tenantId") Integer tenantId) {
|
||||||
@@ -379,4 +455,27 @@ public class ShopOrderController extends BaseController {
|
|||||||
return "fail";
|
return "fail";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否有订单取消权限
|
||||||
|
*/
|
||||||
|
private boolean hasOrderCancelAuthority() {
|
||||||
|
try {
|
||||||
|
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
if (authentication == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否有管理员权限
|
||||||
|
return authentication.getAuthorities().stream()
|
||||||
|
.anyMatch(authority ->
|
||||||
|
authority.getAuthority().equals("shop:shopOrder:cancel") ||
|
||||||
|
authority.getAuthority().equals("shop:shopOrder:update") ||
|
||||||
|
authority.getAuthority().equals("ROLE_ADMIN") ||
|
||||||
|
authority.getAuthority().equals("shop:shopOrder:manage"));
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("检查订单取消权限时发生异常", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -206,6 +206,13 @@ public class ShopOrder implements Serializable {
|
|||||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
private LocalDateTime refundApplyTime;
|
private LocalDateTime refundApplyTime;
|
||||||
|
|
||||||
|
@Schema(description = "取消时间")
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
private LocalDateTime cancelTime;
|
||||||
|
|
||||||
|
@Schema(description = "取消原因")
|
||||||
|
private String cancelReason;
|
||||||
|
|
||||||
@Schema(description = "过期时间")
|
@Schema(description = "过期时间")
|
||||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
private LocalDateTime expirationTime;
|
private LocalDateTime expirationTime;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
193
src/main/java/com/gxwebsoft/shop/task/OrderAutoCancelTask.java
Normal file
193
src/main/java/com/gxwebsoft/shop/task/OrderAutoCancelTask.java
Normal file
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -139,6 +139,34 @@ shop:
|
|||||||
min-order-amount: 0
|
min-order-amount: 0
|
||||||
order-timeout-minutes: 30
|
order-timeout-minutes: 30
|
||||||
|
|
||||||
|
# 订单自动取消配置
|
||||||
|
auto-cancel:
|
||||||
|
# 是否启用自动取消功能
|
||||||
|
enabled: true
|
||||||
|
# 默认超时时间(分钟)
|
||||||
|
default-timeout-minutes: 30
|
||||||
|
# 定时任务检查间隔(分钟)
|
||||||
|
check-interval-minutes: 5
|
||||||
|
# 批量处理大小
|
||||||
|
batch-size: 100
|
||||||
|
# 定时任务执行时间(cron表达式)
|
||||||
|
# 生产环境:每5分钟执行一次
|
||||||
|
# 开发环境:每1分钟执行一次(便于测试)
|
||||||
|
cron: "0 */5 * * * ?"
|
||||||
|
# 开发环境可以设置为: "0 */1 * * * ?"
|
||||||
|
|
||||||
|
# 租户特殊配置
|
||||||
|
tenant-configs:
|
||||||
|
- tenant-id: 10324
|
||||||
|
tenant-name: "百色中学"
|
||||||
|
timeout-minutes: 60 # 捐款订单给更长的支付时间
|
||||||
|
enabled: true
|
||||||
|
# 可以添加更多租户配置
|
||||||
|
# - tenant-id: 10550
|
||||||
|
# tenant-name: "其他租户"
|
||||||
|
# timeout-minutes: 15
|
||||||
|
# enabled: true
|
||||||
|
|
||||||
# 证书配置
|
# 证书配置
|
||||||
certificate:
|
certificate:
|
||||||
# 证书加载模式: CLASSPATH, FILESYSTEM, VOLUME
|
# 证书加载模式: CLASSPATH, FILESYSTEM, VOLUME
|
||||||
|
|||||||
Reference in New Issue
Block a user