首页 > 其他分享 >湘潭大学新生匿名问答网站——解湘 项目总结

湘潭大学新生匿名问答网站——解湘 项目总结

时间:2022-09-02 23:14:41浏览次数:78  
标签:code String 湘潭 private class 匿名 解湘 tempNode public

湘潭大学新生匿名问答网站——解湘 项目总结

一.开发进度

解湘

kSjos.jpg

项目首页

kOMOK.png

大一暑假过半,7月29日建立本地工程文件

kO8Lk.png

其中项目在github上经历七次push(第八次为修改配置文件,防止数据库泄露),但在本地修改次数远远大于七次。

后端开发均为我一人完成,前端开发由他人负责。除此之外,特感谢三翼设计部门设计出此次项目的UI界面

kjOMI.png

实际应用接口数量为31个,采用APIFOX进行团队接口管理。部分后端实现接口实际因前端功能不需没有加入。

二.项目大纲

此项目立项起,我就很明确这是个传统的论坛类项目,砍去一些如上传图片用户信息修改等不必要功能。这是一个很好的练习基于Java进行Web开发的机会,不计所谓回报,我投身到了这次的开发中。

kj6Sm.png

项目技术栈总览如上图所示。

其中因技术不牢固原因,Spring Security框架最终并未能加入项目进行权限管理。考虑使用Shiro,但因假期繁忙,最终放弃。

其中邮件验证功能因避免网站使用的复杂性,最终删去。

kjhN7.png

C/S之间使用Nginx进行反向代理,因图片功能删去,未配置CDN加速。

三.项目中遇到的问题&收获

1.Base64编码后长度必须为4的倍数

​ 起因为项目采用Base64对用户密码进行加盐处理,但因方式不当。导致服务端频繁报错,无法正常使用。

2.Mybatis-plus导致Mysql数据库主键出现负数

​ 测试期间发现ORM框架生成的数据在Mysql中出现了id为负数的情况。如下图所示

kjCMi.png

​ MP-plus有五种主键ID生成策略:

  • AUTO,配合数据库设置自增主键,可以实现主键的自动增长,类型为nmber;

  • INPUT,由用户输入;

  • NONE,不设置,等同于INPUT;

  • ASSIGN_ID,只有当用户未输入时,采用雪花算法生成一个适用于分布式环境的全局唯一主键,类型可以是String和number;

  • ASSIGN_UUID,只有当用户未输入时,生成一个String类型的主键,但不保证全局唯一;

​ 其中默认为ASSIGN_ID,即使用雪花算法进行生成。这也就导致对应生成的主键应该使用Long类型进行存储,且设置主键类型为bigint。也可在Java代码中设置@TableId注解

3.跨域问题

​ 老生长谈的一个问题了。本次跨域代码如下:

@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        //添加映射路径
        registry.addMapping("/**")
                //是否发送Cookie
                .allowCredentials(true)
                //设置放行哪些原始域   SpringBoot2.4.4下低版本使用.allowedOrigins("*")
                .allowedOriginPatterns("*")
                //放行哪些请求方式
                .allowedMethods("GET", "POST", "PUT", "DELETE")
                //.allowedMethods("*") //或者放行全部
                //放行哪些原始请求头部信息
                .allowedHeaders("*")
                //暴露哪些原始请求头部信息
                .exposedHeaders("*");
    }
}

​ 如此常见的一个问题,并不可能在开发前后端分离的项目时未进行考虑。但本次遇到的问题是,发现前端也要在vite中配置有关跨域的内容才能使cookie正常出现。

4.响应头中set-cookie字段

  1. set-cookie是一个函数,由服务器向浏览器发出响应
  2. cookie是服务器发送给浏览器的变量
  3. 浏览器向服务器发送请求(put,get,post,delect方法),服务器会使用set-cookie()方法向本地的浏览器发送cookie,存在客户端主机中的一个文件下

set-cookie响应头可以设置如下属性:

属性 意义
NAME=VALUE 赋予 Cookie 的名称和其值(必需项)
expires=DATE Cookie 的有效期(若不明确指定则默认为浏览器关闭前为止)
path=PATH 将服务器上的文件目录作为Cookie的适用对象(若不指定则默认为文档所在的文件目录)
domain=域名 作为 Cookie 适用对象的域名 (若不指定则默认为创建 Cookie的服务器的域名)
Secure 仅在 HTTPS 安全通信时才会发送 Cookie
HttpOnly 加以限制, 使 Cookie 不能被 JavaScript 脚本访问

