【网课平台】Day13.订单支付模式:生成支付二维码与查询支付

news2024/11/29 8:38:17

文章目录

  • 一、需求:生成支付二维码
    • 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");

到此,被动等着接收第三方支付系统通知支付结果实现。主动和被动都实现了,支付结果一定可以拿到,本地订单表也会被更新(不会重复更新,上面代码中已经写了:如果本地表状态为已支付,则直接返回)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/474060.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

如何写出一份大厂都不会拒绝的简历?

你好&#xff0c;我是宋光璠&#xff0c;今天我以过来人的身份教你写出一份惊艳面试官的简历。 简历算是我们过去经历的一个缩影&#xff0c;虽然只有短短一两页&#xff0c;但也能让人从中发现你的优点&#xff0c;一份优质的简历更是如此&#xff0c;所以今天我就带你从头到…

PLC模糊PID(梯形图实现)

博途PLC的模糊PID控制详细内容请查看下面的博客文章: Matlab仿真+博途PLC模糊PID控制完整SCL源代码参考(带模糊和普通PID切换功能)_博途怎么实现模糊pid_RXXW_Dor的博客-CSDN博客模糊PID的其它相关数学基础,理论知识大家可以参看专栏的其它文章,这里不再赘述,本文就双容…

网络安全常用术语

肉鸡 肉鸡指的就是被黑客成功入侵并取得控制权限的电脑。黑客们可以随意的控制肉鸡&#xff0c;就像在使用自己的电脑一样&#xff0c;很形象的比喻&#xff0c;就像是养的肉鸡&#xff0c;任黑客宰杀和利用。关键的是&#xff0c;在成为肉鸡后&#xff0c;只要黑客不对电脑进…

【VM服务管家】VM4.x算子SDK开发_3.4 控件嵌入类

目录 3.4.1 图片存储&#xff1a;图片保存的方法3.4.2 辅助十字线&#xff1a;给图像添加辅助十字线的方法3.4.3 控件调用&#xff1a;在WPF中使用Winform控件的方法3.4.4 图形改变事件&#xff1a;渲染控件上图形改变事件的实现方法3.4.5 鼠标事件&#xff1a;渲染控件上鼠标事…

Hive的基本操作和查询语法以及案例(大数据查询)

1、 13-Hive的基本操作和查询语法以及案例_hive分区表查询语句_大数据下的画像人的博客-CSDN博客 2、SQL 中多个 and or 的组合运算 SQL 中多个 and or 的组合运算_weixin_30611509的博客-CSDN博客sql关系型运算符优先级高到低为&#xff1a;not >and> orAND、OR运算符…

零基础想成为黑客,只需要四步

前言 首先要明白&#xff0c;该篇文章说的黑客不是那种窃取别人信息、攻击别人系统的黑客&#xff0c;说的是调试和分析计算机安全系统的网络安全工程师。 黑客技术的核心之一就是渗透攻防技术&#xff0c;是为了证明网络防御按照预期计划正常运行而提供的一种机制。就是通过模…

语音处理加窗分帧

语音处理加窗分帧 一、分帧 语音数据和视频数据不同&#xff0c;本没有帧的概念&#xff0c;但是为了传输与存储&#xff0c;我们采集的音频数据都是一段一段 的。为了程序能够进行批量处理&#xff0c;会根据指定的长度(时间段或者采样数)进行分段&#xff0c;结构化为我们编程…

从FPGA说起的深度学习(八)-数据并行性

这是新的系列教程&#xff0c;在本教程中&#xff0c;我们将介绍使用 FPGA 实现深度学习的技术&#xff0c;深度学习是近年来人工智能领域的热门话题。 在本教程中&#xff0c;旨在加深对深度学习和 FPGA 的理解。 用 C/C 编写深度学习推理代码高级综合 (HLS) 将 C/C 代码转换为…

11款专家级渗透测试工具

导语&#xff1a;在本文中&#xff0c;我们将深入研究渗透测试员用来挫败客户防御系统的工具。 渗透测试员&#xff0c;有时也称“道德黑客”&#xff0c;他们本质上是安全专家&#xff0c;负责对客户的网络或系统发起模拟攻击&#xff0c;以寻找潜在漏洞。他们的目标是展示恶意…

电脑技巧:Windows系统原版纯净软件必备的两个网站

