Harmony学习之网络请求与数据获取

Harmony学习之网络请求与数据获取

一、场景引入

小明在上一篇文章中掌握了应用生命周期管理,现在他需要从服务器获取真实的用户数据和商品信息,而不是使用模拟数据。比如用户登录后需要从服务器验证账号密码,首页需要展示从服务器获取的实时行情数据,商品详情页需要加载服务器返回的商品信息。本篇文章将系统讲解HarmonyOS的网络请求机制,帮助小明实现与后端API的数据交互。

二、网络模块导入与配置

1. 导入网络模块

在需要使用网络请求的文件中,导入http模块:

import http from '@ohos.net.http';

2. 配置网络权限

src/main/module.json5文件中添加网络权限:

{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.INTERNET"
      }
    ]
  }
}

三、发起HTTP请求

1. 创建请求对象

// 创建HTTP请求对象
let httpRequest = http.createHttp();

2. 发起GET请求

// 发起GET请求
httpRequest.request(
  'https://api.example.com/users',
  {
    method: http.RequestMethod.GET,
    header: {
      'Content-Type': 'application/json'
    }
  },
  (err, data) => {
    if (err) {
      console.error('请求失败:', err);
      return;
    }
    
    if (data.responseCode === 200) {
      // 请求成功
      const result = JSON.parse(data.result);
      console.log('响应数据:', result);
    } else {
      console.error('请求失败,状态码:', data.responseCode);
    }
  }
);

3. 发起POST请求

// 发起POST请求
httpRequest.request(
  'https://api.example.com/login',
  {
    method: http.RequestMethod.POST,
    header: {
      'Content-Type': 'application/json'
    },
    extraData: JSON.stringify({
      username: 'xiaoming',
      password: '123456'
    })
  },
  (err, data) => {
    if (err) {
      console.error('请求失败:', err);
      return;
    }
    
    if (data.responseCode === 200) {
      const result = JSON.parse(data.result);
      console.log('登录成功:', result);
    } else {
      console.error('登录失败,状态码:', data.responseCode);
    }
  }
);

4. 请求方法枚举

// 支持的请求方法
http.RequestMethod.GET      // GET请求
http.RequestMethod.POST     // POST请求
http.RequestMethod.PUT      // PUT请求
http.RequestMethod.DELETE   // DELETE请求
http.RequestMethod.OPTIONS  // OPTIONS请求
http.RequestMethod.HEAD     // HEAD请求
http.RequestMethod.TRACE     // TRACE请求
http.RequestMethod.CONNECT  // CONNECT请求

四、请求配置详解

1. 请求头配置

// 完整的请求头配置
{
  method: http.RequestMethod.GET,
  header: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer token123',
    'User-Agent': 'HarmonyOS App/1.0',
    'Accept': 'application/json',
    'Accept-Language': 'zh-CN',
    'Cache-Control': 'no-cache'
  }
}

2. 请求参数配置

// GET请求带查询参数
httpRequest.request(
  'https://api.example.com/users?page=1&limit=10',
  {
    method: http.RequestMethod.GET,
    header: {
      'Content-Type': 'application/json'
    }
  },
  (err, data) => {
    // 回调处理
  }
);

// POST请求带请求体
httpRequest.request(
  'https://api.example.com/users',
  {
    method: http.RequestMethod.POST,
    header: {
      'Content-Type': 'application/json'
    },
    extraData: JSON.stringify({
      name: '小明',
      age: 25,
      email: 'xiaoming@example.com'
    })
  },
  (err, data) => {
    // 回调处理
  }
);

3. 超时配置

// 设置请求超时时间(单位:毫秒)
{
  method: http.RequestMethod.GET,
  header: {
    'Content-Type': 'application/json'
  },
  connectTimeout: 10000, // 连接超时10秒
  readTimeout: 10000     // 读取超时10秒
}

五、响应处理

1. 响应数据结构

httpRequest.request(
  'https://api.example.com/users',
  {
    method: http.RequestMethod.GET
  },
  (err, data) => {
    if (err) {
      console.error('请求失败:', err);
      return;
    }
    
    // 响应数据对象结构
    console.log('响应码:', data.responseCode);        // 响应状态码
    console.log('响应头:', data.header);              // 响应头信息
    console.log('响应体:', data.result);              // 响应体字符串
    console.log('响应Cookies:', data.cookies);        // 响应Cookies
    console.log('响应长度:', data.contentLength);     // 响应内容长度
    console.log('响应类型:', data.responseType);      // 响应类型
  }
);

