package com.talentcard.server.business.payment.huifu; import com.alibaba.fastjson.JSON; import com.huifu.bspay.sdk.opps.client.BasePayClient; import com.huifu.bspay.sdk.opps.core.BasePay; import com.huifu.bspay.sdk.opps.core.config.MerConfig; import com.talentcard.server.common.BusinessException; import com.talentcard.server.common.ErrorCode; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; @Service public class HuifuMerchantBusinessOpenGateway { private static final Object SDK_LOCK = new Object(); private static final Logger log = LoggerFactory.getLogger(HuifuMerchantBusinessOpenGateway.class); private final HuifuPaymentProperties properties; public HuifuMerchantBusinessOpenGateway(HuifuPaymentProperties properties) { this.properties = properties; } public Result open(String requestNo, String huifuId, String shortName, String mcc) { ensureConfigured(); Map wechat = new LinkedHashMap<>(); wechat.put("pay_scene", "5"); wechat.put("mcc", mcc); wechat.put("fee_rate", properties.getMerchantWechatFeeRate().trim()); wechat.put("is_check_real_name", "N"); Map media = new LinkedHashMap<>(); media.put("media_type", "S3"); media.put("media_name", properties.getMerchantOnlineMediaName().trim()); Map agreement = new LinkedHashMap<>(); agreement.put("agreement_type", "3"); agreement.put("agreement_url", properties.getMerchantAgreementUrl().trim()); Map request = new LinkedHashMap<>(); request.put("req_seq_id", requestNo); request.put("req_date", LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE)); request.put("huifu_id", huifuId); request.put("upper_huifu_id", properties.getOnboardingUpperHuifuId().trim()); request.put("short_name", shortName); request.put("wx_conf_list", JSON.toJSONString(List.of(wechat))); request.put("online_busi_type", properties.getMerchantOnlineBusinessType().trim()); request.put("online_media_info_list", JSON.toJSONString(List.of(media))); request.put("agreement_info", JSON.toJSONString(agreement)); // 进件时就把延迟入账能力配好:先付钱后核销的订单必须把钱留在通道「待确认」, // 核销确认后才分配给商户。forced_delay_flag 保持关闭——支付即核销的到店消费订单不该被延迟。 request.put("delay_flag", "Y"); request.put("forced_delay_flag", "N"); putIfPresent(request, "async_return_url", properties.getMerchantBusinessOpenNotifyUrl()); putIfPresent(request, "busi_async_return_url", properties.getMerchantBusinessOpenNotifyUrl()); try { synchronized (SDK_LOCK) { initializeSdk(); Map response = BasePayClient.request(request, "v2.merchant.busi.open", false); log.info("Huifu merchant business open response: requestNo={}, huifuId={}, response={}", requestNo, huifuId, JSON.toJSONString(response)); return Result.from(response); } } catch (BusinessException exception) { throw exception; } catch (Exception exception) { log.error("Huifu merchant business open request error: requestNo={}, huifuId={}", requestNo, huifuId, exception); throw new HuifuPaymentGatewayException("Huifu merchant business open request failed", outcomeUnknown(exception), exception); } } private void ensureConfigured() { if (!properties.isEnabled()) throw new BusinessException(ErrorCode.CONFLICT, "Huifu payment is disabled"); if (blank(properties.getSystemId()) || blank(properties.getProductId()) || blank(properties.getOnboardingUpperHuifuId()) || blank(properties.getMerchantWechatFeeRate()) || blank(properties.getMerchantOnlineBusinessType()) || blank(properties.getMerchantOnlineMediaName()) || blank(properties.getMerchantAgreementUrl()) || blank(properties.getPrivateKey()) || blank(properties.getPlatformPublicKey())) throw new BusinessException(ErrorCode.CONFLICT, "Huifu merchant business opening configuration is incomplete"); } private void initializeSdk() throws Exception { BasePay.debug = false; BasePay.prodMode = "prod".equalsIgnoreCase(properties.getEnvironment()) ? BasePay.MODE_PROD : BasePay.MODE_TEST; MerConfig config = new MerConfig(); config.setSysId(properties.getSystemId().trim()); config.setProductId(properties.getProductId().trim()); config.setRsaPrivateKey(properties.getPrivateKey().trim()); config.setRsaPublicKey(properties.getPlatformPublicKey().trim()); BasePay.initWithMerConfig(config); } private static void putIfPresent(Map request, String key, String value) { if (!blank(value)) request.put(key, value.trim()); } private static boolean blank(String value) { return value == null || value.isBlank(); } private boolean outcomeUnknown(Exception exception) { String message = exception.getMessage() == null ? "" : exception.getMessage().toLowerCase(); return message.contains("timeout") || message.contains("timed out") || message.contains("no response"); } @SuppressWarnings("unchecked") public record Result(boolean accepted, String applyNo, String code, String message) { static Result from(Map response) { Object raw = response.get("data"); Map data = raw instanceof Map value ? (Map) value : raw instanceof String json && !json.isBlank() ? JSON.parseObject(json, Map.class) : response; String code = value(data, "sub_resp_code", "resp_code", "code"); return new Result(success(code), value(data, "apply_no", "applyNo"), code, value(data, "sub_resp_desc", "resp_desc", "message")); } private static boolean success(String code) { return "00000000".equals(code) || "00000100".equals(code) || "10000".equals(code) || "0".equals(code) || "SUCCESS".equalsIgnoreCase(code); } private static String value(Map data, String... keys) { for (String key : keys) if (data.get(key) != null) return String.valueOf(data.get(key)).trim(); return ""; } } }