樂(lè)優(yōu)商城學(xué)習(xí)筆記二十六-購(gòu)物車(二)

4.已登錄購(gòu)物車

接下來(lái)咳蔚,我們完成已登錄購(gòu)物車酌媒。

在剛才的未登錄購(gòu)物車編寫時(shí)拥娄,我們已經(jīng)預(yù)留好了編寫代碼的位置,邏輯也基本一致熙暴。

4.1.添加登錄校驗(yàn)

購(gòu)物車系統(tǒng)只負(fù)責(zé)登錄狀態(tài)的購(gòu)物車處理闺属,因此需要添加登錄校驗(yàn),我們通過(guò)JWT鑒權(quán)即可實(shí)現(xiàn)周霉。

4.1.1.引入JWT相關(guān)依賴

我們引入之前寫的鑒權(quán)工具:ly-auth-common

<dependency>
    <groupId>com.leyou.service</groupId>
    <artifactId>ly-auth-common</artifactId>
    <version>1.0.0-SNAPSHOT</version>
</dependency>

4.1.2.配置公鑰

ly:
  jwt:
    pubKeyPath: E:/nginx/rsa/rsa.pub # 公鑰地址
    cookieName: LY_TOKEN # cookie的名稱

4.1.3.加載公鑰

image

代碼:

@ConfigurationProperties(prefix = "ly.jwt")
public class JwtProperties {

    private String pubKeyPath;// 公鑰

    private PublicKey publicKey; // 公鑰

    private String cookieName;

    private static final Logger logger = LoggerFactory.getLogger(JwtProperties.class);

    @PostConstruct
    public void init(){
        try {
            // 獲取公鑰和私鑰
            this.publicKey = RsaUtils.getPublicKey(pubKeyPath);
        } catch (Exception e) {
            logger.error("初始化公鑰失數嗥鳌!", e);
            throw new RuntimeException();
        }
    }

    public String getPubKeyPath() {
        return pubKeyPath;
    }

    public void setPubKeyPath(String pubKeyPath) {
        this.pubKeyPath = pubKeyPath;
    }

    public PublicKey getPublicKey() {
        return publicKey;
    }

    public void setPublicKey(PublicKey publicKey) {
        this.publicKey = publicKey;
    }

    public String getCookieName() {
        return cookieName;
    }

    public void setCookieName(String cookieName) {
        this.cookieName = cookieName;
    }
}

4.1.4.編寫過(guò)濾器

因?yàn)楹芏嘟涌诙夹枰M(jìn)行登錄诗眨,我們直接編寫SpringMVC攔截器唉匾,進(jìn)行統(tǒng)一登錄校驗(yàn)。同時(shí)匠楚,我們還要把解析得到的用戶信息保存起來(lái)巍膘,以便后續(xù)的接口可以使用。

image

代碼:

public class LoginInterceptor extends HandlerInterceptorAdapter {

    private JwtProperties jwtProperties;

    // 定義一個(gè)線程域芋簿,存放登錄用戶
    private static final ThreadLocal<UserInfo> tl = new ThreadLocal<>();

    public LoginInterceptor(JwtProperties jwtProperties) {
        this.jwtProperties = jwtProperties;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 查詢token
        String token = CookieUtils.getCookieValue(request, "LY_TOKEN");
        if (StringUtils.isBlank(token)) {
            // 未登錄,返回401
            response.setStatus(HttpStatus.UNAUTHORIZED.value());
            return false;
        }
        // 有token峡懈,查詢用戶信息
        try {
            // 解析成功,證明已經(jīng)登錄
            UserInfo user = JwtUtils.getInfoFromToken(token, jwtProperties.getPublicKey());
            // 放入線程域
            tl.set(user);
            return true;
        } catch (Exception e){
            // 拋出異常与斤,證明未登錄,返回401
            response.setStatus(HttpStatus.UNAUTHORIZED.value());
            return false;
        }

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        tl.remove();
    }

    public static UserInfo getLoginUser() {
        return tl.get();
    }
}

注意:

  • 這里我們使用了ThreadLocal來(lái)存儲(chǔ)查詢到的用戶信息肪康,線程內(nèi)共享,因此請(qǐng)求到達(dá)Controller后可以共享User
  • 并且對(duì)外提供了靜態(tài)的方法:getLoginUser()來(lái)獲取User信息

