总目标:
2.1.1 包名约定:新创建的包都作为com.atguigu.crowd的子包
2.1.2 主启动类类名:CrowdMainClass
2.1.3 端口号:
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.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.1 实体类的进一步细分
使用 org.springframework.beans.BeanUtils.copyProperties(Object, Object)在不同实体类之间复制属性。MemberVO→复制属性→MemberPO
2.4.2 创建包
2.4.3 lombok,简化JavaBean开发,让我们在开发时不必编写 getXxx()、setXxx()、有参构造器、无参构造器等等这样具备固定模式的代码。
注解:
org.projectlombok
lombok
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
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());
}
}
为了能够对外提供服务,服务本身需要有handler,handler调用service,service调用mapper完成整套业务,所以MySQL工程需要引入web依赖
org.springframework.boot
spring-boot-starter-web
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工程
创建组件
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.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.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.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目录下存放静态资源
②.调整portal.html页面(使用thymeleaf技术)
方式1:
方式2:
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 配置域名(假的,可选)
2.10.5 访问效果:localhost:80
3.1.1 发送验证码的流程
①.目标
②.思路
③.实际操作(挑选重要步骤记录,其他环节省略)
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信息存入到数据库
②.思路
③.实际操作(挑选重要步骤记录,其他环节省略)
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.1 目标:检查账号密码正确后将用户信息存入session,表示用户已登录
3.2.2 思路
③.实际操作(挑选重要步骤记录,其他环节省略)
A. entity工程创建MemberLoginVO对象,以便将登录状态存入session域
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MemberLoginVO {
private Integer id;
private String username;
private String email;
}
B. 在authentication-consumer工程执行登录
// 执行登录
@RequestMapping("/auth/member/do/login")
public String login(
@RequestParam("loginacct") String loginacct,
@RequestParam("userpswd") String userpswd,
ModelMap modelMap,
HttpSession session){
// 1.调用远程接口根据登录账号查询MemberPO对象
ResultEntity 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:/";
}
新目标:使用 Session 共享技术解决 Session 不互通问题
3.3.1 会话控制回顾
①Cookie 的工作机制
②Session 的工作机制
3.3.2 Session共享
在分布式和集群环境下,每个具体模块运行在单独的Tomcat上,而Session是被不同Tomcat所“区隔”的,不能互通。那么如何解决Session共享的问题呢?
解决方案:
①session同步:借助于Tomcat,Tomcat中做一些相关配置,就可以实现Session的同步。
但面临着以下几个问题:
②将Session数据存储在Cookie中
③反向代理hash 一致性
面临的问题:
④后端统一存储Session数据
后端存储 Session 数据时,一般需要使用 Redis 这样的内存数据库,而一般不采用 MySQL 这样的关系型数据库。原因如下:
优点:
但是做起来却很麻烦,那么该如何实现呢?引入技术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.1 目标:把项目中必须登录才能访问的功能保护起来,如果没有登录就访问则跳转到登录页面
3.4.2 思路:
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中的特殊设置
为了能够让整个过程中保持Session工作正常,需要新增如下配置:
zuul:
sensitive-headers: "*" # 在 Zuul 向其他微服务重定向时保持原本头信息(请求头、响应头)
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对象,并归位
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 思路:
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";
}
}
⑤代码:接收回报信息表单数据
注意:上传图片和提交表单是分开的
后端代码:接收页面异步上传的图片
@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.1 目标:在首页上加载真实保存到数据库的项目数据,按分类显示
3.6.2 思路:
3.6.3 操作的步骤:
①.创建实体类:PortalTypeVO、PortalProjectVO
②.mysql-provider 微服务暴露数据查询接口
③.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中显示模型中的数据
使用双层循环遍历数据:
没有加载到任何分类的信息
开启智慧未来
该分类下还没有任何项目
3.7.1 目标:点击项目名字跳转到项目详情页面显示项目数据
3.7.2 思路:
3.7.3 操作的步骤:
①.创建实体类:DetailProjectVO、DetailReturnVO
②.mysql-provider 微服务暴露数据查询接口
③.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中显示模型中的数据
查询项目详情信息失败!
酷驰触控龙头,智享厨房黑科技
智能时代,酷驰触控厨房龙头,让煮妇解放双手,触发更多烹饪灵感,让美味信手拈来。
加载图片详情信息失败
[[${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.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 结构
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对象,并归位
3.8.3 确认回报内容
3.8.3.1 思路
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
活性富氢净水直饮机
深圳市博实永道电子商务有限公司
每满1750人抽取一台活性富氢净水直饮机,至少抽取一台。抽取名额(小数点后一位四舍五入)=参与人数÷1750人,由苏宁官方抽取。
¥ 1.00
免运费
免运费
总价(含运费):¥[[${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 思路
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;
}
③页面显示
地址:
尚未创建收货地址
回报信息:
活性富氢净水直饮机
深圳市博实永道电子商务有限公司
每满1750人抽取一台活性富氢净水直饮机,至少抽取一台。抽取名额(小数点后一位四舍五入)=参与人数÷1750人,由苏宁官方抽取。
55
¥ 1.00
免运费
免运费
3.8.5 新增收货地址
3.8.5.1 思路:保存新地址后重新进入当前页面
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.1 思路
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工程)
@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 把订单信息保存到数据库
①思路
主要代码:
// 设置事务
@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文件中进行相关设置
//
首先去云市场,找到短信接口,然后根据下面的“api接口提示”进行操作。
【三网106短信】短信接口-短信验证码-短信通知-会员短信群发-短信平台API接口-行业短信_支持携号转网_自定义签名和模板【最新版】_电商_API_生活服务-云市场-阿里云 (aliyun.com)
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 测试结果
4.1.5 尚硅谷签名的模板(已失效)
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.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来完成这些任务
②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.1 开通 OSS 服务步骤
4.3.2 OSS的使用
①在Bucket中创建目录
②上传文件
③浏览器访问路径组成
4.3.3 Java 程序调用OSS服务接口前的准备工作
4.3.3.1 官方介绍
阿里云对象存储服务(Object Storage Service,简称 OSS),是阿里云提供的海量、安全、低成本、高可靠的云存储服务。您可以通过调用 API,在任何应用、任何时间、任何地点上传和下载数据,也可以通过 Web 控制台对数据进行简单的管理。OSS 适合存放任意类型的文件,适合各种网站、开发企业及开发者使用。按实际容量付费真正使您专注于核心业务。
4.3.3.2 创建AccessKey,使用java程序登录OSS进行操作
①介绍
访问密钥 AccessKey(AK)相当于登录密码,只是使用场景不同。AccessKey 用于程序方式调用云服务 API,而登录密码用于登录控制台。如果不需要调用 API,那就不需要建AccessKey。
您可以使用 AccessKey 构造一个 API 请求(或者使用云服务 SDK)来操作资源。AccessKey包括AccessKeyId和AccessKeySecret。AccessKeyId用于标识用户,相当于账号。AccessKeySecret是用来验证用户的密钥。AccessKeySecret 必须保密。警告:禁止使用主账号AK,因为主账号AK泄露会威胁您所有资源的安全。请使用子账号(RAM用户)AK 进行操作,可有效降低 AK 泄露的风险。
②创建子账号AK的操作步骤
1.使用主账号登录 RAM 管理控制台。
2.如果未创建 RAM 用户,在左侧导航栏,单击用户管理,然后单击新建用户,创建 RAM 用户。
如果已创建 RAM 用户,跳过此步骤。
3.在左侧导航栏,单击用户管理,然后单击需要创建 AccessKey 的用户名,进入用户详情页面。
4.在用户 AccessKey 区域,单击创建 AccessKey。
5.完成手机验证后,在新建用户 AccessKey 页面,展开 AccessKey 详情,查看 AcessKeyId 和 AccessKeySecret。
然后单击保存 AK 信息,下载 AccessKey 信息。注意 AccessKey 创建后,无法再通过控制台查看。
请您妥善保存 AccessKey,谨防泄露。
6.单击该 RAM 用户对应的授权,给 RAM 用户授予相关权限,例如 AliyunOSSFullAccess 将给RAM 用户授予 OSS 的管理权限。
③ 操作步骤截图
注意:及时保存AccessKeySecret!!!!页面关闭后将无法再次获取。
创建结果:
用户登录名称 [email protected]
AccessKey ID LTAI5t9YDW5GEDt698158gsq
AccessKey Secret 保密
4.3.3.3 需要使用到OSS的SDK
①依赖引入SDK
com.aliyun.oss
aliyun-sdk-oss
3.5.0
4.3.4 将OSS引入项目
4.3.4.1 准备OSSProperties类,用以装配OSS参数信息
所在工程:shangcouwang07-member-project-consumer
全类名:com.atguigu.crowd.config.OSSProperties
@Data
@AllArgsConstructor
@NoArgsConstructor
@Component
@ConfigurationProperties(prefix = "aliyun.oss")
public class OSSProperties {
//Endpoint(地域节点)
private String endPoint;
private String bucketName;
private String accessKeyId;
private String accessKeySecret;
//Bucket 域名
private String bucketDomain;
}
4.3.4.2 将 OSS 代码中用到的属性存入 yaml 配置文件
aliyun:
oss:
endPoint: http://oss-cn-hangzhou.aliyuncs.com
bucketName: nanb0815
accessKeyId: LTAI5t9YDW5GEDt698158gsq
accessKeySecret: HAWgkySb******************b
bucketDomain: http://nanb0815.oss-cn-hangzhou.aliyuncs.com
4.3.4.3 上传文件的工具方法:需要使用到SDK(引入依赖)
public class MyOSSUtils {
/**
* 专门负责上传文件到 OSS 服务器的工具方法
* @param endpoint OSS参数
* @param accessKeyId OSS参数
* @param accessKeySecret OSS参数
* @param inputStream 要上传的文件的输入流
* @param bucketName OSS参数
* @param bucketDomain OSS参数
* @param originalName 要上传的文件的原始文件名
* @return 包含上传结果以及上传的文件在 OSS 上的访问路径
*/
public static ResultEntity 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();
}
}
}
}
}
由于在第一次请求中需要建立缓存、建立连接,操作较多,所以比较耗时。如果按照默认的 ribbon 的超时时间来工作,第一次请求会超过这个时间导致超时报错。为避免这个问题,把ribbon 的超时时间延长。配置方式是在application.yaml中加入如下配置(那个工程需要在哪配):
ribbon:
ReadTimeout: 10000 # 通信超时时间(ms)
ConnectTimeout: 10000 # 连接超时时间(ms)
对象参数前面必须加上@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.1 以前上传文件时保存位置在Tomcat中
但面临着问题:
5.3.2 解决方案介绍
①自己搭建文件服务器
②使用第三方云服务
5.3.3 开通OSS服务步骤:具体步骤见4.3.1
①描述问题:以下的两个是不同网站,浏览器工作时不会使用相同的Cookie
②解决问题:以后重定向的地址都按照通过Zuul访问的方式写地址
redirect:http://localhost:80/auth/member/to/center/page
通过Zuul访问所有工程,在成功登陆之后,要前往会员中心页面。这时,在ZuulFilter中需要从Session域读取MemberLoginVO对象。SpringSession会从Redis中加载相关信息。相关信息中包含了MemberLoginVO类,用来反序列化。可是我们之前没有让Zuul工程依赖entity工程,所以找不到MemberLoginVO类。抛找不到异常。