我的第一个开源项目: UnionPay​​ 聚合扫码支付

【投稿赢 iPhone 17】「我的第一个开源项目」故事征集:用代码换C位出道! 10w+人浏览 58人参与

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需要以下基础环境:

组件版本要求说明
Java8+Spring Boot 2.4.5兼容Java 8及以上版本
MySQL5.7+主从数据库集群,用于存储订单和交易信息
ActiveMQ4.9+消息中间件,用于处理支付回调和异步通知
Redis5.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. 部署步骤
  1. 配置数据库:准备MySQL主从数据库集群,执行初始化SQL脚本
  2. 部署ActiveMQ:配置指南部署ActiveMQ
  3. 配置支付渠道:根据各支付平台文档配置商户ID、密钥和证书
  4. 启动服务:通过Tomcat方法启动UnionPay服务

六、创作历程中的挑战与帮助

1. 开发过程中的技术挑战

在UnionPay的开发过程中,我遇到了诸多技术挑战:

多数据源兼容性问题:微信支付V3接口采用RSA签名和AES-256加密,而云闪付则要求定期更新加密公钥,支付宝则需要验证签名和敏感信息加密。如何将这些差异统一为单一的数据源管理架构,一度让我陷入困境。

MQ事务消息实现:在支付回调场景中,ActiveMQ的提交机制需要精确控制事务状态。我曾遇到网络波动导致commit消息丢失的情况,经过多次调试和优化,最终通过增加重试机制和超时控制解决了这一问题。

读写分离延迟问题:在高并发场景下,从库的数据同步延迟可能导致支付状态查询结果不一致。我通过引入缓存机制和实时状态更新接口,将这一问题的影响降至最低。

2. 社区协作与开源精神

开源项目的成功离不开社区的支持。UnionPay通过以下方式践行开源精神:

  • 文档开放性:提供完整的API文档和部署指南,降低使用门槛
  • 许可证选择:采用MIT许可证,鼓励广泛使用和二次开发
  • 社区互动:通过GitCode Issues和Pull Requests机制鼓励开发者参与改进 

在开发过程中,我曾收到用户关于支付渠道异常的反馈,这让我意识到开源项目不仅是代码的分享,更是责任的承担。面对这些挑战,我坚持每天更新代码,不断优化系统性能,最终打造出了一个稳定、安全、易用的聚合支付系统。

六、结语:开源之路的不易与价值

UnionPay的开发历程充满了艰辛与收获。从最初的灵感到最终的开源发布,我经历了无数个深夜的调试、多次的技术方案重构,以及对支付安全性的不断审视与提升。

开源不仅是一种技术分享,更是一种责任和承诺

在未来的日子里,我将继续完善UnionPay的功能,优化其性能,并探索更多支付场景的应用。同时,我也期待更多的开发者加入UnionPay的开源项目,共同打造一个更加安全、便捷、高效的聚合支付系统。

开源是一场没有终点的旅程,而UnionPay只是我在这条路上迈出的第一步。希望我的经验能够激励更多的开发者投身开源事业,为技术社区贡献自己的力量。

​项目地址​​:GitCode - 全球开发者的开源社区,开源代码托管平台

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

喂完待续

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值