[springboot 開(kāi)發(fā)單體web shop] 5. 用戶登錄及首頁(yè)展示

用戶登錄及前端展示


用戶登錄

在之前的文章中我們實(shí)現(xiàn)了用戶注冊(cè)和驗(yàn)證功能,接下來(lái)我們繼續(xù)實(shí)現(xiàn)它的登錄,以及登錄成功之后要在頁(yè)面上顯示的信息。
接下來(lái),我們來(lái)編寫(xiě)代碼。


實(shí)現(xiàn)service

com.liferunner.service.IUserService接口中添加用戶登錄方法:

public interface IUserService {
    ...
    /**
     * 用戶登錄
     * @param userRequestDTO 請(qǐng)求dto
     * @return 登錄用戶信息
     * @throws Exception
     */
    Users userLogin(UserRequestDTO userRequestDTO) throws Exception;
}

然后,在com.liferunner.service.impl.UserServiceImpl實(shí)現(xiàn)類(lèi)中實(shí)現(xiàn):

@Service
@Slf4j
public class UserServiceImpl implements IUserService {
    ...
    @Override
    public Users userLogin(UserRequestDTO userRequestDTO) throws Exception {
        log.info("======用戶登錄請(qǐng)求:{}", userRequestDTO);
        Example example = new Example(Users.class);
        val condition = example.createCriteria();
        condition.andEqualTo("username", userRequestDTO.getUsername());
        condition.andEqualTo("password", MD5GeneratorTools.getMD5Str(userRequestDTO.getPassword()));
        val user = this.usersMapper.selectOneByExample(example);
        log.info("======用戶登錄處理結(jié)果:{}", user);
        return user;
    }
}

Error Tips:
這里有一個(gè)小小的坑點(diǎn),大家一定要注意,在使用selectOneByExample()查詢的時(shí)候,該方法傳入的參數(shù)一定注意是tk.mybatis.mapper.entity.Example實(shí)例,而不是tk.mybatis.mapper.entity.Example.Criteria,否則會(huì)報(bào)動(dòng)態(tài)SQL生成查詢錯(cuò)誤,信息如下:

org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.reflection.ReflectionException: There is no getter for property named 'distinct' in 'class tk.mybatis.mapper.entity.Example$Criteria'
  at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:92)
  at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:440)
  at com.sun.proxy.$Proxy106.selectOne(Unknown Source)
  at org.mybatis.spring.SqlSessionTemplate.selectOne(SqlSessionTemplate.java:159)
  at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:87)
  at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:93)
  at com.sun.proxy.$Proxy109.selectOneByExample(Unknown Source)
  at com.liferunner.service.impl.UserServiceImpl.userLogin(UserServiceImpl.java:80)
  ...

新人在寫(xiě)代碼的時(shí)候,特別容易在上一行寫(xiě)了查詢變量,下一行就直接開(kāi)用了,越是簡(jiǎn)單的錯(cuò)誤越是讓人無(wú)從下手。

實(shí)現(xiàn)Controller

@RestController
@RequestMapping(value = "/users")
@Slf4j
@Api(tags = "用戶管理")
public class UserController {
    ...
    @ApiOperation(value = "用戶登錄", notes = "用戶登錄接口")
    @PostMapping("/login")
    public JsonResponse userLogin(@RequestBody UserRequestDTO userRequestDTO,
                                  HttpServletRequest request,
                                  HttpServletResponse response) {
        try {
            if (StringUtils.isBlank(userRequestDTO.getUsername()))
                return JsonResponse.errorMsg("用戶名不能為空");
            if (StringUtils.isBlank(userRequestDTO.getPassword()) ||
                    userRequestDTO.getPassword().length() < 8) {
                return JsonResponse.errorMsg("密碼為空或長(zhǎng)度小于8位");
            }
            val user = this.userService.userLogin(userRequestDTO);
            UserResponseDTO userResponseDTO = new UserResponseDTO();
            BeanUtils.copyProperties(user, userResponseDTO);
            log.info("BeanUtils copy object {}", userResponseDTO);
            if (null != userResponseDTO) {
                // 設(shè)置前端存儲(chǔ)的cookie信息
                CookieTools.setCookie(request, response, "user",
                        JSON.toJSONString(userResponseDTO), true);
                return JsonResponse.ok(userResponseDTO);
            }
        } catch (Exception e) {
            e.printStackTrace();
            log.error("用戶登錄失敗,{},exception = {}", userRequestDTO, e.getMessage());
        }
        return JsonResponse.errorMsg("用戶登錄失敗");
    }
}

