liujie
2026-09-14 82a8bc62d5b9d5d607f6952329952b17eda2f0a0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
package com.tourguide.modules.order.service;
 
import com.alibaba.fastjson.JSONObject;
import com.tourguide.common.exception.BusinessException;
import com.tourguide.common.huifu.properties.HuiFuProperties;
import com.tourguide.modules.order.domain.OrderStatus;
import com.tourguide.modules.order.domain.RefundAmountCalculator;
import com.tourguide.modules.order.domain.RefundStatus;
import com.tourguide.modules.order.entity.TourOrder;
import com.tourguide.modules.order.entity.TourOrderRefund;
import com.tourguide.modules.order.mapper.TourOrderMapper;
import com.tourguide.modules.order.mapper.TourOrderRefundMapper;
import com.tourguide.modules.finance.service.GuideBalanceSettlementService;
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
 
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
 
@Service
public class HuifuRefundCallbackService {
    private static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
 
    private final HuiFuProperties properties;
    private final TourOrderMapper orderMapper;
    private final TourOrderRefundMapper refundMapper;
    private final GuideBalanceSettlementService balanceSettlementService;
    private final GuideSessionTransitionService sessionTransitionService;
 
    public HuifuRefundCallbackService(HuiFuProperties properties, TourOrderMapper orderMapper) {
        this(properties, orderMapper, null, null, null);
    }
 
    public HuifuRefundCallbackService(HuiFuProperties properties, TourOrderMapper orderMapper,
                                      TourOrderRefundMapper refundMapper) {
        this(properties, orderMapper, refundMapper, null, null);
    }
 
    public HuifuRefundCallbackService(HuiFuProperties properties, TourOrderMapper orderMapper,
                                      TourOrderRefundMapper refundMapper,
                                      GuideBalanceSettlementService balanceSettlementService) {
        this(properties, orderMapper, refundMapper, balanceSettlementService, null);
    }
 
    @Autowired
    public HuifuRefundCallbackService(HuiFuProperties properties, TourOrderMapper orderMapper,
                                      TourOrderRefundMapper refundMapper,
                                      GuideBalanceSettlementService balanceSettlementService,
                                      GuideSessionTransitionService sessionTransitionService) {
        this.properties = properties;
        this.orderMapper = orderMapper;
        this.refundMapper = refundMapper;
        this.balanceSettlementService = balanceSettlementService;
        this.sessionTransitionService = sessionTransitionService;
    }
 
