[Lilishop Mall] No4-2. Code development of business logic, involving: development of member B-side third-party login-platform registration member interface development

Only the backend is involved, see the top column for all directories, codes, documents, and interface paths are: 

[Lilishop Mall] Record the study notes of the B2B2C mall system~


The whole article will combine the business introduction to focus on the design logic, which includes the interface class and business class, and the specific source code analysis, the source code is not complicated to read~

Caution: Some comments in the source code are wrong, some comments have completely opposite meanings, and some comments are not correct. I updated them during the reading process and added new comments where I did not know. So be careful when reading the source code! 

Table of contents

A1. Member login module

B1. Member controller

C1. Platform registration member interface development

        Business logic:

        Code logic:

C2. Username and password login interface development

        Business logic:

        Code logic:

C3. SMS login interface development

        Business logic:

        Code logic: 

C4 . Development of Mobile App/Mini Program Scan QR Code Login Interface⭐

        Business logic:

        Code logic:


A1. Member login module

In  the article A5. Security framework (spring security) in No2-2. Determining the software architecture construction  , we have already learned about the development architecture of account authorization and authentication.

  1. After the account login is successful, it will receive the Token data returned by the backend, including accesstoken and refreshtoken; 
  2. The account carries the accesstoken to access the backend interface, it will be intercepted by the filter to get the account information from the accesstoken, judge that it is authorized, and then execute the interface.

So the member login module here only involves the above 1. , and you will definitely get Token in the end~

As for 2. You can check the filter inherited from BasicAuthenticationFilter in the api code of each end. The logic is A5 in No2-2, so I won’t repeat the explanation here~

B1. Member controller

C1. Platform registration member interface development

There is only one interface for registered members on the platform. You only need to have the user name, password, mobile phone number, and SMS verification code to create a member. And the interface will directly return the Token for account login, but it should be noted that the front end of the PC side does not use the Token after the registration is successful ~ the user needs to log in manually after the registration is successful


Business logic:

When introducing business logic, some other code structures will be involved. If there is any need to explain, it will be marked with green shading , and then it will be introduced in detail in the following code logic.

controller类:MemberBuyerController

  1. After receiving the input parameters, first verify the SMS verification code [the verification is also performed through the public SmsUtil operation ]
  2.  If there is a problem with the verification code verification, a ServiceException will be thrown . The exception type is : SMS verification code error, please re-verify. [The exception class is customized and needs to be caught and returned in the global exception handling class . The details will be added in the No2-* software architecture, the specific code can be seen: GlobalControllerExceptionHandler]
  3. If there is no problem with the verification, call the registration method of the member business class, get the token and return the response value ResultMessage.

service class: only use mybatis-plus, no custom mapper

  1. First check whether the username and mobile phone number in the membership information already exist, and if they exist, a ServiceException will be thrown. The exception type is: the username or mobile phone number has already been registered
  2. Convert the parameter to the user entity class Member, use the snowflake algorithm in the hutool toolkit to set the id , and then call the save method of IService to save the user to the database;
  3. Handle small events of membership registration: new members give points, new members give coupons, new members give experience, etc. [The logic here is processed by SpringEvent and RocketMQ. SpringEvent is used to publish messages, and its function is to decouple the program. RocketMQ processes specific business after receiving the message]
  4. Finally generate MemberTokenGenerate to generate token and return value; [MemberTokenGenerate see No2-2 A5]

Code logic:

//cn.lili.controller.passport.MemberBuyerController
@Slf4j
@RestController
@Api(tags = "买家端,会员接口")
@RequestMapping("/buyer/passport/member")
public class MemberBuyerController {
    @Autowired
    private MemberService memberService;
    @Autowired
    private 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
     */
    @Autowired
    private MemberTokenGenerate memberTokenGenerate;

    @Autowired
    private ApplicationEventPublisher applicationEventPublisher;

