SSM+bootstrap4+mysql+redis原创项目实战:搭建校园二手交易网

项目已部署到阿里云:hbxytrade.top

本文更新时间:2020/3/7,更新内容:redis缓存中间件(涉及多条件查询)物品管理界面(更新、上架、下架等操作)阿里云部署项目

代码地址:

https://github.com/JiaVaHao/schooltradewebsite_ssm 这是ssm版的整合了shiro作为权限控制
https://gitee.com/jiang_wen_hao/hbxytrade_springboot 这是springboot版的整合了springsecurity作为权限控制

目前进度:

主页面(点我跳转相关实现):

主页

发布物品页面:涉及图片上传、前后端校验(点我跳转相关实现):

出售物品

浏览物品页面:涉及多条件分页查询、一二级菜单(点我跳转相关实现):

浏览物品

物品详情页面:涉及owl-carousel轮播插件、物品id传递(点我跳转相关实现):

物品详情

管理物品页面:涉及物品显示与隐藏、物品更新操作(点我跳转相关实现):

在这里插入图片描述
我也不知道目前做了几天了,总时长应该200小时左右,做的不是很好,大佬轻喷,不要吐槽我的图包里只有妹子。

接下来我会写下我的网页所用到的知识点,以及部分代码,欢迎各位提建议、吸取经验。

一、制作原因

这个项目是为了参加学校比赛做的,不是什么毕设,目前大三,考虑到时间充裕,就决定做个大项目—— 一个基于校园网的二手交易平台,只给学生用,不收中介费,于是这个项目就这样诞生了。

二、环境搭建

因为不是毕设,所以什么概要设计详细设计都没有,直接开始动手做项目。
先配置ssm框架基本的东西,为了省篇幅直接另起一篇项目配置的,需要的自取:https://blog.csdn.net/qq_41468822/article/details/104086957
目前项目所用到的知识点:
后端:mybatis逆向工程,restful,JSR303,文件上传,spring-redis-data,JsonObject(阿里巴巴的json转换工具)
前端:bootstrap4,font-awesome,jquery,ajax与json,javascript(部分),还有需要自己写html5、css3样式
关系型数据库:mysql
菲关系型数据库:redis
发布云服务器需要linux基础
目前项目整体结构:
在这里插入图片描述
接下来就直接讲第一个页面了,不过在这之前推荐大家先学习下bootstrap和逆向工程,我是在这里学的:https://www.bilibili.com/video/av35988777?from=search&seid=12284740285221624741
这个老师讲的很nice。

三、动手实践

先在网上选好模板,毕竟是做后端的,前端的很多东西需要参考,网站的主打色是原谅色

1. 主页面

因为是通过教务系统登录,就直接从application域中取数据就行,表单中的xxx同学就是这样。
直接在index页面用<% application.setAttribute();%>就行。
相同的html先提取到 jsp 中再用<%@ include file="/WEB-INF/xxx.jsp" %>引入
在这里插入图片描述


中间部分省略,因为只是前端的东西,没啥好讲的,不过下面这部分使用了jquery.countTo.js,它可以呈现数据从0开始动态上涨的效果,具体可以到我的网站查看,每次刷新页面都会呈现。
在这里插入图片描述
主页面目前还是静态的,等以后做完再展示新效果。


2. 发布物品页面,主要功能:添加信息到数据库,添加图片文件到磁盘

这部分比较难搞的是上传图片并回显到页面上,这段代码我是网上找到的自己修改了下,原文链接:https://blog.csdn.net/weixin_42211816/article/details/88365440
在这里插入图片描述


