Browse Source

更新乐跑代码

Pchen. 11 months ago
parent
commit
8214afff4b

+ 4 - 0
BaseStdResponse.js

@@ -7,6 +7,10 @@ let BaseStdResponse = {
         code:-1,
         code:-1,
         msg:'err'
         msg:'err'
     },
     },
+    NOTVIP:{
+        code:-666,
+        msg:'您使用了VIP专属权益,请先开通'
+    },
     MISSING_PARAMETER:{//缺少参数
     MISSING_PARAMETER:{//缺少参数
         code:-2,
         code:-2,
         msg:'Missing parameter'
         msg:'Missing parameter'

+ 22 - 39
apis/Captcha/SendEmail.js

@@ -1,70 +1,50 @@
-const API = require("../../lib/API");
-const { BaseStdResponse } = require("../../BaseStdResponse");
-const Redis = require('../../plugin/DataBase/Redis');
-const sendEmail = require('../../plugin/Email/Email');
+const API = require("../../lib/API")
+const { BaseStdResponse } = require("../../BaseStdResponse")
+const Redis = require('../../plugin/DataBase/Redis')
+const EmailTemplate = require('../../plugin/Email/emailTemplate')
 
 
 // 发送邮箱验证码
 // 发送邮箱验证码
 class SendEmail extends API {
 class SendEmail extends API {
     constructor() {
     constructor() {
-        super();
+        super()
 
 
-        this.setMethod("POST");
-        this.setPath("/Captcha/SendEmail");
+        this.setMethod("POST")
+        this.setPath("/Captcha/SendEmail")
     }
     }
 
 
     async onRequest(req, res) {
     async onRequest(req, res) {
-        const { email, text, id, type } = req.body;
+        const { email, text, id, type } = req.body
 
 
-        if ([email, text, id, type].some(value => value === '' || value === null || value === undefined)) {
-            res.json({
+        if ([email, text, id, type].some(value => value === '' || value === null || value === undefined))
+            return res.json({
                 ...BaseStdResponse.MISSING_PARAMETER,
                 ...BaseStdResponse.MISSING_PARAMETER,
                 endpoint: 1513126
                 endpoint: 1513126
-            });
-            return;
-        }
-
+            })
+            
         try {
         try {
-            const code = await Redis.get(`captcha:${id}`);
+            const code = await Redis.get(`captcha:${id}`)
             if (!code || code != text.toLowerCase())
             if (!code || code != text.toLowerCase())
                 return res.json({
                 return res.json({
                     ...BaseStdResponse.SMS_CHECK_FAIL,
                     ...BaseStdResponse.SMS_CHECK_FAIL,
                     msg: '验证码输入错误或已过期'
                     msg: '验证码输入错误或已过期'
                 })
                 })
 
 
-            await Redis.del(`captcha:${id}`);
+            await Redis.del(`captcha:${id}`)
         } catch (err) {
         } catch (err) {
-            this.logger.error(`验证图片验证码失败!${err.stack}`);
+            this.logger.error(`验证图片验证码失败!${err.stack}`)
             return res.json({
             return res.json({
                 ...BaseStdResponse.DATABASE_ERR,
                 ...BaseStdResponse.DATABASE_ERR,
                 msg: '验证失败!'
                 msg: '验证失败!'
             })
             })
         }
         }
 
 
-        let content;
-        switch (type) {
-            case 'register':
-                content = '您正在注册CTBU_CLUB账号,';
-                break;
-            case 'forget':
-                content = '您正在找回CTBU_CLUB账号,';
-                break;
-            case 'bind':
-                content = '您正在进行换绑邮箱操作,';
-                break;
-            default:
-                return res.json({
-                    ...BaseStdResponse.METHOD_NOT_EXIST
-                })
-        }
-
-        const code = Math.random().toFixed(6).slice(-6);
+        const code = Math.random().toFixed(6).slice(-6)
         try {
         try {
             await Redis.set(`email:${email}`, code, {
             await Redis.set(`email:${email}`, code, {
                 EX: 600
                 EX: 600
-            });
-            await sendEmail(email, '验证码', `${content}您的验证码为:${code}。此验证码10分钟内有效,请妥善保管您的验证码,非本人操作请忽略。`);
+            })
         } catch (err) {
         } catch (err) {
-            this.logger.error(`发送邮箱验证码失败!${err.stack}`);
+            this.logger.error(`发送邮箱验证码失败!${err.stack}`)
             return res.json({
             return res.json({
                 ...BaseStdResponse.SMS_SEND_FAIL,
                 ...BaseStdResponse.SMS_SEND_FAIL,
                 msg: '请检查邮箱格式后再试!'
                 msg: '请检查邮箱格式后再试!'
@@ -74,7 +54,10 @@ class SendEmail extends API {
         res.json({
         res.json({
             ...BaseStdResponse.OK
             ...BaseStdResponse.OK
         })
         })
+
+        // 先返回后发送
+        EmailTemplate.checkEmail(email, code)
     }
     }
 }
 }
 
 
-module.exports.SendEmail = SendEmail;
+module.exports.SendEmail = SendEmail

+ 42 - 0
apis/Corn/StartAutoLepao.js

@@ -0,0 +1,42 @@
+const API = require("../../lib/API");
+const db = require('../../plugin/DataBase/db')
+const lepao = require("../../lib/Lepao/Lepao.js").lepao
+
+const { BaseStdResponse } = require("../../BaseStdResponse");
+
+class GetSeatMenu extends API {
+    constructor() {
+        super();
+        this.setPath('/Lepao/BeginLepao');
+        this.setMethod('GET');
+    }
+
+    async onRequest(req, res) {
+        try {
+            res.json({
+                ...BaseStdResponse.OK
+            })
+
+            this.logger.info('开始执行自动乐跑任务')
+            let sql = 'SELECT name, create_user, student_num, token, uid, school_id FROM lepao_account WHERE \`state\` = 1'
+            let r = await db.query(sql)
+            if (!r)
+                return this.logger.error('获取自动乐跑账号失败!')
+
+            for (const item of r) {
+                const { name, uuid, student_num, token, uid, school_id } = item;
+                this.logger.info(`${name}(${student_num})开始乐跑`)
+                try {
+                    await lepao.beginLepao(uuid, student_num, token, uid, school_id)
+                    this.logger.info(`${name}(${student_num})乐跑完成`)
+                } catch (err) {
+                    this.logger.error(`${name}(${student_num})乐跑失败:${err.message || err}`)
+                }
+            }
+        } catch (error) {
+            this.logger.error(error)
+        }
+    }
+}
+
+module.exports.GetSeatMenu = GetSeatMenu;

+ 47 - 0
apis/Goods/GetCount.js

@@ -0,0 +1,47 @@
+const API = require("../../lib/API");
+const db = require("../../plugin/DataBase/db");
+const AccessControl = require("../../lib/AccessControl");
+const { BaseStdResponse } = require("../../BaseStdResponse");
+
+class GetCount extends API {
+    constructor() {
+        super();
+
+        this.setPath('/Goods/GetCount')
+        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
+            })
+
+        // 检查 session
+        if (!await AccessControl.checkSession(uuid, session))
+            return res.status(401).json({
+                ...BaseStdResponse.ACCESS_DENIED
+            })
+
+        let sql = 'SELECT ic_count, lepao_count, vip FROM users WHERE uuid = ? '
+        let rows = await db.query(sql, [uuid])
+
+        if (!rows || rows.length === 0)
+            return res.json({
+                ...BaseStdResponse.MISSING_FILE,
+                msg: '获取用户信息失败!'
+            })
+
+        res.json({
+            ...BaseStdResponse.OK,
+            data: rows[0]
+        })
+    }
+}
+
+module.exports.GetCount = GetCount;

+ 23 - 8
apis/Lepao/AddAccount.js

@@ -3,7 +3,7 @@ const db = require("../../plugin/DataBase/db.js");
 const { BaseStdResponse } = require("../../BaseStdResponse.js");
 const { BaseStdResponse } = require("../../BaseStdResponse.js");
 const AccessControl = require("../../lib/AccessControl");
 const AccessControl = require("../../lib/AccessControl");
 
 
-class GetType extends API {
+class AddAccount extends API {
     constructor() {
     constructor() {
         super();
         super();
 
 
@@ -12,7 +12,7 @@ class GetType extends API {
     }
     }
 
 
     async onRequest(req, res) {
     async onRequest(req, res) {
-        let { uuid, session, student_num, name, email, id } = req.body
+        let { uuid, session, student_num, name, email, id, area, max_distance, min_distance } = req.body
 
 
         if ([uuid, session, student_num, name, email].some(value => value === '' || value === null || value === undefined))
         if ([uuid, session, student_num, name, email].some(value => value === '' || value === null || value === undefined))
             return res.json({
             return res.json({
@@ -25,16 +25,31 @@ class GetType extends API {
                 ...BaseStdResponse.ACCESS_DENIED
                 ...BaseStdResponse.ACCESS_DENIED
             })
             })
 
 
+        // 判断用户是否用了会员权益
+        let userSql = 'SELECT vip FROM users WHERE uuid = ?'
+        let userData = await db.query(userSql, [uuid])
+        if(!userData || userData.length !== 1)
+            return res.json({ ...BaseStdResponse.ERR,  msg: '添加乐跑账号失败!数据库错误' })
+
+        if(userData[0].vip !== 1) {
+            // 占位 限制账号个数
+            
+            if(min_distance != 2.00 || max_distance != 4.00)
+                return res.json({ ...BaseStdResponse.NOTVIP,  msg: '仅VIP用户可设置跑步距离区间' })
+            if(area != '')
+                return res.json({ ...BaseStdResponse.NOTVIP,  msg: '仅VIP用户可指定跑区' })
+        }
+
         const time = new Date().getTime()
         const time = new Date().getTime()
 
 
         let sql, r
         let sql, r
 
 
         if (!id) {
         if (!id) {
-            sql = 'INSERT INTO lepao_account (student_num, name, email, create_user, create_time) VALUES (?, ?, ?, ?, ?)'
-            r = await db.query(sql, [student_num, name, email, uuid, time])
+            sql = 'INSERT INTO lepao_account (student_num, name, email, area, create_user, create_time) VALUES (?, ?, ?, ?, ?, ?)'
+            r = await db.query(sql, [student_num, name, email, area, uuid, time])
         } else {
         } else {
-            sql = 'UPDATE lepao_account SET student_num = ?, name = ?, email = ?,  update_time = ? WHERE id = ?'
-            r = await db.query(sql, [student_num, name, email, time, id])
+            sql = 'UPDATE lepao_account SET student_num = ?, name = ?, email = ?, area = ?,  update_time = ? WHERE id = ?'
+            r = await db.query(sql, [student_num, name, email, area, time, id])
         }
         }
 
 
         try {
         try {
@@ -44,7 +59,7 @@ class GetType extends API {
                     id: r.insertId
                     id: r.insertId
                 })
                 })
             } else {
             } else {
-                res.json({ ...BaseStdResponse.ERR, endpoint: 7894378, msg: '添加乐跑账号失败!数据库错误' })
+                return res.json({ ...BaseStdResponse.ERR, msg: '添加乐跑账号失败!数据库错误' })
             }
             }
         } catch (err) {
         } catch (err) {
             this.logger.error(`添加乐跑账号失败!${err.stack}`)
             this.logger.error(`添加乐跑账号失败!${err.stack}`)
@@ -56,4 +71,4 @@ class GetType extends API {
     }
     }
 }
 }
 
 
-module.exports.GetType = GetType;
+module.exports.AddAccount = AddAccount;

+ 4 - 0
apis/Lepao/GetAccount.js

@@ -33,6 +33,10 @@ class GetAccount extends API {
                     a.create_time,
                     a.create_time,
                     a.update_time,
                     a.update_time,
                     a.state,
                     a.state,
+                    a.area,
+                    a.min_distance,
+                    a.max_distance,
+                    a.race,
                     a.email,
                     a.email,
                     a.auto_run,
                     a.auto_run,
                     u.username AS create_user
                     u.username AS create_user

+ 53 - 0
apis/Lepao/SingleRun.js

@@ -0,0 +1,53 @@
+const API = require("../../lib/API.js");
+const db = require("../../plugin/DataBase/db.js");
+const { BaseStdResponse } = require("../../BaseStdResponse.js");
+const AccessControl = require("../../lib/AccessControl.js");
+const lepao = require("../../lib/Lepao/Lepao.js").lepao
+
+// 单次乐跑
+class SingleRun extends API {
+    constructor() {
+        super();
+
+        this.setPath('/Lepao/SingleRun')
+        this.setMethod('GET')
+    }
+
+    async onRequest(req, res) {
+        let { uuid, session, student_num } = req.query
+
+        if ([uuid, session, student_num].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 token, uid, school_id FROM lepao_account WHERE create_user = ? AND student_num = ?'
+        let rows = await db.query(sql, [uuid, student_num])
+        if(!rows || rows.length === 0)
+            return res.json({
+                ...BaseStdResponse.ERR,
+                msg: '发起乐跑失败!未找到对应的账号信息'
+            })
+
+        res.json({
+            ...BaseStdResponse.OK
+        })
+
+        lepao.beginLepao(uuid, student_num, rows[0].token, rows[0].uid, rows[0].school_id)
+
+    } catch(err) {
+        this.logger.error(`手动乐跑失败!${err.stack}`);
+        res.json({
+            ...BaseStdResponse.ERR,
+            msg: "乐跑失败!数据库异常"
+        })
+    }
+}
+
+module.exports.SingleRun = SingleRun;

+ 9 - 16
apis/Order/CallBack.js

@@ -9,22 +9,13 @@ const PAYMENT_KEY = config.pay.key
 class CallBack extends API {
 class CallBack extends API {
     constructor() {
     constructor() {
         super()
         super()
-        this.setPath('/Order/CallBack')
+        this.setPath('/Corn/StartAutoLepao')
         this.setMethod('GET')
         this.setMethod('GET')
     }
     }
 
 
     async onRequest(req, res) {
     async onRequest(req, res) {
-        const {
-            pid, trade_no, out_trade_no, type, name, money,
-            trade_status, sign, sign_type
-        } = req.query
 
 
-        // 参数校验
-        if ([pid, trade_no, out_trade_no, type, name, money, trade_status, sign, sign_type].some(v => v === undefined)) {
-            return res.json({
-                ...BaseStdResponse.MISSING_PARAMETER
-            })
-        }
+
 
 
         // 签名校验
         // 签名校验
         const rawParams = {
         const rawParams = {
@@ -48,8 +39,9 @@ class CallBack extends API {
 
 
         try {
         try {
             // 更新订单状态为已支付(state=1)
             // 更新订单状态为已支付(state=1)
-            let sql = 'UPDATE orders SET state = 1, pay_type = ?, pay_id = ? WHERE orderId = ? AND state = 0'
-            const result = await db.query(sql, [type, trade_no, out_trade_no])
+            const time = new Date().getTime()
+            let sql = 'UPDATE orders SET state = 1, pay_type = ?, pay_id = ?, pay_time = ? WHERE orderId = ? AND state = 0'
+            const result = await db.query(sql, [type, trade_no, time, out_trade_no])
 
 
             if (result.affectedRows > 0) {
             if (result.affectedRows > 0) {
                 // 查询订单与商品信息
                 // 查询订单与商品信息
@@ -57,6 +49,7 @@ class CallBack extends API {
                     SELECT 
                     SELECT 
                         g.lepao_count,
                         g.lepao_count,
                         g.ic_count,
                         g.ic_count,
+                        g.vip,
                         a.create_user
                         a.create_user
                     FROM 
                     FROM 
                         orders a
                         orders a
@@ -74,11 +67,11 @@ class CallBack extends API {
                     return res.send('fail')
                     return res.send('fail')
                 }
                 }
 
 
-                const { lepao_count, ic_count, create_user } = rows[0]
+                const { lepao_count, ic_count, vip, create_user } = rows[0]
 
 
                 // 更新用户剩余次数
                 // 更新用户剩余次数
-                sql = 'UPDATE users SET lepao_count = lepao_count + ?, ic_count = ic_count + ? WHERE uuid = ?'
-                const updateUser = await db.query(sql, [lepao_count, ic_count, create_user])
+                sql = 'UPDATE users SET lepao_count = lepao_count + ?, ic_count = ic_count + ?, vip = ? WHERE uuid = ?'
+                const updateUser = await db.query(sql, [lepao_count, ic_count, vip, create_user])
                 if (!updateUser || updateUser.affectedRows !== 1) {
                 if (!updateUser || updateUser.affectedRows !== 1) {
                     this.logger.error(`更新用户失败,UUID: ${create_user}`)
                     this.logger.error(`更新用户失败,UUID: ${create_user}`)
                     await db.query('UPDATE orders SET state = 4 WHERE orderId = ?', [out_trade_no])
                     await db.query('UPDATE orders SET state = 4 WHERE orderId = ?', [out_trade_no])

+ 2 - 1
apis/Order/GetOrderDetail.js

@@ -23,7 +23,8 @@ class GetAccount extends API {
                 a.pay_id,
                 a.pay_id,
                 a.pay_type,
                 a.pay_type,
                 a.pay_time,
                 a.pay_time,
-                g.name
+                g.name,
+                g.content
             FROM 
             FROM 
                 orders a
                 orders a
             LEFT JOIN 
             LEFT JOIN 

+ 4 - 4
apis/User/Register.js

@@ -4,7 +4,7 @@ const bcryptjs = require('bcryptjs');
 const { BaseStdResponse } = require("../../BaseStdResponse");
 const { BaseStdResponse } = require("../../BaseStdResponse");
 const db = require("../../plugin/DataBase/db");
 const db = require("../../plugin/DataBase/db");
 const Redis = require('../../plugin/DataBase/Redis');
 const Redis = require('../../plugin/DataBase/Redis');
-const sendEmail = require('../../plugin/Email/Email');
+const EmailTemplate = require('../../plugin/Email/emailTemplate')
 
 
 // 用户注册
 // 用户注册
 class Register extends API {
 class Register extends API {
@@ -45,7 +45,7 @@ class Register extends API {
         if(!this.checkUsername(username))
         if(!this.checkUsername(username))
             return res.json({
             return res.json({
                 ...BaseStdResponse.ERR,
                 ...BaseStdResponse.ERR,
-                msg: '用户名需在4到8位之间,且只能含有英文字母和汉字'
+                msg: '用户名需在4到12位之间,且不能含有特殊字符'
             })
             })
 
 
         password = atob(password);
         password = atob(password);
@@ -102,8 +102,8 @@ class Register extends API {
 
 
             res.json({
             res.json({
                 ...BaseStdResponse.OK
                 ...BaseStdResponse.OK
-            });
-            await sendEmail(email, '账号注册成功', `您已成功注册CTBU_CLUB账号,用户名${username},注册时间:${new Date().toLocaleString()}`);
+            })
+            await EmailTemplate.registerSuccess(email, username)
         } else {
         } else {
             res.json({ ...BaseStdResponse.ERR, endpoint: 7894378, msg: '注册失败!'});
             res.json({ ...BaseStdResponse.ERR, endpoint: 7894378, msg: '注册失败!'});
         }
         }

+ 1 - 0
config.json

@@ -22,6 +22,7 @@
     "url": "https://lepao-api.ctbu.top/",
     "url": "https://lepao-api.ctbu.top/",
     "url2": "http://127.0.0.1:30004",
     "url2": "http://127.0.0.1:30004",
     "url3": "http://127.0.0.1:30001",
     "url3": "http://127.0.0.1:30001",
+    "runpy": "http://8.137.37.202:58000/api",
     "pay": {
     "pay": {
         "url":"https://pay.ctbu.top/submit.php",
         "url":"https://pay.ctbu.top/submit.php",
         "pid": 1,
         "pid": 1,

+ 189 - 0
lib/Lepao/Lepao.js

@@ -0,0 +1,189 @@
+const axios = require('axios')
+const db = require('../../plugin/DataBase/db')
+const Logger = require('../Logger')
+const path = require('path')
+const EmailTemplate = require('../../plugin/Email/emailTemplate')
+const config = require('../../config.json')
+
+class Lepao {
+    constructor() {
+        this.logger = new Logger(path.join(__dirname, '../logs/Lepao.log'), 'INFO')
+        this.runpy = config.runpy
+    }
+
+    async getPath(account, vip) {
+        const accountSql = 'SELECT area, max_distance, min_distance FROM lepao_account WHERE student_num = ?'
+        const rows = await db.query(accountSql, [account])
+        if (!rows || rows.length !== 1) {
+            throw new Error('无法获取账号数据')
+        }
+
+        const { area, max_distance, min_distance } = rows[0]
+        const max = Number(max_distance) || 4.00
+        const min = Number(min_distance) || 1.60
+
+        if (vip !== 1) {
+            if (area) throw new Error('指定乐跑校区为 VIP 专用功能,请先开通 VIP')
+            if (max !== 4.00 || min !== 1.60) {
+                throw new Error('指定乐跑距离区间为 VIP 专用功能,请先开通 VIP')
+            }
+        }
+
+        let pathSql = 'SELECT id FROM path_data WHERE distance < ? AND distance > ?'
+        const pathParams = [max, min]
+
+        if (area) {
+            pathSql += ' AND run_zone_name = ?'
+            pathParams.push(area)
+        }
+
+        const paths = await db.query(pathSql, pathParams)
+        if (!paths || paths.length === 0) {
+            throw new Error('未找到符合条件的路线,请改变选择条件')
+        }
+
+        const randomPath = paths[Math.floor(Math.random() * paths.length)]
+        return randomPath.id
+    }
+
+    async beginLepao(uuid, account, token, uid, school_id) {
+        try {
+            const userPermissionSql = 'SELECT vip, lepao_count FROM users WHERE uuid = ?'
+            const userPermissionData = await db.query(userPermissionSql, [uuid])
+            if (!userPermissionData || userPermissionData.length !== 1)
+                throw new Error('无法获取用户信息')
+            if (userPermissionData[0].lepao_count < 1)
+                throw new Error('用户乐跑次数不足,请购买乐跑套餐!')
+
+            // 获取路径 ID
+            const path_id = await this.getPath(account, userPermissionData[0].vip)
+
+            // 上传 OSS
+            const ossUrl = this.runpy + '/upload_oss_file'
+            const ossData = { uid, token, school_id, student_id: account, random_id: path_id }
+            let oss_path
+
+            try {
+                const ossRes = await axios.post(ossUrl, ossData)
+                const { data } = ossRes
+                if (!data || data.code !== 200 || !data.oss_path) {
+                    throw new Error('请检查登录是否过期')
+                }
+                oss_path = data.oss_path
+            } catch (error) {
+                this.logger.error(`上传OSS记录失败,请检查登录是否过期。${error.stack || error.message}`)
+                throw new Error('请检查登录是否过期')
+            }
+
+            // 扣除乐跑次数
+            const useLepaoCountSql = 'UPDATE users SET lepao_count = lepao_count - 1 WHERE uuid  = ?'
+            await db.query(useLepaoCountSql, [uuid])
+
+            let run_end_time = Math.floor(Date.now() / 1000)
+            let hour = new Date().getHours()
+            if (hour >= 22) run_end_time -= 3600
+
+            const lepaoData = {
+                uid,
+                token,
+                school_id,
+                student_id: account,
+                random_id: path_id,
+                record_file: oss_path,
+                run_end_time
+            }
+
+            console.log(lepaoData)
+
+            const lepaoUrl = this.runpy + '/bind_data'
+            try {
+                const lepaoRes = await axios.post(lepaoUrl, lepaoData)
+                const { data } = lepaoRes
+
+                console.log(data)
+
+                if (!data || data.status !== 1 || !data.data) {
+                    throw new Error(data.info || '未知错误,请尝试重新登录')
+                }
+
+                await this.addRecord(account, data.data)
+                if (data.data.record_failed_reason === '') {
+                    await this.sendSuccessEmail(account, data)
+                } else {
+                    await this.sendFailEmail(account, data.data.record_failed_reason)
+                    await this.lepaoFail(uuid)
+                }
+
+            } catch (error) {
+                await this.lepaoFail(uuid)
+
+                throw error
+            }
+
+        } catch (error) {
+            await this.sendFailEmail(account, error.message || '未知错误,请尝试重新登录')
+        }
+    }
+
+    async addRecord(account, result) {
+        try {
+            const time = Date.now()
+            const sql = 'INSERT INTO lepao_record (time, lepao_account, result) VALUES (?, ?, ?)'
+            await db.query(sql, [time, account, result])
+        } catch (error) {
+            this.logger.error(error.stack || error.message)
+        }
+    }
+
+    async sendSuccessEmail(account, lepaoData) {
+        try {
+            const emailSql = 'SELECT name, email FROM lepao_account WHERE student_num = ?'
+            const rows = await db.query(emailSql, [account])
+            if (!rows || rows.length !== 1) {
+                throw new Error('查找用户邮箱失败')
+            }
+
+            const data = {
+                ...lepaoData,
+                name: rows[0].name,
+                account
+            }
+
+            await EmailTemplate.lepaoSuccess(rows[0].email, data)
+        } catch (error) {
+            this.logger.error(error.stack || error.message)
+        }
+    }
+
+    async sendFailEmail(account, reason) {
+        try {
+            const emailSql = 'SELECT name, email FROM lepao_account WHERE student_num = ?'
+            const rows = await db.query(emailSql, [account])
+            if (!rows || rows.length !== 1) {
+                throw new Error('查找用户邮箱失败')
+            }
+
+            const data = {
+                name: rows[0].name,
+                account,
+                reason
+            }
+
+            await EmailTemplate.lepaoFail(rows[0].email, data)
+        } catch (error) {
+            this.logger.error(error.stack || error.message)
+        }
+    }
+
+    async lepaoFail(uuid) {
+        try {
+            const sql = 'UPDATE users SET lepao_count = lepao_count + 1 WHERE uuid  = ?'
+            await db.query(sql, [uuid])
+        } catch (error) {
+            this.logger.error(`返还用户 ${uuid} 乐跑次数时出错: ${error.stack || error.message}`)
+        }
+    }
+}
+
+const lepao = new Lepao()
+module.exports.lepao = lepao

+ 2 - 2
plugin/Email/Email.js

@@ -18,10 +18,10 @@ const transporter = nodemailer.createTransport({
 async function sendEmail(email, subject, content) {
 async function sendEmail(email, subject, content) {
     return new Promise((resolve, reject) => {
     return new Promise((resolve, reject) => {
         const mail = {
         const mail = {
-            from: `CTBU_CLUB <${transporter.options.auth.user}>`,
+            from: `RunForge <${transporter.options.auth.user}>`,
             to: email,
             to: email,
             subject: subject,
             subject: subject,
-            text: content
+            html: content
         }
         }
         try {
         try {
             transporter.sendMail(mail, (error) => {
             transporter.sendMail(mail, (error) => {

+ 379 - 0
plugin/Email/emailTemplate.js

@@ -0,0 +1,379 @@
+const sendEmail = require('./Email')
+
+class emailTemplate {
+    stramptoTime(time) {
+        if (time < 10)
+            return '';
+        return new Date(+time).toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' });
+    }
+
+    async checkEmail(email, code) {
+        const time = new Date().getTime()
+        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;
+        }
+
+        .code {
+            margin: 20px 0;
+            font-size: 1.5em;
+            text-align: center;
+            font-weight: bold;
+        }
+
+        .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>您正在本站进行邮箱验证操作,如非您本人操作,请忽略此邮件。</p>
+        <p>您的验证码为:</p>
+        <div class="code">
+            ${code}
+        </div>
+        <p class="important">验证码5分钟内有效,超时请重新获取</p>
+        <p class="footer">Copyright © 2025 RunForge</p>
+    </div>
+</body>
+
+</html>`
+        )
+    }
+
+    async registerSuccess(email, username) {
+        const time = new Date().getTime()
+        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>尊敬的 ${username}:</p>
+        <p>您已成功注册RunForge账号:</p>
+        <div class="info">
+            <p><strong>用户名:</strong> ${username}</p>
+            <p><strong>注册时间:</strong> ${this.stramptoTime(time)}</p>
+        </div>
+
+        <p class="footer">Copyright © 2025 RunForge</p>
+    </div>
+</body>
+
+</html>`
+        )
+    }
+
+    async lepaoSuccess(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>尊敬的 ${data.name}:</p>
+                    <p>RunForge已成功为您进行乐跑:</p>
+                    <div class="info">
+                        <p><strong>学号:</strong> ${data.account}</p>
+                        <p><strong>跑区:</strong> ${data.pass_tit}</p>
+                        <p><strong>跑步距离:</strong> ${data.distance} Km</p>
+                        <p><strong>跑步时长:</strong> ${this.formatSecondsToMinSec(data.time)} Km</p>
+                        <p><strong>平均配速:</strong> ${this.calculatePace(data.time, data.distance)}</p>
+                        <p><strong>乐跑时间:</strong> ${this.stramptoTime(time)}</p>
+                    </div>
+
+                    <p class="important">请您开启了自动乐跑,请避免在其他设备上登录“智慧体育”小程序,否则将导致登录失效。</p>
+                    <p class="important">如有疑问请联系RunForge客服。</p>
+                    <p class="footer">Copyright © 2025 RunForge</p>
+                </div>
+            </body>
+
+            </html>`
+        )
+    }
+
+    async lepaoFail(email, data) {
+        const time = new Date().getTime()
+        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>尊敬的 ${data.name}:</p>
+                    <p>RunForge在尝试为您乐跑时遇到错误:</p>
+                    <div class="info">
+                        <p><strong>学号:</strong> ${data.account}</p>
+                        <p><strong>时间:</strong> ${this.stramptoTime(time)}</p>
+                        <p><strong>失败原因:</strong> ${data.reason}</p>
+                    </div>
+                    <p class="important">若登录失效请启动RunForge乐跑登录器重新登录“智慧体育”小程序。</p>
+                    <p class="important">如有疑问请联系RunForge客服。</p>
+                    <p class="footer">Copyright © 2025 RunForge</p>
+                </div>
+            </body>
+
+            </html>`
+        )
+    }
+
+    // 时长计算
+    formatSecondsToMinSec(totalSeconds) {
+        const minutes = Math.floor(totalSeconds / 60);
+        const seconds = totalSeconds % 60;
+
+        return `${minutes}分${seconds.toString().padStart(2, '0')}秒`;
+    }
+
+    // 配速计算
+    calculatePace(seconds, kilometers) {
+        const paceInSeconds = seconds / kilometers;
+        const minutes = Math.floor(paceInSeconds / 60);
+        const remainingSeconds = Math.round(paceInSeconds % 60);
+
+        return `${minutes}'${remainingSeconds.toString().padStart(2, '0')}''`;
+    }
+}
+
+const EmailTemplate = new emailTemplate()
+module.exports = EmailTemplate