29、依赖注入实战:构建 Twitter 克隆应用

依赖注入实战:构建 Twitter 克隆应用

在软件开发中,依赖注入(Dependency Injection,DI)是一种强大的设计模式,它能够提高代码的可维护性、可测试性和可扩展性。本文将通过构建一个名为 Crosstalk 的 Twitter 克隆应用,详细介绍如何在实际项目中应用依赖注入。

1. Crosstalk 应用概述

Crosstalk 是一个简单的微博应用,允许用户在个人主页上发布短消息(即 tweets)。它具有以下特点:
- 可扩展性 :能够轻松应对大量用户同时访问。
- 高并发处理 :确保多个用户可以同时进行操作而不会出现性能问题。
- 模块化设计 :将不同功能模块分离,降低耦合度。

Crosstalk 的主要需求包括:
- 用户认证 :在用户登录时进行身份验证。
- 页面安全 :保护用户主页的安全。
- 数据持久化 :将用户发布的 tweets 存储到数据库中。

为了实现这些需求,我们将使用以下技术栈:
- Google Guice :作为依赖注入框架,负责管理对象之间的依赖关系。
- Google Sitebricks :作为 Web 应用框架,用于渲染网页。
- Hibernate :作为持久化框架,实现数据的存储和读取。
- Hypersonic(HSQL) :作为内存数据库,方便开发和测试。

2. 应用设置

在开始构建 Crosstalk 应用之前,我们需要进行一些基础设置。

2.1 项目结构

项目的基本结构如下:
- src :主源代码目录,包含所有 Java 类。
- test :测试源代码目录,用于存放 Jetty 启动器。
- web :Web 资源目录,包含 HTML 模板和 CSS 样式表。
- web/WEB-INF :Servlet 容器所需的部署描述符目录。

2.2 添加依赖库

我们需要添加一系列依赖库到项目的类路径中,具体如下表所示:

库名称 描述
guice-2.0.jar Google Guice 核心库
aopalliance.jar Guice AOP 接口
google-sitebricks.jar Google Sitebricks Web 框架
guice-servlet-2.0.jar Guice 的 Servlet 集成库
hibernate3.jar Hibernate 核心持久化框架
hibernate-annotations.jar Hibernate 注解库
ejb3-persistence.jar Hibernate 注解库
dom4j-1.6.1.jar Dom4j XML 解析器
jta.jar Java 事务 API
cglib-nodep-2.1_3.jar Hibernate 用于代理对象的 CGLib 库
antlr-2.7.5h3.jar Antlr 编译器库
commons-collections.jar Apache 集合库
commons-logging.jar Apache Commons-Logging 库
warp-persist-2.0-20090214.jar 用于集成 Hibernate 和 Guice 的库
hsqldb.jar Hypersonic 内存 SQL 数据库
servlet-api-2.5-6.1.9.jar Java Servlet API
jetty-6.1.9.jar Mort Bay Jetty
jetty-util-6.1.9.jar Jetty 工具库
jcip-annotations.jar 线程安全注解库

在 IntelliJ IDEA 中添加这些库到类路径的步骤如下:
1. 打开 Settings > Project Settings。
2. 选择主(crosstalk)模块。
3. 打开 Dependencies 标签。
4. 点击 Add。
5. 选择 Project Library 并点击 Add Jar Directory,选择所有 jar 文件所在的目录(或者使用单条目模块库选项逐个添加)。

如果不使用 IDE,可以将所有 jar 文件放在一个 lib 目录中,并在命令行中分别指定它们。

2.3 配置注入器

应用的核心配置位于 CrosstalkBootstrap 类中,它是 GuiceServletContextListener 的子类。以下是该类的代码:

