小柒味来-订单支付实现

一、订单支付

1.1 微信支付介绍

1.1.1 注册商户号

要使用微信支付,必须注册微信支付商户号,并且:

  • 必须是企业
  • 拥有正规营业执照
  • 通过资质审核后,才能开通支付权限

当然,大部分人是没有的(我也没有QAQ)

  • 了解微信支付流程
  • 阅读微信官方接口文档
  • 能与第三方支付平台对接(蓝兔支付)

1.1.2 微信支付类型

本项目选择 小程序支付
参考:微信支付产品中心


1.1.3 微信支付接入流程

1.1.4 微信小程序支付时序图


1.1.5 微信支付相关接口

  1. JSAPI 下单
    商户系统调用此接口,在微信支付服务后台生成预支付交易单

    JSAPI/小程序下单接口文档

  2. 微信小程序调起支付

    • 通过 JSAPI 下单接口获取 prepay_id
    • 使用微信提供的小程序方法调起支付

    小程序调起支付接口文档


1.2 微信支付准备工作

1.2.1 如何保证数据安全?

微信支付有两个关键步骤:

  1. 系统调用微信下单接口 → 生成预支付交易单
  2. 支付成功后,微信后台推送支付结果给系统

这两个接口的数据安全性要求很高。

  • 微信通过加密、解密、签名保证安全
  • 需要提前准备:
    • 微信支付平台证书
    • 商户私钥文件

1.2.2 如何调用到商户系统?

  • 微信后台推送支付结果,本质上是 HTTP 请求
  • 必须保证系统可被访问
  • 如果有公网 IP(云服务器)可直接回调
  • 没有公网 IP,可用内网穿透工具(文末会介绍)

1.3 功能实现

1.3.1 微信支付配置

application.yml

  wechat:
    appid: ${qi.wechat.appid}
    secret: ${qi.wechat.secret}
    mchid: ${qi.wechat.mchid}
    mch-serial-no: ${qi.wechat.mch-serial-no}
    private-key-file-path: ${qi.wechat.private-key-file-path}
    api-v3-key: ${qi.wechat.api-v3-key}
    we-chat-pay-cert-file-path: ${qi.wechat.we-chat-pay-cert-file-path}
    notify-url: ${qi.wechat.notify-url}
    refund-notify-url: ${qi.wechat.refund-notify-url}

WeChatProperties.java

@Component
@ConfigurationProperties(prefix = "qi.wechat")
@Data
public class WeChatProperties {
    private String appid;
    private String secret;
    private String mchid;
    private String mchSerialNo;
    private String privateKeyFilePath;
    private String apiV3Key;
    private String weChatPayCertFilePath;
    private String notifyUrl;
    private String refundNotifyUrl;
}

1.3.2 Controller 层

OrderController.java

@PutMapping("/payment")
@ApiOperation("订单支付")
public Result<OrderPaymentVO> payment(@RequestBody OrdersPaymentDTO ordersPaymentDTO) throws Exception {
    log.info("订单支付:{}", ordersPaymentDTO);
    OrderPaymentVO orderPaymentVO = orderService.payment(ordersPaymentDTO);
    log.info("生成预支付交易单:{}", orderPaymentVO);
    return Result.success(orderPaymentVO);
}

PayNotifyController.java(支付回调)

@RestController
@RequestMapping("/notify")
@Slf4j
public class PayNotifyController {
    @Autowired
    private OrderService orderService;
    @Autowired
    private WeChatProperties weChatProperties;

    /**
     * 支付成功回调
     *
     * @param request
     */
    @RequestMapping("/paySuccess")
    public void paySuccessNotify(HttpServletRequest request, HttpServletResponse response) throws Exception {
        //读取数据
        String body = readData(request);
        log.info("支付成功回调:{}", body);

        //数据解密
        String plainText = decryptData(body);
        log.info("解密后的文本:{}", plainText);

        JSONObject jsonObject = JSON.parseObject(plainText);
        String outTradeNo = jsonObject.getString("out_trade_no");//商户平台订单号
        String transactionId = jsonObject.getString("transaction_id");//微信支付交易号

        log.info("商户平台订单号:{}", outTradeNo);
        log.info("微信支付交易号:{}", transactionId);

        //业务处理,修改订单状态、来单提醒
        orderService.paySuccess(outTradeNo);

        //给微信响应
        responseToWeixin(response);
    }

    /**
     * 读取数据
     *
     * @param request
     * @return
     * @throws Exception
     */
    private String readData(HttpServletRequest request) throws Exception {
        BufferedReader reader = request.getReader();
        StringBuilder result = new StringBuilder();
        String line = null;
        while ((line = reader.readLine()) != null) {
            if (result.length() > 0) {
                result.append("\n");
            }
            result.append(line);
        }
        return result.toString();
    }

