travel-逸游天下

travel-逸游天下

———-第二部分《DDD – 设计表》———

采用领域驱动模型对表进行重新的设计。

2.1 设计步骤:

  • 1、描述 业务(描述项目的背景)

  • 2、统一语言 (对于关键词,像SPU、SKU 进行统一)

  • 3、价值需求分析 (这个项目的前景、针对人群,为什么要做)

  • 4、业务需求分析 ()

  • 5、识别限界上下文及其映射 (模块-微服务- 限界上下文、 调用关系 - 映射)

  • 6、领域分析模型(领域对象)

  • 7、领域设计聚合 (聚合的设计)

  • 8、服务设计 (领域服务 和 应用服务)
    根据上下文,进行领域服务的设计。
    限界上下文 -》 应用服务 -〉 领域服务 -》数据库操作(基础设施层)

  • 9、领域实现 建模

    • 面向服务行为,比如基于 RPC,称为提供者(Provider);
    • 面向服务资源,比如基于 REST,称为资源(Resource);
    • 面向事件,比如基于消息中间件,称为订阅者(Subscriber);
    • 面向视图模型,比如基于 MVC的page,称为控制器(Controller);

    测试驱动开发 :要求开发者在进行逻辑实现前,优先进行测试用例的编写,站在调用者角度而非实现者角度去思考接口。

  • 10、分层架构

单个模块(简单)版本:

image-20230917070322897

10、代码骨架

用户接口层

用户接口层的核心职能:协议转换和适配、鉴权、参数校验和异常处理。

1
2
3
4
5
6
7
8
9
10
11
├── controller                        //面向视图模型&资源
│ ├── ResultController.java
│ ├── assembler // 装配器,将VO转换为DTO
│ │ └── ResultAssembler.java
│ └── vo // VO(View Object)对象
│ ├── EnterResultRequest.java
│ └── ResponseVO.java
├── provider // 面向服务行为,比如dubbo RPC
├── subscriber // 面向事件 ,比如 rocketmq的客户端程序
└── task // 面向策略 ,比如 xxl-job的调度任务
└── TotalResultTask.java

应用层

应用层的核心职能:编排领域服务、事务管理、发布应用事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
├── assembler                         // 装配器,将DTO转换为DO
│ ├── ResultAssembler.java
│ └── TotalResultAssembler.java
├── dto // DTO(Data Transfer Object)对象
│ ├── cmd // 命令相关的DTO对象
│ │ ├── ComputeTotalResultCmd.java
│ │ ├── EnterResultCmd.java
│ │ └── ModifyResultCmd.java
│ ├── event // 应用事件相关的DTO对象, subscriber负责接收
│ └── qry // 查询相关的DTO对象
└── service // 应用服务
├── ResultApplicationService.java
├── event // 应用事件,用于发布
└── adapter // 防腐层适配器接口

领域层

代码组织以聚合为基本单元。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
├── result                            // 成绩聚合
│ ├── entity // 成绩聚合内的实体
│ │ └── Result.java
│ ├── service // 领域服务
│ │ ├── ResultDomainService.java
│ │ ├── event // 领域事件
│ │ ├── adapter // 防腐层适配器接口
│ │ ├── factory // 工厂
│ │ └── repository // 资源库
│ │ └── ResultRepository.java
│ └── valueobject // 成绩聚合的值对象
│ ├── GPA.java
│ ├── ResultUK.java
│ ├── SchoolYear.java
│ └── Semester.java
└── totalresult // 总成绩聚合
├── ... 这段有点长,其代码结构与成绩聚合一致,因此省略 ...

1234567891011121314151617

基础设施实现层

该层主要提供领域层接口(资源库、防腐层接口)和应用层接口(防腐层接口)的实现。

代码组织基本以聚合为基本单元。对于应用层的防腐层接口,则直接以 application 作为包名组织。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
├── application                       // 应用层相关实现
│ └── adapter // 防腐层适配器接口实现
│ ├── facade // 外观接口
│ └── translator // 转换器,DO -> DTO
├── result // 成绩聚合相关实现
│ ├── adapter
│ │ ├── facade
│ │ └── translator
│ └── repository // 成绩聚合资源库接口实现
│ └── ResultRepositoryImpl.java
└── totalresult // 总成绩聚合相关实现
├── adapter
│ ├── CourseAdapterImpl.java
│ ├── facade
│ └── translator
└── repository
└── TotalResultRepositoryImpl.java

工厂:

工厂(Factory)
工厂用来封装创建一个复杂对象尤其是聚合时所需的知识,作用是将创建对象的细节隐藏起来。

客户传递给工厂一些简单的参数,然后工厂可以在内部创建出一个复杂的领域对象然后返回给客户。

工厂(Factory)不是必须的,只有当创建实体和值对象复杂时,建议使用工厂模式。

COLA

层次 包名 功能 必选 DDD
Adapter层 web 处理页面请求的Controller
Adapter层 wireless 处理无线端的适配
Adapter层 wap 处理wap端的适配
App层 executor 处理request,包括command和query 应用服务
App层 consumer 处理外部message 应用服务
App层 scheduler 处理定时任务 应用服务
Domain层 model 领域模型 Domain
Domain层 ability 领域能力,包括DomainService DomainServic、Do、EventService
Domain层 gateway 领域网关,解耦利器 Gateway
Infra层 gatewayimpl 网关实现 领域层实现
Infra层 mapper ibatis数据库映射
Infra层 config 配置信息
Client SDK api 服务对外透出的API
Client SDK dto 服务对外的DTO

image.png

image.png

COLA:

  • 将应用层的 DTO 类 抽取到– 》CLient
    DTO == Event、cmd、query
  • 将用户接口层的controller 抽取到 防腐层(适配器)==》web、mobile 提供 网关多个接口适配
  • client 提供统一 用户接口的接口
  • APP 应用层负责

用户接口层的 task、subscriber(consumer)转移到了 APP

COLA问题:

  • 缺少 VO -》 DTO
  • DTO -> VO
  • 如果需要可以,进行一次提取
  • 应该不是问题

RPC接口 在用户接口层,转到 domain 领域服务内,== 因为只有领域服务需要依赖于其他的领域服务

COLA :

  • 适配器层:适配多个客户端
  • app层 : 实现 cmd查询、query、event事件处理、普通的controller用的service、

xtoon-boot

xtoon-boot-ddd: 基于DDD(领域驱动设计)并支持SaaS平台的单体应用开发框架 (gitee.com)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
xtoon-boot
├─db 数据库SQL脚本

├─xtoon-common 公共模块
│ │
│ └─java
│ ├─domain 领域通用类
│ └─util 工具类

├─xtoon-api 接口模块
│ │
│ ├─web
│ │ ├─common 接口通用类
│ │ ├─util 接口工具类
│ │ └─controller controller类
│ └─resources
│ ├─static.swagger swagger文件
│ ├─application.yml 全局配置文件
│ └─logback-spring.xml 日志配置文件

├─xtoon-sys 系统管理子域
│ │
│ └─java
│ ├─application 应用层
│ │ ├─assembler DTO转换类
│ │ ├─command 命令入参
│ │ ├─dto DTO
│ │ └─impl 应用接口实现
│ ├─domain 领域层(核心)
│ │ ├─model 领域模型
│ │ ├─service 领域服务
│ │ ├─specification 规格校验
│ │ └─external 外部接口(防腐层)
│ └─infrastructure 基础设施层
│ ├─persistence 持久化类
│ └─external 外部服务类

├─xtoon-org 组织管理子域

示例SMS 的限界上下文可划分为:

成绩上下文
课程上下文
审批上下文
权限上下文
邮件上下文
上下文及其映射是啥呢?

咱们都是开发,按照开发的概念来理解吧。

假设一个界限上下文,对应一个微服务。

那么上面就是 5个微服务,微服务之间是 rpc 调用。这种服务的发布,和调用,就是 上下文的映射。

img

2.1.2 项目骨架

自底向上,部分模块需要按照聚合进行划分(聚合)

1
2
3
infrastructure : 
- gatewayimpl : 包含了domain 领域层的database \ rpc \ gatewayimpl

Adapter:外部API网关

Client : DTO 和 应用层接口

APP : 应用层服务 、task 任务服务、事件监听服务

Domain:领域模型

Infrastructure:common

2.2 逸游天下-DDD

1、逸游天下问题空间描述

逸游天下旨在让用户 能够足不出户就能提前知晓旅游风景,购买地方特色商品,了解人文特色文化,制定完美的,

符合自己喜好的旅游攻略。

2、统一语言

英语 中文
SPU - standard product unit 标准商品单元 - 标识商品信息(苹果11)
SKU - stock keeping unit 库存存储单元 – 库存存储(红色、12g、512g 苹果11)
Product 商品
Stock 库存
Seckill 秒杀
订单模块
OrderSn 订单号
return_apply 退货请求
优惠模块
coupon 优惠
(开发专业名词)
Authentication 认证 / 验证
Authorization 授权
Event 事件
Publisher 发布者
Consumer / Listerner 消费者 / 监听者
Feigin 远程调用
Gateway 网关

3、业务需求分析

img

为了打造一个根据 能够根据地点信息 推荐旅游攻略文章、购买地方特色商品的 平台。

使用业务流程、业务场景、业务服务业务规则来表示业务需求。

主要分为两个模块:

文章业务 - 核心域

  • 文章模块:能够阅读、点赞、收藏、编写、修改文章
    浏览记录
1
2
3
4
5
6
7
8
9
细说(包含对象、行为、结果):
(用户 -》 文章)
作者能够 编写文章、发布文章、发布草稿、修改已发布文章
读者能够 浏览文章、浏览(区域)热门文章、收藏文章、点赞文章
用户 评论文章
用户 回复文章

(文章)
文章 需要浏览量、点赞量、收藏量,综合评定三者关系,推荐热门文章。

商品业务 - 核心域

商品模块:商品管理、购物车管理、商品检索
浏览记录

1
2
3
4
5
6
7
8
9
10
11
12
(对象-》动作-》结果)
(用户-》商品)
用户能够 搜索商品、查看商品明细(spu)、加入购物车、(直接/购物车)购买商品(下单)、收藏商品、浏览记录、用户付款
用户 评论商品
------------------------------------------------------------------
(商家-》商品)
商家 上/下-架商品(ES)、商家收账、
商家 为商家指定品牌
商家 回复用户评论

-----------------------------------------------------------------
支付问题:商家 与 用户 一对一支付 (支付宝支付)

订单业务

电商订单逻辑图_瑰的博客-CSDN博客

1
2
3
4
5
6
7
8
用户 创建订单

订单状态 的流转:下单(待付款) -》付款(待发货) -》送达(待收货) -》 收货(待评价) -》 评价 / 超时(订单结束)
支线:退货(各个位置都可以退货)-》商家确认退货 -》 (商家确认收货)账户退款

(下单并发问题:按照时间顺序,先后下单成功)

上面的每一步都涉及到其他模块,可通过画图了解

img

img

img

拓展:

6个部分,详解电商订单管理流程-腾讯云开发者社区-腾讯云 (tencent.com)

库存业务-ware

1
2
3
4
5
6
7
8
9
10
11
用户 -》库存
商家 可以添加库存
用户 下单,需要锁定库存
用户 购买商品,需要扣减库存
秒杀商品 上架,需要提前锁定库存

需要有个库存单,跟踪每一个sku入库、出库时间

库存 -》 商家、商品
库存不足的时候告警商家
库存为零,提前修改商品状态,避免每次都进行查询

优惠业务 - 秒杀、促销:- 通用域

一文搞懂电商订单价格计算逻辑 - 知乎 (zhihu.com)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
用户(商家): 
1、商家 上架秒杀商品、商家设置促销(时间、促销策略)
(预置几种促销策略)
2、优惠绑定到对应的商品

:
3、商品 获取当前的优惠信息


4、如何根据商品当前的优惠策略,计算最终结果?
- 针对单个商品的策略:单独计算商品价格
- 针对总价的策略(优惠卷、满减),计算总价


5、计算价格
在很多场景都有价格展示
- 商品价格展示:获取商品信息的时候,也要获取商品的价格信息(sku-price) + coupon price
- 购物车价格(商品单价、总结)展示:沿用商品价格信息 + 总价信息(商品coupon的 原价、优惠价)
- 订单价格计算:针对所有商品的价格需要根据用户的选择进行计算(coupon)
- 秒杀商品的价格计算,先计算价格,再发布/上架

优惠策略:优惠卷、店铺满减、直接原价/现价,打折

用户业务

(有个问题,字太多了,我自己都懒得看~~)

1
2
3
4
5
6
7
8
9
权限校验:(RBAC)
1、用户 需要登陆验证,有三种验证方式(项目账号登录注册、第三方账号登录、手机号登录 / 注册)

商品/文章 - 信息查看
2、用户 可以查看相关的信息,文章、商品(收藏、点赞信息) -- 这里也可以用到ES聚合搜索

3、用户 修改收货地址

4、用户 查看历史记录信息、收藏记录、购买记录 等等 -- 这些放置到对应的模块(商品、书籍)

鉴权业务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
验证/注册/登录:
用户 通过 qq登录
用户 通过 短信验证码登录

授权:
用户 获取token
用户 刷新token
用户 获取 secret id、key x
用户 创建策略组 == 权限集合x
用户 分配角色
用户 分配权限

用户 为角色分配权限
权限permission 的 细致化管理:接口注册

client 通过网关进行鉴权,

权限管理:
- 用户CRUD 权限信息

秒杀 业务– 做了什么

业务描述:

谷粒商城—商城业务—秒杀服务(311~324)_晨男业务商城24小时在线秒单您值得信赖_张老师的分享的博客-CSDN博客

秒杀自己的业务:

  • 秒杀商品的 上架/下架 逻辑 – 定时上架 : 秒杀 应该为系统管理员统一指定秒杀时间、场次,然后卖家确定秒杀策略
1
2
3
4
5
6
7
8
9
10
商家:
商家上架秒杀商品 (秒杀 -》 es/redis)

用户:
用户下单秒杀商品( 秒杀 -》 商品)

秒杀:
限流、熔断、防止提前秒杀token、超卖的保证、
缓存 预热; -- 定时上架,
商品提前下架

img

搜索业务

根据当前的 type,进行搜索

  • 获取搜索实现类
  • 封装为搜索结果
  • 展示结果的方式 ?

4、识别上下文和映射

用户上下文

  • 鉴权上下文 —
  • 用户上下文 — 用户相关功能

文章上下文:

  • 文章上下文 – 管理文章相关功能
  • 评论和回复 上下文 – 评论内容的检查、举报、分享

商品上下文

  • 商品上下文 – 用于管理商品 SPU信息
  • 优惠上下文 – 实时调整商品的优惠信息
  • 秒杀上下文 – 关于秒杀订单的创建(秒杀其实就是特殊的下单流程)
  • 购物车上下文 — 购物车的 商品添加、删除 -》 状态(优惠、秒杀、库存、)更新
  • 库存上下文 — 管理后台库存信息 、包括跟踪每一个具体的商品的分发
  • 订单上下文 – 用户管理订单状态的流转

其他上下文

  • 搜索上下文 - 负责用户、文章、商品的聚合搜索
  • 物流上下文 — 负责物流信息的查询和配送状态的更新等功能 xx - 订单状态 (通用域)、运费
  • 支付上下文 – 负责支付信息的关系 == 浅谈支付系统开发基本流程 - 掘金 (juejin.cn)
  • 店铺上下文 — 商家管理店铺

(物流、支付、店铺、评论、物流 暂时不是主要,) - 后面再补补

属于基础设施:搜索上下文 — 用于管理文章、商品 搜索,建立索引

上面的模块划分是按照有哪些主要的业务进行划分,并不准确,比如,为什么要划分order、stock、cart模块?

上下文如何划分

1
2
3
4
问题:
秒杀 如何界定?
优惠 在购物车、商品模块的显示如何界定 -- 优惠模块

问题二:

(状态) 指标上下文 – 管理文章、商品的 状态 (点赞、收藏、浏览量)、优惠

1
2
3
4
5
6
7
8
9
1、状态 这个词范围太广,如果只负责管理点赞、收藏、浏览量、应该如何进行限定?
2、这些东西是否可以直接去文章、商品直接拿到?
3、文章的展示,需要这些信息;(浏览量等) 商品的展示需要这些信息(销售量)
4
文章的状态:点赞、收藏、浏览量(热度)
商品的状态:好评率(99%)、销售量(几千、几万)

结论:
不需要这个上下文,会增加调用调用链路的长度

5、领域分析模型 - 实体

提炼出领域对象

gateway接口

评论内容 和 评论人

订单 和 商品信息、库存信息

关联和聚合

在领域驱动设计中,关联关系和聚合关系是两种不同的关系类型。

关联关系(Association)表示两个实体之间的关联,一个实体可以引用另一个实体,但它们之间没有明确的所有权或生命周期的关系。关联关系可以是一对一、一对多或多对多的关系。

聚合关系(Aggregation)表示一个实体包含其他实体,并且这些实体之间有明确的所有权和生命周期的关系。聚合关系是一种强关联关系,表示整体与部分之间的关系。

1
2
3
区别在于聚合关系中,包含实体的实体(聚合根)负责管理包含的实体的生命周期和一致性。聚合根是整个聚合的入口点,通过聚合根可以访问和操作聚合中的其他实体。而关联关系中的实体之间没有明确的所有权和生命周期的关系,它们之间的关联可以是临时的或者可变的。

举个例子,假设有一个订单实体和订单项实体。订单实体是聚合根,它包含了订单项实体。订单项实体是订单的一部分,没有独立存在的意义。在这种情况下,订单实体和订单项实体之间是聚合关系。
1
2
3
4
5
6
7
8
9
10
11
根据上述的描述,订单项是一个实体。尽管订单项不能单独存在,但它仍然是一个具有唯一标识和属性的对象。

在领域驱动设计中,实体是具有唯一标识的对象,它具有自己的生命周期和状态。实体通过其唯一标识来区分和识别,不同的实体具有不同的标识。

订单项实体可以通过订单项ID(Order Item ID)来唯一标识一个订单项。订单项ID是一个独一无二的标识符,用于区分不同的订单项。通过订单项ID,可以准确地找到和操作特定的订单项实体。

例如,一个订单项实体可以包含订单项ID、商品ID、数量、价格等属性。通过订单项ID,可以唯一地标识和操作该订单项实体。

尽管订单项不能单独存在,但它仍然是一个具有唯一标识和属性的实体,它作为订单的一部分存在,并与订单实体形成聚合关系。

需要根据具体的业务需求和设计决策来确定实体的定义和关系。在某些情况下,一个实体可能无法单独存在,但它仍然是一个具有唯一标识和属性的对象。

鉴权模型 – auth

采用简单的版本 –

authentication / authorization – sms

实体:

  • 角色实体
  • 权限实体
  • 第三发客户端信息实体 ()
1
mvn archetype:generate -DgroupId=com.wherezy.travel -DartifactId=travel-auth -Dversion=1.0.0-SNAPSHOT -Dpackage=com.wherezy.travle.auth -DarchetypeArtifactId=cola-framework-archetype-web -DarchetypeVersion=4.3.0 -DarchetypeGroupId=com.alibaba.cola

用户 模型 – member

实体:

  • 用户实体
  • 角色、权限 — 参考 mall 的数据库表设计 spu:xxx:xx == value + url
    • === spring security.md 记录
    • 分为 验证(获取权限 | 验证权限) 、 授权(分token)两个阶段
  • 收件箱,接收/ 存储消息,
  • 收货地址信息

值对象:

聚合:用户信息

