Reggie练习日记

Day 01 初建项目,完成登录、退出功能

1. 基于Maven创建一个普通的项目。不再演示。

2. 编辑pom依赖、创建数据库种各个表

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.5.0</version>
</parent>

<properties>
    <java.version>1.8</java.version>
</properties>

<dependencies>

    <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.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <scope>compile</scope>
    </dependency>

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

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.20</version>
    </dependency>

    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.62</version>
    </dependency>

    <dependency>
        <groupId>commons-lang</groupId>
        <artifactId>commons-lang</artifactId>
        <version>2.6</version>
    </dependency>

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>

    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.1.18</version>
    </dependency>

</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

 对于表,老师已经给了sql语句。这里就不说了

3. 创建spring boot的引导类、配置文件。

java下创建com.huayu.reggie,在其下新建ReggieApplication类表示spring boot的引导类。

@SpringBootApplication
@Slf4j  //用来显示出日志,方便调试
public class ReggieApplication {
    public static void main(String[] args) {
        SpringApplication.run(ReggieApplication.class, args);
        log.info("spring boot项目启动成功!!!");
    }
}

在resource下新建application.yml。

spring:
  application:
    name: my-reggie-takeaway #可设可不设,默认就是项目名
  datasource:
    url: jdbc:mysql:///reggie
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: 1234
    type: com.alibaba.druid.pool.DruidDataSource
mybatis-plus:
  configuration:

    #在映射实体或者属性时,将数据库中表名和字段名中的下划线去掉,按照驼峰命名法映射
    map-underscore-to-camel-case: true # 例如:user_name => UserName

    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #开启控制台 SQL 日志打印

  global-config:
    db-config:
      id-type: ASSIGN_ID # 递增策略

4. 分析登录功能

通过老师给我的前端页面,我们需要先分析下前端代码。

分析login.html;对于前端我们只截取重要代码,能看懂就好。
在点击登陆后我们进入的是这个代码:

async handleLogin() {//我们就关注这个方法就好了
          this.$refs.loginForm.validate(async (valid) => { //validate是一个校验
            if (valid) {
              this.loading = true
              let res = await loginApi(this.loginForm)//loginApi方法封装到了js文件里,
                                                      // 负责发送和接受数据,接受的数据定义为res
              if (String(res.code) === '1') { //规定1就是成功
                localStorage.setItem('userInfo',JSON.stringify(res.data))//localStorage保存到浏览器
                window.location.href= '/backend/index.html' //进入主页面
              } else {
                this.$message.error(res.msg)
                this.loading = false}}})}

封装到js文件里的代码:

function loginApi(data) {
  return $axios({
    'url': '/employee/login',
    'method': 'post',
    data})} //data是服务端给前端的数据

我们只需要得到以下消息:用户输入用户名和密码点击登录,传递后端一个带有这两个信息的JSON数据,传递到 “/employee/login” ;我们查询到数据后,返回给前端一个带有msg、code和data的一种数据,我们需要围绕employee这个表,进行数据传递。

5. 完成登录功能

5.1 创建标准的层次结构,基于spring boot与mybatis-plus的开发

在com.huayu.reggie下先创建经典的这四类或五个包:
1. mapper包  2.service包  3.service.impl包  4.controller包 5.pojo包

1)  pojo.Employee - 要与数据库种表相对应

@Data
public class Employee implements Serializable { //员工实体类
        private static final long serialVersionUID = 1L;
        private Long id;
        private String username;
        private String name;
        private String password;
        private String phone;
        private String sex;
        private String idNumber;
        private Integer status;
        private LocalDateTime createTime;
        private LocalDateTime updateTime;
        @TableField(fill = FieldFill.INSERT)//插入操作时字段进行自动填充
        private Long createUser;
        @TableField(fill = FieldFill.INSERT_UPDATE)//字段在进行插入和更新时进行自动填充
        private Long updateUser;
}

2)mapper.EmployeeMapper - 继承于来自Mybatis-plus的父类

@Mapper  //别忘了!
public interface EmployeeMapper extends BaseMapper<Employee> {
}

3) service.EmployeeService 与 service.impl.EmployeeServiceImpl

public interface EmployeeService extends IService<Employee> {
}
@Service  //要交给spring管理
public class EmployeeServiceImpl 
        extends ServiceImpl<EmployeeMapper, Employee> implements EmployeeService {
}

4) controller.EmployeeController

@RestController  //别忘了
@Slf4j
@RequestMapping("/employee")
public class EmployeeController {

    @Autowired
    private EmployeeService employeeService;
    
}

5.2 编写一个通用类R,用来前端后端传递数据

在reggie下新建common.R
查询成功返回code为1,将data数据传递过去
查是失败返回code为0,将msg错误信息传递过去

@Data
public class R<T> {

    private Integer code; //编码:1成功,0和其它数字为失败
    private String msg; //错误信息
    private T data; //数据
    private Map map = new HashMap(); //动态数据

    public static <T> R<T> success(T object) {//T写什么类,返回值就是T
        R<T> r = new R<T>();
        r.data = object;
        r.code = 1;
        return r;
    }

    public static <T> R<T> error(String msg) {
        R r = new R();
        r.msg = msg;
        r.code = 0;
        return r;
    }

    public R<T> add(String key, Object value) {
        this.map.put(key, value);
        return this;
    }
}

5.3 编写controller层

//登录
    @PostMapping("/login")
    public R<Employee> login(HttpServletRequest request, @RequestBody Employee employee) {
        // @RequestBody 将前端传递来的数据封装为 Employee 对象,要保证名字一样
        // HttpServletRequest为了将员工 id 存入 session
        //1. 传来的密码进行加密 - md5 加密
        String password = employee.getPassword();
        password = DigestUtils.md5DigestAsHex(password.getBytes());
        //2. 传来的用户名在数据库校验
        //查询对象
        LambdaQueryWrapper<Employee> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        //封装条件:第一个参数根据 employee 表中的 username 字段,查是不是和传递来的 username 一样
        lambdaQueryWrapper.eq(Employee::getUsername, employee.getUsername());
        Employee emp = employeeService.getOne(lambdaQueryWrapper);
        //3. 查询用户名失败返回失败结果
        if (emp == null) {
            return R.error("用户名未查到");
        }
        //4. 查询密码是否一致,不一致返回失败结果
        if (!emp.getPassword().equals(password)) {
            return R.error("密码输入错误");
        }
        //5. 查看员工状态
        if (emp.getStatus() == 0) {
            return R.error("员工已禁用");
        }
        //6. 登陆成功,将员工名字存入 session
        request.getSession().setAttribute("employeeId", emp.getId());
        return R.success(emp);
    }

整理一下薄弱的知识点:
1. md5加密使用的是:DigestUtils.md5DigestAsHex(xxx.getBytes)
2. 需要使用mybatis-plus的查询功能,我们需要写的sql语句是:
select * from employee where username = username;
那么就要完成的步骤是:
        1) 新建一个查询对象:LambdaQueryWrapper<Employee>
        2) 封装条件表达式:用 .eq 表示校验查询到数据库此列的值与令一个值是否一致
        3) 通过service对象执行查询语句,将条件放入

6. 注意点

我们静态是否能访问到呢?答案是不可以的。因为spring boot默认就配置了 /static/** 映射,我们的映射文件没有在static包下,解决方法①:将所有文件放到static里;这说一下解决方法②:
添加配置文件WebMVCConfig,然后在添加资源映射:

在reggie包下新建config.WebMVCConfig类

@Slf4j //支持书写日志
@Configuration //配置类必写
public class WebMvcConfig extends WebMvcConfigurationSupport {

    //静态资源访问不到,需要资源映射
    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        log.info("正在进行静态资源文件映射");
        registry.addResourceHandler("/backend/**")
                .addResourceLocations("classpath:/backend/"); // classpath表示resource目录
        registry.addResourceHandler("/front/**")
                .addResourceLocations("classpath:/front/");
    }
}

7. 测试

不再演示测试,输入正确账号与正确密码会进入后台主页面 index.html。

8. 完成退出功能

8.1 分析前端页面代码:

在  index.html  找到退出按钮的执行方法:可以发现执行的logout方法

<img src="images/icons/btn_close@2x.png" class="outLogin" alt="退出" @click="logout" />
logout() { //退出
            logoutApi().then((res)=>{
              if(res.code === 1){
                localStorage.removeItem('userInfo')
                window.location.href = '/backend/page/login/login.html'
              }
            })
          }

还有一个封装的logoutApi

function logoutApi(){
  return $axios({
    'url': '/employee/logout',
    'method': 'post',
  })
}

结论:我们需要将存在session清除了,然会返回一个成功消息,退出成功
但前端并没有显示这个退出成功的书写。

8.2 编写controller层代码:

//退出登录 要删除 session 里员工id
    @PostMapping("/logout")
    public R<String> logout(HttpServletRequest request){
        request.getSession().removeAttribute("employeeId");
        return R.success("退出成功");
    }

Day 02 员工管理的增删改查

1. 完成添加员工功能

1.1 分析前端

我们关联到这儿的一个新增方法,可以看到发送的是json数据(params);post请求
那么我们就需要接受此参数,并将此数据插入数据库。

// 新增---添加员工
function addEmployee (params) {
  return $axios({
    url: '/employee',
    method: 'post',
    data: { ...params }
  })
}

1.2 书写增加员工的方法 

注意:我们要设置一个密码、创造时间、更新时间、更新人id,设置后,利用mybits-plus提供的save方法进行数据插入,将设置值的employee插入数据库表对应的列

//增加员工
    //前端输入信息后肯定是一个;类型为json的Employee数据
    @PostMapping
    public R<String> addEmployee(HttpServletRequest request, @RequestBody Employee employee) {
        //它需要输入一些东西,我们要设置一些东西
        String password = DigestUtils.md5DigestAsHex("123456".getBytes());
        employee.setPassword(password);
        employee.setCreateTime(LocalDateTime.now());//获取当前时间
        employee.setUpdateTime(LocalDateTime.now());
        employee.setCreateUser((Long) request.getSession().getAttribute("employeeId"));
        employee.setUpdateUser((Long) request.getSession().getAttribute("employeeId"));
        employeeService.save(employee);
        return R.success("添加成功");
    }

2. 解决添加员工功能里用户名重复,出现的错误问题

在添加员工中,员工的username字段也就是用户名设置了唯一约束,我们在添加数据时,如果出现添加相同的数据,会爆出错误:

SQLIntegrityConstraintViolationException

 对于这种错误,我们要展示出不同的错误信息,就可以在通用包common下新建ErrorHandler类用来统一处理错误信息。if处的切割是因为错误信息为: Duplicate entry "usenamer" xxx;把key就可以返回成错误信息表示此已存在
ErrorHandler编写如下:

@Slf4j
//都捕获以什么为注释的controller呢?RestController与 Controller
@ControllerAdvice(annotations = {RestController.class, Controller.class})
@ResponseBody // 一会要返回json数据
//前俩合体:@RestControllerAdvice(annotations = {RestController.class, Controller.class})
public class ErrorHandler {

    @ExceptionHandler(SQLIntegrityConstraintViolationException.class)
    public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex){
        log.info(ex.getMessage());
        if(ex.getMessage().contains("Duplicate entry")){//如果含有xxx就会怎么样
            //以空格切割,形成的列表第2索引为重复的key,我们显示出来
            return R.error(ex.getMessage().split(" ")[2] + "已存在");
        }
        return R.error("未知错误");
    }

}

 3. 分页查询

3.1 查看前端发出的axios请求

每次在访问 index.html 也就是主页面,都会进行一次分页查询,可以看到发出的请求路径是:
/xxx/xxx?page=xxx&pageSize=xxx;如果我们通过查询框进行查询,就会发出的请求路径是:
/xxx/xxx?page=xxx&pageSize=xxx&name=xxx;很显然,这里发的数据并不是json。

3.2 分析前端接受的数据

 我们发送的数据前面已经说了;前端接受的是(res.data.records)(res.data.total);可以看出这事myatis-plus里的page类里面的属性,我们需要进行书写。

await getMemberList(params).then(res => {
              if (String(res.code) === '1') {
                this.tableData = res.data.records || []
                this.counts = res.data.total
              }
            }
function getMemberList (params) {
  return $axios({
    url: '/employee/page',
    method: 'get',
    params
  })
}

 这里可能有疑惑,在getMemberList中,传递是params应该是json数据,但为什么发出的请求为携带参数的get请求呢?
其实是因为在一个js文件里写了get请求映射params参数;在request里写了:

// get请求映射params参数
if (config.method === 'get' && config.params) {

        let url = config.url + '?';
等等等等.....

 将get请求的数据,让json数据的值以携带参数的请求方式发送出去,这里了解即可。

3.3 编写分页查询方法

注解已经说明了很多了,不再赘述。

//分页查询
    @GetMapping("/page")
    /*  几个问题
        1. 为什么返回值是 R<Page>:
            前端中,接受值是(res.data.records)(res.data.total),很显然employee没有这两个字
        段。事实上,这两个字段是mybatisPlus提供的一个类Page里的属性,一个为当前页数据列表,一个
        为数据总数,既然前端需要,我们就要把这个数据传递给他。
        2. 为什么参数是这三个
            括号里,是前端给我的数据,我们在利用浏览器查询时,给我发送了get请求,请求路径为:
        xxx/xxx?page=xxx&pageSize=xxx,当我们利用查询功能时又会加上 &name=xxx。那我们应该干
        的就是将page和pageSize作为查询范围,将name作为查询条件
        3. 为什么访问路径是"/page"
            因为前端写的分页查询是:/employee/page
     */
    public R<Page> selectByPage(int page, int pageSize, String name) {

        //先把构造器写出来,将每页显示条数和每页数据量赋值进去,这个是limit
        Page pageCon = new Page(page, pageSize);
        //完成分页和查询数据展示
        LambdaQueryWrapper<Employee> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        lambdaQueryWrapper.like(StringUtils.isNotEmpty(name), Employee::getName, name);
        lambdaQueryWrapper.orderByDesc(Employee::getCreateTime);//排序
        employeeService.page(pageCon, lambdaQueryWrapper);//显然,page第一个参数是sql里面的limit
        //返回查询的数据
        return R.success(pageCon);
        //总结:把limit写出来,把where写出来,直接完成分页查询
    }

 4. 修改状态值与编辑员工功能

