基于 Redis 手写一个“秒杀”

news2024/9/21 14:45:36

博主介绍: ✌博主从事应用安全和大数据领域,有8年研发经验,5年面试官经验,Java技术专家✌

Java知识图谱点击链接:体系化学习Java(Java面试专题)

💕💕 感兴趣的同学可以收藏关注下不然下次找不到哟💕💕

在这里插入图片描述

文章目录

  • 1、什么是秒杀
  • 2、秒杀需要注意哪些问题
  • 3、秒杀有几种实现方式
  • 4、秒杀实现的逻辑步骤
  • 5、基于 Redis 实现一个简单的秒杀
  • 6、“秒杀”生产代码部分展示

1、什么是秒杀

秒杀是一种促销方式,通常指在限定的时间内,以极低的价格或折扣销售商品。秒杀通常会吸引大量的消费者,因为消费者可以在短时间内以非常低的价格购买到自己需要的商品,而商家也可以通过秒杀活动促进销售,提高品牌知名度。秒杀活动通常需要考虑库存、价格、时间等多个因素,因此需要进行精细的规划和执行。

2、秒杀需要注意哪些问题

秒杀活动需要注意以下几个问题:

  1. 库存控制:秒杀活动需要预估销售量,并控制库存,避免出现卖断货或者库存积压的情况。
  2. 网站流量控制:秒杀活动容易引起大量用户访问,需要考虑网站的并发访问量,以避免网站崩溃或者访问延迟等问题。
  3. 价格设置:秒杀活动的价格需要具有吸引力,但是也需要考虑到成本和利润,以避免亏本或者影响品牌形象。
  4. 订单处理:秒杀活动的订单处理需要快速、准确,以满足用户的需求。
  5. 客服支持:秒杀活动期间需要加强客服支持,及时解答用户的问题和投诉,保障用户的购物体验。
  6. 安全防范:秒杀活动容易引起黑客攻击、恶意刷单等问题,需要加强安全防范措施,保障用户的账户和交易安全。

秒杀活动还需要注意超卖问题。由于秒杀活动的商品价格通常比平时低很多,会吸引大量用户参与,但是库存有限,容易出现超卖的情况。为了避免超卖问题,可以采取以下措施:

  1. 合理设置商品数量:在活动策划阶段,需要根据预估的销售量和库存情况,合理设置商品数量,以避免出现超卖的情况。
  2. 实时更新库存信息:在秒杀活动进行期间,需要实时更新库存信息,避免超卖的情况。可以通过技术手段,比如使用分布式锁、队列等方式实现库存的实时更新。
  3. 限制用户购买数量:可以限制每个用户购买的数量,避免某些用户恶意抢购导致超卖的情况。
  4. 及时通知用户:如果出现了超卖的情况,需要及时通知用户,并及时退款或者提供其他的补偿措施,保障用户的权益和满意度。

3、秒杀有几种实现方式

秒杀活动的实现方式有多种,以下是其中几种常用的方式,以及使用的技术实现:

  1. 队列方式:将用户的请求放入队列中,由队列进行处理,避免并发请求对系统造成压力,同时也可以保证请求的顺序。使用技术:消息队列(如RabbitMQ、Kafka)。
  2. 分布式锁方式:使用分布式锁来控制并发请求的访问,避免重复的请求对系统造成压力。使用技术:分布式锁(如Redis、Zookeeper)。
  3. 缓存方式:将商品信息预先缓存到缓存服务器中,当用户请求时从缓存中获取,避免频繁地访问数据库。使用技术:缓存(如Redis、Memcached)。
  4. 限流方式:通过限制每个用户的请求次数来控制并发请求的数量,避免系统崩溃。使用技术:限流算法(如令牌桶算法、漏桶算法)。
  5. 异步方式:使用异步处理的方式来处理请求,通过将请求放入消息队列中异步处理,避免系统压力过大。使用技术:消息队列(如RabbitMQ、Kafka)。

以上是常用的几种秒杀活动的实现方式,不同的方式适用于不同的场景和需求,需要根据实际情况进行选择。同时,还需要注意系统的稳定性和安全性,避免出现系统崩溃或者数据泄露等问题。

4、秒杀实现的逻辑步骤

秒杀活动的实现逻辑步骤如下:

  1. 预热阶段:在秒杀活动开始前,提前进行预热,向用户宣传活动信息,吸引用户的关注。

  2. 商品准备阶段:在秒杀活动开始前,需要准备好秒杀商品的信息,包括商品名称、价格、库存等信息。

  3. 用户抢购阶段:秒杀活动开始后,用户可以进入抢购页面进行抢购。在这个阶段,需要进行以下处理:
    a. 验证用户身份:首先需要验证用户的身份,确保只有注册用户才能参与抢购活动。
    b. 验证商品库存:当用户提交抢购请求时,需要验证商品的库存是否充足,如果库存不足,则返回抢购失败的信息。
    c. 生成订单:当用户抢购成功时,需要生成订单,并将订单信息保存到数据库中。
    d. 扣减库存:当用户抢购成功时,需要扣减商品的库存数量。
    e. 支付订单:当用户抢购成功时,需要进行支付操作,将订单的金额从用户的账户中扣除。

  4. 结束阶段:当秒杀活动结束后,需要进行以下处理:
    a. 结算订单:对于已经生成的订单,需要进行结算操作,将订单的金额结算给商家。
    b. 处理退款:对于用户取消订单或者支付失败的情况,需要进行退款操作,将金额返还给用户。
    c. 统计数据:需要对秒杀活动的数据进行统计,包括参与人数、抢购成功率、销售额等信息。

