Przeglądaj źródła

fix: 系统日志文案审计标准化 + 三表名输入框一行三列

日志规范:<动作>开始( info )/<动作>完成( success|warn )/<动作>失败( error ),
动作名与 UI 口径一致(推送云端/云端拉取/同步),详情带主体名与行数:
- 「AI 表格推送/拉取完成」→「推送云端完成/云端拉取完成」,统一带主体名;
  推送摘要 pushSummary 收敛到 aitable.js 导出,手动/同步后/定时三条链路共用
- 「同步结束(失败)」→「同步失败」并升 error;补「同步完成」系统日志(此前只有失败)
- 补长操作开始日志:推送云端开始(全量/增量 N 行)、云端拉取开始
- 「自动同步开始:同步昨日(20261016)」→「…昨日(2026-10-16)…」日期可读
- 「数据报表:已清除主体 X 的本地数据」→「本地数据已清除:主体 X」
- 设置保存日志补齐分域变更摘要(对话模型/钉钉 AI 表格/自动同步/访问口令)

设置页三个表名输入框由竖排改为一行三列(.sheet-names-grid,窄屏自动换行),
内联样式收敛进 CSS。
KenZ1117 2 tygodni temu
rodzic
commit
e42ce28fe4

+ 5 - 3
public/index.html

@@ -701,9 +701,11 @@
                 <div class="field">
                   <label>Base ID / 三张数据表名称</label>
                   <input type="text" id="settingsAitableBaseId" placeholder="粘贴 AI 表格分享链接或直接填 Base ID" autocomplete="off">
-                  <input type="text" id="settingsAitableSheetDetail" placeholder="门店明细" autocomplete="off" style="margin-top:6px;">
-                  <input type="text" id="settingsAitableSheetChain" placeholder="连锁汇总" autocomplete="off" style="margin-top:6px;">
-                  <input type="text" id="settingsAitableSheetCompany" placeholder="公司汇总" autocomplete="off" style="margin-top:6px;">
+                  <div class="sheet-names-grid">
+                    <input type="text" id="settingsAitableSheetDetail" placeholder="门店明细" autocomplete="off">
+                    <input type="text" id="settingsAitableSheetChain" placeholder="连锁汇总" autocomplete="off">
+                    <input type="text" id="settingsAitableSheetCompany" placeholder="公司汇总" autocomplete="off">
+                  </div>
                   <div class="help">Base ID 可直接粘贴分享链接(形如 alidocs.dingtalk.com/i/nodes/xxx),保存时自动提取 ID。云端按维度拆三张表(门店明细/连锁汇总/公司汇总),每张表内粒度唯一,钉钉 AI 问数/人工聚合不会跨维度算重;表名留空用默认值。</div>
                 </div>
                 <div class="settings-links">

+ 2 - 0
public/style.css

