十张卡券三态入库:鸿蒙卡券种子数据与券包效果


实例:卡券管理(Coupon)|技术:10 张券、三态分布、有效期梯度
一、种子数据的设计目标
实例 18 的种子数据要撑起「券包列表 + 状态筛选 + 过期检测」,设计目标:
- 10 张卡券:券包内容充实——列表多屏;
- 三态分布:未使用 5 / 已使用 2 / 已过期 3——筛选 tab 数字有区分;
- 有效期梯度:过期时间从现在 +3 天到 +60 天——「剩 N 天」有变化(3/7/15/20/30/60);
- 类型覆盖:优惠券 6 / 代金券 3 / 礼品卡 1——类型多样;
- 过期历史:3 张已过期(-10 天 / -5 天 / -30 天前到期)——markExpired 对这些是「已标记」(种子直接 status=2);
- 面值/门槛梯度:¥5 无门槛 ~ ¥100 礼品卡、满 50 可用 ~ 满 500 可用——券面内容有层次。
二、10 张卡券种子数据
| # | 标题 | 类型 | 面值 | 门槛 | 状态 | 生效 | 过期 | 颜色 | 备注 |
|---|---|---|---|---|---|---|---|---|---|
| 1 | 全场满 100 减 20 | 优惠券 | ¥20 | 满100 | 未使用 | -10天 | +20天 | 红 #EF4444 | 全场通用 |
| 2 | 生鲜满 50 减 10 | 优惠券 | ¥10 | 满50 | 未使用 | -8天 | +15天 | 绿 #10B981 | 生鲜专区 |
| 3 | 数码满 500 减 80 | 优惠券 | ¥80 | 满500 | 未使用 | -5天 | +30天 | 蓝 #3B82F6 | 数码专区 |
| 4 | 新人 5 元无门槛 | 代金券 | ¥5 | 无门槛 | 未使用 | -3天 | +7天 | 橙 #F59E0B | 新人专享 |
| 5 | 年终回馈 100 元 | 礼品卡 | ¥100 | 无门槛 | 未使用 | -1天 | +60天 | 靛 #6366F1 | 积分兑换 |
| 6 | 会员生日礼 30 元 | 代金券 | ¥30 | 满200 | 已使用 | -30天 | +5天 | 粉 #EC4899 | 已使用 |
| 7 | 周五咖啡券 | 优惠券 | ¥15 | 满30 | 已使用 | -20天 | +3天 | 紫 #8B5CF6 | 已使用 |
| 8 | 图书满 99 减 30 | 优惠券 | ¥30 | 满99 | 已过期 | -60天 | -10天 | 青 #06B6D4 | 已过期 |
| 9 | 超市满 200 减 50 | 代金券 | ¥50 | 满200 | 已过期 | -45天 | -5天 | 黄绿 #84CC16 | 已过期 |
| 10 | 洗车 5 折券 | 优惠券 | ¥25 | 满50 | 已过期 | -90天 | -30天 | 橙红 #F97316 | 已过期 |
设计要点:
- 三态分布 5/2/3:未使用 5 张(可用券主力)——筛选条「未使用 5 / 已使用 2 / 已过期 3 / 全部 10」;
- 到期梯度:未使用券的剩余天数 +7(新人券)/ +15 / +20 / +30 / +60——「剩 N 天」标签 5 种值,急迫感梯度(新人券最急 7 天);
- 已过期 3 张:直接 status=2(种子就标好)——不依赖 markExpired 演示(markExpired 演示留给「现场把券放过期」的进阶场景,或等时间推移);
- 类型分布:优惠券 6 / 代金券 3 / 礼品卡 1——主流类型多、高端礼品卡少;
- 无门槛 2 张:新人 5 元、年终回馈 100——「无门槛」券面文案与「满 X 可用」形成对比。
三、代码实现
const now = Date.now();
const day = 86400000;
const seed: CouponSeed[] = [ /* 10 张,见第二节表格 */ ];
for (const s of seed) {
const values: relationalStore.ValuesBucket = {
title: s.title, type: s.type, face_value: s.faceValue, min_spend: s.minSpend,
status: s.status, start_time: s.startTime, expire_time: s.expireTime,
color: s.color, remark: s.remark,
};
await store.insert(CouponDao.TABLE, values);
}
hilog.info(DOMAIN, TAG, '已填充 10 张卡券种子数据');
const day = 86400000:一天的毫秒常量——种子时间 now - 10 * day(10 天前)、now + 20 * day(20 天后)——相对时间表达(可读性强于裸毫秒)。幂等保护:查 coupon COUNT,非空即返回。
四、注入后的卡券包效果预测
| 区块 | 预期效果 |
|---|---|
| 筛选条 | 全部 10 / 未使用 5 / 已使用 2 / 已过期 3 |
| 列表顶部 | 未使用 5 张(红/绿/蓝/橙/靛券面):全场减 20(剩 20 天)→ 生鲜减 10(15 天)→ 数码减 80(30 天)→ 新人 5 元(7 天)→ 年终回馈 100(60 天) |
| 中间 | 已使用 2 张(粉/紫券面 + 灰色「已使用」标签) |
| 底部 | 已过期 3 张(青/黄绿/橙红券面 + 灰「已过期」标签) |
| 点券 | 详情弹窗 7 行(类型/面值/门槛/生效/过期/状态/备注) |
演示脚本:指着券包说「我有 10 张券,5 张能用」→ 指新人券「这个最急,剩 7 天就过期了」→ 指年终回馈「这个最值钱,100 块无门槛,还有 60 天」→ 点「未使用」筛选 → 5 张 → 点新人券「使用」→ 确认 → Toast「✅ 已使用」→ 新人券变「已使用」灰色、筛选条数字更新(未使用 5→4、已使用 2→3)→ 点「已过期」→ 3 张灰券。
五、验证方法
- hilog 日志:搜
CouponDao,看到「已填充 10 张卡券种子数据」; - 页面显示:筛选条 10/5/2/3、未使用券面 5 张(带「剩 N 天」)、已使用/已过期标签正确;
- 数据库直查:
SELECT COUNT(*) FROM coupon; -- 10 SELECT status, COUNT(*) FROM coupon GROUP BY status; -- 0:5 1:2 2:3 SELECT title, expire_time FROM coupon WHERE status = 0 ORDER BY expire_time ASC; -- 新人5元无门槛(+7天)最先 SELECT COUNT(*) FROM coupon WHERE status = 0 AND expire_time > 1720869600000; -- 5 - 交互验证:状态筛选、使用卡券(状态流转)、长按删除、点券详情。
六、FAQ
Q1:为什么未使用 5 张、已使用 2 张、已过期 3 张?
A:三态有区分度——未使用最多(可用券主力,筛选「未使用 5」)、已使用 2(演示过使用)、已过期 3(历史券)。「主力 + 演示 + 历史」的分布让每个筛选 tab 都有内容。
Q2:已过期的券为什么不靠 markExpired 自动标?
A:种子直接 status=2 是「静态过期」——保证首启就有 3 张过期券(markExpired 对它们无影响,因为已标记)。markExpired 的动态演示可这样触发:改种子过期时间(now - 1 天)→ 首启时 status=0 但 expire<now → refresh 的 markExpired 自动标为过期。「静态种子 + 动态检查」两条路径都覆盖。
Q3:新人券剩 7 天、年终回馈剩 60 天——梯度怎么设计的?
A:让「剩 N 天」标签有 5 种值(7/15/20/30/60)——到期急迫感梯度(7 天最急提醒使用、60 天从容)。真实券包的到期提醒优先级就是「越急越靠前」(orderByAsc expire_time 已实现)。
Q4:为什么有 2 张无门槛券?
A:新人 5 元(拉新)、年终回馈 100(积分兑换)——无门槛是「高价值券」的标志(直接抵现)。券面「无门槛」文案与「满 100 可用」对比,展示 min_spend=0 的哨兵语义。
Q5:面值为什么从 ¥5 到 ¥100?
A:梯度覆盖——5 元新人券、10/15/20/25/30/50 元满减、80 元数码、100 元礼品卡——**「小面值高频 + 大面值稀缺」**的券包结构(大面值通常门槛高或获取难)。
Q6:10 张券的 markExpired 会误伤吗?
A:不会——已使用的(status=1)不在条件内(status=0 限定)、已过期的(status=2)也不在——只有「未使用且到期」的券会被标过期。种子里没有这种「边界券」(未使用但过期),所以 markExpired 首启影响 0 行——种子数据保持状态自洽。
七、文章小结
本篇文章展示了实例 18 的种子数据:10 张卡券三态分布(未使用 5 / 已使用 2 / 已过期 3 + 到期梯度 7~60 天 + 类型 4 种 + 面值 ¥5~¥100 梯度)。核心是「券包感的种子设计」——到期急迫感梯度(剩 7 天最急)、无门槛高价值券对比、静态过期与动态 markExpired 双路径。卡券包一开屏就三色券面、状态分明。
下一篇(18-5)是实例 18 的收官文章,展示 CouponPage 全量代码与运行效果。
八、10 张卡券种子数据核对表(补全版)
把种子里每张券的 9 个字段 全部列齐(基准时间 now = 2024-07-13,日期为当天 00:00 换算),作为核对与测试断言的参照:
| # | 标题 | 类型 | 面值 | 门槛 | 状态 | 生效 | 过期 | 颜色 | 备注 |
|---|---|---|---|---|---|---|---|---|---|
| 1 | 全场满 100 减 20 | 优惠券 | ¥20 | 满100 | 未使用 | 07-03 | 08-02 | 红 #EF4444 | 全场通用 |
| 2 | 生鲜满 50 减 10 | 优惠券 | ¥10 | 满50 | 未使用 | 07-05 | 07-28 | 绿 #10B981 | 生鲜专区 |
| 3 | 数码满 500 减 80 | 优惠券 | ¥80 | 满500 | 未使用 | 07-08 | 08-12 | 蓝 #3B82F6 | 数码专区 |
| 4 | 新人 5 元无门槛 | 代金券 | ¥5 | 无门槛 | 未使用 | 07-10 | 07-20 | 橙 #F59E0B | 新人专享 |
| 5 | 年终回馈 100 元 | 礼品卡 | ¥100 | 无门槛 | 未使用 | 07-12 | 09-11 | 靛 #6366F1 | 积分兑换 |
| 6 | 会员生日礼 30 元 | 代金券 | ¥30 | 满200 | 已使用 | 06-13 | 07-18 | 粉 #EC4899 | 已使用 |
| 7 | 周五咖啡券 | 优惠券 | ¥15 | 满30 | 已使用 | 06-23 | 07-16 | 紫 #8B5CF6 | 已使用 |
| 8 | 图书满 99 减 30 | 优惠券 | ¥30 | 满99 | 已过期 | 05-14 | 07-03 | 青 #06B6D4 | 已过期 |
| 9 | 超市满 200 减 50 | 代金券 | ¥50 | 满200 | 已过期 | 05-29 | 07-08 | 黄绿 #84CC16 | 已过期 |
| 10 | 洗车 5 折券 | 优惠券 | ¥25 | 满50 | 已过期 | 04-14 | 06-13 | 橙红 #F97316 | 已过期 |
核对要点:
- 未使用 5 张:
expire_time全部 > now 且status=0——筛选「未使用 5」成立; - 已使用 2 张:
status=1——即使过期时间在未来(生日礼 07-18)也不影响状态展示,状态优先于时间; - 已过期 3 张:
expire_time < now且status=2——时间与状态自洽,没有「未使用但已过期」的边界券,markExpired 首启影响 0 行。
九、种子设计复盘:三态分布与到期梯度
三态分布 5/2/3 复盘
| 方案 | 未使用 | 已使用 | 已过期 | 问题 |
|---|---|---|---|---|
| 5/2/3(采用) | 5 | 2 | 3 | 主力多、演示够、历史足——筛选数字 10/5/2/3 有层次 |
| 7/1/2 | 7 | 1 | 2 | 已使用只有 1 张,「使用」演示后该 tab 就空了 |
| 3/3/4 | 3 | 3 | 4 | 可用券太少,「我能用 5 张」的观感立不住 |
为什么未使用要过半:券包的第一观感是「有多少能用的」——5 张可用(占一半)让演示话术成立;已使用 2 张恰好支撑「用完 1 张还剩 1 张」的流转演示;已过期 3 张让「已过期」tab 不是单张孤例。任何一态少于 2 张,对应的筛选 tab 就失去演示价值。
到期梯度 7/15/20/30/60 复盘
| 剩余天数 | 券 | 面值 | 作用 |
|---|---|---|---|
| 7 | 新人 5 元 | ¥5 | 最急——演示「剩 7 天」急迫感 |
| 15 | 生鲜满 50 减 10 | ¥10 | 次急 |
| 20 | 全场满 100 减 20 | ¥20 | 中等 |
| 30 | 数码满 500 减 80 | ¥80 | 从容 |
| 60 | 年终回馈 100 元 | ¥100 | 最值钱最不急——「还有 60 天」 |
梯度设计要点:5 个值覆盖「急 → 从容」全谱,且与面值大体成反比(越值钱越不急)——「新人券最急但最便宜、年终回馈最值钱但最不急」的反差让演示有故事性;同时保证 queryAll 组内按 expire_time 升序后,列表顶部第一张就是最急的新人券,一屏就能看到梯度。
十、initSeedData 完整代码(day 常量版)
interface CouponSeed {
title: string; type: string; faceValue: number; minSpend: number;
status: number; startTime: number; expireTime: number;
color: string; remark: string;
}
static async initSeedData(context: common.Context): Promise<void> {
const store = await CouponDao.getStore(context);
// 幂等保护:已有数据直接返回
const count = await CouponDao.count(context);
if (count > 0) {
hilog.info(DOMAIN, TAG, '种子数据已存在,跳过(当前 %{public}d 张)', count);
return;
}
const now = Date.now();
const day = 86400000; // 一天的毫秒数
const seed: CouponSeed[] = [
{ title: '全场满 100 减 20', type: '优惠券', faceValue: 20, minSpend: 100, status: 0,
startTime: now - 10 * day, expireTime: now + 20 * day, color: '#EF4444', remark: '全场通用' },
{ title: '生鲜满 50 减 10', type: '优惠券', faceValue: 10, minSpend: 50, status: 0,
startTime: now - 8 * day, expireTime: now + 15 * day, color: '#10B981', remark: '生鲜专区' },
{ title: '数码满 500 减 80', type: '优惠券', faceValue: 80, minSpend: 500, status: 0,
startTime: now - 5 * day, expireTime: now + 30 * day, color: '#3B82F6', remark: '数码专区' },
{ title: '新人 5 元无门槛', type: '代金券', faceValue: 5, minSpend: 0, status: 0,
startTime: now - 3 * day, expireTime: now + 7 * day, color: '#F59E0B', remark: '新人专享' },
{ title: '年终回馈 100 元', type: '礼品卡', faceValue: 100, minSpend: 0, status: 0,
startTime: now - 1 * day, expireTime: now + 60 * day, color: '#6366F1', remark: '积分兑换' },
{ title: '会员生日礼 30 元', type: '代金券', faceValue: 30, minSpend: 200, status: 1,
startTime: now - 30 * day, expireTime: now + 5 * day, color: '#EC4899', remark: '已使用' },
{ title: '周五咖啡券', type: '优惠券', faceValue: 15, minSpend: 30, status: 1,
startTime: now - 20 * day, expireTime: now + 3 * day, color: '#8B5CF6', remark: '已使用' },
{ title: '图书满 99 减 30', type: '优惠券', faceValue: 30, minSpend: 99, status: 2,
startTime: now - 60 * day, expireTime: now - 10 * day, color: '#06B6D4', remark: '已过期' },
{ title: '超市满 200 减 50', type: '代金券', faceValue: 50, minSpend: 200, status: 2,
startTime: now - 45 * day, expireTime: now - 5 * day, color: '#84CC16', remark: '已过期' },
{ title: '洗车 5 折券', type: '优惠券', faceValue: 25, minSpend: 50, status: 2,
startTime: now - 90 * day, expireTime: now - 30 * day, color: '#F97316', remark: '已过期' },
];
for (const s of seed) {
const values: relationalStore.ValuesBucket = {
title: s.title, type: s.type, face_value: s.faceValue, min_spend: s.minSpend,
status: s.status, start_time: s.startTime, expire_time: s.expireTime,
color: s.color, remark: s.remark,
};
await store.insert(CouponDao.TABLE, values);
}
hilog.info(DOMAIN, TAG, '已填充 10 张卡券种子数据(三态 5/2/3)');
}
day 常量的价值:now + 20 * day 一眼看懂是「20 天后」;若写裸毫秒 now + 1728000000,维护时没人知道是多少天。所有相对时间都基于 now——种子永远「新鲜」(不管哪一天首启,剩余天数都成立)。幂等保护:先 COUNT 再插入——首启填充 10 张,重启不重复(否则每次启动都翻倍)。
十一、注入后的券包效果预测
按 queryAll 排序(ORDER BY status ASC, expire_time ASC),列表最终形态:
| 位置 | 券 | 状态 | 券面颜色 | 标签 |
|---|---|---|---|---|
| 1 | 新人 5 元无门槛 | 未使用 | 橙 | 剩 7 天 |
| 2 | 生鲜满 50 减 10 | 未使用 | 绿 | 剩 15 天 |
| 3 | 全场满 100 减 20 | 未使用 | 红 | 剩 20 天 |
| 4 | 数码满 500 减 80 | 未使用 | 蓝 | 剩 30 天 |
| 5 | 年终回馈 100 元 | 未使用 | 靛 | 剩 60 天 |
| 6 | 会员生日礼 30 元 | 已使用 | 粉 | 已使用 |
| 7 | 周五咖啡券 | 已使用 | 紫 | 已使用 |
| 8 | 图书满 99 减 30 | 已过期 | 青 | 已过期 |
| 9 | 超市满 200 减 50 | 已过期 | 黄绿 | 已过期 |
| 10 | 洗车 5 折券 | 已过期 | 橙红 | 已过期 |
注意:未使用组内按 expire_time 升序——新人券(剩 7 天)置顶、年终回馈(60 天)垫底——「最急的券排最前」正是 queryAll 双键排序在种子数据上的落地效果。筛选条数字:全部 10 / 未使用 5 / 已使用 2 / 已过期 3;三色券面(红/绿/蓝/橙/靛 + 粉/紫 + 青/黄绿/橙红)铺满列表。
十二、验证 SQL 扩展与 FAQ
-- 三态统计(预期 0:5 1:2 2:3)
SELECT status, COUNT(*) FROM coupon GROUP BY status;
-- 类型分布(预期 优惠券6 / 代金券3 / 礼品卡1)
SELECT type, COUNT(*) FROM coupon GROUP BY type;
-- 未使用券按到期升序(预期第 1 行是新人券,剩 7 天)
SELECT title, expire_time FROM coupon WHERE status = 0 ORDER BY expire_time ASC;
-- 幂等验证:连续两次 initSeedData 后 COUNT 仍为 10(不翻倍)
SELECT COUNT(*) FROM coupon; -- 10
Q7:为什么 now 放在 initSeedData 方法内部而不是模块顶层?
A:顶层常量在模块加载时就固定,App 跑几天后种子时间就「旧」了;方法内每次调用都取最新 now——首启填充的那一瞬间,所有相对时间都是准的。day 则可以是模块级常量(它不随时间变),但为了「时间表达集中在一处」,放在一起更直观。
Q8:已使用的券过期时间在未来(生日礼 +5 天),会被 markExpired 误标吗?
A:不会——markExpired 的条件是 status = 0 AND expire_time < now,已使用券 status=1 直接被条件排除。状态优先于时间:只要 status 不是 0,时间再怎么过期都不处理——这就是状态机设计的护栏,也让种子里「已使用但未过期」「已过期且时间已过」两种组合都能安全共存。
Q9:如果想加第 11 张券,改哪里?
A:只改 initSeedData 的 seed 数组加一行,并同步更新三态统计口径(若分布变化)——页面、Dao 查询、表结构都不用动。种子数据是「数据层最薄的一层」:加券 = 加一条记录 + 调一个计数,这是把数据「配置化」带来的扩展性。
更多推荐




所有评论(0)