4.1 分析修改状态值前端代码

在点击启用/禁用时:

应执行修改操作,前端发送的请求仅是一个 /employee 的一个 put 请求;但可以注意到,他有封装employee中id的值,我们就可以利用id查找后修改此数据的状态值。

enableOrDisableEmployee({ 'id': this.id,
                'status': !this.status ? 1 : 0 }).then(res => {
                console.log('enableOrDisableEmployee',res)
                if (String(res.code) === '1') {
                  this.$message.success('账号状态更改成功!')
                  this.handleQuery()
                }
              }
// 修改---启用禁用接口
function enableOrDisableEmployee (params) {
  return $axios({
    url: '/employee',
    method: 'put',
    data: { ...params }
  })
}

 4.2 编写修改状态值后端代码

具体就是,获取到employee中id值进行查找,mybatis-plus封装了方法,根据id查找,如果设置了其他值,那么其他值就修改成新设置的值。

//修改操作-这是一个通用方法,既完成了启用/禁用,又完成了编辑功能
    @PutMapping
    public R<String> updateById(HttpServletRequest request, @RequestBody Employee employee){
        Long userId = (Long) request.getSession().getAttribute("employeeId");
        employee.setUpdateTime(LocalDateTime.now());//重新设置修改时间
        employee.setUpdateUser(userId);//重新设置修改人id
        //通过设置employee对象,根据id匹配后,其他设置的值会对表进行修改,为null的不修改。
        employeeService.updateById(employee);
        return R.success("修改成功");
    }

4.3 分析编辑员工功能前端代码

在点击编辑时:

大致分析下,如果点击添加员工,会有'add'的值:

<el-button
          type="primary"
          @click="addMemberHandle('add')"
        >
          + 添加员工
        </el-button>

 当不是添加员工时,就走else语句,这个很显然将id发出去,为了回显数据

 // 添加
          addMemberHandle (st) {
            if (st === 'add'){
              window.parent.menuHandle({
                id: '2',
                url: '/backend/page/member/add.html',
                name: '添加员工'
              },true)
            } else {
              window.parent.menuHandle({
                id: '2',
                url: '/backend/page/member/add.html?id='+st,
                name: '修改员工'
              },true)
            }
          }

 即便看不太懂,这就是一个带有id参数的一个请求路径;再看进入这个add页面后:

async init () {
            queryEmployeeById(this.id).then(res => {
              console.log(res)
              if (String(res.code) === '1') {
                console.log(res.data)
                this.ruleForm = res.data
                this.ruleForm.sex = res.data.sex === '0' ? '女' : '男'
                // this.ruleForm.password = ''
              } else {
                this.$message.error(res.msg || '操作失败')
              }
            })
          }

  这是一个初始化进入就要执行的,很显然是一个回显数据,get请求带id参数

// 修改页面反查详情接口
function queryEmployeeById (id) {
  return $axios({
    url: `/employee/${id}`,
    method: 'get'
  })
}

4.4 编辑编辑员工功能后端代码

首先就是回显功能,在前端调用了钩子函数,把id值传给后端,要求就是按照id查数据,查到此数据再返回给前端,所以我们这样书写:

//根据id查数据
    //在点击编辑按钮时发出了一个路径:/employee/{id}
    //其实时根据id查询的数据,为了回显数据
    @GetMapping("/{id}")
    public R<Employee> selectById(@PathVariable Long id){
        Employee employeeServiceById = employeeService.getById(id);
        if(employeeServiceById != null){
            return R.success(employeeServiceById);
        }
        return R.error("没查询到");
    }

还有编辑功能的修改,很显然,我们可以使用之前写的状态修改功能里的代码,因为他传递的数据他传递来的employee里面设置了值,我们调用mybatis-plus里的方法,直接就可以修改了。所以这里就不写了。

4.5 解决无法完成的问题

我们写了代码,我们点击修改后,居然发现我们无法修改,也没有报错,但我现在说一个知识点:
JS里无法识别17位及以上的数值,而我们的id位Long类型,并且18位,JS就给我们四舍五入了,导致于id错误,无法查到数据,所以只能转换为字符串。这时,我们就需要书写一个类,我们在通用包common下创建JacksonObjectMapper类,我们直接使用。
作用是:将Long直接变为String方便识别。

public class JacksonObjectMapper extends ObjectMapper {

    public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
    public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
    public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";

    public JacksonObjectMapper() {
        super();
        //收到未知属性时不报异常
        this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);

        //反序列化时,属性不存在的兼容处理
        this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
        SimpleModule simpleModule = new SimpleModule()
                .addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
                .addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
                .addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))

                .addSerializer(BigInteger.class, ToStringSerializer.instance)
                .addSerializer(Long.class, ToStringSerializer.instance)//将long数据转换为String
                //将下面时间格式转换为对应的格式;如:yyyy-MM-dd
                .addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
                .addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
                .addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));

        //注册功能模块 例如,可以添加自定义序列化器和反序列化器
        this.registerModule(simpleModule);
    }
}

 Day 03 字段填充与菜品管理

1. 字段的自动填充

总有些字段,比如修改时间,我们进行修改操作时,总要重新设置它的值,是一种繁琐的工作。我们就可以利用mybatis-plus提供的字段自动填充功能。

1.1 对实体类添加注解

第一步要在需要自动填充的的属性上添加注解,这样才能被识别到。

//这些值我们每次修改或插入操作都要设置一下,那么就可以使用到mybatis-plus里的功能
        @TableField(fill = FieldFill.INSERT)//插入操作时字段进行自动填充
        private LocalDateTime createTime;
        @TableField(fill = FieldFill.INSERT_UPDATE)//字段在进行插入和更新时进行自动填充
        private LocalDateTime updateTime;
        @TableField(fill = FieldFill.INSERT)//插入操作时字段进行自动填充
        private Long createUser;
        @TableField(fill = FieldFill.INSERT_UPDATE)//字段在进行插入和更新时进行自动填充
        private Long updateUser;

1.2 增加一个元数据处理器

作用是用来书写每次进行insert或update时都需进行的操作。需要实现接口MetaObjectHandler。注意:要添加@Component注解让spring识别到。

我们设定时间,在进行insert或update操作时都这个时间修改了。但我们发现一个问题,我们如何获取修改修改人的id呢,他保存在session里,而我们在这个类里,无法获取session值。这个时候我们就需要用到1.3的知识点。

在common公共包下新建MyMetaObjectHandler类并实现MetaObjectHandler接口。

//自定义的元数据处理器
//用来每次进行insert或update时都需进行的操作
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
    @Override//插入时,要修改创建时间和修改的时间已经那两个值
    public void insertFill(MetaObject metaObject) {
        metaObject.setValue("createTime", LocalDateTime.now());//字段名
        metaObject.setValue("updateTime", LocalDateTime.now());//字段名
    }

    @Override//修改时,只需要更改修改时间和修改人
    public void updateFill(MetaObject metaObject) {
        metaObject.setValue("updateTime", LocalDateTime.now());//字段名
    }
}

1.3 同一线程中的公共数据

当发出一次请求时(比如点击保存),这次请求到修改数据库、到跳转主页面,这一系列完成的操作其实用的是同一个线程(只有再有新的请求时,才会再开一个新的线程)。那我们就可以利用这同一个线程,在本线程中共享数据。我们可以在过滤器中,因为我们每次访问别的页面都会经过过滤器判断是否为登录状态,那么就可以在本次线程中设置一个id值,保存起来,从而可以在元数据处理器里得到此id。

在common公共包下新建工具类BaseContextUtils,通过设置ThreadLocal的Long类型对象,写set和get方法用来设置或获取此个id值。

public class BaseContextUtils {

    private static ThreadLocal<Long> threadLocal = new ThreadLocal<>();

    public static void setCurrentId(Long id){
        threadLocal.set(id);
    }

    public static Long getCurrentId(){
        return threadLocal.get();
    }

}

2. 菜品分类的新增与分页查询

2.1 新增菜品分类与套餐分类

查看点击新增时发送的请求:一个POST请求,路径为"/category",发送了json格式的数据,有name、type、sort,其中type已经在前端写好,添加菜品分类就是1;添加套餐分类就是2。

将category实体类、service层、controller层、mapper层按照employee依次写好。
在controller下新建CategoryController,并书写新增方法。因为无论是菜品分类还是套餐分类都是保存在category表下,只是类型不一样,所以写一个就可以了。

//添加分类
    @PostMapping
    public R<String> save(@RequestBody Category category){
        service.save(category);
        return R.success("添加成功");
    }

 2.2 菜品分类的分页查询

对于这个分页查询没有什么新的道道,这里只是加了排序功能,按照sort进行了排序。

 //分页查询
    @GetMapping("/page")
    public R<Page<Category>> selectByPage(int page, int pageSize){
        //构造器
        Page<Category> pageCon = new Page<Category>(page, pageSize);
        //按照排序排序
        LambdaQueryWrapper<Category> lambdaQueryWrapper = new LambdaQueryWrapper<Category>();
        lambdaQueryWrapper.orderByAsc(Category::getSort);
        service.page(pageCon, lambdaQueryWrapper);
        return R.success(pageCon);
    }

3. 检查后删除与修改

3.1 检查删除

商品表(dish)负责将各个菜品进行展示,套餐表(setmeal)负责将各个套餐规划成套餐A、B、C售卖,他俩都归属于分类表(category)。点击删除,可以删除这个分类,但是如果分类下有东西,是不可以删除的。在分类表中有自己的id,若商品表或套餐表与分类表有关联,那么就不能删除。

我们可以自己在service层中书写一个具有判断能力的删除方法。

void myRemoveById(Long ids);
    @Autowired
    private DishService dishService;

    @Autowired
    private SetmealService setmealService;
    @Override

    public void myRemoveById(Long ids) {

        //查询在Dish和Setmeal表中,是否存在Category中某个分类上

        //先判断Dish表
        LambdaQueryWrapper<Dish> lambdaQueryWrapper1 = new LambdaQueryWrapper<>();
        lambdaQueryWrapper1.eq(Dish::getCategoryId, ids);
        int count1 = dishService.count(lambdaQueryWrapper1);
        if(count1 > 0){
            throw new CustomError("当前分类下有菜品,不能删除");
        }

        //再判断Setmeal表
        LambdaQueryWrapper<Setmeal> lambdaQueryWrapper2 = new LambdaQueryWrapper<>();
        lambdaQueryWrapper2.eq(Setmeal::getCategoryId, ids);
        int count2 = setmealService.count(lambdaQueryWrapper2);

        if(count2 > 0){
            throw new CustomError("当前套餐下有菜品,不能删除");
        }

        super.removeById(ids); //都通过直接调用父类删除
    }

 3.2 自定义的异常

在3.1上方代码有一个自定义的抛出错误;这是为了全局捕获,方便管理,也能给出提示信息。新建自定义的捕获错误类CustomError。

在通用包下新建错误类,参数设定为提示信息即可。

//自定义错误
public class CustomError extends RuntimeException {
    public CustomError(String error){
        super(error);
    }
}

找到捕获全局的错误类,写出遇到此错误时应该干什么。模拟上方捕获sql错误即可

@ExceptionHandler(CustomError.class)
    public R<String> customError(CustomError ex){
        log.info(ex.getMessage());
        return R.error(ex.getMessage());
    }

 3.3 修改功能

这就比较简单,没什么可说的。

//修改功能
    @PutMapping
    public R<String> updateById(@RequestBody Category category){
        service.updateById(category);
        return R.success("修改数据成功");
    }

Day 04 菜品管理的增删改查

1. 新增功能

1.1 文件的上传

        点击新增,先不管一些请求,我们点击图片,上传图片后,会发送 /common/upload 请求,我们要分析它发送来的请求想要得到怎么样的处理。
       我们这里用到了web包里的一个对象:MultipartFile,它会接受到上传的文件,将此文件定义为临时文件,请求结束该文件就会自动清除。
        我们需要完成的:将这个临时文件放到指定位置,将此文件重命名变成唯一的名字发送给前端,前端就能再与后端交互将此图片名字加入到数据库。

路径最好写在配置文件里,在yml文件中写一个图片路径:

reggie:
    imagePath:  D:\img\

 在通用包下新建CommonController通用Servlet用来处理图片上传与下载

@RestController
@RequestMapping("/common")
public class CommonController  {

    @Value("${reggie.imagePath}")
    String imgPath;

    @PostMapping("/upload")
    public R<String> upload(MultipartFile file){//file要写传递来的name里的值
        //file是一个临时文件,我们需要让它转到指定的位置,要不然请求结束此文件消失

        //获取原先的文件名
        String originalFilename = file.getOriginalFilename();
        //获取后缀名 xxx.jpg 中的 .jpg
        String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
        System.out.println(suffix);

        //使用UUID生成唯一的文件名
        String fileName = UUID.randomUUID().toString() + suffix;
        System.out.println(fileName);

        //创建目录,如果没有这个路径(D://img//xxx),我们需要创建这个路径
        //这样以来在配置文件中写什么都会有路径保存
        File dir = new File(imgPath);
        if(!dir.exists()){
            dir.mkdirs();
        }

        try {
            //将文件转到指定位置进行保存
            file.transferTo(new File(imgPath + fileName));
        } catch (IOException e) {
            e.printStackTrace();
        }
        //这个文件名需要保存在菜品那个表里面去的
        //返回给前端,在新增菜品中,前端接受此文件名,点击保存,将图片路径放到数据库
        return R.success(fileName);
    }
}

1.2 文件的下载(回显)