以上是秒杀活动的实现逻辑步骤,不同的实现方式可能会有所不同,需要根据实际情况进行选择。同时,还需要注意系统的稳定性和安全性,避免出现系统崩溃或者数据泄露等问题。

5、基于 Redis 实现一个简单的秒杀

实现代码如下:

package com.pany.camp.redis;

import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

/**
 *
 * @description:  秒杀样例
 * @copyright: @Copyright (c) 2022 
 * @company: Aiocloud
 * @author: pany
 * @version: 1.0.0 
 * @createTime: 2023-06-26 12:37
 */
public class SeckillDemo {
    private static final String REDIS_HOST = "localhost";
    private static final int REDIS_PORT = 6379;
    private static final String REDIS_PASSWORD = null;
    private static final int REDIS_TIMEOUT = 10000;
    private static final int REDIS_MAX_TOTAL = 100;
    private static final int REDIS_MAX_IDLE = 10;
    private static final int REDIS_MAX_WAIT_MILLIS = 10000;
    private static final String STOCK_KEY = "stock";
    private static final String ORDER_QUEUE_KEY = "order_queue";
    private static final String LOCK_KEY = "lock";
    private static JedisPool jedisPool;


    static {
        JedisPoolConfig config = new JedisPoolConfig();
        config.setMaxTotal(REDIS_MAX_TOTAL);
        config.setMaxIdle(REDIS_MAX_IDLE);
        config.setMaxWaitMillis(REDIS_MAX_WAIT_MILLIS);
        jedisPool = new JedisPool(config, REDIS_HOST, REDIS_PORT, REDIS_TIMEOUT, REDIS_PASSWORD);
    }

    public static void main(String[] args) {

        Jedis jedis = jedisPool.getResource();

        try {
            // 初始化库存数量
            jedis.set(STOCK_KEY, "100");

            // 模拟多个用户发起秒杀请求
            for (int i = 0; i < 200; i++) {
                Thread thread = new Thread(new UserThread());
                thread.start();
            }
        } finally {
            if (jedis != null) {
                jedis.close();
            }
        }
    }

    static class UserThread implements Runnable {

        public UserThread() {
        }

        @Override
        public void run() {
            Jedis jedis = jedisPool.getResource();
            try {
                handleSecKill(jedis);
            } catch (Exception e) {

            } finally {
                jedis.close();
            }
        }

        /**
         * 处理秒杀
         * 
         * @since 1.0.0
         * @param 
         * @param jedis
         * @return: void 
         * @author: pany 
         * @version: 1.0.0 
         * @createTime: 2023-06-26 12:13 
         */  
        private void handleSecKill(Jedis jedis) {
            String userId = Thread.currentThread().getName();

            // 检查库存数量是否大于0
            String stock = jedis.get(STOCK_KEY);
            int stockNum = StringUtils.isBlank(stock) ? 0:Integer.parseInt(stock);
            if (stockNum <= 0) {
                System.out.println(userId + " 秒杀失败,库存不足");
                return;
            }

            // 将用户的请求加入到队列中
            jedis.lpush(ORDER_QUEUE_KEY, userId);
            // 获取分布式锁
            String lockValue = System.currentTimeMillis() + "";
            Long result = jedis.setnx(LOCK_KEY, lockValue);
            while (result == 0) {
                // 如果获取锁失败,则等待一段时间后重新尝试获取锁
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                result = jedis.setnx(LOCK_KEY, lockValue);
            }

            try {
                // 再次检查库存数量是否大于0
                stock = jedis.get(STOCK_KEY);
                if (Integer.parseInt(stock) <= 0) {
                    System.out.println(userId + " 秒杀失败,库存不足");
                    return;
                }
                // 减少库存数量,并将用户的请求从队列中移除
                jedis.decr(STOCK_KEY);
                jedis.lrem(ORDER_QUEUE_KEY, 0, userId);
                System.out.println(userId + " 秒杀成功,库存剩余:" + jedis.get(STOCK_KEY));
            } finally {
                // 释放锁
                if (lockValue.equals(jedis.get(LOCK_KEY))) {
                    jedis.del(LOCK_KEY);
                }
            }
        }
    }
}

输出结果如下:

