Bootstrap

基于Maven + SSM的论坛项目

一、先放一张成果图
在这里插入图片描述
源码已放在github:基于SSM的论坛
二、项目分析与构建环境
1、明确论坛都有哪些功能?
1.1、发帖回帖功能是最最基本的
1.2、登录注册功能
1.3、修改用户信息(修改用户名或者密码之后要重新登录)

2、具体从哪一块开始动手编写?
2.1、毫无疑问,当我们访问一个新的网站时,肯定是先登录或者注册,所以,第一步就是构建登录注册系统。
2.2、登录注册成功之后直接就进入到了主页(HomePage),在这个界面可以放一些自己想要的推送或者昨日最佳的消息。
2.3、在主页放上导航栏,导航栏又有哪些呢?第一个肯定是展示所有的帖子,第二个是查看我发布的帖子,第三个就是我的所有评论(在这里最好是点击我的评论就可以查看评论的帖子是什么),第四个就是展示我的详细信息,第五个可以设置成发布帖子。这样看来,功能应该就差不多了,如果想要添加更多的功能,可以自行添加。

3、点击帖子进入评论
把每一个帖子都做成一个超链接,点击之后就是进入到了这个帖子的页面,可以评论和查看别人的评论,也可以点赞和取消点赞,展示评论有两种方式,分别按照发布时间从早到晚以此向下排列和获赞数排序。

4、评论和发帖用CKEditor编辑器。

5、构建项目环境

5.1、分析湾项目之后就可以开始构建项目环境了,使用IDEA首先搭建Maven环境,引入需要的各个包,Spring、SpringMVC以及Mybatis的各种包,此外还有JSON、C3P0(数据源)、单元测试(IDEA构建项目的时候已经自动创建好)、日志log4j(我这里是用的是logback)、JSTL所需的包以及上传文件所需的包。
5.2、然后就是创建各个目录,在src/main创建java目录,并在其下创建controller、pojo、dao、service等包,在src/main下创建resource目录,在项目根目录下创建target目录和test目录,把test作为测试。有关详情可以参考 Maven整合SSM项目
5.3、SSM框架整合到applicationContext.xml中,注意别忘了在resource目录下创建jdbc.properties(数据库文件)、logback.xml(日志文件,可百度进行添加),把这两个配置文件在applicationContext.xml(也是在resource目录下)中进行整合。个人建议加上下面的配置,这样在Controller中进行跳转的时候就不用在写前缀了

<!-- 配置jsp 显示ViewResolver -->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
        <property name="prefix" value="/WEB-INF/views/"/>
        <property name="suffix" value=".jsp"/>
    </bean>

三、登录注册系统
1、分析
1.1、登录系统都有哪些功能?
用户名、密码登录,也可以考虑加入邮箱验证码登录,或者使用oauth第三方登录,我就简单的使用了用户名和密码登录,然后提供记住我的功能。
1.2、注册系统有哪些功能?
项目登录系统注册系统比较麻烦,首先是各个表单项,在注册时,输入用户名要使用AJAX判定用户名是否已经被使用了,填写邮箱时也是如此,确保每个邮箱只能使用一次,对于其他的项要使用JS函数判空,并利用alert()函数告知用户不能为空,至于表单项都有哪些,可以根据实际需要进行设计,我这里使用了username、password、repassword(重复密码)、sex、QQ、Email、selfIntroduction(自我介绍,只有这一项可以为空)。点击注册之后就会跳转到验证邮箱界面,这一步的目的就是为了防止瞎写邮箱(我仅仅使用了QQ邮箱,不支持其他邮箱,填写其他邮箱会跳转到注册失败界面),正确填写验正码之后完成注册进入主页。
1.3、登录和注册都使用了滑块验证,登录界面时滑动滑块之后才会出现登录按钮,注册当提交表单之后,滑动滑块之后才会出现填写验证码的输入框。
2、具体功能实现 – – 登录
2.1、选一张自己满意的背景图,当然如果前端知识学的比较好的话可以自己手动设计,我仅仅引入一张图片作为整体背景,具体方法是在CSS中使用

body{
            background: url("/images/loginbackgroud.png");
        }

注意url的路径
2.2、记住我功能使用了一个类型为checkbox的input,后台进行判断value即可

