# 项目介绍

# 产品定位

为餐饮企业定制的一款软件产品

# 产品架构

# 管理端

  • 员工管理
  • 分类管理
  • 菜品管理
  • 套餐管理
  • 订单管理
  • 工作台
  • 数据统计
  • 来单提醒

# 用户端

  • 微信登录
  • 商品浏览
  • 购物车
  • 用户下单
  • 微信支付
  • 历史订单
  • 地址管理
  • 用户催单

# 技术选型

# 用户层

  • node.js
  • VUE.js
  • ElementUI
  • 微信小程序
  • apache echarts

# 网关层

  • Nginx

# 应用层

  • Spring Boot
  • Spring MVC
  • Spring Task
  • httpclient
  • Spring Cache
  • JWT
  • 阿里云 OSS
  • Swagger
  • POI
  • WebSocket

# 数据层

  • MySQL
  • Redis
  • mybatis
  • pagehelper
  • spring data redis

# 具体实现

# 完善登录功能

  1. 修改数据库中的密码,改为 MD5 加密后的密文
  2. 修改 JAVA 代码,前端提交的密码进行 MD5 加密后再跟数据库中的密码对比
  3. 抛异常使用消息常量类

# swagger 的使用

swagger 可以方便的进行前后端联调,由于该项目仅涉及两位成员且沟通方便故未使用,仅学习了解过如何使用

# 新增员工

前端提交的数据与实体类差别较大,故采用 DTO 来封装数据

实体类(Entity)
与数据库表对应:实体类通常直接映射到数据库表,包含所有字段,便于持久化操作。
封装业务逻辑:实体类不仅仅是数据的载体,还可以包含业务逻辑和数据状态。
简化数据库操作:通过 ORM 框架(如 Hibernate),实体类可以简化数据库的增删改查操作。
DTO(数据传输对象)
数据传输优化:DTO 主要用于在不同层(如控制层和服务层)之间传输数据。它可以只包含需要传输的字段,减少不必要的数据传输,提高性能。
安全性:通过 DTO,可以避免直接暴露实体类中的敏感数据,增强系统的安全性。
解耦前后端:DTO 可以根据前端需求定制,避免前端直接依赖实体类,从而实现前后端的解耦。
简化数据转换:在复杂业务场景中,DTO 可以简化数据转换逻辑,避免在多个地方重复编写转换代码。
使用场景
实体类:适用于需要直接与数据库交互的场景,如 DAO 层。
DTO:适用于需要在不同层之间传输数据的场景,如控制层与服务层之间的数据传输。

# 存储数据

无须逐个设置实体对象属性,而是使用对象属性拷贝 ( BeanUtils.copyProperties ), 再设置其他 DTO 中不存在的属性

# 插入数据

@Insert("insert into employee(name, username, password, phone) values(#{name},#{username},#{password},#{phone})")
void insert(Employee employee)

MyBatis 提供了多种注解来简化 SQL 操作,避免了繁琐的 XML 配置。以下是一些常用的 MyBatis 注解及其用途:

常用注解

@Select:用于执行查询操作。
@Select("SELECT * FROM employee WHERE id = #{id}")
Employee selectById(int id);

@Insert:用于执行插入操作。
@Insert("INSERT INTO employee(name, username, password, phone) VALUES(#{name}, #{username}, #{password}, #{phone})")
void insert(Employee employee);

@Update:用于执行更新操作。
@Update("UPDATE employee SET name = #{name}, username = #{username}, password = #{password}, phone = #{phone} WHERE id = #{id}")
void update(Employee employee);

@Delete:用于执行删除操作。
@Delete("DELETE FROM employee WHERE id = #{id}")
void delete(int id);

@Results 和 @Result:用于映射查询结果到对象属性。
@Select("SELECT * FROM employee WHERE id = #{id}")
@Results({
    @Result(property = "id", column = "id"),
    @Result(property = "name", column = "name"),
    @Result(property = "username", column = "username"),
    @Result(property = "password", column = "password"),
    @Result(property = "phone", column = "phone")
})
Employee selectById(int id);

@One 和 @Many:用于一对一和一对多的关系映射。
@Select("SELECT * FROM department WHERE id = #{id}")
@Results({
    @Result(property = "id", column = "id"),
    @Result(property = "name", column = "name"),
    @Result(property = "employees", column = "id", 
            many = @Many(select = "selectEmployeesByDepartmentId"))
})
Department selectById(int id);

# 代码完善

# 录入的用户名已存在,抛出异常未处理

添加异常处理即可。

# 动态获取当前登录用户 id

这里我们使用 JWT 技术。

通过拦截器将 Token 令牌拦截,解析出用户 id,传给 save 方法。

但是拦截器中并未直接调用 save 方法,那么该如何将解析到的用户 id 传给 save 方法呢?

这里我们采用 ThreadLocal。

ThreadLocal

ThreadLocal 并不是一个 Thread, 而是 Thread 的局部变量。

ThreadLocal 为每个线程提供单独一份存储空间,具有线程隔离的效果,只有线程内才能获取到对应的值,线程外则不能访问。

通过观察 ThreadId 很容易发现,每一次请求都是一个线程,故可以封装上述 ThreadLocal , 添加 setId , getId , removeId 方法成一个工具类 BaseContext、

因此,在拦截器中将 Id 存入 ThreadLocal 然后在 save 方法中取出即可。

# 员工分页查询

# 业务规则

  • 根据页码展示员工信息
  • 每页展示 10 条数据
  • 分页查询时可以根据需要,输入员工的姓名进行查询

# 接口设计

  • 路径:
  • 请求方法:GET
  • 请求参数: namepagepagesize

# 分页查询

一般是通过基于 SQLlimit 关键字进行分页查询,如:

select * from employee limit 0,10

但是我们也可以使用分页查询插件,这里我使用的是 pagehelper , 它基于拦截器动态拼接字符串

PageHelper.startPage(employeePageQueryDTO.getPage(),employeePageQueryDTO.getPageSzie());
Page<employee> Page = employeeMapper.pageQuery(employeePageQueryDTO);

查询是模糊查询不是等值查询,故使用 like

# 代码完善