public final class CrosstalkBootstrap extends GuiceServletContextListener {
    @Override
    protected Injector getInjector() {
        // 绑定所有服务依赖
        final Module services = new ServicesModule();
        // 告诉 Sitebricks 扫描这个包
        final Module sitebricks = new SitebricksModule() {
            protected void configureSitebricks() {
                scan(CrosstalkBootstrap.class.getPackage());
            }
        };
        // 将所有传入请求通过 PersistenceFilter 进行过滤
        final Module servlets = new ServletModule() {
            protected void configureServlets() {
                filter("/*").through(PersistenceFilter.class);
                install(sitebricks);
            }
        };
        // 最后,使用所有配置创建注入器
        return Guice.createInjector(services, servlets);
    }
}

这个类的主要作用是创建并注册我们自己的注入器,同时告诉 Guice 如何将服务连接在一起。

2.4 配置 web.xml

web.xml 文件中,我们需要注册 GuiceFilter CrosstalkBootstrap 监听器,代码如下:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
              http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
         version="2.5">
    <filter>
        <filter-name>guiceFilter</filter-name>
        <filter-class>com.google.inject.servlet.GuiceFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>guiceFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    <listener>
        <listener-class>com.wideplay.crosstalk.CrosstalkBootstrap</listener-class>
    </listener>
</web-app>

通过这个配置,所有传入的请求都会经过 GuiceFilter ,从而允许我们利用依赖注入的特性处理请求。

3. 配置 Google Sitebricks

Google Sitebricks 是一个简单的静态类型开发系统,用于渲染网页。我们需要对其进行一些额外的配置。

3.1 路由请求到 Google Sitebricks

CrosstalkBootstrap 类中,我们已经将所有请求通过 PersistenceFilter 进行过滤,并安装了 SitebricksModule ,代码如下:

final Module servlets = new ServletModule() {
    protected void configureServlets() {
        filter("/*").through(PersistenceFilter.class);
        install(sitebricks);
    }
};

这使得 Google Sitebricks 可以处理所有用户请求,并决定哪些请求需要处理,哪些请求可以直接传递给 Servlet 容器。

3.2 配置扫描包

我们还需要告诉 Google Sitebricks 扫描哪些包,代码如下:

final Module sitebricks = new SitebricksModule() {
    @Override
    protected void configureSitebricks() {
        scan(CrosstalkBootstrap.class.getPackage());
    }
};

通过提供 CrosstalkBootstrap 的包名,我们让 Google Sitebricks 扫描以 com.wideplay.crosstalk 开头的整个包树,查找需要服务的页面类和模板。

4. Crosstalk 的模块化和服务耦合

为了提高代码的可维护性和可扩展性,Crosstalk 采用了模块化设计。

4.1 包结构

Crosstalk 的包结构简单明了,主要分为以下几个部分:
- com.wideplay.crosstalk.web :表示层,包含所有页面类。
- com.wideplay.crosstalk.services :持久化和安全层,只暴露接口,实现类作为包私有隐藏起来。
- com.wideplay.crosstalk.tweets :领域模型包,包含数据模型类。

这种结构的好处是降低了模块之间的耦合度,我们可以在不影响其他模块的情况下修改具体的实现细节。例如,如果我们想将数据存储方式从 Hibernate 和 HSQL 改为集群数据存储,只需要修改 services 包中的实现类即可。

4.2 服务耦合

模块之间的协作通过公开暴露的接口和明确定义的契约进行,避免了意外的服务耦合。这种设计使得代码更加灵活和易于维护。

5. 表示层

Crosstalk 的核心功能是允许用户在个人主页上发布 tweets,因此表示层的设计至关重要。

5.1 HomePage 类

我们创建了一个名为 HomePage 的类来处理用户主页的逻辑,代码如下:

package com.wideplay.crosstalk.web;

@At("/home") @Select("action") @RequestScoped
public class HomePage {
    // 用户上下文,跟踪当前用户
    private User user;
    // 页面状态变量
    private List<Tweet> tweets;
    private Tweet newTweet = new Tweet();
    // 服务依赖
    private final TweetManager tweetManager;

