|
|
@@ -158,12 +158,14 @@ class Worker {
|
|
|
req?.school_id == null ||
|
|
|
!sid
|
|
|
) {
|
|
|
- return
|
|
|
+ return { ok: false, reason: '缺少同步乐跑次数所需凭证' }
|
|
|
}
|
|
|
|
|
|
const recordData = await this.handlers['lepao.getRecord'](req, ctx)
|
|
|
const data = recordData?.data
|
|
|
- if (!data) return
|
|
|
+ if (!data) {
|
|
|
+ return { ok: false, reason: 'getRecord 无有效 data' }
|
|
|
+ }
|
|
|
|
|
|
const term_num = Number(data.term_num ?? 30)
|
|
|
const total_num = Number(data.total_num ?? 0)
|
|
|
@@ -172,11 +174,13 @@ class Worker {
|
|
|
const rows = await db.query(sql, [term_num, total_num, req.account])
|
|
|
if (!rows || rows.affectedRows !== 1) {
|
|
|
this.logger.warn(`${req.account}更新乐跑次数失败`)
|
|
|
- return
|
|
|
+ return { ok: false, reason: '数据库更新 lepao_account 失败', term_num, total_num }
|
|
|
}
|
|
|
this.logger.info(`${req.account}更新乐跑次数成功 term_num=${term_num}, total_num=${total_num}`)
|
|
|
+ return { ok: true, term_num, total_num }
|
|
|
} catch (error) {
|
|
|
this.logger.warn(`${req?.account || 'unknown'}同步乐跑次数失败: ${error.message || error}`)
|
|
|
+ return { ok: false, reason: error.message || String(error) }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -344,6 +348,70 @@ class Worker {
|
|
|
}, name)
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 累计完成次数 >= 跑友目标(且目标>0)时:关闭 auto_run,并发送乐跑目标完成邮件 / Bot 通知
|
|
|
+ */
|
|
|
+ async handleLepaoTargetComplete(account, user, totalNum, traceId) {
|
|
|
+ const target = Number(user?.target_count) || 0
|
|
|
+ const total = Number(totalNum) || 0
|
|
|
+ if (target <= 0 || total < target) return
|
|
|
+
|
|
|
+ try {
|
|
|
+ const up = await db.query(
|
|
|
+ 'UPDATE lepao_account SET auto_run = 0 WHERE student_num = ? AND auto_run = 1',
|
|
|
+ [account]
|
|
|
+ )
|
|
|
+ if (up?.affectedRows !== 1) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ this.logger.info(
|
|
|
+ `${account} 已达目标次数(${total}/${target}),关闭自动乐跑`
|
|
|
+ )
|
|
|
+ } catch (e) {
|
|
|
+ this.logger.error(`关闭自动乐跑失败 ${account}: ${e.message || e}`)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const noticeType = user.notice_type || 'none'
|
|
|
+ const overPayload = {
|
|
|
+ type: 'lepao_over',
|
|
|
+ umo: user.bot_umo,
|
|
|
+ name: user.name,
|
|
|
+ account,
|
|
|
+ total_num: total,
|
|
|
+ target_count: target,
|
|
|
+ traceId
|
|
|
+ }
|
|
|
+
|
|
|
+ if (noticeType === 'email' && user.email) {
|
|
|
+ try {
|
|
|
+ await EmailTemplate.lepaoOver(user.email, {
|
|
|
+ name: user.name,
|
|
|
+ account
|
|
|
+ })
|
|
|
+ } catch (e) {
|
|
|
+ this.logger.error(`lepaoOver 邮件发送失败: ${e.message || e}`)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (noticeType === 'bot' && user.bot_umo) {
|
|
|
+ try {
|
|
|
+ const ch = await mq.getChannel(this.noticeQueue)
|
|
|
+ await ch.assertQueue(this.noticeQueue, { durable: true })
|
|
|
+ ch.sendToQueue(
|
|
|
+ this.noticeQueue,
|
|
|
+ Buffer.from(JSON.stringify(overPayload)),
|
|
|
+ {
|
|
|
+ persistent: true,
|
|
|
+ contentType: 'application/json'
|
|
|
+ }
|
|
|
+ )
|
|
|
+ } catch (e) {
|
|
|
+ this.logger.error(`lepao_over Bot 通知失败: ${e.message || e}`)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
register(type, handler) {
|
|
|
this.handlers[type] = handler
|
|
|
this.logger.info(`注册任务: ${type}`)
|
|
|
@@ -456,12 +524,11 @@ class Worker {
|
|
|
point_data: pointData
|
|
|
}, ctx)
|
|
|
|
|
|
- // 绑定接口有返回即入库(无论成功或失败)
|
|
|
+ // 绑定接口有返回即入库
|
|
|
if (bindRes && bindRes.data) {
|
|
|
await this.addLepaoRecord(userData?.create_user, req.account, bindRes.data, pathId, pointData)
|
|
|
}
|
|
|
|
|
|
- // 使用旧版 Lepao.js 的规则判断“是否真正乐跑成功”
|
|
|
const runResult = this.isRunSuccess(bindRes)
|
|
|
if (runResult.ok || runResult.reason === '当天关联成绩次数已达到上限') {
|
|
|
await this.writeSuccessRedis(req.account)
|
|
|
@@ -470,14 +537,28 @@ class Worker {
|
|
|
throw new Error(runResult.reason)
|
|
|
}
|
|
|
|
|
|
- // 6️⃣ 发送通知
|
|
|
+ // 同步乐跑次数(通知里要带 total_num / term_num,与 getRecord 一致)
|
|
|
+ const syncResult = await this.syncRunCount(req, ctx)
|
|
|
+ if (!syncResult?.ok) {
|
|
|
+ this.logger.error(
|
|
|
+ `[${traceId}] 同步乐跑次数失败:${syncResult?.reason || 'unknown'}`
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ // 6️⃣ 发送通知(把同步后的学期次数、累计完成次数传给 Bot / 邮件)
|
|
|
if (ctx.channel) {
|
|
|
- await this.enqueueTask(ctx.channel, 'lepao.sendNotice', {
|
|
|
- account: req.account,
|
|
|
- success: true,
|
|
|
- data: runResult.payload,
|
|
|
- traceId
|
|
|
- }, { id: `${traceId}:notice:success` })
|
|
|
+ await this.enqueueTask(
|
|
|
+ ctx.channel,
|
|
|
+ 'lepao.sendNotice',
|
|
|
+ {
|
|
|
+ account: req.account,
|
|
|
+ success: true,
|
|
|
+ data: runResult.payload,
|
|
|
+ traceId,
|
|
|
+ total_num: syncResult?.ok ? syncResult.total_num : undefined
|
|
|
+ },
|
|
|
+ { id: `${traceId}:notice:success` }
|
|
|
+ )
|
|
|
}
|
|
|
|
|
|
return { traceId, ossPath, pointData, bindRes }
|
|
|
@@ -519,15 +600,13 @@ class Worker {
|
|
|
}
|
|
|
throw err
|
|
|
} finally {
|
|
|
- // 不论成功/失败,流程结束后同步一次乐跑次数
|
|
|
- await this.syncRunCount(req, ctx)
|
|
|
await Redis.del(`lepaoProgress:${req.account}`)
|
|
|
}
|
|
|
})
|
|
|
|
|
|
/* ---------------- 发送通知(独立 MQ 任务) ---------------- */
|
|
|
this.register('lepao.sendNotice', async (req, ctx) => {
|
|
|
- const { account, success, data, reason, traceId } = req || {}
|
|
|
+ const { account, success, data, reason, traceId, total_num: totalNumArg } = req || {}
|
|
|
|
|
|
if (!account) {
|
|
|
throw new Error('发送通知失败:缺少 account')
|
|
|
@@ -557,12 +636,31 @@ class Worker {
|
|
|
const user = rows[0]
|
|
|
const noticeType = user.notice_type || 'none'
|
|
|
|
|
|
+ let totalForNotice = totalNumArg
|
|
|
+ if (
|
|
|
+ success &&
|
|
|
+ (totalForNotice === undefined || totalForNotice === null)
|
|
|
+ ) {
|
|
|
+ const accRows = await db.query(
|
|
|
+ 'SELECT total_num FROM lepao_account WHERE student_num = ?',
|
|
|
+ [account]
|
|
|
+ )
|
|
|
+ if (accRows && accRows[0]) {
|
|
|
+ totalForNotice = accRows[0].total_num
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (totalForNotice === undefined || totalForNotice === null) {
|
|
|
+ totalForNotice = 0
|
|
|
+ }
|
|
|
+ totalForNotice = Number(totalForNotice)
|
|
|
+ const targetCount = Number(user.target_count) || 0
|
|
|
+
|
|
|
const payload = success ? {
|
|
|
...(data && typeof data === 'object' ? data : {}),
|
|
|
type: 'lepao_success',
|
|
|
umo: user.bot_umo,
|
|
|
- // 沿用原 Lepao.js 字段:term_num 实际传的是 target_count
|
|
|
- term_num: user.target_count ?? 0,
|
|
|
+ total_num: totalForNotice,
|
|
|
+ target_count: targetCount,
|
|
|
name: user.name,
|
|
|
account,
|
|
|
traceId
|
|
|
@@ -575,6 +673,12 @@ class Worker {
|
|
|
traceId
|
|
|
}
|
|
|
|
|
|
+ const afterSuccessNotify = async () => {
|
|
|
+ if (success) {
|
|
|
+ await this.handleLepaoTargetComplete(account, user, totalForNotice, traceId)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
if (noticeType === 'bot' && user.bot_umo) {
|
|
|
const ch = await mq.getChannel(this.noticeQueue)
|
|
|
await ch.assertQueue(this.noticeQueue, { durable: true })
|
|
|
@@ -586,12 +690,14 @@ class Worker {
|
|
|
contentType: 'application/json'
|
|
|
}
|
|
|
)
|
|
|
+ await afterSuccessNotify()
|
|
|
return { delivered: true, via: 'bot' }
|
|
|
}
|
|
|
|
|
|
if (noticeType === 'email' && user.email) {
|
|
|
if (success) {
|
|
|
await EmailTemplate.lepaoSuccess(user.email, payload)
|
|
|
+ await afterSuccessNotify()
|
|
|
return { delivered: true, via: 'email' }
|
|
|
}
|
|
|
|
|
|
@@ -604,6 +710,7 @@ class Worker {
|
|
|
return { delivered: true, via: 'email' }
|
|
|
}
|
|
|
|
|
|
+ await afterSuccessNotify()
|
|
|
return { delivered: false, via: 'none' }
|
|
|
})
|
|
|
|