微人事代码精读

前置知识:在IDEA上配置本地Sonar服务

这边简单说明一下如何操作:

Sonar官网下载

下载时候要注意Sonar版本要与jdk相匹配,使用java -version查看自己的jdk版本(我这边是jdk17)所以我选择10.4.1版本的

下载解压后在目录内找到\bin\windows-x86-64里面有一个StartSonar.bat启动,没有报错后,在浏览器访问127.0.0.1:9000 进入管理(默认账号密码都是admin),并创建一个本地Local项目,跟着默认按就行了,这里就不演示了

打开idea项目,下载SonarLint插件,下载完要重启idea

之后打开setting/tools/sonarlint,配置项目(点击那个+号),只会这里的url就填你本地的访问路径

127.0.0.1:9000

之后创建token

选择允许

之后在项目根目录下面的pom文件放入下面这段代码(注意账号密码用自己的)

<profiles>
        <profile>
            <id>sonar</id>
            <activation>
                <activeByDefault>true</activeByDefault>
            </activation>
            <properties>
                <sonar.host.url>http://localhost:9000</sonar.host.url>
            </properties>
        </profile>
    </profiles>
    <properties>
        <sonar.login>admin</sonar.login>
        <sonar.password>admin</sonar.password>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <executable>true</executable>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.sonarsource.scanner.maven</groupId>
                <artifactId>sonar-maven-plugin</artifactId>
                <version>3.4.0.905</version>
            </plugin>
        </plugins>
    </build>

添加完后重新加载pom

在右侧maven管理可以看到这个,允许即可生成

可能会需要你输入key,这个key是在Sonar上创建项目的时候输入的

1.使用SonarQube+人工检查,进行代码质量分析,从程序的语法、结构、接口等方面进行代码审查,并对代码风格进行分析。)

我们使用了SonarQube对微人事代码进行分析:

密码不应以纯文本形式存储:

代码:

简单修改:

个人感悟:事实上这个项目是有在 hrService 的认证逻辑中完成的密文保存逻辑的,不过这个插件只能静态分析,所以可以无法得出合适结论

2.构造函数问题

错误示例代码:

如何修正:

个人感悟:在原本的代码中,使用@Autowired注解直接注入了javaMailSender,。这种方式存在潜在的风险,因为在类初始化的时候,Spring框架可能还没有完成对依赖的注入,导致javaMailSender为null,从而在一些访问这个javaMailSender时抛出NullPointerException。

而修改后的代码中,通过构造函数注入javaMailSender,可以避免了潜在的NullPointerException风险。

Bean机制问题

错误示例代码:

修改后代码

个人感悟:通过在@Bean方法参数中注入DataSource依赖,使得依赖在方法内部被传递给MyService,更符合Spring的依赖注入机制,确保这个cachingConncetionFactory这个只能在这个bean里面被访问到

Switch问题

代码示例:

个人感悟:一般使用switch应该设置一个default来处理未考虑的情况

Java通配符问题

代码示例:

个人感悟:这个是Sonar给出的建议,一般来说我们使用这个T通配符是为了降低编码难度,但同时也会带来一定的风险,所以原则上不建议使用

Tip:在 Java 中,泛型类型的类型参数默认是不变的,因为它们可能出现在输入和输出位置的 同时。一个典型的例子是 接口。如果不是一成不变的,我们可以构造无效键入的情况。T get()add(T element)java.util.ListListT

在类型参数仅出现在一个位置的情况下,可以使用通配符来实现协方差或逆方差:

  • <? extends Foo>协方差(输入位置)
  • <? super Foo>对于逆方差(输出位置)

方法复杂度过高问题

代码示例:

个人感悟:这个方法的实现嵌套了三个for循环,时间复杂度飙升,使得整个项目的运行效率收到很大影响

代码规范问题:

个人感悟:有一部分的类命名不遵守驼峰,命名风格不统一

2.代码精读

RabbitConfig配置类:这段代码配置了 RabbitMQ 的基本组件,并在消息发送成功或失败时做出相应的处理,同时定义了消息队列、交换机和它们之间的绑定关系,确保消息能够正确地发送和路由到指定的队列中。这样的配置可以使系统能够与 RabbitMQ 进行交互,并实现消息的可靠投递和处理

