Guns二次开发(八):商品分类管理之【增】字诀

 

关于博客中使用的Guns版本问题请先阅读   Guns二次开发目录     

 

上一篇博客中,我们实现了商品分类管理的菜单查询功能,接下啦,我们开始实现添加的功能。

 

1、修改添加页面

(1)定义添加页面的展示逻辑

       首先我们要清楚,我们添加的商品分类,是具有父子级关系的。所以在最终调用后端的添加接口的时候,是需要一个参数——父类分类id的,那么这个父类id从哪里来呢?

我们不妨先参考Guns已经实现的菜单管理功能:

 

      从【菜单管理】的添加页面可以看到,每次添加的时候,都需要选择父级编号,以此来确定当前新增菜单的父级菜单是谁。看到这里,大家应该已经能够发现,菜单管理的实现逻辑与我们即将要实现的商品分类逻辑整体上大同小异。为了简便开发,我们其实可以直接去把菜单管理的代码扣过来用的,但是这里我想带领大家实现另一种思路。

      这种思路的产生,是由于每次打开添加页面之后,我首先要做的第一件事情,都是先选择父级菜单,这时候就会出现一种情况,当数据库中有上百条父级菜单时,我每次添加子菜单,都得眼睛睁得大大的先把父级菜单找出来。而且如果是极端情况下,我要给同一个父菜单添加数十条子菜单,那我每次可能都得花大量的时间在选择父级菜单上,这对于我们平台的运营管理人员来说,会是一场灾难。鉴于此,我便做出如下设想:为何不让用户先选中父级菜单后再点击【添加】按钮,然后按照mvc的原理,后端通过父类id获取父类信息,将信息渲染到页面后再返回,此时打开的添加页面便自然而然的拥有父类菜单的信息了。这样一来,使用者不需要再在选择父类菜单上花费精力,而只需关注自己添加子分类的内容即可。而且,如果用户没有选择父级菜单,那我默认用户就是添加顶级菜单,这种逻辑也是可以接受的。下面是实现之后的效果图。

 

(2)逾期实现的效果图

 

之后打开的添加页面里,已经有父类的信息了:

效果图有了,那就开始吧。

 

(3)开始修改页面的排版和字段名。

      修改显示的字段名,通常要与列表页面的字段名保持一致,还有类似类别id这个字段的值,是由数据库自增产生的,不能由用户设置,所以不能将这个值传给后端,等等。

 

      页面修改的具体过程,以及前端各种标签的使用方法等,我就不在此赘述了,都是很基础的东西,前面的查询功能的实现,大家如果都跟着走了一遍,那这里相信不用带,你也能有能力独立解决。没错,就是独立解决的能力,我自工作以来至今,遇到的所有问题都是自己一个人独立解决的,当然,这也得感谢网上大量的技术博客和开源项目。也正是因为这一份份恩情,使我发愿并以实际行动,尽力将自己的经验通过博文的方式分享出来,以期给给后来者提供些许帮助。

 

(4)代码修改点

老规矩,先将代码修改点贴出来,完整的代码还是在文章末尾贴出来。

 

开始之前,一定要先进行的操作:

 

 

A、第一处是修改【添加】按钮的鼠标点击事件,获取父类分类的信息,并传递给后端接口。

 

B、后端接口获取到前端传来的父类分类信息,并保存到request域对象中

 

C、添加分类页面获取request域对象中的值,并且设置成不可修改

 

上述操作完成后,展示的效果图便实现了。

 

2、实现添加分类页面的提交按钮功能

要修改提交按钮点击事件中的数据采集,原因

(1)原本的数据采集,获取的是所有的字段的参数,但是我们实际并不需要这么多数据,所以要修改数据采集。

(2)有些字段类似分类名称和父类编号都是必传字段,在调用后端接口之前,前端需要做数据校验,如果没有传值,就没必要调用后端接口,同时也要提示用户输入必传字段

 

采集数据需要使用一个 CommonTool.js :

 

 

 

然后是提交按钮触发的事件:

 

(3)将common.js引入到公共页面

因为这个采集数据的方法,在以后的很多js文件中都会用到,所以,我将这个js引入到所有的页面中,方便开发。

 

 

 

3、后端接口的实现逻辑

后端的新增分类管理接口,在Controller层我还定义了一个接收数据的实体类,有如下理由:

(1)Java单一职责原则,Model包下的类,是DAO层的实体类,里面有很多与数据表相关的注解,他的定义就是为了与数据库表打交道。

(2)实际开发中,可能需要接受一些数据库中没有的字段,没有必要往Entity实体类中添加数据库不存在的字段映射属性,虽然对于非数据库字段,mybatis-plus可以加个@TableField(exist = false) 来区分。

(3)实际开发中,针对某些必传字段,后端必须判断前端传来的是否为空值,如果为空值,就不能继续后面的操作。

(4)实际开发中,后端往往要校验某些字段的合法性,比如添加分类管理,后端必须判断parentId字段是否为0,如果不为0,说明就不是顶级菜单,此时就要去数据库查询当前parentId是否合法等。

       综上所述,前端传来的数据,后端不可能直接拿来用,既然不能直接拿来用,再加上单一职责原则,定义一个新的类来接受前端数据就显得合情合理合规范。

       我习惯于给前端接收参数的实体类,加一个“Form”后缀;返回给前端的数据的是实体类,我也会加一个“Dto”后缀。这也算是一种编码规范吧,就跟SpringMVC分为Controller,Service,Dao 三层开发一个意思。

       同时,我使用了@Valid注解,对前端传入的某些必传数据做后端校验,这样一来,就不用写大量的代码去判断必传字段是否为空了。

 

 

下面看一下核心代码点的截图,详细代码最后附上:

controller层:

 

CategoryForm.java 类中,还需要配合 @Valid使用一些注解。跟多@Valid注解请参考 https://blog.csdn.net/weixin_41969587/article/details/88600234 

 

 

如果仅仅只是这样设置了还不够,我们制造一个异常,比如name字段的值为空值:

 

如下图,添加接口因为name字段为空而报错了,这时报错的信息提示的是服务器异常,但是这样的报错不够友好,我们应该进行自定义。

 

此时也可以看到,接口返回的状态码是自定义的500:

 

经过一段时间的开发,如果你有留意,就会发现,guns其实是做了全局异常的处理的,只要返回的状态码不是200,控制台就一定会有异常信息打印:

 

全局搜索的找到全局异常的处理类:

 

找到了全局异常所在的文件之后,我们要在这个全局异常之前(测试发现,其实不用在RuntimeException异常之前也能生效),添加一个对BindException异常的拦截:

 

再次测试,发现异常信息变了,通过下图红框中的内容,可以知道我们设置的拦截生效了:

 

此时还不够,我们还需要从 BindException类中找到有用的信息并单独显示出来:

 

下面是修改后的方法:

 

 

重启后再测试,发现异常提示信息已经足够友好了:

 

 

业务层的方法:

 

 

 

4、添加成功之后在跳转回原来的页面

假如我原来是在第三页,那么我在添加成功之后,模态框退出了,然后我依旧希望在第三页,并且数据是刷新过的。

 

 

 

 

 

 

 

 

 

 

5、条件查询的补充

 

       有这么一个场景,当我总共有10条记录,每页查询5条,那么总共有2页,我点击第二页后,此时修改每页查询20条,点击搜索后,就会发现没有数据。原因是,点击搜索时,获取到的当前页码依旧是2,而每页查询20条,总共也才1页,第二页肯定没有数据了。解决方法是,给设置每页查询条数的input标签设置失去焦点事件,当修改了页码之后,将当前页码置为1就可以了,如下图:

 

6、具体的代码

注意,如果你全部都引入了这里的代码,但是仍旧无法使用,那你还需要引入 Guns二次开发(七):商品分类管理之【查】字诀(3) 这篇博客的代码,如果仍旧不行,可以直接引入我增删改查都完成之后的代码(暂未实现)。

先把设计到修改的所有文件的目录结构贴出来。

 

 

 

 

 

 

 

(1)CategoryController.java

package cn.stylefeng.guns.elephish.controller;

import cn.stylefeng.guns.core.common.constant.factory.PageFactory;
import cn.stylefeng.guns.elephish.bean.PageInfo;
import cn.stylefeng.guns.elephish.bean.QueryParam;
import cn.stylefeng.guns.elephish.form.CategoryForm;
import cn.stylefeng.guns.elephish.wrapper.CategoryWrapper;
import cn.stylefeng.roses.core.base.controller.BaseController;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.plugins.Page;
import org.beetl.core.misc.BeetlUtil;
import javax.validation.Valid;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.beans.factory.annotation.Autowired;
import cn.stylefeng.guns.core.log.LogObjectHolder;
import org.springframework.web.bind.annotation.RequestParam;
import cn.stylefeng.guns.elephish.model.Category;
import cn.stylefeng.guns.elephish.service.ICategoryService;

import javax.servlet.http.HttpServletRequest;
import java.util.List;
import java.util.Map;

/**
 * 分类管理控制器
 *
 * @Date 2020-04-13 11:02:46
 */
@Controller
@RequestMapping("/category")
public class CategoryController extends BaseController {

    private Logger logger = LoggerFactory.getLogger(getClass());

    private String PREFIX = "/elephish/category/";

    @Autowired
    private ICategoryService categoryService;


    /**
     * 跳转到添加分类管理
     */
    @RequestMapping("/category_add")
    public String categoryAdd(Integer parentId,String parentName,Integer depth,
                              Integer currentPage,HttpServletRequest request) {

        request.setAttribute("parentId",parentId);
        request.setAttribute("parentName",parentName);
        request.setAttribute("depth",depth);
        request.setAttribute("currentPage",currentPage);
        return PREFIX + "category_add.html";
    }

    /**
     * 跳转到修改分类管理
     */
    @RequestMapping("/category_update/{categoryId}")
    public String categoryUpdate(@PathVariable Integer categoryId, Model model) {
        Category category = categoryService.selectById(categoryId);
        model.addAttribute("item",category);
        LogObjectHolder.me().set(category);
        return PREFIX + "category_edit.html";
    }

    /**
     * 跳转到分类管理首页
     */
    @RequestMapping("")
    public String index() {

        return PREFIX + "category.html";
    }


    /**
     * 获取分类管理列表
     */
    @RequestMapping(value = "/list")
    @ResponseBody
    public Object list(QueryParam queryParam,PageInfo pageInfo) {

        List<Map<String, Object>> list = categoryService.listCategory(queryParam,pageInfo);

        //因为是自定义分页,所以返回的数据格式需要做特殊封装,主要是两个属性名的定义要固定
        JSONObject jo=new JSONObject();//也可以使用 Map<String,Object>

        //属性名必须是【data】,对应的值是List<Map<String, Object>>格式
        jo.put("data",new CategoryWrapper(list).wrap());
        jo.put("pageInfo",pageInfo);//属性名必须是 pageInfo,

        return jo;
    }

    /**
     * 新增分类管理
     */
    @RequestMapping(value = "/add")
    @ResponseBody
    public Object add(@Valid CategoryForm categoryForm) {

        /**
         * 1、修改接收数据的实体类,因为如果直接使用DAO层的实体类来接收,
         * 会导致一些不需要的数据被写进数据库
         * 2、对必传数据要判断是否为空
         * 3、只接收需要的数据,比如这个CategoryForm实体类,id这个字段我是不需要的,但是只是
         * 添加这个接口不需要,我修改接口是需要的,此时不能在CategoryForm这个类中不定义id这个属性。
         * 所以,正确的做法是,在添加业务里,我不在乎你是否传了id,因为即使你传了我也不会使用
         */
        categoryService.addCategory(categoryForm);
        return SUCCESS_TIP;
    }

    /**
     * 删除分类管理
     */
    @RequestMapping(value = "/delete")
    @ResponseBody
    public Object delete(@RequestParam Integer categoryId) {
        categoryService.deleteById(categoryId);
        return SUCCESS_TIP;
    }

    /**
     * 修改分类管理
     */
    @RequestMapping(value = "/update")
    @ResponseBody
    public Object update(Category category) {
        categoryService.updateById(category);
        return SUCCESS_TIP;
    }

    /**
     * 分类管理详情
     */
    @RequestMapping(value = "/detail/{categoryId}")
    @ResponseBody
    public Object detail(@PathVariable("categoryId") Integer categoryId) {
        return categoryService.selectById(categoryId);
    }
}

 

(2)CategoryMapper.xml (无变动,可忽略)

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.stylefeng.guns.elephish.dao.CategoryMapper">

    <!-- 通用查询映射结果 -->
    <resultMap id="BaseResultMap" type="cn.stylefeng.guns.elephish.model.Category">
        <id column="id" property="id" />
        <result column="parent_id" property="parentId" />
        <result column="parent_ids" property="parentIds" />
        <result column="name" property="name" />
        <result column="status" property="status" />
        <result column="sort" property="sort" />
        <result column="version" property="version" />
        <result column="creat_time" property="creatTime" />
        <result column="update_time" property="updateTime" />
    </resultMap>

    <!-- 通用查询结果列 -->
    <sql id="Base_Column_List">
        id, parent_id, parent_ids, name, status, sort, version, creat_time, update_time
    </sql>


</mapper>

 

(3)CategoryMapper.java (无变动,可忽略)

package cn.stylefeng.guns.elephish.dao;

import cn.stylefeng.guns.elephish.model.Category;
import com.baomidou.mybatisplus.mapper.BaseMapper;

/**
 * <p>
 *  Mapper 接口
 * </p>
 *
 * @author hqq
 * @since 2020-04-13
 */
public interface CategoryMapper extends BaseMapper<Category> {


}

 

 

(4)CategoryForm.java

package cn.stylefeng.guns.elephish.form;

import lombok.Data;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

/**
 * 接收前端传递过来的参数的CategoryForm实体类
 * Created by hqq on 2020/4/19.
 */
@Data
public class CategoryForm {

    private int id;//分类编号

//    @NotNull(message = "分类父编号不能为空")
    private int parentId;//分类父编号,修改时不需要

    @Size(max=10,message = "分类名称的长度不能超过10")
    @NotBlank(message = "分类名称不能为空")
    private String name;//分类名称

    @NotNull(message = "排序编号不能为空")
    private Integer sort;//排序编号

    private int version;//乐观锁字段,修改时需要

    private int status;//状态设置,仅添加时需要
}

 

(5)CategoryServiceImpl.java

package cn.stylefeng.guns.elephish.service.impl;

