练习尚硅谷的尚筹网
项目描述
相当于一个众筹网。
- Java17
- tomcat10.1.x
- mysql5(我有,不想安装高版本),支持事务的版本
- 抛弃 jsp 转用 Thymeleaf
- 尽可能不用旧东西
项目结构
搭建环境
创建工程
创建 Maven 项目,我是一个大的包含三个模块,其中一个模块包含三个子模块。
1 | D:. |
webui 依赖 component ,component 依赖 entity 和 util,所以 install 要从最底层开始。
parent 管理依赖。
1 | <properties> |
整合 MyBatis
逆向工程插件
创建数据库和表。
1 | SET FOREIGN_KEY_CHECKS=0; |
在 reverse 模块里配置逆向工程。插件版本要和依赖的 core 保持一致。
1 | <dependencies> |
在 reverse 模块里创建 generatorConfig.xml
,必须是这个名字。由于我项目结构的原因,路径要写绝对路径。约束要求下面的都要有。
1 |
|
在插件里点击 mybatis-generator:generate
生成代码,会更具配置放在指定位置。找到实体类,添加有参和无参构造器和 toString,我用的插件,暂时省力。
因为是分模块的,所以要移动一些文件:实体移到 entity,mapper 接口移到 component ,mapper.xml 移到 webui。
Spring
大致思路:
①子模块加入需要的依赖
②准备 jdbc.properties
③创建 Spring 配置文件,专门配置 Spring 和 MyBatis 整合相关
④在 Spring 的配置文件中加载 jdbc.properties 属性文件
⑤配置数据源
⑥配置 sqlSessionFactoryBean
①装配数据源
②指定 XxxMapper.xml 配置文件的位置
③指定 MyBatis 全励配置文件的位置(可选,所以创建了空的)
⑦配置 MapperScannerConfigurer
⑧测试是否可以装配 xxxMapper 接口并通过这个接口操作数据库
步骤:
-
component 使用需要的依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
</dependency>
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- 这个应该有替代-->
<dependency>
<groupId>jstl</groupId>
<artifactId>jstl</artifactId>
</dependency>
<!-- 自己使用json-->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency> -
jdbc.properties 放到 webui
1
2
3
4jdbc.driverClassName=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/crowd?useUnicode=true&characterrEncoding=UTF-8
jdbc.username=root
jdbc.password=123456 -
mybatis-config.xml 放到 webui,空的
-
spring-persist-mybatis.xml 放到 webui
1
2
3
4
5
6
7
8<contxet:property-placeholder location="classpath:jdbc.properties"/>
<!-- 数据源,test测试连接-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
<property name="driverClassName" value="${jdbc.driverClassName}"/>
<property name="url" value="${jdbc.url}"/>
</bean> -
测试数据源,记得依赖的模块要 clean 和 install
1
2
3
4
5
6
7
8
9
10
11
public class TestDataSource {
private DataSource dataSource;
public void testConnection() throws SQLException {
Connection connection = dataSource.getConnection();
System.out.println("connection = " + connection);
}
} -
sqlSessionFactoryBean
1
2
3
4
5
6
7
8
9
10<!-- sqlSessionFactoryBean-->
<bean id="sqlSessionFactoryBean" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="configLocation" value="classpath:mybatis-config.xml"/>
<property name="mapperLocations" value="classpath:mappers/*Mapper.xml"/>
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 配置MapperScannerConfigurer来扫描Mapper接口所在的包-->
<bean id="mapperScannerConfigurer" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="cf.pengxiandyou.crowd.mapper"/>
</bean> -
测试 Mapper
1
2
3
4
5
public void testInsert(){
int insert = adminMapper.insert(new Admin(null, "小爱", "123123", "小爱同学", "[email protected]", null));
System.out.println("insert = " + insert);
}当测试不通过,不能部署时,可以配置跳过。
1
2
3
4
5
6
7<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<testFailureIgnore>true</testFailureIgnore>
</configuration>
</plugin>当发现测试执行两边,可以在 setting-maven-runner-skip tests 打上勾。
日志系统
1 | <dependency> |
1 |
|
老版本 Spring 自带的日志是 commons-logging,需要排除,添加。而我使用的是较新的,我翻开到里面是 spring-jcl,所以不用排除和添加。
1 | <exclusions> |
1 | <dependency> |
要配置日志,可以创建 logback.xml
,网上可以找。默认的我很满意,故我不配置。
声明式事务
推荐使用 fitten code 插件提示生成,方便省力。
spring-persist-tx.xml,放到 webui。
1 | <!-- 主要扫描Service--> |
创建相关代码测试。
1 |
|
注意,测试是在 webui,而相关代码在 component ,所以要记得清理和部署。
表述层
配置 web.xml,web.xml 在 webui 模块里。
①配置 ContextLoaderListener
1 | <context-param> |
②配置字符集
1 | <filter> |
③dispatcherServlet
1 | <servlet> |
创建配置 spring-web-mvc.xml。
1 | <context:component-scan base-package="cf.pengxiandyou.crowd.mvc"/> |
准备测试。
①controller
1 |
|
②页面
-
如果 jsp
1
2
3
4
5
6
7
8
9<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
</dependency>跳转可用
${pageContext.request.contextPath}/test/ssm.html
。target 页面可以
${requestScope.all}
获得数据。 -
如果 Thymeleaf
1
2
3
4
5
6<!-- 添加Thymeleaf依赖-->
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring6</artifactId>
<version>3.1.2.RELEASE</version>
</dependency>更换视图解析器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16<bean id="viewResolver" class="org.thymeleaf.spring6.view.ThymeleafViewResolver">
<property name="order" value="1"/>
<property name="characterEncoding" value="UTF-8"/>
<property name="templateEngine">
<bean class="org.thymeleaf.spring6.SpringTemplateEngine">
<property name="templateResolver">
<bean class="org.thymeleaf.spring6.templateresolver.SpringResourceTemplateResolver">
<property name="prefix" value="/WEB-INF/templates/"/>
<property name="suffix" value=".html"/>
<property name="characterEncoding" value="UTF-8"/>
<property name="templateMode" value="HTML"/>
</bean>
</property>
</bean>
</property>
</bean>target 页面可以
<p th:text="${all}"></p>
获得数据。
③访问即可测试
④[base 标签](标签 - 简书 (jianshu.com)),可以设置全局地址
⑤AJAX,get 和 post 必须 200 才能执行回调函数,所以用 ajax
-
$.ajax({ url: "test/array.html", type: "POST", data: { array: [1, 2, 3] }, dataType: "text", contentType: "application/json;charset=UTF-8",//json格式数据 success: function (data) { alert(data); }, error: function (data) { alert(data.status) } }); <!--code26--> system-error.html 里获取异常。 <!--code27--> 我是怎么知道用 `${exception}`,我是看日志 `[view="system-error"; model={exception=java.lang.ArithmeticException: / by zero}]` 得出的。 目前请求页面的出现异常的,可以正常跳转错误页面。返回 json 的 controller 出现异常,前端能拿到页面的代码,不跳。
-
基于注解的异常映射
-
判断请求类型的工具类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public class CrowdUtil {
/**
* 功能描述: <br>
* 〈返回true表示是ajax请求,false表示不是ajax请求〉
*/
public static boolean judgeResquestType(HttpServletRequest request){
String accept = request.getHeader("Accept");
String header = request.getHeader("X-Requested-With");
return (accept!= null && accept.contains("application/json"))
||
(header!= null && header.contains("XMLHttpRequest"));
}
} -
测试判断请求类型的工具类发现报错
1
2
3Caused by: java.lang.NoClassDefFoundError: javax/servlet/http/HttpServletRequest
Caused by: java.lang.ClassNotFoundException: javax.servlet.http.HttpServletRequest我这里的原因是 tomcat 版本是 10.1.x,和前面导入的 2.5servlet-api 依赖不匹配。所以重新导入,并彻底抛弃 jsp。
1
2
3
4
5
6<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>6.0.0</version>
<scope>provided</scope>
</dependency> -
创建自定义异常映射类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22package cf.pengxiandyou.crowd.mvc.config;
//表示当前这个类是一个基于注解的异常处理类
public class CrowdExceptionResolver {
//将一个具体的异常类型和一个方法绑定
public ModelAndView resolveNullPointerException(NullPointerException e,
HttpServletRequest request,
HttpServletResponse response) throws IOException {
if(CrowdUtil.judgeResquestType(request)){
ResultEntity<Object> failed = ResultEntity.failed(e.getMessage());
Gson gson = new Gson();
String json = gson.toJson(failed);
response.getWriter().write(json);
//已经用原生返回了,这里返回null
return null;
}else {
ModelAndView modelAndView = new ModelAndView().addObject("exception", e);
modelAndView.setViewName("system-error");
return modelAndView;
}
}
} -
手动创建空指针异常进行测试
-
注解优先级大于 XML
-
抽取公共方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29//表示当前这个类是一个基于注解的异常处理类
public class CrowdExceptionResolver {
private ModelAndView commonResolve(String viewName,
Exception e,
HttpServletRequest request,
HttpServletResponse response) throws IOException {
if(CrowdUtil.judgeResquestType(request)){
ResultEntity<Object> failed = ResultEntity.failed(e.getMessage());
Gson gson = new Gson();
String json = gson.toJson(failed);
response.getWriter().write(json);
//已经用原生返回了,这里返回null
return null;
}else {
ModelAndView modelAndView = new ModelAndView().addObject("exception", e);
modelAndView.setViewName("system-error");
return modelAndView;
}
}
//将一个具体的异常类型和一个方法绑定
public ModelAndView resolveNullPointerException(NullPointerException e,
HttpServletRequest request,
HttpServletResponse response) throws IOException {
String viewName = "system-error";
return commonResolve(viewName, e, request, response);
}
}
-
常量类
1 | public static final String ATTR_NAME_EXCEPTION = "exception"; |
管理员登录页面
-
正确放在静态资源
-
创建管理员登录页面(后端首页)
-
拿来主义加修改
-
测试访问
1
2<!-- 配置view-controller,将请求转发到指定的视图,不必写controller。-->
<mvc:view-controller path="/admin/to/login/page.html" view-name="admin-login"/>
-
layer
layer 是一款多年来备受青睐的 Web 弹出层组件,具备多种交互模式。任何水平段的开发者都能使用,您的页面会因此拥有丰富友好的操作体验。
在与同类组件的选择中,layer 常一度被推荐为首选。这不仅是因为界面风格,而是它尽可能地在以更少的代码展现更强健的功能,且格外注重功能的扩展、易用和实用性,layer 甚至还兼容了包括 IE6 在内的所有浏览器。其数量可观的基础属性和方法,使得您可以自定义太多您需要的风格。
layer 采用 MIT 开源协议,是一个永久免费的公益性项目。多年来,已被广泛应用于不计其数的 Web 平台。layer 也是 Layui 的代表性组件。
1 | layer.msg('弹窗'); |
修改错误页面
-
代码见之后的 gitee 仓库
-
返回上一页,用的取巧的方式
window.history.go(-1)
window.history.back()
- 利用
Referer: http://localhost:8080/
管理员登录
- 思路
- MD5 加密工具方法
1 | public static String md5(String source) { |
- 创建登录失败的异常类,目前这样写可以
1 | public class LoginFailedException extends RuntimeException{ |
- 登录页面前面已经准备了
- 去往登录页面的方式前面已经准备了
- 登录的 controller
1 |
|
- 实现对应的 service 方法
1 |
|
- 异常映射配置类里配置一个登录相关的异常处理 (之前配置过 XML 的,可以兜底)
1 | //将一个具体的异常类型和一个方法绑定 |
-
数据库配置管理员数据
-
访问测试,用的临时的页面当成功登录页面
-
不使用转发,使用重定向,避免重复提交表单
1
return "redirect:/admin/to/main.html";
1
<mvc:view-controller path="/admin/to/main.html" view-name="admin-main"/>
-
退出
1
2
3
4
5
6
public String doLogout(HttpSession session){
//强制sessioin失效
session.invalidate();
return "redirect:/admin/to/login.html";
} -
模板抽取,将后续常用的部分 html 代码抽出。我抽了 head、导航栏、sidebar。具体知识见 thymleaf 之 fragment 使用、thymeleaf 语法 ——th:text 默认值、字符串连接、th:attr、th:href 传参、th:include 传参、th:inline 内联、th:each 循环、th:with、th:if-CSDN 博客
登录检查
-
创建登录检查的异常类
1
2
3
4
5
6public class AccessForbiddenException extends RuntimeException{
private static final long serialVersionUID = 1L;
public AccessForbiddenException(String message) {
super(message);
}
} -
创建拦截器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18//HandlerInterceptor
//目前只需要使用preHandler,故低版本要使用HandlerInterceptorAdapter,但是我使用的是高版本,接口里有默认方法
public class LoginInterceptor implements HandlerInterceptor {
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1.获取session对象
HttpSession session = request.getSession();
// 2.尝试获取Admin对象
Admin admin = (Admin) session.getAttribute(CrowdConstant.ATTR_NAME_LOGIN_ADMIN);
// 3.如果Admin对象为空,则抛出异常
if (admin == null) {
throw new AccessForbiddenException(CrowdConstant.MESSAGE_ACCESS_FORBIDDEN);
}
// 4.如果Admin对象不为空,则放行
return true;
}
}异常的处理用 XML 里配置的
-
注册拦截器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27<!-- 注册拦截器-->
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/**"/><!--拦截所有请求,要拦截的比较多,不拦截的用排除-->
<mvc:exclude-mapping path="/admin/to/login.html"/>
<mvc:exclude-mapping path="/admin/do/login.html"/>
<mvc:exclude-mapping path="/admin/do/logout.html"/>
<!-- 放行测试主页-->
<mvc:exclude-mapping path="/"/>
<mvc:exclude-mapping path="/test/**"/>
<mvc:exclude-mapping path="/to/**"/>
<!-- 放行静态资源-->
<mvc:exclude-mapping path="/css/**"/>
<mvc:exclude-mapping path="/js/**"/>
<mvc:exclude-mapping path="/img/**"/>
<mvc:exclude-mapping path="/fonts/**"/>
<mvc:exclude-mapping path="/bootstrap/**"/>
<mvc:exclude-mapping path="/jquery/**"/>
<mvc:exclude-mapping path="/layui/**"/>
<mvc:exclude-mapping path="/script/**"/>
<mvc:exclude-mapping path="/ztree/**"/>
<mvc:exclude-mapping path="/favicon.ico"/>
<bean class="cf.pengxiandyou.crowd.mvc.interceptor.LoginInterceptor"/>
</mvc:interceptor>
</mvc:interceptors> -
测试所有路径直到满意
管理员维护
几个维护有相同的代码结构,当前管理员维护先实现一下功能
-
分页
- 关键词
- 不带关键词
-
新增
-
更新
-
单条删除
分页
-
引入依赖,进行配置。高版本可以只指定插件。在
SqlSessionFactoryBean
的配置里配置。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16<!--配置插件-->
<property name="plugins">
<array>
<bean class="com.github.pagehelper.PageInterceptor">
<property name="properties">
<props>
<!-- 不同数据库分页语句不同,这里配置mysql的分页插件-->
<!-- 4.0、5.0以上版本说是不用配置方言-->
<prop key="helperDialect">mysql</prop>
<!-- 页面的合理化修正,处理不合理页面-->
<prop key="reasonable">true</prop>
</props>
</property>
</bean>
</array>
</property>虽然源代码里有
String dialectClass = properties.getProperty("dialect");
,但是报错,要使用helperDialect
进行指定,它在这个类PageAutoDialect
提到。 -
Mapper.xml 里添加查询语句。
1
2
3
4
5
6
7
8
9<select id="selectAdminByKeyword" resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
from t_admin
where
login_acct like CONCAT('%', #{keyword}, '%')
or user_name like CONCAT('%', #{keyword}, '%')
or email like CONCAT('%', #{keyword}, '%')
</select>这个查询比较慢,之后可以使用 ElasticSearch(倒排索引)。
limit 已经交给 pagehelper 了
-
Mapper 接口声明方法。
1
List<Admin> selectAdminByKeyword(; String keyword)
-
Service 实现分页方法
1
2
3
4
5
6
7
8
9
10public PageInfo<Admin> getAdminPageInfo(String keyword, Integer pageNum, Integer pageSize) {
logger.info("keyword:{},pageNum:{},pageSize:{}", keyword, pageNum, pageSize);
// 1.开启分页 体现了非侵入式设计,原本的查询不必有任何修改
PageHelper.startPage(pageNum, pageSize);
// 2.查询
List<Admin> admins = adminMapper.selectAdminByKeyword(keyword);
logger.info("admins size:{}", admins.size());
// 3.封装到PageInfo
return new PageInfo<>(admins);
} -
controller 实现请求分页
1
2
3
4
5
6
7
8
9
10
11
public String getPageInfo(
String keyword,
Integer pageNum,
Integer pageSize,
ModelMap modelMap
){
PageInfo<Admin> adminPageInfo = adminService.getAdminPageInfo(keyword, pageNum, pageSize);
modelMap.addAttribute(CrowdConstant.ATTR_NAME_PAGE_INFO, adminPageInfo);
return "admin-page";
} -
准备页面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15<tr th:each="admin,status:${pageInfo.list}">
<td>[[${status.index+1}]]</td>
<td><input type="checkbox"></td>
<td>[[${admin.loginAcct}]]</td>
<td>[[${admin.userName}]]</td>
<td>[[${admin.email}]]</td>
<td>
<button type="button" class="btn btn-success btn-xs"><i
class=" glyphicon glyphicon-check"></i></button>
<button type="button" class="btn btn-primary btn-xs"><i
class=" glyphicon glyphicon-pencil"></i></button>
<button type="button" class="btn btn-danger btn-xs"><i
class=" glyphicon glyphicon-remove"></i></button>
</td>
</tr>此时页面已经可以拿到数据了,接下来就是分页了。
-
由于我运用拿来主义,不是自己写的。分页导航栏不能正确显示(高亮和禁用)。虽然网友给的前端代码里有 pagination 的 jQuery 代码了,但是没有对应的 css 和错误生成,所以要修改 pagination 的源代码、注释掉最后的回调调用和用拿来主义找 css。之后就是分页的代码了。
-
if(page_id == current_page){ if (appendopts.text == opts.prev_text || appendopts.text == opts.next_text){ var lnk = jQuery("<li class= 'disabled'><span class='current'>"+(appendopts.text)+"</span></li> "); }else { var lnk = jQuery("<li class= 'active'><span class='current'>"+(appendopts.text)+"</span></li> "); } }else{ var lnk = jQuery("<li><a>"+(appendopts.text)+"</a></li>") .bind("click", getClickHandler(page_id)) .attr('href', opts.link_to.replace(/__id__/,page_id)); } //将上述代码放到要分页的html里,就可以分页。 <!--code53-->
-
单条删除
我有点想法:①真的单条删除,多条删除时循环调用单条调用;②多条删除,单条删除时内容一个;是不是单条删除和单条删除都单独实现比较好?
目前就是实现物理删除,逻辑删除暂不考虑。
1 |
|
1 |
|
1 | $("button[name='deleteAdmin']").click(function () { |
删除前要考虑还存在吗?多个管理员删除要考虑吗?(前面配置了声明式事务)
新增
-
修改唯一键约束
1
2ALTER TABLE `t_admin`
ADD UNIQUE INDEX (`login_acct`) -
视频没做校验,我偏要做
-
引入依赖
1
2
3
4
5<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>8.0.0.Final</version>
</dependency>没有像以前那样添加多个依赖
-
配置 validator
1
2
3
4<mvc:annotation-driven validator="validator"/>
<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean">
<property name="providerClass" value="org.hibernate.validator.HibernateValidator"/>
</bean> -
实体类属性添加对应要校验的注解,见 gitee 仓库
-
我使用全局的异常映射,因此要配置一个对应的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public ModelAndView resolveMethodArgumentNotValidException(Exception e
) {
Logger logger = LoggerFactory.getLogger(getClass());
logger.error("数据校验问题:{},异常类型:{}", e.getMessage(), e.getClass());
String viewName = "admin-add";
ModelAndView modelAndView = new ModelAndView(viewName);
BindingResult bindingResult;
if (e instanceof MethodArgumentNotValidException){//不使用新特性
bindingResult = ((MethodArgumentNotValidException)e).getBindingResult();
}else {
bindingResult = ((BindException)e).getBindingResult();
}
bindingResult.getFieldErrors().forEach(
item -> {
logger.error("字段:{},错误信息:{}", item.getField(), item.getDefaultMessage());
modelAndView.addObject(item.getField(), item.getDefaultMessage());
}
);
return modelAndView;
}一开始我只绑定了
MethodArgumentNotValidException.class
,一直报错,查了大量资料,绑定了BindException.class
就 ok 了。
-
-
实现 controller 方法
1
2
3
4
5
public String addAdmin({ Admin admin)
adminService.addAdmin(admin);
return "redirect:/admin/get/page.html?pageNum="+Integer.MAX_VALUE;
} -
实现 service 方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void addAdmin(Admin admin) {
try {
// 加密密码
admin.setUserPswd(CrowdUtil.md5(admin.getUserPswd()));
// 创建时间
admin.setCreateTime(
new SimpleDateFormat(CrowdConstant.DATE_FORMAT_YYYY_MM_DD_HH_MM_SS)
.format(new Date())
);
adminMapper.insertSelective(admin);
}catch (DuplicateKeyException e){
throw new LoginAcctAlreadyUsedException(CrowdConstant.MESSAGE_LOGIN_ACCT_ALREADY_IN_USE);
}
catch (Exception e) {
throw new RuntimeException(e);
}
}因为唯一键约束,异常时会捕获到,但是不是所有 DuplicateKeyException 异常都是因为这个,故要自定义一个异常。
1
2
3
4
5
6
7public class LoginAcctAlreadyUsedException extends RuntimeException{
public static final long serialVersionUID = 1L;
public LoginAcctAlreadyUsedException(String message) {
super(message);
}
}当触发这个异常时,前端可用
${exception}
获取。 -
前端页面显示错误
1
2
3
4
5
6
7
8
9
10<p help-block label label-warning th:text="${exception}"></p>
<form role="form" method="post" action="/admin/do/add.html" >
<div class="form-group">
<label for="loginAcctInput">登陆账号</label>
<input th:value="${param.loginAcct}" type="text" class="form-control" name="loginAcct" id="loginAcctInput" placeholder="请输入登陆账号">
<p th:text="${loginAcct}" class="help-block label label-warning"></p>
</div>
<button type="submit" class="btn btn-success"><i class="glyphicon glyphicon-plus"></i> 新增</button>
<button type="reset" class="btn btn-danger"><i class="glyphicon glyphicon-refresh"></i> 重置</button>
</form>太长了,省略了一部分。可以看到,当某个字段出问题,可通过这个字段获取错误信息(前面的全局异常配置);利用同一个请求的原理,将用户已经输入的值放到输入框,方便用户。
更新
视频里能改账号,却不改密码,我改,同时不能改账号。视频里不检查仅靠唯一键以及不处理回显,我要回显。总之视频较简略。
-
设置分组校验,代码见 gitee 仓库
因为新增的密码是没加密的,而修改时的密码是加密的,故密码的校验规则要修改
-
初步编写 controller 测试校验和回显,前端页面见 gitee 仓库
我利用存在 model 的数据进行初步显示。当错误提交更新时,要回显已经修改的内容,就得利用同一次请求。为了达成目标,前端搞了好久,后端又不想重定向以及将数据放到 session 里。
注意:forward 会保留请求的方式
因为校验规则放开了密码的最大长度,所以要单独检验。
-
实现 controller
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public String updateAdmin( { Admin afterUpdateAdmin,ModelMap modelMap)
Admin beforeUpdateAdmin = adminService.getAdminById(afterUpdateAdmin.getId());
// 不允许修改账号
afterUpdateAdmin.setLoginAcct(beforeUpdateAdmin.getLoginAcct());
// 判断密码修改 因为UpdateGroup.class没有校验超长密码
if (!beforeUpdateAdmin.getUserPswd().equals(afterUpdateAdmin.getUserPswd())) {
if (afterUpdateAdmin.getUserPswd().length() > 16) {
modelMap.addAttribute("userPswdError","密码长度不能超过16位,或者不要更改");
return "forward:/admin/to/update.html?id="+afterUpdateAdmin.getId();
}else {// 密码长度正常且修改了
afterUpdateAdmin.setUserPswd(CrowdUtil.md5(afterUpdateAdmin.getUserPswd()));
}
}else {// 密码没修改
afterUpdateAdmin.setUserPswd(beforeUpdateAdmin.getUserPswd());
}
adminService.updateAdmin(afterUpdateAdmin);
return "redirect:/admin/get/page.html?keyword=" + beforeUpdateAdmin.getLoginAcct();
}我这样写,我觉得还有些问题。更新用
updateByPrimaryKeySelective
,虽然不会触发DuplicateKeyException
异常,但还是处理了。
RBAC 模型(Role-Based Access Control)
RBAC 指的是基于角色的访问控制(Role-Based Access Control),是一种常见的访问控制模型,用于对系统资源的访问进行限制和管理。
在 RBAC 模型中,权限控制是基于用户的角色和角色的权限进行管理的。RBAC 将用户分配给适当的角色,而角色则被授予访问特定资源的权限。这种模型的优势在于能够有效地管理用户对系统资源的访问,并且易于控制和维护。
RBAC 模型通常由以下几个关键的组成部分:
- 角色(Role):角色是用户的功能或职责的抽象。它们用来代表一组用户,这些用户在系统中扮演相似的角色,并需要相似的权限。比如,管理员、普通用户、审计员等就是角色的例子。
- 权限(Permission):权限是指在系统中执行特定操作(如读取、写入、更新等)的能力。权限通常与资源相关联,比如文件、数据库记录、API 端点等等。
- 用户(User):系统中的实际用户,他们会被分配一个或多个角色,从而获得相应的权限。
- 角色与权限的映射关系:角色和权限之间建立映射关系,一个角色可以包含多个权限。
- 用户与角色的映射关系:用户和角色之间建立映射关系,一个用户可以被分配给多个角色。
RBAC 模型能够帮助组织有效地管理系统权限,降低错误(减少手工配置权限的可能性)、减轻管理工作负担,并且提高系统的安全性。
在软件开发中,通常可以使用 RBAC 模型来实现系统的权限控制,确保用户在系统内只能访问其需要的资源和信息。
虽然有自动保存,但是蓝屏后,用winhex查看全零,想用恢复工具,结果又蓝屏异常,怀疑和显卡驱动有关,最近更新了它。蓝屏日志分析和事件查看器我也看不太出东西。还好及时推送,要html的版本。
角色维护
创建表
1 | DROP TABLE IF EXISTS `t_role`; |
逆向生成代码
将配置文件里的表名和类名修改一下。生成后移动代码,添加关键词查询。实体类添加校验注解、构造器、toString。
实现获取列表
1 |
|
1 |
|
前端页面显示数据和分页
1 | <script type="text/javascript"> |
实现新增
前端通过模态框进行新增,方便用户。
1 |
|
1 |
|
异常处理,绑定的异常里面添加路径判断实现不同的效果
1 |
|
1 | //新增 |
实现更新
同样使用模态框。前端逻辑新增差不多,类比实现。
1 |
|
1 |
|
当role_name重复时,新增会报DuplicateKeyException
,而更新报com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException
,神奇的是,还不让我用try
,所以异常处理的配置类里修改了一下。
1 | // 角色名称已存在 |
实现删除
果然是单条删除和多条删除用同一个接口。
记录中断
那段时间为了在找工作的同时,也在学习。当时骑车去广汉进厂面试了,因为面试上进厂了。在厂里干了七天,最后放弃了,因为要进行国企面试了。当然,马后炮一下,肯定面上了。目前在干党建,加油。