[TOC]

讲师模块

项目基本环境搭建

  • 创建service_edu子子模块,继承service子模块,此时service导入依赖
    ```xml

     <!-- 依赖service-base,根据依赖传递,就不需要依赖common_utils -->
     <dependency>
         <groupId>com.atguigu</groupId>
         <artifactId>service-base</artifactId>
         <version>0.0.1-SNAPSHOT</version>
     </dependency>
    
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
    </dependency>

    <!--hystrix依赖,主要是用  @HystrixCommand -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
    </dependency>

    <!--服务注册-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <!--服务调用-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!--mybatis-plus-->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
    </dependency>

    <!--mysql-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>

    <!-- velocity 模板引擎, Mybatis Plus 代码生成器需要 -->
    <dependency>
        <groupId>org.apache.velocity</groupId>
        <artifactId>velocity-engine-core</artifactId>
    </dependency>

    <!--swagger-->
    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger2</artifactId>
    </dependency>
    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger-ui</artifactId>
    </dependency>

    <!--lombok用来简化实体类:需要安装lombok插件-->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>

    <!--xls-->
    <dependency>
        <groupId>org.apache.poi</groupId>
        <artifactId>poi</artifactId>
    </dependency>

    <dependency>
        <groupId>org.apache.poi</groupId>
        <artifactId>poi-ooxml</artifactId>
    </dependency>

    <dependency>
        <groupId>commons-fileupload</groupId>
        <artifactId>commons-fileupload</artifactId>
    </dependency>

    <!--httpclient-->
    <dependency>
        <groupId>org.apache.httpcomponents</groupId>
        <artifactId>httpclient</artifactId>
    </dependency>
    <!--commons-io-->
    <dependency>
        <groupId>commons-io</groupId>
        <artifactId>commons-io</artifactId>
    </dependency>
    <!--gson-->
    <dependency>
        <groupId>com.google.code.gson</groupId>
        <artifactId>gson</artifactId>
    </dependency>

    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
    </dependency>
</dependencies>

-  创建主启动类,**注意扫描com.atguigu下的配置文件**,此时需要使用swagger  
```java
@SpringBootApplication
@ComponentScan("com.atguigu")   // 扫描common工程下的配置文件
public class EduApplication {
    public static void main(String[] args) {
        SpringApplication.run(EduApplication.class, args);
    }
}
  • 创建application.yml配置文件,使用8081端口,配置员时注意加上时区和编码格式
    server:
    port: 8001             # 服务端口
    spring:
    application:
     name: service-edu    # 微服务名称
    profiles:
     active: dev          # 设置为开发环境
    datasource:            # 配置数据源
     driver-class-name: com.mysql.cj.jdbc.Driver
     url: jdbc:mysql://localhost:3306/guli_edu?characterEncoding=utf-8&serverTimezone=GMT%2B8
     username: root
     password: root
    mybatis-plus:            # mybatis-plus日志
    configuration:
     log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    

代码生成器

