1. 项目背景与核心需求

最近在帮朋友做一个饮品店的点餐系统,发现鸿蒙的本地数据库方案特别适合这种需要快速响应、离线使用的场景。这个系统最核心的部分就是数据管理——既要处理用户信息、饮品数据,还要管理购物车和订单。传统方案可能会用网络请求+远程数据库,但对于小型饮品店来说,本地存储反而更简单可靠。

鸿蒙的RDB(关系型数据库)支持完整的SQL语法,配合ArkUI的装饰器可以轻松实现数据绑定。我在实际开发中发现,合理设计数据库表结构和数据模型类,能减少30%以上的重复代码。比如把饮品规格设计成JSON格式存储,就省去了多个字段的关联查询。

2. 数据库设计与实现

2.1 表结构设计思路

先来看最关键的三个表:

  • 用户表(user):存账号密码和昵称
  • 购物车表(buy_car):记录用户选择的饮品及规格
  • 订单表(bought):存储已完成交易

这里有个设计技巧:购物车表使用复合主键(Ctime+Uno),这样同一个用户在不同时间加入的相同饮品会被视为不同记录。实测中发现,如果只用饮品ID作为主键,修改数量时会出现覆盖问题。

建表SQL示例:

create table if not exists buy_car(
  Ctime varchar(50),  -- 时间戳作主键一部分
  Uno varchar(20),    -- 关联用户
  Tname varchar(50),  -- 饮品名称
  Csum int,           -- 数量
  Ccup int,           -- 杯型(0中杯/1大杯)
  Ctemp int,          -- 温度(0冰/1热)
  Csweetness int,     -- 甜度(0-100%)
  Ctno int,           -- 饮品类型索引
  primary key(Ctime,Uno),
  FOREIGN KEY (Uno) REFERENCES user(Uno)
)

2.2 数据库初始化实战

初始化时最容易踩的坑是字段类型。有次我把varchar误写成verchar,调试了两小时才发现问题。建议按这个流程操作:

  1. 创建RDBStore实例:
const config = {
  name: 'DrinkOrder.db',
  securityLevel: relationalStore.SecurityLevel.S1 
}
relationalStore.getRdbStore(context, config, (err, store) => {
  this.rdbStore = store
})
  1. 执行建表语句时一定要加if not exists,避免重复创建报错:
const createUserSql = `create table if not exists user(...)`
rdbStore.executeSql(createUserSql)
  1. 建议封装初始化方法,在应用启动时调用:
initDatabase() {
  // 建表逻辑
  console.log('数据库就绪') // 重要调试点
}

3. 核心数据操作封装

3.1 用户管理模块

用户登录验证是个典型场景,要注意异步查询的处理:

async selectUser(Uno: string, Upassword: string): Promise<boolean> {
  let predicates = new relationalStore.RdbPredicates('user')
  predicates.equalTo('Uno', Uno)
  
  let res = await this.rdbStore.query(predicates)
  while(!res.isAtLastRow) {
    res.goToNextRow()
    if(res.getString(res.getColumnIndex('Upassword')) == Upassword) {
      return true  // 验证通过
    }
  }
  return false
}

密码修改操作需要特别注意数据更新时机。我最初直接调用update没加await,导致有时修改不生效。正确做法:

async updatePassword(Uno, newPassword) {
  let data = {'Upassword': newPassword}
  let predicates = new relationalStore.RdbPredicates('user')
  predicates.equalTo('Uno', Uno)
  await this.rdbStore.update(data, predicates) // 必须await
}

3.2 购物车功能实现

购物车的难点在于处理饮品规格组合。我的方案是将规格打包成JSON对象:

// 添加饮品到购物车
async addToCart(tea: Tea) {
  return await this.rdbStore.insert('buy_car', {
    Ctime: new Date().toISOString(),
    Uno: User.Uno,
    Tname: tea.name,
    Csum: tea.sum,
    Ccup: tea.norms.cup,
    Ctemp: tea.norms.temp,
    Csweetness: tea.norms.sweetness,
    Ctno: tea.no
  })
}

