微信小程序支付2025最新版快速接入
基于
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证书_通用规则|微信支付商户文档中心
配置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/notify4. 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. 订单状态更新 → 验证业务流程九、关键依赖版本
十、完整代码仓库
建议参考完整项目代码理解上下文,核心文件:
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 # 用户状态管理总结
核心要点:
✅ 使用官方SDK
wechatpay-java 0.2.17✅ RSAPublicKeyConfig 模式(公钥模式)
✅ 配置 RestTemplate 处理微信接口的 text/plain
✅ Long 转 String 避免精度丢失
✅ openid 自动获取和存储
✅ 支付回调验签处理
✅ 本地开发使用内网穿透
适用场景:
微信小程序商城
点餐系统
跑腿代购平台
任何需要微信支付的小程序
环境: Spring Boot 2.7 + uni-app + 微信支付APIv3