一、项目架构
1. 架构图
2. 用自己的话描述项目架构
商城项目一般包括用户注册、商品浏览、购物车、订单管理、支付结算等模块,其架构设计需要考虑以下几个方面:
架构要素 | 具体内容 |
---|---|
前端设计 | 商城项目前端设计需要考虑用户体验、交互设计、UI设计等方面,常用的技术包括HTML、CSS、JavaScript、Vue、React等 |
后端设计 | 商城项目后端设计需要考虑系统架构、数据存储、业务逻辑处理等方面,常用的技术包括Java、Spring、Spring Boot、MyBatis、MySQL等 |
架构设计 | 商城项目的架构设计需要考虑高可用、高性能、高并发、安全等方面,一般采用分布式架构和微服务架构,包括前端和后端的架构设计 |
前端架构设计 | 前端架构设计需要考虑前端静态资源的访问速度、请求负载均衡、缓存策略等方面,一般采用CDN技术、反向代理、缓存技术等 |
后端架构设计 | 后端架构设计需要考虑系统的高可用性、高性能、高并发等方面,一般采用分布式架构和微服务架构。 |
数据库设计 | 商城项目的数据库设计需要考虑数据的一致性、高可用性、可扩展性等方面,一般采用主从复制、分库分表等技术。常用的数据库包括MySQL、MongoDB、Redis等 |
安全设计 | 商城项目的安全设计需要考虑用户信息的保密性、系统的可靠性和防止攻击等方面,一般采用SSL/TLS、OAuth、JWT等安全技术 |
运维设计 | 商城项目的运维设计需要考虑系统的可维护性、监控、报警等方面,一般采用容器化技术、自动化部署、日志分析等技术。常用的工具包括Docker、Kubernetes、ELK等 |
二、后台模块:商品管理
1. 基本概念
① spu: 标准化产品单元 不是一件具体的商品 eg iphone14
② sku: 库存量单元 指的就是一件具体的商品 eg iphone14 128G 蓝色
③ 销售属性
出现了商品详情页右侧的商品属性信息
④ 平台属性
出现了商品详情页下方的属性信息
⑤ 图片处理方式
采用oss远程存储方案
数据库保存的是图片的地址
2. 业务话术样板
我们这个商城平台是不支持商家入驻功能的,因此整个商品管理主要是我们这个商城后台系统的一个核心功能模块。主要的功能分为:
1、 对于商品品牌、商品分类、销售属性和平台属性进行管理的一些操作;
2、 对商品有对应操作权限的业务员对于商品的添加、上下架、修改、删除、批量操作、模糊查询等功能。
针对商品这个模块来说,因为不同的分类对应不同的平台属性,平台属性和SKU进行关联,用于后期商品搜索功能实现,可以通过平台属性查询出所选属性下的所有SKU信息;
同时,一个分类下又有多个品牌,品牌下对应多个SPU商品,每一个SPU下有多种销售属性,我们通过商品SPU的不同销售属性组合生成多个SKU。例如 iPhone14是一个SPU,由于iPhone14分内存、颜色等销售属性,内存有64G、128G和256G,颜色有红色、金色和黑色,这样每种不同销售属性组合就是一个新的SKU,64G+黑色、64G+金色、64G+红色,128G+金色…….等。
1、先判断是否有对应的商品添加的权限,如果没有,直接不显示操作按钮;
2、如果有,先选择商品所属的分类,然后商品分类选择之后会对应的查询出所有平台属性和所关联的所有品牌;
3、然后添加产品SPU的一些基本信息(SPU名称、标题、商城价格等);
4、其次,上传产品SPU对应的图片,这块因为整个电商平台中需要保存大量的图片,我们采用了OSS存储服务
5、在SPU列表中有添加sku的按钮,点击后可以添加给SPU添加SKU。进入SKU添加页面时会查出当前SPU的所有销售属性和图片,然后添加SKU信息,选择对应的销售属性,选择对应的图片。
6、调用后台的商品添加接口/服务,然后对应的接口/服务中生成商品添加时的一些默认数据(添加时间、更新时间、操作人等),在商品添加的时候我们默认是下架状态。需要拥有审核功能的业务员针对商品中是否有一些不合法的数据进行审核。
商品上下架
因为商品添加之后,默认是下架状态,如果商品想处于销售状态,必须将商品的状态改为上架状态。
在SKU列表页面中有上、下架操作按钮:
上架时业务:
1、 将SKU信息添加Redis进行缓存
2、 将SKU信息添加到ES一份以供搜索
3、修改上架状态 0未上架 1已上架 2已下架
下架操作:和上架相反
3. 商品模块表关系
三、首页+商品详情
1. 需求分析
① 页面格式相对固定
可以采用页面静态化技术 Thymeleaf
② 会被用户高频访问
预示着读多写少,可以使用缓存技术
③ 数据来源的稳定性
结合页面,商品详情页需要的数据有:
sku的基本信息
sku的所有图片
sku的所有促销信息
sku的所有销售组合
spu的描述信息
spu的所有基本规格分组及规格参数
营销系统中的优惠信息
库存系统中库存信息
金融服务相关系统
① 高并发 短时间内会有大量的请求访问商品详情页或者首页
② 高可用:
- 图片要正常显示
oss 赠送 CDN(内容分发网络) 简称图片/视频/资源加速器
- 页面格式相对稳定
使用页面静态化技术 thymeleaf
- 页面中的数据需要很多个接口提供数据
异步编排
- 数据变化也不大
意味着读多写少,使用redis缓存
商品首页三级分类和商品详情页适用场景不同:
缓存:比较适合数据规模相对较小,并发相对比较频繁的场景。例如:首页三级分类、库存等。
页面静态化:比较适合大规模且相对变化不太频繁的数据。例如:商品详情页、秒杀。
商品三级分类:分布式锁+自定义AOP注解和切面类,其中切面类中使用redis缓存,布隆过滤器。
① 首页的访问量非常大,而首页中的商品类目访问量更大,鼠标移动就在访问,查询所有的数据,如果每次访问都实时到数据库获取数据,数据库的访问压力太大。虽然只是一个查询操作,但是由于频繁的访问所以我们必须对其性能进行最大程度的优化。
② 所以,比如我们一级类目查询下级目录数据放入Redis缓存来提高性能,这里我们为了降级业务之间耦合度,提高程序的可用性,采用优雅的自定义AOP注解和切面类来实现缓存,主功能代码不做修改,只是根据父id查询商品分类列表数据。
③ 在自定义注解中设置以下参数:缓存前缀名,缓存的过期时间(防止死锁),锁的key值,防止雪崩的随机时间值。
④ 在自定义切面类中,首先查询缓存,如果缓存命中则直接返回;未命中,为了防止缓存穿透,添加了布隆过滤器判断是否存在商品id;不存在一定不存在返回null即可。
⑤ 存在,则数据库可能有数据,添加商品自身分布式锁防止缓存击穿;
⑥ 获取锁的过程中,可能有其他请求已经把数据放入缓存,所以要再次查询缓存,如果命中则直接返回;
⑦ 未命中,查询数据库后放入缓存,为了防止缓存雪崩,给缓存时间添加了随机值。
商品详情:异步编排+页面静态化
① 由于商品详情展示的数据需要要调用多个查询接口,为了提高响应速度,我们采用异步编排+自定义线程池的方式进行接口调用。
② 目前,静态化页面都是通过模板引擎来生成,而后保存到nginx服务器来部署。
③ 项目中商品详情读多写少,而咱们使用的就是thymeleaf,只要引入thymeleaf启动器,springboot就会初始化TemplateEngine对象。TemplateEngine就是thymeleaf页面静态化的模板引擎类,使用方式如下:
@Autowired private TemplateEngine templateEngine; templateEngine.process(String template, IContext context, Writer writer);
2. 业务流程图(图有问题,辅助看一下即可)
四、商城总结-单点登录
1. 概念
单点登录 (SSO) 是一种身份验证功能,允许用户使用一组登录凭据访问多个应用程序。企业通常利用 SSO 来更加简单便捷地访问各种网络、内部和云应用,从而获得更好的用户体验。同时,SSO 还能增强 IT 对用户访问的控制,减少密码相关的服务台呼叫,并改善安全性和合规性
2. 两个流程:登录流程-鉴权流程
① 单点登录-注册流程
加盐加密过程 平台不会直接存储用户的明文密码
// 2.生成盐 String salt = StringUtils.substring(UUID.randomUUID().toString(), 0, 6); userEntity.setSalt(salt); // 3.加盐加密 userEntity.setPassword(DigestUtils.md5Hex(userEntity.getPassword() + salt)); // 4.新增用户 userEntity.setLevelId(1l); userEntity.setNickname(userEntity.getUsername()); userEntity.setSourceType(1); userEntity.setIntegration(1000); userEntity.setGrowth(1000); userEntity.setStatus(1); userEntity.setCreateTime(new Date()); this.save(userEntity);
② 单点登录-登录流程
登录并生成token的过程
① 用户提交用户名和密码等登录信息
② 后台获取用户信息并从用户表查询该用户信息③ 如果不存在,提示用户名或者密码错误后者是用户不存在请去注册等错误信息
④ 如果用户存在,借助于jwt工具类将用户id 用户名 ip 作为有效载荷 生成token
补充:jwt json web token
- 头
- 载荷
- 签名
⑤ 将该token携带到cookie中保存到浏览器
⑥ 登录成功重定向到登录成功的页面
使用的 springcloud-gateway 的局部过滤器
① 获取用户的请求路径并判断是否在预先设置的拦截名单中
② 如果不在拦截名单,直接放行
③ 如果在拦截名单,说明要访问的页面需要校验用户身份的页面
④ 分别从请求头或者请求体中获取token
- 如果token存在于请求头中的话,我们使用reqeust.getHeaders.getFirst("token")
- 如果token存在于请求体中的话,我们使用reqeust.getCookies[i].getValue()
⑤ 判断token是否为空 说明token不存在或者刚好过期,重定向登录页面重新生成token
⑥ 如果token不为空,使用预先准备好的公钥进行解析token,期间如果解析出现错误,也会重新登录
⑦ 解析token之后,如果IP地址比对成功,放行到后续的登陆之后的业务中
⑧ 否则,意味着IP地址发生了变化,需要重新登录,重新生成新的token
3. 模块相关问题
① cookie被禁用了能登录吗?怎么解决的?
不能登录,因为token存在cookie中,解决办法是参考京东的给用户提示:请您启用浏览器Cookie功能或更换浏览器
② 怎么防止cookie盗用(盗链)?
采用https进行加密传输,或者在token中加入以用户访问ip作为秘钥,解密token比对用户访问ip地址,ip不匹配,重新登录。
五、商城总结-购物车
1. 需求分析
1.1. 商城购物车
网上商店所说的购物车是对现实的购物车而喻,买家可以像在超市里购物一样,随意添加、删除商品,选购完毕后,统一下单。
网上商店的购物车要能跟踪顾客所选的的商品,记录下所选商品,还要能随时更新,可以支付购买,能给顾客提供很大的方便。
1.2. 功能要求
用户可以在登录状态下将商品添加到购物车
用户可以在未登录状态下将商品添加到购物车
用户可以使用购物车一起结算下单(批量下单)
用户可以查询自己的购物车
用户可以在购物车中修改购买商品的数量。
用户可以在购物车中删除商品。
在购物车中展示商品优惠信息
提示购物车商品价格变化
1.3. 对象字段
{
id: 1,
userId: '2',
skuId: 2131241,
check: true, // 选中状态
title: "Apple iphone.....",
image: "...",
price: 4999,
count: 1,
store: true, // 是否有货
saleAttrs: [{..},{..}], // 销售属性
sales: [{..},{..}] // 营销信息
}
1.4. 数据库表结构
CREATE TABLE `cart_info` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` varchar(30) NOT NULL COMMENT '用户id或者userKey',
`sku_id` bigint(20) NOT NULL COMMENT 'skuId',
`check` tinyint(4) NOT NULL COMMENT '选中状态',
`title` varchar(255) NOT NULL COMMENT '标题',
`default_image` varchar(255) DEFAULT NULL COMMENT '默认图片',
`price` decimal(18,2) NOT NULL COMMENT '加入购物车时价格',
`count` int(11) NOT NULL COMMENT '数量',
`store` tinyint(4) NOT NULL COMMENT '是否有货',
`sale_attrs` varchar(100) DEFAULT NULL COMMENT '销售属性(json格式)',
`sales` varchar(255) DEFAULT NULL COMMENT '营销信息(json格式)',
PRIMARY KEY (`id`),
KEY `idx_uid_sid` (`user_id`,`sku_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
1.5. 购物车的逻辑结构
我们的购物车结构是一个双层Map:Map<String,Map<String,String>>
redis中购物车hash数据模型 Map<userId/userKey, Map<skuId, CartJson>>
第一层Map,Key是用户id
第二层Map,Key是购物车中商品id,值是购物车数据
2. 设计和业务流程
登录校验:购物车的处理方式与用户的状态有关,因此需要对用户状态进行校验,项目中我们使用拦截器统一处理。
首先请求到达购物车服务时会先被handlerInterceptor拦截器前置方法拦截下来,从请求中获取token信息,如果token为空,则会生成一个UUID作为用户临时ID保存到userInfo中,如果token不为空则使用公钥解析token获取到用户真实的userID,同样也会保存到userinfo中去,将userinfo放入threadLocal中,以便后续服务直接获取当前用户信息。
该模块以redis作为主库+mysql作为辅库来存储数据,因为该业务场景为读多写也多的情况,使用redis性能较好,但是做数据分析不太方便,所有考虑使用redis(主库 同步 增删改查) + mysql(辅库 异步 只做数据分析)
新增商品:判断是否登录
- 是:则添加商品到后台Redis+mysql中,把user的唯一标识符作为key。
- 否:则添加商品到后台Redis+mysql中,使用随机生成的user-key作为key。
查询购物车列表:判断是否登录
- 否:直接根据user-key查询redis中数据并展示
- 是:已登录,则需要先根据user-key查询redis是否有数据。
否:直接去后台查询redis,而后返回。
有:需要先合并数据(redis + mysql),而后查询。
① 校验用户状态流程
获取登录头信息 userKey,并判断是否为空;
- 若userKey为空,则制作userKey 后存入cookie并存入UserInfo;
- 若userKey不为空,解析jwt将UserId存入用户实体类UserInfo;
将UserInfo存入线程的局部变量中,放行(放入线程池是解决openfeign远程调用导致请求头丢失问题)。
redis中购物车hash数据模型 Map<userId/userKey, Map<skuId, CartJson>>
1. 获取登录信息,并判断是否登录;
- 是登录状态,则购物车key为userId;
- 未登录状态,则购物车key为userKey;
2. 根据key值从redis中获取用户的购物车 map<skuId,cartJson>;
3. 判断当前用户购物车是否包含该商品;
- 若包含则商品num+1,并入库;
- 若不包含,则根据skuId查询该商品是否存在,不存在就返回不存在;存在则根据skuId查询销售属性、营销信息、库存信息;
4. 然后新增购物记录,这里会对mysql中的数据做异步保存处理,(还会在redis中新增string类型的map<skuid, sku_price>信息存入redis,用来做价格对比的,防止用户将商品加入购物车后,商品价格发生变化导致购物车价格和商品实际价格不一致的问题。)
获取userKey作为redis中key值查询未登录状态的购物车,并判断是否为空;
- 若未登录状态购物车为空,获取userId判断是否为空,若为空,返回未登录购物车;不为空,以userId作为key,查询登录的购物车;
- 若未登录状态购物车不为空,获取未登录状态购物车数据并查询实时价格缓存;
合并登录状态和未登录状态购物车,判断是否包含相同的商品;
- 包含相同商品,则数量叠加并更新到数据库;
- 不包含相同商品,则新增购物车记录并入库;
删除未登录购物车数据,返回已经登录购物车列表(在返回前需要到redis中查询一下sku最新价格,以便前端展示该商品的降价信息。);
④ 更新购物车
- 获取登录状态:已登录-userId 未登录-userKey
- 根据skuid获取购物车对象,更新数量或者状态即可
⑤ 删除购物车
- 获取登录状态:已登录-userId 未登录-userKey
- 根据skuid删除购物车中的记录
3. 相关问题
① 购物车sku最新价格如何同步的?
在后台管理模块中,修改spu属性时,发送消息到MQ携带spu_id,通知购物车服务消费该消息,购物车监听器监听到消息后,根据spuid查询响应的sku,然后遍历修改redis中sku的价格,实现购物车实时价格同步。
② redis宕机了数据会不会丢失?
redis持久化(AOF + RDB持久化) redis集群
依然可能会丢失,因为IO需要时间,但是概率极低,即使失败用户可以再次添加购物车即可,因为购物车的可靠性要求不高
③ 如果mysql写失败了会不会影响下单?
不会,下单是以redis中的购物车为基准
④ redis是内存型的时间久了,会不会存放不下那?
随着时间的推移,购物车的数据量不会增长,只会随着活跃用户的增长而增长。在可预见的未来几年是可以存放下的
⑤ 购物车数据量有多大?多少用户的购物车?(几十万活跃用户)大小是多少?
(10G)
⑥ 针对一些临时用户的购物车,长期不再访问的购物车数据,怎么删除?
淘汰策略
⑦ 价格问题(如果价格更新了,顾客买了更贵的东西,这种情况是怎么处理的)?
使用redis缓存 + MQ的最终一致性实现的。
1.加入购物车时,同时加入购物车数据 和 实时价格缓存
2.查询购物车时,同时查询购物车数据 和 实时价格缓存
3.pms中修改了价格,发送消息给MQ,在Cart服务中获取消息同步价格缓存
用redis缓存将skuid和价格存储到redis中,如果有对sku修改的操作,则发送消息给MQ,异步通知购物车服务更新redis价格缓存数据,在给前端数据时,会携带则用户加入购物车时的数据和sku当前的实时数据。
⑧ 异步执行保存mysql数据时如果出现异常,导致数据不同步怎么办?
自定义异步未捕获异常处理器,实现AsyncUncauchExceptionHandler接口重写handlerUncauchException方法,在异步任务执行失败的时候会调用该方法,在该方法中将购物车数据同步异常的userId作为value保存到redis中,数据类型为:set<cart:exception,userId>,然后使用xxl_job通过定时任务读取异常信息,把异常的购物车数据从redis中同步到mysql数据库。
⑨ 相关编码方法
分布式定时任务的特征:时间驱动 异步执行 批量处理
redisTemplate.boundSetOps(EXCEPTION_KEY)
setOps.add(params[0].toString()) 想set集合中添加一个值
userId = setOps.pop() 从set集合中随机获取一个值,并删除
@XxlJob("cartSyncHandler") 定时任务方法
Cron表达式:编写定时任务什么时候执行。
XXL-job框架相关的看一下了解就行。
六、商城总结-订单模块
1.业务分析
订单业务在整个电商平台中处于核心位置,也是比较复杂的一块业务。是把“物”变为“钱”的一个中转站。整个订单模块一共分两部分组成:
入口:购物车点击计算按钮
1.1. 订单确认
收货人信息有更多地址,即有多个收货地址,其中有一个默认收货地址
支付方式:货到付款、在线支付,不需要后台提供
送货清单:配送方式(不做)及商品列表(根据购物车选中的skuId到数据库中查询)
发票:不做(获取某一个公司的税号+总价+调用具体的发票打印机)
优惠:查询用户领取的优惠券(不做)及可用积分(京豆)
1.2. 提交订单
- 防重校验
- 验总价、校验每一个商品的价格(获取页面上的总价格 和 数据库的实时总价格比较,不一致提示页面过期),该步骤为读取数据,很少出现事务问题。
- 验库存并锁库存(保证原子性:分布式锁)
- 创建订单(调用订单创建接口)
- 异步删除购物车中redis和mysql中的记录
1.3. 对接支付服务
三方支付
1.4. 库存管理系统
由于涉及多个系统之间的数据数据和传递,我们系统采用消息队列实现数据的最终一致性
1.5. 支付成功后
分布式事务修改订单状态
2. 业务流程图和话术模板
1.在订单服务中创建了自定义拦截器实现HandlerInterceptor,从请求头中获取userId和userName并放入局部线程变量并提供getUserInfo方法(这里因为openfeign远程调用会导致请求头信息丢失,所以提前将请求头信息放入局部线程变量中)
2.生成订单结算页:
- 点击购物车中结算按钮跳转到订单页生成订单结算页;
- 从拦截器中获取userId,调用ums、cart、pms、wms、sms根据userId分别查询用户收货地址、选中的购物车记录、商品详情信息、用户购物积分给前端业面展示;
- 使用雪花算法生成ordeerToken作为分布式防重唯一标识,存入数据库并设置过期时间。
3. 提交订单:五步骤
① 防重校验:
- 获取页面中的orderToken,如果orderToken为空,则提示用户非法请求;
- 如果不为空,则使用lua脚本保证操作原子性 删除redis中缓存的orderToken(如果ordertoken存在则表示用户没有提交过订单,不存在则表示用户提交了订单);
- 如果执行删除操作返回的是false(成功返回1,失败返回0),说明orderToken不存在,则提示用户不要重复提交订单。
② 验总价:
- 获取页面上的总价格和数据库的实时总价格比较,不一致提示页面过期。
③ 验库存并锁库存(保证原子性:分布式锁):
- 远程调用库存服务中的锁库存接口,会返回具体的这个商品锁定了多少件库存,只要有一个商品库存不足则无法继续提交;
- 如果存在锁定失败的商品,则需要解锁已经锁定成功的商品库存;
- 一旦锁定成功,缓存锁定信息,以便将来减库存或者解锁库存;
- 为了防止锁库存成功没来得及下单导致的锁死状况的发生,mq发送延时消息,定时解锁库存。
④ 创建订单:
- 根据userId和订单页信息生成一条订单记录保存在数据库中,使用rabbimq发送定时关单;
- 若创建订单出现异常失败,需要mq发送消息解锁库存。
⑤ 异步删除购物车中对应的记录:
- mq发消息到cart购物车服务,cart服务收到消息删除购物车(订单服务通过cart:delete通过交换机路由到cart监听的队列,消息内容Map<userId,skuIds> msg),cart通过消息(消息为空直接确认消息)中的userId和skuIds删除对应用户的购物车中的商品记录。
分布式事务问题:库存锁定成功,订单创建失败就会出现分布式事务问题,失败则发消息给oms将订单标记为无效订单 (this.rabbitTemplate.convertAndSend("order.exchange", "order.fail", orderToken);将orderToken对应的订单status从0->5)同时给wms解锁库存(根据orderToken查redis中的锁定信息,根据skuId和count将库存加回来)并删除缓存中的库存锁定信息
⑥ 定时关单并解锁:创建订单成功的情况下,发送消息给交换机到延时队列(ttl=24h),时间到就通过死信交换机将消息发送到死信队列,关单是判断status是否为待支付状态,待支付状态执行关单0待支付4已关闭0->4(order:order.ttl->order.exchange:order.ttl->ttlQueue:order.dead->order.exchange:order.dead->deadQueue),修改订单状态成功发消息给wms(rk:stock.unlock)解锁库存
七、商城总结-支付模块
1. 4把秘钥
采用的加密方式为RSA:非对称加密。加密和解密用不同的钥匙。
此加密算法需要四把钥匙。
① 2把商户
利用支付宝的秘钥生成工具,生成商户钥匙 第一把为:商户私钥;第二把为商户公钥,需要提交给支付宝。
② 2把支付宝
第一把:商户上传了商户公钥,就可以换取到支付宝公钥,存放到配置文件当中
第二把:不知道
2. 支付流程图
数据传输的流程:验签 使用alipay-sdk 的 AlipayClient对象
1. 准备订单数据:订单号,订单金额
2. 使用AlipayClient给支付宝发送获取支付页请求。网络的数据,会利用商户的私钥进行加密传给支付宝。
3. 数据传给支付宝,支付宝用我们上传的应用的公钥对数据进行验签(核验数据是否正确);
4. 支付宝验签通过以后才会给我们一个页面。[验签失败,非法参数]
5. 我们收到的页面其实是一个能自动提交的表单,表单自动提交给支付宝的支付网关 登录支付宝付款页
6. 这个数据提交给支付宝以后,支付宝会验签。如果通过了。展示扫码的支付页
7. 支付成功
同步跳转:return_url支付成功之后浏览器会自动跳转到我们return_url指定的位置。
异步回调:支付成功以后,支付宝会给我们notify_url指定的位置发送请求。一定要是公网可以访问的。异步通知回来一定要验签。notify_url:自己写的一个controller,支付宝会异步的通知我们。只要controller执行了订单的状态就改为已支付。给支付宝返回”success”;频繁的通过异步的方式通知支付宝
3. 支付宝自动收单
业务场景:
订单支付时间正好处在订单即将失效的边缘,当用户支付成功之后,由于网络延迟,关单服务没有及时关单,但是支付宝的响应速度快速修改订单状态为已支付,此时关单服务才开始执行,将订单改为已关闭,此类问题与我们系统设计无关,因为我们系统关单操作是幂等操作
在支付宝支付页面停留太长时间,如何处理?
设置支付时间与订单失效相同,也即设置订单的绝对过期时间
同步到支付宝,让支付宝收单;告诉支付宝此订单号交易关闭;参考链接小程序文档 - 支付宝文档中心img