1
mvn archetype:generate -DgroupId=com.wherezy.travel -DartifactId=travel-member -Dversion=1.0.0-SNAPSHOT -Dpackage=com.wherezy.travle.member -DarchetypeArtifactId=cola-framework-archetype-web -DarchetypeGroupId=com.alibaba.cola -DarchetypeVersion=4.3.0

文章模型- 核心域 - article

实体:

  • 文章实体 (文章)

    1
    文章:标题、内容、摘要、封面、标签列表(值对象)
  • 分类

  • 标签对象 (用于搜索)

  • 文章评论

    1
    参考哔哩哔哩 的reply 字段设计
    1
    2
    3
    文章评论分为:comment 实体 和 replay 实体
    comment 实体维护评论的内容, 评论人 、被评论人相关信息(昵称、头像等)
    replay 实体维护评论之间的依赖关系 : 评论人、被评论人、评论类型(文章【根】、评论【叶子】)、 root_id(文章id【根】 | 评论根id)、 comment_id
  • 点赞、收藏 ==== 实体

值对象:

聚合:

  • 文章信息
  • 评论信息:应该包含评论人的信息 — 通过gateway 进行转化
1
mvn archetype:generate -DgroupId=com.wherezy.travel -DartifactId=travel-article -Dversion=1.0.0-SNAPSHOT -Dpackage=com.wherezy.travle.article -DarchetypeArtifactId=cola-framework-archetype-web -DarchetypeVersion=4.3.0 -DarchetypeGroupId=com.alibaba.cola

商品模型 - 核心域- product

关联问题:商品是唯一标识的话,

  • 它标识的是– 一件 红色的 、T恤; —- 一个SPU
  • 这件T恤 有 S、L、XL、XXL 尺码 — 多个SKU
  • 则商品 1 SPU - n SKU
  • 因为操作 商品的最小标识,为 SPU_id
  • 转为 1sku - 1spu,则通过skuId 可以唯一标识 商品的库存

商品 和 订单项如何标识:

  • 一个订单项,包含的是一个 SPU + SKU, 这才是一个商品的内容
    即:订单项包含数量

实体:

  • 商品实体 == spu
    如何唯一标识一个商品 – 通过 SPU唯一标识商品

  • SKU === 关联 product_id

  • 商品属性 == attribute

  • 商品分类 == catalog

  • 商品品牌 == brand

值对象:

以手机为例,说明SPU(Standard Product Unit)、SKU(Stock Keeping Unit)和属性(Attribute)的概念。

  1. SPU(Standard Product Unit):
    • 手机的SPU代表了一组具有相同属性的手机集合,它们共享相同的基本属性,如品牌、型号、操作系统等。
    • SPU可以看作是手机的模板或原型,用于定义手机的基本特征和属性。
  2. SKU(Stock Keeping Unit):
    • 手机的SKU代表了手机的具体规格、属性和库存信息。一个SPU可以对应多个不同的SKU,每个SKU可以有自己的唯一标识、价格、颜色、存储容量等属性。
    • 每个SKU代表了手机的一个具体型号或配置,用于区分不同的手机产品。
  3. 属性(Attribute):
    • 手机的属性是描述手机特征和规格的关键信息,如品牌、型号、操作系统、屏幕尺寸、摄像头像素等。
    • 属性可以用于区分不同的手机型号或配置,帮助用户选择符合其需求的手机。

在手机的设计中,可以将SPU视为手机的共有属性,如品牌、型号、操作系统等。而每个SKU则代表了手机的具体规格和配置,如不同的颜色、存储容量等。属性则是描述手机特征和规格的关键信息,用于区分不同的手机型号或配置。

通过SPU和SKU的设计,可以实现手机的分类、搜索和比较。用户可以根据手机的属性进行筛选,选择符合自己需求的手机产品。

如果您需要更详细的信息,可以使用以下查询词进行搜索:手机SPU、手机SKU、手机属性。

1
mvn archetype:generate -DgroupId=com.wherezy.travel -DartifactId=travel-product -Dversion=1.0.0-SNAPSHOT -Dpackage=com.wherezy.travle.product -DarchetypeArtifactId=cola-framework-archetype-web -DarchetypeVersion=4.3.0 -DarchetypeGroupId=com.alibaba.cola

订单模型 - 核心域 - order

实体:

  • 订单实体(包含订单的相关信息:订单支付金额、支付方式、订单号、)
  • 订单1 - n 订单项
  • 退货单实体 ()

值对象:订单状态、支付方式、退货状态、

聚合:订单 - (订单实体、订单状态、spu_id 、sku_id)

1
mvn archetype:generate -DgroupId=com.wherezy.travel -DartifactId=travel-order  -Dversion=1.0.0-SNAPSHOT -Dpackage=com.wherezy.travle.order -DarchetypeArtifactId=cola-framework-archetype-web -DarchetypeVersion=4.3.0 -DarchetypeGroupId=com.alibaba.cola
1
2
3
4
5
6
7
8
9
10
11
订单(Order)和订单项(Order Item)是在订单管理系统中常见的概念,它们之间存在一定的区别。

订单是指客户下达的购买请求或交易请求,代表着客户与商家之间的一次交易。订单通常包含了客户的基本信息(如姓名、联系方式)、订单号、下单时间、支付方式、配送地址等。订单是一个整体,代表了客户购买的所有商品或服务的集合。

订单项是指订单中的每个商品或服务的具体信息,包括商品或服务的名称、数量、单价、总价等。订单项是订单的组成部分,用于描述订单中的每个购买项。

简单来说,订单是一个整体,代表了客户的购买请求或交易请求,包含了订单的基本信息。而订单项是订单中的每个商品或服务的具体信息,用于描述订单中的每个购买项。

举个例子,假设一个客户下了一个购买电子产品的订单。这个订单可以包含订单号、下单时间、支付方式、配送地址等基本信息。而订单项则是描述订单中每个电子产品的具体信息,如产品名称、数量、单价、总价等。

在订单管理系统中,通常会有一个订单表和一个订单项表。订单表用于存储订单的基本信息,而订单项表用于存储订单中每个购买项的具体信息。订单表和订单项表之间通常会通过订单号进行关联。这样可以方便地对订单和订单项进行查询、管理和统计。

库存模型 - 通用域 - ware

  • 库存用于管理物流的,按照地域进行 stock 管理,以及发货问题

实体:

  • 库存工作单实体
  • 发货清单:
1
mvn archetype:generate -DgroupId=com.wherezy.travel -DartifactId=travel-ware -Dversion=1.0.0-SNAPSHOT -Dpackage=com.wherezy.travle.ware -DarchetypeArtifactId=cola-framework-archetype-web -DarchetypeVersion=4.3.0 -DarchetypeGroupId=com.alibaba.cola

购物车模型 - cart

1
需要获取商品信息、库存信息、优惠信息

购物车只有聚合:商品id、库存id、优惠id

  • 购物车项 = 购物车 商品数量 + 商品基本信息 == 数据库持久化信息
1
2
3
4
5
6
7
8
购物车商品信息 涉及:
1、预览信息
- product_id == 预览信息
- sku_id 存储单元信息 == 预览信息
- coupon_id 优惠信息 == 预览信息

2、商品数量信息
3、商铺信息,按照商品聚合在一起)
1
mvn archetype:generate -DgroupId=com.wherezy.travel -DartifactId=travel-cart -Dversion=1.0.0-SNAPSHOT -Dpackage=com.wherezy.travle.cart -DarchetypeArtifactId=cola-framework-archetype-web -DarchetypeVersion=4.3.0 -DarchetypeGroupId=com.alibaba.cola

优惠模型 - coupon

分析:

  • 优惠卷
  • 限时优惠
  • 优惠 和 商品

实体:优惠策略

值对象:

聚合:

1
mvn archetype:generate -DgroupId=com.wherezy.travel -DartifactId=travel-coupon -Dversion=1.0.0-SNAPSHOT -Dpackage=com.wherezy.travle.coupon -DarchetypeArtifactId=cola-framework-archetype-web -DarchetypeVersion=4.3.0 -DarchetypeGroupId=com.alibaba.cola

秒杀模型 - sekill

需要先确定 商品、优惠 等实体,才能确定秒杀的具体

实体:

  • 秒杀订单实体

  • 秒杀商品 实体 == product + 优惠策略

  • 秒杀场次 信息实体

值对象:

1
mvn archetype:generate -DgroupId=com.wherezy.travel -DartifactId=travel-seckill -Dversion=1.0.0-SNAPSHOT -Dpackage=com.wherezy.travle.seckill -DarchetypeArtifactId=cola-framework-archetype-web -DarchetypeVersion=4.3.0 -DarchetypeGroupId=com.alibaba.cola
1
mvn archetype:generate -DgroupId=com.wherezy.travel -DartifactId=travel-seckill -Dversion=1.0.0-SNAPSHOT -Dpackage=com.wherezy.travle.seckill -DarchetypeArtifactId=cola-framework-archetype-web -DarchetypeVersion=4.4.0-SNAPSHOT -DarchetypeGroupId=com.alibaba.cola

实体:

  • 文章搜素实体
  • 商品搜索
  • 用户搜素

这算什么呢?

基础设施层,进行搜索的实体 - DateObject? DTO -》 DO

聚合:文章搜索(ES)、商品搜索(ES)、用户搜索(mysql)、

1
2
3
4
5
6
7
8
问题:
1: 根据搜索的复杂度 判断应该在 es\mysql 进行上下架搜索
- 用户: 通过mysql进行搜索,搜索条件比较简单
- 文章 | 商品 通过ES 进行检索,检索条件比较复杂

2: 检索的条件生成方式
- 文章: 分类\标签\文章内容的模糊搜索
- 商品: 分类\属性\介绍\ == 多个维度进行检索
1
mvn archetype:generate -DgroupId=com.wherezy.travel -DartifactId=travel-search -Dversion=1.0.0-SNAPSHOT -Dpackage=com.wherezy.travle.search -DarchetypeArtifactId=cola-framework-archetype-web -DarchetypeVersion=4.3.0 -DarchetypeGroupId=com.alibaba.cola

6、领域设计模型 - 聚合

  • 每一个业务,如果采用了某个技术,都要思考为什么要用它

聚合\ 业务描述

聚合的一些设计原则

  1. 在一致性边界内建模真正的不变条件:聚合内的实体和值对象按照统一的业务规则运行,实现数据对象的一致性。
  2. 设计小聚合:降低由于业务过大导致的聚合重构的可能性。
  3. 通过唯一标志引用其他聚合:聚合之间通过关联外部聚合根 id 的方式引用。
  4. 在边界之外使用最终一致性:在一次事务中,最多只能更改一个聚合的状态。若一次业务操作导致多个聚合状态的修改。可以采用领域事件异步修改相关的聚合。
  5. 通过应用层实现跨聚合的服务调用:为实现微服务内聚合之间的解耦,以及未来以聚合为单位的微服务组合和拆分,应避免跨聚合的领域服务调用和跨聚合的数据库表关联。

权限聚合

  • 认证服务器聚合
  • 授权服务器聚合

文章 聚合

分为两个聚合:文章展示信息、文章预览信息 和 评论内容(root、更多消息)

项目中的热点数据、排行榜 - 文章

  • zset
  • 如何 缓存预热、缓存存什么 (展示内容,还是完整 文章内容)
  • 文章完整内容 和 文章展示内容的关系,
  • 后台获取展示内容(数据库存放展示内容),
  • 通过展示内容获取完整内容

文章的 点赞、收藏:

  • 实现类似于朋友圈的设计,展示和朋友的共同 收藏、关注、点赞 == redis set 的 intersection 交集

文章 是否点赞:

  • 用户单独缓存 当前点赞文章集合

d、文章包含商品的关键词 附带搜索链接

商品 聚合

商品完整聚合:商品实体、sku实体、coupon预览信息、

商品预览聚合:

  • 缓存预热

  • 展示聚合
    展示商品时涉及到的聚合:主要是商品属性 和 商品spu(实体)、优惠消息展示(满减、秒杀、限时)、

  • 下单 / 购物车 聚合
    下单时需要包含stock信息,即加入购物车、下单时,需要聚合上 stock信息

订单聚合

  • 订单项、订单实体 聚合
  • 退货聚合

img

购物车 聚合

  • 购物车项 = 购物车 商品数量 + 商品基本信息 == 数据库持久化信息
    1
    2
    3
    4
    购物车商品信息 涉及:
    1、product预览信息、sku_id 存储单元信息 == 预览信息,coupon_id 优惠信息 == 预览信息
    2、商品数量信息
    3、商铺信息,按照商品聚合在一起)

img

秒杀 聚合

  • 缓存预热
  • 秒杀场次 + 秒杀商品信息

聚合:秒杀聚合优惠策略 + 秒杀场次 + 秒杀的商品 coupon

问:秒杀聚合对应关系

1
2
3
4
5
6
7
一/ n 个商品 对应 一个秒杀场次+秒杀策略
一个商品 对应 一个策略
一个场次对应 n个商品

a、一个秒杀场次+秒杀策略 === 一个聚合==x
b、一个商品 对应 一个策略 == 一个聚合b
c、聚合b + 场次 == 聚合c

搜素 聚合es

对于文章和商品的搜索条件,可以将其聚合为以下Elasticsearch的搜索条件:

  1. 文章搜索条件:
    • 根据关键字搜索:可以使用match或match_phrase查询,根据文章标题、内容或其他相关字段进行全文搜索。
    • 根据标签搜索:可以使用term或terms查询,根据文章的标签字段进行精确匹配搜索。
    • 根据时间范围搜索:可以使用range查询,根据文章的发布时间字段进行范围搜索。
    • 根据作者搜索:可以使用term或terms查询,根据文章的作者字段进行精确匹配搜索。
  2. 商品搜索条件:
    • 根据关键字搜索:可以使用match或match_phrase查询,根据商品名称、描述或其他相关字段进行全文搜索。
    • 根据分类搜索:可以使用term或terms查询,根据商品的分类字段进行精确匹配搜索。
    • 根据价格范围搜索:可以使用range查询,根据商品的价格字段进行范围搜索。
    • 根据销量排序:可以使用sort查询,根据商品的销量字段进行排序。

需要根据具体的业务需求和数据模型来确定适合的搜索条件。可以根据以上示例,结合具体的字段和查询方式,构建适合的Elasticsearch搜索条件。

7、服务设计 - service

在秒杀业务中,秒杀过程的实现可以涉及领域服务和应用服务。

领域服务(Domain Service)是负责实现业务领域中的核心逻辑和规则的组件。在秒杀业务中,领域服务可以负责处理秒杀商品的库存管理、订单生成、限流控制等核心业务逻辑。领域服务通常是与具体的业务领域紧密相关的,它们封装了业务规则和领域知识,负责处理业务的核心逻辑。

应用服务(Application Service)是负责协调和组织领域对象和领域服务的组件。在秒杀业务中,应用服务可以负责接收用户的秒杀请求,调用领域服务进行业务处理,并返回处理结果给用户。应用服务通常是与用户交互和系统边界相关的,它们负责处理用户请求、协调领域对象和领域服务的交互,以及处理一些与业务流程相关的逻辑。

在划分职责时,可以考虑以下原则:

  1. 领域服务应该关注业务领域的核心逻辑和规则,负责处理与业务领域相关的复杂业务逻辑。
  2. 应用服务应该关注用户交互和系统边界,负责处理用户请求、协调领域对象和领域服务的交互,以及处理一些与业务流程相关的逻辑。
  3. 领域服务和应用服务应该相互配合,共同完成业务需求。领域服务提供核心的业务逻辑,而应用服务负责将领域服务组织起来,与用户进行交互。

需要根据具体的业务需求和系统设计来划分领域服务和应用服务的职责,确保职责清晰、逻辑清晰,并符合业务需求。

网关服务 *

spring boot配置 ssl

使用SpringBoot配置https(SSL证书)_springboot ssl_GavinYCF的博客-CSDN博客

需要整合https:微服务权限终极解决方案,Spring Cloud Gateway + Oauth2 实现统一认证和鉴权!-腾讯云开发者社区-腾讯云 (tencent.com)

1、添加 jks

2、获取公钥地址

3、重定向

a、负载均衡

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
spring:
cloud:
gateway:
routes:
- id: myRoute
uri: lb://service-name
predicates:
- Path=/path
filters:
- RewritePath=/oldPath, /newPath
metadata:
weight: 2
# 指定负载均衡策略
lb:
# 使用Round Robin(轮询)策略
policy: RoundRobin

b、路由转发route

  • 在yaml配置文件中,配置路由策略,/pms/, /oms/

c、统一权限验证

1、网关层通过拦截所有请求,进行token 的解析,拿出对应的 userId、role,进行权限的获取

  • auth:userId – string === 保存用户 permission
  • role 可以确定基本的可访问模块权限
  • permission可以确定 可以访问 / 不可以访问 路径权限

2、通过路径 /oms/xxx/ 进行鉴权

d、Https 配置

spring cloude gateway 配置 TLS:Spring Cloud Gateway

示例:

要在Spring Cloud Gateway中配置HTTPS,您需要在网关服务和后端服务之间建立安全连接。以下是配置Spring Cloud Gateway使用HTTPS的示例:

  1. 生成SSL证书:

首先,您需要生成SSL证书。您可以使用自签名证书或从可信的证书颁发机构(CA)获取证书。生成证书后,将证书和私钥保存在服务器上。

  1. 配置Spring Cloud Gateway:

在Spring Cloud Gateway的配置文件中,您需要指定SSL证书的相关信息。以下是一个示例:

1
2
3
4
5
6
7
8
codeserver:
port: 443
ssl:
enabled: true
key-store: classpath:keystore.p12
key-store-password: your_password
key-store-type: PKCS12
key-alias: your_alias

在上述示例中,您需要将key-store属性设置为您的SSL证书的路径,key-store-password设置为证书的密码,key-store-type设置为证书的类型(如PKCS12),key-alias设置为证书的别名。

  1. 配置后端服务:

如果您的后端服务也需要使用HTTPS,您需要在后端服务的配置文件中进行相应的HTTPS配置。具体的配置方式取决于您使用的后端服务框架和工具。

需要注意的是,配置HTTPS还涉及到服务器的网络配置和域名解析等方面。确保服务器的防火墙和网络配置允许HTTPS流量,并将域名解析正确指向服务器的IP地址。

以上是一个简单的示例,用于配置Spring Cloud Gateway使用HTTPS。具体的配置方式可能因您的环境和需求而有所不同。建议查阅Spring Cloud Gateway的官方文档或相关教程,以获取更详细的配置说明和示例。

如果您需要更具体的配置示例或有其他相关问题,请提供更多的上下文信息,以便我能够更好地帮助您。

f、熔断降级

权限服务 *

  • 认证服务 - 负责当前用户权限验证 、分发token
  • 授权服务 - 负责第三方客户端通过client id、secret id 分发token
  • 网关服务 - 负责接口级权限认证,解析token进行接口权限判断

a、用户(管理员)管理 权限信息

权限分为 通用和管理权限 == 而且,对外暴露的接口只是封装好的功能

1、按模块 = 展示,商品模块,订单模块,

2、按CRUD = 分配(接口级),根据业务,一般的用户不能 CUD, 只能 Retrive 查询

3、按照角色,分配

  • 超级管理员
  • 商品管理员
  • 订单管理员 == 订单状态流转问题,手动修改订单状态(涉及到退货)
  • 普通用户
  • 商家用户 – 可以管理秒杀商品信息、商品信息、退货管理
  • 自定义角色

4、按照用户分配角色

  • 用户拥有 角色,分配权限之前先创建角色

5、按照策略组分配 == 待定(有点复杂)

  • 用户拥有特殊的策略组

b、自定义验证码登录

  • spring security 框架 api 进行修改
  • 获取token /oauth/token

c、验证流程

  • 网关拦截
  • 网关解析
  • 网关ant match 路径
  • 网关放行,并设置用户信息

