项目结构

项目结构.png

SpringBoot版本2.0.2.RELEASE SpringCloud版本Finchley.SR4

包含SpringBoot版本

控制SpringCloud版本 leyou/pom.yml ```java <?xml version=”1.0” encoding=”UTF-8”?> 4.0.0 ly-gateway ly-item ly-registry ly-common ly-upload ly-search org.springframework.boot spring-boot-starter-parent 2.0.2.RELEASE com.leyou.parent leyou 1.0.0-SNAPSHOT pom Finchley.SR4 11 8.0.15 2.1.5 1.3.0 1.27.2 org.springframework.cloud spring-cloud-dependencies ${spring.cloud-version} pom import mysql mysql-connector-java ${mysql.version} tk.mybatis mapper-spring-boot-starter ${tk.mybatis.version} com.github.pagehelper pagehelper-spring-boot-starter 1.3.0 com.github.tobato fastdfs-client 1.26.2
  1. <dependencies>
  2. <dependency>
  3. <groupId>org.apache.commons</groupId>
  4. <artifactId>commons-lang3</artifactId>
  5. <version>3.11</version>
  6. </dependency>
  7. <dependency>
  8. <groupId>org.projectlombok</groupId>
  9. <artifactId>lombok</artifactId>
  10. <version>1.18.16</version>
  11. </dependency>
  12. </dependencies>
  13. <build>
  14. <plugins>
  15. <plugin>
  16. <groupId>org.springframework.boot</groupId>
  17. <artifactId>spring-boot-maven-plugin</artifactId>
  18. </plugin>
  19. </plugins>
  20. </build>

  1. 服务都需要注册到`注册中心`,请求只能通过网关进行访问(上传文件除外 太大了)
  2. <a name="ly-registry"></a>
  3. ### ly-registry
  4. ![image-20210614115440848.png](https://cdn.nlark.com/yuque/0/2021/png/1656653/1629790357944-5f1b0c13-202b-47a5-a45e-adb84f9e50d2.png#clientId=uf1f604a7-2306-4&from=drop&id=uf0d2ca8c&margin=%5Bobject%20Object%5D&name=image-20210614115440848.png&originHeight=273&originWidth=416&originalType=binary&ratio=1&size=56361&status=done&style=none&taskId=u3f892b92-b663-4252-a76a-bd3c6843567)
  5. ```java
  6. <?xml version="1.0" encoding="UTF-8"?>
  7. <project xmlns="http://maven.apache.org/POM/4.0.0"
  8. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  9. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  10. <parent>
  11. <artifactId>leyou</artifactId>
  12. <groupId>com.leyou.parent</groupId>
  13. <version>1.0.0-SNAPSHOT</version>
  14. </parent>
  15. <modelVersion>4.0.0</modelVersion>
  16. <groupId>com.leyou.common</groupId>
  17. <artifactId>ly-registry</artifactId>
  18. <properties>
  19. </properties>
  20. <dependencies>
  21. <dependency>
  22. <groupId>org.springframework.cloud</groupId>
  23. <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
  24. </dependency>
  25. <!-- 解决JDK版本过高问题 -->
  26. <!-- java.lang.TypeNotPresentException: Type javax.xml.bind.JAXBContext not present -->
  27. <dependency>
  28. <groupId>javax.xml.bind</groupId>
  29. <artifactId>jaxb-api</artifactId>
  30. </dependency>
  31. <dependency>
  32. <groupId>com.sun.xml.bind</groupId>
  33. <artifactId>jaxb-impl</artifactId>
  34. <version>2.3.0</version>
  35. </dependency>
  36. <dependency>
  37. <groupId>org.glassfish.jaxb</groupId>
  38. <artifactId>jaxb-runtime</artifactId>
  39. <version>2.3.0</version>
  40. </dependency>
  41. <dependency>
  42. <groupId>javax.activation</groupId>
  43. <artifactId>activation</artifactId>
  44. <version>1.1.1</version>
  45. </dependency>
  46. </dependencies>
  47. </project>

yml配置

  1. server:
  2. port: 10086
  3. spring:
  4. application:
  5. name: ly-registry
  6. eureka:
  7. client:
  8. service-url:
  9. defaultZone: http://127.0.0.1:10086/eureka/
  10. register-with-eureka: false # 自己不注册
  11. fetch-registry: false
  12. # server:
  13. # enable-self-preservation: false #关闭自我保护机制,开发环境使用,生成环境一定要关闭(true)

启动类EnableEurekaServer

  1. @EnableEurekaServer
  2. @SpringBootApplication
  3. public class LyRegisterApplication {
  4. public static void main(String[] args) {
  5. SpringApplication.run(LyRegisterApplication.class,args);
  6. }
  7. }

ly-common

统一异常处理,通用Mapper的BaseMapper,工具类都放在这里面,不需要注册到注册中心

image-20210614115242023.png

ly-item

包含两个服务

ly-item-interface

不需要注册到中心,包含item服务数据库pojo类,api(暴露部分ly-item-service接口,提供给@FeignClient使用)

image-20210614114452951.png

需要在pom中导入相关的包(如 springmvc包)

  1. public interface BrandApi {
  2. @GetMapping("/brand/{id}")
  3. Brand queryBrandById(@PathVariable("id") Long id);
  4. @GetMapping("/brand/list")
  5. List<Brand> queryBrandByIdList(@RequestParam("ids") List<Long> ids);
  6. }

调用方把ly-item-interface添加到pom,使用@FeignClient注册

  1. @FeignClient("item-service")
  2. public interface BrandClient extends BrandApi {
  3. }

ly-item-service

需要注册到注册中心,添加ly-item-interface服务,上面的api包中类之所以没有添加@FeignClient,是因为添加了就是自己服务引用自己了

image-20210614132654817.png

pom

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <project xmlns="http://maven.apache.org/POM/4.0.0"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  5. <parent>
  6. <artifactId>ly-item</artifactId>
  7. <groupId>com.leyou.service</groupId>
  8. <version>1.0.0-SNAPSHOT</version>
  9. </parent>
  10. <modelVersion>4.0.0</modelVersion>
  11. <artifactId>ly-item-service</artifactId>
  12. <properties>
  13. </properties>
  14. <dependencies>
  15. <dependency>
  16. <groupId>org.springframework.boot</groupId>
  17. <artifactId>spring-boot-starter-web</artifactId>
  18. </dependency>
  19. <dependency>
  20. <groupId>org.springframework.cloud</groupId>
  21. <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
  22. </dependency>
  23. <dependency>
  24. <groupId>com.leyou.service</groupId>
  25. <artifactId>ly-item-interface</artifactId>
  26. <version>1.0.0-SNAPSHOT</version>
  27. </dependency>
  28. <dependency>
  29. <groupId>com.leyou.common</groupId>
  30. <artifactId>ly-common</artifactId>
  31. <version>1.0.0-SNAPSHOT</version>
  32. </dependency>
  33. </dependencies>
  34. </project>

yml

  1. server:
  2. port: 8081
  3. spring:
  4. application:
  5. name: item-service
  6. datasource:
  7. username: root
  8. password: 123456
  9. url: jdbc:mysql://localhost:3306/leyou?useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=utf-8
  10. eureka:
  11. client:
  12. service-url:
  13. defaultZone: http://127.0.0.1:10086/eureka/
  14. instance:
  15. prefer-ip-address: true #设置eureka页面链接地址为ip地址
  16. # instance-id: ${spring.application.name} #自定义列表名称
  17. # ip-address: 127.0.0.1
  18. #logging:
  19. # level:
  20. # cn.chy.mapper: debug
  21. logging:
  22. level:
  23. com.leyou.item.mapper: debug

ly-gataway

需要注册到注册中心,所有服务请求通过网关访问,网关再到注册中心找到对应的服务(文件上传服务需要绕过网关)

如何找到:网关配置一个api前缀 http://localhost:10010/api这就是请求网关服务,在网关服务中给其他服务添加前缀,如/item,这样就是访问item服务http://localhost:10010/api/item

image-20210614140152023.png

pom

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <project xmlns="http://maven.apache.org/POM/4.0.0"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  5. <parent>
  6. <artifactId>leyou</artifactId>
  7. <groupId>com.leyou.parent</groupId>
  8. <version>1.0.0-SNAPSHOT</version>
  9. </parent>
  10. <modelVersion>4.0.0</modelVersion>
  11. <groupId>com.leyou.common</groupId>
  12. <artifactId>ly-gateway</artifactId>
  13. <properties>
  14. </properties>
  15. <dependencies>
  16. <dependency>
  17. <groupId>org.springframework.cloud</groupId>
  18. <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
  19. </dependency>
  20. <dependency>
  21. <groupId>org.springframework.cloud</groupId>
  22. <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
  23. </dependency>
  24. <dependency>
  25. <groupId>org.springframework.boot</groupId>
  26. <artifactId>spring-boot-starter-actuator</artifactId>
  27. </dependency>
  28. </dependencies>
  29. <build>
  30. <plugins>
  31. <plugin>
  32. <groupId>org.springframework.boot</groupId>
  33. <artifactId>spring-boot-maven-plugin</artifactId>
  34. </plugin>
  35. </plugins>
  36. </build>
  37. </project>

yml

