1 课程计划
1.1 目标
需求:完成商品添加业务。
第一步:理解商品模块的业务(通过ER图理解)
考核的知识点,通过数据库结构快速生成ER图。同自己的理解画好关系。
问题:为什么数据库表不建外键约束?
答:外键约束确保了数据的完整性,但是也约束数据的灵活性。如果将外键在数据里创建,不适合需求多变的项目。
第二步:查询商品类目(以树形结构显示,UI设计的要求)
考核的知识点,就是如何封装一个树状的数据结构。
第三步:实现图片的上传(要求:上传到指定的FTP服务器)
考核的知识点:
(1)Linux系统的使用
(2)tengine 纯HTTP的web服务器
(3)SpringMVC的上传功能
(4)FTP的数据传到
第四步:设置类目的参数规格模板
考核的知识点:JSON数据格式转换。
第五步:商品的保存
考核的知识点:使用MybatisPlus插入数据
1.2 功能分析
1.2.1 相关数据表
说明:与商品模块有关的表,总共有5张。关系如下:
|
1.2.2 实现的思路
(1)每个商品都有一个分类,所以要实现商品类目选择功能。
(2)商品有一个图片属性,所以要实现图片上传的功能。
(3)每个商品都有规格参数,所以要实现商品规格参数编辑功能。
(4)将商品的规格参数、商品详情、商品信息分别保存到三张表中。
2 第一部分:实现商品类目选择功能
2.1 需求分析
在商品页面,点击”选择类目”按钮,生成商品类目异步树。
|
对应的数据库表为tb_item_cat,表结构为:
|
实现的思路:
业务理解:在加载树控件的时候,将所有顶级的类目显示出来。所有的子节点在展开的时候传入节点对应的类目编号(ID),查询对应的类目数据。
根据业务理解:
(1)加载树控件。(本项目使用的是easyui-tree插件,第一次传递的cid=0)
(2)确定异步树请求的参数及返回的节点结构。(要构建easyui-tree对应的业务模型VO,id、text、status)
(3)请求数据库,生成树结构。(根据parent_id字段查询子节点实现。)
2.2 实现步骤
2.2.1 第一步:加载树控件
(1)定义类目选择的按钮。(点击按钮,加载异步树控件)
|
(2)加载异步树控件
|
查看EasyUI的API文档,我们知道:url是请求路径。
|
2.2.2 第二步:确定加载树请求的参数
查看API文档,我们知道请求的参数名是id,是当前节点的id值。
|
2.2.3 第三步:确定树节点结构
查看API文档,节点包括id、text、state三个基本属性。
|
2.2.4 第四步:java代码实现异步树
2.2.4.1 Step1:代码结构
Controller:负责从页面接收节点的id,返回该节点的所有子节点;
Service:实现查询逻辑,根据父节点id,查询所有的子节点
Mapper:基于BASEMapper实现
2.2.4.2 Step2:请求响应格式
请求路径 | /item/cat/list |
请求参数 | id=nodeId(首次加载生成一级目录时,默认id=0) |
响应格式 | {“id”:”1” “text”:”node1” “state”:”open} |
2.2.4.3 Step3:创建EUTreeNode类
在ego-base工程中创建。
//自定义异步树节点结构 public class EUTreeNode { private long id; private String text; private String state; //补全get、set方法 } |
2.2.4.4 Step4:创建ItemCat类
--在ego-base中创建
@TableName(value="tb_item_cat") public class ItemCat {
@TableId(value="id",type=IdType.AUTO) private Long id;
@TableField(value="parent_id") private Long parentId;
private String name;
private int status; @TableField(value="sort_order") private int sortOrder;
@TableField(value="is_parent") private byte isParent;
private Date created;
private Date updated;
public ItemCat() { super();
}
public Long getId() { return id; }
// 补全get、set方法
}
|
2.2.4.5 Step5:创建ItemCatMapper接口
--在ego-base中创建
package cn.gzsxt.base.mapper;
import com.baomidou.mybatisplus.mapper.BaseMapper;
import cn.gzsxt.base.pojo.ItemCat;
public interface ItemCatMapper extends BaseMapper<ItemCat>{
}
|
2.2.4.6 Step6:创建ItemCatService接口及实现类
在ego-manager项目中创建。
package cn.gzsxt.manager.service.impl;
import java.util.ArrayList; import java.util.List;
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.mapper.EntityWrapper; import com.baomidou.mybatisplus.service.impl.ServiceImpl;
import cn.gzsxt.base.mapper.ItemCatMapper; import cn.gzsxt.base.pojo.ItemCat; import cn.gzsxt.base.vo.EUTreeNode; import cn.gzsxt.manager.service.ItemCatService;
@Service public class ItemCatServiceImpl extends ServiceImpl<ItemCatMapper, ItemCat> implements ItemCatService{
@Override public List<EUTreeNode> getByParentId(Long parentId) {
List<EUTreeNode> nodes = new ArrayList<>();
EntityWrapper<ItemCat> ew = new EntityWrapper<>(); ew.eq("parent_id", parentId);
List<ItemCat> selectList = selectList(ew);
EUTreeNode node = null;
for (ItemCat itemCat : selectList) { node = new EUTreeNode();
node.setId(itemCat.getId()); node.setText(itemCat.getName());
if(1==itemCat.getIsParent()){
node.setState("closed"); }else{ node.setState("open"); }
nodes.add(node); }
return nodes; } } |
2.2.4.7 Step7:创建ItemCatController类
package cn.gzsxt.manager.controller;
import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import cn.gzsxt.common.pojo.EUTreeNode; import cn.gzsxt.manager.service.ItemCatService;
@Controller @RequestMapping("/item/cat") public class ItemCatController { @Autowired private ItemCatService catService;
@RequestMapping(value="/list") @ResponseBody public List<EUTreeNode> initTreeByParentId(@RequestParam(defaultValue="0")Long id){ List<EUTreeNode> list = catService.getByParantId(id);
return list; } } |
2.3 保存类目id到页面表单
说明:当点击叶子节点时,将该节点的id值,保存到页面表单中。
|
类目id的值,保存在页面表单的位置:
|
3 第二部分:实现商品图片上传功能
3.1 传统上传方式的问题
在传统上传方式中,在项目的跟目录下创建upload目录,将图片上传到tomcat服务器中。
|
但是在分布式环境下,是有多个Tomcat存在的,当把图片直接上传到Tomcat服务器时,容易出现图片丢失的问题。
|
3.2 分布式系统图片上传方案
3.2.1 思路分析
直接将图片上传到一个指定的目录,访问、下载图片都访问这个目录。
由于项目最终是要部署到Linux环境,所以直接将图片上传到Linux服务器。
|
问题:那如何将图片上传到Linux呢?
答:使用vsftpd组件,实现文件传输。
3.2.2 vsftpd简介
问题1:vsftpd是什么?
答:ftp(File Transfer Protocol)文件传输协议。(实现不同操作系统之间文件的传输)
vsftpd是一个基于ftp协议的文件传输服务器软件。
问题2:vsftpd作用是什么?
答:传输文件的文件服务器。(跨平台、跨操作系统)
问题3:如何使用?
答:服务端:在linux安装vsftpd软件,开启服务。
客户端:通过FtpClient客户端建立和服务器的连接,向服务器发送请求。
3.3 实现步骤说明
(1)在Linux上安装vsftpd服务。
(2)根据图片的地址访问图片。(最终保存到数据库的是图片的路径)
(3)web工程中实现图片上传。
3.4 实现步骤
3.4.1 第一部分:在Linux上部署vsftpd服务
思路 :(1)安装软件
(2)测试服务是否可用
3.4.1.1 第一步:安装vsftpd软件
[root@node0719 ~]# yum -y install vsftpd |
3.4.1.2 第二步:关闭匿名访问
修改vsftpd配置文件 vim /etc/vsftpd/vsftpd.conf
|
3.4.1.3 第三步:添加一个FTP用户
创建一个用户,专门用来访问vsftpd服务。
[root@node0719 ~]# useradd ftpuser [root@node0719 ~]# passwd ftpuser |
3.4.1.4 第四步:设置防火墙
vsftpd服务默认端口号为21,修改防火墙,开放此端口,重启防火墙。
[root@node0719 ~]# vim /etc/sysconfig/iptables [root@node0719 ~]# service iptables restart |
3.4.1.5 第五步:修改selinux(Linux安全内核系统)
(1)先查看selinux,默认是禁用了ftp访问的。
[root@bogon ~]# getsebool -a | grep ftp allow_ftpd_anon_write --> off allow_ftpd_full_access --> off allow_ftpd_use_cifs --> off allow_ftpd_use_nfs --> off ftp_home_dir --> off ftpd_connect_db --> off ftpd_use_passive_mode --> off httpd_enable_ftp_server --> off tftp_anon_write --> off |
(2)修改selinux,开放ftp访问权限
[root@bogon ~]# setsebool -P allow_ftpd_full_access on [root@bogon ~]# setsebool -P ftp_home_dir on |
3.4.1.6 第六步:启动vsftpd服务
[root@node0719 vsftpd]# service vsftpd start 为 vsftpd 启动 vsftpd: [确定] |
3.4.1.7 第七步:通过浏览器访问测试
访问地址:ftp://192.168.23.12:21,发现无法访问。
原因:被动模式下,数据传输服务被防火墙拦截了。
(1)被动模式
第二次请求过程中,客户端跟服务端建立数据通道;
服务端被动将数据响应给客户端。
第二次请求数据传输,会随机生成一个服务端口。被防火墙禁用。
|
(2)主动模式
服务端主动向客户端发送数据,会被客户端的防火墙禁掉。
多数客户端不支持主动模式,不安全。
|
3.4.1.8 第八步:配置被动模式
(1)编辑/etc/vsftpd/vsftpd.conf文件
[root@bogon ~]# vim /etc/vsftpd/vsftpd.conf |
(2)添加防火墙范围设置(在文件尾部添加即可):
pasv_min_port=30000 pasv_max_port=30999 |
(3)修改防火墙,开启30000:30999之间所有的端口。(30000:30999不行用30000-30999 tcp)
(4)重启防火墙。 service iptables restart
(5)重启vsftpd服务 service vsftpd restart
|
再次访问浏览器,发现可以正常连接了。
|
3.4.1.9 第九步:java代码测试上传功能
Java代码中,是通过FtpClient客户端建立和服务端的连接的。在ego-base工程中测试。
(1)在ego-base中添加ftp服务的依赖。
<dependency> <groupId>commons-net</groupId> <artifactId>commons-net</artifactId> </dependency> |
(2)创建测试类
说明:使用ftpuser用户上传。指定上从目录/home/ftpuser/ego/images
注意:为了保证ftpuser有这个目录下的写权限,我们要用ftpuser用户创建这个目录。
su命令:切换用户
[root@node0719 ~]#su ftpuser [ftpuser@node0719 ~]#mkdir -p /home/ftpuser/ego/images |
测试类TestFtp
package cn.gzsxt.manager.test;
import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.net.SocketException;
import org.apache.commons.net.ftp.FTP; import org.apache.commons.net.ftp.FTPClient;
public class TestFtp {
static String baseUrl = "/home/ftpuser/ego/images"; public static void main(String[] args) { //1、建立和服务端的连接 FTPClient client = new FTPClient(); try { client.connect("192.168.23.12", 21); //2、身份认证 client.login("ftpuser", "ftpuser"); //3、指定源文件 File file = new File("F:\\图片\\5b7a8115N89613314.jpg"); InputStream local = new FileInputStream(file); //4、指定文件上传的方式 二进制字节码 client.setFileType(FTP.BINARY_FILE_TYPE); //5、指定上传目录 默认是/home/ftpuser,即ftpuser用户的家目录 // 切换到ftpuser用户来创建目录。 /home/ftpuser/ego/images/ client.changeWorkingDirectory("/home/ftpuser/ego/images"); //6、设置文件上传的模式,指定为被动模式 client.enterLocalPassiveMode();
boolean flag = client.storeFile("test.jpg", local); if(flag){ System.out.println("上传成功"); }else{ System.out.println("上传失败"); } } catch (SocketException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } } |
3.4.1.10 封装FTPUtils工具类
package cn.gzsxt.base.utils;
import java.io.IOException; import java.io.InputStream;
import org.apache.commons.net.ftp.FTP; import org.apache.commons.net.ftp.FTPClient;
public class FtpUtils {
FTPClient client = null;
/** * 文件上传 * @param hostName ftp主机名 * @param port ftp主机端口 * @param username 上传用户名 * @param password 上传用户密码 * @param basePath 上传基础路径 * @param filePath 文件存放路径 * @param remoteFileName 上传后文件名称 * @param in 文件输入流 * @return */ public static boolean upload(String hostName,int port,String username,String password,String basePath, String filePath,String remoteFileName,InputStream in){
//1、创建客户端 FTPClient client = new FTPClient();
try {
//2、建立和服务端的链接 client.connect(hostName, port);
//3、登陆服务端 client.login(username, password);
//4、指定图片上传的方式为二进制,即字节流 client.setFileType(FTP.BINARY_FILE_TYPE);
//5、指定上传的访问模式为被动模式 说明:大部分的操作系统,默认的都是被动模式,并且禁用了主动了模式 client.enterLocalPassiveMode();
//6、指定上传的目录 默认目录 是当前ftpuser用户的家目录 boolean flag = client.changeWorkingDirectory(basePath+filePath);
//如果切换目录失败,则创建指定的目录 if(!flag){
//创建目录失败,则可能是存在没有创建的父目录 if(!client.makeDirectory(basePath+filePath)){ String tempPath = basePath;
String[] split = filePath.split("/"); for (String string : split) { if(null!=string && !"".equals(string)){ tempPath = tempPath+"/"+string;
//先判断第一层路径是否存在,如果不存在,则创建 if(!client.changeWorkingDirectory(tempPath)){ //如果创建第一层路径成功,则判断是否能切换到这一层路径 if(client.makeDirectory(tempPath)){ //切换失败,则返回false if(!client.changeWorkingDirectory(tempPath)){ return false; } //如果创建第一层路径失败,则直接返回false }else{
return false; } }
//如果有空路径,则直接跳过 }else{ continue; } } }else{ //创建成功,则直接切换到指定的目录 if(!client.changeWorkingDirectory(basePath+filePath)){ return false; } }
}
//8、上传 boolean result = client.storeFile(remoteFileName, in);
return result;
} catch (Exception e) {
e.printStackTrace();
return false; }finally { //9,退出登录,并关闭连接 try { if(client.logout()){ client.disconnect();
} } catch (IOException e) { e.printStackTrace(); } }
} }
|
3.4.2 第二部分:搭建图片服务器访问图片
我们知道,图片等静态资源需要服务器加载,才能被访问到。
这里我们选择Tengine做服务器,来加载图片。
问题1:Tengine是什么?
答:Tengine是web服务器。
问题2:web服务器常用种类?
答:apache、IIS、nginx
问题3:web服务器和web应用服务器的区别?
答:web应用服务器,是用来处理动态请求,常见的以tomcat、jetty等servlet容器为代表。可以用来部署应用。
web服务器,只能处理静态资源请求。
如果要处理动态请求,需要通过其动态代理功能实现。
问题3:为什么不用Tomcat呢?
答:(1)Tomcat是servlet容器,处理静态资源的速度远低于Tengine。
(2)Tomcat的并发连接数,远远低于Tengine。
所以,这里我们选择Tengine做图片服务器。
搭建步骤说明:
(1)安装Tengine。(源码安装)
(2)配置图片服务。
3.4.2.1 第一步:上传、解压
[root@node0719 ~]# tar -zxvf tengine-2.1.0.tar.gz |
3.4.2.2 第二步:预编译
预编译作用:检查编译过程中所需要的依赖、环境。
依次安装预编译过程中,所需要的环境。(根据个人虚拟机安装所缺环境)
[root@node07192 ~]# cd tengine-2.1.0 [root@node07192 tengine-2.1.0]# ./configure |
(1)缺少c编译环境
[root@node07192 tengine-2.1.0]# yum -y install gcc-c++ |
(2)缺少pcre环境
[root@node07192 tengine-2.1.0]# yum -y install pcre-devel |
(3)缺少openssl环境
[root@node07192 tengine-2.1.0]# yum install -y openssl openssl-devel |
(4)缺少zlib环境
[root@node07192 tengine-2.1.0]# yum install -y zlib zlib-devel |
3.4.2.3 第三步:编译
[root@node07192 tengine-2.1.0]# make |
3.4.2.4 第四步:安装
默认安装路径/usr/local/nginx/
[root@node07192 tengine-2.1.0]# make install |
3.4.2.5 第五步:启动Tengine服务器
[root@node07192 tengine-2.1.0]# cd /usr/local/nginx/sbin/ [root@node07192 sbin]# ./nginx |
3.4.2.6 第六步:访问测试
(1)查看配置文件。默认服务端口是80
[root@node07192 sbin]# cd ../conf [root@node07192 conf]# vim nginx.conf
|
(2)修改防火墙,开发80端口。重启防火墙
[root@node07192 conf]# vim /etc/sysconfig/iptables [root@node07192 conf]# service iptables restart |
(3)浏览器访问地址 http://192.168.23.12:80
|
3.4.2.7 第七步:配置图片服务
(1)修改/conf/nginx.conf文件。指定图片根路径和服务端口
|
(2)重启tengine服务器
[root@node07192 sbin]# ./nginx -s reload |
(3)浏览器访问图片
注意:服务器加载的根路径是/home/ftpuser/ego
所以浏览器中访问图片的目录为/images/+图片名称.jpg
|
(4)解决访问图片的权限问题
在第六步中,我们访问的页面是/html/index.html
所以:我们只需要将图片的权限修改为index.html一致即可。
查看/html/index.html的权限
|
修改ftpuser目录的权限为可读、可执行
[root@node07192 nginx]# chmod 705 /home/ftpuser |
(5)重新访问图片,成功!!!
图片访问路径说明:
图片真实目录时 /home/ftpuser/ego/images
在Tengine中,设置得图片资源的根目录为 /home/ftpuser/ego
意味着,我们每次请求图片的时候,是直接到/home/ftpuser/ego这个目录下,找图片的。因此图片的访问路径中,/home/ftpuser/ego这个路径是要省掉的。
3.4.3 第三部分:SpringMVC实现上传
3.4.3.1 思路
(1)使用Springmvc上传组件,从页面表单接收图片
(2)使用vsftpd组件,将图片上传到Linux服务器。
(a)、服务端:在Linux上安装ftp服务端vsftpd软件,并开启服务。
(b)、客户端:在java代码中使用FtpClient客户端建立与服务器的连接
(3)返回值:返回图片上传之后的访问路径。
为什么?
因为保存图片到数据库的时候,保存的就是图片的访问路径。
3.4.3.2 前端js实现
前端使用kindeditor,初始化上传组件
|
调用上传组件的初始化方法:
|
上传组件在common.js中定义
|
上传组件的初始化方法init
|
3.4.3.3 后台java实现
3.4.3.3.1 代码结构
Controller:从表单接收图片,返回图片的回调地址
Service:创建FtpClient客户端,将图片直接上传到Linux服务器
3.4.3.3.2 请求响应格式
请求路径 | /pic/upload |
请求方式 | Post |
请求参数 | uploadFile |
返回值结构 | 参考Kindeditor官方文档(http://kindeditor.net/docs/upload.html) |
Kindeditor官方文档要求的返回格式类型
|
3.4.3.3.3 定义返回值类型
在ego-base工程中定义。
package cn.gzsxt.base.pojo;
/** * KindEditer文件上传返回格式 * @author ccnulyq * */ public class UploadResult {
private int error; //0 表示成功 1表示失败
private String url; //成功时,图片的访问地址
private String message; //失败时,错误信息
public PictureResult() { super(); } //补充get、set方法 } |
3.4.3.3.4 在ego-manager工程中添加Springmvc上传组件及Pom依赖
(1)、修改spring-mvc.xml,添加上传组件
<!-- 定义文件上传解析器 --> <bean name="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"> <!-- 设定默认编码 --> <property name="defaultEncoding" value="UTF-8"></property> <!-- 设定文件上传的最大值5MB,5*1024*1024 --> <property name="maxUploadSize" value="5242880"></property> </bean> |
(2)、修改pom.xml,添加上传依赖common-fileupload.jar
<!-- 文件上传组件 --> <dependency> <groupId>commons-fileupload</groupId> <artifactId>commons-fileupload</artifactId> </dependency> |
(3)将vsftpd服务端请求参数写到properties配置文件中
#图片上传基本配置 FTP_HOST=192.168.4.253 FTP_PORT=21 FTP_USER=ftpuser FTP_PASSWD=ftpuser FTP_BASE_URL=/home/ftpuser/ego/images PICTURE_BASE_URL=http://192.168.4.253/images |
3.4.3.3.5 Service层代码实现
--创建UploadService接口及其实现类
package cn.gzsxt.manager.service.impl;
import java.io.IOException; import java.text.SimpleDateFormat; import java.util.Date;
import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile;
import cn.gzsxt.base.utils.FtpUtils; import cn.gzsxt.base.utils.IDUtils; import cn.gzsxt.base.vo.UploadResult; import cn.gzsxt.manager.service.UploadService;
@Service public class UploadServiceImpl implements UploadService{
/* * FTP_HOST=192.168.4.253 FTP_PORT=21 FTP_USERNAME=ftpuser FTP_PASSWORD=ftpuser FTP_BASE_URL=/home/ftpuser/ego/images PICTURE_BASE_URL=http://192.168.4.253/images */ @Value("${FTP_HOST}") private String FTP_HOST;
@Value("${FTP_PORT}") private Integer FTP_PORT;
@Value("${FTP_USER}") private String FTP_USERNAME;
@Value("${FTP_PASSWD}") private String FTP_PASSWORD;
@Value("${FTP_BASE_URL}") private String FTP_BASE_URL;
@Value("${PICTURE_BASE_URL}") private String PICTURE_BASE_URL;
@Override public UploadResult upload(MultipartFile file) {
UploadResult result = new UploadResult();
//需求:将上传的图片按日期来分类 /2019/02/25/1.jpg
Date date = new Date();
//获取日期的目录格式 String filePath = "/"+ new SimpleDateFormat("yyyy").format(date)+ "/"+new SimpleDateFormat("MM").format(date)+ "/"+new SimpleDateFormat("dd").format(date);
//获取图片的类型 .jpg .png String originalFilename = file.getOriginalFilename();
String filtType = originalFilename.substring(originalFilename.lastIndexOf("."));
String remoteFileName = IDUtils.getImageName()+filtType;
try { boolean upload = FtpUtils.upload(FTP_HOST, FTP_PORT, FTP_USERNAME, FTP_PASSWORD, FTP_BASE_URL, filePath, remoteFileName, file.getInputStream());
if(upload){ result.setError(0); // 192.168.4.253/images /2019/02/25 / 111111.jpg result.setUrl(PICTURE_BASE_URL+filePath+"/"+remoteFileName); }else{ result.setError(1); result.setMessage("上传失败,请稍后再试!"); }
} catch (IOException e) {
e.printStackTrace();
result.setError(1); result.setMessage("上传失败,请稍后再试!"); }
return result; }
} |
3.4.3.3.6 ID生成工具类
package org.ranger.base.utils;
import java.util.Random;
/** * 各种id生成策略 */ public class IDUtils {
/** * 图片名生成 */ public static String getImageName() { //取当前时间的长整形值包含毫秒 long millis = System.currentTimeMillis(); //long millis = System.nanoTime(); //加上三位随机数 Random random = new Random(); int end3 = random.nextInt(999); //如果不足三位前面补0 String str = millis + String.format("%03d", end3);
return str; }
/** * 商品id生成 */ public static long getItemId() { //取当前时间的长整形值包含毫秒 long millis = System.currentTimeMillis(); //long millis = System.nanoTime(); //加上两位随机数 Random random = new Random(); int end2 = random.nextInt(99); //如果不足两位前面补0 String str = millis + String.format("%02d", end2); long id = new Long(str); return id; } }
|
3.4.3.3.7 Controller层代码实现
--创建UploadController类
package cn.gzsxt.manager.controller;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.multipart.MultipartFile;
import cn.gzsxt.base.vo.UploadResult; import cn.gzsxt.manager.service.UploadService;
@Controller public class UploadController {
@Autowired private UploadService uploadService;
@RequestMapping(value="/pic/upload",method=RequestMethod.POST) @ResponseBody public UploadResult upload(MultipartFile uploadFile){ UploadResult result = uploadService.upload(uploadFile);
return result; } }
|
3.4.3.3.8 测试结果,上传成功!!!
|
3.4.3.4 将上传结果保存到页面表单域
|
页面效果
|
4 第三部分:kindEditor编辑商品属性
纯前端js实现,不需要java后台代码支持。
原理:内置了一个HTML编辑器,将HTML页面转换成文本类型,将值传给指定的元素。
|
5 第四部分:商品规格参数
|
5.1 格式
规格分组1
|-规格项1:规格值1
|-规格项2:规格值2
|-规格项n:规格值n
规格分组2
|-规格项11:规格值11
|-规格项22:规格值22
|-规格项nn:规格值nn
规格分组3
|-规格项112:规格值112
|-规格项222:规格值222
|-规格项nnn:规格值nnn
5.2 特点
(1)每一类商品的规格分组是相同的。
(2)每一个规格分组对应多个规格项。
(3)每一个商品的规格值不同。
5.3 设计思路
|
(1)给商品的每一个分类创建一个规格参数模板。(tb_item_param)
(2)添加商品的时候,根据该类商品的参数模板,填写规格值。
(3)将页面填写的规格值,保存到数据库。(tb_item_param_item)
5.4 实现流程
(1)添加商品规格参数模板
(2)根据规格参数模板生成规格值
5.4.1 第一部分:创建规格参数模板
5.4.1.1 第一步:判断是否已经添加规格参数模板
(1)js实现
|
(2)请求响应格式
请求路径 | /item/param/query/itemcatid/{itemCatId} |
请求方式 | GET |
请求参数 | /{itemCatId} 路径变量,商品类目id |
响应结果 | {status:200 data:data} |
(3)创建ItemParam类
package cn.gzsxt.base.pojo;
import java.util.Date;
import com.baomidou.mybatisplus.annotations.TableField; import com.baomidou.mybatisplus.annotations.TableId; import com.baomidou.mybatisplus.annotations.TableName; import com.baomidou.mybatisplus.enums.IdType;
@TableName(value="tb_item_param") public class ItemParam {
@TableId(value="id",type=IdType.AUTO) private Long id;
@TableField(value="item_cat_id") private long itemCatId;
@TableField(value="param_data") private String paramData;
private Date created;
private Date updated;
public ItemParam() { super(); }
//补全get、set方法 }
|
(4)创建EgoResult返回值类
--说明:在ego-base中定义,并修改pom文件,添加json依赖
package cn.gzsxt.base.vo;
import java.util.List;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper;
/** * 好易购商城自定义响应结构 */ public class EgoResult {
// 定义jackson对象 private static final ObjectMapper MAPPER = new ObjectMapper();
// 响应业务状态 private Integer status;
// 响应消息 private String msg;
// 响应中的数据 private Object data;
public static EgoResult build(Integer status, String msg, Object data) { return new EgoResult(status, msg, data); }
public static EgoResult ok(Object data) { return new EgoResult(data); }
public static EgoResult ok() { return new EgoResult(null); }
public EgoResult() {
}
public static EgoResult build(Integer status, String msg) { return new EgoResult(status, msg, null); }
public EgoResult(Integer status, String msg, Object data) { this.status = status; this.msg = msg; this.data = data; }
public EgoResult(Object data) { this.status = 200; this.msg = "OK"; this.data = data; }
// public Boolean isOK() { // return this.status == 200; // }
public Integer getStatus() { return status; }
public void setStatus(Integer status) { this.status = status; }
public String getMsg() { return msg; }
public void setMsg(String msg) { this.msg = msg; }
public Object getData() { return data; }
public void setData(Object data) { this.data = data; }
/** * 将json结果集转化为EgoResult对象 * * @param jsonData json数据 * @param clazz EgoResult中的object类型 * @return */ public static EgoResult formatToPojo(String jsonData, Class<?> clazz) { try { if (clazz == null) { return MAPPER.readValue(jsonData, EgoResult.class); } JsonNode jsonNode = MAPPER.readTree(jsonData); JsonNode data = jsonNode.get("data"); Object obj = null; if (clazz != null) { if (data.isObject()) { obj = MAPPER.readValue(data.traverse(), clazz); } else if (data.isTextual()) { obj = MAPPER.readValue(data.asText(), clazz); } } return build(jsonNode.get("status").intValue(), jsonNode.get("msg").asText(), obj); } catch (Exception e) { return null; } }
/** * 没有object对象的转化 * * @param json * @return */ public static EgoResult format(String json) { try { return MAPPER.readValue(json, EgoResult.class); } catch (Exception e) { e.printStackTrace(); } return null; }
/** * Object是集合转化 * * @param jsonData json数据 * @param clazz 集合中的类型 * @return */ public static EgoResult formatToList(String jsonData, Class<?> clazz) { try { JsonNode jsonNode = MAPPER.readTree(jsonData); JsonNode data = jsonNode.get("data"); Object obj = null; if (data.isArray() && data.size() > 0) { obj = MAPPER.readValue(data.traverse(), MAPPER.getTypeFactory().constructCollectionType(List.class, clazz)); } return build(jsonNode.get("status").intValue(), jsonNode.get("msg").asText(), obj); } catch (Exception e) { return null; } } }
|
(5)创建ItemParamMapper接口
--说明:在ego-base中创建
package cn.gzsxt.base.mapper;
import java.util.List; import java.util.Map;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select;
import com.baomidou.mybatisplus.mapper.BaseMapper;
import cn.gzsxt.base.pojo.ItemParam;
public interface ItemParamMapper extends BaseMapper<ItemParam>{
}
|
(6)Service层实现
--创建ItemParamService接口及其实现类
package cn.gzsxt.manager.service.impl;
import java.util.Date; import java.util.List; import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional;
import com.baomidou.mybatisplus.mapper.EntityWrapper;
import cn.gzsxt.base.mapper.ItemParamMapper; import cn.gzsxt.base.pojo.ItemParam; import cn.gzsxt.base.vo.EUDataGridResult; import cn.gzsxt.base.vo.EgoResult; import cn.gzsxt.manager.service.ItemParamService;
@Service public class ItemParamServiceImpl implements ItemParamService{
@Autowired private ItemParamMapper itemParamMapper;
@Override public EgoResult getByItemCatId(long catId) {
EntityWrapper<ItemParam> ew = new EntityWrapper<>();
ew.eq("item_cat_id", catId);
List<ItemParam> selectList = itemParamMapper.selectList(ew);
if(null!=selectList && selectList.size()>0){
return EgoResult.ok(selectList.get(0)); }
return EgoResult.build(400, "没有查到该类商品的模板"); } } |
(4)Controller层实现
--创建ItemParamController类
package cn.gzsxt.manager.controller;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import cn.gzsxt.base.vo.EUDataGridResult; import cn.gzsxt.base.vo.EgoResult; import cn.gzsxt.manager.service.ItemParamService;
@Controller @RequestMapping("/item/param") public class ItemParamController {
@Autowired private ItemParamService service;
@RequestMapping("/query/itemcatid/{itemcatid}") @ResponseBody public EgoResult selectByCatId(@PathVariable("itemcatid")Long itemCatId){ EgoResult result = service.getByItemCatId(itemCatId);
return result; } } |
5.4.1.2 第二步:添加规格参数模板
(1)前端js实现
|
|
(2)后台java代码实现
请求路径 | /item/param/save/{cid} |
请求方式 | POST |
请求参数 | /{cid} (类目id) ;paramData (json格式) |
响应结果 | EgoResult |
(3)Service层实现
--修改ItemParamService接口及其实现类,添加保存方法
//只有配置了rollbackFor = Exception.class,在service对异常进行处理时,才会有回滚 @Transactional(rollbackFor = Exception.class) @Override public EgoResult save(Long itemCatId, String paramData) {
try { ItemParam entity = new ItemParam(); entity.setItemCatId(itemCatId); entity.setParamData(paramData); entity.setCreated(new Date()); entity.setUpdated(entity.getCreated());
itemParamMapper.insert(entity);
return EgoResult.ok(); } catch (Exception e) {
e.printStackTrace();
return EgoResult.build(400, "保存失败"); } } |
(4)Controller层实现
--修改ItemParamController,添加保存方法
@RequestMapping("/save/{cid}") @ResponseBody public EgoResult saveItemParam(@PathVariable Long cid, String paramData) { EgoResult result = itemParamService.saveItemParam(cid, paramData); } |
5.4.2 第二部分:根据参数模板生成商品规格参数值表单
新增商品 --> 选择类目 --> 查找类目所对应的模板 --> 生成表单
(1)前端js实现
|
|
(2)java后台(已实现)
5.4.2.1 第一步:修改Controller代码
@RequestMapping("/save/{catId}") @ResponseBody public EgoResult save(@PathVariable("catId")Long catId,String paramData){ EgoResult result = service.save(catId, paramData);
return result; } |
5.4.2.2 第二步:修改Service代码
@Override public EgoResult save(Long catId, String paramData) {
ItemParam param = new ItemParam(); param.setItemCatId(catId); param.setParamData(paramData); param.setCreated(new Date()); param.setUpdated(param.getCreated());
mapper.insert(param);
return EgoResult.ok(); }
|
6 第五部分:保存商品
保存商品,需要同时保存商品信息、商品的描述信息和商品的规格参数,分别对应表tb_item、tb_item_desc、tb_item_param_item三张表。
6.1 前端js实现
6.1.1 使用KindEditor富文本编辑器,编辑商品描述信息
|
6.1.2 将商品规格参数表单数据,转换成json格式
|
6.1.3 提交保存商品请求
|
6.2 后台java实现
6.2.1 请求响应格式
请求路径 | /item/save |
请求方式 | POST |
请求参数 | TbItem、desc、itemParams |
响应格式 | {“status”:200 data:data} 参考http响应格式 |
6.2.2 代码结构
Controller:从表单接收数据,封装到JavaBean中
Service:实现保存逻辑,防止事务一致性问题。
Mapper:Mybatis-plus实现
6.2.3 创建pojo
--在ego-base工程中创建
(1)创建ItemDesc类
package cn.gzsxt.base.pojo;
import java.util.Date;
import com.baomidou.mybatisplus.annotations.TableField; import com.baomidou.mybatisplus.annotations.TableId; import com.baomidou.mybatisplus.annotations.TableName; import com.baomidou.mybatisplus.enums.IdType;
@TableName(value="tb_item_desc") public class ItemDesc {
@TableId(value="item_id",type=IdType.INPUT) private Long itemId;
@TableField(value="item_desc") private String itemDesc;
private Date created;
private Date updated;
public ItemDesc() { super(); }
// 补全get、set方法 } |
(2)创建ItemParamItem类
package cn.gzsxt.base.pojo;
import java.util.Date;
import com.baomidou.mybatisplus.annotations.TableField; import com.baomidou.mybatisplus.annotations.TableId; import com.baomidou.mybatisplus.annotations.TableName;
import com.baomidou.mybatisplus.enums.IdType;
/**商品规格参数值表 * * 商品的规格参数(商品的描述信息) * 做了水平拆表的处理。 * * 好处:减小商品的表的体积,让商品表查询效率更高 * * 什么情况下需要做水平拆表? * (1)大文本的字段。 * (2)这个大文本的字段不常用 * * @author ccnulyq * */
@TableName(value="tb_item_param_item") public class ItemParamItem {
@TableId(value="id",type=IdType.AUTO) private Long id;
@TableField(value="item_id") private long itemId;
@TableField(value="param_data") private String paramData;
private Date created;
private Date updated;
public ItemParamItem() { super();
}
// 补全get、set方法 }
|
6.2.4 创建对应的Mapper
--说明:在ego-base工程中创建
(1)创建ItemParamItemMapper接口
package cn.gzsxt.base.mapper;
import com.baomidou.mybatisplus.mapper.BaseMapper;
import cn.gzsxt.base.pojo.ItemParamItem;
public interface ItemParamItemMapper extends BaseMapper<ItemParamItem>{
} |
(2)创建ItemDescMapper接口
package cn.gzsxt.base.mapper;
import com.baomidou.mybatisplus.mapper.BaseMapper;
import cn.gzsxt.base.pojo.ItemDesc;
public interface ItemDescMapper extends BaseMapper<ItemDesc>{
} |
6.2.5 Service代码实现
--修改ItemService接口及其实现类,新增save方法
--注意:注入ItemDescMapper、ItemParamItemMapper
@Service public class ItemServiceImpl extends ServiceImpl<ItemMapper, Item> implements ItemService{
@Autowired private ItemDescMapper descMapper;
@Autowired private ItemParamItemMapper itemParamMapper;
@Transactional(rollbackFor=Exception.class) @Override public EgoResult save(Item item, String desc, String paramData) {
try {
long itemId = IDUtils.getItemId();
item.setStatus((byte) 1); item.setId(itemId); item.setCreated(new Date()); item.setUpdated(item.getCreated());
this.baseMapper.insert(item);
//保存商品的描述信息 ItemDesc itemDesc = new ItemDesc(); itemDesc.setItemId(itemId); itemDesc.setItemDesc(desc); itemDesc.setCreated(item.getCreated()); itemDesc.setUpdated(item.getUpdated()); descMapper.insert(itemDesc);
//保存商品的规格参数值 ItemParamItem paramItem = new ItemParamItem(); paramItem.setItemId(itemId); paramItem.setParamData(paramData); paramItem.setCreated(item.getCreated()); paramItem.setUpdated(item.getCreated());
itemParamMapper.insert(paramItem);
return EgoResult.ok();
} catch (Exception e) { e.printStackTrace(); }
return EgoResult.build(400, "保存失败,请稍后再试"); }
}
|
6.2.6 Controller代码实现
--修改ItemController类,新增save方法
@RequestMapping(value="/save",method=RequestMethod.POST) @ResponseBody public EgoResult save(Item item,String desc,String itemParams){ EgoResult result = itemService.save(item, desc, itemParams);
return result; } |
7 商品规格参数列表实现
7.1 思路
商品规格参数列表的数据,分别存在了tb_item_param和tb_item_cat两张表中,因此在mapper层,需要自定义查询方法,并分页
|
7.2 前端js实现
使用的是easyu-datagrid插件,使用方法参考商品列表实现(第一天内容)。
7.3 后台代码实现
7.3.1 确定请求响应格式
请求路径 | /item/param/list |
请求方式 | Get |
请求参数 | page、rows(分页) |
返回值类型 | EUDataGridResult类型 |
7.3.2 Mapper实现
--说明:连表查询下,需要自定义查询方法,基于注解实现
--修改ItemParamMapper接口,新增查询方法
public interface ItemParamMapper extends BaseMapper<ItemParam>{
@Select(value="select p.id,p.item_cat_id as itemCatId,t.name as itemCatName,p.param_data as paramData,p.created,p.updated " + "from tb_item_param p left join tb_item_cat t on p.item_cat_id = t.id " + "limit ${start},${pageSize}") List<Map<String, Object>> listAndPage(@Param("start")int start,@Param("pageSize")int pageSize); } |
7.3.3 Service层实现
--修改ItemParamService接口及其实现类
@Override public EUDataGridResult listAndPage(int curPage, int pageSize) {
List<Map<String, Object>> list = itemParamMapper.listAndPage((curPage-1)*pageSize, pageSize);
Integer count = itemParamMapper.selectCount(null);
EUDataGridResult result = new EUDataGridResult();
result.setRows(list); result.setTotal(count);
return result; }
|
7.3.4 Controller层实现
--修改ItemParamController接口
@RequestMapping("/list") @ResponseBody public EUDataGridResult listAndPage(Integer page,Integer rows){ EUDataGridResult result = service.listAndPage(page, rows);
return result; } |
7.4 访问测试
|
规格参数列表实现!!!