第6章 移动端开发-体检预约、手机快速登录

学习目标:

  • 了解体检预约流程业务

  • 能够基于阿里云短信服务实现短信发送

  • 掌握体检预约的实现过程【重中之重】

  • 掌握预约成功页面展示的实现过程

  • 了解移动端手机快速登录需求

  • 掌握手机快速登录实现过程

1. 回顾体检预约流程需求

用户可以通过如下操作流程进行体检预约:

1、在移动端首页点击体检预约,页面跳转到套餐列表页面

2、在套餐列表页面点击要预约的套餐,页面跳转到套餐详情页面

3、在套餐详情页面点击立即预约,页面跳转到预约页面,使用页面静态化技术

4、在预约页面录入体检人信息,包括手机号,点击发送验证码

5、在预约页面录入收到的手机短信验证码,点击提交预约,完成体检预约

效果如下图:

img

img

img

img

点击【提交预约】完成预约。

  1. health_web 系统管理员录入检查项, 检查组管理,套餐管理,预约设置
  2. health_mobile, 手机/微信用户户 套餐列表->套餐详情->预约页面

2. 短信发送

【目标】

能够基于阿里云短信服务实现短信发送

【路径】

  1. 短信服务介绍
  2. 注册阿里云账号
  3. 设置短信签名
  4. 设置短信模板
  5. 设置access keys
  6. 短信服务API
  7. 发送短信

【讲解】

2.1. 短信服务介绍

目前市面上有很多第三方提供的短信服务,这些第三方短信服务会和各个运营商(移动、联通、电信)对接,我们只需要注册成为会员并且按照提供的开发文档进行调用就可以发送短信。需要说明的是这些短信服务都是收费的服务。

本项目短信发送我们选择的是阿里云提供的短信服务。

短信服务(Short Message Service)是阿里云为用户提供的一种通信服务的能力,支持快速发送短信验证码、短信通知等。 三网合一专属通道,与工信部携号转网平台实时互联。电信级运维保障,实时监控自动切换,到达率高达99%。短信服务API提供短信发送、发送状态查询、短信批量发送等能力,在短信服务控制台上添加签名、模板并通过审核之后,可以调用短信服务API完成短信发送等操作。

2.2. 注册阿里云账号

阿里云官网:https://www.aliyun.com/

点击官网首页免费注册跳转到如下注册页面:

img

注册后,使用账号名登录

img

2.3. 设置短信签名

注册成功后,点击登录按钮进行登录。登录后进入短信服务管理页面,选择国内消息菜单:

点击产品分类->云计算基础->云通信->短信服务

img

【签名】:

选择签名管理

img

点击添加签名按钮:

img

目前个人用户只能申请适用场景为验证码的签名,通用需要企业认证。

2.4. 设置短信模板

在国内消息菜单页面中,点击模板管理标签页:

img

点击添加模板按钮:

img

我的模板内容是:传智健康 验证码${code},您正进行传智健康系统的身份验证,打死不告诉别人!

其中${code}为动态参数,需要我们后续在代码中控制。

2.5. 设置access keys

在发送短信时需要进行身份认证,只有认证通过才能发送短信。本小节就是要设置用于发送短信时进行身份认证的key和密钥。鼠标放在页面右上角当前用户头像上,会出现下拉菜单:

img

点击accesskeys:

img

点击“开始使用子用户AccessKey”按钮,指定用户权限,而不是分配所有权限。

第一步:新建用户

输入登录名称和显示名称,点击【确认】

img

接收短信,防止信息泄露 ,输入验证码即可。

img

新建用户成功

img

第二步:授权

选择用户,添加权限

img

搜索“SMS”,表示短信服务,选择权限,点击“开始创建”。

img

第三步:创建AccessKeyID

点击创建的用户,进入到详情页面。

img

创建成功,其中AccessKeyID为访问短信服务时使用的ID,AccessKeySecret为密钥。

img

注意:需要马上保存AccessKeyID和AccessKeySecret,因为处于安全考虑,这个只显示1次,一旦退出页面就不再显示了。

点击“查看用户详情”,可以在用户详情页面下禁用刚刚创建的AccessKey:

img

在短信服务中,点击“国内消息设置”。可以设置每日和每月短信发送上限:

img

由于短信服务是收费服务,所以还需要进行充值才能发送短信:

在费用中,点击“充值” 充2元就可以了

img

img

2.6. 短信服务API

点击帮助文档

img

找到短信服务中的“短信发送API”

img
img

将代码可以拷贝到工程中测试:

需要修改:

1:accessKeyId和accessKeySecret

final String accessKeyId = "LTAI4tZAmy7xxxxx";//你的accessKeyId,参考本文档步骤2
final String accessKeySecret = "snM1i338WCE0hd1ws6tdbyxxxxxxx";//你的accessKeySecret,参考本文档步骤2

2:手机号

request.setPhoneNumbers("1326921xxxx");

3:签名和模板

request.setSignName("传智健康");
//必填:短信模板-可在短信控制台中找到,发送国际/港澳台消息时,请使用国际/港澳台短信模版
request.setTemplateCode("SMS_16569xxxx");

4:发送的验证码及参数number(根据模板短信内容)

String params = "111111";
request.setTemplateParam("{\"number\":\""+params+"\"}");

2.7. 发送短信

2.7.1. 导入maven坐标

在health_common中导入坐标

<dependency>
  <groupId>com.aliyun</groupId>
  <artifactId>aliyun-java-sdk-core</artifactId>
  <version>3.3.1</version>
</dependency>
<dependency>
  <groupId>com.aliyun</groupId>
  <artifactId>aliyun-java-sdk-dysmsapi</artifactId>
  <version>1.0.0</version>
</dependency>

