苍穹外卖

0 阅读21分钟

day1 and day2

Swagger的介绍: 使用 Swagger Editor​ 编写 openapi.yaml文件,定义好 API 的整体规划(接口、数据模型)。使用 Swagger UI​ 嵌入项目,自动展示实时 API 文档,供前端或测试人员使用。
Swagger的使用:
Knife4j是一个为 Java项目(特别是 Spring Boot、Spring Cloud​ 等框架)设计的、增强型 Swagger UI 前端实现。你可以把它简单理解为 Swagger 的“超级皮肤”或“增强版”。
使用方法:加入其maven坐标,在配置类加入相关的配置,设置静态资源映射

<dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>knife4j-spring-boot-starter</artifactId>
</dependency>  
相关配置
 @Bean
    public Docket docket() {
        ApiInfo apiInfo = new ApiInfoBuilder()
                .title("苍穹外卖项目接口文档")
                .version("2.0")
                .description("苍穹外卖项目接口文档")
                .build();
        Docket docket = new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo)
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.sky.controller"))
                .paths(PathSelectors.any())
                .build();
        return docket;
    }

    /**
     * 设置静态资源映射
     * @param registry
     */
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/");
        registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
    }
}  

swagger常用注解: 屏幕截图 2026-03-11 102011.png

JWT令牌的使用及其介绍

Employee employee = employeeService.login(employeeLoginDTO);
    //登录成功后,生成jwt令牌  
//构建载荷
    Map<String, Object> claims = new HashMap<>();  
    claims.put(JwtClaimsConstant.EMP_ID,employee.getId());
    //从配置文件读取管理员端的签名密钥(对应 HS256 算法的 secret),从配置文件读取令牌的有效期
    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)

介绍: 可以把 JWT 令牌想象成一张「加密的身份凭证」,比如你登录某网站后,服务器不会再保存你的登录状态(不像传统 Session),而是直接给你发一个 JWT 令牌;之后你每次请求接口,都带上这个令牌,服务器只要验证令牌的合法性,就能确认你的身份,不用再查数据库 / 缓存;令牌本身包含了你的关键信息(比如用户 ID、权限),且经过加密签名,无法被篡改。
使用: 生成jwt令牌需要4步,构建header(头部声明令牌类型还有签名算法),构建payload(载荷,存放自定义声明一般把id放入进去),构建signnature(签名),最后将这三部分进行拼接生成最终的jwt字符串
JwtUtil.createJWT:这是项目自定义的 JWT 工具类(内部会自动完成: 构建 Header(指定算法,比如 HS256); 把 claims 存入 Payload,并添加 exp(过期时间)等注册声明; 用密钥对 Header+Payload 签名,生成最终的 JWT 字符串。

新增员工注意点: :首先要判断用户是否存在数据库,再对实体类employeee未赋值的进行赋值,最后还要根据ThreadLocal

关于异常处理::异常处理分为全局异常和自定义异常,无论是何种异常,都应该将异常捕获然后返回给前端

image.png
自定义业务异常:

package com.sky.common.exception;  
import lombok.Getter;   
/** * 自定义业务异常 * 用于处理业务逻辑中的异常(如:用户名已存在、参数校验失败等) */   
@Getter // 生成getter,方便全局处理器获取属性  
public class BusinessException extends RuntimeException {  
// 异常状态码(对应Result的code)  
private Integer code;  
// 构造器1:只传异常消息(默认业务异常码400) public BusinessException(String msg) { super(msg);  
// 调用父类构造器,传递异常消息 this.code = 400; }  
// 构造器2:自定义状态码和消息 public BusinessException(Integer code, String msg) {   
        super(msg);   
        this.code = code; } }  

系统异常:区分系统层面的异常(比如数据库连接失败、第三方接口调用失败等)

package com.sky.common.exception;

import lombok.Getter;

/**
 * 自定义系统异常
 * 用于处理系统层面的异常(如:数据库连接失败、Redis异常等)
 */
@Getter
public class SystemException extends RuntimeException {
    private Integer code;

    public SystemException(String msg) {
        super(msg);
        this.code = 500; // 默认系统异常码500
    }

    public SystemException(Integer code, String msg) {
        super(msg);
        this.code = code;
    }
}

全局异常处理器:统一捕获所有异常,适配自定义异常和系统默认异常:

package com.sky.common.handler;
import com.sky.common.exception.BusinessException;
import com.sky.common.exception.SystemException;
import com.sky.common.result.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.sql.SQLIntegrityConstraintViolationException;

