#database
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/tmall_springboot?characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=admin
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
#表结构自动生成策略(none)
spring.jpa.hibernate.ddl-auto = none
#thymeleaf,使用 thymeleaf 作为视图
#LEGACYHTML5允许非严格的html出现,元素少点什么也可以编译通过
spring.thymeleaf.mode=LEGACYHTML5
spring.thymeleaf.encoding=UTF-8
spring.thymeleaf.content-type=text/html
#cache=false 表示不要缓存,以免在开发过程中因为停留在缓存而给开发人员带来困扰
spring.thymeleaf.cache=false
#context
#上下文地址为 tmall_springboot, 所以访问的时候,都要加上这个
#比如:http://127.0.0.1:8080/tmall_springboot/admin
server.context-path=/tmall_springboot
#设置上传文件大小,默认只有1m
spring.http.multipart.maxFileSize=100Mb
spring.http.multipart.maxRequestSize=100Mb
#jpa对实体类的默认字段会把驼峰命名的属性
#转换为字段名的时候自动加上下划线。 这个配置的作用就是去掉下划线
#比如属性名称是 createDate, jpa 默认转换为字段名 create_Date
#有了这个配置之后,就会转换为同名字段 createDate
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
#显示 hibernate 执行的sql语句
#这个在上线之后,应该是关掉的,因为大量的 控制台输出会严重影响系统性能
#但是呢,因为本项目会和 redis 和 es 整合,打印 sql 语句的目的是为了观察 缓存是否起效果
spring.jpa.show-sql=true
log4j.rootLogger=debug, stdout, R
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
# Pattern to output the caller's file name and line number.
log4j.appender.stdout.layout.ConversionPattern=%5p [%t] (%F:%L) - %m%n
log4j.appender.R=org.apache.log4j.RollingFileAppender
log4j.appender.R.File=example.log
log4j.appender.R.MaxFileSize=100KB
# Keep one backup file
log4j.appender.R.MaxBackupIndex=5
log4j.appender.R.layout=org.apache.log4j.PatternLayout
log4j.appender.R.layout.ConversionPattern=%p %t %c - %m%n
<div class="navitagorDiv" th:fragment="html" xmlns:th="http://www.w3.org/1999/xhtml">
<nav class="navbar navbar-default navbar-fixed-top navbar-inverse">
<img style="margin-left:10px;margin-right:0px" class="pull-left" src="img/site/tmallbuy.png" height="45px">
<a class="navbar-brand" href="#nowhere">天猫后台</a>
<a class="navbar-brand" href="admin_category_list">分类管理</a>
<a class="navbar-brand" href="admin_user_list">用户管理</a>
<a class="navbar-brand" href="admin_order_list">订单管理</a>
</nav>
</div>
<template th:fragment="html(title)" xmlns:th="http://www.w3.org/1999/xhtml">
<!--2-用到的一系列的 js 和 css 文件-->
<script src="js/jquery/2.0.0/jquery.min.js"></script>
<link href="css/bootstrap/3.3.6/bootstrap.min.css" rel="stylesheet">
<script src="js/bootstrap/3.3.6/bootstrap.min.js"></script>
<script src="js/vue/2.5.16/vue.min.js"></script>
<script src="js/axios/0.17.1/axios.min.js"></script>
<script src="js/moment/2.22.2/moment.js"></script> <!-- vue.js 格式化日期用的 -->
<link href="css/back/style.css" rel="stylesheet">
<!--每个后台页面都在一开始使用了adminHeader.html-->
<!--3-各种自定义函数,这些函数都会在后台管理页面上用到-->
<script>
//判断是否为空
function checkEmpty(value,text){
if(null==value || value.length==0){
alert(text+ "不能为空");
return false;
}
return true;
}
//获取地址栏参数的函数
function getUrlParms(para){
var search=location.search; //页面URL的查询部分字符串
var arrPara=new Array(); //参数数组。数组单项为包含参数名和参数值的字符串,如“para=value”
var arrVal=new Array(); //参数值数组。用于存储查找到的参数值
if(search!=""){
var index=0;
search=search.substr(1); //去除开头的“?”
arrPara=search.split("&");
for(i in arrPara){
var paraPre=para+"="; //参数前缀。即参数名+“=”,如“para=”
if(arrPara[i].indexOf(paraPre)==0&& paraPre.length<arrPara[i].length){
arrVal[index]=decodeURI(arrPara[i].substr(paraPre.length)); //顺带URI解码避免出现乱码
index++;
}
}
}
if(arrVal.length==1){
return arrVal[0];
}else if(arrVal.length==0){
return null;
}else{
return arrVal;
}
}
//判断是否数字 (小数和整数)
function checkNumber(value, text){
if(value.length==0){
alert(text+ "不能为空");
return false;
}
if(isNaN(value)){
alert(text+ "必须是数字");
return false;
}
return true;
}
//判断是否整数
function checkInt(value, text){
if(value.length==0){
alert(text+ "不能为空");
return false;
}
if(parseInt(value)!=value){
alert(text+ "必须是整数");
return false;
}
return true;
}
//确实是否要删除
function checkDeleteLink(){
var confirmDelete = confirm("确认要删除");
if(confirmDelete)
return true;
return false;
}
//分页跳转函数,向前跳或者向后跳,或者跳转到第一页或者最后一页
function jump(page,vue){
if('first'== page && !vue.pagination.first)
vue.list(0);
else if('pre'== page && vue.pagination.hasPrevious )
vue.list(vue.pagination.number-1);
else if('next'== page && vue.pagination.hasNext)
vue.list(vue.pagination.number+1);
else if('last'== page && !vue.pagination.last)
vue.list(vue.pagination.totalPages-1);
}
//分页跳转函数,跳转到指定页
function jumpByNumber(start,vue){
if(start!=vue.pagination.number)
vue.list(start);
}
</script>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<!--把传递进来的 title 参数显示在 title 元素里-->
<title th:text="${title}" ></title>
</template>
//配置 Redis, 这个配置的作用主要是使得保存在 redis 里的key和value转换为如图所示的具有可读性的字符串,否则会是乱码,很不便于观察。
@Configuration
//Redis 缓存配置类
public class RedisConfig extends CachingConfigurerSupport {
@Bean
public CacheManager cacheManager(RedisTemplate<?,?> redisTemplate) {
RedisSerializer stringSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.PUBLIC_ONLY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
redisTemplate.setKeySerializer(stringSerializer);
redisTemplate.setHashKeySerializer(stringSerializer);
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
CacheManager cacheManager = new RedisCacheManager(redisTemplate);
return cacheManager;
}
}
@Entity//表示这是一个实体类
@Table(name = "category")//表示对应的表名是 category
//因为做前后端分离,前后端数据交互用的是json格式,那么Category对象就会被转换为json数据
//本项目使用jps来做实体咧的持久化,就会创造代理类来继承 Category ,并添加 handler 和 hibernateLazyInitializer 这两个无须 json 化的属性,所以这里需要用 JsonIgnoreProperties 把这两个属性忽略掉。
@JsonIgnoreProperties({ "handler","hibernateLazyInitializer" })
// @Data
public class Category {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")//属性property:id对应上数据库列column:id
int id;
String name;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
使用JPA
// CategoryDAO 类集成了 JpaRepository,就提供了CRUD和分页 的各种常见功能。 这就是采用 JPA 方便的地方~
public interface CategoryDAO extends JpaRepository<Category,Integer>{
}
@Service//标记这个类是 Service类
@CacheConfig(cacheNames="categories")
public class CategoryService {
@Autowired CategoryDAO categoryDAO;
/**查询所有数据,并且实现分页
*/
@Cacheable(key="'categories-page-'+#p0+ '-' + #p1")
public Page4Navigator<Category> list(int start, int size, int navigatePages) {
//首先创建一个Sort对象,表示通过id倒排序
Sort sort = new Sort(Sort.Direction.DESC, "id");
Pageable pageable = new PageRequest(start, size,sort);
//通过categoryDAO进行查询
Page pageFromJPA =categoryDAO.findAll(pageable);
//这里抛弃了 CategoryService 接口 加上 CategoryService 实现类的这种累赘的写法,而是直接使用 CategoryService 作为实现类来做。
return new Page4Navigator<>(pageFromJPA,navigatePages);
}
/**查询所有数据
*/
@Cacheable(key="'categories-all'")
public List<Category> list() {
Sort sort = new Sort(Sort.Direction.DESC, "id");
return categoryDAO.findAll(sort);
}
/**添加数据
*/
@CacheEvict(allEntries=true)
public void add(Category bean) {
categoryDAO.save(bean);
}
/**删除数据
*/
@CacheEvict(allEntries=true)
public void delete(int id) {
categoryDAO.delete(id);
}
/**根据id查询单个数据
*/
@Cacheable(key="'categories-one-'+ #p0")
public Category get(int id) {
Category c= categoryDAO.findOne(id);
return c;
}
/**更新数据
*/
@CacheEvict(allEntries=true)
public void update(Category bean) {
categoryDAO.save(bean);
}
}
后台管理页面跳转专用控制器
因为是做前后端分离,所以数据是通过 RESTFUL接口来取的
而在业务上,除了 RESTFUL 服务要提供,还要提供页面跳转服务
所以所有的后台页面跳转都放在 AdminPageController 这个控制器里
而RSTFUL 专门放在 Category 对应的控制器 CategoryController.java 里面。
@Controller
public class AdminPageController {
//路径为admin,客户端跳转到admin_category_list路径
@GetMapping(value="/admin")
public String admin(){
return "redirect:admin_category_list";
}
//路径为admin_category_list,返回到admin文件夹下的listCategory.html
@GetMapping(value="/admin_category_list")
public String listCategory(){
return "admin/listCategory";
}
//路径为admin_category_edit,返回到admin文件夹下的editCategory.html
@GetMapping(value="/admin_category_edit")
public String editCategory(){
return "admin/editCategory";
}
}
对每个方法的返回值都会直接转换为 json 数据格式
@RestController
public class CategoryController {
@Autowired
CategoryService categoryService;
//对于categories 访问,会获取所有的 Category对象集合,并返回这个集合
//因为是声明为 @RestController, 所以这个集合,又会被自动转换为 JSON数组抛给浏览器。
@GetMapping("/categories")
//修改原 list 方法,接受 start 和 size 参数
public Page4Navigator<Category> list(@RequestParam(value = "start", defaultValue = "0") int start, @RequestParam(value = "size", defaultValue = "5") int size) throws Exception {
start = start<0?0:start;
//返回的是 Page4Navigator 类型,并通过 RestController 转换为 json 对象抛给浏览器
Page4Navigator<Category> page =categoryService.list(start, size, 5); //5表示导航分页最多有5个,像 [1,2,3,4,5] 这样
return page;
}
@PostMapping("/categories")
public Object add(Category bean, MultipartFile image, HttpServletRequest request) throws Exception {
categoryService.add(bean);
saveOrUpdateImageFile(bean, image, request);
return bean;
}
public void saveOrUpdateImageFile(Category bean, MultipartFile image, HttpServletRequest request)
throws IOException {
File imageFolder= new File(request.getServletContext().getRealPath("img/category"));
File file = new File(imageFolder,bean.getId()+".jpg");
if(!file.getParentFile().exists())
file.getParentFile().mkdirs();
image.transferTo(file);
BufferedImage img = ImageUtil.change2jpg(file);
ImageIO.write(img, "jpg", file);
}
@DeleteMapping("/categories/{id}")
public String delete(@PathVariable("id") int id, HttpServletRequest request) throws Exception {
categoryService.delete(id);
File imageFolder= new File(request.getServletContext().getRealPath("img/category"));
File file = new File(imageFolder,id+".jpg");
file.delete();
return null;
}
@GetMapping("/categories/{id}")
public Category get(@PathVariable("id") int id) throws Exception {
Category bean=categoryService.get(id);
return bean;
}
@PutMapping("/categories/{id}")
public Object update(Category bean, MultipartFile image,HttpServletRequest request) throws Exception {
String name = request.getParameter("name");
bean.setName(name);
categoryService.update(bean);
if(image!=null) {
saveOrUpdateImageFile(bean, image, request);
}
return bean;
}
}
@SpringBootApplication
@EnableCaching//Redis使用增加注解,用来启动缓存
public class Application {
//检查端口6379是否启动,就是Redis服务器使用的端口,如果没有启动就会退出springboot
static {
PortUtil.checkPort(6379,"Redis 服务端",true);
}
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
因为是二次请求,第一次是获取 html 页面, 第二次通过 html 页面上的 js 代码异步获取数据,一旦部署到服务器就容易面临跨域请求问题,所以允许所有访问都跨域,就不会出现通过 ajax 获取数据获取不到的问题了。
@Configuration
public class CORSConfiguration extends WebMvcConfigurerAdapter{
@Override
public void addCorsMappings(CorsRegistry registry) {
//所有请求都允许跨域
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("*")
.allowedHeaders("*");
}
}
主要是在处理删除父类信息的时候,因为外键约束的存在,而导致违反约束。
@RestController
@ControllerAdvice
public class GloabalExceptionHandler {
@ExceptionHandler(value = Exception.class)
public String defaultErrorHandler(HttpServletRequest req, Exception e) throws Exception {
e.printStackTrace();
Class constraintViolationException = Class.forName("org.hibernate.exception.ConstraintViolationException");
if(null!=e.getCause() && constraintViolationException==e.getCause().getClass()) {
return "违反了约束,多半是外键约束";
}
return e.getMessage();
}
}
用于判断某个端口是否启动。 因为常常忘记启动 redis服务器,而导致系统无法运行, 这个工具的作用,是帮助检查是否启动了,在启动类中使用
public class ImageUtil {
public static BufferedImage change2jpg(File f) {
try {
Image i = Toolkit.getDefaultToolkit().createImage(f.getAbsolutePath());
PixelGrabber pg = new PixelGrabber(i, 0, 0, -1, -1, true);
pg.grabPixels();
int width = pg.getWidth(), height = pg.getHeight();
final int[] RGB_MASKS = { 0xFF0000, 0xFF00, 0xFF };
final ColorModel RGB_OPAQUE = new DirectColorModel(32, RGB_MASKS[0], RGB_MASKS[1], RGB_MASKS[2]);
DataBuffer buffer = new DataBufferInt((int[]) pg.getPixels(), pg.getWidth() * pg.getHeight());
WritableRaster raster = Raster.createPackedRaster(buffer, width, height, width, RGB_MASKS, null);
BufferedImage img = new BufferedImage(RGB_OPAQUE, raster, false, null);
return img;
} catch (InterruptedException e) {
e.printStackTrace();
return null;
}
}
public static void resizeImage(File srcFile, int width,int height, File destFile) {
try {
if(!destFile.getParentFile().exists())
destFile.getParentFile().mkdirs();
Image i = ImageIO.read(srcFile);
i = resizeImage(i, width, height);
ImageIO.write((RenderedImage) i, "jpg", destFile);
} catch (IOException e) {
e.printStackTrace();
}
}
public static Image resizeImage(Image srcImage, int width, int height) {
try {
BufferedImage buffImg = null;
buffImg = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
buffImg.getGraphics().drawImage(srcImage.getScaledInstance(width, height, Image.SCALE_SMOOTH), 0, 0, null);
return buffImg;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
<!--分类查询对应的html文件-->
<!--一共用到了4个公共包含文件-->
<!--1-分类管理-->
<head th:include="include/admin/adminHeader::html('分类管理')" ></head>
<body>
<!--2-导航栏-->
<div th:replace="include/admin/adminNavigator::html" ></div>
<script>
<!--获取数据-->
$(function(){
//这个是jquery的代码,表示当整个html加载好了之后执行
var data4Vue = {
//vue用到的数据, uri表示访问哪个地址去获取数据,这里的值是categories
//和CategoryController.java 相呼应
uri:'categories',
beans: [],
bean: { id: 0, name: ''},
pagination:{},
file: null
};
//ViewModel
//创建Vue对象
//一个Vue对象有4部分,el是关联视图,data是传递的参数,mounted是vue加载成功后的执行,moethods是方法群
var vue = new Vue({
//el表示和本页面的 元素绑定
el: '#workingArea',
//data表示vue 使用上面的data4Vue对象。
data: data4Vue,
//加载Vue对象成功之后会调用,成功的时候去调用 list() 函数
mounted:function(){ //mounted表示这个 Vue 对象加载成功了
this.list(0);
},
methods: {
//list 函数使用 data4Vue里的 uri作为地址,然后调用 axios.js 这个 ajax库,进行异步调用
list:function(start){
var url = this.uri+ "?start="+start;
axios.get(url).then(function(response) {
vue.pagination = response.data;
//调用成功之后,把服务端返回的数据,保存在 vue.beans 上
vue.beans = response.data.content;
});
},
add: function () {
if(!checkEmpty(this.bean.name, "分类名称"))
return;
if(!checkEmpty(this.file, "分类图片"))
return;
var url = this.uri;
//axios.js 上传文件要用 formData 这种方式
var formData = new FormData();
formData.append("image", this.file);
formData.append("name", this.bean.name);
axios.post(url,formData).then(function(response){
vue.list(0);
vue.bean = { id: 0, name: '', hp: '0'};
$("#categoryPic").val('');
vue.file = null;
});
},
deleteBean: function (id) {
if(!checkDeleteLink())
return;
var url = this.uri+"/"+id;
axios.delete(url).then(function(response){
if(0!=response.data.length){
alert(response.data);
}
else{
vue.list(0);
}
});
},
getFile: function (event) {
this.file = event.target.files[0];
},
//增加了两个跳转方法,分别是 jump和 jumpByNumber,而这两个方法的定义在 前面讲解的 adminHeader.html 里
jump: function(page){
jump(page,vue); //定义在adminHeader.html 中
},
jumpByNumber: function(start){
jumpByNumber(start,vue);
}
}
});
});
//上面是获取数据,这里就是显示数据
</script>
<div id="workingArea" >
<h1 class="label label-info" >分类管理</h1>
<br>
<br>
<div class="listDataTableDiv">
<table class="table table-striped table-bordered table-hover table-condensed">
<thead>
<tr class="success">
<th>ID</th>
<th>图片</th>
<th>分类名称</th>
<th>属性管理</th>
<th>产品管理</th>
<th>编辑</th>
<th>删除</th>
</tr>
</thead>
<tbody>
<!--使用 v-for进行遍历, 这个 beans 就表示data4Vue里面的beans属性-->
<tr v-for="bean in beans ">
<!--bean就是遍历出来的每个id, 这里就是输出每个分类的id.-->
<td>{{bean.id}}</td>
<td>
<img height="40px" :src="'img/category/'+bean.id+'.jpg'">
</td>
<td>
{{bean.name}}
</td>
<td>
<!--在超链里的href里拼接分类id.-->
<a :href="'admin_property_list?cid=' + bean.id "><span class="glyphicon glyphicon-th-list"></span></a>
</td>
<td>
<a :href="'admin_product_list?cid=' + bean.id "><span class="glyphicon glyphicon-shopping-cart"></span></a>
</td>
<td>
<a :href="'admin_category_edit?id=' + bean.id "><span class="glyphicon glyphicon-edit"></span></a>
</td>
<td>
<a href="#nowhere" @click="deleteBean(bean.id)"><span class="glyphicon glyphicon-trash"></span></a>
</td>
</tr>
</tbody>
</table>
</div>
<!--3-分页页面-->
<div th:replace="include/admin/adminPage::html" ></div>
<div class="panel panel-warning addDiv">
<div class="panel-heading">新增分类</div>
<div class="panel-body">
<table class="addTable">
<tr>
<td>分类名称</td>
<td><input @keyup.enter="add" v-model.trim="bean.name" type="text" class="form-control"></td>
</tr>
<tr>
<td>分类图片</td>
<td>
<input id="categoryPic" accept="image/*" type="file" name="image" @change="getFile($event)" />
</td>
</tr>
<tr class="submitTR">
<td colspan="2" align="center">
<a href="#nowhere" @click="add" class="btn btn-success">提交</a>
</td>
</tr>
</table>
</div>
</div>
</div>
<!--4-底部显示-->
<div th:replace="include/admin/adminFooter::html" ></div>
</body>
</html>
(6.2)编辑画面editCategory.html
<!--一个项目里面有很多可以共用的部分,使用包含技术可以把这些共用部分包含起来-->
<head th:include="include/admin/adminHeader::html('编辑分类')" ></head>
<body>
<div th:replace="include/admin/adminNavigator::html" ></div>
<script>
$(function(){
var data4Vue = {
uri: 'categories',
listURL:'admin_category_list',
bean: { id: 0, name: '', hp: '0'},
file:''
};
//ViewModel
var vue = new Vue({
el: '#workingArea',
data: data4Vue,
mounted:function(){ //mounted 表示这个 Vue 对象加载成功了
this.get();
},
methods: {
get:function(){
var id = getUrlParms("id");
var url = this.uri+"/"+id;
axios.get(url).then(function(response) {
vue.bean = response.data;
})
},
update:function () {
if(!checkEmpty(this.bean.name, "分类名称"))
return;
var url = this.uri+"/"+this.bean.id;
//axios.js 上传文件要用 formData 这种方式
var formData = new FormData();
formData.append("image", this.file);
formData.append("name", this.bean.name);
axios.put(url,formData).then(function(response){
location.href=vue.listURL;
});
},
getFile: function (event) {
this.file = event.target.files[0];
}
}
});
});
</script>
<div id="workingArea">
<ol class="breadcrumb">
<li><a href="admin_category_list">所有分类</a></li>
<li class="active">编辑分类</li>
</ol>
<div class="panel panel-warning editDiv">
<div class="panel-heading">编辑分类</div>
<div class="panel-body">
<table class="editTable">
<tr>
<td>分类名称</td>
<td><input @keyup.enter="update" v-model.trim="bean.name" type="text" class="form-control"></td>
</tr>
<tr>
<td>分类图片</td>
<td>
<input id="categoryPic" accept="image/*" type="file" name="image" @change="getFile($event)" />
</td>
</tr>
<tr class="submitTR">
<td colspan="2" align="center">
<input type="hidden" name="id" v-model.trim="bean.id" >
<a href="#nowhere" class="btn btn-success" @click="update">提 交</a>
</td>
</tr>
</table>
</div>
</div>
</div>
<div th:replace="include/admin/adminFooter::html" ></div>
</body>
(7)分页
(8)业务流程