基于 Redis 手写一个“秒杀”

本文涉及的产品
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
简介: 基于 Redis 手写一个“秒杀”

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

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

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

1687754999068.jpg

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;
    }
}

1686494501743.jpg

💕💕 本文由激流丶创作,原创不易,感谢支持!
💕💕喜欢的话记得点赞收藏啊!

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
8天前
|
NoSQL 关系型数据库 MySQL
Redis之秒杀系统
秒杀是一种高并发场景,通常指的是在短时间内(秒级别)有大量用户同时访问某个商品或服务,争相抢购的情景。在这种情况下,系统需要处理大量并发请求,确保公平性、一致性,并防止因并发而导致的问题,例如超卖、恶意请求等。以下是在高并发秒杀场景下需要考虑的一些关键问题和解决方案:
|
8天前
|
NoSQL 关系型数据库 MySQL
Redis入门到通关之Redis实现分布式锁
Redis入门到通关之Redis实现分布式锁
29 1
|
8天前
|
存储 缓存 NoSQL
Redis入门到通关之Redis缓存数据实战
Redis入门到通关之Redis缓存数据实战
25 0
|
8天前
|
缓存 NoSQL Java
面试官:Redis如何实现延迟任务?
延迟任务是计划任务,用于在未来特定时间执行。常见应用场景包括定时通知、异步处理、缓存管理、计划任务、订单处理、重试机制、提醒和数据采集。Redis虽无内置延迟任务功能,但可通过过期键通知、ZSet或Redisson实现。然而,这种方法精度有限,稳定性较差,适合轻量级需求。Redisson的RDelayedQueue提供更简单的延迟队列实现。
371 9
|
7月前
|
存储 缓存 NoSQL
订单系统如何使用redis
订单系统如何使用redis
|
7月前
|
缓存 NoSQL Redis
Redis学习笔记-秒杀活动中Redis的作用
Redis学习笔记-秒杀活动中Redis的作用
39 0
|
11月前
|
NoSQL Java 程序员
Redis分布式锁常见坑点分析
Redis分布式锁常见坑点分析
236 0
|
NoSQL Redis
我的分享:第八章: 用Redis轻松实现秒杀系统
我的分享:第八章: 用Redis轻松实现秒杀系统
|
NoSQL 前端开发 PHP
thinkphp+redis实现秒杀功能
thinkphp+redis实现秒杀功能
194 0
thinkphp+redis实现秒杀功能
|
消息中间件 存储 缓存
Redis 的高并发实战:抢购系统 | 学习笔记
快速学习 Redis 的高并发实战:抢购系统
325 0
Redis 的高并发实战:抢购系统 | 学习笔记