From bf20df572f4fccb4957cfd1b654fd2055b9680a9 Mon Sep 17 00:00:00 2001 From: yumoqing Date: Thu, 10 Sep 2026 23:43:57 +0800 Subject: [PATCH] =?UTF-8?q?feat(pricing):=20peak=5Ftimes=20=E5=A3=B0?= =?UTF-8?q?=E6=98=8E=E5=BC=8F=E5=BF=99=E9=97=B2=E6=97=B6=E5=AE=9A=E4=BB=B7?= =?UTF-8?q?=E2=80=94=E2=80=94fields=20=E9=87=8C=20peak=5Ftimes=20=E6=97=B6?= =?UTF-8?q?=E6=AE=B5=E4=B8=B2+=E5=BC=95=E6=93=8E=E8=AE=A1=E7=AE=97=20is=5F?= =?UTF-8?q?peak=20=E5=B8=83=E5=B0=94=E5=AD=97=E6=AE=B5,pricings=20?= =?UTF-8?q?=E7=94=A8=20is=5Fpeak:true/false=20=E4=BD=9C=E5=AE=9A=E4=BB=B7?= =?UTF-8?q?=E8=A6=81=E7=B4=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - _in_peak_times(): 逗号分隔时段串解析(=~/~=/=~=/~ 四种边界语义,浮点小时比较) - hour 缺失(老流水/上游未注入)按非忙时兜底,不炸计费;时段串解析失败记日志走闲时 - beautify_filter_value: bool → 是/否(展示层忙时=是/否) - 试算15例全过(忙闲窗口边界/缓存命中/factor=0/hour缺失),含服务进程端到端 --- pricing/pricing.py | 66 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/pricing/pricing.py b/pricing/pricing.py index e0fb8c3..a00fc57 100644 --- a/pricing/pricing.py +++ b/pricing/pricing.py @@ -1,4 +1,5 @@ import json +import re import yaml from ahserver.serverenv import ServerEnv from ahserver.filestorage import FileStorage @@ -130,6 +131,55 @@ def typevalue(v, t): return v return f(v) +# ── 忙闲时时段解析(2026-09-10)── +# peak_times 串语法:逗号分隔的时段,每段 "H:MM =~= H:MM"(含两端)或 +# "H:MM =~ H:MM"(下含上开);跨午夜段写成两段(如 22:00 =~ 24:00,0:00 =~ 8:00)。 +# 比较单位是小时(浮点),与出账注入的 hour(int 小时数)对齐: +# 9:00 =~= 12:00 覆盖 hour∈{9,10,11,12}(12点整落在忙时内)。 +_PEAK_SEG_RE = None + +def _parse_hhmm(s): + s = (s or '').strip() + if ':' in s: + h, m = s.split(':', 1) + return float(h) + float(m) / 60.0 + return float(s) + +def _in_peak_times(peak_times, hour): + """hour(0-23 的整数小时或浮点小时)是否落在 peak_times 任一时段内。 + + hour 为 None(上游未注入调用时刻)→ 返回 False(按非忙时兜底,不炸计费)。 + 时段串解析失败 → False 并记异常日志(宁可走闲时价也不抛异常中断出账)。 + """ + if hour is None: + return False + try: + hv = float(hour) + except (TypeError, ValueError): + return False + for seg in str(peak_times).split(','): + seg = seg.strip() + if not seg: + continue + m = re.match(r'^([^~]+?)(=~=|=~|~=|~)([^~]+)$', seg) + if not m: + exception(f'peak_times 段解析失败: {seg}') + continue + lo = _parse_hhmm(m.group(1)) + hi = _parse_hhmm(m.group(3)) + op = m.group(2) + if op == '=~=': + ok = lo <= hv <= hi + elif op == '=~': + ok = lo <= hv < hi + elif op == '~=': + ok = lo < hv <= hi + else: + ok = lo < hv < hi + if ok: + return True + return False + # ── 展示层:filter/tier 维度值可读化(2026-09-09,仅展示不参与计费比较)── # 区间操作符 → 数学区间记号,语义与 check_value 的 between 分支一一对应 _INTERVAL_BRACKETS = { @@ -142,12 +192,15 @@ _INTERVAL_BRACKETS = { def beautify_filter_value(v, fdef=None, value_mode=None): """把 filter/tier 维度值渲染成可读文本(纯展示,绝不参与计费比较)。 + - bool(is_peak 等计算字段):True/False → 是/否 - value_mode=between:原始 '0 =~= 512000' → '[0, 512000]', '8 =~ 22' → '[8, 22)',操作符语义对齐 check_value,无歧义 - value_mode=in:'a b c' → 'a / b / c' - 其余:原样返回(如 resolution=1K) 数字保持原值(不转「万」、不加千分位)——与实际比较值一字不差。 """ + if isinstance(v, bool): + return '是' if v else '否' vm = value_mode if vm is None and isinstance(fdef, dict): vm = fdef.get('value_mode') @@ -576,6 +629,19 @@ order by b.enabled_date desc""" for field_name, field_def in d.fields.items(): if not isinstance(field_def, dict): continue + # 忙闲时计算字段(2026-09-10):fields 里声明 peak_times 时段串, + # 引擎按调用时刻(usage 的 hour 键,出账侧从 llm_usage.created_at 注入) + # 计算 is_peak 布尔值,pricings 用 is_peak: true/false 作定价要素。 + # 例:peak_times: "9:00 =~= 12:00,14:00 =~= 18:00" + # is_peak: {type: bool, role: filter, label: 忙时, derived: peak_times} + # hour 缺失(老流水/上游未注入)时按非忙时兜底,不炸计费。 + peak_times = field_def.get('peak_times') + if peak_times: + config_data[field_name] = _in_peak_times( + peak_times, config_data.get('hour')) + debug(f'peak_times field {field_name} = {config_data[field_name]} ' + f'(hour={config_data.get("hour")}, peak_times={peak_times})') + continue derived_expr = field_def.get('derived') if not derived_expr: continue