shop第一天

电商行业发展

近年来,中国的电子商务快速发展,交易额连创新高,电子商务在各领域的应用不断拓展和深化、相关服务业蓬勃发展、支撑体系不断健全完善、创新的动力和能力 不断增强。电子商务正在与实体经济深度融合,进入规模性发展阶段,对经济社会生活的影响不断增大,正成为我国经济发展的新引擎。

中国电子商务研究中心数据显示,截止到2012年底,中国电子商务市场交易规模达7.85万亿人民币,同比增长30.83%。其中,B2B电子商务交易额 达6.25万亿,同比增长27%。而2011年全年,中国电子商务市场交易额达6万亿人民币,同比增长33%,占GDP比重上升到13%;2012年,电子商务占GDP的比重已经高达15%。2013年我国电子商务规模已突破十万亿大关。

电商行业技术特点
  • 技术新

  • 技术范围广

  • 分布式

  • 高并发、集群、负载均衡、高可用

  • 海量数据

  • 业务复杂

  • 系统安全

shop商城介绍

电商行业的模式:
  • B2B:企业到企业,商家到商家。代表:阿里巴巴、慧聪网

  • B2C:商家到客户。代表:京东、淘宝商城(B2B2C)

  • C2C:客户到客户。淘宝集市

  • O2O:线上到线下

shop商城的模式

shop网上商城是一个综合性的B2C平台,类似京东商城、天猫商城。会员可以在商城浏览商品、下订单,以及参加各种活动。

管理员、运营可以在平台后台管理系统中管理商品、订单、会员等。客服可以在后台管理系统中处理用户的询问以及投诉。

功能描述

后台管理系统:管理商品、订单、类目、商品规格属性、用户管理以及内容发布等功能。

前台系统:用户可以在前台系统中进行注册、登录、浏览商品、首页、下单等操作。

会员系统:用户可以在该系统中查询已下的订单、收藏的商品、我的优惠券、团购等信息。

订单系统:提供下单、查询订单、修改订单状态、定时处理订单。

搜索系统:提供商品的搜索功能。

单点登录系统:为多个系统之间提供用户登录凭证以及查询登录用户的信息。

技术架构

随着互联网的发展,网站应用的规模不断扩大,常规的垂直应用架构已无法应对,分布式服务架构以及流动计算架构势在必行,亟需一个治理系统确保架构有条不紊的演进。

  • 单一应用架构

    • 当网站流量很小时,只需一个应用,将所有功能都部署在一起,以减少部署节点和成本。
    • 此时,用于简化增删改查工作量的 数据访问框架(ORM) 是关键。
    • 缺点:随着应用功能的增多,代码量越来越大,越来越难维护,那怎么解决代码一体化的问题?
  • 垂直应用架构

    • 当访问量逐渐增大,单一应用增加机器带来的加速度越来越小,将应用拆成互不相干的几个应用,以提升效率。
    • 此时,用于加速前端页面开发的 Web框架(MVC) 是关键。
    • 缺点:垂直架构中相同逻辑代码需要不断的复制,不能复用。每个垂直模块都相当于一个独立的系统。
  • 分布式服务架构

    • 当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。
    • 此时,用于提高业务复用及整合的 分布式服务框架(RPC) 是关键。
    • 缺点:服务越来越多,需要管理每个服务的地址,调用关系错综复杂,难以理清依赖关系,服务状态难以管理,无法根据服务情况动态管理。
  • 流动计算架构

    • 当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐显现,此时需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率。
    • 此时,用于提高机器利用率的 资源调度和治理中心(SOA) 是关键。
    • 缺点:服务间会有依赖关系,一旦某个环节出错会影响较大,服务关系复杂,运维、测试部署困难,不符合DevOps思想。
  • 微服务架构

    • 单一职责:微服务中每一个服务都对应唯一的业务能力,做到单一职责

    • 微:微服务的服务拆分粒度很小,例如一个用户管理就可以作为一个服务。每个服务虽小,但“五脏俱全”。

    • 面向服务:面向服务是说每个服务都要对外暴露服务接口API。并不关心服务的技术实现,做到与平台和语言无关,也不限定用什么技术实现,只要提供Rest的接口即可。

    • 自治:自治是说服务间互相独立,互不干扰

      • 团队独立:每个服务都是一个独立的开发团队,人数不能过多。
      • 技术独立:因为是面向服务,提供Rest接口,使用什么技术没有别人干涉
      • 前后端分离:采用前后端分离开发,提供统一Rest接口,后端不用再为PC、移动端开发不同接口
      • 数据库分离:每个服务都使用自己的数据源
      • 部署独立,服务间虽然有调用,但要做到服务重启不影响其它服务。有利于持续集成和持续交付。每个服务都是独立的组件,可复用,可替换,降低耦合,易维护 Docker部署服务

思考:有什么问题?

  1. 模块之间耦合度太高,其中一个升级其他都得升级

  2. 开发困难,各个团队开发最后都要整合一起

  3. 系统的扩展性差

  4. 不能灵活的进行分布式部署。

解决方法:系统拆分

优点:把模块拆分成独立的工程,单点运行。如果某一个点压力大可以对这一个点单独增加配置。其他的点不受影响。

分布式架构:把系统按照模块拆分成多个子系统。

优点:

  1. 把模块拆分,使用接口通信,降低模块之间的耦合度。

  2. 把项目拆分成若干个子项目,不同的团队负责不同的子项目。

  3. 增加功能时只需要再增加一个子项目,调用其他系统的接口就可以。

  4. 可以灵活的进行分布式部署。

缺点:

  1. 系统之间交互需要使用远程通信,接口开发增加工作量。

技术选型

  • Java(核心编程语言)

  • SpringBoot、SpringMVC、Mybatis(三大框架)

  • Dubbo(分布式服务框架)

  • Zookeeper(服务注册中心)

  • Redis(缓存数据库)

  • Elasticseach(搜索引擎)

  • SSO(单点登录)

  • MySql(数据库)

  • Nginx(web服务器)

  • 七牛云(文件上传服务器)

  • RabbitMQ(消息队列)

  • Alipay(支付宝支付)

  • 腾讯验证码(验证)

  • jQuery、Bootstrap(前端框架)

  • doT.js(模板引擎)

  • UEditor(富文本编辑器)

  • Google Kaptcha(图形验证码)

开发环境和工具

  • IntelliJ IDEA

  • Maven 3.6.2

  • JDK 1.8.0_231

  • Tomcat 9.0.29

  • MySql 5.7.20

  • Nginx 1.16.1

  • Dubbo 2.6.0

  • Zookeeper 3.4.13

  • Redis 5.0.3

  • Elasticsearch 7.4.2

  • Win10(开发环境)

  • Centos7.3-1908(部署环境)

说明:单独使用可以随意选择版本,如果在集群下需要考虑多种技术之间的兼容性。比如:RedisElasticsearch可以使用单台服务器也可以使用多台服务器,使用多台服务器时需要考虑与其他技术的兼容性,或者统一都用高版本jar包,或者统一降低版本jar包,一高一低可能会出问题。

人员配置

  • 产品经理:3人,确定需求以及给出产品原型图。

  • 项目经理:1人,项目管理。

  • 架构师:2人,负责解决技术难点与定制标准(开发准则、接口标准、技术选型等)。

  • 前端团队:5人,根据产品经理给出的原型制作静态页面。

  • 安卓团队:5人,开发android客户端。

  • IOS团队:5人,开发IOS客户端。

  • 后端团队:20人,实现整个系统业务功能。

  • 测试团队:5人,测试所有的功能、性能、安装。

  • 运维团队:3人,项目的发布以及维护。

  • 如果采用oracle,需要配备DBA:2人。

shop商城研发优势

  1. 节约成本

包括硬件成本和软件成本,硬件包括店面、房租、装修、印刷、纸张等最必须用品,软件包括网上商城购物系统、网络信息、图片、视屏等,都可长期使用、良性循环、经济和环保。

  1. 营销推广经济、便捷

传统媒体广告费用高昂,更适合于进行品牌塑造;而网络营销主要是策略与定位把控的问题,实惠很多,费用与传统媒体相比微乎其微,并且流量与用户也更加精准。

  1. 信息更加立体、全面

通过互联网,企业的信息展示、品牌塑造和形象宣传可以通过文字、图片、音频、视频等多维度进行现实与虚拟相结合的展示,使用户对企业的了解更加立体和全面,有助于形成良好的形象与口碑。

  1. 管理高效、便捷

