目录
架构的细分
使用实体类来接收配置文件中的值
webMvcConfig类:
jwt令牌
管理端的拦截器:
JwtProperties:
JwtTokenAdminInterceptor :
对密码加密操作
Redis:
分页查询
整体思想
为什么动态 SQL 推荐传实体类?
多表操作
Service层:
Mapper:
动态sql——update:
删除或者增加多条数据
增加多条数据:
Service 层:
Mapper 层
删除多条数据:
Service 层调用:
Mapper层:
总结:
什么时候需要外键约束?
什么时候需要用到多个表的数据关联查询?
需要获取相关联表的数据:
公共字段的处理:
微信登录后端开发:
整体的思路:
如何获取openid?
HttpClientUtil:
使用注解自动获取前端的值
1. @RequestParam
适用场景:
数据传递方式:
示例:
请求类型:
2. @RequestBody
适用场景:
数据传递方式:
示例:
请求类型:
3. @PathVariable
适用场景:
数据传递方式:
示例:
请求类型:
总结:
在更新的时候,如何进行覆盖?
如何完成两个 List 集合的转换?
转换逻辑步骤
架构的细分
首先从前单一的pojo类细分成了pojo,dto和vo,约定的就是dto接收前端的数据,vo返回给前端数据,pojo直接处理数据库的操作。(DTO用于数据传输,VO则专注于前端展示,POJO直接操作数据库)
使用实体类来接收配置文件中的值
在开发中,通常使用实体类来接收配置文件中的值,这种做法在Spring Boot等框架中较为常见。具体步骤如下:
使用实体类接收配置文件值:
配置文件引用:
- 在 application.yml文件中,通过${sky.alioss.endpoint}等形式引用了application-dev.yml中的sky.alioss配置项。这种引用方式有助于集中管理各环境的具体配置信息。
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类使用了- @ConfigurationProperties注解,并指定了- prefix = "sky.alioss",可以直接将配置文件中的- sky.alioss属性绑定到类中的字段。这种方式使得配置参数的管理更加结构化。
- 使用了 @Component注解,将AliOssProperties加入 Spring 容器,确保可以在项目中通过依赖注入来使用该类。@Component @ConfigurationProperties(prefix = "sky.alioss") @Data public class AliOssProperties { private String endpoint; private String accessKeyId; private String accessKeySecret; private String bucketName; }
在业务逻辑中引用配置类:
- 通过依赖注入的方式,在 JwtTokenAdminInterceptor中使用JwtProperties实体类(类似于AliOssProperties)来获取配置的 JWT 信息。
- 例如,jwtProperties.getAdminTokenName()和jwtProperties.getAdminSecretKey()引用了JwtProperties中的配置项,这些配置可以通过application.yml或application-dev.yml中的值自动注入,简化了配置管理并提高了代码的可读性。
总结:
- 使用实体类接收配置文件值,通过 @ConfigurationProperties注解实现配置属性的绑定,使得配置和代码逻辑解耦,配置更易管理。
- 通过 ${}方式引用其他配置文件的属性,增强了配置的灵活性,特别适合多环境配置管理。
webMvcConfig类:
package com.sky.config;
import com.sky.interceptor.JwtTokenAdminInterceptor;
import com.sky.interceptor.JwtTokenUserInterceptor;
import com.sky.json.JacksonObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import java.util.List;
/**
 * 配置类,注册web层相关组件
 */
@Configuration
@Slf4j
@EnableSwagger2
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");
    }
    /**
     * 通过knife4j生成接口文档
     * @return
     */
    @Bean
    public Docket docket1() {
        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 docket2() {
        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;
    }
    /**
     * 设置静态资源映射
     * @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/");
    }
    /**
     * 扩展nvc框架的消息转换器
     * @param converters
     */
    protected void extendMessageConverters(List<HttpMessageConverter<?>> converters){
        // 创建一个消息转换器对象,把java对象转为json字符串
        MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
        //设置对象转换器,可以将Java对象转为json字符串
        converter.setObjectMapper(new JacksonObjectMapper());
        //将我们自己的转换器放入spring Mvc框架的容器中
        converters.add(0,converter);
    }
}
webMvcConfig类里面配置的内容:
-  拦截器配置 ( addInterceptors):- 用于注册自定义的拦截器,拦截请求并执行特定的逻辑,比如鉴权、日志记录等。
- 在 addInterceptors方法中,通过registry.addInterceptor(ResourceHandlerRegistry registry)将自定义拦截器加入到 Spring MVC 的拦截链中。
- 注意!!!只有需要对所有请求都要进行身份验证的时候才需要注入到配置类,其他轻量级的就只需要Component注解。
 
