yeg
2026-09-16 242917765d2711b723e98764396054acef2a7297
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
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
package com.talentcard.server.business.merchant;
 
import com.alibaba.fastjson.JSON;
import com.talentcard.server.business.DatabaseRows;
import com.talentcard.server.business.payment.huifu.HuifuOnboardingProgressGateway;
import com.talentcard.server.common.BusinessException;
import com.talentcard.server.common.ErrorCode;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
 
/** Six independent evidence-backed stages; provider calls never hold application row locks. */
@Service
public class MerchantOnboardingProgressService {
    private static final Map<String,String> LABELS = new LinkedHashMap<>();
    static {
        LABELS.put("basic", "基础审核"); LABELS.put("wechat", "微信支付");
        LABELS.put("appId", "人才端绑定"); LABELS.put("realname", "实名与授权");
        LABELS.put("delay", "延迟入账"); LABELS.put("settlement", "自动结算");
    }
    private final JdbcTemplate jdbc;
    private final HuifuOnboardingProgressGateway gateway;
    public MerchantOnboardingProgressService(JdbcTemplate jdbc, HuifuOnboardingProgressGateway gateway) { this.jdbc=jdbc; this.gateway=gateway; }
 
    /**
     * 分阶段进度适用于「已关联结算银行卡、且未被整体拒绝」的申请。
     *
     * <p>原先这里按 huifu_id 白名单只放行一张指定存量商户,新商户走不进这套视图。现在按申请自身的
     * 状态判断:银行卡关联是分阶段视图核实原卡的前提({@code application()} 也用它做条件);
     * 而被拒绝的申请仍留在旧的单状态模型,商户端才有重新提交的入口。
     *
     * <p>这一条同时决定「进件失败后不重建商户与银行卡」的适用范围:支持的申请走局部刷新与重试,
     * 不支持的才允许关闭重来。旧策略里「通道配置失败就释放银行卡、让商户整单重提」已被规格
     * 明确取代——基本进件成功后要保留原商户号和银行卡。
     */
    public boolean supports(Map<String,Object> application) {
        return application.get("initialBankCardId") != null && !"rejected".equals(text(application.get("status")));
    }
 
    public Map<String,Object> view(Long id) { return view(application(id)); }
 
