基于ssm的航空订票系统
一、技术栈
前端
vue全家桶、element-ui组件库、moment.js插件
后端
springboot + springmvc+ mybatis
二、功能描述
本系统是基于B/S架构的航空订票系统
系统分为三大用户–乘客、航空公司、后台管理员,本次课程设计主要实现了乘客界面,包括购票、选座、充值、退款、改签等
三、技术框架
1、界面设计
Vue是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。
上面这段官方解释的意思是,vue提供了一种组件化设计的方式,也就是说,你可以在单个vue对象中构建起实现某些功能compopent,再在下一个vue中将它们组装,最后在一个vue中展示。有过面向对象编程经历的程序员很容易接受这种设计方式,就像手写一个自己的class,然后再别的类中引用类实例和方法一样——就连引入这个行为使用的也是同一个关键字import
Element,一套为开发者、设计师和产品经理准备的基于 Vue 2.0 的桌面端组件库
Element UI 对于新手前端设计师来说是福音,它自带的一系列标签和组件可以帮助那些为JavaScript繁琐而头疼的编程者们开辟一个的新天地。只要基础程度的审美,人人都可以设计出一套简洁好看的界面。
这些组件可以通过Attribute、Event和Methods标签进行个性化的设置,同时也在很大程度上简化了js函数的写法
2、前后端通信
引入axios插件后,在函数中可以通过axios对象向目标端口发送请求并执行回调,例如向8181端口发送一个post请求
this.$axios.post("http://localhost:8181/findAll").then(res => {})
后端可以通过设置一个跨域类CrosConfig来接收前端请求
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configuration
public class CrosConfig implements WebMvcConfigurer {@Overridepublic void addCorsMappings(CorsRegistry registry) {registry.addMapping("/**")//拦截所有请求.allowedOrigins("*").allowedMethods("GET","HEAD","POST","PUT","DELETE")//拦截所有类型.allowCredentials(true).maxAge(3600)//最大线程数.allowedHeaders("*");//允许所有请求头}
}
也有在src/resource/mybatis-config.xml中配置跨域的方法,这边不赘述
映射写法
主流有两种方法,一种是REST风格,一种是直接发送json的写法
- 简单来说,REST风格将发送的数据拼接进请求里,通过 “/” 隔开,在后台用 @RequestMapping 注解完成方法映射 @PathVariable注解直接赋值到确定数据类型里
如:前端
this.$axios.post("http://localhost:8181/login/passenger/"+that.loginform.passengername+"/"+that.loginform.password)
后端
@RequestMapping
(value = "/passenger/{passengername}/{password}",method = RequestMethod.POST,produces = {"application/json;charset=UTF-8"})
public String passengerlogin(@PathVariable("passengername") String passengername,@PathVariable("password") String password)
- 另一种向后台直接发送数据的则是要用*@RequestBody*,把json表封装成类实例,再调用get方法取数据
如:前端
this.$axios.post("http://localhost:8181/order/refund", row)
后端
@RequestMapping
(value = "refund", method = RequestMethod.POST, produces = {"application/json;charset=UTF-8"})
public String refundOrder(@RequestBody OrderInfo orderInfo)
在数据量较小的情况下,REST风格要更加简洁干练
3、持久层
现在使用最广泛的两种持久层框架是JPA和mybatis,这两者都有各自忠实的使用者,孰优孰劣也不是我这样的技术小白讲得清楚的,不过我的建议是,如果你刚刚接触持久层框架而整纠结于二者的选择,不妨就照着这篇博客试试mybatis,在初级阶段,它绝不比JPA更复杂,同时也能保证完成你的各项设计目标。你可以在下一次实验中尝试JPA,再来体会一下二者的区别
MyBatis 是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。
如官方文档所言,持久层框架的价值有二:一是封装JDBC代码,省去繁琐重复的操作,使人的工作能集中到sql代码的书写和数据库设计上来;二是维持POJO,使数据以对象的形式长久稳定地存在于程序运行过程中,提升了数据交互的效率
简单的sql语句可以通过注解直接在方法上书写,复杂查询语句或者大段大段的sql则适合接口 + .xml文档的方式,为了统一形式,对初学者我更加推荐把所有的sql都写在xml文档中的方法
mybatis将数据实体、接口方法和sql实现xml联动的写法可以参照如下代码
数据实体类PassengerInfo
package com.oreki.ptss_rear.domain;import lombok.Data;@Data
public class PassengerInfo {private Integer passengerId;private String passengerName;private String passengerPassword;private String passengerIdentity;private String passengerPhone;private Float passengerAccount;
}
@Data注解来自于lombok插件,它能帮你隐式地生成Set、Get、ToString等方法(虽然也有getter、setter的Constructor自动生成器,不过减少显式出现的代码量总是好的)
另外,不同于JPA,mybatis不需要声明数据实体类,也就不需要设置主键(对比于JPA的@Entity和@Id标签)
接口类PassengerInfoMapper
package com.oreki.ptss_rear.mapper;import com.oreki.ptss_rear.domain.PassengerInfo;
import org.apache.ibatis.annotations.Param;
import java.util.List;public interface PassengerInfoMapper {int deleteByPrimaryKey(Integer passengerId);int insert(PassengerInfo record);int insertSelective(PassengerInfo record);int updateByPrimaryKeySelective(PassengerInfo record);int updateByPrimaryKey(PassengerInfo record);int updateByPrimaryKeyCharge(@Param(value = "passengerId") Integer passengerId,@Param(value = "passengerAccount") Float passengerAccount);List<PassengerInfo> getList();PassengerInfo selectByPassengerName(String passengerName);PassengerInfo selectByPrimaryKey(Integer passengerId);List<PassengerInfo> selectListByName(String searchtext);
}
在接口类中导入实体类后就建立起了方法与实体数据的联系
@Param是MyBatis所提供的,作为Dao层的注解,作用是用于传递参数,从而可以与SQL中的的字段名相对应,解决了可读性和直观性的问题
书写sql语句的xml文档PassengerMapper.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.oreki.ptss_rear.mapper.PassengerInfoMapper"><resultMap id="BaseResultMap" type="com.oreki.ptss_rear.domain.PassengerInfo"><id column="passenger_id" jdbcType="INTEGER" property="passengerId" /><result column="passenger_name" jdbcType="VARCHAR" property="passengerName" /><result column="passenger_password" jdbcType="VARCHAR" property="passengerPassword" /><result column="passenger_identity" jdbcType="VARCHAR" property="passengerIdentity" /><result column="passenger_phone" jdbcType="VARCHAR" property="passengerPhone" /><result column="passenger_account" jdbcType="REAL" property="passengerAccount" /></resultMap><sql id="Base_Column_List">passenger_id, passenger_name, passenger_password, passenger_identity,passenger_phone, passenger_account</sql><select id="selectByPrimaryKey" parameterType="java.lang.Integer" resultMap="BaseResultMap">select *from passenger_infowhere passenger_id = #{passengerId,jdbcType=INTEGER}</select><select id="getList" resultMap="BaseResultMap">select *from passenger_info</select><select id="selectByPassengerName" parameterType="java.lang.String" resultMap="BaseResultMap">select *from passenger_infowhere passenger_name = #{passengerName,jdbcType=VARCHAR}</select><select id="selectListByName" parameterType="java.lang.String" resultMap="BaseResultMap">select *from passenger_infowhere passenger_name like concat ('%',#{searchtext},'%')</select><delete id="deleteByPrimaryKey" parameterType="java.lang.Integer">delete from passenger_infowhere passenger_id = #{passengerId,jdbcType=INTEGER}</delete><insert id="insert" parameterType="com.oreki.ptss_rear.domain.PassengerInfo">insert into passenger_info (passenger_id, avatar, passenger_name,passenger_password, passenger_gender, passenger_identity,passenger_phone, passenger_account, register_time)values (#{passengerId,jdbcType=INTEGER}, #{avatar,jdbcType=VARCHAR}, #{passengerName,jdbcType=VARCHAR},#{passengerPassword,jdbcType=VARCHAR}, #{passengerGender,jdbcType=VARCHAR}, #{passengerIdentity,jdbcType=VARCHAR},#{passengerPhone,jdbcType=VARCHAR}, #{passengerAccount,jdbcType=REAL}, #{registerTime,jdbcType=TIMESTAMP})</insert><insert id="insertSelective" parameterType="com.oreki.ptss_rear.domain.PassengerInfo">insert into passenger_info<trim prefix="(" suffix=")" suffixOverrides=","><if test="passengerId != null">passenger_id,</if><if test="avatar != null">avatar,</if><if test="passengerName != null">passenger_name,</if><if test="passengerPassword != null">passenger_password,</if><if test="passengerGender != null">passenger_gender,</if><if test="passengerIdentity != null">passenger_identity,</if><if test="passengerPhone != null">passenger_phone,</if><if test="passengerAccount != null">passenger_account,</if><if test="registerTime != null">register_time,</if></trim><trim prefix="values (" suffix=")" suffixOverrides=","><if test="passengerId != null">#{passengerId,jdbcType=INTEGER},</if><if test="avatar != null">#{avatar,jdbcType=VARCHAR},</if><if test="passengerName != null">#{passengerName,jdbcType=VARCHAR},</if><if test="passengerPassword != null">#{passengerPassword,jdbcType=VARCHAR},</if><if test="passengerGender != null">#{passengerGender,jdbcType=VARCHAR},</if><if test="passengerIdentity != null">#{passengerIdentity,jdbcType=VARCHAR},</if><if test="passengerPhone != null">#{passengerPhone,jdbcType=VARCHAR},</if><if test="passengerAccount != null">#{passengerAccount,jdbcType=REAL},</if><if test="registerTime != null">#{registerTime,jdbcType=TIMESTAMP},</if></trim></insert><update id="updateByPrimaryKeySelective" parameterType="com.oreki.ptss_rear.domain.PassengerInfo">update passenger_info<set><!--<if test="avatar != null">avatar = #{avatar,jdbcType=VARCHAR},</if>--><if test="passengerName != null">passenger_name = #{passengerName,jdbcType=VARCHAR},</if><if test="passengerPassword != null">passenger_password = #{passengerPassword,jdbcType=VARCHAR},</if><if test="passengerIdentity != null">passenger_identity = #{passengerIdentity,jdbcType=VARCHAR},</if><if test="passengerPhone != null">passenger_phone = #{passengerPhone,jdbcType=VARCHAR},</if><if test="passengerAccount != null">passenger_account = #{passengerAccount,jdbcType=REAL},</if></set>where passenger_id = #{passengerId,jdbcType=INTEGER}</update><update id="updateByPrimaryKey" parameterType="com.oreki.ptss_rear.domain.PassengerInfo">update Passenger_infoset avatar = #{avatar,jdbcType=VARCHAR},passenger_name = #{passengerName,jdbcType=VARCHAR},passenger_password = #{passengerPassword,jdbcType=VARCHAR},passenger_gender = #{passengerGender,jdbcType=VARCHAR},passenger_identity = #{passengerIdentity,jdbcType=VARCHAR},passenger_phone = #{passengerPhone,jdbcType=VARCHAR},passenger_account = #{passengerAccount,jdbcType=REAL},register_time = #{registerTime,jdbcType=TIMESTAMP}where passenger_id = #{passengerId,jdbcType=INTEGER}</update><update id="updateByPrimaryKeyCharge">update Passenger_infoset passenger_account = #{passengerAccount,jdbcType=REAL}where passenger_id = #{passengerId,jdbcType=INTEGER};</update></mapper>
头两行是一般xml文件的规范
标签通过namespace属性,可以确定该文档实现的是哪一个接口
标签,id属性指定了当前对应集类型,一般是BaseResultMap;type属性指明了操作的数据库Table和项目中的Entity实体的对应关系。
resultMap内部的标签说明了主键,标签将Table中的列名和实体的对象名进行对应(注意MySql不区分大小写)
4.MVC框架
模式Model—视图View—控制器Controller(MVC)是上世纪出现的一种软件设计模式,这一模式早期发展(Model1和Model2时代)后,现在有如SpringMVC这样的方案
这对仅有过小型程序编程经验的学习者来说是理解大型软件模块间相互作用和交流的一种优秀模式,结构松散、耦合、与 Spring 无缝集成等特性让其大放异彩
四、功能展示
主界面,进行搜索或点击图片都可进入购票详情页
机票详情
机票检索
钱包界面
订单中心,可进行改签/退票等操作
改签界面,多退少补
五、DEBUG记录
Tip1
数据库:实体类中的变量名(包括类名)与数据库中的字段名(包括表名)匹配时,要注意这样一条规则,java变量大小写敏感,可以在一个长变量名中通过大写字母分隔单词;数据库大小写不敏感,通过数据库管理工具看到的显示大小写只是一种展示,变量名映射为字段名、类名映射为表名,大写分隔转为横杠分隔
e.g
错误×:变量passengerId对应数据库字段passengerId
正确√:passengerId → passenger_id
修改图片路径报错
Module not found Can't resolve './src/assets/img/Aurora.jpg' in 'C:\Users\HP\myproject\passenger_ticket_sell\src\views\passenger\home'
解决办法:项目内绝对路径改写为相对路径
第一次改为相对路径编译成功后,使用绝对路径或者@标签都找得到资源,这可能是因为先要注册一次?不太清楚
url("src/assets/img/Paris.jpg"); → url("../../../assets/img/Paris.jpg");
前端500
有可能使因为后台没有传回数据,这时如果出现后端接收的json数据全为null的情况,当接收体是对象时,需要@RequestBody标签,即>json通过该标签和对象内变量建立联系(同名)
如果后端以参数形式接收数据的话,建议REST风格
josn直接装配到后端参数解决ing
动态读取图片(数据库路径)
img属性
<img@click="showdetial(item.ticketId)":src="imgUrl(item.ticketImg)"width="250px"height="200px"
/>
方法中返回动态的路径,require后必须接路径,所有需要有"…/…/…/assets/img/“前缀,拼接起来解释成路径,
不能直接通过img传入”…/…/…/assets/img/test.jpg"再require(img),这样解释成String,会报错
imgUrl(img){return require("../../../assets/img/" + img);
}