项目-cqwm

image-20250422095318781

前端环境

jwt令牌

  • 相当于一个证件,以后改用户每次请求都要经过验证

    package com.sky.utils;
    
    import io.jsonwebtoken.Claims;
    import io.jsonwebtoken.JwtBuilder;
    import io.jsonwebtoken.Jwts;
    import io.jsonwebtoken.SignatureAlgorithm;
    import java.nio.charset.StandardCharsets;
    import java.util.Date;
    import java.util.Map;
    
    public class JwtUtil {
        /**
         * 生成jwt
         * 使用Hs256算法, 私匙使用固定秘钥
         *
         * @param secretKey jwt秘钥
         * @param ttlMillis jwt过期时间(毫秒)
         * @param claims    设置的信息
         * @return
         */
        public static String createJWT(String secretKey, long ttlMillis, Map<String, Object> claims) {
            // 指定签名的时候使用的签名算法,也就是header那部分
            SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
    
            // 生成JWT的时间
            long expMillis = System.currentTimeMillis() + ttlMillis;
            Date exp = new Date(expMillis);
    
            // 设置jwt的body
            JwtBuilder builder = Jwts.builder()
                    // 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
                    .setClaims(claims)
                    // 设置签名使用的签名算法和签名使用的秘钥
                    .signWith(signatureAlgorithm, secretKey.getBytes(StandardCharsets.UTF_8))
                    // 设置过期时间
                    .setExpiration(exp);
    
            return builder.compact();
        }
    
        /**
         * Token解密
         *
         * @param secretKey jwt秘钥 此秘钥一定要保留好在服务端, 不能暴露出去, 否则sign就可以被伪造, 如果对接多个客户端建议改造成多个
         * @param token     加密后的token
         * @return
         */
        public static Claims parseJWT(String secretKey, String token) {
            // 得到DefaultJwtParser
            Claims claims = Jwts.parser()
                    // 设置签名的秘钥
                    .setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8))
                    // 设置需要解析的jwt
                    .parseClaimsJws(token).getBody();
            return claims;
        }
    
    }
    

md5加密

  • 数据库先改,md5加密工具

  • 方法验证加上

    password = DigestUtils.md5DigestAsHex(password.getBytes());
    

swagger接口文档

  1. 生成文档
  2. 测试
  • 使用方法

  • 导入坐标

  • knife4j 是java mvc框架集成swagger生成api文档增强解决方案

    <dependency>
        <groupId>com.github.xiaoymin</groupId>
        <artifactId>knife4j-spring-boot-starter</artifactId>
    </dependency>
    
  • 配置类,配置属性

        /**
         * 通过knife4j生成接口文档
         * @return
         */
    @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;
    }
    
  • 配置类,配置静态资源过滤,防止访问swagger静态资源被以为是请求

    /**
     * 设置静态资源映射
     * @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/");
    }
    

@Slf4j

  • 开启日志,可以控制台输出了

    log.info("开始注册自定义拦截器...");
    

拷贝属性

  • DTO到普通数据库实体,要求属性名相同

    Employee employee = new Employee();
    // 拷贝属性
    BeanUtils.copyProperties(employeeDTO,employee);
    

时间

//@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
// 创建修改时间
employee.setCreateTime(LocalDateTime.now());
employee.setUpdateTime(LocalDateTime.now());

时间戳

  • 1970.1.1 0:0:0到现在的ms数
  • 可以拿来做订单号
System.currentTimeMillis();

sql提示插件

  • mybatis-x

swagger设置jwt令牌

  • 所有controller请求前 验证jwt

    package com.sky.interceptor;
    
    import com.sky.constant.JwtClaimsConstant;
    import com.sky.properties.JwtProperties;
    import com.sky.utils.JwtUtil;
    import io.jsonwebtoken.Claims;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    import org.springframework.web.method.HandlerMethod;
    import org.springframework.web.servlet.HandlerInterceptor;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    /**
     * jwt令牌校验的拦截器
     */
    @Component
    @Slf4j
    public class JwtTokenAdminInterceptor implements HandlerInterceptor {
    
        @Autowired
        private JwtProperties jwtProperties;
    
        /**
         * 校验jwt在所有controller请求前
         *
         * @param request
         * @param response
         * @param handler
         * @return
         * @throws Exception
         */
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            //判断当前拦截到的是Controller的方法还是其他资源
            if (!(handler instanceof HandlerMethod)) {
                //当前拦截到的不是动态方法,直接放行
                return true;
            }
    
            //1、从请求头中获取令牌
            String token = request.getHeader(jwtProperties.getAdminTokenName());
    
            //2、校验令牌
            try {
                log.info("jwt校验:{}", token);
                Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);
                Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());
                log.info("当前员工id:", empId);
                //3、通过,放行
                return true;
            } catch (Exception ex) {
                //4、不通过,响应401状态码
                response.setStatus(401);
                return false;
            }
        }
    }
    
  • 从登录获取jwt令牌,叫token,在yml资源里面设置名字了

    添加到swagger,之后请求头会加上这个,验证才能通过

    image-20250401212039315

处理注册时用户登录名重复500错

  • SQLIntegrityConstraintViolationException: Duplicate entry '666' for key 'idx_username'

  • 定义全局异常处理器

    @ExceptionHandler
    public Result exceptionHandler(SQLIntegrityConstraintViolationException ex){
        // SQLIntegrityConstraintViolationException
        // Duplicate entry '666' for key 'idx_username'
        log.error("异常信息:{}", ex.getMessage());
        if (ex.getMessage().contains("Duplicate entry")){
            String[] msg = ex.getMessage().split(" ");
            log.info(Arrays.toString(msg));
            return Result.error(msg[2] + MessageConstant.ALREADY_EXISTS);
        }else return Result.error(MessageConstant.UNKNOWN_ERROR);
    }
    

log占位符{}

log.info("jwt校验:{}", token);// token实际值会替代{}

线程空间

  • 每次请求都是新的线程,线程中可以存有线程空间实现当前线程变量共用

  • threadLocal 存取删

  • 定义包装工具类BaseContext和对应方法

    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();
        }
    
    }
    
    
  • jwt验证时存入空间

    String token = request.getHeader(jwtProperties.getAdminTokenName());
    
    log.info("jwt校验:{}", token);
    Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);
    Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());
    log.info("当前员工id:{}", empId);
    // 在线程空间存入id
    BaseContext.setCurrentId(empId);
    //3、通过,放行
    return true;
    
  • service类取出

    // 创建修改人id
    // TODO 用线程空间实现获取修改人id,先解析再放入线程空间(已完成)
    employee.setCreateUser(BaseContext.getCurrentId());
    employee.setUpdateUser(BaseContext.getCurrentId());
    

JWT工作流程原理

  • 用户提交用户名密码认证,成功生成加密JWT 前端保存JWT,每次前端请求都在头携带JWT,后端在执行controller前拦截验证JWT,通过才执行,展示数据,不通过就返回错误信息

image-20250402171351015

查询封装

  • DTO 用户传给后台的参数数据
  • VO 后台返回给前端展示的数据

concat

mqsql函数

<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>
     order by create_time desc
</select>

pagehelper

  • 导入坐标

  • serviceimpl使用,本质也是用threadlocal 存数据,然后动态拦截拼接

    @Override
    public PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO) {
        PageHelper.startPage(employeePageQueryDTO.getPage(),employeePageQueryDTO.getPageSize());
        Page<Employee> page = employeeMapper.pageQuery(employeePageQueryDTO);
        Long total = page.getTotal();
        List<Employee> records = page.getResult();
    
        return new PageResult(total,records);
    }
    

实现代码流程

  1. 看产品原型,定义好接口请求方式,接口传入参数,返回参数

    例:分页查询 需要传入PageQueryDTO

    package com.sky.dto;
    
    import lombok.Data;
    
    import java.io.Serializable;
    
    @Data
    public class EmployeePageQueryDTO implements Serializable {
    
        //员工姓名
        private String name;
    
        //页码
        private int page;
    
        //每页显示记录数
        private int pageSize;
    
    }
    

    统一返回Result

    package com.sky.result;
    
    import lombok.Data;
    
    import java.io.Serializable;
    
    /**
     * 后端统一返回结果
     * @param <T>
     */
    @Data
    public class Result<T> implements Serializable {
    
        private Integer code; //编码:1成功,0和其它数字为失败
        private String msg; //错误信息
        private T data; //数据
    
        public static <T> Result<T> success() {
            Result<T> result = new Result<T>();
            result.code = 1;
            return result;
        }
    
        public static <T> Result<T> success(T object) {
            Result<T> result = new Result<T>();
            result.data = object;
            result.code = 1;
            return result;
        }
    
        public static <T> Result<T> error(String msg) {
            Result result = new Result();
            result.msg = msg;
            result.code = 0;
            return result;
        }
    
    }
    

    此处返回数据,查询一般才有,分页返回数据统一为pageresult

    package com.sky.result;
    
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    
    import java.io.Serializable;
    import java.util.List;
    
    /**
     * 封装分页查询结果
     */
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class PageResult implements Serializable {
    
        private long total; //总记录数
    
        private List records; //当前页数据集合
    
    }
    
  2. 定义完接口后,根据接口定义,从controller开始编写,返回result,传入dto,

    controller调用service,业务在service实现

    @GetMapping("/page")
    @ApiOperation("员工分页查询")
    public Result<PageResult> page(EmployeePageQueryDTO employeePageQueryDTO){
        log.info("打印DTO:{}",employeePageQueryDTO);
        log.info("当前时间:{}",LocalDateTime.now());
        PageResult pageResult = employeeService.pageQuery(employeePageQueryDTO);
        return Result.success(pageResult);
    }
    
  3. service先写接口,再实现类编写具体

    serviceimpl调用mapper,mapper实现具体sql,service想办法实现返回类型

    @Override
    public PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO) {
        PageHelper.startPage(employeePageQueryDTO.getPage(),employeePageQueryDTO.getPageSize());
        Page<Employee> page = employeeMapper.pageQuery(employeePageQueryDTO);
        Long total = page.getTotal();
        List<Employee> records = page.getResult();
    
        return new PageResult(total,records);
    }
    
  4. mapper具体实现sql,动态sql要在xml编写

    Page<Employee> pageQuery(EmployeePageQueryDTO employeePageQueryDTO);
    
    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
    <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>
             order by create_time desc
        </select>
    </mapper>
    

