苍穹外卖笔记

基础笔记

  • 接口文档可以导入到 apifox 方便查看
  • @RequestBody是接受前端的 json 数据,然后反序列化为 Java 对象
  • Result<T>统一封装了返回给前端的数据,code,message,data,之后所有的结果只需要返回一个 Result 对象就行,成功返回 Result.ok(data),错误就返回 Result.error(message)

DTO 与 VO

  • DTO(Data Transfer Object)数据传输对象

  • 传输的数据属性与实体差异大时,用 DTO 来封装

  • 但在 service 层要新建实体对象来接受,可以拷贝属性BeanUtils.copyProperties(employeeDTO, employee);

  • VO(Value Object)值对象

  • 用于封装返回给前端的数据,例如分页查询 Page<SetmealVO>,他要返回 setmeal 的一些属性和 categoryId 对应的 categoryName

  • 用 SetmealVO 封装用 setmeal 表左连接 categoryId 返回的需要的数据

lombok 插件

  • @Data 自动生成 getter 和 setter
  • @Builder 自动生成 builder 模式相关代码
  • @Slf4j 用于自动创建日志对象,并把对象命名为 log
    类上加注解就能用日志对象 log 了log.error("异常信息:{}", ex.getMessage());

@Configuration 和 @Bean

  • @Configuration修饰配置类,@Bean修饰配置类里面的方法将返回对象注册为 Bean
  • 配置类会在启动时加载,调用里面的所有方法,并把对象放到 Spring 容器中
  • 比如注册拦截器,生成接口文档。拦截器是注入进来注册的,接口文档是生成的 Bean,所以配置类里面的拦截器方法不需要@Bean

@Component

  • @Component修饰类,标识类为一个组件,其他类可用@Autowired 注入
  • 比如从配置文件里封装的 jwt 令牌配置类,jwt 令牌校验类,公共字段填充类

@RestController

  • @RestController 是 Spring 框架的注解
  • 它是一个组合注解,相当于同时使用@Controller 和@ResponseBody,表示该类中的所有方法都会直接返回数据而不是视图名称,自动将返回值序列化为 JSON 等格式响应给客户端。
  • 在前后端分离的项目,虽然仍用 MVC 这种模式,但后端就只负责处理数据不需要返回视图,所以这个项目所有控制器都用的这个注解修饰

@ResponseBody 修饰方法,表示该方法返回的数据会被转序列化后直接写入 HTTP 响应体中,而不是被解析为视图。

监控变量,修改变量,计算表达式

  • debug 模式下,右键变量有 add to watches 添加监视器,能直接看变量的各种属性
  • debug 模式,进行到哪步代码后面也会出现这一行对象的全部属性
  • 右键对应属性,还可以动态修改
  • 方法也可以计算,框选后右键可对表达式求值
  • 这个别的 IDE 也有,实时监控各种变量的值都是什么,用来做算法题也非常好用

Swagger 生成接口文档和在线调试

  • Knife4j 是一个 maven 依赖,是 JavaMVC 框架里的 Swagger
  • 要在对应的 DTO,VO 类和属性写上注解才能被识到@ApiModel(description = "员工登录时传递的数据模型")和@ApiModelProperty("用户名")
  • 扫描指定包下的接口,通过反射获取接口信息
  • localhost:8080/doc.html可以访问生成的接口文档,用后端项目端口访问,不是前端
  • 接口文档也能直接调试 api,类似 postman 和 apifox 也可以直接导入到他们里面

批量插入和删除

用 foreach 循环 collection 为传入的集合

1
2
3
4
5
6
7
8
9
10
11
// mapper.java
void insertBatch(List<DishFlavor> flavors);