Thread-30 秒杀成功,库存剩余:99
Thread-60 秒杀成功,库存剩余:98
Thread-65 秒杀成功,库存剩余:97
Thread-110 秒杀成功,库存剩余:96
Thread-77 秒杀成功,库存剩余:95
Thread-107 秒杀成功,库存剩余:94
Thread-140 秒杀成功,库存剩余:93
Thread-109 秒杀成功,库存剩余:92
Thread-103 秒杀成功,库存剩余:91
Thread-104 秒杀成功,库存剩余:90
Thread-67 秒杀成功,库存剩余:89
Thread-111 秒杀成功,库存剩余:88
Thread-112 秒杀成功,库存剩余:87
Thread-73 秒杀成功,库存剩余:86
Thread-114 秒杀成功,库存剩余:85
Thread-115 秒杀成功,库存剩余:84
Thread-116 秒杀成功,库存剩余:83
Thread-117 秒杀成功,库存剩余:82
Thread-118 秒杀成功,库存剩余:81
Thread-82 秒杀成功,库存剩余:80
Thread-120 秒杀成功,库存剩余:79
Thread-121 秒杀成功,库存剩余:78
Thread-137 秒杀成功,库存剩余:77
Thread-123 秒杀成功,库存剩余:76
Thread-138 秒杀成功,库存剩余:75
Thread-124 秒杀成功,库存剩余:74
Thread-108 秒杀成功,库存剩余:73
Thread-127 秒杀成功,库存剩余:72
Thread-141 秒杀成功,库存剩余:71
Thread-93 秒杀成功,库存剩余:70
Thread-142 秒杀成功,库存剩余:69
Thread-159 秒杀成功,库存剩余:68
Thread-143 秒杀成功,库存剩余:67
Thread-174 秒杀成功,库存剩余:66
Thread-126 秒杀成功,库存剩余:65
Thread-162 秒杀成功,库存剩余:64
Thread-145 秒杀成功,库存剩余:63
Thread-175 秒杀成功,库存剩余:62
Thread-147 秒杀成功,库存剩余:61
Thread-131 秒杀成功,库存剩余:60
Thread-148 秒杀成功,库存剩余:59
Thread-165 秒杀成功,库存剩余:58
Thread-133 秒杀成功,库存剩余:57
Thread-149 秒杀成功,库存剩余:56
Thread-150 秒杀成功,库存剩余:55
Thread-166 秒杀成功,库存剩余:54
Thread-179 秒杀成功,库存剩余:53
Thread-96 秒杀成功,库存剩余:52
Thread-136 秒杀成功,库存剩余:51
Thread-152 秒杀成功,库存剩余:50
Thread-153 秒杀成功,库存剩余:49
Thread-102 秒杀成功,库存剩余:48
Thread-163 秒杀成功,库存剩余:47
Thread-154 秒杀成功,库存剩余:46
Thread-155 秒杀成功,库存剩余:45
Thread-156 秒杀成功,库存剩余:44
Thread-157 秒杀成功,库存剩余:43
Thread-158 秒杀成功,库存剩余:42
Thread-128 秒杀成功,库存剩余:41
Thread-160 秒杀成功,库存剩余:40
Thread-161 秒杀成功,库存剩余:39
Thread-92 秒杀成功,库存剩余:38
Thread-130 秒杀成功,库存剩余:37
Thread-192 秒杀成功,库存剩余:36
Thread-193 秒杀成功,库存剩余:35
Thread-194 秒杀成功,库存剩余:34
Thread-167 秒杀成功,库存剩余:33
Thread-168 秒杀成功,库存剩余:32
Thread-169 秒杀成功,库存剩余:31
Thread-170 秒杀成功,库存剩余:30
Thread-171 秒杀成功,库存剩余:29
Thread-139 秒杀成功,库存剩余:28
Thread-172 秒杀成功,库存剩余:27
Thread-173 秒杀成功,库存剩余:26
Thread-144 秒杀成功,库存剩余:25
Thread-146 秒杀成功,库存剩余:24
Thread-177 秒杀成功,库存剩余:23
Thread-178 秒杀成功,库存剩余:22
Thread-151 秒杀成功,库存剩余:21
Thread-180 秒杀成功,库存剩余:20
Thread-181 秒杀成功,库存剩余:19
Thread-182 秒杀成功,库存剩余:18
Thread-183 秒杀成功,库存剩余:17
Thread-184 秒杀成功,库存剩余:16
Thread-185 秒杀成功,库存剩余:15
Thread-186 秒杀成功,库存剩余:14
Thread-187 秒杀成功,库存剩余:13
Thread-188 秒杀成功,库存剩余:12
Thread-189 秒杀成功,库存剩余:11
Thread-190 秒杀成功,库存剩余:10
Thread-191 秒杀成功,库存剩余:9
Thread-164 秒杀成功,库存剩余:8
Thread-132 秒杀成功,库存剩余:7
Thread-134 秒杀成功,库存剩余:6
Thread-195 秒杀成功,库存剩余:5
Thread-196 秒杀成功,库存剩余:4
Thread-197 秒杀成功,库存剩余:3
Thread-198 秒杀成功,库存剩余:2
Thread-199 秒杀成功,库存剩余:1
Thread-200 秒杀成功,库存剩余:0
Thread-176 秒杀失败,库存不足
Thread-44 秒杀失败,库存不足
Thread-75 秒杀失败,库存不足
Thread-40 秒杀失败,库存不足
Thread-99 秒杀失败,库存不足
Thread-42 秒杀失败,库存不足
Thread-90 秒杀失败,库存不足
Thread-50 秒杀失败,库存不足
Thread-135 秒杀失败,库存不足
Thread-70 秒杀失败,库存不足
Thread-74 秒杀失败,库存不足
Thread-66 秒杀失败,库存不足
Thread-94 秒杀失败,库存不足
Thread-24 秒杀失败,库存不足
Thread-95 秒杀失败,库存不足
Thread-54 秒杀失败,库存不足
Thread-129 秒杀失败,库存不足
Thread-3 秒杀失败,库存不足
Thread-86 秒杀失败,库存不足
Thread-28 秒杀失败,库存不足
Thread-113 秒杀失败,库存不足
Thread-9 秒杀失败,库存不足
Thread-88 秒杀失败,库存不足
Thread-98 秒杀失败,库存不足
Thread-41 秒杀失败,库存不足
Thread-87 秒杀失败,库存不足
Thread-58 秒杀失败,库存不足
Thread-72 秒杀失败,库存不足
Thread-4 秒杀失败,库存不足
Thread-85 秒杀失败,库存不足
Thread-63 秒杀失败,库存不足
Thread-100 秒杀失败,库存不足
Thread-10 秒杀失败,库存不足
Thread-80 秒杀失败,库存不足
Thread-33 秒杀失败,库存不足
Thread-106 秒杀失败,库存不足
Thread-43 秒杀失败,库存不足
Thread-83 秒杀失败,库存不足
Thread-17 秒杀失败,库存不足
Thread-76 秒杀失败,库存不足
Thread-35 秒杀失败,库存不足
Thread-84 秒杀失败,库存不足
Thread-78 秒杀失败,库存不足
Thread-125 秒杀失败,库存不足
Thread-27 秒杀失败,库存不足
Thread-119 秒杀失败,库存不足
Thread-15 秒杀失败,库存不足
Thread-89 秒杀失败,库存不足
Thread-39 秒杀失败,库存不足
Thread-68 秒杀失败,库存不足
Thread-71 秒杀失败,库存不足
Thread-91 秒杀失败,库存不足
Thread-32 秒杀失败,库存不足
Thread-62 秒杀失败,库存不足
Thread-34 秒杀失败,库存不足
Thread-81 秒杀失败,库存不足
Thread-49 秒杀失败,库存不足
Thread-97 秒杀失败,库存不足
Thread-7 秒杀失败,库存不足
Thread-79 秒杀失败,库存不足
Thread-22 秒杀失败,库存不足
Thread-23 秒杀失败,库存不足
Thread-25 秒杀失败,库存不足
Thread-53 秒杀失败,库存不足
Thread-38 秒杀失败,库存不足
Thread-37 秒杀失败,库存不足
Thread-29 秒杀失败,库存不足
Thread-6 秒杀失败,库存不足
Thread-19 秒杀失败,库存不足
Thread-14 秒杀失败,库存不足
Thread-48 秒杀失败,库存不足
Thread-31 秒杀失败,库存不足
Thread-51 秒杀失败,库存不足
Thread-64 秒杀失败,库存不足
Thread-122 秒杀失败,库存不足
Thread-61 秒杀失败,库存不足
Thread-55 秒杀失败,库存不足
Thread-36 秒杀失败,库存不足
Thread-52 秒杀失败,库存不足
Thread-69 秒杀失败,库存不足
Thread-2 秒杀失败,库存不足
Thread-26 秒杀失败,库存不足
Thread-12 秒杀失败,库存不足
Thread-20 秒杀失败,库存不足
Thread-57 秒杀失败,库存不足
Thread-8 秒杀失败,库存不足
Thread-59 秒杀失败,库存不足
Thread-13 秒杀失败,库存不足
Thread-47 秒杀失败,库存不足
Thread-5 秒杀失败,库存不足
Thread-46 秒杀失败,库存不足
Thread-21 秒杀失败,库存不足
Thread-101 秒杀失败,库存不足
Thread-45 秒杀失败,库存不足
Thread-1 秒杀失败,库存不足
Thread-18 秒杀失败,库存不足
Thread-16 秒杀失败,库存不足
Thread-56 秒杀失败,库存不足
Thread-105 秒杀失败,库存不足
Thread-11 秒杀失败,库存不足

