打开天猫首页,可以看到基本分为3大块:
- 商品分页列表+主推商品广告
- 知名品牌区域
- “天猫超市”、“居家生活”等按主题分块的商品区
这3大块可以分别用不同的模块处理:
- 分类管理模块:建立不同分类以及用分类划分商品;主推商品模块。
- 品牌管理模块:返回品牌活动和品牌logo
- 主题模块:返回主题列表以及主题内部的商品
分类管理
首先建立一个分类管理的后台页面,用来添加、删除和修改分类。
为了返回这个页面,需要:
1.在数据库建表category
CREATE TABLE `category` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
)
2.有了数据库表,再配合mybatis-generator插件就可以生成对应的pojo、mapper和mapper的xml文件了。有了这些基本的数据操作有了,不过还需要做一些修改。用法参考IDEA中mybatis-generator插件的使用
3.需要一个Controller来处理当前的页面的业务,所以建立一个CategoryController
,并提供分裂列表获取的方法:
package com.shiwei.tmall.controller;
import ***(略)
@Controller
@RequestMapping("")
public class CategoryController {
@Autowired
CategoryService categoryService;
@RequestMapping("/admin_category_list")
public String list(Model model, Page page){
page.setCount(30);
PageHelper.offsetPage(page.getStart(), page.getCount());
List categories = categoryService.list();
int total = (int) new PageInfo<>(categories).getTotal();
page.setTotal(total);
model.addAttribute("cs", categories);
model.addAttribute("page", page);
return "admin/listCategory";
}
}
这里需要说明:
- 使用了一个CategoryService对象,因为为了架构清晰,加入了service层。每一个service对应一个具体的业务,controller通过组合调用不同的service来完成任务。在service层选择连接不同的DAO层,可以是数据库也可以是Redis,这样可以是项目更灵活。
-
@Autowired
是为了能够自动注入categoryService。自动注入的好处是解耦,spring容器会根据类型自动给categoryService赋值,如果要替换,只需要替换容器中的bean就可以了,而不需要动controller的代码。 - 这里使用了
PageHelper
插件,它是通过MyBatis的插件功能,修改了select语句的sql,添加了limit
属性从而实现分页。
4.创建CategoryService以及实现类CategoryServiceImpl
:
public interface CategoryService {
List list();
void add(Category category);
void delete(Integer id);
Category get(Integer id);
void update(Category category);
}
@Service
public class CategoryServiceImpl implements CategoryService {
private CategoryMapper categoryMapper = null;
@Autowired
public CategoryServiceImpl(CategoryMapper categoryMapper){
this.categoryMapper = categoryMapper;
}
@Override
public List list() {
CategoryExample example =new CategoryExample();
example.setOrderByClause("id");
return categoryMapper.selectByExample(example);
}
@Override
public void add(Category category) {
categoryMapper.insert(category);
}
@Override
public void delete(Integer id) {
categoryMapper.deleteByPrimaryKey(id);
}
@Override
public Category get(Integer id) {
return categoryMapper.selectByPrimaryKey(id);
}
@Override
public void update(Category category) {
categoryMapper.updateByPrimaryKeySelective(category);
}
}
需要说明几点:
- 为什么采用接口和实现类的模式而不是直接使用一个类?个人理解是为了以后更好的扩展,当有多个不同的逻辑实现并存时,可以更方便的替换实现。总的思想还是源于实现和声明的分离,更深入的还需要以后更多项目的认识。这里的讨论也不错
- 使用MyBatis插件自动生成的mapper会额外增加一个example类,是用于查询的。这个的一个好处是可以一定程度防止SQL注入,因为SQL注入是通过把sql语句伪装成参数传入,从而修改了sql语句的意思。比如
select * from user where id = 197837 and 1=1
,这里的197837 and 1=1
是传入的参数,但是却会查出所有表数据,但是使用example查询时是参数绑定的方式,实际编译的sql是select * from user where id = ?
,这样sql的语义至少不会因为传参而被修改。
这样分类信息就查询出来了,下面就是对分类的修改。
分类新增和编辑
对应的Controller代码:
//保存分类图片
private void saveCategoryImage(String homePath, MultipartFile imgFile, Integer categoryId) throws IOException{
//判空处理
if (imgFile == null || imgFile.isEmpty()){
return;
}
File imageFolder= new File(homePath+"/img/category");
File file = new File(imageFolder,categoryId+".jpg");
if(!file.getParentFile().exists()){
file.getParentFile().mkdirs();
}
imgFile.transferTo(file);
}
@RequestMapping("/admin_category_add")
public String add(Category category, HttpSession session, MultipartFile imgFile) throws IOException {
if (category.getName() == null){
return "";
}
categoryService.add(category);
saveCategoryImage(session.getServletContext().getRealPath("/"), imgFile, category.getId());
return "redirect:/admin_category_list";
}
@RequestMapping("admin_category_update")
public String update(Category category, HttpSession session, MultipartFile imgFile, @Param("id") Integer id) throws IOException{
categoryService.update(category);
saveCategoryImage(session.getServletContext().getRealPath("/"), imgFile, category.getId());
return "redirect:admin_category_list";
}
插入category数据很简单,service和mapper都准备好了,这里需要注意的是上传图片的功能。上传图片使用HTTP POST的multipart/form-data类型,这个前端都有相应的框架支持,后端使用MultipartFile
类型接受,它源于org.springframework.web.multipart
包,springMVC会处理从上传文件到这个类型的转化,前提是需要开启转化器的支持:
拿到图片数据后,写入本地,这里使用了session.getServletContext().getRealPath("/")
来获取项目的绝对路径,然后由此确定图片存储位置。
如果图片功能更复杂,比如还需要生成对应的各种尺寸缩略图、图片是否违法违规的核查等,就需要一个图片处理的service和单独的处理模块来支持了。
产品和分类关联
分类的作用是用来将产品分组,需要把产品归类到一个个分类里,所以需要:
- 一个产品的模型,包括数据库表,pojo和对应的mapper等
- 因为一个分类里有多个产品,分类和产品属于“1对多”的关系,所以通过在产品的表里加入外键来实现和分类的关联:
CREATE TABLE `product` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`subTitle` varchar(255) DEFAULT NULL,
`originalPrice` float DEFAULT NULL,
`promotePrice` float DEFAULT NULL,
`stock` int(11) DEFAULT NULL,
`cid` int(11) DEFAULT NULL,
`createDate` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `fk_product_category` (`cid`),
CONSTRAINT `fk_product_category` FOREIGN KEY (`cid`) REFERENCES `category` (`id`)
)
这里通过一个外键,把字段cid
关联到category表的id
上了。
- 需要一个创建产品并且划分类别的页面:
产品的业务就需要新增一个ProductController,在这里新增一个产品添加和更新的方法:
//新增产品
@RequestMapping("admin_product_add")
public String add(Model model, Product p) {
p.setCreateDate(new Date());
productService.add(p);
return "redirect:admin_product_list?cid="+p.getCid();
}
//更新产品
@RequestMapping("admin_product_update")
public String update(Product p) {
productService.update(p);
return "redirect:admin_product_list?cid="+p.getCid();
}
这里的参数直接就是Product,因为使用SpringMVC框架可以自动合成pojo对象,比如这里Product里有字段cid
,会自动填充HTTP请求参数里的同名字段。如果参数不同名就会失败,也无法使用@RequestParam
来关联参数。
首页展示
有了分类列表的接口和产品管理的接口,首页这部分就可以展示出来。
品牌管理
需要:
- 品牌模型,包括数据库表、pojo和mapper等
- 产品需要关联品牌
- 品牌活动模块
- 品牌的创建、编辑等基本操作
创建数据库表:
create table brand(
id int not null auto_increment,
name varchar(127),
primary key(id)
);
然后使用Mybatis-generator生成对应pojo和mapper文件,再就是增加一个BrandController:
@RequestMapping("/")
@Controller
public class BrandController {
@Autowired
BrandService brandService;
@RequestMapping("/brandList")
public String list(Model model, Page page){
page.setCount(30);
PageHelper.offsetPage(page.getStart(), page.getCount());
List brands = brandService.list();
page.setTotal((int)new PageInfo<>(brands).getTotal());
model.addAttribute("brands", brands);
return "admin/listBrand";
}
}
同样需要建立配套的service和实现类:
public interface BrandService {
List list();
}
...................................
@Service
public class BrandServiceImpl implements BrandService {
@Autowired
BrandMapper brandMapper;
@Override
public List list() {
BrandExample example = new BrandExample();
example.setOrderByClause("name"); //默认按名字排序
return brandMapper.selectByExample(example);
}
}
注意要加@Controller注解和@Service注解,否则Spring容器找不到它们就无法做URL映射和依赖注入了。
增加品牌
现在品牌的数据还是空的,需要增加品牌的功能,首先在service里增加方法void addBrand(Brand brand);
, BrandServiceImpl里添加实现:
@Override
@Transactional
public void addBrand(Brand brand) {
brandMapper.insert(brand);
}
再在controller里添加增加的方法:
@RequestMapping("/addBrand")
public String addBrand(HttpSession session, Brand brand, MultipartFile imgFile) throws IOException{
brandService.addBrand(brand);
imageSaveService.saveImage(session.getServletContext().getRealPath("/"), ImageSaveService.BRAND_IMAGE_KEY, imgFile, brand.getId());
return "redirect:/brandList";
}
因为图片存储功能和分类那一一致,所以提取这个功能到一个单独的service类里了,通过key来识别存储不同模块的图片:
@Service
public class ImageSaveService {
public static final String CATEGORY_IMAGE_KEY = "category";
public static final String BRAND_IMAGE_KEY = "brand";
//保存图片
public void saveImage(String homePath, String pathKey, MultipartFile imgFile, Integer itemId) throws IOException {
//判空处理
if (imgFile == null || imgFile.isEmpty()){
return;
}
File imageFolder= new File(homePath+"/img/"+pathKey);
File file = new File(imageFolder,itemId+".jpg");
if(!file.getParentFile().exists()){
file.getParentFile().mkdirs();
}
imgFile.transferTo(file);
}
}
后端品牌管理页面:
有了数据,再回到主页,需要显示块状的品牌列表,所以提供一个新的接口,只返回一部分并且随机的品牌列表:
//BrandController部分
@RequestMapping("/randomHomeBrandList")
@ResponseBody
public List randomHomeBrandList(int count){
return brandService.randomList(count);
}
//BrandServiceImpl部分
@Override
public List randomList(int count) {
return brandMapper.randomSelect(count);
}
使用@ResponseBody是为了返回json类型,前端可以通过这个接口单独拉取品牌数据,换一批时可以局部替换。
关键是mapper.xml里的代码:
因为使用了mysql数据库,所以用了一个ORDER BY rand()
来进行随机查询,如果是其他数据库可以加条件判断来确定sql.
前端的效果:
品牌活动
- 建立品牌活动表和对应的DAO层
- 建立后台编辑页面
- 建立对应的controller和service
create table BrandActivity(
id int not null auto_increment,
title varchar(20),
subtitle varchar(50),
mainBrand int,
pageLink varchar(255),
primary key(id),
constraint fk_ba_b foreign key (mainBrand) references brand(id)
);
创建了一个数据库表,包含标题、小标题、图片,以及一个关联到品牌的外键。
@RequestMapping("brandActivityManage")
public String brandActivityManage(Model model, Page page){
PageHelper.offsetPage(page.getStart(), page.getCount());
List brandActivities = brandActivityService.list();
List brands = brandService.list();
model.addAttribute("brands",brands);
model.addAttribute("brandActivities",brandActivities);
for (BrandActivity brandActivity: brandActivities){
int mbid = brandActivity.getMainBrand();
for (Brand b: brands){
if (mbid == b.getId()){
brandActivity.setMainBrandName(b.getName());
}
}
}
return "admin/listBrandActivity";
}
这个是返回后台管理页面的方法,内部查询了所有的品牌活动。BrandActivity的pojo里除了数据库表对应的属性,还增加了一个mainBrandName
,因为数据库表里存储的是id,显示的时候需要转换为名称。
因为需要所有的品牌做选择,所以本来就要查询出所有品牌列表,就直接在这用一个双层for循环给mainBrandName
赋值了。否则需要用mainBrandId
去数据库查到对应的Brand数据。
其他的增删改查的方法都大同小异,就没什么好写的了。
后台管理页面效果:
前台效果:
为了单独拉取活动数据,增加了一个接口:
@RequestMapping("/brandActivityList")
@ResponseBody
public List list(){
return brandActivityService.list();
}
接口很简单,同样是用@ResponseBody返回json类型数据。前段随便网上搜了的代码,为了掩饰凑合着用,思路就是ajax获取数据,事先在jsp文件里留一个div,比如这个,然后请求完数据往这个div里加入内容:
//brand-activities就是要修改的div
$.ajax({
type: "GET",
url: "./brandActivityList",
data: null,
dataType: "json",
success: function(data){
var item = "";
$(".brand-activities").empty(); //清空
for(var i = 0 ; i < data.length; i++) {
item += " "+data[i].title+" "+data[i].subtitle+
"更多 "
}
$(".brand-activities").append(item); // 显示到里面
}
});