Browse Source

✨ feat: 增加文档、下载中心

Pchen. 1 week ago
parent
commit
5edf4bf603

+ 41 - 54
apis/Article/Admin/GetArticle.js

@@ -3,72 +3,59 @@ const db = require("../../../plugin/DataBase/db");
 const AccessControl = require("../../../lib/AccessControl");
 const { BaseStdResponse } = require("../../../BaseStdResponse");
 
-// 管理后台获取文章内容
 class GetArticle extends API {
     constructor() {
         super();
-
-        this.setPath('/Admin/Article')
-        this.setMethod('GET')
+        this.setPath('/Admin/Article');
+        this.setMethod('GET');
     }
 
     async onRequest(req, res) {
-        let { uuid, session, id } = req.query
+        let { uuid, session, id } = req.query;
 
         if ([uuid, session, id].some(value => value === '' || value === null || value === undefined))
-            return res.json({
-                ...BaseStdResponse.MISSING_PARAMETER
-            })
+            return res.json({ ...BaseStdResponse.MISSING_PARAMETER });
 
-        // 检查 session
         if (!await AccessControl.checkSession(uuid, session))
-            return res.status(401).json({
-                ...BaseStdResponse.ACCESS_DENIED
-            })
+            return res.status(401).json({ ...BaseStdResponse.ACCESS_DENIED });
 
-        // 检查权限
-        let permission = await AccessControl.getPermission(uuid)
+        const permission = await AccessControl.getPermission(uuid);
         if (!permission.includes("admin") && !permission.includes("article"))
-            return res.json({
-                ...BaseStdResponse.PERMISSION_DENIED
-            })
-
-        let sql = `
-                SELECT 
-                    a.id,
-                    a.title,
-                    a.cover,
-                    a.describe,
-                    a.content,
-                    a.type,
-                    a.state,
-                    a.views,
-                    a.time,
-                    u.username AS author
-                FROM 
-                    article a
-                LEFT JOIN 
-                    users u 
-                ON 
-                    a.author = u.uuid
-                WHERE 
-                    a.id = ?
-            `
-
-        let rows = await db.query(sql, [id])
-
-        if (!rows || rows.length !== 1)
-            return res.json({
-                ...BaseStdResponse.MISSING_FILE,
-                msg: '文章不存在'
-            })
-
-        res.json({
-            ...BaseStdResponse.OK,
-            data: rows
-        })
+            return res.json({ ...BaseStdResponse.PERMISSION_DENIED });
+
+        const sql = `
+            SELECT 
+                a.id,
+                a.slug,
+                a.title,
+                a.cover,
+                a.describe,
+                a.content,
+                a.type,
+                a.state,
+                a.views,
+                a.time,
+                u.username AS author
+            FROM article a
+            LEFT JOIN users u ON a.author = u.uuid
+            WHERE a.id = ?
+        `;
+
+        try {
+            const rows = await db.query(sql, [id]);
+
+            if (!rows || rows.length !== 1)
+                return res.json({ ...BaseStdResponse.MISSING_FILE, msg: '文章不存在' });
+
+            res.json({
+                ...BaseStdResponse.OK,
+                data: rows[0]
+            });
+        } catch (err) {
+            this.logger.error(`获取文章失败!${err.stack}`);
+            res.json({ ...BaseStdResponse.ERR, msg: '获取文章失败!' });
+        }
     }
-
 }
 
-module.exports.GetArticle = GetArticle;
+module.exports.GetArticle = GetArticle;

+ 45 - 68
apis/Article/Admin/GetArticleList.js

@@ -6,117 +6,94 @@ const { BaseStdResponse } = require("../../../BaseStdResponse");
 class GetArticleList extends API {
     constructor() {
         super();
-
-        this.setPath('/Admin/Article/List')
-        this.setMethod('GET')
+        this.setPath('/Admin/Article/List');
+        this.setMethod('GET');
     }
 
     async onRequest(req, res) {
-        let { uuid, session, pagesize, current, type, state } = req.query
+        let { uuid, session, pagesize, current, type, state } = req.query;
 
         if ([uuid, session, pagesize, current, type, state].some(value => value === '' || value === null || value === undefined)) {
             return res.json({
                 ...BaseStdResponse.MISSING_PARAMETER,
                 endpoint: 1513126
-            })
+            });
         }
 
-        // 检查 session
         if (!await AccessControl.checkSession(uuid, session))
-            return res.status(401).json({
-                ...BaseStdResponse.ACCESS_DENIED
-            })
+            return res.status(401).json({ ...BaseStdResponse.ACCESS_DENIED });
 
-        // 检查权限
-        let permission = await AccessControl.getPermission(uuid)
+        const permission = await AccessControl.getPermission(uuid);
         if (!permission.includes("admin") && !permission.includes("article"))
-            return res.json({
-                ...BaseStdResponse.PERMISSION_DENIED
-            })
+            return res.json({ ...BaseStdResponse.PERMISSION_DENIED });
 