目录 一、MSDN我告诉你 二、helloWindows 三、总结 Windows系统是个人电脑使用最广泛的操作系统&#xff0c;大家可能会遇到下载Windows镜像、SQLServer、Office等官方软件&#xff0c;会遇到各种各样捆绑、广告软件&#xff0c;甚至还有可能电脑被植入病毒的风险。该如何避…

代码随想录算法训练营第四十五天|70. 爬楼梯 (进阶)、322. 零钱兑换、279.完全平方数

文章目录 70. 爬楼梯 &#xff08;进阶&#xff09;322. 零钱兑换279.完全平方数 今天的题一道是求装满背包的可能情况;另两道都是求装满背包的所需的最小物品数目&#xff0c;不用考虑是组合还是排序问题 70. 爬楼梯 &#xff08;进阶&#xff09; 背包问题&#xff0c;求装满…

设置苹果电脑vsode在新窗口中打开文件

0、前言 最近切换到mac电脑工作&#xff0c;又得重新安装一些工具软件并设置。虽然这些设置并表示啥复杂的设置&#xff0c;但是久了不设置还是会忘记。于是记录之&#xff0c;也希望给能帮助到需要的人。 我们使用vscode阅读或者编辑文件时&#xff0c;有时候希望同时打开多…

RabbitMQ 01 概述

什么是消息队列 进行大量的远程调用时&#xff0c;传统的Http方式容易造成阻塞&#xff0c;所以引入了消息队列的概念&#xff0c;即让消息排队&#xff0c;按照队列进行消费。 它能够将发送方发送的信息放入队列中&#xff0c;当新的消息入队时&#xff0c;会通知接收方进行处…

MySQL的JSON 数据类型

概述&#xff1a; MySQL提供了一个专门用于存储JSON数据的数据类型&#xff1a;JSON。JSON数据类型允许您在MySQL数据库中存储和操作JSON格式的数据。 以下是关于JSON数据类型的一些基本操作和函数&#xff1a; 创建表&#xff1a; 要在表中创建一个JSON类型的列&#xff0…

C/C++每日一练(20230429)

目录 1. 螺旋矩阵 &#x1f31f;&#x1f31f; 2. 戳气球 &#x1f31f;&#x1f31f;&#x1f31f; 3. 实现五则运算 &#x1f31f; &#x1f31f; 每日一练刷题专栏 &#x1f31f; Golang每日一练 专栏 Python每日一练 专栏 C/C每日一练 专栏 Java每日一练 专栏 1…

从0搭建Vue3组件库(十):如何搭建一个 Cli 脚手架

本篇文章将实现一个名为create-easyest脚手架的开发,只需一个命令npm init easyest就可以将整个组件库开发框架拉到本地。 创建 Cli 包 首先,我们在 packages 目录下新建 cli 目录,同执行pnpm init进行初始化,然后将包名改为create-easyest 这里需要知道的是当我们执行npm in…

layui框架实战案例(21):layui上传的哪些事(layui.upload组件、 file文件域、php后台上传)

上传的哪些事 一、核心方法与基础参数选项二、使用upload组件1.调用layui.upload2.文件上传进度条3.弹出进度条4.完整核心代码5.效果预览6.后台上传代码7.附带参数data 一、核心方法与基础参数选项 upload.render({elem: #uploadlicense//指向容器选择器, url: ?mIndex&ai…

Python每日一练(20230429)

目录 1. 地下城游戏 &#x1f31f;&#x1f31f;&#x1f31f; 2. 杨辉三角 II &#x1f31f; 3. 旋转数组 &#x1f31f;&#x1f31f; &#x1f31f; 每日一练刷题专栏 &#x1f31f; Golang每日一练 专栏 Python每日一练 专栏 C/C每日一练 专栏 Java每日一练 专栏…

HCIA-RS实验-路由配置-静态路由缺省路由(2)

接上文HCIA-RS实验-路由配置-静态路由&缺省路由 继续完成缺省路由&#xff1b;其他原截图就不再一一截图&#xff0c;有需要往回看一篇。 关闭上一篇的接口shutdown&#xff08;重新启动&#xff09; 上一篇在R2关闭的接口2 需要重新启动&#xff0c;输入 undo shutdown…

React--》Redux Toolkit的使用讲解

目录 Redux Toolkit redux toolkit的基本使用 RTK代码模块化 RTK QUERY的使用 useQuery参数 Redux Toolkit Redux Toolkit是Redux的工具包&#xff0c;简称RTK&#xff0c;可以帮助我们处理使用Redux过程中的重复性工作&#xff0c;简化Redux中的各种操作&#xff0c;当然…