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

总目标:

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

1. 会员系统架构

1.1 架构图

尚筹网-前台-会员系统(springboot,springcloud 实战)_第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

com.atguigu.crowd
shangcouwang01-member-parent
1.0-SNAPSHOT

    shangcouwang02-member-eureka
    shangcouwang03-member-entity
    shangcouwang04-member-mysql-provider
    shangcouwang05-member-redis-provider
    shangcouwang06-member-authentication-consumer
    shangcouwang07-member-project-consumer
    shangcouwang08-member-order-consumer
    shangcouwang09-member-pay-consumer
    shangcouwang10-member-zuul
    shangcouwang11-member-api

pom


    
        
        
            org.springframework.cloud
            spring-cloud-dependencies
            Greenwich.SR2
            pom
            
            import
        
        
        
            org.springframework.boot
            spring-boot-dependencies
            2.1.6.RELEASE
            pom
            import
        
        
        
            org.mybatis.spring.boot
            mybatis-spring-boot-starter
            2.1.0
        
        
        
            com.alibaba
            druid
            1.0.5
        
    

2.3 eureka工程

2.3.1 依赖


    
    
        org.springframework.cloud
        spring-cloud-starter-netflix-eureka-server
    

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

尚筹网-前台-会员系统(springboot,springcloud 实战)_第2张图片

使用 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()方法

    
        org.projectlombok
        lombok
    

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

  • 实体类归位

尚筹网-前台-会员系统(springboot,springcloud 实战)_第3张图片

  • Mapper相关归位

尚筹网-前台-会员系统(springboot,springcloud 实战)_第4张图片

2.5.4 引入依赖



    org.mybatis.spring.boot
    mybatis-spring-boot-starter



    com.alibaba
    druid



    mysql
    mysql-connector-java



    org.springframework.boot
    spring-boot-starter-test
    test



    org.springframework.cloud
    spring-cloud-starter-netflix-eureka-client



    com.atguigu.crowd
    shangcouwang03-member-entity
    1.0-SNAPSHOT

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,"杰克","[email protected]",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依赖 



    org.springframework.boot
    spring-boot-starter-web

尚筹网-前台-会员系统(springboot,springcloud 实战)_第5张图片

2.6.1 api工程

引入依赖


    org.springframework.cloud
    spring-cloud-starter-openfeign

创建接口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 getMemberPOByLoginAcctRemote(@RequestParam("loginacct") String loginacct);
}

2.6.2 MySQL工程

创建组件

尚筹网-前台-会员系统(springboot,springcloud 实战)_第6张图片

handler代码

@RestController
public class MemberProviderHandler {

    @Autowired
    private MemberService memberService;

    // member登陆时,根据 loginAcct 查询 Member 对象
    @RequestMapping("/get/memberpo/by/loginacct/remote")
    public ResultEntity 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 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 依赖



    org.springframework.boot
    spring-boot-starter-data-redis



    org.springframework.cloud
    spring-cloud-starter-netflix-eureka-client



    org.springframework.boot
    spring-boot-starter-web



    org.springframework.boot
    spring-boot-starter-test
    test



    com.atguigu.crowd
    shangcouwang03-member-entity
    1.0-SNAPSHOT

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 redisTemplate;
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

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