经测试,上述编写的代码存在数据格式的问题需要解决

  • 方式一:在属性上加入注解,对日期进行格式化

    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime updateTime;
    

    但上述方式只能对单一方法进行格式化,故推荐使用方式二统一格式化

  • 方式二:在 WebMvcConfiguration 中扩展 SpringMVC 的消息转换器,统一对日期类型进行格式化处理

    protected void extendMessageConverters(List<HttpMessageConverter<?>> converters){
        log.warning("开始扩展消息转换器...");
        //创建一个消息转化器对象
        MappingJackson2HttpmessageConverter converter = new MappingJackson2HttpMessageConverter();
        //设置对象转换器,可以将Java对象转为json字符串
        converter.setObjectmapper(new JacksonObjectMapper());
        //将我们自己的转换器放入spring MVC框架的容器中
        converters.add(0,converter);
    }
    

# 启用禁用员工账号

# 接口设计

  • 路径
  • 请求方式:POST
  • 请求参数:
    • Headers: contentType
    • 路径参数: status
    • Query: id
  • 返回数据: code , msg , data

# 编辑员工

# 公共字段自动填充

# 问题分析

业务表中有公共字段,逐个赋值非常麻烦,之后也难以修改,不便于后期维护

那么如何解决呢?

  • 自定义注解 AutoFill, 用于标识需要进行公共字段自动填充的方法
  • 自定义切面类 AutoFillAspect, 统一拦截加入了 AutoFill 注解的方法,通过反射为公共字段赋值
  • 在 Mapper 方法上加入 AutoFill 注解

技术点:枚举,注解,AOP, 反射

# 新增菜品

# 需求分析

  • 菜品必须是唯一的
  • 菜品必须在某个分类下,不能单独存在
  • 新增菜品时可根据情况选择菜品的口味
  • 每个菜品必须对应一张图片

# 接口设计

  • 根据类型查询分类 (已完成)
  • 文件上传
  • 新增菜品

# 文件上传

使用阿里云 oss 存储上传的二进制文件

使用 UUID 来重命名,防止出现重名的情况

新增菜品和口味时涉及两张表的操作

涉及多张表 -> 保证事务一致性 -> 添加注解 @Transactional

# 菜品分类查询

# 删除菜品

这里需要注意的是,如果菜品属于某一套餐,则不可随意删除,需要对菜品 - 口味关联表进行操作

# 修改菜品

# 接口设计

  • 根据 id 查询菜品
  • 根据类型查询分类 (已实现)
  • 文件上传 (已实现)
  • 修改菜品

# 具体实现

修改操作在技术层面上转化为先删除,再重新插入新的数据

# 店铺营业状态设置

# 使用技术

使用 Spring Data Redis 再 JAVA 中操作 Redis

  1. 导入 Spring Data Redismaven 坐标
  2. 配置 Redis 数据源
  3. 编写配置类, 创建 RedisTemple 对象
  4. 通过 RedisTemple 对象操作 Redis

# 需求分析

  • 设置营业状态
  • 管理端查询营业状态
  • 用户端查询营业状态
  • 营业状态数据存储方式:基于 Redis 的字符串来进行存储

# 微信登录

添加小程序代码 (由他人开发,类似前端,由 js 文件组成)

