尚品汇-秒杀下单实现-页面轮询查询订单状态(五十三)
目录:
(1)整合秒杀业务
(2)秒杀下单
(3)秒杀下单监听
(4)页面轮询接口
(1)整合秒杀业务
秒杀的主要目的就是获取一个下单资格,拥有下单资格就可以去下单支付,获取下单资格后的流程就与正常下单流程一样,只是没有购物车这一 步,总结起来就是,秒杀根据库存获取下单资格,拥有下单资格进入下单页面(选择地址,支付方式,提交订单,然后支付订单)
步骤:
- 校验下单码,只有正确获得下单码的请求才是合法请求
- 校验状态位state,
State为null,说明非法请求;
State为0说明已经售罄;
State为1,说明可以抢购
状态位是在内存中判断,效率极高,如果售罄,直接就返回了,不会给服务器造成太大压力
- 前面条件都成立,将秒杀用户加入队列,然后直接返回
- 前端轮询秒杀状态,查询秒杀结果
(2)秒杀下单
添加mq常量MqConst类
/*** 秒杀*/
public static final String EXCHANGE_DIRECT_SECKILL_USER = "exchange.direct.seckill.user";
public static final String ROUTING_SECKILL_USER = "seckill.user";
//队列
public static final String QUEUE_SECKILL_USER = "queue.seckill.user";
定义实体UserRecode
记录哪个用户要购买哪个商品!
@Data
public class UserRecode implements Serializable {private static final long serialVersionUID = 1L;private Long skuId;private String userId;
}
编写控制器SeckillGoodsApiController
@Autowired
private RabbitService rabbitService;
/*** 根据用户和商品ID实现秒杀下单* @param skuId* @return*/
@PostMapping("auth/seckillOrder/{skuId}")
public Result seckillOrder(@PathVariable("skuId") Long skuId, HttpServletRequest request) throws Exception {//校验下单码(抢购码规则可以自定义)String userId = AuthContextHolder.getUserId(request);String skuIdStr = request.getParameter("skuIdStr");if (!skuIdStr.equals(MD5.encrypt(userId))) {//请求不合法return Result.build(null, ResultCodeEnum.SECKILL_ILLEGAL);}//校验状态位//产品标识, 1:可以秒杀 0:秒杀结束String state = (String) CacheHelper.get(skuId.toString());if (StringUtils.isEmpty(state)) {//请求不合法return Result.build(null, ResultCodeEnum.SECKILL_ILLEGAL);}if ("1".equals(state)) {//用户记录UserRecode userRecode = new UserRecode();userRecode.setUserId(userId);userRecode.setSkuId(skuId);//发送消息rabbitService.sendMessage(MqConst.EXCHANGE_DIRECT_SECKILL_USER, MqConst.ROUTING_SECKILL_USER, userRecode);} else {//已售罄return Result.build(null, ResultCodeEnum.SECKILL_FINISH);}return Result.ok();
}
全局统一返回结果类:
package com.atguigu.gmall.common.result;import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;/*** 全局统一返回结果类**/
@Data
@ApiModel(value = "全局统一返回结果")
public class Result<T> {@ApiModelProperty(value = "返回码")private Integer code;@ApiModelProperty(value = "返回消息")private String message;@ApiModelProperty(value = "返回数据")private T data;public Result(){}// 返回数据protected static <T> Result<T> build(T data) {Result<T> result = new Result<T>();if (data != null)result.setData(data);return result;}public static <T> Result<T> build(T body, ResultCodeEnum resultCodeEnum) {Result<T> result = build(body);result.setCode(resultCodeEnum.getCode());result.setMessage(resultCodeEnum.getMessage());return result;}public static<T> Result<T> ok(){return Result.ok(null);}/*** 操作成功* @param data* @param <T>* @return*/public static<T> Result<T> ok(T data){Result<T> result = build(data);return build(data, ResultCodeEnum.SUCCESS);}public static<T> Result<T> fail(){return Result.fail(null);}/*** 操作失败* @param data* @param <T>* @return*/public static<T> Result<T> fail(T data){Result<T> result = build(data);return build(data, ResultCodeEnum.FAIL);}public Result<T> message(String msg){this.setMessage(msg);return this;}public Result<T> code(Integer code){this.setCode(code);return this;}public boolean isOk() {if(this.getCode().intValue() == ResultCodeEnum.SUCCESS.getCode().intValue()) {return true;}return false;}
}
统一返回结果状态信息类:
package com.atguigu.gmall.common.result;import lombok.Getter;/*** 统一返回结果状态信息类**/
@Getter
public enum ResultCodeEnum {SUCCESS(200,"成功"),FAIL(201, "失败"),SERVICE_ERROR(2012, "服务异常"),ILLEGAL_REQUEST( 204, "非法请求"),PAY_RUN(205, "支付中"),LOGIN_AUTH(208, "未登陆"),PERMISSION(209, "没有权限"),SECKILL_NO_START(210, "秒杀还没开始"),SECKILL_RUN(211, "正在排队中"),SECKILL_NO_PAY_ORDER(212, "您有未支付的订单"),SECKILL_FINISH(213, "已售罄"),SECKILL_END(214, "秒杀已结束"),SECKILL_SUCCESS(215, "抢单成功"),SECKILL_FAIL(216, "抢单失败"),SECKILL_ILLEGAL(217, "请求不合法"),SECKILL_ORDER_SUCCESS(218, "下单成功"),COUPON_GET(220, "优惠券已经领取"),COUPON_LIMIT_GET(221, "优惠券已发放完毕"),;private Integer code;private String message;private ResultCodeEnum(Integer code, String message) {this.code = code;this.message = message;}
}
(3)秒杀下单监听
思路:
- 首先判断产品状态位,我们前面不是已经判断过了吗?因为产品可能随时售罄,mq队列里面可能堆积了十万数据,但是已经售罄了,那么后续流程就没有必要再走了;
- 判断用户是否已经下过订单,这个地方就是控制用户重复下单,同一个用户只能抢购一个下单资格,怎么控制呢?很简单,我们可以利用setnx控制用户,当用户第一次进来时,返回true,可以抢购,以后进入返回false,直接返回,过期时间可以根据业务自定义,这样用户这一段咋们就控制注了
- 获取队列中的商品,如果能够获取,则商品有库存,可以下单。如果获取的商品id为空,则商品售罄,商品售罄我们要第一时间通知兄弟节点,更新状态位,所以在这里发送redis广播
- 将订单记录放入redis缓存,说明用户已经获得下单资格,秒杀成功
- 秒杀成功要更新库存
SeckillReceiver添加监听方法
@Autowired
private SeckillGoodsService seckillGoodsService;// 监听用户与商品的消息!
@SneakyThrows
@RabbitListener(bindings = @QueueBinding(value = @Queue(value = MqConst.QUEUE_SECKILL_USER,durable = "true",autoDelete = "false"),exchange = @Exchange(value = MqConst.EXCHANGE_DIRECT_SECKILL_USER),key = {MqConst.ROUTING_SECKILL_USER}
))
public void seckillUser(UserRecode userRecode,Message message,Channel channel){try {// 判断接收过来的数据if (userRecode!=null){// 预下单处理!seckillGoodsService.seckillOrder(userRecode.getSkuId(),userRecode.getUserId());}} catch (Exception e) {e.printStackTrace();}// 手动确认channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}
预下单接口SeckillGoodsService接口
/*** 根据用户和商品ID实现秒杀下单* @param skuId* @param userId*/
void seckillOrder(Long skuId, String userId);
秒杀订单实体类
package com.atguigu.gmall.model.activity;@Data
public class OrderRecode implements Serializable {private static final long serialVersionUID = 1L;private String userId;private SeckillGoods seckillGoods;private Integer num;private String orderStr;
}

实现类
/**** 创建订单* @param skuId* @param userId*/
@Override
public void seckillOrder(Long skuId, String userId) {//产品状态位, 1:可以秒杀 0:秒杀结束String state = (String) CacheHelper.get(skuId.toString());if("0".equals(state)) {//已售罄return;}//判断用户是否下单boolean isExist = redisTemplate.opsForValue().setIfAbsent(RedisConst.SECKILL_USER + userId, skuId.toString(), RedisConst.SECKILL__TIMEOUT, TimeUnit.SECONDS);if (!isExist) {return;}//获取队列中的商品,取List中的一个,从右边出来一个,如果能够获取,则商品存在,可以下单String goodsId = (String) redisTemplate.boundListOps(RedisConst.SECKILL_STOCK_PREFIX + skuId).rightPop();if (StringUtils.isEmpty(goodsId)) {//商品售罄,更新状态位 0位售罄状态 发布订阅消息,商品已经销售完了redisTemplate.convertAndSend("seckillpush", skuId+":0");//已售罄return;}//订单记录OrderRecode orderRecode = new OrderRecode();orderRecode.setUserId(userId);orderRecode.setSeckillGoods(this.getSeckillGoods(skuId));orderRecode.setNum(1);//生成订单单码orderRecode.setOrderStr(MD5.encrypt(userId+skuId));//订单数据存入ReidsredisTemplate.boundHashOps(RedisConst.SECKILL_ORDERS).put(orderRecode.getUserId(), orderRecode);//更新库存this.updateStockCount(orderRecode.getSeckillGoods().getSkuId());
}
更新库存
// 表示更新mysql -- redis 的库存数据!
public void updateStockCount(Long skuId) {// 加锁!Lock lock = new ReentrantLock();// 上锁lock.lock();try {// 获取到存储库存剩余数!// key = seckill:stock:46String stockKey = RedisConst.SECKILL_STOCK_PREFIX + skuId;// redisTemplate.opsForList().leftPush(key,seckillGoods.getSkuId());//获取库存//Long count = this.redisTemplate.boundListOps(RedisConst.SECKILL_STOCK_PREFIX + skuId).size();Long count = redisTemplate.boundListOps(stockKey).size();// 减少库存数!方式一减少压力!//if (count%2==0){// 开始更新数据!SeckillGoods seckillGoods = this.getSeckillGoods(skuId);// 赋值剩余库存数!seckillGoods.setStockCount(count.intValue());// 更新的数据库!seckillGoodsMapper.updateById(seckillGoods);// 更新缓存!redisTemplate.boundHashOps(RedisConst.SECKILL_GOODS).put(seckillGoods.getSkuId().toString(),seckillGoods);//}} finally {// 解锁!lock.unlock();}
}
/*** 获取商品详情* @param skuId* @return*/@Overridepublic SeckillGoods getSeckillGoods(Long skuId) {return (SeckillGoods) this.redisTemplate.boundHashOps(RedisConst.SECKILL_GOODS).get(skuId.toString());}

减少一个:之前9个

生成一个订单

用户买过之后生成一个用户的信息(防止多次下单)

(4)页面轮询接口


思路:
1. 判断用户是否在缓存中存在
2. 判断用户是否抢单成功
3. 判断用户是否下过订单
4. 判断状态位
接口
SeckillGoodsService接口
/**** 根据商品id与用户ID查看订单信息* @param skuId* @param userId* @return*/
Result checkOrder(Long skuId, String userId);
实现类
@Override
public Result checkOrder(Long skuId, String userId) {// 用户在缓存中存在,有机会秒杀到商品boolean isExist =redisTemplate.hasKey(RedisConst.SECKILL_USER + userId);if (isExist) {//判断用户是否正在排队//判断用户是否抢单成功boolean isHasKey = redisTemplate.boundHashOps(RedisConst.SECKILL_ORDERS).hasKey(userId);if (isHasKey) {//抢单成功 获取用户临时订单:说明还没有支付OrderRecode orderRecode = (OrderRecode) redisTemplate.boundHashOps(RedisConst.SECKILL_ORDERS).get(userId);// 秒杀成功! return Result.build(orderRecode, ResultCodeEnum.SECKILL_SUCCESS);}}//判断是否下单,查询总订单 seckill:orders:users userId OrderIdboolean isExistOrder = redisTemplate.boundHashOps(RedisConst.SECKILL_ORDERS_USERS).hasKey(userId);if(isExistOrder) {String orderId = (String)redisTemplate.boundHashOps(RedisConst.SECKILL_ORDERS_USERS).get(userId);return Result.build(orderId, ResultCodeEnum.SECKILL_ORDER_SUCCESS);}String state = (String) CacheHelper.get(skuId.toString());if("0".equals(state)) {//已售罄 抢单失败return Result.build(null, ResultCodeEnum.SECKILL_FAIL);}//正在排队中return Result.build(null, ResultCodeEnum.SECKILL_RUN);
}
控制器
SeckillGoodsApiController
@GetMapping(value = "auth/checkOrder/{skuId}")
public Result checkOrder(@PathVariable("skuId") Long skuId, HttpServletRequest request) {//当前登录用户String userId = AuthContextHolder.getUserId(request);return seckillGoodsService.checkOrder(skuId, userId);
}



轮询排队页面
该页面有四种状态:
- 排队中
- 各种提示(非法、已售罄等)
- 抢购成功,去下单
- 抢购成功,已下单,显示我的订单
抢购成功,页面显示去下单,跳转下单确认页面
<div class="seckill_dev" v-if="show == 3">抢购成功 <a href="/seckill/trade.html" target="_blank">去下单</a>
</div>
总结:
商品秒杀流程:
1.用户抢单的时候先会生成一个下单码,后面会先校验用户的下单码,只有正确获得下单码的请求才是合法请求,然后再校验状态位state,状态位是在内存中判断,效率极高,如果售罄,直接就返回了,不会给服务器造成太大压力,前面条件都成立,将秒杀用户加入队列,然后直接返回
2.监听队列,进行清单,首先判断产品状态位,我们前面不是已经判断过了吗?因为产品可能随时售罄,mq队列里面可能堆积了十万数据,但是已经售罄了,那么后续流程就没有必要再走了
然后判断用户是否已经下过订单,这个地方就是控制用户重复下单,同一个用户只能抢购一个下单资格,我们可以利用setnx控制用户,当用户第一次进来时,返回true,可以抢购,以后进入返回false
要控制库存数量,不能超卖,提供一种解决方案,那就我们在导入商品缓存数据时,同时将商品库存信息导入队列{list},利用redis队列的原子性,保证库存不超卖
然后将订单记录放入redis缓存,说明用户已经获得下单资格,秒杀成功,秒杀成功要更新库存(更新Mysql的库存和Redis中的库存)
3.前端页面轮训查询订单状态,判断用户是否抢单成功,下单了Redis生成临时订单数据(支付了会删除临时订单)抢单成功,去下单页面,判断用户是否下单查询总订单数据是否支付,下单了去我们订单页面,判断状态位,是否售罄
相关文章:
尚品汇-秒杀下单实现-页面轮询查询订单状态(五十三)
目录: (1)整合秒杀业务 (2)秒杀下单 (3)秒杀下单监听 (4)页面轮询接口 (1)整合秒杀业务 秒杀的主要目的就是获取一个下单资格,拥…...
2024年微电子与纳米技术国际研讨会(ICMN 2024) Microelectronics and Nanotechnology
文章目录 一、会议详情二、重要信息三、大会介绍四、出席嘉宾五、征稿主题六、咨询 一、会议详情 二、重要信息 大会官网:https://ais.cn/u/vEbMBz提交检索:EI Compendex、IEEE Xplore、Scopus大会时间:2024年9月20-22日地点:成都…...
2024最新版,人大赵鑫老师《大语言模型》新书pdf分享
本书主要面向希望系统学习大语言模型技术的读者,将重点突出核心概念与 算法,并且配以示例与代码(伪代码)帮助读者理解特定算法的实现逻辑。由于大语言模型技术的快速更迭,本书无法覆盖所有相关内容,旨在梳理…...
[Leetcode 543][Easy]-二叉树的直径-递归
目录 一、题目描述 二、整体思路 三、代码 一、题目描述 原题地址 二、整体思路 取一个结点的最大直径就是取一个结点的左子树最大深度右子树最大深度之和,因此可以定义一个递归函数,作用是取一个结点的最大直径。这个函数中还实现了求左子树最大深度…...
高级大数据开发学习路线指南
掌握大数据技术是一项系统性工程,涉及到广泛的技能和专业知识。为了帮助初学者构建坚实的基础,并逐步成长为大数据领域的专家,下面详细阐述了一条全面而深入的学习路线: 1. Java 编程基础 - 打造坚实的底层技能 关键知识点&…...
SpringBoot设置mysql的ssl连接
因工作需要,mysql连接需要开启ssl认证,本文主要讲述客户端如何配置ssl连接。 开发环境信息: SpringBoot: 2.0.5.RELEASE mysql-connector-java: 8.0.18 mysql version:8.0.18 一、检查服务端是否开启ssl认…...
2024-1.2.12-Android-Studio配置
本地博客: https://k1t0111.github.io/ K1T0 最近在做一些app方向的移动技术开发学习,但是由于AS的配置问题,市面上找不到最新的2024版本的AS的相关配置。笔者也是踩了很多坑,因此想写一篇文章记录一下最新的AS 2024 1.2.12的对应java环境的一…...
前端vue左侧树的一整套功能实现(一):vue2+vite封装v-resize指令,实现左侧树拖拽宽度和折叠展开
实现v-resize指令,具体以下功能: 指令接收宽度最大最小值,接收一个id用于localStorage存储拖拽宽度,接收padding拖拽时产生虚线拖拽,松开鼠标再进行元素宽度调整折叠展开图标使用本地图片 封装一个vite下使用本地图片…...
本地部署huggingface模型,建立自己的翻译应用
过去,我们使用翻译接口时,往往都是使用百度等的接口,每天有一定量的免费额度。今天为大家介绍一个可以进行翻译的模型,具备英译中、中译英的能力。并且在这个过程中,向大家介绍一个如何在本地部署模型。在之前的”五天…...
基于python+django+vue的在线学习资源推送系统
作者:计算机学姐 开发技术:SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等,“文末源码”。 专栏推荐:前后端分离项目源码、SpringBoot项目源码、SSM项目源码 系统展示 【2025最新】基于协同过滤pythondjangovue…...
.Net Gacutil工具(全局程序集缓存工具)使用教程
GAC介绍: GAC(Global Assembly Cache)全局程序集缓存,是用于存放.Net应用程序共享的程序集。 像平常我们在Visual Studio中引用系统程序集时,这些程序集便来自于GAC。 GAC默认位置为:%windir%\Microsoft…...
安卓13修改设置设备型号和设备名称分析与更改-android13设置设备型号和设备名称更改
总纲 android13 rom 开发总纲说明 文章目录 1.前言2.问题分析3.代码分析4.代码修改5.编译6.彩蛋1.前言 用户要定制一些系统显示的设备型号和设备名称,这就需要我们分析设置里面的相关信息来找到对应的位置进行修改了。 2.问题分析 像这种信息要么是config.xml里面写死了,要…...
AI健身体能测试之基于paddlehub实现引体向上计数个数统计
【引体向上计数】 本项目使用PaddleHub中的骨骼检测模型human_pose_estimation_resnet50_mpii,进行人体运动分析,实现对引体向上的自动计数。 1. 项目介绍 人体运动分析是近几年许多领域研究的热点问题。在学科的交叉研究上,人体运动分析涉…...
Redis常见报错及解决方法总结
Redis常见报错及解决方法总结 Redis作为高效的内存数据库,在实际使用过程中不可避免会遇到一些问题和报错。为了帮助大家更好地应对这些问题,我将常见的Redis报错及其解决方法进行总结,并提供具体的操作步骤。 1. Connection Refused 错误…...
【TabBar嵌套Navigation案例-JSON的简单使用 Objective-C语言】
一、JSON的简单使用 1.我们先来看一下示例程序里边,产品推荐页面, 在我们这个产品推荐页面里面, 它是一个CollectionViewController,注册的是一个xib的一个类型,xib显示这个cell,叫做item,然后,这个邮箱大师啊,包括这个图标,以及这些东西,都是从哪儿来的呢,都是从…...
通过鼠标移动来调整两个盒子的宽度(响应式)
DOM结构: <div class"courer"> // 外层盒子<div class"dividing-line" title"拖动"></div> // 拖动的那个线<div class"course-title-box"> // 第一个盒子<div class"course-content-…...
React Zustand状态管理库的使用
Zustand 是一个轻量级的状态管理库,适用于 React 和浏览器环境中的状态管理需求。它由 Vercel 开发并维护,旨在提供一种简单的方式来管理和共享状态。Zustand 的设计理念是尽可能简化状态管理,使其更加直观和易于使用。 Zustand 官网点击跳转…...
pyrosetta MoveMap介绍
在 PyRosetta 中,MoveMap 是一个非常重要的类,用来控制蛋白质分子中哪些部分可以在某些操作(如折叠、旋转、优化等)中被移动。MoveMap 允许你精确地指定哪些残基、键角或原子可以进行特定的运动,从而帮助你在蛋白质结构预测、优化和设计中进行灵活的控制。 MoveMap 的功能…...
在线安全干货|如何更改IP地址?
更改IP地址是一个常见的需求,无论是为了保护个人隐私、绕过地理限制还是进行商业数据分析。不同的IP更改方法适用于不同的需求和环境。但请注意,更改IP地址应在合法场景下进行,无论使用什么方法,都需要在符合当地网络安全法律法规…...
【C++】【网络】【Linux系统编程】单例模式,加锁封装TCP/IP协议套接字
目录 引言 获取套接字 绑定套接字 表明允许监听 单例模式设计 完整代码示例 个人主页:东洛的克莱斯韦克-CSDN博客 引言 有关套接字编程的细节和更多的系统调用课参考《UNIX环境高级编程》一书,可以在如下网站搜索电子版,该书在第16章详…...
[特殊字符] 智能合约中的数据是如何在区块链中保持一致的?
🧠 智能合约中的数据是如何在区块链中保持一致的? 为什么所有区块链节点都能得出相同结果?合约调用这么复杂,状态真能保持一致吗?本篇带你从底层视角理解“状态一致性”的真相。 一、智能合约的数据存储在哪里…...
Linux应用开发之网络套接字编程(实例篇)
服务端与客户端单连接 服务端代码 #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <pthread.h> …...
C++:std::is_convertible
C++标志库中提供is_convertible,可以测试一种类型是否可以转换为另一只类型: template <class From, class To> struct is_convertible; 使用举例: #include <iostream> #include <string>using namespace std;struct A { }; struct B : A { };int main…...
CMake基础:构建流程详解
目录 1.CMake构建过程的基本流程 2.CMake构建的具体步骤 2.1.创建构建目录 2.2.使用 CMake 生成构建文件 2.3.编译和构建 2.4.清理构建文件 2.5.重新配置和构建 3.跨平台构建示例 4.工具链与交叉编译 5.CMake构建后的项目结构解析 5.1.CMake构建后的目录结构 5.2.构…...
Leetcode 3577. Count the Number of Computer Unlocking Permutations
Leetcode 3577. Count the Number of Computer Unlocking Permutations 1. 解题思路2. 代码实现 题目链接:3577. Count the Number of Computer Unlocking Permutations 1. 解题思路 这一题其实就是一个脑筋急转弯,要想要能够将所有的电脑解锁&#x…...
从零实现STL哈希容器:unordered_map/unordered_set封装详解
本篇文章是对C学习的STL哈希容器自主实现部分的学习分享 希望也能为你带来些帮助~ 那咱们废话不多说,直接开始吧! 一、源码结构分析 1. SGISTL30实现剖析 // hash_set核心结构 template <class Value, class HashFcn, ...> class hash_set {ty…...
Spring Boot+Neo4j知识图谱实战:3步搭建智能关系网络!
一、引言 在数据驱动的背景下,知识图谱凭借其高效的信息组织能力,正逐步成为各行业应用的关键技术。本文聚焦 Spring Boot与Neo4j图数据库的技术结合,探讨知识图谱开发的实现细节,帮助读者掌握该技术栈在实际项目中的落地方法。 …...
Java求职者面试指南:Spring、Spring Boot、MyBatis框架与计算机基础问题解析
Java求职者面试指南:Spring、Spring Boot、MyBatis框架与计算机基础问题解析 一、第一轮提问(基础概念问题) 1. 请解释Spring框架的核心容器是什么?它在Spring中起到什么作用? Spring框架的核心容器是IoC容器&#…...
Go 并发编程基础:通道(Channel)的使用
在 Go 中,Channel 是 Goroutine 之间通信的核心机制。它提供了一个线程安全的通信方式,用于在多个 Goroutine 之间传递数据,从而实现高效的并发编程。 本章将介绍 Channel 的基本概念、用法、缓冲、关闭机制以及 select 的使用。 一、Channel…...
腾讯云V3签名
想要接入腾讯云的Api,必然先按其文档计算出所要求的签名。 之前也调用过腾讯云的接口,但总是卡在签名这一步,最后放弃选择SDK,这次终于自己代码实现。 可能腾讯云翻新了接口文档,现在阅读起来,清晰了很多&…...

