期末项目黑马健康5(完结)

期末项目黑马健康5



前言

这篇用来介绍如何实现数据模型–饮食记录、数据模型–通用DB工具


一、项目名称:黑马健康

  • “黑马健康”致力于为用户提供个性化的营养饮食指导,帮助用户实现健康饮食。它集成了食物营养和热量数据查询、定制食谱、饮食分析、健康社区等功能,为用户提供全方位的健康管理服务。

二、应用运行过程

1.数据模型–饮食记录

  • 我们在设计数据模型的时候,除了要设计饮食记录的模型以外,饮食记录的类型也应该有自己的数据模型,早餐,午餐,晚餐,加餐和运动都是类型。
    饮食记录的数据操作模型需要用到数据库。
    在这里插入图片描述

效果展示

在这里插入图片描述

代码

RecordType.ets

代码如下(示例):

export default class RecordType{
  /**
   * 类型id
   */
  id: number
  /**
   * 类型名称
   */
  name: ResourceStr
  /**
   * 类型图标
   */
  icon: ResourceStr
  /**
   * 类型推荐最小卡路里
   */
  min: number
  /**
   * 类型推荐最大卡路里
   */
  max: number

  constructor(id: number, name: ResourceStr, icon: ResourceStr, min: number = 0, max: number = 0) {
    this.id = id
    this.name = name
    this.icon = icon
    this.min = min
    this.max = max
  }
}

RecordType.ets

import RecordItem from './RecordItem'

/**
 * 饮食记录的页面数据模型
 */
export default class RecordVO {
  /**
   * 记录id
   */
  id: number
  /**
   * 饮食记录类型
   */
  typeId: number

  /**
   * 卡路里总数
   */
  calorie: number

  /**
   * 记录中的食物或运动信息
   */
  recordItem: RecordItem

  /**
   * 食物数量或运动时长,如果是运动信息则无
   */
  amount: number = 0
}

RecordPO.ets

export default class RecordPO{
  /**
   * 记录id
   */
  id: number
  /**
   * 饮食记录类型
   */
  typeId: number
  /**
   * 记录中的食物或运动信息
   */
  itemId: number

  /**
   * 食物数量或运动时长,如果是运动信息则无
   */
  amount: number
  /**
   * 记录的日期
   */
  createTime: number

}

RecordTypeModel.ets

import RecordType from '../viewmodel/RecordType'

enum RecordTypeEnum {
  /**
   * 早餐
   */
  BREAKFAST,
  /**
   * 午餐
   */
  LUNCH,
  /**
   * 晚餐
   */
  DINNER,
  /**
   * 加餐
   */
  EXTRA_MEAL,
  /**
   * 运动
   */
  WORKOUT
}

/**
 * 记录类型常量
 */
const RecordTypes: RecordType[] = [
  new RecordType(0, $r("app.string.breakfast"), $r("app.media.ic_breakfast"), 423, 592),
  new RecordType(1, $r("app.string.lunch"), $r("app.media.ic_lunch"), 592, 761),
  new RecordType(2, $r("app.string.dinner"), $r("app.media.ic_dinner"), 423, 592),
  new RecordType(3, $r("app.string.extra_meal"), $r("app.media.ic_extra_m"), 0, 169),
  new RecordType(4, $r("app.string.workout"), $r("app.media.ic_workout")),
]

export {RecordTypes, RecordTypeEnum}

RecordVO.ets

import RecordItem from './RecordItem'

/**
 * 饮食记录的页面数据模型
 */
export default class RecordVO {
  /**
   * 记录id
   */
  id: number
  /**
   * 饮食记录类型
   */
  typeId: number

  /**
   * 卡路里总数
   */
  calorie: number

  /**
   * 记录中的食物或运动信息
   */
  recordItem: RecordItem

  /**
   * 食物数量或运动时长,如果是运动信息则无
   */
  amount: number = 0
}

2.数据模型–通用DB工具

  • 来完成饮食记录的数据操作,model的开发。

代码

RecordModel.ets
代码如下(示例):

/**
 * 数据库建表语句
 */
