| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103 |
- 'use strict';
- /**
- * 智能问数 · 指标查询工具
- * ----------------------------------------------------------------------------
- * metric_query:按指标 key 查逐日序列(支持派生指标如 7 日移动平均)。
- * 模型引用指标 key,聚合口径全部由指标层决定——比率加权/比值/移动平均都不许自算。
- * 移动平均窗口取「当日及前 N-1 天内已有数据的天」(缺数日按实算,不插值)。
- */
- const metrics = require('../metrics.js');
- const { eachDay, ymdDash } = require('./lake.js');
- const z2 = (n) => String(n).padStart(2, '0');
- function dayShift(ymd, delta) {
- const d = new Date(Number(ymd.slice(0, 4)), Number(ymd.slice(4, 6)) - 1, Number(ymd.slice(6, 8)));
- d.setDate(d.getDate() + delta);
- return `${d.getFullYear()}${z2(d.getMonth() + 1)}${z2(d.getDate())}`;
- }
- function metricSanitize(args, ctx) {
- const a = args || {};
- const m = metrics.get(String(a.metric || ''));
- if (!m) {
- throw new Error(`未知指标:${a.metric}。可用指标:\n${metrics.manifestText()}`);
- }
- const { shops, dateRange } = ctx;
- const from = /^\d{8}$/.test(String(a.from || '')) ? String(a.from) : dateRange.from;
- const to = /^\d{8}$/.test(String(a.to || '')) ? String(a.to) : dateRange.to;
- let shopIds = null;
- if (Array.isArray(a.shops) && a.shops.length) {
- const nlQuery = require('../../../nl-query.js');
- shopIds = nlQuery.matchShopsByName(shops, a.shops);
- if (!shopIds) throw new Error(`门店名未匹配:${a.shops.join('、')}`);
- }
- return {
- metric: String(a.metric),
- from: from < dateRange.from ? dateRange.from : from,
- to: to > dateRange.to ? dateRange.to : to,
- shopIds,
- window: m.window || 1,
- };
- }
- function metricHandler(ctx, args) {
- const { svc, entityKey } = ctx;
- const { metric, from, to, shopIds, window } = args;
- const def = metrics.get(metric);
- const reg = new Map((svc.REPORT_COLUMNS || []).map((c) => [c.key, c]));
- const unitFmt = { money: (v) => `${Math.round(v).toLocaleString('zh-CN')}元`, pct: (v) => `${(v * 100).toFixed(2)}%`, int: (v) => String(Math.round(v)) };
- // 派生移动平均需要窗口前置数据:多取 window-1 天
- const leadFrom = window > 1 ? dayShift(from, -(window - 1)) : from;
- let rows = svc.store.queryRows(entityKey, { from: leadFrom, to, scope: 'shop' });
- if (shopIds) {
- const wanted = new Set(shopIds.map(String));
- rows = rows.filter((r) => wanted.has(String(r.shopId)));
- }
- const byDate = new Map();
- for (const r of rows) {
- if (!byDate.has(r.date)) byDate.set(r.date, []);
- byDate.get(r.date).push(r);
- }
- const dayVal = (d) => metrics.dayValue(def.base || metric, byDate.get(d) || []);
- const days = eachDay(from, to);
- const series = [];
- for (const d of days) {
- let value = null;
- if (window > 1) {
- const win = [];
- for (let i = 0; i < window; i++) {
- const v = dayVal(dayShift(d, -i));
- if (v != null) win.push(v);
- }
- value = win.length ? +(win.reduce((s, x) => s + x, 0) / win.length).toFixed(2) : null;
- } else {
- value = dayVal(d);
- }
- series.push({ date: d, value });
- }
- const values = series.map((s) => s.value).filter((v) => v != null);
- const fmt = unitFmt[def.unit] || ((v) => String(v));
- const lines = [`${def.name}(${ymdDash(from)} ~ ${ymdDash(to)}${window > 1 ? `,${window} 日移动平均,缺数日按实算` : ''}):`];
- for (const s of series) lines.push(` ${ymdDash(s.date)}:${s.value == null ? '无数据' : fmt(s.value)}`);
- if (values.length) {
- const avg = values.reduce((s, x) => s + x, 0) / values.length;
- lines.push(`区间:均值 ${fmt(+avg.toFixed(2))},最高 ${fmt(Math.max(...values))},最低 ${fmt(Math.min(...values))}。`);
- } else {
- lines.push('区间内无数据。');
- }
- return {
- summary: lines.join('\n'),
- data: {
- metric, name: def.name, unit: def.unit, from, to, window,
- series, totalRows: series.length,
- stats: values.length ? { avg: +(values.reduce((s, x) => s + x, 0) / values.length).toFixed(2), max: Math.max(...values), min: Math.min(...values) } : null,
- },
- };
- }
- module.exports = { metricSanitize, metricHandler };
|