用户服务 *

  • 消息接收 == 事件问题
  • 地址管理
  • 个人信息管理

a、地址管理

地址运费交给谁来管理比较合适? – 物流模板

文章服务 *

a、文章的排序缓存

redis 实现 文章内容排序

  • zset:根据点赞(1)、收藏量(10),计算score,缓存文章预览信息
1
2
3
redis 缓存实时更新问题:
- 定期更新缓存内容
- 实时更新最新消息? == 这个做不到吧,最多定时刷新

d、文章预览信息的展示

b、点赞、关注: (方案二)

  • 方案一:article:like:articleId userId == 统计文章有哪些人点赞了 set

  • 方案二:article:like:userId articleId == 统计当前登录用户点赞了哪些文章 set

  • 统计文章点赞和文章收藏数:hash , article:likenum articleId numbers — HINCRBY counter page_view -50

  • article:startnum articleId numbers

(这里有个定时时间,定期将当前 redis 的数据同步到数据库进行持久化)

c、评论、回复

  • 将评论内容于回复分离,通过 评论 实体进行组合关系,通过组合结果,统一查询回复内容 comment
  • 为什么拆成两张表,因为一个维护关系,一个保存实际内容,可以在查询 树形结构关系的时候,减少MySQL的IO次数

e、文章分类、标签管理

f、文章检索- ES上架

ES 上架:

  • 上架内容:文章分类、标签、title、描述、时间

文章内容那么多,肯定不能全部上到ES,截取部分进行上架

MySQL检索:

  • 收藏、点赞数量

商品服务

a、商品上架、下架

上架管理页面:

涉及到ES,商品预览进行展示,重要的检索的关键信息

b、商品预览信息

c、商品展示信息

d、商品分类、属性、spu、sku

e、计算价格(获取价格)
在很多场景都有价格展示

  • 商品价格展示:获取商品信息的时候,也要获取商品的价格信息(sku-price) + coupon price
  • 购物车价格(商品单价、总结)展示:沿用商品价格信息 + 总价信息(商品coupon的 原价、优惠价)
  • 订单价格计算:针对所有商品的价格需要根据用户的选择进行计算(coupon)

订单服务 *

谷粒商城项目笔记总结(2/2)_谷粒商城怎么刷-CSDN博客

在这里插入图片描述**

a、下单流程

  • 电商订单逻辑图_瑰的博客-CSDN博客
  • 秒杀下单 和 正常下单有什么区别
    • 秒杀其实就是正常下单的优惠,有秒杀项(订单项)
  • 下单的幂等性 - 唯一token
  • 为了保证退货的价格问题,创建订单项,将整个订单优惠,保存到所有订单项中
  1. 通过异步的方式,获取 用户地址信息、优惠价格、库存判断
    (每个订单项 可以有一个地址,方便拆单)

在这里插入图片描述

b、订单状态流转: == SAGE协调事务

  • 创建订单:购物车-》订单-计算价格、整体优惠、地址信息
  • 提交订单:锁定库存 – 已创建
  • 支付中:付款 – 支付宝
    • 支付成功:状态 – 待发货、订单拆单 – 支付成功就可以拆单发货
      • 物流的流程:已发货待收货(货到)、待评价(自动 | 手动收货)、订单结束
      • (订单项)退款流程:创建退款reason 、
        • 提交退款 – 待处理、
        • 同意退款:处理商品、物流
        • 商家已收货:物流流程
        • 商家已验货:流程
        • 商家退款:支付宝订单腿快
    • 支付失败:
      • 余额不足 — 退出,待支付状态
    • 超时未支付:
      • 订单关闭 — 库存回滚、修改订单状态

协调事务SAGE:

订单创建 — 订单超时

支付成功 —- 支付失败

库存锁定 — 库存解锁

  • 子事务有哪些:
    • 库存锁定
    • 支付
  • 补偿机制有哪些:
    - 库存解锁

img

c、退货流程

  • 退货最重要的就是价格计算
  • 退货商品拆分,每次退的是订单项 还是 订单 — 订单项
  • 整个订单的优惠,按照价格比例,平分到其他订单项 === 优惠20,4个订单项 10 20 30 40 – 平分后:2 4 6 8 – 则退还 8 16 24 32
  • 为了保证退货的价格问题,创建订单项,将整个订单优惠,保存到所有订单项中

当按照订单项进行退货时,涉及到整个订单的优惠通常会根据商家的退货政策进行处理。以下是一些常见的处理方式:

  1. 部分退款:如果整个订单享受了某种优惠(如满减、折扣等),而退货只涉及部分订单项,商家可以根据退货的具体情况进行部分退款。退款金额将根据退货的订单项和优惠金额进行计算。
  2. 优惠券或代金券的处理:如果顾客在购买时使用了优惠券或代金券,而退货后订单金额不满足优惠券或代金券的使用条件,商家可以根据退货的金额进行相应的调整。例如,将优惠券或代金券的折扣金额从退款中扣除或作为电子券返还给顾客。
  3. 重新计算优惠:如果退货导致整个订单不再满足某种优惠条件(如满减),商家可以重新计算订单金额并相应调整优惠金额。这样可以确保退货后的订单金额仍然符合优惠条件。

需要注意的是,具体的处理方式可能因商家而异。商家应该在退货政策中明确说明涉及到整个订单的优惠处理方式,以便顾客了解和接受。如果您有特定的退货问题,建议咨询商家的客服或查阅其退货政策以获取准确的信息。

d、支付事件分发

e、价格计算

1
2
3
4
5
5、计算价格
在很多场景都有价格展示
- 商品价格展示:获取商品信息的时候,也要获取商品的价格信息(sku-price) + coupon price
- 购物车价格(商品单价、总结)展示:沿用商品价格信息 + 总价信息(商品coupon的 原价、优惠价)
- 订单价格计算:针对所有商品的价格需要根据用户的选择进行计算(coupon)

f、订单拆单

  • 订单项 和 订单
  • 对于每一个订单项,对应一个支付编号
  • 对于一个订单号,有多个订单实体
  • 支付,可以提交多个支付订单,

I、参考状态、流程

电商订单逻辑图-CSDN博客

g、创建订单-消息队列

创建订单消息

在这里插入图片描述

h、订单解锁

  • 订单超时解锁
  • 库存不足解锁

问题:订单创建 和 库存锁定尽量放在一个数据库进行,如何实现这个

订单和库存解锁消息

在这里插入图片描述

库存服务

a、库存事件/消息队列

在这里插入图片描述

在这里插入图片描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
@Configuration
public class MyRabbitConfig {
@Bean
public MessageConverter messageConverter() {
return new Jackson2JsonMessageConverter();
}

@Bean
public Exchange stockEventExchange() {
//String name, boolean durable, boolean autoDelete, Map<String, Object> arguments
return new TopicExchange("stock-event-exchange", true, false);
}

@Bean
public Queue stockReleaseStockQueue() {
//String name【名字】, boolean durable【是否持久化】, boolean exclusive【是否排他】, boolean autoDelete【是否自动删除】, Map<String, Object> arguments【参数】
return new Queue("stock.release.stock.queue", true, false, false);
}

@Bean
public Queue stockDelayQueue() {
Map<String, Object> args = new HashMap<>();
args.put("x-dead-letter-exchange", "stock-event-exchange");
args.put("x-dead-letter-routing-key", "stock.release");
args.put("x-message-ttl", 240000);// 4min
return new Queue("stock.delay.queue", true, false, false, args);
}

@Bean
public Binding stockReleaseBinding() {
return new Binding("stock.release.stock.queue",
Binding.DestinationType.QUEUE,
"stock-event-exchange",
"stock.release.#",
null);
}

@Bean
public Binding stockLockedBinding() {
return new Binding("stock.delay.queue",
Binding.DestinationType.QUEUE,
"stock-event-exchange",
"stock.locked",
null);
}
}

b、库存解锁

库存解锁的场景 :

  • 下订单成功,订单过期没有支付被系统自动取消,被用户手动取消,都要解锁库存。
  • 下订单成功,库存锁定成功,但是接下来的业务调用失败,导致订单回滚,之前锁定的库存就要解锁。

购物车服务

a、购物车下单时的幂等性保证

前端:用户购物车页面的时候分发一个唯一token(保存在服务端),保证当前页面只能发送一个下单请求。

后端:后端校验完token,删除,然后进行(预)下单的逻辑 == 这里进入订单模块

(下单分为两个步骤,进入下单预览 - 不会锁定库存, 正式下单-会锁定库存、生成订单信息)

b、生成订单预览

  • 通过购物车选中的购买商品 == 购物车item
  • 确定商品的数量问题 == 缓存商品数量

c、购物的展示问题

1
2
3
4
购物车商品信息 涉及:
1product预览信息、sku_id 存储单元信息,coupon_id 优惠信息
2、商品数量信息
3、商铺信息,按照商品聚合在一起)

可以缓存商品数量

展示购物车项应该包含这些信息,因为这些信息,需要分别访问两个 模块(商品、优惠),可以用异步的方式,获取信息

  • 购物车缓存问题:

e、 未登录用户的购物车信息

  • 登录后合并问题

f、 已登录购物车信息缓存

我觉得这里的缓存有点问题:缓存商品数量?缓存商品整体信息?

缓存商品数量信息(经常更改的信息)

  • 购物车数量的快速缓存 – 持久化问题,redis持久化,定时持久化到数据库

    • hset cart:userId cartItemId cnt [producet_id cnt]
    • hincrby cart:userId cartItemId x == cnt 增加 x(-x)
    • hlen cart:userId == 购物车商品数量
    • cnt == 商品数量

    img

g、未登录购物车

秒杀服务 *

技术点:

  • 限流 – 负载均衡 + 单机限流
  • 分布式锁:hset : key、uuid 1、expired
    看门狗
  • 预热 == xxx-job
  • 防刷
  • 提前下架

a、秒杀多级限流最终的方案是什么?

  • 前端入口,为秒杀场次分配唯一请求url 参数
  • nginx 四层 负载均衡 ==
  • 网关层 过滤无效请求
  • 分布式限流和单机限流问题:
    • 分布式限流方案:
      • 消息队列限流 — 会多出一个消息队列作为性能瓶颈,而且请求数量不可控,会引出其他的问题
      • sentinel限流 — 可以尝试,
    • 本地限流:guava的工具包,限流方案的实现 == 实现请求连接的限制
      • 固定窗口,滑动窗口,漏桶算法,令牌桶算法
      • 通过 aop 切面的方式,限制秒杀请求熟练,超过数量限制部分 === 漏桶算法
      • 如果库存为空,则后续请求全部返回

b、秒杀-用户数目限制

方案:redis + lua ,直接在lua 中实现限制逻辑

  • 个数问题:每个用户 n 个,秒杀商品总库存
  • 三个参数:想要购买数量 x,可以购买数量 y,剩余库存 stock ,可以购买数量限制:t
    • key-y != null && (y >= x, stock >= x)
      y -= x, stock -=x
    • key-y == null && (t >= x && stock >= x)
      set key-y t -= x
      stock -= x
  • 这样:入参 x , t ; 通过lua 脚本,可以实现,每次一次网络传输就可以实现分布式锁
完整版设计:hash结构
  • key 为 session:seckill:product_id
  • field :stock 、 user_id
  • value :stock - 库存,user_id - 剩余数量

根据你的需求,可以使用Redis的Hash结构来设计秒杀场景的存储结构。以下是一个完整的设计方案:

  1. 使用Hash结构存储秒杀商品的信息,其中Key为session:seckill:product_id,Field为stockuser_id,Value为对应的值。
  2. 在Hash结构中,stock字段表示商品的库存数量,user_id字段表示用户剩余的购买数量。
  3. 入参ARGV[1]表示用户想要购买的数量purchaseCount,入参ARGV[2]表示购买数量的限制limitCount

下面是一个示例的Lua脚本,用于实现秒杀场景中的用户数目限制,并更新Hash结构中的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
-- 获取库存数量和用户剩余数量
local stock = tonumber(redis.call('HGET', KEYS[1], 'stock'))
local userCount = tonumber(redis.call('HGET', KEYS[1], 'user_id'))

-- 获取购买数量和购买数量限制
local purchaseCount = tonumber(ARGV[1])
local limitCount = tonumber(ARGV[2])

-- 判断用户剩余数量是否为空
if userCount then
-- 用户剩余数量不为空,判断用户剩余数量是否大于等于购买数量并且库存数量是否大于等于购买数量
if userCount >= purchaseCount and stock >= purchaseCount then
-- 减少用户剩余数量和库存数量
redis.call('HINCRBY', KEYS[1], 'user_id', -purchaseCount)
redis.call('HINCRBY', KEYS[1], 'stock', -purchaseCount)
return 1
else
return 0
end
else
-- 用户剩余数量为空,判断购买数量限制是否大于等于购买数量并且库存数量是否大于等于购买数量
if limitCount >= purchaseCount and stock >= purchaseCount then
-- 设置用户剩余数量的初始值为购买数量限制减去购买数量,并减少库存数量
redis.call('HSET', KEYS[1], 'user_id', limitCount - purchaseCount)
redis.call('HINCRBY', KEYS[1], 'stock', -purchaseCount)
return 1
else
return 0
end
end

在这个Lua脚本中,首先通过KEYS[1]从Redis中获取Hash结构的值,包括库存数量stock和用户剩余数量user_id。然后,根据入参ARGV[1]ARGV[2]获取购买数量purchaseCount和购买数量限制limitCount。接下来,根据用户剩余数量是否为空进行判断,如果不为空,则判断用户剩余数量是否大于等于购买数量并且库存数量是否大于等于购买数量,如果满足条件,则减少用户剩余数量和库存数量。如果用户剩余数量为空,则判断购买数量限制是否大于等于购买数量并且库存数量是否大于等于购买数量,如果满足条件,则设置用户剩余数量的初始值为购买数量限制减去购买数量,并减少库存数量。

你可以将这个Lua脚本传递给Redis的EVAL命令来执行,根据你的具体场景,将KEYSARGV替换为对应的键名和参数值即可。

请注意,这只是一个示例脚本,具体的实现可能需要根据你的业务需求进行调整。同时,为了保证原子性和避免并发问题,你可能还需要结合分布式锁来确保秒杀操作的正确性。

简单版:采用 string 进行存储
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
-- 获取购买数量和剩余库存
local x = tonumber(ARGV[1])
local t = tonumber(ARGV[2])
local stock = tonumber(redis.call('GET', KEYS[3]))

-- 判断key-y是否为空
local y = redis.call('GET', KEYS[2])

if y then
-- key-y不为空,判断y是否大于等于x并且stock是否大于等于x
if tonumber(y) >= x and stock >= x then
-- 减少y和stock的值
redis.call('DECRBY', KEYS[2], x)
redis.call('DECRBY', KEYS[3], x)
return 1
else
return 0
end
else
-- key-y为空,判断t是否大于等于x并且stock是否大于等于x
if t >= x and stock >= x then
-- 设置key-y的初始值为t-x,并减少stock的值
redis.call('SET', KEYS[2], t - x)
redis.call('DECRBY', KEYS[3], x)
return 1
else
return 0
end
end
  • 将 x 、y、t 分别替换为 purchaseCount、remainCount、limitCount
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
-- 获取购买数量和剩余库存
local purchaseCount = tonumber(ARGV[1])
local limitCount = tonumber(ARGV[2])
local stock = tonumber(redis.call('GET', KEYS[3]))

-- 判断key-y是否为空
local remainCount = redis.call('GET', KEYS[2])

if remainCount then
-- key-y不为空,判断y是否大于等于purchaseCount并且remainCount是否大于等于purchaseCount
if tonumber(remainCount) >= purchaseCount and stock >= purchaseCount then
-- 减少 remainCount 和 stock 的值
redis.call('DECRBY', KEYS[2], purchaseCount)
redis.call('DECRBY', KEYS[3], purchaseCount)
return 1
else
return 0
end
else
-- key-y为空,判断limitCount是否大于等于purchaseCount并且remainCount是否大于等于purchaseCount
if limitCount >= purchaseCount and stock >= purchaseCount then
-- 设置key-y的初始值为limitCount-purchaseCount,并减少remainCount的值
redis.call('SET', KEYS[2], limitCount - purchaseCount)
redis.call('DECRBY', KEYS[3], purchaseCount)
return 1
else
return 0
end
end

c、限流实现 - 采用多层级限流

1、服务器承受能力优先,要尽可能的减少无效请求到达后端:

  • 因为秒杀数量有限,放入多余的请求也是浪费性能

2、前端唯一url : 防止恶意请求

3、nginx 第七层负载均衡,尽可能使得多节点的均衡性

4、网关限流:七层限流,过滤无效请求(恶意IP、未登录请求)

5、秒杀模块限流:本地限流(Guava + 漏桶算法),控制请求速度,并过滤多余(超出库存)的请求

  • 本地限流可以作为请求拦截,intercepted

其他:

  • 合法性限流:验证码、IP、唯一性IP地址
  • 基于负载的限流:Niginx 负载均衡(第七层)、网关层负载均衡
  • 业务层负载均衡:
    令牌桶 - 为什么,保证数量 — 现有的 API
    消息队列
  • 前端采用 CSN 缓存 + Nginx 缓存

如何设计秒杀服务的限流策略? - 掘金 (juejin.cn)

为什么采用本地限流,而不是用分布式限流

  • 对于秒杀业务(高并发),分布式限流会有网络性能损失(多出一次网络传输)
  • 分布式限流 考虑消息队列的平滑、异步、水平拓展、负载均衡

分布式限流: == rabbitmq 限制速度

  • 分布式限流两种实现:rabbitmq 平稳、redis令牌 限流、

RabbitMQ提供了一些机制来控制消费者读取消息的速度,以确保消费者能够按照自身的处理能力进行消费,避免消息堆积或者消费者过载的情况。

以下是一些控制消费者读取速度的方法:

  1. Prefetch Count(预取数量):通过设置消费者的prefetch count值,可以限制消费者一次性从队列中获取的消息数量。这样可以确保消费者在处理完一批消息之前不会再次从队列中获取新的消息。可以使用channel.basicQos(prefetchCount)方法来设置prefetch count值。
  2. Consumer Acknowledgements(消费者确认):消费者可以使用消息确认机制来告知RabbitMQ已经成功处理了一条消息。通过手动确认消息的方式,可以确保消费者在处理完一条消息之后再获取下一条消息。可以使用channel.basicAck(deliveryTag, multiple)方法来手动确认消息。
  3. Consumer Rate Limiting(消费者速率限制):可以在消费者端实现速率限制的逻辑,例如使用计时器或者限制处理消息的频率。可以根据消费者的处理能力和业务需求,自定义控制消费者读取消息的速度。
  4. 使用多个消费者(Consumer):可以通过增加消费者的数量来提高消息的处理速度。每个消费者都可以独立地从队列中获取消息并进行处理,从而提高整体的消费速度。

需要根据具体的业务需求和场景选择合适的方法来控制消费者的读取速度。如果您需要更详细的信息,可以使用以下查询词进行搜索:RabbitMQ消费者速度控制、RabbitMQ prefetch count、RabbitMQ consumer acknowledgements。

服务(本地限流):

比如说令牌桶算法、漏洞算法都是的。那对于这些算法,如果你的编写有些困难,我们也可以直接调一些类库里边儿已经存在的 API。

1
2
3
4
5
6
7
8
9
10
public class RateLimiter {
//令牌桶限流:每秒只生成100个令牌,只有抢到令牌的线程才能抢购。
static RateLimiter tokenRateLimiter = RateLimiter.create( 100.0) ;
public static void miaoShaController () {
//每次抢购操作,都会持续尝试l秒
if (tokenRateLimiter.tryAcquire(1,TimeUnit.SECONDS)) {
//开启抢购线程
}
}
}

