苍穹外卖项目复盘(全)
提高访问速度(可以进行缓存,如果访问相同资源可以直接响应数据)负载均衡保证后端服务安全(前端不能直接请求到后端服务器,需要通过Nginx转发)六种负载均衡策略,默认为轮询1.2.nginx有默认上传文件大小限制,在http中可以配置更改#上传文件的大小限制默认1mServer块详细介绍。
nginx反向代理
1.nginx反向代理的好处:
-
提高访问速度(可以进行缓存,如果访问相同资源可以直接响应数据)
-
负载均衡
-
保证后端服务安全(前端不能直接请求到后端服务器,需要通过Nginx转发)
2.nginx反向代理的搭建:

3.nginx负载均衡的配置:

4. nginx负载均衡的策略:
六种负载均衡策略,默认为轮询

补充
1.nginx目录结构

2.Nginx配置文件结构

nginx有默认上传文件大小限制,在http中可以配置更改
#上传文件的大小限制 默认1m client_max_body_size 8m;
Server块详细介绍

nginx实现反向代理


后端环境搭建
项目框架



登录密码加密
调用Spring提供的DigestUtils类中的md5DigestAsHex方法对密码进行加密转换
password = DigestUtils.md5DigestAsHex(password.getBytes());
JWT使用流程
用户向服务器发送登录请求,服务器进行身份验证,如果验证成功则返回一个JWT令牌给客户端。
客户端收到JWT令牌后,将其保存在本地。每次向服务器发送请求时,在请求的头部中携带该令牌,以便服务器对请求进行身份验证。
服务器收到请求后,从请求头中提取JWT令牌,并进行解析和验证。如果令牌有效,则允许请求继续执行;否则返回错误信息。

在服务端拦截所有的请求,判断算法有合法的jwt请求,如果有,直接放行,否则进行拦截
JWT(JSON Web Token)令牌的优势
1.无状态
-
传统 Session 方式:服务器需要存储 Session ID,并在每次请求时查询数据库或缓存来验证用户身份。
-
JWT 方式:服务器无需存储 Token,只需验证 Token 的签名和有效期即可。
-
跨域/跨服务支持
-
传统方式:Cookie 受同源策略限制,跨域访问需要额外配置(如 CORS)。
-
JWT 方式:Token 可以放在 HTTP Header(如
Authorization: Bearer <token>)或 URL 中,不受同源策略影响。
-
自包含(Self-contained)
-
JWT 的组成:
Header.Payload.Signature,其中Payload可以存储用户 ID、角色、权限等信息。
mybatis开启驼峰命名
mybatis:
#开启驼峰命名
map-underscore-to-camel-case: true
获取当前登录用户信息
ThreadLocal,它是Thread的局部变量,为每个线程提供单独一份的存储空间,具有线程隔离的效果
-
先在拦截器JwtTokenAdminInterceptor里将ID存到存储空间里(set)
-
在需要的地方通过(get)得到
PageHelper帮助mybatis实现分页
PageHelper的startPage方法可以通过传入的参数自动设置Limit,传入的是页码和每页的记录数,好处是:字符串的拼接不用自己做。底层实现是:它会给ThreadLocal设置上述参数,然后在执行SQL语句时会自动被取出,然后拼接成Limit。
完善日期格式在前端的显示
方法一:在Employee实体类中的LocalDateTime属性上加上@JsonFormat注解,格式化时间。
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime updateTime;
方法二:拓展Spring MVC的消息转换器,统一对后端返回给前端的数据进行转换处理:
-
创建json处理的class工具类 (里面有关于日期时间的序列化和反序列化器)
/**
* 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象
* 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象]
* 从Java对象生成JSON的过程称为 [序列化Java对象到JSON]
*/
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_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm";
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);
}
/**
* 为什么需要它?
* 默认的 Jackson 不直接支持 LocalDateTime,需要手动配置格式,否则可能抛出异常或使用不直观的格式(如数组 [2023,10,1,14,30])。
*
* 统一格式化:确保所有 LocalDateTime 字段都按照 "yyyy-MM-dd HH:mm" 的格式处理,避免前端/后端解析不一致的问题。
*
* 如果有一个包含 LocalDateTime 的 Java 对象:
* public class Order {
* private LocalDateTime createTime;
* // getter & setter
* }
* 序列化成 JSON 时会自动格式化为 "createTime": "2023-10-01 14:30",而不是默认的 Jackson 格式(如数组形式 [2023, 10, 1, 14, 30])。
*/
}
-
在sky-server下的com/sky/config/WebMvcConfiguration下创建:
//托转Spring MVC框架的消息转换器
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
//先创建一个消息转换器对象
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
//为消息转换器设置一个对象转换器,对象转换器可以将Java对象序列化为json数据
converter.setObjectMapper(new JacksonObjectMapper());
//消息转换器还没交给框架,需要把消息转换器加到容器里
converters.add(0,converter); //容器自带消息转换器,默认新加的排在末尾,0表示是首位,自己加的消息转换器排在首位
公共字段填充
自定义注解 + AOP实现
1.自定义注解
//自定义注解,用于标识某个方法需要进行功能字段自动填充处理
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {
//数据库操作类型:UPDATE INSERT
OperationType value();
}
2.在com.sky下创建aspect包,创建类AutoFillAspect
在切面方法中通过反射为对应的属性赋值
//自定义切面,实现公共字段自动填充处理逻辑
@Aspect
@Component
@Slf4j
public class AutoFillAspect {
//切入点
@Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
public void autoFillPointCut(){}
//前置通知,在通知中进行公共字段的赋值
@Before("autoFillPointCut()")
public void autoFill(JoinPoint joinPoint){
log.info("开始进行公共字段自动填充...");
}
}
3.在mapper层的Insert和Update方法上加上@AutoFill注解,注解内容用OperationType.INSERT或OperationType.Update。
@AutoFill(value = OperationType.UPDATE ) void update(Employee employee);
切面方法具体实现
1.获取到当前被拦截的方法上的数据库操作类型(比如是Insert还是Update,不同的类型需要给不同的参数赋值)
MethodSignature signature = (MethodSignature) joinPoint.getSignature();//通过连接点对象来获取签名,向下转型为MethodSignature AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class);//获得方法上的注解对象 OperationType operationType = autoFill.value();//获得数据库操作类型(Insert or Update)
2.获取到当前被拦截的方法的参数--实体对象(比如传入的参数是员工还是菜品还是其它的)
Object[] args = joinPoint.getArgs(); //获得了方法所有的参数
if(args == null || args.length==0 ){ //没有参数
return;
}
Object entity = args[0];//现在约定实体放在第1个位置,传入实体可能不同所以用Object
3.准备赋值的数据(给公共字段赋值的数据,比如时间就是系统时间,用户ID是从ThreadLocal获取)
4.根据当前不同的操作类型,为对应的属性通过反射来赋值
LocalDateTime now = LocalDateTime.now();
Long currentId = BaseContext.getCurrentId();
if(operationType == OperationType.INSERT){
//为4个公共字段赋值
try {
Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class); //把方法名全部换成常量类,防止写错
Method setCreateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class);
Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
//4.根据当前不同的操作类型,为对应的属性通过反射来赋值
setCreateTime.invoke(entity,now);
setCreateUser.invoke(entity,currentId);
setUpdateTime.invoke(entity,now);
setUpdateUser.invoke(entity,currentId);
}catch (Exception e){
e.printStackTrace();
}
}else if(operationType == OperationType.UPDATE){
try {
//为2个公共字段赋值
Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
//4.根据当前不同的操作类型,为对应的属性通过反射来赋值
setUpdateTime.invoke(entity, now);
setUpdateUser.invoke(entity, currentId);
}catch (Exception e){
e.printStackTrace();
}
}
}
补充
反射的使用:
反射第一步获取class字节码文件,然后通过class字节码文件获取字段、构造方法或成员方法,从而进行解剖