在上传图片后,既发送了上传请求 /common/upload 又发送了一个 /common/download?name=xxx
这是一个回显数据的请求,我们需要在我们下载在文件夹中的图片,通过输入流读取name为xxx的图片,通过输出流直接写到浏览器上。

    //文件下载,回调图片,显示出插入的图片
    //响应前端发出的请求:/common/download?name=${response.data}
    @GetMapping("/download")
    public void download(String name, HttpServletResponse response){
        try {
            //通过输入流读取文件内容
            FileInputStream fileInputStream = new FileInputStream(new File(imgPath + name));
            //通过输出流写会浏览器,展示图片
            ServletOutputStream outputStream = response.getOutputStream();

            //设置格式
            response.setContentType("image/jpeg");

            int len = 0;
            byte[] bytes = new byte[1024];
            while ((len = fileInputStream.read(bytes)) != -1){
                outputStream.write(bytes, 0, len); //向浏览器往回写
                outputStream.flush();
            }

            outputStream.close();
            fileInputStream.close();

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

1.3 新增菜品的钩子函数

         在我们进入新增菜品页面时我们可以发现,发送了一个请求:/category/list?type=xxx
这是为了显示菜品分类的,现在在做菜品管理功能,自然要在category表中查找为菜品分类的名字,类型就是type=1。

    //回显菜品分类
    @GetMapping("/list")
    //传递来的是type,也可以写type,建议直接写这个类
   public R<List<Category>> list(Category category){

        LambdaQueryWrapper<Category> lambdaQueryWrapper = new LambdaQueryWrapper<Category>();
        lambdaQueryWrapper.eq(Category::getType, category.getType());
        lambdaQueryWrapper.orderByAsc(Category::getSort).orderByAsc(Category::getUpdateTime);
        List<Category> list = service.list(lambdaQueryWrapper);
        return R.success(list);
        
    }

 1.4 新增菜品的保存按钮-Dto的应用

         点击保存,我们要完成将这个菜品的一些基本信息保存到表dish,将此dish对应的口味放到表dish_flavor,这需要操作两个表。因此我们最好在service层自定义一个方法。
         这时我们又发现一个问题,前端传递来的是dish吗,我们接受的应该是dish?并不是的,dish里可没有口味表里面的东西,这时强大的dto就要登场,我们需要用它继承于dish,就有了所有的基础属性,我们在dto里还要增加我们需要的东西,就是口味表里的数据字段

新建dto包编写DishDto类

@Data
public class DishDto extends Dish {
    private List<DishFlavor> flavors = new ArrayList<>();
}

这个时候我们接受数据就直接写DishDto
点击保存发送了一个POST请求:/dish  就在DishController里去写

@RestController
@RequestMapping("/dish")
@Slf4j
public class DishController{

    @Autowired
    private DishService dishService;

    @PostMapping
    public R<String> addDish(@RequestBody DishDto dto){
        dishService.saveTwoTables(dto);
        return R.success("保存成功");
    }
}

这个方法是自定义的,我们去service里创建出这个方法。(DishService略)

@Service
public class DishServiceImpl 
extends ServiceImpl<DishMapper, Dish> implements DishService {

    @Autowired
    private DishFlavorService dishFlavorService;

    @Transactional //两次操作,保持事务的一致性
    public void saveTwoTables(DishDto dto) {

        this.save(dto); //存储到商品表

        //因为getFlavors中没有提供商品的id,它只是一个口味表
        //我们需要从dto里获取到id后,在集合中赋值id。
        List<DishFlavor> dishFlavors = dto.getFlavors();
        for (DishFlavor dishFlavor : dishFlavors) {
            dishFlavor.setDishId(dto.getId());
        }
        //存列表的方法
        dishFlavorService.saveBatch(dishFlavors);

    }
}

2. 查询功能

2.1  书写基本的分页查询

@GetMapping("/page")
    public R<Page> pageR(Integer page, Integer pageSize, String name){

        Page<Dish> pageInfo = new Page<Dish>(page, pageSize);
        LambdaQueryWrapper<Dish> lambdaQueryWrapper = new LambdaQueryWrapper<Dish>();
        lambdaQueryWrapper.like(StringUtils.isNotEmpty(name), Dish::getName, name);
        lambdaQueryWrapper.orderByAsc(Dish::getSort).orderByAsc(Dish::getUpdateTime);
        dishService.page(pageInfo, lambdaQueryWrapper);
        return R.success(pageInfo);

    }

2.2  改善分页查询

书写完成之后,我们发现有一列居然没有展示,其实是因为这一列在前端显示的名字叫做categoryName,这个东西在口味表中只有categoryId,并没有名字,我们又要用dto了。在dto类中加入属性categoryName。

@Data
public class DishDto extends Dish {

    private List<DishFlavor> flavors = new ArrayList<>();

    private String categoryName;
}

        我们应该完成什么操作呢?需要先在口味表中获取categoryId,在category表中查询出名字,赋值给DishDto,将DishDto返回给前端。
        知识点:在page类中有个records属性,这个就是要展示的所有数据列表,这里面泛型之前是dish,我们需要将dish升级为dishdto,才会有名字。
        步骤:先用之前方法查询一遍得到一个page<dish>,得到的数据除了records全都拷贝给一个新的page<dishdto>,接下来将之前列表records的所有数据拷贝到一个新的列表records中,并且在新的列表records中将我们通过id查询到名字赋值给categoryName,然后再返回出去。

重新编辑分页查询

@GetMapping("/page")
    public R<Page<DishDto>> pageR(Integer page, Integer pageSize, String name){

        //Page里有个records属性,表示所有的列表数据
        //这个列表数据中没有categoryName属性,所以前端无法展示分类名
        Page<Dish> pageInfo = new Page<Dish>(page, pageSize);
        Page<DishDto> pageInfo2 = new Page<DishDto>(page, pageSize);
        LambdaQueryWrapper<Dish> lambdaQueryWrapper = new LambdaQueryWrapper<Dish>();
        lambdaQueryWrapper.like(StringUtils.isNotEmpty(name), Dish::getName, name);
        lambdaQueryWrapper.orderByAsc(Dish::getSort).orderByAsc(Dish::getUpdateTime);

        //此时page已经执行完毕,我们需要在处理下records
        dishService.page(pageInfo, lambdaQueryWrapper);

        //将pageInfo里的值拷贝到pageInfo2中,除了我们要处理的records
        BeanUtils.copyProperties(pageInfo,pageInfo2,"records");

        //获取里面的records,就循环它用来处理records
        List<Dish> records = pageInfo.getRecords();
        //一个全新的records,处理后将来赋给pageInfo2
        List<DishDto> dishDtoList = new ArrayList<>();

        for (Dish record : records) {
            //查对应id的种类名字
            Long categoryId = record.getCategoryId();
            Category category = categoryService.getById(categoryId);
            if(category == null){ //如果没这个id,代表他没分类,这是不对的数据造成的。
                continue;
            }
            //将categoryName及record里其他值赋给一个新的dto
            DishDto dishDto = new DishDto();
            String categoryName = category.getName();
            BeanUtils.copyProperties(record,dishDto);
            dishDto.setCategoryName(categoryName);
            //向集合中添加此数据
            dishDtoList.add(dishDto);
        }
         //这个集合就是具有之前的数据和categoryName的值
        pageInfo2.setRecords(dishDtoList);
        return R.success(pageInfo2);

3. 修改功能

 3.1 回显数据

在进入修改页面时,会发出GET请求:dish/xxx ,很显然,这是回显数据。
也跟查询差不多,需要查两个表,因此直接写一个方法。根据id先在dish表查询到此个菜品的基本数据,再根据id查询dish_flavor口味表。将这查询到的两个数据封装到dto里传递给前端。

//这是类:DishController
    //回显数据
    @GetMapping("/{id}")
    public R<DishDto> get(@PathVariable Long id){
        DishDto dishDto = dishService.getByIdWithFlavors(id);
        return R.success(dishDto);
    }

//这是接口:DishService
    //写一个方法,根据id查
    DishDto getByIdWithFlavors(Long id);

//这是类:DishServiceImpl
    //直接查询id查不到口味,口味是另一个表
    @Override
    public DishDto getByIdWithFlavors(Long id) {
        Dish dish = this.getById(id);//查询到此个id的餐品
        DishDto dishDto = new DishDto();
        BeanUtils.copyProperties(dish, dishDto);//将其他属性赋值上去
        LambdaQueryWrapper<DishFlavor> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        lambdaQueryWrapper.eq(DishFlavor::getDishId, dish.getId());
        //根据id名查询到口味
        List<DishFlavor> dishFlavors = dishFlavorService.list(lambdaQueryWrapper);
        dishDto.setFlavors(dishFlavors);//设置上去
        return dishDto;
    }

 3.2 修改数据

        在修改菜品时,也跟查询差不多,需要查两个表,需要改两个表,因此直接写一个方法。
具体步骤:通过前端发送的id和一些数据直接修改dish表,通过id再查询dish_flavor口味表,将有此id的口味直接删除,在将新得到的数据插入进去。完成修改操作

//这是类:DishController
//修改数据
    @PutMapping
    public R<String> updateByDish(@RequestBody DishDto dto){
        dishService.updateByIdWithFlavors(dto);
        return R.success("修改成功!");
    }

//这是接口:DishService
//修改口味表与菜品表
    void updateByIdWithFlavors(DishDto dto);

//这是类:DishServiceImpl
public void updateByIdWithFlavors(DishDto dto) {
        //修改dish表 - 直接修改
        this.updateById(dto);
        //清除菜品对应口味
        LambdaQueryWrapper<DishFlavor> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        lambdaQueryWrapper.eq(DishFlavor::getDishId, dto.getId());
        dishFlavorService.remove(lambdaQueryWrapper);
        //依然有这个问题,flavors里没有菜品id;清除后将新口味添加上去
        List<DishFlavor> dishFlavors = dto.getFlavors();
        for (DishFlavor dishFlavor : dishFlavors) {
            dishFlavor.setDishId(dto.getId());
        }
        dishFlavorService.saveBatch(dishFlavors);
    }

Day 05 套餐管理的增删改查与阿里云SMS短信服务

1. 新增套餐功能

1.1 一些数据的回显

 当点击新增套餐时,发送了很多请求。
1)读取菜品表dish的数据 请求路径:/dish/list
2)读取套餐分类表category  请求路径:/category/list

先在dish里去写获取菜品表的数据:

//回显菜品信息
    @GetMapping("/list")
    public R<List<Dish>> list(Dish dish) {
        LambdaQueryWrapper<Dish> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        //这个dish.getCategoryId() != null一定要加,一是代码健壮性,二是后期有用
        lambdaQueryWrapper.eq(dish.getCategoryId() != null, Dish::getCategoryId, dish.getCategoryId());
        //状态为1代表启售
        lambdaQueryWrapper.eq(Dish::getStatus, 1);
        lambdaQueryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
        List<Dish> list = dishService.list(lambdaQueryWrapper);
        return R.success(list);
    }

在去category中获取获取套餐分类

//回显菜品分类
    @GetMapping("/list")
    //传递来的是type,也可以写type,建议直接写这个类
   public R<List<Category>> list(Category category){
        LambdaQueryWrapper<Category> lambdaQueryWrapper = new LambdaQueryWrapper<Category>();
        lambdaQueryWrapper.eq(category.getType() != null, Category::getType, category.getType());
        lambdaQueryWrapper.orderByAsc(Category::getSort).orderByAsc(Category::getUpdateTime);
        List<Category> list = service.list(lambdaQueryWrapper);
        return R.success(list);
    }

1.2 新增数据

在新增数据时,请求时post请求的:/setmeal。我们需要插入两个表,菜品与套餐的关系表,和套餐表,那么就可以写一个方法,管理两个表。

//controller
@PostMapping
    public R<String> save(@RequestBody SetmealDto setmealDto){
        setmealService.saveSetmealDish(setmealDto);
        return R.success("新增成功");
    }


//service
//存储至两个表
    void saveSetmealDish(SetmealDto setmealDto);

//serviceImpl
 @Override
    public void saveSetmealDish(SetmealDto setmealDto) {
        //插入一些基础数据
        this.save(setmealDto);
        List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();
        for (SetmealDish setmealDish : setmealDishes) {
            setmealDish.setSetmealId(setmealDto.getId());
        }
        //还要插入
        setmealDishService.saveBatch(setmealDishes);
    }

2. 查询套餐功能

 跟菜品管理一样,需要查到套餐分类。就要返回一个SetmealDto。详细看注释

@GetMapping("/page")
    public R<Page> select(int page, int pageSize, String name){

        //将原先数据先进行整理获取
        Page<Setmeal> setmealPage = new Page<>(page, pageSize);
        LambdaQueryWrapper<Setmeal> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        lambdaQueryWrapper.eq(name != null, Setmeal::getName, name);
        setmealService.page(setmealPage, lambdaQueryWrapper);
        //创建新的Page,将原先数据除了records都拷进来
        Page<SetmealDto> setmealDtoPage = new Page<>(page, pageSize);
        BeanUtils.copyProperties(setmealPage, setmealDtoPage, "records");
        //获取原先的records,创建新的records
        List<Setmeal> setmealRecords = setmealPage.getRecords();
        List<SetmealDto> setmealDtoRecords = new ArrayList<>();
        //循环获取原先records的所有值
        for (Setmeal setmealRecord : setmealRecords) {
            //创建新的records下的数据
            SetmealDto setmealDto = new SetmealDto();
            BeanUtils.copyProperties(setmealRecord, setmealDto);
            //在分类表中查询此分类
            Category category = categoryService.getById(setmealRecord.getCategoryId());
            setmealDto.setCategoryName(category.getName());
            //将新的records附上新的数据
            setmealDtoRecords.add(setmealDto);
        }
        //将新的集合在设置上去
        setmealDtoPage.setRecords(setmealDtoRecords);
        //返回这个Page
        return R.success(setmealDtoPage);
    }

3. 删除套餐功能,附带批量删除

 删除只需要注意,我们需要删除两个表中内容。跟新增是一个道理。

//controller
    @DeleteMapping
    public R<String> delete(@RequestParam List<Long> ids){

        setmealService.deleteWithSetmealDish(ids);

        return R.success("删除成功");
    }

//service
    //删除两个表Setmeal与SetmealDish
    void deleteWithSetmealDish(List<Long> ids);

//serviceImpl
    @Override
    @Transactional
    public void deleteWithSetmealDish(List<Long> ids) {

        //判断是否为启售状态
        LambdaQueryWrapper<Setmeal> setmealLambdaQueryWrapper = new LambdaQueryWrapper<>();
        //要删除的这些id们,只会检测到你们
        setmealLambdaQueryWrapper.in(Setmeal::getId, ids);//类中有ids的值的
        setmealLambdaQueryWrapper.eq(Setmeal::getStatus, 1);//状态为1的
        int count = this.count(setmealLambdaQueryWrapper);
        if(count > 0){
            throw new CustomError("此套餐正在售卖中,请先取消");
        }
        //删除套餐表
        this.removeByIds(ids);
        //删除套餐对应菜品表
        LambdaQueryWrapper<SetmealDish> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        lambdaQueryWrapper.in(SetmealDish::getSetmealId, ids);//类中有ids的值的
        setmealDishService.remove(lambdaQueryWrapper);

    }

4.修改套餐功能

 对于修改,我们需要回显数据有套餐的基本数据,还要有套餐中绑定的菜品,还要有套餐属于的总套餐分类(已写好)。点击保存后,我们要提交这些数据,修改以上表。

4.1 回显数据

//controller
    //回显套餐数据
    @GetMapping("/{id}")
    public R<SetmealDto> get(@PathVariable Long id){
        SetmealDto setmealDto = setmealService.getWithSetmealDish(id);
        return R.success(setmealDto);
    }

//service
    //回显数据,一个是基本数据,一个是套餐中的菜品
    SetmealDto getWithSetmealDish(Long id);

//serviceImpl
 @Override
    public SetmealDto getWithSetmealDish(Long id) {

        Setmeal setmeal = this.getById(id);

        SetmealDto setmealDto = new SetmealDto();
        BeanUtils.copyProperties(setmeal, setmealDto);

        LambdaQueryWrapper<SetmealDish> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        lambdaQueryWrapper.eq(SetmealDish::getSetmealId, id);

        List<SetmealDish> list = setmealDishService.list(lambdaQueryWrapper);
        setmealDto.setSetmealDishes(list);

        return setmealDto;
    }

4.2 提交数据

//controller
    //修改数据
    @PutMapping
    public R<String> put(@RequestBody SetmealDto setmealDto){

        setmealService.updateWithSetmealDish(setmealDto);

        return R.success("修改成功");
    }

//service
    //修改数据
    void updateWithSetmealDish(SetmealDto setmealDto);

//serviceImpl
    @Override
    @Transactional
    public void updateWithSetmealDish(SetmealDto setmealDto) {

        //修改基本数据
        this.updateById(setmealDto);

        //修改菜品表 - 这里用的是先清除,再插入
        Long setmealId = setmealDto.getId();
        LambdaQueryWrapper<SetmealDish> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        lambdaQueryWrapper.eq(SetmealDish::getSetmealId, setmealId);
        List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();
        setmealDishService.remove(lambdaQueryWrapper);//先清除

        //依然有这个问题,flavors里没有菜品id;清除后将新口味添加上去
        for (SetmealDish setmealDish : setmealDishes) {
            setmealDish.setSetmealId(setmealId);
        }

        setmealDishService.saveBatch(setmealDishes);//再插入
    }

5. 阿里云的SMS服务

 步骤:
1. 注册账号;
2. 搜索短信服务,创建签名,需要审核。
3. 创建模板,需要审核。
4. 打开AccessKey管理,创建一个子用户,给予权力:短信服务的两个授权,会给一个AccessKeyId和一个AccessSecret,secret要记住,只会出现这一次。
5. 在idea中使用需要导入依赖。

<dependency>
    <groupId>com.aliyun</groupId>
    <artifactId>aliyun-java-sdk-core</artifactId>
    <version>4.5.20</version>
</dependency>

<dependency>
    <groupId>com.aliyun</groupId>
    <artifactId>aliyun-java-sdk-dysmsapi</artifactId>
    <version>2.1.0</version>
</dependency>

6. 编写工具类,用于发送短信;
7. 编写公共类,用于创建验证码

工具类 - 通过阿里云发送验证码

public class SMSUtils {

	//产品名称:云通信短信API产品,开发者无需替换
	static final String product = "Dysmsapi";
	//产品域名,开发者无需替换
	static final String domain = "dysmsapi.aliyuncs.com";

	// TODO 此处需要替换成开发者自己的AK(在阿里云访问控制台寻找)
	static final String accessKeyId = "************************";
	static final String accessKeySecret = "************************";

	public static SendSmsResponse sendSms(String phone, String code, String signName, String templateCode ) throws ClientException {

		//可自助调整超时时间
		System.setProperty("sun.net.client.defaultConnectTimeout", "10000");
		System.setProperty("sun.net.client.defaultReadTimeout", "10000");

		//初始化acsClient,暂不支持region化
		IClientProfile profile = DefaultProfile.getProfile("cn-hangzhou", accessKeyId, accessKeySecret);
		DefaultProfile.addEndpoint("cn-hangzhou", "cn-hangzhou", product, domain);
		IAcsClient acsClient = new DefaultAcsClient(profile);

		//组装请求对象-具体描述见控制台-文档部分内容
		SendSmsRequest request = new SendSmsRequest();
		//必填:待发送手机号
		request.setPhoneNumbers(phone);
		//必填:短信签名-可在短信控制台中找到
		request.setSignName(signName);
		//必填:短信模板-可在短信控制台中找到
		request.setTemplateCode(templateCode);
		//可选:模板中的变量替换JSON串,如模板内容为"亲爱的${name},您的验证码为${code}"时,此处的值为
		request.setTemplateParam("{\"code\":\""+code+"\"}");

		//选填-上行短信扩展码(无特殊需求用户请忽略此字段)
		//request.setSmsUpExtendCode("90997");

		//可选:outId为提供给业务方扩展字段,最终在短信回执消息中将此值带回给调用者
		//request.setOutId("yourOutId");

		//hint 此处可能会抛出异常,注意catch
		SendSmsResponse sendSmsResponse = acsClient.getAcsResponse(request);

		return sendSmsResponse;
	}

}

 通用类 - 发送指定位数的验证码

public class ValidateCodeUtils {
    /**
     * 随机生成验证码
     * @param length 长度为4位或者6位
     * @return
     */
    public static Integer generateValidateCode(int length){
        Integer code =null;
        if(length == 4){
            code = new Random().nextInt(9999);//生成随机数,最大为9999
            if(code < 1000){
                code = code + 1000;//保证随机数为4位数字
            }
        }else if(length == 6){
            code = new Random().nextInt(999999);//生成随机数,最大为999999
            if(code < 100000){
                code = code + 100000;//保证随机数为6位数字
            }
        }else{
            throw new RuntimeException("只能生成4位或6位数字验证码");
        }
        return code;
    }

    /**
     * 随机生成指定长度字符串验证码
     * @param length 长度
     * @return
     */
    public static String generateValidateCode4String(int length){
        Random rdm = new Random();
        String hash1 = Integer.toHexString(rdm.nextInt());
        String capstr = hash1.substring(0, length);
        return capstr;
    }
}

Day 06 用户端的开发

1. 完成登录注册-通过手机号

在输入验证码,点击获取验证码时,会发送一个 user/sendMsg 请求,我们需要在userController里去编写代码,通过调用阿里云SMS服务发送验证码。

    @Autowired
    private UserService userService;

    @PostMapping("/sendMsg")
    public R<String> senMsg(@RequestBody User user, HttpSession session) {

        //获取手机号
        String phone = user.getPhone();
        //验证码
        if(StringUtils.isNotEmpty(phone)){//不为空
            //生成
            String code = ValidateCodeUtils.generateValidateCode(4).toString();
            session.setAttribute("code", code);
            log.info("code为:" + code);
            
            SendSmsResponse sendSms = null;
            try {
                sendSms = SMSUtils.sendSms(phone,code,"register", "SMS_460975275");
            } catch (ClientException e) {
                e.printStackTrace();
            }

            if(sendSms.getCode().equals("OK")) {
                System.out.println("短信发送成功...."+sendSms.getCode());
                //保存session
                session.setAttribute("phone", phone);
                return R.success("手机验证码发送成功");
            }else {
                System.out.println("短信发送失败...."+sendSms.getCode());
                return R.success("手机验证码发送失败");
            }
        }

        return R.success("手机验证码发送失败");
    }

 输入验证码后,进入主页面前,要对手机号判断,完成登录或注册。在点击登录按钮会发送一个请求:user/login 请求,我们需要完成对验证码的效验、手机是注册还是登录。

    @PostMapping("/login")
    public R<String> login(@RequestBody Map map, HttpSession session){//有phone和code,可以直接搞成map

        //获取手机号,数据库有就验证验证码,没有就创建
        String phone = map.get("phone").toString();
        String code = map.get("code").toString();
        Object codeSession = session.getAttribute("code");

        //验证码比对成功
        if(codeSession != null && codeSession.equals(code)){

            LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
            lambdaQueryWrapper.eq(User::getPhone, phone);
            User user = userService.getOne(lambdaQueryWrapper);
            if(user == null){
                user = new User(); //要new一下,user为空时无法在设置某些值
                user.setPhone(phone);
                userService.save(user);
                session.setAttribute("userId", user.getId());
                return R.success("登录成功");
            }else {
                session.setAttribute("userId", user.getId());
                return R.success("登录成功");
            }
        }else {
            return R.error("验证码或手机号错误");
        }
    }

2. 完成地址管理功能

2.1 新增地址

无难度,从线程中取出用户id值,设置一下,将填的数据放进去就好

@Autowired
    private AddressBookService addressBookService;

    //新增
    @PostMapping
    public R<AddressBook> save(@RequestBody AddressBook addressBook){

        //在同一线程中取出当前操作人的 id
        //操作人的id已经在过滤器中设置好了
        addressBook.setUserId(BaseContextUtils.getCurrentId());
        addressBookService.save(addressBook);

        return R.success(addressBook);
    }

2.2 查询所有地址

无难度,还是从线程中取出id直接查询所有就好。

//查询所有的 - 当前用户的
    @GetMapping("/list")
    public R<List<AddressBook>> list(){

        LambdaQueryWrapper<AddressBook> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        lambdaQueryWrapper.eq(AddressBook::getUserId, BaseContextUtils.getCurrentId());
        List<AddressBook> addressBooks = addressBookService.list(lambdaQueryWrapper);

        return R.success(addressBooks);
    }

2.3 设置默认地址

无难度,就是需要先将所有的默认设为0,这个时候用到了LambdaUpdateWrapper中set方法。

@PutMapping("/default")
    public R<AddressBook> defaultR(@RequestBody AddressBook addressBook){

        //将所有的设置为0
        LambdaUpdateWrapper<AddressBook> lambdaUpdateWrapper = new LambdaUpdateWrapper<>();
        lambdaUpdateWrapper.eq(AddressBook::getUserId, BaseContextUtils.getCurrentId());
        lambdaUpdateWrapper.set(AddressBook::getIsDefault, 0);
        addressBookService.update(lambdaUpdateWrapper);

        //通过id查询到是哪个人的地址薄
        //将此地址薄中改为1,为默认地址
        Long addressBookId = addressBook.getId();
        LambdaQueryWrapper<AddressBook> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        lambdaQueryWrapper.eq(AddressBook::getId, addressBookId);

        addressBook.setIsDefault(1);
        addressBookService.update(addressBook, lambdaQueryWrapper);

        //返回给前端
        return R.success(addressBook);
    }

2.4 修改某个地址附带回显数据

无难度。

    //回显数据
    @GetMapping("/{id}")
    public R<AddressBook> get(@PathVariable Long id){
        LambdaQueryWrapper<AddressBook> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        lambdaQueryWrapper.eq(AddressBook::getId, id);
        AddressBook addressBookServiceOne = addressBookService.getOne(lambdaQueryWrapper);
        return R.success(addressBookServiceOne);
    }

    //修改
    @PutMapping
    public R<String> put(@RequestBody AddressBook addressBook){

        addressBookService.updateById(addressBook);
        return R.success("修改成功");
    }

2.5 删除某个地址

    //删除
    @DeleteMapping
    public R<String> delete(Long ids){

        addressBookService.removeById(ids);
        return R.success("删除成功");
    }

 3. 完成主页面

3.1 主页面显示层代码

主页面其实发送的是 category/list ,我们早已经写好了,为什么没有显示呢,同时也发送了一个查询所有的购物车请求,导致错误,从而没有显示,我们为了显示出来,可以进行修改一些内容。修改前端的js文件的请求路径,让它先不访问购物车,将json数据。

在主页面上,找到钩子函数,看看请求的url路径。

initData(){
  Promise.all([categoryListApi(),cartListApi({})]).then(res=>{ 

 里面就有访问购物车的,我们点击查看下。
将上方的注释打开,直接写出一个json文件,让它不出错

//获取购物车内商品的集合
function cartListApi(data) {
    return $axios({
        //'url': '/shoppingCart/list',
        'url': '/front/cartData.json',
        'method': 'get',
        params:{...data}
    })
}

 json文件:
{"code":1,"msg":null,"data":[],"map":{}}

接下来点击套餐时,依然不显示,是因为我们当时写的时候返回的不是dto数据。

重新修改dish里的list路径代码,将dish更正为dishDto
步骤依然是在dishDto里加上口味。

    //回显菜品信息
    //在管理层中不需要显示口味,但是用户层需要展示,变成DishDto
    @GetMapping("/list")
    public R<List<DishDto>> list(Dish dish){
        LambdaQueryWrapper<Dish> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        lambdaQueryWrapper.eq(dish.getCategoryId() != null, Dish::getCategoryId, dish.getCategoryId());
        //状态为1代表启售
        lambdaQueryWrapper.eq(Dish::getStatus, 1);
        lambdaQueryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
        List<Dish> list = dishService.list(lambdaQueryWrapper);

        List<DishDto> dishDtoList = new ArrayList<>();
        for (Dish dish1 : list) {
            DishDto dishDto = new DishDto();
            BeanUtils.copyProperties(dish1, dishDto);
            //加上口味
            LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
            queryWrapper.eq(DishFlavor::getDishId, dish1.getId());
            List<DishFlavor> list1 = dishFlavorService.list(queryWrapper);
            dishDto.setFlavors(list1);
            dishDtoList.add(dishDto);
        }
        return R.success(dishDtoList);
    }

 4. 完成加入购物车功能

需要加入类shoppingCar,只需要写一个增加方法,一个查询所有的方法

4.1 增加方法的路径

 我们需要完成的是,将指定用户的购物车增加一些东西,可以加一个判断,看看传递来的ShoppingCart 中是套餐还是菜品,从而设置不同的条件语句,如果购物车中有一个一样的数据,我们就要在数量上加一,而不是再增加一个一样的。

    @Autowired
    private ShoppingCartService shoppingCartService;

    @PostMapping("/add")
    @Transactional
    public R<ShoppingCart> add(@RequestBody ShoppingCart shoppingCart){

        //将操作人id存上
        Long currentId = BaseContextUtils.getCurrentId();
        LocalDateTime now = LocalDateTime.now();

        Long dishId = shoppingCart.getDishId();

        shoppingCart.setUserId(currentId);
        shoppingCart.setCreateTime(now);

        LambdaQueryWrapper<ShoppingCart> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        lambdaQueryWrapper.eq(ShoppingCart::getUserId, currentId);

        if(dishId != null){ //菜品
            lambdaQueryWrapper.eq(ShoppingCart::getDishId, shoppingCart.getDishId());
        }else { //套餐
            lambdaQueryWrapper.eq(ShoppingCart::getSetmealId, shoppingCart.getSetmealId());
        }

        ShoppingCart shoppingCart1 = shoppingCartService.getOne(lambdaQueryWrapper);

        if(shoppingCart1 != null){ //已经有一样的了
            //先获取多少个
            Integer number = shoppingCart1.getNumber();
            shoppingCart1.setNumber(number + 1);
            shoppingCartService.updateById(shoppingCart1);
        }else {
            shoppingCart.setNumber(1);
            shoppingCartService.save(shoppingCart);
            shoppingCart1 = shoppingCart;
        }

        return R.success(shoppingCart1);

    }

 4.2 查询所有的路径

我们设置好购物车类后,就可以在主页面显示了,将之前修改的url路径还原到最初状态。然后再编辑方法。

    @GetMapping("/list")
    public R<List<ShoppingCart>> list(){

        LambdaQueryWrapper<ShoppingCart> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        lambdaQueryWrapper.eq(ShoppingCart::getUserId, BaseContextUtils.getCurrentId());
        lambdaQueryWrapper.orderByDesc(ShoppingCart::getCreateTime);

        List<ShoppingCart> list = shoppingCartService.list(lambdaQueryWrapper);

        return R.success(list);
    }

 5. 结算功能

 点击结算,会查询一个默认的地址,也会再次查询所有购物车,点击支付后会生成订单。

 查询默认的地址:

     @GetMapping("/default")
    public R<AddressBook> defaultR2(){

        //将所有的设置为0
        LambdaQueryWrapper<AddressBook> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        lambdaQueryWrapper.eq(AddressBook::getIsDefault, 1);
        lambdaQueryWrapper.eq(AddressBook::getUserId, BaseContextUtils.getCurrentId());
        AddressBook addressBook = addressBookService.getOne(lambdaQueryWrapper);

        //返回给前端
        return R.success(addressBook);
    }

 生成订单:生成订单,需要order表插入很多数据,就不一一赘述了。

//controller
    @Autowired
    private OrdersService orderService;

    @PostMapping("/submit")
    public R<String> submit(@RequestBody Orders orders){
        orderService.submit(orders);
        return R.success("下单成功");
    }

//service
    //生成订单
    void submit(Orders orders);

//serviceImpl
    @Autowired
    private ShoppingCartService shoppingCartService;

    @Autowired
    private UserService userService;

    @Autowired
    private AddressBookService addressBookService;

    @Autowired
    private OrderDetailService orderDetailService;

    /**
     * 用户下单
     * @param orders
     */
    @Transactional
    public void submit(Orders orders) {
        //获得当前用户id
        Long userId = BaseContextUtils.getCurrentId();

        //查询当前用户的购物车数据
        LambdaQueryWrapper<ShoppingCart> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(ShoppingCart::getUserId,userId);
        List<ShoppingCart> shoppingCarts = shoppingCartService.list(wrapper);

        if(shoppingCarts == null || shoppingCarts.size() == 0){
            throw new CustomError("购物车为空,不能下单");
        }

        //查询用户数据
        User user = userService.getById(userId);

        //查询地址数据
        Long addressBookId = orders.getAddressBookId();
        AddressBook addressBook = addressBookService.getById(addressBookId);
        if(addressBook == null){
            throw new CustomError("用户地址信息有误,不能下单");
        }

        long orderId = IdWorker.getId();//订单号

        //可以保证在多线程及多并发时保证数据计算的正确性
        AtomicInteger amount = new AtomicInteger(0);

        List<OrderDetail> orderDetails = shoppingCarts.stream().map((item) -> {
            OrderDetail orderDetail = new OrderDetail();
            orderDetail.setOrderId(orderId);
            orderDetail.setNumber(item.getNumber());
            orderDetail.setDishFlavor(item.getDishFlavor());
            orderDetail.setDishId(item.getDishId());
            orderDetail.setSetmealId(item.getSetmealId());
            orderDetail.setName(item.getName());
            orderDetail.setImage(item.getImage());
            orderDetail.setAmount(item.getAmount());
            //addAndGet为+= ;multiply为* ;
            amount.addAndGet(item.getAmount().multiply(new BigDecimal(item.getNumber())).intValue());
            return orderDetail;
        }).collect(Collectors.toList());


        orders.setId(orderId);
        orders.setOrderTime(LocalDateTime.now());
        orders.setCheckoutTime(LocalDateTime.now());
        orders.setStatus(2);
        orders.setAmount(new BigDecimal(amount.get()));//总金额
        orders.setUserId(userId);
        orders.setNumber(String.valueOf(orderId));
        orders.setUserName(user.getName());
        orders.setConsignee(addressBook.getConsignee());
        orders.setPhone(addressBook.getPhone());
        orders.setAddress((addressBook.getProvinceName() == null ? "" : addressBook.getProvinceName())
                + (addressBook.getCityName() == null ? "" : addressBook.getCityName())
                + (addressBook.getDistrictName() == null ? "" : addressBook.getDistrictName())
                + (addressBook.getDetail() == null ? "" : addressBook.getDetail()));
        //向订单表插入数据,一条数据
        this.save(orders);

        //向订单明细表插入数据,多条数据
        orderDetailService.saveBatch(orderDetails);

        //清空购物车数据
        shoppingCartService.remove(wrapper);
    }

Day 07 Git的使用

1. git简介

Git是一个分布式版本控制工具,通过git仓库来存储和管理一些源代码文件,git有本地仓库,是开发人员自己电脑的仓库,和远程仓库,是远程服务器上的仓库。 我们要学习一些常用的命令,然后学习如何在idea中使用

我们在gitee官网中新建仓库,编写仓库名称。
下面有几项:
1. 初始化仓库 - 选择Java语言,添加.gitignore模板(.gitignore文件名字不可变,作用是有些文件不需要git仓库管理,这文件就是用来设置不管理那些东西的)
2. 选择开源还是私有,私有的话只能成为了管理成员才能读取仓库。

2. git命令

我们输入git命令时,在对应的文件夹下,输入的git命令是不一样的

2.1 git仓库全局设置、初始化与状态

GIT全局设置
git config --global user.name "XuZhi"设置了全局的用户名XuZhi
git config --global user.email "Zhi@qq.cn"设置了全局的邮箱Zhi@qq.cn
git config --list查看我们全局的设置信息

初始化仓库分为手动创建仓库和克隆仓库
在创建仓库的时候就会自动创建.git的隐藏文件,这个也成为版本库

GIT的初始化-版本库
git init手动初始化创建仓库
git clone https://github.com/randx/six.git克隆指定url的远程仓库

Git在工作区(仓库下的目录)存在两种状态

1. 未跟踪状态untracked - 未被纳入版本控制 - 查看文件状态时会冒红

2. 已跟踪状态tracked - 已被纳入版本控制 - 查看文件状态时会冒绿
        1)未修改状态Unmodified:没有对文件有任何修改
        2)已修改状态Modified:已经对文件有过了修改
        3)已暂存状态Staged:已属于暂存的状态 - 就是已经进行了 add 操作,被纳入了版本控制

2.2 本地仓库常用命令

本地仓库常用命令
git status查看文件的状态,是未跟踪还是已跟踪啊等
git add将文件加入暂存区,也纳入了版本控制,但并没有提交到版本库
git reset将文件取消暂存,也可以切换到指定版本实现切换历史代码
git commit将暂存区文件修改后提交到版本库,将Staged已暂存状态 -> Unmodified未修改状态,如果修改了文件,又会变成Modified已修改状态。添加 -m 可以增加message,就是对他的介绍信息,也可以不加
git log查看日志,通过log可以看到每次commit的历史,不同历史有不一样的版本号,可以通过reset+版本号可以回溯到历史版本

 2.3 远程仓库常用命令

 提示:1. 第一次push要进行身份认证,就是注册gitee时的用户名和密码
           2. 拉取时,只有已经是修改了的、不一样的才能拉取,且已经绑定链接了才能拉取。
           3. 拉取时,如果仓库是手动创建的,且仓库内有文件,这时候会拉取出错,因为两者的历史是不一样的,这时就需要加入参数 --allow-unrelated-histories 允许无关的历史。输入后,进入一个新的对话框,输入 i 进入插入模式,随便输入消息(这只是消息),摁esc退出编辑模式,这时在输入 :wq 表示保存退出,就已经拉取成功了。

远程仓库常用命令
git remote查看远程仓库,查看是否绑定了远程仓库,加上 -v 可以看到url地址
git remote add origin url添加与远程仓库的连接。origin为远程仓库名,url为他的地址。
git clone url可以直接克隆一个仓库,也创造了链接
git push origin master将暂存区推送到远程仓库,origin为远程仓库名,master为它的一个分支,推送之后,远程仓库就会增加这些暂存区的文件
git pull origin master拉取远程仓库文件,origin为远程仓库名,master为它的一个分支

2.4 Git的分支命令

分支很重要,可以让我们从主线中抽取到出来,避免出错时直接破坏主线

GIT分支命令
git branch查看本地仓库分支,加入 -r 查看远程,加入 -a 查看所有分支
git branch test创建分支, test 为创建分支的名称
git checkout test切换分支,切换到 test 分支
git push origin test推送分支,将 test 分支推送到 origin 远程仓库
git merge test合并分支,可以在master分支下输入这个命令,可以将test分支下的文件复制到master分支

 2.5 Git的标签命令

标签类似于分支,但是两个不同的概念,分支是动态的,处于还在开发阶段,我们还需要慢慢增加代码,增加功能。而标签属于发布的节点,将当前的代码的状态进行发布,比如版本 v0.1 ,在接下来就是v0.2等等等。

GIT标签命令
git tag查看现在已有标签
git tag v0.1在本地创建 v0.1标签
git push origin  v0.1将标签推送到远程仓库
git checkout -b test v0.1创建test分支,指向已经创建好的 v0.1标签,并且进入到test分支

3. idea中使用git

3.1 本地初始化仓库使用步骤

1. File - Settings - Version Control - Git 输入git目录下的cmd中的git.exe

2. VCS - Import into Version Control - Create Git Repository 选择当前项目,就会出现.git这个文件夹,这就是本地初始化创建仓库,也是 git init 命令

3.2 获取远程仓库步骤

 更多用的还是这个
1. 我们可以在VSC - Get from Version Control... 里直接输入url即可

2. 我么可以在创建项目时直接选择连接:

 3.3 具体的Git的操作

 1. add加入缓存区。我们对这个用于git的文件进行修改时,比如增加一个类,就会提醒是否加入暂存区,我们就要点击确认,如果点了×,也不要急,右击此类,也可以加入。
提醒:没有加入暂存区的类会冒红,加入后会变绿,提交后会变黑

  2. commit提交到本地仓库。

直接点对勾,或者右击。

  3. 查看日志。

直接点钟表,可以看log,看历史

 4. 添加远程仓库

增加/添加仓库

  5. 推送到远程仓库
对号也可以完成,提交并推送功能。

   6. 拉取远程仓库

直接可以拉取代码

   7. 创建本地/远程分支

在idea的右下角,有着这么一个小按钮
可以切换分支checkout,可以新建分支new branch,推送分支push,合并分支等。

Day 08 Linux的使用

1. Linux常用命令

提示:tab键可以自动补全,两下tab可以跳出提示的补全,clear或ctrl + l可以清平。

1.1 常用命令
ls

当前目录下所有内容,加 -l 可以更详细,加 -a 可以看到隐藏文件,-l 和 -a 可以直接加 -la 或直接写 ll,也可以指定目录参数

pwd查看当前所在目录
cd [目录名]切换目录,目录名为 ~ 进入当前用户home目录,为 .. 是上一层
touch [文件名]如果文件不存在,新建文件
mkdir [目录名]创建目录,通过 -p 参数可以创建多层级的目录
rm [文件名/目录]删除指定文件,加 -f 不在提示直接删除,可以多个参数多个删除,加上 -r 可以一层级一层级选择确认或取消,可以直接 -rf 可以实现递归式的一层一层删除
cat [文件路径]查看指定文件内容
more [文件路径]以分页形式展示指定文件内容,回车向下滚动一行,空格向下滚动一屏,b 返回上一屏, q 或 ctrl + c 退出 more
tail [文件路径]

默认显示最后20行的,加上 -n n为整数,表示显示最后n行。

加上 -f 为动态的展示这个文件,它变化了,这里也会变化 

rmdir [目录]删除空目录,加 -p 可以多级删除,但也得是空的。在目录名后面加上*可以删除所有这个名开头的
cp [起始文件/目录] [终点位置]复制文件或目录,复制目录需要加上 -r 参数,实现改名就在终点位置在加上 /新名字
mv [文件/目录] [文件/目录]可以对目录、文件进行改名、移动。对于文件,写两个文件名直接改名,一个文件一个目录会实现移动;对于目录,写两个目录名,第二个目录如果已存在就是移动否则就是改名
tar [-zcxvf] fileName [files]

打解包与解压缩,-c 打包,-v 过程,-f 指定文件,-z 解压缩,

-x 解包。常用:cvf打包xvf解包zcvf打压缩包zxvf解压缩包,如果要解压到指定目录,需要加上 -C 加上路径

vi/vim

vim是vi的扩展,vim + 文件名,如果不存在就新建。输入后进入的是命令模式,摁[i,a,o]都可以进入插入模式,下方出现insert,在插入模式摁esc进入命令模式。编辑文档后无法保存,只能进入命令模式,在命令模式输入[:,/]可以进入底行模式,/进入的底行模式是对文件内容进行查找,: 可以输入wq(保存退出) q!(不保存退出)

set nu(显示行号);在命令模式shift + g可以直接到最后一行。使用vim需要安装:yum install vim

find [寻找路径] -name ["文件名"]在那个路径里查找名称是以什么文件名的文件,目录也是如此
grep [字符串] [具体文件]指定的一个文件里查找指定字符串

2. Linux中的软件安装

2.1 jdk 8 的安装

FinalShell可以将本地文件上传到Linux系统中,将jdk8压缩包上传后,通过 tar -zxvf解压缩包到一个指定的位置,这里是usr/local,通过vim插入模式修改/etc/profile配置环境变量,文件末尾加入:

JAVA_HOME=/usr/local/jdk1.8.0_171
PATH=$JAVA_HOME/bin:$PATH

想要立即生效,需要对文件进行:source /etc/profile
最后需要检查安装是否成功:java -version

2.2 Tomcat 的安装

 将Tomcat压缩包上传后,通过 tar -zxvf解压缩包到一个指定的位置,这里是usr/local。

2.2.1 检查Tomcat是否出错的两种方式:
1)通过查看启动日志

