1.需求说明
当乘客通过微信小程序登录的时候,我们可以根据微信接口拿到微信OpenId,到客户表中查询OpenId是否存在,如果不存在需要为乘客创建账号,存在则需要根据用户信息生产token返回。
登录流程时序图
2.乘客登录微服务接口
准备工作
首先要在service-customer里面引入依赖
<dependencies>
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-miniapp</artifactId>
</dependency>
</dependencies>
修改nacos配置中心中的文件,并在修改项目中properties文件中的地址信息。
在common-account.yaml中把第三方账号加到公共账号配置文件里面
wx:
miniapp:
#小程序授权登录
appId: wxcc651fcbab275e33 # 小程序微信公众平台appId
secret: 5f353399a2eae7ff6ceda383e924c5f6 # 小程序微信公众平台api秘钥
在service-customer创建包config,创建类,读取配置文件内容。
@Component
@Data
@ConfigurationProperties(prefix = "wx.miniapp")
public class WxConfigProperties {
private String appId;
private String secret;
}
创建微信工具包对象,通过WxMaService可以快速获取微信OpenId
@Component
public class WxConfigOperator {
@Autowired
private WxConfigProperties wxConfigProperties;
@Bean
public WxMaService wxMaService() {
//微信小程序id和秘钥
WxMaDefaultConfigImpl wxMaConfig = new WxMaDefaultConfigImpl();
wxMaConfig.setAppid(wxConfigProperties.getAppId());
wxMaConfig.setSecret(wxConfigProperties.getSecret());
WxMaService service = new WxMaServiceImpl();
service.setWxMaConfig(wxMaConfig);
return service;
}
}
功能具体实现
在service-customer的CustomerInfoController添加方法
@Slf4j
@RestController
@RequestMapping("/customer/info")
@SuppressWarnings({"unchecked", "rawtypes"})
public class CustomerInfoController {
@Autowired
private CustomerInfoService customerInfoService;
//微信小程序登录接口
@Operation(summary = "小程序授权登录")
@GetMapping("/login/{code}")
public Result<Long> login(@PathVariable String code) {
return Result.ok(customerInfoService.login(code));
}
}
service接口实现
@Slf4j
@Service
@SuppressWarnings({"unchecked", "rawtypes"})
public class CustomerInfoServiceImpl extends ServiceImpl<CustomerInfoMapper, CustomerInfo> implements CustomerInfoService {
@Autowired
private WxMaService wxMaService;
@Autowired
private CustomerInfoMapper customerInfoMapper;
@Autowired
private CustomerLoginLogMapper customerLoginLogMapper;
//微信小程序登录接口
@Override
public Long login(String code) {
//1 获取code值,使用微信工具包对象,获取微信唯一标识openid
String openid = null;
try {
WxMaJscode2SessionResult sessionInfo =
wxMaService.getUserService().getSessionInfo(code);
openid = sessionInfo.getOpenid();
} catch (WxErrorException e) {
throw new RuntimeException(e);
}
//2 根据openid查询数据库表,判断是否第一次登录
//如果openid不存在返回null,如果存在返回一条记录
//select * from customer_info ci where ci.wx_open_id = ''
LambdaQueryWrapper<CustomerInfo> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(CustomerInfo::getWxOpenId,openid);
CustomerInfo customerInfo = customerInfoMapper.selectOne(wrapper);
//3 如果第一次登录,添加信息到用户表
if(customerInfo == null) {
customerInfo = new CustomerInfo();
customerInfo.setNickname(String.valueOf(System.currentTimeMillis()));
customerInfo.setAvatarUrl("https://oss.aliyuncs.com/aliyun_id_photo_bucket/default_handsome.jpg");
customerInfo.setWxOpenId(openid);
customerInfoMapper.insert(customerInfo);
}
//4 记录登录日志信息
CustomerLoginLog customerLoginLog = new CustomerLoginLog();
customerLoginLog.setCustomerId(customerInfo.getId());
customerLoginLog.setMsg("小程序登录");
customerLoginLogMapper.insert(customerLoginLog);
//5 返回用户id
return customerInfo.getId();
}
}
实现远程调用
service-customer-client定义接口
@FeignClient(value = "service-customer")
public interface CustomerInfoFeignClient {
@GetMapping("/customer/info/login/{code}")
public Result<Long> login(@PathVariable String code);
}
在web-customer进行远程调用
@Slf4j
@Tag(name = "客户API接口管理")
@RestController
@RequestMapping("/customer")
@SuppressWarnings({"unchecked", "rawtypes"})
public class CustomerController {
@Autowired
private CustomerService customerInfoService;
@Operation(summary = "小程序授权登录")
@GetMapping("/login/{code}")
public Result<String> wxLogin(@PathVariable String code) {
return Result.ok(customerInfoService.login(code));
}
}
service实现
@Slf4j
@Service
@SuppressWarnings({"unchecked", "rawtypes"})
public class CustomerServiceImpl implements CustomerService {
//注入远程调用接口
@Autowired
private CustomerInfoFeignClient client;
@Autowired
private RedisTemplate redisTemplate;
@Override
public String login(String code) {
//1 拿着code进行远程调用,返回用户id
Result<Long> loginResult = client.login(code);
//2 判断如果返回失败了,返回错误提示
Integer codeResult = loginResult.getCode();
if(codeResult != 200) {
throw new GuiguException(ResultCodeEnum.DATA_ERROR);
}
//3 获取远程调用返回用户id
Long customerId = loginResult.getData();
//4 判断返回用户id是否为空,如果为空,返回错误提示
if(customerId == null) {
throw new GuiguException(ResultCodeEnum.DATA_ERROR);
}
//5 生成token字符串
String token = UUID.randomUUID().toString().replaceAll("-","");
//6 把用户id放到Redis,设置过期时间
// key:token value:customerId
//redisTemplate.opsForValue().set(token,customerId.toString(),30, TimeUnit.MINUTES);
redisTemplate.opsForValue().set(RedisConstant.USER_LOGIN_KEY_PREFIX+token,
customerId,
RedisConstant.USER_LOGIN_KEY_TIMEOUT,
TimeUnit.SECONDS);
//7 返回token
return token;
}
}
代码编写完成后,启动网关、service-customer、web-customer服务进行测试
获取登录用户信息接口
实现流程
在service-customer下的CustomerInfoController创建方法
@Operation(summary = "获取客户登录信息")
@GetMapping("/getCustomerLoginInfo/{customerId}")
public Result<CustomerLoginVo> getCustomerLoginInfo(@PathVariable Long customerId) {
CustomerLoginVo customerLoginVo = customerInfoService.getCustomerInfo(customerId);
return Result.ok(customerLoginVo);
}
service实现类
//获取客户登录信息
@Override
public CustomerLoginVo getCustomerInfo(Long customerId) {
//1 根据用户id查询用户信息
CustomerInfo customerInfo = customerInfoMapper.selectById(customerId);
//2 封装到CustomerLoginVo
CustomerLoginVo customerLoginVo = new CustomerLoginVo();
//customerLoginVo.setNickname(customerInfo.getNickname());
BeanUtils.copyProperties(customerInfo,customerLoginVo);
//@Schema(description = "是否绑定手机号码")
// private Boolean isBindPhone;
String phone = customerInfo.getPhone();
boolean isBindPhone = StringUtils.hasText(phone);
customerLoginVo.setIsBindPhone(isBindPhone);
//3 CustomerLoginVo返回
return customerLoginVo;
}
在web-customer进行远程调用
@Operation(summary = "获取客户登录信息")
@GetMapping("/getCustomerLoginInfo")
public Result<CustomerLoginVo>
getCustomerLoginInfo(@RequestHeader(value = "token") String token) {
//1 从请求头获取token字符串
// HttpServletRequest request
// String token = request.getHeader("token");
//调用service
CustomerLoginVo customerLoginVo = customerInfoService.getCustomerLoginInfo(token);
return Result.ok(customerLoginVo);
}
service实现类
@Override
public CustomerLoginVo getCustomerLoginInfo(String token) {
//2 根据token查询redis
//3 查询token在redis里面对应用户id
String customerId =
(String)redisTemplate.opsForValue()
.get(RedisConstant.USER_LOGIN_KEY_PREFIX + token);
if(StringUtils.isEmpty(customerId)) {
throw new GuiguException(ResultCodeEnum.DATA_ERROR);
}
// if(!StringUtils.hasText(customerId)) {
// throw new GuiguException(ResultCodeEnum.DATA_ERROR);
// }
//4 根据用户id进行远程调用 得到用户信息
Result<CustomerLoginVo> customerLoginVoResult =
customerInfoFeignClient.getCustomerLoginInfo(Long.parseLong(customerId));
Integer code = customerLoginVoResult.getCode();
if(code != 200) {
throw new GuiguException(ResultCodeEnum.DATA_ERROR);
}
CustomerLoginVo customerLoginVo = customerLoginVoResult.getData();
if(customerLoginVo == null) {
throw new GuiguException(ResultCodeEnum.DATA_ERROR);
}
//5 返回用户信息
return customerLoginVo;
}
测试的时候可能会有报错,老师在将用户信息存入redis中的时候将customerId转换为了字符串,这样存入的时候就为"1",当我们从redis中获取customerId的时候又强转为了字符串,这要customerId就会变成"“1"”,这里控制台就会显示报错。
处理方法:在将用户信息存入redis中的时候,customerId不用转为字符串,我上面的代码已经改正。
登录校验
这里老师使用AOP进行的登录校验,不清楚为什么不用拦截器进行校验,要是有小伙伴知道可以告知一下,我个人认为可能是要我们熟悉一下AOP的编写吧!AOP的知识我在这里就不做赘述。
登录状态判断
要判断请求头中是否包含token字符串,再根据token查询redis,如果查询成功则已登录。
如果是在每一个controller中都进行判断,会产生大量的重复代码,对此可以采用自定义注解+aop的方式进行优化。
具体实现
考虑到很多controller中都需要用到登录判断,故放在common中,便于大家都可以使用。
在common下的service-util中创建login包,里面创建所需要的文件。
创建注解
//登录判断
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface GuiguLogin {
}
创建切面类
@Component
@Aspect //切面类
public class GuiguLoginAspect {
@Autowired
private RedisTemplate redisTemplate;
//环绕通知,登录判断
//切入点表达式:指定对哪些规则的方法进行增强
@Around("execution(* com.atguigu.daijia.*.controller.*.*(..)) && @annotation(guiguLogin)")
public Object login(ProceedingJoinPoint proceedingJoinPoint,GuiguLogin guiguLogin) throws Throwable {
//1 获取request对象
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes sra = (ServletRequestAttributes)attributes;
HttpServletRequest request = sra.getRequest();
//2 从请求头获取token
String token = request.getHeader("token");
//3 判断token是否为空,如果为空,返回登录提示
if(!StringUtils.hasText(token)) {
throw new GuiguException(ResultCodeEnum.LOGIN_AUTH);
}
//4 token不为空,查询redis
String customerId = (String)redisTemplate.opsForValue()
.get(RedisConstant.USER_LOGIN_KEY_PREFIX+token);
//5 查询redis对应用户id,把用户id放到ThreadLocal里面
if(StringUtils.hasText(customerId)) {
AuthContextHolder.setUserId(Long.parseLong(customerId));
}
//6 执行业务方法
return proceedingJoinPoint.proceed();
}
}
创建完切面类之后,在CustomerController方法加上@GuiGuLogin注解,实现自动校验
@Operation(summary = "获取客户登录信息")
@GuiGuLogin
@GetMapping("/getCustomerLoginInfo")
public Result<CustomerLoginVo> getCustomerLoginInfo() {
// 1.从ThreadLocal中获取用户id
Long customerId = AuthContextHolder.getUserId();
return Result.ok(customerService.getCustomerLoginInfo(customerId));
}
在实现类里重写方法
// 获取用户信息
@Override
public CustomerLoginVo getCustomerLoginInfo(Long customerId) {
// 1.根据用户id进行远程调用,得到用户信息
Result<CustomerLoginVo> customerLoginVoResult = client.getCustomerLoginInfo(customerId);
Integer code = customerLoginVoResult.getCode();
if(code != 200){
throw new GuiguException(ResultCodeEnum.DATA_ERROR);
}
CustomerLoginVo customerLoginVo = customerLoginVoResult.getData();
if(customerLoginVo == null){
throw new GuiguException(ResultCodeEnum.DATA_ERROR);
}
// 2.返回用户信息
return customerLoginVo;
}
获取微信手机号
乘客下单代驾,司机会电话联系乘客,因此乘客必须绑定手机号码,微信小程序可以申请用户微信平台手机号。
说明:获取手机号必须是企业级微信公众号,个人版获取不到。
因为这里获取不到手机号,自己熟悉流程即可。
在CustomerInfoController里面添加方法
@Operation(summary = "更新客户微信手机号码")
@PostMapping("/updateWxPhoneNumber")
public Result<Boolean> updateWxPhoneNumber(@RequestBody UpdateWxPhoneForm updateWxPhoneForm) {
return Result.ok(customerInfoService.updateWxPhoneNumber(updateWxPhoneForm));
}
实现类如下
更新客户微信手机号码
@Override
public Boolean updateWxPhoneNumber(UpdateWxPhoneForm updateWxPhoneForm) {
//1 根据code值获取微信绑定手机号码
try {
WxMaPhoneNumberInfo phoneNoInfo =
wxMaService.getUserService().getPhoneNoInfo(updateWxPhoneForm.getCode());
String phoneNumber = phoneNoInfo.getPhoneNumber();
//更新用户信息
Long customerId = updateWxPhoneForm.getCustomerId();
CustomerInfo customerInfo = customerInfoMapper.selectById(customerId);
customerInfo.setPhone(phoneNumber);
customerInfoMapper.updateById(customerInfo);
return true;
} catch (WxErrorException e) {
throw new GuiguException(ResultCodeEnum.DATA_ERROR);
}
}