说明:将大型项目按照特定的规则进行拆分.
目的:减少项目架构的耦合性. (化整为零 拆! )
思考:单体项目存在什么问题?
如果在一个大型项目中,将所有的功能模块(eg:登录模块,权限模块,订单模块······)都写在一个服务器上,将来如果其中一个模块出现了问题,gg了,那么整个项目都会gg掉。
而项目如果做成“分布式”架构,即使一个模块gg了,其他模块不受影响,还能继续运行。
说明:按照特定的模块进行拆分,之后各自独立的运行.相互之间不受影响.
但有时候,按照功能业务拆分后,一个个“系统”范围还是很大,需要进一步拆分,这样就能把责任具体落实到单个人。
问题:虽然引入了分布式思想.但是同时也带来了一些问题,怎么解决???
jt项目的父级工程叫做:jt
用来统一管理第三方的jar包,统一各个子项目用的jar包的版本号。
这些jar包通常都是第三方的大厂写的开源的。在我的项目中我直接拿来用就好了。
jt父级工程,它是一个聚合工程,它里面包含了很多的子项目,eg:jt-common,jt-manager…
jt父级工程的最小单位是一个个jar包,而子项目继承了父工程,那么各个子工程也能用父类下的第三方jar包文件了。
各个子项目 继承了 父级工程jt
而我在写各个子项目时,公司的大佬会提前写好一个个工具API,并打成jar包,让我去调用,规范我的代码的书写。
各个子项目 依赖于 大佬写的工具API的jar包
而大佬在写工具API时也是参照第三方大厂的jar包文件写的,最后也会打成jar包文件。
所以 本公司大佬写的 工具API 也是 继承了 父级工程jt
我就可以通过Thymeleaf这个模板引擎,将服务器传回来的数据绑定到页面上。
当前后端完全分录后,前端通过node.js封装前端框架。前端工程师只需要写html/css/js。
前端也会有自己的端口号。
前端的请求数据是由后端服务器提供的。
首先请求发送到后端的“服务消费者”,即Controller层(DispatcherServlet)。
业务比较麻烦时,再在后端的内部,由Controller将请求发送给业务层,数据层等,即发送给(服务提供者)。
提供js的公司会把它们的js产品文件放到cdn服务器上。而cdn服务器会自动选择一个离我最近的一个服务器,让我从它上下载js资源。
jt项目是由一个父工程jt,里面包裹着众多子项目jt-common,jt-manage…构成的。
可以通过项目打包的方式区分它们。
通常,项目的打包方式分3种:
jar (通常工具API都打成jar包(eg:jt-common),springboot中一般项目也可以打成jar包)
war (业务项目可以打成war包,也可以打成jar包,eg:jt-manage)
pom (父级项目都要打成pom包)
一般在一个项目的pom.xml文件中,通过标签,来定义这个项目的打包方式。
如果不写这个标签,这个项目就是默认的打jar包。
整个jt项目中在创建项目时,创建的都是普通的maven项目。这里不用选择“原型”,直接下一步。
一个个原型 ,就是一个个骨架。
我创建的maven长什么样,有什么包结构什么,都可以从不同的骨架去选择。
但现在都是微服务了,骨架这里已经不用选了。直接下一步。
GroupId 是在定义maven的坐标,一般都是公司域名倒着写。
填写完此页的内容,直接finish。
上一步完成后,会自动生成一个父级jt的pom文件,需要手动加一些依赖。
<?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.jt</groupId>
<artifactId>jt</artifactId>
<version>1.0-SNAPSHOT</version>
<!--我是父级工程,是聚合项目可以包含子项目-->
<packaging>pom</packaging>
<!--parent标签作用: 定义了SpringBoot中所有关联项目的版本号信息.-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<java.version>1.8</java.version>
<!--项目打包时,跳过测试类打包-->
<skipTests>true</skipTests>
</properties>
<!--开箱即用:SpringBoot项目只需要引入少量的jar包及配置,即可拥有其功能.
spring-boot-starter 拥有开箱即用的能力.
maven项目中依赖具有传递性.
A 依赖 B 依赖 C项目 导入A bc会自动依赖
-->
<dependencies>
<!--直接依赖web springMVC配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<!--springBoot-start SpringBoot启动项 -->
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--SpringBoot重构了测试方式 可以在测试类中 直接引入依赖对象-->
<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>
<!--支持热部署 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<!--引入插件lombok 自动的set/get/构造方法插件 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--引入数据库驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!--springBoot数据库连接 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!--spring整合mybatis-plus 只导入MP包,删除mybatis包 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.2.0</version>
</dependency>
<!--springBoot整合JSP添加依赖 -->
<!--servlet依赖 -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
</dependency>
<!--jstl依赖 -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
</dependency>
<!--使jsp页面生效 -->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
</dependency>
<!--添加httpClient jar包 -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<!--引入dubbo配置 -->
<!--<dependency>
<groupId>com.alibaba.boot</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>0.2.0</version>
</dependency>-->
<!--添加Quartz的支持 -->
<!--<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>-->
<!-- 引入aop支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!--spring整合redis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
</dependency>
</dependencies>
<!--build标签只有添加了主启动类的java文件才需要 jt是父级工程只做jar包的定义.-->
</project>
这些API是老师提前已经写好的,目前是两个pojo类,我只是CV进了我的项目中。
我在别的子项目,eg:jt-manage中会用到。
注意:如果项目名错了,尽量重新建一个项目,直接改原项目名会引来很多不必要的麻烦。—记一坑
先将druid数据源和driver-class-name 这两行注释掉
server:
port: 8091
servlet:
context-path: /
spring:
datasource:
#引入druid数据源
#type: com.alibaba.druid.pool.DruidDataSource
#driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/jtdb?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true
username: root
password: root
mvc:
view:
prefix: /WEB-INF/views/
suffix: .jsp
#mybatis-plush配置
mybatis-plus:
type-aliases-package: com.jt.pojo
mapper-locations: classpath:/mybatis/mappers/*.xml
configuration:
map-underscore-to-camel-case: true
logging:
level:
com.jt.mapper: debug
引入jt.common依赖,并添加build标签
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.jt</groupId>
<artifactId>jt</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>jt-manage</artifactId>
<packaging>war</packaging>
<!--添加依赖-->
<dependencies>
<dependency>
<groupId>com.jt</groupId>
<artifactId>jt-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
<!--添加插件-->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
哪个子项目需要单独运行发布的,就在哪个项目的pom文件中添加build标签。
org.springframework.boot
spring-boot-maven-plugin
注意:
用idea创建时,pom文件中
而用STS创建时,pom文件中
所以不要盲目CV,切记切记
这是springboot提供的功能。
通常在DB项目中,我访问主页时输入的是:localhost:8080/doIndexUI…(controller中doIndexUI方法对应返回的就是主页:start.html)
但在第二阶段,WEB阶段,老师讲过,在我自己下载的Tomcat文件夹中,有一个web.xml配置的文件。
打开它以后,发现在最后面,它默认进行了如下配置。
似乎是有关“欢迎页面”的一些设置。
具体是啥,还是有点儿模糊。
我再看看项目启动后,控制台输出的内容
这时,老师又讲了,spring在将tomcat内嵌进来后,保留了tomcat的这个功能。
控制台这句话翻译过来也就是,“增加欢迎页面模板:index”
SpringBoot内部通过默认引擎 发送了一个index请求,但是这个请求不需要通过controller进行接收。
SpringBoot会自己拼接视图解析器的前缀和后缀,完成页面的跳转。
拼接完之后就成了:
/WEB-INF/views/index.jsp
正好就对应着我项目中的首页页面
所以,我只要输入localhost:8091就可以访问到index.jsp
easyui是一种基于jQuery、Angular.、Vue和React的用户界面插件集合。
easyui为创建现代化,互动,JavaScript应用程序,提供必要的功能。
使用easyui你不需要写很多代码,你只需要通过编写一些简单HTML标记,就可以定义用户界面。
easyui是个完美支持HTML5网页的完整框架。
easyui节省您网页开发的时间和规模。
easyui很简单但功能还是挺强大的。
以后不管用哪个UI框架,第一步,都需要引入人家自己的js和css样式。
easyui-layout.jsp 这个jsp就是用来定义页面布局的一个demo
第18行,第19行的data-options是 这个EasyUI框架提供的功能。
所以,我们要想用,就要遵守人家的规定。人家怎么规定,我就怎么用这个属性。
“region” :方位。 上北下南,左西右东。就是规定本div位于整合页面的哪里。
在任何UI框架中,要想展现树形结构,都离不开两个标签 :ul 和 li
li标签中的内容是无序的。
加上这个class=“easyui-tree” 才能成功展现树形结构。
要想展现“树”,就是< ul >和
在jsp中如果去掉这的if条件,那么点击几次“商品新增”,右面就会产生几个“商品新增”选项卡。
而加上这个if条件,意思就是“如果存在这个选项卡”,再次点击时这个选项卡就会被选中,而不是再新增一个。
iframe这个标签是画中画效果,相当于引入另一个页面
eg:点击“商品新增”后,在右侧显示“百度地图”
是不是很amazing。
这东西应急还行。
真格的时候 还得去调用百度地图对外的接口。
所谓的“通用页面跳转”的意思就是:
在点击这几个按钮时,在index.jsp中设置的是,客户端向服务器请求这3个地址。
通常情况下,客户端的每个请求,服务器的controller都会为之创建一个方法去接。
但有没有一个通用的一个方法,去接这些长得差不多的url请求呢?
那就用到restFul风格。
最大的功臣就是@PathVariable这个注解。
这个注解的作用就是,把客户端传过来的地址用{moduleName}来接收。再赋值给方法中的变量名 moduleName。
而方法结束后,会return 回相应的页面。
总结1:如果需要获取客户端发送的url地址请求中的参数时,则可以使用RestFul风格来实现。
总结2:可以按照客户端发送的请求的类型,让controller去执行特定的功能。
点击“商品查询”后,在右侧展现出商品信息
这些商品信息是服务端从数据库中查询出来的(按照一定的条件:第几页page,每页最多显示几条数据rows)。
通过一个demo来理解表格数据的呈现
data-options 就是当前这个EasyUI框架的一个属性标识符。
在data-options后面,我可以写很多我需要的属性。
在EasyUI中,它对ajax请求进行了动态的封装。
看到url这个属性信息,在EasyUI内部解析时,它就是一个ajax请求。
fitColumns:true表示自动适应,singleSelect:true 表示选中单个。
pagination:true表示这个表格有没有分页功能
客户端发送了ajax请求,服务器端最后会将数据响应回来。
那么是怎么将数据回显到页面上的呢?
就是通过这个,field后的code,name,price。这里的数据信息会和url请求得到的数据相绑定。
这个就是服务器响应回来的数据。
而服务器响应的数据 是怎么 和页面表格中的标签一一对应上的呢?
这两个地方的名字要对应上,要不然页面中的表格中会显示不出数据
这里的total和rows的数据肯定不是我手动一行一行敲上去的。
是服务器端将响应的结果绑定到一个VO对象上,再将这个VO对象封装成JSON格式的字符串,返回给客户端的。
JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。 易于人阅读和编写。同时也易于机器解析和生成。
官网:https://www.json.org/json-zh.html
两种。
1.Object格式
eg:
{"id":"1","name":"张三","age":"888"}
2.数组格式
eg:
[ '11','22','33','44','4567' ]
//这个对象是根据客户端的请求分析出来,客户端想要的Json对象中的两个属性,1个是total,1个是rows
//total就是一个整数,rows是一个嵌套的JSON,是一堆“code”,“name”,“price”组成的JSON,又组成的数组。
//见webapp/easy-ui/datagrid_data.json
@Data
@Accessors(chain = true)
@NoArgsConstructor
@AllArgsConstructor
public class EasyUITable {
private Integer total; //一共有多少数据
private List- rows; //每页要显示的Item们
}
当一个对象(object)需要在服务器与服务器之间进行传递时,(其实是将对象(object)转成字节数组,在服务器之间传递),必须要实现序列化接口。
JSON的本质是“字符串”,它不是对象(object)。所以在http协议中,字符串可以直接传递,不用转成字节数组。
所以在传输JSON时不需要将其序列化。
概念:序列化,就是将内存中的数据按照特定的规律,以字节的形式(0,1)进行排列组合,进行标识,通过IO流的形式将这些数据保存到磁盘中。
反序列化,将磁盘中的以字节形式(0,1)存储的数据,按照特定的规律进行解码,再通过IO流的形式存回内存中。
用途:就是 传输数据。(根据 类名称 属性/类型 属性值 将其重新组装回原来那个对象的样子)
商品ID
商品标题
叶子类目
卖点
价格
库存数量
条形码
状态
创建日期
更新日期
运行的规则:
在数据库中先按照分页信息 查询对应的Item信息,只返回这一部分Item信息给客户端。
即:
按照分页的规则,(比如,查第1页,显示20条),就从数据库中查出第1页,前20条的数据,返回给客户端;
想查 第2页,也显示20条,就从数据库中 再查出第2页 中 20条数据的记录,返回给客户端。
(绝对不是从数据库中把所有的数据都查出来再在客户端做分页显示)
打开主页后,按F12,点进NetWork sheet。
再点击“查询商品”,可见
状态是404,说明没找到url请求所对应的资源。
(那是肯定的,客户端老师已经提前写好了,而服务器端我还没有写呢)
客户端请求的URL是:http://localhost:8091/item/query?page=1&rows=20
由于我查询时 是带着这的分页信息查的,所以会将查询请求的参数拼接到url后面。
而当我改变分页信息中的任何一个参数(page,rows)如:
客户端发送的请求的url后拼接的参数也会相应的跟着变化。
客户端向服务端发送了Ajax请求
于是我要在Controller中写一个方法去接收请求。
@RestController //返回的是Json字符串 注意是RestController在这被坑了好久!
@RequestMapping("/item")
public class ItemController {
@Autowired
private ItemService itemService;
/**
* 业务需求:客户端点击“查询商品”后,分页显示出商品信息
* 客户端请求的url:http://localhost:8091/item/query?page=1&rows=20
* 客户端提交请求的方式:get
* 客户端传过来的参数:
* @param page 第几页
* @param rows 每页最多显示多少行Item数据
* @return 服务器给的返回值结果就是 当前页(第1页) 要显示的(20条)Item数据(EasyUITable)
*/
@RequestMapping("/query")
public EasyUITable findItemByPage(Integer page,Integer rows){
return itemService.findItemByPage(page,rows);
}
1.自下而上:Mapper----->Service-------->Controller-------->前端JS页面 eg:DB项目
2.自上而下:分析前端JS页面的需求--------------->Controller---------->Service----->Mapper eg: JT项目
当Controller中写完findItemByPage()方法后,会飘红色警告,根据提示,idea会自动帮我在ItemService中生成findItemByPage()方法。
再转战到ItemServiceImpl中,ALT+SHIFT+P,会出现如下框
就能自动生成findItemByPage()的重写方法了。
我在业务层要做的就是,接收到controller层发送过来的两个参数:page和rows后
调用ItemMapper去完成与数据库的交互环节,具体一点就是,根据page和rows,从数据库中查询出相应的信息,封装成EasyUITable对象,将来返回给Controller层。
这里,我用两种方式达成这个目标。
1.传统的手写Sql方式
@Override
public EasyUITable findItemByPage(Integer page, Integer rows) {
/**方式一:手写分页查询的service代码,和sql语句
* sql:select * from tb_item limit 起始位置(startIndex), 每页最多显示的条数(rows)
* 起始位置:startIndex 的理解
* 查询第1页:page=1 rows=20
* sql: select * from tb_item limit 0,20
* 查询第2页:page=2 rows=20
* sql: select * from tb_item limit 20,20
* 查询第3页:page=3 rows=20
* sql: select * from tb_item limit 40,20
*/
//1.计算起始位置的号startIndex
int startIndex = (page - 1) * rows; // 第2步
//2.通过itemMapper去调用我手写的findItemByPage()方法,去数据库中查询出,当页要显示的Item们的信息
List- itemList = itemMapper.findItemByPage(startIndex, rows); //这个方法是我自定义的,在ItemMapper中有对应的方法 第1步
//至此,已经得到了EasyUITable对象中的List
- 的值,还差total
//现在的目标,从数据库中查询出一共有多少条数据,即为total属性赋值
Integer total = itemMapper.countItems(); //这个方法是我自定义的,在ItemMapper中有对应的方法 // 第3步
//至此,total总数也从数据库中查询出来了。
//这样就可以把List
- 和total封装好,返回给controller了。把它俩赋值给一个EasyUITable对象,返回给controller。
//新建个EasyUITable对象
EasyUITable easyUITable = new EasyUITable(); // 第4步
easyUITable.setRows(itemList); // 第5步
easyUITable.setTotal(total); // 第6步
return easyUITable; // 第7步
}
2.借助于MyBatisPlus中的API
@Override
public EasyUITable findItemByPage(Integer page, Integer rows) {
/**方式二:通过MP中的API去查询出List- 和total
* 注意:利用MP进行分页查询时 是有条件的:
* 在进行分页查询时,MP必须添加配置类
*/
//通过MP中的selectPage方法去查出当前页要显示的Item们
//由于selectPage方法中的第一个参数是IPage类型的,所以我要先new一个IPage类型的对象出来,IPage是个接口
//由Page的底层源码可知,new对象时,可以传2个参数:(第几页:page,每页显示多少条数据:rows),正好就是controller将要传过来的那2个参数
IPage
- iPage = new Page<>(page,rows); //第2步
//当我把iPage和queryWrapper这两个参数传给MP中的selectPage()后,MP会自己执行分页操作,将需要的数据进行封装。
//我的查询条件是啥?new一个queryWrapper,来定义一下
QueryWrapper
- queryWrapper = new QueryWrapper<>(); //第5步
//根据修改时间updated降序排列
queryWrapper.orderByDesc("updated"); //第6步
//iPage身上绑定着selectPage方法查询出的所有分页信息
iPage = itemMapper.selectPage(iPage,queryWrapper); //第1步
//调取出iPage中的total
int total = (int)iPage.getTotal(); //第3步
//调取出iPage中符合条件的记录
List
- itemList = iPage.getRecords(); //第4步
EasyUITable easyUITable = new EasyUITable(total,itemList); //第7步
easyUITable.setTotal(total).setRows(itemList); //第8步
return easyUITable; //第9步
}
如果在service中 用的是第一种手写Sql的方法,那么还需要有ItemMapper这个数据层。
//@Mapper //由于在主启动类上加了@MapperScan("com.jt.mapper")注解,这里就不用写@Mapper注解了
public interface ItemMapper extends BaseMapper- {
//查询出当前页要显示的Item们 先排序 再分页
@Select("select * from tb_item order by updated desc limit #{startIndex},#{rows}")
List
- findItemByPage(int startIndex, Integer rows);
//查询出总共有多少条数据,total
@Select("select count(*) from tb_item")
Integer countItems();
}
如果是调用MP的API实现的结果查询,还需要编写一个配置类。
这个配置类的模板可以在MP的官网上找到。(快速入门-------核心功能--------分页插件)
package com.jt.config;
import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
//1.标识一下,这是一个配置类。写配置类的作用就是,给需要交给Spring管理的Bean对象(eg:PaginationInterceptor)进行一些配置
@Configuration //这个注解通常与@Bean注解一起使用
public class MybatisPlusConfig {
//2.将对象交给Spring管理
/**spring是怎么管理对象的呢?
* 会把对象放在一个超大的Map中。map
* 其中K就是方法名,V就是实例化之后的对象
* map
*/
@Bean
public PaginationInterceptor paginationInterceptor(){
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
return paginationInterceptor;
}
}
在主界面点击“查询商品”后,在右侧就能显示出Item的数据了。
注意,“叶子类目”那一列的信息还没有。(所谓的“叶子类目”,就是这个商品所属的类的名字)
接下来我就要想办法把“叶子类目”中的信息展现出来。
在解决这个问题之前,还要再仔细研究一下Item这个POJO
package com.jt.pojo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
import lombok.experimental.Accessors;
@JsonIgnoreProperties(ignoreUnknown=true) //表示JSON转化时忽略未知属性
@TableName("tb_item")
@Data
@Accessors(chain=true)
public class Item extends BasePojo{
@TableId(type=IdType.AUTO) //主键自增
private Long id; //商品id
private String title; //商品标题
private String sellPoint; //商品卖点信息
private Long price; //商品价格 Long > double 算算数时Long算得更快,并且double存在精度问题(算得不准)
//如果一个商品的价格是9.58元,它在存储到数据库中时会*100。数据库在将数据返回时,会再÷100。
private Integer num; //商品数量
private String barcode; //条形码
private String image; //商品图片信息 1.jpg,2.jpg,3.jpg
private Long cid; //表示商品的分类id
private Integer status; //1上架,2下架
//为了满足页面调用需求,添加get方法
public String[] getImages(){
return image.split(",");
}
}
问题1:price为啥是Long类型?
分析:通常price都是double类型,比如:3.58元。
而在程序中,通常会涉及到商品价格的计算。
一般情况下,整数的计算 速度 会 快于 小数的计算速度。
并且double类型的数在计算时,也会存在精度问题。(eg:0.0000000000001+0.999999999999999 会无限趋近于1 而不能等于1)
所以这里的price 选择Long类型。
问题2:客户端那边用户输入的价格是有小数的,eg:99.88元。
而我服务器这边用price接收时,由于price是Long类型,会自动把小数点部分舍掉,就成了99元!!!这不能忍!!!
所以:在前端JS代码中item-add.jsp中有这么一个方法:
客户端把用户输入的带小数点儿的钱数 (eg:99.88元)会*100,转化成分 (9988分),再传给服务器端。
而这时服务器端再用Long类型的price去接收时,就不会有误差了。
所以最后存到数据库中的钱数 的单位也是分。
问题3:image 代表的是商品的图片,那数据库中存的是图片本身吗?
答:不是。数据库中如果真存这种图片,会很占地方,查询的效率也会非常慢。
所以数据库中存储的是图片的 地址。
并且一个商品中通常会有多个图片。
这些图片就以字符串的形式存储在数据库中。(eg: 1.jpg,2.jpg,3.jpg…)
由于用“,”进行了分隔,以后就可以通过数组取值的方式,[0]就代表第1张图片,[1]代表第2张图片…
由5.4.9.1中的问题2中可知,数据库中price都是以“分”来存储的。
而页面上的数据展现给用户时是正常按“元”来展现的。
是怎么做到的呢?
原来在前端页面item-list中,客户端在接收服务器传回来的price时,price上还有这么一个属性:formatter:KindEditorUtil.formatPrice
若我把formatter:KindEditorUtil.formatPrice这个属性去掉,页面上呈现的price就是数据库中以“分”保存的形式。
可以看出 这一切都是KindEditorUtil.formatPrice的功劳。
formatter属性是EasyUI中专门 负责 格式化数据的函数。通过调用.js,将服务器传回来的结果 展现在页面上。
那么.formPrice这个方法在哪里呢?
答:在jt-manage------webapp------js-------common.js中
这个方法就是:客户端接收到服务器传回的price后,要÷100,再保留2位小数。就是以“元”为单位了。
formatPrice这个方法有2个参数:当前这个标签的值,也就是“价格”的值。(val)
当前这个标签的行号。(row)(这个row是这个方法规定要传的一个参数,我如果用不到它,就可以不管它。)
那么问题来了:
item-list.jsp 是怎么引用到 common.js中的方法的呢?它俩又不在同一个目录下,(⊙o⊙)…
问题的关键在于index.jsp !!!!!
啥??跟它有毛关系???
因为我最先登录进系统时,显示的主页就是index.jsp。
而index.jsp中引入了common-js.jsp。
而common-js.jsp中引入了common.js。
所以,也就是说,index.jsp这个整体可以引用common.js中的方法。
而item-list.jsp是index的一部分,相当于儿子。他爸爸index能用common.js,他item-list也就能用common.js。
由于商品的状态 可能是“上架”“下架”,过一段时间可能又叫别的名字了,如“正常”,“异常”…
数据库中最好不要这么存总是变的字符串信息。
如果以数字的形式代表 两种状态,将数字存到数据库中,就很好。
private Integer status; //1上架,2下架
在前端的item.jsp中也用了formatter:KindEditorUtil.formatItemStatus来格式化“状态”。
在common.js中是这样定义的
val就是服务器传回来的商品的status值。(1或2)
可见,叶子类目中 也有这个属性 调用这个方法
formatter:KindEditorUtil.findItemCatName
如果我把这个去掉,页面上显示如下;
“叶子类目”中展示了一些数字,这些数字就是 本商品对应的分类类目的id。
所以,现在的需求就是:
需要通过格式化的函数formatter:KindEditorUtil.findItemCatName ,动态地获取到这些id对应的分类类目的名称,并显示出来。
利用formatter:KindEditorUtil.findItemCatName这个函数的调用,向后端发送ajax请求,获取商品分类的名称。
在common.js中定义了indItemCatName这个方法
注意:这里先用ajax 发同步请求的方式。
稍后 会再用 ajax 发异步请求的方式,即async:false
由JT项目总体的物理模型图可知,商品的分类信息不在商品表中,它在tb_item_cat表中。
所以,要为这个tb_item_cat 创建一个pojo对象,用于封装 商品分类的ID与商品名称的关系。
注意,这个ItemCat的pojo要写在jt-common中。
package com.jt.pojo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.experimental.Accessors;
@TableName("tb_item_cat")
@Data
@Accessors(chain=true)
public class ItemCat extends BasePojo {
@TableId(type= IdType.AUTO)
private Long id; //商品分类的id
private Long parentId; // 对应的父级商品分类的id ,父类目ID=0时,代表的是一级的类目
private String name; // 商品分类的名称
private Integer status; //商品分类信息的状态 (正常 / 已删除)
private Integer sortOrder; //排列序号,表示同级类目的展现次序,如数值相等则按名称次序排列。取值范围:大于零的整数
private Boolean isParent; //该类目是否为父类目,1为true,0为false (在tb_item_cat表中,这项对应的字段是is_parent 类型是tinyint(1),以后看到tinyint(1)类型,在pojo中就用Boolean与之对应)
//created 和 updated在继承的BasePojo中
}
根据前端的JS代码可知,它发送的是ajax 同步请求
url:"/item/cat/queryItemName"
data:{itemCatId:val}
所以我要新建一个ItemCatController,里面要这么写
@RestController
@RequestMapping("/item/cat") //只写请求的业务名
public class ItemCatController {
@Autowired
private ItemCatService itemCatService;
/**
* 分析业务:通过itemCatId获取商品分裂的名称
* 1.url地址:url:"/item/cat/queryItemName"
* 2.参数:data:{itemCatId:val}
* 3.返回值:商品分类的名称 String
*
*/
@RequestMapping("/queryItemName")
public String findItemCatName(Long itemCatId){
//由于tb_item_cat表中的id是bigint类型,所以这里我要用Long类型来接收
return itemCatService.findItemCatNameById(itemCatId);
}
}
由于Controller层派发了任务,所以我又要新建一个ItemCatService,里面要这么写:
public interface ItemCatService {
String findItemCatNameById(Long itemCatId);
}
ItemCatService中接收到了任务,真正干活的还是ItemCatServiceImpl。
所以我又要新建一个ItemCatServiceImpl。
@Service
public class ItemCatServiceImpl implements ItemCatService{
@Autowired
private ItemCatMapper itemCatMapper;
@Override
public String findItemCatNameById(Long itemCatId) {
return itemCatMapper.selectById(itemCatId).getName();
}
}
有了ItemCat这个POJO对象,就要通过这个对象去数据库中查结果了,所以就要写ItemCatMapper了
package com.jt.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jt.pojo.ItemCat;
public interface ItemCatMapper extends BaseMapper {
}
由于我查数据库用的是MP,所以继承 BaseMapper ,我就可以不用手写sql了。
思考:
当前端改发Ajax异步请求时,会发生什么?
不知道会发生什么,试试看先。
服务器端的代码不动,只改客户端这里。
点进jt页面后发现,“叶子类目”又没了!!!!
这就是前端经常遇到的 Ajax请求嵌套问题
当我点完“查询商品”时,客户端一共向服务端发送了2次Ajax请求:
第一次是5.4.4.1中,发送的查询整个页面信息Item
第二次是5.4.9.11中,查询“叶子类目”ItemCat
当第一个查询Item的Ajax请求发出后,服务器返回的Item信息,会一行一行地显示在页面上。
eg:当查询第一行Item信息的Ajax请求发出后,服务器返回第一行Item的值,并刷新页面,把值展现在页面上。而Item的值中是没有包含“叶子类目”ItemCat的值的。
客户端又发送查询第一行ItemCat的请求,服务器返回第一行ItemCat的值。
但注意查询Item第2行,第3行的Ajax请求也在源源不断的发给着服务器,服务器不会等着把上一行的ItemCat返回并刷新页面后再去接查询Item第2行,第3行…的Ajax请求。
所以就会造成这种局面:
Item的信息展现在页面后,ItemCat的信息虽然已经由服务器返回来了,但没来得及刷新显示出来,也就是只是欠缺一次“刷新”。
所以5.4.9.11中,查询“叶子类目”ItemCat,这个Ajax请求,只能是发“同步”请求。
总结,如果在页面中涉及到Ajax请求嵌套的问题,通常的解决方法是:将内层的Ajax设置为同步模式,就可以正确展现数据了。
当我点击“选择类目”按钮时,在当前页面上就弹出一个框
这个效果是通过EasyUI框架中给定的功能实现的,(各大UI框架差不多都有这个功能)。
在本例中,webapp----easy-ui----easyui-5-window.html就是一个弹出框功能的小demo。
页面效果如图:
点击“搜索”,“退出系统”后也会弹出相应的框。
easyui-5-window.html中的代码如下:
<html>
<head>
<meta charset="UTF-8">
<title>EasyUI-3-菜单按钮title>
<script type="text/javascript"
src="/js/jquery-easyui-1.4.1/jquery.min.js">script>
<script type="text/javascript"
src="/js/jquery-easyui-1.4.1/jquery.easyui.min.js">script>
<script type="text/javascript"
src="/js/jquery-easyui-1.4.1/locale/easyui-lang-zh_CN.js">script>
<link rel="stylesheet" type="text/css"
href="/js/jquery-easyui-1.4.1/themes/icon.css" />
<link rel="stylesheet" type="text/css"
href="/js/jquery-easyui-1.4.1/themes/default/easyui.css" />
<script type="text/javascript">
$(function(){
//jQuery中的编程方式:函数式编程 (function(){})
$("#btn1").bind("click",function(){
//.window()是EasyUI框架提供的函数,功能是弹出一个框
$("#win1").window({
title:"弹出框",
width:400,
height:400,
modal:true //这是一个模式窗口,只能点击弹出框,不允许点击别处
})
})
$("#btn3").click(function(){
alert("关闭");
$("#win2").window("close");
});
/*定义退出消息框 */
$("#btn4").click(function(){
$.messager.confirm('退出','你确定要退出吗',function(r){
if (r){
alert("确认退出");
} else{
alert("点错了!");
}
});
})
/*定义消息提示框 */
$.messager.show({
title:'My Title',
msg:'郑哥你都胖成一个球了,圆的',
timeout:5000,
height:200,
width:300,
showType:'slide'
});
})
script>
head>
<body>
<h1>Easy-弹出窗口h1>
<a id="btn1" href="#" class="easyui-linkbutton" data-options="iconCls:'icon-search'">搜索a>
<div id="win1">div>
<div id="win2" class="easyui-window" title="My Window" style="width:600px;height:400px" data-options="iconCls:'icon-save',modal:true">
我是一个窗口
<a id="btn3" href="#" class="easyui-linkbutton" data-options="iconCls:'icon-back'">关闭a>
div>
<div style="float: right">
<a id="btn4" href="#" class="easyui-linkbutton" data-options="iconCls:'icon-cancel'">退出系统a>
div>
body>
html>
一般电商网站的 商品分类信息 都是分3级。级与级之间是父级与子级的关系。
那么在数据库中是怎么存储这种关系的呢?
答:涉及到父级与子级的关系时,一般用表中的parent_id字段进行关联。POJO中对应的属性名就是parentId
查询1级商品分类信息 其parent_id=0 SELECT ∗ \ast ∗ FROM tb_item_cat WHERE parent_id=0;
查询2级商品分类信息 其parent_id=1 SELECT ∗ \ast ∗ FROM tb_item_cat WHERE parent_id=1;
查询3级商品分类信息 其parent_id=2 SELECT ∗ \ast ∗ FROM tb_item_cat WHERE parent_id=2;
在本例中,webapp----easy-ui----easyui-7-tree.html就是一个 树形结构 的小demo。
页面展示如下:
Ztree ,treegride,Bootstrap中都提供了这种树形结构的实现方式。
easyui-7-tree.html中代码如下:
<html>
<head>
<meta charset="UTF-8">
<title>EasyUI-3-菜单按钮title>
<script type="text/javascript"
src="/js/jquery-easyui-1.4.1/jquery.min.js">script>
<script type="text/javascript"
src="/js/jquery-easyui-1.4.1/jquery.easyui.min.js">script>
<script type="text/javascript"
src="/js/jquery-easyui-1.4.1/locale/easyui-lang-zh_CN.js">script>
<link rel="stylesheet" type="text/css"
href="/js/jquery-easyui-1.4.1/themes/icon.css" />
<link rel="stylesheet" type="text/css"
href="/js/jquery-easyui-1.4.1/themes/default/easyui.css" />
<script type="text/javascript">
/*通过js创建树形结构 */
$(function(){
$("#tree").tree({
url:"tree.json", //加载远程JSON数据 看见url 就说明这个发的是一个ajax请求
method:"get", //请求方式 get
animate:true, //表示显示折叠端口动画效果
checkbox:true, //表述复选框
lines:true, //表示显示连接线
dnd:true, //是否拖拽
onClick:function(node){
//添加点击事件
//控制台
console.info(node);
}
});
})
script>
head>
<body>
<h1>EasyUI-树形结构h1>
<ul id="tree">ul>
body>
html>
表示树形结构的时候,通常用的都是
而url中请求的tree.json就在
点开tree.json后:
[
{
"id":"1",
"text":"英雄联盟",
"iconCls":"icon-save",
"children":[
{
"id":"4",
"text":"沙漠死神"
},{
"id":"5",
"text":"德玛西亚"
},{
"id":"6",
"text":"诺克萨斯之手"
},
{
"id":"7",
"text":"蛮族之王"
},
{
"id":"8",
"text":"孙悟空"
}
],
"state":"open"
},{
"id":"2",
"text":"王者荣耀",
"children":[
{
"id":"10",
"text":"阿科"
},{
"id":"11",
"text":"吕布"
},{
"id":"12",
"text":"陈咬金"
},{
"id":"13",
"text":"典韦"
}
],
"state":"closed" //决定着 这个节点 一开始 是“开着的” 还是“关闭的”
},
{
"id":"3",
"text":"吃鸡游戏",
"iconCls":"icon-save"
}
]
可见这个json整体是一个数组,将来我用一个List<>封装一下里面的各个子元素即可,就不用再给它弄一个pojo对象了。
数组内的子元素是一个一个的对象,总体来说,对象们差不多都有“id”和“text”两个属性。所以将来它对应的pojo中的属性肯定要有id和text。
在点击“选择类目”按钮后,要展现出选择类目的弹框,而弹框中要显示的就是 商品分类信息的全部的树结构EasyUITree。
所以我要弄一个EasyUITree对象,在它里面封装一些信息。
封装什么信息呢?
由上面的demo可知,一个tree是由很多节点组成的,而一个节点有3个属性:
节点的id
节点的名称 text
节点的状态(开启/关闭) state
所以EasyUITree的VO要这么写:
//这个对象的主要目的是为了展现树形结构的数据。
@Data
@Accessors(chain = true)
@NoArgsConstructor
@AllArgsConstructor
public class EasyUITree implements Serializable {
//由于在本例中我是用EasyUI这个框架中的树功能,人家的JS中规定树的节点就叫id,节点的名称就叫text,节点的状态就叫state。
//我不能起别的名字,所以这里这3个变量名只能写id,text,state
private Long id; //商品分类的Id信息
private String text; //商品分类的name
private String state; //节点是闭合的形式还是展开的形式。
//由是否为父级决定的。如果是父级,就是closed。如果是子级,就是open。
}
item-add.jsp中代码如下:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<link href="/js/kindeditor-4.1.10/themes/default/default.css" type="text/css" rel="stylesheet">
<script type="text/javascript" charset="utf-8" src="/js/kindeditor-4.1.10/kindeditor-all-min.js"></script>
<script type="text/javascript" charset="utf-8" src="/js/kindeditor-4.1.10/lang/zh_CN.js"></script>
<div style="padding:10px 10px 10px 10px">
<form id="itemAddForm" class="itemForm" method="post">
<table cellpadding="5">
<tr>
<td>商品类目:</td>
<td>
<a href="javascript:void(0)" class="easyui-linkbutton selectItemCat">选择类目</a>
<input type="hidden" name="cid" style="width: 280px;"></input>
</td>
</tr>
<tr>
<td>商品标题:</td>
<td><input class="easyui-textbox" type="text" name="title" data-options="required:true" style="width: 280px;"></input></td>
</tr>
<tr>
<td>商品卖点:</td>
<td><input class="easyui-textbox" name="sellPoint" data-options="multiline:true,validType:'length[0,150]'" style="height:60px;width: 280px;"></input></td>
</tr>
<tr>
<td>商品价格:</td>
<td><input class="easyui-numberbox" type="text" name="priceView" data-options="min:1,max:99999999,precision:2,required:true" />
<input type="hidden" name="price"/>
</td>
</tr>
<tr>
<td>库存数量:</td>
<td><input class="easyui-numberbox" type="text" name="num" data-options="min:1,max:99999999,precision:0,required:true" /></td>
</tr>
<tr>
<td>条形码:</td>
<td>
<input class="easyui-textbox" type="text" name="barcode" data-options="validType:'length[1,30]'" />
</td>
</tr>
<tr>
<td>商品图片:</td>
<td>
<a href="javascript:void(0)" class="easyui-linkbutton picFileUpload">上传图片</a>
<input type="hidden" name="image"/>
</td>
</tr>
<tr>
<td>商品描述:</td>
<td>
<textarea style="width:800px;height:300px;visibility:hidden;" name="itemDesc"></textarea>
</td>
</tr>
<tr class="params hide">
<td>商品规格:</td>
<td>
</td>
</tr>
</table>
<input type="hidden" name="itemParams"/>
</form>
<div style="padding:5px">
<a href="javascript:void(0)" class="easyui-linkbutton" onclick="submitForm()">提交</a>
<a href="javascript:void(0)" class="easyui-linkbutton" onclick="clearForm()">重置</a>
</div>
</div>
<script type="text/javascript">
var itemAddEditor ;
$(function(){
//和form下的itemDesc组件绑定
itemAddEditor = KindEditorUtil.createEditor("#itemAddForm [name=itemDesc]");
KindEditorUtil.init({
fun:function(node){
KindEditorUtil.changeItemParam(node, "itemAddForm");
}});
});
function submitForm(){
//表单校验
if(!$('#itemAddForm').form('validate')){
$.messager.alert('提示','表单还未填写完成!');
return ;
}
//转化价格单位,将元转化为分
$("#itemAddForm [name=price]").val(eval($("#itemAddForm [name=priceView]").val()) * 100);
itemAddEditor.sync();//将输入的内容同步到多行文本中
var paramJson = [];
$("#itemAddForm .params li").each(function(i,e){
var trs = $(e).find("tr");
var group = trs.eq(0).text();
var ps = [];
for(var i = 1;i<trs.length;i++){
var tr = trs.eq(i);
ps.push({
"k" : $.trim(tr.find("td").eq(0).find("span").text()),
"v" : $.trim(tr.find("input").val())
});
}
paramJson.push({
"group" : group,
"params": ps
});
});
paramJson = JSON.stringify(paramJson);//将对象转化为json字符串
$("#itemAddForm [name=itemParams]").val(paramJson);
/*$.post/get(url,JSON,function(data){....})
?id=1&title="天龙八部&key=value...."
*/
//alert($("#itemAddForm").serialize());
$.post("/item/save",$("#itemAddForm").serialize(), function(data){
if(data.status == 200){
$.messager.alert('提示','新增商品成功!');
}else{
$.messager.alert("提示","新增商品失败!");
}
});
}
function clearForm(){
$('#itemAddForm').form('reset');
itemAddEditor.html('');
}
</script>
注意:选择类目 的标签中有easyui-linkbutton selectItemCat这么两个属性。
其中selectItemCat这个属性的具体定义是在common.js中。
// 初始化选择类目组件
initItemCat : function(data){
$(".selectItemCat").each(function(i,e){//i= index 下标,e:element:元素
var _ele = $(e);
if(data && data.cid){
_ele.after(""+data.cid+"");
}else{
_ele.after("");
}
_ele.unbind('click').click(function(){
$("").css({padding:"5px"}).html("")
.window({
width:'500',
height:"450",
modal:true,
closed:true,
iconCls:'icon-save',
title:'选择类目',
onOpen : function(){ //当窗口打开后执行
var _win = this;
$("ul",_win).tree({
url:'/item/cat/list',
animate:true,
onClick : function(node){
if($(this).tree("isLeaf",node.target)){
// 填写到cid中
_ele.parent().find("[name=cid]").val(node.id);
_ele.next().text(node.text).attr("cid",node.id);
$(_win).window('close');
if(data && data.fun){
data.fun.call(this,node);
}
}
}
});
},
onClose : function(){
$(this).window("destroy");
}
}).window('open');
});
});
},
其中onOpen属性就是在实现 商品分类信息树,也就是EasyUITree,的展现。
看到了url:’/item/cat/list’ 说明这是在发送ajax请求,当服务器返回EasyUITree对象时,客户端的页面上就可以显示出这个树的样子。
5.5.4 新增商品 : 点击“选择类目”后,展现商品类型的树形结构 ItemCatController---------服务端Controller
前端发送来了 展现EasyUITree的请求,那我Controller就得接住。
这样,我在ItemCatController中要添加一个方法。
/**
* 分析业务:实现商品分类的树结构的展现
* 1.url地址:http://localhost/item/cat/list
* 2.参数: parentId 查询商品分类菜单
* 3.返回值结果: List
*/
@RequestMapping("/list")
public List findItemCatList(Long id){
//当初始时 树形结构 还没加载呢,不会传递ID,所以此时ID为null,所以此时parentId为0
Long parentId = (id==null)?0:id; //异步树控件加载
return itemCatService.findItemCatList(parentId);
}
知识点:这里用到了 异步树控件加载
jQuery中 EasyUI的API中 对异步树控件加载的解释:
树控件读取URL。子节点的加载依赖于父节点的状态。当展开一个封闭的节点,如果节点没有加载子节点,它将会把节点id的值作为http请求参数并命名为’id’,通过URL发送到服务器上面检索子节点。
简单总结:
(前提:得在页面上先至少展现出父级节点)当点击父级类目的节点时,会向后台继续传递该节点的id,以查询它下一级的类目的信息。
每点一次父级类目的名字,就进行了一次查询。
5.5.5 新增商品 : 点击“选择类目”后,展现商品类型的树形结构 ItemCatService---------服务端Service
ItemCatController层发来了命令,我就得在ItemCatService去新建个findItemCatList()方法。
public interface ItemCatService {
String findItemCatNameById(Long itemCatId);
List findItemCatList(Long parentId);
}
5.5.6 新增商品 : 点击“选择类目”后,展现商品类型的树形结构 ItemCatServiceImpl---------服务端Service
所以,在ItemCatServiceImpl中要实现EasyUITree树的对象的数据获得了。
增加方法:
/**
* 思路:
* 1.通过parentId查询数据库信息,返回值结果类型是List
* 2.将ItemCat信息转化为EasyUITree对象
* 3.返回的是List
* @param parentId
* @return
*/
@Override
public List findItemCatList(Long parentId) {
//1.根据父级分类id,查询数据
QueryWrapper queryWrapper = new QueryWrapper<>();
queryWrapper.eq("parent_id",parentId);
List catList = itemCatMapper.selectList(queryWrapper);
//2.将上一步查询到的数据catList进行转化,挑选想要的信息,封装成EasyUITree,添加到集合treeList中
List treeList = new ArrayList<>();
for (ItemCat itemCat: catList){
Long id = itemCat.getId();
String text = itemCat.getName();
//是父级,节点就打开,否则节点关闭
String state = itemCat.getIsParent()?"closed":"open";
EasyUITree easyUITree = new EasyUITree(id,text,state);
//3.将众多easyUITree对象 封装到treeList中
treeList.add(easyUITree);
}
return treeList;
}
至此,点击“选择类目”后,在弹出框中展示“商品分类的树结构”EasyUITree 功能完成!!!
5.5.7 商品新增:商品信息入库-------------------需求
在“新增商品”页面,需要做的主要有以下几点:
1.点“选择类目”后,展现个弹出框,在弹出框中展现商品类目的树形结构
2.“商品标题”,“商品卖点”,“商品价格”,“库存数量”,“条形码”均为Item信息,对应的是tb_item表。
3.“上传图片”功能先搁置,后面再实现。
4.商品描述为ItemDesc信息,对应的是tb_item_desc表
业务处理一般都会通过JSON串的形式告知客户端 程序是否完成了。通过一个VO对象来返回回执信息。
这个VO对象要封装的就是:业务是否正确 / 业务处理信息 / 业务处理后返回的数据
5.5.7.1 商品新增:Item信息的新增------封装SysResult 这个VO对象
无论是“新增”,“修改”还是“删除”,我在点完“提交”后,通常都需要弹出一个框,告诉我一下,我到底“新增”,“修改”,“删除”成功没有?
这个框中的信息通常包括:
status(状态,用数字表示,如200,201等)
msg(提示信息,如:服务调用成功)
但注意:页面上显示的这句话是在item-add.jsp中定义的
data(就是需要返回给客户端的数据,有时候是需要返回这个的)
所以就需要把这3个属性封装到一个VO对象中,本例中取名为SysResult。
由于SysResult这个对象是系统级别的,不仅jt-manage子系统会用到,如果有个别的jt子系统也可能会用到,所以要放在jt-common中。
@Data
@Accessors(chain = true)
@NoArgsConstructor
@AllArgsConstructor
public class SysResult implements Serializable {
/**
* 定义状态信息:
* 200:业务处理成功
* 201:业务处理失败
*/
private Integer status;
/*服务器返回的提示信息 比如:如果status为201时,异常,则在控制台中会显示对应的msg*/
private String msg;
/**服务器返回的业务数据 */
private Object data;
//写一些静态方法,方便后期别人就可以用这个对象.这些方法,告诉客户端,我服务器这边这次干活是成功了还是失败了。
//由于是static静态的,别人就可以直接调用
//服务器干活失败了
public static SysResult fail(){
return new SysResult(201,"调用服务器失败",null);
}
//服务器干活成功了
public static SysResult success(){
return new SysResult(200, "业务执行成功!",null);
}
//服务器干活成功了,并且把业务数据返回给客户端
public static SysResult success(Object data){
return new SysResult(200, "业务执行成功!",data);
}
}
5.5.7.2 商品新增:Item信息的新增------前端JS
暂时先只实现上半部Item信息的新增入库
由于后端的代码我还没写,前端的JS代码老师已经替我写好了。
此时在点击“提交”后,会报404
由此可知,客户端发送的Ajax请求的
url: http://localhost:8091/item/save
请求方式: POST
在页面中再往下翻,就能看见客户端提交ajax请求的参数
在item-add.jsp中也可以看到,客户端发送ajax请求的代码
在item-add.jsp中往上翻,可以看到“提交”标签的所有属性
显然,submitForm() 是老师自定义的一个点击事件方法,点击完“提交”之后,会触发这个方法。
具体这个方法是什么,需要看下面的定义:
function submitForm(){
//表单校验
if(!$('#itemAddForm').form('validate')){
$.messager.alert('提示','表单还未填写完成!');
return ;
}
//转化价格单位,将元转化为分
$("#itemAddForm [name=price]").val(eval($("#itemAddForm [name=priceView]").val()) * 100);
itemAddEditor.sync();//将输入的内容同步到多行文本中
var paramJson = [];
$("#itemAddForm .params li").each(function(i,e){
var trs = $(e).find("tr");
var group = trs.eq(0).text();
var ps = [];
for(var i = 1;i
注意1:eval的作用是规定做算数运算。
eg: 通常 我输入 20*50 代表的就是 两个数字相乘 。
但在js中 我输入 20 * 50 ,js不一定觉得我输入的是数字,也可能是字符串,那么 字符串20 * 数字50 结果会是error
而用eval()括起来的,就能保证是数字了,即使是这样eval( “20” ) 这个依然会被解析为 数字 20。
注意2:$("#itemAddForm").serialize() 是ajax请求中发送的参数
ajax请求中 参数的提交有2种方式:
1.json方式 $.post("/item/save",{“key1”:“value1”,“key2”:“value2”},回调函数)
2.key1=value1&key2=value2 $.post("/item/save",key1=value1&key2=value2,回调函数)
但如果客户端一次性提交了几百个数据,我也不能写几百个key呀==!
针对这个需求,jQuery中提供了一个函数叫 serialize() ,作用就是将“#itemAddForm”中的数据,(本例中就是表单中所有的数据),封装成key1=value1&key2=value2&key3=value3…的形式,如图:
注意3:$.post("/item/save",$("#itemAddForm").serialize(), function(data){…
中回调函数的参数data就是服务器端返回的VO对象 SysResult ,方法中就是用SysResult对象的status属性值是不是200,201来判断的。
注意4:由于“新增商品”和“商品查询”是两个页面,它们是兄弟关系,所以“新增商品”后,不会直接自动去“商品查询”。
5.5.7.3 商品新增:Item信息的新增------ItemController-----后端Controller
在ItemController中添加对应的方法。
/**
* 业务需求:完成商品入库操作,返回系统vo对象SysResult
* 客户端发送的请求url:http://localhost:8091/item/save
* 参数:整个form表单中的数据,都在item对象中。
* 返回值:SysResult对象
*/
@RequestMapping("/save")
public SysResult saveItem(Item item){
itemService.saveItem(item);
return SysResult.success();
//由于我不能保证用户输入的数据能100%入库成功,服务器或者数据库闹一些幺蛾子呢~~~所以得try{}catch(){}一下
//但这是很古老的方式了,后来就用定义全局异常处理类的方式 代替了这个方法
// try{
// itemService.saveItem(item);
// return SysResult.success();
// }catch (Exception e){
// e.printStackTrace();
// return SysResult.fail();
// }
}
注意:这里用我自定义个全局异常处理类的方式,代替了try{}catch(){}的写法
全局异常处理类
全局异常处理的作用:
如果在每个方法中都添加异常处理的try{}catch(){},则会导致整个代码非常冗余啰嗦,机构混乱。
所以全局异常处理类就可以帮我统一管理程序中出现的异常。
全局异常处理的原理:
AOP中的异常通知的原理,在Spring中是利用AOP的方式实现的。
因为是“全局的”,所以应该写在jt-common中。
package com.jt.aop;
import com.jt.vo.SysResult;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice //作用:标识我是一个通知方法,并且只拦截Controller层的异常,(Service层和Mapper层的异常抛给Controller层就行了),并且返回JSON
public class SysResultException {
//需要定义一个全局的方法,返回指定的报错信息。
//ExceptionHandler 配置异常的类型,这个例子中规定的是:遇到了运行时异常RuntimeException,就执行这个exception方法
@ExceptionHandler(RuntimeException.class)
public Object exception(Exception e){
e.printStackTrace();
return SysResult.fail();//SysResult.fail()会显示201,调用服务器失败。即告诉用户,我服务器这边出错了。
//但在页面上显示的信息是:新增商品失败! 是因为在item-add.jsp中第115行规定的。
}
}
5.5.7.4 商品新增:Item信息的新增------ItemService-----后端Service
在ItemService中新增方法,来接收ItemController中新的任务
//===增加:点击商品管理中的“新增商品”后,按要求输入好商品信息,点击“提交”后,将商品信息保存到数据库中
void saveItem(Item item);
5.5.7.5 商品新增:Item信息的新增------ItemServiceImpl-----后端ServiceImpl
实现新任务:
注意1:当在页面上点击了“提交”后,客户填写了Item的这几个信息,差不多跟Item的pojo的属性对应着呢。
但 仔细观察后发现,id和status没有传。
id在入库后会主键自增,不需要传。
而新增一个商品,提交后,一般状态都是是“上架”,所以可以在这里给status一个默认值 1 。
这个与common.js中的这里是对应的
//===增加:点击商品管理中的“新增商品”后,按要求输入好商品信息,点击“提交”后,将商品信息保存到数据库中
//===现在需要将上半部的商品信息item和下半部的商品详情信息itemDesc同时存入数据库中
@Transactional //控制事务
@Override
public void saveItem(Item item,) {
//1.设置默认商品的为上架状态
//item.setStatus(1).setCreated(new Date()).setUpdated(new Date());
//由于有了自动填充created和updated的配置,我就只给新增的商品设置个默认的上架状态就行了。(见下面的小优化)
item.setStatus(1);
//把页面上半部分的item信息入库了。
itemMapper.insert(item);
//下面这句代码 就是在验证Spring中的事务控制
//int a = 1/0; 有这句话时,就执行全局异常处理类中的异常处理方法
}
注意点:@Transactional 控制事务
如果方法上没有加@Transactional 来控制事务,当我代码的最后加了这句话
int a = 1/0;
就会发生:
我在点击“提交”后,给出了提示
但是,我再重新看我的数据表时,发现 我刚才的数据居然添加上了。
也就是说 “新增”这个操作应该回滚,而没回滚。
这就是没加
@Transactional 来控制事务的 弊端。
并且Spring一般只能控制运行时异常,检查异常还需要手动去封装提示的信息。
@Transactional的属性一般有如下5个:
@Transactional(timeout = 30,
readOnly = false,
isolation = Isolation.READ_COMMITTED,
rollbackFor = Throwable.class,
propagation = Propagation.REQUIRED)
结论:一般“增,删,改”都需要“控制事务”,而“查询”一般不需要。
5.6 商品信息修改 UPDATE
5.6.1 工具栏的介绍
在“商品查询”页面上有这么一行东西:
它就叫 工具栏 。
它是在item_list.jsp中,