2.7.2. 封装工具类

在health_common中添加工具类

1:签名

img

2:模板code

img

在health_common中,封装SMSUtils.java

传递验证码和手机号

package com.itheima.health.utils;

import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.http.MethodType;
import com.aliyuncs.profile.DefaultProfile;
import com.aliyuncs.profile.IClientProfile;

/**
 * 短信发送工具类
 */
public class SMSUtils {
	public static final String VALIDATE_CODE = "SMS_189616640";//发送短信验证码 验证码签名, 改成你的
	public static final String ORDER_NOTICE = "SMS_159771588";//体检预约成功通知, 通知类的模板(需要通用的签名)
	private static final String SIGN_NAEM = "黑马程序员";// 短信的签名,属于验证码签名, 改成你的
	private static final String PARAMETER_NAME="code"; // 短信模板内容中的参数名 , 改成你的 看模板内容中的${}
	private static final String ACCESS_KEY="LTAI4GERJj7v71F3FKjw3z2A"; //你的AccessKey ID , 改成你的
	private static final String SECRET_KEY="dIVZnHGdUTYbqOKMlxZ7R7jXVcnPoz"; //你的AccessKey Secret , 改成你的

	public static void main(String[] args) throws ClientException {
		SMSUtils.sendShortMessage(VALIDATE_CODE,"13652431027","666666");
	}

	/**
	 * 发送短信
	 * @param templateCode 验证码的模板code
	 * @param phoneNumbers 接收的手机号码
	 * @param param        验证码
	 * @throws ClientException
	 */
	public static void sendShortMessage(String templateCode,String phoneNumbers,String param) throws ClientException{
		// 设置超时时间-可自行调整
		System.setProperty("sun.net.client.defaultConnectTimeout", "10000");
		System.setProperty("sun.net.client.defaultReadTimeout", "10000");
		// 初始化ascClient需要的几个参数
		final String product = "Dysmsapi";// 短信API产品名称(短信产品名固定,无需修改)
		final String domain = "dysmsapi.aliyuncs.com";// 短信API产品域名(接口地址固定,无需修改)
		// 替换成你的AK
		//final String accessKeyId = "LTAIak3CfAehK7cE";// 你的accessKeyId,参考本文档步骤2
		//final String accessKeySecret = "zsykwhTIFa48f8fFdU06GOKjHWHel4";// 你的accessKeySecret,参考本文档步骤2
		// 初始化ascClient,暂时不支持多region(请勿修改)
		IClientProfile profile = DefaultProfile.getProfile("cn-hangzhou", ACCESS_KEY, SECRET_KEY);
		DefaultProfile.addEndpoint("cn-hangzhou", "cn-hangzhou", product, domain);
		IAcsClient acsClient = new DefaultAcsClient(profile);
		// 组装请求对象
		SendSmsRequest request = new SendSmsRequest();
		// 使用post提交
		request.setMethod(MethodType.POST);
		// 必填:待发送手机号。支持以逗号分隔的形式进行批量调用,批量上限为1000个手机号码,批量调用相对于单条调用及时性稍有延迟,验证码类型的短信推荐使用单条调用的方式
		request.setPhoneNumbers(phoneNumbers);
		// 必填:短信签名-可在短信控制台中找到
		request.setSignName(SIGN_NAEM);
		// 必填:短信模板-可在短信控制台中找到
		request.setTemplateCode(templateCode);
		// 可选:模板中的变量替换JSON串,如模板内容为"亲爱的${name},您的验证码为${code}"时,此处的值为
		// 友情提示:如果JSON中需要带换行符,请参照标准的JSON协议对换行符的要求,比如短信内容中包含\r\n的情况在JSON中需要表示成\\r\\n,否则会导致JSON在服务端解析失败
		//request.setTemplateParam("{\"code\":\""+param+"\"}");
		request.setTemplateParam(String.format("{\"%s\":\"%s\"}",PARAMETER_NAME,param));
		// 可选-上行短信扩展码(扩展码字段控制在7位或以下,无特殊需求用户请忽略此字段)
		// request.setSmsUpExtendCode("90997");
		// 可选:outId为提供给业务方扩展字段,最终在短信回执消息中将此值带回给调用者
		// request.setOutId("yourOutId");
		// 请求失败这里会抛ClientException异常
		SendSmsResponse sendSmsResponse = acsClient.getAcsResponse(request);
		if (sendSmsResponse.getCode() != null && sendSmsResponse.getCode().equals("OK")) {
			// 请求成功
			System.out.println("请求成功");
		}else{
			System.out.println(sendSmsResponse.getMessage());
		}
	}
}

2.7.3. 测试短信发送

package com.itheima.health.main;

import com.itheima.health.utils.SMSUtils;


public class SMSMain {
    public static void main(String[] args) throws Exception {
        SMSUtils.sendShortMessage(SMSUtils.VALIDATE_CODE,"你的手机号码","666666");
    }
}

测试:查看手机

img

【小结】

阿里云使用步骤

  1. 注册, 登录
  2. 搜索 短信服务, 开通短信服务
  3. 进入短信控制台,认证
  4. 申请 签名 , 模版
  5. 充钱(2元,0.045元/条), 创建access keys和secrectKey(开发者资质认证)
  6. 添加依赖, 拷贝工具类到项目(修改签名,accessKey, secrectKey, 模板code)
  7. SMSUtil存入小抄
  8. 测试

注意事项

​ 工具类里面(需要改 模版code,签名, access keys,验证码code>number)

image-20200629122101149

3.体检预约

【目标】

实现体检预约

需求:

img

  1. 在套餐详情页面(setmeal_detail.html)点击立即预约,页面跳转到预约页面(orderInfo.html)
  2. 在预约页面(orderInfo.html)录入体检人信息,包括手机号,点击发送验证码
  3. 在预约页面录入收到的手机短信验证码,点击提交预约,完成体检预约

