| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132 |
- 'use strict';
- // 经营数据定时自动同步
- // ----------------------------------------------------------------------------
- // 每分钟检查一次:到达配置时间(默认 06:00,本地时区)且今天还没跑过、
- // 商家客户端调试端口在线时,自动同步「昨日」数据进本地数据湖。
- // - 客户端不在线 → 每分钟重试,当天在线的任意时刻补跑(跨夜作废)
- // - 同步失败也计为已跑(避免分钟级重试刷屏);次日再试
- // - 默认关闭,config.json → autoSync.enabled 开启;手动同步不受影响
- // - 同步记录照常写入数据湖 syncs,可在「数据报表 → 同步记录」里查看
- const fs = require('fs');
- const path = require('path');
- const naposService = require('../../napos-service.js');
- const { pushSummary } = require('./aitable.js');
- /** YYYYMMDD → YYYY-MM-DD(日志可读) */
- function ymdDash(v) { return String(v || '').replace(/^(\d{4})(\d{2})(\d{2})$/, '$1-$2-$3'); }
- function todayYmd() {
- const d = new Date();
- const z = (n) => String(n).padStart(2, '0');
- return `${d.getFullYear()}${z(d.getMonth() + 1)}${z(d.getDate())}`;
- }
- function ymdShift(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);
- const z = (n) => String(n).padStart(2, '0');
- return `${d.getFullYear()}${z(d.getMonth() + 1)}${z(d.getDate())}`;
- }
- function createAutoSyncService(ctx) {
- const { config, workspace, logs } = ctx;
- const stateFile = () => path.join(workspace.getWorkspaceRoot(), '.genpic', 'auto-sync.json');
- function loadState() {
- try {
- const parsed = JSON.parse(fs.readFileSync(stateFile(), 'utf8'));
- return parsed && typeof parsed.lastRunDate === 'string' ? parsed : {};
- } catch {
- return {};
- }
- }
- function saveState(state) {
- try {
- const file = stateFile();
- fs.mkdirSync(path.dirname(file), { recursive: true });
- const temp = `${file}.${process.pid}.tmp`;
- fs.writeFileSync(temp, JSON.stringify(state, null, 2), 'utf8');
- fs.renameSync(temp, file);
- } catch { /* 状态落盘失败不阻断同步 */ }
- }
- let running = false; // 本服务的单飞标记(与 naposService 的 SYNC_BUSY 双保险)
- async function tick() {
- const cfg = config.autoSync || {};
- if (!cfg.enabled) return;
- if (running) return;
- if (naposService.syncSnapshot().running) return; // 手动同步进行中,让路
- const time = /^\d{1,2}:\d{2}$/.test(String(cfg.time || '')) ? cfg.time : '06:00';
- const [hh, mm] = time.split(':').map(Number);
- const now = new Date();
- const nowMinutes = now.getHours() * 60 + now.getMinutes();
- const schedMinutes = Math.min(23, Math.max(0, hh)) * 60 + Math.min(59, Math.max(0, mm));
- if (nowMinutes < schedMinutes) return;
- const today = todayYmd();
- const state = loadState();
- if (state.lastRunDate === today) return;
- let status;
- try {
- status = await naposService.naposStatus();
- } catch {
- return; // 探测异常,下一分钟再试
- }
- if (!status.connected) return; // 客户端不在线,等待在线时刻补跑
- running = true;
- const from = ymdShift(today, -1);
- saveState({ ...state, lastRunDate: today, lastTriggeredAt: new Date().toISOString() });
- logs.pushSystemLog(`自动同步开始:昨日(${ymdDash(from)})经营数据`, 'info');
- try {
- const snap = await naposService.runSync({ from, to: from, scope: 'all' });
- if (snap.error) {
- logs.pushSystemLog(`自动同步失败:${snap.error}`, 'error');
- } else {
- logs.pushSystemLog(
- `自动同步完成:${snap.entity ? snap.entity.name + ',' : ''}新增 ${snap.added} 行、更新 ${snap.updated} 行、失败 ${snap.failed} 项`,
- snap.failed > 0 ? 'warn' : 'success'
- );
- // 同步完成后:若启用钉钉 AI 表格,自动把增量推送云端(失败不影响本地同步结果)。
- // 与手动同步路由同款 fire-and-forget;此前定时同步不推云端属遗漏。
- if (snap.entity && ctx.config.aitable && ctx.config.aitable.enabled && ctx.aitable) {
- ctx.aitable.pushLake(snap.entity.key).then((r) => {
- logs.pushSystemLog(`推送云端完成:${r.account ? r.account + ',' : ''}${pushSummary(r)}`, 'success');
- }).catch((e) => {
- logs.pushSystemLog(`推送云端失败:${e.message || e}`, 'error');
- });
- }
- // 同步完成 → 生成当日晨报(确定性口径:体检 + 层间核对 + 环比,零模型;
- // 结果落问数页顶部横幅展示,钉钉推送通道后续按需接入)
- if (snap.entity) {
- const { buildAndSaveDigest } = require('../agent/digest.js');
- buildAndSaveDigest(snap.entity.key, naposService)
- .then((d) => logs.pushSystemLog(`晨报已生成:${d.headline}`, d.status === 'fail' ? 'warn' : 'info'))
- .catch((e) => logs.pushSystemLog(`晨报生成失败:${e.message || e}`, 'warn'));
- }
- }
- } catch (e) {
- logs.pushSystemLog(`自动同步失败:${e.message || e}`, 'error');
- } finally {
- running = false;
- }
- }
- const timer = setInterval(() => { tick().catch(() => {}); }, 60 * 1000);
- timer.unref();
- return {
- /** 立即执行一次检查(供测试/手动触发) */
- tickNow: tick,
- stop: () => clearInterval(timer),
- };
- }
- module.exports = { createAutoSyncService };
|