Browse Source

✨ feat: 增加售后服务功能

Pchen. 9 months ago
parent
commit
42bb27c7fe

+ 108 - 0
apis/Kefu/Order/Admin/GetOrderDetail.js

@@ -0,0 +1,108 @@
+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 GetOrderDetail extends API {
+    constructor() {
+        super();
+
+        this.setPath('/Admin/Kefu/OrderDetail')
+        this.setMethod('GET')
+    }
+
+    async onRequest(req, res) {
+        let { uuid, session, id } = req.query
+
+        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 permission = await AccessControl.getPermission(uuid)
+        if (!permission.includes("admin") && !permission.includes("service"))
+            return res.json({
+                ...BaseStdResponse.PERMISSION_DENIED
+            })
+
+        let sql = `
+                    SELECT 
+                        id,
+                        create_user,
+                        title,
+                        msg,
+                        state,
+                        email,
+                        create_time,
+                        update_time
+                    FROM 
+                        work_order
+                    WHERE 
+                        id = ?
+                `
+
+        let rows = await db.query(sql, [id])
+
+        if (!rows || rows.length !== 1) {
+            return res.json({
+                ...BaseStdResponse.DATABASE_ERR,
+                msg: '工单异常'
+            })
+        }
+
+        let order = rows[0]
+        let uuidSet = new Set()
+
+        order.msg.forEach(msg => {
+            if (msg.uuid) uuidSet.add(msg.uuid)
+        })
+
+        let uuidList = [...uuidSet]
+
+        if (uuidList.length === 0) {
+            return res.json({
+                ...BaseStdResponse.SUCCESS,
+                data: {
+                    ...order,
+                    userInfo: {}
+                }
+            })
+        }
+
+        // 合并查询
+        const placeholders = uuidList.map(() => '?').join(', ')
+        const userSql = `SELECT uuid, username, avatar FROM users WHERE uuid IN (${placeholders})`
+        const userRows = await db.query(userSql, uuidList)
+
+        if (!userRows || userRows.length === 0) {
+            return res.json({
+                ...BaseStdResponse.DATABASE_ERR,
+                msg: '用户信息获取失败'
+            })
+        }
+
+        let userInfo = {}
+        userRows.forEach(user => {
+            userInfo[user.uuid] = {
+                username: user.username,
+                avatar: user.avatar
+            }
+        })
+
+        res.json({
+            ...BaseStdResponse.OK,
+            data: {
+                ...order,
+                userInfo
+            }
+        })
+    }
+}
+
+module.exports.GetOrderDetail = GetOrderDetail

+ 125 - 0
apis/Kefu/Order/Admin/GetOrderList.js

@@ -0,0 +1,125 @@
+const API = require("../../../../lib/API")
+const db = require("../../../../plugin/DataBase/db")
+const AccessControl = require("../../../../lib/AccessControl")
+const { BaseStdResponse } = require("../../../../BaseStdResponse")
+
+class GetOrderList extends API {
+    constructor() {
+        super();
+
+        this.setPath('/Admin/Kefu/Order')
+        this.setMethod('get')
+    }
+
+    async onRequest(req, res) {
+        let { uuid, session, id, state, pagesize, current } = req.query
+
+        if ([uuid, session, pagesize, current].some(value => value === '' || value === null || value === undefined))
+            return res.json({
+                ...BaseStdResponse.MISSING_PARAMETER
+            })
+
+        // 校验分页参数
+        if (isNaN(pagesize) || pagesize <= 0 || pagesize > 50) {
+            return res.json({
+                ...BaseStdResponse.ERR,
+                msg: '参数错误'
+            })
+        }
+
+        if (isNaN(current) || current <= 0) {
+            return res.json({
+                ...BaseStdResponse.ERR,
+                msg: '参数错误'
+            })
+        }
+
+        // 检查 session
+        if (!await AccessControl.checkSession(uuid, session))
+            return res.status(401).json({
+                ...BaseStdResponse.ACCESS_DENIED
+            })
+
+        // 检查权限
+        let permission = await AccessControl.getPermission(uuid)
+        if (!permission.includes("admin") && !permission.includes("service"))
+            return res.json({
+                ...BaseStdResponse.PERMISSION_DENIED
+            })
+
+        // 计算分页的 offset
+        const offset = (current - 1) * pagesize
+
+        let sql = `
+            SELECT 
+                a.id,
+                a.title,
+                a.state,
+                a.create_time,
+                a.update_time,
+                u.username,
+                u.avatar
+            FROM 
+                work_order a
+            LEFT JOIN 
+                users u 
+            ON 
+                a.create_user = u.uuid
+            WHERE 
+                1 = 1
+        `
+
+        let countSql = `
+            SELECT COUNT(*) AS total
+            FROM work_order
+            WHERE 1 = 1
+        `
+
+        let params = []
+        let countParams = []
+
+        if (id) {
+            sql += ` AND a.id = ?`
+            countSql += ` AND id = ?`
+            params.push(id)
+            countParams.push(id)
+        }
+
+        if (state !== -1) {
+            sql += ` AND a.state = ?`
+            countSql += ` AND  state = ?`
+            params.push(state)
+            countParams.push(state)
+        }
+
+        sql += `
+            ORDER BY a.update_time DESC
+            LIMIT ? OFFSET ?
+            
+        `
+        params.push(String(pagesize), String(offset))
+
+        let rows = await db.query(sql, params)
+        let countResult = await db.query(countSql, countParams)
+
+        if (!rows || !countResult)
+            return res.json({
+                ...BaseStdResponse.MISSING_FILE,
+                msg: '获取工单数据失败!'
+            })
+
+        let total = countResult[0].total
+
+        res.json({
+            ...BaseStdResponse.OK,
+            data: rows,
+            pagination: {
+                current,
+                pagesize,
+                total
+            }
+        })
+    }
+}
+
+module.exports.GetOrderList = GetOrderList

