该仓库未声明开源许可证文件(LICENSE),使用请关注具体项目描述及其代码上游依赖。
克隆/下载
贡献代码
同步代码
取消
提示: 由于 Git 不支持空文件夾,创建文件夹后会生成空的 .keep 文件
Loading...
README

hm_mall

翰墨商城 该商城是基于乐优商城进行开发的 主要包括 商品分类,品牌管理,文件上传下载,商品搜索服务,商品页面服务,结合RSA的授权中心

使用到的技术

    该项目是基于springboot,sping,mybatis的通用mapper框架进行开发;
    通过Elasticsearch 实现商品搜索服务;基于rabbitmq消息队列实现商品服务与搜索服务和静态页面的同步管理;
    通过fastDFS实现文件的上传下载;使用redis实现存储用户的临时验证码;
    `基于JWT + RSA非对称加密 实现用户的认证授权;

商品的品牌管理和查询

商品分类表设计注意点:商品分类是分层级的,如何区分儿子和爹的关系,就是在表中添加一个parentID字段
通用mapper会把对象中的非空属性作为查询条件
问题整体解决思路:
    通过前端请求,分析请求方式(post,get..),请求路径(http://api.leyou.com/api/item/category/list?pid=0)请求参数(pid),返回类型(是个json数组)
    进而来设计和构造controller和业务层
利用cors解决跨域问题
    具体使用:在网关中加入GlobalCorsConfig,添加跨域信息,定义请求方式,头信息等

商品分类表设计注意事项

数据库设计的时候注意,tb_category_brand用于商品分类和品牌之间的关联,同时这表里没有外键,为什么不加外键呢?为了提高电商系统的性能,因为外键关联会影响到商品的删除
也会影响数据库的读写性能
根据返回数据的特点:有个用brand对象包装的list还有个总条数,所以选择用对象来包装返回结果
具体逻辑处理时:包括1 分页,2.过滤  3.排序  4.查询   5.解析分页结果
商品新增功能

商品的文件上传

注意一点:由于文件上传在经过zuul网关时,再高并发时可能会导致网络阻塞,zuul网关不可用,所以我们在做文件上传时最好绕过请求的缓存(也会经过zuul网关,只是不会在缓存请求)
解决方法:通过nginx的指令对地址进行重写,修改到以/zuul为前缀

文件上传下载的升级

通过fastDFS进行小文件的上传下载
将代码中的字符串,及变量配置到配置文件中去
具体操作步骤:
    1 在application.yml文件中配置 hm.upload.baseUrl 和 hm.upload.allowTypes属性
    2 定义 UploadProperties属性类
    3 在UploadService中注入 UploadProperties属性类 并使用属性替代字符串

spu与sku的关系

spu:标准产品单位
sku:库存量单位
spu是一个抽象的商品集概念,作用:为了方便后台管理,
sku因具体特征不同而区分出的商品,sku是具体要销售的商品,用户购买的是sku
每一个分类都有统一的规格参数模板,但不同商品其参数值可能不同
	商品分类与规格模板是一对一关系
	商品分类与商品spu是一对多关系
	规模模板与商品spu是一对多关系
	商品spu保存有规格参数的具体值
	因为sku的特有属性是商品规格参数的一部分,
	所以可以将规格参数中的属性划分为 SKU 通用规格与SKU特有规格
	所以 spu与sku是一对多关系
	spu保存着所有sku共享的规格属性
	sku中保存每个sku特有的规格属性

将库存表和sku表分开处理的目的?

因为库存字段写频率较高,而sku的其他字段以读为主,因此我们将两张表分离,读写不会干扰
sku表中的indexes字段是一个特有规格属性字段,它表示spu属性模板中的对应下标组合字段
因为在spu表中其实已经对特有规格参数及可选项作了保存,我们在sku表中将不同的角标串联起来作为spu下不同
sku的标识,这就是index字段;;;、

当前规格参数的显示应该是以能够查询到的所有商品为准,而不是以数据库中的所有查询区间
解决思路是查询数据库中的商品尺寸规格,然后处理成段,再覆盖原来的值


进行搜索时,存的其实是spu,以spu为单位,主要包括,图片,价格,标题,副标题;暗藏的数据:spu的id,sku的id
首先要编写分类和品牌查询的相关服务
当实现将数据库中的数据同步到索引库时 需要用到Feign(使用feign之后,我们调用eureka 注册的其他服务,在代码中就像各个service之间相互调用那么简单。)
同时要想再serach-service中获取品牌信息和分类等信息,如果直接在serach-service中定义接口这种做法不好,因为调用方无法知道被调用方的接口信息和参数,
这样也将两者绑定死了
如何来操作?
在被调用方的item-interface中定义接口信息,在serach-service中引入hm-interface的包,然后继承interface中的接口
@FeignClient(value = "item-service")
public interface GoodsClient extends GoodsApi {
}

解决搜索服务与商品页面服务同步的问题

两种传统路线,
     1 每一次对商品做增删改查时,同时修改索引库数据及页面数据
     2 搜索服务与商品页面服务对外提供接口,后台在商品增删改后调用接口
但是这两种路线有缺陷,代码耦合度较高,违背了微服务的独立原则
新的解决路线
     通过消息队列
     消息队列的优势:消息队列分为生产者和消费者,生产者负责消息的发送,消费者负责消息的接收,两者是异步的,而且也只关注消息的接收与发送,
     没有业务逻辑的侵入,对代码的耦合度较低
     
     消息队列中的问题:
        1 消息丢失如何处理?(如果消费者领取消息后,还没执行操作就挂掉了呢?或者抛出了异常?消息消费失败,这时消息就丢失了)
         采用rabbitmq的ack确认机制
         而ack机制分为两种。自动ack与手动ack,
         两者区别:自动ack是消息一旦被接收,那么消费者就会自动发送ack; 而手动ack是消息接收后不会自动发送ack,需要手动来调用
         而如何来去选择 就要看消息的重要性
         - 如果消息不太重要,丢失也没有影响,那么自动ACK会比较方便
         - 如果消息非常重要,不容丢失。那么最好在消费完成后手动ACK,否则接收消息后就自动ACK,RabbitMQ就会把消息从队列中删除。
            如果此时消费者宕机,那么消息就丢失了。
           如何实现    channel.basicConsume(QUEUE_NAME, false, consumer);  当boolean类型值为true为自动ack,
                      反之为false同时当为手动ack时要注意如果设置为手动ack,一定要有手动ack代码   channel.basicAck(envelope.getDeliveryTag(), false);
          引申:如果消息在消费者还未来得及消费时就丢失了?该如何处理?
            通过将消息持久化,但是消息持久化的前提是:队列、Exchange都持久化,消息的持久化是在发送消息时设置devilerMode为2
            对于生产者方出现消息丢失时可以采用生产者确认机制来保证,就是MQ向生产方发送消息
            而对于除MQ以外的其他消息工具不带生产者确认时,应该怎么做?先将消息持久化到数据库,并记录消息状态(可靠消息服务)
            
        2 如何避免消息堆积?
            什么情况会消息堆积?比如第三方的发短信,转账业务,或者大量订单
            1)采用workqueue,多个消费者监听同一队列
            2)接收到消息后,通过线程池,异步消费
            
        3 如何避免消息的重复执行?
        要保证消息的幂等性(同一接口被重复执行,其结果一致)
        
        4 消息队列的几种类型?
            simple(基本消息模型),--------》一个生产者,一个消费者,一个队列
            worker(work消息模型),----》一个生产者,多个消费者,一个队列
                工作队列模型 默认是队列把消息每一轮平均分给多个消费者,这样就有个缺点,无法根据消费者的自身能力来去消费;
                 解决办法就是配置   channel.basicQos(1); (设置每个消费者同时只能处理一条消息)---》能者多劳
            订阅模型
            fanout(发布订阅模式--fanout)----------》一个生产者,可以有多个消费者,每个消费者绑定自己的队列,每个队列都要绑定到交换机,
                                                      生产者发送的消息只能发送到交换机,
                注意 只有队列可以存消息,而消费者是将消息发送到交换机的,但是交换机是不存储消息的,所以如果没有队列绑定
                该交换机,那么消息就会丢失
            direct(订阅模型--direct) ------》队列与交换机的绑定不能是任意绑定,必须要指定至少一个routingkey(路由key)、

            Topic(订阅模型--Topic)--------》与direct模式类似,但是通过通配符的方式进行配置,所以更加灵活

     具体的业务思路:
        1 当对商品数据做增删改查后会调用消息队列,发送消息,也不关心消息被谁接收
        2 搜索服务和静态页面服务接收到消息后,分别去处理索引库和静态页面

     如何实现?
       在hm-item服务中当对商品做增删改查时执行mq操作;
       同时在hm-search服务中监听商品操作,

实现用户中心服务

具体包括:用户的注册,登录,个人信息管理,用户地址管理,用户收藏管理,订单管理,优惠券
具体操作:新建hm-user

实现发送短信服务

 *因为短信发送API调用时长的不确定性,为了提高程序的响应速度
 短信发送我们都将采用异步发送方式,即:
     - **短信服务监听MQ消息,收到消息后发送短信**。
     - **其它服务要发送短信时,通过MQ通知短信微服务。**
 短信服务心得总结:
    1 短信服务因为不止在一个服务模块用到,所以独立出来作为一个模块
    2 而具体的发送短信请求通过mq消息通知,
        即其他服务(如用户服务):    需要发送短信时--》(1 生成6位数字验证码,2 将验证码存到redis中,并设置有效期,3调用mq消息 通知短信服务)
                    短信服务:短信服务处理短信时-->(1 短信服务的listener负责监听mq传递的消息;  2 调用发短信接口,3 短信服务发送短信返回响应(验证码))

    3 短信服务中用到了一些参数,这些参数可以独立出来放到配置文件中去,
      然后定义一个bean类(包含这些属性)且加上@ConfigurationProperties(prefix = "ly.sms")注解,
      进而在其他类中 加上@EnableConfigurationProperties(SmsProperties.class)就可以读取这些参数

    4 针对手机号码进行限流
        1)在短信工具类,发送短信成功后,将手机号作为key,时间戳作为value存到redis中
        2)然后在每次方法调用前,从redis中取出当前手机号的key对应的value值
        3)判断当前时间与value值的差值是否大于1,如果大于1 调用发送短信,否则直接返回 null

授权中心

1 有状态与无状态区分

    有状态服务:即服务端需要记录每次会话的客户端信息,从而识别客户端身份,根据用户身份进行请求的处理
    缺点:1)服务端保存了大量的数据,增加了服务端的压力
          2)服务端保存了用户的状态,无法进行水平扩展
          3)客户端的请求依赖与服务端
    无状态服务:服务端不保存任何客户端请求者信息,客户端的每次请求必须具备自描述信息,通过这些信息识别客户端身份
    优点:1)减轻了服务端的压力
          2)服务端可以进行任意的迁移和伸缩
          3)客户端的请求信息 不依赖与服务端的信息 任何多次请求不需要必须访问到同一台服务
    典型代表:rest服务

2 如何实现无状态服务?

   无状态登录的流程:
   1)客户端第一次请求访问服务端,服务端会对用户进行信息认证
   2)认证通过后,会将用户信息加密形成token,返回给客户端,作为登录凭证
   3)以后每次请求,客户端都会携带认证的token
   4)客户端会对token进行解密,判断是否有效
   登录过程中的关键点:token的安全性
   具体实现:我们将采用`JWT + RSA非对称加密`

3 结合RSA的鉴权

    1)用户在客户端请求 发起认证
    2)授权中心会对用户信息进行校验,然后通过私钥生成jwt凭证
    3)返回jwt给用户
    4)用户会携带jwt进行访问
    5)zuul网关会直接通过公钥解析jwt,进行验证,验证通过之后则放行
    6)请求到达微服务,微服务直接用公钥解析jwt,获取用户信息,无需访问授权中心
    
4 授权中心的职责:
    1)用户鉴权
    接收用户的请求,通过用户中心的接口进行校验,通过后,使用私钥生成jwt并返回
    
    2)服务鉴权
    微服务之间通过鉴权中心进行认证
    用户登录校验       问题:因为cokile的有效期是30分钟,所以当用户在活跃时,应该刷新token,重新生成token

在zuul里面完成鉴权操作

    需要在网关中编写拦截逻辑,当用户请求到达后,解析cookie,从cookie中获得token,进而判断token是否有效
    具体操作:
    在网关中添加AuthFilter的拦截器
        包括: 添加过滤器(这里用前置过滤器)
              添加过滤器顺序
              是否过滤
              在run方法中添加拦截逻辑(1 获取request,2 获取token ,3 解析token,4 校验权限)
               注意 解析token失败时拦截;解析成功时 在进一步校验可以访问到的权限 
    常见面试题
        如果 微服务地址暴露了怎么办?
            首先微服务地址一般是在局域网内通过zuul进行访问,对外暴露的只有zuul
            而万一暴露了?可以通过服务间鉴权
            定义微服务之间的权限表,将服务与服务之间的访问关系记录下来,而一个服务如果要访问另外一个服务时
            必须要通过管理信息界面进行操作 ,同时服务与服务之间的访问要借助于鉴权中心,将服务当做具体的用户来操作,每一个服务都有
            自己的用户名与密码
       
        如果cookie 被禁用了怎么办?
            首先可以提示用户,网站必须使用cookie,不能禁用
            或者把token 放入头中进行返回,js中获取头信息,存入web存储,每次请求都需要手动携带token写入头中
      
      如果cookie被盗用了怎么办?
            加入ip地址识别身份
            使用Https协议,防止数据泄露
      https://blog.csdn.net/qq_38559956/article/details/103826541
      https://blog.csdn.net/weixin_45443931/article/details/98869617?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-3.channel_param&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-3.channel_param

购物车

        购物车实现技术     
            通过redis(hash结构)来实现
            购物车结构是一个双层Map:Map<String,Map<String,String>>
             - 第一层Map,Key是用户id
             - 第二层Map,Key是购物车中商品id,值是购物车数据
        
        购物车逻辑
            1 通过拦截器来解析cookie中的token,返回user对象,并存入到threadloacal线程域中
           
            2 在service中通过线程域获取当前登录用户
             
            3 判断当前登录用户的(redis)购物车中是否存在该商品   
              (如何判断,利用redis的结构特点,将商品id作为底层map的key,通过key来判断) 
            
            3.1存在
               获取原先购物车中该商品的数量,将当前商品数量累加到原有购物车中的该商品中
               通过redis的操作对象存入到redis购物车中     
            3.2不存在
                直接通过redis操作对象将该商品存入到redis中      
                   
        Springmvc拦截器作用及操作:
          通过springmvc的拦截器来解析token    
                具体操作 在前置拦截方法中,解析cookie中的token,返回user对象,并将user对象传递到controller中
                而如何传递到controller中呢?
                    1)可以通过request 域
                    2)可以通过threadlocal(线程域 threadlocal实质上是一种k-v结构,k是线程本身)    
        为什么要用拦截器----》 当请求到达购物车后,要通过cookie知道当前登录用户是谁  
        
        优惠券的规则制定思路:
                1 首先 对用户做限制,哪些用户可以用,哪些用户不能用
                2 其次 对商品做限制  哪些商品可以用,哪些商品不能用
                3 最后 对价格做限制,满足多少钱可以用优惠券
                还要考虑到用户的退款

下单--订单服务

        订单id没有采用自增长,
        当分库分表时如何保证全局唯一的id ?(如何生成订单)
        通过Twitter公司的雪花算法     
        
        下单逻辑
                1新增订单
                    1.1订单编号,基本信息
                    1.2用户信息
                    1.3收货人地址
                    1.4金额
                    
                2新增订单详情
                    2.1 
                3新增订单状态    
                
                4减库存 
                
        创建订单后如何完成减库存操作?
            减库存可以通过同步减,或者异步减
            而异步减可能会出现 分布式事务不一致的问题,就是说我在订单服务模块通过mq发送消息在商品服务减库存,
            而消息发送后,商品服务是否减库存成功是不知道的,
            而如何解决分布式事务问题呢?
                TCC模式  https://www.cnblogs.com/jajian/p/10014145.html
                
        如何避免超卖现象?
            不能通过加锁,因为在分布式集群环境下加锁只能锁住他自己的jvm,根本不能保证线程安全   
            所以要用分布式锁,而分布式锁如何实现呢?
                 1)使用zookeeper
                 zookeeper的原理是根据节点的唯一性
                当执行decreaseStock操作时,先去通过zookeeper在某个目录下创建节点,创建成功就说明获取了锁,否则没有获取锁
                2)使用redis
                redis的setnx命令,如果返回0,表示当前key已经存在,返回1,不存在
                但是有缺陷,redis宕机,可能会出现死锁问题
                而zookeeper就不会有这个问题,因为zookeeper创建的节点可以是临时节点,当服务器一旦断开连接,会自动删除临时节点
                
            或者可以使用乐观锁思想
                一开始就进行减库存操作,而在sql当中加条件 即update  tb_stock set stock = stock-1 where id =123 and stock >=1
                
            如何保证库存与订单的事务的一致性?
                将创建订单与减库存操作放在一个一个事务里面,我们的逻辑就是先创建订单,然后生成订单的基本信息(包括生成订单编号,
                用户信息,计算金额),之后在减库存,当生成订单抛出异常就不会执行到减库存,而如果减库存出现了问题,整个事务也会回滚,
                这样也就保证了事务的一致性

空文件

简介

基于 SpringBoot 的 SSM(Spring + SpringMVC + MyBatis) 前后端分离电商项目 展开 收起
取消

发行版

暂无发行版

贡献者

全部

近期动态

不能加载更多了
马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化