more /usr/local/apache-tomcat-7.0.57/logs/catalina.out
tail -50 /usr/local/apache-tomcat-7.0.57/logs/catalina.out

 2)查看进程:
ps -ef 可以查看所有当前的进程,我们通常使用管道符 | 配合 grep 一起使用

ps -ef | grep tomcat

 2.2.2 防火墙的应用:
防火墙默认是打开的,导致于我们通过浏览器访问不到,这时我们需要关闭防火墙。为了安全起见,我们通常都是开着防火墙,但访问某些端口时给予通过。

查看防火墙状态:systemctl status firewalld、firewall-cmd --state

暂时关闭防火墙:systemctl stop firewalld

永久关闭防火墙:systemctl disable firewalld

开启防火墙:systemctl start firewalld

开放指定端口:firewall-cmd --zone=public --add-port=8080/tcp --permanent

关闭指定端口:firewall-cmd --zone=public --remove-port=8080/tcp --permanent

立即生效:firewall-cmd --reload

查看开放的端口:firewall-cmd --zone=public --list-ports

 2.3 Mysql 的安装

2.3.1 检测当前系统安装软件
注意:mariadb和mysql会冲突,需要卸载

rpm -qa 查询当前系统安装的所有软件

rpm -qa | grep mysql 查询当前系统安装的 mysql 软件