运用信息化的数据库管理,各类信息精准、清晰、无误的保存,避免出现人工操作出现低级错误的情况,可随时查阅、核算、统计。

功能点

  1. 商家入驻

商家入驻功能包括:商家入驻申请流程、商家店铺自定义装修功能、多套店铺模板选择、商家店铺街展示、商家独立店铺功能、商铺报表统计功能、搜索店铺列表页、商家自定义广告位、区分平台与商家分类、类型、订单分单功能(按商家)、订单退换功能、商家订单佣金结算等。

  1. 会员中心

会员中心功能包括:会员中个人主页美化、会员中心订单列表美化、会员中心收货地址列表美化 、会员中心缺货登记、会员中心退换货、会员中心退换货详情页 、用户信息、新增会员头像上传功能 、平台红包、物流跟踪功能、资金管理

  1. 购物车

购物车功能包括:购物车选择购买功能、购物车加强功能、商品促销满减、满赠、折扣功能、凑单功能、简化购物流程、购物车为空时去购物功能、提示购物车商品是否有库存。

  1. 订单

订单功能包括:商家自定义配送方式和运费、门店自提功能、发票功能、商品无库存时提交订单弹窗提示继续购物、结算页面无货提交弹出框、快递配送方式选择如韵达。

  1. 积分商城

积分商城功能包括:积分商品列表、兑换商品排序、精品推荐、热门兑换、兑换商品详情页。

  1. 拍卖活动

拍卖活动功能包括:拍卖活动列表、竞拍商品排序和搜索、拍卖商品详情页、出价记录、商家店铺。

  1. 优惠活动

优惠活动功能包括:优惠活动列表页、优惠范围、优惠方式。

后台管理系统——项目框架搭建

shop父模块

创建pom项目

Maven提供了聚合类型的项目,其本质就是一个分布式架构。

Maven分布式架构-聚合类型,需要一个parent项目,shop-parent就是parent项目。

编辑pom.xml配置文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<!-- 项目坐标地址 -->
	<groupId>com.xxxx</groupId>
	<!-- 项目模块名称 -->
	<artifactId>shop</artifactId>
	<!-- 项目打包类型 -->
	<packaging>pom</packaging>
	<!-- 项目版本名称 快照版本SNAPSHOT、正式版本RELEASE -->
	<version>1.0-SNAPSHOT</version>

	<!--
		  模块管理,实现 pom 项目之间的聚合关系,
		  聚合关系下对父项目使用 mvn 命令会对其他子项目产生同样效果
	   -->
	<modules>
		<module>shop-common</module>
		<module>shop-generator</module>
		<module>shop-manager</module>
		<module>shop-order</module>
		<module>shop-portal</module>
		<module>shop-rpc</module>
		<module>shop-sso</module>
	</modules>

	<!-- 继承 spring-boot-starter-parent 依赖 -->
	<!-- 使用继承方式,实现复用,符合继承的都可以被使用 -->
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.2.2.RELEASE</version>
	</parent>

	<!--
	   集中定义依赖组件版本号,但不引入,
	   在子工程中用到声明的依赖时,可以不加依赖的版本号,
	   这样可以统一管理工程中用到的依赖版本
	-->
	<properties>
		<!-- JDK 版本定义 -->
		<java.version>1.8</java.version>
		<!-- mybatis 依赖 -->
		<mybatis.version>2.1.1</mybatis.version>
		<!-- pagehelper 分页依赖 -->
		<pagehelper.version>1.2.13</pagehelper.version>
		<!-- mysql 数据库依赖 -->
		<mysql.version>8.0.18</mysql.version>
		<!-- druid 连接池依赖 -->
		<druid.version>1.1.20</druid.version>
	</properties>

	<!-- 项目依赖管理 父项目只是声明依赖,子项目需要写明需要的依赖(可以省略版本信息) -->
	<dependencyManagement>
		<dependencies>
			<!-- mybatis 依赖 -->
			<dependency>
				<groupId>org.mybatis.spring.boot</groupId>
				<artifactId>mybatis-spring-boot-starter</artifactId>
				<version>${mybatis.version}</version>
			</dependency>
			<!-- pagehelper 分页依赖 -->
			<dependency>
				<groupId>com.github.pagehelper</groupId>
				<artifactId>pagehelper-spring-boot-starter</artifactId>
				<version>${pagehelper.version}</version>
			</dependency>
			<!-- mysql 数据库依赖 -->
			<dependency>
				<groupId>mysql</groupId>
				<artifactId>mysql-connector-java</artifactId>
				<version>${mysql.version}</version>
			</dependency>
			<!-- druid 连接池依赖 -->
			<dependency>
				<groupId>com.alibaba</groupId>
				<artifactId>druid</artifactId>
				<version>${druid.version}</version>
			</dependency>
        </dependencies>
	</dependencyManagement>
</project>

shop-common子模块

用于添加公共的工具类、枚举类、拦截器等。

编辑pom.xml配置文件
<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.xxxx</groupId>
  <artifactId>shop-common</artifactId>
  <version>1.0-SNAPSHOT</version>

  <!-- 继承 shop-parent 依赖 -->
  <parent>
    <groupId>com.xxxx</groupId>
    <artifactId>shop</artifactId>
    <version>1.0-SNAPSHOT</version>
  </parent>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
  </properties>

  <dependencies>
    <!-- spring boot web 依赖 -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- pagehelper 分页依赖 -->
    <dependency>
      <groupId>com.github.pagehelper</groupId>
      <artifactId>pagehelper-spring-boot-starter</artifactId>
    </dependency>
  </dependencies>
</project>

shop-manager子模块

后台管理系统

编辑pom.xml配置文件
<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.xxxx</groupId>
  <artifactId>shop-manager</artifactId>
  <version>1.0-SNAPSHOT</version>

  <!-- 继承 shop-parent 依赖 -->
  <parent>
    <groupId>com.xxxx</groupId>
    <artifactId>shop</artifactId>
    <version>1.0-SNAPSHOT</version>
  </parent>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
  </properties>

  <dependencies>
    <!-- shop common 依赖 -->
    <dependency>
      <groupId>com.xxxx</groupId>
      <artifactId>shop-common</artifactId>
      <version>1.0-SNAPSHOT</version>
    </dependency>
    <!-- spring boot freemarker 依赖 -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-freemarker</artifactId>
    </dependency>
    <!-- mybatis 依赖 -->
    <dependency>
      <groupId>org.mybatis.spring.boot</groupId>
      <artifactId>mybatis-spring-boot-starter</artifactId>
    </dependency>
    <!-- mysql 数据库依赖 -->
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <!-- druid 连接池依赖 -->
    <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>druid</artifactId>
    </dependency>
    <!-- spring boot test 依赖 -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
      <exclusions>
        <exclusion>
          <groupId>org.junit.vintage</groupId>
          <artifactId>junit-vintage-engine</artifactId>
        </exclusion>
      </exclusions>
    </dependency>
  </dependencies>
</project>

application-dev.yml

server:
  port: 9090                                # 项目访问端口,默认 8080
  servlet:                                 # 项目访问路径,默认 /
    context-path: /shop-manager

# Spring
spring:
  # 数据源
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/shop?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
    username: root
    password: root
    # 指定 druid 连接池以及 druid 连接池配置
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      initial-size: 1                       # 初始连接数
      max-active: 20                        # 最大连接数
      max-idle: 20                          # 最大空闲
      min-idle: 1                           # 最小空闲
      max-wait: 60000                       # 最长等待时间
  # freemarker 模板引擎
  freemarker:
    cache: false
    charset: UTF-8
    content-type: text/html;charset=UTF-8
    enabled: true
    suffix: .ftl
    template-loader-path: classpath:/views/
    # 配置模板里是否可以直接取request的属性 request是别名
    request-context-attribute: request
    # 配置将request和session中的键值添加到
    # AbstractTemplateView类的renderMergedOutputModel方法中的model这个Map参数中
    expose-request-attributes: true
    expose-spring-macro-helpers: true
    # 配置模板里是否可以直接取session的属性 true 是允许
    expose-session-attributes: true
    settings:
      tag_syntax: auto_detect               # 配置标签语法为自动,页面可以将 <> 改为 [],为了区别 html 标签
      template_update_delay: 0              # 模板更新时间,单位秒
      default_encoding: UTF-8               # 默认编码字符集
      output_encoding: UTF-8                # 模板输出编码字符集
      locale: zh_CN                         # 本地化配置
      date_format: yyyy-MM-dd               # 日期格式化
      time_format: HH:mm:ss                 # 时间格式化
      datetime_format: yyyy-MM-dd HH:mm:ss  # 日期时间格式化
      number_format: #.##                   # 数字格式化
      boolean_format: true,false            # boolean格式化
      # ignore,debug,html_debug,rethrow
      # 1.TemplateExceptionHandler.IGNORE_HANDLER简单地压制所有异常
      # 它对处理异常没有任何作用,也不会重新抛出异常,页面可以正常渲染,后台抛异常
      # 2.TemplateExceptionHandler.DEBUG_HANDLER打印堆栈信息和重新抛出异常。这是默认的异常控制器
      # 3.TemplateExceptionHandler.HTML_DEBUG_HANDLER和DEBUG_HANDLER相同
      # 但是可以格式化堆栈跟踪信息,HTML页面,建议使用它而不是DEBUG_HANDLER
      # 4.TemplateExceptionHandler.RETHROW_HANDLER简单重新抛出所有异常而不会做其他的事情
      # 5.使用自定义异常类实现TemplateExceptionHandler重写handleTemplateException方法
      template_exception_handler: html_debug
