苍穹外卖

苍穹外卖

Day01-01-项目效果展示_哔哩哔哩_bilibili

苍穹外卖项目地址

⎛⎝≥⏝⏝≤⎛⎝⎛⎝≥⏝⏝≤⎛⎝⎛⎝≥⏝⏝≤⎛⎝

一、知识点

1.git

  1. idea点击vcs
  2. 选择 Create Git Repository 然后选择,点击ok(创建本地仓库)
  3. git上面的对勾,然后选择文件,Unversioned Files所有文件
  4. 填写commit message,点击commit
  5. 创建github仓库,名称随便,复制代码地址
  6. 然后点击git的右箭头,点击Define remote,将URL地址填写进去,就ok
  7. 点击push

2.ThreadLocal

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

在被问simpledateformat线程不安全怎么处理的时候,也可以用这个Threadlocal

客户端发起的每一次请求都是一个单独的线程

可以理解为,某个事件的参与者 拉了个私群,方便内部及时交流

ThreadLocal常用方法:

  • public void set(T value)设置当前线程的线程局部变量的值
  • public T get()返回当前线程所对应的线程局部变量的值
  • .public void remove()移除当前线程的线程局部变量

就是通过线程获取到信息,然后将信息存储在线程里面,后面业务可以通过线程获取里面的信息

代码

package com.sky.context;

public class BaseContext {

public static ThreadLocal<Long> threadLocal = new ThreadLocal<>();

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

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

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

}

3.pagehelper

简化分页查询

4.事务 @Transactional

5.Redis

实话实说redis数据库对于后端开发来说非常的重要,因为它是基于内存存储的访问速度非常的快可以将mysql的数据同步到redis从而减少对mysql的访问

Redis是一个基于内存的key-value结构数据库。是内存存储

5.1 5种数据类型

  • 字符串 String
  • 哈希 hash
  • 列表 list
  • 集合 set
  • 有序集合 sorted set/zset (排行榜)

image-20250416202627629

5.2 字符串常用命令

image-20250416203016883

  • SET key value 设置指定key的值
  • GET key 获取指定key的值
  • SETEX key seconds value 设置指定key的值,并将 key的过期时间设为seconds秒(验证码)
  • SETNX key value 只有在key 不存在时设置key的值 (分布式锁)

5.3 哈希操作命令

image-20250416204905608

  • HSET key field value 将哈希表key 中的字段field的值设为value
  • HGET key field 获取存储在哈希表中指定字段的值
  • HDEL key field 删除存储在哈希表中的指定字段
  • HKEYS key 获取哈希表中所有字段
  • HVALS key 获取哈希表中所有值

5.4 列表操作命令

image-20250416205900763

  • LPUSH key value1 [value2] 将一个或多个值插入到列表头部
  • LRANGE key start stop 获取列表指定范围内的元素
  • RPOP key 移除并获取列表最后一个元素
  • LLEN key 获取列表长度

5.5 集合操作命令

image-20250416210951904

  • SADD key member1 [member2]向集合添加一个或多个成员
  • SMEMBERS key 返回集合中的所有成员
  • SCARD key 获取集合的成员数
  • SINTER key1 [key2] 返回给定所有集合的交集
  • SUNION key1 [key2] 返回所有给定集合的并集
  • SREM key member1 [member2]删除集合中一个或多个成员

5.6 有序集合的命令

image-20250416211629487

  • ZADD key score1 member1 [score2 member2] 向有序集合添加一个或多个成员
  • ZRANGE key start stop [WITHSCORES] 通过索引区间返回有序集合中指定区间内的成员
  • ZINCRBY key increment member 有序集合中对指定成员的分数加上增量increment
  • ZREM key member [member …] 移除有序集合中的一个或多个成员

5.7 通用命令

image-20250416212340819

  • KEYS pattern 查找所有符合给定模式( pattern)的 key
  • EXISTS key 检查给定key是否存在
  • TYPE key 返回key所储存的值的类型
  • DEL key 该命令用于在key存在是删除key