下面的 ribbon配置是由zuul才会有

  1. server:
  2. port: 10010
  3. spring:
  4. application:
  5. name: api-gateway
  6. eureka:
  7. client:
  8. service-url:
  9. defaultZone: http://127.0.0.1:10086/eureka/
  10. instance:
  11. prefer-ip-address: true #设置eureka页面链接地址为ip地址
  12. # instance-id: ${spring.application.name} #自定义列表名称
  13. # ip-address: 127.0.0.1
  14. zuul:
  15. prefix: /api #添加路由前缀
  16. routes:
  17. item-service: /item/** # item模块的访问都需要添加 /item
  18. search-service: /search/**
  19. upload-service:
  20. path: /upload/**
  21. serviceId: upload-service # false ,转发到 upload-service服务时候,会带上/upload前缀
  22. stripPrefix: false # 确定在转发之前是否应 删除 此路由的前缀 ,默认为true
  23. hystrix:
  24. command:
  25. default:
  26. execution:
  27. isolation:
  28. thread:
  29. timeoutInMilliseconds: 5000
  30. ribbon:
  31. ConnectionTimeout: 1000 # ribbon连接超时时长 abstractribboncommand 使用zuul才后有
  32. ReadTimeout: 3500 # ribbon的读取超时
  33. MaxAutoRetries: 0 # 当前服务的超时时间
  34. MaxAutoRetriesNextServer: 0 # 切换服务重试次数

ly-upload

一个单独的文件上传服务,上传成功后返回文件地址给前台。

需要注册

pom

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <project xmlns="http://maven.apache.org/POM/4.0.0"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  5. <parent>
  6. <artifactId>leyou</artifactId>
  7. <groupId>com.leyou.parent</groupId>
  8. <version>1.0.0-SNAPSHOT</version>
  9. </parent>
  10. <modelVersion>4.0.0</modelVersion>
  11. <groupId>com.leyou.service</groupId>
  12. <artifactId>upload</artifactId>
  13. <properties>
  14. </properties>
  15. <dependencies>
  16. <dependency>
  17. <groupId>org.springframework.cloud</groupId>
  18. <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
  19. </dependency>
  20. <dependency>
  21. <groupId>org.springframework.boot</groupId>
  22. <artifactId>spring-boot-starter-web</artifactId>
  23. </dependency>
  24. <dependency>
  25. <groupId>com.github.tobato</groupId>
  26. <artifactId>fastdfs-client</artifactId>
  27. </dependency>
  28. <dependency>
  29. <groupId>org.springframework.boot</groupId>
  30. <artifactId>spring-boot-starter-test</artifactId>
  31. </dependency>
  32. <!-- 读取ymljava-->
  33. <dependency>
  34. <groupId>org.springframework.boot</groupId>
  35. <artifactId>spring-boot-configuration-processor</artifactId>
  36. <optional>true</optional>
  37. </dependency>
  38. <dependency>
  39. <groupId>com.leyou.common</groupId>
  40. <artifactId>ly-common</artifactId>
  41. <version>1.0.0-SNAPSHOT</version>
  42. </dependency>
  43. </dependencies>
  44. </project>

yml

  1. server:
  2. port: 8082
  3. spring:
  4. application:
  5. name: upload-service
  6. servlet:
  7. multipart:
  8. max-file-size: 5MB
  9. max-request-size: 10MB
  10. eureka:
  11. client:
  12. service-url:
  13. defaultZone: http://127.0.0.1:10086/eureka/
  14. instance:
  15. prefer-ip-address: true #设置eureka页面链接地址为ip地址
  16. # instance-id: ${spring.application.name} #自定义列表名称
  17. # ip-address: 127.0.0.1
  18. fdfs:
  19. so-timeout: 1501
  20. connect-timeout: 601
  21. thumb-image: # 缩略图
  22. width: 60
  23. height: 60
  24. tracker-list: # tracker地址
  25. - 192.168.32.129:22122
  26. ly:
  27. upload:
  28. baseUrl: http://image.leyou.com/
  29. allowTypes:
  30. - image/png
  31. - image/jpeg
  32. - image/bmp
  33. - image/gif

ly-search

使用ElasticSearch完成搜索

pom

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <project xmlns="http://maven.apache.org/POM/4.0.0"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  5. <parent>
  6. <artifactId>leyou</artifactId>
  7. <groupId>com.leyou.parent</groupId>
  8. <version>1.0.0-SNAPSHOT</version>
  9. </parent>
  10. <modelVersion>4.0.0</modelVersion>
  11. <groupId>com.leyou</groupId>
  12. <artifactId>ly-search</artifactId>
  13. <properties>
  14. <maven.compiler.source>11</maven.compiler.source>
  15. <maven.compiler.target>11</maven.compiler.target>
  16. </properties>
  17. <dependencies>
  18. <dependency>
  19. <groupId>org.springframework.boot</groupId>
  20. <artifactId>spring-boot-starter-web</artifactId>
  21. </dependency>
  22. <dependency>
  23. <groupId>org.springframework.boot</groupId>
  24. <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
  25. </dependency>
  26. <dependency>
  27. <groupId>org.springframework.cloud</groupId>
  28. <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
  29. </dependency>
  30. <dependency>
  31. <groupId>org.springframework.cloud</groupId>
  32. <artifactId>spring-cloud-starter-openfeign</artifactId>
  33. </dependency>
  34. <dependency>
  35. <groupId>org.springframework.boot</groupId>
  36. <artifactId>spring-boot-starter-test</artifactId>
  37. </dependency>
  38. <dependency>
  39. <groupId>com.leyou.service</groupId>
  40. <artifactId>ly-item-interface</artifactId>
  41. <version>1.0.0-SNAPSHOT</version>
  42. </dependency>
  43. </dependencies>
  44. </project>

yml

  1. server:
  2. port: 8083
  3. spring:
  4. application:
  5. name: search-service
  6. data:
  7. elasticsearch:
  8. cluster-name: elasticsearch
  9. cluster-nodes: 192.168.32.129:9300
  10. jackson:
  11. default-property-inclusion: non_null # 为空的字段不返回
  12. eureka:
  13. client:
  14. service-url:
  15. defaultZone: http://127.0.0.1:10086/eureka/
  16. instance:
  17. prefer-ip-address: true #设置eureka页面链接地址为ip地址
  18. # instance-id: ${spring.application.name} #自定义列表名称
  19. ip-address: 127.0.0.1

ElasticSearch

基本概念

Elasticsearch也是基于Lucene的全文检索库,本质也是存储数据,很多概念与MySQL类似的。

Near Realtime(NRT) 近实时。数据提交索引后,立马就可以搜索到。

Cluster 集群,一个集群由一个唯一的名字标识,默认为“elasticsearch”。集群名称非常重要,具有相同集群名的节点才会组成一个集群。集群名称可以在配置文件中指定。

Node 节点:存储集群的数据,参与集群的索引和搜索功能。像集群有名字,节点也有自己的名称,默认在启动时会以一个随机的UUID的前七个字符作为节点的名字,你可以为其指定任意的名字。通过集群名在网络中发现同伴组成集群。一个节点也可是集群。

Index 索引: 一个索引是一个文档的集合(等同于solr中的集合)。每个索引有唯一的名字,通过这个名字来操作它。一个集群中可以有任意多个索引。

Type 类型:指在一个索引中,可以索引不同类型的文档,如用户数据、博客数据。从6.0.0 版本起已废弃,一个索引中只存放一类数据。

Document 文档:被索引的一条数据,索引的基本信息单元,以JSON格式来表示。

Shard 分片:在创建一个索引时可以指定分成多少个分片来存储。每个分片本身也是一个功能完善且独立的“索引”,可以被放置在集群的任意节点上。

Replication 备份: 一个分片可以有多个备份(副本)

es-introduce-1-3.png

操作索引(库)

创建索引

创建索引的请求格式:

  • 请求方式:PUT
  • 请求路径:/索引库名
  • 请求参数:json格式

    1. {
    2. "settings": {
    3. "number_of_shards": 3,
    4. "number_of_replicas": 2
    5. }
    6. }
    • settings:索引库的设置
      • number_of_shards:分片数量
      • number_of_replicas:副本数量(单机情况副本设置为0)

测试:创建一个索引为 heima

  1. PUT http://192.168.32.129:9200/heima
  2. {
  3. "settings": {
  4. "number_of_shards": 3,
  5. "number_of_replicas": 0
  6. }
  7. }

查看索引

可以显示字段和字段类型等…

  1. GET /索引库名GET http://192.168.32.129:9200/heima

查看所以索引库配置

  1. GET http://192.168.32.129:9200/*

3)删除索引库

  1. DELETE /索引库名DELETE http://192.168.32.129:9200/heima

映射配置(表)

什么是映射?

映射是定义文档的过程,文档包含哪些字段,这些字段是否保存,是否索引,是否分词等

创建映射字段

  1. PUT /索引库名/_mapping/类型名称
  2. {
  3. "properties": {
  4. "字段名": {
  5. "type": "类型",
  6. "index": true
  7. "store": true
  8. "analyzer": "分词器"
  9. }
  10. }
  11. }
  • 类型名称:就是前面将的type的概念,类似于数据库中的不同表字段名:任意填写 ,可以指定许多属性,例如:
  • type:类型,可以是text、long、short、date、integer、object等
  • index:是否索引,默认为 true
  • store:是否存储,默认为 false
  • analyzer:分词器,这里的ik_max_word即使用ik分词器

测试:

  1. PUT heima/_mapping/goods
  2. {
  3. "properties": {
  4. "title": {
  5. "type": "text",
  6. "analyzer": "ik_max_word"
  7. },
  8. "images": {
  9. "type": "keyword",
  10. "index": "false"
  11. },
  12. "price": {
  13. "type": "float"
  14. }
  15. }
  16. }

查看映射

  1. GET /索引库名/_mapping
  2. GET /heima/_mapping

响应: 索引(heima),映射(goods)

  1. {
  2. "heima": {
  3. "mappings": {
  4. "goods": {
  5. "properties": {
  6. "images": {
  7. "type": "keyword",
  8. "index": false
  9. },
  10. "price": {
  11. "type": "float"
  12. },
  13. "title": {
  14. "type": "text",
  15. "analyzer": "ik_max_word"
  16. }
  17. }
  18. }
  19. }
  20. }
  21. }

映射字段解释

type

  • String类型,又分两种:
    • text:可分词,不可参与聚合
    • keyword:不可分词,数据会作为完整字段进行匹配,可以参与聚合
  • Numerical:数值类型,分两类
    • 基本数据类型:long、interger、short、byte、double、float、half_float
    • 浮点数的高精度类型:scaled_float
      • 需要指定一个精度因子,比如10或100。elasticsearch会把真实值乘以这个因子后存储,取出时再还原。
  • Date:日期类型
    elasticsearch可以对日期格式化为字符串存储,但是建议我们存储为毫秒值,存储为long,节省空间。

index

index影响字段的索引情况。

  • true:字段会被索引,则可以用来进行搜索。默认值就是true
  • false:字段不会被索引,不能用来搜索

index的默认值就是true,也就是说你不进行任何配置,所有字段都会被索引。

但是有些字段是我们不希望被索引的,比如商品的图片信息,就需要手动设置index为false

store

是否将数据进行额外存储。

在学习lucene和solr时,我们知道如果一个字段的store设置为false,那么在文档列表中就不会有这个字段的值,用户的搜索结果中不会显示出来。

但是在Elasticsearch中,即便store设置为false,也可以搜索到结果。

原因是Elasticsearch在创建文档索引时,会将文档中的原始数据备份,保存到一个叫做_source的属性中。而且我们可以通过过滤_source来选择哪些要显示,哪些不显示。

而如果设置store为true,就会在_source以外额外存储一

boost

激励因子,这个与lucene中一样

其它的不再一一讲解,用的不多,大家参考官方文档:

数据

添加数据

智能判断,没有的字段也能添加数据

随机生成id

通过POST请求,可以向一个已经存在的索引库中添加数据。

语法:

  1. POST /索引库名/类型名
  2. {
  3. "key":"value"
  4. }

示例:

  1. POST /heima/goods/
  2. {
  3. "title":"小米手机",
  4. "images":"http://image.leyou.com/12479122.jpg",
  5. "price":2699.00
  6. }

响应:

  1. {
  2. "_index": "heima",
  3. "_type": "goods",
  4. "_id": "r9c1KGMBIhaxtY5rlRKv",
  5. "_version": 1,
  6. "result": "created",
  7. "_shards": {
  8. "total": 3,
  9. "successful": 1,
  10. "failed": 0
  11. },
  12. "_seq_no": 0,
  13. "_primary_term": 2
  14. }

查看

  1. GET http://192.168.32.129:9200/heima/_search
  2. 或者
  3. GET http://192.168.32.129:9200/heima/_search
  4. {
  5. "query":{
  6. "match_all":{}
  7. }
  8. }

结果

  1. {
  2. "_index": "heima",
  3. "_type": "goods",
  4. "_id": "r9c1KGMBIhaxtY5rlRKv",
  5. "_version": 1,
  6. "_score": 1,
  7. "_source": {
  8. "title": "小米手机",
  9. "images": "http://image.leyou.com/12479122.jpg",
  10. "price": 2699
  11. }
  12. }
  • _source:源文档信息,所有的数据都在里面。
  • _id:这条文档的唯一标示,与文档自己的id字段没有关联

自定义id

  1. POST /索引库名/类型/id值{ ...}

示例:

  1. POST http://192.168.32.129:9200/heima/goods/2
  2. {
  3. "title":"大米手机",
  4. "images":"http://image.leyou.com/12479122.jpg",
  5. "price":2899.00
  6. }

响应:

  1. {
  2. "_index": "heima",
  3. "_type": "goods",
  4. "_id": "2",
  5. "_score": 1,
  6. "_source": {
  7. "title": "大米手机",
  8. "images": "http://image.leyou.com/12479122.jpg",
  9. "price": 2899
  10. }
  11. }

添加数据时,有个智能判断,就是说 添加时使用没有配置映射的字段一样能够成功,它也可以根据你输入的数据来判断类型,动态添加数据映射。

只添加了 字段 title,images,price

  1. POST http://192.168.32.129:9200/heima/goods/3
  2. {
  3. "title":"超米手机",
  4. "images":"http://image.leyou.com/12479122.jpg",
  5. "price":2899.00,
  6. "stock": 200,
  7. "saleable":true
  8. }

额外添加了stock库存,和saleable是否上架两个字段。

来看结果:

  1. {
  2. "_index": "heima",
  3. "_type": "goods",
  4. "_id": "3",
  5. "_version": 1,
  6. "_score": 1,
  7. "_source": {
  8. "title": "超米手机",
  9. "images": "http://image.leyou.com/12479122.jpg",
  10. "price": 2899,
  11. "stock": 200,
  12. "saleable": true
  13. }
  14. }

在看下索引库的映射关系:

  1. {
  2. "heima": {
  3. "mappings": {
  4. "goods": {
  5. "properties": {
  6. "images": {
  7. "type": "keyword",
  8. "index": false
  9. },
  10. "price": {
  11. "type": "float"
  12. },
  13. "saleable": {
  14. "type": "boolean"
  15. },
  16. "stock": {
  17. "type": "long"
  18. },
  19. "title": {
  20. "type": "text",
  21. "analyzer": "ik_max_word"
  22. }
  23. }
  24. }
  25. }
  26. }
  27. }

stock和saleable都被成功映射了。

修改数据

把刚才新增的请求方式改为PUT,就是修改了。不过修改必须指定id,

  • id对应文档存在,则修改
  • id对应文档不存在,则新增

比如,我们把id为3的数据进行修改:

  1. PUT http://192.168.32.129:9200/heima/goods/3
  2. {
  3. "title":"超大米手机",
  4. "images":"http://image.leyou.com/12479122.jpg",
  5. "price":3899.00,
  6. "stock": 100,
  7. "saleable":true
  8. }

结果:

  1. {
  2. "took": 17,
  3. "timed_out": false,
  4. "_shards": {
  5. "total": 9,
  6. "successful": 9,
  7. "skipped": 0,
  8. "failed": 0
  9. },
  10. "hits": {
  11. "total": 1,
  12. "max_score": 1,
  13. "hits": [
  14. {
  15. "_index": "heima",
  16. "_type": "goods",
  17. "_id": "3",
  18. "_score": 1,
  19. "_source": {
  20. "title": "超大米手机",
  21. "images": "http://image.leyou.com/12479122.jpg",
  22. "price": 3899,
  23. "stock": 100,
  24. "saleable": true
  25. }
  26. }
  27. ]
  28. }
  29. }

删除数据

删除使用DELETE请求,同样,需要根据id进行删除:

语法

  1. DELETE /索引库名/类型名/id
  2. DELETE http://192.168.32.129:9200/heima/goods/3

功能

ES6

let 和 const 命令

var

之前,js定义变量只有一个关键字:var
var有一个问题,就是定义的变量有时会莫名奇妙的成为全局变量。
例如这样的一段代码:

  1. for(var i = 0; i < 5; i++){
  2. console.log(i);
  3. }
  4. console.log("循环外:" + i)

你猜下打印的结果是什么?
1529376275020.png

let

let所声明的变量,只在let命令所在的代码块内有效。

我们把刚才的var改成let试试:

  1. for(let i = 0; i < 5; i++){ console.log(i);}console.log("循环外:" + i)

结果:
1529395660265.png

const

const声明的变量是常量,不能被修改

1529420270814.png

字符串扩展

新的API

ES6为字符串扩展了几个新的API:

  • includes():返回布尔值,表示是否找到了参数字符串。
  • startsWith():返回布尔值,表示参数字符串是否在原字符串的头部。
  • endsWith():返回布尔值,表示参数字符串是否在原字符串的尾部。

实验一下:

1526107640349.png

字符串模板

ES6中提供了`来作为字符串模板标记。我们可以这么玩:

1526108070980.png

在两个`之间的部分都会被作为字符串的值,不管你任意换行,甚至加入js脚本

解构表达式

数组解构

比如有一个数组:

  1. let arr = [1,2,3]

我想获取其中的值,只能通过角标。ES6可以这样:

  1. const [x,y,z] = arr;// x,y,z将与arr中的每个位置对应来取值
  2. // 然后打印
  3. console.log(x,y,z);

结果:

1526109778368.png

对象解构

例如有个person对象:

  1. const person = {
  2. name:"jack",
  3. age:21,
  4. language: ['java','js','css']
  5. }

我们可以这么做:

  1. // 解构表达式获取值
  2. const {name,age,language} = person;
  3. // 打印
  4. console.log(name);
  5. console.log(age);
  6. console.log(language);

结果:

1526109984544.png

如过想要用其它变量接收,需要额外指定别名:

1526110159450.png

{name:n}:name是person中的属性名,冒号后面的n是解构后要赋值给的变量。

函数优化

函数参数默认值

在ES6以前,我们无法给一个函数参数设置默认值,只能采用变通写法:

  1. function add(a , b) {
  2. // 判断b是否为空,为空就给默认值1
  3. b = b || 1;
  4. return a + b;
  5. }
  6. // 传一个参数
  7. console.log(add(10));

现在可以这么写:

  1. function add(a , b = 1) {
  2. return a + b;
  3. }
  4. // 传一个参数
  5. console.log(add(10));

箭头函数

ES6中定义函数的简写方式:

一个参数时:

  1. var print = function (obj) {
  2. console.log(obj);
  3. }
  4. // 简写为:
  5. var print2 = obj => console.log(obj);

多个参数:

  1. // 两个参数的情况:
  2. var sum = function (a , b) {
  3. return a + b;
  4. }
  5. // 简写为:
  6. var sum2 = (a,b) => a+b;

代码不止一行,可以用{}括起来

  1. var sum3 = (a,b) => {
  2. return a + b;
  3. }

对象的函数属性简写

比如一个Person对象,里面有eat方法:

  1. let person = {
  2. name: "jack",
  3. // 以前:
  4. eat: function (food) {
  5. console.log(this.name + "在吃" + food);
  6. },
  7. // 箭头函数版:
  8. eat2: food => console.log(person.name + "在吃" + food),// 这里拿不到this
  9. // 简写版:
  10. eat3(food){
  11. console.log(this.name + "在吃" + food);
  12. }
  13. }

箭头函数结合解构表达式

比如有一个函数:

  1. const person = {
  2. name:"jack",
  3. age:21,
  4. language: ['java','js','css']
  5. }
  6. function hello(person) {
  7. console.log("hello," + person.name)
  8. }

如果用箭头函数和解构表达式

  1. var hi = ({name}) => console.log("hello," + name);

map和reduce

数组中新增了map和reduce方法。

map

map():接收一个函数,将原数组中的所有元素用这个函数处理后放入新数组返回。

举例:有一个字符串数组,我们希望转为int数组

  1. let arr = ['1','20','-5','3'];
  2. console.log(arr)
  3. arr = arr.map(s => parseInt(s));
  4. console.log(arr)

1526110796839.png

reduce

reduce():接收一个函数(必须)和一个初始值(可选)。

第一个参数(函数)接收两个参数:

  • 第一个参数是上一次reduce处理的结果
  • 第二个参数是数组中要处理的下一个元素

reduce()会从左到右依次把数组中的元素用reduce处理,并把处理的结果作为下次reduce的第一个参数。如果是第一次,会把前两个元素作为计算参数,或者把用户指定的初始值作为起始参数

举例:

  1. const arr = [1,20,-5,3]

没有初始值:

1526111537204.png

指定初始值:

1526111580742.png

对象扩展

ES6给Object拓展了许多新的方法,如:

  • keys(obj):获取对象的所有key形成的数组
  • values(obj):获取对象的所有value形成的数组
  • entries(obj):获取对象的所有key和value形成的二维数组。格式:[[k1,v1],[k2,v2],...]
  • assign(dest, …src) :将多个src对象的值 拷贝到 dest中(浅拷贝)。

1527210872966.png

数组扩展

ES6给数组新增了许多方法:

  • find(callback):数组实例的find方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true的成员,然后返回该成员。如果没有符合条件的成员,则返回undefined。
  • findIndex(callback):数组实例的findIndex方法的用法与find方法非常类似,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1。
  • includes(数组元素):与find类似,如果匹配到元素,则返回true,代表找到了。

Vue简单分页

v-for="i in Math.min(5,totalPage)",遍历5个页码,总页数小于5就遍历总页数

index(i):计算页码

search.page = index(i):点击后把页码赋值给当前页码

  1. <div class="fr">
  2. <div class="sui-pagination pagination-large">
  3. <ul>
  4. <li :class="{prev:true,disabled:search.page===1}">
  5. <a href="#" @click.prevent.stop="prePage()">«上一页</a>
  6. </li>
  7. <li :class="{active:index(i)===search.page}" v-for="i in Math.min(5,totalPage)">
  8. <a href="#" @click.prevent.stop="search.page = index(i)" v-text="index(i)"></a>
  9. </li>
  10. <li class="dotted" v-show="totalPage-search.page>2 && totalPage> 5"><span>...</span></li>
  11. <li :class="{next:true,disabled:search.page===totalPage}">
  12. <a href="#"
  13. @click.prevent.stop="nextPage()">下一页»</a>
  14. </li>
  15. </ul>
  16. <div><span>共{{totalPage}}页&nbsp; </span><span>
  17. 到第
  18. <input type="text" class="page-num">
  19. <button class="page-confirm" onclick="alert(1)">确定</button></span></div>
  20. </div>
  21. </div>

vue

prePage():上一页

nextPage():下一页

index(i)():下一页

search:里面包含,当前页(search.page)和搜索关键字(search.key,是从头部传递过来的)

输入关键字一搜索,跳转到search.html(window.location = 'search.html?key=' + this.key;),search.html一来构建search对象加载

  1. const search = ly.parse(location.search.substring(1))
  2. search.page = parseInt(search.page) || 1
  3. this.search = search

ly是封装的

  1. var vm = new Vue({
  2. el: "#searchApp",
  3. data: {
  4. ly,
  5. search: {},
  6. total: 0,
  7. totalPage: 0,
  8. goodsList: [],
  9. filters: [],
  10. showMore: false
  11. },
  12. components: {
  13. lyTop: () => import("./js/pages/top.js")
  14. },
  15. // 这是为了刷新页面时,继续显示刷新前的页面数据
  16. watch: {
  17. search: {
  18. deep: true,
  19. handler(val, oldVal) {
  20. // 第一次加载search的时候里面是空的,就不加载 (如果search为空 或者create在初始化就返回)
  21. if (!oldVal || !oldVal.key) {
  22. return;
  23. }
  24. location.search = "?" + ly.stringify(this.search) // location.search 发生改变 页面会自动 刷新浏览器
  25. }
  26. }
  27. },
  28. created() {
  29. const search = ly.parse(location.search.substring(1))
  30. search.page = parseInt(search.page) || 1
  31. this.search = search
  32. this.loadDate();
  33. },
  34. methods: {
  35. loadDate() {
  36. ly.http.post("/search/page", this.search).then(resp => {
  37. this.total = resp.data.total
  38. this.totalPage = resp.data.totalPage
  39. resp.data.items.forEach(goods => {
  40. goods.skus = JSON.parse(goods.skus)
  41. goods.selectedSku = goods.skus[0] //提供选择
  42. });
  43. this.goodsList = resp.data.items
  44. //分类
  45. this.filters.push({
  46. key: 'cid3',
  47. options: resp.data.categories
  48. })
  49. //品牌
  50. this.filters.push({
  51. key: 'brandId',
  52. options: resp.data.brands
  53. })
  54. //规格参数
  55. resp.data.specs.forEach(spec => {
  56. this.filters.push(spec)
  57. })
  58. }).catch(error => {
  59. console.log(error)
  60. })
  61. },
  62. //-------------------------------
  63. prePage() {
  64. //当前页大于1
  65. if (this.search.page > 1) this.search.page--
  66. },
  67. nextPage() {
  68. //当前页小于总页数
  69. if (this.search.page < this.totalPage) this.search.page++
  70. },
  71. //计算页码
  72. index(i) {
  73. if (this.search.page <= 3 || this.totalPage <= 5) { // 页首 [123]45
  74. return i
  75. } else if (this.search.page >= this.totalPage - 2) { // 页尾 67 [8 9 10]
  76. return i + this.totalPage - 5
  77. } else {
  78. return i + this.search.page - 3 // 页中 23456
  79. }
  80. }
  81. },
  82. });

RabbitMQ

为什么要用它:发送消息!商品信息发生增删改通知search服务和page服务操作,还有发送短信

添加,修改商品发送通知

  • 添加(添加对应的index、添加对应的网页),修改(修改对应的index、根据spuId文件后,创建新的文件)

删除商品

  • 删除索引和page页面

saveGoods

发送消息

  1. amqpTemplate.convertAndSend("item.insert",spu.getId());

page服务匹配 item.update,item.insert

  1. @Component
  2. public class PageListener {
  3. @Autowired
  4. private PageService pageService;
  5. @RabbitListener(
  6. bindings = @QueueBinding(
  7. value = @Queue(value = "page.item.insert.queue", durable = "true"),
  8. exchange = @Exchange(value = "ly.item.exchange",type = ExchangeTypes.TOPIC),
  9. key = {"item.update","item.insert"}
  10. )
  11. )
  12. public void insertAndUpdatePage(Long spuId){
  13. if(spuId==null){
  14. return;
  15. }
  16. pageService.createHtml(spuId);
  17. }
  18. }

Search服务 同样匹配item.update,item.insert

  1. @Component
  2. public class GoodsListener {
  3. @Autowired
  4. private SearchService searchService;
  5. @Autowired
  6. private GoodsRepository goodsRepository;
  7. @RabbitListener(
  8. bindings = @QueueBinding(
  9. value = @Queue(value = "index.item.insert.queue", durable = "true"),
  10. exchange = @Exchange(value = "ly.item.exchange",type = ExchangeTypes.TOPIC),
  11. key = {"item.update","item.insert"}
  12. )
  13. )
  14. public void insertAndUpdateIndex(Long spuId){
  15. if(spuId==null){
  16. return;
  17. }
  18. Spu spu = searchService.querySpuById(spuId);
  19. Goods goods = searchService.buildGoods(spu);
  20. goodsRepository.save(goods);
  21. }
  22. }

Nginx

本地需要配置hosts,把域名请求转发到 本地

1526016663674.png

反向代理配置

示例:

1526188831504.png

nginx 中的每个server就是一个反向代理配置,可以有多个server

  1. server {
  2. listen 80;
  3. server_name 192.168.32.129;
  4. location / {
  5. root /leyou/static/; # 访问这个目录下的
  6. index index.html index.htm; # 默认访问页面
  7. }
  8. }

配置上传服务绕过网关

  1. server {
  2. listen 80;
  3. server_name api.leyou.com;
  4. proxy_set_header X-Forwarded-Host $host;
  5. proxy_set_header X-Forwarded-Server $host;
  6. proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  7. proxy_set_header Host $host; # 转发后还是携带自身的hostapi.leyou.com),而不要用本机ip30.40.65.40
  8. # 上传路径的映射
  9. location /api/upload {
  10. proxy_pass http://192.168.1.103:8082;
  11. proxy_connect_timeout 600;
  12. proxy_read_timeout 600;
  13. rewrite "^/api/(.*)$" /$1 break; # $1就是匹配前面的一组(小括号)
  14. }
  15. location / {
  16. proxy_pass http://127.0.0.1:10010;
  17. proxy_connect_timeout 600;
  18. proxy_read_timeout 600;
  19. }
  20. }
  • 首先,我们映射路径是/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:重写路径结束后,将得到的路径重新进行一次路径匹配
      • break:重写路径结束后,不再重新匹配路径。

我们这里不能选择last,否则以新的路径/upload/image来匹配,就不会被正确的匹配到8082端口了

修改完成,输入nginx -s reload命令重新加载配置。

nginx代理静态页面

  1. server {
  2. listen 80;
  3. server_name www.leyou.com;
  4. proxy_set_header X-Forwarded-Host $host;
  5. proxy_set_header X-Forwarded-Server $host;
  6. proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  7. location /item {
  8. # 先找本地
  9. root html;
  10. if (!-f $request_filename) { #请求的文件不存在,就反向代理
  11. proxy_pass http://127.0.0.1:8084;
  12. break;
  13. }
  14. }
  15. location / {
  16. proxy_pass http://127.0.0.1:9002;
  17. proxy_connect_timeout 600;
  18. proxy_read_timeout 600;
  19. }
  20. }

完整配置

  1. # user nginx;
  2. worker_processes 1;
  3. events {
  4. worker_connections 1024;
  5. }
  6. http {
  7. include mime.types;
  8. default_type application/octet-stream;
  9. client_max_body_size 100M;
  10. #log_format main '$remote_addr - $remote_user [$time_local] "$request" '
  11. # '$status $body_bytes_sent "$http_referer" '
  12. # '"$http_user_agent" "$http_x_forwarded_for"';
  13. #access_log logs/access.log main;
  14. sendfile on;
  15. #tcp_nopush on;
  16. #keepalive_timeout 0;
  17. keepalive_timeout 65;
  18. #gzip on;
  19. # 前端:管理后台
  20. server {
  21. listen 80;
  22. server_name manage.leyou.com;
  23. proxy_set_header X-Forwarded-Host $host;
  24. proxy_set_header X-Forwarded-Server $host;
  25. proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  26. location / {
  27. proxy_pass http://192.168.1.103:9001;
  28. proxy_connect_timeout 600;
  29. proxy_read_timeout 600;
  30. }
  31. }
  32. # 前端:前台页面
  33. server {
  34. listen 80;
  35. server_name www.leyou.com;
  36. proxy_set_header X-Forwarded-Host $host;
  37. proxy_set_header X-Forwarded-Server $host;
  38. proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  39. # location /item {
  40. # proxy_pass http://192.168.1.103:8084;
  41. # proxy_connect_timeout 600;
  42. # proxy_read_timeout 600;
  43. # }
  44. # 先找本地
  45. location /item {
  46. root html;
  47. if (!-f $request_filename) { #请求的文件不存在,就反向代理
  48. proxy_pass http://192.168.1.103:8084;
  49. break;
  50. }
  51. }
  52. location / {
  53. proxy_pass http://192.168.1.103:9002;
  54. proxy_connect_timeout 600;
  55. proxy_read_timeout 600;
  56. }
  57. }
  58. server {
  59. listen 80;
  60. server_name manage.leyou.com;
  61. location / {
  62. proxy_pass http://192.168.1.103:9001;
  63. }
  64. }
  65. # 后端接口
  66. server {
  67. listen 80;
  68. server_name api.leyou.com;
  69. proxy_set_header X-Forwarded-Host $host;
  70. proxy_set_header X-Forwarded-Server $host;
  71. proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  72. proxy_set_header Host $host; # 转发后还是携带自身的host(api.leyou.com),而不要用本机ip(30.40.65.40)
  73. # 上传
  74. location /api/upload {
  75. proxy_pass http://192.168.1.103:10010;
  76. proxy_connect_timeout 600;
  77. proxy_read_timeout 600;
  78. rewrite "^/(.*)$" /zuul/$1 break;
  79. }
  80. # api入口(zuul网关)
  81. location / {
  82. proxy_pass http://192.168.1.103:10010;
  83. proxy_connect_timeout 600;
  84. proxy_read_timeout 600;
  85. }
  86. }
  87. # 图片服务器
  88. server {
  89. listen 80;
  90. server_name image.leyou.com;
  91. # 监听域名中带有group的,交给FastDFS模块处理
  92. location ~/group([0-9])/ {
  93. ngx_fastdfs_module;
  94. }
  95. # 将其它图片代理指向本地的/leyou/static目录,/leyou/static已经挂载到fastdfs内部
  96. location / {
  97. root /leyou/static/;
  98. }
  99. error_page 500 502 503 504 /50x.html;
  100. location = /50x.html {
  101. root html;
  102. }
  103. }
  104. server {
  105. listen 80;
  106. server_name 192.168.32.129;
  107. location / {
  108. root /leyou/static/;
  109. index index.html index.htm;
  110. }
  111. }
  112. }

fastDFS

  1. <!-- fastDFS客户端-->
  2. <dependency>
  3. <groupId>com.github.tobato</groupId>
  4. <artifactId>fastdfs-client</artifactId>
  5. <version>1.26.2</version>
  6. </dependency>
  7. <!-- 加载yml中的配置使用-->
  8. <dependency>
  9. <groupId>org.springframework.boot</groupId>
  10. <artifactId>spring-boot-configuration-processor</artifactId>
  11. <optional>true</optional>
  12. </dependency

添加一个配置文件,解决jmx重复注册bean的问题

  1. @Configuration
  2. @Import(FdfsClientConfig.class)
  3. // 解决jmx重复注册bean的问题
  4. @EnableMBeanExport(registration = RegistrationPolicy.IGNORE_EXISTING)
  5. public class FastClientImporter {
  6. }

测试类

  1. @RunWith(SpringRunner.class)
  2. @SpringBootTest
  3. public class FdfsTest {
  4. @Autowired
  5. private FastFileStorageClient storageClient;
  6. @Autowired
  7. private ThumbImageConfig thumbImageConfig;
  8. // 上传文件
  9. @Test
  10. public void testUpload() throws FileNotFoundException {
  11. File file = new File("G:\\image\\simpledesktops.png");
  12. // 上传并且生成缩略图
  13. StorePath storePath = this.storageClient.uploadFile(
  14. new FileInputStream(file), file.length(), "jpg", null);
  15. // 带分组的路径
  16. System.out.println(storePath.getFullPath());
  17. // 不带分组的路径
  18. System.out.println(storePath.getPath());
  19. System.out.println("========================");
  20. System.out.println("========================");
  21. }
  22. // 上传文件并获取缩略图
  23. @Test
  24. public void testUploadAndCreateThumb() throws FileNotFoundException {
  25. File file = new File("G:\\image\\simpledesktops.png");
  26. // 上传并且生成缩略图
  27. StorePath storePath = this.storageClient.uploadImageAndCrtThumbImage(
  28. new FileInputStream(file), file.length(), "png", null);
  29. // 带分组的路径
  30. System.out.println(storePath.getFullPath());
  31. // 不带分组的路径
  32. System.out.println(storePath.getPath());
  33. // 获取缩略图路径
  34. String path = thumbImageConfig.getThumbImagePath(storePath.getPath());
  35. System.out.println(path);
  36. }
  37. }

application.yml 添加

  1. fdfs:
  2. so-timeout: 1501
  3. connect-timeout: 601
  4. thumb-image: # 缩略图
  5. width: 60
  6. height: 60
  7. tracker-list: # tracker地址
  8. - 192.168.32.129:22122
  9. ly:
  10. upload:
  11. baseUrl: http://image.leyou.com/
  12. allowTypes:
  13. - image/png
  14. - image/jpeg
  15. - image/bmp
  16. - image/gif

读取配置

  1. @Component
  2. @ConfigurationProperties(prefix = "ly.upload")
  3. @Data
  4. public class UploadProperties {
  5. private String baseUrl;
  6. private List<String> allowTypes;
  7. }

上传服务

  1. package com.leyou.upload.service;
  2. import com.github.tobato.fastdfs.domain.StorePath;
  3. import com.github.tobato.fastdfs.service.FastFileStorageClient;
  4. import com.leyou.enums.ExceptionEnum;
  5. import com.leyou.exception.LyException;
  6. import com.leyou.upload.config.UploadProperties;
  7. import lombok.extern.slf4j.Slf4j;
  8. import org.apache.commons.lang3.StringUtils;
  9. import org.springframework.beans.factory.annotation.Autowired;
  10. import org.springframework.boot.context.properties.EnableConfigurationProperties;
  11. import org.springframework.stereotype.Service;
  12. import org.springframework.web.multipart.MultipartFile;
  13. import javax.imageio.ImageIO;
  14. import java.awt.image.BufferedImage;
  15. import java.io.File;
  16. import java.io.IOException;
  17. import java.util.Arrays;
  18. import java.util.List;
  19. /**
  20. * @author chy
  21. * @since 2021-05-12 17:48
  22. */
  23. @Slf4j
  24. @Service
  25. //@EnableConfigurationProperties(UploadService.class)
  26. public class UploadService {
  27. @Autowired
  28. private FastFileStorageClient storageClient;
  29. // 自己编写的上传文件配置 包含上传地址 和 允许类型
  30. @Autowired
  31. private UploadProperties uploadProperties;
  32. public String uploadImage(MultipartFile file) {
  33. try {
  34. //校验是否包含文件类型
  35. if (!uploadProperties.getAllowTypes().contains(file.getContentType())) {
  36. throw new LyException(ExceptionEnum.INVALID_FILE_TYPE);
  37. }
  38. //校验文件内容
  39. BufferedImage read = ImageIO.read(file.getInputStream());
  40. if (read == null) {
  41. throw new LyException(ExceptionEnum.INVALID_FILE_TYPE);
  42. }
  43. //校验通过 上传到 fastDFS
  44. //文件后缀
  45. String suffix = StringUtils.substringAfterLast(file.getOriginalFilename(), ".");
  46. StorePath storePath = storageClient.uploadFile(file.getInputStream(), file.getSize(), suffix, null);
  47. log.info("上传到的地址:{}", uploadProperties.getBaseUrl() + storePath.getFullPath());
  48. return uploadProperties.getBaseUrl() + storePath.getFullPath();
  49. } catch (Exception e) {
  50. log.error("【文件上传】文件上传失败:{}", e);
  51. throw new LyException(ExceptionEnum.INVALID_FILE_TYPE);
  52. }
  53. }
  54. }