max-age
设置cookie的相对有效期。expire和max-age通常仅设置一个即可。比如设置max-age为1000,浏览器在添加cookie时,会自动设置它的expire为当前时间加上1000秒,作为过期时间。
如果不设置expire,又没有设置max-age,则表示会话结束后过期。
对于大部分浏览器而言,关闭所有浏览器窗口意味着会话结束。

5.用户无法发送emoji表情

​ Mysql中的utf8支持一个字符最多3个字节,但emoji表情为4个字节,所以需要切换为utf8mb4即可

6.敏感词过滤器

​ 使用前缀树算法实现敏感词过滤器。

@Component
public class SensitiveFilter {

    private static final Logger logger = LoggerFactory.getLogger(SensitiveFilter.class);

    private static final String REPLACEMENT = "***";

    private TrieNode rootNode = new TrieNode();

    @PostConstruct
    public void init() {
        try (
                InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream("sensitive-words.txt");
                BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
        ) {
            String keyword;
            while ((keyword = reader.readLine()) != null) {
                this.addKeyWord(keyword);
            }
        } catch (IOException e) {
            logger.error("敏感词加载失败: " + e.getMessage());
        }
    }


    public String filter(String text) {
        if (StringUtils.isBlank(text)) {
            return null;
        }
        TrieNode tempNode = rootNode;
        int begin = 0;
        int position = 0;
        StringBuilder sb = new StringBuilder();
        while (begin < text.length()) {
            char c = text.charAt(position);
            if (isSymbol(c)) {
                if (tempNode == rootNode) {
                    sb.append(c);
                    begin++;
                }
                position++;
                continue;
            }
            tempNode = tempNode.getSubNode(c);
            if (tempNode == null) {
                sb.append(text.charAt(begin));
                position = ++begin;
                tempNode = rootNode;
            } else if (tempNode.isEnd()) {
                sb.append(REPLACEMENT);
                begin = ++position;
                tempNode = rootNode;
            } else {
                if (position < text.length() - 1) {
                    position++;
                } else {
                    sb.append(text.charAt(begin));
                    position = ++begin;
                    tempNode = rootNode;
                }
            }
        }
        sb.append(text.substring(begin));
        return sb.toString();
    }

    private boolean isSymbol(Character c) {
        return !CharUtils.isAsciiAlphanumeric(c) && (c < 0x2E80 || c > 0x9FFF);
    }

    private void addKeyWord(String keyword) {
        TrieNode tempNode = rootNode;
        for (int i = 0; i < keyword.length(); i++) {
            char c = keyword.charAt(i);
            TrieNode subNode = tempNode.getSubNode(c);
            if (subNode == null) {
                subNode = new TrieNode();
                tempNode.addSubNode(c, subNode);
            }
            tempNode = subNode;
            if (i == keyword.length() - 1) {
                tempNode.setEnd(true);
            }
        }
    }


    private class TrieNode {
        private boolean isEnd = false;

        private Map<Character, TrieNode> setNodes = new HashMap<>();

        public boolean isEnd() {
            return isEnd;
        }

        public void setEnd(boolean end) {
            isEnd = end;
        }

        public void addSubNode(Character c, TrieNode node) {
            setNodes.put(c, node);
        }

        public TrieNode getSubNode(Character c) {
            return setNodes.get(c);
        }

    }
}

7.切实使用如Redis、Kafka等技术

​ 此前只是学习过Redis、Kafka等技术,但离专业课太远,而又没有合适的项目,于是乎一直悬浮在空中,对技术一知半解。

​ 此项目中的点赞功能、查看个人信息功能、登陆凭证存储功能、消息提醒功能均使用了Redis,也即当对象需要被频繁的访问时,我们可以使用Redis解决问题。

​ 此项目中的消息提醒使用Kafka消息队列进行开发,在学习Kafka的过程中,再次巩固了设计模式中的发布-订阅模式。在此抛出一个疑问,能否使用定期查询数据库的形式来实现消息提醒呢?

8.SSL证书配置

​ 如果要使用HTTPS,那么需要配置SSL证书,在腾讯云中可白嫖免费的证书。此次采用nginx形式进行配置,只需在nginx对应配置文件中加入固定内容,并将证书文件放在置顶目录下即可,十分简单。

