黑马点评项目总结

整体架构

在这里插入图片描述

一. 短信登录模块

1.1 基于session

(1)后台发送验证码Code

  1. 前端提交手机号获取验证码(参数为手机号String和HttpSession)
  2. 后台检验手机号是否合法(RegexUtils工具类),不合法就返回错误信息
  3. 合法就用RandUtil工具类随机生成验证码吗,并将验证码setAttribute保存session 中,用于第二个环节的验证;
  4. 向用户发送验证码(这里 log.debug(xxx)写到日志模拟);
    在这里插入图片描述在这里插入图片描述

@RequestParam:GET请求,url传参,表单传参;

(2)登录、注册

  1. 用户提交手机号和验证码
  2. 后台再次验证手机号是否合法(两次请求,都要验证),如果不合法就报错
  3. 取出 session 中的验证码Code,和用户传的Code对比是否正确,不一样则报错;
  4. 正确则:通过手机号从数据库User表查询User对象,
    判断:如果User为空就用Mybatis添加数据库(注册:创建User对象、set手机号、set随机用户名);
  5. 不管有没有user,最后都把user对象保存 session 中,用于保存登录状态用于后续登录校验;
    在这里插入图片描述

(3)校验登录状态

用户点提交后,会跳转到用户详情页,此时前端会向 /user/me 发起请求 ,请求对应的controller的me()方法要返回一个User对象供前端展示,但是这是新的请求,而只有登录了的用户才能访问这个方法;
为了统一去对需要登陆才能访问的页面进行登录校验,以及过滤不需要登录就可以访问的页面,使用拦截器进行校验登录状态,
则每一个请求在到达controller的方法之前都需要经过拦截器!

  1. 定义一个拦截器类,实现HandlerInterceptor接口,重写preHandle方法;
    然后在SpringMvcConfig中配置拦截器,重写addIntercepter()排除不需要登录就能访问的地址;
  2. 在拦截器的preHandle方法中,从 request获取session对象,再从session对象获取user对象(session被Tomcat自动维护,一个浏览器对应一个session)
  3. 判断
    ①如果user对象不存在则表明没有通过第二个环节登录成功,返回报错401(未授权),return false;
    ②如果user对象存在则说明登陆过了,此时需要返回user对象给前端展示,这里为了controller快速读取用户信息,一方面为了安全性,就把user对象存入 Threadlocal,后序用户从ThreadLocal直接读取信息更安全;
  4. preHandle方法return true 放行请求,
    请求会到达controller的me()方法,该方法会从ThreadLocal获取user对象,供前端展示到用户信息页面;
  5. postComplition中remove移除ThreadLocal中的用户信息;

拦截器类:
在这里插入图片描述

配置拦截器:
在这里插入图片描述

使用Session来完成登录时,登录凭证就是sessionID,Tomcat服务器会自动维护SessionID;

以上就是使用session、拦截器、ThreadLocal实现的用户登录和校验的过程;
问题
当Tocmat服务器扩张成一个服务器集群,而不同的Tomcat是不同的JVM内存,不能共享session;
当浏览器发送请求到 Nginx,由Nginx进行 负载均衡 时,同一个浏览器的不同请求就可能发往不同的Tomcat服务器,这样数据读取就出问题;

解决
Tomcat使用session互相拷贝;
①多台服务器拷贝浪费空间 ②拷贝需要时间,有延迟(服务器是进程,进程通信效率低)

所以使用Redis充当 缓存服务器 专门用来存数据,让Tomcat都可以去访问,实现数据的共享;

1.2 基于Redis

(1)后台发送验证码Code

  1. 客户端提交手机号
  2. 后台检验手机号是否合法,不合法就返回错误信息
  3. 合法就用工具类随机生成验证码吗,使用StringRedisTemplate.opsForValue()将验证码作为value保存到 Redis 中,key就是手机号,用于第二个环节的验证;
  4. 向用户发送验证码;
    在这里插入图片描述

Redis中存的第一类数据(验证码):key 为手机号+“前缀”(保证唯一性),value 就是验证码;

