当前位置:网站首页>7.Redis
7.Redis
2022-08-02 14:10:00 【鱼子酱:P】
Redis入门
关系型数据库产品很多,如 MySQL、Oracle、Microsoft SQL Sever 等,但它们的基本模型都是关系型数据模型。NoSQL 并没有统一的模型,而且是非关系型的。如Redis,HBase,Neo4j
1. 概念
- Redis是一款基于键值对的NoSQL数据库,它的key都是String,它的值支持多种数据结构:字符串(strings)、哈希(hashes)、列表(lists)、集合(sets)、有序集合(sorted sets)等。
- Redis将所有的数据都存放在内存中,所以它的读写性能十分惊人。同时,Redis还可以将内存中的数据以快照或日志的形式保存到硬盘上,以保证数据的安全性。存储快照比较耗时,并且产生存储阻塞,不适合实时去做,适合几小时做一次; 以日志的形式存储称为aof,存储命令,可以做到实时存储,以追加的形式存,体积比较大;如果想恢复,是直接把命令再执行一遍,恢复速度慢但实时性好
- Redis典型的应用场景包括:缓存、排行榜、计数器、社交网络、消息队列等。
缓存:访问非常频繁
排行榜:热门帖子访问频繁,进行缓存后效率就高
计数器:比如访问帖子,浏览量+1
社交网络:点赞、点踩、关注等
消息队列:redis不是专门做消息队列的工具 -> kafka
点赞关注模块
分为Redis,点赞,收到的赞,关注和粉丝。
1.Spring整合Redis
- 安装Redis。并且在pom.xml中加入 spring-boot-starter-data-redis 起步依赖。在 application.properties 上配置Redis。
- 在 config 中配置 RedisConfig 类,返回 RedisTemplate 类。该类可以对 Redis 数据库进行操作。
- Redis中的key值都在 RedisKeyUtil 中定义。
(1) 引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
(2) 配置
①配置数据库参数:
# Redis
spring.redis.database=11
spring.redis.host=localhost
spring.redis.port=6379
②编写配置类,构造RedisTemplate
在config目录下新建配置类:RedisConfig
package com.nowcoder.mycommunity.config;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
//设置key的序列化方式
template.setKeySerializer(RedisSerializer.string());
//设置vaule的序列化方式
template.setValueSerializer(RedisSerializer.json());
//设置hash的key序列化方式
template.setHashKeySerializer(RedisSerializer.string());
//设置hsah的vaule的序列化方式
template.setHashKeySerializer(RedisSerializer.json());
template.afterPropertiesSet();
return template;
}
}
2 .点赞
前提:在 util 的 RedisKeyUtil 工具类中写 getEntityLikeKey 方法,传入 entityType, entityId 返回某个实体(帖子或回复)的赞的key值 。
分为两种:① 给帖子点赞 ② 给回复点赞。
- ① 在帖子详情页面对帖子内容点赞,按钮通过 discuss.js 将 entityType=0 , 帖子id:entityId 和 entityUserId 通过 /like 映射传递给 LikeController 层中的 like 方法。
- like 方法通过调用 likeService 层的 like 进行点赞功能,然后再依次调用 findEntityLikeCount,findEntityLikeStatus 方法查询该帖子的点赞数量,查询当前登录用户的点赞状态。
- likeService 层的 like 方法首先判断是否已经点过赞了,如果没有,就在Redis中该帖子key对应的set中加入点赞者的userId,并且对该帖子用户的赞的总数加一。如果已经赞过则移除UserId,并且对该帖子用户的赞的总数减一。(此处使用了事务,因为需要同时进行两个业务)
- 触发 Topic 为 TOPIC_LIKE 的事件: 构建Event,设置当前登录用户id,被点赞对象的type,被点赞对象的id,被点赞对象所在的帖子id(因为被点赞的对象可能是帖子或者回复),被点赞对象的用户id。然后调用 eventProducer 将 Event 发布到指定 Topic。
- 将点赞的数量和状态封装进map,通过json字符串返回。在浏览器中显示。
- ② 在帖子详情页面对回复点赞,按钮通过 discuss.js 将 entityType=1 和 回复id:entityId 和 entityUserId 通过 /like 映射传递给 LikeController 层中的 like 方法。
注: 另外在 HomeController 层中 /index 映射的方法中,增加向浏览器返回帖子赞的数量的功能。在 DiscussPostController 层中 /detail/{discussPostId} 映射的方法中,增加向浏览器返回帖子和回复列表的赞数量和状态的功能。
具体过程:
redis直接写Service层,不用写Dao层进行数据访问。存数据取数据是面向key编程的,所以为了让key反复复用,最好给redis写一个工具专门生成key.
为方便复用创建一个redis工具类。
public class RedisKeyUtil {
private static final String SPLIT = ":";
private static final String PREFIX_ENTITY_LIKE = "like:entity";
// 某个实体的赞
// like:entity:entityType:entityId -> set(userId)用一个集合存赞,谁给这个实体点的赞就存上
public static String getEntityLikeKey(int entityType, int entityId) {
return PREFIX_ENTITY_LIKE + SPLIT + entityType + SPLIT + entityId;
}
}
创建LikeService,注入redisTemplate;
@Service
public class LikeService {
@Autowired
private RedisTemplate redisTemplate;
// 点赞(谁点的赞,点赞的实体)
public void like(int userId, int entityType, int entityId, int entityUserId) {
String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId);
String userLikeKey = RedisKeyUtil.getUserLikeKey(entityUserId);
boolean isMember = operations.opsForSet().isMember(entityLikeKey, userId);
operations.multi();
if (isMember) {
operations.opsForSet().remove(entityLikeKey, userId);
operations.opsForValue().decrement(userLikeKey);
} else {
operations.opsForSet().add(entityLikeKey, userId);
operations.opsForValue().increment(userLikeKey);
}
}
// 查询某实体点赞的数量
public long findEntityLikeCount(int entityType, int entityId) {
String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId);
return redisTemplate.opsForSet().size(entityLikeKey);
}
// 查询某人对某实体的点赞状态
public int findEntityLikeStatus(int userId, int entityType, int entityId) {
String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId);
return redisTemplate.opsForSet().isMember(entityLikeKey, userId) ? 1 : 0;
}
// 查询某个用户获得的赞
public int findUserLikeCount(int userId) {
String userLikeKey = RedisKeyUtil.getUserLikeKey(userId);
Integer count = (Integer) redisTemplate.opsForValue().get(userLikeKey);
return count == null ? 0 : count.intValue();
}
}
创建LikeController,并修改discuss-detial.html
@Controller
public class LikeController {
@Autowired
private LikeService likeService;
@Autowired
private HostHolder hostHolder;
@RequestMapping(path = "/like", method = RequestMethod.POST)
@ResponseBody
public String like(int entityType, int entityId, int entityUserId) {
User user = hostHolder.getUser();
// 点赞
likeService.like(user.getId(), entityType, entityId, entityUserId);
// 数量
long likeCount = likeService.findEntityLikeCount(entityType, entityId);
// 状态
int likeStatus = likeService.findEntityLikeStatus(user.getId(), entityType, entityId);
// 返回的结果
Map<String, Object> map = new HashMap<>();
map.put("likeCount", likeCount);
map.put("likeStatus", likeStatus);
return CommunityUtil.getJSONString(0, null, map);
}
3.收到的赞
前提:在 util 的 RedisKeyUtil 工具类中写 getUserLikeKey 方法,返回某个用户的赞总数的key。
- 点击个人主页或者点击其他用户头像进入他人个人主页,映射到 UserController层(/user)的 /profile/{userId} 。首先判断该用户存不存在。
- 然后根据该用户的 id 调用 likeService.findUserLikeCount 方法返回点赞数量。findUserLikeCount 方法首先根据userId 调用 getUserLikeKey 方法,返回该用户的赞总数的key。然后使用 redisTemplate 查询key对应的值返回给点赞数量。
- 将点赞数量加入model中返回浏览器显示。
具体过程
以用户为key,记录点赞数量。在redisUtil中增加一个方法。
private static final String PREFIX_USER_LIKE = "like:user";
// 某个用户的赞
// like:user:userId -> int
public static String getUserLikeKey(int userId) {
return PREFIX_USER_LIKE + SPLIT + userId;
}
LikeService
// 点赞
public void like(int userId, int entityType, int entityId, int entityUserId) {
redisTemplate.execute(new SessionCallback() {
@Override
public Object execute(RedisOperations operations) throws DataAccessException {
String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId);
String userLikeKey = RedisKeyUtil.getUserLikeKey(entityUserId);
boolean isMember = operations.opsForSet().isMember(entityLikeKey, userId);
operations.multi();
if (isMember) {
operations.opsForSet().remove(entityLikeKey, userId);
operations.opsForValue().decrement(userLikeKey);
} else {
operations.opsForSet().add(entityLikeKey, userId);
operations.opsForValue().increment(userLikeKey);
}
return operations.exec();
}
});
}
// 查询某个用户获得的赞
public int findUserLikeCount(int userId) {
String userLikeKey = RedisKeyUtil.getUserLikeKey(userId);
Integer count = (Integer) redisTemplate.opsForValue().get(userLikeKey);
return count == null ? 0 : count.intValue();
}
controller层
@RequestMapping(path = "/like", method = RequestMethod.POST)
@ResponseBody
public String like(int entityType, int entityId, int entityUserId) {
User user = hostHolder.getUser();
// 点赞
likeService.like(user.getId(), entityType, entityId, entityUserId);
// 数量
long likeCount = likeService.findEntityLikeCount(entityType, entityId);
// 状态
int likeStatus = likeService.findEntityLikeStatus(user.getId(), entityType, entityId);
// 返回的结果
Map<String, Object> map = new HashMap<>();
map.put("likeCount", likeCount);
map.put("likeStatus", likeStatus);
return CommunityUtil.getJSONString(0, null, map);
}
UserController,修改相关的html文件。
// 个人主页
@RequestMapping(path = "/profile/{userId}", method = RequestMethod.GET)
public String getProfilePage(@PathVariable("userId") int userId, Model model) {
User user = userService.findUserById(userId);
if (user == null) {
throw new RuntimeException("该用户不存在!");
}
// 用户
model.addAttribute("user", user);
// 点赞数量
int likeCount = likeService.findUserLikeCount(userId);
model.addAttribute("likeCount", likeCount);
// 关注数量
long followeeCount = followService.findFolloweeCount(userId, ENTITY_TYPE_USER);
model.addAttribute("followeeCount", followeeCount);
// 粉丝数量
long followerCount = followService.findFollowerCount(ENTITY_TYPE_USER, userId);
model.addAttribute("followerCount", followerCount);
// 是否已关注
boolean hasFollowed = false;
if (hostHolder.getUser() != null) {
hasFollowed = followService.hasFollowed(hostHolder.getUser().getId(), ENTITY_TYPE_USER, userId);
}
model.addAttribute("hasFollowed", hasFollowed);
return "/site/profile";
}
4 .关注和粉丝
前提:在 util 的 RedisKeyUtil 工具类中写 getFolloweeKey 和 getFollowerKey 方法,返回某个用户关注的实体的key和某个实体拥有的粉丝的key。
4.1 点击关注按钮
- 在他人个人主页点击关注,根据 profile.js 异步请求映射到 FollowController 层的 /follow。传入entityType 和 entityId。然后调用 followService.follow 方法进行关注。
- followService.follow 方法首先根据 userId, entityType, entityId 获取到前提中的两个key,开启事务,对当前登录用户的关注的key对应的 ZSet 加入 entityId。被关注用户的粉丝的key对应的 ZSet 加入 userId(Redis存的是有序Set,score采用当前时间插入)。
- 触发 Topic 为 TOPIC_FOLLOW 的事件: 构建Event,设置当前登录用户id,被关注对象的type,被关注对象的id,被关注对象的用户id(与被关注对象的id相同)。然后调用 eventProducer 将 Event 发布到指定 Topic。
- 返回关注成功Json。
- 此时按钮显示已关注,若再次点击则映射到 /unfollow。调用 followService.unfollow 方法取消关注。与第2步骤逻辑基本相同,分别减一
FollowService
@Service
public class FollowService implements CommunityConstant {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private UserService userService;
public void follow(int userId, int entityType, int entityId) {
redisTemplate.execute(new SessionCallback() {
@Override
public Object execute(RedisOperations operations) throws DataAccessException {
String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType);
String followerKey = RedisKeyUtil.getFollowerKey(entityType, entityId);
operations.multi();
operations.opsForZSet().add(followeeKey, entityId, System.currentTimeMillis());
operations.opsForZSet().add(followerKey, userId, System.currentTimeMillis());
return operations.exec();
}
});
}
public void unfollow(int userId, int entityType, int entityId) {
redisTemplate.execute(new SessionCallback() {
@Override
public Object execute(RedisOperations operations) throws DataAccessException {
String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType);
String followerKey = RedisKeyUtil.getFollowerKey(entityType, entityId);
operations.multi();
operations.opsForZSet().remove(followeeKey, entityId);
operations.opsForZSet().remove(followerKey, userId);
return operations.exec();
}
});
}
// 查询关注的实体的数量
public long findFolloweeCount(int userId, int entityType) {
String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType);
return redisTemplate.opsForZSet().zCard(followeeKey);
}
// 查询实体的粉丝的数量
public long findFollowerCount(int entityType, int entityId) {
String followerKey = RedisKeyUtil.getFollowerKey(entityType, entityId);
return redisTemplate.opsForZSet().zCard(followerKey);
}
// 查询当前用户是否已关注该实体
public boolean hasFollowed(int userId, int entityType, int entityId) {
String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType);
return redisTemplate.opsForZSet().score(followeeKey, entityId) != null;
}
// 查询某用户关注的人
public List<Map<String, Object>> findFollowees(int userId, int offset, int limit) {
String followeeKey = RedisKeyUtil.getFolloweeKey(userId, ENTITY_TYPE_USER);
Set<Integer> targetIds = redisTemplate.opsForZSet().reverseRange(followeeKey, offset, offset + limit - 1);
if (targetIds == null) {
return null;
}
List<Map<String, Object>> list = new ArrayList<>();
for (Integer targetId : targetIds) {
Map<String, Object> map = new HashMap<>();
User user = userService.findUserById(targetId);
map.put("user", user);
Double score = redisTemplate.opsForZSet().score(followeeKey, targetId);
map.put("followTime", new Date(score.longValue()));
list.add(map);
}
return list;
}
// 查询某用户的粉丝
public List<Map<String, Object>> findFollowers(int userId, int offset, int limit) {
String followerKey = RedisKeyUtil.getFollowerKey(ENTITY_TYPE_USER, userId);
Set<Integer> targetIds = redisTemplate.opsForZSet().reverseRange(followerKey, offset, offset + limit - 1);
if (targetIds == null) {
return null;
}
List<Map<String, Object>> list = new ArrayList<>();
for (Integer targetId : targetIds) {
Map<String, Object> map = new HashMap<>();
User user = userService.findUserById(targetId);
map.put("user", user);
Double score = redisTemplate.opsForZSet().score(followerKey, targetId);
map.put("followTime", new Date(score.longValue()));
list.add(map);
}
return list;
}
}
FollowController,并修改相应的html文件。
@Controller
public class FollowController implements CommunityConstant {
@Autowired
private FollowService followService;
@Autowired
private HostHolder hostHolder;
@Autowired
private UserService userService;
@RequestMapping(path = "/follow", method = RequestMethod.POST)
@ResponseBody
public String follow(int entityType, int entityId) {
User user = hostHolder.getUser();
followService.follow(user.getId(), entityType, entityId);
return CommunityUtil.getJSONString(0, "已关注!");
}
@RequestMapping(path = "/unfollow", method = RequestMethod.POST)
@ResponseBody
public String unfollow(int entityType, int entityId) {
User user = hostHolder.getUser();
followService.unfollow(user.getId(), entityType, entityId);
return CommunityUtil.getJSONString(0, "已取消关注!");
}
@RequestMapping(path = "/followees/{userId}", method = RequestMethod.GET)
public String getFollowees(@PathVariable("userId") int userId, Page page, Model model) {
User user = userService.findUserById(userId);
if (user == null) {
throw new RuntimeException("该用户不存在!");
}
model.addAttribute("user", user);
page.setLimit(5);
page.setPath("/followees/" + userId);
page.setRows((int) followService.findFolloweeCount(userId, ENTITY_TYPE_USER));
List<Map<String, Object>> userList = followService.findFollowees(userId, page.getOffset(), page.getLimit());
if (userList != null) {
for (Map<String, Object> map : userList) {
User u = (User) map.get("user");
map.put("hasFollowed", hasFollowed(u.getId()));
}
}
model.addAttribute("users", userList);
return "/site/followee";
}
@RequestMapping(path = "/followers/{userId}", method = RequestMethod.GET)
public String getFollowers(@PathVariable("userId") int userId, Page page, Model model) {
User user = userService.findUserById(userId);
if (user == null) {
throw new RuntimeException("该用户不存在!");
}
model.addAttribute("user", user);
page.setLimit(5);
page.setPath("/followers/" + userId);
page.setRows((int) followService.findFollowerCount(ENTITY_TYPE_USER, userId));
List<Map<String, Object>> userList = followService.findFollowers(userId, page.getOffset(), page.getLimit());
if (userList != null) {
for (Map<String, Object> map : userList) {
User u = (User) map.get("user");
map.put("hasFollowed", hasFollowed(u.getId()));
}
}
model.addAttribute("users", userList);
return "/site/follower";
}
private boolean hasFollowed(int userId) {
if (hostHolder.getUser() == null) {
return false;
}
return followService.hasFollowed(hostHolder.getUser().getId(), ENTITY_TYPE_USER, userId);
}
4.2 显示关注和粉丝数量
- 在个人主页显示关注和粉丝的个数,与 1.3 相同,点击个人主页或者点击其他用户头像进入他人个人主页,映射到 UserController层(/user)的 /profile/{userId} 。首先判断该用户存不存在。
- 根据 followService 层的 findFolloweeCount 和 findFollowerCount 方法查询该用户的关注数量和粉丝数量。同时通过 hasFollowed 方法查询当前登录用户是否已经关注该用户。
- 将各个参数加入model,返回给 /site/profile。
4.3 显示关注和粉丝列表
- 在个人主页点击关注xx人,映射到 FollowController 层的 /followees/{userId}。首先根据 userId判断该用户是否存在。然后设置分页信息。通过 followService.findFollowees 方法返回该用户的关注列表。
- findFollowees 方法根据 userId 查询关注者id列表。根据id列表生成一个map列表。map列表中装入id对应的User和关注时间。返回给Controller。
- Controller接收到列表,然后再判断一下当前登录用户对该列表中的用户是否已经关注,将该字段放入map列表中。返回给 /site/followee 显示。
- 点击粉丝xx人,映射到 /followers/{userId}。逻辑与上面基本一致,就是查询的key不同。返回给 /site/follower显示。
5.优化登录模块—验证码
- 原来是用session存验证码,现在用Redis存。
- 首先服务器生成验证码:给某客户端生成一个随机字符串,存在客户端的cookie里。将随机字符串当作redis的key,value存生成的字符串。且redis存储的时间仅60s
- 下次客户端输入了验证码,传给服务端:服务端取出cookie,即可到redis的key,根据key找到验证码,然后验证客户端输入的验证码code和redis里存的验证码是否一致,若一致则登录成功。
- key存代表各客户端的随机字符串(在客户端cookie中)
- value存验证码text
(1)RedisKeyUtil
// 登录验证码
public static String getKaptchaKey(String owner) {
return PREFIX_KAPTCHA + SPLIT + owner;
}
(2)LoginController
首次访问页面后,生成验证码,存入redis里;在对登录时使用所有login里也要更改,
@RequestMapping(path = "/kaptcha", method = RequestMethod.GET)
public void getKaptcha(HttpServletResponse response/*, HttpSession session*/) {
// 生成验证码
String text = kaptchaProducer.createText();
BufferedImage image = kaptchaProducer.createImage(text);
// 将验证码存入session
// session.setAttribute("kaptcha", text);
// 验证码的归属
String kaptchaOwner = CommunityUtil.generateUUID();
Cookie cookie = new Cookie("kaptchaOwner", kaptchaOwner);发给客户端
cookie.setMaxAge(60);失效时间60s
cookie.setPath(contextPath);
response.addCookie(cookie);
// 将验证码存入Redis
String redisKey = RedisKeyUtil.getKaptchaKey(kaptchaOwner);
redisTemplate.opsForValue().set(redisKey, text, 60, TimeUnit.SECONDS);
// 将突图片输出给浏览器
response.setContentType("image/png");
try {
OutputStream os = response.getOutputStream();
ImageIO.write(image, "png", os);
} catch (IOException e) {
logger.error("响应验证码失败:" + e.getMessage());
}
}
@RequestMapping(path = "/login", method = RequestMethod.POST)
public String login(String username, String password, String code, boolean rememberme,
Model model, /*HttpSession session, */HttpServletResponse response,
@CookieValue("kaptchaOwner") String kaptchaOwner) {
// 检查验证码
// String kaptcha = (String) session.getAttribute("kaptcha");
String kaptcha = null;
if (StringUtils.isNotBlank(kaptchaOwner)) {
String redisKey = RedisKeyUtil.getKaptchaKey(kaptchaOwner);
kaptcha = (String) redisTemplate.opsForValue().get(redisKey);
}
if (StringUtils.isBlank(kaptcha) || StringUtils.isBlank(code) || !kaptcha.equalsIgnoreCase(code)) {
model.addAttribute("codeMsg", "验证码不正确!");
return "/site/login";
}
// 检查账号,密码
int expiredSeconds = rememberme ? REMEMBER_EXPIRED_SECONDS : DEFAULT_EXPIRED_SECONDS;
Map<String, Object> map = userService.login(username, password, expiredSeconds);
if (map.containsKey("ticket")) {
Cookie cookie = new Cookie("ticket", map.get("ticket").toString());
cookie.setPath(contextPath);
cookie.setMaxAge(expiredSeconds);
response.addCookie(cookie);
return "redirect:/index";
} else {
model.addAttribute("usernameMsg", map.get("usernameMsg"));
model.addAttribute("passwordMsg", map.get("passwordMsg"));
return "/site/login";
}
}
边栏推荐
猜你喜欢
How to add a one-key shutdown option to the right-click menu in Windows 11
win10无法直接用照片查看器打开图片怎么办
【STM32学习1】基础知识与概念明晰
C语言函数参数传递模式入门详解
The SSE instructions into ARM NEON
cmake配置libtorch报错Failed to compute shorthash for libnvrtc.so
win11一直弹出用户账户控制怎么解决
使用 腾讯云搭建一个个人博客
Win10安装了固态硬盘还是有明显卡顿怎么办?
A clean start Windows 7?How to load only the basic service start Windows 7 system
随机推荐
vscode镜像
模板系列-二分
KiCad常用快捷键
模板系列-并查集
Mysql之MVCC
SQL的通用语法和使用说明(图文)
win10任务栏不合并图标如何设置
FP7195转模拟调光技术解决智能家居调光频闪和电感噪音的原理
CS4398音频解码替代芯片DP4398完全兼容DAC解码
Actual combat Meituan Nuxt +Vue family bucket, server-side rendering, mailbox verification, passport authentication service, map API reference, mongodb, redis and other technical points
FP7195降压恒流PWM转模拟调光零压差大功率驱动方案原理图
win10 system update error code 0x80244022 how to do
BLE蓝牙5.2-PHY6222系统级芯片(SoC)智能手表/手环
Publish module to NPM should be how to operate?Solutions to problems and mistake
推开机电的大门《电路》(二):功率计算与判断
Win10电脑不能读取U盘怎么办?不识别U盘怎么解决?
5. Use RecyclerView to elegantly achieve waterfall effect
win11一直弹出用户账户控制怎么解决
How to update Win11 sound card driver?Win11 sound card driver update method
Binder机制(下篇)