# MyBatis
mybatis:
  # 配置 MyBatis数据返回类型别名(默认别名是类名)
  type-aliases-package: com.xxxx.manager.pojo
  # 配置 MyBatis Mapper 映射文件
  mapper-locations: classpath:/mapper/*.xml
# Mybatis SQL 打印(方法接口所在的包,不是 Mapper.xml 所在的包)
logging:
  level:
    com.xxxx.manager.mapper: debug

项目运行测试

PageController.java

package com.xxxx.manager.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;

/**
 * 跳转页面
 *
 * @author zhoubin
 * @since 1.0.0
 */
@Controller
public class PageController {
	/**
	 * 公共页面跳转 restful风格
	 * 比如:前台传welcome就跳转至/WEB-INF/pages/welcome.ftl
	 * 比如:前台传login就跳转至/WEB-INF/pages/login.ftl
	 * ...
	 * @param page
	 * @return
	 */
	@RequestMapping("/{page}")
	public String page(@PathVariable String page) {
		System.out.println(page);
		return page;
	}
}

ManagerApplication.java

package com.xxxx.manager;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * 启动类
 */
@SpringBootApplication
public class ManagerApplication {
	public static void main(String[] args) {
		SpringApplication.run(ManagerApplication.class,args);
	}
}

Welcome.ftl

<!-- 设置项目根路径全局变量 -->
<#assign ctx=request.contextPath/>
<!DOCTYPE html>
<html>
<head>
    <title>shop商城后台管理系统</title>
</head>
<body>
<h1>${ctx}欢迎使用shop商城后台管理系统</h1>
</body>
</html>

程序运行前先把parent项目install一下

将html页面修改为freemarker页面

将.html复制到项目中,并将后缀改为.ftl

首页.html -> index.ftl

欢迎.html -> welcome.ftl

在.ftl文件顶部设置变量ctx,其值为项目的根路径

<!-- 设置项目根路径全局变量 -->
<#assign ctx=request.contextPath/>

批量修改freemarker中资源文件引用路径

css、js、images等资源文件的引用路径

再修改index.ftl的iframe的欢迎页面

<section class="content-wrapper right-side" id="riframe" style="margin:0px;padding:0px;margin-left:230px;">
    <iframe id='rightContent' name='rightContent' src="${ctx}/welcome" width='100%' frameborder="0"></iframe>
</section>

Freemarker提取公用模板

公用模板head.ftl

<!-- 设置项目根路径全局变量 -->
<#assign ctx=request.contextPath/>
<meta charset="UTF-8">
<title>shop管理后台</title>
<!-- Tell the browser to be responsive to screen width -->
<meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!-- 禁止浏览器发起 favicon.ico 请求 -->
<link rel="icon" href="data:image/ico;base64,aWNv">
<!-- Bootstrap 3.3.4 -->
<link href="${ctx}/bootstrap/css/bootstrap.min.css" rel="stylesheet" type="text/css"/>
<!-- FontAwesome 4.3.0 -->
<link href="${ctx}/bootstrap/css/font-awesome.min.css" rel="stylesheet" type="text/css"/>
<!-- Ionicons 2.0.0 -->
<#--<link href="https://code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css" rel="stylesheet" type="text/css"/>-->
<!-- Theme style -->
<link href="${ctx}/dist/css/AdminLTE.min.css" rel="stylesheet" type="text/css"/>
<link href="${ctx}/dist/css/skins/_all-skins.min.css" rel="stylesheet" type="text/css"/>
<!-- iCheck -->
<link href="${ctx}/plugins/iCheck/flat/blue.css" rel="stylesheet" type="text/css"/>
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
<![endif]-->
<!-- jQuery 2.1.4 -->
<script src="${ctx}/plugins/jQuery/jQuery-2.1.4.min.js"></script>
<script src="${ctx}/js/global.js"></script>
<script src="${ctx}/js/upgrade.js"></script>
<script src="${ctx}/js/myFormValidate.js"></script>
<script src="${ctx}/js/layer/layer-min.js"></script>
<!--弹窗js 参考文档 http://layer.layui.com/-->
<script src="${ctx}/bootstrap/js/bootstrap.min.js"></script>
<script src="${ctx}/js/myAjax.js"></script>

商品管理-商品分类页面-新增分类页面

提取freemarker页面

商品分类.html商品分类-新增.html文件导入项目中,修改为category/category-list.ftlcategory/category-add.ftl,处理文件内代码,建议先处理静态文件商品分类.html然后再导入项目,因为文件过大,直接导入项目可能会导致IDEA很卡。

在.ftl文件顶部设置变量ctx,其值为项目的根路径

<!-- 设置项目根路径全局变量 -->
<#assign ctx=request.contextPath/>

Controller控制层

GoodsController.java

package com.xxxx.manager.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

/**
 * 商品管理
 *
 * @author zhoubin
 * @since 1.0.0
 */
@Controller
@RequestMapping("goods")
public class GoodsController {

   /**
    * 商品分类-添加-页面跳转
    * @return
    */
   @RequestMapping("category/add")
   public String categoryAdd(){
      return "goods/category/category-add";
   }

   /**
    * 商品分类-列表-页面跳转
    * @return
    */
   @RequestMapping("category/list")
   public String categoryList(){
      return "goods/category/category-list";
   }
}

访问测试

修改ftl页面对应的访问地址,然后启动项目进行测试。

index.ftl

<li onclick="makecss(this)" data-id="categoryList_Goods">
   <a href='${ctx}/goods/category/list' target='rightContent'><i class="fa fa-circle-o"></i>商品分类</a>
</li>

category-list.ftl

<div class="col-md-2">
<a href="${ctx}/ goods/category/add" class="btn btn-primary pull-right">
<i class="fafa-plus"></i>新增分类</a>
</div>

配置访问根路径对应资源操作

因为每次访问根页面都需要加一个index/,为了方便省事,我们可以通过页面跳转方式来实现:

PageController.java

/**
 * 跳转首页
 *
 * @return
 */
@RequestMapping("/")
public String index() {
    System.out.println("index");
    return "index";
}

MyBatis逆向工程generator的使用

generator是什么?

​ 是一个逆向工程,用于自动生成mapper.xml,mapper.java,pojo

generator能干什么?

​ 对于单表而言,几乎是一个全能的工具,能很大提高编程效率。更多的关注业务逻辑的实现。

怎么使用?

创建一个generator项目

generator本身和我们商城项目没有关联,所以可以单独新建为一个Project,这边也做成Maven聚合项目里的一个子项目