(2)登录、注册

  1. 用户提交手机号和验证码
  2. 后台再次验证手机号是否合法(两次请求,都要验证),如果不合法就报错
  3. 用手机号作为key去GETRedis中对应的验证码Code,和用户提交的Code对比,不一样则返回错误;
  4. 一致则通过手机号从数据库查询User对象,如果User为空就用Mybatis添加数据库(注册);
  5. 不管有没有user,都需要将user对象存到 Redis中(之前是存到session,session是Object可以存任意类型);
    value:此时用BeanUtil工具类把User对象转换Map作为value,这样就能放到Redis中了;(String存json也可以)
    key:key则使用UUID工具类产生token随机字符串(登录凭证);
    最后StringRedisTemplate.opsForHash().putAll()把user对象存入 Redis
  6. 使用StringRedisTemplate.expire() 方法设置Token的有效时间;
  7. 由于此时Token是登陆凭证,将登陆凭证Token 返回给前端,前端执行sessionStroger()将其存到浏览器中,以后每次访问都放在【请求头】中;
    在这里插入图片描述
    在这里插入图片描述

前端会用一个sessionStorage() 将Token存到浏览器中,以后每次访问都在【请求头】中带着Token;

(3)校验登录状态(更新Token有效时间)

用户点击提交之后,会跳转 /user/me 用户详情页面,向controller发起请求,controller中对应的方法会返回一个User对象,但是需要登陆过的用户才能去访问这个方法;
为了统一去对需要登陆才能访问的页面进行登录校验,以及过滤不需要登录就可以访问的页面,使用拦截器进行校验登录状态;

  1. 定义一个拦截器类,实现HandlerInterceptor接口,重写preHandle方法;
    然后在SpringMvcConfig中配置拦截器,排除不需要被拦截的地址;
  2. 在拦截器的preHandle方法中,调用requestgetHeader() 读取【请求头】中的 Token,如果不存即第二个环节没通过,返回错误401(未授权);
  3. 以Token为key,从Redis中取出user的Map
    ①如果Map不存在,也返回错误401,return false;
    ②存在应该放行,放行之前,使用BeanUtil 将Map转换为User对象,存入 Threadlocal(方便和安全性);
  4. StringRedisTemplate.expire() 更新Token(key)的有效期;(刷新登录状态)
  5. return true 放行请求,访问个人信息页面;

在这里插入图片描述在这里插入图片描述在这里插入图片描述

注意Redis存的是两类数据,一个是 手机号–Code ,另一个是Token–User
在这里插入图片描述

1.3 补充

问题:用户登录状态的保持主要靠的是拦截器中更新Token有效时间,但是访问 shop店铺、blog博客时没有更新Tokend有效时间 !

解决:再增加一个拦截器 !
让第一个拦截器对所有请求操作,获取Token,保存User用户到ThreadLocal
第二个拦截器来实现拦截功能;

  1. 第一个拦截器:(对所有请求页面拦截)
    如果发现Token为空,则直接return true放行到第二个拦截器;
    通过Token获取User,不存在则放行,
    将User存储放在第一个拦截器;
  2. 第二个拦截器:(对部分请求页面拦截)
    判断ThreadLocal中是否有用户,没有则拦截,有则放行;

配置拦截器
通过设置拦截器的 order,来控制拦截器的执行顺序!
oder越小,优先级越高;

第一个拦截器:
在这里插入图片描述

第二个拦截器:
在这里插入图片描述
配置拦截器:
在这里插入图片描述

Redis:

  1. 存储验证码,用于登录时验证;
  2. 存储User对象,用来保存用户的登录状态;

二. 商户缓存模块

引入:当前端发来大量请求时,如果都去查询数据库则会导致其崩溃,同时为了提高响应速度(基于内存),所以让Redis作为缓存型数据库

缓存工作模型
在【客户端和数据库】之间添加了一个中间层!
这样客户端请求会优先到达缓存Redis!
如果Redis中有数据就返回,就不用走数据库了(请求命中);
若没有才去查询数据库(未命中),然后把数据更新到缓存,这样下一次再查询就可以使用缓存了;
随着用户请求越多,Redis中缓存的数据越多,Redis的命中率就会越来越高
在这里插入图片描述

2.1 基本查询流程(缓存模型实现)

查询流程:
key:店铺id
value:店铺信息Json

  1. 注入stringRedisTemplate的bean,
    前端提交店铺id请求,后端通过id(key)从Redis GET查询 商户缓存,如果命中则返回商铺信息;
    这里存商铺信息value的是String格式,则需要将String格式的Json 用JSONUtil工具类反序列化为Java对象;
  2. 未命中则,查询数据库中的商铺表,若不存在,报错;
  3. 若数据库中存在,则用stringRedisTemplate.opsForVlaue将商铺对象序列化,再更新Redis中,下次再查就可以命中了;
  4. 再把商铺对象返回
    在这里插入图片描述

