学习中学习的小tips(主要是学习苍穹外卖的一些学习)

news2025/4/1 2:27:34

目录

架构的细分

使用实体类来接收配置文件中的值

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.ymlapplication-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,通常会涉及设置 Docket Bean,用于扫描控制器和接口,生成 API 文档。
  • 静态资源映射

    • 配置 Spring MVC 处理静态资源文件的方式,通常是映射 JavaScript、CSS、图片等静态文件。
  • 消息转换器配置

    • 通过扩展 Spring MVC 的 HttpMessageConverters,可以自定义消息转换器,以满足特定的 JSON 序列化、反序列化需求。

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 方法来对该令牌进行校验。通常的做法如下:

  1. 从请求中获取 JWT 令牌,通常是在请求头中,如 Authorization 字段中提取令牌。
  2. 解析 JWT 令牌,调用 JwtUtil 等工具类进行解析。
  3. 校验令牌的合法性,比如检查是否有效、是否过期、签名是否正确等。
  4. 设置用户信息,通过 BaseContext 将用户信息设置到当前线程中,以便后续的业务逻辑中使用。
  5. 拦截请求,如果令牌无效或校验失败,则返回错误信息,拒绝请求;否则,放行请求,继续执行控制器方法。

 管理端的拦截器:

同样使用使用实体类来接收配置文件中的值。这个方法在第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注解用于管理事务,确保在多个数据库操作过程中要么全部成功,要么全部失败,从而保持数据的一致性和完整性。

场景描述:

  1. Table1 中插入数据后,获取生成的 ID(假设这个 ID 是自增的)。
  2. 使用这个 ID 来插入 Table2,作为外键或关联字段。

解决方法:

  1. Mapper XML 配置中,使用 useGeneratedKeyskeyProperty 来获取自增 ID。
  2. 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_iddish_id 必须分别在 setmealdish 表中存在。如果你尝试插入一个不存在的套餐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代表用户的临时凭证,微信后台用它来换取用户的openidsession_key
  • 后端Controller接收到openid后调用Service层,Service调用mapper.getByOpenid(openid)
    查询数据库,判断该用户是否已注册。
  • 调用controller,controller调用Service层
  • Service层通过调用微信服务接口获取openid(使用自己封装的HttpClientUtil来发送请求获得Response响应的字符串)
    • 后端Service层接收到前端传递的code后,调用微信的API来换取openidsession_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

如果请求中的参数是一个普通类型(例如 LongIntegerString 等),并且该参数名称与方法中的参数名称相同,则通常可以通过 @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)中获取数据,通常与 POSTPUTPATCH 请求一起使用。它常用于接收 JSONXML 数据。

适用场景:
  • 用于处理 请求体 中的复杂数据,尤其是 JSON 数据。
  • 适合用于 API 接口中,接收客户端发送的对象。
数据传递方式:
  • 前端通过 HTTP 请求的 请求体 发送数据,通常是 JSON 格式。
示例:

前端请求:

POST /user
Content-Type: application/json

{
  "name": "JohnDoe",
  "age": 30
}

注意:

  • @RequestBody 注解通常与 对象类型 参数结合使用,Spring 会自动将 JSON 转换成相应的 Java 对象。
  • 需要确保请求头的 Content-Typeapplication/json 或其他合适的媒体类型。
请求类型:
  • 适用于 JSONXML 格式的数据,通常是 POST 或 PUT 请求。

3. @PathVariable

@PathVariable 注解用于从 路径参数 中获取数据,通常用于 RESTful 风格的 URL 中的动态部分。这些动态值是 URL 中的占位符。

适用场景:
  • 用于从 URL 中的路径中获取参数,常用于 REST API 的路径设计。
  • 适合处理资源的唯一标识符,如 ID。
数据传递方式:
  • 前端通过 URL 的路径 传递参数,通常用于资源标识符,如用户 ID、商品 ID 等。
示例:

前端请求:

GET /user/123

注意:

  • @PathVariable 注解用于 URL 模板中的路径变量部分。
  • 路径参数和方法参数之间的映射是通过变量名称来自动匹配的。
请求类型:
  • 用于 路径参数 的数据,通常是 RESTful URL 中的一部分。

总结:

  1. @RequestParam

    • 用于获取 查询参数(query parameters)。
    • 常见于 GET 请求,数据以查询字符串形式传递。
  2. @RequestBody

    • 用于获取 请求体 中的数据,通常是 JSON 或 XML。
    • 常见于 POSTPUT 请求。
  3. @PathVariable

    • 用于获取 路径参数,通常是 URL 的动态部分(如 ID)。
    • 常见于 RESTful API。

在更新的时候,如何进行覆盖?

