0、Git 使用中的"坑点"

0.0第一次推送报错

在gitee创建远程仓库后,并且自动创建了README.md文件后,在本地提交文件时,会报如下错误

1
error: failed to push some refs to ‘https://gitee.com/

原因:本地文件目录中没有远程仓库初始化时的README.md文件,导致推送出错

解决办法:将README.md文件拉取到本地

:git pull --rebase origin master

1、一些文件配置

1.0

返回类

** Code.java **

Result.java

1.1 loback-spring.xml 文件配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 修改一下路径-->
<property name="PATH" value="./log"></property>

<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<!-- <Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %highlight(%-5level) %blue(%-50logger{50}:%-4line) %thread %green(%-18X{LOG_ID}) %msg%n</Pattern>-->
<Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %highlight(%-5level) %blue(%-30logger{30}:%-4line) %thread %green(%-18X{LOG_ID}) %msg%n</Pattern>
</encoder>
</appender>

<appender name="TRACE_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${PATH}/trace.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>${PATH}/trace.%d{yyyy-MM-dd}.%i.log</FileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>10MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<layout>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %-50logger{50}:%-4line %green(%-18X{LOG_ID}) %msg%n</pattern>
</layout>
</appender>

<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${PATH}/error.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>${PATH}/error.%d{yyyy-MM-dd}.%i.log</FileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>10MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<layout>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %-50logger{50}:%-4line %green(%-18X{LOG_ID}) %msg%n</pattern>
</layout>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>

<root level="ERROR">
<appender-ref ref="ERROR_FILE" />
</root>

<root level="TRACE">
<appender-ref ref="TRACE_FILE" />
</root>

