[Centro comercial Lilishop] No 4-2. Desarrollo de código de lógica comercial, que incluye: desarrollo de desarrollo de interfaz de miembro de registro de plataforma de inicio de sesión de terceros del lado B del miembro

Solo el backend está involucrado, vea la columna superior para todos los directorios, códigos, documentos y rutas de interfaz son: 

[Lilishop Mall] Grabe las notas de estudio del sistema de centro comercial B2B2C ~


Todo el artículo combinará la introducción comercial para centrarse en la lógica de diseño, que incluye la clase de interfaz y la clase comercial, y el análisis específico del código fuente, el código fuente no es complicado de leer~

Precaución: algunos comentarios en el código fuente son incorrectos, algunos comentarios tienen significados completamente opuestos y algunos comentarios no son correctos. Los actualicé durante el proceso de lectura y agregué nuevos comentarios donde no sabía. Así que tenga cuidado al leer el código fuente. ! 

Tabla de contenido

A1 Módulo de inicio de sesión de miembros

B1 Miembro controlador

C1. Desarrollo de interfaz de miembro de registro de plataforma

        Lógica de negocios:

        Lógica de código:

C2. Desarrollo de la interfaz de inicio de sesión con nombre de usuario y contraseña

        Lógica de negocios:

        Lógica de código:

C3. Desarrollo de la interfaz de inicio de sesión de SMS

        Lógica de negocios:

        Lógica de código: 

C4 .Desarrollo de la interfaz de inicio de sesión de código QR de escaneo de aplicación móvil/mini programa⭐

        Lógica de negocios:

        Lógica de código:


A1 Módulo de inicio de sesión de miembros

En  el artículo A5 Marco de seguridad (seguridad de primavera) en No2-2 Determinación de la construcción de la arquitectura de software  , ya hemos aprendido sobre la arquitectura de desarrollo de autorización y autenticación de cuentas.

  1. Después de que el inicio de sesión de la cuenta sea exitoso, recibirá los datos del token devueltos por el backend, incluido el token de acceso y el token de actualización; 
  2. La cuenta lleva el token de acceso para acceder a la interfaz de back-end, será interceptado por el filtro para obtener la información de la cuenta del token de acceso, juzgar que está autorizado y luego ejecutar la interfaz.

Entonces, el módulo de inicio de sesión de miembros aquí solo involucra el 1 anterior, y definitivamente obtendrá Token al final ~

En cuanto a 2. Puede verificar el filtro heredado de BasicAuthenticationFilter en el código api de cada extremo.La lógica es A5 en No2-2, por lo que no repetiré la explicación aquí ~

B1 Miembro controlador

C1. Desarrollo de interfaz de miembro de registro de plataforma

Solo hay una interfaz para miembros registrados en la plataforma. Solo necesita tener el nombre de usuario, la contraseña, el número de teléfono móvil y el código de verificación de SMS para crear un miembro. Y la interfaz devolverá directamente el Token para el inicio de sesión de la cuenta, pero debe tenerse en cuenta que la parte frontal del lado de la PC no usa el Token después de que el registro sea exitoso ~ el usuario debe iniciar sesión manualmente después de que el registro sea exitoso


Lógica de negocios:

Al introducir la lógica de negocios, estarán involucradas algunas otras estructuras de código. Si hay alguna necesidad de explicar, se marcará con sombreado verde y luego se presentará en detalle en la siguiente lógica de código.

controlador类:Miembro CompradorControlador

  1. Después de recibir los parámetros de entrada, primero verifique el código de verificación de SMS [la verificación también se realiza a través de la operación pública de SmsUtil ]
  2. Si hay un problema con la verificación del código de verificación, se lanzará  una ServiceException . El tipo de excepción es : error de código de verificación de SMS, vuelva a verificar. [La clase de excepción está personalizada y debe capturarse y devolverse en la clase de manejo de excepciones global . Los detalles se agregarán en la arquitectura de software No2-*, el código específico se puede ver: GlobalControllerExceptionHandler]
  3. Si no hay problema con la verificación, llame al método de registro de la clase empresarial miembro, obtenga el token y devuelva el valor de respuesta ResultMessage.

