当前位置: 首页 > news >正文

苍穹外卖-day08:导入地址簿功能代码(单表crud)、用户下单(业务逻辑)、订单支付(业务逻辑,cpolar软件)

苍穹外卖-day08

课程内容

  • 导入地址簿功能代码
  • 用户下单
  • 订单支付

功能实现:用户下单订单支付

用户下单效果图:

在这里插入图片描述

订单支付效果图:

在这里插入图片描述

1. 导入地址簿功能代码(单表crud)

1.1 需求分析和设计

1.1.1 产品原型(业务功能和接口的关系)

地址簿,指的是消费者用户的地址信息,用户登录成功后可以维护自己的地址信息。同一个用户可以有多个地址信息,但是只能有一个默认地址

默认地址的作用:当用户下单的时候,默认就会使用这个地址。

效果图:

在这里插入图片描述

对于地址簿管理,我们需要实现以下几个功能:

  • 查询地址列表
  • 新增地址
  • 修改地址
  • 删除地址
  • 设置默认地址
  • 查询默认地址
    • 通过当前这个产品原型是看不出来这个功能的,实际上在用户下单的时候在下单页面,会把当前这个用户的默认地址给他查出来,查出来之后显示在我们提交订单那个页面上。

注意:

  • 这些业务功能和接口有的时候它并不是完全对应的
    • 比如:修改收货地址功能,在修改之前需要先把它原先的信息给它展示出来,所以修改这个功能一共会涉及到2个接口。
  • 业务功能和接口并不是百分百一一对应的,有些业务功能呢可能相对来说是比较复杂的,要完成这个业务功能可能就会涉及到多次接口的调用,那也就是有多个接口。
1.1.2 接口设计

根据上述原型图先粗粒度设计接口,共包含7个接口。

接口设计:

  • 新增地址
  • 查询登录用户所有地址
  • 查询默认地址
  • 根据id修改地址
  • 根据id删除地址
  • 根据id查询地址
  • 设置默认地址

接下来细粒度分析每个接口,明确每个接口的请求方式、请求路径、传入参数和返回值。

1). 新增地址

在这里插入图片描述

2). 查询登录用户所有地址

前端不需要传递参数,因为这个用户的id每次发送请求都会携带token,之后在拦截器中解析token在绑定给ThreadLocal获得。
在这里插入图片描述

3). 查询默认地址

同样不需要传递参数,因为我们是能够知道当前登录用户是谁的。

在这里插入图片描述

4). 修改地址

在这里插入图片描述

5). 根据id删除地址

在这里插入图片描述

6). 根据id查询地址

在这里插入图片描述

7). 设置默认地址

设置默认地址,本质是一个修改操作。
在这里插入图片描述

1.1.3 表设计

用户的地址信息会存储在address_book表,即地址簿表中。具体表结构如下:

字段名数据类型说明备注
idbigint主键自增
user_idbigint用户id逻辑外键
consigneevarchar(50)收货人
sexvarchar(2)性别
phonevarchar(11)手机号
province_codevarchar(12)省份编码
province_namevarchar(32)省份名称
city_codevarchar(12)城市编码
city_namevarchar(32)城市名称
district_codevarchar(12)区县编码
district_namevarchar(32)区县名称
detailvarchar(200)详细地址信息具体到门牌号
labelvarchar(100)标签公司、家、学校
is_defaulttinyint(1)是否默认地址1是 0否

这里面有一个字段is_default,实际上我们在设置默认地址时,只需要更新这个字段就可以了。

1.2 代码导入

对于这一类的单表的增删改查,我们已经写过很多了,基本的开发思路都是一样的,那么本小节的用户地址簿管理的增删改查功能,我们就不再一 一实现了,基本的代码我们都已经提供了,直接导入进来,做一个测试即可。

导入课程资料中的地址簿模块功能代码:

在这里插入图片描述

进入到sky-server模块中

1.2.1 Mapper层

创建AddressBookMapper.java

package com.sky.mapper;import com.sky.entity.AddressBook;
import org.apache.ibatis.annotations.*;
import java.util.List;@Mapper
public interface AddressBookMapper {/*** 条件查询* @param addressBook* @return*/List<AddressBook> list(AddressBook addressBook);/*** 新增* @param addressBook*/@Insert("insert into address_book" +"        (user_id, consignee, phone, sex, province_code, province_name, city_code, city_name, district_code," +"         district_name, detail, label, is_default)" +"        values (#{userId}, #{consignee}, #{phone}, #{sex}, #{provinceCode}, #{provinceName}, #{cityCode}, #{cityName}," +"                #{districtCode}, #{districtName}, #{detail}, #{label}, #{isDefault})")void insert(AddressBook addressBook);/*** 根据id查询* @param id* @return*/@Select("select * from address_book where id = #{id}")AddressBook getById(Long id);/*** 根据id修改* @param addressBook*/void update(AddressBook addressBook);/*** 根据 用户id修改 是否默认地址* @param addressBook*/@Update("update address_book set is_default = #{isDefault} where user_id = #{userId}")void updateIsDefaultByUserId(AddressBook addressBook);/*** 根据id删除地址* @param id*/@Delete("delete from address_book where id = #{id}")void deleteById(Long id);}

创建AddressBookMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.AddressBookMapper"><select id="list" parameterType="AddressBook" resultType="AddressBook">select * from address_book<where><if test="userId != null">and user_id = #{userId}</if><if test="phone != null">and phone = #{phone}</if><if test="isDefault != null">and is_default = #{isDefault}</if></where></select><update id="update" parameterType="addressBook">update address_book<set><if test="consignee != null">consignee = #{consignee},</if><if test="sex != null">sex = #{sex},</if><if test="phone != null">phone = #{phone},</if><if test="detail != null">detail = #{detail},</if><if test="label != null">label = #{label},</if><if test="isDefault != null">is_default = #{isDefault},</if></set>where id = #{id}</update></mapper>
1.2.2 Service层

创建AddressBookService.java

package com.sky.service;import com.sky.entity.AddressBook;
import java.util.List;public interface AddressBookService {List<AddressBook> list(AddressBook addressBook);void save(AddressBook addressBook);AddressBook getById(Long id);void update(AddressBook addressBook);void setDefault(AddressBook addressBook);void deleteById(Long id);}

创建AddressBookServiceImpl.java

package com.sky.service.impl;import com.sky.context.BaseContext;
import com.sky.entity.AddressBook;
import com.sky.mapper.AddressBookMapper;
import com.sky.service.AddressBookService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;@Service
@Slf4j
public class AddressBookServiceImpl implements AddressBookService {@Autowiredprivate AddressBookMapper addressBookMapper;/*** 条件查询** @param addressBook* @return*/public List<AddressBook> list(AddressBook addressBook) {return addressBookMapper.list(addressBook);}/*** 新增地址** @param addressBook*/public void save(AddressBook addressBook) {addressBook.setUserId(BaseContext.getCurrentId());//新增的地址都不是默认地址,所以设置为0,如何你想要设置默认地址需要去选择那个单选按钮,//    把那个地址给它设置为默认地址。addressBook.setIsDefault(0);addressBookMapper.insert(addressBook);}/*** 根据id查询** @param id* @return*/public AddressBook getById(Long id) {AddressBook addressBook = addressBookMapper.getById(id);return addressBook;}/*** 根据id修改地址** @param addressBook*/public void update(AddressBook addressBook) {addressBookMapper.update(addressBook);}/*** 设置默认地址:*    分析:对于同一个用户它的所有地址来说,只能有一个是默认地址,现在提交过来了一个,*        那在我这次设置默认地址之前,可能用户已经有了一个默认的地址,现在又设置了一个默认地址*        那原先的默认地址就应该不再是默认地址了,所以需要先把当前用户的所有地址都给它改成不是*        默认的,然后在修改当前地址给它改成是一个默认的。** @param addressBook*/@Transactionalpublic void setDefault(AddressBook addressBook) {//1、将当前用户的所有地址修改为非默认地址 update address_book set is_default = ? where user_id = ?addressBook.setIsDefault(0);addressBook.setUserId(BaseContext.getCurrentId());addressBookMapper.updateIsDefaultByUserId(addressBook);//2、将当前地址改为默认地址 update address_book set is_default = ? where id = ?addressBook.setIsDefault(1);addressBookMapper.update(addressBook);}/*** 根据id删除地址** @param id*/public void deleteById(Long id) {addressBookMapper.deleteById(id);}}
1.2.3 Controller层

注意:是用户端的相关操作,所以应该放在user包下。

package com.sky.controller.user;import com.sky.context.BaseContext;
import com.sky.entity.AddressBook;
import com.sky.result.Result;
import com.sky.service.AddressBookService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;@RestController
@RequestMapping("/user/addressBook")
@Api(tags = "C端地址簿接口")
public class AddressBookController {@Autowiredprivate AddressBookService addressBookService;/*** 查询当前登录用户的所有地址信息** @return*/@GetMapping("/list")@ApiOperation("查询当前登录用户的所有地址信息")public Result<List<AddressBook>> list() {AddressBook addressBook = new AddressBook();addressBook.setUserId(BaseContext.getCurrentId());List<AddressBook> list = addressBookService.list(addressBook);return Result.success(list);}/*** 新增地址:新增的地址都不是默认地址* * @param addressBook* @return*/@PostMapping@ApiOperation("新增地址")public Result save(@RequestBody AddressBook addressBook) {addressBookService.save(addressBook);return Result.success();}@GetMapping("/{id}")@ApiOperation("根据id查询地址")public Result<AddressBook> getById(@PathVariable Long id) {AddressBook addressBook = addressBookService.getById(id);return Result.success(addressBook);}/*** 根据id修改地址** @param addressBook* @return*/@PutMapping@ApiOperation("根据id修改地址")public Result update(@RequestBody AddressBook addressBook) {addressBookService.update(addressBook);return Result.success();}/*** 设置默认地址** @param addressBook* @return*/@PutMapping("/default")@ApiOperation("设置默认地址")public Result setDefault(@RequestBody AddressBook addressBook) {addressBookService.setDefault(addressBook);return Result.success();}/*** 根据id删除地址** @param id* @return*/@DeleteMapping@ApiOperation("根据id删除地址")public Result deleteById(Long id) {addressBookService.deleteById(id);return Result.success();}/*** 查询默认地址*/@GetMapping("default")@ApiOperation("查询默认地址")public Result<AddressBook> getDefault() {//SQL:select * from address_book where user_id = ? and is_default = 1AddressBook addressBook = new AddressBook();addressBook.setIsDefault(1);addressBook.setUserId(BaseContext.getCurrentId());List<AddressBook> list = addressBookService.list(addressBook);//默认地址只有一个,所以查询的结果要么没有要么是只有1个。if (list != null && list.size() == 1) {return Result.success(list.get(0));}return Result.error("没有查询到默认地址");}}

1.3 功能测试

可以通过如下方式进行测试:

  • 查看控制台sql和数据库中的数据变化
  • Swagger接口文档测试
  • 前后端联调

我们直接使用前后端联调测试:

启动后台服务,编译小程序

登录进入首页–>进入个人中心–>进入地址管理

在这里插入图片描述

1). 新增收货地址

添加两条收货地址:

在这里插入图片描述

查看收货地址:

在这里插入图片描述

查看数据库:

在这里插入图片描述

2). 设置默认收货地址

设置默认地址:

在这里插入图片描述

查看数据库:

在这里插入图片描述

3). 删除收货地址

进行编辑:

在这里插入图片描述

删除地址:

在这里插入图片描述

查看数据库:

在这里插入图片描述

1.4 代码提交

在这里插入图片描述

后续步骤和其它功能代码提交一致,不再赘述。

2. 用户下单(业务逻辑)

2.1 需求分析和设计

2.1.1 产品原型

用户下单业务说明:

在电商系统中,用户是通过下单的方式通知商家,用户已经购买了商品,需要商家进行备货和发货。

用户下单后会产生订单相关数据,订单数据需要能够体现如下信息:

在这里插入图片描述
简单分析,用户下单后产生的订单相关的数据都是怎么来的:

  • 用户买的那些商品?每个商品数量是多少?
    • 这些数据来源于用户的购物车,因为点餐的时候用户是先将商品加入到购物车当中然后才能提交订单,所以用户买的哪些商品以及商品的数量是由购物车中的数据决定的,当前用户的购物车数据可以查出来,因为前面已经实现了购物车的相关功能了。
  • 订单总金额是通过程序计算得出来的,总金额由2部分组成,一个是菜品的费用一个是其它的费用。
    • 菜品费用很明显,我们点的有哪些菜品包括数量单价都有,直接乘以累加就可以计算出来。
    • 其它费用:餐盒费和配送费,具体如何计算在当前这个系统当中就简单的处理了,餐盒费呢咱们就根据商品的数量来算,一个商品对应一个餐盒,每个餐盒一元。配送费固定按照6元去算。
  • 用户的名字,收货地址,手机号:通过之前导入的地址簿功能代码,用户的地址簿中就包括用户的名字 手机号 收货地址这些数据,用户在下单的时候就需要选择一个地址簿,这样就可以获取到这些数据了。

用户点餐业务流程(效果图):

用户将菜品或者套餐加入购物车后,可以点击购物车中的 “去结算” 按钮,页面跳转到订单确认页面,点击 “去支付” 按钮则完成下单操作。

  1. 第一步:用户将菜品或者套餐加入购物车
  2. 第二步:用户点击去结算按钮跳转到提交订单这个页面,之后选择收货地址,在这个页面可以看到用户准备购买的商品明细、打包费、配送费等等这些信息。
  3. 第三步:用户点击去支付按钮跳转到订单支付页面,此时用户已经完成了下单 也就是说已经产生了订单数据,只不过这个订单状态是未支付的状态。
  4. 第四步:用户点击确认支付完成付款,如果付款成功小程序就会跳转到下单成功这个界面。
    在这里插入图片描述
2.1.2 接口设计

接口分析:

  • POST请求:用户下单本质上是新增操作,也就是需要将下单后产生的订单数据把它插入到数据库当中。
  • /user/order/submit:用户下单是用户端操作(/user作为前缀),/order代表的是对订单的操作,/submit表示提交,这样就可以做到见名之意。
  • 支付剩余时间倒计时,是由前端小程序按照秒递减,我们只需要给它返回一个下单的时间就可以了,小程序会根据这个下单时间去计算还剩多少时间并且进行倒计时的展示。
    在这里插入图片描述

接口设计:

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

2.1.3 表设计

说明:

  • 用户下单的时候一般都会有一个订单表,把当前用户下单的数据存入到订单这张表里面去,但是只有一种订单表还不足以存储所有的数据,因为用户下单的时候可能点了很多商品,每个商品可能还有很多份,这个时候在我们这个订单表里面并不是很好存,具体我这个订单下面挂了几个商品呢???具体点了几份???
  • 对于这样的数据我们一般都会存储在另一张表里面,叫做订单明细表。

用户下单业务对应的数据表为orders表和order_detail表(一对多关系,一个订单关联多个订单明细):

表名含义说明
orders订单表主要存储订单的基本信息(如: 订单号、状态、金额、支付方式、下单用户、收件地址等)
order_detail订单明细表主要存储订单详情信息(如: 该订单关联的套餐及菜品的信息)

具体的表结构如下:

1). orders订单表

字段名数据类型说明备注
idbigint主键自增
numbervarchar(50)订单号
statusint订单状态1待付款 2待接单 3已接单 4派送中 5已完成 6已取消
user_idbigint用户id逻辑外键
address_book_idbigint地址id逻辑外键
order_timedatetime下单时间
checkout_timedatetime付款时间
pay_methodint支付方式1微信支付 2支付宝支付
pay_statustinyint支付状态0未支付 1已支付 2退款
amountdecimal(10,2)订单金额
remarkvarchar(100)备注信息
phonevarchar(11)手机号冗余字段
addressvarchar(255)详细地址信息冗余字段
consigneevarchar(32)收货人冗余字段
cancel_reasonvarchar(255)订单取消原因
rejection_reasonvarchar(255)拒单原因
cancel_timedatetime订单取消时间
estimated_delivery_timedatetime预计送达时间
delivery_statustinyint配送状态1立即送出 0选择具体时间
delivery_timedatetime送达时间
pack_amountint打包费
tableware_numberint餐具数量
tableware_statustinyint餐具数量状态1按餐量提供 0选择具体数量

2). order_detail订单明细表

字段名数据类型说明备注
idbigint主键自增
namevarchar(32)商品名称冗余字段
imagevarchar(255)商品图片路径冗余字段
order_idbigint订单id逻辑外键
dish_idbigint菜品id逻辑外键
setmeal_idbigint套餐id逻辑外键
dish_flavorvarchar(50)菜品口味
numberint商品数量
amountdecimal(10,2)商品单价

说明:用户提交订单时,需要往订单表orders中插入一条记录,并且需要往order_detail中插入一条或多条记录。

2.2 代码开发

2.2.1 DTO设计

根据用户下单接口的参数设计DTO:

在这里插入图片描述

在sky-pojo模块,OrdersSubmitDTO.java已定义

package com.sky.dto;import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;@Data
public class OrdersSubmitDTO implements Serializable {//地址簿idprivate Long addressBookId;//付款方式private int payMethod;//备注private String remark;//预计送达时间@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")private LocalDateTime estimatedDeliveryTime;//配送状态  1立即送出  0选择具体时间private Integer deliveryStatus;//餐具数量private Integer tablewareNumber;//餐具数量状态  1按餐量提供  0选择具体数量private Integer tablewareStatus;//打包费private Integer packAmount;//总金额private BigDecimal amount;
}
2.2.2 VO设计

根据用户下单接口的返回结果设计VO:

在这里插入图片描述