此模块使用mybatis-plus提供的代码生成器,自动生成controller,service,.xml文件

  • 在测试包中创建自动生成类,单元测试即可生成代码
    以后只需要修改部分配置即可。 ```java public class CodeGenerator {

    @Test public void run() {

     // 1、创建代码生成器
     AutoGenerator mpg = new AutoGenerator();
    
     // 2、全局配置
     GlobalConfig gc = new GlobalConfig();
     String projectPath = System.getProperty("user.dir");
     gc.setOutputDir("E:\\guli_parent\\service\\service-edu" + "/src/main/java");   // 使用绝对路径
     gc.setAuthor("peanut");
     gc.setOpen(false); //生成后是否打开资源管理器
     gc.setFileOverride(false); //重新生成时文件是否覆盖
     gc.setServiceName("%sService");    //去掉Service接口的首字母I
     gc.setIdType(IdType.ID_WORKER_STR); //主键策略
     gc.setDateType(DateType.ONLY_DATE);//定义生成的实体类中日期类型
     gc.setSwagger2(true);//开启Swagger2模式
    
     mpg.setGlobalConfig(gc);
    
     // 3、数据源配置
     DataSourceConfig dsc = new DataSourceConfig();
     dsc.setUrl("jdbc:mysql://localhost:3306/guli_edu?serverTimezone=GMT%2B8");
     dsc.setDriverName("com.mysql.cj.jdbc.Driver");
     dsc.setUsername("root");
     dsc.setPassword("root");
     dsc.setDbType(DbType.MYSQL);
     mpg.setDataSource(dsc);
    
     // 4、包配置
     PackageConfig pc = new PackageConfig();
     pc.setModuleName("eduservice"); //模块名
     pc.setParent("com.atguigu");
     pc.setController("controller");
     pc.setEntity("entity");
     pc.setService("service");
     pc.setMapper("mapper");
     mpg.setPackageInfo(pc);
    
     // 5、策略配置
     StrategyConfig strategy = new StrategyConfig();
     strategy.setInclude("edu_teacher");
     strategy.setNaming(NamingStrategy.underline_to_camel);//数据库表映射到实体的命名策略
     strategy.setTablePrefix(pc.getModuleName() + "_"); //生成实体时去掉表前缀
    
     strategy.setColumnNaming(NamingStrategy.underline_to_camel);//数据库表字段映射到实体的命名策略
     strategy.setEntityLombokModel(true); // lombok 模型 @Accessors(chain = true) setter链式操作
    
     strategy.setRestControllerStyle(true); //restful api风格控制器
     strategy.setControllerMappingHyphenStyle(true); //url中驼峰转连字符
    
     mpg.setStrategy(strategy);
    
    // 6、执行
    mpg.execute();
}

}


<a name="20f525f0"></a>
## 后台系统前端项目创建

vue-element-admin是基于element-ui 的一套后台管理系统集成方案。vueAdmin-template是基于vue-element-admin的一套后台管理系统基础模板(最少精简版),可作为模板进行二次开发。此项目我们使用vue-element-admin模板。

-  将压缩包解压后引入vscode工作区<br />![](https://gitee.com/ljlGitee001/pictures/raw/master/img/202202280924502.png#crop=0&crop=0&crop=1&crop=1&id=YzyqH&originHeight=549&originWidth=252&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=) 
-  总项目结构目录:<br />├── build // 构建脚本<br />├── config // 全局配置<br />├── node_modules // 项目依赖模块<br />├── src //项目源代码<br />├── static // 静态资源<br />└── package.jspon // 项目信息和依赖配置<br />src中项目结构:<br />├── api // 各种接口<br />├── assets // 图片等资源<br />├── components // 各种公共组件,非公共组件在各自view下维护<br />├── icons //svg icon<br />├── router // 路由表<br />├── store // 存储<br />├── styles // 各种样式<br />├── utils // 公共工具,非公共工具,在各自view下维护<br />├── views // 各种layout<br />├── App.vue //**_项目顶层组件_**<br />├── main.js //**_项目入口文件_**<br />└── permission.js //认证入口 
-  修改部分配置 
   -  index.html(项目的html入口)  
```html
 <title>谷粒学院后台管理系统</title>
  • 打开 src/main.js(项目的js入口),第7行,修改语言为 zh-CN,使用中文语言环境,例如:日期时间组件

    import locale from 'element-ui/lib/locale/lang/zh-CN' // lang i18n
    
  • config/index.js中修改配置,第25行,禁止使用Eslint语法检查,过于严格,此项目不使用

    useEslint: false,
    
  • src/components/Breadcrumb/index.vue修改面包屑文字,38行

    meta: { title: '首页' }
    
  • 此时根据依赖配置使用npm下载依赖
    npm install
  • 运行项目
    npm run dev

搭建Nginx反向代理

  • config/dev.env.js 中BASE_API 为项目的easymock地址,目前具有模拟登录、登出、获取用户信息的功能
    config/dev.env.js,只有一个api地址的配置位置,而我们实际的后端有很多微服务,所以接口地址有很多,我们可以使用nginx反向代理让不同的api路径分发到不同的api服务器中

    BASE_API: '"https://easy-mock.com/mock/5950a2419adc231f356a6636/vue-admin"',
    
  • 步骤:

    • 在conf/nginx.conf中配置代理路径,注意监听9001端口,location中~为映射路径

      server {
        listen       9001;
        server_name  localhost;
      
        location ~ /eduservice/ {
            proxy_pass http://localhost:8001;
        }
        location ~ /eduoss/ {
            proxy_pass http://localhost:8002;
        }
        location ~ /eduvod/ {
            proxy_pass http://localhost:8003;
        }
        location ~ /cmsservice/ {
            proxy_pass http://localhost:8004;
        }
        location ~ /ucenterservice/ {
            proxy_pass http://localhost:8006;
        }
        location ~ /edumsm/ {
            proxy_pass http://localhost:8005;
        }
        location ~ /orderservice/ {
            proxy_pass http://localhost:8007;
        }
        location ~ /staservice/ {
            proxy_pass http://localhost:8008;
        }
        location ~ /admin/ {
            proxy_pass http://localhost:8009;
        }
      }
      
    • 此项目使用Windows版的nginx,下载压缩后进入目录,在命令行中启动nginx

讲师模块 - 图1 进入命令行,使用nginx.exe启动nginx

  • 此时修改API访问路径为nginx监听的路径和端口号,注意所有controller中加上跨域注解@CrossOrigin
    BASE_API: '"http://localhost:9001"',
    

查询讲师

跨域问题

跨域问题:一个ip地址访问另一个ip的地址,如果有访问协议,ip地址,端口号其中一个不一致,会造成跨越问题。

解决方法:

