cloud.js 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101
  1. 'use strict';
  2. /**
  3. * 智能问数智能体 · 湖↔云比对工具
  4. * ----------------------------------------------------------------------------
  5. * aitable_verify:本地数据湖 vs 钉钉 AI 表格(在线主档案)只读比对。
  6. * 行数按粒度对比 + 共同行键的营业额抽样值比对,只读不写(记录接口无建列限流,
  7. * 但仍控制在整表拉一次)。云端未启用/配置不齐时优雅降级为说明性结论。
  8. * ctx.cloud 由路由层装配(aitable 服务实例);未装配时同样优雅降级。
  9. */
  10. const { ymdDash } = require('./lake.js');
  11. function cloudSanitize(args, ctx) {
  12. const a = args || {};
  13. const { dateRange } = ctx;
  14. const from = /^\d{8}$/.test(String(a.from || '')) ? String(a.from) : dateRange.from;
  15. const to = /^\d{8}$/.test(String(a.to || '')) ? String(a.to) : dateRange.to;
  16. return { from: from < dateRange.from ? dateRange.from : from, to: to > dateRange.to ? dateRange.to : to, sample: Math.min(Number(a.sample) > 0 ? Number(a.sample) : 5, 10) };
  17. }
  18. function rowKey(row) {
  19. return `${row.date}|${row.scope || ''}|${row.chainId == null ? '' : String(row.chainId)}|${row.shopId == null ? '' : String(row.shopId)}`;
  20. }
  21. async function cloudHandler(ctx, args) {
  22. const { svc, entityKey, cloud } = ctx;
  23. const { from, to, sample } = args;
  24. if (!cloud) {
  25. return {
  26. summary: '湖↔云比对不可用:云端服务未装配(仅云端协同模式提供)。',
  27. data: {
  28. available: false,
  29. verdicts: [{ kind: 'cloud', check: '湖↔云比对', status: 'warn', detail: '云端服务未装配' }],
  30. },
  31. };
  32. }
  33. let snapshot;
  34. try {
  35. snapshot = await cloud.snapshotForVerify(entityKey);
  36. } catch (e) {
  37. return {
  38. summary: `湖↔云比对不可用:${e.message || e}(云端未启用或配置不齐时属预期;如需比对请先到系统设置开启并测试连通)。`,
  39. data: { available: false, reason: String(e.message || e).slice(0, 200) },
  40. verdicts: [{ kind: 'cloud', check: '湖↔云比对', status: 'warn', detail: `云端不可用:${String(e.message || e).slice(0, 80)}` }],
  41. };
  42. }
  43. const inRange = (r) => String(r.date || '') >= from && String(r.date || '') <= to;
  44. const lines = [`湖↔云比对(区间 ${ymdDash(from)} ~ ${ymdDash(to)},业务键 日期|维度|连锁|门店):`];
  45. const perScope = [];
  46. let totalDiff = 0;
  47. const samples = [];
  48. for (const scope of ['shop', 'chain', 'company']) {
  49. const localRows = svc.store.queryRows(entityKey, { from, to, scope }).filter(inRange);
  50. const cloudRows = (snapshot[scope] || []).filter(inRange);
  51. const localMap = new Map(localRows.map((r) => [rowKey(r), r]));
  52. const cloudMap = new Map(cloudRows.map((r) => [rowKey(r), r]));
  53. let onlyLocal = 0;
  54. let onlyCloud = 0;
  55. let valueDiff = 0;
  56. const shared = [];
  57. for (const [k, r] of localMap) {
  58. if (!cloudMap.has(k)) { onlyLocal += 1; continue; }
  59. shared.push([k, r, cloudMap.get(k)]);
  60. }
  61. for (const k of cloudMap.keys()) if (!localMap.has(k)) onlyCloud += 1;
  62. for (const [k, lr, cr] of shared) {
  63. const lv = Number(lr.grossAmt) || 0;
  64. const cv = Number(cr.grossAmt) || 0;
  65. if (Math.abs(lv - cv) > Math.max(lv * 0.005, 1)) {
  66. valueDiff += 1;
  67. if (samples.length < sample) samples.push({ scope, key: k, local: Math.round(lv * 100) / 100, cloud: Math.round(cv * 100) / 100 });
  68. }
  69. }
  70. perScope.push({ scope, local: localRows.length, cloud: cloudRows.length, onlyLocal, onlyCloud, valueDiff, shared: shared.length });
  71. totalDiff += onlyLocal + onlyCloud + valueDiff;
  72. }
  73. const label = { shop: '门店明细', chain: '连锁汇总', company: '公司汇总' };
  74. for (const s of perScope) {
  75. lines.push(` ${label[s.scope]}:本地 ${s.local} 行 / 云端 ${s.cloud} 行(仅本地 ${s.onlyLocal}、仅云端 ${s.onlyCloud}、抽样值不一致 ${s.valueDiff})`);
  76. }
  77. if (samples.length) {
  78. lines.push(`抽样值差异示例:${samples.map((s) => `${label[s.scope]} ${s.key} 本地 ${s.local} vs 云端 ${s.cloud}`).join(';')}`);
  79. }
  80. const ok = totalDiff === 0;
  81. if (!ok) lines.push('湖与云端存在差异:通常由「上次同步/推送之后本地又有新数据」导致,可到数据采集页推送或从中台拉取后再问。');
  82. else lines.push('湖与云端一致。');
  83. return {
  84. summary: lines.join('\n'),
  85. data: {
  86. available: true, from, to, perScope, samples,
  87. verdicts: [{
  88. kind: 'cloud', check: `湖↔云一致性(${ymdDash(from)} ~ ${ymdDash(to)})`,
  89. status: ok ? 'ok' : 'warn',
  90. detail: ok ? '三表行数与抽样值一致' : `差异 ${totalDiff} 处(行数或抽样值)`,
  91. }],
  92. },
  93. };
  94. }
  95. module.exports = { cloudSanitize, cloudHandler };