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
package com.talentcard.server.business.order;
 
import java.util.List;
import java.util.Map;
 
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.Propagation;
import org.springframework.transaction.annotation.Transactional;
 
/**
 * 订单资金边界。
 *
 * <p>一个订单的核销、退款和提现锁定都会先锁定 {@code benefit_order} 的同一行,因此它们按订单串行化:
 * 不会出现「退了款又核销」「核销后被退款」「已经进了提现记录又撤销核销」。
 *
 * <p>{@code refund_status} 由本服务的退款三个方法维护。出款已改由斗拱按结算周期自动完成,
 * 平台不再生成提现记录,因此订单上不再有「已进入提现记录」这一状态。
 * 核销、退款、提现锁定与解锁的业务规则都收敛在这里,调用方只负责在拿到 {@link LockedOrder} 后调用
 * 对应断言。
 *
 * <p>{@link #lock(long)} 要求已经存在数据库事务:行锁只在事务内有效,脱离事务调用会直接失败,
 * 而不是静默地失去串行化能力。其它方法都是单条条件更新,本身就是原子的。
 */
@Service
public class OrderFundBoundaryService {
 
    /** 订单退款状态:没有生效中的退款。 */
    public static final long REFUND_NONE = 0;
    /** 订单退款状态:退款已受理、结果未定。 */
    public static final long REFUND_IN_PROGRESS = 1;
    /** 订单退款状态:退款成功(部分退款也算整单退出结算)。 */
    public static final long REFUNDED = 2;
 
    private final JdbcTemplate jdbc;
 
    public OrderFundBoundaryService(JdbcTemplate jdbc) {
        this.jdbc = jdbc;
    }
 
    /**
     * 锁定订单行并返回它的资金状态。拿到的状态在整个事务内不会被子资金状态变更改动,
     * 因此所有判断都必须基于本方法的返回值。
     */
    @Transactional(propagation = Propagation.MANDATORY)
    public LockedOrder lock(long orderId) {
        List<Map<String, Object>> rows = jdbc.queryForList("""
                select id, order_status, verify_status, refund_status
                from benefit_order
                where id = ? and deleted = 0
                limit 1 for update
                """, orderId);
        if (rows.isEmpty()) {
            throw new BusinessException(ErrorCode.NOT_FOUND, "订单不存在");
        }
        return LockedOrder.fromRow(rows.get(0));
    }
 
    /** 退款受理成功:本笔退款结清前订单不可核销,也不参与提现汇总。 */
    public void markRefundInProgress(long orderId) {
        int updated = jdbc.update("""
                update benefit_order set refund_status = 1
                where id = ? and deleted = 0 and refund_status = 0
                """, orderId);
        if (updated != 1) {
            throw new BusinessException(ErrorCode.CONFLICT, "订单已提交退款申请");
        }
    }
 
    /** 退款成功:整单退出可核销与可提现范围,部分退款同样按整单处理。 */
    public void markRefunded(long orderId) {
        jdbc.update("update benefit_order set refund_status = 2 where id = ? and deleted = 0 and refund_status <> 2", orderId);
    }
 
    /** 退款失败:没有资金变动,订单回到未退款状态。已有成功退款的订单不会被退回。 */
    public void markRefundFailed(long orderId) {
        jdbc.update("""
                update benefit_order o
                set o.refund_status = 0
                where o.id = ? and o.deleted = 0 and o.refund_status = 1
                  and not exists (select 1 from order_refund r
                                  where r.order_id = o.id and r.deleted = 0 and r.refund_status = 2)
                """, orderId);
    }
 
    /** 一条已被行锁固定的订单资金状态。 */
    public record LockedOrder(long orderId, long orderStatus, long verifyStatus, long refundStatus) {
 
        static LockedOrder fromRow(Map<String, Object> row) {
            return new LockedOrder(
                    number(row.get("id")),
                    number(row.get("order_status")),
                    number(row.get("verify_status")),
                    number(row.get("refund_status")));
        }
 
        /** 未支付、已核销、已退款或退款中的订单都不能再核销。 */
        public void requireVerifiable() {
            if (orderStatus != 1) {
                throw new BusinessException(ErrorCode.CONFLICT, "订单未支付,不能核销");
            }
            if (verifyStatus == 1) {
                throw new BusinessException(ErrorCode.CONFLICT, "订单已核销");
            }
            if (refundStatus != REFUND_NONE) {
                throw new BusinessException(ErrorCode.CONFLICT, "订单已退款或退款中,不能核销");
            }
        }
 
        /** 未支付、已退款或退款中的订单都不能再退款。 */
        public void requireRefundable() {
            if (orderStatus != 1) {
                throw new BusinessException(ErrorCode.CONFLICT, "订单当前状态不可退款");
            }
            if (refundStatus == REFUND_IN_PROGRESS) {
                throw new BusinessException(ErrorCode.CONFLICT, "订单退款处理中,不能重复退款");
            }
            if (refundStatus == REFUNDED) {
                throw new BusinessException(ErrorCode.CONFLICT, "订单已退款");
            }
        }
 
        private static long number(Object value) {
            return value instanceof Number number ? number.longValue() : 0L;
        }
    }
}