import relationalStore from '@ohos.data.relationalStore'
import { ColumnInfo, ColumnType } from '../common/bean/ColumnInfo'
import RecordPO from '../common/bean/RecordPO'
import DbUtil from '../common/utils/DbUtil'
const CREATE_TABLE_SQL: string = `
 CREATE TABLE IF NOT EXISTS record (
   id INTEGER PRIMARY KEY AUTOINCREMENT,
   type_id INTEGER NOT NULL,
   item_id INTEGER NOT NULL,
   amount DOUBLE NOT NULL,
   create_time INTEGER NOT NULL
 )
 `

const COLUMNS: ColumnInfo[] = [
  {name: 'id', columnName: 'id', type: ColumnType.LONG},
  {name: 'typeId', columnName: 'type_id', type: ColumnType.LONG},
  {name: 'itemId', columnName: 'item_id', type: ColumnType.LONG},
  {name: 'amount', columnName: 'amount', type: ColumnType.DOUBLE},
  {name: 'createTime', columnName: 'create_time', type: ColumnType.LONG}
]

const TABLE_NAME = 'record'
const ID_COLUMN = 'id'
const DATE_COLUMN = 'create_time'

class RecordModel {
  getCreateTableSql(): string {
    return CREATE_TABLE_SQL
  }

  insert(record: RecordPO): Promise<number>{
    return DbUtil.insert(TABLE_NAME, record, COLUMNS)
  }

  deleteById(id: number): Promise<number>{
    // 1.删除条件
    let predicates = new relationalStore.RdbPredicates(TABLE_NAME)
    predicates.equalTo(ID_COLUMN, id)
    // 2.删除
    return DbUtil.delete(predicates)
  }

  listByDate(date: number): Promise<RecordPO[]>{
    // 1.查询条件
    let predicates = new relationalStore.RdbPredicates(TABLE_NAME)
    predicates.equalTo(DATE_COLUMN, date)
    // 2.查询
    return DbUtil.queryForList(predicates, COLUMNS)
  }
}

let recordModel = new RecordModel()

export default recordModel as RecordModel

DbUtil.ts

import common from '@ohos.app.ability.common';
import relationalStore from '@ohos.data.relationalStore';
import { ColumnInfo, ColumnType } from '../bean/ColumnInfo';
import Logger from './Logger';

const DB_FILENAME: string = 'HeiMaHealthy.db'

class DbUtil {
  rdbStore: relationalStore.RdbStore