那么这就是使用 Google Guava 类裤的令牌桶算法,create() 的方法可以限制每秒钟最多有 100 个线程可以同时参与抢购,tryAcquire 方法可以用于设置每秒的抢购动作会持续 1 秒钟,实现起来非常简单。

image-20210216011102660

d、隐藏秒杀的入口地址

1
2
3
4
5
,它指的是在秒杀开始之前,服务器并不会向外界暴露秒杀服务的地址,当秒杀服务开始之后才开放地址。
这个如何实现:
1、前端到点主动刷新从后端获取 秒杀唯一路径,并进行状态刷新(可秒杀)
2、后端到点 分发秒杀唯一路径
3、gateway鉴权 秒杀路径,唯一路径进行验证(获取秒杀场次Id,去redis 获取提前放好的 唯一标识)

e、秒杀上架

  • 首先确认场次、确认 优惠策略 === 一个场次 对应 多个商品(包含策略)
  • 秒杀 应该为系统管理员统一指定秒杀时间、场次,然后卖家确定秒杀策略
  • redis key-value 设计:
    场次信息: seckill:session:[start_end] sessionId == key value (String)
    商品信息: seckill:product:[sessionId] skuId productInfo == key field value [field value …] (Hash
    库存信息:seckill:stock:[skudId] stocks === key value (String)
  • 秒杀存放的商品信息是什么:seckill item、product item
    • order itme 存放:商品展示信息 和 优惠信息 和 价格
      • 商品展示信息,除了原价不变,优惠信息需要重新计算和获取
      • 所以获取的是list product item
    • seckill itme 存放在redis 中,其实也是商品展示信息
    • 消息队列存放信息,尽量少,商品id、价格、优惠信息、订单id

img

f、秒杀提前下架

因为秒杀商品有一个唯一url 的token,这个token 在网关层进行校验:

  • 在网关层过滤多余请求,在业务层,直接提前删除库存为 0 的秒杀商品
  • 前端下架,如果

优惠服务

  • 关于用户的积分计算
  • 优惠分为两部分:整体优惠 和 局部优惠
    • 整体优惠:整个订单,可以有哪些优惠,针对的是整个订单
    • 局部优惠:针对的是单个订单项,商品有哪些优惠、商家有哪些优惠

a、优惠策略的制定

  1. 满200减20
  2. 打折 7折
  3. 代金券 和 优惠卷
  4. 赠品 - 免费赠送xxx
  5. 节假日限时优惠
  • 策略实体,抽象一个策略实体
    1
    包括策略描述等等自定义信息

b、通过优惠策略判断能够使用该优惠策略

  • 使用前提:满减的、优惠卷的、打折的、(每种策略都需要一个service进行特殊判断,抽象一个策略接口)

    1
    2
    3
    4
    策略接口包括:
    - 使用前提判断 check
    - 策略价格计算 calc
    - 是否支持这个实体的测率 support

e、价格计算

一文搞懂电商订单价格计算逻辑 - 知乎 (zhihu.com)

在很多场景都有价格展示

  • 商品价格展示:获取商品信息的时候,也要获取商品的价格信息(sku-price) + coupon price
  • 购物车价格(商品单价、总结)展示:沿用商品价格信息 + 总价信息(商品coupon的 原价、优惠价)
  • 订单价格计算:针对所有商品的价格需要根据用户的选择进行计算(coupon)
1
2
3
提供给一个接口,进行价格计算:
- 商品聚合的 list 价格计算:返回 商品list, 包含原价,现价
- 订单信息 和 选择的优惠策略(已经提前判断过是否可以进行价格优惠) ; 返回原价:现价

支付服务

手机网站支付 DEMO - 支付宝文档中心 (alipay.com)

更多接口调用详情请参考下表

接口英文名 接口中文名 描述
alipay.trade.wap.pay 手机网页支付接口 通过此接口传入订单参数,同时唤起支付宝手机网页支付页面
alipay.trade.close 交易关闭接口 通过此接口关闭此前已创建的交易,关闭后,用户将无法继续付款。仅能关闭创建后未支付的交易。
alipay.trade.query 交易状态查询接口 通过此接口查询某笔交易的状态,交易状态:交易创建,等待买家付款;未付款交易超时关闭,或支付完成后全额退款;交易支付成功;交易结束,不可退款。
alipay.trade.refund 交易退款接口 通过此接口对单笔交易进行退款操作。
alipay.trade.fastpay.refund.query 退款查询 查询退款订单的状态。
alipay.data.dataservice.bill.downloadurl.query 账单查询接口 调用此接口获取账单的下载链

搜索服务 *

首先需要确定的是商品搜索的 实体是什么

  1. 确认商品类型 – 手机、电脑、耳机
  2. 可以搜索的参数:(当前类型 - catalog)
    • 侧边栏:品牌、(原)价格、
    • 商品栏:分类、商品描述、
  3. 商品展示聚合
    - 用于搜索的字段:品牌、分类、title
    - 用于过滤的字段:价格、属性
    - 用于排序的字段:原价格、上架时间 (基本不变的)

    • 用于后端排序字段:销售量、好评率 (动态变化的,需要通过查询数据库的方式,进行排序)
      • 先通过ES 获取到这些商品的id
      • 通过id 获取到这些字段结果
      • 在后端进行排序 展示item

a、ES 上架 - 双写

商品的信息填写

  • 商品的spu 信息
  • 商品的sku 信息

根据商品聚合实体

aa、ES下架

商品下架分为:

  • 主动下架 商家主动点击下架

  • 被动下架 商品的库存都为0的时候,被动下架 (事件)

    • 如果查询商品–库存的时候,发现所有的库存都为0,则加入名单中
    • 每天定时扫描名单中的商品,二次确认后,如果库存仍然为0,则主动下架

b、ES - mysql 数据同步

  • 最新上架商品 – cancel、定时 == 同步双写
  • 库存为0 的商品 – 消息队列 === 异步下架
  • 主动下架的商品 — 双写(同步、异步) === 同步双写

选用策略:同步双写 + 异步下架

4、cancel

对于本次而言,不太适合使用cancel

  1. ES 中存储的是展示的聚合 item,如果通过订阅变更,则需要进行二次加工

c、ES 搜索-商品

  • 搜索先确定商品分类
  • 再通过当前商品分类的:商品公有属性attribute 缩小搜索范围
    (手机包含:品牌、CPU、屏幕、摄像头、运行内存 等参数
    oppo find x6 - 这是spu:则只拥有部分手机属性 – 比如:没有品牌)
    即:商品的公有属性 为 当前搜索出的商品的 不同attribute
  • 这些搜索方式可以参考淘宝

搜索主要分为两部分:

  • 商品搜索栏 — 确定分类、spu,进而确定侧边栏的属性
  • 商品侧边栏 — 通过已搜索的商品,确定公有的不同属性,进行商品过滤

如何确定侧边栏:

商品排序:

排序这里总共有根据价格排序、根据评价排序、根据新品排序、根据销量排序,排序要想实现非常简单,只需要告知排序的域以及排序方式即可实现。

价格排序:只需要根据价格高低排序即可,降序价格高->低,升序价格低->高

评价排序:评价分为好评、中评、差评,可以在数据库中设计3个列,用来记录好评、中评、差评的量,每次排序的时候,好评的比例来排序,当然还要有条数限制,评价条数需要超过N条。

新品排序:直接根据商品的发布时间或者更新时间排序。

销量排序:销量排序除了销售数量外,还应该要有时间段限制。

d、mysql 搜索-文章、用户

  • 文章为什么使用

8、基础设施

综合的基础设施

  • RPC 远程调用
  • Nacos 服务发现
  • RabbitMQ 消息队列 – 事件驱动
  • repository 持久化
  • 权限认证
  • 日志
  • 配置管理 – Nacos Config
  • 第三方系统

网关层:

  • 熔断限流
    网关层通常根据以下几个因素来判断是否需要进行熔断和限流:

    1. 请求频率:网关可以根据单位时间内的请求频率来判断是否需要进行限流。如果请求频率超过了系统的处理能力,就可以触发限流机制,防止系统过载。
    2. 错误率:网关可以监控请求的错误率,例如HTTP状态码为4xx或5xx的请求比例。如果错误率超过了预设的阈值,就可以触发熔断机制,暂时停止对该服务的请求,避免错误的传播和影响其他服务。
    3. 响应时间:网关可以监控请求的响应时间,如果某个服务的响应时间超过了预设的阈值,就可以触发熔断机制,暂时停止对该服务的请求,避免长时间的等待和影响其他服务。
    4. 资源利用率:网关可以监控后端服务的资源利用率,例如CPU、内存、数据库连接数等。如果资源利用率超过了预设的阈值,就可以触发限流机制,控制请求的并发量,保护后端服务的稳定性。

支付功能 —— 算是

8.2确定持久化Data Object

实体+聚合 == DataObject

用户DO

订单DO

订单:订单总价、收获地址、订单总优惠信息(coupon_ids),

订单项:单个商品预览信息,sku预览信息、优惠信息、商品数量(约等于 cartItem),商品价格

img

退货单:退货信息

购物车DO

cart_item : 包含商品预览信息、sku预览信息、coupon预览信息(或者coupon_id)、商品数量信息、商品价格

9、领域事件

DDD理论学习系列(9)– 领域事件-腾讯云开发者社区-腾讯云 (tencent.com)

下面简单说明领域事件:

事件发布:构建一个事件,需要唯一标识,然后发布;
事件存储:发布事件前需要存储,因为接收后的事建也会存储,可用于重试或对账等;
事件分发:服务内直接发布给订阅者,服务外需要借助消息中间件,比如Kafka,RabbitMQ等;
事件处理:先将事件存储,然后再处理。

事件溯源:发送端需要存储发送消息,接收端需要存储消息;

消息队列的设计

(暂时针对的RabbitMQ,)

  • 每个微服务只能对应一个exchange ()

订单事件

消息通知事件:

  • 订单状态流转:下单成功事件、订单(未)支付事件、到货通知事件、支付成功事件

业务流转事件:

  • 消费队列

用户模块的事件

  • 登录成果事件? - -次要

商家接收:

  • 库存

库存解锁事件

库存扣减事件

库存锁定

  • 商品

商品上架

秒杀商品上架

定时器 事件 - task

xxl-job

Java 定时任务详解 | JavaGuide(Java面试 + 学习指南)

springboot整合xxl-job详解(亲测可用)_开开心心学习工作的博客-CSDN博客

  • 每天的文章缓存预热
  • 秒杀商品的缓存预热
  • 每天的商品的缓存预热

搜索事件

商品被动下架事件

10、库表设计

字段、含义、作用

重要–只有从库表、字段、才能说清楚项目的作用,落实到实处

商品表

SPU(Standard Product Unit)和SKU(Stock Keeping Unit)

商品、分类、属性、品牌、

spu_id、sku_id、

订单表

订单、退货、支付、订单实体、订单状态信息

用户表

用户信息、收货地址列表、

文章表

文章信息、点赞收藏状态表

秒杀-促销表

2.3 详细设计

2.3.1 数据库详细设计

文章实体

1
2
3
4
5
6
7
8
9
10
11
12
13
CREATE TABLE articles (
article_id INT PRIMARY KEY AUTO_INCREMENT, -- 文章ID,使用自增长的整数作为主键
title VARCHAR(255) NOT NULL, -- 标题,限制最大长度为255个字符,不能为空
author VARCHAR(100) NOT NULL, -- 作者,限制最大长度为100个字符,不能为空
content TEXT NOT NULL, -- 内容,使用TEXT类型存储文章正文,不能为空
publish_time DATETIME NOT NULL, -- 发布时间,使用日期时间类型存储,不能为空
update_time DATETIME NOT NULL, -- 更新时间,使用日期时间类型存储,不能为空
likes INT DEFAULT 0, -- 点赞数,默认为0
views INT DEFAULT 0, -- 阅读数,默认为0
tags VARCHAR(255), -- 标签,限制最大长度为255个字符
is_top BOOLEAN DEFAULT false -- 是否置顶,默认为false
);

订单实体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
DROP TABLE IF EXISTS `oms_order`;
CREATE TABLE `oms_order` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`member_id` bigint(20) NULL DEFAULT NULL COMMENT 'member_id',
`order_sn` char(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '订单号',
`coupon_id` bigint(20) NULL DEFAULT NULL COMMENT '使用的优惠券',
`create_time` datetime(0) NULL DEFAULT NULL COMMENT 'create_time',
`member_username` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '用户名',
`total_amount` decimal(18, 4) NULL DEFAULT NULL COMMENT '订单总额',
`pay_amount` decimal(18, 4) NULL DEFAULT NULL COMMENT '应付总额',
`freight_amount` decimal(18, 4) NULL DEFAULT NULL COMMENT '运费金额',
`promotion_amount` decimal(18, 4) NULL DEFAULT NULL COMMENT '促销优化金额(促销价、满减、阶梯价)',
`integration_amount` decimal(18, 4) NULL DEFAULT NULL COMMENT '积分抵扣金额',
`coupon_amount` decimal(18, 4) NULL DEFAULT NULL COMMENT '优惠券抵扣金额',
`discount_amount` decimal(18, 4) NULL DEFAULT NULL COMMENT '后台调整订单使用的折扣金额',
`pay_type` int(4) NULL DEFAULT NULL COMMENT '支付方式【PAYPAL中国->101;PAYPAL香港->102;PAYPAL全球->103;支付宝->201;支付宝香港->202;支付宝全球->203;微信->301;微信香港->302;微信全球->303;银联->401;货到付款->501】',
`source_type` tinyint(4) NULL DEFAULT NULL COMMENT '订单来源[0->PC订单;1->app订单]',
`status` tinyint(4) NULL DEFAULT NULL COMMENT '订单状态【0->待付款;1->待发货;2->已发货;3->已完成;4->已关闭;5->无效订单】',
`delivery_company` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '物流公司(配送方式)',
`delivery_sn` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '物流单号',
`auto_confirm_day` int(11) NULL DEFAULT NULL COMMENT '自动确认时间(天)',
`integration` int(11) NULL DEFAULT NULL COMMENT '可以获得的积分',
`growth` int(11) NULL DEFAULT NULL COMMENT '可以获得的成长值',
`bill_type` tinyint(4) NULL DEFAULT NULL COMMENT '发票类型[0->不开发票;1->电子发票;2->纸质发票]',
`bill_header` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '发票抬头',
`bill_content` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '发票内容',
`bill_receiver_phone` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '收票人电话',
`bill_receiver_email` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '收票人邮箱',
`receiver_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '收货人姓名',
`receiver_phone` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '收货人电话',
`receiver_post_code` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '收货人邮编',
`receiver_province` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '省份/直辖市',
`receiver_city` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '城市',
`receiver_region` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '区',
`receiver_detail_address` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '详细地址',
`note` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '订单备注',
`confirm_status` tinyint(4) NULL DEFAULT NULL COMMENT '确认收货状态[0->未确认;1->已确认]',
`delete_status` tinyint(4) NULL DEFAULT NULL COMMENT '删除状态【0->未删除;1->已删除】',
`use_integration` int(11) NULL DEFAULT NULL COMMENT '下单时使用的积分',
`payment_time` datetime(0) NULL DEFAULT NULL COMMENT '支付时间',
`delivery_time` datetime(0) NULL DEFAULT NULL COMMENT '发货时间',
`receive_time` datetime(0) NULL DEFAULT NULL COMMENT '确认收货时间',
`comment_time` datetime(0) NULL DEFAULT NULL COMMENT '评价时间',
`modify_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `order_sn`(`order_sn`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 40 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '订单' ROW_FORMAT = Dynamic;

订单聚合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
DROP TABLE IF EXISTS `oms_order_item`;
CREATE TABLE `oms_order_item` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`order_id` bigint(20) NULL DEFAULT NULL COMMENT 'order_id',
`order_sn` char(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'order_sn',
`spu_id` bigint(20) NULL DEFAULT NULL COMMENT 'spu_id',
`spu_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'spu_name',
`spu_pic` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'spu_pic',
`spu_brand` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '品牌',
`category_id` bigint(20) NULL DEFAULT NULL COMMENT '商品分类id',
`sku_id` bigint(20) NULL DEFAULT NULL COMMENT '商品sku编号',
`sku_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '商品sku名字',
`sku_pic` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '商品sku图片',
`sku_price` decimal(18, 4) NULL DEFAULT NULL COMMENT '商品sku价格',
`sku_quantity` int(11) NULL DEFAULT NULL COMMENT '商品购买的数量',
`sku_attrs_vals` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '商品销售属性组合(JSON)',
`promotion_amount` decimal(18, 4) NULL DEFAULT NULL COMMENT '商品促销分解金额',
`coupon_amount` decimal(18, 4) NULL DEFAULT NULL COMMENT '优惠券优惠分解金额',
`integration_amount` decimal(18, 4) NULL DEFAULT NULL COMMENT '积分优惠分解金额',
`real_amount` decimal(18, 4) NULL DEFAULT NULL COMMENT '该商品经过优惠后的分解金额',
`gift_integration` int(11) NULL DEFAULT NULL COMMENT '赠送积分',
`gift_growth` int(11) NULL DEFAULT NULL COMMENT '赠送成长值',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 55 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '订单项信息' ROW_FORMAT = Dynamic;

退货实体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
DROP TABLE IF EXISTS `oms_order_return_apply`;
CREATE TABLE `oms_order_return_apply` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`order_id` bigint(20) NULL DEFAULT NULL COMMENT 'order_id',
`sku_id` bigint(20) NULL DEFAULT NULL COMMENT '退货商品id',
`order_sn` char(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '订单编号',
`create_time` datetime(0) NULL DEFAULT NULL COMMENT '申请时间',
`member_username` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '会员用户名',
`return_amount` decimal(18, 4) NULL DEFAULT NULL COMMENT '退款金额',
`return_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '退货人姓名',
`return_phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '退货人电话',
`status` tinyint(1) NULL DEFAULT NULL COMMENT '申请状态[0->待处理;1->退货中;2->已完成;3->已拒绝]',
`handle_time` datetime(0) NULL DEFAULT NULL COMMENT '处理时间',
`sku_img` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '商品图片',
`sku_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '商品名称',
`sku_brand` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '商品品牌',
`sku_attrs_vals` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '商品销售属性(JSON)',
`sku_count` int(11) NULL DEFAULT NULL COMMENT '退货数量',
`sku_price` decimal(18, 4) NULL DEFAULT NULL COMMENT '商品单价',
`sku_real_price` decimal(18, 4) NULL DEFAULT NULL COMMENT '商品实际支付单价',
`reason` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '原因',
`description述` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '描述',
`desc_pics` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '凭证图片,以逗号隔开',
`handle_note` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '处理备注',
`handle_man` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '处理人员',
`receive_man` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '收货人',
`receive_time` datetime(0) NULL DEFAULT NULL COMMENT '收货时间',
`receive_note` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '收货备注',
`receive_phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '收货电话',
`company_address` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '公司收货地址',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '订单退货申请' ROW_FORMAT = Dynamic;

购物车聚合

1
2
3
4
5
6
7
8
9
10
11
12
CREATE TABLE shopping_cart (
cart_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '购物车ID,使用自增长的整数作为主键',
user_id INT NOT NULL COMMENT '用户ID,购物车所属用户的ID',
product_id INT NOT NULL COMMENT '商品ID,购物车中的商品的ID',
quantity INT NOT NULL COMMENT '数量,购物车中商品的数量',
price DECIMAL(10, 2) NOT NULL COMMENT '价格,购物车中商品的单价,保留两位小数',
created_at DATETIME NOT NULL COMMENT '创建时间,购物车记录的创建时间',
updated_at DATETIME NOT NULL COMMENT '更新时间,购物车记录的最后更新时间',
FOREIGN KEY (user_id) REFERENCES users(user_id), -- 外键关联到用户表的用户ID
FOREIGN KEY (product_id) REFERENCES products(product_id) -- 外键关联到商品表的商品ID
);
-- 不完整

优惠卷实体

1
2
3
4
5
6
7
8
9
10
11
12
13
CREATE TABLE coupon (
coupon_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '优惠券ID,使用自增长的整数作为主键',
coupon_code VARCHAR(50) NOT NULL COMMENT '优惠券代码,限制最大长度为50个字符,不能为空',
discount DECIMAL(10, 2) NOT NULL COMMENT '折扣金额,保留两位小数',
start_date DATE NOT NULL COMMENT '优惠券生效日期',
end_date DATE NOT NULL COMMENT '优惠券失效日期',
max_usage INT NOT NULL COMMENT '最大使用次数',
current_usage INT DEFAULT 0 COMMENT '当前使用次数,默认为0',
created_at DATETIME NOT NULL COMMENT '创建时间,优惠券记录的创建时间',
updated_at DATETIME NOT NULL COMMENT '更新时间,优惠券记录的最后更新时间',
is_active BOOLEAN DEFAULT true COMMENT '是否激活,默认为true'
);

2.3.2 Spring Security 权限 完善

  • 自定义 数据库取出数据,然后进行权限验证的逻辑 ,参考mall

xx:xx:xx - 的鉴权 实现

  • 这个是 拿着token 进行权限验证的逻辑,不是授权token 的逻辑

  • 步骤:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    0、授权token的时候,缓存用户信息
    - 如何缓存:
    - 1、用户的token 可以拿到 role
    - 2、缓存策略组,所有人可以用
    - 3、缓存用户 可以访问的特殊路径
    - 4、缓存用户 不可以访问的特殊路径
    - 鉴权逻辑:
    1、拿到当前的current 路径,与角色拥有的策略组比较;
    2、通过:与 不可以访问的特殊路径 匹配
    3、不通过: 与 可以访问的特殊路径 匹配
    4、访问 result

    1、当拿着 token 进行验证的时候,从缓存中取出 用户的authorities(xx:xx 集合)。

    2、通过路径判断是否匹配,匹配,则放行,不匹配,网关拦截。

在Spring Security中,可以通过自定义实现来从数据库获取数据并进行权限验证的逻辑。可以通过以下步骤来实现:

自定义用户认证逻辑:
创建一个实现了UserDetailsService接口的类,用于从数据库中获取用户信息。在该类中,可以通过数据库查询获取用户的权限信息,并将其封装为UserDetails对象返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Service
public class CustomUserDetailsService implements UserDetailsService {

@Autowired
private UserRepository userRepository;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("User not found");
}
return new CustomUserDetails(user);
}
}

自定义权限验证逻辑:
创建一个实现了AccessDecisionVoter接口的类,用于自定义权限验证逻辑。在该类中,可以根据数据库中的权限信息和请求的路径进行匹配判断。

1
2
3
4
5
6
7
8
9
10
11
public interface AccessDecisionVoter<S> {
int ACCESS_GRANTED = 1;
int ACCESS_ABSTAIN = 0;
int ACCESS_DENIED = -1;

boolean supports(ConfigAttribute var1);

boolean supports(Class<?> var1);

int vote(Authentication var1, S var2, Collection<ConfigAttribute> var3);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class CustomAccessDecisionVoter implements AccessDecisionVoter<Object> {

@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}

@Override
public boolean supports(Class<?> clazz) {
return true;
}

@Override
public int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attributes) {
if (authentication == null) {
return ACCESS_DENIED;
}

for (ConfigAttribute attribute : attributes) {
if (attribute instanceof SecurityConfig) {
String requiredPermission = ((SecurityConfig) attribute).getAttribute();
// 根据数据库中的权限信息和请求的路径进行匹配判断
if (authentication.getAuthorities().stream().anyMatch(auth -> auth.getAuthority().equals(requiredPermission))) {
return ACCESS_GRANTED;
}
}
}

return ACCESS_DENIED;
}
}

配置自定义实现:
在Spring Security的配置文件中,配置自定义的用户认证逻辑和权限验证逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
private CustomUserDetailsService userDetailsService;

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.accessDecisionManager(accessDecisionManager())
.and()
.formLogin()
.and()
.httpBasic();
}

@Bean
public AccessDecisionManager accessDecisionManager() {
List<AccessDecisionVoter<? extends Object>> decisionVoters = Arrays.asList(
new WebExpressionVoter(),
new RoleVoter(),
new CustomAccessDecisionVoter()
);
return new AffirmativeBased(decisionVoters);
}
}

通过以上步骤,可以自定义从数据库获取数据并进行权限验证的逻辑。在自定义的权限验证逻辑中,可以根据数据库中的权限信息和请求的路径进行匹配判断。这样,就可以实现基于数据库的动态权限管理。

数据库表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89

-- ----------------------------
-- Table structure for ums_permission
-- ----------------------------
DROP TABLE IF EXISTS `ums_permission`;
CREATE TABLE `ums_permission` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`pid` bigint(20) DEFAULT NULL COMMENT '父级权限id',
`name` varchar(100) DEFAULT NULL COMMENT '名称',
`value` varchar(200) DEFAULT NULL COMMENT '权限值',
`icon` varchar(500) DEFAULT NULL COMMENT '图标',
`type` int(1) DEFAULT NULL COMMENT '权限类型:0->目录;1->菜单;2->按钮(接口绑定权限)',
`uri` varchar(200) DEFAULT NULL COMMENT '前端资源路径',
`status` int(1) DEFAULT NULL COMMENT '启用状态;0->禁用;1->启用',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`sort` int(11) DEFAULT NULL COMMENT '排序',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=19 DEFAULT CHARSET=utf8 COMMENT='后台用户权限表';

-- ----------------------------
-- Records of ums_permission
-- ----------------------------
INSERT INTO `ums_permission` VALUES ('1', '0', '商品', null, null, '0', null, '1', '2018-09-29 16:15:14', '0');
INSERT INTO `ums_permission` VALUES ('2', '1', '商品列表', 'pms:product:read', null, '1', '/pms/product/index', '1', '2018-09-29 16:17:01', '0');
INSERT INTO `ums_permission` VALUES ('3', '1', '添加商品', 'pms:product:create', null, '1', '/pms/product/add', '1', '2018-09-29 16:18:51', '0');
INSERT INTO `ums_permission` VALUES ('4', '1', '商品分类', 'pms:productCategory:read', null, '1', '/pms/productCate/index', '1', '2018-09-29 16:23:07', '0');
INSERT INTO `ums_permission` VALUES ('5', '1', '商品类型', 'pms:productAttribute:read', null, '1', '/pms/productAttr/index', '1', '2018-09-29 16:24:43', '0');
INSERT INTO `ums_permission` VALUES ('6', '1', '品牌管理', 'pms:brand:read', null, '1', '/pms/brand/index', '1', '2018-09-29 16:25:45', '0');
INSERT INTO `ums_permission` VALUES ('7', '2', '编辑商品', 'pms:product:update', null, '2', '/pms/product/updateProduct', '1', '2018-09-29 16:34:23', '0');
INSERT INTO `ums_permission` VALUES ('8', '2', '删除商品', 'pms:product:delete', null, '2', '/pms/product/delete', '1', '2018-09-29 16:38:33', '0');
INSERT INTO `ums_permission` VALUES ('9', '4', '添加商品分类', 'pms:productCategory:create', null, '2', '/pms/productCate/create', '1', '2018-09-29 16:43:23', '0');
INSERT INTO `ums_permission` VALUES ('10', '4', '修改商品分类', 'pms:productCategory:update', null, '2', '/pms/productCate/update', '1', '2018-09-29 16:43:55', '0');
INSERT INTO `ums_permission` VALUES ('11', '4', '删除商品分类', 'pms:productCategory:delete', null, '2', '/pms/productAttr/delete', '1', '2018-09-29 16:44:38', '0');
INSERT INTO `ums_permission` VALUES ('12', '5', '添加商品类型', 'pms:productAttribute:create', null, '2', '/pms/productAttr/create', '1', '2018-09-29 16:45:25', '0');
INSERT INTO `ums_permission` VALUES ('13', '5', '修改商品类型', 'pms:productAttribute:update', null, '2', '/pms/productAttr/update', '1', '2018-09-29 16:48:08', '0');
INSERT INTO `ums_permission` VALUES ('14', '5', '删除商品类型', 'pms:productAttribute:delete', null, '2', '/pms/productAttr/delete', '1', '2018-09-29 16:48:44', '0');
INSERT INTO `ums_permission` VALUES ('15', '6', '添加品牌', 'pms:brand:create', null, '2', '/pms/brand/add', '1', '2018-09-29 16:49:34', '0');
INSERT INTO `ums_permission` VALUES ('16', '6', '修改品牌', 'pms:brand:update', null, '2', '/pms/brand/update', '1', '2018-09-29 16:50:55', '0');
INSERT INTO `ums_permission` VALUES ('17', '6', '删除品牌', 'pms:brand:delete', null, '2', '/pms/brand/delete', '1', '2018-09-29 16:50:59', '0');
INSERT INTO `ums_permission` VALUES ('18', '0', '首页', null, null, '0', null, '1', '2018-09-29 16:51:57', '0');

-- ----------------------------
-- Table structure for ums_resource
-- ----------------------------
DROP TABLE IF EXISTS `ums_resource`;
CREATE TABLE `ums_resource` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`name` varchar(200) DEFAULT NULL COMMENT '资源名称',
`url` varchar(200) DEFAULT NULL COMMENT '资源URL',
`description` varchar(500) DEFAULT NULL COMMENT '描述',
`category_id` bigint(20) DEFAULT NULL COMMENT '资源分类ID',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=33 DEFAULT CHARSET=utf8 COMMENT='后台资源表';

-- ----------------------------
-- Records of ums_resource
-- ----------------------------
INSERT INTO `ums_resource` VALUES ('1', '2020-02-04 17:04:55', '商品品牌管理', '/brand/**', null, '1');
INSERT INTO `ums_resource` VALUES ('2', '2020-02-04 17:05:35', '商品属性分类管理', '/productAttribute/**', null, '1');
INSERT INTO `ums_resource` VALUES ('3', '2020-02-04 17:06:13', '商品属性管理', '/productAttribute/**', null, '1');
INSERT INTO `ums_resource` VALUES ('4', '2020-02-04 17:07:15', '商品分类管理', '/productCategory/**', null, '1');
INSERT INTO `ums_resource` VALUES ('5', '2020-02-04 17:09:16', '商品管理', '/product/**', null, '1');
INSERT INTO `ums_resource` VALUES ('6', '2020-02-04 17:09:53', '商品库存管理', '/sku/**', null, '1');
INSERT INTO `ums_resource` VALUES ('8', '2020-02-05 14:43:37', '订单管理', '/order/**', '', '2');
INSERT INTO `ums_resource` VALUES ('9', '2020-02-05 14:44:22', ' 订单退货申请管理', '/returnApply/**', '', '2');
INSERT INTO `ums_resource` VALUES ('10', '2020-02-05 14:45:08', '退货原因管理', '/returnReason/**', '', '2');
INSERT INTO `ums_resource` VALUES ('11', '2020-02-05 14:45:43', '订单设置管理', '/orderSetting/**', '', '2');
INSERT INTO `ums_resource` VALUES ('12', '2020-02-05 14:46:23', '收货地址管理', '/companyAddress/**', '', '2');
INSERT INTO `ums_resource` VALUES ('13', '2020-02-07 16:37:22', '优惠券管理', '/coupon/**', '', '3');
INSERT INTO `ums_resource` VALUES ('14', '2020-02-07 16:37:59', '优惠券领取记录管理', '/couponHistory/**', '', '3');
INSERT INTO `ums_resource` VALUES ('15', '2020-02-07 16:38:28', '限时购活动管理', '/flash/**', '', '3');
INSERT INTO `ums_resource` VALUES ('16', '2020-02-07 16:38:59', '限时购商品关系管理', '/flashProductRelation/**', '', '3');
INSERT INTO `ums_resource` VALUES ('17', '2020-02-07 16:39:22', '限时购场次管理', '/flashSession/**', '', '3');
INSERT INTO `ums_resource` VALUES ('18', '2020-02-07 16:40:07', '首页轮播广告管理', '/home/advertise/**', '', '3');
INSERT INTO `ums_resource` VALUES ('19', '2020-02-07 16:40:34', '首页品牌管理', '/home/brand/**', '', '3');
INSERT INTO `ums_resource` VALUES ('20', '2020-02-07 16:41:06', '首页新品管理', '/home/newProduct/**', '', '3');
INSERT INTO `ums_resource` VALUES ('21', '2020-02-07 16:42:16', '首页人气推荐管理', '/home/recommendProduct/**', '', '3');
INSERT INTO `ums_resource` VALUES ('22', '2020-02-07 16:42:48', '首页专题推荐管理', '/home/recommendSubject/**', '', '3');
INSERT INTO `ums_resource` VALUES ('23', '2020-02-07 16:44:56', ' 商品优选管理', '/prefrenceArea/**', '', '5');
INSERT INTO `ums_resource` VALUES ('24', '2020-02-07 16:45:39', '商品专题管理', '/subject/**', '', '5');
INSERT INTO `ums_resource` VALUES ('25', '2020-02-07 16:47:34', '后台用户管理', '/admin/**', '', '4');
INSERT INTO `ums_resource` VALUES ('26', '2020-02-07 16:48:24', '后台用户角色管理', '/role/**', '', '4');
INSERT INTO `ums_resource` VALUES ('27', '2020-02-07 16:48:48', '后台菜单管理', '/menu/**', '', '4');
INSERT INTO `ums_resource` VALUES ('28', '2020-02-07 16:49:18', '后台资源分类管理', '/resourceCategory/**', '', '4');
INSERT INTO `ums_resource` VALUES ('29', '2020-02-07 16:49:45', '后台资源管理', '/resource/**', '', '4');
INSERT INTO `ums_resource` VALUES ('31', '2020-08-24 13:43:54', '登录后获取用户信息', '/admin/info', '后台用户登录需要配置', '4');
INSERT INTO `ums_resource` VALUES ('32', '2020-08-24 13:44:37', '后台用户登出', '/admin/logout', '后台用户登出需要配置', '4');

自定义鉴权 – 修改匹配规则

  • 通过 继承,实现 AbstractAuthenticationProcessingFilter ## requiresAuthentication() 达到自定义是否执行 鉴权的方式
    采用

    1
    2
    3
    4
    5
    // AbstractAuthenticationProcessingFilter 
    // 如果匹配失败,可以跳过当前的 attemptAuthentication() 鉴权
    if (!this.requiresAuthentication(request, response)) {
    chain.doFilter(request, response);
    }
  • 可以修改 continueChainBeforeSuccessfulAuthentication 值,达到效果
    这个不采用

    1
    2
    3
    4
    // 如果为 true ,则会一直鉴权,直到成功
    if (this.continueChainBeforeSuccessfulAuthentication) {
    chain.doFilter(request, response);
    }

是的,可以通过继承AbstractAuthenticationProcessingFilter来为每个子类添加一个独立的requiresAuthenticationRequestMatcher

当您创建一个继承自AbstractAuthenticationProcessingFilter的子类时,可以在子类中重写requiresAuthentication方法,并在该方法中定义独立的requiresAuthenticationRequestMatcher。

例如,假设您创建了一个名为CustomAuthenticationFilter的子类,您可以按照以下方式为其添加一个独立的requiresAuthenticationRequestMatcher:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class CustomAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

public CustomAuthenticationFilter() {
super(new AntPathRequestMatcher("/custom/login", "POST"));
}

@Override
protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
// 自定义的判断逻辑
// 返回true表示需要进行身份验证,返回false表示不需要进行身份验证
// 可以根据请求的URL、请求方法、请求参数等进行判断
// 也可以调用父类的requiresAuthentication方法来使用默认的判断逻辑
return true;
}

// 其他方法和逻辑...
}
在上述示例中,CustomAuthenticationFilter继承自AbstractAuthenticationProcessingFilter,并在构造函数中使用AntPathRequestMatcher来定义了一个特定的URL匹配规则。然后,在requiresAuthentication方法中,您可以根据自己的需求实现自定义的判断逻辑。

通过这种方式,您可以为每个继承自AbstractAuthenticationProcessingFilter的子类添加独立的requiresAuthenticationRequestMatcher,以满足不同子类的不同身份验证需求。

如果您有进一步的问题,请随时提问

2.3.2 秒杀设计

负载均衡:

Niginx 负载均衡

单机限流:消息队列+sentinel

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
/**
* 秒杀商品
* 1.校验登录状态
* 2.校验秒杀时间
* 3.校验随机码、场次、商品对应关系
* 4.校验信号量扣减,校验购物数量是否限购
* 5.校验是否重复秒杀(幂等性)【秒杀成功SETNX占位 userId_sessionId_skuId】
* 6.扣减信号量
* 7.发送消息,创建订单号和订单信息
* 8.订单模块消费消息,生成订单
* @param killId sessionId_skuid
* @param key 随机码
* @param num 商品件数
*/
@Override
public String kill(String killId, String key, Integer num) throws InterruptedException {
// TODO 1.拦截器校验登录状态
long start = System.currentTimeMillis();
// 获取当前用户信息
MemberResponseVO user = LoginUserInterceptor.loginUser.get();

// 获取当前秒杀商品的详细信息
BoundHashOperations<String, String, String> skuOps = redisTemplate.boundHashOps(SECKILL_CHARE_KEY);
String jsonString = skuOps.get(killId);// 根据sessionId_skuid获取秒杀商品信息
if (StringUtils.isEmpty(jsonString)) {
// 这一步已经默认校验了场次+商品,如果为空表示校验失败
return null;
}
// json反序列化商品信息
SeckillSkuRedisTO skuInfo = JSON.parseObject(jsonString, SeckillSkuRedisTO.class);
Long startTime = skuInfo.getStartTime();
Long endTime = skuInfo.getEndTime();
long currentTime = System.currentTimeMillis();
// TODO 2.校验秒杀时间
if (currentTime >= startTime && currentTime <= endTime) {
// TODO 3.校验随机码
String randomCode = skuInfo.getRandomCode();// 随机码
if (randomCode.equals(key)) {
// 获取每人限购数量
Integer seckillLimit = skuInfo.getSeckillLimit();
// 获取信号量
String seckillCount = redisTemplate.opsForValue().get(SeckillConstant.SKU_STOCK_SEMAPHORE + randomCode);
Integer count = Integer.valueOf(seckillCount);
// TODO 4.校验信号量(库存是否充足)、校验购物数量是否限购
if (num > 0 && num <= seckillLimit && count > num) {
// TODO 5.校验是否重复秒杀(幂等性)【秒杀成功后占位,userId-sessionId-skuId】
// SETNX 原子性处理
String userKey = SeckillConstant.SECKILL_USER_PREFIX + user.getId() + "_" + killId;
// 自动过期时间(活动结束时间 - 当前时间)
Long ttl = endTime - currentTime;
Boolean isRepeat = redisTemplate.opsForValue().setIfAbsent(userKey, num.toString(), ttl, TimeUnit.MILLISECONDS);
if (isRepeat) {
// 占位成功
// TODO 6.扣减信号量(防止超卖)
RSemaphore semaphore = redissonClient.getSemaphore(SeckillConstant.SKU_STOCK_SEMAPHORE + randomCode);
boolean isAcquire = semaphore.tryAcquire(num, 100, TimeUnit.MILLISECONDS);
if (isAcquire) {
// 信号量扣减成功,秒杀成功,快速下单
// TODO 7.发送消息,创建订单号和订单信息
// 秒杀成功 快速下单 发送消息到 MQ 整个操作时间在 10ms 左右
String orderSn = IdWorker.getTimeId();// 订单号
SeckillOrderTO order = new SeckillOrderTO();// 订单
order.setOrderSn(orderSn);// 订单号
order.setMemberId(user.getId());// 用户ID
order.setNum(num);// 商品上来给你
order.setPromotionSessionId(skuInfo.getPromotionSessionId());// 场次id
order.setSkuId(skuInfo.getSkuId());// 商品id
order.setSeckillPrice(skuInfo.getSeckillPrice());// 秒杀价格
// TODO 需要保证可靠消息,发送者确认+消费者确认(本地事务的形式)
rabbitTemplate.convertAndSend("order-event-exchange", "order.seckill.order", order);
long end = System.currentTimeMillis();
log.info("秒杀成功,耗时..." + (end - start));
return orderSn;
}
}
}
}
}
long end = System.currentTimeMillis();
log.info("秒杀失败,耗时..." + (end - start));
return null;
}