6. HttpClient

7. Spring Cache 简化缓存代码开发

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

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

  • EHCache
  • Caffeine
  • Redis

常用注解

注解说明
@EnableCaching开启缓存注解功能,通常加在启动类上
@Cacheable在方法执行前先查询缓存中是否有数据,如果有数据,则直接返回缓存数据;如果没有缓存数据,调用方法并将方法返回值放到缓存中
@CachePut将方法的返回值放到缓存中
@CacheEvict将一条或多条数据从缓存中删除

image-20250428101435934

8. 微信支付

跳过了

9. Spring Task

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

定位: 定时任务框架

作用:定时自动执行某段java代码

cron表达式

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

构成规则:分为6或7个域,由空格分隔开,每个域代表一个含义

每个域的含义分别为:秒、分钟、小时、日、月、周、年(可选)

image-20250509113400913

可以使用corn在线生成器在线Cron表达式生成器

二、代码

第四天代码

完成套餐管理模块所有业务功能,包括:

  • 新增套餐
  • 套餐分页查询
  • 删除套餐
  • 修改套餐
  • 起售停售套餐

要求:

  1. 根据产品原型进行需求分析,分析出业务规则
  2. 设计接口
  3. 梳理表之间的关系(分类表、菜品表、套餐表、口味表、套餐菜品关系表)
  4. 根据接口设计进行代码实现
  5. 分别通过swagger接口文档和前后端联调进行功能测试

好吧好吧,该来的总还是会来的,前面你可以搓出来分类管理模块,那么现在你肯定可以做到套餐管理模块呢

加油加油加油加油李阳

1. 新增套餐

新增套餐模块肯定要关联菜品表,我想一想🤔

首先肯定要有一个套餐表setmeal,如何根据套餐表的id将菜品与套餐关联在setmeal_dish表中,有点类似菜品表和口味表,这样想就立马有思路了嘿嘿,废话少说直接开干兄弟们

我先看下页面原型

image-20250413170146106

完蛋,好像有点麻烦

涉及功能:

  • 套餐分类:根据分类类型查询分类
  • 套餐菜品:根据id查询菜品(所有菜品我猜)
  • 文件上传:已完成
  • 新增套餐:insert

看下接口

image-20221018141521068

image-20221018141606787

老师给了俩接口文档,那我们先写第一个 根据分类文档查询第一个

1.1 据分类查询套餐

根据文档,是写在dish里面的功能

很简单的代码,这里我就不再多说,具体代码请看我github提交的commit的根据分类id查询菜品的代码

再次感叹,前端功能设计的太好了

需要注意

getByCategoryId

/**
* 根据分类id查询菜品
* @param categoryId 分类id
* @return 菜品集合
*/
@Override
public List<Dish> getByCategoryId(Long categoryId) {
// 查询条件,封装查询条件,设置查询条件,根据分类id查询,查询启用菜品
Dish dish = Dish.builder()
.categoryId(categoryId)
.status(StatusConstant.ENABLE)
.build();
// 查询菜品表
return dishMapper.getByCategoryId(categoryId);
}

这里需要根据id,和启用菜品来查询,对应这三个部分

image-20250413210042184