    @Test
    public void testRedis(){
        // 1.获取用来操作String类型数据的ValueOperations对象
        ValueOperations 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 setRedisKeyValueRemote(
            @RequestParam("key") String key,
            @RequestParam("value") String value);
    // 带超时时间的set
    @RequestMapping("/set/redis/key/value/remote/with/timeout")
    ResultEntity 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 getRedisStringValueByKeyRemote(@RequestParam("key") String key);
    // 移除
    @RequestMapping("/remove/redis/key/remote")
    ResultEntity 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 setRedisKeyValueRemote(
            @RequestParam("key") String key,
            @RequestParam("value") String value) {
        try {
            ValueOperations 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 setRedisKeyValueRemoteWithTimeout(
            @RequestParam("key") String key,
            @RequestParam("value") String value,
            @RequestParam("time") long time,            // 超时的时间
            @RequestParam("timeUnit") TimeUnit timeUnit) {// 时间的单位
        try {
            ValueOperations 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 getRedisStringValueByKeyRemote(@RequestParam("key") String key){
        try {
            ValueOperations 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 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 依赖



    org.springframework.boot
    spring-boot-starter-web



    com.atguigu.crowd
    shangcouwang11-member-api
    1.0-SNAPSHOT



    org.springframework.boot
    spring-boot-starter-thymeleaf



    org.springframework.cloud
    spring-cloud-starter-netflix-eureka-client

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目录下存放静态资源

尚筹网-前台-会员系统(springboot,springcloud 实战)_第7张图片

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

方式1:




    
    
    
    
    
    
    
    

方式2:




    
    
    
    
    
    
    
    
    

2.10 网关Zuul

2.10.1 依赖


    org.springframework.cloud
    spring-cloud-starter-netflix-eureka-client


    org.springframework.cloud
    spring-cloud-starter-netflix-zuul

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 配置域名(假的,可选)

尚筹网-前台-会员系统(springboot,springcloud 实战)_第8张图片

2.10.5 访问效果:localhost:80

尚筹网-前台-会员系统(springboot,springcloud 实战)_第9张图片

3. 具体业务

3.1 会员注册

3.1.1 发送验证码的流程

①.目标

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

②.思路

尚筹网-前台-会员系统(springboot,springcloud 实战)_第10张图片

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

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. 修改注册超链接

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

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

    
    以上修改,没有type,默认是submit按钮,点击之后会提交表单,所以做如下修改,改成普通按钮
    
    
    绑定单击响应函数
    $(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 sendMessage(@RequestParam("phoneNum") String phoneNum){
            // 1.发送验证码到phoneNum手机
            ResultEntity 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 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信息存入到数据库

    ②.思路

    尚筹网-前台-会员系统(springboot,springcloud 实战)_第11张图片

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

    A. 给t_member表增加唯一约束

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

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

    //handler
    @RequestMapping("/save/member/remote")
    public ResultEntity 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 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 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 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 思路

    尚筹网-前台-会员系统(springboot,springcloud 实战)_第12张图片

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

    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 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 会员登录功能的延伸

    尚筹网-前台-会员系统(springboot,springcloud 实战)_第13张图片

    新目标:使用 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的同步。

    尚筹网-前台-会员系统(springboot,springcloud 实战)_第14张图片

    但面临着以下几个问题:

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

    ②将Session数据存储在Cookie中

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

    ③反向代理hash 一致性 

    尚筹网-前台-会员系统(springboot,springcloud 实战)_第15张图片

    面临的问题:

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

    ④后端统一存储Session数据

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

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

    优点:

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

    尚筹网-前台-会员系统(springboot,springcloud 实战)_第16张图片

    但是做起来却很麻烦,那么该如何实现呢?引入技术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 思路:

    尚筹网-前台-会员系统(springboot,springcloud 实战)_第17张图片

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

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

    pom.xml:
    
    
        org.springframework.boot
        spring-boot-starter-data-redis
    
    
    
        org.springframework.session
        spring-session-data-redis
    
    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 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 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域,做消息提示

    这里登录检查后发现不允许访问时的提示消息

    3.4.7 代码:Zuul中的特殊设置

     尚筹网-前台-会员系统(springboot,springcloud 实战)_第18张图片

     为了能够让整个过程中保持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对象,并归位 

    尚筹网-前台-会员系统(springboot,springcloud 实战)_第19张图片

    尚筹网-前台-会员系统(springboot,springcloud 实战)_第20张图片

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

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class ProjectVO implements Serializable {
        private static final long serialVersionUID = 1L;
        // 分类 id 集合
        private List typeIdList;
        // 标签 id 集合
        private List tagIdList;
        // 项目名称
        private String projectName;
        // 项目描述
        private String projectDescription;
        // 计划筹集的金额
        private Integer money;
        // 筹集资金的天数
        private Integer day;
        // 创建项目的日期
        private String createdate;
        // 头图的路径
        private String headerPicturePath;
        // 详情图片的路径
        private List detailPicturePathList;
        // 发起人信息
        private MemberLaunchInfoVO memberLaunchInfoVO;
        // 回报信息集合
        private List 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 思路:

    尚筹网-前台-会员系统(springboot,springcloud 实战)_第21张图片

    3.5.4.3 代码:

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

    authentication-consumer工程:member-crowd.html
    
  • project-consumer工程:project-agree.html

    ②配置访问 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 detailPictureList,
                // 用来将收集了一部分数据的ProjectVO对象存入Session域
                HttpSession session,
                // 用来在当前操作失败后返回上一个表单页面时携带的提示消息
                ModelMap modelMap) throws IOException {
            // 1.完成头图上传
            boolean headerPictureIsEmpty = headerPicture.isEmpty();
            if(!headerPictureIsEmpty){
                // 2.如果用户确实上传了有内容的文件,则执行上传
                ResultEntity 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 detailPicturePathList = new ArrayList<>();
            // 4.遍历detailPictureList集合
            for (MultipartFile detailPicture : detailPictureList) {
                // 5.当前detailPicture是否为空
                boolean detailPictureIsEmpty = detailPicture.isEmpty();
                if(!detailPictureIsEmpty){
                    // 6.不空则执行上传
                    ResultEntity 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";
        }
    }

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

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

    尚筹网-前台-会员系统(springboot,springcloud 实战)_第22张图片

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

    @ResponseBody
    @RequestMapping("/create/upload/return/picture.json")
    public ResultEntity uploadReturnPicture(
            // 接收用户上传的文件
            @RequestParam("returnPicture") MultipartFile returnPicture) throws IOException {
        // 1.执行文件上传
        ResultEntity 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 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 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());
        }
    }

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

    下一步
    view-controller:
    registry.addViewController("/create/confirm/page").setViewName("project-confirm");
    

    调整project-confirm.html页面

    修改提交按钮的HTML标签:
    
    调整表单代码:
    
    给提交按钮绑定单击响应函数

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

    @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 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 getMemberPOByLoginAcctRemote(@RequestParam("loginacct") String loginacct);
    
        @RequestMapping("/save/member/remote")
        public ResultEntity saveMemberRemote(@RequestBody MemberPO memberPO);
    
        @RequestMapping("/save/project/vo/remote")
        public ResultEntity 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 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文件中进行相关设置
            //  typeIdList = projectVO.getTypeIdList();
            projectPOMapper.insertProjectAndTypeRelationShip(typeIdList,projectId);
            // 三、保存项目、标签的关联关系信息
            List tagIdList = projectVO.getTagIdList();
            projectPOMapper.insertProjectAndTagRelationShip(tagIdList,projectId);
            // 四、保存项目中详情图片路径的信息
            List 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 returnVOList = projectVO.getReturnVOList();
            ArrayList 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:
    项目、分类:
    
    
        insert into t_project_type (projectid,typeid) values
        (#{projectId},#{typeId})
    
    项目、标签:
    
    
        insert into t_project_tag (projectid,tagid) values
        (#{projectId},#{tagId})
    
    ProjectItemPicPOMapper.xml:
    详情图片路径:
    
    
        insert into t_project_item_pic (projectid,item_pic_path) values
        (#{projectId},#{detailPicturePath})
    
    ReturnPOMapper.xml:
    回报信息:
    
    
        insert into t_return (
            projectid, 
            type,
            supportmoney, 
            content,
            count,
            signalpurchase, 
            purchase, 
            freight,
            invoice,
            returndate,
            describ_pic_path)
        values
        
               (
                    #{projectId},
                    #{returnPO.type},
                    #{returnPO.supportmoney},
                    #{returnPO.content},
                    #{returnPO.count},
                    #{returnPO.signalpurchase},
                    #{returnPO.purchase},
                    #{returnPO.freight},
                    #{returnPO.invoice},
                    #{returnPO.returndate},
                    #{returnPO.describPicPath}
               )
        
    
    

    3.6 首页显示项目

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

    尚筹网-前台-会员系统(springboot,springcloud 实战)_第23张图片

    3.6.2 思路:

    尚筹网-前台-会员系统(springboot,springcloud 实战)_第24张图片

    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 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中的相关代码:

    
    
        
        
        
        
        
        
        
        
        
    
    
    
    
    List selectPortalTypeVOList();

    2)handler、service方法

    projectService:
    List getPortalTypeVOList();
    projectServiceImpl:
    @Override
    public List getPortalTypeVOList() {
        return projectPOMapper.selectPortalTypeVOList();
    }
    handler:
    @RequestMapping("/select/portal/type/project/data")
    public ResultEntity> getPortalTypeProjectDataRemote(){
        try {
            List 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> getPortalTypeProjectDataRemote();

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

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

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

    使用双层循环遍历数据:
    
    没有加载到任何分类的信息

    开启智慧未来

    该分类下还没有任何项目
    300x200

    活性富氢净水直饮机

    $20,000
    2017-20-20


    40%
    12345

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

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

    3.7.2 思路:

    尚筹网-前台-会员系统(springboot,springcloud 实战)_第25张图片

    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 detailPicturePathList;
        // 回报的信息
        private List 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中的相关代码:

    
    
        
        
        
        
        
        
        
        
        
        
        
        
        
    
    
    
    
    
    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 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 getDetailProjectVORemote(@PathVariable("projectId") Integer projectId);

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

    项目的起点在auth-consumer工程的portal.html中:
    活性富氢净水直饮机
    
    @RequestMapping("/show/project/detail/{projectId}")
    public String showProjectDetail(@PathVariable("projectId") Integer projectId,ModelMap modelMap){
        ResultEntity 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中显示模型中的数据

    查询项目详情信息失败!

    酷驰触控龙头,智享厨房黑科技

    智能时代,酷驰触控厨房龙头,让煮妇解放双手,触发更多烹饪灵感,让美味信手拈来。
    加载图片详情信息失败
    140x140 140x140
    [[${detailProjectVO.statusText}]]

    已筹资金:¥[[${detailProjectVO.supportMoney}]]

    目标金额 : [[${detailProjectVO.money}]]达成 : [[${detailProjectVO.percentage}]]%

    剩余 [[${detailProjectVO.day}]] 天

    已有[[${detailProjectVO.supporterCount}]]人支持该项目

    ...... ......
    没有找到项目回报信息

    ¥[[${detailReturnVO.supportMoney}]] 无限额,447位支持者 限额[[${detailReturnVO.purchase}]]位,剩余465位

    配送费用:包邮

    配送费用:[[${detailReturnVO.freight}]]

    预计发放时间:项目筹款成功后的[[${detailReturnVO.returnDate}]]天内



    感谢您的支持,在众筹开始后,您将以79元的优惠价格获得“遇见彩虹?”智能插座一件(参考价208元)。

    ...... ......

    3.8 订单工程

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

    
        org.projectlombok
        lombok
        true
    
    
        org.springframework.boot
        spring-boot-configuration-processor
        true
    
    
        org.springframework.boot
        spring-boot-starter-web
    
    
        org.springframework.cloud
        spring-cloud-starter-netflix-eureka-client
    
    
    
        com.atguigu.crowd
        shangcouwang11-member-api
        1.0-SNAPSHOT
    
    
    
        org.springframework.boot
        spring-boot-starter-thymeleaf
    
    
    
        org.springframework.boot
        spring-boot-starter-test
        test
    
    
    
        org.springframework.boot
        spring-boot-devtools
    
    
        org.springframework.boot
        spring-boot-loader
    
    
    
    
        org.springframework.boot
        spring-boot-starter-data-redis
    
    
    
        org.springframework.session
        spring-session-data-redis
    

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

    3.8.2 建模

    3.8.2.1 结构

    尚筹网-前台-会员系统(springboot,springcloud 实战)_第26张图片

    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对象,并归位

    尚筹网-前台-会员系统(springboot,springcloud 实战)_第27张图片

    3.8.3 确认回报内容

    3.8.3.1 思路

    尚筹网-前台-会员系统(springboot,springcloud 实战)_第28张图片

    3.8.3.2 代码

    ①操作起点:支持按钮

    project-detail.html中:
    支持
    注意 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 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 getOrderProjectVORemote(@RequestParam("projectId") Integer projectId, @RequestParam("returnId") Integer returnId);

    ⑤暴露接口

    handler代码:
    @Autowired
    private OrderService orderService;
    
    @RequestMapping("/get/order/project/vo/remote")
    public ResultEntity 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语句:
    

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

    总价(含运费):¥[[${session.orderProjectVO.returnCount*session.orderProjectVO.supportPrice}]]

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

    总价(含运费):¥[[${session.orderProjectVO.returnCount*session.orderProjectVO.supportPrice}]]

    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); });

    ⑧提交数据页面跳转

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

    3.8.4 确认订单

    3.8.4.1 思路

    尚筹网-前台-会员系统(springboot,springcloud 实战)_第29张图片

     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> resultEntity = mySQLRemoteService.getAddressVORemote(memberId);
        if(ResultEntity.SUCCESS.equals(resultEntity.getResult())){
            List list = resultEntity.getData();
            session.setAttribute("addressVOList",list);
        }
        return "confirm-order";
    }
    feign:
    @RequestMapping("/get/address/vo/remote")
    ResultEntity> getAddressVORemote(@RequestParam("memberId") Integer memberId);
    mysql-provider中:
    handler暴露接口:
    @RequestMapping("/get/address/vo/remote")
    public ResultEntity> getAddressVORemote(
            @RequestParam("memberId") Integer memberId){
        try {
            List addressVOList = orderService.getAddressVOList(memberId);
            return ResultEntity.successWithData(addressVOList);
        } catch (Exception e) {
            e.printStackTrace();
            return ResultEntity.failed(e.getMessage());
        }
    }
    serviceImpl:
    @Override
    public List getAddressVOList(Integer memberId) {
        AddressPOExample example = new AddressPOExample();
        example.createCriteria().andMemberIdEqualTo(memberId);
        List addressPOList = addressPOMapper.selectByExample(example);
        List addressVOList = new ArrayList<>();
        for (AddressPO addressPo : addressPOList) {
            AddressVO addressVO = new AddressVO();
            BeanUtils.copyProperties(addressPo,addressVO);
            addressVOList.add(addressVO);
        }
        return addressVOList;
    }

    ③页面显示

    地址:
    
    尚未创建收货地址
    回报信息:

    3.8.5 新增收货地址

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

    尚筹网-前台-会员系统(springboot,springcloud 实战)_第30张图片

    3.8.5.2 代码 

    @RequestMapping("/save/address")
    public String saveAddress(
            AddressVO addressVO,
            HttpSession session){
        // 1.执行地址信息的保存
        ResultEntity 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  控制立即付款按钮是否有效

    勾选“我已了解风险和规则”多选框:按钮有效
    未勾“我已了解风险和规则”多选框:按钮无效
    
    
  • $("#knowRoleCheckBox").click(function (){ var currentStatus = this.checked; if(currentStatus){ $("#payBtn").prop("disabled",""); }else{ $("#payBtn").prop("disabled","disabled"); } });

    3.8.7 提交订单表单

    3.8.7.1 构造页面不可见的表单

    
    

    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("")
            .append("")
            .append("")
            .append("")
            .submit();
    });

    3.9 支付工程

    3.9.1 思路

    尚筹网-前台-会员系统(springboot,springcloud 实战)_第31张图片

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

    
    
    
        com.alipay.sdk
        alipay-sdk-java
        3.3.49.ALL
    

    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 params = new HashMap<>();
            Map requestParams = request.getParameterMap();
            for (Iterator 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 params = new HashMap<>();
            Map requestParams = request.getParameterMap();
            for (Iterator 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+"
    out_trade_no:"+orderNum+"
    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 把订单信息保存到数据库

    ①思路

    尚筹网-前台-会员系统(springboot,springcloud 实战)_第32张图片

    主要代码: 

    // 设置事务
    @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文件中进行相关设置
        // 

    4. 第三方接口

    4.1 会员注册-发送短信

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

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

    尚筹网-前台-会员系统(springboot,springcloud 实战)_第33张图片

    尚筹网-前台-会员系统(springboot,springcloud 实战)_第34张图片

    4.1.1 创建short-message工程

    com.atguigu.crowd
    pro01-short-message
    1.0-SNAPSHOT

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

    
    
        
            com.alibaba
            fastjson
            1.2.15
        
        
            org.apache.httpcomponents
            httpclient
            4.2.1
        
        
            org.apache.httpcomponents
            httpcore
            4.2.1
        
        
            commons-lang
            commons-lang
            2.6
        
        
            org.eclipse.jetty
            jetty-util
            9.3.7.v20160115
        
        
            junit
            junit
            4.5
            test
        
    

    4.1.4 测试结果

    尚筹网-前台-会员系统(springboot,springcloud 实战)_第35张图片

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

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

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

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

    
    
        com.alibaba
        fastjson
        1.2.15
    
    
    
        org.apache.httpcomponents
        httpclient
    
    
        org.eclipse.jetty
        jetty-util
    

    ②.把上面的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 headers = new HashMap<>();
            headers.put("Authorization","APPCODE " + appcode);
    
            // 封装其他参数
            HashMap querys = new HashMap<>();
            // 要发送的验证码,也就是模板中会变化的部分
            querys.put("mobile","15565447608");
            // 收短信的手机号
            querys.put("param","**code**:654321,**minute**:5");
            // 签名的编号,测试使用,如果想自己定义,需要找客服申请
            querys.put("smsSignId","2e65b1bb3d054466b82f0c9d125465e2");
            // 模板的编号,测试使用,如果想自己定义,需要找客服申请
            querys.put("templateId","908e94ccf08b4476ba6c876d13f084ad");
            Map bodys = new HashMap();
    
            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 sendShortMessage(
                String host,
                String path,
                String method,
                String phoneNum,
                String appCode,
                String smsSignId,
                String templateId){
            // 用headers去封装appcode,最后在headers中的格式(中间是英文空格)是:Authorization:APPCODE 8ad93a42798f4ab982137fba11d8fbb1
            HashMap headers = new HashMap<>();
            headers.put("Authorization","APPCODE " + appCode);
    
            // 封装其他参数
            HashMap 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 bodys = new HashMap();
    
            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的使用

    ①引入依赖

    
    
        org.springframework.boot
        spring-boot-starter-data-redis
    
    
    
        org.springframework.session
        spring-session-data-redis
    

    ②编写配置

    # 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来完成这些任务

     尚筹网-前台-会员系统(springboot,springcloud 实战)_第36张图片

    ②SessionRepositoryFilter

    源代码:
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
            request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
            SessionRepositoryFilter.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 服务步骤

    尚筹网-前台-会员系统(springboot,springcloud 实战)_第37张图片

    尚筹网-前台-会员系统(springboot,springcloud 实战)_第38张图片

    尚筹网-前台-会员系统(springboot,springcloud 实战)_第39张图片

    尚筹网-前台-会员系统(springboot,springcloud 实战)_第40张图片

    4.3.2 OSS的使用

    ①在Bucket中创建目录

    尚筹网-前台-会员系统(springboot,springcloud 实战)_第41张图片

    ②上传文件

    尚筹网-前台-会员系统(springboot,springcloud 实战)_第42张图片

    尚筹网-前台-会员系统(springboot,springcloud 实战)_第43张图片尚筹网-前台-会员系统(springboot,springcloud 实战)_第44张图片

    ③浏览器访问路径组成

    尚筹网-前台-会员系统(springboot,springcloud 实战)_第45张图片

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

    尚筹网-前台-会员系统(springboot,springcloud 实战)_第46张图片

    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 的管理权限。

    ③ 操作步骤截图

    尚筹网-前台-会员系统(springboot,springcloud 实战)_第47张图片 尚筹网-前台-会员系统(springboot,springcloud 实战)_第48张图片

    尚筹网-前台-会员系统(springboot,springcloud 实战)_第49张图片

    尚筹网-前台-会员系统(springboot,springcloud 实战)_第50张图片

    尚筹网-前台-会员系统(springboot,springcloud 实战)_第51张图片

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

    创建结果:

    用户登录名称 [email protected]
    AccessKey ID LTAI5t9YDW5GEDt698158gsq
    AccessKey Secret 保密

    尚筹网-前台-会员系统(springboot,springcloud 实战)_第52张图片尚筹网-前台-会员系统(springboot,springcloud 实战)_第53张图片

    尚筹网-前台-会员系统(springboot,springcloud 实战)_第54张图片

    4.3.3.3 需要使用到OSS的SDK

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

    ①依赖引入SDK

    
    
        com.aliyun.oss
        aliyun-sdk-oss
        3.5.0
    
    

    4.3.4 将OSS引入项目 

    尚筹网-前台-会员系统(springboot,springcloud 实战)_第55张图片

    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 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 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 saveMemberRemote(@RequestBody MemberPO memberPO);
    // mysql-provider工程方法:
    @RequestMapping("/save/member/remote")
    public ResultEntity 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中

    尚筹网-前台-会员系统(springboot,springcloud 实战)_第56张图片

    但面临着问题:

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

    尚筹网-前台-会员系统(springboot,springcloud 实战)_第57张图片

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

    尚筹网-前台-会员系统(springboot,springcloud 实战)_第58张图片

    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类。抛找不到异常。

    你可能感兴趣的:(数据库,java,开发语言,spring,boot,spring,cloud)

    活性富氢净水直饮机 深圳市博实永道电子商务有限公司 每满1750人抽取一台活性富氢净水直饮机,至少抽取一台。抽取名额(小数点后一位四舍五入)=参与人数÷1750人,由苏宁官方抽取。 ¥ 1.00 免运费 免运费
    活性富氢净水直饮机 深圳市博实永道电子商务有限公司 每满1750人抽取一台活性富氢净水直饮机,至少抽取一台。抽取名额(小数点后一位四舍五入)=参与人数÷1750人,由苏宁官方抽取。 55 ¥ 1.00 免运费 免运费