<input type="checkbox" value="y" name="isLogin">3天内记住我

2.3、滑块验证实现,这个呢,我不会,既然已经有了轮子,那我也不再去花时间造了,直接找到了滑块验证的模板,根据index.html进行改造加入自己的项目中,其实也就是在自己的项目中调用模板封装的JS函数,我的项目中滑块验证只写了这么多(不算引入的JS文件),如下

<div class="container">
    <div id="captcha" style="position: relative;"></div>
    <div id="msg"></div>
</div>

<link rel="stylesheet" href="css/jigsaw.css">
<script type="text/javascript" src="js/jigsaw.js"></script>//上面两行是引入的“轮子”

<script type="text/javascript">
    jigsaw.init(document.getElementById('captcha'), function () {
        document.getElementById("msg").innerHTML='验证成功';
        setTimeout(function() {  //显示登录按钮
            document.getElementById("buttonid").click();});
    })
</script>

具体的滑块验证模板点击这里获取:
链接:https://pan.baidu.com/s/1L_G4p3UbeWvR2Fixj_5Ing
提取码:5acn

2.4、用户名、密码判空处理。可以使用onblur()函数来处理,然后设置JS函数。

function checkpassword() {
            if(document.getElementById("password").value == null || document.getElementById("password").value.trim(" ") == "")
                document.getElementById("testpassword").innerHTML = '密码不能为空';
            else
                document.getElementById("testpassword").innerHTML ='';
        }

2.5、滑块滑动之后出现登录按钮,这里其实是之前就有登录按钮,只不过是display=“none”;隐藏了,当滑块验证成功之后,执行显示按钮的操作即可。
显示登录按钮的JS函数

function toggle(id){
            var tb=document.getElementById(id);
            if(tb.style.display=='none')
                tb.style.display='block';
        }

到此登录功能完成。
3、具体功能实现 – – 注册
3.1、注册功能的实现首先要创建数据库,用于存储用户的信息。我创建的数据库为user,另外除了表单信息之外添加userid(设置为自增、非空)、registertime(类型为datetime),实体类中注册时间要这样设置:private Timestamp registertime;
3.2、和登录时一样,要增加判空验证。
3.3、发送邮件,当点击注册按钮时,会把表单数据发送到注册的Controller,如果其中有空项或者是用户名已经存在,就会跳转到失败界面,不回发送验证码,否则就会跳转到发送验证邮箱的Controller,具体的邮箱验证和生成随机的五位验证码代码如下

@Controller
public class VerifymailController {
    @RequestMapping("/verifymail")
    public String verify(HttpServletRequest request, @RequestParam (value = "email",required = true) String email, Model model) throws MessagingException {
//        String email = (String) request.getSession().getAttribute("email");
        Properties properties = new Properties();
        properties.put("mail.transport.protocol", "smtp");// 连接协议
        properties.put("mail.smtp.host", "smtp.qq.com");// 主机名
//        properties.put("mail.smtp.port", 465);// 端口号
        properties.put("mail.smtp.auth", "true");
//        properties.put("mail.smtp.ssl.enable", "true");// 设置是否使用ssl安全连接
//        properties.put("mail.debug", "true");// 设置是否显示debug信息 true 会在控制台显示相关信息

        // 得到回话对象
        Session session = Session.getInstance(properties);
        // 获取邮件对象
        Message message = new MimeMessage(session);
        // 设置发件人邮箱地址
        message.setFrom(new InternetAddress("*********@qq.com")); //你的QQ邮箱
        // 设置收件人地址
        InternetAddress internetAddress = new InternetAddress(email);
        InternetAddress[] internetAddresses = {internetAddress};
        message.setRecipients(MimeMessage.RecipientType.TO, internetAddresses);
        // 设置邮件标题
        message.setSubject("邮论注册");
        // 设置邮件内容
        String authCode = achieveCode();
//        model.addAttribute("authCode",authCode);
        request.getSession().setAttribute("authCode",authCode);
        request.getSession().setAttribute("authCode",authCode);
//        ServletActionContext.getRequest().getSession().setAttribute("authCode",authCode);
        message.setText("如果不是本人操作请忽略" + "\n" + "注册验证码为:" + authCode;
        // 得到邮差对象
        Transport transport = session.getTransport();
        // 连接自己的邮箱账户
        transport.connect("smtp.qq.com","**********@qq.com", "密码");// 密码为刚才得到的授权码
        // 发送邮件
        try {
            transport.sendMessage(message, message.getAllRecipients());

        }catch(Exception e){
            return "registerdefeat";
        }
        return "verifymailbox";
    }

    // 随机验证码
    public String achieveCode() {  //由于数字 1 、 0 和字母 O 、l 有时分不清楚,所以,没有数字 1 、 0
        String[] beforeShuffle= new String[] { "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F",
                "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "a",
                "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v",
                "w", "x", "y", "z" };
        List list = Arrays.asList(beforeShuffle);//将数组转换为集合
        Collections.shuffle(list);  //打乱集合顺序
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < list.size(); i++) {
            sb.append(list.get(i)); //将集合转化为字符串
        }
        return sb.toString().substring(3, 8);  //截取字符串第4到8
    }
}

