|
@@ -0,0 +1,111 @@
|
|
|
|
|
+'use strict';
|
|
|
|
|
+
|
|
|
|
|
+// 经营数据定时自动同步
|
|
|
|
|
+// ----------------------------------------------------------------------------
|
|
|
|
|
+// 每分钟检查一次:到达配置时间(默认 06:00,本地时区)且今天还没跑过、
|
|
|
|
|
+// 商家客户端调试端口在线时,自动同步「昨日」数据进本地数据湖。
|
|
|
|
|
+// - 客户端不在线 → 每分钟重试,当天在线的任意时刻补跑(跨夜作废)
|
|
|
|
|
+// - 同步失败也计为已跑(避免分钟级重试刷屏);次日再试
|
|
|
|
|
+// - 默认关闭,config.json → autoSync.enabled 开启;手动同步不受影响
|
|
|
|
|
+// - 同步记录照常写入数据湖 syncs,可在「数据报表 → 同步记录」里查看
|
|
|
|
|
+
|
|
|
|
|
+const fs = require('fs');
|
|
|
|
|
+const path = require('path');
|
|
|
|
|
+const naposService = require('../../napos-service.js');
|
|
|
|
|
+
|
|
|
|
|
+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(`自动同步开始:同步昨日(${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'
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+ } 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 };
|