-  Knife4j 配置: - 配置 Swagger 接口文档生成工具 Knife4j,通常会涉及设置 DocketBean,用于扫描控制器和接口,生成 API 文档。
 
- 配置 Swagger 接口文档生成工具 Knife4j,通常会涉及设置 
-  静态资源映射: - 配置 Spring MVC 处理静态资源文件的方式,通常是映射 JavaScript、CSS、图片等静态文件。
 
-  消息转换器配置: - 通过扩展 Spring MVC 的 HttpMessageConverters,可以自定义消息转换器,以满足特定的 JSON 序列化、反序列化需求。
 
- 通过扩展 Spring MVC 的 
jwt令牌
登录成功后,可以使用jwtutil生成jwt令牌
JWTUtil:
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();
     // 当你调用 compact() 方法时,它会将 JWT 的三部分(头部、载荷和签名)进行编码,
     // 并将它们合并成一个字符串。这个字符串是最终的 JWT。
    }
    /**
     * 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;
    }
}
如何使用JWTUtil呢?首先 我们知道的是createJWT的代码需要三个参数,
jwt秘钥,jwt过期时间(毫秒),以及设置的信息(设置的信息是一个map,通常map的key是字符串(Id)Value是其id值)其实就是JWT 载荷
//登录成功后,生成jwt令牌
        Map<String, Object> claims = new HashMap<>();
        claims.put(JwtClaimsConstant.EMP_ID, employee.getId());
        String token = JwtUtil.createJWT(
                jwtProperties.getAdminSecretKey(),
                jwtProperties.getAdminTtl(),
                claims);登录成功后,我们可以将生成的 JWT 令牌存储在另一个实体类中,并将其返回给前端。这样,在前端请求其他需要认证的接口时,可以通过在请求头中携带 JWT 令牌,拦截器就能够通过 preHandle 方法来对该令牌进行校验。通常的做法如下:
- 从请求中获取 JWT 令牌,通常是在请求头中,如 Authorization字段中提取令牌。
- 解析 JWT 令牌,调用 JwtUtil等工具类进行解析。
- 校验令牌的合法性,比如检查是否有效、是否过期、签名是否正确等。
- 设置用户信息,通过 BaseContext将用户信息设置到当前线程中,以便后续的业务逻辑中使用。
- 拦截请求,如果令牌无效或校验失败,则返回错误信息,拒绝请求;否则,放行请求,继续执行控制器方法。
管理端的拦截器:
同样使用使用实体类来接收配置文件中的值。这个方法在第2点提过。
JwtProperties:
package com.sky.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@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;
}
JwtTokenAdminInterceptor :
package com.sky.interceptor;
import com.sky.constant.JwtClaimsConstant;
import com.sky.context.BaseContext;
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
     *
     * @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());
        System.out.println(token);
        //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);
            BaseContext.setCurrentId(empId);
            //3、通过,放行
            return true;
        } catch (Exception ex) {
            //4、不通过,响应401状态码
            response.setStatus(401);
            return false;
        }
    }
}
对密码加密操作
注册的时候,需要对密码进行加密。
 employee.setPassword(DigestUtils.md5DigestAsHex(PasswordConstant.DEFAULT_PASSWORD.getBytes()));PasswordConstant.DEFAULT_PASSWORD.是默认密码123456
Redis:
可以使用Redis来存储个别字段,例如需要被客户端和用户端同时查看到的店铺的状态。
推荐存储到Redis的情况:
-  用户会话信息: 使用 Redis 存储用户的登录状态、权限信息等。这些数据通常被多个客户端访问,Redis 提供了高效的读写性能。 
-  热点数据: 如果某些字段(例如商品价格、库存数量等)频繁被访问,可以将其缓存到 Redis 中,避免每次都查询数据库。 
-  共享字段: 对于需要同时由客户端和用户端访问的字段,Redis 作为缓存可以保证数据的一致性和快速访问。例如,某些公共配置或实时更新的数据,可以存放在 Redis 中,客户端和服务器都能轻松读取。 
-  缓存过期机制: Redis 提供了缓存过期的功能,当字段数据发生变化时,可以自动使缓存失效并重新加载。 
分页查询
分页查询:使用pageHelper,如果存在可选的参数需要根据参数来进行分页查询的时候,特需要动态sql(如果这个参数的值不为null,那么就在sql语句添加条件限制)
CategoryPageQueryDTO:
package com.sky.dto;
import lombok.Data;
import java.io.Serializable;
@Data
public class CategoryPageQueryDTO implements Serializable {
    //页码
    private int page;
    //每页记录数
    private int pageSize;
    //分类名称
    private String name;
    //分类类型 1菜品分类  2套餐分类
    private Integer type;
}
controller:
    /**
     *分类分页查询
     * @param categoryPageQueryDTO
     * @return
     */
    @GetMapping("/page")
    @ApiOperation("分类分页查询")
    public Result<PageResult> page(CategoryPageQueryDTO categoryPageQueryDTO){
       PageResult pageResult   =  categoryService.page(categoryPageQueryDTO);
        System.out.println("-----------------------------------------------");
        System.out.println(categoryPageQueryDTO.getType());
        return Result.success(pageResult);
    }Service:
 /**
     * 分类分页查询
     * @param categoryPageQueryDTO
     * @return
     */
    @Override
    public PageResult page(CategoryPageQueryDTO categoryPageQueryDTO) {
      PageHelper.startPage(categoryPageQueryDTO.getPage(),categoryPageQueryDTO.getPageSize());
      Page<Category> page = categoryMapper.pageQuery(categoryPageQueryDTO);
      // page-> PageResult
        long total = page.getTotal();
        List<Category> result = page.getResult();
        return new PageResult(total,result);
    }categoryPageQueryDTO里面封装了pageSize和Page(第几个页面)