需要引入依赖:

 <dependency>
  	<groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.7.0</version>
</dependency>

6、“秒杀”生产代码部分展示

下面是一个基于微信支付的秒杀程序:

package com.aiocloud.group.camp.controller;

import com.aiocloud.camp.common.bean.BeanConvertor;
import com.aiocloud.camp.common.domain.AiocloudPageResult;
import com.aiocloud.camp.common.domain.AiocloudResult;
import com.aiocloud.camp.common.enums.*;
import com.aiocloud.camp.common.utils.DateUtil;
import com.aiocloud.camp.dao.group.vo.GpOrderVO;
import com.aiocloud.group.camp.ai.IGpOrderApp;
import com.aiocloud.group.camp.ai.dto.GpOrderDTO;
import com.aiocloud.group.camp.config.RedisLockConfig;
import com.aiocloud.group.camp.config.WxConfig;
import com.aiocloud.group.camp.utils.CommonUtil;
import com.aiocloud.group.camp.utils.MD5Util;
import com.github.wxpay.sdk.WXPayConstants;
import com.github.wxpay.sdk.WXPayUtil;
import io.swagger.annotations.ApiOperation;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.util.*;

import static com.aiocloud.group.camp.config.WxConfig.appid;

@RestController
@RequestMapping("/v1/seckill")
public class GpSeckillController {

    private static Logger logger = LoggerFactory.getLogger(GpSeckillController.class);

    /**
     * 超时时间
     */
    private static final int TIMEOUT = 10000;

    @Resource
    private IGpOrderApp gpOrderApp;

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private RedisLockConfig redisLock;

