Java实习开发支付总结

一.思路
  1. 无论开发什么功能(至少以目前我所能接受的任务来说)先以完成主要功能为主
  2. 在测试正常的操作流程没有问题的情况下,来进行细节上的调整
  3. 正常的实现功能之后,会发现一些需要解决的问题,由此问题引发出了一些解决问题的技术栈
  4. 技术栈出现的背景:如在多人操作支付的情况下,因该将支付应用于异步操作,提高整体可用性
  5. 系统出现故障的导致消息丢失,MQ的作用即是解决该问题的一种方式,保挣消息的可靠性
  6. 支付是异步实现的方式,最终支付的信息(即消息回调)会在最终支付流程完成后,自动回调
  7. 后端根据回调的信息对订单信息进行相应的操作,如:支付成功、支付失败、订单超时等
  8. 支付有订单信息的查询接口,可以通过自动定时任务进行查询,根据信息对相应订单信息进行操作
  9. 定时任务会消耗系统的线程等资源,MQ的延迟队列则更具高性能(用于超时订单的信息操作)
    时序图在这里插入图片描述
二.支付技术栈

RabbitMQ(我所使用的)

1.特点
  • 高可用性
  • 灵活的消息路由
  • 支持多种协议
  • 消息持久化
  • 消息确认机制
  • 分布式支持
2.RabbitMQ 的组件
  • Producer(生产者)
  • Consumer(消费者)
  • Exchange(交换机)
    • Direct Exchange:将消息按路由键直接路由到一个队列。(90%场景适用)
    • Fanout Exchange:将消息广播到所有绑定的队列中。
    • Topic Exchange:根据路由键进行模糊匹配路由,适合需要复杂路由的场景。
    • Headers Exchange:根据消息头部的属性进行匹配路由。
  • Queue(队列)
  • Binding(绑定)
3.工作原理(简要描述)
  1. 生产者发送消息:生产者将消息发送到交换机,指定路由键。
  2. 交换机路由消息:交换机根据消息的路由键将消息路由到一个或多个队列。
  3. 队列存储消息:消息被存储在队列中,直到消费者从队列中取出并进行处理。
  4. 消费者处理消息:消费者从队列中取出消息,进行处理后发送确认(acknowledge),表示该消息已经成功消费。
  5. 消息确认:消费者通过确认机制通知 RabbitMQ 消息已经被正确处理。如果没有确认,RabbitMQ 会将消息重新发送到其他消费者。
    Publish/Subscribe
    在这里插入图片描述
三.支付
1.使用框架

JeePay:https://docs.jeequan.com/docs/jeepay/api_rule

2.使用原因

接入多平台支付,一个接口搞定所有支付平台

3.配置(根据各个官方配置来)

设计配置表,不同厂商的数据不同,但是接口参数大同小异,具体更具相关的文档进行获取配置

4.实操(步骤)
  1. 创建请求体,最直观的方式即:copy测试demo,修改参数发送请求
  2. 进行数的请求发送,获取返回的数据
  • 进行订单的创建(进行逻辑判定)
  • 保证订单的可靠性
  • 订单状态的修改
    ⅰ. 下单
    ⅱ. 待支付
    ⅲ. 待生成
    ⅳ. …
  1. 接收支付回调信息,支付是异步流程(手机支付后需要等待一段时间才会通知扣费,就是这有一原因)
    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);
        }
    }
}
四.退款

总体上,步骤和支付流程大同小异

  1. 需求
    用户申请退款之后,需要由管理员来进行审核,通过之后才能退款
  2. 需求分析
    为了减少人工操作的介入,避免操作失误等
  • 用户申请退款时,进行相关逻辑判断(如退款金额,金额是否以及充值如表,请求合法性等)
  • 确保管理员只需审核退款原因
  • 确保退款的可靠性
  • 用户退款申请,就是在客户端生成一个数据保存,设置为申请退款状态,然后发送通知给管理员,进行退款审核操作
  • 管理员的退款操作,其实才是真正退款的操作(即真正向支付商端发送退款请求)
五.消息通知

使用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);
        }
    }

}

Jeepay是一套适合互联网企业使用的开源支付系统,支持多渠道服务商和普通商户模式。已对接微信支付支付宝,云闪付官方接口,支持聚合码支付。 Jeepay使用Spring Boot和Ant Design Vue开发,集成Spring Security实现权限管理功能,是一套非常实用的web开发框架。 Jeepay = Jee + pay,是由原XxPay支付系统作者带领团队开发,“Jee”是公司计全科技名称的表示,pay表示支付。中文名称为计全支付,释为:计出万全、支付安全,让支付更加方便安全。 项目特点: 支持多渠道对接,支付网关自动路由 已对接微信服务商和普通商户接口,支持V2和V3接口 已对接支付宝服务商和普通商户接口,支持RSA和RSA2签名 已对接云闪付服务商接口,可选择多家支付机构 提供http形式接口,提供各语言的sdk实现,方便对接 接口请求和响应数据采用签名机制,保证交易安全可靠 系统安全,支持分布式部署,高并发 管理端包括运营平台和商户系统 管理平台操作界面简洁、易用 支付平台到商户系统的订单通知使用MQ实现,保证了高可用,消息可达 支付渠道的接口参数配置界面自动化生成 使用spring security实现权限管理 前后端分离架构,方便二次开发 由原XxPay团队开发,有着多年支付系统开发经验 Jeepay开源支付系统 更新日志: v1.1.0 增加发起退款,查询退款,退款回调接口 增加微信、支付宝、云闪付通道的退款 增加商户多应用管理 增加操作员删除,重置密码功能 增加商户系统操作员删除,重置密码功能 优化支付API接口(商户应用支持) 兼容Mysql8.0版本 优化部分功能数据列表权限 修复一些已知Bug
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值