json和@RequestBody和get post

关键区别

方式 适用场景 HTTP方法 Content-Type 参数位置
@RequestBody 复杂数据(如JSON) POST/PUT application/json 请求体
查询参数或表单数据 简单键值对 GET x-www-form-urlencoded URL查询字符串

错误示例分析

  • 错误情况:在GET方法中使用@RequestBody,客户端发送JSON请求体。
  • 结果:服务器无法解析请求体,抛出异常,因为GET请求的请求体通常被忽略。

总结

  • 需要传递JSON数据 → 使用@PostMapping + @RequestBody
  • 保持GET请求 → 移除@RequestBody,通过查询字符串传参。

JWT令牌过期

sky:
  jwt:
    # 设置jwt签名加密时使用的秘钥
    admin-secret-key: itcast
    # 设置jwt过期时间 7200s=2h
    admin-ttl: 7200000
    # 设置前端传递过来的令牌名称
    admin-token-name: token
package com.sky.properties;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
// TODO 通过yml配置文件加载
@ConfigurationProperties(prefix = "sky.jwt")
@Data
public class JwtProperties {

    /**
     * 管理端员工生成jwt令牌相关配置
     */
    private String adminSecretKey;
    private long adminTtl;
    private String adminTokenName;

    /**
     * 用户端微信用户生成jwt令牌相关配置
     */
    private String userSecretKey;
    private long userTtl;
    private String userTokenName;

}

注意sql函数concat

{name}不能用单引号包

<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>
     order by create_time desc
</select>

实现日期合理显示

  1. 注解,直接改json显示格式(方便,要改的多了不推荐)

    //    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
        private LocalDateTime createTime;
    
        //@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
        private LocalDateTime updateTime;
    
  2. 定义消息转换器,加入springmvc官方转换器集合,消息转换器内加上对象转换器(规则)

    /**
     * 扩展springmvc的消息转化器
     * @param converters
     */
    @Override
    protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        // 创建一个消息转换器对象
        MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
        // JacksonObjectMapper为规则 对象转换器,可以将java对象序列化为json
        converter.setObjectMapper(new JacksonObjectMapper());
        // 将消息转化器装入springmvc自带的转化器集合list 记得设置为第一个index=0,才能有效使用,顺序生效
        converters.add(0,converter);
    }
    

git提交更新

  1. image-20250403091644162选择勾
  2. 无脑提交并推送

Restful风格

  • http://localhost/api/employee/status/0?id=11 请求路径

  • /status/0 为restful风格 参数记得加@PathVariable

    ? id = 11 普通地址栏传参

    @PostMapping("/status/{status}")
    @ApiOperation("员工账号启用禁用")
    public Result startOrStop(@PathVariable Integer status,Long id){
        log.info("启用禁用员工账户:{},{}",status,id);
        employeeService.startOrStop(status,id);
        return Result.success();
    }
    

请求和增删改查的对应

  • get 查询操作

    有返回,不用携带

  • post 增加操作

    无返回,要json携带

  • put 修改操作

    无返回,要json携带

  • delete 删除操作

    无返回,地址栏传参普通携带

新初始化对象方式build

  • Employee 类上加@Builder注解

  • 使用

    Employee employee = Employee.builder()
            .id(id)
            .status(status)
            .build();java
    

动态sql注意点

  • set标签内最后记得加,逗号分割

  • sql语句 update_time = #{updateTime},

    注意前面后面不一定一样,前面是sql数据库,后面是实体类,要对应

    <update id="update" parameterType="employee">
        update employee
        <set>
            <if test="name!=null">name = #{name},</if>
            <if test="username!=null">username = #{username},</if>
            <if test="password!=null">password = #{password},</if>
            <if test="phone!=null">phone = #{phone},</if>
            <if test="sex!=null">sex = #{sex},</if>
            <if test="idNumber!=null">id_number = #{idNumber},</if>
            <if test="status!=null">status = #{status},</if>
            <if test="updateTime!=null">update_time = #{updateTime},</if>
            <if test="updateUser!=null">update_user = #{updateUser},</if>
        </set>
        where id = #{id};
    </update>
    

公共字段填充(反射)

  1. 定义注解

    package com.sky.annotation;
    
    import com.sky.enumeration.OperationType;
    
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    /**
     * 自定义标识注解
     */
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface AutoFill {
        // 标识是insert还是update
        OperationType value();
    }
    
  2. 定义切入点和切面

    package com.sky.aspect;
    
    import com.sky.annotation.AutoFill;
    import com.sky.constant.AutoFillConstant;
    import com.sky.context.BaseContext;
    import com.sky.enumeration.OperationType;
    import lombok.extern.slf4j.Slf4j;
    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.Signature;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    import org.aspectj.lang.annotation.Pointcut;
    import org.aspectj.lang.reflect.MethodSignature;
    import org.springframework.stereotype.Component;
    
    import java.lang.reflect.Method;
    import java.time.LocalDateTime;
    
    /**
     * 自定义切面类
     */
    @Component
    @Slf4j
    @Aspect
    public class AutoFillAspect {
    
        /**
         * 切入点 3个*为返回类型,类名,方法名
         */
        @Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
        public void autoFillPointCut(){
        }
    
        /**
         * 切入面,要切什么,在哪切
         */
        @Before("autoFillPointCut()")
        public void autoFill(JoinPoint joinPoint){
            log.info("切入方法:{}",joinPoint);
            // 获取操作类型 insert update
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            log.info("切入signature:{}",signature);
            AutoFill annotation = signature.getMethod().getAnnotation(AutoFill.class);
            OperationType type = annotation.value();
    
            // 获取对象
            Object[] args = joinPoint.getArgs();
            if (args==null||args.length==0){
                return;
            }
            Object entity = args[0];
    
            // 获取要修改的值
            LocalDateTime now = LocalDateTime.now();
            Long currentId = BaseContext.getCurrentId();
    
            // 根据操作类型进行修改
            if (type==OperationType.INSERT){
                try {
                    Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);
                    Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_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);
    
                    setCreateTime.invoke(entity,now);
                    setUpdateTime.invoke(entity,now);
                    setCreateUser.invoke(entity,currentId);
                    setUpdateUser.invoke(entity,currentId);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }else if (type==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,now);
                    setUpdateUser.invoke(entity,currentId);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
    
  3. 在mapper对应修改和删除方法上加注解

    @AutoFill(value = OperationType.UPDATE)
    void update(Employee employee);
    
    @AutoFill(value = OperationType.INSERT)
    void save(Employee employee);
    
    @AutoFill(value = OperationType.INSERT)
    void insert(Category category);
    
    @AutoFill(value = OperationType.UPDATE)
    void update(Category category);
    

aliosss配置

  • application-dev.yml

    具体配置

    sky:
      datasource:
        driver-class-name: com.mysql.cj.jdbc.Driver
        host: localhost
        port: 3306
        database: sky_take_out
        username: root
        password: 123456
      alioss:
        endpoint: oss-cn-beijing.aliyuncs.com
        access-key-id: LTAI5tS3dSuNTc2F2H4azLCM
        access-key-secret: 2JdiyCpCXEdRmX27TAi3wu6B4MmxNb
        bucket-name: sky---heima
    
  • 主文件application.yml引用方法

    ${sky.alioss.endpoint}

    spring:
      profiles:
        active: dev
    
    sky:
      jwt:
        # 设置jwt签名加密时使用的秘钥
        admin-secret-key: itcast
        # 设置jwt过期时间 7200s=2h
        admin-ttl: 7200000
        # 设置前端传递过来的令牌名称
        admin-token-name: token
      alioss:
        endpoint: ${sky.alioss.endpoint}
        access-key-id: ${sky.alioss.access-key-id}
        access-key-secret: ${sky.alioss.access-key-secret}
        bucket-name: ${sky.alioss.bucket-name}
    
  • aliossproperties

    引用配置

    package com.sky.properties;
    
    import lombok.Data;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.stereotype.Component;
    
    @Component
    @ConfigurationProperties(prefix = "sky.alioss")
    @Data
    public class AliOssProperties {
    
        private String endpoint;
        private String accessKeyId;
        private String accessKeySecret;
        private String bucketName;
    
    }
    

MultipartFile

  • 简化操作,直接接收文件,要求前后端名字一致file

  • MultipartFile 简化了 Spring 中文件上传的操作,使得开发者可以专注于业务逻辑(如文件存储、处理),而无需关注底层 HTTP 协议的细节。通过合理配置和异常处理,可以构建安全可靠的文件上传功能。

  • MultipartFile 是 Spring Framework 中用于处理 HTTP 文件上传的核心接口。在您提供的代码中,它被用来接收客户端通过 multipart/form-data 格式上传的文件。以下是详细解释:

  • 核心概念

  1. HTTP 文件上传 当客户端通过表单上传文件时,通常会使用 enctype="multipart/form-data"。此时文件内容会以多部分(multipart)形式传输到服务器。
  2. Spring 的封装 Spring 通过 MultipartFile 接口抽象了这一过程,开发者无需直接解析复杂的 HTTP 请求体,而是通过简单的 API 操作文件。

alioss上传

  • 导入依赖

    <dependency>
        <groupId>com.aliyun.oss</groupId>
        <artifactId>aliyun-sdk-oss</artifactId>
        <version>${aliyun.sdk.oss}</version>
    </dependency>
    
  • aliosss配置

    yml

  • 具体实现 工具类

    package com.sky.utils;
    
    import com.aliyun.oss.ClientException;
    import com.aliyun.oss.OSS;
    import com.aliyun.oss.OSSClientBuilder;
    import com.aliyun.oss.OSSException;
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.extern.slf4j.Slf4j;
    import java.io.ByteArrayInputStream;
    
    @Data
    @AllArgsConstructor
    @Slf4j
    public class AliOssUtil {
    
        private String endpoint;
        private String accessKeyId;
        private String accessKeySecret;
        private String bucketName;
    
        /**
         * 文件上传
         *
         * @param bytes
         * @param objectName
         * @return
         */
        public String upload(byte[] bytes, String objectName) {
    
            // 创建OSSClient实例。
            OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
    
            try {
                // 创建PutObject请求。
                ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(bytes));
            } catch (OSSException oe) {
                System.out.println("Caught an OSSException, which means your request made it to OSS, "
                        + "but was rejected with an error response for some reason.");
                System.out.println("Error Message:" + oe.getErrorMessage());
                System.out.println("Error Code:" + oe.getErrorCode());
                System.out.println("Request ID:" + oe.getRequestId());
                System.out.println("Host ID:" + oe.getHostId());
            } catch (ClientException ce) {
                System.out.println("Caught an ClientException, which means the client encountered "
                        + "a serious internal problem while trying to communicate with OSS, "
                        + "such as not being able to access the network.");
                System.out.println("Error Message:" + ce.getMessage());
            } finally {
                if (ossClient != null) {
                    ossClient.shutdown();
                }
            }
    
            //文件访问路径规则 https://BucketName.Endpoint/ObjectName
            StringBuilder stringBuilder = new StringBuilder("https://");
            stringBuilder
                    .append(bucketName)
                    .append(".")
                    .append(endpoint)
                    .append("/")
                    .append(objectName);
    
            log.info("文件上传到:{}", stringBuilder.toString());
    
            return stringBuilder.toString();
        }
    }
    
  • 配置类 初始化aliossutil

    package com.sky.config;
    
    import com.sky.properties.AliOssProperties;
    import com.sky.utils.AliOssUtil;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    /**
     * 配置类,用于创建AliOssUtil对象的属性初始化
     */
    @Configuration
    @Slf4j
    public class OssConfiguration {
        // 项目启动,创建AliOssUtil对象,交给spring管理
        @Bean
        @ConditionalOnMissingBean
        public AliOssUtil aliOssUtil(AliOssProperties aliOssProperties){
            log.info("创建阿里云文件上传工具类对象:{}",aliOssProperties);
            return new AliOssUtil(aliOssProperties.getEndpoint(),
                    aliOssProperties.getAccessKeyId(),
                    aliOssProperties.getAccessKeySecret(),
                    aliOssProperties.getBucketName());
        }
    }
    
  • 调用

    package com.sky.controller.admin;
    
    import com.sky.constant.MessageConstant;
    import com.sky.result.Result;
    import com.sky.utils.AliOssUtil;
    import io.swagger.annotations.Api;
    import io.swagger.annotations.ApiOperation;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    import org.springframework.web.multipart.MultipartFile;
    
    import java.io.IOException;
    import java.util.UUID;
    
    /**
     * 通用接口
     */
    @RestController
    @RequestMapping("/admin/common")
    @Api(tags = "通用接口")
    @Slf4j
    public class CommonController {
    
        @Autowired
        private AliOssUtil aliOssUtil;
    
        /**
         * 文件上传
         * @param file
         * @return
         */
        @PostMapping("/upload")
        @ApiOperation("文件上传")
        public Result<String> upload(MultipartFile file){
            log.info("文件上传:{}",file);
            try {
                // 获取原文件名
                String originalFilename = file.getOriginalFilename();
                // 截取后缀 .png
                String extension = originalFilename.substring(originalFilename.lastIndexOf("."));
                // 构造新文件名
                String objectName = UUID.randomUUID().toString() + extension;
                String filePath = aliOssUtil.upload(file.getBytes(), objectName);
                return Result.success(filePath);
            } catch (IOException e) {
                log.error("上传失败:{}",e.getMessage());
            }
            return Result.error(MessageConstant.UPLOAD_FAILED);
        }
    }
    