2.2 缓存一致性(三点保证)

缓存一致性

2.3 缓存穿透、缓存雪崩、缓存击穿

缓存穿透、缓存雪崩、缓存击穿

三. 优惠券秒杀

3.1 全局唯一ID

引入:分布式场景下,数据库AUTO_INCREMENT自增ID性能有效,

全局唯一ID

3.2 实现优惠券秒杀(基本)

项目中的商铺就是优惠券;

有两种优惠券: 普通券(不需要秒杀) 、秒杀券
所以秒杀是针对秒杀券;

秒杀券
注意有timestamp类型的有效时间字段;
在这里插入图片描述

流程:
商铺是优惠券,主要字段:id、有效时间、库存;
需要注入 秒杀券service的bean,和生成全局ID的redisIdWorker的bean;

  1. 前端提交秒杀券ID,根据ID获取秒杀券对象
  2. 判断时间 是否正确,当前时间早于和晚于秒杀券的有效时间都返回错误;
  3. 判断库存 是否充足;
    ①如果秒杀券中库存不足,则返回错误;
    ②如果秒杀券中库存充足,则对秒杀表 扣库存(超卖安全问题)
  4. 在订单表中 创建订单对象全局唯一的订单id、秒杀券id、用户id)存入订单表
  5. 返回订单ID

在这里插入图片描述
在这里插入图片描述在这里插入图片描述
在这里插入图片描述

3.3 乐观锁解决“超卖问题”(多扣库存)

超卖问题:
当库存还剩余1时,多个线程都去查询库存,剩余1即可以扣库存,然后多个线程都去扣库存,库存被扣为负数,就出现了 超卖现象

解决方案
悲观锁:线程同步串行执行,效率低,不适合高并发场景;
乐观锁:CAS,在线程对数据更新时才做判断,判断在当前线程之前有没有其他线程对数据有修改;

思路:在【扣库存】的时候,当前库存与之前查询到的库存是否相同,是则可以修改,否则不修改;
因为库存是在变化的,适合使用乐观锁;
在这里插入图片描述
结果:异常比例高达89% !

原因:当有一个线程成功,则其他的线程由于stock改变了就都更改失败了! 没有自旋;

改进:改为 where stock>0 即可;因为DML语句默认有 行锁,在修改时会加上行锁,避免线程安全问题;

3.4 实现“一人一单”(一人下了多单)

需求:要求同一个用户只能下一单!(避免黄牛)

思路用户ID秒杀券ID有唯一性,即判断用户ID和秒杀券ID已经同时存在,则不能再下单;

3.4.1 单线程时

流程
在满足秒杀券有效时间和库存后,【查询库存后】【扣库存之前】 要做用户ID秒杀券ID联合查询
①如果联合查询结果>0 存在则返回异常;
②如果联合查询结果=0即不存在,则扣库存,生成订单对象,存入订单数据;
在这里插入图片描述
问题
由于多个线程并发执行,下单时多个线程用同一个用户ID访问,则会下多个订单;
在这里插入图片描述

3.4.2 Synchronized 悲观锁的问题 ※

需求:使用悲观锁主要是防止同一个用户下多个订单!

之前是用扣库存是用乐观锁,因为库存是在变化的,通过判断stock值是否变化就可以实现乐观锁;
而这里是涉及下单,是向订单表新增,没法判断某个值是否变化,所以用悲观锁!

synchronized加到哪?
【查询库存后】,从联合查询、扣减库存、到创建订单 这个过程加上悲观锁!
将这三个步骤提取为一个createVoucherOrder方法;
事务是为了保证减库存和创建订单,可以把@transactional 放到createVoucherOrder方法上;

在这里插入图片描述
在这里插入图片描述

