云商城--基础数据处理和分布式文件存储

第2章 基础数据处理和分布式文件存储

1.分布式文件存储系统Ceph学习

​ 1).掌握Ceph架构

​ 2).掌握Ceph组件

​ 3).搭建Ceph集群(了解)

2.Ceph使用

​ 1).基于Ceph实现文件上传

​ 2).基于Ceph实现文件下载

3.SKU、SPU管理

​ 1).掌握SKU和SPU关系

​ 2).理解商品发布中商品属性、商品分类、商品品牌加载方案

​ 3).实现SKU和SKU管理(商品发布)

4.品牌管理、分类管理、属性管理(作业)

​ 1).实现品牌管理(增删改查)

​ 2).实现分类管理(增删改查)

​ 3).实现属性管理(增删改查)

5.掌握MyBatisPlus代码生成

​ 1).掌握MyBatisPlus代码生成配置

​ 2).掌握MyBatisPlus代码生成controller,service,mapper

1.分布式文件存储系统Ceph

Ceph是一个统一的分布式存储系统,设计初衷是提供较好的性能、可靠性和可扩展性

对比说明 FASTDFS CEPH
开发语言 C C++
数据存储方式 文件/Trunk 对象/文件/块
在线扩容 支持 支持
冗余备份 支持 支持
单点故障 不存在 不存在
易用性 安装简单,社区相对活跃 安装有一定复杂度
适用场景 单集群的中小文件 单集群的大中小文件

1.1 Ceph介绍

Ceph于2004年发表,并随后贡献给开源社区。在经过了数年的发展之后,目前已得到众多云计算厂商的支持并被广泛应用。RedHat及OpenStack都可与Ceph整合以支持虚拟机镜像的后端存储

Ceph特点:

CRUSH算法:Crush算法是ceph的两大创新之一,简单来说,ceph摒弃了传统的集中式存储元数据寻址的方案,转而使用CRUSH算法完成数据的寻址操作。CRUSH在一致性哈希基础上很好的考虑了容灾域的隔离,能够实现各类负载的副本放置规则,例如跨机房、机架感知等。Crush算法有相当强大的扩展性,理论上支持数千个存储节点

高性能:Ceph中的数据副本数量可以由管理员自行定义,并可以通过CRUSH算法指定副本的物理存储位置以分隔故障域,支持数据强一致性; ceph可以忍受多种故障场景并自动尝试并行修复

高扩展性:Ceph本身并没有主控节点,扩展起来比较容易,并且理论上,它的性能会随着磁盘数量的增加而线性增长

特性丰富:Ceph支持三种调用接口:对象存储,块存储,文件系统挂载。三种方式可以一同使用。在国内一些公司的云环境中,通常会采用ceph作为openstack的唯一后端存储来提升数据转发效率

中文学习网:http://docs.ceph.org.cn/

Ceph架构:

组件对象讲解:

RADOS:就是这样一个可用于PB级规模数据存储集群的可伸缩的、可靠的对象存储服务,可以理解成Ceph的整个存储对象,包括逻辑对象

File:用户上传的文件

object:上传的文件被切成N个小文件块对象,RADOS的基本存储单元

MDS:元数据的内存缓存,为了加快元数据的访问

CRUSH:Ceph寻址算法,用于计算当前文件存储到哪个PG对应的OSD中

PG:对object的存储进行组织和位置映射。具体而言,一个PG负责组织若干个object(可以为数千个甚至更多),但一个object只能被映射到一个PG中,即,PG和object之间是"一对多"映射关系。同时,一个PG会被映射到n个OSD上,而每个OSD上都会承载大量的PG,即,PG和OSD之间是"多对多"映射关系

OSD:RADOS中的存储节点被称为OSD

架构图讲解:

1.文件上传,先将文件切片成N个object(如果开启了cephFS,可以使用MDS缓存)

2.切片后的文件object会存入到Ceph中

3.文件存储前,会经过CRUSH算法,计算当前文件存储归结于哪个PG

4.PG是逻辑概念上对文件存储范围划分的索引

5.根据PG索引将文件存储到指定服务器的OSD中

1.2 Ceph集群搭建

集群结构如上图,server1作为主节点(Dashbaord、mon、mds、rgw、mgr、osd),server2和server3作为子节点(mon、mds、rgw、mgr、osd)

节点中信息说明:

dashbaord:Ceph可视化管理界面

rgw:RADOSGW,Ceph对象网关,使客户端能够利用标准对象存储API来访问Ceph集群

mgr:ceph-mgr,主要目标实现 ceph 集群的管理,为外界提供统一的入口

节点信息:

server1:192.168.100.131

server2:192.168.100.132

server3:192.168.100.133

1.2.1 准备工作

1)机器名称修改(131、132、133都执行)

我们给每台机器一个别名192.168.100.131->CENTOS1,192.168.100.132->CENTOS2,192.168.100.133->CENTOS3

修改192.168.100.131/etc/hostname,添加CENTOS1

修改192.168.100.(同上)/etc/hostname,添加CENTOS2(同上)

修改192.168.100.133/etc/hostname,添加CENTOS3(同上)

配置名字解析IP:分别修改131132133/etc/hosts文件,添加如下映射:

properties 复制代码
192.168.100.131 CENTOS1
192.168.100.132 CENTOS2
192.168.100.133 CENTOS3

2)YUM源修改(131、132、133都执行)

这里采用清华镜像源,提升加载速度

vi /etc/yum.repos.d/ceph.repo,添加如下内容:

properties 复制代码
[Ceph]
name=Ceph packages for $basearch
baseurl=https://mirrors.tuna.tsinghua.edu.cn/ceph/rpm-mimic/el7/x86_64/
enabled=1
gpgcheck=1
type=rpm-md
gpgkey=https://download.ceph.com/keys/release.asc