添加依赖
<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.xxxx</groupId>
  <artifactId>shop-generator</artifactId>
  <version>1.0-SNAPSHOT</version>

  <!-- 继承 shop-parent 依赖 -->
  <parent>
    <groupId>com.xxxx</groupId>
    <artifactId>shop</artifactId>
    <version>1.0-SNAPSHOT</version>
  </parent>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
  </properties>

  <dependencies>
    <!-- mybatis 依赖(让生成的 pojo 和 mapper 不缺少注解相关类型) -->
    <dependency>
      <groupId>org.mybatis.spring.boot</groupId>
      <artifactId>mybatis-spring-boot-starter</artifactId>
    </dependency>
    <!-- mybatis generator core 依赖 -->
    <dependency>
      <groupId>org.mybatis.generator</groupId>
      <artifactId>mybatis-generator-core</artifactId>
      <version>1.3.7</version>
    </dependency>
  </dependencies>

  <!-- build 标签常用于添加插件及编译配置 -->
  <build>
    <plugins>
      <!-- mybatis generator plugin 依赖 -->
      <plugin>
        <groupId>org.mybatis.generator</groupId>
        <artifactId>mybatis-generator-maven-plugin</artifactId>
        <version>1.3.7</version>
        <configuration>
          <verbose>true</verbose>
          <!-- 是否覆盖 -->
          <overwrite>true</overwrite>
          <!-- 自动生成的配置 -->
          <configurationFile>src/main/resources/mybatis-generator.xml</configurationFile>
        </configuration>
        <dependencies>
          <!-- mysql 数据库依赖 -->
          <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>${mysql.version}</version>
          </dependency>
          <!-- mybatis generator core 依赖 -->
          <dependency>
            <groupId>org.mybatis.generator</groupId>
            <artifactId>mybatis-generator-core</artifactId>
            <version>1.3.7</version>
          </dependency>
          <!-- 将项目打包至本地仓库并添加依赖 -->
          <dependency>
            <groupId>com.xxxx</groupId>
            <artifactId>shop-generator</artifactId>
            <version>1.0-SNAPSHOT</version>
          </dependency>
        </dependencies>
      </plugin>
    </plugins>
  </build>
</project>
编写mybatis-generator.xml配置文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
		PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
		"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">

<!-- 配置生成器 -->
<generatorConfiguration>
	<context id="MysqlTables" targetRuntime="MyBatis3">
		<!-- 生成的Java文件的编码 -->
		<property name="javaFileEncoding" value="UTF-8"/>
		<!-- 增加Models ToStirng方法 -->
		<plugin type="org.mybatis.generator.plugins.ToStringPlugin"/>
		<!-- 增加Models Serializable实现 -->
		<plugin type="org.mybatis.generator.plugins.SerializablePlugin"/>

		<!-- 分页插件 -->
		<!-- 在example类中增 page 属性,并在mapper.xml的查询中加入page !=null 时的查询 -->
		<!-- <plugin type="org.mybatis.generator.plugins.MySQLPagerPlugin" /> -->
		<!-- 在example类中增 offset和limit属性,并在mapper.xml的查询中加入limit ${offset} , ${limit} 提供在offset和limit>0时的查询 -->
		<!-- <plugin type="org.mybatis.generator.plugins.MySQLPaginationPlugin"></plugin> -->

		<!-- 是否去除自动生成的注释 true:是 : false:否 -->
		<!-- type指定生成注释使用的对象 -->
		<commentGenerator type="com.xxxx.generator.ShopCommentGenerator">
			<property name="suppressAllComments" value="false"/>
		</commentGenerator>

		<!-- mysql数据库连接配置 -->
		<jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"
		                connectionURL="jdbc:mysql://localhost:3306/shop?useUnicode=true&amp;characterEncoding=UTF-8&amp;serverTimezone=Asia/Shanghai&amp;tinyInt1isBit=false"
		                userId="root" password="root">
		</jdbcConnection>

		<!--
			是否忽略BigDecimals 非必填项
				自动生成Java对象的时候,会根据number类型的长度不同生成不同的数据类型
					number长度   Java类型
					1~4          Short
					5~9          Integer
					10~18        Long
					18+          BigDecimal
		 -->
		<javaTypeResolver>
			<property name="forceBigDecimals" value="false"/>
		</javaTypeResolver>

		<!-- 以下内容,需要改动 -->
		<!-- java类生成的位置  -->
		<javaModelGenerator targetPackage="com.xxxx.generator.pojo" targetProject="src/main/java">
			<!-- 在targetPackage的基础上,根据数据库的schema再生成一层package,最终生成的类放在这个package下,默认为false -->
			<property name="enableSubPackages" value="true"/>
			<!-- 从数据库返回的值去除前后空格 -->
			<property name="trimStrings" value="true"/>
		</javaModelGenerator>

		<!-- *Mapper.xml配置文件生成的位置 -->
		<sqlMapGenerator targetPackage="com.xxxx.generator.mapper" targetProject="src/main/java">
			<!-- 是否让schema作为包后缀 -->
			<property name="enableSubPackages" value="true"/>
		</sqlMapGenerator>

		<!-- java mapper接口生成的位置(interface) -->
		<javaClientGenerator type="XMLMAPPER" targetPackage="com.xxxx.generator.mapper" targetProject="src/main/java">
			<!-- 是否让schema作为包后缀 -->
			<property name="enableSubPackages" value="true"/>
		</javaClientGenerator>

		<!--
			指定数据库表
				tableName数据库表名
				domainObjectName生成的实体类名
				是否需要mapper配置文件加入sql的where条件查询,需要将enableCountByExample等设为true,会生成一个对应domainObjectName的Example类
		 -->
		<table tableName="t_order_goods" domainObjectName="OrderGoods"
		       enableCountByExample="true" enableDeleteByExample="true"
		       enableSelectByExample="true" enableUpdateByExample="true">
			<!-- 用于insert时,返回主键的编号 -->
			<generatedKey column="id" sqlStatement="MySql" identity="true"/>
		</table>
	</context>
</generatorConfiguration>
ShopCommentGenerator工具类
package com.xxxx.generator;

import org.mybatis.generator.api.CommentGenerator;
import org.mybatis.generator.api.IntrospectedColumn;
import org.mybatis.generator.api.IntrospectedTable;
import org.mybatis.generator.api.dom.java.CompilationUnit;
import org.mybatis.generator.api.dom.java.Field;
import org.mybatis.generator.api.dom.java.FullyQualifiedJavaType;
import org.mybatis.generator.api.dom.java.InnerClass;
import org.mybatis.generator.api.dom.java.InnerEnum;
import org.mybatis.generator.api.dom.java.JavaElement;
import org.mybatis.generator.api.dom.java.Method;
import org.mybatis.generator.api.dom.java.TopLevelClass;
import org.mybatis.generator.api.dom.xml.XmlElement;
import org.mybatis.generator.config.PropertyRegistry;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Properties;
import java.util.Set;

import static org.mybatis.generator.internal.util.StringUtility.isTrue;

/**
 * 生成注释配置类
 */
public class ShopCommentGenerator implements CommentGenerator {

	/**
	 * properties属性,即配置在 commentGenerator 标签之内的 Property 标签
	 */
	private Properties properties;
	/**
	 * properties配置文件
	 */
	private Properties systemPro;
	/*
	 * 是否生成日期
	 */
	private boolean suppressDate;
	/**
	 * 是否生成注释
	 */
	private boolean suppressAllComments;
	/**
	 * 日期格式
	 */
	private String currentDateStr;

	public ShopCommentGenerator() {
		super();
		properties = new Properties();
		systemPro = System.getProperties();
		suppressDate = false;
		suppressAllComments = false;
		currentDateStr = DateTimeFormatter.ofPattern("yyyy/MM/dd").format(LocalDateTime.now());
	}

	/**
	 * 此方法返回格式化的日期字符串以包含在Javadoc标记中和XML注释。
	 *
	 * @return
	 */
	protected String getDateString() {
		String result = null;
		if (!suppressDate) {
			result = currentDateStr;
		}
		return result;
	}

	/**
	 * 从该配置中的任何属性添加此实例的属性CommentGenerator配置。
	 *
	 * @param properties
	 */
	@Override
	public void addConfigurationProperties(Properties properties) {
		this.properties.putAll(properties);
		suppressDate = isTrue(properties.getProperty(PropertyRegistry.COMMENT_GENERATOR_SUPPRESS_DATE));
		suppressAllComments = isTrue(properties.getProperty(PropertyRegistry.COMMENT_GENERATOR_SUPPRESS_ALL_COMMENTS));
	}

	/**
	 * 为字段添加注释
	 *
	 * @param field
	 * @param introspectedTable
	 * @param introspectedColumn
	 */
	@Override
	public void addFieldComment(Field field, IntrospectedTable introspectedTable, IntrospectedColumn introspectedColumn) {
		if (suppressAllComments)
			return;
		StringBuilder sb = new StringBuilder();
		field.addJavaDocLine("/**");
		sb.append(" * ");
		sb.append(introspectedColumn.getRemarks());
		field.addJavaDocLine(sb.toString().replace("\n", " "));
		field.addJavaDocLine(" */");
	}

