Skip to content

微信支付

1 名词解释

微信支付有两种模式:

  • 普通商户模式

  • 服务商+特约商户模式

2 普通商户模式

3 服务商+特约商户模式

注: 服务商是普通服务商,服务商还有收付通类型

3.1 小程序支付(JavaSDK

支付资料准备:

  • 小程序的AppID和AppSecret

  • 商户API证书私钥:官方文档

  • 商户API证书的证书序列号

  • APIv3秘钥:文档

  • 微信支付公钥(商户接收APIv3的请求应答、回调时验签使用)

  • 微信支付公钥ID(申请支付公钥时获取)

支付流程:

image-20250808150742751

3.1.1 POM依赖
xml
<!-- 官方的微信支付 -->
<dependency>
    <groupId>com.github.wechatpay-apiv3</groupId>
    <artifactId>wechatpay-java</artifactId>
    <version>0.2.17</version>
</dependency>
3.1.2 配置属性类
java
@Data
@Configuration
@ConfigurationProperties(prefix = "wx")
public class WechatPayProperties {
    /** 应用ID */
    private String appId = "wx89dd13b9a9*****";
    /** 小程序密钥 */
    private String secret = "0d883cacfc29acea17b901e2ece*****";
    /** 服务商商户号 */
    private String mchId = "1719******";
    /** 商户API证书序列号 */
    private String mchSerialNo = "41F22582CF54A62BACCE15F2C8*****19BE3816";
    /** 商户APIv3密钥 */
    private String apiKey = "YGmhQdBPnmKUPCcF4afJTdpeNC******";
    /** 商户API私钥文件路径 */
    private String privateKeyPath = "cert/apiclient_key.pem";
    /** 微信支付公钥ID */
    private String publicKeyId = "PUB_KEY_ID_0117193469652025061200442301******";
    /** 微信支付公钥文件路径 */
    private String publicKeyPath = "cert/pub_key.pem";
    /** 回调地址 */
    private String notifyUrl = "http://callback.com";
}
3.1.3 回调工具类
java
public enum CallbackUrl {

    /**
     * 支付成功回调地址
     */
    PAYMENT_CALLBACK("/wechat/pay/payNotify"),
    /**
     * 退款回调地址
     */
    REFUND_CALLBACK("/wechat/pay/refundNotify");

    private static String baseUrl;
    private final String path;

    CallbackUrl(String path) {
        this.path = path;
    }

    public String getUrl() {
        if (baseUrl == null) {
            throw new JeecgBootException("CallbackUrl.baseUrl 未初始化");
        }
        return baseUrl + path;
    }
    /**
     * 设置回调基地址
     */
    public static void initBaseUrl(String baseUrl) {
        CallbackUrl.baseUrl = baseUrl.endsWith("/") ? 
            baseUrl.substring(0, baseUrl.length() - 1) : baseUrl;
    }

}
3.1.4 支付配置类
java
/**
 * 微信支付配置类
 * 配置微信支付相关服务和解析器
 */
@Getter
@Configuration
@RequiredArgsConstructor
public class WechatPayConfig {
    private final WechatPayProperties wechatPayProperties;
    private Config config;


    /**
     * 初始化微信支付配置
     */
    @PostConstruct
    public void init() {
        // 初始化回调URL基地址
        CallbackUrl.initBaseUrl(wechatPayProperties.getNotifyUrl());
        //初始化微信支付核心配置
        initConfig();
    }

    /**
     * RSAPublicKeyConfig 用于 API 请求加密和验签
     */
    public void initConfig() {
        // 一个商户号只能初始化一个配置,否则会因为重复的下载任务报错
        // RSAPublicKeyConfig 就是公钥验签配置
        try {
            String privateKeyContent = PemFileUtil.readPemFromClasspath(
                    wechatPayProperties.getPrivateKeyPath()
            );
            String publicKeyContent = PemFileUtil.readPemFromClasspath(
                    wechatPayProperties.getPublicKeyPath()
            );

            config = new RSAPublicKeyConfig.Builder()
                    .merchantId(wechatPayProperties.getMchId())
                    .privateKey(privateKeyContent)
                    .publicKey(publicKeyContent)
                    .publicKeyId(wechatPayProperties.getPublicKeyId())
                    .merchantSerialNumber(wechatPayProperties.getMchSerialNo())
                    .apiV3Key(wechatPayProperties.getApiKey())
                    .build();
        } catch (Exception e) {
            throw new JeecgBootException("初始化微信支付配置失败", e);
        }
    }
    /**
     * 支付回调 解密
     */
    @Primary
    @Bean
    public NotificationParser notificationParser() {

        RSAPublicKeyNotificationConfig notificationConfig = null;
        try {
            String publicKeyContent = PemFileUtil.readPemFromClasspath(
                    wechatPayProperties.getPublicKeyPath()
            );
            notificationConfig = new RSAPublicKeyNotificationConfig.Builder()
                    .publicKey(publicKeyContent)
                    .publicKeyId(wechatPayProperties.getPublicKeyId())
                    .apiV3Key(wechatPayProperties.getApiKey())
                    .build();
        } catch (Exception e) {
            throw new JeecgBootException("初始化微信支付回调解密配置失败", e);
        }
        return new NotificationParser(notificationConfig);
    }
    /**
     * JSAPI支付/小程序支付
     * 提供商户在微信客户端内部浏览器网页中使用微信支付收款的能力。(包含小程序)
     * 其他接口的Service也可以在这里注册到容器中(H5Service、NativePayService)
     */
    @Bean
    public JsapiService jsapiService() {
        return new JsapiService.Builder().config(config).build();
    }

    /**  申请退款 */
    @Bean
    public RefundService refundService() {
        return new RefundService.Builder().config(config).build();
    }
}
3.1.5 工具类
java
public class WeChatUtil {

    /**
    * 获取支付签名,用于小程序拉起微信支付
    * 签名因为要用到私钥,小程序不方便获取私钥,直接在后端生成签名
    */
    public static String getSign(String signatureStr, String privateKeyPath, String merchantSerialNumber) {
        PrivateKey privateKey = PemUtil.loadPrivateKeyFromString(privateKeyPath);
        RSASigner rsaSigner = new RSASigner(merchantSerialNumber, privateKey);
        SignatureResult signatureResult = rsaSigner.sign(signatureStr);
        return signatureResult.getSign();
    }

    /**
     * 构造 RequestParam 回调的时候获取请求头
     */
    public static RequestParam handleNodifyRequestParam(HttpServletRequest request) throws IOException {
        // 请求头Wechatpay-Signature
        String signature = request.getHeader("Wechatpay-Signature");
        // 请求头Wechatpay-nonce
        String nonce = request.getHeader("Wechatpay-Nonce");
        // 请求头Wechatpay-Timestamp
        String timestamp = request.getHeader("Wechatpay-Timestamp");
        // 微信支付证书序列号
        String serial = request.getHeader("Wechatpay-Serial");
        // 构造 RequestParam
        return new RequestParam.Builder()
                .serialNumber(serial)
                .nonce(nonce)
                .signature(signature)
                .timestamp(timestamp)
                .body(getRequestBody(request))
                .build();
    }

    public static String getRequestBody(HttpServletRequest request) throws IOException {
        ServletInputStream stream;
        BufferedReader reader = null;
        StringBuilder sb = new StringBuilder();
        try {
            stream = request.getInputStream();
            // 获取响应
            reader = new BufferedReader(new InputStreamReader(stream));
            String line;
            while ((line = reader.readLine()) != null) {
                sb.append(line);
            }
        } catch (IOException e) {
            throw new IOException("读取返回支付接口数据流出现异常!");
        } finally {
            if (reader != null) {
                reader.close();
            }
        }
        return sb.toString();
    }
}

/**
 * PEM 文件读取工具类,支持从 classpath 加载 .pem 文件并返回其字符串内容
 */
public class PemFileUtil {

    /**
     * 从 classpath 加载 PEM 文件并返回其字符串内容
     * @param classpathPath 例如 "cert/apiclient_key.pem"
     * @return PEM 文件的字符串内容
     * @throws RuntimeException 加载失败会抛出异常
     */
    public static String readPemFromClasspath(String classpathPath) {
        InputStream is = null;
        BufferedReader reader = null;
        try {
            is = Thread.currentThread().getContextClassLoader().getResourceAsStream(classpathPath);
            if (is == null) {
                throw new IllegalArgumentException("无法从 classpath 加载文件: " + classpathPath);
            }

            reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8));
            StringBuilder sb = new StringBuilder();
            String line;

            while ((line = reader.readLine()) != null) {
                sb.append(line).append("\n");
            }

            return sb.toString();
        } catch (IOException e) {
            throw new RuntimeException("读取 PEM 文件失败: " + classpathPath, e);
        } finally {
            try {
                if (reader != null) reader.close();
                if (is != null) is.close();
            } catch (IOException ignore) {
            }
        }
    }
}
3.1.6 下单方法