通用Mapper使用PageHelper

通用Mapper和PageHelper pom

  1. <dependency>
  2. <groupId>tk.mybatis</groupId>
  3. <artifactId>mapper-spring-boot-starter</artifactId>
  4. </dependency>
  5. <dependency>
  6. <groupId>com.github.pagehelper</groupId>
  7. <artifactId>pagehelper-spring-boot-starter</artifactId>
  8. </dependency>

启动类添加

  1. //import tk.mybatis.spring.annotation.MapperScan
  2. @MapperScan("com.leyou.item.mapper") ;

查询代码

  1. @Autowired
  2. private BrandMapper brandMapper;
  3. // page: 当前页码
  4. // rows: 显示大小
  5. // sortBy: 排序字段
  6. // desc: 是否倒序
  7. // key: 搜索关键字
  8. public PageResult<Brand> queryBrandByPage(Integer page, Integer rows, String sortBy , Boolean desc, String key) {
  9. //分页
  10. PageHelper.startPage(page, rows);
  11. /**
  12. * 过滤
  13. * where `name` like '%x%' or letter =='x'
  14. * order by id desc
  15. */
  16. Example example = new Example(Brand.class);
  17. if(StringUtils.isNoneBlank(key)){
  18. example.createCriteria().andLike("name", "%" + key + "%")
  19. .orEqualTo("letter",key.toUpperCase());
  20. }
  21. //排序
  22. if(StringUtils.isNoneBlank(sortBy)){
  23. example.setOrderByClause(sortBy + (desc ? " DESC":" ASC"));
  24. }
  25. List<Brand> brands = brandMapper.selectByExample(example);
  26. if(CollectionUtils.isEmpty(brands)){
  27. throw new LyException(ExceptionEnum.BRAND_NO_FOUND);
  28. }
  29. PageInfo<Brand> pageInfo = new PageInfo<>(brands);
  30. return new PageResult<Brand>(pageInfo.getTotal(),pageInfo.getList());
  31. }