在sky-pojo模块,OrderSubmitVO.java已定义

package com.sky.vo;import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OrderSubmitVO implements Serializable {//订单idprivate Long id;//订单号private String orderNumber;//订单金额private BigDecimal orderAmount;//下单时间private LocalDateTime orderTime;
}
2.2.3 Controller层

创建OrderController并提供用户下单方法:

用户端所以是在user包下

在这里插入图片描述

package com.sky.controller.user;import com.sky.dto.OrdersSubmitDTO;
import com.sky.result.Result;
import com.sky.service.OrderService;
import com.sky.vo.OrderSubmitVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;/*** 订单** 为什么还要重新指定一下这个bean的名字???* 因为后面再admin里面也需要创建一个OrderController,即在商家这端也需要*    对订单进行操作,所以为了防止它们重名了指定一个bean的名字。*/
@RestController("userOrderController")
@RequestMapping("/user/order")
@Slf4j
@Api(tags = "C端-订单接口")
public class OrderController {@Autowiredprivate OrderService orderService;/*** 用户下单** @param ordersSubmitDTO* @return*/@PostMapping("/submit")@ApiOperation("用户下单")public Result<OrderSubmitVO> submit(@RequestBody OrdersSubmitDTO ordersSubmitDTO) {log.info("用户下单:{}", ordersSubmitDTO);OrderSubmitVO orderSubmitVO = orderService.submitOrder(ordersSubmitDTO);return Result.success(orderSubmitVO);}}
2.2.4 Service层接口

创建OrderService接口,并声明用户下单方法:

package com.sky.service;import com.sky.dto.*;
import com.sky.vo.OrderSubmitVO;public interface OrderService {/*** 用户下单* @param ordersSubmitDTO* @return*/OrderSubmitVO submitOrder(OrdersSubmitDTO ordersSubmitDTO);
}
2.2.5 Service层实现类

创建OrderServiceImpl实现OrderService接口:

package com.sky.service.impl;import com.sky.constant.MessageConstant;
import com.sky.context.BaseContext;
import com.sky.dto.OrdersSubmitDTO;
import com.sky.entity.AddressBook;
import com.sky.entity.OrderDetail;
import com.sky.entity.Orders;
import com.sky.entity.ShoppingCart;
import com.sky.exception.AddressBookBusinessException;
import com.sky.exception.ShoppingCartBusinessException;
import com.sky.mapper.AddressBookMapper;
import com.sky.mapper.OrderDetailMapper;
import com.sky.mapper.OrderMapper;
import com.sky.mapper.ShoppingCartMapper;
import com.sky.service.OrderService;
import com.sky.vo.OrderSubmitVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;/*** 订单*/
@Service
@Slf4j
@Transactional
public class OrderServiceImpl implements OrderService {@Autowiredprivate OrderMapper orderMapper;@Autowiredprivate OrderDetailMapper orderDetailMapper;@Autowiredprivate ShoppingCartMapper shoppingCartMapper;//操作地址簿@Autowiredprivate AddressBookMapper addressBookMapper;//操作购物车@Overridepublic OrderSubmitVO submitOrder(OrdersSubmitDTO ordersSubmitDTO) {/*** 1.异常情况的处理(收货地址为空、购物车为空)*   绝大部分情况下不做这个判断处理问题也不大,因为如果是小程序提交过来的请求*   其实在小程序那端也做了判断(收货地址为空、购物车为空也是不能提交数据的),*   但是为了代码的健壮性建议在后端还是多次判断一下,因为用户如果并不是通过*   小程序提交的而是通过其它的一些方式 比如postman来提交这些请求,这个时候*   是没有任何校验的,此时后端在不校验那再处理的时候可能就会出现各种问题。*///1.1 通过前端传递过来的地址簿id查询数据库是否有收货地址,如果查不到则抛出异常。AddressBook addressBook = addressBookMapper.getById(ordersSubmitDTO.getAddressBookId());if (addressBook == null) {//抛出业务异常throw new AddressBookBusinessException(MessageConstant.ADDRESS_BOOK_IS_NULL);}//1.2 查询当前用户的购物车数据(购物车为空也不能正常下单)Long userId = BaseContext.getCurrentId();//获取当前用户的idShoppingCart shoppingCart = new ShoppingCart();shoppingCart.setUserId(userId);List<ShoppingCart> shoppingCartList = shoppingCartMapper.list(shoppingCart);if (shoppingCartList == null || shoppingCartList.size() == 0) {//抛出业务异常throw new ShoppingCartBusinessException(MessageConstant.SHOPPING_CART_IS_NULL);}//2.向订单表插入1条数据(用户不管买多少个商品,只要它提交就是一个订单,对应一条订单数据)//构造订单数据Orders order = new Orders();//OrdersSubmitDTO已经封装好了一些数据,所以进行一个对象的拷贝BeanUtils.copyProperties(ordersSubmitDTO,order);//设置剩余的参数://用户的手机号,dto并没有给我们传递过来,通过地址簿id查询出地址数据,在地址数据中就包含用户的名字和手机号//    在前面异常判断中已经查过了,所以在这个地方直接取就可以。order.setPhone(addressBook.getPhone());order.setAddress(addressBook.getDetail());order.setConsignee(addressBook.getConsignee());//收货人//要求是字符串类型,这个地方返回的是Long类型,所以需要进行转化order.setNumber(String.valueOf(System.currentTimeMillis()));//订单号,使用当前系统时间的时间戳生成order.setUserId(userId);//当前订单是属于哪个用户的order.setStatus(Orders.PENDING_PAYMENT);//订单状态:此时是待付款order.setPayStatus(Orders.UN_PAID);//支付状态,用户刚完成下单所以是未支付状态order.setOrderTime(LocalDateTime.now());//下单时间//这个sql需要返回插入的主键值,在后面插入订单明细,在订单明细实体类中会使用当前这个订单的idorderMapper.insert(order);//3.向订单明细表插入n条数据(可能是一条也可能是多条)//     具体需要插入多少条数据,是由购物车中的商品决定的,因为前面做需求分析的时候//     提到了我们真正下单购买这些商品其实是由购物车里面的这些数据决定的,所以订单明细//     里面的数据如何封装就应该看购物车中的数据。//  购物车中的数据在前面异常处理的时候已经查过了,直接遍历购物车数据List<OrderDetail> orderDetailList = new ArrayList<>();for (ShoppingCart cart : shoppingCartList) {//一条购物车数据对应就需要封装成一个订单明细对象OrderDetail orderDetail = new OrderDetail();//购物车实体类和订单明细实体类中的属性名相同,所以直接使用对象属性拷贝来封装。BeanUtils.copyProperties(cart, orderDetail);//设置当前订单明细关联的订单id,订单插入生成的主键值,动态sql封装到了order的id属性上。orderDetail.setOrderId(order.getId());//方式一:单条数据插入,遍历一次插入一次//方式二:批量插入,效率更高,所以这里把获得的订单明细数据给它放在list集合里面,然后一次性的批量插入。orderDetailList.add(orderDetail);}orderDetailMapper.insertBatch(orderDetailList);//批量插入//4.清理当前用户的购物车中的数据(用户下单成功后,用户的这些购物车中的数据就不需要了)shoppingCartMapper.deleteByUserId(userId);//前面购物车模块已实现//5.封装VO返回结果OrderSubmitVO orderSubmitVO = OrderSubmitVO.builder().id(order.getId()).orderNumber(order.getNumber()).orderAmount(order.getAmount()).orderTime(order.getOrderTime()).build();return orderSubmitVO;}
}
2.2.6 Mapper层

创建OrderMapper接口和对应的xml映射文件:

OrderMapper.java

package com.sky.mapper;@Mapper
public interface OrderMapper {/*** 插入订单数据* @param order*/void insert(Orders order);
}

OrderMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.OrderMapper"><insert id="insert" parameterType="Orders" useGeneratedKeys="true" keyProperty="id">insert into orders(number, status, user_id, address_book_id, order_time, checkout_time, pay_method, pay_status, amount, remark,phone, address, consignee, estimated_delivery_time, delivery_status, pack_amount, tableware_number,tableware_status)values (#{number}, #{status}, #{userId}, #{addressBookId}, #{orderTime}, #{checkoutTime}, #{payMethod},#{payStatus}, #{amount}, #{remark}, #{phone}, #{address}, #{consignee},#{estimatedDeliveryTime}, #{deliveryStatus}, #{packAmount}, #{tablewareNumber}, #{tablewareStatus})</insert>
</mapper>

创建OrderDetailMapper接口和对应的xml映射文件:

OrderDetailMapper.java

package com.sky.mapper;import com.sky.entity.OrderDetail;
import java.util.List;@Mapper
public interface OrderDetailMapper {/*** 批量插入订单明细数据* @param orderDetails*/void insertBatch(List<OrderDetail> orderDetails);}

OrderDetailMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.OrderDetailMapper"><insert id="insertBatch" parameterType="list">insert into order_detail(name, order_id, dish_id, setmeal_id, dish_flavor, number, amount, image)values<foreach collection="orderDetails" item="od" separator=",">(#{od.name},#{od.orderId},#{od.dishId},#{od.setmealId},#{od.dishFlavor},#{od.number},#{od.amount},#{od.image})</foreach></insert></mapper>

2.3 功能测试

登录小程序,完成下单操作

下单操作时,同时会删除购物车中的数据

在这里插入图片描述

查看shopping_cart表:

在这里插入图片描述

去结算–>去支付

在这里插入图片描述

查看orders表:

在这里插入图片描述

查看order_detail表:

在这里插入图片描述

同时,购物车表中数据删除:

在这里插入图片描述

2.4 代码提交

在这里插入图片描述

后续步骤和其它功能代码提交一致,不再赘述。

3. 订单支付(业务逻辑,cpolar软件)

3.1 微信支付介绍

前面的课程已经实现了用户下单,那接下来就是订单支付,就是完成付款功能。支付大家应该都不陌生了,在现实生活中经常购买商品并且使用支付功能来付款,在付款的时候可能使用比较多的就是微信支付和支付宝支付了。在苍穹外卖项目中,选择的就是微信支付这种支付方式。

要实现微信支付就需要注册微信支付的一个商户号,这个商户号是必须要有一家企业并且有正规的营业执照。只有具备了这些资质之后,才可以去注册商户号,才能开通支付权限。

个人不具备这种资质,以后进入到企业如果要开发支付功能,这些资质其实都是由企业提供好的,所以我们在学习微信支付时,最重要的是了解微信支付的流程,并且能够阅读微信官方提供的接口文档,之后开发功能代码能够和第三方支付平台对接起来就可以了。

这里使用传智注册的商户号来完成微信支付,以后进入到企业开发如果需要实现微信支付功能,只需要替换一下商户号就可以了。

1)微信支付产品:多种支付的形式

  • JSAPI支付:在h5应用中比如h5页面要进行微信支付,这样的话就可以调起微信支付。
  • Native支付:二维码支付,微信扫一扫商家提供的二维码进行支付。
    在这里插入图片描述

本项目选择小程序支付

参考:https://pay.weixin.qq.com/static/product/product_index.shtml

此网站是微信官方给我们提供的叫作微信支付商户平台,要实现微信支付功能就需要到这个平台上面来阅读,它给我们提供的一些文档。

在这里插入图片描述

2)微信支付接入流程:

要实现微信支付首先就需要接入它。
在这里插入图片描述

3)微信小程序支付时序图:

时序图:程序执行的顺序。

  • 微信用户:微信的使用者这个用户。
  • 微信小程序:手机微信里面苍穹外卖这个小程序。
  • 商户系统:苍穹外卖这个后台系统。
  • 微信后台:微信官方给我们提供的一些服务,要实现微信支付就需要来调用它后台提供的一些接口。
    在这里插入图片描述

具体流程:

  1. 微信用户进入小程序点击商品进行下单,下单的时候小程序这端就会发送一个请求,来调用后端服务的某个接口。前面已经实现了用户下单的功能接口,小程序调用下单这个接口后会返回一些数据:订单id、订单金额、订单号、下单时间。

    • 对应1、2、3步
  2. 有了这些数据之后小程序这端就可以申请微信支付,说白了就是它会发起一次请求,请求的是我们外卖系统的后台服务。此时后端服务就会调用微信后台的一个接口(调用微信下单接口),这个接口的URL是微信官方给我们提供的一个地址,我们请求这个地址的时候需要提交一些参数,并且这个参数非常的复杂。

    • mchid:商户号。要开通微信支付就需要使用企业资质注册一个商户号,通过这个商户号就可以完成支付功能,比如说收款 付款等等。
    • out_trade_no:订单号。一般就是有我们业务系统里面的这个订单号,当前项目系统中使用的是时间戳作为订单号。
    • appid:应用的id。苍穹外面小程序对应的appid,要实现支付功能需要让appid和商户号进行绑定。具体如何绑定是在商户平台上进行绑定的,绑定之后就可以使用appid了。
    • description:描述
    • notify_url:回调地址。当用户通过微信支付付款成功之后,这个微信后台就会调用这个地址来通知我们的程序,所以这个地址一般是商户系统的访问地址。
    • amount:金额。total(具体金额数字),currency(币种:人民币,美元等等)
    • payer:支付者,openid对应的就是当前付款的这个微信用户的openid。openid前面已经讲过,在微信登录的时候就获取到了微信用户的openid
    • 注意:一旦调用完这个接口并没有完成付款,调用这个接口其实是生成一个预支付交易单并没有真的完成支付而是提前通知微信希望有那么一份交易,你先记录一下,一会微信用户会通过小程序来完成这份交易,所以叫做预支付交易单。
    • 调用完此接口后会给我们返回一个字符串,叫作预支付交易标识,拿到这个字符串之后需要进行一些列的数据处理并且签名,签名的目的就是为了安全,因为数据在网络上传输有可能会被别人截获,所以会对数据进行签名保证我们这个数据的安全。
    • 这些数据处理好之后,后端系统就会把这些数据给他响应到小程序这一端。
    • 对应4、5、6、7、8
      在这里插入图片描述
  3. 这些经过处理后的参数返回给前端小程序之后,用户就需要来确认支付,前端程序会调用wx.requestPayment方法(这是小程序端的一个方法),执行这个方法的时候需要很多参数,这些参数来自于调用预支付交易单接口时返回给前端小程序封装后的参数(第7步),然后这些小程序直接使用这些参数。一旦调用这个方法手机上就可以显示以下页面效果:其实就是调起了微信支付。
    在这里插入图片描述
    在这里插入图片描述

    • 点击页面上的确认支付显示输入密码,密码输入完成点击确定之后就会发送一个请求,请求这个微信后台, 微信后台就会进行真正的付款操作,付款成功之后就会给这个小程序返回支付结果,在小程序这端就会显示支付结果,到这里小程序实际上就完成了付款,把钱打到商户这个银行卡里面去了。
    • 对应9、10、11、12
  4. 到这个地方实际上还没有完,因为刚才是通过微信小程序支付, 直接给我们这个微信后台来进行交互完成付款,但是这个过程后台商户系统并不知道,刚才你这个微信用户有没有付款,是付款成功了还是失败了这个后台系统根本就不知道。

    • 所以实际上在完成真正付款之后还有2个流程:
      • 微信后台会推送支付结果给商户系统、更新订单状态:通过notify_url回调地址,微信后台就会调用这个地址,这个地址恰好就是商户后台系统的一个服务地址,这样的话就可以调用到商户系统,商户系统接收到通知后就可以去更新订单的状态,当前苍穹外卖系统中完成用户下单时,用户下单成功之后设置的支付状态是未支付,此时用户已经完成了付款所以需要修改支付状态为已支付
    • 对应13、14

总结具体流程

  1. 用户下单:对应1、2、3步
  2. 调用预支付接口生成预支付交易单:对应4、5、6、7、8
  3. 小程序端和后台服务进行交互,调起微信支付真正完成了付款:对应9、10、11、12
  4. 支付结果的推送:微信后台会调用商户系统的某一个地址,从而把结果通知给商户系统,然后商户系统去更新这个订单的支付状态。对应13、14。

4)微信支付相关接口:

  • JSAPI下单:商户系统调用该接口在微信支付服务后台生成预支付交易单(对应时序图的第5步)
    在这里插入图片描述

    • url是微信官方给我们提供的一个接口地址。

    • mchid:商户号

    • 返回结果为预支付交易标识prepay_id:实际上就是一个字符串

    • 注意:调用完这个接口并没有完成真正了付款,只是生成一个预支付交易单并没有真的完成支付,相当于提前通知微信希望有这份交易,你先记录一下,之后微信用户会通过小程序来完成这份交易,所以叫做预支付交易单,相当于先在微信这边先进行一个备案。

  • 微信小程序调起支付(页面中的js方法):通过JSAPI下单接口获取到发起支付的必要参数prepay_id,然后使用微信支付提供的小程序方法调起小程序支付(对应时序图的第10步)
    在这里插入图片描述

    • 真正完成了付款。
    • 这些参数都是后端计算好返回给我们小程序,小程序直接使用这些参数即可。

3.2 微信支付准备工作

3.2.1 如何保证数据安全?

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

第一个就是需要在商户系统当中调用微信后台的一个下单接口,就是生成预支付交易单

第二个就是支付成功之后微信后台会给推送消息

这两个接口数据的安全性,要求其实是非常高的。

解决:微信提供的方式就是对数据进行加密、解密、签名多种方式。要完成数据加密解密,需要提前准备相应的一些文件,其实就是一些证书。

获取微信支付平台证书、商户私钥文件:

在这里插入图片描述

注意:

  • 这2个文件是从微信的商户平台下载下来的,在后续程序开发过程中,就会使用到这两个文件,具体如何用 代码怎么写 后面在说,现在提前把这两个文件准备好。
  • 前提:必须要有一家企业并且有正规的营业执照