    @PostMapping("/get")
    public AiocloudResult<String> getCommodity(
            @RequestParam(value = "seckillId", required = true) String seckillId,
            @RequestParam(value = "userId", required = true) String userId
    ) {
        AiocloudResult<String> result = new AiocloudResult<>();
        try{
            long time = System.currentTimeMillis() + TIMEOUT;

            //加锁
            String lockKey = "aiocloud:seckill:" + seckillId;
            if (!redisLock.lock(lockKey, String.valueOf(time))){
                result.setCode(200);
                result.setMsg("抱歉,没抢到,换个姿式再来一遍");
                result.setData("fail");
                return result;
            }

            Integer stockNum = (Integer) redisTemplate.opsForValue().get(seckillId);
            if (stockNum == 0) {
                //库存不足
                result.setCode(200);
                result.setMsg("抱歉,商品已经被抢光了,请留意下次活动");
                result.setData("fail");
                return result;
            } else {
                String userKey = seckillId + "-" + userId;
                Boolean hasKey = redisTemplate.hasKey(userKey);
                if(hasKey) {
                    result.setCode(200);
                    result.setMsg("您已经抢购到了一份商品,不能重复抢购");
                    result.setData("fail");
                    return result;
                }
                else {
                    stockNum--;
                    redisTemplate.opsForValue().set(seckillId, stockNum);
                    redisTemplate.opsForValue().set(userKey, stockNum);
                }
            }
            //解锁
            redisLock.unlock(lockKey, String.valueOf(time));
            result.setData("success");
        }catch(Exception e){
            result.setCode(AiocloudPageResult.ERROR);
            result.setMsg("秒杀商品失败");
        }
        return result;
    }

    @PostMapping("/stock")
    public AiocloudResult<Integer> getStockNum(
            @RequestParam(value = "seckillId", required = true) String seckillId
    ) {
        AiocloudResult<Integer> result = new AiocloudResult<>();
        try{
            Integer stockNum = (Integer) redisTemplate.opsForValue().get(seckillId);
            result.setData(stockNum);
        } catch(Exception e) {
            logger.error("查询库存异常, cause by: " + e.getMessage());
            result.setCode(AiocloudPageResult.ERROR);
            result.setMsg("查询库存异常");
        }
        return result;
    }

    /**
     * @param [openid, outTradeNo, totalAmount, subject, body, request]
     * @return com.aiocloud.camp.common.domain.AiocloudResult<java.lang.Object>
     * @Title: pay
     * @Description: 秒杀微信支付
     * @author panyong
     * @version 1.0
     * @createtime 2021-04-07 13:42
     */
    @PostMapping("/pay")
    public AiocloudResult<Object> pay(
            @RequestParam(value = "commodityId", required = true) String commodityId,
            @RequestParam(value = "seckillId", required = true) String seckillId,
            @RequestParam(value = "addressId", required = true) String addressId,
            @RequestParam(value = "remark", required = true) String remark,
            @RequestParam(value = "openid", required = true) String openid,
            @RequestParam(value = "outTradeNo", required = true) String outTradeNo,
            @RequestParam(value = "totalAmount", required = true) String totalAmount,
            @RequestParam(value = "subject", required = true) String subject,
            @RequestParam(value = "body", required = true) String body,
            HttpServletRequest request
    ) {
        AiocloudResult<Object> result = new AiocloudResult<>();
        try {
            // 1、参数校验
            if (
                    StringUtils.isBlank(commodityId)
                            || StringUtils.isBlank(addressId) || StringUtils.isBlank(openid)
                            || StringUtils.isBlank(outTradeNo) || StringUtils.isBlank(totalAmount)
                            || StringUtils.isBlank(subject) || StringUtils.isBlank(body) || StringUtils.isBlank(seckillId)
                    ) {
                logger.error("参数提供错误");
                result.setMsg("参数提供错误");
                return result;
            }
            // 生成订单
            GpOrderDTO orderDTO = new GpOrderDTO();
            orderDTO.setOutTradeNo(outTradeNo);
            orderDTO.setPayStatus(PayStatusEnum.Unpaid.getCode());
            orderDTO.setOrderStatus(OrderStatusEnum.OrderHasBeenPlaced.getCode());
            orderDTO.setPayWay(PayWayEnum.WeChat.getCode());
            orderDTO.setSubject(subject);
            orderDTO.setDescription(body);
            orderDTO.setOpenid(openid);
            orderDTO.setCreateTime(new Date());
            BigDecimal bigDecimal = new BigDecimal(totalAmount);
            orderDTO.setTotalAmount(bigDecimal);
            orderDTO.setCommodityId(commodityId);
            orderDTO.setNum(1);
            orderDTO.setType(OrderTypeEnum.seckill.getCode());
            orderDTO.setRemark(remark);
            orderDTO.setAddressId(addressId);
            orderDTO.setRelateId(seckillId);
            boolean isSuccess = gpOrderApp.add(orderDTO);
            if (isSuccess) {
                Map<String, String> resultMap = getWxPayMap(outTradeNo, totalAmount, openid, request);
                resultMap.put("signType", "MD5");
                resultMap.put("total_fee", totalAmount);
                String timestamp = String.valueOf(DateUtil.getSecondTimestamp(new Date()));
                resultMap.put("timestamp", timestamp);
                String stringSignTemp = "appId=" + resultMap.get("appid") +
                        "&nonceStr=" + resultMap.get("nonce_str") +
                        "&package=prepay_id=" + resultMap.get("prepay_id") +
                        "&signType=MD5" +
                        "&timeStamp=" + resultMap.get("timestamp") +
                        "&key=" + WxConfig.apiKey;
                String finalsign = MD5Util.getMD5Str(stringSignTemp).toUpperCase();
                resultMap.put("finalsign", finalsign);
                result.setData(resultMap);
            }
        } catch (Exception e) {
            result.setCode(AiocloudResult.ERROR);
            result.setMsg("支付失败");
        }
        return result;
    }

