SpringBoot+Vue虚拟机管理系统
- 项目介绍和展示
- 搭建环境
- 后端项目部分
- 增加pom.xml依赖配置
- 后端主要代码部分:
- 前端部分
- 登录注册页面布局
- 主体功能页面布局
- 总结
项目介绍和展示
该项目主要运用spring boot,vue,Hadoop,搭建一个功能系统,实现登录注册账户,对多个虚拟机的管理操作,并且可以实现虚拟机的增删改查,一键免密,一键安装Hadoop等功能。如下:
SpringBoot+Vue前后端分离虚拟机管理系统
搭建环境
软件和环境会用到:
jdk1.8
mysql
node 16.150.0
navicat
idea2021.1.3
这里数据库,jdk等我就不详细讲,特别说明一下,我的前端后端都在idea上编写的,所以我单独说一下vue的安装搭建,vue的搭建详见我的博客
后端项目部分
增加pom.xml依赖配置
这里我们增加了mybatis-plus的配置,因为我们后端使用spring boot会连接数据库,使用数据库保存更新数据。
然后增加了swagger,使用Swagger,就是把相关的信息存储在它定义的描述文件里面(yml或json格式),再通过维护这个描述文件可以去更新接口文档,以及生成各端代码。简单理解:swagger就是为了方便系统生成API接口文档的。
然后就是hutool ,JWT,远程连接Linux和SSH-2协议的包等的配置
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.9</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>vuesp</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>vuesp</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- mybatis-plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-actuator-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity</artifactId>
<version>1.7</version>
</dependency>
<!--swagger-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>
<!-- hutool -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.20</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.1.2</version>
</dependency>
<!-- JWT -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.10.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 远程连接Linux-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- SSH-2协议的包 -->
<dependency>
<groupId>ch.ethz.ganymed</groupId>
<artifactId>ganymed-ssh2</artifactId>
<version>262</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>nexus-aliyun</id>
<name>nexus-aliyun</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>public</id>
<name>aliyun nexus</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
</project>
后端主要代码部分:
后端代码项目结构
conmon: 配置与前端交互返回的数据提示和内容
config: 跨域和redis
controller: 控制层
entity: 实体类
service: service层,处理接收到的数据,主要作功能代码
mapper: 从service到mapper,主要实现数据库的增删改查方法的实现
spring boot的经典代码部分;controller,entity,service,HostServiceImpl,mapper以及mapper.xml我就不在文章展示了,详情请查看文末获取全部源码。这里就展示后端与前端跨域的代码以及结果封装部分代码
跨域部分代码CorsConfig
package com.example.vuesp.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
@Configuration
public class CorsConfig {
// 当前跨域请求最大有效时长。这里默认1天
private static final long MAX_AGE = 24 * 60 * 60;
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedOrigin("http://localhost:8080"); // 1 设置访问源地址
corsConfiguration.addAllowedHeader("*"); // 2 设置访问源请求头
corsConfiguration.addAllowedMethod("*"); // 3 设置访问源请求方法
corsConfiguration.setMaxAge(MAX_AGE);
source.registerCorsConfiguration("/**", corsConfiguration); // 4 对接口配置跨域设置
return new CorsFilter(source);
}
}
结果封装部分代码conmon
因为是前后端分离的项目,所以我们有必要统一一个结果返回封装类,这样前后端交互的时候有个统一的标准,约定结果返回的数据是正常的或者遇到异常了。
Constants
package com.example.vuesp.common;
public interface Constants {
String CODE_200 = "200";//成功
String CODE_500 = "500";//系统错误
String CODE_401 = "401";//权限不足
String CODE_400 = "400";//参数错误
String CODE_600 = "600";//其他业务异常
}
Result
package com.example.vuesp.common;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 接口统一返回包装类
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result {
private String code;
private String msg;
private Object data;
public static Result success() {
return new Result(Constants.CODE_200, "", null);
}
public static Result success(Object data) {
return new Result(Constants.CODE_200, "", data);
}
public static Result error(String code, String msg) {
return new Result(code, msg, null);
}
public static Result error() {
return new Result(Constants.CODE_500, "系统错误", null);
}
}
前端部分
在前面搭建好前端的环境项目后,将前端项目复制到idea上,方便操作执行,首先在idea新建一个Java项目然后将前端项目文件放于大文件夹下,如图我的前端项目为vue01放于后端项目下
assets放前端所需要的图片
components放导航栏与上边框
router:设置路由–名字和资源映射起来
utils:与后端的请求配置
views;前端所有展示页面的代码
main.js:主要作用是初始化vue实例并使用需要的插件
登录注册页面布局
1登录页面Login.vue
这里我们用了一个数据库保存了用户的账号密码,并进行绑定,密码错误则不能进行登录。
<template>
<div class="wrapper1" >
<div style="margin: 0px 0px;width: 200px;height: 20px;padding: 0; border-radius: 10px">
<div class="demo-image__placeholder" style="margin: 0px 0px;width: 400px;height: 40px;padding: 0px; border-radius: 100px">
<div class="block">
<el-image :src="require('../assets/logo.png')"></el-image>
</div>
</div>
</div>
<div style="margin: 10px 300px;width: 500px;height: 40px;padding: 20px; border-radius: 10px">
<div style="margin: 200px 70px">
<el-carousel height="450px" :interval="1000">
<el-carousel-item v-for="item in imgs" :key="item">
<img :src="item" alt="" style="width: 100%">
</el-carousel-item>
</el-carousel>
</div>
</div>
<div style="margin: 100px 900px; background-color: #fff; width: 500px;height: 450px; padding: 20px; border-radius: 20px">
<div style="margin: 20px 0; text-align: center; font-size: 24px;left: 400px"><b>登 录</b></div>
<el-form :model="user" :rules="rules" ref="userForm">
<el-form-item prop="username">
<el-input size="medium" style="margin: 30px 0" prefix-icon="el-icon-user" v-model="user.username"></el-input>
</el-form-item>
<el-form-item prop="password">
<el-input size="medium" style="margin: 50px 0" prefix-icon="el-icon-lock" show-password v-model="user.password"></el-input>
</el-form-item>
<el-form-item style="margin: 30px 0; text-align: center;left: 400px">
<el-button type="primary" size="small" autocomplete="off" @click="login">登录</el-button>
<el-button type="warning" size="small" autocomplete="off" @click="$router.push('/register')">注册</el-button>
</el-form-item>
</el-form>
</div>
</div>
</template>
<script>
export default {
name: "Login",
data() {
return {
user: {},
rules: {
username: [
{required: true, message: '请输入用户名', trigger: 'blur'},
{min: 3, max: 10, message: '长度在 3 到 5 个字符', trigger: 'blur'}
],
password: [
{required: true, message: '请输入密码', trigger: 'blur'},
{min: 1, max: 20, message: '长度在 1 到 20 个字符', trigger: 'blur'}
],
},
imgs: [
require('../assets/1.jpg'),
require('../assets/2.jpg'),
require('../assets/3.jpg')
],
files: []
}
},
methods: {
login() {
this.$refs['userForm'].validate((valid) => {
if (valid) { // 表单校验合法
this.request.post("/user/login", this.user).then(res => {
if(res.code === '200') {
localStorage.setItem("user", JSON.stringify(res.data)) // 存储用户信息到浏览器
this.$router.push("/")
this.$message.success("登录成功!")
} else {
this.$message.error(res.msg)
}
})
}
});
}
}
}
</script>
<style >
.wrapper1 {
height: 100vh;
background-image: linear-gradient(to bottom right, #C6F0F6 , #C6F0F6);
overflow: hidden;
}
</style>
2注册页面Register.vue
这里我们做了一个小细节,两次设置密码必须一致。
<template>
<div class="wrapper">
<div style="margin: 100px auto; background-color: #fff; width: 350px; height: 550px; padding: 20px; border-radius: 10px">
<div style="margin: 20px 0; text-align: center; font-size: 24px"><b>注 册</b></div>
<el-form :model="user" :rules="rules" ref="userForm">
<el-form-item prop="nickname">
<el-input placeholder="请输入昵称" size="medium" style="margin: 5px 0" prefix-icon="el-icon-user" v-model="user.nickname"></el-input>
</el-form-item>
<el-form-item prop="username">
<el-input placeholder="请输入账号" size="medium" style="margin: 5px 0" prefix-icon="el-icon-user" v-model="user.username"></el-input>
</el-form-item>
<el-form-item prop="name">
<el-input placeholder="请输入集群名称" size="medium" style="margin: 5px 0" prefix-icon="el-icon-user" v-model="user.name"></el-input>
</el-form-item>
<el-form-item prop="number">
<el-input placeholder="请输入主机数" size="medium" style="margin: 5px 0" prefix-icon="el-icon-user" v-model="user.number"></el-input>
</el-form-item>
<el-form-item prop="password">
<el-input placeholder="请输入密码" size="medium" style="margin: 5px 0" prefix-icon="el-icon-lock" show-password v-model="user.password"></el-input>
</el-form-item>
<el-form-item prop="confirmPassword">
<el-input placeholder="请确认密码" size="medium" style="margin: 5px 0" prefix-icon="el-icon-lock" show-password v-model="user.confirmPassword"></el-input>
</el-form-item>
<el-form-item style="margin: 5px 0; text-align: right">
<el-button type="primary" size="small" autocomplete="off" @click="login">注册</el-button>
<el-button type="warning" size="small" autocomplete="off" @click="$router.push('/login')">返回登录</el-button>
</el-form-item>
</el-form>
</div>
</div>
</template>
<script>
export default {
name: "Login",
data() {
return {
user: {},
rules: {
nickname: [
{ required: true, message: '请输入昵称', trigger: 'blur' },
{ min: 1, max: 10, message: '长度在 1 到 20 个字符', trigger: 'blur' }
],
username: [
{ required: true, message: '请输入账号', trigger: 'blur' },
{ min: 3, max: 20, message: '长度在 3 到 10 个字符', trigger: 'blur' }
],
name: [
{ required: true, message: '请输入集群名称', trigger: 'blur' },
{ min: 1, max: 20, message: '长度在 1 到 20 个字符', trigger: 'blur' }
],
number: [
{ required: true, message: '请输入主机数', trigger: 'blur' },
{ min: 1, max: 1, message: '长度在 1 到 1 个数字', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 1, max: 20, message: '长度在 1 到 20 个字符', trigger: 'blur' }
],
confirmPassword: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 1, max: 20, message: '长度在 1 到 20 个字符', trigger: 'blur' }
],
}
}
},
methods: {
login() {
this.$refs['userForm'].validate((valid) => {
if (valid) { // 表单校验合法
if (this.user.password !== this.user.confirmPassword) {
this.$message.error("两次输入的密码不一致")
return false
}
this.request.post("/user/register", this.user).then(res => {
if(res.code === '200') {
this.$message.success("注册成功")
} else {
this.$message.error(res.msg)
}
})
}
});
}
}
}
</script>
<style>
.wrapper {
height: 100vh;
background-image: linear-gradient(to bottom right, #FC466B , #3F5EFB);
overflow: hidden;
}
</style>
主体功能页面布局
1系统左边导航栏布局Aside.vue
<!--导航栏-->
<template>
<el-menu :default-openeds="['1', '3']" style="min-height: 100%; overflow-x: hidden"
background-color="rgb(48, 65, 86)"
text-color="#fff"
active-text-color="#3DA6CC"
:collapse-transition="false"
:collapse="isCollapse"
router
>
<div style="height: 60px; line-height: 60px; text-align: center">
<img src="../assets/压力.png" alt="" style="width: 27px; position: relative; top: 5px; right: 5px">
<b style="color: white" v-show="logoTextShow">Hadoop|压力怪</b>
</div>
<el-menu-item index="/">
<template slot="title">
<i class="el-icon-s-home"></i>
<span slot="title">首页</span>
</template>
</el-menu-item>
<el-submenu index="1">
<template slot="title">
<i class="el-icon-menu"></i>
<span slot="title">集群概览</span>
</template>
<el-menu-item index="/user">
<i class="el-icon-s-custom"></i>
<span slot="title">集群展示</span>
</el-menu-item>
<el-menu-item index="/Download">
<i class="el-icon-download"></i>
<span slot="title">产品安装</span>
</el-menu-item>
<el-menu-item index="/Validation">
<i class="el-icon-loading"></i>
<span slot="title">产品验证</span>
</el-menu-item>
</el-submenu>
</el-menu>
</template>
<script>
export default {
name: "Aside",
props: {
isCollapse: Boolean,
logoTextShow: Boolean
}
}
</script>
<style scoped>
</style>
2系统顶上信息显示Header.vue
<template>
<div style="line-height: 60px; display: flex">
<div style="flex: 2;">
</div>
<el-dropdown style="width: 100px; cursor: pointer">
<div style="display: inline-block">
<img :src="require('../assets/1.jpg')" alt=""
style="width: 30px; border-radius: 50%; position: relative; top: 10px; right: 5px">
<span style="font-weight:bold">{{ user.nickname }}</span><i class="el-icon-arrow-down" style="margin-left: 5px"></i>
</div>
<el-dropdown-menu slot="dropdown" style="width: 100px; text-align: center">
<!-- <el-dropdown-item style="font-size: 14px; padding: 5px 0">-->
<!-- <router-link to="/person">个人信息</router-link>-->
<!-- </el-dropdown-item>-->
<el-dropdown-item style="font-size: 14px; padding: 5px 0">
<span style="text-decoration: none" @click="logout">退出</span>
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</template>
<script>
export default {
name: "Header",
props: {
collapseBtnClass: String,
collapse: Boolean,
},
data() {
return {
user: localStorage.getItem("user") ? JSON.parse(localStorage.getItem("user")) : {}
}
},
methods: {
logout() {
this.$router.push("/login")
localStorage.removeItem("user")
this.$message.success("退出成功")
}
}
}
</script>
<style scoped>
</style>
3首页Home.vue
<!--主页-->
<template>
<div class="wrapper3" >
<el-row style="top: 100px;left: 450px;">
<!-- <span>压力怪</span>-->
<span style="font-weight:bold;font-size: 35px;" type="success">压力怪!程序猿的选择~</span>
</el-row>
<el-row style="top: 470px;left: 1000px;">
<!-- <span>压力怪</span>-->
<el-button type="primary" round @click="$router.push('/User')">
<i class="el-icon-d-arrow-right" style="font-weight:bold;font-size: 35px">开始使用</i>
</el-button>
</el-row>
</div>
</template>
<script>
export default {
name: "Home",
}
</script>
<style>
.wrapper3 {
height: 85vh;
background:#fff url(../assets/t.webp) top left/1300px 650px no-repeat;
/*repeat 10px 0px*/
overflow: hidden;
}
</style>
4集群展示页面
<!--集群页面-->
<template>
<div>
<div style="margin-bottom: 20px">
<el-breadcrumb separator="/">
<el-button type="info" style="font-size: 18px" @click="$router.push('/User')">
<i class="el-icon-back"></i>返回</el-button>
</el-breadcrumb>
</div>
<div style="margin: 10px 0">
<el-input style="width: 200px" placeholder="请输入主机名称" suffix-icon="el-icon-search" v-model="hostname"></el-input>
<el-button class="ml-5" type="primary" @click="load">搜索</el-button>
</div>
<div style="margin: 10px 0">
<el-button type="warning" @click="handleAdd">新增主机<i class="el-icon-circle-plus-outline"></i></el-button>
<el-button type="primary" @click="pass">一键免密钥 <i class="el-icon-remove-outline"></i></el-button>
<el-button type="primary" @click="jdk">一键JDK <i class="el-icon-remove-outline"></i></el-button>
<el-button type="primary" @click="$router.push('/Download')">产品安装<i class="el-icon-download"></i></el-button>
<el-button type="primary" @click="$router.push('/Validation')">产品验证<i class="el-icon-loading"></i></el-button>
</div>
<el-table :data="tableData" border stripe :header-cell-class-name="headerBg">
<el-table-column prop="id" label="序号" width="200">
</el-table-column>
<el-table-column prop="hostname" label="主机名" width="250">
</el-table-column>
<el-table-column prop="ip" label="IP" width="200">
</el-table-column>
<el-table-column prop="role" label="节点" width="245">
</el-table-column>
<el-table-column prop="password" label="主机密码" width="200">
</el-table-column>
<el-table-column label="操作" width="200" align="center">
<template slot-scope="scope">
<el-popconfirm
class="ml-5"
confirm-button-text='确定'
cancel-button-text='我再想想'
icon="el-icon-info"
icon-color="red"
title="您确定删除吗?"
@confirm="del(scope.row.id)"
>
<el-button type="danger" slot="reference">删除主机<i class="el-icon-remove-outline"></i></el-button>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<!--翻页-->
<div style="padding: 10px 0">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="pageNum"
:page-sizes="[2, 5, 10, 20]"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="total">
</el-pagination>
</div>
<el-dialog title="集群信息" :visible.sync="dialogFormVisible" width="30%" >
<el-form label-width="80px" size="small">
<el-form-item label="主机名">
<el-input v-model="form.hostname" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="IP">
<el-input v-model="form.ip" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="节点">
<el-input v-model="form.role" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="主机密码">
<el-input v-model="form.password" autocomplete="off"></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="dialogFormVisible = false">取 消</el-button>
<el-button type="primary" @click="save">确 定</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
export default {
name: "User",
data() {
return {
tableData: [],
total: 0,
pageNum: 1,
pageSize: 2,
hostname: "",
form: {},
dialogFormVisible: false,
headerBg: 'headerBg'
}
},
created() {
// 请求分页查询数据
this.load()
},
methods: {
load() {
this.request.get("/host/page", {
params: {
pageNum: this.pageNum,
pageSize: this.pageSize,
hostname: this.hostname,
}
}).then(res => {
console.log(res)
this.tableData = res.records
this.total = res.total
})
},
handleAdd() {
this.dialogFormVisible = true
this.form = {}
},
save() {
this.request.post("/host", this.form).then(res => {
if (res) {
this.$message.success("保存成功")
this.dialogFormVisible = false
this.load()
} else {
this.$message.error("保存失败")
}
})
},
del(id) {
this.request.delete("/host/" + id).then(res => {
if (res) {
this.$message.success("删除成功")
this.load()
} else {
this.$message.error("删除失败")
}
})
},
handleSizeChange(pageSize) {
console.log(pageSize)
this.pageSize = pageSize
this.load()
},
handleCurrentChange(pageNum) {
console.log(pageNum)
this.pageNum = pageNum
this.load()
},
pass() {
this.request.get("/pass").then(res => {
if(res.code === '200') {
this.$message.success("结果:"+res.data)
} else {
this.$message.error("失败")
}
})
},
jdk() {
this.request.get("/jdk").then(res => {
if(res.code === '200') {
this.$message.success("结果:"+res.data)
} else {
this.$message.error("失败")
}
})
},
}
}
</script>
<style>
.headerBg {
background: #eee!important;
}
</style>
总结
项目做得不是很完美很好,有许多功能还未完善,也有许多bug但是自学vue做得这样子还是将就,博友们可以参考然后改进,欢迎提出意见与问题。欢迎交流,因为源码太多,需获取源码请评论。