- spring+springmvc+mybatis整合
- 后台工程的商品分页查询实现
- 技术要点:
- 使用了Mybatis-PageHelper分页插件
- EasyUI请求的url:
http://localhost:8080/item/list?page=1&rows=30
- 分页查询的结果迎合EasyUi分页属性(json类型的rows,和整型的total),因而创建一个具有如下属性的pojo类:
private long total; private List<?> rows;
- 商品类目选择
- 技术要点:
- 为了迎合EasyUi树形菜单的特点需要创建如下pojo
private long id; private String text; private String state;
- 技术要点:
- 搭建ftp服务器(使用nginx)
- 完成商品图片上传功能
- 技术要点:
- 使用了commons-net包中的FTPClient类进行图片上传(相关知识点参见PictureService.java和FtpUtil.java)
- 迎合kindeditor上传图片的特点创建如下pojo:
并设置两个静态方法方便返回结果是调用private int error; private String url; private String message;
//成功时调用的方法 public static PictureResult ok(String url) { return new PictureResult(0, url, null); } //失败时调用的方法 public static PictureResult error(String message) { return new PictureResult(1, null, message); }
- 技术要点:
- 商品添加
- 商品更新
- 商品批量删除
- 技术要点:参见 item-*.jsp、ItemServiceImpl.java
- 规格参数模板的创建、列表查询
- 商品规格参数的添加
- 更新商品添加功能
- 技术要点:
- 前端参见WEF-INF下jsp相关文件,common.js
- 后端参见ItemParamServiceImpl.java等
- 查询商品规格参数时,用到了简单的双表连接,用mybatis简单实现:
<resultMap id="BaseResultMap" type="cn.eden.taotao.pojo.ItemParam"> <id column="id" property="id" jdbcType="BIGINT" /> <result column="item_cat_id" property="itemCatId" jdbcType="BIGINT" /> <result column="created" property="created" jdbcType="TIMESTAMP" /> <result column="updated" property="updated" jdbcType="TIMESTAMP" /> <result column="name" property="name" jdbcType="VARCHAR" /> <result column="param_data" property="paramData" jdbcType="LONGVARCHAR" /> </resultMap> <select id="getItemParams" resultMap="BaseResultMap"> SELECT p.*, c.`name` FROM tb_item_param p LEFT JOIN tb_item_cat c ON p.item_cat_id = c.id </select>
- 技术要点:
- 创建了portal前台门户工程,rest服务层工程
- 商品分类展示
- 技术要点:
- 由于portal只负责页面展示,它请求的数据必须要向rest中取。所以这里遇到了ajax访问json数据的跨域问题,这里采用jsonp技术。
- **什么是jsonp?**Json跨域请求数据是不可以的,但是js跨域请求js脚本是可以的。可以把json数据封装成一个js语句,做一个方法的调用。
- json数据格式大致:
- 创建两个pojo,一个是返回值pojo,另一个分类节点pojo
public class CatResult { private List<?> data; } public class CatNode { @JsonProperty("n") private String name; @JsonProperty("u") private String url; @JsonProperty("i") private List<?> item; }
- 具体业务实现参见ItemCatServiceImpl.java
- 为了防止数据的乱码问题,在Controller的requestMapping中添加如下:
@RequestMapping(value="/itemcat/all", produces=MediaType.APPLICATION_JSON_VALUE + ";charset=utf-8")
- 技术要点:
- 内容分类管理:实现内容分类节点增删改
- 技术要点:
-
内容分类也是 迎合EasyUITree菜单的特点,另外业务中的分类节点必须要添加父节点(parentId),所以重新创建了pojo(TreeNode是树形菜单的基本节点结构):
public class ContentCatTreeNode extends TreeNode { private Long parentId; public Long getParentId() { return parentId; } public void setParentId(Long parentId) { this.parentId = parentId; } }
-
- 技术要点:
- 内容管理:内容管理分页查询(分页查询还是运用到了分页插件)及内容的增加。内容主要是给前台的大广告位提供数据服务。
-
技术要点:
- 内容管理分页查询业务代码:
@Override public DataGridResult getContentsByPage(long page, long pageSize) { TbContentExample example = new TbContentExample(); // 开始分页 PageHelper.startPage((int) page, (int) pageSize); // 获取查询结果 List<TbContent> rows = contentMapper.selectByExample(example); DataGridResult dgr = new DataGridResult(); dgr.setRows(rows); // 获取分页信息 商品总数信息 PageInfo<TbContent> pageInfo = new PageInfo<TbContent>(rows); dgr.setTotal(pageInfo.getTotal()); return dgr; }
-
- rest服务层发布内容(大广告位)的数据服务:参见taotao-rest中ContentServiceImpl.java
- 前台portal层通过httpClient来调用rest服务层的服务:
- 技术要点:
-
httpClientUtil.java封装了httpClient包相关方法。
-
业务具体实现参见taotao-portal的ContentServiceImpl.java
-
控制层,通过调用service层方法获得json数据,创建逻辑视图model,从而在前台jsp界面实现。
@RequestMapping("/index") public String showIndex(Model model) { String adJson = contentService.getContentList(); model.addAttribute("ad1", adJson); return "index"; }
-
- 技术要点:
- 使用redis做缓存
- 将jedis整合到spring中
- 缓存添加至业务代码中
- rest工程发布redis服务,portal工程通过HttpClient调用rest工程业务。
- rest工程添加大广告位功能添加redis逻辑,大致:
- 首先查询redis数据库是否存在数据,如果有直接返回数据
- 如果没有,则调用mysql查询,查询出的数据在返回前,增至redis数据库中
- 后台manager工程redis逻辑:后台添加大广告位内容后,需要删除redis中全部该大广告位内容的内容分类下的所有内容。
//前台业务service层
public String getContentList() {
// 调用服务层 查询商品内容信息(即大广告位)
String result = HttpClientUtil.doGet(REST_BASE_URL + REST_INDEX_AD_URL);
try {
// 把字符串转换成TaotaoResult
TaotaoResult taotaoResult = TaotaoResult.formatToList(result, TbContent.class);
// 取出内容列表
List<TbContent> list = (List<TbContent>) taotaoResult.getData();
List<Map> resultList = new ArrayList<Map>();
// 创建一个jsp页码要求的pojo列表
for(TbContent tbContent : list) {
Map map = new HashMap();
map.put("srcB", tbContent.getPic2());
map.put("height", 240);
map.put("alt", tbContent.getTitle());
map.put("width", 670);
map.put("src", tbContent.getPic());
map.put("widthB", 550);
map.put("href", tbContent.getUrl());
map.put("heightB", 240);
resultList.add(map);
}
return JsonUtils.objectToJson(resultList);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
//rest服务层 添加大广告位显示redis的逻辑
@Override
public List<TbContent> getContentList(long contentCategoryId) {
try {
// 从缓存中取内容
String result = jedisClient.hget(INDEX_CONTENT_REDIS_KEY,
contentCategoryId + "");
if (!StringUtils.isBlank(result)) {
// 把字符串转换成list
List<TbContent> resultList = JsonUtils.jsonToList(result,
TbContent.class);
return resultList;
}
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}
// 根据内容分类id查询内容列表
TbContentExample example = new TbContentExample();
Criteria criteria = example.createCriteria();
criteria.andCategoryIdEqualTo(contentCategoryId);
// 执行查询
List<TbContent> list = contentMapper.selectByExample(example);
try {
// 向缓存中添加内容
String cacheString = JsonUtils.objectToJson(list);
jedisClient.hset(INDEX_CONTENT_REDIS_KEY, contentCategoryId + "",
cacheString);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}
return list;
}
/**
* redisServiceImpl.java
* 前台修改内容时调用此服务,删除redis中的该内容的内容分类下的全部内容
*/
@Override
public TaotaoResult syncContent(long contentCategoryId) {
try {
jedisClient.hdel(INDEX_CONTENT_REDIS_KEY, contentCategoryId + "");
} catch (Exception e) {
e.printStackTrace();
return TaotaoResult.build(500, ExceptionUtil.getStackTrace(e));
}
return TaotaoResult.ok();
}
- 将数据库商品信息导入到solr索引库中
- 技术要点1:
- 查询数据库中的商品信息 商品信息对应solr的业务字段,并创建相应的pojo,需要查询mysql中tb_item、tb_item_cat、tb_item_desc三表。
- 业务部分参见ItemServiceImpl.java
- 技术要点1:
SELECT
a.id,
a.title,
a.sell_point,
a.price,
a.image,
b.`name` category_name,
c.item_desc
FROM
tb_item a
LEFT JOIN tb_item_cat b ON a.cid = b.id
LEFT JOIN tb_item_desc c ON a.id = c.item_id
<!-- solr业务字段 -->
<field name="item_title" type="text_ik" indexed="true" stored="true"/>
<field name="item_sell_point" type="text_ik" indexed="true" stored="true"/>
<field name="item_price" type="long" indexed="true" stored="true"/>
<field name="item_image" type="string" indexed="false" stored="true" />
<field name="item_category_name" type="string" indexed="true" stored="true" />
<field name="item_desc" type="text_ik" indexed="true" stored="false" />
<field name="item_keywords" type="text_ik" indexed="true" stored="false" multiValued="true"/>
<copyField source="item_title" dest="item_keywords"/>
<copyField source="item_sell_point" dest="item_keywords"/>
<copyField source="item_category_name" dest="item_keywords"/>
<copyField source="item_desc" dest="item_keywords"/>
//sql语句业务字段对应的pojo
public class Item {
private String id;
private String title;
private String sell_point;
private long price;
private String image;
private String category_name;
private String item_des;
}
- 发布搜索服务,对外提供一个get形式的服务,调用此服务时需要查询条件,分页条件可以使用page(要显示第几页)、rows(每页显示的记录数)。
- 技术要点2:
- 对外提供http形式的get请求服务。请求的url:
/search/query?q={查询条件}&page={page}&rows={rows}
- 需要创建一个搜索结果的pojo
- dao层查询solr索引库返回pojo
- service层设置查询条件
- controller层设置默认的当前页和默认的每页显示的记录数
- 详细参见SearchDao、SearchService、SearchController
- 对外提供http形式的get请求服务。请求的url:
- 技术要点2:
//搜索结果pojo
public class SearchResult {
// 商品列表
private List<Item> Items;
// 总记录数
private long recordCount;
// 总页数
private long pageCount;
// 当前页
private long currentPage;
}
//dao
public SearchResult search(SolrQuery query) {
// 创建返回值对象
SearchResult result = new SearchResult();
// 根据查询条件查询索引库
QueryResponse queryResponse = null;
try {
queryResponse = solrServer.query(query);
} catch (SolrServerException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
// 取查询结果
SolrDocumentList documentList = queryResponse.getResults();
// 取查询结果总数量,并加入result中
result.setRecordCount(documentList.getNumFound());
// 商品列表
List<Item> items = new ArrayList<Item>();
// 取高亮显示
Map<String, Map<String, List<String>>> hightLighting = queryResponse
.getHighlighting();
// 取商品列表
for (SolrDocument document : documentList) {
// 创建商品对象
Item item = new Item();
item.setId((String) document.get("id"));
// 取高亮显示的结果
List<String> list = hightLighting.get(document.get("id")).get("item_title");
String title = "";
if (list != null && list.size() > 0) {
title = list.get(0);
} else {
title = (String) document.get("item_title");
}
item.setTitle(title);
item.setImage((String) document.get("item_image"));
item.setPrice((long) document.get("item_price"));
item.setSell_point((String) document.get("item_sell_point"));
item.setCategory_name((String) document.get("item_category_name"));
// 添加的商品列表
items.add(item);
}
result.setItems(items);
return result;
}
//service
public SearchResult search(String queryString, int page, int rows) {
// 创建查询对象
SolrQuery query = new SolrQuery();
// 设置查询条件
query.setQuery(queryString);
// 设置分页 默认是每页10条
query.setStart((page - 1) * rows);
query.setRows(rows);
// 设置默认搜索域
query.set("df", "item_keywords");
// 设置高亮显示
query.setHighlight(true);
query.addHighlightField("item_title");
query.setHighlightSimplePre("<em style=\"color:red\">");
query.setHighlightSimplePost("</em>");
// 执行查询
SearchResult searchResult = searchDao.search(query);
// 计算查询结果总页数
long recordCount = searchResult.getRecordCount();
long pageCount = recordCount / rows;
if (recordCount % rows > 0) {
pageCount++;
}
// 总页数
searchResult.setPageCount(pageCount);
// 当前页码
searchResult.setCurrentPage(page);
return searchResult;
}
//controller
@RequestMapping(value = "/query", method = RequestMethod.GET)
@ResponseBody
TaotaoResult search(@RequestParam("q") String queryString,@RequestParam(defaultValue = "1") Integer page,@RequestParam(defaultValue = "40") Integer rows) {
// 查询条件不能为空
if (StringUtils.isBlank(queryString)) {
return TaotaoResult.build(400, "查询条件不能为空");
}
SearchResult searchResult = null;
try {
// 解决get乱码问题
searchResult = searchService.search(
new String(queryString.getBytes("iso8859-1"), "utf-8"),page, rows);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
return TaotaoResult.build(500, ExceptionUtil.getStackTrace(e));
}
return TaotaoResult.ok(searchResult);
}
- 发布商品信息服务(包括商品基本信息、商品描述、商品规格参数,可用商品id进行查询,参见rest的ItemServiceImpl.java)
- 前台门户工程通过httpclient调用rest商品信息服务,参见portal的ItemServiceImpl.java。
- 要点:
- rest层存商品信息redis的key-value格式为 数据库表名:字段名:字段值 = value(json类型字符串)
- 商品基本信息加载后,商品描述延迟一秒加载,规格参数按需(如果点击规格参数项才加载)加载
- portal的控制层需要根据item.jsp界面的功能,需要按要求增加逻辑视图model,参见ItemController.java
- 要点:
// 商品规格参数按需加载
$(function(){
//取商品id
var itemId = "${item.id}";
//给商品规格参数tab页绑定事件
$("#p-con-attr").bind("click", function(){
itemControl.getItemParam(itemId);
});
//延迟一秒加载商品描述信息
setTimeout(function(){
itemControl.getItemDesc(itemId);
},1000);
});
//ItemServiceImpl.java
// 规格参数调用rest服务,并包装数据
@Override
public String getItemParamItemById(Long itemId) {
String json = HttpClientUtil.doGet(REST_ITEM_URL + "/itemParamItem/" + itemId);
if(StringUtils.isBlank(json)){
return "";
}
// 把json转换成java对象
TaotaoResult result = TaotaoResult.formatToPojo(json,TbItemParamItem.class);
if (result.getStatus() == 200) {
TbItemParamItem itemParamItem =(TbItemParamItem) result.getData();
String paramData = itemParamItem.getParamData();
// 生成html
// 把规格参数json数据转换成java对象
List<Map> jsonList = JsonUtils.jsonToList(paramData, Map.class);
StringBuffer sb = new StringBuffer();
sb.append("<table cellpadding=\"0\" cellspacing=\"1\" width=\"100%\" border=\"0\" class=\"Ptable\">\n");
sb.append(" <tbody>\n");
for (Map m1 : jsonList) {
sb.append(" <tr>\n");
sb.append(" <th class=\"tdTitle\" colspan=\"2\">" + m1.get("group") + "</th>\n");
sb.append(" </tr>\n");
List<Map> list2 = (List<Map>) m1.get("params");
for (Map m2 : list2) {
sb.append(" <tr>\n");
sb.append(" <td class=\"tdTitle\">" + m2.get("k") + "</td>\n");
sb.append(" <td>" + m2.get("v") + "</td>\n");
sb.append(" </tr>\n");
}
}
sb.append(" </tbody>\n");
sb.append("</table>");
// 返回html片段
return sb.toString();
}
return "";
}
- 不完善的地方:
- 后台新增商品后,无法及时更新至solr索引库,需要手动导入,手动导入还是全部导入一遍数据库中的信息。这也不好
- 搜索排序不完善,新增加的没有首先进入在搜索后的热点页面
- 发布单点登录系统sso(用户登录,数据校验,用户注册,安全退出)
- 用户登录后,使用UUID生成一个Token,以Token作为键,用户信息json作为值,存入到redis中;并在Cookie中设置以"TT_TOKEN"作为键,token值作为值存在Cookie中。以后则可以直接通过token值查询用户信息。
- 门户系统当用户下订单时需要用户登录,使用拦截器实现,并有回调:拦截器首先需要判断用户是否登录,可以从cookie中取出token值,并用token取出user值,如果没有user值,则可以判断用户未登录,跳转至登录页面。
- 购物车的实现(portal中的CartServiceImpl.java)
- 通过Cookie取到"TT_CAET"的购物车商品json值
- 如果没有添加商品,则创建CartItem的Pojo,并创建购物车list添加进去;如果有购物车,没有此商品则创建CartItem,添加至购物车;如果购物车中存在此商品,则增加相应数量。
- 删除购物车商品,直接根据itemId查询商品信息,从购物车list中remove该商品的cartItem。