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{
}
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 implements ItemCatService{
@Override
public List getByParentId(Long parentId) {
List nodes = new ArrayList<>();
EntityWrapper ew = new EntityWrapper<>();
ew.eq("parent_id", parentId);
List 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 initTreeByParentId(@RequestParam(defaultValue="0")Long id){
List 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
12.第五步:修改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
13.第六步:启动vsftpd服务
[root@node0719 vsftpd]# service vsftpd start
为 vsftpd 启动 vsftpd: [确定]
14.第七步:通过浏览器访问测试
访问地址: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之间所有的端口。
(4)重启防火墙。
(5)重启vsftpd服务
再次访问浏览器,发现可以正常连接了。
3.4.1.9.第九步:java代码测试上传功能
Java代码中,是通过FtpClient客户端建立和服务端的连接的。在ego-base工程中测试。
(1)在ego-base中添加ftp服务的依赖。
commons-net
commons-net
(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
23.第六步:访问测试
(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,添加上传组件
(2)、修改pom.xml,添加上传依赖common-fileupload.jar
commons-fileupload
commons-fileupload
(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_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;
}
}
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{
}
(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 ew = new EntityWrapper<>();
ew.eq("item_cat_id", catId);
List 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.2.使用KindEditor富文本编辑器,编辑商品描述信息
6.1.3.将商品规格参数表单数据,转换成json格式
6.1.4.提交保存商品请求
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{
}
(2)创建ItemDescMapper接口
package cn.gzsxt.base.mapper;
import com.baomidou.mybatisplus.mapper.BaseMapper;
import cn.gzsxt.base.pojo.ItemDesc;
public interface ItemDescMapper extends BaseMapper{
}
6.2.5.Service代码实现
--修改ItemService接口及其实现类,新增save方法
--注意:注入ItemDescMapper、ItemParamItemMapper
@Service
public class ItemServiceImpl extends ServiceImpl 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{
@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
7.3.3.Service层实现
--修改ItemParamService接口及其实现类
@Override
public EUDataGridResult listAndPage(int curPage, int pageSize) {
List
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.访问测试
规格参数列表实现!!!