基于 wechatpay-java 0.2.17 + Spring Boot 2.7 + uni-app

核心特性: RSA公钥模式、支付回调验签、openid自动获取

(这篇文章本该早早发布,但是服务器资源迁移遗漏了数据现在补充上)


技术栈

  • 后端: Spring Boot 2.7.7、wechatpay-java 0.2.17

  • 前端: uni-app(微信小程序)

  • SDK模式: RSAPublicKeyConfig(公钥模式)


## 前置准备

### 需要具备的条件

1. **已注册的小程序**
   - 小程序已上线或处于审核状态
   - 已完成微信认证(企业主体)

2. **开通微信支付商户号**
   - 商户主体与小程序主体一致
   - 完成商户号入驻审核

一、Maven依赖

<dependency>
    <groupId>com.github.wechatpay-apiv3</groupId>
    <artifactId>wechatpay-java</artifactId>
    <version>0.2.17</version>
</dependency>

注意: 如果Maven仓库找不到(因为可能是私有仓库拉不下来),需要手动编译安装(博主此次使用的就是):

# 下载源码
git clone https://github.com/wechatpay-apiv3/wechatpay-java.git
cd wechatpay-java
​
# 编译安装
gradlew clean build
gradlew publishToMavenLocal
​
# 复制到自定义Maven仓库(可选)
xcopy "%USERPROFILE%\.m2\repository\com\github\wechatpay-apiv3" "D:\Maven\repo\com\github\wechatpay-apiv3" /E /I /Y

二、配置文件

application.yml

# 微信小程序配置
wechat:
  miniapp:
    app-id: xxxxx                #小程序后台配置
    app-secret: your_app_secret  # 小程序后台获取
​
# 微信支付配置
wxpay:
  app-id: xxxx
  mch-id: xxxx   #商户号
  private-key-path: classpath:wxpay/apiclient_key.pem
  merchant-serial-number: xxx #证书序列号
  wechat-pay-public-key-path: classpath:wxpay/wechatpay_public_key.pem
  wechat-pay-public-key-id: PUB_KEY_ID_xxxx   #公钥id
  api-v3-key: your_32_char_api_v3_key  #APIv3密钥
  notify-url: https://your-domain.com/api/payment/wxpay/notify #回调地址
  refund-notify-url: https://your-domain.com/api/payment/wxpay/refund-notify #回调地址

证书文件位置(自行修改,和yml保持一致就行)

src/main/resources/wxpay/
├── apiclient_key.pem           # 商户API私钥
└── wechatpay_public_key.pem    # 微信支付平台公钥

上述配置文件获取信息资料

  • 小程序appid

  • 小程序密钥

  • 商户号

  • 公钥id以及文件

相关文档:

申请商户API证书_通用规则|微信支付商户文档中心

申请商户API证书_通用规则|微信支付商户文档中心
配置APIv3密钥_通用规则|微信支付商户文档中心

微信支付公钥产品简介及使用说明_微信支付公钥|微信支付商户文档中心

小程序

三、核心配置类

1. 微信支付配置

@Slf4j
@Data
@Configuration
@ConfigurationProperties(prefix = "wxpay")
public class WxPayConfig {
​
    private String appId;
    private String mchId;
    private String privateKeyPath;
    private String merchantSerialNumber;
    private String wechatPayPublicKeyPath;
    private String wechatPayPublicKeyId;
    private String apiV3Key;
    private String notifyUrl;
    private String refundNotifyUrl;
​
    @Bean
    public Config config() {
        try {
            File privateKeyFile = ResourceUtils.getFile(privateKeyPath);
            File wechatPayPublicKeyFile = ResourceUtils.getFile(wechatPayPublicKeyPath);
​
            return new RSAPublicKeyConfig.Builder()
                    .merchantId(mchId)
                    .privateKeyFromPath(privateKeyFile.getAbsolutePath())
                    .publicKeyFromPath(wechatPayPublicKeyFile.getAbsolutePath())
                    .publicKeyId(wechatPayPublicKeyId)
                    .merchantSerialNumber(merchantSerialNumber)
                    .apiV3Key(apiV3Key)
                    .build();
        } catch (Exception e) {
            log.error("微信支付配置初始化失败", e);
            throw new RuntimeException("微信支付配置初始化失败:" + e.getMessage(), e);
        }
    }
​
    @Bean
    public JsapiServiceExtension jsapiService(Config config) {
        return new JsapiServiceExtension.Builder()
                .config(config)
                .build();
    }
​
    @Bean
    public RefundService refundService(Config config) {
        return new RefundService.Builder()
                .config(config)
                .build();
    }
}

