JDK17+SpringBoot3.3.6+Netty4.1.115实现企业级支付系统POS网关签到功能
JDK17+SpringBoot3.3.6+Netty4.1.115实现企业级支付系统POS网关签到功能

我在这篇文章JDK17+SpringBoot3.4.0+Netty4.1.115搭建企业级支付系统POS网关中已经将POS网关的框架搭建流程进行了讲解,今天实现POS网关中的第一个功能签到。我们作为用户经常会被商家拿着智能POS机扫码完成支付,或者我们主动扫智能POS机上的二维码完成支付功能。其实POS智能终端完成支付功能之前需要做一些准备工作,这些工作是为了更好的实现支付功能,那么今天实现的签到功能就是POS机开机后需要做的第一件事情。签到主要是为了从后端网关系统获取工作密钥还有当天的批次号。接下来我们来看具体的代码实现流程吧
完整代码在文章最后,如果觉得本篇文章对你有用,记得点赞、关注、收藏哦。你的支持是我持续更新的动力!
签到功能有多种实现方式第一种是基于自定义协议进行实现,第二种基于Netty支持的开源协议进行实现,我们今天用自定义协议方式实现签到功能。下图为签到功能的整体代码结构

1 协议设计和实现
消息协议整体分为消息头和消息体,协议中需要设定整体协议的长度,客户端按照预先定义好的长度进行数据发送。服务端再按照协议规则进行解析。POS前置系统只需要实现两个功能,协议转换和转发,具体的签到和业务功能在支付交易系统中实现。
1.1 如何确认当前交易
智能POS机上有很多功能,签到、签退、版本更新、心跳、支付、退款那我们是如何知道当前是在做什么交易。我们在自定义协议中是通过指令CommandID+交易类型进行识别,下图为具体的指令设计
指令 | 说明 |
---|---|
0x80000001 | 签到 |
0x80000012 | 签退 |
0x8000000B | 交易:交易类型 201:消费 301:消费撤销 601: 余额查询 501:重打印 401:退货 |
0x8000000A | 软件更新 |
1.2 代码实现
1.2.1 协议封装
package cn.itbeien.protocol;
import cn.itbeien.enums.CommandIDEnums;
import cn.itbeien.protocol.request.RegistrationRequest;
import cn.itbeien.util.ByteUtil;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author itbeien
* 项目网站:https://www.itbeien.cn
* 公众号:贝恩聊架构
* 全网同名,欢迎小伙伴们关注
* 消息协议类V1.0
* Copyright© 2024 itbeien
*/
@Slf4j
public class Protocol {
private final static Map<Integer, Class<? extends Protocol>> packetType = new ConcurrentHashMap<>();
static {
packetType.put(CommandIDEnums.SignIn.getCommand(), RegistrationRequest.class);
}
/**
* 消息头长度
*/
public static final int HEADER_LENGTH = 62;
/**
* 消息总长度
*/
private int length;
/**
* 消息流水号,标识POS终端的每笔交易和服务端交易一一对应
*/
private int sequenceID;
/**
* 设备逻辑编号
*/
private String deviceLogicID;
/**
* 设备物理编号
*/
private String devicePhysicalID;
/**
* 商户编号,预留字段
*/
private String merchantId;
/**
* 批次号
*/
private int batchID;
/**
* 交易流水号
*/
private int transactionID;
/**
* 操作员
*/
private String opNumber;
/**
* 版本号
*/
private String version;
/**
* 消息体
*/
private byte[] body;
public Protocol() {
}
public int getLength() {
return length;
}
public void setLength(int length) {
this.length = length;
}
public int getSequenceID() {
return sequenceID;
}
public void setSequenceID(int sequenceID) {
this.sequenceID = sequenceID;
}
public String getDeviceLogicID() {
return deviceLogicID;
}
public void setDeviceLogicID(String deviceLogicID) {
this.deviceLogicID = deviceLogicID;
}
public String getMerchantId() {
return merchantId;
}
public void setMerchantId(String merchantId) {
this.merchantId = merchantId;
}
public int getBatchID() {
return batchID;
}
public void setBatchID(int batchID) {
this.batchID = batchID;
}
public int getTransactionID() {
return transactionID;
}
public void setTransactionID(int transactionID) {
this.transactionID = transactionID;
}
public String getOpNumber() {
return opNumber;
}
public void setOpNumber(String opNumber) {
this.opNumber = opNumber;
}
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
public String getDevicePhysicalID() {
return devicePhysicalID;
}
public void setDevicePhysicalID(String devicePhysicalID) {
this.devicePhysicalID = devicePhysicalID;
}
public byte[] getBody() {
return body;
}
public void setBody(byte[] body) {
this.body = body;
}
public static Class<? extends Protocol> get(Integer command) {
return packetType.get(command);
}
public static void main(String[] args) {
System.out.println(ByteUtil.getBytes(12345678).length);
}
}
1.2.2 协议解码代码
协议的编解码为POSP网关自定义的编解码规则,这里用到了Netty中的ByteToMessageDecoder和MessageToByteEncoder作为自定义协议编解码的父类。具体代码实现如下
package cn.itbeien.protocol.codec;
import cn.itbeien.protocol.Protocol;
import cn.itbeien.util.ByteUtil;
import cn.itbeien.util.SerializationTool;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
import static cn.itbeien.protocol.Protocol.HEADER_LENGTH;
/**
* @author itbeien
* 项目网站:https://www.itbeien.cn
* 公众号:贝恩聊架构
* 全网同名,欢迎小伙伴们关注
* 反序列化 字节数组->对象
* Copyright© 2024 itbeien
*/
@Slf4j
public class ProtocolDecode extends ByteToMessageDecoder{
@Override
protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {
int readable = byteBuf.readableBytes();
if (readable < 62) {
return;
}
// 将ByteBuf转换为byte[]
byte[] data = new byte[readable];
byteBuf.readBytes(data);
byte[] body = ByteUtil.getValue(data, HEADER_LENGTH, readable - HEADER_LENGTH);
int command = ByteUtil.getInt(ByteUtil.getValue(body, 0, 4));
log.info("开始解析协议");
list.add( SerializationTool.deserialize(data,Protocol.get(command)) );
}
}
1.2.3 协议编码代码
package cn.itbeien.protocol.codec;
import cn.itbeien.protocol.Protocol;
import cn.itbeien.util.SerializationTool;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
import lombok.extern.slf4j.Slf4j;
/**
* @author itbeien
* 项目网站:https://www.itbeien.cn
* 公众号:贝恩聊架构
* 全网同名,欢迎小伙伴们关注
* 序列化 对象->字节数组
* Copyright© 2024 itbeien
*/
@Slf4j
public class ProtocolEncode extends MessageToByteEncoder<Protocol> {
@Override
protected void encode(ChannelHandlerContext channelHandlerContext, Protocol protocol, ByteBuf byteBuf) throws Exception {
byte[] data = SerializationTool.serialize(protocol);
byteBuf.writeBytes(data);
}
}
2 功能测试
2.1 POS客户端代码实现
下面为部分代码,完整代码见文章最后
package cn.itbeien.client;
import cn.itbeien.client.protocol.Protocol;
import cn.itbeien.client.socket.NettyClient;
import cn.itbeien.client.util.ByteUtil;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelPipeline;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import jakarta.annotation.Resource;
import java.util.concurrent.*;
/**
* @author itbeien
* 项目网站:https://www.itbeien.cn
* 公众号:贝恩聊架构
* 全网同名,欢迎小伙伴们关注
* Copyright© 2024 itbeien
*/
@SpringBootApplication
public class ClientApplication implements CommandLineRunner {
@Resource
private NettyClient nettyClient;
private static Logger logger = LoggerFactory.getLogger(ClientApplication.class);
private static ExecutorService executorService = Executors.newFixedThreadPool(2);
private ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
public static void main(String[] args) {
SpringApplication.run(ClientApplication.class, args);
}
@Override
public void run(String... args) throws Exception {
// 1. 启动socket连接
logger.info("NettyClient连接服务开始 host:{} port:{}", "127.0.0.1", 8600);
Future<Channel> future = executorService.submit(nettyClient);
Channel channel = future.get();
if (null == channel) throw new RuntimeException("netty client start error channel is null");
while (!nettyClient.isActive()) {
logger.info("NettyClient启动服务 ...");
Thread.sleep(500);
}
logger.info("NettyClient连接服务完成 {}", channel.localAddress());
logger.info("POS通道检查:通道状态 " + nettyClient.isActive());
//String sendMsg = "{\"amount\":\"100.00\",\"interfaceCode\":\"pay\",\"merchantCode\":\"1000001\",\"payType\":\"js\"}";
//sendMsg(channel, "pos发起支付交易");
sendMsg(channel, encodeMsg());//发送支付交易指令
// Channel状态定时检查;3秒后每5秒执行一次 模拟硬件和软件重连
/* scheduledExecutorService.scheduleAtFixedRate(() -> {
while (!nettyClient.isActive()) {//判断是否和服务器连接上
logger.info("POS通道:通道状态 " + nettyClient.isActive());
try {
logger.info("POS通道:断线重连[Begin]");
Channel freshChannel = executorService.submit(nettyClient).get();
freshChannel.writeAndFlush("abc");
} catch (InterruptedException | ExecutionException e) {
logger.error("POS通道:断线重连[Error]");
}
}
}, 3, 5, TimeUnit.SECONDS);*/
}
public void sendMsg(Channel channel, byte[] msg) {
try {
logger.info("POS通道发送消息:[Begin]");
//Channel channel = executorService.submit(nettyClient).get();
channel.writeAndFlush(msg);
logger.info("POS通道发送消息:[End]");
ChannelPipeline pipelined = channel.pipeline();
//pipelined.addLast(new ClientHandler());
// Channel状态定时检查;3秒后每30秒执行一次
scheduledExecutorService.scheduleAtFixedRate(() -> {
logger.info("POS通道模拟游客支付:通信管道状态 " + nettyClient.isActive());
try {
logger.info("POS通道模拟游客支付:[Begin]");
ByteBuf buf = Unpooled.wrappedBuffer(msg);
channel.writeAndFlush(buf);
//buf.release();
logger.info("POS通道模拟游客支付:[End]");
} catch (Exception e) {
logger.error("POS通道模拟游客支付:异常[Error]");
}
}, 3, 30, TimeUnit.SECONDS);
} catch (Exception e) {
logger.error("POS通道模拟游客支付:异常");
}
}
public byte[] encodeMsg(){
Protocol protocol = new Protocol();
protocol.setBatchID(202412);
//protocol.setLength(60);//签到消息总长度(消息头+消息体)
protocol.setDeviceLogicID("T100000512");//T100000512 //T000000001
protocol.setDevicePhysicalID("12837465426234567645");
protocol.setMerchantId("2345678924");
protocol.setOpNumber("bn");
protocol.setSequenceID(11);
protocol.setVersion("1.00");
protocol.setTransactionID(151815);//流水号
Integer commandId = 0x80000001;//签到
//Integer commandId = 0x80000012;//签退
//Integer commandId = 0x00000014;//软件版本更新
//Integer commandId = 0x8000000B;//交易
String version = "1.00";
byte[] bcommandId = ByteUtil.getBytes(commandId);
byte[] bversion = ByteUtil.getBytes(version);
//byte[] businessNo = ByteUtil.getBytes(11);
//short value = 201; //消费
//short value = 301;//消费撤销
//short value = 601;//查询
//short value = 401;//退货
//short value = 501;//重打印
//byte b[] = ByteUtil.getBytes(value);
//byte[] tradeNo = ByteUtil.getBytes(value);
//String contents ="9996660005778407|5;2;1;1111";//6666661006978001(新卡系统卡BIN);9996660005778407 //消费
//String contents ="9996660005778407|5;202408;151814" ;//消费撤销
//String contents ="6666661006978001|5;202405;151800;2" ;//退货
//String contents ="6666661006978001|5;202405;151800" ;//查询
//String contents ="9996660005778407|5;202405;151800" ;//6666661006978001 //重打印
//byte[] content = ByteUtil.getBytes(contents);
//content = TDes.encryptMode("E4429DEDB7E56E8334C69003".getBytes(), content);
//byte[] length = ByteUtil.getBytes(content.length);
//byte[] extraData = ByteUtil.getBytes(1);
//byte[] extraDataLength = ByteUtil.getBytes(extraData.length);
byte[] data = new byte[bcommandId.length + bversion.length ];//提交消息体数据
//byte[] data = new byte[bcommandId.length + businessNo.length + tradeNo.length + length.length + content.length+extraDataLength.length+extraData.length];//提交消息体数据
System.arraycopy(bcommandId, 0, data, 0, bcommandId.length);
System.arraycopy(bversion, 0, data, bcommandId.length, bversion.length);
//System.arraycopy(businessNo, 0, data, bcommandId.length, businessNo.length);
//System.arraycopy(tradeNo, 0, data, bcommandId.length+businessNo.length, tradeNo.length);
//System.arraycopy(length, 0, data, bcommandId.length+businessNo.length+tradeNo.length, length.length);
//System.arraycopy(content, 0, data, bcommandId.length+businessNo.length+tradeNo.length+length.length, content.length);
//System.arraycopy(extraDataLength, 0, data, bcommandId.length+businessNo.length+tradeNo.length+length.length+content.length, extraDataLength.length);
//System.arraycopy(extraData, 0, data, bcommandId.length+businessNo.length+tradeNo.length+length.length+content.length+extraDataLength.length, extraData.length);
//byte[] signature = TDes.encryptMode("E4429DEDB7E56E8334C69003".getBytes(), data);//签名(对消息体进行签名)
//Integer signatureLength = signature.length;
//byte[] bsignatureLength =ByteUtil.getBytes(signatureLength);
//byte[] data1 =new byte[data.length+signature.length+bsignatureLength.length];//包括签名
//System.arraycopy(data, 0, data1, 0, data.length);
//System.arraycopy(bsignatureLength, 0, data1, data.length, bsignatureLength.length);
//System.arraycopy(signature, 0, data1, data.length+bsignatureLength.length, signature.length);
//protocol.setBody(data1);
//protocol.setLength(62+data1.length);//签到消息总长度(消息头+消息体)
//System.out.println(data.length);
//System.out.println(data1.length);
protocol.setBody(data);
protocol.setLength(62+data.length);
return protocol.encoder();
}
}
2.2 启动POS网关
启动POS网关系统,用于测试签到功能

