【Java实战篇】Day13.在线教育网课平台--生成支付二维码与完成支付

news2024/11/26 16:57:08

文章目录

  • 一、需求:生成支付二维码
    • 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/460706.html

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

相关文章

控制工程有哪些SCI期刊推荐? - 易智编译EaseEditing

控制工程是一门涵盖广泛的学科&#xff0c;其研究内容涉及控制理论、控制工程应用、自动化技术等多个方面&#xff0c;因此相关的SCI期刊也比较多。以下是一些推荐的控制工程SCI期刊&#xff1a; IEEE Transactions on Automatic Control&#xff1a; 该期刊是自动控制领域顶…

【springcloud微微服务】分布式事务框架Seata使用详解

目录 一、前言 二、事务简介 2.1 原子性 2.2 一致性 2.3 隔离性 2.4 持久性 三、分布式事务场景 3.1 分布式事务起源 3.2 分布式事务典型场景 3.2.1 跨库事务 3.2.2 分库分表 3.2.3 服务化 四、分布式事务常用解决方案 4.1 分布式事务理论基础 4.1.1 2PC两阶段提…

降低风险和最大化成功:如何解决项目管理中的成本差异

作为项目经理&#xff0c;你知道让项目按计划进行并按预算进行对于项目管理的成功至关重要。你可以使用的关键工具之一是成本差异分析。但成本差异到底是什么&#xff0c;如何利用它来发挥优势呢&#xff1f; 定义成本差异 成本差异是项目实际成本与预算或计划成本之间的差异…

Linux shell命令行基础

shell简介 shell 与内核沟通的界面、应用程序等。用于将用户操作传递给内核执行。 shell是面向过程 的若类型的解释性语言&#xff0c;不需要编译即可直接执行&#xff0c;常用于作脚本 Linux中的shell 在/etc/shells文件中 存储Linux包含的shell。 最常用的是bash&#xff0c;…

Docker虚拟化技术

1.3 Docker虚拟化技术概念 Docker是一款轻量级、高性能的虚拟化技术&#xff0c;是目前互联网使用最多的虚拟化技术&#xff0c;Docker虚拟化技术的本质类似集装箱机制&#xff0c;最早集装箱没有出现的时候&#xff0c;码头上有许多搬运的工人在搬运货物&#xff0c;集装箱出…

Karl Guttag:现有Micro LED/LCoS+光波导AR眼镜对比解析

轻量化是未来AR眼镜的发展趋势&#xff0c;为了缩减尺寸&#xff0c;AR眼镜厂商尝试了多种方案&#xff0c;长期来看Micro LED光机在小型化上更有优势&#xff0c;但现阶段LCoS光机的图像表现更好。在CES 2023期间&#xff0c;DigiLens、Lumus、Vuzix、OPPO、Avegant也展出了不…

进程通信(同一主机)

1.概述 进程通信机制包括&#xff1a; 传统的UNIX进程间通信&#xff1a;无名管道、有名管道、信号 System V 进程间通信&#xff1a;消息队列、信号量、共享内存 2.管道通信 2.1无名管道 1.特点 &#xff08;1&#xff09;适用具有亲缘关系的进程 &#xff08;2&#x…

C++高精度减法

高精度减法指的是大整数的相减&#xff0c;大整数是用基本数据类型无法存储其精度的整数&#xff0c;位数不超过10^6&#xff0c;注意是位数&#xff0c;不是数值的大小。 因为其精度超过基本的数据类型规定的大小&#xff0c;所以常规的计算方法是不可以实现的&#xff0c;这…

UE4 架构初识(二)

目录 UE4 引擎学习 一、架构基础 1. Pawn &#xff08;1&#xff09;DefaultPawn &#xff08;2&#xff09;SpectatorPawn &#xff08;3&#xff09;Character 2. AController 3. APlayerState 4. 总结 UE4 引擎学习 一、架构基础 1. Pawn UE也是从Actor中再派生…

Zynq-7000、国产zynq-7000的GPIO控制(二)

本文详细说明一下使用SDK中使用MIO/EMIO作为输入中断 SDK中使用MIO/EMIO作为输入中断 这个使用场景可以扩展到PL的可以通过EMIO或者MIO&#xff0c;告知PS中断来了&#xff0c;需要PS处理一些特定事物&#xff0c;当然也可以连接最简单的按键。 这个可以参考SDK自带例程来实…