    @Transactional
    public Map<String, String> getWxPayMap(String outTradeNo, String totalAmount, String openId, HttpServletRequest request) throws Exception {
        String orderExpireTime = DateUtil.getOrderExpireTime(6 * 60 * 1000L);
        SortedMap<String, String> req = new TreeMap<String, String>();
        //公众号
        req.put("appid", appid);
        // 商户号
        req.put("mch_id", WxConfig.mchId);
        // 32位随机字符串
        req.put("nonce_str", WXPayUtil.generateNonceStr());
        // 商品描述
        req.put("body", "aiocloud开发微信支付测试");
        // 商户订单号
        req.put("out_trade_no", outTradeNo);
        DecimalFormat decimalFormat = new DecimalFormat("###################.###########");
        // 标价金额(分)
        String total_fee = decimalFormat.format(Double.parseDouble(totalAmount) * 100);
        req.put("total_fee", "1");
        // 终端IP
        req.put("spbill_create_ip", CommonUtil.getIp(request));
        // 回调地址
        req.put("notify_url", WxConfig.notifyUrl);
        // 交易类型
        req.put("trade_type", WxConfig.tradeType);
        // 超时时间
        req.put("time_expire", orderExpireTime);
        req.put("openid", openId);
        req.put("sign", WXPayUtil.generateSignature(req, WxConfig.apiKey, WXPayConstants.SignType.MD5));  // 签名
        // 生成要发送的 xml
        String xmlBody = WXPayUtil.generateSignedXml(req, WxConfig.apiKey);
        System.err.println(String.format("微信支付预下单请求 xml 格式:\n%s", xmlBody));
        //发送 POST 请求 统一下单 API 并携带 xmlBody 内容,然后获得返回接口结果
        String res = CommonUtil.httpsRequest("https://api.mch.weixin.qq.com/pay/unifiedorder", "POST", xmlBody);
        System.err.println(String.format("%s", res));
        Map<String, String> resultMap = WXPayUtil.xmlToMap(res);
        //将返回结果从 xml 格式转换为 map 格式
        return resultMap;
    }

    /**
     * 接收微信官方返回觉得支付结果通知
     *
     * @return
     * @throws Exception
     */
    @ApiOperation("接收支付结果通知的接口")
    @PostMapping("/getNotifyUrl")
    @ResponseBody
    public String weiXinPayCallBack(HttpServletRequest request) throws Exception {
        Map<String, String> rMap = new HashMap<String, String>();
        //接收微信官方返回的支付结果
        InputStream inputStream = request.getInputStream();
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
        String temp;
        StringBuilder stringBuilder = new StringBuilder();
        while ((temp = bufferedReader.readLine()) != null) {
            stringBuilder.append(temp);
        }
        //关闭流,先打开后关,后打开先关
        bufferedReader.close();
        inputStream.close();
        Map<String, String> resultMap = WXPayUtil.xmlToMap(stringBuilder.toString());
        //判断是不是来自微信官方,进行签名验证
        boolean flag = WXPayUtil.isSignatureValid(resultMap, WxConfig.apiKey, WXPayConstants.SignType.MD5);
        if (flag) {
            //获取支付结果中的result_code,根据此值判断是否进行自身业务实现
            String resultCode = resultMap.get("result_code");
            if (resultCode.equals("SUCCESS")) {
                //获取微信返回的交易号
                String tradeNo = resultMap.get("transaction_id");
                //商户订单号
                String outTradeNo = resultMap.get("out_trade_no");
                //支付状态判断和自身业务实现
                GpOrderVO order = gpOrderApp.getOrderByOutTradeNo(outTradeNo);
                if (order != null) {
                    //格式化成所需的商品金额
                    BigDecimal totalAmount = order.getTotalAmount();
                    double TotalFeeDouble = totalAmount.doubleValue() * 100;
                    //回调接口和实际订单进行比较
                    if (TotalFeeDouble == Double.parseDouble(resultMap.get("total_fee"))) {
                        //修改订单支付状态
                        order.setPayTime(new Date());
                        order.setPayStatus(PayStatusEnum.PaymentSuccessful.getCode());
                        order.setOrderStatus(OrderStatusEnum.CompleteOrder.getCode());
                        order.setConfirm(OrderConfirmEnum.Delivered.getCode());
                        order.setTradeNo(tradeNo);
                        gpOrderApp.update(BeanConvertor.getCopyObject(GpOrderDTO.class, order));
                        rMap.put("return_code", "SUCCESS");
                        rMap.put("return_msg", "已收到");
                    }
                } else {
                    rMap.put("return_code", "FAIL");
                    rMap.put("return_msg", "已收到");
                }
            } else {
                rMap.put("return_code", "FAIL");
                rMap.put("return_msg", "已收到");
            }
        } else {
            rMap.put("return_code", "FAIL");
            rMap.put("return_msg", "已收到");
        }
        //以xml格式给微信官方返回是否成功的应答
        return WXPayUtil.mapToXml(rMap);
    }