/**
 * 全局异常处理器
 * 捕获所有Controller层抛出的异常,统一返回格式
 */
@Slf4j // 日志注解
@RestControllerAdvice // 核心注解:全局异常处理 + 返回JSON
public class GlobalExceptionHandler {

    // ========== 处理自定义异常 ==========
    /**
     * 处理业务异常(BusinessException)
     */
    @ExceptionHandler(BusinessException.class)
    public Result<?> handleBusinessException(BusinessException e) {
        log.error("业务异常:{}", e.getMessage(), e); // 打印异常堆栈,方便排查
        return Result.error(e.getCode(), e.getMessage());
    }

    /**
     * 处理系统异常(SystemException)
     */
    @ExceptionHandler(SystemException.class)
    public Result<?> handleSystemException(SystemException e) {
        log.error("系统异常:{}", e.getMessage(), e);
        return Result.error(e.getCode(), e.getMessage());
    }

    // ========== 处理系统默认异常 ==========
    /**
     * 处理数据库完整性约束异常(比如唯一索引冲突)
     */
    @ExceptionHandler(SQLIntegrityConstraintViolationException.class)
    public Result<?> handleSqlConstraintException(SQLIntegrityConstraintViolationException e) {
        log.error("数据库约束异常:{}", e.getMessage(), e);
        String msg = e.getMessage();
        if (msg.contains("Duplicate entry")) {
            // 提取重复的字段值,返回友好提示
            String value = msg.split("'")[1];
            return Result.error("数据重复:" + value + " 已存在!");
        } else if (msg.contains("cannot be null")) {
            String column = msg.split("'")[1];
            return Result.error(column + " 字段不能为空!");
        }
        return Result.error("数据库操作失败:" + msg);
    }

    /**
     * 处理参数校验异常(@Valid/@Validated注解触发)
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result<?> handleValidException(MethodArgumentNotValidException e) {
        log.error("参数校验异常:{}", e.getMessage(), e);
        // 获取第一个校验失败的消息(比如:用户名不能为空)
        String msg = e.getBindingResult().getFieldError().getDefaultMessage();
        return Result.error(msg);
    }

    /**
     * 兜底处理:捕获所有未明确指定的异常
     */
    @ExceptionHandler(Exception.class)
    public Result<?> handleAllException(Exception e) {
        log.error("未知异常:{}", e.getMessage(), e); // 打印完整堆栈,方便排查
        return Result.systemError("服务器内部错误,请联系管理员");
    }
}

员工分页查询: :员工分页要使用到pagehelper这个插件,导入插件,然后进行使用即可

public PageInfo<User> getUserByAge(int pageNum, int pageSize, Integer age) {
        // 1. 开启分页:仅对后续第一条查询生效
        PageHelper.startPage(pageNum, pageSize);
        // 2. 执行普通查询(PageHelper 会自动拦截并添加分页逻辑)
        List<User> userList = userMapper.selectByAge(age);
        // 3. 封装分页结果(包含总条数、总页数、当前页数据)  
        //这里也可以自定义封装数据
        return new PageInfo<>(userList);
    }

消息转换器: : Spring MVC 里负责前后端数据 “翻译” 的专属官,核心解决的是「HTTP 数据」和「Java 对象」之间的转换问题 —— 没有它,你就得手动写代码解析请求、拼接响应,效率极低。
举例:

用户接口
@PostMapping("/users")
public User addUser(@RequestBody User user) {
    // 保存用户逻辑
    return user;
}  
前端发送的请求  
POST /users HTTP/1.1
Content-Type: application/json
{
  "name": "张三",
  "age": 20
}  

这个过程中,消息转换器做了两件核心事

  1. 请求阶段(解码) :把 HTTP 请求体里的 JSON 字符串,转换成你代码里的User Java 对象(给@RequestBody用);
  2. 响应阶段(编码) :把你代码里返回的User Java 对象,转换成 JSON 字符串,作为 HTTP 响应体返回给前端。

修改员工状态 :这里学到一个知识点就是动态更新,要更新某一条数据时,可以将数据封装到一个对象里面,然后用这个对象去更新数据库,好处就是提高了代码的复用性,当修改其他的字段时,也可以调用同一个方法.

修改员工信息是常规crud不介绍

day3 and day4