import cn.stylefeng.guns.core.common.constant.factory.ConstantFactory;
import cn.stylefeng.guns.core.common.exception.BizExceptionEnum;
import cn.stylefeng.guns.core.common.node.ZTreeNode;
import cn.stylefeng.guns.elephish.bean.PageInfo;
import cn.stylefeng.guns.elephish.bean.QueryParam;
import cn.stylefeng.guns.elephish.constants.LimitationConstant;
import cn.stylefeng.guns.elephish.constants.StatusConstant;
import cn.stylefeng.guns.elephish.constants.WrapperDictNameConstant;
import cn.stylefeng.guns.elephish.dao.ProductAttachMapper;
import cn.stylefeng.guns.elephish.dao.ProductAttributeGroupMapper;
import cn.stylefeng.guns.elephish.form.CategoryForm;
import cn.stylefeng.guns.elephish.model.Category;
import cn.stylefeng.guns.elephish.dao.CategoryMapper;
import cn.stylefeng.guns.elephish.model.ProductAttach;
import cn.stylefeng.guns.elephish.model.ProductAttributeGroup;
import cn.stylefeng.guns.elephish.service.ICategoryService;
import cn.stylefeng.guns.elephish.service.IProductAttributeGroupService;
import cn.stylefeng.guns.elephish.utils.DBUtil;
import cn.stylefeng.guns.elephish.utils.StringUtil;
import cn.stylefeng.guns.elephish.utils.TimeUtil;
import cn.stylefeng.roses.kernel.model.exception.ServiceException;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.mapper.EntityWrapper;
import com.baomidou.mybatisplus.mapper.Wrapper;
import com.baomidou.mybatisplus.plugins.Page;
import com.baomidou.mybatisplus.service.impl.ServiceImpl;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.*;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author hqq
 * @since 2020-04-13
 */
@Service
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements ICategoryService {

    @Autowired
    private CategoryMapper categoryMapper;



        /**
     * 自定义逻辑的添加商品分类实现
     * @param form
     */
    @Transactional
    @Override
    public void addCategory(CategoryForm form) {

        int parentId = form.getParentId();
        int status = form.getStatus();

        //判断status字段是否合法
        if(!(status==PRODUCT_CATEGORY_STATUS_START || status==PRODUCT_CATEGORY_STATUS_STOP)){
            //ILLEGAL_PARAM(400,"非法参数"),
            throw new ServiceException(BizExceptionEnum.ILLEGAL_PARAM);
        }

        /**
         *虽然我前端做了必传字段的半段,但是我在以往的很多博客中都说过,
         *永远不要相信前端传来的数据(即便前后端代码都是同一个人写),
         *该做的判断还是要判断,
         */
        //设置parentIds,默认值是0
        String parentIds = "[0],";
        int depth = 1;

        //判断parentId是否合法
        if(parentId>0){
            Category cg=categoryMapper.selectById(parentId);
            if(cg ==null){
                //CATEGORY_ERROR_PARENT_NOT_EXIST(500,"数据库没有查找到对应的父类信息"),
                throw new ServiceException(BizExceptionEnum.CATEGORY_ERROR_PARENT_NOT_EXIST);
            }

            if(cg.getStatus() == PRODUCT_CATEGORY_STATUS_STOP){
                //CATEGORY_ERROR_PARENT_DELETED(500,"不能为已废弃的父类菜单添加子菜单"),
                throw new ServiceException(BizExceptionEnum.CATEGORY_ERROR_PARENT_DELETED);
            }

            //判断当前是否是叶子节点,如果是,则无法添加子分类
            if(cg.getDepth() >= LimitationConstant.MALL_CATEGORY_TREE_MAX_DEPTH){
                //CATEGORY_ERROR_NO_NEXT_NODE(500,"当前节点已是叶子节点,无法继续添加子分类"),
                throw new ServiceException(BizExceptionEnum.CATEGORY_ERROR_NO_NEXT_NODE);
            }

            parentIds = cg.getParentIds()+ "["+parentId+"],";
            depth = cg.getDepth()+1;
        }

        //开始执行添加操作
        Category category = new Category();
        category.setParentId(parentId);
        category.setParentIds(parentIds);
        category.setDepth(depth);
        category.setSort(form.getSort());
        category.setName(form.getName());
        category.setVersion(1);
        category.setStatus(status);//设置默认状态
        int count = categoryMapper.insert(category);
        if(count==0){
            // ERROR_EXISTED_SAME_NAME_RECORD(500,"数据库中已经存在同名的记录"),
            throw new ServiceException(BizExceptionEnum.ERROR_EXISTED_SAME_NAME_RECORD);
        }


        //判断分类名称是否已经存在。status=1 ,2  时才算重复
        Wrapper<Category> wrapper = new EntityWrapper<>();
        wrapper.eq("parent_id",parentId)
                .eq("name",form.getName())
                .in("status",new Integer[]{PRODUCT_CATEGORY_STATUS_START,PRODUCT_CATEGORY_STATUS_STOP});
        count = categoryMapper.selectCount(wrapper);
        //前面已经添加一次了,这时数据库应该只有一条同名记录,如果大于一条,说明名字重复
        //此时抛出异常,那么整个事务都会回滚,添加操作失败
        if(count>1){
            //ERROR_EXISTED_SAME_NAME_RECORD(500,"数据库中已经存在同名的记录"),
            throw new ServiceException(BizExceptionEnum.ERROR_EXISTED_SAME_NAME_RECORD);
        }
    }


    @Override
    public List<Map<String, Object>> listCategory(QueryParam queryParam, PageInfo pageInfo) {

        //设置排序
        String sortField = "sort";//排序字段
        boolean isAsc = true;//是否正序排序

        //构建查询条件
        Wrapper<Category> wrapper = buildWrapper(queryParam, sortField,isAsc);

        Page<Map<String,Object>> page=new Page<>(pageInfo.getCurrentPage(),pageInfo.getLimit());
        List<Map<String, Object>> maps = categoryMapper.selectMapsPage(page, wrapper);

        //设置总页数
        int total = (int) page.getTotal();
        pageInfo.setTotalPage((int)Math.ceil(1.0*total/pageInfo.getLimit()));//总页数
        pageInfo.setTotalCount(total);//总记录数

        if(maps.isEmpty()){
            return maps;
        }

        //设置查询到的本页记录数,因为默认值为0,所以大于0的时候才需要设置
        pageInfo.setSize(maps.size());

        //如果不查询子类菜单,直接返回
        if(!queryParam.isSearchChild()){
            return maps;
        }

        //遍历查询其子类
        List<Map<String, Object>> list = new ArrayList<>();
        for(int i = 0; i<maps.size();i++){
            findChildCategory(list, maps.get(i),sortField,isAsc,queryParam.getStatus());
        }

        return list;
    }

    /**
     * 封装 category 的查询条件,
     * 注意:这些查询条件是针对顶级菜单的,
     * 子级菜单的查询条件只有一个parent_id和排序方式
     * @param queryParam
     * @return
     */
    private Wrapper<Category> buildWrapper(QueryParam queryParam,String sortField,boolean isAsc){

        int status = queryParam.getStatus();

        Wrapper<Category> wrapper = new EntityWrapper<>();

        //设置排序字段和排序方式
        wrapper.orderBy(sortField,isAsc);

        //是否按照层级查询
        Integer depth =queryParam.getDepth();
        if(depth != null){
            wrapper.eq("depth", depth);
        }

        //是否按照分类名称查询
        if(StringUtils.isNotBlank(queryParam.getName())){
            wrapper.like("name",queryParam.getName());
        }else{
            //只有不按分类名称查询,并且没有指定深度,才设置默认的parentId为0
            if(depth == null){
                wrapper.eq("parent_id", 0);
            }
        }

        //是否按照状态查询
        if(status> 0){
            if(!(status==PRODUCT_CATEGORY_STATUS_START || status==PRODUCT_CATEGORY_STATUS_STOP)){
                //ILLEGAL_STATUS_VALUE(400,"状态字段的值异常"),
                throw new ServiceException(BizExceptionEnum.ILLEGAL_STATUS_VALUE);
            }

            wrapper.eq("status",queryParam.getStatus());
        }else{
            //否则,只查询未删除的记录
            wrapper.in("status",new Integer[]{PRODUCT_CATEGORY_STATUS_START,PRODUCT_CATEGORY_STATUS_STOP});
        }

        return wrapper;

    }


    /**
     * 递归算法,算出子级菜单
     */
    private List<Map<String, Object>> findChildCategory(List<Map<String, Object>> result,
                  Map<String, Object> category,String sortField, boolean isAsc, int status){
        result.add(category);

        //封装子级菜单的查询条件,
        // 子级菜单的查询条件只有一个parent_id和排序方式
        Wrapper<Category> wrapper = new EntityWrapper<>();
        wrapper.orderBy(sortField,isAsc)
                .eq("parent_id", new Integer(category.get("id").toString()));

        if(status>0){
            wrapper.eq("status",status);
        }else{
            //否则,只查询未删除的记录
            wrapper.in("status",new Integer[]{PRODUCT_CATEGORY_STATUS_START,PRODUCT_CATEGORY_STATUS_STOP});

        }

        //查找子节点,递归算法一定要有一个退出的条件
        List<Map<String, Object>> childList = categoryMapper.selectMaps(wrapper);
        for (Map<String, Object> temp : childList) {
            findChildCategory(result,temp,sortField,isAsc, status);
        }
        return result;
    }
}

 