下单API参数说明

java
@Override
public WechatPayDTO jsPay(WechatPayReqParam param) {
    if (param == null) {
        throw new JeecgBootException("支付参数不能为空");
    }
    // 调用下单方法,得到应答
    try {
        // 业务系统根据参数创建订单
        PaymentInfo paymentInfo = buildPaymentInfo(param);
        // 构建预支付请求
        PrepayRequest prepayRequest = buildPrepayRequest(param, paymentInfo);
        prepayRequest.getPayer().setSpOpenid(param.getMiniOpenid());
        // 调用下单api
        PrepayResponse prepayResponse = jsapiService.prepay(prepayRequest);
        // 将 WechatPayDto 中的值返回小程序就可以唤起支付页面 也就是输入密码页面
        return buildWechatPayDTO(prepayResponse, paymentInfo);
    } catch (HttpException e) { 
        // 发送HTTP请求失败
        throw new JeecgBootException("发送HTTP请求失败");
    } catch (ServiceException e) {
        // 服务返回状态小于200或大于等于300,例如500
        throw new JeecgBootException("服务返回状态异常");
    } catch (MalformedMessageException e) {
        // 服务返回成功,返回体类型不合法,或者解析返回体失败
        throw new JeecgBootException("解析返回体失败");
    } catch (Exception e) {
        throw new JeecgBootException("微信支付预下单异常");
    }
}