    /**
     * 数据解密
     *
     * @param body
     * @return
     * @throws Exception
     */
    private String decryptData(String body) throws Exception {
        JSONObject resultObject = JSON.parseObject(body);
        JSONObject resource = resultObject.getJSONObject("resource");
        String ciphertext = resource.getString("ciphertext");
        String nonce = resource.getString("nonce");
        String associatedData = resource.getString("associated_data");

        AesUtil aesUtil = new AesUtil(weChatProperties.getApiV3Key().getBytes(StandardCharsets.UTF_8));
        //密文解密
        String plainText = aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8),
                nonce.getBytes(StandardCharsets.UTF_8),
                ciphertext);

        return plainText;
    }

    /**
     * 给微信响应
     * @param response
     */
    private void responseToWeixin(HttpServletResponse response) throws Exception{
        response.setStatus(200);
        HashMap<Object, Object> map = new HashMap<>();
        map.put("code", "SUCCESS");
        map.put("message", "SUCCESS");
        response.setHeader("Content-type", ContentType.APPLICATION_JSON.toString());
        response.getOutputStream().write(JSONUtils.toJSONString(map).getBytes(StandardCharsets.UTF_8));
        response.flushBuffer();
    }
}

1.3.3 Service 层

OrderService.java

OrderPaymentVO payment(OrdersPaymentDTO ordersPaymentDTO) throws Exception;
void paySuccess(String outTradeNo);

1.3.4 Service 实现类

OrderServiceImpl.java

@Autowired
private UserMapper userMapper;
@Autowired
private WeChatPayUtil weChatPayUtil;

public OrderPaymentVO payment(OrdersPaymentDTO ordersPaymentDTO) throws Exception {
    Long userId = BaseContext.getCurrentId();
    User user = userMapper.getById(userId);

    JSONObject jsonObject = weChatPayUtil.pay(
        ordersPaymentDTO.getOrderNumber(),
        new BigDecimal(0.01),
        "小柒味来订单",
        user.getOpenid()
    );

    if ("ORDERPAID".equals(jsonObject.getString("code"))) {
        throw new OrderBusinessException("该订单已支付");
    }

    OrderPaymentVO vo = jsonObject.toJavaObject(OrderPaymentVO.class);
    vo.setPackageStr(jsonObject.getString("package"));
    return vo;
}

public void paySuccess(String outTradeNo) {
    Long userId = BaseContext.getCurrentId();
    Orders ordersDB = orderMapper.getByNumberAndUserId(outTradeNo, userId);

    Orders orders = Orders.builder()
        .id(ordersDB.getId())
        .status(Orders.TO_BE_CONFIRMED)
        .payStatus(Orders.PAID)
        .checkoutTime(LocalDateTime.now())
        .build();

    orderMapper.update(orders);
}

1.3.4 Mapper 层

OrderMapper.java

@Select("select * from orders where number = #{orderNumber} and user_id= #{userId}")
Orders getByNumberAndUserId(String orderNumber, Long userId);

void update(Orders orders);

OrderMapper.xml

<update id="update" parameterType="com.qi.entity.Orders">
        update orders
        <set>
            <if test="cancelReason != null and cancelReason!='' ">
                cancel_reason=#{cancelReason},
            </if>
            <if test="rejectionReason != null and rejectionReason!='' ">
                rejection_reason=#{rejectionReason},
            </if>
            <if test="cancelTime != null">
                cancel_time=#{cancelTime},
            </if>
            <if test="payStatus != null">
                pay_status=#{payStatus},
            </if>
            <if test="payMethod != null">
                pay_method=#{payMethod},
            </if>
            <if test="checkoutTime != null">
                checkout_time=#{checkoutTime},
            </if>
            <if test="status != null">
                status = #{status},
            </if>
            <if test="deliveryTime != null">
                delivery_time = #{deliveryTime}
            </if>
        </set>
        where id = #{id}
</update>

1.4 联调测试

  • 去支付
  • 扫码支付即可

二、知识点补充

Q1:内网穿透解决方案

为使微信后台能够访问商户系统,需要将本地服务映射到公网。
我们可以通过 cpolar 软件可以获取一个临时域名,该域名对应一个公网 IP,从而实现微信后台对商户系统的访问。

1. 下载与安装

2. 配置 authtoken

  1. 登录 cpolar 控制台,复制个人 authtoken
  1. 在本地终端执行以下命令绑定 authtoken
cpolar.exe authtoken <你的_authtoken>

绑定成功后,cpolar 即可将本地端口映射为公网访问地址。

  1. 获取临时域名,执行命令:
cpolar.exe http 7030
  1. 得到域名:

Q2:我没有商户号又想体验支付流程怎么办?

蓝兔支付api


关于订单支付的功能就基本实现了,下一章小柒味来 - Spring Task 定时任务


欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 1701220998@qq.com
导航页 GitHub