<root level="INFO">
<appender-ref ref="STDOUT" />
</root>
</configuration>
  • 配置文件的存放目录:/resources/

  • 在启动类添加代码

  • 可以在控制台打印出首页地址

  • private static final Logger LOG = LoggerFactory.getLogger(DemoApplication.class);
    public static void main(String[] args){
       SpringApplication app = new SpringApplication(DemoApplication.class);
       Environment env = app.run(args).getEnvironment();
       LOG.info("启动成功!");
       LOG.info("地址:\t http://192.168.4.179:{}",env.getProperty("server.port"));
    <!--code2-->
    
    

3、SpringBoot整合Mybatis

3.0导入依赖与配置文件

  • 导入依赖

  • <dependency>
       <groupId>org.mybatis.spring.boot</groupId>
       <artifactId>mybatis-spring-boot-starter</artifactId>
       <version>2.1.3</version>
    </dependency>
    <dependency>
       <groupId>mysql</groupId>
       <artifactId>mysql-connector-java</artifactId>
    </dependency>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14

    - 在application.yaml中配置datasource

    -

    - ```yaml
    spring:
    datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/demo?useUnicode=true&auseSSL=false&characterEncoding=UTF-8&serverTimezone=GMT%2B8
    password: Jyx1314.+
    username: root
    mybatis:
    mapper-locations: classpath:mapper/**/*.xml

要注意的点

  • 此后配置的xxxMapper.xml文件一定要放在resources目录下,若放在mapper包下则无法静态资源导出!!!

3.1 配置generato

  • 导入依赖

  • <plugin>
       <groupId>org.mybatis.generator</groupId>
       <artifactId>mybatis-generator-maven-plugin</artifactId>
       <version>1.3.7</version>
       <configuration>
          <configurationFile>src/main/resources/generator/generator-config.xml</configurationFile>
          <overwrite>true</overwrite>
          <verbose>true</verbose>
       </configuration>
       <dependencies>
          <dependency>
             <groupId>mysql</groupId>
             <artifactId>mysql-connector-java</artifactId>
             <version>8.0.22</version>
          </dependency>
       </dependencies>
    </plugin>
    <!--code4-->
    
    

4、SpringBoot 写接口

4.0 设置统一返回类 (Result)

  • package com.jyx.Utils;
    
    /**
     * 同意返回结果类(泛型类)
     *
     * @param <T>
     */
    public class Result<T> {
    
    
        //参数:message,code,还有一个泛型的date
        private String message;
        private int code;
        private T data;
    
        //有参构造器
        public Result(String message, int code, T date) {
            this.message = message;
            this.code = code;
            this.data =date;
        }
    
        //无参构造器
        public Result() {
        }
    
        public Result(String message, int code) {
            this.message = message;
            this.code = code;
        }
    
        public String getMessage() {
            return message;
        }
    
        public void setMessage(String message) {
            this.message = message;
        }
    
        public int getCode() {
            return code;
        }
    
        public void setCode(int code) {
            this.code = code;
        }
    
        public T getData() {
            return data;
        }
    
        public void setData(T data) {
            this.data = data;
        }
    
    
        //静态成功返回方法,返回的是一个统一返回类对象,包含date,message,code
        public static  <T> Result<T> success (T Date, String message){
            return new Result<T>(message, Code.SUCCESS.getValue(),Date);
        }
    
        //静态失败返回方法,返回对象中只包含message和code
        public static <T> Result<T> error(String message){
            return new Result<T>(message, Code.ERROR.getValue());
        }
    
        //重写to_string 方法,用于展示结果
        @Override
        public String toString() {
            return'{' +
                    "message=" + message + '\n' +
                    "code=" + code +'\n'+
                    "date="+'\n' + data.toString() +
                    '}';
        }
    
    }
    <!--code5-->
    
    
  • 注意:在使用了generator插件后,Mapper层不需要我们自己写了,相应地,当我们在Service层调用的Mapper的方法也会有所改变,其方法参数大多就是一pojoExample实例

4.3 统一响应类

  • 创建Resq包
  • 下面放的是pojoResq类
  • 进而Service层需要改动
  • 方法参数变为PojoResq
  • 在通过Mapper得到初始返回数据后,
  • 利用BeanUtils的copy方法给pojoResq类赋值,最后Result类返回最终结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
public Result<List<EbookResp>>list(EbookRequestParams ebookRequestParams) {
EbookExample ebookExample = new EbookExample();
EbookExample.Criteria criteria = ebookExample.createCriteria();
criteria.andNameLike("%"+ebookRequestParams.getName()+"%");
criteria.andIdEqualTo(ebookRequestParams.getId());
List<Ebook> ebookList = ebookMapper.selectByExample(ebookExample);
List<EbookResp> list = new ArrayList<>();
ebookList.forEach(ebook -> {
EbookResp ebookResp = new EbookResp();
BeanUtils.copyProperties(ebook,ebookResp);
list.add(ebookResp);
});
return Result.success(list,"ok");
}

4.4 copy工具类

  • 泛型与反射知识

  • package com.jyx.Utils;
    
    import org.springframework.beans.BeanUtils;
    
    import java.util.ArrayList;
    import java.util.List;
    
    
    public class CopyUtil {
        public static <T> T copy(Object source ,Class<T> target){
            if(source==null){
                return null;
            }
            T obj = null;
            try {
                obj = target.newInstance();
                BeanUtils.copyProperties(source,obj);
            } catch (InstantiationException | IllegalAccessException e) {
                e.printStackTrace();
            }
            return obj;
        }
    
        public static <T,E> List<T>  copyList(List<E> source,Class<T> target){
            List<T> list = new ArrayList<>();
            if(!source.isEmpty()) {
                source.forEach(obj -> {
                    T temp = copy(obj, target);
                    list.add(temp);
                });
            }
            return list;
        }
    }
    <!--code7-->
    
    
    
    

5.1 集成Ant—Design-vue

1
2
3
import Antd from "ant-design-vue"
import 'ant-design-vue/dist/antd.css'
createApp(App).use(store).use(router).use(Antd).mount('#app')

5.2自定义组件

  • 组件

  • <template>
      <a-layout-footer style="text-align: center">
        Ant Design ©2018 Created by Ant UED
      </a-layout-footer>
    </template>
    
    <script lang="ts">
    import { defineComponent } from 'vue';
    
    export default defineComponent({
      name: 'TheFooter',
      props: {
        msg: String,
      },
    });
    </script>
    
    <style scoped>
    
    </style>
    <!--code9-->
    
    

6、前后端交互整合

6.0 集成HTTP库Axios

使用npm安装,在vue项目路径下

npm install axios@0.21.0 --save

6.1电子书前后端接口

跨域问题的解决

  • 在Config包下新建CrosConfig类

  • 记得添加@Cinfiguration

  • 去实现WebMvcConfigurer

  • @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOriginPatterns("*")
                .allowedHeaders(CorsConfiguration.ALL)
                .allowedMethods(CorsConfiguration.ALL)
                .allowCredentials(true)
         
    <!--code10-->
    
    

6.4 查询电子书并且在页面展示

技术点

一、使用ref返回单个相应变量数据

该方法的特点是:你给我什么我就拿到什么,也就是你要提前把这个数据封装成想要的结构

  • ref响应式数据类型
  • 导入 ref包
  • 在setup中声明 const ebooks = ref()
  • 在onMounted 中
  • ebooks.value = data.data
  • 在setup方法结尾处返回ebooks 此时,要注意!!!!!!!
  • 返回数据的写法为:return{ebooks}
  • 并且再次注意!!
  • 页面拿到数据的方式为:{{ebooks}}
  • 可以用<pre></pre>标签美化

二、使用reactive()

该方法的特点是,()里面要设置参数,并且这个参数应该是Json类型的,好处是在声明变量的时候就确定了所要传输数据的结构,value也可以是个对象(比如也是一个Json)!!即:{key0: value,key1:value....} value 写成 [],就表明这是一个数组

  • 导入reactive
  • 在setup中声明变量 `const ebook = reactive({…})

三、代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
setup(){
console.log('setup');
const ebooks = ref();
const ebook = reactive({book :[],code: "",msg:""})

onMounted(()=>{
console.log("onMounted");
axios.get('http://localhost:8081/Test1?name=教程').then((response)=>{
const data = response.data;
console.log(data);
ebooks.value = data.data;
ebook.book = data.data;
ebook.msg = data.message;
ebook.code = data.code;
});
})
return{
ebooks,
ebook
}
}

6.5 导入图标库

  • `npm install @ant-design/icons -vue --save

  • 在main.ts中写入

  • import * as Icons from '@ant-design/icons-vue';
    
    const incos : ant = Icons;
    for(const i in incos){
        App.compoent(i,incos[i]);
    }
    <!--code12-->
    
    

如果传进来值就按照传的值处理,要是没穿,则就是查询所有字段

6.8 Vue_Cle多环境配置

  • 在web目录下建立

  • .env.dev.txt

  • NODE_ENV=development
    VUE_APP_SERVER=http://192.168.110.179:8081
    
    1
    2
    3
    4
    5
    6

    - .env.prod.txt

    - ```xml
    NODE_ENV=production
    VUE_APP_SERVER=http://jyx...
  • 更改package.json 中的server名字

  • 在main.js中配置全局变量

  • axios.defaults.baseURL = process.env.VUE_APP_SERVER

6.9 axios拦截器打印日志

在main.ts中加入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
axios.interceptors.request.use((config)=>{
console.log('请求参数:',config);
return config;
}, error => {
return Promise.reject(error);
});
axios.interceptors.response.use((response)=>{
console.log('返回结果',response);
return response;
}, error => {
return Promise.reject(error);
})

const app = createApp(App);
app.use(store).use(router).use(Antd).mount('#app');
const icons : any = Icons;
for(const i in icons){
app.component(i,icons[i]);
}
console.log(process.env.NODE_ENV);
console.log(process.env.VUE_APP_SERVER);

6.10 Servlet 的Filter 检测接口耗时

  • 新建filter包

  • 新建LogFilter类

  • package com.jyx.filter;
    
    
    import com.mysql.cj.log.Log;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.context.annotation.ComponentScan;
    import org.springframework.stereotype.Component;
    
    import javax.servlet.*;
    import javax.servlet.http.HttpServletRequest;
    import java.io.IOException;
    
    @Component
    public class LogFilter implements Filter {
    
        private static final Logger LOG =  LoggerFactory.getLogger(LogFilter.class);
    
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {
        }
    
        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
            //打印请求信息
            HttpServletRequest request1 = (HttpServletRequest) request;
            LOG.info("----------LogFilter开始-----------");
            LOG.info("请求地址:{} {}",request1.getRequestURL().toString(),request1.getMethod());
            LOG.info("请求远程地址:{}",request1.getRemoteAddr());
    
            long startTime = System.currentTimeMillis();
            chain.doFilter(request,response);
            LOG.info("------LogFilter结束:耗时{}ms",System.currentTimeMillis()-startTime);
        }
    
        @Override
        public void destroy() {
            Filter.super.destroy();
        }
    }
    <!--code15-->
    
    

