房聚良源系统功能介绍(SpringBoot)

2 篇文章 0 订阅
1 篇文章 0 订阅

房聚良源系统

(软件系统已进行最后一次迭代,迭代后的系统展示请移步到B站,搜我名字看相关投稿视频讲解或访问space.bilibili.com/397481080)

项目简介

       如今面向外来务工人员的房源越来越多,如何让拥有此类型房源的房东们更便捷地解决日常的房源房间管理、人员管理、业务计算等工作,房聚良源系统应运而生!该系统能够让房东们创建多个房源单位,而在单一房源单位的基础上设置该房源拥有的房间。房东们可在系统中办理房客入住、计算月租、退房等业务。亦可在日常对房客信息和房间信息进行管理。同时也可以面向每一位在系统中登记入住的房客访问系统,查看个人、房间等信息。同时也能让游客浏览房源,挑选自己心仪的房间进行预约看房。

项目技术摘要

项目采用前后端分离式开发

前端

技术:Vue.js、elementUI

开发工具:IDEA、vscode

后端

语言:Java

技术:Spring、SpringMVC、Mybatis、Mybatis-plus、SpringBoot、SpringSecurity、Java Web Token、Swagger2、WebSocket、java web token、elasticSearch7、SpringCloud、Nacos、OpenFeign

数据库:Mysql 8

辅助技术:Redis、docker、Linux centos7、Nginx 1.16、Git(Gitee)、Maven、canal1.1.5、canal Adapter

辅助工具:VMware workstation 12、RedisDesktopManager、Navicat for MySQL、SourceTree、Postman

系统平台:首页房源展示服务平台(display)、管理员管理服务平台(manager)、系统技术支持服务平台(support)

项目中除了某些Mybatis-plus形成的sql没有去做explain(一时疏忽),其他的SQL查询需要使用索引的均使用了索引或者修改不合理的SQL语段来增加查询速度。避免造成大表的全表扫描、filesort等不利情况。使用索引的SQL查询类型最低是ref。

项目功能

最终迭代

有选项卡

1、点击左侧子菜单能动态添加选项卡,重复点击子菜单,选项卡数组定位到对应的选项卡,不会重复添加。

2、已点击过的子菜单随着选项卡数组中的选项卡选择的变化而展示高亮。

3、删除某个选项卡后显示内容页面会切换到前一个选项卡的页面内容。

4、在管理界面中手动刷新当前页面或在地址栏进入某路由后,选项卡保留显示此路由下的页面,并且选项卡数组含有此页面的选项卡,并显示此路由页面。

5、首页选项卡不能被删除。

某些页面的数据表内容某字段的状态样式换成element 的tag。

首页

项目首页,有图片滚动,可进入登录注册。房源设计成一个商品,进入此页会自动搜索房源这个商品的内容,动态展示搜索条件内容、一页20个展示房源数据。某些css参照淘宝。

底部

搜索条件内容来自表:

首页分页查询房源

点击

结果:

首页条件分页查询房源

点击:

结果:

点击:

结果:条件自动变化

点击:

点击:

结果:

点击:

结果:

首页查看房源信息

在新的窗口打开展示房源信息:

elasticSearch辅助首页搜索

如图:默认搜索房源,其次是房东

 

 搜索展示在首页的房源里的title有含有“的”这个字

 点击搜索后:

 

按房东账号搜索房源

 

进入登录页面

登录

界面

(最终迭代)

点击左上标题可回首页

表单自检:

账号不存在登录:

密码错误登录(这里没有修改账号,导致提示还是不存在,懒得再启动项目截图了):

(再次最终迭代)

登录及游客注册登录添加了验证码,尽量确保人为操作。

 

 登录流程(也是SpringSecurity登录流程)

↓前端 http://localhost:8081/

1、进入登录界面,前端js先检查记录用户登录状态的token是否存在(localStorage.getItem("token")),如果token不存在,“等待”用户进行登录。

2、用户输入账号密码进行登录

↓后端 http://localhost:8082/

