Browse Source

✨ feat: 增加电费提醒功能

Pchen. 7 months ago
parent
commit
e598c58be3

+ 106 - 0
apis/Corn/StartPowerCheck.js

@@ -0,0 +1,106 @@
+const API = require("../../lib/API.js")
+const db = require('../../plugin/DataBase/db.js')
+const axios = require("axios")
+const EmailTemplate = require('../../plugin/Email/emailTemplate')
+const { BaseStdResponse } = require("../../BaseStdResponse.js")
+
+class StartPowerCheck extends API {
+    constructor() {
+        super()
+        this.noEncrypt()
+        this.setPath('/Corn/StartPowerCheck')
+        this.setMethod('GET')
+    }
+
+    async onRequest(req, res) {
+        try {
+            res.json({ ...BaseStdResponse.OK })
+
+            this.logger.info('开始执行电费提醒任务')
+
+            const sql = 'SELECT id, email, balance, lowest, area, building, room, is_notice FROM power_task'
+            const tasks = await db.query(sql)
+
+            if (!tasks) {
+                this.logger.error('获取电费提醒任务失败!')
+                return
+            }
+
+            for (const item of tasks) {
+                const { id, email, balance, lowest, area, building, room, is_notice } = item
+                this.logger.info(`${building}-${room} 开始电费查询余额`)
+
+                try {
+                    const endpoint = `https://hqpay.ctbu.edu.cn/weixin/ashx/frmuser.ashx?test=lastlist&pid=${room}&dyid=${building}`
+                    const response = await axios.get(endpoint, { proxy: false, timeout: 8000 })
+
+                    if (!response || !response.data || !response.data[0]) {
+                        this.logger.error('获取电费信息失败!返回数据:' + (response && response.data ? JSON.stringify(response.data) : 'no-response'))
+                        continue
+                    }
+
+                    const now_balance_raw = response.data[0][1]
+                    const now_change_time = response.data[0][2]
+
+                    this.logger.info(`${building}-${room} 电费余额:${now_balance_raw},扣费时间:${now_change_time}`)
+                    const time = Date.now()
+
+                    const nowBalanceNum = parseFloat(String(now_balance_raw).replace(/[^0-9.-]/g, ''))
+                    const balanceNum = parseFloat(String(balance).replace(/[^0-9.-]/g, '')) || 0
+                    const lowestNum = parseFloat(String(lowest).replace(/[^0-9.-]/g, '')) || 0
+
+                    if (Number.isNaN(nowBalanceNum)) {
+                        this.logger.error(`${building}-${room}:解析当前余额失败,原值:${now_balance_raw}`)
+                        continue
+                    }
+
+                    // 如果余额未变,跳过当前任务
+                    if (nowBalanceNum === balanceNum) {
+                        continue
+                    }
+
+                    // 记录变更并更新任务表余额
+                    const insertSql = 'INSERT INTO power_chang_record (time, balance, old_balance, change_time, task_id) VALUES (?, ?, ?, ?, ?)'
+                    const insertRows = await db.query(insertSql, [time, nowBalanceNum, balanceNum, now_change_time, id])
+
+                    const updateSql = 'UPDATE power_task SET update_time = ?, balance = ?, koufei_date = ? WHERE id = ?'
+                    const updateRows = await db.query(updateSql, [time, nowBalanceNum, now_change_time, id])
+
+                    if (!insertRows || insertRows.affectedRows !== 1 || !updateRows || updateRows.affectedRows !== 1) {
+                        this.logger.error(`${building}-${room}:更新电费信息失败! 数据库错误`)
+                        continue
+                    }
+
+                    // 余额低于阈值且尚未通知 -> 发邮件并置 is_notice = 1
+                    if (nowBalanceNum < balanceNum && nowBalanceNum <= lowestNum && Number(is_notice) === 0) {
+                        const data = { now_balance: nowBalanceNum, now_change_time, lowest: lowestNum, area, building, room }
+                        try {
+                            await EmailTemplate.powerCheck(email, data)
+                            const upd = await db.query('UPDATE power_task SET is_notice = 1 WHERE id = ?', [id])
+                            if (!upd || upd.affectedRows !== 1) {
+                                this.logger.error(`${building}-${room}:更新 is_notice=1 失败`)
+                            }
+                        } catch (err) {
+                            this.logger.error(`${building}-${room}:发送邮件失败:${err.stack || err}`)
+                        }
+                    }
+
+                    // 充值后恢复未提醒状态
+                    if (nowBalanceNum > balanceNum && nowBalanceNum > lowestNum) {
+                        const upd = await db.query('UPDATE power_task SET is_notice = 0 WHERE id = ?', [id])
+                        if (!upd || upd.affectedRows !== 1) {
+                            this.logger.error(`${building}-${room}:更新 is_notice=0 失败`)
+                        }
+                    }
+                } catch (error) {
+                    this.logger.error(`获取电费信息失败!${error.stack || error}`)
+                    continue
+                }
+            }
+        } catch (error) {
+            this.logger.error(error)
+        }
+    }
+}
+
+module.exports.StartPowerCheck = StartPowerCheck

