JavaWeb模块化开发与项目搭建流程【SpringBoot】【JavaWeb】【Mybatis】【Swagger】【多模块】

0 模板简介

本文记录了我在最近进行的今日指数项目中,如何从零开始搭建一个Java Web多模块项目,并集成MyBatis和Swagger。同时,还实现了一个根据用户名查询用户信息的简单功能。主要目的是通过规范项目目录结构,达到举一反三的效果。

1 创建父工程

模板由父子模块组成,parent模块负责管理版本,demo_common通用模块,demo_backend业务模块。

1.1 创建空的父工程

在Advanced Settings中填写项目信息。

GroupId:组织的唯一标识符,域名.公司名.项目名,org.apache.tomcat

ArtifactID: 项目的唯一的标识符, 项目名-xxx

在maven中通过GroupId和ArtifactID唯一确定一个jar包。
在这里插入图片描述

1.2 修改项目的maven地址

新建一个项目默认选择的是idea中自带的maven,我们修改为本地maven。
在这里插入图片描述

1.3 修改文件编码

在这里插入图片描述

1.4修改父工程的pom文件

1.4.1修改打包方式为pom

父工程不需要部署,只定义项目的依赖及版本。将项目打包方式修改为pom

<packaging>pom</packaging>

1.4.2.properties+dependencyManagement管理子工程的依赖版本

