练习尚硅谷的尚筹网

项目描述

相当于一个众筹网。

  • Java17
  • tomcat10.1.x
  • mysql5(我有,不想安装高版本),支持事务的版本
  • 抛弃 jsp 转用 Thymeleaf
  • 尽可能不用旧东西

项目结构

尚筹网结构

搭建环境

创建工程

创建 Maven 项目,我是一个大的包含三个模块,其中一个模块包含三个子模块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
D:.

├───.idea

├───ssm_crowdfunding01-admin-parent 打包pom

│ ├───ssm_crowdfunding02-admin-webui 打包war

│ ├───ssm_crowdfunding03-admin-component 打包jar

│ └───ssm_crowdfunding04-admin-entity 打包jar

├───ssm_crowdfunding05-admin-util 打包jar

└───ssm_crowdfunding06-admin-reverse 打包jar

webui 依赖 component ,component 依赖 entity 和 util,所以 install 要从最底层开始。

parent 管理依赖。

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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring.version>6.0.6</spring.version>
<spring.security.version>6.0.6</spring.security.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.26</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.9.1</version>
</dependency>
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>rc2-1.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.37</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.8</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.11</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>3.0.2</version>
</dependency>
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.2.1</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.5</version>
</dependency>
<!-- 其它日志框架的中间转换包-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>1.5.6</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jul-to-slf4j</artifactId>
<version>1.7.36</version>
</dependency>