如果synchronized加到方法上,则共享对象是this,是串行执行,效率低;
而线程安全问题主要是同一个用户ID引起,所以锁的应该是同一个用户ID的情况,则 锁定的共享对象为用户ID
由于toString()方法底层还是new了一个String,不同String会被判定为不同,所以调用 intern()方法(池化,尝试将字符串放入串池,如果已经有了就返回串池中的对象);这样能保证用户ID一致时锁就一样;锁定范围变小,性能更好;
锁应该在事务提交之后再释放!假设synchronized在方法内,则可能锁释放了但事务还没有提交,所以将synchronized放在整个方法之外! ※
当createVoucherOrder方法执行完则事务已经提交 ,再释放锁,就不会有线程安全问题了;
在这里插入图片描述

Spring事务的本质是通过动态代理,而此时使用 return createVoucherOrder方法相当于使用事务的方法是由this调用的,就不是动态代理的,Spring事务会失效!
需要通过AopContext.currentProxy()拿到当前对象(VoucherOrderServiceImpl)的代理对象!
在这里插入图片描述

还需要在类上
①添加aspectjweaver的依赖;
②在启动类中添加注解暴露代理对象
在这里插入图片描述

3.4.3 Redis分布锁实现一人一单(初级)

Synchronized锁的问题:
只适用于单个Web服务器使用;
在Web服务器为集群的情况下,则 锁失败,锁不住

原因
不同的Web服务器→不同的进程、即不同的JVM→不同的字符串对象(userID)→不同的Monitor对象→不是同一把锁;

分布式锁引入
因为Synchronized只能保证单个JVM内部的多个线程的互斥,而多个JVM进程之间无法生效;

(1)分布式锁的实现

SETNX + EXPIRE
key:锁的名称在实现类中被传入
value:锁的值使用线程ID
①定义一个锁接口
在这里插入图片描述

②实现类实现ILock
锁的key为name属性;
在这里插入图片描述
重写tryLock方法:
传入
在这里插入图片描述
key由外部传入;
value使用线程的标识,所以使用Thread.currentThread(); getId获取线程的ID,不会重复;
返回值是Boolean包装类,这里为了防止空指针,使用Boolean.TRUE.equals去比较,如果false和null都会返回false;

释放锁:
在这里插入图片描述

(2)分布式锁实现“一人一单” ★

1.前端提交优惠券ID,在service中查询秒杀券信息(时间、库存)
2.判断时间是否正确;
3.判断秒杀表中的库存是否充足;
①如果库存不足,则返回错误;
②如果库存充足:

  • 创建锁,输入 用户ID 作为 key !这样就实现一个用户只有一把锁!实现一个一单; 并输入锁的超时时间;
  • 尝试获取锁,成功 则执行createVoucherOrder方法:
    扣库存
    在订单表中创建订单(订单id、代金券id、用户id);
    返回订单ID;
  • 释放锁

在这里插入图片描述

3.4.4 Redis分布锁“误删”问题 ★ ★ ★

(1)初级版本
上锁时:使用SETNX+EXPIRE
key为用户ID保证一个用户一把锁,不同用户之间不阻塞,value线程标识(UUID+线程ID)
解锁时:先判断当前线程标识和锁的线程标识是否一致,一致才释放,不一致则不释放,防止误删;

UUID
1.static静态修饰,保证一个服务器对应一个UUID;
2.为了防止不同服务器的线程ID重复;

(2)改进版本
解决误删问题
问题:由于释放锁的时候判断锁和释放锁是两个操作,不具有原子性,所以还是可能导致锁误删;
解决
把判断线程标识和释放锁放到同一个Lua脚本中,然后在使用锁时用execute()执行脚本,保证了原子性;

3.4.5 Redission分布锁实现一人一单 ★ ★ ★

注入bean:
在这里插入图片描述
上锁→执行createVoucherOrder(联合查询、扣库存、创建订单)→释放锁;
在这里插入图片描述

3.4.6 Redission分布式锁总结+联锁

  1. 可重入:基于Hash结构,使用Hash的value来记录锁重入的次数(类似Reentrantlock)

  2. 可重试:借助发布订阅模式,第一次尝试失败以后不会立即失败,而是会等待释放锁的消息,而获取锁成功的线程在释放的时候会发送消息,则等待的线程捕获到消息会再次尝试获取锁;如果再失败,则继续等待释放锁的信号,捕获信号后再次尝试获取锁;超过持续时间就不再重试;

  3. 超时续约:利用watchDog看门狗,获取锁成功后会开启一个定时任务,这个任务每隔一段时间就去重置锁的超时时间!避免锁因为业务阻塞而被删除;

  4. 主从一致性:使用 MultiLock联锁
    不要主从模式,节点都是独立的读写;
    之前客户端获取锁只需要找到Master节点,而现在需要依次向多个Redis节点都去获取锁,都要保存锁的标识;
    假设有节点宕机,因为锁依然存在,所以锁依然有效;
    在这里插入图片描述

