|
|
@@ -8,6 +8,7 @@ import cn.hutool.core.lang.Assert;
|
|
|
import cn.hutool.core.util.ObjectUtil;
|
|
|
import cn.hutool.core.util.StrUtil;
|
|
|
import com.alipay.api.AlipayClient;
|
|
|
+import com.alipay.api.domain.AlipayTradeFastpayRefundQueryModel;
|
|
|
import com.alipay.api.domain.AlipayTradeRefundModel;
|
|
|
import com.alipay.api.request.AlipayTradeRefundRequest;
|
|
|
import com.alipay.api.response.AlipayTradeRefundResponse;
|
|
|
@@ -63,6 +64,9 @@ import com.cyksj.service.mange.MirrorUserCleatService;
|
|
|
import com.cyksj.service.mange.stock.GoodsDonStockService;
|
|
|
import com.cyksj.service.market.MarketFrontService;
|
|
|
import com.cyksj.service.order.OrderCommonService;
|
|
|
+import com.cyksj.service.order.OrderDonBusinessEventService;
|
|
|
+import com.cyksj.service.order.OrderDonTicketRecordService;
|
|
|
+import com.cyksj.service.order.MultiQuantityOrderService;
|
|
|
import com.cyksj.service.order.OrderRefundService;
|
|
|
import com.cyksj.service.order.UserRealOrderBenefitsService;
|
|
|
import com.cyksj.service.relation.GroupRelationClearService;
|
|
|
@@ -81,6 +85,9 @@ import lombok.extern.slf4j.Slf4j;
|
|
|
import org.springframework.dao.DuplicateKeyException;
|
|
|
import org.springframework.stereotype.Service;
|
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
|
+import org.springframework.transaction.support.TransactionSynchronization;
|
|
|
+import org.springframework.transaction.support.TransactionSynchronizationManager;
|
|
|
+import org.springframework.transaction.support.TransactionTemplate;
|
|
|
|
|
|
import java.math.BigDecimal;
|
|
|
import java.math.RoundingMode;
|
|
|
@@ -88,6 +95,7 @@ import java.util.Date;
|
|
|
import java.util.List;
|
|
|
import java.util.Map;
|
|
|
import java.util.Optional;
|
|
|
+import java.util.UUID;
|
|
|
import java.util.concurrent.atomic.AtomicInteger;
|
|
|
import java.util.stream.Collectors;
|
|
|
|
|
|
@@ -238,6 +246,18 @@ public class CmsOrderDonServiceImpl extends ServiceImpl<OrderDonMapper,OrderDon>
|
|
|
|
|
|
private final AccountNetflixFrozenTicketReceivedRecordMapper receivedRecordMapper;
|
|
|
|
|
|
+ private final OrderDonTicketRecordService orderDonTicketRecordService;
|
|
|
+
|
|
|
+ private final MultiQuantityOrderService multiQuantityOrderService;
|
|
|
+
|
|
|
+ private final OrderDonBusinessEventService orderDonBusinessEventService;
|
|
|
+
|
|
|
+ private final OrderDonGalaxyCoinRecordMapper orderDonGalaxyCoinRecordMapper;
|
|
|
+
|
|
|
+ private final UserBalanceSourceRecordMapper userBalanceSourceRecordMapper;
|
|
|
+
|
|
|
+ private final TransactionTemplate transactionTemplate;
|
|
|
+
|
|
|
private static final GlobalThreadPoolTaskExecutor TASK_POOL = GlobalThreadPoolTaskExecutor.getInstance();
|
|
|
|
|
|
@Override
|
|
|
@@ -349,6 +369,9 @@ public class CmsOrderDonServiceImpl extends ServiceImpl<OrderDonMapper,OrderDon>
|
|
|
public void kickOutRelationId(long adminId, ClickOutReq req) {
|
|
|
Long orderId = req.getOrderId();
|
|
|
OrderDon orderDon = getById(orderId);
|
|
|
+ if (orderDon != null && orderDonTicketRecordService.exists(orderId)) {
|
|
|
+ throw BusinessRuntimeException.getInstance("Multi-quantity orders cannot clear a single ticket");
|
|
|
+ }
|
|
|
if (orderDon != null && !orderDon.getIsKickOut()) {
|
|
|
try {
|
|
|
UserRelationKickOutRecord kickOutRecord = new UserRelationKickOutRecord();
|
|
|
@@ -381,6 +404,12 @@ public class CmsOrderDonServiceImpl extends ServiceImpl<OrderDonMapper,OrderDon>
|
|
|
@Override
|
|
|
public void unifiedRefund(OrderRefundReq refundReq) throws Exception {
|
|
|
OrderDon orderDon = orderDonMapper.selectById(refundReq.getOrderId());
|
|
|
+ boolean multiQuantityOrder = orderDon != null
|
|
|
+ && orderDonTicketRecordService.exists(orderDon.getId());
|
|
|
+ if (multiQuantityOrder) {
|
|
|
+ refundMultiQuantityOrder(refundReq, orderDon);
|
|
|
+ return;
|
|
|
+ }
|
|
|
GoodsDonSku sku = null;
|
|
|
if (orderDon.getSkuId() != null) {
|
|
|
sku = goodsDonSkuMapper.selectById(orderDon.getSkuId());
|
|
|
@@ -410,7 +439,9 @@ public class CmsOrderDonServiceImpl extends ServiceImpl<OrderDonMapper,OrderDon>
|
|
|
}
|
|
|
orderDon.setRefundDesc(refundReq.getRefundDesc());
|
|
|
//记录退款信息
|
|
|
- commonRefund(orderDon, refundReq.getRefundMoney(), refundReq.getOperator(), goodsDon, sku, outNo, refundType, refundReq.getIsOnly(), refundReq.getRefundBalance(), refundReq.getChatImg(), refundReq.getDeductBalance(), refundReq.getDpRelationIds());
|
|
|
+ commonRefund(orderDon, refundReq.getRefundMoney(), refundReq.getOperator(), goodsDon, sku, outNo,
|
|
|
+ refundType, refundReq.getIsOnly(), refundReq.getRefundBalance(), refundReq.getChatImg(),
|
|
|
+ refundReq.getDeductBalance(), refundReq.getDpRelationIds(), multiQuantityOrder);
|
|
|
//修改订单状态
|
|
|
orderDon.setStatus(OrderDon.Status.refund);
|
|
|
orderDonMapper.updateById(orderDon);
|
|
|
@@ -430,6 +461,476 @@ public class CmsOrderDonServiceImpl extends ServiceImpl<OrderDonMapper,OrderDon>
|
|
|
groupRelationFrontService.setGroupsTripsParkingTimeEmpty(sku, orderDon.getRelationId());
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * A multi-quantity order is one payment backed by independent ticket slots.
|
|
|
+ * Ticket reclaim, provider refund and order-level side effects are deliberately split so
|
|
|
+ * a provider call never participates in a database transaction.
|
|
|
+ */
|
|
|
+ private void refundMultiQuantityOrder(OrderRefundReq refundReq, OrderDon orderDon) throws Exception {
|
|
|
+ refundMultiQuantityOrder(refundReq, orderDon, 5);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void refundMultiQuantityOrder(OrderRefundReq refundReq, OrderDon orderDon,
|
|
|
+ int leaseMinutes) throws Exception {
|
|
|
+ Long orderId = orderDon.getId();
|
|
|
+ OrderDonBusinessEvent existing = orderDonBusinessEventService.get(orderId, OrderDonBusinessEvent.Type.refund);
|
|
|
+ if (orderDon.getStatus() == OrderDon.Status.refund
|
|
|
+ && existing != null && existing.getStatus() == OrderDonBusinessEvent.Status.success) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ validateMultiQuantityWholeRefund(refundReq, orderDon);
|
|
|
+ multiQuantityOrderService.prepareRefund(orderId);
|
|
|
+ if (orderDon.getType() == OrderDon.Type.PAYPAL || orderDon.getType() == OrderDon.Type.STRIPE) {
|
|
|
+ throw BusinessRuntimeException.getInstance("PayPal/Stripe批量订单请人工核对后退款");
|
|
|
+ }
|
|
|
+ OrderDonBusinessEvent refundEvent = orderDonBusinessEventService.get(
|
|
|
+ orderId, OrderDonBusinessEvent.Type.refund);
|
|
|
+ if (refundEvent != null && refundEvent.getStatus() == OrderDonBusinessEvent.Status.processing) {
|
|
|
+ tryFinalizeProcessingRefund(orderDon, refundReq, refundEvent);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ String workerToken = UUID.randomUUID().toString().replace("-", "");
|
|
|
+ orderDonTicketRecordService.startRefund(orderId, workerToken);
|
|
|
+
|
|
|
+ GoodsDon goodsDon = goodsDonMapper.selectById(orderDon.getGoodsId());
|
|
|
+ GoodsDonSku sku = orderDon.getSkuId() == null ? null : goodsDonSkuMapper.selectById(orderDon.getSkuId());
|
|
|
+ String refundType = getMultiQuantityRefundType(orderDon);
|
|
|
+ String outRefundNo = buildMultiQuantityRefundNo(orderId);
|
|
|
+ if (requiresExternalRefund(orderDon)) {
|
|
|
+ try {
|
|
|
+ orderRefundService.centerRefund(refundReq, orderDon, outRefundNo);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("Multi-quantity refund submission failed, orderId:{}", orderId, e);
|
|
|
+ boolean restored = false;
|
|
|
+ try {
|
|
|
+ ProviderRefundStatus status = queryProviderRefundStatus(orderDon, outRefundNo);
|
|
|
+ if (status == ProviderRefundStatus.FAILED) {
|
|
|
+ orderDonTicketRecordService.restoreRefund(orderId, workerToken,
|
|
|
+ "provider refund submission failed");
|
|
|
+ restored = true;
|
|
|
+ }
|
|
|
+ } catch (Exception queryError) {
|
|
|
+ log.warn("Unable to confirm refund submission result, orderId:{}", orderId, queryError);
|
|
|
+ }
|
|
|
+ if (restored) {
|
|
|
+ throw BusinessRuntimeException.getInstance("退款申请失败,原车票已恢复");
|
|
|
+ }
|
|
|
+ throw BusinessRuntimeException.getInstance("退款申请状态确认中,请稍后查询");
|
|
|
+ }
|
|
|
+ if (orderDon.getType() == OrderDon.Type.WeChat) {
|
|
|
+ // WeChat's refund API only acknowledges the request. Finalize on its SUCCESS
|
|
|
+ // notification or a later query; tickets remain frozen meanwhile.
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ final String finalWorkerToken = workerToken;
|
|
|
+ transactionTemplate.executeWithoutResult(status -> finishMultiQuantityRefund(
|
|
|
+ refundReq, orderDon, goodsDon, sku, outRefundNo, refundType, finalWorkerToken));
|
|
|
+ }
|
|
|
+
|
|
|
+ private void tryFinalizeProcessingRefund(OrderDon order, OrderRefundReq request,
|
|
|
+ OrderDonBusinessEvent event) throws Exception {
|
|
|
+ if (StrUtil.isBlank(event.getWorkerToken())) {
|
|
|
+ throw BusinessRuntimeException.getInstance("订单退款状态异常");
|
|
|
+ }
|
|
|
+ String outRefundNo = buildMultiQuantityRefundNo(order.getId());
|
|
|
+ ProviderRefundStatus status = queryProviderRefundStatus(order, outRefundNo);
|
|
|
+ if (status == ProviderRefundStatus.SUCCESS) {
|
|
|
+ GoodsDon goods = goodsDonMapper.selectById(order.getGoodsId());
|
|
|
+ GoodsDonSku sku = order.getSkuId() == null ? null : goodsDonSkuMapper.selectById(order.getSkuId());
|
|
|
+ transactionTemplate.executeWithoutResult(tx -> finishMultiQuantityRefund(
|
|
|
+ request, order, goods, sku, outRefundNo,
|
|
|
+ getMultiQuantityRefundType(order), event.getWorkerToken()));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (status == ProviderRefundStatus.FAILED) {
|
|
|
+ orderDonTicketRecordService.restoreRefund(order.getId(), event.getWorkerToken(),
|
|
|
+ "provider refund failed or closed");
|
|
|
+ throw BusinessRuntimeException.getInstance("退款失败,原车票已恢复");
|
|
|
+ }
|
|
|
+ throw BusinessRuntimeException.getInstance("订单退款正在处理中,请稍后查询退款状态");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public int recoverMultiQuantityRefunds(int limit, int leaseMinutes) {
|
|
|
+ int boundedLimit = Math.max(1, Math.min(limit, 500));
|
|
|
+ int boundedLeaseMinutes = Math.max(1, Math.min(leaseMinutes, 120));
|
|
|
+ int processed = 0;
|
|
|
+ for (Long orderId : orderDonTicketRecordService.expiredRefundOrderIds(boundedLeaseMinutes, boundedLimit)) {
|
|
|
+ OrderDon order = orderDonMapper.selectById(orderId);
|
|
|
+ if (order == null || order.getStatus() == OrderDon.Status.refund) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ recoverMultiQuantityRefund(order, boundedLeaseMinutes);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("Multi-quantity refund recovery failed, orderId:{}", orderId, e);
|
|
|
+ }
|
|
|
+ processed++;
|
|
|
+ }
|
|
|
+ return processed;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void recoverMultiQuantityRefund(OrderDon order, int leaseMinutes) throws Exception {
|
|
|
+ OrderDonBusinessEvent refundEvent = orderDonBusinessEventService.get(
|
|
|
+ order.getId(), OrderDonBusinessEvent.Type.refund);
|
|
|
+ if (refundEvent == null || refundEvent.getStatus() != OrderDonBusinessEvent.Status.processing) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ String outRefundNo = buildMultiQuantityRefundNo(order.getId());
|
|
|
+ ProviderRefundStatus providerStatus = queryProviderRefundStatus(order, outRefundNo);
|
|
|
+ if (providerStatus == ProviderRefundStatus.PROCESSING
|
|
|
+ || providerStatus == ProviderRefundStatus.UNKNOWN) {
|
|
|
+ if (StrUtil.isNotBlank(refundEvent.getWorkerToken())) {
|
|
|
+ orderDonTicketRecordService.deferRefundCheck(order.getId(), refundEvent.getWorkerToken(),
|
|
|
+ "provider refund is not final");
|
|
|
+ }
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ String workerToken;
|
|
|
+ if (providerStatus == ProviderRefundStatus.SUCCESS) {
|
|
|
+ workerToken = refundEvent.getWorkerToken();
|
|
|
+ if (StrUtil.isBlank(workerToken)) {
|
|
|
+ throw new IllegalStateException("refund worker token is missing");
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ workerToken = orderDonTicketRecordService.reclaimExpiredRefund(order.getId(), leaseMinutes);
|
|
|
+ if (workerToken == null) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (providerStatus == ProviderRefundStatus.FAILED) {
|
|
|
+ orderDonTicketRecordService.restoreRefund(order.getId(), workerToken,
|
|
|
+ "provider refund failed or closed");
|
|
|
+ log.error("Multi-quantity provider refund failed, orderId:{}, outRefundNo:{}",
|
|
|
+ order.getId(), outRefundNo);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ GoodsDon goodsDon = goodsDonMapper.selectById(order.getGoodsId());
|
|
|
+ GoodsDonSku sku = order.getSkuId() == null ? null : goodsDonSkuMapper.selectById(order.getSkuId());
|
|
|
+ OrderRefundReq request = buildRecoveryRefundRequest(order);
|
|
|
+ transactionTemplate.executeWithoutResult(status -> finishMultiQuantityRefund(
|
|
|
+ request, order, goodsDon, sku, outRefundNo,
|
|
|
+ getMultiQuantityRefundType(order), workerToken));
|
|
|
+ }
|
|
|
+
|
|
|
+ private OrderRefundReq buildRecoveryRefundRequest(OrderDon order) {
|
|
|
+ OrderRefundReq request = new OrderRefundReq();
|
|
|
+ request.setOrderId(order.getId());
|
|
|
+ request.setRefundMoney(Optional.ofNullable(order.getMoney()).orElse(BigDecimal.ZERO));
|
|
|
+ request.setRefundBalance(Optional.ofNullable(order.getBalance()).orElse(BigDecimal.ZERO));
|
|
|
+ request.setRefundDesc(StrUtil.blankToDefault(order.getRefundDesc(),
|
|
|
+ "multi-quantity whole-order refund recovery"));
|
|
|
+ request.setOperator("system-recovery");
|
|
|
+ request.setIsDiffer(false);
|
|
|
+ request.setRefundType(getMultiQuantityRefundType(order));
|
|
|
+ return request;
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean requiresExternalRefund(OrderDon order) {
|
|
|
+ return Optional.ofNullable(order.getMoney()).orElse(BigDecimal.ZERO).compareTo(BigDecimal.ZERO) > 0
|
|
|
+ && order.getType() != OrderDon.Type.BALANCE;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String refundShopId(OrderDon order) {
|
|
|
+ return StrUtil.blankToDefault(order.getShopId(),
|
|
|
+ order.getType() == OrderDon.Type.WeChat
|
|
|
+ ? ShopConfig.WX_DEFAULT_APP_ID : ShopConfig.ZFB_DEFAULT_APP_ID);
|
|
|
+ }
|
|
|
+
|
|
|
+ private ProviderRefundStatus queryProviderRefundStatus(OrderDon order, String outRefundNo) throws Exception {
|
|
|
+ if (!requiresExternalRefund(order)) {
|
|
|
+ return ProviderRefundStatus.SUCCESS;
|
|
|
+ }
|
|
|
+ if (order.getType() == OrderDon.Type.WeChat) {
|
|
|
+ Map<String, String> result = weChatService.getWxRefundDetail(
|
|
|
+ weChatConfig.getAppid(), refundShopId(order), outRefundNo);
|
|
|
+ return parseWxRefundStatus(result, order, outRefundNo);
|
|
|
+ }
|
|
|
+ if (order.getType() == OrderDon.Type.ALI_PAY) {
|
|
|
+ com.alipay.api.response.AlipayTradeFastpayRefundQueryResponse response =
|
|
|
+ queryAliRefund(order, outRefundNo);
|
|
|
+ if (response == null) {
|
|
|
+ return ProviderRefundStatus.UNKNOWN;
|
|
|
+ }
|
|
|
+ if (!StrUtil.equals(order.getOrderNo(), response.getOutTradeNo())
|
|
|
+ || !StrUtil.equals(outRefundNo, response.getOutRequestNo())
|
|
|
+ || !StrUtil.equals(order.getTransactionId(), response.getTradeNo())) {
|
|
|
+ throw BusinessRuntimeException.getInstance("支付宝退款查询订单信息不匹配");
|
|
|
+ }
|
|
|
+ BigDecimal expected = Optional.ofNullable(order.getMoney()).orElse(BigDecimal.ZERO);
|
|
|
+ BigDecimal actual = yuanToCent(response.getRefundAmount());
|
|
|
+ return actual.compareTo(expected) == 0
|
|
|
+ ? ProviderRefundStatus.SUCCESS : ProviderRefundStatus.UNKNOWN;
|
|
|
+ }
|
|
|
+ return ProviderRefundStatus.UNKNOWN;
|
|
|
+ }
|
|
|
+
|
|
|
+ private com.alipay.api.response.AlipayTradeFastpayRefundQueryResponse queryAliRefund(
|
|
|
+ OrderDon order, String outRefundNo) throws Exception {
|
|
|
+ AlipayClient alipayClient = AliPayClientFactory.getAlipayClient(
|
|
|
+ refundShopId(order));
|
|
|
+ com.alipay.api.request.AlipayTradeFastpayRefundQueryRequest request =
|
|
|
+ new com.alipay.api.request.AlipayTradeFastpayRefundQueryRequest();
|
|
|
+ AlipayTradeFastpayRefundQueryModel model = new AlipayTradeFastpayRefundQueryModel();
|
|
|
+ model.setTradeNo(order.getTransactionId());
|
|
|
+ model.setOutRequestNo(outRefundNo);
|
|
|
+ request.setBizModel(model);
|
|
|
+ ShopConfig shopConfig = shopConfigMapper.selectOne(Wrappers.lambdaQuery(ShopConfig.class)
|
|
|
+ .eq(ShopConfig::getAppId, order.getShopId()).last("limit 1"));
|
|
|
+ com.alipay.api.response.AlipayTradeFastpayRefundQueryResponse response =
|
|
|
+ shopConfig != null && Boolean.TRUE.equals(shopConfig.getIsCert())
|
|
|
+ ? alipayClient.certificateExecute(request) : alipayClient.execute(request);
|
|
|
+ return response != null && response.isSuccess() ? response : null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private ProviderRefundStatus parseWxRefundStatus(Map<String, String> result,
|
|
|
+ OrderDon order, String outRefundNo) {
|
|
|
+ if (result == null || !"SUCCESS".equals(result.get("result_code"))) {
|
|
|
+ return ProviderRefundStatus.UNKNOWN;
|
|
|
+ }
|
|
|
+ int index = findWxRefundIndex(result, outRefundNo);
|
|
|
+ if (index < 0) {
|
|
|
+ return ProviderRefundStatus.UNKNOWN;
|
|
|
+ }
|
|
|
+ if (!StrUtil.equals(order.getTransactionId(), result.get("transaction_id"))
|
|
|
+ || !StrUtil.equals(order.getOrderNo(), result.get("out_trade_no"))
|
|
|
+ || !StrUtil.equals(refundShopId(order), result.get("mch_id"))
|
|
|
+ || parseCent(result.get("total_fee"))
|
|
|
+ != Optional.ofNullable(order.getMoney()).orElse(BigDecimal.ZERO).longValueExact()
|
|
|
+ || parseCent(result.get("refund_fee_" + index))
|
|
|
+ != Optional.ofNullable(order.getMoney()).orElse(BigDecimal.ZERO).longValueExact()) {
|
|
|
+ throw BusinessRuntimeException.getInstance("微信退款查询订单信息不匹配");
|
|
|
+ }
|
|
|
+ return mapWxRefundStatus(result.get("refund_status_" + index));
|
|
|
+ }
|
|
|
+
|
|
|
+ private int findWxRefundIndex(Map<String, String> result, String outRefundNo) {
|
|
|
+ int count;
|
|
|
+ try {
|
|
|
+ count = Integer.parseInt(result.getOrDefault("refund_count", "0"));
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
+ return -1;
|
|
|
+ }
|
|
|
+ for (int index = 0; index < count; index++) {
|
|
|
+ if (StrUtil.equals(outRefundNo, result.get("out_refund_no_" + index))) {
|
|
|
+ return index;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return -1;
|
|
|
+ }
|
|
|
+
|
|
|
+ private ProviderRefundStatus mapWxRefundStatus(String status) {
|
|
|
+ if ("SUCCESS".equals(status)) {
|
|
|
+ return ProviderRefundStatus.SUCCESS;
|
|
|
+ }
|
|
|
+ if ("PROCESSING".equals(status)) {
|
|
|
+ return ProviderRefundStatus.PROCESSING;
|
|
|
+ }
|
|
|
+ if ("CHANGE".equals(status) || "REFUNDCLOSE".equals(status)) {
|
|
|
+ return ProviderRefundStatus.FAILED;
|
|
|
+ }
|
|
|
+ return ProviderRefundStatus.UNKNOWN;
|
|
|
+ }
|
|
|
+
|
|
|
+ private long parseCent(String value) {
|
|
|
+ try {
|
|
|
+ return Long.parseLong(value);
|
|
|
+ } catch (Exception e) {
|
|
|
+ throw BusinessRuntimeException.getInstance("退款金额格式异常");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private BigDecimal yuanToCent(String value) {
|
|
|
+ try {
|
|
|
+ return new BigDecimal(value).multiply(BigDecimal.valueOf(100))
|
|
|
+ .setScale(0, RoundingMode.UNNECESSARY);
|
|
|
+ } catch (Exception e) {
|
|
|
+ throw BusinessRuntimeException.getInstance("退款金额格式异常");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private enum ProviderRefundStatus {
|
|
|
+ SUCCESS,
|
|
|
+ PROCESSING,
|
|
|
+ FAILED,
|
|
|
+ UNKNOWN
|
|
|
+ }
|
|
|
+
|
|
|
+ private void validateMultiQuantityWholeRefund(OrderRefundReq refundReq, OrderDon orderDon) {
|
|
|
+ if (orderDon.getStatus() == OrderDon.Status.noPayment || orderDon.getStatus() == OrderDon.Status.close) {
|
|
|
+ throw BusinessRuntimeException.getInstance("订单{0}状态不支持退款", orderDon.getStatus());
|
|
|
+ }
|
|
|
+ if (orderDon.getStatus() == OrderDon.Status.refund) {
|
|
|
+ throw BusinessRuntimeException.getInstance("该订单已退款");
|
|
|
+ }
|
|
|
+ if (orderDon.getCouponUserId() != null && orderDon.getCouponUserId() != 0) {
|
|
|
+ throw BusinessRuntimeException.getInstance("批量订单不支持优惠券退款");
|
|
|
+ }
|
|
|
+ BigDecimal money = Optional.ofNullable(orderDon.getMoney()).orElse(BigDecimal.ZERO);
|
|
|
+ BigDecimal balance = Optional.ofNullable(orderDon.getBalance()).orElse(BigDecimal.ZERO);
|
|
|
+ BigDecimal refundMoney = Optional.ofNullable(refundReq.getRefundMoney()).orElse(BigDecimal.ZERO);
|
|
|
+ BigDecimal requestedBalance = refundReq.getRefundBalance();
|
|
|
+ if (refundMoney.compareTo(money) != 0
|
|
|
+ || (requestedBalance != null && requestedBalance.compareTo(balance) != 0)
|
|
|
+ || Boolean.TRUE.equals(refundReq.getIsOnly()) || Boolean.TRUE.equals(refundReq.getIsDiffer())) {
|
|
|
+ throw BusinessRuntimeException.getInstance("批量订单仅支持整单退款,现金和余额必须全部退回");
|
|
|
+ }
|
|
|
+ refundReq.setRefundMoney(money);
|
|
|
+ refundReq.setRefundBalance(balance);
|
|
|
+ refundReq.setRefundType(getMultiQuantityRefundType(orderDon));
|
|
|
+ }
|
|
|
+
|
|
|
+ private void reclaimMultiQuantityTickets(OrderDon orderDon, String workerToken) {
|
|
|
+ List<OrderDonTicketRecord> records = orderDonTicketRecordService.listByOrderId(orderDon.getId());
|
|
|
+ int expected = Optional.ofNullable(orderDon.getNum()).orElse(records.size());
|
|
|
+ if (records.size() != expected) {
|
|
|
+ throw BusinessRuntimeException.getInstance("订单车票数量不匹配");
|
|
|
+ }
|
|
|
+ for (OrderDonTicketRecord record : records) {
|
|
|
+ if (isTicketAlreadyReclaimed(record)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if (record.getStatus() != OrderDonTicketRecord.Status.refunding) {
|
|
|
+ throw BusinessRuntimeException.getInstance("订单车票状态异常,序号: {0}", record.getItemIndex());
|
|
|
+ }
|
|
|
+ if (record.getRelationId() != null) {
|
|
|
+ clearMultiQuantityTicket(orderDon, record);
|
|
|
+ }
|
|
|
+ if (!orderDonTicketRecordService.markRefunded(record.getId(), workerToken)) {
|
|
|
+ throw BusinessRuntimeException.getInstance("车票退款状态更新冲突,序号: {0}", record.getItemIndex());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (!areAllMultiQuantityTicketsReclaimed(orderDon,
|
|
|
+ orderDonTicketRecordService.listByOrderId(orderDon.getId()))) {
|
|
|
+ throw BusinessRuntimeException.getInstance("订单车票未全部回收");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ static boolean areAllMultiQuantityTicketsReclaimed(OrderDon orderDon,
|
|
|
+ List<OrderDonTicketRecord> records) {
|
|
|
+ int expected = Optional.ofNullable(orderDon.getNum()).orElse(records.size());
|
|
|
+ if (records.size() != expected) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ return records.stream().allMatch(CmsOrderDonServiceImpl::isTicketAlreadyReclaimed);
|
|
|
+ }
|
|
|
+
|
|
|
+ static boolean isTicketAlreadyReclaimed(OrderDonTicketRecord record) {
|
|
|
+ return record.getStatus() == OrderDonTicketRecord.Status.refunded
|
|
|
+ || record.getStatus() == OrderDonTicketRecord.Status.cleared;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void clearMultiQuantityTicket(OrderDon orderDon, OrderDonTicketRecord record) {
|
|
|
+ OrderDonTicketRecord activeRecord = orderDonTicketRecordService.getActiveByRelationIdForUpdate(
|
|
|
+ record.getRelationId());
|
|
|
+ if (activeRecord == null || !activeRecord.getId().equals(record.getId())
|
|
|
+ || activeRecord.getStatus() != OrderDonTicketRecord.Status.refunding
|
|
|
+ || !orderDon.getId().equals(activeRecord.getOrderId())) {
|
|
|
+ throw BusinessRuntimeException.getInstance("车票分配记录已变更,relationId: {0}", record.getRelationId());
|
|
|
+ }
|
|
|
+ GroupsRelation relation = relationMapper.selectByIdForUpdate(record.getRelationId());
|
|
|
+ if (relation == null) {
|
|
|
+ throw BusinessRuntimeException.getInstance("退款车票不存在,relationId: {0}", record.getRelationId());
|
|
|
+ }
|
|
|
+ if (relation.getUserId() == null || relation.getUserId() == 0) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (!isMultiQuantityTicketOwner(orderDon, relation)) {
|
|
|
+ throw BusinessRuntimeException.getInstance("车票归属异常,relationId: {0}", relation.getId());
|
|
|
+ }
|
|
|
+ relationClearService.clearTicket(relation, UserTicketClearedRecord.Source.refund);
|
|
|
+ }
|
|
|
+
|
|
|
+ static boolean isMultiQuantityTicketOwner(OrderDon orderDon, GroupsRelation relation) {
|
|
|
+ return orderDon != null && relation != null && orderDon.getUserId() != null
|
|
|
+ && orderDon.getUserId().equals(relation.getUserId());
|
|
|
+ }
|
|
|
+
|
|
|
+ private void finishMultiQuantityRefund(OrderRefundReq refundReq, OrderDon orderDon,
|
|
|
+ GoodsDon goodsDon, GoodsDonSku sku, String outRefundNo,
|
|
|
+ String refundType, String workerToken) {
|
|
|
+ OrderDon latest = orderDonMapper.selectByIdForUpdate(orderDon.getId());
|
|
|
+ if (latest == null) {
|
|
|
+ throw BusinessRuntimeException.getInstance("订单不存在");
|
|
|
+ }
|
|
|
+ OrderDonBusinessEvent refundEvent = orderDonBusinessEventService.get(
|
|
|
+ latest.getId(), OrderDonBusinessEvent.Type.refund);
|
|
|
+ if (refundEvent != null && refundEvent.getStatus() == OrderDonBusinessEvent.Status.success) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (latest.getStatus() == OrderDon.Status.refund) {
|
|
|
+ throw BusinessRuntimeException.getInstance("订单退款状态与幂等事件不一致");
|
|
|
+ }
|
|
|
+ if (refundEvent == null || refundEvent.getStatus() != OrderDonBusinessEvent.Status.processing
|
|
|
+ || !StrUtil.equals(workerToken, refundEvent.getWorkerToken())) {
|
|
|
+ throw BusinessRuntimeException.getInstance("订单退款执行权已失效");
|
|
|
+ }
|
|
|
+ reclaimMultiQuantityTickets(latest, workerToken);
|
|
|
+ latest.setRefundDesc(refundReq.getRefundDesc());
|
|
|
+ latest.setRefundMoney(latest.getMoney());
|
|
|
+ latest.setRefundBalance(Optional.ofNullable(latest.getBalance()).orElse(BigDecimal.ZERO));
|
|
|
+
|
|
|
+ Integer returnedBalance = userBalanceSourceRecordMapper.selectCount(
|
|
|
+ Wrappers.lambdaQuery(UserBalanceSourceRecord.class)
|
|
|
+ .eq(UserBalanceSourceRecord::getOrderId, latest.getId())
|
|
|
+ .eq(UserBalanceSourceRecord::getSource, UserBalanceSourceRecord.Source.refund));
|
|
|
+ if ((OrderRefundReq.RefundType.balance.name().equals(refundType)
|
|
|
+ || OrderRefundReq.RefundType.bxj.name().equals(refundType))
|
|
|
+ && latest.getRefundBalance().compareTo(BigDecimal.ZERO) > 0
|
|
|
+ && returnedBalance != null && returnedBalance > 0) {
|
|
|
+ throw BusinessRuntimeException.getInstance("订单余额已退回,退款幂等状态异常");
|
|
|
+ }
|
|
|
+
|
|
|
+ commonRefund(latest, latest.getRefundMoney(), refundReq.getOperator(), goodsDon, sku,
|
|
|
+ outRefundNo, refundType, null, latest.getRefundBalance(), refundReq.getChatImg(),
|
|
|
+ refundReq.getDeductBalance(), null, true);
|
|
|
+ refundUsedGalaxyCoin(latest);
|
|
|
+ latest.setStatus(OrderDon.Status.refund);
|
|
|
+ if (orderDonMapper.update(latest, Wrappers.lambdaUpdate(OrderDon.class)
|
|
|
+ .ne(OrderDon::getStatus, OrderDon.Status.refund)
|
|
|
+ .eq(OrderDon::getId, latest.getId())) != 1) {
|
|
|
+ throw BusinessRuntimeException.getInstance("订单退款状态更新冲突");
|
|
|
+ }
|
|
|
+ if (!orderDonBusinessEventService.markSuccess(latest.getId(), OrderDonBusinessEvent.Type.refund, workerToken)) {
|
|
|
+ throw BusinessRuntimeException.getInstance("订单退款幂等状态更新冲突");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void refundUsedGalaxyCoin(OrderDon orderDon) {
|
|
|
+ OrderDonGalaxyCoinRecord record = orderDonGalaxyCoinRecordMapper.selectOne(
|
|
|
+ Wrappers.lambdaQuery(OrderDonGalaxyCoinRecord.class)
|
|
|
+ .eq(OrderDonGalaxyCoinRecord::getOrderId, orderDon.getId())
|
|
|
+ .eq(OrderDonGalaxyCoinRecord::getSource, OrderDonGalaxyCoinRecord.Source.ordinary.ordinal())
|
|
|
+ .last("limit 1"));
|
|
|
+ if (record == null || record.getGalaxyCoin() == null || record.getGalaxyCoin().compareTo(BigDecimal.ZERO) <= 0) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ userGalaxyCoinService.recordOrderRefundGalaxyCoinIfValid(orderDon.getId(), record.getUserId(),
|
|
|
+ record.getGalaxyCoin(), OrderDonGalaxyCoinRecord.Source.ordinary);
|
|
|
+ }
|
|
|
+
|
|
|
+ private String getMultiQuantityRefundType(OrderDon orderDon) {
|
|
|
+ BigDecimal money = Optional.ofNullable(orderDon.getMoney()).orElse(BigDecimal.ZERO);
|
|
|
+ BigDecimal balance = Optional.ofNullable(orderDon.getBalance()).orElse(BigDecimal.ZERO);
|
|
|
+ if (money.compareTo(BigDecimal.ZERO) > 0 && balance.compareTo(BigDecimal.ZERO) > 0) {
|
|
|
+ return OrderRefundReq.RefundType.bxj.name();
|
|
|
+ }
|
|
|
+ if (money.compareTo(BigDecimal.ZERO) == 0 && balance.compareTo(BigDecimal.ZERO) > 0) {
|
|
|
+ return OrderRefundReq.RefundType.balance.name();
|
|
|
+ }
|
|
|
+ return OrderRefundReq.RefundType.money.name();
|
|
|
+ }
|
|
|
+
|
|
|
+ private String buildMultiQuantityRefundNo(Long orderId) throws Exception {
|
|
|
+ return Codec.DoDigest.custom()
|
|
|
+ .setAlgorithm(Codec.DoDigest.Algorithm.MD5)
|
|
|
+ .setStringData("multi-order-refund:" + orderId)
|
|
|
+ .toHexString();
|
|
|
+ }
|
|
|
+
|
|
|
@Override
|
|
|
@Transactional(rollbackFor = Throwable.class)
|
|
|
public void unifiedUpgradeRefund(OrderRefundReq refundReq) throws Exception {
|
|
|
@@ -713,29 +1214,90 @@ public class CmsOrderDonServiceImpl extends ServiceImpl<OrderDonMapper,OrderDon>
|
|
|
|
|
|
@Override
|
|
|
public void wxRefundNotify(Map<String, String> map) throws Exception {
|
|
|
- // 对退款数据 解密
|
|
|
- String md5Key = Codec.DoDigest.custom().setAlgorithm(Codec.DoDigest.Algorithm.MD5).setStringData(shopConfigMapper.getByAppId(map.get("mch_id")).getSecret()).toHexString();
|
|
|
- String reqInfoXml = Codec.DoCipher.custom().setAlgorithm(Codec.DoCipher.Trans.AES_ECB_PKCS7Padding).setBase64Data(map.get("req_info")).setStringKey(md5Key).ofDecrypt().toText();
|
|
|
+ ShopConfig callbackShop = shopConfigMapper.getByAppId(map.get("mch_id"));
|
|
|
+ if (callbackShop == null || StrUtil.isBlank(callbackShop.getSecret())) {
|
|
|
+ throw BusinessRuntimeException.getInstance("退款回调商户不存在");
|
|
|
+ }
|
|
|
+ String md5Key = Codec.DoDigest.custom().setAlgorithm(Codec.DoDigest.Algorithm.MD5)
|
|
|
+ .setStringData(callbackShop.getSecret()).toHexString();
|
|
|
+ String reqInfoXml = Codec.DoCipher.custom()
|
|
|
+ .setAlgorithm(Codec.DoCipher.Trans.AES_ECB_PKCS7Padding)
|
|
|
+ .setBase64Data(map.get("req_info")).setStringKey(md5Key).ofDecrypt().toText();
|
|
|
log.info("=== 退款数据解密: \n {}", reqInfoXml);
|
|
|
- Map<String, String> reqInfoMap = Xmls.toMap(reqInfoXml);
|
|
|
- // 退款失败
|
|
|
- if (StrUtil.equals("CHANGE", reqInfoMap.get("refund_status"))) {
|
|
|
- throw BusinessRuntimeException.getInstance("退款异常.");
|
|
|
- } else if (StrUtil.equals("REFUNDCLOSE", reqInfoMap.get("refund_status"))) {
|
|
|
- throw BusinessRuntimeException.getInstance("退款关闭.");
|
|
|
- }
|
|
|
- /* 此处处理业务逻辑 */
|
|
|
- String outRefundNo = reqInfoMap.get("out_refund_no");
|
|
|
- String transactionId = reqInfoMap.get("transaction_id");
|
|
|
- String refundFee = reqInfoMap.get("refund_fee");
|
|
|
- OrderDon orderDon = orderDonMapper.selectOne(Wrappers.lambdaQuery(OrderDon.class).eq(OrderDon::getTransactionId, transactionId).last("limit 1"));
|
|
|
- if (orderDon == null) {
|
|
|
- log.error("退款回调,transactionId:{}订单不存在", transactionId);
|
|
|
+ Map<String, String> refund = Xmls.toMap(reqInfoXml);
|
|
|
+ String transactionId = refund.get("transaction_id");
|
|
|
+ OrderDon order = orderDonMapper.selectOne(Wrappers.lambdaQuery(OrderDon.class)
|
|
|
+ .eq(OrderDon::getTransactionId, transactionId).last("limit 1"));
|
|
|
+ if (order == null) {
|
|
|
+ throw BusinessRuntimeException.getInstance("退款回调订单不存在");
|
|
|
+ }
|
|
|
+ String expectedRefundNo = buildMultiQuantityRefundNo(order.getId());
|
|
|
+ boolean multiQuantity = orderDonTicketRecordService.exists(order.getId());
|
|
|
+ if (!multiQuantity) {
|
|
|
+ log.info("微信普通订单退款回调, transactionId:{}, outRefundNo:{}",
|
|
|
+ transactionId, refund.get("out_refund_no"));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (order.getType() != OrderDon.Type.WeChat) {
|
|
|
+ throw BusinessRuntimeException.getInstance("退款回调支付方式不匹配");
|
|
|
+ }
|
|
|
+ validateWxRefundCallback(refund, order, callbackShop, expectedRefundNo);
|
|
|
+ String status = refund.get("refund_status");
|
|
|
+ if ("SUCCESS".equals(status)) {
|
|
|
+ OrderDonBusinessEvent event = orderDonBusinessEventService.get(
|
|
|
+ order.getId(), OrderDonBusinessEvent.Type.refund);
|
|
|
+ if (event != null && event.getStatus() == OrderDonBusinessEvent.Status.success) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (event == null) {
|
|
|
+ log.error("WeChat refund succeeded without a local refund event, orderId:{}", order.getId());
|
|
|
+ throw BusinessRuntimeException.getInstance("退款回调缺少本地退款记录");
|
|
|
+ }
|
|
|
+ if (event.getStatus() == OrderDonBusinessEvent.Status.failed
|
|
|
+ && StrUtil.isBlank(event.getWorkerToken())) {
|
|
|
+ String callbackToken = UUID.randomUUID().toString().replace("-", "");
|
|
|
+ orderDonTicketRecordService.startRefund(order.getId(), callbackToken);
|
|
|
+ event = orderDonBusinessEventService.get(order.getId(), OrderDonBusinessEvent.Type.refund);
|
|
|
+ }
|
|
|
+ if (event.getStatus() != OrderDonBusinessEvent.Status.processing
|
|
|
+ || StrUtil.isBlank(event.getWorkerToken())) {
|
|
|
+ throw BusinessRuntimeException.getInstance("退款回调状态异常");
|
|
|
+ }
|
|
|
+ GoodsDon goods = goodsDonMapper.selectById(order.getGoodsId());
|
|
|
+ GoodsDonSku sku = order.getSkuId() == null ? null : goodsDonSkuMapper.selectById(order.getSkuId());
|
|
|
+ OrderRefundReq request = buildRecoveryRefundRequest(order);
|
|
|
+ String workerToken = event.getWorkerToken();
|
|
|
+ transactionTemplate.executeWithoutResult(tx -> finishMultiQuantityRefund(
|
|
|
+ request, order, goods, sku, expectedRefundNo,
|
|
|
+ getMultiQuantityRefundType(order), workerToken));
|
|
|
return;
|
|
|
}
|
|
|
- log.info("微信退款: 退款单号[{}] 退款成功.", outRefundNo);
|
|
|
+ if ("CHANGE".equals(status) || "REFUNDCLOSE".equals(status)) {
|
|
|
+ OrderDonBusinessEvent event = orderDonBusinessEventService.get(
|
|
|
+ order.getId(), OrderDonBusinessEvent.Type.refund);
|
|
|
+ if (event != null && event.getStatus() == OrderDonBusinessEvent.Status.processing
|
|
|
+ && StrUtil.isNotBlank(event.getWorkerToken())) {
|
|
|
+ orderDonTicketRecordService.restoreRefund(order.getId(), event.getWorkerToken(),
|
|
|
+ "WeChat refund " + status);
|
|
|
+ }
|
|
|
+ log.error("微信批量订单退款失败, orderId:{}, status:{}", order.getId(), status);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
+ private void validateWxRefundCallback(Map<String, String> refund, OrderDon order,
|
|
|
+ ShopConfig callbackShop, String expectedRefundNo) {
|
|
|
+ long expectedMoney = Optional.ofNullable(order.getMoney()).orElse(BigDecimal.ZERO).longValueExact();
|
|
|
+ if (!StrUtil.equals(callbackShop.getAppId(), refundShopId(order))
|
|
|
+ || !StrUtil.equals(callbackShop.getAppId(), refund.get("mch_id"))
|
|
|
+ || !StrUtil.equals(weChatConfig.getAppid(), refund.get("appid"))
|
|
|
+ || !StrUtil.equals(expectedRefundNo, refund.get("out_refund_no"))
|
|
|
+ || !StrUtil.equals(order.getTransactionId(), refund.get("transaction_id"))
|
|
|
+ || !StrUtil.equals(order.getOrderNo(), refund.get("out_trade_no"))
|
|
|
+ || parseCent(refund.get("total_fee")) != expectedMoney
|
|
|
+ || parseCent(refund.get("refund_fee")) != expectedMoney) {
|
|
|
+ throw BusinessRuntimeException.getInstance("微信退款回调订单信息不匹配");
|
|
|
+ }
|
|
|
+ }
|
|
|
@Override
|
|
|
@Transactional(rollbackFor = Throwable.class)
|
|
|
public void changeRelation(ChangeRelationReq req) throws Exception {
|
|
|
@@ -744,7 +1306,7 @@ public class CmsOrderDonServiceImpl extends ServiceImpl<OrderDonMapper,OrderDon>
|
|
|
UserTicketClearedRecord.Source source = req.getSource();
|
|
|
if (source == null) source = UserTicketClearedRecord.Source.change_ticket;
|
|
|
GroupsRelation relation = relationMapper.selectById(relationId);
|
|
|
- Long yhsId = relation.getYhsId();
|
|
|
+ Long yhsId = relation == null ? null : relation.getYhsId();
|
|
|
Assert.notNull(relation, "车票已不存在");
|
|
|
if (relation.getRechargeStatus() != null) {
|
|
|
throw BusinessRuntimeException.getInstance("代充账号车票无法更换车票");
|
|
|
@@ -760,14 +1322,21 @@ public class CmsOrderDonServiceImpl extends ServiceImpl<OrderDonMapper,OrderDon>
|
|
|
}
|
|
|
Long userId = relation.getUserId();
|
|
|
List<Long> userIdList = userBindRelationService.getRelationUserIdList(userId, null);
|
|
|
+ OrderDonTicketRecord multiTicketRecord = orderDonTicketRecordService.getActiveByRelationIdForUpdate(relationId);
|
|
|
+ if (multiTicketRecord != null
|
|
|
+ && multiTicketRecord.getStatus() == OrderDonTicketRecord.Status.refunding) {
|
|
|
+ throw BusinessRuntimeException.getInstance("订单退款处理中,暂不支持更换车票");
|
|
|
+ }
|
|
|
try {
|
|
|
log.info("用户userId:{}更换车票relationId:{}", userId, relationId);
|
|
|
- OrderDon orderDon = orderDonMapper.selectOne(Wrappers.lambdaQuery(OrderDon.class)
|
|
|
- .in(OrderDon::getUserId, userIdList)
|
|
|
- .eq(OrderDon::getRelationId, relationId)
|
|
|
- .notIn(OrderDon::getStatus, Constant.noOrderStatus)
|
|
|
- .orderByDesc(OrderDon::getId)
|
|
|
- .last("limit 1"));
|
|
|
+ OrderDon orderDon = multiTicketRecord == null
|
|
|
+ ? orderDonMapper.selectOne(Wrappers.lambdaQuery(OrderDon.class)
|
|
|
+ .in(OrderDon::getUserId, userIdList)
|
|
|
+ .eq(OrderDon::getRelationId, relationId)
|
|
|
+ .notIn(OrderDon::getStatus, Constant.noOrderStatus)
|
|
|
+ .orderByDesc(OrderDon::getId)
|
|
|
+ .last("limit 1"))
|
|
|
+ : orderDonMapper.selectById(multiTicketRecord.getOrderId());
|
|
|
OrderDiscountPlusPurchaseSkuRelation plusPurchaseSkuRelation = null;
|
|
|
List<Long> plus_relationList = null;
|
|
|
if (orderDon == null) {
|
|
|
@@ -865,7 +1434,9 @@ public class CmsOrderDonServiceImpl extends ServiceImpl<OrderDonMapper,OrderDon>
|
|
|
relationClearService.clearTicket(relation, source);
|
|
|
|
|
|
//兜底处理 多余车票
|
|
|
- clearOtherChangeTicket(userId, relationId, newRelation, orderDon.getSkuId());
|
|
|
+ if (multiTicketRecord == null) {
|
|
|
+ clearOtherChangeTicket(userId, relationId, newRelation, orderDon.getSkuId());
|
|
|
+ }
|
|
|
|
|
|
//奈飞车票
|
|
|
if (orderDon.getGoodsId() == 1) {
|
|
|
@@ -904,6 +1475,10 @@ public class CmsOrderDonServiceImpl extends ServiceImpl<OrderDonMapper,OrderDon>
|
|
|
}
|
|
|
|
|
|
if ((newRelation.getStartTime() == null || newRelation.getExpiryTime() == null)) {
|
|
|
+ if (multiTicketRecord != null) {
|
|
|
+ relationMapper.updateById(newRelation);
|
|
|
+ return;
|
|
|
+ }
|
|
|
List<OrderDon> orderDonList = orderDonMapper.selectList(Wrappers.lambdaQuery(OrderDon.class).eq(OrderDon::getUserId, userId).eq(OrderDon::getRelationId, relationId).eq(OrderDon::getStatus, OrderDon.Status.hasPayment));
|
|
|
if (orderDonList.isEmpty()) {
|
|
|
orderDonList = orderDonMapper.selectList(Wrappers.lambdaQuery(OrderDon.class)
|
|
|
@@ -942,7 +1517,9 @@ public class CmsOrderDonServiceImpl extends ServiceImpl<OrderDonMapper,OrderDon>
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- List<OrderDon> orderDonList = orderDonMapper.selectList(Wrappers.lambdaQuery(OrderDon.class).eq(OrderDon::getUserId, userId).eq(OrderDon::getRelationId, relationId).eq(OrderDon::getStatus, OrderDon.Status.complete));
|
|
|
+ List<OrderDon> orderDonList = multiTicketRecord == null
|
|
|
+ ? orderDonMapper.selectList(Wrappers.lambdaQuery(OrderDon.class).eq(OrderDon::getUserId, userId).eq(OrderDon::getRelationId, relationId).eq(OrderDon::getStatus, OrderDon.Status.complete))
|
|
|
+ : java.util.Collections.emptyList();
|
|
|
orderDonList.forEach(completeOrder->{
|
|
|
completeOrder.setRelationId(newRelation.getId());
|
|
|
orderDonMapper.updateById(completeOrder);
|
|
|
@@ -1156,12 +1733,16 @@ public class CmsOrderDonServiceImpl extends ServiceImpl<OrderDonMapper,OrderDon>
|
|
|
/**
|
|
|
* 退款后 处理关联
|
|
|
*/
|
|
|
- public void commonRefund(OrderDon orderDon, BigDecimal refundMoney, String operator, GoodsDon goodsDon, GoodsDonSku sku, String outNo, String refundType, Boolean isOnly, BigDecimal refundBalance, String chatImg, BigDecimal deductBalance, List<Long> dpRelationIds) {
|
|
|
+ public void commonRefund(OrderDon orderDon, BigDecimal refundMoney, String operator, GoodsDon goodsDon,
|
|
|
+ GoodsDonSku sku, String outNo, String refundType, Boolean isOnly,
|
|
|
+ BigDecimal refundBalance, String chatImg, BigDecimal deductBalance,
|
|
|
+ List<Long> dpRelationIds, boolean multiQuantityOrder) {
|
|
|
orderRefundService.refundRecord(orderDon, refundMoney, refundBalance, operator, goodsDon, sku, outNo, refundType, chatImg);
|
|
|
- //退款一次
|
|
|
- goodsDonStockService.upStockRelate(null, orderDon.getId(), "refund");
|
|
|
+ if (shouldRestoreStock(goodsDon, multiQuantityOrder)) {
|
|
|
+ goodsDonStockService.upStockRelate(null, orderDon.getId(), "refund");
|
|
|
+ }
|
|
|
try {
|
|
|
- if (!orderDon.getIsDp()) {
|
|
|
+ if (!multiQuantityOrder && !orderDon.getIsDp()) {
|
|
|
GroupsRelation relation = relationMapper.selectById(orderDon.getRelationId());
|
|
|
//冻结车票 清除
|
|
|
if (relation != null && relation.getIsFrozen()) {
|
|
|
@@ -1237,9 +1818,16 @@ public class CmsOrderDonServiceImpl extends ServiceImpl<OrderDonMapper,OrderDon>
|
|
|
} catch (Exception e) {
|
|
|
log.error("wx减少抽奖机会错误,orderId:{},error:{}", orderDon.getId(), e);
|
|
|
}
|
|
|
- if (OrderDon.Type.BALANCE.getType().equals(refundType)) {
|
|
|
- if (isOnly == null && orderDon.getMoney().compareTo(BigDecimal.ZERO) > 0) {
|
|
|
- userBenefitsService.addUserBalance(orderDon.getUserId(), refundMoney, UserBalanceSourceRecord.Source.refund, orderDon.getYhsId(), orderDon.getId(), goodsDon.getTitle() + UserBalanceSourceRecord.Source.refund.getDesc(), true);
|
|
|
+ if (OrderRefundReq.RefundType.balance.name().equals(refundType)) {
|
|
|
+ if (isOnly == null) {
|
|
|
+ BigDecimal balanceAmount = Optional.ofNullable(refundBalance)
|
|
|
+ .orElse(Optional.ofNullable(refundMoney).orElse(BigDecimal.ZERO));
|
|
|
+ if (balanceAmount.compareTo(BigDecimal.ZERO) > 0) {
|
|
|
+ userBenefitsService.addUserBalance(orderDon.getUserId(), balanceAmount,
|
|
|
+ UserBalanceSourceRecord.Source.refund, orderDon.getYhsId(),
|
|
|
+ orderDon.getId(), goodsDon.getTitle()
|
|
|
+ + UserBalanceSourceRecord.Source.refund.getDesc(), true);
|
|
|
+ }
|
|
|
}
|
|
|
} else {
|
|
|
//余额退款不需要扣除积分
|
|
|
@@ -1313,7 +1901,7 @@ public class CmsOrderDonServiceImpl extends ServiceImpl<OrderDonMapper,OrderDon>
|
|
|
}
|
|
|
|
|
|
//同步车票退款 重购记录
|
|
|
- TASK_POOL.execute(() -> {
|
|
|
+ Runnable syncRebuyRecords = () -> {
|
|
|
GroupsRelationExpiryRecord groupsRelationExpiryRecord = groupsRelationExpiryRecordService.getOne(Wrappers.lambdaQuery(GroupsRelationExpiryRecord.class)
|
|
|
.in(GroupsRelationExpiryRecord::getReBuyOrderId, orderDon.getId())
|
|
|
.eq(GroupsRelationExpiryRecord::getRelationId, orderDon.getRelationId()).last("limit 1"));
|
|
|
@@ -1321,19 +1909,26 @@ public class CmsOrderDonServiceImpl extends ServiceImpl<OrderDonMapper,OrderDon>
|
|
|
log.info("订单orderId:{}退款,同步是否有重购记录不是直接续费车票", orderDon.getId());
|
|
|
List<GroupsRelationExpiryRecord> groupsRelationExpiryRecords = groupsRelationExpiryRecordService.list(Wrappers.lambdaQuery(GroupsRelationExpiryRecord.class)
|
|
|
.in(GroupsRelationExpiryRecord::getReBuyOrderId, orderDon.getId()));
|
|
|
- log.info("订单orderId:{}退款,重购记录不是直接续费车票大小:{}", groupsRelationExpiryRecords.size());
|
|
|
+ log.info("订单orderId:{}退款,重购记录不是直接续费车票大小:{}", orderDon.getId(), groupsRelationExpiryRecords.size());
|
|
|
if (CollUtil.isNotEmpty(groupsRelationExpiryRecords)) {
|
|
|
- groupsRelationExpiryRecords.forEach(data -> {
|
|
|
- data.setIsReBuy(false);
|
|
|
- });
|
|
|
+ groupsRelationExpiryRecords.forEach(data -> data.setIsReBuy(false));
|
|
|
groupsRelationExpiryRecordService.saveOrUpdateBatch(groupsRelationExpiryRecords);
|
|
|
}
|
|
|
- }
|
|
|
- if (groupsRelationExpiryRecord != null) {
|
|
|
+ } else {
|
|
|
groupsRelationExpiryRecord.setIsReBuy(false);
|
|
|
groupsRelationExpiryRecordService.saveOrUpdate(groupsRelationExpiryRecord);
|
|
|
}
|
|
|
- });
|
|
|
+ };
|
|
|
+ if (TransactionSynchronizationManager.isActualTransactionActive()) {
|
|
|
+ TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
|
|
|
+ @Override
|
|
|
+ public void afterCommit() {
|
|
|
+ TASK_POOL.execute(syncRebuyRecords);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ TASK_POOL.execute(syncRebuyRecords);
|
|
|
+ }
|
|
|
|
|
|
//是否是礼品卡 现金支付订单
|
|
|
BigDecimal gcCash = orderDon.getGcCash();
|
|
|
@@ -1426,6 +2021,10 @@ public class CmsOrderDonServiceImpl extends ServiceImpl<OrderDonMapper,OrderDon>
|
|
|
}
|
|
|
|
|
|
//处理下级渠道分销提成
|
|
|
+ static boolean shouldRestoreStock(GoodsDon goodsDon, boolean multiQuantityOrder) {
|
|
|
+ return !multiQuantityOrder || goodsDon == null || goodsDon.getType() != 1;
|
|
|
+ }
|
|
|
+
|
|
|
public void handleSubRewards(OrderDon orderDon) {
|
|
|
//是否是分销订单
|
|
|
//处理下级渠道分销提成
|
|
|
@@ -1543,6 +2142,9 @@ public class CmsOrderDonServiceImpl extends ServiceImpl<OrderDonMapper,OrderDon>
|
|
|
if (orderDon == null) {
|
|
|
throw BusinessRuntimeException.getInstance("订单不存在");
|
|
|
}
|
|
|
+ if (orderDonTicketRecordService.exists(orderId)) {
|
|
|
+ throw BusinessRuntimeException.getInstance("Multi-quantity orders only support whole-order refunds");
|
|
|
+ }
|
|
|
if (orderDon.getMoney().compareTo(BigDecimal.ZERO) == 0) {
|
|
|
throw BusinessRuntimeException.getInstance("该订单是0元订单,无法补差价");
|
|
|
}
|
|
|
@@ -1607,6 +2209,12 @@ public class CmsOrderDonServiceImpl extends ServiceImpl<OrderDonMapper,OrderDon>
|
|
|
}
|
|
|
Long orderId = refundOrderDon.getOrderId();
|
|
|
OrderDon orderDon = orderDonMapper.selectById(orderId);
|
|
|
+ if (orderDon == null) {
|
|
|
+ throw BusinessRuntimeException.getInstance("Order does not exist");
|
|
|
+ }
|
|
|
+ if (orderDonTicketRecordService.exists(orderId)) {
|
|
|
+ throw BusinessRuntimeException.getInstance("Multi-quantity orders only support whole-order refunds");
|
|
|
+ }
|
|
|
if (orderDon.getMoney().compareTo(BigDecimal.ZERO) == 0) {
|
|
|
throw BusinessRuntimeException.getInstance("该记录对应的订单无实际支付金额");
|
|
|
}
|