sql映射文件:
    <select id="pageQuery" resultType="com.sky.entity.Category">
        select * from category
        <where>
            <if test="type != null"> and type = #{type} </if>
            <if test="name != null and name != ''" >and name like concat('%',#{name},'%')</if>
        </where>
        order by sort asc,create_time desc
    </select>注意,第二个到后一个if标签中需要包含and字段
整体思想
对于某个单一字段的更新,我们可以先创建一个对象,然后直接调用动态的更新sql语句,参数为带有更新字段的对象,这样不需要重复写单个字段的更新的sql语句
创建对象 :
  Dish dish = Dish.builder()
                .id(id)
                .status(status)
                .build();
        dishMapper.update(dish);动态更新语句
<update id="update" useGeneratedKeys="true" keyProperty="id">
        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>
        </set>
        <where>
            id = #{id}
        </where>
    </update>我们可以在单独改变status的时候调用此接口,也可以在修改菜品的时候调用。注意在这个sql动态语句中,每一个if标签里面最后都有一个,(逗号)
但是一定要注意的是!!!!如果xml里面写的是动态sql的话,确保未使用的字段为 null 或空字符串(也就是要使用实体类,保证字段为空)
为什么动态 SQL 推荐传实体类?
动态 SQL 的逻辑依赖传入参数的字段值:
- 动态 SQL 通过 <if>标签判断参数是否为null或满足某种条件来拼接 SQL。
- 如果只传递一个字段,而其他字段的默认值不为 null,它们仍会参与条件拼接,从而生成错误的 SQL。
我就出现过这个错误,我调用xml的动态sql,但是我的参数只有id,这个时候的动态sql:
==>  Preparing: select * from orders WHERE number like concat('%',?,'%') and phone like concat('%',?,'%') and user_id = ? and status = ? and order_time >= ? and order_time <= ? and id = ?
 ==> Parameters: 7(Long), 7(Long), 7(Long), 7(Long), 7(Long), 7(Long), 7(Long)
 <==      Total: 0 
很明显,其他的字段都被附上了7,这个时候就错了。
多表操作
对于涉及到多个表的操作:
就需要使用注解@Transactional
@Transactional注解用于管理事务,确保在多个数据库操作过程中要么全部成功,要么全部失败,从而保持数据的一致性和完整性。
场景描述:
- 在 Table1 中插入数据后,获取生成的 ID(假设这个 ID 是自增的)。
- 使用这个 ID 来插入 Table2,作为外键或关联字段。
解决方法:
- 在 Mapper XML 配置中,使用 useGeneratedKeys和keyProperty来获取自增 ID。
- 在 Service 层中,通过获取生成的 ID,再执行插入第二个表的操作。
具体实例如下:
新增菜品,首先我们知道菜品中有口味,口味又是一个单独的数据表,所以我们需要开启事务,保证数据的唯一性 ,同时需要注意的是,在插入了菜品后,我们需要得到对应菜品的id值来关联给Flavor_dish的dish_id值。所以我们需要用到useGeneratedKeys="true" keyProperty="id",这样在Service层中使用getId才能获得相应的属性
Service层:
/**
     * 新增菜品和口味,
     * 涉及多张表的数据的一致性,需要加上注解-事务处理的注解
     *
     * @param dishDTO
     */
    @Override
    // 注意需要在启动类上面添加@EnableTransactionManagement
    // 开启注解方式的事务管理
    @Transactional
    public void add(DishDTO dishDTO) {
        Dish dish = new Dish();
        BeanUtils.copyProperties(dishDTO, dish);
        // 像菜品表插入一条数据
        dishMapper.add(dish);
        // 获取insert语句生成的主键值
        Long id = dish.getId();
        List<DishFlavor> flavors = dishDTO.getFlavors();
        if (flavors != null && flavors.size() > 0) {
            // 遍历口味,赋id值
            flavors.forEach(dishFlavor -> {
                dishFlavor.setDishId(id);
            });
            // 向口味表插入n条数据
            disFlavorMapper.addBatch(flavors);
        }
    }Mapper:
    <insert id="add" useGeneratedKeys="true" keyProperty="id">
        insert into dish(name, category_id, price, image, description, create_time, update_time, create_user, update_user,status)
        values (#{name},#{categoryId},#{price},#{image},#{description},#{createTime},#{updateTime},#{createUser},#{updateUser},#{status})
    </insert>动态sql——update:
<update id="update">
    update setmeal
    <set>
        <if test="categoryId != null">category_id = #{categoryId},</if>
        <if test="description != null">description = #{description},</if>
    </set>
    where id = #{id}
</update>
注意啊!!每一个if标签文本的最后都需要逗号!!本人老是喜欢错
删除或者增加多条数据
向一个表中增加或者删除多条数据
增加多条数据:
如果 Service 层的参数是 List 集合,并需要传递给 Mapper 层进行批量操作,可以使用 MyBatis 的批量插入或更新功能。
Service 层:
public void batchInsert(List<Entity> entities) {
    mapper.batchInsert(entities);
}
Mapper 层
<insert id="batchInsert" parameterType="java.util.List">
    INSERT INTO 表名 (列1, 列2, 列3)
    VALUES
    <foreach collection="list" item="item" separator=",">
        (#{item.列1}, #{item.列2}, #{item.列3})
    </foreach>
</insert>
删除多条数据:
Service 层调用:
 void deleteIds(List<Long> ids);Mapper层:
使用 IN 语法
    <delete id="deleteIds">
        delete from dish where id in
        <foreach collection="ids" item="id" open="(" close=")" separator=",">
                #{id}
        </foreach>
    </delete>
总结:
在开发中,尽可能使用 对应类的 List 集合 作为参数,并配合 动态 SQL 来处理批量增删改查(CRUD)操作,能够显著减少 SQL 代码的重复性,提高代码的简洁性和可维护性。 其实也就是整体思想和删除或者增加多条数据的体现。
注意:我在写多条增加语句的时候,写成了:
<insert id="add">
    insert into setmeal_dish(setmeal_id, dish_id, name, price, copies)
    values
        <foreach collection="setmealDishes" item="ds" separator="," open="(" close=")">
            setmeal_id = #{ds.setmealId},dish_id=#{ds.dishId},name=#{ds.name},price = #{ds.price},copies = #{ds.copies}
        </foreach>
    </insert>但实际上,括号要放在里面,每一次都需要括号!!!!
什么时候需要外键约束?
- 多个表的数据关联查询:需要查询多个表中的数据。
- 需要获取相关联表的数据:例如,查询菜品的同时也需要返回菜品所属的分类。
怎么说呢?举个例子吧:
套餐和菜品的关系:
 假设你有一个 setmeal_dish 表,它记录了套餐(setmeal)和菜品(dish)之间的关系。比如,一个套餐可以包含多个菜品,一个菜品也可以出现在多个套餐中。这个表通常会有两个外键:
setmeal_id(指向 setmeal 表的外键)
dish_id(指向 dish 表的外键)
-  外键约束的作用: - 外键约束可以确保 setmeal_dish表中的setmeal_id和dish_id必须分别在setmeal和dish表中存在。如果你尝试插入一个不存在的套餐ID或菜品ID,数据库会拒绝操作,从而避免了无效数据的插入。
- 同时,外键约束还可以保证数据一致性。例如,如果删除了一个套餐(setmeal表中的某个记录),可以通过外键约束配置,确保所有相关的套餐-菜品关系(即setmeal_dish表中的记录)也被删除或更新,从而避免孤立的数据。
 
- 外键约束可以确保 
什么时候需要用到多个表的数据关联查询?
需要获取相关联表的数据:
在开发中,我们需要通过套餐的id值,得到套餐里面包含的菜品 的具体信息。这个时候我们肯定需要查询这个关系表,那么我们就不能单一的写成:
<select id="getDishByCondition" resultMap="SetmealDishMap">
    select * from setmeal_dish
    <where>
        <if test="name != null">and name = #{name}</if>
        <if test="price != null">and price = #{price}</if>
        <if test="setmealId != null">and setmeal_id = #{setmealId}</if>
    </where>
</select>
因为如果这样写的话,我们只能获得菜品的id值,不能获得dish表当中具体的菜品信息。所以就应该使用多个表的关联查询:
    <select id="getDishByCondition" resultType="com.sky.vo.DishItemVO">
        select sd.name, sd.copies, d.image, d.description
        from setmeal_dish sd
        left join dish d on sd.dish_id = d.id
        <where>
            <if test="name != null">and sd.name = #{name}</if>
            <if test="price != null">and sd.price = #{price}</if>
            <if test="setmealId != null">and sd.setmeal_id = #{setmealId}</if>
        </where>
    </select>这样的话,既能获取到 setmeal_dish 的字段还能获取到对应的dish的详细信息。
总之:在查看开发文档时,注意检查响应数据是否包含了来自多个表的字段。如果响应数据中确实有多个表的字段,那么就可以考虑使用多表关联查询来获取这些数据。
公共字段的处理:
对于公共字段的处理:
需要注意的是:
当 AutoFillAspect 切面代码执行时,遇到 List 类型参数,会试图直接对 List 调用 setCreateTime 方法。然而,List 本身是集合接口,没有这个方法,因此会导致 NoSuchMethodException。因此,在多条记录同时增加的时候我们就需要注意不能加上@AutoFill的注解
  void add(List<SetmealDish> setmealDishes);微信登录后端开发:
微信登录的后端开发代码:
整体的思路:
-  前端:获取 code-  前端通过微信登录接口获取到授权码 code。微信登录流程是通过微信SDK来完成的,具体步骤如下:- 前端调用微信的wx.login()接口,获取code。
- code代表用户的临时凭证,微信后台用它来换取用户的- openid和- session_key。
 
- 前端调用微信的
 
-  
- 后端Controller接收到openid后调用Service层,Service调用mapper.getByOpenid(openid)
 查询数据库,判断该用户是否已注册。
- 调用controller,controller调用Service层
- Service层通过调用微信服务接口获取openid(使用自己封装的HttpClientUtil来发送请求获得Response响应的字符串) 
  -  后端Service层接收到前端传递的 code后,调用微信的API来换取openid和session_key。后端通过向微信的服务器发送请求,获取用户的openid。
-  微信的接口 URL: https://api.weixin.qq.com/sns/jscode2session-  请求参数: - appid: 微信小程序的- appID
- secret: 微信小程序的- appSecret
- js_code: 前端传递的- code
- grant_type: 固定为- authorization_code
 
- 然后调用Mapper层根据openid查询用户信息,如果用户已注册,controller层生成JWT token后封装成vo对象返回给前端;如果未注册,则返回相调用接口userMapper.add(user);。
 
-  
 
-  
- 前端根据后端返回的用户信息和token来处理登录状态,进行页面跳转等操作
接下来是细节的介绍:
主要是Service层的impl:
如何获取openid?
private String getOpenId(String code) {
    // 调用微信接口服务,获取当前微信用户的openid
    Map<String, String> map = new HashMap<>();
    map.put("appid", ...);  // 填入小程序的AppID
    map.put("secret", ...);  // 填入小程序的AppSecret
    map.put("js_code", code); // 前端传递的code
    map.put("grant_type", "authorization_code"); // 固定参数,代表授权模式
    // 发送HTTP请求到微信API接口获取用户信息
    String response = HttpClientUtil.doget("https://api.weixin.qq.com/sns/jscode2session", map);
    
    // 解析返回的JSON,获取openid
    JSONObject jsonObject = JSON.parseObject(response);
    return jsonObject.getString("openid"); // 返回openid
}
其他的ServiceImpl:
@Service
@Slf4j
public class UserServiceImpl implements UserService {
    // 微信服务接口地址
    public static final String EX_LOGIN = "https://api.weixin.qq.com/sns/jscode2session";
    @Autowired
    private WeChatProperties weChatProperties;
    @Autowired
    UserMapper userMapper;
    /**
     * 微信登录
     * @param loginDTO
     * @return
     */
    @Override
    public User wxlogin(UserLoginDTO loginDTO) {
        String openid = getOpenId(loginDTO.getCode());
        System.out.println(openid);
        // 判断openid是否为空,如果为空表示登录失败,就抛出异常
        if (openid == null) {
            throw new LoginFailedException(MessageConstant.LOGIN_FAILED);
        }
        // TODo 这里只能获取到openid,由于个人开发的权限问题其他的获取不到
        // 当前用户的openid是否存在于数据库(为新用户)
        User user =  userMapper.getByOpenid(openid);
        if(user == null ){
            user = User.builder()
                    .openid(openid)
                    .createTime(LocalDateTime.now())
                    .build();
            System.out.println("==========================");
            System.out.println(user);
            // 如果是新用户,自动完成注册
            userMapper.add(user);
        }
        // 返回用户对象
        return  user;
    }一个小细节:
由于返回的值需要用户的id值,所以在增加用户的mapper是xml文件形式,useGeneratedKeys="true"和keyProperty="id",可以让数据库生成的自增id自动填充到User对象的id字段中,这样返回的时候user才能有id值

由于小程序端也需要保证用户是登录的状态,所以我们也需要设置一个拦截器,根据用户端的拦截器改一改方法中的一些参数的值就可以了。
同样的在webMvcconfig配置类里面注入该拦截器,将该拦截器加入到拦截链条里面即可。
HttpClientUtil:
package com.sky.utils;
import com.alibaba.fastjson.JSONObject;
import org.apache.http.NameValuePair;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
 * 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();
    }
}
使用注解自动获取前端的值
1. @RequestParam
 
@RequestParam 注解用于从 查询参数 中获取数据,通常用于 GET 请求 或带有查询参数的 POST 请求。前端发送的数据以 URL 的查询字符串形式传递。
适用场景:
- 用于接收 URL 中的查询参数(query parameters)。
- 常见于表单提交或 URL 中的查询字符串。
数据传递方式:
- 前端通过 查询字符串 发送数据,形式如 ?name=value&age=30。
示例:
前端请求:
GET /user?name=JohnDoe&age=30
如果请求中的参数是一个普通类型(例如 Long、Integer、String 等),并且该参数名称与方法中的参数名称相同,则通常可以通过 @RequestParam 来自动映射。如果没有显式地指定 @RequestParam,Spring 会默认从请求参数中提取与方法参数同名的值。(因此,只要参数名和方法中的参数名称相同,那么就不需要显示写注解了)
但是,如果请求中的参数是一个集合(例如 List),那么你需要使用 @RequestParam 来指定参数名,因为 Spring 无法根据名称自动推断列表类型。
    /**
     * 菜品的批量删除
     * @param ids
     * @return
     */
    @DeleteMapping
    @ApiOperation("菜品的批量删除")
    public Result delete(@RequestParam List<Long> ids){
        log.info("ids的值为:{}",ids);
        dishService.delete(ids);
        return Result.success();
    }请求类型:
- 主要用于 query string 类型的数据。
2. @RequestBody
 
@RequestBody 注解用于从 请求体(body)中获取数据,通常与 POST、PUT 或 PATCH 请求一起使用。它常用于接收 JSON 或 XML 数据。
适用场景:
- 用于处理 请求体 中的复杂数据,尤其是 JSON 数据。
- 适合用于 API 接口中,接收客户端发送的对象。
数据传递方式:
- 前端通过 HTTP 请求的 请求体 发送数据,通常是 JSON 格式。
示例:
前端请求:
POST /user
Content-Type: application/json
{
  "name": "JohnDoe",
  "age": 30
}
注意:
- @RequestBody注解通常与 对象类型 参数结合使用,Spring 会自动将 JSON 转换成相应的 Java 对象。
- 需要确保请求头的 Content-Type为application/json或其他合适的媒体类型。
请求类型:
- 适用于 JSON 或 XML 格式的数据,通常是 POST 或 PUT 请求。
3. @PathVariable
 
@PathVariable 注解用于从 路径参数 中获取数据,通常用于 RESTful 风格的 URL 中的动态部分。这些动态值是 URL 中的占位符。
适用场景:
- 用于从 URL 中的路径中获取参数,常用于 REST API 的路径设计。
- 适合处理资源的唯一标识符,如 ID。
数据传递方式:
- 前端通过 URL 的路径 传递参数,通常用于资源标识符,如用户 ID、商品 ID 等。
示例:
前端请求:
GET /user/123
注意:
- @PathVariable注解用于 URL 模板中的路径变量部分。
- 路径参数和方法参数之间的映射是通过变量名称来自动匹配的。
请求类型:
- 用于 路径参数 的数据,通常是 RESTful URL 中的一部分。
总结:
-  @RequestParam:- 用于获取 查询参数(query parameters)。
- 常见于 GET请求,数据以查询字符串形式传递。
 
-  @RequestBody:- 用于获取 请求体 中的数据,通常是 JSON 或 XML。
- 常见于 POST、PUT请求。
 
-  @PathVariable:- 用于获取 路径参数,通常是 URL 的动态部分(如 ID)。
- 常见于 RESTful API。
 
在更新的时候,如何进行覆盖?
就比如说苍穹外卖吧,比如说在修改菜品的时候还需要修改口味,这个时候我刚开始的时候想的是修改菜品+修改口味,我有一个想法就是通过菜品的id值,查数据库找到口味的id值(list集合),然后进行修改。但其实还有一个更加简便的方法就是直接先根据菜品id直接删除最后在添加。
其实这两种没有本质区别,知识第二种逻辑更加简单罢了。
如何完成两个 List 集合的转换?
 
转换逻辑步骤
-  使用 stream()转换流:- orderDetailList.stream()将订单详情列表- orderDetailList转换为流,便于对其中的每个元素进行操作。
 
-  使用 map()映射操作:- map是流操作的一部分,作用是将流中的每个元素进行映射转换。
- 通过映射操作,将 OrderDetail对象转换为ShoppingCart对象。
 
-  复制属性: - BeanUtils.copyProperties(x, shoppingCart, "id")复制两个对象之间的相同属性,忽略- id。
- 忽略字段 "id"是因为你可能希望使用其他逻辑生成新的id,或者保留购物车对象的id不变。
 
-  添加其他属性: - 设置 userId和createTime等特定的属性,这些属性在订单详情中可能不存在,需要手动赋值。
 
- 设置 
-  收集结果: - collect(Collectors.toList())将流操作的结果收集为一个新的- List集合。
 
	/**
     * 再来一单
     *
     * @param id
     */
    public void repetition(Long id) {
        // 查询当前用户id
        Long userId = BaseContext.getCurrentId();
        // 根据订单id查询当前订单详情
        List<OrderDetail> orderDetailList = orderDetailMapper.getByOrderId(id);
        // 将订单详情对象转换为购物车对象
        List<ShoppingCart> shoppingCartList = orderDetailList.stream().map(x -> {
            ShoppingCart shoppingCart = new ShoppingCart();
            // 将原订单详情里面的菜品信息重新复制到购物车对象中
            BeanUtils.copyProperties(x, shoppingCart, "id");
            shoppingCart.setUserId(userId);
            shoppingCart.setCreateTime(LocalDateTime.now());
            return shoppingCart;
        }).collect(Collectors.toList());
        // 将购物车对象批量添加到数据库
        shoppingCartMapper.insertBatch(shoppingCartList);
    }


















