前言


  1.   学习视频:Java项目《谷粒商城》架构师级Java项目实战,对标阿里P6-P7,全网最强
  2.  学习文档: 
    1. 谷粒商城 - 个人笔记(基础篇一)
    2. 谷粒商城 - 个人笔记(基础篇二)
    3. 谷粒商城 - 个人笔记(基础篇三)
    4. 谷粒商城 - 个人笔记(高级篇一)
    5. 谷粒商城 - 个人笔记(高级篇二)
    6. 谷粒商城 - 个人笔记(高级篇三)
    7. 谷粒商城 - 个人笔记(高级篇四)
    8. 谷粒商城 - 个人笔记(高级篇五)
    9. 谷粒商城 - 个人笔记(集群部署篇一)
    10. 谷粒商城 - 个人笔记(集群部署篇二)
    11. 谷粒商城 - 个人笔记(集群部署篇三)
  3.  接口文档:谷粒商城接口文档

  4. 本内容仅用于个人学习笔记,如有侵扰,联系删除

六、异步


初始化线程的 4 种方式:

  1. 继承Thread
  2. 实现Runnable
  3. 实现Callable接口+FutureTask(可以拿到返回结果,可以处理异常)
  4. 线程池
创建“com.atguigu.gulimall.search.thread.ThreadTest”类,代码如下:

1、继承 Thread 类: 这是最简单的方式之一。你可以创建一个类,继承自 Thread 类,并重写它的 run() 方法来定义线程要执行的任务。然后通过创建该类的实例并调用 start() 方法来启动线程。 

package com.atguigu.gulimall.search.thread;

import java.util.concurrent.*;

public class ThreadTest {

    public static void main(String[] args) {
        System.out.println("main.............start.......");

        // 1、继承Thread
        Thread01 thread01 = new Thread01();
        thread01.start(); //启动线程

        System.out.println("main.............end.......");
    }

    public static class Thread01 extends Thread{
        @Override
        public void run() {
            System.out.println("当前线程:"+Thread.currentThread().getId());
            int i = 10/2;
            System.out.println("当前运行结果:" + i);
        }
    }
}

结果

2、实现 Runnable 接口: 这种方式更加灵活,因为 Java 是单继承的,如果你的类已经继承了其他类,你仍然可以实现 Runnable 接口来实现多线程。

然后你可以通过创建 Thread 实例,传入 Runnable 对象来启动线程:

package com.atguigu.gulimall.search.thread;

import java.util.concurrent.*;

public class ThreadTest {

    public static void main(String[] args) {
        System.out.println("main.............start.......");

        // 2、实现Runnable
        Runnable01 runnable01 = new Runnable01();
        new Thread(runnable01).start();

        System.out.println("main.............end.......");
    }

    public static class Runnable01 implements Runnable{
        @Override
        public void run() {
            System.out.println("当前线程:"+Thread.currentThread().getId());
            int i = 10/2;
            System.out.println("当前运行结果:" + i);
        }
    }
}

结果

3、实现 Callable 接口 + FutureTask: Callable 接口允许线程返回结果,并且可以抛出异常。结合 FutureTask 可以异步获取线程执行结果。

 使用 FutureTask 来获取线程执行结果:

package com.atguigu.gulimall.search.thread;

import java.util.concurrent.*;

public class ThreadTest {

    public static void main(String[] args) throws ExecutionException, InterruptedException{
        System.out.println("main.............start.......");

        // 3、实现Callable接口+FutureTask
        FutureTask<Integer> futureTask = new FutureTask<>(new Callable01());
        new Thread(futureTask).start();
        // 阻塞等待整个线程执行完成,获取返回结果
        Integer integer = futureTask.get();
        System.out.println("获取的返回结果:" + integer);

        System.out.println("main.............end.......");
    }

    public static class Callable01 implements Callable<Integer>{
        @Override
        public Integer call() {
            System.out.println("当前线程:"+Thread.currentThread().getId());
            int i = 10/2;
            System.out.println("当前运行结果:" + i);
            return i;
        }
    }

结果

 4、线程池: 使用线程池可以管理和复用线程,避免频繁地创建和销毁线程,提高了资源利用率。

package com.atguigu.gulimall.search.thread;

import java.util.concurrent.*;

public class ThreadTest {
    // 方式一:初始化一个线程池
//    public static ExecutorService executor = Executors.newFixedThreadPool(10);

    // 方式二:初始化一个线程池
    public static ThreadPoolExecutor executor = new ThreadPoolExecutor(
                5,
                200,
                10,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(100000),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());

    public static void main(String[] args) {

        System.out.println("main.............start.......");

        // 4、线程池
        executor.execute(new Runnable01());

        System.out.println("main.............end.......");
    }

    public static class Runnable01 implements Runnable{
        @Override
        public void run() {
            System.out.println("当前线程:"+Thread.currentThread().getId());
            int i = 10/2;
            System.out.println("当前运行结果:" + i);
        }
    }
}

 结果

总结:

我们以后再业务代码里面,以上三种启动线程的方式都不用。将所有的多线程异步任务都交给线程池执行。

分析:
  • 方式1 和 方式2:主进程无法获取线程的运算结果,不适合当前场景。
  • 方式3:主进程可以获取线程的运算结果,但是不利于控制服务器中的线程资源。可以导致服务器资源耗尽。
  • 方式4:可以控制资源,性能稳定。

通过如下两种方式初始化线程池 

Executors.newFiexedThreadPool(3);
//或者
new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit unit, workQueue, threadFactory, handler);

 通过线程池性能稳定,也可以获取执行结果,并捕获异常。但是,在业务复杂情况下,一个异步调用可能会依赖于另一个异步调用的执行结果。

1、线程池

1.1、七大参数

  1. corePoolSize[5]:核心线程数[一直存在,除非(allowCoreThreadTimeOut)];线程池,创建好以后就准备就绪的线程数量,就等待接收异步任务去执行5个 Thread thread = new Thread(); thread.start();
  2. maximumPoolSize[200]:最大线程数量;控制资源。
  3. keepAliveTime:存活时间。如果当前正在运行的线程数量大于core数量,释放空闲的线程(maximumPoolSize-corePoolSize)。只要线程空闲大于指定的keepAliveTime。
  4. unit:时间单位。
  5. BlockingQueue<Runnable> workQueue:阻塞队列。如果任务有很多。就会将多的任务放在队列里面,只要有线程空闲,就会去队列里面取出新的任务继续执行。
  6. ThreadFactory:线程创建的工厂。
  7. RejectedExecutionHandler handler:如果队列满了,按照我们指定的拒绝策略,拒绝执行。

1.2、运行流程

  1. 线程池创建,准备好 core 数量的核心线程,准备接受任务。
  2. 新的任务进来,用 core 准备好的空闲线程执行。
    1.  core 满了,就将再进来的任务放入阻塞队列中。空闲的 core 就会自己去阻塞队 列获取任务执行。
    2. 阻塞队列满了,就直接开新线程执行,最大只能开到 max 指定的数量。
    3. max 都执行好了。Max-core 数量空闲的线程会在 keepAliveTime 指定的时间后自动销毁。最终保持到 core 大小。
    4. 如果线程数开到了 max 的数量,还有新任务进来,就会使用 reject 指定的拒绝策 略进行处理。
  3. 所有的线程创建都是由指定的 factory 创建的。
     ThreadPoolExecutor executor = new ThreadPoolExecutor(
                5,
                200,
                10,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(100000),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());
面试:
一个线程池 core 7 max 20 queue 50 100 并发进来怎么分配的;
先有 7 个能直接得到执行,接下来 50 个进入队列排队,在多开 13 个继续执行。现在 70
被安排上了。剩下 30 个默认拒绝策略。

1.3、常见的4种线程池

  • newCachedThreadPool

创建一个可缓存的线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程

  • newFixedThreadPool

创建一个定长线程池,可控制线程最大并发数,超出的线程会在对垒中等待。

  • newScheduledThreadPool

创建一个定长线程池。支持定势及周期性人去执行

  • newSingleThreadExecutor

创建一个单线程化的线程池,他只会用唯一的工作线程来执行任务,保证所有任务。

1.4、使用线程池的好处

  • 降低资源的消耗
    • 通过重复利用已经创建好的线程降低线程的创建和销毁带来的损耗 
  • 提高响应速度
    •  因为线程池中的线程数没有超过线程池的最大上限时,有的线程处于等待分配任务的状态,当任务来时无需创建新的线程就能执行。
  • 提高线程的可管理性
    •  线程池会根据当前系统特点对池内的线程进行优化处理,减少创建和销毁线程带来的系统开销。无限的创建和销毁线程不仅消耗系统资源,还降低系统的稳定性,使用线程池进行统一分配。

2、CompletableFuture组合式异步编程

业务场景:
查询商品详情页的逻辑比较复杂,有些数据还需要远程调用,必然需要花费更多的时间。

 

假如商品详情页的每个查询,需要如下标注的时间才能完成

那么,用户需要 5.5s 后才能看到商品详情页的内容。很显然是不能接受的。
如果有多个线程同时完成这 6 步操作,也许只需要 1.5s 即可完成响应。

2.1、创建异步对象

1)、runAsync 和 supplyAsync方法

CompletableFuture 提供了四个静态方法来创建一个异步操作。

public static CompletableFuture<Void> runAsync(Runnable runnable)
public static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor)
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)

没有指定Executor的方法会使用ForkJoinPool.commonPool() 作为它的线程池执行异步代码。如果指定线程池,则使用指定的线程池运行。以下所有的方法都类同。

  • runXxxx 都是没有返回结果的,supplyXxx 都是可以获取返回结果的
  • 可以传入自定义的线程池,否则就用默认的线程池
创建“com.atguigu.gulimall.search.thread.CompletableFutureTest” 类,代码如下:
package com.atguigu.gulimall.search.thread;

import java.util.concurrent.*;

/**
 * @Description: CompletableFutureTest
 * @Author: wts
 * @Date: 2024/5/13 13:50
 * @Version 1.0
 */
public class CompletableFutureTest {
    // 初始化一个线程池
    public static ThreadPoolExecutor executor = new ThreadPoolExecutor(
            5,
            200,
            10,
            TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(100000),
            Executors.defaultThreadFactory(),
            new ThreadPoolExecutor.AbortPolicy());

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        System.out.println("main.............start.......");
        CompletableFuture<Void> runAsyncFuture = CompletableFuture.runAsync(() -> {
            System.out.println("runAsync当前线程:" + Thread.currentThread().getId());
            int i = 10 / 2;
            System.out.println("runAsync当前运行结果:" + i);
        }, executor);

        CompletableFuture<Integer> supplyAsyncFuture = CompletableFuture.supplyAsync(() -> {
            System.out.println("supplyAsync当前线程:" + Thread.currentThread().getId());
            int i = 10 / 2;
            System.out.println("supplyAsync当前运行结果:" + i);
            return i;
        }, executor);
        Integer integer = supplyAsyncFuture.get();
        System.out.println("supplyAsync获取当前运行结果:" + integer);

        System.out.println("main.............end.......");
    }
}

结果

2.2、计算结果完成时的回调方法

当CompletableFuture的计算结果完成,或者抛出异常的时候,可以执行特定的Action。主要是下面的方法:

public CompletableFuture<T> whenComplete(BiConsumer<? super T,? super Throwable> action)
public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T,? super Throwable> action)
public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T,? super Throwable> action, Executor executor)
public CompletableFuture<T> exceptionally(Function<Throwable,? extends T> fn)
whenComplete 可以处理正常和异常的计算结果, exceptionally 处理异常情况。
whenComplete whenCompleteAsync 的区别:
  • whenComplete:是执行当前任务的线程执行继续执行 whenComplete 的任务。
  • whenCompleteAsync:是执行把 whenCompleteAsync 这个任务继续提交给线程池来进行执行。
方法不以 Async 结尾,意味着 Action 使用相同的线程执行,而 Async 可能会使用其他线程
执行(如果是使用相同的线程池,也可能会被同一个线程选中执行)

可以看到Action的类型是BiConsumer<? super T,? super Throwable>它可以处理正常的计算结果,或者异常情况。

package com.atguigu.gulimall.search.thread;

import java.util.concurrent.*;

public class CompletableFutureTest {
    // 方式二:初始化一个线程池
    public static ThreadPoolExecutor executor = new ThreadPoolExecutor(
            5,
            200,
            10,
            TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(100000),
            Executors.defaultThreadFactory(),
            new ThreadPoolExecutor.AbortPolicy());

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        System.out.println("main.............start.......");

        /**
         * 方法成功完成后的感知
         */
        CompletableFuture<Integer> supplyAsyncFuture = CompletableFuture.supplyAsync(() -> {
            System.out.println("supplyAsync当前线程:" + Thread.currentThread().getId());
            int i = 10 / 2;
            System.out.println("supplyAsync当前运行结果:" + i);
            return i;
        }, executor).whenComplete((result, exception) -> {
            // 虽然能得到异常信息,但是没法修改返回数据
            System.out.println("异步任务完成了...结果是" + result + ";异常是" + exception);
            // 可以感知异常,同时返回默认值
        }).exceptionally(throwable -> 10);
        Integer integer = supplyAsyncFuture.get();
        System.out.println("supplyAsync获取当前运行结果:" + integer);

        System.out.println("main.............end.......");
    }
}

结果

2.3、handle 方法

handle 是执行任务完成时对结果的处理。 handle 方法和 thenApply 方法处理方式基本一样。不同的是 handle 是在任务完成后再执行,还可以处理异常的任务。thenApply 只可以执行正常的任务,任务出现异常则不执行 thenApply 方法。