【路径】

在/pages/orderInfo.html

套餐信息显示

​ orderInfo.html 加载时,获取套餐的id, 发送请求从后台获取套餐信息,绑定到页面中展示

手机验证码

点击“发送验证码”按钮时,发送请求把手机号码传给后台。结果的提示

  • ValidateCodeController接收手机号码
  • 从redis中获取这个手机号码的验证码
    • redis中存在 已经发送过了,注意查收
    • 不存在
      • 生成验证码
      • 调用SmsUtils发送短信
      • 存入redis

提交体检预约

  1. 校验验证码 OrderController
    • 从redis中取验证码
      • 没值 重新发送
      • 有值 校验前端传过来的验证码
        • 不匹配则报错
        • 匹配则继续
        • 删除验证码

预约成功要返回订单对象给页面

OrderServiceImpl

  1. 通过日期查询预约设置是否存在 t_ordersetting
    不存在就报错
    存在 则判断是否约满
    full 报错
  2. 是否为会员 通过手机号码查询 t_member
    非会员
    添加到会员表 获取id
  3. 判断是否重复预约 t_order 通过member_id,orderDate, setmeal_id
    重复则报错
    没重复
    可预约,添加订单
  4. 更新已预约数量 t_ordersetting

Dao

  • OrderSettingDao
  • MemberDao
  • OrderDao

【讲解】

3.1. 前端代码

img

1:在详情页面(/pages/setmeal_detail.html)点击体检预约

<div class="box-button">
    <a @click="toOrderInfo()" class="order-btn">立即预约</a>
</div>

2:toOrderInfo()方法:

toOrderInfo(){
    window.location.href = "orderInfo.html?id=" + id;
}

3:在预约页面(/pages/orderInfo.html)进行调整

3.1.1. 展示预约的套餐信息(已完成)

第一步:从请求路径中获取当前套餐的id

<script>
    var id = getUrlParam("id");
</script>

第二步:定义模型数据setmeal,用于套餐数据展示

<script>
    var vue = new Vue({
        el:'#app',
        data:{
            setmeal:{},//套餐信息
            orderInfo:{
                setmealId:id, // 用于传递套餐id
                sex:'1' // 用于默认显示性别男
            }//预约信息
        }
    });
</script>

第三步:显示套餐信息

image-20200629173334926

第四步:在VUE的钩子函数中发送ajax请求,根据id查询套餐信息

image-20200629173238209

3.1.2. 手机号校验

第一步:在orderInfo.html页面导入的healthmobile.js文件中已经定义了校验手机号的方法

<script src="../plugins/healthmobile.js"></script>

healthmobile.js:

/**
 * 手机号校验
 1--以1为开头;
 2--第二位可为3,4,5,7,8,中的任意一位;
 3--最后以0-9的9个整数结尾。
 */
function checkTelephone(telephone) {
    var reg=/^[1][3,4,5,7,8][0-9]{9}$/;
    if (!reg.test(telephone)) {
        return false;
    } else {
        return true;
    }
}

第二步:为发送验证码按钮绑定事件sendValidateCode()

<div class="input-row">
    <label>手机号</label>
    <input v-model="orderInfo.telephone" type="text" class="input-clear" placeholder="请输入手机号">
    <input style="font-size: x-small;" id="validateCodeButton" @click="sendValidateCode()" type="button" value="发送验证码">
</div>
<div class="input-row">
    <label>验证码</label>
    <input v-model="orderInfo.validateCode" type="text" class="input-clear" placeholder="请输入验证码">
</div>

sendValidateCode()方法:

对手机号进行校验

//发送验证码
sendValidateCode(){
    //获取用户输入的手机号
    var telephone = this.orderInfo.telephone;
    //校验手机号输入是否正确
    if (!checkTelephone(telephone)) {
        this.$message.error('请输入正确的手机号');
        return false;
    }
},

3.1.3. 30秒倒计时效果

第一步:前面在sendValidateCode方法中进行了手机号校验,如果校验通过,需要显示30秒倒计时效果

//发送验证码
sendValidateCode(){
    //获取用户输入的手机号
    var telephone = this.orderInfo.telephone;
    //校验手机号输入是否正确
    if (!checkTelephone(telephone)) {
        this.$message.error('请输入正确的手机号');
        return false;
    }
    validateCodeButton = $("#validateCodeButton")[0];
    clock = window.setInterval(doLoop, 1000); //一秒执行一次
},

第二步:其中,validateCodeButton和clock是在healthmobile.js文件中定义的属性。

doLoop是在healthmobile.js文件中定义的方法

var clock = '';//定时器对象,用于页面30秒倒计时效果
var nums = 30;
var validateCodeButton;
//基于定时器实现30秒倒计时效果
function doLoop() {
    validateCodeButton.disabled = true;//将按钮置为不可点击
    nums--;
    if (nums > 0) {
        validateCodeButton.value = nums + '秒后重新获取';
    } else {
        clearInterval(clock); //清除js定时器
        validateCodeButton.disabled = false;
        validateCodeButton.value = '重新获取验证码';
        nums = 30; //重置时间
    }
}

3.1.4. 发送ajax请求

第一步:发送ajax请求

//发送验证码
sendValidateCode(){
    //获取用户输入的手机号
    var telephone = this.orderInfo.telephone;
    //校验手机号输入是否正确
    if (!checkTelephone(telephone)) {
        this.$message.error('请输入正确的手机号');
        return false;
    }
    validateCodeButton = $("#validateCodeButton")[0];
    clock = window.setInterval(doLoop, 1000); //一秒执行一次
    axios.post("/validateCode/send4Order.do?telephone=" + telephone).then((res) => {
            this.$message({
                message: res.data.message,
                type: res.data.flag?"success":"error"
            });
    });
},

