EASY spa后端文档

1.1.导入运行

  1. 导入sql脚本到MySQL中,sql位于src/main/resources/sql中;
  2. 使用IDEA打开项目,检查application-dev.yml中数据库账号密码是否正确;
  3. 运行src目录下的EasyWebApplication.java运行项目:

运行教程

1.2.项目结构

src/main
 |-> java/com.xxx
 |    |-> common                           // 框架平台
 |    |    |-> core
 |    |    |    |-> config                 // SpringBoot配置类
 |    |    |    |-> exception              // 全局异常处理、自定义异常
 |    |    |    |-> security               // SpringSecurity配置
 |    |    |    |-> utils                  // 工具类
 |    |    |    |-> annotation             // 自定义注解
 |    |    |    |-> aspect                 // 自定义AOP处理
 |    |    |    |-> web                    // 常用核心类, JsonResult、PageParam等
 |    |    |    |-> Constants.java         // 系统配置常量
 |    |    |-> system                      // 系统管理模块
 |    |         |-> controller             // 控制器
 |    |         |-> entity                 // 实体类
 |    |         |-> mapper、xml            // mapper和xml
 |    |         |-> service、impl          // service和seviceImpl
 |    |-> ***.***                          // 用户自己的业务模块
 |    |    |-> controller
 |    |    |-> ......
 |    |-> EasyWebApplication.java          // 启动类
 |-> resources
      |-> static                           // 静态资源文件
      |-> templates                        // 页面模板文件
      |-> application.yml                  // SpringBoot配置文件
      |-> application-dev.yml              // 开发环境配置
      |-> application-prod.yml             // 生产环境配置
      |-> application-test.yml             // 测试环境配置

项目为单体SpringBoot项目,自己的业务模块应该放在common平级,common下面应该全部是框架公用的部分,便于框架升级。 用户业务模块功能较多的项目可以继续分层,比如wms/order(订单模块)、wms/plan(生产计划模块)等,下面再是controller等, 也可以直接wms下面就是controller,wms表示仓库管理系统。

1.3.所用框架

框架说明
核心框架Spring、SpringBoot、SpringMVC
持久层MyBatis、MyBatisPlus、Druid
权限框架SpringSecurity、jjwt
其他EasyCaptchaHuTool

1.4.数据库表

用户表(sys_user):

字段类型说明
user_idint用户id
usernamevarchar账号
passwordvarchar密码
nick_namevarchar昵称
avatarvarchar头像
sexint性别
phonevarchar手机号
emailvarchar邮箱
email_verifiedint邮箱是否验证,0否,1是
true_namevarchar真实姓名
id_cardvarchar身份证号
birthdaydate出生日期
introductionvarchar个人简介
organization_idint机构id
stateint状态,0正常,1冻结
deletedint是否删除,0否,1是
create_timetimestamp注册时间
update_timetimestamp修改时间

角色表(sys_role):

字段类型说明
role_idint角色id
role_namevarchar角色名称
role_codevarchar角色标识
commentsvarchar备注
deletedint是否删除,0否,1是
create_timetimestamp创建时间
update_timetimestamp修改时间

用户角色关联表(sys_user_role):

字段类型说明
idint主键
user_idint用户id
role_idint角色id
create_timetimestamp创建时间
update_timetimestamp修改时间

菜单表(sys_menu):

字段类型说明
menu_idint菜单id
parent_idint上级id,0是顶级
menu_namevarchar菜单名称
menu_iconvarchar菜单图标
pathvarchar菜单路由关键字
componentvarchar菜单组件地址
menu_typeint类型,0菜单,1按钮
sort_numberint排序号
authorityvarchar权限标识
targetvarchar打开位置
icon_colorvarchar菜单图标颜色
hideint是否隐藏,0否,1是
deletedint是否删除,0否,1是
create_timetimestamp创建时间
update_timetimestamp修改时间

角色菜单关联表(sys_role_menu):

字段类型说明
idint主键
role_idint角色id
menu_idint菜单id
create_timetimestamp创建时间
update_timetimestamp修改时间

组织机构表(sys_organization):

字段类型说明
organization_idint机构id
parent_idint上级id,0是顶级
organization_namevarchar机构名称
organization_full_namevarchar机构名称
organization_typeint机构类型
leader_idint负责人id
sort_numberint排序号
commentsvarchar备注
deletedint是否删除,0否,1是
create_timetimestamp创建时间
update_timetimestamp修改时间

数据字典表(sys_dictionary):

字段类型说明
dict_idint字典id
dict_codevarchar字典标识
dict_namevarchar字典名称
commentsvarchar备注
sort_numberint排序号
deletedint是否删除,0否,1是
create_timetimestamp创建时间
update_timetimestamp修改时间

数据字典项表(sys_dictionary_data):

字段类型说明
dict_data_idint字典项id
dict_idint字典id
dict_data_codeint字典项标识
dict_data_nameint字典项名称
commentsvarchar备注
sort_numberint排序号
deletedint是否删除,0否,1是
create_timetimestamp创建时间
update_timetimestamp修改时间

登录日志表(sys_login_record):

字段类型说明
idint主键
usernamevarchar用户账号
osvarchar操作系统
devicevarchar设备名
browservarchar浏览器类型
ipvarcharip地址
oper_typeint操作类型,0登录成功,1登录失败,2退出登录,3刷新token
commentsvarchar备注
create_timetimestamp登录时间
update_timetimestamp修改时间

操作日志表(sys_oper_record):

字段类型说明
idint主键
user_idint用户id
modelvarchar操作模块
descriptionvarchar操作方法
urlvarchar请求地址
request_methodvarchar请求方式
oper_methodvarchar调用方法
paramvarchar请求参数
resultvarchar返回结果
ipvarcharip地址
spend_timeint请求耗时,单位毫秒
stateint状态,0成功,1异常
commentsvarchar备注
create_timetimestamp登录时间
update_timetimestamp修改时间

 

2.1.返回结果JsonResult

常用核心类都位于com.xxx.common.core.web包下面,使用方法:

@RestController
public class TestController {
    @RequestMapping("/test")
    public JsonResult test() {
        return JsonResult.ok();
    }
}

这样返回给前端的json为 {"code": 0, "msg": "操作成功"} ,更多用法:

JsonResult.ok(); 
JsonResult.ok("添加成功");           // 重写msg
JsonResult.ok(200, "添加成功");      // 重写code和msg

JsonResult.error();                  // {"code": 1, "msg": "操作失败"}
JsonResult.error("添加失败");
JsonResult.error(500, "添加失败");

JsonResult.ok().setData(user);      // {"code": 0, "msg": "操作成功", "data": {}}

// put其他数据,支持链式写法,无限put,而非仅仅局限code、msg、data三个字段
JsonResult.ok().put("token", token);
JsonResult.ok().put("data1", object).put("data2", list);

返回json数据的接口都应该返回JsonResult对象,因为它包含了code、msg等字段便于前端统一处理状态。

 

2.2.分页结果PageResult

PageResult是针对返回给数据表格数据的封装,使用方法:

@RestController
public class TestController {
    @RequestMapping("/test")
    public PageResult<User> test() {
        return new PageResult<>(userList, total);
    }
}

这样返回给前端的json为 {"code": 0, "msg": "", "count": 100, "data": [] },count是总数量,data是当前页数据,更多写法:

new PageResult<>(userList);            // 这样写count为userList的大小
new PageResult<>(userList, total);     // 参数分别是data、count
new PageResult<>(0, userList, total);  // 参数分别是code、data、count

// 设置msg
PageResult<User> pageResult = new PageResult<>(userList, total).setMsg("查询成功");

数据表格都是需要后端返回code、count、data等几个关键字段的,PageResult就是针对返回给数据表格数据的封装。

 

2.3.批量修改BatchParam

BatchParam用于一行代码实现批量修改,使用方法:

@RestController
public class TestController {
    @PutMapping("/updateBatch")
    public JsonResult updateBatch(@RequestBody BatchParam<User> batchParam) {
        if (batchParam.update(userService, "user_id")) {
            return JsonResult.ok();
        }
        return JsonResult.error();
    }
}

这样就完成了对用户的批量修改,前端只需要传递规定参数即可:

var param = {ids: [1, 2, 3], data: {sex: '女', age: 18}};
$.ajax({
    url: 'updateBatch',
    data: JSON.stringify(param),
    type: 'PUT',
    dataType: 'json',
    contentType: 'application/json;charset=UTF-8',
    success: function(res) {
        console.log(res);
    }
});

这样就把id为1、2、3的用户性别修改成女、年龄修改为18,BatchParam需要接收两个字段:

  • ids   数组类型,需要修改的数据主键的值
  • data   Object类型,需要修改的字段和值

batchParam的update(userService, "user_id")方法:

  • 参数一   对应的service的实例
  • 参数二   数据库中主键的字段名称

BatchParam也可以用在批量修改密码、状态等特殊场景:

@RestController
public class TestController {
    @PutMapping("/updateBatch")
    public JsonResult updateBatch(@RequestBody BatchParam<User> batchParam) {
        // 修改密码需要对前端传递的值进行处理
        User user = batchParam.getData();  // BatchParam内部使用泛型,所以你可以直接获取到对应的对象
        user.setPassword(MD5(user.getPassword()));  // 修改前端传递的值
        if (batchParam.update(userService, "user_id")) {
            return JsonResult.ok();
        }
        return JsonResult.error();
    }
}

 

2.4.基类BaseController

方法列表:

方法参数说明
getLoginUser()获取当前登录的User
getLoginUserId()获取当前登录的userId

使用时Controller继承BaseController即可。

 

2.5.配置常量Constants

位于com.xxx.common.core.Constants.java,用于配置一些常量:

public class Constants {
    /* 文件服务器配置 */
    public static final String UPLOAD_DIR = File.listRoots()[0] + "/upload/";  // 上传的目录
    public static final boolean UPLOAD_UUID_NAME = false;  // 文件上传是否用uuid命名
    // OpenOffice在不同操作系统上的安装路径
    public static final String OPEN_OFFICE_PATH_WINDOWS = File.listRoots()[0] + "/OpenOffice";
    public static final String OPEN_OFFICE_PATH_LINUX = "/opt/openoffice.org3";
    public static final String OPEN_OFFICE_PATH_MAC = "/Applications/OpenOffice.org.app/Contents/";

    /* 返回结果统一 */
    public static final int RESULT_OK_CODE = 0;  // 默认成功码
    public static final int RESULT_ERROR_CODE = 1;  // 默认失败码

    /* 其他 */
    public static final Long TOKEN_EXPIRE_TIME = 60 * 60 * 24L;  // token过期时间,单位秒
    public static final int TOKEN_WILL_EXPIRE = 30;  // token将要过期自动刷新,单位分钟
    public static final String TOKEN_KEY = "ULgNsWJ8rPjRtnjzX/Gv2RGS80Ksnm/ZaLpvIL+NrBg=";  // 生成token的密钥
}