public <U> CompletionStage<U> handle(BiFunction<? super T, Throwable, ? extends U> fn);
public <U> CompletionStage<U> handleAsync(BiFunction<? super T, Throwable, ? extends U> fn);
public <U> CompletionStage<U> handleAsync(BiFunction<? super T, Throwable, ? extends U> fn,Execut

complete 一样,可对结果做最后的处理(可处理异常),可改变返回值。 

        // 方法执行完成后的处理
        CompletableFuture<Integer> handleFuture = CompletableFuture.supplyAsync(() -> {
            System.out.println("handleFuture当前线程:" + Thread.currentThread().getId());
            int i = 10 / 5;
            System.out.println("handleFuture当前运行结果:" + i);
            return i;
        }, executor).handle((result, exception) -> {
            if (result != null) {
                return result * 2;
            }
            if (exception != null) {
                return 0;
            }
            return 0;
        });

​结果

2.4、线程串行化

public <U > completableFuture < U > thenApply(Function < ? super T, ? extends U > fn);
public <U > completableFuture < U > thenApplyAsync(Function < ? super T, ? extends U > fn);
public <U > CompletableFuture < U > thenApplyAsync(Function < ? super T, ? extends U > fn, Executor executor);

public completionStage<Void> thenAccept (consumer < ? super T > action);
public Completionstage<Void> thenAcceptAsync (Consumer < ? super T > action);
public Completionstage<Void> thenAcceptAsync (consumer < ? super T > action, Executor executor);

public CompletionStage<Void> thenRun (Runnable action);
public Completionstage<Void> thenRunAsync (Runnable action);
public Completionstage<Void> thenRunAsync (Runnable action, Executor executor);
  • thenApply 方法:当一个线程依赖另一个线程时,获取上一个任务返回的结果,并返回当前任务的返回值。
  • thenAccept 方法:消费处理结果。接收任务的处理结果,并消费处理,无返回结果。
  • thenRun 方法:只要上面的任务执行完成,就开始执行 thenRun,只是处理完任务后,执行 thenRun 的后续操作,不能获取上一步的执行结果
带有 Async 默认是异步执行的。同之前。
以上都要前置任务成功完成。
Function<? super T,? extends U>
  • T:上一个任务返回结果的类型
  • U:当前任务的返回值类型
        /**
         * 4、线程串行化
         */
        // 4.1、thenApplyAsync能接收上一步的返回结果,也有返回值
        CompletableFuture<String> thenApplyAsync = CompletableFuture.supplyAsync(() -> {
            System.out.println("thenApplyAsync当前线程:" + Thread.currentThread().getId());
            int i = 10 / 5;
            System.out.println("thenApplyAsync当前运行结果:" + i);
            return i;
        }, executor).thenApplyAsync(res -> {
            System.out.println("thenApplyAsync任务2启动了。。。。" + res);
            return "hello" + res;
        }, executor);

        String thenApplyAsyncResult = thenApplyAsync.get();
        System.out.println("thenApplyAsync返回结果:" + thenApplyAsyncResult);

        // 4.2、thenAcceptAsync能接收上一步返回结果,但无返回值
        CompletableFuture<Void> thenAcceptAsync = CompletableFuture.supplyAsync(() -> {
            System.out.println("thenAcceptAsync当前线程:" + Thread.currentThread().getId());
            int i = 10 / 5;
            System.out.println("thenAcceptAsync当前运行结果:" + i);
            return i;
        }, executor).thenAcceptAsync(res -> {
            System.out.println("thenAcceptAsync任务2启动了。。。。" + res);
        }, executor);

        // 4.3、thenRun 不能获取得到上一步的执行结果
        CompletableFuture<Void> thenRunAsync = CompletableFuture.supplyAsync(() -> {
            System.out.println("thenRunAsync当前线程:" + Thread.currentThread().getId());
            int i = 10 / 5;
            System.out.println("thenRunAsync当前运行结果:" + i);
            return i;
        }, executor).thenRunAsync(() -> {
            System.out.println("thenRunAsync任务2启动了。。。。");
        }, executor);

 结果

2.5、两任务组合-都要完成

两个任务必须都完成,触发该任务。 

  • runAfterBoth

两个future不需要获取 future的结果,只需两个 future处理完任务后, 处理该任务。

public CompletableFuture<Void> runAfterBoth(CompletionStage<?> other,Runnable action);
public CompletableFuture<Void> runAfterBothAsync(CompletionStage<?> other,Runnable action);
public CompletableFuture<Void> runAfterBothAsync(CompletionStage<?> other,Runnable action,Executor 
  • thenAcceptBoth
组合两个 future,获取两个 future 任务的返回结果,然后处理任务,没有返回值。
public <U> CompletableFuture<Void> thenAcceptBoth(CompletionStage<? extends U> other,BiConsumer<? super T, ? super U> action);
public <U> CompletableFuture<Void> thenAcceptBothAsync(CompletionStage<? extends U> other,BiConsumer<? super T, ? super U> action);
public <U> CompletableFuture<Void> thenAcceptBothAsync(CompletionStage<? extends U> other,BiConsumer<? super T, ? super U> action,     Executor executor);
  • thenCombine

组合两个 future,获取两个 future 的返回结果,并返回当前任务的返回值

public <U,V> CompletableFuture<V> thenCombine(CompletionStage<? extends U> other,BiFunction<? super T,? super U,? extends V> fn);
public <U,V> CompletableFuture<V> thenCombineAsync(CompletionStage<? extends U> other,BiFunction<? super T,? super U,? extends V> fn);
public <U,V> CompletableFuture<V> thenCombineAsync(CompletionStage<? extends U> other,BiFunction<? super T,? super U,? extends V> fn,Executor executor);
        /**
         * 5、两任务组合-都要完成
         */
        CompletableFuture<Object> future01 = CompletableFuture.supplyAsync(() -> {
            System.out.println("任务1线程:" + Thread.currentThread().getId());
            int i = 10 / 5;
            System.out.println("任务1线程结束");
            return i;
        }, executor);
        CompletableFuture<Object> future02 = CompletableFuture.supplyAsync(() -> {
            System.out.println("任务2线程:" + Thread.currentThread().getId());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("任务2线程结束");
            return "hello";
        }, executor);

        // 5.1、不能得到两个任务的参数,也无返回结果
        future01.runAfterBothAsync(future02, () -> {
            System.out.println("runAfterBothAsync任务三开始。。。");
        }, executor);

        // 5.2、能得到两个任务的参数,无返回结果
        future01.thenAcceptBothAsync(future02, (f1, f2) -> {
            System.out.println("thenAcceptBothAsync任务三开始。。。之前的结果" + f1 + ":" + f2);
        }, executor);

        // 5.3、能得到两个任务的参数,并返回结果
        CompletableFuture<String> thenCombineAsync = future01.thenCombineAsync(future02, (f1, f2) -> {
            System.out.println("thenCombineAsync任务三开始。。。之前的结果" + f1 + ":" + f2);
            return f1 + ":" + f2 + "->haha";
        }, executor);
        System.out.println("thenCombineAsync返回结果:" + thenCombineAsync.get());

结果

2.6、两任务组合-只要有一个任务完成就执行第三个

当两个任务中,任意一个 future 任务完成的时候,执行任务。 

  • runAfterEither 方法
两个任务有一个执行完成,不需要获取 future 的结果,处理任务,也没有返回值。
public CompletableFuture<Void> runAfterEither(CompletionStage<?> other,Runnable action);
public CompletableFuture<Void> runAfterEitherAsync(CompletionStage<?> other,Runnable action);
public CompletableFuture<Void> runAfterEitherAsync(CompletionStage<?> other,Runnable action,Executor executor);
  • acceptEither 方法

两个任务有一个执行完成,获取它的返回值,处理任务,没有新的返回值。

public CompletableFuture<Void> acceptEither(CompletionStage<? extends T> other,Consumer<? super T> action);
public CompletableFuture<Void> acceptEitherAsync(CompletionStage<? extends T> other,Consumer<? super T> action);
public CompletableFuture<Void> acceptEitherAsync(CompletionStage<? extends T> other,Consumer<? supe
  • applyToEither 方法

两个任务有一个执行完成,获取它的返回值,处理任务并有新的返回值。

public <U> CompletableFuture<U> applyToEither(CompletionStage<? extends T> other,Function<? super T, U> fn);
public <U> CompletableFuture<U> applyToEitherAsync(CompletionStage<? extends T> other,Function<? super T, U> fn);
public <U> CompletableFuture<U> applyToEitherAsync(CompletionStage<? extends T> other,Function<? sup
        /**
         * 6、两个任务只要有一个完成,我们就执行任务3
         */
        CompletableFuture<Object> future001 = CompletableFuture.supplyAsync(() -> {
            System.out.println("future001任务1线程:" + Thread.currentThread().getId());
            int i = 10 / 5;
            System.out.println("future001任务1线程结束");
            return i;
        }, executor);
        CompletableFuture<Object> future002 = CompletableFuture.supplyAsync(() -> {
            System.out.println("任务2线程:" + Thread.currentThread().getId());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("future002任务2线程结束");
            return "hello";
        }, executor);

        // 6.1、runAfterEitherAsync  不感知结果,自己也无返回值
        future001.runAfterEitherAsync(future002, () -> {
            System.out.println("runAfterEitherAsync任务三开始。。。");
        }, executor);

        // 6.2、acceptEitherAsync   感知结果,自己没有返回值
        future001.acceptEitherAsync(future002, (res) -> {
            System.out.println("acceptEitherAsync任务三开始。。。" + res);
        }, executor);

        // 6.3、acceptEitherAsync   感知结果,自己没有返回值
        CompletableFuture<String> applyToEitherAsync = future001.applyToEitherAsync(future002, (res) -> {
            System.out.println("applyToEitherAsync任务三开始。。。" + res);
            return res.toString() + "-> haha";
        }, executor);
        System.out.println("applyToEitherAsync返回结果:" + applyToEitherAsync.get());

结果

  • thenCompose 方法

thenCompose 方法允许你对两个 CompletionStage 进行流水线操作,第一个操作完成时,将其结果作为参数传递给第二个操作。

public <U> CompletableFuture<U> thenCompose(Function<? super T, ? extends CompletionStage<U>> fn);
public <U> CompletableFuture<U> thenComposeAsync(Function<? super T, ? extends CompletionStage<U>> fn) ;
public <U> CompletableFuture<U> thenComposeAsync(Function<? super T, ? extends CompletionStage

2.7、多任务组合

public static completableFuture<Void> all0f(completableFuture<?>... cfs);
public static completableFuture<Obiect> anyof(completableFuture<?>... cfs);
  1. allOf:等待所有任务完成
  2. anyOf:只要有一个任务完成
  • allOf
        /**
         * 7、多任务组合
         */
        CompletableFuture<String> futureAttr = CompletableFuture.supplyAsync(() -> {
            System.out.println("查询商品的属性");
            return "黑色+256g";
        }, executor);

        CompletableFuture<String> futureImg = CompletableFuture.supplyAsync(() -> {
            try {
                Thread.sleep(3000);
                System.out.println("查询商品的图片信息");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "hello.jpg";
        }, executor);

        CompletableFuture<String> futureDesc = CompletableFuture.supplyAsync(() -> {
            System.out.println("查询商品的介绍");
            return "华为";
        }, executor);
        CompletableFuture<Void> allOf = CompletableFuture.allOf(futureAttr, futureImg, futureDesc);
        allOf.get(); //等待所有线程执行完
        System.out.println("allOf获取结果:" +  futureAttr.get() + "=>" + futureImg.get() + "=>" + futureDesc.get());

结果

  • anyOf
        /**
         * 7、多任务组合
         */
        CompletableFuture<String> futureAttr = CompletableFuture.supplyAsync(() -> {
            System.out.println("查询商品的属性");
            return "黑色+256g";
        }, executor);

        CompletableFuture<String> futureImg = CompletableFuture.supplyAsync(() -> {
            try {
                Thread.sleep(3000);
                System.out.println("查询商品的图片信息");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "hello.jpg";
        }, executor);

        CompletableFuture<String> futureDesc = CompletableFuture.supplyAsync(() -> {
            System.out.println("查询商品的介绍");
            return "华为";
        }, executor);
//        CompletableFuture<Void> allOf = CompletableFuture.allOf(futureAttr, futureImg, futureDesc);
//        allOf.get(); //等待所有线程执行完
//        System.out.println("allOf获取结果:" +  futureAttr.get() + "=>" + futureImg.get() + "=>" + futureDesc.get());
        CompletableFuture<Object> anyOf = CompletableFuture.anyOf(futureAttr, futureImg, futureDesc);
        anyOf.get();
        System.out.println("anyOf获取结果:" +  futureAttr.get() + "=>" + futureImg.get() + "=>" + futureDesc.get());

 七、商城业务-商品详情


1、搭建好域名跳转环境

1、用记事本打开C:\Windows\System32\drivers\etc\host(记得先把属性的只读模式去掉)

#----------gulimall----------
192.168.119.127 gulimall.com
192.168.119.127 search.gulimall.com
192.168.119.127 item.gulimall.com

​2、配置Nginx

之前配置了*.gulimall.com可以匹配上item.gulimall.com

重启nginx

docker restart nginx

3、改网关

4、把详情页的html导入gulimall-product项目里

5、给虚拟机的nginx传入详情页的静态资源(static里再创建一个文件夹item)

 6、把item.html的路径改了(ctrl+r)

添加“com.atguigu.gulimall.product.web.ItemController”类,代码如下:

package com.atguigu.gulimall.product.web;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

/**
 * @Description: ItemController
 * @Author: WangTianShun
 * @Date: 2020/11/16 15:31
 * @Version 1.0
 */
@Controller
public class ItemController {

    /**
     * 展示当前sku的详情
     * @param skuId
     * @return
     */
    @GetMapping("/{skuId}.html")
    public String skuItem(@PathVariable("skuId") Long skuId){
        System.out.println("准备查询"+skuId+"的详情");
        return "item";
    }
}

修改gulimall-search的list.html

<p class="da">
    <a th:href="|http://item.gulimall.com/${product.skuId}.html|">
        <img th:src="${product.skuImg}" class="dim">
    </a>
</p>

测试效果

2、模型抽取

模仿京东商品详情页,如下图所示,包括sku基本信息,图片信息,销售属性,图片介绍和规格参数

因此建立以下vo

添加“com.atguigu.gulimall.product.vo.SkuItemVo”类,代码如下:

package com.atguigu.gulimall.product.vo;

import com.atguigu.gulimall.product.entity.SkuImagesEntity;
import com.atguigu.gulimall.product.entity.SkuInfoEntity;
import com.atguigu.gulimall.product.entity.SpuInfoDescEntity;
import lombok.Data;

import java.util.List;

@Data
public class SkuItemVo {

    // 1、sku基本信息获取    pms_sku_info
    SkuInfoEntity info;

    // 2、sku的图片信息      pms_sku_images
    List<SkuImagesEntity> images;

    // 、获取spu的销售属性组合
    List<SkuItemSaleAttrVo> saleAttr;

    // 4、获取spu的介绍
    SpuInfoDescEntity desc;

    // 5、获取spu的规格参数信息
    List<SpuItemAttrGroupVo> groupAttrs;
}

 添加“com.atguigu.gulimall.product.vo.SkuItemSaleAttrVo”类,代码如下:

package com.atguigu.gulimall.product.vo;

import lombok.Data;

@Data
public class SkuItemSaleAttrVo {

    private Long attrId;

    private String attrName;

    private String attrValues;
}


 添加“com.atguigu.gulimall.product.vo.SpuItemAttrGroupVo”类,代码如下:

package com.atguigu.gulimall.product.vo;

import lombok.Data;
import lombok.ToString;

import java.util.List;

@ToString
@Data
public class SpuItemAttrGroupVo {

    private String groupName;

    private List<Attr> attrs;
}


3、 封装商品属性

1、总体思路

修改“com.atguigu.gulimall.product.web.ItemController”类,代码如下:

@Controller
public class ItemController {

    @Autowired
    SkuInfoService skuInfoService;

    /**
     * 展示当前sku的详情
     *
     * @param skuId
     * @return
     */
    @GetMapping("/{skuId}.html")
    public String skuItem(@PathVariable("skuId") Long skuId, Model model) {
        SkuItemVo skuItemVo = skuInfoService.item(skuId);
        model.addAttribute("item", skuItemVo);
        return "item";
    }

 修改“com.atguigu.gulimall.product.service.SkuInfoService”类,代码如下:

SkuItemVo item(Long skuId);

 修改“com.atguigu.gulimall.product.service.impl.SkuInfoServiceImpl”类,代码如下:

    @Override
    public SkuItemVo item(Long skuId) {
        SkuItemVo skuItemVo = new SkuItemVo();
        // 1、sku基本信息获取    pms_sku_info
        SkuInfoEntity info = getById(skuId);
        skuItemVo.setInfo(info);
        Long catalogId = info.getCatalogId();
        Long spuId = info.getSpuId();

        // 2、sku的图片信息      pms_sku_images
        List<SkuImagesEntity> images = skuImagesService.getImagesBySkuId(skuId);
        skuItemVo.setImages(images);

        // 3、获取spu的销售属性组合
        List<SkuItemSaleAttrVo> saleAttrVos = skuSaleAttrValueService.getSaleAttrsBySpuId(spuId);
        skuItemVo.setSaleAttr(saleAttrVos);

        // 4、获取spu的介绍 pms_spu_info_desc
        SpuInfoDescEntity desc = spuInfoDescService.getById(spuId);
        skuItemVo.setDesc(desc);

        // 5、获取spu的规格参数信息
        List<SpuItemAttrGroupVo> attrGroupVos = attrGroupService.getAttrGroupWithAttrsBySpuId(spuId,catalogId);
        skuItemVo.setGroupAttrs(attrGroupVos);
        return skuItemVo;
    }

2、获取spu的图片信息

 修改“com.atguigu.gulimall.product.service.SkuImagesService”类,代码如下:

List<SkuImagesEntity> getImagesBySkuId(Long skuId);

 修改“com.atguigu.gulimall.product.service.impl.SkuImagesServiceImpl”类,代码如下:

 @Override
    public List<SkuImagesEntity> getImagesBySkuId(Long skuId) {

        List<SkuImagesEntity> imagesEntities = this.baseMapper.selectList(new QueryWrapper<SkuImagesEntity>().eq("sku_id", skuId));
        return imagesEntities;
    }

3、获取spu的销售属性

由于我们需要获取该spu下所有sku的销售属性,因此我们需要先从pms_sku_info查出该spuId对应的skuIdpms_sku_sale_attr_value表中查出上述skuId对应的属性

​ ​

因此我们需要使用连表查询,并且通过分组将单个属性值对应的多个spuId组成集合,效果如下

 修改“com.atguigu.gulimall.product.service.SkuSaleAttrValueService”类,代码如下:

List<SkuItemSaleAttrVo> getSaleAttrsBySpuId(Long spuId);

  修改“com.atguigu.gulimall.product.service.impl.SkuSaleAttrValueServiceImpl”类,代码如下:

    @Override
    public List<SkuItemSaleAttrVo> getSaleAttrsBySpuId(Long spuId) {

        SkuSaleAttrValueDao dao = this.baseMapper;
        List<SkuItemSaleAttrVo> saleAttrVos = dao.getSaleAttrsBySpuId(spuId);
        return saleAttrVos;
    }

  修改“com.atguigu.gulimall.product.dao.SkuSaleAttrValueDao”类,代码如下:

List<SkuItemSaleAttrVo> getSaleAttrsBySpuId(@Param("spuId") Long spuId);

修改SkuSaleAttrValueDao.xml

  <select id="getSaleAttrsBySpuId" resultType="com.atguigu.gulimall.product.vo.SkuItemSaleAttrVo">
        SELECT ssav.attr_id attr_id, ssav.attr_name attr_name, GROUP_CONCAT(DISTINCT ssav.attr_value) attr_values FROM pms_sku_info info
        LEFT JOIN pms_sku_sale_attr_value ssav ON ssav.sku_id = info.sku_id
        WHERE info.spu_id=#{spuId}
        GROUP BY ssav.attr_id,ssav.attr_name
    </select>

4、获取spu的规格参数信息

由于需要通过spuIdcatalogId查询对应规格参数,所以我们需要通过pms_attr_group表获得catalogIdattrGroupName

​ ​

然后通过pms_attr_attrgroup_relation获取分组对应attr_id

再到pms_product_attr_value查询spuId对应的属性

​ ​

最终sql效果,联表含有需要的所有属性

修改“com.atguigu.gulimall.product.service.AttrGroupService”类,代码如下:

List<SpuItemAttrGroupVo> getAttrGroupWithAttrsBySpuId(Long spuId, Long catalogId);

修改“com.atguigu.gulimall.product.service.AttrGroupService”类,代码如下:

    @Override
    public List<SpuItemAttrGroupVo> getAttrGroupWithAttrsBySpuId(Long spuId, Long catalogId) {
        //1、查出当前spu对应的所有属性的分组信息以及当前分组下的所有属性对应的值
        //1.1、
        AttrGroupDao baseMapper = this.getBaseMapper();
        List<SpuItemAttrGroupVo> vos = baseMapper.getAttrGroupWithAttrsBySpuId(spuId,catalogId);
        return vos;
    }

修改“com.atguigu.gulimall.product.service.AttrGroupService”类,代码如下:

@Mapper
public interface AttrGroupDao extends BaseMapper<AttrGroupEntity> {

     List<SpuItemAttrGroupVo> getAttrGroupWithAttrsBySpuId(@Param("spuId") Long spuId, @Param("catalogId") Long catalogId);
}

修改AttrGroupDao.xml

    <!--resultType 返回集合里面元素的类型,只要有嵌套属性就要封装自定义结果集-->
    <resultMap id="spuItemAttrGroupVo" type="com.atguigu.gulimall.product.vo.SpuItemAttrGroupVo">
        <result property="groupName" column="attr_group_name"/>
        <collection property="attrs" ofType="com.atguigu.gulimall.product.vo.Attr">
            <result property="attrName" column="attr_name"/>
            <result property="attrValue" column="attr_value"/>
        </collection>
    </resultMap>
    <select id="getAttrGroupWithAttrsBySpuId" resultMap="spuItemAttrGroupVo">
        SELECT pav.spu_id, ag.attr_group_name, ag.attr_group_id, aar.attr_id, attr.attr_name, pav.attr_value
        FROM pms_attr_group ag
                 LEFT JOIN pms_attr_attrgroup_relation aar ON aar.attr_group_id = ag.attr_group_id
                 LEFT JOIN pms_attr attr ON attr.attr_id = aar.attr_id
                 LEFT JOIN pms_product_attr_value pav ON pav.attr_id = attr.attr_id
        WHERE  ag.catelog_id = #{catalogId} AND pav.spu_id = #{spuId}
    </select>

3、页面渲染

1、添加thymeleaf的名称空间

<html lang="en" xmlns:th="http://www.thymeleaf.org">

2、渲染标题和子标题

						<!-- 标题 -->
						<div class="box-name" th:text="${item.info.skuTitle}">
							华为 HUAWEI Mate 10 6GB+128GB 亮黑色 移动联通电信4G手机 双卡双待
						</div>
						<!-- 子标题 -->
						<div class="box-hide" th:text="${item.info.skuSubtitle}">预订用户预计11月30日左右陆续发货!麒麟970芯片!AI智能拍照!
							<a href="/static/item/"><u></u></a>
						</div>

2、渲染商品图片

                <div class="imgbox">
                    <div class="imgbox">
                        <!--商品默认图片-大图片-->
                        <div class="probox">
                            <img class="img1" alt="" th:src="${item.info.skuDefaultImg}">
                            <div class="hoverbox"></div>
                        </div>
                        <!--商品默认图片-小图片-->
                        <div class="showbox">
                            <img class="img1" alt="" th:src="${item.info.skuDefaultImg}">
                        </div>
                    </div>
                </div>

                <div class="box-lh">
                    <div class="box-lh-one">
                        <ul>
                            <li th:each="skuImage:${item.images}"><img th:src="${skuImage.imgUrl}"/></li>
                        </ul>
                    </div>
                    <div id="left">
                        <
                    </div>
                    <div id="right">
                        >
                    </div>
                </div>

3、渲染价格

                <div class="box-summary clear">
                    <ul>
                        <li>京东价</li>
                        <li>
                            <span>¥</span>
                            <span th:text="${#numbers.formatDecimal(item.info.price,3,2)}">4499.00</span>
                        </li>
                        <li>
                            预约享资格
                        </li>
                        <li>
                            <a href="/static/item/">
                                预约说明
                            </a>
                        </li>
                    </ul>
                </div>       

4、渲染是否有货

                        <li>
                            <span th:text="${item.hasStock ? '有货' : '无货'}">无货</span>, 此商品暂时售完
                        </li>

5、渲染销售属性

							<div class="box-attr clear" th:each="attr:${item.saleAttr}">
								<dl>
									<dt>选择[[${attr.attrName}]]</dt>
									<dd th:each="val:${#strings.listSplit(attr.attrValues,',')}">
										<a href="/static/item/#">
											[[${val}]]
<!--											<img src="/static/item/img/59ddfcb1Nc3edb8f1.jpg" /> -->
										</a>
									</dd>
								</dl>
							</div>

5、渲染商品介绍

<img class="xiaoguo" th:src="${descp}" th:each="descp:${#strings.listSplit(item.desc.decript,',')}"/>

6、去掉标签超链接,不然点击报错

             <ul class="shopjieshao">
                <li class="jieshoa" style="background: #e4393c;">
                    <a style="color: white;">商品介绍</a>
                </li>
                <li class="baozhuang">
                    <a>规格与包装</a>
                </li>
                <li class="baozhang">
                    <a>售后保障</a>
                </li>
                <li class="pingjia">
                    <a>商品评价(4万+)</a>
                </li>
                <li class="shuoming">
                    <a>预约说明</a>
                </li>
            </ul>

7、渲染商品规格

                        <div class="guiGebox">
                            <div class="guiGe" th:each="group:${item.groupAttrs}">
                                <h3 th:text="${group.groupName}">主体</h3>
                                <dl >
                                    <div th:each="attr:${group.attrs}">
                                        <dt th:text="${attr.attrName}">品牌</dt>
                                        <dd th:text="${attr.attrValue}">华为(HUAWEI)</dd>
                                    </div>
                                </dl>
                            </div>
                            <div class="package-list">
                                <h3>包装清单</h3>
                                <p>手机(含内置电池) X 1、5A大电流华为SuperCharge充电器X 1、5A USB数据线 X 1、半入耳式线控耳机
                                    X 1、快速指南X 1、三包凭证 X 1、取卡针 X 1、保护壳 X 1</p>
                            </div>
                        </div>

修改:获取spu的销售属性的逻辑

由于我们需要获取该spu下所有sku的销售属性,因此我们需要先从pms_sku_info查出该spuId对应的skuId

pms_sku_sale_attr_value表中查出上述skuId对应的属性

因此我们需要使用连表查询,并且通过分组将单个属性值对应的多个spuId组成集合,效果如下

为什么要设计成这种模式呢?

因为这样可以在页面显示切换属性时,快速得到对应skuId的值,比如白色对应的sku_ids为12,13,8+128GB对应的sku_ids为10,12,那么销售属性为白色、8+128GB的商品的skuId则为二者的交集12

添加“com.atguigu.gulimall.product.vo.AttrValueWithSkuIdVo”类,代码如下:

@Data
public class AttrValueWithSkuIdVo {
    private String attrValue;

    private String skuIds;
}

修改“com.atguigu.gulimall.product.vo.SkuItemSaleAttrVo”类,代码如下:

@Data
public class SkuItemSaleAttrVo {

    private Long attrId;

    private String attrName;

    private List<AttrValueWithSkuIdVo> attrValues;
}

 修改SkuSaleAttrValueDao.xml

<resultMap id="skuItemSaleAttrVo" type="com.atguigu.gulimall.product.vo.SkuItemSaleAttrVo">
        <result property="attrId" column="attr_id"/>
        <result property="attrName" column="attr_name"/>
        <collection property="attrValues" ofType="com.atguigu.gulimall.product.vo.AttrValueWithSkuIdVo">
            <result property="attrValue" column="attr_value"/>
            <result property="skuIds" column="sku_ids"/>
        </collection>
    </resultMap>

    <select id="getSaleAttrsBySpuId" resultMap="skuItemSaleAttrVo">
        SELECT  ssav.attr_id attr_id, ssav.attr_name attr_name, ssav.attr_value attr_value, GROUP_CONCAT(DISTINCT info.sku_id) sku_ids FROM pms_sku_info info
        LEFT JOIN pms_sku_sale_attr_value ssav ON ssav.sku_id = info.sku_id
        WHERE info.spu_id = #{spuId}
        GROUP BY ssav.attr_id,ssav.attr_name, ssav.attr_value
    </select>

修改item.html页面

                <div class="box-attr-3">
                    <div class="box-attr clear" th:each="attr:${item.saleAttr}">
                        <dl>
                            <dt>选择[[${attr.attrName}]]</dt>
                            <dd th:each="vals:${attr.attrValues}">
                                <a>
                                    [[${vals.attrValue}]]
                                </a>
                            </dd>
                        </dl>
                    </div>
                </div>

​4、页面的sku切换

<script>
    $(".sku_attr_value").click(function () {
        // 1、点击的元素添加上自定义的属性,为了识别我们是刚才被点击的、
        var skus = new Array();
        $(this).addClass("clicked");
        var curr = $(this).attr("skus").split(",");
        // 当前被点击的所有sku组合数组放进去
        skus.push(curr);
        // 去掉同一行的所有的checked
        $(this).parent().parent().find(".sku_attr_value").removeClass("checked");
        $("a[class='sku_attr_value checked']").each(function () {
            skus.push($(this).attr("skus").split(","));
        });
        console.log(skus);
        // 2、取出他们的交集,得到skuId
        // console.log($(skus[0]).filter(skus[1])[0]);
        var filterEle = skus[0];
        for (var i = 1; i < skus.length; i++) {
            filterEle = $(filterEle).filter(skus[i])
        }
        console.log(filterEle[0]);
        // 3、跳转
        location.href = "http://item.gulimall.com/" + filterEle[0] + ".html";
    });
    $(function () {
        $(".sku_attr_value").parent().css({"border": "solid 1px #ccc"});
        $("a[class='sku_attr_value checked']").parent().css({"border": "solid 1px red"});
        //方法二
        // $(".sku_attr_value").parent().css({"border":"solid 1px #ccc"});
        // $(".sku_attr_value.checked").parent().css({"border":"solid 1px red"});
    })
</script>

通过控制class中是否包换checked属性来控制显示样式,因此要根据skuId判断

						<div class="box-attr-3">
							<div class="box-attr clear" th:each="attr:${item.saleAttr}">
								<dl>
									<dt>选择[[${attr.attrName}]]</dt>
									<dd th:each="vals:${attr.attrValues}">
										<a
										   th:attr="skus=${vals.skuIds},class=${#lists.contains(#strings.listSplit(vals.skuIds,','),item.info.skuId.toString())?'sku_attr_value checked' : 'sku_attr_value'}">
											[[${vals.attrValue}]]
										</a>
									</dd>
								</dl>
							</div>

5、使用异步编排

添加线程池属性配置类,并注入到容器中

package com.atguigu.gulimall.product.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@ConfigurationProperties(prefix = "gulimall.thread")
@Component
@Data
public class ThreadPoolConfigProperties {
    private Integer core;
    private Integer maxSize;
    private Integer keepAliveTime;
}

添加application.yml

#线程池属性的配置
gulimall:
  thread:
    core: 20
    max-size: 200
    keep-alive-time: 10

线程池配置,获取线程池的属性值这里直接调用与配置文件相对应的属性配置类

package com.atguigu.gulimall.product.config;

import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

//如果ThreadPoolConfigProperties.class类没有加上@Component注解,那么我们在需要的配置类里开启属性配置的类加到容器中
//@EnableConfigurationProperties(ThreadPoolConfigProperties.class)
@Configuration
public class MyThreadConfig {
    @Bean
    public ThreadPoolExecutor threadPoolExecutor(ThreadPoolConfigProperties pool){
        return new ThreadPoolExecutor(pool.getCore(),
                pool.getMaxSize(),
                pool.getKeepAliveTime(),
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(100000),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());
    }
}

 为了使我们的任务进行的更快,我们可以让查询的各个子任务多线程执行,但是由于各个任务之间可能有相互依赖的关系,因此就涉及到了异步编排。

在这次查询中spu的销售属性、介绍、规格参数信息都需要spuId,因此依赖sku基本信息的获取,所以我们要让这些任务在1之后运行。因为我们需要1运行的结果,因此调用thenAcceptAsync()可以接受上一步的结果且没有返回值。

最后时,我们需要调用get()方法使得所有方法都已经执行完成

修改“com.atguigu.gulimall.product.service.impl.SkuInfoServiceImpl”类,代码如下:

 @Autowired
 private ThreadPoolExecutor executor;
    @Override
    public SkuItemVo item(Long skuId) {
        SkuItemVo skuItemVo = new SkuItemVo();
        // 使用异步编排
        CompletableFuture<SkuInfoEntity> infoFuture = CompletableFuture.supplyAsync(() -> {
            // 1、sku基本信息获取    pms_sku_info
            SkuInfoEntity info = getById(skuId);
            skuItemVo.setInfo(info);
            return info;
        }, executor);

        CompletableFuture<Void> imageFuture = CompletableFuture.runAsync(() -> {
            // 2、sku的图片信息      pms_sku_images
            List<SkuImagesEntity> images = skuImagesService.getImagesBySkuId(skuId);
            skuItemVo.setImages(images);
        }, executor);

        CompletableFuture<Void> saleAttrFuture = infoFuture.thenAcceptAsync((res) -> {
            // 3、获取spu的销售属性组合
            List<SkuItemSaleAttrVo> saleAttrVos = skuSaleAttrValueService.getSaleAttrsBySpuId(res.getSpuId());
            skuItemVo.setSaleAttr(saleAttrVos);
        }, executor);

        CompletableFuture<Void> descFuture = infoFuture.thenAcceptAsync(res -> {
            // 4、获取spu的介绍 pms_spu_info_desc
            SpuInfoDescEntity desc = spuInfoDescService.getById(res.getSpuId());
            skuItemVo.setDesc(desc);
        }, executor);

        CompletableFuture<Void> baseAttrFuture = infoFuture.thenAcceptAsync((res) -> {
            // 5、获取spu的规格参数信息
            List<SpuItemAttrGroupVo> attrGroupVos = attrGroupService.getAttrGroupWithAttrsBySpuId(res.getSpuId(), res.getCatalogId());
            skuItemVo.setGroupAttrs(attrGroupVos);
        }, executor);
        
        // 等待所有任务执行完成
        try {
            CompletableFuture.allOf(saleAttrFuture, imageFuture, descFuture, baseAttrFuture).get();
        } catch (InterruptedException e) {
            log.error("1等待所有任务执行完成异常{}", e);
        } catch (ExecutionException e) {
            log.error("2等待所有任务执行完成异常{}", e);
        }
        return skuItemVo;
    }

八、商城业务-认证服务


1、环境搭建

1、创建gulimall-auth-server模块

2、导入依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.4.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.atguigu.gulimall</groupId>
    <artifactId>gulimall-auth-server</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>gulimall-auth-server</name>
    <description>认证中心(社交登录、OAuth2.0、单点登录)</description>

    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Hoxton.SR8</spring-cloud.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.auguigu.gulimall</groupId>
            <artifactId>gulimall-commom</artifactId>
            <version>0.0.1-SNAPSHOT</version>
            <exclusions>
                <exclusion>
                    <groupId>com.baomidou</groupId>
                    <artifactId>mybatis-plus-boot-starter</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

    <repositories>
        <repository>
            <id>spring-snapshots</id>
            <name>Spring Snapshots</name>
            <url>https://repo.spring.io/snapshot</url>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
        </repository>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
        </repository>
    </repositories>

</project>

3、添加application.properties配置

server.port=20000
spring.application.name=gulimall-auth-server
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848

 4、修改主启动类

package com.atguigu.gulimall.auth;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class GulimallAuthServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(GulimallAuthServerApplication.class, args);
    }

}

5、修改C:\Windows\System32\drivers\etc\hosts的域名

#----------gulimall----------
192.168.119.127 gulimall.com
192.168.119.127 search.gulimall.com
192.168.119.127 item.gulimall.com
192.168.119.127 auth.gulimall.com

6、引入login.htmlreg.html 

7、在虚拟机的/mydata/nginx/html/static/下创建reg,并把静态资源放入reg文件夹下

 在虚拟机的/mydata/nginx/html/static/下创建login,并把静态资源放入login文件夹下

 

8、重启nginx

docker restart nginx 

9、修改login.html和reg.html的引用路径(Ctrl+R)

login.html

 reg.html

10、在网关模块配置认证的路由

        - id: gulimall_auth_route
          uri: lb://gulimall-auth-server
          predicates:
            - Host=auth.gulimall.com

11、测试

先把login.html名字改为index.html,接着启动认证中心和网关服务

http://auth.gulimall.com/

12、修改点击登录页的谷粒商城logo可以跳到首页

1)、先关闭缓存

#关闭thymeleaf的缓存
spring.thymeleaf.cache=false

2)、修改login.html链接地址

		<!--顶部logo-->
		<header>
			<a href="http://gulimall.com"><img src="/static/login/JD_img/logo.jpg" /></a>
			<p>欢迎登录</p>
			<div class="top-1">
				<img src="/static/login/JD_img/4de5019d2404d347897dee637895d02b_06.png" /><span>登录页面,调查问卷</span>
			</div>
		</header>

3)、修改首页index.html登录注册的链接地址

        <ul>
          <li>
            <a href="http://auth.gulimall.com/login.html">你好,请登录</a>
          </li>
          <li>
            <a href="http://auth.gulimall.com/reg.html">免费注册</a>
          </li>
          <span>|</span>
          <li>
            <a href="/static/#">我的订单</a>
          </li>
        </ul>

再把认证中心的index.html改成login.html

添加“com.atguigu.gulimall.auth.controller.LoginController”类,代码如下:

@Controller
public class LoginController {

    @GetMapping("/login.html")
    public String loginPage(){

        return "login";
    }

    @GetMapping("/reg.html")
    public String regPage(){

        return "reg";
    }
}

 点击测试

修改登录页面login.html的立即注册链接地址

					<h5 class="rig">
						<img src="/static/login/JD_img/4de5019d2404d347897dee637895d02b_25.png" />
						<span><a href="http://auth.gulimall.com/reg.html">立即注册</a></span>
					</h5>

修改注册页面reg.html的跳转首页和请登录链接地址

		<header>
			<a href="http://gulimall.com" class="logo"><img src="/static/reg/img/logo1.jpg" alt=""></a>
			<div class="desc">欢迎注册</div>
			<div class="dfg">
				<span>已有账号?</span>
				<a href="http://auth.gulimall.com/login.html">请登录</a>
			</div>
		</header>

2、注册功能

2.1、验证码倒计时

 ​

修改注册页面reg.html的发送验证码倒计时功能 

        <div class="register-box">
            <label for="code" class="other_label">验 证 码
                <input maxlength="20" type="text" placeholder="请输入验证码" class="caa">
            </label>
            <a id="sendCode">发送验证码</a>
            <div class="tips">
            </div>
        </div>
    /**
     * 重发验证码倒计时事件
     */
    $(function () {
        // 点击发送验证码按钮触发下面函数
        $('#sendCode').click(function () {
            // 1、倒计时 如果有disabled,说明最近已经点过,则什么都不做
            if ($(this).hasClass("disabled")) {
                //正在倒计时中
            } else {
                setTimeout("timeoutChangeStyle()", 1000);
            }
        });
    })
    var num = 60;

    function timeoutChangeStyle() {
        // 开启倒计时后设置标志属性disable,使得该按钮不能再次被点击
        $('#sendCode').attr("class", "disabled");
        // 当时间为0时,说明倒计时完成,则重置
        if (num == 0) {
            $('#sendCode').text("发送验证码");
            num = 60;
            $('#sendCode').attr("class", "");
        } else {
            // 每秒调用一次当前函数,使得num--
            var str = num + "s 后再次发送";
            $('#sendCode').text(str);
            setTimeout("timeoutChangeStyle()", 1000);
        }
        num--;
    }
发送一个请求直接跳转到一个页面并且不传值,我们原先是在controller里创建一个跳转页面的空方法。现在我们使用SpringMVC viewController: 将请求html页面映射过来;不需要写空方法添加“com.atguigu.gulimall.auth.config.GulimallWebConfig”类,代码如下:
@Configuration
public class GulimallConfig implements WebMvcConfigurer {

    /**
     * 视图映射
     * @param registry
     */
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        /**
         *     @GetMapping("/login.html")
         *     public String loginPage(){
         *         return "login";
         *     }
         */
        registry.addViewController("/login.html").setViewName("login");
        registry.addViewController("/reg.html").setViewName("reg");
    }
}

把LoginController的页面跳转方法注释掉,重启测试

2.2、整合短信服务

在阿里云网页购买试用的短信服务

https://market.aliyun.com/products/57126001/cmapi00041050.html?spm=5176.2020520132.101.3.bb587218FvQSEj#sku=yuncode35050000010 按照代码案例进行开发测试

 https://market.console.aliyun.com/?spm=5176.28663977.aliyun_market_home_panel.1.171a4d50AEjjKi#/bizlist?_k=ujqo7u

访问该地址可以进appcode

1、根据代码案例在代码里进行测试 

    @Test
    public void sendSms() {
        String host = "https://fsmsn.market.alicloudapi.com";  // 【1】请求地址 支持http 和 https 及 WEBSOCKET
        String path = "/fsms132";  // 【2】后缀
        String appcode = "自己的appcode"; // 【3】开通服务后 买家中心-查看AppCode
        String param = "123456";  // 【4】请求参数,详见文档描述
        String phone = "自己的手机号";  //  【4】请求参数,详见文档描述
        String sign = "175622";   //  【4】请求参数,详见文档描述
        String skin = "1";  //  【4】请求参数,详见文档描述
        String urlSend = host + path + "?param=" + param +"&phone="+phone +"&sign="+sign +"&skin="+skin;   // 【5】拼接请求链接
        try {
            URL url = new URL(urlSend);
            HttpURLConnection httpURLCon = (HttpURLConnection) url.openConnection();
            httpURLCon.setRequestProperty("Authorization", "APPCODE " + appcode);// 格式Authorization:APPCODE (中间是英文空格)
            int httpCode = httpURLCon.getResponseCode();
            if (httpCode == 200) {
                String json = read(httpURLCon.getInputStream());
                System.out.println("正常请求计费(其他均不计费)");
                System.out.println("获取返回的json:");
                System.out.print(json);
            } else {
                Map<String, List<String>> map = httpURLCon.getHeaderFields();
                String error = map.get("X-Ca-Error-Message").get(0);
                if (httpCode == 400 && error.equals("Invalid AppCode `not exists`")) {
                    System.out.println("AppCode错误 ");
                } else if (httpCode == 400 && error.equals("Invalid Url")) {
                    System.out.println("请求的 Method、Path 或者环境错误");
                } else if (httpCode == 400 && error.equals("Invalid Param Location")) {
                    System.out.println("参数错误");
                } else if (httpCode == 403 && error.equals("Unauthorized")) {
                    System.out.println("服务未被授权(或URL和Path不正确)");
                } else if (httpCode == 403 && error.equals("Quota Exhausted")) {
                    System.out.println("套餐包次数用完 ");
                } else {
                    System.out.println("参数名错误 或 其他错误");
                    System.out.println(error);
                }
            }

        } catch (MalformedURLException e) {
            System.out.println("URL格式错误");
        } catch (UnknownHostException e) {
            System.out.println("URL地址错误");
        } catch (Exception e) {
            // 打开注释查看详细报错异常信息
            // e.printStackTrace();
        }
    }

    /*
     * 读取返回结果
     */
    private static String read(InputStream is) throws IOException {
        StringBuffer sb = new StringBuffer();
        BufferedReader br = new BufferedReader(new InputStreamReader(is));
        String line = null;
        while ((line = br.readLine()) != null) {
            line = new String(line.getBytes(), "utf-8");
            sb.append(line);
        }
        br.close();
        return sb.toString();
    }

2、在gulimall-third-party中编写发送短信组件,其中hostpathappcode可以在配置文件中使用前缀spring.cloud.alicloud.sms进行配置

spring:
  cloud:
    alicloud:
      sms:
        host: https://fsmsn.market.alicloudapi.com
        path: /fsms132
        skin: 1
        sign: 1
        appcode: 自己的appcode

 添加“com.atguigu.gulimall.thridparty.component.SmsComponent”类,代码如下:

@Slf4j
@ConfigurationProperties(prefix = "spring.cloud.alicloud.sms")
@Data //为这些方法生成getter,setter
@Component
public class SmsComponent {

    private String host;  // 【1】请求地址 支持http 和 https 及 WEBSOCKET
    private String path;  // 【2】后缀
    private String skin;
    private String sign;
    private String appcode; // 【3】开通服务后 买家中心-查看AppCode

    public void sendSmsCode(String phone, String param) {
        String urlSend = host + path + "?param=" + param + "&phone=" + phone + "&sign=" + sign + "&skin=" + skin;   // 【5】拼接请求链接
        try {
            URL url = new URL(urlSend);
            HttpURLConnection httpURLCon = (HttpURLConnection) url.openConnection();
            httpURLCon.setRequestProperty("Authorization", "APPCODE " + appcode);// 格式Authorization:APPCODE (中间是英文空格)
            int httpCode = httpURLCon.getResponseCode();
            if (httpCode == 200) {
                String json = read(httpURLCon.getInputStream());
                log.info("sendSms获取返回的json:{}", json);
            } else {
                Map<String, List<String>> map = httpURLCon.getHeaderFields();
                String error = map.get("X-Ca-Error-Message").get(0);
                if (httpCode == 400 && error.equals("Invalid AppCode `not exists`")) {
                    log.error("AppCode错误");
                } else if (httpCode == 400 && error.equals("Invalid Url")) {
                    log.error("请求的 Method、Path 或者环境错误");
                } else if (httpCode == 400 && error.equals("Invalid Param Location")) {
                    log.error("参数错误");
                } else if (httpCode == 403 && error.equals("Unauthorized")) {
                    log.error("服务未被授权(或URL和Path不正确)");
                } else if (httpCode == 403 && error.equals("Quota Exhausted")) {
                    log.error("套餐包次数用完 ");
                } else {
                    log.error("参数名错误 或 其他错误:{}", error);
                }
            }

        } catch (MalformedURLException e) {
            System.out.println("URL格式错误");
        } catch (UnknownHostException e) {
            System.out.println("URL地址错误");
        } catch (Exception e) {
            // 打开注释查看详细报错异常信息
            // e.printStackTrace();
        }
    }

    /*
     * 读取返回结果
     */
    private static String read(InputStream is) throws IOException {
        StringBuffer sb = new StringBuffer();
        BufferedReader br = new BufferedReader(new InputStreamReader(is));
        String line = null;
        while ((line = br.readLine()) != null) {
            line = new String(line.getBytes(), "utf-8");
            sb.append(line);
        }
        br.close();
        return sb.toString();
    }
}

添加“com.atguigu.gulimall.thridparty.controller.SmsSendController”,代码如下:

@RestController
@RequestMapping("/sms")
public class SmsSendController {

    @Autowired
    SmsComponent smsComponent;
    /**
     * 提供给别的服务进行调用

     * @param phone
     * @param code
     * @return
     */
    @GetMapping("/sendCode")
    public R sendCode(@RequestParam("phone") String phone, @RequestParam("code") String code){
        smsComponent.sendSmsCode(phone,code);
        return R.ok();
    }
}

2、在gulimall-commom常量包下创建验证码的常量类

添加“com.atguigu.common.constant.AuthServerConstant”类,代码如下:

public class AuthServerConstant {
    public static final String SMS_CODE_CACHE_PREFIX = "sms:code:";
}

修改“com.atguigu.common.exception.BizCodeEnume”类,添加验证码60秒内重复获取验证码的错误提示常量

public enum BizCodeEnume {
    UNKNOW_EXCEPTION(10000,"系统未知异常"),
    VAILD_EXCEPTION(10001,"参数格式校验失败"),
    SMS_CODE_EXCEPTION(10002,"验证码获取频率太高,请稍后再试"),
    PRODUCT_UP_EXCEPTION(11000,"商品上架异常");

    private int code;
    private String msg;
    BizCodeEnume(int code,String msg){
        this.code = code;
        this.msg = msg;
    }
    public int getCode(){
        return code;
    }

    public String getMsg(){
        return msg;
    }
}

3、在gulimall-auth-server中进行远程调用短信验证码服务,并且页面渲染

添加redis依赖

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

配置redis

#配置redis
spring.redis.host=192.168.119.127
spring.redis.port=6379

远程调用验证码

添加”com.atguigu.gulimall.auth.feign.ThirdPartFeignService“类,代码如下:

@FeignClient("gulimall-third-party")
public interface ThirdPartFeignService {

    @GetMapping("/sms/sendCode")
    R sendCode(@RequestParam("phone") String phone, @RequestParam("code") String code);
}

4、接口防刷

由于发送验证码的接口暴露,为了防止恶意攻击,我们不能随意让接口被调用。

  • 在redis中以phone-code将电话号码和验证码进行存储并将当前时间与code一起存储
  • 如果调用时以当前phone取出的值不为空且当前时间在存储时间的60s以内,说明60s内该号码已经调用过,返回错误信息
  • 60s以后再次调用,需要删除之前存储的phone-code
  • code存在一个过期时间,我们设置为10min,10min内验证该验证码有效

添加“com.atguigu.gulimall.auth.controller.LoginController”类,代码如下:

@Controller
public class LoginController {

    @Autowired
    ThirdPartFeignService thirdPartFeignService;

    @Autowired
    StringRedisTemplate stringRedisTemplate;

    @ResponseBody
    @GetMapping("/sms/sendcode")
    public R sendCode(@RequestParam("phone") String phone){
        // 1、接口防刷
        String prefixPhone = AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone;
        String redisCode = stringRedisTemplate.opsForValue().get(prefixPhone);
        if (!StringUtils.isEmpty(redisCode)){
            long l = Long.parseLong(redisCode.split("_")[1]);
            if (System.currentTimeMillis() -l < 60000){
                // 60秒内不能再发
                return R.error(BizCodeEnume.SMS_CODE_EXCEPTION.getCode(),BizCodeEnume.SMS_CODE_EXCEPTION.getMsg());
            }
        }

        // 2、验证码的再次校验。redis 存key-phone, value-code   sms:code:18896736055 ->12345
        String code = String.valueOf((int)((Math.random() + 1) * 100000));
        // redis缓存验证码   防止同一个phone在60s内再次发送验证码  set(K var1, V var2, long var3, TimeUnit var5)
        stringRedisTemplate.opsForValue().set(prefixPhone,code + "_" + System.currentTimeMillis(),10, TimeUnit.MINUTES);
        thirdPartFeignService.sendCode(phone,code);
        return R.ok();
    }
}

修改reg.html的js

    /**
     * 发送验证码事件
     */
    $(function () {
        // 点击发送验证码按钮触发下面函数
        $('#sendCode').click(function () {
            // 1、倒计时 如果有disabled,说明最近已经点过,则什么都不做
            if ($(this).hasClass("disabled")) {
                //正在倒计时中
            } else {
                // 2、给指定手机号发送验证码
                $.get("/sms/sendcode?phone=" + $("#phoneNum").val(), function (data) {
                    if (data.code != 0) {
                        alert(data.msg);
                    }
                });
                setTimeout("timeoutChangeStyle()", 1000);
            }
        });
    })
    var num = 60;

    function timeoutChangeStyle() {
        // 开启倒计时后设置标志属性disable,使得该按钮不能再次被点击
        $('#sendCode').attr("class", "disabled");
        // 当时间为0时,说明倒计时完成,则重置
        if (num == 0) {
            $('#sendCode').text("发送验证码");
            num = 60;
            $('#sendCode').attr("class", "");
        } else {
            //每秒调用一次当前函数,使得num--
            var str = num + "s 后再次发送";
            $('#sendCode').text(str);
            setTimeout("timeoutChangeStyle()", 1000);
        }
        num--;
    }

5、注册接口编写

gulimall-auth-server服务中编写注册的主体逻辑:

  • 若JSR303校验未通过,则通过BindingResult封装错误信息,并重定向至注册页面
  • 若通过JSR303校验,则需要从redis中取值判断验证码是否正确,正确的话通过会员服务注册
  • 会员服务调用成功则重定向至登录页,否则封装远程服务返回的错误信息返回至注册页面

1)、使用JSR303校验要导入validation依赖

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

添加“com.atguigu.gulimall.auth.vo.UserRegistVo”类,代码如下:

@Data
public class UserRegistVo  {
    @NotEmpty(message = "用户名必须提交")
    @Length(min = 6, max = 18, message = "用户名必须是6-18位字符")
    private String userName;

    @NotEmpty(message = "密码必须填写")
    @Length(min = 6, max = 18, message = "密码必须是6-18位字符")
    private String password;

    @NotEmpty(message = "手机号必须填写")
    @Pattern(regexp = "^[1]([3-9])[0-9]{9}$",message = "手机号格式不正确")
    private String phone;

    @NotEmpty(message = "验证码必须填写")
    private String code;
}

注: 

  • RedirectAttributes可以通过session保存信息并在重定向的时候携带过去。
    重定向携带数据,利用session原理。将数据放在session中。只要跳到下一个页面,取出数据以后,session里面的数据就会删掉。
  • 使用 return "forward:/reg.html"; 会出现:
    • 问题:Request method 'POST' not supported的问题
    • 原因:用户注册-> /regist[post] ------>转发/reg.html (路径映射默认都是get方式访问的)
      校验出错转发到注册页

修改“com.atguigu.gulimall.auth.controller.LoginController”类,代码如下:

    /**
     * 重定向携带数据,利用session原理。将数据放在session中。只要跳到下一个页面,取出数据以后,session里面的数据就会删掉
     * RedirectAttributes redirectAttributes 模拟重定向携带数据
     * @param vo
     * @param result
     * @param redirectAttributes
     * @return
     */
    @PostMapping("/regist")
    public String regist(@Valid UserRegistVo vo, BindingResult result,
                         RedirectAttributes redirectAttributes){
        if (result.hasErrors()){
            /**
             * 方法一
             * Map<String, String> errors = result.getFieldErrors().stream().map(fieldError ->{
             *                 String field = fieldError.getField();
             *                 String defaultMessage = fieldError.getDefaultMessage();
             *                 errors.put(field,defaultMessage);
             *                 return errors;
             *             }).collect(Collector.asList());
             */
            // 方法二:
            // 1、如果校验不通过,则封装校验结果
            Map<String, String> errors = result.getFieldErrors().stream().collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));
            // 将错误信息封装到session中
            redirectAttributes.addFlashAttribute("errors",errors);
            /**
             * 使用 return "forward:/reg.html"; 会出现
             * 问题:Request method 'POST' not supported的问题
             * 原因:用户注册-> /regist[post] ------>转发/reg.html (路径映射默认都是get方式访问的)
             * 校验出错转发到注册页
             */
            //return "reg";    //转发会出现重复提交的问题,不要以转发的方式
            //使用重定向  解决重复提交的问题。但面临着数据不能携带的问题,就用RedirectAttributes
            return "redirect:http://auth.gulimall.com/reg.html";
        }

        // 2、校验验证码
        String code = vo.getCode();
        String s = stringRedisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + vo.getPhone());
        if (!StringUtils.isEmpty(s)) {
            if (code.equals(s.split("_")[0])) {
                // 验证码通过,删除缓存中的验证码;令牌机制
                stringRedisTemplate.delete(AuthServerConstant.SMS_CODE_CACHE_PREFIX + vo.getPhone());
                // 真正注册调用远程服务注册
                R r = memberFeignService.regist(vo);
                if (r.getCode() == 0) {
                    //成功
                    return "redirect:http://auth.gulimall.com/login.html";
                } else {
                    Map<String, String> errors = new HashMap<>();
                    errors.put("msg", r.getData(new TypeReference<String>() {
                    }));
                    redirectAttributes.addFlashAttribute("errors", errors);
                }
            } else {
                Map<String, String> errors = new HashMap<>();
                errors.put("code", "验证码错误");
                redirectAttributes.addFlashAttribute("errors", errors);
                return "redirect:http://auth.gulimall.com/reg.html";
            }
        } else {
            Map<String, String> errors = new HashMap<>();
            errors.put("code", "验证码错误");
            redirectAttributes.addFlashAttribute("errors", errors);
            // 校验出错转发到注册页
            return "redirect:http://auth.gulimall.com/reg.html";
        }

        // 注册成功回到登录页
        return "redirect:http://auth.gulimall.com/login.html";
    }