解决跨域

  1. @Configuration
  2. public class GateWayCorsConfig {
  3. @Bean
  4. public CorsFilter corsFilter() {
  5. //1.添加CORS配置信息
  6. CorsConfiguration config = new CorsConfiguration();
  7. //1) 允许的域,不要写*,否则cookie就无法使用了
  8. config.addAllowedOrigin("http://www.leyou.com");
  9. //2) 是否发送Cookie信息
  10. config.setAllowCredentials(true);
  11. //3) 允许的请求方式
  12. config.addAllowedMethod("OPTIONS");
  13. config.addAllowedMethod("HEAD");
  14. config.addAllowedMethod("GET");
  15. config.addAllowedMethod("PUT");
  16. config.addAllowedMethod("POST");
  17. config.addAllowedMethod("DELETE");
  18. config.addAllowedMethod("PATCH");
  19. // 4)允许的头信息
  20. config.addAllowedHeader("*");
  21. // 5)有效时长
  22. config.setMaxAge(3600L);
  23. //2.添加映射路径,我们拦截一切请求
  24. UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource();
  25. configSource.registerCorsConfiguration("/**", config);
  26. //3.返回新的CorsFilter.
  27. return new CorsFilter(configSource);
  28. }
  29. }

页面静态化

就是直接产生html页面,然后把页面放在nginx下面

  1. @Autowired
  2. private TemplateEngine templateEngine;
  3. public void createHtml(Long spuId){
  4. //上下文
  5. Context context = new Context();
  6. //数据
  7. context.setVariables(loadModel(spuId));
  8. //输出流
  9. File dest = new File("G:\\java\\SpringCloud\\html",spuId+ ".html");
  10. if(dest.exists()){
  11. dest.delete();
  12. }
  13. try( PrintWriter writer = new PrintWriter(dest,"UTF-8")) {
  14. templateEngine.process("item",context,writer);
  15. } catch (Exception e) {
  16. log.info("【静态页服务】文生成静态页异常!{}",e);
  17. e.printStackTrace();
  18. }
  19. }
  20. public void deleteHtml(Long spuId){
  21. File dest = new File("G:\\java\\SpringCloud\\html",spuId+ ".html");
  22. if(dest.exists()){
  23. dest.delete();
  24. }
  25. }