    @Inject
    public HomePage(TweetManager tweetManager, User user) {
        this.tweetManager = tweetManager;
        this.user = user;
    }

    @Get("logout")
    public String logout() {
        user.logout();
        return "/login?message=Bye.";
    }

    @Get
    public String get() {
        // 加载当前用户的 tweets
        this.tweets = tweetManager.tweetsFor(user.getUsername());
        // 留在当前页面
        return null;
    }

    @Post
    public String post() {
        newTweet.setAuthor(user.getUsername());
        // 将新 tweet 添加到数据存储中
        tweetManager.addTweet(newTweet);
        // 使用 GET 请求重定向回此页面
        return "/home";
    }

    // getters/setters...
    public String getUser() {
        return user.getUsername();
    }

    public List<Tweet> getTweets() {
        return tweets;
    }

    public Tweet getNewTweet() {
        return newTweet;
    }
}

该类使用了一些注解来实现不同的功能:
- @At("/home") :告诉 Google Sitebricks 在访问 /home 路径时提供该页面。
- @RequestScoped :告诉 Guice 为每个传入请求创建一个新的 HomePage 实例,确保用户数据的独立性。
- @Get @Post :分别处理 GET 和 POST 请求。

5.2 HomePage 模板

HomePage 的 HTML 模板是一个简单的文件,它通过绑定数据到 HomePage 类的请求作用域实例,实现了动态文本的显示。以下是模板代码:

<html>
<head>
    <title>Tweets</title>
    <link rel="stylesheet" href="/crosstalk.css"/>
</head>
<body>
    <h2>Tweets by ${user}</h2>
    <div class="box">
        <div class="box-content">
            <div>What are you doing right now?</div>
            <form action="/home" method="post">
                <textarea name="newTweet.text" rows="5" cols="60" />
                <input type="submit" value="update"/>
            </form>
        </div>
    </div>
    @Repeat(items=tweets, var="tweet")
    <div class="box">
        <div class="box-content">
            ${tweet.text} (${tweet.createdOn})
        </div>
    </div>
    <a href="http://manning.com/prasanna">Help</a> |
    <a href="?action=logout">Sign out</a>
</body>
</html>

模板中的一些关键部分解释如下:
- <h2>Tweets by ${user}</h2> :通过调用 HomePage.getUser() 方法动态显示当前用户的用户名。
- @Repeat(items=tweets, var="tweet") :告诉 Google Sitebricks 重复渲染 <div class="box"> 标签,显示用户发布的所有 tweets。
- 表单部分允许用户输入新的 tweet 并提交,Google Sitebricks 会将表单数据绑定到 HomePage 类的 newTweet 属性上。

6. 总结

通过构建 Crosstalk 应用,我们深入了解了如何在实际项目中应用依赖注入。依赖注入使得代码更加模块化、可维护和可测试,同时提高了代码的可扩展性。在开发过程中,我们使用 Google Guice 管理对象之间的依赖关系,使用 Google Sitebricks 渲染网页,使用 Hibernate 实现数据的持久化。通过合理的包结构和模块化设计,降低了模块之间的耦合度,使得代码更加灵活和易于维护。希望本文能够帮助你更好地理解和应用依赖注入。

下面是 Crosstalk 应用的主要流程 mermaid 流程图:

graph LR
    A[用户请求] --> B[GuiceFilter]
    B --> C{请求类型}
    C -->|静态资源| D[Servlet 容器处理]
    C -->|应用请求| E[Google Sitebricks 处理]
    E --> F[调用 HomePage 类]
    F -->|GET 请求| G[加载用户 tweets]
    F -->|POST 请求| H[添加新 tweet 到数据存储]
    G --> I[渲染 HomePage 模板]
    H --> I
    I --> J[返回页面给用户]

通过以上步骤,我们完成了一个简单的 Twitter 克隆应用的构建,并且充分利用了依赖注入的优势。在实际开发中,我们可以根据需求进一步扩展和优化这个应用。