rpm -qa | grep mariadb 查询当前系统安装的 mariadb 软件

 卸载:rpm -e --nodeps 软件名称

 2.3.2 安装Mysql
只要将包传入后按照往常步骤即可,在完成 -zxvf 后会有几个rpm结尾的文件,因为他们都有着依赖关系,所以我们需要按照步骤一步一步删除:

1. rpm -ivh ...common...

2. rpm -ivh ...libs...

3. rpm -ivh ...devel...

4. rpm -ivh ...libs-compat...

5. rpm -ivh ...client...

6. yum install net-tools

7. rpm -ivh ...server...

  2.3.3 使用Mysql
注意:外部访问需要防火墙开启3306端口或自己设置的MySQL端口

查看mysql服务状态:systemctl status mysqld

启动mysql服务:systemctl start mysqld

设置开机启动mysql服务:systemctl enable mysqld

查看当前启动的服务:netstat -tunlp(一样使用 | grep)

登录mysql数据,查阅临时密码:cat /var/log/mysqld.log | grep password

登录mysql(使用临时密码):mysql -uroot -p

设置密码最低数:set global validate_password_length=4;

设置密码安全等级:set global validate_password_policy=LOW;

设置密码:set password = password('1234');

开启访问权限(一个用户名,一个密码):
grant all on *.* to 'root'@'%' identified by '1234';