2. 响应状态码处理

httpRequest.request(
  'https://api.example.com/users',
  {
    method: http.RequestMethod.GET
  },
  (err, data) => {
    if (err) {
      console.error('请求失败:', err);
      return;
    }
    
    switch (data.responseCode) {
      case 200:
        // 请求成功
        const result = JSON.parse(data.result);
        this.handleSuccess(result);
        break;
      case 400:
        console.error('请求参数错误');
        break;
      case 401:
        console.error('未授权,请重新登录');
        this.handleUnauthorized();
        break;
      case 403:
        console.error('禁止访问');
        break;
      case 404:
        console.error('资源不存在');
        break;
      case 500:
        console.error('服务器内部错误');
        break;
      default:
        console.error('未知错误,状态码:', data.responseCode);
    }
  }
);

3. 错误处理

httpRequest.request(
  'https://api.example.com/users',
  {
    method: http.RequestMethod.GET
  },
  (err, data) => {
    if (err) {
      // 网络错误处理
      switch (err.code) {
        case http.ResponseErrorCode.NETWORK_ERROR:
          console.error('网络连接失败,请检查网络设置');
          break;
        case http.ResponseErrorCode.TIMEOUT_ERROR:
          console.error('请求超时,请稍后重试');
          break;
        case http.ResponseErrorCode.CANCELED_ERROR:
          console.error('请求已取消');
          break;
        default:
          console.error('网络请求失败:', err);
      }
      return;
    }
    
    // 业务错误处理
    if (data.responseCode !== 200) {
      console.error('请求失败,状态码:', data.responseCode);
      return;
    }
    
    // 请求成功
    const result = JSON.parse(data.result);
    this.handleSuccess(result);
  }
);

六、请求取消

1. 取消请求

// 发起请求
const requestTask = httpRequest.request(
  'https://api.example.com/users',
  {
    method: http.RequestMethod.GET
  },
  (err, data) => {
    if (err) {
      if (err.code === http.ResponseErrorCode.CANCELED_ERROR) {
        console.log('请求已取消');
        return;
      }
      console.error('请求失败:', err);
      return;
    }
    console.log('请求成功:', data.result);
  }
);

// 取消请求
requestTask.abort();

2. 组件销毁时取消请求

@Component
struct UserList {
  private requestTask: http.HttpTask | null = null;

  aboutToAppear() {
    this.loadUsers();
  }

  aboutToDisappear() {
    // 组件销毁时取消请求
    if (this.requestTask) {
      this.requestTask.abort();
      this.requestTask = null;
    }
  }

  private loadUsers() {
    let httpRequest = http.createHttp();
    this.requestTask = httpRequest.request(
      'https://api.example.com/users',
      {
        method: http.RequestMethod.GET
      },
      (err, data) => {
        if (err) {
          if (err.code === http.ResponseErrorCode.CANCELED_ERROR) {
            console.log('请求已取消');
            return;
          }
          console.error('请求失败:', err);
          return;
        }
        console.log('请求成功:', data.result);
      }
    );
  }
}

七、网络状态监听

1. 监听网络状态变化

import { network } from '@ohos.net.network';

// 监听网络状态变化
network.on('netAvailable', (data) => {
  console.log('网络已连接:', data);
});

network.on('netUnavailable', () => {
  console.log('网络已断开');
});

network.on('netCapabilitiesChange', (data) => {
  console.log('网络能力变化:', data);
});

network.on('netConnectionPropertiesChange', (data) => {
  console.log('网络连接属性变化:', data);
});

2. 获取当前网络状态

// 获取当前网络状态
network.getDefaultNet((err, data) => {
  if (err) {
    console.error('获取网络状态失败:', err);
    return;
  }
  
  if (data) {
    console.log('网络已连接,类型:', data.netCapabilities.bearerTypes);
  } else {
    console.log('网络未连接');
  }
});

八、实战案例:封装网络请求工具类

