茶器艺科智造HarmonyOS应用实战-26-订单日期与ORD-2026写死,跨年演示为何仍像旧数据:注入Clock与IdFactory

茶器艺科智造HarmonyOS应用实战-26-订单日期与ORD-2026写死,跨年演示为何仍像旧数据:注入Clock与IdFactory

应用运行到 2027 年,商城首屏仍显示ORD-2026-0118与2026-03-15 14:30。用户从切片页新建订单时,ID 又变成ORD-SL-毫秒时间戳,日期取另一次new Date()。同一个列表里,一部分像历史快照,一部分像实时业务数据;测试若想覆盖跨年,只能依赖执行机器的当前时钟。

指定工程的演示订单确实全部写死在 2026 年。HSP 新建切片订单则在一个方法里多次读取时间:一次Date.now()拼 ID,一次new Date()格式化日期,随后再一次Date.now()计算状态转换截止时刻。三次读取通常接近,但不是同一业务快照;快速连点还可能让毫秒 ID 碰撞。本文建议把“现在是什么时候”和“订单 ID 怎么生成”变成依赖,由Clock与IdFactory注入。生产使用系统时钟,测试使用固定时钟,订单创建全程只取一次 now。

本文聚焦可重复的时间与身份生成:

  1. 区分固定演示快照、相对当前时间的演示数据和真实订单三种语义。
  2. 用 Clock 固定跨年、月末和时钟回拨用例。
  3. 用 IdFactory 处理同毫秒创建与已有 ID 冲突。
  4. 说明端侧时钟与端侧 ID 的证据上限,避免把演示唯一性当服务端业务唯一性。

一、源码里同时存在三种互不统一的时间写法

ChaqiModels.ets:133-164中,打印中与待处理订单逐条写ORD-2026-...和 2026 年日期,已完成循环也把 ID 年份固定为 2026、日期固定为 2026-02:

{ id: 'ORD-2026-0118', date: '2026-03-15 14:30', /* ... */ } // 已完成订单循环 id: `ORD-2026-${pad}`, date: `2026-02-${dayText} ${hourText}:${minuteText}`

HSP 的ChaqiExperiencePage.ets:619-631则使用当前时间与随机延迟:

const orderId = `ORD-SL-${Date.now()}`; const sliceOrder: ShopOrder = { id: orderId, status: 'pending', progress: 0, name: `${presetName}·切片`, material: '陶瓷树脂', date: this.formatOrderDateNow() }; const delayMs = 2000 + Math.floor(Math.random() * 3001); this.pendingSlicePrintFireAtMs = Date.now() + delayMs;

当前代码没有把这些订单声明为“固定历史样例”还是“仿实时数据”。如果它们只是教程截图 fixture,固定 2026 未必错误,但界面应明确标注演示快照;若期望每次打开都像近期订单,就需要相对 Clock 生成。不能只把字符串中的 2026 手工改成 2027,因为下一年同样会过期。

二、先选择演示时间语义,再写生成器

演示数据常见三种策略:

策略时间来源优点代价
固定快照fixture 自带 2026 日期截图与测试完全稳定跨年后显旧,必须标注“演示快照”
相对现在Clock.now 减若干小时/天每次打开都像近期列表相同版本截图会变化
服务端样例后端返回带环境标记的数据接近真实协议依赖网络与服务环境

本文选择“相对现在 + 测试固定 Clock”作为建议,因为标题场景要求跨年仍保持近期感。第 25 篇的 fixture 仍然成立:样例的名称、材质、状态、进度和时间偏移是固定的;绝对日期由注入 Clock 派生。若产品选择固定快照,则无需 Clock 生成日期,只需改用DEMO-ID 并在界面明确说明。

三、Clock接口只暴露业务需要的当前毫秒

一个最小 Clock 足够,不需要引入完整日期框架:

export interface Clock { nowMs(): number; } export class SystemClock implements Clock { nowMs(): number { return Date.now(); } } export class FixedClock implements Clock { private valueMs: number; constructor(valueMs: number) { this.valueMs = valueMs; } nowMs(): number { return this.valueMs; } }

生产代码注入 SystemClock;单测注入 2026-12-31 23:59 或 2027-01-01 00:01 对应的毫秒值。Clock 返回 epoch 毫秒,不决定展示时区。格式化层应明确采用设备本地时区还是业务固定时区;当前getFullYear/getMonth/getDate使用设备本地时区,这一行为可保留,但测试应显式提供时区策略,避免不同环境断言不同日期。

四、相对时间fixture保存offset,不保存过期日期

把第 25 篇订单 fixture 的date改成相对偏移。例如打印中订单发生在 now 前 30 分钟,待处理订单可以在 now 前后若干分钟;是否允许“未来待处理时间”需要产品定义。

export interface TimedOrderFixture { readonly sequence: number; readonly ageMinutes: number; readonly status: OrderStatus; readonly progress: number; readonly name: string; readonly material: string; } const PRINTING_FIXTURES: readonly TimedOrderFixture[] = [ { sequence: 118, ageMinutes: 30, status: 'printing', progress: 67, name: '罗汉杯·兔毫釉', material: '陶瓷树脂' } ];

构建时只读一次 anchorMs,再对所有行计算绝对时间。这样列表内部拥有一致参照,不会生成到一半跨过午夜:

export function buildDemoOrders( clock: Clock, idFactory: OrderIdFactory ): ShopOrder[] { const anchorMs = clock.nowMs(); return ALL_FIXTURES.map((item: TimedOrderFixture) => { const createdAtMs = anchorMs - item.ageMinutes * 60 * 1000; return { id: idFactory.demoId(createdAtMs, item.sequence), status: item.status, progress: item.progress, name: item.name, material: item.material, date: formatOrderTime(createdAtMs) }; }); }

