aaden-cli 1.6.116 → 1.6.118

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -121,6 +121,8 @@ const COMMANDS = {
121
121
  /** 每条命令最多跑多久;latest.zip 全量在慢网络上要 20 分钟 */
122
122
  const COMMAND_TIMEOUT_MS = 45 * 60 * 1000
123
123
  const REPORT_INTERVAL_MS = 10 * 1000
124
+ /** 没有新输出也隔这么久回报一次:云端 30 分钟没回报就判失败,而下载 latest.zip 之类一声不吭能跑 20 分钟 */
125
+ const HEARTBEAT_MS = 60 * 1000
124
126
  const MAX_OUTPUT = 60 * 1024
125
127
 
126
128
  /** 每次 auto-upgrade 开始时写;cron(root)和补跑(aaden)都要能写,所以 chmod 666 */
@@ -277,9 +279,11 @@ function runOne(id, deviceId, shellCmd) {
277
279
  })
278
280
  child.stdout.on('data', append)
279
281
  child.stderr.on('data', append)
282
+ let lastSent = Date.now()
280
283
  const timer = setInterval(() => {
281
- if (!dirty) return
284
+ if (!dirty && Date.now() - lastSent < HEARTBEAT_MS) return
282
285
  dirty = false
286
+ lastSent = Date.now()
283
287
  report(id, deviceId, output)
284
288
  }, REPORT_INTERVAL_MS)