(6)ICategoryService.java

package cn.stylefeng.guns.elephish.service;

import cn.stylefeng.guns.elephish.bean.PageInfo;
import cn.stylefeng.guns.elephish.bean.QueryParam;
import cn.stylefeng.guns.elephish.form.CategoryForm;
import cn.stylefeng.guns.elephish.model.Category;
import com.baomidou.mybatisplus.service.IService;

import java.util.List;
import java.util.Map;

/**
 * <p>
 *  服务类
 * </p>
 *
 * @author hqq
 * @since 2020-04-13
 */
public interface ICategoryService extends IService<Category> {

    /**
     * 获取分类管理列表
     */
    List<Map<String,Object>> listCategory(QueryParam queryParam, PageInfo pageInfo);

    /**
     * 自定义逻辑的添加商品分类实现
     */
    void addCategory(CategoryForm categoryForm);
}

 

(7)category.js

/**
 * 分类管理管理初始化
 */
var Category = {
    id: "CategoryTable",	//表格id
    seItem: null,		//选中的条目
    table: null,
    layerIndex: -1,
    maxDepth: 3 //最大的深度
};



/**
 * 初始化表格的列
 */
Category.initColumn = function () {
    return [
        {field: 'selectItem', radio: true , hiddenField:"[version],"},
            {title: '分类名称', field: 'name', visible: true, align: 'center', valign: 'middle'},
            {title: '分类编号', field: 'id', visible: true, align: 'center', valign: 'middle'},
            {title: '分类父编号', field: 'parentId', visible: true, align: 'center', valign: 'middle'},
            {title: '层级', field: 'depth', align: 'center', valign: 'middle', sortable: true},
            {title: '排序', field: 'sort', visible: true, align: 'center', valign: 'middle'},
            {title: '状态', field: 'statusName', visible: true, align: 'center', valign: 'middle'},
            {title: '创建时间', field: 'createTime', visible: true, align: 'center', valign: 'middle',
                formatter: function (value) {
                    return typeof(value)=="undefined"?"":moment(+value).format('YYYY-MM-DD HH:mm:ss');
                }
            },
            {title: '更新时间', field: 'updateTime', visible: true, align: 'center', valign: 'middle',
                formatter: function (value) {
                    return typeof(value)=="undefined"?"":moment(+value).format('YYYY-MM-DD HH:mm:ss');
                }
            },
            {title: '版本', field: 'version', align: 'center', valign: 'middle'}
    ];
};

/**
 * 检查是否选中
 */
Category.check = function () {

    var selected = $('#' + this.id).bootstrapTreeTable('getSelections');;
    if(selected.length == 0){
        Feng.info("请先选中表格中的某一记录!");
        return false;
    }else{

        Category.seItem = selected[0];
        return true;
    }
};



/**
 * 点击添加分类管理
 */
Category.openAddCategory = function () {
    //默认用户是添加顶级分类
    var parentId = 0;
    var parentName = '顶级';
    var depth = 1;

    //尝试获取用户选中的item标签
    var selected = $('#' + this.id).bootstrapTreeTable('getSelections');

    //如果用户选中了某个单选框,说明是在这个单选框下添加
    if(selected.length > 0){
        var item = selected[0];
        parentName = item.name;//分类名

        //如果当前选中的分类已经被废弃了,那么就不允许添加子分类
        if(item.statusName == "已废弃"){
            Feng.info("分类【"+parentName+"】已被废弃,无法添加子分类!");
            return;
        }

        parentId = item.id;//分类id
        depth = item.depth;//分类的深度


        //我项目设计了分类管理项目的最大层级是3级,如果超过3级就不能添加
        if(depth >= Category.maxDepth){
            Feng.error("当前节点已是叶子节点,无法继续添加子分类");
            return ;
        }
    }

    //拼接url上需要的参数
    var urlParams = '?parentId='+parentId+'&parentName='+parentName
        +'&depth='+depth+"&currentPage="+$("#currentPage").val();

    var index = layer.open({
        type: 2,
        title: '添加分类管理',
        area: ['800px', '420px'], //宽高
        fix: false, //不固定
        maxmin: true,
        content: Feng.ctxPath + '/category/category_add'+urlParams
    });
    this.layerIndex = index;


};


/**
 * 打开查看分类管理详情
 */
Category.openCategoryDetail = function () {
    if (this.check()) {
        var index = layer.open({
            type: 2,
            title: '分类管理详情',
            area: ['800px', '420px'], //宽高
            fix: false, //不固定
            maxmin: true,
            content: Feng.ctxPath + '/category/category_update/' + Category.seItem.id
        });
        this.layerIndex = index;
    }
};

/**
 * 重置查询条件条件
 */
Category.reset = function () {
    $("#byStatus").find("option[text='正常']").attr("selected",true);
    $("#byStatus").find("option[text!='正常']").attr("selected",false);

    $("#searchChild").find("option[text='是']").attr("selected",true);
    $("#searchChild").find("option[text!='是']").attr("selected",false);

    $("#currentPage").val("1");//当前页
    $("#limit").val("5");//每页查询条数
    $("#byName").val("");//分类名称
    $("#byDepth").val("");//层级
    $("#searchChild").attr("disabled",false);
}

/**
 * 删除分类管理
 */
Category.delete = function () {
    if (this.check()) {
        var ajax = new $ax(Feng.ctxPath + "/category/delete", function (data) {
            Feng.success("删除成功!");
            Category.table.refresh();
        }, function (data) {
            Feng.error("删除失败!" + data.responseJSON.message + "!");
        });
        ajax.set("categoryId",this.seItem.id);
        ajax.start();
    }
};



/**
 * 条件查询分类管理列表
 */
Category.search = function () {

    //当前页面刷新
    var queryParams = Category.formParams();
    queryParams['currentPage'] = $("#currentPage").val();

    Category.table.refresh({query: queryParams});

};