-
运行方法时,先new一个对象
-
然后通过获取的字class对象m , m.invoke(s , "汉堡包")

HttpClient
在Java中通过编码的方式发送HTTP请求


在java程序中也可以使用hutool工具包封装的HttpUtil工具包
发送GET请求
Map<String, Object> requestMap = new HashMap<>();
requestMap.put("current", requestParam.getCurrent());
requestMap.put("size", requestParam.getSize());
String resultPageStr = HttpUtil.get("http://127.0.0.1:8001/api/short-link/v1/page", requestMap);
发送POST请求
HttpUtil.post("http://127.0.0.1:8001/api/short-link/v1/update", JSON.toJSONString(requestParam));
使用使用MyBatis插入数据并返回自动生成的ID
以便满足后续在关联数据表插入数据时需要此ID
1.方法一
<insert id="insert" parameterType="com.sky.entity.Setmeal" useGeneratedKeys="true" keyProperty="id">
insert into setmeal (category_id, name, price, status, description, image, create_time, update_time, create_user, update_user
) values (#{categoryId}, #{name}, #{price}, #{status}, #{description}, #{image}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser})
</insert>
-
useGeneratedKeys 属性 作用: 用于告诉MyBatis是否使用数据库自动增长的主键。 当设置为true时,表示使用数据库自动生成的主键值。
-
keyProperty 属性 作用: 用于指定将自动生成的主键值赋值给Java对象的哪个属性。 在插入数据后,自动生成的主键值将会存储在指定的属性中。
需要指定插入的类型parameterType="com.sky.entity.Setmeal"
对应的mysql语句返回值设置为void即可
void insert(Setmeal setmeal);
serviceImpl中的使用方式
Setmeal setmeal = new Setmeal();
。。。
setmealMapper.insert(setmeal);
//得到插入返回的id
Long id = setmeal.getId();
2.方法二

SpringCache介绍
SpringCache是Spring提供的缓存框架。提供了基于注解的缓存功能。
SpringCache提供了一层抽象,底层可以切换不同的缓存实现(只需要导入不同的Jar包即可),如EHCache,Caffeine,Redis。

依赖导入
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
在启动类上加@EnableCaching
在controller层的UserController下的save方法上写入如下的代码:
@PostMapping
@CachePut(cacheNames="userCache",key="#user.id") //如果使用spring Cache缓存数据,key的生成:userCache::1。user是从参数取到的。
//@CachePut(cacheNames="userCache",key="#result.id") //result是从返回值return取到的
//@CachePut(cacheNames="userCache",key="#p0.id")
//@CachePut(cacheNames="userCache",key="#a0.id")
//@CachePut(cacheNames="userCache",key="#root.args[0].id")
public User save(@RequestBody User user){
userMapper.insert(user);
return user;
}
注意key="#result.id"中的result取的是返回值返回的那个结果。 key="#user.id"的user取的是传入的参数。p0,a0,root.args[0]表示取的都是第1个参数。
在controller层的UserController下的getById方法上写入如下的代码:
-
@Cacheable和@CachePut的区别:一个在方法执行前执行,一个在方法执行后执行
@Cacheable(cacheNames="userCache",key="#id") //key的形式 userCache::10
-
在controller层的UserController下的deleteById和deleteAll方法上加入如下注解:
@DeleteMapping
@CacheEvict(cacheNames = "userCache",key="#id") //key的形式 userCache::10
public void deleteById(Long id){userMapper.deleteById(id);}
@DeleteMapping("/delAll")
@CacheEvict(cacheNames="userCache",allEntries = true)
public void deleteAll(){
userMapper.deleteAll();
}
Spring Task
Spring Task是Spring框架提供的任务调度工具,可以按照约定的时间自动执行某个代码逻辑。
cron表达式是一个字符串,通过cron表达式可以定义任务触发的时间。
构成规则:分为6或7个域,由空格分隔开,每个域代表一个含义。
每个域的含义分别为:秒、分钟、小时、日、月、周、年(可选)

cron表达式可以上在线Cron表达式生成器生成。
生成工具:在线Cron表达式生成器-奇Q工具网 (qqe2.com)
使用
①导入maven坐标,spring-context(已存在)
②启动类添加注解@EnableScheduling开启任务调度
③自定义定时任务类
//处理超时订单的方法
@Scheduled(cron="0 * * * * ? ")
public void processTimeoutOrder(){
log.info("定时处理超时订单:{}", LocalDateTime.now());
LocalDateTime time = LocalDateTime.now().plusMinutes(-15);
//select * from orders status = ? and order_time < (当前时间 - 15分钟)
List<Orders> ordersList = orderMapper.getByStatusAndOrderTimeLT(Orders.PENDING_PAYMENT, time);
if(ordersList != null && ordersList.size()>0){
for(Orders orders : ordersList){
orders.setStatus(Orders.CANCELLED);
orders.setCancelReason("订单超时,自动取消");
orders.setCancelTime(LocalDateTime.now());
orderMapper.update(orders);
}
}
}
WebSocket
WebSocket是基于TCP的一种新的网络协议。它实现了浏览器域服务器全双工通信——浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的链接,并进行双向数据传输。
HTTP是短连接,是单向的,基于请求响应模式;WebSocket是长连接(有点像打电话,双向消息),支持双向通信。HTTP和WebSocket底层都是TCP连接。
应用:视频弹幕,网页聊天(聊天窗口和客服聊天),体育实况更新,股票基金报价实时更新。
使用步骤:
-
导入WebSocket的maven坐标
-
导入WebSocket服务端组件WebSocketServer,用于和客户端通信
-
导入配置类WebSocketConfiguration,注册WebSocket的服务端组件
-
在service层中调用webSocketServer.sendToAllClient(json)来向客户端发送消息
WebSocketServe代码编写
通过sid来区分不同的客户端。加入@OnOpen注解,就变成了回调方法。加入@OnMessage注解,收到客户端的消息后会调这个方法。
/**
* WebSocket服务
*/
@Component
@ServerEndpoint("/ws/{sid}")
public class WebSocketServer {
// 存放会话对象
private static Map<String, Session> sessionMap = new HashMap<>();
/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session, @PathParam("sid") String sid) {
System.out.println("客户端:" + sid + "建立连接");
sessionMap.put(sid, session);
}
/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message, @PathParam("sid") String sid) {
System.out.println("收到来自客户端:" + sid + "的信息:" + message);
}
/**
* 连接关闭调用的方法
*
* @param sid
*/
@OnClose
public void onClose(@PathParam("sid") String sid) {
System.out.println("连接断开:" + sid);
sessionMap.remove(sid);
}
/**
* 群发
*
* @param message
*/
public void sendToAllClient(String message) {
Collection<Session> sessions = sessionMap.values();
for (Session session : sessions) {
try {
// 服务器向客户端发送消息
session.getBasicRemote().sendText(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
Apache ECharts
Apache ECharts -> 营业额统计 -> 用户统计 ->订单统计 ->销量排名统计top10

Apache POI
一般情况下,POI都是用于操作Excel文件。

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。
更多推荐

所有评论(0)