在health_common中的MessageConstant中添加提示信息的常量

image-20200629175048497

第二步:创建ValidateCodeController,提供方法发送短信验证码,并将验证码保存到redis

package com.itheima.health.controller;

import com.aliyuncs.exceptions.ClientException;
import com.itheima.health.constant.MessageConstant;
import com.itheima.health.constant.RedisMessageConstant;
import com.itheima.health.entity.Result;
import com.itheima.health.utils.SMSUtils;
import com.itheima.health.utils.ValidateCodeUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

/**
 * Description: No Description
 * User: Eric
 */
@RestController
@RequestMapping("/validateCode")
public class ValidateCodeController {

    @Autowired
    private JedisPool jedisPool;

    /**
     * 发送手机验证码
     *
     * @param telephone
     * @return
     */
    @PostMapping("/send4Order")
    public Result send4Order(String telephone) {
        //- 生成验证码之前要检查一下是否发送过了, 通过redis获取key为手机号码,看是否存在
        Jedis jedis = jedisPool.getResource();
        String key = RedisMessageConstant.SENDTYPE_ORDER + "_" + telephone;
        // redis中的验证码
        String codeInRedis = jedis.get(key);
        if (null == codeInRedis) {
            //- 不存在,没发送,生成验证码,调用SMSUtils.发送验证码,把验证码存入redis(5,10,15分钟有效期),value:验证码, key:手机号码
            Integer code = ValidateCodeUtils.generateValidateCode(6);
            try {
                // 发送验证码
                SMSUtils.sendShortMessage(SMSUtils.VALIDATE_CODE, telephone, code + "");
                // 存入redis,有效时间为15分钟
                jedis.setex(key, 15*60, code + "");
                // 返回成功
                return new Result(true, MessageConstant.SEND_VALIDATECODE_SUCCESS);
            } catch (ClientException e) {
                e.printStackTrace();
                // 发送失败
                return new Result(false, MessageConstant.SEND_VALIDATECODE_FAIL);
            }
        }
        //- 存在:验证码已经发送了,请注意查收
        return new Result(false, MessageConstant.SENT_VALIDATECODE);
    }

}

3.1.5. 日历展示

页面中使用DatePicker控件来展示日历。根据需求,最多可以提前一个月进行体检预约,所以日历控件只展示未来一个月的日期

第一步:引入dataPicker.js

<script src="../plugins/datapicker/datePicker.js"></script>

第二步:定义体检日期

通过样式:.picktime,对应input组件中的class=”picktime”

<div class="date">
    <label>体检日期</label>
    <i class="icon-date" class="picktime"></i>
    <input v-model="orderInfo.orderDate" type="text" class="picktime" readonly>
</div>

第三步:定义日期控件

.picktime表示通过样式查找输入框。

<script>
    //日期控件
    var calendar = new datePicker();
    calendar.init({
        'trigger': '.picktime',/*按钮选择器,用于触发弹出插件*/
        'type': 'date',/*模式:date日期;datetime日期时间;time时间;ym年月;*/
        'minDate': getSpecifiedDate(new Date(),1),/*最小日期*/
        'maxDate': getSpecifiedDate(new Date(),30),/*最大日期*/
        'onSubmit': function() { /*确认时触发事件*/
            //var theSelectData = calendar.value;
        },
        'onClose': function() { /*取消时触发事件*/ }
    });
</script>

其中getSpecifiedDate方法定义在healthmobile.js文件中

//获得指定日期后指定天数的日期
function getSpecifiedDate(date,days) {
    date.setDate(date.getDate() + days);//获取指定天之后的日期
    var year = date.getFullYear();
    var month = date.getMonth() + 1;
    var day = date.getDate();
    return (year + "-" + month + "-" + day);
}

img

3.1.6. 提交预约请求(身份证校验)

为提交预约按钮绑定事件

第一步:定义“体检预约”

<div class="box-button">
    <button @click="submitOrder()" type="button" class="btn order-btn">提交预约</button>
</div>

第二步:submitOrder()方法

//提交预约
submitOrder(){
    //校验身份证号格式
    if(!checkIdCard(this.orderInfo.idCard)){
        this.$message.error('身份证号码输入错误,请重新输入');
        return ;
    }
    axios.post("/order/submit.do",this.orderInfo).then((response) => {
        if(response.data.flag){
            //预约成功,跳转到预约成功页面
            window.location.href="orderSuccess.html?orderId=" + response.data.data.id;
        }else{
            //预约失败,提示预约失败信息
            this.$message.error(response.data.message);
        }
    });
}

第三步:其中checkIdCard方法是在healthmobile.js文件中定义的,用来验证身份证的js

/**
 * 身份证号码校验
 * 身份证号码为15位或者18位,15位时全为数字,18位前17位为数字,最后一位是校验位,可能为数字或字符X
 */
function checkIdCard(idCard){
    var reg = /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/;
    if(reg.test(idCard)){
        return true;
    }else{
        return false;
    }
}

3.2. 后台代码

3.2.1. Controller

在health_mobile工程中创建OrderMobileController并提供submit方法

package com.itheima.health.controller;

import com.alibaba.dubbo.config.annotation.Reference;
import com.itheima.health.constant.MessageConstant;
import com.itheima.health.constant.RedisMessageConstant;
import com.itheima.health.entity.Result;
import com.itheima.health.pojo.Order;
import com.itheima.health.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

import java.util.Map;

/**
 * Description: No Description
 * User: Eric
 */
@RestController
@RequestMapping("/order")
public class OrderMobileController {