后端

  • 创建VO对象,来封装查询条件对象

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class TeacherQuery {
     private static final long serialVersionUID = 1L;
    
     @ApiModelProperty(value = "教师名称,模糊查询")
     private String name;
    
     @ApiModelProperty(value = "头衔 1高级讲师 2首席讲师")
     private Integer level;
    
     @ApiModelProperty(value = "查询开始时间", example = "2019-01-01 10:10:10")
     private String begin;//注意,这里使用的是String类型,前端传过来的数据无需进行类型转换
    
     @ApiModelProperty(value = "查询结束时间", example = "2019-12-01 10:10:10")
     private String end;
    }
    
  • controller接口,此时使用mybatis-plus的条件构造来进行模糊查询,使用分页插件进行分页

    /**
    * <p>
    * 讲师 前端控制器
    * </p>
    *
    * @author peanut
    * @since 2022-02-09
    */
    @Api(tags = "讲师管理")
    @RestController
    @RequestMapping("/eduservice/edu-teacher")
    @CrossOrigin
    public class EduTeacherController {
    
     @Autowired
     private EduTeacherService eduTeacherService;
    
     @ApiOperation(value = "分页带条件查询讲师")
     @PostMapping("pageTeacherCondition/{currentPage}/{limit}")
     public ResultEntity pageTeacherCondition(
             @ApiParam(name = "currentPage", value = "当前页码", required = true)
             @PathVariable("currentPage") long currentPage,
             @ApiParam(name = "limit", value = "本页数量", required = true)
             @PathVariable("limit") long limit,
             @ApiParam(name = "teacherQuery", value = "查询条件", required = true)
             // 使用@RequestBody时,前端传入的时json数据,且需要使用post请求
             @RequestBody(required = false) TeacherQuery teacherQuery
     ) {
         // 创建Page对象,封装封装的内容
         Page<EduTeacher> eduTeacherPage = new Page<>(currentPage, limit);
         // 获取所有条件
         String name = teacherQuery.getName();
         Integer level = teacherQuery.getLevel();
         String begin = teacherQuery.getBegin();
         String end = teacherQuery.getEnd();
         // 判单条件是否存在来封装查询条件
         QueryWrapper<EduTeacher> eduTeacherQueryWrapper = new QueryWrapper<>();
         if (!StringUtils.isEmpty(name)) {
             eduTeacherQueryWrapper.like("name", name);
         }
    
         if (!StringUtils.isEmpty(level)) {
             eduTeacherQueryWrapper.eq("level", level);
         }
    
         if (!StringUtils.isEmpty(begin)) {
             eduTeacherQueryWrapper.ge("gmt_create", begin);
         }
    
         if (!StringUtils.isEmpty(end)) {
             eduTeacherQueryWrapper.le("gmt_modified", end);
         }
    
         // 调用方法实现分页,会自动将分页数据封装到eduTeacherPage中
         eduTeacherService.page(eduTeacherPage, eduTeacherQueryWrapper);
         // 获取总页数
         long total = eduTeacherPage.getTotal();
         // 获取封装的记录
         List<EduTeacher> eduTeacherList = eduTeacherPage.getRecords();
         HashMap<String, Object> map = new HashMap<>();
         map.put("total", total);
         map.put("rows", eduTeacherList);
         return ResultEntity.ok().data(map);
     }
    }
    
  • 修改返回json的全局时间格式,在yml配置

    spring:
    jackson:                            # 配置json全局时间
     date-format: yyyy-MM-dd HH:mm:ss  # 配置返回json的时间格式
     time-zone: GMT+8                  # json是格林尼治时间,和我们相差8小时,需要加上8
    
  • 添加配置类,注册分页插件,并统一扫描mapper接口,包括

    @Configuration
    @MapperScan("com.atguigu.eduservice.mapper") // 扫描mapper接口
    public class EduConfig {
     // 分页插件
     @Bean
     public PaginationInterceptor paginationInterceptor() {
         return new PaginationInterceptor();
     }
    }
    

前端

  • 添加路由,在src/router/index.js中修改路由地址,注意使用 @/…,表示在src目录下寻找

    {
     path: '/edu/teacher',
     component: Layout,
     redirect: '/edu/teacher/list',
     name: 'Teacher',
     meta: { title: '讲师管理', icon: 'peoples' },
     children: [
       {
         path: 'list',
         name: '讲师列表',
         component: () => import('@/views/edu/teacher/list'),
         meta: { title: '讲师列表', icon: 'table' }
       },
       {
         path: 'create',
         name: '添加讲师',
         component: () => import('@/views/edu/teacher/save'),
         meta: { title: '添加讲师', icon: 'tree' }
       }
     ]
    },
    
  • 在src/views/中创建路由对应页面
    讲师模块 - 图2

  • 在src/api中创建teacher.js定义访问接口地址
    讲师模块 - 图3
    ```javascript import request from ‘@/utils/request’

export default { // 讲师列表,条件查询 // currentPage表示当前页,limit表示每页的记录数,teacherQuery表示条件对象 getTeacherListPage(currentPage, limit, teacherQuery) { return request({ url: /eduservice/edu-teacher/pageTeacherCondition/${currentPage}/${limit}, method: ‘post’, // post请求后端 // data表示把对象转换为json格式传输到后端 data: teacherQuery }) } }


-  在list讲师显示页面调用接口方法,并获取接口数据  
```vue
<script>
// 导入teacher.js代码
import teacher from "@/api/teacher/teacher.js";

