SpringBoot自定义启动器&&邮件发送

一、starter的作用

starter 的理念:
starter 会把所有用到的依赖都给包含进来,避免了开发者自己去引入依赖所带来的麻烦。 需要注意的是
不同的 starter 是为了解决不同的依赖,所以它们内部的实现可能会有很大的差异,
例如 jpa starter Redis starter 可能实现就不一样,这是因为 starter 的本质在于 synthesize , 这是一
层在逻辑层面的抽象,也许这种理念有点类似于 Docker ,因为它们都是在做一个 包装 的操作。
starter 的实现:
虽然不同的 starter 实现起来各有差异, 但是他们基本上都会使用到两个相同的内容:
ConfigurationProperties AutoConfiguration 。 因为 Spring
Boot 坚信 约定大于配置 这一理念,所以我们使用 ConfigurationProperties 来保存我们的配置, 并且这
些配置都可以有一个默认值,即在我们没有主动覆写原始配置的情况下,默认值就会生效,这在很多情
况下是非常有用的。
除此之外, starter ConfigurationProperties 还使得所有的配置属性被聚集到一个文件中 (一般在
resources 目录下的 application.properties ),这样我们就告别了 Spring 项目中 XML 地狱。

原理图 

二、邮件的使用

1.pom依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-mail</artifactId>
</dependency>

2.yml文件

可以决定是否加载JavaMailSender

spring:
  application:
    name: SpringBoot_06
  mail:
    host: smtp.qq.com
    username: xxxxxx@qq.com
    password: xxxxxxxxxx
    properties:
      mail:
        smtp:
          auth: true
          starttls:
            enable: true
            required: true

password不是QQ密码,是邮箱的授权码

进入邮箱点击设置——》账户——》开启第二行——》生成授权码 

 3、测试类中进行测试

package com.zxy.code;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;

import javax.sql.DataSource;

@SpringBootTest

class SpringBoot06ApplicationTests {

    @Autowired
    JavaMailSender mailSender;

    @Test
    void contextLoads() {
        SimpleMailMessage message = new SimpleMailMessage();
        message.setFrom("2781494496@qq.com");
        message.setTo("zxyqyf@foxmail.com");
        message.setSubject("主题:简单邮件");
        message.setText("测试邮件内容");
        mailSender.send(message);
    }

}

三、自定义启动器

为了更加方便,我们将验证码功能变成启动器,用时直接导入就可以了

由于启动器本身是一个spring项目,这时我们新建一个模块,不需要web

1.新建项目( yzm-spring-boot-starter)

2.导入邮件的pom依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-mail</artifactId>
</dependency>

3.建立类EmailProperties,属性为yml文件的参数

package com.yzm.yzm;


import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * 邮件属性配置
 */
@AllArgsConstructor
@NoArgsConstructor
@Data
@ConfigurationProperties(prefix = "email")
public class EmailProperties {


    /**
     * SMTP的主机协议
     */
    private String host;
    /*
     发送方用户名
     */
    private String username;
    /**
     * 发送方授权码
     */
    private String password;
    /**
     * 是否启用邮箱功能
     */
    private boolean enable;

}

 

报错原因是没有被spring所管理,spring没有填充属性 

4.yml文件

email:
  host: smtp.qq.com
  username: xxxxxx@qq.com
  password: 邮箱授权码

5.创建接口

发送行为的方法,参数是接收方的邮箱

package com.yzm.yzm;

/**
 * 邮箱发送者
 */
public interface EmailSender {
//    发送邮件的方法
    String sendText(String receiver);

}

6.建立实现类 

@EnableConfigurationProperties(value = EmailProperties.class) 表示开启加载配置的属性
相当于在 EmailProperties上加上compoent

@Configuration 将实现类变成一个组件,立马被加载

package com.yzm.yzm;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSenderImpl;

import java.util.Properties;

/**
 * 邮件发送实现类
 */
@Configuration
@EnableConfigurationProperties(value = EmailProperties.class) //开始配置的属性读取
@ConditionalOnProperty(prefix = "email",name = "enable",havingValue = "true")
public class EmailSendImpl implements EmailSender{


    private  EmailProperties emailProperties;
    private  JavaMailSenderImpl mailSender;


//   1. 获得注入的信息
@Autowired
    public EmailSendImpl(EmailProperties emailProperties){

//        初始化一个邮件发送的对象
    this.emailProperties=emailProperties;
      mailSender =new JavaMailSenderImpl();
        mailSender = new JavaMailSenderImpl();
        mailSender.setHost(emailProperties.getHost());
        mailSender.setUsername(emailProperties.getUsername());
        mailSender.setPassword(emailProperties.getPassword());
        mailSender.setDefaultEncoding("Utf-8");
        Properties p = new Properties();
        p.setProperty("mail.smtp.auth", "true");
        p.setProperty("mail.smtp.starttls.enable", "true");
        p.setProperty("mail.smtp.starttls.required", "true");
        mailSender.setJavaMailProperties(p);
    }