-        // 转换参数类型
-        pagesize = parseInt(pagesize, 10)
-        current = parseInt(current, 10)
+        pagesize = parseInt(pagesize, 10);
+        current = parseInt(current, 10);
+        state = parseInt(state, 10);
 
-        // 校验分页参数
-        if (isNaN(pagesize) || pagesize <= 0) {
-            return res.json({
-                ...BaseStdResponse.ERR,
-                msg: '参数错误'
-            })
-        }
-
-        if (isNaN(current) || current <= 0) {
-            return res.json({
-                ...BaseStdResponse.ERR,
-                msg: '参数错误'
-            })
+        if (isNaN(pagesize) || pagesize <= 0 || isNaN(current) || current <= 0) {
+            return res.json({ ...BaseStdResponse.ERR, msg: '参数错误' });
         }
 
-        // 计算分页的 offset
-        const offset = (current - 1) * pagesize
+        const offset = (current - 1) * pagesize;
+        const filterAllTypes = type === 'all';
 
         let sql = `
             SELECT 
                 a.id,
+                a.slug,
                 a.title,
                 a.cover,
-                a.describe,
+                a.\`describe\`,
                 a.type,
                 a.time,
                 a.views,
                 a.state,
                 u.username AS author
-            FROM 
-                article a
-            LEFT JOIN 
-                users u 
-            ON 
-                a.author = u.uuid
-            WHERE 
-                a.type = ?
-                AND a.state = ?
-            ORDER BY 
-                a.id DESC
-        `
-
-        // 查询文章总数,方便返回总页数
-        let countSql = `
-            SELECT COUNT(*) AS total
             FROM article a
-            WHERE a.type = ?;
-        `
+            LEFT JOIN users u ON a.author = u.uuid
+            WHERE a.state = ?
+        `;
+        const params = [state];
+
+        if (!filterAllTypes) {
+            sql += ' AND a.type = ?';
+            params.push(type);
+        }
 
-        try {
-            // 获取文章列表
-            let articles = await db.query(sql, [type, state])
+        sql += ' ORDER BY a.id DESC LIMIT ? OFFSET ?';
+        params.push(String(pagesize), String(offset));
 
-            // 获取总记录数
-            let countResult = await db.query(countSql, [type])
-            let total = countResult[0].total;
+        let countSql = 'SELECT COUNT(*) AS total FROM article a WHERE a.state = ?';
+        const countParams = [state];
+        if (!filterAllTypes) {
+            countSql += ' AND a.type = ?';
+            countParams.push(type);
+        }
 
-            // 计算总页数
-            const totalPages = Math.ceil(total / pagesize)
+        try {
+            const articles = await db.query(sql, params);
+            const countResult = await db.query(countSql, countParams);
+            const total = countResult[0].total;
+            const totalPages = Math.ceil(total / pagesize);
 
-            // 返回结果
             res.json({
                 ...BaseStdResponse.OK,
                 data: articles || [],
                 pagination: {
-                    current: current,
-                    pagesize: pagesize,
-                    total: total,
-                    totalPages: totalPages
+                    current,
+                    pagesize,
+                    total,
+                    totalPages
                 }
-            })
-
+            });
         } catch (err) {
-            this.logger.error(`获取文章列表失败!${err.stack}`)
+            this.logger.error(`获取文章列表失败!${err.stack}`);
             res.json({
                 ...BaseStdResponse.ERR,
                 msg: '获取文章列表失败!',
                 endpoint: 153127
-            })
+            });
         }
     }
 }

+ 41 - 41
apis/Article/Admin/PostArticle.js

@@ -2,76 +2,76 @@ const API = require("../../../lib/API");
 const db = require("../../../plugin/DataBase/db");
 const AccessControl = require("../../../lib/AccessControl");
 const { BaseStdResponse } = require("../../../BaseStdResponse");
+const { slugify, isValidSlug, ensureUniqueSlug } = require("../../../lib/slugify");
 
-// 发布/修改文章
 class PostArticle extends API {
     constructor() {
-        super()
-
-        this.setPath('/Admin/Article')
-        this.setMethod('POST')
+        super();
+        this.setPath('/Admin/Article');
+        this.setMethod('POST');
     }
 
     async onRequest(req, res) {
-
         let {
             uuid,
             session,
             id,
             title,
+            slug,
             cover,
             describe,
             content,
             type,
             state
-        } = req.body
+        } = req.body;
 
         if ([uuid, session, title, cover, content, type].some(value => value === '' || value === null || value === undefined))
-            return res.json({
-                ...BaseStdResponse.MISSING_PARAMETER
-            })
+            return res.json({ ...BaseStdResponse.MISSING_PARAMETER });
 
-        // 检查 session
         if (!await AccessControl.checkSession(uuid, session))
-            return res.status(401).json({
-                ...BaseStdResponse.ACCESS_DENIED
-            })
+            return res.status(401).json({ ...BaseStdResponse.ACCESS_DENIED });
 
-        // 检查权限
-        let permission = await AccessControl.getPermission(uuid)
+        const permission = await AccessControl.getPermission(uuid);
         if (!permission.includes("admin") && !permission.includes("article"))
-            return res.json({
-                ...BaseStdResponse.PERMISSION_DENIED
-            })
+            return res.json({ ...BaseStdResponse.PERMISSION_DENIED });
 
-        const time = new Date().getTime()
-        let sql, r
-        if (!id) {
-            sql = 'INSERT INTO article (title, cover, time, content, author, state, \`describe\`, type) VALUES (?, ?, ?, ?, ?, ?, ?, ?)'
-            r = await db.query(sql, [title, cover, time, content, uuid, state, describe, type])
-        } else {
-            sql = 'UPDATE article SET title = ?, cover = ?, content = ?, state = ?, \`describe\` = ?, type = ? WHERE id = ?'
-            r = await db.query(sql, [title, cover, content, state, describe, type, id])
-        }
+        const time = new Date().getTime();
 
         try {
+            if (!id) {
+                let baseSlug = slug ? String(slug).trim().toLowerCase() : slugify(title);
+                if (!isValidSlug(baseSlug))
+                    return res.json({ ...BaseStdResponse.ERR, msg: '文章标识格式无效(3-64位小写字母、数字、连字符)' });
+
+                const finalSlug = await ensureUniqueSlug(db, baseSlug);
+                const articleCover = (cover && String(cover).trim()) ? String(cover).trim().slice(0, 16) : '📄';
+
+                const sql = 'INSERT INTO article (title, slug, cover, time, content, author, state, `describe`, type) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)';
+                const r = await db.query(sql, [title, finalSlug, articleCover, time, content, uuid, state, describe, type]);
+
+                if (r && r.affectedRows > 0) {
+                    return res.json({ ...BaseStdResponse.OK, id: r.insertId, slug: finalSlug });
+                }
+                return res.json({ ...BaseStdResponse.ERR, endpoint: 7894378, msg: '发表文章失败!数据库错误' });
+            }
+
+            const existing = await db.query('SELECT slug FROM article WHERE id = ?', [id]);
+            if (!existing || existing.length === 0)
+                return res.json({ ...BaseStdResponse.ERR, msg: '文章不存在' });
+
+            const articleCover = (cover && String(cover).trim()) ? String(cover).trim().slice(0, 16) : '📄';
+            const sql = 'UPDATE article SET title = ?, cover = ?, content = ?, state = ?, `describe` = ?, type = ? WHERE id = ?';
+            const r = await db.query(sql, [title, articleCover, content, state, describe, type, id]);
+
             if (r && r.affectedRows > 0) {
-                res.json({
-                    ...BaseStdResponse.OK,
-                    id: r.insertId
-                })
-            } else {
-                res.json({ ...BaseStdResponse.ERR, endpoint: 7894378, msg: '发表文章失败!数据库错误' })
+                return res.json({ ...BaseStdResponse.OK, slug: existing[0].slug });
             }
+            return res.json({ ...BaseStdResponse.ERR, endpoint: 7894378, msg: '发表文章失败!数据库错误' });
         } catch (err) {
-            this.logger.error(`发表文章失败!${err.stack}`)
-            res.json({
-                ...BaseStdResponse.ERR,
-                msg: "发表文章失败!",
-            });
+            this.logger.error(`发表文章失败!${err.stack}`);
+            res.json({ ...BaseStdResponse.ERR, msg: "发表文章失败!" });
         }
     }
-
 }
 
-module.exports.PostArticle = PostArticle;
+module.exports.PostArticle = PostArticle;

+ 45 - 0
apis/Article/Category/Admin/Delete.js

@@ -0,0 +1,45 @@
+const API = require("../../../../lib/API");
+const db = require("../../../../plugin/DataBase/db");
+const AccessControl = require("../../../../lib/AccessControl");
+const { BaseStdResponse } = require("../../../../BaseStdResponse");
+
+class AdminArticleCategoryDelete extends API {
+    constructor() {
+        super();
+        this.setPath('/Admin/Article/Category');
+        this.setMethod('DELETE');
+    }
+
+    async onRequest(req, res) {
+        const { uuid, session, id } = req.body;
+        if ([uuid, session, id].some(v => v === '' || v === null || v === undefined))
+            return res.json({ ...BaseStdResponse.MISSING_PARAMETER });
+
+        if (!await AccessControl.checkSession(uuid, session))
+            return res.status(401).json({ ...BaseStdResponse.ACCESS_DENIED });
+
+        const permission = await AccessControl.getPermission(uuid);
+        if (!permission.includes('admin') && !permission.includes('article'))
+            return res.json({ ...BaseStdResponse.PERMISSION_DENIED });
+
+        try {
+            const cat = await db.query('SELECT slug FROM article_category WHERE id = ?', [id]);
+            if (!cat || cat.length === 0)
+                return res.json({ ...BaseStdResponse.ERR, msg: '分类不存在' });
+
+            const count = await db.query('SELECT COUNT(*) AS total FROM article WHERE type = ?', [cat[0].slug]);
+            if (count && count[0].total > 0)
+                return res.json({ ...BaseStdResponse.ERR, msg: '该分类下仍有文章,无法删除' });
+
+            const r = await db.query('DELETE FROM article_category WHERE id = ?', [id]);
+            if (!r || r.affectedRows === 0)
+                return res.json({ ...BaseStdResponse.ERR, msg: '删除分类失败' });
+            return res.json({ ...BaseStdResponse.OK });
+        } catch (err) {
+            this.logger.error(`删除文章分类失败!${err.stack}`);
+            res.json({ ...BaseStdResponse.ERR, msg: '删除文章分类失败!' });
+        }
+    }
+}
+
+module.exports.AdminArticleCategoryDelete = AdminArticleCategoryDelete;

+ 40 - 0
apis/Article/Category/Admin/List.js

@@ -0,0 +1,40 @@
+const API = require("../../../../lib/API");
+const db = require("../../../../plugin/DataBase/db");
+const AccessControl = require("../../../../lib/AccessControl");
+const { BaseStdResponse } = require("../../../../BaseStdResponse");
+
+class AdminArticleCategoryList extends API {
+    constructor() {
+        super();
+        this.setPath('/Admin/Article/Category/List');
+        this.setMethod('GET');
+    }
+
+    async onRequest(req, res) {
+        const { uuid, session } = req.query;
+        if ([uuid, session].some(v => v === '' || v === null || v === undefined))
+            return res.json({ ...BaseStdResponse.MISSING_PARAMETER });
+
+        if (!await AccessControl.checkSession(uuid, session))
+            return res.status(401).json({ ...BaseStdResponse.ACCESS_DENIED });
+
+        const permission = await AccessControl.getPermission(uuid);
+        if (!permission.includes('admin') && !permission.includes('article'))
+            return res.json({ ...BaseStdResponse.PERMISSION_DENIED });
+
+        const sql = `
+            SELECT id, name, slug, icon, sort_order, is_active, created_at
+            FROM article_category
+            ORDER BY sort_order ASC, id ASC
+        `;
+        try {
+            const rows = await db.query(sql);
+            res.json({ ...BaseStdResponse.OK, data: rows || [] });
+        } catch (err) {
+            this.logger.error(`获取文章分类列表失败!${err.stack}`);
+            res.json({ ...BaseStdResponse.ERR, msg: '获取文章分类列表失败!' });
+        }
+    }
+}
+
+module.exports.AdminArticleCategoryList = AdminArticleCategoryList;

+ 76 - 0
apis/Article/Category/Admin/Upsert.js

@@ -0,0 +1,76 @@
+const API = require("../../../../lib/API");
+const db = require("../../../../plugin/DataBase/db");
+const AccessControl = require("../../../../lib/AccessControl");
+const { BaseStdResponse } = require("../../../../BaseStdResponse");
+const { isValidSlug } = require("../../../../lib/slugify");
+
+class AdminArticleCategoryUpsert extends API {
+    constructor() {
+        super();
+        this.setPath('/Admin/Article/Category');
+        this.setMethod('POST');
+    }
+
+    async onRequest(req, res) {
+        const { uuid, session, id, name, slug, icon, sort_order, is_active } = req.body;
+        if ([uuid, session, name, slug].some(v => v === '' || v === null || v === undefined))
+            return res.json({ ...BaseStdResponse.MISSING_PARAMETER });
+
+        if (!await AccessControl.checkSession(uuid, session))
+            return res.status(401).json({ ...BaseStdResponse.ACCESS_DENIED });
+
+        const permission = await AccessControl.getPermission(uuid);
+        if (!permission.includes('admin') && !permission.includes('article'))
+            return res.json({ ...BaseStdResponse.PERMISSION_DENIED });
+
+        const safeSlug = String(slug).trim().toLowerCase();
+        if (!isValidSlug(safeSlug) || safeSlug.length > 32)
+            return res.json({ ...BaseStdResponse.ERR, msg: '分类标识格式无效(3-32位小写字母、数字、连字符)' });
+
+        const safeIcon = icon ? String(icon).trim().slice(0, 16) : null;
+        const sortOrder = Number(sort_order) || 0;
+        const active = Number(is_active) === 0 ? 0 : 1;
+        const now = Date.now();
+
+        try {
+            if (id) {
+                const existing = await db.query('SELECT slug FROM article_category WHERE id = ?', [id]);
+                if (!existing || existing.length === 0)
+                    return res.json({ ...BaseStdResponse.ERR, msg: '分类不存在' });
+
+                const oldSlug = existing[0].slug;
+                if (oldSlug !== safeSlug) {
+                    const dup = await db.query('SELECT id FROM article_category WHERE slug = ? AND id != ?', [safeSlug, id]);
+                    if (dup && dup.length > 0)
+                        return res.json({ ...BaseStdResponse.ERR, msg: '分类标识已存在' });
+                    await db.query('UPDATE article SET type = ? WHERE type = ?', [safeSlug, oldSlug]);
+                }
+
+                const r = await db.query(
+                    'UPDATE article_category SET name = ?, slug = ?, icon = ?, sort_order = ?, is_active = ? WHERE id = ?',
+                    [String(name).trim(), safeSlug, safeIcon, sortOrder, active, id]
+                );
+                if (!r || r.affectedRows === 0)
+                    return res.json({ ...BaseStdResponse.ERR, msg: '更新分类失败' });
+                return res.json({ ...BaseStdResponse.OK, id });
+            }
+
+            const dup = await db.query('SELECT id FROM article_category WHERE slug = ?', [safeSlug]);
+            if (dup && dup.length > 0)
+                return res.json({ ...BaseStdResponse.ERR, msg: '分类标识已存在' });
+
+            const r = await db.query(
+                'INSERT INTO article_category (name, slug, icon, sort_order, is_active, created_at) VALUES (?, ?, ?, ?, ?, ?)',
+                [String(name).trim(), safeSlug, safeIcon, sortOrder, active, now]
+            );
+            if (!r || r.affectedRows === 0)
+                return res.json({ ...BaseStdResponse.ERR, msg: '创建分类失败' });
+            return res.json({ ...BaseStdResponse.OK, id: r.insertId });
+        } catch (err) {
+            this.logger.error(`保存文章分类失败!${err.stack}`);
+            res.json({ ...BaseStdResponse.ERR, msg: '保存文章分类失败!' });
+        }
+    }
+}
+
+module.exports.AdminArticleCategoryUpsert = AdminArticleCategoryUpsert;

+ 35 - 0
apis/Article/Category/GetCategories.js

@@ -0,0 +1,35 @@
+const API = require("../../../lib/API");
+const db = require("../../../plugin/DataBase/db");
+const { BaseStdResponse } = require("../../../BaseStdResponse");
+
+class GetArticleCategories extends API {
+    constructor() {
+        super();
+        this.setPath('/Article/Categories');
+        this.setMethod('GET');
+    }
+
+    async onRequest(req, res) {
+        const sql = `
+            SELECT name, slug, icon, sort_order
+            FROM article_category
+            WHERE is_active = 1
+            ORDER BY sort_order ASC, id ASC
+        `;
+        try {
+            const rows = await db.query(sql);
+            res.json({
+                ...BaseStdResponse.OK,
+                data: rows || []
+            });
+        } catch (err) {
+            this.logger.error(`获取文章分类失败!${err.stack}`);
+            res.json({
+                ...BaseStdResponse.ERR,
+                msg: '获取文章分类失败!'
+            });
+        }
+    }
+}
+
+module.exports.GetArticleCategories = GetArticleCategories;

+ 33 - 32
apis/Article/GetArticle.js

@@ -5,28 +5,28 @@ const { BaseStdResponse } = require("../../BaseStdResponse");
 class GetArticle extends API {
     constructor() {
         super();
-
-        this.setPath('/Article')
-        this.setMethod('GET')
+        this.setPath('/Article');
+        this.setMethod('GET');
     }
 
     async onRequest(req, res) {
-        let { id } = req.query
+        const { slug } = req.query;
 
-        if (!id) {
-            res.json({
+        if (!slug) {
+            return res.json({
                 ...BaseStdResponse.MISSING_PARAMETER,
                 endpoint: 153123
             });
-            return;
         }
 
-        let sql = 'UPDATE article SET views = views + 1 WHERE state = 1 AND id = ?'
-        await db.query(sql, [id])
+        const safeSlug = String(slug).trim();
 
-        sql = `
+        try {
+            await db.query('UPDATE article SET views = views + 1 WHERE state = 1 AND slug = ?', [safeSlug]);
+
+            const sql = `
                 SELECT 
-                    a.id,
+                    a.slug,
                     a.title,
                     a.cover,
                     a.\`describe\`,
@@ -35,30 +35,31 @@ class GetArticle extends API {
                     a.views,
                     a.time,
                     u.username AS author
-                FROM 
-                    article a
-                LEFT JOIN 
-                    users u 
-                ON 
-                    a.author = u.uuid
-                WHERE 
-                    a.state = 1 AND a.id = ?
-            `
+                FROM article a
+                LEFT JOIN users u ON a.author = u.uuid
+                WHERE a.state = 1 AND a.slug = ?
+            `;
 
-        let rows = await db.query(sql, [id])
+            const rows = await db.query(sql, [safeSlug]);
 
-        if (!rows || rows.length !== 1)
-            return res.json({
-                ...BaseStdResponse.MISSING_FILE,
-                msg: '文章不存在或无查看权限'
-            })
+            if (!rows || rows.length !== 1)
+                return res.json({
+                    ...BaseStdResponse.MISSING_FILE,
+                    msg: '文章不存在或无查看权限'
+                });
 
-        res.json({
-            ...BaseStdResponse.OK,
-            data: rows
-        })
+            res.json({
+                ...BaseStdResponse.OK,
+                data: rows[0]
+            });
+        } catch (err) {
+            this.logger.error(`获取文章失败!${err.stack}`);
+            res.json({
+                ...BaseStdResponse.ERR,
+                msg: '获取文章失败!'
+            });
+        }
     }
-
 }
 
-module.exports.GetArticle = GetArticle;
+module.exports.GetArticle = GetArticle;

+ 30 - 58
apis/Article/GetArticleList.js

@@ -5,9 +5,8 @@ const { BaseStdResponse } = require("../../BaseStdResponse");
 class GetArticleList extends API {
     constructor() {
         super();
-
-        this.setPath('/Article/List')
-        this.setMethod('GET')
+        this.setPath('/Article/List');
+        this.setMethod('GET');
     }
 
     async onRequest(req, res) {
@@ -17,33 +16,21 @@ class GetArticleList extends API {
             return res.json({
                 ...BaseStdResponse.MISSING_PARAMETER,
                 endpoint: 1513126
-            })
+            });
         }
 
-        // 校验分页参数
-        if (isNaN(pagesize) || pagesize <= 0) {
-            return res.json({
-                ...BaseStdResponse.ERR,
-                msg: '参数错误'
-            })
-        }
+        pagesize = parseInt(pagesize, 10);
+        current = parseInt(current, 10);
 
-        if (isNaN(current) || current <= 0) {
-            return res.json({
-                ...BaseStdResponse.ERR,
-                msg: '参数错误'
-            })
+        if (isNaN(pagesize) || pagesize <= 0 || isNaN(current) || current <= 0) {
+            return res.json({ ...BaseStdResponse.ERR, msg: '参数错误' });
         }
 
-        // 计算分页的 offset
-        let offset = (current - 1) * pagesize
+        const offset = (current - 1) * pagesize;
 
-        pagesize = parseInt(pagesize, 10)
-        offset = parseInt(offset, 10)
-
-        let sql = `
+        const sql = `
             SELECT 
-                a.id,
+                a.slug,
                 a.title,
                 a.\`describe\`,
                 a.cover,
@@ -51,57 +38,42 @@ class GetArticleList extends API {
                 a.views,
                 a.time,
                 u.username AS author
-            FROM 
-                article a
-            LEFT JOIN 
-                users u 
-            ON 
-                a.author = u.uuid
-            WHERE 
-                a.state = 1 
-                AND a.type = ?
-            ORDER BY 
-                a.id DESC
-        `
+            FROM article a
+            LEFT JOIN users u ON a.author = u.uuid
+            WHERE a.state = 1 AND a.type = ?
+            ORDER BY a.id DESC
+            LIMIT ? OFFSET ?
+        `;
 
-        // 查询文章总数,方便返回总页数
-        let countSql = `
+        const countSql = `
             SELECT COUNT(*) AS total
             FROM article a
-            WHERE a.state = 1 
-              AND a.type = ?;
-        `
+            WHERE a.state = 1 AND a.type = ?
+        `;
 
         try {
-            // 获取文章列表
-            let articles = await db.query(sql, [type])
-
-            // 获取总记录数
-            let countResult = await db.query(countSql, [type])
-            let total = countResult[0].total;
+            const articles = await db.query(sql, [type, String(pagesize), String(offset)]);
+            const countResult = await db.query(countSql, [type]);
+            const total = countResult[0].total;
+            const totalPages = Math.ceil(total / pagesize);
 
-            // 计算总页数
-            const totalPages = Math.ceil(total / pagesize)
-
-            // 返回结果
             res.json({
                 ...BaseStdResponse.OK,
                 data: articles || [],
                 pagination: {
-                    current: current,
-                    pagesize: pagesize,
-                    total: total,
-                    totalPages: totalPages
+                    current,
+                    pagesize,
+                    total,
+                    totalPages
                 }
-            })
-
+            });
         } catch (err) {
-            this.logger.error(`获取文章列表失败!${err.stack}`)
+            this.logger.error(`获取文章列表失败!${err.stack}`);
             res.json({
                 ...BaseStdResponse.ERR,
                 msg: '获取文章列表失败!',
                 endpoint: 153127
-            })
+            });
         }
     }
 }

+ 37 - 0
apis/Download/Admin/Delete.js

@@ -0,0 +1,37 @@
+const API = require("../../../lib/API");
+const db = require("../../../plugin/DataBase/db");
+const AccessControl = require("../../../lib/AccessControl");
+const { BaseStdResponse } = require("../../../BaseStdResponse");
+
+class AdminDownloadDelete extends API {
+    constructor() {
+        super();
+        this.setPath('/Admin/Download');
+        this.setMethod('DELETE');
+    }
+
+    async onRequest(req, res) {
+        const { uuid, session, id } = req.body;
+        if ([uuid, session, id].some(v => v === '' || v === null || v === undefined))
+            return res.json({ ...BaseStdResponse.MISSING_PARAMETER });
+
+        if (!await AccessControl.checkSession(uuid, session))
+            return res.status(401).json({ ...BaseStdResponse.ACCESS_DENIED });
+
+        const permission = await AccessControl.getPermission(uuid);
+        if (!permission.includes('admin') && !permission.includes('service'))
+            return res.json({ ...BaseStdResponse.PERMISSION_DENIED });
+
+        try {
+            const r = await db.query('DELETE FROM download_item WHERE id = ?', [id]);
+            if (!r || r.affectedRows === 0)
+                return res.json({ ...BaseStdResponse.ERR, msg: '删除下载项失败' });
+            return res.json({ ...BaseStdResponse.OK });
+        } catch (err) {
+            this.logger.error(`删除下载项失败!${err.stack}`);
+            res.json({ ...BaseStdResponse.ERR, msg: '删除下载项失败!' });
+        }
+    }
+}
+
+module.exports.AdminDownloadDelete = AdminDownloadDelete;

+ 42 - 0
apis/Download/Admin/List.js

@@ -0,0 +1,42 @@
+const API = require("../../../lib/API");
+const db = require("../../../plugin/DataBase/db");
+const AccessControl = require("../../../lib/AccessControl");
+const { BaseStdResponse } = require("../../../BaseStdResponse");
+
+class AdminDownloadList extends API {
+    constructor() {
+        super();
+        this.setPath('/Admin/Download/List');
+        this.setMethod('GET');
+    }
+
+    async onRequest(req, res) {
+        const { uuid, session } = req.query;
+        if ([uuid, session].some(v => v === '' || v === null || v === undefined))
+            return res.json({ ...BaseStdResponse.MISSING_PARAMETER });
+
+        if (!await AccessControl.checkSession(uuid, session))
+            return res.status(401).json({ ...BaseStdResponse.ACCESS_DENIED });
+
+        const permission = await AccessControl.getPermission(uuid);
+        if (!permission.includes('admin') && !permission.includes('service'))
+            return res.json({ ...BaseStdResponse.PERMISSION_DENIED });
+
+        const sql = `
+            SELECT id, title, description, download_url, version, icon,
+                   button_text, button_color, platform, extract_code,
+                   changelog_html, sort_order, is_active, created_at, updated_at
+            FROM download_item
+            ORDER BY sort_order ASC, id ASC
+        `;
+        try {
+            const rows = await db.query(sql);
+            res.json({ ...BaseStdResponse.OK, data: rows || [] });
+        } catch (err) {
+            this.logger.error(`获取下载项列表失败!${err.stack}`);
+            res.json({ ...BaseStdResponse.ERR, msg: '获取下载项列表失败!' });
+        }
+    }
+}
+
+module.exports.AdminDownloadList = AdminDownloadList;

+ 97 - 0
apis/Download/Admin/Upsert.js

@@ -0,0 +1,97 @@
+const API = require("../../../lib/API");
+const db = require("../../../plugin/DataBase/db");
+const AccessControl = require("../../../lib/AccessControl");
+const { BaseStdResponse } = require("../../../BaseStdResponse");
+
+class AdminDownloadUpsert extends API {
+    constructor() {
+        super();
+        this.setPath('/Admin/Download');
+        this.setMethod('POST');
+    }
+
+    async onRequest(req, res) {
+        const {
+            uuid, session, id, title, description, download_url, version,
+            icon, button_text, button_color, platform, extract_code,
+            changelog_html, sort_order, is_active
+        } = req.body;
+
+        if ([uuid, session, title, download_url].some(v => v === '' || v === null || v === undefined))
+            return res.json({ ...BaseStdResponse.MISSING_PARAMETER });
+
+        if (!await AccessControl.checkSession(uuid, session))
+            return res.status(401).json({ ...BaseStdResponse.ACCESS_DENIED });
+
+        const permission = await AccessControl.getPermission(uuid);
+        if (!permission.includes('admin') && !permission.includes('service'))
+            return res.json({ ...BaseStdResponse.PERMISSION_DENIED });
+
+        const now = Date.now();
+        const safeIcon = (icon && String(icon).trim()) ? String(icon).trim().slice(0, 16) : '📦';
+        const sortOrder = Number(sort_order) || 0;
+        const active = Number(is_active) === 0 ? 0 : 1;
+
+        try {
+            if (id) {
+                const r = await db.query(`
+                    UPDATE download_item SET
+                        title = ?, description = ?, download_url = ?, version = ?,
+                        icon = ?, button_text = ?, button_color = ?, platform = ?,
+                        extract_code = ?, changelog_html = ?, sort_order = ?,
+                        is_active = ?, updated_at = ?
+                    WHERE id = ?
+                `, [
+                    String(title).trim(),
+                    description || '',
+                    String(download_url).trim(),
+                    version || '',
+                    safeIcon,
+                    button_text || '立即下载',
+                    button_color || 'primary',
+                    platform || '',
+                    extract_code || '',
+                    changelog_html || '',
+                    sortOrder,
+                    active,
+                    now,
+                    id
+                ]);
+                if (!r || r.affectedRows === 0)
+                    return res.json({ ...BaseStdResponse.ERR, msg: '更新下载项失败' });
+                return res.json({ ...BaseStdResponse.OK, id });
+            }
+
+            const r = await db.query(`
+                INSERT INTO download_item
+                    (title, description, download_url, version, icon, button_text,
+                     button_color, platform, extract_code, changelog_html,
+                     sort_order, is_active, created_at, updated_at)
+                VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+            `, [
+                String(title).trim(),
+                description || '',
+                String(download_url).trim(),
+                version || '',
+                safeIcon,
+                button_text || '立即下载',
+                button_color || 'primary',
+                platform || '',
+                extract_code || '',
+                changelog_html || '',
+                sortOrder,
+                active,
+                now,
+                now
+            ]);
+            if (!r || r.affectedRows === 0)
+                return res.json({ ...BaseStdResponse.ERR, msg: '创建下载项失败' });
+            return res.json({ ...BaseStdResponse.OK, id: r.insertId });
+        } catch (err) {
+            this.logger.error(`保存下载项失败!${err.stack}`);
+            res.json({ ...BaseStdResponse.ERR, msg: '保存下载项失败!' });
+        }
+    }
+}
+
+module.exports.AdminDownloadUpsert = AdminDownloadUpsert;

+ 30 - 0
apis/Download/Public/GetDownloadCenter.js

@@ -0,0 +1,30 @@
+const API = require("../../../lib/API");
+const db = require("../../../plugin/DataBase/db");
+const { BaseStdResponse } = require("../../../BaseStdResponse");
+
+class GetDownloadCenter extends API {
+    constructor() {
+        super();
+        this.setPath('/Public/GetDownloadCenter');
+        this.setMethod('GET');
+    }
+
+    async onRequest(req, res) {
+        const sql = `
+            SELECT title, description, download_url, version, icon,
+                   button_text, button_color, platform, extract_code, changelog_html, sort_order
+            FROM download_item
+            WHERE is_active = 1
+            ORDER BY sort_order ASC, id ASC
+        `;
+        try {
+            const rows = await db.query(sql);
+            res.json({ ...BaseStdResponse.OK, data: rows || [] });
+        } catch (err) {
+            this.logger.error(`获取下载中心失败!${err.stack}`);
+            res.json({ ...BaseStdResponse.ERR, msg: '获取下载中心失败!' });
+        }
+    }
+}
+
+module.exports.GetDownloadCenter = GetDownloadCenter;

+ 45 - 0
lib/slugify.js

@@ -0,0 +1,45 @@
+const SLUG_PATTERN = /^[a-z0-9][a-z0-9-]{1,62}[a-z0-9]$|^[a-z0-9]{3}$/
+
+function simpleHash(str) {
+    let h = 0
+    for (let i = 0; i < str.length; i++) {
+        h = ((h << 5) - h) + str.charCodeAt(i)
+        h |= 0
+    }
+    return Math.abs(h).toString(36)
+}
+
+function slugify(title) {
+    let base = String(title || '').trim().toLowerCase()
+        .replace(/\s+/g, '-')
+        .replace(/[^a-z0-9-]/g, '')
+        .replace(/-+/g, '-')
+        .replace(/^-|-$/g, '')
+
+    if (base.length < 3) {
+        base = 'article-' + simpleHash(String(title || Date.now()))
+    }
+    return base.slice(0, 58)
+}
+
+function isValidSlug(slug) {
+    return SLUG_PATTERN.test(String(slug || ''))
+}
+
+async function ensureUniqueSlug(db, slug, excludeId = null) {
+    let candidate = slug
+    let n = 2
+    while (true) {
+        const sql = excludeId
+            ? 'SELECT id FROM article WHERE slug = ? AND id != ? LIMIT 1'
+            : 'SELECT id FROM article WHERE slug = ? LIMIT 1'
+        const params = excludeId ? [candidate, excludeId] : [candidate]
+        const rows = await db.query(sql, params)
+        if (!rows || rows.length === 0) return candidate
+        const suffix = `-${n}`
+        candidate = `${slug.slice(0, 64 - suffix.length)}${suffix}`
+        n++
+    }
+}
+
+module.exports = { slugify, isValidSlug, ensureUniqueSlug, SLUG_PATTERN }