    @org.springframework.transaction.annotation.Transactional(propagation = org.springframework.transaction.annotation.Propagation.NOT_SUPPORTED)
    public Map<String,Object> refresh(Long applicationId, boolean retry) {
        Map<String,Object> application=application(applicationId);
        // 被拒绝的申请不归分阶段模型管:刷下去会把它从 rejected 翻回 pending,商户端的
        // 「重新提交」入口就没了(supports() 对 rejected 返回 false,正是为了让出那个入口)。
        if (!supports(application)) return view(application);
        jdbc.update("insert ignore into merchant_onboarding_progress(application_id,stages_json) values (?, '{}')", applicationId);
        String owner=UUID.randomUUID().toString();
        if(jdbc.update("update merchant_onboarding_progress set sync_owner=?,sync_until=date_add(now(),interval 5 minute) where application_id=? and (sync_owner is null or sync_until<now())", owner, applicationId)==0) return view(application);
        try {
            Map<String,Object> response=gateway.queryApplication(text(application.get("providerHuifuId")), text(application.get("providerApplyNo")));
            if (!"00000000".equals(text(response.get("resp_code")))) {
                String error=queryError(response);
                jdbc.update("update merchant_onboarding_progress set sync_error=? where application_id=? and sync_owner=?",error,applicationId,owner);
            } else {
                Map<String,Object> stages = savedStages(applicationId);
                String audit=text(response.get("apply_status"));
                if ("Y".equals(audit) && "S".equals(text(response.get("reg_status")))) stages.put("basic", "confirmed");
                else if (!"confirmed".equals(stages.get("basic"))) stages.put("basic",List.of("N","F").contains(audit) ? "failed" : "pending");
                jdbc.update("update merchant_onboarding_progress set stages_json=?,last_synced_at=now(),sync_error='' where application_id=? and sync_owner=?",JSON.toJSONString(stages),applicationId,owner);
                Map<String,Object> capabilities=gateway.queryCapabilities(text(application.get("providerHuifuId")));
                if (capabilities != null && "00000000".equals(text(capabilities.get("resp_code")))) {
                    if (capabilities.containsKey("mer_stat")) stages.put("merchantNormal", "NORMAL".equals(text(capabilities.get("mer_stat"))));
                    if ("S".equals(text(response.get("bind_card_status"))) && capabilities.containsKey("qry_cash_card_info_list") && capabilities.containsKey("qry_settle_config_list")) {
                        stages.put("bank", confirmOriginalCard(application,capabilities,owner));
                    }
                    if (capabilities.containsKey("qry_wx_conf_list")) {
                        List<Map<String,Object>> wechat = configurations(capabilities.get("qry_wx_conf_list"));
                        boolean valid = List.of("4", "5", "12").stream().allMatch(scene -> wechat.stream().anyMatch(item ->
                                scene.equals(text(item.get("pay_scene"))) && decimal(item.get("fee_rate"), "0.70")
                                && zero(item.get("fee_min_amt")) && "1".equals(text(item.get("fee_charge_type")))
                                && "2".equals(text(item.get("out_fee_flag"))) && item.containsKey("wx_mer_infos")
                                && configurations(item.get("wx_mer_infos")).stream().anyMatch(merchant ->
                                        !text(merchant.get("bank_mer_code")).isBlank() && "1".equals(text(merchant.get("is_default"))))));
                        stages.put("wechat", valid ? "confirmed" : "missing");
                    }
                    if (capabilities.containsKey("qry_settle_config_list")) {
                        List<Map<String,Object>> settlements = configurations(capabilities.get("qry_settle_config_list"));
                        boolean valid = settlements.size() == 1 && settlements.stream().allMatch(item ->
                                "1".equals(text(item.get("settle_status"))) && "D1".equals(text(item.get("settle_cycle")))
                                && "P0".equals(text(item.get("settle_pattern")))
                                && zero(item.get("min_amt")) && zero(item.get("remained_amt")));
                        stages.put("settlement", valid ? "confirmed" : "missing");
                    }
                    if (capabilities.containsKey("delay_flag") && capabilities.containsKey("forced_delay_flag")) {
                        stages.put("delay", "Y".equals(capabilities.get("delay_flag")) && "N".equals(capabilities.get("forced_delay_flag")) ? "confirmed" : "missing");
                    } else {
                        jdbc.update("update merchant_onboarding_progress set sync_error=? where application_id=? and sync_owner=?","汇付返回缺少延迟配置字段,已保留最后有效状态",applicationId,owner);
                    }
                    jdbc.update("update merchant_onboarding_progress set stages_json=? where application_id=? and sync_owner=?",JSON.toJSONString(stages),applicationId,owner);
                } else {
                    jdbc.update("update merchant_onboarding_progress set sync_error=? where application_id=? and sync_owner=?",queryError(capabilities),applicationId,owner);
                }
                jdbc.update("update merchant_onboarding_progress set last_synced_at=now() where application_id=? and sync_owner=?",applicationId,owner);
                Map<String,Object> appConfig = gateway.queryWechatConfig(text(application.get("providerHuifuId")));
                if (appConfig != null && "00000000".equals(text(appConfig.get("resp_code")))) {
                    boolean bound = false, controlled = false;
                    for (Map<String,Object> config : configurations(appConfig.getOrDefault("atu_config_bo_list", List.of()))) {
                        if (!"01".equals(text(config.get("fee_type")))) continue;
                        for (Map<String,Object> entry : configurations(config.getOrDefault("wx_config_list", List.of()))) {
                            if ("1".equals(text(entry.get("config_type"))) && "wxcfee477acb54d179".equals(text(entry.get("config_value")))) {
                                bound = true;
                                controlled |= "Y".equals(text(entry.get("control_flag")));
                            }
                        }
                    }
                    stages.put("appId", controlled ? "action_required" : bound ? "confirmed" : "missing");
                } else if (appConfig != null && !appConfig.isEmpty()) {
                    jdbc.update("update merchant_onboarding_progress set sync_error=? where application_id=? and sync_owner=?",queryError(appConfig),applicationId,owner);
                }
                Map<String,Object> realname = gateway.queryRealname(text(application.get("providerHuifuId")));
                if (realname != null && "00000000".equals(text(realname.get("resp_code")))) {
                    String status = text(realname.get("applyment_stat"));
                    if (!status.isBlank()) stages.put("realname", realnameStage(status, text(realname.get("authorize_stat"))));
                    stages.put("realnameQr", qrCode(text(realname.get("qrcode_data"))));
                } else if (realname != null && !realname.isEmpty()) {
                    if ("00000001".equals(text(realname.get("resp_code"))) && "AT入驻信息池信息不存在".equals(text(realname.get("resp_desc")))) {
                        if (!"confirmed".equals(stages.get("realname"))) stages.put("realname", "missing");
                    } else jdbc.update("update merchant_onboarding_progress set sync_error=? where application_id=? and sync_owner=?",queryError(realname),applicationId,owner);
                }
                if ("Y".equals(audit) && "S".equals(text(response.get("reg_status"))) && capabilities != null
                        && "00000000".equals(text(capabilities.get("resp_code"))) && "NORMAL".equals(text(capabilities.get("mer_stat")))
                        && capabilities.containsKey("delay_flag") && capabilities.containsKey("forced_delay_flag")) {
                    advanceDelay(applicationId,text(application.get("providerHuifuId")),owner,stages,retry);
                }
                jdbc.update("update merchant_onboarding_progress set stages_json=? where application_id=? and sync_owner=?",JSON.toJSONString(stages),applicationId,owner);
                // 基本资料被通道驳回时要落到 rejected:一是让商户端出现「重新提交」入口(supports() 对
                // rejected 返回 false,交回旧的单状态模型),二是别把一次明确拒绝一直显示成「审核中」。
                // 后续配置失败不算拒绝——那种情况保留原商户号和银行卡,局部重试即可。
                boolean basicRejected = "failed".equals(text(stages.get("basic")));
                String applicationStatus = basicRejected ? "rejected" : complete(stages) ? "approved" : "pending";
                jdbc.update("""
                        update merchant_onboarding_application a join merchant_onboarding_progress p on p.application_id=a.id
                        set a.status=?,a.reviewed_at=case when ? then now() else null end
                        where a.id=? and p.sync_owner=?
                        """,applicationStatus,!"pending".equals(applicationStatus),applicationId,owner);
                if (basicRejected) jdbc.update("update merchant_onboarding_application set reject_reason=? where id=?","基本资料审核未通过,请核对资料后重新提交",applicationId);
            }
        } catch (RuntimeException exception) {
            jdbc.update("update merchant_onboarding_progress set sync_error=? where application_id=? and sync_owner=?","查询结果暂未确认,已保留最后有效进度,请稍后刷新",applicationId,owner);
        } finally {
            jdbc.update("update merchant_onboarding_progress set sync_owner=null,sync_until=null where application_id=? and sync_owner=?",applicationId,owner);
        }
        return view(application(applicationId));
    }
 