根据自己的项目需求进行修改即可。

生成token的密钥建议每个项目都不一样,密钥不要自己随意指定,使用JwtUtil.genKeyStr()来生成唯一的,你可以直接运行项目中写好的测试类src/main/test/TestMain.java来获取。

 

2.6.日志注解@OperLog

加注解com.xxx.common.core.annotation.OperLog.java可实现操作日志记录,加在方法上:

@OperLog(value = "角色管理", desc = "分页查询")
@OperLog(value = "角色管理", desc = "添加", param = false, result = true)
@OperLog(value = "角色管理", desc = "删除", result = true)
  • value     操作模块
  • desc      操作功能
  • param   是否记录请求参数,默认true
  • result     是否记录返回结果,默认为false

一般建议查询操作记录参数不记录结果,添加、修改操作记录结果不记录参数,删除操作参数和结果都记录, 因为查询操作返回结果太大、添加和修改操作请求参数太大如果记录太占资源, 注解实现操作日志记录是通过AOP切面实现的,位于com.xxx.common.core.aspect.OperLogAspect.java

 

2.7.swagger常用注解

// 这个注解是框架增加的,用在分页查询接口上,在com.xxx.common.core.annotation包下
@ApiPageParam

// 参数说明注解
@ApiImplicitParams({
    @ApiImplicitParam(name = "oldPsw", value = "旧密码", required = true, dataType = "string"),
    @ApiImplicitParam(name = "newPsw", value = "新密码", required = true, dataType = "string")
})

// 接口说明注解
@ApiOperation("修改自己密码")

// 这个加在controller类上面
@Api(tags = "用户管理")

// 这个加在实体类上面
@ApiModel(description = "角色")

// 这个加在实体类属性上面
@ApiModelProperty("角色id")

@ApiPageParam这个注解包含了pagelimitsortorder这几个参数。

 

3.1.快速使用

PageParam继承至MyBatisPlus的Page,用于自动接收前端传递的分页、排序、搜索等参数,可以自动完成分页、排序、搜索的功能。

基本用法:

@RestController
public class UserController {
    @GetMapping("/user/page")
    public PageResult<User> page(HttpServletRequest request) {
        PageParam<User> pageParam = new PageParam<>(request);
        List<User> records = userService.page(pageParam, pageParam.getWrapper()).getRecords();
        return new PageResult<>(records, pageParam.getTotal());
    }
}

这样就实现了分页、排序、模糊搜索功能,前端只要传递规定的参数即可:

参数含义
user/page?page=1&limit=10查询第一页,每页10条
user/page?page=2&limit=20查询第二页,每页20条
user/page?sort=sex&order=asc根据sex字段升序
user/page?sort=sex&order=desc根据sex字段降序
user/page?sex=女查询sex为女的用户
user/page?username=admin&sex=女查询username为admin并且sex为女的用户

 

3.2.全部方法

构造方法:

@RestController
public class UserController {
    @GetMapping("/user/page")
    public PageResult<User> page(HttpServletRequest request) {
        // 在new的时候传递request会直接获取前端参数构建PageParam
        PageParam<User> pageParam = new PageParam<>(request);

        // 不在new的时候构建
        PageParam<User> pageParam = new PageParam<>();
        pageParam.setNeedToLine(false);  // 设置是否需要把驼峰转成下划线格式
        pageParam.init(request);  // 调用init方法开始构建
    }
}

其他方法:

方法参数说明
Map getPageData()获取除分页、排序外的其他参数
setDefaultOrder(ascs, descs)两个数组设置默认排序方式
addOrderAsc(String... ascs)ascs在前端的排序参数基础上增加升序字段
addOrderDesc(String... descs)descs在前端的排序参数基础上增加降序字段
   
getWrapper(String... excludes)排除的字段构建查询条件QueryWrapper
getWrapperWith(String... columns)包含的字段构建查询条件QueryWrapper
getOrderWrapper()构建QueryWrapper包含排序
   
put(String key, Object value)key,value往PageData里加参数
Object get(String key)key从PageData中取参数
getString(String key)key从PageData中取String参数
getInt(String key)key从PageData中取Integer参数
getLong(String key)key从PageData中取Long参数
getFloat(String key)key从PageData中取Float参数
getDouble(String key)key从PageData中取Double参数
getBoolean(String key)key从PageData中取Boolean参数
   
Map getNoPageParam()获取除分页的参数用于查询全部
getOne(List records)records获取集合第一条数据
sortRecords(List records)records用代码排序集合

 

3.3.单表使用场景

单表只用写controller不用写service、mapper:

@RestController
public class RoleController {
    /** 分页查询 */
    @GetMapping("/page")
    public PageResult<Role> page(HttpServletRequest request) {
        PageParam<Role> pageParam = new PageParam<>(request);
        List<Role> records = roleService.page(pageParam, pageParam.getWrapper()).getRecords();
        return new PageResult<>(records, pageParam.getTotal());
    }

    /** 查询全部 */
    @GetMapping("/list")
    public JsonResult list(HttpServletRequest request) {
        PageParam<Role> pageParam = new PageParam<>(request);
        List<Role> records = roleService.list(pageParam.getOrderWrapper());
        return JsonResult.ok().setData(records);
    }
}

上述这样写不管是分页查询还是查询全部都支持前端按规则传递的排序和模糊搜索参数。 查询全部需要调用getOrderWrapper()是因为getWrapper()只包含模糊检索不包含排序, service的page方法会自己根据pageParam对象来排序,而list方法不会,所以list需要在QueryWrapper中排序。

 

3.4.多表使用场景

多表查询需要配合自己写service、mapper、sql来使用:

@RestController
public class UserController {
    /** 分页查询 */
    @GetMapping("/page")
    public PageResult<User> page(HttpServletRequest request) {
        PageParam<User> pageParam = new PageParam<>(request);
        return userService.listPage(pageParam);
    }
    /** 查询全部 */
    @GetMapping("/list")
    public JsonResult list(HttpServletRequest request) {
        /* 这里用了pageParam.getNoPageParam获取除分页的参数,
           用了pageParam.sortRecords方法来排序,后面会详细讲 */
        PageParam<User> pageParam = new PageParam<>(request);
        List<User> records = userService.listAll(pageParam.getNoPageParam());
        return JsonResult.ok().setData(pageParam.sortRecords(records));
    }
    /** 根据id查询 */
    @GetMapping("/get")
    public JsonResult get(Integer id) {
        /* 根据id查询直接调用listAll方法最后调用pageParam.getOne */
        PageParam<User> pageParam = new PageParam<>();
        pageParam.put("userId", id);
        List<User> records = userService.listAll(pageParam.getNoPageParam());
        return JsonResult.ok().setData(pageParam.getOne(records));
    }
}

listPage和listAll这个是自己在service中添加的方法:

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
    @Override
    public PageResult<User> listPage(PageParam page) {
        List<User> records = baseMapper.listPage(page);
        return new PageResult<>(records, page.getTotal());
    }
    @Override
    public List<User> listAll(Map page) {
        return baseMapper.listAll(page);
    }
}

mapper的写法:

public interface UserMapper extends BaseMapper<User> {
    /** 分页查询,需要注意一定要加@Param("page") */
    List<User> listPage(@Param("page") PageParam page);

