鸿蒙开发实战:从零构建饮品点餐系统数据库与核心类
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,调试了两小时才发现问题。建议按这个流程操作:
- 创建RDBStore实例:
const config = {
name: 'DrinkOrder.db',
securityLevel: relationalStore.SecurityLevel.S1
}
relationalStore.getRdbStore(context, config, (err, store) => {
this.rdbStore = store
})
- 执行建表语句时一定要加
if not exists,避免重复创建报错:
const createUserSql = `create table if not exists user(...)`
rdbStore.executeSql(createUserSql)
- 建议封装初始化方法,在应用启动时调用:
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 性能优化经验
当购物车商品较多时,发现列表滚动卡顿。通过这两个优化显著提升性能:
- 使用
@LazyForEach替代普通ForEach - 将复杂计算移到
aboutToAppear生命周期中
@LazyForEach(this.cartItems,
item => item.id.toString(),
item => {
TeaItem({ tea: item })
}
)
6. 项目扩展建议
这套架构可以轻松扩展新功能:
- 优惠系统:在StaticValue中添加优惠券数据,在Bill类中增加折扣字段
- 口味定制:扩展Tea类的norms对象,添加"加料"等属性
- 数据同步:通过分布式数据管理实现多设备同步
// 扩展后的规格对象
norms: {
cup: number
temp: number
sweetness: number
toppings: string[] // ['珍珠','椰果']
}
实际开发中发现,良好的类设计能大幅降低后续维护成本。比如把支付逻辑封装在Bill类中,后期新增支付方式只需修改一处代码。
更多推荐


所有评论(0)