动吧旅游生态系统
1 项目简介
1.1 概述
动吧旅游生态系统,应市场高端用户需求,公司决定开发这样的一套旅游系统,此系统包含旅游电商系统(广告子系统,推荐子系统,评价子系统,商品子系统,订单子系统,…),旅游分销系统(分销商的管理),旅游业务系统(产品研发,计调服务,系统权限管理子系统,..),,。。。
1.2 原型分析
基于用户需求,进行原型设计(基于html+css+js进行静态页面实现)。例如系统登录页面:
系统登录成功页面(例如starter.html)
菜单展示页面
说明:原型设计好以后,会与客户进行确认,确认好以后进行签字,然后就是设计和实现.
2 技术架构
2.1 项目分层架构
本项目应用层基于MVC设计思想,进行分层架构设计,其核心目的是将复杂问题简单化,实现各司其职,各尽所能.然后基于“高内聚,低耦合”的设计思想,再实现各对象之间协同,从而提高系统的可维护性,可扩展性。
其中:
1.开放接口层:可直接封装 Service 方法暴露成 RPC (远程过程调用)接口;也可通过 Web 封装成 http 接口;同时也可进行网关安全控制、流量控制等。
2.终端显示层:负责各个端的模板渲染并显示。当前主要是 thymeleaf 渲染,JS 渲染,移动端展示等。
3.Web请求处理层:主要是对访问控制进行转发,请求参数校验,响应结果处理等
4.Service 层:相对具体的业务逻辑服务层(核心业务,扩展业务)。
5.Manager 层:通用业务处理层,它有如下特征:
1) 对第三方平台封装的层,预处理返回结果及转化异常信息;
2) 对 Service 层通用能力的下沉,如缓存方案、中间件通用处理;
3) 与 DAO 层交互,对多个 DAO 的组合复用。
6.DAO 层:数据访问层,与底层 MySQL、Oracle、Hbase 等进行数据交互。
7.外部接口或第三方平台:包括其它部门RPC开放接口,基础平台,其它公司的 HTTP 接口
说明:对如上分层中涉及到知识的点,逐步加强。
总之:分层的目的就是将复杂问题进行拆解,然后分而治,进而提高系统的可扩展性以及可维护性。
2.2 API应用架构
整体API应用架构:
3 技术整合
3.1 环境准备
3.1.1 数据库初始化
启动MySQL客户端并登陆,然后执行
- set names utf8;
- source d:/jtsys.sql
说明:假如在mysql客户端查询表中数据,可以先执行set names gbk,否则可能会出现乱码。还有一点要记住,在拿到任何一个sql脚本文件时,不要上来就执行它,要先打开看一看,检查是否有删除库或删除表的语句,这些语句是否会对你当前数据库中的库和表有影响 ,假如有一定要经过leader审批,才能执行删除等操作.
3.1.2 IDE配置初始化(STS)
- 统一工作区编码(UTF-8)
- 统一JDK版本(JDK1.8)
- 统一MAVEN配置(3.6.3)
3.2 创建项目
- 项目名称:CGB-DB-SYS-V3.01
- 组ID: com.cy
- 打包方式:jar
3.2.1 添加项目依赖
3.2.1 修改配置文件
在application.yml文件中添加如下配置(server,datasource,mybatis,mvc)
#server
server:
port: 80
servlet:
context-path: /
#spring
spring:
datasource:
url: jdbc:mysql:///dbsys?serverTimezone=GMT%2B8&characterEncoding=utf8
username: root
password: root
thymeleaf:
prefix: classpath:/templates/pages/
suffix: .html
#mybatis
mybatis:
configuration:
default-statement-timeout: 30
map-underscore-to-camel-case: true
mapper-locations:
- classpath:/mapper/*/*.xml
#lOG
logging:
level:
com.cy: DEBUG
3.3 首页初始化
3.3.1 定义页面初始资源
- 将js、css、images相关资源拷贝到项目static目录
- 将pages页面拷贝到项目的templates目录
3.3.2 创建页面Controller
创建呈现首页页面的controller对象。
package com.cy.pj.sys.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@RequestMapping("/")
@Controller
public class PageController {
@RequestMapping("doIndexUI")
public String doIndexUI(){
return "starter";
}
}
说明:此controller会作为项目中所有页面访问的入口。
3.3.3 启动项目进行测试
启动tomcat,在地址栏输入localhost/doIndexUI(地址中的端口号要参考自己tomcat启动端口)地址进行访问,假如没有问题会呈现如下页面:
页面访问流程分析,如下图所示(了解):
4 总结
4.1 重点难点分析
- 项目需求调研,分析,原型设计,评审?
- 项目初始化环境配置(数据库,IDE,MAVEN)及运行?
- 项目整体应用分层架构及API设计架构?
4.2 FAQ分析
- 项目是如何进行分层设计的?(MVC)。
- 项目页面是如何设计的? (BootStrap,AdminLTE-网址(adminlte.io))。
- 客户端向服务端发起一个请求,服务端请求处理的一个过程是怎样的?
4.3 BUG分析
- 请求资源不存在,如下图所示:
问题分析:
- 检查url是否正确(是否有对应的映射)
- 检查controller的包是否正确以及是否有对应的注解(例如@Controller)进行描述
- 响应资源解析异常,如下图所示:
问题分析:
- 假如start是模板,检查响应页面是否存在.
- 检查配置文件中视图的前缀、后缀是否正确
- 假如start不是html模板,检查对应的Controller方法上是否有@ReponseBody注解
==========================================
1 日志管理设计说明
1.1业务设计说明
本模块主要是实现对用户行为日志(例如谁在什么时间点执行了什么操作,访问了哪些方法,传递的什么参数,执行时长等)进行记录、查询、删除等操作。其表设计语句如下:
CREATE TABLE `sys_logs` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`username` varchar(50) DEFAULT NULL COMMENT '登陆用户名',
`operation` varchar(50) DEFAULT NULL COMMENT '用户操作',
`method` varchar(200) DEFAULT NULL COMMENT '请求方法',
`params` varchar(5000) DEFAULT NULL COMMENT '请求参数',
`time` bigint(20) NOT NULL COMMENT '执行时长(毫秒)',
`ip` varchar(64) DEFAULT NULL COMMENT 'IP地址',
`createdTime` datetime DEFAULT NULL COMMENT '日志记录时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='系统日志';
1.2 原型设计说明
基于用户需求,实现静态页面(html/css/js),通过静态页面为用户呈现基本需求实现,如下图所示。
说明:假如客户对此原型进行了确认,后续则可以基于此原型进行研发。
1.3 API设计说明
日志业务后台API分层架构及调用关系如下图所示:
说明:分层目的主要将复杂问题简单化,实现各司其职,各尽所能。
2 日志管理列表页面呈现
2.1 业务时序说明
当点击首页左侧的"日志管理"菜单时,其总体时序分析如下图所示:
2.2 服务端实现
2.2.1 Controller实现
- 业务描述与设计实现
基于日志管理的请求业务,在PageController中添加doLogUI方法,doPageUI方法分别用于返回日志列表页面,日志分页页面。
- 关键代码设计与实现
第一步:在PageController中定义返回日志列表的方法。代码如下:
@RequestMapping("log/log_list")
public String doLogUI() {
return "sys/log_list";
}
第二步:在PageController中定义用于返回分页页面的方法。代码如下:
@RequestMapping("doPageUI")
public String doPageUI() {
return "common/page";
}
2.3 客户端实现
2.3.1 日志菜单事件处理
- 业务描述与设计
首先准备日志列表页面(/templates/pages/sys/log_list.html),然后在starter.html页面中点击日志管理菜单时异步加载日志列表页面。
- 关键代码设计与实现
找到项目中的starter.html 页面,页面加载完成以后,注册日志管理菜单项的点击事件,当点击日志管理时,执行事件处理函数。关键代码如下:
$(function(){
doLoadUI("load-log-id","log/log_list")
})
function doLoadUI(id,url){
$("#"+id).click(function(){
$("#mainContentId").load(url);
});
}
其中,load函数为jquery中的ajax异步请求函数。
2.3.2 日志列表页面事件处理
- 业务描述与设计实现
当日志列表页面加载完成以后异步加载分页页面(page.html)。
- 关键代码设计与实现:
在log_list.html页面中异步加载page页面,这样可以实现分页页面重用,哪里需要分页页面,哪里就进行页面加载即可。关键代码如下:
$(function(){
$("#pageId").load("doPageUI");
});
3 日志管理列表数据呈现
3.1 数据架构分析
日志查询服务端数据基本架构,如下图所示。
3.2 服务端API架构及业务时序图分析
服务端日志分页查询代码基本架构,如下图所示:
服务端日志列表数据查询时序图,如下图所示:
3.3 服务端关键业务及代码实现
3.3.1 Entity类实现
- 业务描述及设计实现
构建实体对象(POJO)封装从数据库查询到的记录,一行记录映射为内存中一个的这样的对象。对象属性定义时尽量与表中字段有一定的映射关系,并添加对应的set/get/toString等方法,便于对数据进行更好的操作。
- 关键代码分析及实现
package com.cy.pj.sys.entity;
import java.io.Serializable;
import java.util.Date;
public class SysLog implements Serializable {
private static final long serialVersionUID = 1L;
private Integer id;
//用户名
private String username;
//用户操作
private String operation;
//请求方法
private String method;
//请求参数
private String params;
//执行时长(毫秒)
private Long time;
//IP地址
private String ip;
//创建时间
private Date createdTime;
/**设置:*/
public void setId(Integer id) {
this.id = id;
}
/**获取:*/
public Integer getId() {
return id;
}
/**设置:用户名*/
public void setUsername(String username) {
this.username = username;
}
/** 获取:用户名*/
public String getUsername() {
return username;
}
/**设置:用户操作*/
public void setOperation(String operation) {
this.operation = operation;
}
/**获取:用户操作*/
public String getOperation() {
return operation;
}
/**设置:请求方法*/
public void setMethod(String method) {
this.method = method;
}
/**获取:请求方法*/
public String getMethod() {
return method;
}
/** 设置:请求参数*/
public void setParams(String params) {
this.params = params;
}
/** 获取:请求参数 */
public String getParams() {
return params;
}
/**设置:IP地址 */
public void setIp(String ip) {
this.ip = ip;
}
/** 获取:IP地址*/
public String getIp() {
return ip;
}
/** 设置:创建时间*/
public void setCreateDate(Date createdTime) {
this.createdTime = createdTime;
}
/** 获取:创建时间*/
public Date getCreatedTime() {
return createdTime;
}
public Long getTime() {
return time;
}
public void setTime(Long time) {
this.time = time;
}
}
说明:通过此对象除了可以封装从数据库查询的数据,还可以封装客户端请求数据,实现层与层之间数据的传递。
思考:这个对象的set方法,get方法可能会在什么场景用到?
3.3.2 Dao接口实现
- 业务描述及设计实现
通过数据层对象,基于业务层参数数据查询日志记录总数以及当前页要呈现的用户行为日志信息。
- 关键代码分析及实现:
第一步:定义数据层接口对象,通过将此对象保证给业务层以提供日志数据访问。代码如下:
@Mapper
public interface SysLogDao {
}
第二步:在SysLogDao接口中添加getRowCount方法用于按条件统计记录总数。代码如下
/**
* @param username 查询条件(例如查询哪个用户的日志信息)
* @return 总记录数(基于这个结果可以计算总页数)
*/
int getRowCount(@Param("username") String username);
}
第三步:在SysLogDao接口中添加findPageObjects方法,基于此方法实现当前页记录的数据查询操作。代码如下:
/**
* @param username 查询条件(例如查询哪个用户的日志信息)
* @param startIndex 当前页的起始位置
* @param pageSize 当前页的页面大小
* @return 当前页的日志记录信息
* 数据库中每条日志信息封装到一个SysLog对象中
*/
List<SysLog> findPageObjects(
@Param("username")String username,
@Param("startIndex")Integer startIndex,
@Param("pageSize")Integer pageSize);
说明:
- 当DAO中方法参数多余一个时尽量使用@Param注解进行修饰并指定名字,然后在Mapper文件中便可以通过类似#{username}方式进行获取,否则只能通过#{arg0},#{arg1}或者#{param1},#{param2}等方式进行获取。
- 当DAO方法中的参数应用在动态SQL中时无论多少个参数,尽量使用@Param注解进行修饰并定义。
3.3.3 Mapper文件的实现
- 业务描述及设计实现
基于Dao接口创建映射文件,在此文件中通过相关元素(例如select)描述要执行的数据操作。
- 关键代码设计及实现
第一步:在映射文件的设计目录(mapper/sys)中添加SysLogMapper.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="com.cy.pj.sys.dao.SysLogDao">
</mapper>
第二步:在映射文件中添加sql元素实现,SQL中的共性操作,代码如下:
<sql id="queryWhereId">
from sys_Logs
<where>
<if test="username!=null and username!=''">
username like concat("%",#{username},"%")
</if>
</where>
</sql>
第三步:在映射文件中添加id为getRowCount元素,按条件统计记录总数,代码如下:
<select id="getRowCount"
resultType="int">
select count(*)
<include refid="queryWhereId"/>
</select>
第四步:在映射文件中添加id为findPageObjects元素,实现分页查询。代码如下:
<select id="findPageObjects"
resultType="com.cy.pj.sys.entity.SysLog">
select *
<include refid="queryWhereId"/>
order by createdTime desc
limit #{startIndex},#{pageSize}
</select>
思考:
- 动态sql:基于用户需求动态拼接SQL
- Sql标签元素的作用是什么?对sql语句中的共性进行提取,以遍实现更好的复用.
- Include标签的作用是什么?引入使用sql标签定义的元素
第五步:单元测试类SysLogDaoTests,对数据层方法进行测试。
第五步:单元测试类SysLogDaoTests,对数据层方法进行测试。
package com.cy.pj.sys.dao;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import com.cy.pj.sys.entity.SysLog;
@SpringBootTest
public class SysLogDaoTests {
@Autowired
private SysLogDao sysLogDao;
@Test
public void testGetRowCount() {
int rows=sysLogDao.getRowCount("admin");
System.out.println("rows="+rows);
}
@Test
public void testFindPageObjects() {
List<SysLog> list=
sysLogDao.findPageObjects("admin", 0, 3);
for(SysLog log:list) {
System.out.println(log);
}
}
}
3.3.4 Service接口及实现类
- 业务描述与设计实现
业务层主要是实现模块中业务逻辑的处理。在日志分页查询中,业务层对象首先要通过业务方法中的参数接收控制层数据(例如username,pageCurrent)并校验。然后基于用户名进行总记录数的查询并校验,再基于起始位置及页面大小进行当前页记录的查询,最后对查询结果进行封装并返回。
- 关键代码设计及实现
业务值对象定义,基于此对象封装数据层返回的数据以及计算的分页信息,具体代码参考如下:
package com.cy.pj.common.bo;
public class PageObject<T> implements Serializable {
private static final long serialVersionUID = 6780580291247550747L;//类泛型
/**当前页的页码值*/
private Integer pageCurrent=1;
/**页面大小*/
private Integer pageSize=3;
/**总行数(通过查询获得)*/
private Integer rowCount=0;
/**总页数(通过计算获得)*/
private Integer pageCount=0;
/**当前页记录*/
private List<T> records;
public PageObject(){}
public PageObject(Integer pageCurrent, Integer pageSize, Integer rowCount, List<T> records) {
super();
this.pageCurrent = pageCurrent;
this.pageSize = pageSize;
this.rowCount = rowCount;
this.records = records;
// this.pageCount=rowCount/pageSize;
// if(rowCount%pageSize!=0) {
// pageCount++;
// }
this.pageCount=(rowCount-1)/pageSize+1;
}
public Integer getPageCurrent() {
return pageCurrent;
}
public void setPageCurrent(Integer pageCurrent) {
this.pageCurrent = pageCurrent;
}
public Integer getPageSize() {
return pageSize;
}
public void setPageSize(Integer pageSize) {
this.pageSize = pageSize;
}
public Integer getRowCount() {
return rowCount;
}
public void setRowCount(Integer rowCount) {
this.rowCount = rowCount;
}
public Integer getPageCount() {
return pageCount;
}
public void setPageCount(Integer pageCount) {
this.pageCount = pageCount;
}
public List<T> getRecords() {
return records;
}
public void setRecords(List<T> records) {
this.records = records;
}
}
定义日志业务接口及方法,暴露外界对日志业务数据的访问,其代码参考如下:
package com.cy.pj.sys.service;
public interface SysLogService {
/**
* @param name 基于条件查询时的参数名
* @param pageCurrent 当前的页码值
* @return 当前页记录+分页信息
*/
PageObject<SysLog> findPageObjects(
String username,
Integer pageCurrent);
}
日志业务接口及实现类,用于具体执行日志业务数据的分页查询操作,其代码如下:
package com.cy.pj.sys.service.impl;
@Service
public class SysLogServiceImpl implements SysLogService{
@Autowired
private SysLogDao sysLogDao;
@Override
public PageObject<SysLog> findPageObjects(
String name, Integer pageCurrent) {
//1.验证参数合法性
//1.1验证pageCurrent的合法性,
//不合法抛出IllegalArgumentException异常
if(pageCurrent==null||pageCurrent<1)
throw new IllegalArgumentException("当前页码不正确");
//2.基于条件查询总记录数
//2.1) 执行查询
int rowCount=sysLogDao.getRowCount(name);
//2.2) 验证查询结果,假如结果为0不再执行如下操作
if(rowCount==0)
throw new ServiceException("系统没有查到对应记录");
//3.基于条件查询当前页记录(pageSize定义为2)
//3.1)定义pageSize
int pageSize=2;
//3.2)计算startIndex
int startIndex=(pageCurrent-1)*pageSize;
//3.3)执行当前数据的查询操作
List<SysLog> records=
sysLogDao.findPageObjects(name, startIndex, pageSize);
//4.对分页信息以及当前页记录进行封装
//4.1)构建PageObject对象
PageObject<SysLog> pageObject=new PageObject<>();
//4.2)封装数据
pageObject.setPageCurrent(pageCurrent);
pageObject.setPageSize(pageSize);
pageObject.setRowCount(rowCount);
pageObject.setRecords(records);
pageObject.setPageCount((rowCount-1)/pageSize+1);
//5.返回封装结果。
return pageObject;
}
}
在当前方法中需要的ServiceException是一个自己定义的异常, 通过自定义异常可更好的实现对业务问题的描述,同时可以更好的提高用户体验。参考代码如下:
package com.cy.pj.common.exception;
public class ServiceException extends RuntimeException {
private static final long serialVersionUID = 7793296502722655579L;
public ServiceException() {
super();
}
public ServiceException(String message) {
super(message);
// TODO Auto-generated constructor stub
}
public ServiceException(Throwable cause) {
super(cause);
// TODO Auto-generated constructor stub
}
}
说明:几乎在所有的框架中都提供了自定义异常,例如MyBatis中的BindingException等。
定义Service对象的单元测试类,代码如下:
package com.cy.pj.sys.service;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import com.cy.pj.common.vo.PageObject;
import com.cy.pj.sys.entity.SysLog;
@SpringBootTest
public class SysLogServiceTests {
@Autowired
private SysLogService sysLogService;
@Test
public void testFindPageObjects() {
PageObject<SysLog> pageObject=
sysLogService.findPageObjects("admin", 1);
System.out.println(pageObject);
}
}
3.3.5 Controller类实现
- 业务描述与设计实现
控制层对象主要负责请求和响应数据的处理,例如,本模块首先要通过控制层对象处理请求参数,然后通过业务层对象执行业务逻辑,再通过VO对象封装响应结果(主要对业务层数据添加状态信息),最后将响应结果转换为JSON格式的字符串响应到客户端。
- 关键代码设计与实现
定义控制层值对象(VO),目的是基于此对象封装控制层响应结果(在此对象中主要是为业务层执行结果添加状态信息)。Spring MVC框架在响应时可以调用相关API(例如jackson)将其对象转换为JSON格式字符串。
package com.cy.pj.common.vo;
public class JsonResult implements Serializable {
private static final long serialVersionUID = -856924038217431339L;//SysResult/Result/R
/**状态码*/
private int state=1;//1表示SUCCESS,0表示ERROR
/**状态信息*/
private String message="ok";
/**正确数据*/
private Object data;
public JsonResult() {}
public JsonResult(String message){
this.message=message;
}
/**一般查询时调用,封装查询结果*/
public JsonResult(Object data) {
this.data=data;
}
/**出现异常时时调用*/
public JsonResult(Throwable t){
this.state=0;
this.message=t.getMessage();
}
public int getState() {
return state;
}
public void setState(int state) {
this.state = state;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
}
定义Controller类,并将此类对象使用Spring框架中的@Controller注解进行标识,表示此类对象要交给Spring管理。然后基于@RequestMapping注解为此类定义根路径映射。代码参考如下:
package com.cy.pj.sys.controller;
@Controller
@RequestMapping("/log/")
public class SysLogController {
@Autowired
private SysLogService sysLogService;
}
在Controller类中添加分页请求处理方法,代码参考如下:
@RequestMapping("doFindPageObjects")
@ResponseBody
public JsonResult doFindPageObjects(String username,Integer pageCurrent){
PageObject<SysLog> pageObject=
sysLogService.findPageObjects(username,pageCurrent);
return new JsonResult(pageObject);
}
定义全局异常处理类,对控制层可能出现的异常,进行统一异常处理,代码如下:
package com.cy.pj.common.web;
import java.util.logging.Logger;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import com.cy.pj.common.vo.JsonResult;
@ControllerAdvice
public class GlobalExceptionHandler {
//JDK中的自带的日志API
@ExceptionHandler(RuntimeException.class)
@ResponseBody
public JsonResult doHandleRuntimeException(
RuntimeException e){
e.printStackTrace();//也可以写日志
异常信息
return new JsonResult(e);//封装
}
}
控制层响应数据处理分析,如下图所示:
3.4 客户端关键业务及代码实现
3.4.1 客户端页面事件分析
当用户点击首页日志管理时,其页面流转分析如下图所示:
3.4.2 日志列表信息呈现
- 业务描述与设计实现
日志分页页面加载完成以后,向服务端发起异步请求加载日志信息,当日志信息加载完成需要将日志信息、分页信息呈现到列表页面上。
- 关键代码设计与实现
第一步:分页页面加载完成,向服务端发起异步请求,代码参考如下:
$(function(){
//为什么要将doGetObjects函数写到load函数对应的回调内部。
$("#pageId").load("doPageUI",function(){
doGetObjects();
});
});
第二步:定义异步请求处理函数,代码参考如下:
function doGetObjects(){
//debugger;//断点调试
//1.定义url和参数
var url="log/doFindPageObjects"
var params={"pageCurrent":1};//pageCurrent=2
//2.发起异步请求
//请问如下ajax请求的回调函数参数名可以是任意吗?//可以,必须符合标识符的规范
$.getJSON(url,params,function(result){
//请问result是一个字符串还是json格式的js对象?对象
doHandleQueryResponseResult(result);
}
);//特殊的ajax函数
}
result 结果对象分析,如图-9所示:
第三步:定义回调函数,处理服务端的响应结果。代码如下:
function doHandleQueryResponseResult (result){ //JsonResult
if(result.state==1){//ok
//更新table中tbody内部的数据
doSetTableBodyRows(result.data.records);//将数据呈现在页面上
//更新页面page.html分页数据
//doSetPagination(result.data); //此方法写到page.html中
}else{
alert(result.message);
}
}
第四步:将异步响应结果呈现在table的tbody位置。代码参考如下:
function doSetTableBodyRows(records){
//1.获取tbody对象,并清空对象
var tBody=$("#tbodyId");
tBody.empty();
//2.迭代records记录,并将其内容追加到tbody
for(var i in records){
//2.1 构建tr对象
var tr=$("<tr></tr>");
//2.2 构建tds对象
var tds=doCreateTds(records[i]);
//2.3 将tds追加到tr中
tr.append(tds);
//2.4 将tr追加到tbody中
tBody.append(tr);
}
}
第五步:创建每行中的td元素,并填充具体业务数据。代码参考如下:
function doCreateTds(data){
var tds="<td><input type='checkbox' class='cBox' name='cItem' value='"+data.id+"'></td>"+
"<td>"+data.username+"</td>"+
"<td>"+data.operation+"</td>"+
"<td>"+data.method+"</td>"+
"<td>"+data.params+"</td>"+
"<td>"+data.ip+"</td>"+
"<td>"+data.time+"</td>";
return tds;
}
3.4.3 分页数据信息呈现
- 业务描述与设计实现
日志信息列表初始化完成以后初始化分页数据(调用setPagination函数),然后再点击上一页,下一页等操作时,更新页码值,执行基于当前页码值的查询。
- 关键代码设计与实现:
第一步:在page.html页面中定义doSetPagination方法(实现分页数据初始化),代码如下:
function doSetPagination(page){
//1.始化数据
$(".rowCount").html("总记录数("+page.rowCount+")");
$(".pageCount").html("总页数("+page.pageCount+")");
$(".pageCurrent").html("当前页("+page.pageCurrent+")");
//2.绑定数据(为后续对此数据的使用提供服务)
$("#pageId").data("pageCurrent",page.pageCurrent);
$("#pageId").data("pageCount",page.pageCount);
}
第二步:分页页面page.html中注册点击事件。代码如下:
$(function(){
//事件注册
$("#pageId").on("click",".first,.pre,.next,.last",doJumpToPage);
})
第三步:定义doJumpToPage方法(通过此方法实现当前数据查询)
function doJumpToPage(){
//1.获取点击对象的class值
var cls=$(this).prop("class");//Property
//2.基于点击的对象执行pageCurrent值的修改
//2.1获取pageCurrent,pageCount的当前值
var pageCurrent=$("#pageId").data("pageCurrent");
var pageCount=$("#pageId").data("pageCount");
//2.2修改pageCurrent的值
if(cls=="first"){//首页
pageCurrent=1;
}else if(cls=="pre"&&pageCurrent>1){//上一页
pageCurrent--;
}else if(cls=="next"&&pageCurrent<pageCount){//下一页
pageCurrent++;
}else if(cls=="last"){//最后一页
pageCurrent=pageCount;
}else{
return;
}
//3.对pageCurrent值进行重新绑定
$("#pageId").data("pageCurrent",pageCurrent);
//4.基于新的pageCurrent的值进行当前页数据查询
doGetObjects();
}
修改分页查询方法:(看黄色底色部分)
function doGetObjects(){
//debugger;//断点调试
//1.定义url和参数
var url="log/doFindPageObjects"
//? 请问data函数的含义是什么?(从指定元素上获取绑定的数据)
//此数据会在何时进行绑定?(setPagination,doQueryObjects)
var pageCurrent=$("#pageId").data("pageCurrent");
//为什么要执行如下语句的判定,然后初始化pageCurrent的值为1
//pageCurrent参数在没有赋值的情况下,默认初始值应该为1.
if(!pageCurrent) pageCurrent=1;
var params={"pageCurrent":pageCurrent};//pageCurrent=2
//2.发起异步请求
//请问如下ajax请求的回调函数参数名可以是任意吗?可以,必须符合标识符的规范
$.getJSON(url,params,function(result){
//请问result是一个字符串还是json格式的js对象?对象
doHandleQueryResponseResult(result);
}
);//特殊的ajax函数 }
3.4.4 列表页面信息查询实现
- 业务描述及设计
当用户点击日志列表的查询按钮时,基于用户输入的用户名进行有条件的分页查询,并将查询结果呈现在页面。
- 关键代码设计与实现:
第一步:日志列表页面加载完成,在查询按钮上进行事件注册。代码如下:
$(".input-group-btn").on("click",".btn-search",doQueryObjects)
第二步:定义查询按钮对应的点击事件处理函数。代码如下:
function doQueryObjects(){
//为什么要在此位置初始化pageCurrent的值为1?
//数据查询时页码的初始位置也应该是第一页
$("#pageId").data("pageCurrent",1);
//为什么要调用doGetObjects函数?
//重用js代码,简化jS代码编写。
doGetObjects();
}
第三步:在分页查询函数中追加name参数定义(看黄色底色部分),代码如下:
function doGetObjects(){
//debugger;//断点调试
//1.定义url和参数
var url="log/doFindPageObjects"
//? 请问data函数的含义是什么?(从指定元素上获取绑定的数据)
//此数据会在何时进行绑定?(setPagination,doQueryObjects)
var pageCurrent=$("#pageId").data("pageCurrent");
//为什么要执行如下语句的判定,然后初始化pageCurrent的值为1
//pageCurrent参数在没有赋值的情况下,默认初始值应该为1.
if(!pageCurrent) pageCurrent=1;
var params={"pageCurrent":pageCurrent};
//为什么此位置要获取查询参数的值?
//一种冗余的应用方法,目的时让此函数在查询时可以重用。
var username=$("#searchNameId").val();
//如下语句的含义是什么?动态在json格式的js对象中添加key/value,
if(username) params.username=username;//查询时需要
//2.发起异步请求
//请问如下ajax请求的回调函数参数名可以是任意吗?可以,必须符合标识符的规范
$.getJSON(url,params,function(result){
//请问result是一个字符串还是json格式的js对象?对象
doHandleQueryResponseResult(result);
}
);
}
4 日志管理删除操作实现
4.1 数据架构分析
当用户执行日志删除操作时,客户端与服务端交互时的基本数据架构,如下图所示。
4.1 删除业务时序分析
客户端提交删除请求,服务端对象的工作时序分析,如下图所示。
4.3 服务端关键业务及代码实现
4.3.1 Dao接口实现
- 业务描述及设计实现
数据层基于业务层提交的日志记录id,进行日志删除操作。
- 关键代码设计及实现:
在SysLogDao中添加基于id执行日志删除的方法。代码参考如下:
int deleteObjects(@Param("ids")Integer… ids);
4.3.2 Mapper文件实现
- 业务描述及设计实现
在SysLogDao接口对应的映射文件中添加用于执行删除业务的delete元素,此元素内部定义具体的SQL实现。
- 关键代码设计与实现
在SysLogMapper.xml文件添加delete元素,关键代码如下:
<delete id="deleteObjects">
delete from sys_Logs
where id in
<foreach collection="ids"
open="("
close=")"
separator=","
item="id">
#{id}
</foreach>
</delete>
FAQ分析:如上SQL实现可能会存在什么问题?(可靠性问题,性能问题)
从可靠性的角度分析,假如ids的值为null或长度为0时,SQL构建可能会出现语法问题,可参考如下代码进行改进(先对ids的值进行判定):
<delete id="deleteObjects">
delete from sys_logs
<if test="ids!=null and ids.length>0">
where id in
<foreach collection="ids"
open="("
close=")"
separator=","
item="id">
#{id}
</foreach>
</if>
<if test="ids==null or ids.length==0">
where 1=2
</if>
</delete>
从SQL执行性能角度分析,一般在SQL语句中不建议使用in表达式,可以参考如下代码进行实现(重点是forearch中or运算符的应用):
<delete id="deleteObjects">
delete from sys_logs
<choose>
<when test="ids!=null and ids.length>0">
<where>
<foreach collection="ids"
item="id"
separator="or">
id=#{id}
</foreach>
</where>
</when>
<otherwise>
where 1=2
</otherwise>
</choose>
</delete>
说明:这里的choose元素也为一种选择结构,when元素相当于if,otherwise相当于else的语法。
4.3.3 Service接口及实现类
- 业务描述与设计实现
在日志业务层定义用于执行删除业务的方法,首先通过方法参数接收控制层传递的多个记录的id,并对参数id进行校验。然后基于日志记录id执行删除业务实现。最后返回业务执行结果。
- 关键代码设计与实现
第一步:在SysLogService接口中,添加基于多个id进行日志删除的方法。关键代码如下:
int deleteObjects(Integer… ids) {}
第二步:在SysLogServiceImpl实现类中添加删除业务的具体实现。关键代码如下:
@Override
public int deleteObjects(Integer… ids) {
//1.判定参数合法性
if(ids==null||ids.length==0)
throw new IllegalArgumentException("请选择一个");
//2.执行删除操作
int rows;
try{
rows=sysLogDao.deleteObjects(ids);
}catch(Throwable e){
e.printStackTrace();
//发出报警信息(例如给运维人员发短信)
throw new ServiceException("系统故障,正在恢复中...");
}
//4.对结果进行验证
if(rows==0)
throw new ServiceException("记录可能已经不存在");
//5.返回结果
return rows;
}
4.3.4 Controller类实现
- 业务描述与设计实现
在日志控制层对象中,添加用于处理日志删除请求的方法。首先在此方法中通过形参接收客户端提交的数据,然后调用业务层对象执行删除操作,最后封装执行结果,并在运行时将响应对象转换为JSON格式的字符串,响应到客户端。
- 关键代码设计与实现
第一步:在SysLogController中添加用于执行删除业务的方法。代码如下:
@RequestMapping("doDeleteObjects")
@ResponseBody
public JsonResult doDeleteObjects(Integer… ids){
sysLogService.deleteObjects(ids);
return new JsonResult("delete ok");
}
第二步:启动tomcat进行访问测试,打开浏览器输入如下网址:
http://localhost/log/doDeleteObjects?ids=1,2,3
4.4 客户端关键业务及代码实现
4.4.1 日志列表页面事件处理
- 业务描述及设计实现
用户在页面上首先选择要删除的元素,然后点击删除按钮,将用户选择的记录id异步提交到服务端,最后在服务端执行日志的删除动作。
- 关键代码设计与实现
第一步:页面加载完成以后,在删除按钮上进行点击事件注册。关键代码如下:
...
$(".input-group-btn")
.on("click",".btn-delete",doDeleteObjects)
...
第二步:定义删除操作对应的事件处理函数。关键代码如下:
function doDeleteObjects(){
//1.获取选中的id值
var ids=doGetCheckedIds();
if(ids.length==0){
alert("至少选择一个");
return;
}
//2.发异步请求执行删除操作
var url="log/doDeleteObjects";
var params={"ids":ids.toString()};
console.log(params);
$.post(url,params,function(result){
if(result.state==1){
alert(result.message);
doGetObjects();
}else{
alert(result.message);
}
});
}
第三步:定义获取用户选中的记录id的函数。关键代码如下:
function doGetCheckedIds(){
//定义一个数组,用于存储选中的checkbox的id值
var array=[];//new Array();
//获取tbody中所有类型为checkbox的input元素
$("#tbodyId input[type=checkbox]").
//迭代这些元素,每发现一个元素都会执行如下回调函数
each(function(){
//假如此元素的checked属性的值为true
if($(this).prop("checked")){
//调用数组对象的push方法将选中对象的值存储到数组
array.push($(this).val());
}
});
return array;
}
第四步:Thead中全选元素的状态影响tbody中checkbox对象状态。代码如下:
function doChangeTBodyCheckBoxState(){
//1.获取当前点击对象的checked属性的值
var flag=$(this).prop("checked");//true or false
//2.将tbody中所有checkbox元素的值都修改为flag对应的值。
//第一种方案
/* $("#tbodyId input[name='cItem']")
.each(function(){
$(this).prop("checked",flag);
}); */
//第二种方案
$("#tbodyId input[type='checkbox']")
.prop("checked",flag);
}
第五步:Tbody中checkbox的状态影响thead中全选元素的状态。代码如下:
function doChangeTHeadCheckBoxState(){
//1.设定默认状态值
var flag=true;
//2.迭代所有tbody中的checkbox值并进行与操作
$("#tbodyId input[type='checkbox']")
.each(function(){
flag=flag&$(this).prop("checked")
});
//3.修改全选元素checkbox的值为flag
$("#checkAll").prop("checked",flag);
}
第六步:完善业务刷新方法,当在最后一页执行删除操作时,基于全选按钮状态及当前页码值,刷新页面。关键代码如下:
function doRefreshAfterDeleteOK(){
var pageCount=$("#pageId").data("pageCount");
var pageCurrent=$("#pageId").data("pageCurrent");
var checked=$("#checkAll").prop("checked");
if(pageCurrent==pageCount&&checked&&pageCurrent>1){
pageCurrent--;
$("#pageId").data("pageCurrent",pageCurrent);
}
doGetObjects();
}
说明:最后将如上方法添加在删除操作成功以后的代码块中。
5 日志管理数据添加实现
5.1服务端关键业务及代码实现
这块业务用AOP后面实现.
5.1.1 Dao接口实现
- 业务描述与设计实现
数据层基于业务层的持久化请求,将业务层提交的用户行为日志信息写入到数据库。
- 关键代码设计与实现
在SysLogDao接口中添加用于实现日志信息持久化的方法。关键代码如下:
int insertObject(SysLog entity);
5.2.1 Mapper映射文件
- 业务描述与设计实现
基于SysLogDao中方法的定义,编写用于数据持久化的SQL元素。
- 关键代码设计与实现
在SysLogMapper.xml中添加insertObject元素,用于向日志表写入用户行为日志。关键代码如下:
<insert id="insertObject">
insert into sys_logs
(username,operation,method,params,time,ip,createdTime)
values
(#{username},#{operation},#{method},#{params},#{time},#{ip},#{createdTime})
</insert>
5.2.3 Service接口及实现类
- 业务描述与设计实现
将日志切面中抓取到的用户行为日志信息,通过业务层对象方法持久化到数据库。
- 关键代码实现
第一步:在SysLogService接口中,添加保存日志信息的方法。关键代码如下:
void saveObject(SysLog entity)
第二步:在SysLogServiceImpl类中添加,保存日志的方法实现。关键代码如下:
@Override
public void saveObject(SysLog entity) {
sysLogDao.insertObject(entity);
}
5.1.4 日志切面Aspect实现
- 业务描述与设计实现
在日志切面中,抓取用户行为信息,并将其封装到日志对象然后传递到业务,通过业务层对象对日志日志信息做进一步处理。此部分内容后续结合AOP进行实现(暂时先了解,不做具体实现)。
- 关键代码设计与实现
定义日志切面类对象,通过环绕通知处理日志记录操作。关键代码如下:
@Aspect
@Component
public class SysLogAspect {
private Logger log=LoggerFactory.getLogger(SysLogAspect.class);
@Autowired
private SysLogService sysLogService;
@Pointcut("@annotation(com.cy.pj.common.annotation.RequiredLog)")
public void logPointCut(){}
@Around("logPointCut()")
public Object around(ProceedingJoinPoint //连接点
jointPoint) throws Throwable{
long startTime=System.currentTimeMillis();
//执行目标方法(result为目标方法的执行结果)
Object result=jointPoint.proceed();
long endTime=System.currentTimeMillis();
long totalTime=endTime-startTime;
log.info("方法执行的总时长为:"+totalTime);
saveSysLog(jointPoint,totalTime);
return result;
}
private void saveSysLog(ProceedingJoinPoint point,
long totleTime) throws NoSuchMethodException, SecurityException, JsonProcessingException{
//1.获取日志信息
MethodSignature ms= (MethodSignature)point.getSignature();
Class<?> targetClass=point.getTarget().getClass();
String className=targetClass.getName();
//获取接口声明的方法
String methodName=ms.getMethod().getName();
Class<?>[] parameterTypes=ms.getMethod().getParameterTypes();
//获取目标对象方法(AOP版本不同,可能获取方法对象方式也不同)
Method targetMethod=targetClass.getDeclaredMethod(
methodName,parameterTypes);
//获取用户名,学完shiro再进行自定义实现,没有就先给固定值
String username=ShiroUtils.getPrincipal().getUsername();
//获取方法参数
Object[] paramsObj=point.getArgs();
System.out.println("paramsObj="+paramsObj);
//将参数转换为字符串
String params=new ObjectMapper()
.writeValueAsString(paramsObj);
//2.封装日志信息
SysLog log=new SysLog();
log.setUsername(username);//登陆的用户
//假如目标方法对象上有注解,我们获取注解定义的操作值
RequiredLog requestLog=
targetMethod.getDeclaredAnnotation(RequiredLog.class);
if(requestLog!=null){
log.setOperation(requestLog.value());
}
log.setMethod(className+"."+methodName);//className.methodName()
log.setParams(params);//method params
log.setIp(IPUtils.getIpAddr());//ip 地址
log.setTime(totleTime);//
log.setCreateDate(new Date());
//3.保存日志信息
sysLogService.saveObject(log);
}
}
方法中用到的ip地址获取需要提供一个如下的工具类:(不用自己实现,直接用)
public class IPUtils {
private static Logger logger = LoggerFactory.getLogger(IPUtils.class);
public static String getIpAddr() {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String ip = null;
try {
ip = request.getHeader("x-forwarded-for");
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (StringUtils.isEmpty(ip) || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
} catch (Exception e) {
logger.error("IPUtils ERROR ", e);
}
return ip;
}
}
原理分析,如下图所示:
6 总结
6.1 重难点分析
- 日志管理整体业务分析与实现。
- 分层架构(应用层MVC:基于spring的mvc模块)。
- API架构(SysLogDao,SysLogService,SysLogController)。
- 业务架构(查询,删除,添加用户行为日志)。
- 数据架构(SysLog,PageObject,JsonResult,..)。
- 日志管理持久层映射文件中SQL元素的定义及编写。
- 定义在映射文件”mapper/sys/SysLogMapper.xml”(必须在加载范围内)。
- 每个SQL元素必须提供一个唯一ID,对于select必须指定结果映射(resultType)。
- 系统底层运行时会将每个SQL元素的对象封装一个值对象(MappedStatement)。
- 日志管理模块数据查询操作中的数据封装。
- 数据层(数据逻辑)的SysLog对象应用(一行记录一个log对象)。
- 业务层(业务逻辑)PageObject对象应用(封装每页记录以及对应的分页信息)。
- 控制层(控制逻辑)的JsonResult对象应用(对业务数据添加状态信息)。
- 日志管理控制层请求数据映射,响应数据的封装及转换(转换为json 串)。
- 请求路径映射,请求方式映射(GET,POST),请求参数映射(直接量,POJO)。
- 响应数据两种(页面,JSON串)。
- 日志管理模块异常处理如何实现的。
- 请求处理层(控制层)定义统一(全局)异常处理类。
- 使用注解@ControllerAdvice描述类,使用@ExceptionHandler描述方法.
- 异常处理规则:能处理则处理,不能处理则抛出。
6.2 FAQ分析
- 用户行为日志表中都有哪些字段?(面试时有时会问)
- 用户行为日志是如何实现分页查询的?(limit)
- 用户行为数据的封装过程?(数据层,业务层,控制层)
- 项目中的异常是如何处理的?
- 页面中数据乱码,如何解决?(数据来源,请求数据,响应数据)
- 说说的日志删除业务是如何实现?
- Spring MVC 响应数据处理?(view,json)
- 项目你常用的JS函数说几个?(data,prop,ajax,each,..)
- MyBatis中的@Params注解的作用?(为参数变量指定其其别名)
- Jquery中data函数用于做什么?可以借助data函数将数据绑定到指定对象,语法为data(key[,value]),key和value为自己业务中的任意数据,假如只有key表示取值。
- Jquery中的prop函数用于获取html标签对象中”标准属性”的值或为属性赋值,其语法为prop(propertyName[,propertyValue]),假如只有属性名则为获取属性值。
- Jquery中attr函数为用户获取html标签中任意属性值或为属性赋值的一个方法,其语法为attr(propertyName[,propertyValue]),假如只有属性名则为获取属性值。
- 日志写操作事务的传播特性如何配置?(每次开启新事务)?
- 日志写操作为什么应该是异步的?
- Spring 中的异步操作如何实现?
- Spring 中的@Async如何应用?
- 项目中的BUG分析及解决套路?(排除法,打桩,断点)
6.3 BUG分析
- 无法找到对应的Bean对象(NoSuchBeanDefinitionException),如下图所示:
问题分析:
- 检测key的名字写的是否正确。
- 检测spring对此Bean对象的扫描,对于dao而言。
- 使用有@Mapper注解描述或者在@MapperScan扫描范围之内。
- 以上都正确,要检测是否编译了。
- 绑定异常(BindingException),如下图所示。
问题分析:
- 接口的类全名与对应的映射文件命名空间不同。
- 接口的方法名与对应的映射文件元素名不存在。
- 检测映射文件的路径与application.properties或者application.yml中的配置是否一致。
- 以上都没有问题时,检测你的类和映射文件是否正常编译。
- 反射异常(ReflectionException),如下图所示
问题分析:
- 映射文件中动态sql中使用的参数在接口方法中没有使用@Param注解修饰
- 假如使用了注解修饰还要检测名字是否一致。
说明:当动态sql的参数在接口中没有使用@Param注解修饰,还可以借助_parameter这个变量获取参数的值(mybatis中的一种规范)。
- 结果映射异常,如下图所示:
问题分析:getRowCount元素可能没有写resultType或resultMap。
- 绑定异常,如下图所示:
问题分析:绑定异常,检测findPageObjects方法参数与映射文件参数名字是否匹配或者假如版本不是最新版本需要使用@Param注解描述。
- Bean创建异常,如下图所示
问题分析:应该是查询时的结果映射对的类全名写错了。
- 请求方式不匹配,如下图示
问题分析:请求方式与控制层处理方式不匹配。
- 响应结果异常,如下图所示:
问题分析:服务端响应数据不正确,例如服务端没有注册将对象转换为JSON串的Bean
- 请求参数异常,如下图示:
问题分析:客户端请求参数中不包含服务端控制层方法参数或格式不匹配。
- JS编写错误,如下图所示:
问题分析:点击右侧VM176:64位置进行查看。
- JS编写错误,如图下所示:
问题分析:找到对应位置,检测data的值以及数据来源。
- JS编写错误,如下图所示:
问题分析:找到对应位置,假如无法确定位置,可排除法或打桩,debug分析。
- JS写错误,如下图所示:
问题分析:调用length方法的对象有问题,可先检测下对象的值。
- JS编写错误,如下图所示:
问题分析:检测record定义或赋值的地方。
资源没找到,如下图所示:
问题分析:服务端资源没找到,检查url和controller映射,不要点击图中的jquery。
- 视图解析异常,如下图所示:
问题分析:检查服务端要访问的方法上是否有@ResponseBody注解.
==============================================
1 菜单管理设计说明
1.1 业务设计说明
菜单管理又称为资源管理,是系统资源对外的表现形式。本模块主要是实现对菜单进行添加、修改、查询、删除等操作。其表设计语句如下:
CREATE TABLE `sys_menus` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(50) DEFAULT NULL COMMENT '资源名称',
`url` varchar(200) DEFAULT NULL COMMENT '资源URL',
`type` int(11) DEFAULT NULL COMMENT '类型 1:菜单 2:按钮',
`sort` int(11) DEFAULT NULL COMMENT '排序',
`note` varchar(100) DEFAULT NULL COMMENT '备注',
`parentId` int(11) DEFAULT NULL COMMENT '父菜单ID,一级菜单为0',
`permission` varchar(500) DEFAULT NULL COMMENT '授权(如:sys:user:create)',
`createdTime` datetime DEFAULT NULL COMMENT '创建时间',
`modifiedTime` datetime DEFAULT NULL COMMENT '修改时间',
`createdUser` varchar(20) DEFAULT NULL COMMENT '创建用户',
`modifiedUser` varchar(20) DEFAULT NULL COMMENT '修改用户',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='资源管理';
菜单表与角色表是多对多的关系,在表设计时,多对多关系通常由中间表(关系表)进行维护,如下图所示:
基于角色菜单表的设计,其角色和菜单对应的关系数据要存储到关系表中,其具体存储形式,如下图所示:
菜单与角色的关系表脚本设计如下:
CREATE TABLE `sys_role_menus` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`role_id` int(11) DEFAULT NULL COMMENT '角色ID',
`menu_id` int(11) DEFAULT NULL COMMENT 'ID',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='角色与菜单对应关系';
1.2 原型设计说明
基于用户需求,实现菜单静态页面(html/css/js),通过静态页面为用户呈现菜单模块的基本需求实现。
当在主页左侧菜单栏,点击菜单管理时,在主页内容呈现区,呈现菜单列表页面,如下图所示。
在菜单编辑页面选择上级菜单时,异步加载菜单信息,并以树结构的形式呈现上级菜单,如下图所示。
说明:假如客户对此原型进行了确认,后续则可以基于此原型进行研发。
1.3 API设计说明
菜单管理业务后台API分层架构及调用关系如下图所示:
说明:分层目的主要将复杂问题简单化,实现各司其职,各尽所能。
2 菜单管理列表页面呈现
菜单管理页面的加载过程,其时序分析如下图所示:
2.2 服务端实现
2.2.1 Controller实现
- 业务描述与设计实现
基于菜单管理的请求业务,在PageController中添加doMenuUI方法,用于返回菜单列表页面。
- 关键代码设计与实现
第一步:在PageController中定义返回菜单列表的方法。代码如下:
@RequestMapping("menu/menu_list")
public String doMenuUI() {
return "sys/menu_list";
}
第二步:在PageController中基于rest风格的url方式优化返回UI页面的方法。找出共性进行提取,例如:
@RequestMapping("{module}/{moduleUI}")
public String doModuleUI(@PathVariable String moduleUI) {
return "sys/"+moduleUI;
}
2.3 客户端的实现
2.3.1 首页菜单事件处理
- 业务描述与设计实现
首先准备菜单列表页面(/templates/pages/sys/menu_list.html),然后在starter.html页面中点击菜单管理时异步加载菜单列表页面。
- 关键代码设计与实现
找到项目中的starter.html页面,页面加载完成以后,注册菜单管理项的点击事件,当点击菜单管理时,执行事件处理函数。关键代码如下:
$(function(){
…
doLoadUI("load-menu-id","menu/menu_list")
})
说明:对于doLoadUI函数,假如在starter.html中已经定义,则无需再次定义.
function doLoadUI(id,url){
$("#"+id).click(function(){
$("#mainContentId").load(url);
});
}
其中,load函数为jquery中的ajax异步请求函数。
2.3.2 菜单列表页面
- 业务描述与设计实现
本页面呈现菜单信息时要以树结构形式进行呈现。此树结构会借助jquery中的treeGrid插件进行实现,所以在菜单列表页面需要引入treeGrid相关JS。但是,具体的treeGrid怎么用可自行在网上进行查询(已比较成熟)。
- 关键代码设计与实现:
关键JS引入(menu_list.html),代码如下:
<script type="text/javascript" src="bower_components/treegrid/jquery.treegrid.extension.js"></script>
<script type="text/javascript" src="bowe
r_components/treegrid/jquery.treegrid.min.js"></script>
<script type="text/javascript" src="bower_components/treegrid/tree.table.js"></script>
3 菜单管理列表数据呈现
3.1 数据架构分析
菜单列表页面加载完成,启动菜单数据异步加载操作,本次菜单列表页面要呈现菜单以及上级菜单信息,其数据查询时,数据的封装及传递过程,如下图所示。
说明:本模块将从数据库查询到的菜单数据封装到map对象,一行记录一个map对象,其中key为表中的字段(列)名,值为字段(列)对应的值。
数据加载过程其时序分析,如下图所示:
3.2 服务端关键业务及代码实现
3.2.1 Dao接口实现
- 业务描述及设计实现
通过数据层对象,基于业务层参数,查询菜单以及上级菜单信息(要查询上级菜单名)。
- 关键代码分析及实现
第一步:定义数据层接口对象,通过此对象实现数据库中菜单数据的访问操作。关键代码如下:
@Mapper
public interface SysMenuDao {
}
第二步:在SysMenuDao接口中添加findObjects方法,基于此方法实现菜单数据的查询操作。代码如下:
List<Map<String,Object>> findObjects();
说明:一行记录映射为一个map对象,多行存储到list。
思考:这里为什么使用map存储数据,有什么优势劣势?
3.2.2 Mapper文件的实现
- 业务描述及设计实现
基于Dao接口创建映射文件,在此文件中通过相关元素(例如select)描述要执行的数据操作。
- 关键代码设计及实现
第一步:在映射文件的设计目录中添加SysMenuMapper.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="com.cy.pj.sys.dao.SysMenuDao">
</mapper>
第二步:在映射文件中添加id为findObjects的元素,实现菜单记录查询。我们要查询所有菜单以及菜单对应的上级菜单名称。关键代码如下:
<select id="findObjects" resultType="map">
<!-- 方案1
select c.*,p.name parentName
from sys_menus c left join sys_menus p
on c.parentId=p.id
-->
<!-- 方案2 -->
select c.*,(
select p.name
from sys_menus p
where c.parentId=p.id
) parentName
from sys_menus c
</select>
说明:自关联查询分析,如图-10所示:
3.2.3 Service接口及实现类
- 业务描述与设计实现
在菜单查询中,业务层对象主要是借助数据层对象完成菜单数据的查询。后续还可以基于AOP对数据进行缓存,记录访问日志等。
- 关键代码设计及实现
第一步:定义菜单业务接口及方法,暴露外界对菜单业务数据的访问,其代码参考如下:
package com.cy.pj.sys.service;
public interface SysMenuService {
List<Map<String,Object>> findObjects();
}
第二步:定义菜单业务接口实现类,并添加菜单业务数据对应的查询操作实现,其代码参考如下:
package com.cy.pj.sys.service.impl;
@Service
public class SysMenuServiceImpl implements SysMenuService{
@Autowired
private SysMenuDao sysMenuDao;
@Override
public List<Map<String, Object>> findObjects() {
List<Map<String,Object>> list=
sysMenuDao.findObjects();
if(list==null||list.size()==0)
throw new ServiceException("没有对应的菜单信息");
return list;
}
3.2.4 Controller类实现
- 业务描述与设计实现
控制层对象主要负责请求和响应数据的处理,例如,本模块通过业务层对象执行业务逻辑,再通过VO对象封装响应结果(主要对业务层数据添加状态信息),最后将响应结果转换为JSON格式的字符串响应到客户端。
- 关键代码设计与实现
定义Controller类,并将此类对象使用Spring框架中的@Controller注解进行标识,表示此类对象要交给Spring管理。然后基于@RequestMapping注解为此类定义根路径映射。代码参考如下:
package com.cy.pj.sys.controller;
@RequestMapping("/menu/")
@RestController
public class SysMenuController {
}
说明:这里的@RestController注解等效于在类上同时添加了@Controller和 @ResponseBody注解.
在Controller类中添加菜单查询处理方法,代码参考如下:
@RequestMapping("doFindObjects")
public JsonResult doFindObjects() {
return new JsonResult(sysMenuService.findObjects());
}
3.3 客户端关键业务及代码实现
3.3.1 菜单列表信息呈现
- 业务描述与设计实现
菜单页面加载完成以后,向服务端发起异步请求加载菜单信息,当菜单信息加载完成需要将菜单信息呈现到列表页面上。
- 关键代码设计与实现
第一步:在菜单列表页面引入treeGrid插件相关的JS。
<script type="text/javascript" src="bower_components/treegrid/jquery.treegrid.extension.js"></script>
<script type="text/javascript" src="bower_components/treegrid/jquery.treegrid.min.js"></script>
<script type="text/javascript" src="bower_components/treegrid/tree.table.js"></script>
第二步:在菜单列表页面,定义菜单列表配置信息,关键代码如下:
var columns = [
{
field : 'selectItem',
radio : true
},
{
title : '菜单ID',
field : 'id',
align : 'center',
valign : 'middle',
width : '80px'
},
{
title : '菜单名称',
field : 'name',
align : 'center',
valign : 'middle',
width : '130px'
},
{
title : '上级菜单',
field : 'parentName',
align : 'center',
valign : 'middle',
sortable : true,
width : '100px'
},
{
title : '类型',
field : 'type',
align : 'center',
valign : 'middle',
width : '70px',
formatter : function(item, index) {
if (item.type == 1) {
return '<span class="label label-success">菜单</span>';
}
if (item.type == 2) {
return '<span class="label label-warning">按钮</span>';
}
}
},
{
title : '排序号',
field : 'sort',
align : 'center',
valign : 'middle',
sortable : true,
width : '70px'
},
{
title : '菜单URL',
field : 'url',
align : 'center',
valign : 'middle',
width : '160px'
},
{
title : '授权标识',//要显示的标题名称
field : 'permission',//json串中的key
align : 'center',//水平居中
valign : 'middle',//垂直居中
sortable : false //是否排序
} ];//格式来自官方demos -->treeGrid(jquery扩展的一个网格树插件)
第三步:定义异步请求处理函数,代码参考如下:
function doGetObjects(){//treeGrid
//1.构建table对象(bootstrap框架中treeGrid插件提供)
var treeTable=new TreeTable(
"menuTable",//tableId
"menu/doFindObjects",//url
columns);
//设置从哪一列开始展开(默认是第一列)
//treeTable.setExpandColumn(2);
//2.初始化table对象(底层发送ajax请求获取数据)
treeTable.init();//getJSON,get(),...
}
第四步:页面加载完成,调用菜单查询对应的异步请求处理函数,关键代码如下:
$(function(){
doGetObjects();
})
4 菜单管理删除操作实现
4.1 业务时序分析
基于用户在列表页面上选择的的菜单记录ID,执行删除操作,本次删除业务实现中,首先要基于id判断当前菜单是否有子菜单,假如有子菜单则不允许删除,没有则先删除菜单角色关系数据,然后再删除菜单自身信息。其时序分析如下图所示:
4.2 服务端关键业务及代码实现
4.2.1 Dao接口实现
- 业务描述及设计实现
数据层基于业务层提交的菜单记录id,删除菜单角色关系以及菜单数据,菜单自身记录信息。
- 关键代码设计及实现
第一步:创建SysRoleMenuDao并定义基于菜单id删除关系数据的方法,关键代码如下:
@Mapper
public interface SysRoleMenuDao {
int deleteObjectsByMenuId(Integer menuId);
}
第二步:在SysMenuDao中添加基于菜单id查询子菜单记录的方法。代码参考如下:
int getChildCount(Integer id);
第三步:在SysMenuDao中添加基于菜单id删除菜单记录的方法。代码参考如下:
int deleteObject(Integer id);
4.2.2 mapper文件的实现
- 业务描述及设计实现
在SysRoleMenuDao,SysMenuDao接口对应的映射文件中添加用于执行删除业务的delete元素,然后在元素内部定义具体的SQL实现。
- 关键代码设计与实现
第一步:创建SysRoleMenuMapper.xml文件并添加基于菜单id删除关系数据的元素,关键代码如下:
<?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="com.cy.pj.sys.dao.SysRoleMenuDao">
<delete id="deleteObjectsByMenuId"
parameterType="int">
delete from sys_role_menus
where menu_id=#{menuId}
</delete>
</mapper>
第二步:在SysMenuMapper.xml文件中添加基于id统计子菜单数量的元素,关键代码如下:
<select id="getChildCount"
parameterType="int"
resultType="int">
select count(*)
from sys_menus
where parentId=#{id}
</select>
4.2.3 Service接口及实现类
- 业务描述与设计实现
在菜单业务层定义用于执行菜单删除业务的方法,首先通过方法参数接收控制层传递的菜单id,并对参数id进行校验。然后基于菜单id统计子菜单个数,假如有子菜单则抛出异常,提示不允许删除。假如没有子菜单,则先删除角色菜单关系数据。最后删除菜单自身记录信息后并返回业务执行结果。
- 关键代码设计与实现
第一步:在SysMenuService接口中,添加基于id进行菜单删除的方法。关键代码如下:
int deleteObject(Integer id);
第二步:在SysMenuServiceImpl实现类中注入SysRoleMenuDao相关对象。关键代码如下:
@Autowired
private SysRoleMenuDao sysRoleMenuDao;
第三步:在SysMenuServiceImpl实现类中添加删除业务的具体实现。关键代码如下:
@Override
public int deleteObject(Integer id) {
//1.验证数据的合法性
if(id==null||id<=0)
throw new IllegalArgumentException("请先选择");
//2.基于id进行子元素查询
int count=sysMenuDao.getChildCount(id);
if(count>0)
throw new ServiceException("请先删除子菜单");
//3.删除角色,菜单关系数据
sysRoleMenuDao.deleteObjectsByMenuId(id);
//4.删除菜单元素
int rows=sysMenuDao.deleteObject(id);
if(rows==0)
throw new ServiceException("此菜单可能已经不存在");
//5.返回结果
return rows;
}
4.2.4 Controller类实现
- 业务描述与设计实现
在菜单控制层对象中,添加用于处理菜单删除请求的方法。首先在此方法中通过形参接收客户端提交的数据,然后调用业务层对象执行删除操作,最后封装执行结果,并在运行时将响应对象转换为JSON格式的字符串,响应到客户端。
- 关键代码设计与实现
第一步:在SysMenuController中添加用于执行删除业务的方法。代码如下:
@RequestMapping("doDeleteObject")
public JsonResult doDeleteObject(Integer id){
sysMenuService.deleteObject(id);
return new JsonResult("delete ok");
}
第二步:启动tomcat进行访问测试,打开浏览器输入如下网址:http://localhost/menu/doDeleteObject?id=10
4.3 客户端关键业务及代码实现
4.3.1 菜单列表页面事件处理
- 业务描述及设计实现
用户在页面上首先选择要删除的元素,然后点击删除按钮,将用户选择的记录id异步提交到服务端,最后在服务端执行菜单的删除动作。
- 关键代码设计与实现
第一步:页面加载完成以后,在删除按钮上进行点击事件注册。关键代码如下:
...
$(".input-group-btn")
.on("click",".btn-delete",doDeleteObject)
...
第二步:定义删除操作对应的事件处理函数。关键代码如下:
function doDeleteObject(){
//1.获取选中的记录id
var id=doGetCheckedId();
if(!id){
alert("请先选择");
return;
}
//2.给出提示是否确认删除
if(!confirm("确认删除吗"))return;
//3.异步提交请求删除数据
var url="menu/doDeleteObject";
var params={"id":id};
$.post(url,params,function(result){
if(result.state==1){
alert(result.message);
$("tbody input[type='radio']:checked")
.parents("tr").remove();
}else{
alert(result.message);
}
});
}
第三步:定义获取用户选中的记录id的函数。关键代码如下:
function doGetCheckedId(){
//1.获取选中的记录
var selections=$("#menuTable")
//bootstrapTreeTable是treeGrid插件内部定义的jquery扩展函数
//getSelections为扩展函数内部要调用的一个方法
.bootstrapTreeTable("getSelections");
//2.对记录进行判定
if(selections.length==1)
return selections[0].id;
}
5 菜单添加页面呈现
5.1 业务时序分析
添加页面加载时序分析,如下图所示:
5.2 准备菜单编辑页面
首先准备菜单列表页面(/templates/pages/sys/menu_edit.html),然后在menu_list.html页面中点击菜单添加时异步加载菜单编辑页面。
5.3 菜单编辑页面呈现
- 业务描述与设计实现
菜单列表页面点击添加按钮时,异步加载菜单编辑页面。
- 关键代码设计与实现
第一步:菜单列表页面上,对添加按钮进行事件注册,关键代码如下:
$(document).ready(function(){
...
$(".input-group-btn")
.on("click",".btn-add",doLoadEditUI);
});
第二步:定义添加按钮事件处理函数,关键代码如下:
function doLoadEditUI(){
var title;
if($(this).hasClass("btn-add")){
title="添加菜单"
}
var url="menu/menu_edit";
$("#mainContentId").load(url,function(){
$(".box-title").html(title);
})
}
6 菜单编辑页面上级菜单呈现
6.1 业务时序分析
在菜单编辑页面上,点击上级菜单时,其数据加载时序分析,如下图所示:
6.2 服务端关键业务及代码实现
6.2.1 Node对象
- 业务描述与设计实现
定义值对象封装查询到的上级菜单id,name,parentId信息。
- 关键代码设计与实现
package com.cy.pj.common.vo;
public class Node implements Serializable{
private static final long serialVersionUID = -6577397050669133046L;
private Integer id;
private String name;
private Integer parentId;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getParentId() {
return parentId;
}
public void setParentId(Integer parentId) {
this.parentId = parentId;
}
@Override
public String toString() {
return "Node [id=" + id + ", name=" + name + ", parentId=" + parentId + "]";
}
}
6.2.2 Dao接口实现
- 业务描述与设计实现
基于请求获取数据库对应的菜单表中的所有菜单id,name,parentId,一行记录封装为一个Node对象,多个node对象存储到List集合
- 关键代码设计与实现
在SysMenuDao接口中添加,用于查询上级菜单相关信息。关键代码如下:
List<Node> findZtreeMenuNodes();
6.2.3 Mapper映射文件
- 业务描述与设计实现
基于SysMenuMapper中方法的定义,编写用于菜单查询的SQL元素。
- 关键代码设计与实现
在SysMenuMapper.xml中添加findZtreeMenuNodes元素,用于查询上级菜单信息。关键代码如下:
<select id="findZtreeMenuNodes"
resultType="com.cy.pj.common.vo.Node">
select id,name,parentId
from sys_menus
</select>
6.2.4 Service接口及实现类
- 业务描述与设计实现
- 关键代码实现
第一步:在SysMenuService接口中,添加查询菜单信息的方法。关键代码如下:
List<Node> findZtreeMenuNodes()
第二步:在SysMenuServiceImpl类中添加,查询菜单信息方法的实现。关键代码如下:
@Override
public List<Node> findZtreeMenuNodes() {
return sysMenuDao.findZtreeMenuNodes();
}
6.2.5 Controller类实现
- 业务描述与设计实现
基于客户端请求,访问业务层对象方法,获取菜单节点对象,并封装返回。
- 关键代码设计与实现
@RequestMapping("doFindZtreeMenuNodes")
public JsonResult doFindZtreeMenuNodes(){
return new JsonResult(
sysMenuService.findZtreeMenuNodes());
}
6.3 客户端关键业务及代码实现
6.3.1 ZTree结构定义
- 业务描述与设计实现
本模块以开源JS组件方式实现ZTree结构信息的呈现。
- 关键代码设计与实现
在menu_edit.html页面中定义用于呈现树结构的DIV组件(假如已有则无需定义)
<div class="layui-layer layui-layer-page layui-layer-molv layer-anim" id="menuLayer" type="page" times="2" showtime="0" contype="object"
style="z-index:59891016; width: 300px; height: 450px; top: 100px; left: 500px; display:none">
<div class="layui-layer-title" style="cursor: move;">选择菜单</div>
<div class="layui-layer-content" style="height: 358px;">
<div style="padding: 10px;" class="layui-layer-wrap">
<ul id="menuTree" class="ztree"></ul> <!-- 动态加载树 -->
</div>
</div>
<span class="layui-layer-setwin"> <a class="layui-layer-ico layui-layer-close layui-layer-close1 btn-cancel" ></a></span>
<div class="layui-layer-btn layui-layer-btn-">
<a class="layui-layer-btn0 btn-confirm">确定</a>
<a class="layui-layer-btn1 btn-cancel">取消</a>
</div>
</div>
6.3.2 ZTree数据呈现
- 业务描述与设计实现
引入zTree需要的JS,并,并基于JS中的定义的API初始化zTree中的菜单信息。
- 关键代码设计与实现
第一步:引入js文件
<script type="text/javascript" src="bower_components/ztree/jquery.ztree.all.min.js"></script>
<script type="text/javascript" src="bower_components/layer/layer.js">
</script>
第二步:在menu_edit.html中定义zTree配置信息(初始化zTree时使用)
var zTree;
var setting = {
data : {
simpleData : {
enable : true,
idKey : "id", //节点数据中保存唯一标识的属性名称
pIdKey : "parentId", //节点数据中保存其父节点唯一标识的属性名称
rootPId : null //根节点id
}
}
}
第二步:定义异步加载zTree信息的函数,关键代码如下:
function doLoadZtreeNodes(){
var url="menu/doFindZtreeMenuNodes";
//异步加载数据,并初始化数据
$.getJSON(url,function(result){
if(result.state==1){
//使用init函数需要先引入ztree对应的js文件
zTree=$.fn.zTree.init(
$("#menuTree"),
setting,
result.data);
$("#menuLayer").css("display","block");
}else{
alert(result.message);
}
})
}
第三步:定义zTree中取消按钮事件处理函数,点击取消隐藏zTree。关键代码如下:
function doHideTree(){
$("#menuLayer").css("display","none");
}
第四步:定义zTree中确定按钮对应的事件处理处理函数。关键代码如下:
function doSetSelectNode(){
//1.获取选中的节点对象
var nodes=zTree.getSelectedNodes();
if(nodes.length==1){
var node=nodes[0];
console.log(node);
//2.将对象中内容,填充到表单
$("#parentId").data("parentId",node.id);
$("#parentId").val(node.name);
}
//3.隐藏树对象
doHideTree();
}
第五步:定义页面加载完成以后的事件处理函数:
$(document).ready(function(){
$("#mainContentId")
.on("click",".load-sys-menu",doLoadZtreeNodes)
$("#menuLayer")
.on("click",".btn-confirm",doSetSelectNode)
.on("click",".btn-cancel",doHideTree)
});
7 菜单数据添加实现
7.1 数据基本架构分析
用户在菜单编辑页面输入数据,然后异步提交到服务端,其简易数据传递基本架构,如下图所示:
用户在菜单添加页面中填写好菜单数据,然后点击保存按钮,将用户填写的数据添加
到数据库。其时序分析,如下图所示:
7.2 服务端关键业务及代码实现
7.2.1 Entity类定义
- 业务描述与设计实现
定义持久化对象,封装客户端请求数据,并将数据传递到数据层进行持久化。
- 关键代码设计与实现
菜单持久层对象类型定义,关键代码如下:
public class SysMenu implements Serializable{
private static final long serialVersionUID = -8805983256624854549L;
private Integer id;
/**菜单名称*/
private String name;
/**菜单url: log/doFindPageObjects*/
private String url;
/**菜单类型(两种:按钮,普通菜单)*/
private Integer type=1;
/**排序(序号)*/
private Integer sort;
/**备注*/
private String note;
/**上级菜单id*/
private Integer parentId;
/**菜单对应的权限标识(sys:log:delete)*/
private String permission;
/**创建用户*/
private String createdUser;
/**修改用户*/
private String modifiedUser;
private Date createdTime;
private Date modifiedTime;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public Integer getType() {
return type;
}
public void setType(Integer type) {
this.type = type;
}
public Integer getSort() {
return sort;
}
public void setSort(Integer sort) {
this.sort = sort;
}
public String getNote() {
return note;
}
public void setNote(String note) {
this.note = note;
}
public Integer getParentId() {
return parentId;
}
public void setParentId(Integer parentId) {
this.parentId = parentId;
}
public String getPermission() {
return permission;
}
public void setPermission(String permission) {
this.permission = permission;
}
public String getCreatedUser() {
return createdUser;
}
public void setCreatedUser(String createdUser) {
this.createdUser = createdUser;
}
public String getModifiedUser() {
return modifiedUser;
}
public void setModifiedUser(String modifiedUser) {
this.modifiedUser = modifiedUser;
}
public Date getCreatedTime() {
return createdTime;
}
public void setCreatedTime(Date createdTime) {
this.createdTime = createdTime;
}
public Date getModifiedTime() {
return modifiedTime;
}
public void setModifiedTime(Date modifiedTime) {
this.modifiedTime = modifiedTime;
}
}
7.2.2. DAO接口定义
- 业务描述与设计实现
负责将用户提交的菜单数据,持久化到数据库。
- 关键代码设计与实现
在SysMenuDao接口中定义数据持久化方法:
int insertObject(SysMenu entity);
7.2.3 Mapper映射文件定义
- 业务描述与设计实现
基于SysMenuDao中方法的定义,编写用于实现菜单添加的SQL元素。
- 关键代码设计与实现
在SysMenuMapper.xml中添加insertObject元素,用于写入菜单信息。关键代码如下:
<insert id="insertObject"
parameterType="com.cy.pj.sys.entity.SysMenu">
insert into sys_menus
(name,url,type,sort,note,parentId,permission,
createdTime,modifiedTime,createdUser,modifiedUser)
values
(#{name},#{url},#{type},#{sort},#{note},#{parentId},
#{permission},now(),now(),#{createdUser},#{modifiedUser})
</insert>
7.2.4 Service接口定义及实现
- 业务描述与设计实现
基于控制层请求,调用数据层对象将菜单信息写入到数据库中。
- 关键代码设计与实现
第一步:在SysMenuService接口中,添加用于保存菜单对象的方法。关键代码如下:
int saveObject(SysMenu entity);
第二步:在SysMenuServiceImpl类中,实现菜单保存操作。关键代码如下:
@Override
public int saveObject(SysMenu entity) {
//1.合法验证
if(entity==null)
throw new IllegalArgumentException("保存对象不能为空");
if(StringUtils.isEmpty(entity.getName()))
throw new IllegalArgumentException("菜单名不能为空");
//2.保存数据
int rows=sysMenuDao.insertObject(entity);
//3.返回数据
return rows;
}
7.2.5 Controller类定义
- 业务描述与设计实现
接收客户端提交的菜单数据,并对其进行封装,然后调用业务层对象进行业务处理,最后将业务层处理结果响应到客户端。
- 关键代码设计与实现
定义Controller方法,借助此方法处理保存菜单数据请求和响应逻辑。关键代码如下:
@RequestMapping("doSaveObject")
public JsonResult doSaveObject(SysMenu entity){
sysMenuService.saveObject(entity);
return new JsonRehlt("save ok");
}
7.3 客户端关键业务及代码实现
7.3.1 页面cancel按钮事件处理
- 业务描述与设计实现
点击页面cancel按钮时,加载菜单那列表页面。
- 关键代码设计与实现
第一步:事件注册(页面加载完成以后)
$(".box-footer")
.on("click",".btn-cancel",doCancel)
第二步:事件处理函数定义
function doCancel(){
var url="menu/menu_list";
$("#mainContentId").load(url);
}
7.3.2 页面Save按钮事件处理
- 业务描述与设计实现
点击页面save按钮时,将页面上输入的菜单信息异步提交到服务端。
- 关键代码设计与实现
第一步:事件注册(页面加载完成以后)。
$(".box-footer")
.on("click",".btn-save",doSaveOrUpdate)
第二步:Save按钮事件处理函数定义。关键代码如下:
function doSaveOrUpdate(){
//1.获取表单数据
var params=doGetEditFormData();
//2.定义url
var url="menu/doSaveObject";
//3.异步提交数据
$.post(url,params,function(result){
if(result.state==1){
alert(result.message);
doCancel();
}else{
alert(result.message);
}
});
}
第三步:表单数据获取及封装函数定义。关键代码如下:
function doGetEditFormData(){
var params={
type:$("form input[name='typeId']:checked").val(),
name:$("#nameId").val(),
url:$("#urlId").val(),
sort:$("#sortId").val(),
permission:$("#permissionId").val(),
parentId:$("#parentId").data("parentId")
}
return params;
}
8 菜单修改页面数据呈现
8.1 业务时序分析
当在菜单列表页面中选中某条记录,然后点击修改按钮时,其业务时序分析如下图所示:
8.2 客户端关键业务及代码实现
8.2.1 列表页面修改按钮事件处理
- 业务描述与设计实现
点击页面修改按钮时,获取选中菜单记录,并异步加载编辑页面。
- 关键代码设计与实现
第一步:列表页面修改按钮事件注册,关键代码如下:
$(".input-group-btn")
.on("click",".btn-update",doLoadEditUI);
第二步:修改按钮事件处理函数定义或修改,关键代码如下:
function doLoadEditUI(){
var title;
if($(this).hasClass("btn-add")){
title="添加菜单"
}else if($(this).hasClass("btn-update")){
title="修改菜单"
//获取选中的记录数据
var rowData=doGetCheckedItem();
if(!rowData){
alert("请选择一个");
return;
}
$("#mainContentId").data("rowData",rowData);
}
var url="menu/menu_edit";
$("#mainContentId").load(url,function(){
$(".box-title").html(title);
})
}
第三步:获取用户选中记录的函数定义。关键代码如下:
function doGetCheckedItem(){
var tr=$("tbody input[type='radio']:checked")
.parents("tr");
return tr.data("rowData");
}
8.2.2 编辑页面菜单数据呈现
- 业务描述与设计实现
页面加载完成,在页面指定位置呈现要修改的数据。
- 关键代码设计与实现
第一步:页面加载完成以后,获取页面div中绑定的数据。关键代码如下:
$(function(){
…
//假如是修改
var data=$("#mainContentId").data("rowData");
if(data)doInitEditFormData(data);
});
第二步:定义编辑页面数据初始化方法。关键代码如下:
function doInitEditFormData(data){
/* $("input[type='radio']").each(function(){
if($(this).val()==data.type){
$(this).prop("checked",true);
}
}) */
$(".typeRadio input[value='"+data.type+"']").prop("checked",true);
$("#nameId").val(data.name);
$("#sortId").val(data.sort);
$("#urlId").val(data.url);
$("#permissionId").val(data.permission);
$("#parentId").val(data.parentName);
$("#parentId").data("parentId",data.parentId);
}
9 菜单数据更新实现
9.1 业务时序分析
当点击编辑页面更新按钮时,其时序分析如下图所示:
9.2 服务端关键业务及代码实现
9.2.1 DAO接口实现
- 业务描述与设计实现
负责将用户编辑页面提交到服务端的菜单数据,更新到数据库进行持久性存储。
- 关键代码设计与实现
在SysMenuDao接口中添加数据更新方法,关键代码如下:
int updateObject(SysMenu entity);
9.2.2 Mapper映射文件定义
- 业务描述与设计实现
基于SysMenuDao中updateObject方法的定义,编写用于实现菜单更新的SQL元素。
- 关键代码设计与实现
在SysMenuMapper.xml中添加updateObject元素,用于更新菜单信息。关键代码如下:
<update id="updateObject"
parameterType="com.cy.pj.sys.entity.SysMenu">
update sys_menus
set
name=#{name},
type=#{type},
sort=#{sort},
url=#{url},
parentId=#{parentId},
permission=#{permission},
modifiedUser=#{modifiedUser},
modifiedTime=now()
where id=#{id}
</update>
9.2.3 Service接口及实现
- 业务描述与设计实现
基于控制层请求,对数据进行校验并调用数据层对象将菜单信息更新到数据库中。
- 关键代码设计与实现
第一步:在SysMenuService接口中,添加用于更新菜单对象的方法。关键代码如下:
int updateObject(SysMenu entity);
第二步:在SysMenuServiceImpl类中,实现菜单保存操作。关键代码如下:
@Override
public int updateObject(SysMenu entity) {
//1.合法验证
if(entity==null)
throw new ServiceException("保存对象不能为空");
if(StringUtils.isEmpty(entity.getName()))
throw new ServiceException("菜单名不能为空");
//2.更新数据
int rows=sysMenuDao.updateObject(entity);
if(rows==0)
throw new ServiceException("记录可能已经不存在");
//3.返回数据
return rows;
}
9.2.4 Controller类定义
- 业务描述与设计实现
接收客户端提交的菜单数据,并对其进行封装,然后调用业务层对象进行业务处理,最后将业务层处理结果响应到客户端。
- 关键代码设计与实现
定义Controller方法,借助此方法处理保存菜单数据请求和响应逻辑。关键代码如下:
@RequestMapping("doUpdateObject")
public JsonResult doUpdateObject(SysMenu entity){
sysMenuService.updateObject(entity);
return new JsonResult("update ok");
}
9.3 客户端关键业务及代码实现
9.3.1 编辑页面更新按钮事件处理
- 业务描述与设计实现
点击页面save按钮时,将页面上输入的菜单编辑信息提交到服务端。
- 关键代码设计与实现
编辑Save按钮对应的事件处理函数。关键代码如下:
function doSaveOrUpdate(){
//1.获取表单数据
var params=doGetEditFormData();
var rowData=$("#mainContentId").data("rowData");
//2.定义url
var insertUrl="menu/doSaveObject";
var updateUrl="menu/doUpdateObject";
var url=rowData?updateUrl:insertUrl;
if(rowData)params.id=rowData.id;
//3.异步提交数据
$.post(url,params,function(result){
if(result.state==1){
alert(result.message);
doCancel();
}else{
alert(result.message);
}
});
}
10 总结
10.1 重难点分析
- 菜单管理在整个系统中的定位(资源管理)。
- 菜单数据的自关联查询实现(查询当前菜单以及这个菜单的上级菜单)。
- 菜单管理中数据的封装过程(请求数据,响应数据)。
- 菜单数据在客户端的呈现。(treeGrid,zTree)
10.2 FAQ分析
- 菜单表是如何设计的,都有哪些字段?
- 菜单列表数据在客户端是如何展示的?(TreeGrid)
- 菜单删除业务是如何处理的?
- 菜单编辑页面中上级菜单数据的呈现方式?(zTree)
- 常用表连接方式,如下图所示:
10.3 BUG分析
- 无效参数异常(IllegalArgumentException),,如下图所示:
问题分析:检查当前执行的业务,其结果映射配置,是否将resultType写成了resultMap。
- 菜单编辑页面,上级菜单树结构呈现,如下图所示:
问题分析:检查查询结果中是否有parentId,或映射对象Node中parentId是否写错。
- 属性值注入失败,如下图所示:
问题分析:检查Spring容器中是否有SysMenuService接口的实现类对象,因为在SysMenuController中需要一个这样的对象。