|
易购商城 |
第二天 |
目 录
1 课程计划 3
1.1 目标 3
1.2 功能分析 3
1.2.1 相关数据表 3
1.2.2 实现的思路 3
2 第一部分:实现商品类目选择功能 4
2.1 需求分析 4
2.2 实现步骤 5
2.2.1 第一步:加载树控件 5
2.2.2 第二步:确定加载树请求的参数 6
2.2.3 第三步:确定树节点结构 6
2.2.4 第四步:java代码实现异步树 6
2.3 保存类目id到页面表单 10
3 第二部分:实现商品图片上传功能 11
3.1 传统上传方式的问题 11
3.2 分布式系统图片上传方案 11
3.2.1 思路分析 11
3.2.2 vsftpd简介 12
3.3 实现步骤说明 12
3.4 实现步骤 13
3.4.1 第一部分:在Linux上部署vsftpd服务 13
3.4.2 第二部分:搭建图片服务器访问图片 21
3.4.3 第三部分:SpringMVC实现上传 26
4 第三部分:kindEditor编辑商品属性 35
5 第四部分:商品规格参数 36
5.1 格式 37
5.2 特点 37
5.3 设计思路 38
5.4 实现流程 38
5.4.1 第一部分:创建规格参数模板 38
5.4.2 第二部分:根据参数模板生成商品规格参数值表单 48
6 第五部分:保存商品 50
6.1 前端js实现 51
6.1.1 使用KindEditor富文本编辑器,编辑商品描述信息 51
6.1.2 将商品规格参数表单数据,转换成json格式 51
6.1.3 提交保存商品请求 51
6.2 后台java实现 52
6.2.1 请求响应格式 52
6.2.2 代码结构 52
6.2.3 创建pojo 52
6.2.4 创建对应的Mapper 54
6.2.5 Service代码实现 55
6.2.6 Controller代码实现 56
7 商品规格参数列表实现 57
7.1 思路 57
7.2 前端js实现 57
7.3 后台代码实现 57
7.3.1 确定请求响应格式 57
7.3.2 Mapper实现 58
7.3.3 Service层实现 58
7.3.4 Controller层实现 59
7.4 访问测试 59
- 课程计划
- 目标
需求:完成商品添加业务。
第一步:理解商品模块的业务(通过ER图理解)
考核的知识点,通过数据库结构快速生成ER图。同自己的理解画好关系。
问题:为什么数据库表不建外键约束?
答:外键约束确保了数据的完整性,但是也约束数据的灵活性。如果将外键在数据里创建,不适合需求多变的项目。
第二步:查询商品类目(以树形结构显示,UI设计的要求)
考核的知识点,就是如何封装一个树状的数据结构。
第三步:实现图片的上传(要求:上传到指定的FTP服务器)
考核的知识点:
- Linux系统的使用
- tengine 纯HTTP的web服务器
- SpringMVC的上传功能
- FTP的数据传到
第四步:设置类目的参数规格模板
考核的知识点:JSON数据格式转换。
第五步:商品的保存
考核的知识点:使用MybatisPlus插入数据
-
- 功能分析
- 相关数据表
- 功能分析
说明:与商品模块有关的表,总共有5张。关系如下:
-
-
- 实现的思路
-
(1)每个商品都有一个分类,所以要实现商品类目选择功能。
(2)商品有一个图片属性,所以要实现图片上传的功能。
(3)每个商品都有规格参数,所以要实现商品规格参数编辑功能。
(4)将商品的规格参数、商品详情、商品信息分别保存到三张表中。
- 第一部分:实现商品类目选择功能
- 需求分析
在商品页面,点击”选择类目”按钮,生成商品类目异步树。
对应的数据库表为tb_item_cat,表结构为:
实现的思路:
业务理解:在加载树控件的时候,将所有顶级的类目显示出来。所以的子节点在展开的时候传入节点对应的类目编号(ID),查询对应的类目数据。
根据业务理解:
(1)加载树控件。(本项目使用的是easyui-tree插件,第一次传递的cid=0)
(2)确定异步树请求的参数及返回的节点结构。(要构建easyui-tree对应的业务模型VO,id、text、status)
(3)请求数据库,生成树结构。(根据parent_id字段查询子节点实现。)
-
- 实现步骤
- 第一步:加载树控件
- 实现步骤
(1)定义类目选择的按钮。(点击按钮,加载异步树控件)
(2)加载异步树控件
查看EasyUI的API文档,我们知道:url是请求路径。
-
-
- 第二步:确定加载树请求的参数
-
查看API文档,我们知道请求的参数名是id,是当前节点的id值。
-
-
- 第三步:确定树节点结构
-
查看API文档,节点包括id、text、state三个基本属性。
-
-
- 第四步:java代码实现异步树
- Step1:代码结构
- 第四步:java代码实现异步树
-
Controller:负载从页面接收节点的id,返回该节点的所有子节点;
Service:实现查询逻辑,根据父节点id,查询所有的子节点
Mapper:基于BASEMapper实现
-
-
-
- Step2:请求响应格式
-
-
请求路径 | /item/cat/list |
请求参数 | id=nodeId(首次加载生成一级目录时,默认id=0) |
响应格式 | {“id”:”1” “text”:”node1” “state”:”open} |
-
-
-
- Step3:创建EUTreeNode类
-
-
在ego-base工程中创建。
//自定义异步树节点结构 public class EUTreeNode { private long id; private String text; private String state; //补全get、set方法 } |
-
-
-
- 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方法
}
|
-
-
-
- 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>{
}
|
-
-
-
- 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; } } |
-
-
-
- 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; } } |
-
- 保存类目id到页面表单
说明:当点击叶子节点时,将该节点的id值,保存到页面表单中。
类目id的值,保存在页面表单的位置:
- 第二部分:实现商品图片上传功能
- 传统上传方式的问题
在传统上传方式中,在项目的跟目录下创建upload目录,将图片上传到tomcat服务器中。
但是在分布式环境下,是有多个Tomcat存在的,当把图片直接上传到Tomcat服务器时,容易出现图片丢失的问题。
-
- 分布式系统图片上传方案
- 思路分析
- 分布式系统图片上传方案
直接将图片上传到一个指定的目录,访问、下载图片都访问这个目录。
由于项目最终是要部署到Linux环境,所以直接将图片上传到Linux服务器。
问题:那如何将图片上传到Linux呢?
答:使用vsftpd组件,实现文件传输。
-
-
- vsftpd简介
-
问题1:vsftpd是什么?
答:ftp(File Transfer Protocol)文件传输协议。(实现不同操作系统之间文件的传输)
vsftpd是一个基于ftp协议的文件传输服务器软件。
问题2:vsftpd作用是什么?
答:传输文件的文件服务器。(跨平台、跨操作系统)
问题3:如何使用?
答:服务端:在linux安装vsftpd软件,开启服务。
客户端:通过FtpClient客户端建立和服务器的连接,向服务器发送请求。
-
- 实现步骤说明
(1)在Linux上安装vsftpd服务。
(2)根据图片的地址访问图片。(最终保存到数据库的是图片的路径)
(3)web工程中实现图片上传。
-
- 实现步骤
- 第一部分:在Linux上部署vsftpd服务
- 实现步骤
思路 :(1)安装软件
(2)测试服务是否可用
-
-
-
- 第一步:安装vsftpd软件
-
-
[root@node0719 ~]# yum -y install vsftpd |
-
-
-
- 第二步:关闭匿名访问
-
-
修改vsftpd配置文件 vim /etc/vsftpd/vsftpd.conf
-
-
-
- 第三步:添加一个FTP用户
-
-
创建一个用户,专门用来访问vsftpd服务。
[root@node0719 ~]# useradd ftpuser [root@node0719 ~]# passwd ftpuser |
-
-
-
- 第四步:设置防火墙
-
-
vsftpd服务默认端口号为21,修改防火墙,开放此端口,重启防火墙。
[root@node0719 ~]# vim /etc/sysconfig/iptables [root@node0719 ~]# service iptables restart |
-
-
-
- 第五步:修改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 |
-
-
-
- 第六步:启动vsftpd服务
-
-
[root@node0719 vsftpd]# service vsftpd start 为 vsftpd 启动 vsftpd: [确定] |
-
-
-
- 第七步:通过浏览器访问测试
-
-
访问地址:ftp://192.168.23.12:21,发现无法访问。
原因:被动模式下,数据传输服务被防火墙拦截了。
(1)被动模式
第二次请求过程中,客户端跟服务端建立数据通道;
服务端被动将数据响应给客户端。
第二次请求数据传输,会随机生成一个服务端口。被防火墙禁用。
(2)主动模式
服务端主动向客户端发送数据,会被客户端的防火墙禁掉。
多数客户端不支持主动模式,不安全。
-
-
-
- 第八步:配置被动模式
-
-
(1)编辑/etc/vsftpd/vsftpd.conf文件
[root@bogon ~]# vim /etc/vsftpd/vsftpd.conf |
(2)添加防火墙范围设置(在文件尾部添加即可):
pasv_min_port=30000 pasv_max_port=30999 |
(3)修改防火墙,开启30000:30999之间所有的端口。
(4)重启防火墙。
(5)重启vsftpd服务
再次访问浏览器,发现可以正常连接了。
-
-
-
- 第九步: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(); } } } |
-
-
-
- 封装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(); } }
} }
|
-
-
- 第二部分:搭建图片服务器访问图片
-
我们知道,图片等静态资源需要服务器加载,才能被访问到。
这里我们选择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)配置图片服务。
-
-
-
- 第一步:上传、解压
-
-
[root@node0719 ~]# tar -zxvf tengine-2.1.0.tar.gz |
-
-
-
- 第二步:预编译
-
-
预编译作用:检查编译过程中所需要的依赖、环境。
依次安装预编译过程中,所需要的环境。(根据个人虚拟机安装所缺环境)
[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 |
-
-
-
- 第三步:编译
-
-
[root@node07192 tengine-2.1.0]# make |
-
-
-
- 第四步:安装
-
-
默认安装路径/usr/local/nginx/
[root@node07192 tengine-2.1.0]# make install |
-
-
-
- 第五步:启动Tengine服务器
-
-
[root@node07192 tengine-2.1.0]# cd /usr/local/nginx/sbin/ [root@node07192 sbin]# ./nginx |
-
-
-
- 第六步:访问测试
-
-
(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
-
-
-
- 第七步:配置图片服务
-
-
(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这个路径是要省掉的。
-
-
- 第三部分:SpringMVC实现上传
- 思路
- 第三部分:SpringMVC实现上传
-
(1)使用Springmvc上传组件,从页面表单接收图片
(2)使用vsftpd组件,将图片上传到Linux服务器。
(a)、服务端:在Linux上安装ftp服务端vsftpd软件,并开启服务。
(b)、客户端:在java代码中使用FtpClient客户端建立与服务器的连接
(3)返回值:返回图片上传之后的访问路径。
为什么?
因为保存图片到数据库的时候,保存的就是图片的访问路径。
-
-
-
- 前端js实现
-
-
前端使用kindeditor,初始化上传组件
调用上传组件的初始化方法:
上传组件在common.js中定义
上传组件的初始化方法init
-
-
-
- 后台java实现
- 代码结构
- 后台java实现
-
-
Controller:从表单接收图片,返回图片的回调地址
Service:创建FtpClient客户端,将图片直接上传到Linux服务器
-
-
-
-
- 请求响应格式
-
-
-
请求路径 | /pic/upload |
请求方式 | Post |
请求参数 | uploadFile |
返回值结构 | 参考Kindeditor官方文档(http://kindeditor.net/docs/upload.html) |
Kindeditor官方文档要求的返回格式类型
-
-
-
-
- 定义返回值类型
-
-
-
在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方法 } |
-
-
-
-
- 在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 |
-
-
-
-
- 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_USERNAME}") private String FTP_USERNAME;
@Value("${FTP_PASSWORD}") 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; }
} |
-
-
-
-
- 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; } }
|
-
-
-
-
- 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; } }
|
-
-
-
-
- 测试结果,上传成功!!!
-
-
-
-
-
-
- 将上传结果保存到页面表单域
-
-
页面效果
- 第三部分:kindEditor编辑商品属性
纯前端js实现,不需要java后台代码支持。
原理:内置了一个HTML编辑器,将HTML页面转换成文本类型,将值传给指定的元素。
|
- 第四部分:商品规格参数
-
- 格式
规格分组1
|-规格项1:规格值1
|-规格项2:规格值2
|-规格项n:规格值n
规格分组2
|-规格项11:规格值11
|-规格项22:规格值22
|-规格项nn:规格值nn
规格分组3
|-规格项112:规格值112
|-规格项222:规格值222
|-规格项nnn:规格值nnn
-
- 特点
(1)每一类商品的规格分组是相同的。
(2)每一个规格分组对应多个规格项。
(3)每一个商品的规格值不同。
-
- 设计思路
(1)给商品的每一个分类创建一个规格参数模板。(tb_item_param)
(2)添加商品的时候,根据该类商品的参数模板,填写规格值。
(3)将页面填写的规格值,保存到数据库。(tb_item_param_item)
-
- 实现流程
(1)添加商品规格参数模板
(2)根据规格参数模板生成规格值
-
-
- 第一部分:创建规格参数模板
- 第一步:判断是否已经添加规格参数模板
- 第一部分:创建规格参数模板
-
(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; } } |
-
-
-
- 第二步:添加规格参数模板
-
-
(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); } |
-
-
- 第二部分:根据参数模板生成商品规格参数值表单
-
新增商品 --> 选择类目 --> 查找类目所对应的模板 --> 生成表单
(1)前端js实现
- java后台(已实现)
-
-
- 第一步:修改Controller代码
-
-
@RequestMapping("/save/{catId}") @ResponseBody public EgoResult save(@PathVariable("catId")Long catId,String paramData){ EgoResult result = service.save(catId, paramData);
return result; } |
-
-
-
- 第二步:修改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(); }
|
- 第五部分:保存商品
保存商品,需要同时保存商品信息、商品的描述信息和商品的规格参数,分别对应表tb_item、tb_item_desc、tb_item_param_item三张表。
-
- 前端js实现
- 使用KindEditor富文本编辑器,编辑商品描述信息
- 前端js实现
-
-
- 将商品规格参数表单数据,转换成json格式
-
-
-
- 提交保存商品请求
-
-
- 后台java实现
- 请求响应格式
- 后台java实现
请求路径 | /item/save |
请求方式 | POST |
请求参数 | TbItem、desc、itemParams |
响应格式 | {“status”:200 data:data} 参考http响应格式 |
-
-
- 代码结构
-
Controller:从表单接收数据,封装到JavaBean中
Service:实现保存逻辑,防止事务一致性问题。
Mapper:Mybatis-plus实现
-
-
- 创建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方法 }
|
-
-
- 创建对应的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>{
} |
-
-
- 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, "保存失败,请稍后再试"); }
}
|
-
-
- 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; } |
- 商品规格参数列表实现
- 思路
商品规格参数列表的数据,分别存在了tb_item_param和tb_item_cat两张表中,因此在mapper层,需要自定义查询方法,并分页
-
- 前端js实现
使用的是easyu-datagrid插件,使用方法参考商品列表实现(第一天内容)。
-
- 后台代码实现
- 确定请求响应格式
- 后台代码实现
请求路径 | /item/param/list |
请求方式 | Get |
请求参数 | page、rows(分页) |
返回值类型 | EUDataGridResult类型 |
-
-
- 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); } |
-
-
- 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; }
|
-
-
- Controller层实现
-
--修改ItemParamController接口
@RequestMapping("/list") @ResponseBody public EUDataGridResult listAndPage(Integer page,Integer rows){ EUDataGridResult result = service.listAndPage(page, rows);
return result; } |
-
- 访问测试
规格参数列表实现!!!