# 具体实现

  1. ​ 小程序使用 wx.login() 获取 code (授权码), 然后使用 wx.request 发送 code 到开发者服务器
  2. 开发者服务器获得 code 后使用 httpclientappid , appsecret , code 发送到微信接口
  3. 微信接口返回 session_keyopenid (微信用户唯一标识,然后用其生成一个令牌给小程序
  4. 最后小程序使用令牌来获取各种服务

# 商品与浏览功能

# 接口设计

  • 查询分类
  • 根据分类 id 查询菜品
  • 根据分类 id 查询套餐
  • 根据套餐 id 查询包含的菜品

# 缓存菜品

# 问题说明

用户端小程序展示的菜品都是通过查询数据库获得,如果用户访问量比较大,数据库访问压力随之增大。

# 实现思路

通过 Redis 来缓存菜品数据,减少数据库查询操作

# 缓存套餐

# Spring Cache

Spring Cache 是一个框架,实现了基于 注解 的缓存功能,只需简单地加一个注解,就能实现缓存功能

Spring Cache 提供了一层抽象,底层可以切换不同的缓存实现,例如:

  • EHCache
  • Caffeine
  • Redis

# 添加购物车

对购物车表添加冗余字段,这样就无须多表联合查询,增加速度

# 查看购物车

# 清空购物车

# 导入地址簿

# 业务功能

  • 查询地址列表
  • 新增地址
  • 修改地址
  • 删除地址
  • 设置默认地址
  • 查询默认地址

# 具体实现

订单与订单明细表是一对多的关系

  1. 处理各种业务异常 (地址簿为空 / 购物车为空)
  2. 向订单表插入一条数据
  3. 向订单表明细插入 n 条数据
  4. 清空当前用户的购物车数据
  5. 封装 VO 返回结果

# 微信支付

  1. 首先用户向商户系统 (即本项目) 发出下单请求
  2. 返回订单号
  3. 在向商户系统申请微信支付
  4. 商户系统调用微信下单接口,返回支付参数 (此时为预下单)
  5. 最后用户用支付参数调起微信支付
  6. 返回支付结果,同时商户系统获取支付结果并更新订单状态

# 订单状态定时处理

# Spring Task

Spring TaskSpring 框架提供的任务调度工具,可以按照 约定的时间自动执行某个代码逻辑

# cron 表达式

cron 表达式其实就是一个字符串,通过 cron 表达式可以 定义任务触发的时间

构成规则:分为 67 个域,由空格分隔开,每个域代表一个含义 (秒 / 分钟 / 小时 / 日 / 月 / 周 / 年 (可选))

在线生成 cron 表达式

在线 Cron 表达式生成器 (qqe2.com)

# 需求分析

问题:

  • 用户下单后未支付,订单一直处于 "待支付" 状态
  • 用户收货后管理端未点击完成按钮,订单一直处于 "派送中" 状态

解决:

  • 通过定时任务 每分钟检查一次 是否存在超时订单 (下单后超过 15 分钟未支付), 如果则存在修改为 "已取消"
  • 通过定时任务 每天凌晨检查一次 是否存在 "派送中" 的订单,如果存在则修改为 "已完成"

# 来单提醒

# WebSocket

WebSocket 是基于 TCP 的一种新的网络协议。它实现了浏览器与服务器全双工通信 —— 浏览器和服务器只需要完成一次握手,两者之间就可以创建 持久性 的连接,并进行 双向 数据传输。

# 需求分析

用户下单并且支付成功后,需要第一时间通知外卖商家。通知形式有如下两种:

  • 语言播报
  • 弹出提示框

设计:

  • 通过 WebSocket 实现管理端页面和服务端保持长连接状态
  • 当客户支付后,调用 WebSocket 的相关 API 实现服务端向客户端推送消息
  • 客户端浏览器解析服务端推送的消息,判断是来单提醒还是客户催单,进行相应的消息提示和语音播报
  • 约定服务端发送给客户端浏览器的数据格式为 JSON , 字段包括: type , orderId , content
    • type 为消息类型, 1 为来单提醒, 2 为客户催单
    • orderId 为订单 id
    • content 为消息内容

# 营业额统计

# Apache ECharts

使用 ECharts , 重点在于研究当前图表所需的 数据格式 。通常是需要后端提供符合格式要求的动态数据,然后响应给前端来展示图表。

# 用户统计

统计用户数量

# 订单统计

# 销量排名统计

# 工作台

工作台是系统运营的看板,并提供快捷操作入口,可以有效提高商家的工作效率

工作台展示的数据

  • 今日数据
  • 订单管理
  • 菜品总览
  • 套餐总览
  • 订单信息

每组数据对应一个接口,减少代码耦合性

# 导出运营数据 Excel 报表

# Apache POI

Apache POI 是一个处理 Microsoft Office 各种文件格式的开源项目。简单来说就是,我们可以使用 POIJava 程序中对 Microsoft Office 各种文件进行读写操作。

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

应用场景:

  • 银行网银系统导出交易明细
  • 各种业务系统导出 Excel 报表
  • 批量导入业务数据

# 需求分析

  • 导出 Excel 形式的报表文件
  • 导出最近 30 天的运营数据

当前接口没有返回数据,因为报表导出的本质是文件下载

服务端会通过输出流将 Excel 文件下载到客户端浏览器

# 亮点难点

# Nginx 反向代理和负载均衡

Nginx 是一款轻量级的 web 服务器,反向代理服务器、电子邮件代理服务器,特点是占有内存少、并发能力强。Nginx 反向代理就是将前端发送的动态请求由 Nginx 转发到后端服务器,负载均衡从本质上来说也是基于反向代理来实现的,最终都是转发请求。

Nginx 反向代理的好处:

  • 提高访问速度:因为 nginx 本身可以进行缓存,如果访问的同一接口,并且做了数据缓存,nginx 就直接可把数据返回,不需要真正地访问服务端,从而提高访问速度。
  • 进行负载均衡:所谓负载均衡,就是把大量的请求按照我们指定的方式均衡的分配给集群中的每台服务器。
  • 保证后端服务安全:因为一般后台服务地址不会暴露,所以使用浏览器不能直接访问,可以把 nginx 作为请求访问的入口,请求到达 nginx 后转发到具体的服务中,从而保证后端服务的安全。
负载均衡

负载均衡(Load Balancing)是一种技术,用于将网络流量或工作负载分配到多个服务器或计算资源上,以提高系统的性能、可靠性和可扩展性。它的主要目标是确保每个服务器的负载尽可能均衡,从而避免单个服务器过载,提升整体系统的响应速度和可用性。

负载均衡的实现方式

  1. 硬件负载均衡
    硬件负载均衡器是专用设备,通常具有高性能和高可靠性。常见的硬件负载均衡器包括 F5 和 A10。

优点:性能强大,支持复杂的负载均衡算法,安全性高。
缺点:成本高,扩展性差。

  1. 软件负载均衡
    软件负载均衡器运行在标准服务器上,常见的有 Nginx、HAProxy 和 LVS。

优点:成本低,扩展性好,易于部署和维护。
缺点:性能相对硬件负载均衡器略低。
负载均衡的常见算法
轮询(Round Robin):将请求依次分配给每个服务器,适用于负载均衡器和服务器性能相近的情况。
加权轮询(Weighted Round Robin):根据服务器的性能分配权重,性能高的服务器分配更多请求。
最少连接(Least Connections):将请求分配给当前连接数最少的服务器,适用于长连接的情况。
源地址哈希(Source IP Hash):根据请求源 IP 地址的哈希值分配服务器,适用于需要会话保持的情况。
负载均衡的应用场景
Web 服务器集群:通过负载均衡分配 HTTP 请求,提高网站的并发处理能力。
数据库集群:分配数据库查询请求,提升数据库的读写性能。
微服务架构:在微服务架构中,负载均衡用于分配服务请求,确保各个服务实例的负载均衡。

# 使用 JWT

原本计划使用 Session 和验证码,考虑到如下因素最后选择了 JWT

使用 JWT(JSON Web Token)相比传统的 Session 和验证码有以下几个优点:

  1. 无状态性
    优点:JWT 是无状态的,服务器不需要存储会话信息。每个请求都携带 JWT,服务器只需验证 JWT 的有效性即可。这使得 JWT 非常适合分布式系统和微服务架构。

  2. 可扩展性
    优点:由于 JWT 包含了用户信息和声明,可以在不同的服务器之间传递和验证。这解决了 Session 在跨服务器交换数据时的局限性。

  3. 减少服务器负担
    优点:JWT 存储在客户端(如 Local Storage 或 Cookie 中),每次请求都携带 JWT,减少了服务器的会话管理开销。这对于高并发应用尤其有利。

  4. 跨语言支持
    优点:JWT 采用 JSON 格式,易于读写,适用于多种编程语言。这使得 JWT 在不同技术栈之间的集成变得更加容易。

  5. 安全性
    优点:JWT 通过签名机制确保数据的完整性和真实性。虽然 JWT 的 Payload 部分没有加密,但签名可以防止数据被篡改。

  6. 灵活性
    优点:JWT 可以存储一些常用信息,用于交换信息。这可以减少服务器查询数据库的次数,提高性能。

  7. 适用于移动端和单页应用
    优点:JWT 特别适合移动端应用和单页应用(SPA),因为它们通常需要跨域请求和无状态认证。

  8. 简化认证流程
    优点:JWT 可以简化认证流程,用户登录后,服务器生成 JWT 并返回给客户端,客户端在后续请求中携带 JWT 即可。

当然,JWT 也有一些缺点,比如:

  • 安全性:由于 Payload 部分没有加密,不能存储敏感数据。
  • 性能:JWT 较长,每次请求都携带 JWT,可能增加网络传输开销。

# 高并发处理

由于菜品表,套餐表都存储在硬盘上,故当多人请求时,难免会出现卡顿,影响用户体验,因此我使用 Redis 进行缓存。这样由于数据存储在内存中,数据获取效率大大提升.

# 细节补充

# 实现功能

# 管理端

实现了员工,分类,菜品,套餐,订单管理

工作台用于快速到达所需功能,来单提醒用于接单

最后还有数据统计,包括可视化图表与 excel 导出

# 用户端

用户端主要使用微信小程序进行下单

包括常见的微信登录,支付,商品浏览下单,购物车

历史订单,地址管理,用户催单功能

# 细节问题

该项目有哪些模块,作用分别是什么
序号模块作用
1sky-take-outmaven 父工程,统一管理依赖版本,聚合其他子模块
2sky-common子模块,存放公共类,例如:工具类、常量类、异常类等
3sky-pojo子模块,存放实体类、VO、DTO 等
4sky-server子模块,后端服务,存放配置文件、Controller、Service、Mapper 等
概述登录流程怎么实现的,为什么要加拦截器

# 登录流程

  1. 用户提交登录请求:用户在前端输入用户名和密码,提交登录请求。
  2. 服务器验证用户信息:服务器接收到请求后,验证用户名和密码是否正确。
  3. 生成 JWT 令牌:如果验证通过,服务器生成一个 JWT(JSON Web Token)令牌,并将其返回给前端。
  4. 前端存储令牌:前端接收到 JWT 令牌后,将其存储在本地(如 LocalStorage 或 SessionStorage)。
  5. 携带令牌访问受保护资源:在后续的请求中,前端会在请求头中携带 JWT 令牌,以访问受保护的资源。

# 为什么要加拦截器

拦截器在登录流程中起到了关键作用,主要有以下几个原因:

  1. 验证请求合法性:拦截器可以在每次请求到达服务器之前,检查请求头中是否包含有效的 JWT 令牌。如果没有令牌或令牌无效,拦截器可以直接拒绝请求,确保只有合法用户才能访问受保护的资源。
  2. 简化代码:通过拦截器,可以将令牌验证的逻辑集中处理,避免在每个受保护的接口中重复编写验证代码,提高代码的可维护性。
  3. 增强安全性:拦截器可以防止未授权的访问,保护敏感数据和功能不被非法用户获取。
ThreadLocal作用是什么,怎么使用的

是线程的局部变量,在登录过程中,作为媒介,将用户 id 传给 save 方法保存信息

为什么做全局异常处理,怎么实现的

原因:抛出的自定义异常不会提示给用户,真正出异常又会给客户端一堆看不懂的提示,因此需要全局异常处理

实现:server 模块下,exception 文件夹下

分类删除注意什么问题?怎么实现的

# 注意事项

  1. 检查关联数据
    • 菜品:确保该分类下没有关联的菜品。如果有,需要先处理这些菜品(如重新分类或删除)。
    • 套餐:确保该分类下没有关联的套餐。
  2. 数据完整性
    • 事务管理:使用事务管理,确保删除操作的原子性,避免部分删除导致数据不一致。
    • 级联删除:如果有级联关系(如分类删除后需要删除关联的菜品),需要确保级联删除的正确性。
  3. 业务规则
    • 状态检查:通常不允许删除正在使用中的分类(如有菜品正在销售)。
    • 权限控制:确保只有有权限的用户才能执行删除操作。

# 实现步骤

  1. 检查关联数据
    • 在删除分类前,查询该分类下是否有菜品或套餐。
  2. 删除分类
    • 如果没有关联数据,可以安全删除分类。
  3. 事务管理
    • 使用 @Transactional 注解确保操作的原子性。
AOP字段填充怎么实现的
  1. 自定义注解

    用于标识需要自动填充字段的方法

  2. 定义切面类

    创建一个切面类,拦截带有 @AutoFill 注解的方法,并通过反射为实体对象的公共字段赋值

  3. Mapper 方法上使用注解

    在需要自动填充字段的方法上添加 @AutoFill 注解

文件上传有哪些方式,项目中怎么实现的
  • 本地存储

    • 优点:开发简单,成本低。
    • 缺点:扩展性差,服务器存储空间有限。
    • 实现步骤:
      1. 前端通过表单上传文件,使用 multipart/form-data 编码类型。
      2. 后端接收文件并保存到服务器指定目录。
      3. 返回文件的访问路径给前端。
  • 云存储(如阿里云 OSS)

    • 优点:扩展性好,支持大规模存储,免维护。
    • 缺点:需要付费。
    • 实现步骤:
      1. 配置阿里云 OSS 相关信息(如 endpoint、accessKeyId、accessKeySecret、bucketName)。
      2. 使用阿里云 OSS SDK 上传文件到云存储。
      3. 返回文件的访问路径给前端。

    之前学习的项目都是本地存储,本着尝试的目的,并且作为学生可以试用阿里云 oss, 故使用阿里云 oss, 采用云存储的方式上传图片

菜品分页需要关联展示分类名称,怎么实现的

前端提交的数据与实体类差距较大,例如状态,时间等是实体类没有的,故使用 DTO 类封装数据,然后就是惯例的 Mapper , Controller , Service .

菜品修改时,关联的菜品口味如何修改的
  1. 获取菜品信息
    • 首先,从数据库中获取需要修改的菜品信息,包括其关联的口味信息。
  2. 修改菜品信息
    • 更新菜品的基本信息,如名称、价格、描述等。
  3. 更新关联的口味信息
    • 根据菜品的 ID,查询并更新其关联的口味信息。可以使用 MyBatis 的关联查询功能来实现这一点。
  4. 保存修改
    • 将修改后的菜品信息和口味信息保存到数据库中。
菜品起售停售注意事项,如何实现

这里要考虑的是数据一致性,确保菜品状态的修改不会影响其他关联数据的完整性。

例如,停售菜品要检查是否有未完成的订单。

由于涉及多表操作,建议使用事务管理来确保数据的一致性和操作的原子性

概述HttpClient的作用

HttpClient 的主要作用是发送 HTTP 请求和接收响应数据

在本项目中, HttpClient 是将授权码 codeappIdappsecret 发送到微信接口

概述微信登录实现流程
  1. ​ 小程序使用 wx.login() 获取 code (授权码), 然后使用 wx.request 发送 code 到开发者服务器
  2. 开发者服务器获得 code 后使用 httpclientappid , appsecret , code 发送到微信接口
  3. 微信接口返回 session_keyopenid (微信用户唯一标识,然后用其生成一个令牌给小程序
  4. 最后小程序使用令牌来获取各种服务
小程序首页菜品数据怎么查询的

这部分是由他人负责的,但根据我的了解,主要还是后端编写查询接口,前端通过 wx.request 方法发送 HTTP 请求获取数据的

小程序首页套餐数据怎么查询的

同上

redis中有哪些数据类型

常见的有 String , Hash , List , Set , ZSet

概述菜品缓存流程
数据库与redis如何实现的数据同步
为什么用了redis还用SpringCache
SpringCache有哪些注解
SpringCache在项目中如何使用的
概述添加购物车流程
概述订单提交实现流程
概述历史订单实现流程
概述再来一单实现流程
定时任务如何实现的
cron表达式有哪些特殊符号,分别表示什么意思
项目中定时任务如何使用的

使用 cron 表达式

说说WebSocket和HTTP协议的区别
项目中用WebSocket实现了哪些功能,怎么实现的
营业额统计数据如何设计VO对象封装的
Excel导入导出有哪些技术方案?

有很多,我最终使用的是 Apache POI , 针对 Microsoft Office 的开源项目

JWT储存在前端的哪个地方

在前端存储 JWT(JSON Web Token)时,有几种常见的方法,每种方法都有其优缺点:

Local Storage:
优点:易于实现,持久化存储,即使刷新页面也不会丢失。
缺点:容易受到 XSS(跨站脚本)攻击,因为任何运行在同一域上的 JavaScript 代码都可以访问 Local Storage。
Session Storage:
优点:与 Local Storage 类似,但数据仅在会话期间有效,浏览器关闭后数据会被清除。
缺点:同样容易受到 XSS 攻击。
Cookies:
优点:可以设置 HttpOnly 和 Secure 标志,增加安全性。HttpOnly 标志可以防止 JavaScript 访问 Cookie,从而减少 XSS 攻击的风险。Secure 标志确保 Cookie 只能通过 HTTPS 传输。
缺点:需要处理 CSRF(跨站请求伪造)攻击,可以通过使用 SameSite 属性来缓解。
内存中:
优点:最安全,因为数据仅在内存中存储,页面刷新或关闭浏览器后数据会丢失。
缺点:用户每次刷新页面或关闭浏览器后需要重新登录。
综合考虑安全性和易用性,使用 HttpOnly 和 Secure 标志的 Cookie 是推荐的存储方式。这种方法可以有效防止 XSS 攻击,同时通过适当的配置减少 CSRF 攻击的风险。

项目涉及https传输,说说其原理

HTTPS(HyperText Transfer Protocol Secure)是 HTTP 的安全版本,用于在客户端(如浏览器)和服务器之间安全地传输数据。它通过加密通信来保护数据的机密性和完整性。以下是 HTTPS 的工作原理:

SSL/TLS 协议:
HTTPS 使用 SSL(Secure Sockets Layer)或其后继者 TLS(Transport Layer Security)协议来加密数据传输。TLS 是目前更常用的协议。
这些协议通过加密数据来防止数据在传输过程中被窃取或篡改。
公钥和私钥:
HTTPS 使用一种称为非对称加密的技术,这种技术使用一对密钥:公钥和私钥。
公钥用于加密数据,任何人都可以获取公钥并使用它来加密数据。
私钥用于解密数据,只有服务器拥有私钥。
SSL/TLS 证书:
服务器需要一个 SSL/TLS 证书来启用 HTTPS。证书由受信任的证书颁发机构(CA)签发,包含公钥和服务器的身份信息。
当客户端连接到服务器时,服务器会发送其 SSL/TLS 证书给客户端。
握手过程:
客户端和服务器通过一个称为握手的过程来建立安全连接。
在握手过程中,客户端验证服务器的证书,确保其真实性。
一旦验证通过,客户端和服务器将生成一个对称密钥,用于加密后续的通信。对称加密比非对称加密更快,因此用于实际的数据传输。
数据加密:
一旦握手完成,客户端和服务器之间的所有数据传输都将使用对称密钥进行加密。
这确保了即使数据在传输过程中被截获,攻击者也无法解密和读取数据。
通过这些步骤,HTTPS 提供了一个安全的通信渠道,保护敏感信息(如密码、信用卡信息等)在传输过程中的安全。

如何设计一个短信验证码登录
  1. 前端请求验证码
    • 用户在登录页面输入手机号码并点击获取验证码按钮。
    • 前端发送请求到后端,要求发送短信验证码。
  2. 后端生成验证码
    • 后端生成一个随机的验证码(通常是 6 位数字)。
    • 将验证码和手机号码关联,并存储在缓存中(如 Redis),设置一个有效期(如 5 分钟)。
  3. 发送短信验证码
    • 使用第三方短信服务(如阿里云短信服务、腾讯云短信服务等)将验证码发送到用户的手机号码。
    • 确保短信内容简洁明了,包含验证码和有效期提示。
  4. 用户输入验证码登录
    • 用户在登录页面输入收到的验证码和手机号码,并提交登录请求。
    • 前端将手机号码和验证码发送到后端进行验证。
  5. 后端验证验证码
    • 后端接收到登录请求后,从缓存中获取存储的验证码。
    • 验证用户输入的验证码是否正确且在有效期内。
    • 如果验证通过,生成 JWT(JSON Web Token)或其他类型的令牌,并返回给前端。
  6. 前端存储令牌
    • 前端接收到令牌后,将其存储在合适的位置(如 Local Storage、Session Storage 或 Cookie)。
    • 在后续的请求中,前端将令牌包含在请求头中,以便后端进行身份验证。
怎么拦截获取验证码的重复请求

为了防止用户重复请求验证码,可以采取以下几种方法:

前端按钮禁用:
在用户点击获取验证码按钮后,将按钮禁用一段时间(例如 60 秒),防止用户在短时间内多次点击。
可以使用 JavaScript 设置一个倒计时,倒计时结束后重新启用按钮。

后端请求频率限制:
在后端设置一个请求频率限制,例如每个手机号码每分钟只能请求一次验证码。
可以使用 Redis 等缓存工具来存储请求时间,并在每次请求时检查是否超过了限制。

唯一请求标识:
使用唯一请求标识(如 UUID)来标识每个请求,并在后端记录这些标识。
如果检测到重复的请求标识,则拒绝处理该请求。

通过这些方法,可以有效地防止用户重复请求验证码,提升系统的安全性和用户体验。

get与post的区别

GET 和 POST 是 HTTP 协议中最常用的两种请求方法,它们在使用场景和特性上有一些重要的区别:

用途:
GET:主要用于从服务器获取数据。请求参数会附加在 URL 后面。
POST:主要用于向服务器提交数据。请求参数包含在请求体中。
安全性:
GET:参数暴露在 URL 中,容易被截获,不适合传输敏感信息。
POST:参数在请求体中,相对更安全,但仍需使用 HTTPS 保护数据传输。
数据长度限制:
GET:由于 URL 长度限制,传输的数据量有限(通常不超过 2048 个字符)。
POST:没有数据长度限制,可以传输大量数据。
幂等性:
GET:是幂等的,多次请求同一资源不会改变服务器状态。
POST:不是幂等的,多次请求可能会导致不同的结果(如多次提交表单)。
缓存:
GET:请求可以被缓存,适合获取静态资源。
POST:请求不会被缓存,适合提交动态数据。
浏览器行为:
GET:请求参数会保留在浏览器历史记录中,可以被书签保存。
POST:请求参数不会保留在浏览器历史记录中,不能被书签保存。
这些区别决定了 GET 和 POST 在不同场景下的适用性。例如,GET 适用于获取数据而不改变服务器状态的操作,而 POST 适用于提交数据或改变服务器状态的操作。

简述一个HTTP请求结构

一个 HTTP 请求由以下几个部分组成:

  1. 请求行(Request Line)

    • 方法(Method):指定要执行的操作,如 GET、POST、PUT、DELETE 等。
    • 请求 URI(Request URI):请求的资源路径,通常包括主机名、端口号(如果非默认)、路径和查询字符串。
    • HTTP 版本(HTTP Version):如 HTTP/1.1 或 HTTP/2。

    示例:

    GET /index.html HTTP/1.1
    
  2. 请求头部(Request Headers)

    • 包含了客户端环境信息、请求体的大小(如果有)、客户端支持的压缩类型等。
    • 常见的请求头包括 Host、User-Agent、Accept、Accept-Encoding、Content-Length 等。

    示例:

    Host: www.example.com
    User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:91.0) Gecko/20100101 Firefox/91.0
    Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
    
  3. 空行(Blank Line)

    • 请求头和请求体之间的分隔符,表示请求头的结束。
  4. 请求体(Request Body)(可选):

    • 在某些类型的 HTTP 请求(如 POST 和 PUT)中,请求体包含要发送给服务器的数据。

    示例:

    param1=value1&param2=value2
    

一个完整的 HTTP 请求示例如下:

GET /index.html HTTP/1.1
Host: www.example.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:91.0) Gecko/20100101 Firefox/91.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Encoding: gzip, deflate
Connection: keep-alive

在这个示例中,请求行指定了使用 GET 方法请求 /index.html 资源,使用 HTTP/1.1 协议。请求头部包含了主机名、用户代理、接受的内容类型和编码方式等信息。由于这是一个 GET 请求,没有请求体。

GET可以传输数据吗

GET 请求可以传输数据,但数据是通过 URL 参数传递的,而不是在请求体中。具体来说,GET 请求会将数据附加在 URL 的查询字符串中。例如:

GET /search?q=example HTTP/1.1
Host: www.example.com

在这个例子中,q=example 是通过 URL 参数传递的数据。

需要注意的是,GET 请求有一些限制和特点:

数据长度限制:由于 URL 长度有限(通常不超过 2048 个字符),GET 请求传输的数据量有限。
安全性:因为数据暴露在 URL 中,GET 请求不适合传输敏感信息,如密码或个人数据。
缓存:GET 请求可以被缓存,这对于获取静态资源非常有用。
幂等性:GET 请求是幂等的,多次请求同一资源不会改变服务器状态。
如果需要传输大量数据或敏感信息,建议使用 POST 请求,因为 POST 请求的数据包含在请求体中,不会暴露在 URL 中。

进程与线程

进程和线程是操作系统中的两个重要概念,它们在计算机系统中扮演着不同的角色。

