东软餐厅项目问题解决心得

前言

经过不懈的努力前端在我的磕磕绊绊的情况下终于写得差不多了,今天终于开始搭建后端的项目了。后端遇到的问题和前端的修改思路都会记录在这里

解析MySQL Connector/J依赖时遇到了问题

旧版本的MySQL JDBC依赖mysql-connector-java更新为mysql-connector-j

1
2
3
4
5
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>

分包部署的详细方法

  1. 首先需要通过正常的流程创建springBoot项目。等待项目初始化完成后。

  1. 对着项目进行右键,点击新建,创建新模块

注意: 这里创建的是maven项目

  1. 修改pom文件
    修改父工程下的pom文件中的打包方式为pom,如下

    idea会自动设置为父工程
  2. 创建子工程
    Archetype:选择org.apache.maven.archetypes:maven-archetype-quickstart
    选quickstart没得那么多的其他杂七杂八的东西,是最接近纯骨架的模板

    我这里创建了两个子工程看起来直观一点,只要看到下面的加载完成了就成功了

分包后修改名字

对着需要修改的模块右键,然后点击重构,选择两个都修改

但是事情往往不会这么简单,在你修改名字后你发现包的前面代表那个包的小蓝点不见了,
这就是我们修改后没有将包导入进来,现在这个只是一个修改名字后的文件夹,不是一个模块了。

解决方法:

在项目模块中,上面有个加号。点击导入模块,把你修改后的模块导入进来。

导入后会让你选择构建的东西,选择maven构建就行了。这样就解决了分包部署后修改名字的问题

首页滑动组件数据渲染与随机展示功能

  1. 看过我们这个首页滑动组件的就知道,我们这些数据不是一个固定的数据,而是从数据库中获取的,所以我们需要在页面上渲染出来。总体的流程就是先从数据库中获取数据,然后将数据进行一个随机取出,然后调用uinapp 的 组件,进行多次渲染,当然可以是用for循环进行渲染。

我们这里开启的是横向的滑动, 同时渲染了五个数据。

1
2
3
4
5
6
7
8
9
10
11
<template>
<view class="nsu-goods-item">
<image :src="randomDish.pic" style="width: 202rpx;height:150rpx;" mode="widthFix"></image>
<view class="nsu-goods-name">
{{ randomDish.name }}
</view>
<view class="nsu-goods-price">
¥{{ randomDish.price }}
</view>
</view>
</template>

1. 导入必要的模块和类型

1
2
3
4
5
6
7
8
import {getDishListAPI} from '@/api/dish'
import {getSetmealListAPI} from '@/api/setmeal'
import {onLoad, onShow} from '@dcloudio/uni-app'
import {ref} from 'vue'
import type { DishItem } from '@/types/dish';
import { getCategoryAPI } from '@/api/category';
import type { CategoryItem } from '@/types/category';
import type { SetmealItem } from '@/types/setmeal';
  • 导入API函数:getDishListAPIgetSetmealListAPIgetCategoryAPI
  • 导入生命周期钩子:onLoadonShow
  • 导入Vue的响应式引用函数:ref
  • 导入类型定义:DishItemCategoryItemSetmealItem

2. 定义响应式变量

1
2
3
4
const dishList = ref<(DishItem | SetmealItem)[]>([])
const categoryList = ref<CategoryItem[]>([])
const activeIndex = ref(0)
const randomDish = ref<DishItem | SetmealItem | null>(null)
  • dishList:存储菜品或套餐的列表。
  • categoryList:存储分类列表。
  • activeIndex:存储当前激活的分类索引。
  • randomDish:存储随机选择的菜品或套餐。

3. 定义方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const getCategoryData = async () => {
const res = await getCategoryAPI()
categoryList.value = res.data
}

const getDishOrSetmealList = async (index: number) => {
activeIndex.value = index
let res
if (categoryList.value[index].type === 1) {
res = await getDishListAPI(categoryList.value[index].id)
} else {
res = await getSetmealListAPI(categoryList.value[index].id)
}
dishList.value = res.data
}

const selectRandomDish = () => {
if (dishList.value.length > 0) {
const randomIndex = Math.floor(Math.random() * dishList.value.length);
randomDish.value = dishList.value[randomIndex];
}
}
  • getCategoryData:异步获取分类数据并更新categoryList
  • getDishOrSetmealList:根据分类类型(菜品或套餐)异步获取对应的列表数据并更新dishList
  • selectRandomDish:从dishList中随机选择一个菜品或套餐并更新randomDish