<!--    根据菜品类型id查询菜品-->
<select id="getByCategoryId" resultType="com.sky.entity.Dish">
select * from dish
<where>
<if test="categoryId != null">and category_id = #{categoryId}</if>
<if test="status != null">and status = #{status}</if>
<if test="name != null and name != ''">and name like concat('%', #{name}, '%')</if>
</where>
order by create_time desc
</select>

xml还需要进行动态查询,只查某一项的功能

1.2 批量删除套餐

啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊1啊1啊1啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊1啊啊啊啊啊啊劳资要疯了

劳资删除套餐为什么只能删除套餐表,关联不到套餐菜品表

啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊

操他奶奶的,因为我的插入代码有误,获取id变成了获取套餐名称id,这样我的套餐菜品表关联的套餐id全部变成了口味表里的id,怪不得一直删除不成功,代码明明也和老师的差不多,但是套餐菜品表就是删除不成功

目前已解决,具体代码在github

3. 使用Spring Cache 简化代码思路

image-20250428110832373

4. 绕过订单支付

前端部分

image-20250502172802839

后端部分:

详情请看github的commit

5. Day9 实战内容

用户端历史订单模块:

  • 查询历史订单
  • 查询订单详情
  • 取消订单
  • 再来一单

1. 接口设计(自己想的)

  • 查询历史订单就是查询order表,将相关的查询出来,请求方式为get,查询到的数据封装在一个实体类中VO,然后根据vo将用户id传入过去,根据id查询必要关键字并返回
  • 查询历史订单,根据手机号去查询订单细节表
  • 取消订单就是根据手机号删除一条订单
  • 再来一单add,点击将数据再来一条

2. 查询历史订单

自己写的代码页面显示不出来,原因是为设置状态,我的状态获取不到,后面改为了老师的代码

3. 查询订单详情

抄的代码

三、老师代码

2025

04-11

我已经进修完成javaweb的代码了,苍穹外卖一切从0开始重新写,把我之前遇到的问题做一个巩固

1. 用户登录之初体验

Employeecontroller

 @PostMapping("/login")
public Result<EmployeeLoginVO> login(@RequestBody EmployeeLoginDTO employeeLoginDTO) {
log.info("员工登录:{}", employeeLoginDTO);

Employee employee = employeeService.login(employeeLoginDTO);

//登录成功后,生成jwt令牌
Map<String, Object> claims = new HashMap<>();
claims.put(JwtClaimsConstant.EMP_ID, employee.getId());
String token = JwtUtil.createJWT(
jwtProperties.getAdminSecretKey(),
jwtProperties.getAdminTtl(),
claims);

EmployeeLoginVO employeeLoginVO = EmployeeLoginVO.builder()
.id(employee.getId())
.userName(employee.getUsername())
.name(employee.getName())
.token(token)
.build();

return Result.success(employeeLoginVO);
}

/**
* 退出
*
* @return
*/
@PostMapping("/logout")
public Result<String> logout() {
return Result.success();
}

}

  1. 首先使用<EmployeeLoginVO>的实体类封装操作结果,类型是<EmployeeLoginVO>,就是员工登录返回的数据格式,包括id,用户名,密码,令牌,方便我们获取
    • 需要我们传递一个json类型的返回值,返回用户名和密码,因此参数为employeeLoginDTO类型,里面放的是用户名和密码

@Service
public class EmployeeServiceImpl implements EmployeeService {

@Autowired
private EmployeeMapper employeeMapper;

@Override
public Employee login(EmployeeLoginDTO employeeLoginDTO) {
String username = employeeLoginDTO.getUsername();
String password = employeeLoginDTO.getPassword();

//1、根据用户名查询数据库中的数据
Employee employee = employeeMapper.getByUsername(username);

//2、处理各种异常情况(用户名不存在、密码不对、账号被锁定)
if (employee == null) {
//账号不存在
throw new AccountNotFoundException(MessageConstant.ACCOUNT_NOT_FOUND);
}

//密码比对
// TODO 后期需要进行md5加密,然后再进行比对
if (!password.equals(employee.getPassword())) {
//密码错误
throw new PasswordErrorException(MessageConstant.PASSWORD_ERROR);
}

if (employee.getStatus().equals(StatusConstant.DISABLE)) {
//账号被锁定
throw new AccountLockedException(MessageConstant.ACCOUNT_LOCKED);
}

//3、返回实体对象
return employee;
}

}

然后获取到用户名密码,再将数据库查询到的用户封装再employee中,将我们获取到的用户名和数据中的数据经行比对,再将比对成功的用户数据经行返回到controller中

后面再去生成令牌

突然想明白了,java的泛型好像就是c里面的结构体