flush privileges;

3. Linux中项目的部署

注意:外部访问需要防火墙开启8080端口或自己设置的访问端口

 3.1 手工部署

对于一个Springboot项目,我们需要使用Maven中的package打包,将jar包放入Linux系统中,在Linux中使用执行代码:java -jar xxx.jar ,在外部通过 ip地址+冒号端口号 访问此项目就可以运行项目了(前提有jdk)。

在运行时,我们只要退出了终端,服务器也会挂掉,并且运行时会霸占屏幕无法进行其他操作。这个时候就要用到nohup命令,用于不挂断的运行指令命令。

nohup Command [ Arg ... ] [&]

本案例例子:nohup java -jar xxx.jar &> hello.log &

表示:运行 java -jar xxx.jar 命令,并且将日志生成在 hello.log 日志文件中,且不挂断

说明
nohup:关闭中断,也不会停止运行的命令
Command:要执行的命令
Arg ...:一些参数
&:让命令在后台运行,不在霸屏
&>:表示输出在那里

这个时候我们想要停止,只能利用 kill 进程来结束

查找进程 java -jar 的信息:ps -ef | grep 'java -jar'

结束与之对应的进程码:kill -9 进程码

 3.2 脚本部署

脚本部署时,需要在Linux环境中配置git与maven(git可以直接 yum install git)
注意:都选择安装在 /usr/local/

maven选择一个下载,并且需要配置环境变量
编辑 /etc/profile 文件加入两句:
export MAVEN_HOME=/usr/local/
export PATH=$JAVA_HOME/bin:$MAVEN_HOME/bin:$PATH

编辑 /usr/local/apache-maven-3.5.4/conf/settings.xml

先新建一个仓库 repo 用于Maven仓库存放位置

在 <settings></settings>之中,第一行加入:

<localRepository>/usr/local/repo</localRepository>
表示Maven仓库存放位置

 使用:mvn -version 查看Maven安装情况

 此时,git 与 maven 的工作已经完成

我们需要搞一个sh文件,用于执行shell脚本
注意:执行文件时,需要有执行文件的权限,我们可以用chmod管理权限:
比如:通过 chmod 777 bootStart.sh 就是设置了三个用户(文件所有者,同组用户、其他用户)都是读 + 写 + 执行

权限对应的参数
0---
1只执行--x
2只写-w-
3写 + 执行-wx
4只读r--
5读 + 执行r-x
6读 + 写rw-
7读 + 写 + 执行rwx

sh文件编辑如下:

#!/bin/sh
echo =================================
echo  自动化部署脚本启动
echo =================================

echo 停止原来运行中的工程
APP_NAME=linux-project #查找以此为命名的进程id号,前面不能有空格

tpid=`ps -ef|grep $APP_NAME|grep -v grep|grep -v kill|awk '{print $2}'`
if [ ${tpid} ]; then
    echo 'Stop Process...'
    kill -15 $tpid
fi
sleep 2
tpid=`ps -ef|grep $APP_NAME|grep -v grep|grep -v kill|awk '{print $2}'`
if [ ${tpid} ]; then
    echo 'Kill Process!'
    kill -9 $tpid
else
    echo 'Stop Success!'
fi

echo 准备从Git仓库拉取最新代码
cd /usr/local/hello-world #这里我们克隆来的仓库所在位置

echo 开始从Git仓库拉取最新代码
git pull
echo 代码拉取完成

echo 开始打包
output=`mvn clean package -Dmaven.test.skip=true`

cd target #在克隆的仓库中要有target包

echo 启动项目
nohup java -jar linux-project-1.0-SNAPSHOT.jar &> linuxTest.log & #书写响应的jar包
echo 项目启动完成

 我们在充分了解这个sh文件后,直接执行,可以自动的将仓库里更新的东西实现再拉取。

 3.3 设置静态IP

在虚拟机选项卡编辑下的虚拟网络编辑器查看为本机id那个虚拟机,查看子网IP的三位。

修改文件内容如下:
/etc/sysconfig/network-scripts/ifcfg-ens33

注意:最后一个文件每个人都不一样,需要核证自己的那个

BOOTPROTO="static"       #使用静态IP地址,默认为dhcp
#设置的静态IP地址。这里前三位要与刚刚查到的子网IP一致,第四位随便写

BOOTPROTO="static"       #使用静态IP地址,默认为dhcp

#设置的静态IP地址。这里前三位要与刚刚查到的子网IP一致,第四位随便写
IPADDR="192.168.204.100"
NETMASK="255.255.255.0"  #子网掩码
GATEWAY="192.168.204.2"  #网关地址
DNS1="192.168.204.2"     #DNS服务器

 关于设置静态IP无网络问题:
1. 在设置IPADDR时前三位保持一致与子网IP
2. 在设置GATEWAY时要与虚拟机中NAT设置里的网关IP保持一致
3. DNS1并没有什么具体要求

 注意:最后需要重新刷新网络:
systemctl restart network

此时也会掉线,需要使用新设置的静态ip地址

 Day 09 Redis非关系型数据库

        项目开发中一些特定的数据我们不一定要关系型数据库来存储,使用非关系型数据库反而更方便读取数据,效率高。
        Redis 是一个开源的使用 ANSI C 语言编写、遵守 BSD 协议、支持网络、可基于内存、分布式、可选持久性的键值对(Key-Value)存储数据库,并提供多种语言的 API。

 1. Redis的安装

1.1 Linux中安装

在Linux中安装Redis需要用到c++来编译它。先利用 yum install gcc-c++安装编译器。

yum install gcc-c++

安装过后,需要进入redis目录下输入make命令进行编译

make

 再进入redis下的src目录进行安装

make install

1.2 Windows中安装

Windows中下载zip文件后,直接解压,就可以使用,因为是绿色软件。

2. Redis的配置文件与外部访问

2.1 Redis的配置文件

启动Redis:执行 src 下的redis-server
访问客户端Redis:执行 src 下的 redis-cli

redis的配置文件是redis目录下的 redis.conf 。只需要记住几个常用的就好了

1. daemonize no  默认为no,表示霸屏运行,改为yes为后台运行

2. port 6379 默认的端口号为6379,可以修改端口号

3. #requirepass foobared 默认为不设密码,可以修改foobared实现修改密码

注意:设置密码后,访问客户端时需要密码,我们需要输入:auth [设置的密码]。也可以在访问时加入 -a [设置的密码] 也能实现登录

4. bind 127.0.0.1 默认为打开,指定只能127.0.0.1既本地才能连接,要远程连接将它注释。

 注意:启动服务器时可以指定配置文件,比如在redis目录下:

./src/redis-server ./redis.conf

 2.2 外部访问

要打开防火墙,将bind注释掉。

比如在Windows下:。在进入redis目录中打开windows的cmd输入:

redis-cli.exe -h 192.168.204.100 -p 6379

3. Redis的数据类型与常用命令

 Redis 通常被称为数据结构服务器,因为值(value)可以是字符串(String)、哈希(Hash)、列表(list)、集合(sets)和有序集合(sorted sets)等类型。我们一一解剖。

3.1 字符串 string 操作命令

 注意:都是字符串,设置20也是字符串

set key value设置键与值,key相同会覆盖
get key 获取键的值,key不存在返回nil
setex key seconds value设置过期时间
setnk key value        设置键与值,key已存在不做操作

3.2 哈希 hash 操作命令

 注意:哈希表结构是,一个key一个哈希表,一个哈希表有多个键值对。

hset key field value将哈希表key中字段field设为value
hget key field获取哈希表key中字段field的值
hdel key field删除哈希表key中字段field
hkeys key获取哈希表key的所有字段
hvals key获取哈希表key的所有值
hgetall key获取哈希表key的所有字段与所有值

3.3 列表 list 操作命令

Redis列表list是简单的字符串列表,按照插入顺序排序。他是可以重复的。

lpush key value1 [value2]将一个或多个值插入列表key的头部
lrange key start stop获取列表key指定范围的值,范围0 到 -1
rpop key移除列表key最早插入的数据,也是最后一个元素,索引为-1并返回它的值
llen key获取列表key长度
brpop key1 [key2] timeout移除多个列表keys的最后一个元素,并设置时间,如果有数据直接删除,如果没有元素,会堵塞设置的时间数

3.4 集合 set 操作命令

Redis集合set是String类型的无序集合。算一些交集什么的。

sadd key member1 [mermber2]向集合key添加一个或多个成员
smembers key返回集合key中所有成员
scard key获取集合key中的成员数
sinter key1 [key2]返回给的这些集合keys里所有的交集,返回相同部分
sunion key1 [key2]返回给的这些集合keys里所有的并集,返回所有部分
sdiff key1 [key2]差集,若key1 - key2 - key3,既返回key1中的成员,但不能有与key2和key3里的成员
srem key member1 [member2]移除集合key里的一个或多个成员

3.5 有序集合 sorted set 操作命令

Redis有序集合不同于list,他是每个成员都对应着一个数值,用来排序,从小(0)到大(-1)

zadd key score1 member1 [score2 member2]向集合key增加一个或多个成员,成员都要有自己的数值
zrange key start stop [withscores]获取集合key指定范围的元素,加上withscores表示返回元素的同时也返回他们各自所对应的数值
zincrby key increment member向集合key中某个成员增加指定的数值increment
zrem key member1 [member2]移除集合key中成员们

3.6 通用命令

通用就是针对key使用的,什么类型都可以用

keys pattern查询符合给定模式pattern的key,比如 keys *
exists key检查key是否存在
type key返回key所存储的值(大的值,整个key对应的)的类型
ttl key返回key剩余的生命时间,以秒为单位
del key用于当key存在时,删除key

4. JAVA中操作Redis

Redis的java客户端很多,官方推荐:Jedis、Lettuce、Redisson。
我们既可以在IDEA中通过获取Redis对象,实现连接数据库。但用的最多的还是Spring对Redis客户端的整合,它提供了Spring Data Redis,在Spring Boot项目中还提供了对应的Starter,既spring-boot-starter-data-redis

4.1 IDEA中使用Redis

 需要导入坐标:

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.8.0</version>
</dependency>

直接写一个测试类,用来连接redis数据库 
对于这些方法,与命令一致,不掩饰了,主要学习Spring整合的。

    @Test
    public void jedisTest(){
        //1. 获取连接
        Jedis jedis = new Jedis("127.0.0.1",6379);
        //2. 操作
        jedis.set("name","zhangsan");
        //3. 关闭
        jedis.close();
    }

4.2 Spring整合使用Redis

需要在SpringBoot项目中导入坐标:

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

 接下来将学习,在Springboot中运用刚刚学习的Redis中的命令。