4. 生命周期钩子

1
2
3
4
5
6
7
8
9
onLoad(async () => {
await getCategoryData()
await getDishOrSetmealList(0) // 默认加载第一个分类下的菜品列表
selectRandomDish() // 选择一个随机菜品显示
})

onShow(async () => {
await getCategoryData()
})
  • onLoad:页面加载时执行,获取分类数据、默认分类下的菜品列表,并随机选择一个菜品。
  • onShow:页面显示时执行,重新获取分类数据。

总结代码

主要功能

  • 数据获取:通过API函数异步获取菜品、套餐和分类数据。
  • 数据处理:根据分类类型获取对应的菜品或套餐列表,并从中随机选择一个菜品或套餐。
  • 生命周期管理:在页面加载和显示时执行数据获取和处理逻辑,确保数据的实时性和正确性。

商店的状态获取问题解决

在最初的版本的时候,由于我没有做后台管理系统,这存在了一个问题。就是在前端获取商店状态的时候或出现无法请求的问题。
这里我们将后端的逻辑改一下

1
2
3
4
5
6
7
8
9
10
11
12
13

public static final String KEY = "SHOP_STATUS";

@GetMapping("/status")
public Result<Integer> getStatus(){
Integer status = (Integer)redisTemplate.opsForValue().get(KEY);
if (status == null) {
status = 1; // 默认状态为营业中
}
log.info("当前店铺状态为:{}", status == 1 ? "营业中" : "打烊中");
return Result.success(status);
}

直接强制的为开放状态。这样前端就可以直接请求获取商店状态了。

整体项目部署构思

这个项目实际上是微信小程序端,和后端java端的结合体。我们现在的解决办法是,小程序的前端的部分和后端的java的部分分开部署。

前端部署是最简单的,注册开发者以后,在微信开发者工具后台,选择项目,然后上传代码即可。

后端部署的话,我们可以选择使用云服务器,比如阿里云或者腾讯云,然后在服务器上安装jdk、maven、mysql等环境,然后将项目代码上传到服务器,然后配置好数据库连接,然后启动项目。

这样的话,前端和后端的部署可以分开进行,前端只需要上传代码,后端只需要配置环境、启动项目即可。

微信小程序上传失败,代码审查不通过问题解决


这里可以看到问题出现在,图片和音频资源过大的缘故,由于小程序对于启动速度和资源大小的限制原因,我们如果将大量的静态资源放到本地的话
那么整个项目的占用将十分的大,同时会影响小程序的启动速度。

我们需要把一些图片资源放入云端存储中,通过网络调用进行一些图片的加载。
我这里采用的是我一直在用的阿里云oss存储,具体的配置方法可以参考阿里云的文档。

我这里创建了两个包,一个包装的是菜品的图片以及一些店招等信息

另一个是一存储一些,图标信息。当然是一些不是很重要的图标

第二部分其实很简单

就是需要开启一个组件的按需注入,和懒加载而已。这个不影响上传的

微信小程序上传为体验版后,出现无法获取后端请求问题

错误信息
GET https://你要访问的url(可以到浏览器上验证一下,访问之后成功的话会有一个json格式的文本出现)

net::ERR_PROXY_CONNECTION_FAILED(env: Windows,mp,1.06.2402030; lib: 3.3.5)

大概就是这种报错,这个其实是是由于代理的问题,我们打开微信小程序开发工具,在设置里面找到代理,然后关闭代理即可。

微信小程序上传为体验版后,出现无法获取后端请求问题(第二种情况)

小程序开发的时候,在用微信开发者工具做网络请求的时候,调试会出错,提示“不在以下 request 合法域名列表中,请参考文档” https://developers.weixin.qq.com/miniprogram/dev/framework/ability/network.html

这个就是微信小程序的设定问题,他要求我们在体验版的时候是需要配置合法的request域名的,但是我们在做项目的时候,我们并没有配置合法的request域名,所以就会出现这个问题。 其实我个人不是很明白为什么要这样设定,因为这个是体验版我认为大部分的后端都还没有上传服务器

解决方法:
进入微信公众平台按照下面图片的要求来就行了

配置好后,再次请求,问题就解决了。

在idea中进行maven项目的打包

这里存在一个问题,就是当我们一个有多个模块进行开发的时候例如:

