1. 课程计划
完成商品添加功能
1、商品类目选择
2、图片上传
3、图片服务器搭建
4、kindEditor富文本编辑器的使用
5、商品添加功能
2. 实现商品类目选择功能
2.1. 需求
在商品添加页面,点击“选择类目”显示商品类目列表:
使用easyUI的异步tree控件来完成。
异步tree的行为:
树控件读取URL。子节点的加载依赖于父节点的状态。当展开一个封闭的节点,如果节点没有加载子节点,它将会把节点id的值作为http请求参数并命名为‘id’,通过URL发送到服务器上面检索子节点。
异步tree的数据结构:
- id:节点id
- text:节点名称
- state:如果不是叶子节点就是closed,叶子节点就是open。close的节点点击后会在此发送请求查询子项目。
{
"id":1,
"text":"Node1",
"state":"closed" //如果节点为父节点则状态为“closed”,如果是叶子节点“open”
}
2.2.数据库
表中的数据为树形结构,可以满足要求
2.3 功能分析
index.jsp:新增商品--->item-add.jsp
item-add.jsp: 选择类目--->selectItemCat
selectItemCat在common.js中实现 请求初始化树形控件的url:/item/cast/list
点击父节点,请求初始化子节点动作是tree空间封装好的,每打开一个父节点做一次ajax请求。
请求参数:id:当前节点的id。根据此id查询子节点
返回结果:一个json数据,是一个列表:
[
{
"id":1,
"text":"Node1",
"state":"closed" //如果节点为父节点则状态为“closed”,如果是叶子节点“open”
},
{
"id":2,
"text":"Node2",
"state":"closed" //如果节点为父节点则状态为“closed”,如果是叶子节点“open”
}
]
2.4. DAO层
select * from tb_item_cat where parent_id=2
使用逆向工程生成的mapper文件:TbItemCatMapper.java、TbItemCatMapper.xml
2.5. Service层
功能:接收parentid参数,根据parentid查询子类目类别。返回一个分类列表。可以创建一个pojo来描述一个节点的格式,返回一个pojo列表。
创建一个pojo:
包含id,text,state属性。因为其他工程也可能用到此pojo,所以应放在taotao-common中:
public class EasyUITreeNode implements Serializable { private Long id; //节点id private String text; //节点名称 private String state; //如果不是叶子节点就是close,叶子节点就是open set/get。。。。。 }
:
public interface ItemCatService { public List<TbItemCat> getItemCatList(Long parentId); }
:
@Service public class ItemCatServiceImpl implements ItemCatService { @Autowired private TbItemCatMapper itemCatMapper; @Override public List<EasyUITreeNode> getItemCatList(Long parentId) { //创建查询条件 TbItemCatExample example = new TbItemCatExample(); Criteria criteria = example.createCriteria(); //根据parentid查询子节点 criteria.andParentIdEqualTo(parentId); List<TbItemCat> list = itemCatMapper.selectByExample(example); //把列表转换成treeNodeList List<EasyUITreeNode> resultList = new ArrayList<EasyUITreeNode>(); for(TbItemCat tbItemCat:list){ EasyUITreeNode node = new EasyUITreeNode(); node.setId(tbItemCat.getId()); node.setText(tbItemCat.getName()); node.setState(tbItemCat.getIsParent()?"closed":"open"); resultList.add(node); } //返回结果 return resultList; } }
2.6 Controller层
功能:接收页面请求的参数,名为id。调用service查询分类列表。返回json格式是列表。需要使用@ResponseBody注解。
:
@Controller @RequestMapping("/item/cat") public class ItemCatController { @Autowired private ItemCatService itemCatService; @RequestMapping("/list") @ResponseBody public List<EasyUITreeNode> getItemCatList(@RequestParam(value="id",defaultValue="0")Long parentId){ List<EasyUITreeNode> list = itemCatService.getItemCatList(parentId); return list; } }
3.实现上传图片功能
首先安装nginx来提供http服务。过程略。
FastDFS只是解决了图片保存问题,要通过http访问图片 必须安装nginx。
3.1 分布式文件服务器FastDFS
3.1.1 什么是FastDFS
FastDFS 是用 c 语言编写的一款开源的分布式文件系统。FastDFS 为互联网量身定制,充分考虑了冗余备份、负载均衡、线性扩容等机制,并注重高可用、高性能等指标,使用 FastDFS很容易搭建一套高性能的文件服务器集群提供文件上传、下载等服务。
FastDFS 架构包括 Tracker server 和 Storage server。客户端请求 Tracker server 进行文件上传、下载,通过 Tracker server 调度最终由 Storage server 完成文件上传和下载。
Tracker server 作用是负载均衡和调度,通过 Tracker server 在文件上传时可以根据一些策略找到 Storage server 提供文件上传服务。可以将 tracker 称为追踪服务器或调度服务器。
Storage server 作用是文件存储,客户端上传的文件最终存储在 Storage 服务器上,Storageserver 没有实现自己的文件系统而是利用操作系统 的文件系统来管理文件。可以将storage称为存储服务器。
服务端两个角色:
Tracker:管理集群,tracker 也可以实现集群。每个 tracker 节点地位平等。收集 Storage 集群的状态。
Storage:实际保存文件 Storage 分为多个组,每个组之间保存的文件是不同的。每个组内部可以有多个成员,组成员内部保存的内容是一样的,组成员的地位是一致的,没有主从的概念。
3.1.2 文件上传及下载的流程
(1) 文件上传流程
客户端上传文件后存储服务器将文件 ID 返回给客户端,此文件 ID 用于以后访问该文件的索引信息。文件索引信息包括:组名,虚拟磁盘路径,数据两级目录,文件名。
组名:文件上传后所在的 storage 组名称,在文件上传成功后有 storage 服务器返回,需要客户端自行保存。
虚拟磁盘路径:storage 配置的虚拟路径,与磁盘选项 store_path*对应。如果配置了store_path0 则是 M00,如果配置了 store_path1 则是 M01,以此类推。
数据两级目录:storage 服务器在每个虚拟磁盘路径下创建的两级目录,用于存储数据文件。
文件名:与文件上传时不同。是由存储服务器根据特定信息生成,文件名包含:源存储服务器 IP 地址、文件创建时间戳、文件大小、随机数和文件拓展名等信息。
(2) 文件下载流程
3.1.3 最简单的 FastDFS 架构
3.1.4 FastDFS安装
略,自己安装的FastDFS的IP地址固定为192.168.25.133
3.1.5 FastDFS实现图片上传小测试
这里我们在taotao-manager-web工程下做测试,在其下的的 src/test/java 下建一个测试类FastDfsTest。
(1)首先在pom.xml引入fastdfs的依赖:
<dependency> <groupId>org.csource.fastdfs</groupId> <artifactId>fastdfs</artifactId> <version>1.2</version> </dependency>
(2)在 src/main/resources 的resource 文件夹下建一个资源文件fastdfs.conf,里面内容为trackServer的IP地址与端口号:
tracker_server=192.168.25.133:22122
(3)FastDfsTest:
public class FastDfsTest { @Test public void testUplode() throws Exception{ //创建一个配置文件,文件名任意,内容就是tracker服务器的地址。在fastdfs.conf中已配置,名称为tracker_server //1.加载配置文件。(这时我们就知道tracker在什么地方了) ClientGlobal.init("D:/Workspaces/MyEclipse 2015/taotao-manager-web/src/main/resources/resource/fastdfs.conf"); //2.构建一个管理者客户端TrackerClient TrackerClient trackerClient = new TrackerClient(); //3.连接管理者服务端 TrackerServer trackerServer = trackerClient.getConnection(); //4.声明存储服务端StorageServer StorageServer storageServer = null; //5.获取存储服务端的客户端对象 StorageClient storageClient = new StorageClient(trackerServer, storageServer); //6.上传文件 String[] strings = storageClient.upload_file("F:/pictures/1.jpg", "jpg", null); for (String string : strings) { System.out.println(string); } } }
输出结果:
group1
M00/00/00/wKgZhV0A-POAUmyRAAE0WLGwjmM340.jpg
测试:网页输入http://192.168.25.133/group1/M00/00/00/wKgZhV0A-POAUmyRAAE0WLGwjmM340.jpg便得到了要显示的图片:
3.1.6 使用FastDFSClient工具类实现图片上传功能
将 fastDFS 上传文件封装到一个工具类FastDFSClient中,放到taotao-manager-common工程下的util包下,注意引入fastDFS依赖。
:
package com.taotao.common.util; import org.csource.common.NameValuePair; import org.csource.fastdfs.ClientGlobal; import org.csource.fastdfs.StorageClient1; import org.csource.fastdfs.StorageServer; import org.csource.fastdfs.TrackerClient; import org.csource.fastdfs.TrackerServer; public class FastDFSClient { private TrackerClient trackerClient = null; private TrackerServer trackerServer = null; private StorageServer storageServer = null; private StorageClient1 storageClient = null; public FastDFSClient(String conf) throws Exception { if (conf.contains("classpath:")) { conf = conf.replace("classpath:", this.getClass().getResource("/").getPath()); } ClientGlobal.init(conf); trackerClient = new TrackerClient(); trackerServer = trackerClient.getConnection(); storageServer = null; storageClient = new StorageClient1(trackerServer, storageServer); } /** * 上传文件方法 * @param fileName 文件全路径 * @param extName 文件扩展名,不包含(.) * @param metas 文件扩展信息 * @return * @throws Exception */ public String uploadFile(String fileName, String extName, NameValuePair[] metas) throws Exception { String result = storageClient.upload_file1(fileName, extName, metas); return result; } public String uploadFile(String fileName) throws Exception { return uploadFile(fileName, null, null); } public String uploadFile(String fileName, String extName) throws Exception { return uploadFile(fileName, extName, null); } /** * 上传文件方法 * @param fileContent 文件的内容,字节数组 * @param extName 文件扩展名 * @param metas 文件扩展信息 * @return * @throws Exception */ public String uploadFile(byte[] fileContent, String extName, NameValuePair[] metas) throws Exception { String result = storageClient.upload_file1(fileContent, extName, metas); return result; } public String uploadFile(byte[] fileContent) throws Exception { return uploadFile(fileContent, null, null); } public String uploadFile(byte[] fileContent, String extName) throws Exception { return uploadFile(fileContent, extName, null); } }
测试:
@Test public void testFastDfsClient() throws Exception { FastDFSClient fastDFSClient = new FastDFSClient("D:/Workspaces/MyEclipse 2015/taotao-manager-web/src/main/resources/resource/fastdfs.conf"); String string = fastDFSClient.uploadFile("F:/pictures/2.jpg", "jpg"); System.out.println(string); }
输出结果:
group1/M00/00/00/wKgZhV0BAaiAYtIxAAHlC6LIRGI190.jpg
3.2 实现图片上传
前端:
item-add:上传图片--->picFileUpload
common.js: initPicUpload --->kingEditorParams
kingEditorParams:
使用的是KindEditor的多图片上传插件。
KindEditor4.x文档:http://kindeditor.net/doc.php
请求的url:/pic/upload
参数:MultiParFile uploadFile
返回值:(json数据)
可以创建一个pojo对应返回值,也可以使用map。
图片上传功能在taotao-manager-web(控制层)实现:
3.2.1 配置文件
(1)在pinyougou-shop-web工程springmvc.xml配置文件上传解析器:
<!-- 配置多媒体解析器(文件上传解析器) --> <bean id="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)在 src/main/resources 的resource 文件夹下建一个资源文件resource.properties,里面内容为 图片服务器 的地址:
TAOTAO_IMAGE_SERVER_URL=http://192.168.25.133/
然后在springmvc.xml中加载该配置文件: (之后在Controller中加@value("${...}")取值)
<!-- 加载配置文件 --> <context:property-placeholder location="classpath:resource/resource.properties" />
3.2.2 Controller层:
@Controller public class PictureController { @Value("${TAOTAO_IMAGE_SERVER_URL}") private String TAOTAO_IMAGE_SERVER_URL; @RequestMapping("/pic/upload") @ResponseBody public Map uploadFile(MultipartFile uploadFile) { try { //1.创建一个 FastDFS 的客户端 FastDFSClient fastDFSClient = new FastDFSClient("classpath:resource/fastdfs.conf"); //取文件扩展名 String originalFilename = uploadFile.getOriginalFilename(); String extName = originalFilename.substring(originalFilename.lastIndexOf(".") + 1); //执行上传处理 String url = fastDFSClient.uploadFile(uploadFile.getBytes(), extName); //拼接返回的 url和 ip 地址,拼装成完整的 url url = TAOTAO_IMAGE_SERVER_URL + url; //封装到map中返回 Map result = new HashMap<>(); result.put("error", 0); result.put("url", url); return result; } catch (Exception e) { e.printStackTrace(); Map result = new HashMap<>(); result.put("error", 1); result.put("message", "图片上传失败"); return result; } } }
这里在谷歌浏览器上传失败了!是因为浏览兼容性问题
3.2.3 解决浏览器兼容性问题
使Controller层返回String类型
@Response 直接响应浏览器,不走逻辑视图,相当于调用response对象调用read方法,往浏览器写内容。如果返回值是对象,它会默认将对象变为json再响应。如果直接返回字符串Sting,它就不转换了,因为浏览器可以识别字符串。
为了解决浏览器兼容问题,我们不再返回Map,而是返回String,需要将原来封装到Map的信息手工转为json(把java对象变为json串),再转为String。
JsonUtils实现把java对象变为json串,放在taotao-manager-common的util包下
import java.util.List; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.ObjectMapper; public class JsonUtils { // 定义jackson对象 private static final ObjectMapper MAPPER = new ObjectMapper(); /** * 将对象转换成json字符串。 * <p>Title: pojoToJson</p> * <p>Description: </p> * @param data * @return */ public static String objectToJson(Object data) { try { String string = MAPPER.writeValueAsString(data); return string; } catch (JsonProcessingException e) { e.printStackTrace(); } return null; } /** * 将json结果集转化为对象 * * @param jsonData json数据 * @param clazz 对象中的object类型 * @return */ public static <T> T jsonToPojo(String jsonData, Class<T> beanType) { try { T t = MAPPER.readValue(jsonData, beanType); return t; } catch (Exception e) { e.printStackTrace(); } return null; } /** * 将json数据转换成pojo对象list * <p>Title: jsonToList</p> * <p>Description: </p> * @param jsonData * @param beanType * @return */ public static <T>List<T> jsonToList(String jsonData, Class<T> beanType) { JavaType javaType = MAPPER.getTypeFactory().constructParametricType(List.class, beanType); try { List<T> list = MAPPER.readValue(jsonData, javaType); return list; } catch (Exception e) { e.printStackTrace(); } return null; } }
调用其objectToJson()方法即可实现。
Controller层代码改为:
@Controller public class PictureController { @Value("${TAOTAO_IMAGE_SERVER_URL}") private String TAOTAO_IMAGE_SERVER_URL; @RequestMapping("/pic/upload") @ResponseBody public String uploadFile(MultipartFile uploadFile) { try { //1.创建一个 FastDFS 的客户端 FastDFSClient fastDFSClient = new FastDFSClient("classpath:resource/fastdfs.conf"); //取文件扩展名 String originalFilename = uploadFile.getOriginalFilename(); String extName = originalFilename.substring(originalFilename.lastIndexOf(".") + 1); //执行上传处理 String url = fastDFSClient.uploadFile(uploadFile.getBytes(), extName); //拼接返回的 url和 ip 地址,拼装成完整的 url url = TAOTAO_IMAGE_SERVER_URL + url; //封装到map中返回 Map result = new HashMap<>(); result.put("error", 0); result.put("url", url); return JsonUtils.objectToJson(result); } catch (Exception e) { e.printStackTrace(); Map result = new HashMap<>(); result.put("error", 1); result.put("message", "图片上传失败"); return JsonUtils.objectToJson(result); } } }
这里我点击上传失败:
报错信息:
java.io.FileNotFoundException: D:WorkspacesMyEclipse%202015 aotao-manager-web argetclasses esourcefastdfs.conf (系统找不到指定的路径。)
不明白为什么路径会不对,FastDFSClient(String conf)是这样定义的:
public FastDFSClient(String conf) throws Exception { if (conf.contains("classpath:")) { conf = conf.replace("classpath:", this.getClass().getResource("/").getPath()); } ClientGlobal.init(conf); trackerClient = new TrackerClient(); trackerServer = trackerClient.getConnection(); storageServer = null; storageClient = new StorageClient1(trackerServer, storageServer); }
这里claspath:识别的路径和fastdfs.conf所在路径不符。然后我改成全路径就对了。。。问题还有待解决。。。
FastDFSClient fastDFSClient = new FastDFSClient("D:/Workspaces/MyEclipse 2015/taotao-manager-web/src/main/resources/resource/fastdfs.conf");
上传成功:
4.富文本编辑器
纯js实现,略。
5.提交表单,完成商品添加功能
前端:
itme-add.jsp:提交--->submitForm()
submitForm():
1) url:/item/save
2)要响应一个json数据,包含status属性。这里我们封装一个类TaotaoResult(自定义响应类),放在taotao-manager-common的util包下:
- private Integer status; // 响应业务状态
- private String msg; // 响应消息
- private Object data; // 响应中的数据
package com.taotao.common.util; import java.io.Serializable; import java.util.List; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; /** * 淘淘商城自定义响应结构 */ public class TaotaoResult implements Serializable{ // 定义jackson对象 private static final ObjectMapper MAPPER = new ObjectMapper(); // 响应业务状态 private Integer status; // 响应消息 private String msg; // 响应中的数据 private Object data; public static TaotaoResult build(Integer status, String msg, Object data) { return new TaotaoResult(status, msg, data); } public static TaotaoResult ok(Object data) { return new TaotaoResult(data); } public static TaotaoResult ok() { return new TaotaoResult(null); } public TaotaoResult() { } public static TaotaoResult build(Integer status, String msg) { return new TaotaoResult(status, msg, null); } public TaotaoResult(Integer status, String msg, Object data) { this.status = status; this.msg = msg; this.data = data; } public TaotaoResult(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结果集转化为TaotaoResult对象 * * @param jsonData json数据 * @param clazz TaotaoResult中的object类型 * @return */ public static TaotaoResult formatToPojo(String jsonData, Class<?> clazz) { try { if (clazz == null) { return MAPPER.readValue(jsonData, TaotaoResult.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 TaotaoResult format(String json) { try { return MAPPER.readValue(json, TaotaoResult.class); } catch (Exception e) { e.printStackTrace(); } return null; } /** * Object是集合转化 * * @param jsonData json数据 * @param clazz 集合中的类型 * @return */ public static TaotaoResult 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.1 数据库
用到tb_item和tb_item_desc两张表:
5.2 DAO层
利用反向工程生成的mapper
5.3 Service层
taotao-manager-interface下 com.taotao.service包下的 ItemService 类:
public interface ItemService {/** * 添加商品基本数据和描述数据 * @param item * @param desc * @return */ public TaotaoResult saveItem(TbItem item,String desc); }
taotao-manager-service下 com.taotao.service.impl的 ItemServiceImpl 实现 saveItem() 方法:
@Service public class ItemServiceImpl implements ItemService { //注入mapper @Autowired private TbItemMapper itemMapper; @Autowired private TbItemDescMapper itemDescMapper; @Override public TaotaoResult saveItem(TbItem item, String desc) { //生成商品的id (利用工具类IDUtils下的genItemId()方法生成) long itemId = IDUtils.genItemId(); //1.补全item的其他属性 item.setId(itemId); item.setStatus((byte) 1);//1-正常,2-下架,3-删除 item.setCreated(new Date()); item.setUpdated(new Date()); //2.向商品表(item表)插入数据 itemMapper.insert(item); //3.创建一个商品描述表对应的pojo对象 TbItemDesc itemDesc = new TbItemDesc(); //4.补全商品描述中的属性 itemDesc.setItemId(itemId); itemDesc.setItemDesc(desc); itemDesc.setCreated(new Date()); itemDesc.setUpdated(new Date()); //5.插入商品描述数据 itemDescMapper.insert(itemDesc); //6.返回TaotaoResult return TaotaoResult.ok(); } }
5.4 Controller层
@Controller public class ItemController { //1.引入服务(springmvc.xml中引入) //2.注入服务 @Autowired private ItemService itemService; /** * 商品添加功能 */ @RequestMapping(value="/item/save",method=RequestMethod.POST) @ResponseBody public TaotaoResult saveItem(TbItem item, String desc) { //1.引入服务(springmvc.xml中引入) //2.注入服务(@Autowired ItemService) //3.调用 return itemService.saveItem(item, desc); } }