org.springframework.boot
spring-boot-starter-parent
2.0.6.RELEASE
org.springframework.boot
spring-boot-starter-web
com.github.drtrang
druid-spring-boot2-starter
1.1.10
@Configuration
@PropertySource("classpath:jdbc.properties")
public class JdbcConfiguration {
@Value("${jdbc.url}")
String url;
@Value("${jdbc.driverClassName}")
String driverClassName;
@Value("${jdbc.username}")
String username;
@Value("${jdbc.password}")
String password;
@Bean
public DataSource dataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl(url);
dataSource.setDriverClassName(driverClassName);
dataSource.setUsername(username);
dataSource.setPassword(password);
return dataSource;
}
}
@ConfigurationProperties(prefix = "jdbc")
public class JdbcProperties {
private String url;
private String driverClassName;
private String username;
private String password;
// ... 略
// getters 和 setters
}
注意:没有指定属性文件的地址,SpringBoot默认会读取文件名为application.properties的资源文件,所以我们把jdbc.properties名称改为application.properties
SpringBoot的四种属性注入方式:用于在JdbcConfiguration中注入 JdbcProperties.class
@Autowired
private JdbcProperties jdbcProperties;
private JdbcProperties jdbcProperties;
public JdbcConfiguration(JdbcProperties jdbcProperties){
this.jdbcProperties = jdbcProperties;
}
@Configuration
@EnableConfigurationProperties(JdbcProperties.class)
public class JdbcConfiguration {
@Bean
public DataSource dataSource(JdbcProperties jdbcProperties) {
// ...
}
}
@Component
public class MyInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("preHandle method is running!");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("postHandle method is running!");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("afterCompletion method is running!");
}
}
@Configuration
public class MvcConfiguration implements WebMvcConfigurer {
@Autowired
private HandlerInterceptor myInterceptor;
/**
* 重写接口中的addInterceptors方法,添加自定义拦截器
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(myInterceptor).addPathPatterns("/**");
}
}
org.springframework.boot
spring-boot-starter-jdbc
mysql
mysql-connector-java
# 连接四大参数
spring.datasource.url=jdbc:mysql://localhost:3306/heima
spring.datasource.username=root
spring.datasource.password=root
# 连接池参数,可省略,SpringBoot自行判断
spring.datasource.driverClassName=com.mysql.jdbc.Driver
spring.datasource.hikari.idle-timeout=60000
spring.datasource.hikari.maximum-pool-size=30
spring.datasource.hikari.minimum-idle=10
org.mybatis.spring.boot
mybatis-spring-boot-starter
1.3.2
# mybatis 别名扫描
mybatis.type-aliases-package=cn.itcast.pojo
# mapper.xml文件位置,如果没有映射文件,请注释掉
mybatis.mapper-locations=classpath:mappers/*.xml
@Mapper
public interface UserMapper extends tk.mybatis.mapper.common.Mapper{
}
tk.mybatis
mapper-spring-boot-starter
2.0.2
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public User queryById(Long id){
return this.userMapper.selectByPrimaryKey(id);
}
@Transactional
public void deleteById(Long id){
this.userMapper.deleteByPrimaryKey(id);
}
}
@RestController
@RequestMapping("user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("{id}")
@ResponseBody
public User queryUserById(@PathVariable("id")Long id){
return this.userService.queryById(id);
}
@GetMapping("hello")
public String test(){
return "hello ssm";
}
}
org.springframework.boot
spring-boot-starter-thymeleaf
th-
指令:th-
是利用了Html5中的自定义属性来实现的。如果不支持H5,可以用data-th-
来代替
th:each
:类似于c:foreach
遍历集合,但是语法更加简洁th:text
:声明标签中的文本# 开发阶段关闭thymeleaf的模板缓存
spring.thymeleaf.cache=false
Ctrl + Shift + F9
对项目进行rebuild才可以。@SpringBootApplication
public class HttpDemoApplication {
public static void main(String[] args) {
SpringApplication.run(HttpDemoApplication.class, args);
}
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
@RunWith(SpringRunner.class)
@SpringBootTest(classes = HttpDemoApplication.class)
public class HttpDemoApplicationTests {
@Autowired
private RestTemplate restTemplate;
@Test
public void httpGet() {
// 调用springboot案例中的rest接口
User user = this.restTemplate.getForObject("http://localhost/user/1", User.class);
System.out.println(user);
}
}
tk.mybatis
mapper-spring-boot-starter
2.0.4
RunDashboard-show,可以快速启动。如果取消则在leyou资料里idea快速上手指南打开
server:
port: 8081
spring:
datasource:
url: jdbc:mysql://localhost:3306/mybatis #你学习mybatis时,使用的数据库地址
username: root
password: root
mybatis:
type-aliases-package: cn.itcast.service.pojo
@SpringBootApplication
public class ItcastServiceConsumerApplication {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(ItcastServiceConsumerApplication.class, args);
}
}
编写配置(application.yml)
controller-UserController
mapper-UserMapper
pojo-User
servcieUserService
启动测试
其实上面说的问题,概括一下就是分布式服务必然要面临的问题:
以上的问题,我们都将在SpringCloud中得到答案。
server:
port: 10086 # 端口
spring:
application:
name: eureka-server # 应用名称,会在Eureka中显示
eureka:
client:
service-url: # EurekaServer的地址,现在是自己的地址,如果是集群,需要加上其它Server的地址。
defaultZone: http://127.0.0.1:${server.port}/eureka
@SpringBootApplication
@EnableEurekaServer // 声明当前springboot应用是一个eureka服务中心
public class ItcastEurekaApplication {
public static void main(String[] args) {
SpringApplication.run(ItcastEurekaApplication.class, args);
}
}
org.springframework.cloud
spring-cloud-dependencies
Finchley.SR2
pom
import
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
server:
port: 8081
spring:
datasource:
url: jdbc:mysql://localhost:3306/heima
username: root
password: root
driverClassName: com.mysql.jdbc.Driver
application:
name: service-provider # 应用名称,注册到eureka后的服务名称
mybatis:
type-aliases-package: cn.itcast.service.pojo
eureka:
client:
service-url: # EurekaServer地址
defaultZone: http://127.0.0.1:10086/eureka
@SpringBootApplication
@MapperScan("cn.itcast.service.mapper")//mapper 接口包扫描
@EnableDiscoveryClient //启动Eureka的客户端 @EnableEurekaClient 也可以,不常用
public class ItcastServiceProviderApplication {
public static void main(String[] args) {
SpringApplication.run(ItcastServiceApplication.class, args);
}
}
org.springframework.cloud
spring-cloud-dependencies
Finchley.SR2
pom
import
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
server:
port: 80
spring:
application:
name: service-consumer
eureka:
client:
service-url:
defaultZone: http://localhost:10086/eureka
@SpringBootApplication
@EnableDiscoveryClient // 开启Eureka客户端
public class ItcastServiceConsumerApplication {
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(ItcastServiceConsumerApplication.class, args);
}
}
@Controller
@RequestMapping("consumer/user")
public class UserController {
@Autowired
private RestTemplate restTemplate;
@Autowired
private DiscoveryClient discoveryClient; // eureka客户端,可以获取到eureka中服务的信息
@GetMapping
@ResponseBody
public User queryUserById(@RequestParam("id") Long id){
// 根据服务名称,获取服务实例。有可能是集群,所以是service实例集合
List instances = discoveryClient.getInstances("service-provider");
// 因为只有一个Service-provider。所以获取第一个实例
ServiceInstance instance = instances.get(0);
// 获取ip和端口信息,拼接成服务地址
String baseUrl = "http://" + instance.getHost() + ":" + instance.getPort() + "/user/" + id;
User user = this.restTemplate.getForObject(baseUrl, User.class);
return user;
}
}
server:
port: 10086 # 端口
spring:
application:
name: eureka-server # 应用名称,会在Eureka中显示
eureka:
client:
service-url: # 配置其他Eureka服务的地址,而不是自己,比如10087
defaultZone: http://127.0.0.1:10087/eureka
server:
port: 10087 # 端口
spring:
application:
name: eureka-server # 应用名称,会在Eureka中显示
eureka:
client:
service-url: # 配置其他Eureka服务的地址,而不是自己,比如10087
defaultZone: http://127.0.0.1:10086/eureka
eureka.client.register-with-eureka=true
参数是否正确,事实上默认就是true。如果值确实为true,则会向EurekaServer发起一个Rest请求,并携带自己的元数据信息,Eureka Server会把这些信息保存到一个双层Map结构中。spring.application.name
属性locahost:service-provider:8081
eureka:
instance:
lease-expiration-duration-in-seconds: 90
lease-renewal-interval-in-seconds: 30
eureka.client.fetch-registry=true
参数的值,如果为true,则会拉取Eureka Server服务的列表只读备份,然后缓存在本地。并且每隔30秒
会重新获取并更新数据。我们可以通过下面的参数来修改:eureka:
client:
registry-fetch-interval-seconds: 5
生产环境中,我们不需要修改这个值。
但是为了开发环境下,能够快速得到服务的最新状态,我们可以将其设置小一点。
eureka.server.eviction-interval-timer-in-ms
参数对其进行修改,单位是毫秒,生产环境不要修改。eureka:
server:
enable-self-preservation: false # 关闭自我保护模式(缺省为打开)
eviction-interval-timer-in-ms: 1000 # 扫描失效服务的间隔时间(缺省为60*1000ms)
@LoadBalanced
注解:@Controller
@RequestMapping("consumer/user")
public class UserController {
@Autowired
private RestTemplate restTemplate;
//@Autowired
//private DiscoveryClient discoveryClient; // 注入discoveryClient,通过该客户端获取服务列表
@GetMapping
@ResponseBody
public User queryUserById(@RequestParam("id") Long id){
// 通过client获取服务提供方的服务列表,这里我们只有一个
// ServiceInstance instance = discoveryClient.getInstances("service-provider").get(0);
String baseUrl = "http://service-provider/user/" + id;
User user = this.restTemplate.getForObject(baseUrl, User.class);
return user;
}
}
server:
port: 80
spring:
application:
name: service-consumer
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
service-provider:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
{服务名称}.ribbon.NFLoadBalancerRuleClassName
,值就是IRule的实现类。
org.springframework.cloud
spring-cloud-starter-netflix-hystrix
@SpringCloudApplication
public class ItcastServiceConsumerApplication {
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(ItcastServiceConsumerApplication.class, args);
}
}
@Controller
@RequestMapping("consumer/user")
//@DefaultProperties(defaultFallback = "fallBackMethod") // 指定一个类的全局熔断方法
public class UserController {
@Autowired
private RestTemplate restTemplate;
@GetMapping
@ResponseBody
@HystrixCommand(fallbackMethod = "queryUserByIdFallBack")
public String queryUserById(@RequestParam("id") Long id) {
String user = this.restTemplate.getForObject("http://service-provider/user/" + id, String.class);
return user;
}
public String queryUserByIdFallBack(Long id){
return "请求繁忙,请稍后再试!";
}
}
@GetMapping("{id}")
@HystrixCommand
public String queryUserById(@PathVariable("id") Long id){
if(id == 1){
throw new RuntimeException("太忙了");
}
String user = this.restTemplate.getForObject("http://service-provider/user/" + id, String.class);
return user;
}
circuitBreaker.requestVolumeThreshold=10
circuitBreaker.sleepWindowInMilliseconds=10000
circuitBreaker.errorThresholdPercentage=50
解读:
Feign可以把Rest的请求进行隐藏,伪装成类似SpringMVC的Controller一样。你不用再自己拼接url,拼接参数等等操作,一切都交给Feign去做。
org.springframework.cloud
spring-cloud-starter-openfeign
启动类(consumer的)上添加注解开启Feign功能
@SpringCloudApplication
@EnableFeignClients // 开启feign客户端
public class ItcastServiceConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(ItcastServiceConsumerApplication.class, args);
}
}
删除RestTemplate:feign已经自动集成了Ribbon负载均衡的RestTemplate。所以,此处不需要再注册RestTemplate。
在itcast-service-consumer工程中,添加client/UserClient接口:
@FeignClient(value = "service-provider") // 标注该类是一个feign接口
public interface UserClient {
@GetMapping("user/{id}")
User queryById(@PathVariable("id") Long id);
}
@FeignClient
,声明这是一个Feign客户端,类似@Mapper
注解。同时通过value
属性指定服务名称@Controller
@RequestMapping("consumer/user")
public class UserController {
@Autowired
private UserClient userClient;
@GetMapping
@ResponseBody
public User queryUserById(@RequestParam("id") Long id){
User user = this.userClient.queryUserById(id);
return user;
}
}
RestTemplate
对象。feign:
hystrix:
enabled: true # 开启Feign的熔断功能
1)首先,我们要定义一个类UserClientFallback(在client文件夹中),实现刚才编写的UserClient,作为fallback的处理类
@Component
public class UserClientFallback implements UserClient {
@Override
public User queryById(Long id) {
User user = new User();
user.setUserName("服务器繁忙,请稍后再试!");
return user;
}
}
2)然后在UserFeignClient中,指定刚才编写的实现类
@FeignClient(value = "service-provider", fallback = UserClientFallback.class) // 标注该类是一个feign接口
public interface UserClient {
@GetMapping("user/{id}")
User queryUserById(@PathVariable("id") Long id);
}
我们使用Spring Cloud Netflix中的Eureka实现了服务注册中心以及服务注册与发现;而服务间通过Ribbon或Feign实现服务的消费以及均衡负载。为了使得服务集群更为健壮,使用Hystrix的融断机制来避免在微服务架构中个别服务出现异常时引起的故障蔓延。
在该架构中,我们的服务集群包含:内部服务Service A和Service B,他们都会注册与订阅服务至Eureka Server,而Open Service是一个对外的服务,通过均衡负载公开至服务调用方。我们把焦点聚集在对外服务这块,直接暴露我们的服务地址,这样的实现是否合理,或者是否有更好的实现方式呢?
server:
port: 10010 #服务端口
spring:
application:
name: api-gateway #指定服务名
通过@EnableZuulProxy
注解开启Zuul的功能:
@SpringBootApplication
@EnableZuulProxy // 开启网关功能
public class ItcastZuulApplication {
public static void main(String[] args) {
SpringApplication.run(ItcastZuulApplication.class, args);
}
}
映射规则
server:
port: 10010 #服务端口
spring:
application:
name: api-gateway #指定服务名
zuul:
routes:
service-provider: # 这里是路由id,随意写
path: /service-provider/** # 这里是映射路径
url: http://127.0.0.1:8081 # 映射路径对应的实际url地址
在刚才的路由规则中,我们把路径对应的服务地址写死了
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
eureka:
client:
registry-fetch-interval-seconds: 5 # 获取服务列表的周期:5s
service-url:
defaultZone: http://127.0.0.1:10086/eureka
@SpringBootApplication
@EnableZuulProxy // 开启Zuul的网关功能
@EnableDiscoveryClient
public class ZuulDemoApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulDemoApplication.class, args);
}
}
因为已经有了Eureka客户端,我们可以从Eureka获取服务的地址信息,因此映射时无需指定IP地址,而是通过服务名称来访问,而且Zuul已经集成了Ribbon的负载均衡功能。
zuul:
routes:
service-provider: # 这里是路由id,随意写
path: /service-provider/** # 这里是映射路径
serviceId: service-provider # 指定服务名称
在刚才的配置中,我们的规则是这样的:
zuul.routes..path=/xxx/**
: 来指定映射路径。
是自定义的路由名zuul.routes..serviceId=service-provider
:来指定服务名。而大多数情况下,我们的
路由名称往往和服务名会写成一样的。因此Zuul就提供了一种简化的配置语法:zuul.routes.
比方说上面我们关于service-provider的配置可以简化为一条:
zuul:
routes:
service-provider: /service-provider/** # 这里是映射路径
省去了对服务名称的配置。
在使用Zuul的过程中,上面讲述的规则已经大大的简化了配置项。但是当服务较多时,配置也是比较繁琐的。因此Zuul就指定了默认的路由规则:
service-provider
,则默认的映射路径就 是:/service-provider/**
也就是说,刚才的映射规则我们完全不配置也是OK的,不信就试试看。
配置示例:
zuul:
routes:
service-provider: /service-provider/**
service-consumer: /service-consumer/**
prefix: /api # 添加路由前缀
我们通过zuul.prefix=/api
来指定了路由的前缀,这样在发起请求时,路径就要以/api开头。
ZuulFilter是过滤器的顶级父类。在这里我们看一下其中定义的4个最重要的方法:
public abstract ZuulFilter implements IZuulFilter{
abstract public String filterType();
abstract public int filterOrder();
boolean shouldFilter();// 来自IZuulFilter
Object run() throws ZuulException;// IZuulFilter
}
shouldFilter
:返回一个Boolean
值,判断该过滤器是否需要执行。返回true执行,返回false不执行。run
:过滤器的具体业务逻辑。filterType
:返回字符串,代表过滤器的类型。包含以下4种:
pre
:请求在被路由之前执行route
:在路由请求时调用post
:在route和errror过滤器之后调用error
:处理请求时发生错误调用filterOrder
:通过返回的int值来定义过滤器的执行顺序,数字越小优先级越高。@Component
public class LoginFilter extends ZuulFilter {
/**
* 过滤器类型,前置过滤器
* @return
*/
@Override
public String filterType() {
return "pre";
}
/**
* 过滤器的执行顺序
* @return
*/
@Override
public int filterOrder() {
return 1;
}
/**
* 该过滤器是否生效
* @return
*/
@Override
public boolean shouldFilter() {
return true;
}
/**
* 登陆校验逻辑
* @return
* @throws ZuulException
*/
@Override
public Object run() throws ZuulException {
// 获取zuul提供的上下文对象
RequestContext context = RequestContext.getCurrentContext();
// 从上下文对象中获取请求对象
HttpServletRequest request = context.getRequest();
// 获取token信息
String token = request.getParameter("access-token");
// 判断
if (StringUtils.isBlank(token)) {
// 过滤该请求,不对其进行路由
context.setSendZuulResponse(false);
// 设置响应状态码,401
context.setResponseStatusCode(HttpStatus.SC_UNAUTHORIZED);
// 设置响应信息
context.setResponseBody("{\"status\":\"401\", \"text\":\"request error!\"}");
}
// 校验通过,把登陆信息放入上下文信息,继续向后执行
context.set("token", token);
return null;
}
}
Zuul中默认就已经集成了Ribbon负载均衡和Hystix熔断机制。但是所有的超时策略都是走的默认值,比如熔断超时时间只有1S,很容易就触发了。因此建议我们手动进行配置:
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 2000 # 设置hystrix的超时时间为6000ms
各种企业里面用的管理系统(ERP、HR、OA、CRM、物流管理系统。。。。。。。)
门户网站、电商网站:baidu.com、qq.com、taobao.com、jd.com …
而我们今天要聊的就是互联网项目中的重要角色:电商
SaaS:软件即服务
SOA:面向服务
RPC:远程过程调用
RMI:远程方法调用
PV:(page view),即页面浏览量;用户每1次对网站中的每个网页访问均被记录1次。用户对同一页面的多次访问,访问量累计
UV:(unique visitor),独立访客。指访问某个站点或点击某条新闻的不同IP地址的人数。在同一天内,uv只记录第一次进入网站的具有独立IP的访问者,在同一天内再次访问该网站则不计数。
PV与带宽:
- 计算带宽大小需要关注两个指标:峰值流量和页面的平均大小。
- 计算公式是:网站带宽= ( PV * 平均页面大小(单位MB)* 8 )/统计时间(换算到秒)
- 为什么要乘以8?
- 网站大小为单位是字节(Byte),而计算带宽的单位是bit,1Byte=8bit
- 这个计算的是平均带宽,高峰期还需要扩大一定倍数
PV、QPS、并发
后台管理:
前台门户
无论是前台还是后台系统,都共享相同的微服务集群,包括:
前端技术:
后端技术:
我们在开发的过程中,为了保证以后的生产、测试环境统一。尽量都采用域名来访问项目。
一级域名:www.leyou.com,leyou.com leyou.cn
二级域名:manage.leyou.com/item , api.leyou.com
我们可以通过switchhost工具来修改自己的host对应的地址,只要把这些域名指向127.0.0.1,那么跟你用localhost的效果是完全一样的。
<?xml version="1.0" encoding="UTF-8"?>
://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
>4.0.0 >
>com.leyou.parent >
>leyou >
>1.0.0-SNAPSHOT >
>pom >
>leyou >
>Demo project for Spring Boot >
>
>org.springframework.boot >
>spring-boot-starter-parent >
>2.0.7.RELEASE >
> <!-- lookup parent from repository -->
>
>UTF-8 >
>UTF-8 >
>1.8 >
-cloud.version>Finchley.SR2 -cloud.version>
>1.3.2 >
>2.0.2 >
>1.1.9 >
>5.1.32 >
>1.2.3 >
>1.0.0-SNAPSHOT >
>1.26.1-RELEASE >
>
>
>
<!-- springCloud -->
org.springframework.cloud
spring-cloud-dependencies
${spring-cloud.version}
pom
import
org.mybatis.spring.boot
mybatis-spring-boot-starter
${mybatis.starter.version}
tk.mybatis
mapper-spring-boot-starter
${mapper.starter.version}
com.github.pagehelper
pagehelper-spring-boot-starter
${pageHelper.starter.version}
mysql
mysql-connector-java
${mysql.version}
com.github.tobato
fastdfs-client
${fastDFS.client.version}
>
>
>
>
>
>org.springframework.boot >
>spring-boot-maven-plugin >
>
>
>
>
<?xml version="1.0" encoding="UTF-8"?>
://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
>
>leyou >
>com.leyou.parent >
>1.0.0-SNAPSHOT >
>
>4.0.0 >
>com.leyou.common >
>leyou-registry >
>1.0.0-SNAPSHOT >
>
>
>org.springframework.cloud >
>spring-cloud-starter-netflix-eureka-server >
>
>
>
注意:今天在运行程序的时候,一直报“java.lang.TypeNotPresentException: Type javax.xml.bind.JAXBContext not present”的错误,
百度原因,发现是因为用了jdk12的缘故。因为JAXB-API是java ee的一部分,在jdk12中没有在默认的类路径中。从jdk9开始java引入了模块的概念, 可以使用模块命令–add-modles java.xml.bind引入jaxb-api。也可以选择另一种解决方法,在maven里面加入下面依赖,可以解决这个问题:
javax.xml.bind
jaxb-api
2.3.0
com.sun.xml.bind
jaxb-impl
2.3.0
org.glassfish.jaxb
jaxb-runtime
2.3.0
javax.activation
activation
1.1.1
@SpringBootApplication
@EnableEurekaServer
public class LeyouRegistryApplication {
public static void main(String[] args) {
SpringApplication.run(LeyouRegistryApplication.class, args);
}
}
server:
port: 10086
spring:
application:
name: leyou-registry
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:${server.port}/eureka
register-with-eureka: false # 把自己注册到eureka服务列表
fetch-registry: false # 拉取eureka服务信息
server:
enable-self-preservation: false # 关闭自我保护
eviction-interval-timer-in-ms: 5000 # 每隔5秒钟,进行一次服务列表的清理
leyou
com.leyou.parent
1.0.0-SNAPSHOT
4.0.0
com.leyou.common
leyou-gateway
1.0.0-SNAPSHOT
org.springframework.cloud
spring-cloud-starter-netflix-zuul
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
org.springframework.boot
spring-boot-starter-actuator
@SpringBootApplication
@EnableDiscoveryClient
@EnableZuulProxy
public class LeyouGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(LeyouGatewayApplication.class, args);
}
}
server:
port: 10010
spring:
application:
name: leyou-gateway
eureka:
client:
registry-fetch-interval-seconds: 5
service-url:
defaultZone: http://127.0.0.1:10086/eureka
zuul:
prefix: /api # 路由路径前缀
包含对商品相关的一系列内容管理
为了方便微服务之间相互调用,使用聚合工程将要提供的接口及相关实体类放到独立子工程中
在leyou-item中创建两个子工程:
leyou
com.leyou.parent
1.0.0-SNAPSHOT
4.0.0
com.leyou.item
leyou-item
1.0.0-SNAPSHOT
pom
leyou-item-interface
在leyou-item工程上点击右键,选择new --> module:
依然是使用maven构建,注意父工程是leyou-item:
Group:com.leyou.item Artifact:leyou-item-interface Version:1.0.0-SNAPSHOT
Module name:leyou-item-interface
注意:目录结构,保存到leyou-item
下的leyou-item-interface
目录中:
leyou-item-service
在leyou-item工程上点击右键,选择new --> module:
依然是使用maven构建,注意父工程是leyou-item:
Group:com.leyou.item Artifact:leyou-item-service Version:1.0.0-SNAPSHOT
Module name:leyou-item-service
注意:目录结构,保存到leyou-item
下的leyou-item-service
目录中:
ly-item-interface
中的实体类这些依赖,我们在顶级父工程:leyou中已经添加好了。所以直接引入即可:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>leyou-itemartifactId>
<groupId>com.leyou.itemgroupId>
<version>1.0.0-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<groupId>com.leyou.itemgroupId>
<artifactId>leyou-item-serviceartifactId>
<version>1.0.0-SNAPSHOTversion>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>tk.mybatisgroupId>
<artifactId>mapper-spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>com.github.pagehelpergroupId>
<artifactId>pagehelper-spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-jdbcartifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
<dependency>
<groupId>com.leyou.itemgroupId>
<artifactId>leyou-item-interfaceartifactId>
<version>1.0.0-SNAPSHOTversion>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
dependencies>
project>
leyou-item-interface中需要什么我们暂时不清楚,所以先不管。以后需要什么依赖,再引入。
在整个leyou-item工程
中,只有leyou-item-service
是需要启动的。因此在其中编写启动类即可:
@SpringBootApplication
@EnableDiscoveryClient
public class LeyouItemServiceApplication {
public static void main(String[] args) {
SpringApplication.run(LeyouItemServiceApplication.class, args);
}
}
然后是全局属性文件:
server:
port: 8081
spring:
application:
name: item-service
datasource:
url: jdbc:mysql://localhost:3306/leyou
username: root
password: root
hikari:
max-lifetime: 28830000 # 一个连接的生命时长(毫秒),超时而且没被使用则被释放(retired),缺省:30分钟,建议设置比数据库超时时长少30秒,参考MySQL wait_timeout参数(show variables like '%timeout%';)
maximum-pool-size: 9 # 连接池中允许的最大连接数。缺省值:10;推荐的公式:((core_count * 2) + effective_spindle_count)
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
instance:
lease-renewal-interval-in-seconds: 5 # 5秒钟发送一次心跳
lease-expiration-duration-in-seconds: 10 # 10秒不发送就过期
既然商品微服务已经创建,接下来肯定要添加路由规则到Zuul中,我们不使用默认的路由规则。
修改leyou-gateway工程的application.yml配置文件:
zuul:
prefix: /api # 路由路径前缀
routes:
item-service: /item/** # 商品微服务的映射路径
其实不需要,SpringBoot提供了一个依赖:actuator
只要我们添加了actuator的依赖,它就会为我们生成一系列的访问接口:
添加依赖:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
重启后访问Eureka控制台:
右键leyou工程,使用maven来构建module:
Group:com.leyou.common Artifact:leyou-common Version:1.0.0-SNAPSHOT
Module name:leyou-common
就是ECMAScript第6版标准。
新建空工程 demo-es6
新建模块、Static Web -StaticWeb
var 定义的变量可能变为循环变量
let定义的变量只在代码块内有效
const声明的变量是常量,不能被修改
新API
includes()
:返回布尔值,表示是否找到了参数字符串。startsWith()
:返回布尔值,表示参数字符串是否在原字符串的头部。endsWith()
:返回布尔值,表示参数字符串是否在原字符串的尾部。字符串模板:在两个`之间的部分都会被作为字符串的值,不管你任意换行,甚至加入js脚本
数组解构
比如对一个数组 原本智能通过角标访问
现在可以直接赋值
let arr = [1,2,3]
const [x,y,z] = arr;// x,y,z将与arr中的每个位置对应来取值
// 然后打印
console.log(x,y,z); //得1 2 3
对象解构
const person = {
name:“jack”,
age:21,
language: [‘java’,‘js’,‘css’]
}
const {name,age,language} = person; 各自完成赋值
如果要用其他变量接收,需要额外指定别名
const {name:n} =person
{name:n}
:name是person中的属性名,冒号后面的n是解构后要赋值给的变量。
参数默认值
在ES6以前,我们无法给一个函数参数设置默认值,只能采用变通写法:
function add(a , b) {
// 判断b是否为空,为空就给默认值1
b = b || 1;
return a + b;
}
// 传一个参数
console.log(add(10));
//now
function add(a , b = 1) {
return a + b;
}
// 传一个参数
console.log(add(10));
箭头参数
一个参数时:
var print = function (obj) {
console.log(obj);
}
// 简写为:
var print2 = obj => console.log(obj);
多个参数:
// 两个参数的情况:
var sum = function (a , b) {
return a + b;
}
// 简写为:
var sum2 = (a,b) => a+b;
代码不止一行,可以用{}
括起来
var sum3 = (a,b) => {
return a + b;
}
对象的函数属性简写
比如一个Person对象,里面有eat方法:
let person = {
name: "jack",
// 以前:
eat: function (food) {
console.log(this.name + "在吃" + food);
},
// 箭头函数版:
eat2: food => console.log(person.name + "在吃" + food),// 这里拿不到this
// 简写版:
eat3(food){
console.log(this.name + "在吃" + food);
}
}
箭头函数结合解构表达式
比如有一个函数:
const person = {
name:"jack",
age:21,
language: ['java','js','css']
}
function hello(person) {
console.log("hello," + person.name)
}
如果用箭头函数和解构表达式
var hi = ({name}) => console.log("hello," + name);
map
map()
:接收一个函数,将原数组中的所有元素用这个函数处理后放入新数组返回。
举例:有一个字符串数组,我们希望转为int数组
let arr = ['1','20','-5','3'];
console.log(arr)
arr = arr.map(s => parseInt(s));
console.log(arr)
reduce
reduce()
:接收一个函数(必须)和一个初始值(可选)。
第一个参数(函数)接收两个参数:
reduce()
会从左到右依次把数组中的元素用reduce处理,并把处理的结果作为下次reduce的第一个参数。如果是第一次,会把前两个元素作为计算参数,或者把用户指定的初始值作为起始参数
举例:
const arr = [1,20,-5,3]
没有初始值:
arr.reduce((a,b)=> a+b) //19
指定初始值:
arr.reduce((a,b)=> a*b,1) //-300
ES6给Object拓展了许多新的方法,如:
[[k1,v1],[k2,v2],...]
基于事件循环的异步IO框架:Node.js
在node的基础上
都是MVC的分支
Vue (读音 /vjuː/,类似于 view) 用于构建用户界面的渐进式框架
NPM是Node提供的模块管理工具,可以非常方便的下载安装很多前端框架,包括Jquery、AngularJS、VueJs都有。为了后面学习方便,我们先安装node及NPM工具。
https://nodejs.org/en/
查看node版本信息:控制台输入node-v
node自带NPM //dos 用命令 npm-v 可查看
npm默认的仓库地址是在国外网站,速度较慢,建议大家设置到淘宝镜像。但是切换镜像是比较麻烦的。推荐一款切换镜像的工具:nrm
nrm ls
命令查看npm的仓库列表,带*的就是当前选中的镜像仓库:nrm use taobao
来指定要使用的镜像源:nrm test npm
来测试速度:new module static web
执行命令方式
<body>
<div id="app">
<h2>{{name}},非常帅!!!h2>
div>
body>
<script src="node_modules/vue/dist/vue.js" >script>
<script>
// 创建vue实例
var app = new Vue({
el:"#app", // el即element,该vue实例要渲染的页面元素
data:{ // 渲染页面需要的数据
name: "峰哥"
}
});
script>
h2
元素中,我们通过{{name}}的方式,来渲染刚刚定义的name属性。我们对刚才的案例进行简单修改:
<body>
<div id="app">
<input type="text" v-model="num">
<h2>
{{name}},非常帅!!!有{{num}}位女神为他着迷。
h2>
div>
body>
<script src="node_modules/vue/dist/vue.js" >script>
<script>
// 创建vue实例
var app = new Vue({
el: "#app", // el即element,该vue实例要渲染的页面元素
data: { // 渲染页面需要的数据
name: "峰哥",
num: 5
},
methods:{
incr(){
this.num++;
}
}
});
script>
num
input
元素,通过v-model
与num
进行绑定。{{num}}
在页面输出我们可以观察到,输入框的变化引起了data中的num的变化,同时页面输出也跟着变化。
{{num}}
与数据num绑定,因此num值变化,引起了页面效果变化。没有任何dom操作,这就是双向绑定的魅力。
我们在页面添加一个按钮:
<button v-on:click="num++">点我button>
v-on
指令绑定点击事件,而不是普通的onclick
,然后直接操作num每个 Vue 应用都是通过用 Vue
函数创建一个新的 Vue 实例开始的:
var vm = new Vue({
// 选项
})
在构造函数中传入一个对象,并且在对象中声明各种Vue需要的数据和方法,包括:
等等
每个Vue实例都需要关联一段Html模板,Vue会基于此模板进行视图渲染。
我们可以通过el属性来指定。
当Vue实例被创建时,它会尝试获取在data中定义的所有属性,用于视图的渲染,并且监视data中的属性变化,当data发生改变,所有相关的视图都将重新渲染,这就是“响应式“系统。
input
的值Vue实例中除了可以定义data属性,也可以定义方法,并且在Vue实例的作用范围内使用。
每个 Vue 实例在被创建时都要经过一系列的初始化过程 :创建实例,装载模板,渲染模板等等。Vue为生命周期中的每个状态都设置了钩子函数(监听函数)。每当Vue实例处于不同的生命周期时,对应的函数就会被触发调用。
beforeCreated:我们在用Vue时都要进行实例化,因此,该函数就是在Vue实例化时调用,也可以将他理解为初始化函数比较方便一点,在Vue1.0时,这个函数的名字就是init。
created(常用):在创建实例之后进行调用。
beforeMount:页面加载完成,没有渲染。如:此时页面还是{{name}}
mounted:我们可以将他理解为原生js中的window.οnlοad=function({.,.}),或许大家也在用jquery,所以也可以理解为jquery中的$(document).ready(function(){….}),他的功能就是:在dom文档渲染完毕之后将要执行的函数,该函数在Vue1.0版本中名字为compiled。 此时页面中的{{name}}已被渲染成峰哥
beforeDestroy:该函数将在销毁实例前进行调用 。
destroyed:改函数将在销毁实例时进行调用。
beforeUpdate:组件更新之前。
updated:组件更新之后。
指令 (Directives) 是带有 v-
前缀的特殊特性。指令特性的预期值是:单个 JavaScript 表达式。指令的职责是,当表达式的值改变时,将其产生的连带影响,响应式地作用于 DOM。
格式:
{{表达式}}
说明:
花括号缺陷:使用{{}}方式在网速较慢时会出现问题。在数据未加载完成时,页面会显示出原始的{{}}
,加载完毕后才显示正确数据,我们称为插值闪烁。
使用v-text和v-html指令来替代{{}}
说明:
并且不会出现插值闪烁,当没有数据时,会显示空白。
<div id="app">
v-text:<span v-text="hello">span> <br/>
v-html:<span v-html="hello">span>
div>
JS:
var vm = new Vue({
el:"#app",
data:{
hello: "大家好,我是峰哥
"
}
})
刚才的v-text和v-html可以看做是单向绑定,数据影响了视图渲染,但是反过来就不行。接下来学习的v-model是双向绑定,视图(View)和模型(Model)之间会互相影响。
既然是双向绑定,一定是在视图中可以修改数据,这样就限定了视图的元素类型。目前v-model的可使用元素有:
基本上除了最后一项,其它都是表单的输入项。
举例:
<div id="app">
<input type="checkbox" v-model="language" value="Java" />Java<br/>
<input type="checkbox" v-model="language" value="PHP" />PHP<br/>
<input type="checkbox" v-model="language" value="Swift" />Swift<br/>
<h1>
你选择了:{{language.join(',')}}
h1>
div>
<script src="./node_modules/vue/dist/vue.js">script>
<script type="text/javascript">
var vm = new Vue({
el:"#app",
data:{
language: []
}
})
script>
CheckBox
对应一个model时,model的类型是一个数组,单个checkbox值默认是boolean类型text
和textarea
默认对应的model是字符串select
单选对应字符串,多选对应也是数组v-on指令用于给页面元素绑定事件。
语法:
v-on:事件名="js片段或函数名"
示例:
<div id="app">
<button v-on:click="num++">增加一个button><br/>
<button v-on:click="decrement">减少一个button><br/>
<h1>有{{num}}个女神迷恋峰哥h1>
div>
<script src="./node_modules/vue/dist/vue.js">script>
<script type="text/javascript">
var app = new Vue({
el:"#app",
data:{
num:100
},
methods:{
decrement(){
this.num--;
}
}
})
script>
另外,事件绑定可以简写,例如v-on:click='add'
可以简写为@click='add'
在事件处理程序中调用 event.preventDefault()
或 event.stopPropagation()
是非常常见的需求。尽管我们可以在方法中轻松实现这点,但更好的方式是:方法只有纯粹的数据逻辑,而不是去处理 DOM 事件细节。
为了解决这个问题,Vue.js 为 v-on
提供了事件修饰符。修饰符是由点开头的指令后缀来表示的。
.stop
:阻止事件冒泡到父元素.prevent
:阻止默认事件发生*.capture
:使用事件捕获模式.self
:只有元素自身触发事件才执行。(冒泡或捕获的都不执行).once
:只执行一次阻止默认事件
<div id="app">
<button v-on:contextmenu.prevent="num++">增加一个button>
<br/>
<button v-on:contextmenu="decrement($event)">减少一个button>
<br/>
<h1>有{{num}}个女神迷恋峰哥h1>
div>
<script src="./node_modules/vue/dist/vue.js">script>
<script type="text/javascript">
var app = new Vue({
el: "#app",
data: {
num: 100
},
methods: {
decrement(ev) {
// ev.preventDefault();
this.num--;
}
}
})
script>
效果:(右键“增加一个”,不会触发默认的浏览器右击事件;右键“减少一个”,会触发默认的浏览器右击事件)
在监听键盘事件时,我们经常需要检查常见的键值。Vue 允许为 v-on
在监听键盘事件时添加按键修饰符:
<input v-on:keyup.13="submit">
记住所有的 keyCode
比较困难,所以 Vue 为最常用的按键提供了别名:
<input v-on:keyup.enter="submit">
<input @keyup.enter="submit">
全部的按键别名:
.enter
*.tab
.delete
(捕获“删除”和“退格”键).esc
.space
.up
.down
.left
.right
可以用如下修饰符来实现仅在按下相应按键时才触发鼠标或键盘事件的监听器。
.ctrl
.alt
.shift
例如:
<input @keyup.alt.67="clear">
<div @click.ctrl="doSomething">Do somethingdiv>
语法:
v-for="item in items"
示例
<div id="app">
<ul>
<li v-for="user in users">
{{user.name}} - {{user.gender}} - {{user.age}}
li>
ul>
div>
<script src="./node_modules/vue/dist/vue.js">script>
<script type="text/javascript">
var app = new Vue({
el: "#app",
data: {
users:[
{name:'柳岩', gender:'女', age: 21},
{name:'峰哥', gender:'男', age: 18},
{name:'范冰冰', gender:'女', age: 24},
{name:'刘亦菲', gender:'女', age: 18},
{name:'古力娜扎', gender:'女', age: 25}
]
},
})
script>
在遍历的过程中,如果我们需要知道数组角标,可以指定第二个参数:
语法
v-for="(item,index) in items"
示例
<ul>
<li v-for="(user, index) in users">
{{index + 1}}. {{user.name}} - {{user.gender}} - {{user.age}}
li>
ul>
v-for除了可以迭代数组,也可以迭代对象。语法基本类似
语法:
v-for="value in object"
v-for="(value,key) in object"
v-for="(value,key,index) in object"
示例:
<div id="app">
<ul>
<li v-for="(value, key, index) in user">
{{index + 1}}. {{key}} - {{value}}
li>
ul>
div>
<script src="./node_modules/vue/dist/vue.js">script>
<script type="text/javascript">
var vm = new Vue({
el:"#app",
data:{
user:{name:'峰哥', gender:'男', age: 18}
}
})
script>
当 Vue.js 用 v-for
正在更新已渲染过的元素列表时,它默认用“就地复用”策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序, 而是简单复用此处每个元素,并且确保它在特定索引下显示已被渲染过的每个元素。
这个功能可以有效的提高渲染的效率。
但是要实现这个功能,你需要给Vue一些提示,以便它能跟踪每个节点的身份,从而重用和重新排序现有元素,你需要为每项提供一个唯一 key
属性。理想的 key
值是每项都有的且唯一的 id。
示例:
<ul>
<li v-for="(item,index) in items" :key=index>li>
ul>
:key=""
我们后面会讲到,它可以让你读取vue中的属性,并赋值给key属性v-if,顾名思义,条件判断。当得到结果为true时,所在的元素才会被渲染。
语法:
v-if="布尔表达式"
示例:
<div id="app">
<button v-on:click="show = !show">点我呀button>
<br>
<h1 v-if="show">
看到我啦?!
h1>
<h1 v-show="show">
看到我啦?!show
h1>
div>
<script src="./node_modules/vue/dist/vue.js">script>
<script type="text/javascript">
var app = new Vue({
el: "#app",
data: {
show: true
}
})
script>
当v-if和v-for出现在一起时,v-for优先级更高。也就是说,会先遍历,再判断条件。
修改v-for中的案例,添加v-if:
<ul>
<li v-for="(user, index) in users" v-if="user.gender == '女'">
{{index + 1}}. {{user.name}} - {{user.gender}} - {{user.age}}
li>
ul>
你可以使用 v-else
指令来表示 v-if
的“else 块”:
<div id="app">
<h1 v-if="Math.random() > 0.5">
看到我啦?!if
h1>
<h1 v-else>
看到我啦?!else
h1>
div>
v-else
元素必须紧跟在带 v-if
或者 v-else-if
的元素的后面,否则它将不会被识别。
v-else-if
,顾名思义,充当 v-if
的“else-if 块”,可以连续使用:
<div id="app">
<button v-on:click="random=Math.random()">点我呀button><span>{{random}}span>
<h1 v-if="random >= 0.75">
看到我啦?!if
h1>
<h1 v-else-if="random > 0.5">
看到我啦?!if 0.5
h1>
<h1 v-else-if="random > 0.25">
看到我啦?!if 0.25
h1>
<h1 v-else>
看到我啦?!else
h1>
div>
<script src="./node_modules/vue/dist/vue.js">script>
<script type="text/javascript">
var app = new Vue({
el: "#app",
data: {
random: 1
}
})
script>
类似于 v-else
,v-else-if
也必须紧跟在带 v-if
或者 v-else-if
的元素之后。
另一个用于根据条件展示元素的选项是 v-show
指令。用法大致一样:
Hello!
不同的是带有 v-show
的元素始终会被渲染并保留在 DOM 中。v-show
只是简单地切换元素的 CSS 属性 display
。
示例:
<div id="app">
<button v-on:click="show = !show">点击切换button><br/>
<h1 v-if="show">
你好
h1>
div>
<script src="./node_modules/vue/dist/vue.js">script>
<script type="text/javascript">
var app = new Vue({
el:"#app",
data:{
show:true
}
})
script>
html属性不能使用双大括号形式绑定,只能使用v-bind指令。
表明属性内的值是动态值
在将 v-bind
用于 class
和 style
时,Vue.js 做了专门的增强。表达式结果的类型除了字符串之外,还可以是对象或数组。
<div id="app">
<div v-bind:title="title" style="border: 1px solid red; width: 50px; height: 50px;">div>
div>
<script src="./node_modules/vue/dist/vue.js">script>
<script type="text/javascript">
var app = new Vue({
el: "#app",
data: {
title: "title",
}
})
script>
HTML:
<div id="app">
<div v-bind:class="activeClass">div>
<div v-bind:class="errorClass">div>
<div v-bind:class="[activeClass, errorClass]">div>
div>
<script src="./node_modules/vue/dist/vue.js">script>
<script type="text/javascript">
var app = new Vue({
el: "#app",
data: {
activeClass: 'active',
errorClass: ['text-danger', 'text-error']
}
})
script>
渲染后的效果:(具有active和hasError的样式)
对象语法
我们可以传给 v-bind:class
一个对象,以动态地切换 class:
<div v-bind:class="{ active: isActive }">div>
上面的语法表示 active
这个 class 存在与否将取决于数据属性 isActive
的 truthiness(所有的值都是真实的,除了false,0,“”,null,undefined和NaN)。
你可以在对象中传入更多属性来动态切换多个 class。此外,v-bind:class
指令也可以与普通的 class 属性共存。如下模板:
<div class="static"
v-bind:class="{ active: isActive, 'text-danger': hasError }">
div>
和如下 data:
data: {
isActive: true,
hasError: false
}
结果渲染为:
<div class="static active">div>
active样式和text-danger样式的存在与否,取决于isActive和hasError的值。本例中isActive为true,hasError为false,所以active样式存在,text-danger不存在。
数组语法
数组语法可以将多个样式对象应用到同一个元素上:
<div v-bind:style="[baseStyles, overridingStyles]">div>
数据:
data: {
baseStyles: {'background-color': 'red'},
overridingStyles: {border: '1px solid black'}
}
渲染后的结果:
<div style="background-color: red; border: 1px solid black;">div>
对象语法
v-bind:style
的对象语法十分直观——看着非常像 CSS,但其实是一个 JavaScript 对象。CSS 属性名可以用驼峰式 (camelCase) 或短横线分隔 (kebab-case,记得用单引号括起来) 来命名:
<div v-bind:style="{ color: activeColor, fontSize: fontSize + 'px' }">div>
数据:
data: {
activeColor: 'red',
fontSize: 30
}
效果:
<div style="color: red; font-size: 30px;">div>
v-bind:class
可以简写为:class
在插值表达式中使用js表达式是非常方便的,而且也经常被用到。
但是如果表达式的内容很长,就会显得不够优雅,而且后期维护起来也不方便,例如下面的场景,我们有一个日期的数据,但是是毫秒值:
data:{
birthday:1529032123201 // 毫秒值
}
我们在页面渲染,希望得到yyyy-MM-dd的样式:
<h1>您的生日是:{{
new Date(birthday).getFullYear() + '-'+ new Date(birthday).getMonth()+ '-' + new Date(birthday).getDay()
}}
h1>
在使用方法时需要加括号
虽然能得到结果,但是非常麻烦。
Vue中提供了计算属性,来替代复杂的表达式:
var vm = new Vue({
el:"#app",
data:{
birthday:1429032123201 // 毫秒值
},
computed:{
birth(){// 计算属性本质是一个方法,但是必须返回结果
const d = new Date(this.birthday);
return d.getFullYear() + "-" + d.getMonth() + "-" + d.getDay();
}
}
})
页面使用:
<div id="app">
<h1>您的生日是:{{birth}} h1>
div>
我们可以将同一函数定义为一个方法而不是一个计算属性。两种方式的最终结果确实是完全相同的。然而,不同的是计算属性是基于它们的依赖进行缓存的。计算属性只有在它的相关依赖发生改变时才会重新求值。这就意味着只要birthday
还没有发生改变,多次访问 birthday
计算属性会立即返回之前的计算结果,而不必再次执行函数。
watch可以让我们监控一个值的变化。从而做出相应的反应。
watch中的方法名必须和监听的数据名一致
示例:
<div id="app">
<input type="text" v-model="message">
div>
<script src="./node_modules/vue/dist/vue.js">script>
<script type="text/javascript">
var vm = new Vue({
el:"#app",
data:{
message:""
},
watch:{
message(newVal, oldVal){
console.log(newVal, oldVal);
}
}
})
script>
在大型应用开发的时候,页面可以划分成很多部分。往往不同的页面,也会有相同的部分。例如可能会有相同的头部导航。
但是如果每个页面都独自开发,这无疑增加了我们开发的成本。所以我们会把页面的不同部分拆分成独立的组件,然后在不同页面就可以共享这些组件,避免重复开发。
在vue里,所有的vue实例都是组件
我们通过Vue的component方法来定义一个全局组件。
<div id="app">
<counter>counter>
div>
<script src="./node_modules/vue/dist/vue.js">script>
<script type="text/javascript">
// 定义全局组件,两个参数:1,组件名称。2,组件参数
Vue.component("counter",{
template:'',
data(){
return {
count:0
}
}
})
var app = new Vue({
el:"#app"
})
script>
定义好的组件,可以任意复用多次:
<div id="app">
<counter>counter>
<counter>counter>
<counter>counter>
div>
你会发现每个组件互不干扰,都有自己的count值。怎么实现的?
组件的data属性必须是函数!
当我们定义这个
组件时,它的data 并不是像之前直接提供一个对象:
data: {
count: 0
}
取而代之的是,一个组件的 data 选项必须是一个函数,因此每个实例可以维护一份被返回对象的独立的拷贝:
data: function () {
return {
count: 0
}
}
如果 Vue 没有这条规则,点击一个按钮就会影响到其它所有实例!
一旦全局注册,就意味着即便以后你不再使用这个组件,它依然会随着Vue的加载而加载。
因此,对于一些并不频繁使用的组件,我们会采用局部注册。
我们先在外部定义一个对象,结构与创建组件时传递的第二个参数一致:
const counter = {
template:'',
data(){
return {
count:0
}
}
};
然后在Vue中使用它:
var app = new Vue({
el:"#app",
components:{
counter:counter // 将定义的对象注册为组件
}
})
父组件使用子组件,并自定义了title属性:
<div id="app">
<h1>打个招呼:h1>
<introduce title="大家好,我是锋哥"/>
div>
<script src="./node_modules/vue/dist/vue.js">script>
<script type="text/javascript">
Vue.component("introduce",{
// 直接使用props接收到的属性来渲染页面
template:'{{title}}
',
props:['title'] // 通过props来接收一个父组件传递的属性
})
var app = new Vue({
el:"#app"
})
script>
我们定义一个子组件,并接收复杂数据:
const myList = {
template: '\
\
- {{item.id}} : {{item.name}}
\
\
',
props: {
items: {
type: Array,
default: [],
required: true
}
}
};
当 prop 验证失败的时候,(开发环境构建版本的) Vue 将会产生一个控制台的警告。
我们在父组件中使用它:
<div id="app">
<h2>传智播客已开设如下课程:h2>
<my-list :items="lessons"/>
div>
var app = new Vue({
el:"#app",
components:{
myList // 当key和value一样时,可以只写一个
},
data:{
lessons:[
{id:1, name: 'java'},
{id:2, name: 'php'},
{id:3, name: 'ios'},
]
}
})
给 prop 传入一个静态的值:
<introduce title="大家好,我是锋哥"/>
给 prop 传入一个动态的值: (通过v-bind从数据模型中,获取title的值)
<introduce :title="title"/>
静态传递时,我们传入的值都是字符串类型的,但实际上任何类型的值都可以传给一个 props。
<blog-post v-bind:likes="42">blog-post>
<blog-post v-bind:likes="post.likes">blog-post>
来看这样的一个案例:
<div id="app">
<h2>num: {{num}}h2>
<counter :num="num">counter>
div>
<script src="./node_modules/vue/dist/vue.js">script>
<script type="text/javascript">
Vue.component("counter", {// 子组件,定义了两个按钮,点击数字num会加或减
template:'\
\
\
\
',
props:['num']// count是从父组件获取的。
})
var app = new Vue({
el:"#app",
data:{
num:0
}
})
script>
我们尝试运行,好像没问题,点击按钮试试:
子组件接收到父组件属性后,默认是不允许修改的。怎么办?
既然只有父组件能修改,那么加和减的操作一定是放在父组件:
var app = new Vue({
el:"#app",
data:{
num:0
},
methods:{ // 父组件中定义操作num的方法
increment(){
this.num++;
},
decrement(){
this.num--;
}
}
})
但是,点击按钮是在子组件中,那就是说需要子组件来调用父组件的函数,怎么做?
我们可以通过v-on指令将父组件的函数绑定到子组件上:
<div id="app">
<h2>num: {{num}}h2>
<counter :count="num" @inc="increment" @dec="decrement">counter>
div>
在子组件中定义函数,函数的具体实现调用父组件的实现,并在子组件中调用这些函数。当子组件中按钮被点击时,调用绑定的函数:
Vue.component("counter", {
template:'\
\
\
\
',
props:['count'],
methods:{
plus(){
this.$emit("inc");
},
reduce(){
this.$emit("dec");
}
}
})
效果:
一个页面,包含登录和注册,点击不同按钮,实现登录和注册页切换:
为了让接下来的功能比较清晰,我们先新建一个文件夹:src
然后新建一个HTML文件,作为入口:index.html
然后编写页面的基本结构:
<div id="app">
<span>登录span>
<span>注册span>
<hr/>
<div>
登录页/注册页
div>
div>
<script src="../node_modules/vue/dist/vue.js">script>
<script type="text/javascript">
var vm = new Vue({
el:"#app"
})
script>
接下来我们来实现登录组件,以前我们都是写在一个文件中,但是为了复用性,开发中都会把组件放入独立的JS文件中,我们新建一个user目录以及login.js及register.js:
编写组件,这里我们只写模板,不写功能。
login.js内容如下:
const loginForm = {
template:'\
\
登录页
\
用户名:
\
密码:
\
\
'
}
register.js内容:
const registerForm = {
template:'\
\
注册页
\
用 户 名:
\
密 码:
\
确认密码:
\
\
'
}
<div id="app">
<span>登录span>
<span>注册span>
<hr/>
<div>
<login-form>login-form>
<register-form>register-form>
div>
div>
<script src="../node_modules/vue/dist/vue.js">script>
<script src="user/login.js">script>
<script src="user/register.js">script>
<script type="text/javascript">
var vm = new Vue({
el: "#app",
components: {
loginForm,
registerForm
}
})
script>
问题:
我们期待的是,当点击登录或注册按钮,分别显示登录页或注册页,而不是一起显示。
但是,如何才能动态加载组件,实现组件切换呢?
虽然使用原生的Html5和JS也能实现,但是官方推荐我们使用vue-router模块。
使用vue-router和vue可以非常方便的实现 复杂单页应用的动态路由功能。
官网:https://router.vuejs.org/zh-cn/
使用npm安装:npm install vue-router --save
在index.html中引入依赖:
<script src="../node_modules/vue-router/dist/vue-router.js">script>
新建vue-router对象,并且指定路由规则:
// 创建VueRouter对象
const router = new VueRouter({
routes:[ // 编写路由规则
{
path:"/login", // 请求路径,以“/”开头
component:loginForm // 组件名称
},
{
path:"/register",
component:registerForm
}
]
})
在父组件中引入router对象:
var vm = new Vue({
el:"#app",
components:{// 引用登录和注册组件
loginForm,
registerForm
},
router // 引用上面定义的router对象
})
页面跳转控制:
<div id="app">
<span><router-link to="/login">登录router-link>span>
<span><router-link to="/register">注册router-link>span>
<hr/>
<div>
<router-view>router-view>
div>
div>
来指定一个锚点,当路由的路径匹配时,vue-router会自动把对应组件放到锚点位置进行渲染
指定一个跳转链接,当点击时,会触发vue-router的路由功能,路径中的hash值会随之改变注意:单页应用中,页面的切换并不是页面的跳转。仅仅是地址最后的hash值变化。
事实上,我们总共就一个HTML:index.html
Vue.component("组件名",{
templete:"html模板",
data(){
return{
数据类型
}
},
methods,watch
})
vue-router npm install vue-router --save
引入vue-router组件
实例化:vue-router:
const router=new VueRouter({
routes:[
{
path:路由路径,要以"/"开头
component:路由组件
},
{}
]
})
引入到vue实例中:通过router
:锚点
解压后的leyou-manage-web (是下一个目录是多个文件的那个,别复制错了)复制到hm49/code打开
其中的pcakage.json中定义了安装的内容
执行npm install 不成功可以多执行几次
实在安装不上复制node_modules 到leyou-manage-web即可
安装完成之后
执行npm start命令 //在pcakage.json 中定义的热部署
项目结构
index.html:html模板文件。定义了空的div,其id为app。
main.js:实例化vue对象,并且绑定通过id选择器,绑定到index.html的div中,因此main.js的内容都将在index.html的div中显示。main.js中使用了App组件,即App.vue,也就是说index.html中最终展现的是App.vue中的内容。index.html引用它之后,就拥有了vue的内容(包括组件、样式等),所以,main.js也是webpack打包的入口。
index.js:定义请求路径和组件的映射关系。相当于之前的
App.vue中也没有内容,而是定义了vue-router的锚点:,我们之前讲过,vue-router路由后的组件将会在锚点展示。
最终结论:一切路由后的内容都将通过App.vue在index.html中显示。
访问流程:用户在浏览器输入路径,例如:http://localhost:9001/#/item/brand --> index.js(/item/brand路径对应pages/item/Brand.vue组件) --> 该组件显示在App.vue的锚点位置 --> main.js使用了App.vue组件,并把该组件渲染在index.html文件中(id为“app”的div中)
Vue虽然会帮我们进行视图的渲染,但样式还是由我们自己来完成。这显然不是我们的强项,因此后端开发人员一般都喜欢使用一些现成的UI组件,拿来即用,常见的例如:
然而这些UI组件的基因天生与Vue不合,因为他们更多的是利用DOM操作,借助于jQuery实现,而不是MVVM的思想。
而目前与Vue吻合的UI框架也非常的多,国内比较知名的如:
然而我们都不用,我们今天推荐的是一款国外的框架:Vuetify
官方网站:https://vuetifyjs.com/zh-Hans/
有中国的为什么还要用外国的?原因如下:
有没有什么问题?
实际开发中,会有不同的环境:
如果不同环境使用不同的ip去访问,可能会出现一些问题。为了保证所有环境的一致,我们会在各种环境下都使用域名来访问。
我们将使用以下域名:
主域名是:www.leyou.com,
管理系统域名:manage.leyou.com
网关域名:api.leyou.com
…
但是最终,我们希望这些域名指向的还是我们本机的某个端口。
那么,当我们在浏览器输入一个域名时,浏览器是如何找到对应服务的ip和端口的呢?
一个域名一定会被解析为一个或多个ip。这一般会包含两步:
浏览器会首先在本机的hosts文件中查找域名映射的IP地址,如果查找到就返回IP ,没找到则进行域名服务器解析,一般本地解析都会失败,因为默认这个文件是空的。
# My hosts
127.0.0.1 localhost
0.0.0.0 account.jetbrains.com
127.0.0.1 www.xmind.net
本地解析失败,才会进行域名服务器解析,域名服务器就是网络中的一台计算机,里面记录了所有注册备案的域名和ip映射关系,一般只要域名是正确的,并且备案通过,一定能找到。
我们不可能去购买一个域名,因此我们可以伪造本地的hosts文件,实现对域名的解析。修改本地的host为:
127.0.0.1 api.leyou.com
127.0.0.1 manage.leyou.com
这样就实现了域名的关系映射了。
可以用软件修改hosts
资料中的SwitchHosts-win32-x64
解压,复制到hm49/tool 运行文件
新建方案
在cmd中ping该网址可以访问,但从浏览器访问时会出现问题
原因:我们配置了项目访问的路径,虽然manage.leyou.com映射的ip也是127.0.0.1,但是webpack会验证host是否符合配置。
必须在在webpack.dev.conf.js中取消host验证:
因为修改了配置文件,所以需要重新启动
Terminal 中crtl+c 选择Y
重新执行cpm start指令
域名问题解决了,但是现在要访问后台页面,还得自己加上端口:http://manage.taotao.com:9001。
这就不够优雅了。我们希望的是直接域名访问:http://manage.taotao.com。这种情况下端口默认是80,如何才能把请求转移到9001端口呢?
这里就要用到反向代理工具:Nginx
nginx可以作为web服务器,但更多的时候,我们把它作为网关,因为它具备网关必备的功能:
反向代理
负载均衡
动态路由
请求过滤
Web服务器分2类:
web应用服务器,如:
web服务器,如:
区分:web服务器不能解析jsp等页面,只能处理js、css、html等静态资源。 并发:web服务器的并发能力远高于web应用服务器。
什么是反向代理?
nginx可以当做反向代理服务器来使用:
利用反向代理,就可以解决我们前面所说的端口问题
将资料内的nginx压缩包解压后复制到tool文件夹即可
其中
conf:配置目录
contrib:第三方依赖
html:默认的静态资源目录,类似于tomcat的webapps
logs:日志目录
nginx.exe:启动程序。可双击运行,但不建议这么做。
反向代理配置 在conf中的nginx.conf
nginx中的每个server就是一个反向代理配置,可以有多个server
这里因为网关也需要访问,所以复制了一次server
完整配置:
#user nobody;
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
gzip on;
server {
listen 80;
server_name manage.leyou.com;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Server $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
location / {
proxy_pass http://127.0.0.1:9001;
proxy_connect_timeout 600;
proxy_read_timeout 600;
}
}
server {
listen 80;
server_name api.leyou.com;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Server $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
location / {
proxy_pass http://127.0.0.1:10010;
proxy_connect_timeout 600;
proxy_read_timeout 600;
}
}
}
使用
nginx可以通过命令行来启动,操作命令:
在路径栏里输入cmd 执行
启动过程会闪烁一下,启动成功后,任务管理器中会有两个nginx进程:
启动不了的话建议查看这篇文章
https://blog.csdn.net/shanlijia/article/details/109284192
https://blog.csdn.net/weixin_39640298/article/details/107042209
商品分类
CREATE TABLE `tb_category` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '类目id',
`name` varchar(20) NOT NULL COMMENT '类目名称',
`parent_id` bigint(20) NOT NULL COMMENT '父类目id,顶级类目填0',
`is_parent` tinyint(1) NOT NULL COMMENT '是否为父节点,0为否,1为是',
`sort` int(4) NOT NULL COMMENT '排序指数,越小越靠前',
PRIMARY KEY (`id`),
KEY `key_parent_id` (`parent_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1424 DEFAULT CHARSET=utf8 COMMENT='商品类目表,类目和商品(spu)是一对多关系,类目与品牌是多对多关系';
因为商品分类会有层级关系,因此这里我们加入了parent_id
字段,对本表中的其它分类进行自关联。
页面模板:
v-card:卡片,是vuetify中提供的组件,提供一个悬浮效果的面板,一般用来展示一组
v-flex:布局容器,用来控制响应式布局。与BootStrap的栅格系统类似,整个屏幕被分为12格。我们可以控制所占的格数来控制宽度:
v-tree:树组件。Vuetify并没有提供树组件,这个是我们自己编写的自定义组件:
树组件内容简述
属性列表:
属性名称 说明 数据类型 默认值
url 用来加载数据的地址,即延迟加载 String -
isEdit 是否开启树的编辑功能 boolean false
treeData 整颗树数据,这样就不用远程加载了 Array -
这里推荐使用url进行延迟加载,每当点击父节点时,就会发起请求,根据父节点id查询子节点信息。
当有treeData属性时,就不会触发url加载
给大家的页面中,treeData是假数据,我们删除数据treeData属性,只保留url看看会发生什么:
刷新页面,可以看到:
页面中的树没有了,并且发起了一条请求:http://localhost/api/item/category/list?pid=0
大家可能会觉得很奇怪,我们明明是使用的相对路径,讲道理发起的请求地址应该是:
http://manage.leyou.com/item/category/list
但实际却是:
http://localhost/api/item/category/list?pid=0
这是因为,我们有一个全局的配置文件,对所有的请求路径进行了约定:
路径是localhost,并且默认加上了/api的前缀,这恰好与我们的网关设置匹配,我们只需要把地址改成网关的地址即可,因为我们使用了nginx反向代理,这里可以写域名:
package com.leyou.item.pojo;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
@Table(name="tb_category")
public class Category {
@Id
@GeneratedValue(strategy= GenerationType.IDENTITY)
private Long id;
private String name;
private Long parentId;
private Boolean isParent;
private Integer sort;
// getter和setter略
// 注意isParent的get和set方法,需要手动加上Is
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Long getParentId() {
return parentId;
}
public void setParentId(Long parentId) {
this.parentId = parentId;
}
public Boolean getIsParent() {
return isParent;
}
public void setIsParent(Boolean parent) {
isParent = parent;
}
public Integer getSort() {
return sort;
}
public void setSort(Integer sort) {
this.sort = sort;
}
}
需要注意的是,这里要用到jpa的注解,因此我们在leyou-item-iterface
中添加jpa依赖
<dependency>
<groupId>javax.persistencegroupId>
<artifactId>persistence-apiartifactId>
<version>1.0version>
dependency>
后续都是写在leyou-item-service
中
编写一个controller一般需要知道四个内容:
在刚才页面发起的请求中,我们就能得到绝大多数信息:
请求方式:Get,插叙肯定是get请求
请求路径:/api/item/category/list。其中/api是网关前缀,/item是网关的路由映射,真实的路径应该是/category/list
请求参数:pid=0,根据tree组件的说明,应该是父节点的id,第一次查询为0,那就是查询一级类目
返回结果:??
根据前面tree组件的用法我们知道,返回的应该是json数组:
[
{
"id": 74,
"name": "手机",
"parentId": 0,
"isParent": true,
"sort": 2
},
{
"id": 75,
"name": "家用电器",
"parentId": 0,
"isParent": true,
"sort": 3
}
]
对应的java类型可以是List集合,里面的元素就是类目对象了。也就是List
添加Controller:
package com.leyou.item.controller;
import com.leyou.item.pojo.Category;
import com.leyou.item.service.CategoryService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.List;
@Controller
@RequestMapping("category")
public class CategoryController {
@Autowired
private CategoryService categoryService;
/**
* 根据父节点id查询子节点
*
* @param pid
* @return
*/
@GetMapping("list")
public ResponseEntity<List<Category>> queryCategoryiesByPid(@RequestParam(value = "pid", defaultValue = "0") Long pid) {
if (pid == null || pid < 0) {
// 400 param is wrong
//return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
return ResponseEntity.badRequest().build();
}
List<Category> categories = this.categoryService.queryCategoriesByPid(pid);
if (CollectionUtils.isEmpty(categories)) {
//404 is not found
//return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
return ResponseEntity.notFound().build();
}
//200 query successfully
return ResponseEntity.ok(categories);
}
}
package com.leyou.item.service;
import com.leyou.item.mapper.CategoryMapper;
import com.leyou.item.pojo.Category;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class CategoryService {
@Autowired
private CategoryMapper categoryMapper;
/**
* 根据父节点查询子节点
* @param pid
* @return
*/
public List<Category> queryCategoriesByPid(Long pid) {
Category record = new Category();
record.setParentId(pid);
return this.categoryMapper.select(record);
}
}
package com.leyou.item.mapper;
import com.leyou.item.pojo.Category;
import tk.mybatis.mapper.common.Mapper;
public interface CategoryMapper extends Mapper<Category> {
}
要注意,我们并没有在mapper接口上声明@Mapper注解,那么mybatis如何才能找到接口呢?
我们在启动类上添加一个扫描包功能:
@SpringBootApplication
@EnableDiscoveryClient
@MapperScan("com.leyou.item.mapper") // 扫描mapper包
public class LyItemService {
public static void main(String[] args) {
SpringApplication.run(LyItemService.class, args);
}
}
结构
运行后出现跨域问题
无法正常运行
跨域:浏览器对于javascript的同源策略的限制 。
以下情况都属于跨域:
跨域原因说明 | 示例 |
---|---|
域名不同 | www.jd.com 与 www.taobao.com |
域名相同,端口不同 | www.jd.com:8080 与 www.jd.com:8081 |
二级域名不同 | item.jd.com 与 miaosha.jd.com |
如果域名和端口都相同,但是请求路径不同,不属于跨域,如:
www.jd.com/item
www.jd.com/goods
http和https也属于跨域
而我们刚才是从manage.leyou.com
去访问api.leyou.com
,这属于二级域名不同,跨域了。
跨域不一定都会有跨域问题。
因为跨域问题是浏览器对于ajax请求的一种安全限制:一个页面发起的ajax请求,只能是与当前页域名相同的路径,这能有效的阻止跨站攻击。
因此:跨域问题 是针对ajax的一种限制。
但是这却给我们的开发带来了不便,而且在实际生产环境中,肯定会有很多台服务器之间交互,地址和端口都可能不同,怎么办?
目前比较常用的跨域解决方案有3种:
Jsonp
最早的解决方案,利用script标签可以跨域的原理实现。
限制:
nginx反向代理
思路是:利用nginx把跨域反向代理为不跨域,支持各种请求方式
缺点:需要在nginx进行额外配置,语义不清晰
CORS
规范化的跨域请求解决方案,安全可靠。
优势:
缺点:
我们这里会采用cors的跨域方案。
CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing)。
它允许浏览器向跨源服务器,发出[XMLHttpRequest
]请求,从而克服了AJAX只能同源使用的限制。
CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。
浏览器端:
目前,所有浏览器都支持该功能(IE10以下不行)。整个CORS通信过程,都是浏览器自动完成,不需要用户参与。
服务端:
CORS通信与AJAX没有任何差别,因此你不需要改变以前的业务逻辑。只不过,浏览器会在请求中携带一些头信息,我们需要以此判断是否允许其跨域,然后在响应头中加入一些信息即可。这一般通过过滤器完成即可。
浏览器会将ajax请求分为两类,其处理方案略有差异:简单请求、特殊请求。
只要同时满足以下两大条件,就属于简单请求。:
(1) 请求方法是以下三种方法之一:
(2)HTTP的头信息不超出以下几种字段:
application/x-www-form-urlencoded
、multipart/form-data
、text/plain
当浏览器发现发起的ajax请求是简单请求时,会在请求头中携带一个字段:Origin
.
Origin中会指出当前请求属于哪个域(协议+域名+端口)。服务会根据这个值决定是否允许其跨域。
如果服务器允许跨域,需要在返回的响应头中携带下面信息:
Access-Control-Allow-Origin: http://manage.leyou.com
Access-Control-Allow-Credentials: true
Content-Type: text/html; charset=utf-8
有关cookie:
要想操作cookie,需要满足3个条件:
不符合简单请求的条件,会被浏览器判定为特殊请求,,例如请求方式为PUT。
预检请求
特殊请求会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。
浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest
请求,否则就报错。
一个“预检”请求的样板:
OPTIONS /cors HTTP/1.1
Origin: http://manage.leyou.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.leyou.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
与简单请求相比,除了Origin以外,多了两个头:
预检请求的响应
服务的收到预检请求,如果许可跨域,会发出响应:
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://manage.leyou.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Max-Age: 1728000
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain
除了Access-Control-Allow-Origin
和Access-Control-Allow-Credentials
以外,这里又额外多出3个头:
如果浏览器得到上述响应,则认定为可以跨域,后续就跟简单请求的处理是一样的了。
虽然原理比较复杂,但是前面说过:
事实上,SpringMVC已经帮我们写好了CORS的跨域过滤器:CorsFilter ,内部已经实现了刚才所讲的判定逻辑,我们直接用就好了。
在leyou-gateway
中编写一个配置类,并且注册CorsFilter:
package com.leyou.gateway.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
@Configuration
public class LeyouCorsConfiguration {
@Bean
public CorsFilter corsFilter(){
//初始化cors配置对象
CorsConfiguration configuration = new CorsConfiguration();
//允许跨域的域名,如果要携带cookid,不能写*。*:代表所有域名都可以跨域访问
configuration.addAllowedOrigin("http://manage.leyou.com");
configuration.setAllowCredentials(true);//允许携带cookie
configuration.addAllowedMethod("*");//代表所有的请求方法:GET、PUT、POST、DELETE。。
configuration.addAllowedHeader("*");//允许携带任何头信息
//初始化cors配置源对象
UrlBasedCorsConfigurationSource corsConfigurationSource=new UrlBasedCorsConfigurationSource();
corsConfigurationSource.registerCorsConfiguration("/**",configuration);
//反悔CorsFilter实例,参数:cors配置源对象
return new CorsFilter(corsConfigurationSource);
}
}
路由路径:/item/brand
根据路由文件知,对应的页面是:src/pages/item/Brand.vue
页面会发送如下请求:
前台页面已经准备好,接下来就是后台提供数据接口了。
CREATE TABLE `tb_brand` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '品牌id',
`name` varchar(50) NOT NULL COMMENT '品牌名称',
`image` varchar(200) DEFAULT '' COMMENT '品牌图片地址',
`letter` char(1) DEFAULT '' COMMENT '品牌的首字母',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=325400 DEFAULT CHARSET=utf8 COMMENT='品牌表,一个品牌下有多个商品(spu),一对多关系';
简单的四个字段,不多解释。
这里需要注意的是,品牌和商品分类之间是多对多关系。因此我们有一张中间表,来维护两者间关系:
CREATE TABLE `tb_category_brand` (
`category_id` bigint(20) NOT NULL COMMENT '商品类目id',
`brand_id` bigint(20) NOT NULL COMMENT '品牌id',
PRIMARY KEY (`category_id`,`brand_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='商品分类和品牌的中间表,两者是多对多关系';
但是,你可能会发现,这张表中并没有设置外键约束,似乎与数据库的设计范式不符。为什么这么做?
在电商行业,性能是非常重要的。我们宁可在代码中通过逻辑来维护表关系,也不设置外键。
interface中的
package com.leyou.item.pojo;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
@Table(name = "tb_brand")
public class Brand {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String image;
private Character letter;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getImage() {
return image;
}
public void setImage(String image) {
this.image = image;
}
public Character getLetter() {
return letter;
}
public void setLetter(Character letter) {
this.letter = letter;
}
}
package com.leyou.item.mapper;
import com.leyou.item.pojo.Brand;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Param;
import tk.mybatis.mapper.common.Mapper;
public interface BrandMapper extends Mapper {
@Insert("INSERT INTO tb_category_brand (category_id,brand_id) VALUES (#{cid},#{bid})")
void insertCategoryAndBrand(@Param("cid") Long cid,@Param("bid") Long bid);
}
编写controller先思考四个问题,参照前端页面的控制台
这里我们封装一个类,来表示分页结果:
需要先在item-common 写封装类
package com.leyou.common.pojo;
import java.util.List;
public class PageResult {
private Long total;
private Integer totalPage;
private List items;
public PageResult() {
}
public PageResult(Long total, List items) {
this.total = total;
this.items = items;
}
public PageResult(Long total, Integer totalPage, List items) {
this.total = total;
this.totalPage = totalPage;
this.items = items;
}
public Long getTotal() {
return total;
}
public void setTotal(Long total) {
this.total = total;
}
public Integer getTotalPage() {
return totalPage;
}
public void setTotalPage(Integer totalPage) {
this.totalPage = totalPage;
}
public List getItems() {
return items;
}
public void setItems(List items) {
this.items = items;
}
}
这个PageResult以后可能在其它项目中也有需求,因此我们将其抽取到leyou-common
中,提高复用性:
不要忘记在leyou-item-service工程的pom.xml中引入leyou-common的依赖:
<dependency>
<groupId>com.leyou.commongroupId>
<artifactId>leyou-commonartifactId>
<version>1.0.0-SNAPSHOTversion>
dependency>
接下来,我们编写Controller
package com.leyou.item.controller;
import com.leyou.common.pojo.PageResult;
import com.leyou.item.pojo.Brand;
import com.leyou.item.service.BrandService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.List;
@Controller
@RequestMapping("brand")
public class BrandController {
@Autowired
private BrandService brandService;
/**
* 根据查询条件分页并排序查询
* @param key
* @param page
* @param rows
* @param sortBy
* @param desc
* @return
*/
@GetMapping("page")
public ResponseEntity> queryBrandsByPage(
@RequestParam(value = "key",required = false)String key,//能确定是默认值是什么内容就用defaultValue,不确定就用required
@RequestParam(value = "page",defaultValue = "1")Integer page,
@RequestParam(value = "rows",defaultValue = "5")Integer rows,
@RequestParam(value = "sortBy",required = false)String sortBy,
@RequestParam(value = "desc",required = false)Boolean desc
){
PageResult result =this.brandService.queryBrandsByPage(key,page,rows,sortBy,desc);
if(CollectionUtils.isEmpty(result.getItems())){
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(result);
}
@PostMapping
public ResponseEntity saveBrand(Brand brand, @RequestParam("cids")List cids){
this.brandService.saveBrand(brand,cids);
return ResponseEntity.status(HttpStatus.CREATED).build();
}
}
package com.leyou.item.service;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.leyou.common.pojo.PageResult;
import com.leyou.item.mapper.BrandMapper;
import com.leyou.item.pojo.Brand;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import tk.mybatis.mapper.entity.Example;
import java.util.List;
@Service
public class BrandService {
@Autowired
private BrandMapper brandMapper;
/**
*
* @param key
* @param page
* @param rows
* @param sortBy
* @param desc
* @return
*/
public PageResult queryBrandsByPage(String key, Integer page, Integer rows, String sortBy, Boolean desc) {
//初始化example对象去new一个example
Example example=new Example(Brand.class);
Example.Criteria criteria=example.createCriteria();
//根据name模糊查询,或者根据首字母查询
if(StringUtils.isNotBlank(key)){
criteria.andLike("name","%"+key+"%").orEqualTo("letter",key);
}
//添加分页条件
PageHelper.startPage(page,rows);
//添加排序条件
if(StringUtils.isNotBlank(sortBy)){
example.setOrderByClause(sortBy+" "+(desc ? "desc" :"asc") );
}
List brands = this.brandMapper.selectByExample(example);
//包装成pageinfo
PageInfo pageInfo = new PageInfo<>(brands);
//包装成分页结果返回
return new PageResult<>(pageInfo.getTotal(), pageInfo.getList());
}
/**
* 新增品牌
* @param brand
* @param cids
*/
@Transactional
public void saveBrand(Brand brand, List cids) {
//先新增brand
//Boolean flag=this.brandMapper.insertSelective(brand)==1;
this.brandMapper.insertSelective(brand);
//新增中间表 ,因为加了注解出问题会自动回滚,所以不需要再判断
//if(flag){
cids.forEach(cid ->{
this.brandMapper.insertCategoryAndBrand(cid,brand.getId());
});
//}
}
}
异步查询数据,自然是通过ajax查询,大家首先想起的肯定是jQuery。但jQuery与MVVM的思想不吻合,而且ajax只是jQuery的一小部分。因此不可能为了发起ajax请求而去引用这么大的一个库。
Vue官方推荐的ajax请求框架叫做:axios,看下demo:
axios的Get请求语法:
axios.get("/item/category/list?pid=0") // 请求路径和请求参数拼接
.then(function(resp){
// 成功回调函数
})
.catch(function(){
// 失败回调函数
})
// 参数较多时,可以通过params来传递参数
axios.get("/item/category/list", {
params:{
pid:0
}
})
.then(function(resp){})// 成功时的回调
.catch(function(error){})// 失败时的回调
axios的POST请求语法:
比如新增一个用户
axios.post("/user",{
name:"Jack",
age:21
})
.then(function(resp){})
.catch(function(error){})
注意,POST请求传参,不需要像GET请求那样定义一个对象,在对象的params参数中传参。post()方法的第二个参数对象,就是将来要传递的参数
PUT和DELETE请求与POST请求类似
而在我们的项目中,已经引入了axios,并且进行了简单的封装,在src下的http.js中:
http.js中对axios进行了一些默认配置:
import Vue from 'vue'
import axios from 'axios'
import config from './config'
// config中定义的基础路径是:http://api.leyou.com/api
axios.defaults.baseURL = config.api; // 设置axios的基础请求路径
axios.defaults.timeout = 2000; // 设置axios的请求时间
Vue.prototype.$http = axios;// 将axios赋值给Vue原型的$http属性,这样所有vue实例都可使用该对象
http.js对axios进行了全局配置:baseURL=config.api
,即http://api.leyou.com/api
。因此以后所有用axios发起的请求,都会以这个地址作为前缀。
通过Vue.property.$http = axios
,将axios
赋值给了 Vue原型中的$http
。这样以后所有的Vue实例都可以访问到$http,也就是访问到了axios了。
我们在组件MyBrand.vue的getDataFromServer方法,通过$http发起get请求,测试查询品牌的接口,看是否能获取到数据:
可以看到,在请求成功的返回结果response中,有一个data属性,里面就是真正的响应数据。
响应结果中与我们设计的一致,包含3个内容:
total:总条数,目前是165
items:当前页数据
totalPage:总页数,我们没有返回
虽然已经通过ajax请求获取了品牌数据,但是刚才的请求没有携带任何参数,这样显然不对。我们后端接口需要5个参数:
page:当前页,int
rows:每页大小,int
sortBy:排序字段,String
desc:是否为降序,boolean
key:搜索关键词,String
而页面中分页信息应该是在pagination对象中,我们通过浏览器工具,查看pagination中有哪些属性:
分别是:
descending:是否是降序,对应请求参数的desc
page:当前页,对应参数的page
rowsPerpage:每页大小,对应参数中的rows
sortBy:排序字段,对应参数的sortBy
缺少一个搜索关键词,这个应该是通过v-model与输入框绑定的属性:search。这样,所有参数就都有了。
另外,不要忘了把查询的结果赋值给brands和totalBrands属性,Vuetify会帮我们渲染页面。
接下来,我们在getDataFromServer方法中完善请求参数:
// 发起请求
this.$http.get("/item/brand/page",{
params:{
key: this.search, // 搜索条件
page: this.pagination.page,// 当前页
rows: this.pagination.rowsPerPage,// 每页大小
sortBy: this.pagination.sortBy,// 排序字段
desc: this.pagination.descending// 是否降序
}
}).then(resp => { // 这里使用箭头函数
// 将得到的数据赋值给本地属性
this.brands = resp.data.items;
this.totalBrands = resp.data.total;
// 完成赋值后,把加载状态赋值为false
this.loading = false;
})
点击分页,会发起请求,通过浏览器工具查看,会发现pagination对象的属性一直在变化:
我们可以利用Vue的监视功能:watch,当pagination发生改变时,会调用我们的回调函数,我们在回调函数中进行数据的查询!
成功实现分页功能:
过滤字段对应的是search属性,我们只要监视这个属性即可:
页面实现略去,自行学习
package com.leyou.item.controller;
import com.leyou.common.pojo.PageResult;
import com.leyou.item.pojo.Brand;
import com.leyou.item.service.BrandService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.List;
@Controller
@RequestMapping("brand")
public class BrandController {
@Autowired
private BrandService brandService;
/**
* 根据查询条件分页并排序查询
* @param key
* @param page
* @param rows
* @param sortBy
* @param desc
* @return
*/
@GetMapping("page")
public ResponseEntity<PageResult<Brand>> queryBrandsByPage(
@RequestParam(value = "key",required = false)String key,//能确定是默认值是什么内容就用defaultValue,不确定就用required
@RequestParam(value = "page",defaultValue = "1")Integer page,
@RequestParam(value = "rows",defaultValue = "5")Integer rows,
@RequestParam(value = "sortBy",required = false)String sortBy,
@RequestParam(value = "desc",required = false)Boolean desc
){
PageResult<Brand> result =this.brandService.queryBrandsByPage(key,page,rows,sortBy,desc);
if(CollectionUtils.isEmpty(result.getItems())){
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(result);
}
@PostMapping
public ResponseEntity<Void> saveBrand(Brand brand, @RequestParam("cids")List<Long> cids){
this.brandService.saveBrand(brand,cids);
return ResponseEntity.status(HttpStatus.CREATED).build();
}
}
package com.leyou.item.service;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.leyou.common.pojo.PageResult;
import com.leyou.item.mapper.BrandMapper;
import com.leyou.item.pojo.Brand;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import tk.mybatis.mapper.entity.Example;
import java.util.List;
@Service
public class BrandService {
@Autowired
private BrandMapper brandMapper;
/**
*
* @param key
* @param page
* @param rows
* @param sortBy
* @param desc
* @return
*/
public PageResult<Brand> queryBrandsByPage(String key, Integer page, Integer rows, String sortBy, Boolean desc) {
//初始化example对象去new一个example
Example example=new Example(Brand.class);
Example.Criteria criteria=example.createCriteria();
//根据name模糊查询,或者根据首字母查询
if(StringUtils.isNotBlank(key)){
criteria.andLike("name","%"+key+"%").orEqualTo("letter",key);
}
//添加分页条件
PageHelper.startPage(page,rows);
//添加排序条件
if(StringUtils.isNotBlank(sortBy)){
example.setOrderByClause(sortBy+" "+(desc ? "desc" :"asc") );
}
List<Brand> brands = this.brandMapper.selectByExample(example);
//包装成pageinfo
PageInfo<Brand> pageInfo = new PageInfo<>(brands);
//包装成分页结果返回
return new PageResult<>(pageInfo.getTotal(), pageInfo.getList());
}
/**
* 新增品牌
* @param brand
* @param cids
*/
@Transactional
public void saveBrand(Brand brand, List<Long> cids) {
//先新增brand
//Boolean flag=this.brandMapper.insertSelective(brand)==1;
this.brandMapper.insertSelective(brand);
//新增中间表 ,因为加了注解出问题会自动回滚,所以不需要再判断
//if(flag){
cids.forEach(cid ->{
this.brandMapper.insertCategoryAndBrand(cid,brand.getId());
});
//}
}
}
通用Mapper只能处理单表,也就是Brand的数据,因此我们手动编写一个方法及sql,实现中间表的新增:
package com.leyou.item.mapper;
import com.leyou.item.pojo.Brand;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Param;
import tk.mybatis.mapper.common.Mapper;
public interface BrandMapper extends Mapper {
@Insert("INSERT INTO tb_category_brand (category_id,brand_id) VALUES (#{cid},#{bid})")
void insertCategoryAndBrand(@Param("cid") Long cid,@Param("bid") Long bid);
}
发现请求的数据格式是JSON格式。
原因分析:
axios处理请求体的原则会根据请求数据的格式来定:
如果请求体是对象:会转为json发送
如果请求体是String:会作为普通表单请求发送,但需要我们自己保证String的格式是键值对。
如:name=jack&age=12
QS是一个第三方库,我们可以用npm install qs --save来安装。不过我们在项目中已经集成了,大家无需安装:
这个工具的名字:QS,即Query String,请求参数字符串。
什么是请求参数字符串?例如: name=jack&age=21
QS工具可以便捷的实现 JS的Object与QueryString的转换。
在我们的项目中,将QS注入到了Vue的原型对象中,我们可以通过this.$qs
来获取这个工具:
我们将this.$qs
对象打印到控制台:
created(){
console.log(this.$qs);
}
发现其中有3个方法:
这里我们要使用的方法是stringify,它可以把Object转为QueryString。
测试一下,使用浏览器工具,把qs对象保存为一个临时变量temp1,然后调用stringify方法:
成功将person对象变成了 name=jack&age=21的字符串了
我们发现有一个问题:新增不管成功还是失败,窗口都一致在这里,不会关闭。
这样很不友好,我们希望如果新增失败,窗口保持;但是新增成功,窗口关闭才对。
因此,我们需要在新增的ajax请求完成以后,关闭窗口
但问题在于,控制窗口是否显示的标记在父组件:MyBrand.vue中。子组件如何才能操作父组件的属性?或者告诉父组件该关闭窗口了?
之前我们讲过一个父子组件的通信,有印象吗?
<v-card-text class="px-5" style="height:400px">
<brand-form @close="closeWindow" :oldBrand="oldBrand" :isEdit="isEdit"/>
v-card-text>
刚才的新增实现中,我们并没有上传图片,接下来我们一起完成图片上传逻辑。
文件的上传并不只是在品牌管理中有需求,以后的其它服务也可能需要,因此我们创建一个独立的微服务,专门处理各种上传。
新建module
GroupId com.leyou.service
ArtifactiId leyou-upload
Module name:leyou-upload
我们需要EurekaClient和web依赖:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>leyouartifactId>
<groupId>com.leyou.parentgroupId>
<version>1.0.0-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<groupId>com.leyou.uploadgroupId>
<artifactId>leyou-uploadartifactId>
<version>1.0.0-SNAPSHOTversion>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
dependency>
dependencies>
project>
application.yaml
server:
port: 8082
spring:
application:
name: upload-service
servlet:
multipart:
max-file-size: 5MB
eureka:
client:
service-url:
defaultZone: http:/127.0.0.1:10086/eureka
instance:
lease-renewal-interval-in-seconds: 5
lease-expiration-duration-in-seconds: 15
fdfs:
so-timeout: 1501 # 超时时间
connect-timeout: 601 # 连接超时时间
thumb-image: # 缩略图
width: 60
height: 60
tracker-list: # tracker地址:你的虚拟机服务器地址+端口(默认是22122)
- 192.168.56.101:22122
package com.leyou;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient
public class LeyouUploadApplication {
public static void main(String[] args) {
SpringApplication.run(LeyouUploadApplication.class);
}
}
编写controller需要知道4个内容:结合用法指南
代码如下:
package com.leyou.upload.controller;
import com.leyou.upload.service.UploadService;
import org.apache.commons.lang.StringUtils;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
@Controller
@RequestMapping("upload")
public class UploadController {
private UploadService uploadService;
@PostMapping("image")
public ResponseEntity uploadImage(@RequestParam("file")MultipartFile file){
String url=this.uploadService.uploadImage(file);
if(StringUtils.isNotBlank(url)){
return ResponseEntity.badRequest().build();
}
return ResponseEntity.status(HttpStatus.CREATED).body(url);
}
}
在上传文件过程中,我们需要对上传的内容进行校验:
文件大小在Spring的配置文件中设置,因此已经会被校验,我们不用管。
具体代码:
@Service
public class UploadService {
private static final List content_types= Arrays.asList("image/gif","image/jpeg");
private static final Logger LOGGER= LoggerFactory.getLogger(UploadService.class);
public String uploadImage(MultipartFile file) {
String originalFilename = file.getOriginalFilename();
//StringUtils.substringAfterLast(originalFilename,".") 不去使用此方法
//校验文件类型
String contentType = file.getContentType();
if(!content_types.contains(contentType)){
LOGGER.info("文件类型不合法:{}",originalFilename);
return null;
}
try {
//校验文件内容
BufferedImage bufferedImage = ImageIO.read(file.getInputStream());
if (bufferedImage==null){
LOGGER.info("文件内容不合法:{}",originalFilename);
return null;
}
// 保存到文件服务器
file.transferTo(new File("D:\\hm49\\image"+originalFilename));
// 返回url进行回显
return "http://image.leyou.com/"+originalFilename;
} catch (IOException e) {
LOGGER.info("服务器内部错误:"+originalFilename);
e.printStackTrace();
}
return null;
}
}
这里有一个问题:为什么图片地址需要使用另外的url?
图片上传是文件的传输,如果也经过Zuul网关的代理,文件就会经过多次网路传输,造成不必要的网络负担。在高并发时,可能导致网络阻塞,Zuul网关不可用。这样我们的整个系统就瘫痪了。
所以,我们上传文件的请求就不经过网关来处理了。
Zuul中提供了一个ignored-patterns属性,用来忽略不希望路由的URL路径,示例:
zuul.ignored-patterns: /upload/**
路径过滤会对一切微服务进行判定。
Zuul还提供了ignored-services
属性,进行服务过滤:
zuul.ignored-services: upload-servie
我们这里采用忽略服务:
zuul:
ignored-services:
- upload-service # 忽略upload-service服务
上面的配置采用了集合语法,代表可以配置多个
现在,我们修改页面的访问路径:
<v-upload
v-model="brand.image"
url="/upload/image"
:multiple="false"
:pic-width="250" :pic-height="90"
/>
可以看到这个地址不对,依然是去找Zuul网关,因为我们的系统全局配置了URL地址。怎么办?
有同学会想:修改页面请求地址不就好了。
注意:原则上,我们是不能把除了网关以外的服务对外暴露的,不安全。
既然不能修改页面请求,那么就只能在Nginx反向代理上做文章了。
我们修改nginx配置,将以/api/upload开头的请求拦截下来,转交到真实的服务地址:
location /api/upload {
proxy_pass http://127.0.0.1:8082;
proxy_connect_timeout 600;
proxy_read_timeout 600;
}
这样写大家觉得对不对呢?
显然是不对的,因为ip和端口虽然对了,但是路径没变,依然是:http://127.0.0.1:8002/api/upload/image
前面多了一个/api
Nginx提供了rewrite指令,用于对地址进行重写,语法规则:
rewrite "用来匹配路径的正则" 重写后的路径 [指令];
我们的案例:
server {
listen 80;
server_name api.leyou.com;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Server $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 上传路径的映射
location /api/upload {
proxy_pass http://127.0.0.1:8082;
proxy_connect_timeout 600;
proxy_read_timeout 600;
rewrite "^/api/(.*)$" /$1 break;
}
location / {
proxy_pass http://127.0.0.1:10010;
proxy_connect_timeout 600;
proxy_read_timeout 600;
}
}
首先,我们映射路径是/api/upload,而下面一个映射路径是 / ,根据最长路径匹配原则,/api/upload优先级更高。也就是说,凡是以/api/upload开头的路径,都会被第一个配置处理
proxy_pass
:反向代理,这次我们代理到8082端口,也就是upload-service服务
rewrite "^/api/(.*)$" /$1 break
,路径重写:
"^/api/(.*)$"
:匹配路径的正则表达式,用了分组语法,把/api/
以后的所有部分当做1组
/$1
:重写的目标路径,这里用$1引用前面正则表达式匹配到的分组(组编号从1开始),即/api/
后面的所有。这样新的路径就是除去/api/
以外的所有,就达到了去除/api
前缀的目的
break
:指令,常用的有2个,分别是:last、break
我们这里不能选择last,否则以新的路径/upload/image来匹配,就不会被正确的匹配到8082端口了
修改完成,输入nginx -s reload
命令重新加载配置。然后再次上传试试。
重启nginx,再次上传,发现报错了:
不过庆幸的是,这个错误已经不是第一次见了,跨域问题。
我们在upload-service中添加一个CorsFilter即可:
@Configuration
public class GlobalCorsConfig {
@Bean
public CorsFilter corsFilter() {
//1.添加CORS配置信息
CorsConfiguration config = new CorsConfiguration();
//1) 允许的域,不要写*,否则cookie就无法使用了
config.addAllowedOrigin(“http://manage.leyou.com”);
//2) 是否发送Cookie信息
config.setAllowCredentials(false);
//3) 允许的请求方式
config.addAllowedMethod(“OPTIONS”);
config.addAllowedMethod(“POST”);
config.addAllowedHeader("*");
//2.添加映射路径,我们拦截一切请求
UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource();
configSource.registerCorsConfiguration("/**", config);
//3.返回新的CorsFilter.
return new CorsFilter(configSource);
}
}
再次测试:
不过,非常遗憾的是,访问图片地址,却没有响应。
这是因为我们并没有任何服务器对应image.leyou.com这个域名。。
这个问题,我们暂时放下,回头再来解决。
先思考一下,之前上传的功能,有没有什么问题?
上传本身没有任何问题,问题出在保存文件的方式,我们是保存在服务器机器,就会有下面的问题:
单机器存储,存储能力有限
无法进行水平扩展,因为多台机器的文件无法共享,会出现访问不到的情况
数据没有备份,有单点故障风险
并发能力差
这个时候,最好使用分布式文件存储来代替本地文件存储。
分布式文件系统(Distributed File System)是指文件系统管理的物理存储资源不一定直接连接在本地节点上,而是通过计算机网络与节点相连。
通俗来讲:
FastDFS是由淘宝的余庆先生所开发的一个轻量级、高性能的开源分布式文件系统。用纯C语言开发,功能丰富:
适合有大容量存储需求的应用或系统。同类的分布式文件系统有谷歌的GFS、HDFS(Hadoop)、TFS(淘宝)等。
FastDFS两个主要的角色:Tracker Server 和 Storage Server 。
安装在虚拟机上的步骤
参考资料进行
记录几个需要记得的点
配置要配置内网ip
查看虚拟机的内网ip用命令ifconfig在终端执行
余庆先生提供了一个Java客户端,但是作为一个C程序员,写的java代码可想而知。而且已经很久不维护了。
这里推荐一个开源的FastDFS客户端,支持最新的SpringBoot2.0。
配置使用极为简单,支持连接池,支持自动生成缩略图,狂拽酷炫吊炸天啊,有木有。
地址:tobato/FastDFS_client
在父工程中,我们已经管理了依赖,版本为:
<fastDFS.client.version>1.26.2fastDFS.client.version>
因此,这里我们直接在taotao-upload工程的pom.xml中引入坐标即可:
<dependency>
<groupId>com.github.tobatogroupId>
<artifactId>fastdfs-clientartifactId>
dependency>
纯java配置:
package com.leyou.upload.config;
import com.github.tobato.fastdfs.FdfsClientConfig;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableMBeanExport;
import org.springframework.context.annotation.Import;
import org.springframework.jmx.support.RegistrationPolicy;
@Configuration
@Import(FdfsClientConfig.class)
// 解决jmx重复注册bean的问题
@EnableMBeanExport(registration = RegistrationPolicy.IGNORE_EXISTING)
public class FastClientImporter {
}
在application.yml配置文件中追加如下内容:
fdfs:
so-timeout: 15001 # 超时时间
connect-timeout: 6000 # 连接超时时间
thumb-image: # 缩略图
width: 60
height: 60
tracker-list: # tracker地址:你的虚拟机服务器地址+端口(默认是22122)
- 192.168.59.128:22122
将来通过域名:image.leyou.com这个域名访问fastDFS服务器上的图片资源。所以,需要代理到虚拟机地址:
配置hosts文件,使image.leyou.com可以访问fastDFS服务器
把以下内容copy进去:
package com.leyou;
import com.github.tobato.fastdfs.domain.StorePath;
import com.github.tobato.fastdfs.domain.ThumbImageConfig;
import com.github.tobato.fastdfs.service.FastFileStorageClient;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
@SpringBootTest
@RunWith(SpringRunner.class)
public class FastDFSTest {
@Autowired
private FastFileStorageClient storageClient;
@Autowired
private ThumbImageConfig thumbImageConfig;
@Test
public void testUpload() throws FileNotFoundException {
// 要上传的文件
File file = new File("C:\\Users\\Public\\Pictures\\xbx1.jpg");
// 上传并保存图片,参数:1-上传的文件流 2-文件的大小 3-文件的后缀 4-可以不管他
StorePath storePath = this.storageClient.uploadFile(
new FileInputStream(file), file.length(), "jpg", null);
// 带分组的路径
System.out.println(storePath.getFullPath());
// 不带分组的路径
System.out.println(storePath.getPath());
}
@Test
public void testUploadAndCreateThumb() throws FileNotFoundException {
File file = new File("C:\\Users\\Public\\Pictures\\xbx1.jpg");
// 上传并且生成缩略图
StorePath storePath = this.storageClient.uploadImageAndCrtThumbImage(
new FileInputStream(file), file.length(), "jpg", null);
// 带分组的路径
System.out.println(storePath.getFullPath());
// 不带分组的路径
System.out.println(storePath.getPath());
// 获取缩略图路径
String path = thumbImageConfig.getThumbImagePath(storePath.getPath());
System.out.println(path);
}
}
package com.leyou.upload.service;
import com.github.tobato.fastdfs.domain.StorePath;
import com.github.tobato.fastdfs.service.FastFileStorageClient;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
@Service
public class UploadService {
private static final List content_types= Arrays.asList("image/gif","image/jpeg");
private static final Logger LOGGER= LoggerFactory.getLogger(UploadService.class);
@Autowired
private FastFileStorageClient storageClient;
public String uploadImage(MultipartFile file) {
String originalFilename = file.getOriginalFilename();
//StringUtils.substringAfterLast(originalFilename,".") 不去使用此方法
//校验文件类型
String contentType = file.getContentType();
if(!content_types.contains(contentType)){
LOGGER.info("文件类型不合法:{}",originalFilename);
return null;
}
try {
//校验文件内容
BufferedImage bufferedImage = ImageIO.read(file.getInputStream());
if (bufferedImage==null){
LOGGER.info("文件内容不合法:{}",originalFilename);
return null;
}
// 保存到文件服务器
// file.transferTo(new File("D:\\hm49\\image"+originalFilename));
String ext = StringUtils.substringAfterLast(originalFilename, ".");
StorePath storePath = this.storageClient.uploadFile(file.getInputStream(), file.getSize(), ext, null);
// 返回url进行回显
//return "http://image.leyou.com/"+originalFilename;
return "http://image.leyou.com/"+storePath.getFullPath();
} catch (IOException e) {
LOGGER.info("服务器内部错误:"+originalFilename);
e.printStackTrace();
}
return null;
}
}
为了编写后续功能,需要了解商品类的数据库结构
SPU:Standard Product Unit (标准产品单位) ,一组具有共同属性的商品集
SKU:Stock Keeping Unit(库存量单位),SPU商品集因具体特性不同而细分的每个商品
可以看出:
仔细查看每一种商品的规格你会发现:
虽然商品规格千变万化,但是同一类商品(如手机)的规格是统一的,有图为证:
华为的规格
SPU中会有一些特殊属性,用来区分不同的SKU,我们称为SKU特有属性。如华为META10的颜色、内存属性。
不同种类的商品,一个手机,一个衣服,其SKU属性不相同。
同一种类的商品,比如都是衣服,SKU属性基本是一样的,都是颜色、尺码等。
这样说起来,似乎SKU的特有属性也是与分类相关的?事实上,仔细观察你会发现,SKU的特有属性是商品规格参数的一部分:
也就是说,我们没必要单独对SKU的特有属性进行设计,它可以看做是规格参数中的一部分。这样规格参数中的属性可以标记成两部分:
打开一个搜索页,我们来看看过滤的条件:
你会发现,过滤条件中的屏幕尺寸、运行内存、网路、机身内存、电池容量、CPU核数等,在规格参数中都能找到:
规格参数中的数据,将来会有一部分作为搜索条件来使用。我们可以在设计时,将这部分属性标记出来,将来做搜索的时候,作为过滤条件。要注意的是,无论是SPU的全局属性,还是SKU的特有属性,都有可能作为搜索过滤条件的,并不冲突,而是有一个交集:
可以看到规格参数是分组的,每一组都有多个参数键值对。不过对于规格参数的模板而言,其值现在是不确定的,不同的商品值肯定不同,模板中只要保存组信息、组内参数信息即可。
因此我们设计了两张表:
意思就是对于一个商品分类手机
他的规格组内包括主体、基本信息等
而规格组之下的主体组中的规格参数包括品牌、型号等
规格参数分组表:tb_spec_group
CREATE TABLE `tb_spec_group` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`cid` bigint(20) NOT NULL COMMENT '商品分类id,一个分类下有多个规格组',
`name` varchar(50) NOT NULL COMMENT '规格组的名称',
PRIMARY KEY (`id`),
KEY `key_category` (`cid`)
) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8 COMMENT='规格参数的分组表,每个商品分类下有多个规格参数组';
规格组有3个字段:
规格参数表:tb_spec_param
CREATE TABLE `tb_spec_param` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`cid` bigint(20) NOT NULL COMMENT '商品分类id',
`group_id` bigint(20) NOT NULL,
`name` varchar(255) NOT NULL COMMENT '参数名',
`numeric` tinyint(1) NOT NULL COMMENT '是否是数字类型参数,true或false',
`unit` varchar(255) DEFAULT '' COMMENT '数字类型参数的单位,非数字类型可以为空',
`generic` tinyint(1) NOT NULL COMMENT '是否是sku通用属性,true或false',
`searching` tinyint(1) NOT NULL COMMENT '是否用于搜索过滤,true或false',
`segments` varchar(1000) DEFAULT '' COMMENT '数值类型参数,如果需要搜索,则添加分段间隔值,如CPU频率间隔:0.5-1.0',
PRIMARY KEY (`id`),
KEY `key_group` (`group_id`),
KEY `key_category` (`cid`)
) ENGINE=InnoDB AUTO_INCREMENT=24 DEFAULT CHARSET=utf8 COMMENT='规格参数组下的参数名';
按道理来说,我们的规格参数就只需要记录参数名、组id、商品分类id即可。但是这里却多出了很多字段,为什么?
还记得我们之前的分析吧,规格参数中有一部分是 SKU的通用属性,一部分是SKU的特有属性,而且其中会有一些将来用作搜索过滤,这些信息都需要标记出来。
通用属性
用一个布尔类型字段来标记是否为通用:
搜索过滤
与搜索相关的有两个字段:
数值类型
某些规格参数可能为数值类型,这样的数据才需要划分区间,我们有两个字段来描述:
商品分类树我们之前已经做过,所以这里可以直接展示出来。
因为规格是跟商品分类绑定的,因此首先会展现商品分类树,并且提示你要选择商品分类,才能看到规格参数的模板。一起了解下页面的实现:
这里使用了v-layout
来完成页面布局,并且添加了row属性,代表接下来的内容是行布局(左右)。
可以看出页面分成2个部分:
:左侧,内部又分上下两部分:商品分类树及标题
v-card-title
:标题部分,这里是提示信息,告诉用户要先选择分类,才能看到模板v-tree
:这里用到的是我们之前讲过的树组件,展示商品分类树,
:右侧:内部是规格参数展示页面实现:
可以看到右侧并不是我们熟悉的 v-data-table
,而是一个spec-group
组件(规格组)和spec-param
组件(规格参数),这是我们定义的独立组件:
当我们点击树节点时,要将v-dialog
打开,因此必须绑定一个点击事件:(Specification.vue)
我们来看下handleClick
方法:(Specification.vue)
点击事件发生时,发生了两件事:
showGroup
被置为true,则规格组就会显示了。同时,我们把被选中的节点(商品分类)的id传递给了SpecGroup
组件:(Specification.vue)
来看下SpecGroup.vue
中的实现:
我们查看页面控制台,可以看到请求已经发出:
实体类
package com.leyou.item.pojo;
import javax.persistence.*;
import java.util.List;
@Table(name = "tb_spec_group")
public class SpecGroup {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long cid;
private String name;
@Transient
private List<SpecParam> params;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getCid() {
return cid;
}
public void setCid(Long cid) {
this.cid = cid;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<SpecParam> getParams() {
return params;
}
public void setParams(List<SpecParam> params) {
this.params = params;
}
// getter和setter省略
}
package com.leyou.item.pojo;
import javax.persistence.*;
@Table(name = "tb_spec_param")
public class SpecParam {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long cid;
private Long groupId;
private String name;
@Column(name = "`numeric`") //列名和字段名不一样的情况下使用
private Boolean numeric;//numrtic 在mysql中属于关键字 所以加''
private String unit;
private Boolean generic;
private Boolean searching;
private String segments;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getCid() {
return cid;
}
public void setCid(Long cid) {
this.cid = cid;
}
public Long getGroupId() {
return groupId;
}
public void setGroupId(Long groupId) {
this.groupId = groupId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Boolean getNumeric() {
return numeric;
}
public void setNumeric(Boolean numeric) {
this.numeric = numeric;
}
public String getUnit() {
return unit;
}
public void setUnit(String unit) {
this.unit = unit;
}
public Boolean getGeneric() {
return generic;
}
public void setGeneric(Boolean generic) {
this.generic = generic;
}
public Boolean getSearching() {
return searching;
}
public void setSearching(Boolean searching) {
this.searching = searching;
}
public String getSegments() {
return segments;
}
public void setSegments(String segments) {
this.segments = segments;
}
// getter和setter ...
}
mapper
public interface SpecGroupMapper extends Mapper<SpecGroup> {
}
controller
先分析下需要的东西,在页面的ajax请求中可以看出:
请求方式:get
请求路径:/spec/groups/{cid} ,这里通过路径占位符传递商品分类的id
请求参数:商品分类id
controller
public class SpecificationController {
@Autowired
private SpecificationService specificationService;
/**
* 根据分类查询参数组
* @param cid
* @return
*/
@GetMapping("groups/{cid}")
public ResponseEntity> queryGroupsById(@PathVariable("cid") Long cid){
List groups=this.specificationService.queryGroupsByCid(cid);
if (CollectionUtils.isEmpty(groups)){
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(groups);
}
/**
* 根据gid查参数组
* @param gid
* @return
*/
@GetMapping("params")
public ResponseEntity> queryParams(@RequestParam("gid") Long gid){
List params =this.specificationService.queryParams(gid);
if(CollectionUtils.isEmpty(params)){
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(params);
}
}
service
package com.leyou.item.service;
import com.leyou.item.mapper.SpecGroupMapper;
import com.leyou.item.mapper.SpecParamMapper;
import com.leyou.item.pojo.SpecGroup;
import com.leyou.item.pojo.SpecParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class SpecificationService {
@Autowired
private SpecGroupMapper groupMapper;
@Autowired
private SpecParamMapper paramMapper;
/**
* 根据分类查询属性组
* @param cid
* @return
*/
public List queryGroupsByCid(Long cid) {
SpecGroup record = new SpecGroup();
record.setCid(cid);
return this.groupMapper.select(record);
}
public List queryParams(Long gid) {
SpecParam record = new SpecParam();
record.setGroupId(gid);
return this.paramMapper.select(record);
}
}
点击规格组:如点击主体、基本信息、操作系统中的任意一项
当我们点击规格组,会切换到规格参数显示,肯定是在规格组中绑定了点击事件:
事件处理
可以看到这里是使用了父子通信,子组件触发了select事件:
事件处理
这里我们记录了选中的分组,并且把标记设置为false,这样规格组就不显示了,而是显示:SpecParam
我们来看SpecParam.vue
的实现:
查看页面控制台,发现请求已经发出:
报404,因为我们还没有实现后台逻辑,接下来就去实现。
SpecificationController
分析:
List
代码:
/**
* 根据条件查询规格参数
* @param gid
* @return
*/
@GetMapping("params")
public ResponseEntity<List<SpecParam>> queryParams(@RequestParam("gid")Long gid){
List<SpecParam> params = this.specificationService.queryParams(gid);
if (CollectionUtils.isEmpty(params)){
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(params);
}
SpecificationService
@Autowired
private SpecParamMapper paramMapper;
/**
* 根据条件查询规格参数
* @param gid
* @return
*/
public List<SpecParam> queryParams(Long gid) {
SpecParam param = new SpecParam();
param.setGroupId(gid);
return this.paramMapper.select(param);
}
SpecParamMapper
public interface SpecParamMapper extends Mapper<SpecParam> {
}
SPU表:
CREATE TABLE `tb_spu` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'spu id',
`title` varchar(255) NOT NULL DEFAULT '' COMMENT '标题',
`sub_title` varchar(255) DEFAULT '' COMMENT '子标题',
`cid1` bigint(20) NOT NULL COMMENT '1级类目id',
`cid2` bigint(20) NOT NULL COMMENT '2级类目id',
`cid3` bigint(20) NOT NULL COMMENT '3级类目id',
`brand_id` bigint(20) NOT NULL COMMENT '商品所属品牌id',
`saleable` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否上架,0下架,1上架',
`valid` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否有效,0已删除,1有效',
`create_time` datetime DEFAULT NULL COMMENT '添加时间',
`last_update_time` datetime DEFAULT NULL COMMENT '最后修改时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=208 DEFAULT CHARSET=utf8 COMMENT='spu表,该表描述的是一个抽象的商品,比如 iphone8';
与我们前面分析的基本类似,但是似乎少了一些字段,比如商品描述。
我们做了表的垂直拆分,将SPU的详情放到了另一张表:tb_spu_detail
CREATE TABLE `tb_spu_detail` (
`spu_id` bigint(20) NOT NULL,
`description` text COMMENT '商品描述信息',
`generic_spec` varchar(10000) NOT NULL DEFAULT '' COMMENT '通用规格参数数据',
`special_spec` varchar(1000) NOT NULL COMMENT '特有规格参数及可选值信息,json格式',
`packing_list` varchar(3000) DEFAULT '' COMMENT '包装清单',
`after_service` varchar(3000) DEFAULT '' COMMENT '售后服务',
PRIMARY KEY (`spu_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
这张表中的数据都比较大,而且对于同一SPU商品的不同规格来说不会变化。为了不影响主表的查询效率我们拆分出这张表。
需要注意的是这两个字段:generic_spec和special_spec。
前面讲过规格参数与商品分类绑定,一个分类下的所有SPU具有类似的规格参数。SPU下的SKU可能会有不同的规格参数信息,因此我们计划是这样:
来看下我们的表如何存储这些信息。
首先是generic_spec
,其中保存通用规格参数信息的值,这里为了方便查询,使用了json格式:
json结构,其中都是键值对:
spec_param
的id我们说spu中只保存通用规格参数,那么为什么有多出了一个special_spec
字段呢?
以手机为例,品牌、操作系统等肯定是全局通用属性,内存、颜色等肯定是特有属性。
当你确定了一个SPU,比如小米的:红米4X
全局属性值都是固定的了:
品牌:小米
型号:红米4X
特有属性举例:
颜色:[香槟金, 樱花粉, 磨砂黑]
内存:[2G, 3G]
机身存储:[16GB, 32GB]
颜色、内存、机身存储,作为SKU特有属性,key虽然一样,但是SPU下的每一个SKU,其值都不一样,所以值会有很多,形成数组。
我们在SPU中,会把特有属性的所有值都记录下来,形成一个数组:
里面又有哪些内容呢?
来看数据格式:
也是json结构:
那么问题来:特有规格参数应该在sku中记录才对,为什么在spu中也要记录一份?
因为我们有时候需要把所有规格参数都查询出来,而不是只查询1个sku的属性。比如,商品详情页展示可选的规格参数时:
刚好符合我们的结构,这样页面渲染就非常方便了。
CREATE TABLE `tb_sku` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'sku id',
`spu_id` bigint(20) NOT NULL COMMENT 'spu id',
`title` varchar(255) NOT NULL COMMENT '商品标题',
`images` varchar(1000) DEFAULT '' COMMENT '商品的图片,多个图片以‘,’分割',
`price` bigint(15) NOT NULL DEFAULT '0' COMMENT '销售价格,单位为分',
`indexes` varchar(100) COMMENT '特有规格属性在spu属性模板中的对应下标组合',
`own_spec` varchar(1000) COMMENT 'sku的特有规格参数,json格式',
`enable` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否有效,0无效,1有效',
`create_time` datetime NOT NULL COMMENT '添加时间',
`last_update_time` datetime NOT NULL COMMENT '最后修改时间',
PRIMARY KEY (`id`),
KEY `key_spu_id` (`spu_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='sku表,该表表示具体的商品实体,如黑色的64GB的iphone 8';
还有一张表,代表库存:
CREATE TABLE `tb_stock` (
`sku_id` bigint(20) NOT NULL COMMENT '库存对应的商品sku id',
`seckill_stock` int(9) DEFAULT '0' COMMENT '可秒杀库存',
`seckill_total` int(9) DEFAULT '0' COMMENT '秒杀总数量',
`stock` int(9) NOT NULL COMMENT '库存数量',
PRIMARY KEY (`sku_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='库存表,代表库存,秒杀库存等信息';
问题:为什么要将库存独立一张表?
因为库存字段写频率较高,而SKU的其它字段以读为主,因此我们将两张表分离,读写不会干扰。
特别需要注意的是sku表中的indexes
字段和own_spec
字段。sku中应该保存特有规格参数的值,就在这两个字段中。
在SPU表中,已经对特有规格参数及可选项进行了保存,结构如下:
{
"4": [
"香槟金",
"樱花粉",
"磨砂黑"
],
"12": [
"2GB",
"3GB"
],
"13": [
"16GB",
"32GB"
]
}
这些特有属性如果排列组合,会产生12个不同的SKU,而不同的SKU,其属性就是上面备选项中的一个。
比如:
你会发现,每一个属性值,对应于SPUoptions数组的一个选项,如果我们记录下角标,就是这样:
既然如此,我们是不是可以将不同角标串联起来,作为SPU下不同SKU的标示。这就是我们的indexes字段。
这个设计在商品详情页会特别有用:
当用户点击选中一个特有属性,你就能根据 角标快速定位到sku。
看结构:
{"4":"香槟金","12":"2GB","13":"16GB"}
保存的是特有属性的键值对。
SPU中保存的是可选项,但不确定具体的值,而SKU中的保存的就是具体的值。
先看整体页面结构(Goods.vue):
并且在Vue实例挂载后就会发起查询(mounted调用getDataFromServer方法初始化数据):
我们刷新页面,可以看到浏览器发起已经发起了查询商品数据的请求:
在leyou-item-interface工程中添加实体类:
SPU
@Table(name = "tb_spu")
public class Spu {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long brandId;
private Long cid1;// 1级类目
private Long cid2;// 2级类目
private Long cid3;// 3级类目
private String title;// 标题
private String subTitle;// 子标题
private Boolean saleable;// 是否上架
private Boolean valid;// 是否有效,逻辑删除用
private Date createTime;// 创建时间
private Date lastUpdateTime;// 最后修改时间
// 省略getter和setter
}
SPU详情
@Table(name="tb_spu_detail")
public class SpuDetail {
@Id
private Long spuId;// 对应的SPU的id
private String description;// 商品描述
private String specialSpec;// 商品特殊规格的名称及可选值模板
private String genericSpec;// 商品的全局规格属性
private String packingList;// 包装清单
private String afterService;// 售后服务
// 省略getter和setter
}
public interface SpuMapper extends Mapper<Spu> {
}
先分析:
请求方式:GET
请求路径:/spu/page
请求参数:
返回结果:商品SPU的分页信息。
要注意,页面展示的是商品分类和品牌名称,而数据库中保存的是id,怎么办?
我们可以新建一个类,继承SPU,并且拓展cname和bname属性,写到leyou-item-interface
public class SpuBo extends Spu {
String cname;// 商品分类名称
String bname;// 品牌名称
// 略 。。
}
编写controller代码:
我们把与商品相关的一切业务接口都放到一起,起名为GoodsController,业务层也是这样
package com.leyou.item.controller;
import com.leyou.common.pojo.PageResult;
import com.leyou.item.bo.Spubo;
import com.leyou.item.service.GoodsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
public class GoodsController {
@Autowired
private GoodsService goodsService;
/**
* 根据条件分页查询spu
* @param key
* @param saleable
* @param page
* @param rows
* @return
*/
@GetMapping("spu/page")
public ResponseEntity> querySpuByPage(
@RequestParam (value="key",required = false) String key,
@RequestParam (value="saleable",required = false) Boolean saleable,
@RequestParam (value="page",defaultValue = "1") Integer page,
@RequestParam (value="rows",defaultValue = "5") Integer rows
){
PageResult result=this.goodsService.querySpuByPage(key,saleable,page,rows);
if (result==null|| CollectionUtils.isEmpty(result.getItems())){
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(result);
}
}
所有商品相关的业务(包括SPU和SKU)放到一个业务下:GoodsService。
package com.leyou.item.service;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.leyou.common.pojo.PageResult;
import com.leyou.item.bo.Spubo;
import com.leyou.item.mapper.BrandMapper;
import com.leyou.item.mapper.SpuDetailMapper;
import com.leyou.item.mapper.SpuMapper;
import com.leyou.item.pojo.Brand;
import com.leyou.item.pojo.Spu;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import tk.mybatis.mapper.entity.Example;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class GoodsService {
@Autowired
private SpuMapper spuMapper;
@Autowired
private BrandMapper brandMapper;
@Autowired
private SpuDetailMapper spuDetailMapper;
@Autowired
private CategoryService categoryService;
/**
* 根据分页查询spu
* @param key
* @param saleable
* @param page
* @param rows
* @return
*/
public PageResult querySpuByPage(String key, Boolean saleable, Integer page, Integer rows) {
Example example = new Example(Spu.class);
Example.Criteria criteria = example.createCriteria();
//添加查询条件
if(StringUtils.isNotBlank(key)) {
criteria.andLike("title", "%" + key + "%");
}
//添加上下架的过滤条件
if(saleable!=null){
criteria.andEqualTo("saleable",saleable);
}
//添加分页
PageHelper.startPage(page,rows);
//执行查询,获取spu集合
List spus = this.spuMapper.selectByExample(example);
PageInfo pageInfo = new PageInfo<>(spus);
//spu集合转化成spubo集合
List spubos = spus.stream().map(spu -> {
Spubo spubo = new Spubo();
BeanUtils.copyProperties(spu, spubo);
//查询品牌名称
Brand brand = this.brandMapper.selectByPrimaryKey(spu.getBrandId());
spubo.setBname(brand.getName());
// 查询分类名称
List names = this.categoryService.queryNamesByIds(Arrays.asList(spu.getCid1(), spu.getCid2(), spu.getCid3()));
spubo.setCname(StringUtils.join(names, "-"));
return spubo;
}).collect(Collectors.toList());
// 返回pageresult
return new PageResult<>(pageInfo.getTotal(),spubos);
}
}
页面需要商品的分类名称需要在这里查询,因此要额外提供查询分类名称的功能,
在CategoryService中添加功能:
package com.leyou.item.service;
import com.leyou.item.mapper.CategoryMapper;
import com.leyou.item.pojo.Category;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class CategoryService {
@Autowired
private CategoryMapper categoryMapper;
/**
* 根据父节点查询子节点
* @param pid
* @return
*/
public List queryCategoriesByPid(Long pid) {
Category record = new Category();
record.setParentId(pid);
return this.categoryMapper.select(record);
}
public List queryNamesByIds(List ids){
List categories = this.categoryMapper.selectByIdList(ids);
return categories.stream().map(category -> category.getName()).collect(Collectors.toList());
}
}
mapper的selectByIdList方法是来自于通用mapper。不过需要我们在mapper上继承一个通用mapper接口:
public interface CategoryMapper extends Mapper<Category>, SelectByIdListMapper<Category, Long> {
}
点击商品列表页面的商品新增按钮
里面把商品的数据分为了4部分来填写:
cid1
,cid2
,cid3
属性brandId
属性title
属性subTitle
属性afterService
属性packingList
属性description
属性,数据较多,所以单独放一个页面genericSpec
属性商品分类信息查询我们之前已经做过,所以这里的级联选框已经实现完成:
品牌也是一个下拉选框,不过其选项是不确定的,只有当用户选择了商品分类,才会把这个分类下的所有品牌展示出来。
所以页面编写了watch函数,监控商品分类的变化,每当商品分类值有变化,就会发起请求,查询品牌列表:
选择商品分类后,可以看到请求发起:
接下来,我们只要编写后台接口,根据商品分类id,查询对应品牌即可。
后台接口
页面需要去后台查询品牌信息,我们自然需要提供:
请求方式:GET
请求路径:/brand/cid/{cid}
请求参数:cid
响应数据:品牌集合
BrandController
@GetMapping("cid/{cid}")
public ResponseEntity<List<Brand>> queryBrandsByCid(@PathVariable("cid")Long cid){
List<Brand> brands = this.brandService.queryBrandsByCid(cid);
if (CollectionUtils.isEmpty(brands)) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(brands);
}
BrandService
public List<Brand> queryBrandsByCid(Long cid) {
return this.brandMapper.selectBrandByCid(cid);
}
BrandMapper
根据分类查询品牌有中间表,需要自己编写Sql:
@Select("SELECT b.* from tb_brand b INNER JOIN tb_category_brand cb on b.id=cb.brand_id where cb.category_id=#{cid}")
List<Brand> selectBrandByCid(Long cid);
商品描述信息比较复杂,而且图文并茂,甚至包括视频。
这样的内容,一般都会使用富文本编辑器。
通俗来说:富文本,就是比较丰富的文本编辑器。普通的框只能输入文字,而富文本还能给文字加颜色样式等。
富文本编辑器有很多,例如:KindEditor、Ueditor。但并不原生支持vue
但是我们今天要说的,是一款支持Vue的富文本编辑器:vue-quill-editor
GitHub的主页:https://github.com/surmon-china/vue-quill-editor
Vue-Quill-Editor是一个基于Quill的富文本编辑器:
使用非常简单:已经在项目中集成。以下步骤不需操作,仅供参考
第一步:安装,使用npm命令:
npm install vue-quill-editor --save
第二步:加载,在js中引入:
全局引入:
import Vue from 'vue'
import VueQuillEditor from 'vue-quill-editor'
const options = {}; /* { default global options } */
Vue.use(VueQuillEditor, options); // options可选
局部引入:
import 'quill/dist/quill.core.css'
import 'quill/dist/quill.snow.css'
import 'quill/dist/quill.bubble.css'
import {quillEditor} from 'vue-quill-editor'
var vm = new Vue({
components:{
quillEditor
}
})
<quill-editor v-model="goods.spuDetail.description" :options="editorOption"/>
不过这个组件有个小问题,就是图片上传的无法直接上传到后台,因此我们对其进行了封装,支持了图片的上传。
使用也非常简单:
<v-stepper-content step="2">
<v-editor v-model="goods.spuDetail.description" upload-url="/upload/image"/>
v-stepper-content>
规格参数的查询我们之前也已经编写过接口,因为商品规格参数也是与商品分类绑定,所以需要在商品分类变化后去查询,我们也是通过watch监控来实现:
可以看到这里是根据商品分类id查询规格参数:SpecParam。我们之前写过一个根据gid(分组id)来查询规格参数的接口,我们接下来完成根据分类id查询规格参数。
改造查询规格参数接口
我们在原来的根据 gid(规格组id)查询规格参数的接口上,添加一个参数:cid,即商品分类id。
等一下, 考虑到以后可能还会根据是否搜索、是否为通用属性等条件过滤,我们多添加几个过滤条件:SpecificationController
第二个方法改造为
/**
* 根据gid查参数组
* @param gid
* @return
*/
@GetMapping("params")
public ResponseEntity<List<SpecParam>> queryParams(
@RequestParam(value = "gid",required = false) Long gid,
@RequestParam(value = "cid",required = false) Long cid,
@RequestParam(value = "generic",required = false) Boolean generic,
@RequestParam(value = "searching",required = false) Boolean searching
){
List<SpecParam> params =this.specificationService.queryParams(gid,cid,generic,searching);
if(CollectionUtils.isEmpty(params)){
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(params);
}
改造SpecificationService:
public List queryParams(Long gid,Long cid,Boolean generic,Boolean searching) {
SpecParam record = new SpecParam();
record.setGroupId(gid);
record.setCid(cid);
record.setGeneric(generic);
record.setSearching(searching);
return this.paramMapper.select(record);
}
如果param中有属性为null,则不会把属性作为查询条件,因此该方法具备通用性,即可根据gid查询,也可根据cid查询。
Sku属性是SPU下的每个商品的不同特征,如图:
当我们填写一些属性后,会在页面下方生成一个sku表格,大家可以计算下会生成多少个不同属性的Sku呢?
当你选择了上图中的这些选项时:
此时会产生多少种SKU呢? 应该是 3 * 2 * 2 = 12种,这其实就是在求笛卡尔积。
我们会在页面下方生成一个sku的表格:
可以选择是否启用
在sku列表的下方,有一个提交按钮:保存商品信息
并且绑定了点击事件:
点击后会组织数据并向后台提交:
submit() {
// 表单校验。
if(!this.$refs.basic.validate){
this.$message.error("请先完成表单内容!");
}
// 先处理goods,用结构表达式接收,除了categories外,都接收到goodsParams中
const {
categories: [{ id: cid1 }, { id: cid2 }, { id: cid3 }],
...goodsParams
} = this.goods;
// 处理规格参数
const specs = {};
this.specs.forEach(({ id,v }) => {
specs[id] = v;
});
// 处理特有规格参数模板
const specTemplate = {};
this.specialSpecs.forEach(({ id, options }) => {
specTemplate[id] = options;
});
// 处理sku
const skus = this.skus
.filter(s => s.enable)
.map(({ price, stock, enable, images, indexes, ...rest }) => {
// 标题,在spu的title基础上,拼接特有规格属性值
const title = goodsParams.title + " " + Object.values(rest).map(v => v.v).join(" ");
const obj = {};
Object.values(rest).forEach(v => {
obj[v.id] = v.v;
});
return {
price: this.$format(price), // 价格需要格式化
stock,
indexes,
enable,
title, // 基本属性
images: images ? images.join(",") : '', // 图片
ownSpec: JSON.stringify(obj) // 特有规格参数
};
});
Object.assign(goodsParams, {
cid1,
cid2,
cid3, // 商品分类
skus // sku列表
});
goodsParams.spuDetail.genericSpec = JSON.stringify(specs);
goodsParams.spuDetail.specialSpec = JSON.stringify(specTemplate);
// 提交到后台
this.$http({
method: this.isEdit ? "put" : "post",
url: "/item/goods",
data: goodsParams
})
.then(() => {
// 成功,关闭窗口
this.$emit("close");
// 提示成功
this.$message.success("保存成功了");
})
.catch(() => {
this.$message.error("保存失败!");
});
}
点击提交,查看控制台提交的数据格式:
整体是一个json格式数据,包含Spu表所有数据:
SPU和SpuDetail实体类已经添加过,添加Sku和Stock对象:
Sku
@Table(name = "tb_sku")
public class Sku {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long spuId;
private String title;
private String images;
private Long price;
private String ownSpec;// 商品特殊规格的键值对
private String indexes;// 商品特殊规格的下标
private Boolean enable;// 是否有效,逻辑删除用
private Date createTime;// 创建时间
private Date lastUpdateTime;// 最后修改时间
@Transient
private Integer stock;// 库存
}
注意:这里保存了一个库存字段,在数据库中是另外一张表保存的,方便查询。
Stock
@Table(name = "tb_stock")
public class Stock {
@Id
private Long skuId;
private Integer seckillStock;// 秒杀可用库存
private Integer seckillTotal;// 已秒杀数量
private Integer stock;// 正常库存
}
结合浏览器页面控制台,可以发现:
请求方式:POST
请求路径:/goods
请求参数:Spu的json格式的对象,spu中包含spuDetail和Sku集合。这里我们该怎么接收?我们之前定义了一个SpuBo对象,作为业务对象。这里也可以用它,不过需要再扩展spuDetail和skus字段:
public class SpuBo extends Spu {
String cname;// 商品分类名称
String bname;// 品牌名称
SpuDetail spuDetail;// 商品详情
List<Sku> skus;// sku列表
}
@PostMapping("goods")
public ResponseEntity<Void> saveGoods(@RequestBody SpuBo spuBo){
this.goodsService.saveGoods(spuBo);
return ResponseEntity.status(HttpStatus.CREATED).build();
}
注意:通过@RequestBody注解来接收Json请求
这里的逻辑比较复杂,我们除了要对SPU新增以外,还要对SpuDetail、Sku、Stock进行保存
/**
* 新增商品
* @param spubo
*/
@Transactional
public void saveGoods(Spubo spubo) {
//先新增spu
spubo.setId(null);
spubo.setSaleable(true);
spubo.setValid(true);
spubo.setCreateTime(new Date());
spubo.setLastUpdateTime(spubo.getCreateTime());
this.spuMapper.insertSelective(spubo);
// 再新增spudetail
SpuDetail spuDetail = spubo.getSpuDetail();
spuDetail.setSpuId(spubo.getId());
this.spuDetailMapper.insertSelective(spuDetail);
// 新增sku
spubo.getSkus().forEach(sku -> {
//新增sku
sku.setId(null);
sku.setSpuId(spubo.getId());
sku.setCreateTime(new Date());
sku.setLastUpdateTime(sku.getCreateTime());
this.skuMapper.insertSelective(sku);
//新增stock
Stock stock = new Stock();
stock.setSkuId(sku.getId());
stock.setStock(sku.getStock());
this.stockMapper.insertSelective(stock);
});
// 新增stock
}
在商品详情页,每一个商品后面,都会有一个编辑按钮:
点击这个按钮,就会打开一个商品编辑窗口,我们看下它所绑定的点击事件:(在item/Goods.vue)
对应的方法
可以看到这里发起了两个请求,在查询商品详情和sku信息。
因为在商品列表页面,只有spu的基本信息:id、标题、品牌、商品分类等。比较复杂的商品详情(spuDetail)和sku信息都没有,编辑页面要回显数据,就需要查询这些内容。
因此,接下来我们就编写后台接口,提供查询服务接口。
需要分析的内容:
@GetMapping("spu/detail/{spuId}")
public ResponseEntity<SpuDetail> querySpuDetailBySpuId(@PathVariable("spuId")Long spuId){
SpuDetail spuDetail = this.goodsService.querySpuDetailBySpuId(spuId);
if (spuDetail == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(spuDetail);
}
GoodsService
/**
* 根据spuId查询spuDetail
* @param spuId
* @return
*/
public SpuDetail querySpuDetailBySpuId(Long spuId) {
return this.spuDetailMapper.selectByPrimaryKey(spuId);
}
分析
GoodsController
@GetMapping("sku/list")
public ResponseEntity<List<Sku>> querySkusBySpuId(@RequestParam("id")Long spuId){
List<Sku> skus = this.goodsService.querySkusBySpuId(spuId);
if (CollectionUtils.isEmpty(skus)) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(skus);
}
GoodsService
需要注意的是,为了页面回显方便,我们一并把sku的库存stock也查询出来
/**
* 根据spuId查询sku的集合
* @param spuId
* @return
*/
public List<Sku> querySkusBySpuId(Long spuId) {
Sku sku = new Sku();
sku.setSpuId(spuId);
List<Sku> skus = this.skuMapper.select(sku);
skus.forEach(s -> {
Stock stock = this.stockMapper.selectByPrimaryKey(s.getId());
s.setStock(stock.getStock());
});
return skus;
}
接下来,我们编写后台,实现修改商品接口。
@PutMapping("goods")
public ResponseEntity<Void> updateGoods(@RequestBody SpuBo spuBo){
this.goodsService.updateGoods(spuBo);
return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
}
该方法写在新增下方,方便区分
spu数据可以修改,但是SKU数据无法修改,因为有可能之前存在的SKU现在已经不存在了,或者以前的sku属性都不存在了。比如以前内存有4G,现在没了。
因此这里直接删除以前的SKU,然后新增即可。
笔记代码:
@Transactional
public void update(SpuBo spu) {
// 查询以前sku
List<Sku> skus = this.querySkuBySpuId(spu.getId());
// 如果以前存在,则删除
if(!CollectionUtils.isEmpty(skus)) {
List<Long> ids = skus.stream().map(s -> s.getId()).collect(Collectors.toList());
// 删除以前库存
Example example = new Example(Stock.class);
example.createCriteria().andIn("skuId", ids);
this.stockMapper.deleteByExample(example);
// 删除以前的sku
Sku record = new Sku();
record.setSpuId(spu.getId());
this.skuMapper.delete(record);
}
// 新增sku和库存
saveSkuAndStock(spuBo);
// 更新spu
spu.setLastUpdateTime(new Date());
spu.setCreateTime(null);
spu.setValid(null);
spu.setSaleable(null);
this.spuMapper.updateByPrimaryKeySelective(spu);
// 更新spu详情
this.spuDetailMapper.updateByPrimaryKeySelective(spu.getSpuDetail());
}
视频中的代码
/**
* sku数据都是直接删除再新增
* @param spu
*/
@Transactional
public void upDateGoods(Spubo spu) {
//查询以前sku
Sku record = new Sku();
record.setSpuId(spu.getId());
List<Sku> skus = this.skuMapper.select(record);
//如果以前存在,则删除
skus.forEach(sku -> {
//delete stock
this.stockMapper.deleteByPrimaryKey(sku.getId());
});
//delete sku
Sku sku = new Sku();
sku.setSpuId(spu.getId());
this.skuMapper.delete(sku);
//新增sku和库存
saveSkuAndStock(spu);
//更新spu
spu.setLastUpdateTime(new Date());
spu.setCreateTime(null);
spu.setValid(null);
spu.setSaleable(null);
this.spuMapper.updateByPrimaryKeySelective(spu);
//跟新spu详情
this.spuDetailMapper.updateByPrimaryKeySelective(spu.getSpuDetail());
}
其中的方法saveSkuAndStock()采取提取的方式生成,在新增方法中的的内容选中,以右键 Refactor => Extract => Method ,修改名字生成
private void saveSkuAndStock(Spubo spubo) {
spubo.getSkus().forEach(sku -> {
//新增sku
sku.setId(null);
sku.setSpuId(spubo.getId());
sku.setCreateTime(new Date());
sku.setLastUpdateTime(sku.getCreateTime());
this.skuMapper.insertSelective(sku);
//新增stock
Stock stock = new Stock();
stock.setSkuId(sku.getId());
stock.setStock(sku.getStock());
this.stockMapper.insertSelective(stock);
});
}
与以前一样。