clase de servicio: solo use mybatis-plus, sin mapeador personalizado

  1. Primero verifique si el nombre de usuario y el número de teléfono móvil en la información de membresía ya existen, y si existen, se lanzará una ServiceException. El tipo de excepción es: el nombre de usuario o el número de teléfono móvil ya se ha registrado
  2. Convierta el parámetro en el miembro de clase de entidad de usuario, use el algoritmo de copo de nieve en el kit de herramientas de hutool para configurar el id y luego llame al método de guardado de IService para guardar al usuario en la base de datos;
  3. Maneje pequeños eventos de registro de membresía: los nuevos miembros dan puntos, los nuevos miembros dan cupones, los nuevos miembros dan experiencia, etc. [La lógica aquí es procesada por SpringEvent y RocketMQ. SpringEvent se usa para publicar mensajes, y su función es desacoplar el programa. RocketMQ procesa negocios específicos después de recibir el mensaje]
  4. Finalmente genere MemberTokenGenerate para generar token y devolver valor; [MemberTokenGenerate vea No2-2 A5]

Lógica de código:

//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. Operaciones públicas de SmsUtil

Para obtener más información, consulte: cn.lili.modules.sms.SmsUtil, que incluye métodos para enviar códigos de verificación de SMS y verificar códigos de verificación de SMS, todos los cuales son públicos.

En el método de envío del código de verificación por SMS, después de enviarlo al número de teléfono móvil, el código de verificación finalmente se almacenará en redis. Por lo tanto, en el método de verificación del código de verificación de SMS, el código de verificación de los Enum registrados del número de teléfono móvil también se obtiene de redis y luego se compara y verifica.

La lógica de envío de SMS la utiliza un tercero (Alibaba Cloud) y se utiliza de acuerdo con las herramientas proporcionadas por Alibaba Cloud.

Otro punto es que debido a que SmsUtil es público, existen diferentes tipos de uso, tales como: registro de membresía, inicio de sesión, recuperación de contraseña, etc. Los diferentes tipos de plantillas de código de verificación de SMS son diferentes (el código de plantilla también se requiere en Alibaba Cloud. diferenciado ), por lo que se necesitan diferentes tipos para distinguir.

Entonces, hay una clase de enumeración VerificationEnums para marcar el tipo, y application.yml también se usa para configurar la información de la plantilla.

//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. Excepción ServiceException, el tipo de excepción es, clase de manejo de excepción global

ServiceException es una clase de excepción comercial global, la mayoría de las cuales son excepciones, y debido a que hay muchos tipos de excepciones, es necesario tener una clase de enumeración de tipo de excepción para ilustrar los diferentes tipos de información de excepción. La información incluye código, mensaje (algunos sistemas se utiliza Mensaje Internacionalizado, pero este sistema no lo utiliza).

La ServiceException lanzada finalmente es capturada por GlobalControllerExceptionHandler y obtiene el código y el mensaje para su procesamiento.

Nota: No es tan conveniente almacenar el tipo de excepción en la clase de enumeración como en el archivo de configuración.Si desea modificar la información del tipo de excepción, debe modificar el código y reiniciar.

//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. ID de configuración del algoritmo Snowflake en el kit de herramientas hutool

 La plataforma de la tienda usa SnowFlake, que usa la clase de herramienta hutool. Para aprendizaje distribuido específico, puede leer este artículo ID único global distribuido (Resumen de aprendizaje --- Desde el inicio hasta la profundización) - 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. Evento de primavera, RocketMQ

Después de que el registro del miembro en el servicio sea exitoso, las operaciones comerciales, como cupones y puntos, deben emitirse al miembro más adelante. Para evitar el acoplamiento de programas, se utiliza el método SpringEvent, es decir, el detector de eventos TransactionCommitSendMQListener y el evento TransactionCommitSendMQEvent. . Luego, en el detector de eventos, se llama a coheteMQTemplate para enviar el mensaje, y finalmente se procesa en el detector de cohetes ~

 Para decirlo sin rodeos, SpringEvent y RocketMQ se usan en combinación. No entiendo por qué se usan juntos, porque no es problemático usar RocketMQ directamente. La función más importante de SpringEvent en sí es la eliminación de negocios y el desacoplamiento de programas, que son también el papel de RocketMQ. [Otros módulos usan directamente RocketMQ]

Hasta que vi el nombre de la clase y la anotación de SpringEvent: el evento mq ocurre después del envío de la transacción, se usa el oyente del envío de la transacción y la anotación @TransactionalEventListener, creo que esta clase solo se usa para tratar negocios relacionados después del envío de la transacción.

//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. Desarrollo de la interfaz de inicio de sesión con nombre de usuario y contraseña

Este también tiene una sola interfaz, y finalmente se obtiene el Token para el inicio de sesión del usuario de acuerdo con el nombre de usuario y la contraseña.

Lógica de negocios:

Al introducir la lógica de negocios, estarán involucradas algunas otras estructuras de código. Si hay alguna necesidad de explicar, se marcará con sombreado verde y luego se presentará en detalle en la siguiente lógica de código.

controlador类:Miembro CompradorControlador

  1. Después de recibir los parámetros de entrada, primero verifique el código de verificación de la imagen [la verificación se realiza a través  de la operación VerificationService ]
  2. Si hay un problema con la verificación del código de verificación, se generará una ServiceException. El tipo de excepción es: el código de verificación ha caducado, vuelva a verificar.
  3. Si no hay ningún problema con la verificación, llame al método de inicio de sesión de nombre de usuario y contraseña de la clase empresarial miembro, obtenga el token y devuelva el valor de respuesta ResultMessage.

clase de servicio: solo use mybatis-plus, sin mapeador personalizado

  1. Primero obtenga la información de la cuenta correspondiente al nombre de usuario o número de teléfono móvil, y lance una ServiceException si no existe, el tipo de excepción es: el usuario no existe.
  2. Si el usuario existe, se evalúa si la contraseña se ingresó correctamente y, si es incorrecta, se lanza una ServiceException [porque BCryptPasswordEncoder guarda la contraseña durante el registro y, naturalmente, se usa para descifrar]
  3. De acuerdo con la información de la cuenta de miembro obtenida, genere un token y devuélvalo;

Lógica de código:

//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.  Operación del servicio de verificación

[Lilishop Mall] No 3-2. Diseño detallado del módulo A4 Diseño detallado del código de verificación del control deslizante imagen-CSDN blog  ha descrito el proceso cuando el usuario usa el código de verificación del control deslizante para iniciar sesión, aquí está la descripción:

Proceso de validación del control deslizante:

1. El backend convierte el mapa base y la imagen del control deslizante en base64 y los devuelve, y al mismo tiempo almacena la posición correcta del eje X de la sombra en redis (la clave contiene el uuid pasado desde el frontend para una posterior verificación de getkey), y luego lo devuelve a la exhibición frontal.

2. La parte delantera obtiene la base64 y la convierte en una pantalla de imagen y se da cuenta del efecto dinámico de deslizamiento. El usuario desliza el control deslizante a una determinada posición después de verlo. La posición del control deslizante en este momento es un parámetro de entrada. Después de soltarlo, llame a la interfaz del control deslizante de calibración para obtener la posición correcta del eje X de redis y compararla con la posición del control deslizante en este momento.Después de pasar, la verificación de caché es exitosa nuevamente (la clave también contiene el uuid en este momento), y luego devuelve éxito.

3. Después de que el front-end descubre que la verificación del control deslizante fue exitosa, llama a la interfaz de inicio de sesión. En la interfaz de inicio de sesión, primero obtendrá el éxito de la verificación del caché e iniciará sesión si la verificación es exitosa.

Los tres pasos anteriores corresponderán a la interfaz de un módulo de código de verificación, y todos son bastante fáciles de entender. Solo recuerde que debe almacenar dos tipos en redis, almacenar en caché el contenido que debe verificarse y almacenar en caché el resultado de la verificación . .

//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. Desarrollo de la interfaz de inicio de sesión de SMS

Lógica de negocios:

Al introducir la lógica de negocios, estarán involucradas algunas otras estructuras de código. Si hay alguna necesidad de explicar, se marcará con sombreado verde y luego se presentará en detalle en la siguiente lógica de código.

controlador类:Miembro CompradorControlador

  1. Después de recibir los parámetros de entrada, primero verifique el código de verificación de SMS
  2. Si hay un problema con la verificación del código de verificación, se lanzará una ServiceException
  3. Si no hay problema con la verificación, llame al método de inicio de sesión del código de verificación del número de teléfono móvil de la clase comercial de membresía, obtenga el token y devuelva el valor de respuesta ResultMessage.

clase de servicio: solo use mybatis-plus, sin mapeador personalizado

  1. Obtener usuarios por número de teléfono
  2. Si el número de teléfono móvil no existe, el usuario se registrará automáticamente, utilizando el método abstracto this.registerHandler(member)
  3. De acuerdo con la información de la cuenta del miembro, genere token y devuelva

Lógica de código: 

No hay una lógica complicada en esta parte, solo recuerde que la cuenta registrada de acuerdo con el número de teléfono móvil, ¡el nombre de usuario es el número de teléfono móvil!