[Ceph-noarch]
name=Ceph noarch packages
# 清华源
baseurl=https://mirrors.tuna.tsinghua.edu.cn/ceph/rpm-mimic/el7/noarch/
enabled=1
gpgcheck=1
type=rpm-md
gpgkey=https://download.ceph.com/keys/release.asc

[ceph-source]
name=Ceph source packages
baseurl=https://mirrors.tuna.tsinghua.edu.cn/ceph/rpm-mimic/el7/SRPMS/
enabled=1
gpgcheck=1
type=rpm-md
gpgkey=https://download.ceph.com/keys/release.asc

3)ceph与ceph-deploy安装(131)

更新yum源,并安装cephceph-deploy,这个过程非常耗时间,执行如下命令:

yum update && yum -y install ceph ceph-deploy

注意:yum updateyum -y install python2-pip最好在每台机器都更新下yum。

安装过程中, 如果执行ceph-deploy出现ImportError: No module named pkg_resources,则需要安装python2-pip,执行yum -y install python2-pip 安装即可

如果遇到如下错误,安装下epel即可

操作命令:(最好先执行该命令)

properties 复制代码
yum install epel-release -y
 
yum install lttng-ust -y

4)NTP时间同步工具(131执行)

为了保证时间同步,我们需要安装NTP时间同步工具:

properties 复制代码
yum install ntp ntpdate ntp-doc -y

设为开机启动:

properties 复制代码
systemctl enable ntpd

设置每隔1小时自动校准同步。编辑 vi /etc/rc.d/rc.local 追加:

properties 复制代码
/usr/sbin/ntpdate ntp1.aliyun.com > /dev/null 2>&1; /sbin/hwclock -w

配置定时任务, 执行crontab -e 加入:

properties 复制代码
0 */1 * * * ntpdate ntp1.aliyun.com > /dev/null 2>&1; /sbin/hwclock -w

5)免密配置(131、132、133都执行)

官方建议不用系统内置用户, 创建名为cuser用户, 密码也设为cuser:

properties 复制代码
useradd -d /home/cuser -m cuser
passwd cuser

设置sudo权限:(免密+只读权限)

properties 复制代码
echo "cuser ALL = (root) NOPASSWD:ALL" | sudo tee /etc/sudoers.d/cuser
sudo chmod 0440 /etc/sudoers.d/cuser

6)生成秘钥:(131执行)

切换用户: su cuser

执行ssh-keygen,一直按默认提示点击生成RSA密钥信息。

分发密钥至各机器节点

properties 复制代码
ssh-copy-id cuser@CENTOS1
ssh-copy-id cuser@CENTOS2
ssh-copy-id cuser@CENTOS3

修改管理节点上的 ~/.ssh/config (当前用户目录下的.ssh/config)文件, 简化SSH远程连接时的输入信息:

管理节点是会有root和cuser多个用户, ssh远程连接默认会以当前用户身份进行登陆, 如果我们是root身份进行远程连接, 还是需要输入密码,我们可以修改配置 使用root远程连接时也不用输入密码。

切换root身份,

复制代码
su root

编辑config

复制代码
vi ~/.ssh/config

添加如下内容:

properties 复制代码
Host CENTOS1
   Hostname CENTOS1
   User cuser
Host CENTOS2
   Hostname CENTOS2
   User cuser
Host CENTOS3
   Hostname CENTOS3
   User cuser

修改文件权限:

properties 复制代码
chmod 600 ~/.ssh/config

禁用SELINUX:

properties 复制代码
vi /etc/selinux/config

SELINUX=disabled
1.2.2 集群搭建

安装集群,用root安装,可以避免很多权限问题。

1)创建集群管理目录,作为ceph配置信息存储目录

properties 复制代码
mkdir -p /usr/local/gupao/cephcluster

cd /usr/local/gupao/cephcluster

2)创建集群

properties 复制代码
ceph-deploy new CENTOS1  CENTOS2 CENTOS3

创建成功后, 会生配置文件和秘钥信息

3)修改配置文件

编辑ceph.conf文件vi /usr/local/gupao/cephcluster/ceph.conf,添加如下配置:

properties 复制代码
#对外开放网段
public network = 192.168.100.0/24
# 设置pool池默认分配数量
osd pool default size = 2
# 容忍更多的时钟误差
mon clock drift allowed = 2
mon clock drift warn backoff = 30
# 允许删除pool
mon_allow_pool_delete = true
[mgr]
# 开启WEB仪表盘
mgr modules = dashboard

注意:Pool是存储对象的逻辑分区,它规定了数据冗余的类型和对应的副本分布策略

完整内容如下:

文件修改后执行安装(131执行),此时3台机器都会执行安装执行如下安装命令:

properties 复制代码
ceph-deploy install  CENTOS1  CENTOS2 CENTOS3

如果出现ceph_deploy][ERROR ] RuntimeError: Failed to execute command: ceph --version错误,可以直接在每个节点单独执行yum -y install ceph 进行单独安装。如果没有仓库文件ceph.repo, 按上面的步骤手工创建

4)初始化Monitor信息

properties 复制代码
ceph-deploy mon create-initial

此时会生成很多秘钥文件信息

5)同步管理信息

properties 复制代码
ceph-deploy admin  CENTOS1  CENTOS2 CENTOS3

6)安装mgr(管理守护进程)

properties 复制代码
ceph-deploy mgr create CENTOS1  CENTOS2 CENTOS3

7)安装rgw

properties 复制代码
ceph-deploy rgw create CENTOS1 CENTOS2 CENTOS3

mds服务:

properties 复制代码
ceph-deploy mds create CENTOS1 CENTOS2 CENTOS3

注意:任意一个环节安装失败了,需要卸载重装:

properties 复制代码
ceph-deploy purge CENTOS1 CENTOS2 CENTOS3
ceph-deploy purgedata CENTOS1 CENTOS2 CENTOS3
ceph-deploy forgetkeys

将三台节点的mon信息也删除:

properties 复制代码
rm -rf /var/run/ceph/

如果出现错误:

复制代码
ceph_deploy][ERROR ] RuntimeError: Failed to execute command: ceph --version

可以在各节点上单独进行安装:

复制代码
yum -y install ceph 

8)OSD安装

OSD服务是对象存储守护进程, 负责把对象存储到本地文件系统, 必须要有一块独立的磁盘作为存储。如果没有独立磁盘,怎么办? 可以在Linux下面创建一个虚拟磁盘进行挂载

添加磁盘:

执行fdisk -l查看磁盘信息如下,我们需要添加一个磁盘,直接用VMware添加即可。

使用VMware选择设置->硬盘->添加,如下图:

一直点击下一步,设置磁盘空间大小为10G即可。操作完后重启虚拟机,并输入fdisk -l查看磁盘信息如下,明显多了/dev/sdb 10G大小

执行创建OSD命令:(注意,每条命令都是在131中执行,不要在每台机器中单独执行)

properties 复制代码
ceph-deploy osd create --data /dev/sdb CENTOS1

ceph-deploy osd create --data /dev/sdb CENTOS2

ceph-deploy osd create --data /dev/sdb CENTOS3

Monitor查看

/usr/bin下执行./ceph -s可以查看集群状态

可以执行ntpdate ntp1.aliyun.com 同步各个节点的时间

如果出现如下情况,执行systemctl restart ceph.target重启每个节点即可(131,132,133都执行)

1.2.3 dashboard安装

Ceph 提供了原生的Dashboard功能,通过Dashboard可以获取Ceph集群的各种基本状态信息。我们接下来安装一下Dashboard,并使用它的功能

1)开启dashboard模块

properties 复制代码
ceph mgr module enable dashboard

2)生成签名

properties 复制代码
ceph dashboard create-self-signed-cert

3)创建目录

properties 复制代码
mkdir -p /usr/local/gupao/cephcluster/mgr-dashboard

4)生成密钥对

properties 复制代码
openssl req -new -nodes -x509   -subj "/O=IT/CN=ceph-mgr-dashboard" -days 3650   -keyout dashboard.key -out dashboard.crt -extensions v3_ca

5)启动dashboard

properties 复制代码
ceph mgr module disable dashboard
ceph mgr module enable dashboard

6)设置IP与PORT

properties 复制代码
ceph config set mgr mgr/dashboard/server_addr 192.168.100.131
ceph config set mgr mgr/dashboard/server_port 9001

7)关闭HTTPS

properties 复制代码
ceph config set mgr mgr/dashboard/ssl false

8)查看服务信息

properties 复制代码
ceph mgr services

9)设置管理员账号密码

properties 复制代码
ceph dashboard set-login-credentials admin admin

10)访问<https://192.168.100.131:8443/#/dashboard>

11)RGW访问

访问 http://192.168.100.131:7480/ 效果如下:

1.3 Cephfs管理

集群创建完后, 默认没有文件系统, 我们创建一个Cephfs可以支持对外访问的文件系统。

1)创建两个存储池, 执行两条命令:

properties 复制代码
ceph osd pool create cephfs_data 128
ceph osd pool create cephfs_metadata 64

少于5个OSD可把pg_num设置为128

OSD数量在5到10,可以设置pg_num为512

OSD数量在10到50,可以设置pg_num为4096

OSD数量大于50,需要计算pg_num的值

通过下面命令可以列出当前创建的存储池:

properties 复制代码
ceph osd lspools

2)创建fs, 名称为fs_test:

properties 复制代码
ceph fs new fs_test cephfs_metadata cephfs_data

3)状态查看, 以下信息代表正常

ceph fs ls

properties 复制代码
name: fs_test, metadata pool: cephfs_metadata, data pools: [cephfs_data ]

ceph mds stat:

properties 复制代码
fs_test-0/0/1 up

4)fuse挂载

先确定ceph-fuse命令能执行, 如果没有, 则安装:

properties 复制代码
 yum -y install ceph-fuse

创建挂载目录

properties 复制代码
mkdir -p /usr/local/gupao/cephfs_directory

挂载cephfs

properties 复制代码
ceph-fuse -k /etc/ceph/ceph.client.admin.keyring -m 192.168.100.131:6789 /usr/local/gupao/cephfs_directory

出现下面信息表示挂载成功了:

properties 复制代码
ceph-fuse[28003]: starting fuse

5)挂载信息查看

properties 复制代码
[root@CENTOS1 cephcluster]# df -h
Filesystem               Size  Used Avail Use% Mounted on
devtmpfs                 4.1G     0  4.1G   0% /dev
tmpfs                    4.1G     0  4.1G   0% /dev/shm
tmpfs                    4.1G   20M  4.1G   1% /run
tmpfs                    4.1G     0  4.1G   0% /sys/fs/cgroup
/dev/mapper/centos-root   17G  2.0G   16G  12% /
/dev/sda1               1014M  189M  826M  19% /boot
tmpfs                    4.1G   28K  4.1G   1% /var/lib/ceph/osd/ceph-0
tmpfs                    838M     0  838M   0% /run/user/0
ceph-fuse                 13G     0   13G   0% /usr/local/gupao/cephfs_directory

1.4 Ceph Swift API接口开发

Swift是由Rackspace开发的用来为云计算提供可扩展存储的项目。专注于对象存储, 并提供一套REST风格的Api来访问, 与Ceph强一致性不同, 它是最终一致性。两者都是优秀的开源项目, 并无明显优劣之分,在使用场景上有所不同, 如果是专注于对象存储, 那么可以选择swift即可满足需要, 如果还有块存储要求, 那么选择Ceph更为合适。这里选择Ceph, 因为通过网关可以适配兼容swift api, 同时在数据访问上具有较强的扩展性

