shinnosuke
4 天以前 6b9a855848ad31bb80fcb4966f85512ae8ec6421
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
import java.util.Map;
import java.util.UUID;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;
import static org.mockito.Mockito.*;
import com.talentcard.server.business.merchant.*;
import com.talentcard.server.business.merchant.auth.MerchantPrincipal;
import com.talentcard.server.business.admin.MerchantSettlementAdminService;
import com.talentcard.server.business.payment.*;
import com.talentcard.server.business.payment.huifu.*;
 
/** Audit-only characterization probe. REPRODUCED means an unsafe behavior exists, not a safety test passing.
 * Uses actual service code and SQL in isolated H2; only provider calls/signature verification are mocked.
 * Does not load application configuration, start Spring, or contact any external service.
 */
public class PaymentFlowAuditProbe {
    private JdbcTemplate jdbc;
    private TransactionTemplate tx;
    private MerchantSettlementService settlement;
    private HuifuMerchantWithdrawalService withdrawals;
 
    public static void main(String[] args) throws Exception {
        PaymentFlowAuditProbe p = new PaymentFlowAuditProbe();
        p.multipleStoresCountSameOrder();
        p.partialRefundDoesNotReduceAvailable();
        p.adminFailureReleasesUnconfirmedWithdrawal();
        p.differentKeysCreateTwoActivePayments();
        p.expiredOrderAcceptsMoneyButRemainsCancelled();
        p.callbackDoesNotValidateAmount();
        p.withdrawalOverwritesPaymentSdkConfiguration();
        System.out.println("REPRODUCED 7/7 audit scenarios (not evidence of financial safety).");
    }
 
    private void setup() {
        DriverManagerDataSource ds = new DriverManagerDataSource(
            "jdbc:h2:mem:audit_" + UUID.randomUUID() + ";MODE=MySQL;DATABASE_TO_LOWER=TRUE;DB_CLOSE_DELAY=-1", "sa", "");
        jdbc = new JdbcTemplate(ds);
        tx = new TransactionTemplate(new DataSourceTransactionManager(ds));
        jdbc.execute("create table merchant_store(id bigint primary key, merchant_id bigint, status int, deleted int)");
        jdbc.execute("create table talent_user(id bigint primary key, openid varchar(64), deleted int)");
        jdbc.execute("""
            create table benefit_order(id bigint primary key, order_no varchar(64), talent_id bigint,
              merchant_id bigint, store_id bigint, pay_amount bigint, order_status int, verify_status int,
              deleted int default 0, price_version int default 1, expires_at timestamp,
              product_name_snapshot varchar(64), order_type varchar(32))
            """);
        jdbc.execute("""
            create table merchant_withdrawal(id bigint primary key, merchant_id bigint, store_id bigint,
              withdrawal_no varchar(64), amount_cents bigint, status varchar(32), failure_reason varchar(255),
              reviewed_at timestamp, paid_at timestamp, provider_trade_no varchar(64),
              provider_response_code varchar(64), provider_response_message varchar(255))
            """);
        jdbc.execute("""
            create table payment_transaction(id bigint auto_increment primary key, payment_no varchar(64) unique,
              provider varchar(32), order_id bigint, order_no varchar(64), talent_id bigint, merchant_id bigint,
              amount bigint, status varchar(32), idempotency_key varchar(128), attempt_no int,
              client_payment_payload varchar(2000), provider_transaction_no varchar(64),
              provider_response_code varchar(64), provider_response_message varchar(255), paid_at timestamp,
              unique(idempotency_key, attempt_no))
            """);
        jdbc.execute("""
            create table order_refund(id bigint primary key, order_id bigint, refund_no varchar(64),
              refund_amount bigint, refund_status int, deleted int default 0)
            """);
        jdbc.execute("""
            create table payment_callback_event(provider varchar(32), provider_request_no varchar(64),
              payload_sha256 varchar(64), signature_valid int, process_status varchar(32),
              received_at timestamp, processed_at timestamp, unique(provider, provider_request_no))
            """);
        jdbc.update("insert into merchant_store values(10,1,1,0),(20,1,1,0)");
        jdbc.update("insert into talent_user values(1,'audit-open-id',0)");
        withdrawals = new HuifuMerchantWithdrawalService(jdbc, mock(HuifuWithdrawalGateway.class));
        settlement = new MerchantSettlementService(jdbc, null, null, null, null, null, withdrawals, null, null, null);
    }
 