2.3 启动支付微服务和POS客户端
启动支付微服务和POS客户端用于和POS网关系统联调签到功能


2.4 签到功能测试
最后开启签到功能的测试,POS客户端->POS网关->支付交易微服务测试整体签到流程



欢迎大家关注我的项目实战内容itbeien.cn,一起学习一起进步,在项目和业务中理解各种技术。

欢迎沟通交流技术和支付业务,一起探讨聚合支付/预付卡系统业务、技术、系统架构、微服务、容器化。并结合聚合支付系统深入技术框架/微服务原理及分布式事务原理。加入我的知识星球吧

跟着我学微服务系列
01跟着我学微服务,什么是微服务?微服务有哪些主流解决方案?
05SpringCloudAlibaba之图文搞懂微服务核心组件在企业级支付系统中的应用
06JDK17+SpringBoot3.4.0+Netty4.1.115搭建企业级支付系统POS网关
07JDK17+SpringCloud2023.0.3搭建企业级支付系统-预付卡支付交易微服务
08JDK17+Dubbo3.3.2搭建企业级支付系统-预付卡支付交易微服务
贝恩聊架构-项目实战地址
3 源码地址
跟着我学微服务-基于企业级支付项目系列文章、资料和源代码会同步到以下地址,代码和资料每周都会同步更新
该仓库地址主要用于基于企业级支付系统,学习微服务整体技术栈