然后是选中图片效果,也是网上找的修改下 https://blog.csdn.net/QAEARQ/article/details/76258729
在这里插入图片描述
选中的图片作为物品的封面,所以只能选中一张,加个var变量0为已有图片被选中,1为无图片被选中,不过还要考虑到选中图片后,清空了或者删除了选中的图片,var的值也要赋为0。图片的取消选中、清空等操作,用 jqyery 的 removeClass 和 empty 就能实现。
在这里插入图片描述
具体代码实现:

	var count = 0, length = 0; //count是总图片数量,length是多图片上传时图片的个数
    document.getElementById("add-pic-btn").addEventListener("change", function () {
        var obj = this,
            arrTitle = []; //存标题名
        length = obj.files.length;
        count += length;
        if (count <= 10) {
            _html = '';
            for (var i = 0; i < length; i++) {
                var reader = new FileReader();
                if (!reader) {
                    console.log('对不起,您的浏览器不支持!请尝试更换更高版本的浏览器');
                    return
                }
                //存储上传的多张图片名字
                arrTitle.push(obj.files[i].name);
                reader.error = function () {
                    console.log("读取异常")
                }
                //iffi语法
                ;(function (i) {
                    //读取成功
                    reader.onload = function (e) {
                        //console.log(e)
                        var _src = e.target.result;
                        //节点
                        var divItem = document.createElement('div');
                        divItem.setAttribute('class', 'item');
                        var divPic = document.createElement('div');
                        divPic.setAttribute('class', 'pic-box');
                        var img = document.createElement('img');
                        img.setAttribute('class', 'img');
                        img.setAttribute('src', _src);
                        var divTk = document.createElement('div');
                        divTk.setAttribute('class', 'tk');
                        var spanDel = document.createElement('span');
                        spanDel.setAttribute('class', 'del');
                        spanDel.setAttribute('title', arrTitle[i]);
                        spanDel.innerText = 'x';
                        divTk.innerHTML = arrTitle[i];
                        divItem.appendChild(divPic);
                        divPic.appendChild(img);
                        divItem.appendChild(divTk);
                        divItem.appendChild(spanDel);
                        //增加节点结构
                        var groupPic = document.getElementById('group_pic');
                        groupPic.insertBefore(divItem, groupPic.firstChild);
                        //删除节点方法
                        spanDel.onclick = function () {
                            removeItem(this);
                            return false;
                        };
                    };
                })(i);
                reader.readAsDataURL(obj.files[i]);
            }
        } else {
            alert("添加的图片不能超过10张");
            count -= length;
        }
        length = 0;
    });

    //删除图片
    function removeItem(delNode) {
        var itemNode = delNode.parentNode,
            title = delNode.getAttribute('title');
        var flag_confirm = confirm("确认要删除名为:" + title + "的图片?");
        if (flag_confirm) {
            if (itemNode.className == 'item box') {
                flag = 0;
            }
            itemNode.parentNode.removeChild(itemNode);
            count--;
        }
        return false;
    }

    var selected_file;
    //图片选中效果
    var flag = 0; //flag就是上文中的0,1标志
    $(document).on("click", "#group_pic img", function () {
        if ($(this).parent("div").parent("div").hasClass("box") && flag == 1) {
            $(this).parent("div").parent("div").removeClass("box");
            selected_file = null;
            flag--;
        } else {
            if (flag == 0) {
                $(this).parent("div").parent("div").addClass("box");
                var files = document.getElementById("add-pic-btn").files;
                selected_file = files.length - $(this).parent("div").parent("div").index()-1;
                flag++;
            } else {
                alert("只能选择一张图片作为封面,亲")
            }
        }
    });

    //清空图片
    $('#clear-pic-btn').click(function () {
        if (confirm("确定要清空所有图片吗?")) {
            $('#group_pic').children("*").remove();
            flag = 0;
            count = 0;
        }
    });

图片选中取消的css样式:

.group-coms-pic {
    padding-top: 30px;
    overflow: hidden;
}

.group-coms-pic .item {
    width: 119px;
    height: 148px;
    border: 1px solid #f0f0f0;
    position: relative;
    float: left;
    margin-right: 20px;
    margin-bottom: 20px;
}

.group-coms-pic .pic-box {
    width: 118px;
    height: 117px;
    border-bottom: 1px solid #f0f0f0;
    overflow: hidden;
    position: relative;
}

.group-coms-pic .pic-box .img {
    height: 117px;
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
}

.group-coms-pic .tk {
    padding: 0 9px;
    height: 32px;
    line-height: 32px;
    text-align: left;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
    color: #353535;
    font-size: 14px;
}

.group-coms-pic .item .del {
    width: 20px;
    height: 20px;
    line-height: 20px;
    text-align: center;
    border-radius: 50%;
    color: #fff;
    background-color: #ff6982;
    cursor: pointer;
    font-style: normal;
    position: absolute;
    right: -15px;
    top: -15px;
}

.box div{
    position: relative;
    width: 118px;
    height: 117px;
    overflow: hidden;
    background-color: #67b168;
    border: 3px solid #4cae4c;
}

.box:before {
    position: absolute;
    display: block;
    border-top: 30px solid #4cae4c;
    border-left: 30px solid transparent;
    right: 0;
    top: 0;
    content: "";
    z-index: 101;
}

.box:after {
    position: absolute;
    display: block;
    content: "\f00c";
    font-family: FontAwesome;
    top: 0;
    right: 0;
    font-size: 10pt;
    font-weight: normal;
    z-index: 102;
    color: #fff;
}