2.3.4 聚合搜索 - 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
@Component
@Slf4j
public class SearchFacade {

@Resource
private PostDataSource postDataSource;

@Resource
private UserDataSource userDataSource;

@Resource
private PictureDataSource pictureDataSource;

@Resource
private DataSourceRegistry dataSourceRegistry;

// [编程学习交流圈](https://www.code-nav.cn/) 快速入门编程不走弯路!30+ 原创学习路线和专栏、500+ 编程学习指南、1000+ 编程精华文章、20T+ 编程资源汇总

public SearchVO searchAll(@RequestBody SearchRequest searchRequest, HttpServletRequest request) {
String type = searchRequest.getType();
SearchTypeEnum searchTypeEnum = SearchTypeEnum.getEnumByValue(type);
ThrowUtils.throwIf(StringUtils.isBlank(type), ErrorCode.PARAMS_ERROR);
String searchText = searchRequest.getSearchText();
long current = searchRequest.getCurrent();
long pageSize = searchRequest.getPageSize();
// 搜索出所有数据
if (searchTypeEnum == null) {
CompletableFuture<Page<UserVO>> userTask = CompletableFuture.supplyAsync(() -> {
UserQueryRequest userQueryRequest = new UserQueryRequest();
userQueryRequest.setUserName(searchText);
Page<UserVO> userVOPage = userDataSource.doSearch(searchText, current, pageSize);
return userVOPage;
});

CompletableFuture<Page<PostVO>> postTask = CompletableFuture.supplyAsync(() -> {
PostQueryRequest postQueryRequest = new PostQueryRequest();
postQueryRequest.setSearchText(searchText);
Page<PostVO> postVOPage = postDataSource.doSearch(searchText, current, pageSize);
return postVOPage;
});

CompletableFuture<Page<Picture>> pictureTask = CompletableFuture.supplyAsync(() -> {
Page<Picture> picturePage = pictureDataSource.doSearch(searchText, 1, 10);
return picturePage;
});

CompletableFuture.allOf(userTask, postTask, pictureTask).join();
try {
Page<UserVO> userVOPage = userTask.get();
Page<PostVO> postVOPage = postTask.get();
Page<Picture> picturePage = pictureTask.get();
SearchVO searchVO = new SearchVO();
searchVO.setUserList(userVOPage.getRecords());
searchVO.setPostList(postVOPage.getRecords());
searchVO.setPictureList(picturePage.getRecords());
return searchVO;
} catch (Exception e) {
log.error("查询异常", e);
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "查询异常");
}
} else {
SearchVO searchVO = new SearchVO();
DataSource<?> dataSource = dataSourceRegistry.getDataSourceByType(type);
Page<?> page = dataSource.doSearch(searchText, current, pageSize);
searchVO.setDataList(page.getRecords());
return searchVO;
}
}
}

《订单流程》

生成订单

img

img

  • 用户确认订单

img

img

商品信息:商品信息属于订单系统的上游端,所有订单都是从商品演进而来,从商品到订单,订单系统必须搜集相关的商品信息,包括店铺信息,商品id,商品规格,商品数量,商品价格。获取到的商品信息将在订单详情页内展示,形成订单信息后供仓库方便拣货,包装。

用户信息:用户信息包括购买用户的ID,收货人,收货地址,联系方式。有些平台的用户成长体系是基于用户对平台的活跃度来计算的,例如京东,它有会员等级及积分卡等类似的成长标识,此时获取到的用户信息除了普通的信息字段外,还需要获取该用户的等级,该次购买后所获得的积分,以及该用户所在等级能在该订单上扣除的优惠等信息,具体怎么操作取决于公司的业务方向。

金额信息:因为金额信息的特殊性,所以独立出来讲,理论上金额信息应归属商品信息。金额信息的特殊性在于其不止一种金额,其涉及到商品金额,优惠金额,支付金额。而优惠金额中涉及到的信息较复杂,像有自营和第三方入驻的电商平台,都会有商家优惠和跨店优惠,而这些优惠又分不同类型,例如现金扣减,消费券扣减,积分获取,礼品卡扣减,或者以上几种的组合使用。想要涉及好这一块内容,需要根据目前自己公司的业务情况,列出所支持的优惠类型,再枚举出各种组合下的优惠类型,才能保证流程的完整性。

时间信息:记录各个卡点下的时间,一是记录,二也是方便售后验证和客户分析。订单时间是根据订单状态改变而改变的,比如:我们常见的用户。

  • 下单未付款:即展示订单创建时间、下单时间;
  • 待发货状态:展示订单创建时间、下单时间、支付时间;
  • 待收货状态:展示订单创建时间、下单时间、支付时间、发货时间;
  • 交易完成状态:展示订单创建时间、下单时间、下单时间、支付时间、发货时间、完成时间;
  • 待退款状态:展示退款订单创建时间、申请退款时间;
  • 交易关闭-用户取消:展示订单创建时间、下单时间、用户取消时间;
  • 交易关闭-仅退款:订单创建时间、下单时间、支付时间、退款申请时间、退款成功时间;
  • 交易关闭-退货退款(包含部分仅退款):订单创建时间、下单时间、支付时间、交易完成时间、退款申请时间、退款时间。

订单信息:订单信息在订单系统最为核心,订单信息最重要的又是订单状态。

电商购物中,订单状态分别有以下几种:【待付款】、【待发货】、【待收货】、【待评价】、【交易完成】、【用户取消】、【仅退款】、【退货退款】。而我们一般会将后三种统一放在订单售后独立呈现,去方便平时商家操作的便捷性。

img

订单流程

订单流程是指从订单产生到完成整个流转的过程,其中包括正想流程和逆向流程。正向流程就是一个正常的网购步骤:订单生成–>支付订单–>卖家发货–>确认收货–>交易成功。

1、正向流程

img

订单生成:用户下单后,系统需要生成订单,此时需要先获取下单中涉及的商品信息,然后获取该商品所涉及到的优惠信息,如果商品不参与优惠信息,则无此环节。

接着获取该账户的会员权益(这里其实需要注意的是:优惠信息与会员权益是有区别的,就好比商品满减是优惠信息,新人立减是会员权益,一个是针对商品,另一个是针对账户)。

库存扣减是指可销售库存数量-1,严格来讲库存扣减目前分为两种:

  • 一种是下单减库存;
  • 另一种是付款减库存。

个人觉得中小创业者也许竞争者不比淘宝中的卖家,在电商这个存量市场,需要精细化的运营才能存活下来,如此说保证用户体验才是根本。所以我这里的观点是生成订单扣减库存,这种做法会避免用户支付成功商家却没货的情况。然后计算运费,订单生成成功。

支付订单:用户支付完订单后,需要获取订单的支付信息,包括支付流水号、支付时间等。支付完订单接着就是等商家发货,但在发货过程中,往往还有一种情况存在,很正常却也比较复杂,就是订单拆单。

  • 订单拆单分两种:一种是用户挑选的商品来自于不同渠道(自营与商家,商家与商家),此时就需要拆分订单,并分开结算,这里还涉及父子订单的说法,这里不再赘述。
  • 另一种是在SKU层面上拆分订单:不同仓库,不同运输要求的SKU,包裹重量体积限制等因素都需要将订单拆分。比如:商品A只在甲仓库有,商品B又只在乙仓库有,此时会将商品A与商品B拆分成两个订单。或者有些企业的做法是将商品A/B调拨到另外一个仓库统一发货,也方便了用户。

订单拆单看起来简单,其实里面涉及到底层的系统支持,如你需要对每一个仓库的货品进行相对准确的盘点,且做到实时同步(涉及到仓库精细化管理),对商品进行准确分类与摆放,对商品信息记录准确无误等。

这其中哪一模块都是一个浩大的工程,PM一般进入一家公司都会在原有(半成品)的基础上进行优化,大家不妨多思考一下底层业务,只有在底层做好精细化管理,才能支持线上丰富的用户需求。

商家发货:商家发货过程也有一个标准化的流程,上面也有讲到,订单拆分时会涉及到仓库间调拨,然后仓库会对商品进行打单、拣货、包装、交接快递配送。这套标准化流程如果优化好,也是一个大工程,这里不再赘述,建议大家看看库存与仓库管理方面的书籍,详细了解。

确认收货:商家发货后,就是等快递配送了,订单系统需要接入一些常用快递企业的接口,方便用户与商家在站内查询快递信息。

交易成功:收到货后,不是一个服务的结束,相反是一个服务的开始。订单系统需要在快递被签收后提醒用户对商品做评价,这里要注意,确认收到货不代表交易成功,交易成功是指在收到货X天的状态,此时订单不在售后的支持时间范围内。到此,一个订单的正向流程就算走完了。

目前我也没有研究过,不过我的经验告诉我订单系统对售后订单的处理并不比正产订单少,身为电商PM,我们的工作就是去优化这些流程,提高用户粘性。本身售后订单的出现,在某种程度上已经伤害到了用户,如果流程还一团糟的话,我们根本没有机会等到用户的复购。

2、逆向流程

img

取消订单:用户提交订单时,在跳转至支付前直接退出,此时用户原则上属于取消订单,因为还未付款,则比较简单,只需要将原本提交订单时扣减的库存补回即可。

支付失败:用户进行支付时退出,或者取消支付,我们将其列为支付失败状态,此时处理同上,将扣减的库存补回可销售库存即可。

付款后退款:用户支付成功后,商家还未发货,支持用户申请退款,此时如果仓库与客服是分离的,则需要先检查仓库是否已经发货,若已发货则应与客户沟通是否可以收到货后再进行退款,如果仓库还未发货,则可直接同意用户退款。或者企业接入菜鸟物流,实行截件功能,不过这种操作还不成熟,成本会比较大,不适合中小创业型公司。

缺货退款:用户支付成功后,商家发货时发现仓库缺货(如果提交订单扣减库存,则会减少缺货情况,为什么是减少而不是避免?因为仓库管理商品时没办法做到100%精准,所以信息有时候会不准确,导致线上的可销售库存显示有库存而仓库已经售空的状态),则需要与用户协商是否退款。

这个流程订单系统可以做到流程化、自动化,连接消息中心和仓库管理系统去实现,难点在于消息的实时性。我就遇到过在淘宝买过一件上衣,一天过去了,商家跟我说没货了,我当时杀人的心都有了。

待收货退款:这个问题目前还没有特别完美的解决方法,商家发了货之后,用户还未收到货,此时货在路上。

我曾经在一些交流群里提出过这个问题,大家的看法都不一样,大体上分为两种做法:

  • 一种是用户收到货后重新寄回;
  • 另一种是用户直接拒收包裹,包裹直接退回原地址。

我个人倾向于第一种,第一种比较灵活,因为用户未收到货就退款的原因一般与商品质量关系不大,所以如果允许用户直接拒收退回,相当于商家需要承担回退运费,而本身可能与商家并无太大关系。

另外一个原因就是,有些商家发货地址与退货地址不在同个地方,不支持直接退回。尽管如此,在到处强调用户体验的今天,增加用户的售后成本也是在消耗用户对平台的耐心,大家不妨去思考一下,有没有更好的解决方法。

订单推送

订单推送的触发依赖于状态机的改变,涉及到的信息包括:

  • 推送对象(用户、商家、仓库);
  • 推送方式(站内消息、push、短信、微信);
  • 推送节点(状态机)。

——–第一部分《数据库表驱动设计》——

基于业务实现数据库表设计,

1、模块划分; 2、需求设计; 3、数据库表设计; 4、具体业务逻辑分析;

leifengyang/gulimall - 码云 - 开源中国 (gitee.com)

gulimall: gulimall(谷粒商城)【笔记全,资料全,代码全,集群篇已完成】gulimall谷粒商城项目由雷丰阳老师教学 (gitee.com)

需求分析 + 业务逻辑

鉴权模块

如何进行分库分表下的RBAC模型设计,并能够

一般鉴权方式:携带用于的角色信息

此处鉴权方式:更具用户id进行权限验证,+ 权限查询

1
分库分表之下方案一:鉴权模块与用户模块共享一个数据库 (天才)

自定义认证方式–手机号/短信认证

(13条消息) Spring Security 解析(四) ——短信登录开发_BUG9_的博客-CSDN博客

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
1、自定义filter(这个filter 的父类为 AbstractAuthenticationProcessingFilter,不需要doFilter() )
,封装 authentication 参数
- 自定义attemptAuthentication
- UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
// 调用Manager的 authenticate()方法进行校验,
// 返回校验后的完整Authentication对象 ---或者自定义AbstractAuthenticationToken -- Token
return this.getAuthenticationManager().authenticate(authRequest);
2、自定义 AuthenticationManager : support方法、authenticate方法自定义认证方式
-
3、(不一定要)自定义service方法,loadxxx() xx
4、自定义AuthenticationToken --》 Authentication
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
private final Object principal;
}


http security 配置,配置对应的路径,进入对应的filter

整合第三方授权–QQ

resource owner(user)、authentication server(qq)、resource server (qq)、client(travel)

1
2
3
4
5
6
7
8
9
10
11
12
一、
请求时携带关键参数:
1、travel的 接收返回值code的 回调地址
2qq 的 appId、appKey : travel去 qq申请开发应用的认证;
3、跳转到qq进行 认证的地址:travel跳转到qq的页面,让resource owner安全认证(后面涉及到qq的 认证授权方式 -- 授权码模式)

响应的关键参数:
1、code : 授权码 -- qq 认证通过后分发
二、
携带code 获取 token,
三、
携带token 访问API获取用户开放信息

用户模块

用户种类:游客、普通用户、商家、管理员(库存管理员,商品管理员,积分策略管理员(商家与积分关系) – 策略模式,秒杀管理员)

用户权限验证:

用户登录 : 普通登录,短信登录,(自定义认证方式)用户第三方登录:微博/微信/qq登录 – oauth2(授权方式采用 Oauth2 框架自带的)

1
2
3
Oauth2实现鉴权服务器,配合api网关实现接口级权限验证功能。

spring security的验证方式,实现三种登录方式:UsernamePasswordFilter,SmsFilter,ThirdPartLoginFilter

用户注册:

1
2
3
几种需要跳转注册页面的逻辑: --- 弹出用户未登录提示框,是否需要跳转登录页面
1、在上方登录失败,返回用户未注册时;
2、访问某些需要登录权限的api -- 生成订单,收藏,

用户积分管理 , 用户积分获取详情信息

1
与具体业务想关联,存放在具体业务中 -- 其实与用户信息关系不大

用户收藏列表信息, 用户收藏 – 商品/店铺/文章/景点

1
2
业务逻辑:用户如何查收藏列表:用户个人中心 -> 收藏列表 -> 根据用户id查询表,获取收藏列表id -> 通过id查询收藏信息(每个信息都有两种展示状态 粗略 / 详情) -> 先暂时粗略 -> 再展示详情 -> 跳转到详情页(如何跳转:完整路径跳转-> 详情页展示逻辑)
问题:高效->将收藏列表id与收藏信息放在一个库下,就只用联表查询,不需要网络传输

用户获取点赞记录

用户浏览历史信息; — 同上

用户个人信息的CRUD

用户会话状态 – 用户登录状态保持

1
2
3
分布式session。+ JWT 方案对比:
首先确定一点 --- 这个项目采用JWT 做为回话保持的方案,不需要多余的开发步骤,而且适合分布式场景下的登录在 后台服务器传递

用户收货地址管理 – 订单生成

