
简介面向移动支付开发者的一份云闪付tn转paydata可运行源码包聚焦苹果与安卓双端的支付数据格式转换。在云闪付交易流程中tn代表交易编号paydata是承载支付信息的核心数据结构两者能否正确转换直接关系到支付流程能否顺利发起因此常是开发者在对接云闪付时优先需要攻克的环节。苹果端实现采用3DES对称加密算法通过密钥将交易编号加密为支付数据安卓端则使用base64编码方式完成转换资源内部提供了对应的调用示例、paydata样例和在线测试入口便于开发者在真实环境先行验证再迁移至自身项目。包体共3个文件以工程配置文件、HTML页面和版本控制辅助文件为主其中HTML页面可用于直接查看运行效果配置文件记录基础工程环境整体仅4KB结构精简适合作为轻量集成模块或功能验证起点。目前已有502人学习浏览对于正在对接云闪付、希望缩短支付数据转换调试周期的研发人员这份源码包提供了清晰的双端实现参照可以直接运行或在此基础上二次开发降低重复开发与排错成本。 做支付系统对接的兄弟应该都有体会支付这东西表面上看就是一个下单、回调、查单的闭环真正联调起来全是细节。尤其云闪付这种老牌渠道商户系统这边拿到tn之后下游的paydata中台要的却是另一套标准报文中间没个转换层字段和格式就够你折腾半天。最近我正好把一套“云闪付tn转paydata”的可运行源码整理出来了从下单拿tn、落库、转标准报文到签名提交整条链路都是通的。今天就把这套工程怎么设计的、核心代码怎么写的、联调时踩了哪些坑一次说清楚。这套方案解决的是一个非常具体的业务问题收银台发起云闪付支付后后端从云闪付下单接口拿到tn这个tn是一个单纯的交易凭证号它本身的字段体系跟内部支付数据中台的统一规范对不上需要把tn以及对应的订单信息重新组装成paydata要求的报文格式再提交。适合正在做云闪付接入、或者准备搭建多渠道支付数据归集系统的团队参考哪怕你只是刚接触支付对接看完也能少走不少弯路。1. 先把业务链路捋清楚云闪付tn从哪来paydata要什么动手写代码之前我习惯先把链路拆明白。这个项目里其实只有两端一端是云闪付一端是paydata。搞清楚这两端各自的特点转换逻辑才能写得顺。1.1 云闪付tn是什么它解决什么问题tn的全称是Transaction Number翻译过来就是交易凭证号。你调云闪付的下单接口下单成功之后它返回给你的一串数字就是tn。可以把它理解成电影院的取票码你拿着这个码才能在云闪付那边唤起对应的那笔订单完成支付。关键点在于tn本身是不带订单明细的。订单号、金额、商品描述这些信息在下单那一刻就已经和tn绑定了绑定关系在云闪付侧和商户侧各存一份。所以商户系统拿到tn之后第一件事就是自己把tn和商户订单号的对应关系落库这个映射后面转换报文、查单、对账都会用到。还有一个容易忽略的点是tn的时效性。云闪付返回的tn一般都有有效期超时未支付的话前端拿这个tn去唤起支付会直接报错。所以落库的时候我把下单时间一并存了下来后面判断tn是不是过期、要不要主动关单都靠这个时间字段。1.2 paydata需要的是一份标准化报文paydata可以理解成商户内部的一个支付数据中台。微信、支付宝、云闪付所有渠道的支付数据最后都要汇聚到这里做统一处理、统一对账。问题在于每个渠道返回的原生数据结构都不一样字段命名风格也千差万别。如果每个渠道都按原生格式往中台塞中台的处理逻辑根本没法统一。所以paydata一般会定义一套自己的报文规范。这套规范和云闪付侧没有一一对应关系它只关心自己业务要用的那些字段要求各个渠道尽量转成统一格式再传进来。比如云闪付叫orderIdpaydata可能叫merOrderId云闪付金额按分来paydata可能要求按元来。命名不同还只是小事单位不一致往往最容易出问题。1.3 字段对照是转换的关键前提写转换代码之前我先把云闪付侧和paydata侧的字段做了一张对照表这一步看着简单实际效果比后面写代码还重要。下面是这个项目里最核心的几组字段对照云闪付原始字段paydata标准字段说明tntn交易凭证号原样透传merIdchannelMerId渠道商户号orderIdmerOrderId商户订单号txnAmt单位分amount单位元金额单位必须转换txnTimepayTime时间格式需要统一这张表拉清楚之后后面的映射代码基本就是在抄表。反过来如果表没拉清楚就急着写代码联调的时候你会发现不是缺字段就是格式不对来回改很浪费时间。2. 转换方案选型为什么不用SDK返回的Map直接传方案选型这部分我当初是真的纠结过。银联本身提供了官方SDK下单接口调一下就直接返回数据了看起来好像没必要再包一层转换。但实际做下来直接传的方案问题很多。2.1 最省事的方案为什么行不通最省事的方案就是调用云闪付SDK下单拿到返回的Map把tn取出来然后整个Map原封不动地塞给paydata。听起来很省事上线跑起来才发现一堆隐患。第一个问题是字段不可控。SDK返回的Map里什么字段都有哪些是paydata要的、哪些是多余的完全靠下游自己捞。万一paydata侧解析出问题你根本分不清是哪个字段引起的排查效率极低。第二个问题是字段类型和格式完全没法在写代码的时候保证。金额是字符串还是数字、时间是什么格式、空值怎么处理这些全靠下游容错等于把一个本可以在源头解决的问题甩给了下游所有消费方。第三个问题更隐蔽SDK返回的Map在某些接口版本里字段命名是有变化的。直接用Map对接等于把这种不确定性原样传导给了paydata后面渠道升级一次你就得跟着排查一次。2.2 独立转换层的设计思路所以最终我采用了第二种方案在云闪付和paydata之间加一个独立转换层。向下对接云闪付SDK向上对接paydata接口中间用一套标准DTO承载数据。这套设计有三个明显的好处。第一字段映射集中在一个地方维护渠道字段变了只改一处第二DTO是类型安全的金额、时间这些格式问题可以在代码层面就规范掉第三转换逻辑可以单独写单元测试不用每次联调都依赖真实支付环境。设计上我把它拆成了三个环节获取tn并落库、查询对应订单信息、组装paydata报文。每个环节一个类各干各的互不干扰。后面想加其他渠道的时候只需要对照着新增一套映射器就好。2.3 方案对比小结对比项直接用SDK返回的Map独立转换层代码量少多一些字段可控性弱强格式统一无法保证在源头保证扩展新渠道每个渠道都要下游适配只需加一个映射器排查问题效率低高实际做下来多加的那点代码量远小于联调和排查时省下的时间。3. 可运行源码核心实现拆解下面进入正题把可运行工程里的核心实现拆开讲。这套源码是一个标准的Spring Boot工程Java 8 Spring Boot 2.x依赖用了Hutool的工具类主要是为了省掉手写日期转换和Base64编码的功夫。3.1 工程整体结构与配置工程是标准的Maven结构包名用的com.paybridge核心模块分成三块controller层负责接收前端带tn的回跳请求service层负责业务逻辑和转换util层放签名和金额转换这类通用工具。paydata-bridge/ ├── pom.xml └── src/main/ ├── java/com/paybridge/ │ ├── PayBridgeApplication.java │ ├── controller/PayNotifyController.java │ ├── service/UnionPayService.java │ ├── service/PayDataService.java │ ├── service/TransferService.java │ ├── dto/OrderTnMapping.java │ ├── dto/PayDataRequest.java │ ├── util/SignUtil.java │ └── util/AmountUtil.java └── resources/ ├── application.yml └── certs/test_mer.pfx配置文件的重点参数是这几项测试环境和正式环境换一套配置就行unionpay: mer-id: 你的商户号 cert-path: certs/test_mer.pfx cert-pwd: 证书密码 gateway-url: https://gateway.95516.com/gateway/api/frontTransReq.do paydata: server-url: http://localhost:8081/paydata/receive app-id: paydata分配的AppId private-key: classpath:certs/merchant_private.key需要提醒的是证书路径这里Windows和Linux的写法不一样。certs/test_mer.pfx这种相对路径在本地能跑部署到Linux服务器上建议改成绝对路径不然后面排查证书加载失败会非常痛苦。3.2 tn获取与订单映射的落库设计云闪付下单成功拿到tn之后代码第一件事是构建一个订单与tn的映射对象并落库。tn返回的时候顺手把merId、orderId、下单时间一起存下来这一步非常关键。MapString, String resp unionPayService.placeOrder(orderInfo); OrderTnMapping mapping new OrderTnMapping(); mapping.setTn(resp.get(tn)); mapping.setMerId(resp.get(merId)); mapping.setOrderId(resp.get(orderId)); mapping.setCreateTime(LocalDateTime.now()); orderTnMapper.insert(mapping);这里我踩过一个很小但很要命的坑tn字段在数据库里一定要用varchar不要用bigint或者number。有些版本的SDK返回的tn是纯数字看起来像long但实际长度已经超过了long的安全精度范围一旦转成数字类型再转回字符串末尾的几位会变成0前端拿这个tn去支付必挂。3.3 转换器核心代码与金额单位处理转换这一步是整个工程最核心的部分把tn映射对象和订单信息组装成一个paydata标准报文。核心转换方法大概长这样public PayDataRequest convertToPayDataRequest(OrderTnMapping mapping) { Order order orderRepository.findByOrderId(mapping.getOrderId()); PayDataRequest request new PayDataRequest(); request.setChannel(UNIONPAY); request.setTn(mapping.getTn()); request.setMerOrderId(mapping.getOrderId()); request.setChannelMerId(mapping.getMerId()); request.setAmount(AmountUtil.fenToYuan(order.getAmountFen())); request.setCurrency(CNY); request.setPayTime(TimeUtil.format(mapping.getCreateTime())); return request; }AmountUtil.fenToYuan这个方法值得单独说一下。云闪付返回的金额单位是分paydata报文要求是元转换逻辑我统一封装成了一个工具方法内部用BigDecimal做除法避免直接用浮点数除100带来的精度问题。public static String fenToYuan(long amountFen) { return BigDecimal.valueOf(amountFen) .divide(BigDecimal.valueOf(100)) .setScale(2, RoundingMode.HALF_UP) .toPlainString(); }时间格式我也单独做了统一。云闪付返回的txnTime是yyyyMMddHHmmss这种紧凑格式paydata侧要求的是yyyy-MM-dd HH:mm:ss。转换之后就放在PayDataRequest里依赖这个DTO的下游就可以直接用标准格式了。3.4 签名与验签的细节paydata接收报文一般是要求验签的。签名规则业内基本一致把参与签名的字段按key的ASCII码升序排列每个字段以keyvalue形式拼接字段之间用连接最后用商户私钥做RSA签名签名结果Base64编码放到报文里。public static String sign(MapString, String data, PrivateKey privateKey) throws Exception { String content data.entrySet().stream() .filter(e - e.getValue() ! null !e.getValue().isEmpty()) .sorted(Map.Entry.comparingByKey()) .map(e - e.getKey() e.getValue()) .collect(Collectors.joining()); Signature signature Signature.getInstance(SHA256withRSA); signature.initSign(privateKey); signature.update(content.getBytes(StandardCharsets.UTF_8)); return Base64.getEncoder().encodeToString(signature.sign()); }签名这里有三个细节很容易踩坑。第一拼接前一定要把空值的字段过滤掉不然你本地签名串和服务端验签串对不上排查半天发现是空值字段的问题第二排序必须按ASCII码升序注意ASCII排序和中文拼音排序不是一回事第三编码统一用UTF-8GBK和UTF-8混用会导致中文参数签名必失败。4. 部署运行与联调排坑实录代码写得再顺联调永远是支付对接里最磨人的环节。这一节把本地跑通这套工程的具体步骤和联调中遇到的典型问题都记录下来。4.1 本地跑通完整流程的三步第一步先把源码里自带的一个paydata模拟服务跑起来。这个mock服务非常轻量就是接收POST过来的JSON报文做一次验签然后返回一个固定成功响应方便本地调试转换逻辑不用每次都依赖真实paydata环境。第二步启动paydata-bridge主服务。启动之前确认好application.yml里的商户号、证书路径、证书密码都正确尤其证书密码这个东西云闪付测试环境的证书密码是固定的正式环境是单独的别混用。第三步模拟一笔云闪付下单。由于云闪付的真实下单需要商户号和证书配合我建议先跑一遍源码里写好的下单测试用例确认能正常拿到tn再走回跳通知流程触发transferService的转换逻辑最后到paydata mock服务里看接收到的报文。4.2 联调阶段踩过的经典坑第一个坑是金额差100倍。云闪付下单返回的金额单位是分paydata报文要求是元第一次联调时转换方法里忘了除以100结果paydata侧收到一笔金额放大了100倍的支付单。这个问题不会报错所以特别隐蔽排查了好久才发现是单位问题。第二个坑是tn精度丢失。前面提到过tn在数据库里用了varchar才躲过一劫但在内存里如果误用了Long类型接收JSON序列化时末尾几位就会变成0。这个问题在测试环境偶尔会出现因为测试环境的tn位数不一定触发精度丢失正式环境一下就暴露了。第三个坑是证书路径不生效。本地Windows跑得好好的部署到Linux服务器上下单就报错查了半天是证书路径用的相对路径程序启动目录一变就找不到证书了。改成绝对路径之后问题解决。4.3 常见问题速查表现象可能原因处理方法下单返回respCode非00商户号或证书不匹配检查配置确认测试环境证书tn唤起云闪付失败tn过期或类型精度丢失检查落库类型确认时效性转换后金额不对分了和元单位混用用BigDecimal统一转换金额签名验签失败空值未过滤、编码混用按ASCII排序统一UTF-8Linux上证书加载失败证书路径用相对路径改为绝对路径5. 这套转换器的扩展玩法这套工程本身跑通一个渠道已经够了但后来我做其他渠道接入时发现它的设计还能往几个方向继续扩展。5.1 多渠道路由扩展如果后面要接微信或者支付宝不需要把转换逻辑重写一遍只需要把每家的原始字段映射成PayDataRequest这个标准DTO就行。我现在就是用一个渠道枚举加一个映射器Map来做路由新增渠道就是新增一个枚举值和一个映射器实现类老代码完全不用动。5.2 tn映射的缓存与对账应用tn映射关系落库之后除了满足转换需求还可以天然地为对账服务。每天晚上跑对账任务时拿本地订单号和tn的映射去跟云闪付的账单做关联比用订单号关联更准确因为tn是云闪付侧的唯一下单凭证。如果你担心订单量大了之后落库mysql性能跟不上可以把这个映射先放Redis缓存设置一个跟tn有效期匹配的过期时间过期未支付的直接淘汰有效减少数据库压力。5.3 一个真实的改造案例后来有一个项目需要对接三个渠道我用了同一套PayDataRequest标准报文只写了三个不同的转换器。整个改造花了不到一个工作日而且上线之后基本没出过问题。反而是在字段对照表上多花了半天时间把每个渠道的所有字段都拉出来仔细对了一遍这三家渠道的单位、时间和字段命名差异都在表上标得清清楚楚。这套工程从整理到跑通前后折腾了大半个周末最耗时间的反而不是写转换逻辑而是在字段对照上反复确认。后来跟几个做支付的老同事聊大家都有同样的感受支付对接的坑十有八九不在技术难度而在那些特别不起眼的细节上金额单位、时间格式、字段命名、tn精度随便一个都能让你排查半天。我现在的习惯是接到任何新渠道的对接任务第一件事永远是列字段对照表把每个字段的单位、格式、空值策略都标清楚然后再动代码。这个习惯帮我省下的时间远比想象中多。本文还有配套的精品资源点击获取