<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.12.3</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.12.3</version>
</dependency>
<!-- 这个应该有替代-->
<dependency>
<groupId>jstl</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
<version>2.2</version>
</dependency>
<!-- 自己使用json-->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
</dependency>
<!-- spring-security-->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>${spring.security.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>${spring.security.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-taglibs</artifactId>
<version>${spring.security.version}</version>
</dependency>
</dependencies>
</dependencyManagement>

整合 MyBatis

逆向工程插件

创建数据库和表。

1
2
3
4
5
6
7
8
9
10
11
SET FOREIGN_KEY_CHECKS=0;
DROP TABLE IF EXISTS `t_admin`;
CREATE TABLE `t_admin` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`login_acct` varchar(255) NOT NULL COMMENT '登录账号',
`user_pswd` char(32) NOT NULL COMMENT '登录密码',
`user_name` varchar(255) NOT NULL COMMENT '昵称',
`email` varchar(255) NOT NULL COMMENT '邮件地址',
`create_time` char(19) DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

在 reverse 模块里配置逆向工程。插件版本要和依赖的 core 保持一致。

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
<dependencies>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.11</version>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.3.0</version>
<dependencies>
<dependency>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-core</artifactId>
<version>1.3.0</version>
</dependency>
<dependency>
<groupId>c3p0</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.1.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.37</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>

在 reverse 模块里创建 generatorConfig.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
30
31
32
33
34
35
36
37
38
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>

<context id="crowdfundTables" targetRuntime="MyBatis3">
<commentGenerator>
<!-- 自动生成注释-->
<property name="suppressAllComments" value="true"/>
</commentGenerator>
<jdbcConnection driverClass="com.mysql.jdbc.Driver"
connectionURL="jdbc:mysql://localhost:3306/crowd"
userId="root"
password="123456"/>
<javaTypeResolver>
<!--默认false,把JDBCDECIMAL和NUMERIC类型解折为Integer,为true时把JDBC DECIMAL
和NUMERIC类型解析为java.math.BigDecimal -->
<property name="forceBigDecimals" value="false"/>
</javaTypeResolver>
<!--targetProject:生成Entity类的路径-->
<javaModelGenerator targetProject="D:\IntelliJ IDEA\WorkSpace\ssm_crowdfunding\ssm_crowdfunding06-admin-reverse\src\main\java" targetPackage="cf.pengxiandyou.crowd.entity">
<!--enableSubPacka 是否让schema作为包的后缴-->
<property name="enableSubPackages" value="false"/>
<!--从数据库返回的值被清理前后的空格-->
<property name="trimString" value="true"/>
</javaModelGenerator>
<!--targetProject:XxxMapper.xml映射文件生成的路径-->
<sqlMapGenerator targetProject="D:\IntelliJ IDEA\WorkSpace\ssm_crowdfunding\ssm_crowdfunding06-admin-reverse\src\main\resources" targetPackage="mappers">
<property name="enableSubPackages" value="false"/>
</sqlMapGenerator>
<!--targetPackage:Mapper接口生成的位置-->
<javaClientGenerator type="XMLMAPPER" targetProject="D:\IntelliJ IDEA\WorkSpace\ssm_crowdfunding\ssm_crowdfunding06-admin-reverse\src\main\java" targetPackage="cf.pengxiandyou.crowd.mapper">
<property name="enableSubPackages" value="false"/>
</javaClientGenerator>
<table tableName="t_admin" domainObjectName="Admin"/>
</context>
</generatorConfiguration>

在插件里点击 mybatis-generator:generate 生成代码,会更具配置放在指定位置。找到实体类,添加有参和无参构造器和 toString,我用的插件,暂时省力。

因为是分模块的,所以要移动一些文件:实体移到 entity,mapper 接口移到 component ,mapper.xml 移到 webui。

Spring

spring和mybatis整合

大致思路:

①子模块加入需要的依赖

②准备 jdbc.properties

③创建 Spring 配置文件,专门配置 Spring 和 MyBatis 整合相关

④在 Spring 的配置文件中加载 jdbc.properties 属性文件

⑤配置数据源

⑥配置 sqlSessionFactoryBean

①装配数据源

②指定 XxxMapper.xml 配置文件的位置

③指定 MyBatis 全励配置文件的位置(可选,所以创建了空的)

⑦配置 MapperScannerConfigurer

⑧测试是否可以装配 xxxMapper 接口并通过这个接口操作数据库

步骤:

  1. 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>
  2. jdbc.properties 放到 webui

    1
    2
    3
    4
    jdbc.driverClassName=com.mysql.jdbc.Driver
    jdbc.url=jdbc:mysql://localhost:3306/crowd?useUnicode=true&characterrEncoding=UTF-8
    jdbc.username=root
    jdbc.password=123456
  3. mybatis-config.xml 放到 webui,空的

  4. 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>
  5. 测试数据源,记得依赖的模块要 clean 和 install

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(locations = {"classpath:spring-persist-mybatis.xml"})
    public class TestDataSource {
    @Autowired
    private DataSource dataSource;
    @Test
    public void testConnection() throws SQLException {
    Connection connection = dataSource.getConnection();
    System.out.println("connection = " + connection);
    }
    }
  6. 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>
  7. 测试 Mapper

    1
    2
    3
    4
    5
    @Test
    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
2
3
4
5
6
7
8
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
1
2
3
4
5
6
7
8
@Test
public void testLog(){
Logger logger = LoggerFactory.getLogger(TestMybatis.class);//slf4j
logger.info("info");
logger.debug("debug");
logger.warn("warn");
logger.error("error");
}

老版本 Spring 自带的日志是 commons-logging,需要排除,添加。而我使用的是较新的,我翻开到里面是 spring-jcl,所以不用排除和添加。

1
2
3
4
5
6
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>1.5.6</version>
</dependency>
<dependency><!-- 用于java的log-->
<groupId>org.slf4j</groupId>
<artifactId>jul-to-slf4j</artifactId>
<version>1.7.36</version>
</dependency>

要配置日志,可以创建 logback.xml,网上可以找。默认的我很满意,故我不配置。

声明式事务

声明式事务图示

推荐使用 fitten code 插件提示生成,方便省力。

spring-persist-tx.xml,放到 webui。

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
<!--    主要扫描Service-->
<context:component-scan base-package="cf.pengxiandyou.crowd.service"/>

<!-- 事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- 虽然不在同一个文件,但在同一个容器-->
<property name="dataSource" ref="dataSource"/>
</bean>

<!-- 事务切面-->
<aop:config><!-- 如果后面SpringSecurity的UserDetailsService报错,改成实现类-->
<aop:pointcut id="txPointcut" expression="execution(* cf.pengxiandyou.crowd.service.*Service.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut"/>
</aop:config>
<!-- 事务通知-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="get*" read-only="true"/>
<tx:method name="find*" read-only="true"/>
<tx:method name="query*" read-only="true"/>
<tx:method name="count*" read-only="true"/>
<!-- 增删改-->
<!-- rollback-for 默认运行时异常,建议编译时和运行时异常都回滚-->
<tx:method name="save*" propagation="REQUIRES_NEW" rollback-for="RuntimeException"/>
<tx:method name="insert*" propagation="REQUIRES_NEW" rollback-for="Exception"/>
<tx:method name="update*" propagation="REQUIRES_NEW"/>
<tx:method name="delete*" propagation="REQUIRES_NEW"/>
</tx:attributes>
</tx:advice>

创建相关代码测试。

1
2
3
4
5
6
7
8
9
@Service
public class AdminServiceImpl implements AdminService {
@Autowired
private AdminMapper adminMapper;
@Override
public void saveAdmin(Admin admin) {
adminMapper.insert(admin);
}
}

注意,测试是在 webui,而相关代码在 component ,所以要记得清理和部署。

表述层

tomcat启动

配置 web.xml,web.xml 在 webui 模块里。

①配置 ContextLoaderListener

1
2
3
4
5
6
7
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-persist-*.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

②配置字符集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<filter>
<filter-name>CharacterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceRequestEncoding</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<param-name>forceResponseEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CharacterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

③dispatcherServlet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
	<servlet>
<servlet-name>dispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-web-mvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcherServlet</servlet-name>
<!-- 拦截所有请求-->
<!-- <url-pattern>/</url-pattern>-->
<!-- 配置请求扩展名,优点:
1.静态资源完全不经过springmvc。
2.可以实现伪静态效果,可让seo更好(钱)。
3.不符合RESTFul规范,-->
<url-pattern>*.html</url-pattern>
<!-- 如果ajax请求扩展名为html,但实际返回json数据,二者不匹配,出现406错误-->
<url-pattern>*.json</url-pattern>
</servlet-mapping>

创建配置 spring-web-mvc.xml。

1
2
3
4
5
6
7
<context:component-scan base-package="cf.pengxiandyou.crowd.mvc"/>
<mvc:annotation-driven/>
<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/"/>
<!-- 暂时用jsp,后面具体页面的时候,我再改造为Thymeleaf-->
<property name="suffix" value=".jsp"/>
</bean>

准备测试。

①controller

1
2
3
4
5
6
@GetMapping("/test/ssm.html")
public String test(ModelMap modelMap){
List<Admin> all = adminService.getALL();
modelMap.addAttribute("all", all);
return "target";
}

②页面

  • 如果 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. 判断请求类型的工具类

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      public 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"));
      }
      }
    2. 测试判断请求类型的工具类发现报错

      1
      2
      3
      Caused 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>

      tomcat各版本搭配

    3. 创建自定义异常映射类

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      package cf.pengxiandyou.crowd.mvc.config;
      @ControllerAdvice//表示当前这个类是一个基于注解的异常处理类
      public class CrowdExceptionResolver {

      @ExceptionHandler(NullPointerException.class)//将一个具体的异常类型和一个方法绑定
      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;
      }
      }
      }
    4. 手动创建空指针异常进行测试

    5. 注解优先级大于 XML

    6. 抽取公共方法

      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
      @ControllerAdvice//表示当前这个类是一个基于注解的异常处理类
      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;
      }
      }

      @ExceptionHandler(NullPointerException.class)//将一个具体的异常类型和一个方法绑定
      public ModelAndView resolveNullPointerException(NullPointerException e,
      HttpServletRequest request,
      HttpServletResponse response) throws IOException {
      String viewName = "system-error";
      return commonResolve(viewName, e, request, response);
      }
      }