server {
    listen       443 ssl;
    server_name  question.tsky31.cn;
    ssl_certificate question.tsky31.cn_bundle.crt;
    ssl_certificate_key question.tsky31.cn.key;
    ssl_session_timeout 5m;
    #请按照以下协议配置
    ssl_protocols SSLv2 SSLv3 TLSv1 TLSv1.1 TLSv1.2; 
    #请按照以下套件配置,配置加密套件,写法遵循 openssl 标准。
    ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;
    ssl_prefer_server_ciphers on;

    client_max_body_size    1000m;
}

server {
    listen 80;
    server_name question.tsky31.cn; 
    return 301 https://$host$request_uri; 
}

9.域名配置流程

​ 首先要申请一个域名,例如本次的tsky31.cn,如今域名一般都在云服务提供商的后台可以进行管理,以腾讯云为例,在申请到域名后,进行对应域名的DNS解析管理,添加进项目所需要的域名,例如question.tsky31.cn,完成解析

​ 之后在服务器中进行nginx的配置即可。

10.统一接口形式

​ 在接口的返回部分,最好是可以统一形式,采用创建一个类的方式来达到目的。其中进行重载,以适应不同的返回情况

package com.sky31.domain;

import com.fasterxml.jackson.annotation.JsonInclude;

@JsonInclude(JsonInclude.Include.NON_NULL)
public class ResponseResult<T> {
    /**
     * 状态码
     */
    private Integer code;
    /**
     * 提示信息,如果有错误时,前端可以获取该字段进行提示
     */
    private String msg;
    /**
     * 查询到的结果数据,
     */
    private T data;

    public ResponseResult(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public ResponseResult(Integer code, T data) {
        this.code = code;
        this.data = data;
    }

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }

    public ResponseResult(Integer code, String msg, T data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }
}

11.tomcat日志查看

​ 关于tomcat的日志,启动部分的成功与否,以及报错信息,存放在tomcat目录下/logs中的catalina.xxxx-xx-xx.log文件中,可以使用cat命令查看;而成功启动后的信息,如请求接口时终端的输出,则存放在目录中的catalina.out文件中,可以使用tail -f的命令进行查看。

12.内网穿透

​ 因此次服务器环境较为特殊,所以跟着教程学习了内网穿透,使用了工具frp。

13.拦截器配置

​ 关于SpringBoot的拦截器,可建一名为interceptor的软件包,在包下进行拦截器的编写。使拦截器类实现HandlerInterceptor这个接口,选择性重写其中的perHandlepostHandleafterCompletion方法。

​ 之后新建配置类,实现WebMvcConfigurer接口,在其中的addInterceptors方法中注册编写的拦截器类即可。其中拦截器注册类拥有增加排除路径的方法。

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {

    @Autowired
    private LoginInterceptor loginInterceptor;

    @Autowired
    private MessageInterceptor messageInterceptor;

    @Autowired
    private DataInterceptor dataInterceptor;


    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor)
                .excludePathPatterns();
        registry.addInterceptor(messageInterceptor)
                .excludePathPatterns();
        registry.addInterceptor(dataInterceptor)
                .excludePathPatterns();
    }
}

14.UV与DAU的统计

​ 利用拦截器的特性。我们可以将uv与dau的统计功能使用redis+拦截器进行实现。因preHandle方法会在请求到达dispatcherServlet前进行拦截,所以我们可以在其中获得用户的ip地址,根据ip进行UV的统计,按照年份天数等标准构建redis中的key,进而存放到redis中。

​ 而DAU则是获取当前用户的信息,例如可以从token中提取等,判空后进行记录。

15.用户的保存

​ 因许多接口都需要获取当前用户的身份,但每次都要用HttpServletRequest中获取,十分麻烦。如果想要在service、dao层中使用,就需要从controller层层传递。所以我们可以创建一个实用类来解决。

​ 使用方式就是利用ThreadLocal类进行保存,将用户信息保存在线程中。浏览器每一次请求就是启动了一个线程,当请求结束,我们将用户的信息销毁即可