这个图片中,我们存在三个模块,其中,最后一个server模块依赖前两个模块的内容,而每个模块都是一个独立的maven项目,我们该如何打包才能使其打包后能够正常的使用呢?

解决办法:

首先需要明确,此时的项目模块的依赖关系是向下一次依赖的,即二级依赖依赖于一级依赖

所以我们的打包的思路就是先打包一级的依赖,再打包二级的依赖,最后打包三级依赖。(也就是最后一个运行的依赖)
但是看起来很复杂,这个问题其实解决很简单,只需要在maven项目下直接打包就行了。idea会根据我们的依赖关系进行一依次的打包,这也就对应了上述的多模块开发的思路。我们总体的打包操作是直接在父项目下进行的操作。

上传服务进行配置

我们这里使用宝塔面板进行一个部署,对于linux的相关操作我这里就不在过多的陈述了。

  1. 点击文件 ,创建一个文件用于存放打包好的包

  2. 上传完成后进入网站页面,找到java管理,这里看到我们已经存在了一个springboot的选项。这个是为什么呢?
    那就是,因为打包后的springboot项目是自带tomcat的,执行命令后可以直接脱离外部环境直接运行的项目。所以这里我们可以直接部署

  3. 将所在文件下的包进行选择,然后系统会自动的分配对应的端口,但是此时需要注意你的jdk版本,如果你的jdk版本和你的项目不适配的话,会出现运行失败的情况。

  1. 这里的端口也需要注意:点击开放端口后,只是你宝塔面板这里开放了端口,这个是不行的,因为你用的服务器,服务器厂商会有自己的拦截。我用的是阿里云的服务器,那么我需要去阿里云的安全组中放行对应的端口

Redis配置

目前应对,相对较小的访问量还不足以看出系统的访问速度的快慢,当有大量的人同时访问时,就会出现访问缓慢的情况,特别时加载菜品数据和切换菜品的时候,会出现加载不出来,或者加载缓慢的情况。这个问题的核心就是一个问题,那就是我们访问的数据是相同的,那么为什么我们不把他进行一个缓存,这部分内容属于高频访问的数据。这个就是Redis存在的核心

Redis的基本使用,我已经放在了前面的博客中,可以去找一找。

宝塔面板部署Redis数据库

宝塔面板部署Redis数据库其实核心是和自己电脑上部署没有什么区别。但是唯一存在的问题就是如何远程访问Redis服务器

宝塔面板下载Redis数据库

进入宝塔面板的应用商店,下载Redis并且等待Redis的配置完成

配置Redis


这里外网的配置为0.0.0.0 表示全部映射到公网上面去
这里映射到公网上面去需要配置Redis的密码。

开启端口访问权限

在宝塔面板中和服务器控制台的安全组中都需要放开这个端口,否则你在本地的SSH访问是不能访问的

端口开放后,直接输入相应的服务器ip地址,以及密码即可正常访问

后端重要代码记录

BaseContext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class BaseContext {
public static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

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

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

public static void removeCurrentId() {
threadLocal.remove();
}
}

ThreadLocal 用于在每个线程中存储独立的数据副本,确保线程安全。每个用户独立的一个线程避免了紊乱

BaseException

1
2
3
4
5
6
7
8
public class BaseException extends RuntimeException {
public BaseException(){}

public BaseException(String msg){
super(msg);
}
}

BaseException 基础的线程类,后面的各种异常信息都继承自这个类
RuntimeException 是 Java 中所有运行时异常的基类。

空参构造:

1
2
3
public class BaseException extends RuntimeException {
public BaseException(){}
}

这个是个空参构造,创建一个没有消息的实例。反正我一般喜欢加一个空参构造

带参构造:

1
2
3
4
5
public class BaseException extends RuntimeException {
public BaseException(String msg){
super(msg);
}
}

带参构造这边接收一个String类型的msg消息,调用父类(即 RuntimeException)的构造函数,并将 msg 传递给它。这使得 msg 成为异常的消息。

处理日期和时间的序列化与反序列化

这个序列化器是一个相对固定的代码

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
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";
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(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);
}

}

Result

1
2
3
4
5
6
7
8
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageResult implements Serializable {

private Long total; // 总记录数
private List records; // 当前页数据集合
}