# 进程

  • 定义:进程是一个正在运行的程序实例,是操作系统进行资源分配和调度的基本单位。
  • 特点:
    • 每个进程都有独立的内存空间,包括代码段、数据段、堆和栈。
    • 进程之间相互独立,一个进程的崩溃不会影响其他进程。
    • 进程切换开销较大,因为需要保存和恢复大量的上下文信息。

# 线程

  • 定义:线程是进程中的一个执行单元,是 CPU 调度和分派的基本单位。
  • 特点:
    • 线程共享进程的内存空间(代码段、数据段、堆),但每个线程有自己的栈和寄存器。
    • 线程之间的切换开销较小,因为共享了进程的大部分资源。
    • 一个进程可以包含多个线程,这些线程可以并发执行,提高程序的执行效率。

# 进程与线程的区别

  1. 资源分配:进程是资源分配的基本单位,而线程是 CPU 调度的基本单位。
  2. 内存空间:进程有独立的内存空间,线程共享进程的内存空间。
  3. 开销:进程切换开销大,线程切换开销小。
  4. 独立性:进程之间相互独立,一个进程的崩溃不会影响其他进程;而线程之间相互依赖,一个线程的崩溃可能导致整个进程崩溃。

# 实际应用

在实际开发中,使用多线程可以提高程序的并发性和响应速度。例如,在一个 Web 服务器中,可以为每个请求创建一个线程来处理,从而提高服务器的吞吐量和响应速度。