就比如说苍穹外卖吧,比如说在修改菜品的时候还需要修改口味,这个时候我刚开始的时候想的是修改菜品+修改口味,我有一个想法就是通过菜品的id值,查数据库找到口味的id值(list集合),然后进行修改。但其实还有一个更加简便的方法就是直接先根据菜品id直接删除最后在添加。

其实这两种没有本质区别,知识第二种逻辑更加简单罢了。

如何完成两个 List 集合的转换?

转换逻辑步骤

  1. 使用 stream() 转换流

    • orderDetailList.stream() 将订单详情列表 orderDetailList 转换为流,便于对其中的每个元素进行操作。
  2. 使用 map() 映射操作

    • map 是流操作的一部分,作用是将流中的每个元素进行映射转换。
    • 通过映射操作,将 OrderDetail 对象转换为 ShoppingCart 对象。
  3. 复制属性

    • BeanUtils.copyProperties(x, shoppingCart, "id") 复制两个对象之间的相同属性,忽略 id
    • 忽略字段 "id" 是因为你可能希望使用其他逻辑生成新的 id,或者保留购物车对象的 id 不变。
  4. 添加其他属性

    • 设置 userIdcreateTime 等特定的属性,这些属性在订单详情中可能不存在,需要手动赋值。
  5. 收集结果

    • 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);
    }

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2323922.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【极速版 -- 大模型入门到进阶】LORA:大模型轻量级微调

文章目录 &#x1f30a; 有没有低成本的方法微调大模型&#xff1f;&#x1f30a; LoRA 的核心思想&#x1f30a; LoRA 的初始化和 r r r 的值设定&#x1f30a; LoRA 实战&#xff1a;LoraConfig参数详解 论文指路&#xff1a;LORA: LOW-RANK ADAPTATION OF LARGE LANGUAGE M…

线程同步——读写锁

Linux——线程同步 读写锁 目录 一、基本概念 1.1 读写锁的基本概念 1.2 读写锁的优点 1.3 读写锁的实现 1.4 代码实现 一、基本概念 线程同步中的读写锁&#xff08;Read-Write Lock&#xff09;&#xff0c;也常被称为共享-独占锁&#xff08;Shared-Exclusive Lock&a…

邪性!Anaconda安装避坑细节Windows11

#工作记录 最近不断重置系统和重装Anaconda&#xff0c;配置的要累死&#xff0c;经几十次意料之外的配置状况打击之后&#xff0c;最后发现是要在在Anaconda安装时&#xff0c;一定要选“仅为我安装”这个选项&#xff0c;而不要选“为所有用户安装”这个选项。 选“仅为我安…

【大模型】激活函数之SwiGLU详解

文章目录 1. Swish基本定义主要特点代码实现 2. GLU (Gated Linear Unit)基本定义主要特点代码实现 3. SwiGLU基本定义主要特点代码实现 参考资料 SWiGLU是大模型常用的激活函数&#xff0c;是2020年谷歌提出的激活函数&#xff0c;它结合了Swish和GLU两者的特点。SwiGLU激活函…

AOA与TOA混合定位,MATLAB例程,三维空间下的运动轨迹,滤波使用EKF,附下载链接

本文介绍一个MATLAB代码&#xff0c;实现基于 到达角&#xff08;AOA&#xff09; 和 到达时间&#xff08;TOA&#xff09; 的混合定位算法&#xff0c;结合 扩展卡尔曼滤波&#xff08;EKF&#xff09; 对三维运动目标的轨迹进行滤波优化。代码通过模拟动态目标与基站网络&am…

【211】线上教学系统

--基于SSM线上教学平添 主要实现的功能有&#xff1a; 管理员 : 首页、个人中心、学员管理、资料类型管理、学习资料管理、交流论坛、我的收藏管理、试卷管理、留言板管理、试题管理、系统管理、考试管理。 学员 : 首页、个人中心、我的收藏管理、留言板管理、考试管理。 前台…

从混乱思绪到清晰表达:记录想法如何改变你的学习人生

关键要点 • 记录想法似乎是发现自己想法并将其组织成可传播形式的最佳理由&#xff0c;研究表明写作和教学能增强学习和理解。 • 证据倾向于支持写作有助于澄清思想&#xff0c;而教学通过“教授效应”深化知识。 • 教学和分享被认为是最有效的学习方法&#xff0c;这与记录…

电机控制常见面试问题(二十)

文章目录 一.整流电路绕组接法二.电机为什么需要转速器三.电机转矩产生原理四.电机控制中载波频率大小的确定五.开关周期 Tpwm 一.整流电路绕组接法 为了引出直流的输出&#xff0c;一定要在整流变压器的二次侧引出零线&#xff0c;所以二次侧绕组必须接成星形 一次绕组必须要…