+ 81 - 0
apis/Power/AddAccount.js

@@ -0,0 +1,81 @@
+const API = require("../../lib/API.js")
+const db = require("../../plugin/DataBase/db.js")
+const axios = require("axios")
+const { BaseStdResponse } = require("../../BaseStdResponse.js")
+const AccessControl = require("../../lib/AccessControl.js")
+
+class AddAccount extends API {
+    constructor() {
+        super()
+
+        this.setPath('/Power/Account')
+        this.setMethod('POST')
+    }
+
+    async onRequest(req, res) {
+        let { uuid, session, area, building, room, email, lowest, notes, id } = req.body
+
+        if ([uuid, session, area, building, lowest, room].some(value => value === '' || value === null || value === undefined))
+            return res.json({
+                ...BaseStdResponse.MISSING_PARAMETER
+            })
+
+        if (!await AccessControl.checkSession(uuid, session))
+            return res.status(401).json({
+                ...BaseStdResponse.ACCESS_DENIED
+            })
+
+        const time = new Date().getTime()
+
+        let sql, r
+
+        if (!id) {
+            let balance, koufei_date
+            try {
+                const endpoint = `https://hqpay.ctbu.edu.cn/weixin/ashx/frmuser.ashx?test=lastlist&pid=${room}&dyid=${building}`
+                const res = await axios.get(endpoint, {
+                    proxy: false
+                })
+
+                if (!res || !res.data || !res.data[0])
+                    return res.json({
+                        ...BaseStdResponse.ERR,
+                        msg: '获取电费信息失败!请稍后再试'
+                    })
+                balance = res.data[0][1]
+                koufei_date = res.data[0][2]
+
+            } catch (error) {
+                this.logger.error(`获取电费信息失败!${error.stack}`)
+                return res.json({
+                    ...BaseStdResponse.ERR,
+                    msg: '获取电费信息失败!请稍后再试'
+                })
+            }
+
+            sql = 'INSERT INTO power_task (create_user, create_time, update_time, area, building, room, email, lowest, notes, balance, koufei_date) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
+            r = await db.query(sql, [uuid, time, time, area, building, room, email, lowest, notes ?? '', balance, koufei_date])
+        } else {
+            sql = 'UPDATE power_task SET area = ?, building = ?, room = ?, email = ?, lowest = ?, notes = ? WHERE id = ?'
+            r = await db.query(sql, [area, building, room, email, lowest, notes ?? '', id])
+        }
+
+        try {
+            if (r && r.affectedRows > 0) {
+                res.json({
+                    ...BaseStdResponse.OK
+                })
+            } else {
+                return res.json({ ...BaseStdResponse.ERR, msg: '添加电费提醒任务失败!数据库错误' })
+            }
+        } catch (err) {
+            this.logger.error(`添加电费提醒任务失败!${err.stack}`)
+            res.json({
+                ...BaseStdResponse.ERR,
+                msg: "添加电费提醒任务失败!",
+            })
+        }
+    }
+}
+
+module.exports.AddAccount = AddAccount