统一返回分页数据的分页数据
Serializable

  • 序列化(Serialization)是将对象转换为字节流的过程,以便将其存储在文件中、在网络中传输或在内存中进行持久化。序列化的主要目的是保存对象的状态,以便在需要时可以重新创建该对象。

  • 反序列化(Deserialization)是序列化的逆过程,即从字节流中重新创建对象。

为什么需要序列化和反序列化?
持久化存储:将对象序列化后可以存储在文件中,以便在程序重启后可以重新加载。
网络传输:在分布式系统中,对象需要在网络中传输,序列化可以将对象转换为字节流,便于传输。
进程间通信:在多进程环境中,序列化可以用于进程间传递对象。
Java中的序列化和反序列化
在Java中,实现序列化的方式是让类实现java.io.Serializable接口。这个接口是一个标记接口,没有任何方法,只是告诉JVM这个类的对象可以被序列化。

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
/**
* 后端统一返回结果
* @param <T>
*/
@Data
public class Result<T> implements Serializable {

private Integer code; //编码:1成功,0和其它数字为失败
private String msg; //错误信息
private T data; //数据

/**
* 注意:静态方法是属于类的而不是对象的,因此才可以Result.success()直接 “类” 调用方法!!!
* @return
* @param <T>
*/
public static <T> Result<T> success() {
Result<T> result = new Result<T>();
result.code = 0;
return result;
}
public static <T> Result<T> success(T object) {
Result<T> result = new Result<T>();
result.data = object;
result.code = 0;
return result;
}
public static <T> Result<T> error(String msg) {
Result result = new Result();
result.msg = msg;
result.code = 1;
return result;
}

}

  • 这里的第一个succes是空的,对应的是部分操作返回的是空的结果(例如删除时,删除后不需要像前端返回对应的数据)

  • 它们都接收一个泛型,三个成员变量:code、msg 和 data,分别表示状态码、错误信息和返回数据。通过静态方法 success 和 error,可以方便地创建表示成功或失败的结果对象。主要功能是提供一个统一的接口来返回后端处理的结果,便于前端进行统一处理和展示。

Utils

HttpClientUtil:

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
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
/**
* Http工具类
*/
public class HttpClientUtil {

static final int TIMEOUT_MSEC = 5 * 1000;

/**
* 发送GET方式请求
*
* @param url
* @param paramMap
* @return
*/
public static String doGet(String url, Map<String, String> paramMap) {
// 创建Httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();

String result = "";
CloseableHttpResponse response = null;

try {
URIBuilder builder = new URIBuilder(url);
if (paramMap != null) {
for (String key : paramMap.keySet()) {
builder.addParameter(key, paramMap.get(key));
}
}
URI uri = builder.build();

//创建GET请求
HttpGet httpGet = new HttpGet(uri);

//发送请求
response = httpClient.execute(httpGet);

//判断响应状态
if (response.getStatusLine().getStatusCode() == 200) {
result = EntityUtils.toString(response.getEntity(), "UTF-8");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
response.close();
httpClient.close();
} catch (IOException e) {
e.printStackTrace();
}
}

return result;
}

/**
* 发送POST方式请求
*
* @param url
* @param paramMap
* @return
* @throws IOException
*/
public static String doPost(String url, Map<String, String> paramMap) throws IOException {
// 创建Httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
CloseableHttpResponse response = null;
String resultString = "";

try {
// 创建Http Post请求
HttpPost httpPost = new HttpPost(url);

// 创建参数列表
if (paramMap != null) {
List<NameValuePair> paramList = new ArrayList();
for (Map.Entry<String, String> param : paramMap.entrySet()) {
paramList.add(new BasicNameValuePair(param.getKey(), param.getValue()));
}
// 模拟表单
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(paramList);
httpPost.setEntity(entity);
}

httpPost.setConfig(builderRequestConfig());

// 执行http请求
response = httpClient.execute(httpPost);

resultString = EntityUtils.toString(response.getEntity(), "UTF-8");
} catch (Exception e) {
throw e;
} finally {
try {
response.close();
} catch (IOException e) {
e.printStackTrace();
}
}

return resultString;
}

/**
* 发送POST方式请求
*
* @param url
* @param paramMap
* @return
* @throws IOException
*/
public static String doPost4Json(String url, Map<String, String> paramMap) throws IOException {
// 创建Httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
CloseableHttpResponse response = null;
String resultString = "";

try {
// 创建Http Post请求
HttpPost httpPost = new HttpPost(url);

if (paramMap != null) {
//构造json格式数据
JSONObject jsonObject = new JSONObject();
for (Map.Entry<String, String> param : paramMap.entrySet()) {
jsonObject.put(param.getKey(), param.getValue());
}
StringEntity entity = new StringEntity(jsonObject.toString(), "utf-8");
//设置请求编码
entity.setContentEncoding("utf-8");
//设置数据类型
entity.setContentType("application/json");
httpPost.setEntity(entity);
}

httpPost.setConfig(builderRequestConfig());

// 执行http请求
response = httpClient.execute(httpPost);

resultString = EntityUtils.toString(response.getEntity(), "UTF-8");
} catch (Exception e) {
throw e;
} finally {
try {
response.close();
} catch (IOException e) {
e.printStackTrace();
}
}

return resultString;
}

private static RequestConfig builderRequestConfig() {
return RequestConfig.custom()
.setConnectTimeout(TIMEOUT_MSEC)
.setConnectionRequestTimeout(TIMEOUT_MSEC)
.setSocketTimeout(TIMEOUT_MSEC).build();
}

}