2)、添加远程调用会员服务注册用户的接口

添加“com.atguigu.gulimall.auth.feign.MemberFeignService”类,代码如下:

@FeignClient("gulimall-member")
public interface MemberFeignService {

    @PostMapping("/member/member/regist")
    public R regist(@RequestBody UserRegistVo vo);
}

通过gulimall-member会员服务注册逻辑

  • 通过异常机制判断当前注册会员名和电话号码是否已经注册,如果已经注册,则抛出对应的自定义异常,并在返回时封装对应的错误信息
  • 如果没有注册,则封装传递过来的会员信息,并设置默认的会员等级、创建时间

修改“com.atguigu.gulimall.member.controller.MemberController” 类,代码如下

    @PostMapping("/regist")
    public R regist(@RequestBody MemberRegistVo vo){
        try{
            memberService.regist(vo);
            //异常机制:通过捕获对应的自定义异常判断出现何种错误并封装错误信息
        }catch (PhoneExistException e){
            return R.error(BizCodeEnume.PHONE_EXIST_EXCEPTION.getCode(),BizCodeEnume.PHONE_EXIST_EXCEPTION.getMsg());
        }catch (UserNameExistException e){
            return R.error(BizCodeEnume.USER_EXIST_EXCEPTION.getCode(),BizCodeEnume.USER_EXIST_EXCEPTION.getMsg());
        }
            return R.ok();
    }

 添加“com.atguigu.gulimall.member.vo.MemberRegistVo”类,代码如下:

@Data
public class MemberRegistVo {

    private String userName;

    private String password;

    private String phone;
}

在gulimall-common添加“com.atguigu.common.exception.BizCodeEnume”类,用户错误信息常量

/***
 * 错误码和错误信息定义类
 * 1. 错误码定义规则为5为数字
 * 2. 前两位表示业务场景,最后三位表示错误码。例如:100001。10:通用 001:系统未知异常
 * 3. 维护错误码后需要维护错误描述,将他们定义为枚举形式
 * 错误码列表:
 *  10: 通用
 *      001:参数格式校验
 *      002:短信验证码频率太高
 *  11: 商品
 *  12: 订单
 *  13: 购物车
 *  14: 物流
 *  15: 用户
 */
public enum BizCodeEnume {
    UNKNOW_EXCEPTION(10000,"系统未知异常"),
    VAILD_EXCEPTION(10001,"参数格式校验失败"),
    SMS_CODE_EXCEPTION(10002,"验证码获取频率太高,请稍后再试"),
    PRODUCT_UP_EXCEPTION(11000,"商品上架异常"),
    USER_EXIST_EXCEPTION(15001,"用户已存在"),
    PHONE_EXIST_EXCEPTION(15002,"手机号已存在");

    private int code;
    private String msg;
    BizCodeEnume(int code,String msg){
        this.code = code;
        this.msg = msg;
    }
    public int getCode(){
        return code;
    }

    public String getMsg(){
        return msg;
    }
}

修改“com.atguigu.gulimall.member.service.MemberService”类,代码如下:


    /**
     * 注册用户
     *
     * @param vo
     */
    void regist(MemberRegistVo vo);

    /**
     * 校验手机号唯一性
     * 
     * @param phone
     * @throws PhoneExistException
     */
    void checkPhoneUnique(String phone) throws PhoneExistException;

    /**
     * 校验用户名唯一性
     * 
     * @param userName
     * @throws UserNameExistException
     */
    void checkUserNameUnique(String userName) throws UserNameExistException;

修改“com.atguigu.gulimall.member.service.impl.MemberServiceImpl”类,代码如下:

    @Override
    public void regist(MemberRegistVo vo) {
        MemberDao memberDao = this.baseMapper;
        MemberEntity entity = new MemberEntity();
        // 设置默认等级
        MemberLevelEntity levelEntity = memberLevelDao.getDefaultLevel();
        entity.setLevelId(levelEntity.getId());

        // 检查用户名和手机号是否唯一。为了让controller能感知异常,异常机制
        checkPhoneUnique(vo.getPhone());
        checkUserNameUnique(vo.getUserName());

        entity.setMobile(vo.getPhone());
        entity.setUsername(vo.getUserName());
        // 密码加密
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        String encode = passwordEncoder.encode(vo.getPassword());
        entity.setPassword(encode);
        memberDao.insert(entity);
    }

    @Override
    public void checkPhoneUnique(String phone) throws PhoneExistException {
        MemberDao memberDao = this.baseMapper;
        Integer count = memberDao.selectCount(new QueryWrapper<MemberEntity>().eq("mobile", phone));
        if (count > 0){
            throw new PhoneExistException();
        }
    }

    @Override
    public void checkUserNameUnique(String userName) throws UserNameExistException  {
        MemberDao memberDao = this.baseMapper;
        Integer count = memberDao.selectCount(new QueryWrapper<MemberEntity>().eq("username", userName));
        if (count > 0){
            throw new UserNameExistException();
        }
    }

添加“com.atguigu.gulimall.member.dao.MemberLevelDao”类,代码如下:

@Mapper
public interface MemberLevelDao extends BaseMapper<MemberLevelEntity> {

