对每一个模块流程的具体介绍
public Map<String, Object> loginUser(LoginVo loginVo) {
//从loginVo获取输入的手机号,和验证码
String phone = loginVo.getPhone();
String code = loginVo.getCode();
//判断手机号和验证码是否为空
if(StringUtils.isEmpty(phone) || StringUtils.isEmpty(code)) {
throw new YyghException(ResultCodeEnum.PARAM_ERROR);
}
//判断手机验证码和输入的验证码是否一致
String redisCode = redisTemplate.opsForValue().get(phone);
if(!code.equals(redisCode)) {
throw new YyghException(ResultCodeEnum.CODE_ERROR);
}
//绑定手机号码
UserInfo userInfo = null;
if(!StringUtils.isEmpty(loginVo.getOpenid())) {
userInfo = this.selectWxInfoOpenId(loginVo.getOpenid());
if(null != userInfo) {
userInfo.setPhone(loginVo.getPhone());
this.updateById(userInfo);
} else {
throw new YyghException(ResultCodeEnum.DATA_ERROR);
}
}
//如果userinfo为空,进行正常手机登录
if(userInfo == null) {
//判断是否第一次登录:根据手机号查询数据库,如果不存在相同手机号就是第一次登录
QueryWrapper<UserInfo> wrapper = new QueryWrapper<>();
wrapper.eq("phone",phone);
userInfo = baseMapper.selectOne(wrapper);
if(userInfo == null) { //第一次使用这个手机号登录
//添加信息到数据库
userInfo = new UserInfo();
userInfo.setName("");
userInfo.setPhone(phone);
userInfo.setStatus(1);
baseMapper.insert(userInfo);
}
}
//校验是否被禁用
if(userInfo.getStatus() == 0) {
throw new YyghException(ResultCodeEnum.LOGIN_DISABLED_ERROR);
}
//不是第一次,直接登录
//返回登录信息
//返回登录用户名
//返回token信息
Map<String, Object> map = new HashMap<>();
String name = userInfo.getName();
if(StringUtils.isEmpty(name)) {
name = userInfo.getNickName();
}
if(StringUtils.isEmpty(name)) {
name = userInfo.getPhone();
}
map.put("name",name);
//jwt生成token字符串
String token = JwtHelper.createToken(userInfo.getId(), name);
map.put("token",token);
return map;
}
base64编码,并不是加密,只是把明文信息变成了不可见的字符串。但是其实只要用一些工具就可以把base64编码解成明文,所以不要在JWT中放入涉及私密的信息。
由id+name得到token,且能由token反向得到name和id
private static long tokenExpiration = 24*60*60*1000;
//签名秘钥
private static String tokenSignKey = "123456";
//根据参数生成token
public static String createToken(Long userId, String userName) {
String token = Jwts.builder()
.setSubject("YYGH-USER")
.setExpiration(new Date(System.currentTimeMillis() + tokenExpiration))
.claim("userId", userId)
.claim("userName", userName)
.signWith(SignatureAlgorithm.HS512, tokenSignKey)
.compressWith(CompressionCodecs.GZIP)
.compact();
return token;
}
wx.open.app_id=wxed9954c01bb89b47
wx.open.app_secret=a7482517235173ddb4083788de60b90e
wx.open.redirect_url=http://guli.shop/api/ucenter/wx/callback
yygh.baseUrl=http://localhost:3000
生成扫描二维码 (参考文档)
//1 生成微信扫描二维码
//返回生成二维码需要参数
@GetMapping("getLoginParam")
@ResponseBody
public Result genQrConnect() {
try {
Map<String, Object> map = new HashMap<>();
map.put("appid", ConstantWxPropertiesUtils.WX_OPEN_APP_ID);
map.put("scope","snsapi_login");
String wxOpenRedirectUrl = ConstantWxPropertiesUtils.WX_OPEN_REDIRECT_URL;
wxOpenRedirectUrl = URLEncoder.encode(wxOpenRedirectUrl, "utf-8");
map.put("redirect_uri",wxOpenRedirectUrl);
map.put("state",System.currentTimeMillis()+"");
return Result.ok(map);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
return null;
}
}
回调函数,扫码后的返回内容
扫码后的过程:
回调方法的逻辑:
access_token token凭证
openid 微信的唯一id,判断用户是否存在
在第三步完成后,还需要绑定手机号,返回token,跳转到前端页面
跳转前端页面:携带token,openid重定向到前端页面
//微信扫描后回调的方法
@GetMapping("callback")
public String callback(String code,String state) {
//第一步 获取临时票据 code
System.out.println("code:"+code);
//第二步 拿着code和微信id和秘钥,请求微信固定地址 ,得到两个值
//使用code和appid以及appscrect换取access_token
// %s 占位符
StringBuffer baseAccessTokenUrl = new StringBuffer()
.append("https://api.weixin.qq.com/sns/oauth2/access_token")
.append("?appid=%s")
.append("&secret=%s")
.append("&code=%s")
.append("&grant_type=authorization_code");
String accessTokenUrl = String.format(baseAccessTokenUrl.toString(),
ConstantWxPropertiesUtils.WX_OPEN_APP_ID,
ConstantWxPropertiesUtils.WX_OPEN_APP_SECRET,
code);
//使用httpclient请求这个地址
try {
String accesstokenInfo = HttpClientUtils.get(accessTokenUrl);
System.out.println("accesstokenInfo:"+accesstokenInfo);
//从返回字符串获取两个值 openid 和 access_token
JSONObject jsonObject = JSONObject.parseObject(accesstokenInfo);
String access_token = jsonObject.getString("access_token");
String openid = jsonObject.getString("openid");
//判断数据库是否存在微信的扫描人信息
//根据openid判断
UserInfo userInfo = userInfoService.selectWxInfoOpenId(openid);
if(userInfo == null) { //数据库不存在微信信息
//第三步 拿着openid 和 access_token请求微信地址,得到扫描人信息
String baseUserInfoUrl = "https://api.weixin.qq.com/sns/userinfo" +
"?access_token=%s" +
"&openid=%s";
String userInfoUrl = String.format(baseUserInfoUrl, access_token, openid);
String resultInfo = HttpClientUtils.get(userInfoUrl);
System.out.println("resultInfo:"+resultInfo);
JSONObject resultUserInfoJson = JSONObject.parseObject(resultInfo);
//解析用户信息
//用户昵称
String nickname = resultUserInfoJson.getString("nickname");
//用户头像
String headimgurl = resultUserInfoJson.getString("headimgurl");
//获取扫描人信息添加数据库
userInfo = new UserInfo();
userInfo.setNickName(nickname);
userInfo.setOpenid(openid);
userInfo.setStatus(1);
userInfoService.save(userInfo);
}
//返回name和token字符串
Map<String,String> map = new HashMap<>();
String name = userInfo.getName();
if(StringUtils.isEmpty(name)) {
name = userInfo.getNickName();
}
if(StringUtils.isEmpty(name)) {
name = userInfo.getPhone();
}
map.put("name", name);
//判断userInfo是否有手机号,如果手机号为空,返回openid
//如果手机号不为空,返回openid值是空字符串
//前端判断:如果openid不为空,绑定手机号,如果openid为空,不需要绑定手机号
if(StringUtils.isEmpty(userInfo.getPhone())) {
map.put("openid", userInfo.getOpenid());
} else {
map.put("openid", "");
}
//使用jwt生成token字符串
String token = JwtHelper.createToken(userInfo.getId(), name);
map.put("token", token);
//跳转到前端页面
return "redirect:" + ConstantWxPropertiesUtils.YYGH_BASE_URL + "/weixin/callback?token="+map.get("token")+ "&openid="+map.get("openid")+"&name="+URLEncoder.encode(map.get("name"),"utf-8");
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
资源拥有者可以访问资源,但是客户应用不能。但是此时客户应用要访问资源,因此拥有者需要给客户应用授权。
那么如何授权呢?
采用颁发令牌的方式。接近OAuth2方式,需要考虑如何管理令牌、颁发令牌、吊销令牌,需要统一的协议,因此就有了OAuth2协议。
package com.atguigu.yygh.gateway.filter;
import com.alibaba.fastjson.JSONObject;
import com.atguigu.yygh.common.helper.JwtHelper;
import com.atguigu.yygh.common.result.Result;
import com.atguigu.yygh.common.result.ResultCodeEnum;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
import java.util.List;
/**
*
* 全局Filter,统一处理会员登录与外部不允许访问的服务
*
* * @author qy
* @since 2019-11-21
*/
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {
private AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String path = request.getURI().getPath();
System.out.println("==="+path);
//内部服务接口,不允许外部访问
if(antPathMatcher.match("/**/inner/**", path)) {
ServerHttpResponse response = exchange.getResponse();
return out(response, ResultCodeEnum.PERMISSION);
}
//api接口,异步请求,校验用户必须登录
if(antPathMatcher.match("/api/**/auth/**", path)) {
Long userId = this.getUserId(request);
if(StringUtils.isEmpty(userId)) {
ServerHttpResponse response = exchange.getResponse();
return out(response, ResultCodeEnum.LOGIN_AUTH);
}
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0;
}
/**
* api接口鉴权失败返回数据
* @param response
* @return
*/
private Mono<Void> out(ServerHttpResponse response, ResultCodeEnum resultCodeEnum) {
Result result = Result.build(null, resultCodeEnum);
byte[] bits = JSONObject.toJSONString(result).getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory().wrap(bits);
//指定编码,否则在浏览器中会中文乱码
response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
return response.writeWith(Mono.just(buffer));
}
/**
* 获取当前登录用户id
* @param request
* @return
*/
private Long getUserId(ServerHttpRequest request) {
String token = "";
List<String> tokenList = request.getHeaders().get("token");
if(null != tokenList) {
token = tokenList.get(0);
}
if(!StringUtils.isEmpty(token)) {
return JwtHelper.getUserId(token);
}
return null;
}
}
展示科室信息页面
Feign调用获取就诊人接口,排班下单信息接口
挂号功能
预约成功后,消息队列发送短信和更新排班数量信息(在短信和排班模块中,添加一个监听器,监听消息队列,有消息就调用方法)
支付功能
(1)通过appid与商户编号还有密钥,可以生成微信支付二维码
(2)前端点开预约订单,之后点开支付,前端会传给后端一个订单id,这是订单数据库中的主键,后端根据这个主键,来生成支付二维码,存储支付信息等。controller层接到订单id之后,调用service层的方法
(3)在service中,首先会在redis中查找是否已经存在该订单id相关的二维码信息,也就是二维码是否已经生成。如果找到了,就直接返回二维码信息就可以了。
(4)如果用户第一次点开收款码,那么后端就会根据订单id生成微信收款码。首先需要更新收款信息表,先根据订单xid取出订单信息,根据订单信息保存收款信息,将微信支付所需要的参数都封装到map集合中。使用写好的HttpClient工具类向微信发送请求,在这个类中微信返回的数据会使用content参数接收。将微信返回的结果集封装到map中,其中包括订单id,订单金额,订单状态码,以及返回的二维码图片地址。如果微信能够返回状态码的话,说明生成支付二维码成功,将这些信息保存在redis数据库中两个小时,当用户关闭支付页面后,再次点开就不需要再次请求微信了。
(5)微信支付状态判断,前端在生成二维码之后,会每隔一段时间申请后端的服务器方法,判断支付状态。前端传入订单id,后端根据订单id来申请微信,判断支付状态。后端的controller根据前端传入的订单id,调用service层,接收到一个map集合,如果为null,那么说明二维码已过期,如果map中的支付状态显示SUCCESS,表示已经支付成功,那么就可以根据订单编号修改数据库中的支付状态,返回支付成功。其他情况就是支付中。
//生成挂号订单
//入参:排班id,就诊人id
@Override
public Long saveOrder(String scheduleId, Long patientId) {
//获取就诊人信息
Patient patient = patientFeignClient.getPatientOrder(patientId);
//获取排班相关信息
ScheduleOrderVo scheduleOrderVo = hospitalFeignClient.getScheduleOrderVo(scheduleId);
//判断当前时间是否还可以预约
if(new DateTime(scheduleOrderVo.getStartTime()).isAfterNow()
|| new DateTime(scheduleOrderVo.getEndTime()).isBeforeNow()) {
throw new YyghException(ResultCodeEnum.TIME_NO);
}
//获取签名信息
SignInfoVo signInfoVo = hospitalFeignClient.getSignInfoVo(scheduleOrderVo.getHoscode());
//添加到订单表
OrderInfo orderInfo = new OrderInfo();
//scheduleOrderVo 数据复制到 orderInfo
BeanUtils.copyProperties(scheduleOrderVo,orderInfo);
//向orderInfo设置其他数据
String outTradeNo = System.currentTimeMillis() + ""+ new Random().nextInt(100);
orderInfo.setOutTradeNo(outTradeNo);
orderInfo.setScheduleId(scheduleId);
orderInfo.setUserId(patient.getUserId());
orderInfo.setPatientId(patientId);
orderInfo.setPatientName(patient.getName());
orderInfo.setPatientPhone(patient.getPhone());
orderInfo.setOrderStatus(OrderStatusEnum.UNPAID.getStatus());
baseMapper.insert(orderInfo);
//调用医院接口,实现预约挂号操作
//设置调用医院接口需要参数,参数放到map集合
Map<String, Object> paramMap = new HashMap<>();
paramMap.put("hoscode",orderInfo.getHoscode());
paramMap.put("depcode",orderInfo.getDepcode());
paramMap.put("hosScheduleId",orderInfo.getScheduleId());
paramMap.put("reserveDate",new DateTime(orderInfo.getReserveDate()).toString("yyyy-MM-dd"));
paramMap.put("reserveTime", orderInfo.getReserveTime());
paramMap.put("amount",orderInfo.getAmount());
paramMap.put("name", patient.getName());
paramMap.put("certificatesType",patient.getCertificatesType());
paramMap.put("certificatesNo", patient.getCertificatesNo());
paramMap.put("sex",patient.getSex());
paramMap.put("birthdate", patient.getBirthdate());
paramMap.put("phone",patient.getPhone());
paramMap.put("isMarry", patient.getIsMarry());
paramMap.put("provinceCode",patient.getProvinceCode());
paramMap.put("cityCode", patient.getCityCode());
paramMap.put("districtCode",patient.getDistrictCode());
paramMap.put("address",patient.getAddress());
//联系人
paramMap.put("contactsName",patient.getContactsName());
paramMap.put("contactsCertificatesType", patient.getContactsCertificatesType());
paramMap.put("contactsCertificatesNo",patient.getContactsCertificatesNo());
paramMap.put("contactsPhone",patient.getContactsPhone());
paramMap.put("timestamp", HttpRequestHelper.getTimestamp());
String sign = HttpRequestHelper.getSign(paramMap, signInfoVo.getSignKey());
paramMap.put("sign", sign);
//请求医院系统接口
JSONObject result = HttpRequestHelper.sendRequest(paramMap, signInfoVo.getApiUrl() + "/order/submitOrder");
if(result.getInteger("code")==200) {
JSONObject jsonObject = result.getJSONObject("data");
//预约记录唯一标识(医院预约记录主键)
String hosRecordId = jsonObject.getString("hosRecordId");
//预约序号
Integer number = jsonObject.getInteger("number");;
//取号时间
String fetchTime = jsonObject.getString("fetchTime");;
//取号地址
String fetchAddress = jsonObject.getString("fetchAddress");;
//更新订单
orderInfo.setHosRecordId(hosRecordId);
orderInfo.setNumber(number);
orderInfo.setFetchTime(fetchTime);
orderInfo.setFetchAddress(fetchAddress);
baseMapper.updateById(orderInfo);
//排班可预约数
Integer reservedNumber = jsonObject.getInteger("reservedNumber");
//排班剩余预约数
Integer availableNumber = jsonObject.getInteger("availableNumber");
//发送mq消息,号源更新和短信通知
//发送mq信息更新号源
OrderMqVo orderMqVo = new OrderMqVo();
orderMqVo.setScheduleId(scheduleId);
orderMqVo.setReservedNumber(reservedNumber);
orderMqVo.setAvailableNumber(availableNumber);
//短信提示
MsmVo msmVo = new MsmVo();
msmVo.setPhone(orderInfo.getPatientPhone());
String reserveDate = new DateTime(orderInfo.getReserveDate()).toString("yyyy-MM-dd") + (orderInfo.getReserveTime()==0 ? "上午" : "下午");
Map<String,Object> param = new HashMap<String,Object>(){{
put("title", orderInfo.getHosname()+"|"+orderInfo.getDepname()+"|"+orderInfo.getTitle());
put("amount", orderInfo.getAmount());
put("reserveDate", reserveDate);
put("name", orderInfo.getPatientName());
put("quitTime", new DateTime(orderInfo.getQuitTime()).toString("yyyy-MM-dd HH:mm"));
}};
msmVo.setParam(param);
orderMqVo.setMsmVo(msmVo);
rabbitService.sendMessage(MqConst.EXCHANGE_DIRECT_ORDER, MqConst.ROUTING_ORDER, orderMqVo);
} else {
throw new YyghException(result.getString("message"), ResultCodeEnum.FAIL.getCode());
}
return orderInfo.getId();
}
添加链接描述
添加链接描述
1)登录采取弹出层的形式
2)登录方式:
手机+手机验证码
微信扫描
3)无注册界面,第一次登录根据手机号判断系统是否存在,如果不存在则自动注册
4)微信扫描登录成功必须绑定手机号码,即:第一次扫描成功后绑定手机号,以后登录扫描直接登录成功
5)网关统一判断登录状态,如何需要登录,页面弹出登录层(api接口异步请求的,采取url规则匹配)
所有请求经过服务网关,服务网关对外暴露接口,网关进行统一用户认证。
Api接口异步请求的,我们采取url规则匹配,如:/api//auth/,如凡是满足该规则的都必须用户认证
我们统一从header头信息中获取
如何判断用户信息合法:
登录时我们返回用户token,在服务网关中获取到token后,我在到redis中去查看用户id,如何用户id存在,则token合法,否则不合法
订单相关操作时,用mq发送短信消息给短信消费者。
如果商品服务和订单服务是两个不同的微服务,在下单的过程中订单服务需要调用商品服务进行扣库存操作。按照传统的方式,下单过程要等到调用完毕之后才能返回下单成功,如果网络产生波动等原因使得商品服务扣库存延迟或者失败,会带来较差的用户体验,使用MQ发送短信
1)使用Redis作为缓存
2)验证码有效时间、支付二维码有效时间