保质期计算器 - 生产日期过期日期在线计算
到期计算是基于生产日期的整数日期算术。正向路径使用 Date.setDate(date.getDate() + n) 计算 expiry = production + shelf_life_days,该方法原生处理月份和年份进位(如 2025-01-31 + 30 = 2025-03-02,因为二月有 28 天)。反向路径用相同方式计算 production = expiry - shelf_life_days。为避免午夜附近的夏令时偏移,页面通过 Date.UTC(y, m, d) 将算术锚定到 UTC,并加上整毫秒数(n * 86400000),因为本地时间的 Date.setDate 在跨越夏令时边界时会产生一小时偏移,可能在三月和十一月(美国)或十月(欧盟)的切换日翻转可见日期。
保质期单位在加法运算前先归一化为天数:1 个月视为 30 天,1 年视为 365 天,这与大多数消费品包装上的计算方式一致,但不是监管机构使用的精确日历规则。《食品法典委员会预包装食品标签通用标准》(CXS 1-1985 修订版)和中国的 GB 7718-2025 规定保质期从生产日期或包装日期起算;药品有效期遵循 ICH Q1A (R2) 稳定性测试指南,打印的有效期取当月最后一天(因此 2024-08-15 生产、保质期 18 个月的药品标注为 2026-02-28,而非 2026-02-15)。剩余天数计数器为 (expiry_utc - now_utc) / 86400000,已过期用 Math.floor 向下取整,距过期时间用 Math.ceil 向上取整;新鲜度分级是阈值比较:remaining < 0 为已过期,0 ≤ remaining ≤ 30 为即将过期,否则为正常。
持久化的关注列表存储在 window.localStorage 中,使用单一命名空间键,通过 JSON.stringify 序列化,大多数浏览器对每个来源的存储上限约为 5-10 MB(Chrome 约 10 MB,Safari 约 5 MB)。超出配额时浏览器抛出 name 为 'QuotaExceededError' 的 DOMException,之后的写入会被静默丢弃,因此页面必须回退到内存状态。localStorage 是同步且同源的,条目在页面刷新后保留,但数据不会离开设备。浏览器通知 API(提醒功能所用)需要 Notification.requestPermission(),按来源授权,在 Windows ≥ 86 的 Chrome 上实际通过系统通知中心而非浏览器本身弹出。
日期算术:Date.setDate(d.getDate() + n) 原生处理月份/年份进位,但在本地时间跨越夏令时边界时可能翻转日期;Date.UTC + 整毫秒数是安全的做法。单位归一化:1 个月 ≈ 30 天,1 年 ≈ 365 天——简单但不是精确日历值;药品标签遵循 ICH Q1A (R2),取有效期当月最后一天。剩余天数 = Math.floor((expiry_utc - now_utc) / 86400000);阈值:remaining < 0 → 已过期,0 ≤ remaining ≤ 30 → 即将过期,否则正常。法规依据各异:食品保质期遵循 Codex CXS 1-1985 / GB 7718-2025(从生产或包装日期起算);化妆品使用 PAO(开封后使用期限),从首次使用起算;药品使用 ICH Q1A 稳定性测试日期。开封后保质期独立于印刷有效期且更短(如橄榄油开封后约 3 个月,而密封保质期为 24 个月);需单独记录开封日期。localStorage 配额约为每个来源 5-10 MB,满时抛出 QuotaExceededError(DOMException);抛出后的写入会被静默丢弃,因此需将 setItem 包裹在 try/catch 中并向用户提示警告。浏览器时钟偏差(系统日期配置错误)会偏移新鲜度分级;页面读取 new Date() 而非远程时间源,因此继承客户端的任何时钟误差。