  initDB(context: common.UIAbilityContext): Promise<void> {
    let config: relationalStore.StoreConfig = {
      name: DB_FILENAME,
      securityLevel: relationalStore.SecurityLevel.S1
    }
    return new Promise<void>((resolve, reject) => {
      relationalStore.getRdbStore(context, config)
        .then(rdbStore => {
          this.rdbStore = rdbStore
          Logger.debug('rdbStore 初始化完成!')
          resolve()
        })
        .catch(reason => {
          Logger.debug('rdbStore 初始化异常', JSON.stringify(reason))
          reject(reason)
        })
    })
  }
//创建数据表
  createTable(createSQL: string): Promise<void> {
    return new Promise((resolve, reject) => {
      this.rdbStore.executeSql(createSQL)
        .then(() => {
          Logger.debug('创建表成功', createSQL)
          resolve()
        })
        .catch(err => {
          Logger.error('创建表失败,' + err.message, JSON.stringify(err))
          reject(err)
        })
    })
  }
//向指定表中插入数据。
  insert(tableName: string, obj: any, columns: ColumnInfo[]): Promise<number> {
    return new Promise((resolve, reject) => {
      // 1.构建新增数据
      let value = this.buildValueBucket(obj, columns)
      // 2.新增
      this.rdbStore.insert(tableName, value, (err, id) => {
        if (err) {
          Logger.error('新增失败!', JSON.stringify(err))
          reject(err)
        } else {
          Logger.debug('新增成功!新增id:', id.toString())
          resolve(id)
        }
      })
    })
  }
//根据条件删除数据。
  // 参数:predicates 是用于定义删除条件的对象。
  delete(predicates: relationalStore.RdbPredicates): Promise<number> {
    return new Promise((resolve, reject) => {
      this.rdbStore.delete(predicates, (err, rows) => {
        if (err) {
          Logger.error('删除失败!', JSON.stringify(err))
          reject(err)
        } else {
          Logger.debug('删除成功!删除行数:', rows.toString())
          resolve(rows)
        }
      })
    })
  }
  //方法是用来从数据库中查询数据并返回一个指定类型 T 的数组
  // 一个 RdbPredicates 对象,定义了查询的条件。
  //一个 ColumnInfo 对象的数组,定义了要查询的列信息。
  queryForList<T>(predicates: relationalStore.RdbPredicates, columns: ColumnInfo[]): Promise<T[]> {
    return new Promise((resolve, reject) => {
      //使用 rdbStore.query 方法执行查询。
      this.rdbStore.query(predicates, columns.map(info => info.columnName), (err, result) => {
        if (err) {
          Logger.error('查询失败!', JSON.stringify(err))
          reject(err)
        } else {
          Logger.debug('查询成功!查询行数:', result.rowCount.toString())
          resolve(this.parseResultSet(result, columns))
        }
      })
    })
  }
//用于将数据库查询的结果集(ResultSet)解析为指定类型 T 的数组。
  parseResultSet<T> (result: relationalStore.ResultSet, columns: ColumnInfo[]): T[] {
    // 1.声明最终返回的结果
    let arr = []
    // 2.判断是否有结果
    if (result.rowCount <= 0) {
      return arr
    }
    // 3.处理结果
    while (!result.isAtLastRow) {
      // 3.1.去下一行 goToNextRow 方法将结果集的指针移动到下一行。
      result.goToNextRow()
      // 3.2.解析这行数据,转为对象
      let obj = {}
      columns.forEach(info => {
        let val = null
        switch (info.type) {
          case ColumnType.LONG:
            val = result.getLong(result.getColumnIndex(info.columnName))
            break
          case ColumnType.DOUBLE:
            val = result.getDouble(result.getColumnIndex(info.columnName))
            break
          case ColumnType.STRING:
            val = result.getString(result.getColumnIndex(info.columnName))
            break
          case ColumnType.BLOB:
            val = result.getBlob(result.getColumnIndex(info.columnName))
            break
        }
        obj[info.name] = val
      })
      // 3.3.将对象填入结果数组
      arr.push(obj)
      Logger.debug('查询到数据:', JSON.stringify(obj))
    }
    return arr
  }
//buildValueBucket 它的目的是从 obj 中提取与 columns 中列名相关的信息,并将这些信息组织成一个新的对象 value。
  buildValueBucket(obj: any, columns: ColumnInfo[]): relationalStore.ValuesBucket {
    let value = {}
    //forEach 方法遍历 columns 数组中的每一个 ColumnInfo 对象。
    columns.forEach(info => {
      let val = obj[info.name]
      //检查 val 是否为 undefined。如果不是 undefined,则将值添加到 value 对象中。
      if (typeof val !== 'undefined') {
        value[info.columnName] = val
      }
    })
    return value
  }
}
//这段代码定义了一个用于从给定对象中 提取 与指定列相关的信息 并返回一个新对象的函数 buildValueBucket

let dbUtil: DbUtil = new DbUtil();

export default dbUtil as DbUtil

EntryAbility.ets

import UIAbility from '@ohos.app.ability.UIAbility';
import hilog from '@ohos.hilog';
import window from '@ohos.window';
import { CommonConstants } from '../common/constants/CommonConstants';
import DateUtil from '../common/utils/DateUtil';
import DbUtil from '../common/utils/DbUtil';
import PreferenceUtil from '../common/utils/PreferenceUtil';
import RecordModel from '../model/RecordModel';

export default class EntryAbility extends UIAbility {
  async onCreate(want, launchParam) {
    // 1.加载用户首选项
    PreferenceUtil.loadPreference(this.context)
    // 2.初始化日期
    AppStorage.SetOrCreate(CommonConstants.RECORD_DATE, DateUtil.beginTimeOfDay(new Date()))
    // 3.初始化RDB工具
    await DbUtil.initDB(this.context)
    // 4.创建record表
    DbUtil.createTable(RecordModel.getCreateTableSql())

    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate');
  }

