[苍穹外卖]-08微信支付详解
地址簿管理
分析需求: 查询地址列表/新增地址/修改地址/删除地址/设置默认地址/查询默认地址
接口设计
- 新增地址接口
- 查询用户所有的地址接口
- 查询默认地址接口
- 根据id修改地址接口
- 根据id删除地址接口
- 根据id查询地址接口
- 设置默认地址接口
数据库设计: 收货地址簿(address_book表)
代码导入
功能测试
用户下单
需求分析和设计
在电商系统中,用户是通过下单的方式通知商家, 用户已经购买了商品, 需要商家备货和发货
用户下单后会产生订单相关数据, 订单数据需要有: 商品名称/商品数量/订单金额/用户名称/手机号/收货地址
接口分析
接口设计
数据库设计
代码开发
设计VO: 根据用户下单接口返回的数据设计OrderSubmitVO, 用户封装返回给前端的数据
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OrderSubmitVO implements Serializable {
//订单id
private Long id;
//订单号
private String orderNumber;
//订单金额
private BigDecimal orderAmount;
//下单时间
private LocalDateTime orderTime;
}
设计DTO: 根据用户下单接口的参数设计OrdersSubmitDTO, 用于封装前端传递的参数
@Data
public class OrdersSubmitDTO implements Serializable {
//地址簿id
private 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;
}
Controller: 新建user/OrderController
@RestController("userOrderController")
@RequestMapping("/user/order")
@Slf4j
@Api(tags = "用户端订单相关接口")
public class OrderController {
@Autowired
private OrderService orderService;
@PostMapping("/submit")
@ApiOperation("用户下单")
public Result<OrderSubmitVO> submit(@RequestBody OrdersSubmitDTO ordersSubmitDTO) {
log.info("用户下单, 数据: {}", ordersSubmitDTO);
OrderSubmitVO orderSubmitVO = orderService.submitOrder(ordersSubmitDTO);
return Result.success(orderSubmitVO);
}
}
设计Orders订单实体类, 用于封装插入到订单表的数据, 字段与订单表一致
/**
* 订单
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Orders implements Serializable {
/**
* 订单状态 1待付款 2待接单 3已接单 4派送中 5已完成 6已取消
*/
public static final Integer PENDING_PAYMENT = 1;
public static final Integer TO_BE_CONFIRMED = 2;
public static final Integer CONFIRMED = 3;
public static final Integer DELIVERY_IN_PROGRESS = 4;
public static final Integer COMPLETED = 5;
public static final Integer CANCELLED = 6;
/**
* 支付状态 0未支付 1已支付 2退款
*/
public static final Integer UN_PAID = 0;
public static final Integer PAID = 1;
public static final Integer REFUND = 2;
private static final long serialVersionUID = 1L;
private Long id;
//订单号
private String number;
//订单状态 1待付款 2待接单 3已接单 4派送中 5已完成 6已取消 7退款
private Integer status;
//下单用户id
private Long userId;
//地址id
private Long addressBookId;
//下单时间
private LocalDateTime orderTime;
//结账时间
private LocalDateTime checkoutTime;
//支付方式 1微信,2支付宝
private Integer payMethod;
//支付状态 0未支付 1已支付 2退款
private Integer payStatus;
//实收金额
private BigDecimal amount;
//备注
private String remark;
//用户名
private String userName;
//手机号
private String phone;
//地址
private String address;
//收货人
private String consignee;
//订单取消原因
private String cancelReason;
//订单拒绝原因
private String rejectionReason;
//订单取消时间
private LocalDateTime cancelTime;
//预计送达时间
private LocalDateTime estimatedDeliveryTime;
//配送状态 1立即送出 0选择具体时间
private Integer deliveryStatus;
//送达时间
private LocalDateTime deliveryTime;
//打包费
private int packAmount;
//餐具数量
private int tablewareNumber;
//餐具数量状态 1按餐量提供 0选择具体数量
private Integer tablewareStatus;
}
设计Orders订单明细实体类, 用于封装插入到订单明细表的数据, 字段与订单明细表一致
/**
* 订单明细
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OrderDetail implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
//名称
private String name;
//订单id
private Long orderId;
//菜品id
private Long dishId;
//套餐id
private Long setmealId;
//口味
private String dishFlavor;
//数量
private Integer number;
//金额
private BigDecimal amount;
//图片
private String image;
}
Service: 新建OrderService接口和实现类
@Service
public interface OrderService {
/**
* 用户下单
* @param ordersSubmitDTO
* @return
*/
OrderSubmitVO submitOrder(OrdersSubmitDTO ordersSubmitDTO);
}
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private OrderDetailMapper orderDetailMapper;
@Autowired
private AddressBookMapper addressBookMapper;
@Autowired
private ShoppingCartMapper shoppingCartMapper;
/**
* 用户下单
*
* @param ordersSubmitDTO
* @return
*/
@Transactional
public OrderSubmitVO submitOrder(OrdersSubmitDTO ordersSubmitDTO) {
// 处理可能的业务异常(地址簿为空, 购物车为空)
AddressBook addressBook = addressBookMapper.getById(ordersSubmitDTO.getAddressBookId());
if (addressBook == null) {
// 地址簿为空,抛出业务异常
throw new AddressBookBusinessException(MessageConstant.ADDRESS_BOOK_IS_NULL);
}
Long userId = BaseContext.getCurrentId();
ShoppingCart shoppingCart = new ShoppingCart();
shoppingCart.setUserId(userId);
List<ShoppingCart> list = shoppingCartMapper.list(shoppingCart);
if (list.size() == 0 && list == null) {
// 购物车为空,抛出业务异常
throw new ShoppingCartBusinessException(MessageConstant.SHOPPING_CART_IS_NULL);
}
// 向订单表插入1条数据
Orders orders = new Orders();
BeanUtils.copyProperties(ordersSubmitDTO, orders);
orders.setOrderTime(LocalDateTime.now());
orders.setPayStatus(Orders.UN_PAID);
orders.setStatus(Orders.PENDING_PAYMENT);
orders.setNumber(String.valueOf(System.currentTimeMillis()));
orders.setPhone(addressBook.getPhone());
orders.setConsignee(addressBook.getConsignee());
orders.setUserId(userId);
orderMapper.insert(orders);
// 向订单明细表插入n条数据
ArrayList<OrderDetail> orderDetailList = new ArrayList<>();
for (ShoppingCart cart : list) {
OrderDetail orderDetail = new OrderDetail(); // 订单明细
BeanUtils.copyProperties(cart, orderDetail);
orderDetail.setOrderId(orders.getId()); // 设置当前明细关联的订单id
orderDetailList.add(orderDetail);
}
orderDetailMapper.insertBatch(orderDetailList);
// 清空当前用户的购物车数据
shoppingCartMapper.deleteByUserId(userId);
// 封装VO返回结果
OrderSubmitVO orderSubmitVO = OrderSubmitVO.builder()
.id(orders.getId())
.orderTime(orders.getOrderTime())
.orderNumber(orders.getNumber())
.orderAmount(orders.getAmount())
.build();
return orderSubmitVO;
}
}
- 主要逻辑:
- 处理各种业务异常(地址簿为空, 购物车数据为空)
- 向订单表插入1条数据
- 向订单明细表插入n条数据
- 清空当前用户的购物车数据
- 封装VO返回数据
- 虽然前端对异常情况进行了限制, 但是为了代码的健壮性, 后端还是要进行判断
- 通过事务注解进行事务管理
Maaper: 新建OrderMapper(操作订单表) 和 OrderDetailMapper(操作订单明细表)
@Mapper
public interface OrderMapper {
/**
* 插入订单数据
* @param orders
*/
void insert(Orders orders);
}
<mapper namespace="com.sky.mapper.OrderMapper">
<insert id="insert" 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>
@Mapper
public interface OrderDetailMapper {
/**
* 批量插入订单明细数据
* @param orderDetailList
*/
void insertBatch(ArrayList<OrderDetail> orderDetailList);
}
<mapper namespace="com.sky.mapper.OrderDetailMapper">
<insert id="insertBatch">
insert into order_detail (name,image,order_id,dish_id,setmeal_id,dish_flavor,number,amount)
values
<foreach collection="orderDetailList" item="od" separator=",">
(#{od.name},#{od.image},#{od.orderId},#{od.dishId},#{od.setmealId},#{od.dishFlavor},#{od.number},#{od.amount})
</foreach>
</insert>
</mapper>
测试
订单支付
介绍
资质限制: 需要使用营业执照申请商户号, 个人账户无法开通支付权限, 有了商户号可以开通支付资质
微信支付的接入流程: 在商户平台把商户号与AppId绑定后, 就可以使用appid和商户号进行支付
微信小程序支付时序图
重要步骤
- 后端调用微信官方的预支付接口,拿到预支付订单,返给前端
- 前端根据预支付订单发起微信支付
- 微信服务器把支付结果返给后端, 后端更新支付状态
预支付交易订单(JSAPI下单): 商户系统调用该接口在微信支付服务后台生成预支付交易单, 目的是获取预支付交易会话标识
调起支付: 使用微信支付提供的小程序方法调起小程序支付, 传入预支付交易会话标识(prepay_id), 完成支付
准备工作
为了保证支付的安全, 在支付过程中要对数据加密/解密/签名, 就要使用到微信支付平台证书/商户私钥文件, 这两个文件要在商户管理后台获取
支付成功后, 微信服务器要通过我们配置的域名, 回调我们的系统程序, 通知我们用户支付的结果
内网穿透:
- 我们的程序处于开发阶段, 没有部署到公网上, 只在本地的局域网运行
- 所以需要使用内网穿透技术, 获得一个临时公网ip, 供微信后台回调
软件安装: 通过官网下载, 或者使用资料中的安装包, wind双击安装即可
验证: 登录官网 cpolar - secure introspectable tunnels to localhost, 选择验证菜单, 复制 Authtoken 值
生成配置文件: 在软件安装目录, 打开cmd窗口, 执行命令 cpolar.exe authtoken XXXXXXXXX值
- 生成配置文件, 只需执行一次就行
启动服务: 软件安装目录, 打开cmd窗口, 执行命令 cpolar.exe http 8080
- 苍穹外卖后端服务运行在8080端口, 所以在命令中要指定映射端口是8080
- 访问测试: https://1ded711e.r27.cpolar.top/doc.html#/home
代码导入
配置微信支付相关的参数
准备配置属性封装类, 用来封装配置文件中的数据, 使用时注入该类, 就可以获取配置文件中的数据
@Component
@ConfigurationProperties(prefix = "sky.wechat")
@Data
public class WeChatProperties {
private String appid; //小程序的appid
private String secret; //小程序的秘钥
private String mchid; //商户号
private String mchSerialNo; //商户API证书的证书序列号
private String privateKeyFilePath; //商户私钥文件
private String apiV3Key; //证书解密的密钥
private String weChatPayCertFilePath; //平台证书
private String notifyUrl; //支付成功的回调地址
private String refundNotifyUrl; //退款成功的回调地址
}
导入代码: 新增nofity包存放PayNotifyController.java文件, 处理微信后台的消息, 其他代码按需复制不要覆盖
阅读代码
用户进行订单支付, 首先访问OrderController的支付接口, 前端要把订单号传递过来, 我们使用DTO接收
@RestController("userOrderController")
@RequestMapping("/user/order")
@Slf4j
@Api(tags = "用户端订单相关接口")
public class OrderController {
@Autowired
private OrderService orderService;
/**
* 订单支付
*
* @param ordersPaymentDTO
* @return
*/
@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);
}
}
再调用orderService中payment方法, 处理支付逻辑
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
@Autowired
private UserMapper userMapper;
@Autowired
private WeChatPayUtil weChatPayUtil;
@Value("${sky.shop.address}")
private String shopAddress;
@Value("${sky.baidu.ak}")
private String ak;
/**
* 订单支付
*
* @param ordersPaymentDTO
* @return
*/
public OrderPaymentVO payment(OrdersPaymentDTO ordersPaymentDTO) throws Exception {
// 当前登录用户id
Long userId = BaseContext.getCurrentId();
User user = userMapper.getById(userId);
//调用微信支付接口,生成预支付交易单
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("该订单已支付");
}
OrderPaymentVO vo = jsonObject.toJavaObject(OrderPaymentVO.class);
vo.setPackageStr(jsonObject.getString("package"));
return vo;
}
}
- 通过weChatPayUtil工具类中的pay方法完成订单支付
- pay方法首先调用 jsapi方法完成订单数据的封装
- 再调用 post方法请求微信后台的下单接口, 完成下单操作, 并获取预支付交易标识
- 对支付数据进行一系列的封装, 加密和签名, 最终得的一个JSON对象, 供前端用于调起微信支付
- 把JSON数据在封装到VO对象中, 返回给前端
前端微信支付完成后, 微信后台服务器会回调我们的系统, 通知用户支付的结果, 我们使用controller接收并处理
/**
* 支付回调相关接口
*/
@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();
}
}
- 用户支付成功, 微信后台会返回给我们用户支付的数据, 我们进行数据读取和解密
- 再调用orderService的paySuccess方法, 修改该订单的状态为成功
- 订单状态修改完成后, 我们要带给微信后台一个响应, 否则微信后台会一直通知我们
修改订单状态
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderMapper orderMapper;
/**
* 支付成功,修改订单状态
*
* @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);
}
}
@Mapper
public interface OrderMapper {
/**
* 根据订单号查询订单
*
* @param orderNumber
*/
@Select("select * from orders where number = #{orderNumber}")
Orders getByNumber(String orderNumber);
/**
* 修改订单信息
*
* @param orders
*/
void update(Orders orders);
}
<?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">
<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>
</mapper>