仅涉及后端,全部目录看顶部专栏,代码、文档、接口路径在:

【Lilishop商城】记录一下B2B2C商城系统学习笔记~_清晨敲代码的博客-CSDN博客


全篇会结合业务介绍重点设计逻辑,其中重点包括接口类、业务类,具体的结合源代码分析,源码读起来也不复杂~

谨慎:源代码中有一些注释是错误的,有的注释意思完全相反,有的注释对不上号,我在阅读过程中就顺手更新了,并且在我不会的地方添加了新的注释,所以在读源代码过程中一定要谨慎啊!

目录

A1.会员登录模块

B1.会员controller

C1.平台注册会员接口开发

业务逻辑:

代码逻辑:

C2.用户名密码登录接口开发

业务逻辑:

代码逻辑:

C3.短信登录接口开发

业务逻辑:

代码逻辑:

C4.手机App/小程序扫码二维码登录接口开发⭐

业务逻辑:

代码逻辑:


A1.会员登录模块

在 No2-2.确定软件架构搭建 里的 A5.安全框架(springsecurity)  文章中,我们已经了解了账号授权及认证的开发架构了。

  1. 账号登录成功后,会接收到后端返回的 Token 数据,包括 accesstoken 和 refreshtoken ;
  2. 账号携带 accesstoken 访问后端接口,会先被 filter 拦截到 accesstoken 拿到帐号信息,判断是有权限,进而执行接口。

所以此处的会员登录模块只涉及到上方的 1. ,最终都一定会拿到 Token  ~

至于 2. 可以去各个端的api代码里查看继承了 BasicAuthenticationFilter 的 filter ,逻辑就是 No2-2 里的 A5 ,这里就不重复说明了~

B1.会员controller

C1.平台注册会员接口开发

平台注册会员就一个接口,只需要有入参 用户名、密码、手机号码、短信验证码就可以创建会员。并且接口会直接返回账号登陆的 Token ,但是要注意PC端的前端并没有在注册成功后使用 Token ~用户注册成功后还需要手动登录


业务逻辑:

在介绍业务逻辑时,会涉及到一些其他代码结构,有需要说明的就用绿色底纹标注,然后在后面的代码逻辑里面详细介绍。

controller类:MemberBuyerController

  1. 接受到入参,先校验短信验证码【校验也是通过公共的 SmsUtil 操作】
  2. 若验证码校验有问题就抛出 ServiceException 异常,异常类型为:短信验证码错误,请重新校验。【改异常类是自定义的,需要在全局异常处理类中捕获到并返回结果。具体会在 No2-* 软件架构 里面补充,具体代码可见:GlobalControllerExceptionHandler】
  3. 若校验没问题,则调用会员业务类的注册方法,拿到 token 并返回响应值ResultMessage。

service类:仅使用的 mybatis-plus 的,没有自定义mapper

  1. 先检测会员信息中用户名和手机号码是否已存在,已存在则抛出 ServiceException 异常,异常类型为:该用户名或手机号已被注册
  2. 将参数转为用户实体类 Member,使用hutool工具包中的雪花算法设置 id,再调用 IService 自带 save 方法保存用户到数据库;
  3. 处理会员注册的小事件:新会员赠送积分、新会员赠送优惠券、新会员赠送经验等等。【此处的逻辑使用的 SpringEvent 和 RocketMQ 处理的,SpringEvent 用于发布消息,作用在于程序解耦,RocketMQ 是拿到消息后处理具体的业务】
  4. 最后生成 MemberTokenGenerate 生成 token ,并返回值;【MemberTokenGenerate见No2-2 A5】

代码逻辑:

//cn.lili.controller.passport.MemberBuyerController
@Slf4j
@RestController
@Api(tags = "买家端,会员接口")
@RequestMapping("/buyer/passport/member")
public class MemberBuyerController {@Autowiredprivate MemberService memberService;@Autowiredprivate SmsUtil smsUtil;@ApiOperation(value = "注册用户")@PostMapping("/register")public ResultMessage<Object> register(@NotNull(message = "用户名不能为空") @RequestParam String username,@NotNull(message = "密码不能为空") @RequestParam String password,@NotNull(message = "手机号为空") @RequestParam String mobilePhone,@RequestHeader String uuid,@NotNull(message = "验证码不能为空") @RequestParam String code) {if (smsUtil.verifyCode(mobilePhone, VerificationEnums.REGISTER, uuid, code)) {return ResultUtil.data(memberService.register(username, password, mobilePhone));} else {throw new ServiceException(ResultCode.VERIFICATION_SMS_CHECKED_ERROR);}}
...
}//cn.lili.modules.member.serviceimpl.MemberServiceImpl@Service
public class MemberServiceImpl extends ServiceImpl<MemberMapper, Member> implements MemberService {/*** 会员token*/@Autowiredprivate MemberTokenGenerate memberTokenGenerate;@Autowiredprivate ApplicationEventPublisher applicationEventPublisher;@Override@Transactionalpublic Token register(String userName, String password, String mobilePhone) {//检测会员信息checkMember(userName, mobilePhone);//设置会员信息Member member = new Member(userName, new BCryptPasswordEncoder().encode(password), mobilePhone);//进行用户注册处理。抽象出一个方法this.registerHandler(member);return memberTokenGenerate.createToken(member, false);}/*** 注册方法抽象出来:会员注册、第三方授权自动注册、员工账号注册登都需要改逻辑~** @param member*/@Transactionalpublic void registerHandler(Member member) {//hutool工具包 中的雪花算法member.setId(SnowFlake.getIdStr());//保存会员this.save(member);//处理会员保存成功后的小事件:监听event,发送mp消息。最终在 mq 里面处理小事件applicationEventPublisher.publishEvent(new TransactionCommitSendMQEvent("new member register", rocketmqCustomProperties.getMemberTopic(), MemberTagsEnum.MEMBER_REGISTER.name(), member));}/*** 检测会员** @param userName    会员名称* @param mobilePhone 手机号*/private void checkMember(String userName, String mobilePhone) {//判断手机号是否存在if (this.findMember(mobilePhone, userName) > 0) {throw new ServiceException(ResultCode.USER_EXIST);}}
。。。
}

1.公共的 SmsUtil 操作

详见:cn.lili.modules.sms.SmsUtil,里面包括发送短信验证码、校验短信验证码方法,都是公共的。

在发送短信验证码的方法中,发送给手机号码后,最终会将验证码存到redis里面的。所以校验短信验证码的方法里面,也从redis里面拿到该手机号码的注册Enums的校验码,然后进行对比校验。

短信发送逻辑是使用的第三方(阿里云)的,按照阿里云提供的工具使用。

还有一点,由于 SmsUtil 是公用的,所以有不同种使用类型,例如:会员注册、登录、找回密码等等,不同的类型发送短信验证码模板是不一样的(阿里云里面也是需要模板code进行区分的),所以需要不同的类型进行区分。

所以就有了 VerificationEnums 枚举类来标注类型,并且也使用了 application.yml 来配置模板信息

//cn.lili.modules.verification.entity.enums.VerificationEnums
public enum VerificationEnums {/*** 登录* 注册* 找回用户* 修改密码* 支付钱包密码*/LOGIN,REGISTER,FIND_USER,UPDATE_PASSWORD,WALLET_PASSWORD;
}//cn.lili.modules.sms.impl.SmsUtilAliImplService
@Component
@Slf4j
public class SmsUtilAliImplService implements SmsUtil, AliSmsUtil {@Autowiredprivate Cache cache;@Autowiredprivate SettingService settingService;@Autowiredprivate MemberService memberService;@Autowiredprivate SmsTemplateProperties smsTemplateProperties;@Autowiredprivate SystemSettingProperties systemSettingProperties;@Overridepublic void sendSmsCode(String mobile, VerificationEnums verificationEnums, String uuid) {
。。。//缓存中写入要验证的信息cache.put(cacheKey(verificationEnums, mobile, uuid), code, 300L);}@Overridepublic boolean verifyCode(String mobile, VerificationEnums verificationEnums, String uuid, String code) {Object result = cache.get(cacheKey(verificationEnums, mobile, uuid));if (code.equals(result) || code.equals("0")) {//校验之后,删除cache.remove(cacheKey(verificationEnums, mobile, uuid));return true;} else {return false;}}/*** 生成缓存key** @param verificationEnums 验证场景* @param mobile            手机号码* @param uuid              用户标识 uuid* @return*/static String cacheKey(VerificationEnums verificationEnums, String mobile, String uuid) {return CachePrefix.SMS_CODE.getPrefix() + verificationEnums.name() + uuid + mobile;}
。。。
}
# /lilishop-master/common-api/src/main/resources/application.ymllili:  #短信模版配置sms:#登录LOGIN: SMS_205755300#注册REGISTER: SMS_205755298#找回密码FIND_USER: SMS_205755301#设置密码UPDATE_PASSWORD: SMS_205755297#支付密码WALLET_PASSWORD: SMS_205755301
//使用例子
smsUtil.verifyCode(mobilePhone, VerificationEnums.REGISTER, uuid, code)

2.ServiceException 异常、异常类型为、全局异常处理类

ServiceException是全局业务异常类,大部分的异常都是他,又由于异常类型多,所以需要有个异常类型枚举类,来说明不同类型的异常信息,信息里包含code、message(有的系统是用的是国际化Message,但本系统没有使用)。

最终抛出的ServiceException异常由 GlobalControllerExceptionHandler 捕获到并拿到code、message进行处理。

注:异常类型存放到枚举类里面,不如放到配置文件里面方便,如果要修改异常类型信息就需要修改代码并重新启动。

//cn.lili.common.enums.ResultCode
/*** 返回状态码* 第一位 1:商品;2:用户;3:交易,4:促销,5:店铺,6:页面,7:设置,8:其他** @author Chopper* @since 2020/4/8 1:36 下午*/
public enum ResultCode {/*** 成功状态码*/SUCCESS(200, "成功"),/*** 失败返回码*/ERROR(400, "服务器繁忙,请稍后重试"),。。。private final Integer code;private final String message;ResultCode(Integer code, String message) {this.code = code;this.message = message;}public Integer code() {return this.code;}public String message() {return this.message;}}//cn.lili.common.exception.ServiceException
/*** 全局业务异常类** @author Chopper*/
@EqualsAndHashCode(callSuper = true)
@Data
public class ServiceException extends RuntimeException {private static final long serialVersionUID = 3447728300174142127L;public static final String DEFAULT_MESSAGE = "网络错误,请稍后重试!";/*** 异常消息*/private String msg = DEFAULT_MESSAGE;/*** 错误码*/private ResultCode resultCode;public ServiceException(String msg) {this.resultCode = ResultCode.ERROR;this.msg = msg;}public ServiceException() {super();}public ServiceException(ResultCode resultCode) {this.resultCode = resultCode;}public ServiceException(ResultCode resultCode, String message) {this.resultCode = resultCode;this.msg = message;}}//cn.lili.common.exception.GlobalControllerExceptionHandler
/*** 异常处理** @author Chopper*/
@RestControllerAdvice
@Slf4j
public class GlobalControllerExceptionHandler {/*** 如果超过长度,则前后段交互体验不佳,使用默认错误消息*/static Integer MAX_LENGTH = 200;/*** 自定义异常** @param e* @return*/@ExceptionHandler(ServiceException.class)//设置响应状态码code@ResponseStatus(value = HttpStatus.BAD_REQUEST)public ResultMessage<Object> handleServiceException(HttpServletRequest request, final Exception e, HttpServletResponse response) {//如果是自定义异常,则获取异常,返回自定义错误消息if (e instanceof ServiceException) {ServiceException serviceException = ((ServiceException) e);ResultCode resultCode = serviceException.getResultCode();Integer code = null;String message = null;if (resultCode != null) {code = resultCode.code();message = resultCode.message();}//如果有扩展消息,则输出异常中,跟随补充异常if (!serviceException.getMsg().equals(ServiceException.DEFAULT_MESSAGE)) {message += ":" + serviceException.getMsg();}log.error("全局异常[ServiceException]:{}-{}", serviceException.getResultCode().code(), serviceException.getResultCode().message(), e);return ResultUtil.error(code, message);} else {log.error("全局异常[ServiceException]:", e);}//默认错误消息String errorMsg = "服务器异常,请稍后重试";if (e != null && e.getMessage() != null && e.getMessage().length() < MAX_LENGTH) {errorMsg = e.getMessage();}return ResultUtil.error(ResultCode.ERROR.code(), errorMsg);}
。。。
}//使用例子
throw new ServiceException(ResultCode.VERIFICATION_SMS_CHECKED_ERROR);

3.hutool工具包中的雪花算法设置 id

shop平台使用的使 hutool 工具类的 SnowFlake,具体的分布式学习可以看这篇文章分布式全局唯一ID(学习总结---从入门到深化)-CSDN博客

//cn.lili.common.utils.SnowFlake/*** 雪花分布式id获取** @author Chopper*/
@Slf4j
public class SnowFlake {//静态private static Snowflake snowflake;/*** 初始化配置** @param workerId* @param datacenterId*/public static void initialize(long workerId, long datacenterId) {snowflake = IdUtil.getSnowflake(workerId, datacenterId);}public static long getId() {return snowflake.nextId();}/*** 生成字符,带有前缀的id。例如,订单编号 O202103301376882313039708161** @param prefix* @return*/public static String createStr(String prefix) {return prefix + DateUtil.toString(new Date(), "yyyyMMdd") + SnowFlake.getId();}public static String getIdStr() {return snowflake.nextId() + "";}
}//cn.lili.common.utils.SnowflakeInitiator@Component
@Slf4j
public class SnowflakeInitiator {/*** 缓存前缀*/private static final String KEY = "{Snowflake}";@Autowiredprivate Cache cache;/*** 尝试初始化** @return*///Java自带的注解,在方法上加该注解会在项目启动的时候执行该方法@PostConstructpublic void init() {//从 redis 里面获取到自增长的主键Long num = cache.incr(KEY);long dataCenter = num / 32;long workedId = num % 32;//如果数据中心大于32,则抹除缓存,从头开始if (dataCenter >= 32) {cache.remove(KEY);num = cache.incr(KEY);dataCenter = num / 32;workedId = num % 32;}//初始化SnowFlake.initialize(workedId, dataCenter);}public static void main(String[] args) {SnowFlake.initialize(0, 8);System.out.println(SnowFlake.getId());}
}//使用例子
member.setId(SnowFlake.getIdStr());order.setSn(SnowFlake.createStr("G"));

4.SpringEvent、RocketMQ

service里面会员注册成功后,后面需要给该会员发放优惠券、积分等业务操作,为了避免程序耦合,就是使用了 SpringEvent 的方式,也就是 TransactionCommitSendMQListener事件监听器和TransactionCommitSendMQEvent事件。然后在事件监听器里面又调用了rocketMQTemplate发送消息,最终在rocket监听器里面处理的哦~

说白了就是将SpringEvent、RocketMQ结合使用了,我是没搞懂为啥结合使用,因为直接使用RocketMQ也不费事,SpringEvent本身最重要的作用就是业务剥离、程序解耦,这些也是RocketMQ的作用。【其他模块就有直接使用RocketMQ的】

直到我看到 SpringEvent 的类名字及注释:事务提交后发生mq事件、事务提交监听器,并且使用了@TransactionalEventListener注释,我想那这部分类只是专门用来处理事务提交后相关的业务的。

//cn.lili.common.event.TransactionCommitSendMQEvent
/*** 事务提交后发生mq事件** @author paulG* @since 2022/1/19**/
public class TransactionCommitSendMQEvent extends ApplicationEvent {private static final long serialVersionUID = 5885956821347953071L;@Getterprivate final String topic;@Getterprivate final String tag;@Getterprivate final Object message;public TransactionCommitSendMQEvent(Object source, String topic, String tag, Object message) {super(source);this.topic = topic;this.tag = tag;this.message = message;}
}//cn.lili.common.listener.TransactionCommitSendMQListener
/*** 事务提交监听器** @author paulG* @since 2022/1/19**/
@Component
@Slf4j
public class TransactionCommitSendMQListener {/*** rocketMq*/@Autowiredprivate RocketMQTemplate rocketMQTemplate;//在事务提交后再触发某一事件@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)public void send(TransactionCommitSendMQEvent event) {log.info("事务提交,发送mq信息!{}", event);String destination = event.getTopic() + ":" + event.getTag();//发送订单变更mq消息rocketMQTemplate.asyncSend(destination, event.getMessage(), RocketmqSendCallbackBuilder.commonCallback());}}//使用例子:
//处理会员保存成功后的小事件:监听event,发送mp消息。最终在 mq 里面处理小事件
applicationEventPublisher.publishEvent(new TransactionCommitSendMQEvent("new member register", rocketmqCustomProperties.getMemberTopic(), MemberTagsEnum.MEMBER_REGISTER.name(), member));

C2.用户名密码登录接口开发

这个也只有一个接口,最终是根据用户名和密码,拿到用户登录的 Token 的。

业务逻辑:

在介绍业务逻辑时,会涉及到一些其他代码结构,有需要说明的就用绿色底纹标注,然后在后面的代码逻辑里面详细介绍。

controller类:MemberBuyerController