  onDestroy() {
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onDestroy');
  }

  onWindowStageCreate(windowStage: window.WindowStage) {
    // Main window is created, set main page for this ability
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate');

    windowStage.loadContent('pages/WelcomePage', (err, data) => {
      if (err.code) {
        hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '');
        return;
      }
      hilog.info(0x0000, 'testTag', 'Succeeded in loading the content. Data: %{public}s', JSON.stringify(data) ?? '');
    });
  }

  onWindowStageDestroy() {
    // Main window is destroyed, release UI related resources
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageDestroy');
  }

  onForeground() {
    // Ability has brought to foreground
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onForeground');
  }

  onBackground() {
    // Ability has back to background
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onBackground');
  }
}

3.饮食记录业务层开发

  • 利用饮食记录model操作对象的增删改查来完成页面上的互动,实现饮食记录的这种持久化保存。用groupInfo实现数据的分组,StatsInfo来统计当日的卡路里和运动消耗等。
    在这里插入图片描述

代码

  • 实现了把数据库查询出来的数据转换成页面所需要的数据接口。
    GroupInfo.ets
export default class GroupInfo<TYPE, ELEMENT> {
  /**
   * 分组类型
   */
  type: TYPE
  /**
   * 组内数据集合
   */
  items: ELEMENT[]

  /**
   * 组内记录的总热量
   */
  calorie: number = 0

  constructor(type: TYPE, items: ELEMENT[]) {
    this.type = type
    this.items = items
  }
}

RecordService.ets

import RecordPO from '../common/bean/RecordPO'
import DateUtil from '../common/utils/DateUtil'
import ItemModel from '../model/ItemModel'
import RecordModel from '../model/RecordModel'
import { RecordTypeEnum, RecordTypes } from '../model/RecordTypeModel'
import GroupInfo from '../viewmodel/GroupInfo'
import RecordType from '../viewmodel/RecordType'
import RecordVO from '../viewmodel/RecordVO'
import StatsInfo from '../viewmodel/StatsInfo'
class RecordService {
  /**
   * 新增饮食记录
   * @param typeId 记录类型id
   * @param itemId 记录项id
   * @param amount 记录项数量(食物量、运动时长)
   * @returns 新增数量
   */
  insert(typeId: number, itemId: number, amount: number): Promise<number>{
    // 1.获取时间
    let createTime = (AppStorage.Get('selectedDate') || DateUtil.beginTimeOfDay(new Date())) as number
    // 2.新增
    return RecordModel.insert({typeId, itemId, amount, createTime})
  }

  /**
   * 根据id删除饮食记录
   * @param id 记录id
   * @returns 删除条数
   */
  deleteById(id: number): Promise<number>{
    return RecordModel.deleteById(id)
  }

  /**
   * 根据日期查询饮食记录列表
   * @param date 要查询的日期
   * @returns 记录列表
   */
  async queryRecordByDate(date: number): Promise<RecordVO[]>{
    // 1.查询数据库的RecordPO
    let rps = await RecordModel.listByDate(date)
    // 2.将RecordPO转为RecordVO
    return rps.map(rp => {
      // 2.1.获取po中的基本属性
      let rv = {id: rp.id, typeId: rp.typeId, amount: rp.amount} as RecordVO
      // 2.2.查询记录项
      rv.recordItem = ItemModel.getById(rp.itemId, rp.typeId !== RecordTypeEnum.WORKOUT)
      // 2.3.计算热量
      rv.calorie = rp.amount * rv.recordItem.calorie
      return rv
    })
  }

  /**
   * 根据记录列表信息统计出热量、营养素信息
   * @param records 饮食记录列表
   * @returns 热量、营养素信息
   */
  calculateStatsInfo(records: RecordVO[]): StatsInfo{
    // 1.准备结果
    let info = new StatsInfo()
    if(!records || records.length <= 0){
      return info
    }
    // 2.计算统计数据
    records.forEach(r => {
      if(r.typeId === RecordTypeEnum.WORKOUT){
        // 运动,累加消耗热量
        info.expend += r.calorie
      }else{
        // 食物,累加摄入热量、蛋白质、碳水、脂肪
        info.intake += r.calorie
        info.carbon += r.recordItem.carbon
        info.protein += r.recordItem.protein
        info.fat += r.recordItem.fat
      }
    })
    // 3.返回
    return info
  }

