Browse Source

🎈 perf: 优化乐跑成功邮件

Pchen. 1 month ago
parent
commit
3c4580257e
2 changed files with 153 additions and 27 deletions
  1. 124 17
      lib/Lepao/Worker.js
  2. 29 10
      plugin/Email/emailTemplate.js

+ 124 - 17
lib/Lepao/Worker.js

@@ -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' }
         })
 

+ 29 - 10
plugin/Email/emailTemplate.js

@@ -377,6 +377,29 @@ class emailTemplate {
     }
 
     async lepaoSuccess(email, data) {
+        const target_count = Number(data.target_count) || 0
+        const total_num = Number(data.total_num) || 0
+        const timeSec = Number(data.time) || 0
+        const distanceKm = Number(data.distance) || 0
+        const passTit = data.pass_tit != null && data.pass_tit !== '' ? data.pass_tit : '—'
+        const paceStr =
+            distanceKm > 0 && timeSec > 0
+                ? this.calculatePace(timeSec, distanceKm)
+                : '—'
+        const timeStr = timeSec > 0 ? this.formatSecondsToMinSec(timeSec) : '—'
+
+        let goalHtml = ''
+        if (target_count === 0) {
+            goalHtml = `
+                <p><strong>累计次数:</strong> ${total_num} 次 ✨</p>`
+        } else {
+            const remain = Math.max(0, target_count - total_num)
+            const hitGoal = total_num >= target_count
+            goalHtml = `
+                <p><strong>目标次数:</strong> ${target_count} 次 🎯</p>
+                <p><strong>累计次数:</strong> ${total_num} 次 ✨</p>`
+        }
+
         await sendEmail(email, '乐跑成功提醒',
             `<html lang="zh-CN">
             <head>
@@ -424,6 +447,7 @@ class emailTemplate {
 
                     .info p {
                         margin: 5px 0;
+                        text-indent: 0;
                     }
 
                     .important {
@@ -451,16 +475,11 @@ class emailTemplate {
 
                 <div class="info">
                 <p><strong>学号:</strong> ${data.account}</p>
-                <p><strong>跑区:</strong> ${data.pass_tit} 🌈</p>
-                <p><strong>跑步时间:</strong> ${this.formatSecondsToMinSec(data.time)} ⏱️</p>
-                <p><strong>平均配速:</strong> ${this.calculatePace(data.time, data.distance)} 🐇</p>
-                <p><strong>跑步距离:</strong> ${data.distance} Km 💕</p>
-                <p><strong>累计次数:</strong> ${data.total_num} 次 ✨</p>
-                <p><strong>剩余次数:</strong>  ${data.term_num === 0 
-                                                ? '∞' 
-                                                : (data.term_num - data.total_num >= 0 
-                                                    ? (data.term_num - data.total_num) 
-                                                    : '已完成')} 次 🎯</p>
+                <p><strong>跑区:</strong> ${passTit} 🌈</p>
+                <p><strong>跑步时间:</strong> ${timeStr} ⏱️</p>
+                <p><strong>平均配速:</strong> ${paceStr} 🐇</p>
+                <p><strong>跑步距离:</strong> ${distanceKm || '—'} Km 💕</p>
+                ${goalHtml}
                 </div>
 
                 <p class="important">如果宝宝开启了自动乐跑,要记得不要在其他设备上登录“智慧体育”小程序哦 🚫📱,不然登录就会失效,要重新来一次啦~</p>