前记

这里主要是记录一下最近小程序开发支付和退款功能,主要还是实践为主。本文没有涉及 mch-id、mch-key和商户证书(用来退款)的申请和下载。相信你做小程序开发一定知道他们什么和怎么申请。

其实遇到最大的坑是微信退款的证书在相对路径的还是绝对路径下的问题,先描述一下这个坑,这个坑在下面解决。我们使用的是阿里的SAE部署,所以证书文件我是采用的相对路径存储

1.绝对路径:如果你不是容器化部署(不是使用docker镜像或者阿里的SAE),yml文件中key-path你就直接在服务器或者本地的绝对路径。

2.相对路径:你的商户证书是在资源路径下,比如我的在resources/CRT/ 下,那么你会碰到打包后上传到服务器你的配置文件读取不到的问题。

下面开始怎么实现。主要流程还是微信官网的介绍,贴一张图

地址:https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=7_4&index=3

首先贴一下订单服务的结构,因为我们是微服务架构,所以我就贴一个订单服务的。

1.在pom文件中引入相关的依赖

<dependency>
    <groupId>com.github.binarywang</groupId>
    <artifactId>weixin-java-pay</artifactId>
    <exclusions>
        <exclusion>
            <artifactId>qrcode-utils</artifactId>
            <groupId>com.github.binarywang</groupId>
        </exclusion>
    </exclusions>
    <version>3.7.0</version>
</dependency>

<dependency>
    <groupId>com.github.binarywang</groupId>
    <artifactId>weixin-java-miniapp</artifactId>
    <version>3.3.0</version>
</dependency>

2.yum文件配置

在yml文件中配置小程序相关配置,当然如果你的项目用的是properties配置文件,自行修改一下。

blade:
    wx:
        miniapp:
          configs:
            - appid: *** (必须)
              secret: ** (必须)
              token: #微信小程序消息服务器配置的token
              aesKey: #微信小程序消息服务器配置的EncodingAESKey
              msgDataFormat: JSON
              mch-id: ** (必须)
              mch-key: ** (必须)
              notify-url: https://***/payNotify (必须 支付成功后微信回调的接口)
              key-path:/**/CRT/apiclient_cert.p12 (退款必须。微信用来双向校验的。这里在开发的时候碰到相对路径和绝对路径的问题,会在后面详细描述)

3. 将配置文件属性封装为对象

使用@Configuration和@ConfigurationProperties将配置文件封装为一个对象,并在springboot启动的初始化对象,主要是方便获取。主要涉及到的是前面项目结构config包下的两个类WxConfig.class和WxProperties.class。

WxProperties.class。这个类的封装结构是根据配置文件中的结构定义的,自行定义就好。

@Configuration
@ConfigurationProperties(prefix = "blade.wx.miniapp")
@Data
public class WxProperties {

    private List<Config> configs;

    @Data
    public static class Config {
        private String appSecret;

        /**
         * 设置微信小程序的appid
         */
        private String appid;

        /**
         * 设置微信小程序的Secret
         */
        private String secret;

        /**
         * 设置微信小程序消息服务器配置的token
         */
        private String token;

        /**
         * 设置微信小程序消息服务器配置的EncodingAESKey
         */
        private String aesKey;

        /**
         * 消息格式,XML或者JSON
         */
        private String msgDataFormat;

        /**
         * 商户id
         */
        private String mchId;

        /**
         * 商户id密钥
         */
        private String mchKey;

        /**
         * 微信支付回调地址,当微信支付成功或者失败
         */
        private String notifyUrl;

        private String keyPath;
    }
}

WxConfig.class。这里有个很重的问题需要说一下。就是开头提到的相对路径和绝对路径的问题,先看代码。

@Configuration
public class WxConfig {

    private static final Logger logger = LoggerFactory.getLogger(WxConfig.class);

    @Autowired
    private WxProperties properties;

    @Bean
    public WxMaConfig wxMaConfig() {
        List<WxProperties.Config> configs = properties.getConfigs();
        WxMaInMemoryConfig config = new WxMaInMemoryConfig();
        config.setAppid(configs.get(0).getAppid());
        config.setSecret(configs.get(0).getAppSecret());
        return config;
    }