菜品管理:对于一些公共字段进场需要填充,这里可以使用切面类来进行
@Component: 是 Spring 最基础的注解,作用是把普通类纳入 Spring 容器管理,让类成为 Spring 的 “Bean”;告诉 Spring 容器:“这个类需要被你管理,创建它的实例并放入容器中,其他地方可以通过依赖注入(@Autowired)使用它”。
@Aspect:标识一个类为切面类,这个类中会定义:切点(Pointcut):指定要拦截哪些方法(比如所有 com.example.service 包下的方法);通知(Advice):拦截方法后要执行的逻辑(比如前置日志、异常处理、事务控制)。
@Target(ElementType.METHOD):是 Java 内置的元注解(用来修饰注解的注解),作用是限定当前自定义注解能标注在什么位置,这里表示只能在方法上
@Retention(RetentionPolicy.RUNTIME),作用是限定注解的生命周期(保留到哪个阶段),RetentionPolicy.RUNTIME 表示:这个注解在源代码编译 → 字节码文件 → 程序运行时全程保留,程序运行时可以通过反射获取到这个注解的存在。

自动填充的过程:①首先自定义注解类,用于标识哪些方法需要进行公共字段进行填充

/**
 * 自定义注解,用于标识某个方法需要进行功能字段自动填充处理
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {
    //数据库操作类型,update,insert
    OperationType value();
}

②自定义切面类,统一拦截加入自定义注解的方法,通过反射为公共字段进行赋值

@Aspect
@Component
@Slf4j
public class AutoFillAspect {
    /**
     * 切入点,定义切点规则:拦截 com.sky.mapper 包下带 @AutoFill 注解的方法
     */
    @Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
    public  void autoFillPointCut(){}

    /**
     * 前置通知
     */
    @Before("autoFillPointCut()")  //核心作用是指定这个前置通知要作用于哪些方法。
    public void autoFill(JoinPoint joinPoint){
        log.info("开始公共字段进行填充");
        //获取当前被拦截的方法上的数据库类型操作
        MethodSignature signature = (MethodSignature)joinPoint.getSignature();
        AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class);
        OperationType operationType = autoFill.value(); //获取数据库操作类型字段
        //获取到当前被拦截的实体对象
        Object[] args = joinPoint.getArgs();
        if(args == null || args.length == 0){
            return;
        }
        //实体默认放到参数的第一个位置
        Object entity = args[0];
        //准备要赋值的数据
        if(operationType == OperationType.INSERT){
            try {
                Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
                Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);
                Method setCreateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class);
                Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
                //紧接着为属性赋值
                setUpdateTime.invoke(entity, LocalDateTime.now());
                setCreateTime.invoke(entity,LocalDateTime.now());
                setCreateUser.invoke(entity, UserHolder.getUserId());
                setUpdateUser.invoke(entity,UserHolder.getUserId());
            } catch (Exception e) {
                e.printStackTrace();
            }
        } else if (operationType == OperationType.UPDATE) {
            try {
                Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
                Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
                setUpdateTime.invoke(entity, LocalDateTime.now());
                setUpdateUser.invoke(entity,UserHolder.getUserId());
            } catch (Exception e) {
                e.printStackTrace();
            }

        }
    }

}

③在mapper上加入自定义注解即可

@AutoFill(value = OperationType.INSERT)
    void insert(Category category);  

AOP 拦截匹配,程序怎么知道要拦截的? 当你调用 categoryMapper.insert(category) 时,你实际调用的不是 Mapper 接口的原生实现,而是 Spring 为 Mapper 生成的代理对象—— 这个代理对象会先执行 “匹配切点规则” 的逻辑,再决定是否执行原方法。

当Spring Boot项目启动时,会执行以下关键步骤: 扫描切面类:Spring 会扫描所有带 @Component + @Aspect 的类(也就是 AutoFillAspect),把它加载为 Spring Bean; 解析切点表达式:Spring 会解析 @Pointcut 里的表达式,绑定通知和切点:Spring 会把 @Before("autoFillPointCut()") 关联到上面的切点规则, “只要匹配这个切点的方法,执行前先跑 autoFill() 方法”。当代码执行 categoryMapper.insert(category) 时,实际调用的是 Spring 生成的 Mapper 代理对象,此时会触发以下步骤:代理对象拦截方法调用:代理对象接收到 insert 方法的调用请求,不会立刻执行原方法;匹配切点规则:代理对象会检查当前调用的方法是否符合阶段 1 解析的切点规则,符合规则就需要拦截然后致型前置通知再执行原方法

