继续上一节的内容,本节导入地址簿功能代码,并实现用户下单和订单支付功能。
这里写目录标题
- 导入地址簿功能代码
- 接口分析
- 代码实现
- 用户下单
- 接口分析
- 代码实现
- 订单支付
- 内网穿透——cpolar软件
- 代码导入
- 绕开微信支付实现
导入地址簿功能代码
地址簿,指的是消费者用户的地址信息,用户登录成功后可以维护自己的地址信息。同一个用户可以有多个地址信息,但是只能有一个默认地址。
接口分析
对于地址簿管理,我们需要实现以下几个功能:查询地址列表、新增地址、修改地址、删除地址、设置默认地址、查询默认地址。共包含7个接口:
1). 新增地址
2). 查询登录用户所有地址
3). 查询默认地址
4). 修改地址
5). 根据id删除地址
6). 根据id查询地址
7). 设置默认地址
用户的地址信息会存储在address_book表,即地址簿表中。这里面有一个字段is_default,实际上我们在设置默认地址时,只需要更新这个字段就可以了。
字段名 | 数据类型 | 说明 | 备注 |
---|---|---|---|
id | bigint | 主键 | 自增 |
user_id | bigint | 用户id | 逻辑外键 |
consignee | varchar(50) | 收货人 | |
sex | varchar(2) | 性别 | |
phone | varchar(11) | 手机号 | |
province_code | varchar(12) | 省份编码 | |
province_name | varchar(32) | 省份名称 | |
city_code | varchar(12) | 城市编码 | |
city_name | varchar(32) | 城市名称 | |
district_code | varchar(12) | 区县编码 | |
district_name | varchar(32) | 区县名称 | |
detail | varchar(200) | 详细地址信息 | 具体到门牌号 |
label | varchar(100) | 标签 | 公司、家、学校 |
is_default | tinyint(1) | 是否默认地址 | 1是 0否 |
代码实现
对于这一类的单表的增删改查,我们已经写过很多了,直接导入进来,做一个测试即可。进入到sky-server模块中导入课程资料中的地址簿模块功能代码:
Controller层
com.sky.controller.user.AddressBookController
@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);if (list != null && list.size() == 1) {return Result.success(list.get(0));}return Result.error("没有查询到默认地址");}}
Service层实现类
com.sky.service.impl.AddressBookServiceImpl
@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());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);}}
Mapper层
com.sky.mapper.AddressBookMapper
@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);}
mapper/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>
使用前后端联调测试:
登录进入首页–>进入个人中心–>进入地址管理
在这里进行新增收货地址、查看收货地址、设置默认收货地址、删除收货地址等接口的测试,同时观察数据库数据确保无误。测试通过提交代码。
用户下单
用户是通过下单的方式通知商家,用户已经购买了商品,需要商家进行备货和发货。用户下单后会产生订单相关数据,订单数据需要能够体现如下信息:
用户将菜品或者套餐加入购物车后,可以点击购物车中的 “去结算” 按钮,页面跳转到订单确认页面,点击 “去支付” 按钮则完成下单操作。效果图如下所示:
接口分析
接口如下:
用户下单业务对应的数据表为orders表和order_detail表(一对多关系,一个订单关联多个订单明细),用户提交订单时,需要往订单表orders中插入一条记录,并且需要往order_detail中插入一条或多条记录(来自购物车)。
表名 | 含义 | 说明 |
---|---|---|
orders | 订单表 | 主要存储订单的基本信息(如: 订单号、状态、金额、支付方式、下单用户、收件地址等) |
order_detail | 订单明细表 | 主要存储订单详情信息(如: 该订单关联的套餐及菜品的信息) |
1). orders订单表
字段名 | 数据类型 | 说明 | 备注 |
---|---|---|---|
id | bigint | 主键 | 自增 |
number | varchar(50) | 订单号 | |
status | int | 订单状态 | 1待付款 2待接单 3已接单 4派送中 5已完成 6已取消 |
user_id | bigint | 用户id | 逻辑外键 |
address_book_id | bigint | 地址id | 逻辑外键 |
order_time | datetime | 下单时间 | |
checkout_time | datetime | 付款时间 | |
pay_method | int | 支付方式 | 1微信支付 2支付宝支付 |
pay_status | tinyint | 支付状态 | 0未支付 1已支付 2退款 |
amount | decimal(10,2) | 订单金额 | |
remark | varchar(100) | 备注信息 | |
phone | varchar(11) | 手机号 | 冗余字段 |
address | varchar(255) | 详细地址信息 | 冗余字段 |
consignee | varchar(32) | 收货人 | 冗余字段 |
cancel_reason | varchar(255) | 订单取消原因 | |
rejection_reason | varchar(255) | 拒单原因 | |
cancel_time | datetime | 订单取消时间 | |
estimated_delivery_time | datetime | 预计送达时间 | |
delivery_status | tinyint | 配送状态 | 1立即送出 0选择具体时间 |
delivery_time | datetime | 送达时间 | |
pack_amount | int | 打包费 | |
tableware_number | int | 餐具数量 | |
tableware_status | tinyint | 餐具数量状态 | 1按餐量提供 0选择具体数量 |
2). order_detail订单明细表
字段名 | 数据类型 | 说明 | 备注 |
---|---|---|---|
id | bigint | 主键 | 自增 |
name | varchar(32) | 商品名称 | 冗余字段 |
image | varchar(255) | 商品图片路径 | 冗余字段 |
order_id | bigint | 订单id | 逻辑外键 |
dish_id | bigint | 菜品id | 逻辑外键 |
setmeal_id | bigint | 套餐id | 逻辑外键 |
dish_flavor | varchar(50) | 菜品口味 | |
number | int | 商品数量 | |
amount | decimal(10,2) | 商品单价 |
根据用户下单接口的参数设计DTO,在sky-pojo模块,OrdersSubmitDTO.java已定义。
根据用户下单接口的返回结果设计VO,在sky-pojo模块,OrderSubmitVO.java已定义。
代码实现
Controller层
创建com.sky.controller.user.OrderController并提供用户下单方法
/*** 订单*/
@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);}}
Service层实现类
创建com.sky.service.impl.OrderServiceImpl实现OrderService接口
/*** 订单*/
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {@Autowiredprivate OrderMapper orderMapper; // 订单表@Autowiredprivate OrderDetailMapper orderDetailMapper; // 订单明细表@Autowiredprivate ShoppingCartMapper shoppingCartMapper;@Autowiredprivate AddressBookMapper addressBookMapper;/*** 用户下单** @param ordersSubmitDTO* @return*/@Transactionalpublic OrderSubmitVO submitOrder(OrdersSubmitDTO ordersSubmitDTO) {//异常情况的处理(收货地址为空、超出配送范围、购物车为空)AddressBook addressBook = addressBookMapper.getById(ordersSubmitDTO.getAddressBookId()); //根据地址簿id查询收货地址信息if (addressBook == null) {throw new AddressBookBusinessException(MessageConstant.ADDRESS_BOOK_IS_NULL);//收货地址为空异常}Long userId = BaseContext.getCurrentId();ShoppingCart shoppingCart = new ShoppingCart();shoppingCart.setUserId(userId); //构造一个购物车对象用于根据用户id查询购物车数据//查询当前用户的购物车数据List<ShoppingCart> shoppingCartList = shoppingCartMapper.list(shoppingCart);if (shoppingCartList == null || shoppingCartList.size() == 0) {throw new ShoppingCartBusinessException(MessageConstant.SHOPPING_CART_IS_NULL);//购物车为空异常}//构造订单数据Orders order = new Orders();BeanUtils.copyProperties(ordersSubmitDTO,order);order.setPhone(addressBook.getPhone());order.setAddress(addressBook.getDetail());order.setConsignee(addressBook.getConsignee());order.setNumber(String.valueOf(System.currentTimeMillis())); //订单号就用时间戳order.setUserId(userId);order.setStatus(Orders.PENDING_PAYMENT); //设置订单状态为代付款order.setPayStatus(Orders.UN_PAID); //设置支付状态为未支付order.setOrderTime(LocalDateTime.now());//向订单表插入1条数据orderMapper.insert(order);//订单明细数据List<OrderDetail> orderDetailList = new ArrayList<>();for (ShoppingCart cart : shoppingCartList) {OrderDetail orderDetail = new OrderDetail();BeanUtils.copyProperties(cart, orderDetail);orderDetail.setOrderId(order.getId());orderDetailList.add(orderDetail);}//向明细表插入n条数据orderDetailMapper.insertBatch(orderDetailList);//清理购物车中的数据shoppingCartMapper.deleteByUserId(userId);//封装返回结果OrderSubmitVO orderSubmitVO = OrderSubmitVO.builder().id(order.getId()).orderNumber(order.getNumber()).orderAmount(order.getAmount()).orderTime(order.getOrderTime()).build();return orderSubmitVO;}}
Mapper层
创建OrderMapper接口和对应的xml映射文件
OrderMapper.java
@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
@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>
功能测试,登录小程序,完成下单操作,下单操作时,同时会删除购物车中的数据
查看shopping_cart表:
去结算–>去支付
查看orders表:
查看order_detail表:
同时,购物车表中数据被删除:
测试通过,提交代码到github。
订单支付
已经实现了用户下单,那接下来就是订单支付,就是完成付款功能。要实现微信支付就需要注册微信支付的一个商户号,这个商户号是必须要有一家企业并且有正规的营业执照。只有具备了这些资质之后,才可以去注册商户号,才能开通支付权限。
个人不具备这种资质,所以我们在学习微信支付时,主要是是了解微信支付的流程,并且能够阅读微信官方提供的接口文档,能够和第三方支付平台对接起来就可以了。微信支付产品:
本项目选择小程序支付,可以参考官网,微信小程序支付时序图:
微信支付相关接口:
JSAPI下单:商户系统调用该接口在微信支付服务后台生成预支付交易单(对应时序图的第5步)
微信小程序调起支付:通过JSAPI下单接口获取到发起支付的必要参数prepay_id,然后使用微信支付提供的小程序方法调起小程序支付(对应时序图的第10步)
如何保证数据安全?完成微信支付有两个关键的步骤:
第一个就是需要在商户系统当中调用微信后台的一个下单接口,就是生成预支付交易单。第二个就是支付成功之后微信后台会给推送消息。这两个接口数据的安全性,要求其实是非常高的。
微信提供的方式就是对数据进行加密、解密、签名多种方式。要完成数据加密解密,需要提前准备相应的一些文件,其实就是一些证书。微信支付平台证书、商户私钥文件:
内网穿透——cpolar软件
微信后台会调用到商户系统给推送支付的结果,在这里我们就会遇到一个问题,就是微信后台怎么就能调用到我们这个商户系统呢?因为这个调用过程,其实本质上也是一个HTTP请求。
目前,商户系统它的ip地址就是当前自己电脑的ip地址,只是一个局域网内的ip地址,微信后台无法调用到。
解决:内网穿透。通过cpolar软件可以获得一个临时域名,而这个临时域名是一个公网ip,这样,微信后台就可以请求到商户系统了。下载地址
复制authtoken:
在安装目录下执行下面的命令在我们的电脑上设置配置文件:
windows系统是执行命令:cpolar.exe authoken ***
再执行命令获取临时域名:
访问接口文档,原本是localhost:8080/doc.html,现在使用临时域名访问验证临时域名有效性:
代码导入
老师让导入资料中的代码,导个钩子,导了也不能用,秘钥都是过期的,微信支付平台证书、商户私钥文件老师也不能提供,涉及商户隐私。代码放这,以后开发的时候需要用到的时候再看看吧。
微信支付相关配置
application-dev.yml
sky:wechat:appid: # 小程序的appidsecret: # 小程序的秘钥mchid : # 商户号mchSerialNo: # 商户API证书的证书序列号privateKeyFilePath: # 商户私钥文件路径apiV3Key: # 证书解密的密钥weChatPayCertFilePath: # 平台证书路径notifyUrl: # 支付成功的回调地址 我们这里要填写内网穿透后的地址# 比如老师的 notifyUrl: https://6619cf50.r6.cpolar.top/notify/paySuccessrefundNotifyUrl: # 退款成功的回调地址 我们这里要填写内网穿透后的地址
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:读取配置(已定义)
@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; //退款成功的回调地址
}
Controller层
在OrderController.java中添加payment方法
/*** 订单支付** @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);
}
com.sky.controller.notify.PayNotifyController.java
/*** 支付回调相关接口*/
@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();}
}
Service层实现类
在OrderServiceImpl.java中实现payment和paySuccess两个方法
@Autowired
private UserMapper userMapper;
@Autowired
private WeChatPayUtil weChatPayUtil;
/*** 订单支付** @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;
}/*** 支付成功,修改订单状态** @param outTradeNo*/
public void paySuccess(String outTradeNo) {// 当前登录用户idLong userId = BaseContext.getCurrentId();// 根据订单号查询当前用户的订单Orders ordersDB = orderMapper.getByNumberAndUserId(outTradeNo, userId);// 根据订单id更新订单的状态、支付方式、支付状态、结账时间Orders orders = Orders.builder().id(ordersDB.getId()).status(Orders.TO_BE_CONFIRMED).payStatus(Orders.PAID).checkoutTime(LocalDateTime.now()).build();orderMapper.update(orders);
}
其中WeChatPayUtil已经在common模块下的com.sky.utils中已定义好:
/*** 微信支付工具类*/
@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方式请求** @param url* @param body* @return*/private String post(String url, String body) throws Exception {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());jsonObject.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();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 {//统一下单,生成预支付交易单String bodyAsString = jsapi(orderNum, total, description, openid);//解析返回结果JSONObject jsonObject = JSON.parseObject(bodyAsString);System.out.println(jsonObject);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());//构造数据给微信小程序,用于调起微信支付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);}
}
Mapper层
在OrderMapper.java中添加getByNumberAndUserId和update两个方法
/*** 根据订单号和用户id查询订单* @param orderNumber* @param userId*/
@Select("select * from orders where number = #{orderNumber} and user_id= #{userId}")
Orders getByNumberAndUserId(String orderNumber, Long userId);/*** 修改订单信息* @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>
绕开微信支付实现
由于个人无法申请微信支付资质,固有必要绕开这部分代码以继续这个项目。可以直接修改微信小程序代码中的这一部分内容,将requestPayment方法整体注释掉,然后将下方的重定向注释取消即可:
后端代码,路径:OrderServiceImpl中的payment方法中,将如下部分注释掉:
相当于没真正执行paysuccess方法,所以将修改订单状态的逻辑也加入到payment函数中,最后payment函数如下:
/*** 订单支付** @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("该订单已支付");
// }JSONObject jsonObject = new JSONObject();jsonObject.put("code", "ORDERPAID");OrderPaymentVO vo = jsonObject.toJavaObject(OrderPaymentVO.class);vo.setPackageStr(jsonObject.getString("package"));// 替代微信支付成功后的数据库订单状态更新,直接在这里更新了// 根据订单号查询当前用户的该订单Orders ordersDB = orderMapper.getByNumberAndUserId(ordersPaymentDTO.getOrderNumber(), userId);// 根据订单id更新订单的状态、支付方式、支付状态、结账时间Orders orders = Orders.builder().id(ordersDB.getId()).status(Orders.TO_BE_CONFIRMED) // 订单状态,待接单.payStatus(Orders.PAID) // 支付状态,已支付.checkoutTime(LocalDateTime.now()) // 更新支付时间.build();orderMapper.update(orders);return vo;
}
后端和小程序端都重新编译,重新请求即可,最后可以看到已经支付成功:
测试成功,提交代码到github。