项目实战——搭建网站(持续更新)
一步一步copy一个自己的网站
前言:之前学完ssm框架就想自己搭建一个网站了,但苦于ssm的配置地狱,最近发现boot搭建简易网站的难度要小得多,就着手准备起来了
1. 基础搭建
1.1 搭建一个简易的springboot项目
包括初始化、资源路径文件夹、架构层文件夹、导入thymeleaf依赖并关闭thymeleaf缓存
1.2 加入静态资源
由于自身的前端框架确实太拉,做不出好看的界面,就直接下载的现成的模板,大概是这样子的,也推荐下这个网站,搜了好多后台模板网站都是收费的,只有这个是公开的站长素材

我是将所有的页面都放进了templates文件夹,将css,js,图片等都放进了static文件夹。
1.3 用thymeleaf和controller渲染所有交互接口
- 普通静态资源,像js,css,图片这些不需要服务,只需要链接就行,原本是这样的,用thymeleaf接管的好处是它能动态适应路径

修改后是这样的,th爆红的可能是因为没有加thymeleaf命名空间

-
跳转静态资源,比如说html网页,需要整合controller,编写一个controller类,然后跳转的URl写成控制mapping就行了
@Controller public class IndexController { @RequestMapping("/toIndex") public String toIndex(){ return "index"; } ..... }
1.4 抽取公共部分
我选的这个网页的公共部分有顶部工具栏、侧边栏和底部,抽取的方式是用thymeleaf的fregment标注id,再用insert注入


1.5 遇到的问题
-
静态资源丢失问题,最最恶心的问题之一
我目前遇到的三种情况
第一种,资源路径问题,我开始写的资源路径是对的,因为我清楚的直到boot项目有默认的资源路径,但结果页面演示,就这个功能坑了我,它怎么也加载不出来,我昧着我的良心修改了好多次路径,都不行,在我偶然运行了一次项目后发现成功加载了,顿时人都不好了,这问题也是最经典的boot关于静态资源的问题,我在boot笔记也总结过相关

