使用SSM框架实现仿天猫主页

打开天猫首页,可以看到基本分为3大块:

  1. 商品分页列表+主推商品广告
  2. 知名品牌区域
  3. “天猫超市”、“居家生活”等按主题分块的商品区

这3大块可以分别用不同的模块处理:

  1. 分类管理模块:建立不同分类以及用分类划分商品;主推商品模块。
  2. 品牌管理模块:返回品牌活动和品牌logo
  3. 主题模块:返回主题列表以及主题内部的商品

分类管理

首先建立一个分类管理的后台页面,用来添加、删除和修改分类。


仿 分类管理.png

为了返回这个页面,需要:

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<Category> 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<Category> 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<Category> 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的语义至少不会因为传参而被修改。

这样分类信息就查询出来了,下面就是对分类的修改。

分类新增和编辑
仿 分类编辑.png

对应的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会处理从上传文件到这个类型的转化,前提是需要开启转化器的支持:

    <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
        <property name="defaultEncoding" value="UTF-8"/>
    </bean>

拿到图片数据后,写入本地,这里使用了session.getServletContext().getRealPath("/")来获取项目的绝对路径,然后由此确定图片存储位置。

如果图片功能更复杂,比如还需要生成对应的各种尺寸缩略图、图片是否违法违规的核查等,就需要一个图片处理的service和单独的处理模块来支持了。

产品和分类关联

分类的作用是用来将产品分组,需要把产品归类到一个个分类里,所以需要:

  1. 一个产品的模型,包括数据库表,pojo和对应的mapper等
  2. 因为一个分类里有多个产品,分类和产品属于“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上了。

  1. 需要一个创建产品并且划分类别的页面:
仿 产品编辑.png

产品的业务就需要新增一个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来关联参数。

首页展示
仿 前端 分类.png

有了分类列表的接口和产品管理的接口,首页这部分就可以展示出来。

品牌管理

需要:

  1. 品牌模型,包括数据库表、pojo和mapper等
  2. 产品需要关联品牌
  3. 品牌活动模块
  4. 品牌的创建、编辑等基本操作

创建数据库表:

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<Brand> brands = brandService.list();
        page.setTotal((int)new PageInfo<>(brands).getTotal());
        
        model.addAttribute("brands", brands);
        
        return "admin/listBrand";
    }
}

同样需要建立配套的service和实现类:

public interface BrandService {
    List<Brand> list();
}
...................................
@Service
public class BrandServiceImpl implements BrandService {

    @Autowired
    BrandMapper brandMapper;

    @Override
    public List<Brand> 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);
    }
}

后端品牌管理页面:


仿 品牌管理.png

有了数据,再回到主页,需要显示块状的品牌列表,所以提供一个新的接口,只返回一部分并且随机的品牌列表:

//BrandController部分
@RequestMapping("/randomHomeBrandList")
@ResponseBody
public List randomHomeBrandList(int count){
    return brandService.randomList(count);
}

//BrandServiceImpl部分
@Override
public List<Brand> randomList(int count) {
    return brandMapper.randomSelect(count);
}
使用@ResponseBody是为了返回json类型,前端可以通过这个接口单独拉取品牌数据,换一批时可以局部替换。

关键是mapper.xml里的代码:

<select id="randomSelect" parameterType="int" resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
from brand
ORDER BY rand()
LiMIT 0, #{count}
</select>

因为使用了mysql数据库,所以用了一个ORDER BY rand()来进行随机查询,如果是其他数据库可以加条件判断来确定sql.

前端的效果:


品牌列表.jpg
品牌活动
  1. 建立品牌活动表和对应的DAO层
  2. 建立后台编辑页面
  3. 建立对应的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<BrandActivity> brandActivities = brandActivityService.list();


    List<Brand> 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数据。

其他的增删改查的方法都大同小异,就没什么好写的了。

后台管理页面效果:


品牌活动管理.png

前台效果:


品牌活动.png

为了单独拉取活动数据,增加了一个接口:

@RequestMapping("/brandActivityList")
@ResponseBody
public List<BrandActivity> list(){
    return brandActivityService.list();
}

接口很简单,同样是用@ResponseBody返回json类型数据。前段随便网上搜了的代码,为了掩饰凑合着用,思路就是ajax获取数据,事先在jsp文件里留一个div,比如这个<div class="brand-activities"></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 += "<li class='brand-activity-item'> <div class='brand-activity-header'>"+data[i].title+"  "+data[i].subtitle+
                    "<a style='text-align: right;vertical-align: center font-size: 13pt; flex: 1'>更多</a> </div>  <div class='brand-activity-img' style='text-align: center;width: 100%'> <img src='./img/brandActivity/"+data[i].id+
                    ".jpg' style='width: 100%;vertical-align: middle;'> </div> <a class='brand-mask' href="+data[i].pageLink+
                    "> </a> </li>"
        }
        $(".brand-activities").append(item);   // 显示到里面

    }
});
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 115,902评论 1 231
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 50,002评论 1 197
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 70,756评论 0 162
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 34,843评论 0 123
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 41,590评论 1 204
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 34,471评论 1 124
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 26,468评论 2 204
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 25,632评论 0 117
  • 想象着我的养父在大火中拼命挣扎,窒息,最后皮肤化为焦炭。我心中就已经是抑制不住地欢快,这就叫做以其人之道,还治其人...
    爱写小说的胖达阅读 24,595评论 5 169
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 28,617评论 0 176
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 25,894评论 1 166
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 27,142评论 1 173
  • 白月光回国,霸总把我这个替身辞退。还一脸阴沉的警告我。[不要出现在思思面前, 不然我有一百种方法让你生不如死。]我...
    爱写小说的胖达阅读 21,496评论 0 24
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 24,110评论 2 162
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 27,858评论 3 169
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 22,864评论 0 4
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 23,018评论 0 112
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 29,073评论 2 183
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 29,492评论 2 186

推荐阅读更多精彩内容

  • 前言:首先这个项目是基于网站http://www.how2j.com 参考而完成的一个学习项目,仅供学习参考。 项...
    RichardYee阅读 512评论 0 0
  • 经过上篇文章Spring、Spring MVC与Mybatis整合工程搭建我们便将SSM的环境搭建了,接下来我们便...
    codingXiaxw阅读 22,440评论 1 38
  • 作用: SSM是sping+springMVC+mybatis集成的框架。是标准的MVC模式,将整个系统划分为表现...
    梦昼初心阅读 26,918评论 0 15
  • Mybatis 创建数据库 Mybatis框架原理(掌握) 1、Mybatis 是什么? Mybatis 是一个持...
    printf200阅读 688评论 0 4
  • 一片秋叶,几朵白云鹿足印小径,鸟鸣伴风声空枝濯濯,苍然萧瑟满地落叶,灿灿澄澄驻足林间,听涛声阵阵登高望远,目送南归...
    丛子_c03a阅读 294评论 1 4