具体的每一步都这是在了上面,然后会跳转到输入验证码的jsp界面,当填写验证码并点击确定之后,发送验证码到“/homePage”的controller,如果验证码正确,就会注册成功,并跳转到主页。

小结: 到此为止基本的登录注册模块逻辑已经清晰了,具体的功能实现可能是会需要大量的时间来测试(对于像我一样的新手),当把登录注册模块完成之后剩下的发帖回帖评功能的实现其实和这个很相似。

四、后台模板的选择
综述: 就我个人而言,前端知识可以说基本不会,这个时候怎么办呢?想要做出一个唯美的界面,又不会,肯定是很难受的,但是,不要慌,直接百度“后台模板”上网找轮子,然后根据index.html实例来改编自己界面,这样既节省了时间,又能编写出还可以的界面,岂不妙哉!

我这里使用的轮子在这里:链接:https://pan.baidu.com/s/1QDc0Z6HU2dhoWvgl9v0eOA
提取码:w1mw
效果就是效果图里的那样,当然你也可以选择其他的,自己用着舒服就可以。

五、CKEditor编辑器的引入
综述: 这一步其实也很关键,是论坛能否运行的命脉(数据库已经创建完成),当然你也可以不引入CKEditor,可以引入其他的编辑器,或者自己手动编写一个简易的文本框。第一次弄着实有点麻烦,其实弄好之后感觉并不复杂,CKEditor压缩包可以去官网上下载,然后整个复制到webapp目录下即可。
1、 工具栏的选择,根据个人需求选择工具栏,我就选了其中几个,至于每个单词是什么意思可以参考:CKEditor自定义工具栏
我的工具栏配置如下,Source最好是加上,因为这可以方便查看编辑器中的源代码,以此发送到后台进行处理并存入数据库。

<form action="/releaseChats" method="post">
    <center>
        <textarea id="contect_text" cols="20" rows="2" class="ckeditor" name="mycomment" style="resize: none" onkeyup="wordStatic(this)" ></textarea>
        <%--<div class="introduction_counter"><span id="num">0</span>/50</div>--%>
        <br>
        <br>
        <input type="submit" name="" id="" value="发布" style="font-size: 22px">
        <br>
        <h3>不要超过300</h3>
    </center>
</form>

<script type="text/javascript">
    var maxlength = 300;
    // CKEDITOR.replace('contect_text',
    //     { toolbar:'Basic', height:400,width:1000 });
    //示例2:工具栏为自定义类型
    CKEDITOR.replace('contect_text',
        {
            filebrowserImageUploadUrl:'/uploadPic',
            language : 'zh-cn',
            width:1000,
            height:400,
            toolbar :
                [
                    //加粗     斜体
                    ['Source','Undo','Redo'],
                    //超链接  取消超链接
                    ['Link','Unlink'],
                    //图片     表情      特殊字符   查找
                    ['Image','Smiley','SpecialChar','Find '],
                    '/',
                        // 字体    字体大小
                    ['Font','FontSize'],
                    //文本颜色     背景颜色
                    ['TextColor','BGColor','About'],
                ]
        }
    );
</script>