+ 52 - 0
apis/Power/DeleteAccount.js

@@ -0,0 +1,52 @@
+const API = require("../../lib/API")
+const db = require("../../plugin/DataBase/db")
+const AccessControl = require("../../lib/AccessControl")
+const { BaseStdResponse } = require("../../BaseStdResponse")
+
+class DeleteAccount extends API {
+    constructor() {
+        super()
+
+        this.setPath('/Power/Account')
+        this.setMethod('DELETE')
+    }
+
+    async onRequest(req, res) {
+        let { uuid, session, id } = req.body
+
+        if ([uuid, session, id].some(value => value === '' || value === null || value === undefined))
+            return res.json({
+                ...BaseStdResponse.MISSING_PARAMETER
+            })
+
+        // 检查 session
+        if (!await AccessControl.checkSession(uuid, session))
+            return res.status(401).json({
+                ...BaseStdResponse.ACCESS_DENIED
+            })
+
+        let sql = 'DELETE FROM power_task WHERE create_user = ? AND id = ?'
+        let r = await db.query(sql, [uuid, id])
+
+        try {
+            if (r && r.affectedRows > 0) {
+                res.json({
+                    ...BaseStdResponse.OK
+                })
+            } else {
+                return res.json({
+                    ...BaseStdResponse.ERR,
+                    msg: '删除电费提醒任务失败!数据库错误'
+                })
+            }
+        } catch (err) {
+            this.logger.error(`删除电费提醒任务失败!${err.stack}`)
+            res.json({
+                ...BaseStdResponse.ERR,
+                msg: "删除电费提醒任务失败!",
+            })
+        }
+    }
+}
+
+module.exports.DeleteAccount = DeleteAccount

+ 52 - 0
apis/Power/GetAccount.js

@@ -0,0 +1,52 @@
+const API = require("../../lib/API.js");
+const db = require("../../plugin/DataBase/db.js");
+const { BaseStdResponse } = require("../../BaseStdResponse.js");
+const AccessControl = require("../../lib/AccessControl.js");
+
+class GetAccount extends API {
+    constructor() {
+        super();
+
+        this.setPath('/Power/Account')
+        this.setMethod('GET')
+    }
+
+    async onRequest(req, res) {
+        let { uuid, session } = req.query
+
+        if ([uuid, session].some(value => value === '' || value === null || value === undefined))
+            return res.json({
+                ...BaseStdResponse.MISSING_PARAMETER,
+                endpoint: 1513126
+            })
+
+        if (!await AccessControl.checkSession(uuid, session))
+            return res.status(401).json({
+                ...BaseStdResponse.ACCESS_DENIED
+            })
+
+        let sql = `
+                SELECT 
+                    *
+                FROM 
+                    power_task
+                WHERE 
+                    create_user = ?
+                ORDER BY id DESC
+            `
+
+        let rows = await db.query(sql, [uuid])
+
+        if (!rows)
+            return res.json({
+                ...BaseStdResponse.DATABASE_ERR
+            })
+
+        res.json({
+            ...BaseStdResponse.OK,
+            data: rows
+        })
+    }
+}
+
+module.exports.GetAccount = GetAccount

+ 51 - 0
apis/Power/GetChangeRecord.js

