JavaWeb项目测试实战:从单元测试到接口自动化的完整方案
1. 项目概述与核心价值
最近在带几个实习生做项目复盘,发现一个挺普遍的现象:很多同学在学校里JavaWeb开发的基础语法和框架都学得不错,但一到实际项目测试环节就有点懵。要么是测试用例写得零零散散,要么是面对一个完整的Web项目不知道从何“下手”去系统地验证。这让我想起自己刚入行那会儿,也是拿着一堆零散的测试理论,却不知道如何把它们组装成一个能真正“跑起来”、覆盖全面的测试方案。所以,今天我想结合一个典型的JavaWeb课程设计项目,从头到尾拆解一遍,如何搭建一个“麻雀虽小,五脏俱全”的测试资源文件体系。这不仅仅是写几个JUnit测试类那么简单,而是涵盖从单元测试、集成测试、接口测试到前端功能与兼容性测试的一整套实践方案。目标很明确:让你手头的课程设计项目,不仅能跑通功能,更能经得起各种“刁钻”场景的考验,把“实践技能”这个虚词,落到实实在在的代码和报告里。
这个资源文件包,你可以理解为是一个为JavaWeb项目量身定制的“测试脚手架”。它预设了目录结构、提供了常用工具配置模板、编写了典型场景的测试用例示例,更重要的是,它会告诉你每一步“为什么”要这么做。比如,为什么要在 pom.xml 里引入 spring-boot-starter-test 而不是一个个单独引入JUnit和Mockito?为什么接口测试要区分 @SpringBootTest 和 @WebMvcTest ?面对一个文件上传功能,测试用例该怎么设计边界?这些经验性的东西,往往是文档里不会细说,但又直接决定测试效率和深度的关键。接下来,我们就进入正题,看看这个全面的测试资源文件到底包含哪些内容,以及如何应用到你的项目中。
2. 测试体系整体设计与环境搭建
2.1 测试金字塔与资源文件结构映射
在动手写任何测试代码之前,我们必须先理清思路:要对一个JavaWeb项目进行“全面”测试,到底要测哪些层面?业界普遍认可的“测试金字塔”模型给了我们很好的指导。金字塔从下到上分别是:单元测试(最多)、集成测试(中等)、端到端(E2E)测试(最少)。对应到我们的JavaWeb项目,可以具体化为:
- 单元测试 :针对最小的代码单元(通常是类中的一个方法)进行隔离测试。核心是“快”和“独立”。我们会使用JUnit 5作为框架,配合Mockito来模拟所有外部依赖(如数据库、第三方服务、其他类),确保只测试当前方法自身的逻辑。
- 集成测试 :测试多个模块或组件在一起工作是否正常。在JavaWeb中,这通常指:
- Service层集成测试 :测试Service与真实的Repository(如MyBatis Mapper、JPA Repository)的交互。
- Web层集成测试 :测试Controller的HTTP接口,包括请求映射、参数绑定、序列化/反序列化、过滤器/拦截器等,但通常不启动完整的Servlet容器(使用MockMvc模拟)。
- 数据访问层集成测试 :使用内存数据库(如H2)测试SQL映射或JPA操作的正确性。
- 端到端测试 :模拟真实用户操作,从前端界面发起请求,经过完整后端链路,再到数据库,最后验证前端展示。这通常使用Selenium或Cypress等工具。在课程设计中,出于复杂度和环境考虑,我们可能会用更轻量的 接口自动化测试 (使用RestAssured或TestRestTemplate)来覆盖核心业务流,并辅以 前端功能与兼容性测试 的检查清单。
基于这个模型,我们的测试资源文件目录结构设计如下:
src/test/java/
├── unit/ # 单元测试包
│ ├── service/ # Service单元测试
│ ├── utils/ # 工具类单元测试
│ └── ...
├── integration/ # 集成测试包
│ ├── service/ # Service集成测试(与真实Repo交互)
│ ├── web/ # Controller层测试(MockMvc)
│ └── persistence/ # 数据层测试(使用H2)
├── e2e/ # 端到端/接口自动化测试包
│ └── api/ # 使用RestAssured的API测试
└── resources/
├── application-test.yml # 测试专用配置文件
├── sql/ # 测试数据脚本
│ ├── schema-h2.sql # H2数据库建表语句
│ └── data-test.sql # 基础测试数据
└── static/ # 前端测试相关(如测试用图片)
注意 :这个结构是逻辑上的划分,实际项目中可以根据团队习惯调整。关键是要有清晰的隔离,避免单元测试里启动了Spring容器,或者集成测试里混用了大量的Mock。
2.2 核心依赖配置与测试环境隔离
一个清晰的依赖管理和环境隔离是高效测试的基石。我们以Maven项目为例,在 pom.xml 中配置测试相关依赖。
核心依赖引入 :
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<!-- 排除旧的JUnit 4,确保使用JUnit 5 -->
<exclusion>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 内存数据库,用于集成测试 -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
<!-- 更强大的API测试库 -->
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
为什么是
spring-boot-starter-test? 它是一个“全家桶”,默认包含了JUnit 5, Spring Test, AssertJ, Hamcrest, Mockito, JSONassert, JsonPath等测试所需的绝大部分库。用这一个依赖,省去了手动管理多个依赖版本兼容性的麻烦。
环境隔离配置 : 在 src/test/resources/ 下创建 application-test.yml 。这是Spring Boot的Profile特性,当以 test Profile运行时,该配置会覆盖 application.yml 中的配置。
spring:
datasource:
url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;MODE=MySQL # 使用内存H2,模拟MySQL模式
driver-class-name: org.h2.Driver
username: sa
password:
jpa:
hibernate:
ddl-auto: create-drop # 测试启动时建表,结束时删表,保持环境干净
show-sql: true # 方便查看测试执行的SQL
properties:
hibernate:
dialect: org.hibernate.dialect.H2Dialect
sql:
init:
mode: always # 总是执行初始化脚本
schema-locations: classpath:sql/schema-h2.sql
data-locations: classpath:sql/data-test.sql
# 关闭一些非必要的组件,加速测试
management:
endpoints:
web:
exposure:
include: '*' # 根据测试需要开启或关闭
logging:
level:
root: WARN
com.yourpackage: DEBUG # 将你自己项目的包调为DEBUG,便于排查
实操心得 :
ddl-auto: create-drop在集成测试中非常有用,它能保证每次测试都是全新的、干净的数据环境,避免了测试用例间的数据污染。但切记, 绝对不要 在生产配置中使用这个选项。
3. 分层测试策略详解与实战
3.1 单元测试:聚焦单一方法逻辑
单元测试的目标是验证一个方法在给定输入下,是否产生预期的输出或行为。关键在于 隔离 。我们使用Mockito来模拟该方法所有的外部依赖。
场景示例 :测试一个 UserService 的 login 方法。
// src/test/java/unit/service/UserServiceUnitTest.java
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.mockito.BDDMockito.*;
import static org.assertj.core.api.Assertions.*;
@ExtendWith(MockitoExtension.class) // 启用Mockito
public class UserServiceUnitTest {
@Mock
private UserRepository userRepository; // 模拟依赖
@Mock
private PasswordEncoder passwordEncoder;
@InjectMocks
private UserService userService; // 被测试对象,自动注入模拟的依赖
@Test
void login_Success_WhenCredentialsAreCorrect() {
// 1. 准备数据 (Given)
String username = "testUser";
String rawPassword = "password123";
String encodedPassword = "encodedPassword";
User mockUser = new User(username, encodedPassword);
given(userRepository.findByUsername(username)).willReturn(Optional.of(mockUser));
given(passwordEncoder.matches(rawPassword, encodedPassword)).willReturn(true);
// 2. 执行操作 (When)
User result = userService.login(username, rawPassword);
// 3. 验证结果 (Then)
assertThat(result).isNotNull();
assertThat(result.getUsername()).isEqualTo(username);
// 验证依赖的交互行为
then(userRepository).should().findByUsername(username);
then(passwordEncoder).should().matches(rawPassword, encodedPassword);
}
@Test
void login_Fails_WhenUserNotFound() {
// Given
String username = "nonExist";
given(userRepository.findByUsername(username)).willReturn(Optional.empty());
// When & Then
assertThatThrownBy(() -> userService.login(username, "anyPassword"))
.isInstanceOf(UserNotFoundException.class);
// 验证passwordEncoder没有被调用
then(passwordEncoder).shouldHaveNoInteractions();
}
}
关键点解析 :
-
@ExtendWith(MockitoExtension.class):这是JUnit 5的扩展方式,替代了JUnit 4的@RunWith。它负责初始化@Mock和@InjectMocks注解的字段。 - BDDMockito风格 :
given(...).willReturn(...)和then(...).should(...)是BDD(行为驱动开发)风格的Mockito语法,让测试的“准备-执行-验证”三段论更清晰。 - AssertJ断言 :比JUnit自带的
Assertions更强大,提供流式API,断言失败信息更友好。例如assertThat(result).isNotNull().hasFieldOrPropertyWithValue("username", username)。 - 验证交互 :单元测试不仅要验证结果,还要验证与模拟对象的交互是否符合预期(如方法被调用了一次、从未被调用等),这是确保方法逻辑正确的关键。
常见坑点 :不要过度Mock。如果一个被测方法内部调用另一个私有方法,通常不应该去Mock这个私有方法。单元测试应关注公共契约。过度Mock会导致测试与实现细节紧密耦合,一旦重构,测试就需要大量修改。
3.2 集成测试:验证组件协作
集成测试允许部分或全部组件以真实方式交互。这里我们重点看Service集成测试和Web层测试。
Service集成测试(与真实数据库交互) :
// src/test/java/integration/service/UserServiceIntegrationTest.java
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.ActiveProfiles;
import javax.transaction.Transactional;
import static org.assertj.core.api.Assertions.assertThat;
@DataJpaTest // 只加载JPA相关组件,速度快
@Import({UserServiceImpl.class}) // 显式导入要测试的Service,因为@DataJpaTest默认不加载@Service
@ActiveProfiles("test") // 激活test profile,使用H2数据库
public class UserServiceIntegrationTest {
@Autowired
private UserRepository userRepository;
@Autowired
private UserService userService;
@Test
@Transactional // 测试后回滚,保持数据库干净
void createUser_ShouldPersistToDatabase() {
// Given
CreateUserRequest request = new CreateUserRequest("newUser", "email@test.com", "pass");
// When
User savedUser = userService.createUser(request);
// Then
assertThat(savedUser.getId()).isNotNull();
assertThat(userRepository.findById(savedUser.getId())).isPresent();
}
}
关键点解析 :
-
@DataJpaTest:这是一个“切片测试”注解。它只初始化数据层相关的Bean(Repository, EntityManager, DataSource),不加载@Service, @Controller等,因此测试启动非常快。 -
@Transactional:这是确保测试独立性的关键。每个测试方法在一个事务中执行,方法结束后事务回滚,数据库状态恢复到测试前,避免了测试间的数据污染。 - 使用真实Repository :这里
userRepository是Spring管理的真实Bean,操作的是H2内存数据库。这测试了Service逻辑与数据访问层协作的正确性。
Web层集成测试(使用MockMvc) :
// src/test/java/integration/web/UserControllerTest.java
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@WebMvcTest(UserController.class) // 只加载Web层相关组件,Controller, Filter等
public class UserControllerTest {
@Autowired
private MockMvc mockMvc; // 模拟HTTP请求的核心类
@MockBean
private UserService userService; // Controller依赖的Service需要Mock
@Test
void getUser_ShouldReturnUser_WhenUserExists() throws Exception {
// Given
User mockUser = new User(1L, "testUser");
given(userService.getUserById(1L)).willReturn(mockUser);
// When & Then
mockMvc.perform(get("/api/users/1") // 模拟GET请求
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk()) // 断言状态码
.andExpect(jsonPath("$.id").value(1)) // 使用JsonPath断言JSON响应体
.andExpect(jsonPath("$.username").value("testUser"));
}
@Test
void createUser_ShouldReturn201_WhenInputIsValid() throws Exception {
// Given
CreateUserRequest request = new CreateUserRequest("newUser", "email", "pass");
given(userService.createUser(any(CreateUserRequest.class))).willReturn(new User(...));
// When & Then
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request))) // 对象序列化为JSON
.andExpect(status().isCreated())
.andExpect(header().string("Location", containsString("/api/users/")));
}
}
关键点解析 :
-
@WebMvcTest:另一个“切片测试”注解,专门用于测试Controller。它不会启动完整的嵌入式Servlet容器(如Tomcat),因此速度极快。它会自动配置MockMvc。 -
@MockBean:因为@WebMvcTest只加载Web层,所以Controller依赖的Service需要用@MockBean来模拟并注入到Spring上下文中。 -
MockMvc:提供了强大的API来模拟HTTP请求、验证响应状态、头部、内容等。jsonPath是验证JSON响应体的利器。 -
objectMapper:通常需要自动注入ObjectMapper来序列化请求体。Spring Boot会自动配置一个。
3.3 接口自动化测试:模拟真实HTTP调用
当需要测试的接口跨多个服务,或者你想模拟更接近真实网络调用的场景时,可以使用RestAssured。它语法直观,适合测试RESTful API。
// src/test/java/e2e/api/UserApiTest.java
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.http.HttpStatus;
import org.springframework.test.context.ActiveProfiles;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) // 启动真实容器,随机端口
@ActiveProfiles("test")
public class UserApiTest {
@LocalServerPort
private int port; // 注入随机端口
@BeforeEach
void setup() {
RestAssured.port = port; // 配置RestAssured使用这个端口
RestAssured.basePath = "/api"; // 可选,设置基础路径
}
@Test
void testGetUserById() {
// 假设数据库初始化脚本已插入ID为1的用户
given()
.accept(ContentType.JSON)
.when()
.get("/users/1")
.then()
.statusCode(HttpStatus.OK.value())
.body("id", equalTo(1))
.body("username", notNullValue());
}
@Test
void testCreateUser_ValidationFailure() {
Map<String, Object> invalidUser = new HashMap<>();
invalidUser.put("username", ""); // 用户名空,应触发验证失败
given()
.contentType(ContentType.JSON)
.body(invalidUser)
.when()
.post("/users")
.then()
.statusCode(HttpStatus.BAD_REQUEST.value())
.body("errors.username", hasItem("用户名不能为空")); // 验证错误信息
}
}
关键点解析 :
-
SpringBootTest.WebEnvironment.RANDOM_PORT:启动一个真实的、监听随机端口的嵌入式Servlet容器。这比MockMvc更“重”,但能测试完整的HTTP栈(过滤器、拦截器、SSL等)。 - Given-When-Then语法 :RestAssured的DSL(领域特定语言)非常流畅,读起来就像自然语言。
- 验证复杂响应 :可以轻松验证JSON/XML的嵌套结构、数组大小、特定字段值等。
注意事项 :接口自动化测试启动较慢,且依赖外部服务(如数据库)状态。通常将其放在持续集成(CI)流水线的后期阶段执行,而不是每次本地构建都跑。
4. 测试数据管理与前端测试考量
4.1 测试数据准备策略与最佳实践
稳定的测试离不开可控的数据。我们主要采用两种策略:
1. 基于SQL脚本的静态数据 : 在 src/test/resources/sql/ 下准备两个脚本:
schema-h2.sql:创建表结构。可以从生产数据库导出DDL,并修改为H2兼容的语法。data-test.sql:插入基础数据。这些数据应该是测试用例依赖的“基线数据”,比如管理员账号、基础配置项等。
-- data-test.sql
INSERT INTO users (id, username, password, email) VALUES
(1, 'admin', '$2a$10$...', 'admin@example.com'), -- 密码建议用BCrypt加密后的值
(2, 'test_user', '$2a$10$...', 'user@example.com');
INSERT INTO products (id, name, price, stock) VALUES
(1, '测试商品A', 100.00, 50),
(2, '测试商品B', 200.00, 0); -- 库存为0的商品,用于边界测试
在 application-test.yml 中配置 spring.sql.init.mode=always 和对应的脚本位置,Spring Boot会在启动测试上下文时自动执行。
2. 使用 @Sql 注解的动态数据 : 对于特定测试方法需要特殊数据,或需要清理 data-test.sql 中数据的情况,可以使用 @Sql 注解。
@Test
@Sql(scripts = "/sql/cleanup-users.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
@Sql(scripts = "/sql/setup-special-user.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
void testWithSpecialUser() {
// 这个测试方法执行前会运行setup-special-user.sql
// 执行后会运行cleanup-users.sql清理
}
最佳实践 :
- 数据独立性 :每个测试方法应尽可能不依赖其他测试方法产生的数据。使用
@Transactional或@Sql进行清理。 - 数据可读性 :在
data-test.sql中,可以使用注释说明每条数据用于测试什么场景。 - 避免硬编码ID :在测试中,尽量避免硬编码数据库中的ID。可以通过查询或使用已知的唯一属性(如用户名)来获取实体。
4.2 前端功能与兼容性测试要点
对于JavaWeb课程设计,前端可能是JSP、Thymeleaf或简单的HTML/JS。虽然不要求像专业前端一样写自动化测试,但一份手动的 测试检查清单 至关重要,可以确保基本功能可用。
你可以创建一个 前端测试检查清单.md 文件,包含以下内容:
功能测试 :
- [ ] 所有表单提交(登录、注册、数据新增/编辑)是否正常工作?
- [ ] 表单验证(前端JS验证)是否生效?错误提示是否友好?
- [ ] 页面链接(导航栏、按钮链接)是否都能正确跳转?
- [ ] 分页功能(如果有)是否能正确显示数据、跳转页码?
- [ ] 模态框(Modal)、下拉菜单等交互组件是否正常显示和关闭?
- [ ] 文件上传/下载功能是否正常?
用户体验与兼容性 :
- [ ] 在Chrome、Firefox、Edge最新版本下,页面布局和功能是否正常?
- [ ] 页面在移动设备屏幕(或浏览器模拟移动设备)上是否可正常浏览和操作?(响应式设计)
- [ ] 网络较慢时,页面是否有加载中的提示?
- [ ] 表单提交后,按钮是否有防重复点击处理?
- [ ] 关键的异步操作(如Ajax请求)失败时,是否有错误提示?
实操建议 :在项目答辩或演示前,按照这个清单逐项手动检查并打勾。这不仅能发现问题,也是你项目完整性和严谨性的体现。
5. 测试报告、持续集成与经验总结
5.1 生成可读的测试报告
测试不仅要能跑过,结果还要一目了然。Maven Surefire Plugin(运行单元测试)和Failsafe Plugin(运行集成测试)可以生成多种格式的报告。
在 pom.xml 中配置:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<reportFormat>brief</reportFormat>
<useFile>false</useFile>
<!-- 生成JUnit XML报告,CI工具(如Jenkins)可以解析 -->
<reportsDirectory>${project.build.directory}/surefire-reports</reportsDirectory>
</configuration>
</plugin>
<!-- 可选:配置生成HTML报告 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-site-plugin</artifactId>
<version>3.12.1</version>
</plugin>
</plugins>
</build>
运行 mvn test 后,在 target/surefire-reports 目录下会有 .txt 和 .xml 格式的报告。更直观的方式是使用像 Surefire Report 这样的插件生成HTML。或者,在IDE(如IntelliJ IDEA)中直接运行测试,其内置的测试运行器已经提供了非常清晰的树状视图和失败详情。
5.2 集成到持续集成(CI)流水线
对于团队项目或个人想提升工程化水平,将测试集成到CI中是必选项。这里以GitHub Actions为例,提供一个简单的 .github/workflows/ci.yml 配置:
name: Java CI with Maven
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Run Unit Tests
run: mvn clean test # 运行单元测试
- name: Run Integration Tests
run: mvn clean verify -DskipTests # verify阶段会运行failsafe插件的集成测试
这样,每次代码推送或发起拉取请求时,都会自动在云端运行全套测试,确保新代码不会破坏现有功能。
5.3 常见问题排查与实战心得
问题1: @SpringBootTest 启动巨慢怎么办?
- 原因 :它加载了完整的应用上下文。
- 解决 :
- 优先使用切片测试(
@WebMvcTest,@DataJpaTest,@JsonTest等)。 - 如果必须用
@SpringBootTest,使用@SpringBootTest(classes = {YourSpecificConfig.class})来限定加载的配置类。 - 使用
@MockBean来模拟那些初始化慢的Bean(如外部服务客户端)。
- 优先使用切片测试(
问题2:测试时数据库连接失败或表不存在?
- 检查 :确保
application-test.yml配置正确,特别是H2的URL和驱动。 - 检查 :确保
spring.sql.init.mode设置为always,并且schema-h2.sql文件路径正确、语法兼容H2。 - 技巧 :在测试类上加
@DirtiesContext注解,可以告诉Spring在测试结束后重置应用上下文,有时能解决因上下文缓存导致的数据库状态问题。
问题3: MockMvc 测试时遇到 HttpMediaTypeNotSupportedException 或参数绑定失败?
- 检查请求Content-Type :使用
.contentType(MediaType.APPLICATION_JSON)。 - 检查参数注解 :Controller方法参数是否使用了
@RequestBody,@PathVariable,@RequestParam等正确注解。 - 使用
MockMvc的调试模式 :在perform()后加上.andDo(print()),可以打印出详细的请求和响应信息,是排查问题的神器。
问题4:测试覆盖率上不去?
- 不要盲目追求高覆盖率 :优先覆盖核心业务逻辑、复杂条件分支和异常流程。
- 使用Jacoco插件 :在
pom.xml中配置Jacoco,运行mvn clean test jacoco:report,在target/site/jacoco下查看详细的HTML覆盖率报告,直观看到哪些行、哪些分支未被覆盖。 - 关注“覆盖到的”而非“没覆盖的” :覆盖率报告更重要的是帮你发现那些你以为测了但其实没测到的“盲区”。
个人心得 :测试不是开发完成后才进行的“附加活动”,而应该是驱动设计、保障质量的核心实践。在写业务代码之前,先思考“这个功能该怎么测”,往往会促使你写出接口更清晰、职责更单一、更易于测试的代码。这份为JavaWeb课程设计准备的测试资源文件,其价值不仅在于提供了一套可运行的代码模板,更在于传递这种“测试先行”和“全面验证”的工程思维。当你习惯为每个方法编写单元测试,为每个接口编写集成测试,并把这些测试纳入自动化流程时,你对代码质量的掌控力和自信心会得到质的提升。
更多推荐

所有评论(0)