    private boolean confirmOriginalCard(Map<String,Object> application, Map<String,Object> capabilities, String owner) {
        List<Map<String,Object>> original=rows("select * from merchant_bank_card where id=? and merchant_id=?",application.get("initialBankCardId"),application.get("merchantId"));
        if (original.size()!=1) return false;
        Map<String,Object> card=original.get(0);
        for (Map<String,Object> remote:configurations(capabilities.get("qry_cash_card_info_list"))) {
            String number=text(remote.get("card_no")), token=text(remote.get("token_no"));
            if (!number.matches("[0-9]{8,32}") || token.isBlank() || !sha256(number).equals(text(card.get("cardHash")))) continue;
            String previous=text(card.get("providerTokenNo"));
            if (!previous.isBlank() && !previous.equals(token)) continue;
            boolean linked=configurations(capabilities.get("qry_settle_config_list")).stream().anyMatch(settle -> token.equals(text(settle.get("token_no"))));
            if (!linked) continue;
            return jdbc.update("""
                    update merchant_bank_card b join merchant_onboarding_progress p on p.application_id=?
                    set b.status='approved',b.provider_token_no=?,b.reviewed_at=coalesce(b.reviewed_at,now())
                    where b.id=? and p.sync_owner=?
                    """,application.get("id"),token,card.get("id"),owner)>0;
        }
        return false;
    }
 