HTTP GET请求,处理查询参数,并返回响应内容。它使用了Apache HttpClient库来执行HTTP请求,并确保在请求完成后正确关闭资源。

JWT:

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
@Slf4j
public class JwtUtil {
/**
* 生成jwt
* 使用Hs256算法, 私匙使用固定秘钥
*
* @param secretKey jwt秘钥
* @param ttlMillis jwt过期时间(毫秒)
* @param claims 设置的信息
* @return
*/
public static String createJWT(String secretKey, long ttlMillis, Map<String, Object> claims) {
// 1. header:指定签名的时候使用的签名算法,至于token名称-那不就是JWT嘛!
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
// 生成JWT的时间
long expMillis = System.currentTimeMillis() + ttlMillis;
Date exp = new Date(expMillis);
// 设置jwt的 payload 和 signature
JwtBuilder builder = Jwts.builder()
// 2. payload: 如果有私有声明,一定要先设置这个自己创建的私有的声明
// 这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
.setClaims(claims)
// 3. signature: 设置签名使用的签名算法和签名使用的秘钥
.signWith(signatureAlgorithm, secretKey.getBytes(StandardCharsets.UTF_8))
// 设置过期时间
.setExpiration(exp);
// 至于转成base64,这个Jwts.builder会自动帮我们做的
// 信息整合构建好,生成JWT字符串并返回
return builder.compact();
}

/**
* Token解密
*
* @param secretKey jwt秘钥 此秘钥一定要保留好在服务端, 不能暴露出去, 否则sign就可以被伪造, 如果对接多个客户端建议改造成多个
* @param token 加密后的token
* @return
*/
public static Claims parseJWT(String secretKey, String token) {
// 得到DefaultJwtParser
log.info("来到这里校验token是否一致");
System.out.println(token);
Claims claims = Jwts.parser()
// 设置签名的秘钥
.setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8))
// 设置需要解析的jwt
.parseClaimsJws(token).getBody();
System.out.println("claims " + claims);
return claims;
}
}
  • JWT(JSON Web Token)生成Token的基本流程可以总结如下:
  1. 定义Header:

指定签名算法,例如HS256。
2. 生成过期时间:

计算当前时间加上过期时间(以毫秒为单位),得到Token的过期时间。
3. 构建JWT:

使用Jwts.builder()创建一个JWT构建器。
设置Payload(Claims):将自定义的声明(claims)添加到JWT中。
设置签名:使用指定的签名算法和秘钥对JWT进行签名。
设置过期时间:将计算得到的过期时间添加到JWT中。
生成JWT字符串:

调用builder.compact()方法生成最终的JWT字符串。

Config

RedisConfiguration:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