1
2
3
4
5
6
7
8
```



## 网关模块

限流

局部限流:对单一接口限流,通过消息队列限流
整体限流:

1
2
3
4
5

### 分析

Galileo 就是一个 API组合器 + 路由转发

API 网关 == 路由 + 额外职责:安全、认证、授权、限流、监控、缓存 (特殊:后端前置-为每一个客户端设计一个API网关)

路由:router == predict、filter、host 等规则 路由,可以添加负载均衡算法,完善路由功能

额外职责:
1、API组合器
2、客户端 – API版本管理
3、认证 授权 === 作为本项目重要职责之一,统一认证、授权转发,能够减少服务端的 RPC调用
4、协议转化 (暂时不知)
5、负载均衡 ==== filter转发,与路由规则相互配合
6、限流 ==== sentinel 总体限流,单个服务的特殊限流
7、熔断降级 === Hystrix、
– 断路器:CircuitBreaker 【 https://blog.csdn.net/boling_cavalry/article/details/119849436
– 降级
8、API版本控制 如何实现?
9、后端前置模式(BFF)
10、监控– 可观测性 ==== Spring Actuator

1
2
3
4
5
6
7
8
9
10
11
12
13



### 监控 -- Actuator







### 后端前置 (BFF)

API网关的”BFF”(Backends for Frontends)是一种设计模式,用于为不同的前端应用程序提供定制化的后端服务。BFF模式的核心思想是将后端服务的功能和数据按照前端应用程序的需求进行拆分和组合,以提供更高效、更灵活的服务。

具体来说,BFF模式的实现通常包括以下几个步骤:

  1. 根据前端应用程序的需求,确定需要提供的功能和数据。不同的前端应用程序可能有不同的需求,因此可以根据具体情况来确定需要提供的后端服务功能和数据。

  2. 在API网关中创建相应的BFF服务。API网关可以根据前端应用程序的需求,创建一个或多个BFF服务。每个BFF服务可以对应一个前端应用程序或一组具有相似需求的前端应用程序。

  3. 在BFF服务中聚合和转换后端服务的数据。BFF服务可以通过调用一个或多个后端服务的API来获取所需的数据,并进行聚合和转换,以适应前端应用程序的需求。这样可以避免前端应用程序需要多次请求后端服务,提高性能和效率。

  4. 提供定制化的API接口给前端应用程序。BFF服务可以根据前端应用程序的需求,提供定制化的API接口。这些接口可以根据前端应用程序的数据模型和业务逻辑进行设计,以提供更简洁、更直观的接口给前端应用程序使用。

通过使用BFF模式,API网关可以为不同的前端应用程序提供定制化的后端服务,减少前后端之间的通信次数和数据传输量,提高系统的性能和响应速度。同时,BFF模式还可以提供更好的灵活性和可维护性,使得前端和后端的开发团队可以更独立地进行开发和维护。

如果您需要更详细的信息或示例,请提供更具体的问题或查询。

1
2
3
4
5



这个不是BFF

API网关可以通过实现后端前置功能来提供一些额外的功能和增强服务的能力。后端前置功能是指在请求到达后端服务之前,在API网关层面对请求进行处理、转换或增强的功能。下面是后端前置功能的一些好处、特点和优势:

  1. 集中化处理:API网关作为系统的入口,可以集中处理所有的请求,统一管理和处理请求的逻辑。通过在API网关中实现后端前置功能,可以将一些通用的处理逻辑集中在网关层面,避免在每个后端服务中重复实现。

  2. 安全性增强:API网关可以在请求到达后端服务之前进行安全性检查和认证授权,以保护后端服务免受恶意请求和未经授权的访问。例如,可以在API网关中实现身份验证、访问控制、API密钥管理等功能,确保只有经过验证和授权的请求才能访问后端服务。

  3. 请求转换和协议适配:API网关可以将不同的请求转换为后端服务所需的格式和协议。例如,可以将RESTful请求转换为SOAP请求,或者将HTTP请求转换为消息队列的消息。这样可以解耦前端和后端服务之间的协议差异,提高系统的灵活性和可扩展性。

  4. 缓存和性能优化:API网关可以实现请求的缓存功能,将经常请求的数据缓存在网关层面,减少对后端服务的请求次数,提高系统的性能和响应速度。同时,可以通过在网关层面进行请求的聚合和优化,减少网络传输和后端服务的负载,提高系统的吞吐量和并发能力。

  5. 监控和日志记录:API网关可以对请求进行监控和日志记录,收集关键指标和日志信息,用于系统的监控、故障排查和性能优化。通过在网关层面进行统一的监控和日志记录,可以方便地追踪请求的流程和性能指标,提高系统的可观察性和故障排查的效率。

总的来说,后端前置功能通过在API网关层面实现一些通用的处理逻辑和增强功能,可以提高系统的安全性、性能、灵活性和可观察性,同时减轻后端服务的负担,提高整个系统的稳定性和可靠性。

1
2
3
4
5
6
7
8
9
10
11





### 熔断、降级方案

熔断:Request 请求处理

降级:Response 响应处理

API网关的熔断和降级是常见的微服务架构中的重要组件,用于提高系统的稳定性和可靠性。下面是熔断和降级的一些常见实现策略:

熔断实现策略:

  1. 错误阈值熔断:设置一个错误阈值,当接口的错误率超过阈值时,触发熔断机制,暂时停止对该接口的请求。可以通过监控接口的错误率来动态调整阈值。
  2. 超时熔断:设置一个超时时间,当接口的响应时间超过该时间时,触发熔断机制,暂时停止对该接口的请求。可以通过监控接口的响应时间来动态调整超时时间。
  3. 异常熔断:当接口返回特定的异常码或异常信息时,触发熔断机制,暂时停止对该接口的请求。可以根据接口的具体业务逻辑来定义异常条件。

降级实现策略:

  1. 限流降级:设置一个请求的并发数限制,当并发请求数超过限制时,对部分请求进行降级处理,返回预先定义的默认响应或错误信息。
  2. 功能降级:根据业务优先级,对某些功能进行降级处理,例如关闭某些不重要的功能模块或减少某些功能的复杂度,以保证核心功能的稳定性。
  3. 数据降级:当底层服务不可用时,可以返回缓存的旧数据或默认数据,以保证用户的基本体验。

需要注意的是,熔断和降级策略的具体实现方式会根据不同的API网关或框架而有所差异。一些常见的API网关,如Netflix的Zuul、Spring Cloud Gateway和Kong等,提供了内置的熔断和降级功能,可以通过配置文件或代码来实现相应的策略。

如果您正在使用特定的API网关或框架,建议查阅其官方文档或相关的技术资料,以了解如何在该网关或框架中实现熔断和降级策略。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32



API网关可以通过以下方式实现降级功能:

1. 响应缓存:API网关可以缓存一些常用的响应结果,当后端服务不可用或响应时间过长时,可以直接返回缓存的响应结果,避免对后端服务的请求。

2. 静态响应:API网关可以配置一些静态的响应结果,当后端服务不可用时,可以直接返回预先定义好的静态响应,以提供基本的功能或错误提示。

3. 降级服务:API网关可以配置一个备用的降级服务,当后端服务不可用时,可以将请求转发到降级服务,提供基本的功能或错误处理。

4. 错误处理:API网关可以捕获后端服务的错误响应,并根据配置的规则进行处理。例如,可以返回自定义的错误信息或重试请求。

熔断和降级是两个相关但不同的概念:

1. 熔断(Circuit Breaker):熔断是一种保护机制,用于防止故障的扩散和影响整个系统。当后端服务出现故障或响应时间过长时,熔断器会打开,停止向该服务发送请求,并快速失败。熔断器会在一段时间后尝试重新请求后端服务,如果请求成功,则关闭熔断器,恢复正常的请求流量。

2. 降级(Fallback):降级是一种应对后端服务不可用或性能下降的策略。当后端服务不可用或响应时间过长时,API网关可以返回预先定义好的静态响应或转发请求到降级服务,提供基本的功能或错误处理。降级可以保证系统的基本功能可用,但可能会损失一些高级功能或数据。

区别:
- 熔断是一种保护机制,用于防止故障的扩散和影响整个系统,而降级是一种应对后端服务不可用或性能下降的策略。
- 熔断是在请求级别上进行的,当后端服务出现故障或响应时间过长时,熔断器会停止向该服务发送请求,并快速失败。而降级是在响应级别上进行的,当后端服务不可用时,API网关会返回预先定义好的静态响应或转发请求到降级服务。
- 熔断是一种自动的机制,根据后端服务的状态自动打开或关闭熔断器。而降级是一种手动配置的策略,需要预先定义好降级的响应或配置降级服务。

需要根据具体的系统需求和架构选择合适的降级策略,以确保系统的可用性和性能。





### API 版本控制方案

API网关可以通过以下方式实现API版本控制:

  1. URL路径版本控制:在API网关中,可以通过在URL路径中包含版本号来实现版本控制。例如,可以将不同版本的API分别映射到不同的URL路径上,如/v1/users/v2/users。API网关根据请求的URL路径将请求路由到相应的API版本。

  2. 请求头版本控制:另一种常见的方式是使用请求头来指定API的版本。在请求头中添加一个自定义的头部字段,如X-API-Version,并将版本号作为字段的值。API网关根据请求头中的版本信息将请求路由到相应的API版本。

  3. 查询参数版本控制:还可以使用查询参数来指定API的版本。在URL中添加一个查询参数,如?version=1,将版本号作为参数的值。API网关根据查询参数中的版本信息将请求路由到相应的API版本。

  4. 请求体版本控制:在某些情况下,可以使用请求体中的数据来指定API的版本。例如,可以在请求体中添加一个字段来指定版本号,并根据请求体中的版本信息将请求路由到相应的API版本。

无论使用哪种方式,API网关都需要根据请求中的版本信息将请求路由到正确的API版本。这可以通过配置路由规则、使用路由表或使用API网关的路由功能来实现。同时,API网关还可以提供版本管理和控制的功能,如灰度发布、版本迁移等,以便更好地管理和控制API的版本。

需要注意的是,在实现API版本控制时,应考虑向后兼容性和版本迁移的问题,以确保系统的稳定性和可维护性。同时,还应与团队和开发者进行良好的沟通,明确版本控制的策略和规范,以便统一管理和使用API的不同版本。

1
2
3
4
5
6
7





### 基础功能汇总

微服务架构的API网关可以实现以下功能:

  1. 路由和转发:API网关作为系统的入口,可以根据请求的URL路径和HTTP方法将请求路由到相应的微服务实例。它可以根据配置规则将请求转发到不同的微服务,实现请求的分发和路由功能。

  2. 负载均衡:API网关可以通过负载均衡算法将请求均匀地分发到多个微服务实例上,以提高系统的性能和可靠性。它可以根据实时的负载情况动态调整请求的分发策略,实现负载均衡功能。

  3. 认证和授权:API网关可以集中处理认证和授权逻辑,对请求进行身份验证和权限验证。它可以与身份提供者(如OAuth、LDAP等)进行集成,验证请求的身份和权限,并根据配置规则决定是否允许请求访问相应的微服务。

  4. 安全性和防护:API网关可以提供一些安全性和防护功能,如请求的合法性检查、防止恶意请求、防止DDoS攻击等。它可以对请求进行过滤和验证,确保只有合法的请求能够访问微服务。

  5. 缓存和性能优化:API网关可以缓存一些常用的请求结果,以减少对后端微服务的请求次数,提高系统的性能和响应速度。它可以根据配置规则对请求进行缓存策略的管理和控制。

  6. 日志和监控:API网关可以记录请求和响应的日志信息,并提供监控和统计功能。它可以收集和分析请求的数据,监控系统的性能和可用性,并提供实时的监控指标和报警功能。

  7. API版本控制:API网关可以支持对不同版本的API进行管理和控制。它可以根据请求中的版本信息,将请求路由到相应的API版本,实现对API版本的控制和管理。

  8. 降级和容错:API网关可以根据后端微服务的状态和负载情况,进行降级和容错处理。当某个微服务不可用或负载过高时,它可以根据配置规则进行降级处理,返回默认值或错误信息,以保证系统的可用性。

API网关作为微服务架构的入口和门面,承担了很多重要的功能。它可以提供统一的接口和管理,简化了客户端和微服务之间的通信和交互,提高了系统的可维护性和可扩展性。

1
2
3
4
5
6
7



## 缓存的设计

需要用到缓存的地方

1、快速响应点赞的 点赞数的更改和查询:(对于一个文章,有点赞数、浏览量、收藏数、=== 可以采用 hash 结构)
2、秒杀商品的缓存问题:秒杀商品数据需要快速响应、快速修改问题,所以采用缓存 (一个秒杀场次 + 秒杀商品id 对应 一个商品信息)

3、购物车缓存问题;

1
2
3
4
5
6
7
8
9



设计Redis Key-Value 关系,最重要的一点就是 :多对多、还是 一对多的关系,对于特别对象(HyperLogLog、BloomFilter)只需要参照对象特性和使用特性 进行使用。



### 缓存需要考虑的问题

1、数据看 与 缓存 一致性问题:缓存双删、先删缓存再删数据看、先修改数据库再删缓存

  • 采用:先修改数据库再删缓存 – 保证缓存不会存在过时数据
    2、缓存同步问题:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59







    ## 购物车模块

    未登录的游客购物车, 临时购物车合并,

    购物车信息获取 – 商品模块,



    ## 店铺模块

    商家(另外一种用户)创建店铺,进行一类商品的售卖和管理

    店铺商品管理





    ## 商品模块

    商品上架、下架; — 商家负责商品上架下架(商品管理员负责审核商品)

    商品展示 – 展示信息,spu、sku问题,粗略展示 / 详情展示

    商品搜索 – 搜索条件,分类查询、关键词查询、

    商品收藏,统计收藏用户数量 – 用户收藏商品记录保存,



    ## 文章/帖子模块(商品评论)

    商品评论管理,

    评论内容 – 登陆后评论,购买后评价,

    商品评分,

    评论、点赞、转发(尝试整合第三方转发功能)

    帖子点赞数统计

    帖子点赞



    后续更新计划:可以发出视频



    ### 文档模型 - 存储文章

    自己采用的方案:
    用户信息:
    1、将用户信息的Id进行单独文档存储 (考虑同步更新问题 –废弃)
    2、存储用户信息Id,但是需要远程调用 用户信息(如何优化问题,暂时可以用 – 废弃)(RPC调用 损失一半性能)
    3、存储用户信息,但是每次对比当前用户信息 修改时间,如果修改时间不一致,则(后端/前端)发送更新文章用户信息请求。(需要完善)
  • 完善:采用 消息队列,当用户信息修改就发送 事件,更新 文章信息 (废弃 – 过于复杂,且文章不止一篇,更新操作过于离谱)
    4、redis 缓存单独存储用户登录信息、每次去缓存中读取相关信息(可行 – 需要关注同时在线人数问题)
    5、

点赞信息;
1、单独存储文章 点赞、收藏、浏览 数据信息,文档中存储 id进行查询
2、redis 进行缓存(考虑一致性 和 同步问题 – 可行)

1
2
3



对于像点赞数、阅读量和作者相关信息这样的实时变更信息,可以考虑以下几种存储方式:

  1. 在文档内作为字段存储:可以将点赞数、阅读量和作者相关信息作为文档的字段存储在文档内部。这样可以将相关信息与文章内容一起存储,方便查询和展示。但是,频繁的更新操作可能会导致整个文档的读取和写入,对性能产生一定影响。

  2. 单独存储为独立文档:可以将点赞数、阅读量和作者相关信息存储为独立的文档,与文章文档进行关联。这样可以将实时变更的信息与文章内容分离,减少对整个文档的读取和写入操作。但是,需要进行额外的关联查询,增加了一定的复杂性。

  3. 使用缓存:可以将点赞数、阅读量和作者相关信息存储在缓存中,如Redis。通过缓存可以提高读取和更新的性能,减少对数据库的访问。但是,需要考虑缓存与数据库之间的一致性和同步问题。

在存储文章的细节方面,可以考虑包含以下信息:

  • 文章ID:用于唯一标识文章。
  • 标题:文章的标题。
  • 作者ID:文章的作者ID,用于关联作者信息。
  • 发布日期:文章的发布日期。
  • 内容:文章的正文内容。
  • 标签:文章的标签,用于分类和检索。
  • 点赞数:文章的点赞数。
  • 阅读量:文章的阅读量。
  • 评论:文章的评论内容,包括评论者、评论时间、评论内容等。

需要根据具体的业务需求和性能要求来确定存储的细节。同时,可以根据实际情况进行性能测试和评估,选择合适的存储方式和数据模型。


针对存储用户信息的问题,有两种方案可以考虑优化:

  1. 缓存用户信息:可以使用缓存技术,如Redis,将用户信息缓存起来。当需要获取用户信息时,首先从缓存中查询,如果缓存中不存在,则再进行远程调用获取用户信息,并将获取到的用户信息存储到缓存中。这样可以减少对远程调用的频率,提高读取用户信息的性能。

  2. 异步更新用户信息:可以使用消息队列或异步任务来处理用户信息的更新。当用户信息发生变化时,后端或前端可以发送一个更新用户信息的消息或任务,然后由后台任务异步处理更新操作。这样可以避免每次对比用户信息的修改时间,减少对用户信息的频繁读取和更新请求。

需要注意的是,以上方案仅提供了一些优化思路,具体的实现方式需要根据系统架构和业务需求进行调整。同时,还需要考虑缓存与数据库之间的一致性和同步问题,以及异步更新操作的可靠性和处理速度。

对于存储用户信息的方案,可以考虑存储以下信息:

  • 用户ID:用于唯一标识用户。
  • 用户名:用户的用户名。
  • 邮箱:用户的邮箱地址。
  • 头像:用户的头像图片地址。
  • 注册时间:用户的注册时间。
  • 最后登录时间:用户的最后登录时间。
  • 其他个人信息:根据业务需求,可以存储其他用户相关的信息,如性别、年龄、地址等。

需要根据具体的业务需求和隐私安全考虑,确定存储的用户信息。同时,需要根据系统的实际情况进行性能测试和评估,选择合适的存储和优化方案。

1
2
3
4
5
6
7
8
9
10
11
12
13





## 评论模块(帖子评论)

### 评论

评论帖子,评论评论 – 区别:位于一级评论,二级评论

评论区设计:按时间排序,按点赞数排序

问题一:评论表设计方案
1、评论 / 回复表
2、统一评论表
3、评论表(维持评论布局和关系) + 内容表 – 管理评论内容展示(可以比被应用,转发,)
内容:评论文章/商品Id、评论id、被评论(文章/人)id、

考虑问题:
1、评论的布局、搜索问题:按照 时间 结构搜索 + 按照bilibili – 同一个根评论搜索
算法问题:sort -
2、评论人信息: member信息 — 需要展示
3、评论内容信息: context

1
2
3
4
5



评论reply

1、parentId – 当前回复的人 Id @xxx
2、rootId – 当前回复内容根 (没有回复别人,回复文章的人 没有rootId,可以当作别人的rootId)
3、replyId – 评论
4、context ID 内容

1
2
3
4
5
6
7





评论内容context

1、内容
2、被评论人 信息 @XXX - 通过id 获取
2、跳转路径

1
2
3
4
5
6
7
8
9





### 点赞

评论点赞数统计 、 评论点赞

1、点赞功能设计:使用缓存减少数据库查询次数,应对快速响应问题
方案一:从文章角度
判断点赞状态:redis设计 hset key(articeId) userid1,userid2, userid3 — 利用set特性,快速判断点赞状态
获取点赞数:hmember 获取当前点赞数
方案二:从用户角度
判断点赞状态:redis设计:hset key(userId) articeIdid1,articeIdid2,articeIdid3….
用于快速判断当前用户的点赞信息(受到在线用户数量的影响)

方案三:用户 + 系统
    点赞数统计 + bitMap判断
方案二 可以用来帮助用户快速判断当前点赞信息,便于前端展示渲染
方案三 补充部分:可以用于快速统计点赞信息,以及判断

2、如何快速判断用户时候已经点赞问题: (bloom filter – 存在则一定为true,不存在可能为true)
方案一:从文章角度,布隆过滤器(Bloom Filter)
方案二:从用户角度,用户每次访问时,将当前用户的点赞记录存入bloom filter,用户判断某些文章是否已经点赞/收藏等 需要显示操作
(只存储点赞/收藏状态和数据)
布隆过滤器设计:(收藏/点赞)key(userId) articleId,

3、保证点赞数持久化存储:
当前帖子的数据库需要存储点赞数,使用xxl-job分布式定时框架,定时刷新

总结:
选取方案二。
原因:
对于当前系统而言,用户为访问当前系统的最小颗粒度;文章锁包含的用户点赞数据过于冗杂,且不满足当前用户访问需求;如果我们要判断用户是否点赞文章,需要存储所有文章的点赞用户的布隆过滤器数据,冗余度太高;
使用方案二,我们只关注当前用户的点赞状态,当前用户点赞文章数量也比较少,存在时间也是有用户登录查询阶段,当系统访问量不高的时候,redis存储数据为用户级别;而且redis存储数据能够针对的对当前用户进行定制化处理,减少荣誉查询和无关数据的存储。
业务流程:
用户登录 -> 缓存用户点赞/收藏文章id (分散在各个模块,需要消息队列处理) -> 当用户访问文章获取状态的时候,先查询布隆过滤器 -> 如果布隆过滤器中没有,则查询数据库 ->

更改:选取方案三 == 方案二 set。+ bitmap
优点:1、用户文章访问状态的快速响应、 – 用户角度
2、bitmap快速判断是否点在,进行自增、文章存储 位图信息,可以快速判断用户是否可以点赞 – 文章角度
好像功能冗余:二选一?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17







## 订单模块

![在这里插入图片描述](https://img-blog.csdnimg.cn/5b3b81bac3ba4c899ca964edb9b3ad19.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBASEdXNjg5,size_20,color_FFFFFF,t_70,g_se,x_16#pic_center)

订单生成(幂等性处理) — 费用计算:邮费-地址,优惠计算,



### RabbitMQ 的消息队列 - 事务 - 订单

业务:订单下单-支付

  • 秒杀业务订单,先下单(锁库存),再支付
  • 正常订单:
    参考淘宝–
    1、结算 == (cart模块)
    2、提交订单(锁定库存)(库存- stock模块)
    2.0 获取商品信息(product 模块) + 收货地址(member模块)
    2.1 获取优惠券、优惠信息(coupon模块) + 计算价格(order模块)
    2.2 未提交,不付款(订单- order模块)
    3、同步付款–成功 (订单模块)
    4、异步付款:30min过期
    4.1 解锁库存
    4.2 修改订单状态
    5、不付款,订单结束

消息队列设计:

消息有哪些?
1、支付消息

  • 支付成功,发送消息 ==》扣除库存
  • 支付失败消息 ==》
  • 支付超时消息 ==》 恢复锁定库存
    (如果支付成功后,支付超时;所以恢复库存的时候需要判断)

消息如何流转?
1、支付成功消息 – 目的:扣减库存 、修改订单状态

  • 谁发送:order- 订单模块负责控制 支付功能
  • 流转规则:一个发送,一个监听
  • 谁接受:
    order:修改订单状态–同步修改,所以可以不用发送消息
    ware:扣减锁定库存(修改状态) – 同步修改,可以不用发送消息
    (不需要 消息队列)

2、支付超时消息: – 目的:解锁库存,修改订单状态

  • 谁发送:order-创建订单成功之后,
  • 流转规则:(创建)发送延迟消息(死信队列)
  • 谁接收:
    ware - 库存解锁
    order - 订单状态修改(异步修改30min)

3、如何解决问题:虽然 支付成功,但是 支付超时
3.1 先支付,再超时

  • 支付超时主要是 库存和订单 状态修改
  • 如果先支付成功,则
    订单状态已经修改
    库存状态,延迟修改 == 改为同步修改

3.2 先超时,再支付

  • 由于超时 使用消息队列 修改状态,有一定延迟,则可能虽然超时,但是支付成功~
  • 如何解决?
    order模块:- 支付的时候检查时间问题,如果已经超时就默认更新 订单状态
    - 然后,当消息到达,保证订单状态的幂等性;
    ware模块:不用处理~~
    1
    2
    3
    4
    5
    6
    7





    ### 考虑问题

    在商品订单生成过程中,需要注意以下几个问题,以确保业务的安全性和完整性:
  1. 并发冲突:在高并发场景下,多个用户同时提交订单可能会导致并发冲突。为了解决这个问题,可以使用乐观锁或悲观锁来控制并发访问,保证订单生成的原子性和一致性。

  2. 库存扣减:在生成订单之前,需要进行库存扣减操作。为了避免超卖问题,可以在扣减库存之前先检查库存数量,确保库存充足。可以使用数据库事务来保证库存扣减和订单生成的原子性。

  3. 幂等性设计:为了避免重复生成订单,需要设计幂等性机制。可以通过生成唯一的订单号或使用分布式锁等方式来保证订单生成操作的幂等性。

  4. 价格校验:在生成订单之前,需要校验商品的价格是否与用户提交的价格一致。防止用户通过篡改价格等方式进行恶意操作。

  5. 用户身份验证:在生成订单之前,需要对用户进行身份验证,确保只有合法的用户才能生成订单。可以使用用户认证和授权机制来验证用户身份。

  6. 订单状态管理:生成订单后,需要对订单状态进行管理。包括订单的支付状态、配送状态等。可以使用状态机来管理订单的状态转换,确保订单状态的一致性和可追溯性。

  7. 异常处理:在订单生成过程中,可能会出现各种异常情况,如库存不足、支付失败等。需要对这些异常情况进行合理的处理,包括给用户友好的提示、进行日志记录等。

  8. 数据安全性:订单中可能包含用户的敏感信息,如姓名、地址、手机号码等。需要对这些信息进行合理的加密和保护,确保数据的安全性。

  9. 业务监控和告警:为了及时发现和解决订单生成过程中的问题,可以设置业务监控和告警机制。监控订单生成的关键指标,如订单生成速度、成功率等,及时发现异常情况并进行处理。

以上是在商品订单生成过程中需要注意的一些问题,具体的实现方式和技术选择可以根据项目需求和团队技术栈进行决策。如果需要更详细的信息,可以使用搜索引擎进行进一步的调研。

在商品订单生成过程中,还有一些其他需要注意的问题:

  1. 平台费用计算:如果平台需要收取一定的手续费或服务费,需要在订单生成过程中计算并记录相应的费用。

  2. 优惠券和促销活动:如果用户使用了优惠券或参与了促销活动,需要在订单生成过程中计算并应用相应的优惠或折扣。

  3. 支付方式选择:在生成订单后,用户需要选择支付方式进行支付。需要提供多种支付方式的选择,并确保支付流程的安全性和稳定性。

  4. 配送方式选择:根据用户的选择,提供多种配送方式供用户选择,并记录用户的配送方式以便后续处理。

  5. 订单确认和通知:在生成订单后,需要向用户发送订单确认信息,并提供订单详情和支付方式等相关信息。可以通过短信、邮件、推送等方式进行通知。

  6. 订单超时处理:如果用户在一定时间内未完成支付,需要对订单进行超时处理,释放库存并取消订单。

  7. 订单退款和售后:在订单生成后,用户可能需要进行退款或售后操作。需要提供相应的退款和售后流程,并记录相关信息以便后续处理。

  8. 数据统计和分析:对生成的订单数据进行统计和分析,包括订单数量、销售额、用户行为等,以便进行业务分析和决策。

  9. 日志记录和审计:对订单生成过程中的关键操作进行日志记录,包括用户操作、系统异常等,以便进行审计和故障排查。

以上是在商品订单生成过程中需要注意的一些问题,具体的实现方式和技术选择可以根据项目需求和团队技术栈进行决策。如果需要更详细的信息,可以使用搜索引擎进行进一步的调研。

1
2
3
4
5
6
7





创建订单 - 锁定库存,优惠计算、邮费计算、支付优惠(忽略)

库存锁定– 发送需要锁定商品信息 -> wms模块api -> 判断是否锁定库存成功 (涉及到分布式事务一致性问题)
邮费计算 – 需要获取用户的收获地址之后再进行计算 -> 策略者模式,计算邮费
优惠计算 – 每个商品的优惠信息不一样, cms优惠模块保存商品优惠信息,在用户查看、添加购物车、支付阶段可能优惠信息有所变化, 需要实时查询;发送商品信息 -> cms根据策略计算优惠价格,并返回详情信息 -> oms订单模块记录商品价格详细 : 原件、成交价(活动价)、优惠卷信息、优惠价格均摊、结算价 (都是单价)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

![img](https://pic4.zhimg.com/80/v2-9580bfa3a2adcdef3e21c399ac344133_1440w.webp)

![img](https://pic3.zhimg.com/80/v2-da8b2230cd3c76f7e6d6400156b213f6_1440w.webp)

超时关单

支付订单 -



订单退款 – 退一件,全退、

[做电商: 必须知道这些订单退款逻辑 (附流程图) | 人人都是产品经理 (woshipm.com)](https://www.woshipm.com/pd/5134731.html)



## 优惠模块

### 优惠

[一文搞懂电商订单价格计算逻辑 - 知乎 (zhihu.com)](https://zhuanlan.zhihu.com/p/89951053)

策略者模式 – 每种优惠卷的算法策略不同,需要进行退款方式也不同

计算优惠,记录优惠价平摊(用于退款),退款方式 – 退一个、退一群

优惠券叠加 – 同类只能选一个,不同类选多个

无法平摊优惠价问题:(3.33333 -> 3.33,最后补上)
总会遇见无法平均分摊的问题、方法只有一个就是全部商品退款完毕的的时候把无法分摊的几分钱补上、

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68

**营销优惠 + 支付优惠**

营销优惠:优惠卷, 秒杀(活动价)

支付优惠:根据不同平台的优惠策略(淘宝–淘宝币,京东 – 京东豆,会员优惠,购买补贴淘宝币 —-=== 暂时不考虑)



营销优惠:商家的每个商品的优惠策略进行设置 – 满减,商品优惠卷发放,活动价设置



支付优惠:又是一套优惠策略 – 例如:用户购买返积分策略,积分抵消支付价格策略



### 秒杀

秒杀活动管理 –

1、用户可以提前订阅秒杀活动、

2、管理员上架秒杀活动- 秒杀场次(每日几场,每场开始结束时间)

3、商家基于秒杀活动 - 上架秒杀商品 (需要记录秒杀优惠信息,支付时可以选择**营销优惠** – 获取用户的优惠券计算价格)

获取秒杀信息





## 秒杀模块

[万字超详解秒杀系统! - 知乎 (zhihu.com)](https://zhuanlan.zhihu.com/p/433618121)



### 分布式锁

`这里的问题(业务):保存在redis 中,能够通过 lua 脚本实现原子操作`

1、检测用户秒杀个数 问题

- --限制单个用户 可以在 redis + lua 的脚本中实现判断逻辑,比如 redis hash 判断当前用户是否购买超过个数,超过就直接 return,没有超过就 扣减 (适合单机)
- redis + lua 如何实现分布式锁 == redisson 客户端 整合 redis集群,想所有节点发送 lua 脚本,实现reids + lua脚本的分布式方式,能够满足 业务需求

**很巧妙的方案**

2、直接使用分布式锁

- 锁定的粒度问题



`对于业务 不在redis中的`

1、只能用分布式锁保证了 --- 具体业务具体分析





### 定时上架 秒杀商品

问题描述:分布式情况可能出现多个service 同时进行商品上架,**一个商品被上架多次**

思路:
如何保证一个商品被上架一次,如何保证
解决:

1
2
3
4
5
6
7
8
9







### 秒杀商品信息

1、

1
2
3
4
5
6
7
8
9
10
11



(秒杀模块 只负责秒杀逻辑,优惠逻辑以及秒杀商品入数据库[上架]、锁定库存逻辑 都在优惠模块

秒杀模块直接获取优惠模块保存的秒杀信息,上架到Redis)

直接生成秒杀订单,快速响应 – 只记录秒杀商品临时信息

秒杀商品管理 —- 商家设置秒杀商品,选择场次、数量、商品、原来价格、优惠价格 / 优惠折扣

商家进入后台管理系统,将

1
2
3
4
5



商品上架 — 后台倒计时秒杀商品,定期扫描秒杀商品进行后台上架 – 缓存预热

1
2
3
4
5



### 问题分析

秒杀商品的实现涉及到多个方面的考虑和设计,下面逐一介绍相关内容:

  1. 提前上架:为了确保秒杀活动的顺利进行,需要提前将秒杀商品上架,使用户可以在秒杀开始前浏览和选择商品。

  2. 秒杀逻辑:秒杀逻辑包括用户提交秒杀请求、库存扣减、生成订单等步骤。在秒杀开始时,用户可以提交秒杀请求,服务器接收请求后进行库存扣减,如果库存足够,则生成订单;如果库存不足,则返回秒杀失败的信息。

  3. 幂等性设计:为了避免重复秒杀和重复扣减库存的问题,需要设计幂等性机制。可以通过生成唯一的秒杀订单号或使用分布式锁等方式来保证秒杀操作的幂等性。

  4. 防抖功能:为了防止用户频繁点击秒杀按钮导致的恶意请求和服务器压力过大,可以在前端实现防抖功能,限制用户在一段时间内只能提交一次秒杀请求。

  5. Token校验URL:为了防止恶意请求和保护秒杀接口的安全性,可以使用Token校验URL的方式来限制只有具有有效Token的请求才能进行秒杀操作。

  6. 超卖问题:秒杀活动可能会面临超卖问题,即库存不足的情况下仍然接受了多个用户的秒杀请求。为了解决这个问题,可以在秒杀请求到达服务器时进行库存检查,并在库存不足时返回秒杀失败的信息。

  7. 快速响应:秒杀活动通常会吸引大量用户参与,为了保证用户体验,服务器需要快速响应秒杀请求,避免因请求过多而导致的延迟和堵塞。

  8. 限流:为了保护服务器的稳定性和防止恶意攻击,可以实现限流机制,限制单位时间内的秒杀请求数量,防止服务器过载。

  9. 及时告知秒杀结束:在秒杀活动结束时,需要及时向用户告知秒杀已结束的信息,避免用户继续提交秒杀请求。

以上是秒杀商品实现中的一些关键考虑和设计,具体的实现方式和技术选择可以根据项目需求和团队技术栈进行决策。如果需要更详细的信息,可以使用搜索引擎进行进一步的调研。
除了上述提到的问题,秒杀业务还需要考虑以下几个方面的问题:

  1. 秒杀商品的数量限制:为了控制秒杀活动的规模和保证用户的公平性,可以设置每个用户在秒杀活动中可以购买的商品数量限制。

  2. 秒杀活动的时间限制:需要确定秒杀活动的开始时间和结束时间,并在活动结束后禁止用户继续提交秒杀请求。

  3. 秒杀商品的展示和推广:为了吸引用户参与秒杀活动,需要在前端页面中展示秒杀商品的信息,并进行相应的推广和宣传。

  4. 秒杀订单的处理:生成秒杀订单后,需要及时处理订单,包括订单支付、订单配送等环节。同时,需要考虑订单的售后服务和退款处理。

  5. 秒杀活动的监控和统计:需要对秒杀活动进行监控和统计,包括用户参与人数、秒杀成功率、服务器负载等指标,以便进行活动优化和性能调优。

  6. 秒杀活动的安全性:为了防止恶意攻击和作弊行为,需要加强秒杀活动的安全性,如防止重放攻击、验证码验证等。

  7. 秒杀活动的营销策略:可以结合秒杀活动进行一些营销策略,如限时折扣、优惠券发放等,以提高用户参与度和购买意愿。

  8. 秒杀活动的容错处理:在秒杀活动中,可能会出现各种异常情况,如网络故障、系统崩溃等。需要进行容错处理,保证系统的稳定性和可用性。

  9. 秒杀活动的后续分析和总结:秒杀活动结束后,需要对活动进行分析和总结,包括活动效果评估、用户反馈收集等,以便进行改进和优化。

以上是秒杀业务中需要考虑的一些问题,具体的实现方式和技术选择可以根据项目需求和团队技术栈进行决策。如果需要更详细的信息,可以使用搜索引擎进行进一步的调研

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37







## 库存模块

锁定库存

库存回滚

商品库存CRUD



## 积分模块

商品积分信息管理,– 为每个商品分配积分

用户积分信息管理 – 用户积分信息CRUD

额外积分获取管理 – 通过其他方式(奖励)获取积分



## 搜索模块= ElasticSearch + MySQL

1、关键词提醒 -- 输入关键词的时候异步搜索 ES 获取完整检索词

2、文章上传 + 检索 功能 ??? 文章是否需要用ES进行检索问题

3、商品上传 检索功能

4、ES实现聚合搜索 — 文章 // 商品 // 用户 // xxx等需要ES进行检索时 : 只用输入一次关键词,后面可以直接切换 tag进行再次检索

分析:如何通过设计模式更加高效的实现该操作?
1、搜索方式有 ES + MySQL两种,所以搜索需要抽象为接口方法
2、更加tag选择搜索的业务逻辑
3、通用ES进行搜索的时候,tag可以作为搜索条件进行检索的过滤问题
4、好像没了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35





# 数据库表设计



![img](https://img-blog.csdnimg.cn/img_convert/143e5f57108634fea45ffc9f20f5963f.png)



## 新思路

参考Oceanus设计,将id设计为自增,单独一个serialId 作为唯一标识







四件套:id, create_time, deleta_statue, updata_time

**根据需求进行设计**

## 用户模块

### 用户表

属性:role, username, nickname,性别,头像,

用于存储用户的基本信息:

分析:在

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55





### 用户地址表

用户存储用户地址







**用户收藏表 – 商品/店铺/文章/景点**

1、(在对应收藏信息下建表)

2、根据领域模型设计,关系表按照业务划分,属于用户的业务,与文章、商品无关,则放在当前用户模块



## 店铺模块

### 店铺信息表

包含店铺名,定位,自我介绍等等完整的店铺详情信息







## 商品模块

### 用户商品收藏表

用于存储用户收藏商品id 与 用户id 关系表,也能快速统计当前商品的收藏数量



### 店铺商品关系表

根据店铺,获取商品信息 – 感觉应该放在商品库中9



### SPU 和 SKU

SPU : iPhone 12、Samsung Galaxy

SKU:白色 12G + 512G 的 iPhone 12、黑色 6G + 256 G 的 iPhone 12

在电子商务领域,SKU和SPU是常用的商品标识概念。

  1. SKU(Stock Keeping Unit):SKU是库存单位的缩写,用于唯一标识一个具体的商品。每个SKU通常对应着一个具体的产品变体,它可以包含一组唯一的属性和特征,如颜色、尺寸、款式等。SKU是用于库存管理和销售跟踪的重要标识。

举例说明:
假设有一家服装店,他们销售一款T恤衫,这款T恤衫有不同的颜色和尺码可选。对于每个颜色和尺码的组合,可以为其分配一个唯一的SKU。例如,红色XL尺码的T恤衫可以分配一个SKU为”TS001-RD-XL”,蓝色M尺码的T恤衫可以分配一个SKU为”TS001-BL-M”。每个SKU都代表着不同的产品变体。

  1. SPU(Standard Product Unit):SPU是标准产品单元的缩写,用于标识一组具有相同功能和属性的商品。SPU通常是指一种产品的基本型号或基准型号,它可以包含多个SKU。SPU用于商品分类、展示和描述,方便消费者对商品进行比较和选择。

举例说明:
继续以服装为例,假设这家服装店还销售其他款式的T恤衫,如短袖、长袖、印花等。这些不同款式的T恤衫可以被归类为同一个SPU,例如”TS001”。每个SPU代表着一种基本款式的T恤衫,而不同的SKU则代表着不同的颜色和尺码的变体。

举例来说,假设有一家电子产品商店,他们销售手机。不同品牌和型号的手机可以被归类为不同的SPU。例如,iPhone 12、Samsung Galaxy S21和Huawei P40可以分别作为不同的SPU。每个SPU代表着一种手机的基本型号,它们具有相似的功能和属性。

在电商平台上,通常以SPU为单位进行商品展示和描述,而SKU则用于具体的库存管理和销售跟踪。通过SPU和SKU的组合,可以实现对商品的灵活管理和销售。

总结:
SKU是用于唯一标识具体商品变体的库存单位,而SPU是用于标识一组具有相同功能和属性的商品的标准产品单元。SKU用于库存管理和销售跟踪,而SPU用于商品分类和展示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17





## 点赞/收藏/踩 – 状态模块

### 商品/店铺收藏关系表

### 用户文章收藏关系表

### 用户文章点赞关系表

### 用户评论点赞关系表

存储用户于文章的关系,点赞/收藏等操作状态 – 只存储有关系数据,没有关系不存储

问题:各种关系 —- 收藏文章 为 文章模块数据, 帖子为文章/评论文章,评论就是评论

点赞对象:文章,评论,帖子,
收藏对象:一类:店铺,商品;二类:文章,帖子
理清对象关系:用于设计redis
需要判断是否点赞,是否收藏,分为两种redis的(布隆过滤器 – 数据量不大,不用bloom filter)进行设计

方案一;√ — 简单,易管理
表的存放位置;
收藏:文章收藏关系表 – 为文章模块的库,商品收藏关系表 – 为商品模块库,店铺收藏关系表 – 为店铺模块库;
点赞;文章点赞关系表 – 为文章模块的库,评论点赞关系表 – 为评论模块的库
用户收藏布隆过滤器
用户点赞布隆过滤器
通过消息队列进行调度 文章, 商品,店铺,评论模块

方案二:比较麻烦
表存放位置;
单独抽象一个模块,用于关系关系状态表示;
流程:
展示流程:每次查询布隆过滤器也能获取对应的状态数据 -> 通过状态数据修改返回状态 -> 前端通过状态展示信息
添加流程:需要添加关系表 -> 判断bloom filter是否存在对应关系 -> 存在,添加失败;不存在,修改数据库信息,添加redis,添加bloom filter;
删除流程:删除添加关系表 -> 判断bloom filter是否存在对应关系 -> 存在,删除bloom filter,删除redis,删除数据库;不存在,修改失败








# 收获记录

## 一:sss

收藏表存放位置:与收藏的信息表存放在一个库里

总结:将于具体业务模块关联的信息表/ 关系表,一律存放在业务模块的数据库表中,减少网路通讯的影响。

travel-逸游天下
http://example.com/2023/09/02/travel-readme/逸游天下--业务/
作者
where
发布于
2023年9月2日
许可协议