    /** 查询全部,这里也加@Param("page")是为了xml中共用一个sql */
    List<User> listAll(@Param("page") Map page);
}
<mapper namespace="com.xxx.common.system.mapper.UserMapper">
    <!-- 关联查询sql -->
    <sql id="relSelect">
        SELECT a.*, b.organization_name
        FROM sys_user a
        LEFT JOIN sys_organization b ON a.organization_id = b.organization_id
        <where>
            <if test="page.pageData.userId!=null">
                AND a.user_id = #{page.pageData.userId}
            </if>
            <if test="page.pageData.username!=null">
                AND a.username LIKE CONCAT('%', #{page.pageData.username}, '%')
            </if>
            <if test="page.pageData.sex!=null">
                AND a.sex = #{page.pageData.sex}
            </if>
            <if test="page.pageData.organizationName!=null">
                AND b.organization_name LIKE CONCAT('%', #{page.pageData.organizationName}, '%')
            </if>
        </where>
    </sql>
    <!-- 分页查询 -->
    <select id="listPage" resultType="com.xxx.common.system.entity.User">
        <include refid="relSelect"></include>
    </select>
    <!-- 查询全部 -->
    <select id="listAll" resultType="com.xxx.common.system.entity.User">
        <include refid="relSelect"></include>
    </select>
</mapper>

因为PageParam继承MyBatisPlus的Page,所以mapper里面只要有page(PageParam)参数就会自动分页和排序, 你只需要在xml中写关联的sql和模糊搜索即可,模糊搜索从page.pageData中取前端传递的参数, 查询全部就不能使用PageParam了,要使用Map,这个Map是在controller中通过pageParam.getNoPageParam()获取的, Map的结构与pageParam对象一致,便于查询全部和分页查询共用一个sql,查询全部MyBatsiPlus框架不会自动排序, 所以在controller中调用了pageParam.sortRecords(records)来进行排序,这个方法是使用stream方法进行排序的, 你也可以在xml中使用sql形式的排序:

<!-- 查询全部 -->
<select id="listAll" resultType="com.xxx.common.system.entity.User">
    <include refid="relSelect"></include>
    <if test="page!=null and page.orders!=null">
        order by
        <foreach collection="page.orders" item="item" separator=",">
            #{item.column} ${item.asc?'asc':'desc'}
        </foreach>
    </if>
</select>

 

3.5.几个重要方法

设置默认排序方式:

public class Test {
    public void test(HttpServletRequest request) {
        PageParam<User> pageParam = new PageParam<>(request);

        // 当前端没有传递排序参数时根据createTime升序
        pageParam.setDefaultOrder(new String[]{"createTime"}, null);

        // 当前端没有传递排序参数时根据createTime降序
        pageParam.setDefaultOrder(null, new String[]{"createTime"});

        pageParam.setDefaultOrder(new String[]{}, new String[]{});

        service.page(pageParam, pageParam.getWrapper()).getRecords();
    }
}

构建查询条件:

// 使用除分页、排序外的参数构建QueryWrapper
QueryWrapper<User> wrapper = pageParam.getWrapper();

// 排除一些字段,比如有些字段想自己处理,构建的大部分是like,你可以排除自己用eq
QueryWrapper<User> wrapper = pageParam.getWrapper("age", "sex");
wrapper.eq("age", pageParam.get("age"));
// 还比如有些时间你可以排除然后自己用大于小于

// 只包含一些字段,只用指定的字段构建QueryWrapper
QueryWrapper<User> wrapper = pageParam.getWrapperWith("age", "sex");

// 构建QueryWrapper包含排序,可用于list方法不分页查询全部
QueryWrapper<User> wrapper = pageParam.getOrderWrapper();

// 加一个null参数表示构建的QueryWrapper只有排序,没有模糊搜索的功能
QueryWrapper<User> wrapper = pageParam.getOrderWrapper(null);

getWrapper()方法内部也做了一些特殊的处理:

  • "deleted"、"access_token"这两个参数会直接过滤掉
  • "id"、"sortNumber"、"state"、以"Id"结尾的这些用eq构建,其他用like构建
  • 前端还可以传递"createTimeStart"和"createTimeEnd"用于对"createTime"字段做范围筛选

getNoPageParam()方法:

Map map = pageParam.getNoPageParam();

这个方法是用来做关联查询全部获取除了分页外的参数,它获取的Map的结构与PageParam的结构一致, 便于xml里面查询全部和分页查询共用一个sql,Map里面包含pageData(Map)和orders(List)。

 

4.1.单表CRUD

像角色role这样位于最顶层、不需要关联查询的表,只用写controller就可以实现CRUD:

@RestController
@RequestMapping("/sys/role")
public class RoleController {
    @Autowired
    private RoleService roleService;

    /** 分页查询、排序、模糊搜索 */
    @GetMapping("/page")
    public PageResult<Role> page(HttpServletRequest request) {
        PageParam pageParam = new PageParam(request);
        pageParam.setDefaultOrder(new String[]{"createTime"}, null);
        List<Role> records = roleService.page(pageParam, pageParam.getQueryWrapper()).getRecords();
        return new PageResult<>(records, pageParam.getTotal());
    }

    /** 查询全部、排序、模糊搜索 */
    @GetMapping()
    public JsonResult list(HttpServletRequest request) {
        PageParam pageParam = new PageParam(request);
        List<Role> records = roleService.list(pageParam, pageParam.getOrderWrapper());
        return JsonResult.ok().setData(records);
    }

    /** 根据id查询 */
    @GetMapping("/{id}")
    public JsonResult get(@PathVariable("id") Integer id) {
        return JsonResult.ok().setData(roleService.getById(id));
    }

    /** 添加 */
    @PostMapping()
    public JsonResult save(@RequestBody Role role) {
        if (roleService.save(role)) {
            return JsonResult.ok("添加成功");
        }
        return JsonResult.error("添加失败");
    }

    /** 修改 */
    @PutMapping()
    public JsonResult update(@RequestBody Role role) {
        if (roleService.updateById(role)) {
            return JsonResult.ok("修改成功");
        }
        return JsonResult.error("修改失败");
    }

    /** 删除 */
    @DeleteMapping("/{id}")
    public JsonResult remove(@PathVariable("id") Integer id) {
        if (roleService.removeById(id)) {
            return JsonResult.ok("删除成功");
        }
        return JsonResult.error("删除失败");
    }

    /** 批量添加 */
    @PostMapping("/batch")
    public JsonResult saveBatch(@RequestBody List<Role> list) {
        if (roleService.saveBatch(list)) {
            return JsonResult.ok("添加成功");
        }
        return JsonResult.error("添加失败");
    }

    /** 批量修改 */
    @PutMapping("/batch")
    public JsonResult updateBatch(@RequestBody BatchParam<Role> batchParam) {
        if (batchParam.update(roleService, "role_id")) {
            return JsonResult.ok("修改成功");
        }
        return JsonResult.error("修改失败");
    }

    /** 批量删除 */
    @DeleteMapping("/batch")
    public JsonResult removeBatch(@RequestBody List<Integer> ids) {
        if (roleService.removeByIds(ids)) {
            return JsonResult.ok("删除成功");
        }
        return JsonResult.error("删除失败");
    }
}

 

4.2.多表CRUD

像用户user这样的表往往查询的时候需要关联其他表,添加、修改、删除跟单表是一样的:

@RestController
@RequestMapping("/sys/user")
public class UserController {
    @Autowired
    private UserService userService;

    /** 分页查询、排序、模糊搜索 */
    @GetMapping("/page")
    public PageResult<Role> page(HttpServletRequest request) {
        PageParam pageParam = new PageParam(request);
        pageParam.setDefaultOrder(new String[]{"createTime"}, null);
        return userService.listPage(pageParam);
    }

    /** 查询全部、排序、模糊搜索 */
    @GetMapping()
    public JsonResult list(HttpServletRequest request) {
        PageParam pageParam = new PageParam(request);
        List<Role> records = roleService.listAll(pageParam.getNoPageParam());
        return JsonResult.ok().setData(pageParam.sortRecords(records));
    }

    /** 根据id查询 */
    @GetMapping("/{id}")
    public JsonResult get(@PathVariable("id") Integer id) {
        PageParam<User> pageParam = new PageParam<>();
        pageParam.put("userId", id);
        List<User> records = userService.listAll(pageParam.getNoPageParam());
        return JsonResult.ok().setData(pageParam.getOne(records));
    }
}

service的写法:

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
    @Override
    public PageResult<User> listPage(PageParam page) {
        List<User> records = baseMapper.listPage(page);
        return new PageResult<>(records, page.getTotal());
    }
    @Override
    public List<User> listAll(Map page) {
        return baseMapper.listAll(page);
    }
}

mapper的写法:

public interface UserMapper extends BaseMapper<User> {
    /** 分页查询,需要注意一定要加@Param("page") */
    List<User> listPage(@Param("page") PageParam page);

    /** 查询全部,这里也加@Param("page")是为了xml中共用一个sql */
    List<User> listAll(@Param("page") Map page);
}

xml的写法:

<mapper namespace="com.xxx.common.system.mapper.UserMapper">
    <!-- 关联查询sql -->
    <sql id="relSelect">
        SELECT a.*, b.organization_name
        FROM sys_user a
        LEFT JOIN sys_organization b ON a.organization_id = b.organization_id
        <where>
            <if test="page.pageData.userId!=null">
                AND a.user_id = #{page.pageData.userId}
            </if>
            <if test="page.pageData.username!=null">
                AND a.username LIKE CONCAT('%', #{page.pageData.username}, '%')
            </if>
            <if test="page.pageData.sex!=null">
                AND a.sex = #{page.pageData.sex}
            </if>
            <if test="page.pageData.organizationName!=null">
                AND b.organization_name LIKE CONCAT('%', #{page.pageData.organizationName}, '%')
            </if>
        </where>
    </sql>
    <!-- 分页查询 -->
    <select id="listPage" resultType="com.xxx.common.system.entity.User">
        <include refid="relSelect"></include>
    </select>
    <!-- 查询全部 -->
    <select id="listAll" resultType="com.xxx.common.system.entity.User">
        <include refid="relSelect"></include>
    </select>
</mapper>

还需要在实体类中增加关联的字段,并加@TableField(exist = false)注解:

@TableName("sys_user")
public class User implements Serializable {
    @TableId(value = "user_id", type = IdType.AUTO)
    private Integer userId;   // 用户id

    @TableField(exist = false)
    private String organizationName;   // 机构名称
}

 

5.1.CoreUtil

框架已经引入HuTool工具库,原先工具类进行了删除,并将一些独有的方法整合到一起。

方法说明
String toString(String str)toString,为null返回空白字符
String connect(Object... objects)连接多个字符串,null自动过滤
String upperHead(String str)首字母变大写
  
String randomUUID8()生成8位uuid
String randomUUID16()生成16位uuid
  
Map objectToMap(Object obj)对象转Map
Map objectToMap(Object o, String[] f)对象转Map只包含指定字段
copyAttribute(F f, C c)复制父类的属性到子类
cloneToChild(F f, Class clazz)把父类克隆为子类
getFieldValue(Object o,String field)获取任意对象的任意字段的值
  
excelCheckBlank(list,startRow,cols)判断excel某列是否有空值
excelCheckRepeat(list,startRow,cols)判断excel某列是否有重复值
listCheckBlank(list,field)检查集合中元素某字段是否有空值
listCheckRepeat(list,field)检查集合中元素某字段是否有重复

检查集合中元素某字段是否有空值:

// 做批量添加时有用
List<Role> list;

// 获取到list中roleName字段为空的元素(遇到空的就直接返回)
Role o = CoreUtil.listCheckBlank(list, "roleName");

// 返回提示信息,第1条数据的角色名称不能为空\r\n第2条数据的角色名称不能为空
String msg = CoreUtil.listCheckBlank(list, "roleName", "角色名称");

检查集合中元素某字段是否有重复:

// 获取到list中roleName字段重复的元素(遇到重复的就直接返回)
Role o = CoreUtil.listCheckRepeat(list, "roleName");

// 返回提示信息,第1条数据与第2条数据的角色名称重复
String msg = CoreUtil.listCheckRepeat(list, "roleName", "角色名称");

判断excel某列是否有空值或重复:

List<List<Object>> list = ExcelUtil.getReader(file.getInputStream(), 0).read(startRow);

// 返回错误的提示信息,第1行第1列不能为空\r\n第1行第2列不能为空
String msg = CoreUtil.excelCheckBlank(list, startRow, 0, 1, 2, 3, 4, 7)

// 返回错误的提示信息,第1行第1列与第2行第1列重复
String msg = CoreUtil.excelCheckRepeat(list, startRow, 0, 1, 2, 3, 4, 7)
  • 参数二   起始行,用于纠正提示信息
  • 参数三   需要检查的列的索引

 

5.2.JSONUtil

JSONUtil使用fastjson库,全部方法:

方法说明
int getCode(String json)获取json里面的code
String getMessage(String json)获取json里面的msg
getObject(String json, String key, Class clazz)得到对象类型的值
getArray(String json, String key, Class clazz)得到对象类型的集合
parseObject(String json, Class clazz)json转换换成对象
parseArray(String json, Class clazz)json转换换成集合
String getString(String json, String key)获取String类型数据
int getIntValue(String json, String key)得到int类型的值
short getShortValue(String json, String key)得到short类型的值
long getLongValue(String json, String key)得到long类型的值
float getFloatValue(String json, String key)得到float类型的值
double getDoubleValue(String json, String key)得到double类型的值
getBooleanValue(String json, String key)得到boolean类型的值
byte getByteValue(String json, String key)得到byte类型的值
byte[] getBytes(String json, String key)得到byte[]类型的值
Integer getInteger(String json, String key)得到Integer类型的值
Short getShort(String json, String key)得到Short类型的值
long getLong(String json, String key)得到Long类型的值
Float getFloat(String json, String key)得到Float类型的值
Double getDouble(String json, String key)得到Double类型的值
Boolean getBoolean(String json, String key)得到Boolean类型的值
Byte getByte(String json, String key)得到Byte类型的值
BigDecimal getBigDecimal(String json, String key)得到BigDecimal类型的值
BigInteger getBigInteger(String json, String key)得到BigInteger类型的值
Date getDate(String json, String key)得到Date类型的值

使用示例:

String json = "{\"code\": 200, \"msg\": \"ok\", \"data\": {\"name\": \"Joe\", \"sex\": \"man\"} }";
int code = JSONUtil.getCode(json);
User user = JSONUtil.getObject(json, "data", User.class);

String json = "{\"name\": \"Joe\", \"sex\": \"man\"}";
User user = JSONUtil.parseObject(json, User.class);

 

5.3.FormCheckUtil

格式校验工具类,全部方法:

方法说明
boolean isPassword(String password)密码是否符合格式(5-12位非空白字符)
boolean equals(Object obj1, Object obj2)两个对象是否相等
boolean isPhone(String phone)是否是手机号
boolean isEmail(String email)是否是邮箱
boolean isUrl(String url)是否是网址
boolean isNumber(String number)是否是数字
boolean isDate(String date)是否是日期
boolean isIdentity(String identity)是否是身份证
String isIdentityStrong(String identity)是否是身份证(强校验)
boolean isDigits(String str)是否是整数
boolean isDigitsP(String str)是否是正整数
boolean isDigitsN(String str)是否是负整数
static boolean isDigitsPZ(String str)是否是非负整数(正整数或0)
boolean isDigitsNZ(String str)是否是非正整数(负整数或0)
boolean maxMinLength(str,max,min)验证最大长度、最小长度
boolean maxMin(value, max, min)验证最大值、最小值
boolean isIn(Object v, Object... vs)值是否在给定值内
boolean test(String str, String reg)字符串是否匹配正则表达式

 

5.4.Redis操作

建议使用StringRedisTemplate操作:

@Service
public class TestServiceImpl {
    @Autowired
    private StringRedisTemplate redisTemplate;
}

操作key的方法:

void delete(String key);                               // 删除key
void delete(Collection<String> keys);                  // 批量删除key
byte[] dump(String key);                               // 序列化key
Boolean hasKey(String key);                            // 是否存在key
Boolean expire(String key,long timeout,TimeUnit unit); // 设置过期时间
Boolean expireAt(String key, Date date);               // 设置过期时间
Set<String> keys(String pattern);                      // 查找匹配的key
Boolean move(String key, int dbIndex);                 // 将当前数据库的key移动到给定的数据库中
Boolean persist(key);                                  // 移除key的过期时间, key将持久保持
Long getExpire(String key, TimeUnit unit);             // 返回key的剩余的过期时间
String randomKey();                                    // 从当前数据库中随机返回一个key
void rename(String oldKey, String newKey);             // 修改key的名称
Boolean renameIfAbsent(String oldKey, String newKey);  // 仅当newkey不存在时,将oldKey改名为newkey
DataType type(String key);                             // 返回key所储存的值的类型

// 示例
redisTemplate.delete("name");

操作不同数据类型:

redisTemplate.opsForValue();  // 操作字符串
redisTemplate.opsForHash();   // 操作hash
redisTemplate.opsForList();   // 操作list
redisTemplate.opsForSet();    // 操作set
redisTemplate.opsForZSet();   // 操作有序set

// 示例
redisTemplate.opsForValue().set("name", "admin");

操作string类型的方法:

// 设置指定key的值
void set(String key, String value);

// 获取指定key的值
String get(String key);

// 返回key中字符串值的子字符
String getRange(String key, long start, long end);

// 将给定key值设为value并返回旧值
String getAndSet(String key, String value);

// 对key所储存的字符串值,获取指定偏移量上的位(bit)
Boolean getBit(String key, long offset);

// 批量获取
List<String> multiGet(Collection<String> keys);

// 设置ASCII码, 字符串'a'的ASCII码是97, 转为二进制是'01100001', 此方法是将二进制第offset位值变为value
boolean setBit(String key, long offset, boolean value);

// 将值 value 关联到key,并将key的过期时间设为 timeout
void set(String key,String value,long timeout,TimeUnit unit);

// 只有在key不存在时设置key的值
boolean setIfAbsent(String key, String value);

// 用 value 参数覆写给定key所储存的字符串值,从偏移量 offset 开始
void set(String key, String value, long offset);

// 获取字符串的长度
Long size(String key);

// 批量添加
void multiSet(Map<String, String> maps);

// 同时设置一个或多个 key-value 对,当且仅当所有给定key都不存在,之前已经存在返回false, 不存在返回true
boolean multiSetIfAbsent(Map<String, String> maps);

// 增加(自增长), 负数则为自减
Long increment(String key, long increment);

// 追加到末尾
Integer append(String key, String value);

操作hash类型的方法:

// 获取存储在哈希表中指定字段的值
Object get(String key, String field);

//获取所有给定字段的值
Map<Object, Object> entries(String key);

// 获取所有给定字段的值
List<Object> multiGet(String key, Collection<Object> fields);

// 增加值
void put(String key, String hashKey, String value);

// 批量增加值
void putAll(String key, Map<String, String> maps);

// 仅当hashKey不存在时才设置
Boolean putIfAbsent(String key, String hashKey, String value);

// 删除一个或多个哈希表字段
Long delete(String key, Object... fields);

// 查看哈希表 key 中,指定的字段是否存在
boolean hasKey(String key, String field);

// 为哈希表 key 中的指定字段的整数值加上增量 increment
Long increment(String key, Object field, long increment)

// 为哈希表 key 中的指定字段的整数值加上增量 increment
Double increment(String key, Object field, double delta);

// 获取所有哈希表中的字段
Set<Object> keys(String key);

// 获取哈希表中字段的数量
Long size(String key);

// 获取哈希表中所有值
List<Object> values(String key);

// 迭代哈希表中的键值对
Cursor<Entry<Object, Object>> scan(String key, ScanOptions options);

操作list类型的方法:

// 通过索引获取列表中的元素
String index(String key, long index);

// 获取列表指定范围内的元素
List<String> range(String key, long start, long end);

// 存储在list头部
Long leftPush(String key, String value);
Long leftPushAll(String key, String... value);
Long leftPushAll(String key, Collection<String> value);

// 当list存在的时候才加入
Long leftPushIfPresent(String key, String value);

// 如果pivot存在,再pivot前面添加
Long leftPush(String key, String pivot, String value);

// 存储在list尾部
Long rightPush(String key, String value);
Long rightPushAll(String key, String... value);
Long rightPushAll(String key, Collection<String> value);

// 为已存在的列表添加值
Long rightPushIfPresent(String key, String value);

// 在pivot元素的右边添加值
Long rightPush(String key, String pivot, String value);

// 通过索引设置列表元素的值
void set(String key, long index, String value);

// 移出并获取列表的第一个元素
String leftPop(String key);

// 移出并获取列表的第一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止
String leftPop(String key, long timeout, TimeUnit unit);

// 移除并获取列表最后一个元素
String rightPop(String key);

// 移出并获取列表的最后一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止
String rightPop(String key, long timeout, TimeUnit unit);

// 移除列表的最后一个元素,并将该元素添加到另一个列表并返回
String rightPopAndLeftPush(String sourceKey, String destinationKey);

// 从列表中弹出一个值,将弹出的元素插入到另外一个列表中并返回它; 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止
String rightPopAndLeftPush(String sourceKey, String destinationKey, long timeout, TimeUnit unit);

// 删除集合中值等于value得元素,index=0, 删除所有值等于value的元素; index>0, 从头部开始删除第一个值等于value的元素; index<0, 从尾部开始删除第一个值等于value的元素;
Long remove(String key, long index, String value);

// 裁剪list
void trim(String key, long start, long end);

// 获取列表长度
Long size(String key);

操作set(无序集合)类型的方法:

// set添加元素
Long add(String key, String... values);

// set移除元素
Long remove(String key, Object... values);

// 移除并返回集合的一个随机元素
String pop(String key);

// 将元素value从一个集合移到另一个集合
Boolean move(String key, String value, String destKey);

// 获取集合的大小
Long size(String key);

// 判断集合是否包含value
Boolean isMember(String key, Object value);

// 获取两个集合的交集
Set<String> intersect(String key, String otherKey);

// 获取key集合与多个集合的交集
Set<String> intersect(String key, Collection<String> otherKeys);

// key集合与otherKey集合的交集存储到destKey集合中
Long intersectAndStore(String key, String otherKey, String destKey);

// key集合与多个集合的交集存储到destKey集合中
Long intersectAndStore(String key, Collection<String> otherKeys, String destKey);

// 获取两个集合的并集
Set<String> union(String key, String otherKeys);

// 获取key集合与多个集合的并集
Set<String> union(String key, Collection<String> otherKeys);

// key集合与otherKey集合的并集存储到destKey中
Long unionAndStore(String key, String otherKey, String destKey);

// key集合与多个集合的并集存储到destKey中
Long unionAndStore(String key, Collection<String> otherKeys, String destKey);

// 获取两个集合的差集
Set<String> difference(String key, String otherKey);

// 获取key集合与多个集合的差集
Set<String> difference(String key, Collection<String> otherKeys);

// key集合与otherKey集合的差集存储到destKey中
Long differenceAndStore(String key, String otherKey, String destKey);

// key集合与多个集合的差集存储到destKey中
Long differenceAndStore(String key, Collection<String> otherKeys, String destKey);

// 获取集合所有元素
Set<String> members(String key);

// 随机获取集合中的一个元素
String randomMember(String key);

// 随机获取集合中count个元素
List<String> randomMember(String key, long count);

// 随机获取集合中count个元素并且去除重复的
Set<String> distinctRandomMembers(String key, long count);

// 游标迭代
Cursor<String> scan(String key, ScanOptions options);

操作zset(有序集合)类型数据:

// 添加元素,有序集合是按照元素的score值由小到大排列
Boolean add(String key, String value, double score);

// 批量添加
Long add(String key, Set<TypedTuple<String>> values);

// 移除
Long remove(String key, Object... values);

// 增加元素的score值,并返回增加后的值
Double incrementScore(String key, String value, double delta);

// 返回元素在集合的排名,有序集合是按照元素的score值由小到大排列
Long rank(String key, Object value);

// 返回元素在集合的排名,按元素的score值由大到小排列
Long reverseRank(String key, Object value);

// 获取集合的元素, 从小到大排序
Set<String> range(String key, long start, long end);

// 获取集合元素, 并且把score值也获取
Set<TypedTuple<String>> rangeWithScores(String key, long start, long end);

// 根据Score值查询集合元素
Set<String> rangeByScore(String key, double min, double max);

// 根据Score值查询集合元素, 从小到大排序
Set<TypedTuple<String>> rangeByScoreWithScores(String key, double min, double max);
Set<TypedTuple<String>> rangeByScoreWithScores(String key, double min, double max, long start, long end);

// 获取集合的元素, 从大到小排序
Set<String> reverseRange(String key, long start, long end);

// 获取集合的元素, 从大到小排序, 并返回score值
Set<TypedTuple<String>> reverseRangeWithScores(String key, long start, long end);

// 根据Score值查询集合元素, 从大到小排序
Set<String> reverseRangeByScore(String key, double min, double max);

// 根据Score值查询集合元素, 从大到小排序
Set<TypedTuple<String>> reverseRangeByScoreWithScores( String key, double min, double max);
Set<String> reverseRangeByScore(String key, double min, double max, long start, long end);

// 根据score值获取集合元素数量
Long count(String key, double min, double max);

// 获取集合大小
Long size(String key);

// 获取集合大小
Long zCard(String key);

// 获取集合中value元素的score值
Double score(String key, Object value);

// 移除指定索引位置的成员
Long removeRange(String key, long start, long end);

// 根据指定的score值的范围来移除成员
Long removeRangeByScore(String key, double min, double max);

// 获取key和otherKey的并集并存储在destKey中
Long unionAndStore(String key, String otherKey, String destKey);
Long unionAndStore(String key, Collection<String> otherKeys, String destKey);

// 交集
Long intersectAndStore(String key, String otherKey, String destKey);
Long intersectAndStore(String key, Collection<String> otherKeys, String destKey);

// 游标迭代
Cursor<TypedTuple<String>> scan(String key, ScanOptions options);

其他:

TimeUnit是时间单位,可选值有:

  • 天: TimeUnit.DAYS
  • 小时: TimeUnit.HOURS
  • 分钟: TimeUnit.MINUTES
  • 秒: TimeUnit.SECONDS
  • 毫秒: TimeUnit.MILLISECONDS

xxBit方法的使用:

例如字符'a'的ASCII码是97,转为二进制是'01100001',setBit方法就是把第offset位置上变成0或者1,true是1,false是0。

批量添加时TypedTuple的使用:

TypedTuple typedTuple = new DefaultTypedTuple(value,score);

游标迭代scan操作:

try {
    Cursor<Map.Entry<Object, Object>> cursor = redisTemplate.opsForHash().scan("field",
        ScanOptions.scanOptions().match("*").count(1000).build());
    while (cursor.hasNext()) {
        Object key = cursor.next().getKey();
        Object valueSet = cursor.next().getValue();
    }
    cursor.close();
} catch (IOException e) {
    e.printStackTrace();
}

 

5.5.网络请求

在SpringBoot项目中可以使用RestTemplate快速发起网络请求。

常用的get请求方式:

String url = "http://localhost:8081/sys/user/1";
String res = new RestTemplate().getForObject(url, String.class);

// 直接返回User对象
User user = new RestTemplate().getForObject(url, User.class);

// 占位符传参数
String url = "http://localhost:8081/sys/user/{1}/{2}";
User user = new RestTemplate().getForObject(url, User.class, 1, "男");

// map传参数
Map<String, String> map = new HashMap<>();
map.put("id", "1");
map.put("sex", "男");
User user = new RestTemplate().getForObject(url, User.class, map);

// 返回ResponseEntity对象
ResponseEntity<User> entity = new RestTemplate().getForEntity(url, User.class);
HttpStatus statusCode = entity.getStatusCode();
User user = entity.getBody();

常用的post请求:

String url = "http://localhost:8081/sys/user";
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
map.add("username", "admin");
map.add("sex", "男");
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(map, headers);
String res = new RestTemplate().postForObject(url, request, String.class);

// 返回ResponseEntity对象
ResponseEntity<String> entity = new RestTemplate().getForEntity(url, request, String.class);
HttpStatus statusCode = entity.getStatusCode();
String res = entity.getBody();

 

5.6.更多方法

MD5加密(Spring框架中的):

String md5 = DigestUtils.md5DigestAsHex("123456".getBytes());

Base64编码、解码(JDK内置的):

// 把图片进行base64编码
FileInputStream in = new FileInputStream(new File("C:/a.png"));
byte[] bytes = new byte[in.available()];
in.read(bytes);
String base64 = Base64.getEncoder().encodeToString(bytes);
System.out.println("data:image/png;base64," + base64);  // 带上头部

// base64转为图片
String base64 = "data:image/png;base64,iVBORw0KAYAAABzenr0AAAAvABJRU5ErkJggg==";
String base64Str = base64.substring(base64.indexOf(";") + 8);  // 去掉头部
byte[] bytes = Base64.getDecoder().decode(base64Str.getBytes());
String suffix = base64.substring(11, base64.indexOf(";"));  // 获取文件后缀
FileOutputStream out = new FileOutputStream(new File("C:/a." + suffix));
out.write(bytes);

 

6.1.上传文件

框架已经内置了FileController支持上传、预览、下载、缩略图、office预览等功能。

接口参数方法说明
file/uploadfilepost上传文件
file/upload/base64base64post上传Base64文件
file/{dir}/{name}get预览文件(支持office)
file/thumbnail/{dir}/{name}get预览缩略图
file/download/{dir}/{name}get下载文件
file/listdirget查询文件列表
file/removepathdelete删除文件

前端使用示例:

<form action="file/upload" method="post" enctype="multipart/form-data">
    <input type="file" name="file" />
    <button type="submit">上传</button>
</form>

上传成功返回的json数据示例,fileName是文件原始名称:

{
    "code":0,
    "msg":"上传成功",
    "url":"20200611/QQ截图20200611105349(1).png",
    "location":"http://192.168.1.126:8081/api/file/20200611/QQ截图20200611105349(1).png",
    "fileName":"QQ截图20200611105349.png",
    "dir":"/20200611"
}

base64形式:

$.post('file/upload/base64', {
    base64: 'data:image/png;base64,xJsfek3hJHSfejhj6sdakjed=='
}, function(res){
    console.log(res);  // 返回结果同上,没有fileName字段
});

预览、缩略图、下载文件:

// 预览文件,支持office文件预览
window.open('file/20191230/xxx.jpg');

// 查看缩略图
window.open('file/thumbnail/20191230/xxx.jpg');

// 下载原文件
window.open('file/download/20191230/xxx.jpg');

支持office预览需要服务器安装OpenOffice,然后在Constants.java中配置安装位置,也可在群里下载。

 

6.2.异步任务

异步任务需要在Application上面加@EnableAsync注解开启,框架已经加了:

@EnableAsync
@SpringBootApplication
public class EasyWebApplication {
    public static void main(String[] args) {
        SpringApplication.run(EasyWebApplication.class, args);
    }
}

在你需要异步执行的方法上面加@Async注解:

@Service
public class Test {
    @Async
    public void doTask() {
        Thread.sleep(10000);
        System.out.println("任务完成,耗时10s");
    }
}

框架里面的登录日志、操作日记添加都是使用的异步任务,异步任务一般用于跟业务逻辑无关的操作, 使业务流程处理完后立即返回给前端,与业务流程无关的操作异步执行,提升业务功能的执行效率。

 

6.3.定时任务

定时任务需要在Application上面加@EnableScheduling注解开启:

@EnableScheduling
@SpringBootApplication
public class EasyWebApplication {
    public static void main(String[] args) {
        SpringApplication.run(EasyWebApplication.class, args);
    }
}

在需要定时执行的方法上面加@Scheduled注解:

@Service
public class Test {
    @Scheduled(cron="*/5 * * * * *")
    public void reportCurrentTime() {
        System.out.println(new Date());
    }
}

相关链接:在线Cron表达式生成器

 

6.4.发送邮件

EmailService中已经封装了三个通用的发邮件的方法:

方法说明
sendTextEmail(String title, String content, String[] toEmails)发送普通文本邮件
sendFullTextEmail(String title, String html, String[] toEmails)发送富文本邮件
sendHtmlEmail(String title, String tpl, Map map, String[] toEmails)发送html模板邮件

使用方法:

public class Test {
    @Autowired
    private EmailService emailService;

    public void test() throws MessagingException, IOException {
        // 发送纯文本邮件,参数三是收件人
        emailService.sendTextEmail("邮件标题", "邮件内容", new String[]{"xxx@qq.com"});

        // 发送富文本邮件
        emailService.sendFullTextEmail("邮件标题", "<span style=\"color:red;\">邮件内容</span>", new String[]{"xxx@qq.com"});

        // 发送模板邮件
        Map<String, Object> map = new HashMap<>();  // 页面的动态数据
        map.put("name", "admin");
        map.put("sex", "女");
        emailService.sendHtmlEmail("邮件标题", "sys/test.html", map, new String[]{"222222@qq.com"});
    }
}

html模板放在templates下面,使用beetl模板语法:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <style> /** 你还可以加样式在这里 */ </style>
</head>
<body>
<table class="my-table">
    <tr><th>账号:</th><td>${name!}</td></tr>
    <tr><th>性别:</th><td>${sex!}</td></tr>
</table>
</body>
</html>

EmailService使用前需要确保配置正确,检查application.properties中的配置:

spring.mail.host=smtp.qq.com
# 这里改成你的邮箱
spring.mail.username=xxxxx@foxmail.com
# 这里改成你的密码
spring.mail.password=xxxxxxxxxx
spring.mail.default-encoding=UTF-8
## 使用25端口认证
#spring.mail.port=25
#spring.mail.properties.mail.smtp.auth=true
#spring.mail.properties.mail.smtp.starttls.enable=true
#spring.mail.properties.mail.smtp.starttls.required=true
## 使用465端口SSl认证
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory
spring.mail.properties.mail.smtp.socketFactory.port=465

这里的密码不是登录密码,获取方式以QQ邮箱为例,进入“设置/账户”:

找到“POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV服务”:

 

点击“开启”就会弹出密码,properties文件中25端口和465端口配置一个即可,一般服务器25端口是封闭的,建议使用465端口。

 

7.1.application.yml

application.yml是SpringBoot的核心配置文件,所有的配置都在它里面,另外还有三个是做多环境配置的:

配置文件说明
application-dev.yml开发环境配置,配置开发环境的数据源、日志处理等
application-prod.yml生成环境配置,配置生产环境的数据源、日志处理等
application-test.yml测试环境配置,配置测试环境的数据源、日志处理等

在application.yml中配置spring.profiles.active=dev来指定使用哪个环境,建议像数据库、redis、日志处理等 在不同环境中配置不一样的写在三个不同环境的配置文件中,不会随着环境改变的配置写在application.yml中。

7.2.日期转换器

日期类型转换器位于com.xxx.common.core.config.DateConverterConfig.java,如果对象有Date类型的字段,对象在Controller中 作为参数接收时会将前端传递的string格式转成Date格式,前端直接传递如下几种格式的字符串,会自动转换成Date类型:

yyyy-MM-dd
yyyy-MM-dd HH:mm
yyyy-MM-dd HH:mm:ss

注意: 只会转成Date类型,如果对象里面用的是LocalTime类型是不支持的

7.3.异常处理

全局异常处理器位于com.xxx.common.core.exception.GlobalExceptionHandler.java,在代码的任何位置都可以抛出异常, 异常信息不会直接到达页面,会跳转到错误页面,如果是ajax请求,会返回json数据:

{"code": 500, "msg": "系统错误", "error": "java.lang.NullPointerException..."}

自定义异常:

异常类说明
BusinessException业务异常,默认错误码500,错误信息“系统错误”
ParameterException参数异常,默认错误码400,错误信息“参数错误”

有时候异常是需要抛出的,而不是捕获,尤其是Service层,因为抛出异常才可以做事物回滚,例如:

public class UserServiceImpl {
    @Transactional(rollbackFor = Exception.class)
    public boolean addUser(User user, List<Integer> roleIds) {
        boolean result = baseMapper.insert(user) > 0;
        if (result) {
            if (userRoleMapper.insertBatch(user.getUserId(), roleIds) < roleIds.size()) {
                throw new BusinessException("添加失败");
            }
        }
        return result;
    }
}

上面是一个添加用户的例子,添加完用户后还要添加用户的角色,如果角色添加失败,应该回滚添加的用户,此时抛出异常才可回滚, 也别忘了加@Transactional注解。

使用场景二:

public class UserServiceImpl {
    public boolean addUser(User user) {
        if (baseMapper.selectByUsername(user.getUsername()) != null) {
            throw new BusinessException("账号已经存在");
        }
        return baseMapper.insert(user) > 0;
    }
}

上面仍然是一个添加用户的例子,添加用户之前要判断账号是否存在,但service返回的是boolean的类型,直接返回false错误信息不明确, 如果在controller里判断,每个调用service的controller都要判断,此时可以用自定义的BusinessException抛出异常, 经过异常处理器的处理之后,前端收到的json仍然是:{"code": 500, "msg": "账号已经存在"},简单方便。

自定义异常的使用:

throw new BusinessException("账号已经存在");
throw new BusinessException(500, "账号已经存在");  // 重写code,默认是500

throw new ParameterException("参数不能为空");
throw new ParameterException(400, "参数不能为空");  // 重写code,默认是400

BusinessException和ParameterException都继承IException,因为Exception没有code这个字段,只有message字段, IException增加了code字段,所以要自定义其他类型的异常,最好也继承IException。

7.4.MybatisPlus配置

MybatisPlus的配置位于com.xxx.common.core.config下面,代码如下:

@Configuration
@EnableTransactionManagement
public class MybatisPlusConfig {
    // 分页插件
    @Bean
    public PaginationInterceptor paginationInterceptor() {
        return new PaginationInterceptor().setLimit(1000);
    }
}

@EnableTransactionManagement注解是开启事务,这里加了就不用在Application上加了。

7.5.Security配置

SpringSecurity框架的配置位于com.xxx.common.core.config.SecurityConfig.java

public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers(HttpMethod.GET, "/", "/assets/**", "/**.html", "/file/**")
                .permitAll()
                .antMatchers("/login", "/error", "/druid/**", "/swagger-ui.html",
                        "/swagger-resources/**", "/webjars/**", "/v2/api-docs")
                .permitAll()
                .anyRequest().authenticated()
                .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and().headers().frameOptions().sameOrigin()
                .and().cors()
                .and().csrf().disable();
        http.exceptionHandling().accessDeniedHandler(jwtExceptionHandler()).authenticationEntryPoint(jwtExceptionHandler());
        http.logout().logoutUrl("/logout").logoutSuccessHandler(jwtLogoutSuccessHandler());
        http.addFilterBefore(jwtLoginFilter(), UsernamePasswordAuthenticationFilter.class);
        http.addFilterBefore(jwtRequestFilter(), UsernamePasswordAuthenticationFilter.class);
    }
}

permitAll()代表排除拦截,authenticated()代表需要登录后访问。

SpringSecurity相关的其他配置类:

配置文件说明
JwtLoginFilter.java登录过滤器,处理登录请求成功和失败
JwtRequestFilter.java请求过滤器,处理携带token的请求
JwtLogoutSuccessHandler.java处理退出登录成功的操作
JwtExceptionHandler.java处理没有权限的异常
UserDetailsServiceImpl.java用于登录查询用户和权限

JwtLoginFilter登录过滤器:

这个类主要是处理登录接口,在登录成功后签发token并返回json数据给前端,在登录失败后 记录登录日志并返回json数据给前端:

public class JwtLoginFilter extends UsernamePasswordAuthenticationFilter {
    @Autowired
    private LoginRecordService loginRecordService;
    public JwtLoginFilter(AuthenticationManager authenticationManager) {
        super.setAuthenticationManager(authenticationManager);
    }
    /** 登录成功签发token返回json数据 */
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult)
            throws IOException, ServletException {
        UserDetails user = (UserDetails) authResult.getPrincipal();
        String access_token = JwtUtil.buildToken(user.getUsername(), Constants.TOKEN_EXPIRE_TIME, Constants.TOKEN_KEY);
        // 记录登录日志
        loginRecordService.saveAsync(user.getUsername(), request);
        // 返回json数据
        response.setContentType("application/json;charset=UTF-8");
        PrintWriter out = response.getWriter();
        out.write(JSON.toJSONString(JsonResult.ok("登录成功").put("access_token", access_token)
                .put("token_type", JwtUtil.TOKEN_TYPE)));
        out.flush();
    }
    /** 登录失败处理 */
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
        String username = request.getParameter("username");
        response.setContentType("application/json;charset=UTF-8");
        PrintWriter out = response.getWriter();
        JsonResult result;
        if (e instanceof UsernameNotFoundException) {
            result = JsonResult.error("账号不存在");
            loginRecordService.saveAsync(username, LoginRecord.TYPE_ERROR, "账号不存在", request);
        } else if (e instanceof BadCredentialsException) {
            result = JsonResult.error("账号或密码错误");
            loginRecordService.saveAsync(username, LoginRecord.TYPE_ERROR, "账号或密码错误", request);
        } else if (e instanceof LockedException) {
            result = JsonResult.error("账号被锁定");
            loginRecordService.saveAsync(username, LoginRecord.TYPE_ERROR, "账号被锁定", request);
        } else {
            result = JsonResult.error(e.getMessage());
        }
        out.write(JSON.toJSONString(result));
        out.flush();
    }
}