  /**
   * 将记录列表按照记录类型分组
   * @param records 记录列表
   * @returns 分组记录信息
   */
  calculateGroupInfo(records: RecordVO[]): GroupInfo<RecordType, RecordVO>[]{
    // 1.创建空的记录类型分组
    let groups = RecordTypes.map(recordType => new GroupInfo(recordType, []))
    if(!records || records.length <= 0){
      return groups
    }
    // 2.遍历所有饮食记录,
    records.forEach(record => {
      // 2.1.把每个记录存入其对应类型的分组中
      groups[record.typeId].items.push(record)
      // 2.2.计算该组的总热量
      groups[record.typeId].calorie += record.calorie
    })
    return groups
  }
}

let recordService = new RecordService()

export default recordService as RecordService

4.实现数据持久化和页面交互

  • 利用前面的功能来实现饮食记录页面的交互,最终完成数据的持久化保存。

效果展示

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

代码

RecordIndex.ets

import DateUtil from '../../common/utils/DateUtil'
import RecordService from '../../service/RecordService'
import RecordVO from '../../viewmodel/RecordVO'
import RecordList from './RecordList'
import SearchHeader from './SearchHeader'
import StatsCard from './StatsCard'
@Component
export default struct RecordIndex {

  @StorageProp('selectedDate')
  @Watch('aboutToAppear')
  selectedDate: number = DateUtil.beginTimeOfDay(new Date())

  @Provide records: RecordVO[] = []

  @Prop @Watch('handlePageShow') isPageShow: boolean

  handlePageShow(){
    if(this.isPageShow){
      this.aboutToAppear()
    }
  }

  async aboutToAppear(){
    this.records = await RecordService.queryRecordByDate(this.selectedDate)
  }

  build() {
    Column(){
      // 1.头部搜索栏
      SearchHeader()
      // 2.统计卡片
      StatsCard()
      // 3.记录列表
      RecordList()
        .layoutWeight(1)
    }
    .width('100%')
    .height('100%')
    .backgroundColor($r('app.color.index_page_background'))
  }
}

RecordList.ets

import router from '@ohos.router'
import RecordPO from '../../common/bean/RecordPO'
import { CommonConstants } from '../../common/constants/CommonConstants'
import RecordService from '../../service/RecordService'
import GroupInfo from '../../viewmodel/GroupInfo'
import RecordType from '../../viewmodel/RecordType'
import RecordVO from '../../viewmodel/RecordVO'

@Extend(Text) function grayText(){//1.分组的标题 中 Text单独样式
  .fontSize(14)
  .fontColor($r('app.color.light_gray'))

}