3.2.2 如何调用到商户系统(cpolar)

微信后台会调用到商户系统给推送支付的结果,在这里我们就会遇到一个问题,就是微信后台怎么就能调用到我们这个商户系统呢?因为这个调用过程,其实本质上也是一个HTTP请求。

目前,商户系统它的ip地址就是当前自己电脑的ip地址,只是一个局域网内的ip地址,微信后台无法调用到。

解决:内网穿透。通过cpolar软件可以获得一个临时域名,而这个临时域名是一个公网ip,这样,微信后台就可以请求到商户系统了。

注意:当前开发阶段电脑大部分都在局域网之内并没有公网ip,所以需要这种方式获得一个临时域名,但是最终项目上线之后一般都会有公网ip,我们直接使用公网ip即可。

cpolar软件的使用:

1). 下载与安装

下载地址:https://dashboard.cpolar.com/get-started

在这里插入图片描述

在资料中已提供,可无需下载。

在这里插入图片描述

安装过程中,一直下一步即可,不再演示。

2). cpolar指定authtoken

复制authtoken:

在这里插入图片描述

执行命令:注意./cpolar authtoken ...是linux下的命令。

在这里插入图片描述
具体步骤:

  • 进入到安装cpolar软件的命令行窗口目录:
    在这里插入图片描述

  • 当前使用的是windows,所以使用windows下面的命令
    cpolar.exe authtoken cpolar官网生成的令牌
    在这里插入图片描述

  • cpolar官网生成的令牌
    在这里插入图片描述

  • 回车后会在c盘生成一个文件,即内网穿透工具的一个配置文件。
    在这里插入图片描述

3). 获取临时域名

在你安装的cpolar目录下执行

为什么设置8080:因为后端服务的端口号就是8080

执行命令:cpolar.exe http 8080

在这里插入图片描述
在这里插入图片描述

获取域名:

在这里插入图片描述
在这里插入图片描述

4). 验证临时域名有效性

访问接口文档

使用localhost:8080访问

http://localhost:8080/doc.html
在这里插入图片描述

使用临时域名访问:域名/doc.html

在这里插入图片描述

在这里插入图片描述

证明临时域名生效。

3.3 代码导入

导入资料中的微信支付功能代码即可

说明:

  • 微信支付的代码是非常固定的并且比较繁琐,我们没必要手写这种代码这里直接导入即可。
  • 这些代码都可以复用,在企业开发者如果要开发微信支付功能,直接拷贝这里的代码。
    在这里插入图片描述
3.3.1 微信支付相关配置

application-dev.yml

在这里插入图片描述

sky:wechat:appid: wxcd2e39f677fd30ba  secret: 84fbfdf5ea288f0c432d829599083637 mchid : 1561414331 #传智播客申请的商户号mchSerialNo: 4B3B3DC35414AD50B1B755BAF8DE9CC7CF407606 #证书的序列号privateKeyFilePath: D:\apiclient_key.pem #私钥文件(不是企业 没有)apiV3Key: CZBK51236435wxpay435434323FFDuv3 #解密的秘钥(商户平台设置 一般公司提供好不用自己手动配置)weChatPayCertFilePath: D:\wechatpay_166D96F876F45C7D07CE98952A96EC980368ACFC.pem #平台证书文件notifyUrl: https://www.weixin.qq.com/wxpay/pay.php #支付成功的回调(域名地址+controller的访问路径)refundNotifyUrl: https://www.weixin.qq.com/wxpay/pay.php #退款成功的回调# 使用内网穿透工具每次获取到的临时域名都不一样,注意替换为自己最新的临时域名

application.yml

在这里插入图片描述

sky:wechat:appid: ${sky.wechat.appid}secret: ${sky.wechat.secret}mchid : ${sky.wechat.mchid}mchSerialNo: ${sky.wechat.mchSerialNo}privateKeyFilePath: ${sky.wechat.privateKeyFilePath}apiV3Key: ${sky.wechat.apiV3Key}weChatPayCertFilePath: ${sky.wechat.weChatPayCertFilePath}notifyUrl: ${sky.wechat.notifyUrl}refundNotifyUrl: ${sky.wechat.refundNotifyUrl}

WeChatProperties.java:读取配置(已定义)

在这里插入图片描述

package com.sky.properties;import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;@Component
@ConfigurationProperties(prefix = "sky.wechat")
@Data
public class WeChatProperties {private String appid; //小程序的appidprivate String secret; //小程序的秘钥private String mchid; //商户号private String mchSerialNo; //商户API证书的证书序列号private String privateKeyFilePath; //商户私钥文件private String apiV3Key; //证书解密的密钥private String weChatPayCertFilePath; //平台证书private String notifyUrl; //支付成功的回调地址private String refundNotifyUrl; //退款成功的回调地址
}
3.3.2 Mapper层

在UserMapper中添加getById方法

    //根据主键查用户@Select("select * from user where id = #{id}")User getById(Long userId);

在OrderMapper.java中添加getByNumberAndUserId和update两个方法

   /*** 根据订单号查询订单* @param orderNumber*/@Select("select * from orders where number = #{orderNumber}")Orders getByNumber(String orderNumber);/*** 修改订单状态信息* @param orders*/void update(Orders orders);

在OrderMapper.xml中添加

   <update id="update" parameterType="com.sky.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>
3.3.3 Service层

在OrderService.java中添加payment和paySuccess两个方法定义

    /*** 订单支付* @param ordersPaymentDTO* @return*/OrderPaymentVO payment(OrdersPaymentDTO ordersPaymentDTO) throws Exception;/*** 支付成功,修改订单状态* @param outTradeNo*/void paySuccess(String outTradeNo);