2. 微信小程序配置

@Data
@Configuration
@ConfigurationProperties(prefix = "wechat.miniapp")
public class WechatMiniappConfig {
    private String appId;
    private String appSecret;
}

3. RestTemplate 配置(解决微信接口 text/plain 问题)

@Configuration
public class RestTemplateConfig {
​
    @Bean
    public RestTemplate restTemplate() {
        RestTemplate restTemplate = new RestTemplate();
        
        // 微信接口返回 Content-Type: text/plain,但内容是JSON
        MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
        converter.setSupportedMediaTypes(Arrays.asList(
                MediaType.APPLICATION_JSON,
                MediaType.TEXT_PLAIN,
                MediaType.TEXT_HTML
        ));
        
        restTemplate.getMessageConverters().add(0, converter);
        return restTemplate;
    }
}

四、核心业务代码

1. 获取 OpenID

后端:UserService

@Override
public String getAndSaveOpenid(String code) {
    try {
        // 调用微信接口
        String url = String.format(
            "https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code",
            wechatMiniappConfig.getAppId(),
            wechatMiniappConfig.getAppSecret(),
            code
        );
        
        WxLoginResponse response = restTemplate.getForObject(url, WxLoginResponse.class);
        
        if (response == null || response.getErrcode() != null && response.getErrcode() != 0) {
            throw new BusinessException("获取openid失败:" + response.getErrmsg());
        }
        
        String openid = response.getOpenid();
        
        // 保存到用户表
        Long userId = StpUtil.getLoginIdAsLong();
        User user = userMapper.selectById(userId);
        user.setOpenid(openid);
        userMapper.updateById(user);
        
        return openid;
    } catch (Exception e) {
        log.error("获取并保存openid失败", e);
        throw new BusinessException("获取openid失败:" + e.getMessage());
    }
}

DTO:WxLoginResponse

@Data
public class WxLoginResponse {
    @JsonProperty("openid")
    private String openid;
    
    @JsonProperty("session_key")
    private String sessionKey;
    
    @JsonProperty("errcode")
    private Integer errcode;
    
    @JsonProperty("errmsg")
    private String errmsg;
}

前端:获取 OpenID

async getUserOpenId() {
  try {
    // 1. 先从store获取
    const openid = this.$store.getters['user/openid']
    if (openid) return openid
    
    // 2. 调用wx.login获取code
    const loginRes = await uni.login({ provider: 'weixin' })
    const code = loginRes[1].code
    
    // 3. 用code换openid
    const { getWxOpenid } = require('@/api/user')
    const result = await getWxOpenid(code)
    
    // 4. 保存到store
    this.$store.commit('user/SET_OPENID', result.openid)
    
    return result.openid
  } catch (error) {
    throw error
  }
}

2. 创建支付订单

后端:WxPayService

@Override
public WxPayResponseDTO createPrepayOrder(Order order, String openid) {
    try {
        PrepayRequest request = new PrepayRequest();
        request.setAppid(wxPayConfig.getAppId());
        request.setMchid(wxPayConfig.getMchId());
        request.setDescription("订单支付:" + order.getOrderNo());
        request.setOutTradeNo(order.getOrderNo());
        request.setNotifyUrl(wxPayConfig.getNotifyUrl());
        
        // 金额(单位:分)
        Amount amount = new Amount();
        amount.setTotal(order.getActualAmount().multiply(new BigDecimal("100")).intValue());
        amount.setCurrency("CNY");
        request.setAmount(amount);
        
        // 用户openid
        Payer payer = new Payer();
        payer.setOpenid(openid);
        request.setPayer(payer);
        
        // 调用SDK
        PrepayWithRequestPaymentResponse response = jsapiService.prepayWithRequestPayment(request);
        
        return WxPayResponseDTO.builder()
                .timeStamp(response.getTimeStamp())
                .nonceStr(response.getNonceStr())
                .packageValue(response.getPackageVal())
                .signType(response.getSignType())
                .paySign(response.getPaySign())
                .build();
    } catch (Exception e) {
        log.error("创建预支付订单失败:orderNo={}", order.getOrderNo(), e);
        throw new BusinessException("创建支付订单失败:" + e.getMessage());
    }
}

前端:调起支付