@Configuration
@Slf4j
public class RedisConfiguration {

@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
log.info("开始创建redis模板对象...");
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
// 设置redis key的序列化器
redisTemplate.setKeySerializer(new StringRedisSerializer());
return redisTemplate;
}
}
  • RedisTemplate 是 Spring Data Redis 提供的一个核心类,用于在 Spring 应用中与 Redis 进行交互。它封装了对 Redis 的各种操作,如字符串操作、列表操作、集合操作、哈希操作等,使得开发者可以更方便地使用 Redis 作为数据存储和缓存解决方案。

  • 主要功能和特点

  1. 简化操作:RedisTemplate 提供了一系列简化的方法,如 opsForValue()、opsForList()、opsForSet() 等,用于处理不同类型的 Redis 数据结构。
  2. 序列化支持:RedisTemplate 支持自定义序列化器,可以将 Java 对象序列化为 Redis 存储的字节流,或者将字节流反序列化为 Java 对象。
  3. 线程安全:RedisTemplate 是线程安全的,可以在多个线程中共享使用。
  4. 事务支持:RedisTemplate 支持 Redis 事务,可以通过 multi()、exec() 等方法实现事务操作。
  5. 连接管理:RedisTemplate 内部管理 Redis 连接,开发者无需手动管理连接的创建和释放。

WebMvcConfiguration:

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
@Configuration
@Slf4j
public class WebMvcConfiguration extends WebMvcConfigurationSupport {

@Autowired
private JwtTokenAdminInterceptor jwtTokenAdminInterceptor;
@Autowired
private JwtTokenUserInterceptor jwtTokenUserInterceptor;

/**
* 配置,添加自定义拦截器
* @param registry
*/
public void addInterceptors(InterceptorRegistry registry){
log.info("自定义好了拦截器,还要在这个WebMvc配置类里注册");
registry.addInterceptor(jwtTokenAdminInterceptor)
.addPathPatterns("/admin/**")
.excludePathPatterns("/admin/employee/login")
.excludePathPatterns("/admin/employee/register");
registry.addInterceptor(jwtTokenUserInterceptor)
.addPathPatterns("/user/**")
.excludePathPatterns("/user/user/login")
.excludePathPatterns("/user/shop/status");
}

/**
* 扩展Spring MVC框架的消息转化器,用于格式化时间等
* @param converters
*/
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
log.info("扩展消息转换器...");
//创建一个消息转换器对象
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
//需要为消息转换器设置一个对象转换器,对象转换器可以将Java对象序列化为json数据
converter.setObjectMapper(new JacksonObjectMapper());
//将自己的消息转化器加入容器中
converters.add(0,converter);
}
}
  • 配置自定义拦截器:通过addInterceptors方法注册了两个JWT令牌验证拦截器,分别用于管理员和用户的路径拦截和排除
  • 扩展消息转换器:通过extendMessageConverters方法扩展了Spring MVC的消息转换器,使用自定义的JacksonObjectMapper来处理Java对象与JSON数据之间的转换,并确保自定义的消息转换器优先使用。

展示的列表

数据模型:
categoryList:这个数组包含了所有的分类信息,每个分类都有一个id和name属性。
activeIndex:这是一个变量,用来追踪当前左侧分类列表中哪个分类是被激活的(即用户当前选中的分类)。
dishList:这个数组包含了当前选中分类下的所有菜品或套餐信息。

左侧分类列表:
使用v-for指令循环categoryList,为每个分类创建一个视图。
通过:class=”{active: index === activeIndex}”动态绑定active类,以突出显示当前激活的分类。
@tap=”getDishOrSetmealList(index)”:当用户点击某个分类时,会触发getDishOrSetmealList方法,并传递该分类的索引。

右侧菜品/套餐列表:
使用v-for指令循环dishList,为每个菜品或套餐创建一个视图。
:url属性动态生成跳转到详情页的链接,根据分类的sort属性决定是传递dishId还是setmealId。
方法getDishOrSetmealList:
这个方法接收一个参数index,即用户点击的分类的索引。
当该方法被触发时,它应该更新activeIndex的值,使其等于index。
同时,该方法会根据categoryList[activeIndex]中的分类信息,去获取该分类下的所有菜品或套餐,并更新dishList。

时间

arrivalTime.value = hours + ‘:’ + minutes.toString().padStart(2, ‘0’);

这行代码的作用是将arrivalTime的值设置为格式化后的时间字符串,确保分钟部分始终是两位数。具体解释如下:

代码解释
arrivalTime.value = hours + ‘:’ + minutes.toString().padStart(2, ‘0’);
CopyInsert
hours:表示小时部分,是一个整数。
minutes:表示分钟部分,是一个整数。
minutes.toString():将分钟部分转换为字符串。
padStart(2, ‘0’):确保分钟部分的字符串长度为2,如果不足2位,则在前面补0。
hours + ‘:’ + minutes.toString().padStart(2, ‘0’):将小时和分钟部分拼接成一个时间字符串,格式为HH:MM。