4.1.5.配置過(guò)濾器

配置SpringMVC撩穿,使過(guò)濾器生效:

image
@Configuration
@EnableConfigurationProperties(JwtProperties.class)
public class MvcConfig implements WebMvcConfigurer {

    @Autowired
    private JwtProperties jwtProperties;

    @Bean
    public LoginInterceptor loginInterceptor() {
        return new LoginInterceptor(jwtProperties);
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor()).addPathPatterns("/**");
    }
}

4.2.后臺(tái)購(gòu)物車設(shè)計(jì)

數(shù)據(jù)結(jié)構(gòu)設(shè)計(jì)

當(dāng)用戶登錄時(shí)磷支,我們需要把購(gòu)物車數(shù)據(jù)保存到后臺(tái),可以選擇保存在數(shù)據(jù)庫(kù)食寡。但是購(gòu)物車是一個(gè)讀寫頻率很高的數(shù)據(jù)雾狈。因此我們這里選擇讀寫效率比較高的Redis作為購(gòu)物車存儲(chǔ)。

Redis有5種不同數(shù)據(jù)結(jié)構(gòu)抵皱,這里選擇哪一種比較合適呢善榛?

  • 首先不同用戶應(yīng)該有獨(dú)立的購(gòu)物車,因此購(gòu)物車應(yīng)該以用戶的作為key來(lái)存儲(chǔ)呻畸,Value是用戶的所有購(gòu)物車信息移盆。這樣看來(lái)基本的k-v結(jié)構(gòu)就可以了。
  • 但是伤为,我們對(duì)購(gòu)物車中的商品進(jìn)行增咒循、刪、改操作绞愚,基本都需要根據(jù)商品id進(jìn)行判斷剑鞍,為了方便后期處理,我們的購(gòu)物車也應(yīng)該是k-v結(jié)構(gòu)爽醋,key是商品id蚁署,value才是這個(gè)商品的購(gòu)物車信息。

綜上所述蚂四,我們的購(gòu)物車結(jié)構(gòu)是一個(gè)雙層Map:Map<String,Map<String,String>>

  • 第一層Map光戈,Key是用戶id
  • 第二層Map,Key是購(gòu)物車中商品id遂赠,值是購(gòu)物車數(shù)據(jù)

實(shí)體類

public class Cart {
    private Long userId;// 用戶id
    private Long skuId;// 商品id
    private String title;// 標(biāo)題
    private String image;// 圖片
    private Long price;// 加入購(gòu)物車時(shí)的價(jià)格
    private Integer num;// 購(gòu)買數(shù)量
    private String ownSpec;// 商品規(guī)格參數(shù)

    public Long getUserId() {
        return userId;
    }

    public void setUserId(Long userId) {
        this.userId = userId;
    }

    public Long getSkuId() {
        return skuId;
    }

    public void setSkuId(Long skuId) {
        this.skuId = skuId;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getImage() {
        return image;
    }

    public void setImage(String image) {
        this.image = image;
    }

    public Long getPrice() {
        return price;
    }

    public void setPrice(Long price) {
        this.price = price;
    }

    public Integer getNum() {
        return num;
    }

    public void setNum(Integer num) {
        this.num = num;
    }

    public String getOwnSpec() {
        return ownSpec;
    }

    public void setOwnSpec(String ownSpec) {
        this.ownSpec = ownSpec;
    }
}

4.3.添加商品到購(gòu)物車

4.3.1.頁(yè)面發(fā)起請(qǐng)求:

已登錄情況下久妆,向后臺(tái)添加購(gòu)物車:

image

這里發(fā)起的是Json請(qǐng)求。那么我們后臺(tái)也要以json接收跷睦。

4.3.2.后臺(tái)添加購(gòu)物車

controller

先分析一下:

  • 請(qǐng)求方式:新增筷弦,肯定是Post
  • 請(qǐng)求路徑:/cart ,這個(gè)其實(shí)是Zuul路由的路徑,我們可以不管
  • 請(qǐng)求參數(shù):Json對(duì)象烂琴,包含skuId和num屬性
  • 返回結(jié)果:無(wú)
@RequestMapping
public class CartController {