登录接口调用形式:

$.post('/login', {
    username: 'admin',
    password: '123456'
},function(res){
    console.log(res);
});

登录成功返回的json格式为:

{"code": 0, "msg": "登录成功", "access_token": "xxxxxxxxxxxxxxxx", "token_type": "Bearer"}

JwtRequestFilter请求过滤器:

这个类主要是处理所有携带token的请求,从request中获取前端传递的token,并验证token,然后把token对应的User设置到Security中:

public class JwtRequestFilter extends OncePerRequestFilter {
    private UserDetailsService userDetailsService;
    public JwtRequestFilter(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {
        String access_token = JwtUtil.getAccessToken(request);
        if (access_token != null) {
            try {
                Claims claims = JwtUtil.parseToken(access_token, Constants.TOKEN_KEY);
                String username = claims.getSubject();
                UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                SecurityContextHolder.getContext().setAuthentication(authentication);
                // token将要过期签发新token, 防止突然退出登录
                if ((claims.getExpiration().getTime() - new Date().getTime()) / 1000 / 60 < Constants.TOKEN_WILL_EXPIRE) {
                    String access_token_new = JwtUtil.buildToken(username, Constants.TOKEN_EXPIRE_TIME, Constants.TOKEN_KEY);
                    response.addHeader(JwtUtil.TOKEN_HEADER_NAME, access_token_new);
                }
            } catch (Exception e) {
                System.out.println(e.getMessage());
            }
        }
        chain.doFilter(request, response);
    }
}

如果token即将过期生成新的token并设置在response的header中,防止用户正在操作的情况需要退出重新登录,所以前端在每次请求完成后 应该判断response的header中是否有Authorization,有应该保存新的token。

前端请求接口传递token的形式:

// 在参数中传递token
$.get('/sys/user', {
    access_token: 'xxxxx'
}, function(res){
    console.log(res);
}, 'json');

// 在header中传递token
$.ajax({
    url: '/sys/user', 
    headers: { Authorization: 'Bearer ' + token },
    success: function(res){
        console.log(res);
    }
});

在参数中传递用access_token,在header中传递用Authorization,值前面要加Bearer,注意有个空格。

security权限注解使用:

// 一般加在controller的方法上面
@PreAuthorize("hasAuthority('sys:user:save')")

7.6.Swagger配置

Swagger配置类位于com.xxx.common.core.config.Swagger2Config.java,Swagger的配置很简单,基本打开看看就明白了, 需要注意的是加了一个可以在yml中配置的参数swagger.host用来配置生产环境的接口地址(域名等)。

比如你打包部署到线上并绑定了域名可以这样配置以保证swagger文档的在线接口测试功能可以正常访问:

java -jar easyweb-security.jar --swagger.host=easyweb.vip
// 或者
java -jar easyweb-security.jar --swagger.host=192.168.1.245:8081

swagger接口文档如何正确使用,访问ip:端口/swagger-ui.html进入文档后,先找到登录接口输入账号密码执行后获取token, 然后在文档的右上角点击Authorize按钮,在弹出窗的输入框里面输入Bearer 登录返回的token点击Authorize按钮, 然后执行文档上面的所有接口都会自动带上token了。

7.7.resources目录

resources下的static目录用于存放静态资源,因为是前后端分离的,所以不需要templates目录,static下面的文件可以直接放到nginx中分离部署, 之所以放在static下面是为了一个人开发的方便,如果有专门的前端完全可以把static下面的文件单独作为一个前端项目,分离开发、分离部署。

7.8.集成redis

在pom.xml中增加:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>  

在application.properties中配置

spring.redis.database=0
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.password=

 

8.1.项目生成

项目生成器可以生成一个完整的项目,包括controller、service、entity、mapper、页面等,生成的功能包含添加、修改、删除、批量添加、批量修改、 批量删除、查询全部、分页查询、根据id查询,项目生成器可以自定义模板,目前提供了shiro版本的框架模板以及SpringSecurity版本的框架模板, 也可以自己制作框架模板。

项目生成器使用步骤,首先你要创建好你的数据库,数据库的字符集选utf8mb4,排序规则选utf8mb4_general_ci, 然后导入系统管理部分的sql脚本:

 

创建好数据库后再创建你的业务表,这里新增了两个表举例子,文章和评论,注意表备注后面不要带表,不然生成后的代码就是文章表管理了:

第一步配置你的数据库连接信息:

第二步选择你的表,功能比较少的项目可以不划分模块,模块名空着:

第三步配置其它信息:

配置完后点击立即生成,生成完毕会自动下载,解压下载的压缩包:

用IDEA开发工具导入项目,生成的项目结构如下:

resources\sql\目录下会有一个生成的sql脚本,是用来添加菜单和角色菜单的,你可以适当修改后导入数据库, 也可以直接导入到数据库中:

对于功能较多的项目,数据表较多的项目推荐把不同的表划分到不同的模块中,这里的模块最后生成的不是maven的多模块, 是以划分包的形式,你可以根据自己业务进行划分:

划分多模块最后生成的项目结构为:

 

生成好的项目可以直接运行,添删改查、批量添、改、删、分页查、根据id查、页面都会生成:

 

8.2.多表关联

生成的代码没有多表关联的操作,但是提供好了模板,你可以看到生成的Controller中关联的操作注释了:

/** 评论管理 */
@Controller
@RequestMapping("/model2/comment")
public class CommentController extends BaseController {
    @Autowired
    private CommentService commentService;
    /** 分页查询评论 */
    @ResponseBody
    @RequestMapping("/page")
    public PageResult<Comment> page(HttpServletRequest request) {
        PageParam<Comment> pageParam = new PageParam<>(request);
        return new PageResult<>(commentService.page(pageParam, pageParam.getWrapper()).getRecords(), pageParam.getTotal());
        //return commentService.listPage(pageParam);  // 使用关联查询
    }
    /** 查询全部评论 */
    @ResponseBody
    @RequestMapping("/list")
    public JsonResult list(HttpServletRequest request) {
        PageParam<Comment> pageParam = new PageParam<>(request);
        return JsonResult.ok().setData(commentService.list(pageParam.getOrderWrapper()));
        //List<Comment> records = commentService.listAll(pageParam.getNoPageParam());  // 使用关联查询
        //return JsonResult.ok().setData(pageParam.sortRecords(records));
    }
    /** 根据id查询评论 */
    @ResponseBody
    @RequestMapping("/get")
    public JsonResult get(Integer id) {
        return JsonResult.ok().setData(commentService.getById(id));
        // 使用关联查询
        //PageParam<Comment> pageParam = new PageParam<>();
        //pageParam.put("comment_id", id);
        //List<Comment> records = commentService.listAll(pageParam.getNoPageParam());
        //return JsonResult.ok().setData(pageParam.getOne(records));
    }
}

如果需要多表关联查询,把注释的代码放开,service不用修改,需要在mapper.xml中增加关联的sql语句,模板会生成好sql语句, 你只需要在原来的语句上面增加LEFT JOIN语句去关联表即可:

<mapper namespace="com.egao.wms.model2.mapper.CommentMapper">
    <!-- 关联查询sql -->
    <sql id="relSelect">
        SELECT a.*, b.title
        FROM tb_comment a
        LEFT JOIN tb_blog b ON a.blog_id = b.blog_id
        <where>
            <if test="page!=null and page.pageData!=null">
                <if test="page.pageData.commentId != null">
                    AND a.comment_id = #{page.pageData.commentId}
                </if>
                <if test="page.pageData.blogId != null">
                    AND a.blog_id = #{page.pageData.blogId}
                </if>
                <if test="page.pageData.userName != null">
                    AND a.user_name LIKE CONCAT('%', #{page.pageData.userName}, '%')
                </if>
                <if test="page.pageData.content != null">
                    AND a.content LIKE CONCAT('%', #{page.pageData.content}, '%')
                </if>
                <if test="page.pageData.deleted!=null">
                    AND a.deleted = #{page.pageData.deleted}
                </if>
                <if test="page.pageData.deleted==null">
                    AND a.deleted = 0
                </if>
                <if test="page.pageData.createTimeStart!=null">
                    AND a.create_time &gt;= #{page.pageData.createTimeStart}
                </if>
                <if test="page.pageData.createTimeEnd!=null">
                    AND a.create_time &lt;= #{page.pageData.createTimeEnd}
                </if>
                <if test="page.pageData.title != null">
                    AND b.title LIKE CONCAT('%', #{page.pageData.title}, '%')
                </if>
            </if>
        </where>
    </sql>
    <!-- 分页查询 -->
    <select id="listPage" resultType="com.egao.wms.model2.entity.Comment">
        <include refid="relSelect"></include>
    </select>
    <!-- 查询全部 -->
    <select id="listAll" resultType="com.egao.wms.model2.entity.Comment">
        <include refid="relSelect"></include>
    </select>
</mapper>

上面的例子就是在评论表中把对应文章的标题title关联查询出来,你还需要在实体类中增加title字段:

/** 评论 */
@TableName("tb_comment")
public class Comment implements Serializable {
    private static final long serialVersionUID = 1L;
    /** 评论id */
    @TableId(value = "comment_id", type = IdType.AUTO)
    private Integer commentId;
    /** 评论人 */
    private String userName;
    //......省略
    /** 修改时间 */
    private Date updateTime;

    /** 文章标题 */
    @TableField(exist = false)
    private String title;
}

增加关联的字段并加@TableField(exist = false)注解,然后Controller中查询全部、分页查询、根据id查询 这三个方法返回的json数据里面就会有这个关联的字段了。

 

8.3.模板制作

一套完整的模板应该包含:

名称类型说明
project目录项目的框架
config.json文件对于框架的配置
controller.java.btl文件controller生成模板
entity.java.btl文件实体类生成模板
mapper.java.btl文件mapper生成模板
mapper.xml.btl文件mapper的xml生成模板
service.java.btl文件service生成模板
serviceImpl.java.btl文件serviceImpl生成模板
generator.sql.btl文件sql脚本生成模板
page.html.btl文件页面生成模板

最后一个page.html.btl是非必须的,而且名字、个数等可以在config.json中配置,除此之外,其它文件都是是必须要有的。

project这个名字是固定的不能修改,里面放的是基础框架的源码:

config.json说明:

{
  "packageName": "com.egao",
  "pages": [
    {
      "tpl": "page.html.btl",
      "output": "resources/templates/"
    }
  ],
  "replaces": [
    {
      "files": [
        "src/main/resources/application-dev.yml",
        "src/main/resources/application-prod.yml"
      ],
      "items": [
        {
          "orgStr": "username: root",
          "newStr": "username: ${dbUserName}"
        },
        {
          "orgStr": "password: 123456",
          "newStr": "password: ${dbPassword}"
        },
        {
          "orgStr": "driver-class-name: com.mysql.cj.jdbc.Driver",
          "newStr": "driver-class-name: ${dbDriverName}"
        }
      ]
    },
    {
      "files": [
        "src/main/resources/application.yml"
      ],
      "items": [
        {
          "orgStr": "aop-patterns: com.egao.*.*.service.*",
          "newStr": "aop-patterns: ${groupId}.*.*.service.*"
        },
        {
          "orgStr": "typeAliasesPackage: com.egao.**.entity",
          "newStr": "typeAliasesPackage: ${groupId}.**.entity"
        }
      ]
    }
  ]
}

config.json可以配置三个属性:

  • packageName 基础框架的原始包名
  • pages 配置页面生成模板,不要页面可以配置为pages: []
  • replaces 配置需要修改基础框架的内容

pages是一个数组,可以配置多个,tpl是模板的名称,output是模板输出位置。

replaces也是一个数组,里面的files是需要修改基础框架的文件,items是需要修改的内容, orgStr是需要修改的原始内容,newStr是替换的内容,newStr可以使用${}语法获取一些模板数据。

newStr可以获取的数据:

数据描述
projectName生成的项目名称
groupId生成的项目的groupId
groupIdPathgroupId的点变成斜杠形式
packageName生成的项目的包名
packageNamePath包名的点变成斜杠形式
author作者的名称
dbUrl数据库的url
dbUserName数据库的账号
dbPassword数据库的密码
dbDriverName数据库的驱动名

其他模板可以取的数据:

  • author   作者名称
  • package   包相关数据
  • table   表数据
  • cfg   项目生成器相关的数据
    • genConfig   项目配置
      • dbUrl   数据库连接地址
      • dbUserName   数据库连接账号
      • dbPassword   数据库连接密码
      • dbDriverName   数据库连接驱动
      • projectName   项目名称
      • groupId
      • packageName   包名
      • tplName   使用模板名称
      • needPerm   是否需要权限注解
      • needLog   是否需要日志注解
      • menuStartId   菜单起始id
    • genModel   当前模块
      • modelName   模块名称
      • tables   模块下所有的表名(集合)
      • prefix   模块下所有表前缀(集合)
    • genDateTime   当前时间
    • genModelList   所有模块信息(集合)

制作好的模板压缩并上传到项目生成器中就可以使用了:

提示 如果上传失败请自己把压缩包放在C:\easyweb-generator\tpl目录下面

 

8.4.注意事项

  • 逻辑删除字段名必须为deleted
  • 数据库是下划线形式,实体类是驼峰形式 (固定的配置)
  • 数据库表的名称(表备注)不要带
  • 创建时间字段建议为create_time (模板会对表单过滤此字段)
  • 修改时间字段建议为update_time (模板会对表单过滤此字段)
  • 只能生成单体单模块项目,制作模板不要用多模块项目

当然你可以修改项目生成器源码满足更多的配置。

 

 

9.1.上传文件实例

代码示例:

<button id="btnUpload">上传</button>
<img id="imgView" src=""/>

<script>
layui.use(['layer', 'upload'], function () {
    var $ = layui.jquery;
    var layer = layui.layer;
    var upload = layui.upload;

    upload.render({
        elem: '#btnUpload',
        url: '/file/upload',
        before: function(obj){  // 上传前回调
            layer.load(); 
        },
        done: function (res) {  // 上传完毕回调
            layer.closeAll('loading');
            if (0 == res.code) {
                layer.msg(res.msg, {icon: 1});
                $('#imgView').attr('src', 'file/' + res.url); 
            } else {
                layer.msg(res.msg, {icon: 2});
            }
        },
        error: function () {  // 请求异常回调
            layer.closeAll('loading');
            layer.msg('上传失败', {icon: 2});
        }
    });
});
</script>

9.2.表单中上传文件

完整代码:

<form class="layui-form model-form" id="demoForm">
    <div class="layui-form-item">
        <label class="layui-form-label">课程名称:</label>
        <div class="layui-input-block">
            <input name="courseName" class="layui-input"/>
        </div>
    </div>
    <div class="layui-form-item">
        <label class="layui-form-label">课程封面:</label>
        <div class="layui-input-block">
            <input name="cover" class="layui-input" disabled="disabled" style="padding-right: 70px"/>
            <button style="position: absolute;right: 0;top: 0;border-top-left-radius: 0;border-bottom-left-radius: 0;"
                    class="layui-btn" type="button" id="btnUpload">上传
            </button>
        </div>
    </div>
    <div class="layui-form-item">
        <label class="layui-form-label">课程简介:</label>
        <div class="layui-input-block">
            <input name="desc" class="layui-input"/>
        </div>
    </div>
    <div class="layui-form-item">
        <div class="layui-input-block text-right">
            <button class="layui-btn layui-btn-primary" type="button" ew-event="closeDialog">取消</button>
            <button class="layui-btn" lay-filter="submitDemo" lay-submit>保存</button>
        </div>
    </div>
</form>

<script>
layui.use(['layer', 'upload', 'form'], function () {
    var $ = layui.jquery;
    var layer = layui.layer;
    var upload = layui.upload;
    var form = layui.form;

    // 监听提交
    form.on('submit(submitDemo)', function (data) {
        console.log(data.field);
        return false;
    });

    // 渲染上传按钮
    upload.render({
        elem: '#btnUpload',
        url: '/file/upload',
        before: function(obj){  // 上传前回调
            layer.load(); 
        },
        done: function (res) {  // 上传完毕回调
            layer.closeAll('loading');
            if (0 == res.code) {
                layer.msg(res.msg, {icon: 1});
                $('#demoForm input[name="cover"]').val(res.url); 
            } else {
                layer.msg(res.msg, {icon: 2});
            }
        },
        error: function () {  // 请求异常回调
            layer.closeAll('loading');
            layer.msg('上传失败', {icon: 2});
        }
    });
});
</script>

建议表单内上传文件像上面这样,先上传获取url,提交表单时只是把上传后的url提交,而不是传统的表单提交又有文件、 又有其他参数,当然这样做会存在文件上传了,表单不提交导致的垃圾文件的问题,这个问题可以通过定时清理很久没有使用的文件来解决。

9.3.刷新token实例

在setter.js的ajaxSuccessBefore中判断服务器的response的header中是否有Authorization字段,有就表示token马上要过期了, 服务端生成了新的token,前端需要把新的token存起来:

layui.define(['table'], function (exports) {
    var setter = {
        /* ajax请求结束后的处理 */
        ajaxSuccessBefore: function (res, url, obj) {
            // 判断response是否有token
            var newToken = obj.xhr.getResponseHeader('Authorization');
            if (newToken) setter.putToken({access_token: newToken});
            // ......其他代码省略
            return true;
        }
    };
    exports('setter', setter);
});

或者使用ajaxSetup实现,这个对table的请求也有用:

// ajax统一处理
$.ajaxSetup({
    complete: function (xhr) {
        var newToken = xhr.getResponseHeader('Authorization');
        if (newToken) setter.putToken({access_token: newToken});
    }
});

 

 

10.1.使用IDEA打包

点击右侧的Maven/package即可开始打包,打好的包在target下面:

10.2.部署到Linux

先把jar包上传到linux里面,建议一个项目(jar包)建一个文件夹,然后通过如下命令启动:

nohup java -jar easyweb-shiro-0.0.1.jar --spring.profiles.active=prod

命令执行后直接关闭此窗口,不要用Ctrl+C停止

如果要实时查看打印信息,使用如下命令:

tail -f nohup.out

 

关闭项目使用如下命令:

# 查看端口号的进程id
netstat -anp |grep 8083

# 显示如下信息
tcp   0    0 0.0.0.0:8083     0.0.0.0:*    LISTEN   22812/java  

# 关闭进程
kill 22812

10.3.部署到Windows

在windows上部署建议创建一个bat文件,这样只要双击bat文件就可以启动了:

title XXX系统
java -jar easyweb-shiro-0.0.1.jar --spring.profiles.active=test

 

bat文件前面加一个title的好处是打开的命令窗口可以显示项目的名字,避免部署太多项目无法区分哪个命令窗口对应哪个项目。

windows查看端口占用及停掉端口:

netstat -ano |findstr "8081"
tskill 6124

10.4.nginx的使用

不同域名映射到不同端口的项目:

http {
    server {
        listen       80;
        server_name  localhost;
        location / {
            root   html;
            index  index.html index.htm;
        }
    }

    # 域名一
    server {
        listen       80;
        server_name aaa.xxx.com;
        location / {
            proxy_pass http://localhost:8081/;
        }
    }

    # 域名二
    server {
        listen       80;
        server_name bbb.xxx.com;
        location / {
            proxy_pass http://localhost:8082/;
        }
    }
}

https配置:

http {
    server {
        listen       80;
        server_name  localhost;
        location / {
            root   html;
            index  index.html index.htm;
        }
    }

    # 配置http转发到https
    server {
        listen       80;
        server_name  aaa.xxx.com;
        rewrite ^ https://$http_host$request_uri? permanent;
    }

    # 配置https
    server {
        listen       443 ssl;
        server_name  aaa.xxx.com;

        # ssl证书
        ssl_certificate      cert/xxxxxxx.pem;
        ssl_certificate_key  cert/xxxxxxx.key;

        ssl_session_cache    shared:SSL:1m;
        ssl_session_timeout  5m;

        ssl_ciphers  HIGH:!aNULL:!MD5;
        ssl_prefer_server_ciphers  on;

        location / {
            proxy_pass http://localhost:8081/;
        }
    }
}

上面都是后端项目的配置,前端项目的配置:

http {
    server {
        listen       80;
        server_name  localhost;
        location / {
            root   html;
            index  index.html index.htm;
        }
    }

    # 项目一
    server {
        listen       80;
        server_name  aaa.xxx.com;

        location / {
            root   C:/www/project1;
            index  index.html index.htm;
        }
    }

    # 项目二
    server {
        listen       80;
        server_name  bbb.xxx.com;

        location / {
            root   C:/www/project2;
            index  index.html index.htm;
        }
    }

    # 项目三http转发到https
    server {
        listen       80;
        server_name  ccc.xxx.com;
        rewrite ^ https://$http_host$request_uri? permanent;
    }

    # 项目三https配置
    server {
        listen       443 ssl;
        server_name  ccc.xxx.com;

        # ssl证书
        ssl_certificate      cert/xxxxxxx.pem;
        ssl_certificate_key  cert/xxxxxxx.key;

        ssl_session_cache    shared:SSL:1m;
        ssl_session_timeout  5m;

        ssl_ciphers  HIGH:!aNULL:!MD5;
        ssl_prefer_server_ciphers  on;

        location / {
            root   C:/www/project3;
            index  index.html index.htm;
        }
    }
}

nginx常用命令:

# linux进入sbin下
./nginx -s reload

# windows在nginx.exe目录
nginx.exe -s reload

 

 

11.1.IDEA热更新

如果修改了页面代码,需要重启服务器才能更新,对IDEA做如下设置:

  1. 按住 Shift+Ctrl+Alt+/,选择Registry;

  2. 找到compiler.automake.allow.when.app.running并勾选;

  3.  

  4. 打开file/settings/Compiler,勾选Build project automatically

  5.  

完成上面三步设置,重启服务器即可热更新,如果还不行,再做如下设置:

第一个选择Update Classes and resources,第二个On frame deactivation:选择Update resources

 

11.2.修改包名后无法运行

一般都是包名没有修改彻底,修改包名建议使用全局替换的方式,对着src目录右键:

 

 

11.3.部署后字体图标不显示

pom.xml文件增加如下配置:

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
    <resources>
        <resource>
            <directory>src/main/resources</directory>
            <filtering>true</filtering>
            <excludes>
                <exclude>**/assets/**</exclude>
            </excludes>
        </resource>
        <resource>
            <directory>src/main/resources</directory>
            <filtering>false</filtering>
            <includes>
                <include>**/assets/**</include>
            </includes>
        </resource>
    </resources>
</build>

原因是maven打包会对项目进行转码,导致文件损坏,assets目录下都是静态资源,无需转码,添加上面代码排除即可。

 

11.4.导入后提示非法字符

导入后运行提示非法字符\ufeff,一些功能提示演示系统不能操作,修改密码提示原始密码不正确等这些问题你需要重新下载最新的压缩包, 提示非法字符你也可以通过IDEA右下角编码切换为GBK再切换为UTF-8解决。

 

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
从基础知识、环境需求、系统安装、开发工具、开发规则到开发实例,由浅入深全面介绍轻开平台的Web和移动App(安卓、IOS)服务器开发过程,非职业程序员一周即能上手开发,有基础的程序员只需一天。 轻开平台CSDN下载:http://download.csdn.net/detail/tx18/8711175 ,百度云盘:http://pan.baidu.com/s/1eQzY418 开发实例:轻开B2C电子商务网站,免费下载:http://download.csdn.net/detail/tx18/8818883 ,百度云盘:http://pan.baidu.com/s/1eQxselW 快速搭建移动(安卓、IOS)App服务器 (win+tomcat+轻开平台+access入门版)下载:http://download.csdn.net/detail/tx18/8737507,百度云盘:http://pan.baidu.com/s/1eQzY418 本次更新的主要内容: 1. 数据库访问模块配置文件database.xml ESql标签:SQL语句 module:连接数据库的模块名,必选项,在WebEasy系统目录下(如D:/webeasy),可以建立多个子目录,如help/base等,在子目录如果有一个database.xml文件,该子目录就会被系统识别为一个可用的数据库连接模块。系统要访问数据库时,首先要根据模块来确定如何与数据库建立持久关系,database.xml文件中保存了连接配置信息 1.1. 配置文件格式及参数 version:xml的版本,默认为1.0即可 encoding:xml字符集,默认为GB2312即可 DbUrl:数据库访问地址,必需。可以为基本的连接(协议:IP或主机名:端口),也可以为完整的连接(基本连接+数据库名、用户名、密码及其他参数) DriverName:jdbc驱动程序包名,可选。在windows下使用ODBC时不用,使用纯JDBC时先把jar驱动包拷贝到项目的WEB-INF/lib目录中 Name:数据库名,可选。如果DbUrl中已经包括时这项可以不用 UserName:用户名,可选。如果DbUrl中已经包括时这项可以不用 Password:密码,可选。如果DbUrl中已经包括时这项可以不用 Unicode:连接中数据内容编码,可选。少部分数据库在识别数据存储字符集时需要(如MySQL),另外,如果DbUrl中已经包括时这项可以不用 max:连接池中最多连接数,可选。为空时系统自动启用缺省值 min:连接池最少连接数,可选。为空时系统自动启用缺省值 age:每个连接最长生命周期(能使用时间,单位为秒),可选。为空时系统自动启用缺省值 timesUsed:每个连接最多能使用次数,可选。为空时系统自动启用缺省值 frequency:连接池刷新周期,单位为秒,可选。为空时系统自动启用缺省值 pause:几次连接不上数据库时暂停连接以节省资源,可选。为空时系统自动启用缺省值 trace:是否输出连接池日志true为输出,false为不输出,缺省为false,可选。为空时系统自动启用缺省值 More:更多参数项,如vertica等MPP架构数据平台,有更多与性能相关的配置项。如果没有,为空即可 weName:数据模块显示名 taskFrequency:后台任务周期,暂未启用 1.2. 部分类型数据库配置文件实例 Access数据库的配置文件实例 ODBC数据源的配置文件实例 Excel表的配置文件实例 SQLServer数据库的配置文件实例 MySQL数据库的配置文件实例 my.ini或my.cnf中的对应配置 max_connect_errors=4294967295 wait_timeout=250 interactive_timeout=250 Oracle数据库的配置文件实例 vertica数据库的配置文件实例
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Adger_mi

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值