+ 76 - 0
apis/Kefu/Order/Admin/ReplyOrder.js

@@ -0,0 +1,76 @@
+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 EmailTemplate = require('../../../../plugin/Email/emailTemplate')
+
+class ReplyOrder extends API {
+    constructor() {
+        super();
+
+        this.setPath('/Admin/Kefu/Order')
+        this.setMethod('PUT')
+    }
+
+    async onRequest(req, res) {
+        let { uuid, session, id, content, files } = req.body
+
+        if ([uuid, session, id, content].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 msg = []
+        let message = {
+            time,
+            content,
+            files: files ?? [],
+            uuid,
+            type: 'server'
+        }
+
+        const selectSql = 'SELECT msg, state, email FROM work_order WHERE id = ?'
+        const selectRows = await db.query(selectSql, [id])
+        if (!selectRows || selectRows.length !== 1 || selectRows[0].state === 2)
+            return res.json({
+                ...BaseStdResponse.ERR,
+                msg: '工单异常或已关闭'
+            })
+
+        msg = selectRows[0].msg
+        msg.push(message)
+
+        let sql = 'UPDATE work_order SET msg = ?, update_time = ?, state = 0 WHERE id = ?'
+        let r = await db.query(sql, [msg, time, id])
+
+        try {
+            if (r && r.affectedRows > 0) {
+                res.json({
+                    ...BaseStdResponse.OK,
+                    data: r.insertId
+                })
+
+                if(selectRows[0].email) {
+                    await EmailTemplate.lepaoSuccess(selectRows[0].email, {id, content, files})
+                }
+            } else {
+                return res.json({ ...BaseStdResponse.ERR, msg: '回复工单失败!数据库错误' })
+            }
+        } catch (err) {
+            this.logger.error(`回复工单失败!${err.stack}`)
+            res.json({
+                ...BaseStdResponse.ERR,
+                msg: "回复工单失败!",
+            })
+        }
+    }
+}
+
+module.exports.ReplyOrder = ReplyOrder

+ 76 - 0
apis/Kefu/Order/CloseOrder.js

@@ -0,0 +1,76 @@
+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 CloseOrder extends API {
+    constructor() {
+        super();
+
+        this.setPath('/Kefu/Order')
+        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
+            })
+
+        if (!await AccessControl.checkSession(uuid, session))
+            return res.status(401).json({
+                ...BaseStdResponse.ACCESS_DENIED
+            })
+
+        const time = new Date().getTime()
+
+        let msg = []
+        const systemMsg = {
+            time,
+            content: '当前工单已关闭,若仍有疑问可提交新工单哦~~',
+            uuid: 'e4fe0277-0b1a-41a1-b25f-8b6e4cec3281',
+            type: 'system'
+        }
+
+        const selectSql = 'SELECT msg, state, create_user FROM work_order WHERE id = ? AND create_user = ?'
+        const selectRows = await db.query(selectSql, [id, uuid])
+        if (!selectRows || selectRows.length !== 1 || selectRows[0].state === 2)
+            return res.json({
+                ...BaseStdResponse.ERR,
+                msg: '工单异常或已关闭,请提交新工单'
+            })
+
+        if(selectRows[0].create_user !== uuid && !permission.includes("admin") && !permission.includes("service"))
+            return res.json({
+                ...BaseStdResponse.PERMISSION_DENIED,
+                msg: '无操作权限'
+            })
+
+        msg = selectRows[0].msg
+        msg.push(systemMsg)
+
+        let sql = 'UPDATE work_order SET msg = ?, update_time = ?, state = 2 WHERE id = ? AND create_user = ?'
+        let r = await db.query(sql, [msg, time, id, uuid])
+
+        try {
+            if (r && r.affectedRows > 0) {
+                res.json({
+                    ...BaseStdResponse.OK,
+                    data: r.insertId
+                })
+            } else {
+                return res.json({ ...BaseStdResponse.ERR, msg: '关闭工单失败!数据库错误' })
+            }
+        } catch (err) {
+            this.logger.error(`关闭工单失败!${err.stack}`)
+            res.json({
+                ...BaseStdResponse.ERR,
+                msg: "关闭工单失败!",
+            })
+        }
+    }
+}
+
+module.exports.CloseOrder = CloseOrder