    @Override
    @Transactional
    public 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
     */
    @Transactional
    public 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. Public SmsUtil operations

For details, see: cn.lili.modules.sms.SmsUtil, which includes methods for sending SMS verification codes and verifying SMS verification codes, all of which are public.

In the method of sending SMS verification code, after sending to the mobile phone number, the verification code will eventually be stored in redis. Therefore, in the method of verifying the SMS verification code, the verification code of the registered Enums of the mobile phone number is also obtained from redis, and then compared and verified.

The SMS sending logic is used by a third party (Alibaba Cloud), and it is used according to the tools provided by Alibaba Cloud.

Another point is that because SmsUtil is public, there are different types of usage, such as: membership registration, login, password retrieval, etc. Different types of SMS verification code templates are different (the template code is also required in Alibaba Cloud. differentiated), so different types are needed to distinguish.

So there is a VerificationEnums enumeration class to mark the type, and application.yml is also used to configure the template information

//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 {

    @Autowired
    private Cache cache;
    @Autowired
    private SettingService settingService;
    @Autowired
    private MemberService memberService;

    @Autowired
    private SmsTemplateProperties smsTemplateProperties;

    @Autowired
    private SystemSettingProperties systemSettingProperties;

    @Override
    public void sendSmsCode(String mobile, VerificationEnums verificationEnums, String uuid) {
。。。
        //缓存中写入要验证的信息
        cache.put(cacheKey(verificationEnums, mobile, uuid), code, 300L);
    }

    @Override
    public 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.yml

lili:  
  #短信模版配置
  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 exception, exception type is, global exception handling class

ServiceException is a global business exception class, most of which are exceptions, and because there are many types of exceptions, it is necessary to have an exception type enumeration class to illustrate different types of exception information. The information includes code, message (some systems are Internationalized Message is used, but this system does not use it).

The finally thrown ServiceException is caught by GlobalControllerExceptionHandler and gets the code and message for processing.

Note: It is not as convenient to store the exception type in the enumeration class as in the configuration file. If you want to modify the exception type information, you need to modify the code and restart.

//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. Snowflake algorithm setting id in hutool toolkit

 The shop platform uses SnowFlake, which uses the hutool tool class. For specific distributed learning, you can read this article Distributed Global Unique ID (Learning Summary --- From Getting Started to Deepening) - CSDN Blog

//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}";

    @Autowired
    private Cache cache;

    /**
     * 尝试初始化
     *
     * @return
     */
    //Java自带的注解,在方法上加该注解会在项目启动的时候执行该方法
    @PostConstruct
    public 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

After the member registration in the service is successful, business operations such as coupons and points need to be issued to the member later. In order to avoid program coupling, the SpringEvent method is used, that is, the TransactionCommitSendMQListener event listener and the TransactionCommitSendMQEvent event. Then in the event listener, rocketMQTemplate is called to send the message, and finally it is processed in the rocket listener~

 To put it bluntly, SpringEvent and RocketMQ are used in combination. I don’t understand why they are used together, because it is not troublesome to use RocketMQ directly. The most important role of SpringEvent itself is business stripping and program decoupling, which are also the role of RocketMQ. [Other modules directly use RocketMQ]

Until I saw the class name and annotation of SpringEvent: mq event occurs after transaction submission, transaction submission listener, and @TransactionalEventListener annotation is used, I think that this class is only used to deal with related business after transaction submission.

//cn.lili.common.event.TransactionCommitSendMQEvent
/**
 * 事务提交后发生mq事件
 *
 * @author paulG
 * @since 2022/1/19
 **/
public class TransactionCommitSendMQEvent extends ApplicationEvent {

    private static final long serialVersionUID = 5885956821347953071L;

    @Getter
    private final String topic;

    @Getter
    private final String tag;

    @Getter
    private 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
     */
    @Autowired
    private 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. Username and password login interface development

This also has only one interface, and finally the Token for user login is obtained according to the user name and password.

Business logic:

When introducing business logic, some other code structures will be involved. If there is any need to explain, it will be marked with green shading , and then it will be introduced in detail in the following code logic.

controller类:MemberBuyerController

  1. After receiving the input parameters, first verify the picture verification code [verification is through  the VerificationService operation ]
  2. If there is a problem with the verification code verification, a ServiceException will be thrown. The exception type is: the verification code has expired, please re-verify.
  3. If there is no problem with the verification, call the user name and password login method of the member business class, get the token and return the response value ResultMessage.

service class: only use mybatis-plus, no custom mapper