常量类

1
2
3
4
5
public static final String ATTR_NAME_EXCEPTION = "exception";

public static final String MESSAGE_LOGIN_FAIL = "登录失败,请检查账号和密码";
public static final String MESSAGE_LOGIN_ACCT_ALREADY_IN_USE = "账号已被占用";
public static final String MESSAGE_ACCESS_FORBIDDEN = "无权访问该页面,请登录后再试";

管理员登录页面

  1. 正确放在静态资源

  2. 创建管理员登录页面(后端首页)

    1. 拿来主义加修改

    2. 测试访问

      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 的代表性组件。

layui

1
layer.msg('弹窗');

修改错误页面

  • 代码见之后的 gitee 仓库

  • 返回上一页,用的取巧的方式

    • window.history.go(-1)
    • window.history.back()
    • 利用 Referer: http://localhost:8080/

管理员登录

  1. 思路

管理员登录思路

  1. MD5 加密工具方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static String md5(String source) {
if (source == null || source.isEmpty()) {
throw new RuntimeException(CrowdConstant.MESSAGE_STRING_INVALIDATE);
}
try {
// 使用的算法
String algorithm = "MD5";
// 获得MD5摘要算法对象
MessageDigest md5 = MessageDigest.getInstance(algorithm);
// 加密结果
byte[] output = md5.digest(source.getBytes());
int signum = 1;
BigInteger bigInteger = new BigInteger(signum, output);
int radix = 16;
return bigInteger.toString(radix).toUpperCase();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return null;
}
  1. 创建登录失败的异常类,目前这样写可以
1
2
3
4
5
public class LoginFailedException extends RuntimeException{
public LoginFailedException(String message) {
super(message);
}
}
  1. 登录页面前面已经准备了
  2. 去往登录页面的方式前面已经准备了
  3. 登录的 controller
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Controller
@RequestMapping("/admin")
public class AdminController {

@Autowired
private AdminService adminService;

@PostMapping("/do/login.html")
public String doLogin(@RequestParam("loginAcct") String loginAcct,
@RequestParam("userPswd") String userPswd,
HttpSession session){
Admin admin = adminService.getAdminByLoginAcctAndUserPswd(loginAcct, userPswd);
session.setAttribute(CrowdConstant.ATTR_NAME_LOGIN_ADMIN, admin);
return "admin-main";
}
}
  1. 实现对应的 service 方法
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
@Override
public Admin getAdminByLoginAcctAndUserPswd(String loginAcct, String userPswd) {
// 1.根据登录账号和密码查询管理员
AdminExample adminExample = new AdminExample();
AdminExample.Criteria criteria = adminExample.createCriteria();
criteria.andLoginAcctEqualTo(loginAcct);
List<Admin> adminList = adminMapper.selectByExample(adminExample);

// 2.判断查询结果是否为null
if (adminList == null || adminList.size() == 0) {
logger.info("adminList is null or size is 0");
throw new LoginFailedException(CrowdConstant.MESSAGE_LOGIN_FAILED);
}
if (adminList.size() > 1) {
logger.info("adminList size is more than 1");
// 实际开发会使用唯一键约束吗?,我加
throw new RuntimeException(CrowdConstant.MESSAGE_SYSTEM_ERROR_LOGINACCT_NOT_UNIQUE);
}
Admin admin = adminList.get(0);
if (admin == null) {
logger.info("admin is null");
throw new LoginFailedException(CrowdConstant.MESSAGE_LOGIN_FAILED);
}

// 3.如果不为null,取出密码,与加密的表单密码对比
String userPswdDB = admin.getUserPswd();
String userPswdForm = CrowdUtil.md5(userPswd);
if(!Objects.equals(userPswdDB, userPswdForm)){
logger.info("userPswd is not equal");
throw new LoginFailedException(CrowdConstant.MESSAGE_LOGIN_FAILED);
}

logger.info("login success");
return admin;
}
  1. 异常映射配置类里配置一个登录相关的异常处理 (之前配置过 XML 的,可以兜底)
1
2
3
4
5
6
7
@ExceptionHandler(LoginFailedException.class)//将一个具体的异常类型和一个方法绑定
public ModelAndView resolveLoginFailedException(LoginFailedException e,
HttpServletRequest request,
HttpServletResponse response) throws IOException {
String viewName = "admin-login";
return commonResolve(viewName, e, request, response);
}
  1. 数据库配置管理员数据

  2. 访问测试,用的临时的页面当成功登录页面

  3. 不使用转发,使用重定向,避免重复提交表单

    1
    return "redirect:/admin/to/main.html";
    1
    <mvc:view-controller path="/admin/to/main.html" view-name="admin-main"/>
  4. 退出

    1
    2
    3
    4
    5
    6
    @GetMapping("/do/logout.html")
    public String doLogout(HttpSession session){
    //强制sessioin失效
    session.invalidate();
    return "redirect:/admin/to/login.html";
    }
  5. 模板抽取,将后续常用的部分 html 代码抽出。我抽了 head、导航栏、sidebar。具体知识见 thymleaf 之 fragment 使用thymeleaf 语法 ——th:text 默认值、字符串连接、th:attr、th:href 传参、th:include 传参、th:inline 内联、th:each 循环、th:with、th:if-CSDN 博客

登录检查

登录检查

  1. 创建登录检查的异常类

    1
    2
    3
    4
    5
    6
    public class AccessForbiddenException extends RuntimeException{
    private static final long serialVersionUID = 1L;
    public AccessForbiddenException(String message) {
    super(message);
    }
    }
  2. 创建拦截器

    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 {

    @Override
    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 里配置的

  3. 注册拦截器

    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>
  4. 测试所有路径直到满意

管理员维护

几个维护有相同的代码结构,当前管理员维护先实现一下功能

  • 分页

    • 关键词
    • 不带关键词
  • 新增

  • 更新

  • 单条删除

分页

分页思路

  1. 引入依赖,进行配置。高版本可以只指定插件。在 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 提到。

  2. 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 了

  3. Mapper 接口声明方法。

    1
    List<Admin> selectAdminByKeyword(@Param("keyword") String keyword);
  4. Service 实现分页方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public 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);
    }
  5. controller 实现请求分页

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @GetMapping("/get/page.html")
    public String getPageInfo(
    @RequestParam(value = "keyword",defaultValue = "") String keyword,
    @RequestParam(value = "pageNum",defaultValue = "1") Integer pageNum,
    @RequestParam(value = "pageSize",defaultValue = "5") Integer pageSize,
    ModelMap modelMap
    ){
    PageInfo<Admin> adminPageInfo = adminService.getAdminPageInfo(keyword, pageNum, pageSize);
    modelMap.addAttribute(CrowdConstant.ATTR_NAME_PAGE_INFO, adminPageInfo);
    return "admin-page";
    }
  6. 准备页面

    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>

    此时页面已经可以拿到数据了,接下来就是分页了。

  7. 由于我运用拿来主义,不是自己写的。分页导航栏不能正确显示(高亮和禁用)。虽然网友给的前端代码里有 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