async doWxPay() {
  try {
    // 1. 获取openid
    const openid = await this.getUserOpenId()
    
    // 2. 创建预支付订单
    const { createWxPayOrder } = require('@/api/payment')
    const payParams = await createWxPayOrder({
      orderId: this.orderId,
      openid: openid
    })
    
    // 3. 调起微信支付
    await uni.requestPayment({
      provider: 'wxpay',
      timeStamp: payParams.timeStamp,
      nonceStr: payParams.nonceStr,
      package: payParams.packageValue,
      signType: payParams.signType,
      paySign: payParams.paySign
    })
    
    // 4. 支付成功处理
    uni.showToast({ title: '支付成功', icon: 'success' })
    this.navigateToOrderDetail()
  } catch (error) {
    console.error('微信支付失败:', error)
  }
}

3. 支付回调处理

Controller:接收回调

@PostMapping("/wxpay/notify")
public Map<String, String> wxPayNotify(
        @RequestBody String requestBody,
        @RequestHeader("Wechatpay-Signature") String signature,
        @RequestHeader("Wechatpay-Serial") String serial,
        @RequestHeader("Wechatpay-Timestamp") String timestamp,
        @RequestHeader("Wechatpay-Nonce") String nonce) {
    
    try {
        // 验签并解析回调数据
        WxPayNotifyResult result = wxPayService.handlePayNotify(
            requestBody, signature, serial, timestamp, nonce
        );
        
        // 处理订单状态
        if ("SUCCESS".equals(result.getTradeState())) {
            orderService.handlePaySuccess(
                result.getOutTradeNo(),
                result.getTransactionId()
            );
        }
        
        // 返回成功响应
        Map<String, String> response = new HashMap<>();
        response.put("code", "SUCCESS");
        response.put("message", "成功");
        return response;
        
    } catch (Exception e) {
        log.error("处理支付回调失败", e);
        
        Map<String, String> response = new HashMap<>();
        response.put("code", "FAIL");
        response.put("message", e.getMessage());
        return response;
    }
}

Service:验签和解析

@Override
public WxPayNotifyResult handlePayNotify(String requestBody, String signature,
                                         String serial, String timestamp, String nonce) {
    try {
        // 创建解析器
        NotificationParser parser = new NotificationParser(
            (RSAPublicKeyConfig) wxPayConfig.config()
        );
        
        // 构建请求参数
        RequestParam requestParam = new RequestParam.Builder()
                .serialNumber(serial)
                .nonce(nonce)
                .signature(signature)
                .timestamp(timestamp)
                .body(requestBody)
                .build();
        
        // 验签并解析
        Transaction transaction = parser.parse(requestParam, Transaction.class);
        
        // 转换为业务对象
        return convertToNotifyResult(transaction);
        
    } catch (Exception e) {
        log.error("处理支付回调通知失败", e);
        throw new BusinessException("处理支付回调失败:" + e.getMessage());
    }
}

4. 申请退款

@Override
public String refund(Order order, String refundReason) {
    try {
        String refundNo = "RF" + order.getOrderNo();
        
        CreateRequest request = new CreateRequest();
        request.setOutTradeNo(order.getOrderNo());
        request.setOutRefundNo(refundNo);
        request.setReason(refundReason);
        request.setNotifyUrl(wxPayConfig.getRefundNotifyUrl());
        
        // 退款金额(单位:分)
        AmountReq amountReq = new AmountReq();
        amountReq.setRefund(order.getActualAmount().multiply(new BigDecimal("100")).intValue());
        amountReq.setTotal(order.getActualAmount().multiply(new BigDecimal("100")).intValue());
        amountReq.setCurrency("CNY");
        request.setAmount(amountReq);
        
        Refund response = refundService.create(request);
        
        log.info("申请退款成功:orderNo={}, refundNo={}", order.getOrderNo(), refundNo);
        
        return response.getOutRefundNo();
        
    } catch (Exception e) {
        log.error("申请退款失败:orderNo={}", order.getOrderNo(), e);
        throw new BusinessException("申请退款失败:" + e.getMessage());
    }
}

五、数据库表结构

ALTER TABLE `t_order` 
ADD COLUMN `pay_type` TINYINT(1) COMMENT '支付方式:1-余额,2-微信支付' AFTER `order_status`,
ADD COLUMN `transaction_id` VARCHAR(64) COMMENT '微信支付交易号' AFTER `pay_type`,
ADD COLUMN `prepay_id` VARCHAR(64) COMMENT '微信支付预支付ID' AFTER `transaction_id`,
ADD INDEX `idx_transaction_id` (`transaction_id`);

ALTER TABLE `t_user`
ADD COLUMN `openid` VARCHAR(64) COMMENT '微信openid' AFTER `id`;

六、常见问题

1. Content-Type 错误