    @Autowired
    private CartService cartService;

    /**
     * 添加購(gòu)物車
     *
     * @return
     */
    @PostMapping
    public ResponseEntity<Void> addCart(@RequestBody Cart cart) {
        this.cartService.addCart(cart);
        return ResponseEntity.ok().build();
    }
}

Service

這里我們不訪問(wèn)數(shù)據(jù)庫(kù)爹殊,而是直接操作Redis〖楸粒基本思路:

  • 先查詢之前的購(gòu)物車數(shù)據(jù)
  • 判斷要添加的商品是否存在
    • 存在:則直接修改數(shù)量后寫回Redis
    • 不存在:新建一條數(shù)據(jù)梗夸,然后寫入Redis

代碼:

@Service
public class CartService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private GoodsClient goodsClient;

    static final String KEY_PREFIX = "ly:cart:uid:";

    static final Logger logger = LoggerFactory.getLogger(CartService.class);

    public void addCart(Cart cart) {
        // 獲取登錄用戶
        UserInfo user = LoginInterceptor.getLoginUser();
        // Redis的key
        String key = KEY_PREFIX + user.getId();
        // 獲取hash操作對(duì)象
        BoundHashOperations<String, Object, Object> hashOps = this.redisTemplate.boundHashOps(key);
        // 查詢是否存在
        Long skuId = cart.getSkuId();
        Integer num = cart.getNum();
        Boolean boo = hashOps.hasKey(skuId.toString());
        if (boo) {
            // 存在,獲取購(gòu)物車數(shù)據(jù)
            String json = hashOps.get(skuId.toString()).toString();
            cart = JsonUtils.parse(json, Cart.class);
            // 修改購(gòu)物車數(shù)量
            cart.setNum(cart.getNum() + num);
        } else {
            // 不存在号醉,新增購(gòu)物車數(shù)據(jù)
            cart.setUserId(user.getId());
            // 其它商品信息反症, 需要查詢商品服務(wù)
            ResponseEntity<Sku> resp = this.goodsClient.querySkuById(skuId);
            if (resp.getStatusCode() != HttpStatus.OK || !resp.hasBody()) {
                logger.error("添加購(gòu)物車的商品不存在:skuId:{}", skuId);
                throw new RuntimeException();
            }
            Sku sku = resp.getBody();
            cart.setImage(StringUtils.isBlank(sku.getImages()) ? "" : StringUtils.split(sku.getImages(), ",")[0]);
            cart.setPrice(sku.getPrice());
            cart.setTitle(sku.getTitle());
            cart.setOwnSpec(sku.getOwnSpec());
        }
        // 將購(gòu)物車數(shù)據(jù)寫入redis
        hashOps.put(cart.getSkuId().toString(), JsonUtils.serialize(cart));
    }
}

4.3.3.結(jié)果:

image

4.4.查詢購(gòu)物車

4.4.1.頁(yè)面發(fā)起請(qǐng)求

image

4.4.2.后臺(tái)實(shí)現(xiàn)

Controller

/**
 * 查詢購(gòu)物車列表
 *
 * @return
 */
@GetMapping
public ResponseEntity<List<Cart>> queryCartList() {
    List<Cart> carts = this.cartService.queryCartList();
    if (carts == null) {
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null);
    }
    return ResponseEntity.ok(carts);
}

Service

public List<Cart> queryCartList() {
        // 獲取登錄用戶
        UserInfo user = LoginInterceptor.getLoginUser();

        // 判斷是否存在購(gòu)物車
        String key = KEY_PREFIX + user.getId();
        if(!this.redisTemplate.hasKey(key)){
            // 不存在,直接返回
            return null;
        }
        BoundHashOperations<String, Object, Object> hashOps = this.redisTemplate.boundHashOps(key);
        List<Object> carts = hashOps.values();
        // 判斷是否有數(shù)據(jù)
        if(CollectionUtils.isEmpty(carts)){
            return null;
        }
        // 查詢購(gòu)物車數(shù)據(jù)
        return carts.stream().map(o -> JsonUtils.parse(o.toString(), Cart.class)).collect(Collectors.toList());
    }