@@ -167,6 +167,8 @@ main.content { padding: var(--page-padding-y) var(--page-padding-x) var(--page-p
 .settings-col { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
 /* 显式定位:1 API设置 左上,2 采集配置 右列纵跨两行,3 安全设置 紧贴 1 下方(避免行对齐把 3 推远) */
 .settings-forms .field textarea { min-height: 78px; resize: vertical; line-height: 1.5; }
+/* 钉钉 AI 表格三张表名:一行三列(窄屏自动换行) */
+.sheet-names-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(110px, 1fr)); gap: 8px; margin-top: 6px; }
 .settings-forms .panel,
 .panel-designed,
 .settings-log { position: relative; overflow: hidden; background: linear-gradient(135deg, #fff 0%, #faf8f5 100%); border-radius: 14px; transition: border-color 0.18s var(--ease-out), box-shadow 0.18s var(--ease-out); }

+ 16 - 19
server/routes/reports.js

@@ -8,16 +8,7 @@ const { makeChatCfg } = require('../context.js');
 const naposService = require('../../napos-service.js');
 const nlQuery = require('../../nl-query.js');
 const reportHtml = require('../../report-html.js');
-
-/** 三表推送结果的一行摘要(日志用):合计 + 分表明细 */
-function aitablePushSummary(r) {
-  const base = `新增 ${r.created} 行、更新 ${r.updated} 行`;
-  const parts = Object.entries(r.perSheet || {})
-    .filter(([, s]) => s.rows > 0)
-    .map(([scope, s]) => `${s.sheet} 新增${s.created}/更新${s.updated}`)
-    .join(',');
-  return parts ? `${base}(${parts})` : base;
-}
+const { pushSummary } = require('../services/aitable.js');
 
 function register(app, ctx) {
   // 不限方法(保持拆分前行为:该路由未做方法检查)
@@ -129,18 +120,24 @@ function register(app, ctx) {
       chainId: body.chainId || '',
       shopIds: Array.isArray(body.shopIds) ? body.shopIds : undefined,
     }).catch((e) => {
-      // 同步失败也要让自动推送保持静默,但保留日志
-      ctx.logs.pushSystemLog(`同步结束(失败):${e.message || e}`, 'warn');
+      ctx.logs.pushSystemLog(`同步失败:${e.message || e}`, 'error');
       return null;
     });
-    // 同步完成后:若启用钉钉 AI 表格,自动把增量推上云端(失败不影响本地同步结果)
+    // 同步完成后:若启用钉钉 AI 表格,自动把增量推送云端(失败不影响本地同步结果)
     entityPromise.then(async (snap) => {
-      if (!snap || !snap.entity || !(ctx.config.aitable && ctx.config.aitable.enabled)) return;
+      if (!snap) return; // 失败已在 catch 记日志
+      if (snap.entity) {
+        ctx.logs.pushSystemLog(
+          `同步完成:${snap.entity.name},新增 ${snap.added} 行、更新 ${snap.updated} 行、失败 ${snap.failed} 项`,
+          snap.failed > 0 ? 'warn' : 'success'
+        );
+      }
+      if (!snap.entity || !(ctx.config.aitable && ctx.config.aitable.enabled)) return;
       try {
         const r = await ctx.aitable.pushLake(snap.entity.key);
-        ctx.logs.pushSystemLog(`AI 表格推送完成:${aitablePushSummary(r)}`, 'success');
+        ctx.logs.pushSystemLog(`推送云端完成:${r.account ? r.account + ',' : ''}${pushSummary(r)}`, 'success');
       } catch (e) {
-        ctx.logs.pushSystemLog(`AI 表格推送失败:${e.message || e}`, 'error');
+        ctx.logs.pushSystemLog(`推送云端失败:${e.message || e}`, 'error');
       }
     });
     return sendJson(res, 200, { ok: true, ...naposService.syncSnapshot() });
@@ -158,7 +155,7 @@ function register(app, ctx) {
     const entity = body.entity || (naposService.store.listEntities()[0] || {}).key;
     if (!entity) { const e = new Error('暂无数据主体,请先完成一次同步'); e.code = 'AITABLE_NO_ENTITY'; e.status = 400; throw e; }
     const r = await ctx.aitable.pushLake(entity, { force: !!body.force });
-    ctx.logs.pushSystemLog(`AI 表格推送完成:${aitablePushSummary(r)}`, 'success');
+    ctx.logs.pushSystemLog(`推送云端完成:${r.account ? r.account + ',' : ''}${pushSummary(r)}`, 'success');
     sendJson(res, 200, r);
   });
 
@@ -166,7 +163,7 @@ function register(app, ctx) {
     const body = await readBody(req);
     const r = await ctx.aitable.pullLake(body.entity);
     const accounts = (r.accounts || []).map((a) => `${a.account}(新增${a.added}/更新${a.updated})`).join('、');
-    ctx.logs.pushSystemLog(`AI 表格拉取完成:${r.pulled} 行 ${accounts}`, 'success');
+    ctx.logs.pushSystemLog(`云端拉取完成:${r.pulled} 行${accounts ? `(${accounts})` : ''}`, 'success');
     sendJson(res, 200, r);
   });
 
@@ -249,7 +246,7 @@ function register(app, ctx) {
   app.del('/api/reports/data', async ({ res, query }) => {
     if (!query.entity) return sendJson(res, 400, { error: '缺少 entity 参数' });
     naposService.store.deleteEntityData(query.entity);
-    ctx.logs.pushSystemLog(`数据报表:已清除主体 ${query.entity} 的本地数据`, 'warn');
+    ctx.logs.pushSystemLog(`本地数据已清除:主体 ${query.entity}`, 'warn');
     sendJson(res, 200, { ok: true });
   });
 }

+ 11 - 2
server/routes/settings.js

@@ -120,10 +120,19 @@ function register(app, ctx) {
       ctx.session.cookie = body.cookie;
       ctx.session.saveSession();
     }
+    // 变更摘要:只列本次实际提交过的面板域(设置页按面板分域保存)
+    const changed = [];
+    if (previousApiKey !== config.apiKey) changed.push('API Key 已更新');
+    if (body.cookie !== undefined && previousCookie !== ctx.session.cookie) changed.push('采集 Cookie 已更新');
+    if (body.chatModel !== undefined || body.chatApiBase !== undefined || body.chatApiKey !== undefined) changed.push('对话模型已更新');
+    if (body.aitableEnabled !== undefined || body.aitableAppKey !== undefined || body.aitableBaseId !== undefined
+      || body.aitableSheetDetail !== undefined || body.aitableSheetChain !== undefined || body.aitableSheetCompany !== undefined
+      || body.aitableAppSecret !== undefined || body.aitableOperatorId !== undefined) changed.push('钉钉 AI 表格配置已更新');
+    if (body.autoSyncEnabled !== undefined || body.autoSyncTime !== undefined) changed.push('自动同步已更新');
+    if (body.accessToken !== undefined) changed.push('访问口令已更新');
     ctx.logs.pushSystemLog(
       `系统设置已保存:模型=${config.model},根目录=${config.rootDir},每页=${config.pageSize},间隔=${config.delayMs}ms` +
-      (previousApiKey !== config.apiKey ? ',API Key 已更新' : '') +
-      (body.cookie !== undefined && previousCookie !== ctx.session.cookie ? ',Cookie 已更新' : ''),
+      (changed.length ? `,${changed.join(',')}` : ''),
       'success'
     );
     sendJson(res, 200, { ok: true });

+ 14 - 1
server/services/aitable.js

@@ -147,6 +147,16 @@ function resolveSheets(cfg) {
   return { sheets, fingerprint: [sheets.detail, sheets.chain, sheets.company].join('|') };
 }
 
+/** 推送结果的一行摘要(日志与状态徽标共用):合计 + 分表明细 */
+function pushSummary(r) {
+  const base = `新增 ${r.created} 行、更新 ${r.updated} 行`;
+  const parts = Object.entries(r.perSheet || {})
+    .filter(([, s]) => s.rows > 0)
+    .map(([, s]) => `${s.sheet} 新增${s.created}/更新${s.updated}`)
+    .join(',');
+  return parts ? `${base}(${parts})` : base;
+}
+
 function createAitableService(ctx) {
   const { config, workspace, logs } = ctx;
 
@@ -327,6 +337,8 @@ function createAitableService(ctx) {
     const parts = splitRowsBySheet(rows);
     const pushed = parts.detail.length + parts.chain.length + parts.company.length;
     if (!pushed) return { pushed: 0, created: 0, updated: 0, skippedSummary: parts.skippedSummary, skipped: '无新增数据' };
+    // 大批量(尤其首推全量)耗时较长,开始时先留痕,避免长时间静默
+    logs.pushSystemLog(`推送云端开始:${accountName}(${watermark ? '增量' : '全量'} ${pushed} 行)`, 'info');
 
     // 全零保护:大批量门店行指标全零基本可断定取数异常(已知门店级 bug 的症状),
     // 防止把坏数据灌进云端主档案;确认要推(force)可绕过。
@@ -369,6 +381,7 @@ function createAitableService(ctx) {
   async function pullLake(entity) {
     const cfg = requireConfig();
     const naposService = require('../../napos-service.js');
+    logs.pushSystemLog(`云端拉取开始${entity ? `:${entity}` : '(全部账号)'}`, 'info');
     const { sheets } = resolveSheets(cfg);
     const records = [];
     for (const sheetName of [sheets.detail, sheets.chain, sheets.company]) {
@@ -445,5 +458,5 @@ function createAitableService(ctx) {
 
 module.exports = {
   createAitableService, FIELD_COLUMNS, DIMENSION_LABEL, SHEET_DEFAULTS,
-  rowToRecord, recordKey, recordToRow, splitRowsBySheet, resolveSheets,
+  rowToRecord, recordKey, recordToRow, splitRowsBySheet, resolveSheets, pushSummary,
 };

+ 8 - 4
server/services/auto-sync.js

@@ -12,6 +12,10 @@
 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();
@@ -80,7 +84,7 @@ function createAutoSyncService(ctx) {
     running = true;
     const from = ymdShift(today, -1);
     saveState({ ...state, lastRunDate: today, lastTriggeredAt: new Date().toISOString() });
-    logs.pushSystemLog(`自动同步开始:同步昨日(${from})经营数据`, 'info');
+    logs.pushSystemLog(`自动同步开始:昨日(${ymdDash(from)})经营数据`, 'info');
     try {
       const snap = await naposService.runSync({ from, to: from, scope: 'all' });
       if (snap.error) {
@@ -90,13 +94,13 @@ function createAutoSyncService(ctx) {
           `自动同步完成:${snap.entity ? snap.entity.name + ',' : ''}新增 ${snap.added} 行、更新 ${snap.updated} 行、失败 ${snap.failed} 项`,
           snap.failed > 0 ? 'warn' : 'success'
         );
-        // 同步完成后:若启用钉钉 AI 表格,自动把增量推上云端(失败不影响本地同步结果)。
+        // 同步完成后:若启用钉钉 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(`AI 表格自动推送完成:新增 ${r.created} 行、更新 ${r.updated} 行`, 'success');
+            logs.pushSystemLog(`推送云端完成:${r.account ? r.account + ',' : ''}${pushSummary(r)}`, 'success');
           }).catch((e) => {
-            logs.pushSystemLog(`AI 表格自动推送失败:${e.message || e}`, 'error');
+            logs.pushSystemLog(`推送云端失败:${e.message || e}`, 'error');
           });
         }
       }