错误: no suitable HttpMessageConverter found for response type [class xxx] and content type [text/plain]

原因: 微信接口返回 Content-Type: text/plain,但内容是JSON

解决: 配置 RestTemplate 支持 text/plain

MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
converter.setSupportedMediaTypes(Arrays.asList(
    MediaType.APPLICATION_JSON,
    MediaType.TEXT_PLAIN  // 关键
));
restTemplate.getMessageConverters().add(0, converter);

2. Long 类型精度丢失

现象: 订单ID、交易号末尾变成00

原因: JavaScript Number 最大安全整数是 2^53-1

解决: 后端配置 Jackson 将 Long 序列化为 String

@Configuration
public class JacksonConfig {
    @Bean
    public Jackson2ObjectMapperBuilderCustomizer customizer() {
        return builder -> {
            SimpleModule module = new SimpleModule();
            module.addSerializer(Long.class, ToStringSerializer.instance);
            module.addSerializer(Long.TYPE, ToStringSerializer.instance);
            builder.modules(module);
        };
    }
}

3. 本地开发无法收到回调

原因: 微信服务器无法访问 localhost

解决: 使用内网穿透工具/或者就只能是线上域名调试了

# Natapp(推荐)
natapp.exe -authtoken=your_token

# 获得临时域名,如:http://abc123.natapp1.cc
# 修改配置:notify-url: http://abc123.natapp1.cc/api/payment/wxpay/notify

4. Bean 创建失败

错误: Parameter 0 of constructor required a bean that could not be found

原因: 证书文件不存在,导致 WxPayConfig Bean 初始化失败

解决: 确保证书文件放在正确位置

src/main/resources/wxpay/
├── apiclient_key.pem
└── wechatpay_public_key.pem

七、生产环境部署

1. 安全配置

.gitignore

# 证书文件
*.pem
admin/src/main/resources/wxpay/*.pem

# 配置文件(包含密钥)
application-prod.yml

使用环境变量

wechat:
  miniapp:
    app-secret: ${WECHAT_APP_SECRET}

wxpay:
  api-v3-key: ${WXPAY_API_V3_KEY}
  notify-url: ${WXPAY_NOTIFY_URL}

2. 服务器域名配置

小程序后台配置白名单:

request合法域名:https://your-api-domain.com

八、测试流程

1. 启动后端 → 检查日志确认微信支付配置成功
2. 启动前端 → 微信开发者工具登录
3. 创建订单 → 选择微信支付
4. 获取openid → 自动调用wx.login
5. 调起支付 → 扫码支付(开发者工具支持)
6. 支付成功 → 检查回调是否正常(需要内网穿透)
7. 订单状态更新 → 验证业务流程

九、关键依赖版本

依赖

版本

说明

Spring Boot

2.7.7

wechatpay-java

0.2.17

官方SDK

Sa-Token

1.34.0

权限认证

MyBatis-Plus

3.5.3.1

ORM框架


十、完整代码仓库

建议参考完整项目代码理解上下文,核心文件:

backend/
├── config/
│   ├── WxPayConfig.java              # 微信支付配置
│   ├── WechatMiniappConfig.java      # 小程序配置
│   └── RestTemplateConfig.java       # HTTP客户端配置
├── service/
│   ├── WxPayService.java             # 支付服务接口
│   └── impl/WxPayServiceImpl.java    # 支付服务实现
├── controller/
│   └── PaymentController.java        # 支付控制器
└── dto/
    ├── WxPayRequestDTO.java          # 支付请求
    ├── WxPayResponseDTO.java         # 支付响应
    ├── WxPayNotifyDTO.java           # 回调通知
    └── WxLoginResponse.java          # 登录响应

frontend/
├── api/
│   ├── payment.js                    # 支付API
│   └── user.js                       # 用户API
├── pages/order/
│   └── payment.vue                   # 支付页面
└── store/modules/
    └── user.js                       # 用户状态管理

总结

核心要点:

  1. ✅ 使用官方SDK wechatpay-java 0.2.17

  2. ✅ RSAPublicKeyConfig 模式(公钥模式)

  3. ✅ 配置 RestTemplate 处理微信接口的 text/plain

  4. ✅ Long 转 String 避免精度丢失

  5. ✅ openid 自动获取和存储

  6. ✅ 支付回调验签处理

  7. ✅ 本地开发使用内网穿透

适用场景:

  • 微信小程序商城

  • 点餐系统

  • 跑腿代购平台

  • 任何需要微信支付的小程序


环境: Spring Boot 2.7 + uni-app + 微信支付APIv3