1. 多商户微信小程序登录与支付配置中心概述
在企业级应用中,经常需要同时管理多个微信小程序和支付商户号。比如一个电商平台可能为不同地区的商户提供独立的小程序,每个商户又有自己的微信支付账号。传统做法是为每个小程序和支付账号单独配置,但随着业务增长,这种模式会变得难以维护。
binary-wang(WxJava)SDK提供了一套完善的解决方案,通过Spring Boot的配置管理机制,我们可以构建一个集中式的配置中心。这个配置中心能够动态管理多个小程序的登录认证(code2session)和多个微信支付商户号(V3接口)的切换,大幅简化配置工作。
我去年参与过一个跨境电商项目,需要同时对接8个小程序和12个支付商户号。最初采用传统配置方式,每次新增商户都要改代码重启服务。后来改用WxJava的集中配置方案后,新增商户只需在配置文件中添加几行即可,效率提升了90%以上。
2. 环境准备与依赖配置
2.1 引入必要的Maven依赖
首先需要在项目中引入WxJava的核心依赖。建议使用BOM方式管理版本,避免依赖冲突:
<dependencyManagement> <dependencies> <dependency> <groupId>com.github.binarywang</groupId> <artifactId>wx-java-bom</artifactId> <version>4.8.3.B</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <!-- 小程序相关 --> <dependency> <groupId>com.github.binarywang</groupId> <artifactId>weixin-java-miniapp</artifactId> </dependency> <!-- 支付相关 --> <dependency> <groupId>com.github.binarywang</groupId> <artifactId>weixin-java-pay</artifactId> </dependency> <!-- Spring Boot配置注解支持 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency> </dependencies>2.2 配置文件结构设计
对于多商户场景,推荐使用YAML格式的配置文件,结构更清晰。下面是一个典型的多商户配置示例:
wx: mini: configs: - appid: wx1234567890abcdef # 小程序1 secret: 4023c4f58d704d9a8b2e3d4f5a6b7c8d token: merchant1_token aesKey: abcdefghijklmnopqrstuvwxyz012345 - appid: wx9876543210fedcba # 小程序2 secret: 5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0 token: merchant2_token aesKey: 0123456789abcdefghijklmnopqrstuv pay: configs: - mchId: 1230001 # 商户号1 mchSerialNo: 1D685BC1A16B008C wxApiV3Key: abcdef1234567890abcdef1234567890 notifyUrl: https://yourdomain.com/api/pay/notify keyPath: classpath:certs/merchant1_key.pem - mchId: 1230002 # 商户号2 mchSerialNo: 2E796BC2B27C119D wxApiV3Key: 0987654321abcdef0987654321abcdef notifyUrl: https://yourdomain.com/api/pay/notify keyPath: classpath:certs/merchant2_key.pem注意:证书文件(.pem)需要放在resources/certs目录下,确保打包后能被正确加载
3. 核心配置类实现
3.1 配置属性映射类
首先创建两个配置类来映射YAML中的配置:
@Data @ConfigurationProperties(prefix = "wx.mini") public class WxMaProperties { private List<MaConfig> configs; @Data public static class MaConfig { private String appid; private String secret; private String token; private String aesKey; private String msgDataFormat = "JSON"; } } @Data @ConfigurationProperties(prefix = "wx.pay") public class WxPayProperties { private List<PayConfig> configs; @Data public static class PayConfig { private String mchId; private String mchSerialNo; private String wxApiV3Key; private String notifyUrl; private String keyPath; @PostConstruct public void validate() { if (StringUtils.isAnyBlank(mchId, mchSerialNo, wxApiV3Key)) { throw new IllegalArgumentException("微信支付配置不完整"); } } } }3.2 动态配置中心实现
接下来创建核心配置类,负责初始化WxJava的服务实例:
@Slf4j @Configuration @EnableConfigurationProperties({WxMaProperties.class, WxPayProperties.class}) public class WxConfigCenter { private final WxMaProperties maProperties; private final WxPayProperties payProperties; public WxConfigCenter(WxMaProperties maProperties, WxPayProperties payProperties) { this.maProperties = maProperties; this.payProperties = payProperties; } @Bean public WxMaService wxMaService() { if (CollectionUtils.isEmpty(maProperties.getConfigs())) { throw new IllegalStateException("未配置微信小程序参数"); } WxMaServiceImpl service = new WxMaServiceImpl(); Map<String, WxMaConfig> configMap = maProperties.getConfigs() .stream() .map(config -> { WxMaDefaultConfigImpl cfg = new WxMaDefaultConfigImpl(); cfg.setAppid(config.getAppid()); cfg.setSecret(config.getSecret()); cfg.setToken(config.getToken()); cfg.setAesKey(config.getAesKey()); cfg.setMsgDataFormat(config.getMsgDataFormat()); return cfg; }) .collect(Collectors.toMap(WxMaDefaultConfigImpl::getAppid, Function.identity())); service.setMultiConfigs(configMap); return service; } @Bean public WxPayService wxPayService() throws Exception { if (CollectionUtils.isEmpty(payProperties.getConfigs())) { throw new IllegalStateException("未配置微信支付参数"); } WxPayServiceImpl payService = new WxPayServiceImpl(); for (WxPayProperties.PayConfig config : payProperties.getConfigs()) { WxPayConfig payConfig = new WxPayConfig(); payConfig.setMchId(config.getMchId()); payConfig.setMchSerialNo(config.getMchSerialNo()); payConfig.setApiV3Key(config.getWxApiV3Key()); payConfig.setNotifyUrl(config.getNotifyUrl()); // 加载商户证书 Resource resource = new ClassPathResource(config.getKeyPath()); try (InputStream is = resource.getInputStream()) { payConfig.setPrivateKeyContent(IOUtils.toByteArray(is)); } payService.addConfig(config.getMchId(), payConfig); } return payService; } }4. 业务层实现
4.1 小程序登录服务
实现一个支持多小程序的登录服务:
@Service @RequiredArgsConstructor public class WxAuthService { private final WxMaService wxMaService; public WxMaJscode2SessionResult login(String appId, String code) { if (!wxMaService.switchover(appId)) { throw new IllegalArgumentException("无效的小程序ID: " + appId); } try { WxMaJscode2SessionResult session = wxMaService.getUserService() .getSessionInfo(code); // 这里可以添加自己的业务逻辑,比如用户注册/登录 processUserLogin(appId, session.getOpenid(), session.getUnionid()); return session; } catch (WxErrorException e) { throw new RuntimeException("微信登录失败: " + e.getError().getErrorMsg()); } } private void processUserLogin(String appId, String openid, String unionid) { // 实现你的用户处理逻辑 } }4.2 支付服务实现
实现支持多商户的支付服务:
@Service @RequiredArgsConstructor public class WxPaymentService { private final WxPayService wxPayService; public Map<String, String> createPayment(String mchId, PaymentRequest request) { if (!wxPayService.switchover(mchId)) { throw new IllegalArgumentException("无效的商户ID: " + mchId); } try { WxPayUnifiedOrderV3Request wxRequest = new WxPayUnifiedOrderV3Request(); wxRequest.setOutTradeNo(request.getOrderNo()); wxRequest.setDescription(request.getDescription()); // 金额设置(单位:分) WxPayUnifiedOrderV3Request.Amount amount = new WxPayUnifiedOrderV3Request.Amount(); amount.setTotal(request.getTotalFee()); wxRequest.setAmount(amount); // 根据支付类型调用不同接口 switch (request.getTradeType()) { case "JSAPI": wxRequest.setPayer(new WxPayUnifiedOrderV3Request.Payer() .setOpenid(request.getOpenid())); WxPayUnifiedOrderV3Result.JsapiResult result = wxPayService.createOrderV3(TradeTypeEnum.JSAPI, wxRequest); return buildJsapiParams(result.getPrepayId(), mchId); case "NATIVE": WxPayUnifiedOrderV3Result.NativeResult nativeResult = wxPayService.createOrderV3(TradeTypeEnum.NATIVE, wxRequest); return Collections.singletonMap("codeUrl", nativeResult.getCodeUrl()); default: throw new IllegalArgumentException("不支持的支付类型"); } } catch (WxPayException e) { throw new RuntimeException("支付创建失败: " + e.getReturnMsg()); } } private Map<String, String> buildJsapiParams(String prepayId, String mchId) { Map<String, String> params = new HashMap<>(); params.put("appId", wxPayService.getConfig().getAppId()); params.put("timeStamp", String.valueOf(System.currentTimeMillis() / 1000)); params.put("nonceStr", WxPayUtil.generateNonceStr()); params.put("package", "prepay_id=" + prepayId); params.put("signType", "RSA"); try { String sign = wxPayService.sign(params, SignType.RSA); params.put("paySign", sign); return params; } catch (WxPayException e) { throw new RuntimeException("签名生成失败", e); } } }5. 最佳实践与注意事项
5.1 配置管理建议
敏感信息保护:不要将证书和密钥直接放在代码中,可以通过以下方式加强安全:
- 使用配置中心(如Nacos、Apollo)
- 使用Kubernetes Secrets或Vault
- 对配置文件中的敏感字段加密
配置验证:在启动时验证配置的完整性,避免运行时出错:
@Bean public CommandLineRunner validateConfig(WxMaService maService, WxPayService payService) { return args -> { log.info("已加载 {} 个小程序配置", maService.getMultiConfigs().size()); log.info("已加载 {} 个支付商户配置", payService.getMultiConfigMap().size()); }; }5.2 性能优化
- 连接池配置:WxJava底层使用OkHttp,可以通过以下方式优化:
@Bean public WxMaService wxMaService() { // ... 原有配置代码 // 配置HTTP连接池 OkHttpClient.Builder builder = new OkHttpClient.Builder() .connectTimeout(5, TimeUnit.SECONDS) .readTimeout(10, TimeUnit.SECONDS) .writeTimeout(10, TimeUnit.SECONDS) .connectionPool(new ConnectionPool(10, 5, TimeUnit.MINUTES)); service.setRequestHttpClientBuilder(builder); return service; }- 缓存策略:对于access_token等凭证,WxJava已经内置了缓存机制,默认使用内存缓存。对于分布式系统,可以自定义Redis存储:
@Bean public WxMaRedisConfigImpl wxMaRedisConfig(RedisTemplate<String, String> redisTemplate) { return new WxMaRedisConfigImpl(redisTemplate) { @Override public String getAccessToken(String appId) { // 自定义获取逻辑 } @Override public void updateAccessToken(String appId, String accessToken, int expiresIn) { // 自定义更新逻辑 } }; }6. 常见问题排查
在实际项目中,我们遇到过几个典型问题:
证书加载失败:确保证书文件路径正确,且文件没有被IDE过滤。遇到过因为.idea配置导致证书文件没有被打包的情况。
商户号切换无效:检查是否每次支付请求前都正确调用了switchover方法。我们曾因为线程池异步调用导致上下文切换丢失。
签名错误:V3接口使用RSA签名,确保:
- 使用正确的APIv3密钥
- 时间戳和随机字符串每次请求都重新生成
- 签名参数按照字典序排序
跨商户支付问题:不同商户号不能混用,确保订单、回调、查询都使用同一商户号。我们曾因为缓存导致查询使用了错误商户号。