​ 实现方式:

  • 我们需要创建一个ThreadLocal类,创建一个ThreadLocal对象,设置ThreadLocal的set,remove,get方法
  • 定义一个登录的拦截器类,实现HandlerInterceptor ,重写 preHandle() 和afterCompletion()方法 ,preHandle ()方法把登录信息写入ThreadLocal,afterCompletion()方法清除登录信息
  • 我们需要设置一些配置信息,创建一个类实现 WebMvcConfigurer ,重写addInterceptors()方法,创建一个登录拦截器类的对象,给他添加到配置中,我们就实现了ThreadLocal保存用户信息
//HostHolder.java
@Component
public class HostHolder {
    private ThreadLocal<User> users=new ThreadLocal<>();


    public void setUser(User user){
        users.set(user);
    }

    public User getUser(){
        return users.get();
    }

    public void clear(){
        users.remove();
    }
}

//在拦截器中的preHandle进行用户信息的获取
@Component
public class DataInterceptor implements HandlerInterceptor {

    public DataInterceptor() {
    }

    @Autowired
    private DataService dataService;

    @Autowired
    private HostHolder hostHolder;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //统计UV
        String ip = request.getRemoteHost();
        dataService.recordUV(ip);

        //统计DAU
        User user = hostHolder.getUser();
        if (user!=null){
            dataService.recordDAU(user.getId());
        }
        return true;
    }
}

@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
 
    @Override
    public boolean preHandle(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull Object handler) {
        //此方法是获取登录信息,登录方式不一样获取方法不一样,用户信息保存用的UserInfoVO,里边具体信息自己定义即可
        UserInfoVO userInfo = getUserInfo(request);
        ThreadLocalUser.set(userInfo);
        return true;
    }
 
    @Override
    public void afterCompletion(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull Object handler, Exception ex) {
        ThreadLocalUser.clear();
    }
}

16.环境配置

​ 本项目使用到了Redis、ElasticSearch、Kafka等,其中不可避免地在服务器环境搭建时会遇到各种各样的报错,这里进行一个总结。

​ Redis:

  • 一定要为服务器的redis设置密码,有不怀好意之人扫到6379端口后会放置挖矿病毒或是清空redis
  • redis的配置中要关闭保护模式,且redis默认无法远程连接,要更改bind设置。可选择注释掉
  • 在启动redis时,要使用上级目录中配置好的config文件,且可以使用-d参数后台运行

​ Kafka:

  • 在运行Kafka前,要先运行zookeeper,可以将这两个设置为系统的服务。并且有先后级关系
  • Kafka运行前,一定要在配置文件中进行集群的配置

​ ElasticSearch:

17.maven

​ 本项目构建工具选择了maven,关于pom.xml不再进行赘述。

​ 在服务端进行maven打包时,可以使用mvn clean package -Dmaven.test.skip=true来跳过测试类进行打包

18.Linux命令

​ 记录一些项目部署中经常使用的命令:

  • ps -ef |grep tomcat 在运行的进程中查找tomcat 之后可利用pid进行kill
  • kill -9 pid
  • tail -f $filename

19.统一处理报错

​ 当项目规模逐渐增大时,报错的处理就成为了一个问题。我们可以使用一个类来统一处理报错

@ControllerAdvice(annotations = Controller.class)
public class MyControllerAdvice {

    private static final Logger logger= LoggerFactory.getLogger(MyControllerAdvice.class);
    @ExceptionHandler(Exception.class)
    @ResponseBody
    public void handlerException(Exception e, HttpServletRequest request, HttpServletResponse response) throws IOException {
        logger.error("服务器发生异常"+e.getMessage());
        for (StackTraceElement element:e.getStackTrace()){
            logger.error(element.toString());
        }
        String requestHeader = request.getHeader("x-requested-with");
        if (requestHeader.equals("XMLHttpRequest")){
            response.setContentType("application/plain;charset=utf-8");
            PrintWriter writer=response.getWriter();
            writer.write(Md5AndJsonUtil.getJSONString(1,"服务器异常"));
        }else{
            response.sendRedirect(request.getContextPath()+"/error");
        }
    }

}

20.git

​ 起初打算采用团队形式进行开发。但因特殊原因,后端实际情况仅我一人。但仓库是已经配好的。这次学习到了使用.gitignore文件进行非上传文件的设置,只上传必要的文件,使其他用户方便使用。

HELP.md
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/

### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache

### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr

### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/

### VS Code ###
.vscode/