    MemberLevelEntity getDefaultLevel();
}
    <select id="getDefaultLevel" resultType="com.atguigu.gulimall.member.entity.MemberLevelEntity">
        select * from ums_member_level where default_status = 1
    </select>

添加“com.atguigu.gulimall.member.exception.PhoneExistException”类,代码如下:

public class PhoneExistException extends RuntimeException {
    public PhoneExistException(){
        super("手机号存在");
    }
}

添加“com.atguigu.gulimall.member.exception.UserNameExistException”类,代码如下:

public class UserNameExistException extends RuntimeException {
    public UserNameExistException(){
        super("用户名已存在");
    }
}

3)、修改注册页面reg.html

    <form action="/regist" method="post" class="one">
        <div class="tips" style="color:red"
             th:text="${errors != null ? (#maps.containsKey(errors,'msg')?errors.msg:'') : ''}">
        </div>
        <div class="register-box">
            <label class="username_label">用 户 名
                <input name="userName" maxlength="20" type="text" placeholder="您的用户名和登录名">
            </label>
            <div class="tips" style="color:red"
                 th:text="${errors != null ? (#maps.containsKey(errors,'userName')?errors.userName:'') : ''}">
            </div>
        </div>
        <div class="register-box">
            <label class="other_label">设 置 密 码
                <input name="password" maxlength="20" type="password" placeholder="建议至少使用两种字符组合">
            </label>
            <div class="tips" style="color:red"
                 th:text="${errors != null ? (#maps.containsKey(errors,'password')?errors.password:'') : ''}">
            </div>
        </div>
        <div class="register-box">
            <label class="other_label">确 认 密 码
                <input maxlength="20" type="password" placeholder="请再次输入密码">
            </label>
            <div class="tips">
            </div>
        </div>
        <div class="register-box">
            <label class="other_label">
                <span>中国 0086∨</span>
                <input name="phone" class="phone" id="phoneNum" maxlength="20" type="text"
                       placeholder="建议使用常用手机">
            </label>
            <div class="tips" style="color:red"
                 th:text="${errors != null ? (#maps.containsKey(errors,'phone')?errors.phone:'') : ''}">
            </div>
        </div>
        <div class="register-box">
            <label class="other_label">验 证 码
                <input name="code" maxlength="20" type="text" placeholder="请输入验证码" class="caa">
            </label>
            <a id="sendCode">发送验证码</a>
            <div class="tips" style="color:red"
                 th:text="${errors != null ? (#maps.containsKey(errors,'code')?errors.code:'') : ''}">
            </div>
        </div>
        <div class="arguement">
            <input type="checkbox" id="xieyi"> 阅读并同意
            <a href="/static/reg/#">《谷粒商城用户注册协议》</a>
            <a href="/static/reg/#">《隐私政策》</a>
            <div class="tips">
            </div>
            <br/>
            <div class="submit_btn">
                <button type="submit" id="submit_btn">立 即 注 册</button>
            </div>
        </div>
    </form>

 注释掉提交按钮事件

        //	提交按钮
        // $("#submit_btn").click(function(e) {
        // 	for(var j = 0; j < 5; j++) {
        // 		if($('input').eq(j).val().length == 0) {
        // 			$('input').eq(j).focus();
        // 			if(j == 4) {
        // 				$('input').eq(j).parent().next().next("div").text("此处不能为空");
        // 				$('input').eq(j).parent().next().next("div").css("color", 'red');
        // 				e.preventDefault();
        // 				return;
        // 			}
        // 			$('input').eq(j).parent().next(".tips").text("此处不能为空");
        // 			$('input').eq(j).parent().next(".tips").css("color", 'red');
        // 			e.preventDefault();
        // 			return;
        // 		}
        // 	}
        // 	//协议
        // 	if($("#xieyi")[0].checked) {
        // 		//向变量stuList数组添加一个数值,数值内部格式Student(name,password,tel,id)
        // 		//发送用户信息
        // 		stuList.push(new Student($('input').eq(0).val(), $('input').eq(1).val(), $('input').eq(3).val(), stuList.length + 1));
        // 		localStorage.setItem('stuList', JSON.stringify(stuList));
        // 		alert("注册成功");
        // 		window.open("userlist.html", "_blank");
        // 	} else {
        // 		$("#xieyi").next().next().next(".tips").text("请勾选协议");
        // 		$("#xieyi").next().next().next(".tips").css("color", 'red');
        // 		e.preventDefault();
        // 		return;
        // 	}
        // })

4)、测试校验功能

3、用户名密码登录

gulimall-auth-server模块中的主体逻辑

  • 通过会员服务远程调用登录接口
    • 如果调用成功,重定向至首页
    • 如果调用失败,则封装错误信息并携带错误信息重定向至登录页

添加“com.atguigu.gulimall.auth.vo.UserLoginVo”类,代码如下:

@Data
public class UserLoginVo {

    private String loginacct;

    private String password;
}

修改“com.atguigu.gulimall.auth.vo.UserLoginVo”类,代码如下:

    @PostMapping("/member/member/login")
    public R login(@RequestBody UserLoginVo vo);

修改“com.atguigu.gulimall.auth.controller.LoginController”类,代码如下:

    @PostMapping("/login")
    public String login(UserLoginVo vo, RedirectAttributes redirectAttributes){
        // 远程登录
        R login = memberFeignService.login(vo);
        if (login.getCode() == 0){
            // 成功
            return "redirect:http://gulimall.com";
        }else{
            Map<String,String> errors = new HashMap<>();
            errors.put("msg",login.getData("msg",new TypeReference<String>(){}));
            redirectAttributes.addFlashAttribute("errors",errors);
            return "redirect:http://auth.gulimall.com/login.html";
        }
    }

 在gulimall-member模块中完成登录

  • 当数据库中含有以当前登录名为用户名或电话号且密码匹配时,验证通过,返回查询到的实体
  • 否则返回null,并在controller返回用户名或者密码错误。

添加“com.atguigu.gulimall.member.vo.MemberLoginVo”类,代码如下:

@Data
public class MemberLoginVo {
    private String loginacct;

    private String password;
}

 在gulimall-common服务中,添加登录错误信息提示的常量

添加“com.atguigu.common.exception.BizCodeEnume”类,代码如下:

LOGINACCT_PASSWORD_INVAILD_EXCEPTION(15003,"账号或密码错误")

修改“com.atguigu.gulimall.member.controller.MemberController”类,代码如下:

    @PostMapping("/login")
    public R login(@RequestBody MemberLoginVo vo) {
        MemberEntity entity = memberService.login(vo);
        if (entity != null) {
            return R.ok().setData(entity);
        } else {
            return R.error(BizCodeEnume.LOGINACCT_PASSWORD_INVAILD_EXCEPTION.getCode(), BizCodeEnume.LOGINACCT_PASSWORD_INVAILD_EXCEPTION.getMsg());
        }
    }

 修改“com.atguigu.gulimall.member.service.MemberService”类,代码如下:

MemberEntity login(MemberLoginVo vo);

修改“com.atguigu.gulimall.member.service.impl.MemberServiceImpl”类,代码如下:

    @Override
    public MemberEntity login(MemberLoginVo vo) {
        String loginacct = vo.getLoginacct();
        String password = vo.getPassword();
        // 去数据库查询
        MemberDao memberDao = this.baseMapper;
        MemberEntity memberEntity = memberDao.selectOne(new QueryWrapper<MemberEntity>().eq("username", loginacct).or().eq("mobile", loginacct));
        if (memberEntity == null){
            // 登录失败
            return null;
        }else {
            // 1、获取到数据库的password
            String passwordDB = memberEntity.getPassword();
            BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
            // 2、密码匹配
            boolean matches = passwordEncoder.matches(password, passwordDB);
            if (matches){
                return memberEntity;
            }else {
                return null;
            }
        }

    }

修改登录页面login.html

            <form action="/login" method="post">
                <ul>
                    <div class="tips" style="color:red"
                         th:text="${errors != null ? (#maps.containsKey(errors,'msg')?errors.msg:'') : ''}">
                    </div>
                    <li class="top_1">
                        <img src="/static/login/JD_img/user_03.png" class="err_img1"/>
                        <input name="loginacct" type="text" placeholder=" 邮箱/用户名/已验证手机" class="user"/>
                    </li>
                    <li>
                        <img src="/static/login/JD_img/user_06.png" class="err_img2"/>
                        <input type="password" name="password" placeholder=" 密码" class="password"/>
                    </li>
                    <li class="bri">
                        <a href="/static/login/">忘记密码</a>
                    </li>
                    <li class="ent">
                        <button type="submit" class="btn2"><a>登 &nbsp; &nbsp;录</a></button>
                    </li>
                </ul>
            </form>

 4、社交登录

QQ 、微博、 github 等网站的用户量非常大,别的网站为了简化自我网站的登陆与注册逻辑,引入社交登陆功能;
步骤:
1 )、用户点击 QQ 按钮
2 )、引导跳转到 QQ 授权页

 

3)、用户主动点击授权,跳回之前网页。 

4.1OAuth2.0

  • OAuthOAuth(开放授权)是一个开放标准,允许用户授权第三方网站访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方网站或分享他们数据的所有内容。
  • OAuth2.0对于用户相关的 OpenAPI(例如获取用户信息,动态同步,照片,日志,分享等),为了保护用户数据的安全和隐私,第三方网站访问用户数据前都需要显式的向用户征求授权。
  • 官方版流程: 

        (A )用户打开客户端以后,客户端要求用户给予授权。
        (B )用户同意给予客户端授权。
        (C )客户端使用上一步获得的授权,向认证服务器申请令牌。
        (D )认证服务器对客户端进行认证以后,确认无误,同意发放令牌。
        (E )客户端使用令牌,向资源服务器申请获取资源。
        (F )资源服务器确认令牌无误,同意向客户端开放资源。

OAuth2.0流程:

  1. 使用Code换取AccessTokenCode只能用一次
  2. 同一个用户的accessToken一段时间是不会变化的,即使多次获取

4.2、微博登陆准备工作 

1)、进入微博开放平台 

https://open.weibo.com/widgets?cat=wb

2)、登陆微博,进入微连接,选择网站接入 

微博组件_微博开放平台

 (2) 在微博开放平台创建应用

3)、选择立即接入 

  ​4)、创建自己的应用

5)、我们可以在开发阶段进行测试了 

记住自己的 app key app secret 我们一会儿用 

6)、进入高级信息,填写授权回调页的地址 

7)、添加测试账号(选做) 

8)、进入文档,按照流程测试社交登陆 

4.3、微博登陆测试 

1)、在登录页引导用户至授权页

GET
https://api.weibo.com/oauth2/authorize?client_id=YOUR_CLIENT_ID&response_type=code&redirect_uri=YOUR_REGISTERED_REDIRECT_URI
  • client_id: 创建网站应用时的App Key
  • YOUR_REGISTERED_REDIRECT_URI授权回调页(需要和平台高级设置一致)

点击微博跳转到授权页

 ​2)、用户同意授权,页面跳转至 xxx/?code=CODE

如果用户同意授权,页面跳转至 YOUR_REGISTERED_REDIRECT_URI/?code=CODE

 code是我们用来换取令牌的参数

 3、使用返回的 code,换取 access token

POST
https://api.weibo.com/oauth2/access_token?client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET&grant_type=authorization_code&redirect_uri=YOUR_REGISTERED_REDIRECT_URI&code=CODE
  • client_id: 创建网站应用时的app key
  • client_secret: 创建网站应用时的app secret
  • YOUR_REGISTERED_REDIRECT_URI: 授权回调页(需要和平台高级设置一致)
  • code:换取令牌的认证码

返回数据如下

{
    "access_token": "2.00J9DmJG0UPMCs0f1217523aBnzmZD",
    "remind_in": "157679999",
    "expires_in": 157679999,
    "uid": "5641237037",
    "isRealName": "true"
}

4)、使用 AccessToken 调用开发 API 获取用户信息

获取用户信息接口文档https://open.weibo.com/wiki/2/users/show

 返回结果

JSON示例
{
    "id": 1404376560,
    "screen_name": "zaku",
    "name": "zaku",
    "province": "11",
    "city": "5",
    "location": "北京 朝阳区",
    "description": "人生五十年,乃如梦如幻;有生斯有死,壮士复何憾。",
    "url": "http://blog.sina.com.cn/zaku",
    "profile_image_url": "http://tp1.sinaimg.cn/1404376560/50/0/1",
    "domain": "zaku",
    "gender": "m",
    "followers_count": 1204,
    "friends_count": 447,
    "statuses_count": 2908,
    "favourites_count": 0,
    "created_at": "Fri Aug 28 00:00:00 +0800 2009",
    "following": false,
    "allow_all_act_msg": false,
    "geo_enabled": true,
    "verified": false,
    "status": {
        "created_at": "Tue May 24 18:04:53 +0800 2011",
        "id": 11142488790,
        "text": "我的相机到了。",
        "source": "<a href="http://weibo.com" rel="nofollow">新浪微博</a>",
        "favorited": false,
        "truncated": false,
        "in_reply_to_status_id": "",
        "in_reply_to_user_id": "",
        "in_reply_to_screen_name": "",
        "geo": null,
        "mid": "5610221544300749636",
        "annotations": [],
        "reposts_count": 5,
        "comments_count": 8
    },
    "allow_all_comment": true,
    "avatar_large": "http://tp1.sinaimg.cn/1404376560/180/0/1",
    "verified_reason": "",
    "follow_me": false,
    "online_status": 0,
    "bi_followers_count": 215
}

至此微博登陆调试完成。

4.4、代码实现

1)、修改登录页面login.html,在登录页引导用户至授权页

        <div class="si_out">
            <ul>
                <li>
                    <a href="https://api.weibo.com/oauth2/authorize?client_id=798445888&response_type=code&redirect_uri=http://auth.gulimall.com/oauth2.0/weibo/success">
                        <img style="width: 50px;height: 18px" src="/static/login/JD_img/weibo.png"/>
                    </a>
                </li>

 2)、添加社交登录回调接口

 认证接口

  • 通过HttpUtils发送请求获取token,并将token等信息交给member服务进行社交登录
  • 若获取token失败或远程调用服务失败,则封装错误信息重新转回登录页

修改“com.atguigu.gulimall.auth.feign.MemberFeignService”类,代码如下:

    @PostMapping("/member/member/oauth2/login")
    public R oauth2Login(@RequestBody SocialUser socialUser);

添加“com.atguigu.gulimall.auth.vo.SocialUser”类,代码如下:

package com.atguigu.gulimall.auth.vo;

import lombok.Data;

@Data
public class SocialUser {
    private String access_token;
    private String remind_in;
    private long expires_in;
    private String uid;
    private String isRealName;
}

 添加“com.atguigu.gulimall.auth.vo.MemberResponseVO”类,代码如下:

package com.atguigu.gulimall.auth.vo;

import lombok.Data;
import lombok.ToString;

import java.util.Date;

@ToString
@Data
public class MemberResponseVO {
    private Long id;
    /**
     * 会员等级id
     */
    private Long levelId;
    /**
     * 用户名
     */
    private String username;
    /**
     * 密码
     */
    private String password;
    /**
     * 昵称
     */
    private String nickname;
    /**
     * 手机号码
     */
    private String mobile;
    /**
     * 邮箱
     */
    private String email;
    /**
     * 头像
     */
    private String header;
    /**
     * 性别
     */
    private Integer gender;
    /**
     * 生日
     */
    private Date birth;
    /**
     * 所在城市
     */
    private String city;
    /**
     * 职业
     */
    private String job;
    /**
     * 个性签名
     */
    private String sign;
    /**
     * 用户来源
     */
    private Integer sourceType;
    /**
     * 积分
     */
    private Integer integration;
    /**
     * 成长值
     */
    private Integer growth;
    /**
     * 启用状态
     */
    private Integer status;
    /**
     * 注册时间
     */
    private Date createTime;

    private String socialUid;

    private String accessToken;

    private long expiresIn;
}

添加“com.atguigu.gulimall.auth.controller.Oauth2Controller”类,代码如下:

@Controller
public class OauthController {

    @Autowired
    private MemberFeignService memberFeignService;

    @RequestMapping("/oauth2.0/weibo/success")
    public String authorize(String code, RedirectAttributes attributes) throws Exception {
        // 1、使用code换取token,换取成功则继续2,否则重定向至登录页
        Map<String, String> query = new HashMap<>();
        query.put("client_id", "2144***074");
        query.put("client_secret", "ff63a0d8d5*****29a19492817316ab");
        query.put("grant_type", "authorization_code");
        query.put("redirect_uri", "http://auth.gulimall.com/oauth2.0/weibo/success");
        query.put("code", code);
        // 发送post请求换取token
        HttpResponse response = HttpUtils.doPost("https://api.weibo.com", "/oauth2/access_token", "post", new HashMap<String, String>(), query, new HashMap<String, String>());
        Map<String, String> errors = new HashMap<>();
        if (response.getStatusLine().getStatusCode() == 200) {
            // 2. 调用member远程接口进行oauth登录,登录成功则转发至首页并携带返回用户信息,否则转发至登录页
            String json = EntityUtils.toString(response.getEntity());
            SocialUser socialUser = JSON.parseObject(json, new TypeReference<SocialUser>() {
            });
            R login = memberFeignService.login(socialUser);
            // 2.1 远程调用成功,返回首页并携带用户信息
            if (login.getCode() == 0) {
                String jsonString = JSON.toJSONString(login.get("memberEntity"));
                MemberResponseVo memberResponseVo = JSON.parseObject(jsonString, new TypeReference<MemberResponseVo>() {
                });
                attributes.addFlashAttribute("user", memberResponseVo);
                return "redirect:http://gulimall.com";
            }else {
                // 2.2 否则返回登录页
                errors.put("msg", "登录失败,请重试");
                attributes.addFlashAttribute("errors", errors);
                return "redirect:http://auth.gulimall.com/login.html";
            }
        }else {
            errors.put("msg", "获得第三方授权失败,请重试");
            attributes.addFlashAttribute("errors", errors);
            return "redirect:http://auth.gulimall.com/login.html";
        }
    }

登录接口

  • 登录包含两种流程,实际上包括了注册和登录
  • 如果之前未使用该社交账号登录,则使用token调用开放api获取社交账号相关信息,注册并将结果返回
  • 如果之前已经使用该社交账号登录,则更新token并将结果返回

 添加“com.atguigu.gulimall.member.vo.SocialUser”类,代码如下:

package com.atguigu.gulimall.member.vo;

import lombok.Data;

@Data
public class SocialUser {
    private String access_token;
    private String remind_in;
    private long expires_in;
    private String uid;
    private String isRealName;
}

修改“com.atguigu.gulimall.member.controller.MemberController”类,代码如下:

    @PostMapping("/oauth2/login")
    public R oauth2Login(@RequestBody SocialUser socialUser){
        MemberEntity entity = memberService.login(socialUser);
        if (entity != null){
            return R.ok().setData(entity);
        }else {
            return R.error(BizCodeEnume.LOGINACCT_PASSWORD_INVAILD_EXCEPTION.getCode(),BizCodeEnume.LOGINACCT_PASSWORD_INVAILD_EXCEPTION.getMsg());
        }

    }

修改gulimall_ums.ums_member表结构,sql如下:

ALTER TABLE `gulimall_ums`.`ums_member` 
ADD COLUMN `social_uid` varchar(255) NULL COMMENT '社交用户id' AFTER `create_time`,
ADD COLUMN `access_token` varchar(255) NULL COMMENT '访问token' AFTER `social_uid`,
ADD COLUMN `expires_in` int NULL COMMENT '过期时间戳' AFTER `access_token`;

修改“com.atguigu.gulimall.member.entity.MemberEntity”类,新增三个属性,代码如下:

修改“com.atguigu.gulimall.member.service.MemberService”类,代码如下:

  MemberEntity login(SocialUser socialUser);

 修改“com.atguigu.gulimall.member.service.impl.MemberServiceImpl”类,代码如下:

    @Override
    public MemberEntity login(SocialUser socialUser) {
        // 1 根据 uid 判断当前用户是否以前用社交平台登录过系统
        MemberEntity memberEntity = this.baseMapper.selectOne(new QueryWrapper<MemberEntity>().eq("social_uid", socialUser.getUid()));
        if (!StringUtils.isEmpty(memberEntity)) {
            // 说明这个用户之前已经注册过
            MemberEntity update = new MemberEntity();
            update.setId(memberEntity.getId());
            update.setAccessToken(socialUser.getAccess_token());
            update.setExpiresIn(socialUser.getExpires_in());
            this.baseMapper.updateById(update);

            memberEntity.setAccessToken(socialUser.getAccess_token());
            memberEntity.setExpiresIn(socialUser.getExpires_in());
            return memberEntity;
        } else {
            // 未找到则注册 根据社交平台的开放接口查询用户的开放信息存储到系统
            MemberEntity register = new MemberEntity();
            try {
                Map<String, String> query = new HashMap<>();
                query.put("access_token", socialUser.getAccess_token());
                query.put("uid", socialUser.getUid());
                HttpResponse response = HttpUtils.doGet("https://api.weibo.com", "/2/users/show.json", "get", new HashMap<>(), query);
                if (response.getStatusLine().getStatusCode() == 200) {
                    String json = EntityUtils.toString(response.getEntity());
                    JSONObject jsonObject = JSON.parseObject(json);
                    String name = jsonObject.getString("name");
                    String gender = jsonObject.getString("gender");
                    // ......
                    register.setNickname(name);
                    register.setGender("m".equals(gender) ? 1 : 0);
                    // .....
                }
            } catch (Exception e) {
                log.warn("调用微博接口获取信息异常{}", e);
            }
            register.setSocialUid(socialUser.getUid());
            register.setAccessToken(socialUser.getAccess_token());
            register.setExpiresIn(socialUser.getExpires_in());
            this.baseMapper.insert(register);
            return register;
        }
    }
小结
Oauth2.0 ;授权通过后,使用 code 换取 access_token ,然后去访问任何开放 API
1 )、 code 用后即毁
2 )、 access_token 在几天内是一样的
3 )、 uid 永久固定

5、SpringSession

5.1、Session共享问题

1)、session原理

jsessionid相当于银行卡,存在服务器的session相当于存储的现金,每次通过jsessionid取出保存的数据。

问题:但是正常情况下session不可跨域,它有自己的作用范围

2)、分布式下session共享问题

  • 同一个服务,复制多份,session不同步问题
  • 不同服务,session不能共享问题

5.2、Session共享问题解决

1)、session复制

2)、客户端存储

3)、hash一致性

 ​4)、统一存储

5)、不同服务,子域session共享

5.3、SpringSession整合redis

通过SpringSession修改session的作用域

1)、环境搭建

gulimall-auth-server模块

pom导入依赖

    <dependency>
        <groupId>org.springframework.session</groupId>
        <artifactId>spring-session-data-redis</artifactId>
    </dependency>
    

修改apllication.properties配置

spring.session.store-type=redis

主配置类添加注解@EnableRedisHttpSession

修改“com.atguigu.gulimall.auth.GulimallAuthServerApplication”类,代码如下: 

@EnableRedisHttpSession
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class GulimallAuthServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(GulimallAuthServerApplication.class, args);
    }

}

2)、自定义配置

  • 由于默认使用jdk进行序列化,通过导入RedisSerializer修改为json序列化

  • 并且通过修改CookieSerializer扩大session的作用域至**.gulimall.com

添加“com.atguigu.gulimall.auth.config.GulimallSessionConfig”类,代码如下: 

@Configuration
public class GulimallSessionConfig {

    @Bean
    public CookieSerializer cookieSerializer(){
        DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
        cookieSerializer.setDomainName("gulimall.com");
        cookieSerializer.setCookieName("GULISESSION");
        return cookieSerializer;
    }

    @Bean
    public RedisSerializer<Object> springSessionDefaultRedisSerializer(){
        return new GenericJackson2JsonRedisSerializer();
    }
}

把MemberResponseVo类移到gulimall-common服务里,并且序列化

添加“com.atguigu.common.vo.MemberResponseVO”类,代码如下

@ToString
@Data
public class MemberResponseVO implements Serializable {
    private Long id;
    /**
     * 会员等级id
     */
    private Long levelId;
    /**
     * 用户名
     */
    private String username;
    /**
     * 密码
     */
    private String password;
    /**
     * 昵称
     */
    private String nickname;
    /**
     * 手机号码
     */
    private String mobile;
    /**
     * 邮箱
     */
    private String email;
    /**
     * 头像
     */
    private String header;
    /**
     * 性别
     */
    private Integer gender;
    /**
     * 生日
     */
    private Date birth;
    /**
     * 所在城市
     */
    private String city;
    /**
     * 职业
     */
    private String job;
    /**
     * 个性签名
     */
    private String sign;
    /**
     * 用户来源
     */
    private Integer sourceType;
    /**
     * 积分
     */
    private Integer integration;
    /**
     * 成长值
     */
    private Integer growth;
    /**
     * 启用状态
     */
    private Integer status;
    /**
     * 注册时间
     */
    private Date createTime;

    private String socialUid;

    private String accessToken;

    private long expiresIn;
}

修改“com.atguigu.gulimall.auth.controller.Oauth2Controller”类,代码如下:

    @GetMapping("/oauth2.0/weibo/success")
    public String weibo(@RequestParam("code") String code, HttpSession session, RedirectAttributes attributes) throws Exception {
        // 1. 使用code换取token,换取成功则继续2,否则重定向至登录页
        Map<String,String> map = new HashMap<>();
        map.put("client_id","798445888");
        map.put("client_secret","7886f4db232d2e932690e08e346c3e67");
        map.put("grant_type","authorization_code");
        map.put("redirect_uri","http://auth.gulimall.com/oauth2.0/weibo/success");
        map.put("code",code);
        // 1、根据code换取accessToken
        HttpResponse response = HttpUtils.doPost("https://api.weibo.com", "/oauth2/access_token", "post", new HashMap<>(), map, new HashMap<>());
        Map<String, String> errors = new HashMap<>();
        if (response.getStatusLine().getStatusCode() == 200){
            // 2. 调用member远程接口进行oauth登录,登录成功则转发至首页并携带返回用户信息,否则转发至登录页
            String jsonString = EntityUtils.toString(response.getEntity());
            SocialUser socialUser = JSON.parseObject(jsonString, SocialUser.class);
            // 1)、当前用户如果是第一次进网站,自动注册进来(为当前社交用户生成一个会员信息账号,以后这个社交账号就对应指定的会员)
            // 获取用户的登录平台,然后判断用户是否该注册到系统中
            R r = memberFeignService.oauth2Login(socialUser);
            // 2.1 远程调用成功,返回首页并携带用户信息
            if (r.getCode() == 0) {
                // session 子域共享问题
                MemberResponseVO loginUser = r.getData(new TypeReference<MemberResponseVO>() {});
                log.info("登陆成功:用户信息"+loginUser.toString());
                //TODO 1、默认发的令牌。 session=dakadja; 作用域:当前域。(解决子域session共享问题)
                //TODO 2、使用json的序列化方式来序列化对象数据到redis中
                session.setAttribute("loginUser", loginUser);

                return "redirect:http://gulimall.com";
            } else {
                //2.2 否则返回登录页
                errors.put("msg", "登录失败,请重试");
                attributes.addFlashAttribute("errors", errors);
                return "redirect:http://auth.catmall.com/login.html ";
            }
        }else {
            errors.put("msg", "获得第三方授权失败,请重试");
            attributes.addFlashAttribute("errors", errors);
            return "redirect:http://auth.gulimall.com/login.html";
        }

    }

gulimall-product模块

添加依赖

        <!--整合SpringSession完成session共享问题-->
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>

修改配置

spring:
  session:
    store-type: redis   #指定session的存储格式

添加注解

配置“com.atguigu.gulimall.product.config.GulimallSessionConfig”类,代码如下:

@Configuration
public class GulimallSessionConfig {

    @Bean
    public CookieSerializer cookieSerializer(){
        DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
        cookieSerializer.setDomainName("gulimall.com");
        cookieSerializer.setCookieName("GULISESSION");
        return cookieSerializer;
    }

    @Bean
    public RedisSerializer<Object> springSessionDefaultRedisSerializer(){
        return new GenericJackson2JsonRedisSerializer();
    }
}

3)、修改主页index.html的代码

          <li>
            <a th:if="${session.loginUser != null}">欢迎:[[${session.loginUser == null ? '' : session.loginUser.nickname}]]</a>
            <a href="http://auth.gulimall.com/login.html" th:if="${session.loginUser == null}">欢迎,请登录</a>
          </li>
          <li>
            <a th:if="${session.loginUser == null}" href="http://auth.gulimall.com/reg.html">免费注册</a>
          </li>

 4)、测试

可以看到当我们社交登录之后,主页显示当前登录的用户名

5.5、SpringSession核心原理

核心原理:
1)、@EnableRedisHttpSession导入RedisHttpSessionConfiguration.class配置
      1、给容器中添加了一个组件
          SessionRepository=》》》 【RedisIndexedSessionRepository】=>redis操作session.session的增删改查的封装类
      2、SessionRepositoryFilter=》Filter:  session存储过滤器,每个请求过来都必须经过filter
          1、创建的时候,就自动从容器中获取到了SessionRepository:
          2、原生的request,response都被包装。SessionRepositoryRequestWrapper,SessionRepositoryResponseWrapper
          3、以后获取session.request.getSession()
          4、wrapperedRequest.getSession();===>SressionRepository中获取到

   自动延期。redis中的数据也是有过期时间的

装饰者模式 - SessionRepositoryFilter

  • 原生的获取session时是通过HttpServletRequest获取的
  • 这里对request进行包装,并且重写了包装request的getSession()方法

6、页面调整

1)、只要登录成功,缓存有用户数据,再点击登录链接,直接调转到首页;把GulimallWebConfig登录页的映射注释掉

修改“com.atguigu.gulimall.auth.controller.LoginController”类,代码如下:

    @GetMapping("/login.html")
    public String loginPage(HttpSession session){
        Object attribute = session.getAttribute(AuthServerConstant.LOGIN_USER);
        if (attribute == null) {
            //没登录
            return "login";
        } else{
            return "redirect:http://gulimall.com";
        }
    }

 2)、账号密码方式登录也要显示用户名

正常登录也要显示用户名,返回时也要给他放入用户信息

修改“com.atguigu.gulimall.auth.controller.LoginController”类,代码如下:

    @PostMapping("/login")
    public String login(UserLoginVo vo, RedirectAttributes redirectAttributes, HttpSession session) {
        log.info("登录请求参数:{}", JSON.toJSONString(vo));
        //远程登录
        R r = memberFeignService.login(vo);
        if (r.getCode() == 0) {
            MemberResponseVO loginUser = r.getData(new TypeReference<MemberResponseVO>() {
            });
            // 成功放到session中
            session.setAttribute(AuthServerConstant.LOGIN_USER, loginUser);
            return "redirect:http://gulimall.com";
        } else {
            Map<String, String> errors = new HashMap<>();
            errors.put("msg", r.getData("msg", new TypeReference<String>() {
            }));
            redirectAttributes.addFlashAttribute("errors", errors);
            return "redirect:http://auth.gulimall.com/login.html";
        }
    }

 3)、只要登陆成功每个页面都显示用户名 

详情页item.html

							<li style="border: 0;width: 150px">
								<a href="http://auth.gulimall.com/login.html" th:if="${session.loginUser == null}" class="aa">你好,请登录</a>
								<a th:else class="aa">你好,[[${session.loginUser.nickname}]]</a>
							</li>
							<li th:if="${session.loginUser == null}"><a href="http://auth.gulimall.com/reg.html" style="color: red;">免费注册</a> |</li>

 gulimall-search服务页面显示用户名,需要先搭建好SpringSession环境

导入依赖

         <!--整合SpringSession完成session共享问题-->
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

修改配置

#配置redis
spring.redis.host=192.168.119.127
spring.redis.port=6379
#session存储格式
spring.session.store-type=redis

加注解

添加SpringSession配置类

@Configuration
public class GulimallSessionConfig {

    @Bean
    public CookieSerializer cookieSerializer(){
        DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
        cookieSerializer.setDomainName("gulimall.com");
        cookieSerializer.setCookieName("GULISESSION");
        return cookieSerializer;
    }

    @Bean
    public RedisSerializer<Object> springSessionDefaultRedisSerializer(){
        return new GenericJackson2JsonRedisSerializer();
    }
}

 修改list.html页面

            <li style="width: 150px">
                <a href="http://auth.gulimall.com/login.html" class="li_2"
                   th:if="${session.loginUser == null}">你好,请登录</a>
                <a th:else class="aa">你好,[[${session.loginUser.nickname}]]</a>
            </li>
            <li>
                <a href="http://auth.gulimall.com/reg.html" th:if="${session.loginUser == null}">免费注册</a>
            </li>

7、SSO(单点登陆)

Single Sign On 一处登陆、处处可用

前置概念

1 )、单点登录业务介绍
早期单一服务器,用户认证。

 

 缺点:单点性能压力,无法扩展

分布式,SSO(single sign on)模式

多系统 

解决 :
  • 用户身份信息独立管理,更好的分布式管理。
  • 可以自己扩展安全策略
  • 跨域不是问题
缺点:
  • 认证服务器访问压力较大。

 gitee参考项目:https://gitee.com/xuxueli0323/xxl-sso

 xxl-sso流程:

  • /xxl-sso-server 登录服务器 8080 ssoserver.com
  • /xxl-sso-web-sample-springboot 项目1 8081 client1.com
  • /xxl-sso-web-sample-springboot 项目2 8082 client2.com
#----------sso----------
127.0.0.1 ssoserver.com
127.0.0.1 client1.com
127.0.0.1 client2.com

 核心:三个系统即使域名不一样,想办法给三个系统同步同一个用户的票据;

1)、中央认证服务器;ssoserver.com

2)、其他系统,想要登录去ssoserver.com登录,登录成功跳转回来

3)、只要有一个登录,其他都不用登录

4)、全系统统一一个sso-sessionid;所有系统可能域名都不相同

九、商城业务-购物车


1、环境搭建

1)、创建gulimall-cart模块

2)、修改C:\Windows\System32\drivers\etc\hosts里的域名

#----------gulimall----------
192.168.119.127 gulimall.com
192.168.119.127 search.gulimall.com
192.168.119.127 item.gulimall.com
192.168.119.127 auth.gulimall.com
192.168.119.127 cart.gulimall.com

3)、在/mydata/nginx/html/static/目录创建cart文件夹,将所有的静态资源全部都传到虚拟机/mydata/nginx/html/static/cart目录下

将两个静态页面加入gulimall-cart服务里

4)、改静态资源的访问路径,只举一个例子

5)、添加依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.4.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.atguigu</groupId>
    <artifactId>gulimall-cart</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>gulimall-cart</name>
    <description>购物车</description>

    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Hoxton.SR8</spring-cloud.version>
    </properties>

    <dependencies>

        <dependency>
            <groupId>com.auguigu.gulimall</groupId>
            <artifactId>gulimall-commom</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

    <repositories>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
        </repository>
    </repositories>

</project>

6)、添加配置

server.port=40000
spring.application.name=gulimall-cart
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848

7)、为主启动类添加注解

@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class GulimallCartApplication {

    public static void main(String[] args) {
        SpringApplication.run(GulimallCartApplication.class, args);
    }

}

8)、修改网关,给购物车配置路由

        - id: gulimall_cart_route
          uri: lb://gulimall-cart
          predicates:
            - Host=cart.gulimall.com

测试

先把cartList.html改成index.html启动项目

http://cart.gulimall.com/

 前端页面修改跳转到首页的链接

					<ul class="header-left">
						<li>
							<a href="http://gulimall.com">首页</a>
						</li>
					</ul>
					<div class="one_top_left">
						<a href="http://gulimall.com" class="one_left_logo"><img src="/static/cart/img/logo1.jpg"></a>
						<a href="#" class="one_left_link">购物车</a>
					</div>

2、数据模型分析

2.1、需求描述

  • 用户可以在登录状态下将商品添加到购物车【用户购物车/在线购物车】
    • 放入数据库
    • mongodb
    • 放入 redis(采用)   
      • 登录以后,会将临时购物车的数据全部合并过来,并清空临时购物车;
  • 用户可以在未登录状态下将商品添加到购物车【游客购物车/离线购物车/临时购物车】
    • 放入 localstorage(客户端存储,后台不存)
    • cookie
    • WebSQL
    • 放入 redis(采用)
      • 浏览器即使关闭,下次进入,临时购物车数据都在
  • 用户可以使用购物车一起结算下单
  • 给购物车添加商品
  • 用户可以查询自己的购物车
  • 用户可以在购物车中修改购买商品的数量
  • 用户可以在购物车中删除商品
  • 选中不选中商品
  • 在购物车中展示商品优惠信息
  • - 提示购物车商品价格变化

2.2、数据存储

购物车是一个读多写多的场景,因此放入数据库并不合适,但购物车又是需要持久化,因此这里我们选用redis存储购物车数据。 

2.3、数据结构

购物项

 因此每一个购物项信息,都是一个对象,基本字段包括: 

{
    skuId: 2131241, 
    check: true, 
    title: "Apple iphone.....", 
    defaultImage: "...", 
    price: 4999, 
    count: 1, 
    totalPrice: 4999, 
    skuSaleVO: {...}
}

另外,购物车中不止一条数据,因此最终会是对象的数组。即: 

[
    {...},{...},{...}
]

 Redis 有 5 种不同数据结构,这里选择哪一种比较合适呢?Map<String, List<String>>

  • 首先不同用户应该有独立的购物车,因此购物车应该以用户的作为 key 来存储,Value 用户的所有购物车信息。这样看来基本的`k-v`结构就可以了。
  • 但是,我们对购物车中的商品进行增、删、改操作,基本都需要根据商品 id 进行判断,为了方便后期处理,我们的购物车也应该是`k-v`结构,key 是商品 idvalue 才是这个商品的购物车信息。