腾讯云上传文件: :步骤①导入腾讯云相关插件②配置文件里面配置相关的信息比如id和密钥以及存储桶名称和位置③创建腾讯云上传类来进行上传文件
@Configuration 用来标记一个类是 Spring 的配置类,替代传统 Spring 中的 XML 配置文件(比如 applicationContext.xml)。Spring 容器会扫描并解析这个类,将其中定义的 Bean 加载到容器中管理。在 @Configuration 类中想要生产Bean,必须给对应的方法加上 @Bean 注解,少了这一步 Spring 容器是不会识别并管理这个 Bean 的。 @ConditionalOnMissingBean 注解的作用,这个注解是 Spring Boot 中非常实用的条件注解,核心作用是仅当容器中不存在指定类型 / 名称的 Bean 时,才会创建当前注解标注的 Bean。

image.png

PostMapping("/upload")
    @ApiOperation("文件上传")
    public Result<String> upload(MultipartFile file){
        log.info("文件上传到腾讯云:{}",file.getOriginalFilename());
        if(file.isEmpty()){
            return Result.error("文件为空上传失败");
        }
        //上传文件,要保证文件名字是唯一的
        //获取文件的原始文件名
        String originalFileName = file.getOriginalFilename();
        //substring 截取文件名索引位置到字符串末尾的片段
        String suffix = originalFileName.substring(originalFileName.lastIndexOf("."));
        String fileName = UUID.randomUUID().toString()+suffix;
        String fileAddress = null;
        try {
            //file.getBytes()文件全部内容读取到内存中
            fileAddress = tencentOsUtil.upload(file.getBytes(), fileName);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        return Result.success(fileAddress);
    }  

配置类:

@Data
@AllArgsConstructor
@NoArgsConstructor
@Slf4j
public class TencentOsUtil {

   private String secretId;
   private String secretKey;
   private String bucketName;
   private String region;
   public String upload(byte[] bytes,String ObjectName) {
       /**
        * 构建cos上传请求
        */
       //初始化cos客户端
       COSCredentials credentials = new BasicCOSCredentials(secretId, secretKey);
       ClientConfig clientConfig = new ClientConfig(new Region(region));
       COSClient cosClient = new COSClient(credentials, clientConfig);
       //上传路径
       String cosFilePath = "upload/"+ObjectName;
       //把byte[] 转换为字节流
       ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);
       //上传到cos
       PutObjectRequest request = new PutObjectRequest(bucketName, cosFilePath, inputStream, null);
       try {
           cosClient.putObject(request);
           log.info("cos文件上传成功,文件路径是:{}",cosFilePath);
           //拼接文件访问url
           String accessUrl = String.format("https://%s.cos.%s.myqcloud.com/%s", bucketName, region, cosFilePath);
           return accessUrl;
       } catch (CosClientException e) {
           log.error("cos客户端异常");
       }finally {
           if(inputStream != null){
               try {
                   inputStream.close();
               } catch (IOException e) {
                   log.error("关闭输入流失败",e);
               }
           }
           if(cosClient != null){
               cosClient.shutdown();//关闭cos客户端,释放连接池
           }
       }


       return null;
   }
}
/**
 * 配置类,用于创建tencentUtil对象
 */
@Configuration
@Slf4j
public class TencentConfiguration {
    @Bean
    @ConditionalOnMissingBean
    public TencentOsUtil tencentOsUtil(TencentOsProperties tencentOsProperties){
        return new TencentOsUtil(tencentOsProperties.getSecretId(),tencentOsProperties.getSecretKey(), tencentOsProperties.getBucketName(),
                tencentOsProperties.getRegion());
    }
}  

添加菜品注意点是多条口味数据批量插入,优化性能