@@ -0,0 +1,51 @@
+const API = require("../../lib/API.js");
+const db = require("../../plugin/DataBase/db.js");
+const { BaseStdResponse } = require("../../BaseStdResponse.js");
+const AccessControl = require("../../lib/AccessControl.js");
+
+class GetAccount extends API {
+    constructor() {
+        super();
+
+        this.setPath('/Power/GetChangeRecord')
+        this.setMethod('POST')
+    }
+
+    async onRequest(req, res) {
+        let { uuid, session, id } = req.body
+
+        if ([uuid, session, id].some(value => value === '' || value === null || value === undefined))
+            return res.json({
+                ...BaseStdResponse.MISSING_PARAMETER
+            })
+
+        if (!await AccessControl.checkSession(uuid, session))
+            return res.status(401).json({
+                ...BaseStdResponse.ACCESS_DENIED
+            })
+
+        let sql = `
+                SELECT 
+                    *
+                FROM 
+                    power_change_record
+                WHERE 
+                    id = ?
+                ORDER BY id DESC
+            `
+
+        let rows = await db.query(sql, [id])
+
+        if (!rows)
+            return res.json({
+                ...BaseStdResponse.DATABASE_ERR
+            })
+
+        res.json({
+            ...BaseStdResponse.OK,
+            data: rows
+        })
+    }
+}
+
+module.exports.GetAccount = GetAccount

+ 51 - 0
apis/Power/GetPowerData.js

@@ -0,0 +1,51 @@
+const API = require("../../lib/API.js")
+const axios = require("axios")
+const { BaseStdResponse } = require("../../BaseStdResponse.js")
+
+// 获取楼栋
+class GetDyList extends API {
+    constructor() {
+        super()
+
+        this.setPath("/Power/GetPowerData")
+        this.setMethod("POST")
+    }
+
+    async onRequest(req, res) {
+        try {
+            const { type, pid } = req.body
+            if ([type].some(value => value === '' || value === null || value === undefined))
+                return res.json({
+                    ...BaseStdResponse.MISSING_PARAMETER
+                })
+            if (type !== 'buildlist')
+                if ([pid].some(value => value === '' || value === null || value === undefined))
+                    return res.json({
+                        ...BaseStdResponse.MISSING_PARAMETER
+                    })
+
+            const url = `https://hqpay.ctbu.edu.cn/weixin/ashx/frmuser.ashx?test=${type}${type !== 'buildlist' ? '&pid=' + pid : ''}`
+            const response = await axios.get(url, {
+                proxy: false
+            })
+            if (!response || !response.data)
+                return res.json({
+                    ...BaseStdResponse.ERR,
+                    msg: "请稍后再试"
+                })
+
+            res.json({
+                ...BaseStdResponse.OK,
+                data: response.data ?? []
+            })
+        } catch (error) {
+            this.logger?.error(`${error.stack}`)
+            return res.json({
+                ...BaseStdResponse.ERR,
+                msg: `${error.message ?? "请稍后再试"}`
+            })
+        }
+    }
+}
+
+module.exports.GetDyList = GetDyList

+ 3 - 1
lib/Lepao/Lepao.js