1. rabbitTemplate

//通过该方法创建并配置了 RabbitTemplate 对象,用于在应用程序中与 RabbitMQ 进行交互。

@Bean

RabbitTemplate rabbitTemplate(CachingConnectionFactory cachingConnectionFactory) {

    RabbitTemplate rabbitTemplate = new RabbitTemplate(cachingConnectionFactory);

    // RabbitTemplate 中设置了消息发送成功的确认回调和消息发送失败的返回回调,用于处理消息发送结果。

    rabbitTemplate.setConfirmCallback((data, ack, cause) -> {

        String msgId = data.getId();

        if (ack) {

            logger.info(msgId + ":消息发送成功");

            mailSendLogService.updateMailSendLogStatus(msgId, 1);//修改数据库中的记录,消息投递成功

        } else {

            logger.info(msgId + ":消息发送失败");

        }

    });

    rabbitTemplate.setReturnCallback((msg, repCode, repText, exchange, routingkey) -> {

        //当消息发送失败时,会打印日志信息。

        logger.info("消息发送失败");

    });

    return rabbitTemplate;

}

2. mailQueue

//通过该方法创建了一个消息队列对象,用于接收发送的邮件消息。

@Bean

Queue mailQueue() {

    return new Queue(MailConstants.MAIL_QUEUE_NAME, true);

}
3. mailExchange
//用于指定邮件消息的路由规则
@Bean

DirectExchange mailExchange() {

    return new DirectExchange(MailConstants.MAIL_EXCHANGE_NAME, true, false);

}

4. mailBinding

//将消息队列和交换机绑定在一起,并指定了路由键
@Bean

Binding mailBinding() {

    return BindingBuilder.bind(mailQueue()).to(mailExchange()).with(MailConstants.MAIL_ROUTING_KEY_NAME);

}

SecurityConfig安全配置(登录逻辑):此类用于配置安全相关的内容,并且实现了对用户登录认证、权限控制、会话管理等安全功能的配置,并且通过自定义的方式实现了一些特定的处理逻辑。
1. passwordEncoder
//加密密码

@Bean

PasswordEncoder passwordEncoder() {

    return new BCryptPasswordEncoder();

}
2. configure1
//指定了认证逻辑,这里使用了 HrService hrService 来处理用户认证

@Override

protected void configure(AuthenticationManagerBuilder auth) throws Exception {

    auth.userDetailsService(hrService);

}
3. configure2

//配置了不需要经过安全过滤的静态资源路径

@Override

public void configure(WebSecurity web) throws Exception {

    web.ignoring().antMatchers("/css/**", "/js/**", "/index.html", "/img/**", "/fonts/**", "/favicon.ico", "/verifyCode");

}
4. loginFilter

//登录成功和登录失败时的处理方式,并配置了并发会话控制策略

@Bean

LoginFilter loginFilter() throws Exception {

    LoginFilter loginFilter = new LoginFilter();

    loginFilter.setAuthenticationSuccessHandler((request, response, authentication) -> {

                response.setContentType("application/json;charset=utf-8");

                PrintWriter out = response.getWriter();

                Hr hr = (Hr) authentication.getPrincipal();

                hr.setPassword(null);

                RespBean ok = RespBean.ok("登录成功!", hr);

                String s = new ObjectMapper().writeValueAsString(ok);

                out.write(s);

                out.flush();

                out.close();

            }

    );

    loginFilter.setAuthenticationFailureHandler((request, response, exception) -> {

                response.setContentType("application/json;charset=utf-8");

                PrintWriter out = response.getWriter();

                RespBean respBean = RespBean.error(exception.getMessage());

                if (exception instanceof LockedException) {

                    respBean.setMsg("账户被锁定,请联系管理员!");

                } else if (exception instanceof CredentialsExpiredException) {

                    respBean.setMsg("密码过期,请联系管理员!");

                } else if (exception instanceof AccountExpiredException) {

                    respBean.setMsg("账户过期,请联系管理员!");

                } else if (exception instanceof DisabledException) {

                    respBean.setMsg("账户被禁用,请联系管理员!");

                } else if (exception instanceof BadCredentialsException) {

                    respBean.setMsg("用户名或者密码输入错误,请重新输入!");

                }

                out.write(new ObjectMapper().writeValueAsString(respBean));

                out.flush();

                out.close();

            }

    );

    loginFilter.setAuthenticationManager(authenticationManagerBean());

    loginFilter.setFilterProcessesUrl("/doLogin");

    ConcurrentSessionControlAuthenticationStrategy sessionStrategy = new ConcurrentSessionControlAuthenticationStrategy(sessionRegistry());

    sessionStrategy.setMaximumSessions(1);

    loginFilter.setSessionAuthenticationStrategy(sessionStrategy);

    return loginFilter;

}