/**
* 构建支付凭证DTO
*/
private WechatPayDTO buildWechatPayDTO(PrepayResponse prepayResponse, PaymentInfo paymentInfo) {
    WechatPayDTO dto = new WechatPayDTO();
    // 业务系统订单id
    dto.setOrderId(paymentInfo.getOrderId());
    // 小程序的appId
    dto.setAppid(wechatPayProperties.getAppId());
    dto.setTimeStamp(String.valueOf(System.currentTimeMillis() / 1000));
    dto.setNonceStr(NonceUtil.createNonce(32));
    dto.setPrepayId("prepay_id=" + prepayResponse.getPrepayId());
    dto.setSignType("RSA");
    // 生成支付签名
    String signContent = Stream.of(
        dto.getAppid(),
        dto.getTimeStamp(),
        dto.getNonceStr(),
        dto.getPrepayId()
    ).collect(Collectors.joining("\n", "", "\n"));

    String privateKey = PemFileUtil.readPemFromClasspath(wechatPayProperties.getPrivateKeyPath());
    dto.setPaySign(WeChatUtil.getSign(
        signContent,
        privateKey,
        wechatPayProperties.getMchSerialNo()
    ));
    return dto;
}
3.1.7 成功回调方法
java
@Override
    public Map<String,String> payNotify(HttpServletRequest request, HttpServletResponse response) {
        Transaction transaction;
        HashMap<String, String> resultMap = new HashMap<>(2);
        try {
            //解析微信回调参数
            transaction = notificationParser.parse(WeChatUtil.handleNodifyRequestParam(request), Transaction.class);
            if (transaction.getTradeState() != Transaction.TradeStateEnum.SUCCESS) {
                log.warn("交易未成功: 当前状态={}", transaction.getTradeState());
                resultMap.put("code", "FAIL");
                resultMap.put("message", "交易未成功");
                return resultMap;
            }
            //获取并验证订单信息
            String outTradeNo = transaction.getOutTradeNo();
            log.info("处理支付回调: 订单号={}, 交易信息={}", outTradeNo, JSON.toJSONString(transaction));
            //查询支付记录
            LambdaQueryWrapper<PaymentInfo> queryWrapper = new LambdaQueryWrapper<>();
            queryWrapper.eq(PaymentInfo::getOrderId, outTradeNo);
            PaymentInfo paymentInfo = paymentInfoMapper.selectOne(queryWrapper);
            if (paymentInfo == null) {
                log.error("订单不存在: orderId={}", outTradeNo);
                resultMap.put("code", "FAIL");
                resultMap.put("message", "订单不存在");
                return resultMap;
            }
            //幂等性检查(防止重复处理)
            if (paymentInfo.getPayStatus() != PaymentStatusEnum.UNPAID.getPayStatus()) {
                log.warn("订单已处理: orderId={}, 当前状态={}", outTradeNo, paymentInfo.getPayStatus());
                resultMap.put("code", "SUCCESS");
                resultMap.put("message", "订单已处理");
                return resultMap;
            }
            //更新支付记录
            updatePaymentInfo(paymentInfo, transaction);
            paymentInfoMapper.updateById(paymentInfo);
            //触发服务号推送信息给用户
            //根据小程序的openid查询服务号的openid
            UserWechat userWechat =
                    userWechatService.getUserWechat(UserWechat.builder().build()
                            .setMiniOpenid(transaction.getPayer().getSpOpenid()));
            if (userWechat != null && CharSequenceUtil.isNotBlank(userWechat.getMpOpenid())) {
                TemplateDataDTO templateDataDTO = new TemplateDataDTO();
                templateDataDTO.setOpenId(userWechat.getMpOpenid());
                templateDataDTO.setOrderId(paymentInfo.getOrderId());
                templateDataDTO.setAmount(paymentInfo.getPayTotal());
                templateDataDTO.setPayTime(paymentInfo.getPayTime());
                templateDataDTO.setPayType(Integer.valueOf(paymentInfo.getReason()));
                Merchant merchant = merchantService.getById(paymentInfo.getMerchantId());
                templateDataDTO.setBusinessName(merchant.getName());
                wechatMpService.sendTemplateMessage(templateDataDTO);
            }

            //返回给微信的成功相应
            resultMap.put("code", "SUCCESS");
            resultMap.put("message", "处理成功");
            return resultMap;
        } catch (ValidationException | IOException e) {
            // 签名验证失败,返回 401 UNAUTHORIZED 状态码
            String errorMsg = String.format("处理回调异常: %s", e.getMessage());
            log.error(errorMsg, e);
            resultMap.put("code", "FAIL");
            resultMap.put("message", "处理失败");
            return resultMap;
        }
    }
3.2 特约商户进件

注:对于30天未签约的商户自动取消申请

方式:

  • 微信支付合作伙伴后台进件
  • API进件
3.2.1 后台进件

进入开发者后台(合作伙伴管理后台) => 合作伙伴功能 => 商户基础服务 => 商户资料填写 => “前往填写”按钮

3.2.2 API进件

商户进件API文档:文档

主要是填写资料,然后进行发送API请求之前对必要数据进行加密

收集填写资料这里的步骤省略

3.2.3 微信请求工具类
java
/**
 * 用于发送微信支付请求
 * 微信支付的相关API请求需要携带公钥加密后的请求头
 */
@Component
@Slf4j
@RequiredArgsConstructor
public class WxRequestUtil {

    private final WechatPayConfig wechatPayConfig;
    private final WechatPayProperties wechatPayProperties;

    /**
     * 微信 POST请求
     * @param requestPath 请求路径
     * @param requestBody 请求体
     * @return 返回响应体
     */
    public HashMap wxPostRequest(String requestPath, Object requestBody){
        try {
            Config config = wechatPayConfig.getConfig();
            // 以此创建的HttpClient会自动在请求前加上验签的请求头
            HttpClient httpClient = new DefaultHttpClientBuilder().config(config).build();
            HttpRequest request = new HttpRequest.Builder()
                    .addHeader(Constant.ACCEPT, MediaType.APPLICATION_JSON.getValue())
                    .addHeader(Constant.CONTENT_TYPE, MediaType.APPLICATION_JSON.getValue())
                    .httpMethod(HttpMethod.POST)
                    .url(requestPath)
                    .body(createRequestBody(requestBody))
                    .build();
            return httpClient.execute(request, HashMap.class).getServiceResponse();
        } catch (ServiceException e) {
            throw new JeecgBootBizTipException(e.getErrorMessage());
        }
    }

    private RequestBody createRequestBody(Object requestBody) {
        return new JsonRequestBody.Builder().body(JSONUtil.toJsonStr(requestBody)).build();
    }

    /**
     * 微信 GET请求
     * @param requestPath 请求路径
     * @return 返回响应体
     */
    public HashMap wxGetRequest(String requestPath){
        try {
            Config config = wechatPayConfig.getConfig();
            HttpClient httpClient = new DefaultHttpClientBuilder().config(config).build();
            HttpRequest request = new HttpRequest.Builder()
                    .addHeader(Constant.ACCEPT, MediaType.APPLICATION_JSON.getValue())
                    .httpMethod(HttpMethod.GET)
                    .url(requestPath)
                    .build();
            return httpClient.execute(request, HashMap.class).getServiceResponse();
        } catch (ServiceException e) {
            throw new JeecgBootBizTipException(e.getErrorMessage());
        }
    }

    /**
     * 微信公钥加密
     * @param message 加密内容
     * @return 加密串
     */
    public String wxPubKeyEncrypt(String message) {
        // 为空直接返回
        if(CharSequenceUtil.isBlank(message)){
            return message;
        }
        try {
            String publicKeyStr = PemFileUtil.readPemFromClasspath(wechatPayProperties.getPublicKeyPath());
            String publicKeyPEM = publicKeyStr
                    .replace("-----BEGIN PUBLIC KEY-----", "")
                    .replace("-----END PUBLIC KEY-----", "")
                    .replaceAll("\\s", "");
            byte[] encoded = Base64.getDecoder().decode(publicKeyPEM);
            X509EncodedKeySpec keySpec = new X509EncodedKeySpec(encoded);
            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            PublicKey publicKey = keyFactory.generatePublic(keySpec);
            Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding");
            cipher.init(Cipher.ENCRYPT_MODE, publicKey);
            byte[] data = message.getBytes(StandardCharsets.UTF_8);
            byte[] cipherData = cipher.doFinal(data);
            return Base64.getEncoder().encodeToString(cipherData);
        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
            throw new JeecgBootBizTipException("当前Java环境不支持RSA v1.5/OAEP", e);
        } catch (InvalidKeyException e) {
            throw new JeecgBootBizTipException("无效的公钥", e);
        } catch (IllegalBlockSizeException | BadPaddingException e) {
            throw new JeecgBootBizTipException("加密原文错误:" + e.getMessage(), e);
        } catch (InvalidKeySpecException e) {
            throw new JeecgBootBizTipException(e);
        }
    }
}
3.2.4 提交进件申请