    @PostMapping("/wait/pay")
    public AiocloudResult<Object> pay(
            @RequestParam(value = "outTradeNo", required = true) String outTradeNo,
            @RequestParam(value = "openid", required = true) String openid,
            HttpServletRequest request
    ) {
        AiocloudResult<Object> result = new AiocloudResult<>();
        try {
            // 1、参数校验
            if (StringUtils.isBlank(outTradeNo)) {
                logger.error("参数提供错误");
                result.setMsg("参数提供错误");
                return result;
            }

            GpOrderVO order = gpOrderApp.getOrderByOutTradeNo(outTradeNo);
            String totalAmount = order.getTotalAmount().toString();
            Map<String, String> resultMap = getWxPayMap(outTradeNo, totalAmount, openid, request);
            resultMap.put("signType", "MD5");
            resultMap.put("total_fee", totalAmount);
            String timestamp = String.valueOf(DateUtil.getSecondTimestamp(new Date()));
            resultMap.put("timestamp", timestamp);
            String stringSignTemp = "appId=" + resultMap.get("appid") +
                    "&nonceStr=" + resultMap.get("nonce_str") +
                    "&package=prepay_id=" + resultMap.get("prepay_id") +
                    "&signType=MD5" +
                    "&timeStamp=" + resultMap.get("timestamp") +
                    "&key=" + WxConfig.apiKey;
            String finalsign = MD5Util.getMD5Str(stringSignTemp).toUpperCase();
            resultMap.put("finalsign", finalsign);
            result.setData(resultMap);
        } catch (Exception e) {
            result.setCode(AiocloudResult.ERROR);
            result.setMsg("支付失败");
        }
        return result;
    }
}

在这里插入图片描述

💕💕 本文由激流原创,首发于CSDN博客,博客主页 https://blog.csdn.net/qq_37967783?spm=1010.2135.3001.5421
💕💕喜欢的话记得点赞收藏啊

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

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

相关文章

Git分布式版本控制工具 —— 详细笔记

❤ 作者主页&#xff1a;欢迎来到我的技术博客&#x1f60e; ❀ 个人介绍&#xff1a;大家好&#xff0c;本人热衷于Java后端开发&#xff0c;欢迎来交流学习哦&#xff01;(&#xffe3;▽&#xffe3;)~* &#x1f34a; 如果文章对您有帮助&#xff0c;记得关注、点赞、收藏、…

现代操作系统(中)

第三章 内存管理 概述 内存&#xff08;RAM&#xff09;是计算机中一种需要认真管理的重要资源。 经过多年探索&#xff0c;人们提出了分层存储器体系&#xff08;memory hierarchy&#xff09;的概念&#xff0c;即在这个体系中&#xff0c;计算机有若干兆&#xff08;MB&a…

统信UOS系统开发笔记(七):在统信UOS系统上使用linuxdeployqt发布qt程序

若该文为原创文章&#xff0c;转载请注明原文出处 本文章博客地址&#xff1a;https://hpzwl.blog.csdn.net/article/details/131411975 红胖子(红模仿)的博文大全&#xff1a;开发技术集合&#xff08;包含Qt实用技术、树莓派、三维、OpenCV、OpenGL、ffmpeg、OSG、单片机、软…

qt QSqlRelationalTableModel 详解

背景知识&#xff1a; Qt SQL的API分为不同层&#xff1a; 驱动层 驱动层 对于QT是基于C来实现的框架&#xff0c;该层主要包括QSqlDriver、QSqlDriverCreator、QSqlDriverCreatorbase、QSqlDriverPlugin and QSqlResult。这一层提供了特定数据库和SQL API层之间的底层桥梁…

Problem I Rank LED题解 - 2018年第一届GXCPC广西大学生程序设计大赛 正式赛

Problem I Rank LED题解 题目大意 ‘0’到‘9’的数字亮线依次为{6、2、5、5、4、5、6、3、7、6}。 Luras想修改每条光线的位置&#xff0c;使她的新等级尽可能小&#xff0c;同时新等级也是一个不带任何前导零的正整数。 另外&#xff0c;光线总数应与开始时相同。 官方题…

【AIGC】1、爆火的 AIGC 到底是什么 | 全面介绍

文章目录 一、AIGC 的简要介绍二、AIGC 的发展历程三、AIGC 的基石3.1 基本模型3.2 基于人类反馈的强化学习3.3 算力支持 四、生成式 AI&#xff08;Generative AI&#xff09;4.1 单模态4.1.1 生成式语言模型&#xff08;Generative Language Models&#xff0c;GLM&#xff0…

消息处理机制(AOSP4.4.2)

消息处理机制&#xff08;AOSP4.4.2&#xff09; Android 应用程序是通过消息来驱动的&#xff0c;系统为每一个应用程序维护一个消息队列&#xff0c;应用程序的主线程&#xff0c;不断地从这个消息队列中获取消息&#xff08;Looper&#xff09;&#xff0c;然后对消息进行处…