// mapper.xml
<insert id="insertBatch">
insert into dish_flavor (dish_id, name, value) VALUES
<foreach collection="flavors" item="df" separator=",">
(#{df.dishId},#{df.name},#{df.value})
</foreach>
</insert>

1
2
3
4
5
6
7
8
void deleteByDishIds(List<Long> dishIds);

<delete id="deleteByDishIds">
delete from dish_flavor where dish_id in
<foreach collection="dishIds" item="dishId" separator="," open="(" close=")">
#{dishId}
</foreach>
</delete>

全局异常处理器

  • 捕获整个应用的异常统一处理,避免在每个控制器中重复编写异常处理代码

  • @ControllerAdvice注解修饰,@ExceptionHandler修饰方法

  • 放在 service 模块里面的 handler 文件夹里

  • 前端请求到达 Controller 后,若执行顺利则调用 controller 里面的return Result.success(orderSubmitVO);

  • 若发生异常,则会被全局异常处理器拦截并统一调用拦截器里面的return Result.error(ex.getMessage());

  • result 是一个封装了 msg,code,data 的对象,用静态工厂方法构造success(),success(T object),error(String msg)

异常处理

  • 可以在方法上 throws Exception 抛出异常
  • 方法内用 throw new Exception(“异常信息”)抛出异常
  • throws / throw 抛出的异常是按调用栈会向上传递,从 ServiceImpl 到 Service 再到 Controller,最终会调用全局异常处理器
  • try catch 会捕获异常,异常就会直接处理,不会向上传递

lambda 表达式

语法:对象::方法
将当前集合里面每个元素的 dishId 赋值为 dishId
flavors.forEach(dishFlavor -> dishFlavor.setDishId(dishId));一条语句可以省略掉 {}

1
2
3
4
5
6

flavors.forEach(dishFlavor -> {// 更多语句就要加上 {}
dishFlavor.setDishId(dishId);
dishFlavor.setUpdateTime(new Date());
});

路径参数

  • 是用 URL 路径,例如 GET /dish/1
  • 要先在 mapping 上面加占位符 @GetMapping("/{id}")
  • 再在 controller 方法的参数上加 public Result<DishVO> getById(@PathVariable Long id)
  • 最终路径为 /dish/{id}

http 传输都是明文的,GET 用 URL 传数据,POST 用 body 传数据,抓个包都能看见,用 https 后传输的数据会被加密

query 参数

  • 是出现在 URL 里?后面的参数
  • 例如分页查询 GET /products?category=electronics&page=2&limit=10
  • controller 可以直接接收 query 参数例如(int page, int pageSize, Integer status)
  • controller 也可以用 DTO 接受参数,Spring 会自动绑定字段
  • @RequestParam("key")修饰参数可以指定给对应形参传值,都同名的时候可以省略
  • 也可以搭配路径参数一起使用,例如 GET /users/123/posts?status=published&sort=date

mybatis 查询的整个逻辑

  • Controller → Service → ServiceImpl → Mapper 接口 → Mapper.xml 是标准的四层架构(表现层、业务层、持久层、数据库)
  • Controller 注入的是 EmployeeService 接口,实际使用的是 EmployeeServiceImpl 实现类,这是 Spring 推荐的面向接口编程方式。
  • DTO 用于接收前端参数,VO 用于返回给前端,Entity 是数据库实体
  • mapper 是与其对应数据表库中的表的操作,所以多表查询是在 ServiceImpl 中调用多个 mapper 完成的
  1. 发出请求到 Controller

    1
    2
    3
    4
    5
    6
    7
    8
    // 注意之前要注入 Service 接口
    @PostMapping("/login")
    public Result<EmployeeLoginVO> login(@RequestBody EmployeeLoginDTO employeeLoginDTO) {
    // 请求进来后,在里面调用Service
    Employee employee = employeeService.login(employeeLoginDTO);
    // 返回员工后,封装成VO,最后返回给前端
    return Result.success(employeeLoginVO);
    }
  2. Controller 调用 Service 里的方法

    1
    2
    3
    4
    // 传递DTO给实现类
    public interface EmployeeService {
    Employee login(EmployeeLoginDTO employeeLoginDTO);
    }
  3. Service 使用对应的 ServiceImpl 里的实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @Service
    public class EmployeeServiceImpl implements EmployeeService {
    @Autowired
    private EmployeeMapper employeeMapper;

    @Override
    public Employee login(EmployeeLoginDTO employeeLoginDTO){
    // 调用mapper查询
    Employee employee = employeeMapper.getByUsername(username);
    // 后续有验证登录的逻辑,不通过抛出对应异常
    return employee;
    }
    }
  4. ServiceImpl 调用 DAO/Mapper.java 接口

    1
    2
    3
    4
    5
    6
    @Mapper
    public interface EmployeeMapper {
    // 简单的查询可以直接用注解
    // @Select("select * from employee where username = #{username}")
    Employee getByUsername(String username);
    }
  5. DAO/Mapper.java 接口调用 Mapper.xml 里面的 sql 实现

    1
    2
    3
    4
    5
    6
    7
    // xml 文件里封装了实际使用的 SQL
    // MyBatis 根据 XML 中的 SQL 执行数据库操作,并将结果映射为 Java 对象
    <mapper namespace="com.sky.mapper.EmployeeMapper">
    <select id="getByUsername" parameterType="string" resultType="com.sky.entity.Employee">
    select * from employee where username = #{username}
    </select>
    </mapper>
  6. 最后返回的逻辑

    ServiceImpl 调用 Mapper 执行查询后返回数据库结果
    Mapper.xml 封装成实体类,返回给 ServiceImpl
    最后返回给 Controller,Controller 层返回给前端

mapper 接口的注解 sql

直接在 mapper 接口里写简单的 sql,就不用在 xml 文件里写实现了
末尾要留空格,因为实际的 sql 是这几行拼起来

1
2
3
4
@Select("select sd.name, sd.copies, d.image, d.description " +
"from setmeal_dish sd left join dish d on sd.dish_id = d.id " +
"where sd.dish_id = #{id}")
List<DishItemVO> getDishItemById(Long id);

Repository 层

都是持久层(DAO 层),Repository 层可以替代 Mapper 层,一般二选一,这个项目用的是 Mapper 层
Mapper 技术用的是 MyBatis,Repository 层用 JPA(JAVA 持久化 API)

JPA 的使用

创建 Repository 接口

Spring 会自动扫描所有继承 Repository(或其子接口)的接口,并为每个接口创建动态代理对象
会根据方法名自动解析行为,最后将代理对象注入到 Service 中

1
2
3
4
5
6
7
8
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

// 继承 JpaRepository<实体类, 主键类型>
@Repository // 可加可不加(Spring Data JPA 会自动注册)
public interface UserRepository extends JpaRepository<User, Long> {
// 基础的增删改查方法已经自动生成,无需写实现!
}

在 Service 或 Controller 中调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Service
public class UserService {

@Autowired
private UserRepository userRepository;

public User createUser(String name, String email) {
User user = new User();
user.setName(name);
user.setEmail(email);
return userRepository.save(user); // 自动插入数据库
}

public List<User> getAllUsers() {
return userRepository.findAll();
}
}

查询语句格式

方法名必须遵循这个结构
[操作类型] + [字段名] + [条件关键词]

  • 方法名以 find、read、query、get、count、exists 等开头(语义不同,但效果一样,推荐用 find)
  • 后面跟 实体类的属性名(首字母大写)
  • 可加 条件关键词:By、And、Or、Between、Like、IgnoreCase 等
  • 区分大小写:属性名必须和 Entity 中的字段名一致(按 JavaBean 规范)
  1. 单条件查询
    findByEmail(String email)
  2. 多条件查询
    findByEmailAndName(String email, String name)
  3. 比较操作
    剩下的操作就自己用ai去查吧
  4. 模糊查询
  5. 忽略大小写
  6. 排序
  7. 限制结果数量
  8. 统计&判断存在

查询太复杂时就不用约定了,如 JOIN、GROUP BY 等,方法名就会变得极长,甚至无法表达
这时候就用@Query自己写 JPQL/HQL 语句

1
2
@Query("SELECT u FROM User u WHERE u.name LIKE %:name%")
List<User> searchByName(@Param("name") String name);

模糊查询

1
2
3
4
5
6
7
8
9
10
<mapper namespace="com.sky.mapper.EmployeeMapper">
<select id="pageQuery" resultType="com.sky.entity.Employee">
select * from employee
<where>
<if test="name != null and name != ''">
and name like concat('%',#{name},'%')
</if>
</where>
</select>
</mapper>

PageHelper 分页查询

  • 自动处理分页逻辑,只需调用 PageHelper.startPage(pageNum, pageSize),就会自动拦截并改写 sql
  • 返回的 pageResult 对象包含两个属性,total 和 records

Controller 层

1
2
3
4
5
6
7
8
@GetMapping("/historyOrders")
@ApiOperation("查看历史订单")
// 传入的参数可直接接收,也可以用DTO对象接收,Spring会自动绑定
public Result<PageResult> page(int page, int pageSize, Integer status) {
log.info("历史订单查询");
PageResult pageResult = orderService.pageQuery(page, pageSize, status);
return Result.success(pageResult);
}

Service 层

1
2
/** 历史订单查询 */
PageResult pageQuery(int pageNum, int pageSize, Integer status);

ServiceImpl 层

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
/** 历史订单查询 */
@Override
public PageResult pageQuery(int pageNum, int pageSize, Integer status) {
// 开始分页,自动拦截并改写 sql
PageHelper.startPage(pageNum, pageSize);
// 用分页查询的 DTO 对象封装查询的条件
OrdersPageQueryDTO ordersPageQueryDTO = new OrdersPageQueryDTO();
ordersPageQueryDTO.setStatus(status);
ordersPageQueryDTO.setUserId(BaseContext.getCurrentId());

// 分页条件查询,这个mapper里面应该与传入的DTO对象一致
Page<Orders> page = orderMapper.pageQuery(ordersPageQueryDTO);

// 查询page里面的每一条订单的订单明细,并封装入OrderVO进行响应
List<OrderVO> list = new ArrayList();// VO列表
if (page != null && page.getResult() != null) {
for (Orders orders : page) {
Long orderId = orders.getId();
// 查询订单明细
List<OrderDetail> orderDetailList = orderDetailMapper.getByOrderId(orderId);
// 封装进VO
OrderVO orderVO = new OrderVO();
BeanUtils.copyProperties(orders, orderVO);
orderVO.setOrderDetailList(orderDetailList);
// 添加进VO列表
list.add(orderVO);
}
}
// PageResult根据总记录数和传入的list构造出分页结果
return new PageResult(page.getTotal(), list);
}

Mapper.java

1
Page<Orders> pageQuery(OrdersPageQueryDTO ordersPageQueryDTO);

Mapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<select id="pageQuery" resultType="com.sky.entity.Orders">
select * from orders
<where>
user_id = #{userId}
<if test="number != null">
and number like concat('%',#{number},'%')
</if>
<if test="status != null">
and status = #{status}
</if>
<if test="phone != null">
and phone = #{phone}
</if>
<if test="beginTime != null">
and order_time <![CDATA[ >= ]]> #{beginTime}
</if>
<if test="endTime != null">
and order_time <![CDATA[ <= ]]> #{endTime}
</if>
</where>
</select>

AOP 面向切面编程

例如执行插入时需要创建时间,创建人

  1. 自定义注解(annotation),标记需要处理的方法

    1
    2
    3
    4
    5
    6
    7
    // 表示当前注解用于方法
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface AutoFill {
    // 数据库操作类型: UPDATE INSERT
    OperationType value();
    }
  2. 定义切面类(aspect),统一拦截加入注解的方法,然后用反射赋值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    @Aspect
    @Component
    @Slf4j
    public class AutoFillAspect {
    // 切点,在 mapper 包下,并且加入了 AutoFill 注解的方法
    @Pointcut("execution(_ com.sky.mapper._.\*(..)) && @annotation(com.sky.annotation.AutoFill)")
    public void autoFillPointCut() {
    }
    // 前置通知,在切点方法执行前执行,在通知中为公共字段赋值
    @Before("autoFillPointCut()")
    public void autoFill(JoinPoint joinPoint) {
    log.info("开始进行公共字段填充");
    }
    }
    • 最后在需要的 Mapper 方法上加上注解,注解会自动把参数里面的公共字段赋值,注意传参的对象要有这个属性来接受
    • 然后在 xml 里面加上 set if 对应字段,注解是给对象赋值,最后要根据对象写完整的 sql
    • 最好不同时用两个注解,@AutoFill 和@update 都进行了 sql 可能会冲突导致 AutoFill 失败,所以把那个简单的 sql 还是写在 xml

    @AutoFill(value = OperationType.INSERT)
    void insert(Employee employee);

阿里云 OSS

pom 文件导入依赖
新建一个 OSS 存储空间,并获取访问密钥
在 yml 文件中配置 endpoint access-key-id access-key-secret bucket-name
给上面的属性封装成 AliOssProperties 类
最后新建一个工具类 AliOssUtils 处理文件上传

注解方式的事务管理

@Transactional功能,方法执行前自动开启事务,方法正常结束时提交事务,出现异常时回滚事务
一般涉及两个及以上表的数据库操作,要使用事务管理,确保数据一致性
要先在 Application 启动类加上 @EnableTransactionManagement 才能开启

ThreadLocal

  • 每一次请求都是一个单独的线程,验证可以用System.out.println("当前线程的ID:" + Thread.currentThread().getId());
  • 同一个线程内可以用ThreadLocal存取数据
  • BaseContext.setCurrentId(empId);在拦截器里存储当前用户的 ID,BaseContext 是放在 Common 里面的工具类

主键回显,获取自增的 id

mapper.xml 的标签里面加入后两个选项来打开回显和指定写回的字段
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
或者 mapper.java 的标签里面加入注解
@Options(useGeneratedKeys = true, keyProperty = "id")

同时修改两张表时的操作

修改菜品时,修改他的口味的数据
先把原来的口味数据删除,再插入新改动后的数据

1
2
3
4
5
6
7
8
9
10
// 获取口味列表
List<DishFlavor> flavors = dishDTO.getFlavors();
// 删除原有口味数据
dishFlavorMapper.deleteByDishId(dishDTO.getId());
if (flavors != null && flavors.size() > 0) {
flavors.forEach(dishFlavor -> dishFlavor.setDishId(dishDTO.getId()));
// 插如新的口味数据
dishFlavorMapper.insertBatch(flavors);
}

用户端管理端 controller 重名,bean 冲突

  • 在@RestController 后面加上别名,@RestController("userShopController")
  • 生成接口文档的配置类在 new 的 docket 后面加.groupName(“管理端接口”),扫描的包名也改,之后接口文档就能按模块进行分组

JWT 令牌运作的整个流程

登录服务器存储一个私钥,在用户登陆成功后生成 jwt 令牌给用户
之后其他服务器存储一个统一的公钥,在收到请求时就会用公钥和 jwt 令牌前面的部分与数字签名进行验证

  • 对称加密,所有服务器端都用同一个密钥进行签名验签
  • 非对称加密,登录服务器生成 jwt 时用私钥签名,其他服务器用同一公钥进行验签

非对称加密的整个流程

  1. 首先用户发送用户名密码进行登录,服务器收到后在数据库中比对用户名密码
  2. 比对成功后服务器根据算法,用户的信息,和服务器存储的私钥生成 token 发回给用户端
  3. 用户端会将 token 存储在本地,每次请求都会携带这个 token 作为认证信息
  4. 之后服务器收到用户请求时,会验证 token 的合法性,通过共有的公钥生成签名与发送来的签名对比,如果一致则验证通过
  5. 在 token 过期或生成新的 token 之前,用户端会一直携带这个 token

token 结构

header.payload.signature

  • header: 存放 token 的生成算法,token 的类型
  • payload: 存放 token 的内容,比如用户信息(非用户敏感信息,如 id 等用于标识用户),过期时间,生成时间,随机数
  • signature: 签名,根据服务器秘钥由 header 和 payload 生成

jwt 其他要点

  • 因为秘钥存储在服务器,所以别人只拿到 header 和 payload,无法生成 signature,从而无法验证 token 的合法性
  • jwt 解决了 session 服务器存储开销大和跨域/分布式支持困难的问题
  • jwt 的解密在拦截类中进行(苍穹外卖定义在 interceptor 包下的 JwtInterceptor 类),用@Component 注解修饰,实现 HandlerInterceptor 接口,最后在 WebMvcConfiguration 类中注册自定义拦截器,拦截/排除指定路径
  • token 的名称是约定,由前端带过来的 admin 端是 token,user 端是authentication,都写在application.yml文件中

如何使jwt主动失效

jwt是无状态的,客户端每次发过来服务端只有验签的能力,并不能清除登录状态
几种失效方案

  1. 黑名单方案:
    点退出登录时,主动将jwt存入Redis,并设置过期时间
    每次业务服务器收到请求时,除了验签,还要去Redis中查询是否在黑名单中,如果在就拒绝
  2. 短期Token + 刷新Token:
    access token(短期token):有效期很短,几十分钟,用于请求接口
    refresh token(刷新token):有效期长,一周、一个月,用于刷新 access token
    用户请求时,先用refresh token 去获取 access token,之后的请求再用 access token 访问接口
    之后access token 过期时,就用refresh token 去获取新的 access token,refresh token 过期时,用户需要重新登录
  3. 白名单方案:
    和黑名单相反,就是存入每个用户的token,请求时,只有Redis中存在才放行
  4. 版本号/时间戳机制:
    在token中加一个版本号,用户表中也加一个token版本号字段
    用户登出时把版本号加1,服务器收到token时,解析出来里面的版本号,然后取数据库中查当前用户的版本号
    如果用户的token版本号小于数据库中的就让当前的token失效

jwt内部包含着exp(有效期)字段,所以后端可以直接获取exp,判断是否过期,jwt因为有签名也无法伪造,不怕用户自己改exp
所以jwt自己到期就能失效,上面这些都是让jwt主动失效的手段,比如退出登录或异常登录管控

微信小程序登录过程

openid 是微信用户的唯一标识符
小程序点击登录后,用户端带着 code 到服务器
服务器拿到 code 后,调用微信的接口,获取 openid
服务器拿到 openid 后,生成 token,返回给用户端

redis 操作

先在 config 文件夹里创建 RedisConfiguration 类,返回 RedisTemplate 对象
之后在别的地方器里注入 RedisTemplate 对象,就可以操作 redis 了

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class RedisConfiguration {
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
log.info("开始创建 redisTemplate 对象...");
RedisTemplate redisTemplate = new RedisTemplate();
// 设置 redis 的连接工厂对象,让 redisTemplate 对象通过工厂与 redis 连接起来
redisTemplate.setConnectionFactory(redisConnectionFactory);
// 设置 redis key 的序列化容器,给 key 序列化
redisTemplate.setKeySerializer(new StringRedisSerializer());
return redisTemplate;
}
}

从 redis 中取出的数据是字符串,强制转换的类型取决去当初放进去时的类型
List<DishVO> list = (List<DishVO>) redisTemplate.opsForValue().get(key);
list = dishService.listWithFlavor(dish);
redisTemplate.opsForValue().set(key, list);

增删改的操作都需要清理缓存

Spring Cache 缓存

是 Spring 提供的缓存框架,用注解开启缓存功能

  • @EnableCaching开启缓存功能,通常加在启动类上,例如 SkyApplication 类
  • @Cacheable在方法执行前先查询缓存数据中是否有数据,如果有则直接返回不会调用方法,没有则执行方法,将方法返回值放入缓存中
  • @CachePut将方法的返回值放入缓存横中
  • @CacheEvict从缓存中删除数据
1
2
3
4
5
6
7
8
9
10
// 用@CachePut 修饰,把返回值放入缓存中
// Spring Cache 会生成一个 key 给 redis,格式为 cacheName::传入形参 id
// 也可以用 key = "#result.id" 获取返回值的 id
// 也可以用 key = "#p0.id"代表第一个形参的 id,还是第一种最直观
@PostMapping
@Cacheable(cacheNames = "userCache", key = "#user.id")
public User save(@RequestBody User user) {
userService.insert(user);
return user;
}

查询缓存数据@Cacheable(cacheNames = "userCache", key = "#id")
查到就直接返回,没有就执行方法查数据库,将返回值放入缓存中

删除一条缓存@CacheEvict(cacheNames = "userCache", key = "#id")
删除所有缓存@CacheEvict(cacheNames = "userCache", allEntries = true)

查询操作加查询缓存注解
增删改操作加删除缓存

cpolar 内网穿透

可以获取一个临时的公网 ip,将他映射到 nginx 的 localhost:端口号,然后就能用 cpolar 提供的公网 ip 访问内网了

  1. 下载登录后进入到官网的验证界面复制 Authtoken
  2. 然后进入到安装目录打开 cmd 运行cpolar.exe Authtoken 复制的Authtoken(只需做一次验证)
  3. 最后 cmd 界面运行cpolar.exe http 端口号就能获取一个临时的公网 ip

list 转换成字符串

.stream()转换成 stream 流
.map(x -> {...})把每一个元素(x)映射成新的元素{…}
.collect(Collectors.toList())将 stream 流转换成 list

1
2
3
4
List<String> orderDishList = orderDetailList.stream().map(x -> {
String orderDish = x.getName() + "*" + x.getNumber() + ";";
return orderDish;
}).collect(Collectors.toList());

假设有两个对象
new OrderDetail(“宫保鸡丁”, 2)
new OrderDetail(“鱼香肉丝”, 1)
执行之后
orderDishList = [“宫保鸡丁2;”, “鱼香肉丝1;”]

百度地图 API 引入

百度地图的 API
左上角产品服务那里可以找到需要的 API

  1. 先去官网创建应用或取 AK 秘钥
  2. 将店铺地址 address 和 AK 秘钥配置在application.yml文件中
  3. 然后在 ServiceImpl 层里引入这两个属性 @Value("${sky.baidu.ak}")
  4. 在 ServiceImpl 层里新建一个私有方法用于校验距离范围
  5. 方法里面建立 map 对象用来传参,把店铺地址,AK,输出类型都放进去
  6. 然后用 HttpClientUtil 类以 map 作参数,发起 get 请求来调用百度地图的 API 接口
  7. 先获取返回的 json 数据,然后解析为经纬度,map 存储店铺和用户的经纬度坐标
  8. 再调用路线规划的 API,传两个坐标,返回距离等数据
  9. 最后用获取的距离判断是否超出配送范围
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
/**
* 检测地址是否超出配送范围
*
* @param address
*/
private void CheckOutOfRange(String address) {
Map map = new HashMap();
map.put("address", shopAddress);// 注入的店铺地址
map.put("output", "json");
map.put("ak", ak);

// 获取店铺的经纬度坐标
String shopCoordinate = HttpClientUtil.doGet("https://api.map.baidu.com/geocoding/v3", map);

JSONObject jsonObject = JSON.parseObject(shopCoordinate);
if (!jsonObject.getString("status").equals("0")) {
throw new OrderBusinessException("店铺地址解析失败");
}

// 数据解析 提取jsonObject对象中的result对象中的location对象
JSONObject location = jsonObject.getJSONObject("result").getJSONObject("location");
String lat = location.getString("lat");
String lng = location.getString("lng");
// 店铺经纬度坐标
String shopLngLat = lat + "," + lng;

map.put("address", address);// 传进来的用户地址
// 获取用户收货地址的经纬度坐标
String userCoordinate = HttpClientUtil.doGet("https://api.map.baidu.com/geocoding/v3", map);

jsonObject = JSON.parseObject(userCoordinate);
if (!jsonObject.getString("status").equals("0")) {
throw new OrderBusinessException("收货地址解析失败");
}

// 数据解析
location = jsonObject.getJSONObject("result").getJSONObject("location");
lat = location.getString("lat");
lng = location.getString("lng");
// 用户收货地址经纬度坐标
String userLngLat = lat + "," + lng;

// 放入两个坐标
map.put("origin", shopLngLat);
map.put("destination", userLngLat);
map.put("steps_info", "0");// 不返回详细的路线步骤

// 路线规划
String json = HttpClientUtil.doGet("https://api.map.baidu.com/direction/v2/riding", map);

jsonObject = JSON.parseObject(json);
if (!jsonObject.getString("status").equals("0")) {
throw new OrderBusinessException("配送路线规划失败");
}

// 数据解析
JSONObject result = jsonObject.getJSONObject("result");
JSONArray jsonArray = (JSONArray) result.get("routes");
Integer distance = jsonArray.getJSONObject(0).getInteger("distance");

if (distance > 5000) {
// 配送距离超过5000米
throw new OrderBusinessException("超出配送范围");
}
}

Spring Task 任务调度管理

掌管定时任务

  • 包含在 Spring-context 里
  • 启动类添加 @EnableScheduling 开启任务调度
  • 自定义定时任务类

cron 表达式
本质是字符串,分为六或七个域,由空格分割
秒 分 时 日 月 周 年
2020 年 10 月 12 日 9 小时 0 分 0 秒
0 0 9 12 10 ? 2020
可以用在线 Cron 表达式生成器

自定义定时任务类

1
2
3
4
5
6
7
8
9
@Component
@Slf4j
public class MyTask {
// 每5秒执行一次
@Scheduled(cron = "0/5 * * * * ?")
public void myTask() {
log.info("定时任务开始执行");
}
}

WebSocket

  • 一种基于 TCP 的一种新的网络协议,实现客户端与服务器的双向通讯
  • http 是 短连接,ws 是 长连接;http 是单向通讯,ws 是双向通讯
  • 应用场景,弹幕,聊天窗口,实况数据推送
  1. 创建 WebSocketConfig 类,用@Configuration 和 @Bean 注解注册 ServerEndpointExporter
    返回的实例会扫描所有使用 @ServerEndpoint 注解的类,并注册成 WebSocket 服务
  2. 创建 websocket 包,里面创建 WebSocketServer 类,用@Component @ServerEndpoint(“ws/{sid}”) 注解标识 WebSocket 服务
    这个类里面用哈希表存放 session 并用@OnOpen @OnClose @OnMessage 等创建回调方法,以及群发的方法
  3. 在对应 ServiceImpl 里面注入 WebSocketServer 类,就可以新建哈希表放入约定的传参
    把哈希表转换成 JSON (JSON.toJSONString(map))作为参数调用 WebSocketServer 类的群发方法

创建 ws 连接:

  1. 管理端在进入时就会发出 ws 请求,随机生成 sid,并把 sid 作为参数,发送给服务端
  2. 这个请求会被符合对应路径的这个注解方法拦截@ServerEndpoint(“/ws/{sid}”),这时候就建立连接了,会自动创建一个 session 对象,包含当前连接的所有相关信息
  3. 然后服务端会把 sid 作为 key,把 session 作为 value,保存在 map 中,发消息就会用这个 map 获取 session,然后调用 session.getBasicRemote().sendText(message)

Apache POI

一个开源的 Java API,用于读写 Microsoft Office Excel 文件。

用 Java 的 api 新建 sheet,row 对象,填充修改 resource 里面的 template 文件夹里的模版文件
用输出流输出到浏览器下载ServletOutputStream out = response.getOutputStream();
输出完后关闭资源(outStream 和 excel)

Spring 依赖注入

IOC(Inversion of Control,控制反转)
传统设计横中,类会主动创建他所依赖的对象

1
2
3
public class UserService {
private UserRepository repository = new UserRepository();
}

用 IOC 之后,对象的控制权反转给 Spring 容器,由 Spring 容器创建对象,并注入给 UserService 类
推荐用构造器注入(如果只有一个构造函数,可以省略 @Autowired)
其次是用 setter 注入(都要加@Autowired 注解)
字段注入加@Autowired 的方式不推荐

1
2
3
4
5
6
7
8
9
10
public class UserService {// 构造器,苍穹外卖里基本都没写构造器
// 当前类的依赖对象,作为这个类的属性/成员变量
private final UserRepository repository;
// 当前类的构造器
// 参数里面是当前类所依赖的类,Spring会创建这个对象实例
public UserService(UserRepository repository) {
// 把Spring创建的实例赋给当前类的成员变量,就完成了依赖注入
this.repository = repository;
}
}

依赖注入的类要启动时就注册为 Bean 才能被 Spring 容器管理
所以要有@Component、@Service、@Mapper、@Repository 、@Bean、@Mapper 注解

做完了,接下来的打算

背八股文,java mysql spring redis 还有消息队列,分布式,网络协议,操作系统都去背一遍吧
然后还要继续刷算法
再练练打字


苍穹外卖笔记
http://www.981928.xyz/2025/11/17/苍穹外卖笔记/
作者
981928
发布于
2025年11月17日
许可协议