本文主要是介绍从零开始—仿牛客网讨论社区项目(二),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
主要技术架构:
SpringBoot Spring SpringMVC MyBatis Redis Kakfa Elasticsearch Spring Security Spring Actator
1.过滤敏感词
可以使用JDK自带的replace方法替换敏感词,但在实际应用中敏感词比较多、字符串可能比较长(发布的一篇文章)这种情况下用replace去替换性能就比较差,使用前缀树来实现过滤敏感词的算法。但前缀树方法也有一些局限性,不能实现对停顿词、重复词的检查,考虑到此可以优化为DFA算法过滤敏感词。
先创建一个SensitiveUtil工具类,配置一个敏感词txt放在resource文件中。
@Component
public class SensitiveFilter {private static final Logger logger = LoggerFactory.getLogger(SensitiveFilter.class);// 替换符private static final String REPLACEMENT = "***";// 根节点private TrieNode rootNode = new TrieNode();@PostConstructpublic void init() {try (InputStream is = this.getClass().getClassLoader().getResourceAsStream("sensitive-words.txt");BufferedReader reader = new BufferedReader(new InputStreamReader(is));) {String keyword;while ((keyword = reader.readLine()) != null) {// 添加到前缀树this.addKeyword(keyword);}} catch (IOException e) {logger.error("加载敏感词文件失败: " + e.getMessage());}}// 将一个敏感词添加到前缀树中private void addKeyword(String keyword) {TrieNode tempNode = rootNode;for (int i = 0; i < keyword.length(); i++) {char c = keyword.charAt(i);TrieNode subNode = tempNode.getSubNode(c);if (subNode == null) {// 初始化子节点subNode = new TrieNode();tempNode.addSubNode(c, subNode);}// 指向子节点,进入下一轮循环tempNode = subNode;// 设置结束标识if (i == keyword.length() - 1) {tempNode.setKeywordEnd(true);}}}/*** 过滤敏感词** @param text 待过滤的文本* @return 过滤后的文本*/public String filter(String text) {if (StringUtils.isBlank(text)) {return null;}// 指针1TrieNode tempNode = rootNode;// 指针2int begin = 0;// 指针3int position = 0;// 结果StringBuilder sb = new StringBuilder();while (position < text.length()) {char c = text.charAt(position);// 跳过符号if (isSymbol(c)) {// 若指针1处于根节点,将此符号计入结果,让指针2向下走一步if (tempNode == rootNode) {sb.append(c);begin++;}// 无论符号在开头或中间,指针3都向下走一步position++;continue;}// 检查下级节点tempNode = tempNode.getSubNode(c);if (tempNode == null) {// 以begin开头的字符串不是敏感词sb.append(text.charAt(begin));// 进入下一个位置position = ++begin;// 重新指向根节点tempNode = rootNode;} else if (tempNode.isKeywordEnd()) {// 发现敏感词,将begin~position字符串替换掉sb.append(REPLACEMENT);// 进入下一个位置begin = ++position;// 重新指向根节点tempNode = rootNode;} else {// 检查下一个字符position++;}}// 将最后一批字符计入结果sb.append(text.substring(begin));return sb.toString();}// 判断是否为符号private boolean isSymbol(Character c) {// 0x2E80~0x9FFF 是东亚文字范围return !CharUtils.isAsciiAlphanumeric(c) && (c < 0x2E80 || c > 0x9FFF);}// 前缀树private class TrieNode {// 关键词结束标识private boolean isKeywordEnd = false;// 子节点(key是下级字符,value是下级节点)private Map<Character, TrieNode> subNodes = new HashMap<>();public boolean isKeywordEnd() {return isKeywordEnd;}public void setKeywordEnd(boolean keywordEnd) {isKeywordEnd = keywordEnd;}// 添加子节点public void addSubNode(Character c, TrieNode node) {subNodes.put(c, node);}// 获取子节点public TrieNode getSubNode(Character c) {return subNodes.get(c);}}
2.发布帖子
使用jQuery发送AJAX(异步)请求,网页能够将增量更新呈现在页面上,而不需要刷新整个页面 。在 Maven Repository搜索fastjson配置文件,在resources文件包内的pom.xml文件中导入相关的配置文件依赖。
在CommunityUtil类中实现获取JSON字符串的方法。
public static String getJSONString(int code, String msg, Map<String, Object> map) {JSONObject json = new JSONObject();json.put("code", code);json.put("msg", msg);if (map != null) {for (String key : map.keySet()) {json.put(key, map.get(key));}}return json.toJSONString();}public static String getJSONString(int code, String msg) {return getJSONString(code, msg, null);}public static String getJSONString(int code) {return getJSONString(code, null, null);}
在DiscussPostMapper中增加插入数据的方法。
int insertDiscussPost(DiscussPost discussPost);
在mapper文件中增加insert方法的实现。
<sql id="insertFields">user_id, title, content, type, status, create_time, comment_count, score</sql><insert id="insertDiscussPost" parameterType="DiscussPost">insert into discuss_post(<include refid="insertFields"></include>)values(#{userId},#{title},#{content},#{type},#{status},#{createTime},#{commentCount},#{score})</insert>
在DiscussPostService中增加调用的实现。
public 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);}public DiscussPost findDiscussPostById(int id) {return discussPostMapper.selectDiscussPostById(id);}public int updateCommentCount(int id, int commentCount) {return discussPostMapper.updateCommentCount(id, commentCount);}
增加DiscussPostController类,并修改index.js。
@Controller
@RequestMapping("/discuss")
public class DiscussPostController implements CommunityConstant {@Autowiredprivate DiscussPostService discussPostService;@Autowiredprivate HostHolder hostHolder;@Autowiredprivate UserService userService;@RequestMapping(path = "/add", method = RequestMethod.POST)@ResponseBodypublic 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);// 报错的情况,将来统一处理.return CommunityUtil.getJSONString(0, "发布成功!");}
$(function(){$("#publishBtn").click(publish);
});function publish() {$("#publishModal").modal("hide");// 获取标题和内容var title = $("#recipient-name").val();var content = $("#message-text").val();// 发送异步请求(POST)$.post(CONTEXT_PATH + "/discuss/add",{"title":title,"content":content},function(data) {data = $.parseJSON(data);// 在提示框中显示返回消息$("#hintBody").text(data.msg);// 显示提示框$("#hintModal").modal("show");// 2秒后,自动隐藏提示框setTimeout(function(){$("#hintModal").modal("hide");// 刷新页面if(data.code == 0) {window.location.reload();}}, 2000);});}
3.帖子详情
只需要在对应的Mapper Service 和Controller层上增加相应的功能。
DiscussPostMapper
DiscussPost selectDiscussPostById(int id);
mapper.xml
<select id="selectDiscussPostById" resultType="DiscussPost">select <include refid="selectFields"></include>from discuss_postwhere id = #{id}</select>
DiscussPostService
public DiscussPost findDiscussPostById(int id) {return discussPostMapper.selectDiscussPostById(id);}
DiscussPostController,并修改相关html文件
@RequestMapping(path = "/detail/{discussPostId}", method = RequestMethod.GET)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);return "/site/discuss-detail";}
4.事务管理
声明式事务Demo:
// REQUIRED: 支持当前事务(外部事务),如果不存在则创建新事务.// REQUIRES_NEW: 创建一个新事务,并且暂停当前事务(外部事务).// NESTED: 如果当前存在事务(外部事务),则嵌套在该事务中执行(独立的提交和回滚),否则就会REQUIRED一样.@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)public Object save1() {// 新增用户User user = new User();user.setUsername("alpha");user.setSalt(CommunityUtil.generateUUID().substring(0, 5));user.setPassword(CommunityUtil.md5("123" + user.getSalt()));user.setEmail("alpha@qq.com");user.setHeaderUrl("http://image.nowcoder.com/head/99t.png");user.setCreateTime(new Date());userMapper.insertUser(user);// 新增帖子DiscussPost post = new DiscussPost();post.setUserId(user.getId());post.setTitle("Hello");post.setContent("新人报道!");post.setCreateTime(new Date());discussPostMapper.insertDiscussPost(post);Integer.valueOf("abc");return "ok";}
编程式Demo:
@Autowiredprivate TransactionTemplate transactionTemplate;public Object save2() {transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);return transactionTemplate.execute(new TransactionCallback<Object>() {@Overridepublic Object doInTransaction(TransactionStatus status) {// 新增用户User user = new User();user.setUsername("beta");user.setSalt(CommunityUtil.generateUUID().substring(0, 5));user.setPassword(CommunityUtil.md5("123" + user.getSalt()));user.setEmail("beta@qq.com");user.setHeaderUrl("http://image.nowcoder.com/head/999t.png");user.setCreateTime(new Date());userMapper.insertUser(user);// 新增帖子DiscussPost post = new DiscussPost();post.setUserId(user.getId());post.setTitle("你好");post.setContent("我是新人!");post.setCreateTime(new Date());discussPostMapper.insertDiscussPost(post);Integer.valueOf("abc");return "ok";}});}
5.评论私信
创建一个实体类Comment
public class Comment {private int id;private int userId;private int entityType;private int entityId;private int targetId;private String content;private int status;private Date createTime;public int getId() {return id;}public void setId(int id) {this.id = id;}public int getUserId() {return userId;}public void setUserId(int userId) {this.userId = userId;}public int getEntityType() {return entityType;}public void setEntityType(int entityType) {this.entityType = entityType;}public int getEntityId() {return entityId;}public void setEntityId(int entityId) {this.entityId = entityId;}public int getTargetId() {return targetId;}public void setTargetId(int targetId) {this.targetId = targetId;}public String getContent() {return content;}public void setContent(String content) {this.content = content;}public int getStatus() {return status;}public void setStatus(int status) {this.status = status;}public Date getCreateTime() {return createTime;}public void setCreateTime(Date createTime) {this.createTime = createTime;}@Overridepublic String toString() {return "Comment{" +"id=" + id +", userId=" + userId +", entityType=" + entityType +", entityId=" + entityId +", targetId=" + targetId +", content='" + content + '\'' +", status=" + status +", createTime=" + createTime +'}';}
}
dao层 CommentMapper
@Mapper
public interface CommentMapper {List<Comment> selectCommentsByEntity(int entityType, int entityId, int offset, int limit);int selectCountByEntity(int entityType, int entityId);int insertComment(Comment comment);}
mapper
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.nowcoder.community.dao.CommentMapper"><sql id="selectFields">id, user_id, entity_type, entity_id, target_id, content, status, create_time</sql><sql id="insertFields">user_id, entity_type, entity_id, target_id, content, status, create_time</sql><select id="selectCommentsByEntity" resultType="Comment">select <include refid="selectFields"></include>from commentwhere status = 0and entity_type = #{entityType}and entity_id = #{entityId}order by create_time asclimit #{offset}, #{limit}</select><select id="selectCountByEntity" resultType="int">select count(id)from commentwhere status = 0and entity_type = #{entityType}and entity_id = #{entityId}</select><insert id="insertComment" parameterType="Comment">insert into comment(<include refid="insertFields"></include>)values(#{userId},#{entityType},#{entityId},#{targetId},#{content},#{status},#{createTime})</insert></mapper>
Service层加入了悲观锁使用的是注解的方式。
@Service
public class CommentService implements CommunityConstant {@Autowiredprivate CommentMapper commentMapper;@Autowiredprivate SensitiveFilter sensitiveFilter;@Autowiredprivate DiscussPostService discussPostService;public List<Comment> findCommentsByEntity(int entityType, int entityId, int offset, int limit) {return commentMapper.selectCommentsByEntity(entityType, entityId, offset, limit);}public int findCommentCount(int entityType, int entityId) {return commentMapper.selectCountByEntity(entityType, entityId);}@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)public int addComment(Comment comment) {if (comment == null) {throw new IllegalArgumentException("参数不能为空!");}// 添加评论comment.setContent(HtmlUtils.htmlEscape(comment.getContent()));comment.setContent(sensitiveFilter.filter(comment.getContent()));int rows = commentMapper.insertComment(comment);// 更新帖子评论数量if (comment.getEntityType() == ENTITY_TYPE_POST) {int count = commentMapper.selectCountByEntity(comment.getEntityType(), comment.getEntityId());discussPostService.updateCommentCount(comment.getEntityId(), count);}return rows;}}
controller,并修改相应的html文件。
@Controller
@RequestMapping("/comment")
public class CommentController {@Autowiredprivate CommentService commentService;@Autowiredprivate HostHolder hostHolder;@RequestMapping(path = "/add/{discussPostId}", method = RequestMethod.POST)public String addComment(@PathVariable("discussPostId") int discussPostId, Comment comment) {comment.setUserId(hostHolder.getUser().getId());comment.setStatus(0);comment.setCreateTime(new Date());commentService.addComment(comment);return "redirect:/discuss/detail/" + discussPostId;}}
私信功能与评论功能类似。
创建Message实体类
public class Message {private int id;private int fromId;private int toId;private String conversationId;private String content;private int status;private Date createTime;public int getId() {return id;}public void setId(int id) {this.id = id;}public int getFromId() {return fromId;}public void setFromId(int fromId) {this.fromId = fromId;}public int getToId() {return toId;}public void setToId(int toId) {this.toId = toId;}public String getConversationId() {return conversationId;}public void setConversationId(String conversationId) {this.conversationId = conversationId;}public String getContent() {return content;}public void setContent(String content) {this.content = content;}public int getStatus() {return status;}public void setStatus(int status) {this.status = status;}public Date getCreateTime() {return createTime;}public void setCreateTime(Date createTime) {this.createTime = createTime;}@Overridepublic String toString() {return "Message{" +"id=" + id +", fromId=" + fromId +", toId=" + toId +", conversationId='" + conversationId + '\'' +", content='" + content + '\'' +", status=" + status +", createTime=" + createTime +'}';}
}
创建MessgaeMapper
@Mapper
public interface MessageMapper {// 查询当前用户的会话列表,针对每个会话只返回一条最新的私信.List<Message> selectConversations(int userId, int offset, int limit);// 查询当前用户的会话数量.int selectConversationCount(int userId);// 查询某个会话所包含的私信列表.List<Message> selectLetters(String conversationId, int offset, int limit);// 查询某个会话所包含的私信数量.int selectLetterCount(String conversationId);// 查询未读私信的数量int selectLetterUnreadCount(int userId, String conversationId);// 新增消息int insertMessage(Message message);// 修改消息的状态int updateStatus(List<Integer> ids, int status);}
配置message-mapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.nowcoder.community.dao.MessageMapper"><sql id="selectFields">id, from_id, to_id, conversation_id, content, status, create_time</sql><sql id="insertFields">from_id, to_id, conversation_id, content, status, create_time</sql><select id="selectConversations" resultType="Message">select <include refid="selectFields"></include>from messagewhere id in (select max(id) from messagewhere status != 2and from_id != 1and (from_id = #{userId} or to_id = #{userId})group by conversation_id)order by id desclimit #{offset}, #{limit}</select><select id="selectConversationCount" resultType="int">select count(m.maxid) from (select max(id) as maxid from messagewhere status != 2and from_id != 1and (from_id = #{userId} or to_id = #{userId})group by conversation_id) as m</select><select id="selectLetters" resultType="Message">select <include refid="selectFields"></include>from messagewhere status != 2and from_id != 1and conversation_id = #{conversationId}order by id desclimit #{offset}, #{limit}</select><select id="selectLetterCount" resultType="int">select count(id)from messagewhere status != 2and from_id != 1and conversation_id = #{conversationId}</select><select id="selectLetterUnreadCount" resultType="int">select count(id)from messagewhere status = 0and from_id != 1and to_id = #{userId}<if test="conversationId!=null">and conversation_id = #{conversationId}</if></select><insert id="insertMessage" parameterType="Message" keyProperty="id">insert into message(<include refid="insertFields"></include>)values(#{fromId},#{toId},#{conversationId},#{content},#{status},#{createTime})</insert><update id="updateStatus">update message set status = #{status}where id in<foreach collection="ids" item="id" open="(" separator="," close=")">#{id}</foreach></update></mapper>
创建MessageService
@Service
public class MessageService {@Autowiredprivate MessageMapper messageMapper;@Autowiredprivate SensitiveFilter sensitiveFilter;public List<Message> findConversations(int userId, int offset, int limit) {return messageMapper.selectConversations(userId, offset, limit);}public int findConversationCount(int userId) {return messageMapper.selectConversationCount(userId);}public List<Message> findLetters(String conversationId, int offset, int limit) {return messageMapper.selectLetters(conversationId, offset, limit);}public int findLetterCount(String conversationId) {return messageMapper.selectLetterCount(conversationId);}public int findLetterUnreadCount(int userId, String conversationId) {return messageMapper.selectLetterUnreadCount(userId, conversationId);}public int addMessage(Message message) {message.setContent(HtmlUtils.htmlEscape(message.getContent()));message.setContent(sensitiveFilter.filter(message.getContent()));return messageMapper.insertMessage(message);}public int readMessage(List<Integer> ids) {return messageMapper.updateStatus(ids, 1);}}
创建MeassageController,并修改相关的html文件。
@Controller
public class MessageController {@Autowiredprivate MessageService messageService;@Autowiredprivate HostHolder hostHolder;@Autowiredprivate UserService userService;// 私信列表@RequestMapping(path = "/letter/list", method = RequestMethod.GET)public String getLetterList(Model model, Page page) {User user = hostHolder.getUser();// 分页信息page.setLimit(5);page.setPath("/letter/list");page.setRows(messageService.findConversationCount(user.getId()));// 会话列表List<Message> conversationList = messageService.findConversations(user.getId(), page.getOffset(), page.getLimit());List<Map<String, Object>> conversations = new ArrayList<>();if (conversationList != null) {for (Message message : conversationList) {Map<String, Object> map = new HashMap<>();map.put("conversation", message);map.put("letterCount", messageService.findLetterCount(message.getConversationId()));map.put("unreadCount", messageService.findLetterUnreadCount(user.getId(), message.getConversationId()));int targetId = user.getId() == message.getFromId() ? message.getToId() : message.getFromId();map.put("target", userService.findUserById(targetId));conversations.add(map);}}model.addAttribute("conversations", conversations);// 查询未读消息数量int letterUnreadCount = messageService.findLetterUnreadCount(user.getId(), null);model.addAttribute("letterUnreadCount", letterUnreadCount);return "/site/letter";}@RequestMapping(path = "/letter/detail/{conversationId}", method = RequestMethod.GET)public String getLetterDetail(@PathVariable("conversationId") String conversationId, Page page, Model model) {// 分页信息page.setLimit(5);page.setPath("/letter/detail/" + conversationId);page.setRows(messageService.findLetterCount(conversationId));// 私信列表List<Message> letterList = messageService.findLetters(conversationId, page.getOffset(), page.getLimit());List<Map<String, Object>> letters = new ArrayList<>();if (letterList != null) {for (Message message : letterList) {Map<String, Object> map = new HashMap<>();map.put("letter", message);map.put("fromUser", userService.findUserById(message.getFromId()));letters.add(map);}}model.addAttribute("letters", letters);// 私信目标model.addAttribute("target", getLetterTarget(conversationId));// 设置已读List<Integer> ids = getLetterIds(letterList);if (!ids.isEmpty()) {messageService.readMessage(ids);}return "/site/letter-detail";}private User getLetterTarget(String conversationId) {String[] ids = conversationId.split("_");int id0 = Integer.parseInt(ids[0]);int id1 = Integer.parseInt(ids[1]);if (hostHolder.getUser().getId() == id0) {return userService.findUserById(id1);} else {return userService.findUserById(id0);}}private List<Integer> getLetterIds(List<Message> letterList) {List<Integer> ids = new ArrayList<>();if (letterList != null) {for (Message message : letterList) {if (hostHolder.getUser().getId() == message.getToId() && message.getStatus() == 0) {ids.add(message.getId());}}}return ids;}@RequestMapping(path = "/letter/send", method = RequestMethod.POST)@ResponseBodypublic String sendLetter(String toName, String content) {User target = userService.findUserByName(toName);if (target == null) {return CommunityUtil.getJSONString(1, "目标用户不存在!");}Message message = new Message();message.setFromId(hostHolder.getUser().getId());message.setToId(target.getId());if (message.getFromId() < message.getToId()) {message.setConversationId(message.getFromId() + "_" + message.getToId());} else {message.setConversationId(message.getToId() + "_" + message.getFromId());}message.setContent(content);message.setCreateTime(new Date());messageService.addMessage(message);return CommunityUtil.getJSONString(0);}}
6异常处理
使用@ControllerAdvice处理异常,在Controller文件下创建Advice文件包,创建ExceptionAdivce类。
@ControllerAdvice(annotations = Controller.class)
public class ExceptionAdvice {private static final Logger logger = LoggerFactory.getLogger(ExceptionAdvice.class);@ExceptionHandler({Exception.class})public void handleException(Exception e, HttpServletRequest request, HttpServletResponse response) throws IOException {logger.error("服务器发生异常: " + e.getMessage());for (StackTraceElement element : e.getStackTrace()) {logger.error(element.toString());}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(1, "服务器异常!"));} else {response.sendRedirect(request.getContextPath() + "/error");}}}
统一记录日志
切面示例:
@Component
@Aspect
public class AlphaAspect {@Pointcut("execution(* com.nowcoder.community.service.*.*(..))")public void pointcut() {}@Before("pointcut()")public void before() {System.out.println("before");}@After("pointcut()")public void after() {System.out.println("after");}@AfterReturning("pointcut()")public void afterRetuning() {System.out.println("afterRetuning");}@AfterThrowing("pointcut()")public void afterThrowing() {System.out.println("afterThrowing");}@Around("pointcut()")public Object around(ProceedingJoinPoint joinPoint) throws Throwable {System.out.println("around before");Object obj = joinPoint.proceed();System.out.println("around after");return obj;}}
创建Aspect文件包,创建ServiceLogAspect。
@Component
@Aspect
public class ServiceLogAspect {private static final Logger logger = LoggerFactory.getLogger(ServiceLogAspect.class);@Pointcut("execution(* com.nowcoder.community.service.*.*(..))")public void pointcut() {}@Before("pointcut()")public void before(JoinPoint joinPoint) {// 用户[1.2.3.4],在[xxx],访问了[com.nowcoder.community.service.xxx()].ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();HttpServletRequest request = attributes.getRequest();String ip = request.getRemoteHost();String now = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());String target = joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName();logger.info(String.format("用户[%s],在[%s],访问了[%s].", ip, now, target));}}
至此讨论区基本功能实现完毕!
接下来涉及到:
Redis 一站式高性能存储方案
Kafka 构建TB级异步消息系统
Elasticsearch 分布式搜索引擎
Spring Security 构建安全的企业服务
项目代码及相关资源:Ming-XMU (Yiming Zhang) · GitHub
麻烦点点小星星!!!!!!
CSDN下载需要积分基于SpringBoot仿牛客网讨论社区项目-Java文档类资源-CSDN下载
从零开始—仿牛客网讨论社区项目(一)_芙蓉铁蛋的博客-CSDN博客
从零开始—仿牛客网讨论社区项目(二)_芙蓉铁蛋的博客-CSDN博客
从零开始—仿牛客网讨论社区项目(三)_芙蓉铁蛋的博客-CSDN博客
从零开始—仿牛客网讨论社区项目(四)_芙蓉铁蛋的博客-CSDN博客
从零开始—仿牛客网讨论社区项目(五)_芙蓉铁蛋的博客-CSDN博客
从零开始—仿牛客网讨论社区项目(六)_芙蓉铁蛋的博客-CSDN博客
仿牛客网讨论社区项目—优化网站性能_芙蓉铁蛋的博客-CSDN博客
仿牛客网讨论社区项目—项目总结及项目常见面试题_芙蓉铁蛋的博客-CSDN博客
这篇关于从零开始—仿牛客网讨论社区项目(二)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!