[TOC]
数据库
userID, message , login_ticket ,disscuss_post comment
用户表,包括 id、用户名、密码、盐值、邮箱、类型(普通/管理员/版主)、状态(激活/未激活)、激活码(随机字符串)、头像 url、注册时间。
登录凭证表,包括 id、用户 id、登陆凭证(随机字符串)、登录状态(有效/无效)、过期时间。
评论表,包括 id、评论用户 id(索引)、评论实体 id(索引)、评论类型(帖子/回复)、被评论目标 id、评论内容、评论状态(有效/无效)、评论时间。
帖子表,包括 id、发帖用户 id(索引)、标题、帖子内容、类型(普通/置顶)、评论数量、状态(普通/精华/拉黑)、发帖时间。
消息表,包括 id、发消息 id(索引)、收消息 id(索引)、会话 id(由发消息双方 id 拼接,索引)、内容、状态(未读/已读/删除)、发消息时间。
社区论坛
项目描述:该项目是设计一个社区论坛,方便用户发言与讨论
使用技术:本项目使用SpringBoot进行开发,使用的技术主要有 MySQL,Redis , Kafka , Quartz
主要功能:登录注册,发帖评论,点赞关注,消息提醒,热帖排行
项目亮点:
- 通过Kaptcha来生成验证码图片,作登录和注册的验证。
- 通过自定义注解和拦截器,防止用户在未登录的情况下通过url访问没有权限的页面。
- 利用数据结构Trie实现前缀树,对发表帖子评论进行简单的敏感词过滤。
- 对点赞关注等高频功能利用Redis的Set来提升性能,并通过Redis的Hyperloglog统计UV, Bitmap统计DAU。
- 利用Kafka来实现消息提醒功能,起到异步和解耦的功能。
- 使用定时任务Quartz来更新帖子的评分,实现帖子排行
注册
判断注册合法性
利用 StringUtils 判断用户名、密码、邮箱是否非空。
分别通过用户名和邮箱查询是否已经注册(为数据库的用户名和邮箱字段添加索引)。
1
2
3
4
5
6//Service层
public Map<String, Object> register(User user)
{
//Mapper层
User u = userMapper.selectByName(user.getUsername());
}
通过 set
方法为用户设置各项信息,包括 salt,MD5 加密后的密码、用户类型type,用户status状态、创建时间等,然后插入数据库。(keyProperty=”id” 标明主键)
1 | user.setSalt(CommunityUtil.generateUUID().substring(0, 5)); |
1 | **给用户发送激活邮件** |
用户表,包括 id、用户名、密码、盐值、类型(普通/管理员/版主)、状态(激活/未激活)、注册时间。
登录
生成验证码
引入 kaptcha 依赖,将验证码的大小、范围、长度等属性封装到 Properties 对象,作为参数构造 Config 对象,再用 Config 对象作为 DeafultKaptcha 对象
setConfig
方法的参数为验证码设置属性。随后在登录模块就通过kaptchaProducer.createText()生成并封装在BufferedImage里,最后通过ImageIO写到response的输出流里1
2String text = kaptchaProducer.createText();
BufferedImage image = kaptchaProducer.createImage(text);1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16@Bean
public Producer kaptchaProducer() {
Properties properties = new Properties();
properties.setProperty("kaptcha.image.width", "100");
properties.setProperty("kaptcha.image.height", "40");
properties.setProperty("kaptcha.textproducer.font.size", "32");
properties.setProperty("kaptcha.textproducer.font.color", "0,0,0");
properties.setProperty("kaptcha.textproducer.char.string", "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYAZ");
properties.setProperty("kaptcha.textproducer.char.length", "4");
properties.setProperty("kaptcha.noise.impl", "com.google.code.kaptcha.impl.NoNoise");
DefaultKaptcha kaptcha = new DefaultKaptcha();
Config config = new Config(properties);
kaptcha.setConfig(config);
return kaptcha;
}在登录的 controller 处理验证码,设置页面的响应类型为 png,通过 ImageIO 的
write
方法将图片输出到浏览器。1
2
3
4
5
6
7
8
9
10
11
12
13// 生成验证码
String text = kaptchaProducer.createText();
BufferedImage image = kaptchaProducer.createImage(text);
// 验证码的归属
// 将验证码存入Redis
// 将突图片输出给浏览器
response.setContentType("image/png");
try {
OutputStream os = response.getOutputStream();
ImageIO.write(image, "png", os);
} catch (IOException e) {
logger.error("响应验证码失败:" + e.getMessage());
}
验证码我们会生成一个随机ID,写到cookie设置过期时间,并把真正的value值设置到redis里面。在后面的登录模块就根据cookie的值从redis里面取出验证码,与传入的验证码进行验证。
判断验证码正确后,调用业务层处理
利用 StringUtils 判断用户名、密码是否非空,之后判断用户是否存在、用户是否激活、密码是否正确,将错误信息存到 map 集合。(LoginTicketMapper)
如果全部合法,为用户生成一个包含过期时间的登录凭证,将凭证存入 redis 和 map 集合。(redis的key以及cookie存储loginTicket.getTicket(),value是loginTicket)
1
2
3
4
5
6
7
8
9// 验证码的归属
String kaptchaOwner = CommunityUtil.generateUUID();
Cookie cookie = new Cookie("kaptchaOwner", kaptchaOwner);
cookie.setMaxAge(60);
cookie.setPath(contextPath);
response.addCookie(cookie);
// 将验证码存入Redis
String redisKey = RedisKeyUtil.getKaptchaKey(kaptchaOwner);
redisTemplate.opsForValue().set(redisKey, text, 60, TimeUnit.SECONDS);用户ID,ticket,状态status,expired
1
2
3
4
5LoginTicket loginTicket = new LoginTicket();
loginTicket.setUserId(user.getId());
loginTicket.setTicket(CommunityUtil.generateUUID());
loginTicket.setStatus(0);
loginTicket.setExpired(new Date(System.currentTimeMillis() + expiredSeconds * 1000));@Insert({ "insert into login_ticket(user_id,ticket,status,expired) ", "values(#{userId},#{ticket},#{status},#{expired})" }) @Options(useGeneratedKeys = true, keyProperty = "id") int insertLoginTicket(LoginTicket loginTicket);
/** * 默认状态的登录凭证的超时时间 */ int DEFAULT_EXPIRED_SECONDS = 3600 * 12; /** * 记住状态的登录凭证超时时间 */ int REMEMBER_EXPIRED_SECONDS = 3600 * 24 * 100;1
2Cookie cookie = new Cookie("ticket", map.get("ticket").toString()); cookie.setPath(contextPath); cookie.setMaxAge(expiredSeconds); response.addCookie(cookie);1
2
3
4
**根据返回的 map 是否包含登陆凭证判断登陆状态**
- 如果登录成功,将凭证存入 cookie 并重定向至首页。并且ticket也加入redis1
2// 检查凭证是否有效 if (loginTicket != null && loginTicket.getStatus() == 0 && loginTicket.getExpired().after(new Date())) { // 根据凭证查询用户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
- 如果登陆失败,将 map 中的错误信息添加到 Model 对象,返回登录页。
### 登录的改进?
用户密码是如何保存在数据库中的?如何在用户登录时验证身份信息?如何防止登录请求报文被窃取?
**Jwt与token的区别**
1)token对应的内容存放在Redis中
2)Jwt对应的playload数据存放在客户端
**Jwt优点**
1),减轻服务端压力。
2),查询效率比token高。
3),不容易被客户端篡改数据。
**缺点**
1)如果一旦生成好一个jwt之后,后期是否可以销毁
2)Jwt playload数据多,占据服务器端带宽资源
jwt不是很安全,playload中不能存放敏感的信息,必要须加密
**jwt注销**
无法做真正意义上的注销
1)用户注销时,直接将cookie缓存清除。
2)最好将jwt过期时间定义不要太长。
3)每个用户对应的盐值不一样,用户注销的时候直接将该盐值发生变化。
### 会话管理
### 显示登录信息
(HttpServletResponse)
创建 CookieUtil 工具类,通过 name 查询对应 cookie 的 value。
在 UserService 中新增 `findLoginTicket` 方法,根据 ticket 查询 LoginTicket。(redis的key以及cookie存储loginTicket.getTicket(),value是loginTicket)hostHolder.setUser(user);1
2
创建 HostHolder 类用来模拟 session 的功能,利用 ThreadLocal 实现,存储用户信息。@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME)//有效的时长 public @interface LoginRequired { }1
2
3
4
5
6
7
8
9
10
11
12
13
14
创建 LoginTicketInterceptor 拦截器,实现 HandlerInterceptor 接口。
- 在 `preHandle` 方法中通过 CookieUtil 的 `getValue` 方法查询是否有凭证 cookie,如果有则通过 UserService 的 `findloginTicket` 方法查询用户 ID,再通过用户 ID 查询用户。最后将用户放入 hostHolder 中。
- 在 `postHandle` 方法中通过 hostHolder 的 `get` 方法获取用户,并将其存入视图中。
- 在 `afterCompletion` 方法中清除 hostHolder 中存放的用户信息。
创建 WebMvcConfig 配置类,实现 WebMvcConfigurer接口,配置 LoginTicketInterceptor,拦截除了静态资源之外的所有路径。
### 检查登录状态
**目标**:防止用户通过url直接访问相应的页面。
只处理带有自定义注解的方法,防止用户在未登录情况下通过 url 访问没有权限的页面。@Component public class LoginRequiredInterceptor implements HandlerInterceptor { @Autowired private HostHolder hostHolder; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (handler instanceof HandlerMethod) { // 是不是方法 HandlerMethod handlerMethod = (HandlerMethod) handler; Method method = handlerMethod.getMethod(); LoginRequired loginRequired = method.getAnnotation(LoginRequired.class); // 获取注解 if (loginRequired != null && hostHolder.getUser() == null) { response.sendRedirect(request.getContextPath() + "/login"); return false; } } return true; } }1
2
3
4
5
6
7
8
利用 ThreadLocal 创建 HostHolder 类,包括 `set`、`get`、`remove` 方法,模拟 session 存储用户信息。
通过实现 HandlerInterceptor 接口创建一个拦截器,在 `preHandle` 方法中通过查询是否有登录凭证的 cookie,如果有则通过登录凭证查询用户 ID,再通过用户 ID 查询用户。最后将用户放入 hostHolder 中,在本次请求中持有用户信息。
创建 `@LoginRequired` 自定义注解,作用范围在方法上,有效期为运行时。为需要在登录状态下调用的方法,例如修改密码、上传头像等方法上等加上自定义注解。
创建拦截器,在 `preHandle` 中判断方法是否添加了 `@LoginRequired` 注解,如果加了并且从 hostHolder 获取不到用户则拒绝访问。public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(alphaInterceptor) .excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg") .addPathPatterns("/register", "/login");1
2
3
4
在webMVC中排除掉静态资源
// 2 2@RequestMapping(path = "/add", method = RequestMethod.POST) @ResponseBody public String addDiscussPost(String title, String content) { User user = hostHolder.getUser(); if (user == null) { return CommunityUtil.getJSONString(403, "你还没有登录哦!"); } DiscussPost post = new DiscussPost(); post.setUserId(user.getId()); post.setTitle(title); post.setContent(content); post.setCreateTime(new Date()); discussPostService.addDiscussPost(post);1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
---
### 发帖、评论、私信
**敏感词过滤**
- 创建静态内部类 TrieNode ,通过 boolean 结束符判断是否匹配到关键字尾部。
- 利用 `@PostConstruct` 注解,在构造方法执行后初始化字典树。
- 添加 `filter` 方法,利用双指针进行匹配,过滤敏感词。
**发帖、评论、私信**
- 对内容进行 HTML 转义以及过滤敏感词。
- 将信息插入数据库的帖子/评论/消息表。
发帖
涉及的数据库的字段:用户id,title,content,createTimepublic int addDiscussPost(DiscussPost post) { if (post == null) { throw new IllegalArgumentException("参数不能为空!"); } // 转义HTML标记 post.setTitle(HtmlUtils.htmlEscape(post.getTitle())); post.setContent(HtmlUtils.htmlEscape(post.getContent())); // 过滤敏感词 post.setTitle(sensitiveFilter.filter(post.getTitle())); post.setContent(sensitiveFilter.filter(post.getContent())); return discussPostMapper.insertDiscussPost(post); }1
2<insert id="insertDiscussPost" parameterType="DiscussPost" keyProperty="id"> insert into discuss_post(<include refid="insertFields"></include>) values(#{userId},#{title},#{content},#{type},#{status},#{createTime},#{commentCount},#{score}) </insert>1
2DiscussPost post = discussPostService.findDiscussPostById(discussPostId);1
2
3
4
帖子详情
- 通过帖子id,查出帖子的相关信息User user = userService.findUserById(post.getUserId());1
2
- 根据帖子实体传来的用户id查出作者long likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_POST, discussPostId);1
2
- 根据帖子类型和帖子id找出点赞数量///点赞状态 int likeStatus = hostHolder.getUser() == null ? 0 : likeService.findEntityLikeStatus(hostHolder.getUser().getId(), ENTITY_TYPE_POST, discussPostId);1
2List<Comment> commentList = commentService.findCommentsByEntity( ENTITY_TYPE_POST, post.getId(), page.getOffset(), page.getLimit());1
2
- 获取评论列表List<Comment> replyList = commentService.findCommentsByEntity( ENTITY_TYPE_COMMENT, comment.getId(), 0, Integer.MAX_VALUE);1
2
- 对每一个评论,也会根据评论进行查询public String getDiscussPost(@PathVariable("discussPostId") int discussPostId, Model model, Page page) { // 帖子 DiscussPost post = discussPostService.findDiscussPostById(discussPostId); model.addAttribute("post", post); // 作者 User user = userService.findUserById(post.getUserId()); model.addAttribute("user", user); // 点赞数量 long likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_POST, discussPostId); model.addAttribute("likeCount", likeCount); // 点赞状态 int likeStatus = hostHolder.getUser() == null ? 0 : likeService.findEntityLikeStatus(hostHolder.getUser().getId(), ENTITY_TYPE_POST, discussPostId); model.addAttribute("likeStatus", likeStatus); // 评论分页信息 page.setLimit(5); page.setPath("/discuss/detail/" + discussPostId); page.setRows(post.getCommentCount()); // 评论: 给帖子的评论 // 回复: 给评论的评论 // 评论列表 List<Comment> commentList = commentService.findCommentsByEntity( ENTITY_TYPE_POST, post.getId(), page.getOffset(), page.getLimit()); // 评论VO列表 List<Map<String, Object>> commentVoList = new ArrayList<>(); if (commentList != null) { for (Comment comment : commentList) { // 评论VO // 评论 // 作者 // 点赞数量 // 点赞状态1
2
// 回复列表
// 回复VO列表
List<Map<String, Object>> replyVoList = new ArrayList<>();
if (replyList != null) {
for (Comment reply : replyList) {
// 回复
// 作者
// 回复目标
// 点赞数量
// 点赞状态
}
}
// 回复数量
}
}
model.addAttribute("comments", commentVoList);
return "/site/discuss-detail";
}
1 |
|
1 |
|
1 |
|
1 |
|
1 |
|
1 |
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();
}
});
1 |
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// 设置key的序列化方式
template.setKeySerializer(RedisSerializer.string());
// 设置value的序列化方式
template.setValueSerializer(RedisSerializer.json());
// 设置hash的key的序列化方式
template.setHashKeySerializer(RedisSerializer.string());
// 设置hash的value的序列化方式
template.setHashValueSerializer(RedisSerializer.json());
template.afterPropertiesSet();
return template;
}
}
1 |
// 某个实体的赞
// like:entity:entityType:entityId -> set(userId)
public static String getEntityLikeKey(int entityType, int entityId) {
return PREFIX_ENTITY_LIKE + SPLIT + entityType + SPLIT + entityId;
}
1 |
|
int userId, int entityType, int entityId
1 |
|
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();
}
});
}
1 |
// 查询某用户关注的人
public List<Map<String, Object>> findFollowees(int userId, int offset, int limit) {
String followeeKey = RedisKeyUtil.getFolloweeKey(userId, ENTITY_TYPE_USER);
Set
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;
}
1 |
// 查询某用户的粉丝
public List<Map<String, Object>> findFollowers(int userId, int offset, int limit) {
String followerKey = RedisKeyUtil.getFollowerKey(ENTITY_TYPE_USER, userId);
Set
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;
}
1 |
|
public class Event {
private String topic;
private int userId;
private int entityType;
private int entityId;
private int entityUserId;
}
1 |
|
public class Message {
private int id;
private int fromId;
private int toId;
private String conversationId;
private String content;
private int status;
private Date createTime;
}
1 |
|
// 修改消息的状态
int updateStatus(List
// 查询某个主题下最新的通知
Message selectLatestNotice(int userId, String topic);
// 查询某个主题所包含的通知数量
int selectNoticeCount(int userId, String topic);
// 查询未读的通知的数量
int selectNoticeUnreadCount(int userId, String topic);
// 查询某个主题所包含的通知列表
List<Message> selectNotices(int userId, String topic, int offset, int limit);
1 |
|
map.put(“unreadCount”, messageService.findLetterUnreadCount(user.getId(), message.getConversationId()));
1 |
|
2已读
0未读
1 |
|
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
User user = hostHolder.getUser();
if (user != null && modelAndView != null) {
int letterUnreadCount = messageService.findLetterUnreadCount(user.getId(), null);
int noticeUnreadCount = messageService.findNoticeUnreadCount(user.getId(), null);
modelAndView.addObject("allUnreadCount", letterUnreadCount + noticeUnreadCount);
}
1 |
|
public class Event {
private String topic;
private int userId;
private int entityType;
private int entityId;
private int entityUserId;
private Map<String, Object> data = new HashMap<>();
}
1 |
|
public class EventProducer {
@Autowired
private KafkaTemplate kafkaTemplate;
// 处理事件
public void fireEvent(Event event) {
// 将事件发布到指定的主题
kafkaTemplate.send(event.getTopic(), JSONObject.toJSONString(event));
}
}
1 |
// 触发发帖事件
Event event = new Event()
.setTopic(TOPIC_PUBLISH)
.setUserId(user.getId())
.setEntityType(ENTITY_TYPE_POST)
.setEntityId(post.getId());
eventProducer.fireEvent(event);
1 |
|
// 消费发帖事件
@KafkaListener(topics = {TOPIC_PUBLISH})
public void handlePublishMessage(ConsumerRecord record) {
if (record == null || record.value() == null) {
logger.error("消息的内容为空!");
return;
}
Event event = JSONObject.parseObject(record.value().toString(), Event.class);
if (event == null) {
logger.error("消息格式错误!");
return;
}
DiscussPost post = discussPostService.findDiscussPostById(event.getEntityId());
elasticsearchService.saveDiscussPost(post);
}
1 |
|
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 统计UV
String ip = request.getRemoteHost();
dataService.recordUV(ip);
// 统计DAU
User user = hostHolder.getUser();
if (user != null) {
dataService.recordDAU(user.getId());
}
return true;
}
1 |
// 将指定的IP计入UV
public void recordUV(String ip) {
String redisKey = RedisKeyUtil.getUVKey(df.format(new Date()));
redisTemplate.opsForHyperLogLog().add(redisKey, ip);
}
// 统计指定日期范围内的UV
public long calculateUV(Date start, Date end) {
if (start == null || end == null) {
throw new IllegalArgumentException(“参数不能为空!”);
}
// 整理该日期范围内的key
List<String> keyList = new ArrayList<>();
Calendar calendar = Calendar.getInstance();
calendar.setTime(start);
while (!calendar.getTime().after(end)) {
String key = RedisKeyUtil.getUVKey(df.format(calendar.getTime()));
keyList.add(key);
calendar.add(Calendar.DATE, 1);
}
// 合并这些数据
String redisKey = RedisKeyUtil.getUVKey(df.format(start), df.format(end));
redisTemplate.opsForHyperLogLog().union(redisKey, keyList.toArray());
// 返回统计的结果
return redisTemplate.opsForHyperLogLog().size(redisKey);
}
1 |
|
@PostConstruct
public void init() {
// 初始化帖子列表缓存
postListCache = Caffeine.newBuilder()
.maximumSize(maxSize)
.expireAfterWrite(expireSeconds, TimeUnit.SECONDS)
.build(new CacheLoader<String, List
@Nullable
@Override
public List
if (key == null || key.length() == 0) {
throw new IllegalArgumentException(“参数错误!”);
}
String[] params = key.split(":");
if (params == null || params.length != 2) {
throw new IllegalArgumentException("参数错误!");
}
int offset = Integer.valueOf(params[0]);
int limit = Integer.valueOf(params[1]);
// 二级缓存: Redis -> mysql
logger.debug("load post list from DB.");
return discussPostMapper.selectDiscussPosts(0, offset, limit, 1);
}
});
// 初始化帖子总数缓存
postRowsCache = Caffeine.newBuilder()
.maximumSize(maxSize)
.expireAfterWrite(expireSeconds, TimeUnit.SECONDS)
.build(new CacheLoader<Integer, Integer>() {
@Nullable
@Override
public Integer load(@NonNull Integer key) throws Exception {
logger.debug("load post rows from DB.");
return discussPostMapper.selectDiscussPostRows(key);
}
});
}
1 |
|
// FactoryBean可简化Bean的实例化过程:
// 1.通过FactoryBean封装Bean的实例化过程.
// 2.将FactoryBean装配到Spring容器里.
// 3.将FactoryBean注入给其他的Bean.
// 4.该Bean得到的是FactoryBean所管理的对象实例.
// 刷新帖子分数任务
@Bean
public JobDetailFactoryBean postScoreRefreshJobDetail() {
JobDetailFactoryBean factoryBean = new JobDetailFactoryBean();
factoryBean.setJobClass(PostScoreRefreshJob.class);
factoryBean.setName("postScoreRefreshJob");
factoryBean.setGroup("communityJobGroup");
factoryBean.setDurability(true);
factoryBean.setRequestsRecovery(true);
return factoryBean;
}
@Bean
public SimpleTriggerFactoryBean postScoreRefreshTrigger(JobDetail postScoreRefreshJobDetail) {
SimpleTriggerFactoryBean factoryBean = new SimpleTriggerFactoryBean();
factoryBean.setJobDetail(postScoreRefreshJobDetail);
factoryBean.setName("postScoreRefreshTrigger");
factoryBean.setGroup("communityTriggerGroup");
factoryBean.setRepeatInterval(1000 * 60 * 5);
factoryBean.setJobDataMap(new JobDataMap());
return factoryBean;
}
1 |
|
Comment
String redisKey = RedisKeyUtil.getPostScoreKey();
redisTemplate.opsForSet().add(redisKey, discussPostId);
点赞
// 计算帖子分数
String redisKey = RedisKeyUtil.getPostScoreKey();
redisTemplate.opsForSet().add(redisKey, postId);
1 |
|
String redisKey = RedisKeyUtil.getPostScoreKey();
BoundSetOperations operations = redisTemplate.boundSetOps(redisKey);
if (operations.size() == 0) {
logger.info("[任务取消] 没有需要刷新的帖子!");
return;
}
1 |
epoch = new SimpleDateFormat(“yyyy-MM-dd HH:mm:ss”).parse(“2014-08-01 00:00:00”);
double score = Math.log10(Math.max(w, 1))
+ (post.getCreateTime().getTime() - epoch.getTime()) / (1000 * 3600 * 24);
1 |
|
@RequestParam(name = “orderMode”, defaultValue = “0”)
1 |
<select id="selectDiscussPosts" resultType="DiscussPost">
select <include refid="selectFields"></include>
from discuss_post
where status != 2
<if test="userId!=0">
and user_id = #{userId}
</if>
<if test="orderMode==0">
order by type desc, create_time desc
</if>
<if test="orderMode==1">
order by type desc, score desc, create_time desc
</if>
limit #{offset}, #{limit}
</select>
1 |
|
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers(“/resources/**”);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 授权
http.authorizeRequests()
.antMatchers(
"/user/setting",
"/user/upload",
"/discuss/add",
"/comment/add/**",
"/letter/**",
"/notice/**",
"/like",
"/follow",
"/unfollow"
)
.hasAnyAuthority(
AUTHORITY_USER,
AUTHORITY_ADMIN,
AUTHORITY_MODERATOR
)
.antMatchers(
"/discuss/top",
"/discuss/wonderful"
)
.hasAnyAuthority(
AUTHORITY_MODERATOR
)
.antMatchers(
"/discuss/delete",
"/data/**"
)
.hasAnyAuthority(
AUTHORITY_ADMIN
)
.anyRequest().permitAll()
.and().csrf().disable();
// 权限不够时的处理
http.exceptionHandling()
.authenticationEntryPoint(new AuthenticationEntryPoint() {
// 没有登录
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
String xRequestedWith = request.getHeader("x-requested-with");
if ("XMLHttpRequest".equals(xRequestedWith)) {
response.setContentType("application/plain;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.write(CommunityUtil.getJSONString(403, "你还没有登录哦!"));
} else {
response.sendRedirect(request.getContextPath() + "/login");
}
}
})
.accessDeniedHandler(new AccessDeniedHandler() {
// 权限不足
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
String xRequestedWith = request.getHeader("x-requested-with");
if ("XMLHttpRequest".equals(xRequestedWith)) {
response.setContentType("application/plain;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.write(CommunityUtil.getJSONString(403, "你没有访问此功能的权限!"));
} else {
response.sendRedirect(request.getContextPath() + "/denied");
}
}
});
// Security底层默认会拦截/logout请求,进行退出处理.
// 覆盖它默认的逻辑,才能执行我们自己的退出代码.
http.logout().logoutUrl("/securitylogout");
1 |
|
public Collection<? extends GrantedAuthority> getAuthorities(int userId) {
User user = this.findUserById(userId);
List<GrantedAuthority> list = new ArrayList<>();
list.add(new GrantedAuthority() {
@Override
public String getAuthority() {
switch (user.getType()) {
case 1:
return AUTHORITY_ADMIN;
case 2:
return AUTHORITY_MODERATOR;
default:
return AUTHORITY_USER;
}
}
});
return list;
}
1 |
|
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 从cookie中获取凭证
String ticket = CookieUtil.getValue(request, “ticket”);
if (ticket != null) {
// 查询凭证
LoginTicket loginTicket = userService.findLoginTicket(ticket);
// 检查凭证是否有效
if (loginTicket != null && loginTicket.getStatus() == 0 && loginTicket.getExpired().after(new Date())) {
// 根据凭证查询用户
User user = userService.findUserById(loginTicket.getUserId());
// 在本次请求中持有用户
hostHolder.setUser(user);
// 构建用户认证的结果,并存入SecurityContext,以便于Security进行授权.
Authentication authentication = new UsernamePasswordAuthenticationToken(
user, user.getPassword(), userService.getAuthorities(user.getId()));
SecurityContextHolder.setContext(new SecurityContextImpl(authentication));
}
}
return true;
}
1 |
|
@Repository
public interface DiscussPostRepository extends ElasticsearchRepository<DiscussPost, Integer> {
}
1 |
|
PUT my_index
{
“mappings”: {
“my_type”: {
“properties”: {
“title”: {
“type”: “string”
}
}
}
}
}
1 |
|
#! Deprecation: The [string] field is deprecated, please use [text] or [keyword] instead on [title]
{
“acknowledged”: true,
“shards_acknowledged”: true
}
1 |
|
PUT my_index
{
“mappings”: {
“my_type”: {
“properties”: {
“full_name”: {
“type”: “text”
}
}
}
}
}
```
1.3 keyword类型
keyword类型适用于索引结构化的字段,比如email地址、主机名、状态码和标签。如果字段需要进行过滤(比如查找已发布博客中status属性为published的文章)、排序、聚合。keyword类型的字段只能通过精确值搜索到。
其他
编译期和运行时注解的区别
怎么样去实现一个编译型的注解
注解是如何注入的(不是声明,是底层原理!)通用的逻辑注入的原理,是这样完成的
lamda为什么能推导出哪个类型(参数中为什么只写x,y就够了)
mybatis mapper为啥不需要实现类,jdk动态代理原理