文章目录
- 前言
- 1. 首先介绍项目的相关技术和工具:
- 2. 首先创建项目
- 3. 项目的结构
- 3.1实体类:
- 3.2 Mapper.xml
- 3.3 mapper.inteface
- 3.4 Service
- 3.5 Controller
- 3.6 html页面
- 3.7 yml文件配置
- 3.8 POM文件依赖
- 4. 踩坑秘籍
- 4.1 xml文件返回结果是list的size为1,但是结果为null
- 4.2 Controller返回到html
- 4.3 关于静态资源的配置
- 4.4 Swagger 配置
- 4.5 mapstruct的配置和使用
- 代码下载地址
前言
最近在做一个关于SpringBoot的项目,首先从最简单的注册登陆开始,从前端到后端。
1. 首先介绍项目的相关技术和工具:
开发工具使用IDEA,技术使用SpringBoot2.1.3+Mybatis+Jpa+mysql,项目中主要使用Mybatis,jpa只做了demo,实体转换使用的是mapstruct,集成了swagger文档配置,redis缓存demo。
2. 首先创建项目
两种方式:1、直接在IDEA中file–>new–>project,选择spring Initalizr创建一个springBoot项目。
2、或者直接在spring的官网创建一个springboot项目springBoot官网创建项目
3. 项目的结构
首先项目的结构和普通的spring项目是一样的,采用controller、service、dao三层
接下来项目采用逆向介绍:
实体—>Mapper.xml—>Mapper.inteface–>service–>controller—>html
项目中的一些实体和mybatis的代码,采用mybatis generator逆向生成。
如果逆向生成不太懂,请自行百度了解或点击Mybatis-Generator之最完美配置详解
3.1实体类:
@Table(name = "user_info")
@Entity
public class UserInfo extends BaseEntity{
@Id
@Column(name = "USER_ID")
private Long userId;
@Column(name = "USER_NICK_NAME")
private String userNickName;
@Column(name = "USER_SUR_NAME")
private String userSurName;
@Column(name = "USER_NAME")
private String userName;
@Column(name = "USER_DESC")
private String userDesc;
@Column(name = "USER_SEX")
private Boolean userSex;
@Column(name = "USER_PHONE")
private String userPhone;
@Column(name = "USER_EMAIL")
private String userEmail;
@Column(name = "USER_HOME")
private String userHome;
@Column(name = "USER_BIRTHDAY")
private Date userBirthday;
@Column(name = "USER_REGISTER_DATE")
private Date userRegisterDate;
@Column(name = "USER_LEVEL")
private Long userLevel;
@Column(name = "PASS_WORD")
private String passWord;
@Column(name = "LOGIN_NUM")
private Long loginNum;
GetSet方法省略
由于项目中集成了Jpa所以实体类上有表和列的注解。
3.2 Mapper.xml
USER_ID, USER_NICK_NAME, USER_SUR_NAME, USER_NAME, USER_DESC, USER_SEX, USER_PHONE,
USER_EMAIL, USER_HOME, USER_BIRTHDAY, USER_REGISTER_DATE, CREATE_DATE, UPDATE_DATE,
STATUS_CD, STATUS_DATE, REMARK, USER_LEVEL, PASS_WORD, LOGIN_NUM
delete from user_info
where USER_ID = #{userId,jdbcType=BIGINT}
insert into user_info (USER_ID, USER_NICK_NAME, USER_SUR_NAME,
USER_NAME, USER_DESC, USER_SEX,
USER_PHONE, USER_EMAIL, USER_HOME,
USER_BIRTHDAY, USER_REGISTER_DATE, CREATE_DATE,
UPDATE_DATE, STATUS_CD, STATUS_DATE,
REMARK, USER_LEVEL, PASS_WORD,
LOGIN_NUM)
values (#{userId,jdbcType=BIGINT}, #{userNickName,jdbcType=VARCHAR}, #{userSurName,jdbcType=VARCHAR},
#{userName,jdbcType=VARCHAR}, #{userDesc,jdbcType=VARCHAR}, #{userSex,jdbcType=BIT},
#{userPhone,jdbcType=VARCHAR}, #{userEmail,jdbcType=VARCHAR}, #{userHome,jdbcType=VARCHAR},
#{userBirthday,jdbcType=TIMESTAMP}, #{userRegisterDate,jdbcType=TIMESTAMP}, #{createDate,jdbcType=TIMESTAMP},
#{updateDate,jdbcType=TIMESTAMP}, #{statusCd,jdbcType=DECIMAL}, #{statusDate,jdbcType=TIMESTAMP},
#{remark,jdbcType=VARCHAR}, #{userLevel,jdbcType=BIGINT}, #{passWord,jdbcType=VARCHAR},
#{loginNum,jdbcType=BIGINT})
insert into user_info
USER_ID,
USER_NICK_NAME,
USER_SUR_NAME,
USER_NAME,
USER_DESC,
USER_SEX,
USER_PHONE,
USER_EMAIL,
USER_HOME,
USER_BIRTHDAY,
USER_REGISTER_DATE,
CREATE_DATE,
UPDATE_DATE,
STATUS_CD,
STATUS_DATE,
REMARK,
USER_LEVEL,
PASS_WORD,
LOGIN_NUM,
#{userId,jdbcType=BIGINT},
#{userNickName,jdbcType=VARCHAR},
#{userSurName,jdbcType=VARCHAR},
#{userName,jdbcType=VARCHAR},
#{userDesc,jdbcType=VARCHAR},
#{userSex,jdbcType=BIT},
#{userPhone,jdbcType=VARCHAR},
#{userEmail,jdbcType=VARCHAR},
#{userHome,jdbcType=VARCHAR},
#{userBirthday,jdbcType=TIMESTAMP},
#{userRegisterDate,jdbcType=TIMESTAMP},
#{createDate,jdbcType=TIMESTAMP},
#{updateDate,jdbcType=TIMESTAMP},
#{statusCd,jdbcType=DECIMAL},
#{statusDate,jdbcType=TIMESTAMP},
#{remark,jdbcType=VARCHAR},
#{userLevel,jdbcType=BIGINT},
#{passWord,jdbcType=VARCHAR},
#{loginNum,jdbcType=BIGINT},
update user_info
USER_NICK_NAME = #{userNickName,jdbcType=VARCHAR},
USER_SUR_NAME = #{userSurName,jdbcType=VARCHAR},
USER_NAME = #{userName,jdbcType=VARCHAR},
USER_DESC = #{userDesc,jdbcType=VARCHAR},
USER_SEX = #{userSex,jdbcType=BIT},
USER_PHONE = #{userPhone,jdbcType=VARCHAR},
USER_EMAIL = #{userEmail,jdbcType=VARCHAR},
USER_HOME = #{userHome,jdbcType=VARCHAR},
USER_BIRTHDAY = #{userBirthday,jdbcType=TIMESTAMP},
USER_REGISTER_DATE = #{userRegisterDate,jdbcType=TIMESTAMP},
CREATE_DATE = #{createDate,jdbcType=TIMESTAMP},
UPDATE_DATE = #{updateDate,jdbcType=TIMESTAMP},
STATUS_CD = #{statusCd,jdbcType=DECIMAL},
STATUS_DATE = #{statusDate,jdbcType=TIMESTAMP},
REMARK = #{remark,jdbcType=VARCHAR},
USER_LEVEL = #{userLevel,jdbcType=BIGINT},
PASS_WORD = #{passWord,jdbcType=VARCHAR},
LOGIN_NUM = #{loginNum,jdbcType=BIGINT},
where USER_ID = #{userId,jdbcType=BIGINT}
update user_info
set USER_NICK_NAME = #{userNickName,jdbcType=VARCHAR},
USER_SUR_NAME = #{userSurName,jdbcType=VARCHAR},
USER_NAME = #{userName,jdbcType=VARCHAR},
USER_DESC = #{userDesc,jdbcType=VARCHAR},
USER_SEX = #{userSex,jdbcType=BIT},
USER_PHONE = #{userPhone,jdbcType=VARCHAR},
USER_EMAIL = #{userEmail,jdbcType=VARCHAR},
USER_HOME = #{userHome,jdbcType=VARCHAR},
USER_BIRTHDAY = #{userBirthday,jdbcType=TIMESTAMP},
USER_REGISTER_DATE = #{userRegisterDate,jdbcType=TIMESTAMP},
CREATE_DATE = #{createDate,jdbcType=TIMESTAMP},
UPDATE_DATE = #{updateDate,jdbcType=TIMESTAMP},
STATUS_CD = #{statusCd,jdbcType=DECIMAL},
STATUS_DATE = #{statusDate,jdbcType=TIMESTAMP},
REMARK = #{remark,jdbcType=VARCHAR},
USER_LEVEL = #{userLevel,jdbcType=BIGINT},
PASS_WORD = #{passWord,jdbcType=VARCHAR},
LOGIN_NUM = #{loginNum,jdbcType=BIGINT}
where USER_ID = #{userId,jdbcType=BIGINT}
关于这个xml多说一句,在代码运行期间遇到的坑:如果数据库表字段和实体的类型匹配不到,在结果返回的时候使用resultType是不能将结果正常的返回出来的,我遇到的情况是查询出数据条数为1,结果的list的size的确是1,但是里面却是null,这个就是因为字段类型不匹配,导致的数据没有被填充到指定的实体中,这个时候应该使用resultMap对表字段和实体字段进行指定。这样就能正常的返回数据了。
3.3 mapper.inteface
@Mapper
public interface UserInfoMapper{
int deleteByPrimaryKey(Long userId);
int insert(UserInfo record);
int insertSelective(UserInfo record);
UserInfo selectByPrimaryKey(Long userId);
int updateByPrimaryKeySelective(UserInfo record);
int updateByPrimaryKey(UserInfo record);
List findByExample(UserInfo userInfo);
}
这里说一句注意点,这个接口是用来和xml文件进行交互的,xml文件你可以理解为是当前接口的实现类。注意点1:当前接口需要加注解@Mapper,将当前类交由spring管理。
注意点2:如果接口中的方法参数是多个(>1)的时候需要对参数加@Param进行名称的指定,这里使用注解指定的名称就是在xml中取值使用的名称而不是参数的名称,eg:String find(@Param(“aa”) int bb),如果想在xml中取到参数bb对应的值,那么就应该获取名称为aa的变量,${aa}或者#{aa}。如果方法的参数是一个或者没有,那么@Param注解可加可不加。
3.4 Service
项目中采用接口+实现类的形式:
实现类ServiceImpl:
@Service("userServiceImpl")
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
@Autowired
private UserInfoMapper userInfoMapper;
@Override
public UserInfo findByName(String name) {
return userDao.findByUserNickName(name);
}
@Override
public List findAll() {
return userDao.findAll();
}
@Override
public UserInfo findOne(Long id) {
return userDao.findById(id).isPresent()?userDao.findById(id).get():new UserInfo();
}
@Override
public UserInfo save(UserInfo UserInfo) {
return userDao.save(UserInfo);
}
@Override
public UserInfo saveAndUpdate(UserInfo UserInfo) {
return userDao.saveAndFlush(UserInfo);
}
@Override
public void deleteUser(long id) {
userDao.deleteById(id);
}
@Override
public UserInfo findById(long id){
return userInfoMapper.selectByPrimaryKey(id);
}
@Override
public int insert(UserInfo userInfo) {
//用户注册的时候只有用户名和密码,所以为用户设置必填的一些字段,
//主键
userInfo.setUserId(KeyUtils.UUID());
Date nowDate = new Date();
//注册时间、创建时间
userInfo.setUserRegisterDate(nowDate);
userInfo.setCreateDate(nowDate);
//剩余登陆次数,默认5次
userInfo.setLoginNum(L_FIVE);
//性别,若没有则设置为true,true表示男,数据库对应的是1
if(StringUtils.isEmpty(userInfo.getUserSex())){
userInfo.setUserSex(true);
}
//用户姓氏取用户名的第一个字符
userInfo.setUserSurName(userInfo.getUserNickName().substring(I_ZERO, I_ONE));
userInfo.setUserName(userInfo.getUserNickName().substring(I_ONE));
//用户状态默认有效
userInfo.setStatusCd(StatusCdEnum.ACTIVE_ENUM.getCode());
//用户等级
userInfo.setUserLevel(L_ONE);
//用户密码默认采用MD5进行加密
userInfo.setPassWord(EncryptUtil.getInstance().MD5(userInfo.getPassWord()));
return userInfoMapper.insert(userInfo);
}
@Override
@Transactional
public UserEnum login(UserInfo userInfo) {
String userNickName = userInfo.getUserNickName();
String passWord = userInfo.getPassWord();
//用户名或密码为空,直接返回
if(StringUtils.isEmpty(userNickName) || StringUtils.isEmpty(passWord)){
return LOGIN_USER_PWD_NULL;
}
//登陆的步骤:
//1、首先根据用户名查询数据库有效的数据且次数大于0的是否存在,若用户名不存在则直接返回,若用户名存在则继续下面的操作
UserInfo ui = new UserInfo();
ui.setStatusCd(StatusCdEnum.ACTIVE_ENUM.getCode());
ui.setLoginNum(L_ZERO);
ui.setUserNickName(userNickName);
List userInfos = userInfoMapper.findByExample(ui);
if(CollectionUtils.isEmpty(userInfos)){
return LOGIN_USER_NULL;
}
//2、对比查询出的密码,密码不正确修改对应的数据的登陆次数
for (UserInfo info : userInfos) {
//如果密码不相等,比较次数
if(!EncryptUtil.getInstance().MD5(passWord).equals(info.getPassWord())){
//密码不相等,首先将该账号的登陆次数减一
UserInfo u = new UserInfo();
u.setUserId(info.getUserId());
u.setLoginNum(info.getLoginNum()-L_ONE);
//次数>1表示当前用户此次失败后不会被锁定
if(info.getLoginNum() > L_ONE){
int i = userInfoMapper.updateByPrimaryKeySelective(u);
if (i>I_ZERO) {
LOGIN_USER_ERROR.setNum(String.valueOf(info.getLoginNum()-L_ONE));
return LOGIN_USER_ERROR;
}
}else{
//次数<1表示此次失败后,账号将被锁定
u.setStatusCd(StatusCdEnum.FROZEN_ENUM.getCode());
u.setStatusDate(new Date());
int i = userInfoMapper.updateByPrimaryKeySelective(u);
if(i>I_ZERO){
return LOGIN_USER_NUM;
}
}
}
}
//根据用户名和密码查询数据库
return UserEnum.SUCCESS;
}
}
实现类中主要对登陆和注册的相关逻辑操作,这里说下相关逻辑:
注册:界面中只输入用户名和密码,在业务层将必填的字段进行补全,密码进行加密,状态为有效,登陆次数为默认5次,注册时间为当前时间等。
登陆:根据用户名进行数据库的查询(有效的、登陆次数>1),如果查不到,报错返回,如果查到数据则判断查到的密码和输入的密码是否相等,如果相等,通过,如果不相等,则判断登陆次数是否>1,如果大于1则将剩余的次数报错(用户名或密码不正确,剩余次数*次)返回,如果<1则报错返回错误信息(该用户失败次数已达5次,请次日再试)。
接口Service:
public interface UserService {
/**
* 根据名称查询user对象
* @param name 名称
* @return user对象
*/
UserInfo findByName(String name);
List findAll();
UserInfo findOne(Long id);
UserInfo save(UserInfo UserInfo);
UserInfo saveAndUpdate(UserInfo UserInfo);
void deleteUser(long id);
UserInfo findById(long id);
int insert(UserInfo userInfo);
UserEnum login(UserInfo userInfo);
}
3.5 Controller
这个是业务的controller控制层,也没有什么复杂的逻辑,只需要注意一点就是,如果想要返回到页面中那么controller的注解不能使用@RestController,应该使用@Controller,如果只是返回数据则无所谓。因为@RestController会将返回的结果转换为json串,所以不能返回到页面中。
@Api("用户表操作控制层")
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
/**
* 根据名称查询
* @param name 名称
* @return 返回User对象
*/
@GetMapping("/user/name")
@ApiOperation("根据名称查询user对象,JPA版")
public UserInfo findByName(@RequestParam("name") String name){
return userService.findByName(name);
}
/**
* 查询所有的user对象
* @return user列表
*/
@GetMapping("/user/all")
@ApiOperation("查询所有的user对象,JPA版")
public List findAll(){
return userService.findAll();
}
/**
* 新增
*/
@PostMapping("/user")
@ApiOperation("新增user对象,JPA版")
public UserInfo save(@RequestBody UserInfo userInfo){
return userService.save(userInfo);
}
/**
* 删除
*/
@DeleteMapping("/TUserInfo/{id}")
@ApiOperation("删除user对象,JPA版")
public void delete(@PathVariable("id") long id){
userService.deleteUser(id);
}
/**
* 更新
*/
@PutMapping("/user")
@ApiOperation("更新user对象,JPA版")
public UserInfo update(UserInfo userInfo){
return userService.saveAndUpdate(userInfo);
}
/**
* 通过Id查询
*/
@RequestMapping("/user/{id}")
@ApiOperation("查询user对象,JPA版")
public UserInfo findOne(@PathVariable("id") Long id){
return userService.findOne(id);
}
/**
* 通过Id查询,MyBatis版
*/
@GetMapping("/user/mybatis/{id}")
@ApiOperation("根据Id查询UserInfo对象,Mybatis版")
public UserInfo findById(@PathVariable Long id){
return userService.findById(id);
}
/**
* 新增,Mybatis版
*/
@PostMapping("/user/mybatis")
@ApiOperation("新增user对象,Mybatis版")
public int insert(@RequestBody UserInfo userInfo){
return userService.insert(userInfo);
}
@PostMapping("/register")
@ApiOperation("用户注册")
public ResultDTO userRegister(UserInfoDTO userInfoDTO){
UserInfo userInfo = EntityToDTO.INIT.toUserInfo(userInfoDTO);
int result = userService.insert(userInfo);
if(result > 0){
return ResultUtils.SUCCESS();
}
return ResultUtils.ERROR();
}
@PostMapping("/login")
@ApiOperation("用户登陆")
public ResultDTO userLogin(UserInfoDTO userInfoDTO){
UserInfo userInfo = EntityToDTO.INIT.toUserInfo(userInfoDTO);
UserEnum result = userService.login(userInfo);
if(result == UserEnum.SUCCESS){
return ResultUtils.SUCCESS();
}
return ResultUtils.ERROR(result.getCode(),result.getMsg(),result.getNum());
}
}
这个是视图controller,专门用户视图的跳转
@Api("用于视图转换")
@Controller
@RequestMapping("/view")
public class ViewController {
/**
* 进入注册界面
*/
@GetMapping("/register")
@ApiOperation("进入用户注册")
public String enterRegister(){
System.out.println("进入注册界面...");
return "index";
}
}
没有什么复杂的逻辑,这里不赘述。
3.6 html页面
html页面 中采用ajax进行登录和注册的交互,具体的页面如下:
注册和登陆的代码相差无几,这里就只贴注册的代码。
$.ajax({
type:"POST",
url:"/user/register",
data:{
"userNickName": username,
"passWord": password
},
dataType:"json",
success:function(dataX){
console.log("返回的code为:"+dataX.code);
if(dataX.code === "000000"){
$("#login-username").val(username);
$("#login-password").val(password);
//注册成功
spop({
template: '注册成功
即将于3秒后返回登录',
position: 'top-center',
style: 'success',
autoclose: 3000,
onOpen : function(){
var second = 2;
var showPop = setInterval(function(){
if(second == 0){
clearInterval(showPop);
}
$('.spop-body').html('注册成功
即将于'+second+'秒后返回登录');
second--;
},1000);
},
onClose : function(){
goto_login();
}
});
}else{
alert("注册失败:" + dataX.msg)
}
},
error:function(jqXHR){
alert("注册异常:"+ jqXHR.statusText);
}
});
页面效果如下:
3.7 yml文件配置
由于springBoot的结构是在resources目录下有专门的templates文件夹和static文件夹,
templates用于放置一些html页面,static放置一些静态资源,由于SpringBoot会默认访问resources下的这两个文件夹下的html和js、css…文件,关于html和静态资源需要在yml文件中进行路径的配置,否则会访问不到静态资源,这里本人就踩了不少坑。
spring:
# 配置这个表示访问templates页面路径的前缀
thymeleaf:
prefix: classpath:/templates/
# 访问静态资源的路径,可以是多个,表示请求的静态资源会查找的目录
resources:
static-locations:
classpath:/META-INF/resources/,classpath:/resources/,classpath:/static/,classpath:/public/
# 访问静态资源的请求方式,就是html请求静态资源的时候需要以static开头
mvc:
static-path-pattern: /static/**
3.8 POM文件依赖
这里引用的依赖大多都是直接从maven仓库中找的稳定的新的,需要的可自行去搜索查找,点我
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.1.3.RELEASE
com.example
demo
0.0.1-SNAPSHOT
demo
Demo project for Spring Boot
1.8
2.9.2
2.0.0
1.2.57
1.2.0.Final
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-data-jpa
mysql
mysql-connector-java
org.springframework.boot
spring-boot-starter-data-redis
io.springfox
springfox-swagger2
${swagger.version}
io.springfox
springfox-swagger-ui
${swagger.version}
org.mybatis.spring.boot
mybatis-spring-boot-starter
${mybatis.version}
com.alibaba
fastjson
${fastjson.version}
org.mapstruct
mapstruct-jdk8
${mapstruct.version}
org.mapstruct
mapstruct-processor
${mapstruct.version}
provided
org.springframework.boot
spring-boot-starter-thymeleaf
org.springframework.boot
spring-boot-devtools
true
true
org.springframework.boot
spring-boot-starter-test
test
org.springframework.boot
spring-boot-maven-plugin
**至此从前端到后端使用springboot开发的登陆注册就完成了。**
4. 踩坑秘籍
4.1 xml文件返回结果是list的size为1,但是结果为null
如果数据库表字段和实体的类型匹配不到,在结果返回的时候使用resultType是不能将结果正常的返回出来的,我遇到的情况是查询出数据条数为1,结果的list的size的确是1,但是里面却是null,这个就是因为字段类型不匹配,导致的数据没有被填充到指定的实体中,这个时候应该使用resultMap对表字段和实体字段进行指定。这样就能正常的返回数据了。
4.2 Controller返回到html
如果想要返回到页面中那么controller的注解不能使用@RestController,应该使用@Controller,如果只是返回数据则无所谓。因为@RestController会将返回的结果转换为json串,所以不能返回到页面中。
4.3 关于静态资源的配置
由于springBoot的结构是在resources目录下有专门的templates文件夹和static文件夹,
templates用于放置一些html页面,static放置一些静态资源,由于SpringBoot会默认访问resources下的这两个文件夹下的html和js、css…文件,关于html和静态资源需要在yml文件中进行路径的配置,否则会访问不到静态资源,这里本人就踩了不少坑。
spring:
# 配置这个表示访问templates页面路径的前缀
thymeleaf:
prefix: classpath:/templates/
# 访问静态资源的路径,可以是多个,表示请求的静态资源会查找的目录
resources:
static-locations:
classpath:/META-INF/resources/,classpath:/resources/,classpath:/static/,classpath:/public/
# 访问静态资源的请求方式,就是html请求静态资源的时候需要以static开头
mvc:
static-path-pattern: /static/**
4.4 Swagger 配置
swagger需要在pom文件先添加依赖,具体依赖看上面的介绍,然后自定义一个swagger配置类,就可以在Controller中使用了,配置类参考:
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket buildDocket(){
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(buildApiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.example.demo.controller"))
.paths(PathSelectors.any())
.build();
}
public ApiInfo buildApiInfo(){
return new ApiInfoBuilder()
.title("SpringBoot配置Swagger文档API")
.description("简单优雅的RestFun风格")
.version("1.0")
.build();
}
}
4.5 mapstruct的配置和使用
mapstruct的配置我踩坑比较多,最初直接在在maven仓库中搜索找到了依赖直接放进去,写了接口类,发现编译后并不能自动生成实现类。
我在maven仓库下载的是core
代码执行过程中一直报错,后面在网上查找答案才发现,我引用错依赖了,应该引用下面两个:
添加完正确的引用后,写一个工具接口类就可以了:
代码下载地址
如果文章能够看懂,请自行编码,如实在完成不了再下载源码,编码注重的是自己敲代码的过程。下载地址点我
至此文章已全部结束,文章用来记录自己踩过的坑,同时希望能帮助一些人,文章篇幅过长,有问题希望大家指正。我们一起成长!!