1、项目介绍
系统功能
登录、修改密码、登出
(1)首页
(1.1)数据统计:小区人员统计对比图,占比图
(2)物业管理
(2.1)小区管理:小区数据的增删改查
(2.1.1)摄像头管理:小区摄像头数据的增删改查
(2.2)居民管理:居民数据的增删改查,居民人脸采集,Excel 数据导入,Excel 数据导出
(2.3)小区地图:所有小区在地图上的分布情况
(3)门禁管理
(3.1) 人脸识别:居民出入小区人脸识别功能
(3.2)出入记录:所有居民出入小区的人脸识别记录查询
(3.3)访客登记:访客数据的增删改查,进入登记,离开登记
(4)系统管理
(4.1)用户管理:用户数据的增删改查,给用户赋予角色设置不同的管理权限
(4.2)角色管理:角色数据的增删改查,给角色赋予权限
(4.3)菜单管理:菜单数据的增删改查,不同角色可设置不同的菜单权限
(4.4)日志管理:实时记录系统所有操作的日志,为排查问题提供依据
技术栈描述
前端:Vue+ElementUI+BaiduMap+ECharts
后端:SpringBoot+SpringMVC+MyBatisPlus+Spring Data Redis+ Swagger
第三方服务:人脸识别,腾讯AI接口(后端)
BaiduMap,ECharts(前端)
数据库:MySQL 数据库存储、Redis 缓存
其他技术:POI Excel 文件导入导出、Swagger 接口文档管理、JWT 登录认证、Spring AOP 日志管理、前端代理解决跨域问题
2、登录模块
数据表设计
用户信息表(user)
用于存储登录用户的登录信息
实现思路
①获取验证码:首先借助UUID或者其它工具生成一个符合要求的验证码;然后存入到缓存数据库redis当中,并设置超时时间;最后将验证码返回给前端
②登录验证:第一步在redis中查询验证码,验证验证码是否有效和正常 ;第二步验证用户名;第三步验证密码(JWT加密,生成token);第四步验证用户状态是否正常;第五步创建token,生成令牌,将token存入到redis中;第六步获取token的过期时间,将token和过期时间返回给前端,允许用户登录并访问首页
获取验证码
①redis配置,redis用于存储验证码和登录生成的token
spring:datasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/community?useUnicode=true&characterEncoding=utf-8username: rootpassword: 123456jackson:time-zone: GMT+8date-format: yyyy-MM-dd HH:mm:ssredis:open: truedatabase: 2host: localhostport: 6379
②导入验证码的依赖
<dependency><groupId>com.github.whvcse</groupId><artifactId>easy-captcha</artifactId><version>1.6.2</version>
</dependency><!--工具包-->
<dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.2.4</version>
</dependency>
③获取生成验证码
@Autowiredprivate RedisTemplate redisTemplate;/** * 获取生成验证码* @return*/@GetMapping("/captcha")public Result getCaptcha(){//1、借助UUID或者其它工具生成一个符合要求的验证码//2、存入到缓存数据库redis当中,并设置超时时间//3、验证码返回给前端//利用生成验证码图片的工具类,指定宽和高,以及生成的验证码数量4SpecCaptcha specCaptcha = new SpecCaptcha(130, 48, 4);//将生成的验证码图片转成小写字符串String code = specCaptcha.text().toLowerCase();//通过工具类产生一个UUID值,当成验证码信息存储在redis数据库中的主键String uuid = IdUtil.simpleUUID();//存入redis并设置过期时间为2分钟this.redisTemplate.opsForValue().set(uuid, code, 120, TimeUnit.SECONDS);Map<String, String> map = new HashMap<String, String>(3);map.put("uuid", uuid);map.put("code", code);//将验证码图片转成base64的图片信息,方便前端将图片解析进行显示map.put("captcha", specCaptcha.toBase64());//响应200,就是请求到达了控制器return Result.ok().put("data", map);}
登录验证
①jwt配置,jwt用于验证用户的身份和权限,生成token
jwt:secret: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4K67DMlSPXbgG0MPp0gHexpire: 86400000# expire: 10000subject: door
②导入jwt依赖
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version></dependency>
③jwt工具包
package com.qcby.community.util;import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;import java.util.Date;
import java.util.UUID;@ConfigurationProperties(prefix = "jwt")
@Component
public class JwtUtil {private long expire;private String secret;private String subject;/*** 生成token** @param userId* @return*/public String createToken(String userId) {String token = Jwts.builder()//载荷:自定义信息.claim("userId", userId)//载荷:默认信息.setSubject(subject) //令牌主题.setExpiration(new Date(System.currentTimeMillis() + expire)) //过期时间.setId(UUID.randomUUID().toString())//签名哈希.signWith(SignatureAlgorithm.HS256, secret)//组装jwt字符串.compact();return token;}//Token校验public boolean checkToken(String token){if(StringUtils.isEmpty(token)){return false;}try {Jws<Claims> claimsJws = Jwts.parser().setSigningKey(secret).parseClaimsJws(token);} catch (Exception e) {return false;}return true;}public long getExpire() {return expire;}public void setExpire(long expire) {this.expire = expire;}public String getSecret() {return secret;}public void setSecret(String secret) {this.secret = secret;}public String getSubject() {return subject;}public void setSubject(String subject) {this.subject = subject;}
}
④登录请求处理
@Autowiredprivate JwtUtil jwtUtil;/*** 前端发送过来的登录请求* @param loginForm* @param session* @return*/@PostMapping("/login")public Result login(@RequestBody LoginForm loginForm, HttpSession session){//1、查询redis验证,验证码是否有效和正常//2、验证用户名//3、验证密码(JWT加密,生成token)//4、验证用户状态是否正常//5、允许用户登录并访问首页//验证码校验String code = (String) this.redisTemplate.opsForValue().get(loginForm.getUuid());//判断验证码是否有效if(code == null){
// return Result.error("验证码已过期");return Result.ok().put("status", "fail").put("date", "验证码已过期");}//判断验证码是否正确if(!code.equals(loginForm.getCaptcha())){
// return Result.error("验证码错误");return Result.ok().put("status", "fail").put("date", "验证码错误");}//判断用户名是否正确QueryWrapper<User> queryWrapper = new QueryWrapper<>();queryWrapper.eq("username", loginForm.getUsername());User user = this.userService.getOne(queryWrapper);if(user == null){return Result.error("用户名错误");}//判断密码是否正确,密码加密之后比对String password = SecureUtil.sha256(loginForm.getPassword());if(!password.equals(user.getPassword())){return Result.error("密码错误");}//验证用户是否可用if(user.getStatus() == 0) {return Result.error("账号已被锁定,请联系管理员");}//登录成功session.setAttribute("user", user);//创建token,生成令牌String token = this.jwtUtil.createToken(String.valueOf(user.getUserId()));//将token存入redis,每一次访问不需要重复登录,直接验证令牌this.redisTemplate.opsForValue().set("communityuser-"+user.getUserId(), token,jwtUtil.getExpire());Map<String,Object> map = new HashMap<>();map.put("token", token);map.put("expire", jwtUtil.getExpire());
// LogAspect.user = user;return Result.ok().put("data", map);}
界面
3、修改密码
实现思路
①发送更新密码请求,弹出更新密码弹出层
②前端密码格式验证,新旧密码是否一致验证
③修改密码:第一步获取session中的用户信息;第二步将根据用户查询的密码和前端传来的旧密码进行比较,如果相等,将新密码加密后在数据库中更新密码字段信息,密码更新成功,返回。
修改密码请求处理
/*** 修改密码* @return*/@PutMapping("/updatePassword")public Result updatePassword(@RequestBody UpdatePasswordForm updatePasswordForm, HttpSession session){User user = (User)session.getAttribute("user");String pwd = user.getPassword();String password = SecureUtil.sha256(updatePasswordForm.getPassword());if(pwd.equals(password)){String newpassword = SecureUtil.sha256(updatePasswordForm.getNewPassword());user.setPassword(newpassword);if(userService.updateById(user)){return Result.ok().put("status","success");}return Result.error("更新密码失败");}return Result.ok().put("status","passwordError");}
界面
4、登出模块
实现思路
①登出请求:将当前session设置为无效
②将token设置为空
③将router设置为空
④将cookie里面的token信息清空
⑤返回登录页面
登出请求处理
/*** 用户退出* @param session* @return*/@PostMapping("/logout")public Result logOut(HttpSession session){session.invalidate();return Result.ok();}
5、首页
登录成功后,根据当前的登录用户来动态加载对应的菜单列表(路由),显示该用户能访问的菜单;并且同时查看小区数据统计,通过柱状图和饼状图展示
数据表设计
角色表(role)
用于存储用户的角色信息
用户角色关联表(user_role)
用户存储角色和用户的关联信息
菜单表(menu)
用于存储前端展示的菜单信息,其中parent_id代表父级菜单的序号,0代表一级菜单,name代表菜单名称,path代表菜单url,component代表组件路径,icon代表组件图标
角色菜单关联表(role_menu)
用于存储角色拥有的菜单权限信息
小区信息表(community)
用户存储小区信息,其中term_count代表楼栋数量,seq代表排序,lng和lat分别代表经度和纬度
住户表(person)
存储小区的住户信息,其中state代表人脸录入状态,2代表已录入,1代表未录入
加载动态路由
实现思路
①通过session获取用户信息
②根据userId获取角色名称,需要在user_role表和role表中联表查询
③根据userId获取用户的权限菜单:
第一步:根据用户的id查询该用户所对应的角色以及该角色所对应的菜单,需要user_role、user_menu、menu三个表联表查询;
第二步:按照查询出来的菜单进行封装,一个一级菜单的信息封装进一个列表,此菜单下的二级菜单的信息封装进此列表的子列表中,若有三级菜单以此类推进行封装
④返回用户信息、角色名称和用户的权限菜单信息,格式如下
"data": {
"userId": 1,
"username": "admin",
"password": "8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92",
"userType": 1,
"realName": "管理员",
"contact": "",
"mobile": "15679711120",
"status": 1,
"roleIdList": null
},
"roles": "超级管理员",
"routers": [
{
"name": "系统管理",
"path": "/sys",
"hidden": "false",
"redirect": "noRedirect",
"component": "Layout",
"alwaysShow": true,
"meta": {
"title": "系统管理",
"icon": "system"
},
"children": [
{
"name": "管理员管理",
"path": "/user",
"hidden": "false",
"component": "sys/user/index",
"meta": {
"title": "管理员管理",
"icon": "user"
}
}
]
}
]
封装返回的routers信息MenuRouterVO
@Data
public class MenuRouterVO {private String name;private String path;private String component;private String hidden;private String redirect = "noRedirect";private Boolean alwaysShow = true;private MetaVO meta;private List<ChildMenuRouterVO> children;
}@Data
public class ChildMenuRouterVO {private String name;private String path;private String component;private String hidden;private MetaVO meta;
}@Data
public class MetaVO {private String title;private String icon;
}
加载动态路由controller请求
/*** 通过登录的用于加载动态路由* 显示该用户能访问的菜单* @param session* @return*/@GetMapping("/getRouters")public Result getRouters(HttpSession session){//获取用户名称User user = (User)session.getAttribute("user");//获取用户的角色名称String roles = roleMapper.getRoleNameByUserId(user.getUserId());//获取用户的权限菜单List<MenuRouterVO> routers = this.menuService.getMenuRouterByUserId(user.getUserId());return Result.ok().put("data", user).put("roles", roles).put("routers",routers);}
获取用户的角色名称的mapper
//根据userId获取角色名称@Select("SELECT role_name FROM role, user_role where user_role.role_id=role.role_id and user_role.user_id=#{userId}")public String getRoleNameByUserId(Integer userId);
获取用户的菜单信息service
@Autowiredprivate MenuMapper menuMapper;@Overridepublic List<MenuRouterVO> getMenuRouterByUserId(Integer userId) {//1.根据用户的id查询该用所对应的角色以及该角色所对应的菜单List<Menu> menuList = this.menuMapper.getMenusByUserId(userId);//2.创建一个集合List<MenuRouterVO> 最终的集合List<MenuRouterVO> list = new ArrayList<>();//3.遍历该用户所能查看的所有菜单找到一级菜单封装进MenuRouterVOfor (Menu menu : menuList) {//挑选出父级菜单if (menu.getParentId() == 0) {MenuRouterVO menuRouterVO = new MenuRouterVO();//给父级菜单对象赋值,bean实体类的封装工具类,框架提供//将源对象menu中的相同属性赋值给新对象menuRouterVOBeanUtils.copyProperties(menu, menuRouterVO);//再将没有的属性进行赋值MetaVO metaVO = new MetaVO();metaVO.setTitle(menu.getName());metaVO.setIcon(menu.getIcon());menuRouterVO.setMeta(metaVO);//生成childrenInteger menuId = menu.getMenuId();//4.不是一级菜单的继续遍历找到属于哪个一级菜单下挂在该菜单下List<ChildMenuRouterVO> children = new ArrayList<>();for (Menu child : menuList) {if(child.getParentId() == menuId){//5.封装子菜单ChildMenuRouterVO 在放进集合List<ChildMenuRouterVO>ChildMenuRouterVO childVO = new ChildMenuRouterVO();BeanUtils.copyProperties(child, childVO);MetaVO childMetaVO = new MetaVO();childMetaVO.setTitle(child.getName());childMetaVO.setIcon(child.getIcon());childVO.setMeta(childMetaVO);children.add(childVO);}}//6.将子菜单集合挂在MenuRouterVO的children的集合属性下menuRouterVO.setChildren(children);//7.将每一个MenuRouterVO放进大集合list.add(menuRouterVO);}}return list;}
获取用户的菜单信息mapper
@Repository
public interface MenuMapper extends BaseMapper<Menu> {@Select({"select m.menu_id,m.parent_id,m.name,m.path,m.component," +"m.menu_type,m.status,m.icon,m.sort,m.hidden from " +"user_role ur,role_menu rm,menu m where ur.role_id = rm.role_id" +" and rm.menu_id = m.menu_id " +"and ur.user_id = #{userId} order by m.sort"})public List<Menu> getMenusByUserId(Integer userId);
}
查看小区数据统计
实现思路
查询出所有的小区名称,以及每个小区的对应的住户数量,并将小区名称与其对应的住户数量封装成单个list,从小区信息表community和住户表person中联表查询出来,返回数据如下所示:
"data": {
"names": [
"栖海澐颂",
"宸悦国际",
"流星花园二区",
"农学院家属院",
"金达园",
"建发城建·文源府",
"北清云际"
],
"nums": [
5,
3,
1,
2,
4,
2,
1
],
"list":[
{
"name": "栖海澐颂",
value: 5
}
]
echarts安装
cnpm install echarts@4.9.0 --save
前端代码
drawLine(){chart().then(res => {// 基于准备好的dom,初始化echarts实例let myChart = this.$echarts.init(document.getElementById('myChart'))// 绘制图表myChart.setOption({color: ['#3398DB'],title: {text: '智慧社区住户量统计',subtext: '对比图',left: 'center'},tooltip: {trigger: 'axis',axisPointer: {type: 'shadow'}},xAxis: {data: res.data.names},yAxis: {},series: [{name: '住户量',type: 'bar',data: res.data.nums}],animationType: 'scale',animationEasing: 'elasticOut',animationDelay: function (idx) {return Math.random() * 200;}});let myChart2 = this.$echarts.init(document.getElementById('myChart2'))myChart2.setOption({title: {text: '智慧社区住户量统计',subtext: '占比图',left: 'center'},tooltip: {trigger: 'item',formatter: '{a} <br/>{b} : {c} ({d}%)'},visualMap: {show: false,min: 80,max: 600,inRange: {colorLightness: [0, 1]}},series: [{name: '住户量',type: 'pie',radius: '55%',center: ['50%', '50%'],data: res.data.list.sort(function (a, b) { return a.value - b.value; }),roseType: 'radius',itemStyle: {color: '#3398DB'},animationType: 'scale',animationEasing: 'elasticOut',animationDelay: function (idx) {return Math.random() * 200;}}]});});}
chartVO数据封装
@Data
public class ChartVO {private Integer value;private String name;
}
查看小区数据统计controller请求
/*** 查看小区数据统计* @return*/@GetMapping("/chart")public Result chart(){Map map = this.inOutRecordService.chart();return Result.ok().put("data", map);}
查看小区数据统计service
@Autowiredprivate InOutRecordMapper inOutRecordMapper;@Overridepublic Map chart() {Map<String, List> map = new HashMap<>();List<String> names = new ArrayList<>();List<Integer> nums = new ArrayList<>();List<ChartVO> chartVOList = inOutRecordMapper.chart();List<ChartVO> list = new ArrayList<>();for(ChartVO chartVo: chartVOList){names.add(chartVo.getName());nums.add(chartVo.getValue());list.add(chartVo);}map.put("names",names);map.put("nums",nums);map.put("list",list);return map;}
查看小区数据统计mapper
@Select("select count(*) value, c.community_name name from community c, person p where p.community_id=c.community_id group by c.community_id")List<ChartVO> chart();
界面
6、代码生成器
生成代码
生成entity、mapper、mapper.xml、service、serviceImpl、controller文件
package com.qcby.community;import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.config.DataSourceConfig;
import com.baomidou.mybatisplus.generator.config.GlobalConfig;
import com.baomidou.mybatisplus.generator.config.PackageConfig;
import com.baomidou.mybatisplus.generator.config.StrategyConfig;
import com.baomidou.mybatisplus.generator.config.po.TableFill;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;import java.util.Arrays;
import java.util.List;public class Main {public static void main(String[] args) {AutoGenerator autoGenerator = new AutoGenerator();DataSourceConfig dataSourceConfig = new DataSourceConfig();dataSourceConfig.setDbType(DbType.MYSQL);dataSourceConfig.setDriverName("com.mysql.cj.jdbc.Driver");dataSourceConfig.setUsername("root");dataSourceConfig.setPassword("123456");dataSourceConfig.setUrl("jdbc:mysql://localhost:3306/community?useUnicode=true&characterEncoding=UTF-8");autoGenerator.setDataSource(dataSourceConfig);GlobalConfig globalConfig = new GlobalConfig();globalConfig.setOpen(false);globalConfig.setOutputDir(System.getProperty("user.dir")+"/src/main/java");globalConfig.setAuthor("admin");globalConfig.setServiceName("%sService");autoGenerator.setGlobalConfig(globalConfig);PackageConfig packageConfig = new PackageConfig();packageConfig.setParent("com.qcby.community");packageConfig.setEntity("entity");packageConfig.setMapper("mapper");packageConfig.setController("controller");packageConfig.setService("service");packageConfig.setServiceImpl("service.impl");autoGenerator.setPackageInfo(packageConfig);StrategyConfig strategyConfig = new StrategyConfig();strategyConfig.setEntityLombokModel(true);strategyConfig.setNaming(NamingStrategy.underline_to_camel);strategyConfig.setColumnNaming(NamingStrategy.underline_to_camel);strategyConfig.setInclude("community");TableFill tableFill1 = new TableFill("create_time", FieldFill.INSERT);List<TableFill> list = Arrays.asList(tableFill1);strategyConfig.setTableFillList(list);autoGenerator.setStrategy(strategyConfig);autoGenerator.execute();}
}
生成示例,以community为例
entity
package com.qcby.community.entity;import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import java.io.Serializable;
import java.util.Date;import lombok.Data;
import lombok.EqualsAndHashCode;/*** <p>* * </p>** @author admin* @since 2024-03-26*/
@Data@EqualsAndHashCode(callSuper = false)public class Community implements Serializable {private static final long serialVersionUID=1L;@TableId(value = "community_id", type = IdType.AUTO)private Integer communityId;/*** 小区名称*/private String communityName;/*** 楼栋数量*/private Integer termCount;/*** 序号*/private Integer seq;/*** 创建人*/private String creater;/*** 创建时间*/@TableField(fill = FieldFill.INSERT)private Date createTime;/*** 经度*/private Float lng;/*** 维度*/private Float lat;}
mapper
package com.qcby.community.mapper;import com.qcby.community.entity.Community;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.springframework.stereotype.Repository;/*** <p>* Mapper 接口* </p>** @author admin* @since 2024-03-26*/
public interface CommunityMapper extends BaseMapper<Community> {}
mapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.qcby.community.mapper.CommunityMapper"></mapper>
service
package com.qcby.community.service;import com.qcby.community.entity.Community;
import com.baomidou.mybatisplus.extension.service.IService;
import com.qcby.community.form.CommunityListForm;
import com.qcby.community.vo.PageVO;/*** <p>* 服务类* </p>** @author admin* @since 2024-03-26*/
public interface CommunityService extends IService<Community> {}
serviceImpl
package com.qcby.community.service.impl;import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.qcby.community.entity.Community;
import com.qcby.community.form.CommunityListForm;
import com.qcby.community.mapper.CommunityMapper;
import com.qcby.community.mapper.PersonMapper;
import com.qcby.community.service.CommunityService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.qcby.community.vo.CommunityVO;
import com.qcby.community.vo.PageVO;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import java.util.ArrayList;
import java.util.List;/*** <p>* 服务实现类* </p>** @author admin* @since 2024-03-26*/
@Service
public class CommunityServiceImpl extends ServiceImpl<CommunityMapper, Community> implements CommunityService {}
controller
package com.qcby.community.controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.stereotype.Controller;/*** <p>* 前端控制器* </p>** @author admin* @since 2024-03-26*/
@Controller
@RequestMapping("//community")
public class CommunityController {}
7、返回封装结果Result
此系统所有的结果都是返回json格式,统一返回样式如下,因此进行返回结果集的统一封装
{
"msg": "操作成功",
"code": 200// 其它数据
}
代码如下:
package com.qcby.community.util;import java.util.HashMap;public class Result extends HashMap<String,Object> {public static Result ok(){Result result = new Result();result.put("code", 200);result.put("msg", "操作成功");return result;}public static Result error(String msg){Result result = new Result();result.put("code", 500);result.put("msg", msg);return result;}@Overridepublic Result put(String key, Object value) {super.put(key, value);return this;}
}