综上所述,我们的购物车结构是一个双层 Map Map<String,Map<String,String>>
  • 第一层 MapKey 是用户 id
  • 第二层 MapKey 是购物车中商品 id,值是购物项数据
一个购物车是由各个购物项组成的,但是我们用 List进行存储并不合适,因为使用 List查找某个购物项时需要挨个遍历每个购物项,会造成大量时间损耗,为保证查找速度,我们使用 hash进行存储。

2.4、流程

参照京东 

user-key 是随机生成的 id,不管有没有登录都会有这个 cookie 信息。

两个功能:新增商品到购物车、查询购物车。
新增商品:判断是否登录
  • 是:则添加商品到后台 Redis 中,把 user 的唯一标识符作为 key
  • 否:则添加商品到后台 redis 中,使用随机生成的 user-key 作为 key
查询购物车列表:判断是否登录
  • 否:直接根据 user-key 查询 redis 中数据并展示
  • 是:已登录,则需要先根据 user-key 查询 redis 是否有数据。
    • 有:需要提交到后台添加到 redis,合并数据,而后查询。
    • 否:直接去后台查询 redis,而后返回。

3、VO编写

因此每一个购物项信息,都是一个对象,基本字段包括:

 ​添加“com.atguigu.gulimall.cart.vo.CartItem”类,代码如下:

package com.atguigu.gulimall.cart.vo;

import java.math.BigDecimal;
import java.util.List;

/**
 * @Description: 购物项
 * @Date: 2024/5/19 19:04
 * @Version 1.0
 */
public class CartItem {
    /**
     * 商品id
     */
    private Long skuId;

    /**
     * 是否选中
     */
    private Boolean check = true;

    /**
     * 标题
     */
    private String title;

    /**
     * 图片
     */
    private String image;

    /**
     * 商品套餐属性
     */
    private List<String> skuAttr;

    /**
     * 价格
     */
    private BigDecimal price;

    /**
     * 数量
     */
    private Integer count;

    /**
     * 总价
     */
    private BigDecimal totalPrice;

    public Long getSkuId() {
        return skuId;
    }

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

    public Boolean getCheck() {
        return check;
    }

    public void setCheck(Boolean check) {
        this.check = check;
    }

    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 List<String> getSkuAttr() {
        return skuAttr;
    }

    public void setSkuAttr(List<String> skuAttr) {
        this.skuAttr = skuAttr;
    }

    public BigDecimal getPrice() {
        return price;
    }

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

    public Integer getCount() {
        return count;
    }

    public void setCount(Integer count) {
        this.count = count;
    }

    /**
     * 计算当前购物项总价
     *
     * @return
     */
    public BigDecimal getTotalPrice() {
        return this.price.multiply(new BigDecimal("" + this.count));
    }

    public void setTotalPrice(BigDecimal totalPrice) {
        this.totalPrice = totalPrice;
    }
}

添加“com.atguigu.gulimall.cart.vo.Cart”类,代码如下:

package com.atguigu.gulimall.cart.vo;

import java.math.BigDecimal;
import java.util.List;

/**
 * @Description: 整体购物车  需要计算的属性,必须重写他的get方法,保证每次获取属性都会进行计算
 * @Date: 2024/5/19 19:07
 * @Version 1.0
 */
public class Cart {
    /**
     * 购物车子项信息
     */
    List<CartItem> items;

    /**
     * 商品数量
     */
    private Integer countNum;

    /**
     * 商品类型数量
     */
    private Integer countType;

    /**
     * 商品总价
     */
    private BigDecimal totalAmount;

    /**
     * 减免价格
     */
    private BigDecimal reduce = new BigDecimal("0.00");

    public List<CartItem> getItems() {
        return items;
    }

    public void setItems(List<CartItem> items) {
        this.items = items;
    }

    public Integer getCountNum() {
        int count = 0;
        if (items != null && items.size() > 0) {
            for (CartItem item : items) {
                count += item.getCount();
            }
        }
        return count;
    }

    public Integer getCountType() {
        int count = 0;
        if (items != null && items.size() > 0) {
            for (CartItem item : items) {
                count += 1;
            }
        }
        return count;
    }

    public BigDecimal getTotalAmount() {
        BigDecimal amount = new BigDecimal("0");
        // 1、计算购物项总价
        if (items != null && items.size() > 0) {
            for (CartItem item : items) {
                if (item.getCheck()) {
                    BigDecimal totalPrice = item.getTotalPrice();
                    amount = amount.add(totalPrice);
                }
            }
        }
        // 2、减去优惠总价
        BigDecimal subtract = amount.subtract(getReduce());
        return subtract;
    }


    public BigDecimal getReduce() {
        return reduce;
    }

    public void setReduce(BigDecimal reduce) {
        this.reduce = reduce;
    }
}

2、导入redis和SpringSession的依赖

        <!--整合SpringSession完成session共享问题-->
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>

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

3、配置redis和SpringSession存储类型 

#配置redis
spring.redis.host=192.168.119.127
spring.redis.port=6379

spring.session.store-type=redis

 4、添加SpringSession配置类

添加“com.atguigu.gulimall.cart.config.GulimallSessionConfig”类,代码如下:

@EnableRedisHttpSession  //自动开启RedisHttpSession
@Configuration
public class GulimallSessionConfig {

    @Bean
    public CookieSerializer cookieSerializer(){
        DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
        cookieSerializer.setDomainName("gulimall.com");
        cookieSerializer.setCookieName("GULISESSION");
        return cookieSerializer;
    }

    @Bean
    public RedisSerializer<Object> springSessionDefaultRedisSerializer(){
        return new GenericJackson2JsonRedisSerializer();
    }
}

4、ThreadLocal用户身份鉴别

1)、ThreadLocal同一个线程共享数据

 2)、核心原理:

Map<Thread,Object> threadLocal

3)、用户身份鉴别方式

参考京东,在点击购物车时,会为临时用户生成一个nameuser-keycookie临时标识,过期时间为一个月,如果手动清除user-key,那么临时购物车的购物项也被清除,所以user-key是用来标识和存储临时购物车数据的。

4)、使用ThreadLocal进行用户身份鉴别信息传递

  • 在调用购物车的接口前,先通过session信息判断是否登录,并分别进行用户身份信息的封装,并把user-key放在cookie中
  • 这个功能使用拦截器进行完成

添加“com.atguigu.gulimall.cart.vo.UserInfoTo”类,代码如下:

@ToString
@Data
public class UserInfoTo {

    private Long userId;

    private String userKey; //一定封装

    private boolean tempUser = false;  //判断是否有临时用户
}

添加“com.atguigu.gulimall.cart.interceptor.CartInterceptor”类,代码如下:

package com.atguigu.gulimall.cart.interceptor;

import com.atguigu.common.constant.AuthServerConstant;
import com.atguigu.common.constant.CartConstant;
import com.atguigu.common.vo.MemberResponseVO;
import com.atguigu.gulimall.cart.vo.UserInfoTo;
import org.apache.commons.lang.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.UUID;

/**
 * @Description: 在执行目标方法之前,判断用户的登录状态。并封装传递给目标请求
 * @Date: 2024/5/19 19:39
 * @Version 1.0
 */
public class CartInterceptor implements HandlerInterceptor {
    // ThreadLocal同一个线程共享数据
    public static ThreadLocal<UserInfoTo> threadLocal = new ThreadLocal<>();

    /**
     * 在目标方法执行之前拦截
     *
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        UserInfoTo userInfoTo = new UserInfoTo();
        HttpSession session = request.getSession();
        MemberResponseVO member = (MemberResponseVO) session.getAttribute(AuthServerConstant.LOGIN_USER);
        // 1、用户登录,封装用户id
        if (member != null) {
            userInfoTo.setUserId(member.getId());

        }
        // 2、如果有临时用户,封装临时用户
        Cookie[] cookies = request.getCookies();
        if (cookies != null && cookies.length > 0) {
            for (Cookie cookie : cookies) {
                String name = cookie.getName();
                if (name.equals(CartConstant.TEMP_USER_COOKIE_NAME)) {
                    userInfoTo.setUserKey(cookie.getValue());
                    userInfoTo.setTempUser(true);
                }
            }
        }

        // 3、如果没有临时用户,一定保存一个临时用户
        if (StringUtils.isEmpty(userInfoTo.getUserKey())) {
            String uuid = UUID.randomUUID().toString();
            userInfoTo.setUserKey(uuid);
        }
        // 目标方法执行之前,将用户信息保存到ThreadLocal
        threadLocal.set(userInfoTo);
        return true;
    }

    /**
     * 业务执行之后 分配临时用户,让浏览器保存
     *
     * @param request
     * @param response
     * @param handler
     * @param modelAndView
     * @throws Exception
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
        UserInfoTo userInfoTo = threadLocal.get();
        // 如果没有临时用户,第一次访问购物车就添加临时用户
        if (!userInfoTo.isTempUser()) {
            // 持续的延长用户的过期时间
            Cookie cookie = new Cookie(CartConstant.TEMP_USER_COOKIE_NAME, userInfoTo.getUserKey());
            cookie.setDomain("gulimall.com");
            cookie.setMaxAge(CartConstant.TEMP_USER_COOKIE_TIMEOUT);
            response.addCookie(cookie);
        }
    }
}
添加拦截器的配置,不能只把拦截器加入容器中,不然拦截器不生效的

添加“com.atguigu.gulimall.cart.config.GulimallWebConfig”类,代码如下:

@Configuration
public class GulimallWebConfig implements WebMvcConfigurer {
    /**
     * 添加拦截器的配置,不能只把拦截器加入容器中,不然拦截器不生效的
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new CartInterceptor()).addPathPatterns("/**");
    }
}

修改index.html名字为cartList.html 

添加“com.atguigu.gulimall.cart.controller.CartController”类,代码如下:

@Controller
public class CartController {

    /**
     * 浏览器有一个cookie;user-key:标识用户身份,一个月后过期
     * 如果第一次使用jd购物车功能,都会给一个临时的用户身份
     * 浏览器保存,每次访问都会带上有这个cookies
     *
     * 登录  session有
     * 没登录,按照cookie里面带来的user-key来做
     * 第一次,如果没有临时用户,帮忙创建一个临时用户
     */
    @GetMapping("/cart.html")
    public String cartListPage(){

        //快速得到用户信息,id,user-key
        UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
        System.out.println(userInfoTo);
        return "cartList";
    }
}

5、页面调整

1、点击商品详情里面的“加入购物车”按钮,可以跳转到购物车页面

修改gulimall-product模块下的item.html页面代码:

                    <div class="box-btns-two">
                        <a href="http://cart.gulimall.com/addToCart">加入购物车</a>
                    </div>

修改"com.atguigu.gulimall.cart.controller.CartController"类,代码如下:

    /**
     * 添加商品到购物车
     */    
    @GetMapping("/addToCart")
    public String addToCart() {
        return "sucess";
    }

2、点击首页的“我的购物车”按钮,可以跳转到购物车列表

修改gulimall-product模块的index.html页面代码:

        <div class="header_gw">
          <img src="/static/index/img/img_15.png" />
          <span><a href="http://cart.gulimall.com/addToCart">我的购物车</a></span>
          <span>0</span>
        </div>

3、实现购物车列表的首页链接跳转

修改gulimall-cart模块的cartList.html页面代码:

					<ul class="header-left">
						<li>
							<a href="http://gulimall.com">首页</a>
						</li>
					</ul>
					<div class="one_top_left">
						<a href="http://gulimall.com" class="one_left_logo"><img src="/static/cart/img/logo1.jpg"></a>
						<a href="#" class="one_left_link">购物车</a>
					</div>

4、实现加入购物车成功页的首页链接跳转

修改gulimall-cart模块的success.html页面代码: 

        <ul class="hd_wrap_left">
            <li class="hd_home"><i class="glyphicon glyphicon-home"></i>
                <a href="http://gulimall.com">谷粒商城首页</a>
            </li>
        </ul>
        <div class="nav_top">
            <div class="nav_top_one">
                <a href="http://gulimall.com"><img src="/static/cart/img/logo1.jpg" style="height: 60px;width:180px;"/></a>
            </div>
            <div class="nav_top_two"><input type="text"/>
                <button>搜索</button>
            </div>
        </div>

 5、实现加入购物车成功页的“查看商品详情”和“去购物车结算”链接跳转

修改gulimall-cart模块的success.html页面代码:

                        <div class="bg_shop">
                            <a class="btn-tobback" href="'http://item.gulimall.com/10.html'">查看商品详情</a>
                            <a class="btn-addtocart" href="http://cart.gulimall.com/cart.html"
                               id="GotoShoppingCart"><b></b>去购物车结算</a>
                        </div>

6、添加商品到购物车

 在gulimall-product模块,修改“加入购物车”按钮

item.html

    $("#addToCart").click(function () {
        var num = $("#numInput").val();
        var skuId = $(this).attr("skuId");
        location.href = "http://cart.gulimall.com/addToCart?skuId=" + skuId + "&num=" + num;

        return false;
    });

修改“com.atguigu.gulimall.cart.controller.CartController”类,代码如下:

    /**
     * 添加商品到购物车
     */
    @GetMapping("/addToCart")
    public String addToCart(@RequestParam("skuId") Long skuId,
                            @RequestParam("num") Integer num,
                            Model model) {
        CartItem cartItem = cartService.addToCart(skuId,num);
        model.addAttribute("item", cartItem);
        return "success";
    }

修改“com.atguigu.gulimall.cart.service.CartService”类,代码如下:

public interface CartService {
    CartItem addToCart(Long skuId, Integer num) throws ExecutionException, InterruptedException;
}

修改gulimall-cart模块的加入购物车成功页success.html的代码,实现动态显示

            <div class="m succeed-box">
                <div th:if="${item != null}" class="mc success-cont">
                    <div class="success-lcol">
                        <div class="success-top"><b class="succ-icon"></b>
                            <h3 class="ftx-02">商品已成功加入购物车</h3></div>
                        <div class="p-item">
                            <div class="p-img">
                                <a href="/javascript:;" target="_blank"><img style="height: 60px;width:60px;"
                                                                             th:src="${item.image}"
                                                                             src="/img/shop1.jpg"></a>
                            </div>
                            <div class="p-info">
                                <div class="p-name">
                                    <a th:href="'http://item.gulimall.com/'+${item.skuId}+'.html'"
                                       th:text="${item.title}">TCL 55A950C 55英寸32核人工智能 HDR曲面超薄4K电视金属机身(枪色)</a>
                                </div>
                                <div class="p-extra"><span class="txt" th:text="'数量:'+${item.count}">  数量:1</span></div>
                            </div>
                            <div class="clr"></div>
                        </div>
                    </div>
                    <div class="success-btns success-btns-new">
                        <div class="success-ad">
                            <a href="/#none"></a>
                        </div>
                        <div class="clr"></div>
                        <div class="bg_shop">
                            <a class="btn-tobback" th:href="'http://item.gulimall.com/' + ${item.skuId} + '.html'">查看商品详情</a>
                            <a class="btn-addtocart" href="http://cart.gulimall.com/cart.html"
                               id="GotoShoppingCart"><b></b>去购物车结算</a>
                        </div>
                    </div>
                </div>
                <div th:if="${item == null}" class="mc success-cont">
                    <h2>购物车中心无商品</h2>
                    <a href="http://gulimall.com">去购物</a>
                </div>
            </div>

业务逻辑

  • 若当前商品已经存在购物车,只需增添数量
  • 否则需要查询商品购物项所需信息,并添加新商品至购物车

修改“com.atguigu.gulimall.cart.service.impl.CartServiceImpl”类,代码如下:

@Slf4j
@Service
public class CartServiceImpl implements CartService {
    private final String CART_PREFIX = "gulimall:cart";
    @Autowired
    StringRedisTemplate stringRedisTemplate;

    @Autowired
    ProductFeignService productFeignService;

    @Autowired
    ThreadPoolExecutor executor;

    @Override
    public CartItem addToCart(Long skuId, Integer num) throws ExecutionException, InterruptedException {
        // 获取我们要操作的购物车,临时购物车、用户购物车
        BoundHashOperations<String, Object, Object> cartOps = getCartOps();

        String res = (String) cartOps.get(skuId.toString());
        // 1、添加新商品到购物车(购物车无此商品)
        if (StringUtils.isEmpty(res)) {
            CartItem cartItem = new CartItem();
            /**
             * 异步查询
             */
            CompletableFuture<Void> getSkuInfo = CompletableFuture.runAsync(() -> {
                // 1.1、远程查询要添加的商品信息
                R skuInfo = productFeignService.getSkuInfo(skuId);
                SkuInfoVo data = skuInfo.getData("skuInfo", new TypeReference<SkuInfoVo>() {
                });
                cartItem.setCheck(true);
                cartItem.setCount(1);
                cartItem.setImage(data.getSkuDefaultImg());
                cartItem.setTitle(data.getSkuTitle());
                cartItem.setSkuId(skuId);
                cartItem.setPrice(data.getPrice());
            }, executor);
            CompletableFuture<Void> getSkuSaleAttrValues = CompletableFuture.runAsync(() -> {
                // 1.2、远程查询sku的组合信息
                List<String> values = productFeignService.getSkuSaleAttrValues(skuId);
                cartItem.setSkuAttr(values);
            }, executor);
            CompletableFuture.allOf(getSkuInfo, getSkuSaleAttrValues).get();
            String jsonString = JSON.toJSONString(cartItem);
            cartOps.put(skuId.toString(), jsonString);
            return cartItem;
        } else {
            // 2、购物车有此商品,将数据取出修改数量即可
            CartItem cartItem = JSON.parseObject(res, CartItem.class);
            cartItem.setCount(cartItem.getCount() + num);
            cartOps.put(skuId.toString(), JSON.toJSONString(cartItem));
            return cartItem;
        }
    }
    
    /**
     * 获取我们要操作的购物车,临时购物车、用户购物车
     *
     * @return
     */
    private BoundHashOperations<String, Object, Object> getCartOps() {
        // 得到用户信息 账号用户 、临时用户
        UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
        // 1、userInfoTo.getUserId()不为空表示账号用户,反之临时用户  然后决定用临时购物车还是用户购物车
        // 放入缓存的key
        String cartKey = "";
        if (userInfoTo.getUserId() != null) {
            cartKey = CART_PREFIX + userInfoTo.getUserId();
        } else {
            cartKey = CART_PREFIX + userInfoTo.getUserKey();
        }
        BoundHashOperations<String, Object, Object> operations = stringRedisTemplate.boundHashOps(cartKey);
        return operations;
    }
}

0、配置线程池

application.properties添加线程池配置

gulimall.thread.core= 20
gulimall.thread.max-size= 200
gulimall.thread.keep-alive-time= 10

添加“com.atguigu.gulimall.cart.config.ThreadPoolConfigProperties”类,代码如下:

@ConfigurationProperties(prefix = "gulimall.thread")
@Component
@Data
public class ThreadPoolConfigProperties {
    private Integer core;
    private Integer maxSize;
    private Integer keepAliveTime;
}