  1. 接受到入参,先校验图片验证码【校验是通过 VerificationService 操作】
  2. 若验证码校验有问题就抛出 ServiceException 异常,异常类型为:验证码已失效,请重新校验。
  3. 若校验没问题,则调用会员业务类的用户名密码登录方法,拿到 token 并返回响应值ResultMessage。

service类:仅使用的 mybatis-plus 的,没有自定义mapper

  1. 先获取用户名或手机号码对应的帐号信息,不存在就抛出 ServiceException 异常,异常类型为:用户不存在。
  2. 若用户存在则判断密码是否输入正确,不正确就抛出 ServiceException 异常【因为注册时密码使用的BCryptPasswordEncoder保存的,解密时也自然是用他】
  3. 根据拿到的会员账号信息,生成token,并返回;

代码逻辑:

//cn.lili.controller.passport.MemberBuyerController
@Slf4j
@RestController
@Api(tags = "买家端,会员接口")
@RequestMapping("/buyer/passport/member")
public class MemberBuyerController {@Autowiredprivate MemberService memberService;@Autowiredprivate VerificationService verificationService;@ApiOperation(value = "用户名密码登录接口")@PostMapping("/userLogin")public ResultMessage<Object> userLogin(@NotNull(message = "用户名不能为空") @RequestParam String username,@NotNull(message = "密码不能为空") @RequestParam String password,@RequestHeader String uuid) {verificationService.check(uuid, VerificationEnums.LOGIN);return ResultUtil.data(this.memberService.usernameLogin(username, password));}
。。。
}//cn.lili.modules.member.serviceimpl.MemberServiceImpl
/*** 会员接口业务层实现** @author Chopper* @since 2021-03-29 14:10:16*/
@Service
public class MemberServiceImpl extends ServiceImpl<MemberMapper, Member> implements MemberService {/*** 会员token*/@Autowiredprivate MemberTokenGenerate memberTokenGenerate;@Overridepublic Token usernameLogin(String username, String password) {//获取用户名或手机号码对应的帐号信息Member member = this.findMember(username);//判断用户是否存在if (member == null || !member.getDisabled()) {throw new ServiceException(ResultCode.USER_NOT_EXIST);}//判断密码是否输入正确if (!new BCryptPasswordEncoder().matches(password, member.getPassword())) {throw new ServiceException(ResultCode.USER_PASSWORD_ERROR);}//成功登录,则检测cookie中的信息,进行会员绑定。但是我发现前端并没有操作对应的cookies,所以暂时是没有用的this.loginBindUser(member);//根据会员账号信息,生成tokenreturn memberTokenGenerate.createToken(member, false);}...
}

1. VerificationService 操作

【Lilishop商城】No3-2.模块详细设计 A4 滑块验证码图片的详细设计-CSDN博客 里面描述过用户使用滑块验证码登录时的流程,这里在描述一下:

滑块验证流程:

1.后端将底图、滑块图转化成base64并返回,同时将正确的阴影X轴位置存储到redis(key里面包含前端传过来的uuid,以便于后面getkey校验),然后返回给前端展示。

2.前端拿到base64转化成图片展示,并且实现滑动的动态效果。用户看到后滑动滑块到某个位置,此时的滑块位置为入参,松手后调用校验滑块接口,从redis里面拿到正确X轴位置与此时滑块位置作比较,比较通过后,再次缓存校验成功true(key里面也包含刚才的uuid),然后返回success。

3.前端发现滑块校验成功后,就调用登录接口,在登录接口里面会先从缓存中获取校验成功,如果是校验成功则进行登录。

上方三步骤都会对应一个验证码模块的接口,也都挺好理解的,就是要记住需要往 redis 里存储两种,缓存需要验证的内容、缓存验证的结果。

//cn.lili.modules.verification.service.impl.VerificationServiceImpl
/*** 验证码认证处理类** @author Chopper* @version v1.0* 2020-11-17 14:59*/
@Slf4j
@Component
public class VerificationServiceImpl implements VerificationService {@Autowiredprivate VerificationSourceService verificationSourceService;@Autowiredprivate VerificationCodeProperties verificationCodeProperties;@Autowiredprivate Cache cache;/*** 创建校验* @param uuid 前端传过来的的标识* @return 验证码参数*/@Overridepublic Map<String, Object> createVerification(VerificationEnums verificationEnums, String uuid) {if (uuid == null) {throw new ServiceException(ResultCode.ILLEGAL_REQUEST_ERROR);}
。。。try {
。。。            //⭐重点,生成验证码数据Map<String, Object> resultMap = SliderImageUtil.pictureTemplatesCut(sliderFile, interfereSliderFile, originalFile,verificationCodeProperties.getWatermark(), verificationCodeProperties.getInterfereNum());//生成验证参数 有效时间 默认600秒,可以自行配置,存储到rediscache.put(cacheKey(verificationEnums, uuid), resultMap.get("randomX"), verificationCodeProperties.getEffectiveTime());resultMap.put("key", cacheKey(verificationEnums, uuid));resultMap.put("effectiveTime", verificationCodeProperties.getEffectiveTime());//移除横坐标移动距离,不能返回给用户哦resultMap.remove("randomX");return resultMap;} catch (ServiceException e) {throw e;} catch (Exception e) {log.error("生成验证码失败", e);throw new ServiceException(ResultCode.ERROR);}}/*** 根据网络地址,获取源文件* 这里简单说一下,这里是将不可序列化的inputstream序列化对象,存入redis缓存** @param originalResource* @return*/private SerializableStream getInputStream(String originalResource) throws Exception {Object object = cache.get(CachePrefix.VERIFICATION_IMAGE.getPrefix() + originalResource);if (object != null) {return (SerializableStream) object;}if (StringUtils.isNotEmpty(originalResource)) {URL url = new URL(originalResource);InputStream inputStream = url.openStream();SerializableStream serializableStream = new SerializableStream(inputStream);cache.put(CachePrefix.VERIFICATION_IMAGE.getPrefix() + originalResource, serializableStream);return serializableStream;}return null;}/*** 预校验图片 用于前端回显** @param xPos              X轴移动距离* @param verificationEnums 验证key* @return 验证是否成功*/@Overridepublic boolean preCheck(Integer xPos, String uuid, VerificationEnums verificationEnums) {Integer randomX = (Integer) cache.get(cacheKey(verificationEnums, uuid));if (randomX == null) {throw new ServiceException(ResultCode.VERIFICATION_CODE_INVALID);}log.debug("{}{}", randomX, xPos);//验证结果正确 && 删除标记成功if (Math.abs(randomX - xPos) < verificationCodeProperties.getFaultTolerant() && cache.remove(cacheKey(verificationEnums, uuid))) {//验证成功,则记录验证结果 验证有效时间与验证码创建有效时间一致cache.put(cacheResult(verificationEnums, uuid), true, verificationCodeProperties.getEffectiveTime());return true;}throw new ServiceException(ResultCode.VERIFICATION_ERROR);}/*** 验证码校验** @param uuid              用户标识* @param verificationEnums 验证key* @return 验证是否成功*/@Overridepublic boolean check(String uuid, VerificationEnums verificationEnums) {//如果有校验标记,则返回校验结果if (Boolean.TRUE.equals(cache.remove(this.cacheResult(verificationEnums, uuid)))) {return true;}throw new ServiceException(ResultCode.VERIFICATION_CODE_INVALID);}/*** 生成缓存key 记录缓存需要验证的内容** @param verificationEnums 验证码枚举* @param uuid              用户uuid* @return 缓存key*/public static String cacheKey(VerificationEnums verificationEnums, String uuid) {return CachePrefix.VERIFICATION_KEY.getPrefix() + verificationEnums.name() + uuid;}/*** 生成缓存key 记录缓存验证的结果** @param verificationEnums 验证码枚举* @param uuid              用户uuid* @return 缓存key*/public static String cacheResult(VerificationEnums verificationEnums, String uuid) {return CachePrefix.VERIFICATION_RESULT.getPrefix() + verificationEnums.name() + uuid;}}
//使用例子verificationService.createVerification(verificationEnums, uuid)verificationService.preCheck(xPos, uuid, verificationEnums)verificationService.check(uuid, VerificationEnums.LOGIN)

C3.短信登录接口开发

业务逻辑:

在介绍业务逻辑时,会涉及到一些其他代码结构,有需要说明的就用绿色底纹标注,然后在后面的代码逻辑里面详细介绍。

controller类:MemberBuyerController

  1. 接收到入参,先校验短信验证码
  2. 若验证码校验有问题就抛出 ServiceException 异常
  3. 若校验没问题,则调用会员业务类的手机号验证码登录方法,拿到 token 并返回响应值ResultMessage。

service类:仅使用的 mybatis-plus 的,没有自定义mapper

  1. 根据手机号码获取用户
  2. 如果手机号不存在则自动注册用户,使用注册抽象出来的方法this.registerHandler(member)
  3. 根据会员账号信息,生成token,并返回

代码逻辑:

这一块儿没有复杂的逻辑,只是记住一点,根据手机号码注册的账号,用户名就是手机号码!

注意:直接放具体使用的方法啦,毕竟还是要对照着源码学习的,而且在这里贴代码,看着也不方便,重点在思想、逻辑啊~

//cn.lili.controller.passport.MemberBuyerController#smsLogin@PostMapping("/smsLogin")public ResultMessage<Object> smsLogin(@NotNull(message = "手机号为空") @RequestParam String mobile,@NotNull(message = "验证码为空") @RequestParam String code,@RequestHeader String uuid) {if (smsUtil.verifyCode(mobile, VerificationEnums.LOGIN, uuid, code)) {return ResultUtil.data(memberService.mobilePhoneLogin(mobile));} else {throw new ServiceException(ResultCode.VERIFICATION_SMS_CHECKED_ERROR);}}//cn.lili.modules.member.serviceimpl.MemberServiceImpl#mobilePhoneLogin@Override@Transactionalpublic Token mobilePhoneLogin(String mobilePhone) {QueryWrapper<Member> queryWrapper = new QueryWrapper<>();queryWrapper.eq("mobile", mobilePhone);//根据手机号码获取用户。疑问,不是有 findMember(String userNameOrMobile) 方法吗?为啥不用呢?因为有可能会有用户名为该手机号码的,所以不可以使用哦Member member = this.baseMapper.selectOne(queryWrapper);//如果手机号不存在则自动注册用户if (member == null) {member = new Member(mobilePhone, UuidUtils.getUUID(), mobilePhone);//使用注册抽象出来的方法this.registerHandler(member);}this.loginBindUser(member);//根据会员账号信息,生成tokenreturn memberTokenGenerate.createToken(member, false);}

C4.手机App/小程序扫码二维码登录接口开发

这个有第四个接口,两个是PC端的,两个是手机小程序/APP端的。

重点在于第四个长轮训校验二维码接口,他贯穿了整个过程。

  1. 首先,用户点击PC前端的扫码登录,会调用后端获取二维码接口,后端会返回二维码的信息,二维码信息包括token、过期时限等信息,前端拿到后生成二维码展示;
  2. 用户使用App/小程序进行扫码,扫码之后会拿到二维码中的 token ,然后调用后端扫码接口,并将 token 作为入参,然后根据token拿到缓存的二维码信息,并修改状态为已经扫码,并重新缓存二维码信息,然后返回此时的已经扫码的二维码状态;
  3. 前端拿到已经扫码的状态后,会打开授权确认页面,点击授权确认/拒绝按钮,都会调用后端的二维码登录确认接口,并将 token 和授权状态作为入参,然后根据token拿到缓存的二维码信息,并修改状态为确认/拒绝扫码,如果是确认会再将userid设置为当前登录用户,并重新缓存二维码信息,然后返回成功;
  4. 在 1. 中拿到二维码信息后,前端以 token 和 等待扫描状态 为入参,调用后端长轮训校验二维码接口,接口中按照二维码的有效期每隔一秒循环判断是否能够返回连接结果。若用户调用了 2. 的扫码接口,则此轮询接口就返回状态是已经扫码的二维码信息。然后前端接收到已经扫码状态的响应后,会再次以 token 和 已经扫描状态 为入参,调用后端长轮训校验二维码接口。若用户调用了 3. 的确认/拒绝接口,则此轮询接口就返回状态是确认/拒绝的二维码信息,返回的如果是确认状态则会包含登录的Token信息。前端拿到确认状态的响应就按照 Token 执行登录成功方法,如果拿到的是拒绝状态的响应就调用刷新二维码的方法。

业务逻辑:

业务逻辑就看上面,都描述的很清楚了~

重点就是 4. 接口里面的轮询判断,根据业务可以得出此方法的返回值中的二维码登录结果信息中的状态可以是 1:已经扫码、2:同意、3:拒绝、4:过期。

返回 1 是因为需要在前端展示该二维码已经扫码的的状态。

方法返回的状态是 1 后,前端就需要再次调用此长轮询方法,因为还要拿到2:同意/3:拒绝状态,但是针对当前token来说,就不能再次返回状态 1 了,不然前端就会在未授权期间不断调用此接口了!!!需要再添加对应的判断!!!

于是,我们在该方法的入参中添加了 beforeSessionStatus 参数,用来表示上次记录的session状态,前端第一次调用时传值为 0:等待扫码,当后端返回 1:已经扫码后,就将新的 1:已经扫码赋值给 beforeSessionStatus 传参,然后后端经过判断后会返回最终的授权结果~~~

那后端是怎样判断的呢?看下方的代码逻辑~~~

代码逻辑:

//cn.lili.controller.passport.MemberBuyerController/*** 买家端,会员接口** @author Chopper* @since 2020/11/16 10:07 下午*/
@Slf4j
@RestController
@Api(tags = "买家端,会员接口")
@RequestMapping("/buyer/passport/member")
public class MemberBuyerController {@Autowiredprivate MemberService memberService;@ApiOperation(value = "web-获取手机App/小程序登录二维码")@PostMapping(value = "/pc_session", produces = "application/json;charset=UTF-8")public ResultMessage<Object> createPcSession() {return ResultUtil.data(memberService.createPcSession());}/*** 长轮询:参考nacos** 此方法的返回值中的二维码登录结果信息的状态可以是 1,2,3,4,返回 1 是因为需要在前端展示该二维码已经扫码的的状态,* 返回 1 然后前端会再次调用此长轮询方法,并且之后(针对当前token来说)就不能再次返回 1 了,不然前端就会在未授权期间不断调用此接口了!* 所以为了增加token状态的判断,我们在入参中添加了 beforeSessionStatus 参数,表示上次记录的session状态** @param token* @param beforeSessionStatus 上次记录的session状态,前端只可能传递 0 或 1* @return*/@ApiOperation(value = "web-二维码长轮训校验登录")@PostMapping(value = "/session_login/{token}", produces = "application/json;charset=UTF-8")public Object loginWithSession(@PathVariable("token") String token, Integer beforeSessionStatus) {log.info("receive login with session key {}", token);//ResponseEntity继承了HttpEntity类,HttpEntity代表一个http请求或者响应实体ResponseEntity<ResultMessage<Object>> timeoutResponseEntity =new ResponseEntity<>(ResultUtil.error(ResultCode.ERROR), HttpStatus.OK);int timeoutSecond = 20;//建立一次连接,让他们等待尽可能长的时间。这样同时如果有新的数据到达服务器,服务器可以直接返回响应DeferredResult<ResponseEntity<Object>> deferredResult = new DeferredResult<>(timeoutSecond * 1000L, timeoutResponseEntity);//异步执行CompletableFuture.runAsync(() -> {try {int i = 0;while (i < timeoutSecond) {//根据二维码 token 获取二维码登录结果信息QRLoginResultVo queryResult = memberService.loginWithSession(token);int status = queryResult.getStatus();//为了满足接口调用,此处借助于 beforeSessionStatus 来判断。//但是源代码里面写的是下面这个逻辑,我觉得不太好理解,于是按照此方法的使用流程写了自己的思考(其实就是将他的判断反转了一下,但是这个思维更好理解点,我觉得好理解了)
//                    if (status == beforeSessionStatus
//                            && (QRCodeLoginSessionStatusEnum.WAIT_SCANNING.getCode() == status
//                            || QRCodeLoginSessionStatusEnum.SCANNING.getCode() == status)) {//如果status是等待扫描,             并且 beforeSessionStatus 是等待扫描,则(true || false && true) = true//如果status是已经扫描/同意/拒绝/过期,并且 beforeSessionStatus 是等待扫描,则( false || T/F && false) = false//如果status是已经扫描,             并且 beforeSessionStatus 是已经扫描,则( false || true && true) = true//如果status是同意/拒绝/过期,        并且 beforeSessionStatus 是已经扫描,则( false || false && false) = falseif (QRCodeLoginSessionStatusEnum.WAIT_SCANNING.getCode() == status|| (QRCodeLoginSessionStatusEnum.SCANNING.getCode() == status)&& status == beforeSessionStatus) {//睡眠一秒种,继续等待结果TimeUnit.SECONDS.sleep(1);} else {//设置长轮询的返回值deferredResult.setResult(new ResponseEntity<>(ResultUtil.data(queryResult), HttpStatus.OK));break;}i++;}} catch (Exception e) {log.error("获取登录状态异常,", e);deferredResult.setResult(new ResponseEntity<>(ResultUtil.error(ResultCode.ERROR), HttpStatus.OK));Thread.currentThread().interrupt();}}, Executors.newCachedThreadPool());//返回长轮询return deferredResult;}@ApiOperation(value = "App/小程序扫码")@PostMapping(value = "/app_scanner", produces = "application/json;charset=UTF-8")public ResultMessage<Object> appScanner(String token) {return ResultUtil.data(memberService.appScanner(token));}@ApiOperation(value = "app扫码-登录确认:同意/拒绝")@ApiImplicitParams({@ApiImplicitParam(name = "token", value = "sessionToken", required = true, paramType = "query"),@ApiImplicitParam(name = "code", value = "操作:0拒绝登录,1同意登录", required = true, paramType = "query")})@PostMapping(value = "/app_confirm", produces = "application/json;charset=UTF-8")public ResultMessage<Object> appSConfirm(String token, Integer code) {boolean flag = memberService.appSConfirm(token, code);return flag ? ResultUtil.success() : ResultUtil.error(ResultCode.ERROR);}...
}//cn.lili.modules.member.serviceimpl.MemberServiceImpl
@Service
public class MemberServiceImpl extends ServiceImpl<MemberMapper, Member> implements MemberService {@Overridepublic QRCodeLoginSessionVo createPcSession() {//创建二维码信息QRCodeLoginSessionVo session = new QRCodeLoginSessionVo();//设置二维码状态:等待扫码session.setStatus(QRCodeLoginSessionStatusEnum.WAIT_SCANNING.getCode());//过期时间,20sLong duration = 20 * 1000L;session.setDuration(duration);String token = CachePrefix.QR_CODE_LOGIN_SESSION.name() + SnowFlake.getIdStr();session.setToken(token);//将二维码信息缓存起来cache.put(token, session, duration, TimeUnit.MILLISECONDS);return session;}@Overridepublic Object appScanner(String token) {//获取当前登录用户。这里也没用到,其实可以去掉,或者先存到二维码结果里面AuthUser tokenUser = UserContext.getCurrentUser();if (tokenUser == null) {throw new ServiceException(ResultCode.USER_NOT_LOGIN);}//根据token获取二维码信息QRCodeLoginSessionVo session = (QRCodeLoginSessionVo) cache.get(token);if (session == null) {//没有二维码或者二维码已过期,则返回二维码不存在/或者已经过期状态的二维码信息return QRCodeLoginSessionStatusEnum.NO_EXIST.getCode();}//将拿到的二维码状态修改:已经扫码session.setStatus(QRCodeLoginSessionStatusEnum.SCANNING.getCode());//然后重新缓存二维码信息cache.put(token, session, session.getDuration(), TimeUnit.MILLISECONDS);//返回二维码状态return QRCodeLoginSessionStatusEnum.SCANNING.getCode();}@Overridepublic boolean appSConfirm(String token, Integer code) {//获取当前登录用户。AuthUser tokenUser = UserContext.getCurrentUser();if (tokenUser == null) {throw new ServiceException(ResultCode.USER_NOT_LOGIN);}//根据 token 获取二维码信息QRCodeLoginSessionVo session = (QRCodeLoginSessionVo) cache.get(token);if (session == null) {//没有二维码或者二维码已过期,则返回二维码不存在/或者已经过期状态的二维码信息return false;}if (code == 1) {//若登录状态是同意,则修改状态:确认登录session.setStatus(QRCodeLoginSessionStatusEnum.VERIFIED.getCode());//并且设置用户idsession.setUserId(Long.parseLong(tokenUser.getId()));} else {//若登录状态是拒绝,则修改状态:取消登录session.setStatus(QRCodeLoginSessionStatusEnum.CANCELED.getCode());}//然后重新缓存二维码信息cache.put(token, session, session.getDuration(), TimeUnit.MILLISECONDS);return true;}@Overridepublic QRLoginResultVo loginWithSession(String sessionToken) {//创建二维码登录结果对象QRLoginResultVo result = new QRLoginResultVo();result.setStatus(QRCodeLoginSessionStatusEnum.NO_EXIST.getCode());//获取根据token获取缓存里的二维码信息QRCodeLoginSessionVo session = (QRCodeLoginSessionVo) cache.get(sessionToken);if (session == null) {//没有二维码或者二维码已过期,则返回二维码不存在/或者已经过期状态的二维码信息return result;}result.setStatus(session.getStatus());//若存在二维码,则校验状态是否是:确认登录,是的话会修改二维码登录结果状态if (QRCodeLoginSessionStatusEnum.VERIFIED.getCode().equals(session.getStatus())) {//若是,则根据二维码里面的会员id拿到帐号信息Member member = this.getById(session.getUserId());if (member == null) {throw new ServiceException(ResultCode.USER_NOT_EXIST);} else {//拿到帐号信息后,生成tokenToken token = memberTokenGenerate.createToken(member, false);//将token添加到二维码登录结果result.setToken(token);//删除缓存里面的二维码信息cache.vagueDel(sessionToken);}}//返回二维码登录结果return result;}...
}
前端的部分代码  /lilishop-ui-master/buyer/src/pages/Login.vue//调用web-二维码长轮训校验登录async qrLogin() {if(!this.qrSessionToken) return;sCLogin(this.qrSessionToken,{beforeSessionStatus:this.scannerCodeLoginStatus}).then(response=>{if (response.success) {//拿到响应里面的二维码结果状态,并设置给 scannerCodeLoginStatus ,再下次调用此方法时会传递this.scannerCodeLoginStatus = response.result.status;switch (response.result.status) {case 0:case 1://已经扫码状态,继续调用web-二维码长轮训校验登录接口this.qrLogin();break;case 2://已经授权状态,调用登录成功方法this.loginSuccess(response.result.token.accessToken,response.result.token.refreshToken);break;case 3://拒绝授权状态,调用刷新二维码方法this.createPCLoginSession();break;default:this.clearQRLoginInfo();break}}  else{this.clearQRLoginInfo();}});},

【Lilishop商城】No4-2.业务逻辑的代码开发,涉及到:会员B端第三方登录的开发-平台注册会员接口开发相关推荐

  1. 【Lilishop商城】No4-1.业务逻辑的代码开发,涉及到:会员B端第三方登录使用及后端接口(微信、QQ等)

    仅涉及后端,全部目录看顶部专栏,代码.文档.接口路径在: [Lilishop商城]记录一下B2B2C商城系统学习笔记~_清晨敲代码的博客-CSDN博客 全篇会结合业务介绍重点设计逻辑,其中重点包括接口 ...

  2. 【Lilishop商城】No4-6.业务逻辑的代码开发,涉及到:接口入参、出参开发逻辑,及POJO的各种总结

     仅涉及后端,全部目录看顶部专栏,代码.文档.接口路径在: [Lilishop商城]记录一下B2B2C商城系统学习笔记~_清晨敲代码的博客-CSDN博客 全篇会结合业务介绍重点设计逻辑,其中重点包括接 ...

  3. (转)淘淘商城系列——在业务逻辑中添加缓存

    http://blog.csdn.net/yerenyuan_pku/article/details/72871268 上文我们一起学习了如何使用Spring容器来管理Redis单机版和集群版实现,本 ...

  4. 分销系统商城小程序业务逻辑功能设计_OctShop

    在移动互联网的不断发展壮大的形势下,用户和流量也在飞速的激增.小程序商城的体系也越来越成熟,很多小程序商城的营销方式也不断涌现.如:限时抢,拼团,优惠券等等,其中分销系统商城是受欢迎的,引起了高度关注 ...

  5. SSM米米商城项目笔笔记二(登录业务逻辑实现)

    米米商城项目笔笔记二(登录业务逻辑实现) Service层业务逻辑实现 由于在笔记一中已经完成了底层的搭建,所以可以直接上手service层代码的编写 在service包下创建AdminService ...

  6. 如何先梳理业务逻辑再写代码

    1.业务逻辑与代码 代码是需求逻辑的一种展现形式 需求文档是业务逻辑的一种展现形式,而代码不过是业务逻辑的另一种表现形式:如果逻辑本身有问题,那么它的各种展示形式自然也是错的,所以写代码前应该先思考清 ...

  7. 项目代码架构-业务分层和各层业务逻辑

    项目代码架构分层 1.代码分层现状 传统项目开发中,代码分层架构大概是controller层,Service层,Dao层,在SOA架构中会有facade层,Service层,Dao层,两种方式都是将所 ...

  8. 业务安全 –业务逻辑漏洞

    目录 业务安全 –业务逻辑漏洞 业务安全概述; 业务安全测试流程: 业务数据安全 商品支付金额篡改 前端JS限制绕过验证 请求重放测试 业务上线测试 *商品订购数量篡改 密码找回安全 注入 业务逻辑 ...

  9. 抽象工厂+反射+依赖注入 实现对数据访问层和业务逻辑层的优化

    分层思想的一个核心就是部件化,各个层之间是相互独立的,每一层可以随便抽取换成一个其他语言的版本,但只要与相应的接口吻合就行. 我用的三层架构大致是这样的,基本的三层就不说了,然后分别为业务逻辑层和数据 ...

最新文章

  1. SAP S/4HANA BP功能
  2. ubuntu14 安装JDK
  3. 电脑经典的小技巧48条
  4. memcached java 多线程_springboot使用memcache缓存
  5. CodeFirst 的编程方式
  6. 深度学习与概率、统计的有趣探讨
  7. java8 遍历目录_使用java8API遍历过滤文件目录及子目录及隐藏文件
  8. hdu 4407 Sum
  9. Hyper-v Server动态内存
  10. Reservoir Computing: Harnessing a Universal Dynamical System
  11. 图片服务 - thumbor自定义检测
  12. Linux配置JDK1.7和Resin4.0
  13. [转载] 财经郎眼20120526:山东首富挑战国家电网
  14. 排版怎么排?八大技巧提升版面设计感
  15. Linux 文本去重——uniq
  16. Latex数学用法总结
  17. 个人博客项目——登录和注册
  18. Vue echarts 修改 X轴、Y轴 样式以及文字样式
  19. css 设置鼠标经过的时候鼠标变成手状假装是个链接
  20. u盘格式化时提示“系统找不到指定文件”问题解决方法

热门文章

  1. SANER 2018 论文阅读- Dissection of a Bug Dataset: Anatomy of 395 Patches from Defects4J
  2. 404-Thenbsp;requestednbsp;reso…
  3. 如何快速查找下载文献
  4. Excel VBA ListBox列表框学习
  5. QQ头像变灰算法[图]
  6. 人工智能机器人——水中机器人
  7. 水安ABC考试多选练习题库
  8. 【100%通过率】华为OD机试真题 Python 实现【核酸最快检测效率】【2022.11 Q4 新题】
  9. 根据输入的年月日,确定这一天是星期几。
  10. 分享一个网易云会员包项目刷下载量的脚本