2、 设置完工具栏很多功能已经可以实现了,但是上传图片、添加表情等还是不能使用,这一步来介绍添加emoji表情,首先下载emoji表情,可以自行百度下载,如果嫌麻烦,可以使用这里的,有将近400个表情:链接:https://pan.baidu.com/s/1l6yqNfgD64BuC0TXkE-Qvg
提取码:d4bg
下载之后把图片,注意:不要把目录,直接进入到目录下复制图片到ckeditor/plugins/smiley/images目录下,然后就可以使用了。

3、 上传图片,这一步坑也很多,特别是图片回显(预览)的功能,后台使用SpringMVC来处理,但是有一个很难受的问题就是,当Tomcat重启时上传的图片就会消失,这里呢我采用的是通过IDEA进行虚拟映射的方式来处理,上传到服务器时只是为了进行回显,而真正保存图片的是本地磁盘,通过虚拟目录的方式,这样在重启Tomcat时就不会再消失了,IDEA配置虚拟目录参考:IDEA配置虚拟目录
具体的后台上传图片代码如下

package com.controller;

import org.apache.commons.io.FileUtils;
import org.json.JSONObject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import org.springframework.web.multipart.commons.CommonsMultipartResolver;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.*;

/**
 * com.controller
 * Created by ForMe
 * 2019/4/10
 * 19:35
 * CKEditor上传图片所用
 */
@Controller
public class UploadPicController {