21.application.yaml

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/****?characterEncoding=utf-8&userSSL=false
    username: root
    password: 
  redis:
    host: localhost
    port: 6379
    password: 
    database: 0
  kafka:
    bootstrap-servers: localhost:9092
    consumer:
      group-id: test-consumer-group
      enable-auto-commit: true
      auto-commit-interval: 3000
  task:
    execution:
      pool:
        core-size: 5
        max-size: 15
        queue-capacity: 100

    scheduling:
      pool:
        size: 5

  elasticsearch:
    uris: localhost:9200

mybatis-plus:
  global-config:
    db-config:
      id-type: none
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

三.写在最后

​ 本次项目的一大遗憾是因工时原因,最终没有使用SpringSecurity,会在之后研究加入

​ 项目起初想过使用Go来编写,但最终因框架不熟悉而放弃。希望下一个项目能用Go

​ 本次项目一定程度上参考了牛客社区

​ 在19年来看,这样一个项目可以被拿来当作简历上的高薪项目来宣传,但在如今,行情十分低迷,这样的项目也早已烂大街了。也许现在正是秒杀当道吧

​ 这样也算是写过一个完整的Web开发项目,也在实验室写过了数据处理类的项目。十分圆满,总算能让浮躁的心静下来一点,之后还是着手408的学习吧。

​ 在项目的编写过程中,遇到很多毛躁的时刻,有时因服务器环境配置需要推翻重来,有时因数据结构的复杂不知如何下手。感谢和我合作的同学以及父母的鼓励

​ 目前打算使用Go手写一个RPC框架,或是尝试在Windows平台下使用DLL进行多语言合作编写一个程序,不再懊恼于大一学了很多语言的语法,却没有深入过这一点。

这个项目自认为并不是什么多么牛逼的东西。包括他的竞品也十分多,也不会获得什么奖励。但是至少就这个时间点,我的心气还能支撑我做这些事情。不知今后回顾此文,又会是何等感想呢? 2022.9.2 深夜

标签:code,String,湘潭,private,class,匿名,解湘,tempNode,public
From: https://www.cnblogs.com/appletree24/p/16651618.html

相关文章

  • C#-嵌套类匿名类与密封类
    1.嵌套类1.概念在C#中可以将一个类定义在另一个类的内部;外面的类叫“外部类”,内部的类叫“嵌套类”;嵌套类和普通类相似,只是声明的位置比较特殊。2.注意事项如果想实......
  • 【Java基础】匿名对象
    1.匿名对象在创建对象时,没有显式的赋给一个变量名,匿名对象只能调用一次。Phonephone=newPhone();//正常的对象mail.show(phone);mall.show(newPhone());2.......
  • 学习:python进阶 匿名函数,内置函数filter
               enumerate内置函数 ......
  • 进程间通信之匿名管道
    Linux手册中对匿名管道的描述如下:DESCRIPTIONpipe()createsapipe,aunidirectionaldatachannelthatcanbeusedforinterprocesscommunication.Thearray......
  • 嵌套类匿名类与封装类
    嵌套类在C#中可以将一个类定义在另一个类的内部;外面的类叫“外部类”,内部的类叫“嵌套类”;嵌套类和普通类相似,只是声明的位置比较特殊。classPerson{//外部类......
  • BTC笔记-10-匿名性
    BTC-匿名性B站视频链接比特币的匿名性比特币的匿名性弱于现金,也弱于无需实名的银行,强于实名制的银行一个人可以拥有很多个账户,但这些账户可能会被关联起来(账户与账户......
  • 匿名函数lambda
    Golang//https://blog.csdn.net/yyclassmvp/article/details/124942527sum:=func(xint,yint)int{returnx*y}Nodejsconstsum=(x,y)=>x*y;Python......
  • js-forEach和匿名函数
    foreach[].foreach(function(item,index,array){ //item:[]中的每一个元素对象 //index:[]中每一个元素对象的索引 //array:[]本身 //循环体})匿名函数arr......
  • JavaScript知识-函数基础知识、匿名函数、闭包函数、箭头函数、js内置对象和方法
    目录JavaScript函数1.函数的语法格式2.无参函数3.有参函数4.关键字arguments5.函数返回值关键字return6.匿名函数(没有函数名)7.箭头函数8.函数的全局变量与局部变量9.闭包......
  • 匿名函数
    定义时不取名字的函数,我们称之为匿名函数,匿名函数通常整体传递给其他函数,或者从其他函数的返回有了匿名函数我们可以给标准库里的内置函数(标准函数)制定特殊规则例如: ......