在上面的代碼中,基本校驗(yàn)問(wèn)題就不再贅述,我們主要關(guān)注幾點(diǎn)新的特性信息:

  • com.liferunner.dto.UserResponseDTO 將我們需要展示給前端的數(shù)據(jù)封裝為一個(gè)新的返回對(duì)象,我們從數(shù)據(jù)庫(kù)中查詢出來(lái)的Userspojo包含用戶的所有數(shù)據(jù),比如其中的passwordmobile等等一些用戶私密的數(shù)據(jù)是不應(yīng)該展示給前端的,即便要展示,那也是需要經(jīng)過(guò)脫敏以及加密。因此,常見(jiàn)的做法就是封裝一個(gè)新的返回對(duì)象,其中只需要包含前端需要的數(shù)據(jù)字段就可以了。
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@ApiModel(value = "用戶信息返回DTO", description = "用戶登錄成功后需要的返回對(duì)象")
public class UserResponseDTO {
    /**
     * 主鍵id
     */
    private String id;

    /**
     * 用戶名
     */
    private String username;

    /**
     * 昵稱(chēng) 昵稱(chēng)
     */
    private String nickname;

    /**
     * 頭像
     */
    private String face;

    /**
     * 性別  1:男  0:女  2:保密
     */
    private Integer sex;
}

在這里建議大家使用Ctrl+C我們的com.liferunner.pojo.Users對(duì)象,然后刪除掉我們不需要的字段就可以了,為什么這么建議呢,是因?yàn)橄乱粋€(gè)好處啦。

  • org.springframework.beans.BeanUtils.copyProperties(user, userResponseDTO);
    大家可以看到,這里直接使用的是Spring BeanUtils工具類(lèi)進(jìn)行的值拷貝,就減少了我們循環(huán)遍歷每一個(gè)字段去挨個(gè)賦值(SetValue)的工作。(也是一種偷懶小技巧哦,這樣是不對(duì)的~)

  • CookieTools.setCookie();
    之前我們有提過(guò),一般情況下,我們用戶登錄之后,數(shù)據(jù)都會(huì)被存儲(chǔ)在本地瀏覽器Cookie中,比如我登錄的baidu.com:

    baidu

    此時(shí),鼠標(biāo)在圖片中左側(cè)的Cookies => www.baidu.com右鍵clear,然后再次刷新我們當(dāng)前界面,效果如下:
    clear cookies

    我們可以看到,從登錄狀態(tài)已經(jīng)變?yōu)橥顺鰻顟B(tài)了,并且Cookies中的內(nèi)容也少了很多,這就說(shuō)明,百度是把我們的用戶登錄信息加密后存儲(chǔ)在了瀏覽器cookie中。
    大家可以查看京東,淘寶等等,也是基于這種方式實(shí)現(xiàn)的,開(kāi)篇之初就說(shuō)過(guò),我們的系統(tǒng)是基于生產(chǎn)來(lái)實(shí)現(xiàn)的demo,那么我們就是用主流的實(shí)現(xiàn)方法來(lái)做。當(dāng)然,有的同學(xué)會(huì)說(shuō),這個(gè)應(yīng)該我們把數(shù)據(jù)傳遞給前端,讓前端來(lái)實(shí)現(xiàn)的?。?!當(dāng)然,你說(shuō)的對(duì),可是我們掌握一種實(shí)現(xiàn)方式,對(duì)于我們個(gè)人而言應(yīng)該是沒(méi)有壞處的吧?
    這里就需要一個(gè)工具類(lèi)了,大家可以在github傳送門(mén)來(lái)下載相關(guān)代碼。目錄com.liferunner.utils.CookieTools.

  • com.alibaba.fastjson.JSON.toJSONString(userResponseDTO)
    因?yàn)槲覀円祷氐氖且粋€(gè)對(duì)象,但是cookie中我們需要放入的是String,這里我們引入了alibaba的JSON工具,在mscx-shop-common/pom.xml,加入依賴(lài):

        <dependencies>
          <dependency>
              <groupId>com.alibaba</groupId>
              <artifactId>fastjson</artifactId>
              <version>1.2.56</version>
          </dependency>
      </dependencies>
    

用戶登出

在用戶操作結(jié)束之后,我們需要將用戶從系統(tǒng)中退出登錄,因?yàn)槲覀兊挠脩舻卿浶畔?huì)存儲(chǔ)在瀏覽器cookie中,因此,我們需要根據(jù)用戶的登出操作來(lái)刪除相關(guān)用戶緩存:

    @ApiOperation(value = "用戶登出",notes = "用戶登出",httpMethod = "POST")
    @PostMapping("/logout")
    public JsonResponse userLogout(@RequestParam String uid,
        HttpServletRequest request,HttpServletResponse response){
        // clear front's user cookies
        CookieTools.deleteCookie(request,response,"user");
        // return operational result
        return JsonResponse.ok();
    }

開(kāi)發(fā)調(diào)試小福利

java日志追蹤

一般在電商場(chǎng)景中,對(duì)于請(qǐng)求的響應(yīng)時(shí)間有著極其嚴(yán)格的要求,比如你在一個(gè)網(wǎng)站買(mǎi)商品的時(shí)候,如果每點(diǎn)擊一次按鈕都要等待,或者系統(tǒng)感覺(jué)卡頓一下,你會(huì)毫不猶豫的選擇右上角的小紅叉,把它干掉。因此,在我們系統(tǒng)的開(kāi)發(fā)過(guò)程中,很多時(shí)候需要對(duì)我們的請(qǐng)求響應(yīng)時(shí)間進(jìn)行監(jiān)控,甚至?xí)ㄟ^(guò)壓力測(cè)試來(lái)進(jìn)行測(cè)試。但是,讓我們?cè)诿恳粋€(gè)方法中都做這種請(qǐng)求的實(shí)現(xiàn),顯然是不合理甚至說(shuō)是讓開(kāi)發(fā)人員難受的,所以,我們來(lái)實(shí)現(xiàn)一種通用的做法,那就是通過(guò)AOP,面向切面來(lái)實(shí)現(xiàn)。關(guān)于切面的基本使用,大家可以參考AOP傳送門(mén),接下來(lái),開(kāi)始我們的編碼。
根據(jù)springboot實(shí)現(xiàn)功能三部曲:
setp 1. 添加依賴(lài)

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

step 2. 啟動(dòng)配置(沒(méi)有就忽略掉這一步)
setp 3. 加注解
在我們的mscx-shop-api項(xiàng)目中,創(chuàng)建com.liferunner.api.aspectpackage,然后創(chuàng)建com.liferunner.api.aspect.CommonLogAspect,代碼如下:

package com.liferunner.api.aspect;

import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

import java.util.Date;

/**
 * CommonLogAspect for : AOP切面實(shí)現(xiàn)日志確認(rèn)
 *
 * @author <a href="mailto:magicianisaac@gmail.com">Isaac.Zhang | 若初</a>
 * @since 2019/11/11
 */
@Component
@Aspect
@Slf4j
public class CommonLogAspect {

    @Around("execution(* com.liferunner.api.controller..*.*(..))")
    public void recordLogTime(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        log.info("----------- {}.{} process log time started.---------------",
                proceedingJoinPoint.getTarget().getClass(),
                proceedingJoinPoint.getSignature().getName());

        val startTime = System.currentTimeMillis();
        proceedingJoinPoint.proceed();
        val afterTime = System.currentTimeMillis();
        if (afterTime - startTime > 1000) {
            log.warn("cost : {}", afterTime - startTime);
        } else {
            log.info("cost : {}", afterTime - startTime);
        }

        log.info("----------- {}.{} process log time ended.---------------",
                proceedingJoinPoint.getSourceLocation().getClass(),
                proceedingJoinPoint.getSignature().getName());
    }
}
  • 第一行日志代表我們想要監(jiān)控的是哪個(gè)類(lèi)的哪個(gè)方法
  • proceedingJoinPoint.proceed();表示方法執(zhí)行
  • 當(dāng)請(qǐng)求查過(guò)1000ms的時(shí)候,我們使用log.warn(...)進(jìn)行日志告警

step 4. 效果演示

  • 查詢用戶耗時(shí)


    get
  • 注冊(cè)用戶耗時(shí)


    insert

從上圖,我們明顯能看出來(lái)我們每一次的請(qǐng)求耗時(shí),之后就可以針對(duì)性的對(duì)每一個(gè)方法進(jìn)行優(yōu)化?。?!

sql日志追蹤