export default {
  // 核心代码位置
  data() {
    // 定义变量和初始值
    return {
      list: null, // 查询之后接口返回的集合
      currentPage: 1, // 当前页
      total: 0,
      limit: 5, // 当前页的记录数
      teacherQuery: {}, // 条件封装对象
    };
  },
  created() {
    // 页面渲染之前执行,一般调用methods定义的方法
    this.getList();
  },
  methods: {
    // 创建具体的方法,调用teacher.js的方法

    // 讲师列表的方法,此处传入currentPage参数(默认为1),分页时element UI单击事件时自动传入currentPage
    getList(currentPage = 1) {
      this.currentPage = currentPage; // 将点击分页传入的currentPage赋值给data中定义的currentPage
      teacher
        .getTeacherListPage(this.currentPage, this.limit, this.teacherQuery)
        // 请求成功执行的方法
        .then((response) => {
          // response接口返回的数据
          this.list = response.data.rows;
          this.total = response.data.total;
        })
        // 请求失败执行的方法
        .catch((error) => {
          console.log(error)
        })
    }
  }
}
</script>
  • 此处使用element UI,显示讲师数据,显示分页条,显示条件查询表单

    <template>
    <div class="app-container">
     <!--查询表单-->
     <el-form :inline="true" class="demo-form-inline">
       <el-form-item>
         <el-input v-model="teacherQuery.name" placeholder="讲师名" />
       </el-form-item>
    
       <el-form-item>
         <el-select v-model="teacherQuery.level" clearable placeholder="讲师头衔">
           <el-option :value="1" label="高级讲师" />
           <el-option :value="2" label="首席讲师" />
         </el-select>
       </el-form-item>
    
       <el-form-item label="添加时间">
         <el-date-picker
           v-model="teacherQuery.begin"
           type="datetime"
           placeholder="选择开始时间"
           value-format="yyyy-MM-dd HH:mm:ss"
           default-time="00:00:00"
         />
       </el-form-item>
       <el-form-item>
         <el-date-picker
           v-model="teacherQuery.end"
           type="datetime"
           placeholder="选择截止时间"
           value-format="yyyy-MM-dd HH:mm:ss"
           default-time="00:00:00"
         />
       </el-form-item>
    
       <el-button type="primary" icon="el-icon-search" @click="getList()"
         >查询</el-button
       >
       <el-button type="default" @click="resetData()">清空</el-button>
     </el-form>
    
     <!-- 表格 -->
     <el-table :data="list" border fit highlight-current-row>
       <el-table-column label="序号" width="70" align="center">
         <template slot-scope="scope">
           {{ (currentPage - 1) * limit + scope.$index + 1 }}
         </template>
       </el-table-column>
    
       <el-table-column prop="name" label="名称" width="80" />
    
       <el-table-column label="头衔" width="80">
         <template slot-scope="scope">
           {{ scope.row.level === 1 ? "高级讲师" : "首席讲师" }}
         </template>
       </el-table-column>
    
       <el-table-column prop="intro" label="资历" />
    
       <el-table-column prop="gmtCreate" label="添加时间" width="160" />
    
       <el-table-column prop="sort" label="排序" width="60" />
    
       <el-table-column label="操作" width="200" align="center">
         <template slot-scope="scope">
           <router-link :to="'/edu/teacher/edit/' + scope.row.id">
             <el-button type="primary" size="mini" icon="el-icon-edit"
               >修改</el-button
             >
           </router-link>
           <el-button
             type="danger"
             size="mini"
             icon="el-icon-delete"
             @click="removeDataById(scope.row.id)"
             >删除</el-button
           >
         </template>
       </el-table-column>
     </el-table>
    
     <!-- 分页 -->
     <el-pagination
       :current-page="currentPage"
       :page-size="limit"
       :total="total"
       style="padding: 30px 0; text-align: center"
       layout="total, prev, pager, next, jumper"
       @current-change="getList"
     />
    </div>
    </template>
    
  • 表单清空方法,注意此时表单的属性值时上香绑定查询条件的,在methods中定义方法

    // 清空按钮执行方法,清空条件查询数据
      resetData(){
          // 由于表单查询时是双向绑定,此时将teacherQuery设置为空即可清空
           this.teacherQuery = {}
           // 默认查询讲师数据
           this.getList()
      }
    

删除讲师