1、解决跨域访问(每个请求都是要解决跨域问题的,以下介绍的每个请求都不再表述

2、进入SpringSecurity过滤链

2.1、进入自定义的MyOncePerRequestFilter!

检查请求头是否包含token信息(自然没有token,就没有被过滤)

2.2、进入自定义的UsernamePasswordAuthenticationFilter

UsernamePasswordAuthenticationFilter继承AbstractAuthenticationProcessingFilter

public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
/**
 * 重写UsernamePasswordAuthenticationFilter过滤器
 */
public class MyUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

2.2.1、根据输入流方式获取request,从而根据获取请求字段的值,也就是获取提交的username和password。

2.2.2、根据username和password查询数据库表是否有这个用户,如果有就创建一个安全的token,把账号和密码装入进去,再经过getAuthenticationManager().authenticate(authRequest);生成一个身份认证Authentication(登录成功)

身份认证管理器再给这用户授权的时候由身份认证提供器提供!

系统获取到username和password还不够,还需要经过UserDetailsService和密码执行器!

UserDetailsService的用途主要是:查询该用户拥有的角色名称列表(角色名称是这个类SimpleGrantedAuthority),通过用户名、密码、角色名称列表等信息实例化一个UserDetails,最终得到一个Authentication,把存入SecurityContextHolder下的SecurityContext的Authentication中!

看看Authentication是一个接口,也就是说UsernamePasswordAuthenticationToken是它的一个实现类。

public interface Authentication extends Principal, Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();

    Object getCredentials();

    Object getDetails();

    Object getPrincipal();

    boolean isAuthenticated();

    void setAuthenticated(boolean var1) throws IllegalArgumentException;
}
public abstract class AbstractAuthenticationToken implements Authentication, CredentialsContainer {
    private final Collection<GrantedAuthority> authorities;
    private Object details;
    private boolean authenticated = false;

