{"id":20791102,"url":"https://github.com/xumingyu2018/demonowcoder","last_synced_at":"2025-05-05T21:10:28.935Z","repository":{"id":183128794,"uuid":"482283545","full_name":"xumingyu2018/DemoNowcoder","owner":"xumingyu2018","description":"仿牛客网项目","archived":false,"fork":false,"pushed_at":"2023-03-08T07:13:55.000Z","size":43968,"stargazers_count":8,"open_issues_count":0,"forks_count":5,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-03-30T23:41:12.032Z","etag":null,"topics":["java","spring-boot"],"latest_commit_sha":null,"homepage":"https://xumingyu2018.github.io/projects/nowcoder-demo.html","language":"HTML","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/xumingyu2018.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null}},"created_at":"2022-04-16T15:00:21.000Z","updated_at":"2024-04-09T11:51:44.000Z","dependencies_parsed_at":null,"dependency_job_id":"a75a54a3-00f3-4a5d-ad6c-3994053516bb","html_url":"https://github.com/xumingyu2018/DemoNowcoder","commit_stats":null,"previous_names":["xumingyu2018/demonowcoder"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/xumingyu2018%2FDemoNowcoder","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/xumingyu2018%2FDemoNowcoder/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/xumingyu2018%2FDemoNowcoder/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/xumingyu2018%2FDemoNowcoder/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/xumingyu2018","download_url":"https://codeload.github.com/xumingyu2018/DemoNowcoder/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":252577020,"owners_count":21770721,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["java","spring-boot"],"created_at":"2024-11-17T15:41:12.412Z","updated_at":"2025-05-05T21:10:28.880Z","avatar_url":"https://github.com/xumingyu2018.png","language":"HTML","funding_links":[],"categories":[],"sub_categories":[],"readme":"# 仿牛客网项目学习\n\n# 主页讨论区分页查询功能！\n\n## 1.首先设计Dao层接口（实体类略）\n\n**以下是查询功能不包括分页** **（其中userId在DiscussPost类中作为外键）**\n\n```java\n//查询\n//userId=0为所有帖子，1为我的帖子\n//每个参数必须加@Param(\"\")\nList\u003cDiscussPost\u003e selectDiscussPosts(@Param(\"userId\") int userId,@Param(\"offset\")int offset,@Param(\"limit\")int limit);\n\n//为分页查询服务的查询总条数\n//给参数起别名，如果只有一个参数并且要在\u003cif\u003e里使用，则必须加别名\nint selectDiscussRows(@Param(\"userId\")int userId);\n```\n\n```sql\n  \u003c!------------- Mapper.xml -------------\u003e\n  \u003csql id=\"selectFields\"\u003e\n      id,user_id,title,content,type,status,create_time,comment_count,score\n  \u003c/sql\u003e\n  \u003csql id=\"insertFields\"\u003e\n      user_id,title,content,type,status,create_time,comment_count,score\n  \u003c/sql\u003e\n\n  \u003c!--查询不是被拉黑的帖子并且userId不为0按照type指定，时间排序--\u003e\n  \u003cselect id=\"selectDiscussPosts\" resultType=\"DiscussPost\"\u003e\n      select \u003cinclude refid=\"selectFields\"\u003e\u003c/include\u003e\n      from discuss_post\n      where status!=2\n      \u003cif test=\"userId!=0\"\u003e\n          and user_id=#{userId}\n      \u003c/if\u003e\n      \u003cif test=\"orderMode==0\"\u003e\n          order by type desc,create_time desc\n      \u003c/if\u003e\n      \u003cif test=\"orderMode==1\"\u003e\n          order by type desc,score desc,create_time desc\n      \u003c/if\u003e\n      limit #{offset},#{limit}\n  \u003c/select\u003e\n\n  \u003c!--userId=0查所有;userId!=0查个人发帖数--\u003e\n  \u003cselect id=\"selectDiscussRows\" resultType=\"int\"\u003e\n      select count(id)\n      from discuss_post\n      where status!=2\n      \u003cif test=\"userId!=0\"\u003e\n          and user_id=#{userId}\n      \u003c/if\u003e\n  \u003c/select\u003e\n\n```\n\n## 2.然后设计Service层调用Dao层接口\n\n```java\n  @Autowired\n  private DiscussPostMapper discussPostMapper;\n  \n  public List\u003cDiscussPost\u003e findDiscussPosts(int userId,int offset,int limit){\n      return discussPostMapper.selectDiscussPosts(userId,offset,limit);\n  }\n  \n  public int findDiscussPostRows(int userId){\n      return discussPostMapper.selectDiscussRows(userId);\n  }\n```\n\n## 3.其次封装分页功能\n\n**封装分页功能相关信息在Page类！！**\n\n```java\npublic class Page {\n\n    //当前页面\n    private int current=1;\n    //显示上限\n    private int limit=6;\n    //数据总数(用于计算总页数)\n    private int rows;\n    //查询路径(用于复用分页链接)\n    private String path;\n\n    public int getCurrent() {\n        return current;\n    }\n\n    public void setCurrent(int current) {\n        //要作输入判断\n        if (current\u003e=1){\n            this.current = current;\n        }\n    }\n\n    public int getLimit() {\n        return limit;\n    }\n\n    public void setLimit(int limit) {\n        if (limit\u003e=1\u0026\u0026limit\u003c=100){\n            this.limit = limit;\n        }\n    }\n\n    public int getRows() {\n        return rows;\n    }\n\n    public void setRows(int rows) {\n        if (rows\u003e=0){\n            this.rows = rows;\n        }\n    }\n\n    public String getPath() {\n        return path;\n    }\n\n    public void setPath(String path) {\n        this.path = path;\n    }\n    \n    /** 获取当前页的起始行**/\n    public int getOffset(){\n        //current*limit-limit\n        return (current-1)*limit;\n    }\n    \n    /**获取总页数**/\n    public int getTotal(){\n        //rows/limit[+1]\n        if (rows%limit==0){\n            return rows/limit;\n        }else{\n            return rows/limit+1;\n        }\n    }\n    \n    /**获取起始页码**/\n    public int getFrom(){\n        int from=current-2;\n        return from \u003c 1 ? 1 : from;\n    }\n    \n    /**获取结束页码**/\n    public int getTo(){\n        int to=current+2;\n        int total=getTotal();\n        return to \u003e total ? total : to;\n    }\n}\n```\n\n## 4.最后设计Controller层\n\n```java\n    @Autowired\n    private DiscussPostService discussPostService;\n\n    @Autowired\n    private UserService userService;\n\n    @RequestMapping(value = \"/index\",method = RequestMethod.GET)\n    public String getIndexPage(Model model, Page page){//传入model参数是因为要返回值给View\n        /*方法调用前，springMVC自动实例化Model和Page,并将Page注入Model\n          在thymeleaf中可以直接访问Page对象中的数据 */\n        \n        //分页\n        page.setRows(discussPostService.findDiscussPostRows(0));\n        page.setPath(\"/community/index\");\n        \n        //查询所有，起始为page.getOffset()，终止为page.getLimit()个帖子，\n        List\u003cDiscussPost\u003e list=discussPostService.findDiscussPosts(0, page.getOffset(), page.getLimit());\n        \n        /*将查询的post帖子和user用户名拼接后放入map中,最后把全部map放入新的List中,\n          因为UserId是外键，需要显示的是对应的名字即可 */\n        List\u003cMap\u003cString,Object\u003e\u003e discussPost =new ArrayList\u003c\u003e();\n\n        if (list!=null){\n            for(DiscussPost post:list){\n                HashMap\u003cString, Object\u003e map = new HashMap\u003c\u003e();\n                // 将查询到的帖子放入map\n                map.put(\"post\",post);\n                // 将发布帖子对应的用户id作为参数\n                User user = userService.findUser(post.getUserId());\n                // 将发帖子的所有用户放入map\n                map.put(\"user\",user);\n                // 显示帖子点赞数量\n                long likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_POST, post.getId());\n                map.put(\"likeCount\", likeCount);\n                \n                //将组合的map放入List\u003c\u003e\n                discussPost.add(map);\n            }\n        }\n        model.addAttribute(\"discussPosts\",discussPost);\n        return \"/index\";\n    }\n```\n\n## 5.前端页面设计（Thymeleaf）\n\n### 5.1查询页面\n\n```html\n  \u003c!-- 帖子列表 --\u003e\n  \u003cul class=\"list-unstyled\"\u003e\n  \u003c!--th:each=\"map:${discussPosts}循环遍历model.addAttribute传过来的discussPosts这个集合，每次循环得到map对象--\u003e\n    \u003cli class=\"media pb-3 pt-3 mb-3 border-bottom\" th:each=\"map:${discussPosts}\"\u003e\n      \u003ca href=\"site/profile.html\"\u003e\n  \u003c!--th:src=\"${map.user.headerUrl}\"底层是map.get(\"user\")-\u003euser.get(\"headerUrl\")--\u003e\n        \u003cimg th:src=\"${map.user.headerUrl}\" class=\"mr-4 rounded-circle\" alt=\"用户头像\" style=\"width:50px;height:50px;\"\u003e\n      \u003c/a\u003e\n      \u003cdiv class=\"media-body\"\u003e\n        \u003ch6 class=\"mt-0 mb-3\"\u003e\n  \u003c!--th:utext可以转义文本中特殊字符--\u003e\n          \u003ca href=\"#\" th:utext=\"${map.post.title}\"\u003e备战春招，面试刷题跟他复习，一个月全搞定！\u003c/a\u003e\n          \u003cspan class=\"badge badge-secondary bg-primary\" th:if=\"${map.post.type==1}\"\u003e置顶\u003c/span\u003e\n          \u003cspan class=\"badge badge-secondary bg-danger\" th:if=\"${map.post.status==1}\"\u003e精华\u003c/span\u003e\n        \u003c/h6\u003e\n        \u003cdiv class=\"text-muted font-size-12\"\u003e\n  \u003c!--th:text=\"${#dates.format(map.post.createTime)} #是引用thymeleaf自带的工具--\u003e\n          \u003cu class=\"mr-3\" th:utext=\"${map.user.username}\"\u003e寒江雪\u003c/u\u003e 发布于 \u003cb th:text=\"${#dates.format(map.post.createTime,'yyyy-MM-dd HH:mm:ss')}\"\u003e2019-04-15 15:32:18\u003c/b\u003e\n          \u003cul class=\"d-inline float-right\"\u003e\n            \u003cli class=\"d-inline ml-2\"\u003e赞 11\u003c/li\u003e\n            \u003cli class=\"d-inline ml-2\"\u003e|\u003c/li\u003e\n            \u003cli class=\"d-inline ml-2\"\u003e回帖 7\u003c/li\u003e\n          \u003c/ul\u003e\n        \u003c/div\u003e\n      \u003c/div\u003e            \n    \u003c/li\u003e\n  \u003c/ul\u003e\n```\n\n### 5.2分页功能页面\n\n```html\n  \u003c!-- 分页 --\u003e\n  \u003cnav class=\"mt-5\" th:if=\"${page.rows\u003e0}\" th:fragment=\"pagination\"\u003e\n    \u003cul class=\"pagination justify-content-center\"\u003e\n      \u003cli class=\"page-item\"\u003e\n  \u003c!--th:href=\"@{${page.path}(current=1,limit=5)}\"等效于/index?current=1\u0026limit=5--\u003e\n        \u003ca class=\"page-link\" th:href=\"@{${page.path}(current=1)}\"\u003e首页\u003c/a\u003e\n      \u003c/li\u003e\n  \u003c!--th:class=\"|page-item ${page.current==1?'disabled':''}|\" 动态上一页禁用  固定数据+变量使用方法:加|| --\u003e\n      \u003cli th:class=\"|page-item ${page.current==1?'disabled':''}|\"\u003e\n        \u003ca class=\"page-link\" th:href=\"@{${page.path}(current=${page.current-1})}\"\u003e上一页\u003c/a\u003e\n      \u003c/li\u003e\n  \u003c!--#numbers.sequence #调用thymelead自带工具numbers,从from到to的数组--\u003e\n      \u003cli th:class=\"|page-item ${i==page.current?'active':''}|\" th:each=\"i:${#numbers.sequence(page.from,page.to)}\"\u003e\n        \u003ca class=\"page-link\" href=\"#\" th:text=\"${i}\"\u003e1\u003c/a\u003e\n      \u003c/li\u003e\n\n      \u003cli th:class=\"|page-item ${page.current==page.total?'disabled':''}|\"\u003e\n        \u003ca class=\"page-link\" th:href=\"@{${page.path}(current=${page.current+1})}\"\u003e下一页\u003c/a\u003e\n      \u003c/li\u003e\n      \u003cli class=\"page-item\"\u003e\n        \u003ca class=\"page-link\" th:href=\"@{${page.path}(current=${page.total})}\"\u003e末页\u003c/a\u003e\n      \u003c/li\u003e\n    \u003c/ul\u003e\n  \u003c/nav\u003e\n```\n\n# 注册登录功能\n\n## 发送邮件\n\n### 1.邮箱设置：启用SMTP服务\n\n### 2.SpringEmail\n\n#### 2.1配置xml文件\n\n```xml\n\u003cdependency\u003e\n    \u003cgroupId\u003eorg.springframework.boot\u003c/groupId\u003e\n    \u003cartifactId\u003espring-boot-starter-mail\u003c/artifactId\u003e\n    \u003cversion\u003e2.6.6\u003c/version\u003e\n\u003c/dependency\u003e\n\n```\n\n#### 2.2在application.yml配置邮箱参数\n\n```yaml\n#  配置邮箱\nspring:\n  mail:\n    host: smtp.qq.com\n    port: 465\n    username: xxx@qq.com //本网站的发送方\n    password: xxx  //密码为生成授权码后给的密码\n    protocol: smtps\n```\n\n#### 2.3创建MailClient邮箱工具类\n\n```java\n@Component\npublic class MailClient {\n\n    private static final Logger logger= LoggerFactory.getLogger(MailClient.class);\n\n    @Autowired\n    private JavaMailSender javaMailSender;\n\n    @Value(\"${spring.mail.username}\")//将yml的属性注入到from\n    private String from;\n\n    public void sendMail(String to,String subject,String content){\n        try {\n        //MimeMessage用于封装邮件相关信息\n            MimeMessage message = javaMailSender.createMimeMessage();\n            //需要一个邮件帮助器，负责构建MimeMessage对象\n            MimeMessageHelper helper = new MimeMessageHelper(message);\n            helper.setFrom(from);\n            helper.setTo(to);\n            helper.setSubject(subject);\n            //支持HTML文本\n            helper.setText(content,true);\n            //发送邮件都有JavaMailSender来做\n            javaMailSender.send(helper.getMimeMessage());\n        }catch (MessagingException e){\n            logger.error(\"发送邮件失败：\"+e.getMessage());\n        }\n    }\n}\n```\n\n#### 2.4测试类\n\n````java\n@Autowired\nprivate MailClient mailClient;\n\n@Autowired\nprivate TemplateEngine templateEngine;//注入HTML模板引擎类，模板格式化\n\n@Test\npublic void testTextMail(){//发送文本类型邮件\n    mailClient.sendMail(\"xmy981022@163.com\",\"Test\",\"Welcome\");\n}\n\n@Test\npublic void testHTMLMail(){//发送thymeleaf html类型文件\n    Context context = new Context();\n    context.setVariable(\"username\",\"Nevermore\");\n    String content = templateEngine.process(\"/mail/activation\", context);\n    mailClient.sendMail(\"xmy981022@163.com\",\"HTML\",content);\n}\n\n\n注意：JavaMailSender和TemplateEngine会被自动注入到spring中\n\n````\n\n## 注册功能\n\n\n### 1.配置application.properties文件\n\n\n````yml\ncommunity.path.domain: http://localhost:8080\nserver.servlet.context-path: /community\n````\n\n### 2.创建工具类（处理MD5加密、生成随机数、激活标志接口）\n\n```java\npublic class CommunityUtil {\n    /*\n    * 生成随机字符串\n    * 用于邮件激活码，salt5位随机数加密\n    **/\n    public static String generateUUID(){\n        return UUID.randomUUID().toString().replaceAll(\"-\",\"\");\n    }\n    /* MD5加密\n    * hello--\u003eabc123def456\n    * hello + 3e4a8--\u003eabc123def456abc\n    */\n    public static String md5(String key){\n        if (StringUtils.isBlank(key)){\n            return null;\n        }\n        //MD5加密方法\n        return DigestUtils.md5DigestAsHex(key.getBytes());\n        //参数是bytes型\n    }\n}\n```\n\n```java\npublic interface CommuityConstant {\n    /*      以下用于注册功能      */\n    /** 激活成功*/\n    int ACTIVATION_SUCCESS=0;\n    /** 重复激活 */\n    int ACTIVATION_REPEAT=1;\n    /** 激活失败 */\n    int ACTIVATION_FAILURE=2;\n    \n    /*      以下用于登录功能*      /\n    /**  \n     * 默认状态的登录凭证的超时时间\n     */\n    int DEFAULT_EXPIRED_SECONDS=3600*12;\n    /**\n     * 记住状态的登录凭证超时时间\n     */\n    int REMEMBER_EXPIRED_SECONDS=3600*24*7;\n}\n```\n\n### 3.编写Service业务层(实现CommuityConstant接口)\n\n#### 3.1注册业务\n\n```java\n//..注入userMapper，mailClient，templateEngine\n@Value(\"${community.path.domain}\")\nprivate String domain;\n@Value(\"${server.servlet.context-path}\")\nprivate String contextPath;\n//注册功能\n/**为什么返回的是Map类型，因为用Map来存各种情况下的信息，返回给前端页面* */\npublic Map\u003cString,Object\u003e register(User user){\n    HashMap\u003cString, Object\u003e map = new HashMap\u003c\u003e();\n    /*\n        判输入\n     */\n    if (user == null) {\n        throw new IllegalArgumentException(\"参数不能为空！\");\n    }\n    if (StringUtils.isBlank(user.getUsername())){\n        map.put(\"usernameMsg\",\"账户不能为空\");\n    }\n    if (StringUtils.isBlank(user.getPassword())){\n        map.put(\"passwordMsg\",\"密码不能为空\");\n    }\n    if (StringUtils.isBlank(user.getEmail())){\n        map.put(\"emailMsg\",\"邮箱不能为空\");\n    }\n    /*\n        判存在\n     */\n    User u = userMapper.selectByName(user.getUsername());\n    if (u != null) {\n        map.put(\"usernameMsg\",\"该账号已存在！\");\n        return map;\n    }\n    u = userMapper.selectByEmail(user.getEmail());\n    if (u != null) {\n        map.put(\"emailMsg\",\"该邮箱已被注册！\");\n        return map;\n    }\n    /*\n        注册账户\n        1.设置salt加密(随机5位数加入密码)\n        2.设置密码+salt\n        3.设置随机数激活码\n        4.设置status,type=0,时间\n        5.设置头像(动态)\n          user.setHeaderUrl(String.format(\"http://images.nowcoder.com/head/%dt.png\",new Random().nextInt(1000))\n     */\n    user.setSalt(CommunityUtil.generateUUID().substring(0,5));\n    user.setPassword(CommunityUtil.md5(user.getPassword()+user.getSalt()));\n    user.setActivationCode(CommunityUtil.generateUUID());\n    user.setHeaderUrl(String.format(\"http://images.nowcoder.com/head/%dt.png\", new Random().nextInt(1000)));\n    user.setStatus(0);\n    user.setType(0);\n    user.setCreateTime(new Date());\n    userMapper.insertUser(user);\n    /*\n        激活邮件\n        1.创建Context对象--\u003econtext.setVariable(name,value)将name传入前端\n          为thymeleaf提供变量\n        2.设置email和url\n        3.templateEngine.process执行相应HTML\n        4.发送邮件\n     */\n    Context context = new Context();\n    context.setVariable(\"email\",user.getEmail());\n    //http://localhost:8080/community/activation/101/code激活链接\n    String url=domain+contextPath+\"/activation/\"+user.getId()+\"/\"+user.getActivationCode();\n    context.setVariable(\"url\",url);\n\n    String content = templateEngine.process(\"/mail/activation\", context);\n    mailClient.sendMail(user.getEmail(),\"激活账号\",content);\n    return map;\n}\n```\n\n#### \u0026#x20;  3.2激活邮件业务\n\n```java\n/**激活邮件功能* */\n  public int activation(int userId,String code){\n      User user = userMapper.selectById(userId);\n      if (user.getStatus()==1){\n          return ACTIVATION_REPEAT;\n      }else if (user.getActivationCode().equals(code)){\n          userMapper.updateStatus(userId,1);\n          return ACTIVATION_SUCCESS;\n      }else {\n          return ACTIVATION_FAILURE;\n      }\n  }\n```\n\n### 4.编写Controller层\n\n```java\n//注册Controller\n@RequestMapping(value = \"/register\",method = RequestMethod.POST)\npublic String register(Model model, User user){\n    Map\u003cString, Object\u003e map = userService.register(user);\n    if (map == null || map.isEmpty()){\n        map.put(\"msg\",\"注册成功,我们已经向您的邮件发送了一封激活邮件,请尽快激活！\");\n        map.put(\"target\",\"/index\");\n        return \"/site/operate-result\";\n    }else{\n        model.addAttribute(\"usernameMsg\",map.get(\"usernameMsg\"));\n        model.addAttribute(\"passwordMsg\",map.get(\"passwordMsg\"));\n        model.addAttribute(\"emailMsg\",map.get(\"emailMsg\"));\n        return \"/site/register\";\n    }\n}\n```\n\n```java\n/**激活邮件Controller**/\n//http://localhost:8080/community/activation/101/code激活链接\n@RequestMapping(value = \"/activation/{userId}/{code}\",method = RequestMethod.GET)\npublic String activation(Model model, @PathVariable(\"userId\") int userId,@PathVariable(\"code\") String code){\n    int result = userService.activation(userId, code);\n    if (result == ACTIVATION_SUCCESS){\n        model.addAttribute(\"msg\",\"激活成功,你的账号已经可以正常使用了！\");\n        model.addAttribute(\"target\",\"/login\");\n    }else if (result == ACTIVATION_REPEAT){\n        model.addAttribute(\"msg\",\"无效操作,该账号已经激活过了！\");\n        model.addAttribute(\"target\",\"/index\");\n    }else {\n        model.addAttribute(\"msg\",\"激活失败,你提供的激活码不正确！\");\n        model.addAttribute(\"target\",\"/index\");\n    }\n    return \"/site/operate-result\";\n}\n```\n\n### 5.编写前端Thymeleaf页面核心点\n\n```html\n/**注册页面 */\n\u003cform class=\"mt-5\" method=\"post\" th:action=\"@{/register}\"\u003e\n  \u003cdiv class=\"col-sm-10\"\u003e\n    \u003cinput type=\"text\"\n         th:class=\"|form-control ${usernameMsg!=null?'is-invalid':''}|\"\n         th:value=\"${user!=null?user.username:''}\"\n         id=\"username\" name=\"username\" placeholder=\"请输入您的账号!\" required\u003e\n    \u003cdiv class=\"invalid-feedback\" th:text=\"${usernameMsg}\"\u003e\n      该账号已存在!    \u003c!--该div的显示与is-invalid有关--\u003e\n    \u003c/div\u003e\n    \u003cbutton type=\"submit\" class=\"btn btn-info text-white form-control\"\u003e立即注册\u003c/button\u003e\n  \u003c/div\u003e\n\u003c/form\u003e\n\n/**账号激活中间页* */\n\u003cdiv class=\"jumbotron\"\u003e\n  \u003cp class=\"lead\" th:text=\"${msg}\"\u003e激活状态信息\u003c/p\u003e\n  \u003cp\u003e\n    系统会在 \u003cspan id=\"seconds\" class=\"text-danger\"\u003e8\u003c/span\u003e 秒后自动跳转,\n    您也可以点此 \u003ca id=\"target\" th:href=\"@{${target}}\" class=\"text-primary\"\u003e链接\u003c/a\u003e, 手动跳转!\n  \u003c/p\u003e\n\u003c/div\u003e\n\u003c!--自动跳转Js --\u003e\n\u003cscript\u003e\n  $(function(){\n    setInterval(function(){\n      var seconds = $(\"#seconds\").text();\n      $(\"#seconds\").text(--seconds);\n      if(seconds == 0) {\n        location.href = $(\"#target\").attr(\"href\");\n      }\n    }, 1000);\n  });\n\u003c/script\u003e\n\n/**邮箱模板页* */\n\u003cdiv\u003e\n  \u003cp\u003e\u003cb th:text=\"${email}\"\u003exxx@xxx.com\u003c/b\u003e, 您好!\u003c/p\u003e\n  \u003cp\u003e\n    您正在注册xxx, 这是一封激活邮件, 请点击 \n    \u003ca th:href=\"${url}\"\u003e此链接\u003c/a\u003e,\n    激活您的xxx账号!\n  \u003c/p\u003e\n\u003c/div\u003e\n```\n\n## 生成验证码\n\n参考网站 ：[http://code.google.com/archive/p/kaptcha/](http://code.google.com/archive/p/kaptcha/ \"http://code.google.com/archive/p/kaptcha/\")\n\n注意：1.Producer是Kaptcha的核心接口   2.DefaultKaptcha是Kaptcha核心接口的默认实现类\n\n\u0026#x20;     3.Spring Boot没有为Kaptcha提供自动配置\n\n### 1.引入pom.xml\n\n```xml\n\u003cdependency\u003e\n    \u003cgroupId\u003ecom.github.penggle\u003c/groupId\u003e\n    \u003cartifactId\u003ekaptcha\u003c/artifactId\u003e\n    \u003cversion\u003e2.3.2\u003c/version\u003e\n\u003c/dependency\u003e\n\n```\n\n### 2.创建配置类装配第三方bean\n\n```java\n@Configuration\npublic class KaptchaConfig {\n    @Bean\n    public Producer KaptchaProducer(){\n        /**         \n         * 手动创建properties.xml配置文件对象*         \n         * 设置验证码图片的样式，大小，高度，边框，字体等\n         */\n        Properties properties=new Properties();\n        properties.setProperty(\"kaptcha.border\", \"yes\");\n        properties.setProperty(\"kaptcha.border.color\", \"105,179,90\");\n        properties.setProperty(\"kaptcha.textproducer.font.color\", \"black\");\n        properties.setProperty(\"kaptcha.image.width\", \"110\");\n        properties.setProperty(\"kaptcha.image.height\", \"40\");\n        properties.setProperty(\"kaptcha.textproducer.font.size\", \"32\");\n        properties.setProperty(\"kaptcha.textproducer.char.string\", \"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ\");\n        properties.setProperty(\"kaptcha.textproducer.char.length\", \"4\");\n        properties.setProperty(\"kaptcha.textproducer.font.names\", \"宋体,楷体,微软雅黑\");\n\n        DefaultKaptcha Kaptcha=new DefaultKaptcha();\n        Config config=new Config(properties);\n        Kaptcha.setConfig(config);\n\n        return Kaptcha;\n    }\n}\n```\n\n### 3.编写Controller接口\n\n```java\n@RequestMapping(value = \"/kaptcha\",method = RequestMethod.GET)\npublic void getKaptcha(HttpServletResponse response, HttpSession session){\n    //生成验证码\n    String text = kaptchaProducer.createText();\n    BufferedImage image = kaptchaProducer.createImage(text);\n    //将验证码存入session\n    session.setAttribute(\"kaptcha\",text);\n    //将图片输出给浏览器\n    response.setContentType(\"image/png\");\n    try {\n        ServletOutputStream os = response.getOutputStream();\n        ImageIO.write(image,\"png\",os);\n    }catch (IOException e){\n        logger.error(\"响应验证码失败:\"+e.getMessage());\n    }\n}\n```\n\n### 4.Thymeleaf前端页面核心点\n\n```html\n\u003cdiv class=\"col-sm-4\"\u003e\n  \u003cimg th:src=\"@{/kaptcha}\" id=\"kaptchaImage\" style=\"width:100px;height:40px;\" class=\"mr-2\"/\u003e\n  \u003ca href=\"javascript:refresh_kaptcha();\" class=\"font-size-12 align-bottom\"\u003e刷新验证码\u003c/a\u003e\n\u003c/div\u003e\n\u003c!--对应的Js--\u003e\n\u003cscript\u003e\n  function refresh_kaptcha() {\n    var path = CONTEXT_PATH + \"/kaptcha?p=\" + Math.random();\n    $(\"#kaptchaImage\").attr(\"src\", path);\n  }\n\u003c/script\u003e\n\u003c!--全局Js配置文件--\u003e\nvar CONTEXT_PATH=\"/community\";\n```\n\n## 登录功能\n\n验证账号,密码,验证码（成功：生成登录凭证ticket，发放给客户端  失败：跳转回登录页 ）\n\n### 1.创建登录凭证实体类（登录凭证相当于Session的作用）\n\n注意 :**为什么要搞一个登录凭证，因为最好不要将User信息存入Model返回给前端，敏感信息尽量不要返回给浏览器，不安全，而是选择ticket凭证，通过ticket可以在服务器端得到User**\n\n### 2.编写Dao层接口(注解方式实现)\n\n```java\n  @Insert({\n          \"insert into login_ticket(user_id,ticket,status,expired) \",\n          \"values (#{userId},#{ticket},#{status},#{expired})\"\n  })\n  @Options(useGeneratedKeys = true,keyProperty = \"id\")\n  //登录功能需要添加登录凭证ticket\n  int insertLoginTicket(LoginTicket loginTicket);\n  \n  @Select({\n          \"select id,user_id,ticket,status,expired \",\n          \"from login_ticket \",\n          \"where ticket=#{ticket}\"\n  })\n  //检查登录状态\n  LoginTicket selectByTicket(String ticket);\n  \n  /**\n   *  一定要加@Param()不然会报错\n   *  退出功能需要修改status状态\n   *  @return error:com.mysql.jdbc.MysqlDataTruncation:Data truncation:Truncated incorrect DOUBLE value:...\n   */\n  @Update({\n          \"update login_ticket set status=#{status} where ticket=#{ticket} \"\n  })\n  int updateStatus(@Param(\"ticket\") String ticket, @Param(\"status\") int status);\n```\n\n### 3.编写Service层登录业务\n\n```java\n  /**登录功能**/\n  public Map\u003cString,Object\u003e login(String username,String password,int expiredSeconds){\n      HashMap\u003cString, Object\u003e map = new HashMap\u003c\u003e();\n      //空值处理\n      if(StringUtils.isBlank(username)){\n          map.put(\"usernameMsg\",\"号码不能为空！\");\n          return map;\n      }\n      if(StringUtils.isBlank(password)){\n          map.put(\"passwordMsg\",\"密码不能为空！\");\n          return map;\n      }\n      //验证账号\n      User user = userMapper.selectByName(username);\n      if (user==null){\n          map.put(\"usernameMsg\",\"该账号不存在！\");\n          return map;\n      }\n      //验证激活状态\n      if (user.getStatus()==0){\n          map.put(\"usernameMsg\",\"该账号未激活！\");\n          return map;\n      }\n      //验证密码(先加密再对比)\n      password=CommunityUtil.md5(password+user.getSalt());\n      if (!user.getPassword().equals(password)){\n          map.put(\"passwordMsg\",\"密码输入错误！\");\n          return map;\n      }\n      //生成登录凭证(相当于记住我这个功能==session)\n      LoginTicket ticket = new LoginTicket();\n      ticket.setUserId(user.getId());\n      ticket.setTicket(CommunityUtil.generateUUID());\n      ticket.setStatus(0);\n      //当前时间的毫秒数+过期时间毫秒数\n      ticket.setExpired(new Date(System.currentTimeMillis() + expiredSeconds * 1000));\n      loginTicketMapper.insertLoginTicket(ticket);\n\n      map.put(\"ticket\",ticket.getTicket());\n\n      return map;\n  }\n```\n\n### 4.编写Controller层\n\n```java\n   /**\n    * 登录功能\n    * @param username\n    * @param password\n    * @param code 用于校验验证码\n    * @param rememberme  记住我（登录凭证）\n    * @param model 用于将数据传递给前端页面\n    * @param session 用于获取kaptcha验证码\n    * @param response 用于浏览器接受cookie\n    * @return\n    */\n    @RequestMapping(value = \"/login\",method = RequestMethod.POST)\n    /**注意username,password这些没有封装进model* */\n    public String login(String username, String password, String code, boolean rememberme,\n                        Model model, HttpSession session,HttpServletResponse response){\n        //首先检验验证码\n        String kaptcha = (String) session.getAttribute(\"kaptcha\");\n        if (StringUtils.isBlank(kaptcha)||StringUtils.isBlank(code)||!kaptcha.equalsIgnoreCase(code)){\n            model.addAttribute(\"codeMsg\",\"验证码不正确！\");\n            return \"/site/login\";\n        }\n        /**\n         * 1.验证用户名和密码(重点)\n         * 2.传入浏览器cookie=ticket\n         */\n        int expiredSeconds=rememberme?REMEMBER_EXPIRED_SECONDS:DEFAULT_EXPIRED_SECONDS;\n        Map\u003cString, Object\u003e map = userService.login(username, password, expiredSeconds);\n        if (map.containsKey(\"ticket\")){\n            Cookie cookie = new Cookie(\"ticket\",map.get(\"ticket\").toString());\n            cookie.setPath(contextPath);\n            cookie.setMaxAge(expiredSeconds);\n            response.addCookie(cookie);\n            return \"redirect:/index\";\n        }else{\n            model.addAttribute(\"usernameMsg\",map.get(\"usernameMsg\"));\n            model.addAttribute(\"passwordMsg\",map.get(\"passwordMsg\"));\n            return \"/site/login\";\n        }\n    }\n```\n\n### 5.编写前端Thymeleaf页面核心点\n\n```html\n\u003cdiv class=\"col-sm-10\"\u003e\n  \u003cinput type=\"text\"\n       th:class=\"|form-control ${usernameMsg!=null?'is-invalid':''}|\"\n       \u003c!--注意param,因为username,password这些没有封装进model--\u003e\n       th:value=\"${param.username}\"\n       id=\"username\" name=\"username\" placeholder=\"请输入您的账号!\" required\u003e\n  \u003cdiv class=\"invalid-feedback\" th:text=\"${usernameMsg}\"\u003e\n    该账号不存在!\n  \u003c/div\u003e\n\u003c/div\u003e\n\u003cdiv class=\"col-sm-10\"\u003e\n  \u003cinput type=\"checkbox\" id=\"remember-me\" name=\"rememberme\"\n       th:checked=\"${param.rememberme}\"\u003e\n  \u003clabel class=\"form-check-label\" for=\"remember-me\"\u003e记住我\u003c/label\u003e\n  \u003ca href=\"forget.html\" class=\"text-danger float-right\"\u003e忘记密码?\u003c/a\u003e\n\u003c/div\u003e\n```\n\n## 退出登录功能\n\n将登录凭证loginTicket中的status置为无效\n\n### 1.编写Service层\n\n```java\npublic void logout(String ticket){\n    loginTicketMapper.updateStatus(ticket,1);//来源于LoginTicket的Dao层\n}\n```\n\n### 2.编写Controller层\n\n```java\n  /**\n   * 退出登录功能\n   * @CookieValue()注解:将浏览器中的Cookie值传给参数 \n   */\n  @RequestMapping(value = \"/logout\",method = RequestMethod.GET)\n  public String logout(@CookieValue(\"ticket\") String ticket){\n      userService.logout(ticket);\n      return \"redirect:/login\";//重定向\n  }\n```\n\n## 显示登录信息\n\n涉及到 ：****拦截器，多线程****\n\n![](image/1_b7J4nGtYHK.PNG)\n\n### 拦截器Demo示例\n\n注意：\n\n       1. 拦截器需实现HandlerInterceptor接口而配置类需实现WebMvcConfigurer接口。\n\n       2. preHandle方法在Controller之前执行，若返回false，则终止执行后续的请求。\n\n       3. postHandle方法在Controller之后、模板页面之前执行。\n\n       4. afterCompletion方法在模板之后执行。\n\n       5. 通过addInterceptors方法对拦截器进行配置\n\n\n**1.创建拦截器类，实现****HandlerInterceptor****接口**\n\n```java\n@Component\npublic class DemoInterceptor implements HandlerInterceptor {\n    @Override\n    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {\n        System.out.println(\"preHandle:在Controller之前执行\");\n        return true;\n    }\n    @Override\n    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {\n        System.out.println(\"afterCompletion:在模板之后执行\");\n    }\n    @Override\n    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {\n        System.out.println(\"postHandle:在Controller之后，前端模板引擎页面渲染之前执行\");\n    }\n}\n```\n\n**2.创建拦截器配置类，实现****WebMvcConfigurer****接口**\n\n```java\n@Configuration\npublic class WebMvcConfig implements WebMvcConfigurer {\n    @Autowired\n    private DemoInterceptor demoInterceptor;\n    @Override\n    public void addInterceptors(InterceptorRegistry registry) {\n        registry.addInterceptor(demoInterceptor)\n                .excludePathPatterns(\"/ **/ *.css\",\"/* */*.js\",\"/**/ *.png\",\"/* */*.jpg\",\"/ **/ *.jpeg\")\n                .addPathPatterns(\"/register\",\"/login\");\n}\n```\n\n### 1.首先创建两个工具类降低耦合（Request获取Cookie工具类，获取凭证ticket多线程工具类）\n\n注意：1.ThreadLocal采用**线程隔离**的方式存放数据，可以避免多线程之间出现数据访问冲突。\n\n2.ThreadLocal提供**set**方法，能够以当前线程为key存放数据。**get**方法，能够以当前线程为key获取数据。\n\n3.ThreadLocal提供**remove**方法，能够以当前线程为key删除数据。\n\n```java\npublic class CookieUtil {\n    public static String getValue(HttpServletRequest request,String name){\n        if (request==null||name==null){\n            throw new IllegalArgumentException(\"参数为空！\");\n        }\n        Cookie[] cookies = request.getCookies();\n        if (cookies!=null){\n            for (Cookie cookie : cookies){\n                if (cookie.getName().equals(name)){\n                    return cookie.getValue();\n                }\n            }\n        }\n        return null;\n    }\n}\n```\n\n```java\n@Component  //放入容器里不用设为静态方法\npublic class HostHolder {\n//key就是线程对象，值为线程的变量副本\n    private ThreadLocal\u003cUser\u003e users = new ThreadLocal\u003c\u003e();\n    /**以线程为key存入User* */\n    public void setUser(User user){\n        users.set(user);\n    }\n    /**从ThreadLocal线程中取出User* */\n    public User getUser(){\n        return users.get();\n    }\n    /**释放线程* */\n    public void clear(){\n        users.remove();\n    }\n}\n```\n\n### 2.编写Service层\n\n```java\n/**通过Cookie=ticket获取登录用户* */\npublic LoginTicket getLoginTicket(String ticket){\n    return loginTicketMapper.selectByTicket(ticket);\n}\n```\n\n### 3.创建登录凭证拦截器类（等同于Controller类）\n\n```java\n@Component\npublic class LoginTicketInterceptor implements HandlerInterceptor {\n\n    @Autowired\n    private UserService userService;\n    @Autowired\n    private HostHolder hostHolder;\n\n    @Override\n    /**在Controller访问所有路径之前获取凭证* */\n    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {\n        /**从浏览器Cookie中获取凭证* */\n        String ticket=CookieUtil.getValue(request,\"ticket\");\n\n        if (ticket!=null){\n            //查询凭证\n            LoginTicket loginTicket = userService.getLoginTicket(ticket);\n            //检查凭证是否有效(after：当前时间之后)\n            if (loginTicket!=null\u0026\u0026loginTicket.getStatus()==0\u0026\u0026loginTicket.getExpired().after(new Date())){\n                //根据凭证查询用户\n                User user = userService.findUserById(loginTicket.getUserId());\n                /**在本次请求中持有用户\n                 * 类似于存入Map,只是考虑到多线程\n                 */\n                hostHolder.setUser(user);\n            }\n        }\n        return true;\n    }\n    @Override\n    /**模板之前处理数据* */\n    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {\n        User user = hostHolder.getUser();\n        if (user!=null \u0026\u0026 modelAndView !=null){\n            modelAndView.addObject(\"loginUser\",user);\n        }\n    }\n    @Override\n    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {\n        //释放线程资源\n        hostHolder.clear();\n    }\n}\n```\n\n### 4.编写拦截器配置类\n\n```java\n@Configuration\npublic class WebMvcConfig implements WebMvcConfigurer {\n    @Autowired\n    private LoginTicketInterceptor loginTicketInterceptor;\n    @Override\n    public void addInterceptors(InterceptorRegistry registry) {\n        registry.addInterceptor(loginTicketInterceptor)\n                .excludePathPatterns(\"/* */*.css\",\"/**/ *.js\",\"/* */*.png\",\"/ **/ *.jpg\",\"/* */*.jpeg\");\n    }}\n```\n\n### 5.前端页面核心点修改\n\nth:if=\"\\${loginUser!=null}\" **存在凭证显示\\\u003cli\u003e,不存在则不显示**\n\n```html\n\u003cli class=\"nav-item ml-3 btn-group-vertical\" th:if=\"${loginUser!=null}\"\u003e\n  \u003ca class=\"nav-link position-relative\" href=\"site/letter.html\"\u003e消息\u003cspan class=\"badge badge-danger\"\u003e12\u003c/span\u003e\u003c/a\u003e\n\u003c/li\u003e\n```\n\n## 拦截未登录页面的路径访问(自定义拦截器注解)\n\n常用的元注解： **@Target：注解作用目标（方法or类）   @Retention：注解作用时间（运行时or编译时） @Document：注解是否可以生成到文档里  @Inherited**：**注解继承该类的子类将自动使用@Inherited修饰**\n\n 注意： **若有2个拦截器，拦截器执行顺序为注册在WebMvcConfig配置类中的顺序**\n\n### 1.自定义拦截方法类注解(annotation包)并加在需要拦截的方法上\n\n```java\n@Target(ElementType.METHOD)\n@Retention(RetentionPolicy.RUNTIME)\n/**\n * 标记未登录时要拦截的路径访问方法\n */\npublic @interface LoginRequired {\n}\n/**加在需要拦截的方法**/\n@LoginRequired\n```\n\n### 2.编写拦截器类实现HandlerInterceptor父类\n\n```java\n  @Autowired\n  //注入hostHolder工具类获取当前状态登录用户\n  private HostHolder hostHolder;\n  \n  @Override\n  /**在请求路径前执行该方法* */\n  public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {\n      //判断拦截的目标是不是一个方法\n      if (handler instanceof HandlerMethod){\n          //如果是一个方法，将handler转化我HandlerMethod类型\n          HandlerMethod handlerMethod = (HandlerMethod) handler;\n          Method method = handlerMethod.getMethod();\n          //获取方法上的自定义注解\n          LoginRequired loginRequired = method.getAnnotation(LoginRequired.class);\n         /**\n          * 如果没有登录并且有自定义注解（需要登录才能访问的方法注解）\n          * 通过response来重定向，这里不可以通过return 重定向\n          */\n          if (hostHolder.getUser()==null\u0026\u0026loginRequired!=null){\n              response.sendRedirect(request.getContextPath() + \"/login\");\n              return false;\n          }\n      }\n      return true;\n  }\n```\n\n### 3.注册进拦截器配置类WebMvcConfig\n\n```java\n  @Autowired\n  private LoginRequiredInterceptor loginRequiredInterceptor;\n  \n   @Override\n  public void addInterceptors(InterceptorRegistry registry) {\n      registry.addInterceptor(loginRequiredInterceptor)\n              .excludePathPatterns(\"/* */*.css\",\"/**/ *.js\",\"/* */*.png\",\"/ **/ *.jpg\",\"/* */*.jpeg\");\n  }\n```\n\n## 修改密码\n\n### 1.编写Dao层\n\n```java\nint updatePassword(@Param(\"id\") int id,@Param(\"password\")String password);\n\u003cupdate id=\"updatePassword\"\u003e\n    update user set password=#{password} where id=#{id}\n\u003c/update\u003e\n\n```\n\n### 2.编写Service层\n\n```java\n  /**修改密码**/\n  public Map\u003cString,Object\u003e updatePassword(int userId,String oldPassword,String newPassword){\n      HashMap\u003cString, Object\u003e map = new HashMap\u003c\u003e();\n  \n      // 空值处理\n      if (StringUtils.isBlank(oldPassword)) {\n          map.put(\"oldPasswordMsg\", \"原密码不能为空!\");\n          return map;\n      }\n      if (StringUtils.isBlank(newPassword)) {\n          map.put(\"newPasswordMsg\", \"新密码不能为空!\");\n          return map;\n      }\n  \n      // 验证原始密码\n      User user = userMapper.selectById(userId);\n      oldPassword = CommunityUtil.md5(oldPassword + user.getSalt());\n  \n      if (!user.getPassword().equals(oldPassword)){\n          map.put(\"oldPasswordMsg\",\"您输入的原密码错误！\");\n          return map;\n      }\n      newPassword = CommunityUtil.md5(newPassword + user.getSalt());\n      userMapper.updatePassword(userId,newPassword);\n  \n      return map;\n  }\n```\n\n### 3.编写Controller层\n\n```java\n  /**修改密码 **/\n  @RequestMapping(value = \"/updatePassword\",method = RequestMethod.POST)\n  public String updatePassword(String oldPassword, String newPassword, Model model){\n      User user = hostHolder.getUser();\n      Map\u003cString, Object\u003e map = userService.updatePassword(user.getId(), oldPassword, newPassword);\n      if (map == null || map.isEmpty()){\n          /**如果更改密码成功，退出登录，并跳到登录页面 **/\n          return \"redirect:/logout\";\n      }else{\n          model.addAttribute(\"oldPasswordMsg\",map.get(\"oldPasswordMsg\"));\n          model.addAttribute(\"newPasswordMsg\",map.get(\"newPasswordMsg\"));\n          return \"/site/setting\";\n      }\n  }\n```\n\n## 忘记密码\n\n### 1.编写Service层\n\n```Java\n    // 判断邮箱是否已注册\n    public boolean isEmailExist(String email) {\n        User user = userMapper.selectByEmail(email);\n        return user != null;\n    }\n    \n     /**\n      * 重置忘记密码\n      */\n    public Map\u003cString, Object\u003e resetPassword(String email, String password) {\n        HashMap\u003cString, Object\u003e map = new HashMap\u003c\u003e();\n\n        //空值处理\n        if (StringUtils.isBlank(email)) {\n            map.put(\"emailMsg\", \"邮箱不能为空！\");\n            return map;\n        }\n        if (StringUtils.isBlank(password)) {\n            map.put(\"passwordMsg\", \"密码不能为空！\");\n            return map;\n        }\n\n        //根据邮箱查找用户\n        User user = userMapper.selectByEmail(email);\n        if (user == null) {\n            map.put(\"emailMsg\", \"该邮箱尚未注册!\");\n            return map;\n        }\n\n        //重置密码\n        password = CommunityUtil.md5(password + user.getSalt());\n        userMapper.updatePassword(user.getId(), password);\n        // 清理缓存\n        clearCache(user.getId());\n\n        //注意这里！\n        map.put(\"user\", user);\n\n        return map;\n    }\n```\n\n### 2.编写Controller层\n\n```Java\n    /**\n     * 忘记密码页面\n     */\n    @RequestMapping(path = \"/forget\", method = RequestMethod.GET)\n    public String getForgetPage() {\n        return \"/site/forget\";\n    }\n    \n    /**\n     * 重置密码\n     */\n    @RequestMapping(path = \"/forget/password\", method = RequestMethod.POST)\n    public String resetPassword(String email, String verifyCode, String password, Model model, HttpSession session) {\n        String code = (String) session.getAttribute(email + \"_verifyCode\");\n\n        if (StringUtils.isBlank(verifyCode) || StringUtils.isBlank(code) || !code.equalsIgnoreCase(verifyCode)) {\n            model.addAttribute(\"codeMsg\", \"验证码错误!\");\n            return \"/site/forget\";\n        }\n\n        Map\u003cString, Object\u003e map = userService.resetPassword(email, password);\n        if (map.containsKey(\"user\")) {\n            return \"redirect:/login\";\n        } else {\n            model.addAttribute(\"emailMsg\", map.get(\"emailMsg\"));\n            model.addAttribute(\"passwordMsg\", map.get(\"passwordMsg\"));\n            return \"/site/forget\";\n        }\n    }    \n```\n\n### 3.编写前端核心部分\n\n```HTML\n  \u003cform method=\"post\" th:action=\"@{/forget/password}\"\u003e\n      \u003cdiv\u003e\n          \u003clabel class=\"col-sm-2\" for=\"your-email\"\u003e邮箱:\u003c/label\u003e\n          \u003cdiv\u003e\n              \u003cinput id=\"your-email\" name=\"email\" placeholder=\"请输入您的邮箱!\" required\n                     th:class=\"|form-control ${emailMsg!=null?'is-invalid':''}|\" th:value=\"${param.email}\"\n                     type=\"email\"\u003e\n              \u003cdiv th:text=\"${emailMsg}\"\u003e\n              \u003cinput **id=\"your-email\" name=\"email\" placeholder=\"请输入您的邮箱!\" required\n                     th:class=\"|form-control ${emailMsg!=null?'is-invalid':''}|\" th:value=\"${param.email}\"\n                     type=\"email\"\u003e**\n              \u003cdiv **th:text=\"${emailMsg}\"**\u003e\n                  该邮箱已被注册!\n              \u003c/div\u003e\n          \u003c/div\u003e\n      \u003c/div\u003e\n      \u003cdiv \u003e\n          \u003clabel class=\"col-sm-2\" for=\"verifycode\"\u003e验证码:\u003c/label\u003e\n          \u003cdiv\u003e\n              \u003cinput id=\"verifycode\" name=\"verifyCode\" placeholder=\"请输入验证码!\"\n                     th:class=\"|form-control ${codeMsg!=null?'is-invalid':''}|\" th:value=\"${param.verifyCode}\"\n                     type=\"text\"\u003e\n              \u003cinput **id=\"verifycode\" name=\"verifyCode\" placeholder=\"请输入验证码!\"\n                     th:class=\"|form-control ${codeMsg!=null?'is-invalid':''}|\" th:value=\"${param.verifyCode}\"\n                     type=\"text\"\u003e**\n              \u003cdiv th:text=\"${codeMsg}\"\u003e\n                  验证码不正确!\n              \u003c/div\u003e\n          \u003c/div\u003e\n          \u003cdiv\u003e\n              \u003ca class=\"btn\" id=\"verifyCodeBtn\"\u003e获取验证码\u003c/a\u003e\n          \u003c/div\u003e\n      \u003c/div\u003e\n      \u003cdiv\u003e\n          \u003clabel class=\"col-sm-2\" for=\"your-password\"\u003e新密码:\u003c/label\u003e\n          \u003cdiv class=\"col-sm-10\"\u003e\n              \u003cinput id=\"your-password\" name=\"password\" placeholder=\"请输入新的密码!\" required\n                     th:class=\"|form-control ${passwordMsg!=null?'is-invalid':''}|\"\n                     th:value=\"${param.password}\" type=\"password\"\u003e\n              \u003cinput **id=\"your-password\" name=\"password\" placeholder=\"请输入新的密码!\" required\n                     th:class=\"|form-control ${passwordMsg!=null?'is-invalid':''}|\"\n                     th:value=\"${param.password}\" type=\"password\"\u003e**\n              \u003cdiv class=\"invalid-feedback\" th:text=\"${passwordMsg}\"\u003e\n                  密码长度不能小于8位!\n              \u003c/div\u003e\n          \u003c/div\u003e\n      \u003c/div\u003e\n      \u003cbutton type=\"submit\" class=\"btn\"\u003e重置密码\u003c/button\u003e\n  \u003c/form\u003e\n```\n\n# 优化登录功能(使用Redis)\n\n## 使用Redis存储验证码\n\n### 1.编写RedisUtil工具类设置验证码key值\n\n```java\npublic class RedisKeyUtil {\n    // 验证码\n    private static final String PREFIX_KAPTCHA = \"kaptcha\";\n    /**登录验证码**/\n    public static String getKaptchaKey(String owner) {\n        return PREFIX_KAPTCHA + SPLIT + owner;\n    }\n}\n```\n\n### 2.优化LoginController验证码相关代码（优化前是存在session中的）\n\n```java\n    @Autowired\n    private RedisTemplate redisTemplate;\n    /**\n     * 验证码功能 (Redis优化)\n     * @param response\n     */\n    @RequestMapping(value = \"/kaptcha\", method = RequestMethod.GET)\n    public void getKaptcha(HttpServletResponse response) {\n        //生成验证码\n        String text = kaptchaProducer.createText();\n        BufferedImage image = kaptchaProducer.createImage(text);\n        //优化前：将验证码存入session.....\n\n        //优化后：生成验证码的归属传给浏览器Cookie\n        String kaptchaOwner = CommunityUtil.generateUUID();\n        Cookie cookie = new Cookie(\"kaptchaOwner\", kaptchaOwner);\n        cookie.setMaxAge(60);\n        cookie.setPath(contextPath);\n        response.addCookie(cookie);\n\n        //优化后：将验证码存入Redis\n        String redisKey = RedisKeyUtil.getKaptchaKey(kaptchaOwner);\n        redisTemplate.opsForValue().set(redisKey, text, 60 , TimeUnit.SECONDS);\n\n        //将图片输出给浏览器\n        response.setContentType(\"image/png\");\n        try {\n            ServletOutputStream os = response.getOutputStream();\n            ImageIO.write(image, \"png\", os);\n        } catch (IOException e) {\n            logger.error(\"响应验证码失败:\" + e.getMessage());\n        }\n    }\n```\n\n```java\n    /**\n     * 登录功能\n     * @param redisKey 用于获取kaptcha验证码\n     * @param @CookieValue用于浏览器接受cookie\n     * @return\n     */\n    @RequestMapping(value = \"/login\", method = RequestMethod.POST)\n    /**注意username,password这些没有封装进model**/\n    public String login(String username, String password, String code, boolean rememberme,\n                        Model model, HttpServletResponse response,\n                        @CookieValue(\"kaptchaOwner\") String kaptchaOwner) {\n        /**\n         * 优化前：首先检验验证码(从session取验证码)\n         * String kaptcha = (String) session.getAttribute(\"kaptcha\");\n         */\n\n        // 优化后：从redis中获取kaptcha的key\n        String kaptcha = null;\n        // 判断从浏览器传来的Cookie是否为空\n        if (StringUtils.isNotBlank(kaptchaOwner)) {\n            String redisKey = RedisKeyUtil.getKaptchaKey(kaptchaOwner);\n            // 获取key为验证码的redis数据\n            kaptcha  = (String) redisTemplate.opsForValue().get(redisKey);\n        }\n\n        if (StringUtils.isBlank(kaptcha) || StringUtils.isBlank(code) || !kaptcha.equalsIgnoreCase(code)) {\n            model.addAttribute(\"codeMsg\", \"验证码不正确！\");\n            return \"/site/login\";\n        }\n        /**\n         * 1.验证用户名和密码(重点)\n         * 2.传入浏览器cookie=ticket\n         */\n        int expiredSeconds = rememberme ? REMEMBER_EXPIRED_SECONDS : DEFAULT_EXPIRED_SECONDS;\n        Map\u003cString, Object\u003e map = userService.login(username, password, expiredSeconds);\n        if (map.containsKey(\"ticket\")) {\n            Cookie cookie = new Cookie(\"ticket\", map.get(\"ticket\").toString());\n            cookie.setPath(contextPath);\n            cookie.setMaxAge(expiredSeconds);\n            response.addCookie(cookie);\n            return \"redirect:/index\";\n        } else {\n            model.addAttribute(\"usernameMsg\", map.get(\"usernameMsg\"));\n            model.addAttribute(\"passwordMsg\", map.get(\"passwordMsg\"));\n            return \"/site/login\";\n        }\n    }\n```\n\n## 使用Redis存存登录凭证\n\n### 1.编写RedisUtil工具类设置登录凭证key值\n\n```java\n    // 登录凭证\n    private static final String PREFIX_TICKET = \"ticket\";\n    /**登录凭证**/\n    public static String getTicketKey(String ticket) {\n        return PREFIX_TICKET + SPLIT + ticket;\n    }\n```\n\n### 2.优化UserService中LoginTicket相关代码（废弃LoginTicket数据库表，使用redis）\n\n```java\n    @Autowired\n    private RedisTemplate redisTemplate;\n    /**\n     * 登录功能（redis优化）\n     */\n    public Map\u003cString, Object\u003e login(String username, String password, int expiredSeconds) {\n        HashMap\u003cString, Object\u003e map = new HashMap\u003c\u003e();\n        //空值处理\n        if (StringUtils.isBlank(username)) {\n            map.put(\"usernameMsg\", \"号码不能为空！\");\n            return map;\n        }\n        if (StringUtils.isBlank(password)) {\n            map.put(\"passwordMsg\", \"密码不能为空！\");\n            return map;\n        }\n        //验证账号\n        User user = userMapper.selectByName(username);\n        if (user == null) {\n            map.put(\"usernameMsg\", \"该账号不存在！\");\n            return map;\n        }\n        //验证激活状态\n        if (user.getStatus() == 0) {\n            map.put(\"usernameMsg\", \"该账号未激活！\");\n            return map;\n        }\n        //验证密码(先加密再对比)\n        password = CommunityUtil.md5(password + user.getSalt());\n        if (!user.getPassword().equals(password)) {\n            map.put(\"passwordMsg\", \"密码输入错误！\");\n            return map;\n        }\n        //生成登录凭证(相当于记住我这个功能==session)\n        LoginTicket ticket = new LoginTicket();\n        ticket.setUserId(user.getId());\n        ticket.setTicket(CommunityUtil.generateUUID());\n        ticket.setStatus(0);\n        //当前时间的毫秒数+过期时间毫秒数\n        ticket.setExpired(new Date(System.currentTimeMillis() + expiredSeconds*  1000));\n        // 优化前：loginTicketMapper.insertLoginTicket(ticket);\n        \n        // 优化后：loginticket对象放入redis中\n        String redisKey = RedisKeyUtil.getTicketKey(ticket.getTicket());\n        // opsForValue将ticket对象序列化为json字符串\n        redisTemplate.opsForValue().set(redisKey, ticket);\n\n        map.put(\"ticket\", ticket.getTicket());\n\n        return map;\n    }\n```\n\n```java\n    /**\n    * 通过Cookie=ticket获取登录用户(redis优化)\n    */\n    public LoginTicket getLoginTicket(String ticket) {\n        //优化前： return loginTicketMapper.selectByTicket(ticket);\n        String redisKey = RedisKeyUtil.getTicketKey(ticket);\n        return (LoginTicket) redisTemplate.opsForValue().get(redisKey);\n    }\n```\n\n## 使用Redis缓存用户信息\n\n### 1.编写RedisUtil工具类设置用户缓存key值\n\n```java\n    // 用户缓存\n    private static final String PREFIX_USER = \"user\";\n    /**用户缓存**/\n    public static String getUserKey(int userId) {\n        return PREFIX_USER + SPLIT + userId;\n    }\n```\n\n### 2.优化UserService中findUserById和userMapper.updateXXX方法\n\n```java\n     /**\n     * 因为经常使用这个方法，所以将它用redis缓存优化\n     * 若缓存中有访问的用户直接从缓存中取出，否则从数据库查询后加入redis中作为缓存\n     */\n    public User findUserById(int userId) {\n        // return userMapper.selectById(userId);\n        // 从redis缓存中取值\n        User user = getCache(userId);\n        if (user == null) {\n            user = initCache(userId);\n        }\n        return user;\n    }\n    \n    /**\n    * 更新头像\n    */\n    public int updateHeader(int userId, String headerUrl) {\n        /** 同时处理mysql和redis事务的方法，报错回滚* */\n        int rows = userMapper.updateHeader(userId, headerUrl);\n        clearCache(userId);\n        return rows;\n    }\n   \n    // 1.优先从缓存中取值\n    private User getCache(int userId) {\n        String redisKey = RedisKeyUtil.getUserKey(userId);\n        return (User) redisTemplate.opsForValue().get(redisKey);\n    }\n    // 2.取不到时初始化缓存数据(redis存值)\n    private User initCache(int userId) {\n        User user = userMapper.selectById(userId);\n        String redisKey = RedisKeyUtil.getUserKey(userId);\n\n        redisTemplate.opsForValue().set(redisKey, user, 3600, TimeUnit.SECONDS);\n        return user;\n    }\n    // 3.数据变更时清除缓存(删除redis的key)\n    private void clearCache(int userId) {\n        String redisKey = RedisKeyUtil.getUserKey(userId);\n        redisTemplate.delete(redisKey);\n    }\n```\n\n# 会话管理（暂时仅有demo）\n\n### 1.面试题Cookie和Session的区别？\n\n1.cookie是存放在浏览器上的，session是存放在服务器上的。\n\n2.cookie数据不安全，如果考虑到安全应使用session。\n\n3.session会增加服务端的内存压力,考虑到减轻服务器性能方面，应当使用cookie。\n\n4.cookie只能存放一对字符串k-v\n\n![](image/cookie_ydwZWF6ZCb.PNG)\n\n![](image/session_4ZmfBdmJQn.PNG)\n\n### 2.Cookie是干嘛的？\n\n因为Http是无状态的，所以需要用到cookie。通俗说cookie是用来让服务器记住浏览器的。\n\n### 3.分布式session共享方案\n\n1、粘性session：在nginx中提供一致性哈希策略，可以保持用户ip进行hash值计算固定分配到某台服务器上，负载也比较均衡，其问题是假如有一台服务器挂了，session也丢失了。\n\n2、同步session：当某一台服务器存了session后，同步到其他服务器中，其问题是同步session到其他服务器会对服务器性能产生影响，服务器之间耦合性较强。\n\n3、共享session：单独搞一台服务器用来存session，其他服务器都向这台服务器获取session，其问题是这台服务器挂了，session就全部丢失。\n\n4、redis集中管理session(主流方法)：redis为内存数据库，读写效率高，并可在集群环境下做高可用。\n\n![](image/Session集群2_6m4V-afro7.PNG)\n\n### 4.简单API实现\n\n```java\n/**\n * Cookie示例(获取Cookie时@CookieValue有点问题！！)\n */\n@RequestMapping(value = \"/cookie/set\",method = RequestMethod.GET)\n@ResponseBody\npublic String setCookie(HttpServletResponse response){\n    //cookie存的必须是字符串\n    Cookie cookie = new Cookie(\"code\", CommunityUtil.generateUUID());\n    cookie.setPath(\"/Community/test\");\n    cookie.setMaxAge(60*10);\n    response.addCookie(cookie);\n\n    return \"set cookie!\";\n}\n\n@RequestMapping(value = \"/cookie/get\",method = RequestMethod.GET)\n@ResponseBody\npublic String getCookie(@CookieValue(\"code\") String code){\n    System.out.println(code);\n    return \"get cookie!\";\n}\n\n/**\n * Session示例\n */\n@RequestMapping(value = \"/session/set\",method = RequestMethod.GET)\n@ResponseBody\npublic String setSession(HttpSession session){\n    session.setAttribute(\"id\",1);\n    session.setAttribute(\"name\",\"xmy\");\n    return \"set session!\";\n}\n\n@RequestMapping(value = \"/session/get\",method = RequestMethod.GET)\n@ResponseBody\npublic String getSession(HttpSession session){\n    System.out.println(session.getAttribute(\"id\"));\n    System.out.println(session.getAttribute(\"name\"));\n    return \"get session!\";\n}\n```\n\n# 上传头像功能\n\n注意：1. 必须是Post请求 \n2.表单：enctype=\"multipart/form-data\"\n3.参数类型MultipartFile只能封装一个文件\n\n上传路径可以是本地路径也可以是web路径\n\n访问路径**必须**是符合HTTP协议的**Web路径**\n\n## 1.编写Service和Dao层\n\n```java\n//Dao层\n\u003cupdate id=\"updatePassword\"\u003e\n    update user set password=#{password} where id=#{id}\n\u003c/update\u003e\nint updateHeader(@Param(\"id\") int id,@Param(\"headerUrl\") String headerUrl);\n\n//Service层\n/**更换上传头像**/\npublic int updateHeader(int userId,String headerUrl){\n    return userMapper.updateHeader(userId,headerUrl);\n}\n```\n\n## 2.编程Controller层\n\n```java\n@Controller\n@RequestMapping(\"/user\")\npublic class UserController {\n\n    private static final Logger logger = LoggerFactory.getLogger(UserController.class);\n\n    //community.path.upload = d:/DemoNowcoder/upload\n    @Value(\"${community.path.upload}\")\n    private String uploadPath;\n\n    @Value(\"${community.path.domain}\")\n    private String domain;\n\n    @Value(\"${server.servlet.context-path}\")\n    private String contextPath;\n\n    @Autowired\n    private UserService userService;\n\n    @Autowired\n    /**获得当前登录用户的信息* */\n    private HostHolder hostHolder;\n\n    @RequestMapping(value = \"/setting\",method = RequestMethod.GET)\n    public String getSettingPage(){\n        return \"/site/setting\";\n    }\n    \n    //上传头像\n    @RequestMapping(value = \"/upload\",method = RequestMethod.POST)\n    public String uploadHeader(MultipartFile headerImage, Model model){\n    //StringUtils.isBlank(headerImage)\n        if (headerImage == null){\n            model.addAttribute(\"error\",\"您还没有选择图片！\");\n            return \"/site/setting\";\n        }\n        /*\n        * 获得原始文件名字\n        * 目的是：生成随机不重复文件名，防止同名文件覆盖\n        * 方法：获取.后面的图片类型 加上 随机数\n        */\n        String filename = headerImage.getOriginalFilename();\n        String suffix = filename.substring(filename.lastIndexOf(\".\") );\n\n        //任何文件都可以上传,根据业务在此加限制\n        if (StringUtils.isBlank(suffix)){\n            model.addAttribute(\"error\",\"文件格式不正确！\");\n            return \"/site/setting\";\n        }\n\n        //生成随机文件名\n        filename = CommunityUtil.generateUUID() + suffix;\n        //确定文件存放路劲\n        File dest = new File(uploadPath + \"/\" +filename);\n        try{\n            //将文件存入指定位置\n            headerImage.transferTo(dest);\n        }catch (IOException e){\n            logger.error(\"上传文件失败： \"+ e.getMessage());\n            throw new RuntimeException(\"上传文件失败，服务器发生异常！\",e);\n        }\n        //更新当前用户的头像的路径（web访问路径）\n        //http://localhost:8080/community/user/header/xxx.png\n        User user = hostHolder.getUser();\n        String headerUrl = domain + contextPath + \"/user/header/\" + filename;\n        userService.updateHeader(user.getId(),headerUrl);\n\n        return \"redirect:/index\";\n    }\n```\n\n```java\n  //得到服务器图片\n  @RequestMapping(path = \"/header/{fileName}\", method = RequestMethod.GET)\n  /**void:返回给浏览器的是特色的图片类型所以用void**/\n  public void getHeader(@PathVariable(\"fileName\") String fileName, HttpServletResponse response) {\n      // 服务器存放路径(本地路径)\n      fileName = uploadPath + \"/\" + fileName;\n      // 文件后缀\n      String suffix = fileName.substring(fileName.lastIndexOf(\".\") + 1);\n      // 浏览器响应图片\n      response.setContentType(\"image/\" + suffix);\n      try (\n              //图片是二进制用字节流\n              FileInputStream fis = new FileInputStream(fileName);\n              OutputStream os = response.getOutputStream();\n      ) {\n          //设置缓冲区\n          byte[] buffer = new byte[1024];\n          //设置游标\n          int b = 0;\n          while ((b = fis.read(buffer)) != -1) {\n              os.write(buffer, 0, b);\n          }\n      } catch (IOException e) {\n          logger.error(\"读取头像失败: \" + e.getMessage());\n      }\n  }\n```\n\n## 3.前端核心页面\n\n```html\n\u003cform class=\"mt-5\" method=\"post\" enctype=\"multipart/form-data\" th:action=\"@{/user/upload}\"\u003e\n    \u003cdiv class=\"custom-file\"\u003e\n      \u003cinput type=\"file\"\n           th:class=\"|custom-file-input ${error!=null?'is-invalid':''}|\"\n           name=\"headerImage\" id=\"head-image\" lang=\"es\" required=\"\"\u003e\n      \u003clabel class=\"custom-file-label\" for=\"head-image\" data-browse=\"文件\"\u003e选择一张图片\u003c/label\u003e\n      \u003cdiv class=\"invalid-feedback\" th:text=\"${error}\"\u003e\n        该账号不存在!\n      \u003c/div\u003e\n    \u003c/div\u003e\n\u003c/form\u003e\n```\n\n# 过滤敏感词\n\n前缀树  ：1.根节点不包含字符，除根节点以外的每个节点，只包含一个字符\n\n\u0026#x20;        2.从根节点到某一个节点，路径上经过的字符连接起来，为该节点对应字符串\n\n\u0026#x20;    3.每个节点的所有子节点，包含的字符串不相同\n\n核心  ：1.有一个指针指向前缀树，用以遍历敏感词的每一个字符\n\n\u0026#x20;         2.有一个指针指向被过滤字符串，用以标识敏感词的开头\n\n\u0026#x20;         3.有一个指针指向被过滤字符串，用以标识敏感词的结尾\n\n![](image/前缀树_nTNaIPnorr.PNG)\n\n### 1.过滤敏感词算法\n\n**在resources创建sensitive-words.txt文敏感词文本**\n\n```java\n/**\n * 过滤敏感词工具类\n * 类似于二叉树的算法\n */\n@Component\npublic class SensitiveFilter {\n\n    private static final Logger logger = LoggerFactory.getLogger(SensitiveFilter.class);\n\n    // 替换符\n    private static final String REPLACEMENT = \"* **\";\n\n    // 根节点\n    private TrieNode rootNode = new TrieNode();\n\n    // 编译之前运行\n    @PostConstruct\n    public void init() {\n        try (\n                // 读取文件流 BufferedReader带缓冲区效率更高\n                InputStream is = this.getClass().getClassLoader().getResourceAsStream(\"sensitive-words.txt\");\n                BufferedReader reader = new BufferedReader(new InputStreamReader(is));\n        ) {\n            String keyword;\n            // 一行一行读取文件中的字符\n            while ((keyword = reader.readLine()) != null) {\n                // 添加到前缀树\n                this.addKeyword(keyword);\n            }\n        } catch (IOException e) {\n            logger.error(\"加载敏感词文件失败: \" + e.getMessage());\n        }\n    }\n    /**\n     * 将一个敏感词添加到前缀树中\n     * 类似于空二叉树的插入\n     */\n    private void addKeyword(String keyword) {\n        TrieNode tempNode = rootNode;\n        for (int i = 0; i \u003c keyword.length(); i++) {\n            //将汉字转化为Char值\n            char c = keyword.charAt(i);\n            TrieNode subNode = tempNode.getSubNode(c);\n\n            if (subNode == null) {\n                // 初始化子节点并加入到前缀树中\n                subNode = new TrieNode();\n                tempNode.addSubNode(c, subNode);\n            }\n\n            // 指向子节点,进入下一轮循环\n            tempNode = subNode;\n\n            // 设置结束标识\n            if (i == keyword.length() - 1) {\n                tempNode.setKeywordEnd(true);\n            }\n        }\n    }\n\n    /**\n     * 过滤敏感词\n     * @param text 待过滤的文本\n     * @return 过滤后的文本\n     */\n    public String filter(String text) {\n        if (StringUtils.isBlank(text)) {\n            return null;\n        }\n\n        // 指针1\n        TrieNode tempNode = rootNode;\n        // 指针2\n        int begin = 0;\n        // 指针3\n        int position = 0;\n        // 结果(StringBuilder：可变长度的String类)\n        StringBuilder sb = new StringBuilder();\n\n        while (position \u003c text.length()) {\n            char c = text.charAt(position);\n            // 跳过符号\n            if (isSymbol(c)) {\n                // 若指针1处于根节点,将此符号计入结果,让指针2向下走一步\n                if (tempNode == rootNode) {\n                    sb.append(c);\n                    begin++;\n                }\n                // 无论符号在开头或中间,指针3都向下走一步\n                position++;\n                continue;\n            }\n\n            // 检查下级节点\n            tempNode = tempNode.getSubNode(c);\n            if (tempNode == null) {\n                // 以begin开头的字符串不是敏感词\n                sb.append(text.charAt(begin));\n                // 进入下一个位置\n                position = ++begin;\n                // 重新指向根节点\n                tempNode = rootNode;\n            } else if (tempNode.isKeywordEnd()) {\n                // 发现敏感词,将begin~position字符串替换掉\n                sb.append(REPLACEMENT);\n                // 进入下一个位置\n                begin = ++position;\n                // 重新指向根节点\n                tempNode = rootNode;\n            } else {\n                // 检查下一个字符\n                position++;\n            }\n        }\n\n        // 将最后一批字符计入结果\n        sb.append(text.substring(begin));\n\n        return sb.toString();\n    }\n\n    // 判断是否为符号\n    private boolean isSymbol(Character c) {\n        // 0x2E80~0x9FFF 是东亚文字范围\n        return !CharUtils.isAsciiAlphanumeric(c) \u0026\u0026 (c \u003c 0x2E80 || c \u003e 0x9FFF);\n    }\n\n    // 构造前缀树数据结构\n    private class TrieNode {\n\n        // 关键词结束标识\n        private boolean isKeywordEnd = false;\n\n        // 子节点(key是下级字符,value是下级节点)\n        private Map\u003cCharacter, TrieNode\u003e subNodes = new HashMap\u003c\u003e();\n\n        public boolean isKeywordEnd() {\n            return isKeywordEnd;\n        }\n\n        public void setKeywordEnd(boolean keywordEnd) {\n            isKeywordEnd = keywordEnd;\n        }\n\n        // 添加子节点\n        public void addSubNode(Character c, TrieNode node) {\n            subNodes.put(c, node);\n        }\n\n        // 获取子节点\n        public TrieNode getSubNode(Character c) {\n            return subNodes.get(c);\n        }\n\n    }\n\n}\n```\n\n### 2.引入第三方Maven,如下：\n\n[*https://github.com/jinrunheng/sensitive-words-filter* ](https://github.com/jinrunheng/sensitive-words-filter \"https://github.com/jinrunheng/sensitive-words-filter\")\n\n```xml\n\u003cdependency\u003e\n  \u003cgroupId\u003eio.github.jinrunheng\u003c/groupId\u003e\n  \u003cartifactId\u003esensitive-words-filter\u003c/artifactId\u003e\n  \u003cversion\u003e0.0.1\u003c/version\u003e\n\u003c/dependency\u003e\n```\n\n# 发布贴子\n\n核心 **：ajax异步：整个网页不刷新，访问服务器资源返回结果，实现局部的刷新。**\n\n实质：**JavaScript**和XML（但目前**JSON**的使用比XML更加普遍）\n\n封装**Fastjson**工具类\n\n```javascript\n  //使用fastjson，将JSON对象转为JSON字符串(前提要引入Fastjson)\n  public static String getJSONString(int code, String msg, Map\u003cString, Object\u003e map) {\n      JSONObject json = new JSONObject();\n      json.put(\"code\",code);\n      json.put(\"msg\",msg);\n      if (map != null) {\n          //从map里的key集合中取出每一个key\n          for (String key : map.keySet()) {\n              json.put(key, map.get(key));\n          }\n      }\n      return json.toJSONString();\n  }\n  public static String getJSONString(int code, String msg) {\n    return getJSONString(code, msg, null);\n  }\n  public static String getJSONString(int code) {\n    return getJSONString(code, null, null);\n  }\n```\n\n### ajax异步Demo示例\n\n```java\n  /**\n   * Ajax异步请求示例\n   */\n  @RequestMapping(value = \"/ajax\", method = RequestMethod.POST)\n  @ResponseBody\n  public String testAjax(String name, int age) {\n      System.out.println(name);\n      System.out.println(age);\n      return CommunityUtil.getJSONString(200,\"操作成功！\");\n  }\n```\n\n```javascript\n  //异步JS\n  \u003cinput type=\"button\" value=\"发送\" onclick=\"send();\"\u003e\n  function send() {\n      $.post(\n          \"/community/test/ajax\",\n          {\"name\":\"张三\",\"age\":25},\n          //回调函数返回结果\n          function(data) {\n              console.log(typeof (data));\n              console.log(data);\n              //返回json字符串格式(fastJson)\n              data = $.parseJSON(data);\n              console.log(typeof (data));\n              console.log(data.code);\n              console.log(data.msg);\n          }\n      )\n  }\n\n```\n\n## 1.编写Mapper层\n\n```xml\nint insertDiscussPost(DiscussPost discussPost);\n\n\u003csql id=\"insertFields\"\u003e\n    user_id,title,content,type,status,create_time,comment_count,score\n\u003c/sql\u003e\n\u003cinsert id=\"insertDiscussPost\" parameterType=\"DiscussPost\"\u003e\n    insert into discuss_post(\u003cinclude refid=\"insertFields\"\u003e\u003c/include\u003e)\n    values (#{userId}, #{title}, #{content}, #{type}, #{status}, #{createTime}, #{commentCount}, #{score})\n\u003c/insert\u003e\n```\n\n## 2.编写Service层\n\n```java\n  public int addDiscussPost(DiscussPost post){\n      if(post == null){\n          //不用map直接抛异常\n          throw new IllegalArgumentException(\"参数不能为空！\");\n      }\n      //转义HTML标签,Springboot自带转义工具HtmlUtils.htmlEscape()\n      post.setTitle(HtmlUtils.htmlEscape(post.getTitle()));\n      post.setContent(HtmlUtils.htmlEscape(post.getContent()));\n      //过滤敏感词\n      post.setTitle(sensitiveFilter.filter(post.getTitle()));\n      post.setContent(sensitiveFilter.filter(post.getContent()));\n\n      return discussPostMapper.insertDiscussPost(post);\n  }\n\n```\n\n## 3.编写Controller层(异步请求要加@ResponseBody,且不用在Controller层用Model，用Js)\n\n```java\n  @Autowired\n  private DiscussPostService discussPostService;\n  @Autowired\n  private HostHolder hostHolder;\n  \n  @RequestMapping(value = \"/add\", method = RequestMethod.POST)\n  @ResponseBody    //返回Json格式，一定要加@ResponseBody\n  public String addDiscussPost(String title, String content){\n      //获取当前登录的用户\n      User user = hostHolder.getUser();\n      if (user == null){\n          //403权限不够\n          return CommunityUtil.getJSONString(403,\"你还没有登录哦！\");\n      }\n      DiscussPost post = new DiscussPost();\n      post.setUserId(user.getId());\n      post.setTitle(title);\n      post.setContent(content);\n      post.setCreateTime(new Date());\n      //业务处理，将用户给的title，content进行处理并添加进数据库\n      discussPostService.addDiscussPost(post);\n\n      //返回Json格式字符串给前端JS,报错的情况将来统一处理\n      return CommunityUtil.getJSONString(0,\"发布成功！\");\n  }\n```\n\n## 4.编写前端异步JS\n\n注意：\\$.parseJSON(data) →通过jQuery，将服务端返回的JSON格式的字符串转为js对象\n\n```javascript\n$(function(){\n  $(\"#publishBtn\").click(publish);\n});\n\nfunction publish() {\n  $(\"#publishModal\").modal(\"hide\");\n  /**\n  * 服务器处理\n  */\n  // 获取标题和内容\n  var title = $(\"#recipient-name\").val();\n  var content = $(\"#message-text\").val();\n  // 发送异步请求(POST)\n  $.post(\n    CONTEXT_PATH + \"/discuss/add\",\n    //与Controller层两个属性要一致！！！\n    {\"title\":title,\"content\":content},\n    function(data) {\n      //把json字符串转化成Js对象,后面才可以调用data.msg\n      data = $.parseJSON(data);\n      // 在提示框中显示返回消息\n      $(\"#hintBody\").text(data.msg);\n      // 显示提示框\n      $(\"#hintModal\").modal(\"show\");\n      // 2秒后,自动隐藏提示框\n      setTimeout(function(){\n        $(\"#hintModal\").modal(\"hide\");\n        // 刷新页面\n        if(data.code == 0) {\n          window.location.reload();\n        }\n      }, 2000);\n    }\n  );\n}\n```\n\n# 查看帖子详情\n\n## 1.编写Mapper层\n\n```xml\nDiscussPost selectDiscussPostById(int id);\n\u003c----------------------\u003e\n\u003cselect id=\"selectDiscussPostById\" resultType=\"DiscussPost\"\u003e\n    select \u003cinclude refid=\"selectFields\"\u003e\u003c/include\u003e\n    from discuss_post\n    where id = #{id}\n\u003c/select\u003e\n```\n\n## 2.编写Service层\n\n```java\n  public DiscussPost findDiscussPostById(int id){\n      return discussPostMapper.selectDiscussPostById(id);\n  }\n```\n\n## 3.编写Controller层\n\n```java\n  @RequestMapping(value = \"/detail/{discussPostId}\", method = RequestMethod.GET)\n  public String getDiscusspost(@PathVariable(\"discussPostId\") int discussPostId, Model model){\n      //通过前端传来的Id查询帖子\n      DiscussPost post = discussPostService.findDiscussPostById(discussPostId);\n      model.addAttribute(\"post\",post);\n\n      //用以显示发帖人的头像及用户名\n      User user = userService.findUserById(post.getUserId());\n      model.addAttribute(\"user\",user);\n      return \"/site/discuss-detail\";\n  }\n```\n\n## 4.编写前端核心部分（进入详情链接及Controller层中的model）\n\n```html\n\u003c!--前端点击进入详情的链接--\u003e\n\u003cli th:each=\"map:${discussPosts}\"\u003e\n  \u003ca th:href=\"@{|/discuss/detail/${map.post.id}|}\" th:utext=\"${map.post.title}\"\u003e标题链接\u003c/a\u003e\n\u003c/li\u003e\n\nth:utext=\"${post.getTitle()}\"     \u003c!--标题--\u003e\nth:src=\"${user.getHeaderUrl()}\"   \u003c!--用户头像--\u003e\nth:utext=\"${user.getUsername()}\"  \u003c!--用户名字--\u003e\nth:text=\"${#dates.format(post.getCreateTime(),'yyyy-MM-dd HH:mm:ss')}\"   \u003c!--发帖时间--\u003e\nth:utext=\"${post.getContent()}\"   \u003c!--发帖内容--\u003e\n```\n\n# 事务管理\n\n## 1.概念\n\n### 1.1事务的特性\n\n原子性：**即事务是应用中不可再分的最小执行体。**\n\n一致性：**即事务执行的结果，必须使数据从一个一致性状态，变为另一个一致性状态。**\n\n隔离性：**即各个事务的执行互不干扰，任何事务的内部操作对其他的事务都是隔离的。**\n\n持久性：**事务一旦提交，对数据所做的任何改变都要记录到永久存储器。**\n\n### 1.2事务的四种隔离级别\n\nRead Uncommitted： 读未提交（级别**最低**）\n\nRead Committed： 读已提交\n\nRepeatable Read： 可重复读\n\nSerializable： 串行化（级别**最高** ，*性能最低，因为要加锁）*\n\n### 1.3并发异常\n\n- 第一类丢失更新\n\n- 第二类丢失更新\n\n- 脏读\n\n- 不可重复读\n\n- 幻读\n\n![](image/3_Mbdb-PY0NL.PNG)\n\n![](image/4_YlSXH6_OBG.PNG)\n\n![](image/5_a3J6VuhqzZ.PNG)\n\n![](image/6_4dy3KJ0Wtd.PNG)\n\n![](image/7_B6xJyOOtTx.PNG)\n\n![](image/8_Nd_jlUfSXc.PNG)\n\n![](image/9_hSNFRdQQ1L.PNG)\n\n## 2.Spring声明式事务\n\n 方法： **1.通过XML配置    2.通过注解@Transaction，如下：**\n\n```java\n/* REQUIRED: 支持当前事务（外部事务），如果不存在则创建新事务\n * REQUIRED_NEW: 创建一个新事务，并且暂停当前事务（外部事务）\n * NESTED: 如果当前存在事务（外部事务），则嵌套在该事务中执行（独立的提交和回滚），否则就会和REQUIRED一样\n * 遇到错误，Sql回滚  （A-\u003eB）\n */\n@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)\n```\n\n## 3.Spring编程式事务(通常用来管理中间某一小部分事务)\n\n**方法：** **通过TransactionTemplate组件执行SQL管理事务，如下：**\n\n```java\n  public Object save2(){\n      transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);\n      transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);\n      \n      return transactionTemplate.execute(new TransactionCallback\u003cObject\u003e() {\n          @Override\n          public Object doInTransaction(TransactionStatus status) {\n              User user = new User();\n              user.setUsername(\"Marry\");\n              user.setSalt(CommunityUtil.generateUUID().substring(0,5));\n              user.setPassword(CommunityUtil.md5(\"123123\")+user.getSalt());\n              user.setType(0);\n              user.setHeaderUrl(\"http://localhost:8080/2.png\");\n              user.setCreateTime(new Date());\n              userMapper.insertUser(user);\n              //设置error,验证事务回滚\n              Integer.valueOf(\"abc\");\n              return \"ok\"; }\n      });\n }\n```\n\n# 评论功能\n\n## 显示评论（评论和评论中的回复）\n\n### 1.编写Dao层接口\n\n```java\n  /**\n   * 根据评论类型(帖子评论和回复评论)和评论Id--分页查询评论\n   * @return Comment类型集合\n   */\n  List\u003cComment\u003e selectCommentsByEntity(@Param(\"entityType\") int entityType, @Param(\"entityId\") int entityId,\n                                       @Param(\"offset\") int offset, @Param(\"limit\") int limit);\n\n  int selectCountByEntity(@Param(\"entityType\") int entityType, @Param(\"entityId\") int entityId);\n  \u003c!------------------Mapper.xml----------------------\u003e \n    \u003cselect id=\"selectCommentsByEntity\" resultType=\"Comment\"\u003e\n        select \u003cinclude refid=\"selectFields\"\u003e\u003c/include\u003e\n        from comment\n        where status = 0\n        and entity_type = #{entityType}\n        and entity_Id = #{entityId}\n        order by create_time asc\n        limit  #{offset}, #{limit}\n    \u003c/select\u003e\n\n    \u003cselect id=\"selectCountByEntity\" resultType=\"int\"\u003e\n        select count(id)\n        from comment\n        where status = 0\n        and entity_type = #{entityType}\n        and entity_id = #{entityId}\n    \u003c/select\u003e\n  \n```\n\n### 2.编写业务Service层\n\n```java\n  public List\u003cComment\u003e findCommentsByEntity(int entityType, int entityId, int offset, int limit){\n      return commentMapper.selectCommentsByEntity(entityType, entityId, offset, limit);\n  }\n  public int findCommentCount(int entityType, int entityId){\n      return commentMapper.selectCountByEntity(entityType, entityId);\n  }\n```\n\n### 3.编写Controller控制层（接查看帖子详情，如上）难点（类似于套娃）！\n\n```java\n  @RequestMapping(value = \"/detail/{discussPostId}\", method = RequestMethod.GET)\n  public String getDiscusspost(@PathVariable(\"discussPostId\") int discussPostId, Model model, Page page) {\n      //通过前端传来的Id查询帖子\n      DiscussPost post = discussPostService.findDiscussPostById(discussPostId);\n      model.addAttribute(\"post\", post);\n\n      //查询发帖人的头像及用户名\n      User user = userService.findUserById(post.getUserId());\n      model.addAttribute(\"user\", user);\n      \n      // 点赞数量\n      long likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_POST, discussPostId);\n      model.addAttribute(\"likeCount\", likeCount);\n\n      // 点赞状态 (没登录就显示0)\n      int likeStatus = hostHolder.getUser() == null ? '0' : likeService.findEntityLikeStatus(hostHolder.getUser().getId(), ENTITY_TYPE_POST, discussPostId);\n      model.addAttribute(\"likeStatus\", likeStatus);\n\n      //设置评论分页信息\n      page.setLimit(3);\n      page.setPath(\"/discuss/detail/\"+discussPostId);\n      page.setRows(post.getCommentCount());\n      \n      // 评论: 给帖子的评论\n      // 回复: 给评论的评论\n      // 评论列表集合\n      List\u003cComment\u003e commentList = commentService.findCommentsByEntity(ENTITY_TYPE_POST, post.getId(), page.getOffset(), page.getLimit());\n\n      // 评论VO(viewObject)列表 (将comment,user信息封装到每一个Map，每一个Map再封装到一个List中)\n      List\u003cMap\u003cString, Object\u003e\u003e commentVoList = new ArrayList\u003c\u003e();\n      if (commentList != null){\n          // 每一条评论及该评论的用户封装进map集合\n          for (Comment comment : commentList){\n              // 评论Map--\u003ecommentVo\n              HashMap\u003cString, Object\u003e commentVo = new HashMap\u003c\u003e();\n              // 评论\n              commentVo.put(\"comment\", comment);\n              // 作者(由comment表中 entity = 1 查user表)\n              commentVo.put(\"user\", userService.findUserById(comment.getUserId()));\n              \n              // 点赞数量\n              likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_COMMENT, comment.getId());\n              commentVo.put(\"likeCount\", likeCount);\n              // 点赞状态 (没登录就显示0)\n              likeStatus = hostHolder.getUser() == null ? '0' : likeService.findEntityLikeStatus(hostHolder.getUser().getId(), ENTITY_TYPE_COMMENT, comment.getId());\n              commentVo.put(\"likeStatus\", likeStatus);\n\n              // 回复列表集合（每一条评论的所有回复,不分页）\n              List\u003cComment\u003e replyList = commentService.findCommentsByEntity(ENTITY_TYPE_COMMENT, comment.getId(), 0, Integer.MAX_VALUE);\n\n              // 回复VO\n              List\u003cMap\u003cString, Object\u003e\u003e replyVoList = new ArrayList\u003c\u003e();\n              if (replyList !=null){\n                  for (Comment reply : replyList){\n                      // 回复Map\n                      HashMap\u003cString, Object\u003e replyVo = new HashMap\u003c\u003e();\n                      // 回复\n                      replyVo.put(\"reply\", reply);\n                      // 作者 (由comment表中 entity = 2 查user表)\n                      replyVo.put(\"user\", userService.findUserById(reply.getUserId()));\n                      // 回复目标 (有2种：1.直接回复 2.追加回复)\n                      User target = reply.getTargetId() == 0 ? null : userService.findUserById(reply.getTargetId());\n                      replyVo.put(\"target\", target);\n                      \n                      // 点赞数量\n                      likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_COMMENT, reply.getId());\n                      replyVo.put(\"likeCount\", likeCount);\n                      // 点赞状态 (没登录就显示0)\n                      likeStatus = hostHolder.getUser() == null ? '0' : likeService.findEntityLikeStatus(hostHolder.getUser().getId(), ENTITY_TYPE_COMMENT, reply.getId());\n                      replyVo.put(\"likeStatus\", likeStatus);\n                      \n                      // 将每一个回复Map放在回复List中\n                      replyVoList.add(replyVo);\n                  }\n              }\n              // 将每一个回复List放在评论Map中\n              commentVo.put(\"replys\", replyVoList);\n\n              // 回复数量统计\n              int replyCount = commentService.findCommentCount(ENTITY_TYPE_COMMENT, comment.getId());\n              commentVo.put(\"replyCount\", replyCount);\n\n              // 再将每一个评论Map放在评论List中\n              commentVoList.add(commentVo);\n          }\n      }\n      // 最后将整个List传给前端model渲染\n      model.addAttribute(\"comments\", commentVoList);\n\n      return \"/site/discuss-detail\";\n  }\n```\n\n### 4.编写前端Thymeleaf页面（核心部分）\n\n注意： xxxStat—\u003eThymeleaf内置对象\n\n```html\n\u003c!-- 回帖列表 --\u003e\n\u003cli class=\"media pb-3 pt-3 mb-3 border-bottom\" th:each=\"cvo:${comments}\"\u003e\n  \u003cimg th:src=\"${cvo.user.getHeaderUrl()}\" alt=\"用户头像\"\u003e\n  \u003cdiv\u003e\n    \u003cspan th:utext=\"${cvo.user.getUsername()}\"\u003e用户姓名\u003c/span\u003e\n    \u003cspan\u003e\n      \u003ci th:text=\"${page.offset + cvoStat.count}\"\u003e1 评论楼层\u003c/i\u003e#\n    \u003c/span\u003e\n  \u003c/div\u003e\n  \u003cdiv th:utext=\"${cvo.comment.content}\"\u003e\n    评论内容\n  \u003c/div\u003e\n  \u003cspan\u003e发布于 \u003cb th:text=\"${#dates.format(cvo.comment.createTime,'yyyy-MM-dd HH:mm:sss')}\"\u003e时间\u003c/b\u003e\u003c/span\u003e\n  \u003cul\u003e\n    \u003cli\u003e\u003ca href=\"#\"\u003e回复(\u003ci th:text=\"${cvo.replyCount}\"\u003e2\u003c/i\u003e)\u003c/a\u003e\u003c/li\u003e\n  \u003c/ul\u003e\n  \n  \u003c!-- 回复列表 --\u003e\n  \u003cli th:each=\"rvo:${cvo.replys}\"\u003e\n    \u003cdiv\u003e\n      \u003c!--直接回复--\u003e\n      \u003cspan th:if=\"${rvo.target==null}\"\u003e\n        \u003cb th:utext=\"${rvo.user.username}\"\u003e回复人姓名\u003c/b\u003e\n      \u003c/span\u003e\n      \u003c!--追加回复--\u003e\n      \u003cspan th:if=\"${rvo.target!=null}\"\u003e\n        \u003ci th:text=\"${rvo.user.username}\"\u003e回复人姓名\u003c/i\u003e 回复\n        \u003cb th:text=\"${rvo.target.username}\"\u003e被回复人姓名\u003c/b\u003e\n      \u003c/span\u003e\n      \u003cspan th:utext=\"${rvo.reply.content}\"\u003e回复内容\u003c/span\u003e\n    \u003c/div\u003e\n    \n    \u003cdiv\u003e\n      \u003cspan th:text=\"${#dates.format(rvo.reply.createTime,'yyyy-MM-dd HH:mm:ss')}\"\u003e回复时间\u003c/span\u003e\n      \u003cul\u003e\n        \u003cli\u003e\u003ca href=\"#\"\u003e赞(1)\u003c/a\u003e\u003c/li\u003e\n        \u003cli\u003e|\u003c/li\u003e\n        \u003c!--关联id对应回复  动态拼接--\u003e\n        \u003cli\u003e\u003ca th:href=\"|#huifu-${rvoStat.count}|\" data-toggle=\"collapse\"\u003e回复\u003c/a\u003e\u003c/li\u003e\n      \u003c/ul\u003e\n      \n      \u003cdiv th:id=\"|huifu-${rvoStat.count}|\"\u003e\n        \u003cinput type=\"text\" th:placeholder=\"|回复${rvo.user.username}|\"/\u003e\n        \u003cbutton type=\"button\" onclick=\"#\"\u003e回复\u003c/button\u003e                   \n      \u003c/div\u003e\n    \u003c/div\u003e                \n  \u003c/li\u003e\n\u003c/li\u003e\n最后复用分页：th:replace=\"index::pagination\"\n```\n\n## 添加评论  (用到事务管理)\n\n### 1.编写Dao层 （1.增加评论数据CommentMapper 2.修改帖子评论数量DiscussPostMapper）\n\n```java\n //CommentMapper \n int insertComment(Comment comment);\n \u003cinsert id=\"insertComment\" parameterType=\"Comment\"\u003e\n    insert into comment(\u003cinclude refid=\"insertFields\"\u003e\u003c/include\u003e)\n    values (#{userId}, #{entityType}, #{entityId}, #{targetId}, #{content}, #{status}, #{createTime})\n \u003c/insert\u003e\n \n //DiscussPostMapper\n int updateCommentCount(@Param(\"id\") int id,@Param(\"commentCount\") int commentCount);\n \u003cupdate id=\"updateCommentCount\"\u003e\n    update discuss_post set comment_count = #{commentCount}\n    where id = #{id}\n \u003c/update\u003e\n\n```\n\n### 2.编写业务Service层\n\n```java\n  //DiscussPostService\n   public int updateCommentCount(int id, int commentCount){\n      return discussPostMapper.updateCommentCount(id, commentCount);\n   }\n   \n  //CommentService\n  /**\n   * 添加评论(涉及事务)\n   * 先添加评论，后修改discuss_post中的评论数（作为一个整体事务，出错需要整体回滚！）\n   */\n  @Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)\n  public int addComment(Comment comment){\n      if (comment == null){\n          throw new IllegalArgumentException(\"参数不能为空！\");\n      }\n      /**添加评论**/\n      //过滤标签\n      comment.setContent(HtmlUtils.htmlEscape(comment.getContent()));\n      //过滤敏感词\n      comment.setContent(sensitiveFilter.filter(comment.getContent()));\n      int rows =commentMapper.insertComment(comment);\n      /**\n       * 更新帖子评论数量\n       * 如果是帖子类型才更改帖子评论数量，并且获取帖子评论的id\n       */\n      if (comment.getEntityType() == ENTITY_TYPE_POST){\n          int count = commentMapper.selectCountByEntity(comment.getEntityType(), comment.getEntityId());\n          discussPostService.updateCommentCount(comment.getEntityId(), count);\n      }\n      return rows;\n  }\n```\n\n### 3.编写Controller层\n\n```java\n  //需要从前端带一个参数\n  @RequestMapping(value = \"/add/{discussPostId}\", method = RequestMethod.POST)\n  public String addComment(@PathVariable(\"discussPostId\") int discussPostId, Comment comment){\n      comment.setUserId(hostHolder.getUser().getId());\n      comment.setStatus(0);\n      comment.setCreateTime(new Date());\n      commentService.addComment(comment);\n      return \"redirect:/discuss/detail/\" + discussPostId;\n  }\n```\n\n### 4.编写Thymleaf前端页面（核心）\n\n```html\n\u003c!--帖子评论框--\u003e\n\u003cform method=\"post\" th:action=\"@{|/comment/add/${post.id}|}\"\u003e\n  \u003cp\u003e\n    \u003ctextarea placeholder=\"帖子评论框!\" name=\"content\"\u003e\u003c/textarea\u003e\n    \u003cinput type=\"hidden\" name=\"entityType\" value=\"1\"\u003e\n    \u003cinput type=\"hidden\" name=\"entityId\" th:value=\"${post.id}\"\u003e\n  \u003c/p\u003e\n  \u003cbutton type=\"submit\"\u003e回帖\u003c/button\u003e\n\u003c/form\u003e\n\n\u003c!--回复输入框--\u003e\n\u003cform method=\"post\" th:action=\"@{|/comment/add/${post.id}|}\"\u003e\n  \u003cdiv\u003e\n    \u003cinput type=\"text\" name=\"content\" placeholder=\"回复输入框\"/\u003e\n    \u003cinput type=\"hidden\" name=\"entityType\" value=\"2\"\u003e\n    \u003c!--回复评论id，即entityType=2的评论id--\u003e\n    \u003cinput type=\"hidden\" name=\"entityId\" th:value=\"${cvo.comment.id}\"\u003e\n  \u003c/div\u003e\n  \u003cbutton type=\"submit\" onclick=\"#\"\u003e回复\u003c/button\u003e\n\u003c/form\u003e\n\n\u003c!--追加回复框--\u003e\n\u003cform method=\"post\" th:action=\"@{|/comment/add/${post.id}|}\"\u003e\n  \u003cdiv\u003e\n    \u003cinput type=\"text\" name=\"content\" th:placeholder=\"|回复${rvo.user.username}|\"/\u003e\n    \u003cinput type=\"hidden\" name=\"entityType\" value=\"2\"\u003e\n    \u003cinput type=\"hidden\" name=\"entityId\" th:value=\"${cvo.comment.id}\"\u003e\n    \u003c!--回复评论的作者id--\u003e\n    \u003cinput type=\"hidden\" name=\"targetId\" th:value=\"${rvo.user.id}\"\u003e\n  \u003c/div\u003e\n  \u003cbutton type=\"submit\" onclick=\"#\"\u003e回复\u003c/button\u003e\n\u003c/form\u003e\n```\n\n# 私信功能\n\n## 显示私信列表（难度在写SQL）\n\n### 1.编写Dao层\n\n```java\n  /**查询当前用户的会话列表,针对每个会话只返回一条最新的私信**/\n  List\u003cMessage\u003e selectConversations(@Param(\"userId\") int userId,@Param(\"offset\") int offset,@Param(\"limit\") int limit);\n\n  /**查询当前用户的会话数量**/\n  int selectConversationCount(@Param(\"userId\") int userId);\n\n  /**查询某个会话所包含的私信列表**/\n  List\u003cMessage\u003e selectLetters(@Param(\"conversationId\") String conversationId,@Param(\"offset\") int offset,@Param(\"limit\") int limit);\n\n  /**查询某个会话所包含的私信数量**/\n  int selectLetterCount(@Param(\"conversationId\") String conversationId);\n  /**\n   * 查询未读的数量\n   * 1.带参数conversationId ：私信未读数量\n   * 2.不带参数conversationId ：当前登录用户 所有会话未读数量\n   */\n  int selectLetterUnreadCount(@Param(\"userId\")int userId,@Param(\"conversationId\") String conversationId);\n```\n\n### 2.编写Mapper.xml(难度)\n\n```sql\n\u003csql id=\"selectFields\"\u003e\n    id, from_id, to_id, conversation_id, content, status, create_time\n\u003c/sql\u003e\n\n\u003cselect id=\"selectConversations\" resultType=\"Message\"\u003e\n    select \u003cinclude refid=\"selectFields\"\u003e\u003c/include\u003e\n    from message\n    where id in (\n        //子句根据id大小查与每个用户最新的私信（同一会话id越大，私信越新）\n        //也可根据时间戳判断\n        select max(id) from message\n        where status != 2\n        and from_id != 1\n        and (from_id = #{userId} or to_id = #{userId})\n        group by conversation_id   //同一会话只显示一条\n    )\n    order by id desc\n    limit #{offset}, #{limit}\n\u003c/select\u003e\n\n\u003cselect id=\"selectConversationCount\" resultType=\"int\"\u003e\n    select count(m.maxid) from (\n       select max(id) as maxid from message\n       where status != 2\n       and from_id != 1\n       and (from_id = #{userId} or to_id = #{userId})\n       group by conversation_id\n   ) as m\n\u003c/select\u003e\n\n\u003cselect id=\"selectLetters\" resultType=\"Message\"\u003e\n    select \u003cinclude refid=\"selectFields\"\u003e\u003c/include\u003e\n    from message\n    where status != 2\n    and from_id != 1\n    and conversation_id = #{conversationId}\n    order by id asc\n    limit #{offset}, #{limit}\n\u003c/select\u003e\n\n\u003cselect id=\"selectLetterCount\" resultType=\"int\"\u003e\n    select count(id)\n    from message\n    where status != 2\n    and from_id != 1\n    and conversation_id = #{conversationId}\n\u003c/select\u003e\n\n\u003cselect id=\"selectLetterUnreadCount\" resultType=\"int\"\u003e\n    select count(id)\n    from message\n    where status = 0\n    and from_id != 1\n    and to_id = #{userId}\n    \u003cif test=\"conversationId!=null\"\u003e //=null:所有会话未读数 !=null:每条会话未读数\n        and conversation_id = #{conversationId}\n    \u003c/if\u003e\n\u003c/select\u003e\n```\n\n### 3.编写Service层\n\n```java\n  @Autowired\n  private MessageMapper messageMapper;\n\n  public List\u003cMessage\u003e findConversations(int userId, int offset, int limit){\n      return messageMapper.selectConversations(userId, offset, limit);\n  }\n  public int findConversationCount(int userId) {\n      return messageMapper.selectConversationCount(userId);\n  }\n  public List\u003cMessage\u003e findLetters(String conversationId, int offset, int limit) {\n      return messageMapper.selectLetters(conversationId, offset, limit);\n  }\n  public int findLetterCount(String conversationId) {\n      return messageMapper.selectLetterCount(conversationId);\n  }\n  public int findLetterUnreadCount(int userId, String conversationId) {\n      return messageMapper.selectLetterUnreadCount(userId, conversationId);\n  }\n```\n\n### 4.编写Controller层\n\n#### 4.1私信列表Controller\n\n```java\n  /**私信列表**/\n  @RequestMapping(value = \"/letter/list\", method = RequestMethod.GET)\n  public String getLetterList(Model model, Page page){\n      // 获取当前登录用户\n      User user = hostHolder.getUser();\n      // 分页信息\n      page.setLimit(5);\n      page.setPath(\"/letter/list\");\n      page.setRows(messageService.findConversationCount(user.getId()));\n      // 会话列表\n      List\u003cMessage\u003e conversationList = messageService.findConversations(user.getId(), page.getOffset(), page.getLimit());\n      List\u003cMap\u003cString, Object\u003e\u003e conversations = new ArrayList\u003c\u003e();\n      if (conversationList != null){\n          for (Message message : conversationList){\n              HashMap\u003cString, Object\u003e map = new HashMap\u003c\u003e();\n              // 与当前登录用户每一条会话的所有信息\n              map.put(\"conversation\", message);\n              // 当前登录用户与每一个会话人的私信条数\n              map.put(\"letterCount\", messageService.findLetterCount(message.getConversationId()));\n              // 当前登录用户与每一个会话人的未读条数\n              map.put(\"unreadCount\", messageService.findLetterUnreadCount(user.getId(), message.getConversationId()));\n              // 当前登录用户若与当前会话信息中fromId相同，则目标id为ToId;\n              int targetId = user.getId() == message.getFromId() ? message.getToId() : message.getFromId();\n              User target = userService.findUserById(targetId);\n              map.put(\"target\", target);\n              \n              conversations.add(map);\n          }\n      }\n      model.addAttribute(\"conversations\", conversations);\n      // 当前登录用户总未读条数\n      int letterUnreadCount = messageService.findLetterUnreadCount(user.getId(), null);\n      model.addAttribute(\"letterUnreadCount\", letterUnreadCount);\n\n      return \"/site/letter\";\n  }\n```\n\n#### 4.2私信详情Controller\n\n```java\n  /**私信详情**/\n  @RequestMapping(value = \"/letter/detail/{conversationId}\", method = RequestMethod.GET)\n  public String getLetterDetail(@PathVariable(\"conversationId\")String conversationId, Model model, Page page){\n      //分页信息\n      page.setLimit(5);\n      page.setPath(\"/letter/detail/\"+conversationId);\n      page.setRows(messageService.findLetterCount(conversationId));\n\n      //获取私信信息\n      List\u003cMessage\u003e letterlist = messageService.findLetters(conversationId, page.getOffset(), page.getLimit());\n      List\u003cMap\u003cString, Object\u003e\u003e letters = new ArrayList\u003c\u003e();\n      if (letterlist != null){\n          for(Message message : letterlist){\n              HashMap\u003cString, Object\u003e map = new HashMap\u003c\u003e();\n              //map封装每条私信\n              map.put(\"letter\", message);\n              map.put(\"fromUser\",userService.findUserById(message.getFromId()));\n\n              letters.add(map);\n          }\n      }\n      model.addAttribute(\"letters\",letters);\n      //私信目标\n      model.addAttribute(\"target\",getLetterTarget(conversationId));\n      return \"/site/letter-detail\";\n  }\n\n  /**封装获取目标会话用户(将如：101_107拆开) **/\n  private User getLetterTarget(String conversationId) {\n      String[] ids = conversationId.split(\" _\");\n      int id0 = Integer.parseInt(ids[0]);\n      int id1 = Integer.parseInt(ids[1]);\n\n      if (hostHolder.getUser().getId() == id0) {\n          return userService.findUserById(id1);\n      } else {\n          return userService.findUserById(id0);\n      }\n  }\n\n```\n\n### 5.编写Thymeleaf前端页面（核心）\n\n#### 5.1私信列表页面\n\n```html\n\u003ca th:href=\"@{/letter/list}\"\u003e\n  朋友私信\u003cspan th:text=\"${letterUnreadCount}\" th:if=\"${letterUnreadCount!=0}\"\u003e总私信未读数\u003c/span\u003e\n\u003c/a\u003e\n\n\u003cli th:each=\"map:${conversations}\"\u003e\n  \u003cspan th:text=\"${map.unreadCount}\" th:if=\"${map.unreadCount!=0}\"\u003e单个会话未读数\u003c/span\u003e\n  \u003ca th:href=\"@{/profile}\"\u003e\n    \u003cimg th:src=\"${map.target.headerUrl}\" alt=\"用户头像\" \u003e\n  \u003c/a\u003e\n  \u003cdiv\u003e\n    \u003cspan th:utext=\"${map.target.username}\"\u003e会话目标姓名\u003c/span\u003e\n    \u003cspan th:text=\"${#dates.format(map.conversation.createTime,'yyyy-MM-dd HH:mm:ss')}\"\u003e会话最新时间\u003c/span\u003e\n    \u003ca th:href=\"@{|/letter/detail/${map.conversation.conversationId}|}\" th:utext=\"${map.conversation.content}\"\u003e会话内容，可进入详情页\u003c/a\u003e\n    \u003cul\u003e\n      \u003cli\u003e\u003ca href=\"#\"\u003e共\u003ci th:text=\"${map.letterCount}\"\u003e5\u003c/i\u003e条会话\u003c/a\u003e\u003c/li\u003e\n    \u003c/ul\u003e\n  \u003c/div\u003e\n\u003c/li\u003e\n```\n\n#### 5.2私信详情页面\n\n```html\n\u003ch6\u003e来自 \u003ci th:utext=\"${target.username}\"\u003e目标会话用户\u003c/i\u003e 的私信\u003c/h6\u003e\n\n\u003cli th:each=\"map:${letters}\"\u003e\n  \u003cimg th:src=\"${map.fromUser.headerUrl}\" alt=\"用户头像\" \u003e\n\u003cdiv\u003e\n  \u003cstrong th:utext=\"${map.fromUser.username}\"\u003e会话发起人姓名\u003c/strong\u003e\n  \u003csmall th:text=\"${#dates.format(map.letter.createTime,'yyyy-MM-dd HH:mm:ss')}\"\u003e时间\u003c/small\u003e\n\u003c/div\u003e\n\u003cdiv th:utext=\"${map.letter.content}\"\u003e\n   私信内容\n\u003c/div\u003e\n\u003c/li\u003e\n\n```\n\n## 发送私信功能（异步）\n\n### 1.编写Dao层\n\n```sql\n  /**插入会话**/\n  int insertMessage(Message message);\n  /**批量更改每个会话的所有未读消息为已读**/\n  int updateStatus(@Param(\"id\") List\u003cInteger\u003e ids,@Param(\"status\") int status);\n  \n  -----------------------Mapper.xml-----------------------------\n  \u003cinsert id=\"insertMessage\" parameterType=\"Message\" keyProperty=\"id\"\u003e\n    insert into message(\u003cinclude refid=\"insertFields\"\u003e\u003c/include\u003e)\n    values(#{fromId},#{toId},#{conversationId},#{content},#{status},#{createTime})\n  \u003c/insert\u003e\n  \n  \u003cupdate id=\"updateStatus\"\u003e\n      update message set status = #{status}\n      where id in\n      -----批量传入id写法\n      \u003cforeach collection=\"ids\" item=\"id\" open=\"(\" separator=\",\" close=\")\"\u003e\n          #{id}\n      \u003c/foreach\u003e\n  \u003c/update\u003e\n  \n```\n\n### 2.编写Service层\n\n```java\n  public int addMessage(Message message){\n      //转义标签\n      message.setContent(HtmlUtils.htmlEscape(message.getContent()));\n      //过滤敏感词\n      message.setContent(sensitiveFilter.filter(message.getContent()));\n      return messageMapper.insertMessage(message);\n  }\n  public int readMessage(List\u003cInteger\u003e ids){\n      return messageMapper.updateStatus(ids, 1);\n  }\n```\n\n### 3.编写Controller层\n\n#### 3.1设置已读\n\n```java\n  @RequestMapping(value = \"/letter/detail/{conversationId}\", method = RequestMethod.GET)\n  public String getLetterDetail(@PathVariable(\"conversationId\")String conversationId, Model model, Page page){\n        /**\n        * 以上省略。。。。。。\n        */\n        //设置已读(当打开这个页面是就更改status =1)\n        List\u003cInteger\u003e ids = getLetterIds(letterlist);\n        if (!ids.isEmpty()) {\n            messageService.readMessage(ids);\n       }\n   }\n\n  /**获得批量私信的未读数id* */\n  private List\u003cInteger\u003e getLetterIds(List\u003cMessage\u003e letterList){\n      List\u003cInteger\u003e ids = new ArrayList\u003c\u003e();\n\n      if (letterList != null) {\n          for (Message message : letterList) {\n              //只有当前登录用户与message列表中目标用户一致并且staus = 0 时才是未读数，加入未读私信集合\n              if (hostHolder.getUser().getId() == message.getToId() \u0026\u0026 message.getStatus() == 0) {\n                  ids.add(message.getId());\n              }\n          }\n      }\n      return ids;\n  }\n```\n\n#### 3.2 发送私信\n\n```java\n  /**发送私信* */\n  @RequestMapping(value = \"/letter/send\", method = RequestMethod.POST)\n  @ResponseBody\n  public String sendLetter(String toName, String content){\n      //根据目标发送人姓名获取其id\n      User target = userService.findUserByName(toName);\n      if (target == null){\n          return CommunityUtil.getJSONString(1,\"目标用户不存在!\");\n      }\n\n      //设置message属性\n      Message message = new Message();\n      message.setFromId(hostHolder.getUser().getId());\n      message.setToId(target.getId());\n      message.setContent(content);\n      message.setCreateTime(new Date());\n      // conversationId (如101_102: 小_大)\n      if (message.getFromId() \u003c message.getToId()) {\n          message.setConversationId(message.getFromId() + \" _\" +message.getToId());\n      }else{\n          message.setConversationId(message.getToId() + \"_\" +message.getFromId());\n      }\n      messageService.addMessage(message);\n\n      return CommunityUtil.getJSONString(0);\n  }\n```\n\n### 4.编写前端JS异步请求（ajax）\n\n```javascript\nfunction send_letter() {\n  $(\"#sendModal\").modal(\"hide\");\n  //若用JS异步请求，前端参数不用name= \"xxx\",用如下方法\n  var toName = $(\"#recipient-name\").val();\n  var content = $(\"#message-text\").val();\n\n  $.post(\n    // 接口路径(与@RequestMapping(value = \"/letter/send\", method = RequestMethod.POST)路径一致)\n    CONTEXT_PATH + \"/letter/send\",\n    // 接口参数(与public String sendLetter(String toName, String content)参数一致)\n    {\"toName\":toName, \"content\":content},\n    function (data) {\n      // 把{\"toName\":toName, \"content\":content}转换成JS对象\n      data = $.parseJSON(data);\n      // 与CommunityUtil.getJSONString(0,\"msg\")匹配--0：成功\n      if (data.code == 0){\n        $(\"#hintBody\").text(\"发送成功！\");\n      }else {\n        $(\"#hintBody\").text(data.msg);\n      }\n\n      $(\"#hintModal\").modal(\"show\");\n      setTimeout(function(){\n        $(\"#hintModal\").modal(\"hide\");\n        //刷新页面\n        location.reload();\n      }, 2000);\n    }\n  );\n}\n```\n\n# 点赞功能（Redis+异步ajax）\n\n## 点赞、取消点赞\n\n\u0026#x20;   注意：**1引入pom,配置Yaml**\n\n\u0026#x20;         2.因为访问的是Redis，无需编写Dao层\n\n### 1.创建RedisKeyUtil工具类(统一格式化redis的key)\n\nk:v = like:entity:entityType:entityId -\u003e set(userId)\n\n```java\n    private static final String SPLIT = \":\";\n    private static final String PREFIX_ENTITY_LIKE = \"like:entity\";\n    private static final String PREFIX_USER_LIKE = \"like:user\";\n    /**\n    * 某个实体的赞\n    * key= like:entity:entityType:entityId -\u003e value= userId\n    */\n    public static String getEntityLikeKey(int entityType, int entityId){\n        return PREFIX_ENTITY_LIKE + SPLIT + entityType + SPLIT + entityId;\n    }\n```\n\n### 2.直接编写Service业务层\n\n```java\n    @Autowired\n    private RedisTemplate redisTemplate;\n\n    // 点赞 (记录谁点了哪个类型哪个留言/帖子id)\n    public void like(int userId, int entityType, int entityId){\n        String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId);\n        //判断like:entity:entityType:entityId 是否有对应的 userId\n        Boolean isMember = redisTemplate.opsForSet().isMember(entityLikeKey, userId);\n\n        // 第一次点赞，第二次取消点赞\n        if (isMember){\n            // 若已被点赞(即entityLikeKey里面有userId)则取消点赞-\u003e将userId从中移除\n            red","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fxumingyu2018%2Fdemonowcoder","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fxumingyu2018%2Fdemonowcoder","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fxumingyu2018%2Fdemonowcoder/lists"}