行业资讯

【寻迹校园 HarmonyOS NEXT 实战 19】“今天/近3天/近7天”为什么容易出错:自然日边界测试实战

发布时间:2026/8/21 6:04:27
【寻迹校园 HarmonyOS NEXT 实战 19】“今天/近3天/近7天”为什么容易出错:自然日边界测试实战 【寻迹校园 HarmonyOS NEXT 实战 19】“今天/近3天/近7天”为什么容易出错自然日边界测试实战这是“寻迹校园 HarmonyOS NEXT 实战”系列第 19 篇。本文结合matchesHomeTimeRange()与固定时钟测试说明为什么不能直接用两个时间戳相减判断“近 3 天”以及如何严格解析YYYY-MM-DD、按本机自然日计算边界、拒绝未来日期和非法日期。上图为原创生成的技术插画不是项目截图。时间筛选比较的是日历格而不是“当前时刻向前滚动 72 小时”的连续窗口这两个定义在每天的大部分时间里并不相同。一、“近 3 天”首先是产品定义问题假设现在是 2026-08-12 23:30一条记录发生在 2026-08-10 00:10。按自然日理解它属于今天、昨天、前天应进入“近 3 天”。但两个时间戳相差超过 71 小时若边界稍有偏差就可能被误排除。反过来一条发生在 8 月 9 日 23:40 的记录距当前不足 72 小时却已经跨过 8 月 9、10、11、12 四个日期不应进入“近 3 天自然日”。因此项目明确采用今天日期差为 0近 3 天今天 前 2 个自然日近 7 天今天 前 6 个自然日未来日期不命中非法日期不命中全部不施加时间过滤。先写清业务语义代码才有可验证目标。二、为什么直接除以 24 小时不可靠常见错误写法是constdaysMath.floor((Date.now()-newDate(eventDate).getTime())/86400000);returndays2;它有四类风险比较的是时刻差不是自然日差new Date(YYYY-MM-DD)的解析语义容易受平台和时区理解影响夏令时地区某些自然日不是严格 24 小时非法输入和未来日期可能被静默转换后继续计算。项目不依赖字符串构造 Date 的隐式规则而是显式拆年、月、日再验证 Date 没有自动溢出。三、严格解析 YYYY-MM-DDcalendarDayNumber()先执行trim()再用正则检查固定的四位年、两位月、两位日并显式拆出 year、month、day。这会拒绝2026/08/12、2026-8-12、空字符串和带额外内容的值。格式正确仍不代表日期存在。JavaScript 的 Date 会把2026-02-30自动滚动到 3 月因此还要回读年、月、日只要和输入不一致就返回-1。只要发生溢出原输入就被判为非法。四、为什么转成 UTC day number解析时使用本地 Date 验证用户选择的本地年月日比较时则用Date.UTC(year, month - 1, day) / MILLISECONDS_PER_DAY把年月日映射为统一的日序号。这里的 UTC 不是把事件强行解释成“UTC 时刻”而是用Date.UTC(year, month, day)为一个日历日期生成稳定编号。只比较编号差就不会把本地时分秒或夏令时某天的 23/25 小时带入自然日计算。五、今天也要先取本地年月日当前时间来自毫秒值但“今天”必须按本机日历理解先用getFullYear/getMonth/getDate取出本机年月日再映射到同一种 day number。先用本地getFullYear/getMonth/getDate取出今天再映射到 day number。若直接用Math.floor(nowMs / 86400000)得到的是 UTC 日期边界北京时间凌晨的数小时会被归到前一个 UTC 日。这一前一后的组合很重要业务日历使用本机日期差值计算使用稳定的 UTC 编号。六、三个范围的边界写法下面把严格解析、日序号和三个边界放在一个连续实现中展示constMILLISECONDS_PER_DAY:number24*60*60*1000;functioncalendarDayNumber(value:string):number{constnormalizedvalue.trim();if(!/^\d{4}-\d{2}-\d{2}$/.test(normalized))return-1;constyearNumber(normalized.substring(0,4));constmonthNumber(normalized.substring(5,7));constdayNumber(normalized.substring(8,10));constselectednewDate(year,month-1,day);if(Number.isNaN(selected.getTime())||selected.getFullYear()!year||selected.getMonth()!month-1||selected.getDate()!day){return-1;}returnMath.floor(Date.UTC(year,month-1,day)/MILLISECONDS_PER_DAY);}exportfunctionmatchesHomeTimeRange(eventDate:string,timeRange:HomeTimeRange,nowMs:numberDate.now()):boolean{if(timeRangeHomeTimeRange.ALL)returntrue;constselectedDaycalendarDayNumber(eventDate);if(selectedDay0)returnfalse;constnownewDate(nowMs);consttodayMath.floor(Date.UTC(now.getFullYear(),now.getMonth(),now.getDate())/MILLISECONDS_PER_DAY);constdifferencetoday-selectedDay;if(difference0)returnfalse;if(timeRangeHomeTimeRange.TODAY)returndifference0;if(timeRangeHomeTimeRange.THREE_DAYS)returndifference2;if(timeRangeHomeTimeRange.SEVEN_DAYS)returndifference6;returntrue;}为什么近 3 天是 2而不是 3因为今天已经占一天差值 0、1、2 一共三个日期。同理近 7 天对应 0 到 6。把 UI 文案、代码边界和测试用例放在同一张表里最不容易误解筛选允许的 difference以 2026-08-12 为今天时最早日期今天02026-08-12近 3 天022026-08-10近 7 天062026-08-06七、未来日期必须单独拒绝difference 0表示事件日在今天之后。即使未来日期绝对差很小也不应进入任何活动时间范围。发布表单已经限制事件日期不晚于今天但数据可能来自旧版本、迁移、手工测试或未来远端同步。筛选 Policy 仍要自我保护不能假设所有调用方永远正确。这体现了两层校验Service 在写入前保护数据质量Policy 在读取时保护结果语义。八、ALL 为什么允许空日期函数一开始对HomeTimeRange.ALL返回true。因此旧记录即使没有eventDate在“全部时间”下仍可展示只有用户启用活动时间筛选时非法或缺失日期才被排除。这是兼容策略而不是认为空日期有效。若产品要求所有公开记录必须有合法事件日期应在迁移和写入层完成修复再决定是否收紧 ALL 的行为。过早在列表层隐藏旧记录会让用户误以为数据丢失。上图以固定日期展示三个包含区间并把未来日期和非法日期放在拒绝区域。测试边界时最有价值的不是区间中间值而是最早允许日的前后两天。九、测试必须注入固定时钟如果测试直接调用Date.now()今天一变固定样例就会失效。项目把当前时间作为可选参数生产环境默认使用Date.now()测试则传入new Date(2026, 7, 12, 12, 0, 0, 0).getTime()作为固定 NOW。选择中午而不是临近午夜也能减少测试运行环境在日期边界附近产生的偶然干扰。十、边界用例比普通用例更重要项目测试包含输入范围预期2026-08-12今天true2026-08-11今天false2026-08-10近 3 天true2026-08-09近 3 天false2026-08-06近 7 天true2026-08-05近 7 天false2026-08-13近 7 天false2026-02-30近 7 天false空字符串近 7 天false空字符串全部true这组用例同时固定了包含边界、排除边界、未来日期、非法日期和兼容语义。仅测“昨天在近 3 天内”无法发现 off-by-one。十一、跨时区同步会改变问题当前项目是单机本地应用eventDate表示用户在本设备选择的校园自然日算法以本机时区为准。未来接入远端同步时需要先回答权威时区是什么事件发生地所在校园时区发布者设备时区服务器统一时区查看者设备时区。对于校园失物通常应以校园所在地自然日作为业务日期并把eventDate当成日期值而非 UTC 时间戳。若跨校跨时区就需要在数据合约中保存校园时区或业务时区不能只依赖查看设备。十二、日期值与时间戳不要混用项目同时有eventDate和createdAteventDate用户选择的事件自然日格式YYYY-MM-DDcreatedAt记录创建的时间点用于排序和审计。筛选“事件发生在近 3 天”应该使用eventDate而不是createdAt。用户今天补发三天前丢失的物品两者可能相差很大。迁移旧记录时可以临时从createdAt推导 eventDate但那只是兼容回填不应改变两个字段长期承担的不同语义。十三、页面层还要处理哪些状态纯函数正确不代表交互已经正确。ArkUI 页面还应检查筛选标签显示“近 3 天”时传入的是THREE_DAYS清空筛选恢复ALL应用跨过午夜后重新查询而不是继续使用旧结果从后台返回时是否需要刷新“今天”日期为空的历史记录是否在 ALL 下正常显示小屏下三个时间选项是否可点击且不挤压。如果页面长期缓存nowMs自然日切换后会出现“昨天仍被当作今天”的问题。当前纯函数默认每次调用读取Date.now()页面刷新策略仍需要设备验证。十四、运行真实规则测试本地测试命令powershell-ExecutionPolicy Bypass-File.\scripts\test-report-filter.ps1该测试能证明固定时钟下的自然日边界与非法输入处理。它不能证明设备时区切换、跨午夜生命周期、系统日期异常、远端同步和 ArkUI 交互。更完整的设备测试应至少覆盖本地 23:59 到次日 00:01、修改系统时区、后台恢复、旧记录空日期和日期选择器最大值。十五、维护日期代码的原则先写清“自然日”还是“滚动小时窗口”日期值使用稳定格式时间点使用时间戳不依赖字符串 Date 的隐式解析验证自动溢出的非法日期用本地年月日确定业务今天用稳定日序号计算差值测试固定时钟和边界前后值跨时区前先定义权威业务时区。这些原则比记住某个 Date API 更能避免线上边界错误。十六、日期规则变更前的影响分析把“近 3 天”改成“过去 72 小时”看起来只是替换一个表达式实际上会改变边界记录、产品文案、测试数据和用户预期。改动前应先确认业务采用自然日还是滚动窗口、权威时区在哪里、未来日期是否允许以及旧客户端产生的日期字符串如何解释。日期规则还会影响首页筛选、匹配候选、统计报表与消息提醒。若多个入口各自计算边界同一条记录可能在首页可见却不进入匹配。集中到纯策略并注入时钟才能让所有消费者共享同一合同。十七、异常时间与回滚边界系统时间被手动修改、设备跨时区、夏令时切换或远端数据缺少时区时程序都不应猜测成“今天”。非法输入应返回明确的不匹配结果并记录来源若业务必须兼容旧格式应通过版本化解析器处理而不是放宽正则到任何字符串都能通过。上线新规则前要保留旧算法和固定样本比较两版命中差异。若差异超出产品确认范围应回滚策略实现而不是修改历史日期。远端同步场景还应保存事件发生地时区或统一业务时区否则客户端本地时间会让同一记录在不同设备得到不同答案。十八、验收证据如何覆盖午夜边界自动化至少需要固定在某一天的中午、23:59、次日 00:00 和时区切换前后执行分别记录 TODAY、LAST_3_DAYS、LAST_7_DAYS 的命中集合。设备层则应验证应用进入后台跨过午夜再恢复时是否重新计算而不是继续使用页面创建时缓存的“今天”。证据记录要包含系统时区、固定时钟、输入日期、预期日差与实际结果。普通 Node 用例可以证明纯算法但不能证明系统日期选择器、生命周期刷新或设备时区通知已经接通这些未运行项必须单独保留。十九、本文小结“今天/近 3 天/近 7 天”看似只是毫秒比较实际同时包含产品语义、日期解析、时区边界、非法输入和测试时钟。“寻迹校园”严格解析YYYY-MM-DD用本地年月日确定业务日期再映射为 UTC day number 计算自然日差近 3 天使用差值 02近 7 天使用 06并明确拒绝未来和非法日期。固定时钟测试把这些边界变成可重复契约。系列导航第 19 篇 / 共 50 篇。上一篇《六维 AND 组合筛选》下一篇《标准值与历史文本共存》。