2. 日期格式化

sky-server/src/main/java/com/sky/config/WebMvcConfiguration.java

/**
* 配置类,注册web层相关组件
*/
@Configuration
@Slf4j
public class WebMvcConfiguration extends WebMvcConfigurationSupport {
/**
* 扩展SpringMVC的消息转换器
* @param converters
*/
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
log.info("扩展消息转换器...");
// 创建消息转换器对象
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();

// 需要为消息转换器设置一个对象转换器,将Java对象转为json
converter.setObjectMapper(new JacksonObjectMapper());

// 将消息转换器对象追加到mvc框架的转换器集合中
// 0表示优先级最高
converters.add(0, converter);
}
}

固定格式

sky-common/src/main/java/com/sky/json/JacksonObjectMapper.java

package com.sky.json;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;

import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;

/**
* 对象映射器:基于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);
}
}

3. AOP

三、遇到的错误

1.前端nginx打开报502错误

在项目的第一步,我把前端代码放在了没有中文的路径,然后启动了nginx,访问localhost成功,但是点击登录不跳转,控制台报了

Nginx 502错误:

Failed to load resource: the server responded with a status of 502 (Bad Gateway)

我现在在conf文件夹下的nginx.conf文件中添加了

# 502 bad gateway 错误解决配置 start
proxy_buffer_size 64k;
proxy_buffers 32 32k;
proxy_busy_buffers_size 128k;
# 502 bad gateway 错误解决配置 end

发现也不行,后面了解弹幕知道好像是因为没有开后台所以登录不成功,不过服务能启动就ok

2.后端报错误

java: java.lang.NoSuchFieldError: Class com.sun.tools.javac.tree.JCTree$JCImport does not have member field ‘com.sun.tools.javac.tree.JCTree qualid’

问题原因是Lombok ,与 JDK 21 兼容的最低 Lombok 版本是 1.18.30,最小的 Spring Boot 版本是 3.1.4。

我的jdk是21

升级呗,还能咋弄

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
</dependency>

升级完成后build就没有报错了,后序报错再说

3.项目没有iml文件

我导入老师的项目没有inl文件

alt+F12打开命令行

执行mvn idea:module

完美解决

4.Steam++与nginx冲突问题

劳资的steam++与nginx又冲突,我说怎么一直启动不成功

关掉steam++后nginx成功启动,任务管理器中有在运行,成功进入

5.OSS前端不显示图片,后台正确获取问题

yml的endpoint: oss-cn-hangzhou.aliyuncs.com不能加http://

6. day9地址解析失败错误

image-20250508223812920

哇我操他奶奶的

这个错误又改了很长时间

/**
* 检查客户的收货地址是否超出配送范围
* @param address
*/
private void checkOutOfRange(String address) {
HashMap 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 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 userJson = JSON.parseObject(userCoordinate);
if (!userJson.getString("status").equals("0")) {
throw new OrderBusinessException("用户地址解析失败,错误码:" + userJson.getString("status"));
}

// 数据解析
location = userJson.getJSONObject("result").getJSONObject("location");
lat = location.getString("lat");
lng = location.getString("lng");

// 用户收货地址经纬度坐标
String userLngLat = lat + "," + lng;

log.info("店铺坐标响应:{}", shopCoordinate);
log.info("用户坐标响应:{}", userCoordinate);

map.put("origin", shopLngLat);
map.put("destination", userLngLat);
map.put("steps_info", "0");

//路线规划
String json = HttpClientUtil.doGet("https://api.map.baidu.com/directionlite/v1/driving", 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 = (Integer) ((JSONObject) jsonArray.get(0)).get("distance");

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

原因是我创建应用的时候选择了sn校验,一直校验不通过(我是傻逼,不知道用Debug),后面用debug发现一直提醒sn校验不通过,我就改为了地址白名单模式,然后将本机ip放了进去,然后就跑成功了,后面有点点小错误改了以下运行就成功了

我以后一定要好好用debug