在我們開(kāi)發(fā)的過(guò)程中,往往會(huì)遇到針對(duì)數(shù)據(jù)庫(kù)的CRUD的操作,但是,因?yàn)槲覀兪褂昧?code>mybatis 動(dòng)態(tài)生成了簡(jiǎn)單的SQL查詢,而不是手動(dòng)編寫(xiě)的,比如我們?cè)?code>UserServiceImpl.java中實(shí)現(xiàn)的用戶查詢以及用戶注冊(cè)代碼中的tk.mybatis.mapper.entity.Example 以及 this.usersMapper.insertSelective(user);


    public Users findUserByUserName(String username) {
        // 構(gòu)建查詢條件
        Example example = new Example(Users.class);
        val condition = example.createCriteria()
                .andEqualTo("username", username);
        return this.usersMapper.selectOneByExample(example);
    }

    @Transactional(propagation = Propagation.REQUIRED)
    @Override
    public Users createUser(UserRequestDTO userRequestDTO) throws Exception {
        log.info("======begin create user : {}=======", userRequestDTO);
        val user = Users.builder()
                .id(sid.next()) //生成分布式id
                .username(userRequestDTO.getUsername())
                .password(MD5GeneratorTools.getMD5Str(userRequestDTO.getPassword()))
                .birthday(DateUtils.parseDate("1970-01-01", "yyyy-MM-dd"))
                .nickname(userRequestDTO.getUsername())
                .face(this.FACE_IMG)
                .sex(SexEnum.secret.type)
                .createdTime(new Date())
                .updatedTime(new Date())
                .build();
        this.usersMapper.insertSelective(user);
        log.info("======end create user : {}=======", userRequestDTO);
        return user;
    }

一旦遇到了問(wèn)題之后,我們往往不知道到底是哪里出現(xiàn)了錯(cuò)誤,這個(gè)時(shí)候我們的SQL是否有問(wèn)題我們也不知道,因此,接下來(lái)我們來(lái)配置一種可以讓我們到SQL的小實(shí)現(xiàn):

1.設(shè)置日志配置(如圖)

log4j.properties

2.修改mybatis配置(log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
mybatis

3.SELECT效果演示
result

4.INSERT效果演示
INSERT

從上圖可以看出控制臺(tái)JDBC操作進(jìn)行了2次,其實(shí)第一次是對(duì)我們的用戶名進(jìn)行校驗(yàn)。第二次INSERT是真實(shí)的插入。

通過(guò)上面的演示結(jié)果,大家可以想到,這個(gè)日志針在我們?nèi)粘5拈_(kāi)發(fā)中解決問(wèn)題是非常有必要的。但是一定記得,在上生產(chǎn)的時(shí)候,日志一定要關(guān)閉,否則數(shù)據(jù)量一旦大了之后,會(huì)對(duì)系統(tǒng)的性能造成嚴(yán)重傷害!?。?/p>

源碼下載

Github 傳送門(mén)
Gitee 傳送門(mén)

下節(jié)預(yù)告


下一節(jié)我們將繼續(xù)開(kāi)發(fā)我們電商的核心部分-商品以及廣告的展示,在過(guò)程中使用到的任何開(kāi)發(fā)組件,我都會(huì)通過(guò)專(zhuān)門(mén)的一節(jié)來(lái)進(jìn)行介紹的,兄弟們末慌!

gogogo!


奔跑的人生 | 博客園 | segmentfault | spring4all | csdn | 掘金 | OSChina | 簡(jiǎn)書(shū) | 頭條 | 知乎 | 51CTO

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • ORA-00001: 違反唯一約束條件 (.) 錯(cuò)誤說(shuō)明:當(dāng)在唯一索引所對(duì)應(yīng)的列上鍵入重復(fù)值時(shí),會(huì)觸發(fā)此異常。 O...
    我想起個(gè)好名字閱讀 6,023評(píng)論 0 9
  • 1.abstract class 和interface 有什么區(qū)別? 抽象類(lèi)可以有構(gòu)造方法 接口不行 抽象類(lèi)可以有...
    sunnysans閱讀 967評(píng)論 0 1
  • --- layout: post title: "如果有人問(wèn)你關(guān)系型數(shù)據(jù)庫(kù)的原理,叫他看這篇文章(轉(zhuǎn))" date...
    藍(lán)墜星閱讀 919評(píng)論 0 3
  • 一、MySQL優(yōu)化 MySQL優(yōu)化從哪些方面入手: (1)存儲(chǔ)層(數(shù)據(jù)) 構(gòu)建良好的數(shù)據(jù)結(jié)構(gòu)??梢源蟠蟮奶嵘覀僑...
    寵辱不驚丶?xì)q月靜好閱讀 2,663評(píng)論 1 8
  • 初冬的白晝已經(jīng)變得很短,在11樓的陽(yáng)臺(tái)上看見(jiàn)落日橘黃的余暉,走下樓,路燈已經(jīng)亮起來(lái)了。 我把手插在口袋,獨(dú)自悠閑地...
    如小玉閱讀 314評(píng)論 0 1

友情鏈接更多精彩內(nèi)容