尚筹网-前台-会员系统(springboot,springcloud 实战)

总目标:

  • 环境搭建
  • 会员登录注册
  • 发起众筹项目
  • 展示众筹项目
  • 支持众筹项目
  • 订单
  • 支付

1. 会员系统架构

1.1 架构图

1.2  需要创建的工程

  • 父工程、聚合工程:shangcouwang01-member-parent(唯一的pom工程)
  • 注册中心:shangcouwang02-member-eureka
  • 实体类模块:shangcouwang03-member-entity
  • MySQL数据服务:shangcouwang04-member-mysql-provider
  • Redis数据服务:shangcouwang05-member-redis-provider
  • 会员中心:shangcouwang06-member-authentication-consumer
  • 项目维护:shangcouwang07-member-project-consumer
  • 订单维护:shangcouwang08-member-order-consumer
  • 支付功能:shangcouwang09-member-pay-consumer
  • 网关:shangcouwang10-member-zuul
  • API模块:shangcouwang11-member-api

2. 搭建环境

2.1 搭建环境约定

2.1.1 包名约定:新创建的包都作为com.atguigu.crowd的子包

2.1.2 主启动类类名:CrowdMainClass

2.1.3 端口号:

  • 1000:shangcouwang02-member-eureka
  • 2000:shangcouwang04-member-mysql-provider
  • 3000:shangcouwang05-member-redis-provider
  • 4000:shangcouwang06-member-authentication-consumer
  • 5000:shangcouwang07-member-project-consumer
  • 7000:shangcouwang08-member-order-consumer
  • 8000:shangcouwang09-member-pay-consumer
  • 80:shangcouwang10-member-zuul

2.2 parent工程配置pom.xml

<groupId>com.atguigu.crowd</groupId>
<artifactId>shangcouwang01-member-parent</artifactId>
<version>1.0-SNAPSHOT</version>
<modules>
    <module>shangcouwang02-member-eureka</module>
    <module>shangcouwang03-member-entity</module>
    <module>shangcouwang04-member-mysql-provider</module>
    <module>shangcouwang05-member-redis-provider</module>
    <module>shangcouwang06-member-authentication-consumer</module>
    <module>shangcouwang07-member-project-consumer</module>
    <module>shangcouwang08-member-order-consumer</module>
    <module>shangcouwang09-member-pay-consumer</module>
    <module>shangcouwang10-member-zuul</module>
    <module>shangcouwang11-member-api</module>
</modules>
<packaging>pom</packaging>
<!--配置在父工程中要管理的依赖-->
<dependencyManagement>
    <dependencies>
        <!-- 导入 SpringCloud 需要使用的依赖信息 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Greenwich.SR2</version>
            <type>pom</type>
            <!-- import依赖范围表示将spring-cloud-dependencies包中的依赖信息导入 -->
            <scope>import</scope>
        </dependency>
        <!-- 导入 SpringBoot 需要使用的依赖信息 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>2.1.6.RELEASE</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <!--引入Mybatis的依赖-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.0</version>
        </dependency>
        <!--数据库连接池druid-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.0.5</version>
        </dependency>
    </dependencies>
</dependencyManagement>

2.3 eureka工程

2.3.1 依赖

<dependencies>
    <!--引入Eureka注册中心依赖-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    </dependency>
</dependencies>

2.3.2 主启动类

// 启用 Eureka 服务器功能
@EnableEurekaServer
@SpringBootApplication
public class CrowdMainClass {
    public static void main(String[] args) {
        SpringApplication.run(CrowdMainClass.class,args);
    }
}

2.3.3 application.yaml配置文件

server:
  port: 1000
spring:
  application:
    name: atguigu-crowd-eureka
eureka:
  instance:
    hostname: localhost        # 配置当前Eureka服务的主机地址
  client:
    registerWithEureka: false  # 注册:自己就是注册中心,所以自己不注册自己
    fetchRegistry: false       # 订阅:自己就是注册中心,所以不需要 "从注册中心取回信息"
    service-url:               # 客户端(指的是Consumer、Provider)访问 当前Eureka 时使用的地址
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

2.4 entity工程

2.4.1 实体类的进一步细分

  • VO:View Object 视图对象
    • 用途 1:接收浏览器发送过来的数据
    • 用途 2:把数据发送给浏览器去显示
  • PO:Persistent Object 持久化对象
    • 用途 1:将数据封装到PO对象存入数据库
    • 用途 2:将数据库数据查询出来存入PO对象
    • 所以 PO 对象是和数据库表对应,一个数据库表对应一个 PO 对象
  • DO:Data Object 数据对象
    • 用途 1:从 Redis查询得到数据封装为 DO 对象
    • 用途 2:从 ElasticSearch 查询得到数据封装为 DO 对象
    • 用途 3:从 Solr 查询得到数据封装为 DO 对象
    • ……
    • 从中间件或其他第三方接口查询到的数据封装为 DO 对象
  • DTO:Data Transfer Object 数据传输对象
    • 用途 1:从 Consumer 发送数据到 Provider
    • 用途 2:Provider 返回数据给 Consumer

使用 org.springframework.beans.BeanUtils.copyProperties(Object, Object)在不同实体类之间复制属性。MemberVO→复制属性→MemberPO

2.4.2 创建包

  • com.atguigu.crowd.entity.po
  • com.atguigu.crowd.entity.vo

2.4.3 lombok,简化JavaBean开发,让我们在开发时不必编写 getXxx()、setXxx()、有参构造器、无参构造器等等这样具备固定模式的代码。

注解:

  • @Data:每一个字段都加入 getXxx()、setXxx()方法
  • @NoArgsConstructor:无参构造器
  • @AllArgsConstructor:全部字段都包括的构造器
  • @EqualsAndHashCode:equals 和 hashCode 方法
  • @Getter
    • 类:所有字段都加入 getXxx()方法
    • 字段:当前字段加入 getXxx()方法
  • @Setter
    • 类:所有字段都加入 setXxx()方法
    • 字段:当前字段加入 setXxx()方法
<dependencies>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
</dependencies>

2.5 MySQL 工程基础环境

2.5.1 目标:抽取整个项目中所有针对数据库的操作。

2.5.2 创建数据库表

create table t_member(
    id int(11) not null auto_increment,
    loginacct varchar(255) not null,
    userpswd char(200) not null,
    username varchar(255),
    email varchar(255),
    authstatus int(4) comment '实名认证状态 0 - 未实名认证, 1 - 实名认证申请中, 2 - 已实名认证',
    usertype int(4) comment ' 0 - 个人, 1 - 企业',
    realname varchar(255),
    cardnum varchar(255),
    accttype int(4) comment '0 - 企业, 1 - 个体, 2 - 个人, 3 - 政府',
    primary key (id)
);

2.5.3 逆向生成 entity、*mapper、*mapper.xml

  • 实体类归位

  • Mapper相关归位

2.5.4 引入依赖

<!--引入Mybatis的依赖-->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<!--数据库连接池druid-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
</dependency>
<!--数据库驱动-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
<!--引入SpringBoot测试的依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
<!--eureka客户端-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--为了能够使用实体类-->
<dependency>
    <groupId>com.atguigu.crowd</groupId>
    <artifactId>shangcouwang03-member-entity</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

2.5.5 主启动类

// 扫描 MyBatis 的 Mapper 接口所在的包
@MapperScan("com.atguigu.crowd.mapper")
@SpringBootApplication
public class CrowdMainClass {
    public static void main(String[] args) {
        SpringApplication.run(CrowdMainClass.class,args);
    }
}

2.5.6 application.yaml配置文件

server:
  port: 2000
spring:
  application:
    name: atguigu-crowd-mysql
  datasource:
    # 不重要,用于标识,相当于bean标签的 id 属性
    name: mydb
    # 数据源的类型:DruidDataSource
    type: com.alibaba.druid.pool.DruidDataSource
    # 连接数据库的 url 地址
    url: jdbc:mysql://localhost:3306/project_crowd?serverTimezone=UTC
    # 用户名
    username: root
    # 密码
    password: abc123
    # 数据库驱动
    driver-class-name: com.mysql.cj.jdbc.Driver
eureka:
  client:
    service-url:
      defaultZone: http://localhost:1000/eureka/