添加“com.atguigu.gulimall.cart.config.MyThreadConfig”类,代码如下:

//如果ThreadPoolConfigProperties.class类没有加上@Component注解,那么我们在需要的配置类里开启属性配置的类加到容器中
//@EnableConfigurationProperties(ThreadPoolConfigProperties.class)
@Configuration
public class MyThreadConfig {
    @Bean
    public ThreadPoolExecutor threadPoolExecutor(ThreadPoolConfigProperties pool){
        return new ThreadPoolExecutor(pool.getCore(),
                pool.getMaxSize(),
                pool.getKeepAliveTime(),
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(100000),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());
    }
}

1、远程查询要添加的商品信息

添加“com.atguigu.gulimall.cart.vo.SkuInfoVo”类,代码如下

@Data
public class SkuInfoVo {
    private Long skuId;
    /**
     * spuId
     */
    private Long spuId;
    /**
     * sku名称
     */
    private String skuName;
    /**
     * sku介绍描述
     */
    private String skuDesc;
    /**
     * 所属分类id
     */
    private Long catalogId;
    /**
     * 品牌id
     */
    private Long brandId;
    /**
     * 默认图片
     */
    private String skuDefaultImg;
    /**
     * 标题
     */
    private String skuTitle;
    /**
     * 副标题
     */
    private String skuSubtitle;
    /**
     * 价格
     */
    private BigDecimal price;
    /**
     * 销量
     */
    private Long saleCount;
}

添加“com.atguigu.gulimall.cart.feign.ProductFeignService”类,代码如下:

@FeignClient("gulimall-product")
public interface ProductFeignService {

    @RequestMapping("/product/skuinfo/info/{skuId}")
    R getSkuInfo(@PathVariable("skuId") Long skuId);

    @GetMapping("product/skusaleattrvalue/stringlist/{skuId}")
    public List<String> getSkuSaleAttrValues(@PathVariable("skuId") Long skuId);
}

2、远程查询sku的组合信息
修改“com.atguigu.gulimall.product.app.SkuSaleAttrValueController”类,代码如下:

    @GetMapping("stringlist/{skuId}")
    public List<String> getSkuSaleAttrValues(@PathVariable("skuId") Long skuId){
        return skuSaleAttrValueService.getSkuSaleAttrValuesAsStringList(skuId);
    }

修改“com.atguigu.gulimall.product.service.SkuSaleAttrValueService”,代码如下:

 List<String> getSkuSaleAttrValuesAsStringList(Long skuId);

修改“com.atguigu.gulimall.product.service.impl.SkuSaleAttrValueServiceImpl”类,代码如下:

     @Override
    public List<String> getSkuSaleAttrValuesAsStringList(Long skuId) {
        return this.baseMapper.getSkuSaleAttrValuesAsStringList(skuId);
    }

修改“com.atguigu.gulimall.product.dao.SkuSaleAttrValueDao”类,代码如下:

List<String> getSkuSaleAttrValuesAsStringList(@Param("skuId") Long skuId);
    <select id="getSkuSaleAttrValuesAsStringList" resultType="java.lang.String">
        select CONCAT(attr_name,":",attr_value) from pms_sku_sale_attr_value
        where sku_id = #{skuId}
    </select>

我们重启服务,准备测试,点击搜索商品页面发现,报错

报错内容如下

Caused by: org.attoparser.ParseException: Exception evaluating SpringEL expression: "session.loginUser.nickname" (template: "list" - line 69, col 44)
	at org.attoparser.MarkupParser.parseDocument(MarkupParser.java:393)
	at org.attoparser.MarkupParser.parse(MarkupParser.java:257)
	at org.thymeleaf.templateparser.markup.AbstractMarkupTemplateParser.parse(AbstractMarkupTemplateParser.java:230)
	... 52 more

通过查询资料了解到Thymeleaf并没有提供th:else语法,所以我们修改相关的代码

修改gulimall-search模块的list.html页面的代码

修改gulimall-search模块的list.html页面的代码

bug:不断刷新页面会一直增加数量

不断刷新页面会一直增加数量,所以我们修改逻辑在controller的addToCart方法里添加商品,商品添加完跳转到成功页面我们改为改成重定向另一个方法,专门查询数据跳转到成功页面

修改”com.atguigu.gulimall.cart.controller.CartController“类,代码如下: 

    /**
     * 添加商品到购物车
     * <p>
     * RedirectAttributes attributes
     * attributes.addFlashAttribute();将数据放在session里面可以在页面取出,但只能取一次
     * attributes.addAttribute("skuId",skuId); 将数据放在url后面
     *
     * @return
     */
    @GetMapping("/addToCart")
    public String addToCart(@RequestParam("skuId") Long skuId,
                            @RequestParam("num") Integer num,
                            RedirectAttributes attributes) throws ExecutionException, InterruptedException {
        cartService.addToCart(skuId, num);
        attributes.addAttribute("skuId", skuId);

        return "redirect:http://cart.gulimall.com/addToCartSuccess.html";
    }

    /**
     * 跳转到成功页
     *
     * @param skuId
     * @param model
     * @return
     */
    @GetMapping("/addToCartSuccess.html")
    public String addToCartSuccessPage(@RequestParam("skuId") Long skuId, Model model) {
        CartItem cartItem = cartService.getCartItem(skuId);
        model.addAttribute("item", cartItem);
        return "success";
    }

修改”com.atguigu.gulimall.cart.service.CartService“类,代码如下: 

    /**
     * 获取购物车中某个购物项
     *
     * @param skuId
     * @return
     */
    CartItem getCartItem(Long skuId);

 修改”com.atguigu.gulimall.cart.service.impl.CartServiceImpl“类,代码如下: 

    @Override
    public CartItem getCartItem(Long skuId) {
        BoundHashOperations<String, Object, Object> cartOps = getCartOps();
        String s = (String) cartOps.get(skuId.toString());
        CartItem cartItem = JSON.parseObject(s, CartItem.class);
        return cartItem;
    }

7、获取&合并购物车

  • 若用户未登录,则直接使用user-key获取购物车数据
  • 否则使用userId获取购物车数据,并将user-key对应临时购物车数据与用户购物车数据合并,并删除临时购物车

 修改“com.atguigu.gulimall.cart.controller.CartController”类,代码如下

    @GetMapping("/cart.html")
    public String cartListPage(Model model) throws ExecutionException, InterruptedException {

        // 快速得到用户信息,id,user-key
//        UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
        Cart cart = cartService.getCart();
        model.addAttribute("cart", cart);
        return "cartList";
    }

修改“com.atguigu.gulimall.cart.service.CartService”类,代码如下:

    /**
     * 获取整个购物车
     * 
     * @return
     */
    Cart getCart() throws ExecutionException, InterruptedException;

 修改“com.atguigu.gulimall.cart.service.impl.CartServiceImpl”类,代码如下:

    @Override
    public Cart getCart() throws ExecutionException, InterruptedException {
        Cart cart = new Cart();
        // 1、登录
        UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
        if (userInfoTo.getUserId() != null) {
            String cartKey = CART_PREFIX + userInfoTo.getUserId();
            // 1.1、如果临时购物车的数据还没有合并【合并购物车】
            String tempCartKey = CART_PREFIX + userInfoTo.getUserKey();
            List<CartItem> tempsCartItems = getCartItems(tempCartKey);
            if (tempsCartItems != null) {
                // 临时购物车有数据,需要合并
                for (CartItem item : tempsCartItems) {
                    addToCart(item.getSkuId(), item.getCount());
                }
                // 清除临时购物车的数据
                clearCart(tempCartKey);
            }
            // 1.2、获取登录后的购物车数据【包含合并过来的临时购物车的数据,和登录后的购物车数据】
            List<CartItem> cartItems = getCartItems(cartKey);
            cart.setItems(cartItems);
        } else {
            // 2、没登录
            String cartKey = CART_PREFIX + userInfoTo.getUserKey();
            // 获取临时购物车的所有购物项
            List<CartItem> cartItems = getCartItems(cartKey);
            cart.setItems(cartItems);
        }
        return cart;
    }

    /**
     * 获取购物项
     * 
     * @param cartKey
     * @return
     */
    private List<CartItem> getCartItems(String cartKey){
        BoundHashOperations<String, Object, Object> operations = stringRedisTemplate.boundHashOps(cartKey);
        List<Object> values = operations.values();
        if (values != null && values.size() > 0){
            List<CartItem> collect = values.stream().map(obj -> {
                String str = (String) obj;
                CartItem cartItem = JSON.parseObject(str, CartItem.class);
                return cartItem;
            }).collect(Collectors.toList());
            return collect;
        }
        return null;
    }

清空购物车数据 

 修改“com.atguigu.gulimall.cart.service.CartService”类,代码如下:

    /**
     * 清空购物车数据
     * 
     * @param cartKey
     */
    void clearCart(String cartKey);

  修改“com.atguigu.gulimall.cart.service.CartService”类,代码如下:

    @Override
    public void clearCart(String cartKey) {
        stringRedisTemplate.delete(cartKey);
    }

 cartList.html

		<div class="one_search">
			<div class="one_sousuo">
				<div class="one_search_top">
					<div class="one_top_left">
						<a href="http://gulimall.com" class="one_left_logo"><img src="/static/cart/img/logo1.jpg"></a>
						<a href="#" class="one_left_link">购物车</a>
					</div>
					<div class="one_top_right">
						<input type="text" class="one_right_txt" placeholder="" onfocus="this.placeholder=''" onblur="this.placeholder='' ">
						<input type="button" value="搜索" class="one_right_btn">
					</div>
				</div>
				<div class="one_search_load" th:if="${session.loginUser == null}">
					<img src="/static/cart/img/shop_07.jpg" class="one_load_wraing">
					<span>您还没有登录!登录后购物车的商品将保存到您账号中</span>
					<a href="#"><input type="button" onclick="login()" value="立即登录" class="one_load_btn"></a>
				</div>
			</div>
		</div>
		
		<div class="One_BdyShop">
			<div class="OneBdy_box">
				<div class="One_tabTop">
					<div class="One_Topleft">
						<span>全部商品 </span>

					</div>

				</div>
				<div class="One_ShopTop">
					<ul>
						<li><input type="checkbox" class="allCheck">全选</li>
						<li>商品</li>
						<li>单价</li>
						<li>数量</li>
						<li>小计</li>
						<li>操作</li>
					</ul>
				</div>

				<div class="One_ShopCon">
					<h1 th:if="${cart.items == null}">
						购物车还没有商品,<a href="http://gulimall.com">去购物</a>
					</h1>
					<ul  th:if="${cart.items != null}">
						<li th:each="item:${cart.items}">
							<div>
							</div>
							<div>
								<ol>
									<li><input type="checkbox" class="check" th:checked="${item.check}"></li>
									<li>
										<dt><img th:src="${item.image}" alt=""></dt>
										<dd style="width: 300px">
											<p>
												<span th:text="${item.title}">TCL 55A950C 55英寸32核</span>
												<br>
												<span th:each="attr:${item.skuAttr}" th:text="${attr}">尺码: 55时 超薄曲面 人工智能</span>
											</p>
										</dd>
									</li>
									<li>
										<p class="dj" th:text="'¥'+${#numbers.formatDecimal(item.price,3,2)}">4599.00</p>
									</li>
									<li>
										<p>
											<span>-</span>
											<span th:text="${item.count}">5</span>
											<span>+</span>
										</p>
									</li>
									<li style="font-weight:bold"><p class="zj">¥[[${#numbers.formatDecimal(item.totalPrice,3,2)}]]</p></li>
									<li>
										<p>删除</p>
									</li>
								</ol>
							</div>
						</li>
					</ul>
				</div>
				<div class="One_ShopFootBuy fix1">
					<div>
						<ul>
							<li><input type="checkbox" class="allCheck"><span>全选</span></li>
							<li>删除选中的商品</li>
							<li>移到我的关注</li>
							<li>清除下柜商品</li>
						</ul>
					</div>
					<div>
						<font style="color:#e64346;font-weight:bold;" class="sumNum"> </font>&nbsp;
						<ul>
							<li><img src="img/buyNumleft.png" alt=""></li>
							<li><img src="img/buyNumright.png" alt=""></li>
						</ul>
					</div>
					<div>
						<ol>
							<li>总价:<span style="color:#e64346;font-weight:bold;font-size:16px;" class="fnt">¥[[${#numbers.formatDecimal(cart.totalAmount,3,2)}]]</span></li>
							<li>优惠:<span style="color:#e64346;font-weight:bold;font-size:16px;" class="fnt">[[${#numbers.formatDecimal(cart.reduce,1,2)}]]</span></li>
						</ol>
					</div>
					<div>
						<button onclick="toTrade()" type="button">去结算</button>
					</div>
				</div>
			</div>
		</div>

		<div class="One_isDel">
			<p>
				<span>删除</span><span><img src="img/错误.png" alt=""></span>
			</p>
			<div>
				<dl>
					<dt><img src="img/感叹三角形 (2).png" alt=""></dt>
					<dd>
						<li>删除商品?</li>
						<li>您可以选择移到关注,或删除商品。</li>
					</dd>
				</dl>
			</div>
			<div>
				<button type="button">删除</button>
			</div>
		</div>

		<div class="One_moveGzIfNull">
			<p>
				<span>删除</span><span><img src="img/错误.png" alt=""></span>
			</p>
			<dl>
				<dt><img src="img/感叹三角形 (2).png" alt=""></dt>
				<dd>请至少选中一件商品!</dd>
			</dl>
		</div>
		function login() {
			window.location.href = "http://auth.gulimall.com/login.html";
		}

8、选中购物车项

  修改cartList.html

<li><input type="checkbox" th:attr="skuId=${item.skuId}" class="itemCheck" th:checked="${item.check}"></li>
		$(".itemCheck").click(function(){
			var skuId = $(this).attr("skuId");
			var check = $(this).prop("checked");
			location.href = "http://cart.gulimall.com/checkItem?skuId=" + skuId + "&check=" + (check? 1 : 0);
		})

修改“com.atguigu.gulimall.cart.controller.CartController” 类,代码如下:

@GetMapping("/checkItem")
    public String checkItem(@RequestParam("skuId") Long skuId, @RequestParam("check") Integer check){
        cartService.checkItem(skuId,check);
        return "redirect:http://cart.gulimall.com/cart.html";
    }

修改“com.atguigu.gulimall.cart.service.CartService”类,代码如下:

    /**
     * 勾选购物项
     * @param skuId
     * @param check
     */
    void checkItem(Long skuId, Integer check);

修改“com.atguigu.gulimall.cart.service.impl.CartServiceImpl”类,代码如下:

    @Override
    public void checkItem(Long skuId, Integer check) {
        BoundHashOperations<String, Object, Object> cartOps = getCartOps();
        CartItem cartItem = getCartItem(skuId);
        cartItem.setCheck(check == 1 ? true : false);
        String jsonString = JSON.toJSONString(cartItem);
        cartOps.put(skuId.toString(), jsonString);
    }

    /**
     * 获取我们要操作的购物车,临时购物车、用户购物车
     *
     * @return
     */
    private BoundHashOperations<String, Object, Object> getCartOps() {
        // 得到用户信息 账号用户 、临时用户
        UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
        // 1、userInfoTo.getUserId()不为空表示账号用户,反之临时用户  然后决定用临时购物车还是用户购物车
        // 放入缓存的key
        String cartKey = "";
        if (userInfoTo.getUserId() != null) {
            cartKey = CART_PREFIX + userInfoTo.getUserId();
        } else {
            cartKey = CART_PREFIX + userInfoTo.getUserKey();
        }
        BoundHashOperations<String, Object, Object> operations = stringRedisTemplate.boundHashOps(cartKey);
        return operations;
    }

9、修改购物项数量

修改CartList.html

                            <li>
                                <p th:attr="skuId=${item.skuId}">
                                    <span class="countOpsBtn">-</span>
                                    <span class="countOpsNum" th:text="${item.count}">5</span>
                                    <span class="countOpsBtn">+</span>
                                </p>
                            </li>
$('.countOpsBtn').click(function(){
        var skuId = $(this).parent().attr("skuId");
        var num = $(this).parent().find(".countOpsNum").text();
        // alert("商品:" +skuId+"===数量:"+num);
        location.href = "http://cart.gulimall.com/changeItemCount?skuId="+skuId+"&num="+num;
    })

修改“com.atguigu.gulimall.cart.controller.CartController”类,代码如下:

    @GetMapping("/changeItemCount")
    public String changeItemCount(@RequestParam("skuId") Long skuId, @RequestParam("num") Integer num){
        cartService.changeItemCount(skuId,num);
        return "redirect:http://cart.gulimall.com/cart.html";
    }

修改“com.atguigu.gulimall.cart.service.CartService”类,代码如下:

     /**
     * 修改购物项数量
     * 
     * @param skuId
     * @param num
     */
    void changeItemCount(Long skuId, Integer num);

修改“com.atguigu.gulimall.cart.service.impl.CartServiceImpl”类,代码如下:

    @Override
    public void changeItemCount(Long skuId, Integer num) {
        CartItem cartItem = getCartItem(skuId);
        cartItem.setCount(num);
        BoundHashOperations<String, Object, Object> cartOps = getCartOps();
        cartOps.put(skuId.toString(),JSON.toJSONString(cartItem));
    }

10、删除购物车项

 修改cartList.html

									<li>
										<p class="deleteItemBtn" th:attr="skuId=${item.skuId}">删除</p>
									</li>
		<div class="One_isDel">
			<p>
				<span>删除</span><span><img src="img/错误.png" alt=""></span>
			</p>
			<div>
				<dl>
					<dt><img src="img/感叹三角形 (2).png" alt=""></dt>
					<dd>
						<li>删除商品?</li>
						<li>您可以选择移到关注,或删除商品。</li>
					</dd>
				</dl>
			</div>
			<div>
				<button type="button" onclick="deleteItem()">删除</button>
			</div>
		</div>
		/**
		 * 删除购物项
		 */
		var deleteId = 0;
		$(".deleteItemBtn").click(function(){
			deleteId = $(this).attr("skuId");
		})
		function deleteItem() {
			location.href = "http://cart.gulimall.com/deleteItem?skuId=" + deleteId;
		}

修改“com.atguigu.gulimall.cart.controller.CartController”类,代码如下:

    @GetMapping("/deleteItem")
    public String deleteItem(@RequestParam("skuId") Long skuId){
        cartService.deleteItem(skuId);
        return "redirect:http://cart.gulimall.com/cart.html";
    }

修改“com.atguigu.gulimall.cart.service.CartService”类,代码如下:

    /**
     * 删除购物项
     * @param skuId
     */
    void deleteItem(Long skuId);

修改“com.atguigu.gulimall.cart.service.impl.CartServiceImpl”类,代码如下:

    @Override
    public void deleteItem(Long skuId) {
        BoundHashOperations<String, Object, Object> cartOps = getCartOps();
        cartOps.delete(skuId.toString());
    }

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