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
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
package com.talentcard.server.business.payment;
 
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.List;
import java.util.Map;
import java.util.Set;
 
import com.alibaba.fastjson.JSON;
import com.talentcard.server.business.DatabaseRows;
import com.talentcard.server.business.order.OrderFundBoundaryService;
import com.talentcard.server.business.payment.PaymentSettlementService.ProviderPaymentFact;
import com.talentcard.server.business.payment.PaymentSettlementService.SettlementOutcome;
import com.talentcard.server.business.payment.huifu.HuifuPayload;
import com.talentcard.server.business.payment.huifu.HuifuPayload.TransStatus;
import com.talentcard.server.business.payment.huifu.HuifuSignatureService;
import com.talentcard.server.common.BusinessException;
import com.talentcard.server.common.ErrorCode;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
 
/**
 * Handles Huifu asynchronous notifications.
 *
 * <p>The signature is verified first; only then is the payload interpreted. A successful payment
 * notification is handed to {@link PaymentSettlementService}, which compares the request number,
 * merchant number and amount with the order's fixed collecting sub-merchant before any state
 * changes. Duplicated notifications for an already processed request are acknowledged without
 * touching business state.
 */
@Service
public class HuifuPaymentCallbackService {
 
    private static final String PROVIDER = "HUIFU";
 
    /** Event states that already describe a final decision for this exact payload. */
    private static final Set<String> TERMINAL_EVENT_STATUSES = Set.of(
            "PROCESSED", "PAYMENT_CLOSED", "UNKNOWN_STATUS",
            "MERCHANT_MISMATCH", "MERCHANT_UNVERIFIED", "AMOUNT_MISMATCH", "COLLECTOR_UNKNOWN");
 
    private final JdbcTemplate jdbc;
    private final HuifuSignatureService signatureService;
    private final PaymentSettlementService settlementService;
    private final OrderFundBoundaryService orderFundBoundary;
 
    public HuifuPaymentCallbackService(JdbcTemplate jdbc, HuifuSignatureService signatureService, PaymentSettlementService settlementService, OrderFundBoundaryService orderFundBoundary) {
        this.jdbc = jdbc;
        this.signatureService = signatureService;
        this.settlementService = settlementService;
        this.orderFundBoundary = orderFundBoundary;
    }
 
    @Transactional
    public String handle(String responseData, String signature) {
        if (!signatureService.verifyCallbackPayload(responseData, signature)) {
            throw new BusinessException(ErrorCode.FORBIDDEN, "汇付回调验签失败");
        }
        Map<String, Object> envelope = parse(responseData);
        Map<String, Object> data = HuifuPayload.businessData(envelope);
        String requestNo = HuifuPayload.firstNonBlank(HuifuPayload.value(data, "req_seq_id"), HuifuPayload.value(envelope, "req_seq_id"));
        if (requestNo.isBlank()) throw new BusinessException(ErrorCode.BAD_REQUEST, "汇付回调缺少请求流水号");
 
        String payloadHash = sha256(responseData);
        boolean alreadyHandled = alreadyHandled(requestNo, payloadHash);
        upsertEvent(requestNo, payloadHash, "RECEIVED");
        if (alreadyHandled) return acknowledgement(requestNo);
 
        if (handleRefund(requestNo, data)) {
            return acknowledgement(requestNo);
        }
 
        String transStatus = HuifuPayload.value(data, "trans_stat", "transStat");
        if (HuifuPayload.status(transStatus) == TransStatus.UNKNOWN) {
            updateEvent(requestNo, "UNKNOWN_STATUS");
            return acknowledgement(requestNo);
        }
 
        if (!transactionExists(requestNo)) {
            updateEvent(requestNo, "UNMATCHED");
            return acknowledgement(requestNo);
        }
 
        if (!HuifuPayload.paid(transStatus, HuifuPayload.code(data))) {
            // The provider did not confirm money: keep the payment pending for the query path.
            jdbc.update("""
                    update payment_transaction
                    set provider_response_code = ?, provider_response_message = ?
                    where provider = ? and payment_no = ? and status in ('CREATED', 'CREATE_FAILED', 'PREPAY_CREATED', 'CREATE_UNKNOWN')
                    """, HuifuPayload.code(data), HuifuPayload.message(data), PROVIDER, requestNo);
            updateEvent(requestNo, "PENDING_QUERY");
            return acknowledgement(requestNo);
        }
 
        SettlementOutcome outcome = settlementService.settle(new ProviderPaymentFact(
                requestNo,
                HuifuPayload.value(data, "huifu_id", "huifuId"),
                HuifuPayload.amountInCent(HuifuPayload.value(data, "trans_amt", "transAmt")),
                providerTransactionNo(data),
                HuifuPayload.code(data),
                HuifuPayload.message(data),
                "CALLBACK"));
        updateEvent(requestNo, eventStatus(outcome));
        return acknowledgement(requestNo);
    }
 