# 做 mybatis 的配置
mybatis:
  # 用于指定 XxxMapper.xml 配置文件的位置
  mapper-locations: classpath:mybatis/mapper/*Mapper.xml

# 针对具体的某个包,设置日志级别,以便打印日志,就可以看到Mybatis打印的 SQL 语句了
logging:
  level:
    com.atguigu.crowd.mapper: debug
    com.atguigu.crowd.test: debug

2.5.7 测试类

@RunWith(SpringRunner.class)
@SpringBootTest
public class MyBatisTest {
    @Autowired
    private DataSource dataSource;
    @Autowired
    private MemberPOMapper memberPOMapper;

    private Logger logger = LoggerFactory.getLogger(MyBatisTest.class);

    @Test
    public void testMapper(){
        // 1.创建BCryptPasswordEncoder对象进行带盐值的加密
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        // 2.准备一个原始的密码
        String source = "123123";
        // 3.加密
        String encode = passwordEncoder.encode(source);

        MemberPO memberPO = new MemberPO(null,"jack",encode,"杰克","jack@qq.com",1,1,"杰克","123123",2);
        memberPOMapper.insert(memberPO);
    }

    @Test
    public void testConnection() throws SQLException {
        Connection connection = dataSource.getConnection();
        logger.debug(connection.toString());
    }
}

2.6 MySQL 工程对外暴露服务

        为了能够对外提供服务,服务本身需要有handler,handler调用service,service调用mapper完成整套业务,所以MySQL工程需要引入web依赖 

<!-- 对外暴露服务 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

2.6.1 api工程

引入依赖

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

创建接口MemberRemoteService

package com.atguigu.crowd.api;
// @FeignClient注解表示当前接口和一个Provider对应
//      注解中value属性指定要调用的Provider的微服务名称
@FeignClient(value = "atguigu-crowd-mysql")
public interface MySQLRemoteService {
    // member登陆时,根据 loginAcct 查询 Member 对象
    @RequestMapping("/get/memberpo/by/loginacct/remote")
    ResultEntity<MemberPO> getMemberPOByLoginAcctRemote(@RequestParam("loginacct") String loginacct);
}

2.6.2 MySQL工程

创建组件

handler代码

@RestController
public class MemberProviderHandler {

    @Autowired
    private MemberService memberService;

    // member登陆时,根据 loginAcct 查询 Member 对象
    @RequestMapping("/get/memberpo/by/loginacct/remote")
    public ResultEntity<MemberPO> getMemberPOByLoginAcctRemote(@RequestParam("loginacct") String loginacct){
        MemberPO memberPO = null;
        try {
            // 1.调用本地service完成查询
            memberPO = memberService.getMemberPOByLoginAcct(loginacct);
            // 2.如果没有抛异常,那么就返回成功的结果
            return ResultEntity.successWithData(memberPO);
        } catch (Exception e) {
            e.printStackTrace();
            // 3.如果捕获到异常,那么就返回失败的结果
            return ResultEntity.failed(e.getMessage());
        }
    }
}

service代码

// 在类上使用@Transactional(readOnly = true)注解针对查询操作设置事务属性,增删改操作需要在方法上写
@Transactional(readOnly = true)
@Service
public class MemberServiceImpl implements MemberService {
    @Autowired
    private MemberPOMapper memberPOMapper;

    @Override
    public MemberPO getMemberPOByLoginAcct(String loginacct) {
        // 1.创建Example对象
        MemberPOExample memberPOExample = new MemberPOExample();
        // 2.创建Criteria对象
        MemberPOExample.Criteria criteria = memberPOExample.createCriteria();
        // 3.封装查询条件
        criteria.andLoginacctEqualTo(loginacct);
        // 4.执行查询
        List<MemberPO> list = memberPOMapper.selectByExample(memberPOExample);
        // 5.获取结果
        if(list == null || list.size() == 0){
            return null;
        }
        return list.get(0);
    }
}

2.7 Redis 工程基础环境

2.7.1 目标:抽取项目中所有访问Redis的操作

2.7.2 依赖

<!--引入redis依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--作为eureka客户端访问eureka注册中心-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--对外暴露服务-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--引入SpringBoot测试的依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
<!--为了能够使用实体类-->
<dependency>
    <groupId>com.atguigu.crowd</groupId>
    <artifactId>shangcouwang03-member-entity</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

2.7.3 主启动类

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

2.7.4 application.yaml配置文件

server:
  port: 3000
spring:
  application:
    name: atguigu-crowd-redis
  redis:
    # 指定 redis 服务器的地址,也就是redis安装在Linux虚拟机的IP地址
    host: 192.168.56.100
    port: 6379
    password: 123456
eureka:
  client:
    service-url:
      defaultZone: http://localhost:1000/eureka/

2.7.5 测试

@RunWith(SpringRunner.class)
@SpringBootTest
public class MyRedisTest {
    @Autowired
    private RedisTemplate<String,String> redisTemplate;
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    private Logger logger = LoggerFactory.getLogger(MyRedisTest.class);

    @Test
    public void testRedis(){
        // 1.获取用来操作String类型数据的ValueOperations对象
        ValueOperations<String, String> operations = redisTemplate.opsForValue();
        // 2.借助ValueOperations对象存入数据
        operations.set("hello","world");
        // 3.读取刚才设置的数据
        String readValue = operations.get("hello");
        logger.debug(readValue);
    }
}

2.8 Redis 工程对外暴露服务

2.8.1 api工程创建接口

@FeignClient(value = "atguigu-crowd-redis")
public interface RedisRemoteService {
    // 不带超时时间的set
    @RequestMapping("/set/redis/key/value/remote")
    ResultEntity<String> setRedisKeyValueRemote(
            @RequestParam("key") String key,
            @RequestParam("value") String value);
    // 带超时时间的set
    @RequestMapping("/set/redis/key/value/remote/with/timeout")
    ResultEntity<String> setRedisKeyValueRemoteWithTimeout(
            @RequestParam("key") String key,
            @RequestParam("value") String value,
            @RequestParam("time") long time,            // 超时的时间
            @RequestParam("timeUnit") TimeUnit timeUnit);// 时间的单位
    // 查询
    @RequestMapping("/get/redis/string/value/by/key")
    ResultEntity<String> getRedisStringValueByKeyRemote(@RequestParam("key") String key);
    // 移除
    @RequestMapping("/remove/redis/key/remote")
    ResultEntity<String> removeRedisKeyRemote(@RequestParam("key") String key);

}

2.8.2 Redis工程handler代码

@RestController
public class RedisHandler {
    @Autowired
    private StringRedisTemplate redisTemplate;
    // 不带超时时间的set
    @RequestMapping("/set/redis/key/value/remote")
    public ResultEntity<String> setRedisKeyValueRemote(
            @RequestParam("key") String key,
            @RequestParam("value") String value) {
        try {
            ValueOperations<String, String> operations = redisTemplate.opsForValue();
            operations.set(key,value);
            return ResultEntity.successWithoutData();
        } catch (Exception e) {
            e.printStackTrace();
            return ResultEntity.failed(e.getMessage());
        }
    };
    // 带超时时间的set
    @RequestMapping("/set/redis/key/value/remote/with/timeout")
    public ResultEntity<String> setRedisKeyValueRemoteWithTimeout(
            @RequestParam("key") String key,
            @RequestParam("value") String value,
            @RequestParam("time") long time,            // 超时的时间
            @RequestParam("timeUnit") TimeUnit timeUnit) {// 时间的单位
        try {
            ValueOperations<String, String> operations = redisTemplate.opsForValue();
            operations.set(key,value,time,timeUnit);
            return ResultEntity.successWithoutData();
        } catch (Exception e) {
            e.printStackTrace();
            return ResultEntity.failed(e.getMessage());
        }
    };
    // 查询
    @RequestMapping("/get/redis/string/value/by/key")
    public ResultEntity<String> getRedisStringValueByKeyRemote(@RequestParam("key") String key){
        try {
            ValueOperations<String, String> operations = redisTemplate.opsForValue();
            String value = operations.get(key);
            return ResultEntity.successWithData(value);
        } catch (Exception e) {
            e.printStackTrace();
            return ResultEntity.failed(e.getMessage());
        }
    };
    // 移除
    @RequestMapping("/remove/redis/key/remote")
    public ResultEntity<String> removeRedisKeyRemote(@RequestParam("key") String key){
        try {
            redisTemplate.delete(key);
            return ResultEntity.successWithoutData();
        } catch (Exception e) {
            e.printStackTrace();
            return ResultEntity.failed(e.getMessage());
        }
    };
}

2.9 认证工程(会员中心)显示首页

2.9.1 依赖

<!--为了引入web-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--为了引入feign和entity实体类-->
<dependency>
    <groupId>com.atguigu.crowd</groupId>
    <artifactId>shangcouwang11-member-api</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>
<!--整合thymeleaf-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!--作为eureka客户端访问eureka注册中心-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

2.9.2 主启动类

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

2.9.3 application.yaml配置文件

server:
  port: 4000
spring:
  application:
    name: atguigu-crowd-auth
  thymeleaf:
    prefix: classpath:/templates/
    suffix: .html
    cache: false #开发的时候禁用缓存
eureka:
  client:
    service-url:
      defaultZone: http://localhost:1000/eureka/

2.9.4 显示首页的handler

@Controller
public class PortalHandler {
    @RequestMapping("/")
    public String showPortalPage(){
        // 这里实际开发中需要加载数据
        return "portal";
    }
}

2.9.5 加入静态资源

①.静态资源加入的位置:springboot要求在static目录下存放静态资源

②.调整portal.html页面(使用thymeleaf技术)

方式1:

<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="description" content="">
    <meta name="author" content="">
    <link rel="stylesheet" th:href="@{/bootstrap/css/bootstrap.min.css}">
    <link rel="stylesheet" th:href="@{/css/font-awesome.min.css}">
    <link rel="stylesheet" th:href="@{/css/carousel.css}">

方式2:

<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="description" content="">
    <meta name="author" content="">
    <base th:href="@{/}"/>
    <link rel="stylesheet" href="bootstrap/css/bootstrap.min.css">
    <link rel="stylesheet" href="css/font-awesome.min.css">
    <link rel="stylesheet" href="css/carousel.css">

2.10 网关Zuul

2.10.1 依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>

2.10.2 主启动类

// 启用 Zuul 代理功能
@EnableZuulProxy
@SpringBootApplication
public class CrowdMainClass {
    public static void main(String[] args) {
        SpringApplication.run(CrowdMainClass.class,args);
    }
}

2.10.3 application.yaml配置文件

server:
  port: 80
spring:
  application:
    name: atguigu-crowd-zuul
eureka:
  client:
    service-url:
      defaultZone: http://localhost:1000/eureka/
zuul:
  ignored-services: "*"  # 忽略所有微服务名称
  sensitive-headers: "*" # 在 Zuul 向其他微服务重定向时保持原本头信息(请求头、响应头)
  routes:
    crowd-portal:        # 自定义路由规则的名称,在底层的数据结构中是 Map 的键
      serviceId: atguigu-crowd-auth
      path: /**          # 这里一定要使用两个"*"号,不然"/"路径后面的多层路径将无法访问

2.10.4 配置域名(假的,可选)

2.10.5 访问效果:localhost:80

3. 具体业务

3.1 会员注册

3.1.1 发送验证码的流程

①.目标

  • 将验证码发送到用户的手机上
  • 将验证码存入Redis中

②.思路

③.实际操作(挑选重要步骤记录,其他环节省略)

A. 前往注册的页面-创建注解版view-controller

package com.atguigu.crowd.config;
@Configuration
public class CrowdWebMvcConfig implements WebMvcConfigurer {
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        // 浏览器访问的地址
        String urlPath = "/auth/member/to/reg/page.html";
        // 目标视图的名称,将来拼接视图的前后缀
        String viewName = "member-reg";
        // 添加一个view-controller
        registry.addViewController(urlPath).setViewName(viewName);
    }
}
// 效果相当于以下方法:
@RequestMapping("/auth/member/to/reg/page.html")
public String memberToRegPage(){
    return "member-reg";
}

B. 修改注册超链接

<li><a href="reg.html" th:href="@{/auth/member/to/reg/page.html}">注册</a></li>

C. 注册:点击获取验证码按钮发送短信,并把验证码保存到redis中

1)前端代码:在获取验证码按钮这绑定单击响应函数

<button id="sendBtn" class="btn btn-lg btn-success btn-block "> 获取验证码</button>
以上修改,没有type,默认是submit按钮,点击之后会提交表单,所以做如下修改,改成普通按钮
<button type="button" id="sendBtn" class="btn btn-lg btn-success btn-block "> 获取验证码</button>

绑定单击响应函数
$(function () {
    $("#sendBtn").click(function () {
        // 1.获取接收短信的手机号,[]表示根据属性去定位,要的是name=phoneName的属性
        var phoneNum = $.trim($("[name=phoneNum]").val()); // trim:去前后的空格
        // 2.发送Ajax请求
        $.ajax({
            "url": "/auth/member/send/short/message.json",
            "type":"post",
            "data":{
                "phoneNum":phoneNum
            },
            "dataType":"json",
            "success":function (response) {
                console.log(response);
                var result = response.result;
                if(result == "SUCCESS"){
                    layer.msg("发送成功!")
                }
                if(result == "FAILED"){
                    layer.msg("发送失败!请再试一次")
                }
            },
            "error":function (response) {
                layer.msg(response.status + " " + response.statusText)
            }
        });
    });
});

2)后端代码:发送短信

切记:不要忘了在主启动类上加注解:@EnableFeignClients,启用 Feign 客户端功能

@Controller
public class MemberHandler {
    @Autowired
    private ShortMessageProperties shortMessageProperties;
    @Autowired
    private RedisRemoteService redisRemoteService;
    @ResponseBody
    @RequestMapping("/auth/member/send/short/message.json")
    public ResultEntity<String> sendMessage(@RequestParam("phoneNum") String phoneNum){
        // 1.发送验证码到phoneNum手机
        ResultEntity<String> sendMessageResultEntity = SendMessageUtil.sendShortMessage(
                shortMessageProperties.getHost(),
                shortMessageProperties.getPath(),
                shortMessageProperties.getMethod(),
                phoneNum,
                shortMessageProperties.getAppCode(),
                shortMessageProperties.getSmsSignId(),
                shortMessageProperties.getTemplateId()
        );
        // 2.判断短信发送结果
        if(ResultEntity.SUCCESS.equals(sendMessageResultEntity.getResult())){
            // 3.如果发生成功,则将验证码存入Redis
            // ①从上一步操作的结果中获取随机生成的验证码
            String code = sendMessageResultEntity.getData();
            // ②拼接一个用于在redis中存储数据的key
            String key = "REDIS_CODE_PREFIX_" + phoneNum;
            // ③调用远程的接口的把验证码存入到redis中
            ResultEntity<String> saveCodeResultEntity = redisRemoteService.setRedisKeyValueRemoteWithTimeout(key, code, 15, TimeUnit.MINUTES);
            // ④判断结果
            if(ResultEntity.SUCCESS.equals(saveCodeResultEntity.getResult())){
                return ResultEntity.successWithoutData();
            }else{
                return saveCodeResultEntity;
            }
        }else{
            return sendMessageResultEntity;
        }
    }
}

3.1.2 执行注册

①.目标:如果针对注册操作所做的各项验证能够通过,则将Member信息存入到数据库

②.思路

③.实际操作(挑选重要步骤记录,其他环节省略)

A. 给t_member表增加唯一约束

ALTER TABLE `project_crowd`.`t_member` ADD UNIQUE INDEX (`loginacct`); 

B. 在mysql-provider中创建远程接口,实现执行保存操作

//handler
@RequestMapping("/save/member/remote")
public ResultEntity<String> saveMemberRemote(@RequestBody MemberPO memberPO){
    try {
        memberService.saveMember(memberPO);
        return ResultEntity.successWithoutData();
    } catch (Exception e) {
        if(e instanceof DuplicateKeyException){
            return ResultEntity.failed("抱歉!这个账号已经被使用了!");
        }
        return ResultEntity.failed(e.getMessage());
    }
}
//service
void saveMember(MemberPO memberPO);
//serviceimpl
@Override
public void saveMember(MemberPO memberPO) {
    // 使用insertSelective,进行有值的保存,无值的为null
    memberPOMapper.insertSelective(memberPO);
}

在FeignClient接口中(api工程)声明新的方法

@RequestMapping("/save/member/remote")
public ResultEntity<String> saveMemberRemote(@RequestBody MemberPO memberPO);

C.  在authentication-consumer工程中,完成注册操作

1)创建MemberVO类接收表单数据(entity工程中创建)

@Data
@NoArgsConstructor
@AllArgsConstructor
public class MemberVO {
    private String loginacct;
    private String userpswd;
    private String username;
    private String email;
    private String phoneNum;
    private String code;
}

2)注册操作的具体实现:auth工程的handler方法

// 执行注册
@RequestMapping("/auth/do/member/register")
public String register(MemberVO memberVO, ModelMap modelMap){
    // 1.获取用户输入的手机号
    String phoneNum = memberVO.getPhoneNum();
    // 2.拼Redis中存储验证码的key
    String key = "REDIS_CODE_PREFIX_" + phoneNum;
    // 3.从Redis中读取key对应的value
    ResultEntity<String> redisResultEntity = redisRemoteService.getRedisStringValueByKeyRemote(key);
    // 4.检查查询操作是否有效
    if(ResultEntity.FAILED.equals(redisResultEntity.getResult())){
        modelMap.addAttribute("message",redisResultEntity.getMessage());
        return "member-reg";
    }
    String redisCode = redisResultEntity.getData();
    if(redisCode == null){
        modelMap.addAttribute("message","验证码不存在!请检查手机号是否正确或重新发送");
        return "member-reg";
    }
    // 5.如果从Redis能够查询到value则比较"表单验证码"和"redis验证码"
    String formCode = memberVO.getCode();
    if(!Objects.equals(formCode,redisCode)){
        modelMap.addAttribute("message","验证码不正确!");
        return "member-reg";
    }
    // 6.如果验证码一致,则从Redis删除
    redisRemoteService.removeRedisKeyRemote(key);
    // 7.执行密码加密
    BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
    String userpswd = memberVO.getUserpswd();
    // 加密
    String encode = passwordEncoder.encode(userpswd);
    memberVO.setUserpswd(encode);
    // 8.执行保存
    // ①.创建空的MemberPO的对象
    MemberPO memberPO = new MemberPO();
    // ②.复制属性
    BeanUtils.copyProperties(memberVO,memberPO);
    // ③.调用远程的方法
    ResultEntity<String> saveMemberResultEntity = mySQLRemoteService.saveMemberRemote(memberPO);
    // 如果失败
    if(ResultEntity.FAILED.equals(saveMemberResultEntity.getResult())){
        modelMap.addAttribute("message",saveMemberResultEntity.getMessage());
        return "member-reg";
    }
    // 使用重定向避免刷新浏览器导致重新执行注册流程
    return "redirect:/auth/member/to/login/page";
}

3.2 会员登录

3.2.1 目标:检查账号密码正确后将用户信息存入session,表示用户已登录

3.2.2 思路

③.实际操作(挑选重要步骤记录,其他环节省略)

A. entity工程创建MemberLoginVO对象,以便将登录状态存入session域

@Data
@NoArgsConstructor
@AllArgsConstructor
public class MemberLoginVO {
    private Integer id;
    private String username;
    private String email;
}

B.  在authentication-consumer工程执行登录

// 执行登录
@RequestMapping("/auth/member/do/login")
public String login(
        @RequestParam("loginacct") String loginacct,
        @RequestParam("userpswd") String userpswd,
        ModelMap modelMap,
        HttpSession session){
    // 1.调用远程接口根据登录账号查询MemberPO对象
    ResultEntity<MemberPO> memberPOResultEntity = mySQLRemoteService.getMemberPOByLoginAcctRemote(loginacct);
    // 2.如果失败回到登录页面
    if(ResultEntity.FAILED.equals(memberPOResultEntity.getResult())){
        modelMap.addAttribute("message",memberPOResultEntity.getMessage());
        return "member-login";
    }
    // 3.拿到查询出来的MemberPO对象
    MemberPO memberPO = memberPOResultEntity.getData();
    if(memberPO == null){
        modelMap.addAttribute("message","您的账号不存在,请检查是否输入正确!");
        return "member-login";
    }
    // 4.比较输入的密码和查询出来的密码是否一致
    String userpswdDataBase = memberPO.getUserpswd();
    BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
    if(!passwordEncoder.matches(userpswd,userpswdDataBase)){
        modelMap.addAttribute("message","密码错误,请重新输入!");
        return "member-login";
    }
    // 5.创建MemberLoginVO对象存入session域
    MemberLoginVO memberLoginVO = new MemberLoginVO();
    BeanUtils.copyProperties(memberPO,memberLoginVO);
    session.setAttribute("loginMember",memberLoginVO);

    return "redirect:/auth/member/to/center/page";
}

C. 退出登录

// 退出登录
@RequestMapping("/auth/member/logout")
public String logout(HttpSession session){
    // 使session失效
    session.invalidate();
    // 重定向到首页
    return "redirect:/";
}

3.3 会员登录功能的延伸

新目标:使用 Session 共享技术解决 Session 不互通问题

3.3.1 会话控制回顾

①Cookie 的工作机制

  • 服务器端返回 Cookie 信息给浏览器:
    • java代码:response.addCookie(cookie对象)
    • HTTP响应消息头:Set-Cookie:Cookie的名字=Cookie的值
  • 浏览器接收到服务器端返回的Cookie,以后的每一次请求都会把Cookie带上
    • HTTP请求消息头:Cookie:Cookie的名字=Cookie的值

②Session 的工作机制

  • 获取Session 对象:request.getSession()
    • 检查当前请求是否携带了JSESSIONID这个Cookie
      • 带了:根据这个JSESSIONID在服务器端查找对应的Session对象
        • 能找到:就把找到的Session对象返回
        • 没找到:新建Session对象返回,同时返回JSESSIONID的Cookie
      • 没带:新建Session对象返回,同时返回JSESSIONID的Cookie

3.3.2 Session共享

        在分布式和集群环境下,每个具体模块运行在单独的Tomcat上,而Session是被不同Tomcat所“区隔”的,不能互通。那么如何解决Session共享的问题呢?

解决方案:

①session同步:借助于Tomcat,Tomcat中做一些相关配置,就可以实现Session的同步。

但面临着以下几个问题:

  • 问题1:造成 Session 在各个服务器上“同量”保存。TomcatA 保存了 1G的 Session 数据,TomcatB 也需要保存 1G 的 Session 数据。数据量太大会导致 Tomcat 性能下降。
  • 问题2:数据同步对性能有一定影响。

②将Session数据存储在Cookie中

  • 做法:所有会话数据在浏览器端使用 Cookie 保存,服务器端不存储任何会话数据。
  • 好处:服务器端大大减轻了数据存储的压力。不会有 Session 不一致问题
  • 缺点:
    • Cookie 能够存储的数据非常有限。一般是 4KB。不能存储丰富的数据。
    • Cookie 数据在浏览器端存储,很大程度上不受服务器端控制,如果浏览器端清理 Cookie,相关数据会丢失。

③反向代理hash 一致性 

面临的问题:

  • 问题 1:具体一个浏览器,专门访问某一个具体服务器,如果服务器宕机,会丢失数据。存在单点故障风险。
  • 问题 2:仅仅适用于集群范围内,超出集群范围,负载均衡服务器无效。

④后端统一存储Session数据

        后端存储 Session 数据时,一般需要使用 Redis 这样的内存数据库,而一般不采用 MySQL 这样的关系型数据库。原因如下:

  • Session 数据存取比较频繁。内存访问速度快。
  • Session有过期时间,Redis这样的内存数据库能够比较方便实现过期释放。 

优点:

  • 访问速度比较快。虽然需要经过网络访问,但是现在硬件条件已经能够达到网络访问比硬盘访问还要快。
    • 硬盘访问速度:200M/s
    • 网络访问速度:1G/s
  • Redis可以配置成主从复制集群(master/slave),在master/slave中来个哨兵(sentine),如果slave宕机的话,哨兵会检测到,如果master宕机的话,从slave中选取一个。所以不用担心单点故障问题。  

但是做起来却很麻烦,那么该如何实现呢?引入技术SpringSession即可,具体内容见:4.2

浏览器:发送Cookie数据
服务器:解析Cookie数据
服务器:查找对应的Session,如果没有则创建
服务器:把新建的Session存入Redis
========================================
浏览器:请求要求在原有的Session中存入新数据
服务器:根据Cookie把旧的Session找到,存入数据,存回Redis
========================================
最理想的状态:原有的开发习惯不要有任何改变
@RequestMapping("/xx/xx")
public String xxx(HttpSession session){
    session.setAttribute("xx","xx");
    return "...";
}
那么如何才能做到呢?

3.4 登录检查

3.4.1 目标:把项目中必须登录才能访问的功能保护起来,如果没有登录就访问则跳转到登录页面

3.4.2 思路:

3.4.3 代码:设置Session共享(每个Consumer和Zuul都需要做)

实现:zuul工程、auth-consumer工程均做此配置

pom.xml:
<!-- 引入 springboot&redis 整合场景 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 引入 springboot&springsession 整合场景 -->
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>
application.yaml:
spring:
  # redis 配置
  redis:
    host: 192.168.56.100 #redis的主机地址
    port: 6379
    password: 123456
    jedis:
      pool:
        max-idle: 100    #jedis连接池的最大连接数,不是必须的
  # springsession配置
  session:
    store-type: redis    #告诉SpringSession存储的类型是在哪存

3.4.4 代码:准备不需要登录检查的资源(放在entity工程的util包里)

①准备好可以放行的资源

public class AccessPassResources {
    public static final Set<String> PASS_RES_SET = new HashSet<>();

    static {
        // 统一入口
        PASS_RES_SET.add("/");
        // 去注册页面
        PASS_RES_SET.add("/auth/member/to/reg/page");
        // 去登录页
        PASS_RES_SET.add("/auth/member/to/login/page");
        // 退出功能
        PASS_RES_SET.add("/auth/member/logout");
        // 做登录
        PASS_RES_SET.add("/auth/member/do/login");
        // 执行注册
        PASS_RES_SET.add("/auth/do/member/register");
        // Ajax请求:发送验证码
        PASS_RES_SET.add("/auth/member/send/short/message.json");
    }

    public static final Set<String> STATIC_RES_SET = new HashSet<>();

    static {
        // 静态资源
        STATIC_RES_SET.add("bootstrap");
        STATIC_RES_SET.add("css");
        STATIC_RES_SET.add("fonts");
        STATIC_RES_SET.add("img");
        STATIC_RES_SET.add("jquery");
        STATIC_RES_SET.add("layer");
        STATIC_RES_SET.add("script");
        STATIC_RES_SET.add("ztree");
    }
}

②判断当前请求是否为静态资源

/**
 * 用于判断某个ServletPath值是否对应一个静态资源
 * @param servletPath:资源路径
 * @return true:是静态资源;false:不是静态资源
 */