@@ -132,7 +132,9 @@ class Lepao {
             let oss_path
             let oss_path
 
 
             try {
             try {
-                const ossRes = await axios.post(ossUrl, ossData)
+                const ossRes = await axios.post(ossUrl, ossData, {
+                    proxy: false
+                })
 
 
                 const { data } = ossRes
                 const { data } = ossRes
                 this.logger.info(`${account}上传OSS记录返回结果: ${JSON.stringify(data)}`)
                 this.logger.info(`${account}上传OSS记录返回结果: ${JSON.stringify(data)}`)

+ 94 - 4
plugin/Email/emailTemplate.js

@@ -259,8 +259,8 @@ class emailTemplate {
 
 
                 <p class="important">
                 <p class="important">
                 ${data.auto_run === 0
                 ${data.auto_run === 0
-                    ? '现在还没有帮宝宝开启自动乐跑呢 🐾💦 如果想要开始跑跑的话,记得登录后去RunForge手动点一下哦~ 🌈💕'
-                    : '已经为宝宝开启了自动乐跑啦 🏃‍♀️✨ 登录后系统会乖乖替你完成乐跑 💖 记得留意邮箱提醒哟~ 📬'}
+                ? '现在还没有帮宝宝开启自动乐跑呢 🐾💦 如果想要开始跑跑的话,记得登录后去RunForge手动点一下哦~ 🌈💕'
+                : '已经为宝宝开启了自动乐跑啦 🏃‍♀️✨ 登录后系统会乖乖替你完成乐跑 💖 记得留意邮箱提醒哟~ 📬'}
                 </p>
                 </p>
                 <p class="important">宝宝要记得不要在其他设备上登录“智慧体育”小程序哦 🚫📱,不然会失效,到时候又要重新登录啦 😢💦~</p>
                 <p class="important">宝宝要记得不要在其他设备上登录“智慧体育”小程序哦 🚫📱,不然会失效,到时候又要重新登录啦 😢💦~</p>
                 <p class="important">有问题随时喊RunForge客服小可爱呀 💕💌 我们都会耐心陪宝宝解决的哟 ✨🥰</p>
                 <p class="important">有问题随时喊RunForge客服小可爱呀 💕💌 我们都会耐心陪宝宝解决的哟 ✨🥰</p>
@@ -446,7 +446,7 @@ class emailTemplate {
                 <h2>RunForge - 乐跑成功啦 🎉💖</h2>
                 <h2>RunForge - 乐跑成功啦 🎉💖</h2>
                 </div>
                 </div>
 
 
-                <p>亲爱的 ${data.name } 宝宝:</p>
+                <p>亲爱的 ${data.name} 宝宝:</p>
                 <p>耶耶耶~ RunForge已经乖乖帮你完成了一次乐跑啦 ✨🏃‍♀️💨</p>
                 <p>耶耶耶~ RunForge已经乖乖帮你完成了一次乐跑啦 ✨🏃‍♀️💨</p>
 
 
                 <div class="info">
                 <div class="info">
@@ -541,7 +541,7 @@ class emailTemplate {
                     <h2>RunForge - 乐跑没有成功呢 😢💦</h2>
                     <h2>RunForge - 乐跑没有成功呢 😢💦</h2>
                     </div>
                     </div>
 
 
-                    <p>亲爱的 ${data.name ?? data.account } 宝宝 🌸:</p>
+                    <p>亲爱的 ${data.name ?? data.account} 宝宝 🌸:</p>
                     <p>RunForge刚刚尝试帮你完成乐跑的时候,遇到了一点小意外呢 💔💦:</p>
                     <p>RunForge刚刚尝试帮你完成乐跑的时候,遇到了一点小意外呢 💔💦:</p>
 
 
                     <div class="info">
                     <div class="info">
@@ -647,6 +647,96 @@ class emailTemplate {
         )
         )
     }
     }
 
 
+    async powerCheck(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>RunForge - 宿舍电费提醒</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;
+                    }
+
+                    .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>RunForge - 宿舍电费提醒</h2>
+                </div>
+
+                <p>尊敬的用户:</p>
+                <p>${data.building}${data.room}宿舍电费已低于预设提醒阈值,请留意:</p>
+
+                <div class="info">
+                <p><strong>校区:</strong> ${data.area}</p>
+                <p><strong>楼栋:</strong> ${data.building}</p>
+                <p><strong>寝室号:</strong> ${data.room}</p>
+                <p><strong>当前余额:</strong> ${data.now_balance}</p>
+                <p><strong>扣费时间:</strong> ${data.now_change_time}</p>
+                </div>
+
+                <p class="important">当前电费已低于预设提醒阈值${data.lowest - data.now_balance}元,请及时充值</p>
+                <p class="footer">Copyright © 2025 RunForge ✨</p>
+            </div>
+            </body>
+
+            </html>`
+        )
+    }
+
     // 时长计算
     // 时长计算
     formatSecondsToMinSec(totalSeconds) {
     formatSecondsToMinSec(totalSeconds) {
         const minutes = Math.floor(totalSeconds / 60);
         const minutes = Math.floor(totalSeconds / 60);