继续上一节的内容,本节导入地址簿功能代码,并实现用户下单和订单支付功能。
地址簿,指的是消费者用户的地址信息,用户登录成功后可以维护自己的地址信息。同一个用户可以有多个地址信息,但是只能有一个默认地址。
对于地址簿管理,我们需要实现以下几个功能:查询地址列表、新增地址、修改地址、删除地址、设置默认地址、查询默认地址。共包含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 {
@Autowired
private 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 = 1
AddressBook 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 {
@Autowired
private 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
*/
@Transactional
public 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 {
@Autowired
private 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 {
@Autowired
private OrderMapper orderMapper; // 订单表
@Autowired
private OrderDetailMapper orderDetailMapper; // 订单明细表
@Autowired
private ShoppingCartMapper shoppingCartMapper;
@Autowired
private AddressBookMapper addressBookMapper;
/**
* 用户下单
*
* @param ordersSubmitDTO
* @return
*/
@Transactional
public 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步)
如何保证数据安全?完成微信支付有两个关键的步骤:
第一个就是需要在商户系统当中调用微信后台的一个下单接口,就是生成预支付交易单。第二个就是支付成功之后微信后台会给推送消息。这两个接口数据的安全性,要求其实是非常高的。
微信提供的方式就是对数据进行加密、解密、签名多种方式。要完成数据加密解密,需要提前准备相应的一些文件,其实就是一些证书。微信支付平台证书、商户私钥文件:
微信后台会调用到商户系统给推送支付的结果,在这里我们就会遇到一个问题,就是微信后台怎么就能调用到我们这个商户系统呢?因为这个调用过程,其实本质上也是一个HTTP请求。
目前,商户系统它的ip地址就是当前自己电脑的ip地址,只是一个局域网内的ip地址,微信后台无法调用到。
解决:内网穿透。通过cpolar软件可以获得一个临时域名,而这个临时域名是一个公网ip,这样,微信后台就可以请求到商户系统了。下载地址
复制authtoken:
在安装目录下执行下面的命令在我们的电脑上设置配置文件:
windows系统是执行命令:cpolar.exe authoken ***
再执行命令获取临时域名:
访问接口文档,原本是localhost:8080/doc.html,现在使用临时域名访问验证临时域名有效性:
老师让导入资料中的代码,导个钩子,导了也不能用,秘钥都是过期的,微信支付平台证书、商户私钥文件老师也不能提供,涉及商户隐私。代码放这,以后开发的时候需要用到的时候再看看吧。
微信支付相关配置
application-dev.yml
sky:
wechat:
appid: # 小程序的appid
secret: # 小程序的秘钥
mchid : # 商户号
mchSerialNo: # 商户API证书的证书序列号
privateKeyFilePath: # 商户私钥文件路径
apiV3Key: # 证书解密的密钥
weChatPayCertFilePath: # 平台证书路径
notifyUrl: # 支付成功的回调地址 我们这里要填写内网穿透后的地址
# 比如老师的 notifyUrl: https://6619cf50.r6.cpolar.top/notify/paySuccess
refundNotifyUrl: # 退款成功的回调地址 我们这里要填写内网穿透后的地址
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; //小程序的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; //退款成功的回调地址
}
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 {
@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();
}
}
Service层实现类
在OrderServiceImpl.java中实现payment和paySuccess两个方法
@Autowired
private UserMapper userMapper;
@Autowired
private WeChatPayUtil weChatPayUtil;
/**
* 订单支付
*
* @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;
}
/**
* 支付成功,修改订单状态
*
* @param outTradeNo
*/
public void paySuccess(String outTradeNo) {
// 当前登录用户id
Long 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";
@Autowired
private 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 {
// 当前登录用户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("该订单已支付");
// }
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。