    @RequestMapping("/uploadPic")
    public void imageUpload(HttpServletRequest request, HttpServletResponse response) {
        String DirectoryName = "upload/";
        try {
            ImageUploadUtil.ckeditor(request, response, DirectoryName);
        } catch (IllegalStateException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

//    @RequestMapping("/uploadHeadImage")  //处理头像
//    public void uploadHeadImage(HttpServletRequest request,HttpServletResponse response,@RequestParam(value = "avatar_file") MultipartFile file) {
//        String uploadheadimage = "headimage/";
//        try {
//            ImageUploadUtil.ckeditor(request, response, uploadheadimage);
//        } catch (IllegalStateException e) {
//            e.printStackTrace();
//        } catch (IOException e) {
//            e.printStackTrace();
//        }
//    }
}



class ImageUploadUtil {  //上传图片的工具类

    // 图片类型
    private static List<String> fileTypes = new ArrayList<>();
    static {
        fileTypes.add(".jpg");
        fileTypes.add(".jpeg");
        fileTypes.add(".bmp");
        fileTypes.add(".gif");
        fileTypes.add(".png");
    }
    /**
     * 图片上传
     *
     * @param request
     * @param DirectoryName 文件上传目录:比如upload(无需带前面的/) upload/news ..
     * @return
     * @throws IllegalStateException
     * @throws IOException
     * @Title upload
     */
    public static String upload(HttpServletRequest request, String DirectoryName) throws IllegalStateException,
            IOException {
        // 创建一个通用的多部分解析器
        CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver(request.getSession()
                .getServletContext());
        // 图片名称
        String fileName = null;
        // 判断 request 是否有文件上传,即多部分请求
        if (multipartResolver.isMultipart(request)) {
            // 转换成多部分request
            MultipartHttpServletRequest multiRequest = (MultipartHttpServletRequest) request;
            // 取得request中的所有文件名
            Iterator<String> iter = multiRequest.getFileNames();
            while (iter.hasNext()) {
                // 记录上传过程起始时的时间,用来计算上传时间
                // int pre = (int) System.currentTimeMillis();
                // 取得上传文件
                MultipartFile file = multiRequest.getFile(iter.next());
                InputStream in = file.getInputStream();
                if (file != null) {
                    // 取得当前上传文件的文件名称
                    String myFileName = file.getOriginalFilename();
                    // 如果名称不为“”,说明该文件存在,否则说明该文件不存在
                    if (myFileName.trim() != "") {
                        // 获得图片的原始名称
                        String originalFilename = file.getOriginalFilename();
                        // 获得图片后缀名称,如果后缀不为图片格式,则不上传
                        String suffix = originalFilename.substring(originalFilename.lastIndexOf(".")).toLowerCase();
                        if (!fileTypes.contains(suffix)) {
                            continue;
                        }
                        // 获得上传路径的绝对路径地址(/upload)-->
                        String realPath = request.getSession().getServletContext().getRealPath("/" + DirectoryName);
                        System.out.println(realPath);
                        // 如果路径不存在,则创建该路径
                        File realPathDirectory = new File(realPath);
                        if (realPathDirectory == null || !realPathDirectory.exists()) {
                            realPathDirectory.mkdirs();
                        }
                        // 重命名上传后的文件名 111112323.jpg
                        fileName = UUID.randomUUID().toString().replaceAll("-", "") + suffix;
                        String realPath2 = "F:/upload";
                        OutputStream os = new FileOutputStream(new File(realPath2+"\\"+fileName));

//                        System.out.println(in == null);
                        int b = 0;
                        byte[] bytes = new byte[1024];
                        while((b = in.read(bytes))!= -1){ //读取文件,存到本地磁盘,并通过虚拟目录映射
                            os.write(bytes,0,b);
//                            System.out.println(new String(bytes,0,b));
                        }
                        os.flush(); //关闭流
                        in.close();
                        os.close();
                        // 定义上传路径 .../upload/111112323.jpg
                        File uploadFile = new File(realPathDirectory + "\\" + fileName);
                        System.out.println(uploadFile);
                        file.transferTo(uploadFile);
                    }
                }
            }
        }
        return fileName;
    }

    /**
     * ckeditor文件上传功能,回调,传回图片路径,实现预览效果。
     *
     * @param request
     * @param response
     * @param DirectoryName 文件上传目录:比如upload(无需带前面的/) upload/..
     * @throws IOException
     * @Title ckeditor
     */
    public static void ckeditor(HttpServletRequest request, HttpServletResponse response, String DirectoryName)
            throws IOException {
        String fileName = upload(request, DirectoryName);
        // 结合ckeditor功能
        // imageContextPath为图片在服务器地址,如upload/123.jpg,非绝对路径
        String imageContextPath = DirectoryName + fileName;
        System.out.println(imageContextPath);
        response.setContentType("text/html;charset=UTF-8");
        String callback = request.getParameter("CKEditorFuncNum");
        PrintWriter out = response.getWriter();
        JSONObject jsonObject = new JSONObject(); //一定要返回一个JSON
        jsonObject.put("uploaded",1);
        jsonObject.put("fileName",fileName);
        jsonObject.put("url",imageContextPath);
        out.write(jsonObject.toString());
        out.flush();
        out.close();
    }

}

具体上传图片如何处理可根据自己的实际情况改动。
4、 点击发布按钮进行发布。事实上,数据库中保存的就是编辑器中的源码(Source),如下图
在这里插入图片描述
六、展示所有的帖子和自己发布的所有帖子
进行到这一步,可以说已经完成一半了,展示所有帖子的功能,其实很简单,就是查询数据库,并把查询结果放在List中,然后再在jsp页面中通过<c:forEach>循环调用就可以了,关于后台的List怎样发送到前台,可以在后台使用ModelAndView进行封装。展示所有的帖子和自己曾经发布过的原理是一样的,只不过访问数据库时方式不一致。不再重复。
七、删除自己的评论和帖子
原理和第六步基本是一致的,只是使用的是delete语句,然后就是一些简单的跳转操作。

八、个人信息
个人信息的处理我这里是用的是JQuery 的AJAX,异步请求,先看源代码

$(document).ready(function(){
            $("#btn").click(function(){
                $.ajax({
                    //提交数据的类型 POST GET
                    type:"POST",
                    //提交的网址
                    url:"aboutme",
                    //提交的数据
                    // data:{username:$("#username").val(),password:$("#password").val()},
                    //返回数据的格式
                    datatype: "json",
                    //成功返回之后调用的函数
                    success:function (data){
                        $("#username").val(data.username);
                        $("#password").val(data.password);
                        $("#sex").val(data.sex);
                        $("#qqnum").val(data.QQ);
                        $("#email").val(data.email);
                        $("#selfIntroduction").val(data.selfIntroduction);
                        toggle("orignmessage");
                        // document.getElementById("aboutmemessage").innerText = data.data;
                    },
                });
            });
        });

当点击“个人信息”的时候就会异步访问,aboutme的Controller,并返回JSON,之后再将数据回显到提前设计好的隐藏表单中,并将表单显示出来,到此为止此项目就完成了,搞定。

总结: 本项目其实并不是很难,只是对于刚一接触SSM框架的人来说可能会有点困难,但真正当自己做出来之后就会发现也不过如此。所以熟能生巧。

;