Nota: Simplemente coloque el método específico de uso directamente. Después de todo, todavía tiene que aprender del código fuente, y no es conveniente publicar el código aquí. El enfoque está en el pensamiento y la lógica ~ 

//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. Desarrollo de interfaz de inicio de sesión de código QR de escaneo de aplicaciones móviles/pequeños programas

Este tiene una cuarta interfaz, dos están en el lado de la PC y dos están en el lado del applet/APP del teléfono móvil.

La atención se centra en la cuarta interfaz de código QR de verificación de capacitación de larga duración, que se ejecuta a través de todo el proceso.

  1. Primero, cuando el usuario hace clic en el código de escaneo en el extremo frontal de la PC para iniciar sesión, se llamará al extremo posterior para obtener la interfaz del código QR, y el extremo posterior devolverá la información del código QR. incluye token, tiempo de caducidad y otra información visualización de código;
  2. El usuario escanea el código con la aplicación/pequeño programa.Después de escanear el código, obtendrá el token en el código QR, luego llamará a la interfaz de código de escaneo de back-end, usará el token como parámetro de entrada y luego obtendrá el información del código QR almacenada en caché de acuerdo con el token.Y modifique el estado para haber escaneado el código, y vuelva a almacenar en caché la información del código QR, y luego regrese al estado del código QR escaneado en este momento;
  3. Después de que el front-end obtenga el estado del código escaneado, abrirá la página de confirmación de autorización, hará clic en el botón de confirmación/rechazo de autorización, llamará a la interfaz de confirmación de inicio de sesión del código QR en el back-end y utilizará el token y el estado de autorización como ingrese los parámetros, y luego obtenga el caché basado en la información del código QR del token, y modifique el estado para confirmar / rechazar escanear el código.Si se confirma, el ID de usuario se establecerá en el usuario de inicio de sesión actual y la información del código QR se volverá a almacenar en caché y luego devolverá el éxito;
  4. Después de obtener la información del código QR en 1., el front-end toma el token y el estado de espera como parámetros de entrada, y llama al back-end de entrenamiento de ronda larga para verificar la interfaz del código QR.La interfaz juzga si el código QR es válido cada segundo o no Capacidad para devolver resultados de conexión. Si el usuario llama a la interfaz de escaneo de código de 2., la interfaz de sondeo devolverá la información del código QR que se ha escaneado. Luego, después de que el front-end reciba la respuesta del estado del código escaneado, tomará el token y el estado escaneado como parámetros de entrada nuevamente, y llamará al back-end de entrenamiento de ronda larga para verificar la interfaz del código QR. Si el usuario llama a la interfaz de confirmación/rechazo de 3., la interfaz de sondeo devolverá la información del código QR cuyo estado es confirmación/rechazo, y si el estado devuelto es confirmación, incluirá la información del token de inicio de sesión. Cuando el front-end recibe la respuesta de estado de confirmación, ejecuta el método de éxito de inicio de sesión de acuerdo con Token. Si recibe una respuesta de estado de rechazo, llama al método de actualización del código QR.

Lógica de negocios:

La lógica comercial se puede ver arriba, que se describe muy claramente ~

El punto clave es 4. El juicio de sondeo en la interfaz, según el negocio, se puede concluir que el estado de la información del resultado de inicio de sesión del código QR en el valor de retorno de este método puede ser 1: escaneado, 2: acordado, 3 : rechazado, 4: Caducado.

Devolver 1 se debe a que el estado del código QR escaneado debe mostrarse en la parte delantera.

Después de que el estado devuelto por el método sea 1, el front-end debe volver a llamar a este método de sondeo largo, porque aún necesita obtener el estado 2: aceptar/3: rechazar, pero para el token actual, no puede devolver el estado. 1 de nuevo, de lo contrario, el front-end ¡Esta interfaz se llamará continuamente durante el período no autorizado! ! ! Necesidad de añadir el juicio correspondiente! ! !

Por lo tanto, agregamos el parámetro beforeSessionStatus al parámetro de entrada de este método para indicar el estado de la sesión registrado la última vez.Cuando el front-end llama por primera vez, el valor es 0: esperando el código de escaneo, cuando el back-end devuelve 1: después de escanear el código, asigne el nuevo 1: código ya escaneado al parámetro beforeSessionStatus, y luego el backend devolverá el resultado de autorización final después del juicio ~~~

¿Cómo juzga el back-end? Mire la lógica del código a continuación ~~~

Lógica de código:

//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();
        }
      });
    },

  

Supongo que te gusta

Origin blog.csdn.net/vaevaevae233/article/details/128440529
Recomendado
Clasificación