苍穹外卖-黑马程序员

苍穹外卖-黑马程序员(笔记)

Day01

1.通过knife4j生成接口文档
  • 在SpringMVC配置类中加入
@Bean
public Docket docket() {
    ApiInfo apiInfo = new ApiInfoBuilder()
            .title("苍穹外卖项目接口文档")
            .version("2.0")
            .description("苍穹外卖项目接口文档")
            .build();
    Docket docket = new Docket(DocumentationType.SWAGGER_2)
            .apiInfo(apiInfo)
            .select()
            .apis(RequestHandlerSelectors.basePackage("com.sky.controller"))
            .paths(PathSelectors.any())
            .build();
    return docket;
}

    /**
     * 设置静态资源映射
     * @param registry
     */
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
    registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/");
    registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
}
2.MD5加密
  • Spring提供的工具类
// 123456 => e10adc3949ba59abbe56e057f20f883e
password = DigestUtils.md5DigestAsHex(password.getBytes());
3.Swagger常用注解
  • @Api 在类上, Controller…
  • @ApiModle 在类上, pojo
  • @ApiMoldeProprety 在属性上,描述信息
  • @ApiOperation 在方法上,描述方法

Day02

1.Mybatis分页管理
  • 先导入依赖
<pagehelper>1.3.0</pagehelper>

<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>${pagehelper}</version>
</dependency>
  • 开启分页配置,自动拦截下一条SQL加limit条件
@Override
public PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO) {
    
    // 开启分页查询,这条代码是关键
    PageHelper.startPage(employeePageQueryDTO.getPage(), employeePageQueryDTO.getPageSize());
   
    // 查询数据
    Page<Employee> page = employeeMapper.pageQuery(employeePageQueryDTO);
    return new PageResult(page.getTotal(), page);
}
2.更新数据时使用动态SQL来更新,减少代码
<update id="update">
    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="status != null">status = #{status},</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="updateTime != null">update_time = #{updateTime},</if>
        <if test="updateUser != null">update_user = #{updateUser},</if>
    </set>
    where id = #{id}
</update>
3.插入数据利用唯一约束抛异常来捕获,解决用户名唯一问题
/**
 * 插入异常
 * @param ex
 * @return
 */
@ExceptionHandler
public Result exceptionHandler(SQLIntegrityConstraintViolationException ex){
    // Duplicate entry 'ewqe1' for key 'employee.idx_us
    String msg = ex.getMessage();
    log.error("异常信息:{}", msg);
    if (msg.contains("Duplicate entry")) {
        return Result.error(msg.split(" ")[2] + "已存在!");
    } else {
        return Result.error("未知错误!");
    }

}
4.利用Lombok的@Builder注解简化代码
  • 在实体类上加@Builder即可
/**
 * 更新员工状态
 * @param status
 */
@Override
public void updateStatus(Integer status, Long id) {
    Employee employee = Employee.builder()
            .id(id)
            .status(status)
            .updateTime(LocalDateTime.now())
            .updateUser(BaseContext.getCurrentId())
            .build();

    employeeMapper.update(employee);
}

Day03