<insert id="insertDishFlavors" parameterType="list" useGeneratedKeys="true" keyColumn="id" keyProperty="id">
            insert into sky_take_out.dish_flavor(dish_id, name, value) VALUES 
                                    <foreach collection="dishFlavors" separator="," item="flavor">
                                        (#{flavor.dishId},#{flavor.name},#{flavor.value})
                                    </foreach>
    </insert>  

parameterType="list":指定传入参数的类型为 List 集合
collection="dishFlavors":指定要遍历的集合参数名称,低版本需要用参数类型
separator=",":指定遍历集合时,每个元素生成的 SQL 片段之间的分隔符
item="flavor":遍历集合时,给当前元素起的别名(后续通过 #{flavor.属性名} 取值)
对查询菜品的性能优化: 还有一种就是直接多表查询即可

 //开启分页
        PageHelper.startPage(dishPageQueryDTO.getPage(),dishPageQueryDTO.getPageSize());
        Page<Dish> dishes = dishMapper.pageQuery(dishPageQueryDTO);
        //封装
        List<DishVO> dishVOS = new ArrayList<>();
        Long total = dishes.getTotal();
        if(total == 0){
            return new PageResult(0,dishVOS);
        }
        List<Dish> result = dishes.getResult();
        //查重category表获得分类名称,第一种就是每次都去数据库查询分类名称,这里可以性能优化,将名称先查询出来,一次,再放入map中,后面再提取即可
        //1.提取所有的菜品的分类id
        Set<Long> categoryIds = result.stream().map(Dish::getCategoryId).filter(Objects::nonNull).collect(Collectors.toSet());
        //批量查询所有分类,转换为map
        Map<Long,Category> categoryMap = new HashMap<>();
        if(categoryIds!=null){
            //一次拿到所有的分类
            List<Category> categories = categoryMapper.selectCategoryByIds(categoryIds);
            categoryMap.putAll(categories.stream().collect(Collectors.toMap(Category::getId, category -> category)));
        }
        dishVOS = result.stream().map(dish -> {
            DishVO dishVO = new DishVO();
            BeanUtils.copyProperties(dish, dishVO);
            Category category = categoryMap.get(dish.getCategoryId());
            dishVO.setCategoryName(category!=null?category.getName():"未知分类");
            return dishVO;
        }).collect(Collectors.toList());
        return new PageResult(total,dishVOS);  

套餐接口类也都是普通的增删改查,无知识点

day5 and day6

redis:
redis的基本操作类型:字符串,哈希,列表,集合,有序集合
使用方式:①导入Spring Data Redis的maven坐标(操作redis)②配置redis数据源③编写配置类创建RedisTemplate对象④通过RedisTemplate对象操作redis;代码不在列出,上网搜索即可.
HttpClient介绍 :可以让Java 程序像浏览器一样,主动发送 HTTP 请求(GET/POST/PUT/DELETE 等),并接收服务器返回的响应(状态码、响应体、响应头)。简单说:浏览器是 “人” 访问网页,HttpClient 是 “Java 程序” 访问网页 / 接口。在开发中会调用第三方接口,或者微服务之间的接口调用等都会使用倒HttpClient
使用过程: :导入相关的依赖包->创建 HttpClient对象->创建 Http 请求对象->调用 HttpClient的execute方法发送请求

public class HttpClientDemo {
    public static void main(String[] args) throws Exception {
        // 1. 创建 HttpClient 实例(相当于打开一个浏览器)
        CloseableHttpClient httpClient = HttpClients.createDefault();
        
        // 2. 创建 GET 请求(相当于在地址栏输入网址)
        HttpGet httpGet = new HttpGet("https://www.baidu.com");
        
        // 3. 发送请求,获取响应(相当于点击回车,接收网页内容)
        CloseableHttpResponse response = httpClient.execute(httpGet);
        
        // 4. 解析响应(获取状态码、响应体)
        System.out.println("状态码:" + response.getStatusLine().getStatusCode());
        String responseBody = EntityUtils.toString(response.getEntity(), "UTF-8");
        System.out.println("响应内容:" + responseBody);
        
        // 5. 关闭资源(相当于关闭浏览器)
        response.close();
        httpClient.close();
    }
}  

文件配置 :传统 Java 项目常用.properties文件做配置,格式是key=value,层级嵌套时可读性差;而 YML 使用缩进(空格)表示层级,结构更清晰,支持列表、嵌套对象等复杂结构
@ConfigurationProperties(prefix = "sky.wechat"):告诉 Spring,去配置文件中读取以sky.wechat为前缀的所有配置项,自动映射到这个类的属性上,并把这些配置值封装成一个可复用的 Java 对象,方便在项目其他地方调用。

@Component
@ConfigurationProperties(prefix = "sky.wechat")
@Data
public class WeChatProperties {

    private String appid; //小程序的appid
    private String secret; //小程序的秘钥
    private String mchid; //商户号
    private String mchSerialNo; //商户API证书的证书序列号
    private String privateKeyFilePath; //商户私钥文件
    private String apiV3Key; //证书解密的密钥
    private String weChatPayCertFilePath; //平台证书
    private String notifyUrl; //支付成功的回调地址
    private String refundNotifyUrl; //退款成功的回调地址

}

day7 and day8 and day9 进入redis学习

Redis 中存储业务数据时,字段(Key)该如何规范命名,以及不同场景该选什么数据类型:
命名格式: 业务模块:数据类型:唯一标识[:子标识]

image.png
具体事例:

image.png
redis基本数据类型使用场景:

image.png
redis缓存清理 :当管理端修改增加某些数据时,需要同步的对redis缓存里面的数据进行修改

SpringCache的介绍和使用 :Spring Cache 是 Spring 框架提供的缓存抽象层,它不直接实现缓存,而是定义了一套统一的缓存注解和接口,底层可以对接 Redis、Ehcache、Caffeine 等具体缓存实现。
价值: 解耦缓存逻辑和业务逻辑,只需加注解就能给方法添加缓存能力,无需手动编写redisTemplate.opsForValue().set()等代码;
常用注解:

image.png
例子:

@RestController
@RequestMapping("/user")
@Slf4j
@EnableCaching
public class UserController {

    @Autowired
    private UserMapper userMapper;

    @PostMapping
    @CachePut(cacheNames ="userCache",key = "#user.id")  //如果用springCache缓存数据,那么key的生成 cacheNames::key
    //@CachePut(cacheNames ="userCache",key = "#result.id")  //对象导航
    //@CachePut(cacheNames ="userCache",key = "#a0.id") //取方法的第一个参数,#p0.id也可以,#root.args[0].id也可以
    public User save(@RequestBody User user){
        userMapper.insert(user);
        return user;
    }

    @DeleteMapping
    @CacheEvict(cacheNames = "userCache",key = "#id")
    public void deleteById(Long id){
        userMapper.deleteById(id);
    }

	@DeleteMapping("/delAll")
    @CacheEvict(cacheNames = "userCache",allEntries = true) //删除该缓存下的所有键值对
    public void deleteAll(){
        userMapper.deleteAll();
    }

    @GetMapping
    @Cacheable(cacheNames = "userCache",key = "#id")
    public User getById(Long id){
        User user = userMapper.getById(id);
        return user;
    }

@RestController
public class UserController {
    @PostMapping
    @CachePut(cacheNames = "userCache", key = "#result.id")
    public User save(@RequestBody User user) {
        userMapper.insert(user); // 核心业务逻辑
        return user;
    }
}  

spring Cache注解的执行原理: 例如上面代码 ①Spring 启动时,发现这个类有@RestController(被 Spring 管理),且方法有@CachePut注解;②Spring 会自动为UserController创建一个代理对象(JDK/CGLIB 代理);③当你调用save方法时,实际调用的是代理对象的 save 方法,而非原对象的方法;④代理对象的逻辑:先调用原对象的save方法(执行插入数据库、主键回填),拿到返回的user对象后,解析@CachePut注解的规则(cacheNames=userCachekey=#result.id),把user对象存入 Redis(Key:userCache::用户ID),最后返回user对象给调用者。如果没有代理对象,步骤 4 的缓存逻辑就无法 “插入” 到方法执行流程中,注解也就完全失效。

代理对象介绍: 代理对象就像业务对象的 “代言人” :你原本要直接调用UserController.save()方法(目标对象),但 Spring 会创建一个代理对象包裹原对象,你实际调用的是代理对象的save()方法;代理对象会在调用原方法前后,自动执行额外逻辑(如缓存、事务、日志),最后再调用原对象的方法。
代理的两种类型:

image.png Spring 自动创建代理对象: ①目标对象被 Spring 管理(加@Controller/@Service/@Component)②方法上添加 Spring 注解(如@CachePut、@Transactional)。

// 1. 目标对象(被Spring管理)
@RestController // 关键:Spring会为这个类创建代理对象
public class UserController {
    @Autowired
    private UserMapper userMapper;

    // 2. 标注注解,Spring代理会自动处理缓存逻辑
    @PostMapping
    @CachePut(cacheNames = "userCache", key = "#result.id")
    public User save(@RequestBody User user) {
        userMapper.insert(user); // 核心业务逻辑
        return user;
    }
}

// 3. 调用方式(通过Spring容器获取代理对象)
@Service
public class TestService {
    @Autowired
    private UserController userController; // 注入的是代理对象,而非原对象

    public void test() {
        // 实际调用的是代理对象的save方法,缓存逻辑自动执行
        userController.save(new User("张三", 25));
    }
}  

也可以手动创建代理对象: 分为上面的接口和无接口场景
注意: 内部方法调用、手动 new 对象会导致代理失效,注解(如@CachePut)无法生效。

所有返回 List 类型的方法,无论是否查到数据,都会返回一个有效的 List 对象(不会返回 null);只是查到数据时列表有元素,查不到时列表为空(元素数量为 0)。

当sql语句需要二选一时,用choose不用if,choose只会执行第一个选中的,其他的when都会忽略掉

<delete id="deleteCarts">
        delete from sky_take_out.shopping_cart where user_id = #{userId}
        <choose>
            <when test="shoppingCartDTO.dishId !=null"> and  dish_id = #{shoppingCartDTO.dishId}</when>
            <when test="shoppingCartDTO.setmealId !=null">and setmeal_id = #{shoppingCartDTO.setmealId} </when>
            <otherwise>
                and 1=0 <!-- 让SQL永远不执行,或抛异常 -->
            </otherwise>
        </choose>

    </delete>  

微信支付时序图:

image.png

String.join("",orderDishList); 将数组用""分隔符拼接到一起

day10 and # day11 and dya 12

JSON.toJSONString()可以将数据转化为json格式

Spring task: 可以自己定时执行某一段代码
应用场景 :清理过期数据,清理用户上传的临时课件 / 作业文件(比如 24 小时未提交的草稿文件),清理 Redis 中过期的登录验证码、临时课程预览链接。订单超时,定时数据统计等 使用方法::加入Maven坐标->在启动类上添加@EnableScheduling注解,然后创建任务,在任务上加入@Scheduled(fixedRate = 5000)即可

@Component
@Slf4j
public class OrderTask {
    @Autowired
    private OrderMapper orderMapper;
    /**
     * 处理超时订单的方法
     */

    @Scheduled(fixedRate = 5000)
    public void processTimeoutOrder(){
        log.info("定时处理超时订单:{}", LocalDateTime.now());
        //处理代付款状态的订单
        List<Orders> ordersList = orderMapper.getByStatusAndOrderTimeLT(Orders.PENDING_PAYMENT,LocalDateTime.now().plusMinutes(-15));
        if(ordersList !=null && ordersList.size() >0){
            for(Orders orders:ordersList){
                //将订单的状态修改成取消,并且要进行退款
                orders.setStatus(Orders.CANCELLED);
                orders.setCancelReason("订单超时自动取消");
                orders.setCancelTime(LocalDateTime.now());
                orderMapper.update(orders);
            }
        }else{
            System.out.println("没有超时订单");
        }
    }
    /**
     * 处理派送中的订单
     *
     */
    @Scheduled(cron ="20 * * * * ?" )
    public void processDeliveryOrder(){
        log.info("处理一直在派送中的订单:{}",LocalDateTime.now());
        List<Orders> ordersList = orderMapper.getByStatusAndOrderTimeLT(Orders.DELIVERY_IN_PROGRESS, LocalDateTime.now().plusMinutes(-60));
        if(ordersList !=null && ordersList.size() >0){
            for(Orders orders:ordersList){
                //将订单的状态修改成已完成
                orders.setStatus(Orders.COMPLETED);
                orderMapper.update(orders);
            }
        }else{
            System.out.println("没有一直派送的订单");
        }
    }
}  

WebSocket介绍:WebSocket 是一种全双工、长连接的网络通信协议(协议标识:ws:///wss://),能在客户端(浏览器)和服务器之间建立持续的双向通信通道,实现服务器主动向客户端推送数据,无需客户端频繁轮询。底层和http一样都是基于TCP连接
应用场景: :在线聊天,实时消息通知
使用方式:①配置类

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

@Configuration
public class WebSocketConfiguration {

    // 注册ServerEndpointExporter,让Spring识别@ServerEndpoint注解
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}  

②:定义 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();
            }
        }
    }

}
 

使用例子:

@Override
    public void reminder(Long id) {
        Orders ordersDB = orderMapper.queryOrder(id);
        if(ordersDB == null ){
            throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR);
        }
        //进行催单
        Map map = new HashMap();
        map.put("type",2);//1来单提醒,2客户催单
        map.put("orderId:",id);
        map.put("content","订单号:"+ordersDB.getNumber());
        String json = JSON.toJSONString(map);
        webSocketServer.sendToAllClient(json);
    }