根据填写的资料构建请求体,发送请求 https://pay.weixin.qq.com/doc/v3/partner/4012719997

java
/**
* 提交申请
*/
@Override
@Transactional(rollbackFor = Exception.class)
public MerchantApplyment submitApplyment(String applymentId){
    MerchantApplyment applyment = baseMapper.selectById(applymentId);
    // 超级管理员信息加密处理
    ContactInfoDTO contactInfo = JSONUtil.toBean(applyment.getContactInfo(), ContactInfoDTO.class);
    contactInfo.setContact_name(wxRequestUtil.wxPubKeyEncrypt(contactInfo.getContact_name()));
    contactInfo.setContact_id_number(wxRequestUtil.wxPubKeyEncrypt(contactInfo.getContact_id_number()));
    contactInfo.setMobile_phone(wxRequestUtil.wxPubKeyEncrypt(contactInfo.getMobile_phone()));
    contactInfo.setContact_email(wxRequestUtil.wxPubKeyEncrypt(contactInfo.getContact_email()));

    SubjectInfoDTO subjectInfo = JSONUtil.toBean(applyment.getSubjectInfo(), SubjectInfoDTO.class);
    if(subjectInfo == null){
        throw new JeecgBootBizTipException("主体信息不能为空!");
    }
    if("SUBJECT_TYPE_INDIVIDUAL".equals(subjectInfo.getSubject_type()) || "SUBJECT_TYPE_ENTERPRISE".equals(subjectInfo.getSubject_type())){
        // 当主体是个体户/企业时,登记证书为空
        subjectInfo.setCertificate_info(null);
    }else{
        // 当主体是政府机关/事业单位/其他组织时,营业执照为空
        subjectInfo.setBusiness_license_info(null);
    }
    if(!"SUBJECT_TYPE_ENTERPRISE".equals(subjectInfo.getSubject_type())){
        // 主体不是企业时,是否为受益人和受益人列表为空
        subjectInfo.getIdentity_info().setOwner(null);
        subjectInfo.setUbo_info_list(null);
    }
    // 身份证信息加密, 本系统不对 非身份证证件类型的信息处理
    IdCardInfoDTO idCardInfo = subjectInfo.getIdentity_info().getId_card_info();
    if(idCardInfo != null){
        idCardInfo.setId_card_name(wxRequestUtil.wxPubKeyEncrypt(idCardInfo.getId_card_name()));
        idCardInfo.setId_card_number(wxRequestUtil.wxPubKeyEncrypt(idCardInfo.getId_card_number()));
        idCardInfo.setId_card_address(wxRequestUtil.wxPubKeyEncrypt(idCardInfo.getId_card_address()));
    }

    if(CollUtil.isNotEmpty(subjectInfo.getUbo_info_list())){
        // 受益人信息加密处理
        for (UboInfoDTO uboInfo : subjectInfo.getUbo_info_list()) {
            uboInfo.setUbo_id_doc_name(wxRequestUtil.wxPubKeyEncrypt(uboInfo.getUbo_id_doc_name()));
            uboInfo.setUbo_id_doc_number(wxRequestUtil.wxPubKeyEncrypt(uboInfo.getUbo_id_doc_number()));
            uboInfo.setUbo_id_doc_address(wxRequestUtil.wxPubKeyEncrypt(uboInfo.getUbo_id_doc_address()));
        }
    }

    // 设置经营场景,锁死小程序
    BusinessInfoDTO businessInfo = JSONUtil.toBean(applyment.getBusinessInfo(), BusinessInfoDTO.class);
    businessInfo.setSales_info(SalesInfoDTO.builder()
                               // 小程序经营场景类型
                               .sales_scenes_type(List.of("SALES_SCENES_MINI_PROGRAM"))
                               .mini_program_info(MiniProgramInfoDTO.builder()
                                                  //.mini_program_sub_appid(wechatPayProperties.getAppId())
                                                  // mini_program_appid 是服务商小程序的appId
                                                  .mini_program_appid(wechatPayProperties.getAppId())
                                                  .build()).build());

    SettlementInfoDTO settlementInfo = JSONUtil.toBean(applyment.getSettlementInfo(), SettlementInfoDTO.class);
    // 计算银行账户信息加密处理
    BankAccountInfoDTO bankAccountInfo = JSONUtil.toBean(applyment.getBankAccountInfo(), BankAccountInfoDTO.class);
    bankAccountInfo.setAccount_name(wxRequestUtil.wxPubKeyEncrypt(bankAccountInfo.getAccount_name()));
    bankAccountInfo.setAccount_number(wxRequestUtil.wxPubKeyEncrypt(bankAccountInfo.getAccount_number()));

    ApplyReqBodyDTO body = ApplyReqBodyDTO.builder()
        .business_code(applyment.getBusinessCode())
        .contact_info(contactInfo)
        .subject_info(subjectInfo)
        .business_info(businessInfo)
        .settlement_info(settlementInfo)
        .bank_account_info(bankAccountInfo)
        .build();
    // 提交申请
    String requestUrl = "https://api.mch.weixin.qq.com/v3/applyment4sub/applyment/";
    HashMap<String, Object> res = wxRequestUtil.wxPostRequest(requestUrl, body);
    // 保存微信支付返回的申请单号
    Double applymentNoDoubleType = (Double) res.get("applyment_id");
    DecimalFormat df = new DecimalFormat("0");
    // 获取返回的微信支付申请单号
    String applymentNo = df.format(applymentNoDoubleType);
    applyment.setApplymentNo(applymentNo);
    baseMapper.updateById(applyment);
    getStatusByApplymentId(applymentId);
    return applyment;
}
3.2.5 获取状态