    @Autowired
    private JedisPool jedisPool;

    @Reference
    private OrderService orderService;

    /**
     * 提交 预约
     * @param paraMap
     * @return
     */
    @PostMapping("/submit")
    public Result submit(@RequestBody Map<String,String> paraMap){
        // 验证码校验
        Jedis jedis = jedisPool.getResource();
        // 手机号码
        String telephone = paraMap.get("telephone");
        // 验证码的 key
        String key = RedisMessageConstant.SENDTYPE_ORDER + ":" + telephone;
        // redis中的验证码
        String codeInRedis = jedis.get(key);
        if(StringUtils.isEmpty(codeInRedis)){
            //没值 重新发送
            return new Result(false, "请重新获取验证码!");
        }
        // 前端传的验证码
        String validateCode = paraMap.get("validateCode");
        if(!codeInRedis.equals(validateCode)){
            return new Result(false, MessageConstant.VALIDATECODE_ERROR);
        }
        // 移除redis中的验证码,防止重复提交,
        jedis.del(key);// 测试时可注释掉
        // 设置预约的类型
        paraMap.put("orderType",Order.ORDERTYPE_WEIXIN);
        // 预约成功页面展示时需要用到id
        Order order = orderService.submitOrder(paraMap);
        return new Result(true, MessageConstant.ORDER_SUCCESS, order);
    }
}

3.2.2. 服务接口

在health_interface工程中创建体检预约服务接口OrderService并提供预约方法

package com.itheima.health.service;

import com.itheima.health.exception.HealthException;
import com.itheima.health.pojo.Order;

import java.util.Map;

/**
 * Description: No Description
 * User: Eric
 */
public interface OrderService {
    /**
     * 提交预约
     * @param orderInfo
     */
    Order submitOrder(Map<String, String> orderInfo) throws HealthException;
}

3.2.3. 服务实现类

在health_service工程中创建体检预约服务实现类OrderServiceImpl并实现体检预约方法。

体检预约方法处理逻辑比较复杂,需要进行如下业务处理:

【路径】

1、检查用户所选择的预约日期是否已经提前进行了预约设置,如果没有设置则无法进行预约

2、检查用户所选择的预约日期是否已经约满,如果已经约满则无法预约

3、检查用户是否重复预约(同一个用户在同一天预约了同一个套餐),如果是重复预约则无法完成再次预约

4、检查当前用户是否为会员,如果是会员则直接完成预约,如果不是会员则自动完成注册并进行预约

5、预约成功,更新当日的已预约人数

实现代码如下:

/**
 * 提交预约
 * @param paraMap
 * @return
 */
@Override
@Transactional
public Order submitOrder(Map<String, String> paraMap) throws HealthException {
    //1. 通过日期查询预约设置是否存在 t_ordersetting
    String orderDateStr = paraMap.get("orderDate");
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
    Date orderDate = null;
    try {
        orderDate = sdf.parse(orderDateStr);
    } catch (ParseException e) {
        //e.printStackTrace();
        throw new HealthException("日期格式错误");
    }
    OrderSetting orderSetting = orderSettingDao.findByOrderDate(orderDate);
    if(null == orderSetting) {
        //不存在就报错
        throw new HealthException("所选日期,不能预约");
    }
    //存在 则判断是否约满
    if(orderSetting.getReservations() >= orderSetting.getNumber()) {
        //   full 报错
        throw new HealthException("所选日期,预约已满");
    }
    //2. 是否为会员 通过手机号码查询 t_member
    String telephone = paraMap.get("telephone");
    Member member = memberDao.findByTelephone(telephone);
    if(null == member) {
        //非会员
        //   添加到会员表 获取id
        member = new Member();
        member.setIdCard(paraMap.get("idCard"));
        member.setName(paraMap.get("name"));
        member.setPhoneNumber(telephone);
        member.setRegTime(new Date());
        member.setSex(paraMap.get("sex"));

        // 添加会员, 获取id
        memberDao.add(member);
    }
    //3. 判断是否重复预约 t_order 通过member_id,orderDate, setmeal_id
    Order order = new Order();
    order.setMemberId(member.getId());
    order.setOrderDate(orderDate);
    order.setSetmealId(Integer.valueOf(paraMap.get("setmealId")));
    List<Order> orderList = orderDao.findByCondition(order);

    if(null != orderList && orderList.size() > 0){
        //重复则报错
        throw new HealthException("不能重复预约");
    }
    //没重复
    //可预约,添加订单
    order.setOrderType(paraMap.get("orderType"));
    order.setOrderStatus(Order.ORDERSTATUS_NO);
    orderDao.add(order);
    //4. 更新已预约数量 t_ordersetting
    orderSettingDao.editReservationsByOrderDate(orderSetting);
    return order;
}

导入DateUtils放置到health_common中

img

  • 使用将一个字符串转换成日期。

3.2.4. Dao接口

3.2.4.1. OrderSettingDao.java

image-20200629174225702

3.2.4.2. MemberDao 与OrderDao

复制资料中的MemberDao.java 和OrderDao.java到 health_dao下

image-20200629174441080

3.2.5. Mapper映射文件

3.2.5.1. OrderSettingDao.xml

<!--更新已预约人数-->
<update id="editReservationsByOrderDate" parameterType="date">
    update t_ordersetting set reservations = reservations+1 where orderDate = #{orderDate} and number>reservations
</update>

3.2.5.2. MemberDao.xml和OrderDao.xml

image-20200629174549516

修改包名,添加health包路径,另外把所有的resultType、parameterType中的全限定名去掉包名,只留下类名即可

image-20200629174711353

image-20200629174803554

测试:如果出现以下异常

img

说明t_order表的id没有设置自增长,可以设置:

img