STM32单片机(三)第二节:GPIO输出练习2(LED流水灯)

❤️ 专栏简介&#xff1a;本专栏记录了从零学习单片机的过程&#xff0c;其中包括51单片机和STM32单片机两部分&#xff1b;建议先学习51单片机&#xff0c;其是STM32等高级单片机的基础&#xff1b;这样再学习STM32时才能融会贯通。 ☀️ 专栏适用人群 &#xff1a;适用于想要…

3 分钟为英语学习神器 Anki 部署一个专属同步服务器

原文链接&#xff1a;https://icloudnative.io/posts/anki-sync-server/ Anki 介绍 Anki 是一个辅助记忆软件&#xff0c;其本质是一个卡片排序工具--即依据使用者对卡片上的自定义内容进行主动测试、自我评判后&#xff0c;其内部算法根据评判结果更改每张卡片下次测试时间的…

将win上的文件传输到Ubuntu虚拟机

首先获取Ubuntu系统的ip地址&#xff0c;在Ubuntu的Terminal中输入ifconfig&#xff0c;可以看到Ubuntu的ip地址 可以看到我电脑的ip地址是10.0.2.15。更改虚拟机的网络连接 这里以VirtualBox为例&#xff0c;打开VirtualBox设置&#xff0c;选择网络&#xff0c;将连接方式改…

React.JS实战项目(三):图书购物网站

React.JS实战项目(三):图书购物网站 1、菜单 首页图书新书购物车2、首页 首页视频预览 首页预览 首页主要展示了友情链接、图书分类、好书推荐、新书广场等等信息。 首页部分代码展示 <Row><Col

SpringSecurity整合ssm

SpringSecurity 1. SpringSecurity 框架简介 Spring 是非常流行和成功的 Java 应用开发框架&#xff0c;Spring Security 正是 Spring 家族中的成员。Spring Security 基于 Spring 框架&#xff0c;提供了一套 Web 应用安全性的完整解决方 案。 正如你可能知道的关于安全方面…

nexus 配置pypi代理

在研发环境中由于网络限制&#xff0c;无法访问外网&#xff0c;但经常使用npm、maven、pip等工具&#xff0c;这种场景中使用nexus 做代理是一个比较好的解决办法。 在配置pypi代理时&#xff0c;和配置npm、maven代理有所不同&#xff0c;在配置远程地址时&#xff0c;需要将…

我的IDEA插件

文章目录 前言一、.ignore二、Adapter for Eclipse Code Formatter三、Convert YAML and Properties File四、EasyCode五、Free MyBatis Tool六、Maven Helper七、Rainbow Brackets 前言 目前使用比较顺手的插件&#xff0c;具体使用方法自行查阅 一、.ignore git 忽略文件&…

【算法与数据结构】344、LeetCode反转字符串

文章目录 一、题目二、解法三、完整代码 所有的LeetCode题解索引&#xff0c;可以看这篇文章——【算法和数据结构】LeetCode题解。 一、题目 二、解法 思路分析&#xff1a;关于变量交换有两种办法&#xff0c;一种是最常见的引入一个临时变量方法&#xff0c;另一种是使用位运…

【Java面试题】Java基础——集合

文章目录 集合的形式List和Set的区别ArrayList和LinkedList的区别ArrayList和数组的区别ArrayList的扩容机制是什么&#xff1f;ArrayList有哪些特点List和Map的区别如何让map存储有序数据如何创建Map?常用的Map有哪些?如何在HashMap中插入一个数据遍历一个 List 有哪些不同的…

高清音频文件如何压缩?分享轻松压缩音频文件的方法!

如何进行音频压缩&#xff1f;在我们日常生活中&#xff0c;音频文件扮演着重要的角色&#xff0c;我们可以通过它们享受音乐、收听播客或处理语音录音等。然而&#xff0c;有时候这些音频文件的大小可能会成为问题&#xff0c;特别是当我们需要在有限的存储空间中存储更多的文…

深入浅出设计模式 - 原型模式

博主介绍&#xff1a; ✌博主从事应用安全和大数据领域&#xff0c;有8年研发经验&#xff0c;5年面试官经验&#xff0c;Java技术专家✌ Java知识图谱点击链接&#xff1a;体系化学习Java&#xff08;Java面试专题&#xff09; &#x1f495;&#x1f495; 感兴趣的同学可以收…

STM32模拟I2C获取TCS34725光学颜色传感器数据

STM32模拟I2C获取TCS34725光学颜色传感器数据 TCS34725是RGB三色颜色传感器&#xff0c;和TCS34727都属于TCS3472系列&#xff0c;在电气特性上略有差别&#xff0c;TCS34727相比TCS34725在I2C总线的访问电平上可以更低&#xff0c;而在I2C软件访问地址方面则一致。 TCS3472内…

leetcode:1431. 拥有最多糖果的孩子(python3解法)

难度&#xff1a;简单 给你一个数组 candies 和一个整数 extraCandies &#xff0c;其中 candies[i] 代表第 i 个孩子拥有的糖果数目。 对每一个孩子&#xff0c;检查是否存在一种方案&#xff0c;将额外的 extraCandies 个糖果分配给孩子们之后&#xff0c;此孩子有 最多 的糖…