根据申请单号查询申请状态 https://pay.weixin.qq.com/doc/v3/partner/4012697052

java
@Override
public HashMap getStatusByApplymentId(String applymentId){
    MerchantApplyment applyment = baseMapper.selectById(applymentId);
    String applymentNo = applyment.getApplymentNo();
    String requestPath =
        CharSequenceUtil.format("https://api.mch.weixin.qq.com/v3/applyment4sub/applyment/applyment_id/{}", applymentNo);
    HashMap result = wxRequestUtil.wxGetRequest(requestPath);
    // 修改商户状态
    String applymentState = (String) result.get("applyment_state");
    Integer statusCode = ApplymentStatusEnum.getCode(applymentState);
    if(!applyment.getApplymentStatus().equals(statusCode)){
        // 本地状态和线上状态不一致,更新
        applyment.setApplymentStatus(statusCode);
        baseMapper.updateById(applyment);
        // 商户表也需要更新
        Merchant merchant = Merchant.builder().applymentStatus(statusCode).applymentId(applymentId).build();
        // 状态是待签约,获取商户号
        if(statusCode.equals(ApplymentStatusEnum.APPLYMENT_STATE_TO_BE_SIGNED.getCode())){
            merchant.setSubMchId((String) result.get("sub_mchid"));
        }
        merchantService.updateInfoByApplymentId(merchant);
        baseMapper.updateById(applyment);
    }
    ContactInfoDTO contactInfo = JSONUtil.toBean(applyment.getContactInfo(), ContactInfoDTO.class);
    Map<Object, Object> merchantInfo = MapUtil.builder().put("superAdmin", contactInfo.getContact_name()).build();
    SubjectInfoDTO subjectInfo = JSONUtil.toBean(applyment.getSubjectInfo(), SubjectInfoDTO.class);
    if("SUBJECT_TYPE_INDIVIDUAL".equals(subjectInfo.getSubject_type()) || "SUBJECT_TYPE_ENTERPRISE".equals(subjectInfo.getSubject_type())){
        // 当主体是个体户/企业时,登记证书为空
        merchantInfo.put("merchantName", subjectInfo.getBusiness_license_info().getMerchant_name());
    }else{
        // 当主体是政府机关/事业单位/其他组织时
        merchantInfo.put("merchantName", subjectInfo.getCertificate_info().getMerchant_name());
    }
    result.put("merchantInfo", merchantInfo);
    result.put("applymentInfo", applyment);
    return result;
}
3.2.6 资料文件上传

微信特约商户进件API所需的图片和视频都是上传mediaId

java
@Service
@Slf4j
@RequiredArgsConstructor
public class WxFileService {

    private final WechatPayConfig wechatPayConfig;
    private static final String IMAGE_UPLOAD_PATH = "https://api.mch.weixin.qq.com/v3/merchant/media/upload";
    private static final String VIDEO_UPLOAD_PATH = "https://api.mch.weixin.qq.com/v3/merchant/media/video_upload";
    // 2MB
    private static final long IMAGE_MAX_FILE_SIZE = 2 * 1024 * 1024;
    // 允许上传的图片
    private static final List<String> IMAGE_ALLOWED_EXTENSIONS = Arrays.asList("jpg", "png", "bmp", "jpeg");
    // 5MB
    private static final long VIDEO_MAX_FILE_SIZE = 5 * 1024 * 1024;
    // 允许上传的视频
    private static final List<String> VIDEO_ALLOWED_EXTENSIONS = Arrays.asList("avi", "wmv", "mpeg", "mp4", "mov", "mkv", "flv", "f4v", "m4v", "rmvb");