在properties中定义版本号(相当于全局变量),dependencyManagement中使用${},引入参数即可
dependencyManagement只做依赖的版本定义,并不直接导入依赖。
dependencyManagement中爆红解决方案
以下是模板使用的全局依赖定义,个人开发时根据实际情况删减。

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.encoding>UTF-8</maven.compiler.encoding>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <maven.surefire.version>2.12.4</maven.surefire.version>
        <mybatis-spring-boot-starter.version>2.1.4</mybatis-spring-boot-starter.version>
        <pagehelper-spring-boot-starter.version>1.2.12</pagehelper-spring-boot-starter.version>
        <mysql-driver.version>5.1.49</mysql-driver.version>
        <fastjson.version>1.2.71</fastjson.version>
        <springfox-swagger2.version>2.9.2</springfox-swagger2.version>
        <druid-spring-boot-starter.version>1.1.22</druid-spring-boot-starter.version>
        <druid-core-version>1.2.8</druid-core-version>
        <sharding-jdbc.version>4.0.0-RC1</sharding-jdbc.version>
        <jjwt.version>0.9.1</jjwt.version>
        <easyExcel.version>3.0.4</easyExcel.version>
        <xxl-job-core.version>2.3.0</xxl-job-core.version>
        <spring-boot.version>2.5.3</spring-boot.version>
        <joda-time.version>2.10.5</joda-time.version>
        <google.guava.version>30.0-jre</google.guava.version>
        <knif4j.version>2.0.2</knif4j.version>
        <hutool.version>5.7.21</hutool.version>
        <swagger.version>2.6.1</swagger.version>
        <junit.version>3.8.1</junit.version>
    </properties>
    <!--定义依赖的版本,不负责依赖的引入,只负责版本的约定-->
    <dependencyManagement>
        <!--!!!易错点-->
        <!--因为dependencyManagement标签只负责定义版本,不负责导入,如果maven仓库中没有指定的依赖,则报红色警告
            解决方式:先通过dependencies标签下载资源,然后删除
        -->
        <dependencies>
            <!--引入junit依赖-->
            <dependency>
                <groupId>junit</groupId>
                <artifactId>junit</artifactId>
                <version>${junit.version}</version>
            </dependency>
            <!--引入springboot依赖-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-parent</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!--引入mybatis场景依赖-->
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>${mybatis-spring-boot-starter.version}</version>
            </dependency>
            <!--pageHelper场景依赖-->
            <dependency>
                <groupId>com.github.pagehelper</groupId>
                <artifactId>pagehelper-spring-boot-starter</artifactId>
                <version>${pagehelper-spring-boot-starter.version}</version>
            </dependency>
            <!--mysql驱动包-->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>${mysql-driver.version}</version>
            </dependency>
            <!--shardingjdbc分库分表-->
            <dependency>
                <groupId>org.apache.shardingsphere</groupId>
                <artifactId>sharding-jdbc-spring-boot-starter</artifactId>
                <version>${sharding-jdbc.version}</version>
            </dependency>
            <!--json工具包-->
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>fastjson</artifactId>
                <version>${fastjson.version}</version>
            </dependency>
            <!--druid-boot依赖-->
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid-spring-boot-starter</artifactId>
                <version>${druid-spring-boot-starter.version}</version>
            </dependency>
            <!--druid core-->
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid</artifactId>
                <version>${druid-core-version}</version>
            </dependency>
            <!--引入jwt依赖-->
            <dependency>
                <groupId>io.jsonwebtoken</groupId>
                <artifactId>jjwt</artifactId>
                <version>${jjwt.version}</version>
            </dependency>
            <!-- 导出 excel -->
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>easyexcel</artifactId>
                <version>${easyExcel.version}</version>
            </dependency>
            <!--xxl-job定义任务框架支持-->
            <dependency>
                <groupId>com.xuxueli</groupId>
                <artifactId>xxl-job-core</artifactId>
                <version>${xxl-job-core.version}</version>
            </dependency>
            <!--时间小工具-->
            <dependency>
                <groupId>joda-time</groupId>
                <artifactId>joda-time</artifactId>
                <version>${joda-time.version}</version>
            </dependency>
            <!--引入google的工具集-->
            <dependency>
                <groupId>com.google.guava</groupId>
                <artifactId>guava</artifactId>
                <version>${google.guava.version}</version>
            </dependency>
            <!--knife4j的依赖-->
            <dependency>
                <groupId>com.github.xiaoymin</groupId>
                <artifactId>knife4j-spring-boot-starter</artifactId>
                <version>${knif4j.version}</version>
            </dependency>
            <!--hutool万能工具包-->
            <dependency>
                <groupId>cn.hutool</groupId>
                <artifactId>hutool-all</artifactId>
                <version>${hutool.version}</version>
            </dependency>
			<!--swagger-->
            <dependency>
                <groupId>io.springfox</groupId>
                <artifactId>springfox-swagger2</artifactId>
                <version>${swagger.version}</version>
            </dependency>
			<!--swagger-ui支持类-->
            <dependency>
                <groupId>io.springfox</groupId>
                <artifactId>springfox-swagger-ui</artifactId>
                <version>${swagger.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

1.4.3 统一插件管理

在父工程中定义项目的Maven插件版本。

<build>
    <!--插件的版本锁定-->
    <pluginManagement>
        <plugins>
            <!--Springboot核心插件-->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>${spring-boot.version}</version>
                <executions>
                    <execution>
                        <goals>
                            <!--防止打包时出现找不到springboot启动类的情况-->
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <excludes>
                        <!--插件运行时排除依赖-->
                        <exclude>
                            <groupId>org.springframework.boot</groupId>
                            <artifactId>spring-boot-configuration-processor</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </pluginManagement>
</build>

1.4.4 pom整体结构

在这里插入图片描述

2 定义通用模块

定义一个common模块为项目中的其他模块提供服务。多个模块公用的依赖可以在common文件中引入,其他模块引入stock_common模块可以直接使用通用模块里引入的所有依赖。

2.1创建common模块

Name定义为 项目名_模块名
Parent选中父工程
Archetype的选择及效果
在这里插入图片描述

2.2定义pom.xml文件

2.2.1 修改打包方式为jar

<packaging>jar</packaging>

2.2.2 添加依赖

这里的依赖只需要定义<groupId> <artifactId> 即可,版本由父工程的pom文件控制。

<dependencies>
    	<!--省略类的getset方法-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!--mybatis场景依赖-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>
		<!--jackson相关注解,实现日期格式转换和类型格式转换并序列化等-->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-annotations</artifactId>
        </dependency>
        <!--json工具包-->
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
        </dependency>
    
        <!--jode日期插件-->
        <dependency>
            <groupId>joda-time</groupId>
            <artifactId>joda-time</artifactId>
        </dependency>

        <!--工具包-->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
        </dependency>

        <!--分页插件-->
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper-spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
        </dependency>

        <!--apache工具包-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <!--配置提示,在写application.yml文件时提供提示-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
        </dependency>
		<!--引入swagger依赖-->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
        </dependency>

        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
        </dependency>
    </dependencies>

2.3 目录结构

├─constant  	静态变量
├─mapper		Mybatis对应的Mapper
├─pojo			java普通对象,范围比较大,本质上就是一个数据容器,且无需提供复杂的方法(getter and setter)
│  ├─domain 	领域对象,说白了就是将属于某个业务方向的数据加以封装形成的对象比如:以数据库为例,数据可能来自多张表、单表的部分字段值
│  ├─entity		实体对象,严格和数据库相对应
│  └─vo			value object值对象,说白了就是保存值的对象,内置的属性之间不一定有强相关
└─utils			工具类

2.4 MybatisX逆向工程

MybatisX插件功能简介:https://developer.aliyun.com/article/1203863
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
结果如下
在这里插入图片描述

3 定义业务模块

3.1 创建demo_backend模块

仿照demo_common模块的创建方式,backend模块集成自parent模块,打包方式改为jar

3.2 修改pom文件

引入demo_common模块,springboot的基本依赖,测试依赖。
编译插件的版本在父工程中指定。

    <dependencies>
        <!--引入公共依赖-->
        <dependency>
            <groupId>org.example</groupId>
            <artifactId>demo_common</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <!-- 基本依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--springboot测试依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <build>
        <!--打包名称-->
        <finalName>${project.artifactId}</finalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

3.3 模块目录

org
    └─example
        ├─config		存放配置文件
        ├─controller	负责处理外部请求和响应。
        ├─service		负责业务逻辑的设计和实现
        │  └─impl
        └─vo			View Object 与前端交互的数据对象
            ├─req		request:用对象接收前端传来的数据
            └─resp		respose:将数据封装为对象并返回给前端

4 集成Mybatis

4.1 pom中引入依赖

mysql durid mybatis 依赖已经在demo_common的pom中引入。

4.2 配置

在application.yml中设置项目的启动端口,配置数据库连接池,mybatis的相关参数。

# 定义端口号
server:
  port: 8091
# 配置数据源
spring:
#  main:
#    allow-bean-definition-overriding: true # 配置允许容器中的bean资源被覆盖。
  datasource:
    druid:
      username: root
      password: 123456
      url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
      driver-class-name: com.mysql.jdbc.Driver
      # 初始化时建立物理连接的个数。初始化发生在显示调用 init 方法,或者第一次 getConnection 时
      initialSize: 6
      # 最小连接池数量
      minIdle: 2
      # 最大连接池数量
      maxActive: 20
      # 获取连接时最大等待时间,单位毫秒。配置了 maxWait 之后,缺省启用公平锁,
      # 并发效率会 有所下降,如果需要可以通过配置 useUnfairLock 属性为 true 使用非公平锁。
      maxWait: 60000
# 配置mybatis
mybatis:
  type-aliases-package: org.example.pojo
  mapper-locations: classpath:mapper/*.xml
  configuration:
    map-underscore-to-camel-case: true # 开启驼峰映射
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 打印运行的sql
    cache-enabled: false # 禁止二级缓存
    local-cache-scope: statement # 一级缓存默认开启session statement

Mybatis参数说明:

type-aliases-package: org.example.pojo

  • 这个配置项指向了org.example.pojo包下的所有类,在MyBatis的映射文件中可以直接使用这些类的简写名,而不需要全限定名。例如,org.example.pojo.User可以直接用User

mapper-locations: classpath:mapper/*.xml

  • 表示在类路径下的mapper文件夹中的所有XML文件,这些文件定义了SQL映射关系。

map-underscore-to-camel-case: true

  • 将数据库中的下划线命名(如user_id)自动映射为Java对象中的驼峰命名(如userId)。由测试结果可以看到当直接打印从数据库中查找到的结果时,已经完成了驼峰转化过程,因此可以直接用sysUser接收。
<select id="selectByPrimaryKey1" resultType="org.example.pojo.entity.SysUser">
    select * from sys_user where id = #{id}
</select>
//测试mybatis的驼峰转换
@Test
public void testCamel(){
    System.out.println(sysUserMapper.selectByPrimaryKey1(1237361915165020161L));
}
JDBC Connection [com.mysql.jdbc.JDBC4Connection@1c4057f9] will not be managed by Spring
==>  Preparing: select * from sys_user where id = ?
==> Parameters: 1237361915165020161(Long)
<==    Columns: id, username, password, phone, real_name, nick_name, email, status, sex, deleted, create_id, update_id, create_where, create_time, update_time
<==        Row: 1237361915165020161, admin, $2a$10$JqoiFCw4LUj184ghgynYp.4kW5BVeAZYjKqu7xEKceTaq7X3o4I4W, 13888888888, 小池, 超级管理员, 875267425@qq.com, 1, 1, 1, null, 1237361915165020161, 1, 2019-09-22 19:38:05.0, 2020-04-07 18:08:52.0
<==      Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@401926df]
SysUser(id=1237361915165020161, username=admin, password=$2a$10$JqoiFCw4LUj184ghgynYp.4kW5BVeAZYjKqu7xEKceTaq7X3o4I4W, phone=13888888888, realName=小池, nickName=超级管理员, email=875267425@qq.com, status=1, sex=1, deleted=1, createId=null, updateId=1237361915165020161, createWhere=1, createTime=Sun Sep 22 19:38:05 CST 2019, updateTime=Tue Apr 07 18:08:52 CST 2020)

cache-enabled: false
二级缓存是跨SqlSession的,也就是说,不同的SqlSession可以共享二级缓存中的数据。

6 demo_backend模块

实现一个根据用户名查询用户信息的访问接口,完成接收前端访问请求并从数据库查询数据并返回的完整过程。

6.1 定义前后端交互数据格式

  • 在前后端分离架构下,前后端使用json格式进行交互;
  • 后端响应数据封装到统一类中;
{
  code: 1 //状态码
  msg: "" //提示信息
  data:   //任意响应数据
}

统一数据封装:

/**
 * 返回数据类
 * @JsonInclude 保证序列化json的时候,如果是null的对象,key也会消失
 * @param <T>
 */
@JsonInclude(JsonInclude.Include.NON_NULL)
public class R<T> implements Serializable {
    private static final long serialVersionUID = 7735505903525411467L;

    // 成功值,默认为1
    private static final int SUCCESS_CODE = 1;
    // 失败值,默认为0
    private static final int ERROR_CODE = 0;

    //状态码
    private int code;
    //消息
    private String msg;
    //返回数据
    private T data;

    private R(int code){
        this.code = code;
    }
    private R(int code, T data){
        this.code = code;
        this.data = data;
    }
    private R(int code, String msg){
        this.code = code;
        this.msg = msg;
    }
    private R(int code, String msg, T data){
        this.code = code;
        this.msg = msg;
        this.data = data;
    }

    public static <T> R<T> ok(){
        return new R<T>(SUCCESS_CODE,"success");
    }
    public static <T> R<T> ok(String msg){
        return new R<T>(SUCCESS_CODE,msg);
    }
    public static <T> R<T> ok(T data){
        return new R<T>(SUCCESS_CODE,data);
    }
    public static <T> R<T> ok(String msg, T data){
        return new R<T>(SUCCESS_CODE,msg,data);
    }

    public static <T> R<T> error(){
        return new R<T>(ERROR_CODE,"error");
    }
    public static <T> R<T> error(String msg){
        return new R<T>(ERROR_CODE,msg);
    }
    public static <T> R<T> error(int code, String msg){
        return new R<T>(code,msg);
    }
    public static <T> R<T> error(ResponseCode res){
        return new R<T>(res.getCode(),res.getMessage());
    }

    public int getCode(){
        return code;
    }
    public String getMsg(){
        return msg;
    }
    public T getData(){
        return data;
    }
}

ResponseCode:枚举类状态码

public enum ResponseCode{
    ERROR(0,"操作失败"),
    SUCCESS(1,"操作成功"),
    DATA_ERROR(0,"参数异常"),
    NO_RESPONSE_DATA(0,"无响应数据"),
    CHECK_CODE_NOT_EMPTY(0,"验证码不能为空");
    private int code;
    private String message;

    private ResponseCode(int code, String message) {
        this.code = code;
        this.message = message;
    }

    public int getCode() {
        return code;
    }

    public String getMessage() {
        return message;
    }
}

6.2 集成swagger+knife4j

官网:https://swagger.io/
knife4j是为Java MVC框架集成Swagger生成Api文档的增强解决方案

6.2.1 demo_common工程引入依赖

<!--swagger-->
<dependency>
  <groupId>io.springfox</groupId>
  <artifactId>springfox-swagger2</artifactId>
</dependency>
<dependency>
  <groupId>io.springfox</groupId>
  <artifactId>springfox-swagger-ui</artifactId>
</dependency>
<!--knife4j的依赖-->
<dependency>
  <groupId>com.github.xiaoymin</groupId>
  <artifactId>knife4j-spring-boot-starter</artifactId>
</dependency>
<!--支持接口参数校验处理,表单验证工具-->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

6.2.2 demo_backend工程config包定义swagger配置类

@Configuration
@EnableSwagger2
@EnableKnife4j
@Import(BeanValidatorPluginsConfiguration.class)
public class SwaggerConfiguration {
    @Bean
    public Docket buildDocket() {
        //构建在线API概要对象
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(buildApiInfo())
                .select()
                // 要扫描的API(Controller)基础包
                .apis(RequestHandlerSelectors.basePackage("org.example.controller"))
                .paths(PathSelectors.any())
                .build();
    }
    private ApiInfo buildApiInfo() {
        //网站联系方式
        Contact contact = new Contact("mmj","https://www.test.com/","test@163.com");
        return new ApiInfoBuilder()
                .title("文档标题")//文档标题
                .description("文档描述信息")//文档描述信息
                .contact(contact)//站点联系人相关信息
                .version("1.0.0")//文档版本
                .build();
    }
}

6.2.3 生成swagger注解

https://blog.csdn.net/m0_61466807/article/details/128430020
访问在线文档:http://localhost:8091/doc.html

6.2 Controller层

@Api(value = "/api", tags = {"test"})
@RestController
@RequestMapping("/api")
public class UserController {
    @Autowired
    private UserService userService;

    /**
     * 根据用户名查询用户信息
     * @param userName
     * @return
     */
    @ApiImplicitParams({
            @ApiImplicitParam(paramType = "path", dataType = "string", name = "userName", value = "", required = true)
    })
    @ApiOperation(value = "根据用户名查询用户信息", notes = "根据用户名查询用户信息", httpMethod = "GET")
    @GetMapping("/user/{userName}")
    public R<SysUser> getUserByUserName(@PathVariable("userName") String userName){
        return userService.getUserByUserName(userName);
    }
}

@RestController = @Controller + @ResponseBody
用于返回json格式的数据。

6.3 service层

public interface UserService{
    /**
     * 根据用户查询用户信息
     * @param userName 用户名称
     * @return
     */
    R<SysUser> getUserByUserName(String userName);
}
@Service
public class UserServiceImpl implements UserService {
    @Autowired
    private SysUserMapper sysUserMapper;

    /**
     * 根据用户名称查询用户信息
     *
     * @param userName 用户名称
     * @return
     */
    @Override
    public R<SysUser> getUserByUserName(String userName) {
        SysUser user=sysUserMapper.findByUserName(userName);
        if(user == null) return R.error(ResponseCode.USERNAME_NOT_EXISTS);
        return R.ok(user);
    }
}

6.4 DAO层

SysUserMapper

public interface SysUserMapper {
    /**
     * 根据用户名称查询用户信息
     * @param userName 用户名称
     * @return
     */
    SysUser findByUserName(String userName);
}

SysUserMapper.xml

<select id="findByUserName" resultMap="BaseResultMap">
    select
    <include refid="Base_Column_List" />
    from sys_user
    where   username= #{name}
</select>

6.5 测试

启动项目访问 localhost:8091/api/user/admin
访问在线文档:http://localhost:8091/doc.html
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值