    private static String sha256(String value) {
        try { return java.util.HexFormat.of().formatHex(java.security.MessageDigest.getInstance("SHA-256").digest(value.getBytes(java.nio.charset.StandardCharsets.UTF_8))); }
        catch (java.security.NoSuchAlgorithmException exception) { throw new IllegalStateException(exception); }
    }
 
    private static boolean complete(Map<String,Object> stages) {
        return Boolean.TRUE.equals(stages.get("bank")) && Boolean.TRUE.equals(stages.get("merchantNormal"))
                && LABELS.keySet().stream().allMatch(key -> "confirmed".equals(stages.get(key)));
    }
 
    private void advanceDelay(Long id, String huifuId, String owner, Map<String,Object> stages, boolean retry) {
        List<Map<String,Object>> attempts=rows("select * from merchant_onboarding_attempt where application_id=? and step='delay' order by id desc limit 1",id);
        Map<String,Object> attempt=attempts.isEmpty() ? null : attempts.get(0);
        if ("confirmed".equals(stages.get("delay"))) {
            if (attempt != null) jdbc.update("update merchant_onboarding_attempt set status='confirmed' where id=?",attempt.get("id"));
            return;
        }
        if (attempt != null && !"failed".equals(text(attempt.get("status")))) {
            String applyNo=text(attempt.get("applyNo"));
            if (!applyNo.isBlank() && !List.of("succeeded","confirmed").contains(text(attempt.get("status")))) {
                Map<String,Object> result=gateway.queryApplication(huifuId,applyNo);
                if (result != null && "00000000".equals(text(result.get("resp_code"))) && List.of("N","F").contains(text(result.get("apply_status")))) {
                    jdbc.update("update merchant_onboarding_attempt set status='failed',response_code=? where id=? and status<>'confirmed'",text(result.get("resp_code")),attempt.get("id"));
                    attempt.put("status","failed");
                }
            }
            if (!"failed".equals(text(attempt.get("status")))) { stages.put("delay","pending"); return; }
        }
        if (attempt != null && !retry) { stages.put("delay","failed"); return; }
        String requestNo=UUID.randomUUID().toString().replace("-", "");
        String date=java.time.LocalDate.now(java.time.ZoneId.of("Asia/Shanghai")).format(java.time.format.DateTimeFormatter.BASIC_ISO_DATE);
        int inserted=jdbc.update("""
                insert into merchant_onboarding_attempt(application_id,step,request_no,request_date,status)
                select ?, 'delay', ?, ?, 'unknown' from merchant_onboarding_progress
                where application_id=? and sync_owner=? and sync_until>now()
                """,id,requestNo,date,id,owner);
        if (inserted==0) return;
        stages.put("delay","pending");
        try {
            Map<String,Object> result=gateway.configureDelay(huifuId,requestNo,date);
            String code=result==null ? "" : text(result.get("resp_code"));
            String status=code.isBlank() ? "unknown" : List.of("00000000","00000100","90000000").contains(code) ? "pending" : "failed";
            jdbc.update("update merchant_onboarding_attempt set status=?,apply_no=nullif(?,''),response_code=? where request_no=?",status,result==null ? "" : text(result.get("apply_no")),code,requestNo);
            if ("failed".equals(status)) stages.put("delay","failed");
        } catch (RuntimeException ignored) {
            // The durable unknown attempt blocks resubmission after a crash, timeout or invalid response.
        }
    }
 