    @Bean
    public WxMaService wxMaService(WxMaConfig maConfig) {
        WxMaService service = new WxMaServiceImpl();
        service.setWxMaConfig(maConfig);
        return service;
    }

    @Bean
    public WxPayConfig wxPayConfig() {
        List<WxProperties.Config> configs = properties.getConfigs();
        WxPayConfig payConfig = new WxPayConfig();
        payConfig.setAppId(configs.get(0).getAppid());
        payConfig.setMchId(configs.get(0).getMchId());
        payConfig.setMchKey(configs.get(0).getMchKey());
        payConfig.setNotifyUrl(configs.get(0).getNotifyUrl());
        payConfig.setKeyContent(getCertStream());           //如果你的证书是相对路径,那自己主动去读取p12证书  如果是绝对路径,此配置可以不设置 注释掉
        payConfig.setKeyPath(configs.get(0).getKeyPath());  //如果是绝对路径,那就直接配置本地路径或者服务器上证书的绝对路径
        payConfig.setTradeType("JSAPI");
        payConfig.setSignType("MD5");
        return payConfig;
    }


    @Bean
    public WxPayService wxPayService(WxPayConfig payConfig) {
        WxPayService wxPayService = new WxPayServiceImpl();
        wxPayService.setConfig(payConfig);
        return wxPayService;
    }
    /**
     * 获取p12证书文件内容的字节数组
     * 在相对路径下需要获取p12证书的内容
     * @return
     */
    public byte[] getCertStream() {
        byte[] certBis = null;
        try {
            InputStream certStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("CRT/apiclient_cert.p12");
            certBis = IOUtils.toByteArray(certStream);
            certStream.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return certBis;
    }
}

我的p12商户证书文件是在相对路径下,在我在开发的时候碰到的问题是,读取不到证书文件。

查看支付源码:

所以就有了我们在上面使用getCertStream()方法获取证书内容。并在创建WxPayConfig对象的时候将其设置进去。至此,证书相对路径读取不到证书文件的问题就解决了。

/**
	 * 获取p12证书文件内容的字节数组
	 * 在相对路径下需要获取p12证书的内容
	 * @return
	 */
	public byte[] getCertStream() {
		byte[] certBis = null;
		try {
			InputStream certStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("CRT/apiclient_cert.p12");
			certBis = IOUtils.toByteArray(certStream);
			certStream.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
		return certBis;
	}

4.主要代码

下面就是比较简单的控制层和服务层的代码了。支付和退款的我写在一起了,就不分开描述了。官网描述的流程所涉及的方法都在下面了。(注:凡事涉及到的业务代码忽略掉就行了)

4.1contrller层:

    @PostMapping("/submitOrder")
    @ApiOperationSupport(order = 8)
    @ApiOperation(value = "下订单", notes = "订单实体")
    public R submitOrder(@RequestBody OrderDTO orderDTO){
        return invoiceService.submitOrder(orderDTO);
    }

    /**
     * 付款订单的预支付会话标识
     * <p>
     * 1. 检测当前订单是否能够付款
     * 2. 微信商户平台返回支付订单ID
     * 3. 设置订单付款状态
     * @param userId
     * @param orderSn 订单id
     * @param request
     * @return 支付订单ID
     */
    @PostMapping("/prePay")
    @ApiOperationSupport(order = 9)
    @ApiOperation(value = "预支付", notes = "预支付")
    public R prePay(Long userId, String orderSn, HttpServletRequest request){
        return invoiceService.prePay(userId,orderSn,request);
    }

    /**
     * 微信付款成功或失败回调接口
     * <p>
     * 1. 检测当前订单是否是付款状态;
     * 2. 设置订单付款成功状态相关信息;
     * 3. 响应微信商户平台.
     *
     * @return 操作结果
     */
    @PostMapping("/payNotify")
    @ApiOperationSupport(order = 10)
    @ApiOperation(value = "预约专家支付成功或者失败接口", notes = "回调接口")
    public R payNotify(@RequestBody String xmlData){
        return invoiceService.payNotify(xmlData);
    }
    @PostMapping("/refund")
	@ApiOperationSupport(order = 14)
	@ApiOperation(value = "退款", notes = "传入当前用户userId和订单id")
	public R refund(Long userId, String orderSn){
		return invoiceService.refund(userId,orderSn);
	}

4.2 sevice层:

@Override
    @Transactional(rollbackFor = Exception.class)
    public R submitOrder(OrderDTO orderDTO) {

        if (orderDTO == null) {
            return R.fail("参数值不能为空!");
        }
        ScheduleSlot scheduleSlot = scheduleSlotClient.selectScheduleSlotById(Func.toLong(orderDTO.getScheduleSlotId()));

        if (scheduleSlot == null) {
            return R.fail("当前时间不可用!");
        }
        if (scheduleSlot.getScheduleType() == 0) {
            if (scheduleSlot.getStatusSch() == 1) {//1 表示已经被预约
                return R.fail("当前时间短已经被预约!");
            }
        } else if (scheduleSlot.getScheduleType() == 1) {
            if (scheduleSlot.getSourceNum() < 1) {
                return R.fail("当前时间段号源已被预约完!");
            }
        }
        logger.info("订单参数信息!!!");
        logger.info(orderDTO.toString());
        synchronized (this) {
            //创建订单
            Invoice invoice = new Invoice();
            invoice.setStatus(101);
            invoice.setSubjectId(Func.toLong(orderDTO.getPatientId()));
            String orderSn = generateOrderSn(Func.toLong(orderDTO.getUserId()), 4);
            invoice.setOrderSn(orderSn);
            invoice.setScheduleSlotId(Func.toLong(orderDTO.getScheduleSlotId()));
            invoice.setRecipientId(Func.toLong(orderDTO.getUserId()));
            invoice.setPractitionerId(Func.toLong(orderDTO.getPractitionerId()));
            invoice.setLineItem(orderDTO.getLineItem());
            invoice.setDate(DateUtil.now());
            invoice.setType(orderDTO.getInvoiceType());//预约订单
            invoice.setTotalGross(orderDTO.getCharge());
            invoice.setCreateUser(Func.toLong(orderDTO.getUserId()));
            invoice.setCreateTime(DateUtil.now());
            invoice.setTenantId(orderDTO.getTenantId());
            invoice.setUpdateTime(DateUtil.now());
            boolean result = this.save(invoice);
            int orderNum = 0;
            if (result) {
                //查询当前预约序号
                if (orderDTO.getClinicClass() == 1) {//普通科室
                    orderNum = appointmentClient.selectLookNumByDeptId(Func.toLong(orderDTO.getDeptId()));
                } else if (orderDTO.getClinicClass() == 0) {//专家
                    orderNum = appointmentClient.selectCountAppointment(Func.toLong(orderDTO.getPractitionerId()));
                }
                //创建预约记录
                Appointment appointment = new Appointment();
                appointment.setCharge(orderDTO.getCharge());
                appointment.setClinicType(orderDTO.getClinicType());
                appointment.setDeptId(orderDTO.getDeptId());
                appointment.setOrgId(orderDTO.getOrgId());
                appointment.setPatientId(orderDTO.getPatientId());
                appointment.setStart(orderDTO.getStart());
                appointment.setScheduleType(scheduleSlot.getScheduleType());
                appointment.setOrderSn(orderSn);
                appointment.setEnd(orderDTO.getEnd());
                appointment.setPractitionerId(Func.toLong(orderDTO.getPractitionerId()));
                appointment.setClinicClass(orderDTO.getClinicClass());
                appointment.setParticipant(orderDTO.getParticipant());
                appointment.setCreateTime(DateUtil.now());
                appointment.setCreateTime(DateUtil.now());
                appointment.setOrderNum(orderNum + 1);
                appointment.setStatus(101);//已提交
                boolean res = appointmentClient.add(appointment);
                if (!res) {
                    throw new ServiceException("创建订单失败!");
                }
                //更新时间片状态为忙的状态-1  这个操作失败 上面创建预约记录的操作不会回滚
                if (scheduleSlot.getScheduleType() == 0) {//时间段预约
                    scheduleSlot.setStatusSch(1);
                    scheduleSlot.setUpdateTime(DateUtil.now());
                    if (!scheduleSlotClient.saveOrupdate(scheduleSlot)) {
                        throw new ServiceException("创建订单,更新可预约时间状态失败!");
                    }
                } else if (scheduleSlot.getScheduleType() == 1) {//号源预约
                    //减少一个号源数量
                    int count = scheduleSlot.getSourceNum() - 1;
                    if (count == 0) {
                        scheduleSlot.setStatusSch(1);
                    }
                    scheduleSlot.setSourceNum(count);
                    if (!scheduleSlotClient.saveOrupdate(scheduleSlot)) {
                        throw new ServiceException("创建订单,更新可预约时间数量失败!");
                    }
                }
            }
            return R.data(invoice);
        }
    }


    @Override
    @Transactional(rollbackFor = Exception.class)
    public R prePay(Long userId, String orderSn, HttpServletRequest request) {
        if (userId == null || orderSn == null) {
            return R.fail("参数值不能为空!");
        }
        Invoice invoice = selectByUserIdAndOrderSn(userId, orderSn);
        if (!userId.equals(invoice.getCreateUser())) {
            return R.fail("订单信息错误!");
        }
        // 检测是否能够取消
        OrderHandleOption handleOption = OrderUtil.build(invoice);
        if (!handleOption.isPay()) {
            return R.fail("订单状态不能支付!");
        }
        User user = iUserClient.getUserByUserIdOnly(userId);
        if (user == null) {
            return R.fail("参数值错误");
        }
        String openId = user.getOpenId();
        if (openId == null) {
            return R.fail("订单不能支付!openId为空");
        }


        WxPayMpOrderResult result = null;
        try {
            WxPayUnifiedOrderRequest orderRequest = new WxPayUnifiedOrderRequest();
            orderRequest.setOutTradeNo(invoice.getOrderSn());
            orderRequest.setOpenid(openId);
            orderRequest.setBody("订单:" + invoice.getOrderSn());
            // 元转成分
            int fee = 0;
            BigDecimal actualPrice = invoice.getTotalGross();
            fee = actualPrice.multiply(new BigDecimal(100)).intValue();
            orderRequest.setTotalFee(fee);
            orderRequest.setSpbillCreateIp(IpUtil.getIpAddr(request));

            result = wxPayService.createOrder(orderRequest);
        } catch (Exception e) {
            e.printStackTrace();
            return R.fail("订单不能支付!");
        }
        return R.data(result);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public R refund(Long userId, String orderSn) {
        if (userId == null || orderSn == null) {
            return R.fail("参数不能为空!");
        }
        Invoice invoice = selectByUserIdAndOrderSn(userId, orderSn);

        if (invoice == null) {
            return R.fail("订单不存在");
        }
        if (!userId.equals(invoice.getCreateUser())) {
            return R.fail("订单信息错误!");
        }
        OrderHandleOption handleOption = OrderUtil.build(invoice);
        if (!handleOption.isRefund()) {
            return R.fail("订单不能退款!");
        }
        Appointment appointment = appointmentClient.selectAppointmentByOrderSn(orderSn);

        if (checkCancelTime(invoice, appointment)) {
            return R.fail("已经过了可退款时间!");
        }

        // 微信退款
        WxPayRefundRequest wxPayRefundRequest = new WxPayRefundRequest();
        wxPayRefundRequest.setOutTradeNo(invoice.getOrderSn());
        wxPayRefundRequest.setOutRefundNo("refund_" + invoice.getOrderSn());
        // 元转成分
        Integer totalFee = invoice.getTotalGross().multiply(new BigDecimal(100)).intValue();
        wxPayRefundRequest.setTotalFee(totalFee);
        wxPayRefundRequest.setRefundFee(totalFee);

        WxPayRefundResult wxPayRefundResult;
        try {
            wxPayRefundResult = wxPayService.refund(wxPayRefundRequest);
        } catch (WxPayException e) {
            logger.error(e.getMessage(), e);
            return R.fail("订单退款失败");
        }
        if (!wxPayRefundResult.getReturnCode().equals("SUCCESS")) {
            logger.warn("refund fail: " + wxPayRefundResult.getReturnMsg());
            return R.fail("订单退款失败");
        }
        if (!wxPayRefundResult.getResultCode().equals("SUCCESS")) {
            logger.warn("refund fail: " + wxPayRefundResult.getReturnMsg());
            return R.fail("订单退款失败");
        }

        // 设置订单取消状态
        Date now = DateUtil.now();
        invoice.setStatus(OrderUtil.STATUS_REFUND_CONFIRM);
        invoice.setEndTime(now);
        // 记录订单退款相关信息
        invoice.setRefundAmount(invoice.getTotalGross());
        invoice.setRefundType("微信退款接口");
        invoice.setRefundContent(wxPayRefundResult.getRefundId());
        invoice.setRefundTime(now);
        if (!updateWithOptimisticLocker(invoice)) {
            throw new RuntimeException("更新数据已失效");
        }
        //设置预约状态为取消 102
        if (appointment != null) {
            appointment.setStatus(102);
            appointment.setUpdateTime(DateUtil.now());
            logger.info("退款 appointment 信息 ");
            logger.info(appointment.toString());
            if (appointment.getPractitionerId() == null) {
                appointment.setPractitionerId(0L);
            }
            if (!appointmentClient.saveOrupdate(appointment)) {
                throw new ServiceException("订单退款失败,更新可预约状态失败!");
            }
        }
        updateScheduleSlot(invoice.getScheduleSlotId());
        return R.success("退款成功!");
    }

    /**
     * 微信付款成功或失败回调接口
     * <p>
     * 1. 检测当前订单是否是付款状态;
     * 2. 设置订单付款成功状态相关信息;
     * 3. 响应微信商户平台.
     *
     * @return 操作结果
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public R payNotify(String xmlData) {
        String xmlResult = xmlData;
        WxPayOrderNotifyResult result = null;
        try {
            logger.info("打印回传xml结果开始========");
            logger.info(xmlData);
            logger.info("打印回传xml结果结束=========");
            result = wxPayService.parseOrderNotifyResult(xmlResult);

            if (!WxPayConstants.ResultCode.SUCCESS.equals(result.getResultCode())) {
                logger.error(xmlResult);
                throw new WxPayException("微信通知支付失败!");
            }
            if (!WxPayConstants.ResultCode.SUCCESS.equals(result.getReturnCode())) {
                logger.error(xmlResult);
                throw new WxPayException("微信通知支付失败!");
            }
        } catch (WxPayException e) {
            e.printStackTrace();
            return R.fail(e.getMessage());
        }
        logger.info("处理腾讯支付平台的订单支付");
        logger.info(String.valueOf(result));
        String orderSn = result.getOutTradeNo();
        String payId = result.getTransactionId();

        // 分转化成元
        String totalFee = BaseWxPayResult.fenToYuan(result.getTotalFee());
        Invoice order = selectByUserIdAndOrderSn(null, orderSn);
        if (order == null) {
            return R.fail("订单不存在 sn=" + orderSn);
        }

        // 检查这个订单是否已经处理过
        if (OrderUtil.hasPayed(order)) {
            return R.fail("订单已经处理过了!");
        }
        // 检查支付订单金额
        if (!totalFee.equals(order.getTotalGross().toString())) {
            return R.fail(order.getOrderSn() + " : 支付金额不符合 totalFee=" + totalFee);
        }
        order.setPayId(payId);
        order.setPayTime(DateUtil.now());
        order.setStatus(OrderUtil.STATUS_PAY);
        if (!updateWithOptimisticLocker(order)) {
            return R.fail("更新数据已失效");
        }
        //TODO 发送邮件和短信通知,这里采用异步发送
        // 订单支付成功以后,会发送短信给用户,以及发送邮件给管理员
        return R.data("订单处理成功");
    }

至此,微信的支付和退款功能就开发完成。主要参考的是这一下两个开源项目的代码:

https://github.com/Wechat-Group/WxJava

https://github.com/linlinjava/litemall

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