微信支付

payOrderAPI 是一个用于处理支付请求的API调用函数。以下是关于payOrderAPI的详细解释:

功能
payOrderAPI 的主要功能是向服务器发送支付请求,并更新订单的支付状态。

实现细节
在代码中,payOrderAPI 的实现如下:

const toSuccess = async () => {
// 若订单已超时,跳转到订单已取消页面
if (countdownStore.showM == -1 && countdownStore.showS == -1) {
uni.redirectTo({
url: ‘/pages/orderDetail/orderDetail?orderId=’ + orderId.value,
})
return
}
console.log(‘支付成功’)
// 支付后修改订单状态
const payDTO = {
orderNumber: orderNumber.value,
payMethod: 1, // 本平台默认微信支付
}
await payOrderAPI(payDTO)
// 关闭定时器
if (countdownStore.timer !== undefined) {
clearInterval(countdownStore.timer)
countdownStore.timer = undefined
}
uni.redirectTo({
url:
‘/pages/submit/success?orderId=’ +
orderId.value +
‘&orderNumber=’ +
orderNumber.value +
‘&orderAmount=’ +
orderAmount.value +
‘&orderTime=’ +
orderTime.value,
})
}
CopyInsert
参数
payOrderAPI 接受一个参数对象 payDTO,包含以下字段:

orderNumber:订单号。
payMethod:支付方式,这里默认是微信支付(值为1)。
调用方式
在 toSuccess 函数中调用 payOrderAPI:

const payDTO = {
orderNumber: orderNumber.value,
payMethod: 1, // 本平台默认微信支付
}
await payOrderAPI(payDTO)
CopyInsert
作用
发送支付请求:

payOrderAPI 向服务器发送支付请求,更新订单的支付状态。
关闭定时器:

在支付成功后,关闭倒计时定时器,确保不再继续倒计时。
跳转到支付成功页面:

支付成功后,跳转到支付成功页面,显示支付成功的信息。
总结
payOrderAPI 是一个用于处理支付请求的API调用函数,主要功能是向服务器发送支付请求并更新订单的支付状态。支付成功后,关闭倒计时定时器并跳转到支付成功页面。

请求的封装

这段代码是uni-app框架中的一个网络请求封装函数,以下是代码的重要部分及其解释:

泛型函数定义:
export const http = (options: UniApp.RequestOptions) => { … }
http 是一个导出的常量,表示它可以在其他文件中被导入并使用。
是一个泛型标记,它允许函数返回的数据类型根据调用时传入的类型参数来确定。
(options: UniApp.RequestOptions) 表示函数接收一个参数 options,该参数的类型是 UniApp.RequestOptions,这是uni-app定义的网络请求配置类型。
Promise封装:
return new Promise<Data>((resolve, reject) => { … })
返回一个新的 Promise 对象,这个 Promise 的解决值类型是 Data,这是自定义的泛型接口,表示响应数据的结构。
(resolve, reject) 是Promise的执行器函数,resolve 用于在异步操作成功时解决Promise,reject 用于在操作失败时拒绝Promise。
uni.request调用:
uni.request({ …options, success: (res) => { … }, fail: (err) => { … } })
调用uni-app的 uni.request 方法来发送网络请求。
{ …options } 是将传入的请求配置展开,这样可以直接使用传入的配置。
success: (res) => { … } 是请求成功的回调函数,其中 res 是响应数据。
fail: (err) => { … } 是请求失败的回调函数,其中 err 是错误信息。
响应成功处理:
if (res.statusCode >= 200 && res.statusCode < 300) { … }
检查响应的状态码,如果是在200到299之间,则认为请求成功。
resolve(res.data as Data) 使用类型断言将 res.data 断言为 Data 类型,并调用 resolve 解决Promise。
401错误处理:
else if (res.statusCode === 401) { … }
如果状态码是401,表示未授权(通常是token过期)。
清理用户信息 userStore.clearProfile() 并跳转到登录页面 uni.navigateTo({ url: ‘/pages/login/login’ })。
调用 reject(res) 拒绝Promise。
其他错误处理:
else { … }
对于其他错误状态码,使用 uni.showToast 显示错误信息。
调用 reject 拒绝Promise,但在这种情况下没有显式调用,因为该分支仅用于显示错误信息。
响应失败处理:
fail: (err) => { … }
如果请求过程中发生网络错误或其他异常,则执行此回调。
使用 uni.showToast 显示网络错误信息。
调用 reject(err) 拒绝Promise。
这段代码的关键在于它封装了uni-app的网络请求,并提供了错误处理和Promise风格的异步操作,使得在组件或页面中调用网络请求更加方便和一致。