Apache Echarts :用于绘制图表数据,前端内容,官网可以查询资料
@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin 这是一个用于日格式化的注解,将传过来的对象转换成这个格式
StringUtils.join(localDateList, ","):将列表转换为字符串
Apche POI:用于操作excel文件,下面有代码例子,一般来说对于导出文件,需要自己先创建一个模板文件然后导出即可.导出过程: 创建获取()excel文件->创建(获取)标签页sheet->创建(获取)行然后进行数据填充.
HttpServletResponse: 是 Servlet 规范中定义的核心接口,中文译作「HTTP 响应对象」,代表服务器向客户端(浏览器 / 客户端程序)返回的所有内容(包括响应头、响应体、状态码等)。

public class POITest {

//通过poi创建excel文件并且写入文件内容



    //poi创建excel文件并且写入文件内容
    public static void write() throws IOException {
        //内存中创建excel文件
        XSSFWorkbook excel = new XSSFWorkbook();
        //文件中创建一个sheet页
        XSSFSheet sheet = excel.createSheet("info");
        //在sheet中创建行对象,rownum编号从0开始
        XSSFRow row = sheet.createRow(1);
        //创建单元格并且写入文件内容
        row.createCell(1).setCellValue("姓名");
        row.createCell(2).setCellValue("城市");

        //创建一个新行
        row = sheet.createRow(2);
        row.createCell(1).setCellValue("张三");
        row.createCell(2).setCellValue("北京");

        row = sheet.createRow(3);
        row.createCell(1).setCellValue("李斯");
        row.createCell(2).setCellValue("南京");

        FileOutputStream out = new FileOutputStream(new File("D:\\info.xlsx"));
        excel.write(out);
        //关闭资源
        out.close();
        excel.close();
    }


//poi读取excel文件

