知识目标
-
了解FreeMarker,能够简述FreeMarker的作用和生成文件的原理
-
熟悉FreeMarker的常用指令,能够在FTL标签中正确使用assign指令、include指令、if指令和list指令
-
掌握显示套餐列表功能的实现
-
掌握显示套餐详情功能的实现
-
掌握体检预约功能的实现
-
掌握静态页面的实现方式,能够使用FreeMarker技术实现套餐列表与套餐详情页面的静态化
体检是了解自身健康状况、及时发现身体异常,以及预防疾病的重要手段之一。为了给广大体检用户提供便利,医疗健康用户端设立了线上体检预约服务,用户可以随时随地进行体检预约。医疗健康的用户端可以展示套餐列表、套餐详情和进行体检预约。接下来,本模块将对用户端的体检预约进行详细讲解。
8-1 套餐列表
体检套餐是医疗健康面向体检用户销售的产品,为了让用户全方位地了解体检套餐的种类,医疗健康用 户端提供套餐列表页面供用户浏览。
在浏览器中访问用户端首页 index.html。
在用户端首页单击“体检预约”超链接后会跳转到套餐列表页面 setmeal.html,该页面以列表的形式展示所有的体检套餐。
(1)提交查询所有套餐的请求
要实现在访问setmeal.html页面时展示所有的套餐,可以将查询的操作定义在钩子函数created()中,在created()函数中使用Axios发送异步请求的方式获取所有的套餐数据。
(2)实现查询套餐控制器
在 controller 包下的控制器类 SetmealController,在类中定义 getSetmeal( )方法,用于接收和处理查询所有套餐的请求。
//获取所有套餐@RequestMapping("/getSetmeal")public Result getSetmeal() {try {List<Setmeal> list = setmealService.findAll();return new Result(true,MessageConstant.QUERY_SETMEAL_SUCCESS, list);} catch (Exception e) {e.printStackTrace();return new Result(false, MessageConstant.QUERY_SETMEAL_FAIL);}}
(3)创建查询套餐服务
在SetmealService接口中定义findAll()方法,用于查询套餐列表。
//查询所有套餐List<Setmeal> findAll();
(4)实现查询套餐服务
在SetmealServiceImpl类中重写接口的findAll()方法,用于查询套餐列表。
//查询所有套餐@Overridepublic List<Setmeal> findAll() {return setmealDao.findAll();}
(5)实现持久层查询套餐
在SetmealDao接口中定义findAll()方法,用于查询所有套餐。
List<Setmeal> findAll(); //获取所有套餐信息
在SetmealDao.xml映射文件中使用select元素映射查询语句,查询所有的套餐数据。
<!--获取所有套餐信息--><select id="findAll" resultType="com.health.entity.Setmeal">SELECT * FROM t_setmeal</select>
(6)测试套餐列表功能
重启服务器,访问客户端预约体检页面http://localhost:8080/mobile/pages/setmeal.html
8-2 套餐详情
用户想要了解套餐的所有检查组和检查项,可以访问套餐详情页面 setmeal_detail.html,在页面上查看套餐的详细信息。
(1)提交查询套餐详细信息的请求
将查询的操作定义在钩子函数 created( )中,在 created( )函数中通过使用 Axios 发送异步请求的方式查询套餐详细信息。
<script src="../plugins/vue/axios-0.18.0.js"></script>
<script>var vue = new Vue({......created(){axios.post("/setmeal/findById_m.do?id=" + id).then((response) => {if(response.data.flag){this.setmeal = response.data.data;this.imgUrl = 'http://sfaztaij0.hd-bkt.clouddn.com/' + this.setmeal.img;}});}});
</script>
(2)实现查询套餐详细信息控制器
在 SetmealController 类中定义 findById( )方法,用于接收和处理查询套餐详细信息的请求。
//根据套餐id查询套餐详情,包含(套餐基本信息、套餐关联的检查组、检查组关联的检查项)@RequestMapping("/findById_m")public Result findById_m(Integer id) {try {//根据套餐id查询套餐详情Setmeal setmeal = setmealService.findSetmealById(id);return new Result(true,MessageConstant.QUERY_SETMEAL_SUCCESS, setmeal);} catch (Exception e) {e.printStackTrace();return new Result(false, MessageConstant.QUERY_SETMEAL_FAIL);}}
(3)创建查询套餐详细信息服务
在 SetmealService 接口中定义 findSetmealById( )方法,用于根据套餐 id 查询套餐详细信息。
Setmeal findSetmealById(Integer id);//根据id查询套餐详情
(4)实现查询套餐详细信息服务
在 SetmealServiceImpl 类中重写 SetmealService 接口的 findSetmealById( )方法,用于根据套餐 id 查询套餐详细信息。
//根据id查询套餐详情public Setmeal findSetmealById(Integer id) {return setmealDao.findById4Detail(id);}
(5)实现持久层查询套餐详细信息
在 SetmealDao 接口中定义 findById4Detail( )方法,用于根据套餐 id 查询套餐详细信息。
Setmeal findById4Detail(Integer id); //查询套餐详情
在 SetmealDao.xml 映射文件中使用select元素映射查询语句,根据套餐id 查询套餐详细信息。
<resultMap id="baseResultMap" type="com.health.entity.Setmeal"><id column="id" property="id"/><result column="name" property="name"/><result column="code" property="code"/><result column="helpCode" property="helpCode"/><result column="sex" property="sex"/><result column="age" property="age"/><result column="price" property="price"/><result column="remark" property="remark"/><result column="attention" property="attention"/><result column="img" property="img"/></resultMap><!--配置多对多映射关系--><resultMap id="findByIdResultMap" extends="baseResultMap"type="com.health.entity.Setmeal"><!--column用于指定将哪个字段的值传递给第二条sql--><collection property="checkGroups"ofType="com.health.entity.CheckGroup"column="id"select="com.health.dao.CheckGroupDao.selectCheckGroupsBySetmealId"></collection></resultMap><!--根据套餐id查询套餐详情(包含基本信息、关联的检查组、检查项信息)--><select id="findById4Detail" parameterType="int"resultMap="findByIdResultMap">SELECT * FROM t_setmeal WHERE id = #{id}</select>
在CheckGroupDao.xml 映射文件中使用select元素映射查询语句,根据套餐 id 查询套餐对检查组的引用。
<resultMap id="baseResultMap" type="com.health.entity.CheckGroup"><id column="id" property="id"/><result column="name" property="name"/><result column="code" property="code"/><result column="helpCode" property="helpCode"/><result column="sex" property="sex"/><result column="remark" property="remark"/><result column="attention" property="attention"/></resultMap><!--配置多对多映射关系--><resultMap id="findByIdResultMap" extends="baseResultMap"type="com.health.entity.CheckGroup"><collection property="checkItems" ofType="com.health.entity.CheckItem"column="id"select="com.health.dao.CheckItemDao.findCheckItemsByCheckGroupId"></collection></resultMap><!--根据套餐id查询关联的检查组集合--><select id="selectCheckGroupsBySetmealId" parameterType="int"resultMap="findByIdResultMap">SELECT * FROM t_checkgroup WHERE id IN(SELECT checkgroup_id FROM t_setmeal_checkgroupWHERE setmeal_id = #{setmealId})</select>
在CheckItemDao.xml 映射文件中使用select元素映射查询语句,根据检查组 id 查询检查组对检查项的引用。
<!--根据检查组id查询关联的检查项--><select id="findCheckItemsByCheckGroupId" parameterType="int"resultType="com.health.entity.CheckItem">SELECT * FROM t_checkitem WHERE id IN(SELECT checkitem_id FROM t_checkgroup_checkitemWHERE checkgroup_id = #{checkgroup_id})</select>
(6)测试套餐详情功能
重启服务器,在浏览器中访问 http://localhost:8080/mobile/pages/setmeal.html。单击名称为肝肾检查的套餐,跳转到套餐详情页面。
8-3 体检预约
用户选择好体检套餐后,单击套餐详情页面的“立即预约”进入对应的体检预约页面 orderInfo.html。
在体检预约页面中,输入体检人信息后,单击“提交预约”按钮,如果预约失败,在 orderInfo.html 页面会提示失败原因;如果预约成功,跳转到预约成功页面 orderSuccess.html。
体检预约可以分解成 4 个功能,分别是跳转到体检预约页面后显示套餐、发送短信验证码、预约体检、跳转到预约成功页面。
1.跳转体检预约页面后显示套餐
2.发送短信验证码
为 orderInfo.html 页面的“发送验证码”绑定单击事件,在单击事件触发后提交填写的手机号。由 ValidateCodeController类的 send4Order( )方法接收页面提交的手机号,并调用短信服务SMSUtils 发送短信验证码。
3.预约体检
4.跳转到预约成功页面
1.跳转到体检预约页面后显示套餐
(1)提交跳转到体检预约页面的请求
为“立即预约”绑定单击事件,并设置单击时要调用的方法,在方法中提交跳转页面的请求。
<div class="box-button"><a class="order-btn" @click="toOrderInfo()">立即预约</a>
</div>
在 setmeal_detail.html 页面中定义 toOrderInfo( )方法,用于提交跳转页面的请求。
<script>var vue = new Vue({......methods:{toOrderInfo(){window.location.href = "orderInfo.html?id=" + id;}}});
</script>
(2)提交查询套餐的请求
要实现访问orderInfo.html页面时展示套餐信息,可以将查询套餐的操作定义在钩子函数created()中。
<script src="../plugins/vue/axios-0.18.0.js"></script>
<script>created(){//发送axios请求,根据套餐id查询套餐信息,用于页面展示axios.post("/setmeal/findById_m.do?id=" + id).then((response)=>{if(response.data.flag){this.setmeal = response.data.data;}});},......
</script>
(3)实现查询套餐信息
接下来应该实现查询套餐信息的后台逻辑代码,但由于查询套餐信息的 findById( )方法在套餐详情中已经实现,所以这里不再重复展示,稍后直接进行功能测试即可。
(4)测试跳转到体检预约页面后显示套餐
重启服务器,在浏览器中访问 http://localhost:8080/mobile/pages/setmeal_detail.html?id=6 进入肝肾检查的套餐详情页面,单击“立即预约”跳转到对应的体检预约页面。
2.发送短信验证码
(1)提交发送短信验证码的请求
要实现单击“发送验证码”按钮后接收短信验证码,可以为“发送验证码”按钮绑定单击事件,并设置单击时要调用的方法,在该方法中提交发送验证码的请求。
<input style="font-size: x-small;" id="validateCodeButton" @click="sendValidateCode()" type="button" value="发送验证码">
orderInfo.html页面中定义sendValidateCode()方法,用于发送短信验证码
sendValidateCode() {var telephone = this.orderInfo.telephone;//获取用户输入的手机号if (!checkTelephone(telephone)) {this.$message.error("手机号输入错误,请检查后重新输入!");return false;}validateCodeButton = $("#validateCodeButton")[0];//锁定按钮clock = window.setInterval(doLoop, 1000);//使用定时器方法每隔1秒执行一次axios.post("/validatecode/send4Order.do?telephone=" + telephone).then((res) => {if (!res.data.flag) {this.$message.error(res.data.message);}});
}
(2)实现发送短信验证码控制器
在 ValidateCodeController 类中定义 send4Order( )方法,用于接收和处理发送短信验证码的请求。
//体检预约发送验证码@RequestMapping("/send4Order")public Result send4Order(String telephone,HttpSession session){String code = ValidateCodeUtils.generateValidateCode(4).toString();System.out.println("验证码:" + code);session.setAttribute("phone",telephone);session.setAttribute("code",code);try{SMSUtils.sendShortMessage(telephone,code);return new Result(true, MessageConstant.SEND_VALIDATECODE_SUCCESS);}catch (Exception e){e.printStackTrace();return new Result(false, MessageConstant.SEND_VALIDATECODE_FAIL);}}
(3)测试发送短信验证码
重启服务器,在浏览器中访问http://localhost:8080/mobile/pages/orderInfo.html?id=6。填写手机号后,单击“发送验证码”,通过手机接收短信验证码后将验证码填写到输入框中。
3.预约体检
(1)提交预约请求
要实现单击“提交预约”按钮时提交预约请求,可以为该按钮绑定单击事件,并设置单击时要调用的方法,在该方法中提交预约请求。
<button type="button" class="btn order-btn" @click="submitOrder()">提交预约</button>
在 orderInfo.html 页面定义 submitOrder( )方法,用于提交预约请求。
submitOrder() {var idCard = this.orderInfo.idCard;//身份证进行校验if (!checkIdCard(idCard)) {this.$message.error("身份证号输入错误,请检查后重新输入!");return false;}axios.post("/order/submitOrder.do", this.orderInfo).then((res) => {if (res.data.flag) {window.location.href = "orderSuccess.html?orderId=" + res.data.data;} else {this.$message.error(res.data.message);//预约失败,弹出提示}});
}
(2)创建预约类
在 entity 包下创建 Order 类,在类中声明预约的属性,定义各属性的getter/setter 方法,并定义构造方法。
package com.health.entity;import java.io.Serializable;
import java.util.Date;/*** 体检预约信息*/
public class Order implements Serializable {public static final String ORDERTYPE_TELEPHONE = "电话预约";public static final String ORDERTYPE_CLIENT = "客户端预约";public static final String ORDERSTATUS_YES = "已到诊";public static final String ORDERSTATUS_NO = "未到诊";private Integer id;private Integer memberId;//会员idprivate Date orderDate;//预约日期private String orderType;//预约类型 电话预约/客户端预约private String orderStatus;//预约状态(是否到诊)private Integer setmealId;//体检套餐idpublic Order() {}public Order(Integer id) {this.id = id;}public Order(Integer memberId, Date orderDate, Integer setmealId) {this.memberId = memberId;this.orderDate = orderDate;this.setmealId = setmealId;}public Order(Integer memberId, Date orderDate, String orderType, String orderStatus, Integer setmealId) {this.memberId = memberId;this.orderDate = orderDate;this.orderType = orderType;this.orderStatus = orderStatus;this.setmealId = setmealId;}public Order(Integer id, Integer memberId, Date orderDate, String orderType, String orderStatus, Integer setmealId) {this.id = id;this.memberId = memberId;this.orderDate = orderDate;this.orderType = orderType;this.orderStatus = orderStatus;this.setmealId = setmealId;}public Integer getId() {return id;}public void setId(Integer id) {this.id = id;}public Integer getMemberId() {return memberId;}public void setMemberId(Integer memberId) {this.memberId = memberId;}public Date getOrderDate() {return orderDate;}public void setOrderDate(Date orderDate) {this.orderDate = orderDate;}public String getOrderType() {return orderType;}public void setOrderType(String orderType) {this.orderType = orderType;}public String getOrderStatus() {return orderStatus;}public void setOrderStatus(String orderStatus) {this.orderStatus = orderStatus;}public Integer getSetmealId() {return setmealId;}public void setSetmealId(Integer setmealId) {this.setmealId = setmealId;}
}
(3)导入公共资源
在 utils 包中导入 DateUtils 类,用于日期格式化操作。
package com.health.utils;import java.text.SimpleDateFormat;
import java.util.*;/*** 日期操作工具类*/
public class DateUtils {/*** 日期转换- String -> Date** @param dateString 字符串时间* @return Date类型信息* @throws Exception 抛出异常*/public static Date parseString2Date(String dateString) throws Exception {if (dateString == null) {return null;}return parseString2Date(dateString, "yyyy-MM-dd");}/*** 日期转换- String -> Date** @param dateString 字符串时间* @param pattern 格式模板* @return Date类型信息* @throws Exception 抛出异常*/public static Date parseString2Date(String dateString, String pattern) throws Exception {if (dateString == null) {return null;}SimpleDateFormat sdf = new SimpleDateFormat(pattern);Date date = sdf.parse(dateString);return date;}/*** 日期转换 Date -> String** @param date Date类型信息* @return 字符串时间* @throws Exception 抛出异常*/public static String parseDate2String(Date date) throws Exception {if (date == null) {return null;}return parseDate2String(date, "yyyy-MM-dd");}/*** 日期转换 Date -> String** @param date Date类型信息* @param pattern 格式模板* @return 字符串时间* @throws Exception 抛出异常*/public static String parseDate2String(Date date, String pattern) throws Exception {if (date == null) {return null;}SimpleDateFormat sdf = new SimpleDateFormat(pattern);String strDate = sdf.format(date);return strDate;}/*** 获取当前日期的本周一是几号** @return 本周一的日期*/public static Date getThisWeekMonday() {Calendar cal = Calendar.getInstance();cal.setTime(new Date());// 获得当前日期是一个星期的第几天int dayWeek = cal.get(Calendar.DAY_OF_WEEK);if (1 == dayWeek) {cal.add(Calendar.DAY_OF_MONTH, -1);}// 设置一个星期的第一天,按中国的习惯一个星期的第一天是星期一cal.setFirstDayOfWeek(Calendar.MONDAY);// 获得当前日期是一个星期的第几天int day = cal.get(Calendar.DAY_OF_WEEK);// 根据日历的规则,给当前日期减去星期几与一个星期第一天的差值cal.add(Calendar.DATE, cal.getFirstDayOfWeek() - day);return cal.getTime();}/*** 获取当前日期周的最后一天** @return 当前日期周的最后一天*/public static Date getSundayOfThisWeek() {Calendar c = Calendar.getInstance();int dayOfWeek = c.get(Calendar.DAY_OF_WEEK) - 1;if (dayOfWeek == 0) {dayOfWeek = 7;}c.add(Calendar.DATE, -dayOfWeek + 7);return c.getTime();}/*** 根据日期区间获取月份列表** @param minDate 开始时间* @param maxDate 结束时间* @return 月份列表* @throws Exception*/public static List<String> getMonthBetween(String minDate, String maxDate, String format) throws Exception {ArrayList<String> result = new ArrayList<>();SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM");Calendar min = Calendar.getInstance();Calendar max = Calendar.getInstance();min.setTime(sdf.parse(minDate));min.set(min.get(Calendar.YEAR), min.get(Calendar.MONTH), 1);max.setTime(sdf.parse(maxDate));max.set(max.get(Calendar.YEAR), max.get(Calendar.MONTH), 2);SimpleDateFormat sdf2 = new SimpleDateFormat(format);Calendar curr = min;while (curr.before(max)) {result.add(sdf2.format(curr.getTime()));curr.add(Calendar.MONTH, 1);}return result;}/*** 根据日期获取年度中的周索引** @param date 日期* @return 周索引* @throws Exception*/public static Integer getWeekOfYear(String date) throws Exception {Date useDate = parseString2Date(date);Calendar cal = Calendar.getInstance();cal.setTime(useDate);return cal.get(Calendar.WEEK_OF_YEAR);}/*** 根据年份获取年中周列表** @param year 年分* @return 周列表* @throws Exception*/public static Map<Integer, String> getWeeksOfYear(String year) throws Exception {Date useDate = parseString2Date(year, "yyyy");Calendar cal = Calendar.getInstance();cal.setTime(useDate);//获取年中周数量int weeksCount = cal.getWeeksInWeekYear();Map<Integer, String> mapWeeks = new HashMap<>(55);for (int i = 0; i < weeksCount; i++) {cal.get(Calendar.DAY_OF_YEAR);mapWeeks.put(i + 1, parseDate2String(getFirstDayOfWeek(cal.get(Calendar.YEAR), i)));}return mapWeeks;}/*** 获取某年的第几周的开始日期** @param year 年分* @param week 周索引* @return 开始日期* @throws Exception*/public static Date getFirstDayOfWeek(int year, int week) throws Exception {Calendar c = new GregorianCalendar();c.set(Calendar.YEAR, year);c.set(Calendar.MONTH, Calendar.JANUARY);c.set(Calendar.DATE, 1);Calendar cal = (GregorianCalendar) c.clone();cal.add(Calendar.DATE, week * 7);return getFirstDayOfWeek(cal.getTime());}/*** 获取某年的第几周的结束日期** @param year 年份* @param week 周索引* @return 结束日期* @throws Exception*/public static Date getLastDayOfWeek(int year, int week) throws Exception {Calendar c = new GregorianCalendar();c.set(Calendar.YEAR, year);c.set(Calendar.MONTH, Calendar.JANUARY);c.set(Calendar.DATE, 1);Calendar cal = (GregorianCalendar) c.clone();cal.add(Calendar.DATE, week * 7);return getLastDayOfWeek(cal.getTime());}/*** 获取当前时间所在周的开始日期** @param date 当前时间* @return 开始时间*/public static Date getFirstDayOfWeek(Date date) {Calendar c = new GregorianCalendar();c.setFirstDayOfWeek(Calendar.SUNDAY);c.setTime(date);c.set(Calendar.DAY_OF_WEEK, c.getFirstDayOfWeek());return c.getTime();}/*** 获取当前时间所在周的结束日期** @param date 当前时间* @return 结束日期*/public static Date getLastDayOfWeek(Date date) {Calendar c = new GregorianCalendar();c.setFirstDayOfWeek(Calendar.SUNDAY);c.setTime(date);c.set(Calendar.DAY_OF_WEEK, c.getFirstDayOfWeek() + 6);return c.getTime();}//获得上周一的日期public static Date geLastWeekMonday(Date date) {Calendar cal = Calendar.getInstance();cal.setTime(getThisWeekMonday(date));cal.add(Calendar.DATE, -7);return cal.getTime();}//获得本周一的日期public static Date getThisWeekMonday(Date date) {Calendar cal = Calendar.getInstance();cal.setTime(date);// 获得当前日期是一个星期的第几天int dayWeek = cal.get(Calendar.DAY_OF_WEEK);if (1 == dayWeek) {cal.add(Calendar.DAY_OF_MONTH, -1);}// 设置一个星期的第一天,按中国的习惯一个星期的第一天是星期一cal.setFirstDayOfWeek(Calendar.MONDAY);// 获得当前日期是一个星期的第几天int day = cal.get(Calendar.DAY_OF_WEEK);// 根据日历的规则,给当前日期减去星期几与一个星期第一天的差值cal.add(Calendar.DATE, cal.getFirstDayOfWeek() - day);return cal.getTime();}//获得下周一的日期public static Date getNextWeekMonday(Date date) {Calendar cal = Calendar.getInstance();cal.setTime(getThisWeekMonday(date));cal.add(Calendar.DATE, 7);return cal.getTime();}//获得今天日期public static Date getToday(){return new Date();}//获得本月一日的日期public static Date getFirstDay4ThisMonth(){Calendar calendar = Calendar.getInstance();calendar.set(Calendar.DAY_OF_MONTH,1);return calendar.getTime();}public static void main(String[] args) {try {System.out.println("本周一" + parseDate2String(getThisWeekMonday()));System.out.println("本月一日" + parseDate2String(getFirstDay4ThisMonth()));} catch (Exception e) {e.printStackTrace();}}
}
(4)实现预约体检控制器
在 controller 包下创建控制器类 OrderController,在类中定义submitOrder( )方法,用于接收和处理预约体检的请求。
(1)获取用户输入的验证码与存储在session中的验证码进行比对,判断是否正确;
(2)验证码比对通过后,调用OrderService接口的order()方法实现预约体检。
/*** 体检预约*/
@RestController
@RequestMapping("/order")
public class OrderController {@Autowiredprivate OrderService orderService;//提交体检预约请求@RequestMapping("/submitOrder")public Result submitOrder(@RequestBody Map map, HttpSession session){String telephone = (String) map.get("telephone");//获取用户页面输入的验证码String validateCode = (String) map.get("validateCode");//从session获取保存的验证码Object code=session.getAttribute("code");//校验用户输入的验证码是否正确if(code == null || !String.valueOf(code).equals(validateCode)){//验证码输入错误return new Result(false, MessageConstant.VALIDATECODE_ERROR);}//通过dubbo调用服务实现预约逻辑Result result = null;try{map.put("orderType", Order.ORDERTYPE_CLIENT);//预约类型result = orderService.order(map);//调用服务}catch (Exception e){e.printStackTrace();return result;}return result;}
}
(5)创建预约体检服务
在 service 包下创建接口 OrderService,在接口中定义 order( )方法,用于预约体检。
/*** 体检预约接口*/
public interface OrderService {Result order(Map map)throws Exception;//体检预约
}
(6)实现预约体检服务
在service.impl 包下创建 OrderService 接口的实现类OrderServiceImpl,在类中重写 order( )方法,用于预约体检。
(1)查询用户是否在某日期进行过体验预约并判断该日期是否已经约满。
(2)查询当前用户是否为会员,如果是会员,查询该会员是否重复预约,若是重复预约则无法继续预约;如果不是会员,则将该用户自动注册为会员。
(3)新增预约,更新已预约人数。
/*** 体检预约服务接口实现类*/
@Service("orderService")
@Transactional
public class OrderServiceImpl implements OrderService {@Autowiredprivate OrderDao orderDao;@Autowiredprivate OrderSettingDao orderSettingDao;@Autowiredprivate MemberDao memberDao;//体检预约public Result order(Map map) throws Exception{//获取日期String orderDate = (String) map.get("orderDate");Date date = DateUtils.parseString2Date(orderDate);//根据日期查询预约设置信息OrderSetting orderSetting = orderSettingDao.findByOrderDate(date);if(orderSetting == null){//所选日期没有提前进行预约设置,不能完成预约return new Result(false,MessageConstant.SELECTED_DATE_CANNOT_ORDER);}if(orderSetting.getReservations() >= orderSetting.getNumber()){//所选日期已经约满,无法预约return new Result(false, MessageConstant.ORDER_FULL);}//判断是否在重复预约String telephone = (String) map.get("telephone");Member member = memberDao.findByTelephone(telephone);if(member != null){Integer memberId = member.getId();//会员idInteger setmealId =Integer.parseInt((String)map.get("setmealId"));//套餐idOrder order = new Order(memberId,date,setmealId);List<Order> orderList = orderDao.findByCondition(order);if(orderList != null && orderList.size() > 0){//用户在重复预约,不能完成预约return new Result(false,MessageConstant.HAS_ORDERED);}}if(member == null){//当前用户不是会员,需要自动完成注册member = new Member();member.setName((String) map.get("name"));member.setPhoneNumber(telephone);member.setIdCard((String) map.get("idCard"));member.setSex((String) map.get("sex"));member.setRegTime(new Date());memberDao.add(member);}//保存预约信息Order order = new Order(member.getId(),date,(String)map.get("orderType"),Order.ORDERSTATUS_NO,Integer.parseInt((String) map.get("setmealId")));orderDao.add(order);//更新已预约人数orderSetting.setReservations(orderSetting.getReservations() + 1);orderSettingDao.editReservationsByOrderDate(orderSetting);return new Result(true,MessageConstant.ORDER_SUCCESS,order.getId());}
}
(7)实现持久层预约体检
在 dao 包下创建持久层接口 OrderDao,用于处理与体检预约相关的操作。
/*** 体检预约持久层接口*/
public interface OrderDao {public List<Order> findByCondition(Order order);//查询预约记录public void add(Order order);//新增预约
}
在dao 目录下创建与 OrderDao 接口同名的映射文件 OrderDao.xml。在文件中使用insert 元素映射新增语句,使用select元素映射查询语句。
<?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.health.dao.OrderDao" ><resultMap id="baseResultMap" type="com.health.entity.Order"><id column="id" property="id"/><result column="member_id" property="memberId"/><result column="orderDate" property="orderDate"/><result column="orderType" property="orderType"/><result column="orderStatus" property="orderStatus"/><result column="setmeal_id" property="setmealId"/></resultMap><!--新增--><insert id="add" parameterType="com.health.entity.Order"><selectKey resultType="java.lang.Integer" order="AFTER"keyProperty="id">SELECT LAST_INSERT_ID()</selectKey>INSERT INTO t_order(member_id,orderDate,orderType,orderStatus,setmeal_id)VALUES (#{memberId},#{orderDate},#{orderType},#{orderStatus},#{setmealId})</insert><!--动态条件查询--><select id="findByCondition" parameterType="com.health.entity.Order"resultMap="baseResultMap">SELECT * FROM t_order<where><if test="id != null">AND id = #{id}</if><if test="memberId != null">AND member_id = #{memberId}</if><if test="orderDate != null">AND orderDate = #{orderDate}</if><if test="orderType != null">AND orderType = #{orderType}</if><if test="orderStatus != null">AND orderStatus = #{orderStatus}</if><if test="setmealId != null">AND setmeal_id = #{setmealId}</if></where></select></mapper>
在OrderSettingDao接口中定义findByOrderDate()方法和editReservationsByOrderDate( )方法。
//根据日期查询预约设置信息OrderSetting findByOrderDate(Date date);//更新已预约人数void editReservationsByOrderDate(OrderSetting orderSetting);
在 OrderSettingDao.xml 映射文件中使用select元素映射查询语句,使用update元素映射更新语句。
<!--根据日期查询预约设置信息--><select id="findByOrderDate" parameterType="date" resultType="com.health.entity.OrderSetting">SELECT * FROM t_ordersetting WHERE orderDate = #{orderDate}</select><!--更新已预约人数--><update id="editReservationsByOrderDate" parameterType="com.health.entity.OrderSetting">UPDATE t_ordersetting SET reservations = #{reservations}WHERE orderDate = #{orderDate}</update>
(8)查询体检预约信息表 t_order
通过查询体检预约信息表 t_order 中的数据验证新增预约的结果。
CREATE TABLE `health`.`t_order`( `id` INT NOT NULL AUTO_INCREMENT, `member_id` INT, `orderDate` DATE, `orderType` VARCHAR(100), `orderStatus` VARCHAR(100), `setmeal_id` INT, PRIMARY KEY (`id`) );
4.跳转到预约成功页面
(1)提交跳转到预约成功页面的请求
要实现跳转到 orderSuccess.html 页面之后展示预约信息,可以将查询预约信息的操作定义在 Vue 提供的钩子函数 created( )中,created( )函数在 Vue 对象初始化完成后自动执行。
<script src="../plugins/vue/axios-0.18.0.js"></script>
<script>var vue = new Vue({......created(){axios.post("/order/findById.do?id=" + id).then((response) => {this.orderInfo = response.data.data;});}});
</script>
(2)实现查询预约控制器
在 OrderController 类中定义 findById( )方法,用于接收和处理根据预约 id 查询预约信息的请求。
//根据预约id查询预约信息@RequestMapping("/findById")public Result findById(Integer id){try{Map map = orderService.findById(id);return new Result(true,MessageConstant.QUERY_ORDER_SUCCESS,map);}catch (Exception e){e.printStackTrace();return new Result(false,MessageConstant.QUERY_ORDER_FAIL);}}
(3)创建查询预约服务
在 OrderService 接口中定义 findById( )方法,用于根据预约 id 查询预约信息。
Map findById(Integer id);//根据预约id查询预约信息
(4)实现查询预约服务
在 OrderServiceImpl 类中重写 OrderService 接口的findById( )方法,用于根据预约 id 查询预约信息。
//根据id查询预约详细信息(包括会员姓名、套餐名称、预约基本信息)public Map findById(Integer id) {Map map = orderDao.findById4Detail(id);if(map != null){//处理日期格式Date orderDate = (Date) map.get("orderDate");try {map.put("orderDate",DateUtils.parseDate2String(orderDate));} catch (Exception e) {e.printStackTrace();}}return map;}
(5)实现持久层查询预约信息
在 OrderDao 接口中定义 findById4Detail( )方法,用于根据预约 id 查询预约信息。
Map findById4Detail(Integer id);//查询预约信息
在 OrderDao.xml 映射文件中使用select元素映射查询语句,查询预约信息,包括体检人信息和套餐信息。
<!--根据预约id查询预约信息,包括体检人信息、套餐信息--><select id="findById4Detail" parameterType="int" resultType="map">SELECT m.name AS member,s.name AS setmeal,o.orderDate AS orderDate,o.orderType AS orderTypeFROM t_order o, t_member m, t_setmeal sWHERE o.member_id=m.id AND o.setmeal_id=s.id AND o.id=#{id}</select>
(6)测试跳转到预约成功页面
重启服务器,在浏览器中访问 http://localhost:8080/mobile/pages/orderSuccess.html?orderId=22。
8-4 页面静态化(可略)
用户登录用户端进行体检预约时,需要访问套餐列表页面和套餐详情页面,此时访问这两个页面,页面展示的所有信息都需要从数据库中查询,如果访问量大,会造成数据库的访问压力大、页面刷新缓慢等问题。 从套餐包含的信息可以看出,套餐包含基本信息、对检查组的引用信息。一般情况下套餐内容变化频率不高,所以我们可以将套餐列表页面和套餐详情页面动态查询的结果分别转化成固定的静态页面进行展示,从而为数据库减压并提高系统运行性能。
页面静态化就是将原来的动态网页使用静态化技术生成静态网页,这样用户在访问网页时,服务器直接响应静态页面,不需要反复查询数据库,从而有效降低数据库的访问压力。 与数据库中数据保持一致的静态页面才是有效可用的。当管理端执行套餐新增、编辑或删除的操作后,会改变套餐的信息,这时需要重新生成静态页面。
FreeMarker概述
FreeMarker是一款用Java语言编写的模板引擎,是一种基于模板和要改变的数据生成输出文本的通用工具。例如,生成HTML页面、配置文件、源代码等。它不是面向最终用户的,而是一个Java类库,是一款可以嵌入其他产品的组件。
Template指的是模板;Java objects指的是准备数据;Output指的是最终的文件。通过FreeMarker将数据填充到模板中,然后通过Output进行输出,最终生成静态文件。
FreeMarker 模板的开发语言是 FreeMarker Template Language(FreeMarker 模板语言,下文简称 FTL),FTL的基本语法由文本、插值、FTL 标签和注释组成,具体如下。 文本:文本会按原样输出。 插值:这部分的输出会被计算的值替换。插值由 ${ and } (或者 #{ and })分隔。 FTL 标签:FTL 标签与 HTML 标签相似,用于给 FreeMarker 指示,不会在输出内容中显示。 注释:其注释与 HTML 的注释也很相似,是由 <#-- 和 -->来分隔的。其注释会被 FreeMarker 直接忽略,不会在输出内容中显示。
FreeMarker 指令
FreeMarker 指令通过 FTL 标签调用,FreeMarker 标签的语法与 HTML、XML 标签的语法类似,为了对FreeMarker 标签和 HTML、XML 标签予以区分,FreeMarker 标签以#开头。接下来讲解 FreeMarker 中 4 种常用的指令。
(1)assign指令
assign 指令用于在页面上定义一个变量,可以定义简单类型和对象类型。
-
定义简单类型
<#assign linkman="周先生"> 联系人:${linkman}
上述代码中,指令都是以“<#”开始,以“>”结束的。其中,assign 是指令名称,linkman 是定义的变量名,不是固定写法,可以任意指定。通过${变量名}的方式获取变量值。
-
定义对象类型
<#assign info={"mobile":"13812345678",'address':'北京市昌平区'} > 电话:${info.mobile} 地址:${info.address}
上述代码中,定义对象info,对象中包含两个变量mobile和address。
(2)include指令
include指令用于文件的嵌套。例如创建文件head.ftl,文件内容如下所示。
<h1>医疗健康</h1>
创建文件test.ftl,在test.ftl文件中使用include指令引入文件head.ftl。
<html> <#include "head.ftl"/> <body><#assign info={"mobile":"13812345678",'address':'北京市昌平区'} >电话:${info.mobile} 地址:${info.address} </body> </html>
上述代码中,使用include指令引入文件head.ftl。head.ftl文件的内容会在include指令出现的位置插入。
(3)if指令
if指令用于判断,与Java中的if用法类似。
<#if x == 1>x is 1 <#else if x == 2>x is 2 <#else if x = 3>x is 4 <#else>x is one</#if>
在FreeMarker的判断中,可以使用“=”,也可以使用“==”,二者含义相同。
(4)list指令
list指令用于遍历。
<#list goodsList as goods>商品名称: ${goods.name} 价格:${goods.price}<br> </#list>
上述代码中,goodsList 表示想要被迭代的项,可以是集合或序列;goods 表示循环变量的名称,每次迭代时,循环变量会存储当前项的值。
(1)提供静态页面模板
在 WEB-INF 目录下创建 ftl 目录,在 ftl 目录中创建模板文件 mobi le_ setmeal.ftl 和 mobile_setmeal_detail.ftl,使用 FreeMarker 技术生成套餐列表静态页面和套餐详情静态页面。
(2)引入 FreeMarker 的依赖
要想在Java程序中使用FreeMarker服务,需要引入FreeMarker的依赖。
(3)创建FreeMarker配置文件
在 resources 目录下属性文件 db.properties,指定了生成的静态页面的存放位置。
out_put_path=E:/IdeaProjects/HealthWeb/web/mobile/pages
在配置文件中如果文件内容需要换行显示,需要在换行的位置添加“\”,否则会报错;在指定静态页面存放的目录位置时,目录中不能有中文字符,否则会报错。
(4)Spring与FreeMarker 整合
在applicationContext.xml文件中添加一下配置信息
<bean id="freemarkerConfig" class="org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer"><!--指定模板文件所在目录--><property name="templateLoaderPath" value="/WEB-INF/ftl/" /><!--指定字符集--><property name="defaultEncoding" value="UTF-8" /></bean>
(5)生成静态页面
由于套餐新增、编辑和删除的执行过程是在 SetmealServiceImpl 类中完成的,为了方便代码调用,我们 可以在 SetmealServiceImpl 类中增加生成静态页面的代码。
@Autowiredprivate FreeMarkerConfigurer freeMarkerConfigurer;@Value("${out_put_path}")private String outPutPath;//从属性文件中读取要生成的html对应的目录//生成当前方法所需的静态页面public void generateMobileStaticHtml(){//在生成静态页面之前需要查询数据List<Setmeal> list = setmealDao.findAll();//查询套餐列表数据generateMobileSetmealListHtml(list);//需要生成套餐列表静态页面generateMobileSetmealDetailHtml(list);//需要生成套餐详情页面}//生成套餐列表静态页面public void generateMobileSetmealListHtml(List<Setmeal> list){Map map = new HashMap();map.put("setmealList",list);//为模板提供数据,用于生成静态页面generteHtml("mobile_setmeal.ftl","m_setmeal.html",map);}//生成套餐详情静态页面(多个)public void generateMobileSetmealDetailHtml(List<Setmeal> setmealList){for (Setmeal setmeal : setmealList) {Map<String, Object> dataMap = new HashMap<String, Object>();dataMap.put("setmeal",setmealDao.findById4Detail(setmeal.getId()));generteHtml("mobile_setmeal_detail.ftl","setmeal_detail_"+setmeal.getId()+".html",dataMap);}}//通用的方法,用于生成静态页面public void generteHtml(String templateName,String htmlPageName,Map map){Configuration configuration =freeMarkerConfigurer.getConfiguration();//获得配置对象Writer out = null;try {Template template = configuration.getTemplate(templateName);//构造输出流out = new FileWriter(new File(outPutPath + "/" + htmlPageName));//输出文件template.process(map,out);out.close();} catch (Exception e) {e.printStackTrace();}}
(6)实现套餐查询方法
在 generateMobileStaticHtml( )方法中调用了 SetmealDao 接口中的 findAll( )方法查询套餐列表,调用了findById4Detail( )方法查询套餐详情,由于这 2 个方法已经在套餐列表功能和套餐详情功能中实现,所以这里不再重复讲解。
(7)完善 SetmealServiceImpl 类中的方法
当我们在管理端对套餐进行新增、编辑或删除操作时,会导致套餐内容发生改变,这时需要重新生成静态页面。在 SetmealServiceImpl 类的 add( )、edit( )和 delete( )方法中调用生成静态页面的 generateMobileStaticHtml( )方法。
(8)修改用户端访问地址
为了能够在用户端访问到生成的套餐列表静态页面,需要对 health_mobile 模块下 index.html 页面中体检预约的超链接地址进行修改,将/pages/setmeal.html 修改为/pages/m_setmeal.html。
......
<li class="type-item"><a href="/pages/m_setmeal.html" class="link-page"><div class="type-title"><h3>体检预约</h3><p>实时预约</p></div>......</a>
</li>
......
(9)测试静态页面展示
启动服务,访问http://localhost:8080/mobile/pages/index.html以执行套餐编辑操作为例,生成套餐列表静态页面与套餐详情静态页面。
在浏览器访问http://localhost:8080/mobile/pages/m_setmeal.html。
模块小结
本模块主要对用户端的体检套餐展示、体检预约和页面静态化进行了讲解。首先讲解了套餐列表和套餐详情的功能;然后讲解了体检预约的功能;最后讲解了 FreeMarker 静态页面生成技术的配置及使用,并讲解了通过静态页面展示套餐列表和套餐详情的功能。希望通过本模块的学习,可以掌握用户端套餐列表、套餐详情和体检预约的操作,熟悉 FreeMar ker 的配置及使用,并熟悉生成套餐列表和套餐详情静态页面的操作。