    private boolean handleRefund(String refundNo, Map<String, Object> data) {
        List<Map<String, Object>> refunds = rows("""
                select id, order_id, refund_status
                from order_refund
                where deleted = 0 and refund_no = ?
                limit 1 for update
                """, refundNo);
        if (refunds.isEmpty()) return false;
 
        Map<String, Object> refund = refunds.get(0);
        long orderId = ((Number) refund.get("orderId")).longValue();
        String transStatus = HuifuPayload.value(data, "trans_stat", "transStat");
        String code = HuifuPayload.code(data);
        if (HuifuPayload.paid(transStatus, code)) {
            jdbc.update("""
                    update order_refund
                    set refund_status = 2, provider = 'HUIFU', provider_refund_no = ?,
                        provider_response_code = ?, provider_response_message = ?, refund_time = coalesce(refund_time, now())
                    where id = ? and refund_status = 1
                    """, providerTransactionNo(data), code, HuifuPayload.message(data), refund.get("id"));
            // 通道确认退款成功是最终事实:订单级退款状态无条件收敛为已退款。
            orderFundBoundary.markRefunded(orderId);
            jdbc.update("""
                    update benefit_order o
                    set o.order_status = 2
                    where o.id = ? and o.order_status = 1
                      and o.pay_amount <= (select coalesce(sum(r.refund_amount), 0) from order_refund r where r.order_id = o.id and r.refund_status = 2 and r.deleted = 0)
                    """, refund.get("orderId"));
            updateEvent(refundNo, "PROCESSED");
        } else if (HuifuPayload.terminalFailure(transStatus, code)) {
            int failed = jdbc.update("""
                    update order_refund
                    set provider_response_code = ?, provider_response_message = ?, refund_status = 3
                    where id = ? and refund_status = 1
                    """, code, HuifuPayload.message(data), refund.get("id"));
            // 只有本事务真的把退款置为失败,才把订单退回未退款状态;否则会覆盖并发的退款成功。
            if (failed == 1) orderFundBoundary.markRefundFailed(orderId);
            updateEvent(refundNo, "REFUND_FAILED");
        } else {
            jdbc.update("""
                    update order_refund
                    set provider_response_code = ?, provider_response_message = ?
                    where id = ? and refund_status = 1
                    """, code, HuifuPayload.message(data), refund.get("id"));
            updateEvent(refundNo, "PENDING_QUERY");
        }
        return true;
    }
 
    private String eventStatus(SettlementOutcome outcome) {
        return switch (outcome) {
            case SETTLED, ALREADY_SETTLED -> "PROCESSED";
            case CLOSED_LATE_SUCCESS -> "PAYMENT_CLOSED";
            case MERCHANT_MISMATCH -> "MERCHANT_MISMATCH";
            case MERCHANT_UNVERIFIED -> "MERCHANT_UNVERIFIED";
            case AMOUNT_MISMATCH -> "AMOUNT_MISMATCH";
            case COLLECTOR_UNKNOWN -> "COLLECTOR_UNKNOWN";
            case UNMATCHED -> "UNMATCHED";
            case NOT_SETTLEABLE -> "PENDING_QUERY";
        };
    }
 
    private boolean transactionExists(String paymentNo) {
        Long count = jdbc.queryForObject("select count(*) from payment_transaction where provider = ? and payment_no = ?", Long.class, PROVIDER, paymentNo);
        return count != null && count > 0;
    }
 
    private boolean alreadyHandled(String requestNo, String payloadHash) {
        List<Map<String, Object>> rows = rows("""
                select process_status, payload_sha256
                from payment_callback_event
                where provider = ? and provider_request_no = ?
                limit 1
                """, PROVIDER, requestNo);
        if (rows.isEmpty()) return false;
        Map<String, Object> event = rows.get(0);
        return payloadHash.equals(HuifuPayload.text(event.get("payloadSha256")))
                && TERMINAL_EVENT_STATUSES.contains(HuifuPayload.text(event.get("processStatus")));
    }
 
    private void upsertEvent(String requestNo, String payloadHash, String status) {
        jdbc.update("""
                insert into payment_callback_event(provider, provider_request_no, payload_sha256, signature_valid, process_status)
                values (?, ?, ?, 1, ?)
                on duplicate key update payload_sha256 = values(payload_sha256), signature_valid = 1, received_at = now()
                """, PROVIDER, requestNo, payloadHash, status);
    }
 
    private void updateEvent(String requestNo, String status) {
        jdbc.update("""
                update payment_callback_event set process_status = ?, processed_at = now()
                where provider = ? and provider_request_no = ?
                """, status, PROVIDER, requestNo);
    }
 
    private String providerTransactionNo(Map<String, Object> data) {
        return HuifuPayload.value(data, "hf_seq_id", "bank_order_no", "hfSeqId");
    }
 
    private String acknowledgement(String requestNo) {
        return "RECV_ORD_ID_" + requestNo;
    }
 
    @SuppressWarnings("unchecked")
    private Map<String, Object> parse(String payload) {
        try {
            return JSON.parseObject(payload, Map.class);
        } catch (Exception exception) {
            throw new BusinessException(ErrorCode.BAD_REQUEST, "汇付回调数据格式错误");
        }
    }
 
    private String sha256(String value) {
        try {
            byte[] digest = MessageDigest.getInstance("SHA-256").digest(value.getBytes(StandardCharsets.UTF_8));
            StringBuilder builder = new StringBuilder();
            for (byte current : digest) builder.append(String.format("%02x", current));
            return builder.toString();
        } catch (Exception exception) {
            throw new BusinessException(ErrorCode.INTERNAL_ERROR, "支付回调摘要失败");
        }
    }
 
    private List<Map<String, Object>> rows(String sql, Object... args) {
        return DatabaseRows.normalize(jdbc.queryForList(sql, args));
    }
}