TCP,UDP以及IP相关知识
  1. TCP 和 UDP 的区别
    考察点:

是否了解 TCP 和 UDP 的基本特性和应用场景。
回答思路:

连接性:TCP 是面向连接的协议,需要建立连接(通过三次握手)后才能传输数据;UDP 是无连接的协议,不需要建立连接。
可靠性:TCP 提供可靠的数据传输,具有确认应答、重传机制和流量控制;UDP 不保证数据的可靠性,适用于实时应用。
传输方式:TCP 是面向字节流的,数据按顺序传输;UDP 是面向报文的,每个报文独立传输。
开销:TCP 的首部较大(20 字节),UDP 的首部较小(8 字节)。
应用场景:TCP 适用于需要可靠传输的场景,如文件传输、邮件;UDP 适用于对速度要求高但对可靠性要求低的场景,如视频直播、在线游戏。
2. TCP 的可靠性机制
考察点:

是否了解 TCP 如何保证数据传输的可靠性。
回答思路:

校验和:用于检测数据在传输过程中是否被篡改。
确认应答和序列号:每个数据包都有序列号,接收方收到数据后会发送确认应答(ACK),发送方只有收到 ACK 后才会发送下一个数据包。
超时重传:如果发送方在一定时间内没有收到 ACK,会重传数据包。
流量控制:通过滑动窗口机制,控制发送方的发送速度,以适应接收方的处理能力。
拥塞控制:通过慢启动、拥塞避免、快速重传和快速恢复等算法,防止网络拥塞。
3. TCP 的三次握手和四次挥手
考察点:

是否了解 TCP 连接的建立和关闭过程。
回答思路:

三次握手:
客户端发送 SYN 报文,表示请求建立连接。
服务器收到 SYN 后,发送 SYN-ACK 报文,表示同意建立连接。
客户端收到 SYN-ACK 后,发送 ACK 报文,连接建立完成。
四次挥手:
客户端发送 FIN 报文,表示请求关闭连接。
服务器收到 FIN 后,发送 ACK 报文,表示同意关闭连接。
服务器发送 FIN 报文,表示准备关闭连接。
客户端收到 FIN 后,发送 ACK 报文,连接关闭完成。
4. IP 协议
考察点:

是否了解 IP 协议的基本概念和功能。
回答思路:

IP 地址:用于标识网络中的设备,每个设备都有一个唯一的 IP 地址。
路由:IP 协议负责将数据包从源地址传输到目标地址,通过路由器进行转发。
分片和重组:如果数据包超过网络的最大传输单元(MTU),IP 协议会将其分片传输,并在目标地址进行重组。
版本:IPv4 和 IPv6 是两种主要的 IP 协议版本,IPv6 提供了更大的地址空间和更好的安全性。

DNS相关知识

DNS(Domain Name System)服务器是互联网的一项基础服务,用于将域名转换为对应的 IP 地址,使用户能够通过易记的域名访问网站,而不是复杂的数字 IP 地址。以下是 DNS 服务器的相关知识:

# DNS 服务器的工作原理

  1. 域名解析
    • 当用户在浏览器中输入一个域名(如 www.example.com)时,浏览器会向 DNS 服务器发送查询请求,询问该域名对应的 IP 地址。
    • DNS 服务器会查找其数据库,如果找到匹配的记录,就返回对应的 IP 地址。
  2. 分层结构
    • DNS 采用分层结构,包括根域名服务器、顶级域名服务器(如 .com、.org)、权威域名服务器和本地域名服务器。
    • 查询请求会从本地域名服务器开始,如果本地没有缓存记录,则逐级向上查询,直到根域名服务器。
  3. 缓存机制
    • 为了提高查询效率,DNS 服务器会缓存查询结果。下次查询相同域名时,可以直接从缓存中获取结果,而不需要再次查询上级服务器。
  4. 递归查询和迭代查询
    • 递归查询:本地域名服务器代表用户向上级服务器查询,直到找到结果。
    • 迭代查询:本地域名服务器向上级服务器查询时,上级服务器只返回下一步查询的地址,由本地服务器继续查询。

# 常见的 DNS 服务器

  • 公共 DNS 服务器:如 Google Public DNS(8.8.8.8)、Cloudflare DNS(1.1.1.1)、阿里云 DNS(223.5.5.5)等,提供快速、稳定的域名解析服务。
  • 本地 DNS 服务器:由 ISP(互联网服务提供商)或企业内部部署,用于处理本地网络的域名解析请求。

# DNS 安全

  • DNS 劫持:攻击者通过篡改 DNS 记录,将用户引导到恶意网站。可以通过使用 DNSSEC(DNS Security Extensions)来防止。
  • DNS 缓存中毒:攻击者向 DNS 服务器注入虚假记录,导致用户访问错误的 IP 地址。使用安全的 DNS 服务器和定期清理缓存可以减少风险。

# DNS 在面试中的考察

考察点

  • 是否了解 DNS 的基本概念和工作原理。
  • 是否了解 DNS 的分层结构和查询过程。
  • 是否了解常见的 DNS 安全问题及其防护措施。

回答思路

  • 简要介绍 DNS 的作用和工作原理。
  • 说明 DNS 的分层结构和查询过程。
  • 提及常见的 DNS 服务器和其用途。
  • 讨论 DNS 安全问题及其防护措施。
mysql基本查询方法

SELECT 语句
用于从数据库中检索数据。
-- 从表中选择所有列
SELECT * FROM table_name;

-- 从表中选择特定列
SELECT column1, column2 FROM table_name;

WHERE 子句
用于根据指定条件过滤数据。
-- 选择满足条件的行
SELECT * FROM table_name WHERE condition;

-- 示例:选择年龄大于 30 的用户
SELECT * FROM users WHERE age > 30;

ORDER BY 子句
用于对结果集进行排序。
-- 按照指定列升序排序
SELECT * FROM table_name ORDER BY column1 ASC;

-- 按照指定列降序排序
SELECT * FROM table_name ORDER BY column1 DESC;

LIMIT 子句
用于限制返回的行数。
-- 选择前 10 行
SELECT * FROM table_name LIMIT 10;

INSERT INTO 语句
用于向表中插入新数据。
-- 插入一行数据
INSERT INTO table_name (column1, column2) VALUES (value1, value2);