dto vo entity

使用场景
Entity的使用:

在数据库操作中,使用User、Order和Product实体来表示数据库中的记录。
例如,通过User实体来查询用户信息,通过Order实体来查询订单信息。
DTO的使用:

在Web服务中,客户端和服务器之间传输数据时使用UserDTO、OrderDTO和ProductDTO。
例如,客户端请求用户信息时,服务器返回UserDTO对象。
VO的使用:

在业务逻辑中,使用MoneyVO来表示货币金额和货币类型。
例如,计算订单总金额时,使用MoneyVO来封装金额和货币类型。
总结
Entity:用于持久化对象,包含业务逻辑和持久化操作。
DTO:用于数据传输,减少网络调用。
VO:用于表示不可变的值对象,封装相关属性。
通过这个例子,可以看到DTO、VO和Entity在不同场景下的具体应用和区别。

后端的处理

后端进来: 配置类 -> 拦截器 -> controller -> service -> serviceImpl ->mapper -> mapper.xml

微信登录

逐步分解并详细解释代码

  1. 类定义与依赖注入
    public class UserServiceImpl implements UserService {

    // 微信服务接口地址
    public static final String WX_LOGIN = “https://api.weixin.qq.com/sns/jscode2session“;

    @Autowired
    private WeChatProperties weChatProperties;
    @Autowired
    private UserMapper userMapper;

CopyInsert
UserServiceImpl类实现了UserService接口。
WX_LOGIN是一个静态常量,存储了微信登录接口的URL。
weChatProperties和userMapper是通过Spring的依赖注入机制注入的,分别用于获取微信配置和操作数据库。
2. 用户微信登录方法
/**

  • 用户微信登录
  • @param userLoginDTO
  • @return
    */
    public User wxLogin(UserLoginDTO userLoginDTO) {
    CopyInsert
    wxLogin方法用于处理用户的微信登录请求。
    方法参数userLoginDTO是一个数据传输对象,包含了用户登录所需的信息。
  1. 获取OpenID
    // 调用私有方法,其中利用HttpClient来调用微信API服务,获取openid
    String openid = getOpenId(userLoginDTO.getCode());
    CopyInsert
    调用私有方法getOpenId,传入userLoginDTO中的code参数,获取用户的OpenID。
  2. 检查OpenID是否为空
    // 判断openid是否为空,如果为空表示登录失败,抛出业务异常
    if (openid == null) {
    throw new LoginFailedException(MessageConstant.LOGIN_FAILED);
    }
    CopyInsert
    检查获取到的OpenID是否为空。如果为空,表示登录失败,抛出LoginFailedException异常。
  3. 检查用户是否为新用户
    // 判断当前用户是否为新用户
    User user = userMapper.getByOpenid(openid);
    CopyInsert
    通过userMapper查询数据库,检查是否存在该OpenID对应的用户。
  4. 新用户自动注册
    // 如果是新用户,自动完成注册,插入到数据库
    if (user == null) {
    user = User.builder()
    .openid(openid)
    .createTime(LocalDateTime.now())
    .build();
    userMapper.insert(user);
    }
    CopyInsert
    如果用户不存在(即user为null),则创建一个新的User对象,并设置OpenID和创建时间,然后插入到数据库中。
  5. 返回用户对象
    return user;
    }
    CopyInsert
    最后,返回用户对象。
    代码总结
    这段代码的主要功能是处理用户的微信登录请求。具体步骤如下:

通过调用微信API获取用户的OpenID。
检查OpenID是否为空,如果为空则抛出登录失败异常。
检查数据库中是否存在该OpenID对应的用户。
如果用户不存在,则自动完成注册并插入到数据库中。
返回用户对象。
通过这些步骤,代码实现了微信登录功能,并确保新用户能够自动注册。

运维:

买服务器 -> 服务器备案 -> 购买域名 -> 域名审核 -> 进入服务器后台 -> 安装宝塔面板 -> 部署流程 -> 绑定域名 -> 域名安全认证 ->
微信小程序绑定安全域名 -> 进入宝塔面板 -> 放开端口 -> 进入阿里云控制台 -> 放开端口