在OrderServiceImpl.java中实现payment和paySuccess两个方法

 	@Autowiredprivate UserMapper userMapper;@Autowiredprivate WeChatPayUtil weChatPayUtil;/*** 订单支付** @param ordersPaymentDTO* @return*/@Overridepublic OrderPaymentVO payment(OrdersPaymentDTO ordersPaymentDTO) throws Exception {//1.获取当前登录用户id,之后根据id查询数据库把用户数据查询出来Long userId = BaseContext.getCurrentId();User user = userMapper.getById(userId);//2.调用微信支付接口工具类,生成预支付交易单(对应第5步)JSONObject jsonObject = weChatPayUtil.pay(ordersPaymentDTO.getOrderNumber(), //商户订单号new BigDecimal(0.01), //支付金额,单位 元"苍穹外卖订单", //商品描述user.getOpenid() //微信用户的openid);if (jsonObject.getString("code") != null && jsonObject.getString("code").equals("ORDERPAID")) {throw new OrderBusinessException("该订单已支付");}//3.转化为vo对象在返回给controllerOrderPaymentVO vo = jsonObject.toJavaObject(OrderPaymentVO.class);vo.setPackageStr(jsonObject.getString("package"));return vo;}/*** 支付成功,修改订单状态** @param outTradeNo*/public void paySuccess(String outTradeNo) {// 根据订单号查询订单Orders ordersDB = orderMapper.getByNumber(outTradeNo);// 根据订单id更新订单的状态、支付方式、支付状态、结账时间Orders orders = Orders.builder().id(ordersDB.getId()).status(Orders.TO_BE_CONFIRMED).payStatus(Orders.PAID).checkoutTime(LocalDateTime.now()).build();orderMapper.update(orders);}
3.3.4 使用到的微信支付工具类

在这里插入图片描述

package com.sky.utils;import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.sky.properties.WeChatProperties;
import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
import org.apache.commons.lang.RandomStringUtils;
import org.apache.http.HttpHeaders;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.math.BigDecimal;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.List;/*** 微信支付工具类*/
@Component
public class WeChatPayUtil {//微信支付下单接口地址(生成预支付交易单)public static final String JSAPI = "https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi";//申请退款接口地址public static final String REFUNDS = "https://api.mch.weixin.qq.com/v3/refund/domestic/refunds";@Autowiredprivate WeChatProperties weChatProperties;/*** 获取调用微信接口的客户端工具对象** @return*/private CloseableHttpClient getClient() {PrivateKey merchantPrivateKey = null;try {//merchantPrivateKey商户API私钥,如何加载商户API私钥请看常见问题merchantPrivateKey = PemUtil.loadPrivateKey(new FileInputStream(new File(weChatProperties.getPrivateKeyFilePath())));//加载平台证书文件X509Certificate x509Certificate = PemUtil.loadCertificate(new FileInputStream(new File(weChatProperties.getWeChatPayCertFilePath())));//wechatPayCertificates微信支付平台证书列表。你也可以使用后面章节提到的“定时更新平台证书功能”,而不需要关心平台证书的来龙去脉List<X509Certificate> wechatPayCertificates = Arrays.asList(x509Certificate);WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create().withMerchant(weChatProperties.getMchid(), weChatProperties.getMchSerialNo(), merchantPrivateKey).withWechatPay(wechatPayCertificates);// 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签CloseableHttpClient httpClient = builder.build();return httpClient;} catch (FileNotFoundException e) {e.printStackTrace();return null;}}/*** 发送post方式请求:底层使用的还是httpClient发起的请求** @param url* @param body* @return*/private String post(String url, String body) throws Exception {//构造httpClient对象:调用上面的方法生成的对象CloseableHttpClient httpClient = getClient();HttpPost httpPost = new HttpPost(url);//设置的请求头httpPost.addHeader(HttpHeaders.ACCEPT, ContentType.APPLICATION_JSON.toString());httpPost.addHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.toString());httpPost.addHeader("Wechatpay-Serial", weChatProperties.getMchSerialNo());httpPost.setEntity(new StringEntity(body, "UTF-8"));//发起请求CloseableHttpResponse response = httpClient.execute(httpPost);try {String bodyAsString = EntityUtils.toString(response.getEntity());return bodyAsString;} finally {httpClient.close();response.close();}}/*** 发送get方式请求** @param url* @return*/private String get(String url) throws Exception {CloseableHttpClient httpClient = getClient();HttpGet httpGet = new HttpGet(url);httpGet.addHeader(HttpHeaders.ACCEPT, ContentType.APPLICATION_JSON.toString());httpGet.addHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.toString());httpGet.addHeader("Wechatpay-Serial", weChatProperties.getMchSerialNo());CloseableHttpResponse response = httpClient.execute(httpGet);try {String bodyAsString = EntityUtils.toString(response.getEntity());return bodyAsString;} finally {httpClient.close();response.close();}}/*** jsapi下单** @param orderNum    商户订单号* @param total       总金额* @param description 商品描述* @param openid      微信用户的openid* @return*/private String jsapi(String orderNum, BigDecimal total, String description, String openid) throws Exception {JSONObject jsonObject = new JSONObject();jsonObject.put("appid", weChatProperties.getAppid());//读取的配置文件  AppidjsonObject.put("mchid", weChatProperties.getMchid());//读取的配置文件  商户号jsonObject.put("description", description); //参数传递过来的  描述信息jsonObject.put("out_trade_no", orderNum); //参数传递过来的  订单号jsonObject.put("notify_url", weChatProperties.getNotifyUrl());//读取的配置文件  退款成功的回调JSONObject amount = new JSONObject();amount.put("total", total.multiply(new BigDecimal(100)).setScale(2, BigDecimal.ROUND_HALF_UP).intValue());amount.put("currency", "CNY");jsonObject.put("amount", amount); //订单金额JSONObject payer = new JSONObject();payer.put("openid", openid);jsonObject.put("payer", payer); //参数传递过来的   支付者String body = jsonObject.toJSONString();//调用上面这个post方法,参数:生成预支付交易单的接口地址(本类中定义的常量)、封装的请求参数//    post方法作用:底层使用的还是httpClient发起的请求return post(JSAPI, body);}/*** 小程序支付:加密相关的代码都是固定的,以后想要使用的话直接复制即可。** @param orderNum    商户订单号* @param total       金额,单位 元* @param description 商品描述* @param openid      微信用户的openid* @return*/public JSONObject pay(String orderNum, BigDecimal total, String description, String openid) throws Exception {//统一下单,生成预支付交易单//jsapi方法(上面这个方法就是):封装的是调用微信下单接口,生成预支付交易单接口时所需要的参数//bodyAsString:返回的是预支付标识,是个字符串String bodyAsString = jsapi(orderNum, total, description, openid);//解析返回结果:需要把这个预支付标识转化为jsonJSONObject jsonObject = JSON.parseObject(bodyAsString);System.out.println(jsonObject);//获取prepayId预支付标识的值String prepayId = jsonObject.getString("prepay_id");//封装一系列的数据,对数据进行加密并且签名if (prepayId != null) {String timeStamp = String.valueOf(System.currentTimeMillis() / 1000);String nonceStr = RandomStringUtils.randomNumeric(32);ArrayList<Object> list = new ArrayList<>();list.add(weChatProperties.getAppid());list.add(timeStamp);list.add(nonceStr);list.add("prepay_id=" + prepayId);//二次签名,调起支付需要重新签名StringBuilder stringBuilder = new StringBuilder();for (Object o : list) {stringBuilder.append(o).append("\n");}String signMessage = stringBuilder.toString();byte[] message = signMessage.getBytes();Signature signature = Signature.getInstance("SHA256withRSA");signature.initSign(PemUtil.loadPrivateKey(new FileInputStream(new File(weChatProperties.getPrivateKeyFilePath()))));signature.update(message);String packageSign = Base64.getEncoder().encodeToString(signature.sign());//构造数据给微信小程序,用于调起微信支付// 前端微信小程序调用wx.requestPayment方法需要传递的参数,封装好之后传递给前端小程序JSONObject jo = new JSONObject();jo.put("timeStamp", timeStamp);jo.put("nonceStr", nonceStr);jo.put("package", "prepay_id=" + prepayId);jo.put("signType", "RSA");jo.put("paySign", packageSign);return jo;}return jsonObject;}/*** 申请退款** @param outTradeNo    商户订单号* @param outRefundNo   商户退款单号* @param refund        退款金额* @param total         原订单金额* @return*/public String refund(String outTradeNo, String outRefundNo, BigDecimal refund, BigDecimal total) throws Exception {JSONObject jsonObject = new JSONObject();jsonObject.put("out_trade_no", outTradeNo);jsonObject.put("out_refund_no", outRefundNo);JSONObject amount = new JSONObject();amount.put("refund", refund.multiply(new BigDecimal(100)).setScale(2, BigDecimal.ROUND_HALF_UP).intValue());amount.put("total", total.multiply(new BigDecimal(100)).setScale(2, BigDecimal.ROUND_HALF_UP).intValue());amount.put("currency", "CNY");jsonObject.put("amount", amount);jsonObject.put("notify_url", weChatProperties.getRefundNotifyUrl());String body = jsonObject.toJSONString();//调用申请退款接口return post(REFUNDS, body);}
}
3.3.5 Controller层

在OrderController.java中添加payment方法
在这里插入图片描述

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

PayNotifyController.java:支付成功后微信后台会回调这个controller

在这里插入图片描述

package com.sky.controller.notify;import com.alibaba.druid.support.json.JSONUtils;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.sky.annotation.IgnoreToken;
import com.sky.properties.WeChatProperties;
import com.sky.service.OrderService;
import com.wechat.pay.contrib.apache.httpclient.util.AesUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.entity.ContentType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;/*** 支付回调相关接口*/
@RestController
@RequestMapping("/notify")
@Slf4j
public class PayNotifyController {@Autowiredprivate OrderService orderService;@Autowiredprivate 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();}
}
3.3.6 用到的DTO

在这里插入图片描述

package com.sky.dto;import lombok.Data;
import java.io.Serializable;@Data
public class OrdersPaymentDTO implements Serializable {//订单号private String orderNumber;//付款方式private Integer payMethod;}

3.4 阅读订单支付功能代码

说明:

  • 阅读这个代码还是有一定难度的,一个是导入的代码量比较大,另一个是代码比较分散,并不是某一个方法就把所有的功能完成了,它其实是由流程的,所以阅读这个代码要结合着时序图来看。
  • 前面已经实现了用户下单所以这里直接从第4步开始看。
    在这里插入图片描述

步骤说明:

  • 调用预支付接口生成预支付交易单:对应4、5、6、7、8
    • 控制层方法:
      在这里插入图片描述
    • 用到的DTO:订单号、付款方式(因为当前只提供了一种支付方式,微信小程序支付,所以这个参数没有用到)
      在这里插入图片描述
    • 业务层方法:获取用户数据,生成预支付交易单,转化为vo对象在返回给controller
      在这里插入图片描述
    • 用到的工具类:层层调用,pay—》jsapi—》post----》getClient
      在这里插入图片描述

支付成功回调:修改后台订单状态

  • 控制层:
    在这里插入图片描述
  • 业务层:
    在这里插入图片描述

3.5 功能测试

说明:缺少上面那2个文件运行不了

测试过程中,可通过断点方式查看后台每一步执行情况。

下单:

在这里插入图片描述

去支付:

在这里插入图片描述

确认支付:

在这里插入图片描述

进行扫码支付即可。

3.6 解决没有商户号问题

说明:

  • 要实现微信支付就需要注册微信支付的一个商户号,这个商户号是必须要有一家企业并且有正规的营业执照。
  • 个人没办法获得商户号,所以这里直接修改前后端代码跳过微信支付接口,点击微信支付直接现显示支付成功,之后修改订单的状态。
3.6.1 前端小程序修改

修改为:点击支付按钮后直接跳转支付成功

  • 首先在微信小程序里的pay包下的index.js中将如下的代码注释掉:
    在这里插入图片描述
  • 然后把原先注释掉的重定向解除:
    在这里插入图片描述
3.6.2 后端修改

修改为:要求在收到前端支付操作后,不进行任何判断,直接给数据库设置已支付状态。