2
3
4
5
6
7
@ResponseBody
@PostMapping("/to/delete.html")
public ResultEntity deleteAdmin(@RequestParam("id") Integer adminId, HttpSession session){
System.out.println("adminId = " + adminId);
ResultEntity resultEntity = adminService.deleteAdminById(adminId, session);
return resultEntity;
}
1
2
3
4
5
6
7
8
9
10
11
@Override
public ResultEntity deleteAdminById(Integer adminId, HttpSession session) {
Admin currentAdmin = (Admin) session.getAttribute(CrowdConstant.ATTR_NAME_LOGIN_ADMIN);
if (currentAdmin.getId().equals(adminId)) {
logger.info("cannot delete self");
return ResultEntity.failed(CrowdConstant.MESSAGE_CANNOT_DELETE_SELF);
}
int i = adminMapper.deleteByPrimaryKey(adminId);
logger.info(i + " rows affected");
return ResultEntity.successWithoutData();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$("button[name='deleteAdmin']").click(function () {
let id = $(this).attr("data");
$.ajax({
url: "/admin/to/delete.html",
type: "post",
data: {
id: id
},
contentTye: "application/json;charset=utf-8",
success: function (data) {
if (data.result == "success") {
layer.msg("删除成功!");
$("button[name='deleteAdmin'][data="+id+"]").parent().parent().remove();
} else {
layer.msg("删除失败!"+data.message);
}
}
});
});

删除前要考虑还存在吗?多个管理员删除要考虑吗?(前面配置了声明式事务)

新增

  1. 修改唯一键约束

    1
    2
    ALTER TABLE `t_admin`
    ADD UNIQUE INDEX (`login_acct`)
  2. 视频没做校验,我偏要做

    1. 引入依赖

      1
      2
      3
      4
      5
      <dependency>
      <groupId>org.hibernate.validator</groupId>
      <artifactId>hibernate-validator</artifactId>
      <version>8.0.0.Final</version>
      </dependency>

      没有像以前那样添加多个依赖

    2. 配置 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>
    3. 实体类属性添加对应要校验的注解,见 gitee 仓库

    4. 我使用全局的异常映射,因此要配置一个对应的方法

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      @ExceptionHandler({MethodArgumentNotValidException.class, BindException.class})
      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 了。

  3. 实现 controller 方法

    1
    2
    3
    4
    5
    @PostMapping("/do/add.html")
    public String addAdmin(@Validated Admin admin){
    adminService.addAdmin(admin);
    return "redirect:/admin/get/page.html?pageNum="+Integer.MAX_VALUE;
    }
  4. 实现 service 方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    @Override
    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
    7
    public class LoginAcctAlreadyUsedException extends RuntimeException{
    public static final long serialVersionUID = 1L;

    public LoginAcctAlreadyUsedException(String message) {
    super(message);
    }
    }

    当触发这个异常时,前端可用 ${exception} 获取。

  5. 前端页面显示错误

    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>

    太长了,省略了一部分。可以看到,当某个字段出问题,可通过这个字段获取错误信息(前面的全局异常配置);利用同一个请求的原理,将用户已经输入的值放到输入框,方便用户。

更新

视频里能改账号,却不改密码,我改,同时不能改账号。视频里不检查仅靠唯一键以及不处理回显,我要回显。总之视频较简略。

  1. 设置分组校验,代码见 gitee 仓库

    因为新增的密码是没加密的,而修改时的密码是加密的,故密码的校验规则要修改

  2. 初步编写 controller 测试校验和回显,前端页面见 gitee 仓库

    我利用存在 model 的数据进行初步显示。当错误提交更新时,要回显已经修改的内容,就得利用同一次请求。为了达成目标,前端搞了好久,后端又不想重定向以及将数据放到 session 里。

    注意:forward 会保留请求的方式

    因为校验规则放开了密码的最大长度,所以要单独检验。

  3. 实现 controller

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    @PostMapping("/do/update.html")
    public String updateAdmin(@Validated(UpdateGroup.class) 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 模型通常由以下几个关键的组成部分:

  1. 角色(Role):角色是用户的功能或职责的抽象。它们用来代表一组用户,这些用户在系统中扮演相似的角色,并需要相似的权限。比如,管理员、普通用户、审计员等就是角色的例子。
  2. 权限(Permission):权限是指在系统中执行特定操作(如读取、写入、更新等)的能力。权限通常与资源相关联,比如文件、数据库记录、API 端点等等。
  3. 用户(User):系统中的实际用户,他们会被分配一个或多个角色,从而获得相应的权限。
  4. 角色与权限的映射关系:角色和权限之间建立映射关系,一个角色可以包含多个权限。
  5. 用户与角色的映射关系:用户和角色之间建立映射关系,一个用户可以被分配给多个角色。

RBAC 模型能够帮助组织有效地管理系统权限,降低错误(减少手工配置权限的可能性)、减轻管理工作负担,并且提高系统的安全性。

在软件开发中,通常可以使用 RBAC 模型来实现系统的权限控制,确保用户在系统内只能访问其需要的资源和信息。

  虽然有自动保存,但是蓝屏后,用winhex查看全零,想用恢复工具,结果又蓝屏异常,怀疑和显卡驱动有关,最近更新了它。蓝屏日志分析和事件查看器我也看不太出东西。还好及时推送,要html的版本。

角色维护

创建表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
DROP TABLE IF EXISTS `t_role`;
CREATE TABLE `t_role` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '角色id',
`role_name` varchar(100) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `role_name` (`role_name`)
) ENGINE=InnoDB AUTO_INCREMENT=110 DEFAULT CHARSET=utf8;
INSERT INTO `t_role` VALUES ('9', 'CMO/CMS-配置管理员');
INSERT INTO `t_role` VALUES ('5', 'GL-组长');
INSERT INTO `t_role` VALUES ('3', 'PG-程序员');
INSERT INTO `t_role` VALUES ('1', 'PM-项目经理');
INSERT INTO `t_role` VALUES ('6', 'QA-品质保证');
INSERT INTO `t_role` VALUES ('7', 'QC-品质控制');
INSERT INTO `t_role` VALUES ('8', 'SA-软件架构师');
INSERT INTO `t_role` VALUES ('2', 'SE-软件工程师');
INSERT INTO `t_role` VALUES ('4', 'TL-组长');

逆向生成代码

  将配置文件里的表名和类名修改一下。生成后移动代码,添加关键词查询。实体类添加校验注解、构造器、toString。

实现获取列表

1
2
3
4
5
6
7
8
9
10
@GetMapping("/get/page.html")
@ResponseBody
public ResultEntity<PageInfo> getRolePageInfo(
@RequestParam(value = "keyword",defaultValue = "") String keyword,
@RequestParam(value = "pageNum",defaultValue = "1") Integer pageNum,
@RequestParam(value = "pageSize",defaultValue = "5") Integer pageSize
){
PageInfo<Role> rolePageInfo = roleService.getRolePageInfo(keyword, pageNum, pageSize);
return ResultEntity.successWithData(rolePageInfo);
}
1
2
3
4
5
6
7
8
9
10
11
12
@Override
public PageInfo<Role> getRolePageInfo(String keyword, Integer pageNum, Integer pageSize) {
logger.info("keyword:{},pageNum:{},pageSize:{}", keyword, pageNum, pageSize);
// 1.开启分页 体现了非侵入式设计,原本的查询不必有任何修改
PageHelper.startPage(pageNum, pageSize);
// 2.查询
List<Role> roles = roleMapper.selectByKeyword(keyword);
logger.info("Roles size:{}", roles.size());
logger.info(roles.toString());
// 3.封装到PageInfo
return new PageInfo<>(roles);
}

前端页面显示数据和分页

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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
<script type="text/javascript">
//初始化
window.keyword = "";
window.pageNum = 1;
window.pageSize = 5;
$(function (){
//获取页面数据
getPageInfo();
//关键词查询
$("button[name='searchBtn']").click(function(){
//更新关键词
window.keyword = $(this).parent().find("input").val();
//回到第一页
window.pageNum = 1;
getPageInfo();
})
//获取页面数据
function getPageInfo(){
$.ajax({
url: "/role/get/page.html",
type: "get",
data: {
keyword: window.keyword,
pageNum: window.pageNum,
pageSize: window.pageSize
},
success: function(data){
if (data.result == "success"){
//填充表格
fillTbody(data);
//初始化分页导航栏
initPagination(data)
}
},
error: function(data){
layer.msg("获取数据失败",data)
}
});
}
//填充表格
function fillTbody(data) {
let tbody = $("tbody");
tbody.empty();
if (data.data.list.length == 0){
tbody.append("<tr><td colspan='4' align='center'>暂无数据</td></tr>");
$("#pagination, .pagination").empty();
return;
}
$.each(data.data.list, function (index, item) {
let tr = " <tr>\n" +
" <td>"+index+1+"</td>\n" +
" <td><input type=\"checkbox\"></td>\n" +
" <td>"+item.roleName+"</td>\n" +
" <td>\n" +
" <button data="+item.id+" type=\"button\" class=\"btn btn-success btn-xs\"><i class=\" glyphicon glyphicon-check\"></i></button>\n" +
" <button data="+item.id+" type=\"button\" class=\"btn btn-primary btn-xs\"><i class=\" glyphicon glyphicon-pencil\"></i></button>\n" +
" <button data="+item.id+" type=\"button\" class=\"btn btn-danger btn-xs\"><i class=\" glyphicon glyphicon-remove\"></i></button>\n" +
" </td>\n" +
" </tr>"
tbody.append(tr);
});
}
//初始化分页导航栏
function initPagination(data) {
let total = data.data.total;
window.pageSize = data.data.pageSize;
$("#pagination, .pagination").pagination(total, {
num_edge_entries: 1, //边缘页数
num_display_entries: 3, //主体页数
callback: pageselectCallback, //页面选择回调函数
items_per_page: data.data.pageSize,//每页显示条目数
current_page: data.data.pageNum - 1, //当前页码,pageInfo从1开始,所以要减1
prev_text: "上一页",
next_text: "下一页"
});
}

function pageselectCallback(page_index, jQuery) {
let pageNum = page_index + 1;
window.pageNum = pageNum;
getPageInfo();
return false;
}
</script>

实现新增

  前端通过模态框进行新增,方便用户。

1
2
3
4
5
6
@PostMapping("/do/add.html")
@ResponseBody
public ResultEntity addRole(@Validated(AddGroup.class) Role role) {
roleService.addRole(role);
return ResultEntity.successWithoutData();
}
1
2
3
4
5
6
7
8
9
10
@Override
public void addRole(Role role) {
try {
roleMapper.insertSelective(role);
}catch (DuplicateKeyException e){
throw new RoleNameAlreadyExistException("角色名称已存在");
} catch (Exception e){
logger.error("add role error", e);
}
}

  异常处理,绑定的异常里面添加路径判断实现不同的效果

1
2
3
4
5
@ResponseBody
@ExceptionHandler(RoleNameAlreadyExistException.class)
public ResultEntity<Object> resolveRoleNameAlreadyExistException(RoleNameAlreadyExistException e){
return ResultEntity.failed(e.getMessage());
}
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
//新增
$("button[name='addBtn']").click(function (){
$(this).parent().find("input").val("");
$("#idError").text("");
$("#roleNameError").text("");
$("#addModal").modal("show");
});
$("#addRoleBtn").click(function () {
let newRoleName = $(this).parent().find("input").val();
let idErrorP = $("#idError");
let roleNameErrorP = $("#roleNameError");
let errorMsgP = $("#errorMsg");
$.ajax({
url:"/role/do/add.html",
method:"post",
data:{
roleName: newRoleName
},
dataType:"json",
success:function(data){
roleNameErrorP.text("");
idErrorP.text("");
errorMsgP.text("");
if (data.result === "fail"){
if (data.map){
idErrorP.text(data.map["id"]);
roleNameErrorP.text(data.map["roleName"]);
}
if (data.message){
errorMsgP.text(data.message);
}
}
if (data.result === "success") {
layer.msg("新增成功");
$("#addModal").modal("hide");
window.keyword = newRoleName;
window.pageNum = 1;
getPageInfo();
}
},
error:function(data){
layer.msg("新增失败",data)
}
});
});

实现更新

  同样使用模态框。前端逻辑新增差不多,类比实现。

1
2
3
4
5
6
@PostMapping("/do/update.html")
@ResponseBody
public ResultEntity updateRole(@Validated(UpdateGroup.class) Role role) {
roleService.updateRole(role);
return ResultEntity.successWithoutData();
}
1
2
3
4
5
6
7
8
9
10
@Override
public void updateRole(Role role) {
try {
roleMapper.updateByPrimaryKeySelective(role);
} catch (DuplicateKeyException e) {
throw new RoleNameAlreadyExistException(CrowdConstant.MESSAGE_ROLE_NAME_ALREADY_EXIST);
}catch (Exception e){
logger.error("update role error", e);
}
}

  当role_name重复时,新增会报DuplicateKeyException,而更新报com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException,神奇的是,还不让我用try,所以异常处理的配置类里修改了一下。

1
2
3
4
5
6
// 角色名称已存在
@ResponseBody
@ExceptionHandler({RoleNameAlreadyExistException.class, MySQLIntegrityConstraintViolationException.class})//这个完整性约束异常要注意其它情况
public ResultEntity<Object> resolveRoleNameAlreadyExistException(Exception e){
return ResultEntity.failed(CrowdConstant.MESSAGE_ROLE_NAME_ALREADY_EXIST);
}

实现删除

  果然是单条删除和多条删除用同一个接口。

记录中断

  那段时间为了在找工作的同时,也在学习。当时骑车去广汉进厂面试了,因为面试上进厂了。在厂里干了七天,最后放弃了,因为要进行国企面试了。当然,马后炮一下,肯定面上了。目前在干党建,加油。