1.利用AOP自动填充公共字段
  • 创建枚举类,区分update和insert

    /**
     * 数据库操作类型
     */
    public enum OperationType {
    
        /**
         * 更新操作
         */
        UPDATE,
    
        /**
         * 插入操作
         */
        INSERT
    
    }
    
  • 利用注解来区分是否需要填充,创建注解@AutoFill

    /**
     * 用于公共字段自动填充,规定自动填充在参数第一位
     */
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface AutoFill {
        OperationType value();
    }
    
  • 利用Spring的AOP,切面编程,利用反射给对象赋值

    /**
     * 公共字段自动填充相关常量
     */
    public class AutoFillConstant {
        /**
         * 实体类中的方法名称
         */
        public static final String SET_CREATE_TIME = "setCreateTime";
        public static final String SET_UPDATE_TIME = "setUpdateTime";
        public static final String SET_CREATE_USER = "setCreateUser";
        public static final String SET_UPDATE_USER = "setUpdateUser";
    }
    
    
    /**
     * 自定义切面,用来处理公共字段填充
     */
    @Aspect
    @Component
    @Slf4j
    public class AutoFillAspect {
        /**
         * 切入点
         */
        // 第一个*是返回值,*(..)是所有方法的所有参数,并且加了AutoFill的方法
        @Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
        public void autoFillPointCut() {
        }
    
        /**
         * 自动填充公共字段
         *
         * @param joinPoint
         */
        @Before("autoFillPointCut()")
        public void autoFill(JoinPoint joinPoint) {
            log.info("自动填充公共字段...");
            // 利用Java反射,获取被拦截方法的数据库操作类型
            // 方法对象签名
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            // 获取方法上的注解
            AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class);
            // 获取操作类型
            OperationType operationType = autoFill.value();
    
            // 获取实体对象,规定取第一位
            Object[] args = joinPoint.getArgs();
            if (args == null || args.length == 0) {
                return;
            }
            Object entity = args[0];
    
            // 准备赋值的数据
            LocalDateTime now = LocalDateTime.now();
            Long currentId = BaseContext.getCurrentId();
    
            if (operationType == OperationType.INSERT) {
                // 设置创建时变量
                try {
                    // 获取对应方法
                    Method setCreateTime = entity.getClass().getMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);
                    Method setCreateUser = entity.getClass().getMethod(AutoFillConstant.SET_CREATE_USER, Long.class);
    
                    // 通过反射为对象赋值
                    setCreateTime.invoke(entity, now);
                    setCreateUser.invoke(entity, currentId);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }
    
            // 设置更新时变量
            try {
                // 获取对应方法
                Method setUpdateTime = entity.getClass().getMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
                Method setUpdateUser = entity.getClass().getMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
    
                // 通过反射为对象赋值
                setUpdateTime.invoke(entity, now);
                setUpdateUser.invoke(entity, currentId);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }
    
2.上传文件-阿里云Oss(对象存储服务器)基于SpringBoot
  • 在阿里云上注册,并申请 对象存储 OSS ,并创建好Bucket。(在Bucket概览里记录好外网访问地址例如:oss-cn-beijing.aliyuncs.com)

  • 创建AccessKey。(记录好key和密钥)

  • 导入阿里云OssMaven依赖

    在Maven项目中加入依赖项(推荐方式)

    在Maven工程中使用OSS Java SDK,只需在pom.xml中加入相应依赖即可。以3.15.1版本为例,在中加入如下内容:

    <dependency>
        <groupId>com.aliyun.oss</groupId>
        <artifactId>aliyun-sdk-oss</artifactId>
        <version>3.15.1</version>
    </dependency>
    

    如果使用的是Java 9及以上的版本,则需要添加jaxb相关依赖。添加jaxb相关依赖示例代码如下:

    <dependency>
        <groupId>javax.xml.bind</groupId>
        <artifactId>jaxb-api</artifactId>
        <version>2.3.1</version>
    </dependency>
    <dependency>
        <groupId>javax.activation</groupId>
        <artifactId>activation</artifactId>
        <version>1.1.1</version>
    </dependency>
    <!-- no more than 2.3.3-->
    <dependency>
        <groupId>org.glassfish.jaxb</groupId>
        <artifactId>jaxb-runtime</artifactId>
        <version>2.3.3</version>
    </dependency>
    
  • 为Oss创建一个配置文件类AliOssProperties,将数据交给spring管理。

    @Component
    @ConfigurationProperties(prefix = "sky.alioss")
    @Data
    public class AliOssProperties {
    
        private String endpoint;
        private String accessKeyId;
        private String accessKeySecret;
        private String bucketName;
    
    }
    
  • 在Spring application.yml中加入对应数据。

    sky:
      alioss:
        # 访问的外网地址
        endpoint: ${sky.alioss.endpoint}
        # AccessKey
        access-key-id: ${sky.alioss.access-key-id}
        # AccessSecret
        access-key-secret: ${sky.alioss.access-key-secret}
        # bucket的名字
        bucket-name: ${sky.alioss.bucket-name}
    

    application-dev.yml中根据自己实际情况更改。

    sky:
      alioss:
        endpoint: oss-cn-beijing.aliyuncs.com
        access-key-id: LTAI5tG3yF...
        access-key-secret: 6K6yYA2MM...
        bucket-name: sky-take-out...
    
  • 创建AliOssUtil工具类封装官网实例代码。

    官网实例代码。

    import com.aliyun.oss.ClientException;
    import com.aliyun.oss.OSS;
    import com.aliyun.oss.OSSClientBuilder;
    import com.aliyun.oss.OSSException;
    import java.io.ByteArrayInputStream;
    
    public class Demo {
    
        public static void main(String[] args) throws Exception {
            // Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
            String endpoint = "https://oss-cn-hangzhou.aliyuncs.com";
            // 强烈建议不要把访问凭证保存到工程代码里,否则可能导致访问凭证泄露,威胁您账号下所有资源的安全。本代码示例以从环境变量中获取访问凭证为例。运行本代码示例之前,请先配置环境变量。
            EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider();
            // 填写Bucket名称,例如examplebucket。
            String bucketName = "examplebucket";
            // 填写Object完整路径,例如exampledir/exampleobject.txt。Object完整路径中不能包含Bucket名称。
            String objectName = "exampledir/exampleobject.txt";
    
            // 创建OSSClient实例。
            OSS ossClient = new OSSClientBuilder().build(endpoint, credentialsProvider);
    
            try {
                String content = "Hello OSS";
                ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(content.getBytes()));
            } 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();
                }
            }
        }
    }
    

    封装的AliOssUtil。

    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));
                log.info("AliOss创建PutObject请求完毕!");
            } 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 AliOssConfiguration {
    
        @Bean
        // 保证只会创建一个
        @ConditionalOnMissingBean
        // 利用构造器注入aliOssProperties
        public AliOssUtil aliOssUtil(AliOssProperties aliOssProperties) {
            log.info("开始创建阿里云Oss上传工具类:{}", aliOssProperties);
            return new AliOssUtil(aliOssProperties.getEndpoint(),
                    aliOssProperties.getAccessKeyId(),
                    aliOssProperties.getAccessKeySecret(),
                    aliOssProperties.getBucketName());
        }
    }
    
  • 最后创建upload方法,利用UUID解决文件名重复问题。

    package com.sky.controller.admin;
    
    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.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 javax.annotation.Resource;
    import java.io.IOException;
    import java.util.UUID;
    
    @RestController
    @RequestMapping("/admin/common")
    @Slf4j
    @Api(tags = "通用接口")
    public class CommonController {
    
        @Resource
        private AliOssUtil aliOssUtil;
    
        /**
         * 文件上传到AliOss
         * @param file
         * @return
         */
        @PostMapping("/upload")
        @ApiOperation("文件上传")
        public Result<String> upload(MultipartFile file) {
            log.info("文件上传:{}", file);
            // 获取UUID
            String uuid = UUID.randomUUID().toString();
            // 原始文件名
            String originalFilename = file.getOriginalFilename();
            // 获取源文件后缀
            String extension = originalFilename.substring(originalFilename.lastIndexOf("."));
            // 新文件名
            String filename = uuid + extension;
            // 上传到阿里云Oss
            try {
                // 上传成功
                String filePath = aliOssUtil.upload(file.getBytes(), filename);
                return Result.success(filePath);
            } catch (IOException e) {
                log.error("文件上传失败:{}", e.toString());
                return Result.error("文件上传失败");
            }
    
        }
    }
    
    
注意事项
  • 阿里云Oss默认只能上传1MB大小的文件,可以在application.yml加入:

    spring:
      servlet:
        multipart:
          # 单个图片最大空间
          max-file-size: 2MB
          # 多个图片最大空间
          max-request-size: 10MB
    
  • 如果使用了Nginx作为代理,Nginx默认请求体大小也是最大1MB,可以在nginx.conf里加入:

    # 上传文件大小限制
    client_max_body_size 10m;
    # 配置请求体缓存区大小
    client_body_buffer_size 1m;
    
    # http server location三个地方都可以加,作用范围不同
    
    http {
    
    	#上传文件大小限制
    	client_max_body_size 10m;
    	#配置请求体缓存区大小
    	client_body_buffer_size 1m;
    
        server {
    
    		#上传文件大小限制
    		client_max_body_size 10m;
    		#配置请求体缓存区大小
    		client_body_buffer_size 1m;
    
    
            location / {
                #上传文件大小限制
    			client_max_body_size 10m;
    			#配置请求体缓存区大小
    			client_body_bu                                                                                                                                     ffer_size 1m;
            }
    
    }
    
  • 最后记得重启Nginx服务器cmd: nginx -s reload(不行就重启电脑)。

3.mybatis插入数据后返回主键id
  • 在mapper.xml中insert 上配置

    <!--返回生成的id赋值到service的dish.id中-->
    <!--useGeneratedKeys 开启使用生成的键  keyProperty  绑定到哪个属性上-->
    <insert id="insert" 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>
    

Day05

1.Redis基本数据类型
  • 字符串String, key对应一个String, key => String

    // 设置键
    set key value => set name 小明
    // 获取键
    get key	=> get name
    // 设置键和过期时间
    setex key value time => setex code 669090 400
    // 只有不存在时,设置键
    setnx key value => setnx name 小明
    
  • 哈希Hash,key对应一个HashMap, key => HashMap(field, value)

    // 将Hash表key中field字段设置为value
    hset key field value => hset class1 stu1 小明
    // 获取hash指定field字段的值
    hget key field => hget class1 stu1
    // 删除hash表中指定字段
    hdel key field => hdel class1 stu1
    // 获取hash表中所有字段
    hkeys key => hkeys key
    // 获取hash表中所有值
    hvals key => hvals key
    
  • 列表List,key对应一个LinkedList, key => LinkedList(双向列表)

    // 从左侧插入值 : 从左边开始放数据 , value2 在 value1 左边 , value3 在 value2 左边 ;
    lpush key value1 value2 value3 ...
    // 从右侧插入值 : 从右边开始放数据 , value2 在 value1 右边 , value3 在 value2 右边 ;
    rpush key value1 value2 value3 ...
    // 从左侧移除值 : 从 List 列表左侧移除一个值 , 如果所有的值都被移除 , 则 键 Key 也随之消亡 ;
    lpop key
    // 从右侧移除值 : 从 List 列表右侧移除一个值 , 如果所有的值都被移除 , 则 键 Key 也随之消亡 ;
    rpop key
    // 获取 key 列表 的长度 ;
    llen key
    // 获取指定下标索引的元素
    lindex key index
    // 根据下标获取元素 : 获取从 start 索引开始 , 到 stop 索引结束的元素值 ;
    lrange key start stop
    // 查询键对应的 List 列表值
    lrange key 0 -1
    // 在 key 列表 的 value 值 前面 / 后面 插入新值 newValue 
    linsert key before/after value newValue
    // 在 key 列表 中 删除左侧的 n 个 value 值
    lrem key n value
    // key 列表 中 将 第 index 索引位置 的 元素 替换为 value 值 
    lset key index value
    // 从 key1 列表右边取出一个值 , 放在 key2 列表的左边 ;
    rpoplpush key1 key2
    
  • 集合Set, key对应一个HashSet, key => HashSet(member)

    // 向集合添加多个值
    sadd key member1 member2 ...
    // 返回集合所有成员
    smembers key
    // 返回集合成员数
    scard key
    // 返回所有集合的交集
    sinter key1 key2 ...
    // 返回所有集合的并集
    sunion key1 key2 ...
    // 删除集合中一个或多个成员
    srem key member1 member2 ...
    
  • 有序集合ZSet, key对应一个TreeSet, key => TreeSet(score, member), 根据score排序

    // 添加一个或多个成员
    zadd key score1 member1 score2 member2 ...
    // 通过索引返回区间内成员,选择是否返回分数
    zrange key start stop [withscores]
    // 给指定成员加上增量increment
    zincrby key increment member
    // 删除集合中一个或多个成员
    zrem key member1 member2 ...
    
  • 通用命令

    // 查询所有给定模式(pattern)的key
    keys pattern
    // 检查key是否存在
    exists key
    // 返回key存储的值的类型
    type key
    // 删除key
    del key
    
2.Spring Data Redis, Java中使用redis
  • maven中导入坐标

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    
  • yml配置redis数据源

    # application.yml
    spring:
      redis:
        host: ${sky.redis.host}
        port: ${sky.redis.port}
        database: ${sky.redis.database}
        password: ${sky.redis.password}
    
    # application-dev.yml
    sky:
      redis:
        host: localhost
        port: 6379
        database: 0
    
  • 编写配置类创建RedisTemplate对象

    import lombok.extern.slf4j.Slf4j;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.data.redis.connection.RedisConnectionFactory;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.serializer.StringRedisSerializer;
    
    /**
     * 创建RedisTemplate对象
     */
    @Configuration
    @Slf4j
    public class RedisConfiguration {
    
        @Bean
        public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
            log.info("开始创建RedisTemplate:{}", redisConnectionFactory);
            RedisTemplate redisTemplate = new RedisTemplate<>();
            // 设置redis工厂连接
            redisTemplate.setConnectionFactory(redisConnectionFactory);
            // key的序列化器
            redisTemplate.setKeySerializer(new StringRedisSerializer());
            // value 的序列化器
            redisTemplate.setValueSerializer(new StringRedisSerializer());
            return redisTemplate;
        }
    }
    
  • 调用RedisTemplate操作redis

    import org.junit.jupiter.api.Test;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.data.redis.core.RedisTemplate;
    
    import javax.annotation.Resource;
    
    @SpringBootTest
    public class SpringDataRedisTest {
        @Resource
        private RedisTemplate redisTemplate;
    
        @Test
        public void testRedis(){
            redisTemplate.opsForValue().set("name", "小明");
            Object name = redisTemplate.opsForValue().get("name");
            System.out.println(name);
        }
    }
    
  • 对于key和value的序列化都为String的可以使用StringRedisTemplate,spring已经自动创建好了

    @Test
    public void testStringRedisTemplate(){
        stringRedisTemplate.opsForValue().set("key", "阿萨德");
        System.out.println(stringRedisTemplate.opsForValue().get("key"));
    }
    

Day06

1.利用用Httpclient实现客户端发送HTTP请求
  • 导入Maven坐标

    <dependency>
        <groupId>org.apache.httpcomponents</groupId>
        <artifactId>httpclient</artifactId>
        <version>4.4.1</version>
    </dependency>
    
  • 封装成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_MSE = 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_MSE)
                    .setConnectionRequestTimeout(TIMEOUT_MSE)
                    .setSocketTimeout(TIMEOUT_MSE).build();
        }
    
    }
    
    
2.小程序微信登录(后端)
  • 配置登录所需要的appid和secret,对应的jwt

    # application.yml
    sky:
      jwt:
        # jwt用户端配置
        user-secret-key: itheima
        user-ttl: 7200000
        user-token-name: authorization
      wechat:
        # 小程序ID
        appid: ${sky.wechat.appid}
        # 小程序秘钥
        secret: ${sky.wechat.secret}
        
    #######################################
    
    # application-dev.yml
    sky:
      wechat:
        # 小程序ID
        appid: wxb6****
        # 小程序秘钥
        secret: 2058e****
    
  • 创建对应的配置类,交给spring管理

    package com.sky.properties;
    
    import lombok.Data;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.stereotype.Component;
    
    @Component
    @ConfigurationProperties(prefix = "sky.wechat")
    @Data
    public class WeChatProperties {
        
        private String appid; //小程序的appid
        private String secret; //小程序的秘钥
        
    }
    
  • 实现微信登录

    package com.sky.constant;
    
    /**
     * 微信相关常量
     */
    
    public class WXConstant {
        public static final String WX_LONGIN_PATH = "https://api.weixin.qq.com/sns/jscode2session";
        public static final String APP_ID = "appid";
        public static final String SECRET = "secret";
        public static final String JS_CODE = "js_code";
        public static final String GRANT_TYPE= "grant_type";
        public static final String OPEN_ID= "openid";
    }
    
    package com.sky.service.impl;
    
    import com.alibaba.fastjson.JSON;
    import com.alibaba.fastjson.JSONObject;
    import com.sky.constant.MessageConstant;
    import com.sky.constant.WXConstant;
    import com.sky.dto.UserLoginDTO;
    import com.sky.entity.User;
    import com.sky.exception.LoginFailedException;
    import com.sky.mapper.UserMapper;
    import com.sky.properties.WeChatProperties;
    import com.sky.service.UserService;
    import com.sky.utils.HttpClientUtil;
    import org.apache.commons.lang3.StringUtils;
    import org.apache.poi.util.StringUtil;
    import org.springframework.stereotype.Service;
    
    import javax.annotation.Resource;
    import java.time.LocalDateTime;
    import java.util.HashMap;
    import java.util.Map;
    import java.util.Objects;
    
    /**
     * C端用户相关接口,服务层
     */
    @Service
    public class UserServiceImpl implements UserService {
    
        @Resource
        private WeChatProperties weChatProperties;
        @Resource
        private UserMapper userMapper;
        /**
         * 微信登录
         * @param userLoginDTO
         * @return
         */
        public User wxLogin(UserLoginDTO userLoginDTO) {
            // 调用微信接口获取用户openid
            String openid = getOpenid(userLoginDTO);
    
            // 判断openid是否合法
            if (StringUtils.isBlank(openid)) {
                // openid为空
                throw new LoginFailedException(MessageConstant.LOGIN_FAILED);
            }
    
            // 判断是否为新用户
            User user = userMapper.getByOpenId(openid);
            if (user == null) {
                // 新用户注册
                user = User.builder()
                        .openid(openid)
                        .createTime(LocalDateTime.now())
                        .build();
                // 插入并返回id
                userMapper.insert(user);
            }
            // 返回用户对象
            return user;
        }
    
        /**
         * 获取openid
         * @param userLoginDTO
         * @return
         */
        private String getOpenid(UserLoginDTO userLoginDTO) {
            // 封装请求参数
            Map<String, String> map = new HashMap<>();
            // 微信官方小程序已提供规范,去官网查看即可
            map.put(WXConstant.APP_ID, weChatProperties.getAppid());
            map.put(WXConstant.SECRET, weChatProperties.getSecret());
            map.put(WXConstant.JS_CODE, userLoginDTO.getCode());
            map.put(WXConstant.GRANT_TYPE, "authorization_code");
            // 调用HttpClient请求微信接口
            String json = HttpClientUtil.doGet(WXConstant.WX_LONGIN_PATH, map);
            // fastjson变成JSONObject对象
            JSONObject jsonObject = JSON.parseObject(json);
            String openid = jsonObject.getString(WXConstant.OPEN_ID);
            return openid;
        }
    }
    
    

Day07

1.Spring Cache整合Redis(SpringBoot)
  • 导入Maven坐标

     <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-cache</artifactId>
     </dependency>
    
  • 在配置文件中加入Redis的配置信息

    # application.yml 
    spring:  
      redis:
        host: ${sky.redis.host}
        port: ${sky.redis.port}
        database: ${sky.redis.database}
        #password: ${sky.redis.password}
        
    # application-dev.yml
    sky:
      redis:
        host: localhost
        port: 6379
        database: 7
        #password: 123456
    
  • 在SpringBoot的启动类上加上注解@EnableCaching,即可开启Spring cache

    @SpringBootApplication
    @EnableTransactionManagement //开启注解方式的事务管理
    @EnableCaching  // 开启Spring Cache
    @Slf4j
    public class SkyApplication {
        public static void main(String[] args) {
            SpringApplication.run(SkyApplication.class, args);
            log.info("server started");
        }
    }
    
  • 注解使用例子

    • @Cacheable注解
    • 调用该方法前,会去检查是否缓存中已经存在,如果有就直接返回,不调用方法。如果没有,就调用方法,然后把结果缓存起来。

    value、cacheNames:标明该缓存的的片区(两个属性一样的作用)
    key:标明该缓存的key值,该值是Spel表达式,不是普通的字符串,如果我们要手动显示指定的话,必须用小括号才可以正常使用,如下所示:
    @Cacheable(value = “category”, key = “#id”),框架为我们默认设置了一套规 则,常见的有:
    key = “#root.methodName”、 key = "#root.args[1]"等,可参考官网说明
    sync:当值为true时,相当于添加了本地锁,可以有效地解决缓存击穿问题

    public static final String DISH_PREFIX = "dish";
    
    @GetMapping("/list")
    @Cacheable(cacheNames = DISH_PREFIX, key = "#categoryId")
    @ApiOperation("根据分类id查询菜品")
    public Result<List<DishVO>> list(Long categoryId) {
        Dish dish = new Dish();
        dish.setCategoryId(categoryId);
        dish.setStatus(StatusConstant.ENABLE);//查询起售中的菜品
    
        List<DishVO> list = dishService.listWithFlavor(dish);
    
        return Result.success(list);
    }
    
    • @CacheEvict
    • 会清空指定缓存。

    1.删除指定缓存

    @CacheEvict(value = {“category”},key=“#id”)

    2.删除value下所有的缓存

    @CacheEvict(value = {“category”},allEntries = true)

    public static final String DISH_PREFIX = "dish";
    
    @PostMapping
    @CacheEvict(cacheNames = DISH_PREFIX, key = "#dishDTO.categoryId")
    @ApiOperation("新增菜品")
    public Result saveWithFlavor(@RequestBody DishDTO dishDTO) {
        dishService.saveWithFlavor(dishDTO);
        return Result.success();
    }
    
    @DeleteMapping
    @CacheEvict(cacheNames = DISH_PREFIX, allEntries = true)
    @ApiOperation("批量删除菜品")
    public Result delete(@RequestParam List<Long> ids) {
        dishService.delete(ids);
        return Result.success();
    }
    
    • @CachePut
    • 把方法的返回值直接缓存起来。

    @CachePut(value = {“category”},key=“#id”)

    public static final String SET_MEAL_PREFIX = "setmeal";
    
    @PostMapping
    @CachePut(cacheNames = SET_MEAL_PREFIX, key = "#setmealDTO.id")
    @ApiOperation("新增套餐")
    public Result save(@RequestBody SetmealDTO setmealDTO) {
        log.info("新增套餐:{}", setmealDTO);
        setmealService.save(setmealDTO);
        return Result.success();
    }
    
  • SpringCache的使用注意事项

    @CacheEvict注解中的allEntries = true属性会将当前片区中的所有缓存数据全部清除,请谨慎使用
    @CacheEvict注解适用用于失效模式,也即更新完数据库数据后删除缓存数据
    @CachePut注解用于适用于双写模式,更新完数据库后写入到缓存中

    配置文件中spring.cache.redis.cache-null-values=true一般需要设置(null值缓存),可以有效的防止缓存穿透

Day10

1.Spring Task(定时任务)
  • 导入Maven坐标Spring context包下已有,

    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>${mybatis.spring}</version>
    </dependency>
    
  • 在启动类加上@EnableScheduling

    @SpringBootApplication
    @EnableScheduling // 开启Spring Task
    @Slf4j
    public class SkyApplication {
        public static void main(String[] args) {
            SpringApplication.run(SkyApplication.class, args);
            log.info("server started");
        }
    }
    
  • 编写定时任务类。

    @Component
    @Slf4j
    public class MyTask {
    
        // 每五秒执行一次
        @Scheduled(cron = "0/5 * * * * ?")
        public void testTask() {
            log.info("定时任务:{}", LocalDateTime.now());
        }
    }
    
    
  • cron表达式,按cron表达式来执行定时任务。

  • 字段允许值允许的特殊字符
    秒(Seconds)0~59的整数, - * / 四个字符
    分(Minutes)0~59的整数, - * / 四个字符
    小时(Hours)0~23的整数, - * / 四个字符
    日期(DayofMonth)1~31的整数(但是你需要考虑你月的天数),- * ? / L W C 八个字符
    月份(Month)1~12的整数或者 JAN-DEC, - * / 四个字符
    星期(DayofWeek)1~7的整数或者 SUN-SAT (1=SUN), - * ? / L C # 八个字符
    年(可选,留空)(Year)1970~2099, - * / 四个字符

    cron表达式详解_**星光*的博客-CSDN博客

    特殊字符含义示例
    *所有可能的值。在月域中,*表示每个月;在星期域中,*表示星期的每一天。
    ,列出枚举值。在分钟域中,5,20表示分别在5分钟和20分钟触发一次。
    -范围。在分钟域中,5-20表示从5分钟到20分钟之间每隔一分钟触发一次。
    /指定数值的增量。在分钟域中,0/15表示从第0分钟开始,每15分钟。在分钟域中3/20表示从第3分钟开始,每20分钟。
    ?不指定值,仅日期和星期域支持该字符。当日期或星期域其中之一被指定了值以后,为了避免冲突,需要将另一个域的值设为?
    L单词Last的首字母,表示最后一天,仅日期和星期域支持该字符。说明 指定L字符时,避免指定列表或者范围,否则,会导致逻辑问题。在日期域中,L表示某个月的最后一天。在星期域中,L表示一个星期的最后一天,也就是星期日(SUN)。如果在L前有具体的内容,例如,在星期域中的6L表示这个月的最后一个星期六。
    W除周末以外的有效工作日,在离指定日期的最近的有效工作日触发事件。W字符寻找最近有效工作日时不会跨过当前月份,连用字符LW时表示为指定月份的最后一个工作日。在日期域中5W,如果5日是星期六,则将在最近的工作日星期五,即4日触发。如果5日是星期天,则将在最近的工作日星期一,即6日触发;如果5日在星期一到星期五中的一天,则就在5日触发。
    #确定每个月第几个星期几,仅星期域支持该字符。在星期域中,4#2表示某月的第二个星期四。
{秒数} {分钟} {小时} {日期} {月份} {星期} {年份(可为空)}10 0 2 1 * ? *   表示在每月的1日的凌晨2点调整任务
(20 15 10 ? * MON-FRI   表示周一到周五每天上午10:15执行作业
(30 15 10 ? 6L 2002-2006   表示2002-2006年的每个月的最后一个星期五上午10:15执行
(40 0 10,14,16 * * ?   每天上午10点,下午2点,450 0/30 9-17 * * ?   朝九晚五工作时间内每半小
(60 0 12 ? * WED    表示每个星期三中午1270 0 12 * * ?   每天中午12点触
(80 15 10 ? * *    每天上午10:15触
(90 15 10 * * ?     每天上午10:15触
(100 15 10 * * ? *    每天上午10:15触
(110 15 10 * * ? 2005    2005年的每天上午10:15触
(120 * 14 * * ?     在每天下午2点到下午2:59期间的每1分钟触
(130 0/5 14 * * ?    在每天下午2点到下午2:55期间的每5分钟触
(140 0/5 14,18 * * ?     在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触
(150 0-5 14 * * ?    在每天下午2点到下午2:05期间的每1分钟触
(160 10,44 14 ? 3 WED    每年三月的星期三的下午2:102:44触发
(170 15 10 ? * MON-FRI    周一至周五的上午10:15触发
(180 15 10 15 * ?    每月15日上午10:15触发
(190 15 10 L * ?    每月最后一日的上午10:15触发
(200 15 10 ? * 6L    每月的最后一个星期五上午10:15触发
(210 15 10 ? * 6L 2002-2005   2002年至2005年的每月的最后一个星期五上午10:15触发
(220 15 10 ? * 6   #3   每月的第三个星期五上午10:15触发
2.SpringBoot + WebSocket
  • websocket可以实现全双工通信。

  • 导入Maven坐标

    <dependency>
    	<groupId>org.springframework.boot</groupId>
    	<artifactId>spring-boot-starter-websocket</artifactId>
    </dependency>
    
  • 编写配置类将webSocket交给Spring管理

    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 {
    
        @Bean
        public ServerEndpointExporter serverEndpointExporter() {
            return new ServerEndpointExporter();
        }
    
    }
    
  • WebSocket服务

    package com.sky.websocket;
    
    import org.springframework.stereotype.Component;
    import javax.websocket.OnClose;
    import javax.websocket.OnMessage;
    import javax.websocket.OnOpen;
    import javax.websocket.Session;
    import javax.websocket.server.PathParam;
    import javax.websocket.server.ServerEndpoint;
    import java.util.Collection;
    import java.util.HashMap;
    import java.util.Map;
    
    /**
     * 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();
                }
            }
        }
    
    }
    
  • 定时任务测试

    package com.sky.task;
    
    import com.sky.websocket.WebSocketServer;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.scheduling.annotation.Scheduled;
    import org.springframework.stereotype.Component;
    import java.time.LocalDateTime;
    import java.time.format.DateTimeFormatter;
    
    @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()));
        }
    }
    
  • 前端测试页面

    <!DOCTYPE HTML>
    <html>
    <head>
        <meta charset="UTF-8">
        <title>WebSocket Demo</title>
    </head>
    <body>
        <input id="text" type="text" />
        <button onclick="send()">发送消息</button>
        <button onclick="closeWebSocket()">关闭连接</button>
        <div id="message">
        </div>
    </body>
    <script type="text/javascript">
        var websocket = null;
        var clientId = Math.random().toString(36).substr(2);
    
        //判断当前浏览器是否支持WebSocket
        if('WebSocket' in window){
            //连接WebSocket节点
            websocket = new WebSocket("ws://localhost:8080/ws/"+clientId);
        }
        else{
            alert('Not support websocket')
        }
    
        //连接发生错误的回调方法
        websocket.onerror = function(){
            setMessageInnerHTML("error");
        };
    
        //连接成功建立的回调方法
        websocket.onopen = function(){
            setMessageInnerHTML("连接成功");
        }
    
        //接收到消息的回调方法
        websocket.onmessage = function(event){
            setMessageInnerHTML(event.data);
        }
    
        //连接关闭的回调方法
        websocket.onclose = function(){
            setMessageInnerHTML("close");
        }
    
        //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
        window.onbeforeunload = function(){
            websocket.close();
        }
    
        //将消息显示在网页上
        function setMessageInnerHTML(innerHTML){
            document.getElementById('message').innerHTML += innerHTML + '<br/>';
        }
    
        //发送消息
        function send(){
            var message = document.getElementById('text').value;
            websocket.send(message);
        }
    	
    	//关闭连接
        function closeWebSocket() {
            websocket.close();
        }
    </script>
    </html>
    

    …未完

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
黑马程序员苍穹外卖项目中的Nginx配置文件可以根据具体需求进行配置。根据引用\[1\]中的描述,可以通过双击nginx.exe启动Nginx,并在http://localhost/访问前端页面。这意味着Nginx的配置文件应该包含有关前端页面的相关配置。另外,根据引用\[2\]中的描述,Nginx还可以用作反向代理和负载均衡,因此配置文件还应包含有关反向代理和负载均衡的相关配置。最后,根据引用\[3\]中的描述,苍穹外卖项目还需要与第三方配送公司进行对接和管理,因此配置文件还应包含有关与第三方配送公司对接的相关配置。综上所述,黑马程序员苍穹外卖项目的Nginx配置文件应包含前端页面的相关配置、反向代理和负载均衡的相关配置以及与第三方配送公司对接的相关配置。 #### 引用[.reference_title] - *1* [黑马程序员_Java项目实战《苍穹外卖》_Day01_开发环境搭建](https://blog.csdn.net/BallerWang9/article/details/131824385)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* *3* [最适合新手的SpringBoot+SSM项目《苍穹外卖》实战—(一)项目概述](https://blog.csdn.net/qq_20185737/article/details/131575898)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值