之前客户端获取锁只需要找到Master节点,而现在需要依次向多个Redis节点都去获取锁,都要保存锁的标识,全部获取才能成功;
假设有节点宕机,因为锁依然存在,所以锁依然有效;

3.4. Redis秒杀优化

(1)优化思路

引入
Web服务器中执行的整个步骤是串行的,而查询数据库速度慢,且有分布式锁,导致性能差;

思路
将秒杀任务分成两部分交给两个线程去做:

  1. 主线程(秒杀):先判断秒杀资格:判断(有效时间省略)、库存是否充足 ,然后查询用户ID是否买过(联合查询)保证一人一单(Redis读操作耗时短 );
  2. 异步线程(下单):减库存+创建订单,(数据库写操作耗时长);

优点

  1. 缩短秒杀业务的流程,让主线程秒杀,异步线程去扣减库存和创建订单
  2. 减轻数据库的压力

准备
由于要查询数据库,所以将秒杀券订单信息缓存到Redis;
Redis存秒杀券库存:用String类型,key是秒杀券ID,value是库存数量;
Redis存秒杀券ID和用户ID:一个秒杀券有多个库存,所以一个秒杀券ID会对应多个用户ID,且要保证一人一单,key是秒杀券ID,value是用户ID的集合Set,即一个秒杀券ID的用户中不能重复!所以使用Set;
在这里插入图片描述

流程
Lua脚本(原子性):

  1. 判断库存是否充足,不足则retuern 1结束;
  2. 充足则再判断秒杀券ID对应的用户ID的set中是否存在当前用户ID(相当于联合查询),已存在则return 2;
  3. 如果都满足,①则在Redis中预扣减库存;②将用户ID存入set中;
  4. return 0表示有下单资格;

根据Lua脚本返回的结果来处理:

  1. 如果返回0即有下单资格,则将秒杀券ID、用户ID、订单ID存入 阻塞队列,并返回订单ID给用户,此时秒杀已经结束,
    扣库存创建订单由 异步线程 去完成,此时时效性要求并不高;
  2. 如果Lua脚本返回1/2则返回错误信息;
    在这里插入图片描述
(2)实现:主线程秒杀

需求
1.新增秒杀券时,将优惠券信息保存到Redis中(缓存模型);(用户ID信息是在秒杀的过程中添加的!)
2.基于Lua脚本,①判断秒杀券的库存、②查询一人一单,决定用户是否有下单资格,有则预扣库存,用户ID存入set;
3.如果抢购成功,将优惠券ID、用户ID和订单ID存入阻塞队列;
4.开启线程任务,不断从阻塞队列获取信息,实现异步下单

  1. Lua脚本:
    ①先判断库存是否充足,通过秒杀券ID取出库存数量判断是否<0(String要转换为数字);
    ②然后判断该优惠券ID对应的用户set中是否有当前用户ID;
    SISMEMBER判断当前oderKey对应的Set中是否存在 用户ID,返回1即存在;
    ③Set中不存在用户ID则可以去预扣库存,用INCRBY命令,向订单orderKey对应的Set去存入用户ID,用SADD;
    ④成功秒杀则直接向消息队列发送消息
    在这里插入图片描述
    在这里插入图片描述

  2. DefualRedisScript类和静态代码块预先读取脚本
    在这里插入图片描述

  3. execute()执行Lua脚本
    Lua的参数是秒杀券ID和用户ID;
    在这里插入图片描述

如果返回为0则下单成功,生成全局唯一ID,并将信息传到阻塞队列:

测试:1000个线程
之前:
在这里插入图片描述
优化后:
在这里插入图片描述

(3)实现:异步线程下单

1.创建线程池:实际下单处理不需要很高的时效性,所以创建一个单个核心线程的线池;

2.线程池自动提交任务:要让任务在当前类初始化之后就执行,因为项目一启动,用户随时都可以去抢购,所以使用 @PostConstruct 让任务在类初始化后就执行;
在这里插入图片描述
3.Runnable任务
①获取Stream消息队列中的订单消息,没有消息则阻塞2秒
②判断消息获取是否成功
如果失败则继续下一次循环读取
③如果成功则解析list,转成Voucher对象,执行下单;
④ACK确认;
在这里插入图片描述
在这里插入图片描述