	/**
	 * Java 属性注释
	 *
	 * @param field
	 * @param introspectedTable
	 */
	@Override
	public void addFieldComment(Field field, IntrospectedTable introspectedTable) {
		if (suppressAllComments)
			return;
		StringBuilder sb = new StringBuilder();
		field.addJavaDocLine("/**");
		sb.append(" * ");
		sb.append(introspectedTable.getFullyQualifiedTable());
		field.addJavaDocLine(sb.toString().replace("\n", " "));
		field.addJavaDocLine(" */");
	}

	/**
	 * 为模型类添加注释
	 *
	 * @param topLevelClass
	 * @param introspectedTable
	 */
	@Override
	public void addModelClassComment(TopLevelClass topLevelClass, IntrospectedTable introspectedTable) {
		if (suppressAllComments) {
			return;
		}
		topLevelClass.addJavaDocLine("/**");
		topLevelClass.addJavaDocLine(" * @author zhoubin ");
		topLevelClass.addJavaDocLine(" * @since 1.0.0");
		topLevelClass.addJavaDocLine(" */");
	}

	/**
	 * Java类的类注释
	 *
	 * @param innerClass
	 * @param introspectedTable
	 */
	@Override
	public void addClassComment(InnerClass innerClass, IntrospectedTable introspectedTable) {
		if (suppressAllComments) {
			return;
		}
		// 获取表注释
		String remarks = introspectedTable.getRemarks();
		innerClass.addJavaDocLine("/**");
		innerClass.addJavaDocLine("/* "+remarks);
		innerClass.addJavaDocLine(" * @author zhoubin ");
		innerClass.addJavaDocLine(" * @since 1.0.0");
		innerClass.addJavaDocLine(" */");
	}

	/**
	 * 为类添加注释
	 *
	 * @param innerClass
	 * @param introspectedTable
	 * @param b
	 */
	@Override
	public void addClassComment(InnerClass innerClass, IntrospectedTable introspectedTable, boolean b) {
		// 获取表注释
		String remarks = introspectedTable.getRemarks();
		innerClass.addJavaDocLine("/**");
		innerClass.addJavaDocLine("/* "+remarks);
		innerClass.addJavaDocLine(" * @author zhoubin ");
		innerClass.addJavaDocLine(" * @since 1.0.0");
		innerClass.addJavaDocLine(" */");
	}

	/**
	 * 为枚举添加注释
	 *
	 * @param innerEnum
	 * @param introspectedTable
	 */
	@Override
	public void addEnumComment(InnerEnum innerEnum, IntrospectedTable introspectedTable) {
		if (suppressAllComments)
			return;
		StringBuilder sb = new StringBuilder();
		innerEnum.addJavaDocLine("/**");
		sb.append(" * ");
		sb.append(introspectedTable.getFullyQualifiedTable());
		innerEnum.addJavaDocLine(sb.toString().replace("\n", " "));
		innerEnum.addJavaDocLine(" */");
	}

	/**
	 * 给getter方法加注释
	 *
	 * @param method
	 * @param introspectedTable
	 * @param introspectedColumn
	 */
	@Override
	public void addGetterComment(Method method, IntrospectedTable introspectedTable, IntrospectedColumn introspectedColumn) {
	}

	/**
	 * 给setter方法加注释
	 *
	 * @param method
	 * @param introspectedTable
	 * @param introspectedColumn
	 */
	@Override
	public void addSetterComment(Method method, IntrospectedTable introspectedTable, IntrospectedColumn introspectedColumn) {
	}

	/**
	 * 普通方法的注释,这里主要是XXXMapper.java里面的接口方法的注释
	 *
	 * @param method
	 * @param introspectedTable
	 */
	@Override
	public void addGeneralMethodComment(Method method, IntrospectedTable introspectedTable) {
	}

	/**
	 * 给Java文件加注释,这个注释是在文件的顶部,也就是package上面。
	 *
	 * @param compilationUnit
	 */
	@Override
	public void addJavaFileComment(CompilationUnit compilationUnit) {
	}

	/**
	 * Mybatis的Mapper.xml文件里面的注释
	 *
	 * @param xmlElement
	 */
	@Override
	public void addComment(XmlElement xmlElement) {
	}

	/**
	 * 此方法为其添加了自定义javadoc标签。
	 *
	 * @param javaElement
	 * @param markAsDoNotDelete
	 */
	protected void addJavadocTag(JavaElement javaElement, boolean markAsDoNotDelete) {
	}

	/**
	 * 为调用此方法作为根元素的第一个子节点添加注释。
	 *
	 * @param xmlElement
	 */
	@Override
	public void addRootComment(XmlElement xmlElement) {
	}

	@Override
	public void addGeneralMethodAnnotation(Method method, IntrospectedTable introspectedTable, Set<FullyQualifiedJavaType> set) {
	}

	@Override
	public void addGeneralMethodAnnotation(Method method, IntrospectedTable introspectedTable, IntrospectedColumn introspectedColumn, Set<FullyQualifiedJavaType> set) {
	}

	@Override
	public void addFieldAnnotation(Field field, IntrospectedTable introspectedTable, Set<FullyQualifiedJavaType> set) {
	}

	@Override
	public void addFieldAnnotation(Field field, IntrospectedTable introspectedTable, IntrospectedColumn introspectedColumn, Set<FullyQualifiedJavaType> set) {
	}

	@Override
	public void addClassAnnotation(InnerClass innerClass, IntrospectedTable introspectedTable, Set<FullyQualifiedJavaType> set) {
	}

}
执行

执行前一定要先install,否则有可能报错

结果

商品分类-新增分类-级联查询

Service服务层

GoodsCategoryService.java

package com.xxxx.manager.service;

import com.xxxx.manager.pojo.GoodsCategory;

import java.util.List;

/**
 * 商品分类服务接口
 *
 * @author zhoubin
 * @since 1.0.0
 */
public interface GoodsCategoryService {
   /**
    * 商品分类-新增分类-查询所有顶级分类
    * @return
    */
   List<GoodsCategory> selectCategoryTopList();

   /**
    * 商品分类-新增分类-根据父id查询子分类
    */
   List<GoodsCategory> selectCategoryListByParentId(short parentId);

   /**
    * 商品分类-新增分类-查询商品分类(通用)
    */
   List<GoodsCategory> selectCategoryList(short parentId);
}

GoodsCategoryServiceImpl.java

package com.xxxx.manager.service.impl;

import com.xxxx.manager.mapper.GoodsCategoryMapper;
import com.xxxx.manager.pojo.GoodsCategory;
import com.xxxx.manager.pojo.GoodsCategoryExample;
import com.xxxx.manager.service.GoodsCategoryService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * 商品分类服务类
 *
 * @author zhoubin
 * @since 1.0.0
 */
@Service
public class GoodsCategoryServiceImpl implements GoodsCategoryService {

   @Autowired
   private GoodsCategoryMapper goodsCategoryMapper;

   /**
    * 商品分类-新增分类-查询所有顶级分类
    * @return
    */
   @Override
   public List<GoodsCategory> selectCategoryTopList() {
      // 创建查询对象
      GoodsCategoryExample example = new GoodsCategoryExample();
      // where条件
      example.createCriteria().andParentIdEqualTo((short) 0);
      return goodsCategoryMapper.selectByExample(example);
   }

   /**
    * 商品分类-新增分类-根据父id查询子分类
    */
   @Override
   public List<GoodsCategory> selectCategoryListByParentId(short parentId) {
      // 创建查询对象
      GoodsCategoryExample example = new GoodsCategoryExample();
      // where条件
      example.createCriteria().andParentIdEqualTo(parentId);
      return goodsCategoryMapper.selectByExample(example);
   }

   /**
    * 商品分类-新增分类-查询商品分类(通用)
    */
   @Override
   public List<GoodsCategory> selectCategoryList(short parentId) {
      // 创建查询对象
      GoodsCategoryExample example = new GoodsCategoryExample();
      // where条件
      example.createCriteria().andParentIdEqualTo(parentId);
      return goodsCategoryMapper.selectByExample(example);
   }
}

Controller控制层

GoodsController.java

package com.ego.manager.controller;

import com.xxxx.manager.pojo.GoodsCategory;
import com.xxxx.manager.service.GoodsCategoryService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.List;

/**
 * 商品
 *
 * @author zhoubin
 * @since 1.0.0
 */