    private void order(Integer storeId, int state, int verified) {
        jdbc.update("""
            insert into benefit_order(id,order_no,talent_id,merchant_id,store_id,pay_amount,order_status,
                verify_status,expires_at,product_name_snapshot,order_type)
            values(1,'AUDIT-O',1,1,?,10000,?,?,dateadd('MINUTE',15,current_timestamp),'Audit product','STORE_CONSUME')
            """, storeId, state, verified);
    }
    private long available(long storeId) {
        return ((Number) settlement.withdrawalSummary(new MerchantPrincipal(1L, 1L, storeId, "OWNER"))
            .get("availableAmountCents")).longValue();
    }
    private void payment(String state) {
        jdbc.update("""
            insert into payment_transaction(payment_no,provider,order_id,order_no,talent_id,merchant_id,
                amount,status,idempotency_key,attempt_no)
            values('AUDIT-P','HUIFU',1,'AUDIT-O',1,1,10000,?,'audit-key',1)
            """, state);
    }
    private void paidCallback(String amount) {
        HuifuSignatureService signature = mock(HuifuSignatureService.class);
        when(signature.verifyCallbackPayload(anyString(), anyString())).thenReturn(true);
        HuifuPaymentCallbackService callback = new HuifuPaymentCallbackService(jdbc, signature);
        tx.executeWithoutResult(s -> callback.handle("{\"req_seq_id\":\"AUDIT-P\",\"trans_stat\":\"S\","
            + "\"resp_code\":\"00000000\",\"hf_seq_id\":\"AUDIT-HF\",\"trans_amt\":\"" + amount + "\"}", "mocked-valid-signature"));
    }
    private void multipleStoresCountSameOrder() {
        setup(); order(null, 1, 1);
        require(available(10) == 10000 && available(20) == 10000, "multi-store duplication");
        System.out.println("REPRODUCED: one 100.00 order -> store A 100.00 + store B 100.00 available.");
    }
    private void partialRefundDoesNotReduceAvailable() {
        setup(); order(10, 1, 1);
        jdbc.update("insert into order_refund values(1,1,'AUDIT-R',4000,2,0)");
        require(available(10) == 10000, "refund not deducted");
        System.out.println("REPRODUCED: paid 100.00, refunded 40.00 -> still 100.00 withdrawable.");
    }
    private void adminFailureReleasesUnconfirmedWithdrawal() {
        setup(); order(10, 1, 1);
        jdbc.update("insert into merchant_withdrawal(id,merchant_id,store_id,withdrawal_no,amount_cents,status) values(1,1,10,'AUDIT-W',10000,'processing')");
        require(available(10) == 0, "initial reservation");
        new MerchantSettlementAdminService(jdbc, withdrawals, null).reviewWithdrawal(1L, "failed", "audit manual action");
        withdrawals.handleCallback("AUDIT-W", Map.of("resp_code", "00000000", "trans_stat", "S", "hf_seq_id", "AUDIT-PAID"));
        require(available(10) == 10000, "released balance after provider success");
        require("failed".equals(jdbc.queryForObject("select status from merchant_withdrawal where id=1", String.class)), "late success ignored");
        System.out.println("REPRODUCED: admin fails processing withdrawal; provider success ignored; 100.00 released.");
    }
    private void differentKeysCreateTwoActivePayments() {
        setup(); order(10, 0, 0);
        HuifuMiniProgramPaymentGateway gateway = mock(HuifuMiniProgramPaymentGateway.class);
        when(gateway.createMiniProgramPayment(any())).thenReturn(Map.of("data", Map.of("trans_stat", "P", "resp_code", "00000000", "pay_info", Map.of("package", "audit"))));
        HuifuPaymentService service = new HuifuPaymentService(jdbc, tx, gateway);
        service.beginMiniProgramPayment("AUDIT-O", 1L, 1, "device-a");
        service.beginMiniProgramPayment("AUDIT-O", 1L, 1, "device-b");
        require(jdbc.queryForObject("select count(*) from payment_transaction where status='PREPAY_CREATED'", Integer.class) == 2, "two live payment attempts");
        verify(gateway, times(2)).createMiniProgramPayment(any());
        System.out.println("REPRODUCED: one order + two request keys -> two provider prepayments.");
    }
    private void expiredOrderAcceptsMoneyButRemainsCancelled() {
        setup(); order(10, 2, 0); payment("PREPAY_CREATED"); paidCallback("100.00");
        require("SUCCEEDED".equals(jdbc.queryForObject("select status from payment_transaction", String.class)), "payment success");
        require(jdbc.queryForObject("select order_status from benefit_order", Integer.class) == 2, "order remains cancelled");
        require(jdbc.queryForObject("select count(*) from order_refund", Integer.class) == 0, "no compensating refund");
        System.out.println("REPRODUCED: expired order + valid payment success -> SUCCEEDED payment, cancelled order, no refund.");
    }
    private void callbackDoesNotValidateAmount() {
        setup(); order(10, 0, 0); payment("PREPAY_CREATED"); paidCallback("0.01");
        require(jdbc.queryForObject("select order_status from benefit_order", Integer.class) == 1, "amount mismatch accepted");
        System.out.println("REPRODUCED: authenticated callback for 0.01 marks a 100.00 order paid (signature mocked, not bypassed).");
    }
    private void withdrawalOverwritesPaymentSdkConfiguration() throws Exception {
        HuifuPaymentProperties properties = new HuifuPaymentProperties();
        properties.setSystemId("AUDIT-SYSTEM");
        properties.setProductId("AUDIT-PAYMENT-PRODUCT");
        properties.setWithdrawalProductId("AUDIT-WITHDRAWAL-PRODUCT");
        HuifuMiniProgramPaymentGateway payment = new HuifuMiniProgramPaymentGateway(properties);
        HuifuWithdrawalGateway withdrawal = new HuifuWithdrawalGateway(properties);
        var paymentInit = HuifuMiniProgramPaymentGateway.class.getDeclaredMethod("initializeSdk");
        var withdrawalInit = HuifuWithdrawalGateway.class.getDeclaredMethod("initializeSdk");
        paymentInit.setAccessible(true);
        withdrawalInit.setAccessible(true);
        paymentInit.invoke(payment);
        require("AUDIT-PAYMENT-PRODUCT".equals(com.huifu.bspay.sdk.opps.core.BasePay.getConfig(null).getProductId()), "payment SDK initialized");
        withdrawalInit.invoke(withdrawal);
        paymentInit.invoke(payment);
        require("AUDIT-WITHDRAWAL-PRODUCT".equals(com.huifu.bspay.sdk.opps.core.BasePay.getConfig(null).getProductId()), "payment initialization skips restoring overwritten config");
        System.out.println("REPRODUCED: payment -> withdrawal -> payment initialization retains WITHDRAWAL product in real SDK default config.");
    }
    private void require(boolean value, String name) {
        if (!value) throw new AssertionError("Observed behavior changed or probe failed: " + name);
    }
}