查询购物车时,需要将数据库记录转成Tea对象。这里有个性能优化点:避免在循环中频繁访问静态数据:

async getCartItems(): Promise<Tea[]> {
  let teas: Tea[] = []
  let res = await this.rdbStore.query(...)
  
  while(!res.isAtLastRow) {
    res.goToNextRow()
    let staticTea = StaticValue.teas[res.getLong('Ctno')] // 先获取静态数据
    
    let tea = new Tea(
      res.getString('Tname'),
      staticTea.image,
      staticTea.about,
      staticTea.price * res.getLong('Csum'),
      staticTea.type,
      res.getLong('Csum'),
      '',
      {
        cup: res.getLong('Ccup'),
        temp: res.getLong('Ctemp'),
        sweetness: res.getLong('Csweetness')
      }
    )
    teas.push(tea)
  }
  return teas
}

4. 核心类设计技巧

4.1 饮品类(Tea)的灵活设计

@Observed装饰器是实现UI自动更新的关键。当购物车中饮品数量变化时,这个装饰器能让关联的UI组件自动刷新:

@Observed
export class Tea {
  name: string
  price: number
  sum: number
  norms: {
    cup: number
    temp: number
    sweetness: number
  }
  
  // 构造方法支持可选参数
  constructor(name: string, price: number, sum?: number, norms?: object) {
    this.name = name
    this.price = price
    if(sum) this.sum = sum
    if(norms) this.norms = norms
  }
}

4.2 订单类(Bill)的扩展性考虑

订单类需要包含支付信息,我建议将支付类型设计为枚举:

export enum PayType {
  WECHAT = '微信',
  ALIPAY = '支付宝',
  CASH = '现金'
}

export class Bill {
  Bno: string  // 订单号
  Btime: string
  Btype: PayType
  Bprice: number
  
  constructor(bno: string, price: number, type: PayType) {
    this.Bno = bno
    this.Bprice = price
    this.Btype = type
    this.Btime = new Date().toISOString()
  }
}

4.3 静态数据管理方案

StaticValue类管理全局常量时,有个实用技巧:用getter实现按需加载:

export class StaticValue {
  private static _teas: Tea[] = []
  
  static get teas(): Tea[] {
    if(this._teas.length === 0) {
      // 初始化饮品数据
      this._teas = [
        new Tea('珍珠奶茶', 15),
        new Tea('柠檬红茶', 12)
      ]
    }
    return this._teas
  }
}

5. 开发踩坑与解决方案

5.1 数据库操作常见问题

事务处理是个易错点。有一次我连续执行插入和更新操作,由于没有用事务,中途出错导致数据不一致。正确做法:

async safeUpdate() {
  await this.rdbStore.beginTransaction()  // 开始事务
  try {
    await this.insertData()
    await this.updateData()
    await this.rdbStore.commit()  // 提交
  } catch(e) {
    await this.rdbStore.rollback() // 回滚
  }
}

类型转换也需要特别注意。从数据库取出的整数默认是number类型,但如果是大整数可能会丢失精度。建议:

let id = res.getLong(res.getColumnIndex('id')) // 用getLong不是getDouble

5.2 性能优化经验

当购物车商品较多时,发现列表滚动卡顿。通过这两个优化显著提升性能:

  1. 使用@LazyForEach替代普通ForEach
  2. 将复杂计算移到aboutToAppear生命周期中
@LazyForEach(this.cartItems, 
  item => item.id.toString(),
  item => {
    TeaItem({ tea: item })
  }
)

6. 项目扩展建议

这套架构可以轻松扩展新功能:

  1. 优惠系统:在StaticValue中添加优惠券数据,在Bill类中增加折扣字段
  2. 口味定制:扩展Tea类的norms对象,添加"加料"等属性
  3. 数据同步:通过分布式数据管理实现多设备同步
// 扩展后的规格对象
norms: {
  cup: number
  temp: number
  sweetness: number
  toppings: string[]  // ['珍珠','椰果']
}

实际开发中发现,良好的类设计能大幅降低后续维护成本。比如把支付逻辑封装在Bill类中,后期新增支付方式只需修改一处代码。

Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