1. 移动端开发
1.1 移动端开发方式
随着移动互联网的兴起和手机的普及,目前移动端应用变得愈发重要,成为了各个商家的必争之地。例如,我们可以使用手机购物、支付、打车、玩游戏、订酒店、购票等,以前只能通过PC端完成的事情,现在通过手机都能够实现,而且更加方便,而这些都需要移动端开发进行支持,那如何进行移动端开发呢?
移动端开发主要有三种方式:
- 基于手机API开发(原生APP)
- 基于手机浏览器开发(移动web)
- 混合开发(混合APP)
1.2 微信公众号开发
微信公众号开发:https://blog.csdn.net/ZGL_cyy/article/details/111936201
2. 需求分析和环境搭建
2.1 需求分析
用户在体检之前需要进行预约,可以通过电话方式进行预约,此时会由体检中心客服人员通过后台系统录入预约信息。用户也可以通过手机端自助预约。本章节开发的功能为用户通过手机自助预约。
预约流程如下:
- 访问移动端首页
- 点击体检预约进入体检套餐列表页面
- 在体检套餐列表页面点击具体套餐进入套餐详情页面
- 在套餐详情页面点击立即预约进入预约页面
- 在预约页面录入体检人相关信息点击提交预约
效果如下图:
2.2 搭建移动端工程
本项目是基于SOA架构进行开发,前面我们已经完成了后台系统的部分功能开发,在后台系统中都是通过Dubbo调用服务层发布的服务进行相关的操作。本章节我们开发移动端工程也是同样的模式,所以我们也需要在移动端工程中通过Dubbo调用服务层发布的服务,如下图:
2.2.1 导入maven坐标
在health_common工程的pom.xml文件中导入阿里短信发送的maven坐标
<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.2.2 导入通用组件
在health_common
工程中导入如下通用组件
ValidateCodeUtils工具类:
package com.oldlu.utils; import java.util.Random; /** * 随机生成验证码工具类 */ public class ValidateCodeUtils { /** * 随机生成验证码 * @param length 长度为4位或者6位 * @return */ public static Integer generateValidateCode(int length){ Integer code =null; if(length == 4){ code = new Random().nextInt(9999);//生成随机数,最大为9998 if(code < 1000){ code = code + 1000;//保证随机数为4位数字 } }else if(length == 6){ code = new Random().nextInt(999999);//生成随机数,最大为999998 if(code < 100000){ code = code + 100000;//保证随机数为6位数字 } }else{ throw new RuntimeException("只能生成4位或6位数字验证码"); } return code; } /** * 随机生成指定长度字符串验证码 * @param length 长度 * @return */ public static String generateValidateCode4String(int length){ Random rdm = new Random(); String hash1 = Integer.toHexString(rdm.nextInt()); String capstr = hash1.substring(0, length); return capstr; } }
SMSUtils工具类:
package com.oldlu.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_159620392";//发送短信验证码 public static final String ORDER_NOTICE = "SMS_159771588";//体检预约成功通知 /** * 发送短信 * @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", accessKeyId, accessKeySecret); 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("XX健康"); // 必填:短信模板-可在短信控制台中找到 request.setTemplateCode(templateCode); // 可选:模板中的变量替换JSON串,如模板内容为"亲爱的${name},您的验证码为${code}"时,此处的值为 // 友情提示:如果JSON中需要带换行符,请参照标准的JSON协议对换行符的要求,比如短信内容中包含\r\n的情况在JSON中需要表示成\\r\\n,否则会导致JSON在服务端解析失败 request.setTemplateParam("{\"code\":\""+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("请求成功"); } } }
RedisMessageConstant常量类:
package com.oldlu.constant; public class RedisMessageConstant { public static final String SENDTYPE_ORDER = "001";//用于缓存体检预约时发送的验证码 public static final String SENDTYPE_LOGIN = "002";//用于缓存手机号快速登录时发送的验证码 public static final String SENDTYPE_GETPWD = "003";//用于缓存找回密码时发送的证码 }
2.2.3 health_mobile
创建移动端工程health_mobile,打包方式为war,用于存放Controller,在Controller中通过Dubbo可以远程访问服务层相关服务,所以需要依赖health_interface接口工程。
pom.xml:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>health_parent</artifactId> <groupId>com.oldlu</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>health_mobile</artifactId> <packaging>war</packaging> <name>health_mobile Maven Webapp</name> <!-- FIXME change it to the project's website --> <url>http://www.example.com</url> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>com.oldlu</groupId> <artifactId>health_interface</artifactId> <version>1.0-SNAPSHOT</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.tomcat.maven</groupId> <!-- tomcat7的插件, 不同tomcat版本这个也不一样 --> <artifactId>tomcat7-maven-plugin</artifactId> <version>2.1</version> <configuration> <!-- 通过maven tomcat7:run运行项目时,访问项目的端口号 --> <port>84</port> <!-- 项目访问路径 本例:localhost:9090, 如果配置的aa, 则访问路径为localhost:9090/aa--> <path>/</path> </configuration> </plugin> </plugins> </build> </project>
静态资源(CSS、html、img等,详见资料):
web.xml:
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd" > <web-app> <display-name>Archetype Created Web Application</display-name> <!-- 解决post乱码 --> <filter> <filter-name>CharacterEncodingFilter</filter-name> <filterclass>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>utf-8</param-value> </init-param> <init-param> <param-name>forceEncoding</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>CharacterEncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <servlet> <servlet-name>springmvc</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servletclass> <!-- 指定加载的配置文件 ,通过参数contextConfigLocation加载 --> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:springmvc.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>springmvc</servlet-name> <url-pattern>*.do</url-pattern> </servlet-mapping> <welcome-file-list> <welcome-file>/pages/index.html</welcome-file> </welcome-file-list> </web-app>
springmvc.xml:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xmlns:context="http://www.springframework.org/schema/context" xmlns:dubbo="http://code.alibabatech.com/schema/dubbo" xmlns:mvc="http://www.springframework.org/schema/mvc" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <mvc:annotation-driven> <mvc:message-converters register-defaults="true"> <bean class="com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter"> <property name="supportedMediaTypes" value="application/json"/> <property name="features"> <list> <value>WriteMapNullValue</value> <value>WriteDateUseDateFormat</value> </list> </property> </bean> </mvc:message-converters> </mvc:annotation-driven> <!-- 指定应用名称 --> <dubbo:application name="health_mobile"/> <!--指定服务注册中心地址--> <dubbo:registry address="zookeeper://47.100.63.146:2181"/> <!--批量扫描--> <dubbo:annotation package="com.oldlu.controller"/> <!-- 超时全局设置 10分钟 check=false 不检查服务提供方,开发阶段建议设置为false check=true 启动时检查服务提供方,如果服务提供方没有启动则报错 --> <dubbo:consumer timeout="600000" check="false"/> <import resource="spring-redis.xml"></import> </beans>
spring-redis.xml:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xmlns:context="http://www.springframework.org/schema/context" xmlns:dubbo="http://code.alibabatech.com/schema/dubbo" xmlns:mvc="http://www.springframework.org/schema/mvc" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <context:property-placeholder location="classpath:redis.properties"/> <!--Jedis连接池的相关配置--> <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig"> <property name="maxTotal"> <value>${redis.pool.maxActive}</value> </property> <property name="maxIdle"> <value>${redis.pool.maxIdle}</value> </property> <property name="testOnBorrow" value="true"/> <property name="testOnReturn" value="true"/> </bean> <bean id="jedisPool" class="redis.clients.jedis.JedisPool"> <constructor-arg name="poolConfig" ref="jedisPoolConfig"/> <constructor-arg name="host" value="${redis.host}"/> <constructor-arg name="port" value="${redis.port}" type="int"/> <constructor-arg name="timeout" value="${redis.timeout}" type="int"/> <constructor-arg name="password" value="${redis.password}"/> </bean> </beans>
redis.properties:
#最大分配的对象数 redis.pool.maxActive=200 #最大能够保持idel状态的对象数 redis.pool.maxIdle=50 redis.pool.minIdle=10 redis.pool.maxWaitMillis=20000 #当池内没有返回对象时,最大等待时间 redis.pool.maxWait=300 #格式:redis://:[密码]@[服务器地址]:[端口]/[db index] #redis.uri = redis://:12345@127.0.0.1:6379/0 redis.host = 127.0.0.1 redis.port = 6379 redis.timeout = 30000 redis.password=* 1234567891011121314
log4j.properties:
### direct log messages to stdout ### log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.Target=System.err log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L - %m%n ### direct messages to file mylog.log ### log4j.appender.file=org.apache.log4j.FileAppender log4j.appender.file.File=c:\\mylog.log log4j.appender.file.layout=org.apache.log4j.PatternLayout log4j.appender.file.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L - %m%n ### set log levels - for more verbose logging change 'info' to 'debug' ### log4j.rootLogger=info, stdout
3. 套餐列表页面动态展示
移动端首页为/pages/index.html,效果如下:
点击体检预约直接跳转到体检套餐列表页面(/pages/setmeal.html)
3.1 完善页面
3.1.1 展示套餐信息
<ul class="list"> <li class="list-item" v-for="setmeal in setmealList"> <a class="link-page" :href="'setmeal_detail.html?id='+setmeal.id"> <img class="img-object f-left" :src="'http://pqjroc654.bkt.clouddn.com/'+setmeal.img" alt=""> <div class="item-body"> <h4 class="ellipsis item-title">{{setmeal.name}}</h4> <p class="ellipsis-more item-desc">{{setmeal.remark}}</p> <p class="item-keywords"> <span>{{setmeal.sex == '0' ? '性别不限' : setmeal.sex == '1' ? '男':'女'}}</span> <span>{{setmeal.age}}</span> </p> </div> </a> </li> </ul>
3.1.2 获取套餐列表数据
var vue = new Vue({ el: '#app', data: { setmealList: [] }, mounted() { //发送ajax请求,获取所有的套餐数据,赋值给setmealList模型数据,用于页面展示 axios.post("/setmeal/getAllSetmeal.do").then((reps) => { if (reps.data.flag) { //查询成功,给模型赋值 this.setmealList = reps.data.data; } else { //查询失败,弹出提示信息 this.$message.error(reps.message) } }) }
3.2 后台代码
3.2.1 Controller
在health_mobile工程中创建SetmealController并提供getSetmeal方法,在此方法中通过Dubbo远程调用套餐服务获取套餐列表数据
package com.oldlu.controller; import com.alibaba.dubbo.config.annotation.Reference; import com.oldlu.constant.MessageConstant; import com.oldlu.entity.Result; import com.oldlu.pojo.Setmeal; import com.oldlu.service.SetmealService; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; /** * 套餐管理 * @author 嘿嘿嘿1212 * @version 1.0 * @date 2019/10/20 11:19 */ @RestController @RequestMapping("/setmeal") public class SetmealController { @Reference private SetmealService setmealService; /** * 查询所有套餐 * @return */ @RequestMapping("/getAllSetmeal") public Result getAllSetmeal() { try { List<Setmeal> list = setmealService.findAll(); return new Result(true, MessageConstant.GET_SETMEAL_LIST_SUCCESS, list); } catch (Exception e) { e.printStackTrace(); return new Result(false, MessageConstant.GET_SETMEAL_LIST_FAIL); } } }
3.2.2 服务接口
在SetmealService服务接口中扩展findAll方法
public List<Setmeal> findAll();
3.2.3 服务实现类
在SetmealServiceImpl服务实现类中实现findAll方法
public List<Setmeal> findAll() { return setmealDao.findAll(); }
3.2.4 Dao接口
在SetmealDao接口中扩展findAll方法
public List<Setmeal> findAll();
3.2.5 Mapper映射文件
在SetmealDao.xml映射文件中扩展SQL语句
<select id="findAll" resultType="com.oldlu.pojo.Setmeal"> select * from t_setmeal </select>
4. 套餐详情页面动态展示
前面我们已经完成了体检套餐列表页面动态展示,点击其中任意一个套餐则跳转到对应的套餐详情页面(/pages/setmeal_detail.html),并且会携带此套餐的id作为参数提交。
请求路径格式:http://localhost/pages/setmeal_detail.html?id=10
在套餐详情页面需要展示当前套餐的信息(包括图片、套餐名称、套餐介绍、适用性别、适用年龄)、此套餐包含的检查组信息、检查组包含的检查项信息等。
4.1 完善页面
4.1.1 获取请求参数中套餐id
在页面中已经引入了healthmobile.js
文件,此文件中已经封装了getUrlParam
方法可以根据URL请求路径中的参数名获取对应的值
f//获取指定的URL参数值 http://localhost/pages/setmeal_detail.html?id=3&name=jack function getUrlParam(paraName) { var url = document.location.toString(); //alert(url); var arrObj = url.split("?"); if (arrObj.length > 1) { var arrPara = arrObj[1].split("&"); var arr; for (var i = 0; i < arrPara.length; i++) { arr = arrPara[i].split("="); if (arr != null && arr[0] == paraName) { return arr[1]; } } return ""; } else { return ""; } }
在setmeal_detail.html中调用上面定义的方法获取套餐id的值
<script> var id = getUrlParam("id"); </script> 123
4.1.2 获取套餐详细信息
var vue = new Vue({ el:'#app', data:{ imgUrl:null,//套餐对应的图片链接 setmeal:{} }, methods:{ toOrderInfo(){ window.location.href = "orderInfo.html?id=" + id; } }, mounted(){ axios.post("/setmeal/findById.do?id=" + id).then((response) => { if(response.data.flag){ this.setmeal = response.data.data; this.imgUrl = 'http://pqjroc654.bkt.clouddn.com/' + this.setmeal.img; } }); } });
4.1.3 展示套餐信息
<!-- 页面内容 --> <div class="contentBox"> <div class="card"> <div class="project-img"> <img :src="imgUrl" width="100%" height="100%" /> </div> <div class="project-text"> <h4 class="tit">{{setmeal.name}}</h4> <p class="subtit">{{setmeal.remark}}</p> <p class="keywords"> <span>{{setmeal.sex == '0' ? '性别不限' : setmeal.sex == '1' ? '男':'女'}}</span> <span>{{setmeal.age}}</span> </p> </div> <!--<div class="project-know"> <a href="orderNotice.html" class="link-page"> <i class="icon-ask-circle"><span class="path1"></span><span class="path2"></span></i> <span class="word">预约须知</span> <span class="arrow"><i class="icon-rit-arrow"></i></span> </a> </div>--> </div> <div class="table-listbox"> <div class="box-title"> <i class="icon-zhen"><span class="path1"></span><span class="path2"></span></i> <span>套餐详情</span> </div> <div class="box-table"> <div class="table-title"> <div class="tit-item flex2">项目名称</div> <div class="tit-item flex3">项目内容</div> <div class="tit-item flex3">项目解读</div> </div> <div class="table-content"> <ul class="table-list"> <li class="table-item" v-for="checkgroup in setmeal.checkGroups"> <div class="item flex2">{{checkgroup.name}}</div> <div class="item flex3"> <label v-for="checkitem in checkgroup.checkItems"> {{checkitem.name}} </label> </div> <div class="item flex3">{{checkgroup.remark}}</div> </li> </ul> </div> <div class="box-button"> <a @click="toOrderInfo()" class="order-btn">立即预约</a> </div> </div> </div> </div>
4.2 后台代码
4.2.1 Controller
在SetmealController中提供findById方法
//根据id查询套餐信息 @RequestMapping("/findById") public Result findById(int id){ try{ Setmeal setmeal = setmealService.findById(id); return new Result(true,MessageConstant.QUERY_SETMEAL_SUCCESS,setmeal); }catch (Exception e){ e.printStackTrace(); return new Result(false,MessageConstant.QUERY_SETMEAL_FAIL); } }
4.2.2 服务接口
在SetmealService服务接口中提供findById方法
public Setmeal findById(int id); 1
4.2.3 服务实现类
在SetmealServiceImpl服务实现类中实现findById方法
public Setmeal findById(int id) { return setmealDao.findById(id); } 123
4.2.4 Dao接口
在SetmealDao接口中提供findById方法
public Setmeal findById(int id); 1
4.2.5 Mapper映射文件
此处会使用mybatis提供的关联查询,在根据id查询套餐时,同时将此套餐包含的检查组都查询出来,并且将检查组包含的检查项都查询出来。
SetmealDao.xml文件:
<resultMap type="com.oldlu.pojo.Setmeal" id="baseResultMap"> <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 type="com.oldlu.pojo.Setmeal" id="findByIdResultMap" extends="baseResultMap"> <collection property="checkGroups" javaType="ArrayList" ofType="com.oldlu.pojo.CheckGroup" column="id" select="com.oldlu.dao.CheckGroupDao.findCheckGroupById"> </collection> </resultMap> <select id="findById" resultMap="findByIdResultMap"> select * from t_setmeal where id=#{id} </select>
CheckGroupDao.xml文件:
<resultMap type="com.oldlu.pojo.CheckGroup" id="baseResultMap"> <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 type="com.oldlu.pojo.CheckGroup" id="findByIdResultMap" extends="baseResultMap"> <collection property="checkItems" javaType="ArrayList" ofType="com.oldlu.pojo.CheckItem" column="id" select="com.oldlu.dao.CheckItemDao.findCheckItemById"> </collection> </resultMap> <!--根据套餐id查询检查项信息--> <select id="findCheckGroupById" resultMap="findByIdResultMap"> select * from t_checkgroup where id in (select checkgroup_id from t_setmeal_checkgroup where setmeal_id=#{id}) </select>
CheckItemDao.xml文件:
<!--根据检查组id查询检查项信息--> <select id="findCheckItemById" resultType="com.oldlu.pojo.CheckItem"> select * from t_checkitem where id in (select checkitem_id from t_checkgroup_checkitem where checkgroup_id=# {id}) </select>
5. 短信发送
5.1 阿里云短信服务介绍
阿里云短信发送服务:https://blog.csdn.net/ZGL_cyy/article/details/111936289