Cookie相关

前台界面,访问需要用户信息的服务时(如购物车),每次请求之前都需要先发送一次校验请求,目的是刷新token

解决host地址的变化

那么问题来了:为什么我们这里的请求serverName变成了:127.0.0.1:8087呢?

这里的server name其实就是请求时的主机名:Host,之所以改变,有两个原因:

  • 我们使用了nginx反向代理,当监听到api.leyou.com的时候,会自动将请求转发至127.0.0.1:10010,即Zuul。
  • 而后请求到达我们的网关Zuul,Zuul就会根据路径匹配,我们的请求是/api/auth,根据规则被转发到了 127.0.0.1:8087 ,即我们的授权中心。

我们首先去更改nginx配置,让它不要修改我们的host:proxy_set_header Host $host;

1533303544219.png

zuul配置add-host-header: true sensitive-headers:无内容

  1. zuul:
  2. prefix: /api #添加路由前缀
  3. routes:
  4. item-service: /item/** # item模块的访问都需要添加 /item
  5. search-service: /search/** # 搜索微服务
  6. user-service: /user/**
  7. auth-service: /auth/**
  8. add-host-header: true #添加host头信息
  9. sensitive-headers: #配置禁止使用的头信息,这里设置为null,否则set-cookie无效

登录成功返回token,写入到cookie,设置cookie有效期cookie.setMaxAge();

主要是这个两个

  • cookie.setDomain("leyou.com");
  • cookie.setPath("/");

以后每次访问前,都需要发送一个请求来刷新token

品牌分类

商城的核心自然是商品,而商品多了以后,肯定要进行分类,并且不同的商品会有不同的品牌信息,其关系如图所示:

1525999005260.png

  • 一个商分类下有很多商品
  • 一个商品分类下有很多品牌
  • 而一个品牌,可能属于不同的分类
  • 一个品牌下也会有很多商品

因此,我们需要依次去完成:商品分类、品牌、商品的开发。

商品规格参数

表结构

我们看下规格参数的格式:

1526092179381.png

可以看到规格参数是分组的,每一组都有多个参数键值对。不过对于规格参数的模板而言,其值现在是不确定的,不同的商品值肯定不同,模板中只要保存组信息、组内参数信息即可。

因此我们设计了两张表:

  • tb_spec_group:组,与商品分类关联
  • tb_spec_param:参数名,与组关联,一对多

规格组

规格参数分组表:tb_spec_group

  1. CREATE TABLE `tb_spec_group` (
  2. `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  3. `cid` bigint(20) NOT NULL COMMENT '商品分类id,一个分类下有多个规格组',
  4. `name` varchar(50) NOT NULL COMMENT '规格组的名称',
  5. PRIMARY KEY (`id`),
  6. KEY `key_category` (`cid`)
  7. ) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8 COMMENT='规格参数的分组表,每个商品分类下有多个规格参数组';

规格组有3个字段:

  • id:主键
  • cid:商品分类id,一个分类下有多个模板
  • name:该规格组的名称。

规格参数

规格参数表:tb_spec_param

  1. CREATE TABLE `tb_spec_param` (
  2. `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  3. `cid` bigint(20) NOT NULL COMMENT '商品分类id',
  4. `group_id` bigint(20) NOT NULL,
  5. `name` varchar(255) NOT NULL COMMENT '参数名',
  6. `numeric` tinyint(1) NOT NULL COMMENT '是否是数字类型参数,true或false',
  7. `unit` varchar(255) DEFAULT '' COMMENT '数字类型参数的单位,非数字类型可以为空',
  8. `generic` tinyint(1) NOT NULL COMMENT '是否是sku通用属性,true或false',
  9. `searching` tinyint(1) NOT NULL COMMENT '是否用于搜索过滤,true或false',
  10. `segments` varchar(1000) DEFAULT '' COMMENT '数值类型参数,如果需要搜索,则添加分段间隔值,如CPU频率间隔:0.5-1.0',
  11. PRIMARY KEY (`id`),
  12. KEY `key_group` (`group_id`),
  13. KEY `key_category` (`cid`)
  14. ) ENGINE=InnoDB AUTO_INCREMENT=24 DEFAULT CHARSET=utf8 COMMENT='规格参数组下的参数名';

按道理来说,我们的规格参数就只需要记录参数名、组id、商品分类id即可。但是这里却多出了很多字段,为什么?

还记得我们之前的分析吧,规格参数中有一部分是 SKU的通用属性,一部分是SKU的特有属性,而且其中会有一些将来用作搜索过滤,这些信息都需要标记出来。

通用属性

用一个布尔类型字段来标记是否为通用:

  • generic来标记是否为通用属性:
    • true:代表通用属性
    • false:代表sku特有属性

搜索过滤

与搜索相关的有两个字段:

  • searching:标记是否用作过滤
    • true:用于过滤搜索
    • false:不用于过滤
  • segments:某些数值类型的参数,在搜索时需要按区间划分,这里提前确定好划分区间
    • 比如电池容量,0~2000mAh,2000mAh 3000mAh,3000mAh~4000mAh

数值类型

某些规格参数可能为数值类型,这样的数据才需要划分区间,我们有两个字段来描述:

  • numberic:是否为数值类型
    • true:数值类型
    • false:不是数值类型
  • unit:参数的单位

SPU和SKU数据结构(重点)

规格确定以后,就可以添加商品了,先看下数据库表

SPU表

SPU表:

  1. CREATE TABLE `tb_spu` (
  2. `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'spu id',
  3. `title` varchar(255) NOT NULL DEFAULT '' COMMENT '标题',
  4. `sub_title` varchar(255) DEFAULT '' COMMENT '子标题',
  5. `cid1` bigint(20) NOT NULL COMMENT '1级类目id',
  6. `cid2` bigint(20) NOT NULL COMMENT '2级类目id',
  7. `cid3` bigint(20) NOT NULL COMMENT '3级类目id',
  8. `brand_id` bigint(20) NOT NULL COMMENT '商品所属品牌id',
  9. `saleable` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否上架,0下架,1上架',
  10. `valid` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否有效,0已删除,1有效',
  11. `create_time` datetime DEFAULT NULL COMMENT '添加时间',
  12. `last_update_time` datetime DEFAULT NULL COMMENT '最后修改时间',
  13. PRIMARY KEY (`id`)
  14. ) ENGINE=InnoDB AUTO_INCREMENT=208 DEFAULT CHARSET=utf8 COMMENT='spu表,该表描述的是一个抽象的商品,比如 iphone8';

与我们前面分析的基本类似,但是似乎少了一些字段,比如商品描述。

我们做了表的垂直拆分,将SPU的详情放到了另一张表:tb_spu_detail

  1. CREATE TABLE `tb_spu_detail` (
  2. `spu_id` bigint(20) NOT NULL,
  3. `description` text COMMENT '商品描述信息',
  4. `generic_spec` varchar(10000) NOT NULL DEFAULT '' COMMENT '通用规格参数数据',
  5. `special_spec` varchar(1000) NOT NULL COMMENT '特有规格参数及可选值信息,json格式',
  6. `packing_list` varchar(3000) DEFAULT '' COMMENT '包装清单',
  7. `after_service` varchar(3000) DEFAULT '' COMMENT '售后服务',
  8. PRIMARY KEY (`spu_id`)
  9. ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

这张表中的数据都比较大,为了不影响主表的查询效率我们拆分出这张表。

需要注意的是这两个字段:generic_spec和special_spec。

前面讲过规格参数与商品分类绑定,一个分类下的所有SPU具有类似的规格参数。SPU下的SKU可能会有不同的规格参数信息,因此我们计划是这样:

  • SPUDetail中保存通用的规格参数信息。
  • SKU中保存特有规格参数。

来看下我们的表如何存储这些信息。

generic_spec字段

首先是generic_spec,其中保存通用规格参数信息的值,这里为了方便查询,使用了json格式:

整体来看:

1529554390912.png

json结构,其中都是键值对:

  • key:对应的规格参数的spec_param的id
  • value:对应规格参数的值

special_spec字段

注:为了搜索完成后显示

我们说spu中只保存通用规格参数,那么为什么有多出了一个special_spec字段呢?

以手机为例,品牌、操作系统等肯定是全局通用属性,内存、颜色等肯定是特有属性。

当你确定了一个SPU,比如小米的:红米4X

全局属性值都是固定的了:

  1. 品牌:小米
  2. 型号:红米4X

特有属性举例:

  1. 颜色:[香槟金, 樱花粉, 磨砂黑]
  2. 内存:[2G, 3G]
  3. 机身存储:[16GB, 32GB]

颜色、内存、机身存储,作为SKU特有属性,key虽然一样,但是SPU下的每一个SKU,其值都不一样,所以值会有很多,形成数组。

我们在SPU中,会把特有属性的所有值都记录下来,形成一个数组:

里面又有哪些内容呢?

来看数据格式:

1529554916252.png

也是json结构:

  • key:规格参数id
  • value:spu属性的数组

那么问题来:特有规格参数应该在sku中记录才对,为什么在spu中也要记录一份?

因为我们有时候需要把所有规格参数都查询出来,而不是只查询1个sku的属性。比如,商品详情页展示可选的规格参数时:

1526267828817.png

刚好符合我们的结构,这样页面渲染就非常方便了。

SKU表

  1. CREATE TABLE `tb_sku` (
  2. `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'sku id',
  3. `spu_id` bigint(20) NOT NULL COMMENT 'spu id',
  4. `title` varchar(255) NOT NULL COMMENT '商品标题',
  5. `images` varchar(1000) DEFAULT '' COMMENT '商品的图片,多个图片以‘,’分割',
  6. `price` bigint(15) NOT NULL DEFAULT '0' COMMENT '销售价格,单位为分',
  7. `indexes` varchar(100) COMMENT '特有规格属性在spu属性模板中的对应下标组合',
  8. `own_spec` varchar(1000) COMMENT 'sku的特有规格参数,json格式',
  9. `enable` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否有效,0无效,1有效',
  10. `create_time` datetime NOT NULL COMMENT '添加时间',
  11. `last_update_time` datetime NOT NULL COMMENT '最后修改时间',
  12. PRIMARY KEY (`id`),
  13. KEY `key_spu_id` (`spu_id`) USING BTREE
  14. ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='sku表,该表表示具体的商品实体,如黑色的64GB的iphone 8';

还有一张表,代表库存:

  1. CREATE TABLE `tb_stock` (
  2. `sku_id` bigint(20) NOT NULL COMMENT '库存对应的商品sku id',
  3. `seckill_stock` int(9) DEFAULT '0' COMMENT '可秒杀库存',
  4. `seckill_total` int(9) DEFAULT '0' COMMENT '秒杀总数量',
  5. `stock` int(9) NOT NULL COMMENT '库存数量',
  6. PRIMARY KEY (`sku_id`)
  7. ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='库存表,代表库存,秒杀库存等信息';

问题:为什么要将库存独立一张表?

因为库存字段写频率较高,而SKU的其它字段以读为主,因此我们将两张表分离,读写不会干扰。

特别需要注意的是sku表中的indexes字段和own_spec字段。sku中应该保存特有规格参数的值,就在这两个字段中。

indexes字段

注:就只是为了选择时候定位

在SPU表中,已经对特有规格参数及可选项进行了保存,结构如下:

  1. {
  2. "4": [
  3. "香槟金",
  4. "樱花粉",
  5. "磨砂黑"
  6. ],
  7. "12": [
  8. "2GB",
  9. "3GB"
  10. ],
  11. "13": [
  12. "16GB",
  13. "32GB"
  14. ]
  15. }

这些特有属性如果排列组合,会产生12个不同的SKU,而不同的SKU,其属性就是上面备选项中的一个。

比如:

  • 红米4X,香槟金,2GB内存,16GB存储
  • 红米4X,磨砂黑,2GB内存,32GB存储

你会发现,每一个属性值,对应于SPUoptions数组的一个选项,如果我们记录下角标,就是这样:

  • 红米4X,0,0,0
  • 红米4X,2,0,1

既然如此,我们是不是可以将不同角标串联起来,作为SPU下不同SKU的标示。这就是我们的indexes字段。

1526266901335.png

这个设计在商品详情页会特别有用:

1526267180997.png

当用户点击选中一个特有属性,你就能根据 角标快速定位到sku。

own_spec字段

看结构:

  1. { "4":"香槟金","12":"2GB","13":"16GB"}

保存的是特有属性的键值对。

SPU中保存的是可选项,但不确定具体的值,而SKU中的保存的就是具体的值。

购物车

1.实现未登录状态的购物车

2.实现登陆状态下的购物车

流程图

1527585343248.png

这幅图主要描述了两个功能:新增商品到购物车、查询购物车。

新增商品:

  • 判断是否登录
    • 是:则添加商品到后台Redis中
    • 否:则添加商品到本地的Localstorage

无论哪种新增,完成后都需要查询购物车列表:

  • 判断是否登录
    • 否:直接查询localstorage中数据并展示
    • 是:已登录,则需要先看本地是否有数据,
      • 有:需要提交到后台添加到redis,合并数据,而后查询
      • 否:直接去后台查询redis,而后返回

添加购物车:

先发送一个axios请求去获取用户信息(判断是否登录)

已登陆

添加商品到后台Redis中

我们要知道是谁发送的请求,才能在Redis中获取数据,编写一个拦截器拦截所有请求,从中获取cookie中token内的用户信息,存入threadLocal(每次请求完成一定要释放threadLocal)

  1. @Autowired
  2. private StringRedisTemplate redisTemplate;
  3. public static final String prefix = "cart:user:id:";
  4. public void addCart(Cart cart) {
  5. UserInfo userInfo = ThreadUtils.get();
  6. String key = prefix + userInfo.getId();
  7. String skuIdKey = cart.getSkuId().toString();
  8. //获取加入的数量
  9. int num = cart.getNum();
  10. //另一种操作hashkey的api
  11. BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(key);
  12. //如果存在这个商品 就在原来的基础上加数量
  13. if (operations.hasKey(skuIdKey)) {
  14. String jsonCart = Objects.requireNonNull(operations.get(skuIdKey)).toString();
  15. cart = JsonUtils.parse(jsonCart, Cart.class);
  16. cart.setNum(cart.getNum() + num);
  17. }
  18. operations.put(skuIdKey, JsonUtils.serialize(cart));
  19. }

未登录

添加商品到本地的Localstorage

获取localStorage中的购物车(可能为空,空就设置为[]),判断是否有新添加的商品,有就添加数量,没有就添加商品

  1. // 获取以前的购物车
  2. const carts = ly.store.get("LY_CART") || [];
  3. // 获取与当前商品id一致的购物车数据
  4. const cart = carts.find(c => c.skuId === this.sku.id);
  5. if (cart) {
  6. // 存在,修改数量
  7. cart.num += this.num;
  8. } else {
  9. // 不存在,新增
  10. carts.push({
  11. skuId: this.sku.id,
  12. title: this.sku.title,
  13. image: this.images[0],
  14. price: this.sku.price,
  15. num: this.num,
  16. ownSpec: JSON.stringify(this.ownSpec)
  17. })
  18. }
  19. // 未登录
  20. ly.store.set("LY_CART", carts);
  21. // 跳转到购物车列表页
  22. window.location.href = "http://www.leyou.com/cart.html";

查询购物车

同样发送一个axios请求去获取用户信息(判断是否登录)
已登录

获取用户信息,然后在redis中进行查询

  1. public List<Cart> list() {
  2. UserInfo userInfo = ThreadUtils.get();
  3. String key = prefix + userInfo.getId();
  4. //如果没有这个key
  5. if (!redisTemplate.hasKey(key)) {
  6. throw new LyException(ExceptionEnum.CART_NOT_FOUND);
  7. }
  8. BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(key);
  9. return operations.values().stream().map(o -> JsonUtils.parse(o.toString(), Cart.class)).collect(Collectors.toList());
  10. }

未登录

从localStorage取出商品信息进行显示

登录后购物车合并(包括了查询)

当跳转到购物车页面,查询购物车列表前,需要判断用户登录状态,

  • 如果登录:
    • 首先检查用户的LocalStorage中是否有购物车信息,
    • 如果有,则提交到后台保存,
    • 清空LocalStorage
    • 查询购物车
    • 查询商品列表(主要是提示价格变化)
  • 如果未登录,直接查询即可

工具

雪花算法

  1. public class IdWorker {
  2. // 时间起始标记点,作为基准,一般取系统的最近时间(一旦确定不能变动)
  3. private final static long twepoch = 1288834974657L;
  4. // 机器标识位数
  5. private final static long workerIdBits = 5L;
  6. // 数据中心标识位数
  7. private final static long datacenterIdBits = 5L;
  8. // 机器ID最大值
  9. private final static long maxWorkerId = -1L ^ (-1L << workerIdBits);
  10. // 数据中心ID最大值
  11. private final static long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
  12. // 毫秒内自增位
  13. private final static long sequenceBits = 12L;
  14. // 机器ID偏左移12位
  15. private final static long workerIdShift = sequenceBits;
  16. // 数据中心ID左移17位
  17. private final static long datacenterIdShift = sequenceBits + workerIdBits;
  18. // 时间毫秒左移22位
  19. private final static long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
  20. private final static long sequenceMask = -1L ^ (-1L << sequenceBits);
  21. /* 上次生产id时间戳 */
  22. private static long lastTimestamp = -1L;
  23. // 0,并发控制
  24. private long sequence = 0L;
  25. private final long workerId;
  26. // 数据标识id部分
  27. private final long datacenterId;
  28. public IdWorker(){
  29. this.datacenterId = getDatacenterId(maxDatacenterId);
  30. this.workerId = getMaxWorkerId(datacenterId, maxWorkerId);
  31. }
  32. /**
  33. * @param workerId
  34. * 工作机器ID
  35. * @param datacenterId
  36. * 序列号
  37. */
  38. public IdWorker(long workerId, long datacenterId) {
  39. if (workerId > maxWorkerId || workerId < 0) {
  40. throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
  41. }
  42. if (datacenterId > maxDatacenterId || datacenterId < 0) {
  43. throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
  44. }
  45. this.workerId = workerId;
  46. this.datacenterId = datacenterId;
  47. }
  48. /**
  49. * 获取下一个ID
  50. *
  51. * @return
  52. */
  53. public synchronized long nextId() {
  54. long timestamp = timeGen();
  55. if (timestamp < lastTimestamp) {
  56. throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
  57. }
  58. if (lastTimestamp == timestamp) {
  59. // 当前毫秒内,则+1
  60. sequence = (sequence + 1) & sequenceMask;
  61. if (sequence == 0) {
  62. // 当前毫秒内计数满了,则等待下一秒
  63. timestamp = tilNextMillis(lastTimestamp);
  64. }
  65. } else {
  66. sequence = 0L;
  67. }
  68. lastTimestamp = timestamp;
  69. // ID偏移组合生成最终的ID,并返回ID
  70. long nextId = ((timestamp - twepoch) << timestampLeftShift)
  71. | (datacenterId << datacenterIdShift)
  72. | (workerId << workerIdShift) | sequence;
  73. return nextId;
  74. }
  75. private long tilNextMillis(final long lastTimestamp) {
  76. long timestamp = this.timeGen();
  77. while (timestamp <= lastTimestamp) {
  78. timestamp = this.timeGen();
  79. }
  80. return timestamp;
  81. }
  82. private long timeGen() {
  83. return System.currentTimeMillis();
  84. }
  85. /**
  86. * <p>
  87. * 获取 maxWorkerId
  88. * </p>
  89. */
  90. protected static long getMaxWorkerId(long datacenterId, long maxWorkerId) {
  91. StringBuffer mpid = new StringBuffer();
  92. mpid.append(datacenterId);
  93. String name = ManagementFactory.getRuntimeMXBean().getName();
  94. if (!name.isEmpty()) {
  95. /*
  96. * GET jvmPid
  97. */
  98. mpid.append(name.split("@")[0]);
  99. }
  100. /*
  101. * MAC + PID 的 hashcode 获取16个低位
  102. */
  103. return (mpid.toString().hashCode() & 0xffff) % (maxWorkerId + 1);
  104. }
  105. /**
  106. * <p>
  107. * 数据标识id部分
  108. * </p>
  109. */
  110. protected static long getDatacenterId(long maxDatacenterId) {
  111. long id = 0L;
  112. try {
  113. InetAddress ip = InetAddress.getLocalHost();
  114. NetworkInterface network = NetworkInterface.getByInetAddress(ip);
  115. if (network == null) {
  116. id = 1L;
  117. } else {
  118. byte[] mac = network.getHardwareAddress();
  119. id = ((0x000000FF & (long) mac[mac.length - 1])
  120. | (0x0000FF00 & (((long) mac[mac.length - 2]) << 8))) >> 6;
  121. id = id % (maxDatacenterId + 1);
  122. }
  123. } catch (Exception e) {
  124. System.out.println(" getDatacenterId: " + e.getMessage());
  125. }
  126. return id;
  127. }
  128. }