@Controller
@RequestMapping("goods")
public class GoodsController {

   @Autowired
   private GoodsCategoryService goodsCategoryService;

   /**
    * 商品分类-添加-页面跳转
    * @return
    */
   @RequestMapping("category/add")
   public String categoryAdd(Model model){
      List<GoodsCategory> gcList = goodsCategoryService.selectCategoryTopList();
      model.addAttribute("gcList",gcList);
      return "goods/category/category-add";
   }

   /**
    * 商品分类-列表-页面跳转
    * @return
    */
   @RequestMapping("category/list")
   public String categoryList(){
      return "goods/category/category-list";
   }

   /**
    * 商品分类-添加-分类联查
    * @param parentId
    * @return
    */
   @RequestMapping("category/{parentId}")
   @ResponseBody
   public List<GoodsCategory> selectCategoryList(@PathVariable Short parentId){
      return goodsCategoryService.selectCategoryList(parentId);
   }
}

ftl页面

category-add.ftl

<div class="col-sm-3">
    <select name="parent_id_1" id="parent_id_1"
            onchange="getCategory(this.value,'parent_id_2','0');"
            class="small form-control">
        <option value="0">顶级分类</option>
        <#list gcList as gc>
            <option value="${gc.id}">${gc.name}</option>
        </#list>
    </select>
</div>
/**
 * 获取多级联动的商品分类
 * id:当前选择框的值
 * next:下级选择框显示的内容
 * select_id:
 */
function getCategory(id, next, select_id) {
    var url = '${ctx}/goods/category/' + id;
// 用户重新选择顶级分类时,重置下级分类为:请选择商品分类,且清空下级分类信息
    var htmlStr = "<option value='0'>请选择商品分类</option>";
    if(0==id){
        $("#"+next).html(htmlStr);
        return;
    }
    $.ajax({
        type: "GET",
        url: url,
        error: function (request) {
            layer.alert("获取子分类失败!");
        },
        success: function (result) {
            if (result.length > 0) {
                for (i = 0; i < result.length; i++) {
                    htmlStr += "<option value='" + result[i].id + "'>" + result[i].name + "</option>"
                }
                $("#"+next).html(htmlStr);
            }else {
                layer.alert("获取子分类失败!");
            }
        }
    });
}

商品分类-新增分类-保存

编写公共保存状态返回对象

shop-common的BaseResultEnum.java

package com.xxxx.common.enums;

/**
 * 为什么使用枚举类?
 *  1.提升代码的阅读性,避免硬编码。
 *  2.程序解耦
 */
public enum BaseResultEnum {
    // 自定义枚举类
    SUCCESS(200, "成功"),
    ERROR(400, "失败"),
    PASS_ERROR_01(501, "两次输入的密码不一致"),
    PASS_ERROR_02(502, "输入的原始密码不正确"),
    PASS_ERROR_03(503, "输入的验证码不正确"),
    PASS_ERROR_04(504, "用户名或者密码错误");


    private Integer code;
    private String message;

    private BaseResultEnum(Integer code, String message) {
        this.code = code;
        this.message = message;
    }

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

}

shop-common的BaseResult.java

package com.xxxx.common.result;

import com.github.pagehelper.PageInfo;
import com.xxxx.common.enums.BaseResultEnum;

import java.io.Serializable;

/**
 * 公共保存状态返回对象
 */
public class BaseResult implements Serializable {
    // 状态编码
    private Integer code;
    // 状态描述
    private String message;
    // 分页对象(商品列表需要-pom.xml添加依赖)
    PageInfo<?> pageInfo;

    //成功返回的对象
    public static BaseResult success() {
        BaseResult result = new BaseResult();
        result.setCode(BaseResultEnum.SUCCESS.getCode());
        result.setMessage(BaseResultEnum.SUCCESS.getMessage());
        return result;
    }

    //成功返回的对象-带分页对象
    public static BaseResult success(PageInfo<?> pageInfo) {
        BaseResult result = new BaseResult();
        result.setCode(BaseResultEnum.SUCCESS.getCode());
        result.setMessage(BaseResultEnum.SUCCESS.getMessage());
        result.setPageInfo(pageInfo);
        return result;
    }

    //失败返回的对象
    public static BaseResult error() {
        BaseResult result = new BaseResult();
        result.setCode(BaseResultEnum.ERROR.getCode());
        result.setMessage(BaseResultEnum.ERROR.getMessage());
        return result;
    }

    public BaseResult(Integer code, String message) {
        this.code = code;
        this.message = message;
    }

    public BaseResult() {
    }

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public PageInfo<?> getPageInfo() {
        return pageInfo;
    }

    public void setPageInfo(PageInfo<?> pageInfo) {
        this.pageInfo = pageInfo;
    }

    @Override
    public String toString() {
        return "BaseResult [code=" + code + ", message=" + message + "]";
    }
}

Service服务层

GoodsCategoryService.java

/**
 * 商品分类-新增分类-保存
 * @param goodsCategory
 * @return
 */
int categorySave(GoodsCategory goodsCategory);

GoodsCategoryServiceImpl.java

/**
 * 商品分类-新增分类-保存
 * @param goodsCategory
 * @return
 */
@Override
public int categorySave(GoodsCategory goodsCategory) {
   return goodsCategoryMapper.insertSelective(goodsCategory);
}

Controller控制层

GoodsController.java

/**
 * 商品分类-添加-保存
 *
 * @param goodsCategory
 * @return
 */
@RequestMapping("category/save")
@ResponseBody
public BaseResult CategorySave(GoodsCategory goodsCategory) {
   int i = goodsCategoryService.categorySave(goodsCategory);
   //三目运算符
   return i > 0 ? BaseResult.success() : BaseResult.error();
}

ftl页面

category-add.ftl

添加l两个隐藏域,根据用户操作onchange动态赋值传往后台

<input type="hidden" name="parentId" id="parentId" value="0">
<input type="hidden" name="level" id="level" value="1">
<div class="col-sm-3">
    <select name="parent_id_1" id="parent_id_1"
            onchange="getCategory(this.value,'parent_id_2','0');"
            class="small form-control">
        <option value="0">顶级分类</option>
        <#list gcList as gc>
            <option value="${gc.id}">${gc.name}</option>
        </#list>
    </select>
</div>
<div class="col-sm-3">
    <select name="parent_id_2" id="parent_id_2" class="small form-control"
            onchange="setParentId(this.value,'3')">
        <option value="0">请选择商品分类</option>
    </select>
</div>
<div class="box-footer">
    <input type="hidden" name="id" value="">
    <button type="reset" class="btn btn-primary pull-left"><i class="icon-ok"></i>重填</button>
    <button type="button"
            onclick="ajaxSubmitForm();"
            class="btn btn-primary pull-right"><i class="icon-ok"></i>提交
    </button>
</div>

修改页面form表单中name的域名称与GoodsCategory对象中的属性一一对应

js动态赋值,实现保存新增分类

/**
 * 获取多级联动的商品分类
 * id:当前选择框的值
 * next:下级选择框显示的内容
 * select_id:level
 */
function getCategory(id, next, select_id) {
    var url = '${ctx}/goods/category/' + id;
    // 用户重新选择顶级分类时,重置下级分类为:请选择商品分类,且清空下级分类信息
    var htmlStr = "<option value='0'>请选择商品分类</option>";
    //修改parentId
    $("#parentId").val(id);
    if (0 == id) {
        $("#" + next).html(htmlStr);
        //修改level
        $("#level").val(1);
        return;
    }
    $.ajax({
        type: "GET",
        url: url,
        error: function (request) {
            layer.alert("获取子分类失败!");
        },
        success: function (result) {
            if (result.length > 0) {
                for (i = 0; i < result.length; i++) {
                    htmlStr += "<option value='" + result[i].id + "'>" + result[i].name + "</option>"
                }
                $("#" + next).html(htmlStr);
                $("#level").val(2);
            } else {
                layer.alert("获取子分类失败!");
            }
        }
    });
}

/**
 *保存分类
 */
function ajaxSubmitForm() {
    $.ajax({
        url: "${ctx}/goods/category/save",
        type: "post",
        data: $("#category_form").serialize(),
        dataType:"JSON",
        success:function (result) {
            if (result.code==200){
                layer.confirm("保存成功",{btn:['继续新增','返回列表']},
                    function () {
                        window.location.href="${ctx}/goods/category/add";
                    },function () {
                        window.location.href="${ctx}/goods/category/list";
                });
            }else {
                layer.alert("保存失败");
            }
        },
        error:function () {
            layer.alert("保存失败");
        }
    });
}