  1. First obtain the account information corresponding to the user name or mobile phone number, and throw a ServiceException if it does not exist. The exception type is: the user does not exist.
  2. If the user exists, it is judged whether the password is entered correctly, and if it is incorrect, a ServiceException is thrown [because the password is saved by BCryptPasswordEncoder during registration, and it is naturally used for decryption]
  3. According to the obtained member account information, generate a token and return it;

Code logic:

//cn.lili.controller.passport.MemberBuyerController
@Slf4j
@RestController
@Api(tags = "买家端,会员接口")
@RequestMapping("/buyer/passport/member")
public class MemberBuyerController {

    @Autowired
    private MemberService memberService;
    @Autowired
    private 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
     */
    @Autowired
    private MemberTokenGenerate memberTokenGenerate;

    @Override
    public 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);
        //根据会员账号信息,生成token
        return memberTokenGenerate.createToken(member, false);
    }

...
}

1.  VerificationService operation

[Lilishop Mall] No3-2. Module detailed design A4 Detailed design of the slider verification code picture-CSDN blog  has described the process when the user uses the slider verification code to log in, here is the description:

Slider validation process:

1. The backend converts the basemap and the slider image into base64 and returns them, and at the same time stores the correct shadow X-axis position in redis (the key contains the uuid passed from the frontend for later getkey verification), and then returns it to the frontend exhibit.

2. The front end gets the base64 and converts it into a picture display, and realizes the dynamic effect of sliding. The user slides the slider to a certain position after seeing it. The position of the slider at this time is an input parameter. After letting go, call the calibration slider interface to get the correct X-axis position from redis and compare it with the position of the slider at this time. After passing, the cache verification is successful again (the key also contains the uuid just now), and then returns success.

3. After the front-end finds that the slider verification is successful, it calls the login interface. In the login interface, it will first obtain the verification success from the cache, and log in if the verification is successful.

The above three steps will correspond to the interface of a verification code module, and they are all quite easy to understand. Just remember that you need to store two types in redis, cache the content that needs to be verified, and cache the verification result.

//cn.lili.modules.verification.service.impl.VerificationServiceImpl
/**
 * 验证码认证处理类
 *
 * @author Chopper
 * @version v1.0
 * 2020-11-17 14:59
 */
@Slf4j
@Component
public class VerificationServiceImpl implements VerificationService {

    @Autowired
    private VerificationSourceService verificationSourceService;


    @Autowired
    private VerificationCodeProperties verificationCodeProperties;

    @Autowired
    private Cache cache;

    /**
     * 创建校验
     * @param uuid 前端传过来的的标识
     * @return 验证码参数
     */
    @Override
    public 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秒,可以自行配置,存储到redis
            cache.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 验证是否成功
     */
    @Override
    public 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 验证是否成功
     */
    @Override
    public 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. SMS login interface development

Business logic:

When introducing business logic, some other code structures will be involved. If there is any need to explain, it will be marked with green shading , and then it will be introduced in detail in the following code logic.

controller类:MemberBuyerController

  1. After receiving the input parameters, first verify the SMS verification code
  2. If there is a problem with the verification code verification, a ServiceException will be thrown
  3. If there is no problem with the verification, call the mobile phone number verification code login method of the membership business class, get the token and return the response value ResultMessage.

service class: only use mybatis-plus, no custom mapper

  1. Get users by phone number
  2. If the mobile phone number does not exist, the user will be registered automatically, using the abstracted method this.registerHandler(member)
  3. According to the member account information, generate token and return

Code logic: 

There is no complicated logic in this part, just remember that the account registered according to the mobile phone number, the user name is the mobile phone number!

Note: Just put the specific method of use directly. After all, you still have to learn from the source code, and it is not convenient to post the code here. The focus is on thinking and logic~ 

//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
    @Transactional
    public 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);
        //根据会员账号信息,生成token
        return memberTokenGenerate.createToken(member, false);
    }

C4. Development of mobile app/small program scanning QR code login interface

This one has a fourth interface, two are on the PC side, and two are on the mobile phone applet/APP side.