新建Springboot项目将坐标导入,并加入redis的依赖,编写启动类,编写配置文件yml文件如下:

#Redis相关配置
spring:
  redis:
    host: 127.0.0.1
    port: 6379
    database: 0 #默认0数据库
    #password:
    jedis:
      #Redis连接池配置
      pool:
        max-active: 8 #最大连接数
        max-wait: 1ms #连接池最大堵塞等待时间
        max-idle: 4 #连接池中最大空闲连接
        min-idle: 0 #连接池最小的空闲连接

在测试中新建测试类。在测试类中编写,将RedisTemplate注入进来。

@SpringBootTest
@RunWith(SpringRunner.class)
public class SpringDateRedisTest {

    @Autowired
    private RedisTemplate redisTemplate;

}

 Spring Data Redis中提供了一个高度封装的类:RedisTemplate,针对jedis客户端中大量api进行了归类封装,将同一类型操作封装为operation接口,具体分类如下:

ValueOperations简单的 K-V 操作redisTemplate.opsForValue()
SetOperationsset类型数据操作redisTemplate.opsForSet()
ZSetOpationszset类型数据操作redisTemplate.opsForZSet()
HashOpationsmap类型数据操作redisTemplate.*
ListOpationslist类型数据操作redisTemplate.opsForList()

 自定义自己的序列化器,如果使用默认的,key会不准确。在配置类里书写:

/**
 * Redis配置类
 * 我们需要自定义序列化器,要不然设置key时会不一致
 * 其实value要想显示出来,也需要自定义序列化器,但没有必要,因为我们在idea里会自动序列化回来
 */

@Configuration
public class RedisConfig extends CachingConfigurerSupport {

    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {

        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();

        //默认的Key序列化器为:JdkSerializationRedisSerializer
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());

        redisTemplate.setConnectionFactory(connectionFactory);

        return redisTemplate;
    }
}

 接下来,我们将针对五个类型,一个一个进行测试。

String类型:

    @Test
    public void testString(){//String类型
        
        //设置简单的键值对
        redisTemplate.opsForValue().set("city", "shandong");
        
        //设置对应键的值
        String val = (String) redisTemplate.opsForValue().get("city");
        System.out.println(val);
        
        //设置简单的键值对的同时设置存活时长
        redisTemplate.opsForValue().set("life", "life", 10l, TimeUnit.SECONDS);
        
        //设置简单的键值对,key已存在不会进行任何操作
        Boolean bool = redisTemplate.opsForValue().setIfAbsent("city", "shandong");
        System.out.println(bool);
        
    }

hash类型:

    @Test
    //Hash类型
    public void testHash(){

        //获取负责Hash类型的类
        HashOperations hashOperations = redisTemplate.opsForHash();

        //添加
        hashOperations.put("hash", "name", "zhangsan");
        hashOperations.put("hash", "age", "20");
        hashOperations.put("hash", "address", "shandong");

        //取值
        String name = (String) hashOperations.get("hash", "name");
        System.out.println(name);

        //获取所有字段
        Set keys = hashOperations.keys("hash");
        for (Object hash : keys) {
            System.out.println(hash);
        }

        //获取所有值
        List values = hashOperations.values("hash");
        for (Object value : values) {
            System.out.println(value);
        }
    }
List类型
    @Test
    //List类型
    public void testList(){

        //获取负责List类型的类
        ListOperations listOperations = redisTemplate.opsForList();

        //单个添加
        listOperations.leftPush("list", "a");
        //多个添加
        listOperations.leftPushAll("list", "b", "c", "d");

        //取值
        List lists = listOperations.range("list", 0, -1);
        for (Object list : lists) {
            System.out.println(list);
        }

        //获取列表长度
        Long _size = listOperations.size("list");
        int size = _size.intValue();
        for (int i = 0; i < size; i++) {
            //出队列,删除最后一位
            Object rightPop = listOperations.rightPop("list");
            System.out.println(rightPop);
        }
    }

ZSet类型:

    @Test
    //ZSet类型-有序不可重复集合
    public void testZSet() {

        ZSetOperations zSetOperations = redisTemplate.opsForZSet();

        //添加
        zSetOperations.add("Zset", "a", 10.0);
        zSetOperations.add("Zset", "a", 18.0);
        zSetOperations.add("Zset", "b", 12.0);
        zSetOperations.add("Zset", "c", 11.0);
        zSetOperations.add("Zset", "d", 14.0);
        zSetOperations.add("Zset", "e", 13.0);
        zSetOperations.add("Zset", "f", 9.0);

        //取值
        Set<String> zset = zSetOperations.range("Zset", 0, -1);
        for (String set : zset) {
            System.out.println(set);
        }

        System.out.println("==========");

        //修改数值
        zSetOperations.incrementScore("Zset", "f", 15.0);
        //验证
        zset = zSetOperations.range("Zset", 0, -1);
        for (String set : zset) {
            System.out.println(set);
        }

        System.out.println("==========");


        zSetOperations.remove("Zset", "a");
        //验证
        zset = zSetOperations.range("Zset", 0, -1);
        for (String set : zset) {
            System.out.println(set);
        }
    }

通用类型

    @Test
    //通用
    public void testCommon(){

        //获取所有key
        Set<String> keys = redisTemplate.keys("*");
        for (String key : keys) {
            System.out.println(key);
        }

        System.out.println("=========");

        //判断某个key是否存在
        Boolean aBoolean = redisTemplate.hasKey("no");
        System.out.println(aBoolean);

        System.out.println("=========");

        //删除指定key
        redisTemplate.delete("Zset");

        //获取key对应的value数据类型
        DataType dataType = redisTemplate.type("set");
        System.out.println(dataType.name());
    }

 Day 10 缓存优化

1. 使用Git管理与配置

        具体步骤:将Reggie项目并入git管理,新建仓库Reggie,先创建本地仓库,再加入缓存区,最后提交、推从至远程仓库。是在原有的基础上提高性能,创建v1.0分支,再推送一遍。

       导入redis坐标。

<dependency>

        <groupId>org.springframework.boot</groupId>

        <artifactId>spring-boot-starter-data-redis</artifactId>

</dependency>

        配置文件。

#Redis相关配置

spring:

    redis: host: 127.0.0.1

    port: 6379

    database: 0 #默认0数据库

    #password:

        配置类,将key的序列化器写好。

@Configuration
public class RedisConfig extends CachingConfigurerSupport {

    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();

        //默认的Key序列化器为:JdkSerializationRedisSerializer
        redisTemplate.setKeySerializer(new StringRedisSerializer()); // key序列化
        //redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); // value序列化

        redisTemplate.setConnectionFactory(connectionFactory);
        return redisTemplate;
    }
}

 配置完成后,推送至仓库。

2. 用户登录短信优化

         思路:短信需要在五分钟有效,我们之前是放到session里。所以我们要改造UserController的代码。将发送验证码的方法中的验证码存入redis中,有效期五分钟,在登录方法中,如果登录成功,需要将验证码从redis中删除。(代码只展示重要的)

@Autowired
private RedisTemplate redisTemplate;            

//session.setAttribute("code", code);不在用session存储验证码
ValueOperations valueOperations = redisTemplate.opsForValue();
valueOperations.set("code", code, 5L, TimeUnit.MINUTES);

redisTemplate.delete("code"); //删除的

3. 查询菜品的优化

        每次我们查看所有菜品,都会查询一次数据库,频繁的查询会使数据库濒临崩溃,我们需要将一些数据直接存入redis,提高性能。只会在第一次访问查询数据库,第二次就会直接从redis中拿走数据。
        具体思路:在登录成功后,显示各种分类下的菜品、套餐,第一次直接查询数据库,并且把查询到的结果放到redis中,针对于key的名字认定,需要指定一个动态的,能保持唯一的。且要注意一点时,在进行修改、新增操作时,需要将redis中的数据清空或是指定key删除。

修改查看所有菜品的方法,在一开始需要查看redis

        Long categoryId = dish.getCategoryId();
        Integer status = dish.getStatus();
        String key = "dish_" + categoryId + "_" + status;

        ValueOperations valueOperations = redisTemplate.opsForValue();
        dishDtoes = (List<DishDto>) valueOperations.get(key);

        if(dishDtoes != null){
            return R.success(dishDtoes);
        }

 最后是要保存的。

    valueOperations.set(key, dishDtoList, 60, TimeUnit.MINUTES);    //一个小时

在进行保存、修改操作时需要完成清除redis数据。

//删除指定的
String key = "dish_" + dto.getCategoryId() + "_1";
redisTemplate.delete(key);
//直接删除所有
Set keys = redisTemplate.keys("dish_*");
redisTemplate.delete(keys);

4. spring cache框架

使用spring cache框架只需要开启缓存注解功能即可。它本事就依赖于spring boot中的web下。

Spring cache常用注解

@EnableCaching开启缓存注解功能 - 启动类上加
@Cacheable缓存中有数据直接返回缓存数据,无数据将方法只放到缓存中,经常放入查询中
@CachPut将方法的返回值放到缓存中,经常放入新增的方法里。
@CacheEvict将一条或多条数据从缓存中删除,经常放入删除、更新的方法里。

4.1 @Cacheable使用案例 

    @GetMapping("/list")
    //一般都将查询条件定为key,即便只有名字,那也是这样的key:null_张三
    @Cacheable(value = "userCache", key = "#user.id + '_' + #user.name")
    public List<User> list(User user){
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(user.getId() != null, User::getId,user.getId());
        queryWrapper.eq(user.getName() != null,User::getName,user.getName());
        List<User> list = userService.list(queryWrapper);
        return list;
    }
    @GetMapping("/{id}")
    /*
     * 如果我们不希望返回值为null时进行缓存,则使用unless="#result == null",
     * 排除掉返回值为null的结果	
     * 如果我们不希望参数为空的时候进行缓存,则需要使用condition = "#id==null",
     * 这时函数还没执行,排除掉参数为空的情况
    */
    @Cacheable(value = "userCache", key = "#id", unless= "#result == null")
    public User getById(@PathVariable Long id){
        User user = userService.getById(id);
        return user;
    }

4.2 @CachPut使用案例

    @PostMapping
    //将此user的id作为key,user数据放入缓存
    @CachePut(value = "userCache", key = "#user.id")
    public User save(User user){
        userService.save(user);//这里保存后key会获得user的id值
        return user;
    }

4.3 @CacheEvict使用案例

    @DeleteMapping("/{id}")
    //@CacheEvict(value = "userCache" key = "#p0")表示参数第0位
    //@CacheEvict(value = "userCache" key = "#root.arg[0]")表示参数第0位
    @CacheEvict(value = "userCache" key = "#id")
    public void delete(@PathVariable Long id){
        userService.removeById(id);
    }
    @PutMapping
    //可以用result表示返回结果
    @CacheEvict(value = "userCache", key = "#result.id")
    public User update(User user){
        userService.updateById(user);
        return user;
    }

 4.4 套餐优化缓存。

1. 直接改吧,没啥难的。

    @DeleteMapping
    //走着这个方法,直接删除所有的缓存
    @CacheEvict(value = "setmealCache", allEntries = true)
    public R<String> delete(@RequestParam List<Long> ids){
        setmealService.deleteWithSetmealDish(ids);
        return R.success("删除成功");
    }


    @PutMapping
    //走着这个方法,直接删除所有的缓存
    @CacheEvict(value = "setmealCache", allEntries = true)
    public R<String> put(@RequestBody SetmealDto setmealDto){

        setmealService.updateWithSetmealDish(setmealDto);

        return R.success("修改成功");
    }    


    @GetMapping("/list")
    //查看套餐时,将每个不一样的数据都分成不一样的 key
    //注意:返回值是R,R必须要序列化
    @Cacheable(value = "setmealCache", key = "#setmeal.categoryId+ '_'+ #setmeal.status")
    public R<List<Setmeal>> list(Setmeal setmeal){
        LambdaQueryWrapper<Setmeal> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        lambdaQueryWrapper.eq(Setmeal::getCategoryId, setmeal.getCategoryId());
        lambdaQueryWrapper.eq(Setmeal::getStatus, 1);
        List<Setmeal> list = setmealService.list(lambdaQueryWrapper);
        return R.success(list);
    }

Day 11 Mysql主从复制

1. 完成主库和从库的设置

准备两台服务器,这里采用Linux中的mysql服务实现的主从复制。既准备两台虚拟器,一个作为主库,一个作为从库。

1.1 配置主库

第一步:修改Mysql数据库的配置文件 /etc/my.cnf;添加以下信息:

[mysqld]

log-bin=mysql-bin   #【必需】启用二进制日志

server-id=100         #【必需】服务器唯一ID,100并不唯一

第二步:重启Mysql服务

systemctl restart mysqld

第三步:登录Mysql,执行下面sql语句:

grant replication slave on *.* to 'xiaozhi'@'%' identified by '1234';

解释一下:这条语句作用是创建一个用户xiaozhi,密码为1234,并且给xiaoming用户授予replication slave权限。常用于建立复制时所需要用到的用户权限,也就是slave必须被master授权具有该权限的用户,才能通过该用户复制。

第四步:登录mysql,执行下面sql语句:

show master status;

解释一下: 这条语句作用是查看master的状态,注意,此时不要再进行任何操作,否则位置和文件会发生变化。

1.2 从库的配置

第一步:修改Mysql数据库的配置文件 /etc/my.cnf;添加以下信息:

[mysqld]

server-id=101

注意:server-id要和主库不一样。

第二步:重启Mysql服务

systemctl restart mysqld

第三步:登录Mysql,执行下面两个sql语句:

change master to master_host='192.168.204.100',master_user='xiaozhi', master_password='1234',master_log_file='mysql-bin.000002',master_log_pos=154;

 解释一下:这条语句作用是绑定主库,填写主库的ip地址,填写主库创建的角色,主库中通过命令
show master status;获取到的文件名和位置。

 start slave;

这个是启动线程。此时,就已经完成绑定了。

可以输入以下命令来查看从库状态

show slave status;

2. Sharding-jdbc实现读写分离

实现对数据库的读写分离,只需要完成以下步骤即可:

第一步:导入Maven坐标

<dependency>

        <groupId>org.apache.shardingsphere</groupId>

        <artifactId>sharding-jdbc-spring-boot-starter</artifactId>

        <version>4.0.0-RC1</version>