5. sessionRegistry

//管理会话信息

@Bean

SessionRegistryImpl sessionRegistry() {

    return new SessionRegistryImpl();

}

6. configure3

//配置了请求的授权规则、退出登录处理、异常处理等

@Override

protected void configure(HttpSecurity http) throws Exception {

    http.authorizeRequests()

            .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {

                @Override

                public <O extends FilterSecurityInterceptor> O postProcess(O object) {

                    object.setAccessDecisionManager(customUrlDecisionManager);

                    object.setSecurityMetadataSource(customFilterInvocationSecurityMetadataSource);

                    return object;

                }

            })

            .and()

            .logout()

            .logoutSuccessHandler((req, resp, authentication) -> {

                        resp.setContentType("application/json;charset=utf-8");

                        PrintWriter out = resp.getWriter();

                        out.write(new ObjectMapper().writeValueAsString(RespBean.ok("注销成功!")));

                        out.flush();

                        out.close();

                    }

            )

            .permitAll()

            .and()

            .csrf().disable().exceptionHandling()

            //没有认证时,在这里处理结果,不要重定向

            .authenticationEntryPoint((req, resp, authException) -> {

                        resp.setContentType("application/json;charset=utf-8");

                        resp.setStatus(401);

                        PrintWriter out = resp.getWriter();

                        RespBean respBean = RespBean.error("访问失败!");

                        if (authException instanceof InsufficientAuthenticationException) {

                            respBean.setMsg("请求失败,请联系管理员!");

                        }

                        out.write(new ObjectMapper().writeValueAsString(respBean));

                        out.flush();

                        out.close();

                    }

            );

    http.addFilterAt(new ConcurrentSessionFilter(sessionRegistry(), event -> {

        HttpServletResponse resp = event.getResponse();

        resp.setContentType("application/json;charset=utf-8");

        resp.setStatus(401);

        PrintWriter out = resp.getWriter();

        out.write(new ObjectMapper().writeValueAsString(RespBean.error("您已在另一台设备登录,本次登录已下线!")));

        out.flush();

        out.close();

    }), ConcurrentSessionFilter.class);

    http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);

}
MailReceiver邮件接收类:实现了一个 RabbitMQ 消息监听器,用于接收消息并发送邮件。在处理消息时,会对消息进行去重判断,发送邮件并记录处理日志,确保消息的可靠处理和及时通知
1.handler
//这个注解指定了监听的消息队列为 MailConstants.MAIL_QUEUE_NAME

/*

当有消息到达时,会调用该方法进行处理

* message 代表接收到的消息内容

* channel 用于消息确认

* */

@RabbitListener(queues = MailConstants.MAIL_QUEUE_NAME)

public void handler(Message message, Channel channel) throws IOException {

    Employee employee = (Employee) message.getPayload();

    MessageHeaders headers = message.getHeaders();

    Long tag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG);

    String msgId = (String) headers.get("spring_returned_message_correlation");

    if (redisTemplate.opsForHash().entries("mail_log").containsKey(msgId)) {

        //redis 中包含该 key,说明该消息已经被消费过

        logger.info(msgId + ":消息已经被消费");

        channel.basicAck(tag, false);//确认消息已消费

        return;

    }

    //收到消息,发送邮件

    MimeMessage msg = javaMailSender.createMimeMessage();

    MimeMessageHelper helper = new MimeMessageHelper(msg);

    try {

        helper.setTo(employee.getEmail());

        helper.setFrom(mailProperties.getUsername());

        helper.setSubject("入职欢迎");

        helper.setSentDate(new Date());

        Context context = new Context();

        context.setVariable("name", employee.getName());

        context.setVariable("posName", employee.getPosition().getName());

        context.setVariable("joblevelName", employee.getJobLevel().getName());

        context.setVariable("departmentName", employee.getDepartment().getName());

        String mail = templateEngine.process("mail", context);

        helper.setText(mail, true);

        javaMailSender.send(msg);

        redisTemplate.opsForHash().put("mail_log", msgId, "javaboy");

        channel.basicAck(tag, false);

        logger.info(msgId + ":邮件发送成功");

    } catch (MessagingException e) {

        channel.basicNack(tag, false, true);

        e.printStackTrace();

        logger.error("邮件发送失败:" + e.getMessage());

    }

}
LoginFilter登录过滤器 :实现了一个自定义的登录过滤器,用于处理用户登录认证请求。它支持处理 JSON 格式的登录请求,并在认证成功后注册新的会话,同时也提供了验证码的验证功能
//该过滤器用于处理用户登录认证的请求,并包含了一些自定义的逻辑

public class LoginFilter extends UsernamePasswordAuthenticationFilter {

    @Autowired

    SessionRegistry sessionRegistry;

    @Override

    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {

        //判断请求方法是否为 POST,如果不是则抛出异常

        if (!request.getMethod().equals("POST")) {

            throw new AuthenticationServiceException(

                    "Authentication method not supported: " + request.getMethod());

        }

        String verify_code = (String) request.getSession().getAttribute("verify_code");

        //根据请求的 Content-Type 判断请求类型,如果是 JSON 格式的登录请求,则从请求体中读取用户名、密码和验证码,然后进行验证码校验

        if (request.getContentType().contains(MediaType.APPLICATION_JSON_VALUE) || request.getContentType().contains(MediaType.APPLICATION_JSON_UTF8_VALUE)) {

            Map<String, String> loginData = new HashMap<>();

            try {

                loginData = new ObjectMapper().readValue(request.getInputStream(), Map.class);

            } catch (IOException e) {

            }finally {

                String code = loginData.get("code");

                checkCode(response, code, verify_code);

            }

            String username = loginData.get(getUsernameParameter());

            String password = loginData.get(getPasswordParameter());

            if (username == null) {

                username = "";

            }

            if (password == null) {

                password = "";

            }

            username = username.trim();

            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(

                    username, password);

            setDetails(request, authRequest);

            Hr principal = new Hr();

            principal.setUsername(username);

            sessionRegistry.registerNewSession(request.getSession(true).getId(), principal);

            //调用 getAuthenticationManager().authenticate(authRequest) 方法进行认证。

            return this.getAuthenticationManager().authenticate(authRequest);

        } else {

            checkCode(response, request.getParameter("code"), verify_code);

            return super.attemptAuthentication(request, response);

        }

    }

    //验证用户输入的验证码是否正确

    public void checkCode(HttpServletResponse resp, String code, String verify_code) {

        if (code == null || verify_code == null || "".equals(code) || !verify_code.toLowerCase().equals(code.toLowerCase())) {

            //验证码不正确

            throw new AuthenticationServiceException("验证码不正确");

        }

    }

}
 
CustomFilterInvocationSecurityMetadataSource 权限过滤:自定义安全元数据源用于动态地根据请求的 URL 获取所需的安全配置属性,以便进行访问控制和权限验证。这样可以灵活地根据菜单和角色配置来控制系统的访问权限
1. getAttributes
//匹配请求的 URL 和菜单的 URL。如果匹配成功,则将该菜单所需的角色转换为 ConfigAttribute 集合返回。

@Override

public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {

    String requestUrl = ((FilterInvocation) object).getRequestUrl();

    List<Menu> menus = menuService.getAllMenusWithRole();

    for (Menu menu : menus) {

        if (antPathMatcher.match(menu.getUrl(), requestUrl)) {

            List<Role> roles = menu.getRoles();

            String[] str = new String[roles.size()];

            for (int i = 0; i < roles.size(); i++) {

                str[i] = roles.get(i).getName();

            }

            //匹配请求的 URL 和菜单的 URL。如果匹配成功,则将该菜单所需的角色转换为 ConfigAttribute 集合返回。

            return SecurityConfig.createList(str);

        }

    }

    //如果未匹配到任何菜单,或者菜单列表为空,将返回一个默认的 ROLE_LOGIN,表示需要登录才能访问。

    return SecurityConfig.createList("ROLE_LOGIN");

}

2. getAllConfigAttributes

//返回了所有定义好的安全配置属性

@Override

public Collection<ConfigAttribute> getAllConfigAttributes() {

    return null;

}

3. supports

//指示了该安全元数据源是否支持给定的安全对象类型,true表示支持所有类型的安全对象

@Override

public boolean supports(Class<?> clazz) {

    return true;

}
 
Router.js 路由分配:

export default new Router({
   
// 定义路由表
    routes: [
        {
            path:
'/',
            name:
'Login',
            component: Login,
            hidden: true
        }, {
            path:
'/home',
            name:
'Home',
            component: Home,
            hidden: true,
            meta: {
                roles: [
'admin', 'user'] // 设置访问该路由需要的角色为 admin user
            },
            children: [
                {
                    path:
'/chat',
                    name:
'在线聊天',
                    component: FriendChat,
                    hidden: true
                }, {
                    path:
'/hrinfo',
                    name:
'个人中心',
                    component: HrInfo,
                    hidden: true
//
                }
            ]
        }, {
            path:
'*', // 匹配任意路径
            redirect: '/home' // 重定向到 /home 路由
        }
    ]
})

Api.js 请求api重新封装:

// axios 响应拦截器
axios.interceptors.response.use(success => {
    if (success.status && success.status ==
200 && success.data.status == 500) {
        Message.error({message: success.data.msg})
        return;
    }
    if (success.data.msg) {
        Message.success({message: success.data.msg})
    }
    return success.data;
}, error => {
   
//服务器错误码处理
    if (error.response.status == 504 || error.response.status == 404) {
        Message.error({message:
'服务器被吃了( ╯□╰ )'})
    } else if (error.response.status ==
403) {
        Message.error({message:
'权限不足,请联系管理员'})
    } else if (error.response.status ==
401) {
        mymessage.error({message: error.response.data.msg ? error.response.data.msg :
'尚未登录,请登录'})
        router.replace(
'/');
    } else {
        if (error.response.data.msg) {
            Message.error({message: error.response.data.msg})
        } else {
            Message.error({message:
'未知错误!'})
        }
    }
    return;
})

let base =
'';

// 发送键值对格式的 POST 请求
export const postKeyValueRequest = (url, params) => {
    return axios({
        method:
'post',
        url:
`${base}${url}`,
        data: params,
        transformRequest: [function (data) {
            let ret =
'';
            for (let i in data) {
                ret +=
encodeURIComponent(i) + '=' + encodeURIComponent(data[i]) + '&'
            }
            return ret;
        }],
        headers: {
           
'Content-Type': 'application/x-www-form-urlencoded'
        }
    });
}

// 发送 POST 请求
export const postRequest = (url, params) => {
    return axios({
        method:
'post',
        url:
`${base}${url}`,
        data: params
    })
}

// 发送 PUT 请求
export const putRequest = (url, params) => {
    return axios({
        method:
'put',
        url:
`${base}${url}`,
        data: params
    })
}

// 发送 GET 请求
export const getRequest = (url, params) => {
    return axios({
        method:
'get',
        url:
`${base}${url}`,
        params: params
    })
}

// 发送 DELETE 请求
export const deleteRequest = (url, params) => {
    return axios({
        method:
'delete',
        url:
`${base}${url}`,
        params: params
    })
}

Login.vue 登录基本页面ui设计:
  1. template模板
其中<el-***>标签是来自于Element-ui前端组件提供的标签,通过这个标签来实现一个基本的模板

<template>
    <div>
        <el-form
                :rules=
"rules"
                ref="loginForm"
                v-loading="loading"
                element-loading-text="正在登录..."
                element-loading-spinner="el-icon-loading"
                element-loading-background="rgba(0, 0, 0, 0.8)"
                :model="loginForm"
                class="loginContainer">
            <h3 class=
"loginTitle">系统登录</h3>
            <el-form-item prop=
"username">
                <el-
input size="normal" type="text" v-model="loginForm.username" auto-complete="off"
                          placeholder="请输入用户名"></el-input>
            </el-form-item>
            <el-form-item prop=
"password">
                <el-
input size="normal" type="password" v-model="loginForm.password" auto-complete="off"
                          placeholder="请输入密码"></el-input>
            </el-form-item>
            <el-form-item prop=
"code">
                <el-
input size="normal" type="text" v-model="loginForm.code" auto-complete="off"
                          placeholder="点击图片更换验证码" @keydown.enter.native="submitLogin" style="width: 250px"></el-input>
                <img :src=
"vcUrl" @click="updateVerifyCode" alt="" style="cursor: pointer">
            </el-form-item>
            <el-checkbox size=
"normal" class="loginRemember" v-model="checked"></el-checkbox>
            <el-button size=
"normal" type="primary" style="width: 100%;" @click="submitLogin">登录</el-button>
        </el-form>
    </div>
</template>

 
 
  1. script脚本
通过这个script定义了表单验证规则。在 data 方法中定义了表单的初始数据,包括 loading 状态、验证码的 URL、登录表单的数据以及验证规则。
两个方法:
updateVerifyCode:用于更新验证码的 URL,以便刷新验证码图片。
submitLogin:用于提交登录表单的方法。在方法内部首先进行表单验证,然后通过 postRequest 方法发送登录请求。根据响应结果来更新页面状态和处理登录成功与失败的逻辑。

<script>
    export default {
        name:
"Login",
        data() {
            return {
                loading:
false,
                vcUrl:
'/verifyCode?time='+new Date(),
                loginForm: {
                    username:
'admin',
                    password:
'123',
                    code:
''
                },
                checked:
true,
                rules: {
                    username: [{required:
true, message: '请输入用户名', trigger: 'blur'}],
                    password: [{required:
true, message: '请输入密码', trigger: 'blur'}],
                    code: [{required:
true, message: '请输入验证码', trigger: 'blur'}]
                }
            }
        },
        methods: {
            updateVerifyCode() {
                this.vcUrl =
'/verifyCode?time='+new Date();
            },
            submitLogin() {
                this.$refs.loginForm.validate((valid) => {
                    if (valid) {
                        this.loading =
true;
                        this.postRequest(
'/doLogin', this.loginForm).then(resp => {
                            this.loading =
false;
                            if (resp) {
                                this.$store.commit(
'INIT_CURRENTHR', resp.obj);
                               
window.sessionStorage.setItem("user", JSON.stringify(resp.obj));
                                let path = this.$route.query.redirect;
                                this.$router.replace((path ==
'/' || path == undefined) ? '/home' : path);
                            }else{
                                this.vcUrl =
'/verifyCode?time='+new Date();
                            }
                        })
                    } else {
                        return
false;
                    }
                });
            }
        }
    }
</script>

  1. style 风格
基本的css,通过id选择器选择一些标签,并通过设置一些常见的属性来美化template模板

<style>
   
.loginContainer {
        border-radius:
15px;
        background-clip: padding-box;
        margin:
180px auto;
        width:
350px;
        padding:
15px 35px 15px 35px;
        background:
#fff;
        border:
1px solid #eaeaea;
        box-shadow:
0 0 25px #cac6c6;
    }

   
.loginTitle {
        margin:
15px auto 20px auto;
        text-align: center;
        color:
#505458;
    }

   
.loginRemember {
        text-align: left;
        margin:
0px 0px 15px 0px;
    }
   
.el-form-item__content{
        display: flex;
        align-items: center;
    }
</style>

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值