创建表时设置主键自增长(主键必须是整型才可以自增长):

CREATE TABLE `t_order` (

  `id` int(11) NOT NULL AUTO_INCREMENT,

  `member_id` int(11) DEFAULT NULL COMMENT '员会id',

  `orderDate` date DEFAULT NULL COMMENT '约预日期',

  `orderType` varchar(8) DEFAULT NULL COMMENT '约预类型 电话预约/微信预约',

  `orderStatus` varchar(8) DEFAULT NULL COMMENT '预约状态(是否到诊)',

  `setmeal_id` int(11) DEFAULT NULL COMMENT '餐套id',

  PRIMARY KEY (`id`),

  KEY `key_member_id` (`member_id`),

  KEY `key_setmeal_id` (`setmeal_id`),

  CONSTRAINT `key_member_id` FOREIGN KEY (`member_id`) REFERENCES `t_member` (`id`),

  CONSTRAINT `key_setmeal_id` FOREIGN KEY (`setmeal_id`) REFERENCES `t_setmeal` (`id`)

) ENGINE=InnoDB AUTO_INCREMENT=18 DEFAULT CHARSET=utf8;

l修改表时设置主键自增长:

ALTER TABLE t_order CHANGE id id INT AUTO_INCREMENT;

l修改表时删除主键自增长:

ALTER TABLE t_order CHANGE id id INT;

或者:选择表—>更改表—>修改表时设置主键自增长:

img

【小结】

一:验证码

  1. 调用阿里服务, 发送成功后, 验证码存到Redis(存5,10,15分钟)
  2. 用户点击了预约, 需要把用户输入的验证码和redis里面存的验证码进行校验

二:预约业务

  1. 判断当前的日期是否可以预约(t_ordersetting表)
  2. 判断当前的日期预约是否已满(t_ordersetting表)
  3. 判断是否 是会员(t_member表)
    • 如果是会员, 避免重复预约(t_order表)
    • 不是会员, 自动注册成会员,t_member 表插入一条记录(t_member表)
  4. 进行预约
    • 向t_order 表插入一条记录(t_order表)
    • 向t_ordersetting更新reservations+1(t_ordersetting表)

4. 预约成功页面展示

【目标】

前面已经完成了体检预约,预约成功后页面会跳转到成功提示页面(orderSuccess.html)并展示预约的相关信息(体检人、体检套餐、体检时间等)。

orderSuccess.html

img

跳转到orderSuccess.html,传递预约成功的订单id。

axios.post("/order/submit.do",this.orderInfo).then((response) => {
    if(response.data.flag){
        //预约成功,跳转到预约成功页面
        window.location.href="orderSuccess.html?orderId=" + response.data.data.id;
    }else{
        //预约失败,提示预约失败信息
        this.$message.error(response.data.message);
    }
});

【路径】

前台代码编写

  1. 在/pages/orderSuccess.html

    体检人: t_member
    体检套餐: t_setmeal
    体检日期: t_order
    预约类型: t_order

完成需求:
1.页面输出订单相关人的信息
2.使用订单id,查询订单详细信息。存放到orderInfo的模型中。

页面加载时发送请求,提交订单的id,获取结果绑定orderInfo {member:, setmeal:, orderDate:, orderType:}

后台代码编写:

​ 1.类OrderController.java

​ 2.类OrderService.java

​ 3.类OrderServiceImpl.java

​ 4.类OrderDao.java

​ 5.配置文件OrderDao.xml

select m.name member,s.name setmeal,o.orderDate, o.orderType From t_member m, t_setmeal s, t_order o
where s.id=o.setmeal_id and m.id=o.member_id and o.id=#{orderId}

【讲解】

4.1. 页面调整

提供orderSuccess.html页面,展示预约成功后相关信息

第一步:页面输出订单相关人的信息

<div class="info-title">
    <span class="name">体检预约成功</span>
</div>
<div class="notice-item">
    <div class="item-title">预约信息</div>
    <div class="item-content">
        <p>体检人:{{orderInfo.member}}</p>
        <p>体检套餐:{{orderInfo.setmeal}}</p>
        <p>体检日期:{{orderInfo.orderDate}}</p>
        <p>预约类型:{{orderInfo.orderType}}</p>
    </div>
</div>

第二步:使用订单id,查询订单详细信息。存放到orderInfo的变量中。

<script>
    var vue = new Vue({
        el:'#app',
        data:{
            orderInfo:{}
        },
        mounted(){
            axios.get("/order/findById.do?id=" + id).then((res) => {
                if(res.data.flag){
                    var orderInfo = res.data.data;
                    // 去掉时分秒
                    orderInfo.orderDate=orderInfo.orderDate.substring(0,10);
                    this.orderInfo = orderInfo;
                }else{
                    alert(res.data.message);
                }
            });
        }
    });
</script>

4.2. 后台代码

4.2.1. Controller

在OrderController中提供findById方法,根据预约id查询预约相关信息

/**
 * 预约成功展示
 */
@GetMapping("/findById")
public Result findById(int id){
    // 调用服务通过id查询订单信息
    Map<String,Object> orderInfo = orderService.findOrderDetailById(id);
    return new Result(true, MessageConstant.QUERY_ORDER_SUCCESS,orderInfo);
}

4.2.2. 服务接口

在OrderService服务接口中扩展findOrderDetailById方法

/**
 * 通过订单id查询预约信息
 * @param id
 * @return
 */
Map<String, Object> findOrderDetailById(int id);

4.2.3. 服务实现类

在OrderServiceImpl服务实现类中实现findOrderDetailById方法

/**
 * 通过订单id查询预约信息
 * @param id
 * @return
 */
@Override
public Map<String, Object> findOrderDetailById(int id) {
    return orderDao.findById4Detail(id);
}