    public Map<String, Object> uploadImage(MultipartFile file) {
        // 文件大小不能超过2M
        // 1. 检查文件大小
        if (file.getSize() > IMAGE_MAX_FILE_SIZE) {
            throw new JeecgBootBizTipException("文件大小不能超过 2MB!");
        }
        String filename = file.getOriginalFilename();
        String suffix = FileUtil.getSuffix(filename);
        // 图片类型支持JPG、BMP、PNG格式
        if (!IMAGE_ALLOWED_EXTENSIONS.contains(suffix)) {
            throw new JeecgBootBizTipException("只支持 JPG、BMP、PNG 格式的文件!");
        }
        Map<String, Object> result = MapUtil.<String, Object>builder().build();
        Config config = wechatPayConfig.getConfig();
        FileUploadService service = new FileUploadService.Builder().config(config).build();
        File localFile = FileUtil.createTempFile("." + suffix, true);
        try {
            // 上传到对象存储
            // 把文件上传到Oss放在 transferTo方法执行之前
            // transferTo 执行之后,OssBootUtil获取不到文件内容
            String url = OssBootUtil.upload(file, "upload/wx");
            result.put("url", url);
            file.transferTo(localFile);
        } catch (Exception e) {
            log.error(e.getMessage());
            throw new JeecgBootBizTipException("文件上传失败!");
        }
        try {
            byte[] bytes = FileUtil.readBytes(localFile);
            String sha256 = DigestUtil.sha256Hex(bytes);
            Map<Object, Object> meta = MapUtil.builder().put("filename", filename).put("sha256", sha256).build();
            // 上传到微信服务器
            FileUploadResponse response = service.uploadImage(IMAGE_UPLOAD_PATH, JSONUtil.toJsonStr(meta), filename, bytes);
            result.put("mediaId", response.getMediaId());
            return result;
        } catch (Exception e) {
            log.error(e.getMessage());
            throw new JeecgBootBizTipException("文件上传失败!");
        }
    }

    public Map<String, Object> uploadVideo(MultipartFile file) {
        // 文件大小不能超过5M
        // 1. 检查文件大小
        if (file.getSize() > VIDEO_MAX_FILE_SIZE) {
            throw new JeecgBootBizTipException("文件大小不能超过 5MB!");
        }
        String filename = file.getOriginalFilename();
        String suffix = FileUtil.getSuffix(filename);
        // 检查视频格式
        if (!VIDEO_ALLOWED_EXTENSIONS.contains(suffix)) {
            throw new JeecgBootBizTipException("只支持 avi、wmv、mpeg、mp4、mov、mkv、flv、f4v、m4v、rmvb格式的文件!");
        }
        Map<String, Object> result = MapUtil.<String, Object>builder().build();
        Config config = wechatPayConfig.getConfig();
        FileUploadService service = new FileUploadService.Builder().config(config).build();
        File localFile = FileUtil.createTempFile("." + suffix, true);
        try {
            // 上传到对象存储
            // 把文件上传到Oss放在 transferTo方法执行之前
            // transferTo 执行之后,OssBootUtil获取不到文件内容
            String url = OssBootUtil.upload(file, "upload/wx");
            result.put("url", url);
            file.transferTo(localFile);
        } catch (Exception e) {
            log.error("文件保存失败!");
            throw new JeecgBootBizTipException("文件上传失败!");
        }
        try {
            byte[] bytes = FileUtil.readBytes(localFile);
            String sha256 = DigestUtil.sha256Hex(bytes);
            Map<Object, Object> meta = MapUtil.builder().put("filename", filename).put("sha256", sha256).build();
            // 上传到微信服务器
            FileUploadResponse response = service.uploadVideo(VIDEO_UPLOAD_PATH, JSONUtil.toJsonStr(meta), filename, bytes);
            result.put("mediaId", response.getMediaId());
            return result;
        } catch (Exception e) {
            log.error(e.getMessage());
            throw new JeecgBootBizTipException("文件上传失败!");
        }
    }
}
3.2.7 银行信息获取

特约商户进件获取银行信息

因为获取的银行列表直接获取全部,因此对请求进行缓存处理

java
@Service
@Slf4j
@RequiredArgsConstructor
public class WxBankService {

    private final WxRequestUtil wxRequestUtil;
    private final RedisUtil redisUtil;

    /**
     * 获取省份列表
     */
    public HashMap getProvinces() {
        String url = "https://api.mch.weixin.qq.com/v3/capital/capitallhh/areas/provinces";
        return wxRequestUtil.wxGetRequest(url);
    }