7.1 使用Spring 特有的 拦截器 interceptor 第二开始

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package com.jyx.interceptor;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
* 拦截器:Spring框架特有的,常用于登录校验,权限校验,请求日志打印 /login
*/
@Component
public class LogInterceptor implements HandlerInterceptor {

private static final Logger LOG = LoggerFactory.getLogger(LogInterceptor.class);

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 打印请求信息
LOG.info("------------- LogInterceptor 开始 -------------");
LOG.info("请求地址: {} {}", request.getRequestURL().toString(), request.getMethod());
LOG.info("远程地址: {}", request.getRemoteAddr());

long startTime = System.currentTimeMillis();
request.setAttribute("requestStartTime", startTime);
return true;
}

@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
long startTime = (Long) request.getAttribute("requestStartTime");
LOG.info("------------- LogInterceptor 结束 耗时:{} ms -------------", System.currentTimeMillis() - startTime);
}
}

7.2 AOP 。。。。

7.3 打印sql日志

1
2
3
logging:
level:
com.jyx.mapper: trace

8、管理员页面

8.0 初始化

  • 添加一个页面后第一件事:添加路由

  • ,
    {
      path: '/admin/ebooks',
      name: 'ebookAdmin',
      // route level code-splitting
      // this generates a separate chunk (about.[hash].js) for this route
      // which is lazy-loaded when the route is visited.
      component: ebooksAdmin
    }
    <!--code18-->
    
    