第二种,thymeleaf缓存问题,项目自身的缓存,当你修改了静态资源,开开心心的去运行项目时,他提交给网页的却是上次加载缓存在jar包里的,所以静态资源修改了但没有完全修改
解决办法是在配置里关掉thymeleaf缓存
spring.thymeleaf.cache=false
第三种,浏览器缓存,和第二种比较像,不过缓存是浏览器帮你做的,真是谢谢了,平时看来很方便的一个功能却成了鸡肋,而且特别恶心人,浏览器提供的清除会清除掉cookie和一些自动帮你填入账号密码的记录,我又不想删除这些,只能选择每次关闭浏览器清除图片和静态资源,每次测试都得关一次浏览器
2022.3.29 时隔多月,我回来了,自从签了工作之后一直在摆烂,最近也是毕设题目刚好是一个网页项目,就想着顺便拿这个做个记录;
浅谈一下新项目吧,新项目是一个会议室预约管理的项目,我也是打算采用springboot去做成一个前后端Web项目,再接着前面的准备工作后,这次我选择了集成springsecurity去认证和授权。
1.6 集成springsecurity测试
比较令我头疼的是,集成在最开始的测试就失败了(用security的原生登录页面登录不进去,显示账号密码错误),原因我至今还没摸清楚,但是我可以浅谈一下我排除的一些错误
①密码编码问题
security选择将前端输入的密码进行编码后再传输到后端进行逻辑比较(这情况只在高版本出现),那么如果后端的密码是直接从数据库取出来的或者后端直接指定的,那么显然密码比对不会正确,所以要在后端进行密码编码
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//定义一个用户以及他的权限等级
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())//定义前端编码方式
.withUser("amlia").
password(new BCryptPasswordEncoder().encode("123456")).roles("权限1","权限2");
}
②未开启security注解
@EnableWebSecurity
//springsecurity的配置类必须加这个注解告诉springboot开启安全模式
//如果security配置类没有被boot识别,还需要再加上@Configuration表明这是一个配置类
③依赖冲突+版本问题
这是我在排除了上面两种可能性之后比较怀疑的,因为我为了后期方便,在项目初始化的时候勾选了很多依赖,但是我后来重新导了一遍冲突,甚至只加了Jdbc和Web的依赖,还是不行,最后我使用之前的MyWeb项目重置了才可以。
1.7 自定义security登录页
在使用Web项目“旧换新”的方法解决了登录问题后,当然是自定义登录页,自定义登录页有一些注意事项,也就是要开始授权,这是初步授权的配置:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.formLogin()
.loginPage("/toLogin")//登录页
.loginProcessingUrl("/toLogin")//登陆访问路径
.defaultSuccessUrl("/toHome").permitAll()//登录成功跳转页面
.and().authorizeRequests()
//放行登录界面和静态资源
.antMatchers("/","/toHome","login.html","/css/**","/js/**","/images/**").permitAll()
.anyRequest().authenticated();
//所有请求都必须被认证,必须登录后才能访问
}
1.8 登录问题
2022.4.10 这个问题在最开始测试登录的时候并没发现,但是最近在测试前端渲染的时候需要重复刷新页面测试,而且我设置了页面拦截。也就是说每次刷新都要重新登录一遍,看起来没问题,但是每次刷新后的第一次登录总是会出问题,报404错误。
最开始我怀疑是不是默认的错误路径/login/error?前面的/login路径和登录路径/login冲突了,我自定义了错误页面并自定义了失败路径.failureUrl("/toError"),但是问题没有得到解决
第一次登录跳转到了一个空白页面,并且路径是/fonts/fontawesome-webfont.ttf?v=4.7.0
我在项目里并没有找到这个路径,我又怀疑是不是缺少了这个文件,我还去fontawesome官网下载了这个文件(真是大冤种)新建了一个对应路径把他放了进去,结果还是不行,一样的错误但是空表页面显示了404,结果我就是给404下载了个样式:-(
在搜索了相关问题后,最后找到了解决办法,原来是重定向的问题,每次刷新之前,比如我之前停留在一个toRoom路径的界面,刷新了之后由于受到了springsecurity的拦截,本应该走向错误界面,但是此时springsecurity将其强制跳转到登录界面,在用户登陆完成之后,默认跳转到了刚才的错误界面,恍然大悟了
要改变这个很简单,只需要在.defaultSuccessUrl("/toHome")后加个true就可以
.defaultSuccessUrl("/toHome",true)
百度是这么说的
.defaultSuccessUrl ("/", true) 如果您未将其设置为 true ,它将自动将用户重定向到上一个上一个请求,在我的情况下,上一个请求将发送到url /error ,这解释了我的问题。 通过将其设置为 true ,您可以强制将用户重定向到您 defaultSuccessUrl 所需要的任何位置。
2.数据库集成
由于我感觉这个小项目的sql语句可能并不复杂,我使用的是JdbcTemplate
数据库结构初步是这样的

编写对应的映射实体类就行,对于关系表的映射,我使用的是Map集合
private Map<Meetroom,Equipment> belongList
添加数据之后,在test里进行测试,测试出来是成功的,证明数据库连接和Template都是没问题的
@SpringBootTest
class MyWebApplicationTests {
@Autowired
JdbcTemplate jdbcTemplate;
@Test
void contextLoads() {
List<Map<String,Object>> list = jdbcTemplate.queryForList("select * from user");
for (Map<String, Object> map : list) {
System.out.println(map.toString());
}
}
}

之后我编写了对应的dao层接口,最开始,我是这样用的,关系类直接走dao层,然后就用queryForList查找出来一个一个去赋值,meetroomService和userService是一样的
@Data
@Component
public class Occupy {
private Map<Meetroom,User> occupyList;
@Autowired
MeetroomService meetroomService;
@Autowired
UserService userService;
@Autowired
JdbcTemplate jdbcTemplate;
public Occupy() {
occupyList = new HashMap<>();
List<Map<String,Object>> list = jdbcTemplate.queryForList("select * from occupy");
for (Map<String, Object> map : list) {
occupyList.put(meetroomService.findRoomById((int)map.get("rid")), userService.findUserById((int)map.get("eid")));
}
}
}
但是是行不通的,一直报空指针异常,最开始我以为是 JdbcTemplate对象没有注入成功,但是boot官方文档确实是这么教的,所以我一度很怀疑自己,也怀疑是不是和@Component注解冲突的问题 ,网上确实有只言片语这么说,但是我如果放到测试类里就是好的。证明这个不是问题的关键
这个bug困扰了我许久,在辗转反侧几天后,发现了问题的根源,空指针是语句查询返回空报的,这是springdata的防御机制,为了让程序员的代码更健壮,防止对null值得错误比较,所以是语句的问题,语句需要在加一些约束,不能丢简单的sql,而且为了使代码更具有专业性,我还丢了一个映射类进去,对arm进行解释,这样其实已经有点像Mybatis模式了
@Repository
public class EquipmentService {
@Autowired
JdbcTemplate jdbcTemplate;
public List<Equipment> findAllEquip() {
return jdbcTemplate.query("select * from equipment", new EquipRowMapper());
}
public Equipment findEquipById(int id) {//根据设备id查询到设备
return jdbcTemplate.queryForObject("select * from equipment where eid=?", new Object[]{id}, new EquipRowMapper());
}
}
class EquipRowMapper implements RowMapper<Equipment> {//映射
@Override
public Equipment mapRow(ResultSet rs, int rowNum) throws SQLException {
Equipment equipment = new Equipment();
equipment.setEid(rs.getInt("eid"));
equipment.setEname(rs.getString("ename"));
equipment.setEstate(rs.getBoolean("estate"));
return equipment;
}
}
2022.4.2 在jdbcTemplate方式下的开发遇到了瓶颈,苦于我对jdbcTemplate的映射方式不是很清楚,导致关联表的映射没法进行,是这样的,我有一个用户表和一个会议室表,还有一个额外的表格专门用来记录他们之间的关系,列值就用用户id和会议室号,我想的是返回一个Map<Integer,Integer>来存储比较利于后续的使用,但是我不懂如何用jdbcTemplate去映射,搜寻了一天也无果,好像映射只能用自定义类,像Integer这种没有set方法去设定值得就很麻烦,但是为其专门自定义一个类又很多此一举,倒不如直接把关系表映射到一个实体类上,比如这样
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Occpy {
private Map<Integer,Integer> occpyMap;
}
但实际上,除此之外,我还有一个关系表用来表示会议室和设备之间的关系,如果要为关系表设置实体类,会显得dao层和service层很臃肿,最终还是决定放弃掉用jdbcTemplate去操作数据库
2022.4.6 经过几天的温习Mybatis和重写架构,新数据库架构已经初步完成,总体思路是修改数据库架构,因为多表子查询对Mybatis也异常复杂,我把关系表改成了多对一绑定,比如一个人可能预约多个会议室,但是会议室同一时间只能被一个人预约,那么在会议室里加一列用户id用来绑定用户,为空时证明会议室无人预约,这样一来只用一个简单的子查询就能解决,而且整个架构也很清晰

简单举例一个UserMapper.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="www.amlia.com.demo.mapper.UserMapper">
<select id="findUserById" parameterType="int" resultMap="MyUser">
SELECT * FROM user WHERE uid=#{id}
</select>
<select id="findGradeById" parameterType="int" resultType="Integer">
select ugrade from user where uid=#{id}
</select>
<select id="findAllUser" resultMap="MyUser">
select * from user meetroom
</select>
<insert id="addUser" parameterType="User">
insert into user (uid,ugrade,uname,unumber,upassword,phone) values
(#{uid},#{ugrade},#{uname},#{unumber},#{upassword},#{phone})
</insert>
<update id="updateUser" parameterType="User">
update user set uname=#{uname},unumber=#{unumber},ugrade=#{ugrade}
,upassword=#{upassword},phone=#{phone}
</update>
<delete id="deleteUser" parameterType="int">
delete from user where uid=#{id}
</delete>
<select id="findRoomByUser" parameterType="int" resultType="Meetroom">
select * from meetroom where uid=#{id}
</select>
<!--由于我的数据库列名和类属性名基本一样,这里没进行映射-->
<resultMap id="MyUser" type="User">
<collection property="roomList" column="uid" ofType="Meetroom" javaType="java.util.List" select="findRoomByUser"/>
</resultMap>
</mapper>
2.3 遇到的问题
2022.4.10 这里解决一下在3.3遇到的问题,我对所有数据库进行了测试,发现id相关的全都输出为0

我检查数据库列名和类属性的确能对应上,因为我当时为了方便,把两个名字定义的一模一样,所以不存在映射不上的问题,我又检查了属性类型发现也确实都是int,随后我发现了,在联合查询下,子查询的结果id是可以显示的,我立马感觉到问题所在了。由于我把属性名和列名定义的一样,所以在联合查询的mapper结果集映射里没有进行映射,按理说可以不用,但是问题出在,我的meetroom表里也存了一个uid,两个冲突了,所以id的映射是必须的
<resultMap id="MyUser" type="User">
<result column="uid" property="uid"></result>
<collection property="roomList" column="uid" ofType="Meetroom" javaType="java.util.List" select="findRoomByUser"/>
</resultMap>
加了映射之后,输出正确
3.前端渲染
3.1 首页标题栏高亮

一般我们都希望将当前所显示的大标题高亮起来,而且会随着我点击不同的标题去动态高亮,这个该如何实现呢,正好这个模板也没有用到高亮,我也去学习了一下相关知识,标题这快的代码是这样的
<div class="collapse navbar-collapse" id="navbarsExample04">
<ul class="navbar-nav mr-auto nav-pills" id="myul">
<!--这个active其实就是高亮,他是vue中的一个元件-->
<li class="nav-item active">
<a class="nav-link " th:href="@{/toHome}">首页</a>
</li>
<li class="nav-item">
<a class="nav-link" th:href="@{/toLogin}">登录</a>
</li>
<li class="nav-item">
<a class="nav-link " th:href="@{/toRoom}">会议室</a>
</li>
<li class="nav-item">
<a class="nav-link" th:href="@{gallery.html}">通告</a>
</li>
<li class="nav-item">
<a class="nav-link" th:href="@{blog.html}">历史评价</a>
</li>
<li class="nav-item">
<a class="nav-link" th:href="@{login.html}">我的</a>
</li>
</ul>
</div>
知道了active就是高亮,该怎么让它动态显示呢,这就要用到js去实现,这串代码充分显示了js和java的相似性
$(document).ready(function () {
//each是为每一个选中的元素该方法
$("#myul").find("li").each(function () {
var a = $(this).find("a:first")[0];//定义一个变量存储当前的第一个a标签,事实上每个li也只有一个标签所以这样写
// location.pathname获取当前浏览器上的url地址,查看与之相同的href即是当前点击的标题
if ($(a).attr("href") == location.pathname) {
$(this).addClass("active");
} else {
$(this).removeClass("active");//remove为了取消默认首页高亮
}
});
});
用js标签直接写到html里或者存在文件里然后引用都可以
3.2 插入模板
2022.4.11因为原先的模板资源有限,并没有我想要的一些页面,比如个人信息页,直接的方式就是再拿一个模板,把想要的html代码插入到我们的页面中。但是这样会带来一个问题,就是css-class命名冲突问题,因为模板的很多命名都是偏系统的,比较相似,冲突问题会导致模板不能呈现出想要的结果,像这样:


就导致了不能单纯引入插入模块的样式,需要有一些手段,最开始我想的比较简单,就是说在div块里有没有一种引用方式可以引用css样式,也就是说,使css样式只针对部分div生效,但是搜寻无果,有一个可能的解决方式是css-moudle,但是碍于我的前端知识过于薄弱,看了半天就好像看天书一般。最后耽搁了几天,选择了一种暴力的方式,就是我把插入模板要引用的css全部整合到一个css里面,然后对其所有引用到的class命进行统一修改,ctrl+f搜索class名,点击这个选中所有就可以统一修改

3.3 渲染数据
对于个人信息页面的数据渲染,总体就是用thymeleaf提供的api去实现,非常简单,拿用户姓名的渲染举例,我们需要后端首先获取到当前登录用户的信息,由于我们使用了springsecurity框架,这点变得很容易,只需要定义一个参数Principal principal,这个类对象其实就是springsecurity为我们封装的用户信息
@Autowired
UserService userService;
@RequestMapping("/toMine")
public String toMine(Model model,Principal principal){
List<User> list = userService.findAllUser();//获取一个用户列表
for (User user : list) {
if(user.getUnumber().equals(principal.getName()))
model.addAttribute("user",user);//找到当前用户,返回给前端
}
return "mine";
}
前端方面,直接用th:text="${}"获取文本
<h4 class="card-title m-t-10" th:text="${user.getUname()}"></h4>
如果是循环的话,用th:each,比如我的用户里存的会议室list就可以这么渲染到前端,有点类似于foreach循环
<div th:each="room:${user.getRoomList()}">
<h6 th:text="${room.getRname()}"/><a href="#" class="btn btn-success" th:text="${room.getRgrade()}"></a>
<a href="#" class="btn btn-success">取消预订</a><br>
<br>
</div>
但是在进行渲染头像的时候出了问题,因为头像每个人是不同的,而且要根据uid去动态渲染,thymeleaf也有这种用法,比如在路径里我需要提取uid,就可以这样
<img th:src="@{images/profile/{jpgName}.jpg(jpgName=${user.getUid()%10000})}" class="img-circle" width="150">
我的头像路径是images/profile/*.jpg, 星号代表的是用户uid对10000取余,写法是没问题的,但是无论如何也加载不出来问题,排除了路径、缓存等等问题后,我在前端查看源码发现了,不管用户登录为什么,取余都是0,我起初以为是写法的错误导致传过来为null值然后被初始化了,但是我试了其他的用户等级发现是可以的,我在后台测试了数据库的输出,这才恍然发现,uid从查过来就一直是空的,这个问题我放到数据库那个模块了 ↑,解决之后果然渲染了正确头像

4.增删改查
4.1 我的页面
4.1.1 评论系统
为了先简单的测试一下增删改查的可用性,我新增了一个数据库表blog,blog里存储的是用户的评论,以及通告等,这是几个例子

@Data
@AllArgsConstructor
@NoArgsConstructor
@Component
public class Blog {
private int replyid;//回复的用户id
private int uid;//发表评论的用户id
private int bid;//唯一标识一个评论的id
private String blog;//发表的评论
private String picture;//发表的图片
private int rid;//评论针对的会议室
private Date date;//发表的时间
}
评论我打算展示在我的页面中,渲染完之后是这样

回复他按钮会新增一个评论,并且指向这个人,具体的前端代码是这样
<div class="tab-pane active" id="home" role="tabpanel">
<div class="card-body">
<div class="profiletimeline">
<div class="sl-item" th:each="replyBolg:${replyBlogs}">
<div class="sl-left"> <img th:src="@{images/profile/{jpgName}.jpg(jpgName=${replyBolg.getUid()%10000})}" alt="user" class="img-circle"> </div>
<div class="sl-right" th:with="user1=${userService.findUserById(replyBolg.getUid())}">
<div><a href="#" class="link" th:text="${user1.getUname()}"></a> <span class="sl-date" th:text="${replyBolg.getDate().toString()}"></span>
<p th:text="${replyBolg.getBlog()}">
<div class="row">
<div class="col-lg-3 col-md-6 m-b-20"><img th:src="@{images/img1.jpg}" class="img-responsive radius"></div>
</div>
<form th:action="@{/ReplyToOther}" th:method="post" th:object="${blog}">
<input type="hidden" th:name="uid" th:value="${user.getUid()}">
<input type="hidden" th:name="replyid" th:value="${replyBolg.getUid()}" >
<input type="hidden" th:name="date" th:value="${#dates.format(#dates.createNow(),'yyyy-MM-dd')}">
<input placeholder="输入评论" th:name="blog"><button class="btn-primary">回复他</button>
</form>
</div>
</div>
</div>
<hr>
</div>
</div>
</div>
这里涉及一个知识点,就是themeleaf表单向后端返回bean,th:object去声明这个对象,前提是这个对象要先从后端传到前端来,所以实质上themeleaf只是在修改这个对象的属性,在返回给前端,th:name是这个对象的属性名,th:value是要赋给它的值,要记住,这个对象所对应的类必须要有对应的set方法

Controller层是这样的
@RequestMapping("/ReplyToOther")
public String ReplyToOther(Blog blog){
blogService.addBlog(blog);
return "redirect:/toMine";
}
这里我遇到了一个问题,就是在数据库里,没有赋值的情况下,int型的初值会是null,但是java里没有赋值的int型是0,也就是说,当我新建了一个回复评论时,他是没有roomid的,回复评论只存储被回复用户的id,也就造成了传到数据库的roomid值为0,但是这个回复评论也需要在我的评论里展示

由于我在我得评论里展示了会议室的名字,所以这个回复评论的roomid查找不出来报错了,也促发了我的另一种想法,用th:if去判断存在性,然后决定是否展出
<label class="col-md-12" th:if="${myBlog.getRid()!=0}" th:text="${room.getRname()}+会议室"></label>
<label class="col-md-12" th:if="${myBlog.getReplyid()!=0}" th:with="replyuser=${userService.findUserById(myBlog.getReplyid())}" th:text="回复 +${replyuser.getUname()}"></label>
图片目前还没有解决,涉及更多的问题
4.1.2 取消预约会议室
我的预约信息是在meetroom表里绑定的,就是每个会议室绑定了一个用户id,所以取消预约实质上是对会议室信息的修改,但是我为了展示方便,在实体类里,用了一个User对象去存储,这也直接导致了修改的困难,总不能是uid=#{user.geiUid()},实不相瞒,我确实试过这个,最终是新加了一个sql解决的,两参数,一个是rid用来精准查找,一个是uid用来修改。
int deleteUser(@Param("uid")int uid,@Param("rid")int rid);
<update id="deleteUser">
update meetroom set uid=#{uid} where rid=#{rid}
</update>
4.1.3 上传图片和显示
2022.4.19因为评论的图片不能总是固定的存在项目里,这样缺乏动态性,用户可能会选择自己的表情包或者照片去评论,而且这些"动态"的图片不能存储在静态的项目包里,所以只能存储在电脑磁盘,图片的存取我用的是File,前端用的是form表单,controller用的是java提供的api MultipartFile,这里由于我的文件知识比较匮乏,采纳的一个博主的博客![(9条消息) springboot2整合thymeleaf实现图片上传及回显文件系统的图片_mzjmmc的博客-CSDN博客_thymeleaf图片回显]
propertis配置
# 开启文件上传
spring.servlet.multipart.enabled=true
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=10MB
# 一个全局常量
uploadFolder=D:/myApplication/picture/
# 静态资源匹配
spring.mvc.static-path-pattern=/**
# springboot静态扫描路径,前几项是默认的,file的路径是新加的,也就是存图片的路径,这样就可以直接通过图片名找到图片
spring.resources.static-locations=classpath:/META-INF/resources/,classpath:/resources/, classpath:/static/, classpath:/public/, file:D:/myApplication/picture/
前端:
<form id="form2" enctype="multipart/form-data" method="post" th:action="@{/uploadPicture}">
图片: <input type="file" name="fileUpload"/>
<input type="submit" form="form2" value="上传"/>
</form>
controller
@Controller
public class UploadController {
@RequestMapping("/myApplication/picture")
public String upload() {
return "myApplication/picture";
}
@Value("${uploadFolder}")
private String fileDir;
@RequestMapping("/uploadPicture")
public String uploadPicture( RedirectAttributes attribute, MultipartFile fileUpload) {
String filename = fileUpload.getOriginalFilename();
String suffixname = filename.substring(filename.lastIndexOf('.'));
filename = UUID.randomUUID()+suffixname;//形成一个独一无二的文件名
File file = new File(fileDir);
if(!file.exists()) {
file.mkdir();
}//文件路径
File file2 = new File(fileDir + filename);
try {
fileUpload.transferTo(file2);//图片注入文件
} catch (IllegalStateException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
attribute.addFlashAttribute("imgDirectory",filename);//文件名参数
return "redirect:/toMine";//重定向
}
}
这样前端显示就可以只用这个imgDirectory就可以直接找到图片并显示,但要注意,因为是找图片,所以前提是th:src="@{}",里面的值才是${imgDirectory}
4.1.4 form表单嵌套问题
因为我的显示格式问题,上传的这个form也必须在整个评论的form里面,也就是一个form嵌套

但问题来了,在idea模式下,上传这个按钮被匹配到了form1,点击它就把form1提交了,而后面的input全是null,这当然是我所不想看到的,我去搜了下,button能不能绑定到form上,发现可以这样,组件有一个form属性可以把他绑定到具体的
<form th:if="${item.count!=Item}" id="form2" enctype="multipart/form-data" method="post" th:action="@{/uploadPicture/{item}(item=${item.count})}">
图片: <input type="file" name="fileUpload"/>
<input type="submit" form="form2" value="上传"/>
</form>
这还不够,还有一个问题是,form表单在范围内有一个提交成功的话,其他的提交会被屏蔽掉,也就是说,这样做的结果虽然可以让图片表单提交成功,但是blog表单的提交按钮却失效了,也就是说,必须把图片form下面所有blog form的子集全提出来
<form id="form1" th:action="@{/ReplyToOther}" th:method="post" th:object="${blog}">
<input type="hidden" th:name="uid" th:value="${user.getUid()}">
<input type="hidden" th:name="replyid" th:value="${replyBolg.getUid()}" >
<input type="hidden" th:name="date" th:value="${#dates.format(#dates.createNow(),'yyyy-MM-dd')}">
</form>
<form id="form2" enctype="multipart/form-data" method="post" th:action="@{/uploadPicture}">
图片: <input type="file" name="fileUpload"/>
<input type="submit" form="form2" value="上传"/>
</form>
<!--提取的子集,使他的提交按钮在form范围之外不被强制失效就可以了-->
<div form="form1" class="blog_img">
<figure><img class="img-profile" th:src="@{${imgDirectory}}" alt="#"/></figure>
<input form="form1" type="hidden" th:name="picture" th:value="${imgDirectory}">
</div>
<input form="form1" placeholder="输入评论" th:name="blog"><button form="form1" class="btn-primary">回复他</button>
4.1.5 上传图片的遗留问题
由于我的上传图片的页面有很多这样的上传接口和展示,所以就会出现上传了一张图片后,所有的接口都显示了这张图片,像这样

但实际上我只在第一个接口上传了图片,这是由于我用的th:each打印的这些块,所以每部分的块逻辑是一样的,所以要加一些约束,我几乎在第一时间想到了路径传参,可以把当前的循坏迭代次数item.count当做参数传入url,然后再传回给前端,在th:if里面做一个比较,就可以清除知道,我点击了哪个接口的上传,在选择是否打印
<form th:if="${item.count!=Item}" id="form2" enctype="multipart/form-data" method="post" th:action="@{/uploadPicture/{item}(item=${item.count})}">
图片: <input type="file" name="fileUpload"/>
<input type="submit" form="form2" value="上传"/>
</form>
<div th:if="${imgDirectory!=null&&item.count==Item}" form="form1" class="blog_img">
<figure><img class="img-profile" th:src="@{${imgDirectory}}" alt="#"/></figure>
<input form="form1" type="hidden" th:name="picture" th:value="${imgDirectory}">
</div>

4.1.6 上传图片的遗留问题2
2022.4.20 在解决了上面的问题后,有一个新的问题接踵而至,就在我在我得界面有三个跳转锚点,默认的是评论界面,如果我在我的会议室上传了一张图片后,我希望的是他依旧跳转后停留在这个锚点,但实际上每次都会跳转到默认的,就很不银杏

就涉及到了每次跳转到页面固定的锚点问题,其实这个有很多方法详情可见这篇博客![(9条消息) HTML页面跳转的方法_可乐6666的博客-CSDN博客_html页面跳转],但是我的跳转是动态的,所以必须要有themeleaf,我试了themeleaf和这几种方式的结合,好像都不太行,最后我发现好像实际上这些锚点是用active控制的,我直接破防,弄了半天是老本行
<script>
$(document).ready(function () {
//获取到参数
var log = '[[${flag}]]';
if (log) {
$("#tab_ul").find("li").each(function () {
var a = $(this).find("a:first")[0];
//比较当前a标签的href和参数是否一致
if ($(a).attr("href") == '#' + log) {
$(a).addClass("active");
//修改所有的id为参数的为active
$("#" + log).addClass("active")
} else {
$(a).removeClass("active");
$($(a).attr("href")).removeClass("active");
}
});
}
});
</script>
参数是我从form传过去为了标识当前active的,比如我从rooms标签上传了图片,我想要跳转后停留在rooms标签,我就把rooms的id当错参数传递给后端在传递回来,还有一点要注意的是,就是这个js需要获取model里的值,所以只能写在html页面里,而且只能写在body外,不能想在哪里插在哪里插
4.1.7 个人信息编辑页面
没什么说的,直接贴,Item在这里不是迭代次数,所以为了与之前的区分开,我直接给的0
<div class="tab-pane" id="compile" role="tabpanel">
<div class="card-body">
<h3 class="card-title">个人信息编辑</h3>
<hr>
<div form="form4" class="blog_img">
<figure>
<img th:if="${Item!=0}" class="img-profile" th:src="@{${user.getHead()}}" alt="#"/>
<img th:if="${Item==0}" class="img-profile" th:src="@{${imgDirectory}}" alt="#"/>
</figure>
<input form="form4" type="hidden" th:name="head" th:value="${imgDirectory==null}?${user.getHead()}:${imgDirectory}">
</div>
<form enctype="multipart/form-data" method="post" th:action="@{/uploadPicture/0}">
修改头像:<input type="file" name="fileUpload"/>
<input type="submit" value="上传"/>
<input type="hidden" name="flag" value="compile"/>
</form>
<form id="form4" th:action="@{/EditInformation}" th:method="post" th:object="${user}">
<input type="hidden" th:name="uid" th:value="${user.getUid()}">
<input type="hidden" th:name="unumber" th:value="${user.getUnumber()}">
<div class="form-body">
<div class="row p-t-20">
<div class="col-md-6">
<div class="form-group">
<label class="control-label">姓名</label>
<input type="text" th:name="uname" class="form-control" th:value="${user.getUname()}">
<small class="form-control-feedback" th:text="'账号:'+${user.getUnumber()}"></small>
</div>
</div>
<div class="col-md-6">
<div class="form-group has-danger">
<label class="control-label">电话号</label>
<input type="text" th:name="phone"
class="form-control form-control-danger" th:value="${user.getPhone()}" >
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group has-success">
<label class="control-label">性别</label>
<select th:name="sex" class="form-control custom-select">
<option value="1">男</option>
<option value="0">女</option>
</select>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label class="control-label">等级</label>
<label class="form-control" th:text="${user.getUgrade()}"></label>
<small class="form-control-feedback">不可更改</small>
</div>
</div>
</div>
<div class="row p-t-20">
<div class="col-md-6">
<div class="form-group">
<label class="control-label">邮箱</label>
<input type="text" th:name="email" class="form-control"
th:value="${user.getEmail()}">
</div>
</div>
<div class="col-md-6">
<div class="form-group has-danger">
<label class="control-label">密码</label>
<input type="text" th:name="upassword"
class="form-control form-control-danger" th:value="${user.getUpassword()}">
</div>
</div>
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-success"><i class="fa fa-check"></i>提交
</button>
<button type="button" class="btn btn-inverse">取消</button>
</div>
</form>
</div>
</div>
在给一个Mine的所有controller
package www.amlia.com.demo.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import www.amlia.com.demo.pojo.Blog;
import www.amlia.com.demo.pojo.Meetroom;
import www.amlia.com.demo.pojo.User;
import www.amlia.com.demo.service.BlogService;
import www.amlia.com.demo.service.MeetroomService;
import www.amlia.com.demo.service.UserService;
import java.lang.reflect.Parameter;
import java.security.Principal;
import java.util.List;
/**
* @classname
* @Author 姬如千泷
* @Date 2021/7/29 1:04
* @Version 1.0
*/
@Controller
public class MineController {
@Autowired
UserService userService;
@Autowired
BlogService blogService;
@Autowired
MeetroomService meetroomService;
@RequestMapping("/CancelTheReservation/{rid}")//取消预约
public String CancelTheReservation(@PathVariable(name = "rid")Integer rid){
meetroomService.findRoomById(rid).setUser(null);
meetroomService.updateUser(rid,0);
return "redirect:/toMine";
}
@RequestMapping("/ReplyToOther")//回复他人
public String ReplyToOther(Blog blog){
blogService.addBlog(blog);
return "redirect:/toMine";
}
@RequestMapping("/CommentToRoom")//评论会议室
public String CommentToRoom(Blog blog){
blogService.addBlog(blog);
return "redirect:/toMine";
}
@RequestMapping("/DeleteBlog/{bid}")//删除评论
public String DeleteBlog(@PathVariable(name = "bid")Integer bid){
blogService.deleteBlog(bid);
return "redirect:/toMine";
}
@RequestMapping("/EditInformation")//编辑个人信息
public String EditInformation(User user){
userService.updateUser(user);
return "redirect:/toMine";
}
}
4.2 通知页面
2022.4.17通知页面比较简单,因为在用户界面,通知是不能更改和删除的,所以只需要把blog内容展示出来就行了,说一下我遇到的两个问题
4.2.1 通知blog的查找问题
在之前,我将通知类的blog设置为uid为null以及replyid为null,然后查找是找这两个属性都为null的行。但带来的问题是通知的来源得不到记录,如果同时存在多个管理员,那么将不能知道通知由哪个管理员发出,所以我给通知blog也加了uid,让确定rid和replyid同时为null的行,还有一种情况是,数据库里的数据是java后端写入的,所以值可能为0,所以要加一个||为0 的判断,所以我开始是这样写的
<select id="findInforms" resultType="Blog">
SELECT * FROM blogs WHERE replyid=null and (rid=null or rid=0)
</select>
但根本查不出结果,而且在idea提供的数据库工具里这个语句也查不到任何结果,我去网上搜了下,原来在数据库里,不能用等号去判断一个列值是否为null,因为如果他为null就不是一个存在的值,但是java中依旧有引用指向它,所以sql语句中要用is null
<select id="findInforms" resultType="Blog">
SELECT * FROM blogs WHERE replyid is null and (rid is null or rid=0)
</select>
4.2.2 页面css样式问题
在组合页面模板的时候这是一个很致命的问题,我之前采用的暴力修改class名的方式,对某些css是不适用,比如我这个模板里有这样一个字段
<section class="services section">
<div class="container2">
<div class="row2" th:each="inform:${informs}">
<div class="color-md-4 color-sm-6 color-xs-12">
我像之前一样最开始将services section改成了services2 section2,但是样式显示不出来,后来我猜想,像services section这种看起来很正规的命名可能本身带有一些bootstrap的样式,果不其然,我改回来之后显示了样式

4.3 会议室预约页面
该页面主要就是会议室信息的修改,每一次预约就是一次uid,begin,end(我新加的代表着会议室预约开始和结束时间的列)的修改
4.3.1 时间显示
因为主界面有一个记录预约时间的轴,所以存在两种情况,选好了时间点击现在预订进入会议室界面,另一种是什me都没选直接进入,两种情况自然是不同的。

首先面临的问题是date数据的前后端传输,他首先在首页的toHome传给后台的controller,然后controller在传给room界面,因为前端传到后端的date还是字符串形式,所以得用string接收而不是Date
@RequestMapping("/toRoom")
public String toRome(Model model, Principal principal,String begin,String end) throws ParseException {
SimpleDateFormat ft = new SimpleDateFormat("yyyy-MM-dd");//时间转换模板
List<Meetroom> rooms = meetroomService.findAllRoom();
model.addAttribute("rooms",rooms);//会议室列表
model.addAttribute("room_t",new Meetroom());//需要一个MeetRoom对象,便于form表单接收再次传给后端
model.addAttribute("begin",ft.parse(begin));
model.addAttribute("end",ft.parse(end));//转化后的时间传给前端
model.addAttribute("blogService",blogService);
List<User> list = userService.findAllUser();
for (User user : list) {
if(user.getUnumber().equals(principal.getName()))
model.addAttribute("user",user);
}
return "room";
}
而当在前段需要传一个完整的meetroom到后端去,但是begin和end是字符串,无法把他赋值给Date类型,所以我还是以字符串返回,在后端进行赋值处理
@RequestMapping("/ReservingRoom")
public String ReservingRoom(Meetroom room,String begin_date,String end_date,Integer user_id) throws ParseException {
SimpleDateFormat ft = new SimpleDateFormat("yyyy-MM-dd");//转换模板
room.setBegin(ft.parse(begin_date));
room.setEnd(ft.parse(end_date));//赋值
meetroomService.updateRoom(room);//先修改时间
meetroomService.updateUser(room.getRid(),user_id);//单独修改uid
return "redirect:/toMine";
}
但是本来我想的是,如果是从现在预约进入的,那么在选择会议室时就可以显示我之前调好的时间,但是不管用value还是placehooder都不能使date的初值改变,最终放弃

最终定的方案是,判断一下,如果我在前面调好了时间,那么日期框将不会展示,而是在页眉展示时间

<div class="blog">
<div class="container">
<div class="row">
<div class="col-md-12">
<div class="titlepage">
<p class="margin_0">点击预约来为您预约一个会议室</p>
<p class="margin_0" th:if="${begin!=null&&end!=null}" th:text="${begin.toLocaleString()}+'至'+${end.toLocaleString()}"></p>
</div>
</div>
</div>
<div class="row" >
<div class="col-md-4" th:each="room,item:${rooms}">
<div class="blog_box">
<div class="blog_img">
<figure><img th:src="@{images/blog1.jpg}" alt="#"/></figure>
</div>
<form class="blog_room" th:method="post" th:action="@{/ReservingRoom}" th:object="${room_t}">
<input th:name="user_id" type="hidden" th:value="${user.getUid()}">
<input th:name="rid" type="hidden" th:value="${room.getRid()}">
<input th:name="rgrade" type="hidden" th:value="${room.getRgrade()}">
<input th:name="rstate" type="hidden" th:value="${room.rstate}">
<input th:name="rname" type="hidden" th:value="${room.getRname()}">
<h3 th:text="${room.getRname()}"></h3>
<th:block th:each="equip:${room.getEquipList()}">
<span th:text="${equip.getEname()}"></span>
</th:block>
<div th:each="blog:${blogService.findBolgsByRid(room.getRid())}">
<p th:text="'匿名用户:'+${blog.getBlog()}"></p>
</div>
<div th:if="${begin!=null&&end!=null}">
<input class="online_book" type="hidden" th:name="begin_date" th:value="${begin.toLocaleString()}">
<input class="online_book" type="hidden" th:name="end_date" th:value="${end.toLocaleString()}">
</div>
<div th:if="${room.rstate&&room.getUser()==null&&(begin==null||end==null)}">
<input class="online_book" type="date" th:name="begin_date">至<input class="online_book" type="date" th:name="end_date">
</div>
<button class="read_more" th:if="${room.rstate&&room.getUser()==null}">点击预约</button>
<p th:if="${room.rstate&&room.getUser()!=null}" th:text="'被预约至'+${room.getEnd().toLocaleString()}"></p>
<a class="read_more" th:if="${room.rstate&&room.getUser()!=null}" th:href="@{Javascript:void(0)}">已被预约</a>
<a class="read_more" th:if="${!room.rstate}" th:href="@{Javascript:void(0)}">已损坏</a>
</form>
</div>
</div>
</div>
</div>
</div>
5.整合admin后台
2022.4.23我想要的效果是在项目路径上加个/admin就跳转到后台登陆页面,省的去做一个新系统,大概是这样的

5.1 基本配置
5.1.1 多HttpSecurity配置
我几乎在一开始就想到可不可以实现多个登录验证器,从而达到不同用户的登录实现,实际上确实也可以这么做,只需要在原来配置的基础上,增加一个静态内部类继承WebSecurityConfigurerAdapter就行
@Order(1)
@Configuration
public static class AdminConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.formLogin()
.loginPage("/admin")//登陆访问路径
.loginProcessingUrl("/admin")//表单提交路径
.defaultSuccessUrl("/admin/toAdminHome",true).permitAll()//登录成功跳转页面
.failureForwardUrl("/admin?error=true"); http.authorizeRequests().antMatchers("/style/**","/new/**","Mapper/**").permitAll();
//所有/admin/**路径的请求都会验证manager权限
http.antMatcher("/admin/**").authorizeRequests()
.anyRequest().hasRole("MANAGER").and();
}
}
注意这个Order注解,他很重要,代表了配置的加载权限,一般有特殊路径的登录验证通常要先加载,不带Order注解的配置默认加载权重为100,我的整个SecurityLoginConfig是这样的
@EnableWebSecurity
public class SecurityLoginConfig{
@Autowired
UserService userService;
@Autowired
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
List<User> allUser = userService.findAllUser();
for (User user : allUser) {
if (user.getUgrade() >= 3) {
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
//定义一个用户以及他的权限等级
.withUser(user.getUnumber()).password(new BCryptPasswordEncoder().encode(user.getUpassword())).roles("USER","MANAGER");
} else {
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
//定义一个用户以及他的权限等级
.withUser(user.getUnumber()).password(new BCryptPasswordEncoder().encode(user.getUpassword())).roles("USER");
}
}
}
@Configuration
public static class UserConfig extends WebSecurityConfigurerAdapter{
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.formLogin()
.loginPage("/toLogin")//登录页
.loginProcessingUrl("/toLogin")//表单提交路径
.defaultSuccessUrl("/toHome",true).permitAll()//登录成功跳转页面
.and().authorizeRequests()
//放行登录界面和静态资源
.antMatchers("/toLogin","login.html","/style/**","/new/**","Mapper/**","/css/**","/js/**","/images/**").permitAll()
.anyRequest().authenticated();
//所有请求都必须被认证,必须登录后才能访问
}
}
@Order(1)
@Configuration
public static class AdminConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.formLogin()
.loginPage("/admin")//登陆访问路径
.loginProcessingUrl("/admin")
.defaultSuccessUrl("/admin/toAdminHome",true).permitAll();//登录成功跳转页面
http.antMatcher("/admin/**").authorizeRequests()
.anyRequest().hasRole("MANAGER").and();
}
}
}
在用户授权中,如果哪个用户的等级大于等于3,就给予它管理员权限,可以访问/admin路径,AdminConfig优先加载,表明了/admin路径下的权限验证交由它接管,那么接下来加载的config如果没有特别声明,那么除了/admin模式的其他所有路径都交由UserConfig管理,所以才需要把他叔路径的config提前加载
然而我遇到了问题,就是每次点击登录之后又会重定向到当前登录页,而且请求中并没有携带用户信息