4.4.3.測(cè)試

image

4.5.修改商品數(shù)量

4.5.1.頁(yè)面發(fā)起請(qǐng)求

image

4.5.2.后臺(tái)實(shí)現(xiàn)

Controller

@PutMapping
public ResponseEntity<Void> updateNum(@RequestParam("skuId") Long skuId, 
                                      @RequestParam("num") Integer num) {
    this.cartService.updateNum(skuId, num);
    return ResponseEntity.ok().build();
}

Service

public void updateNum(Long skuId, Integer num) {
    // 獲取登錄用戶
    UserInfo user = LoginInterceptor.getLoginUser();
    String key = KEY_PREFIX + user.getId();
    BoundHashOperations<String, Object, Object> hashOps = this.redisTemplate.boundHashOps(key);
    // 獲取購(gòu)物車
    String json = hashOps.get(skuId.toString()).toString();
    Cart cart = JsonUtils.parse(json, Cart.class);
    cart.setNum(num);
    // 寫入購(gòu)物車
    hashOps.put(skuId.toString(), JsonUtils.serialize(cart));
}

4.6.刪除購(gòu)物車商品

4.6.1.頁(yè)面發(fā)起請(qǐng)求

image

注意:后臺(tái)成功響應(yīng)后畔派,要把頁(yè)面的購(gòu)物車中數(shù)據(jù)也刪除

4.6.2.后臺(tái)實(shí)現(xiàn)

Controller

@DeleteMapping("{skuId}")
public ResponseEntity<Void> deleteCart(@PathVariable("skuId") String skuId) {
    this.cartService.deleteCart(skuId);
    return ResponseEntity.ok().build();
}

Service

public void deleteCart(String skuId) {
    // 獲取登錄用戶
    UserInfo user = LoginInterceptor.getLoginUser();
    String key = KEY_PREFIX + user.getId();
    BoundHashOperations<String, Object, Object> hashOps = this.redisTemplate.boundHashOps(key);
    hashOps.delete(skuId);
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末铅碍,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子线椰,更是在濱河造成了極大的恐慌胞谈,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,386評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件士嚎,死亡現(xiàn)場(chǎng)離奇詭異呜魄,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)莱衩,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,142評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門爵嗅,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人笨蚁,你說(shuō)我怎么就攤上這事睹晒。” “怎么了括细?”我有些...
    開封第一講書人閱讀 164,704評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵伪很,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我奋单,道長(zhǎng)锉试,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,702評(píng)論 1 294
  • 正文 為了忘掉前任览濒,我火速辦了婚禮呆盖,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘贷笛。我一直安慰自己应又,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,716評(píng)論 6 392
  • 文/花漫 我一把揭開白布乏苦。 她就那樣靜靜地躺著株扛,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上洞就,一...
    開封第一講書人閱讀 51,573評(píng)論 1 305
  • 那天盆繁,我揣著相機(jī)與錄音,去河邊找鬼奖磁。 笑死改基,一個(gè)胖子當(dāng)著我的面吹牛繁疤,可吹牛的內(nèi)容都是我干的咖为。 我是一名探鬼主播,決...
    沈念sama閱讀 40,314評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼稠腊,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼躁染!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起架忌,我...
    開封第一講書人閱讀 39,230評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤吞彤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后叹放,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體饰恕,經(jīng)...
    沈念sama閱讀 45,680評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,873評(píng)論 3 336
  • 正文 我和宋清朗相戀三年井仰,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了埋嵌。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,991評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡俱恶,死狀恐怖雹嗦,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情合是,我是刑警寧澤了罪,帶...
    沈念sama閱讀 35,706評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站聪全,受9級(jí)特大地震影響泊藕,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜难礼,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,329評(píng)論 3 330
  • 文/蒙蒙 一娃圆、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧鹤竭,春花似錦踊餐、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,910評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春窜管,著一層夾襖步出監(jiān)牢的瞬間散劫,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,038評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工幕帆, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留获搏,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,158評(píng)論 3 370
  • 正文 我出身青樓失乾,卻偏偏與公主長(zhǎng)得像常熙,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子碱茁,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,941評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容