宾馆客房管理系统——前后端分离
前后端分离项目-——宾馆管理系统(springboot+vue+mybatisplus+redis缓存+elementUI)
使用springboot+vue+mybatisplus+redis+elementUI
一、 实现功能
除员工登录外功能外:
所有功能都要通过登录实现(安全方面 前端通过vue-router 和vuex实现页面访问拦截,后端通过mysql和redis缓存中校验用户合法性,判断其身份状态)
员工分为 管理员 和 前台职工
共有功能:
登录功能:
登录前检查状态 防止重复登录,能够判断身份 vue据此进入不同主页,同时将信息保存在redis中,减少sql与服务器压力且查询 校验方面更加便捷
普通员工功能
查看所有房间,查看剩余房间(可点击房间直接进行开房),查看营业中的房间,查看暂停营业的房间(维修,清理等等)
添加顾客信息(给客人进行登记 ),按条件查询顾客信息(可个根据 电话号码查询 、身份证号查询)、查询所有在住的人员信息,查询历史顾客信息、对顾客信息进行修改
开房,退房,办理会员,查询所有未完成的订单和已完成的订单,查询某个顾客的订单,退出登录
管理员功能
添加员工,修改员工信息,查看所有在线的员工,强制员工下线
添加房间,删除房间,修改房间,进入普通员工页面实现所有普通员工功能
二、库表设计
员工表
房间表
房间类型映射表
数据部分
顾客等级表
数据部分
订单表
数据
三。最终效果
springboot后端
vue前端:
管理员界面
…
源码地址
https://gitee.com/wang-yongyan188/HotelManger.git
后端具体流程
总共分为4个模块:
员工模块(负责进行一些安全方面 员工的登录认证,身份校验,员工的增改查)
顾客模块(负责将顾客信息存储,对顾客信息进行增删改查),
房间模块(对房间的增删改查)
订单模块(负责开房(增),退房信息记录(改),外键房间号,员工号,顾客编号 可根据订单获取办理业务的员工所有信息,该顾客以及同住人员的所有信息,房间的所有信息以及自身订单信息(查))
接口为restful形式
事先准备
建立一个maven项目,引入springboot父项目,web依赖,测试依赖,druid连接池,lombok,mybatis-plus,redis,hutool工具包
pom文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>hotelsys</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.4</version>
<relativePath/>
</parent>
<properties>
<maven.compiler.source>9</maven.compiler.source>
<maven.compiler.target>9</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.19</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.20</version>
</dependency>
</dependencies>
</project>
在recources目录下 创建application springboot的配置文件 分为开发环境和生产环境,两种配置方案,随时切换
application.yml
spring:
profiles:
active: dev
application:
name: HotelManger
mybatis-plus:
mapper-locations: classpath:/com/yan/*.xml
type-aliases-package: com.yan.entity
application-dev.yml
server:
port: 8888
spring:
datasource:
username: root
password: 1234
type: com.alibaba.druid.pool.DruidDataSource
url: jdbc:mysql://localhost:3306/hotelsys?serverTimezone=Asia/Shanghai&zeroDateTimeBehavior=convertToNull&autoReconnect=true&useSSL=false&failOverReadOnly=false
driver-class-name: com.mysql.cj.jdbc.Driver
redis:
host: 192.168.83.100
port: 6380
database: 1
logging:
level:
com.yan.mapper: debug
# root: warn
isStartRedis: 0
application-prod.yml 放入部署时的配置
创建启动类
@SpringBootApplication
public class HotelMangerApplication {
public static void main(String[] args) {
SpringApplication.run(HotelMangerApplication.class,args);
}
}
按照数据库表,建立对应实体类,以及统一返回给前端格式的类
package com.yan.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import lombok.experimental.Accessors;
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
@Accessors(chain = true)
public class Common{
private Integer status;
private String msg;
private Object data;
public static Common error(int status,String msg){
return new Common(status,msg,null);
}
public static Common success(String msg,Object data){
return new Common(200,msg,data);
}
public static Common success(String msg){
return new Common(200,msg,null);
}
}
创建mapper层,service层,control层,根据需要,如果是多表查询,mybatisplus需要使用mapper.xml文件
例如 customerMapper.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.yan.dao.CustomerMapper">
<resultMap id="custype" type="Customer">
<result property="cid" column="cid"></result>
<result property="name" column="name"></result>
<result property="telnum" column="telnum"></result>
<result property="grade" column="grade"></result>
<result property="isdel" column="isdel"></result>
<association property="grade1" javaType="com.yan.entity.Grade">
<result property="gradeid" column="gradeid"></result>
<result property="gradename" column="gradename"></result>
<result property="discount" column="discount"></result>
</association>
</resultMap>
<select id="getAllCus" resultMap="custype">
SELECT * FROM `customer` c,grade g WHERE c.grade=g.gradeid
</select>
<select id="getIsDel" resultType="Customer">
select * from customer where isdel=1
</select>
</mapper>
员工模块 EmployeeController
员工登录
@PostMapping("/login")
public Common login(@RequestBody Employee employee, HttpSession session)
前端传递实体类对象,根据对象的eid和name通过mapper去数据库中获取两者同时相符合的数据库对象,先判断对象是否为空 否则返回失败 再将数据库对象的密码与传递过来的实体类对象密码进行equals比较 不同返回失败,相同则进一步判断数据库对象的状态(确保一个账号,只能同时在线一人)。到此就能够判断用户的合法性,将自己的状态设置为已经登录,根据前端传递 设置自己的柜台号,判断是否使用redis 是则以json方式 用户id作为key存入redis 否则存入Servletcontext中,最后在将对象传入mapper使数据库中此用户数据修改,为了保障安全 ,将密码置空通过map传递给前端
判断是否为管理者
@PostMapping("isManger/{token}")
public Boolean isManger(@PathVariable("token") String token,HttpSession session)
这个接口权限认证,可能被频繁调用 改为从缓存中获取数据 因为需要校验时用户一定已登录,缓存中有数据
前端从localstore中获取的key进行传递,接收到后,如果使用redis缓存,则从缓存中由key寻找到对象,进行转化后 获取ismanger 的值,如果没使用redis从Servletcontext中获取,最后根据ismnger 的值返回true或false
退出登录
@GetMapping("/logout/{token}")
public Common logout(@PathVariable String token,HttpSession session)
根据前端传递过来的token 从缓存中按key寻找 ,没有找到,则代表没有登录,返回对应信息,若找到该用户,将对象转为员工对象,并对登录状态进行修改,移除缓存中放入key ,最后调用mapper,将修改后的对象传递修改数据库(问题:暴露接口 不安全)
查询所有员工
@GetMapping("/getAll")
public Common getAllEmployee()
底层通过 return mapper.selectList(new QueryWrapper());无条件查询所有 一句搞定
修改某个员工信息
@PostMapping("updateEms")
public Common updateEms(@RequestBody Employee employee)
前端传递对象,底层 mapper.update( employee, new QueryWrapper<>().eq("eid",eid))
并校验是否成功,返回对应信息
查询所有在线员工 和不在线员工
@GetMapping("/getWorking")
public Common getWorkingEmS()
@GetMapping("/getNoWork")
public Common getNOWorkEmS()
底层通过 return mapper.selectList(new QueryWrapper<Employee>().eq("status",status));
实现 不同接口 ,调用相同service ,传递不同status值
添加员工
@PostMapping("/addEms")
public Common addEms(@RequestBody Employee employee)
前端传递对象,后端 mapper.insert(employee);
强制退出登录(适用于管理员使员工下线)
@GetMapping("/forceLeave/{eid}")
public Common forceLeave(@PathVariable int eid,
HttpSession session)
底层调用logout方法(接口地址暴露,不安全)
强制退出登录(适用于员工本地token不存在时)
@PostMapping("/forceLogout")
public Common forceLogout(@RequestBody Employee
employee,HttpSession session)
前端员工点击退出,若本地localstore中key被删除,将无法通过传递key退出
这是检测本地key不存在,会弹出一个表格进行退出,员工要匹配 id name passwd 才能确认身份,安全退出
顾客模块CustomerController
查询所有顾客
@GetMapping("getAll")
public Common getAllCus()
底层进行多表查询 ,通过grade字段去匹配grade表
<resultMap id="custype" type="Customer">
<result property="cid" column="cid"></result>
<result property="name" column="name"></result>
<result property="telnum" column="telnum"></result>
<result property="grade" column="grade"></result>
<result property="isdel" column="isdel"></result>
<association property="grade1" javaType="com.yan.entity.Grade">
<result property="gradeid" column="gradeid"></result>
<result property="gradename" column="gradename"></result>
<result property="discount" column="discount"></result>
</association>
</resultMap>
<select id="getAllCus" resultMap="custype">
SELECT * FROM `customer` c,grade g WHERE c.grade=g.gradeid
</select>
查询在住顾客
@GetMapping("getInAll")
public Common getNoAllCus()
通过用户表iddel字段实现,该字段开始为了实现假删除设计,后来觉得用来判断是否在住更加合适 即用户是否有效 在开房时,通过用户cid将此字段修改为0 退房时,通过用户cid将此字段修改为1
底层
mapper.selectList(new QueryWrapper<Customer>().eq("isdel",0));
根据身份证号 或 顾客电话号 查询
@GetMapping("getByTel/{tel}")
public Common getCusByTel(@PathVariable Long tel)
@GetMapping("getById/{id}")
public Common getCusById(@PathVariable Long id)
两者只有字段不同 用相同底层实现,通过service层 封装 getCusByParam(String param,Long val)
调用dao层 mapper.selectOne(new QueryWrapper<Customer>().eq(param,val));
注册员工
@PostMapping("regCus")
public Common regCus(@RequestBody Customer customer)
前端传递对象 dao层使用mapper.insert(customer);
更新顾客信息
@PostMapping("update")
public Common updateCus(@RequestBody Customer customer)
当前端实现注册会员时候 也是修改顾客信息 dao层使用mapper.update(customer,new QueryWrapper<Customer>().eq("cid",customer.getCid())
房间模块 RoomController
查询房间所有信息
@GetMapping("/getAll")
public Common getAllRoom()
使用多表查询 连接roomtype表
package com.yan.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
@TableName("room")
public class Room {
@TableId
private Integer rid;
private Integer rtype;
private Double extraprice;
private Integer status;
private String comment;
@TableField(exist = false)
private RoomType roomtype;
}
mapper映射
<?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.yan.dao.RoomMapper">
<resultMap id="roomtype" type="Room">
<result property="rid" column="rid"></result>
<result property="rtype" column="rtype"></result>
<result property="extraprice" column="extraprice"></result>
<result property="status" column="status"></result>
<result property="comment" column="comment"></result>
<association property="roomtype" javaType="com.yan.entity.RoomType">
<result property="describe" column="describe"></result>
<result property="standardprice" column="standardprice"></result>
<result property="typename" column="typename"></result>
<result property="tid" column="tid"></result>
</association>
</resultMap>
<select id="getAllRoom" resultMap="roomtype">
SELECT r.rid ,r.rtype,r.extraprice,r.`status`,r.`comment`,t.typename ,t.`describe` ,t.standardprice,t.tid
from room r , roomtype t
WHERE r.rtype=t.tid
</select>
查询剩余房间 ,满的房间,暂停营业房间
房间状态status依次为0 ,1,2
底层同一个方法
public List<Room>getRoomByParam(@Param("status")int status);
<select id="getRoomByParam" resultMap="roomtype" parameterType="int">
SELECT r.rid ,r.rtype,r.extraprice,r.`status`,r.`comment`,t.typename ,t.`describe` ,t.standardprice,t.tid
from room r , roomtype t
WHERE r.rtype=t.tid
and r.`status`=#{status}
</select>
public List<Room> getAllFullRoom() {
return mapper.getRoomByParam(1);
}
public List<Room> getAllRemainRoom() {
return mapper.getRoomByParam(0);
}
public List<Room> getAllStopRoom() {
return mapper.getRoomByParam(2);
}
**添加房间 **
@PostMapping("/addRoom")
public Common addRoom(@RequestBody Room room)
前传递对象 底层一句mapper.insert(room);
更新房间
@PostMapping("/updateRoom")
public Common updateRoom(@RequestBody Room room)
mapper.update(room,new QueryWrapper<>().
eq("rid",room.getRid);
删除房间
@DeleteMapping("/{rid}")
public Common delRoom(@PathVariable("rid") int rid)
前端以restful形式传递房间号 底层调用
mapper.delete(new QueryWrapper<Room>().eq("rid",rid)
订单模块 OrderController
创建订单(开房操作)
@PostMapping("/create")
public Common createOrder(@RequestBody Map map)
前端传递对象,对象的属性 后端用map作为key接收(也可以用order接收)
创建一个order对象 order
利用hutool工具包 获取一个唯一标识id 并设置为order的id,将订单的状态设置为未完成(因为开房,订单为进行时 退房时 为完成时 ) 从前台获取业务员的eid,房间号rid,顾客身份号cid 并设置 。获取房间使用时长,根据rid从数据中查询对象,判断房间状态 判断类型,由此确定时长为天数还是小时数 和对应价钱(房间类型标准价钱±每个房间的浮动) ,根据顾客身份号获取顾客对象 将开房人的状态设置为0 获取顾客等级由此知道顾客享受的折扣 ,最终价钱=(房间类型标准价钱±每个房间的浮动)*折扣 。
获取系统日期为startTime 如果房间类型为钟点房 利用hutool 在现在时间加上对应小时数的时间 转为string 进行设置 ;如果为普通间 再次基础上加上天数,将日期去掉时间,加上当天12:00 确保不管几点入住 截止时间为对应天数第二天12点退房 。其余获取备注,同住人cid 最后对订单对象进行插入保存。加上事务 ,使得过程出错,数据得以回滚。
public Common createOrder(Map map) {
try {
System.out.println(map.toString());
MyOrder order = new MyOrder();
// oid
order.setOid( IdUtil.fastSimpleUUID());
// status
order.setStatus(1);
// 必要的key
String session1 = map.get("key").toString().substring(4);
System.out.println(session1);
// 停掉redis 测试
Employee employee = (Employee) eMapper.getEmsById(Integer.parseInt(session1)).getData();
// Employee employee = (Employee) redisTemplate.opsForValue().get(session);
// eid
order.setEid(employee.getEid());
// 原计划 直接前端传入间隔
Integer daysOrHours = Integer.parseInt(String.valueOf(map.get("daysOrHours"))) ;
// 现在前端传入截止日期
Integer rid =Integer.parseInt(String.valueOf(map.get("rid"))) ;
// rid
order.setRid(rid);
Room room = rService.getRoomByRid(rid);
if(room.getStatus()!=0){
return Common.error(500,"该房间不可预订");
}
// realprice
Double extraprice = room.getExtraprice();
RoomType roomtype = room.getRoomtype();
Double standardprice = roomtype.getStandardprice();
String cid =String.valueOf(map.get("cid"));
long l = Long.parseLong(cid);
//cid
order.setCid(l);
Common cus = cService.getCusByParam("cid", l);
Customer customer = (Customer) cus.getData();
customer.setIsdel(1);
double dis= customer.getGrade1().getDiscount();
Double realprice=(standardprice+extraprice)*dis;
DecimalFormat df = new DecimalFormat("0.00");
String format = df.format(realprice);
double v = Double.parseDouble(format);
order.setRealprice(v);
String now = DateUtil.now();
order.setStarttime(now);
// date
if(room.getRtype()==6){
// 假设 钟点房 时长 最低1h 最高 6h 任意时间入住
Date dateNow= DateUtil.parse(now);
DateTime dateTime = DateUtil.offsetHour(dateNow, daysOrHours);
order.setEndtime(dateTime.toString());
}else {
// 假设一次最多14天 一律中午12点前退房
Date dateNow= DateUtil.parse(now);
DateTime dateTime = DateUtil.offsetDay(dateNow, daysOrHours);
Date parse = DateUtil.parse(dateTime.toString("yyyy-MM-dd"));
DateTime endtime = DateUtil.offsetHour(parse, 12);
order.setEndtime(endtime.toString());
}
//可选操作
// remark
Object remark = map.get("remark");
if(!ObjectUtil.isNull(remark)){
order.setRemark(remark.toString());
}
// cidrest
Object cidrest = map.get("cidrest");
if (!ObjectUtil.isNull(cidrest)) {
order.setCrest((List<Customer>) cidrest);
}
System.out.println(order);
int insert = oMapper.createOrder(order);
if(insert==1){
room.setStatus(1);
rMapper.updateRoom(room);
cService.updateCus(customer);
return Common.success("订单创建成功",order);
}
return Common.error(500,"出错");
}
catch (Exception e){
return Common.error(500,"出错");
}
}
退房操作(获取订单,修改状态后保存)
@GetMapping("/finish/{rid}/{cid}")
public Common finishOrder(@PathVariable("rid") int rid,@PathVariable("cid") Long cid){
return service.finishOrder(rid,cid);
}
前端传递房间号和开房人身份证号查找唯一订单(因为在开房时设置 每个用户只能开一个房间,也可以根据订单号作为凭证,开房时返回给用户,用户由此退房),检测订单状态 防止重复退房 在对订单状态设置为已完成(1) 将房间状态进行修改 ,对用户是否在住进行修改 获取当前系统时间作为最终离开时间 并与截止时间都转为时间对象再咋转为 毫秒数进行比较,如果离开时间大于截止时间,判定为超时,并将大于的毫秒数转为小时数 对订单备注进行设置 最后update 进行更新 同样如果出错,进行数据回滚
public Common finishOrder(int rid, Long cid) {
try {
Common orderByCR = getOrderByCR(rid, cid);
MyOrder data = (MyOrder) orderByCR.getData();
if(data.getStatus().equals(1)){
return Common.error(500,"订单已完成");
}
// 订单失效
data.setStatus(0);
String now = DateUtil.now();
data.setRealtime(now);
// 如果退房超时 给出提示 打在订单上
Date dateNow= DateUtil.parse(now);
String endtime = data.getEndtime();
Date dateend= DateUtil.parse(endtime);
if(dateNow.getTime()>dateend.getTime()){
Long overtime=dateNow.getTime()-dateend.getTime();
long overHours = TimeUnit.MILLISECONDS.toHours(overtime);
data.setRemark("超时 "+overHours+"小时");
}
// 房间
Room roomByRid = rService.getRoomByRid(data.getRid());
if(roomByRid.getStatus().equals(0)){
return Common.error(500,"房间号错误");
}
roomByRid.setStatus(0);
// 顾客
Common cus = cService.getCusByParam("cid", cid);
Customer data1 = (Customer) cus.getData();
data1.setIsdel(0);
cService.updateCus(data1);
rService.updateRoom(roomByRid);
updateOrder(data);
return Common.success("退房成功",data);
}
catch (Exception e){
return Common.error(500,"系统错误");
}
}
按条件查询订单
@GetMapping("/getAllByCid/{cid}")
public Common getAllOrderByCid(Long cid)
oMapper.selectList(new QueryWrapper<MyOrder>().eq("cid", cid));
查询正在进行订单和已完成订单
@GetMapping("/getRunOrder")
public Common getRunOrder(){
return service.getRunOrder(1);
}
@GetMapping("/getFinOrder")
public Common getFinOrder()
oMapper.selectList(new QueryWrapper<MyOrder>().eq("status",i)));
后端部分完成
后端有待改进
缺少安全保护,数据库密码用明文存储 ,后续将使用shiro技术
对接口的保护全靠前端,接口暴露,后续在后端设置拦截器 利用jwt判断身份拦截
更多推荐
所有评论(0)