后端

  • 配置逻辑删除插件,在EduConfig中配置

    // 逻辑删除组件
    @Bean
    public ISqlInjector injector() {
     return new LogicSqlInjector();
    }
    
  • 在isDeleted添加逻辑删除注解
    默认删除后的设为1,未删除的为0,不用自己修改策略,使用默认的即可。

    @TableLogic // 逻辑删除字段
    @ApiModelProperty(value = "逻辑删除 1(true)已删除, 0(false)未删除")
    private Integer isDeleted;
    
  • controller方法,注意使用delete请求

    @ApiOperation(value = "根据id逻辑删除讲师")
    @DeleteMapping("{id}")
    public ResultEntity removeTeacher(
         @ApiParam(name = "id", value = "讲师id", required = true)
         @PathVariable("id") String id) {
     boolean flag = eduTeacherService.removeById(id);
     if (flag) {
         return ResultEntity.ok();
     } else {
         return ResultEntity.error();
     }
    }
    

前端

  • 修改删除按钮,scope指的是表格的所有行数据,row.id指的是点击该行的id值,调用removeDataById进行删除

    <el-button
             type="danger"
             size="mini"
             icon="el-icon-delete"
             @click="removeDataById(scope.row.id)"
             >删除</el-button
           >
    
  • 在methods中定义删除方法,实现弹框并调用api中删除接口

    // 删除讲师
     removeDataById(id) {
       // element UI弹框
       this.$confirm("此操作将永久删除该文件, 是否继续?", "提示", {
         confirmButtonText: "确定",
         cancelButtonText: "取消",
         type: "warning",
       })
         .then(() => {
           // 调用删除的方法
           teacher.removeById(id);
           this.$message({
             type: "success",
             message: "删除成功!",
           });
           // 再次调用分页条件查询,可能刷新不生效,此处多调用几次
           this.getList(this.currentPage);
           this.getList(this.currentPage);
           this.getList(this.currentPage);
         })
         .catch(() => {
           this.$message({
             type: "info",
             message: "已取消删除",
           });
         });
     }
    
  • 在api中定义删除接口的地址,注意使用delete请求方式

    // 删除讲师
     removeById(teacherId){
         return request({
             url: `/eduservice/edu-teacher/${teacherId}`,
             method: 'delete'
         })
     }
    

新增讲师