    public Map<String,Object> view(Map<String,Object> application) {
        Map<String,Object> result=new LinkedHashMap<>();
        result.put("id",text(application.get("id"))); result.put("applicationNo",text(application.get("applicationNo")));
        result.put("status","pending"); result.put("subjectType",text(application.get("subjectType")));
        result.put("submittedAt",text(application.get("createdAt"))); result.put("retryable",false);
        result.put("refreshable",true); result.put("lastSyncedAt",""); result.put("syncError","");
        List<Map<String,Object>> saved=rows("select * from merchant_onboarding_progress where application_id=?",application.get("id"));
        Map<String,Object> stages=new LinkedHashMap<>();
        if (!saved.isEmpty()) {
            stages=JSON.parseObject(text(saved.get(0).get("stagesJson")), Map.class);
            result.put("lastSyncedAt",text(saved.get(0).get("lastSyncedAt")));
            result.put("syncError",text(saved.get(0).get("syncError")));
        }
        List<Map<String,Object>> progress=new ArrayList<>();
        for (var entry:LABELS.entrySet()) {
            String status = text(stages.getOrDefault(entry.getKey(),"unknown"));
            progress.add(Map.of("key",entry.getKey(),"label",entry.getValue(),"status",status,"reason",stageReason(entry.getKey(),status),
                    "qrCode", "realname".equals(entry.getKey()) ? text(stages.get("realnameQr")) : ""));
        }
        // 被通道明确驳回基本资料的申请仍显示 rejected(并带通道给的驳回原因),不能被阶段计算
        // 盖成 pending——那样商户端看不到「重新提交」。
        boolean rejected = "rejected".equals(text(application.get("status")));
        result.put("stages",progress); result.put("reason","请刷新进度,核实原申请与各项能力");
        result.put("retryable", "failed".equals(stages.get("delay")));
        result.put("status",rejected ? "rejected" : complete(stages) ? "approved" : "pending");
        result.put("reason",rejected ? text(application.get("rejectReason"))
                : complete(stages) ? "收款能力已核实,沿用原商户号和结算银行卡" : "请刷新进度核实各项能力,并按待办理项补齐缺失能力");
        return result;
    }
 
    /** Called only after the existing HTTP callback boundary has verified Huifu's signature. */
    public Long acceptDelayCallback(Map<String,Object> data) {
        // 用回调自带的 huifu_id 去匹配它自己的申请单,不再按固定商户号过滤。
        String huifuId=text(data.get("huifu_id"));
        if (huifuId.isBlank()) return null;
        List<Map<String,Object>> matches=rows("""
                select t.id,t.application_id from merchant_onboarding_attempt t
                join merchant_onboarding_application a on a.id=t.application_id
                where t.step='delay' and t.request_no=? and t.request_date=? and a.provider_huifu_id=?
                """,text(data.get("req_seq_id")),text(data.get("req_date")),huifuId);
        if (matches.isEmpty()) return null;
        Map<String,Object> attempt=matches.get(0);
        String audit=text(data.get("audit_status"));
        String state="Y".equals(audit) ? "succeeded" : List.of("N","F","C").contains(audit) ? "failed" : "pending";
        jdbc.update("""
                update merchant_onboarding_attempt set status=?,apply_no=coalesce(apply_no,nullif(?,'')),response_code=?
                where id=? and status not in ('succeeded','confirmed')
                """,state,text(data.get("apply_no")),text(data.getOrDefault("sub_resp_code",data.get("resp_code"))),attempt.get("id"));
        return ((Number)attempt.get("applicationId")).longValue();
    }
 