</dependency>

第二步:在配置文件中配置读写分离规则

需要在yml中将 datasource 进行修改,在spring下级进行配置:

spring:
  shardingsphere:
    datasource:
      names:
        master,slave
      # 主数据源
      master:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://192.168.204.100:3306/reggie?characterEncoding=utf-8
        username: root
        password: 1234
      # 从数据源
      slave:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://192.168.204.200:3306/reggie?characterEncoding=utf-8
        username: root
        password: 1234
    masterslave:
      # 读写分离配置
      load-balance-algorithm-type: round_robin # 轮询
      # 最终数据源名称
      name: dataSource
      # 主数据源名称
      master-data-source-name: master
      # 从数据源名称
      slave-data-source-names: slave

第三步:在配置文件中配置允许bean定义覆盖配置项

如果不配置那么 druid 生成的 datasource 会和 Sharding-jdbc 生成的冲突。

spring:
  main:
    allow-bean-definition-overriding: true # 允许bean定义覆盖配置项

3. Nginx服务器

 3.1 Nginx的安装

Linux中下载:在官网中直接下载压缩包,linux联网可以直接wget(wget需要install一下)

1. 安装依赖包:yum -y install pcre-devel zlib-devel openssl openssl-devel

2. 下载Nginx安装包:wget 安装下载连接

3. 解压:tar -zxvf 安装包名

4. 进入此文件夹:cd 安装包文件夹

5. 安装指定的目录:./configure --prefix=/usr/local/nginx

6. 进行编译后安装:make && make install

 安装完成后会出现四个文件:

conf - nginx的配置文件

html - 放一些静态页面

logs - 存放日志文件

sbin - 二进制文件,用于启动、停止Nginx服务

3.2 Nginx的常用命令

在sbin目录下执行命令

./nginx -v查看版本
./nginx -t查看配置文件是否存在语法错误
./nginx直接启动Nginx服务
./nginx -s stop停止Nginx服务
./nginx -s reload修改配置文件后,重新加载一下

我们每次都需要进入sbin目录,可以为nginx配置环境变量,修改 /etc/profile
使用冒号实现追加。

注意:修改 /etc/profile 想要立即生效,需要对文件进行:source /etc/profile

PATH=/usr/local/nginx/sbin:$JAVA_HOME/bin:$PATH

3.3  Nginx的配置文件conf

 配置文件分为三块,分级如下(ⅠⅡⅢ):

Ⅰ-全局块:和Nginx运行相关的全局配置

Ⅰ-events块:和网络连接相关的配置

Ⅰ-http块:代理、缓存、日志记录、虚拟主机配置

        Ⅱ-http全局块

        Ⅱ-Server块

                Ⅲ-Server全局块

                Ⅲ-location块

 查看配置文件 nginx.conf - 删去了带 # 的注释部分

worker_processes  1;     # 全局块,此表示开启1个线程
events {
    worker_connections  1024;      # events块,此表示开启1个线程
}
http {
    include       mime.types;
    default_type  application/octet-stream;
    sendfile        on;
    keepalive_timeout  65;

    server {
        listen       80;      # Server全局块,此表示端口号80
        server_name  localhost;      # 服务器名称

        location / {      # location块,“ / ” 表示截取所有的请求
            root   html;      # 表示默认加载根目录下的html(root就是根目录)
            index  index.html index.htm; # 表示默认访问 index.html 没有就访问 index.htm
        }
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    }

 3.4  Nginx的反向代理

为什么使用反向代理?

首先,我们知道,为了系统的高可用,我们的系统一般会部署多个实例,项目部署于多个服务器上。

但是,这样是存在问题的,我们应该要对用户屏蔽掉这一信息。因为我们是不可能安排好每个用户在特定时刻去访问特定的服务器。这明显是行不通的,首先,用户无法记住这么多的服务器地址,其次,系统管理者难以管理运营。

即无论应用有多少实例,我们只需要访问一个地址就可以得到服务。就需要在客户端与服务端之间加上一层服务器。

反向代理概述

 如何实现反向代理呢?其实添加下面的配置

    server{
        listen       82;
        server_name  localhost;
        location / {

        #反向代理配置,将请求转发到以下,就是web服务器

        #当访问 82 端口时转发到此 ip 为 http://192.168.204.100:8080 的web服务器
            proxy_pass http://192.168.204.100:8080;
        }
    }

  3.5  Nginx的负载均衡

为什么使用负载均衡?

由Nginx的反向代理得到:我们的系统一般会部署多个实例,项目部署于多个服务器上。

而有了多个服务器,当我们访问时,应该去用那个服务器进行处理呢?

此时就需要配置Nginx的负载均衡

 如何实现负载均衡呢?其实添加下面的配置

    upstream server_list{

           # 这个是web服务器的访问路径

            server 192.168.204.100:8080;

            server 192.168.204.100:8081;

    }

    server {
            listen       8080;
            server_name  localhost;
            location / {
                proxy_pass http://server_list;
            }
        }

默认情况下, 我们书写的8080和8081端口会采用轮询,既一次8080下次一就是8081下一次就是8080等等。可以进行修改,比如增加权重:

    upstream server_list{

            server 192.168.204.100:8080 weight=10;

            server 192.168.204.100:8081 weight=5;

    }

 此时访问8080的频率会比8081的频率多二倍。还有一些其他的策略:

ip_hash
每个请求按访问ip的hash值分配,这样每个访问客户端会固定访问一个后端服务器,可以解决会话Session丢失的问题

    upstream server_list{

            ip_hash;

            server 192.168.204.100:8080;

            server 192.168.204.100:8081;

    }

最少连接
web请求会被转发到连接数最少的服务器上

    upstream server_list{

            least_conn;

            server 192.168.204.100:8080;

            server 192.168.204.100:8081;

    }

url_hash
依据url分配方式

fair 
依据响应时间方式

Day 12 前后段分离开发

1. YApi

 1.1 什么是YApi?

YApi 是高效、易用、功能强大的 api 管理平台,旨在为开发、产品、测试人员提供更优雅的接口管理服务。可以帮助开发者轻松创建、发布、维护 API,YApi 还为用户提供了优秀的交互体验,开发人员只需利用平台提供的接口数据写入工具以及简单的点击操作就可以实现接口的管理。

 1.2 用YApi可以做什么?(常用)

后端定义好api结构,然后导入YApi, 在测试里输入样例参数,直接丢给前端即可完全实现前后端分离,有效提高api对接联调速度。

  • 项目集成swagger, 接口添加api注解
  • 导出api-docs.json
  • YApi导入api-docs.json

1.3 YApi的使用

进入项目页,项目页展示了属于该项目的全部接口,并提供项目、接口的全部操作。
通过测试集合可以像 postman 一样发送请求路径到后端。
通过数据管理选项卡实现数据导入和数据导出。

使用最多的就是数据导入功能,通过 swagger 生成的 json 文件实现导入。

2. Swagger

2.1 什么是Swagger?

Swagger 是一个用于生成、描述和调用 RESTful 接口的 Web 服务。通俗的来讲,Swagger 就是将项目中所有(想要暴露的)接口展现在页面上,并且可以进行接口调用和测试的服务。

2.2  Swagger的使用

 使用 Swagger 一般都用框架 knife4j ,对 Swagger 进行了增强,下面介绍操作 knife4j

操作步骤:

1. 导入 Maven 坐标

<dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>knife4j-spring-boot-starter</artifactId>
    <version>3.0.2</version>
</dependency>

 2. 导入 knife4j 相关配置(WebMvcConfig)

@EnableSwagger2
@EnableKnife4j

public class WebMvcConfig extends WebMvcConfigurationSupport {

    @Bean
    public Docket createRestApi() {
        // 文档类型
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.huayu.reggie.controller"))
                .paths(PathSelectors.any())
                .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("瑞吉外卖")
                .version("1.0")
                .description("瑞吉外卖接口文档")
                .build();
    }
}

 3. 设置静态资源映射

不添加的话帮我们生成接口文档 doc.html 则无法显示

    //静态资源访问不到,需要资源映射
    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("doc.html")
                .addResourceLocations("classpath:/META-INF/resources/");
        registry.addResourceHandler("/webjars/**")
                .addResourceLocations("classpath:/META-INF/resources/webjars/");
    }

 4. 在过滤器中定义不需要处理的请求路径

        String[] urls = new String[]{ //定义这些直接放行
                "/doc.html",
                "/webjars/**",
                "/swagger-resources",
                "/v2/api-docs",
        };

 此时,启动项目,可以通过 localhost:8080/doc.html 直接访问到生成的需求文档

localhost:8080/doc.html

在此页面里的选项卡中文档管理-离线文档-OpenAPI可以导出json文件,在YApi中可以导入此文件

2.3  Swagger的常用注解

 通过注解可以让需求文档可读性增加。

注意:只有一个参数要使用@ApiImplicitParam不能使用@ApiImplicitParams

@Api用在请求类中,例如Controller,表示类的说明
@ApiModel用在类上,通常为实体类,表示返回响应数据的信息
@ApiModelProperty用在属性上,描述响应的属性
@ApiOperation用在请求的方法上,说明方法的用途、作用
@ApiImplicitParams用在请求的方法上,表示一组参数说明
@ApiImplicitParam用在@ApiImplicitParams中,指定一个请求参数的各个方面

举例:

@ApiModel("返回结果")
public class R<T> implements Serializable {

    @ApiModelProperty("编码")
    private Integer code; //编码:1成功,0和其它数字为失败

    ......

}
@RestController
@RequestMapping("/addressBook")
@Slf4j
@Api("地址相关接口")
public class AddressBookController {

    @Autowired
    private AddressBookService addressBookService;

    //新增
    @PostMapping
    @ApiOperation("新增地址接口")
    @ApiImplicitParam(name = "addressBook", value = "地址信息")

    public R<AddressBook> save(@RequestBody AddressBook addressBook){

        //在同一线程中取出当前操作人的 id
        //操作人的id已经在过滤器中设置好了
        addressBook.setUserId(BaseContextUtils.getCurrentId());
        addressBookService.save(addressBook);

        return R.success(addressBook);
    }

    //回显数据
    @GetMapping("/{id}")
    @ApiOperation("回显地址信息接口")
    //required 默认false,开启表示不能为空
    @ApiImplicitParams({
            @ApiImplicitParam(name = "id", value = "用户ID", required = true),
            @ApiImplicitParam(name = "test", value = "演示")
    })
    public R<AddressBook> get(@PathVariable Long id, String test){
        LambdaQueryWrapper<AddressBook> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        lambdaQueryWrapper.eq(AddressBook::getId, id);
        AddressBook addressBookServiceOne = addressBookService.getOne(lambdaQueryWrapper);
        return R.success(addressBookServiceOne);
    }

}

3. 项目部署

3.1 项目部署服务器准备

对于项目部署,我们要完成的步骤是:
注意:虚拟机有限,主从库服务器就用反向代理服务器作为主库,Web 服务器作为从库

1. 准备两台Linux虚拟机模拟两个服务器,一个端口号为 100 作为反向代理服务器,一个端口号为 200 作为Web服务器。准备在本机 Window 下作为缓存 Redis 服务器

2. 为反向代理服务器安装以下:

1. Mysql:负责主从复制中的主库

2. Nginx:负责静态页面展示

 3. 为Web 服务器安装以下:

1. Git:负责拉取仓库最新代码

2. Maven:负责项目清理与打包

3. JDK:负责运行 Java 程序

4. Mysql:负责主从复制中的从库

5. Jar:Spring Boot项目打成 jar 包基于内置 Tomcat 运行 - 就是导入 jar 包

  3. 在本机中安装以下:

1. Redis:作为缓存数据,避免反复查询数据库

 3.2 项目部署运行准备

在项目运行时,需要完成以下准备工作:

1. 完成反向代理服务器中相关的 Nginx 配置

2. 将前端打包好的代码文件夹放入 Nginx 中的 html 文件夹内

3. 利用 git 克隆远程代码后,运行脚本文件 sh 文件实现对项目的启动

针对以上问题,解决方案如下:

 1. 完成反向代理服务器中相关的 Nginx 配置

对 Nginx 进行下列修改:

在 server 中加入:

   #反向代理服务器
        location ^~ /api/ {

                rewrite ^/api/(.*)$ /$1 break;
                proxy_pass http://192.168.204.200:8080;


        }

 rewrite:表示重写 url ,因前端每次请求就带有 api 这个路径,而这样访问是无法对后端起作用的,此处做法就是在删除 url 路径里的 “ /api/ ”
举例:^ 表示开始,$ 表示结束。第一个()是$1,第二个()是$2,等等。.* 表示到最后所有

^/(abcd)e/(fg)/hijk$ lm$1

这个获取到的就是 :lmabcd

^/(abcd)/.*$ $1

这个获取到的就是 :abcd

 并且修改 下面的 location:

        location / {
            root   html/dist;
            index  index.html index.htm;
        }

 2. 利用 git 克隆远程代码后,运行脚本文件 sh 文件实现对项目的启动

 编写以下 sh 文件:

#!/bin/sh
echo =================================
echo  自动化部署脚本启动
echo =================================

echo 停止原来运行中的工程

# 名字为仓库内项目名
APP_NAME=new-reggie
tpid=`ps -ef|grep $APP_NAME|grep -v grep|grep -v kill|awk '{print $2}'`
if [ ${tpid} ]; then
    echo 'Stop Process...'
    kill -15 $tpid
fi
sleep 2
tpid=`ps -ef|grep $APP_NAME|grep -v grep|grep -v kill|awk '{print $2}'`
if [ ${tpid} ]; then
    echo 'Kill Process!'
    kill -9 $tpid
else
    echo 'Stop Success!'
else
    echo 'Stop Success!'
fi

echo 准备从Git仓库拉取最新代码
cd /usr/local/javaapp/new-reggie

echo 开始从Git仓库拉取最新代码
git pull
echo 代码拉取完成

echo 开始打包
output=`mvn clean package -Dmaven.test.skip=true`

cd target

echo 启动项目

# jar 包名与 log 文件是我们项目起的名字
nohup java -jar my-reggie-takeaway-1.0-SNAPSHOT.jar &> my-reggie-takeaway.log &
 

echo 项目启动完成

此时可以通过访问 反向代理服务器,访问到项目了! 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值