1.4.1 准备工作

创建Swift用户, 用于接口请求认证

properties 复制代码
sudo radosgw-admin user create --subuser="cephtester:subtester" --uid="cephtester" --display-name="cephtester" --key-type=swift --secret="gupao" --access=full

uid 为主用户, subuser为子用户信息, secret指定密钥, 不指定则随机生成, access拥有权限设定,代码中需使用返回信息中的user和secret_key

swift_keys:

properties 复制代码
"swift_keys": [
    {
        "user": "cephtester:subtester",
        "secret_key": "gupao"
    }
],

创建管理员账号:

properties 复制代码
radosgw-admin user create --uid=mgruser --display-name=mgruser  --system

返回信息如下:

properties 复制代码
{
    "user_id": "mgruser",
    "display_name": "mgruser",
    "email": "",
    "suspended": 0,
    "max_buckets": 1000,
    "auid": 0,
    "subusers": [],
    "keys": [
        {
            "user": "mgruser",
            "access_key": "AZ6L40PH9WB37EKVVMCZ",
            "secret_key": "rk8PEjtYaMTo7nMDM62hqqN1tOnZPBEe4GA0LQMW"
        }
    ],
    "swift_keys": [],
    "caps": [],
    "op_mask": "read, write, delete",
    "system": "true",
    "default_placement": "",
    "placement_tags": [],
    "bucket_quota": {
        "enabled": false,
        "check_on_raw": false,
        "max_size": -1,
        "max_size_kb": 0,
        "max_objects": -1
    },
    "user_quota": {
        "enabled": false,
        "check_on_raw": false,
        "max_size": -1,
        "max_size_kb": 0,
        "max_objects": -1
    },
    "temp_url_keys": [],
    "type": "rgw",
    "mfa_ids": []
}

根据生成的access_key与secret_key, 执行:

properties 复制代码
ceph dashboard set-rgw-api-access-key AZ6L40PH9WB37EKVVMCZ
ceph dashboard set-rgw-api-secret-key rk8PEjtYaMTo7nMDM62hqqN1tOnZPBEe4GA0LQMW

打开管理界面,http://192.168.100.131:9001/#/rgw/user 可以查看到我们刚才创建的两个用户:

1.4.2 文件服务搭建

我们搭建一个单独的工程,专门用于实现文件上传和文件下载,工程坐标如下:

xml 复制代码
<groupId>com.gupaoedu.vip.mall</groupId>
<version>0.0.1-SNAPSHOT</version>
<artifactId>mall-file-service</artifactId>

pom.xml:

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<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>mall-service</artifactId>
        <groupId>com.gupaoedu.vip.mall</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>mall-file-service</artifactId>
    <description>
        文件上传微服务
    </description>

    <dependencies>
        <!-- Rados Java Api依赖 -->
        <dependency>
            <groupId>com.ceph</groupId>
            <artifactId>rados</artifactId>
            <version>0.6.0</version>
        </dependency>
        <!-- Cephfs 文件系统依赖 -->
        <dependency>
            <groupId>com.ceph</groupId>
            <artifactId>libcephfs</artifactId>
            <version>0.80.5</version>
        </dependency>

        <!--swift-->
        <dependency>
            <groupId>org.javaswift</groupId>
            <artifactId>joss</artifactId>
            <version>0.10.2</version>
        </dependency>
    </dependencies>
</project>

bootstrap.yml:

yml 复制代码
server:
  port: 8082
spring:
  application:
    name: mall-file
  cloud:
    nacos:
      config:
        file-extension: yaml
        server-addr: 192.168.1.11:8848
      discovery:
        #Nacos的注册地址
        server-addr: 192.168.1.11:8848

ceph:
  username: cephtester:subtester  #Ceph配置 主用户名:子用户名
  password: gupao #秘钥
  authUrl: http://192.168.100.131:7480/auth/1.0 #接口访问路径
  defaultContainerName: user_datainfo #默认容器名字
#图片路径
cephurl: http://localhost:8082/file/download/

#日志配置
logging:
  pattern:
    console: "%msg%n"

创建com.gupaoedu.vip.mall.file.ceph.ContainerConfig配置类,在类中创建AccountContainer对象,代码如下:

java 复制代码
/**
 * 容器的初始化操作
 */
@Data
@Configuration
@ConfigurationProperties(prefix = "ceph") //yaml配置文件中配置的属性
public class ContainerConfig {

    private String username;
    private String password;
    private String authUrl;
    private String defaultContainerName;


    /**
     * 1.创建账号信息
     */
    @Bean
    public Account account(){
        AccountConfig config = new AccountConfig();
        //配置认证信息
        config.setUsername(username);
        config.setPassword(password);
        config.setAuthUrl(authUrl);
        //配置认证方式
        config.setAuthenticationMethod(AuthenticationMethod.BASIC);
        return new AccountFactory(config).createAccount();
    }


    /**
     * 2.创建容器对象
     */
    @Bean
    public Container container(){
        //获取账号信息
        Container container = account().getContainer(defaultContainerName);
        if(!container.exists()){
            return  container.create();
        }
        return container;
    }

}

创建文件上传下载工具类com.gupaoedu.vip.mall.file.ceph.FileHandler,代码如下:

java 复制代码
@Component
public class FileHandler {

    @Autowired
    private Container container;

    /**
     * 文件上传
     */
    public void upload(String filename,byte[] buffer) {//buffer:文件内容字节
        //获取容器对象
        StoredObject object = container.getObject(filename);
        //文件上传
        object.uploadObject(buffer);
    }