中间是个form表单,除了单选和文本域之外,每个元素都需要被前后端校验,前端用jquery的正则表达式,后端用JSR303的hibernate实现
在这里插入图片描述
前端:

	function validate_add_form() {
        //拿到要校验的数据
        var title = $("#title").val();
        var regTitle = /^[\u2E80-\u9FFFa-zA-Z0-9_-]{1,20}$/;
        if (!regTitle.test(title)) {
            show_validate_msg("#title", "error", "请输入1~20个汉字、数字或英文");
            return false;
        } else {
            show_validate_msg("#title", "success", "");
        }
        var price = $("#price").val();
        var regPrice = /^(([0-9]{0,5})\.([0-9]{1,2}))$|^([1-9][0-9]{0,4})$/;
        if (!regPrice.test(price)) {
            show_validate_msg("#price", "error", "价格必须是0.1~99999之间");
            return false;
        } else {
            show_validate_msg("#price", "success", "");
        }
        var qq = $("#qq").val();
        var regQQ = /^[1-9]*[1-9][0-9]*$/;
        if (!regQQ.test(qq)) {
            show_validate_msg("#qq", "error", "请输入正确的QQ号");
            return false;
        } else {
            show_validate_msg("#qq", "success", "");
        }
        var describe = $("#describe").val();
        var regDescribe = /^[\u2E80-\u9FFFa-zA-Z0-9_-]{0,200}$/;
        if (!regDescribe.test(describe)) {
            show_validate_msg("#describe", "error", "字数不能超过200字");
            return false;
        } else {
            show_validate_msg("#describe", "success", "");
        }
        if (flag == 0) {
            show_validate_msg("#group_pic", "error", "*请选择一张图片作为物品的封面");
            return false;
        } else {
            show_validate_msg("#group_pic", "success", "");
        }
        var size = 0;
        for(var i=0;i<files.length;i++){
            if(files[i].size > 3*1024*1024){
                show_validate_msg("#group_pic", "error", "*单个图片大小不能超过3MB");
                return false;
            } else {
                show_validate_msg("#group_pic", "success", "");
                size += files[i].size;
            }
        }
        return true;
    }

后端:

	@Pattern(regexp = "^[\u2E80-\u9FFFa-zA-Z0-9_-]{1,20}$"
            ,message = "请输入1~20个汉字、数字或英文")
    private String goodsTitle;
    
    @DecimalMin(value = "0.1",message = "价格必须是0.1~99999之间")
    @DecimalMax(value = "99999.99",message = "价格必须是0.1~99999之间")
    private BigDecimal goodsPrice;

    @Pattern(regexp = "^[1-9]*[1-9][0-9]*$",message = "请输入正确的QQ号")
    private String masterQq;

    @Pattern(regexp = "^[\u2E80-\u9FFFa-zA-Z0-9_-]{0,200}$",message = "字数不能超过200字")
    private String goodsDescribe;

    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;

上传图片方式是向数据库中存的是名字的字符串,查询的时候直接与项目路径拼接就行了,以下是我自己写的文件上传删除工具

public class FileHandlerUtil {
    public static Boolean uploadFiles(HttpServletRequest request, Goods goods, Integer num,
                                      MultipartFile[] groupImg, Map<String, Object> map) {
        StringBuilder stringBuilder = new StringBuilder();
        //上传文件
        String path = request.getServletContext().getRealPath("/") + "assets\\img\\upload_pic_package\\";
        try {
            String uid, uidSub;
            int count = 0;
            for (MultipartFile multipartFile : groupImg) {
                if (multipartFile.getSize() > 3 * 1024 * 1024) {
                    map.put("imgError", "*单个图片大小不能超过3MB");
                    return false;
                }
                uid = UUID.randomUUID().toString();
                uidSub = uid.substring(0, 5);
                String fileName = uidSub + ".jpg";
                File targetFile = new File(path + fileName);
                stringBuilder.append(uidSub).append(',');
                multipartFile.transferTo(targetFile);
                if (count == num)
                    goods.setShowImg(uidSub);
                count++;
            }
            System.out.println(stringBuilder);
            goods.setGroupImg(stringBuilder.toString().substring(0, stringBuilder.length() - 1));
        } catch (Exception e) {
            e.printStackTrace();
        }
        return true;
    }

