文章目录
- 一、需求:生成支付二维码
- 1、需求分析
- 2、表设计
- 3、接口定义
- 4、接口实现
- 5、完善controller
- 二、需求:查询支付结果
- 1、需求分析
- 2、表设计与模型类
- 3、接口定义
- 4、接口实现
- 步骤一:查询支付结果
- 步骤二:保存支付结果(更新订单相关表)
- 5、完善Controller
- 三、接收支付通知
一、需求:生成支付二维码
1、需求分析
UI设计图:
- 点击支付宝支付
- 生成支付二维码
逻辑设计:
- 点击支付,前端调用学习中心服务的添加选课接口
- 添加选课成功请求订单服务生成支付二维码接口
- 生成二维码接口进行:创建商品订单、生成支付交易记录、生成二维码
- 将二维码返回到前端,用户扫码
2、表设计
订单支付模式的核心由三张表组成:
- 订单表
- 订单明细表
- 支付交易记录表
其中:订单表记录订单信息
注意最后一个字段:out_business_id
这是一个订单表中的记录,订单类型是选课(一对一答疑、电子书..),那这个订单是哪个选课记录对应的
即此时out_business_id等于选课表的id
订单明细表记录订单的详细信息(一个订单可能有多个商品,因此订单表和订单明细表是一对多的关系)
支付交易记录表记录每次支付的交易明细
三张表的关系为:
为什么要有支付交易记录表?
在请求微信或支付宝下单接口时需要传入 商品订单号,在与第三方支付平台对接时发现,当用户支付失败或因为其它原因最终该订单没有支付成功,此时再次调用第三方支付平台的下单接口发现报错“订单号已存在”,此时如果我们传入一个没有使用过的订单号就可以解决问题,但是商品订单已经创建,因为没有支付成功重新创建一个新订单是不合理的。
解决以上问题的方案是:
- 用户每次发起都创建一个新的支付交易记录 ,此交易记录与商品订单关联。
- 将支付交易记录的流水号传给第三方支付系统下单接口,这样就即使没有支付成功就不会出现上边的问题。
- 需要提醒用户不要重复支付。
注意订单号要唯一、安全,生成思路有:
- 时间戳+随机数:年月日时分秒毫秒+随机数
- 高并发下使用:年月日时分秒毫秒+随机数+redis自增序列
- 订单号中加上业务标识:如拼接出来的订单号的第十位代表业务类型,最后一位代表用户类型。这样也方便客服识别。
- 雪花算法:推特内部使用的分布式环境下的唯一ID生成算法,它基于时间戳生成,保证有序递增。可以满足高并发环境下ID不重复
生成订单号的雪花算法工具类:
package com.xuecheng.base.utils;
import java.util.Random;
/**
* snow flow .
*
*/
public final class IdWorkerUtils {
private static final Random RANDOM = new Random();
private static final long WORKER_ID_BITS = 5L;
private static final long DATACENTERIDBITS = 5L;
private static final long MAX_WORKER_ID = ~(-1L << WORKER_ID_BITS);
private static final long MAX_DATACENTER_ID = ~(-1L << DATACENTERIDBITS);
private static final long SEQUENCE_BITS = 12L;
private static final long WORKER_ID_SHIFT = SEQUENCE_BITS;
private static final long DATACENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;
private static final long TIMESTAMP_LEFT_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATACENTERIDBITS;
private static final long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS);
private static final IdWorkerUtils ID_WORKER_UTILS = new IdWorkerUtils();
private long workerId;
private long datacenterId;
private long idepoch;
private long sequence = '0';
private long lastTimestamp = -1L;
private IdWorkerUtils() {
this(RANDOM.nextInt((int) MAX_WORKER_ID), RANDOM.nextInt((int) MAX_DATACENTER_ID), 1288834974657L);
}
private IdWorkerUtils(final long workerId, final long datacenterId, final long idepoch) {
if (workerId > MAX_WORKER_ID || workerId < 0) {
throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", MAX_WORKER_ID));
}
if (datacenterId > MAX_DATACENTER_ID || datacenterId < 0) {
throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", MAX_DATACENTER_ID));
}
this.workerId = workerId;
this.datacenterId = datacenterId;
this.idepoch = idepoch;
}
/**
* Gets instance.
*
* @return the instance
*/
public static IdWorkerUtils getInstance() {
return ID_WORKER_UTILS;
}
public synchronized long nextId() {
long timestamp = timeGen();
if (timestamp < lastTimestamp) {
throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
}
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & SEQUENCE_MASK;
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = timestamp;
return ((timestamp - idepoch) << TIMESTAMP_LEFT_SHIFT)
| (datacenterId << DATACENTER_ID_SHIFT)
| (workerId << WORKER_ID_SHIFT) | sequence;
}
private long tilNextMillis(final long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
private long timeGen() {
return System.currentTimeMillis();
}
/**
* Build part number string.
*
* @return the string
*/
public String buildPartNumber() {
return String.valueOf(ID_WORKER_UTILS.nextId());
}
/**
* Create uuid string.
*
* @return the string
*/
public String createUUID() {
return String.valueOf(ID_WORKER_UTILS.nextId());
}
public static void main(String[] args) {
System.out.println(IdWorkerUtils.getInstance().nextId());
}
}
3、接口定义
在订单服务中定义生成支付二维码的接口。根据请求参数写dto类:
package com.xuecheng.orders.model.dto;
import com.xuecheng.orders.model.po.XcOrders;
import lombok.Data;
import lombok.ToString;
@Data
@ToString
public class AddOrderDto {
/**
* 总价
*/
private Float totalPrice;
/**
* 订单类型
*/
private String orderType;
/**
* 订单名称
*/
private String orderName;
/**
* 订单描述
*/
private String orderDescrip;
/**
* 订单明细json,不可为空
* [{"goodsId":"","goodsType":"","goodsName":"","goodsPrice":"","goodsDetail":""},{...}]
*/
private String orderDetail;
/**
* 外部系统业务id
*/
private String outBusinessId;
}
需要返回二维码与支付相关的信息,定义Vo:
//继承支付记录Po类
@Data
@ToString
public class PayRecordVo extends XcPayRecord {
//二维码
private String qrcode;
}
接口定义:
@Api(value = "订单支付接口", tags = "订单支付接口")
@Slf4j
@Controller
public class OrderController {
@ApiOperation("生成支付二维码")
@PostMapping("/generatepaycode")
@ResponseBody
public PayRecordVo generatePayCode(@RequestBody AddOrderDto addOrderDto) {
//订单信息插入
//插入支付记录
//生成二维码
return null;
}
}
用户扫码下单,和二维码关联的下单接口如下:
@ApiOperation("扫码下单接口")
@GetMapping("/requestpay")
public void requestpay(String payNo,HttpServletResponse httpResponse) throws IOException {
}
4、接口实现
定义Service接口:
public interface OrderService {
/**
* @param addOrderDto 订单信息
* @return PayRecordVo 支付交易记录(包括二维码)
*/
public PayRecordVo createOrder(String userId,AddOrderDto addOrderDto);
写实现类:(先注释写每一步的逻辑
)
@Slf4j
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
XcOrdersMapper ordersMapper;
@Autowired
XcOrdersGoodsMapper ordersGoodsMapper;
@Autowired
XcPayRecordMapper payRecordMapper;
@Transactional
@Override
public PayRecordDto createOrder(String userId, AddOrderDto addOrderDto) {
//一个选课记录只能有一个订单,插入前要进行幂等性判断
//添加商品订单(订单表+订单明细表,同成功同失败,要事务控制)
//添加支付交易记录
//生成二维码
return null;
}
}
注释中的逻辑,分别定义不同的方法实现。编写创建商品订单方法,商品订单的数据来源于选课记录,在订单表需要存入选课记录的ID,这里需要作好幂等处理。
@Transactional
public XcOrders saveXcOrders(String userId,AddOrderDto addOrderDto){
//幂等性处理
XcOrders order = getOrderByBusinessId(addOrderDto.getOutBusinessId());
if(order!=null){
return order;
}
//接下来插入订单表,先new一个对象
order = new XcOrders();
//生成订单号
long orderId = IdWorkerUtils.getInstance().nextId(); //雪花算法工具类
order.setId(orderId);
order.setTotalPrice(addOrderDto.getTotalPrice());
order.setCreateDate(LocalDateTime.now());
order.setStatus("600001");//未支付
order.setUserId(userId);
order.setOrderType(addOrderDto.getOrderType());
order.setOrderName(addOrderDto.getOrderName());
order.setOrderDetail(addOrderDto.getOrderDetail());
order.setOrderDescrip(addOrderDto.getOrderDescrip());
order.setOutBusinessId(addOrderDto.getOutBusinessId());//选课记录id
int insert = ordersMapper.insert(order);
if(insert <= 0){
MyException.cast("添加订单失败"); //添加失败时抛出异常,好让事务回滚
}
//前端传入的订单明细json串[{"goodsId":"","goodsType":"","goodsName":"","goodsPrice":"","goodsDetail":""},{...}]
String orderDetailJson = addOrderDto.getOrderDetail();
//json转List<订单明细表PO>
List<XcOrdersGoods> xcOrdersGoodsList = JSON.parseArray(orderDetailJson, XcOrdersGoods.class);
xcOrdersGoodsList.forEach(goods->{
XcOrdersGoods xcOrdersGoods = new XcOrdersGoods();
BeanUtils.copyProperties(goods,xcOrdersGoods);
xcOrdersGoods.setOrderId(orderId);//订单号
ordersGoodsMapper.insert(xcOrdersGoods);
});
return order;
}
//根据业务id查询订单
//这里的业务id即选课记录的id,选课记录表的主键
public XcOrders getOrderByBusinessId(String businessId) {
XcOrders orders = ordersMapper.selectOne(new LambdaQueryWrapper<XcOrders>().eq(XcOrders::getOutBusinessId, businessId));
return orders;
}
接下来写注释逻辑中的第二个方法(保存支付记录):
public XcPayRecord createPayRecord(XcOrders orders){
if(order==null){
MyException.cast("订单不存在");
}
if(orders.getStatus().equals("600002")){
MyException.cast("订单已支付"); //避免重复支付
}
XcPayRecord payRecord = new XcPayRecord();
//生成支付交易流水号,还是用雪花
long payNo = IdWorkerUtils.getInstance().nextId();
payRecord.setPayNo(payNo);
payRecord.setOrderId(orders.getId());//商品订单号
payRecord.setOrderName(orders.getOrderName());
payRecord.setTotalPrice(orders.getTotalPrice());
payRecord.setCurrency("CNY");
payRecord.setCreateDate(LocalDateTime.now());
payRecord.setStatus("601001");//未支付
payRecord.setUserId(orders.getUserId());
int insert = payRecordMapper.insert(payRecord);
if(insert <= 0){
MyException.cast("插入支付记录失败");
}
return payRecord;
}
最后完善注释代码中的最后一步:生成支付二维码
# 扫描二维码要请求支付接口
# 配置二维码的url直接写代码里,硬编码不合适,直接写nacos,%s占位符
pay:
qrcodeurl: http://192.168.101.1/api/orders/requestpay?payNo=%s
写方法的具体内容,完善整个方法:
@Value("${pay.qrcodeurl}")
String qrcodeurl;
@Transactional
@Override
public PayRecordVo createOrder(String userId, AddOrderDto addOrderDto) {
//创建商品订单
XcOrders orders = saveXcOrders(userId, addOrderDto);
if(orders==null){
XueChengPlusException.cast("订单创建失败");
}
if(orders.getStatus().equals("600002")){
XueChengPlusException.cast("订单已支付");
}
//生成支付记录
XcPayRecord payRecord = createPayRecord(orders);
//生成二维码
String qrCode = null;
try {
//传入支付记录id,拼接出url(即支付接口传参)
String url = String.format(qrcodeurl, payRecord.getPayNo());
qrCode = new QRCodeUtil().createQRCode(url, 200, 200);
} catch (IOException e) {
MyException.cast("生成二维码出错");
}
PayRecordVo payRecordVo = new PayRecordVo();
BeanUtils.copyProperties(payRecord,payRecordVo);
payRecordDto.setQrcode(qrCode);
return payRecordVo;
}
5、完善controller
@Autowired
OrderService orderService;
@ApiOperation("生成支付二维码")
@PostMapping("/generatepaycode")
@ResponseBody
public PayRecordDto generatePayCode(@RequestBody AddOrderDto addOrderDto) {
//工具类获取当前登录用户
SecurityUtil.XcUser user = SecurityUtil.getUser();
if(user == null){
XueChengPlusException.cast("请登录后继续选课");
}
return orderService.createOrder(user.getId(), addOrderDto);
}
到此,调试一下,二维码可以成功生成。接下来完成扫描二维码时请求的接口,进行向支付宝下单,支付宝响应js唤起支付宝进行支付。
@ApiOperation("扫码下单接口")
@GetMapping("/requestpay")
public void requestpay(String payNo,HttpServletResponse httpResponse) throws IOException {
}
先定义查询支付交易记录的Service接口与实现方法:
/**
* @description 查询支付交易记录
* @param payNo 交易记录号
*/
public XcPayRecord getPayRecordByPayno(String payNo);
//根据支付记录号查询支付记录
public XcPayRecord getPayRecordByPayno(String payNo) {
XcPayRecord xcPayRecord = payRecordMapper.selectOne(new LambdaQueryWrapper<XcPayRecord>().eq(XcPayRecord::getPayNo, payNo));
return xcPayRecord;
}
完善controller中的方法:
@Value("${pay.alipay.APP_ID}")
String APP_ID;
@Value("${pay.alipay.APP_PRIVATE_KEY}")
String APP_PRIVATE_KEY;
@Value("${pay.alipay.ALIPAY_PUBLIC_KEY}")
String ALIPAY_PUBLIC_KEY;
@ApiOperation("扫码下单接口")
@GetMapping("/requestpay")
public void requestpay(String payNo,HttpServletResponse httpResponse) throws IOException {
//如果payNo不存在则提示重新发起支付
XcPayRecord payRecord = orderService.getPayRecordByPayno(payNo);
if(payRecord == null){
MyException.cast("支付记录不存在,请重新点击支付获取二维码");
}
//支付状态
String status = payRecord.getStatus();
if("601002".equals(status)){
MyException.cast("订单已支付,请勿重复支付。");
}
//构造sdk的客户端对象
AlipayClient client = new DefaultAlipayClient(AlipayConfig.URL, APP_ID, APP_PRIVATE_KEY, AlipayConfig.FORMAT, AlipayConfig.CHARSET, ALIPAY_PUBLIC_KEY, AlipayConfig.SIGNTYPE);//获得初始化的AlipayClient
AlipayTradeWapPayRequest alipayRequest = new AlipayTradeWapPayRequest();//创建API对应的request
//alipayRequest.setReturnUrl("http://domain.com/CallBack/return_url.jsp");
//alipayRequest.setNotifyUrl("http://tjxt-user-t.itheima.net/xuecheng/orders/paynotify");//在公共参数中设置回跳和通知地址
//通知地址先注释掉
alipayRequest.setBizContent("{" +
" \"out_trade_no\":\""+payRecord.getPayNo()+"\"," +
" \"total_amount\":\""+payRecord.getTotalPrice()+"\"," +
" \"subject\":\""+payRecord.getOrderName()+"\"," +
" \"product_code\":\"QUICK_WAP_PAY\"" +
" }");//填充业务参数
String form = "";
try {
//请求支付宝下单接口,发起http请求
form = client.pageExecute(alipayRequest).getBody(); //调用SDK生成表单
} catch (AlipayApiException e) {
e.printStackTrace();
}
httpResponse.setContentType("text/html;charset=" + AlipayConfig.CHARSET);
httpResponse.getWriter().write(form);//直接将完整的表单html输出到页面,唤起手机支付宝客户端进行支付
httpResponse.getWriter().flush();
httpResponse.getWriter().close();
}
到此,可以生成支付二维码、扫描二维码后唤起支付宝客户端进行支付。
二、需求:查询支付结果
1、需求分析
UI设计图:
- 用户支付完成后,可点击支付完成,不用等系统自动跳回上一页面
逻辑设计:
获取支付宝的支付结果,可以选择:
- 主动查询支付结果
- 被动接收支付结果
这里实现主动查询支付结果,当用户点击“支付完成”,通过接口请求第三方支付平台,查询支付结果。查询结果为已付款时,要更新订单表和支付记录表的支付状态字段。
2、表设计与模型类
无新增表
3、接口定义
@ApiOperation("查询支付结果")
@GetMapping("/payresult")
@ResponseBody
public PayRecordVo payresult(String payNo) throws IOException {
//查询支付结果
return null;
}
4、接口实现
Service层接口:
PayRecord queryPayResult(String payNo);
Service接口的实现,在这里做两件事:
- 调用支付宝的接口查询支付结果
- 拿到支付结果为成功时,更新支付记录表和订单记录表
@Override
public PayRecordVo queryPayResult(String payNo){
//查询支付结果
//保存支付结果到支付记录表和订单记录表
}
步骤一:查询支付结果
对这两个步骤写方法:
//定义从支付宝查询结果中封装出一个Vo返回(别光拿个支付状态,后面更新表还得要交易号等字段)
@Data
public class PayStatusVo{
String out_trade_no;
String trade_no;
String trade_status;
String app_id;
String total_amount;
}
/**
* 请求支付宝查询支付结果
* @param payNo 支付交易号
* @return 支付结果
*/
public PayStatusVo queryPayResultFromAlipay(String payNo) {
//========请求支付宝查询支付结果=============
AlipayClient alipayClient = new DefaultAlipayClient(AlipayConfig.URL, APP_ID, APP_PRIVATE_KEY, "json", AlipayConfig.CHARSET, ALIPAY_PUBLIC_KEY, AlipayConfig.SIGNTYPE); //获得初始化的AlipayClient
AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
JSONObject bizContent = new JSONObject();
bizContent.put("out_trade_no", payNo); //传入支付记录号
request.setBizContent(bizContent.toString());
AlipayTradeQueryResponse response = null;
try {
response = alipayClient.execute(request);
if (!response.isSuccess()) {
MyException.cast("请求支付查询查询失败");
}
} catch (AlipayApiException e) {
log.error("请求支付宝查询支付结果异常:{}", e.toString(), e);
MyException.cast("请求支付查询查询失败");
}
//获取支付结果
String resultJson = response.getBody();
//转map
Map resultMap = JSON.parseObject(resultJson, Map.class);
Map alipay_trade_query_response = (Map) resultMap.get("alipay_trade_query_response");
//支付结果
String trade_status = (String) alipay_trade_query_response.get("trade_status");
String total_amount = (String) alipay_trade_query_response.get("total_amount");
String trade_no = (String) alipay_trade_query_response.get("trade_no");
//保存支付结果
PayStatusVo payStatusVo = new PayStatusVo();
payStatusVo.setOut_trade_no(payNo);
payStatusVo.setTrade_status(trade_status);
payStatusVo.setApp_id(APP_ID);
payStatusVo.setTrade_no(trade_no);
payStatusVo.setTotal_amount(total_amount);
return payStatusVo;
}
//到这儿先测测这个方法,下一步的保存得用这步的数据
get、set时截图出来照着写就行
这里和前端联调时发现,Long类型的支付记录id,前端拿到后有精度损失。这里需要解决
《Long转String后的精度损失问题》
//Long转String后的精度损失问题
//可直接在属性上加序列化注解,但Long属性太多时,挨个加很低效,直接配置这个转换类
//这样就不用在每个属性上去加
package com.xuecheng.base.config;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@Configuration
public class LocalDateTimeConfig {
/*
* 序列化内容
* LocalDateTime -> String
* 服务端返回给客户端内容
* */
@Bean
public LocalDateTimeSerializer localDateTimeSerializer() {
return new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}
/*
* 反序列化内容
* String -> LocalDateTime
* 客户端传入服务端数据
* */
@Bean
public LocalDateTimeDeserializer localDateTimeDeserializer() {
return new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}
//long转string避免精度损失
@Bean
public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
ObjectMapper objectMapper = builder.createXmlMapper(false).build();
//忽略value为null 时 key的输出
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
SimpleModule module = new SimpleModule();
module.addSerializer(Long.class, ToStringSerializer.instance);
module.addSerializer(Long.TYPE, ToStringSerializer.instance);
objectMapper.registerModule(module);
return objectMapper;
}
// 配置
@Bean
public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
return builder -> {
builder.serializerByType(LocalDateTime.class, localDateTimeSerializer());
builder.deserializerByType(LocalDateTime.class, localDateTimeDeserializer());
};
}
}
步骤二:保存支付结果(更新订单相关表)
拿到支付结果对象后,支付成功时要更新订单表和支付记录表。(很明显,这两个动作要事务控制)注意这时调用它的方法不是事务方法。要注入自身使用代理对象,否则事务失效。
/**
* @description 保存支付宝支付结果
* @param payStatusDto 支付结果信息
* @return void
*/
public void saveAliPayStatus(PayStatusVo payStatusVo) ;
接口方法实现:
@Transactional
@Override
public void saveAliPayStatus(PayStatusVo payStatusVo) {
//支付流水号
String payNo = payStatusVo.getOut_trade_no();
XcPayRecord payRecord = getPayRecordByPayno(payNo);
if (payRecord == null) {
MyException.cast("支付记录找不到");
}
//支付结果
String trade_status = payStatusVo.getTrade_status();
log.debug("收到支付结果:{},支付记录:{}}", payStatusVo.toString(),payRecord.toString());
if (trade_status.equals("TRADE_SUCCESS")) {
//支付金额变为分
Float totalPrice = payRecord.getTotalPrice() * 100;
Float total_amount = Float.parseFloat(payStatusVo.getTotal_amount()) * 100;
//校验是否一致
if (!payStatusVo.getApp_id().equals(APP_ID) || totalPrice.intValue() != total_amount.intValue()) {
//校验失败
log.info("校验支付结果失败,支付记录:{},APP_ID:{},totalPrice:{}" ,payRecord.toString(),payStatusDto.getApp_id(),total_amount.intValue());
MyException.cast("校验支付结果失败");
}
log.debug("更新支付结果,支付交易流水号:{},支付结果:{}", payNo, trade_status);
XcPayRecord payRecord_u = new XcPayRecord();
payRecord_u.setStatus("601002");//支付成功
payRecord_u.setOutPayChannel("Alipay");
payRecord_u.setOutPayNo(payStatusVo.getTrade_no());//支付宝交易号
payRecord_u.setPaySuccessTime(LocalDateTime.now());//通知时间
int update1 = payRecordMapper.update(payRecord_u, new LambdaQueryWrapper<XcPayRecord>().eq(XcPayRecord::getPayNo, payNo));
if (update1 > 0) {
log.info("更新支付记录状态成功:{}", payRecord_u.toString());
} else {
log.info("更新支付记录状态失败:{}", payRecord_u.toString());
XueChengPlusException.cast("更新支付记录状态失败");
}
//关联的订单号
Long orderId = payRecord.getOrderId();
XcOrders orders = ordersMapper.selectById(orderId);
if (orders == null) {
log.info("根据支付记录[{}}]找不到订单", payRecord_u.toString());
MyException.cast("根据支付记录找不到订单");
}
XcOrders order_u = new XcOrders();
order_u.setStatus("600002");//支付成功
int update = ordersMapper.update(order_u, new LambdaQueryWrapper<XcOrders>().eq(XcOrders::getId, orderId));
if (update > 0) {
log.info("更新订单表状态成功,订单号:{}", orderId);
} else {
log.info("更新订单表状态失败,订单号:{}", orderId);
MyException.cast("更新订单表状态失败");
}
}
}
两个步骤的方法都写完了,完善Service中总的方法:
@Override
public PayRecordVo queryPayResult(String payNo){
XcPayRecord payRecord = getPayRecordByPayno(payNo);
if (payRecord == null) {
MyException.cast("没有支付记录,请重新点击支付获取二维码");
}
//支付状态
String status = payRecord.getStatus();
//如果查到的支付巨鹿中,已经是支付成功,则直接返回
if ("601002".equals(status)) {
PayRecordVo payRecordVo = new PayRecordVo();
BeanUtils.copyProperties(payRecord, payRecordVo);
return payRecordVo;
}
//从支付宝查询支付结果
PayStatusVo payStatusVo = queryPayResultFromAlipay(payNo);
//保存支付结果
//代理对象,避免事务控制失效
currentProxy.saveAliPayStatus( payStatusVo);
//更新保存完支付结果后,重新查询支付记录
payRecord = getPayRecordByPayno(payNo);
PayRecordVo payRecordVo = new PayRecordVo();
BeanUtils.copyProperties(payRecord, payRecordVo);
return payRecordVo;
}
5、完善Controller
@ApiOperation("查询支付结果")
@GetMapping("/payresult")
@ResponseBody
public PayRecordVo payresult(String payNo) throws IOException {
//调用支付宝接口查询
PayRecordVo payRecordVo = orderService.queryPayResult(payNo);
return payRecordVo;
}
到此,点击支付完成,可主动查询支付结果
三、接收支付通知
支付成功后,第三方支付系统会主动通知支付结果,要想收到通知,得在请求支付系统下单时传入两个URL:
- ReturnUrl:支付完成后支付系统携带支付结果重定向到ReturnUrl地址
- NotifyUrl:支付完成后支付系统在后台异步定时去通知,比使用ReturnUrl更有保证
接口定义与实现:
@ApiOperation("接收支付结果通知")
@PostMapping("/receivenotify")
public void receivenotify(HttpServletRequest request) throws UnsupportedEncodingException, AlipayApiException {
Map<String,String> params = new HashMap<String,String>();
Map requestParams = request.getParameterMap();
for (Iterator iter = requestParams.keySet().iterator(); iter.hasNext();) {
String name = (String) iter.next();
String[] values = (String[]) requestParams.get(name);
String valueStr = "";
for (int i = 0; i < values.length; i++) {
valueStr = (i == values.length - 1) ? valueStr + values[i]
: valueStr + values[i] + ",";
}
params.put(name, valueStr);
}
//验签
boolean verify_result = AlipaySignature.rsaCheckV1(params, ALIPAY_PUBLIC_KEY, AlipayConfig.CHARSET, "RSA2");
if(verify_result) {//验证成功
//商户订单号
String out_trade_no = new String(request.getParameter("out_trade_no").getBytes("ISO-8859-1"),"UTF-8");
//支付宝交易号
String trade_no = new String(request.getParameter("trade_no").getBytes("ISO-8859-1"),"UTF-8");
//交易状态
String trade_status = new String(request.getParameter("trade_status").getBytes("ISO-8859-1"),"UTF-8");
//appid
String app_id = new String(request.getParameter("app_id").getBytes("ISO-8859-1"),"UTF-8");
//total_amount
String total_amount = new String(request.getParameter("total_amount").getBytes("ISO-8859-1"),"UTF-8");
//交易成功处理
if (trade_status.equals("TRADE_SUCCESS")) {
PayStatusVo payStatusVo = new PayStatusVo();
payStatusVo.setOut_trade_no(out_trade_no);
payStatusVo.setTrade_status(trade_status);
payStatusVo.setApp_id(app_id);
payStatusVo.setTrade_no(trade_no);
payStatusVo.setTotal_amount(total_amount);
//保存支付状态到各个表(主动查询结果中定义的方法,已经暴露成接口了,可直接调用)
orderService.saveAliPayStatus(payStatusVo);
}
}
}
更改请求第三方支付系统下单时的传参,指定异步通知的地址(上面定义的接口)
//指定notify通知url
alipayRequest.setNotifyUrl("http://tjxt-user-t.itheima.net/xuecheng/orders/receivenotify");
到此,被动等着接收第三方支付系统通知支付结果实现。主动和被动都实现了,支付结果一定可以拿到,本地订单表也会被更新(不会重复更新,上面代码中已经写了:如果本地表状态为已支付,则直接返回)