8.1 普通日志

1
private static final Logger LOG = LoggerFactory.getLogger(EbookService.class);

8.2PageHelper的使用

  • 添加依赖

  • <dependency>
       <groupId>com.github.pagehelper</groupId>
       <artifactId>pagehelper-spring-boot-starter</artifactId>
       <version>1.4.2</version>
    </dependency>
    <!--code20-->
    
    

其中,title为显示的字段名,dataIndex为a-table中datasource中每一个Json类型数据中的key值,要对应起来,否则每一列下的value不能正确显示

2、编辑按钮

1
2
3
<a-button type="primary" @click="edit(record)">
编辑
</a-button>

3、点击编辑按钮出现的表单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!-- 在编辑页面展示的电子书表单  -->
<a-form :model="ebook" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<a-form-item label="封面">
<a-input v-model:value="ebook.cover" />
</a-form-item>
<a-form-item label="名称">
<a-input v-model:value="ebook.name" />
</a-form-item>
<a-form-item label="分类1">
<a-input v-model:value="ebook.category1Id" />
</a-form-item>
<a-form-item label="分类2">
<a-input v-model:value="ebook.category2Id" />
</a-form-item>
<a-form-item label="描述">
<a-input v-model:value="ebook.description" type="textarea" />
</a-form-item>
</a-form>

</a-modal>

4、给予表单数据的

1
2
3
const ebook = ref({});
const modalVisible = ref(false);
const modalLoading = ref(false);

5 edit主方法

1
2
3
4
const edit =(record:any)=>{
ebook.value = record;
modalVisible.value=true;
}

6、点击ok,请求更新数据,刷新列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const handleModalOk = () =>{
modalLoading.value = true;
axios.post('/ebook/save',ebook.value).then((response) => {
modalLoading.value = false;
if (response.data.code==1){
modalVisible.value = false;

// 重新加载列表
handleQuery({
page: pagination.value.current,
size: pagination.value.pageSize,
});
}
else {
message.error(response.data.message);
}
})
};

7、保存操作的后端

  • 根据id是否为null来决定是新增还是保存
  • 新增和保存公用一个方法
  • Controller层 在接收前端传来的表达数据时候
  • 要在参数前加上@ResponseBody注解,来保证接收到

8.4 雪花算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
package com.jyx.Utils;