/**
 * 设置parentId和level
 */
function setParentId(parentId,level) {
    // 修改parentId和level
    if (0==parentId){
        $("#parentId").val($("#parent_id_1").val);
        $("#level").val(2);
        return;
    }
    // 修改parentId和level
    $("#parentId").val(parentId);
    $("#level").val(level);
}

商品分类-列表页

分析页面结构

页面三级之间并无直接关系,而是由html、css页面样式实现了分级样式。

页面结构三个普通tr,不同等级的tr缩进值(左边距)不同。

第一级<td align="left" style="padding-left:5em"></td>
第二级<td align="left" style="padding-left:10em"></td>
第三级<td align="left" style="padding-left:15em"></td>

后台只需要获取每一级的商品列表内容返回页面循环处理展示即可

后台代码实现

shop-manager-pojo添加VO对象

GoodsCategoryVo.java

package com.xxxx.manager.vo;

import java.io.Serializable;
import java.util.List;

/**
 * @author zhoubin 
 * @since 1.0.0
 */
public class GoodsCategoryVo implements Serializable {
    
    /**
    *商品下级分类列表
    */
   private List<GoodsCategoryVo> children;
    /**
     * 商品分类id
     */
    private Short id;

    /**
     * 商品分类名称
     */
    private String name;

    /**
     * 手机端显示的商品分类名
     */
    private String mobileName;

    /**
     * 父id
     */
    private Short parentId;

    /**
     * 家族图谱
     */
    private String parentIdPath;

    /**
     * 等级
     */
    private Byte level;

    /**
     * 顺序排序
     */
    private Byte sortOrder;

    /**
     * 是否显示
     */
    private Byte isShow;

    /**
     * 分类图片
     */
    private String image;

    /**
     * 是否推荐为热门分类
     */
    private Byte isHot;

    /**
     * 分类分组默认0
     */
    private Byte catGroup;

    /**
     * 分佣比例
     */
    private Byte commissionRate;

    /**
     * t_goods_category
     */
    private static final long serialVersionUID = 1L;

    public List<GoodsCategoryVo> getChildren() {
        return children;
    }

    public void setChildren(List<GoodsCategoryVo> children) {
        this.children = children;
    }

    public Short getId() {
        return id;
    }

    public void setId(Short id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name == null ? null : name.trim();
    }

    public String getMobileName() {
        return mobileName;
    }

    public void setMobileName(String mobileName) {
        this.mobileName = mobileName == null ? null : mobileName.trim();
    }

    public Short getParentId() {
        return parentId;
    }

    public void setParentId(Short parentId) {
        this.parentId = parentId;
    }

    public String getParentIdPath() {
        return parentIdPath;
    }

    public void setParentIdPath(String parentIdPath) {
        this.parentIdPath = parentIdPath == null ? null : parentIdPath.trim();
    }

    public Byte getLevel() {
        return level;
    }

    public void setLevel(Byte level) {
        this.level = level;
    }

    public Byte getSortOrder() {
        return sortOrder;
    }

    public void setSortOrder(Byte sortOrder) {
        this.sortOrder = sortOrder;
    }

    public Byte getIsShow() {
        return isShow;
    }

    public void setIsShow(Byte isShow) {
        this.isShow = isShow;
    }

    public String getImage() {
        return image;
    }

    public void setImage(String image) {
        this.image = image == null ? null : image.trim();
    }

    public Byte getIsHot() {
        return isHot;
    }

    public void setIsHot(Byte isHot) {
        this.isHot = isHot;
    }

    public Byte getCatGroup() {
        return catGroup;
    }

    public void setCatGroup(Byte catGroup) {
        this.catGroup = catGroup;
    }

    public Byte getCommissionRate() {
        return commissionRate;
    }

    public void setCommissionRate(Byte commissionRate) {
        this.commissionRate = commissionRate;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append(getClass().getSimpleName());
        sb.append(" [");
        sb.append("Hash = ").append(hashCode());
        sb.append(", id=").append(id);
        sb.append(", name=").append(name);
        sb.append(", mobileName=").append(mobileName);
        sb.append(", parentId=").append(parentId);
        sb.append(", parentIdPath=").append(parentIdPath);
        sb.append(", level=").append(level);
        sb.append(", sortOrder=").append(sortOrder);
        sb.append(", isShow=").append(isShow);
        sb.append(", image=").append(image);
        sb.append(", isHot=").append(isHot);
        sb.append(", catGroup=").append(catGroup);
        sb.append(", commissionRate=").append(commissionRate);
        sb.append("]");
        return sb.toString();
    }
}
Service服务层

GoodsCategoryService.java

/**
 * 商品分类-列表
 * @return
 */
List<GoodsCategoryVo> selectCategoryListForView();

GoodsCategoryServiceImpl.java

/**
 * 商品分类-列表
 * @return
 */
@Override
public List<GoodsCategoryVo> selectCategoryListForView() {
   //创建查询对象
   GoodsCategoryExample example = new GoodsCategoryExample();
   //where条件 parentId=0 and level = 1
   example.createCriteria().andParentIdEqualTo((short) 0).andLevelEqualTo((byte) 1);
   //查询一级分类
   List<GoodsCategory> gcList01 = goodsCategoryMapper.selectByExample(example);
   //处理一级分类查询下级分类
   List<GoodsCategoryVo> gcvList01 = new ArrayList<>();
   for (GoodsCategory gc01 : gcList01) {
      //将GoodsCategory对象转成GoodsCategoryVo对象
      GoodsCategoryVo gcv01 = new GoodsCategoryVo();
      BeanUtils.copyProperties(gc01,gcv01);
   // 清空查询对象
      example.clear();
   //设置查询参数
      example.createCriteria().andParentIdEqualTo(gcv01.getId()).andLevelEqualTo((byte) 2);
      //查询二级分类
      List<GoodsCategory> gcList02 = goodsCategoryMapper.selectByExample(example);
      //处理二级分类查询下级分类
      List<GoodsCategoryVo> gcvList02 = new ArrayList<>();
      for (GoodsCategoryVo gc02 : gcvList02) {
         //将GoodsCategory对象转成GoodsCategoryVo对象
         GoodsCategoryVo gcv02 = new GoodsCategoryVo();
         BeanUtils.copyProperties(gc02,gcv02);
         // 清空查询对象
         example.clear();
         //设置查询参数
         example.createCriteria().andParentIdEqualTo(gcv02.getId()).andLevelEqualTo((byte) 3);
         //查询三级分类
         List<GoodsCategory> gcList03 = goodsCategoryMapper.selectByExample(example);
      // 处理三级分类添加至二级二级分类vo对象
         List<GoodsCategoryVo> gcvList03 = new ArrayList<>();
         for (GoodsCategory gc03 : gcList03) {
            //将GoodsCategory对象转成GoodsCategoryVo对象
            GoodsCategoryVo gcv03 = new GoodsCategoryVo();
            BeanUtils.copyProperties(gc03,gcv03);
            //将对象添加至三级分类vo对象List
            gcvList03.add(gcv03);
         }
      // 将三级分类List添加至二级分类
         gcv02.setChildren(gcvList03);
      // 讲对象添加至二级分类Vo对象List
         gcvList02.add(gcv02);
      }
   // 将二级分类List添加至一级分类
      gcv01.setChildren(gcvList02);
   // 将对象添加至一级分类Vo对象List
      gcvList01.add(gcv01);
   }
    
    //=====================JDK8新特性=========================
    //创建查询对象
    // GoodsCategoryExample example = new GoodsCategoryExample();
    // //查询所有的商品分类list
    // List<GoodsCategory> list = goodsCategoryMapper.selectByExample(example);
    // //将List<GoodsCategory>转成List<GoodsCategoryVo>
    // List<GoodsCategoryVo> gcvList = list.stream().map(e -> {
    // 	GoodsCategoryVo gcv = new GoodsCategoryVo();
    // 	BeanUtils.copyProperties(e, gcv);
    // 	return gcv;
    // }).collect(Collectors.toList());
    // /**
    //  * 将List<GoodsCategoryVo>转成Map<parentId,List<GoodsCategoryVo>>
    //  * 按parentId分组,key为parentId,value为parentId对应的List
    //  */
    // Map<Short, List<GoodsCategoryVo>> map =
    // 		gcvList.stream().collect(Collectors.groupingBy(GoodsCategoryVo::getParentId));
    // /**
    //  * 循环,将childrenList赋值
    //  */
    // gcvList.forEach(e -> e.setChildrenList(map.get(e.getId())));
    // /**
    //  * 拦截器,返回Level为1的List,也就是顶级分类
    //  */
    // List<GoodsCategoryVo> gcvList01 = gcvList.stream().filter(e -> 1 == e.getLevel()).collect(Collectors.toList
    // ());
    //=====================JDK8新特性=========================
    
   return gcvList01;
}
Service服务层的测试类