    public static void read() throws Exception{
        //读取磁盘上已经存在的文件
        String filePath = "D:\\info.xlsx";
        FileInputStream in = new FileInputStream(filePath);
        XSSFWorkbook excel = new XSSFWorkbook(in);
        //读取第一个sheet页
        XSSFSheet sheet = excel.getSheetAt(0);
        //获取最后一行的行号
        int lastRowNum = sheet.getLastRowNum();
        for (int i = 1; i <= lastRowNum; i++) {
            //获得某一行
            XSSFRow row = sheet.getRow(i);
            //获得每一行的单元格对象
            String cellValue1 = row.getCell(1).getStringCellValue();
            String cellValue2 = row.getCell(2).getStringCellValue();
            System.out.println(cellValue1+"  "+cellValue2);
        }
        //关闭资源
        excel.close();
        in.close();
    }
@Override
    public void exportBusinessData(HttpServletResponse response) {
        //1.查询数据库,获取营业数据
        LocalDate begin = LocalDate.now().minusDays(30);
        LocalDate end = LocalDate.now().minusDays(1);
        BusinessDataVO businessDataVO = workspaceService.getBusinessData(begin,end );


        //2.通过POI将数据写入到excel文件中
        InputStream in = this.getClass().getClassLoader().getResourceAsStream("template/运营数据报表模板.xlsx");
        try {
            XSSFWorkbook excel = new XSSFWorkbook(in);
            //对应模板填充数据
            //获取标签页
            XSSFSheet sheet = excel.getSheet("Sheet1");
            //设置行数据
            sheet.getRow(1).getCell(1).setCellValue("时间:"+LocalDate.now().minusMonths(1)+"----"+LocalDate.now().minusDays(1));
            sheet.getRow(3).getCell(2).setCellValue(businessDataVO.getTurnover());
            sheet.getRow(3).getCell(4).setCellValue(businessDataVO.getOrderCompletionRate());
            sheet.getRow(3).getCell(6).setCellValue(businessDataVO.getNewUsers());
            sheet.getRow(4).getCell(3).setCellValue(businessDataVO.getValidOrderCount());
            sheet.getRow(4).getCell(2).setCellValue(businessDataVO.getUnitPrice());

            //填充明细数据
            for (int i = 0; i < 30; i++) {
                LocalDate date = begin.plusDays(i);
                BusinessDataVO businessData = workspaceService.getBusinessData(date,date);
                //从第8行开始填充
                sheet.getRow(7+i).getCell(1).setCellValue(date.toString());
                sheet.getRow(7+i).getCell(2).setCellValue(businessDataVO.getTurnover());
                sheet.getRow(7+i).getCell(3).setCellValue(businessData.getValidOrderCount());
                sheet.getRow(7+i).getCell(4).setCellValue(businessData.getOrderCompletionRate());
                sheet.getRow(7+i).getCell(5).setCellValue(businessData.getUnitPrice());
                sheet.getRow(7+i).getCell(6).setCellValue(businessData.getNewUsers());
            }

            //3.通过输出流将excel文件下载到浏览器
            ServletOutputStream out = response.getOutputStream();
            excel.write(out);
            out.close();
            excel.close();
        } catch (IOException e) {
            e.printStackTrace();

        } finally {

        }