import org.springframework.stereotype.Component;

import java.text.ParseException;

/**
* Twitter的分布式自增ID雪花算法
**/
@Component
public class SnowFlake {

/**
* 起始的时间戳
*/
private final static long START_STMP = 1653580800000L; // 2021-01-01 08:00:00 起始时间

/**
* 每一部分占用的位数
*/
private final static long SEQUENCE_BIT = 12; //序列号占用的位数
private final static long MACHINE_BIT = 5; //机器标识占用的位数 2^5=32 最多可以表示32台机器
private final static long DATACENTER_BIT = 5;//数据中心占用的位数

/**
* 每一部分的最大值
*/
private final static long MAX_DATACENTER_NUM = -1L ^ (-1L << DATACENTER_BIT);
private final static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT);
private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT);

/**
* 每一部分向左的位移
*/
private final static long MACHINE_LEFT = SEQUENCE_BIT;
private final static long DATACENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;
private final static long TIMESTMP_LEFT = DATACENTER_LEFT + DATACENTER_BIT;

private long datacenterId = 1; //数据中心
private long machineId = 1; //机器标识
private long sequence = 0L; //序列号
private long lastStmp = -1L;//上一次时间戳

public SnowFlake() {
}

public SnowFlake(long datacenterId, long machineId) {
if (datacenterId > MAX_DATACENTER_NUM || datacenterId < 0) {
throw new IllegalArgumentException("datacenterId can't be greater than MAX_DATACENTER_NUM or less than 0");
}
if (machineId > MAX_MACHINE_NUM || machineId < 0) {
throw new IllegalArgumentException("machineId can't be greater than MAX_MACHINE_NUM or less than 0");
}
this.datacenterId = datacenterId;
this.machineId = machineId;
}

/**
* 产生下一个ID
*
* @return
*/
public synchronized long nextId() {
long currStmp = getNewstmp();//这个是跟起始时间戳的一个差值
if (currStmp < lastStmp) {
throw new RuntimeException("Clock moved backwards. Refusing to generate id");
}

if (currStmp == lastStmp) {
//相同毫秒内,序列号自增
sequence = (sequence + 1) & MAX_SEQUENCE; //一个毫秒内,最多可以生成的序列号是 2^SEQUENCE_BIT
//同一毫秒的序列数已经达到最大
if (sequence == 0L) {
currStmp = getNextMill();
}
} else {
//不同毫秒内,序列号置为0
sequence = 0L;
}

lastStmp = currStmp;

return (currStmp - START_STMP) << TIMESTMP_LEFT //时间戳部分
| datacenterId << DATACENTER_LEFT //数据中心部分
| machineId << MACHINE_LEFT //机器标识部分
| sequence; //序列号部分
}

private long getNextMill() {
long mill = getNewstmp();
while (mill <= lastStmp) {
mill = getNewstmp();
}
return mill;
}

private long getNewstmp() {
return System.currentTimeMillis();
}

public static void main(String[] args) throws ParseException {
// 时间戳
// System.out.println(System.currentTimeMillis());
// System.out.println(new Date().getTime());
//
// String dateTime = "2022-05-27 00:00:00";
// SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
// System.out.println(sdf.parse(dateTime).getTime());

SnowFlake snowFlake = new SnowFlake(1, 1);

long start = System.currentTimeMillis();
for (int i = 0; i < 10; i++) {
System.out.println(snowFlake.nextId());//每次都去生成id
System.out.println(System.currentTimeMillis() - start);//计算生成所耗费的时间
}
}
}

8.5 删除功能

  • 前端
1
2
3
4
5
6
7
8
9
10
<a-popconfirm
title="Are you sure delete this task?"
ok-text="Yes"
cancel-text="No"
@confirm="handleDelete(record.id)"
>
<a-button type="danger">
删除
</a-button>
</a-popconfirm>

8.6 查询功能

  • 横向的form表单
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<p>
<a-form layout="inline" :model="param">
<a-form-item>
<a-input v-model:value="param.name" placeholder="名称"></a-input>
</a-form-item>
<a-form-item>
<a-button type="primary" @click="handleQuery({page: 1, size: pagination.pageSize})">
查询
</a-button>
</a-form-item>
<a-form-item>
<a-button type="primary" @click="add()">
新增
</a-button>
</a-form-item>
</a-form>
</p>
  • param为响应式数据变量 其值从input获得
  • 注意此处 并没有在 查询按钮的@Click绑定的函数中直接给到 查询参数name 的值 而是在setup函数中 定义param变量
  • 重点:定义完之后一定要初始化!!!
  • 在查询函数中再去 添加 传给后端的参数
  • 最后要在setup 后 return 回去

8.7 axios异步加载可能出现的问题

  • 如果分类查询在书籍查询之后

  • 并且书籍中要用到书籍分类的数据

  • 则会报错。 书籍展示–>(依赖于)分类数据

  • 应该在axios中将查询内置在分类查询中,

  • /**
     * 查询所有分类
     **/
    const handleQueryCategory = () => {
      loading.value = true;
      axios.get("/category/all").then((response) => {
        loading.value = false;
        const data = response.data;
        if (data.code==1) {
          categorys = data.data;
          console.log("原始数组:", categorys);
    
          level1.value = [];
          level1.value = Tool.array2Tree(categorys, 0);
          console.log("树形结构:", level1.value);
          // 加载完分类,再加载电子书,否则如果分类树加载很慢,则电子书渲染会报错
          //因为axios异步加载的原因
          handleQuery({
            page: 1,
            size: pagination.value.pageSize
          });
    
        } else {
          message.error(data.message);
        }
      });
    };
    <!--code29-->
    
    

渲染数据

1
2
3
<template v-slot:category="{ text, record }">
<span>{{ queryCategoryName(record.category1Id) }} / {{queryCategoryName(record.category2Id) }}</span>
</template>
1
2
3
4
5
{
title: '分类',

slots: { customRender: 'category' }
},

9、文档管理

9.0 vue中的route-link标签中url设置参数

1
<router-link :to="'/admin/doc?ebookId=' + record.id">
  • ''中给出的为字符串,以+方式拼接

被跳转的页面中 使用useRoute()拿到参数

  • const route = useRoute();
1
2
3
doc.value = {
ebookId: route.query.ebookId,
};

9.1 文档内容的保存(saveOrAdd 的逻辑,以及CopyUtils的注意问题,不能有构造器)

一、SaveOrAdd 的逻辑

主要思路:1,对新增的文档而言:文档内容(实际是Content表)一定是 Add操作

​ 2,对要Save(即已经创建过的文档)的文档而言,依据:**Content中对应文档ID的content字段是否为空(具体判断的依据是:update的返回值)**分两种情况

I:update的返回值若为0,则表示并docId对应的字段,还不存在,update当然返回值为0,此时就应该去insert

II: update的返回值若为1,则与I相反,则不需要另外的操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public void save(DocSaveReq req){
Doc doc = CopyUtil.copy(req,Doc.class);
Content content = CopyUtil.copy(req,Content.class);
if(ObjectUtils.isEmpty(req.getId())){
//新增
doc.setId(snowFlake.nextId());
doc.setViewCount(0);
doc.setVoteCount(0);
docMapper.insertSelective(doc);

//对content insert
content.setId(doc.getId());
contentMapper.insertSelective(content);
} else{
//更新
docMapper.updateByPrimaryKey(doc);
int count = contentMapper.updateByPrimaryKeyWithBLOBs(content);
//返回值判断
if(count == 0){
contentMapper.insert(content);
}
}
}

二、CopyUtils的注意问题

Doc doc = CopyUtil.copy(req,Doc.class);

如果req中有了全字段的构造器,这个时候手动地去再添加一个无参构造器,则报错,则会Copy失败。。。。

9.2 阅读数加一&点赞

注意的问题

  1. 尽管数据库中设置了view和vote的字段默认值为0,但如果是在insert的时候选择了这两个字段,则新增的一条数据的
  2. 解决办法:

未弄明白的问题:

(具体的在一段时间内同一ip的人不能对同一篇文章多次点赞)

RedisUtil , RequestContext,Aop

9.3 修复 在不刷新的情况下在同一电子书页面内点击不同的文档,页面来回跳转时,阅读数不能实时更新的bug。

9.4 自定义sql更新ebookInfor

9.5 SpringBoot定时任务

9.6WebSocket,map修饰符问题

9.7 异步化中注解冲突问题

9.8 事务注解

10、User管理

10.0 生成User 的问题

  • Mybatis中generator在生成User时,Mapper类中没有 update方法
  • 不可知的原因使得 表明 为 user的表生成代码文件不对
  • 经过修改表名得以解决问题

10.1 业务异常统一处理类

  • BusinessExceptionCode

  • package com.jyx.exception;
    
    public enum BusinessExceptionCode {
        USER_LOGIN_NAME_EXIST("登录名已存在"),
        LOGIN_USER_ERROR("用户名不存在或密码错误"),
        VOTE_REPEAT("您已点赞过"),
        ;
        ;
    
        private String desc;
    
        BusinessExceptionCode() {
        }
    
        BusinessExceptionCode(String desc) {
            this.desc = desc;
        }
    
        public String getDesc() {
            return desc;
        }
    
        public void setDesc(String desc) {
            this.desc = desc;
        }
    }
    <!--code35-->
    
    

  • ControllerExceptionHandler中新增加

  • /**
     * 业务异常统一处理
     *
     */
    @ExceptionHandler(value = BusinessException.class)
    @ResponseBody
    public Result<Object> validExceptionHandler(BusinessException e) {
        LOG.warn("业务异常:{}", e.getCode().getDesc());
        return Result.error(Code.BUSINESS_EXCEPTION.getCode(),BusinessExceptionCode.USER_LOGIN_NAME_EXIST.getDesc());
    }
    <!--code36-->
    
    

10.3 不可修改用户名(前端)

1
<a-input v-model:value="user.name" :disabled="user.id!==undefined" />

**特别注意: : **千万不能 少!!!!

10.4 CopyUtils 的作用!!

重置密码需求分析:

  • 前景:前端有密码格式校验,所以如果再编辑页面处理重置密码的需求,由于经过加密的密码长度为32位,就不符合了校验规则,导致每次想更改其他字段的时候,都不得不去修改加密过后的密码,并且要想维持原密码,每次还需要去输入原来的密码
  • 如此编辑功能应该与重置密码功能解耦(此处应该是由于密码具有检验规则导致了这两个功能产生了复杂关系)

CopyUtils 的作用

  • 由于mybatis的自动生成的代码中的方法参数类型基本都以domain中的原生实体类为主,
  • 又因为在Service层中为了可拓展性的提高,Service方法的参数基本都是经过包装过的XXXReq 类,
  • 因此不能直接将传到Service方法中的Req类型参数给到Mapper层,
  • 因此就有了 要从Req类型转到原生类型 并且达到已有数据拷贝的需求,即CopUtils

10.5 User登录页面

一、前端