The focus is on the fourth long-round training verification QR code interface, which runs through the entire process.

  1. First, when the user clicks the scan code on the front end of the PC to log in, the back end will be called to obtain the QR code interface, and the back end will return the information of the QR code. The QR code information includes token, expiration time and other information. code display;
  2. The user scans the code with the App/small program. After scanning the code, he will get the token in the QR code, then call the back-end scan code interface, and use the token as an input parameter, and then get the cached QR code information according to the token. And modify the status to have scanned the code, and re-cache the QR code information, and then return to the status of the scanned QR code at this time;
  3. After the front-end gets the scanned code status, it will open the authorization confirmation page, click the authorization confirmation/rejection button, it will call the QR code login confirmation interface on the back-end, and use the token and authorization status as input parameters, and then get the cache based on the token QR code information, and modify the status to confirm/refuse to scan the code. If it is confirmed, the userid will be set to the current login user, and the QR code information will be re-cached, and then return success;
  4. After getting the QR code information in 1., the front-end takes the token and the waiting status as input parameters, and calls the back-end long-round training to verify the QR code interface. The interface judges whether the QR code is valid every second or not Ability to return connection results. If the user calls the code scanning interface of 2., the polling interface will return the QR code information that has been scanned. Then, after the front-end receives the response of the scanned code status, it will take the token and the scanned status as input parameters again, and call the back-end long-round training to verify the QR code interface. If the user calls the confirmation/rejection interface of 3., the polling interface will return the QR code information whose status is confirmation/rejection, and if the returned status is confirmation, it will include the login Token information. When the front end receives the confirmation status response, it executes the login success method according to Token. If it receives a rejection status response, it calls the method of refreshing the QR code.

Business logic:

The business logic can be seen above, which is very clearly described~

The key point is 4. The polling judgment in the interface, according to the business, it can be concluded that the status of the QR code login result information in the return value of this method can be 1: scanned, 2: agreed, 3: rejected, 4: Expired.

Returning 1 is because the status of the scanned QR code needs to be displayed on the front end.

After the status returned by the method is 1, the front-end needs to call this long polling method again, because it still needs to get the status of 2: agree/3: reject, but for the current token, it cannot return status 1 again, otherwise the front-end This interface will be called continuously during the unauthorized period! ! ! Need to add the corresponding judgment! ! !

Therefore, we added the beforeSessionStatus parameter to the input parameter of this method to indicate the session status recorded last time. When the front end calls for the first time, the value is 0: waiting for scanning code, when the back end returns 1: after scanning code , assign the new 1: already scanned code to the beforeSessionStatus parameter, and then the backend will return the final authorization result after judgment~~~

How does the back end judge? Look at the code logic below~~~

Code logic:

//cn.lili.controller.passport.MemberBuyerController

/**
 * 买家端,会员接口
 *
 * @author Chopper
 * @since 2020/11/16 10:07 下午
 */
@Slf4j
@RestController
@Api(tags = "买家端,会员接口")
@RequestMapping("/buyer/passport/member")
public class MemberBuyerController {

    @Autowired
    private 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) = false
                    if (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 {
    @Override
    public QRCodeLoginSessionVo createPcSession() {
        //创建二维码信息
        QRCodeLoginSessionVo session = new QRCodeLoginSessionVo();
        //设置二维码状态:等待扫码
        session.setStatus(QRCodeLoginSessionStatusEnum.WAIT_SCANNING.getCode());
        //过期时间,20s
        Long 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;
    }

    @Override
    public 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();
    }

    @Override
    public 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());
            //并且设置用户id
            session.setUserId(Long.parseLong(tokenUser.getId()));
        } else {
            //若登录状态是拒绝,则修改状态:取消登录
            session.setStatus(QRCodeLoginSessionStatusEnum.CANCELED.getCode());
        }
        //然后重新缓存二维码信息
        cache.put(token, session, session.getDuration(), TimeUnit.MILLISECONDS);

        return true;
    }

    @Override
    public 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 {
                //拿到帐号信息后,生成token
                Token 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();
        }
      });
    },

  

Guess you like

Origin blog.csdn.net/vaevaevae233/article/details/128440529