| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101 |
- 'use strict';
- /**
- * 智能问数智能体 · 湖↔云比对工具
- * ----------------------------------------------------------------------------
- * aitable_verify:本地数据湖 vs 钉钉 AI 表格(在线主档案)只读比对。
- * 行数按粒度对比 + 共同行键的营业额抽样值比对,只读不写(记录接口无建列限流,
- * 但仍控制在整表拉一次)。云端未启用/配置不齐时优雅降级为说明性结论。
- * ctx.cloud 由路由层装配(aitable 服务实例);未装配时同样优雅降级。
- */
- const { ymdDash } = require('./lake.js');
- function cloudSanitize(args, ctx) {
- const a = args || {};
- const { 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;
- 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) };
- }
- function rowKey(row) {
- return `${row.date}|${row.scope || ''}|${row.chainId == null ? '' : String(row.chainId)}|${row.shopId == null ? '' : String(row.shopId)}`;
- }
- async function cloudHandler(ctx, args) {
- const { svc, entityKey, cloud } = ctx;
- const { from, to, sample } = args;
- if (!cloud) {
- return {
- summary: '湖↔云比对不可用:云端服务未装配(仅云端协同模式提供)。',
- data: {
- available: false,
- verdicts: [{ kind: 'cloud', check: '湖↔云比对', status: 'warn', detail: '云端服务未装配' }],
- },
- };
- }
- let snapshot;
- try {
- snapshot = await cloud.snapshotForVerify(entityKey);
- } catch (e) {
- return {
- summary: `湖↔云比对不可用:${e.message || e}(云端未启用或配置不齐时属预期;如需比对请先到系统设置开启并测试连通)。`,
- data: { available: false, reason: String(e.message || e).slice(0, 200) },
- verdicts: [{ kind: 'cloud', check: '湖↔云比对', status: 'warn', detail: `云端不可用:${String(e.message || e).slice(0, 80)}` }],
- };
- }
- const inRange = (r) => String(r.date || '') >= from && String(r.date || '') <= to;
- const lines = [`湖↔云比对(区间 ${ymdDash(from)} ~ ${ymdDash(to)},业务键 日期|维度|连锁|门店):`];
- const perScope = [];
- let totalDiff = 0;
- const samples = [];
- for (const scope of ['shop', 'chain', 'company']) {
- const localRows = svc.store.queryRows(entityKey, { from, to, scope }).filter(inRange);
- const cloudRows = (snapshot[scope] || []).filter(inRange);
- const localMap = new Map(localRows.map((r) => [rowKey(r), r]));
- const cloudMap = new Map(cloudRows.map((r) => [rowKey(r), r]));
- let onlyLocal = 0;
- let onlyCloud = 0;
- let valueDiff = 0;
- const shared = [];
- for (const [k, r] of localMap) {
- if (!cloudMap.has(k)) { onlyLocal += 1; continue; }
- shared.push([k, r, cloudMap.get(k)]);
- }
- for (const k of cloudMap.keys()) if (!localMap.has(k)) onlyCloud += 1;
- for (const [k, lr, cr] of shared) {
- const lv = Number(lr.grossAmt) || 0;
- const cv = Number(cr.grossAmt) || 0;
- if (Math.abs(lv - cv) > Math.max(lv * 0.005, 1)) {
- valueDiff += 1;
- if (samples.length < sample) samples.push({ scope, key: k, local: Math.round(lv * 100) / 100, cloud: Math.round(cv * 100) / 100 });
- }
- }
- perScope.push({ scope, local: localRows.length, cloud: cloudRows.length, onlyLocal, onlyCloud, valueDiff, shared: shared.length });
- totalDiff += onlyLocal + onlyCloud + valueDiff;
- }
- const label = { shop: '门店明细', chain: '连锁汇总', company: '公司汇总' };
- for (const s of perScope) {
- lines.push(` ${label[s.scope]}:本地 ${s.local} 行 / 云端 ${s.cloud} 行(仅本地 ${s.onlyLocal}、仅云端 ${s.onlyCloud}、抽样值不一致 ${s.valueDiff})`);
- }
- if (samples.length) {
- lines.push(`抽样值差异示例:${samples.map((s) => `${label[s.scope]} ${s.key} 本地 ${s.local} vs 云端 ${s.cloud}`).join(';')}`);
- }
- const ok = totalDiff === 0;
- if (!ok) lines.push('湖与云端存在差异:通常由「上次同步/推送之后本地又有新数据」导致,可到数据采集页推送或从中台拉取后再问。');
- else lines.push('湖与云端一致。');
- return {
- summary: lines.join('\n'),
- data: {
- available: true, from, to, perScope, samples,
- verdicts: [{
- kind: 'cloud', check: `湖↔云一致性(${ymdDash(from)} ~ ${ymdDash(to)})`,
- status: ok ? 'ok' : 'warn',
- detail: ok ? '三表行数与抽样值一致' : `差异 ${totalDiff} 处(行数或抽样值)`,
- }],
- },
- };
- }
- module.exports = { cloudSanitize, cloudHandler };
|