Browse Source

✨ feat: 赠送次数新增邮件提醒

Pchen0 3 weeks ago
parent
commit
065531bf75

+ 34 - 1
apis/Goods/Admin/ApproveSendCountRequest.js

@@ -2,6 +2,7 @@ const API = require("../../../lib/API")
 const db = require("../../../plugin/DataBase/db")
 const db = require("../../../plugin/DataBase/db")
 const AccessControl = require("../../../lib/AccessControl")
 const AccessControl = require("../../../lib/AccessControl")
 const { BaseStdResponse } = require("../../../BaseStdResponse")
 const { BaseStdResponse } = require("../../../BaseStdResponse")
+const EmailTemplate = require("../../../plugin/Email/emailTemplate")
 
 
 class ApproveSendCountRequest extends API {
 class ApproveSendCountRequest extends API {
     constructor() {
     constructor() {
@@ -32,7 +33,7 @@ class ApproveSendCountRequest extends API {
             await conn.beginTransaction()
             await conn.beginTransaction()
 
 
             const [requestRows] = await conn.execute(
             const [requestRows] = await conn.execute(
-                `SELECT id, receiver_user_id, count, status
+                `SELECT id, sender_uuid, receiver_user_id, count, status
                  FROM lepao_send_count_request
                  FROM lepao_send_count_request
                  WHERE id = ?
                  WHERE id = ?
                  FOR UPDATE`,
                  FOR UPDATE`,
@@ -71,6 +72,38 @@ class ApproveSendCountRequest extends API {
             }
             }
 
 
             await conn.commit()
             await conn.commit()
+            const requestId = request.id
+            const reviewTime = new Date().getTime()
+
+            // 非阻塞通知接收人,不影响审核结果
+            Promise.resolve().then(async () => {
+                try {
+                    const infoSql = `
+                        SELECT
+                            ru.email AS receiver_email,
+                            ru.username AS receiver_username,
+                            su.username AS sender_username
+                        FROM users ru
+                        LEFT JOIN users su ON su.uuid = ?
+                        WHERE ru.id = ?
+                    `
+                    const infoRows = await db.query(infoSql, [request.sender_uuid, request.receiver_user_id])
+                    if (!infoRows || infoRows.length !== 1 || !infoRows[0].receiver_email) {
+                        this.logger.warn(`[SendCountNotify][approve][requestId=${requestId}] 接收人邮箱为空或用户不存在,跳过通知`)
+                        return
+                    }
+
+                    await EmailTemplate.sendCountRequestApproved(infoRows[0].receiver_email, {
+                        requestId,
+                        senderUsername: infoRows[0].sender_username || "未知用户",
+                        count: request.count,
+                        reviewTime
+                    })
+                } catch (mailErr) {
+                    this.logger.error(`[SendCountNotify][approve][requestId=${requestId}] 接收人通知发送失败:${mailErr.message || "未知错误"}`)
+                }
+            })
+
             return res.json({ ...BaseStdResponse.OK, msg: "审核通过成功" })
             return res.json({ ...BaseStdResponse.OK, msg: "审核通过成功" })
         } catch (err) {
         } catch (err) {
             try { await conn.rollback() } catch (_) { }
             try { await conn.rollback() } catch (_) { }

+ 35 - 1
apis/Goods/Admin/RejectSendCountRequest.js

@@ -2,6 +2,7 @@ const API = require("../../../lib/API")
 const db = require("../../../plugin/DataBase/db")
 const db = require("../../../plugin/DataBase/db")
 const AccessControl = require("../../../lib/AccessControl")
 const AccessControl = require("../../../lib/AccessControl")
 const { BaseStdResponse } = require("../../../BaseStdResponse")
 const { BaseStdResponse } = require("../../../BaseStdResponse")
+const EmailTemplate = require("../../../plugin/Email/emailTemplate")
 
 
 class RejectSendCountRequest extends API {
 class RejectSendCountRequest extends API {
     constructor() {
     constructor() {
@@ -36,7 +37,7 @@ class RejectSendCountRequest extends API {
             await conn.beginTransaction()
             await conn.beginTransaction()
 
 
             const [requestRows] = await conn.execute(
             const [requestRows] = await conn.execute(
-                `SELECT id, sender_uuid, count, status
+                `SELECT id, sender_uuid, receiver_user_id, count, status
                  FROM lepao_send_count_request
                  FROM lepao_send_count_request
                  WHERE id = ?
                  WHERE id = ?
                  FOR UPDATE`,
                  FOR UPDATE`,
@@ -75,6 +76,39 @@ class RejectSendCountRequest extends API {
             }
             }
 
 
             await conn.commit()
             await conn.commit()
+            const requestId = request.id
+            const reviewTime = new Date().getTime()
+
+            // 非阻塞通知赠送人,不影响审核结果
+            Promise.resolve().then(async () => {
+                try {
+                    const infoSql = `
+                        SELECT
+                            su.email AS sender_email,
+                            su.username AS sender_username,
+                            ru.username AS receiver_username
+                        FROM users su
+                        LEFT JOIN users ru ON ru.id = ?
+                        WHERE su.uuid = ?
+                    `
+                    const infoRows = await db.query(infoSql, [request.receiver_user_id, request.sender_uuid])
+                    if (!infoRows || infoRows.length !== 1 || !infoRows[0].sender_email) {
+                        this.logger.warn(`[SendCountNotify][reject][requestId=${requestId}] 赠送人邮箱为空或用户不存在,跳过通知`)
+                        return
+                    }
+
+                    await EmailTemplate.sendCountRequestRejected(infoRows[0].sender_email, {
+                        requestId,
+                        receiverUsername: infoRows[0].receiver_username || "未知用户",
+                        count: request.count,
+                        reviewTime,
+                        rejectReason: reject_reason || "无"
+                    })
+                } catch (mailErr) {
+                    this.logger.error(`[SendCountNotify][reject][requestId=${requestId}] 赠送人通知发送失败:${mailErr.message || "未知错误"}`)
+                }
+            })
+
             return res.json({ ...BaseStdResponse.OK, msg: "已拒绝该赠送申请" })
             return res.json({ ...BaseStdResponse.OK, msg: "已拒绝该赠送申请" })
         } catch (err) {
         } catch (err) {
             try { await conn.rollback() } catch (_) { }
             try { await conn.rollback() } catch (_) { }

+ 35 - 0
apis/Goods/SendCount.js

@@ -2,6 +2,7 @@ const API = require("../../lib/API")
 const db = require("../../plugin/DataBase/db")
 const db = require("../../plugin/DataBase/db")
 const AccessControl = require("../../lib/AccessControl")
 const AccessControl = require("../../lib/AccessControl")
 const { BaseStdResponse } = require("../../BaseStdResponse")
 const { BaseStdResponse } = require("../../BaseStdResponse")
+const EmailTemplate = require("../../plugin/Email/emailTemplate")
 
 
 class SendCount extends API {
 class SendCount extends API {
     constructor() {
     constructor() {
@@ -72,6 +73,40 @@ class SendCount extends API {
             }
             }
 
 
             await conn.commit()
             await conn.commit()
+            const requestId = insertResult.insertId
+            const createTime = new Date().getTime()
+
+            // 非阻塞通知管理员,不影响主业务流程
+            Promise.resolve().then(async () => {
+                try {
+                    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(`[SendCountNotify][submit][requestId=${requestId}] 未找到可通知的管理员邮箱`)
+                        return
+                    }
+
+                    const emails = [...new Set(adminRows.map(row => row.email).filter(Boolean))]
+                    for (const email of emails) {
+                        await EmailTemplate.sendCountRequestNotifyAdmins(email, {
+                            requestId,
+                            senderUsername: senderRows[0].username,
+                            receiverUsername: username,
+                            count,
+                            createTime
+                        })
+                    }
+                } catch (mailErr) {
+                    this.logger.error(`[SendCountNotify][submit][requestId=${requestId}] 管理员通知发送失败:${mailErr.message || "未知错误"}`)
+                }
+            })
+
             return res.json({ ...BaseStdResponse.OK, msg: "已提交审核,审核通过后接收方将到账" })
             return res.json({ ...BaseStdResponse.OK, msg: "已提交审核,审核通过后接收方将到账" })
 
 
         } catch (err) {
         } catch (err) {

+ 258 - 0
plugin/Email/emailTemplate.js

@@ -670,6 +670,264 @@ class emailTemplate {
         )
         )
     }
     }
 
 
+    async sendCountRequestNotifyAdmins(email, data) {
+        await sendEmail(email, 'RunForge - 赠送次数待审核提醒',
+            `<html lang="zh-CN">
+            <head>
+                <meta charset="UTF-8">
+                <meta name="viewport" content="width=device-width, initial-scale=1.0">
+                <title>赠送次数待审核提醒</title>
+                <style>
+                    body {
+                        font-family: Arial, sans-serif;
+                        background-color: #f4f4f4;
+                        margin: 0;
+                        padding: 0;
+                    }
+
+                    .container {
+                        width: 80%;
+                        margin: 20px auto;
+                        background-color: #fff;
+                        padding: 20px;
+                        border-radius: 8px;
+                        box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+                    }
+
+                    .head {
+                        display: flex;
+                        justify-content: center;
+                        align-items: center;
+                        gap: 10px;
+                        color: #2c3e50;
+                    }
+
+                    p {
+                        font-size: 16px;
+                        color: #34495e;
+                        line-height: 1.6;
+                        text-indent: 2em;
+                    }
+
+                    .info {
+                        background-color: #ecf0f1;
+                        padding: 15px;
+                        border-radius: 5px;
+                        margin: 20px 0;
+                    }
+
+                    .info p {
+                        margin: 5px 0;
+                        text-indent: 0;
+                    }
+
+                    .important {
+                        color: #e74c3c;
+                        font-weight: bold;
+                    }
+
+                    .footer {
+                        font-size: 14px;
+                        text-align: center;
+                        color: #7f8c8d;
+                        margin-top: 50px;
+                    }
+                </style>
+            </head>
+
+            <body>
+                <div class="container">
+                    <div class="head">
+                        <h2>赠送次数待审核提醒</h2>
+                    </div>
+
+                    <p>尊敬的管理员:</p>
+                    <p>系统收到一条新的赠送次数申请,请及时审核处理:</p>
+                    <div class="info">
+                        <p><strong>申请ID:</strong> ${data.requestId}</p>
+                        <p><strong>赠送人:</strong> ${data.senderUsername}</p>
+                        <p><strong>接收人:</strong> ${data.receiverUsername}</p>
+                        <p><strong>赠送次数:</strong> ${data.count}</p>
+                        <p><strong>申请时间:</strong> ${this.stramptoTime(data.createTime)}</p>
+                    </div>
+                    <p class="important">请前往 RunForge 管理后台进行审核,请勿直接回复邮件。</p>
+                    <p class="footer">Copyright © 2025 RunForge</p>
+                </div>
+            </body>
+
+            </html>`
+        )
+    }
+
+    async sendCountRequestApproved(email, data) {
+        await sendEmail(email, 'RunForge - 赠送次数审核通过提醒',
+            `<html lang="zh-CN">
+            <head>
+                <meta charset="UTF-8">
+                <meta name="viewport" content="width=device-width, initial-scale=1.0">
+                <title>赠送次数审核通过提醒</title>
+                <style>
+                    body {
+                        font-family: Arial, sans-serif;
+                        background-color: #f4f4f4;
+                        margin: 0;
+                        padding: 0;
+                    }
+
+                    .container {
+                        width: 80%;
+                        margin: 20px auto;
+                        background-color: #fff;
+                        padding: 20px;
+                        border-radius: 8px;
+                        box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+                    }
+
+                    .head {
+                        display: flex;
+                        justify-content: center;
+                        align-items: center;
+                        gap: 10px;
+                        color: #2c3e50;
+                    }
+
+                    p {
+                        font-size: 16px;
+                        color: #34495e;
+                        line-height: 1.6;
+                        text-indent: 2em;
+                    }
+
+                    .info {
+                        background-color: #ecf0f1;
+                        padding: 15px;
+                        border-radius: 5px;
+                        margin: 20px 0;
+                    }
+
+                    .info p {
+                        margin: 5px 0;
+                        text-indent: 0;
+                    }
+
+                    .footer {
+                        font-size: 14px;
+                        text-align: center;
+                        color: #7f8c8d;
+                        margin-top: 50px;
+                    }
+                </style>
+            </head>
+
+            <body>
+                <div class="container">
+                    <div class="head">
+                        <h2>赠送次数审核通过提醒</h2>
+                    </div>
+
+                    <p>尊敬的用户:</p>
+                    <p>您收到的一笔赠送次数申请已审核通过,次数已到账:</p>
+                    <div class="info">
+                        <p><strong>赠送人:</strong> ${data.senderUsername}</p>
+                        <p><strong>到账次数:</strong> ${data.count}</p>
+                        <p><strong>审核时间:</strong> ${this.stramptoTime(data.reviewTime)}</p>
+                    </div>
+                    <p class="footer">Copyright © 2025 RunForge</p>
+                </div>
+            </body>
+
+            </html>`
+        )
+    }
+
+    async sendCountRequestRejected(email, data) {
+        await sendEmail(email, 'RunForge - 赠送次数审核失败提醒',
+            `<html lang="zh-CN">
+            <head>
+                <meta charset="UTF-8">
+                <meta name="viewport" content="width=device-width, initial-scale=1.0">
+                <title>赠送次数审核失败提醒</title>
+                <style>
+                    body {
+                        font-family: Arial, sans-serif;
+                        background-color: #f4f4f4;
+                        margin: 0;
+                        padding: 0;
+                    }
+
+                    .container {
+                        width: 80%;
+                        margin: 20px auto;
+                        background-color: #fff;
+                        padding: 20px;
+                        border-radius: 8px;
+                        box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+                    }
+
+                    .head {
+                        display: flex;
+                        justify-content: center;
+                        align-items: center;
+                        gap: 10px;
+                        color: #2c3e50;
+                    }
+
+                    p {
+                        font-size: 16px;
+                        color: #34495e;
+                        line-height: 1.6;
+                        text-indent: 2em;
+                    }
+
+                    .info {
+                        background-color: #ecf0f1;
+                        padding: 15px;
+                        border-radius: 5px;
+                        margin: 20px 0;
+                    }
+
+                    .info p {
+                        margin: 5px 0;
+                        text-indent: 0;
+                    }
+
+                    .important {
+                        color: #e74c3c;
+                        font-weight: bold;
+                    }
+
+                    .footer {
+                        font-size: 14px;
+                        text-align: center;
+                        color: #7f8c8d;
+                        margin-top: 50px;
+                    }
+                </style>
+            </head>
+
+            <body>
+                <div class="container">
+                    <div class="head">
+                        <h2>赠送次数审核失败提醒</h2>
+                    </div>
+
+                    <p>尊敬的用户:</p>
+                    <p>您发起的一笔赠送次数申请未通过审核,已将次数退回到您的账户:</p>
+                    <div class="info">
+                        <p><strong>接收人:</strong> ${data.receiverUsername}</p>
+                        <p><strong>退回次数:</strong> ${data.count}</p>
+                        <p><strong>审核时间:</strong> ${this.stramptoTime(data.reviewTime)}</p>
+                        <p><strong>拒绝原因:</strong> ${data.rejectReason}</p>
+                    </div>
+                    <p class="important">如有疑问,请前往 RunForge 联系客服处理。</p>
+                    <p class="footer">Copyright © 2025 RunForge</p>
+                </div>
+            </body>
+
+            </html>`
+        )
+    }
+
     async powerCheck(email, data) {
     async powerCheck(email, data) {
         await sendEmail(email, '宿舍电费提醒',
         await sendEmail(email, '宿舍电费提醒',
             `<html lang="zh-CN">
             `<html lang="zh-CN">