电商行业发展
近年来,中国的电子商务快速发展,交易额连创新高,电子商务在各领域的应用不断拓展和深化、相关服务业蓬勃发展、支撑体系不断健全完善、创新的动力和能力 不断增强。电子商务正在与实体经济深度融合,进入规模性发展阶段,对经济社会生活的影响不断增大,正成为我国经济发展的新引擎。
中国电子商务研究中心数据显示,截止到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部署服务
-
思考:有什么问题?
-
模块之间耦合度太高,其中一个升级其他都得升级
-
开发困难,各个团队开发最后都要整合一起
-
系统的扩展性差
-
不能灵活的进行分布式部署。
解决方法:系统拆分
优点:把模块拆分成独立的工程,单点运行。如果某一个点压力大可以对这一个点单独增加配置。其他的点不受影响。
分布式架构:把系统按照模块拆分成多个子系统。
优点:
-
把模块拆分,使用接口通信,降低模块之间的耦合度。
-
把项目拆分成若干个子项目,不同的团队负责不同的子项目。
-
增加功能时只需要再增加一个子项目,调用其他系统的接口就可以。
-
可以灵活的进行分布式部署。
缺点:
- 系统之间交互需要使用远程通信,接口开发增加工作量。
技术选型
-
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(部署环境)
说明:单独使用可以随意选择版本,如果在集群下需要考虑多种技术之间的兼容性。比如:Redis
、Elasticsearch
可以使用单台服务器也可以使用多台服务器,使用多台服务器时需要考虑与其他技术的兼容性,或者统一都用高版本jar包,或者统一降低版本jar包,一高一低可能会出问题。
人员配置
-
产品经理:3人,确定需求以及给出产品原型图。
-
项目经理:1人,项目管理。
-
架构师:2人,负责解决技术难点与定制标准(开发准则、接口标准、技术选型等)。
-
前端团队:5人,根据产品经理给出的原型制作静态页面。
-
安卓团队:5人,开发android客户端。
-
IOS团队:5人,开发IOS客户端。
-
后端团队:20人,实现整个系统业务功能。
-
测试团队:5人,测试所有的功能、性能、安装。
-
运维团队:3人,项目的发布以及维护。
-
如果采用oracle,需要配备DBA:2人。
shop商城研发优势
- 节约成本
包括硬件成本和软件成本,硬件包括店面、房租、装修、印刷、纸张等最必须用品,软件包括网上商城购物系统、网络信息、图片、视屏等,都可长期使用、良性循环、经济和环保。
- 营销推广经济、便捷
传统媒体广告费用高昂,更适合于进行品牌塑造;而网络营销主要是策略与定位把控的问题,实惠很多,费用与传统媒体相比微乎其微,并且流量与用户也更加精准。
- 信息更加立体、全面
通过互联网,企业的信息展示、品牌塑造和形象宣传可以通过文字、图片、音频、视频等多维度进行现实与虚拟相结合的展示,使用户对企业的了解更加立体和全面,有助于形成良好的形象与口碑。
- 管理高效、便捷
运用信息化的数据库管理,各类信息精准、清晰、无误的保存,避免出现人工操作出现低级错误的情况,可随时查阅、核算、统计。
功能点
- 商家入驻
商家入驻功能包括:商家入驻申请流程、商家店铺自定义装修功能、多套店铺模板选择、商家店铺街展示、商家独立店铺功能、商铺报表统计功能、搜索店铺列表页、商家自定义广告位、区分平台与商家分类、类型、订单分单功能(按商家)、订单退换功能、商家订单佣金结算等。
- 会员中心
会员中心功能包括:会员中个人主页美化、会员中心订单列表美化、会员中心收货地址列表美化 、会员中心缺货登记、会员中心退换货、会员中心退换货详情页 、用户信息、新增会员头像上传功能 、平台红包、物流跟踪功能、资金管理
- 购物车
购物车功能包括:购物车选择购买功能、购物车加强功能、商品促销满减、满赠、折扣功能、凑单功能、简化购物流程、购物车为空时去购物功能、提示购物车商品是否有库存。
- 订单
订单功能包括:商家自定义配送方式和运费、门店自提功能、发票功能、商品无库存时提交订单弹窗提示继续购物、结算页面无货提交弹出框、快递配送方式选择如韵达。
- 积分商城
积分商城功能包括:积分商品列表、兑换商品排序、精品推荐、热门兑换、兑换商品详情页。
- 拍卖活动
拍卖活动功能包括:拍卖活动列表、竞拍商品排序和搜索、拍卖商品详情页、出价记录、商家店铺。
- 优惠活动
优惠活动功能包括:优惠活动列表页、优惠范围、优惠方式。
后台管理系统——项目框架搭建
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.ftl
和category/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&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&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> <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>
<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>