public static boolean judgeCurrentServletPathWhetherStaticResource(String servletPath){
    // 1.排除字符串无效的情况
    if(servletPath == null || servletPath.length() == 0){
        throw new RuntimeException("字符串不合法!请不要传入空字符串");
    }
    // 2.根据"/"拆分ServletPath字符串
    String[] split = servletPath.split("/");
    // 3.考虑到第一个斜杠左边经过拆分后得到一个空字符串是数组的第一个元素,所以需要使用下标1取第二个元素
    String firstLevelPath = split[1];
    // 4.判断是否在集合中
    return STATIC_RES_SET.contains(firstLevelPath);
}

3.4.5 代码:ZuulFilter

@Component
public class MyZuulFilter extends ZuulFilter {

    @Override
    public String filterType() {
        // 这里返回"pre"意思是在目标微服务前执行过滤
        return "pre";
    }

    @Override
    public int filterOrder() {
        return 0;
    }

    /**
     * 判断当前请求是否要进行过滤
     *
     * @return 要过滤:返回true,继续执行run()方法
     *         不过滤:返回false,直接放行
     */
    @Override
    public boolean shouldFilter() {
        // 1.获取当前 RequestContext 对象
        RequestContext requestContext = RequestContext.getCurrentContext();
        // 2.通过requestContext对象获取当前请求对象
        //(框架底层是借助 ThreadLocal 从当前线程上获取事先绑定的 Request 对象)
        HttpServletRequest request = requestContext.getRequest();
        // 3.获取当前请求要访问的目标地址
        String servletPath = request.getServletPath();
        // 4.根据ServletPath判断当前请求是否对应可以直接放行的特定功能
        boolean containsResult = AccessPassResources.PASS_RES_SET.contains(servletPath);
        if(containsResult){
            // 5.如果当前请求是可以直接放行的特定功能请求则返回false放行
            return false;
        }
        // 6.判断当前请求是否为静态资源
        // 工具方法返回true: 说明当前请求是静态资源请求,取反为 false 表示放行不做登录检查
        // 工具方法返回false: 说明当前请求不是可以放行的特定请求也不是静态资源,取反为 true 表示需要做登录检查
        return !AccessPassResources.judgeCurrentServletPathWhetherStaticResource(servletPath);
    }