我搜寻了很多解决办法,有的说是删掉.loginProcessingUrl配置,有的说是原生服务器的session名字和授权服务器的session名字重命名然后被覆盖掉了,需要修改session名,然而我试了都不行,我尝试着让它显示security封装的错误信息,关于这个详情可见这篇博客![SpringBoot+SpringSecurity+Thymeleaf认证失败返回错误信息 - 简书 (jianshu.com)],它讲解了从底层方面探究这个错误信息的过程,但是他说的方法不对,不知道是版本问题还是什么
我是这样打印成功的,爆红信息不要理会,这是themeleaf的"特色",打印出来的错误是这样的
<p style="color: red" th:if="${param.error}" th:text="${SPRING_SECURITY_LAST_EXCEPTION.message}"></p>

这个错误一般是用户名错误或者密码错误,但是我的肯定是没问题的检查了前都后端,都没问题,直接崩溃,偶然的一次我发现,我的表单里账号密码栏的映射用的是id而不是name,我顿时恍然,id是和后端映射不到的,修改了之后才正常显示。
5.1.2 admin进入后台主页后样式不显示
我的admin后台的样式文件全部在static路径下的style文件夹存储

所以按理说我的temeleaf路径这样写是正确的,但实际进去页面后却压根不加载样式,我怀疑是不是静态资源被拦截了,然而我用路径访问静态css是可以加载的,也就证明问题不在拦截上,而且登录页面的样式都加载的好好的

然后我在前端控制台发现了一件不可思议的事

我的路径上无缘无故多出来一个admin,所以静态资源全是基于/admin的,但是我的资源路径根本没有这个admin的文件夹,所以找到不到资源,我猜想可能是因为antMatchers("/admin/**").hasRole("MANAGER");这个配置导致了从/admin路径下访问静态资源都默认加上了/admin路径,所以只需要在加载不出样式的html加一句这样的声明,代表拼接请求路径依旧是以根目录为基础
<base th:href="@{/}">

浙公网安备 33010602011771号