entity.js 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778
  1. 'use strict';
  2. /**
  3. * 智能问数智能体 · 跨主体对比工具
  4. * ----------------------------------------------------------------------------
  5. * cross_entity:湖内全部主体(多账号/多食堂)本期 vs 基期核心指标对比,
  6. * 供「跨校对比」「哪家餐厅更好」类问题。窗口锚定当前主体的湖内区间,
  7. * 无本期数据的主体自动滤除并说明。
  8. */
  9. const { ymdDash } = require('./lake.js');
  10. const z2 = (n) => String(n).padStart(2, '0');
  11. function dayShift(ymd, delta) {
  12. const d = new Date(Number(ymd.slice(0, 4)), Number(ymd.slice(4, 6)) - 1, Number(ymd.slice(6, 8)));
  13. d.setDate(d.getDate() + delta);
  14. return `${d.getFullYear()}${z2(d.getMonth() + 1)}${z2(d.getDate())}`;
  15. }
  16. function entitySanitize(args, ctx) {
  17. const a = args || {};
  18. const { dateRange } = ctx;
  19. const dates = ctx.svc.store.datesInLake(ctx.entityKey);
  20. let from = /^\d{8}$/.test(String(a.from || '')) ? String(a.from) : '';
  21. let to = /^\d{8}$/.test(String(a.to || '')) ? String(a.to) : '';
  22. from = from && from >= dateRange.from && from <= dateRange.to ? from : '';
  23. to = to && to >= dateRange.from && to <= dateRange.to ? to : '';
  24. if (!from || !to || from > to) {
  25. if (!dates.length) return { empty: true };
  26. to = dates[dates.length - 1];
  27. from = dates[Math.max(0, dates.length - 7)];
  28. }
  29. const len = Math.max(1, Math.round(
  30. (new Date(Number(to.slice(0, 4)), Number(to.slice(4, 6)) - 1, Number(to.slice(6, 8)))
  31. - new Date(Number(from.slice(0, 4)), Number(from.slice(4, 6)) - 1, Number(from.slice(6, 8)))) / 86400000) + 1);
  32. const prevTo = dayShift(from, -1);
  33. const prevFrom = dayShift(prevTo, -(len - 1));
  34. return { from, to, prevFrom, prevTo, empty: false };
  35. }
  36. function windowTotals(svc, key, from, to) {
  37. const rows = svc.store.queryRows(key, { from, to, scope: 'shop' });
  38. const s = svc.summarizeRows(rows);
  39. return { gross: s.grossAmt, orders: s.validOrderCnt, days: s.dayCount, hasData: rows.length > 0 };
  40. }
  41. function entityHandler(ctx, args) {
  42. const { svc } = ctx;
  43. if (args.empty) return { summary: '当前主体湖内暂无数据,无法跨主体对比。', data: { empty: true } };
  44. const { from, to, prevFrom, prevTo } = args;
  45. const entities = svc.store.listEntities();
  46. const out = [];
  47. const skipped = [];
  48. for (const e of entities) {
  49. const cur = windowTotals(svc, e.key, from, to);
  50. if (!cur.hasData) { skipped.push(e.name || e.key); continue; }
  51. const prev = windowTotals(svc, e.key, prevFrom, prevTo);
  52. out.push({
  53. key: e.key,
  54. name: e.name || e.key,
  55. shopCount: e.shopCount || 0,
  56. cur,
  57. prev,
  58. grossDeltaPct: prev.gross > 0 ? +(((cur.gross - prev.gross) / prev.gross) * 100).toFixed(1) : null,
  59. });
  60. }
  61. out.sort((a, b) => b.cur.gross - a.cur.gross);
  62. const lines = [`跨主体对比(本期 ${ymdDash(from)} ~ ${ymdDash(to)} vs 上期 ${ymdDash(prevFrom)} ~ ${ymdDash(prevTo)},按本期营业额降序):`];
  63. for (const e of out) {
  64. const delta = e.grossDeltaPct == null ? '上期无数据' : `环比 ${e.grossDeltaPct >= 0 ? '+' : ''}${e.grossDeltaPct}%`;
  65. lines.push(` ${e.name}(${e.shopCount} 店):营业额 ${Math.round(e.cur.gross)} / 有效订单 ${Math.round(e.cur.orders)}(${delta})`);
  66. }
  67. if (skipped.length) lines.push(`本期无数据未纳入:${skipped.join('、')}`);
  68. if (!out.length) lines.push('湖内没有任何主体在本期有数据。');
  69. return { summary: lines.join('\n'), data: { from, to, prevFrom, prevTo, entities: out, skipped } };
  70. }
  71. module.exports = { entitySanitize, entityHandler };