  • 把service/impl下的OrderServiceImpl中的如下代码注释掉:
    在这里插入图片描述

  • 同样在OrderServiceImpl中,写入如下代码,用于设置参数:
    在这里插入图片描述

完整的订单支付代码如下:

    /*** 订单支付** @param ordersPaymentDTO* @return*/@Overridepublic OrderPaymentVO payment(OrdersPaymentDTO ordersPaymentDTO) throws Exception {//1.获取当前登录用户id,之后根据id查询数据库把用户数据查询出来Long userId = BaseContext.getCurrentId();User user = userMapper.getById(userId);/*        //2.调用微信支付接口工具类,生成预支付交易单(对应第5步)JSONObject jsonObject = weChatPayUtil.pay(ordersPaymentDTO.getOrderNumber(), //商户订单号new BigDecimal(0.01), //支付金额,单位 元"苍穹外卖订单", //商品描述user.getOpenid() //微信用户的openid);if (jsonObject.getString("code") != null && jsonObject.getString("code").equals("ORDERPAID")) {throw new OrderBusinessException("该订单已支付");}//3.转化为vo对象在返回给controllerOrderPaymentVO vo = jsonObject.toJavaObject(OrderPaymentVO.class);vo.setPackageStr(jsonObject.getString("package"));*/JSONObject jsonObject = new JSONObject();jsonObject.put("code","ORDERPAID");OrderPaymentVO vo = jsonObject.toJavaObject(OrderPaymentVO.class);vo.setPackageStr(jsonObject.getString("package"));Integer OrderPaidStatus = Orders.PAID;//支付状态,已支付Integer OrderStatus = Orders.TO_BE_CONFIRMED;  //订单状态,待接单LocalDateTime check_out_time = LocalDateTime.now();//更新支付时间//获得的是String类型,需要的是Long类型,所以需要进行转化String orderidS = ordersPaymentDTO.getOrderNumber();Long orderidL =Long.parseLong(orderidS);//获取订单号orderMapper.updateStatus(OrderStatus, OrderPaidStatus, check_out_time,orderidL );return vo;}

在OrderMapper中写入如下代码:
在这里插入图片描述

    //手动修改订单状态:解决个人没有商户号不能测试订单支付问题@Update("update orders set status = #{orderStatus},pay_status = #{orderPaidStatus} ,checkout_time = #{check_out_time} " +"where number = #{orderidL}")void updateStatus(Integer orderStatus, Integer orderPaidStatus, LocalDateTime check_out_time, Long orderidL);
3.6.3 测试
  • 重启项目
  • 下单
    在这里插入图片描述
  • 去支付:
    在这里插入图片描述
    在这里插入图片描述
  • 查看数据库的订单表发现:订单状态修改为2(待接单)、支付状态修改为1(已支付)
    在这里插入图片描述

3.7 代码提交

在这里插入图片描述

后续步骤和其它功能代码提交一致,不再赘述。

相关文章:

苍穹外卖-day08:导入地址簿功能代码(单表crud)、用户下单(业务逻辑)、订单支付(业务逻辑,cpolar软件)

苍穹外卖-day08 课程内容 导入地址簿功能代码用户下单订单支付 功能实现&#xff1a;用户下单、订单支付 用户下单效果图&#xff1a; 订单支付效果图&#xff1a; 1. 导入地址簿功能代码&#xff08;单表crud&#xff09; 1.1 需求分析和设计 1.1.1 产品原型&#xff08…...

Java面试相关问题

一.MySql篇 1优化相关问题 1.1.MySql中如何定位慢查询&#xff1f; 慢查询的概念&#xff1a;在MySQL中&#xff0c;慢查询是指执行时间超过一定阈值的SQL语句。这个阈值是由long_query_time参数设定的&#xff0c;它的默认值是10秒1。也就是说&#xff0c;如果一条SQL语句的执…...

Linux Shell中的循环控制语句

Linux Shell中的循环控制语句 在编写Shell脚本时&#xff0c;循环是一种常用的控制结构&#xff0c;用于重复执行一系列命令。在Shell中&#xff0c;主要有三种循环控制语句&#xff1a;for循环&#xff0c;while循环&#xff0c;和until循环。 1. For循环 for循环是最常见的…...

proto3语言指南

Language Guide (proto3) 本指南介绍了如何使用 protocol buffer 语言来构建protocol buffer数据,包括.proto文件语法以及如何从.proto 文件生成数据访问类。它涵盖了proto3 版本的协议缓冲语言:有关proto2语法的信息,请参阅proto2语言指南。 文章目录 Language Guide (pro…...

解决后端传给前端的日期问题

解决方式&#xff1a; 1). 方式一 在属性上加上注解&#xff0c;对日期进行格式化 但这种方式&#xff0c;需要在每个时间属性上都要加上该注解&#xff0c;使用较麻烦&#xff0c;不能全局处理。 2). 方式二&#xff08;推荐 ) 在WebMvcConfiguration中扩展SpringMVC的消息转…...

MySQL中的索引失效情况介绍

MySQL中的索引是提高查询性能的重要工具。然而&#xff0c;在某些情况下&#xff0c;索引可能无法发挥作用&#xff0c;甚至导致查询性能下降。在本教程中&#xff0c;我们将探讨MySQL中常见的索引失效情况&#xff0c;以及它们的特点和简单的例子。 1. **索引失效的情况** …...

SpringBoot异常:类文件具有错误的版本 61.0, 应为 52.0的解决办法

问题&#xff1a; java: 无法访问org.mybatis.spring.annotation.MapperScan 错误的类文件: /D:/Program Files/apache-maven-3.6.0/repository/org/mybatis/mybatis-spring/3.0.3/mybatis-spring-3.0.3.jar!/org/mybatis/spring/annotation/MapperScan.class 类文件具有错误的…...

Cloudways搭建WordPress外贸独立站完整教程

现在做个网站不比从前了&#xff0c;搭建网站非常的简单&#xff0c;主要是由于开源的CMS建站系统的崛起&#xff0c;就算不懂编程写代码的人也能搭建一个自己的网站&#xff0c;这些CMS系统提供了丰富的主题模板和插件&#xff0c;使用户可以通过简单的拖放和配置操作来建立自…...

关于 闰年 的小知识,为什么这样判断闰年

闰年的规定&#xff1a; 知道了由来&#xff0c;我们就可以写程序来判断&#xff1a; #include <stdio.h> int main() {int year, leap;scanf("%d",&year);if((year%4 0 && year%100 ! 0) || year%400 0)leap 1;else leap 0;if(leap) printf(…...

Elasticsearch:调整近似 kNN 搜索

在我之前的文章 “Elasticsearch&#xff1a;调整搜索速度”&#xff0c;我详细地描述了如何调整正常的 BM25 的搜索速度。在今天的文章里&#xff0c;我们来进一步探讨如何提高近似 kNN 的搜索速度。希望对广大的向量搜索开发者有一些启示。 Elasticsearch 支持近似 k 最近邻…...

UE5数字孪生系列笔记(二)

智慧城市数字孪生系统 制作流云动画效果 首先添加一个图像在需要添加流云效果的位置 添加动画效果让其旋转 这个动画效果是程序开始就要进行的&#xff0c;所以要在EventConstruct中就可以启动这个动画效果 添加一个一样的图像在这里&#xff0c;效果是从此处进行放大消散 添…...

基于vue实现bilibili网页

学校要求的实验设计,基于vue实现bilibili网页版,可实现以下功能 (1)基本的悬浮动画和页面渲染 (2)可实现登录和未登录的页面变化 (3)在登录页面的,实现密码判断,或者短信验证方式的倒数功能 (4)实现轮播图 (5)实现预览视频(GIF) (6)页面下拉到一定高度出现top栏以及右下角的返回…...

计算机二级(Python)真题讲解每日一题:《十字叉》

描述‪‬‪‬‪‬‪‬‪‬‮‬‪‬‫‬‪‬‪‬‪‬‪‬‪‬‮‬‪‬‮‬‪‬‪‬‪‬‪‬‪‬‮‬‪‬‭‬‪‬‪‬‪‬‪‬‪‬‮‬‫‬‮‬‪‬‪‬‪‬‪‬‪‬‮‬‭‬‫‬‪‬‪‬‪‬‪‬‪‬‮‬‫‬‪‬‪‬‪‬‪‬‪‬‪‬‮‬‪‬‮‬ ‪‬‪‬‪‬‪‬‪‬‮‬‪…...

基于正点原子潘多拉STM32L496开发板的简易示波器

一、前言 由于需要对ADC采样性能的评估&#xff0c;重点在于对原波形的拟合性能。 考虑到数据的直观性&#xff0c;本来计划采集后使用串口导出&#xff0c;并用图形做数据拟合&#xff0c;但是这样做的效率低下&#xff0c;不符合实时观察的需要&#xff0c;于是将开发板的屏幕…...

【Docker】apisix 容器化部署

APISIX环境标准软件基于Bitnami apisix 构建。当前版本为3.8.0 你可以通过轻云UC部署工具直接安装部署&#xff0c;也可以手动按如下文档操作&#xff0c;该项目已经全面开源&#xff0c;可以从如下环境获取 配置文件地址: https://gitee.com/qingplus/qingcloud-platform qi…...

基于YOLOv8/YOLOv7/YOLOv6/YOLOv5的障碍物检测系统(深度学习代码+UI界面+训练数据集)

摘要&#xff1a;开发障碍物检测系统对于道路安全性具有关键作用。本篇博客详细介绍了如何运用深度学习构建一个障碍物检测系统&#xff0c;并提供了完整的实现代码。该系统基于强大的YOLOv8算法&#xff0c;并对比了YOLOv7、YOLOv6、YOLOv5&#xff0c;展示了不同模型间的性能…...

从零开始学HCIA之SDN04

1、VXLAN数据封装 &#xff08;1&#xff09;Original L2 Frame&#xff0c;原始以太网报文&#xff0c;业务应用的以太网帧。 &#xff08;2&#xff09;VXLAN Header&#xff0c;VXLAN协议新定义的VXLAN头&#xff0c;长度为8字节。VXLAN ID&#xff08;VNI&#xff09;为2…...

GET 和 POST 有什么区别?

1.从缓存的角度&#xff0c;GET 请求会被浏览器主动缓存下来&#xff0c;留下历史记录&#xff0c;而 POST 默认不会。 2.从编码的角度&#xff0c;GET 只能进行 URL 编码&#xff0c;只能接收 ASCII 字符&#xff0c;而 POST 没有限制。 3.从参数的角度&#xff0c;GET 一般放…...

Qt学习--继承(并以分文件实现)

基类 & 派生类 一个类可以派生自多个类&#xff0c;这意味着&#xff0c;它可以从多个基类继承数据和函数。定义一个派生类&#xff0c;我们使用一个类派生列表来指定基类。类派生列表以一个或多个基类命名。 总结&#xff1a;简单来说&#xff0c;父类有的&#xff0c;子…...

软考75-上午题-【面向对象技术3-设计模式】-设计模式的要素

一、题型概括 上午、下午题&#xff08;试题五、试题六&#xff0c;二选一&#xff09; 每一个设计模式都有一个对应的类图。 二、23种设计模式 创建型设计模式&#xff1a;5 结构型设计模式&#xff1a;7 行为设计模式&#xff1a;11 考试考1-2种。 三、设计模式的要素 3…...

Matlab|面向低碳经济运行目标的多微网能量互联优化调度

目录 主要内容 优化流程 部分程序 结果一览 下载链接 主要内容 该程序为多微网协同优化调度模型&#xff0c;系统在保障综合效益的基础上&#xff0c;调度时优先协调微网与微网之间的能量流动&#xff0c;将与大电网的互联交互作为备用&#xff0c;降低微网与大电…...

3.Gen<I>Cam文件配置

Gen<I>Cam踩坑指南 我使用的是大恒usb相机&#xff0c;第一步到其官网下载大恒软件安装包,安装完成后图标如图所示&#xff0c;之后连接相机&#xff0c;打开软件&#xff0c;相机显示一切正常。之后查看软件的安装目录如图&#xff0c;发现有GenICam和GenTL两个文件&am…...

【兆易创新GD32H759I-EVAL开发板】 TLI(TFT LCD Interface)用法详细介绍

大纲 1. 引言 2. TLI外设特点 3. TLI硬件架构 4. TLI寄存器功能 5. TLI的配置和使用步骤 6. TLI图层概念 7. 图像处理和显示优化 8. 基于GD32H759I-EVAL开发板的TLI应用示例 1. 引言 在当今的嵌入式系统设计中&#xff0c;图形用户界面&#xff08;GUI&#xff09;的应…...

恒创科技:什么是BGP线路服务器?BGP机房的优点是什么?

在当今的互联网架构中&#xff0c;BGP(边界网关协议)线路服务器和BGP机房扮演着至关重要的角色。BGP作为一种用于在自治系统(AS)之间交换路由信息的路径向量协议&#xff0c;它确保了互联网上的数据能够高效、准确地从一个地方传输到另一个地方。那么&#xff0c;究竟什么是BGP…...

苍穹外卖-day04:项目实战-套餐管理(新增套餐,分页查询套餐,删除套餐,修改套餐,起售停售套餐)业务类似于菜品模块

苍穹外卖-day04 课程内容 新增套餐套餐分页查询删除套餐修改套餐起售停售套餐 要求&#xff1a; 根据产品原型进行需求分析&#xff0c;分析出业务规则设计接口梳理表之间的关系&#xff08;分类表、菜品表、套餐表、口味表、套餐菜品关系表&#xff09;根据接口设计进行代…...

深入探索C与C++的混合编程

实现混合编程的技术细节 混合使用C和C可能由多种原因驱动。一方面&#xff0c;现有的大量优秀C语言库为特定任务提供了高效的解决方案&#xff0c;将这些库直接应用于C项目中可以节省大量的开发时间和成本。另一方面&#xff0c;C的高级特性如类、模板和异常处理等&#xff0c;…...

数组中的flat方法如何实现

数组的成员有时还是数组&#xff0c;Array.prototype.flat()用于将嵌套的数组“拉平”&#xff0c;变成一维的数组。该方法返回一个新数组&#xff0c;对原数据没有影响。 [1, 2, [3, 4]].flat() // [1, 2, 3, 4]那flat怎么来实现呢&#xff1f; 1、使用while循环 实现的代码…...

计算机考研|北航北理北邮怎么选?

北航985&#xff0c;北理985&#xff0c;北邮211 虽然北邮事211&#xff0c;但是北邮的计算机实力一点也不弱&#xff0c;学科评级&#xff0c;计算机是A 北航计算机评级也是A&#xff0c;北理的计算机评级是A- 所以&#xff0c;这三所学校在实力上来说&#xff0c;真的大差…...

面试算法-52-对称二叉树

题目 给你一个二叉树的根节点 root &#xff0c; 检查它是否轴对称。 示例 1&#xff1a; 输入&#xff1a;root [1,2,2,3,4,4,3] 输出&#xff1a;true 解 class Solution {public boolean isSymmetric(TreeNode root) {return dfs(root, root);}public boolean dfs(Tr…...

独立维基和验收测试框架 Fitnesse 入门介绍

拓展阅读 junit5 系列教程 基于 junit5 实现 junitperf 源码分析 Auto generate mock data for java test.(便于 Java 测试自动生成对象信息) Junit performance rely on junit5 and jdk8.(java 性能测试框架。压测测试报告生成。) Fitnesse 完全集成的独立维基和验收测试…...

邯郸建设网站制作/新手怎么学网络运营

下载workerman的linux包。并新建start.php文件&#xff0c;内容如下&#xff1a; <?php use Workerman\Worker; require_once Autoloader.php;$global_uid 0;// 当客户端连上来时分配uid&#xff0c;并保存连接&#xff0c;并通知所有客户端 function handle_connection(…...

营销型网站建设的费用报价单/中国优化网

react-redux使用小结 react-reduxstorereduceraction整合storereduceraction补充 使用redux-dev-tools让改变reducer后能够即时刷新页面总结需要使用的库redux&#xff0c;react-redux&#xff0c;react-router-redux react-redux 使用一个react-redux 的库使得redux的使用更…...

主流网站 技术/重庆人力资源和社会保障网

进入系统后切换为root权限 su 注意权限问题 前面切换到root执行下面代码 备份原来的源 mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.backup 下载阿里的yum源 curl -o /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Ce…...

可以合成装备的传奇手游/搜索引擎优化的方式

但作为专业人士&#xff0c;此时&#xff0c;应该来一波专业的回答&#xff0c;故事的起源是这样的&#xff1a;从前&#xff0c;有家叫 Netscape 的公司开发了一个名叫 Livescript 的脚本语言&#xff0c;但是&#xff0c;开发出来之后呢&#xff1f;一直没啥名气。公司正当愁…...

福建建设工程信息网官网/太原关键词优化公司

帕雷托最优&#xff08;英语&#xff1a;Pareto optimality&#xff09;&#xff0c;或帕雷托最适&#xff0c;也称为帕雷托效率&#xff08;英语&#xff1a;Pareto efficiency&#xff09;&#xff0c;是经济学中的重要概念&#xff0c;并且在博弈论、工程学和社会科学中有着…...

网站方案策划书/北京朝阳区疫情最新情况

unix/linux内核在系统里扮演什么角色&#xff1f;不仅仅是FreeBSD系统&#xff0c;每一个操作系统都有一个内核---从MS-DOS、Windows到高级终端大型机&#xff0c;但是各种系统对内核的态度不同&#xff0c;有些系统花费了很大精力对用户隐藏内核。不管是Windows或UNIX系统的发…...