依赖注入实战:构建 Twitter 克隆应用

7. 持久化层

持久化层负责将数据存储到数据库中,并在需要时从数据库中读取数据。在 Crosstalk 应用中,我们使用 Hibernate 作为持久化框架,结合 Hypersonic(HSQL)内存数据库。

7.1 配置 Hibernate

为了使用 Hibernate,我们需要进行一些配置。首先,确保在项目中添加了 Hibernate 相关的依赖库,如 hibernate3.jar hibernate-annotations.jar 等。

然后,创建 Hibernate 的配置文件 hibernate.cfg.xml ,示例如下:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-configuration PUBLIC
        "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
        "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
    <session-factory>
        <!-- 数据库连接配置 -->
        <property name="hibernate.connection.driver_class">org.hsqldb.jdbcDriver</property>
        <property name="hibernate.connection.url">jdbc:hsqldb:mem:crosstalk</property>
        <property name="hibernate.connection.username">sa</property>
        <property name="hibernate.connection.password"></property>

        <!-- 数据库方言 -->
        <property name="hibernate.dialect">org.hibernate.dialect.HSQLDialect</property>

        <!-- 自动创建表 -->
        <property name="hibernate.hbm2ddl.auto">update</property>

        <!-- 显示 SQL 语句 -->
        <property name="hibernate.show_sql">true</property>

        <!-- 映射实体类 -->
        <mapping class="com.wideplay.crosstalk.tweets.Tweet"/>
    </session-factory>
</hibernate-configuration>

这个配置文件指定了数据库连接信息、数据库方言、自动创建表的策略以及需要映射的实体类。

7.2 实体类定义

com.wideplay.crosstalk.tweets 包中,定义 Tweet 实体类,示例如下:

package com.wideplay.crosstalk.tweets;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import java.util.Date;

@Entity
public class Tweet {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String author;
    private String text;
    private Date createdOn;

    // 构造函数、getters 和 setters
    public Tweet() {
    }

    public Tweet(String author, String text) {
        this.author = author;
        this.text = text;
        this.createdOn = new Date();
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getAuthor() {
        return author;
    }

    public void setAuthor(String author) {
        this.author = author;
    }

    public String getText() {
        return text;
    }

    public void setText(String text) {
        this.text = text;
    }

    public Date getCreatedOn() {
        return createdOn;
    }

    public void setCreatedOn(Date createdOn) {
        this.createdOn = createdOn;
    }
}

使用 JPA 注解将 Tweet 类映射到数据库表中。

7.3 数据访问对象(DAO)

为了方便对 Tweet 实体进行数据库操作,我们创建一个数据访问对象(DAO)类。示例如下:

package com.wideplay.crosstalk.services;

import com.wideplay.crosstalk.tweets.Tweet;
import javax.inject.Singleton;
import java.util.List;

@Singleton
public interface TweetManager {
    List<Tweet> tweetsFor(String username);
    void addTweet(Tweet tweet);
}
package com.wideplay.crosstalk.services.impl;

import com.wideplay.crosstalk.tweets.Tweet;
import com.wideplay.crosstalk.services.TweetManager;
import javax.inject.Singleton;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.util.List;

@Singleton
public class TweetManagerImpl implements TweetManager {
    @PersistenceContext
    private EntityManager entityManager;

    @Override
    public List<Tweet> tweetsFor(String username) {
        return entityManager.createQuery("SELECT t FROM Tweet t WHERE t.author = :username", Tweet.class)
               .setParameter("username", username)
               .getResultList();
    }

