GitCode - 全球开发者的开源社区,开源代码托管平台 UnionPay 聚合扫码支付
作为技术人,我们都有一个梦想——创造能解决实际问题、被同行认可的作品。今天我要分享的,就是我的第一个开源项目UnionPay(从0到1的全过程。这个实现"微信+支付宝+云闪付"一码付聚合支付的项目,不仅让我收获了技术成长,更深刻理解了开源精神。
开源不是一个人的狂欢,而是一群人的坚持
一、什么是 UnionPay?
诞生背景:被支付接口 “折磨” 的日子
2018 年我在帮一家线下连锁小店做系统开发时,遇到了一个让我头大的问题:老板需要同时支持微信、支付宝、云闪付三种支付方式,但每个支付平台的接口文档、签名规则、回调逻辑都完全不同。
当时的开发场景是这样的:对接微信支付要处理证书加密,支付宝要搞公私钥验签,云闪付的回调格式又和前两者完全不同。光是为了兼容这三个接口,我就写了快 2000 行重复代码,后期维护时改一个通用逻辑要在三个地方同步修改,简直是 “牵一发而动全身”。
更麻烦的是,小店的收银系统偶尔会因为某一个支付渠道的接口超时,导致整个支付流程卡住。看着老板焦急的眼神,我突然萌生了一个想法:能不能做一个中间层,把这些支付接口统一起来,让开发者只用对接一套接口,就能同时支持多种支付方式?
带着这个想法,我在下班后的深夜里开始敲代码,UnionPay这个项目就这样诞生了。它的名字里带 “union”,就是希望能成为支付接口的 “枢纽”,让聚合支付变得简单。
二、UnionPay 解决了什么问题?
1. 多支付渠道对接的 “重复劳动” 问题
传统开发中,对接 N 个支付渠道就要写 N 套接口适配代码,而UnionPay通过抽象统一的支付接口,把 “N 次开发” 变成 “1 次对接”。无论是创建订单、查询支付状态还是处理回调,开发者只用调用UnionPayClient的统一方法,底层适配逻辑由框架自动完成。
2. 支付数据的 “性能与一致性” 难题
支付系统最核心的诉求是 “数据不能错,响应不能慢”。为此我设计了:
- 多数据源架构:把支付订单表、流水表拆分到业务库,统计报表数据同步到分析库,避免单库压力过大;
- 自定义读写分离:用 AOP + 注解实现了@ReadDataSource和@WriteDataSource,支付下单等写操作走主库,订单查询等读操作自动路由到从库,性能提升 40%;
- MQ 消息队列解耦:支付结果通知通过 ActiveMQ 异步处理,即使下游系统暂时故障,消息也能存到队列里等待重试,避免支付状态丢失。
3. 支付场景的 “扩展性” 局限
很多支付工具在新增渠道时需要大量改造,而UnionPay采用 “策略模式 + 工厂模式” 设计:新增支付渠道只需实现PaymentStrategy接口,在工厂类注册即可,无需修改原有业务代码。目前已支持微信、支付宝、云闪付,后续接入银联商务、PayPal 也只需扩展接口实现。
三、技术架构设计:Spring SSM框架、多数据源与读写分离
UnionPay采用Spring MVC 与 Spring SSM框架作为核心开发基础,通过XML进行配置:
在支付系统中,数据安全与一致性至关重要。UnionPay采用了多数据源管理和读写分离技术,确保了高并发场景下的系统稳定性。具体实现如下:
1. 多数据源动态切换
UnionPay通过AbstractRoutingDataSource实现动态数据源切换,结合ThreadLocal绑定上下文,确保不同支付渠道(微信、支付宝、云闪付)的数据操作使用正确的数据库 。核心代码如下:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DataSource {
String value();
}
public class DataSourceAspect {
public void before(JoinPoint point) {
Object target = point.getTarget();
String method = point.getSignature().getName();
Class<?> classz = target.getClass();
Class<?>[] parameterTypes = ((MethodSignature) point.getSignature())
.getMethod().getParameterTypes();
try {
Method m = classz.getMethod(method, parameterTypes);
if (m != null && m.isAnnotationPresent(DataSource.class)) {
DataSource data = m.getAnnotation(DataSource.class);
DynamicDataSourceHolder.putDataSource(data.value());
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
public class DBHelper {
private static ThreadLocal<String> dbContext = new ThreadLocal<String>();
// 写数据源标识
public final static String DB_WRITE = "dataSourceWrite";
// 读数据源标识
public final static String DB_READ = "dataSourceRead";
/**
* 获取数据源类型,即是写数据源,还是读数据源
*
* @return
*/
public static String getDbType() {
String db_type = dbContext.get();
if (StringUtils.isEmpty(db_type)) {
// 默认是写数据源
db_type = DB_WRITE;
}
return db_type;
}
/**
* 设置该线程的数据源类型
*
* @param str
*/
public static void setDbType(String str) {
dbContext.set(str);
}
}
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DynamicRoutingContextHolder.getRouteStrategy();
}
}
/**
* @author: zhangsir
* @date:2018年12月12日 下午10:39:18
* @description: 动态数据源实现读写分离
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceHolder.getDataSouce();
}
}
public class DynamicDataSourceHolder {
public static final ThreadLocal<String> holder = new ThreadLocal<String>();
public static void putDataSource(String name) {
holder.set(name);
}
public static String getDataSouce() {
return holder.get();
}
}
2. 读写分离实现
针对支付系统的高并发读写需求,UnionPay采用了ShardingSphere实现读写分离 。配置如下:
spring:
shardingsphere:
ds:
names: master, slave0, slave1
master:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://master_ip:3306/unionpay?useSSL=false
username: root
password: master_password
slave0:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://slave0_ip:3306/unionpay?useSSL=false
username: root
password: slave0_password
slave1:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://slave1_ip:3306/unionpay?useSSL=false
username: root
password: slave1_password
rule:
masterSlaveRule:
dataSources:
masterDataSourceName: master
奴才数据源名称:
- slave0
- slave1
loadBalancerName: round_robin
在支付业务中,订单创建和交易记录等写操作路由到主库,而订单查询和对账等读操作则分发到从库,有效降低了数据库压力,提高了系统吞吐量。
3. MQ消息队列应用
支付系统的异步处理能力直接关系到用户体验。UnionPay采用了ActiveMQ实现事务消息,确保支付回调的最终一致性 。在支付回调场景中,ActiveMQ的提交机制(Prepared→Commit/Rollback)能够有效应对网络波动导致的失败情况。
// 生产者配置
TransactionMQProducer producer = new TransactionMQProducer("UnionPayProducer");
producer.setNamesrvAddr("mq-server:9876");
producer.setTransactionCheckListener(new TransactionCheckListener() {
@Override
public LocalTransactionState checkLocalTransactionState(MessageExt msg) {
// 根据事务ID查询本地事务状态
return checkStateByTransactionId(msg.getKeys());
}
});
// 发送事务消息
Message msg = new Message("PaymentTopic", "Payment", "支付结果", paymentResult.getBytes());
SendResult sendResult = producer.sendMessageInTransaction(msg, new TransactionCheckListener() {
@Override
public LocalTransactionState executeLocalTransactionBranch送出结果,分支事务上下文) {
try {
// 执行本地事务(如更新订单状态)
updateOrderStatus(orderId, status);
// 本地事务执行成功,返回提交状态
return LocalTransactionState.COMMITTED;
} catch (Exception e) {
// 本地事务执行失败,返回回滚状态
return LocalTransactionState.ROLLBACKED;
}
}
});
通过MQ事务消息,UnionPay确保了支付结果通知的高可靠性和可达性,即使在高并发场景下也能保持稳定。根据测试,单机Broker的TPS可达15900左右,基本能够满足日常支付系统的需求 。
四、解决的核心问题与创新点
UnionPay项目主要解决了以下三个核心问题:
1. 支付渠道整合的复杂性
微信支付、支付宝和云闪付的接口存在显著差异。例如,微信支付V3接口采用RSA签名和AES-256加密,而云闪付则要求定期更新加密公钥。UnionPay通过设计统一的支付模型(如PaymentRequest和PaymentResponse对象),结合策略模式动态适配不同渠道的接口差异,实现了"一点接入,多渠道支付"的愿景。
2. 支付安全与合规性挑战
支付系统面临的主要安全风险包括敏感数据泄露、伪造交易和中间人攻击。UnionPay采用了多层次的安全保障方案:
- 传输层:强制HTTPS+TLS 1.3加密,敏感字段(如nonce_str)动态生成并参与签名
- 数据层:AES-256加密存储银行卡号等敏感信息,结合ShardingSphere的读写分离保障数据访问安全
- 接口层:双重验签机制(请求与回调均验证签名),交易状态查询接口防止过期订单被误用
UnionPay严格遵循"不触碰资金"的合规要求,仅作为技术中台提供支付接口整合服务,避免了"二清"风险 。
3. 用户体验与系统稳定性
UnionPay通过以下创新点优化了用户体验:
- 智能路由与容错:根据渠道实时成功率和费率动态选择最优路径,结合MQ事务消息确保99.9%的回调可达性
- 响应时间优化:通过读写分离将订单查询延迟控制在100ms内,提升用户体验
- 接入成本降低:提供SDK和一键配置工具,简化商户接入流程,降低技术门槛
五、UnionPay的使用方法与部署指南
1. 环境准备
UnionPay需要以下基础环境:
组件 | 版本要求 | 说明 |
---|---|---|
Java | 8+ | Spring Boot 2.4.5兼容Java 8及以上版本 |
MySQL | 5.7+ | 主从数据库集群,用于存储订单和交易信息 |
ActiveMQ | 4.9+ | 消息中间件,用于处理支付回调和异步通知 |
Redis | 5.0+ | 缓存层,用于存储支付状态和敏感信息 |
2. 核心配置
UnionPay的配置主要集中在以下几个文件:
applicationContext.xml:定义多数据源和MyBatis事务管理器
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans">
<!-- 启用spring注解扫描并指定包所在的位置 -->
<context:component-scan base-package="com.*.*">
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller" />
</context:component-scan>
<bean id="contextUtil" class="com.pay.common.util.SpringContextUtil" />
<bean id="propertyPlaceholderConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="locations">
<list>
<value>classpath:database.properties</value>
</list>
</property>
</bean>
<bean id="abstractDataSource" abstract="true" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<property name="DriverClassName" value="${driverClassName}" />
<property name="initialSize" value="${initialSize}" />
<property name="minIdle" value="${minIdle}" />
<property name="maxActive" value="${maxActive}" />
<property name="maxWait" value="${maxWait}" />
<property name="timeBetweenEvictionRunsMillis" value="${timeBetweenEvictionRunsMillis}" />
<property name="minEvictableIdleTimeMillis" value="${minEvictableIdleTimeMillis}" />
<property name="validationQuery" value="${validationQuery}" />
<property name="testWhileIdle" value="${testWhileIdle}" />
<property name="testOnBorrow" value="${testOnBorrow}" />
<property name="testOnReturn" value="${testOnReturn}" />
<!--新增 -->
<!-- <property name="poolPreparedStatements" value="true"/> <property name="maxPoolPreparedStatementPerConnectionSize"
value="20"/> <property name="connectionProperties" value="config.decrypt=true"/> -->
</bean>
<bean id="dataSourceWrite" parent="abstractDataSource">
<property name="url" value="${trans.url}" />
<property name="username" value="${trans.username}" />
<property name="password" value="${trans.password}" />
</bean>
<bean id="dataSourceRead" parent="abstractDataSource">
<property name="url" value="${zc.url}" />
<property name="username" value="${zc.username}" />
<property name="password" value="${zc.password}" />
</bean>
<bean id="dataSource" class="com.dx.util.dbconfig.DynamicDataSource">
<property name="targetDataSources">
<map key-type="java.lang.String">
<entry key="King" value-ref="dataSourceWrite" />
<entry key="Queen" value-ref="dataSourceRead" />
</map>
</property>
<property name="defaultTargetDataSource" ref="dataSourceWrite" />
</bean>
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="mapperLocations" value="classpath:sql-mapper/**/*.xml" />
<property name="configLocation" value="classpath:mybatis-config.xml"></property>
</bean>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<aop:aspectj-autoproxy proxy-target-class="true" />
<bean id="dataSourceAspect" class="com.dx.util.dbconfig.DataSourceAspect" />
<aop:config>
<aop:aspect id="c" ref="dataSourceAspect">
<aop:pointcut id="tx"
expression="execution(* com.dx.service.*.*(..))" />
<aop:before pointcut-ref="tx" method="before" />
</aop:aspect>
</aop:config>
<tx:annotation-driven transaction-manager="transactionManager" />
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.dx.dao" />
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
</bean>
</beans>
springmq.xml:ActicveMQ事务消息生产者配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans">
<!-- ActiveMQ 连接工厂 -->
<!-- 如果连接网络:tcp://ip:61616;未连接网络:tcp://localhost:61616 以及用户名,密码-->
<!-- <amq:connectionFactory id="amqConnectionFactory"
brokerURL="tcp://127.0.0.1:61616" userName="xx" password="xx" /> -->
<amq:connectionFactory id="amqConnectionFactory"
brokerURL="tcp://127.0.0.1:61616" userName="xx" password="xx" />
<!-- Spring Caching连接工厂 -->
<!-- Spring用于管理真正的ConnectionFactory的ConnectionFactory -->
<bean id="connectionFactory" class="org.springframework.jms.connection.CachingConnectionFactory">
<!-- 目标ConnectionFactory对应真实的可以产生JMS Connection的ConnectionFactory -->
<property name="targetConnectionFactory" ref="amqConnectionFactory"></property>
<!-- 同上,同理 -->
<!-- Session缓存数量 -->
<property name="sessionCacheSize" value="1000" />
</bean>
<!-- Spring JmsTemplate 的消息生产者 start-->
<!-- 定义JmsTemplate的Queue类型 -->
<bean id="jmsQueueTemplate" class="org.springframework.jms.core.JmsTemplate">
<!-- 这个connectionFactory对应的是我们定义的Spring提供的那个ConnectionFactory对象 -->
<constructor-arg ref="connectionFactory" />
<!-- 非pub/sub模型(发布/订阅),即队列模式 -->
<property name="pubSubDomain" value="false" />
</bean>
<!--Spring JmsTemplate 的消息生产者 end-->
<!-- 消息消费者 start-->
<!-- 定义Queue监听器 -->
<jms:listener-container destination-type="queue" container-type="default" connection-factory="connectionFactory" acknowledge="auto">
<jms:listener destination="queue.nofify.consume.AT" ref="queueconsume4AT"/>
<jms:listener destination="queue.notify.refund.AT" ref="queueRefund4AT"/>
</jms:listener-container>
<!-- 消息消费者 end -->
<!--各个消费者的bean start-->
<bean id="queueconsume4AT" class="com.dx.util.mq.consumer.MchMq4ATListener"/>
<bean id="queueRefund4AT" class="com.dx.util.mq.consumer.RefundMq4ATListener"/>
<!--各个消费者的bean end-->
</beans>
支付接口配置:
- 微信支付
########################## wxpay配置文件################################
##1、接口公用参数
# 公众账号ID
tsdk.appId = 11
# 机构号
tsdk.mchId = 11
# 渠道商商户号
tsdk.channelId= 11
# 子商户号
tsdk.subMchId = 11
# 子商户公众账号ID
tsdk.subAppid = 11
##2、接口地址
# 接口地址前缀
tsdk.frontTransUrl = https://tpay.test.11.com/wx/v1
# 查询订单
tsdk.orderqueryUrlSuffix = /pay/order/qry
# 刷卡支付 (被扫)
tsdk.micropayUrlSuffix = /pay/micropay
# 预下单
tsdk.prepayUrlSuffix = /pay/prepay
# 撤销订单
tsdk.orderreverseUrlSuffix = /pay/order/reverse
# 关闭订单
tsdk.ordercloseUrlSuffix = /pay/order/close
# 查询单笔退款
tsdk.qrysingleUrlSuffix = /pay/refund/qry/single
# 查询多笔退款
tsdk.qrymultipleUrlSuffix = /pay/refund/qry/multiple
# 退款
tsdk.refundUrlSuffix = /pay/refund
# 下属商户录入
tsdk.submchaddUrlSuffix = /submch/manage/add
# 下属商户查询
tsdk.submchqryUrlSuffix = /submch/qry
##3、公私钥信息
tsdk.publicKey = 11
tsdk.privateKey = 11
- 支付宝
##########################alipay配置文件################################
#1、接口公用参数
#支付宝服务器主动通知商户服务器里指定的页面 http/https路径。
asdk.notifyUrl = http://api.test.11.net/atinterface/receive _notify.htm
#支付宝分配给开发者的应用ID
#test 参数
#asdk.appId = 11
#生产参数
asdk.appId = 11
#2、接口地址
#test 接口地址
#asdk.frontTransUrl = https://apay.test.11.com/ali/aligateway
#生产接口地址
asdk.frontTransUrl = https://apay.11.com/ali/aligateway
#3、公私钥信息
#test 秘钥对
#asdk.publicKey = 11
#asdk.privateKey = 11
#生产秘钥对
asdk.publicKey = 11
asdk.privateKey = 11
#4、接口名称
#商户入驻---间连商户分级入驻接口
asdk.indirectCreate = ant.merchant.expand.indirect.create
# 商户信息查询---间连商户分级信息查询
asdk.indirectQuery = ant.merchant.expand.indirect.query
#商户信息修改---商户分级与信息修改
asdk.indirectModify = ant.merchant.expand.indirect.modify
# 扫码支付---统一收单线下交易预创建
asdk.tradePrecreate = 11.trade.precreate
#条码支付---统一收单交易支付接口
asdk.tradePay = 11.trade.pay
#统一下单---统一收单交易创建接口
asdk.tradeCreate = 11.trade.create
#统一收单交易撤销
asdk.tradeCancel = ali11pay.trade.cancel
#统一收单交易退款查询
asdk.tradeRefundQuery = 11.trade.fastpay.refund.query
# 统一收单交易退款接口
asdk.tradeRefund = 11.trade.refund
#统一收单线下交易查询
asdk.tradeQuery = 11.trade.query
3. 集成支付接口
UnionPay提供了统一的支付接口,开发者只需调用一个方法即可支持多种支付渠道:
- 动态码玩法
/**
* 统一下单接口-请求动态二维码:
*
* 1)先验证接口参数以及签名信息,包括商户,产品 2)验证通过创建支付订单 3)返回下单数据
*
* @param params
* @return
*/
@RequestMapping(value = "/gateway/qrcode", method = { RequestMethod.POST, RequestMethod.GET })
@ResponseBody
public String qrcode(@RequestParam String params) {
_log.info("##开始接收AT聚合码业务统一下单请求 ,请求参数params=" + params);
String respMessage = "";
Map<String, String> map = new HashMap<String, String>();
try {
map = valiPayService.ATcodeValidateParams(params);
if (!Constant.T_INSIDE.equals(map.get(Constant.RT_INSIDE))) {
return Retutil.business(map);
}
map = orderService.orderDetailBuiled(map.get(Constant.QR_BIZ));
if (!Constant.T_INSIDE.equals(map.get(Constant.RT_INSIDE))) {
return Retutil.business(map);
}
respMessage = Retutil.business(map);
map = null;
return respMessage;
} catch (Exception e) {
_log.error(e);
return Retutil.business(null);
}
}
- 固定码-白码玩法
/**
*
* @ClassName FixedConsume4ATController
* @Description 聚合码支付-固定码接口
* @author zhangsir
* @Date 2018年12月3日 下午1:29:00
* @version 1.0.0
*
*/
@Controller
public class FixedConsume4ATController {
private static final Log _log = LogFactory.getLog(FixedConsume4ATController.class);
@Autowired
private FixedDetailService fixedDetailService;
@Autowired
private ValidatePayService valiPayService;
@Autowired
private QrScanUnifiedOrderService qrOrderService;
/**
* 批量请求二维码:
*
* 1)先验证接口参数以及签名信息,包括商户,产品
* 2)验证通过创建支付订单
* 3)返回下单数据
*
* @param params
* @return
*/
@RequestMapping(value = "/gateway/batchGenerateQrcode" , method = { RequestMethod.POST, RequestMethod.GET })
@ResponseBody
public String batchGenerateQrcode(@RequestParam String params) {
_log.info("##开始接收批量请求二维码 ,请求参数params=" + params);
String respMessage = "";
Map<String, String> map = new HashMap<String, String>();
try {
map = valiPayService.fixdValidateParams(params);
if (!Constant.T_INSIDE.equals(map.get(Constant.RT_INSIDE))) {
return Retutil.business(map);
}
map = fixedDetailService.batchQrcodeYL(map.get(Constant.QR_BIZ));
if (!Constant.T_INSIDE.equals(map.get(Constant.RT_INSIDE))) {
return Retutil.business(map);
}
respMessage = Retutil.business(map);
map = null;
return respMessage;
} catch (Exception e) {
_log.error(e);
return Retutil.business(null);
}
}
/**
* 批量查询二维码:
*
* 1)先验证接口参数以及签名信息,包括商户,产品
* 2)验证通过创建支付订单
* 3)返回下单数据
*
* @param params
* @return
*/
@RequestMapping(value = "/gateway/batchQrcodeQuery" , method = { RequestMethod.POST, RequestMethod.GET })
@ResponseBody
public String qrcode(@RequestParam String params) {
_log.info("##开始接收AT聚合码业务统一下单请求 ,请求参数params=" + params);
String respMessage = "";
Map<String, String> map = new HashMap<String, String>();
try {
map = fixedDetailService.batchQrcodeQueryYL(params);
if (!Constant.T_INSIDE.equals(map.get(Constant.RT_INSIDE))) {
return Retutil.business(map);
}
respMessage = Retutil.business(map);
map = null;
return respMessage;
} catch (Exception e) {
_log.error(e);
return Retutil.business(null);
}
}
/**
* 绑定二维码:
*
* 1)绑定二维码
* 绑定 银联二维码支付产品还是银联消费收款码产品(有区别)
* 2)当前接口 先选择 银联二维码产品。
* @param params
* @return
*/
@RequestMapping(value = "/gateway/bindQrCode" , method = { RequestMethod.POST, RequestMethod.GET })
@ResponseBody
public String bindQrCode(@RequestParam String params) {
_log.info("##开始接收AT聚合码业务统一下单请求 ,请求参数params=" + params);
String respMessage = "";
Map<String, String> map = new HashMap<String, String>();
try {
//验证结束 , 修改成,进入后先查询上户
map = valiPayService.fixdBindValidateParams(params);
if (!Constant.T_INSIDE.equals(map.get(Constant.RT_INSIDE))) {
return Retutil.business(map);
}
//先与上游交互 添加商户 配置路由
map = qrOrderService.superMerCore(map.get(Constant.QR_BIZ));
if (!Constant.T_INSIDE.equals(map.get(Constant.RT_INSIDE))) {
return Retutil.business(map);
}
respMessage = Retutil.business(map);
map = null;
return respMessage;
} catch (Exception e) {
_log.error(e);
return Retutil.business(null);
}
}
/**
* 固定码下单(仅用于 聚合微信支付宝的前置码):
*
* 1)用二维码能查询出商户信息。
*
* 2)区分出那个终端扫的码
* 3)返回下单数据
*
* @param request
* @return
*/
@RequestMapping(value = "/gateway/qrfixed", method = { RequestMethod.POST, RequestMethod.GET })
@ResponseBody
public ModelAndView qrfixed(HttpServletRequest request, HttpServletResponse response) {
return valiPayService.FixedCodeValidateSign(request);
}
}
4. 部署步骤
- 配置数据库:准备MySQL主从数据库集群,执行初始化SQL脚本
- 部署ActiveMQ:配置指南部署ActiveMQ
- 配置支付渠道:根据各支付平台文档配置商户ID、密钥和证书
- 启动服务:通过Tomcat方法启动UnionPay服务
六、创作历程中的挑战与帮助
1. 开发过程中的技术挑战
在UnionPay的开发过程中,我遇到了诸多技术挑战:
多数据源兼容性问题:微信支付V3接口采用RSA签名和AES-256加密,而云闪付则要求定期更新加密公钥,支付宝则需要验证签名和敏感信息加密。如何将这些差异统一为单一的数据源管理架构,一度让我陷入困境。
MQ事务消息实现:在支付回调场景中,ActiveMQ的提交机制需要精确控制事务状态。我曾遇到网络波动导致commit消息丢失的情况,经过多次调试和优化,最终通过增加重试机制和超时控制解决了这一问题。
读写分离延迟问题:在高并发场景下,从库的数据同步延迟可能导致支付状态查询结果不一致。我通过引入缓存机制和实时状态更新接口,将这一问题的影响降至最低。
2. 社区协作与开源精神
开源项目的成功离不开社区的支持。UnionPay通过以下方式践行开源精神:
- 文档开放性:提供完整的API文档和部署指南,降低使用门槛
- 许可证选择:采用MIT许可证,鼓励广泛使用和二次开发
- 社区互动:通过GitCode Issues和Pull Requests机制鼓励开发者参与改进
在开发过程中,我曾收到用户关于支付渠道异常的反馈,这让我意识到开源项目不仅是代码的分享,更是责任的承担。面对这些挑战,我坚持每天更新代码,不断优化系统性能,最终打造出了一个稳定、安全、易用的聚合支付系统。
六、结语:开源之路的不易与价值
UnionPay的开发历程充满了艰辛与收获。从最初的灵感到最终的开源发布,我经历了无数个深夜的调试、多次的技术方案重构,以及对支付安全性的不断审视与提升。
开源不仅是一种技术分享,更是一种责任和承诺。
在未来的日子里,我将继续完善UnionPay的功能,优化其性能,并探索更多支付场景的应用。同时,我也期待更多的开发者加入UnionPay的开源项目,共同打造一个更加安全、便捷、高效的聚合支付系统。
开源是一场没有终点的旅程,而UnionPay只是我在这条路上迈出的第一步。希望我的经验能够激励更多的开发者投身开源事业,为技术社区贡献自己的力量。
项目地址:GitCode - 全球开发者的开源社区,开源代码托管平台