异常则执行handlePendingList:(当出现异常时,消息没有做ack确认,所以消息依然在appeding-list中,这时使用起始ID使用0 )
此时读的不是消息队列而是 pending-list,所以是 标识为0;
如果消息获取失败,说明pending-list中没有异常消息,则结束循环;
有则取出数据去下单;
在这里插入图片描述

整体流程:
1.尝试从消息队列中读取消息,没有则continue,下一轮循环
2.如果有消息则提取信息,传入handleVoucherOrder去下单;
3.ACK确认;
如果抛出异常导致没有ACK确认,则执行handlePendingList:
1.从pendingList中读取,起始ID为0,
2.读到则解析消息,下单
3.如果异常消息处理完了即返回的list不存在,则break;
4.如果过程中再抛异常,则继续循环即可,知道pending-list返回的list不存在即异常都处理完成就break;

回顾秒杀中Redis做了什么?
1.一人一单的线程安全问题用了SETNX锁、redission锁;
2.将同步秒杀变成异步,用redis存储库存和订单信息,再Lua脚本完成秒杀资格判断;而后把下单任务放到阻塞队列中
3.将阻塞队列优化成消息队列;

四. 达人探店

4.1 发布笔记(保存)

(1) UploadController 上传图片(“upload/bolg”):
1.前端以post的方式上传照片到controller,@requestParam即普通参数;
2.传入图片的参数类型为MultipartFile,继承自InputStreamSource,使用MultipartFile的transferTo()方法保存图片到Nginx,并返回图片的url
一般企业会把图片会放在图片服务器,这里简化为保存到Nginx中,将来前端才能访问;
在这里插入图片描述
@RestController= responseBody+Controller
@RequesMapping即urlPattern
@RequestParam即普通参数类型

(2) BlogController保存笔记(“blog”):
1.前端将店铺ID、图片地址、笔记内容放在请求体提交,controller用一个blog形参对象接收;
2.从ThreadLocal获取用户id,放入blog对象;
3.将blog对象存入数据库中blog表;
在这里插入图片描述

4.2 查看笔记(返回blog对象+作者信息)

在这里插入图片描述

准备:由于用户的头像和姓名并不在blog对象中,所以在blog实体类中使用 @TableField(exist=false) 添加头像和姓名这两个属性;

流程
1.前端提交笔记id,后端根据笔记id从数据库查到 blog对象;不存在则返回错误;
2.存在则根据blog取到作者的userid;
3.根据userid从user表获取用户头像和姓名,封装到 blog对象,最后返回blog对象给前端;

在这里插入图片描述
在这里插入图片描述

4.3 实现点赞功能

需求一:

一个用户只能点一次赞,再点赞则取消;

流程
1.从ThreadLocal获取user对象,拿到userid;
2.用Sismember在Redis的set集合判断是否点赞过,(防止一个用户反复点赞) key=blogid,value=userid
①未点赞过则数据库like+1,将userID放入Redis的set;(先数据库再Redis)
②已点赞过则数据库like-1,将userID移除Redis的set;
3.打开首页时(分页查询),会根据笔记ID查询blog,通过set判断当前用户是否点赞过,是则赋值给isLike字段,会由前端显示高亮;
4.分页查询blog业务时,通过set判断当前用户是否点赞过,是则赋值给isLike字段;
在这里插入图片描述
在这里插入图片描述

需求二:

查看当前用户是否点过赞,点过则点赞按钮高亮显示(判断字段isLike属性),【需要在查询的时候才判断】!
准备
给blog实体类添加isLike字段(TableField(exist=false)),标识是否被当前用户点赞;
在这里插入图片描述
1.当查询某一个笔记时,判断当前用户是否点赞过,是则赋值给isLike字段
将赋值的操作封装到函数isBlogLiked:
在这里插入图片描述
isBlogUser函数:
从Threadlocal获取userid;
isMember查询set中是否有userid;
赋值给isLike字段;
在这里插入图片描述

2.首页的分页查询,也调用isBlogUser函数;
在这里插入图片描述
效果:当前用户高亮;
在这里插入图片描述

  • 2
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值