@Component
export default struct RecordList {
  @Consume @Watch('handleRecordsChange')records:RecordVO[]
  @State groups:GroupInfo<RecordType,RecordVO>[]=[]
  handleRecordsChange(){
    this.groups=RecordService.calculateGroupInfo(this.records)
  }
  build() {
    List({space:CommonConstants.SPACE_10}){//上下有间隔
      ForEach(this.groups,(group:GroupInfo<RecordType,RecordVO>)=>{
        ListItem(){
          Column() {
            //1.分组的标题
            Row({ space: CommonConstants.SPACE_4 }) {
              Image(group.type.icon).width(24)
              Text(group.type.name).fontSize(18).fontWeight(CommonConstants.FONT_WEIGHT_700)
              Text('建议${group.type.min}~${group.type.min}千卡').grayText()
              Blank() //空白
              Text('group.calorie.toFixed(0)').fontSize(14).fontColor($r('app.color.light_primary_color'))
              Text('千卡').grayText()
              Image($r('app.media.ic_public_add_norm_filled'))
                .width(20)
                .fillColor($r('app.color.primary_color'))
            }
            .width('100%')
            .onClick(()=>{
              router.pushUrl({
                url:'pages/ItemIndex',
                params:{type:group.type}

              })
            })

            //2.组内记录列表
            List() {
              ForEach(group.items, (item:RecordVO) => {
                ListItem(){
                  Row({space:CommonConstants.SPACE_4}){
                    Image(item.recordItem.image).width(50)
                    Column({space:CommonConstants.SPACE_4}){
                      Text(item.recordItem.name).fontWeight(CommonConstants.FONT_WEIGHT_500)
                      Text('${item.amount}${item.recordItem.unit}}').grayText()
                    }
                    Blank()
                    Text('${item.calorie}千卡').grayText()
                  }
                  .width('100%')
                  .padding(CommonConstants.SPACE_6)
                }.swipeAction({end:this.delectButton.bind(this)})//左滑显示删除按钮


              })
            }
          }
          .width('100%')
          .backgroundColor(Color.White)
          .borderRadius(CommonConstants.DEFAULT_18)//边框弧度
          .padding(CommonConstants.SPACE_12)//边距 内容不贴边
        }
      })
    }
    .width(CommonConstants.THOUSANDTH_940)
    .margin({top:10})
    .height('100%')
  }
  @Builder delectButton(){
    Image($r('app.media.ic_public_delete_filled'))
      .width(20)
      .fillColor(Color.Red)
      .margin(5)
  }
}

  • 在RecordList中完成跳转,跳转到ItemIndex
.onClick(()=>{
              router.pushUrl({
                url:'pages/ItemIndex',
                params:{type:group.type}

              })
            })
  • 在ItemIndex中,写取出跳转时传给的参数,要写一个生命周期钩子onPageShow(),去获取传给的参数,通过router.getParams()。
onPageShow(){
    // 1.获取跳转时的参数
    let params: any = router.getParams()
    // 2.获取点击的饮食记录类型
    this.type = params.type
    this.isFood = this.type.id !== RecordTypeEnum.WORKOUT
  }
  • 实现持久化保存:
.onClick(() => {
          // 1.持久化保存
          RecordService.insert(this.type.id, this.item.id, this.amount)
            .then(() => {
              // 2.关闭弹窗
              this.showPanel = false
            })
        })

总结

1.数据模型–饮食记录

  • 根据饮食记录数据模型来编写数据库字段,数据库表结构,建立表。

2.数据模型–通用DB工具

  • 可根据日期条件查询出来,代码如下
listByDate(date: number): Promise<RecordPO[]>{
    // 1.查询条件
    let predicates = new relationalStore.RdbPredicates(TABLE_NAME)
    predicates.equalTo(DATE_COLUMN, date)
    // 2.查询
    return DbUtil.queryForList(predicates, COLUMNS)
  }
}

3.饮食记录业务层开发

  • 在RecordService.ets中,
    将记录列表按照记录类型分组
    @param records 记录列表
    @returns 分组记录信息
    用forEach遍历所有饮食记录
records.forEach(record => {
      // 2.1.把每个记录存入其对应类型的分组中
      groups[record.typeId].items.push(record)
      // 2.2.计算该组的总热量
      groups[record.typeId].calorie += record.calorie
    })

4.实现数据持久化和页面交互

  • handleRecordsChange用于在前端框架中响应某些数据变化(例如,当“records”数据发生变化时)并更新相关的统计信息或视图。
handleRecordsChange(){
    this.info = RecordService.calculateStatsInfo(this.records)
  }
  • 在触发页面渲染时,例如:要把StatsBuilder里面的参数变成传递引用。
Builder StatsBuilder($$:{label: string, value: number, tips?: string}){
    Column({space: CommonConstants.SPACE_6}){
      Text($$.label)
        .fontColor($r('app.color.gray'))
        .fontWeight(CommonConstants.FONT_WEIGHT_600)
      Text($$.value.toFixed(0))
        .fontSize(20)
        .fontWeight(CommonConstants.FONT_WEIGHT_700)
      if($$.tips){
        Text($$.tips)
          .fontSize(12)
          .fontColor($r('app.color.light_gray'))
      }
    }
  • 8
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值