    public static Boolean deleteFiles(HttpServletRequest request, Integer id, GoodsService goodsService) {
        //删除文件
        String path = request.getServletContext().getRealPath("/") + "assets\\img\\upload_pic_package\\";
        Goods goods = goodsService.getGoodsById(id);
        List<String> list = Arrays.asList(goods.getGroupImg().split(","));
        try {
            for (String fileName : list) {
                File targetFile = new File(path + fileName + ".jpg");
                if(!targetFile.delete()){
                    return false;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return true;
    }
}

ajax提交的代码也放上来了,上传图片的话需要用FormData()这个函数,然后springmvc-servlet还需要配置文件上传解析器,直接去上面我的配置中复制就行了。

	$("#add-goods-btn").click(function () {
        if (!validate_add_form())
            return false;
        var formData = new FormData($("#add_goods_form")[0]);
        formData.append('show_pic', selected_file);  //添加图片信息的参数
        $.ajax({
            url: "${ctp }/addgoods",
            type: "POST",
            //data: decodeURIComponent($("#add_goods_form").serialize()),
            data: formData,
            contextType: "application/json;charset=utf-8",
            dataType: "json",
            cache: false, //上传文件不需要缓存
            processData: false, //序列化
            contentType: false, //内容类型为空
            success: function (result) {
                if (result.code == 1) {
                    alert("商品提交成功");
                    document.getElementById("add_goods_form").reset();
                    window.location.reload(); //刷新页面
                } else {
                    //哪个字段有错就显示哪个字段
                    if (undefined != result.extend.errorFields.goodsTitle)
                        show_validate_msg("#title", "error", result.extend.errorFields.goodsTitle);
                    if (undefined != result.extend.errorFields.goodsPrice)
                        show_validate_msg("#price", "error", result.extend.errorFields.goodsPrice);
                    if (undefined != result.extend.errorFields.masterQq)
                        show_validate_msg("#qq", "error", result.extend.errorFields.masterQq);
                    if (undefined != result.extend.errorFields.goodsDescribe)
                        show_validate_msg("#describe", "error", result.extend.errorFields.goodsDescribe);
                    if (undefined != result.extend.errorFields.imgError)
                        show_validate_msg("#group_pic", "error", result.extend.errorFields.imgError);
                }
            }
        });
        return true;
    });

最后发一下数据库中的表吧,目前就三张表(两张是物品分类的表),考虑到需要频繁查询,部分的地方就不用第三范式了
在这里插入图片描述
在这里插入图片描述
最后说一下两个分类的下拉框是发ajax请求动态拼接的,我发的视频教程里有。


3. 浏览物品页面

在这里插入图片描述
在这里插入图片描述
这一部分代码我是照着教程悟出来的,最上面的是一个导航栏,bootstrap没有自带的导航栏,所以是我自己写的,附上代码

				<div class="category-header no-margin-bottom">
                        <div class="row">
                            <div class="collapse navbar-collapse filter">
                                <label class="col-sm-2 control-label">一级分类:</label>
                                <div class="col-sm-10">
                                    <ul class="nav navbar-nav" id="first_level_ul">
                                    </ul>
                                </div>
                            </div>
                            <div class="collapse navbar-collapse filter">
                                <label class="col-sm-2 control-label">二级分类:</label>
                                <div class="col-sm-10">
                                    <ul class="nav navbar-nav" id="second_level_ul">
                                    </ul>
                                </div>
                            </div>
                            <div class="collapse navbar-collapse filter">
                                <label class="col-sm-2 control-label">新旧程度</label>
                                <div class="col-sm-10">
                                    <ul class="nav navbar-nav" id="goods_degree">
                                        <li value="0"><a class="selected">全部</a></li>
                                        <li value="1"><a href="#">九五新</a></li>
                                        <li value="2"><a href="#">九成新</a></li>
                                        <li value="3"><a href="#">八成新</a></li>
                                        <li value="4"><a href="#">其他</a></li>
                                    </ul>
                                </div>
                            </div>
                            <div class="collapse navbar-collapse filter">
                                <label class="col-sm-2 control-label">物主性别:</label>
                                <div class="col-sm-10">
                                    <ul class="nav navbar-nav" id="master_gender">
                                        <li value="0"><a class="selected">全部</a></li>
                                        <li value="2"><a href="#"></a></li>
                                        <li value="3"><a href="#"></a></li>
                                    </ul>
                                </div>
                            </div>
                            <div class="collapse navbar-collapse filter">
                                <label class="col-sm-2 control-label">交易方式:</label>
                                <div class="col-sm-10">
                                    <ul class="nav navbar-nav" id="trade_type">
                                        <li value="0"><a class="selected">全部</a></li>
                                        <li value="1"><a href="#">一口价</a></li>
                                        <li value="2"><a href="#">可砍价</a></li>
                                        <li value="3"><a href="#">拍卖</a></li>
                                    </ul>
                                </div>
                            </div>
                            <div class="collapse navbar-collapse filter">
                                <label class="col-sm-2 control-label">价格:</label>
                                <div class="col-sm-10">
                                    <ul class="nav navbar-nav" id="price_between">
                                        <li value="0"><a class="selected">全部</a></li>
                                        <li value="1"><a href="#">0.110</a></li>
                                        <li value="2"><a href="#">10100</a></li>
                                        <li value="3"><a href="#">100500</a></li>
                                        <li value="4"><a href="#">5001000</a></li>
                                        <li value="5"><a href="#">10005000</a></li>
                                        <li value="6"><a href="#">500010000</a></li>
                                        <li value="7"><a href="#">1000099999</a></li>
                                    </ul>
                                </div>
                            </div>
                        </div>
                    </div>

css样式:

.filter{
    padding-top: 5px;
}
.filter li{
    padding-top: 5px;
    padding-left: 5px;
}
.selected{
    background-color: #58b325;
    color: white;
}

我的逻辑是这样的:一二级分类需要动态拼接,但是一开始进入页面一级分类选中的是全部(值为0,数据库没有0的值,所以查全部),所以不能拼接二级分类,而选中了带值的一级分类之后才开始拼接二级分类,当鼠标点击选中了任何一个筛选项后都需要发ajax请求查询符合条件的物品,而当点击分页条的时候不需要重新发请求,只是查询剩余的物品,解决办法就是在ajax请求中携带筛选项的值,例如:
data: {“pn” : pn, “firstLevelId” : firstLevelId, “secondLevelId” : secondLevelId,
“goodsDegree” : goodsDegree, “masterGender” : masterGender,
“tradeType” : tradeType},

然后通过后端的自动绑定javabean,再通过mybatis的条件查询达到筛选效果
直接上代码,有兴趣自行研究

$(function () {
        $('#first_level_ul').empty();
        $('#first_level_ul').append('<li value="0"><a class="selected">全部</a></li>');
        $.ajax({
            url: "${ctp }/firstlevels",
            type: "GET",
            contextType: "application/json;charset=utf-8",
            dataType: "json",
            success: function (result) {
                $.each(result.extend.firstlevels, function () {
                    var aEle = $("<a href='#'></a>").append(this.firstLevelName);
                    var liEle = $('<li></li>').append(aEle).attr('value', this.firstLevelId);
                    liEle.appendTo('#first_level_ul');
                });
                getSecondLevel();
            }
        });
        to_page(1);
    });

    function getSecondLevel() {
        $('#second_level_ul').empty();
        $('#second_level_ul').append('<li value="0"><a class="selected">全部</a></li>');
        var firstLevelId = $('#first_level_ul a[class=\'selected\']').parent("li").attr('value');
        $.ajax({
            url: "${ctp }/secondlevels/" + firstLevelId,
            type: "GET",
            contextType: "application/json;charset=utf-8",
            dataType: "json",
            success: function (result) {
                $.each(result.extend.secondlevels, function () {
                    var aEle = $('<a href="#"></a>').append(this.secondLevelName);
                    var liEle = $('<li></li>').append(aEle).attr('value', this.secondLevelId);
                    liEle.appendTo('#second_level_ul');
                })
            }
        })
    }

    $(document).on("click", "#first_level_ul", function () {
        getSecondLevel();
    });

    //为filter下的所有a标签添加单击事件
    $(document).on("click", "#filter div[class=\"collapse navbar-collapse filter\"] a", function () {
        $(this).parents("ul").children("li").each(function () {
            $(this).find("a").removeClass("selected");
        });
        $(this).addClass("selected");
        //alert(ReSelected()); //返回选中结果
        to_page(1)
    });

    function ReSelected() {
        var result = "";
        $("#filter a[class='selected']").each(function () {
            result += $(this).html() + "\n";
        });
        return result;
    }

    function to_page(pn) {
        var firstLevelId = $('#first_level_ul a[class=\'selected\']').parent("li").attr('value');
        var secondLevelId = $('#second_level_ul a[class=\'selected\']').parent("li").attr('value');
        var goodsDegree = $('#goods_degree a[class=\'selected\']').parent("li").attr('value');
        var masterGender = $('#master_gender a[class=\'selected\']').parent("li").attr('value');
        var tradeType = $('#trade_type a[class=\'selected\']').parent("li").attr('value');
        $.ajax({
            url: "${ctp }/querygoods",
            type: "GET",
            data: {"pn" : pn, "firstLevelId" : firstLevelId, "secondLevelId" : secondLevelId,
                "goodsDegree" : goodsDegree, "masterGender" : masterGender,
                "tradeType" : tradeType},
            contextType: "application/json;charset=utf-8",
            dataType: "json",
            success: function (result) {
                //商品信息
                build_goods_table(result);
                //分页信息
                build_page_info(result);
                //分页数据
                bulid_page_nav(result);
            }
        });
    }
	@ResponseBody
    @RequestMapping("/querygoods")
    public Msg querygoods(@RequestParam(value = "pn", defaultValue = "1") Integer pn, Goods goods) {
        //分页查询,要查的对象紧跟pageHelper,每次显示10行
        //System.out.println(goods);
        PageHelper.startPage(pn, 10);
        List<Goods> list = goodsService.getAll(goods);
        //包装分页结果,每次显示五页
        PageInfo<Goods> pageInfo = new PageInfo<>(list, 10);
        return Msg.success().add("pageInfo", pageInfo);
    }
	public List<Goods> getAll(Goods goods) {
        GoodsExample example = new GoodsExample();
        example.setOrderByClause("goods_id");
        GoodsExample.Criteria criteria = example.createCriteria();
        if(goods.getFirstLevelId()!=0){
            criteria.andFirstLevelIdEqualTo(goods.getFirstLevelId());
            if(goods.getSecondLevelId()!=0 && goods.getSecondLevelId()!=null){
                criteria.andSecondLevelIdEqualTo(goods.getSecondLevelId());
            }//俄罗斯套娃般的if语句,以后会改善的
        }
        if(goods.getGoodsDegree()!=0){
            criteria.andGoodsDegreeEqualTo(goods.getGoodsDegree());
        }
        if(goods.getMasterGender()!=0){
            criteria.andMasterGenderEqualTo(goods.getMasterGender());
        }
        if(goods.getTradeType()!=0){
            criteria.andTradeTypeEqualTo(goods.getTradeType());
        }
        return goodsMapper.selectByExampleWithLevelName(example);
    }

接下放一段计算物品发布时间的算法,自己写的

			var differ;
            var nowDate = new Date();
            var year = nowDate.getFullYear();
            var month = nowDate.getMonth() + 1 < 10 ? "0" + (nowDate.getMonth() + 1) : nowDate.getMonth() + 1;
            var date = nowDate.getDate() < 10 ? "0" + nowDate.getDate() : nowDate.getDate();
            var hour = nowDate.getHours()< 10 ? "0" + nowDate.getHours() : nowDate.getHours();
            var minute = nowDate.getMinutes()< 10 ? "0" + nowDate.getMinutes() : nowDate.getMinutes();
            if(year-item.createTime.year>0){
                differ="&nbsp;"+(year-item.createTime.year)+"年前";
            }else if(year-item.createTime.year==0&&month-item.createTime.monthValue>0){
                differ="&nbsp;"+(month-item.createTime.monthValue)+"个月前";
            }else if(month-item.createTime.monthValue==0&&date-item.createTime.dayOfMonth>0){
                differ="&nbsp;"+(date-item.createTime.dayOfMonth)+"天前"
            }else if(date-item.createTime.dayOfMonth==0&&hour-item.createTime.hour>0){
                differ="&nbsp;"+(hour-item.createTime.hour)+"小时前"
            }else{
                differ="&nbsp;"+(minute-item.createTime.minute)+"分钟前";
            }

4.物品详情页面

如何点击物品标题后查询到的是该物品的信息?道理很简单,就是给a标签的链接带上参数,我做动态拼接的时候这样的:

var titleEle = $('<div class="item-title"></div>').append('<a href="detail?goodsId='+item.goodsId+'">'+'<h4>'+item.goodsTitle+'</h4>'+'</a>');

通过requestParam传参得到id值,再把id值赋给详情页面的控制的中的静态变量,再查询的话返回的就是单条记录了。

	@RequestMapping("/detail")
    public String detail(Integer goodsId){
        detailPageController.goodsId = goodsId;
        return "detail";
    }

先把id赋给静态变量,并来到详情页面

	static Integer goodsId = null;
	
    @ResponseBody
    @RequestMapping("/querygoodsbyid")
    public Msg querygoodsById() {
        //System.out.println(goodsId);
        Goods goods = goodsService.getGoodsById(goodsId);
        return Msg.success().add("goods", goods);
    }

再通过发ajax请求动态拼接数据就行了。
这里也可以通过request域传递id,两种方法都行。
在这里插入图片描述
在这里插入图片描述
这页还用到了owl-carousel,效果就是把轮播图片一样,前后翻页按钮可以在这上面找到
教程:https://www.cnblogs.com/linjiaxin/p/5960998.html
在这里插入图片描述
jquery.flexslider.js中这行代码可以改按钮下边的字


5.物品管理页面

该页面主要用于显示和隐藏用户发布的商品,相当于上架和下架,也可以更新商品和删除商品。
显示和隐藏的方式就是数据库多加一列属性,1为显示,0为隐藏,查询的时候匹配就行
在这里插入图片描述
点击修改会弹出模态框,通过更新按钮获取选中的物品id,从而给表格传递不同的值,代码和前面发布页面的类似,点击保存会调用文件工具类的方法,先把之前的图片删除,再存入新的图片。
删除按钮等不说了,看上面发的教程视频就能学会。
在这里插入图片描述


以下是java web高级知识:

四、redis作缓存中间件

我通过https://www.bilibili.com/video/av76235738?from=search&seid=2504220265010695682学习了redis的基础知识,这个讲的比较完整,也可以通过https://www.bilibili.com/video/av49517046?from=search&seid=2504220265010695682速学。

我的项目目前需要用redis作缓存来提高查询效率,经测试redis比MySQL查询耗时快四分之三
目前项目需要作缓存的就三个表,一级分类,二级分类以及商品表。
以下是spring整合redis的配置,放在applicationContext.xml中:

	<!--设置jedisPool链接池的配置-->
    <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
        <property name="maxTotal" value="${redis.maxTotal}"/>
        <property name="maxIdle" value="${redis.maxIdle}"/>
        <property name="maxWaitMillis" value="${redis.maxWaitMillis}"/>
        <property name="minEvictableIdleTimeMillis" value="${redis.minEvictableIdleTimeMillis}"/>
        <property name="numTestsPerEvictionRun" value="${redis.numTestsPerEvictionRun}"/>
        <property name="timeBetweenEvictionRunsMillis" value="${redis.timeBetweenEvictionRunsMillis}"/>
        <property name="testOnBorrow" value="${redis.testOnBorrow}"/>
        <property name="testWhileIdle" value="${redis.testWhileIdle}"/>
    </bean>
    <!--redis链接密码-->
    <bean id="redisPassword" class="org.springframework.data.redis.connection.RedisPassword">
        <constructor-arg name="thePassword" value="${redis.password}"/>
    </bean>
    <!--单一redis连接池-->
    <bean id="redisStandaloneConfiguration"
          class="org.springframework.data.redis.connection.RedisStandaloneConfiguration">
        <property name="hostName" value="${redis.host}"/>
        <property name="port" value="${redis.port}"/>
        <property name="password" ref="redisPassword"/>
        <property name="database" value="${redis.database}"/>
    </bean>
    <!--分片式集群连接池-->
    <bean id="redisClusterConfig" class="org.springframework.data.redis.connection.RedisClusterConfiguration">
        <property name="maxRedirects" value="3"/>
        <property name="password" ref="redisPassword"/>
        <property name="clusterNodes">
            <set>
                <bean class="org.springframework.data.redis.connection.RedisNode">
                    <constructor-arg name="host" value="${redis.host}"/>
                    <constructor-arg name="port" value="${redis.port1}"/>
                </bean>
                <bean class="org.springframework.data.redis.connection.RedisNode">
                    <constructor-arg name="host" value="${redis.host}"/>
                    <constructor-arg name="port" value="${redis.port2}"/>
                </bean>
                <bean class="org.springframework.data.redis.connection.RedisNode">
                    <constructor-arg name="host" value="${redis.host}"/>
                    <constructor-arg name="port" value="${redis.port3}"/>
                </bean>
                <bean class="org.springframework.data.redis.connection.RedisNode">
                    <constructor-arg name="host" value="${redis.host}"/>
                    <constructor-arg name="port" value="${redis.port4}"/>
                </bean>
                <bean class="org.springframework.data.redis.connection.RedisNode">
                    <constructor-arg name="host" value="${redis.host}"/>
                    <constructor-arg name="port" value="${redis.port5}"/>
                </bean>
                <bean class="org.springframework.data.redis.connection.RedisNode">
                    <constructor-arg name="host" value="${redis.host}"/>
                    <constructor-arg name="port" value="${redis.port6}"/>
                </bean>
            </set>
        </property>
    </bean>
    <!--配置jedis链接工厂-->
    <bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
        <!--<constructor-arg name="clusterConfig" ref="redisClusterConfig"/>
        <constructor-arg name="poolConfig" ref="jedisPoolConfig"/>-->
        <constructor-arg name="standaloneConfig" ref="redisStandaloneConfiguration"/>
    </bean>
    <!--配置jedis模板  -->
    <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
        <property name="connectionFactory" ref="jedisConnectionFactory"/>
        <!--序列化设置-->
        <property name="keySerializer">
            <bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
        </property>
        <property name="valueSerializer">
            <bean class="org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer"/>
        </property>
        <property name="hashKeySerializer">
            <bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
        </property>
        <property name="hashValueSerializer">
            <bean class="org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer"/>
        </property>
    </bean>

redis.properties

redis.password=xxx
redis.database=0
# 连接超时时间
redis.timeout=10000
#最大空闲数
redis.maxIdle=20
#控制一个pool可分配多少个jedis实例,用来替换上面的redis.maxActive,如果是jedis 2.4以后用该属性
redis.maxTotal=100
#最大建立连接等待时间。如果超过此时间将接到异常。设为-1表示无限制。
redis.maxWaitMillis=10000
#连接的最小空闲时间 默认1800000毫秒(30分钟)
redis.minEvictableIdleTimeMillis=300000
#每次释放连接的最大数目,默认3
redis.numTestsPerEvictionRun=1024
#逐出扫描的时间间隔(毫秒) 如果为负数,则不运行逐出线程, 默认-1
redis.timeBetweenEvictionRunsMillis=30000
#是否在从池中取出连接前进行检验,如果检验失败,则从池中去除连接并尝试取出另一个
redis.testOnBorrow=true
#在空闲时检查有效性, 默认false
redis.testWhileIdle=true

*重要:需要在pom.xml加入以下依赖,JSONObject是json格式转换的工具,用了它存入redis时javabean就不用实现序列化接口了,用JSONObject的好处就是jdk8中的LocalDataTime也能以json的格式完美存入redis中

		<dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <!--<version>2.10.2</version>-->
            <version>3.2.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-redis</artifactId>
            <!--<version>1.8.23.RELEASE</version>-->
            <version>2.2.4.RELEASE</version>
        </dependency>

        <!-- JSONObject依赖包 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.62</version>
        </dependency>
        <dependency>
            <groupId>commons-beanutils</groupId>
            <artifactId>commons-beanutils</artifactId>
            <version>1.9.2</version>
        </dependency>
        <dependency>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId>
            <version>3.2.1</version>
        </dependency>
        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
            <version>2.6</version>
        </dependency>
        <dependency>
            <groupId>net.sf.ezmorph</groupId>
            <artifactId>ezmorph</artifactId>
            <version>1.0.6</version>
        </dependency>
        <dependency>
            <groupId>net.sf.json-lib</groupId>
            <artifactId>json-lib</artifactId>
            <version>2.4</version>
            <classifier>jdk15</classifier>
        </dependency>

redis使用方式:判断是否有key,没有就从数据库查,有就从redis查
redis配置好了,但是会遇到一些问题,比如 List < Goods >我该用什么形式存入数据库中?
redis给我们提供了五种类型:string、hash、list、set、zset
javabean理所应当的应该存在hash中,但是存hash会遇到一些问题
代码:

		list = secondLevelMapper.selectByExample(example); //这里查出的数据按id排序
        for (SecondLevel each : list) {
            redisTemplate.opsForHash().put("second_level",
                    JSONObject.toJSONString(each), each.getSecondLevelId());
        }

可以看出代码没问题,也是按id升序的,但是存到redis中就乱序了
在这里插入图片描述
在这里插入图片描述
网上估计没有资料说明这个问题,但是我自己找出了原因:大家可以看到我的javabean是含有两个id的,一个是一级分类的,还有一个是二级分类的,存的时候我指定了key是二级分类的,但是却不是按二级分类的id来排序,原因是这个,opsForHash().put默认调用defaultHashOperations方法
在这里插入图片描述
然后put的是set,set是无序的,同时也不能重复,这就是为什么hash不能重复的原因,存相同的话会更新原先的,此外,如果id只有一个的话存入hash是有序的,id有多个即连接查询存入会变成无序,因为它不知道应该按哪个来排序,*注:很多人会认为hash是靠key来排序的,其实并不是,排序规则要复杂更多
在这里插入图片描述
解决思路:我各种找资料,尝试了两周弄出来了,需要的拿走,必成功。
三种方法,第一种:还是用hash存,取的时候放入集合中,利用集合自带的sort方法对特定的id排序
第二种:用list存,指定id作为键,会以升序进行排列
第三种:用zset存,我用的是这种方法,zset可以指定score来自定义排序规则
上代码:

		list = secondLevelMapper.selectByExample(example);
        for (SecondLevel each : list) {
             redisTemplate.opsForZSet().add("second_level_" + id,
                     JSONObject.toJSONString(each), each.getSecondLevelId());
        }

然后是取出数据,这里需要用到多条件查询,然后redis好像没这功能,貌似有大佬能写出条件查询,但是这对于非关系型数据库来说效率不高,所以需要更好的解决思路。
以下是我自己想出的多条件查询方法:先利用scan逐个遍历,再判断遍历的值是否与需要查询的数据匹配
代码:

	Cursor<ZSetOperations.TypedTuple<String>> cursor;
    if (redisTemplate.hasKey("goods")) {
        Goods redisGoods;
        cursor = redisTemplate.opsForZSet().scan("goods", ScanOptions.NONE);
        while (cursor.hasNext()) {
            ZSetOperations.TypedTuple<String> item = cursor.next();
            redisGoods = JSONObject.parseObject(item.getValue(), Goods.class);
            if ((goods.getFirstLevelId() == 0 || redisGoods.getFirstLevelId().equals(goods.getFirstLevelId()))
                    && (goods.getSecondLevelId() == 0 || goods.getSecondLevelId() == null || redisGoods.getSecondLevelId().equals(goods.getSecondLevelId()))
                    && (goods.getMasterGender() == 0 || redisGoods.getMasterGender().equals(goods.getMasterGender()))
                    && (goods.getTradeType() == 0 || redisGoods.getTradeType().equals(goods.getTradeType()))
                    && (goods.getGoodsDegree() == 0 || redisGoods.getGoodsDegree().equals(goods.getGoodsDegree())))
                list.add(redisGoods);
        }
        try {
            cursor.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

这部分代码和mysql查询时很像,最后必须要写上 cursor.close() 否则会阻塞线程!
之后需要整合消息队列,就能做到redis与MySQL定时同步,以后就都从redis查,不再经过mysql
此部分完~


五、部署项目至阿里云

推荐安装宝塔面板(操作很方便,教程自己找其他博客),在阿里云、宝塔面板中都需要配置以下安全组才能保证访问网站:
在这里插入图片描述
在宝塔面板的软件商店安装以下
在这里插入图片描述
千万别安装Nginx!,这是个坑,它会把你的80端口占用,之后就不能通过域名访问项目了,不要安装它推荐给你的,安装以上的三个就行,其他的都没用。

redis需要改成:
在这里插入图片描述
tomcat需要改成:

<Connector port="80" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />
<Engine name="Catalina" defaultHost="hbxytrade.top">
<Host name="hbxytrade.top"  appBase="webapps" unpackWARs="true" autoDeploy="true">
        <Context docBase="/www/server/tomcat/webapps/hbxytrade" path="" reloadable="true" />

注意:docBase要绝对路径,这也是个坑,网上很多博客都写错了,写成相对路径,写错就不能访问!

忘了说了,域名需要购买https://wanwang.aliyun.com/domain/1yuan 1元买一年的,不过需要填身份证等,还需要备案,很麻烦,大概需要3~5天
此部分完~


  • 6
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值