小爱控制via电视浏览器搜索图片-Homeassistant重制上一个自动化

制作自动化详情 为了完成图片搜&#xff0c;暂定指令找找{描述} 在执行脚本的adb地方输入以下指令&#xff0c;百度 因安全不让在图片地址直接搜转用bing >- >am start -n mark.via.gp/mark.via.Shell -a android.intent.action.VIEW -d https://cn.bing.com/images/…

unity一个图片的物体,会有透明的效果

如图 想要去掉这个透明效果 选择一个高层级的layer即可。

docker网桥问题导致ldap组件安装失败分析解决

使用pass_install_x86_64_0124版部署k8s底座、kem&#xff1b; 问题&#xff1a;一台kem节点部署ldap组件失败 解决&#xff1a;恢复问题主机的docker0网卡&#xff0c;重新部署kem相关组件 二、问题详情 现象描述 ansible部署kem组件 TASK [kem : start ldap] **********…

【Python】pillow库学习笔记1-Image类

《Python语言程序设计基础 》第3版&#xff0c;嵩天 黄天羽 杨雅婷著&#xff0c;P293 1.pillow库概述 Pillow 库是Python图像处理重要的第三方库。 Pillow库是PIL (Python image library) 库的一个扩展&#xff0c;需要通过pip工具安装。安装PIL库需要注意&#xff0c;安装…

智能网联交通加速落地,光路科技TSN技术助推车路云一体化发展

今日&#xff0c;为期两天的第二十七届高速公路信息化大会在青岛国际会展中心&#xff08;红岛馆&#xff09;圆满落幕。本次大会以“数智转型安全”为主题&#xff0c;聚焦高速公路数字化转型、车路云协同以及新一代信息技术的融合应用。会议汇聚了交通行业的专家学者、企业代…

boost.asio

as&#xff08;async&#xff09;:异步 同步io&#xff1a; reactor (非阻塞)&#xff08;需要注册一次&#xff0c;在等待消息时可以干别的事&#xff09; 阻塞io网络模型 接口&#xff1a;read\accept\connect\write 接口返回时&#xff0c;io完成 异步…

当贝AI知识库评测 AI如何让知识检索快人一步

近日,国内领先的人工智能服务商当贝AI正式推出“个人知识库”功能,这一创新性工具迅速引发行业关注。在信息爆炸的时代,如何高效管理个人知识资产、快速获取精准答案成为用户的核心需求。当贝AI通过将“闭卷考试”变为“开卷考试”的独特设计,为用户打造了一个高度个性化的智能…

深度解读:智能体2.0 AI Agent多推演进

AI Agent即AI 代理&#xff0c;长期以来&#xff0c;研究人员一直在追求更完美的AI&#xff0c;可以与人类相当、甚至是超越人类。在1950年代&#xff0c;AIan Turing就将“智能”的概念扩展到了人工实体&#xff0c;并提出了著名的图灵测试。这些人工智能实体就被称为——Agen…

Golang 的 GMP 调度机制常见问题及解答

文章目录 Golang GMP 调度模型详解常见问题基础概念1. GMP 各组件的作用是什么&#xff1f;2. 为什么 Go 需要自己的调度器&#xff1f;3. GOMAXPROCS 的作用是什么&#xff1f; 调度流程4. Goroutine 如何被调度到 M 上执行&#xff1f;5. 系统调用会阻塞整个线程吗&#xff1…

项目-苍穹外卖(十五) Apache ECharts+数据统计

一、介绍 二、营业额统计 需求分析和设计&#xff1a; Controller: Service: /*** 营业额统计* param begindate* param enddate* return* */Overridepublic TurnoverReportVO turnoverStatistics(LocalDate begindate, LocalDate enddate) {//创建时间集合List<LocalDate&…

Spring Data审计利器:@LastModifiedDate详解(依赖关系补充篇)!!!

&#x1f552; Spring Data审计利器&#xff1a;LastModifiedDate详解&#x1f525;&#xff08;依赖关系补充篇&#xff09; &#x1f50c; 核心依赖解析 使用LastModifiedDate必须知道的依赖关系 #mermaid-svg-qm1OUa9Era9ktbeK {font-family:"trebuchet ms",verd…

Tweak Power:全方位电脑系统优化的高效工具

Tweak Power&#xff08;系统&#xff09; Tweak Power是一款功能强大的系统优化工具&#xff0c;专为提升Windows电脑的性能和稳定性而设计。它提供了全面的清理、优化和调整选项&#xff0c;帮助用户轻松管理系统资源、提高运行速度、延长设备寿命。 快速扫描并清理系统垃圾…