    @Transactional
    public boolean handle(JSONObject body) {
        String refundOrderNo = first(body, "req_seq_id", "refund_req_seq_id");
        if (!StringUtils.hasText(refundOrderNo)) {
            throw new BusinessException(400, "Huifu refund callback request sequence is missing");
        }
        validateMerchant(first(body, "huifu_id", "out_huifu_id"));
        TourOrderRefund refundRecord = refundMapper == null
                ? null : refundMapper.selectByRefundOrderNoForUpdate(refundOrderNo);
        TourOrder order = refundRecord == null
                ? orderMapper.selectByRefundOrderNoForUpdate(refundOrderNo)
                : orderMapper.selectForUpdate(refundRecord.getOrderId());
        if (order == null) throw new BusinessException(404, "Refund order does not exist");
 
        String status = first(body, "refund_stat", "trans_stat");
        String result = StringUtils.hasText(status) ? status : first(body, "resp_code", "code");
        if (!isSuccess(result)) {
            if (isRefunding(order, refundRecord)) {
                order.setRefundStatus(RefundStatus.REFUND_FAILED.getCode());
                updateRefundRecord(refundRecord, RefundStatus.REFUND_FAILED.getCode(), null);
                orderMapper.updateById(order);
            }
            return false;
        }
        if (!isRefunding(order, refundRecord)) {
            return false;
        }
        boolean restoreSession = OrderStatus.PENDING_MEETING.matches(order.getStatus())
                && order.getGuideId() != null && order.getReceivingSessionId() != null;
        BigDecimal callbackAmount = amount(body);
        BigDecimal expectedAmount = refundRecord == null
                ? order.getRefundAmount() : refundRecord.getRefundAmount();
        if (expectedAmount == null || expectedAmount.compareTo(callbackAmount) != 0) {
            throw new BusinessException(409, "Refund amount does not match order");
        }
        BigDecimal previousRefundedAmount = RefundAmountCalculator.refundedAmount(order);
        BigDecimal refundedAmount = previousRefundedAmount.add(callbackAmount);
        BigDecimal paidAmount = RefundAmountCalculator.paidAmount(order);
        if (refundedAmount.compareTo(paidAmount) > 0) {
            throw new BusinessException(409, "Refund amount exceeds order payment");
        }
        order.setRefundedAmount(refundedAmount);
        order.setRefundAmount(callbackAmount);
        if (refundedAmount.compareTo(paidAmount) >= 0) {
            order.setRefundStatus(RefundStatus.REFUNDED.getCode());
            order.setStatus(OrderStatus.REFUNDED.getCode());
        } else {
            order.setRefundStatus(RefundStatus.PARTIAL.getCode());
            order.setStatus(OrderStatus.CANCELLED.getCode());
        }
        LocalDateTime refundCompletedAt = callbackTime(body);
        order.setRefundTime(refundCompletedAt);
        if (order.getCancelledAt() == null) order.setCancelledAt(refundCompletedAt);
        if (balanceSettlementService != null) {
            balanceSettlementService.reverseRefund(order, previousRefundedAmount, refundedAmount);
        }
        // 回调确认的是当前退款流水,明细必须单独标记为已完成;订单状态仍表示累计退款进度。
        updateRefundRecord(refundRecord, RefundStatus.REFUNDED.getCode(), refundCompletedAt);
        orderMapper.updateById(order);
        if (restoreSession && sessionTransitionService != null) {
            sessionTransitionService.restoreAfterOrderCancellation(order);
        }
        return true;
    }
 
    private boolean isRefunding(TourOrder order, TourOrderRefund refund) {
        return refund == null
                ? RefundStatus.REFUNDING.matches(order.getRefundStatus())
                : RefundStatus.REFUNDING.matches(refund.getRefundStatus());
    }
 
    private void updateRefundRecord(TourOrderRefund refund, String status, LocalDateTime refundTime) {
        if (refund == null) return;
        refund.setRefundStatus(status);
        refund.setRefundTime(refundTime);
        refundMapper.updateById(refund);
    }
 
    private void validateMerchant(String callbackHuifuId) {
        if (!StringUtils.hasText(callbackHuifuId)
                || !callbackHuifuId.equals(properties.getHuifuId())) {
            throw new BusinessException(400, "Huifu refund callback merchant does not match");
        }
    }
 
    private BigDecimal amount(JSONObject body) {
        String value = first(body, "ord_amt", "refund_amt", "trans_amt");
        if (!StringUtils.hasText(value)) {
            throw new BusinessException(400, "Huifu refund callback amount is missing");
        }
        try {
            return new BigDecimal(value);
        } catch (NumberFormatException exception) {
            throw new BusinessException(400, "Huifu refund callback amount is invalid");
        }
    }
 
    private LocalDateTime callbackTime(JSONObject body) {
        String value = first(body, "end_time", "trans_time", "refund_time");
        if (!StringUtils.hasText(value)) return LocalDateTime.now();
        String normalized = value.replace("-", "").replace(":", "").replace(" ", "");
        try {
            return LocalDateTime.parse(normalized, TIME_FORMAT);
        } catch (DateTimeParseException exception) {
            return LocalDateTime.now();
        }
    }
 
    private boolean isSuccess(String status) {
        return "S".equalsIgnoreCase(status) || "SUCCESS".equalsIgnoreCase(status)
                || "SUCCEEDED".equalsIgnoreCase(status) || "00000100".equalsIgnoreCase(status);
    }
 
    private String first(JSONObject body, String... keys) {
        if (body == null) return null;
        for (String key : keys) {
            String value = body.getString(key);
            if (StringUtils.hasText(value)) return value;
        }
        JSONObject data = body.getJSONObject("data");
        if (data != null) return first(data, keys);
        return null;
    }
}