Python入门教程+项目实战-11.2节: 元组的操作符

目录 11.2.1 元组的常用操作符 11.2.2 []操作符: 索引访问元组 11.2.3 [:]操作符&#xff1a;元组的切片 11.2.4 操作符&#xff1a;元组的加法 11.2.5 *操作符&#xff1a;元组的乘法 11.2.6 元组的关系运算 11.2.7 in操作符&#xff1a;查找元素 11.2.8 知识要点 11…

企业的信息化和数字化有什么区别

数字化是业务新的存在形式&#xff0c;如果说信息化是对业务的局部支撑&#xff0c;那么数字化就是对业务的整体重塑&#xff0c;这是数字化和信息化之间最大的区别&#xff0c;也决定了数字化转型在实施时有着与信息化建设完全不同的底层逻辑。信息化建设和数字化转型有着相同…

手把手教你编写SQLMap的Tamper脚本过狗

本文仅用于技术讨论与学习 测试环境 最新版某狗 测试方法 安全狗其实是比较好绕的WAF&#xff0c;绕过方法很多&#xff0c;但这里我们就用一种&#xff1a;注释混淆 一招鲜吃遍天 注释混淆&#xff0c;其实就是在敏感位置添加垃圾字符注释&#xff0c;常用的垃圾字符有/、…

关于Vue中使用全屏容器无法占满屏幕以及样式不生效问题解决方案

先来看示例问题 App.vue文件 global.css文件 网页效果 可以看到即使设置了宽度和高度为100%都无法占满屏幕&#xff0c;而且容器还超出了屏幕&#xff0c;上拉才可以看到下边框。查看网上解决方法&#xff1a; 1.height设置为100vh&#xff0c; 或者设置为calc&#xff08;10…

订单交期迟滞,销售回应慢,怎么解决客户问题?

按客户定制产品订单&#xff0c;进行报价和生产的制造企业&#xff0c;有拆解图纸生成物料BOM的工序&#xff0c;通常由企业产品设计部门的拆图员岗位专门负责。 手工制作BOM数据&#xff0c;准确性低 拆图员肉眼查看每页图纸中的表格数据&#xff0c;手动敲键盘填入到企业要…

判空、基本数据类型、stream的groupby、空指针异常

0什么是序列化 1 第一行、第二行就是一个空对象【一个对象的所有元素的值都为null–空对象】 第三行不是空对象&#xff0c;是元素为的对象 那么如何过滤第一行与第二行呢 方式一&#xff1a;在mysql里面 where ISNULL(t.relevance_id)0 and LENGTH(trim(t.relevance_id))&…

Docker Harbor | 私有仓库 | 用户登录 |用户创建

Docker Harbor | 私有仓库 |用户登录 |用户创建 一、Docker Harbor 概述二、Harbor 的核心组件四、Harbor 构建 Docker 私有仓库实战4.1 搭建本地私有仓库4.2 部署 Docker-Compose 服务4.3 启动harbor验证4.4 在其他客户端上传镜像4.5 维护管理Harbor 一、Docker Harbor 概述 …

【校招VIP】面试了一个抽奖的项目,我终于搞明白了,是8股文终于开始作恶了

最近因为招实习生&#xff0c;进行了很多次面试。 但面试的结果不尽人意。 就感觉今年的面试跟以前差距太大了。 直到经过这个同学的面试&#xff0c;我终于明白了是什么原因。 这个同学是南京一所211的研究生&#xff0c;他的项目经历是做了一个抽奖的微服务管理平台。 也…

JAVA 类型的类型转换

JAVA 类型的类型转换 一、基本类型的类型转换 箭头开始的地方是小类型,箭头指向的地方是大类型 我们此处所指的"大"和"小",指的是对应类型的取值范围,不是字节数哦 1.1 小到大(隐式转换) byte m 120; int n m;//小转大,右面的m是小类型,给左面的n大…

【微服务笔记21】微服务组件之Sentinel服务熔断、服务降级、流量控制介绍

这篇文章&#xff0c;主要介绍微服务组件之Sentinel服务熔断、服务降级、流量控制。 目录 一、Sentinel组件 1.1、Sentinel介绍 1.2、Sentinel环境搭建 &#xff08;1&#xff09;引入依赖 &#xff08;2&#xff09;资源和规则 1.3、使用SphU定义资源 &#xff08;1&am…