$(function () {

    var defaultColunms = Category.initColumn();
    var table = new BSTreeTable(Category.id, "/category/list", defaultColunms);
    table.setExpandColumn(1);//设置第一列展示下拉列表
    table.setIdField("id");//分类编号
    table.setCodeField("id");//分类父编号,用于设置父子关系
    table.setParentCodeField("parentId");//分类父编号,用于设置父子关系
    table.setExpandAll(true);

    //设置请求时的参数
    var queryData = Category.formParams();
    queryData['limit'] = 5;//
    // alert(JSON.stringify(queryData));
    table.setData(queryData);

    table.init();
    Category.table = table;


    $("#limit").val("5");//设置每页的查询的默认条数

    //设置当前对象的名称,分页时需要使用
    PageTool.callerName="Category";

});


/**
 * 查询表单提交参数对象
 * @returns {{}}
 */
Category.formParams = function() {
    var queryData = {};

    queryData['name'] = $("#byName").val().trim();//名称条件
    queryData['depth'] = $("#byDepth").val();//层级条件
    queryData['status'] = $("#byStatus").val();//状态条件
    queryData['searchChild'] = $("#searchChild").val();//是否查询子菜单
    queryData['limit'] = $("#limit").val();//设置每页查询条数

    return queryData;
}

/**
 * 每页查询的页码数修改之后触发失去焦点事件,
 * 将当前页码重置为 1 .
 * 主要是为了解决以下情况:
 * 假设总共10条记录,每页查询3条,那么总共就有4页,当用户在第三页的时候,
 * 修改成每页查询10条,修改后点击查询,会出现没有数据显示的情况。
 * 原因是,用户的当前页码 currentPage 的值依旧是3,
 * 而每页查询10条后,总共只有1页,查询第三页时肯定没有数据啦
 */
$("#limit").on('blur',function(){
    $("#currentPage").val(1);
});


/**
 * 【分类名称】输入框失去焦点事件
 */
$("#byName").on('blur',function(){
    Category.setSearchChildSelected();
});

/**
 *【深度】输入框失去焦点事件
 */
$("#byDepth").on('blur',function(){
    Category.setSearchChildSelected();
});

/**
 * 设置【是否查询子菜单】选择框是否可用
 */
Category.setSearchChildSelected = function () {
    var byName = $("#byName").val().trim();
    var byDepth = $("#byDepth").val().trim();


    if(byName && !byDepth){
        //当选择分类名称查询,不选择层级查询时,默认无法查询子类菜单,这样是为了防止查重和查出不必要的数据
        $("#searchChild").val("false");
        $("#searchChild").attr("disabled",true);
    }else{
        //其它情况,都可以自主觉得是否查询子菜单
        $("#searchChild").val("true");
        $("#searchChild").attr("disabled",false);
    }
}

 

(8)category_info.js

/**
 * 初始化分类管理详情对话框
 */
var CategoryInfoDlg = {
    ztreeInstance: null
};



/**
 * 关闭此对话框
 */
CategoryInfoDlg.close = function() {
    parent.layer.close(window.parent.Category.layerIndex);
}


/**
 * 收集数据
 */
CategoryInfoDlg.collectData = function(type) {

    var fieldArr;//定义一个数组,封装所有需要收集的数据的字段
    var needArr;//定义一个数组,封装字段是否是必传字段,注意必须与 fieldArr数组中的容量保持一致
    var fieldNameArr;//定义一个数组,封装对应字段的字段名称,用于收集信息出错时定位错误信息
    var typeArr;//定义数组,封装需要收集的字段的值的类型
    var strMaxLengthArr;//定义数组,指定字符串类型的字段的值的最大长度。-1表示该字段不用比较

    if(type === 'add'){
        //设置添加需要的字段
        fieldArr = ['parentId','name','sort','status'];
        needArr = [true,true,true,true];
        fieldNameArr = ['父类编号','分类名称','排序编号','状态设置'];
        typeArr = ['number','','number','number'];
        strMaxLengthArr = [11,10,11,2];

    }else if(type==='edit'){
        //设置修改需要的字段
        fieldArr = ['id','parentId','name','sort','version'];
        needArr = [true,true,true,true,true];
        fieldNameArr = ['分类编号','父类编号','分类名称','排序编号','版本信息'];
        typeArr = ['number','number','','number','number'];
        strMaxLengthArr = [11,11,10,11,11];
    }else{
        return "收集信息出错啦";
    }

    return CommonTool.collectData(fieldArr,needArr,fieldNameArr,typeArr,strMaxLengthArr);
}


/**
 * 提交添加
 */
CategoryInfoDlg.addSubmit = function() {

    //重新采集数据
    var result = this.collectData("add");

    //如果返回的字段是字符串类型的,说明出错了,直接返回出错信息
    if(typeof(result)=='string'){
        Feng.error(result);
        return ;
    }

    //提交信息
    var ajax = new $ax(Feng.ctxPath + "/category/add", function(data){
        Feng.success("添加成功!");
        //修改成功之后,刷新数据,依旧跳转到原来的页面
        var queryParams = window.parent.Category.formParams();
        queryParams['currentPage'] = $("#currentPage").val()
        window.parent.Category.table.refresh({query:queryParams });

        CategoryInfoDlg.close();
    },function(data){
        Feng.error("添加失败:" + data.responseJSON.message + "!");
    });
    ajax.set(result);//设置请求参数
    ajax.start();

}

/**
 * 提交修改
 */
CategoryInfoDlg.editSubmit = function() {

    this.clearData();
    this.collectData();

    //提交信息
    var ajax = new $ax(Feng.ctxPath + "/category/update", function(data){
        Feng.success("修改成功!");
        window.parent.Category.table.refresh();
        CategoryInfoDlg.close();
    },function(data){
        Feng.error("修改失败!" + data.responseJSON.message + "!");
    });
    ajax.set(this.categoryInfoData);
    ajax.start();
}

$(function() {

});

 

(9)category_add.html

@layout("/common/_container.html"){
<div class="ibox float-e-margins">

    <strong style="color:red;">
        注意:默认添加顶级分类,如果要添加子级分类,请先选中对应的父类分类。
        <br/>
        <span style="white-space: pre">          带<b style='color:red'>*</b>号字段为必填项</span>

    </strong>
    <br/>
    <br/>
    <div class="ibox-content">
        <div class="form-horizontal">
            <div class="row">
                <div class="col-sm-6 b-r">
                    <h3 style="color:green;text-align:center;">父类信息</h3>

                    <!--保存当前页码-->
                    <input id="currentPage" type="hidden" value="${nvl(currentPage,1)}">

                    <#input id="parentName" disabled="disabled"
                            value="${nvl(parentName,'无')}" name="父类名称" underline="false"/>
                    <#input id="parentId" disabled="disabled"
                            value="${nvl(parentId,'无')}" name="父类编号" underline="false"/>
                    <#input id="parentDepth" disabled="disabled"
                            value="${nvl(depth,'无')}" name="父类层级" underline="false"/>
                </div>

                <div class="col-sm-6">
                    <h3 style="color:green;text-align:center;">新增分类信息</h3>
                        <#input id="name" name="分类名称<b style='color:red'>*</b>" underline="false"/>
                        <#input id="sort" name="排序编号" value="1" underline="false"/>
                        <#SelectCon id="status" name="状态设置<b style='color:red'>*</b>" >
                            <option value="1">启用</option>
                            <option value="2">停用</option>
                        </#SelectCon>
                </div>
            </div>

            <div class="row btn-group-m-t">
                <div class="col-sm-10">
                    <#button btnCss="info" name="提交" id="ensure" icon="fa-check" clickFun="CategoryInfoDlg.addSubmit()"/>
                    <#button btnCss="danger" name="取消" id="cancel" icon="fa-eraser" clickFun="CategoryInfoDlg.close()"/>
                </div>
            </div>
        </div>

    </div>
</div>
<script src="${ctxPath}/static/modular/elephish/product/category_info.js?j=${date().time}"></script>
@}

 

 