UPDATE 语句
用于修改表中的现有数据。
-- 更新满足条件的行
UPDATE table_name SET column1 = value1 WHERE condition;

-- 示例:将所有用户的年龄增加 1
UPDATE users SET age = age + 1;

DELETE 语句
用于删除表中的数据。
-- 删除满足条件的行
DELETE FROM table_name WHERE condition;

-- 示例:删除年龄小于 18 的用户
DELETE FROM users WHERE age < 18;

GROUP BY 子句
用于将结果集中的行分组,并对每个组进行聚合操作。
-- 按照指定列分组
SELECT column1, COUNT(*) FROM table_name GROUP BY column1;

-- 示例:按年龄分组并统计每个年龄段的用户数量
SELECT age, COUNT(*) FROM users GROUP BY age;

JOIN 操作
用于从多个表中检索数据。
-- 内连接
SELECT * FROM table1 INNER JOIN table2 ON table1.column = table2.column;

-- 左连接
SELECT * FROM table1 LEFT JOIN table2 ON table1.column = table2.column;

-- 右连接
SELECT * FROM table1 RIGHT JOIN table2 ON table1.column = table2.column;

WHEREHAVING 的区别
WHERE 子句:在分组和聚合之前筛选记录,不能包含聚合函数。
HAVING 子句:在分组和聚合之后筛选记录,通常包含聚合函数。

HashMap的底层实现

HashMap 是 Java 中常用的数据结构,它的底层实现主要依赖于数组和链表。在 Java 8 及之后的版本中,还引入了红黑树来优化性能。以下是 HashMap 的底层实现细节:

# 1. 数据存储结构

HashMap 使用一个数组来存储数据,每个数组元素称为一个 “桶”(bucket)。每个桶中存储的是一个链表或红黑树的头节点。

# 2. 哈希函数

当我们向 HashMap 中插入一个键值对时,首先会对键进行哈希运算,得到一个哈希值。然后通过哈希值计算出该键值对应该存储在数组中的哪个位置(即哪个桶中)。计算位置的公式通常是 index = hash % array.length

# 3. 解决哈希冲突

哈希冲突是指不同的键经过哈希运算后得到相同的数组索引。 HashMap 通过链表和红黑树来解决哈希冲突:

  • 链表:在 Java 8 之前,当发生哈希冲突时, HashMap 会将冲突的键值对存储在同一个桶中的链表中。
  • 红黑树:在 Java 8 及之后的版本中,当链表长度超过一定阈值(默认是 8)时,链表会转换为红黑树,以提高查询效率。

# 4. 扩容机制

HashMap 有一个负载因子(默认是 0.75),当 HashMap 中的元素数量超过 容量 * 负载因子 时, HashMap 会进行扩容。扩容时, HashMap 会创建一个新的、更大的数组,并将原数组中的所有元素重新哈希并放入新数组中。

# 5. 主要方法

  • put(K key, V value) :将键值对插入 HashMap 中。如果键已经存在,则更新对应的值。
  • get(Object key) :根据键获取对应的值。
  • remove(Object key) :根据键移除对应的键值对。
  • containsKey(Object key) :判断 HashMap 中是否包含指定的键。

# 6. 示例代码

以下是 HashMap 中节点的实现示例:

static class Node<K, V> implements Map.Entry<K, V> {
    final int hash;
    final K key;
    V value;
    Node<K, V> next;
    Node(int hash, K key, V value, Node<K, V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }
    public final K getKey() { return key; }
    public final V getValue() { return value; }
    public final String toString() { return key + "=" + value; }
    public final int hashCode() { return Objects.hashCode(key) ^ Objects.hashCode(value); }
    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }
    public final boolean equals(Object o) {
        if (o == this)
            return true;
        if (o instanceof Map.Entry) {
            Map.Entry<?, ?> e = (Map.Entry<?, ?>)o;
            if (Objects.equals(key, e.getKey()) &&
                Objects.equals(value, e.getValue()))
                return true;
        }
        return false;
    }
}
虚拟内存技术

虚拟内存是计算机系统中的一种内存管理技术,它使得应用程序可以使用比实际物理内存更大的内存空间。虚拟内存通过将物理内存和磁盘存储结合起来,为每个进程提供一个独立的、连续的地址空间。以下是虚拟内存的主要概念和工作原理:

# 1. 基本概念

  • 虚拟地址空间:每个进程都有一个独立的虚拟地址空间,这个地址空间是连续的,并且对进程来说是唯一的。
  • 物理内存:实际存在的内存硬件,通常是 RAM(随机存取存储器)。
  • 页面(Page):虚拟内存和物理内存都被划分成固定大小的块,称为页面。常见的页面大小是 4KB。
  • 页表(Page Table):操作系统维护的一个数据结构,用于映射虚拟地址到物理地址。

# 2. 工作原理

  • 地址转换:当进程访问内存时,CPU 会将虚拟地址转换为物理地址。这个转换过程由内存管理单元(MMU)完成,MMU 使用页表来查找对应的物理地址。
  • 页面置换:当物理内存不足时,操作系统会将不常用的页面暂时存储到磁盘上(称为交换空间或页面文件),并将需要的页面加载到物理内存中。这种过程称为页面置换。
  • 缺页中断:当进程访问的页面不在物理内存中时,会触发缺页中断。操作系统会处理这个中断,将所需页面从磁盘加载到内存中。

# 3. 优点

  • 扩展内存容量:虚拟内存使得应用程序可以使用比实际物理内存更大的内存空间。
  • 内存保护:每个进程都有独立的虚拟地址空间,防止进程之间的内存访问冲突,提高系统稳定性和安全性。
  • 内存管理灵活性:操作系统可以更灵活地管理内存,优化内存使用效率。

# 4. 示例

假设一个系统有 4GB 的物理内存,但通过虚拟内存技术,可以让每个进程使用高达 16GB 的虚拟内存。当一个进程需要访问超过物理内存容量的数据时,操作系统会将不常用的数据页面存储到磁盘上,并将需要的数据页面加载到物理内存中。

# 5. 实际应用

虚拟内存广泛应用于现代操作系统中,如 Windows、Linux 和 macOS。它使得系统能够运行大型应用程序,并提高系统的稳定性和安全性。

更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

KagurazakaAsahi 微信支付

微信支付