后端

  • 给EduTeacher实体的字段添加自动填充注解
    ```java @TableField(fill = FieldFill.INSERT) // 插入时自动填充 @ApiModelProperty(value = “创建时间”) private Date gmtCreate;

@TableField(fill = FieldFill.INSERT) // 插入和修改时自动填充 @ApiModelProperty(value = “更新时间”) private Date gmtModified;


-  配置自动填充策略,注意在**service_base**模块下,因为这两个字段为所有表都需要的的字段,此时策略可以在**其他模块使用**,此时主启动类已经扫描了com.atguigu包,**注入IOC容器即可自动装配**<br />![](https://gitee.com/ljlGitee001/pictures/raw/master/img/202202280925402.png#crop=0&crop=0&crop=1&crop=1&id=crnSi&originHeight=252&originWidth=367&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)<br />注意使用[**@Component **](/Component )** **注入IOC容器  

/**

-  配置自动填充值<br />*/<br />[@Component ](/Component ) <br />public class MyMetaObjectHandler implements MetaObjectHandler {<br />[@Override ](/Override ) <br />public void insertFill(MetaObject metaObject) {<br />this.setFieldValByName("gmtCreate", new Date(), metaObject);<br />this.setFieldValByName("gmtModified", new Date(), metaObject);<br />}<br />[@Override ](/Override ) <br />public void updateFill(MetaObject metaObject) {<br />this.setFieldValByName("gmtModified", new Date(), metaObject);<br />}<br />}
  • Controller方法,使用POST方式提交即可,注意测试数据是以json传输,使用@RequestBody
@ApiOperation(value = "新增讲师")
@PostMapping("addTeacher")
public ResultEntity addTeacher(
        @ApiParam(name = "eduTeacher", value = "讲师信息", required = true)
        @RequestBody EduTeacher eduTeacher
) {
    boolean flag = eduTeacherService.save(eduTeacher);
    if (flag) {
        return ResultEntity.ok();
    } else {
        return ResultEntity.error();
    }
}

前端

  • 初始化组件,显示表单填写数据,通过双向绑定封装Teacher对象

    <template>
    <div class="app-container">
     <el-form label-width="120px">
       <el-form-item label="讲师名称">
         <el-input v-model="teacher.name" />
       </el-form-item>
       <el-form-item label="讲师排序">
         <el-input-number
           v-model="teacher.sort"
           controls-position="right"
           :min="0"
         />
       </el-form-item>
       <el-form-item label="讲师头衔">
         <el-select v-model="teacher.level" clearable placeholder="请选择">
           <!--
             数据类型一定要和取出的json中的一致,否则没法回填
             因此,这里value使用动态绑定的值,保证其数据类型是number
           -->
           <el-option :value="1" label="高级讲师" />
           <el-option :value="2" label="首席讲师" />
         </el-select>
       </el-form-item>
       <el-form-item label="讲师资历">
         <el-input v-model="teacher.career" />
       </el-form-item>
       <el-form-item label="讲师简介">
         <el-input v-model="teacher.intro" :rows="10" type="textarea" />
       </el-form-item>
    
       <!-- 讲师头像:TODO -->
    
       <el-form-item>
         <el-button
           :disabled="saveBtnDisabled"
           type="primary"
           @click="saveOrUpdate"
           >保存</el-button
         >
       </el-form-item>
     </el-form>
    </div>
    </template>
    
  • 定义api地址,注意teacher后端是以@RequestBody接收,需要将teacher用json传输过去

    // 新增讲师
     saveTeacher(teacher){
         return request({
             url: `/eduservice/edu-teacher/addTeacher`,
             method: 'post',
             // 用json传输的方式
             data: teacher
         })
     }
    
  • 新增方法,在数据初始化后需要将保存按钮禁用,防止重复提交、保存成功后,使用了路由跳转跳转到讲师列表
    ```javascript

    
    <a name="cebb120b"></a>
    ## 更新讲师
    
    <a name="e778d61a-3"></a>
    ### 后端
    
    -  先根据id查找讲师,用于回显  
    ```java
    @ApiOperation(value = "根据id查询讲师")
    @GetMapping("{id}")
    public ResultEntity getTeacherById(
            @ApiParam(name = "id", value = "讲师id", required = true)
            @PathVariable("id") String id
    ) {
        EduTeacher eduTeacher = eduTeacherService.getById(id);
        return ResultEntity.ok().data("teacher", eduTeacher);
    }
    
    • 根据id更新讲师,使用put请求
      @ApiOperation(value = "根据id修改讲师")
      @PutMapping("{id}")
      public ResultEntity updateById(
           @ApiParam(name = "id", value = "讲师id", required = true)
           @PathVariable("id") String id,
           @ApiParam(name = "eduTeacher", value = "讲师信息", required = true)
           @RequestBody EduTeacher eduTeacher
      ) {
       // 将需要修改的id值设置到eduTeacher中
       eduTeacher.setId(id);
       // 修改讲师数据
       boolean flag = eduTeacherService.updateById(eduTeacher);
       if (flag) {
           return ResultEntity.ok();
       } else {
           return ResultEntity.error();
       }
      }
      

    前端

    回显

    • 由于新增讲师和更新讲师使用的是同一个页面,更新讲师的通过点击修改按钮跳转到页面,因此更新讲师的路由不需要显示(hidden: true),且路由需要传入id值,用于根据id查询讲师

      {
           // 相当于通配符,需要在路径中传入id
           path: 'edit/:id',
           name: '修改讲师',
           component: () => import('@/views/edu/teacher/save'),
           meta: { title: '修改讲师', icon: 'tree' },
           // 由于修改使用的是按钮,此处为隐藏路由
           hidden: true
         }
      
    • 删除按钮

      <el-button
               type="danger"
               size="mini"
               icon="el-icon-delete"
               @click="removeDataById(scope.row.id)"
               >删除</el-button
             >
      
    • api中定义接口方法

      // 根据id查找讲师
       getTeacherById(id){
           return request({
               url: `/eduservice/edu-teacher/${id}`,
               method: 'get'
           })
       }
      
    • 在methods中定义方法,此时利用vue的双向绑定来回显表单数据

      // 通过id查找讲师
       getTeacherById(id) {
         teacher
           .getTeacherById(id)
           .then((response) => {
             // 由于表单和teacher是双向绑定,只需将查询的数据赋值给teacher即可显示在表单上
             this.teacher = response.data.teacher;
           })
           .catch((error) => {
             this.$message({
               type: "error",
               message: "没有该讲师信息",
             });
           });
       }
      
    • 在created()页面渲染前调用回显方法即可,此处通过获取路由上的参数id,来判断进行什么操作

        // 在页面渲染前,判断路径是否有id值,从而判断进行修改还是新增
         if (this.$route.params && this.$route.params.id) {
           // 有id即,通过id值查找讲师
           this.getTeacherById(this.$route.params.id);
         } else {
           // 如果没有id值,防止修改的数据回显在新增表单上,此时清空表单
           this.teacher = {};
         }
      

    修改

    • api中定义修改接口地址

      // 根据id修改讲师
       updateTeacherById(teacher){
           return request({
               url: `/eduservice/edu-teacher/${teacher.id}`,
               method: 'put',
               data: teacher
           })
       }
      
    • methods中定义修改方法,最后路由跳转回到讲师列表

      updateTeacherById() {
         teacher
           .updateTeacherById(this.teacher)
           .then((response) => {
             this.$message({
               type: "success",
               message: "修改成功!",
             });
             // 回到讲师列表 路由跳转实现
             this.$router.push({ path: "/edu/teacher/list" });
           })
           .catch((error) => {
             this.$message({
               type: "error",
               message: "修改失败",
             });
           });
       }
      
    • methods中定义判断更新和新增方法,通过teacher中的id值是否存在来判断调用的方法

      saveOrUpdate() {
         this.saveBtnDisabled = true;
         // 根据teacher中是否有id来判断进行删除还是新增
         if (this.teacher.id) {
           this.updateTeacherById();
         } else {
           this.saveData();
         }
       }
      

    vue监听机制

    在点击修改按钮后表单回显,此时点击新增按钮,表单回显数据还在,此时需要清空表单,即this.teacher = {};

    但created()方法是在页面渲染前执行,而此时只是路由变化,页面并没有变化,因此不能解决,此时需要使用路由监听机制

    增加watch方法来监听路由变化,一旦发生变化,调用封装的判断方法

      created() {
        this.init();
      },
      watch: {
        // 监听
        $route(to, from) {
          // 路由变化方式,路由发生变化,方法就会执行,区分新增和修改,从而是否清空数据
          this.init();
        },
      },
      methods: {
        // 封装初始化方法
        init() {
          // 在页面渲染前,判断路径是否有id值,从而判断进行修改还是新增
          if (this.$route.params && this.$route.params.id) {
            // 有id即,通过id值查找讲师
            this.getTeacherById(this.$route.params.id);
          } else {
            // 如果没有id值,防止修改的数据回显在新增表单上,此时清空表单
            this.teacher = {};
          }
        },
    

    讲师头像上传

    OSS存储

    介绍

    阿里云对象存储OSS(Object Storage Service)是一款海量、安全、低成本、高可靠的云存储服务,可提供99.9999999999%(12个9)的数据持久性,99.995%的数据可用性。多种存储类型供选择,全面优化存储成本。

    尚筹网使用过OSS,以下为主要步骤:

    创建bucket

    讲师模块 - 图4

    创建Access Key

    • 使用Access Key访问,为了安全,我使用的是子Access Key
      点击头像 -> Access Key管理 -> 使用子账户Access Key -> 创建用户即可
      讲师模块 - 图5
    • 使用子AK一定要授权oss管理
      讲师模块 - 图6
    • 最后保存好生成的AccessKey Secret和AccessKey ID

    后端

    工程搭建

    • 创建service_oss工程

      • service模块下只有service_oss使用oss的sdk,所以在service_oss导入依赖

        <dependencies>
        <!--aliyunOSS-->
        <dependency>
          <groupId>com.aliyun.oss</groupId>
          <artifactId>aliyun-sdk-oss</artifactId>
        </dependency>
        
        <!-- 日期工具栏依赖 -->
        <dependency>
          <groupId>joda-time</groupId>
          <artifactId>joda-time</artifactId>
        </dependency>
        </dependencies>
        
      • 主启动类,需要排除数据源的自动装配

        @SpringBootApplication(exclude = DataSourceAutoConfiguration.class) // 不用配置数据库,排除数据库的自动装配
        @ComponentScan("com.atguigu")   // 扫描common工程下的配置文件
        public class AliOssApplication {
        public static void main(String[] args) {
          SpringApplication.run(AliOssApplication.class , args);
        }
        }
        
      • 配置文件

        server:
        port: 8002
        spring:
        profiles:
        active: dev
        
    • 创建项目结构
      讲师模块 - 图7

    调用OSS接口

    • 创建常量类,将配置信息放在yml配置文件中,此处使用@value来映射yml的配置信息,也可以使用ConfigurationProperties(prefix = “aliyun.oss.file”)
      ```java /**

      • 常量类,读取application.yml中的配置文件 */ @Component public class ConstantPropertiesUtil implements InitializingBean { // 当项目已启动,实现spring接口,spring加载之后,执行接口的一个方法

        @Value(“${aliyun.oss.file.endpoint}”) private String endpoint;

        @Value(“${aliyun.oss.file.bucketName}”) private String bucketName;

        @Value(“${aliyun.oss.file.accessKeyId}”) private String accessKeyId;

        @Value(“${aliyun.oss.file.accessKeySecret}”) private String accessKeySecret;

        public static String END_POINT; public static String ACCESS_KEY_ID; public static String ACCESS_KEY_SECRET; public static String BUCKET_NAME;

    @Override
    public void afterPropertiesSet() throws Exception {
        END_POINT = endpoint;
        ACCESS_KEY_ID = accessKeyId;
        ACCESS_KEY_SECRET = accessKeySecret;
        BUCKET_NAME = bucketName;
    }
    

    }

    
    -  yml配置文件,**可以设置上传图片大小限制,aliyun.oss.file配置信息**  
    ```yaml
    spring:
        servlet:
            multipart:
              max-request-size: 10MB
    aliyun:
      oss:
        file:
          endpoint: oss-cn-chengdu.aliyuncs.com
          bucketName: project-guli-education
          accessKeyId: ...
          accessKeySecret: ...
    
    • controller映射前端请求,注意加上@CrossOrigin解决跨域
      ```java @RestController @RequestMapping(“/eduoss/fileoss”) @CrossOrigin public class OssController {

      @Autowired private OssService ossService;

      // 上传头像方法 @PostMapping public ResultEntity uploadOssFile(MultipartFile multipartFile) {

       // 获取上传的文件地址
       String url = ossService.uploadFileAvatar(multipartFile);
       return ResultEntity.ok().data("url", url);
      

      }

    }

    
    -  接口以及接口实现<br />OssService接口 <br />OssService实现  
    ```java
    public interface OssService {
        String uploadFileAvatar(MultipartFile multipartFile);
    }
    
    @Service
    public class OssServiceImpl implements OssService {
    
        @Override
        public String uploadFileAvatar(MultipartFile multipartFile) {
    
            //获取阿里云存储相关常量
            String endPoint = ConstantPropertiesUtil.END_POINT;
            String accessKeyId = ConstantPropertiesUtil.ACCESS_KEY_ID;
            String accessKeySecret = ConstantPropertiesUtil.ACCESS_KEY_SECRET;
            String bucketName = ConstantPropertiesUtil.BUCKET_NAME;
    
            // 上传的url
            String uploadUrl = null;
    
            try {
                // 创建OSS实例
                OSS ossClient = new OSSClientBuilder().build(endPoint, accessKeyId, accessKeySecret);
    
                // 获取上传文件流
                InputStream inputStream = multipartFile.getInputStream();
    
                // 构建日期路径:avatar/2019/02/26/文件名
                String datePath = new DateTime().toString("yyyy/MM/dd");
    
                // 文件名:uuid + 文件名
                String fileName = multipartFile.getOriginalFilename();
                String uuid = UUID.randomUUID().toString().replaceAll("-","");
                String newName = uuid + fileName;
                String fileUrl =  datePath + "/" + newName;
    
                // 文件上传至阿里云
                ossClient.putObject(bucketName, fileUrl, inputStream);
    
                // 关闭OSSClient。
                ossClient.shutdown();
    
                // 拼接返回的url地址
                uploadUrl = "https://" + bucketName + "." + endPoint + "/" + fileUrl;
    
                return uploadUrl;
            } catch (IOException e) {
                e.printStackTrace();
                return null;
            }
        }
    }
    

    前端

    • 将资料中的头像框组件复制到项目
      讲师模块 - 图8
    • 引入头像框组件

      // 引入头像弹框组件
      import ImageCropper from "@/components/ImageCropper";
      import PanThumb from "@/components/PanThumb";
      
    • export default 中申明头像框组件

      // 声明头像弹框组件
      components: { ImageCropper, PanThumb },
      
    • 显示上传头像和上传弹框,注意弹框中的 field=”multipartFile” file值必须为后端接口Multipart的变量值一致,不然会空指针报错
      url=”BASE_API + ‘/eduoss/fileoss’” 中为nginx的访问路由和映射路径组成访问路径

        <!-- 讲师头像 -->
         <el-form-item label="讲师头像">
           <!-- 头衔缩略图 -->
           <pan-thumb :image="teacher.avatar" />
           <!-- 文件上传按钮 -->
           <el-button
             type="primary"
             icon="el-icon-upload"
             @click="imagecropperShow = true"
             >更换头像
           </el-button>
      
           <!--
           v-show:是否显示上传组件
           :key:类似于id,如果一个页面多个图片上传控件,可以做区分
           :url:后台上传的url地址
           @close:关闭上传组件
           @crop-upload-success:上传成功后的回调 -->
           <image-cropper
             v-show="imagecropperShow"
             :width="300"
             :height="300"
             :key="imagecropperKey"
             :url="BASE_API + '/eduoss/fileoss'"
             field="multipartFile"
             @close="close"
             @crop-upload-success="cropSuccess"
           />
         </el-form-item>
      
    • data中初始化弹框中的属性值

          BASE_API: process.env.BASE_API,
         imagecropperShow: false, // 是否显示上传组件
         imagecropperKey: 0, // 上传组件id
      
    • 申明方法上传方法

       // 上传成功后的回调函数
       cropSuccess(data) {
         // 将url赋值给teacher对象
         this.teacher.avatar = data.url;
         // 关闭弹窗
         this.imagecropperShow = false;
         // 上传组件初始化
         this.imagecropperKey = this.imagecropperKey + 1;
       },
      
       // 关闭上传组件
       close() {
         this.imagecropperShow = false;
         // 上传组件初始化
         this.imagecropperKey = this.imagecropperKey + 1;
       },