1. 网络请求工具类

// src/main/ets/common/HttpUtil.ts
import http from '@ohos.net.http';

// 定义响应类型
interface HttpResponse {
  code: number;
  data: any;
  message: string;
}

// 定义请求配置
interface RequestOptions {
  url: string;
  method: http.RequestMethod;
  data?: any;
  header?: Record<string, string>;
  timeout?: number;
}

export class HttpUtil {
  private static instance: HttpUtil;
  private httpRequest: http.Http;

  private constructor() {
    this.httpRequest = http.createHttp();
  }

  public static getInstance(): HttpUtil {
    if (!HttpUtil.instance) {
      HttpUtil.instance = new HttpUtil();
    }
    return HttpUtil.instance;
  }

  // 发起请求
  public request(options: RequestOptions): Promise<HttpResponse> {
    return new Promise((resolve, reject) => {
      const { url, method, data, header, timeout } = options;

      const requestOptions: http.HttpRequestOptions = {
        method: method,
        header: {
          'Content-Type': 'application/json',
          ...header
        },
        connectTimeout: timeout || 10000,
        readTimeout: timeout || 10000
      };

      // 处理请求数据
      if (data && method !== http.RequestMethod.GET) {
        requestOptions.extraData = JSON.stringify(data);
      }

      // 处理GET请求参数
      let requestUrl = url;
      if (method === http.RequestMethod.GET && data) {
        const params = new URLSearchParams();
        Object.keys(data).forEach(key => {
          params.append(key, data[key]);
        });
        requestUrl = `${url}?${params.toString()}`;
      }

      this.httpRequest.request(
        requestUrl,
        requestOptions,
        (err, response) => {
          if (err) {
            reject(err);
            return;
          }

          if (response.responseCode === 200) {
            try {
              const result = JSON.parse(response.result);
              resolve(result);
            } catch (parseError) {
              reject(new Error('解析响应数据失败'));
            }
          } else {
            reject(new Error(`请求失败,状态码: ${response.responseCode}`));
          }
        }
      );
    });
  }

  // GET请求
  public get(url: string, params?: any, header?: Record<string, string>): Promise<HttpResponse> {
    return this.request({
      url: url,
      method: http.RequestMethod.GET,
      data: params,
      header: header
    });
  }

  // POST请求
  public post(url: string, data?: any, header?: Record<string, string>): Promise<HttpResponse> {
    return this.request({
      url: url,
      method: http.RequestMethod.POST,
      data: data,
      header: header
    });
  }

  // PUT请求
  public put(url: string, data?: any, header?: Record<string, string>): Promise<HttpResponse> {
    return this.request({
      url: url,
      method: http.RequestMethod.PUT,
      data: data,
      header: header
    });
  }

  // DELETE请求
  public delete(url: string, data?: any, header?: Record<string, string>): Promise<HttpResponse> {
    return this.request({
      url: url,
      method: http.RequestMethod.DELETE,
      data: data,
      header: header
    });
  }

  // 设置请求拦截器
  public setRequestInterceptor(interceptor: (config: RequestOptions) => RequestOptions) {
    // 实际场景:实现请求拦截器
  }

  // 设置响应拦截器
  public setResponseInterceptor(interceptor: (response: HttpResponse) => HttpResponse) {
    // 实际场景:实现响应拦截器
  }
}

2. 用户服务类

// src/main/ets/service/UserService.ts
import { HttpUtil } from '../common/HttpUtil';

export class UserService {
  private static instance: UserService;
  private httpUtil: HttpUtil;

  private constructor() {
    this.httpUtil = HttpUtil.getInstance();
  }

  public static getInstance(): UserService {
    if (!UserService.instance) {
      UserService.instance = new UserService();
    }
    return UserService.instance;
  }

  // 用户登录
  public async login(username: string, password: string): Promise<any> {
    try {
      const response = await this.httpUtil.post('https://api.example.com/login', {
        username: username,
        password: password
      });

      if (response.code === 200) {
        return response.data;
      } else {
        throw new Error(response.message || '登录失败');
      }
    } catch (error) {
      throw error;
    }
  }