    private Map<String,Object> application(Long id) {
        List<Map<String,Object>> records=rows("select * from merchant_onboarding_application where id=? and initial_bank_card_id is not null",id);
        if(records.isEmpty()) throw new BusinessException(ErrorCode.NOT_FOUND,"Onboarding application not found");
        return records.get(0);
    }
    @SuppressWarnings("unchecked")
    private Map<String,Object> savedStages(Long id) {
        return JSON.parseObject(jdbc.queryForObject("select stages_json from merchant_onboarding_progress where application_id=?",String.class,id),Map.class);
    }
    private List<Map<String,Object>> rows(String sql,Object... args) { return DatabaseRows.normalize(jdbc.queryForList(sql,args)); }
    private static String text(Object value) { return value==null ? "" : String.valueOf(value); }
    private static String stageReason(String key, String status) {
        if ("confirmed".equals(status)) return "已取得通道生效证据";
        if ("realname".equals(key) && "action_required".equals(status)) return "请法人或授权联系人完成微信实名及授权,办理后刷新;没有二维码时请联系平台运营";
        if ("realname".equals(key) && "missing".equals(status)) return "尚未发起微信实名认证,请先发起;实名走法人,需要法人本人的手机号";
        if ("appId".equals(key) && "action_required".equals(status)) return "人才端绑定被微信管控,请联系平台运营解除后刷新";
        if ("delay".equals(key) && "missing".equals(status)) return "需开启延迟入账并关闭强制延迟";
        if ("settlement".equals(key) && "missing".equals(status)) return "需开启 D1 批次结算,起结金额与留存金额均为 0";
        if ("pending".equals(status)) return "通道正在审核,请稍后刷新";
        if ("failed".equals(status)) return "该步骤未通过,请联系平台运营核对资料后局部重试";
        return "尚未取得通道生效证据";
    }
    /**
     * 实名阶段的状态映射。微信实名分「实名认证申请」与「商户授权」两个阶段,各有各的状态,
     * 必须分别看:
     *
     * <ul>
     *   <li>{@code NOAPPLY}/{@code CANCELED} 是没申请或已作废,要提示去发起,不能和「需要本人办理」
     *       混为一谈;</li>
     *   <li>{@code REJECTED} 是驳回,要按原因补资料并更换业务申请编号重来;</li>
     *   <li>授权是否完成**只看 {@code authorize_stat}**,不看申请单状态。{@code PASSED} 单独不等于
     *       授权完成;反过来 {@code FREEZED}(「该主体已完成过入驻」)也可能已经授权成功——真实样本
     *       里就是 {@code APPLYMENT_STATE_FREEZED} + {@code authorize_stat=1}。按申请单状态判会把
     *       已经授权好的商户显示成待办理。</li>
     * </ul>
     */
    private static String realnameStage(String applymentStat, String authorizeStat) {
        if ("APPLYMENT_STATE_REJECTED".equals(applymentStat)) return "failed";
        if ("APPLYMENT_STATE_NOAPPLY".equals(applymentStat) || "APPLYMENT_STATE_CANCELED".equals(applymentStat)) return "missing";
        if ("1".equals(authorizeStat)) return "confirmed";
        if ("APPLYMENT_STATE_EDITTING".equals(applymentStat) || "APPLYMENT_STATE_WAITTING_FOR_AUDIT".equals(applymentStat)) return "pending";
        return "action_required";
    }
 
    private static String qrCode(String raw) {
        String value = raw.replaceFirst("^data:image/png;base64,", "");
        if (value.length() > 200000 || !value.startsWith("iVBORw0KGgo") || !value.matches("[A-Za-z0-9+/=\\r\\n]+")) return "";
        return "data:image/png;base64," + value;
    }
    @SuppressWarnings("unchecked")
    private static List<Map<String,Object>> configurations(Object raw) {
        Object parsed = raw instanceof String value ? JSON.parse(value) : raw;
        if (!(parsed instanceof List<?> list)) throw new IllegalArgumentException("Invalid provider configuration list");
        return list.stream().map(item -> (Map<String,Object>)item).toList();
    }
    private static boolean zero(Object value) {
        return decimal(value, "0");
    }
    private static boolean decimal(Object value, String expected) {
        return value != null && new java.math.BigDecimal(text(value)).compareTo(new java.math.BigDecimal(expected)) == 0;
    }
    private static String queryError(Map<String,Object> response) {
        return response != null && "20002".equals(text(response.get("resp_code")))
                ? "汇付查询权限不足(20002),请联系平台运营核实接口权限"
                : "汇付查询未成功,请稍后刷新或联系平台运营";
    }
}