|
|
@@ -57,6 +57,83 @@ class Worker {
|
|
|
|
|
|
/* ================= 工具 ================= */
|
|
|
|
|
|
+ formatYmdLocal(d = new Date()) {
|
|
|
+ const yyyy = d.getFullYear()
|
|
|
+ const mm = String(d.getMonth() + 1).padStart(2, '0')
|
|
|
+ const dd = String(d.getDate()).padStart(2, '0')
|
|
|
+ return `${yyyy}-${mm}-${dd}`
|
|
|
+ }
|
|
|
+
|
|
|
+ simpleHash(str) {
|
|
|
+ const s = String(str || '')
|
|
|
+ let h = 0
|
|
|
+ for (let i = 0; i < s.length; i++) {
|
|
|
+ h = ((h << 5) - h + s.charCodeAt(i)) | 0
|
|
|
+ }
|
|
|
+ return (h >>> 0).toString(16)
|
|
|
+ }
|
|
|
+
|
|
|
+ isCommonLepaoErrorMessage(msg) {
|
|
|
+ const m = String(msg || '')
|
|
|
+ return (
|
|
|
+ m.includes('当天已乐跑成功') ||
|
|
|
+ m.includes('当前不在有效乐跑时间范围内') ||
|
|
|
+ m.includes('乐跑账号登录已过期')
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ async notifyAdminsUncommonLepaoError(payload) {
|
|
|
+ try {
|
|
|
+ const { account, traceId, taskType, reason, code, name } = payload || {}
|
|
|
+ if (!account || !reason) return
|
|
|
+ if (this.isCommonLepaoErrorMessage(reason)) return
|
|
|
+
|
|
|
+ const day = this.formatYmdLocal()
|
|
|
+ const msgHash = this.simpleHash(reason)
|
|
|
+ const rateKey = `lepao:adminWarn:${account}:${day}:${msgHash}`
|
|
|
+ const existed = await Redis.get(rateKey)
|
|
|
+ if (existed) return
|
|
|
+ await Redis.set(rateKey, '1', { EX: 86400 })
|
|
|
+
|
|
|
+ const adminSql = `
|
|
|
+ SELECT email
|
|
|
+ FROM users
|
|
|
+ WHERE email IS NOT NULL
|
|
|
+ AND email <> ''
|
|
|
+ AND (JSON_CONTAINS(permission, '"admin"') OR JSON_CONTAINS(permission, '"service"'))
|
|
|
+ `
|
|
|
+ const adminRows = await db.query(adminSql)
|
|
|
+ if (!adminRows || adminRows.length === 0) {
|
|
|
+ this.logger.warn(`[lepaoAdminWarn][${traceId}] 未找到可通知的管理员邮箱`)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const emails = [...new Set(adminRows.map(r => r.email).filter(Boolean))]
|
|
|
+ for (const email of emails) {
|
|
|
+ await EmailTemplate.lepaoAdminWarning(email, {
|
|
|
+ server: (() => {
|
|
|
+ try {
|
|
|
+ // config.json 可能在其他模块缓存读取,这里动态 require 一次避免循环依赖
|
|
|
+ // eslint-disable-next-line global-require
|
|
|
+ const cfg = require('../../config.json')
|
|
|
+ return cfg?.server || ''
|
|
|
+ } catch (_) {
|
|
|
+ return ''
|
|
|
+ }
|
|
|
+ })(),
|
|
|
+ account,
|
|
|
+ name: name || '',
|
|
|
+ taskType: taskType || 'lepao.startRun',
|
|
|
+ traceId: traceId || '',
|
|
|
+ reason,
|
|
|
+ code: code || ''
|
|
|
+ })
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ this.logger.error(`[lepaoAdminWarn] 告警邮件发送失败:${e.message || e}`)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
api(path) {
|
|
|
return this.baseUrl + path
|
|
|
}
|
|
|
@@ -657,6 +734,20 @@ class Worker {
|
|
|
} catch (err) {
|
|
|
this.logger.error(`[${traceId}] 乐跑流程失败:`, err)
|
|
|
|
|
|
+ // 非阻塞:非常见错误时通知管理员(过滤:当天已跑/不在时间/登录过期)
|
|
|
+ Promise.resolve().then(async () => {
|
|
|
+ try {
|
|
|
+ await this.notifyAdminsUncommonLepaoError({
|
|
|
+ account: req?.account,
|
|
|
+ name: userData?.name,
|
|
|
+ traceId,
|
|
|
+ taskType: 'lepao.startRun',
|
|
|
+ reason: err?.message || '未知错误',
|
|
|
+ code: err?.code
|
|
|
+ })
|
|
|
+ } catch (_) { }
|
|
|
+ })
|
|
|
+
|
|
|
// 若已扣减次数,则失败时返还(幂等)
|
|
|
try {
|
|
|
await this.handlers['lepao.refundCount']({
|