  // 获取用户信息
  public async getUserInfo(userId: string): Promise<any> {
    try {
      const response = await this.httpUtil.get(`https://api.example.com/users/${userId}`);

      if (response.code === 200) {
        return response.data;
      } else {
        throw new Error(response.message || '获取用户信息失败');
      }
    } catch (error) {
      throw error;
    }
  }

  // 更新用户信息
  public async updateUserInfo(userId: string, userInfo: any): Promise<any> {
    try {
      const response = await this.httpUtil.put(`https://api.example.com/users/${userId}`, userInfo);

      if (response.code === 200) {
        return response.data;
      } else {
        throw new Error(response.message || '更新用户信息失败');
      }
    } catch (error) {
      throw error;
    }
  }
}

3. 商品服务类

// src/main/ets/service/ProductService.ts
import { HttpUtil } from '../common/HttpUtil';

export class ProductService {
  private static instance: ProductService;
  private httpUtil: HttpUtil;

  private constructor() {
    this.httpUtil = HttpUtil.getInstance();
  }

  public static getInstance(): ProductService {
    if (!ProductService.instance) {
      ProductService.instance = new ProductService();
    }
    return ProductService.instance;
  }

  // 获取商品列表
  public async getProductList(page: number = 1, limit: number = 10): Promise<any> {
    try {
      const response = await this.httpUtil.get('https://api.example.com/products', {
        page: page,
        limit: limit
      });

      if (response.code === 200) {
        return response.data;
      } else {
        throw new Error(response.message || '获取商品列表失败');
      }
    } catch (error) {
      throw error;
    }
  }

  // 获取商品详情
  public async getProductDetail(productId: string): Promise<any> {
    try {
      const response = await this.httpUtil.get(`https://api.example.com/products/${productId}`);

      if (response.code === 200) {
        return response.data;
      } else {
        throw new Error(response.message || '获取商品详情失败');
      }
    } catch (error) {
      throw error;
    }
  }

  // 搜索商品
  public async searchProducts(keyword: string, page: number = 1, limit: number = 10): Promise<any> {
    try {
      const response = await this.httpUtil.get('https://api.example.com/products/search', {
        keyword: keyword,
        page: page,
        limit: limit
      });

      if (response.code === 200) {
        return response.data;
      } else {
        throw new Error(response.message || '搜索商品失败');
      }
    } catch (error) {
      throw error;
    }
  }
}

九、实战案例:登录页面改造

1. 登录页面使用网络请求

// src/main/ets/pages/Login.ets
import router from '@ohos.router';
import { UserService } from '../service/UserService';
import { LoadingUtil } from '../common/LoadingUtil';

@Entry
@Component
struct Login {
  @State username: string = '';
  @State password: string = '';
  @State isLoading: boolean = false;

  build() {
    Column({ space: 20 }) {
      Text('用户登录')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .margin({ bottom: 40 })

      TextInput({ placeholder: '请输入用户名' })
        .width('90%')
        .height(50)
        .backgroundColor(Color.White)
        .borderRadius(8)
        .borderWidth(1)
        .borderColor('#E0E0E0')
        .padding({ left: 15, right: 15 })
        .onChange((value: string) => {
          this.username = value;
        })

      TextInput({ placeholder: '请输入密码' })
        .width('90%')
        .height(50)
        .backgroundColor(Color.White)
        .borderRadius(8)
        .borderWidth(1)
        .borderColor('#E0E0E0')
        .padding({ left: 15, right: 15 })
        .type(InputType.Password)
        .onChange((value: string) => {
          this.password = value;
        })

      Button('登录')
        .width('90%')
        .height(50)
        .backgroundColor('#007DFF')
        .fontColor(Color.White)
        .fontSize(18)
        .enabled(!this.isLoading)
        .onClick(() => {
          this.handleLogin();
        })

      if (this.isLoading) {
        Loading()
          .color('#007DFF')
          .margin({ top: 20 })
      }
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .backgroundColor('#F5F5F5')
  }

  // 处理登录
  private async handleLogin() {
    if (!this.username || !this.password) {
      promptAction.showToast({
        message: '请输入用户名和密码',
        duration: 2000
      });
      return;
    }

    this.isLoading = true;

    try {
      const userService = UserService.getInstance();
      const userInfo = await userService.login(this.username, this.password);

      // 登录成功,保存用户信息
      await this.saveUserInfo(userInfo);

      // 跳转到首页
      router.replaceUrl({
        url: 'pages/Home',
        params: {
          userId: userInfo.id,
          userName: userInfo.name
        }
      });

      promptAction.showToast({
        message: '登录成功',
        duration: 2000
      });
    } catch (error) {
      console.error('登录失败:', error);
      promptAction.showToast({
        message: error.message || '登录失败,请稍后重试',
        duration: 2000
      });
    } finally {
      this.isLoading = false;
    }
  }

  // 保存用户信息
  private async saveUserInfo(userInfo: any) {
    // 实际场景:保存用户信息到本地存储
    // 这里使用模拟保存
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve(true);
      }, 100);
    });
  }
}