(10)paging.js

/**
 * 本分页功能是在bootrap-treetable.js基础上实现的,使用前必须严格遵守guns生成的代码规范
 * 使用说明:
 * (1)使用之前,请先配套获取修改后的 bootstrap-treetable.js,并替换掉guns v5.1-final
 *      原来的bootstrap-treetable.js,
 *      guns原js修改的地方也才几行代码,你也可以自己修改,而不用获取本处提供的
 * (2)将本文件 paging.js 引入到你需要分页的html页面中,比如本篇博客的 category.html页面中
 * (3)在你需要插入【分页条】的地方(html页面中的某处位置)引入这段div :<div id="pageDiv"></div>
 * (4)接口返回的数据需是json格式的,并且必须保证用以下两个字段封装对应的数据:
        "data" : 列表所需要的实体类数据,用list集合封装
        "pageInfo" : 分页信息

        注:
        data 集合中实体类需符合原 guns v5.1-final 【菜单管理】列表查询功能的标准;
        pageInfo 需包含以下5个字段:{
             currentPage : 要查询第几页,默认是1,防止空指针
             totalPage :总页数,默认值0
             totalCount : 总记录数,默认值0
             limit :每页查询的条数,默认值每页查询1条,防止空指针
             size : 本页实际查询到的条数
        }
 * (5)在bootstrap表格相关的js中,本示例就category.js文件,在这个文件中,
 * 对象的名称是"Category",此时需要在该js文件的$(function () {})中做如下
 * 赋值操作:
 * $(function () {
 *      PageTool.callerName("Category");
 * })
 *
 * (6)尽量使用guns代码生成的页面和js,要确保你的js中拥有guns代码生成的
 * 两个方法,A、对象.formParams()  ; B、对象.table.refresh();
 * 该分页函数是以guns为基础做的补充,所以使用时请遵守Guns原有的规范
 *
 */
var PageTool = {
    callerName:"" //设置顶级调用者的名称,用于创建该对象时使用
};


/**
 * 自定义的分页之构建构建div里面的内容
 *
 * 记住,使用之前,必须要在你需要显示页码条的位置引入这段div标签:
 * <div id="pageDiv"></div>
 */
PageTool.buildPageDiv = function  (pageInfo) {
    // alert(JSON.stringify(pageInfo));

    //先清空之前的内容
    $("#pageDiv").empty();

    var currentPage=pageInfo.currentPage;//当前页
    var totalPage=pageInfo.totalPage;//总页数
    var totalCount=pageInfo.totalCount;//符合条件的总条数(不包含子菜单)
    var size=pageInfo.size;//本页查询到记录数(不包含子菜单)
    var limit=pageInfo.limit;//每页查询的记录数

    //添加一个隐藏标签,用于记录当前页码
    var pageDiv = "<br/><input id='currentPage' type='hidden' value='"+currentPage+"'/>" +
        "<strong style='color:red'>注意:此处的分页只相对同级别的父类菜单而言,所有子类菜单的记录数不在分页之内" +
        "</strong><br/><ul class='pagination'>";
    <!-- 首页,如果当前页是第一页,则设置首页不可用-->
    if(currentPage==1){
        pageDiv +="<li><a href='javascript:void(0)' disabled='true' style='color:black;font-weight:bold;'>首页</a></li>";
    }else{
        pageDiv += "<li><a href='javascript:void(0)' onclick='PageTool.pageSkip(this)' currentPage='1'>首页</a></li>";
    }

    <!-- 上一页,先判断是不是第一页,第一页,就没有上一页 -->
    if(currentPage > 1){
        pageDiv += "<li><a href='javascript:void(0)' onclick='PageTool.pageSkip(this)'" +
            " currentPage='"+(currentPage - 1)+"'>上一页</a></li>";
    }

    <!-- #####展示页码功能 开始#####-->

    <!-- 定义两个模板变量 -->
    var begin=1;
    var end=5;

    if(totalPage <= 5){
        //当总的页数小于等于5的情况
        begin=1 ;
        end=totalPage;
    }else{
        // 总页码大于5的情况
        <!-- 如果当前页码为 1 - 3,那么 我们显示前面5个页码,也就是1-5 -->
        if(currentPage<=3){
            begin=1 ;
            end=5;
        }else if(currentPage >= totalPage-2 ){
            <!-- 如果当前页码为 末尾的3个,也就是 currentPage >=totalPage - 2 ,那么 就显示末尾5个页码 -->
            begin=totalPage-4 ;
            end=totalPage;
        }else{
            <!-- 其他中间的页码,显示 currentPage - 2 到 currentPage + 2  -->
            begin=currentPage-2 ;
            end=currentPage+2;
        }
    }

    <!-- 遍历页码  -->
    for(var i=begin; i <= end; i++){
        if(currentPage == i){
            <!-- 如果页码是当前页,则设置标签不可用 -->
            pageDiv += "<li><a href='javascript:void(0)' disabled='true' style='color:black;font-weight:bold;'>"+i+"</a></li>";

        }else{
            <!-- 如果页码不是当前页,则加连接 -->
            pageDiv += "<li><a href='javascript:void(0)' onclick='PageTool.pageSkip(this)' currentPage='"+i+"'>"+i+"</a></li>";
        }
    }
    <!-- #####展示页码功能 结束#####-->

    <!-- 下一页,先判断是不是最后一页,最后一页,就没有下一页 -->
    if(currentPage < totalPage ){
        pageDiv += "<li><a href='javascript:void(0)' onclick='PageTool.pageSkip(this)' currentPage='"+(currentPage + 1)+"'>下一页</a></li>";
    }

    <!-- 最后一页 -->
    if(currentPage==totalPage) {
        pageDiv += "<li><a href='javascript:void(0)' disabled='true' style='color:black;font-weight:bold;'>末页</a></li>";
    }else{
        pageDiv += "<li><a href='javascript:void(0)' onclick='PageTool.pageSkip(this)' currentPage='"+(totalPage)+"'>末页</a></li>";
    }

    pageDiv +="&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;当前第 "+(currentPage)+" 页," +
        "显示第 "+((currentPage-1)*limit+1)+" 到 "+((currentPage-1)*limit+size)+" 条记录" +
        "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 总共 "+totalPage+" 页,总共 "+totalCount+" 条记录数," +
        "可直接搜索第<input value='"+currentPage+"' id='pageNow' style='width:40px;text-align:center;'/> 页" +
        "<input type='button' class='btn btn-primary' currentPage='button' onclick='PageTool.pageSkip(this)' value='点击跳转'></ul>";

    $("#pageDiv").append(pageDiv);
    // alert(pageDiv);
}



/**
 * 点击指定页码刷新数据
 */
PageTool.pageSkip =function(obj) {

    //获取设置当前页码
    var currentPage = obj.attributes["currentPage"].nodeValue;

    //如果是直接指定页码跳转的请求,则要从input标签中获取值
    if(currentPage == "button"){
        currentPage = $("#pageNow").val();
    }

    //如果最终没有值,设置默认值为1
    if(!currentPage){
        currentPage = 1;
    }

    //获取调用者的名称,并转换成对象
    if(!PageTool.callerName ||PageTool.callerName==''){
        Feng.error("请按paging.js开头的说明,设置对象的名称");
        return false;
    }
    //根据对象名称获取对象
    var caller = window[PageTool.callerName];
    if(!caller || typeof(caller)!='object' ){
        Feng.error("您设置的对象名称无法转换成对象,请阅读paging.js文件的使用说明");
        return false;
    }

    var queryData = caller.formParams();
    queryData['currentPage'] = currentPage;//当前再第几页
    //刷新table数据
    caller.table.refresh({query: queryData});

    //当前方法被写死了,必须动态获取到
    // 先获取公共的参数
    // var queryData = Category.formParams();
    //
    // //添加私有参数
    // queryData['currentPage'] = currentPage;//当前再第几页
    //
    // //刷新table数据
    // Category.table.refresh({query: queryData});

}

 

 

 

(11)GlobalExceptionHandler.java

/**
 * Copyright 2018-2020 stylefeng & fengshuonan (https://gitee.com/stylefeng)
 * <p>
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package cn.stylefeng.guns.core.aop;

import cn.stylefeng.guns.core.common.exception.BizExceptionEnum;
import cn.stylefeng.guns.core.common.exception.InvalidKaptchaException;
import cn.stylefeng.guns.core.log.LogManager;
import cn.stylefeng.guns.core.log.factory.LogTaskFactory;
import cn.stylefeng.guns.core.shiro.ShiroKit;
import cn.stylefeng.guns.elephish.utils.StringUtil;
import cn.stylefeng.roses.core.reqres.response.ErrorResponseData;
import cn.stylefeng.roses.kernel.model.exception.ServiceException;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.CredentialsException;
import org.apache.shiro.authc.DisabledAccountException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.http.HttpStatus;
import org.springframework.ui.Model;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;

import java.lang.reflect.UndeclaredThrowableException;
import java.sql.SQLIntegrityConstraintViolationException;
import java.util.List;

import static cn.stylefeng.roses.core.util.HttpContext.getIp;
import static cn.stylefeng.roses.core.util.HttpContext.getRequest;

/**
 * 全局的的异常拦截器(拦截所有的控制器)(带有@RequestMapping注解的方法上都会拦截)
 *
 * @author fengshuonan
 * @date 2016年11月12日 下午3:19:56
 */
@ControllerAdvice
@Order(-1)
public class GlobalExceptionHandler {

    private Logger log = LoggerFactory.getLogger(this.getClass());

    /**
     * 拦截业务异常
     */
    @ExceptionHandler(ServiceException.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ResponseBody
    public ErrorResponseData bussiness(ServiceException e) {
        LogManager.me().executeLog(LogTaskFactory.exceptionLog(ShiroKit.getUser().getId(), e));
        getRequest().setAttribute("tip", e.getMessage());
        log.error("业务异常:", e);
        return new ErrorResponseData(e.getCode(), e.getMessage());
    }

    /**
     * 拦截数据绑定异常
     * @param e
     * @return
     */
    @ExceptionHandler(BindException.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ResponseBody
    public ErrorResponseData bindException(BindException e) {
        //添加错误日志
        LogManager.me().executeLog(LogTaskFactory.exceptionLog(ShiroKit.getUser().getId(), e));

        BindingResult result = e.getBindingResult();

        String errorMsg = "数据绑定异常";
        if (result.hasErrors()) {
            errorMsg = result.getFieldErrors().get(0).getDefaultMessage();
        }

        getRequest().setAttribute("tip", errorMsg);
        log.error("数据绑定异常:", errorMsg);

        return new ErrorResponseData(BizExceptionEnum.SERVER_ERROR.getCode(),errorMsg);
    }








    /**
     * 用户未登录异常
     */
    @ExceptionHandler(AuthenticationException.class)
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    public String unAuth(AuthenticationException e) {
        log.error("用户未登陆:", e);
        return "/login.html";
    }

    /**
     * 账号被冻结异常
     */
    @ExceptionHandler(DisabledAccountException.class)
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    public String accountLocked(DisabledAccountException e, Model model) {
        String username = getRequest().getParameter("username");
        LogManager.me().executeLog(LogTaskFactory.loginLog(username, "账号被冻结", getIp()));
        model.addAttribute("tips", "账号被冻结");
        return "/login.html";
    }

    /**
     * 账号密码错误异常
     */
    @ExceptionHandler(CredentialsException.class)
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    public String credentials(CredentialsException e, Model model) {
        String username = getRequest().getParameter("username");
        LogManager.me().executeLog(LogTaskFactory.loginLog(username, "账号密码错误", getIp()));
        model.addAttribute("tips", "账号密码错误");
        return "/login.html";
    }

    /**
     * 验证码错误异常
     */
    @ExceptionHandler(InvalidKaptchaException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public String credentials(InvalidKaptchaException e, Model model) {
        String username = getRequest().getParameter("username");
        LogManager.me().executeLog(LogTaskFactory.loginLog(username, "验证码错误", getIp()));
        model.addAttribute("tips", "验证码错误");
        return "/login.html";
    }

    /**
     * 无权访问该资源异常
     */
    @ExceptionHandler(UndeclaredThrowableException.class)
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    @ResponseBody
    public ErrorResponseData credentials(UndeclaredThrowableException e) {
        getRequest().setAttribute("tip", "权限异常");
        log.error("权限异常!", e);
        return new ErrorResponseData(BizExceptionEnum.NO_PERMITION.getCode(), BizExceptionEnum.NO_PERMITION.getMessage());
    }




    /**
     * 拦截未知的运行时异常
     */
    @ExceptionHandler(RuntimeException.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ResponseBody
    public ErrorResponseData notFount(RuntimeException e) {
        LogManager.me().executeLog(LogTaskFactory.exceptionLog(ShiroKit.getUser().getId(), e));
        getRequest().setAttribute("tip", "服务器未知运行时异常");
        log.error("运行时异常:", e);
        return new ErrorResponseData(BizExceptionEnum.SERVER_ERROR.getCode(), e.getMessage());
    }
}

 

(12)common.js

var CommonTool={}

/**
 * 判断是字符串是否为null,'undifined',空串,空格 如果是,返回true;
 * 判断对象是否为空,如果为空对象或集合,返回true
 *
 * @param value
 * @returns {boolean}
 */
CommonTool.isEmpty=function (value) {

    //判断是否是undefined和""
    if(!value){
        return true;
    }

    //如果是字符串,还要去掉首尾空格后判断长度是否为0
    if(typeof(value) == 'string' && value.trim().length==0){
        return true;
    }


    //如果是对象
    if(typeof(value) == 'object' || JSON.stringify(value) === '{}'){
        return true;
    }
    return false;

}


/**
 * 收集请求参数的工具类,每一个字段的值只能是字符串或数字,不能为对象或数组
 * @param fieldArr 定义一个数组,封装所有收据数据的字段,必传
 * @param needArr 定义一个数组,封装字段是否是必传字段,注意必须与 fieldArr中对应字段的角标一致,必传
 * @param fieldNameArr 定义一个数组,封装对应字段的字段名称,可以为null
 * @param typeArr 定义一个数组,用于判断获取的字段的值的是字符串还是数字,如果是字符串,就默认不判断 。为''时不判断
 * @param strMaxLengthArr 定义数组,指定字符串类型的字段的值的最大长度。<=0 ,表示该字段不用比较
 * @returns 返回收集到的数据或者错误信息
 */
CommonTool.collectData = function (fieldArr,needArr,fieldNameArr,typeArr,strMaxLengthArr) {
    var result = {} ;

    //如果这三个数组中有空数组,直接返回空map
    if(!fieldArr ||!needArr || !fieldNameArr){
        return result;
    }

    //收集数据
    for(var i = 0; i<fieldArr.length;i++){
        var key = fieldArr[i];

        //获取对话框中的值
        var value = $("#" + key).val();

        var fileName =fieldNameArr[i];

        //判断是否为null
        if(value){
            //判断是否是数组或对象,如果是,则返回错误信息
            if(typeof (value)!='string'){
                return "【"+fileName+"】只能是字符串或数字,不能是【"+typeof (value)+"】";
            }
            //去除首尾空格
            value =value.trim();
        }

        //如果该字段必传,但是没有传值,则前端提示错误信息,并终止后面的操作
        if(needArr && needArr[i] && !value){
            //将异常返回出去
            return "【"+fileName+"】不能为空!";
        }

        //如果走到这里了,发现还是空串,直接进入下一个循环
        if(!value){
            continue;
        }


        //判断是否需要判断类型,如果是,则判断类型是否正确,

        if(typeArr && typeArr[i]){

            var type = typeArr[i];

            //判断是否是数字
            if(type === 'number' && isNaN(value)){
                return "【"+fileName+"】只能是数字!";
            }

            //判断是否是大写字母
            if(type === 'capitalLetter' && !(value>='A' && value<='Z')){
                return "【"+fileName+"】只能是单个大写字母!";
            }

        }

        //判断长度
        if(strMaxLengthArr && strMaxLengthArr[i]  ){

            var maxLength = strMaxLengthArr[i];
            if( maxLength>0 && value.length>maxLength){
                return "【"+fileName+"】长度不能超过 "+maxLength+"";
            }


        }

        result[key] = value;
    }

    return result;

}

/**
 * 对数组中的元素进行笛卡尔积
 *  如: CommonTool.calcDescartes([[1,2,3],['a','b','c'],['X','Y','Z']])
 *  结果:返回一个数组,容量为27 ,结果如下:
 *  [[1,"a","X"],[1,"a","Y"],[1,"a","Z"],[1,"b","X"],[1,"b","Y"],[1,"b","Z"],
 *  [1,"c","X"],[1,"c","Y"],[1,"c","Z"],[2,"a","X"],[2,"a","Y"],[2,"a","Z"],
 *  [2,"b","X"],[2,"b","Y"],[2,"b","Z"],[2,"c","X"],[2,"c","Y"],[2,"c","Z"],
 *  [3,"a","X"],[3,"a","Y"],[3,"a","Z"],[3,"b","X"],[3,"b","Y"],[3,"b","Z"],
 *  [3,"c","X"],[3,"c","Y"],[3,"c","Z"]]
 * @param array
 * @returns {*}
 */
CommonTool.calcDescartes=function (array) {
    if (array.length < 2) {
        return array[0] || [];
    }

    return [].reduce.call(array, function (col, set) {
        var res = [];
        col.forEach(function (c) {
            set.forEach(function (s) {
                var t = [].concat(Array.isArray(c) ? c : [c]);
                t.push(s);
                res.push(t);
            })
        });
        return res;
    });
}

 

 

(13)_container.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="renderer" content="webkit"/><!-- 让360浏览器默认选择webkit内核 -->

    <!-- 全局css -->
    <link rel="shortcut icon" href="${ctxPath}/static/favicon.ico">
    <link href="${ctxPath}/static/css/bootstrap.min.css?v=3.3.6" rel="stylesheet">
    <link href="${ctxPath}/static/css/font-awesome.css?v=4.4.0" rel="stylesheet">
    <link href="${ctxPath}/static/css/plugins/chosen/chosen.css" rel="stylesheet">
    <link href="${ctxPath}/static/css/plugins/bootstrap-table/bootstrap-table.min.css" rel="stylesheet">
    <link href="${ctxPath}/static/css/plugins/validate/bootstrapValidator.min.css" rel="stylesheet">
    <link href="${ctxPath}/static/css/style.css?v=4.1.0" rel="stylesheet">
    <link href="${ctxPath}/static/css/_fstyle.css" rel="stylesheet">
    <link href="${ctxPath}/static/css/plugins/iCheck/custom.css" rel="stylesheet">
    <link href="${ctxPath}/static/css/plugins/webuploader/webuploader.css?j=${date().time}" rel="stylesheet">
    <link href="${ctxPath}/static/css/plugins/ztree/zTreeStyle.css" rel="stylesheet">
    <link href="${ctxPath}/static/css/plugins/bootstrap-treetable/bootstrap-treetable.css" rel="stylesheet"/>


    <!-- 全局js -->
    <script src="${ctxPath}/static/js/jquery.min.js?v=2.1.4"></script>
    <script src="${ctxPath}/static/js/bootstrap.min.js?v=3.3.6"></script>
        <!-- 时间操作相关的common.js -->
    <script src="${ctxPath}/static/modular/elephish/common/moment.js"></script>
    <script src="${ctxPath}/static/modular/elephish/common/common.js?j=${date().time}"></script>
    <script src="${ctxPath}/static/js/plugins/ztree/jquery.ztree.all.min.js"></script>
    <script src="${ctxPath}/static/js/plugins/bootstrap-table/bootstrap-table.min.js"></script>
    <script src="${ctxPath}/static/js/plugins/validate/bootstrapValidator.min.js"></script>
    <script src="${ctxPath}/static/js/plugins/validate/zh_CN.js"></script>
    <script src="${ctxPath}/static/js/plugins/bootstrap-table/bootstrap-table-mobile.min.js"></script>
    <script src="${ctxPath}/static/js/plugins/bootstrap-table/locale/bootstrap-table-zh-CN.min.js"></script>
    <script src="${ctxPath}/static/js/plugins/bootstrap-treetable/bootstrap-treetable.js?j=${date().time}"></script>
    <script src="${ctxPath}/static/js/plugins/layer/layer.js"></script>
    <script src="${ctxPath}/static/js/plugins/chosen/chosen.jquery.js"></script>
    <script src="${ctxPath}/static/js/plugins/iCheck/icheck.min.js"></script>
    <script src="${ctxPath}/static/js/plugins/laydate/laydate.js"></script>
    <!--<script src="${ctxPath}/static/js/plugins/webuploader/webuploader.min.js"></script>-->
    <script src="${ctxPath}/static/js/plugins/webuploader/webuploader.js?j=${date().time}"></script>
    <script src="${ctxPath}/static/js/common/ajax-object.js"></script>
    <script src="${ctxPath}/static/js/common/bootstrap-table-object.js?j=${date().time}"></script>
    <script src="${ctxPath}/static/js/common/tree-table-object.js?j=${date().time}"></script>
    <script src="${ctxPath}/static/js/common/web-upload-object.js?j=${date().time}"></script>
    <script src="${ctxPath}/static/js/common/ztree-object.js"></script>
    <script src="${ctxPath}/static/js/common/Feng.js"></script>




    <style type="text/css">
        table{  
            width:100px;  
            table-layout:fixed;/* 只有定义了表格的布局算法为fixed,下面td的定义才能起作用。 */  
        }
        td{  
            width:100%;  
            word-break:keep-all;/* 不换行 */  
            white-space:nowrap;/* 不换行 */  
            overflow:hidden;/* 内容超出宽度时隐藏超出部分的内容 */  
            text-overflow:ellipsis;/* 当对象内文本溢出时显示省略标记(...) ;需与overflow:hidden;一起使用*/  
        }
    </style>

    <script type="text/javascript">
        Feng.addCtx("${ctxPath}");
        Feng.sessionTimeoutRegistry();
    </script>
</head>

<body class="gray-bg">
<div class="wrapper wrapper-content">
    ${layoutContent}
</div>
<script src="${ctxPath}/static/js/content.js?v=1.0.0"></script>
</body>
</html>

 

至此,商品分类管理的添加功能实现。

 

该系列更多文章请前往 Guns二次开发目录

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值