4.2.4. Dao接口

在OrderDao接口中扩展findById4Detail方法

Map findById4Detail(Integer id);

4.2.5. Mapper映射文件

在OrderDao.xml映射文件中提供SQL语句

<!--根据预约id查询预约信息,包括体检人信息、套餐信息-->
<select id="findById4Detail" parameterType="int" resultType="map">
    select m.name member ,s.name setmeal,o.orderDate orderDate,o.orderType orderType
    from
    t_order o,
    t_member m,
    t_setmeal s
    where o.member_id=m.id and o.setmeal_id=s.id and o.id=#{id}
</select>

m.name member ,s.name setmeal,o.orderDate orderDate,o.orderType orderType

对应:

页面内容

<p>体检人:{{orderInfo.member}}</p>
<p>体检套餐:{{orderInfo.setmeal}}</p>
<p>体检日期:{{orderInfo.orderDate}}</p>
<p>预约类型:{{orderInfo.orderType}}</p>

【小结】

前端中需要什么样的数据,来源于哪些表中的字段,找出表与表之间的关系 写出sql语句

再分析前端的数据格式,后端返回前端需要的数据格式。

mybatis可以返回map数据类型,通过给列名取别名方式满足前端数据格式需求

报表步骤

  1. 找出要展示的数据所在的表 t_member t_setmeal t_order
  2. 找出条件所在的表 t_order.id
  3. 找出数据表之间的表关系,如果没有则找中间表
  4. 找出条件表之间的关系,如果没有则找中间表
  5. 找出数据与条件表之间的关系,如果没有则找中间表

5. 手机快速登录

【需求分析】

手机快速登录功能,就是通过短信验证码的方式进行登录。这种方式相对于用户名密码登录方式,用户不需要记忆自己的密码,只需要通过输入手机号并获取验证码就可以完成登录,是目前比较流行的登录方式。

img

【目标】

实现手机快速登录

【路径】

(1)发送验证码

  1. 获得用户输入的手机号码
  2. 先判断 是否已经发送过了。否则生成动态验证码(4或者6位)
  3. 使用阿里云发送短信验证码
  4. 把验证码存到redis(存5分钟), 防止用户重复发送验证码,登陆时验证前端输入的是否与redis的一致

(2)登录

  1. 获得用户输入的信息(Map)
  2. 取出redis里面的验证码和用户输入的验证码进行校验
  3. 如果校验通过
    • 判断是否是会员, 不是会员, 自动注册为会员
    • 保存用户的登录状态, 这里只存手机号码(OPEN auth 2.0 或者自己手动签发token ,我们这里可以使用Cookie存储用户_手机号码)

【讲解】

5.1. 前台代码

登录页面为/pages/login.html

5.1.1. 发送验证码

(1)为获取验证码按钮绑定事件,并在事件对应的处理函数中校验手机号,如果手机号输入正确则显示30秒倒计时效果并发送ajax请求,发送短信验证码

<div class="input-row">
    <label>手机号</label>
    <div class="loginInput">
        <input v-model="loginInfo.telephone" id='account' type="text" placeholder="请输入手机号">
        <input id="validateCodeButton" @click="sendValidateCode()" type="button" style="font-size: 12px" value="获取验证码">
    </div>
</div>

(2)调用sendValidateCode()方法

//发送验证码
sendValidateCode(){
    var telephone = this.loginInfo.telephone;
    if (!checkTelephone(telephone)) {
        this.$message.error('请输入正确的手机号');
        return false;
    }
    validateCodeButton = $("#validateCodeButton")[0];
    clock = window.setInterval(doLoop, 1000); //一秒执行一次
    axios.post("/validateCode/send4Login.do?telephone=" + telephone).then((res) => {
        this.$message({
            message: res.data.message,
            type: res.data.flag?"success":"error"
        })
    });
},

(3)在ValidateCodeController中提供send4Login方法,调用短信服务发送验证码并将验证码保存到redis

注意:存放到redis的可以值:手机号+002(RedisMessageConstant.SENDTYPE_LOGIN)

/**
 * 发送登陆手机验证码
 *
 * @param telephone
 * @return
 */
@PostMapping("/send4Login")
public Result send4Login(String telephone) {
    //- 生成验证码之前要检查一下是否发送过了, 通过redis获取key为手机号码,看是否存在
    Jedis jedis = jedisPool.getResource();
    String key = RedisMessageConstant.SENDTYPE_LOGIN + "_" + telephone;
    // redis中的验证码
    String codeInRedis = jedis.get(key);
    if (null == codeInRedis) {
        //- 不存在,没发送,生成验证码,调用SMSUtils.发送验证码,把验证码存入redis(5,10,15分钟有效期),value:验证码, key:手机号码
        Integer code = ValidateCodeUtils.generateValidateCode(6);
        try {
            // 发送验证码
            SMSUtils.sendShortMessage(SMSUtils.VALIDATE_CODE, telephone, code + "");
            // 存入redis,有效时间为15分钟
            jedis.setex(key, 15*60, code + "");
            // 返回成功
            return new Result(true, MessageConstant.SEND_VALIDATECODE_SUCCESS);
        } catch (ClientException e) {
            e.printStackTrace();
            // 发送失败
            return new Result(false, MessageConstant.SEND_VALIDATECODE_FAIL);
        }
    }
    //- 存在:验证码已经发送了,请注意查收
    return new Result(false, MessageConstant.SENT_VALIDATECODE);
}

5.1.2. 提交登录请求

(1)为登录按钮绑定事件

<div class="btn yes-btn"><a @click="login()" href="#">登录</a></div>

(2)点击登录,login方法:

//登录
login(){
    var telephone = this.loginInfo.telephone;
    if (!checkTelephone(telephone)) {
        this.$message.error('请输入正确的手机号');
        return false;
    }
    axios.post("/login/check.do",this.loginInfo).then((response) => {
        if(response.data.flag){
            //登录成功,跳转到index.html
            window.location.href="index.html";
        }else{
            //失败,提示失败信息
            this.$message.error(response.data.message);
        }
    });
}

5.2. 后台代码

5.2.1. Controller

在health_mobile工程中创建LoginController并提供check方法进行登录检查,处理逻辑为:

【路径】

1、校验用户输入的短信验证码是否正确,如果验证码错误则登录失败

2、如果验证码正确,则判断当前用户是否为会员,如果不是会员则自动完成会员注册

3、向客户端写入Cookie,内容为用户手机号

package com.itheima.health.controller;

import com.alibaba.dubbo.config.annotation.Reference;
import com.itheima.health.constant.MessageConstant;
import com.itheima.health.constant.RedisMessageConstant;
import com.itheima.health.entity.Result;
import com.itheima.health.pojo.Member;
import com.itheima.health.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import java.util.Date;
import java.util.Map;

/**
 * Description: No Description
 * User: Eric
 */
@RestController
@RequestMapping("/login")
public class LoginController {

    @Autowired
    private JedisPool jedisPool;

    @Reference
    private MemberService memberService;

    @PostMapping("/check")
    public Result checkMember(@RequestBody Map<String,String> loginInfo, HttpServletResponse res){
        String telephone = loginInfo.get("telephone");
        String validateCode = loginInfo.get("validateCode");
        // 验证码的验证
        String key = RedisMessageConstant.SENDTYPE_LOGIN + "_" + telephone;
        Jedis jedis = jedisPool.getResource();
        // 获取 redis中的验证码
        String codeInRedis = jedis.get(key);
        if(null == codeInRedis){
            // 失效或没有发送
            return new Result(false, "请点击发送验证码");
        }
        if(!codeInRedis.equals(validateCode)){
            return new Result(false, "验证码不正确");
        }
        jedis.del(key);// 清除验证码,已经使用过了

        // 判断是否为会员
        Member member = memberService.findByTelephone(telephone);
        if(null == member){
            // 会员不存在,添加为新会员
            member = new Member();
            member.setRegTime(new Date());
            member.setPhoneNumber(telephone);
            member.setRemark("手机快速注册");
            memberService.add(member);
        }
        // 跟踪记录的手机号码,代表着会员
        Cookie cookie = new Cookie("login_member_telephone",telephone);
        cookie.setMaxAge(30*24*60*60); // 存1个月
        cookie.setPath("/"); // 访问的路径 根路径下时 网站的所有路径 都会发送这个cookie
        res.addCookie(cookie);
        return new Result(true, MessageConstant.LOGIN_SUCCESS);
    }

}

5.2.2. 服务接口

在MemberService服务接口中提供findByTelephone和add方法

package com.itheima.health.service;

import com.itheima.health.pojo.Member;

/**
 * Description: No Description
 * User: Eric
 */
public interface MemberService {
    /**
     * 通过手机号码查询会员信息
     * @param telephone
     * @return
     */
    Member findByTelephone(String telephone);

    /**
     * 添加会员
     * @param member
     */
    void add(Member member);
}

5.2.3. 服务实现类

在MemberServiceImpl服务实现类中实现findByTelephone和add方法

【路径】

1:使用手机号查询会议

2:新增会员

package com.itheima.health.service.impl;

import com.alibaba.dubbo.config.annotation.Service;
import com.itheima.health.dao.MemberDao;
import com.itheima.health.pojo.Member;
import com.itheima.health.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;

/**
 * Description: No Description
 * User: Eric
 */
@Service(interfaceClass = MemberService.class)
public class MemberServiceImpl implements MemberService {

    @Autowired
    private MemberDao memberDao;

    /**
     * 通过手机号码查询会员信息
     * @param telephone
     * @return
     */
    @Override
    public Member findByTelephone(String telephone) {
        return memberDao.findByTelephone(telephone);
    }

    /**
     * 添加会员
     * @param member
     */
    @Override
    public void add(Member member) {
        memberDao.add(member);
    }
}

5.2.4. Dao接口(已完成)

在MemberDao接口中声明findByTelephone和add方法

public void add(Member member);
public Member findByTelephone(String telephone);

5.2.5. Mapper映射文件(已完成)

在MemberDao.xml映射文件中定义SQL语句

<!--新增会员-->
<insert id="add" parameterType="com.itheima.health.pojo.Member">
    <selectKey resultType="java.lang.Integer" order="AFTER" keyProperty="id">
        SELECT LAST_INSERT_ID()
    </selectKey>
    insert into
    t_member
    (fileNumber,name,sex,idCard,phoneNumber,
    regTime,password,email,birthday,remark)
    values
    (#{fileNumber},#{name},#{sex},#{idCard},#{phoneNumber},
    #{regTime},#{password},#{email},#{birthday},#{remark})
</insert>
<!--根据手机号查询会员-->
<select id="findByTelephone" parameterType="string" resultType="com.itheima.health.pojo.Member">
    select * from t_member where phoneNumber = #{phoneNumber}
</select>

登录成功之后可以查看浏览器Cookie是否写入成功。

img

【小结】

  1. 发送验证码 存入小抄
    • 获得用户输入的手机号码
    • 生成验证码
    • 阿里云发送验证码
    • 把验证码存到redis(5分钟)
  2. 登录
    • 获得用户输入的信息(Map)
    • 取出redis里面存的验证码和用户输入的验证码进行比较 存入小抄
    • 判断是否是会员
      • 不是会员, 自动注册为会员
  3. Cookie跟踪