+ 96 - 0
apis/Kefu/Order/CreateOrder.js

@@ -0,0 +1,96 @@
+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 CreateOrder extends API {
+    constructor() {
+        super();
+
+        this.setPath('/Kefu/Order')
+        this.setMethod('POST')
+    }
+
+    async onRequest(req, res) {
+        let { uuid, session, id, title, content, files, email } = req.body
+
+        if ([uuid, session, content].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 msg = []
+        let message = {
+            time,
+            content,
+            files: files ?? [],
+            uuid,
+            type: 'user'
+        }
+
+        let sql, r
+
+        if (!id) {
+            if ([title].some(value => value === '' || value === null || value === undefined))
+                return res.json({
+                    ...BaseStdResponse.MISSING_PARAMETER
+                })
+
+            msg.push(message)
+            const systemMsg = {
+                time,
+                content: '您的问题已收到,我们将会尽快处理您的问题,请耐心等待,感谢您的支持理解。',
+                uuid: 'e4fe0277-0b1a-41a1-b25f-8b6e4cec3281',
+                type: 'system'
+            }
+            msg.push(systemMsg)
+
+            sql = 'INSERT INTO work_order (title, email, msg, create_user, create_time, update_time) VALUES (?, ?, ?, ?, ?, ?)'
+            r = await db.query(sql, [title, email, msg, uuid, time, time])
+        } else {
+            const selectSql = 'SELECT msg, state, email FROM work_order WHERE id = ? AND create_user = ?'
+            const selectRows = await db.query(selectSql, [id, uuid])
+            if (!selectRows || selectRows.length !== 1 || selectRows[0].state === 2)
+                return res.json({
+                    ...BaseStdResponse.ERR,
+                    msg: '工单异常或已关闭,请提交新工单'
+                })
+
+            msg = selectRows[0].msg
+            msg.push(message)
+
+            sql = 'UPDATE work_order SET msg = ?, update_time = ?, state = 0 WHERE id = ? AND create_user = ?'
+            r = await db.query(sql, [msg, time, id, uuid])
+        }
+
+        try {
+            if (r && r.affectedRows > 0) {
+                res.json({
+                    ...BaseStdResponse.OK,
+                    data: r.insertId
+                })
+
+                // 完善功能:发送提醒邮件
+
+
+            } else {
+                return res.json({ ...BaseStdResponse.ERR, msg: '提交工单失败!数据库错误' })
+            }
+        } catch (err) {
+            this.logger.error(`提交工单失败!${err.stack}`)
+            res.json({
+                ...BaseStdResponse.ERR,
+                msg: "提交工单失败!",
+            })
+        }
+    }
+}
+
+module.exports.CreateOrder = CreateOrder

+ 100 - 0
apis/Kefu/Order/GetOrderDetail.js

@@ -0,0 +1,100 @@
+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 GetOrderDetail extends API {
+    constructor() {
+        super();
+
+        this.setPath('/Kefu/OrderDetail')
+        this.setMethod('GET')
+    }
+
+    async onRequest(req, res) {
+        let { uuid, session, id } = req.query
+
+        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 
+                        id,
+                        title,
+                        msg,
+                        state,
+                        email,
+                        create_time,
+                        update_time
+                    FROM 
+                        work_order
+                    WHERE 
+                        create_user = ? AND id = ?
+                `
+
+        let rows = await db.query(sql, [uuid, id])
+
+        if (!rows || rows.length !== 1) {
+            return res.json({
+                ...BaseStdResponse.DATABASE_ERR,
+                msg: '工单异常,请提交新工单'
+            })
+        }
+
+        let order = rows[0]
+        let uuidSet = new Set()
+
+        order.msg.forEach(msg => {
+            if (msg.uuid) uuidSet.add(msg.uuid)
+        })
+
+        let uuidList = [...uuidSet]
+
+        if (uuidList.length === 0) {
+            return res.json({
+                ...BaseStdResponse.SUCCESS,
+                data: {
+                    ...order,
+                    userInfo: {}
+                }
+            })
+        }
+
+        // 合并查询
+        const placeholders = uuidList.map(() => '?').join(', ')
+        const userSql = `SELECT uuid, username, avatar FROM users WHERE uuid IN (${placeholders})`
+        const userRows = await db.query(userSql, uuidList)
+
+        if (!userRows || userRows.length === 0) {
+            return res.json({
+                ...BaseStdResponse.DATABASE_ERR,
+                msg: '用户信息获取失败'
+            })
+        }
+
+        let userInfo = {}
+        userRows.forEach(user => {
+            userInfo[user.uuid] = {
+                username: user.username,
+                avatar: user.avatar
+            }
+        })
+
+        res.json({
+            ...BaseStdResponse.OK,
+            data: {
+                ...order,
+                userInfo
+            }
+        })
+    }
+}
+
+module.exports.GetOrderDetail = GetOrderDetail

+ 88 - 0
apis/Kefu/Order/GetOrderList.js

@@ -0,0 +1,88 @@
+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 GetOrderList extends API {
+    constructor() {
+        super();
+
+        this.setPath('/Kefu/Order')
+        this.setMethod('GET')
+    }
+
+    async onRequest(req, res) {
+        let { uuid, session, pagesize, current } = req.query
+
+        if ([uuid, session, pagesize, current].some(value => value === '' || value === null || value === undefined))
+            return res.json({
+                ...BaseStdResponse.MISSING_PARAMETER
+            })
+
+        // 校验分页参数
+        if (isNaN(pagesize) || pagesize <= 0 || pagesize > 50) {
+            return res.json({
+                ...BaseStdResponse.ERR,
+                msg: '参数错误'
+            })
+        }
+
+        if (isNaN(current) || current <= 0) {
+            return res.json({
+                ...BaseStdResponse.ERR,
+                msg: '参数错误'
+            })
+        }
+
+        if (!await AccessControl.checkSession(uuid, session))
+            return res.status(401).json({
+                ...BaseStdResponse.ACCESS_DENIED
+            })
+
+        // 计算分页的 offset
+        const offset = (current - 1) * pagesize
+
+        let sql = `
+                SELECT 
+                    id,
+                    title,
+                    state,
+                    email,
+                    create_time,
+                    update_time
+                FROM 
+                    work_order
+                WHERE 
+                    create_user = ?
+                ORDER BY update_time DESC
+                LIMIT ? OFFSET ?
+            `
+        let countSql = `
+            SELECT COUNT(*) AS total
+            FROM work_order
+            WHERE create_user = ?
+        `
+
+        let rows = await db.query(sql, [uuid, String(pagesize), String(offset)])
+        let countResult = await db.query(countSql, [uuid])
+
+        if (!rows || !countResult)
+            return res.json({
+                ...BaseStdResponse.DATABASE_ERR
+            })
+
+        let total = countResult[0].total
+
+        res.json({
+            ...BaseStdResponse.OK,
+            data: rows,
+            pagination: {
+                current,
+                pagesize,
+                total
+            }
+        })
+    }
+}
+
+module.exports.GetOrderList = GetOrderList

+ 87 - 1
plugin/Email/emailTemplate.js

@@ -267,7 +267,7 @@ class emailTemplate {
         )
         )
     }
     }
 
 
-        async lepaoOver(email, data) {
+    async lepaoOver(email, data) {
         await sendEmail(email, '🎉 RunForge - 乐跑完成祝贺',
         await sendEmail(email, '🎉 RunForge - 乐跑完成祝贺',
             `<!DOCTYPE html>
             `<!DOCTYPE html>
                 <html lang="zh-CN">
                 <html lang="zh-CN">
@@ -551,6 +551,92 @@ class emailTemplate {
         )
         )
     }
     }
 
 
+    async orderNewReply(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.id}的工单有新的回复:</p>
+                    <div class="info">
+                        <p><strong>回复内容:</strong> ${data.content}</p>
+                        <p><strong>附件数量:</strong> ${data.files.length}</p>
+                        <p><strong>回复时间:</strong> ${this.stramptoTime(new Date().getTime())}</p>
+                    </div>
+                    <p class="important">请前往RunForge官网回复工单,请勿直接回复邮件。</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);