    @Override
    public Object run() throws ZuulException {
        // 1.获取当前请求对象
        RequestContext requestContext = RequestContext.getCurrentContext();
        HttpServletRequest request = requestContext.getRequest();
        // 2.获取当前Session对象
        HttpSession session = request.getSession();
        // 3.尝试从Session对象中获取已登录的对象
        Object loginMember = session.getAttribute("loginMember");
        // 4.判断loginMember是否为空
        if(loginMember == null){
            // 5.从requestContext对象中获取Response对象
            HttpServletResponse response = requestContext.getResponse();
            // 6.将提示消息存入Session域
            session.setAttribute("message","还没有进行登录!请先登录。");
            // 7.重定向到auth-consumer工程中的登录页面
            try {
                response.sendRedirect("/auth/member/to/login/page");
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        // 否则就放行
        return null;
    }
}

3.4.6 代码:登录页面读取Session域,做消息提示

<p th:text="${session.message}">这里登录检查后发现不允许访问时的提示消息</p>

3.4.7 代码:Zuul中的特殊设置

 

 为了能够让整个过程中保持Session工作正常,需要新增如下配置

zuul:
  sensitive-headers: "*" # 在 Zuul 向其他微服务重定向时保持原本头信息(请求头、响应头)

3.5 发起项目

3.5.1 建模:创建数据库表

①分类表

create table t_type
(
    id int(11) not null auto_increment,
    name varchar(255) comment '分类名称',
    remark varchar(255) comment '分类介绍',
    primary key (id)
);

②项目分类中间表

create table t_project_type
(
    id int not null auto_increment,
    projectid int(11),
    typeid int(11),
    primary key (id)
);

③标签表

create table t_tag
(
    id int(11) not null auto_increment,
    pid int(11),
    name varchar(255),
    primary key (id)
);

④项目标签中间表

create table t_project_tag
(
    id int(11) not null auto_increment,
    projectid int(11),
    tagid int(11),
    primary key (id)
);

⑤项目表

create table t_project
(
    id int(11) not null auto_increment,
    project_name varchar(255) comment '项目名称',
    project_description varchar(255) comment '项目描述',
    money bigint (11) comment '筹集金额',
    day int(11) comment '筹集天数',
    status int(4) comment '0-即将开始,1-众筹中,2-众筹成功,3-众筹失败',
    deploydate varchar(10) comment '项目发起时间',
    supportmoney bigint(11) comment '已筹集到的金额',
    supporter int(11) comment '支持人数',
    completion int(3) comment '百分比完成度',
    memberid int(11) comment '发起人的会员 id',
    createdate varchar(19) comment '项目创建时间',
    follower int(11) comment '关注人数',
    header_picture_path varchar(255) comment '头图路径',
    primary key (id)
);

⑥项目表项目详情图片表

create table t_project_item_pic
(
    id int(11) not null auto_increment,
    projectid int(11),
    item_pic_path varchar(255),
    primary key (id)
);

⑦项目发起人信息表

create table t_member_launch_info
(
    id int(11) not null auto_increment,
    memberid int(11) comment '会员 id',
    description_simple varchar(255) comment '简单介绍',
    description_detail varchar(255) comment '详细介绍',
    phone_num varchar(255) comment '联系电话',
    service_num varchar(255) comment '客服电话',
    primary key (id)
);

⑧回报信息表

create table t_return
(
    id int(11) not null auto_increment,
    projectid int(11),
    type int(4) comment '0 - 实物回报, 1 虚拟物品回报',
    supportmoney int(11) comment '支持金额',
    content varchar(255) comment '回报内容',
    count int(11) comment '回报产品限额,“0”为不限回报数量',
    signalpurchase int(11) comment '是否设置单笔限购',
    purchase int(11) comment '具体限购数量',
    freight int(11) comment '运费,“0”为包邮',
    invoice int(4) comment '0 - 不开发票, 1 - 开发票',
    returndate int(11) comment '项目结束后多少天向支持者发送回报',
    describ_pic_path varchar(255) comment '说明图片路径',
    primary key (id)
);

⑨发起人确认信息表

create table t_member_confirm_info
(
    id int(11) not null auto_increment,
    memberid int(11) comment '会员 id',
    paynum varchar(200) comment '易付宝企业账号',
    cardnum varchar(200) comment '法人身份证号',
    primary key (id)
);

3.5.2 逆向工程生成PO对象,并归位 

<table tableName="t_type" domainObjectName="TypePO" />
<table tableName="t_tag" domainObjectName="TagPO" />
<table tableName="t_project" domainObjectName="ProjectPO" />
<table tableName="t_project_item_pic" domainObjectName="ProjectItemPicPO" />
<table tableName="t_member_launch_info" domainObjectName="MemberLaunchInfoPO" />
<table tableName="t_return" domainObjectName="ReturnPO" />
<table tableName="t_member_confirm_info" domainObjectName="MemberConfirmInfoPO" />

3.5.3 创建VO对象接收表单数据

@Data
@NoArgsConstructor
@AllArgsConstructor
public class ProjectVO implements Serializable {
    private static final long serialVersionUID = 1L;
    // 分类 id 集合
    private List<Integer> typeIdList;
    // 标签 id 集合
    private List<Integer> tagIdList;
    // 项目名称
    private String projectName;
    // 项目描述
    private String projectDescription;
    // 计划筹集的金额
    private Integer money;
    // 筹集资金的天数
    private Integer day;
    // 创建项目的日期
    private String createdate;
    // 头图的路径
    private String headerPicturePath;
    // 详情图片的路径
    private List<String> detailPicturePathList;
    // 发起人信息
    private MemberLaunchInfoVO memberLaunchInfoVO;
    // 回报信息集合
    private List<ReturnVO> returnVOList;
    // 发起人确认信息
    private MemberConfirmInfoVO memberConfirmInfoVO;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MemberLaunchInfoVO implements Serializable {
    private static final long serialVersionUID = 1L;
    // 简单介绍
    private String descriptionSimple;
    // 详细介绍
    private String descriptionDetail;
    // 联系电话
    private String phoneNum;
    // 客服电话
    private String serviceNum;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ReturnVO implements Serializable {
    private static final long serialVersionUID = 1L;
    // 回报类型:0 - 实物回报, 1 虚拟物品回报
    private Integer type;
    // 支持金额
    private Integer supportmoney;
    // 回报内容介绍
    private String content;
    // 总回报数量,0 为不限制
    private Integer count;
    // 是否限制单笔购买数量,0 表示不限购,1 表示限购
    private Integer signalpurchase;
    // 如果单笔限购,那么具体的限购数量
    private Integer purchase;
    // 运费,“0”为包邮
    private Integer freight;
    // 是否开发票,0 - 不开发票, 1 - 开发票
    private Integer invoice;
    // 众筹结束后返还回报物品天数
    private Integer returndate;
    // 说明图片路径
    private String describPicPath;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MemberConfirmInfoVO implements Serializable {
    private static final long serialVersionUID = 1L;
    private String paynum;
    private String cardnum;
}

3.5.4 发起项目

3.5.4.1 总目标:将各个表单页面提交的数据汇总到一起保存到数据库。 

3.5.4.2 思路:

3.5.4.3 代码:

①为发起众筹按钮、阅读并同意协议按钮绑定单击响应函数

authentication-consumer工程:member-crowd.html
<li class=" pull-right">
    <script type="text/javascript">
        $(function () {
            $("#launchCrowdBtn").click(function () {
                // project是配的路由规则,
                window.location.href="http://localhost:80/project/agree/protocol/page";
            });
        });
    </script>
    <button type="button" id="launchCrowdBtn" class="btn btn-warning">发起众筹</button>
</li>
project-consumer工程:project-agree.html
<div class="panel-footer" style="text-align:center;">
    <script type="text/javascript">
        $(function () {
            $("#agreeProtocolBtn").click(function () {
                window.location.href="/project/launch/project/page"
            });
        });
    </script>
    <button type="button" id="agreeProtocolBtn" class="btn  btn-warning btn-lg">阅读并同意协议</button>
</div>

②配置访问 project-consumer 工程的路由规则

zuul:
  ignored-services: "*"  # 忽略所有微服务名称
  sensitive-headers: "*" # 在 Zuul 向其他微服务重定向时保持原本头信息(请求头、响应头)
  routes:
    crowd-project:
      serviceId: atguigu-crowd-project
      path: /project/**

③在 project-consumer 工程配置 view-controller

@Configuration
public class CrowdWebMvcConfig implements WebMvcConfigurer {
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        // view-controller是在project-consumer内部定义的,所以这里是一个不经过zuul访问的地址。所以这个路径前面不加路由规则中定义的前缀project
        registry.addViewController("/agree/protocol/page").setViewName("project-agree");
        registry.addViewController("/launch/project/page").setViewName("project-launch");
    }
}

④代码:接收项目及发起人信息表单数据

内容:上传头图、上传详情图片、把ProjectVO存入Session域
@Controller
public class ProjectConsumerHandler {
    @Autowired
    private OSSProperties ossProperties;
    @RequestMapping("/create/project/information")
    public String saveProjectBasicInfo(
            // 用于接收除了上传图片之外的其他普通数据
            ProjectVO projectVO,
            // 用于接收上传的头图
            MultipartFile headerPicture,
            // 用于接收上传的详情图片
            List<MultipartFile> detailPictureList,
            // 用来将收集了一部分数据的ProjectVO对象存入Session域
            HttpSession session,
            // 用来在当前操作失败后返回上一个表单页面时携带的提示消息
            ModelMap modelMap) throws IOException {
        // 1.完成头图上传
        boolean headerPictureIsEmpty = headerPicture.isEmpty();
        if(!headerPictureIsEmpty){
            // 2.如果用户确实上传了有内容的文件,则执行上传
            ResultEntity<String> uploadHeaderPicResultEntity = MyOSSUtils.uploadFileToOss(
                    ossProperties.getEndPoint(),
                    ossProperties.getAccessKeyId(),
                    ossProperties.getAccessKeySecret(),
                    headerPicture.getInputStream(),
                    ossProperties.getBucketName(),
                    ossProperties.getBucketDomain(),
                    headerPicture.getOriginalFilename());
            String result = uploadHeaderPicResultEntity.getResult();
            // 3.判断头图是否上传成功
            if(ResultEntity.SUCCESS.equals(result)){
                // 4.从返回的数据中获取图片的访问路径
                String headerPicturePath = uploadHeaderPicResultEntity.getData();
                // 5.存入ProjectVO对象
                projectVO.setHeaderPicturePath(headerPicturePath);
            }else{
                // 6.如果上传失败则返回到表单页面并显示错误消息
                modelMap.addAttribute("message","头图上传失败!");
                return "project-launch";
            }
        }
        // 创建一个用来存放详情图片路径的集合
        ArrayList<String> detailPicturePathList = new ArrayList<>();
        // 4.遍历detailPictureList集合
        for (MultipartFile detailPicture : detailPictureList) {
            // 5.当前detailPicture是否为空
            boolean detailPictureIsEmpty = detailPicture.isEmpty();
            if(!detailPictureIsEmpty){
                // 6.不空则执行上传
                ResultEntity<String> uploadDetailPicResultEntity = MyOSSUtils.uploadFileToOss(
                        ossProperties.getEndPoint(),
                        ossProperties.getAccessKeyId(),
                        ossProperties.getAccessKeySecret(),
                        detailPicture.getInputStream(),
                        ossProperties.getBucketName(),
                        ossProperties.getBucketDomain(),
                        detailPicture.getOriginalFilename());
                String detailPicResult = uploadDetailPicResultEntity.getResult();
                // 7.判断详情图片是否上传成功
                if(ResultEntity.SUCCESS.equals(detailPicResult)){
                    String detailPicturePath = uploadDetailPicResultEntity.getData();
                    detailPicturePathList.add(detailPicturePath);
                }
            }
        }
        // 9.将存放了详情图片路径的集合存入ProjectVO中
        projectVO.setDetailPicturePathList(detailPicturePathList);
        // 10.把ProjectVO存入Session域中:临时的Project
        session.setAttribute("tempProject",projectVO);
        // 11.去下一个表单:回报信息页面。重定向是为了防止表单重复提交
        return "redirect:http://localhost:80/project/return/info/page";
    }
}

⑤代码:接收回报信息表单数据

注意:上传图片和提交表单是分开的

后端代码:接收页面异步上传的图片

@ResponseBody
@RequestMapping("/create/upload/return/picture.json")
public ResultEntity<String> uploadReturnPicture(
        // 接收用户上传的文件
        @RequestParam("returnPicture") MultipartFile returnPicture) throws IOException {
    // 1.执行文件上传
    ResultEntity<String> uploadReturnPicResultEntity = MyOSSUtils.uploadFileToOss(
            ossProperties.getEndPoint(),
            ossProperties.getAccessKeyId(),
            ossProperties.getAccessKeySecret(),
            returnPicture.getInputStream(),
            ossProperties.getBucketName(),
            ossProperties.getBucketDomain(),
            returnPicture.getOriginalFilename());
    // 2.返回上传的结果
    return uploadReturnPicResultEntity;
}

后端代码:接收整个回报信息数据,并存入redis

@ResponseBody
@RequestMapping("/create/save/return.json")
public ResultEntity<String> saveReturnInfo(ReturnVO returnVO,HttpSession session){
    try {
        // 1.从 session 域中读取之前缓存的 ProjectVO 对象
        ProjectVO projectVO = (ProjectVO) session.getAttribute("tempProject");
        // 2.判断 projectVO 是否为 null
        if(projectVO == null){
            return ResultEntity.failed("临时存储的Project对象丢失!");
        }
        // 3.从 projectVO 对象中获取存储回报信息的集合
        List<ReturnVO> returnVOList = projectVO.getReturnVOList();
        // 4.判断 returnVOList 集合是否有效
        if(returnVOList ==null || returnVOList.size() == 0){
            // 5.创建集合对象对 returnVOList 进行初始化
            returnVOList = new ArrayList<>();
            // 6.为了让以后能够正常使用这个集合,设置到 projectVO 对象中
            projectVO.setReturnVOList(returnVOList);
        }
        // 7.将收集了表单数据的 returnVO 对象存入集合
        returnVOList.add(returnVO);
        // 8.把数据有变化的 ProjectVO 对象重新存入 Session 域,以确保新的数据最终能够存入 Redis
        session.setAttribute("tempProject",projectVO);
        // 9.所有操作成功完成返回成功
        return ResultEntity.successWithoutData();
    } catch (Exception e) {
        e.printStackTrace();
        return ResultEntity.failed(e.getMessage());
    }
}

⑥页面上修改 “ 下一步 ”按钮,从收集回报信息页面跳转到确认信息页面

<a th:href="@{/project/create/confirm/page}" class="btn btn-warning btn-lg">下一步</a>
view-controller:
registry.addViewController("/create/confirm/page").setViewName("project-confirm");

调整project-confirm.html页面

修改提交按钮的HTML标签:
<button type="button" id="submitBtn" class="btn btn-warning btn-lg">提交</button>
调整表单代码:
<form id="confirmFomr" th:action="@{/project/create/confirm}" method="post" role="form">
    <div class="form-group">
        <label for="exampleInputEmail1">易付宝企业账号:</label>
        <input type="email" name="paynum" class="form-control" id="exampleInputEmail1" />
    </div>
    <div class="form-group">
        <label for="exampleInputPassword1">法人身份证号:</label>
        <input type="password" name="cardnum" class="form-control" id="exampleInputPassword1" />
    </div>
</form>
给提交按钮绑定单击响应函数
<script type="text/javascript">
    $(function(){
        $("#submitBtn").click(function(){
            $("#confirmFomr").submit();
        });
    });
</script>

收集表单数据,调用远程接口执行保存

@Autowired
private MySQLRemoteService mySQLRemoteService;
@RequestMapping("/create/confirm")
public String saveConfirmInfo(
        MemberConfirmInfoVO memberConfirmInfoVO,
        HttpSession session,
        ModelMap modelMap){
    // 1.从 session 域中读取之前缓存的 ProjectVO 对象
    ProjectVO projectVO = (ProjectVO) session.getAttribute("tempProject");
    // 2.判断 projectVO 是否为 null
    if(projectVO == null){
        throw new RuntimeException("临时存储的Project对象丢失!");
    }
    // 3.将确认信息数据设置到 projectVO 对象中
    projectVO.setMemberConfirmInfoVO(memberConfirmInfoVO);
    System.out.println(projectVO);
    // 4.从 Session 域读取当前登录的用户
    MemberLoginVO memberLoginVO = (MemberLoginVO) session.getAttribute("loginMember");
    // 5.登录用户的Id
    Integer memberId = memberLoginVO.getId();
    // 6.调用远程方法保存 projectVO 对象
    ResultEntity<String> saveResultEntity =
            mySQLRemoteService.saveProjectVORemote(projectVO, memberId);
    // 7.判断远程的保存操作是否成功
    String result = saveResultEntity.getResult();
    if(ResultEntity.FAILED.equals(result)) {
        modelMap.addAttribute("message",saveResultEntity.getMessage());
        return "project-confirm";
    }
    // 8.将临时的 ProjectVO 对象从 Session 域移除
    session.removeAttribute("tempProject");
    // 9.如果远程保存成功则跳转到最终完成页面
    return "redirect:http://localhost:80/project/create/success";
}

⑦执行保存的远程调用接口方法

1)声明 mysql-provider 的  Feign 接口(api工程MySQLRemoteService接口)

// @FeignClient注解表示当前接口和一个Provider对应
//      注解中value属性指定要调用的Provider的微服务名称
@FeignClient(value = "atguigu-crowd-mysql")
public interface MySQLRemoteService {
    // member登陆时,根据 loginAcct 查询 Member 对象
    @RequestMapping("/get/memberpo/by/loginacct/remote")
    ResultEntity<MemberPO> getMemberPOByLoginAcctRemote(@RequestParam("loginacct") String loginacct);

    @RequestMapping("/save/member/remote")
    public ResultEntity<String> saveMemberRemote(@RequestBody MemberPO memberPO);

    @RequestMapping("/save/project/vo/remote")
    public ResultEntity<String> saveProjectVORemote(@RequestBody ProjectVO projectVO,@RequestParam("memberId") Integer memberId);
}

2)在 mysql-provider 中执行具体操作

handler方法:

@RestController
public class ProjectProviderHandler {
    @Autowired
    private ProjectService projectService;
    @RequestMapping("/save/project/vo/remote")
    public ResultEntity<String> saveProjectVORemote(
            @RequestBody ProjectVO projectVO,
            @RequestParam("memberId") Integer memberId) {
        try {
            // 调用"本地"Service执行保存
            projectService.saveProject(projectVO,memberId);
            return ResultEntity.successWithoutData();
        } catch (Exception e) {
            e.printStackTrace();
            return ResultEntity.failed(e.getMessage());
        }
    }
}

service方法:

@Transactional(readOnly = true)
@Service
public class ProjectServiceImpl implements ProjectService {
    @Autowired
    private ProjectPOMapper projectPOMapper;
    @Autowired
    private ProjectItemPicPOMapper projectItemPicPOMapper;
    @Autowired
    private MemberLaunchInfoPOMapper memberLaunchInfoPOMapper;
    @Autowired
    private MemberConfirmInfoPOMapper memberConfirmInfoPOMapper;
    @Autowired
    private ReturnPOMapper returnPOMapper;

    @Transactional(readOnly = false,propagation = Propagation.REQUIRES_NEW,rollbackFor = Exception.class)
    @Override
    public void saveProject(ProjectVO projectVO, Integer memberId) {
        // 一、保存ProjectPO对象
        // 1.创建空的ProjectPO对象
        ProjectPO projectPO = new ProjectPO();
        // 2.把ProjectVO中的属性复制到ProjectPO中
        BeanUtils.copyProperties(projectVO,projectPO);
        // 把memberId设置到ProjectPO中
        projectPO.setMemberid(memberId);
        // 生成创建时间存入ProjectPO中
        String createDate = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
        projectPO.setCreatedate(createDate);
        // status设置成0,表示即将开始
        projectPO.setStatus(0);
        // 3.保存ProjectPO
        // 为了能够获取到ProjectPO保存后的自增主键,需要到ProjectPOMapper.xml文件中进行相关设置
        // <insert id="insertSelective" useGeneratedKeys="true" keyProperty="id" ...
        projectPOMapper.insertSelective(projectPO);
        // 4.从projectPO对象这里获取自增主键
        Integer projectId = projectPO.getId();
        // 二、保存项目、分类的关联关系信息
        // 1.从ProjectVO对象中获取typeIdList
        List<Integer> typeIdList = projectVO.getTypeIdList();
        projectPOMapper.insertProjectAndTypeRelationShip(typeIdList,projectId);
        // 三、保存项目、标签的关联关系信息
        List<Integer> tagIdList = projectVO.getTagIdList();
        projectPOMapper.insertProjectAndTagRelationShip(tagIdList,projectId);
        // 四、保存项目中详情图片路径的信息
        List<String> detailPicturePathList = projectVO.getDetailPicturePathList();
        projectItemPicPOMapper.insertPathList(detailPicturePathList,projectId);
        // 五、保存项目发起人信息
        MemberLaunchInfoVO memberLaunchInfoVO = projectVO.getMemberLaunchInfoVO();
        MemberLaunchInfoPO memberLaunchInfoPO = new MemberLaunchInfoPO();
        BeanUtils.copyProperties(memberLaunchInfoVO,memberLaunchInfoPO);
        memberLaunchInfoPO.setMemberid(memberId);
        memberLaunchInfoPOMapper.insert(memberLaunchInfoPO);
        // 六、保存项目回报的信息
        List<ReturnVO> returnVOList = projectVO.getReturnVOList();
        ArrayList<ReturnPO> returnPOList = new ArrayList<>();
        for (ReturnVO returnVO : returnVOList) {
            ReturnPO returnPO = new ReturnPO();
            BeanUtils.copyProperties(returnVO,returnPO);
            returnPOList.add(returnPO);
        }
        returnPOMapper.insertReturnPOBatch(projectId,returnPOList);
        // 七、保存项目的确认信息
        MemberConfirmInfoVO memberConfirmInfoVO = projectVO.getMemberConfirmInfoVO();
        MemberConfirmInfoPO memberConfirmInfoPO = new MemberConfirmInfoPO();
        BeanUtils.copyProperties(memberConfirmInfoVO,memberConfirmInfoPO);
        memberConfirmInfoPO.setMemberid(memberId);
        memberConfirmInfoPOMapper.insert(memberConfirmInfoPO);
    }
}

*Mapper.xml配置文件中相关的SQL代码:

ProjectPOMapper.xml:
项目、分类:
<!--
    void insertProjectAndTypeRelationShip(
        @Param("typeIdList") List<Integer> typeIdList, 
        @Param("projectId") Integer projectId);
-->
<insert id="insertProjectAndTypeRelationShip">
    insert into t_project_type (projectid,typeid) values
    <foreach collection="typeIdList" item="typeId" separator=",">(#{projectId},#{typeId})</foreach>
</insert>
项目、标签:
<!--
    void insertProjectAndTagRelationShip(
        @Param("tagIdList") List<Integer> tagIdList,
        @Param("projectId") Integer projectId);
-->
<insert id="insertProjectAndTagRelationShip">
    insert into t_project_tag (projectid,tagid) values
    <foreach collection="tagIdList" item="tagId" separator=",">(#{projectId},#{tagId})</foreach>
</insert>
ProjectItemPicPOMapper.xml:
详情图片路径:
<!--
    void insertPathList(
       @Param("detailPicturePathList") List<String> detailPicturePathList,
       @Param("projectId") Integer projectId);
-->
<insert id="insertPathList">
    insert into t_project_item_pic (projectid,item_pic_path) values
    <foreach collection="detailPicturePathList" item="detailPicturePath" separator=",">(#{projectId},#{detailPicturePath})</foreach>
</insert>
ReturnPOMapper.xml:
回报信息:
<!--
    void insertReturnPOBatch(
        @Param("projectId") Integer projectId,
        @Param("returnPOList") ArrayList<ReturnPO> returnPOList);
-->
<insert id="insertReturnPOBatch">
    insert into t_return (
        projectid, 
        type,
        supportmoney, 
        content,
        count,
        signalpurchase, 
        purchase, 
        freight,
        invoice,
        returndate,
        describ_pic_path)
    values
    <foreach collection="returnPOList" item="returnPO" separator=",">
           (
                #{projectId},
                #{returnPO.type},
                #{returnPO.supportmoney},
                #{returnPO.content},
                #{returnPO.count},
                #{returnPO.signalpurchase},
                #{returnPO.purchase},
                #{returnPO.freight},
                #{returnPO.invoice},
                #{returnPO.returndate},
                #{returnPO.describPicPath}
           )
    </foreach>
</insert>

3.6 首页显示项目

3.6.1 目标:在首页上加载真实保存到数据库的项目数据,按分类显示

3.6.2 思路:

3.6.3 操作的步骤:

①.创建实体类:PortalTypeVO、PortalProjectVO

②.mysql-provider 微服务暴露数据查询接口

  • ProjectPOMapper.xml中编写查询数据的SQL语句
  • ProjectPOMapper接口声明查询数据的方法
  • ProjectService中调用Mapper的方法拿到数据
  • ProjectHandler中调用Service方法拿到数据

③.api工程声明Feign的接口

④.在auth-consumer中调用mysql-provider暴露的接口拿到数据存入模型

⑤.在portal.html中显示模型中的数据

3.6.4 具体操作:

①.代码:创建实体类

@Data
@NoArgsConstructor
@AllArgsConstructor
public class PortalTypeVO {
    private Integer id;
    private String name;
    private String remark;
    private List<PortalProjectVO> portalProjectVOList;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PortalProjectVO {
    private Integer id;
    // 头图路径
    private String headerPicturePath;
    // 项目名称
    private String projectName;
    // 目标金额
    private Integer money;
    // 发布的时间
    private String deploydate;
    // 支持的人数
    private Integer supporter;
    // 完成的百分比
    private Integer percentage;
}

②.mysql-provider 微服务暴露数据查询接口

1)ProjectPOMapper.xml和ProjectPOMapper中的相关代码:

<!--ProjectPOMapper.xml:-->
<resultMap id="LoadPortalProjectListResultMap" type="com.atguigu.crowd.entity.vo.PortalTypeVO">
    <!--分类数据的常规属性-->
    <id column="id" property="id" jdbcType="INTEGER"/>
    <result column="name" property="name"/>
    <result column="remark" property="remark"/>
    <!--分类数据中包含的项目数据的List-->
    <!--property属性:对应PortalTypeVO中分类数据中的项目数据的List-->
    <!--column属性:接下来查询项目时需要用到分类id,就是通过column属性把值传入-->
    <!--ofType属性:项目数据的实体类型PortalProjectVO-->
    <collection
            property="portalProjectVOList"
            column="id"
            ofType="com.atguigu.crowd.entity.vo.PortalProjectVO"
            select="com.atguigu.crowd.mapper.ProjectPOMapper.selectPortalProjectVOList"/>
</resultMap>
<select id="selectPortalTypeVOList" resultMap="LoadPortalProjectListResultMap">
    select id,name,remark from t_type
</select>
<select id="selectPortalProjectVOList" resultType="com.atguigu.crowd.entity.vo.PortalProjectVO">
    select
           t_project.id projectId,
           project_name projectName,
           money,
           deploydate deployDate,
           supporter,
           header_picture_path headerPicturePath,
           supportmoney/money*100 percentage
    from t_project left  join t_project_type
    on t_project.id = t_project_type.projectid
    where t_project_type.typeid = #{id}
    order by t_project.id desc
    limit 0,4
</select>
<!--ProjectPOMapper-->
List<PortalTypeVO> selectPortalTypeVOList();

2)handler、service方法

projectService:
List<PortalTypeVO> getPortalTypeVOList();
projectServiceImpl:
@Override
public List<PortalTypeVO> getPortalTypeVOList() {
    return projectPOMapper.selectPortalTypeVOList();
}
handler:
@RequestMapping("/select/portal/type/project/data")
public ResultEntity<List<PortalTypeVO>> getPortalTypeProjectDataRemote(){
    try {
        List<PortalTypeVO> portalTypeVOList = projectService.getPortalTypeVOList();
        return ResultEntity.successWithData(portalTypeVOList);
    } catch (Exception e) {
        e.printStackTrace();
        return ResultEntity.failed(e.getMessage());
    }
}

③.api工程声明Feign的接口

@RequestMapping("/select/portal/type/project/data")
public ResultEntity<List<PortalTypeVO>> getPortalTypeProjectDataRemote();

④.在auth-consumer中调用mysql-provider暴露的接口拿到数据存入模型

@Controller
public class PortalHandler {
    @Autowired
    private MySQLRemoteService mySQLRemoteService;
    @RequestMapping("/")
    public String showPortalPage(ModelMap modelMap){
        // 这里实际开发中需要加载数据
        // 1.调用MySQLRemoteService提供的方法查询首页要显示的数据
        ResultEntity<List<PortalTypeVO>> resultEntity = mySQLRemoteService.getPortalTypeProjectDataRemote();
        // 2.检查查询结果
        String result = resultEntity.getResult();
        if(ResultEntity.SUCCESS.equals(result)){
            // 3.获取查询结果的数据
            List<PortalTypeVO> portalTypeVOList = resultEntity.getData();
            // 4.存入模型
            modelMap.addAttribute("portal_data",portalTypeVOList);
        }
        return "portal";
    }
}

⑤.在portal.html中显示模型中的数据

使用双层循环遍历数据:
<div th:if="${#strings.isEmpty(portal_data)}">没有加载到任何分类的信息</div>
<div th:if="${not #strings.isEmpty(portal_data)}">
    <div th:each="portalType : ${portal_data}" class="container">
        <div class="row clearfix">
            <div class="col-md-12 column">
                <div class="box ui-draggable" id="mainBox">
                    <div class="mHd" style="">
                        <div class="path">
                            <a href="projects.html">更多...</a>
                        </div>
                        <h3>
                            <label th:text="${portalType.name}">科技</label> <small th:text="${portalType.remark}" style="color:#FFF;">开启智慧未来</small>
                        </h3>
                    </div>
                    <div class="mBd" style="padding-top:10px;">
                        <div class="row">
                            <div th:if="${#strings.isEmpty(portalType.portalProjectVOList)}">该分类下还没有任何项目</div>
                            <div th:if="${not #strings.isEmpty(portalType.portalProjectVOList)}">
                                <div th:each="project : ${portalType.portalProjectVOList}" class="col-md-3">
                                    <div class="thumbnail">
                                        <img alt="300x200" th:src="${project.headerPicturePath}" src="img/product-1.jpg"/>
                                        <div class="caption">
                                            <h3 class="break">
                                                <a th:href="'http://localhost:80/project/show/project/detail/' + ${project.projectId}" href="project.html" th:text="${project.projectName}">活性富氢净水直饮机</a>
                                            </h3>
                                            <p>
                                            <div style="float:left;"><i class="glyphicon glyphicon-screenshot"
                                                                        title="目标金额"></i> $<span th:text="${project.money}">20,000</span>
                                            </div>
                                            <div style="float:right;"><i title="截至日期"
                                                                         class="glyphicon glyphicon-calendar"></i>
                                                <span th:text="${project.deployDate}">2017-20-20</span>
                                            </div>
                                            </p>
                                            <br>
                                            <div class="progress" style="margin-bottom: 4px;">
                                                <div class="progress-bar progress-bar-success" role="progressbar"
                                                     th:aria-valuenow="${project.percentage}" aria-valuenow="40" aria-valuemin="0" aria-valuemax="100"
                                                     th:style="'width: '+${project.percentage}+'%'" style="width: 40%">
                                                    <span th:text="${project.percentage}+'% '">40% </span>
                                                </div>
                                            </div>
                                            <div><span style="float:right;"><i
                                                    class="glyphicon glyphicon-star-empty"></i></span> <span><i
                                                    class="glyphicon glyphicon-user" title="支持人数"></i> <span th:text="${project.supporter}">12345</span></span></div>
                                        </div>
                                    </div>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>

            </div>
        </div>
    </div>
</div>

3.7 点击项目名字显示项目详情

3.7.1 目标:点击项目名字跳转到项目详情页面显示项目数据

3.7.2 思路:

3.7.3 操作的步骤:

①.创建实体类:DetailProjectVO、DetailReturnVO

②.mysql-provider 微服务暴露数据查询接口

  • ProjectPOMapper.xml中编写查询数据的SQL语句
  • ProjectPOMapper接口声明查询数据的方法
  • ProjectService中调用Mapper的方法拿到数据
  • ProjectHandler中调用Service方法拿到数据

③.api工程声明Feign的接口

④.在project-consumer中调用mysql-provider暴露的接口拿到数据存入模型

⑤.在portal.html中显示模型中的数据

3.7.4 具体操作:

①.代码:创建实体类

@Data
@NoArgsConstructor
@AllArgsConstructor
public class DetailProjectVO {
    // 项目ID
    private Integer projectId;
    // 项目名称
    private String projectName;
    // 项目描述
    private String projectDesc;
    // 关注人数
    private Integer followerCount;
    // 0-即将开始,1-众筹中,2-众筹成功,3-众筹失败
    private Integer status;
    private String statusText;
    // 需要筹集的金额
    private Integer money;
    // 已筹集金额
    private Integer supportMoney;
    // 筹集百分比
    private Integer percentage;
    // 项目发起时间
    private String deployDate;
    // 剩余多少时间
    private Integer lastDate;
    // 支持的人数
    private Integer supporterCount;
    // 头图的路径
    private String headerPicturePath;
    // 详情图片的路径
    private List<String> detailPicturePathList;
    // 回报的信息
    private List<DetailReturnVO> detailReturnVOList;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class DetailReturnVO {
    // 回报信息主键
    private Integer returnId;
    // 当前档位需支持的金额
    private Integer supportMoney;
    // 单笔限购,0 表示不限购,1 表示限购
    private Integer signalPurchase;
    // 具体限额数量
    private Integer purchase;
    // 当前该档位的支持数量
    private Integer supportCount;
    // 运费,“0”为包邮
    private Integer freight;
    // 众筹成功后多少天发货
    private Integer returnDate;
    // 回报内容
    private String content;    
}

②.mysql-provider 微服务暴露数据查询接口 

1)ProjectPOMapper.xml和ProjectPOMapper中的相关代码:

<!--ProjectPOMapper.xml文件中使用级联查询的方式:-->
<resultMap id="loadProjectDetailResultMap" type="com.atguigu.crowd.entity.vo.DetailProjectVO">
    <id column="id" property="projectId"/>
    <result column="project_name" property="projectName"/>
    <result column="project_description" property="projectDesc"/>
    <result column="money" property="money"/>
    <result column="status" property="status"/>
    <result column="day" property="day"/>
    <result column="deploydate" property="deployDate"/>
    <result column="supportmoney" property="supportMoney"/>
    <result column="supporter" property="supporterCount"/>
    <result column="follower" property="followerCount"/>
    <result column="header_picture_path" property="headerPicturePath"/>
    <collection
            property="detailPicturePathList"
            column="id"
            select="com.atguigu.crowd.mapper.ProjectPOMapper.selectDetailPicturePath"/>
    <collection
            property="detailReturnVOList"
            column="id"
            select="com.atguigu.crowd.mapper.ProjectPOMapper.selectDetailReturnVO"/>
</resultMap>
<select id="selectDetailProjectVO" resultMap="loadProjectDetailResultMap">
    select
           id,
           project_name,
           project_description,
           money,
           status,
           day,
           deploydate,
           supportmoney,
           supporter,
           supportmoney/money*100 percentage,
           follower,
           header_picture_path
    from t_project
    where id = #{projectId}
</select>
<select id="selectDetailPicturePath" resultType="string">
    SELECT item_pic_path FROM t_project_item_pic WHERE projectid=#{id}
</select>
<select id="selectDetailReturnVO" resultType="com.atguigu.crowd.entity.vo.DetailReturnVO">
    select
        id returnId,
        supportmoney supportMoney,
        content,
        signalpurchase signalPurchase,
        purchase,
        freight,
        returndate returnDate
    from t_return
    where projectid=#{id}
</select>
<!--ProjectPOMapper:-->
DetailProjectVO selectDetailProjectVO(@Param("projectId") Integer projectId);

2)handler、service方法

projectService:
DetailProjectVO getDetailProjectVO(Integer projectId);
projectServiceImpl:
@Override
public DetailProjectVO getDetailProjectVO(Integer projectId) {
    // 1.查询得到 DetailProjectVO 对象
    DetailProjectVO detailProjectVO = projectPOMapper.selectDetailProjectVO(projectId);
    // 2.根据status确定statusText
    Integer status = detailProjectVO.getStatus();
    switch (status){
        case 0:
            detailProjectVO.setStatusText("审核中");
            break;
        case 1:
            detailProjectVO.setStatusText("众筹中");
            break;
        case 2:
            detailProjectVO.setStatusText("众筹成功");
            break;
        case 3:
            detailProjectVO.setStatusText("已关闭");
        default:
            break;
    }
    // 3.根据deployDate计算lastDay
    // 2022-09-13
    String deployDate = detailProjectVO.getDeployDate();
    // 获取当前日期
    Date currentDay = new Date();
    // 把众筹日期解析成Date类型
    SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
    try {
        Date deployDay = dateFormat.parse(deployDate);
        // 获取当前日期的时间戳
        long currentTimeStamp = currentDay.getTime();
        // 获取众筹日期的时间戳
        long deployTimeStamp = deployDay.getTime();
        // 两个时间戳相减计算当前已经过去的时间
        long pastDays = (currentTimeStamp - deployTimeStamp) / 1000 / 60 / 60 / 24;
        // 获取总的众筹参数
        Integer totalDays = detailProjectVO.getDay();
        // 使用总的众筹天数减去已经过去的天数得到剩余天数
        Integer lastDay = (int) (totalDays - pastDays);
        detailProjectVO.setLastDate(lastDay);
    } catch (ParseException e) {
        e.printStackTrace();
    }
    return detailProjectVO;
}
handler:
@RequestMapping("/get/project/detail/remote/{projectId}")
public ResultEntity<DetailProjectVO> getDetailProjectVORemote(@PathVariable("projectId") Integer projectId){
    try {
        DetailProjectVO detailProjectVO = projectService.getDetailProjectVO(projectId);
        return ResultEntity.successWithData(detailProjectVO);
    } catch (Exception e) {
        e.printStackTrace();
        return ResultEntity.failed(e.getMessage());
    }
}

③.api工程声明Feign的接口

@RequestMapping("/get/project/detail/remote/{projectId}")
public ResultEntity<DetailProjectVO> getDetailProjectVORemote(@PathVariable("projectId") Integer projectId);

④.在project-consumer中调用mysql-provider暴露的接口拿到数据存入模型

项目的起点在auth-consumer工程的portal.html中:
<a th:href="'http://localhost:80/project/show/project/detail/' + ${project.projectId}" href="project.html" th:text="${project.projectName}">活性富氢净水直饮机</a>

@RequestMapping("/show/project/detail/{projectId}")
public String showProjectDetail(@PathVariable("projectId") Integer projectId,ModelMap modelMap){
    ResultEntity<DetailProjectVO> resultEntity = mySQLRemoteService.getDetailProjectVORemote(projectId);
    String result = resultEntity.getResult();
    if(ResultEntity.SUCCESS.equals(result)){
        // 获取查询结果的数据
        DetailProjectVO detailProjectVO = resultEntity.getData();
        // 存入模型
        modelMap.addAttribute("detailProjectVO",detailProjectVO);
    }
    return "project-detail";
}

⑤.在project-detail中显示模型中的数据

<div th:if="${#strings.isEmpty(detailProjectVO)}">查询项目详情信息失败!</div>
<div th:if="${not #strings.isEmpty(detailProjectVO)}">
    <div class="container">
        <div class="row clearfix">
            <div class="col-md-12 column">
                <div class="jumbotron nofollow" style="    padding-top: 10px;">
                    <h3 th:text="${detailProjectVO.projectName}">酷驰触控龙头,智享厨房黑科技</h3>
                    <div style="float:left;width:70%;" th:text="${detailProjectVO.projectDesc}">
                        智能时代,酷驰触控厨房龙头,让煮妇解放双手,触发更多烹饪灵感,让美味信手拈来。
                    </div>
                    <div style="float:right;">
                        <button type="button" class="btn btn-default">
                            <i style="color:#f60" class="glyphicon glyphicon-heart"></i> 关注[[${detailProjectVO.followerCount}]]
                        </button>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <div class="container">
        <div class="row clearfix">
            <div class="col-md-12 column">
                <div class="row clearfix">
                    <div class="col-md-8 column" th:if="${#strings.isEmpty(detailProjectVO.detailPicturePathList)}">加载图片详情信息失败</div>
                    <div class="col-md-8 column" th:if="${not #strings.isEmpty(detailProjectVO.detailPicturePathList)}">
                        <img alt="140x140" width="740" src="img/product_detail_head.jpg" th:src="${detailProjectVO.headerPicturePath}"/>
                        <img alt="140x140" width="740" src="img/product_detail_body.jpg" th:each="detailProjectPath:${detailProjectVO.detailPicturePathList}" th:src="${detailProjectPath}"/>
                    </div>
                    <div class="col-md-4 column">
                        <div class="panel panel-default" style="border-radius: 0px;">
                            <div class="panel-heading" style="background-color: #fff;border-color: #fff;">
                                <span class="label label-success"><i class="glyphicon glyphicon-tag"></i> [[${detailProjectVO.statusText}]]</span>
                            </div>
                            <div class="panel-body">
                                <h3>已筹资金:¥[[${detailProjectVO.supportMoney}]]</h3>
                                <p><span>目标金额 : [[${detailProjectVO.money}]]</span><span style="float:right;">达成 : [[${detailProjectVO.percentage}]]%</span></p>
                                <div class="progress" style="height:10px; margin-bottom: 5px;">
                                    <div class="progress-bar progress-bar-success" role="progressbar" th:aria-valuenow="${detailProjectVO.percentage}" aria-valuenow="60"
                                         aria-valuemin="0" aria-valuemax="100" th:style="'width:' + ${detailProjectVO.percentage} + '%'" style="width: 60%;"></div>
                                </div>
                                <p>剩余 [[${detailProjectVO.day}]] 天</p>
                                <div>
                                    <p><span>已有[[${detailProjectVO.supporterCount}]]人支持该项目</p>
                                    <button type="button" class="btn  btn-warning btn-lg btn-block" data-toggle="modal"
                                            data-target="#myModal">立即支持
                                    </button>
                                </div>
                            </div>
                            ......
                            ......
                        </div>
                        <div th:if="${#strings.isEmpty(detailProjectVO.detailReturnVOList)}">没有找到项目回报信息</div>
                        <div th:if="${not #strings.isEmpty(detailProjectVO.detailReturnVOList)}">
                            <div th:each="detailReturnVO : ${detailProjectVO.detailReturnVOList}" class="panel panel-default" style="border-radius: 0px;">
                                <div class="panel-heading">
                                    <h3>
                                        ¥[[${detailReturnVO.supportMoney}]]
                                        <span th:if="${detailReturnVO.signalPurchase==0}" style="float:right;font-size:12px;">无限额,447位支持者</span>
                                        <span th:if="${detailReturnVO.signalPurchase==1}" style="float:right;font-size:12px;">限额[[${detailReturnVO.purchase}]]位,剩余465位</span>
                                    </h3>
                                </div>
                                <div class="panel-body">
                                    <p th:if="${detailReturnVO.freight==0}">配送费用:包邮</p>
                                    <p th:if="${detailReturnVO.freight>0}">配送费用:[[${detailReturnVO.freight}]]</p>
                                    <p>预计发放时间:项目筹款成功后的[[${detailReturnVO.returnDate}]]天内</p>
                                    <button type="button" class="btn  btn-warning btn-lg"
                                            onclick="window.location.href='pay-step-1.html'">支持
                                    </button>
                                    <br><br>
                                    <p th:text="${detailReturnVO.content}">感谢您的支持,在众筹开始后,您将以79元的优惠价格获得“遇见彩虹?”智能插座一件(参考价208元)。</p>
                                </div>
                            </div>
                        </div>
                        ......
                        ......

                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

3.8 订单工程

3.8.1 搭建order-consumer环境(参照project-consumer)

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--为了引入feign和entity实体类-->
<dependency>
    <groupId>com.atguigu.crowd</groupId>
    <artifactId>shangcouwang11-member-api</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>
<!--整合thymeleaf-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!--引入SpringBoot测试的依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
<!--类似于热部署功能-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-loader</artifactId>
</dependency>
<!--使用Session共享引入的依赖-->
<!-- 引入 springboot&redis 整合场景 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 引入 springboot&springsession 整合场景 -->
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>

注意:不要忘了在zuul里面添加order-consumer对应的路由规则

3.8.2 建模

3.8.2.1 结构

3.8.2.2 物理建模

①订单表

CREATE TABLE `project_crowd`.`t_order`
(
	`id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
	`order_num` CHAR(100) COMMENT '订单号',
	`pay_order_num` CHAR(100) COMMENT '支付宝流水号',
	`order_amount` DOUBLE(10,5) COMMENT '订单金额',
	`invoice` INT COMMENT '是否开发票(0 不开,1 开)',
	`invoice_title` CHAR(100) COMMENT '发票抬头',
	`order_remark` CHAR(100) COMMENT '订单备注',
	`address_id` CHAR(100) COMMENT '收货地址 id',
	PRIMARY KEY (`id`)
);

②收货地址表

CREATE TABLE `project_crowd`.`t_address`
(
	`id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
	`receive_name` CHAR(100) COMMENT '收件人',
	`phone_num` CHAR(100) COMMENT '手机号',
	`address` CHAR(200) COMMENT '收货地址',
	`member_id` INT COMMENT '用户 id',
	PRIMARY KEY (`id`)
);

③项目信息表

CREATE TABLE `project_crowd`.`t_order_project`
(
	`id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
	`project_name` CHAR(200) COMMENT '项目名称',
	`launch_name` CHAR(100) COMMENT '发起人',
	`return_content` CHAR(200) COMMENT '回报内容',
	`return_count` INT COMMENT '回报数量',
	`support_price` INT COMMENT '支持单价',
	`freight` INT COMMENT '配送费用',
	`order_id` INT COMMENT '订单表的主键',
	PRIMARY KEY (`id`)
);

3.8.2.3 逆向工程生成PO对象,并归位

<table tableName="t_order" domainObjectName="OrderPO" />
<table tableName="t_address" domainObjectName="AddressPO" />
<table tableName="t_order_project" domainObjectName="OrderProjectPO" />

3.8.3 确认回报内容

3.8.3.1 思路

3.8.3.2 代码

①操作起点:支持按钮

project-detail.html中:
<a th:href="'http://localhost:80/order/confirm/return/info/'+${detailProjectVO.projectId}+'/'+${detailReturnVO.returnId}"  class="btn btn-warning btn-lg" >支持</a>
注意 1:因为需要从 project-consumer 跳转到 order-consumer,所以要通过域名经过网关进行访问,以保证能够保持会话状态。
注意 2:需要携带项目 id 和回报 id 便于查询数据

②创建OrderProjectVO

@Data
@AllArgsConstructor
@NoArgsConstructor
public class OrderProjectVO implements Serializable {
    private static  final  long SerialVersionUID = 1L;
    private Integer id;

    private String projectName;

    private String launchName;

    private String returnContent;

    private Integer returnCount;

    private Integer supportPrice;

    private Integer freight;

    private Integer orderId;
    
    private Integer signalpurchase;

    private Integer purchase;
}

③创建handler方法,调用接口

@Autowired
private MySQLRemoteService mySQLRemoteService;
@RequestMapping("/confirm/return/info/{projectId}/{returnId}")
public String showReturnConfirmInfo(
        @PathVariable("projectId") Integer projectId,
        @PathVariable("returnId") Integer returnId,
        HttpSession session){
    ResultEntity<OrderProjectVO> resultEntity = mySQLRemoteService.getOrderProjectVORemote(projectId,returnId);
    if(ResultEntity.SUCCESS.equals(resultEntity.getResult())){
        OrderProjectVO orderProjectVO = resultEntity.getData();
        // 为了能够在后续操作中保持orderProjectVO数据,存入Session域
        session.setAttribute("orderProjectVO",orderProjectVO);
    }
    return "confirm-return";
}

④实现Feign接口(MysqlRemoteService)

@RequestMapping("/get/order/project/vo/remote")
ResultEntity<OrderProjectVO> getOrderProjectVORemote(@RequestParam("projectId") Integer projectId, @RequestParam("returnId") Integer returnId);

⑤暴露接口

handler代码:
@Autowired
private OrderService orderService;

@RequestMapping("/get/order/project/vo/remote")
public ResultEntity<OrderProjectVO> getOrderProjectVORemote(
        @RequestParam("projectId") Integer projectId,
        @RequestParam("returnId") Integer returnId){
    try {
        OrderProjectVO orderProjectVO = orderService.getOrderProjectVO(projectId,returnId);
        return ResultEntity.successWithData(orderProjectVO);
    } catch (Exception e) {
        e.printStackTrace();
        return ResultEntity.failed(e.getMessage());
    }
}
OrderServiceImpl代码:
@Autowired
private OrderProjectPOMapper orderProjectPOMapper;
@Override
public OrderProjectVO getOrderProjectVO(Integer projectId, Integer returnId) {
    return orderProjectPOMapper.selectOrderProjectVO(returnId);
}
OrderProjectPOMapper代码:
OrderProjectVO selectOrderProjectVO(Integer returnId);
OrderProjectPOMapper.xml中的sql语句:
<select id="selectOrderProjectVO" resultType="com.atguigu.crowd.entity.vo.OrderProjectVO">
    SELECT DISTINCT
        project_name projectName,
        content returnContent,
        `description_simple` launchName,
        t_return.`supportmoney` supportPrice,
        `freight` freight,
        `count` returnCount,
        `signalpurchase` signalPurchase,
        `purchase` purchase
    FROM t_project LEFT JOIN t_member_launch_info
    ON t_project.memberid=t_member_launch_info.memberid
    LEFT JOIN t_return ON t_project.id=t_return.projectid
    WHERE t_return.id=#{returnId}
</select>

⑥完成页面显示:confirm-return.html

<tbody>
<tr>
<td th:text="${session.orderProjectVO.projectName}">活性富氢净水直饮机</td>
<td th:text="${session.orderProjectVO.launchName}">深圳市博实永道电子商务有限公司</td>
<td th:text="${session.orderProjectVO.returnContent}">每满1750人抽取一台活性富氢净水直饮机,至少抽取一台。抽取名额(小数点后一位四舍五入)=参与人数÷1750人,由苏宁官方抽取。</td>
<td><input type="text" class="form-control" style="width:60px;" th:value="${session.orderProjectVO.returnCount}"></td>
<td style="color:#F60" th:text="${session.orderProjectVO.supportPrice}">¥ 1.00</td>
<td th:if="${session.orderProjectVO.freight==0}">免运费</td>
<td th:if="${session.orderProjectVO.freight!=0}" th:text="${session.orderProjectVO.freight}">免运费</td>
</tr>
</tbody>

<p>总价(含运费):<span style="font-size:16px;color:#F60;">¥[[${session.orderProjectVO.returnCount*session.orderProjectVO.supportPrice}]]</span></p>

⑦给回报数量输入框绑定js事件并修改总价

<td><input id="returnCountInput" type="text" class="form-control" style="width:60px;" th:value="${session.orderProjectVO.returnCount}"></td>
                                            
<p>总价(含运费):<span id="totalAmount" style="font-size:16px;color:#F60;">¥[[${session.orderProjectVO.returnCount*session.orderProjectVO.supportPrice}]]</span></p>

var signalPurchase = [[${session.orderProjectVO.signalpurchase}]];
var purchase = [[${session.orderProjectVO.purchase}]];
$("#returnCountInput").change(function () {
    var returnCount = $.trim($(this).val());
    if(returnCount == null || returnCount == ""){
        alert("请输入有效的数据");
        return;
    }
    if(signalPurchase == 1 && returnCount > purchase){
        alert("不能超过限购的数量");
        return;
    }
    var supportPrice = [[${session.orderProjectVO.supportPrice}]]
    $("#totalAmount").text("¥"+supportPrice*returnCount);
});

⑧提交数据页面跳转

<button id="submitBtn" type="button" class="btn btn-warning btn-lg" style="float:right;">
    <i class="glyphicon glyphicon-credit-card"></i> 去结算
</button>

$("#submitBtn").click(function () {
    var returnCount = $("#returnCountInput").val();
    window.location.href="/order/confirm/order/"+returnCount;
});

3.8.4 确认订单

3.8.4.1 思路

 3.8.4.2 代码

①创建AddressVO

@Data
@AllArgsConstructor
@NoArgsConstructor
public class AddressVO {
    private String receiveName;

    private String phoneNum;

    private String address;

    private Integer memberId;
}

②Session域合并回报数量,并查询AddressVO

handler:
@RequestMapping("/confirm/order/{returnCount}")
public String showConfirmOrderInfo(
        @PathVariable("returnCount") Integer returnCount,
        HttpSession session){
    // 1.把接收到的回报数量合并到session域
    OrderProjectVO orderProjectVO = (OrderProjectVO)session.getAttribute("orderProjectVO");
    orderProjectVO.setReturnCount(returnCount);
    session.setAttribute("orderProjectVO",orderProjectVO);
    // 2.获取当前已登录用户的Id
    MemberLoginVO loginMember = (MemberLoginVO) session.getAttribute("loginMember");
    Integer memberId = loginMember.getId();
    // 3.查询现有的收货地址
    ResultEntity<List<AddressVO>> resultEntity = mySQLRemoteService.getAddressVORemote(memberId);
    if(ResultEntity.SUCCESS.equals(resultEntity.getResult())){
        List<AddressVO> list = resultEntity.getData();
        session.setAttribute("addressVOList",list);
    }
    return "confirm-order";
}
feign:
@RequestMapping("/get/address/vo/remote")
ResultEntity<List<AddressVO>> getAddressVORemote(@RequestParam("memberId") Integer memberId);
mysql-provider中:
handler暴露接口:
@RequestMapping("/get/address/vo/remote")
public ResultEntity<List<AddressVO>> getAddressVORemote(
        @RequestParam("memberId") Integer memberId){
    try {
        List<AddressVO> addressVOList = orderService.getAddressVOList(memberId);
        return ResultEntity.successWithData(addressVOList);
    } catch (Exception e) {
        e.printStackTrace();
        return ResultEntity.failed(e.getMessage());
    }
}
serviceImpl:
@Override
public List<AddressVO> getAddressVOList(Integer memberId) {
    AddressPOExample example = new AddressPOExample();
    example.createCriteria().andMemberIdEqualTo(memberId);
    List<AddressPO> addressPOList = addressPOMapper.selectByExample(example);
    List<AddressVO> addressVOList = new ArrayList<>();
    for (AddressPO addressPo : addressPOList) {
        AddressVO addressVO = new AddressVO();
        BeanUtils.copyProperties(addressPo,addressVO);
        addressVOList.add(addressVO);
    }
    return addressVOList;
}

③页面显示

地址:
<div th:if="${#strings.isEmpty(session.addressVOList)}">尚未创建收货地址</div>
<div th:if="${not #strings.isEmpty(session.addressVOList)}">
    <div class="radio" th:each="address : ${session.addressVOList}">
        <label>
            <input type="radio" name="addressId" id="optionsRadios1" th:value="${address.id}" checked>
            [[${address.receiveName}]] [[${address.phoneNum}]] [[${address.address}]]
        </label>
    </div>
</div>
回报信息:
<td th:text="${session.orderProjectVO.projectName}">活性富氢净水直饮机</td>
<td th:text="${session.orderProjectVO.launchName}">深圳市博实永道电子商务有限公司</td>
<td th:text="${session.orderProjectVO.returnContent}">每满1750人抽取一台活性富氢净水直饮机,至少抽取一台。抽取名额(小数点后一位四舍五入)=参与人数÷1750人,由苏宁官方抽取。</td>
<td th:text="${session.orderProjectVO.returnCount}">55</td>
<td style="color:#F60" th:text="${session.orderProjectVO.supportPrice}">¥ 1.00</td>
<td th:if="${session.orderProjectVO.freight==0}">免运费</td>
<td th:if="${session.orderProjectVO.freight!=0}" th:text="${session.orderProjectVO.freight}">免运费</td>

3.8.5 新增收货地址

3.8.5.1 思路:保存新地址后重新进入当前页面

3.8.5.2 代码 

@RequestMapping("/save/address")
public String saveAddress(
        AddressVO addressVO,
        HttpSession session){
    // 1.执行地址信息的保存
    ResultEntity<String> resultEntity = mySQLRemoteService.saveAddressRemote(addressVO);
    // 2.从session域获取OrderProjectVO对象
    OrderProjectVO orderProjectVO = (OrderProjectVO) session.getAttribute("orderProjectVO");
    // 3.从OrderProjectVO获取returnCount
    Integer returnCount = orderProjectVO.getReturnCount();
    // 4.重定向到指定地址,重新进入确认订单页面
    return "redirect:http://localhost:80/order/confirm/order/"+returnCount;
}

3.8.6  控制立即付款按钮是否有效

勾选“我已了解风险和规则”多选框:按钮有效
未勾“我已了解风险和规则”多选框:按钮无效

<li style="margin-top:10px;">
    <button id="payBtn" disabled="disabled" type="button" class="btn btn-warning btn-lg"
            onclick="window.location.href='pay-step-3.html'"><i
            class="glyphicon glyphicon-credit-card"></i> 立即付款
    </button>
</li>
<li style="margin-top:10px;">
    <div class="checkbox">
        <label>
            <input id="knowRoleCheckBox" type="checkbox"> 我已了解风险和规则
        </label>
    </div>
</li>

$("#knowRoleCheckBox").click(function (){
    var currentStatus = this.checked;
    if(currentStatus){
        $("#payBtn").prop("disabled","");
    }else{
        $("#payBtn").prop("disabled","disabled");
    }
});

3.8.7 提交订单表单

3.8.7.1 构造页面不可见的表单

<!--为了收集当前页面中的所有数据,构造空表单-->
<form id="summaryForm" th:action="@{/pay/generate/order}" method="post">
    
</form>

3.8.7.2 给立即付款按钮绑定单击响应函数

$("#payBtn").click(function () {
    // 1.收集所有要提交的表单项的数据
    // 地址id
    var addressId = $("[name=addressId]:checked").val();
    // 是否开发票:0开1不开
    var invoice = $("[name=invoiceRadio]:checked").val();
    // 发票抬头
    var invoiceTitle = $.trim($("[name=invoiceTitle]:checked").val());
    // 备注
    var remark = $.trim($("[name=remark]:checked").val());
    // 2.将上面收集到的表单数据填充到空的表单中并提交
    $("#summaryForm")
        .append("<input type='hidden' name='addressId' value='"+addressId+"'/>")
        .append("<input type='hidden' name='invoice' value='"+invoice+"'/>")
        .append("<input type='hidden' name='invoiceTitle' value='"+invoiceTitle+"'/>")
        .append("<input type='hidden' name='orderRemark' value='"+remark+"'/>")
        .submit();
});

3.9 支付工程

3.9.1 思路

3.9.2 pay-consumer工程基础环境参照order-consumer,在此基础上引入支付接口调用所需环境

<!--支付宝sdk:支付接口调用所需环境-->
<!-- https://mvnrepository.com/artifact/com.alipay.sdk/alipay-sdk-java -->
<dependency>
    <groupId>com.alipay.sdk</groupId>
    <artifactId>alipay-sdk-java</artifactId>
    <version>3.3.49.ALL</version>
</dependency>

3.9.3 创建PayProperties类维护支付接口参数

@Component
@ConfigurationProperties(prefix = "ali.pay")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PayProperties {
    // 应用ID,您的APPID,收款账号既是您的APPID对应支付宝账号(使用沙箱环境的APPID)
    public String appId;
    // 商户私钥,您的PKCS8格式RSA2私钥
    public String merchantPrivateKey;
    // 支付宝公钥,查看地址:https://openhome.alipay.com/platform/keyManage.htm 对应APPID下的支付宝公钥。
    public String alipayPublicKey;
    // 服务器异步通知页面路径  需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
    // 工程公网访问地址使用内网穿透客户端提供的域名:http://489t2i.natappfree.cc:内网穿透的域名
    public String notifyUrl;
    // 页面跳转同步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
    public String returnUrl;
    // 签名方式
    public String signType;
    // 字符编码格式
    public String charset;

    // 支付宝网关(正式环境)
    //public String gatewayUrl = "https://openapi.alipay.com/gateway.do";
    // 支付宝网关(沙箱环境)
    public String gatewayUrl;
}

3.9.4 配置支付参数

ali:
  pay:
    app-id: 2021000121667111
    merchant-private-key: MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDisVYxYHtlvuFfJGG0IlYvff/+JCIc5ayv3UbklCnYT7hFs1QdE3o9YA/3+eLJdQBue2Sgaa4wJODXSxRX8ApBg6F8oLm0GYHZF6+8YHmuA/4BYJSxJvWjIilAm4NknvbPqNmqDE0dorElx4jQ0VTtw+hzYqMTq5+sF7rK8S07AgzU1IxiHVQ2N/SHzOUOSm6JtWslChtQX+nEV91BQB32NJT/eqxWsIMQ6nulqHM8/7ROAmm2qOFlk/W0rmtabFzIteWGkEQUN9jduT4DDH1Kr8Cs47BeBp0zl2orBQMJ55ZFjaBwwesVEmyqMgAe797BxNACs9R9pnAjv9OlJ2XJAgMBAAECggEBANlnwYXxRea6PWIFfj5Hf+hkKpINDToxen/e8xJclhUBv3P5G/4Wo/Ego6/qUvlp4FQUutitAYTimU9gjc4YQ325Q7JGYlK687DD6qH61DdzVLL1cSTEfGdLZ8yyWDyzx3g4MyfGTF7TnJji1++MEqtEazXdrxA6VBOzXk0rJ3miG6IYUXh0nc/Q70cWK7ZjqJjh+Fno+wZLKaN+oQwyEpl3QYCq+ceDrekC8GjGDzm1YdjGPZgNu7nUXYkdLsihqbP8KfcDHNEI7cBYDhwdkjVs/ZuiltsDgXEfwLZZOAYKxgQdrblDLN3o+oTg8Tt4zdII8tDOJHQ0fhtjm1VkXAECgYEA8UNre/gl6S5XhvA7EwC64u5J4fQ0LeQTC6T43Dq+grUgaUjn0heGB2N4rzmLsgwYJXA44i39+gfl3KiGBsuOYoPZ0bcX5IK/Nwy7XQXYjIF8Jqi0ghSD7f3TVN1ToCl5wjsc10FfSFG4/XtBDfA0fHGXCRKHReZnJ/PPZlASCmECgYEA8IoUP5wB+4Zf71O9Ni7MqWMahp340lT+UrLx9TO4iFMS/FHBW15w7Mqvhiew7r6+B+zbcPWysT6xHU9eVjrUdBRRRpztYohn1vkH2yOz6Bs0JKkoozGZo2lwqNI48b8RQWpF1QBUY0BigeEZW8VYb5zGDqZEvu/Gf4hRTlL7pGkCgYAsLBrezLUsN0bhNtSqCwUsjVJLo2l2SX7PL/o8YCkHR2BSxn1jMtlgOu8ard+MzrgRCrXve1o30ABe4SAA2H4OPXPA+NPQC7w0uQkI5Awc1YxEi7jY5CaviTyLGia4eT+It0f1hUuLsyK6jjl/8s25RxbPG2xW+PNEFliPs/NJoQKBgQC5c7vozv84TYHpo0ZeX/arIh1xbJpKj/0FBbJGunmroWEh6GaLa2TlK9/oLvHbIHSi55rInKYIwa0MTAUPtovWc1O2fYcIUOK+e4HzErPCYDbzjPgn2jX6J3EUt//vYsCLDsSIVJi7bQiF2mcSujRU2SpaYRbfnz4LVa5aFOCvAQKBgBfmpAl4IPy3gtrt3j4//3kKKBcEuzt5oWnKPLFHPL4OOJGvmdGZV/yPOMXjKQEWsqkdsOlMj4g/6XXhYxTNKysd4EJ6ZkgXeWtC+PBWZ2a5ABuh4lsicLo1JRc1hvOF6Sbm5qxpGmaE5lJYwGerjx7yAcf71i6zL7uVSFW/JNUZ
    alipay-public-key: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAq2viuLp2UHyQhUk2WFmRs+Q52e0YeSz3WsyegpD7NVvvn9XQjI619NAczbUQprWz6n/DdLFoxYshEBF63y7UxwDJSCKH+S+F1W9oPpqQP7ZhrmB6VP7V7EzyxO90XIoKT37sRa+eXUKBeq1cUhA7dHEdVH3qmF39Mi06825Lqwl35F/vS1dUoCIt/hm9x2F3JfLEArefxMzKi4NeFvZK5KKNDm6+KREmSn8K4L5cFlcRBbgm3oqsv74Tt3goU0tdJNl7WgrxoZD0geLnt6WpCOkezSXTTVk5S0dh0Qnb1Sj4vTXDHr4dJQygaCb9kKhtIIwtfta4AYKrJY+TOq/XmwIDAQAB
    notify-url: http://489t2i.natappfree.cc/pay/notify
    return-url: http://localhost/pay/return
    sign-type: RSA2
    charset: utf-8
    gateway-url: https://openapi.alipaydev.com/gateway.do

3.9.5 提交订单表单见3.8.7

3.9.6 handler方法(位于pay-consumer工程)

  • generateOrder():是保存订单数据的表单方法,在里面调用支付宝接口进行付款操作
  • sendRequestToAlipay():为了调用支付宝接口专门封装的方法,返回的是支付页面
  • returnUrlMethod():支付宝服务器同步通知页面,在里面进行数据库保存数据操作
  • notifyUrlMethod():支付宝服务器异步通知页面
@Slf4j
@Controller
public class PayHandler {
    @RequestMapping("/notify")
    public void notifyUrlMethod(HttpServletRequest request) throws UnsupportedEncodingException, AlipayApiException {
        //支付宝服务器异步通知页面

        //获取支付宝POST过来反馈信息
        Map<String,String> params = new HashMap<>();
        Map<String,String[]> requestParams = request.getParameterMap();
        for (Iterator<String> iter = requestParams.keySet().iterator(); iter.hasNext();) {
            String name = (String) iter.next();
            String[] values = (String[]) requestParams.get(name);
            String valueStr = "";
            for (int i = 0; i < values.length; i++) {
                valueStr = (i == values.length - 1) ? valueStr + values[i]
                        : valueStr + values[i] + ",";
            }
            //乱码解决,这段代码在出现乱码时使用
            valueStr = new String(valueStr.getBytes("ISO-8859-1"), "utf-8");
            params.put(name, valueStr);
        }

        boolean signVerified = AlipaySignature.rsaCheckV1(
                params,
                payProperties.getAlipayPublicKey(),
                payProperties.getCharset(),
                payProperties.getSignType()); //调用SDK验证签名

        //——请在这里编写您的程序(以下代码仅作参考)——

        /*
         * 实际验证过程建议商户务必添加以下校验:
         * 1、需要验证该通知数据中的out_trade_no是否为商户系统中创建的订单号,
         * 2、判断total_amount是否确实为该订单的实际金额(即商户订单创建时的金额),
         * 3、校验通知中的seller_id(或者seller_email) 是否为out_trade_no这笔单据的对应的操作方(有的时候,一个商户可能有多个seller_id/seller_email)
         * 4、验证app_id是否为该商户本身
         */
        if(signVerified) {//验证成功
            //商户订单号
            String out_trade_no = new String(request.getParameter("out_trade_no").getBytes("ISO-8859-1"),"UTF-8");

            //支付宝交易号
            String trade_no = new String(request.getParameter("trade_no").getBytes("ISO-8859-1"),"UTF-8");

            //交易状态
            String trade_status = new String(request.getParameter("trade_status").getBytes("ISO-8859-1"),"UTF-8");

            log.info("out_trade_no" + out_trade_no);
            log.info("trade_no" + trade_no);
            log.info("trade_status" + trade_status);

        }else {//验证失败
            log.info("验证失败!");
            //调试用,写文本函数记录程序运行情况是否正常
            //String sWord = AlipaySignature.getSignCheckContentV1(params);
            //AlipayConfig.logResult(sWord);
        }
    }
    @ResponseBody
    @RequestMapping("/return")
    public String returnUrlMethod(HttpServletRequest request) throws UnsupportedEncodingException, AlipayApiException {
        //支付宝服务器同步通知页面
        //获取支付宝GET过来反馈信息
        Map<String,String> params = new HashMap<>();
        Map<String,String[]> requestParams = request.getParameterMap();
        for (Iterator<String> iter = requestParams.keySet().iterator(); iter.hasNext();) {
            String name = (String) iter.next();
            String[] values = (String[]) requestParams.get(name);
            String valueStr = "";
            for (int i = 0; i < values.length; i++) {
                valueStr = (i == values.length - 1) ? valueStr + values[i]
                        : valueStr + values[i] + ",";
            }
            //乱码解决,这段代码在出现乱码时使用
            valueStr = new String(valueStr.getBytes("ISO-8859-1"), "utf-8");
            params.put(name, valueStr);
        }

        boolean signVerified = AlipaySignature.rsaCheckV1(
                params,
                payProperties.getAlipayPublicKey(),
                payProperties.getCharset(),
                payProperties.getSignType()); //调用SDK验证签名

        //——请在这里编写您的程序(以下代码仅作参考)——
        if(signVerified) {
            // 商户订单号
            String orderNum = new String(request.getParameter("out_trade_no").getBytes("ISO-8859-1"),"UTF-8");

            // 支付宝交易号
            String payOrderNum = new String(request.getParameter("trade_no").getBytes("ISO-8859-1"),"UTF-8");

            // 付款金额
            String orderAmount = new String(request.getParameter("total_amount").getBytes("ISO-8859-1"),"UTF-8");

            // 保存到数据库
            // ...
            return "trade_no:"+payOrderNum+"<br/>out_trade_no:"+orderNum+"<br/>total_amount:"+orderAmount;
        }else {
            // 页面显示信息,验签失败
            return "验签失败!";
        }
    }

    // 这里必须加@ResponseBody注解,让当前方法的返回值作为响应体,在浏览器界面上显示支付宝支付界面
    @ResponseBody
    @RequestMapping("/generate/order")
    public String generateOrder(HttpSession session, OrderVO orderVO) throws AlipayApiException {
        // 1.从session域获取OrderProjectVO对象
        OrderProjectVO orderProjectVO = (OrderProjectVO) session.getAttribute("orderProjectVO");
        // 2.将OrderProjectVO对象和OrderVO对象组装到一起
        orderVO.setOrderProjectVO(orderProjectVO);
        // 3.生成订单号并设置到 orderVO 对象中
        // ①根据当前日期时间生成字符串
        String time = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
        // ②使用 UUID 生成用户 ID 部分
        String user = UUID.randomUUID().toString().replace("-", "").toUpperCase();
        // ③组装
        String orderNum = time + user;
        // ④设置到 OrderVO 对象中
        orderVO.setOrderNum(orderNum);
        // 4.计算订单总金额并设置到 orderVO 对象中
        Integer returnCount = orderProjectVO.getReturnCount();
        Integer supportPrice = orderProjectVO.getSupportPrice();
        Integer freight = orderProjectVO.getFreight();
        Double orderAmount = (double) (returnCount * supportPrice + freight);
        orderVO.setOrderAmount(orderAmount);
        // 5.调用专门封装好的方法给支付宝接口发送请求
        return sendRequestToAlipay(orderNum,orderAmount,orderProjectVO.getProjectName(),orderProjectVO.getReturnContent());
    }

    @Autowired
    private PayProperties payProperties;

    /**
     * 为了调用支付宝接口专门封装的方法
     * @param outTradeNo    外部订单号,也就是商户的订单号,我们自己生成的
     * @param totalAmount   订单总金额
     * @param subject       订单的标题,这里可以使用项目的名称
     * @param body          商品的描述,这里可以使用回报的描述
     * @return              返回到页面上显示的支付宝登录的界面
     * @throws AlipayApiException
     */
    private String sendRequestToAlipay(
            // 商户订单号
            String outTradeNo,
            // 付款金额
            Double totalAmount,
            // 订单名称
            String subject,
            //商品描述
            String body) throws AlipayApiException {
        //获得初始化的AlipayClient
        AlipayClient alipayClient = new DefaultAlipayClient(
                payProperties.getGatewayUrl(),
                payProperties.getAppId(),
                payProperties.getMerchantPrivateKey(),
                "json",
                payProperties.getCharset(),
                payProperties.getAlipayPublicKey(),
                payProperties.getSignType());

        //设置请求参数
        AlipayTradePagePayRequest alipayRequest = new AlipayTradePagePayRequest();
        alipayRequest.setReturnUrl(payProperties.getReturnUrl());
        alipayRequest.setNotifyUrl(payProperties.getNotifyUrl());


        alipayRequest.setBizContent("{\"out_trade_no\":\""+ outTradeNo +"\","
                + "\"total_amount\":\""+ totalAmount +"\","
                + "\"subject\":\""+ subject +"\","
                + "\"body\":\""+ body +"\","
                + "\"product_code\":\"FAST_INSTANT_TRADE_PAY\"}");

        //若想给BizContent增加其他可选请求参数,以增加自定义超时时间参数timeout_express来举例说明
        //alipayRequest.setBizContent("{\"out_trade_no\":\""+ out_trade_no +"\","
        //		+ "\"total_amount\":\""+ total_amount +"\","
        //		+ "\"subject\":\""+ subject +"\","
        //		+ "\"body\":\""+ body +"\","
        //		+ "\"timeout_express\":\"10m\","
        //		+ "\"product_code\":\"FAST_INSTANT_TRADE_PAY\"}");
        //请求参数可查阅【电脑网站支付的API文档-alipay.trade.page.pay-请求参数】章节

        //请求
        return alipayClient.pageExecute(alipayRequest).getBody();
    }
}

3.9.7 把订单信息保存到数据库

①思路

主要代码: 

// 设置事务
@Transactional(
        propagation = Propagation.REQUIRES_NEW,
        rollbackFor = Exception.class,
        readOnly = false)
@Override
public void saveOrder(OrderVO orderVO) {
    OrderPO orderPO = new OrderPO();
    BeanUtils.copyProperties(orderVO,orderPO);
    OrderProjectPO orderProjectPO = new OrderProjectPO();
    BeanUtils.copyProperties(orderVO.getOrderProjectVO(),orderProjectPO);
    // 保存orderPO时自动生成的主键是orderProjectPO需要用到的外键
    // 为了能够获取到orderPO保存后的自增主键,需要到orderPOMapper.xml文件中进行相关设置
    // <insert id="insert" useGeneratedKeys="true" keyProperty="id" ...
    orderPOMapper.insert(orderPO);
    // 从orderPO中获取id
    Integer id = orderPO.getId();
    orderProjectPO.setOrderId(id);
    orderProjectPOMapper.insert(orderProjectPO);
}

4. 第三方接口

4.1 会员注册-发送短信

首先去云市场,找到短信接口,然后根据下面的“api接口提示”进行操作。

【三网106短信】短信接口-短信验证码-短信通知-会员短信群发-短信平台API接口-行业短信_支持携号转网_自定义签名和模板【最新版】_电商_API_生活服务-云市场-阿里云 (aliyun.com)

4.1.1 创建short-message工程

<groupId>com.atguigu.crowd</groupId>
<artifactId>pro01-short-message</artifactId>
<version>1.0-SNAPSHOT</version>

4.1.2 独立测试使用

public class ShortMessageTest {
    public static void main(String[] args) {
        // 短信接口调用的url地址
        String host = "https://gyytz.market.alicloudapi.com";

        // 具体发送短信功能的地址
        String path = "/sms/smsSend";

        // 请求方式
        String method = "POST";

        // 登录到阿里云,进入控制台,找到已购买的短信接口的 appcode
        String appcode = "8ad93a42798f4ab982137fba11d8fbb1";

        // 用headers去封装appcode,最后在headers中的格式(中间是英文空格)是:Authorization:APPCODE 8ad93a42798f4ab982137fba11d8fbb1
        HashMap<String, String> headers = new HashMap<>();
        headers.put("Authorization","APPCODE " + appcode);

        // 封装其他参数
        HashMap<String, String> querys = new HashMap<>();
        // 要发送的验证码,也就是模板中会变化的部分
        querys.put("mobile","18336078792");
        // 收短信的手机号
        querys.put("param","**code**:12345,**minute**:5");
        // 签名的编号,测试使用,如果想自己定义,需要找客服申请
        querys.put("smsSignId","2e65b1bb3d054466b82f0c9d125465e2");
        // 模板的编号,测试使用,如果想自己定义,需要找客服申请
        querys.put("templateId","908e94ccf08b4476ba6c876d13f084ad");
        Map<String, String> bodys = new HashMap<String, String>();

        try {
            /**
             * 重要提示如下:HttpUtils请从
             * https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/src/main/java/com/aliyun/api/gateway/demo/util/HttpUtils.java
             * 或者直接下载:http://code.fegine.com/HttpUtils.zip下载
             *
             * 相应的依赖请参照
             * http://github.com/aliyun/api-gateway-demo-sign-java/blob/master/pom.xml
             * 相关jar包(非pom)直接下载:http://code.fegine.com/aliyun-jar.zip下载
             */
            HttpResponse response = HttpUtils.doPost(host, path, method, headers, querys,bodys);
            // System.out.println(response.toString());如不输出json,请打开这行代码,打印调试头部状态码
            // 状态码:200正常,400 URL无效,401 appcode错误,403 次数用完,500 api网关错误
            // 获取response的body
            System.out.println(EntityUtils.toString(response.getEntity()));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

4.1.3 引入的依赖

<!--相应的依赖请参照:http://github.com/aliyun/api-gateway-demo-sign-java/blob/master/pom.xml-->
<dependencies>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.15</version>
    </dependency>
    <dependency>
        <groupId>org.apache.httpcomponents</groupId>
        <artifactId>httpclient</artifactId>
        <version>4.2.1</version>
    </dependency>
    <dependency>
        <groupId>org.apache.httpcomponents</groupId>
        <artifactId>httpcore</artifactId>
        <version>4.2.1</version>
    </dependency>
    <dependency>
        <groupId>commons-lang</groupId>
        <artifactId>commons-lang</artifactId>
        <version>2.6</version>
    </dependency>
    <dependency>
        <groupId>org.eclipse.jetty</groupId>
        <artifactId>jetty-util</artifactId>
        <version>9.3.7.v20160115</version>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.5</version>
        <scope>test</scope>
    </dependency>
</dependencies>

4.1.4 测试结果

4.1.5 尚硅谷签名的模板(已失效)

  • 签名编号:151003
  • 模板编号:84683

4.1.6 拿到项目中(authentication-consumer)使用短信功能

①.在项目中(authentication-consumer)引入实现短信功能所需要的依赖

<!--以下是发送短信时调用第三方API所需依赖-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.15</version>
</dependency>
<!--当前环境本身就有httpclient和jetty-util的依赖,这里不写版本号就能够使用已有的版本,避免因为版本号不一致而产生冲突-->
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
</dependency>
<dependency>
    <groupId>org.eclipse.jetty</groupId>
    <artifactId>jetty-util</artifactId>
</dependency>

②.把上面的HttpUtils类加入到该工程中,并测试,通过。

@RunWith(SpringRunner.class)
@SpringBootTest
public class CrowdTest {
    @Test
    public void testSendMessage(){
        // 短信接口调用的url地址
        String host = "https://gyytz.market.alicloudapi.com";

        // 具体发送短信功能的地址
        String path = "/sms/smsSend";

        // 请求方式
        String method = "POST";

        // 登录到阿里云,进入控制台,找到已购买的短信接口的 appcode
        String appcode = "8ad93a42798f4ab982137fba11d8fbb1";

        // 用headers去封装appcode,最后在headers中的格式(中间是英文空格)是:Authorization:APPCODE 8ad93a42798f4ab982137fba11d8fbb1
        HashMap<String, String> headers = new HashMap<>();
        headers.put("Authorization","APPCODE " + appcode);

        // 封装其他参数
        HashMap<String, String> querys = new HashMap<>();
        // 要发送的验证码,也就是模板中会变化的部分
        querys.put("mobile","15565447608");
        // 收短信的手机号
        querys.put("param","**code**:654321,**minute**:5");
        // 签名的编号,测试使用,如果想自己定义,需要找客服申请
        querys.put("smsSignId","2e65b1bb3d054466b82f0c9d125465e2");
        // 模板的编号,测试使用,如果想自己定义,需要找客服申请
        querys.put("templateId","908e94ccf08b4476ba6c876d13f084ad");
        Map<String, String> bodys = new HashMap<String, String>();

        try {
            /**
             * 重要提示如下:HttpUtils请从
             * https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/src/main/java/com/aliyun/api/gateway/demo/util/HttpUtils.java
             * 或者直接下载:http://code.fegine.com/HttpUtils.zip下载
             *
             * 相应的依赖请参照
             * http://github.com/aliyun/api-gateway-demo-sign-java/blob/master/pom.xml
             * 相关jar包(非pom)直接下载:http://code.fegine.com/aliyun-jar.zip下载
             */
            HttpResponse response = HttpUtils.doPost(host, path, method, headers, querys,bodys);
            // System.out.println(response.toString());如不输出json,请打开这行代码,打印调试头部状态码
            // 状态码:200正常,400 URL无效,401 appcode错误,403 次数用完,500 api网关错误
            // 获取response的body
            System.out.println(EntityUtils.toString(response.getEntity()));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

4.1.7 封装成工具类

public class SendMessageUtil {
    /**
     * 给远程第三方短信接口发送请求把验证码发送到用户手机上
     * @param host       短信接口调用的url地址
     * @param path       具体发送短信功能的地址
     * @param method     请求方式
     * @param phoneNum   接收验证码的手机号
     * @param appCode    用来调用第三方短信api的appCode
     * @param smsSignId  签名的编号
     * @param templateId 模板的编号
     * @return 返回调用结果是否成功
     *  成功:返回验证码
     *  失败:失败的消息
     *  状态码:200正常,400 URL无效,401 appcode错误,403 次数用完,500 api网关错误
     */
    public static ResultEntity<String> sendShortMessage(
            String host,
            String path,
            String method,
            String phoneNum,
            String appCode,
            String smsSignId,
            String templateId){
        // 用headers去封装appcode,最后在headers中的格式(中间是英文空格)是:Authorization:APPCODE 8ad93a42798f4ab982137fba11d8fbb1
        HashMap<String, String> headers = new HashMap<>();
        headers.put("Authorization","APPCODE " + appCode);

        // 封装其他参数
        HashMap<String, String> querys = new HashMap<>();
        
        // 生成验证码
        StringBuilder builder = new StringBuilder();
        for (int i = 0 ; i < 4 ; i++){
            int random = (int)(Math.random() * 10);
            builder.append(random);
        }
        String code = builder.toString();

        // 收短信的手机号
        querys.put("mobile",phoneNum);
        // 要发送的验证码,也就是模板中会变化的部分
        querys.put("param","**code**:" + code + ",**minute**:5");
        // 签名的编号,测试使用,如果想自己定义,需要找客服申请
        querys.put("smsSignId",smsSignId);
        // 模板的编号,测试使用,如果想自己定义,需要找客服申请
        querys.put("templateId",templateId);
        Map<String, String> bodys = new HashMap<String, String>();

        try {
            HttpResponse response = HttpUtils.doPost(host, path, method, headers, querys, bodys);

            StatusLine statusLine = response.getStatusLine();

            // 状态码:200正常,400 URL无效,401 appcode错误,403 次数用完,500 api网关错误
            int statusCode = statusLine.getStatusCode();

            String reasonPhrase = statusLine.getReasonPhrase();
            
            if(statusCode == 200){
                // 操作成功,把生成的验证码返回
                return ResultEntity.successWithData(code);
            }
            return ResultEntity.failed(reasonPhrase);
        } catch (Exception e) {
            e.printStackTrace();
            return ResultEntity.failed(e.getMessage());
        }
    }
}

4.2 SpringSession解决Session共享问题

4.2.1 SpringSession的使用

①引入依赖

<!-- 引入 springboot&redis 整合场景 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 引入 springboot&springsession 整合场景 -->
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>

②编写配置

# redis 配置
spring:
  redis:
    host: 192.168.56.100 #redis的主机地址
    port: 6379
    password: 123456
    jedis:
      pool:
        max-idle: 100    #jedis连接池的最大连接数,不是必须的
# springsession配置
  session:
    store-type: redis    #告诉SpringSession存储的类型是在哪存

 注意:存入Session域的实体类对象必须支持序列化!!!

③测试:可以非侵入式的实现模块之间的Session共享

set工程存:
@RequestMapping("/test/spring/session/set")
public String testSession(HttpSession session){
    session.setAttribute("king","hello-king");
    return "数据已存入";
}
get工程取:
@RequestMapping("/test/spring/session/get")
public String testSession(HttpSession session){
    String value = (String) session.getAttribute("king");
    return value;
}

4.2.2 SpringSession基本原理:SpringSession从底层全方位接管了Tomcat对Session的管理。

①SpringSession需要完成的任务:SpringSession使用Filter来完成这些任务

 

②SessionRepositoryFilter

源代码:
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
        SessionRepositoryFilter<S>.SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryFilter.SessionRepositoryRequestWrapper(request, response, this.servletContext);
        SessionRepositoryFilter.SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryFilter.SessionRepositoryResponseWrapper(wrappedRequest, response);

        try {
            filterChain.doFilter(wrappedRequest, wrappedResponse);
        } finally {
            wrappedRequest.commitSession();
        }

    }

利用Filter原理,在每次请求到达目标方法之前,将原生HttpServletRequest/HttpServletResponse对象包装为SessionRepositoryRequest/ResponseWrapper。
包装request对象时要做到:包装后和包装前类型兼容。
所谓类型兼容:“包装得到的对象 instanceof 包装前类型”返回true。
只有做到了类型的兼容,后面使用包装过的对象才能够保持使用方法不变。包装过的对象类型兼容、使用方法不变,才能实现“偷梁换柱”。
但是如果直接实现 HttpServletRequest 接口,我们又不知道如何实现各个抽象方法。这个问题可以借助原始被包装的对象来解决。

4.3  阿里云的OSS 对象存储服务

4.3.1 开通 OSS 服务步骤

4.3.2 OSS的使用

①在Bucket中创建目录

②上传文件

③浏览器访问路径组成

4.3.3  Java 程序调用OSS服务接口前的准备工作

4.3.3.1 官方介绍

        阿里云对象存储服务(Object Storage Service,简称 OSS),是阿里云提供的海量、安全、低成本、高可靠的云存储服务。您可以通过调用 API,在任何应用、任何时间、任何地点上传和下载数据,也可以通过 Web 控制台对数据进行简单的管理。OSS 适合存放任意类型的文件,适合各种网站、开发企业及开发者使用。按实际容量付费真正使您专注于核心业务。 

4.3.3.2 创建AccessKey,使用java程序登录OSS进行操作

①介绍

        访问密钥 AccessKey(AK)相当于登录密码,只是使用场景不同。AccessKey 用于程序方式调用云服务 API,而登录密码用于登录控制台。如果不需要调用 API,那就不需要建AccessKey。
您可以使用 AccessKey 构造一个 API 请求(或者使用云服务 SDK)来操作资源。AccessKey包括AccessKeyId和AccessKeySecret。AccessKeyId用于标识用户,相当于账号。AccessKeySecret是用来验证用户的密钥。AccessKeySecret 必须保密。警告禁止使用主账号AK,因为主账号AK泄露会威胁您所有资源的安全。请使用子账号(RAM用户)AK 进行操作,可有效降低 AK 泄露的风险。

②创建子账号AK的操作步骤

1.使用主账号登录 RAM 管理控制台。
2.如果未创建 RAM 用户,在左侧导航栏,单击用户管理,然后单击新建用户,创建 RAM 用户。
如果已创建 RAM 用户,跳过此步骤。
3.在左侧导航栏,单击用户管理,然后单击需要创建 AccessKey 的用户名,进入用户详情页面。
4.在用户 AccessKey 区域,单击创建 AccessKey。
5.完成手机验证后,在新建用户 AccessKey 页面,展开 AccessKey 详情,查看 AcessKeyId 和 AccessKeySecret。
然后单击保存 AK 信息,下载 AccessKey 信息。注意 AccessKey 创建后,无法再通过控制台查看。
请您妥善保存 AccessKey,谨防泄露。
6.单击该 RAM 用户对应的授权,给 RAM 用户授予相关权限,例如 AliyunOSSFullAccess 将给RAM 用户授予 OSS 的管理权限。

③ 操作步骤截图

 

注意:及时保存AccessKeySecret!!!!页面关闭后将无法再次获取。

创建结果:

用户登录名称 nanb220815oss@1441361933506409.onaliyun.com
AccessKey ID LTAI5t9YDW5GEDt698158gsq
AccessKey Secret 保密

4.3.3.3 需要使用到OSS的SDK

  • JDK:Java Development Kit          Java开发工具包
  • SDK:Software Development Kit   软件开发工具包

①依赖引入SDK

<!--OSS 客户端 SDK-->
<dependency>
    <groupId>com.aliyun.oss</groupId>
    <artifactId>aliyun-sdk-oss</artifactId>
    <version>3.5.0</version>
</dependency>

4.3.4 将OSS引入项目 

4.3.4.1 准备OSSProperties类,用以装配OSS参数信息

所在工程:shangcouwang07-member-project-consumer
全类名:com.atguigu.crowd.config.OSSProperties
@Data
@AllArgsConstructor
@NoArgsConstructor
@Component
@ConfigurationProperties(prefix = "aliyun.oss")
public class OSSProperties {
    //Endpoint(地域节点)
    private String endPoint;
    private String bucketName;
    private String accessKeyId;
    private String accessKeySecret;
    //Bucket 域名
    private String bucketDomain;
}

4.3.4.2 将 OSS 代码中用到的属性存入 yaml 配置文件

aliyun:
  oss:
    endPoint: http://oss-cn-hangzhou.aliyuncs.com
    bucketName: nanb0815
    accessKeyId: LTAI5t9YDW5GEDt698158gsq
    accessKeySecret: HAWgkySb******************b
    bucketDomain: http://nanb0815.oss-cn-hangzhou.aliyuncs.com

4.3.4.3 上传文件的工具方法:需要使用到SDK(引入依赖)

public class MyOSSUtils {
    /**
     * 专门负责上传文件到 OSS 服务器的工具方法
     * @param endpoint             OSS参数
     * @param accessKeyId          OSS参数
     * @param accessKeySecret      OSS参数
     * @param inputStream         要上传的文件的输入流
     * @param bucketName           OSS参数
     * @param bucketDomain         OSS参数
     * @param originalName        要上传的文件的原始文件名
     * @return 包含上传结果以及上传的文件在 OSS 上的访问路径
     */
    public static ResultEntity<String> uploadFileToOss(
            String endpoint,
            String accessKeyId,
            String accessKeySecret,
            InputStream inputStream,
            String bucketName,
            String bucketDomain,
            String originalName){
        // 创建OSSClient实例
        OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
        // 生成上传文件的目录
        String folderName = new SimpleDateFormat("yyyyMMdd").format(new Date());
        // 生成上传文件在OSS服务器上保存时的文件名
        // 原始文件名:
        // 生成文件名:
        // 使用UUID生成文件主体名称
        String fileMainName = UUID.randomUUID().toString().replace("-", "");
        // 从原始文件名中获取文件扩展名
        String extensionName = originalName.substring(originalName.lastIndexOf("."));
        // 使用目录、文件主体名称、文件扩展名称拼接得到对象名称
        String objectName = folderName + "/" + fileMainName + extensionName;
        try {
            // 调用OSS客户端对象的方法上传文件并获取响应结果数据
            PutObjectResult putObjectResult = ossClient.putObject(bucketName, objectName, inputStream);
            // 从响应结果中获取具体响应消息
            ResponseMessage responseMessage = putObjectResult.getResponse();
            // 根据响应状态码判断请求是否成功
            if(responseMessage == null){
                // 拼接访问刚刚上传的文件路径
                String ossFileAccessPath = bucketDomain + "/" + objectName;
                // 当前方法返回成功
                return ResultEntity.successWithData(ossFileAccessPath);
            }else{
                // 获取响应状态码
                int statusCode = responseMessage.getStatusCode();
                // 如果请求没有成功,获取错误消息
                String errorMessage = responseMessage.getErrorResponseAsString();
                // 当前方法返回失败
                return ResultEntity.failed("当前响应状态码=" + statusCode + " 错误消息=" + errorMessage);
            }
        } catch (Exception e) {
            e.printStackTrace();
            // 当前方法返回失败
            return ResultEntity.failed(e.getMessage());
        } finally {
            if(ossClient != null){
                // 关闭OSSClient
                ossClient.shutdown();
            }
        }
    }
}

4.3.4.4 测试

返回结果:ResultEntity{result='SUCCESS', message='NO_MESSAGE', data=http://nanb0815.oss-cn-hangzhou.aliyuncs.com/20220909/5ff5f29d231d413491a1cc2911310744.jpeg}
@RunWith(SpringRunner.class)
@SpringBootTest
public class OSSTest {
    @Test
    public void ossTest(){
        FileInputStream fileInputStream = null;
        try {
            fileInputStream = new FileInputStream("123.jpeg");
            ResultEntity<String> resultEntity = MyOSSUtils.uploadFileToOss("http://oss-cn-hangzhou.aliyuncs.com", "LTAI5t9YDW5GEDt698158gsq",
                    "HAWgky****************", fileInputStream, "nanb0815",
                    "http://nanb0815.oss-cn-hangzhou.aliyuncs.com", "123.jpeg");
            System.out.println(resultEntity);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } finally {
            if(fileInputStream != null){
                try {
                    fileInputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

5. 特别记录

5.1 关于第一次请求超时

        由于在第一次请求中需要建立缓存、建立连接,操作较多,所以比较耗时。如果按照默认的 ribbon 的超时时间来工作,第一次请求会超过这个时间导致超时报错。为避免这个问题,把ribbon 的超时时间延长。配置方式是在application.yaml中加入如下配置(那个工程需要在哪配):

ribbon:
  ReadTimeout: 10000              # 通信超时时间(ms)
  ConnectTimeout: 10000           # 连接超时时间(ms)

5.2 @RequestBody的专门测试:必须加该注解!!!

对象参数前面必须加上@RequestBody注解,否则参数传不过来,会报错。因为数据是以json的格式{ " loginacct " : " tom "...}传送的,springMVC只能接收loginacct=tom的格式,然后去找set方法,不写@RequestBody注解的话,不能解析json格式,所以接收数据会失败。

// api接口:
@RequestMapping("/save/member/remote")
public ResultEntity<String> saveMemberRemote(@RequestBody MemberPO memberPO);
// mysql-provider工程方法:
@RequestMapping("/save/member/remote")
public ResultEntity<String> saveMemberRemote(@RequestBody MemberPO memberPO){
    try {
        memberService.saveMember(memberPO);
        return ResultEntity.successWithoutData();
    } catch (Exception e) {
        if(e instanceof DuplicateKeyException){
            return ResultEntity.failed("抱歉!这个账号已经被使用了!");
        }
        return ResultEntity.failed(e.getMessage());
    }
}

5.3 OSS-提出问题:项目维护中涉及到这样一个功能:上传图片

5.3.1 以前上传文件时保存位置在Tomcat中

但面临着问题:

  • 问题1:Web应用重新部署导致文件丢失
    • 重新部署Web应用时,卸载(删除)旧的Web应用,连同用户上传的文件一起删除。重新加载新的Web应用后以前用户上传的文件不会自动恢复。
    • 危害总结:Web应用重新部署会导致用户上传的文件丢失。
  • 问题2:集群环境下文件难以同步,可能存在访问时有时无的情况

  • 问题3:Tomcat被拖垮
    • 用户上传的文件如果数据量膨胀到了一个非常庞大的体积,那么就会严重影响Tomcat的运行效率。
  • 问题4:服务器存储自动扩容问题
    • 危害总结:手动对服务器进行扩容,有可能导致项目中其他地方需要进行连带修改。

5.3.2 解决方案介绍

①自己搭建文件服务器

  • 举例:FastDFS
  • 好处:服务器可以自己维护、自己定制。
  • 缺点:需要投入的人力、物力更多。
  • 适用:规模比较大的项目,要存储海量的文件。

②使用第三方云服务

  • 举例:阿里云提供的 OSS 对象存储服务。
  • 好处:不必自己维护服务器的软硬件资源。直接调用相关 API 即可操作,更加轻量级。
  • 缺点:数据不在自己手里。服务器不由自己维护。
  • 适用:较小规模的应用,文件数据不是绝对私密。

5.3.3 开通OSS服务步骤:具体步骤见4.3.1

5.4 今后项目中重定向的问题

①描述问题:以下的两个是不同网站,浏览器工作时不会使用相同的Cookie

  • http://localhost:4000
  • http://localhost:80 

②解决问题:以后重定向的地址都按照通过Zuul访问的方式写地址

redirect:http://localhost:80/auth/member/to/center/page

 5.5 Zuul需要依赖entity工程

        通过Zuul访问所有工程,在成功登陆之后,要前往会员中心页面。这时,在ZuulFilter中需要从Session域读取MemberLoginVO对象。SpringSession会从Redis中加载相关信息。相关信息中包含了MemberLoginVO类,用来反序列化。可是我们之前没有让Zuul工程依赖entity工程,所以找不到MemberLoginVO类。抛找不到异常。

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值