一:前言
在很多项目中都会涉及到支付问题,我这里的项目是外卖项目,需要再用户点击菜品加入购物车进行结算时候使用支付功能,可以采用微信支付或者支付宝支付,由于微信支付比较普遍,在这里我选用的就是微信支付。首先要说明的是,要使用微信支付是有一定的要求的,你需要获取到你的商户证书,商户私钥,商户号等等,这些东西个人是很难获取的,只有商家才能申请,可以到B站尚硅谷下载他们的证书来使用,亲测可以直接用。微信支付总体来说开发并不难,就是繁琐,因为涉及到交易问题,处理逻辑一定要严谨,但是中间很多繁琐的工作微信已经给我们封装好了方法,直接查看接口文档调用即可,省去了中间一些验签的细节,大大提高了开发效率。这里附上微信支付开发文档入口地址。我使用的是微信的Native支付,该支付方式特点是商户系统按微信支付协议生成支付二维码,用户再用微信“扫一扫”完成支付的模式,当然如果你的是小程序的话可以使用小程序支付。
二:流程图
获取支付二维码并不难,难的地方在于如何对订单进行处理,当用户点击“去支付”按钮时候,这时候订单就已经生成了,需要将其存入数据库中,订单状态为未支付。这时候有一个问题,一般我们下单付款途径有两种,一种是通过购物车点击去支付直接进行付款,另一种是在购物车中点击去支付弹出付款界面之后点击取消,然后到“我的订单”界面进行支付,这种称为“锁单”。这两者有什么区别呢?区别无非就在于订单是否已经创建,当我们在购物车点击去支付在向微信服务器发送请求之前,这时候其实订单还没生成,前端传过来的参数并不是一个完整的订单信息,而是一些订单的相关信息,比如收货地址,订单备注等等,后台需要拿着这些参数生成一个订单。要注意的是,出于安全考虑,一般不会在前端将订单金额直接传到后端进行使用,因为这样有可能会在前端对金额进行修改。我这里采用的策略是前端将用户的购物车id传过来,然后统计其购物车金额。
说了那么多,要如何区分两种支付发起方式呢?其实很简单,前面说过两者区别就是订单有没有创建,在“我的订单”界面发起的支付请求是已经生成有订单信息了的,我们就可以通过判断前端穿过来的数据是否携带订单号来判断该支付请求是在哪发起的。假如订单号为空,那么该支付请求就是在购物车界面“去支付”处发起的,假如订单号不为空,那么该支付请求就是在“我的订单”处发起的。
这时候还要考虑另外一个问题,用户点击去支付按钮弹出支付二维码之后,用户可以点击空白处取消该弹窗,这时候假如用户多次这样反复操作,那么我们是否就需要针对用户的每一次点击都向微信支付服务器申请一个新的二维码呢?显然这是不需要的,只有当该支付二维码过期了或者用户重新选择商品才需要重新申请。为了解决这个问题,我采取的策略是将第一次获得的code_url存入redis,由于code_url的有效期为2小时,不妨就设置该code_url在redis的存活时间就为2小时。这样每次用户发起新请求时候先尝试在redis获取code_url,获取成功则直接返回,不需要再向微信支付后台服务器发送请求。这时候还有另外一个问题,这个问题我将在下面代码中讲解。
三:代码实现
1.Controller层
@PostMapping("/native")
@ApiOperation("调用统一下单API,生成支付二维码")
public R<Map> nativePay(@RequestBody Orders orders, HttpSession session) throws IOException {
log.info("发起支付请求");
Map body = wxPayService.nativePay(orders, session);
if(body != null) {
return R.success(body);
} else {
return R.error("获取支付信息失败!");
}
}
2.OrdersServiceImpl
@Override
@Transactional
public Orders createOrder(Orders orders, HttpSession session) {
Long userId = (Long) session.getAttribute("user");
Long addressBookId = orders.getAddressBookId();
AddressBook addressBook = addressBookService.getById(addressBookId);
if (addressBook == null) {
throw new CustomException("用户地址信息有误,不能下单");
}
LambdaQueryWrapper < ShoppingCart > SCLqw = new LambdaQueryWrapper <> ();
SCLqw.eq(ShoppingCart:: getUserId, userId);
List < ShoppingCart > shoppingCartList = shoppingCartService.list(SCLqw);
long orderId = IdWorker.getId();
orders.setNumber(String.valueOf(orderId));
orders.setStatus(1);
orders.setUserId(userId);
orders.setOrderTime(LocalDateTime.now());
orders.setCheckoutTime(LocalDateTime.now());
AtomicInteger amount = new AtomicInteger(0);
for (ShoppingCart shoppingCart : shoppingCartList) {
amount.addAndGet(shoppingCart.getAmount().multiply(new BigDecimal(100)).multiply(new BigDecimal(shoppingCart.getNumber())).intValue());
}
orders.setAmount(BigDecimal.valueOf(amount.get()));
User user = userService.getById(userId);
orders.setPhone(addressBook.getPhone());
orders.setAddress(addressBook.getDetail());
orders.setUserName(user.getPhone());
orders.setConsignee(addressBook.getConsignee());
this.save(orders);
List < OrderDetail > orderDetailList = new ArrayList <> ();
for (ShoppingCart shoppingCart : shoppingCartList) {
OrderDetail orderDetail = new OrderDetail();
orderDetail.setName(shoppingCart.getName());
orderDetail.setImage(shoppingCart.getImage());
orderDetail.setOrderId(orderId);
orderDetail.setDishId(shoppingCart.getDishId());
orderDetail.setSetmealId(shoppingCart.getSetmealId());
orderDetail.setDishFlavor(shoppingCart.getDishFlavor());
orderDetail.setNumber(shoppingCart.getNumber());
AtomicInteger detailAmount = new AtomicInteger(0);
detailAmount.addAndGet(shoppingCart.getAmount().multiply(new BigDecimal(shoppingCart.getNumber())).intValue());
orderDetail.setAmount(BigDecimal.valueOf(detailAmount.get()));
orderDetailList.add(orderDetail);
}
orderDetailService.saveBatch(orderDetailList);
shoppingCartService.remove(SCLqw);
return orders;
}
3.WxPayServiceImpl
@Override
public Map nativePay(Orders orders, HttpSession session) throws IOException {
Long userId = (Long) session.getAttribute("user");
Map < String, String > map = new HashMap <> ();
LambdaQueryWrapper < ShoppingCart > SCLqw = new LambdaQueryWrapper <> ();
SCLqw.eq(ShoppingCart:: getUserId, userId);
List < ShoppingCart > shoppingCartList = shoppingCartService.list(SCLqw);
String number = orders.getNumber();
Orders order = new Orders();
if (shoppingCartList.size() == 0 && number == null) {
String code_url = (String) redisTemplate.opsForValue().get("codeUrl");
String order_id = (String) redisTemplate.opsForValue().get("orderId");
if (code_url != null && order_id != null) {
map.put("codeUrl", code_url);
map.put("orderId", order_id);
return map;
}
if (order_id == null && code_url != null) {
map.put("message", "timeout");
return map;
}
}
if (number != null) {
String key = "orderNo_" + number + "_codeUrl";
String codeUrl = (String) redisTemplate.opsForValue().get(key);
if (codeUrl != null) {
map.put("codeUrl", codeUrl);
map.put("orderId", number);
return map;
}
order = orders;
} else {
order = ordersService.createOrder(orders, session);
}
redisTemplate.opsForValue().set("orderId", order.getNumber(), 5, TimeUnit.MINUTES);
map.put("orderId", order.getNumber());
log.info("计算支付金额...");
LambdaQueryWrapper < Orders > lqw = new LambdaQueryWrapper <> ();
lqw.eq(Orders:: getNumber, order.getNumber());
Orders order_getAmount = ordersService.getOne(lqw);
BigDecimal amount = order_getAmount.getAmount();
int PayAmount = amount.intValue();
HttpPost httpPost = new HttpPost(wxPayConfig.getDomain().concat(WxApiType.NATIVE_PAY.getType()));
httpPost.addHeader("Accept", "application/json");
httpPost.addHeader("Content-type", "application/json; charset=utf-8");
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectMapper objectMapper = new ObjectMapper();
ObjectNode rootNode = objectMapper.createObjectNode();
rootNode.put("mchid", wxPayConfig.getMchId())
.put("appid", wxPayConfig.getAppid())
.put("description", "外卖订单")
.put("notify_url", wxPayConfig.getNotifyDomain().concat(WxNotifyType.NATIVE_NOTIFY.getType()))
.put("out_trade_no", orders.getNumber());
rootNode.putObject("amount")
.put("total", PayAmount);
objectMapper.writeValue(bos, rootNode);
httpPost.setEntity(new StringEntity(bos.toString(StandardCharsets.UTF_8), "UTF-8"));
CloseableHttpResponse response = wxPayClient.execute(httpPost);
String bodyAsString = EntityUtils.toString(response.getEntity());
JSONObject body = JSON.parseObject(bodyAsString);
String code_url = (String) body.get("code_url");
map.put("codeUrl", code_url);
redisTemplate.opsForValue().set("codeUrl", code_url, 2, TimeUnit.HOURS);
redisTemplate.opsForValue().set("orderNo_" + order.getNumber() + "_codeUrl", code_url, 2, TimeUnit.HOURS);
log.info("Code_url===>{}", code_url);
return map;
}
可以看到,在判断订单号是否为空的 if(shoppingCartList.size() = = 0 && number = = null)语句中加入了shoppingCartList.size() == 0条件,为什么要加入这个条件呢?试想下我们将code_url存入redis中会存在什么问题,redis中code_url是有存活时间的,假如用户知道这个漏洞,买了一个1分钱的商品下单之后没付款,然后退出页面挑选了一个999大洋的商品下单,这时候加入判定条件是if(number = = null),这时候该条件肯定成立,因为用户是在购物车界面发起的支付请求,这时候压根没有订单号,那么问题就来了,这时候继续执行if里面的代码
String code_url = (String) redisTemplate.opsForValue().get("codeUrl");
String order_id = (String) redisTemplate.opsForValue().get("orderId");
if(code_url != null && order_id != null){
map.put("codeUrl",code_url);
map.put("orderId",order_id);
return map;
}
这时候获取到的还是上一次1分钱商品的支付码呀!这样聪明的用户不就可以支付1分钱获得价值999大洋的商品了吗,显然这样资本家是不同意的。如何解决这个问题呢?首先肯定是不能将redis取消掉的,万一取消掉之后有闲着没事干的用户要采用某些技术频繁发送支付请求就是不支付呢?这样微信那边不就禁了你的账号。这时候就要思考,当用户在购物车界面发起支付请求还携带有哪些信息可以利用到呢?没错,就是购物车数据,我们通过购物车去结算当你支付完成之后你会发现购物车原本的商品会被清空,出于严谨考虑我这也会实现这一功能。购物车是什么时候被清空的呢,看程序,只有在订单创建完成之后购物车才会清空,而订单创建的前提又是订单号为空并且能够成功在redis中获取支付URL,当用户想利用这个漏洞时候前面两个条件肯定是符合的,那么我们就正好可以利用这两个条件来修复漏洞。实现过程很简单,也就仅仅加了一个判断用户购物车数据是否为空,当用户购物车为空并且能够成功获取支付URL时候将code_url返回即可。当用户购物车不为空,说明用户更换了商品,这时候就需要向微信支付服务端发送新的请求获取新的支付URL。
至此,微信支付的用户下单功能已完成,至于如何在前端将Code_url转换成二维码展示给用户,可以查看我这篇文章附上链接。
项目完整代码(已优化):这是一个链接
|