按工作日计时的违约计时器,如果合同没有写明所用日历、起算日和时区,就会向两个方向出错。Narwhal 提供的是某个国家某一年的节假日,其余规则由服务台自己的代码和合同决定。
用合同可以明确指定的日历来启动计时器
如果计时器在节假日照常运行,就会显示合同从未允许的违约;如果它在合同没有排除的日子停下,又会掩盖真实的违约。解决办法是使用双方都能指认的日历。下面的请求读取新加坡 2026 年的节假日,只保留 Deepavali 前后的两个条目。
curl -s -H "Authorization: Bearer $NARWHAL_API_KEY" \
"https://api.narwhalapi.com/v1/calendars/holidays/SGP/2026"{
"country": "SGP",
"coverage": [
{
"scope": "national_public",
"subdivision_codes": []
}
],
"holidays": [
{
"date": "2026-11-08",
"name": "Deepavali",
"holiday_key": "deepavali",
"scope": "national_public",
"subdivision_codes": []
},
{
"date": "2026-11-09",
"name": "Deepavali (Observed)",
"holiday_key": "deepavali",
"scope": "national_public",
"subdivision_codes": [],
"observed": true
}
]
}每个条目都有 date、name、scope 和 subdivision_codes。Deepavali 的日期是 11 月 8 日周日;标有 observed: true 的条目是 11 月 9 日周一的补假,落在工作日上。coverage 列表给出日历包含的 scope,新加坡为 national_public。API 只提供节假日,计时器的其余部分属于合同约定的事项。
运行代码之前合同必须定义的内容
代码需要表中每一行都有明确答案。合同没有规定的地方,服务台和客户会在第一张有争议的工单上才发现。
| 设置 | 需要向合同提出的问题 | 由谁提供 |
|---|---|---|
| 日历 | 哪个国家,用哪种节假日 scope? | 先看合同,再用 API 的 scope 过滤 |
| 计入的日子 | 周六和周日是否排除? | 合约 |
| 起算日 | 受理当天算第 0 天还是第 1 天? | 合约 |
| 截止日期 | 到期时间是当天结束时吗? | 合约 |
| 时区 | 以哪个地区的当地日期开始计时? | 合约 |
| 暂停时间 | 客户答复期间计时是否暂停? | 合同和工单系统 |
| 每天的时数 | 工作日是否指办公时间? | 合同和支持排班 |
每一行都有书面答案,计时器就成了一个不留任何主观判断的函数。API 只解决日历这一行,别的什么都不管。
计算到期日并判断是否违约
脚本把节假日日期收集到一个集合里,把不在集合中的工作日视为工作日。受理当天为第 0 天,所以从第二天开始计数。第一个代码块把工单时间戳转换成合同所在时区的日期,第二个代码块检验已解决的工单。
import json, os, urllib.request
from datetime import date, datetime, timedelta
from zoneinfo import ZoneInfo
def get(path):
req = urllib.request.Request("https://api.narwhalapi.com" + path,
headers={"Authorization": "Bearer " + os.environ["NARWHAL_API_KEY"]})
with urllib.request.urlopen(req, timeout=30) as r:
return json.load(r)
def holiday_dates(country, years):
return {date.fromisoformat(h["date"]) for y in years for h in get(f"/v1/calendars/holidays/{country}/{y}")["holidays"]}
def is_working_day(d, off):
return d.weekday() < 5 and d not in off
def due_date(opened, working_days, off):
d, left = opened, working_days
while left:
d += timedelta(1)
left -= is_working_day(d, off)
return d
def elapsed_working_days(opened, resolved, off):
return sum(is_working_day(opened + timedelta(n), off) for n in range(1, (resolved - opened).days + 1))
off = holiday_dates("SGP", (2026,))
sg = ZoneInfo("Asia/Singapore")
# Clock start: the ticket timestamp becomes a date in the contract's time zone.
created = datetime(2026, 11, 5, 20, 0, tzinfo=ZoneInfo("America/New_York"))
opened = created.astimezone(sg).date()
print("created", created.date(), "in New York is", opened, "in Singapore")
print("3 working days from the Singapore date:", due_date(opened, 3, off))
print("3 working days from the New York date: ", due_date(created.date(), 3, off))
# Breach test: compare the resolution date with the due date.
opened, resolved = date(2026, 2, 13), date(2026, 2, 27)
due = due_date(opened, 5, off)
late = elapsed_working_days(opened, resolved, off) - 5
print("5 working days from", opened, "is due", due, "; resolved", resolved, "so", late, "working days late")
created 2026-11-05 in New York is 2026-11-06 in Singapore
3 working days from the Singapore date: 2026-11-12
3 working days from the New York date: 2026-11-11
5 working days from 2026-02-13 is due 2026-02-24 ; resolved 2026-02-27 so 3 working days late纽约的工单创建于 11 月 5 日周四 20:00,此时新加坡已是 11 月 6 日周五。从新加坡日期起算 3 个工作日,到期日为 11 月 12 日周四,因为 11 月 9 日周一是 Deepavali 的补假。如果从纽约日期起算,同一张工单会在 11 月 11 日周三到期,提前一天,所以时区这一行决定了服务台是否违约。第二张工单在 2 月 13 日周五受理,因春节占去 2 月 17 日周二和 18 日周三,到期日为 2 月 24 日周二。它在 2 月 27 日周五才解决,晚了 3 个工作日。
遇到落在周末的节假日,计时器怎么处理?
工作日判断会先把该日排除,所以不会被扣两次。新加坡响应中,Hari Raya Puasa 是 2026 年 3 月 21 日周六,Deepavali 是 11 月 8 日周日,两者都不会改变计数。补假是带有 observed: true 的独立条目,所以计时器无需额外代码就会把它排除。
SLA 应该使用哪种 scope?
请使用与客户业务所遵循的日历相符的 scope。新加坡只返回 national_public;印度尼西亚返回 national_public 和 collective_leave;澳大利亚返回带有 subdivision_codes 的地区条目,以及 bank scope。年度接口有一个可选的 scope 过滤参数。当 SLA 要排除某一类日子时传入它,并在每次请求中使用同一个 scope,这样计数结果才能重复得到。
API 能计算工作小时数吗?
不能。日历接口返回的是节假日日期。营业时间、午休,以及客户答复期间的暂停,都来自支持排班和工单系统。如果 SLA 是以小时为单位写的,就用这段代码得到工作日,再把排班套用到第一天和最后一天。
未结工单很多时,如何减少请求次数?
每个国家和年份只获取一次,把日期集合放在内存或自己的表里。该国家和年份的所有工单都复用这个集合。只有在工单跨入尚未加载的年份时,才再次调用年度接口。计算两个日期之间的工作日这篇指南用电子表格方式演示了同样的运算。
发布于。