截取文件字符串

// 截取后缀 .png
String extension = originalFilename.substring(originalFilename.lastIndexOf("."));

UUID随机命名,防止文件名字重复

// 构造新文件名
String objectName = UUID.randomUUID().toString();

操作多表开启事务包装原子性

  1. 启动类加

    @EnableTransactionManagement

    package com.sky;
    
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.transaction.annotation.EnableTransactionManagement;
    
    @SpringBootApplication
    @EnableTransactionManagement //开启注解方式的事务管理
    @Slf4j
    public class SkyApplication {
        public static void main(String[] args) {
            SpringApplication.run(SkyApplication.class, args);
            log.info("server started");
        }
    }
    
  2. 操作多表的serviceimpl方法加

    @Transactional 开启事务管理

    @Override
    // TODO 开启事务管理
    @Transactional
    public void saveWithFlavor(DishDTO dishDTO) {
        // 1 增加1条dish菜品数据
        Dish dish = new Dish();
        BeanUtils.copyProperties(dishDTO,dish);
        dishMapper.insert(dish);
    
        // 获取主键值给给口味表
        Long dishId = dish.getId();
    
        // 2 增加n条flavor口味数据
        List<DishFlavor> flavors = dishDTO.getFlavors();
        if (flavors!=null && flavors.size()>0){
            flavors.forEach(dishFlavor -> {
                dishFlavor.setDishId(dishId);
            });
            dishFlavorMapper.insertBatch(flavors);
        }
    
    }
    

实现流程

  1. 写controller

    加四个注解 创建service controller

    controller调用service

    @RestController
    @RequestMapping("/admin/dish")
    @Slf4j
    @Api(tags = "菜品相关接口")
    
  2. 写service接口

  3. 写serviceimpl实现类

    加@service接口,实现自动装配

    service调用mapper

  4. 写对应mapper接口

    记得@Mapper接口,实现mapper自动装配

    简单sql就用注解,复杂需要xml文件

    xml记得改命名空间namespace

    如果有公共字段记得@AutoFill(value = OperationType.INSERT)