建议长期把createdAtMs存进 ShopOrder,UI 再格式化,避免 date string 承担排序与时区语义;为缩小迁移,本片段仍返回当前模型需要的 date。

五、IdFactory统一演示ID和切片订单ID

ID 生成不应散落模板字符串。接口可以分别表达 demo 与用户触发订单:

export interface OrderIdFactory { demoId(createdAtMs: number, sequence: number): string; sliceId(createdAtMs: number, occupied: readonly string[]): string; } function fourDigits(value: number): string { if (value < 10) return `000${value}`; if (value < 100) return `00${value}`; if (value < 1000) return `0${value}`; return `${value}`; }

演示 ID 可从 createdAt 的年份生成DEMO-2027-0118,明确它不是后端订单;若 UI 必须保留 ORD 前缀,也要有单独 demo 标记。切片 ID 以时间为基底,并在已有订单中检查冲突:

export class LocalOrderIdFactory implements OrderIdFactory { demoId(createdAtMs: number, sequence: number): string { const year = new Date(createdAtMs).getFullYear(); return `DEMO-${year}-${fourDigits(sequence)}`; } sliceId(createdAtMs: number, occupied: readonly string[]): string { const base = `ORD-SL-${createdAtMs}`; let candidate = base; let suffix = 1; while (occupied.includes(candidate)) { candidate = `${base}-${suffix}`; suffix++; } return candidate; } }

冲突循环解决当前列表内同毫秒创建,不证明跨设备、跨进程或服务端全局唯一。真实订单应由服务端或正式唯一 ID 协议分配,端侧临时 ID 还需在同步后与服务端 ID 建立映射。

六、一次订单创建只能取一次now快照

新建切片订单时,把 clock 和 factory 放到服务层,先读一次 now:

export function createSliceOrder( clock: Clock, idFactory: OrderIdFactory, occupiedIds: readonly string[], presetName: string, delayMs: number ): PendingSliceOrder { const nowMs = clock.nowMs(); const order: ShopOrder = { id: idFactory.sliceId(nowMs, occupiedIds), status: 'pending', progress: 0, name: `${presetName}·切片`, material: '陶瓷树脂', date: formatOrderTime(nowMs) }; return { order: order, fireAtMs: nowMs + delayMs }; }

ID、显示时间和 deadline 都基于同一个 nowMs,不会出现跨分钟或跨年不一致。随机 delay 仍会影响测试,可再注入 DelayPolicy;这不是 Clock 的职责,不应把随机数塞进时钟接口。

页面只负责把结果写入状态与启动定时器。到期判断中的Date.now()也应改为clock.nowMs(),否则创建测试可控、恢复判断仍不可控。

七、跨年与碰撞用例无需等待真实时间

it('uses injected year after new year', 0, () => { const clock = new FixedClock(localEpoch(2027, 1, 2, 9, 30)); const orders = buildDemoOrders(clock, new LocalOrderIdFactory()); expect(orders[0].id.startsWith('DEMO-2027-')).assertTrue(); expect(orders[0].date.startsWith('2027-')).assertTrue(); }); it('avoids same millisecond collision', 0, () => { const ids: string[] = ['ORD-SL-1800000000000']; const factory = new LocalOrderIdFactory(); const next = factory.sliceId(1800000000000, ids); expect(next).assertEqual('ORD-SL-1800000000000-1'); });

localEpoch应由测试工具按明确时区构造,或直接使用审阅过的 epoch 常量。还要覆盖月末、闰日、设备时区切换和系统时钟回拨。时钟回拨时,occupied 检查仍保证当前列表不重号,但排序若依赖 createdAt 可能倒退,需要产品定义。

八、验证矩阵分开演示新鲜度与业务唯一性

场景注入预期
2026-12-31FixedClockID/日期采用 2026
2027-01-01FixedClock不再硬编码 2026
同一 now 连续创建固定时钟 + occupied后一 ID 带冲突后缀
不同 now 创建递增 ClockID 不同,日期对应各自快照
跨午夜创建一次 now 快照同一订单 ID 年份与日期一致
到期判断可推进 ClockfireAt 前不转换,到达后转换
真机改时区SystemClock展示策略符合产品约定
服务端同步正式环境本地 ID 被映射,不冒充全局 ID

纯单测可证明固定时刻的字符串与冲突策略;页面计时器需要假时钟或调度器;真实时区和系统时钟变化需要设备验证;服务端唯一性只能由服务协议和回读证明。

九、故障排查与证据边界

现象首查常见根因修正
2028年仍显示2027fixture 是否保存绝对日期又手改了年份保存offset并注入Clock
ID年份与date年份不同now读取次数跨边界多次取时创建开始只读一次
测试在不同时区失败formatter时区策略依赖机器本地设置测试显式时区
快速连点出现相同IDIdFactory是否查occupied只拼Date.now冲突后缀或序列
到期用例需要真实等待判断仍调用Date.nowClock只接入创建所有时间读取统一依赖
本地ID被当正式订单号前缀与同步协议演示/临时/正式身份混合明确DEMO与本地临时标记

源码审计能确认演示 ID/日期固定为 2026,切片订单 ID 使用一次 Date.now,显示时间使用另一次 new Date,deadline 又读取 Date.now;当前格式按设备本地时间输出。本文的 Clock、FixedClock、IdFactory、相对时间 fixture 和创建服务均为建议,没有修改只读项目。本文未运行单测、HAP 构建、跨年设备、时区切换、服务端同步或 CSDN 发布;端侧用例即使通过,也只能证明本地生成契约,不能证明真实订单的服务端唯一性与权威时间。