yml

  1. ly:
  2. worker:
  3. workerId: 1
  4. datacenterId: 1

IdWorkerProperties

  1. @Component
  2. @Data
  3. @ConfigurationProperties(prefix = "ly.worker")
  4. public class IdWorkerProperties {
  5. private long workerId;// 当前机器id
  6. private long datacenterId;// 序列号
  7. }

IdWorkerConfig

  1. @Configuration
  2. public class IdWorkerConfig {
  3. @Bean
  4. public IdWorker idWorker(IdWorkerProperties prop) {
  5. return new IdWorker(prop.getWorkerId(), prop.getDatacenterId());
  6. }
  7. }

使用

  1. @Autowired
  2. private IdWorker idWorker;
  3. Long orderId = idWorker.nextId();

JsonUtil

  1. public class JsonUtils {
  2. public static final ObjectMapper mapper = new ObjectMapper();
  3. private static final Logger logger = LoggerFactory.getLogger(JsonUtils.class);
  4. public static String serialize(Object obj) {
  5. if (obj == null) {
  6. return null;
  7. }
  8. if (obj.getClass() == String.class) {
  9. return (String) obj;
  10. }
  11. try {
  12. return mapper.writeValueAsString(obj);
  13. } catch (JsonProcessingException e) {
  14. logger.error("json序列化出错:" + obj, e);
  15. return null;
  16. }
  17. }
  18. public static <T> T parse(String json, Class<T> tClass) {
  19. try {
  20. return mapper.readValue(json, tClass);
  21. } catch (IOException e) {
  22. logger.error("json解析出错:" + json, e);
  23. return null;
  24. }
  25. }
  26. public static <E> List<E> parseList(String json, Class<E> eClass) {
  27. try {
  28. return mapper.readValue(json, mapper.getTypeFactory().constructCollectionType(List.class, eClass));
  29. } catch (IOException e) {
  30. logger.error("json解析出错:" + json, e);
  31. return null;
  32. }
  33. }
  34. public static <K, V> Map<K, V> parseMap(String json, Class<K> kClass, Class<V> vClass) {
  35. try {
  36. return mapper.readValue(json, mapper.getTypeFactory().constructMapType(Map.class, kClass, vClass));
  37. } catch (IOException e) {
  38. logger.error("json解析出错:" + json, e);
  39. return null;
  40. }
  41. }
  42. public static <T> T nativeRead(String json, TypeReference<T> type) {
  43. try {
  44. return mapper.readValue(json, type);
  45. } catch (IOException e) {
  46. logger.error("json解析出错:" + json, e);
  47. return null;
  48. }
  49. }
  50. }

使用

  1. // 获取通用规格参数
  2. Map<Long, String> genericMap = JsonUtils.parseMap(spuDetail.getGenericSpec(), Long.class, String.class);
  3. // 获取特有规格参数
  4. Map<Long, List<String>> specialMap = JsonUtils.nativeRead(spuDetail.getSpecialSpec(), new TypeReference<Map<Long, List<String>>>() {
  5. });