泉城酒店系统技术报告
博主原创系统泉城酒店系统的技术报告。
综述
一个包括前台用户功能和后台管理功能的酒店管理系统,使用Vue+Springboot+MySQL进行前后端分离Web开发。
前台示例:
后台示例:
背景概述
背景:由于经常在旅游时入住酒店,我想到了编写一个酒店系统。又因为系统在济南编写,故将系统命名为泉城酒店系统。
目标:编写一个酒店系统,采用vue+springboot前后端分离框架,实现用户预订酒店等前台功能、管理员后台管理功能,并提供不同的界面以区分用户角色。
主要功能:
管理员与用户的注册与登录;
用户: 宾馆与客房信息查看、预订、支付、收藏、留言等;
管理员: 用户、酒店、客房、订单、留言等管理(增删改查)。
系统用户:普通用户、管理员。
需求分析
系统功能分析
用户:个人信息的修改;查看酒店图片、信息与酒店评论;查看房间图片、信息与房间评论;通过搜索筛选出目标酒店与房间;对感兴趣的酒店、客房进行收藏;预订客房并对订单进行修改、退订操作等。
管理员:个人信息的修改;对用户、酒店、客房、订单、留言等进行增删改查等后台操作。
系统数据分析
系统中保存的数据:
用户信息(id、用户名、密码、姓名、手机、身份证等);
管理员信息(id、管理员名、密码、角色等);
酒店信息(id、酒店名称、酒店图片、酒店星级、酒店地址、酒店电话等);
酒店评论(id、评论时间、用户名、评论内容、评论回复等);
客房信息(id、客房名称、客房图片、客房类型、客房设施、客房价格、客房简介等);
客房评论(id、评论时间、用户名、评论内容、评论回复等);
客房订单(id、客房名称、入住时间、入住用户、价格、支付状态等)
用户收藏(id、用户id、收藏类型、收藏名称等)……
数据之间的联系:
用户收藏中包含相关用户信息,如用户id;
客房信息中包含相关酒店信息,如酒店id;
客房订单中包含相关客房信息,如客房id;也包含相关用户信息,如用户id;
酒店评论中包含相关酒店信息、用户信息;
客房评论中包含相关客房信息、用户信息。
系统非功能分析
系统的性能:单个用户的最大响应时间小于100ms;平均响应时间约30ms;
安全性:SQL注入过滤,判断是否具有非法字符等;数据库中密码使用md5加密;
可用性:用户每次登录都生成或更新一个token,有效时间为1h,在有效时间内用户访问系统无需再次登录,超出有效时间后用户访问系统必须重新登录。
系统设计
应用程序设计
系统架构:vue+html+springboot;
前后端采用的技术:前端:vue html js;后端:springboot mybatis-plus;数据库:mysql GaussDB navicat;
前后端的关系:采用vue+springboot前后端分离框架,前台负责静态页面的呈现、js功能的实现、以及向后台发送数据(通过ajax);后台接收前端发送来的请求、判断请求类型并处理请求。
前后端分离的优点:
1.提高开发效率:前后端各负其责, 前端和后端都做自己擅长的事情,不互相依赖,开发效率更快,而且分工比较均衡,会大大提高开发效率;
2.用户访问速度快,提升页面性能,优化用户体验:没有页面之间的跳转,资源都在同一个页面里面,无刷线加载数据,页面片段间的切换快,使用户体验上升了一大截;
3.增强代码可维护性,降低维护成本,改善代码的质量:后端不分离,代码较为繁杂,维护起来难度大,成本高;
4.减轻了后端服务器的请求压力:公共资源只需要加载一次,减少了HTTP请求数;
5.同一套后端程序代码,不用修改就可以用于Web界面、手机、平板等多种客户端。
系统的模块、模块之间的关系:
信息展示模块:前台首页轮播图、前台首页客房信息展示、酒店信息展示、客房信息展示等;
留言模块:酒店评论、客房评论、系统留言等;
个人信息模块:用户与管理员的注册与登录;个人信息的修改等;
客房预订模块:前台客房预订;前台订单查看与管理等;
后台信息管理模块:用户信息管理、酒店信息管理、客房信息管理、客房预订管理、留言管理等。
数据库设计
概念设计
建立ER模型:
使用Navicat Premium绘制ER模型:
逻辑设计
将ER模型转换为关系模式:
用户(id,添加时间,用户名,姓名,性别,头像,手机,邮箱,身份证);
收藏内容(id,添加时间,用户id,收藏内容id,收藏内容表名,收藏名称,图片);
收藏(收藏内容id,用户id);
留言内容(id,添加时间,用户名,留言内容,回复);
留言(留言id,用户id);
酒店信息(id,酒店名称,酒店类别,酒店星级,酒店图片,酒店地址,酒店电话,酒店介绍);
酒店评论(id,添加时间,客房id,用户id,用户名,内容,回复);
评论酒店属于(用户id,酒店评论id);
客房信息(id,客房名称,客房类型,客房图片,客房设施,客房价格,客房状态,酒店id,联系电话);
客房属于(客房id,酒店id);
客房评论(id,添加时间,客房id,用户id,用户名,内容,回复);
客房评论属于(客房评论id,客房id);
订单(订单id,创建时间,客房名称,客房图片,酒店名称,酒店电话,客房id,入住天数,总价格,入住人数,下单时间,入住时间,备注,用户名,姓名,手机,身份证,是否支付);
下订单(用户id,订单id);
订单对应(订单id,客房id)。
物理设计
建立索引:
系统实现
关键技术实现
中国居民身份证校验码算法
1.将身份证号码前面的17位数分别乘以不同的系数。从第一位到第十七位的系数分别为:7-9-10-5-8-4-2-1-6-3-7-9-10-5-8-4-2。
2.将这17位数字和系数相乘的结果相加。
3.用加出来和除以11,取余数。
4.余数只可能有0-1-2-3-4-5-6-7-8-9-10这11个数字。其分别对应的最后一位身份证的号码为1-0-X-9-8-7-6-5-4-3-2。
5.通过上面计算得知如果余数是3,第18位的校验码就是9。如果余数是2那么对应的校验码就是X,X实际是罗马数字10。
支付宝沙箱支付
使用支付宝开放平台服务端 SDK 快速接入电脑网站支付。
1.在pom.xml中引入依赖:
2.在 application.yml 里面进行配置appID、app私钥、阿里pay 公钥、notifyUrl:
3.获取配置alipay的参数等,编写controller:
@RestController
@RequestMapping("/alipay")
public class AliPayController {
private static final String GATEWAY_URL = "https://openapi.alipaydev.com/gateway.do";
private static final String FORMAT = "JSON";
private static final String CHARSET = "UTF-8";
//签名方式
private static final String SIGN_TYPE = "RSA2";
@Resource
private AlipayConfig aliPayConfig;
@Resource
private KefangxinxiDao ordersMapper;
@GetMapping("/pay") // &subject=xxx&traceNo=xxx&totalAmount=xxx
public void pay(AliPay aliPay, HttpServletResponse httpResponse) throws Exception {
// 1. 创建Client,通用SDK提供的Client,负责调用支付宝的API
AlipayClient alipayClient = new DefaultAlipayClient(GATEWAY_URL, aliPayConfig.getAppId(),
aliPayConfig.getAppPrivateKey(), FORMAT, CHARSET, aliPayConfig.getAlipayPublicKey(), SIGN_TYPE);
// 2. 创建 Request并设置Request参数
AlipayTradePagePayRequest request = new AlipayTradePagePayRequest(); // 发送请求的 Request类
request.setNotifyUrl(aliPayConfig.getNotifyUrl());
JSONObject bizContent = new JSONObject();
bizContent.set("out_trade_no", aliPay.getTraceNo()); // 我们自己生成的订单编号
bizContent.set("total_amount", aliPay.getTotalAmount()); // 订单的总金额
bizContent.set("subject", aliPay.getSubject()); // 支付的名称
bizContent.set("product_code", "FAST_INSTANT_TRADE_PAY"); // 固定配置
request.setBizContent(bizContent.toString());
// 执行请求,拿到响应的结果,返回给浏览器
String form = "";
try {
form = alipayClient.pageExecute(request).getBody(); // 调用SDK生成表单
} catch (AlipayApiException e) {
e.printStackTrace();
}
httpResponse.setContentType("text/html;charset=" + CHARSET);
httpResponse.getWriter().write(form);// 直接将完整的表单html输出到页面
httpResponse.getWriter().flush();
httpResponse.getWriter().close();
}
@PostMapping("/notify") // 注意这里必须是POST接口
public String payNotify(HttpServletRequest request) throws Exception {
if (request.getParameter("trade_status").equals("TRADE_SUCCESS")) {
System.out.println("=========支付宝异步回调========");
Map<String, String> params = new HashMap<>();
Map<String, String[]> requestParams = request.getParameterMap();
for (String name : requestParams.keySet()) {
params.put(name, request.getParameter(name));
// System.out.println(name + " = " + request.getParameter(name));
}
String outTradeNo = params.get("out_trade_no");
String gmtPayment = params.get("gmt_payment");
String alipayTradeNo = params.get("trade_no");
String sign = params.get("sign");
String content = AlipaySignature.getSignCheckContentV1(params);
boolean checkSignature = AlipaySignature.rsa256CheckContent(content, sign, aliPayConfig.getAlipayPublicKey(), "UTF-8"); // 验证签名
// 支付宝验签
if (checkSignature) {
// 验签通过
System.out.println("交易名称: " + params.get("subject"));
System.out.println("交易状态: " + params.get("trade_status"));
System.out.println("支付宝交易凭证号: " + params.get("trade_no"));
System.out.println("商户订单号: " + params.get("out_trade_no"));
System.out.println("交易金额: " + params.get("total_amount"));
System.out.println("买家在支付宝唯一id: " + params.get("buyer_id"));
System.out.println("买家付款时间: " + params.get("gmt_payment"));
System.out.println("买家付款金额: " + params.get("buyer_pay_amount"));
}
}
return "success";
}
}
运行示例:
继承SpringMVC提供的HandlerInterceptor接口,验证请求是否经过了登录页面的验证
1.如果界面不需要登录权限即可访问,则不验证权限;
2.如果访问界面需要登录权限,则获取token:如果token不存在,则需登录,同时创建一个该用户的token;如果token存在,则不需要再次登录即可直接访问界面。
代码:
/**
* 权限(Token)验证
*/
@Component
public class AuthorizationInterceptor implements HandlerInterceptor {
public static final String LOGIN_TOKEN_KEY = "Token";
@Autowired
private TokenService tokenService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//支持跨域请求
response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Allow-Headers", "x-requested-with,request-source,Token, Origin,imgType, Content-Type, cache-control,postman-token,Cookie, Accept,authorization");
response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));
// 跨域时会首先发送一个OPTIONS请求,这里我们给OPTIONS请求直接返回正常状态
if (request.getMethod().equals(RequestMethod.OPTIONS.name())) {
response.setStatus(HttpStatus.OK.value());
return false;
}
IgnoreAuth annotation;
if (handler instanceof HandlerMethod) {
annotation = ((HandlerMethod) handler).getMethodAnnotation(IgnoreAuth.class);
} else {
return true;
}
//从header中获取token
String token = request.getHeader(LOGIN_TOKEN_KEY);
/**
* 不需要验证权限的方法直接放过
*/
if(annotation!=null) {
return true;
}
TokenEntity tokenEntity = null;
if(StringUtils.isNotBlank(token)) {
tokenEntity = tokenService.getTokenEntity(token);
}
if(tokenEntity != null) {
request.getSession().setAttribute("userId", tokenEntity.getUserid());
request.getSession().setAttribute("role", tokenEntity.getRole());
request.getSession().setAttribute("tableName", tokenEntity.getTablename());
request.getSession().setAttribute("username", tokenEntity.getUsername());
return true;
}
PrintWriter writer = null;
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
try {
writer = response.getWriter();
writer.print(JSONObject.toJSONString(R.error(401, "请先登录")));
} finally {
if(writer != null){
writer.close();
}
}
// throw new EIException("请先登录", 401);
return false;
}
}
运行示例:
1.前台首页、浏览酒店、客房等界面不需要登录即可访问,不需要验证登录:
2.预订酒店、个人中心等界面需要登录才能访问,因此获取并验证token。如果token不存在,需要登录:
功能实现
1.Mybatis-Plus
后台功能通过Mybatis-Plus自动生成。Mybatis-Plus封装好了增删改查功能,可以直接调用。
源代码分析:
AutoGenerator代码生成器:
public class CodeGenerator {
/**
* <p>
* 读取控制台内容
* </p>
*/
public static String scanner(String tip) {
Scanner scanner = new Scanner(System.in);
StringBuilder help = new StringBuilder();
help.append("请输入" + tip + ":");
System.out.println(help.toString());
if (scanner.hasNext()) {
String ipt = scanner.next();
if (StringUtils.isNotBlank(ipt)) {
return ipt;
}
}
throw new MybatisPlusException("请输入正确的" + tip + "!");
}
public static void main(String[] args) {
// 代码生成器
AutoGenerator mpg = new AutoGenerator();
// 全局配置
GlobalConfig gc = new GlobalConfig();
String projectPath = System.getProperty("user.dir");
gc.setOutputDir(projectPath + "/src/main/java");
gc.setAuthor("jobob");
gc.setOpen(false);
// gc.setSwagger2(true); 实体属性 Swagger2 注解
mpg.setGlobalConfig(gc);
// 数据源配置
DataSourceConfig dsc = new DataSourceConfig();
dsc.setUrl("jdbc:mysql://localhost:3306/ant?useUnicode=true&useSSL=false&characterEncoding=utf8");
// dsc.setSchemaName("public");
dsc.setDriverName("com.mysql.jdbc.Driver");
dsc.setUsername("root");
dsc.setPassword("密码");
mpg.setDataSource(dsc);
// 包配置
PackageConfig pc = new PackageConfig();
pc.setModuleName(scanner("模块名"));
pc.setParent("com.baomidou.ant");
mpg.setPackageInfo(pc);
// 自定义配置
InjectionConfig cfg = new InjectionConfig() {
@Override
public void initMap() {
// to do nothing
}
};
// 如果模板引擎是 freemarker
String templatePath = "/templates/mapper.xml.ftl";
// 如果模板引擎是 velocity
// String templatePath = "/templates/mapper.xml.vm";
// 自定义输出配置
List<FileOutConfig> focList = new ArrayList<>();
// 自定义配置会被优先输出
focList.add(new FileOutConfig(templatePath) {
@Override
public String outputFile(TableInfo tableInfo) {
// 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!
return projectPath + "/src/main/resources/mapper/" + pc.getModuleName()
+ "/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;
}
});
/*
cfg.setFileCreate(new IFileCreate() {
@Override
public boolean isCreate(ConfigBuilder configBuilder, FileType fileType, String filePath) {
// 判断自定义文件夹是否需要创建
checkDir("调用默认方法创建的目录,自定义目录用");
if (fileType == FileType.MAPPER) {
// 已经生成 mapper 文件判断存在,不想重新生成返回 false
return !new File(filePath).exists();
}
// 允许生成模板文件
return true;
}
});
*/
cfg.setFileOutConfigList(focList);
mpg.setCfg(cfg);
// 配置模板
TemplateConfig templateConfig = new TemplateConfig();
// 配置自定义输出模板
//指定自定义模板路径,注意不要带上.ftl/.vm, 会根据使用的模板引擎自动识别
// templateConfig.setEntity("templates/entity2.java");
// templateConfig.setService();
// templateConfig.setController();
templateConfig.setXml(null);
mpg.setTemplate(templateConfig);
// 策略配置
StrategyConfig strategy = new StrategyConfig();
strategy.setNaming(NamingStrategy.underline_to_camel);
strategy.setColumnNaming(NamingStrategy.underline_to_camel);
strategy.setSuperEntityClass("你自己的父类实体,没有就不用设置!");
strategy.setEntityLombokModel(true);
strategy.setRestControllerStyle(true);
// 公共父类
strategy.setSuperControllerClass("你自己的父类控制器,没有就不用设置!");
// 写于父类中的公共字段
strategy.setSuperEntityColumns("id");
strategy.setInclude(scanner("表名,多个英文逗号分割").split(","));
strategy.setControllerMappingHyphenStyle(true);
strategy.setTablePrefix(pc.getModuleName() + "_");
mpg.setStrategy(strategy);
mpg.setTemplateEngine(new FreemarkerTemplateEngine());
mpg.execute();
}
}
模块运行截图示例:
参考博客:Mybatis-Plus详解 MyBatis-Plus快速开始
2.数据自动计算、填入
A.后台酒店信息自动填入:
下拉框选择酒店后,获取该酒店对应的类别、星级、酒店地址、联系电话,并自动填入。
模块运行截图示例:
B.客房预订信息自动填入:
用户预订客房时,不需要填写客房信息和自己的信息,系统自动获取用户信息并自动填入。
模块运行截图示例:
C.客房预订总价格自动计算
模块运行截图示例:
3.数据校验
对数据的格式校验(手机、邮箱等)和合理性校验(入住天数应为正整数等)。
数据合理性校验举例:
客房价格应为正数(可以为小数,不可以为负数或0);
客房入住天数、入住人数应为正整数(不可以为小数、负数或0);
模块运行截图示例:
代码:
/**
* 邮箱
* @param {*} s
*/
export function isEmail (s) {
return /^([a-zA-Z0-9_-])+@([a-zA-Z0-9_-])+((.[a-zA-Z0-9_-]{2,3}){1,2})$/.test(s)
}
/**
* 手机号码
* @param {*} s
*/
export function isMobile (s) {
return /^1[0-9]{10}$/.test(s)
}
/**
* 电话号码
* @param {*} s
*/
export function isPhone (s) {
return /^([0-9]{3,4}-)?[0-9]{7,8}$/.test(s)
}
/**
* URL地址
* @param {*} s
*/
export function isURL (s) {
return /^http[s]?:\/\/.*/.test(s)
}
/**
* 匹配正数,可以是小数,不可以是负数,可以为空
* @param {*} s
*/
export function isNumber(s){
return /(^-?[+-]?([0-9]*\.?[0-9]+|[0-9]+\.?[0-9]*)([eE][+-]?[0-9]+)?$)|(^$)/.test(s);
}
/**
* 匹配正整数,可以为空
* @param {*} s
*/
export function isIntNumer(s){
return /(^-?\d+$)|(^$)/.test(s);
}
系统测试
设计测试用例、记录测试用例的执行情况:
测试用例:后台管理员新增一个酒店,新增一个客房,前台注册用户,登录,预订刚刚新增的房间,并支付订单。
执行情况:成功执行,完成订单全过程。
总结
课程设计总结
这个学期我学习了数据库课程设计,我对使用Vue+Springboot框架制作网页有了更深入的了解。我一直更擅长做前端界面设计,所以我对前端更了解。在泉城酒店网课设的制作中,我采用了天青色(RGB:(115,160,165))作为项目的主题色,并且使用了html取色器等工具,以帮助我美化前端主题与颜色。通过各种资料的查阅与工具的使用,我对前端设计的理解更加深入,这让我在设计方面更加得心应手。
完成了课设之后,我对后端代码也有了更好的把握。后端曾是我的弱项,所以为了学习更多网页制作的技能,我在7月-8月初在金现代公司实习了一段时间。在这次实习经历中,我第一次使用商业级的平台——金现代公司的轻骑兵(hussar)平台进行网页制作,这也让我跳出了传统Vue+Springboot框架进行了解和学习。相比于传统Vue+Springboot,轻骑兵平台对大部分常用功能进行封装,使得网页开发可以通过可视化的方式来进行。
封装好的平台使用起来固然方便,但是也有一些缺点,比如代码框架冗杂。光是启动前后端就需要两三分钟,而且生成的代码量非常庞大,添加自己想要加入的功能十分困难。虽然最后我放弃了使用hussar平台,但是在实习期间使用hussar平台设计网页时,我对后端的理解更加深入,也更了解和熟练使用Mybatis-Plus生成后端代码,这对我的课设制作和以后可能从事的网页制作工作来说非常重要。
除了能力的提高,在数据库课程设计中我还学到了很多经验教训。比如在课设展示前夕有同学电脑的硬盘损坏了,由于固态硬盘不像机械硬盘有数据恢复的可能,他所有的课设文件全都遗失了。这给我也提了个醒,所以我购买了2TB的机械硬盘用于关键数据的备份,以防万一。
华为GaussDB for MySQL数据库使用总结
GaussDB for MySQL优点:
高兼容性:完全兼容MySQL,无需分库分表,应用无需改造即可轻松迁移上云。在课设的开发过程中,我一开始使用Navicat连接本地MySQL进行数据库开发,部分成型后我尝试使用华为GaussDB for MySQL,惊奇地发现连接和运行非常顺利。既可以在网页端直接操作,也可以用Navicat使用熟悉的操作。
高效备份:采用Log Stream技术,分钟级快速备份和恢复TB级数据,最大支持732天备份保存,支持备份保留期限内任意时间点恢复数据,相比于本地占用大量硬盘空间,云数据库可以便捷、高性价比地备份大量数据。
GaussDB for MySQL缺点:
虽然GaussDB for MySQL具有高性能、大数据容量的优点,但是在课设这种小项目中发挥不出优势。相比于本地数据库,云数据库受网络波动的影响,可能有延迟或者断连。
与上学期实验使用的Oracle数据库相比,MySQL在一些语法细节上略有不同。由于课设的开发中主要使用Mybatis-Plus进行后端开发,因此用到SQL语句较少,在这方面的理解也较少。