    @Override
    public void addTweet(Tweet tweet) {
        entityManager.persist(tweet);
    }
}

TweetManager 接口定义了对 Tweet 实体的操作方法, TweetManagerImpl 类实现了这些方法,使用 JPA 的 EntityManager 进行数据库操作。

8. 安全层

安全层负责用户认证和页面安全,确保只有经过认证的用户才能访问特定页面。

8.1 用户认证

在 Crosstalk 应用中,我们需要实现用户认证功能。可以创建一个 User 类来表示用户信息,并创建一个 UserService 类来处理用户认证逻辑。示例如下:

package com.wideplay.crosstalk.tweets;

public class User {
    private String username;
    private boolean authenticated;

    public User(String username) {
        this.username = username;
        this.authenticated = false;
    }

    public String getUsername() {
        return username;
    }

    public boolean isAuthenticated() {
        return authenticated;
    }

    public void authenticate() {
        this.authenticated = true;
    }

    public void logout() {
        this.authenticated = false;
    }
}
package com.wideplay.crosstalk.services;

import com.wideplay.crosstalk.tweets.User;
import javax.inject.Singleton;

@Singleton
public interface UserService {
    User authenticate(String username, String password);
}
package com.wideplay.crosstalk.services.impl;

import com.wideplay.crosstalk.tweets.User;
import com.wideplay.crosstalk.services.UserService;
import javax.inject.Singleton;

@Singleton
public class UserServiceImpl implements UserService {
    @Override
    public User authenticate(String username, String password) {
        // 简单示例,实际中应验证用户名和密码
        User user = new User(username);
        user.authenticate();
        return user;
    }
}

User 类表示用户信息, UserService 接口定义了用户认证方法, UserServiceImpl 类实现了该方法。

8.2 页面安全

为了确保页面安全,我们可以创建一个过滤器来验证用户是否已经认证。示例如下:

package com.wideplay.crosstalk.web;

import com.wideplay.crosstalk.tweets.User;
import javax.inject.Inject;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebFilter(urlPatterns = {"/home"})
public class AuthenticationFilter implements Filter {
    @Inject
    private User user;

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;

        if (!user.isAuthenticated()) {
            httpResponse.sendRedirect("/login?message=Please login.");
            return;
        }

