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

[苍穹外卖]-08微信支付详解

地址簿管理

分析需求: 查询地址列表/新增地址/修改地址/删除地址/设置默认地址/查询默认地址

接口设计

  1. 新增地址接口

  1. 查询用户所有的地址接口

  1. 查询默认地址接口

  1. 根据id修改地址接口

  1. 根据id删除地址接口

  1. 根据id查询地址接口

  1. 设置默认地址接口

数据库设计: 收货地址簿(address_book表)

代码导入

功能测试

用户下单

需求分析和设计

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

用户下单后会产生订单相关数据, 订单数据需要有: 商品名称/商品数量/订单金额/用户名称/手机号/收货地址

接口分析

接口设计

数据库设计

代码开发

设计VO: 根据用户下单接口返回的数据设计OrderSubmitVO, 用户封装返回给前端的数据

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OrderSubmitVO implements Serializable {//订单idprivate Long id;//订单号private String orderNumber;//订单金额private BigDecimal orderAmount;//下单时间private LocalDateTime orderTime;
}

设计DTO: 根据用户下单接口的参数设计OrdersSubmitDTO, 用于封装前端传递的参数

@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;
}

Controller: 新建user/OrderController

@RestController("userOrderController")
@RequestMapping("/user/order")
@Slf4j
@Api(tags = "用户端订单相关接口")
public class OrderController {@Autowiredprivate 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;//下单用户idprivate Long userId;//地址idprivate 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;//订单idprivate Long orderId;//菜品idprivate Long dishId;//套餐idprivate 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 {@Autowiredprivate OrderMapper orderMapper;@Autowiredprivate OrderDetailMapper orderDetailMapper;@Autowiredprivate AddressBookMapper addressBookMapper;@Autowiredprivate ShoppingCartMapper shoppingCartMapper;/*** 用户下单** @param ordersSubmitDTO* @return*/@Transactionalpublic 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()); // 设置当前明细关联的订单idorderDetailList.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. 主要逻辑:
  • 处理各种业务异常(地址簿为空, 购物车数据为空)
  • 向订单表插入1条数据
  • 向订单明细表插入n条数据
  • 清空当前用户的购物车数据
  • 封装VO返回数据
  1. 虽然前端对异常情况进行了限制, 但是为了代码的健壮性, 后端还是要进行判断
  2. 通过事务注解进行事务管理

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和商户号进行支付

微信小程序支付时序图

重要步骤

  1. 后端调用微信官方的预支付接口,拿到预支付订单,返给前端
  2. 前端根据预支付订单发起微信支付
  3. 微信服务器把支付结果返给后端, 后端更新支付状态

预支付交易订单(JSAPI下单): 商户系统调用该接口在微信支付服务后台生成预支付交易单, 目的是获取预支付交易会话标识

调起支付: 使用微信支付提供的小程序方法调起小程序支付, 传入预支付交易会话标识(prepay_id), 完成支付

准备工作

为了保证支付的安全, 在支付过程中要对数据加密/解密/签名, 就要使用到微信支付平台证书/商户私钥文件, 这两个文件要在商户管理后台获取

支付成功后, 微信服务器要通过我们配置的域名, 回调我们的系统程序, 通知我们用户支付的结果

内网穿透:

  1. 我们的程序处于开发阶段, 没有部署到公网上, 只在本地的局域网运行
  2. 所以需要使用内网穿透技术, 获得一个临时公网ip, 供微信后台回调

软件安装: 通过官网下载, 或者使用资料中的安装包, wind双击安装即可

验证: 登录官网 cpolar - secure introspectable tunnels to localhost, 选择验证菜单, 复制 Authtoken 值

生成配置文件: 在软件安装目录, 打开cmd窗口, 执行命令 cpolar.exe authtoken XXXXXXXXX值

  • 生成配置文件, 只需执行一次就行

启动服务: 软件安装目录, 打开cmd窗口, 执行命令 cpolar.exe http 8080

  1. 苍穹外卖后端服务运行在8080端口, 所以在命令中要指定映射端口是8080
  2. 访问测试: https://1ded711e.r27.cpolar.top/doc.html#/home

代码导入

配置微信支付相关的参数

准备配置属性封装类, 用来封装配置文件中的数据, 使用时注入该类, 就可以获取配置文件中的数据

@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; //退款成功的回调地址}

导入代码: 新增nofity包存放PayNotifyController.java文件, 处理微信后台的消息, 其他代码按需复制不要覆盖

阅读代码

用户进行订单支付, 首先访问OrderController的支付接口, 前端要把订单号传递过来, 我们使用DTO接收

@RestController("userOrderController")
@RequestMapping("/user/order")
@Slf4j
@Api(tags = "用户端订单相关接口")
public class OrderController {@Autowiredprivate 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 {@Autowiredprivate UserMapper userMapper;@Autowiredprivate 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 {// 当前登录用户idLong 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;}}
  1. 通过weChatPayUtil工具类中的pay方法完成订单支付
  2. pay方法首先调用 jsapi方法完成订单数据的封装
  3. 再调用 post方法请求微信后台的下单接口, 完成下单操作, 并获取预支付交易标识
  4. 对支付数据进行一系列的封装, 加密和签名, 最终得的一个JSON对象, 供前端用于调起微信支付
  5. 把JSON数据在封装到VO对象中, 返回给前端

前端微信支付完成后, 微信后台服务器会回调我们的系统, 通知用户支付的结果, 我们使用controller接收并处理

/*** 支付回调相关接口*/
@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();}}
  1. 用户支付成功, 微信后台会返回给我们用户支付的数据, 我们进行数据读取和解密
  2. 再调用orderService的paySuccess方法, 修改该订单的状态为成功
  3. 订单状态修改完成后, 我们要带给微信后台一个响应, 否则微信后台会一直通知我们

修改订单状态

@Service
@Slf4j
public class OrderServiceImpl implements OrderService {@Autowiredprivate 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>

功能测试

http://www.lryc.cn/news/435091.html

相关文章:

  • 教你五句在酒桌上和领导说的话语
  • 景联文科技:专业图像采集服务,助力智能图像分析
  • QT QTcpSocket作为客户端
  • 【系统架构设计师-2023年】综合知识-答案及详解
  • 树莓派3B点灯(1)-- 四种方法
  • Android解析XML格式数据
  • 数学建模笔记—— 灰色关联分析[GRA]
  • ICM20948 DMP代码详解(13)
  • 【论软件需求获取方法及其应用】
  • 使用ESP8266和OLED屏幕实现一个小型电脑性能监控
  • Nexpose v6.6.266 for Linux Windows - 漏洞扫描
  • ess6新特性
  • C语言蓝桥杯:语言基础
  • axure之变量
  • vue缓存用法
  • 栈入门,括号匹配问题
  • Vue入门学习笔记-表单
  • TCP通信三次握手、四次挥手
  • 【实施文档】软件项目实施方案(Doc原件2024实际项目)
  • BeanFactory vs. ApplicationContext
  • JDBC客户端连接Starrocks 2.5
  • 004——双向链表和循环链表
  • framebuffer帧缓存
  • 24_竞赛中的高效并查集
  • 新手c语言讲解及题目分享(十七)--运算符与表达式专项练习
  • 香帅的金融学讲义:深入剖析与解读
  • java基础-IO(6)转换流InputStreamReader、OutputStreamWriter
  • 使用Azure Devops Pipeline将Docker应用部署到你的Raspberry Pi上
  • 91、K8s之ingress上集
  • NISP 一级 | 2.1 密码学