2. 首页使用网络请求

// src/main/ets/pages/Home.ets
import router from '@ohos.router';
import { UserService } from '../service/UserService';
import { ProductService } from '../service/ProductService';

@Entry
@Component
struct Home {
  @State userId: string = '';
  @State userName: string = '';
  @State userInfo: any = {};
  @State productList: any[] = [];
  @State isLoading: boolean = false;

  aboutToAppear() {
    const params = router.getParams();
    this.userId = params?.['userId'] || '';
    this.userName = params?.['userName'] || '';
    
    this.loadUserInfo();
    this.loadProductList();
  }

  build() {
    Column({ space: 20 }) {
      Text(`欢迎回来,${this.userName}`)
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .margin({ bottom: 20 })

      if (this.userInfo.balance !== undefined) {
        Text(`账户余额: ${this.userInfo.balance.toFixed(2)}元`)
          .fontSize(18)
          .margin({ bottom: 20 })
      }

      if (this.isLoading) {
        Loading()
          .color('#007DFF')
          .margin({ bottom: 20 })
      }

      if (this.productList.length > 0) {
        Text('推荐商品')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .margin({ bottom: 10 })

        ForEach(this.productList, (item: any) => {
          Column() {
            Text(item.name)
              .fontSize(16)
            Text(`¥${item.price.toFixed(2)}`)
              .fontSize(14)
              .fontColor('#FF3B30')
          }
          .width('100%')
          .padding(10)
          .backgroundColor(Color.White)
          .borderRadius(8)
          .margin({ bottom: 10 })
        })
      }

      Button('刷新数据')
        .width(200)
        .onClick(() => {
          this.refreshData();
        })

      Button('退出登录')
        .width(200)
        .backgroundColor(Color.Red)
        .onClick(() => {
          this.logout();
        })
    }
    .width('100%')
    .height('100%')
    .padding(16)
    .backgroundColor('#F5F5F5')
  }

  // 加载用户信息
  private async loadUserInfo() {
    if (!this.userId) {
      return;
    }

    try {
      const userService = UserService.getInstance();
      const userInfo = await userService.getUserInfo(this.userId);
      this.userInfo = userInfo;
    } catch (error) {
      console.error('获取用户信息失败:', error);
      promptAction.showToast({
        message: '获取用户信息失败,请稍后重试',
        duration: 2000
      });
    }
  }

  // 加载商品列表
  private async loadProductList() {
    this.isLoading = true;

    try {
      const productService = ProductService.getInstance();
      const result = await productService.getProductList(1, 10);
      this.productList = result.list || [];
    } catch (error) {
      console.error('获取商品列表失败:', error);
      promptAction.showToast({
        message: '获取商品列表失败,请稍后重试',
        duration: 2000
      });
    } finally {
      this.isLoading = false;
    }
  }

  // 刷新数据
  private async refreshData() {
    this.isLoading = true;
    await Promise.all([
      this.loadUserInfo(),
      this.loadProductList()
    ]);
    this.isLoading = false;
  }

  // 退出登录
  private logout() {
    promptAction.showDialog({
      title: '确认退出登录',
      message: '确定要退出登录吗?',
      buttons: [
        { text: '取消', color: '#666666' },
        { text: '确定', color: '#FF3B30' }
      ]
    }).then((result) => {
      if (result.index === 1) {
        // 清除登录状态
        this.clearLoginStatus();
        // 跳转到登录页
        router.replaceUrl({
          url: 'pages/Login'
        });
      }
    });
  }