获取自增id

  1. useGeneratedKeys 表示启用数据库的「自动生成主键」功能(例如 MySQL 的 AUTO_INCREMENT、PostgreSQL 的 SERIAL)。 设置为 true 后,MyBatis 会通过 JDBC 获取数据库生成的主键值。

  2. keyProperty ="id"把数据库生成的主键赋值给 实体类的id,此时实体类对象才有id

    <insert id="insert" parameterType="dish" useGeneratedKeys="true" keyProperty="id">
        insert into dish (name, category_id, price, image, description, status, create_time, update_time, create_user, update_user)
            VALUES (#{name},#{categoryId},#{price},#{image},#{description},#{status},#{createTime},#{updateTime},#{createUser},#{updateUser})
    </insert>
    
  3. 再通过getid获取

    Dish dish = new Dish();
    BeanUtils.copyProperties(dishDTO,dish);
    dishMapper.insert(dish);
    
    // 获取主键值给给口味表
    Long dishId = dish.getId();
    
    // 2 增加n条flavor口味数据
    List<DishFlavor> flavors = dishDTO.getFlavors();
    if (flavors!=null && flavors.size()>0){
        flavors.forEach(dishFlavor -> {
            dishFlavor.setDishId(dishId);
        });
        dishFlavorMapper.insertBatch(flavors);
    }
    
    

foreach

  • 跟增强for差不多

    List<DishFlavor> flavors = dishDTO.getFlavors();
    if (flavors!=null && flavors.size()>0){
        flavors.forEach(dishFlavor -> {
            dishFlavor.setDishId(dishId);
        });
        dishFlavorMapper.insertBatch(flavors);
    }
    

动态sql遍历

  • 传入集合

    void insertBatch(List<DishFlavor> flavors);
    
  • foreach动态sql

    collection 参数名

    item 每一项的遍历值

    separator 分割符号

    <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>
    

实体类Dish,前端传值DishDTO,后端返回DishVO

  • 实体类,与数据库完全一一对应

  • DTO,数据传输对象,封装前端传参,一般传后端需要的,用户输入的,后端可以自动生成的不传(时间,操作人)

    还有就是其他关联表的数据,类似菜品口味等

    封装数据库返回结果

  • VO,视图显示对象,后端返回给前端,传前端需要的,一般是多表关联的数据,连表

image-20250412112258486

sql连表查询

  • left join

    image-20250412133359005
  • right join

    image-20250412133410202
  • inner join

    image-20250412133344804
  • 实际sql

    <select id="pageQuery" resultType="com.sky.vo.DishVO">
        select d.*,c.name as categoryName from dish d left join category c on d.category_id = c.id
        <where>
            <if test="name!=null">and d.name like concat('%',#{name},'%')</if>
            <if test="categoryId!=null">and category_id = #{categoryId}</if>
            <if test="status!=null">and d.status = #{status}</if>
        </where>
        order by d.create_time desc
    </select>
    

RequestParam

@RequestParam 是 Spring MVC 框架中的一个注解,用于从 HTTP 请求中提取请求参数(如 URL 中的查询参数或表单提交的参数),并将其绑定到控制器(Controller)方法的参数上。它是处理客户端请求参数的常用工具,尤其在 RESTful API 或 Web 应用开发中非常实用。

主要用途

  1. 绑定请求参数到方法参数 将 HTTP 请求中的参数(如 ?name=John&age=20)映射到 Controller 方法的参数上。

  2. 处理可选参数 通过设置 required 属性,可以控制参数是否为必传。

  3. 设置默认值 当请求未提供参数时,可以通过 defaultValue 属性指定默认值。

  4. 处理多值参数 支持将多个同名参数(如复选框数据)绑定到集合或数组。

    可以把前端ids=1,2,3绑定到ids集合

    @DeleteMapping
    @ApiOperation("批量删除菜品")
    public Result delete(@RequestParam List<Long> ids){
        log.info("批量删除菜品对应id:{}",ids);
        dishService.deleteBatch(ids);
        return Result.success();
    }
    

全局异常处理

  • BaseException ex代表拦截处理抛出这类异常,包括BaseException 子类
package com.sky.handler;

import com.sky.constant.MessageConstant;
import com.sky.exception.BaseException;
import com.sky.result.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.sql.SQLIntegrityConstraintViolationException;
import java.util.Arrays;

/**
 * 全局异常处理器,处理项目中抛出的业务异常
 */
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    /**
     * 捕获业务异常
     * @param ex
     * @return
     */
    @ExceptionHandler
    public Result exceptionHandler(BaseException ex){
        log.error("异常信息:{}", ex.getMessage());
        return Result.error(ex.getMessage());
    }

    @ExceptionHandler
    public Result exceptionHandler(SQLIntegrityConstraintViolationException ex){
        // SQLIntegrityConstraintViolationException
        // Duplicate entry '666' for key 'idx_username'
        log.error("异常信息:{}", ex.getMessage());
        if (ex.getMessage().contains("Duplicate entry")){
            String[] msg = ex.getMessage().split(" ");
            log.info(Arrays.toString(msg));
            return Result.error(msg[2] + MessageConstant.ALREADY_EXISTS);
        }else return Result.error(MessageConstant.UNKNOWN_ERROR);
    }

}

foreach动态sql

delete from dish where id in(1,2,3...)

<delete id="deleteByIds">
    delete from dish where id in
    <foreach collection="ids" item="id" open="(" close=")" separator=",">
        #{id}
    </foreach>
</delete>

set动态sql

  • ,记得别忘了
<update id="update">
    update dish
    <set>
        <if test="name!=null">name = #{name},</if>
        <if test="categoryId!=null">category_id = #{categoryId},</if>
        <if test="price!=null">price = #{price},</if>
        <if test="image!=null">image = #{image},</if>
        <if test="description!=null">description = #{description},</if>
        <if test="status!=null">status = #{status},</if>
        <if test="updateTime!=null">update_time = #{updateTime},</if>
        <if test="updateUser!=null">update_user = #{updateUser},</if>
    </set>
    where id = #{id}
</update>

修改小技巧

  • 修改如果有不好改的,例如原来有想改成无delete,原来无想改成有insert

  • 可以先删除原来,再插入新数据实现修改

    public void update(DishDTO dishDTO) {
        Dish dish = new Dish();
        BeanUtils.copyProperties(dishDTO,dish);
        // 修改菜品
        dishMapper.update(dish);
    
        // 修改口味 = 先删除全部,再新增口味
        dishFlavorMapper.deleteByDishId(dishDTO.getId());
    
        List<DishFlavor> flavors = dishDTO.getFlavors();
        if (flavors!=null && flavors.size()>0){
            flavors.forEach(dishFlavor -> {
                dishFlavor.setDishId(dishDTO.getId());
            });
            dishFlavorMapper.insertBatch(flavors);
        }
    }
    

动态sql和普通sql易错点

  1. 插入修改数据过多,分清楚普通sql语句是数据库字段,下划线,update_time

    #{}引用的是实体类,驼峰,#{updateTime}

    不要直接复制,记得看一眼

    <insert id="insert" 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>
    
  2. 修改update对应动态set

    记得包裹,并且每一个记得最后加逗号,

    <update id="update">
        update setmeal
        <set>
            <if test="categoryId!=null">category_id = #{categoryId},</if>
            <if test="name!=null">name = #{name},</if>
            <if test="price!=null">price = #{price},</if>
            <if test="status!=null">status = #{status},</if>
            <if test="description!=null">description = #{description},</if>
            <if test="image!=null">image = #{image},</if>
            <if test="updateTime!=null">update_time = #{updateTime},</if>
            <if test="updateUser!=null">update_user = #{updateUser},</if>
        </set>
        where id = #{id}
    </update>
    
  3. 多条件查询动态where

    标签包裹,每一个前面记得加and

    <select id="pageQuery" resultType="com.sky.vo.SetmealVO">
        select s.*,c.name as categoryName from setmeal s left join category c
        on s.category_id = c.id
        <where>
            <if test="name!=null">and s.name like concat('%',#{name},'%')</if>
            <if test="status!=null">and s.status = #{status}</if>
            <if test="categoryId!=null">and s.category_id = #{categoryId}</if>
        </where>
        order by s.update_time desc
    </select>
    
  4. 批量插入多条数据

    foreach 用法

    insert into dish () values (),(),()

    <insert id="insertBatch">
        insert into setmeal_dish (setmeal_id, dish_id, name, price, copies)
        VALUES
        <foreach collection="setmealDishes" item="sd" separator=",">
            (#{sd.setmealId},#{sd.dishId},#{sd.name},#{sd.price},#{sd.copies})
        </foreach>
    </insert>
    
  5. 查询多条数据

    foreach 用法

    select * from dish where id in(1,2,3...)

    <select id="getSetmealIdsbyDishIds" resultType="java.lang.Long">
        select setmeal_id from setmeal_dish where dish_id in
        <foreach collection="ids" item="id" separator="," open="(" close=")">
            #{id}
        </foreach>
    </select>
    
  6. 模糊查询

    concat

    name like concat('%',#{name},'%')
    

mysql语法crud

image-20250413170213146

redis

  • 非关系型数据库,存储方式k-v键值对
  • 存数据在内存之中,热点读写更快
  • 在项目中和mysql互补,mysql存大多数数据

cmd启动redis

  • 文件夹cmd
  • image-20250415155048968
  • ctrl + c 关闭服务
  • 开启服务后,客户端可以连接,有密码要加密码,exit退出image-20250415155211393

cmd小操作

  • 关闭服务 ctrl+c
  • 退出exit
  • 再调用上一次输入命令 移动键上和右
  • 清屏cls
  • tab 根据文件夹内文件名自动补全

redis数据类型

  • 数据类型指的是value,key只有字符串这一种

image-20250415160601094

字符串string操作

  • set k v

  • get k

  • setex k 时间(秒)v

  • setnx k v

    不存在这个k才执行

image-20250415163937969

哈希表map操作

  • map集合,key对应多个field-value,一般存对象,field value都是string

  • hset k f v

  • hget k f

  • hdel k f (删光f,系统会自动删除这个k)

  • hkeys k 获取所有field

  • hvals k 获取所有value

    image-20250415165202914

列表list操作

  • 跟list集合,链表一样,有序有下标可重复,双向链表
  • lpush k v 从k的头部插入v
  • lrange k start stop 返回k的[sta,stop],stop=-1表示到最后
  • rpop k 删除最后一个v 返回这个v
  • llen k 返回k的长度

image-20250415172014199

集合set操作

  • 跟set集合一样,无序无下标不可重复
  • sadd k v...
  • smembers k 返回全部v
  • scard k 返回v的数量
  • sinter k1 k2 返回k1,2交集
  • sunion k1 k2 返回 k1,k2并集
  • srem k1 v.. 删除这个v

image-20250415181518706

有序集合zset操作

  • 集合set加一个double分数用于排序,本质还是set,元素不能重复

  • zadd k score v,score代表分数

  • zrange k 0 -1 withscores,从分数小到大查询[0,-1]元素加上分数

  • zincrby k score v ,给v的分数加5

  • zrem k v,移除v

    image-20250415215508627image-20250415215517474

key操作

  • 直接操作key
  • keys * 返回所有的k keys set* 返回所有开头是set的k
  • exists k 当前k是否存在,存在就1,不存在0
  • type k 当前k类型
  • del k1 k2 删除k1k2

image-20250415220445703

java操作redis

  1. 导入坐标

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    
  2. 编写连接配置

    redis:
      host: localhost
      port: 6379
      password: 123456
      database: 10
    
  3. 创建redis配置类,用于创建redistemplate对象,交给spring

    /**
     * redis配置类
     * 创建RedisTemplate对象
     */
    @Configuration
    @Slf4j
    public class RedisConfiguration {
    
        // RedisConnectionFactory由坐标初始化类用配置的数据创建,由spring管理
        @Bean
        public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){
            log.info("redis连接工厂初始化:{}",redisConnectionFactory);
            RedisTemplate redisTemplate = new RedisTemplate();
            // 设置redis 的连接工厂对象
            redisTemplate.setConnectionFactory(redisConnectionFactory);
            // 设置redis key的序列化器
            redisTemplate.setKeySerializer(new StringRedisSerializer());
            return redisTemplate;
        }
    }
    
  4. 测试

    @SpringBootTest
    public class SpringDataRedisTest {
    
        @Autowired
        private RedisTemplate redisTemplate;
    
        @Test
        public void testRedisTemplate(){
            System.out.println(redisTemplate);
            // 从上到下 string hash list set zset
            ValueOperations valueOperations = redisTemplate.opsForValue();
            HashOperations hashOperations = redisTemplate.opsForHash();
            ListOperations listOperations = redisTemplate.opsForList();
            SetOperations setOperations = redisTemplate.opsForSet();
            ZSetOperations zSetOperations = redisTemplate.opsForZSet();
        }
    }
    

string类型

@Test
public void testValueOperations(){
    ValueOperations valueOperations = redisTemplate.opsForValue();
    valueOperations.set("name","小明");
    
    System.out.println(valueOperations.get("name"));
    System.out.println(valueOperations.get("name").toString());
    
    valueOperations.set("code","1234",3, TimeUnit.MINUTES);
    
    valueOperations.setIfAbsent("city","北京");
    valueOperations.setIfAbsent("city","上海");
}

hash类型

@Test
public void testHashOperations(){
    HashOperations hashOperations = redisTemplate.opsForHash();
    // hset hget hkeys hvals hdel
    hashOperations.put("100","name","tom");
    hashOperations.put("100","age","18");

    String name = (String) hashOperations.get("100", "name");
    System.out.println(name);

    Set keys = hashOperations.keys("100");
    System.out.println(keys);

    List values = hashOperations.values("100");
    System.out.println(values);

    hashOperations.delete("100","age");

}

list类型

@Test
public void testListOperations(){
    ListOperations listOperations = redisTemplate.opsForList();
    // lpush llen rpop lrange
    listOperations.leftPushAll("list1","a","b","c");
    listOperations.leftPush("list1","d");

    List list1 = listOperations.range("list1", 0, -1);
    System.out.println(list1);

    listOperations.rightPop("list1");

    Long list11 = listOperations.size("list1");
    System.out.println(list11);

}

set类型

@Test
public void testSetOperations() {
    SetOperations setOperations = redisTemplate.opsForSet();
    // sadd smembers sinter sunion srem scard
    setOperations.add("set1","a","b","c","d");
    setOperations.add("set2","a","b","x","y");

    Long set1 = setOperations.size("set1");
    System.out.println(set1);

    Set set11 = setOperations.members("set1");
    System.out.println(set11);

    Set intersect = setOperations.intersect("set1", "set2");
    System.out.println(intersect);

    Set union = setOperations.union("set1", "set2");
    System.out.println(union);

    setOperations.remove("set1","a","b");

}

zset类型

@Test
public void testZSetOperations(){
    ZSetOperations zSetOperations = redisTemplate.opsForZSet();
    // zadd zrange zrem zincrby
    zSetOperations.add("zset","a",10);
    zSetOperations.add("zset","b",13);
    zSetOperations.add("zset","c",15);

    Set zset = zSetOperations.range("zset", 0, -1);
    System.out.println(zset);

    zSetOperations.incrementScore("zset","a",6);

    zSetOperations.remove("zset","a","b");
}

通用key操作

@Test
public void testKeyCommon(){
    // keys exists type del
    Set keys = redisTemplate.keys("*");
    System.out.println(keys);

    Boolean name = redisTemplate.hasKey("name");
    Boolean name1 = redisTemplate.hasKey("name1");

    for (Object key : keys) {
        System.out.println(redisTemplate.type(key));
    }

    redisTemplate.delete("set1");
}

序列化和反序列化

  • 序列化和反序列化就像生活中的打包和拆包过程:

    1. 序列化(打包)

      假设你有一套乐高玩具,想寄给朋友。你需要把零件拆开,按顺序摆进盒子里(比如说明书放最上面),这就是序列化——把复杂的数据(比如对象、游戏存档)变成一串可以存储或传输的格式(如JSON、二进制)。

    2. 反序列化(拆包)

      朋友收到盒子后,按照说明书把零件重新拼成乐高模型,这就是反序列化——把存储的格式(如文件、网络传输的数据)还原成原来的数据。

      举个栗子🌰

      • 保存游戏进度:把角色位置、装备等序列化成存档文件
      • 加载游戏:读取存档文件并反序列化回内存中的游戏数据
      • 微信发消息:聊天内容先序列化成数据包,对方收到后再反序列化成文字
      • 简单说就是: 序列化 = 把东西变成能保存/传输的格式 反序列化 = 把保存的格式还原成原来的东西

枚举类enum

  • Java 枚举类(Enum)是一种特殊的类,用于表示一组固定的常量。它通过 enum 关键字定义,提供了一种更安全、更直观的方式来表示有限的、预定义的值集合(如星期、状态、颜色等)。

    枚举类的核心特点

  1. 固定常量集合 枚举类的值在定义时确定,不可在运行时修改。
  2. 类型安全 编译时检查,避免使用无效值(如 if(status == 1) 中的魔法数字)。
  3. 可添加方法和字段 枚举类可以有构造方法、普通方法、字段,甚至实现接口。
  4. 单例特性 每个枚举常量都是枚举类的唯一实例,天然线程安全。
  5. 内置方法values(), valueOf(), name(), ordinal() 等。

不同包的相同名类给spring bean托管会报错

  • 因为bean托管不能重名,默认restcontroller不指定就是自动以小写命名bean

  • 解决方法

    restcontroller("x")

    restcontroller("y")

    自定义指定不同的bean名字就行

swageer多个包分类

  • 复制两份docket,分别扫两个包,可以实现分开查看

    @Bean
    public Docket docketAdmin() {
        log.info("swagger文档");
        ApiInfo apiInfo = new ApiInfoBuilder()
                .title("苍穹外卖项目接口文档")
                .version("2.0")
                .description("苍穹外卖项目接口文档")
                .build();
        Docket docket = new Docket(DocumentationType.SWAGGER_2)
                .groupName("管理端接口")
                .apiInfo(apiInfo)
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.sky.controller.admin"))
                .paths(PathSelectors.any())
                .build();
        return docket;
    }
    
    @Bean
    public Docket docketUser() {
        log.info("swagger文档");
        ApiInfo apiInfo = new ApiInfoBuilder()
                .title("苍穹外卖项目接口文档")
                .version("2.0")
                .description("苍穹外卖项目接口文档")
                .build();
        Docket docket = new Docket(DocumentationType.SWAGGER_2)
                .groupName("用户端接口")
                .apiInfo(apiInfo)
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.sky.controller.user"))
                .paths(PathSelectors.any())
                .build();
        return docket;
    }
    

httpservlet和httpclient

  • HttpServlet:服务端处理 HTTP 请求的核心组件。
  • HttpClient:客户端发送 HTTP 请求的工具库。
  • 联系:两者基于 HTTP 协议协作,但方向相反,共同支持完整的 HTTP 通信场景。
特性 HttpServlet HttpClient
角色 服务端组件(处理请求) 客户端组件(发送请求)
所属技术栈 Java Servlet API(如 Tomcat、Jetty) 第三方库(如 Apache HttpClient、OkHttp)
主要用途 接收并响应 HTTP 请求(如处理浏览器请求) 主动向其他服务发送 HTTP 请求
典型场景 实现 Web 应用的接口(如处理表单提交) 调用外部 API(如访问 RESTful 服务)

httpclient测试

  • 导入maven
@SpringBootTest
public class HttpClientTest {

    @Test
    public void testHttpGet() throws IOException {
        // 创建httpClient对象
        CloseableHttpClient httpClient = HttpClients.createDefault();

        // 创建请求对象
        HttpGet httpGet = new HttpGet("http://localhost:8080/user/shop/status");

        // 执行请求,获取结果
        CloseableHttpResponse response = httpClient.execute(httpGet);

        // 返回状态码
        int statusCode = response.getStatusLine().getStatusCode();
        System.out.println("执行请求的状态码:"+statusCode);

        // 获得返回数据,解析entity
        HttpEntity entity = response.getEntity();
        String s = EntityUtils.toString(entity);
        System.out.println("返回数据为:"+s);

        // 关闭资源
        response.close();
        httpClient.close();
    }

    @Test
    public void testHttpPost() throws IOException {
        // 创建httpClient
        CloseableHttpClient httpClient = HttpClients.createDefault();

        // 创建post请求
        HttpPost httpPost = new HttpPost("http://localhost:8080/admin/employee/login");

        // 创建需要的请求参数
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("username","admin");
        jsonObject.put("password","123456");

        StringEntity entity = new StringEntity(jsonObject.toString());
        entity.setContentEncoding("utf-8");
        entity.setContentType("application/json");
        httpPost.setEntity(entity);

        // 处理请求返回数据
        CloseableHttpResponse response = httpClient.execute(httpPost);
        int statusCode = response.getStatusLine().getStatusCode();
        System.out.println("执行状态码:"+statusCode);

        HttpEntity entity1 = response.getEntity();
        String s = EntityUtils.toString(entity1);
        System.out.println("执行返回结果:"+s);

        // 关闭资源
        response.close();
        httpClient.close();

    }
}

httpclient工具类

  • get 由于地址栏传参,需要封装?k=v,所有url封装成uri

  • post 通过UrlEncodedFormEntity 自带

    doPost方法通过UrlEncodedFormEntity将参数编码为application/x-www-form-urlencoded格式的请求体。

    doPost4Json方法直接将参数转为JSON格式的请求体。

/**
 * Http工具类
 */
public class HttpClientUtil {

    static final  int TIMEOUT_MSEC = 5 * 1000;

    /**
     * 发送GET方式请求
     * @param url
     * @param paramMap
     * @return
     */
    public static String doGet(String url,Map<String,String> paramMap){
        // 创建Httpclient对象
        CloseableHttpClient httpClient = HttpClients.createDefault();

        String result = "";
        CloseableHttpResponse response = null;

        try{
            URIBuilder builder = new URIBuilder(url);
            if(paramMap != null){
                for (String key : paramMap.keySet()) {
                    builder.addParameter(key,paramMap.get(key));
                }
            }
            URI uri = builder.build();

            //创建GET请求
            HttpGet httpGet = new HttpGet(uri);

            //发送请求
            response = httpClient.execute(httpGet);

            //判断响应状态
            if(response.getStatusLine().getStatusCode() == 200){
                result = EntityUtils.toString(response.getEntity(),"UTF-8");
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            try {
                response.close();
                httpClient.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        return result;
    }

    /**
     * 发送POST方式请求
     * @param url
     * @param paramMap
     * @return
     * @throws IOException
     */
    public static String doPost(String url, Map<String, String> paramMap) throws IOException {
        // 创建Httpclient对象
        CloseableHttpClient httpClient = HttpClients.createDefault();
        CloseableHttpResponse response = null;
        String resultString = "";

        try {
            // 创建Http Post请求
            HttpPost httpPost = new HttpPost(url);

            // 创建参数列表
            if (paramMap != null) {
                List<NameValuePair> paramList = new ArrayList();
                for (Map.Entry<String, String> param : paramMap.entrySet()) {
                    paramList.add(new BasicNameValuePair(param.getKey(), param.getValue()));
                }
                // 模拟表单
                UrlEncodedFormEntity entity = new UrlEncodedFormEntity(paramList);
                httpPost.setEntity(entity);
            }

            httpPost.setConfig(builderRequestConfig());

            // 执行http请求
            response = httpClient.execute(httpPost);

            resultString = EntityUtils.toString(response.getEntity(), "UTF-8");
        } catch (Exception e) {
            throw e;
        } finally {
            try {
                response.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        return resultString;
    }

    /**
     * 发送POST方式请求
     * @param url
     * @param paramMap
     * @return
     * @throws IOException
     */
    public static String doPost4Json(String url, Map<String, String> paramMap) throws IOException {
        // 创建Httpclient对象
        CloseableHttpClient httpClient = HttpClients.createDefault();
        CloseableHttpResponse response = null;
        String resultString = "";

        try {
            // 创建Http Post请求
            HttpPost httpPost = new HttpPost(url);

            if (paramMap != null) {
                //构造json格式数据
                JSONObject jsonObject = new JSONObject();
                for (Map.Entry<String, String> param : paramMap.entrySet()) {
                    jsonObject.put(param.getKey(),param.getValue());
                }
                StringEntity entity = new StringEntity(jsonObject.toString(),"utf-8");
                //设置请求编码
                entity.setContentEncoding("utf-8");
                //设置数据类型
                entity.setContentType("application/json");
                httpPost.setEntity(entity);
            }

            httpPost.setConfig(builderRequestConfig());

            // 执行http请求
            response = httpClient.execute(httpPost);

            resultString = EntityUtils.toString(response.getEntity(), "UTF-8");
        } catch (Exception e) {
            throw e;
        } finally {
            try {
                response.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        return resultString;
    }
    private static RequestConfig builderRequestConfig() {
        return RequestConfig.custom()
                .setConnectTimeout(TIMEOUT_MSEC)
                .setConnectionRequestTimeout(TIMEOUT_MSEC)
                .setSocketTimeout(TIMEOUT_MSEC).build();
    }

}

总结

请求方法 参数位置 是否需要URI对象 原因
GET URL查询字符串 需要构建完整的URL并处理编码
POST 请求体(Body) 参数通过Entity设置,URL无需修改

这种设计既符合HTTP协议规范,也确保了代码的安全性和可维护性

@Bean @Component @Autowired

  • 是的,无论是通过 @Component(及其派生注解如 @Service, @Controller 等)还是 @Bean 注册的 Bean,都可以通过 @Autowired 注入。它们的核心区别在于 Bean 的注册方式适用场景。以下是详细对比:
特性 @Component @Bean
注册方式 类级别注解,自动扫描注册 方法级别注解,手动在配置类中定义
适用场景 自己编写的类(如 Service、Controller 等) 需要复杂初始化的类、第三方库的类(如数据库连接池)
控制权 Spring 自动实例化 开发者手动控制实例化逻辑
依赖注入方式 自动注入(通过构造函数或字段) 通过方法参数显式注入依赖
Bean 名称 默认类名首字母小写(可自定义) 默认方法名(可指定 name 属性)

1. 使用 @Component 注册 Bean

假设你有一个自己编写的服务类:

Java@Service  // @Component 的派生注解
public class UserService {
    public void saveUser() {
        // 业务逻辑
    }
}

使用方式:Spring 会自动扫描 UserService 并注册为 Bean,其他类可以直接注入:

Java@RestController
public class UserController {
    @Autowired  // 注入 UserService
    private UserService userService;
}

2. 使用 @Bean 注册 Bean

假设你需要集成第三方库的类(例如数据库连接池),或者需要复杂初始化逻辑:

Java@Configuration
public class AppConfig {

    @Bean  // 手动定义 Bean
    public DataSource dataSource() {
        // 创建并配置连接池
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/mydb");
        dataSource.setUsername("root");
        dataSource.setPassword("123456");
        return dataSource;
    }
}

使用方式:其他类可以直接注入 DataSource

Java@Service
public class OrderService {
    @Autowired
    private DataSource dataSource;  // 注入第三方库的 Bean
}

常量修饰

public static final String url = ""

FastJSON相关

String json = HttpClientUtil.doGet(url, map);

// 解析json字符串成json对象
JSONObject jsonObject = JSON.parseObject(json);
String openid = jsonObject.getString("openid");
return openid;

定义拦截器

  • 要加一个用户端jwt拦截校验器

  • 首先,JwtTokenUserInterceptor实现HandlerInterceptor

    /**
     * jwt令牌校验的拦截器
     */
    @Component
    @Slf4j
    public class JwtTokenUserInterceptor implements HandlerInterceptor {
    
        @Autowired
        private JwtProperties jwtProperties;
    
        /**
         * 校验jwt在所有controller请求前
         *
         * @param request
         * @param response
         * @param handler
         * @return
         * @throws Exception
         */
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            //判断当前拦截到的是Controller的方法还是其他资源
            if (!(handler instanceof HandlerMethod)) {
                //当前拦截到的不是动态方法,直接放行
                return true;
            }
    
            //1、从请求头中获取令牌
            String token = request.getHeader(jwtProperties.getUserTokenName());
    
            //2、校验令牌
            try {
                log.info("jwt校验:{}", token);
                Claims claims = JwtUtil.parseJWT(jwtProperties.getUserSecretKey(), token);
                Long userId = Long.valueOf(claims.get(JwtClaimsConstant.USER_ID).toString());
                log.info("当前用户id:{}", userId);
                // 在线程空间存入id
                BaseContext.setCurrentId(userId);
                //3、通过,放行
                return true;
            } catch (Exception ex) {
                //4、不通过,响应401状态码
                response.setStatus(401);
                return false;
            }
        }
    }
    
  • 然后,webmvcconfiguration

    在webmvc拦截器列表内添加自己定义拦截器,设置对应的拦截路径

    /**
     * 配置类,注册web层相关组件
     */
    @Configuration
    @Slf4j
    public class WebMvcConfiguration extends WebMvcConfigurationSupport {
    
        @Autowired
        private JwtTokenAdminInterceptor jwtTokenAdminInterceptor;
    
        @Autowired
        private JwtTokenUserInterceptor jwtTokenUserInterceptor;
    
        /**
         * 注册自定义拦截器
         *
         * @param registry
         */
        protected void addInterceptors(InterceptorRegistry registry) {
            log.info("开始注册自定义拦截器...");
            registry.addInterceptor(jwtTokenAdminInterceptor)
                    .addPathPatterns("/admin/**")
                    .excludePathPatterns("/admin/employee/login");
    
            registry.addInterceptor(jwtTokenUserInterceptor)
                    .addPathPatterns("/user/**")
                    .excludePathPatterns("/user/user/login")
                    .excludePathPatterns("/user/shop/status");
        }
    

操作哪个数据库就用哪个controller service mapper

  • 查询某个分类下的菜品,就操作菜品dish 对应 controller service

缓存加快速度

  • 用户多点餐,每次查询都要调用sql查询数据库,会比较慢,使用把查询的dish数据放入redis存储,加快查询速度

  • 修改user查询

    @RestController("userDishController")
    @RequestMapping("/user/dish")
    @Slf4j
    @Api(tags = "C端-菜品浏览接口")
    public class DishController {
        @Autowired
        private DishService dishService;
    
        @Autowired
        private RedisTemplate redisTemplate;
    
        /**
         * 根据分类id查询菜品
         *
         * @param categoryId
         * @return
         */
        @GetMapping("/list")
        @ApiOperation("根据分类id查询菜品")
        public Result<List<DishVO>> list(Long categoryId) {
            Dish dish = new Dish();
            dish.setCategoryId(categoryId);
            dish.setStatus(StatusConstant.ENABLE);//查询起售中的菜品
    
            // 构造redis的key dish_categoryId
            String key = "dish_" + categoryId;
    
            // 查询先看redis有没有这样的key,(string反序列化list)
            List<DishVO> list = (List<DishVO>) redisTemplate.opsForValue().get(key);
    
            // 如果有,直接返回
            if (list!=null && list.size()>0){
                log.info("redis缓存存在,直接取出:{}",list);
                return Result.success(list);
            }
    
            // 没有就查数据库,然后注册到redis中,(list序列化成string)
            list = dishService.listWithFlavor(dish);
    
            redisTemplate.opsForValue().set(key,list);
    
            return Result.success(list);
        }
    
    }
    
  • 如果管理端改变了菜品数据,需要清理对应缓存,保证数据一致性

    增删改都要

    /**
     * 清理redis缓存
     */
    private void cleanCache(String pattern){
        Set keys = redisTemplate.keys(pattern);
        log.info("清理redis缓存: {}",keys);
        redisTemplate.delete(keys);
    }
    
    @PostMapping("/status/{status}")
    @ApiOperation("修改菜品状态")
    public Result startOrStop(@PathVariable Integer status,Long id){
        log.info("修改菜品状态 {},{}",status,id);
        dishService.startOrStop(status,id);
        // 清理缓存
        cleanCache("dish_*");
        return Result.success();
    }
    

注解获取自增id,赋值给实体类

@Insert("insert into user(name,age) values (#{name},#{age})")
@Options(useGeneratedKeys = true,keyProperty = "id")
void insert(User user);

redis树形存储

  • k如果中间有: redis会把这个当成树形结构,本质k还是a:b:c:d和userCache::1

    image-20250418111655033

Spring-cache注解实现缓存

  • 导入哪个底层用哪个,类似导入redis坐标就是redis

  • <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    
  • 导入Springcache坐标

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-cache</artifactId>
    </dependency>
    
  • 常用方法

    @EnableCaching

    加启动类上,开启使缓存注解生效

    @CachePut

    执行方法后,把方法执行结果存入redis

    @Cacheable

    执行方法前,看redis有没有这个kv,有就直接返回,不执行方法,没有就执行方法,存入redis

    @CacheEvict

    执行方法后,删除这个kv

    @RestController
    @RequestMapping("/user")
    @Slf4j
    public class UserController {
    
        @Autowired
        private UserMapper userMapper;
    
        @PostMapping
        // 把返回结果作为v存入redis,
        // 执行完方法后执行存redis
        @CachePut(cacheNames = "userCache",key = "#user.id")// 构造key cacheNames::key
        public User save(@RequestBody User user){
            userMapper.insert(user);
            return user;
        }
    
        @DeleteMapping
        // 方法执行完后删除redis
        @CacheEvict(cacheNames = "userCache",key = "#id")
        public void deleteById(Long id){
            userMapper.deleteById(id);
        }
    
    
       @DeleteMapping("/delAll")
        // 方法执行完删除所有userCache下的k
        @CacheEvict(cacheNames = "userCache",allEntries = true)
        public void deleteAll(){
            userMapper.deleteAll();
        }
    
        @GetMapping
        // 本质是创建了一个代理
        // 执行方法前先查redis有没有key,有直接返回,不走方法了;
        // 没有就通过反射执行方法,查数据库,然后把key和返回数据v存入redis
        // 执行方法前执行操作redis
        @Cacheable(cacheNames = "userCache",key = "#id")// 构造key
        public User getById(Long id){
            User user = userMapper.getById(id);
            return user;
        }
    
    }
    

套餐 分类 菜品

  • 分类有套餐分类和菜品分类
  • 套餐相当于菜品集合,可以理解为也是一种菜品

jwt令牌校验取出操作的用户(员工)id

  • 登录注册时,自增id放入对象里

    <insert id="insert" useGeneratedKeys="true" keyProperty="id">
    
  • 生成jwt令牌后,放入id,加密成token

    // 生成jwt令牌
    Map<String, Object> claims = new HashMap<>();
    claims.put(JwtClaimsConstant.USER_ID,user.getId());
    String token = JwtUtil.createJWT(jwtProperties.getUserSecretKey(), jwtProperties.getUserTtl(), claims);
    
  • jwt拦截器从请求头获取令牌,解密出userid,放入线程空间

    BaseContext.setCurrentId(userId);

    //1、从请求头中获取令牌
    String token = request.getHeader(jwtProperties.getUserTokenName());
    
    //2、校验令牌
    try {
        log.info("jwt校验:{}", token);
        Claims claims = JwtUtil.parseJWT(jwtProperties.getUserSecretKey(), token);
        Long userId = Long.valueOf(claims.get(JwtClaimsConstant.USER_ID).toString());
        log.info("当前用户id:{}", userId);
        // 在线程空间存入id
        BaseContext.setCurrentId(userId);
        //3、通过,放行
        return true;
    } catch (Exception ex) {
        //4、不通过,响应401状态码
        response.setStatus(401);
        return false;
    }
    
  • 所以每次操作会携带令牌,验证令牌后,线程空间就有操作者的id,可以取出

    BaseContext.getCurrentId()

    @Override
    public List<ShoppingCart> list() {
        ShoppingCart shoppingCart = ShoppingCart
                .builder()
                .userId(BaseContext.getCurrentId())
                .build();
        return shoppingCartMapper.list(shoppingCart);
    }
    

冗余字段设置

  • 方便业务操作,避免需要多次查询数据库,为了后续业务更简单

  • 这里地址id可以查到地址信息和手机号,但为了方便展示,直接设计成冗余字段

    image-20250419104209658

BeanUtils.copyProperties使用

  • 这里的

    BeanUtils.copyProperties(addressBook,orders);

    BeanUtils.copyProperties(cart,orderDetail);

    虽然会复制id,但可以正常使用,因为插入不插入id,id为数据库自增

    修改会传入id,所以不能直接复制,要set

    @Transactional
        public OrderSubmitVO submit(OrdersSubmitDTO ordersSubmitDTO) {
            // 处理业务异常
            // 校验提交的地址薄是否为空,空就抛异常
            AddressBook addressBook = addressBookMapper.getById(ordersSubmitDTO.getAddressBookId());
            if (addressBook==null){
                throw new AddressBookBusinessException(MessageConstant.ADDRESS_BOOK_IS_NULL);
            }
    
            // 购物车是否为空
            Long userId = BaseContext.getCurrentId();
            ShoppingCart shoppingCart = ShoppingCart
                    .builder()
                    .userId(userId)
                    .build();
            List<ShoppingCart> shoppingCartList = shoppingCartMapper.list(shoppingCart);
            if (shoppingCartList==null || shoppingCartList.size()==0){
                throw new ShoppingCartBusinessException(MessageConstant.SHOPPING_CART_IS_NULL);
            }
    
            // 都不空插入一条订单数据
            Orders orders = new Orders();
            BeanUtils.copyProperties(ordersSubmitDTO,orders);
            orders.setNumber(String.valueOf(System.currentTimeMillis()));// 以时间戳为订单号
            orders.setStatus(Orders.PENDING_PAYMENT);
            orders.setUserId(userId);
            orders.setOrderTime(LocalDateTime.now());
            orders.setPayStatus(Orders.UN_PAID);
            // 这里会复制id,但也不影响,因为插入是自增
            BeanUtils.copyProperties(addressBook,orders);
    //        orders.setUserName(addressBook.getConsignee());
    //        orders.setPhone(addressBook.getPhone());
    //        orders.setAddress(addressBook.getDetail());
    //        orders.setConsignee(addressBook.getConsignee());
    
            orderMapper.insert(orders);
    
            // 插入多条订单详情数据,订单详情就是订单对应的菜品套餐详情
            List<OrderDetail> orderDetails = new ArrayList<>();
            for (ShoppingCart cart : shoppingCartList) {
                OrderDetail orderDetail = OrderDetail
                        .builder()
                        .orderId(orders.getId())
                        .build();
                // 这里会复制id,但没有影响,因为插入不插入id,id自增不影响
                BeanUtils.copyProperties(cart,orderDetail);
                orderDetails.add(orderDetail);
            }
            orderDetailMapper.insertBatch(orderDetails);
    
            // 清空购物车
            shoppingCartMapper.clean(userId);
    
            // 构造返回类型
            OrderSubmitVO orderSubmitVO = OrderSubmitVO
                    .builder()
                    .id(orders.getId())
                    .orderNumber(orders.getNumber())
                    .orderAmount(orders.getAmount())
                    .orderTime(orders.getOrderTime())
                    .build();
    
            return orderSubmitVO;
        }
    

微信支付

开发指引_JSAPI支付|微信支付商户文档中心

image-20250419173520569

一对多放入page

  • controller

    /**
     * 分页查询历史订单
     * @param ordersPageQueryDTO
     * @return
     */
    @GetMapping("/historyOrders")
    @ApiOperation("分页查询历史订单")
    public Result<PageResult> page(OrdersPageQueryDTO ordersPageQueryDTO){
        log.info("分页查询历史订单: {}",ordersPageQueryDTO);
        PageResult pageResult = orderService.page(ordersPageQueryDTO);
        return Result.success(pageResult);
    }
    
  • service

    这里是一对多

    如果连表查,数据会有问题,所以要先查订单,再通过订单查详情,最后放一起返回

    /**
     * 分页查询历史订单
     * @param ordersPageQueryDTO
     * @return
     */
    @Override
    public PageResult page(OrdersPageQueryDTO ordersPageQueryDTO) {
        ordersPageQueryDTO.setUserId(BaseContext.getCurrentId());
        PageHelper.startPage(ordersPageQueryDTO.getPage(),ordersPageQueryDTO.getPageSize());
        // 分页查询所有该用户不同状态的订单
        Page<Orders> page = orderMapper.page(ordersPageQueryDTO);
    
        // 遍历所有订单取出订单详情
        List<OrderVO> voList = new ArrayList<>();
    
        if (page!=null && page.size()>0){
            for (Orders order : page) {
                OrderVO orderVO = new OrderVO();
                List<OrderDetail> orderDetails = orderDetailMapper.getByOrderId(order.getId());
                BeanUtils.copyProperties(order,orderVO);
                orderVO.setOrderDetailList(orderDetails);
    
                // 放入OrdersVO集合,返回集合结果(一对多)
                voList.add(orderVO);
            }
        }
    
        return new PageResult(page.getTotal(),voList);
    }
    

    如果是多对一,类似查菜品和对应分类,或者一对一

    可以直接连表

    Page<DishVO> page = dishMapper.pageQuery(dishPageQueryDTO);
    
  • mapper

    <select id="page" resultType="com.sky.entity.Orders">
        select * from orders
        <where>
            <if test="status!=null">and status=#{status}</if>
            <if test="userId!=null">and user_id=#{userId}</if>
        </where>
    </select>
    

Mybatis大于小于

image-20250420170823750

<if test="beginTime != null">
    and order_time &gt;= #{beginTime}
</if>
<if test="endTime != null">
    and order_time &lt;= #{endTime}
</if>

调用API流程

  1. 配置令牌,和发请求需要的参数

    看官方文档,获取请求方式和url以及需要的参数和返回

    地理编码 | 百度地图API SDK

    轻量级路线规划 | 百度地图API SDK

  2. 请求工具类,发送请求获取结果

  3. 解析结果

    /**
     * 检查客户的收货地址是否超出配送范围
     * @param address
     */
    private void checkOutOfRange(String address){
        Map<String,String> map = new HashMap<>();
        map.put("ak",ak);
        map.put("address",shopAddress);
        map.put("output","json");
    
        // 获取店铺经纬坐标
        String shopCoordinate = HttpClientUtil.doGet("https://api.map.baidu.com/geocoding/v3", map);
        JSONObject jsonObject = JSONObject.parseObject(shopCoordinate);
    
        if (!jsonObject.getString("status").equals("0")){
            throw new OrderBusinessException("地址解析失败");
        }
    
        JSONObject location = jsonObject.getJSONObject("result").getJSONObject("location");
        String lng = location.getString("lng");
        String lat = location.getString("lat");
        String shopLatLng = lat + "," +lng;
    
        // 获取订单用户的经纬坐标
        map.put("address",address);
        String userCoordinate = HttpClientUtil.doGet("https://api.map.baidu.com/geocoding/v3", map);
        jsonObject = JSONObject.parseObject(userCoordinate);
    
        if (!jsonObject.getString("status").equals("0")){
            throw new OrderBusinessException("地址解析失败");
        }
    
        location = jsonObject.getJSONObject("result").getJSONObject("location");
        lng = location.getString("lng");
        lat = location.getString("lat");
        String userLatLng = lat + "," +lng;
    
        // 算路线
        map.put("origin",shopLatLng);
        map.put("destination",userLatLng);
        map.put("steps_info","0");
    
        String json = HttpClientUtil.doGet("https://api.map.baidu.com/directionlite/v1/driving", map);
        jsonObject = JSONObject.parseObject(json);
        if (!jsonObject.getString("status").equals("0")){
            throw new OrderBusinessException("路线解析失败");
        }
    
        JSONObject result = jsonObject.getJSONObject("result");
        JSONArray routes = result.getJSONArray("routes");
        Integer distance = Integer.valueOf(((JSONObject)routes.get(0)).getString("distance"));
        if (distance>5000){
            throw new OrderBusinessException("超出配送范围");
        }
    
    }
    

Cron表达式

  1. 分6(7)块域,秒分时日月周(年可选)

  2. 周和日互斥,一般有一个,另一个是?

  3. 直接搜在线cron生成器

    Cron - 在线Cron表达式生成器

    image-20250421101744498

    0 1-2 0/1 1W 7 L *

SpirngTask

  • 定时任务

  • 类似闹钟

  • 使用流程

    1. 导入坐标

      在spring框架自带,context里面

      image-20250421111903609

    2. 主启动类注解开启开关

      @EnableScheduling // 开启任务调度
      
    3. 编写Task类,加@Component注解

      方法加@scheduled注解,配置定时cron表达式,编写需要按条件定时执行的业务代码

      /**
       * 定时处理订单
       */
      @Component
      @Slf4j
      public class OrderTask {
      
          @Autowired
          private OrderMapper orderMapper;
      
          /**
           * 处理超时订单
           * 每分钟检查一次
           */
          @Scheduled(cron = "0 * * * * ?")
          public void processTimeoutOrder(){
              log.info("定时检查超时订单,当前时间:{},", LocalDateTime.now());
              // 找到超时15min且状态为待支付的订单
              // select * from orders where status = ? and create_time < (当前时间-15min)
              Integer status = Orders.PENDING_PAYMENT;
              LocalDateTime orderTime = LocalDateTime.now().plusMinutes(-15);
              List<Orders> ordersList = orderMapper.getByStatusAndTimeLT(status,orderTime);
      
              // 修改订单状态
              if (ordersList!=null && ordersList.size()>0){
                  for (Orders orders : ordersList) {
                      orders.setStatus(Orders.CANCELLED);
                      orders.setCancelTime(LocalDateTime.now());
                      orders.setCancelReason("用户支付超时,自动取消订单");
                      orderMapper.update(orders);
                  }
              }
          }
      
          /**
           * 处理派送中订单
           * 每天凌晨1点处理
           */
          @Scheduled(cron = "0 0 1 * * ?")
          public void processDeliverOrders(){
              log.info("定时处理昨日订单,执行时间:{}",LocalDateTime.now());
              // 查询状态为派送中,且下单时间是当前时间每日凌晨1点前(-60)
              Integer status = Orders.DELIVERY_IN_PROGRESS;
              LocalDateTime orderTime = LocalDateTime.now().plusMinutes(-60);
              List<Orders> ordersList = orderMapper.getByStatusAndTimeLT(status,orderTime);
      
              // 修改订单状态
              if (ordersList!=null && ordersList.size()>0){
                  for (Orders orders : ordersList) {
                      orders.setStatus(Orders.COMPLETED);
                      orderMapper.update(orders);
                  }
              }
      
          }
      
      }
      

WebSocket

  • 基于tcp新网络协议
  • websocket和http都是基于tcp,先连接再发
  • http 短连接,基于请求响应,单向
  • websocke 长连接,握手协议后就是双向数据传输,服务器可以给客户端数据传输,客户端也可以给服务器数据传输

image-20250421113914153

websocket使用

  1. 导入websocket坐标

    image-20250421133050334

  2. 编写前端html浏览器,客户端

    回调函数,配置路径

  3. 编写后端websocket配置

    /**
     * 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();
                }
            }
        }
    
    }
    
  4. 编写配置类开启@ServerEndpoint注解

    package com.sky.config;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.socket.server.standard.ServerEndpointExporter;
    
    /**
     * WebSocket配置类,用于注册WebSocket的Bean
     */
    @Configuration
    public class WebSocketConfiguration {
    
        /**
         * 配置类注册ServerEndpointExporter
         * ServerEndpointExporter会扫描所有的websocket使他们生效
         * @return
         */
        @Bean
        public ServerEndpointExporter serverEndpointExporter() {
            return new ServerEndpointExporter();
        }
    
    }
    
  5. 测试

    @Component
    public class WebSocketTask {
        @Autowired
        private WebSocketServer webSocketServer;
    
        /**
         * 通过WebSocket每隔5秒向客户端发送消息
         */
        @Scheduled(cron = "0/5 * * * * ?")
        public void sendMessageToClient() {
            webSocketServer.sendToAllClient("这是来自服务端的消息:" + DateTimeFormatter.ofPattern("HH:mm:ss").format(LocalDateTime.now()));
        }
    }
    

配置类@Configuration

  • 加完@Configuration相当于spring配置文件

    注册bean时使用

    这种就是把ServerEndpointExporter交给spring管理,项目启动就被注册到spring托管

    <id="serverEndpointExporter" class ="ServerEndpointExporter">

    package com.sky.config;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.socket.server.standard.ServerEndpointExporter;
    
    /**
     * WebSocket配置类,用于注册WebSocket的Bean
     */
    @Configuration
    public class WebSocketConfiguration {
    
        /**
         * 配置类注册ServerEndpointExporter
         * ServerEndpointExporter会扫描所有的websocket使他们生效
         * @return
         */
        @Bean
        public ServerEndpointExporter serverEndpointExporter() {
            return new ServerEndpointExporter();
        }
    
    }
    
  • @component也是注册bean,需要配置类扫包,但主启动类自动扫子包

  • autowired就是找getbyname 找id一样的类bean,自动注入spring容器里面的bean

apache echarts

  • 数据可视化计算

  • 柱状图,饼状图,折线图

  • 前端要求规定数据格式

ECharts

```

LocalDateTime和LocalDate转化

  • LocalDate年月日,LocalDateTime年月日时分秒

    LocalDateTime beginTime = LocalDateTime.of(localDate, LocalTime.MIN);
    LocalDateTime endTime = LocalDateTime.of(localDate, LocalTime.MAX);
    

    image-20250422112421423

StringUtils(lang3)

  • 把列表转化为字符串的工具,还可以有分割符

  • join方法

    String turnoverList = StringUtils.join(sumList,",");
    
    2022-10-01,2022-10-02,2022-10-03
    

时间格式

  • 定义前端传入格式
public Result<TurnoverReportVO> turnoverReport(
        @DateTimeFormat(pattern = "yyyy-MM-dd")
        LocalDate begin,
        @DateTimeFormat(pattern = "yyyy-MM-dd")
        LocalDate end){
    TurnoverReportVO turnoverReportVO = reportService.getTurnoverReport(begin,end);
    return Result.success(turnoverReportVO);
}

==和equals

  • ==基本类型比值,引用比地址
  • equals比值
  • 一般equals用的多

websocket引起@springbootTest的bug

  • 原因分步解释

    1. ServerEndpointExporter 的作用 WebSocket 配置类(如 WebSocketConfiguration)通常包含一个 ServerEndpointExporter Bean,它的作用是:
      • 扫描所有 @ServerEndpoint 注解的类。
      • 将这些 WebSocket 端点注册到 Servlet 容器的 ServerContainer 中。
      • 关键依赖ServerContainer 是 Servlet 容器(如 Tomcat)提供的接口,只有在真实 Servlet 容器中才存在。
    2. 测试环境的默认行为 当使用 @SpringBootTest 时:
      • 默认模式webEnvironment = WebEnvironment.MOCK(模拟 Servlet 环境)。
      • 问题:此模式不会启动真实的 Servlet 容器,仅模拟 HTTP 请求(如 MockMvc)。
      • 结果ServerContainer 不可用,导致 ServerEndpointExporter 初始化失败。
    3. 上下文加载的完整性 Spring 测试要求完整加载应用上下文:
      • 如果某个 Bean 初始化失败(如 ServerEndpointExporter),整个上下文将无法加载。
      • 错误表现Failed to load ApplicationContext + ServerContainer not available
  • WebSocket 配置类

    package com.sky.config;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.socket.server.standard.ServerEndpointExporter;
    
    /**
     * WebSocket配置类,用于注册WebSocket的Bean
     */
    @Configuration
    public class WebSocketConfiguration {
    
        /**
         * 配置类注册ServerEndpointExporter
         * ServerEndpointExporter会扫描所有的websocket使他们生效
         * @return
         */
        @Bean
        public ServerEndpointExporter serverEndpointExporter() {
            return new ServerEndpointExporter();
        }
    
    }
    
  • WebSocket 配置类导致测试失败的核心矛盾在于:

  1. 依赖冲突ServerEndpointExporter 需要 Servlet 容器的 ServerContainer
  2. 环境差异:测试默认不启动真实 Servlet 容器,无法满足依赖。
  • 通过调整测试的 Web 环境或条件化配置,即可解决这一问题。

  • 解决方法

    方法 1:启动真实 Servlet 容器(推荐)

    在测试类中显式指定启动嵌入式容器(如 Tomcat):

    Java@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
    public class MyTest {
        // 测试代码
    }
    

    方法 2:条件化配置 WebSocket

    在 WebSocket 配置类中添加条件注解,避免在非 Servlet 环境中初始化:

    Java@Configuration
    @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) // 仅限 Servlet 环境
    public class WebSocketConfiguration {
        @Bean
        public ServerEndpointExporter serverEndpointExporter() {
            return new ServerEndpointExporter();
        }
    }
    

stream流

  • 遍历结果放入集合
List<String> nameList = salesDTOList.stream().map(GoodsSalesDTO::getName).collect(Collectors.toList());

List<Integer> numberList = salesDTOList.stream().map(GoodsSalesDTO::getNumber).collect(Collectors.toList());

这两行代码使用Java 8的Stream API对salesDTOList(一个包含GoodsSalesDTO对象的列表)进行转换,分别提取出每个对象的namenumber属性,并收集成两个独立的列表。具体解释如下:

第一行代码

JavaList<String> nameList = salesDTOList.stream()
 .map(GoodsSalesDTO::getName)
 .collect(Collectors.toList());
  1. salesDTOList.stream()salesDTOList转换为一个流(Stream),以便使用流式操作(如mapfilter等)。
  2. .map(GoodsSalesDTO::getName) 对每个GoodsSalesDTO对象调用getName()方法,将流中的每个元素(DTO对象)转换为它的name属性(String类型)。
    • GoodsSalesDTO::getName是方法引用,等价于:dto -> dto.getName()
  3. .collect(Collectors.toList()) 将转换后的流(包含所有name字符串)收集到一个新的List<String>中,赋值给nameList

结果nameList是一个包含所有GoodsSalesDTO对象名称的字符串列表。

第二行代码

JavaList<Integer> numberList = salesDTOList.stream()
    .map(GoodsSalesDTO::getNumber)
    .collect(Collectors.toList());
  1. salesDTOList.stream() 同样将salesDTOList转换为流。
  2. .map(GoodsSalesDTO::getNumber) 对每个GoodsSalesDTO对象调用getNumber()方法,将流中的每个元素(DTO对象)转换为它的number属性(Integer类型)。
  3. .collect(Collectors.toList()) 将转换后的流(包含所有number整数值)收集到一个新的List<Integer>中,赋值给numberList

结果numberList是一个包含所有GoodsSalesDTO对象数量的整数列表。

总结

  • 目的:将对象列表中的两个不同属性(namenumber)分别提取成两个独立的列表。

  • 输入List<GoodsSalesDTO> salesDTOList

  • 输出

    • nameList: List<String>,包含所有商品的名称。
    • numberList: List<Integer>,包含所有商品的数量。

类比

假设GoodsSalesDTO表示商品销售记录,包含商品名称和销售数量。 原始数据:[{name: "Apple", number: 10}, {name: "Banana", number: 20}] 运行代码后:

  • nameList["Apple", "Banana"]
  • numberList[10, 20]

DTO封装查询对象

  • 查询结果多个用list接收

    1. 先看sql查询结果是如何

      <select id="getTop10" resultType="com.sky.dto.GoodsSalesDTO">
          select od.name,sum(od.number) as 'number' from order_detail od,orders o
          where o.id = od.order_id and o.status = 5
          <if test="beginTime!=null">and o.order_time &gt; #{beginTime}</if>
          <if test="endTime!=null">and o.order_time &lt; #{endTime}</if>
          group by od.name
          order by number desc
          limit 0,10
      </select>
      
    2. 这里结果是name 和 number(别名),所有对应DTO对象也应该是这两个

      这里name对应name,number对应别名number,不能错

      public class GoodsSalesDTO implements Serializable {
          //商品名称
          private String name;
      
          //销量
          private Integer number;
      }
      
    3. 接收成功,因为查出来可能多个,所有要用集合接收

      List<GoodsSalesDTO> salesDTOList = orderMapper.getTop10(beginTime,endTime);
      
posted @ 2025-04-22 22:10  学习java的白菜  阅读(10)  评论(0)    收藏  举报