主要思路:

  1. 添加登录的<a></a>标签,或者按钮,点击则会弹出 loginModel(即要去@Click一个函数);

  2. loginModel 中应该有: 用户名,密码(要加密),代码:

  3. <a-modal
        title="登录"
        v-model:visible="loginModalVisible"
        :confirm-loading="loginModalLoading"
        @ok="login"
    >
      <a-form :model="loginUser" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
        <a-form-item label="登录名">
          <a-input v-model:value="loginUser.name" />
        </a-form-item>
        <a-form-item label="密码">
          <a-input v-model:value="loginUser.password" type="password" />
        </a-form-item>
      </a-form>
    </a-modal>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

    4. loginModel 中涉及的变量,

    5. ```typescript
    const loginModalVisible = ref(false);
    const loginModalLoading = ref(false);
    const loginUser = ref({
    name: 'never',
    password: 'never',
    })
    //loginUser 为一个响应式变量,会动态监听模态框中的字段值,更新到 loginModel中 form中 的 :model指定的变量 本例中就是 loginUser (:model="loginUser" )
  4. 弹出model的函数:

  5. const showLoginModal = () => {
      loginModalVisible.value =true;
    }
    <!--code39-->
    
    

二、后端

就是增加一个接口,一个req,一个resp(这个resp在后面用于取到该用户的一些信息)

login接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public UserLoginResp login(UserLoginReq req) {
User userDb = selectUserByName(req.getName());
if (ObjectUtils.isEmpty(userDb)) {
//用户名不存在
LOG.info("用户名不存在:{}", req.getName());
throw new BusinessException(BusinessExceptionCode.LOGIN_USER_ERROR);
}
if (!userDb.getPassword().equals(req.getPassword())) {
//密码错误
LOG.info("用户:{} 密码错误", req.getName());
throw new BusinessException(BusinessExceptionCode.LOGIN_USER_ERROR);
}

return CopyUtil.copy(userDb, UserLoginResp.class);
}

此处的注意点:

  • 当用户发生密码或者用户名错误的时候,Service方法不应该直接返回null

  • 而是,因该去自定义一个BusinessException异常

  • package com.jyx.exception;
    
    public class BusinessException extends RuntimeException {
    
        BusinessExceptionCode code;
    
        public BusinessException(String message, BusinessExceptionCode code) {
            super(message);
            this.code = code;
        }
    
        public BusinessException() {
        }
    
        public BusinessException(BusinessExceptionCode code) {
            super(code.getDesc());
            this.code = code;
        }
    
        public BusinessExceptionCode getCode() {
            return code;
        }
    
        public void setCode(BusinessExceptionCode code) {
            this.code = code;
        }
    
        /**
         * 不写入堆栈信息,提高性能
         */
        @Override
        public Throwable fillInStackTrace() {
            return this;
        }
    }
    <!--code41-->
    
    
  • ControllerExceptionHandler

  • /**
     * 业务异常统一处理
     *
     */
    @ExceptionHandler(value = BusinessException.class)
    @ResponseBody
    public Result<Object> validExceptionHandler(BusinessException e) {
        LOG.warn("业务异常:{}", e.getCode().getDesc());
        return Result.error(Code.BUSINESS_EXCEPTION.getCode(),e.getCode().getDesc());
    }
    <!--code42-->
    
    
  • 定义一个全局的常量USER,它作为sessionStore中的一个储存user信息的K-V对,其Key为 USER,通过sessionStore.get()方法可以获取USER对应的Value,并将其赋值给state中user对应的value,(此处之所以要经过这样的一次中转,应该是为了便于维护,其他位置对store的操作更方便些,所以实质上user的信息因该是储存在sessionStore中,应该和Session道理类似)

  • mutations中定义的是对state的一些操作方法,

  • 此处参数,state是必要的,user为Source,target为state中的user的value和sessionStore中的USER的value。

  • 应该注意的是,state是一个json类型对象,其中定义一个user,其值从SessionStore.get方法动态获取,有两种情况:

  • 1,最开始未登录的时候,sessionStore中USER的value并无值,因为在下面的login方法中才会调用**stor.commit(mehtodName,parameters)**方法去

    state.user = user;
    SessionStorage.set(USER,user);

    所以才有了这一行代码的后半段user:SessionStorage.get(USER)||{},

    赋一个空对象,防止null

  • 登录后的刷新问题,

  • 在登陆后,sessionStore的USER对应的Valu中就存有user的信息,value为一个json对象,在刷新页面后,USER仍然存在,这个时候,vue组件重新加载,以下代码

  • state: {
        user:SessionStorage.get(USER)||{},
      },
    <!--code43-->
    
    
  • computed 的方式 声明 user是一个响应式变量,user会从computed中的函数的返回值动态的更新数据,也就是说,use的值的变化取决于,函数值的变化,结果的功能上与ref()有点像,(个人理解就相当于是store.state.user的一个"影分身")并且该变量最后要return出去,以便给到页面使用

  • 登录的时候要

  • axios.post('user/login',loginUser.value).then((response)=>{
      loginModalLoading.value = false;
      const data = response.data;
      if(data.code===1){
        message.success("登录成功");
        //登录成功后将data里面的user信息存到 vuex 的 user 里面
        store.commit("setUser",data.data);
        loginModalVisible.value = false;
      }else {
       message.error(data.message);
      }
    })
    <!--code44-->
    
    

11、项目发布

0.0 idea 连接云数据库

0.1 idea 连接云服务器

0.2 通过idea向服务器上传文件