  // 清除登录状态
  private clearLoginStatus() {
    // 实际场景:清除本地存储的登录状态
  }
}

十、最佳实践与注意事项

1. 网络请求最佳实践

使用Promise封装

// ✅ 推荐:使用Promise封装网络请求
public async getData(): Promise<any> {
  return new Promise((resolve, reject) => {
    httpRequest.request(
      'https://api.example.com/data',
      {
        method: http.RequestMethod.GET
      },
      (err, data) => {
        if (err) {
          reject(err);
          return;
        }
        resolve(data);
      }
    );
  });
}

错误处理统一

// ✅ 推荐:统一错误处理
try {
  const result = await this.getData();
  this.handleSuccess(result);
} catch (error) {
  this.handleError(error);
}

请求取消机制

// ✅ 推荐:组件销毁时取消请求
aboutToDisappear() {
  if (this.requestTask) {
    this.requestTask.abort();
    this.requestTask = null;
  }
}

2. 性能优化建议

避免重复请求

// ❌ 不推荐:每次build都发起请求
build() {
  this.loadData(); // 会导致重复请求
}

// ✅ 推荐:在生命周期方法中发起请求
aboutToAppear() {
  this.loadData(); // 只会在组件创建时执行一次
}

合理使用缓存

// ✅ 推荐:使用缓存减少网络请求
private cachedData: any = null;

private async loadData() {
  if (this.cachedData) {
    this.data = this.cachedData;
    return;
  }
  
  const result = await this.getData();
  this.cachedData = result;
  this.data = result;
}

分页加载

// ✅ 推荐:分页加载数据
private async loadMore() {
  if (this.isLoading || this.isEnd) {
    return;
  }
  
  this.isLoading = true;
  try {
    const result = await this.getData(this.page + 1);
    this.dataList = [...this.dataList, ...result.list];
    this.page++;
    this.isEnd = result.isEnd;
  } catch (error) {
    console.error('加载更多失败:', error);
  } finally {
    this.isLoading = false;
  }
}

3. 安全注意事项

HTTPS加密

// ✅ 推荐:使用HTTPS协议
const url = 'https://api.example.com/data'; // 使用HTTPS

// ❌ 不推荐:使用HTTP协议
const url = 'http://api.example.com/data'; // 不安全

敏感信息保护

// ✅ 推荐:敏感信息不存储在代码中
const token = await this.getTokenFromStorage(); // 从安全存储获取

// ❌ 不推荐:敏感信息硬编码
const token = 'hardcoded_token'; // 不安全

输入验证

// ✅ 推荐:验证用户输入
private validateInput(username: string, password: string): boolean {
  if (!username || !password) {
    return false;
  }
  
  if (username.length < 3 || username.length > 20) {
    return false;
  }
  
  if (password.length < 6) {
    return false;
  }
  
  return true;
}

十一、总结与行动建议

核心要点回顾

  1. 网络模块导入import http from '@ohos.net.http'
  2. 发起请求:使用http.createHttp().request()方法
  3. 请求方法GETPOSTPUTDELETE
  4. 请求配置:请求头、请求体、超时时间等
  5. 响应处理:状态码判断、错误处理、数据解析
  6. 请求取消:使用abort()方法取消请求
  7. 网络状态监听:监听网络连接状态变化
  8. 最佳实践:Promise封装、错误处理、性能优化、安全防护

行动建议

  1. 动手实践:按照本文示例,完成登录页和首页的网络请求改造
  2. 封装工具类:封装自己的网络请求工具类,支持请求拦截器和响应拦截器
  3. 错误处理:添加完整的错误处理逻辑,包括网络错误和业务错误
  4. 性能优化:实现数据缓存、分页加载、请求取消等功能
  5. 安全防护:使用HTTPS协议、验证用户输入、保护敏感信息
  6. 测试验证:测试不同网络环境下的请求表现(正常网络、弱网、断网)

通过本篇文章的学习,你已经掌握了HarmonyOS网络请求与数据获取的核心能力。下一篇文章将深入讲解本地数据存储,帮助你实现数据的本地持久化存储。

posted @ 2025-12-23 23:16  J_____P  阅读(0)  评论(0)    收藏  举报