    public AbstractAuthenticationToken(Collection<? extends GrantedAuthority> authorities) {
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
    private static final long serialVersionUID = 540L;
    private final Object principal;
    private Object credentials;

    public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {

看看官网怎么说的,包含关系!而Authentication由AuthenticationManager管理的。

登录是采用这种方式的:

2.3、如果登录失败会走MyAuthenticationEntryPoint这个自定义的类!

还需要把错误信息返回给前端:

2.4如果登录成功执行AuthenticationSuccessHandler

/**
 * 登录成功操作
 */
@Component
public class MyAuthenticationSuccessHandler extends JSONAuthentication implements AuthenticationSuccessHandler {
    private static final Logger logger = LoggerFactory.getLogger(MyAuthenticationSuccessHandler.class);

    @Autowired
    private RedisToken redisToken;
    @Autowired
    private FrontendMenuService frontendMenuService;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request,
                                        HttpServletResponse response,
                                        Authentication authentication) throws IOException, ServletException {

在重写的onAuthenticationSuccess方法中进行一些自定义的业务

比如生成jwt、用户能访问的前端菜单数据、用户的权限、用户名称等等,我这里生成了两个jwt token,一个是设置15分钟,返回给前端的,用户下一次访问就带上它!另一个token设置时间一周,把它存到redis中,设置其过期时间为1周。至于作用看后面登录后的再一次访问服务器。

↓前端

登录成功后前端js需要保存jwt token、用户能访问的前端菜单数据、用户的权限、用户名称!并把当前路由转到登录后的界面!

例如localStorage.setItem("menus",JSON.stringify(menus))

(最终迭代)

页头数据查询(讲解SpringSecurity的身份认证过程)

↓前端 

登录成功进入主页面后,js立即查找进行查找localStorage是否有token,如果没有则强制退回到登录界面,如果有token,js立即向服务器发送携带有token的请求访问有哪些业务需要进行计算的数目,并且查询用户的头像名称并显示。

↓后端 

1、进入SpringSecurity过滤链

1.2、进入自定义的MyOncePerRequestFilter!

/**
 * 请求API的拦截器
 *
 */
@Component
public class MyOncePerRequestFilter extends OncePerRequestFilter {

MyOncePerRequestFilter获取用户请求的请求头携带的token,对他进行jwt的解析,获取用户名后,对redis的缓存中是否存在其名称的缓存,如果缓存不存在就真正的过期,需要强制登录!存在则刷新一个有效期为15分钟的token返回给前端(返回头方式),前端替换掉原来的token!

如果当前用户不存在SecurityContextHolder的Authentication中,则采用类似登录成功后的操作:把完整的UsernamePasswordAuthenticationToken存入SecurityContextHolder中。

1.3、进入自定义动态鉴权类DynamicPermission

根据用户名获取这个用户能访问的后端接口类对象的数组,然后挨个进行匹配,同时请求的方法还需要匹配,如果匹配成功,则用户能正常访问该接口,反之抛出异常,无法进行访问!

package com.myhome.myhomemanageradmin.configuration.auth;

import com.baomidou.mybatisplus.extension.api.R;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 权限校验处理器
 */
@Component
public class MyAccessDeniedHandler extends JSONAuthentication implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request,
                       HttpServletResponse response,
                       AccessDeniedException e) throws IOException, ServletException {
        R<String> data = R.failed("权限不足:"+e.getMessage());
        //输出
        this.WriteJSON(request, response, data);
    }
}

结束

自动获取新token:

之前说过如果请求的token15分钟过期后会自动获得服务器返回的新的具有15分钟有效期的token!

查看第一个请求的请求头

返回了一个新的token

查看第二个接口访问所使用的token

毫无疑问是新的token没错,换新token之间是不需要用户参与进去的!

页头信息提示

进入首页后查询要提示的信息总数,如果没有要处理的信息,则不显示“0”,当用户点击需要处理信息的选项时相应的提示数目永久消失,若用户进行其他操作此提示信息数不会消失。

登录后进入系统时:

 点击【计算房租】后

查看个人信息

从这里开始访问的请求为倒叙显示

鼠标悬停【我的】

点击

(最终迭代)

修改密码

修改密码的时候需要提交当前的密码和新密码,请求到后端后,后端会把当前的密码进行加密,加密的工具类或者说是算法是SpringSecurity提供的,也就是之前看到的图的PasswordEncoder,我选的是:

BCryptPasswordEncoder:
package org.springframework.security.crypto.bcrypt;

import java.security.SecureRandom;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.crypto.password.PasswordEncoder;

public class BCryptPasswordEncoder implements PasswordEncoder {
    private Pattern BCRYPT_PATTERN;
    private final Log logger;
    private final int strength;
    private final BCryptPasswordEncoder.BCryptVersion version;
    private final SecureRandom random;

为了方便查看使用了哪个encoder我生成了自己实现的bean,代码都没动过,直接实现父类的方法。

package com.myhome.myhomemanageradmin.components;

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Component;

@Component
public class BCryptPasswordEncoderUtil extends BCryptPasswordEncoder {
    @Override
    public String encode(CharSequence rawPassword) {
        return super.encode(rawPassword);
    }

    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) {

        return super.matches(rawPassword,encodedPassword);
    }
}

当用户需要修改密码的时候,服务器把童虎当前的密码传到这里进行加密操作,把进行加密操作的密码在对数据库中用户的密码进行比较,如果一致,则把新的密码传入这里,把新密码进行加密后,更新数据,返回更新成功的信息给前端,反之则抛出一个自定义的运行期异常,告诉用户无法修改密码。

鼠标悬停【我的】

(最终迭代)

进入

输入相关的数据后点击【修改】

修改成功后,前端js把localStorage中的所有数据都清空,并且把路由转到登录界面

数据库用户密码展示

使用新密码再次登录检测是否更换密码成功

成功!

更换头像

选择图片

点击确定后,js会立即向后台发送数据的

因为这个组件是自动发送请求的,请求的路径什么的都写到组件标签的属性上:

因为vue.js会缓存某些方法的,比如你更换头像后再进行加载头像方法是不能奏效的,因此在用户提交图片后还是要重新加载当前页面,即加载当前页面所有方法(main是主页面,包含页头,而路由所转换的页面仅是页面的右部分区域)。

服务器收到用户的请求后,查询数据库这个用户是否之前修改过头像,如果没修改过,则新增一条数据,记录该用户的名称及图片名称(图片名称是UUID,包含图片类型),反之需要修改数据库的相关数据,比如说图片的名称,同时把之前的头像图片从磁盘中删除!

修改成功:

更新

(最终迭代)

(再次最终迭代)

改为横向

消息中心

在主页进入消息中心

点击

结果

在新的页面打开

功能要点

1.默认与聊天列表第一个聊天对象聊天,并搜索与其聊天的记录

2.聊天记录不是每一条信息都显示发送时间

3.会搜索所有的聊天列表中聊天对象头像

4.如果用户滚动到聊天列表的底部时继续加载往后的聊天列表(有的话)

5.如果用户滚动到聊天内容顶部时继续加载往后的聊天记录(有的话)

6.发送消息后,发送的聊天信息与聊天对象账号一致的聊天列表Item的最后一条信息(比如说,如下图,我现在对13631170754发送一条“你好”的信息,聊天列表对应此对象的Item的最后一条信息“12”变成“你好”)

7.发送消息后,如果此前没有与此用户聊过,则添加相关信息,否则直接添加聊天信息数据

8.点击不同的聊天对象时,聊天列表的底部背景颜色样式要修改回来,之前点击的聊天对象Item底部换回默认颜色,而新点击的聊天对象Item变成选中的颜色

9.聊天内容在适当的时候会显示“上次阅读到此”的DIV

10.在其他位置进入信息中心,记录此聊天对象,如果记录的对象以前没有聊过,则会在聊天列表表的第一个位置记录,否则在聊天列表表的第一个位置记录后循环把与此聊天对象过滤,避免出现聊天列表出现相同的聊天对象,当然,完成此功能的同时其他的功能都能正常运作。

11.任何用户都不能与自己聊天

12.任何用户都不能看到不属于自己的私信信息

13.左侧的聊天列表在用户滚动到底部时则继续加载下一页的数据,如果下一页数据为空,则不能再次查询下下一页数据;如果有数据则添加聊天列表数组中。

14.显示右侧的聊天信息框时默认搜索第一页数据(没有聊过的聊天对象除外),如果聊天信息框有滚动条,当用户滚动到顶部时,加载下一页数据,如果有数据则在第一页的数据的基础上加载新的数据,反之提示“没有更多的消息了”的DIV

15.当有其他用户发送即时消息给当前用户时,弹出相关提示信息

16.当有其他用户发送即时消息给当前用户时,如果是当前聊天对象发送信息,则直接在聊天content末尾添加聊天内容,并判断是否显示日期时间,同时发送请求修改此信息数据为已读状态,聊天Item未读数为0,此聊天列表的Item的lastContent替换成信息内容

17.当有其他用户发送即时消息给当前用户时,如果不是当前聊天对象发送信息,此用户已经加载进此时的聊天列表里,则修改此聊天Item未读数+1,并修改lastContent替换成信息内容

18.当有其他用户发送即时消息给当前用户时,如果此用户此前已聊过天,但未加载进此时的聊天列表里,则添加此聊天列表的Item并把它放在聊天列表第一个位置,同时相关信息确保无误。如果用户此时查询下一页或其他页数据有此用户时,则跳过此用户数据,添加未添加的其他聊天对象

19.当有其他用户发送即时消息给当前用户时,如果此用户此前从来没有聊过天,则添加此聊天列表的Item并把它放在聊天列表第一个位置,同时相关信息确保无误

20.当有其他用户发送即时消息时,都要把发送消息的用户的聊天Item放在首位

21.不管离线或即时的消息都要保存到数据库,同时保证具体每一条的数据的状态或时间在必要的时候会修改

21.其他

超级管理员与管理员聊天时进入消息中心

点击与13631170268聊天(此前已聊天)

 结果(可与上上图对照聊天列表位置变化) 

 点击与13565478932聊天(此前未聊天)

  结果(可与上上上上图对照聊天列表位置变化) 

发送消息

输入内容后点击发送

发送255个字符的消息(部分CSS已与上面不同,修改了页面展示bug)

管理员与租客聊天进入消息中心

 结果 

房东管理员查看消息中心

点击

结果

有一条未读信息“收到了”,提示“上次阅读到此”DIV,然后自己发送了“好的”信息

超级管理员与注册用户聊天进入消息中心

点击

结果

租客在首页进入消息中心

 点击

结果

在首页选择房东进入消息中心

(与房东聊天,系统用户角色操作为例)

首页

选择一个房源,点击

 进入,点击按钮

 结果

 聊天

注册用户在首页进入消息中心

点击

点击

滚动到底部搜索聊天列表数据

如图

滚动到底部继续加载数据

查看新加载的数据

滚动到顶部时加载下一页聊天数据

如图

 滚动到底

往上滚动显示无数据

往上滚动加载数据

如图

 到底

 往上搜索下一页的聊天数据并加载出来

再次到顶

避免情况

特殊情况下导致不能继续加载下一页数据,如用户在较短的时间内发送多次少量数据导致没有出现滚动条,从而导致无法继续查找下一页数据。

解决方法就是多添加搜索页条数

WebSocket发送消息

测试准备:

使用不同种类的浏览器登录系统账号,进入【消息中心】

超级(房东)管理员进入(谷歌浏览器):

 其他角色用户进入(火狐浏览器):

说明:双方进入此聊天页面,说明都已经建立了websocket连接

发送即时消息/当前的聊天对象发送信息给自己

输入要发送的消息:

 点击后:

当前:

 接受方(当前的聊天对象发送信息给自己):

 非当前的聊天对象发送信息给自己,同时此前聊过天

选择数据:

我在超级管理员消息中心的聊天列表中选择第二页第一条数据的账号对超级管理员发送即时消息

我们此时刷新当前页面,让数据加载第一页数据

结果:

 火狐浏览器登录此账号并进入聊天中心

 发送消息给超级管理员

点击发送:

当前结果:

超级管理员接收端结果:

加载第二页数据查看是否存在与此发送消息的用户账号

滚动到底部加载第二页数据:

没有刚刚发送消息的账号,说明聊天列表在加载数据时避免了重复出现同一账号的聊天对象

点击刚刚发送消息的聊天列表Item:

 非当前的聊天对象发送信息给自己,此前从未聊过天

选择一个从来没有与超级管理员聊过天的账号发送即时消息给超级管理员

点击此按钮与超级(房东)管理员聊天

 进入聊天中心:

 发送消息:

 结果:

 接收方:

 点击:

 多用户发送消息给自己

 当前页面

 多用户发送消息:

默认选择的对象没有变化,新消息都是通过websocket发送过来的

 各自处理:

 

添加系统(房东)管理员

点击

输入必要的数据,
1、添加栏目
2、点击后删除当前行的栏目
3、提交数据,在提交数据之前会进行数据合法性校验

最多只能同时添加4条数据,多则提示用户!

结果:
如果提交的数据的用户账号在数据库表中已经创建,则不会新增此条数据的。

用户的默认密码是123456!

查看系统(房东)管理员

点击

可以进行条件和分页查询

查看系统所有用户(超级管理员)

点击

其他页:

条件查询:

如果查询结束时间在查询开始时间之前则js会提示用户相关错误提示信息。

点击:

查询过后,查询的条件保持不变;每次查询都是查询符合条件的第一页的数据,比如说用户当前浏览的是第二页,就把查询页设置成第一页后再依据条件查询,保证不出现错误!

(最终迭代)

日志管理

点击

条件查询:

查询系统运行接口后出现异常的日志

(最终迭代)

(再次最终迭代)

分日志平台

系统关键字管理(敏感词)

进入

 

添加关键字

点击

 输入点击

 结果

匹配搜索关键字

输入点击

 结果

修改关键字

点击

 修改并点击

 结果

因为保留了搜索条件,所以没有数据结果

去掉搜索条件并点击搜索按钮

删除关键字

点击

结果

提交关键字时

输入关键字

 菜单图标管理

点击

查询菜单图标

点击

 结果

添加菜单图标

 点击:

 结果:

修改菜单图标

点击:

修改数据:

结果:

展示为空是因为element没有此名称的图标,如果要用自己的要多做一些事.

删除菜单图标

点击

 点击确定按钮

注册游客管理

点击

查询指定账号游客

输入

 结果

权限管理  前端菜单权限分配管理

点击

房东管理员查看租客能访问的前端菜单

现任租客:

前任租客:

房东管理员查看租客能访问的后端菜单

现任租客:

前任租客:

查询系统所有前端菜单(超级管理员)

只有系统超级管理员才能查询所有的前端菜单,前端菜单分为基础菜单和非基础菜单。基础菜单仅超级管理员能查看,普通房东管理员只能查看非基础菜单。

(最终迭代)

添加了图标

查询某角色可浏览的系统前端菜单(超级管理员)

点击【超级管理员】

1、超级管理员显示是 1
2、获取用户可访问的前端二级菜单数据,为什么只是获取二级菜单?如果把父级菜单也获取,前端ui组件会把该父菜单下的所有子菜单都勾选了,如果用户不能访问该父菜单下的某些子菜单也显示可以访问!会造成错误!

3、返回数据的时候前端还需要把子菜单的数据都存起来(子菜单的id),如果用户添加修改用户可访问的菜单项后提交修改请求,js获取所有勾选的子菜单并与之前保存的子菜单的id数组进行比较,如果发现之前的子菜单数组没有的子菜单id,则当作要添加的子菜单。用户取消子菜单的勾选状态是被监听的,也是需要把取消子菜单的取消勾选状态的子菜单记录下来,也是要提交的!

(快懵了吗?用户获取后端可访问的子菜单数据后把这些数据的id传给ui组件,让ui组件把对应的菜单勾选起来,之后把已勾选的菜单和没有勾选的菜单记录起来,分别储存在一个数组中,用户每操作某子菜单的勾选状态都进行数组的增加或删除操作,提交数据请求的时候都发给后端!)

点击【房东管理员】

树的勾选状态随角色的不同动态改变

点击【现任租客】

添加一级菜单(超级管理员)

点击

添加一级菜单后,菜单的勾选状态没有改变,而且添加成功后再一次查询所有的菜单并显示出来!

(最终迭代)

数据要么从数据库加载,要么从redis拿。

 编辑一级菜单(超级管理员)

点击

显示当前菜单的详细信息(几乎所有的系统的编辑或添加操作,显示的数据不经过服务器再次查询得出!!!而是直接获取当前选中的菜单的信息)

修改相关数据(前端URL只是一个提示的作用)

先提交数据,后查询数据

put请求

get请求

添加二级菜单(超级管理员)

修改数据

因为是添加数据,则使用post请求,另一个请求当然是查询菜单列表了

(最终迭代)

为某角色操作可访问的前端菜单

超级管理员为例

提交后再次查询

这里我把日志的勾选状态取消了,等会测试的时候看看能不能访问得到

目前控制台是没有错误信息,证明js没有错误

测试

因为登录的时候,我把用户能访问的所有菜单都保存在localStorage了,需要退出系统才能显示修改的,如果需要改这个缺陷的话我觉得还是简单的就没改ta。

(注意:最终迭代已经把这个问题解决了,利用vue的store保存的状态属性,当超级管理员修改了角色能访问的前端菜单成功后,vue的js代码把状态改为“需要查询数据库”,前端再次经路由时查询数据库,再动态绑定路由对应的页面数据,这样就能在用户分配角色前端菜单后能立刻看到效果,不用再退出系统再登录系统了)

删除菜单(超级管理员)

删除菜单,删除菜单的同时,需要把数据库表中该菜单的记录和依赖数据(包括子菜单的记录和依赖数据Role)都要删除!

编辑菜单

一级菜单

 二级菜单

锁定用户(超级管理员)

点击【锁定】

尝试锁定用户后使用此用户的账号和密码登录

登录失败,但是不知为什么我抛出的异常没有被显示出来,就很纳闷,正常来说是提示密码错误的。(文章最后已解决)

这个实现是在登录,验证账号和密码成功的时候同时把该用户的锁定状态、权限创建于UserDetails,既然UserDetails存入Authentication,则表明它不能登录。

(最终迭代)

取消锁定用户(超级管理员)

回到登录页面:

能成功登录了

(最终迭代)

(再次最终迭代)

管理员重置用户密码

默认密码是“123456” 

点击

 结果

 确认最后

权限管理  接口API权限分配管理

查询列表(超级管理员)

查询某角色可浏览的系统接口API

超级管理员

房东管理员

添加接口API一级菜单(超级管理员)

更新:

添加接口API二级菜单(超级管理员)

这里写的Api就不能乱写了,是真的会用到的,服务器的接口就是在这里添加修改删除的!

更新:

修改用户能访问的API接口(超级管理员)

超级管理员

测试

编辑用户能访问的API接口信息(超级管理员)

更新:

删除父Api接口(超级管理员)

同时删除其子接口、所有角色用户访问此接口的权限数据!

租客入住

进入此页面后会根据用户名查询用户所具有的房源单位数据及中国56个民族数据

填写资料:

用户选择了房源单位后,js发送请求以获得该房源下所有未入住的房间号码!

当前系统服务器使用的邮箱是913899287@qq.com,admin账号用户使用的邮箱、租客的邮箱都是913899287@qq.com!因此,在租客入住时,系统会发送2条邮件信息,一封发送给admin账号的邮箱,一封发送给租客的邮箱!

邮箱信息

查看PC端QQ邮箱:

房东管理员角色:

这里正常来说还要标出是哪个房源单位的信息,懒得改了。

租客角色:

会把租客入住的房间某些信息罗列出来。

这里正常来说还要标出服务器网站地址的,有必要才会改。

如果房东和租客的微信绑定了QQ邮箱,则房东和租客的微信会显示收到租房的信息的。

查询租客

(最终迭代)

普通查询

条件查询

查询某个租客的详细信息

编辑某个租客的详细信息

编辑数据

查看所有房间

查看某个房间的详细信息

空房:

如果房间没有上传过图片会这样显示:

查看房间室内图片:

点击:

还可以放大缩小左右旋转

其他房间(最多上传8张图片,最多显示8张图片)

条件查询某个房间的详细信息

无数据:

这里的创建日期指的是房间的创建日期,不是租客入住的日期

编辑某个房间的详细信息(添加房间图片)

加个图片吧

最多添加8张图片

编辑房间信息查看室内图片

没有图片显示:

有图片显示:

删除房间室内图片

添加房间

路由页面跳转

最多可以同时添加3个房间

填写信息,如果该房源下已创建了该房间是不能添加成功的

添加成功略

添加房间(模板方式)

下载模板

下载成功

打开模板

(文件模板最终迭代内容)sheet1

有些字段是有数据有效性的

sheet2

填写数据

导入模板

因为添加的房间号在选择的房源单位已经存在了,所以没有进行添加。

我之前导入输出:

删除某个房间

点击

结果:

只能删除未入住的

删除后手动查询C303:

查看房间历史入住

每月房租计算

系统自动地查询哪些房间需要进行计算房租

把新的水表和电表数填上,如果【房间应计算房租的日期】在上次计算房租时间之前会提示用户!【房间应计算房租的日期】比上次计算房租时间不大于28天,还是会提示错误的。而且当前的水表和电表数不能比上次记录的水表和电表数小!

成功:

房东操作成功后,系统会自动为房东及该房客发送相关的邮件!

房东管理员:

租客:

查询未确认收取房租的信息

确认收取房租

确认收取房间后,如果这租客还是需要进行房租计算的化,还是能在【每日房租计算】中找到

查询收取房租总览

条件查询收取房租总览

修改收取房租某数据

这功能防止房东输入了错误的水电表度数计算错房租。如果修改了水电表数据,那房间当前的水电表数据也要一致更改。这里的修改就不展示了。

退房计算房租

进入此页面立刻加载该用户所拥有的房源单位

选择房源单位后,立刻加载所选择的房源单位下的已入住的房间,选择要退房的房间号后立即显示提示数据信息

如果该房间出现不允许退房操作的情况下,会拒绝该房间的进行退房操作。

如果本次的水电表度数比上次记录低则进行错误提示

此时房东管理员和租客会收到一条邮件

房东管理员:

租客:

查看T103房间是否无人居住:

查看租客信息:

查看房间历史入住信息:

查看系统所有用户信息(超级管理员)

(最后迭代)

查看邮件发送

(最后迭代)

条件查询邮件发送

查询房源单位

条件查询房源单位

添加房源单位

编辑房源单位信息

删除房源单位

我们查看一下添加房间时显示的房源单位:

退出系统(SpringSecurity登出流程)

package com.myhome.myhomemanageradmin.configuration.auth;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 退出Handler
 */
@Component
public class MyLogoutHandler extends JSONAuthentication implements LogoutHandler {
    private final static Logger logger = LoggerFactory.getLogger(MyLogoutHandler.class);
    //private String header = "Authorization";
    @Override
    public void logout(HttpServletRequest request,
                       HttpServletResponse response,
                       Authentication authentication) {

        String headerToken = request.getHeader("Authorization");
        logger.info("logout header Token = " + headerToken);
        logger.info("logout request getMethod = " + request.getMethod());
        //
        if (!StringUtils.isEmpty(headerToken)) {
            //postMan测试时,自动假如的前缀,要去掉。
            String token = headerToken.replace("Bearer", "").trim();
            logger.info("authentication = " + authentication);
            SecurityContextHolder.clearContext();
        }
    }
}
package com.myhome.myhomemanageradmin.configuration.auth;

import com.baomidou.mybatisplus.extension.api.R;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 成功退出处理器
 */
@Component
public class MyLogoutSuccessHandler extends JSONAuthentication implements LogoutSuccessHandler {
    private final static Logger logger = LoggerFactory.getLogger(MyLogoutSuccessHandler.class);
    @Override
    public void onLogoutSuccess(HttpServletRequest request,
                                HttpServletResponse response,
                                Authentication authentication) throws IOException, ServletException {
        logger.info("退出成功!");

        R<String> data = R.ok("退出成功!");

        super.WriteJSON(request,response,data);
    }
}

租客登录系统

(最终迭代)

租客查看个人信息

租客修改个人密码

现任租客查看房间信息

(最终迭代)

查看图片:

租客查看已收到的邮件

条件查询

(最终迭代)

租客查看交纳房租

(最终迭代)

游客注册账号

游客注册的账号不能进入管理系统,只用作预约。

点击:

进入:

表单自检:

填写数据:

点击注册按钮:

游客登录账号

点击游客登录按钮

输入不存在的账号:

输入的密码不正确:

登录成功会回到首页:

游客预约看房

此功能使用了线程池技术,可以最大限度地处理预约任务。

后端部分代码:

配置类:

@Configuration
public class AsyncTaskConfig implements AsyncConfigurer {
    private int maxPoolSize = Runtime.getRuntime().availableProcessors();

    @Override
    @Bean
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor threadPool = new ThreadPoolTaskExecutor();

控制器类某方法:

    @ApiOperation(value = "首页预约看房接口")
    @PostMapping("/house/order")
    public Result orderLookHouse(
            @RequestBody IndexOrderHouseDto indexOrderHouseDto){
        taskOrderHouseService.orderLookHouse(javaMailSender,indexOrderHouseDto,indexOrderHouseTableMapper, housesMapper,housesResourcesMapper, managerInfoMapper);
        return Result.ok();
    }

服务类:

@Service
@Scope("prototype")
public class AsyncTaskOrderHouseServiceImpl {
    private final String emailSubject = "房东出租房管理系统信息";
    private final String emailFrom = "oujianbin200@163.com";
    private final String spanString = "<span style='color:#67C23A;font-weight: bold;'>";

    @Transactional
    @Async
    public void orderLookHouse(JavaMailSenderImpl javaMailSender, IndexOrderHouseDto indexOrderHouseDto, IndexOrderHouseTableMapper indexOrderHouseTableMapper, HousesMapper housesMapper, HousesResourcesMapper housesResourcesMapper, ManagerInfoMapper managerInfoMapper){
        //先查询该号码是否已经预约了本房源的单位的房间了,不能重复预约
        QueryWrapper<IndexOrderHouseTable> queryWrapper = new QueryWrapper<>();

房源信息:

点击:

结果:

无论用户是否是重复预约,服务器出现自定义的异常,前端都显示预约成功。

房东收到的邮件信息:

(最终迭代)

在预约前添加了验证码,尽量确保预约是人为操作。

 

房东管理员查看预约房间信息

点击:

房东查阅预约房间信息

点击:

点击:

结果:

项目主线程名字:

http-.....

线程池线程名字:

Simple...

服务器每日凌晨0点定时查询需要计算房租的任务

此功能用了线程池技术,单线程的线程池。

结果测试的时候没有按上图的时间来,并不是整点):

这里因为我debug测试发送邮件的逻辑代码是否正确的缘故,导致一些发送邮件的时间间隔大!

某个房东:admin

因为查询需要计算房租的数据需要查询两个SQL语句,把第二个查询的结果List加到第一个查询结果的末尾(需要查询 用户已交过1次以上房租和用户是第1次交房租 的数据组合)。因为代码执行逻辑是:如果遍历的当前 n (n>=2)数据的账号不等于上个遍历的账号就会直接发送Email,不再判断房源单位是否相同!

如果房源单位相同,字符串不再添加房源单位名称,只添加房间名称!

某个房东:13631111111

如果房源单位不相同,字符串要添加房源单位名称,还要添加房间名称!

租客:

因为此功能对于租客来说仅仅是提示功能,所以在给租客发送邮件的时候没有提供详细信息

房东管理员某些功能查看

这里只是展示其他房东能查看的只是属于自己能查看的信息,功能跟上面描述的一致

Swagger2

导航栏输入地址:http://localhost:8082/swagger-ui.html访问swagger

管理员后台:

(最终迭代)

代码中添加注解设置查询字段是否必要,查询

查询:请求的时候好像不能添加token,因此访问失败(感觉使用swagger测试的时候,要把验证去除。或者在服务器验证代码未设计完成之前使用swagger进行测试)

租客的登录后台的接口:

疑问:问什么我把swagger的ui等资源都放行不拦截,为什么还会报一个错误呢?

Nginx配置

下载好Nginx软件,这里是window版的,就是个“玩具”,不过可以学习一下什么是“反向代理”、“负载均衡”

1.16版本,对配置文件进行修改

配置文件:

权重越高,优先级就越高。

如果请求有4个,则3个进入权重为3的,1个进入权重为1的.

我开启了两个后台,一个端口号是8084:

另一个是8082:

vue前端所有访问后端的接口ip都换成127.0.0.1,不填写任何的端口号,让Nginx决定!

例如:

测试:

先查看nginx有没有问题,结果显然是没问题的!

向服务器请求一部分接口,看控制台输出信息:

IDEA:

cmd:

证明都有输入信息,则负载均衡没有问题:

比如说上图cmd中有信息是【共缴纳223.5元】(第五行前部分),把它复制到IDEA的控制台进行查询:

并没有查询到,这样的话就真的有两个服务器接收前端请求并不互相干扰。

SourceTree

提交代码到gitee

先暂存所有文件

提交到本地仓库:

推送:

选择分支:

gitee:

RedisDesktopManager

用户登录有效期7天的token数据。如果redis没有用户登录的数据,那么用户将强制登录!

Nacos

服务注册、发现、配置

补充说明

隶属于用户个人的页面或者功能必须进行权限控制校验。
说明:防止没有做水平权限校验就可随意访问、修改、删除别人的数据,比如查看他人的私信内容——阿里巴巴开发手册

本系统做基础开发后,根据阿里巴巴开发手册的内容,把要对项目修改的地方能修改的都修改了。

  • 6
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值