一.思路
- 无论开发什么功能(至少以目前我所能接受的任务来说)先以完成主要功能为主
- 在测试正常的操作流程没有问题的情况下,来进行细节上的调整
- 正常的实现功能之后,会发现一些需要解决的问题,由此问题引发出了一些解决问题的技术栈
- 技术栈出现的背景:如在多人操作支付的情况下,因该将支付应用于异步操作,提高整体可用性
- 系统出现故障的导致消息丢失,MQ的作用即是解决该问题的一种方式,保挣消息的可靠性
- 支付是异步实现的方式,最终支付的信息(即消息回调)会在最终支付流程完成后,自动回调
- 后端根据回调的信息对订单信息进行相应的操作,如:支付成功、支付失败、订单超时等
- 支付有订单信息的查询接口,可以通过自动定时任务进行查询,根据信息对相应订单信息进行操作
- 定时任务会消耗系统的线程等资源,MQ的延迟队列则更具高性能(用于超时订单的信息操作)
时序图
二.支付技术栈
RabbitMQ(我所使用的)
1.特点
- 高可用性
- 灵活的消息路由
- 支持多种协议
- 消息持久化
- 消息确认机制
- 分布式支持
2.RabbitMQ 的组件
- Producer(生产者)
- Consumer(消费者)
- Exchange(交换机)
- Direct Exchange:将消息按路由键直接路由到一个队列。(90%场景适用)
- Fanout Exchange:将消息广播到所有绑定的队列中。
- Topic Exchange:根据路由键进行模糊匹配路由,适合需要复杂路由的场景。
- Headers Exchange:根据消息头部的属性进行匹配路由。
- Queue(队列)
- Binding(绑定)
3.工作原理(简要描述)
- 生产者发送消息:生产者将消息发送到交换机,指定路由键。
- 交换机路由消息:交换机根据消息的路由键将消息路由到一个或多个队列。
- 队列存储消息:消息被存储在队列中,直到消费者从队列中取出并进行处理。
- 消费者处理消息:消费者从队列中取出消息,进行处理后发送确认(acknowledge),表示该消息已经成功消费。
- 消息确认:消费者通过确认机制通知 RabbitMQ 消息已经被正确处理。如果没有确认,RabbitMQ 会将消息重新发送到其他消费者。
Publish/Subscribe
三.支付
1.使用框架
JeePay:https://docs.jeequan.com/docs/jeepay/api_rule
2.使用原因
接入多平台支付,一个接口搞定所有支付平台
3.配置(根据各个官方配置来)
设计配置表,不同厂商的数据不同,但是接口参数大同小异,具体更具相关的文档进行获取配置
4.实操(步骤)
- 创建请求体,最直观的方式即:copy测试demo,修改参数发送请求
- 进行数的请求发送,获取返回的数据
- 进行订单的创建(进行逻辑判定)
- 保证订单的可靠性
- 订单状态的修改
ⅰ. 下单
ⅱ. 待支付
ⅲ. 待生成
ⅳ. …
- 接收支付回调信息,支付是异步流程(手机支付后需要等待一段时间才会通知扣费,就是这有一原因)
a. 支付商一般会多次发送消息回调通知,需要做过滤处理,避免重复处理
b. 保证回调信息的可靠性,即放入MQ中处理
c. 更具回调信息对订单做处理
核心下单代码
//Order即为你数据库表的Order,将相关内容放入到Order中,再将下单的返回的一些数据赋值给Order
//将该Order对象保存到数据库中,这就是一个简单的支付
Order order = new Order();
PayOrderCreateRequest request = new PayOrderCreateRequest();
PayOrderCreateReqModel model = new PayOrderCreateReqModel();
request.setBizModel(model);
String mchNo = mchApp.getMchNo();
model.setMchNo(mchNo); // 商户号
model.setAppId(order.getAppId());
model.setMchOrderNo(mchOrderNo);
model.setWayCode(order.getWayCode());
model.setAmount(order.getAmount());
model.setCurrency("CNY");
model.setClientIp(OConvertUtils.getIp());
model.setSubject(order.getSubject());
model.setBody(order.getBody());
model.setReturnUrl(order.getReturnUrl());
//回调地址
model.setNotifyUrl(smartMeterApiBase+"/py/mch/notify/url");
model.setDivisionMode(order.getDivisionMode()); //分账模式
// 过期时间 设置为当前时间 + 30 分钟
//设置扩展参数
/*JSONObject extParams = new JSONObject();
if(StringUtils.isNotEmpty(order.getExtParam())) {
extParams.put("payDataType", order.getPayDataType().trim());
}
if(StringUtils.isNotEmpty(order.getAuthCode())) {
extParams.put("authCode", dto.getAuthCode().trim());
}
model.setChannelExtra(extParams.toString());*/
JeepayClient jeepayClient = new JeepayClient(apiBase, mchApp.getAppSecret());
try {
PayOrderCreateResponse response = jeepayClient.execute(request);
order.setPayOrderId(response.get().getPayOrderId());
order.setMchOrderNo(mchOrderNo);
order.setMchNo(mchNo);
order.setState(response.get().getOrderState());
order.setErrCode(response.get().getErrCode());
order.setErrMsg(response.get().getErrMsg());
if(response.getCode() != 0){
order.setErrMsg(response.get().getErrMsg());
order.setErrCode(response.get().getErrCode());
throw new ApiException(response.getMsg());
}
Map<String, String> map = JsonUtil.objectToMap(response.get());
map.put("mchOrderNo", mchOrderNo);
return map;
} catch (JeepayException e) {
order.setState(StateEnum.ORDER_CLOSED.getCode());
throw new ApiException(e.getMessage());
} finally {
if (order.getPayOrderId() != null) {
this.save(order);
//同时需要加入延迟队列中,在队列超时时进行订单关闭等操作
notifyService.sendDelayMessageCancelOrder(map.get("payOrderId"));
}
}
需要注意的是:支付相关返回都是Json数据,需要处理接收Json数据
支付回调接口
@ApiOperation("支付回调信息")
@PostMapping("/notify/url")
public void payOrderNotify() {
JSONObject params = getReqParamJSON();
String appId = params.getString("appId");
String mchNo = params.getString("mchNo");
String sign = params.getString("sign");
PayMchApp mchApp = mchAppService.getOne(Wrappers.<PayMchApp>lambdaQuery().eq(PayMchApp::getAppId, appId));
// 校验商户信息
if (mchApp == null || !mchApp.getMchNo().equals(mchNo)) {
log.error("商户不存在或商户号不匹配: {}", params);
return;
}
log.info("支付回调信息: {}", params);
// 移除签名参数进行验证
params.remove("sign");
// 校验签名
if (!JeepayKit.getSign(params, mchApp.getAppSecret()).equalsIgnoreCase(sign)) {
log.error("签名验证失败: {}", params);
return;
}
notifyService.dealWithPayNotify(params);
}
public void dealWithPayNotify(JSONObject params) {
String mchOrderNo = params.getString("mchOrderNo");
Order order = payOrderService.getOne(Wrappers.<Order>lambdaQuery().eq(Order::getMchOrderNo, mchOrderNo));
//进行去重操作
if (Order.getPayOrderId() != null) {
//已经成功处理了支付端回调的消息
return;
}
try {
String payOrderId = params.getString("payOrderId");
PayOrder order = payOrderService.getOne(Wrappers.<PayOrder>lambdaQuery()
.eq(PayOrder::getPayOrderId, payOrderId));
params.put("type", order.getType());//添加订单类型
params.put("meterId", order.getMeterId());//添加 表id
// 将时间戳转换为 Date 对象
long successTimeMillis = Long.parseLong(params.getString("successTime"));
Date successTime = new Date(successTimeMillis);
payOrderService.update(Wrappers.<PayOrder>lambdaUpdate()
.eq(PayOrder::getPayOrderId, payOrderId)
.set(PayOrder::getChannelOrderNo, params.getString("channelOrderNo"))
.set(PayOrder::getSuccessTime, successTime)
.set(PayOrder::getChannelUser, params.getString("ifCode")));
// 当订单状态为2时,进行充值操作
if ("2".equals(params.getString("state"))) {
//将数据放入到充值mq中
paymentProducer.sendMessage(JSONUtil.toJsonStr(params));
log.info("Recharge initiated for payment order {}", payOrderId);
}
} catch (Exception e) {
log.error("notify message error", e);
}
}
RabbitMQ
/**
* 充值消息的发出者
*/
@Component
@Slf4j
public class PaymentProducer {
@Autowired
private AmqpTemplate amqpTemplate;
@Autowired
private RabbitMqConfig rabbitMqConfig;
public void sendMessage(String params){
//给充值队列发送消息
amqpTemplate.convertAndSend(
rabbitMqConfig.getPayOrderExchange(),
rabbitMqConfig.getPayOrderRoutingKey(),
params
);
log.info("send Json:{}",params);
}
}
/**
* 充值消息的消费者
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class PaymentConsumer {
private final MmsElectricityMeterService mmsElectricityMeterService;
private final MmsWaterMeterService mmsWaterMeterService;
private final PayOrderService payOrderService;
@RabbitListener(queues = "smartmeter.pay.queue")
@RabbitHandler
public void handlePaymentMessage(String message) {
try {
System.out.println("Received message: " + message);
JSONObject params = JSONObject.parseObject(message);
String payOrderId = params.getString("payOrderId");
String type = params.getString("type");
String meterId = params.getString("meterId");
String amount = params.getString("amount");
String state = params.getString("state");
if ("2".equals(state)) {
log.info("支付订单:{}已支付成功", payOrderId);
payOrderService.update(Wrappers.<PayOrder>lambdaUpdate()
.eq(PayOrder::getPayOrderId, payOrderId)
.set(PayOrder::getState, state));
if (type!=null){
//进行相关的逻辑充值操作
if ("0".equals(type)){ //电表
mmsElectricityMeterService.reCharge(meterId, amount, "app用户充值","", payOrderId);
}else if("1".equals(type)){ //水表
mmsWaterMeterService.reCharge(meterId, amount, "app用户充值","", payOrderId);
}else{
log.error("未知设备类型!");
}
}
log.info("process the payment order:{} successful!", payOrderId);
}
} catch (Exception e) {
log.error("Error processing payment message: {}", message, e);
}
}
}
四.退款
总体上,步骤和支付流程大同小异
- 需求
用户申请退款之后,需要由管理员来进行审核,通过之后才能退款 - 需求分析
为了减少人工操作的介入,避免操作失误等
- 用户申请退款时,进行相关逻辑判断(如退款金额,金额是否以及充值如表,请求合法性等)
- 确保管理员只需审核退款原因
- 确保退款的可靠性
- 用户退款申请,就是在客户端生成一个数据保存,设置为申请退款状态,然后发送通知给管理员,进行退款审核操作
- 管理员的退款操作,其实才是真正退款的操作(即真正向支付商端发送退款请求)
五.消息通知
使用Websocket实现,直接上代码,更具不同逻辑进行调用不同的WebSocket的方法(使用时注入即可)
/**
* 监听消息(采用redis发布订阅方式发送消息)
* @author zt
*/
@Slf4j
@Component
public class SocketHandler implements CustomRedisListener {
@Autowired
private WebSocket webSocket;
@Override
public void onMessage(BaseMap map) {
log.info("【SocketHandler消息】Redis Listerer:{}", map.toString());
String userId = map.get("userId");
String message = map.get("message");
String cmd = (String) JSONUtil.parseObj(message).get(WebsocketConst.MSG_CMD);
if (StrUtil.isNotBlank(cmd)) {
//只处理租户消息
if (cmd.equals(WebsocketConst.CMD_TENANT)) {
if (StrUtil.isNotBlank(userId)) {
webSocket.pushMessage(userId, message);
//app端消息推送
// webSocket.pushMessage(userId+ CommonSendStatus.APP_SESSION_SUFFIX, message);
}
} else {
webSocket.pushMessage(message);
}
}
}
}
/**
* @Author scott
* @Date 2019/11/29 9:41
* @Description: 此注解相当于设置访问URL
*/
@Component
@Slf4j
@ServerEndpoint("/websocket/{userId}/{token}") //此注解相当于设置访问URL
public class WebSocket {
private Session session;
private String userId;
private static final String REDIS_TOPIC_NAME = "socketHandler";
@Resource
private CustomRedisClient customRedisClient;
private static UmsMemberService umsMemberService ;
private static JwtTokenUtil jwtTokenUtil;
@Autowired
public void setJwtTokenUtil(JwtTokenUtil jwtTokenUtil) {
WebSocket.jwtTokenUtil = jwtTokenUtil;
}
@Autowired
public void setUmsMemberService(UmsMemberService umsMemberService) {
WebSocket.umsMemberService = umsMemberService;
}
/**
* 缓存 webSocket连接到单机服务class中(整体方案支持集群)
*/
private static CopyOnWriteArraySet<WebSocket> webSockets = new CopyOnWriteArraySet<>();
private static Map<String, Session> sessionPool = new HashMap<String, Session>();
@OnOpen
public void onOpen(Session session, @PathParam(value = "userId") String userId, @PathParam(value = "token") String authHeader) {
try {
if (StrUtil.isNotBlank(authHeader)&&StrUtil.isNotBlank(userId)&&authHeader.startsWith(jwtTokenUtil.getTokenHead())){
String token = authHeader.substring(jwtTokenUtil.getTokenHead().length());
UmsMember umsMember = umsMemberService.getById(userId);
if (umsMember!=null){
UserDetails userDetails = umsMemberService.loadUserByPhone(umsMember.getPhone());
if (jwtTokenUtil.validateToken(token,userDetails)){
this.session = session;
this.userId = userId;
webSockets.add(this);
sessionPool.put(userId, session);
log.info("【websocket消息】有新的连接,总数为:" + webSockets.size());
}else{
throw new ApiException("签名验证失败,拒绝访问!");
}
}else {
throw new ApiException("非法用户!");
}
}else{
throw new ApiException("传参错误,拒绝访问!");
}
} catch (Exception e) {
log.error(e.getMessage());
throw new ApiException(e);
}
}
@OnClose
public void onClose() {
try {
webSockets.remove(this);
sessionPool.remove(this.userId);
log.info("【websocket消息】连接断开,总数为:" + webSockets.size());
} catch (Exception e) {
}
}
/**
* 服务端推送消息
*
* @param userId
* @param message
*/
public void pushMessage(String userId, String message) {
Session session = sessionPool.get(userId);
if (session != null && session.isOpen()) {
try {
log.info("【websocket消息】 单点消息:" + message);
session.getAsyncRemote().sendText(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 服务器端推送消息
*/
public void pushMessage(String message) {
try {
webSockets.forEach(ws -> ws.session.getAsyncRemote().sendText(message));
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* @param message:
* @return void
* @author le
* @description 收到消息会同时发送给所有在线的客户端
* @date 2022/12/3 11:30
*/
@OnMessage
public void onMessage(String message) {
//todo 前端未开启定时刷新任务时使用
log.debug("【websocket消息】收到客户端消息:" + message);
JSONObject obj = new JSONObject();
//业务类型
obj.put(WebsocketConst.MSG_CMD, WebsocketConst.CMD_CHECK);
//消息内容
obj.put(WebsocketConst.MSG_TXT, "心跳响应");
for (WebSocket webSocket : webSockets) {
webSocket.pushMessage(JSONUtil.toJsonStr(obj));
}
}
/**
* 后台发送消息到redis
*
* @param message
*/
public void sendMessage(String message) {
log.info("【websocket消息】广播消息:" + message);
BaseMap baseMap = new BaseMap();
baseMap.put("userId", "");
baseMap.put("message", message);
customRedisClient.sendMessage(REDIS_TOPIC_NAME, baseMap);
}
/**
* 此为单点消息
*
* @param userId
* @param message
*/
public void sendMessage(String userId, String message) {
BaseMap baseMap = new BaseMap();
baseMap.put("userId", userId);
baseMap.put("message", message);
customRedisClient.sendMessage(REDIS_TOPIC_NAME, baseMap);
}
/**
* 此为单点消息(多人)
*
* @param userIds
* @param message
*/
public void sendMessage(String[] userIds, String message) {
for (String userId : userIds) {
sendMessage(userId, message);
}
}
}