285
289
  const killer = setTimeout(() => {
@@ -690,6 +694,15 @@ const WINDOWS_TSE_DIR = 'Desktop/AadenTseOnWindows'
690
694
  const WINDOWS_DEFAULT_JAR = 'tse-0.0.3.jar'
691
695
  // 和 PHP 的 Api.php 一样给 3 秒
692
696
  const PROBE_TIMEOUT_MS = 3000
697
+ const PING_RETRY_DELAY_MS = 20000
698
+
699
+ function safeSync(fn, fallback = null) {
700
+ try {
701
+ return fn()
702
+ } catch (e) {
703
+ return fallback
704
+ }
705
+ }
693
706
 
694
707
  async function safe(fn, fallback = null) {
695
708
  try {
@@ -772,20 +785,67 @@ function servingJarPath() {
772
785
  return match ? match[1] : LINUX_DEFAULT_JAR
773
786
  }
774
787
 
775
- // 门店库里最新的那块 TSE(换过 TSE 会有多行);只读数据库,TSE 坏了也读得到
776
- function readCertFromDb() {
788
+ // 只读门店库,返回第一行按 tab 分开的各列(没有结果为 null)
789
+ function queryDb(sql) {
777
790
  const db = readIniSection('DB')
778
791
  const out = execFileSync('mysql', [
779
792
  '-h', db.host || 'localhost', '-u', db.user || 'aaden', '-N', '-B', '--raw', db.name || 'aaden',
780
- '-e', 'select serialNumber, certificate from RealTseInfo order by id desc limit 1',
793
+ '-e', sql,
781
794
  ], {
782
795
  encoding: 'utf-8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'],
783
796
  // 密码走环境变量,不出现在进程列表里
784
797
  env: Object.assign({}, process.env, {MYSQL_PWD: db.password || ''}),
785
798
  }).trim()
786
799
  if (!out) return null
787
- const [serial, certificate] = out.split('\t')
788
- return {serial, certificate: certificate && certificate !== 'NULL' ? certificate : null}
800
+ return out.split('\n')[0].split('\t').map(v => (v === 'NULL' ? null : v))
801
+ }
802
+
803
+ // 门店库里最新的那块 TSE(换过 TSE 会有多行);只读数据库,TSE 坏了也读得到
804
+ function readCertFromDb() {
805
+ const row = queryDb('select serialNumber, certificate from RealTseInfo order by id desc limit 1')
806
+ if (!row) return null
807
+ const [serial, certificate] = row
808
+ return {serial, certificate: certificate || null}
809
+ }
810
+
811
+ // 结账后签名任务进 TseExecuteQueue(finish / finish-zero),后台几秒内执行;
812
+ // 结账超过 5 分钟还没有签名的就算漏签(任务报错、卡在队列里、或者 TSE 回了错)。
813
+ // 同一张单可能有多条任务(补签),按单算
814
+ const DUE_ORDERS = `select q.orderId, max(q.createdAt) as createdAt, max(q.id) as taskId,
815
+ exists (select 1 from RealTseLog l
816
+ where l.orderId = q.orderId and l.bestellungId is null and l.signature <> '') as signed
817
+ from TseExecuteQueue q
818
+ where q.executeType in ('finish', 'finish-zero')
819
+ and q.createdAt >= now() - interval 24 hour
820
+ and q.createdAt <= now() - interval 5 minute
821
+ group by q.orderId`
822
+
823
+ // 最近 24 小时结账的单有没有真的签上:这是 TSE 有没有坏的实际结果,比 ping 准——
824
+ // 关店、睡眠、开机自检时没人结账,不会算成故障;真坏了也一定有单签不上
825
+ function readSigningFromDb() {
826
+ const counts = queryDb(`select count(*), coalesce(sum(not signed), 0),
827
+ unix_timestamp(max(if(signed, createdAt, null))),
828
+ unix_timestamp(max(if(signed, null, createdAt)))
829
+ from (${DUE_ORDERS}) t`)
830
+ if (!counts) return null
831
+ const [due, unsigned, lastSigned, lastUnsigned] = counts
832
+ // 最近一张漏签的单为什么没签上:TSE 回的错(RealTseLog.tseError),没有就看队列任务的报错 / 还在排队
833
+ // 单独兜底:原因查不出来(比如两张表排序规则不一样)也不能连单数一起丢
834
+ const reason = Number(unsigned) > 0 ? safeSync(() => queryDb(`select coalesce(
835
+ (select l.tseError from RealTseLog l where l.orderId = t.orderId and l.tseError <> '' order by l.id desc limit 1),
836
+ nullif(q.note, ''),
837
+ if(q.finished = 0, '签名任务还在排队', null))
838
+ from (${DUE_ORDERS}) t join TseExecuteQueue q on q.id = t.taskId
839
+ where not t.signed
840
+ order by t.createdAt desc limit 1`)) : null
841
+ const toIso = sec => (sec ? new Date(Number(sec) * 1000).toISOString() : null)
842
+ return {
843
+ tseOrders24h: Number(due) || 0,
844
+ tseUnsigned24h: Number(unsigned) || 0,
845
+ tseLastSignedAt: toIso(lastSigned),
846
+ tseLastUnsignedAt: toIso(lastUnsigned),
847
+ tseLastUnsignedReason: reason && reason[0] ? reason[0].slice(0, 250) : null,
848
+ }
789
849
  }
790
850
 
791
851
  // TSE 自己的序列号、证书有效期和型号(dnQualifier:BSI-DSZ-CC-1121 是第一代,-V2 是第二代)
@@ -821,7 +881,12 @@ async function probeTse() {
821
881
  }
822
882
 
823
883
  // ok:TSE 正常;error:服务在但 TSE 在自检 / 没初始化 / 硬件报错;down:服务连不上
824
- const ping = await httpGet(`${tseHost}/ping`)
884
+ // 开机 / 唤醒时 TseHost 先自检约 15 秒,这期间 /ping 回 400:失败就等 20 秒再试一次,免得报成故障
885
+ let ping = await httpGet(`${tseHost}/ping`)
886
+ if (ping.status !== 200) {
887
+ await new Promise(resolve => setTimeout(resolve, PING_RETRY_DELAY_MS))
888
+ ping = await httpGet(`${tseHost}/ping`)
889
+ }
825
890
  const tseStatus = ping.status === 0 ? 'down' : (ping.status === 200 ? 'ok' : 'error')
826
891
 
827
892
  // 连不上时报 null,云端保留上次的版本;服务在但没有这个接口(2024-09 之前的构建)报空串
@@ -839,8 +904,10 @@ async function probeTse() {
839
904
  }
840
905
 
841
906
  const cert = await collectCert(tseHost, ping.status !== 0)
907
+ // 老版本门店库没有 TseExecuteQueue(2025-03 之前)就不报,云端保留上次的值
908
+ const signing = (await safe(readSigningFromDb)) || {}
842
909
 
843
- return {tseEnabled, tseHost, tseStatus, tseAdapterVersion, tseJarSize, ...cert}
910
+ return {tseEnabled, tseHost, tseStatus, tseAdapterVersion, tseJarSize, ...cert, ...signing}
844
911
  }
845
912
 
846
913
  // 整体出错就什么都不报,云端保留上次的值
@@ -19179,7 +19246,7 @@ module.exports = JSON.parse('{"name":"axios","version":"0.19.2","description":"P
19179
19246
  /***/ ((module) => {
19180
19247
 
19181
19248
  "use strict";
19182
- module.exports = {"i8":"1.6.116"};
19249
+ module.exports = {"i8":"1.6.118"};
19183
19250
 
19184
19251
  /***/ })
19185
19252
 
@@ -34,6 +34,8 @@ const COMMANDS = {
34
34
  /** 每条命令最多跑多久;latest.zip 全量在慢网络上要 20 分钟 */
35
35
  const COMMAND_TIMEOUT_MS = 45 * 60 * 1000
36
36
  const REPORT_INTERVAL_MS = 10 * 1000
37
+ /** 没有新输出也隔这么久回报一次:云端 30 分钟没回报就判失败,而下载 latest.zip 之类一声不吭能跑 20 分钟 */
38
+ const HEARTBEAT_MS = 60 * 1000
37
39
  const MAX_OUTPUT = 60 * 1024
38
40
 
39
41
  /** 每次 auto-upgrade 开始时写;cron(root)和补跑(aaden)都要能写,所以 chmod 666 */
@@ -190,9 +192,11 @@ function runOne(id, deviceId, shellCmd) {
190
192
  })
191
193
  child.stdout.on('data', append)
192
194
  child.stderr.on('data', append)
195
+ let lastSent = Date.now()
193
196
  const timer = setInterval(() => {
194
- if (!dirty) return
197
+ if (!dirty && Date.now() - lastSent < HEARTBEAT_MS) return
195
198
  dirty = false
199
+ lastSent = Date.now()
196
200
  report(id, deviceId, output)
197
201
  }, REPORT_INTERVAL_MS)
198
202
  const killer = setTimeout(() => {
@@ -21,6 +21,15 @@ const WINDOWS_TSE_DIR = 'Desktop/AadenTseOnWindows'
21
21
  const WINDOWS_DEFAULT_JAR = 'tse-0.0.3.jar'
22
22
  // 和 PHP 的 Api.php 一样给 3 秒
23
23
  const PROBE_TIMEOUT_MS = 3000
24
+ const PING_RETRY_DELAY_MS = 20000
25
+
26
+ function safeSync(fn, fallback = null) {
27
+ try {
28
+ return fn()
29
+ } catch (e) {
30
+ return fallback
31
+ }
32
+ }
24
33
 
25
34
  async function safe(fn, fallback = null) {
26
35
  try {
@@ -103,20 +112,67 @@ function servingJarPath() {
103
112
  return match ? match[1] : LINUX_DEFAULT_JAR
104
113
  }
105
114
 
106
- // 门店库里最新的那块 TSE(换过 TSE 会有多行);只读数据库,TSE 坏了也读得到
107
- function readCertFromDb() {
115
+ // 只读门店库,返回第一行按 tab 分开的各列(没有结果为 null)
116
+ function queryDb(sql) {
108
117
  const db = readIniSection('DB')
109
118
  const out = execFileSync('mysql', [
110
119
  '-h', db.host || 'localhost', '-u', db.user || 'aaden', '-N', '-B', '--raw', db.name || 'aaden',
111
- '-e', 'select serialNumber, certificate from RealTseInfo order by id desc limit 1',
120
+ '-e', sql,
112
121
  ], {
113
122
  encoding: 'utf-8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'],
114
123
  // 密码走环境变量,不出现在进程列表里
115
124
  env: Object.assign({}, process.env, {MYSQL_PWD: db.password || ''}),
116
125
  }).trim()
117
126
  if (!out) return null
118
- const [serial, certificate] = out.split('\t')
119
- return {serial, certificate: certificate && certificate !== 'NULL' ? certificate : null}
127
+ return out.split('\n')[0].split('\t').map(v => (v === 'NULL' ? null : v))
128
+ }
129
+
130
+ // 门店库里最新的那块 TSE(换过 TSE 会有多行);只读数据库,TSE 坏了也读得到
131
+ function readCertFromDb() {
132
+ const row = queryDb('select serialNumber, certificate from RealTseInfo order by id desc limit 1')
133
+ if (!row) return null
134
+ const [serial, certificate] = row
135
+ return {serial, certificate: certificate || null}
136
+ }
137
+
138
+ // 结账后签名任务进 TseExecuteQueue(finish / finish-zero),后台几秒内执行;
139
+ // 结账超过 5 分钟还没有签名的就算漏签(任务报错、卡在队列里、或者 TSE 回了错)。
140
+ // 同一张单可能有多条任务(补签),按单算
141
+ const DUE_ORDERS = `select q.orderId, max(q.createdAt) as createdAt, max(q.id) as taskId,
142
+ exists (select 1 from RealTseLog l
143
+ where l.orderId = q.orderId and l.bestellungId is null and l.signature <> '') as signed
144
+ from TseExecuteQueue q
145
+ where q.executeType in ('finish', 'finish-zero')
146
+ and q.createdAt >= now() - interval 24 hour
147
+ and q.createdAt <= now() - interval 5 minute
148
+ group by q.orderId`
149
+
150
+ // 最近 24 小时结账的单有没有真的签上:这是 TSE 有没有坏的实际结果,比 ping 准——
151
+ // 关店、睡眠、开机自检时没人结账,不会算成故障;真坏了也一定有单签不上
152
+ function readSigningFromDb() {
153
+ const counts = queryDb(`select count(*), coalesce(sum(not signed), 0),
154
+ unix_timestamp(max(if(signed, createdAt, null))),
155
+ unix_timestamp(max(if(signed, null, createdAt)))
156
+ from (${DUE_ORDERS}) t`)
157
+ if (!counts) return null
158
+ const [due, unsigned, lastSigned, lastUnsigned] = counts
159
+ // 最近一张漏签的单为什么没签上:TSE 回的错(RealTseLog.tseError),没有就看队列任务的报错 / 还在排队
160
+ // 单独兜底:原因查不出来(比如两张表排序规则不一样)也不能连单数一起丢
161
+ const reason = Number(unsigned) > 0 ? safeSync(() => queryDb(`select coalesce(
162
+ (select l.tseError from RealTseLog l where l.orderId = t.orderId and l.tseError <> '' order by l.id desc limit 1),
163
+ nullif(q.note, ''),
164
+ if(q.finished = 0, '签名任务还在排队', null))
165
+ from (${DUE_ORDERS}) t join TseExecuteQueue q on q.id = t.taskId
166
+ where not t.signed
167
+ order by t.createdAt desc limit 1`)) : null
168
+ const toIso = sec => (sec ? new Date(Number(sec) * 1000).toISOString() : null)
169
+ return {
170
+ tseOrders24h: Number(due) || 0,
171
+ tseUnsigned24h: Number(unsigned) || 0,
172
+ tseLastSignedAt: toIso(lastSigned),
173
+ tseLastUnsignedAt: toIso(lastUnsigned),
174
+ tseLastUnsignedReason: reason && reason[0] ? reason[0].slice(0, 250) : null,
175
+ }
120
176
  }
121
177
 
122
178
  // TSE 自己的序列号、证书有效期和型号(dnQualifier:BSI-DSZ-CC-1121 是第一代,-V2 是第二代)
@@ -152,7 +208,12 @@ async function probeTse() {
152
208
  }
153
209
 
154
210
  // ok:TSE 正常;error:服务在但 TSE 在自检 / 没初始化 / 硬件报错;down:服务连不上
155
- const ping = await httpGet(`${tseHost}/ping`)
211
+ // 开机 / 唤醒时 TseHost 先自检约 15 秒,这期间 /ping 回 400:失败就等 20 秒再试一次,免得报成故障
212
+ let ping = await httpGet(`${tseHost}/ping`)
213
+ if (ping.status !== 200) {
214
+ await new Promise(resolve => setTimeout(resolve, PING_RETRY_DELAY_MS))
215
+ ping = await httpGet(`${tseHost}/ping`)
216
+ }
156
217
  const tseStatus = ping.status === 0 ? 'down' : (ping.status === 200 ? 'ok' : 'error')
157
218
 
158
219
  // 连不上时报 null,云端保留上次的版本;服务在但没有这个接口(2024-09 之前的构建)报空串
@@ -170,8 +231,10 @@ async function probeTse() {
170
231
  }
171
232
 
172
233
  const cert = await collectCert(tseHost, ping.status !== 0)
234
+ // 老版本门店库没有 TseExecuteQueue(2025-03 之前)就不报,云端保留上次的值
235
+ const signing = (await safe(readSigningFromDb)) || {}
173
236
 
174
- return {tseEnabled, tseHost, tseStatus, tseAdapterVersion, tseJarSize, ...cert}
237
+ return {tseEnabled, tseHost, tseStatus, tseAdapterVersion, tseJarSize, ...cert, ...signing}
175
238
  }
176
239
 
177
240
  // 整体出错就什么都不报,云端保留上次的值
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aaden-cli",
3
- "version": "1.6.116",
3
+ "version": "1.6.118",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {