Parcourir la source

fix: DingTalk notable live contract — operatorId, update key 'id', default sheet name

KenZ1117 il y a 2 semaines
Parent
commit
e0449fe23c
4 fichiers modifiés avec 17 ajouts et 16 suppressions
  1. 1 1
      public/index.html
  2. 1 1
      server/context.js
  3. 3 3
      server/routes/settings.js
  4. 12 11
      server/services/aitable.js

+ 1 - 1
public/index.html

@@ -589,7 +589,7 @@
                 <div class="field">
                   <label>Base ID / 数据表名称</label>
                   <input type="text" id="settingsAitableBaseId" placeholder="粘贴 AI 表格分享链接或直接填 Base ID" autocomplete="off">
-                  <input type="text" id="settingsAitableSheetName" placeholder="经营数据" autocomplete="off" style="margin-top:6px;">
+                  <input type="text" id="settingsAitableSheetName" placeholder="数据表" autocomplete="off" style="margin-top:6px;">
                   <div class="help">Base ID 可直接粘贴分享链接(形如 alidocs.dingtalk.com/i/nodes/xxx),保存时自动提取 ID;网页版打开表格时地址栏里也能看到。</div>
                 </div>
                 <div class="settings-links">

+ 1 - 1
server/context.js

@@ -31,7 +31,7 @@ const CONFIG_DEFAULTS = {
   httpLog: false,
   // 钉钉 AI 表格(在线主档案/共享层):同步完成后自动推送,其他机器可拉取
   // operatorId = 操作人 unionId(notable 接口必填,scripts/aitable-discover.js 发现)
-  aitable: { enabled: false, appKey: '', appSecret: '', baseId: '', sheetName: '经营数据', operatorId: '' },
+  aitable: { enabled: false, appKey: '', appSecret: '', baseId: '', sheetName: '数据表', operatorId: '' },
   // 经营数据每日自动同步(商家客户端在线时自动补昨日数据),默认关闭
   autoSync: { enabled: false, time: '06:00' },
 };

+ 3 - 3
server/routes/settings.js

@@ -34,7 +34,7 @@ function register(app, ctx) {
       aitableEnabled: !!(ctx.config.aitable && ctx.config.aitable.enabled),
       aitableAppKey: (ctx.config.aitable && ctx.config.aitable.appKey) || '',
       aitableBaseId: (ctx.config.aitable && ctx.config.aitable.baseId) || '',
-      aitableSheetName: (ctx.config.aitable && ctx.config.aitable.sheetName) || '经营数据',
+      aitableSheetName: (ctx.config.aitable && ctx.config.aitable.sheetName) || '数据表',
       aitableSecretSet: !!(ctx.config.aitable && ctx.config.aitable.appSecret),
       aitableOperatorSet: !!(ctx.config.aitable && ctx.config.aitable.operatorId),
     });
@@ -89,7 +89,7 @@ function register(app, ctx) {
       else if (t) config.accessToken = t;
     }
     if (body.aitableEnabled !== undefined || body.aitableAppKey !== undefined || body.aitableBaseId !== undefined || body.aitableSheetName !== undefined || body.aitableAppSecret !== undefined || body.aitableOperatorId !== undefined) {
-      if (!config.aitable || typeof config.aitable !== 'object') config.aitable = { enabled: false, appKey: '', appSecret: '', baseId: '', sheetName: '经营数据', operatorId: '' };
+      if (!config.aitable || typeof config.aitable !== 'object') config.aitable = { enabled: false, appKey: '', appSecret: '', baseId: '', sheetName: '数据表', operatorId: '' };
       if (body.aitableEnabled !== undefined) config.aitable.enabled = !!body.aitableEnabled;
       if (body.aitableAppKey !== undefined) config.aitable.appKey = String(body.aitableAppKey).trim();
       if (body.aitableBaseId !== undefined) {
@@ -98,7 +98,7 @@ function register(app, ctx) {
         const m = /i\/nodes\/([A-Za-z0-9]+)/.exec(raw) || /[?&]baseId=([A-Za-z0-9]+)/.exec(raw);
         config.aitable.baseId = m ? m[1] : raw;
       }
-      if (body.aitableSheetName !== undefined) config.aitable.sheetName = String(body.aitableSheetName).trim() || '经营数据';
+      if (body.aitableSheetName !== undefined) config.aitable.sheetName = String(body.aitableSheetName).trim() || '数据表';
       if (body.aitableAppSecret !== undefined) {
         const sec = String(body.aitableAppSecret).trim();
         if (sec === '-') config.aitable.appSecret = ''; // 约定:输入 - 清除

+ 12 - 11
server/services/aitable.js

@@ -7,15 +7,16 @@
 //   · pullLake  — 把 AI 表格记录拉回本地湖(其他机器补数据用,云端为准)
 // 业务键:日期|账号名|维度|门店ID(与本地 rowKey 语义对齐,幂等可重跑)。
 //
-// 钉钉侧契约(企业内部应用 access_token,域 api.dingtalk.com):
-//   token:  POST /v1.0/oauth2/accessToken  {appKey, appSecret} → {accessToken, expireIn}
-//   记录:   /v1.0/notable/bases/{baseId}/sheets/{sheetName}/records
-//           POST 批量新增 / PUT 批量更新 / GET 分页列表
-//   ⚠ 所有 notable 接口必须在 query 上带 operatorId=操作人 unionId(企业内部应用
-//     无法从 token 推出用户身份;unionId 用 scripts/aitable-discover.js 发现一次后
-//     配到 config 的 aitable.operatorId,同一组织内固定不变)。
-// ⚠ 端点形态与批量上限在首次真机联调时核对——所有钉钉细节都收敛在本文件,
-//   如有偏差只需改这里的常量与两个 transport 方法。
+// 钉钉侧契约(企业内部应用 access_token,域 api.dingtalk.com;2026-10-17 真机核对):
+//   token:   POST /v1.0/oauth2/accessToken  {appKey, appSecret} → {accessToken, expireIn}
+//   列记录:  GET  …/records?operatorId&maxResults&nextToken → {records:[{id,fields}],nextToken,hasMore}
+//   新增:    POST …/records {records:[{fields}]} → {value:[{id}]}(响应键是 value)
+//   更新:    PUT  …/records {records:[{id,fields}]}(记录标识键是 id,不是 record_id)
+//   删除:    POST …/records/delete {recordIds:[…]}(同步语义只 upsert,用不到)
+//   · 所有 notable 接口 query 必带 operatorId=操作人 unionId(scripts/aitable-discover.js 发现)
+//   · 数值字段读回是字符串("12345.67"),recordToRow 已做 Number() 归一
+//   · number 列 formatter=INT 只影响显示,存储小数保真
+// ⚠ 批量上限(100)是保守值,首推大批量时如遇限流再调。
 
 const fs = require('fs');
 const path = require('path');
@@ -251,12 +252,12 @@ function createAitableService(ctx) {
     let created = 0;
     let updated = 0;
     const toCreate = [];
-    const toUpdate = []; // {recordId, fields}
+    const toUpdate = []; // {id, fields}(更新接口的记录标识键是 id,不是 record_id)
     for (const row of rows) {
       const rec = rowToRecord(row, accountName);
       const k = recordKey(rec.fields);
       const rid = existing.get(k);
-      if (rid) toUpdate.push({ record_id: rid, fields: rec.fields });
+      if (rid) toUpdate.push({ id: rid, fields: rec.fields });
       else toCreate.push(rec);
     }
     for (const part of chunk(toCreate, INSERT_CHUNK)) {