泉城酒店系统技术报告

目录

泉城酒店系统技术报告

博主原创系统泉城酒店系统的技术报告。

综述

一个包括前台用户功能和后台管理功能的酒店管理系统,使用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语句较少,在这方面的理解也较少。

-->