    /**
     * 获取城市列表
     * @param provinceCode 省份编码
     */
    public HashMap getCities(String provinceCode) {
        String url = StrUtil.format("https://api.mch.weixin.qq.com/v3/capital/capitallhh/areas/provinces/{}/cities", provinceCode);
        return wxRequestUtil.wxGetRequest(url);
    }

    /**
     * 获取支持个人业务的银行列表
     */
    public List<HashMap> getPersonalBankList(){
        // 目前支持个人业务的银行有4408个
        String key = "payment:wx:PersonalBankList";
        Object cacheObj = redisUtil.get(key);
        List<HashMap> result;
        if (cacheObj != null) {
            log.info(" 查询缓存");
            result = JSONUtil.toList(String.valueOf(cacheObj), HashMap.class);
        }else{
            log.info(" getPersonalBankList没有走缓存");
            // 还没有存入缓存
            result = new ArrayList<>(4500);
            String currentUrl = "/v3/capital/capitallhh/banks/personal-banking?limit=200";
            while (StrUtil.isNotBlank(currentUrl)) {
                String url = StrUtil.format("https://api.mch.weixin.qq.com{}", currentUrl);
                HashMap hashMap = wxRequestUtil.wxGetRequest(url);
                Object data = hashMap.get("data");
                JSONArray array = JSONUtil.parseArray(data);
                List<HashMap> list = array.toList(HashMap.class);
                result.addAll(list);
                log.info("当前currentUrl:{}", currentUrl);
                currentUrl = (String) JSONUtil.parseObj(hashMap.get("links")).getOrDefault("next", "");
            }
            String value = JSONUtil.toJsonStr(result);
            redisUtil.set(key, value);
            redisUtil.expire(key, 60L *  60L * 24L * 30L);
        }
        return result;
    }

    /**
     * 获取支持对公业务的银行列表
     */
    public List<HashMap> getPublicBankList(){
        // 目前支持对公业务的银行有4414个
        String key = "payment:wx:PublicBankList";
        Object cacheObj = redisUtil.get(key);
        List<HashMap> result;
        if (cacheObj != null) {
            log.info("getPublicBankList 查询缓存");
            result = JSONUtil.toList(String.valueOf(cacheObj), HashMap.class);
        }else{
            log.info("getPublicBankList 没有走缓存");
            // 还没有存入缓存
            result = new ArrayList<>(4500);
            // 一次最大请求200条数据
            String currentUrl = "/v3/capital/capitallhh/banks/corporate-banking?limit=200";
            while (StrUtil.isNotBlank(currentUrl)) {
                String url = StrUtil.format("https://api.mch.weixin.qq.com{}", currentUrl);
                HashMap hashMap = wxRequestUtil.wxGetRequest(url);
                Object data = hashMap.get("data");
                JSONArray array = JSONUtil.parseArray(data);
                List<HashMap> list = array.toList(HashMap.class);
                result.addAll(list);
                log.info("getPublicBankList 当前currentUrl:{}", currentUrl);
                currentUrl = (String) JSONUtil.parseObj(hashMap.get("links")).getOrDefault("next", "");
            }
            String value = JSONUtil.toJsonStr(result);
            redisUtil.set(key, value);
            redisUtil.expire(key, 60L *  60L * 24L * 30L);
        }
        return result;
    }

    /**
     * 获取支行列表
     */
    public List<HashMap> getBranchBankList(String bankCode, String cityCode) {
        // 一次最大请求200条数据
        String currentUrl = StrUtil.format("/v3/capital/capitallhh/banks/{}/branches?city_code={}&limit=200", bankCode, cityCode);
        List<HashMap> result = new  ArrayList<>(200);
        while (CharSequenceUtil.isNotBlank(currentUrl)) {
            String url = StrUtil.format("https://api.mch.weixin.qq.com{}", currentUrl);
            HashMap hashMap = wxRequestUtil.wxGetRequest(url);
            Object data = hashMap.get("data");
            JSONArray array = JSONUtil.parseArray(data);
            List<HashMap> list = array.toList(HashMap.class);
            result.addAll(list);
            log.info("getBranchBankList 当前currentUrl:{}", currentUrl);
            String temp = (String) JSONUtil.parseObj(hashMap.get("links")).getOrDefault("next", "");
            if(CharSequenceUtil.isNotBlank(temp)){
                // 返回的链接不会带上city_code
                currentUrl = temp + "&city_code=10";
            }else {
                currentUrl = temp;
            }
        }
        return result;
    }
}

4 商户注销

注:特约商户也算是属于特约商户

特约商户注销后,服务商的特约商户列表中注销的特约商户仍然存在

商户注销链接:https://kf.qq.com/touch/sappfaq/221220E32IBV221220rEnqI7.html?scene_id=kf594&platform=14

目前服务商还不支持主动注销

Last updated:

开发者笔记仓库