        chain.doFilter(request, response);
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void destroy() {
    }
}

这个过滤器会拦截 /home 路径的请求,如果用户未认证,则重定向到登录页面。

9. 测试与部署

在完成开发后,我们需要对 Crosstalk 应用进行测试和部署。

9.1 单元测试

可以使用 JUnit 和 Mockito 等工具对各个模块进行单元测试。例如,对 TweetManager 类进行单元测试:

import com.wideplay.crosstalk.tweets.Tweet;
import com.wideplay.crosstalk.services.TweetManager;
import com.wideplay.crosstalk.services.impl.TweetManagerImpl;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
import java.util.Arrays;
import java.util.List;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class TweetManagerTest {
    @Test
    public void testTweetsFor() {
        EntityManager entityManager = Mockito.mock(EntityManager.class);
        TypedQuery<Tweet> query = Mockito.mock(TypedQuery.class);
        List<Tweet> tweets = Arrays.asList(new Tweet("user1", "Tweet 1"));

        Mockito.when(entityManager.createQuery(Mockito.anyString(), Mockito.eq(Tweet.class))).thenReturn(query);
        Mockito.when(query.setParameter(Mockito.anyString(), Mockito.anyString())).thenReturn(query);
        Mockito.when(query.getResultList()).thenReturn(tweets);

        TweetManager tweetManager = new TweetManagerImpl();
        ((TweetManagerImpl) tweetManager).setEntityManager(entityManager);

        List<Tweet> result = tweetManager.tweetsFor("user1");
        assertEquals(tweets, result);
    }
}
9.2 部署

将 Crosstalk 应用部署到 Servlet 容器中,如 Apache Tomcat 或 Mort Bay Jetty。确保在部署前已经完成了所有依赖库的添加和配置。

10. 扩展与优化

Crosstalk 应用可以根据需求进行进一步的扩展和优化。

10.1 功能扩展
  • 添加评论功能 :允许用户对 tweets 进行评论,需要创建评论实体类和相应的服务类。
  • 关注功能 :实现用户之间的关注关系,用户可以关注其他用户并查看他们的 tweets。
10.2 性能优化
  • 缓存机制 :使用缓存技术,如 Redis,缓存热门 tweets 或用户信息,减少数据库访问次数。
  • 数据库优化 :对数据库进行优化,如添加索引、优化查询语句等。
11. 总结

通过构建 Crosstalk 应用,我们详细介绍了如何在实际项目中应用依赖注入,包括表示层、持久化层和安全层的实现。依赖注入使得代码更加模块化、可维护和可测试,同时提高了代码的可扩展性。在开发过程中,我们使用 Google Guice 管理对象之间的依赖关系,使用 Google Sitebricks 渲染网页,使用 Hibernate 实现数据的持久化,使用自定义的过滤器实现用户认证和页面安全。通过合理的包结构和模块化设计,降低了模块之间的耦合度,使得代码更加灵活和易于维护。

以下是 Crosstalk 应用的整体架构 mermaid 流程图:

graph LR
    A[用户] --> B[Web 浏览器]
    B --> C[Servlet 容器]
    C --> D[GuiceFilter]
    D --> E{请求类型}
    E -->|静态资源| F[Servlet 容器处理]
    E -->|应用请求| G[Google Sitebricks]
    G --> H[HomePage 类]
    H -->|GET 请求| I[TweetManager 加载 tweets]
    H -->|POST 请求| J[TweetManager 添加 tweet]
    I --> K[HomePage 模板渲染]
    J --> K
    K --> L[返回页面给用户]
    M[数据库] <--> N[Hibernate]
    N <--> I
    N <--> J
    O[用户认证] <--> P[UserService]
    P <--> H

希望本文能够帮助你更好地理解和应用依赖注入,在实际项目中构建出高质量的应用。

Delphi 12.3 作为一款面向 Windows 平台的集成开发环境,由 Embarcadero Technologies 负责其持续演进。该环境以 Object Pascal 语言为核心,并依托 Visual Component Library(VCL)框架,广泛应用于各类桌面软件、数据库系统及企业级解决方案的开发。在此生态中,Excel4Delphi 作为一个重要的社区开源项目,致力于搭建 Delphi 与 Microsoft Excel 之间的高效桥梁,使开发者能够在自研程序中直接调用 Excel 的文档处理、工作表管理、单元格操作及宏执行等功能。 该项目以库文件与组件包的形式提供,开发者将其集成至 Delphi 工程后,即可通过封装良好的接口实现对 Excel 的编程控制。具体功能涵盖创建与编辑工作簿、格式化单元格、批量导入导出数据,乃至执行内置公式与宏指令等高级操作。这一机制显著降低了在财务分析、报表自动生成、数据整理等场景中实现 Excel 功能集成的技术门槛,使开发者无需深入掌握 COM 编程或 Excel 底层 API 即可完成复杂任务。 使用 Excel4Delphi 需具备基础的 Delphi 编程知识,并对 Excel 对象模型有一定理解。实践中需注意不同 Excel 版本间的兼容性,并严格遵循项目文档进行环境配置与依赖部署。此外,操作过程中应遵循文件访问的最佳实践,例如确保目标文件未被独占锁定,并实施完整的异常处理机制,以防数据损毁或程序意外中断。 该项目的持续维护依赖于 Delphi 开发者社区的集体贡献,通过定期更新以适配新版开发环境与 Office 套件,并修复已发现的问题。对于需要深度融合 Excel 功能的 Delphi 应用而言,Excel4Delphi 提供了经过充分测试的可靠代码基础,使开发团队能更专注于业务逻辑与用户体验的优化,从而提升整体开发效率与软件质量。 资源来源于网络分享,仅用于学习交流使用,请勿用于商业,如有侵权请联系我删除!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值