    @Override
    public String sendText(String receiver) {
        SimpleMailMessage message = new SimpleMailMessage();
        message.setFrom(emailProperties.getUsername());
        message.setTo(receiver);
        message.setSubject("网站验证码");
        message.setText("aaaaaaaa");
        mailSender.send(message);
        return "aabb";

    }
}



@Configuration:将此类声明为组件,并且交给spring进行管理
@Slf4j:日志输出
@EnableConfigurationProperties(value = EmailProperties.class)完成对应元素组件化
@ConditionalOnProperty(prefix = "email", value = "enable", havingValue = "true")

控制邮件功能是否可用 

 测试类放入一个接收邮箱

package com.yzm.yzm;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class YzmSpringBootStarterApplicationTests {

    @Autowired
    private  EmailSender sender;

    @Test
    void contextLoads() {
//      String yzm =  sender.sendText("zxyqyf@foxmail.com");
//        System.out.println(yzm);
        sender.sendText("2781494496@qq.com");
    }

}

运行效果:

为了控制是否启用,可以在yml文件中增加一个属性 enable

email:
  enable: true
  host: smtp.qq.com
  username: xxxxxxxx@qq.com
  password: 邮箱授权码

 EmailProperties中增加一个属性

 在实现类上添加注解

@EnableConfigurationProperties(value = EmailProperties.class) //开始配置的属性读取

表示只有enable属性为true才被spring管理,如果为false会直接报错 

四、邮箱的使用

   提高邮箱使用灵活性

由于每个人的邮箱都不一样,不可能将发送和收件人的邮箱以及是否启用邮箱都固定,所以要将yml中的内容进行更改,提高邮箱使用灵活性。

  步骤:

                          1、在yzm-spring-boot-starter导入yml文件注入依赖

     <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
     </dependency>
 
 
     <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.4.1</version>
                <configuration><classifier>exec</classifier></configuration>
     </plugin>
 
 

2、在resource文件下添加文件 (如下图)

META-INF > spring.factories

文件内容:

#当前springboot的自动配置类
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.yzm.yzm.EmailSendImpl

此段代码的说明:当整个项目运行时,对应类就会随着运行

 2、将整个项目打包成jar包

 最后到本地仓库中查看有没有yzm这个文件夹(这个名字就是自己命名的)可以在pom文件中查看

<modelVersion>4.0.0</modelVersion>
<groupId>com.yzm</groupId>
<artifactId>yzm-spring-boot-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>yzm-spring-boot-starter</name>
<description>yzm-spring-boot-starter</description>
 

 打包成功。

3、到之前的项目中导入邮箱依赖(必须使用同一个本地仓库) 

<dependency>
    <groupId>com.yzm</groupId>
    <artifactId>yzm-spring-boot-starter</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

4、在测试类中加入注解

//    使用自定义的邮箱启动器
    @Autowired
    private EmailSender sender;

导入此注解时会进行报错,原因:没有配置资源文件

email:
  enable: true
  host: smtp.qq.com
  username: xxxxxx@qq.com
  password: 邮箱授权吗

 

加了基本配置文件后,还是报错(原因:没有开启邮箱,有提示说明导入依赖成功)

 导入成功

五、结合redis实现操作

 由于现在数据繁多,不可能每次都会进入到数据库拿取数据,这时候就会运用缓存来提高程序运行速度。   

1、步骤

                     1、导入操作redis的依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

   2、在applicaion.yml资源文件中加入redis资源

  redis:
    database: 0         #数据库索引
    host: 127.0.0.1     #主机位置
    port: 6379          #端口
    password:            #密码
    jedis:
      pool:
        max-active: 8   #最大连接数
        max-wait: -1    #最大阻塞等待时间(负数表示没限制)
        max-idle: 8     #最大空闲
        min-idle: 0     #最小空闲
    timeout: 10000      #连接超时时间

  3、将帮助类写入到项目中

conf--->CrossConfiguration

package com.zxy.code.conf;

import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.util.ClassUtils;

import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.time.Duration;


@Configuration
@EnableCaching //开启缓存
public class RedisConfiguration extends CachingConfigurerSupport {

    //@Bean 相当于 @Component,只不过Bean可以写在方法上面,内在跟Component是一样的
    @Bean
    @Primary
    //cachemManager缓存管理器
    CacheManager cacheManager(RedisConnectionFactory factory) {
        RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                .computePrefixWith(cacheName -> cacheName + ":-cache-:")
                /*设置缓存过期时间*/
                .entryTtl(Duration.ofHours(1))
                /*禁用缓存空值,不缓存null校验*/
                .disableCachingNullValues()
                /*设置CacheManager的值序列化方式为json序列化,可使用加入@Class属性*/
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(
                        new GenericJackson2JsonRedisSerializer()
                ));
        /*使用RedisCacheConfiguration创建RedisCacheManager*/
        RedisCacheManager manager = RedisCacheManager.builder(factory)
                .cacheDefaults(cacheConfiguration)
                .build();
        return manager;
    }

    @Bean
        //cachemManager缓存管理器
    CacheManager yzmManager(RedisConnectionFactory factory) {
        RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                .computePrefixWith(cacheName -> cacheName + ":-yzm-:")
                /*设置缓存过期时间*/
                .entryTtl(Duration.ofSeconds(60))
                /*禁用缓存空值,不缓存null校验*/
                .disableCachingNullValues()
                /*设置CacheManager的值序列化方式为json序列化,可使用加入@Class属性*/
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(
                        new GenericJackson2JsonRedisSerializer()
                ));
        /*使用RedisCacheConfiguration创建RedisCacheManager*/
        RedisCacheManager manager = RedisCacheManager.builder(factory)
                .cacheDefaults(cacheConfiguration)
                .build();
        return manager;
    }

    @Bean
    @Primary
    //redisTemplate直接用它操作redis里面的内容
    public RedisTemplate redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
        redisTemplate.setConnectionFactory(factory);
        RedisSerializer stringSerializer = new StringRedisSerializer();
        /* key序列化 */
        redisTemplate.setKeySerializer(stringSerializer);
        /* value序列化 */
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        /* Hash key序列化 */
        redisTemplate.setHashKeySerializer(stringSerializer);
        /* Hash value序列化 */
        redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

    @Bean
    @Primary
    @Override
    //keyGenerator主键生成器
    public KeyGenerator keyGenerator() {
        return (Object target, Method method, Object... params) -> {
            final int NO_PARAM_KEY = 0;
            final int NULL_PARAM_KEY = 53;
            StringBuilder key = new StringBuilder();
            /* Class.Method: */
            key.append(target.getClass().getSimpleName())
                    .append(".")
                    .append(method.getName())
                    .append(":");
            if (params.length == 0) {
                return key.append(NO_PARAM_KEY).toString();
            }
            int count = 0;
            for (Object param : params) {
                /* 参数之间用,进行分隔 */
                if (0 != count) {
                    key.append(',');
                }
                if (param == null) {
                    key.append(NULL_PARAM_KEY);
                } else if (ClassUtils.isPrimitiveArray(param.getClass())) {
                    int length = Array.getLength(param);
                    for (int i = 0; i < length; i++) {
                        key.append(Array.get(param, i));
                        key.append(',');
                    }
                } else if (ClassUtils.isPrimitiveOrWrapper(param.getClass()) || param instanceof String) {
                    key.append(param);
                } else {
                    /*JavaBean一定要重写hashCode和equals*/
                    key.append(param.hashCode());
                }
                count++;
            }
            return key.toString();
        };
    }
}
conf--->RedisConfiguration
package com.zxy.code.conf;
 
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.util.ClassUtils;
 
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.time.Duration;
 

@Configuration
@EnableCaching
public class RedisConfiguration extends CachingConfigurerSupport {
 
    @Bean
    @Primary
    CacheManager cacheManager(RedisConnectionFactory factory) {
        RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                .computePrefixWith(cacheName -> cacheName + ":-cache-:")
                /*设置缓存过期时间*/
                .entryTtl(Duration.ofHours(1))
                /*禁用缓存空值,不缓存null校验*/
                .disableCachingNullValues()
                /*设置CacheManager的值序列化方式为json序列化,可使用加入@Class属性*/
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(
                        new GenericJackson2JsonRedisSerializer()
                ));
        /*使用RedisCacheConfiguration创建RedisCacheManager*/
        RedisCacheManager manager = RedisCacheManager.builder(factory)
                .cacheDefaults(cacheConfiguration)
                .build();
        return manager;
    }
 
    @Bean
    @Primary
    public RedisTemplate redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
        redisTemplate.setConnectionFactory(factory);
        RedisSerializer stringSerializer = new StringRedisSerializer();
        /* key序列化 */
        redisTemplate.setKeySerializer(stringSerializer);
        /* value序列化 */
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        /* Hash key序列化 */
        redisTemplate.setHashKeySerializer(stringSerializer);
        /* Hash value序列化 */
        redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
 
    @Bean
    @Primary
    @Override
    public KeyGenerator keyGenerator() {
        return (Object target, Method method, Object... params) -> {
            final int NO_PARAM_KEY = 0;
            final int NULL_PARAM_KEY = 53;
            StringBuilder key = new StringBuilder();
            /* Class.Method: */
            key.append(target.getClass().getSimpleName())
                    .append(".")
                    .append(method.getName())
                    .append(":");
            if (params.length == 0) {
                return key.append(NO_PARAM_KEY).toString();
            }
            int count = 0;
            for (Object param : params) {
                /* 参数之间用,进行分隔 */
                if (0 != count) {
                    key.append(',');
                }
                if (param == null) {
                    key.append(NULL_PARAM_KEY);
                } else if (ClassUtils.isPrimitiveArray(param.getClass())) {
                    int length = Array.getLength(param);
                    for (int i = 0; i < length; i++) {
                        key.append(Array.get(param, i));
                        key.append(',');
                    }
                } else if (ClassUtils.isPrimitiveOrWrapper(param.getClass()) || param instanceof String) {
                    key.append(param);
                } else {
                    /*JavaBean一定要重写hashCode和equals*/
                    key.append(param.hashCode());
                }
                count++;
            }
            return key.toString();
        };
    }
 
}

   4、新建一个实体类和控制类

pojo:

package com.zxy.code.pojo;

import lombok.Data;
import org.springframework.stereotype.Component;

@Data
@Component
public class User {
    private String account;
}
UserController:
package com.zxy.code.controller;


import com.yzm.yzm.EmailSender;
import com.zxy.code.pojo.User;
import com.zxy.code.util.RedisUtil;
import com.zxy.code.util.Task;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.TimeUnit;

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

//    @Autowired
//    private EmailSender sender;

    @Autowired
    private Task task ;

    @GetMapping("/register")
    public String register(User user) {
        if (user.getAccount().length() < 5) {
            return "不符合规则";
        }
//        普安段邮箱验证码无效
      String yzm =  RedisUtil.StringOps.get(user.getAccount());
//       消息队列
//        异步操作
        if(yzm==null){
//            yzm = sender.sendText(user.getAccount());
//            RedisUtil.StringOps.setEx(user.getAccount(),yzm,1, TimeUnit.SECONDS);
            task.sendyzm(user.getAccount());
      return  "验证码已经发送";
        }
        return "yzm目前还是有效的";
    }

    @GetMapping("/verify")
    public String verify(User user, String yzm) {
        String code =  RedisUtil.StringOps.get(user.getAccount());
        if(code==null){
            return "有毛病";
        }
            return code.equals(yzm)?"yes":"no";
        }

    }


运行之后出现一个错误:

原因是没有打上自动注入注解

2、邮箱的使用

前言:在我们注册一个邮箱时,往往会出现注册缓慢,理想状态应该为先出现注册成功,在之后出现邮箱发送成功

 解决方法:

1、消息队列

2、异步操作

通常采取第二种方法,使用异步操作

                    步骤:

                                1、创建一个task类,专用来发送验证码

package com.zxy.code.util;
 

import com.yzm.yzm.EmailSender;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
 
import java.util.concurrent.TimeUnit;

/**
 * 存放所有的異步操作
 */
 
@Component
public class Task {
    @Autowired
    private EmailSender emailSender;
 
    @Async
    public void sendyzm(String account){
        String yzm = emailSender.sendText(account);
        RedisUtil.StringOps.setEx(account,yzm,1, TimeUnit.MINUTES);
    }
}

 注意点:

一定要注入注解,不然会报错

其中

@Component(将此类声明为bean对象交给spring进行管理)

@Autowired(将其他类注入进来)

@Async(异步注解)这三个注解尤为重要

  2、在启动类中注入开启异步注解

@EnableAsync

 3、在controller类中注入task注解,

@Autowired
private Task task;

package com.zxy.code.controller;


import com.yzm.yzm.EmailSender;
import com.zxy.code.pojo.User;
import com.zxy.code.util.RedisUtil;
import com.zxy.code.util.Task;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.TimeUnit;

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

//    @Autowired
//    private EmailSender sender;

    @Autowired
    private Task task ;

    @GetMapping("/register")
    public String register(User user) {
        if (user.getAccount().length() < 5) {
            return "不符合规则";
        }
//        普安段邮箱验证码无效
      String yzm =  RedisUtil.StringOps.get(user.getAccount());
//       消息队列
//        异步操作
        if(yzm==null){
//            yzm = sender.sendText(user.getAccount());
//            RedisUtil.StringOps.setEx(user.getAccount(),yzm,1, TimeUnit.SECONDS);
            task.sendyzm(user.getAccount());
      return  "验证码已经发送";
        }
        return "yzm目前还是有效的";
    }

    @GetMapping("/verify")
    public String verify(User user, String yzm) {
        String code =  RedisUtil.StringOps.get(user.getAccount());
        if(code==null){
            return "有毛病";
        }
            return code.equals(yzm)?"yes":"no";
        }

    }


 4、进行测试

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值