GoodsCategoryServiceITest.java

package com.xxxx.manager;

import com.xxxx.manager.service.GoodsCategoryService;
import com.xxxx.manager.vo.GoodsCategoryVo;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.List;

/**
 * 商品分类服务测试类
 *
 * @author zhoubin
 * @since 1.0.0
 */
@SpringBootTest(classes = ManagerApplication.class)
public class GoodsCategoryServiceTest {

   @Autowired
   private GoodsCategoryService goodsCategoryService;

   @Test
   public void testSelectCategoryListForView(){
      List<GoodsCategoryVo> gcvList = goodsCategoryService.selectCategoryListForView();
      System.out.println(gcvList);
   }
}
Controller控制层

GoodsController.java

/**
 * 商品分类-列表-页面跳转
 *
 * @return
 */
@RequestMapping("category/list")
public String categoryList(Model model) {
   List<GoodsCategoryVo> gcvList = goodsCategoryService.selectCategoryListForView();
   model.addAttribute("gcvList",gcvList);
   return "goods/category/category-list";
}

ftl页面

category-list.ftl 的<div class="box-body"></div>中的所有内容

<div class="box-body">
    <div class="row">
        <div class="col-sm-12">
            <table id="list-table" class="table table-bordered table-striped dataTable" role="grid"
                   aria-describedby="example1_info">
                <thead>
                <tr role="row">
                    <th valign="middle">分类ID</th>
                    <th valign="middle">分类名称</th>
                    <th valign="middle">手机显示名称</th>
                    <th valign="middle">是否推荐</th>
                    <th valign="middle">是否显示</th>
                    <th valign="middle">分组</th>
                    <th valign="middle">排序</th>
                    <th valign="middle">操作</th>
                </tr>
                </thead>
                <tbody>
                <#list gcvList as gcv01>
                    <tr role="row" align="center" class="1" id="1_1">
                        <td>${gcv01.id}</td>
                        <td align="left" style="padding-left:5em">
                        <span class="glyphicon glyphicon-plus btn-warning"
                              style="padding:2px; font-size:12px;" id="icon_1_1" aria-hidden="false"
                              onclick="rowClicked(this)"></span>&nbsp; <span>${gcv01.name}</span>
                        </td>
                        <td><span>${gcv01.mobileName}</span></td>
                        <td>
                            <img width="20" height="20" src="${ctx}/images/yes.png"
                                 onclick="changeTableVal('goods_category','id','1','is_hot',this)"/>
                        </td>
                        <td>
                            <img width="20" height="20" src="${ctx}/images/yes.png"
                                 onclick="changeTableVal('goods_category','id','1','is_show',this)"/>
                        </td>
                        <td>
                            <input type="text"
                                   onchange="updateSort('goods_category','id','1','cat_group',this)"
                                   onkeyup="this.value=this.value.replace(/[^\d]/g,'')"
                                   onpaste="this.value=this.value.replace(/[^\d]/g,'')" size="4"
                                   value="${gcv01.catGroup}" class="input-sm"/>
                        </td>
                        <td>
                            <input type="text"
                                   onchange="updateSort('goods_category','id','1','sort_order',this)"
                                   onkeyup="this.value=this.value.replace(/[^\d]/g,'')"
                                   onpaste="this.value=this.value.replace(/[^\d]/g,'')" size="4"
                                   value="${gcv01.sortOrder}" class="input-sm"/>
                        </td>
                        <td>
                            <a class="btn btn-primary" href="商品分类-编辑.html"><i class="fa fa-pencil"></i></a>
                            <a class="btn btn-danger"
                               href="javascript:del_fun('/index/Admin/Goods/delGoodsCategory/id/1');"><i
                                        class="fa fa-trash-o"></i></a>
                        </td>
                    </tr>
                    <#list gcv01.children as gcv02>
                        <tr role="row" align="center" class="2" id="2_12" style="display:none">
                            <td>${gcv02.id}</td>
                            <td align="left" style="padding-left:10em">
                        <span class="glyphicon glyphicon-plus btn-warning"
                              style="padding:2px; font-size:12px;" id="icon_2_12"
                              aria-hidden="false" onclick="rowClicked(this)"></span>&nbsp;
                                <span>${gcv02.name}</span>
                            </td>
                            <td><span>${gcv02.mobileName}</span></td>
                            <td>
                                <img width="20" height="20" src="${ctx}/images/cancel.png"
                                     onclick="changeTableVal('goods_category','id','12','is_hot',this)"/>
                            </td>
                            <td>
                                <img width="20" height="20" src="${ctx}/images/yes.png"
                                     onclick="changeTableVal('goods_category','id','12','is_show',this)"/>
                            </td>
                            <td>
                                <input type="text"
                                       onchange="updateSort('goods_category','id','12','cat_group',this)"
                                       onkeyup="this.value=this.value.replace(/[^\d]/g,'')"
                                       onpaste="this.value=this.value.replace(/[^\d]/g,'')" size="4"
                                       value="${gcv02.catGroup}" class="input-sm"/>
                            </td>
                            <td>
                                <input type="text"
                                       onchange="updateSort('goods_category','id','12','sort_order',this)"
                                       onkeyup="this.value=this.value.replace(/[^\d]/g,'')"
                                       onpaste="this.value=this.value.replace(/[^\d]/g,'')" size="4"
                                       value="${gcv02.sortOrder}" class="input-sm"/>
                            </td>
                            <td>
                                <a class="btn btn-primary"
                                   href="/index/Admin/Goods/addEditCategory/id/12"><i
                                            class="fa fa-pencil"></i></a>
                                <a class="btn btn-danger"
                                   href="javascript:del_fun('/index/Admin/Goods/delGoodsCategory/id/12');"><i
                                            class="fa fa-trash-o"></i></a>
                            </td>
                        </tr>
                        <#list gcv02.children as gcv03>
                            <tr role="row" align="center" class="3" id="3_100" style="display:none">
                                <td>${gcv03.id}</td>
                                <td align="left" style="padding-left:15em">
                                    <span>${gcv03.name}</span>
                                </td>
                                <td><span>${gcv03.mobileName}</span></td>
                                <td>
                                    <img width="20" height="20" src="${ctx}/images/cancel.png"
                                         onclick="changeTableVal('goods_category','id','100','is_hot',this)"/>
                                </td>
                                <td>
                                    <img width="20" height="20" src="${ctx}/images/yes.png"
                                         onclick="changeTableVal('goods_category','id','100','is_show',this)"/>
                                </td>
                                <td>
                                    <input type="text"
                                           onchange="updateSort('goods_category','id','100','cat_group',this)"
                                           onkeyup="this.value=this.value.replace(/[^\d]/g,'')"
                                           onpaste="this.value=this.value.replace(/[^\d]/g,'')" size="4"
                                           value="${gcv03.catGroup}" class="input-sm"/>
                                </td>
                                <td>
                                    <input type="text"
                                           onchange="updateSort('goods_category','id','100','sort_order',this)"
                                           onkeyup="this.value=this.value.replace(/[^\d]/g,'')"
                                           onpaste="this.value=this.value.replace(/[^\d]/g,'')" size="4"
                                           value="${gcv03.sortOrder}" class="input-sm"/>
                                </td>
                                <td>
                                    <a class="btn btn-primary" href="/index/Admin/Goods/addEditCategory/id/100"><i
                                                class="fa fa-pencil"></i></a>
                                    <a class="btn btn-danger"
                                       href="javascript:del_fun('/index/Admin/Goods/delGoodsCategory/id/100');"><i
                                                class="fa fa-trash-o"></i></a>
                                </td>
                            </tr>
                        </#list>
                    </#list>
                </#list>
                </tbody>
            </table>
        </div>
    </div>
    <div class="row">
        <div class="col-sm-5">
            <div class="dataTables_info" id="example1_info" role="status" aria-live="polite">分页
            </div>
        </div>
    </div>
</div>
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值