    /*
     * 文件下载
     */
    public byte[] download(String filename){
        //获取容器中远程存储的信息
        StoredObject object = container.getObject(filename);
        //执行文件下载
        byte[] bytes = object.downloadObject();
        return bytes;
    }
}

控制器创建:com.gupaoedu.vip.mall.file.controller.FileController

java 复制代码
@RestController
@RequestMapping(value = "/file")
public class FileController {
    
    @Autowired
    private FileHandler fileHandler;

    @Value("${cephurl}")
    private String cephurl;
    
    /**
     * 文件上传
     */
    @PostMapping(value = "/upload")
    public RespResult upload(MultipartFile file) throws IOException {
        //上传
        fileHandler.upload(file.getOriginalFilename(),file.getBytes());
        return RespResult.ok(cephurl+file.getOriginalFilename());
    }

    /**
     * 下载
     */
    @GetMapping(value = "/download/{filename}")
    public void download(@PathVariable String filename, HttpServletResponse response) throws IOException {
        //下载
        byte[] bytes = fileHandler.download(filename);
        //输出文件
        ServletOutputStream os = response.getOutputStream();
        os.write(bytes);
    }
}

创建启动类:com.gupaoedu.vip.mall.file.MallFileApplication

java 复制代码
/**
 * http://localhost:8082/file/download/news1.jpg
 */
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)//屏蔽掉数据库的设置,这里不需要用到数据库
public class MallFileApplication {

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

文件上传测试http://localhost:8082/file/upload

文件下载测试http://localhost:8082/file/download/20200716-af9fc1ae9d24f880.png

2.Spu和Sku(产品发布)

2.1 掌握SKU和SPU关系

SPU = Standard Product Unit (标准化产品单元),SPU是商品信息聚合的最小单位,是一组可复用、易检索的标准化信息的集合,该集合描述了一个产品的特性。通俗点讲,属性值、特性相同的商品就可以称为一个SPU

SKU=stock keeping unit(库存量单位),SKU即库存进出计量的单位, 可以是以件、盒、托盘等为单位。SKU是物理上不可分割的最小存货单元。在使用时要根据不同业态,不同管理模式来处理。在服装、鞋类商品中使用最多最普遍

举个例子:

购买手机的时候,你可以选择华为Mate40系列手机,Mate40系列手机的生产制造商是华为,品牌是华为,手机分类也是华为,不过Mate40系列手机有多款,比如 Mate40 、Mate40 Pro 、 Mate40 Pro +,每款手机的架构也不一样,颜色也不一定一样,那么这个例子中哪些是Spu哪些是Sku呢?

Spu:

手机系列:Mate40系列

厂家:华为

品牌:华为

分类:手机

Sku:价格、颜色、网络格式

2.2 表结构设计

2.21 Spu和Sku

spu:

sql 复制代码
CREATE TABLE `spu` (
  `id` varchar(60) NOT NULL COMMENT '主键',
  `name` varchar(100) DEFAULT NULL COMMENT 'SPU名',
  `intro` varchar(200) DEFAULT NULL COMMENT '简介',
  `brand_id` int(11) DEFAULT NULL COMMENT '品牌ID',
  `category_one_id` int(20) DEFAULT NULL COMMENT '一级分类',
  `category_two_id` int(10) DEFAULT NULL COMMENT '二级分类',
  `category_three_id` int(10) DEFAULT NULL COMMENT '三级分类',
  `images` varchar(1000) DEFAULT NULL COMMENT '图片列表',
  `after_sales_service` varchar(50) DEFAULT NULL COMMENT '售后服务',
  `content` longtext COMMENT '介绍',
  `attribute_list` varchar(3000) DEFAULT NULL COMMENT '规格列表',
  `is_marketable` int(1) DEFAULT '0' COMMENT '是否上架,0已下架,1已上架',
  `is_delete` int(1) DEFAULT '0' COMMENT '是否删除,0:未删除,1:已删除',
  `status` int(1) DEFAULT '0' COMMENT '审核状态,0:未审核,1:已审核,2:审核不通过',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

sku:

sql 复制代码
CREATE TABLE `sku` (
  `id` varchar(60) NOT NULL COMMENT '商品id',
  `name` varchar(200) NOT NULL COMMENT 'SKU名称',
  `price` int(20) NOT NULL DEFAULT '1' COMMENT '价格(分)',
  `num` int(10) DEFAULT '100' COMMENT '库存数量',
  `image` varchar(200) DEFAULT NULL COMMENT '商品图片',
  `images` varchar(2000) DEFAULT NULL COMMENT '商品图片列表',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  `spu_id` varchar(60) DEFAULT NULL COMMENT 'SPUID',
  `category_id` int(10) DEFAULT NULL COMMENT '类目ID',
  `category_name` varchar(200) DEFAULT NULL COMMENT '类目名称',
  `brand_id` int(11) DEFAULT NULL COMMENT '品牌id',
  `brand_name` varchar(100) DEFAULT NULL COMMENT '品牌名称',
  `sku_attribute` varchar(200) DEFAULT NULL COMMENT '规格',
  `status` int(1) DEFAULT '1' COMMENT '商品状态 1-正常,2-下架,3-删除',
  PRIMARY KEY (`id`),
  KEY `cid` (`category_id`),
  KEY `status` (`status`),
  KEY `updated` (`update_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='商品表';
2.2.2 商品发布流程分析

商品发布流程如下:

1)分类选择

发布商品前,需要先选择发布商品所属分类,分类严格定义为3级分类

分类表:

sql 复制代码
CREATE TABLE `category` (
  `id` int(20) NOT NULL AUTO_INCREMENT COMMENT '分类ID',
  `name` varchar(50) DEFAULT NULL COMMENT '分类名称',
  `sort` int(11) DEFAULT NULL COMMENT '排序',
  `parent_id` int(20) DEFAULT NULL COMMENT '上级ID',
  PRIMARY KEY (`id`),
  KEY `parent_id` (`parent_id`)
) ENGINE=InnoDB AUTO_INCREMENT=11182 DEFAULT CHARSET=utf8 COMMENT='商品类目';

2)选择品牌

分类选择完成后,需要加载品牌,品牌加载并非一次性加载完成,而是根据选择的分类进行加载。

分类品牌关系表:

sql 复制代码
CREATE TABLE `category_brand` (
  `category_id` int(11) NOT NULL COMMENT '分类ID',
  `brand_id` int(11) NOT NULL COMMENT '品牌ID',
  PRIMARY KEY (`brand_id`,`category_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

品牌表:

sql 复制代码
CREATE TABLE `brand` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '品牌id',
  `name` varchar(100) NOT NULL COMMENT '品牌名称',
  `image` varchar(1000) DEFAULT '' COMMENT '品牌图片地址',
  `initial` varchar(1) DEFAULT '' COMMENT '品牌的首字母',
  `sort` int(11) DEFAULT NULL COMMENT '排序',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8 COMMENT='品牌表';

3)属性加载

当选择分类后,加载分类对应的属性。

分类属性表:

sql 复制代码
CREATE TABLE `category_attr` (
  `category_id` int(11) NOT NULL,
  `attr_id` int(11) NOT NULL COMMENT '属性分类表',
  PRIMARY KEY (`category_id`,`attr_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

属性表:

sql 复制代码
CREATE TABLE `sku_attribute` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `name` varchar(50) DEFAULT NULL COMMENT '属性名称',
  `options` varchar(2000) DEFAULT NULL COMMENT '属性选项',
  `sort` int(11) DEFAULT NULL COMMENT '排序',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8;

2.3 商品发布加载功能

2.3.1 分类加载

分类功能需要实现按照父ID查询,最开始初始化加载的是顶级父类,parent_id=0,后面每次点击的时候都根据传入的id查询子分类。

1)Mapper

创建com.gupaoedu.vip.mall.goods.mapper.CategoryMapper,代码如下:

java 复制代码
public interface CategoryMapper extends BaseMapper<Category> {
}

2)Service

接口:com.gupaoedu.vip.mall.goods.service.CategoryService代码如下:

java 复制代码
public interface CategoryService extends IService<Category> {

    /**
     * 根据父ID查询子分类
     */
    List<Category> queryByParentId(Integer pid);
}

实现类:com.gupaoedu.vip.mall.goods.service.impl.CategoryServiceImpl代码如下:

java 复制代码
@Service
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper,Category> implements CategoryService {

    @Autowired
    private CategoryMapper categoryMapper;

    /***
     * 根据父ID查询子分类
     */
    @Override
    public List<Category> queryByParentId(Integer pid) {
        //条件封装
        QueryWrapper<Category> queryWrapper = new QueryWrapper<Category>();
        queryWrapper.eq("parent_id",pid);
        return categoryMapper.selectList(queryWrapper);
    }
}

3)Controller

创建com.gupaoedu.vip.mall.goods.controller.CategoryController代码如下;

java 复制代码
@RestController
@RequestMapping(value = "/category")
@CrossOrigin
public class CategoryController {

    @Autowired
    private CategoryService categoryService;

    /****
     * 根据父ID查询子分类
     */
    @GetMapping(value = "/parent/{pid}")
    public RespResult<List<Category>> list(@PathVariable(value = "pid")Integer pid){
        List<Category> categories = categoryService.queryByParentId(pid);
        return RespResult.ok(categories);
    }
}
2.3.2 品牌加载

品牌需要根据分类进行加载,当用户选择第3级分类的时候,加载品牌,品牌数据需要经过category_brand表关联查询。

我们可以按照如下步骤实现:

1、查询category_brand中指定分类对应的品牌ID集合

2、从brand查出品牌集合

1)Mapper

修改com.gupaoedu.vip.mall.goods.mapper.BrandMapper,添加根据分类ID查询品牌ID集合:

java 复制代码
//根据分类ID查询品牌集合
@Select("select brand_id from category_brand where category_id=#{id}")
List<Integer> queryBrandIds(Integer id);

2)Service

接口:com.gupaoedu.vip.mall.goods.service.BrandService中添加根据分类ID查询品牌集合方法

java 复制代码
//根据分类ID查询品牌
List<Brand> queryByCategoryId(Integer id);

实现类:com.gupaoedu.vip.mall.goods.service.impl.BrandServiceImpl

java 复制代码
/***
 * 根据分类ID查询品牌
 */
@Override
public List<Brand> queryByCategoryId(Integer id) {
    //查询分类ID对应的品牌集合
    List<Integer> brandIds = brandMapper.queryBrandIds(id);
    //根据品牌ID集合查询品牌信息
    List<Brand> brands = brandMapper.selectBatchIds(brandIds);
    return brands;
}

3)Controller

修改com.gupaoedu.vip.mall.goods.controller.BrandController添加根据分类ID查询品牌集合

java 复制代码
/****
 * 根据分类ID查询品牌
 */
@GetMapping(value = "/category/{id}")
public RespResult<List<Brand>> categoryBrands(@PathVariable(value = "id")Integer id){
    List<Brand> brands = brandService.queryByCategoryId(id);
    return RespResult.ok(brands);
}
2.3.3 属性加载

属性也称为规格,属性也需要根据分类查询,我们可以按照如下思路实现:

1、先从category_attr根据分类ID查询出当前分类拥有的属性ID集合

2、从sku_attribute中查询属性集合

1)Mapper

创建com.gupaoedu.vip.mall.goods.mapper.SkuAttributeMapper实现根据分类ID查询属性信息。

java 复制代码
/***
 * 根据分类ID查询属性集合
 */
@Select("SELECT * FROM sku_attribute WHERE id IN(SELECT attr_id FROM category_attr WHERE category_id=#{id})")
List<SkuAttribute> queryByCategoryId(Integer id);

2)Service

接口:com.gupaoedu.vip.mall.goods.service.SkuAttributeService添加根据分类ID查询属性集合方法

java 复制代码
//根据分类ID查询属性集合
List<SkuAttribute> queryList(Integer id);

实现类:com.gupaoedu.vip.mall.goods.service.impl.SkuAttributeServiceImpl添加如下实现方法

java 复制代码
/***
 * 根据分类ID查询属性集合
 */
@Override
public List<SkuAttribute> queryList(Integer id) {
    return skuAttributeMapper.queryByCategoryId(id);
}

3)Controller

创建com.gupaoedu.vip.mall.goods.controller.SkuAttributeController,添加如下方法

java 复制代码
/***
 * 根据分类ID查询
 */
@GetMapping(value = "/category/{id}")
public RespResult<SkuAttribute> categoryAttributeList(@PathVariable(value = "id")Integer id){
    //根据分类ID查询属性参数
    List<SkuAttribute> skuAttributes = skuAttributeService.queryList(id);
    return RespResult.ok(skuAttributes);
}

2.4 商品发布

2.4.1 复合对象分析

商品发布,如上图,我们可以发现发布的商品信息包含Sku和Spu,因此我们应该在后端能有一个对象同时能接到Spu和多个Sku,方法有很多种,我们可以直接在Spu中写一个List<Sku>,但这种方法不推荐,按照对象设计原则,对一个对象进行扩展时,尽量避免对原始对象造成改变,因此我们可以使用复合类,可以创建一个Prodcut类,该类中有Spu也有List<Sku>,代码如下:

java 复制代码
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Product {
    // Spu
    private Spu spu;
    // Sku
    private List<Sku> skus;
}
2.4.2 添加商品

添加商品的时候,我们需要保存Spu,同时需要添加多个Sku。我们可以在华为商城中看看真实电商中Sku名字特征,每次点击不同属性的时候,前部分名字一样,只是将名字中的规格替换了,也就是说Sku的名字其实是组合成的,一部分是Spu的一部分是Sku的,可以进行组合

1)名字分析

添加商品的时候,会将商品的属性传入后台,格式如下,如果把规格名字添加到名字中,那就是华为商城中的效果了,我们可以这么做,把属性解析成Map,然后每个属性值添加到商品名字中即可

{"适合人群":"有一定java基础的人","书籍分类":"软件编程"}

2)实现代码

Mapper

com.gupaoedu.vip.mall.goods.mapper.SpuMapper代码如下:

java 复制代码
public interface SpuMapper extends BaseMapper<Spu> {
}

com.gupaoedu.vip.mall.goods.mapper.SkuMapper代码如下:

java 复制代码
public interface SkuMapper extends BaseMapper<Sku> {
}

Service

com.gupaoedu.vip.mall.goods.service.SpuService中添加产品方法如下

java 复制代码
public interface SpuService extends IService<Spu> {
    //保存商品
    void saveProduct(Product product);
}

com.gupaoedu.vip.mall.goods.service.impl.SpuServiceImpl中添加产品方法如下:

java 复制代码
@Service
public class SpuServiceImpl extends ServiceImpl<SpuMapper,Spu> implements SpuService {

    @Autowired
    private SkuMapper skuMapper;

    @Autowired
    private SpuMapper spuMapper;

    @Autowired
    private CategoryMapper categoryMapper;

    @Autowired
    private BrandMapper brandMapper;

    // 保存商品
    @Override
    public void saveProduct(Product product) {
        //Spu
        Spu spu = product.getSpu();
        //上架
        spu.setIsMarketable(1);
        //未删除
        spu.setIsDelete(0);
        //状态
        spu.setStatus(1);
        //添加
        spuMapper.insert(spu);


        //查询三级分类
        Category category = categoryMapper.selectById(spu.getCategoryThreeId());
        //查询品牌
        Brand brand = brandMapper.selectById(spu.getBrandId());
        //当前时间
        Date now = new Date();

        //新增Sku集合
        for (Sku sku : product.getSkus()) {
            //设置名字
            String skuName = spu.getName();
            Map<String,String> attrMap = JSON.parseObject(sku.getSkuAttribute(), Map.class);
            for (Map.Entry<String, String> entry : attrMap.entrySet()) {
                skuName+= "   "+entry.getValue();
            }
            sku.setName(skuName);
            //设置图片
            sku.setImages(spu.getImages());
            //设置状态
            sku.setStatus(1);
            //设置类目ID
            sku.setCategoryId(spu.getCategoryThreeId());
            //设置类目名称
            sku.setCategoryName(category.getName());
            //设置品牌ID
            sku.setBrandId(brand.getId());
            //设置品牌名称
            sku.setBrandName(brand.getName());
            //设置Spuid
            sku.setSpuId(spu.getId());
            //时间
            sku.setCreateTime(now);
            sku.setUpdateTime(now);
            //增加
            skuMapper.insert(sku);
        }
    }
}

Controller

创建com.gupaoedu.vip.mall.goods.controller.SpuController,添加产品代码如下:

java 复制代码
@Autowired
private SpuService spuService;

/***
 * 保存
 */
@PostMapping(value = "/save")
public RespResult save(@RequestBody Product product){
    //保存
    spuService.saveProduct(product);
    return RespResult.ok();
}
2.4.3 产品修改

产品修改其实和产品添加几乎一致,只需要做小改动即可,实现步骤如下:

1、如果Spu的id值不为空,说明是修改操作

2、如果是修改操作,先删除之前对应的Sku集合

3、其他流程和添加商品一致

修改com.gupaoedu.vip.mall.goods.service.impl.SpuServiceImplsave方法,代码如下:

源码如下:

java 复制代码
@Override
public void saveProduct(Product product) {
    //Spu
    Spu spu = product.getSpu();

    //如果ID为空,则增加
    if(StringUtils.isEmpty(spu.getId())){
        //上架
        spu.setIsMarketable(1);
        //未删除
        spu.setIsDelete(0);
        //状态
        spu.setStatus(1);
        //添加
        spuMapper.insert(spu);
    }else{
        //ID 不为空,则修改
        spuMapper.updateById(spu);
        //删除之前的Sku记录
        skuMapper.delete(new QueryWrapper<Sku>().eq("spu_id",spu.getId()));
    }

    //查询三级分类
    Category category = categoryMapper.selectById(spu.getCategoryThreeId());
    //查询品牌
    Brand brand = brandMapper.selectById(spu.getBrandId());
    //当前时间
    Date now = new Date();

    //新增Sku集合
    for (Sku sku : product.getSkus()) {
        //设置名字
        String skuName = spu.getName();
        Map<String,String> attrMap = JSON.parseObject(sku.getSkuAttribute(), Map.class);
        for (Map.Entry<String, String> entry : attrMap.entrySet()) {
            skuName+= "   "+entry.getValue();
        }
        sku.setName(skuName);
        //设置图片
        sku.setImages(spu.getImages());
        //设置状态
        sku.setStatus(1);
        //设置类目ID
        sku.setCategoryId(spu.getCategoryThreeId());
        //设置类目名称
        sku.setCategoryName(category.getName());
        //设置品牌ID
        sku.setBrandId(brand.getId());
        //设置品牌名称
        sku.setBrandName(brand.getName());
        //设置Spuid
        sku.setSpuId(spu.getId());
        //时间
        sku.setCreateTime(now);
        sku.setUpdateTime(now);
        //增加
        skuMapper.insert(sku);
    }
}

3.MyBatis Plus代码生成

我们可以发现个问题,刚才写的很多增删改查代码都比较简单,比较枯燥,重复写一些类的创建、单表增删改查非常类,而创建对象和单标操作的代码,在开发中几乎占用了开发时间的80%,如果能够用工具生成就可以大大节省我们开发成本了

3.1 MyBatis Plus介绍

AutoGenerator 是 MyBatis-Plus 的代码生成器,通过 AutoGenerator 可以快速生成 Entity、Mapper、Mapper XML、Service、Controller 等各个模块的代码,极大的提升了开发效率

3.2 MyBatisPlus代码生成配置

1)引入依赖

xml 复制代码
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-generator</artifactId>
    <version>3.4.0</version>
</dependency>

<dependency>
    <groupId>org.apache.velocity</groupId>
    <artifactId>velocity-engine-core</artifactId>
    <version>2.2</version>
</dependency>

2)代码生成

java 复制代码
public static void main(String[] args) {
    // 代码生成器
    AutoGenerator mpg = new AutoGenerator();

    // 全局配置
    GlobalConfig gc = new GlobalConfig();
    String projectPath = System.getProperty("user.dir");
    gc.setOutputDir(projectPath + "/src/main/java");    // 文件输出路径
    gc.setAuthor("bobo");   //作者
    gc.setOpen(false);   //生成之后是否打开目录
    gc.setIdType(IdType.NONE);  //主键策略
    gc.setServiceName("%sService"); //名字设置 %s是占位符,可以理解成类的名字
    mpg.setGlobalConfig(gc);

    // 数据源配置
    DataSourceConfig dsc = new DataSourceConfig();
    dsc.setUrl("jdbc:mysql://192.168.100.130:3306/shop_goods?useUnicode=true&useSSL=false&characterEncoding=utf8");
    dsc.setDriverName("com.mysql.jdbc.Driver");
    dsc.setUsername("root");
    dsc.setPassword("123456");
    mpg.setDataSource(dsc);

    // 包配置
    PackageConfig pc = new PackageConfig();
    pc.setModuleName("mall-goods");
    pc.setParent("com.gupaoedu.code");
    mpg.setPackageInfo(pc);

    // 策略配置
    StrategyConfig strategy = new StrategyConfig();
    strategy.setNaming(NamingStrategy.underline_to_camel);  //驼峰命名
    strategy.setColumnNaming(NamingStrategy.underline_to_camel);    //驼峰命名
    strategy.setEntityLombokModel(true);    //是否使用Lombok
    strategy.setRestControllerStyle(true);  //是否生成RestController
    // 写于父类中的公共字段
    strategy.setSuperEntityColumns("id");   //公共字段定义
    strategy.setControllerMappingHyphenStyle(true); //驼峰转连字符
    strategy.setTablePrefix(pc.getModuleName() + "_");  //表前缀
    mpg.setStrategy(strategy);
    mpg.execute();
}
相关推荐
KIDAKN30 分钟前
RabbitMQ 初步认识
分布式·rabbitmq
pan30350747935 分钟前
Kafka 和 RabbitMQ的选择
分布式·kafka·rabbitmq
hzulwy3 小时前
Kafka基础理论
分布式·kafka
明达智控技术4 小时前
MR30分布式IO在全自动中药煎药机中的应用
分布式·物联网·自动化
jakeswang5 小时前
细说分布式ID
分布式
失散136 小时前
分布式专题——1.2 Redis7核心数据结构
java·数据结构·redis·分布式·架构
王中阳Go7 小时前
头一次见问这么多kafka的问题
分布式·kafka
boonya8 小时前
Kafka核心原理与常见面试问题解析
分布式·面试·kafka
KIDAKN10 小时前
RabbitMQ 重试机制 和 TTL
分布式·rabbitmq
JAVA学习通10 小时前
【RabbitMQ】----初识 RabbitMQ
分布式·rabbitmq