diff --git a/bun.lockb b/bun.lockb
index 3adb219..3882124 100644
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/jsconfig.json b/jsconfig.json
index 46359a5..8b7bdc7 100644
--- a/jsconfig.json
+++ b/jsconfig.json
@@ -18,9 +18,13 @@
"src/services/*"
]
},
- "module": "commonjs",
- "target": "es6",
- "allowSyntheticDefaultImports": true
+ "module": "ESNext",
+ "target": "ES2020",
+ "moduleResolution": "node",
+ "allowSyntheticDefaultImports": true,
+ "esModuleInterop": true,
+ "allowJs": true,
+ "checkJs": false
},
"include": [
"src/**/*",
diff --git a/knexfile.mjs b/knexfile.mjs
index f30202e..e90a593 100644
--- a/knexfile.mjs
+++ b/knexfile.mjs
@@ -30,12 +30,13 @@ export default {
createRetryIntervalMillis: 200, // 创建连接重试间隔
afterCreate: (conn, done) => {
// SQLite 性能优化设置
- conn.run("PRAGMA journal_mode = WAL", done) // 启用 WAL 模式提高并发
- conn.run("PRAGMA synchronous = NORMAL", done) // 平衡性能和安全性
- conn.run("PRAGMA cache_size = 1000", done) // 增加缓存大小
- conn.run("PRAGMA temp_store = MEMORY", done) // 临时数据存储在内存中
- conn.run("PRAGMA mmap_size = 67108864", done) // 启用内存映射,64MB
- conn.run("PRAGMA foreign_keys = ON", done) // 启用外键约束
+ conn.run("PRAGMA journal_mode = WAL")
+ conn.run("PRAGMA synchronous = NORMAL")
+ conn.run("PRAGMA cache_size = 1000")
+ conn.run("PRAGMA temp_store = MEMORY")
+ conn.run("PRAGMA mmap_size = 67108864")
+ conn.run("PRAGMA foreign_keys = ON")
+ done() // 只调用一次 done()
},
},
},
@@ -71,13 +72,14 @@ export default {
createRetryIntervalMillis: 200,
afterCreate: (conn, done) => {
// SQLite 性能优化设置
- conn.run("PRAGMA journal_mode = WAL", done)
- conn.run("PRAGMA synchronous = NORMAL", done)
- conn.run("PRAGMA cache_size = 2000", done) // 生产环境更大缓存
- conn.run("PRAGMA temp_store = MEMORY", done)
- conn.run("PRAGMA mmap_size = 134217728", done) // 128MB 内存映射
- conn.run("PRAGMA foreign_keys = ON", done)
- conn.run("PRAGMA auto_vacuum = INCREMENTAL", done) // 增量清理
+ conn.run("PRAGMA journal_mode = WAL")
+ conn.run("PRAGMA synchronous = NORMAL")
+ conn.run("PRAGMA cache_size = 2000") // 生产环境更大缓存
+ conn.run("PRAGMA temp_store = MEMORY")
+ conn.run("PRAGMA mmap_size = 134217728") // 128MB 内存映射
+ conn.run("PRAGMA foreign_keys = ON")
+ conn.run("PRAGMA auto_vacuum = INCREMENTAL") // 增量清理
+ done() // 只调用一次 done()
},
},
},
diff --git a/package.json b/package.json
index dbdf18b..c25f9e6 100644
--- a/package.json
+++ b/package.json
@@ -36,6 +36,8 @@
"get-paths": "^0.0.7",
"image-thumbnail": "^1.0.17",
"jsonwebtoken": "^9.0.0",
+ "jstransformer-markdown-it": "^3.0.0",
+ "jstransformer-scss": "^2.0.0",
"knex": "^3.1.0",
"koa": "^3.0.0",
"koa-bodyparser": "^4.4.1",
diff --git a/public/css/page/index.css b/public/css/page/index.css
index 77f4e86..7a6483a 100644
--- a/public/css/page/index.css
+++ b/public/css/page/index.css
@@ -1,69 +1,146 @@
-.list {
- display: flex;
- gap: 15px;
- flex-wrap: wrap;
-
- &.blog {
-
- >* {
- width: calc(25% - 15px * 3 / 4);
- }
-
- /* ≥1024px 默认4列;介于768px-1023px 显示3列 */
- @media (max-width: 1023px) {
- >* {
- width: calc(33.3333% - 15px * 2 / 3);
- }
- }
-
- /* 介于640px-767px 显示2列 */
- @media (max-width: 767px) {
- >* {
- width: calc(50% - 15px * 1 / 2);
- }
- }
-
- /* <640px 显示1列,并优化间距与字号 */
- @media (max-width: 639px) {
- gap: 12px;
-
- >* {
- width: 100%;
- }
-
- .article-card {
- padding: 14px;
- }
-
- .article-title {
- font-size: 16px;
- }
-
- .article-meta {
- font-size: 12px;
- }
-
- .article-desc {
- font-size: 14px;
- }
- }
- }
-}
-
-.list a:hover {
- text-decoration: underline;
-}
-
-.material-symbols-light--info-rounded {
- display: inline-block;
- width: 24px;
- height: 24px;
- --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23000' d='M12 16.5q.214 0 .357-.144T12.5 16v-4.5q0-.213-.144-.356T11.999 11t-.356.144t-.143.356V16q0 .213.144.356t.357.144M12 9.577q.262 0 .439-.177t.176-.438t-.177-.439T12 8.346t-.438.177t-.177.439t.177.438t.438.177M12.003 21q-1.867 0-3.51-.708q-1.643-.709-2.859-1.924t-1.925-2.856T3 12.003t.709-3.51Q4.417 6.85 5.63 5.634t2.857-1.925T11.997 3t3.51.709q1.643.708 2.859 1.922t1.925 2.857t.709 3.509t-.708 3.51t-1.924 2.859t-2.856 1.925t-3.509.709'/%3E%3C/svg%3E");
- background-color: currentColor;
- -webkit-mask-image: var(--svg);
- mask-image: var(--svg);
- -webkit-mask-repeat: no-repeat;
- mask-repeat: no-repeat;
- -webkit-mask-size: 100% 100%;
- mask-size: 100% 100%;
+/* 首页样式 */
+
+.hero-section {
+ position: relative;
+ overflow: hidden;
+}
+
+.hero-section::before {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: url('/images/hero-bg.svg') no-repeat center center;
+ background-size: cover;
+ opacity: 0.1;
+ z-index: 0;
+}
+
+.hero-content {
+ position: relative;
+ z-index: 1;
+}
+
+.feature-card {
+ transition: all 0.3s ease;
+}
+
+.feature-card:hover {
+ transform: translateY(-5px);
+}
+
+.feature-card .material-symbols-light--article,
+.feature-card .material-symbols-light--bookmark,
+.feature-card .material-symbols-light--person {
+ transition: all 0.3s ease;
+}
+
+.feature-card:hover .material-symbols-light--article,
+.feature-card:hover .material-symbols-light--bookmark,
+.feature-card:hover .material-symbols-light--person {
+ transform: scale(1.1);
+}
+
+.stats-section {
+ position: relative;
+}
+
+.stats-section::before {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: url('/images/stats-bg.svg') no-repeat center center;
+ background-size: cover;
+ opacity: 0.05;
+ z-index: 0;
+}
+
+.stat-item {
+ transition: all 0.3s ease;
+}
+
+.stat-item:hover {
+ transform: scale(1.05);
+}
+
+.user-dashboard {
+ position: relative;
+}
+
+.user-dashboard::before {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: url('/images/dashboard-bg.svg') no-repeat center center;
+ background-size: cover;
+ opacity: 0.03;
+ z-index: 0;
+}
+
+.avatar {
+ transition: all 0.3s ease;
+}
+
+.avatar:hover {
+ transform: scale(1.05);
+}
+
+/* 响应式设计 */
+@media (max-width: 768px) {
+ .hero-section {
+ padding: 4rem 0;
+ }
+
+ .hero-content h1 {
+ font-size: 2.5rem;
+ }
+
+ .features-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .stats-grid {
+ grid-template-columns: 1fr 1fr;
+ }
+
+ .user-info {
+ text-align: center;
+ margin-bottom: 1.5rem;
+ }
+
+ .user-actions {
+ justify-content: center;
+ }
+}
+
+@media (max-width: 480px) {
+ .hero-content h1 {
+ font-size: 2rem;
+ }
+
+ .hero-content p {
+ font-size: 1rem;
+ }
+
+ .stats-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .hero-actions {
+ flex-direction: column;
+ gap: 1rem;
+ }
+
+ .hero-actions a {
+ width: 100%;
+ text-align: center;
+ }
}
\ No newline at end of file
diff --git a/public/images/dashboard-bg.svg b/public/images/dashboard-bg.svg
new file mode 100644
index 0000000..f21bff1
--- /dev/null
+++ b/public/images/dashboard-bg.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/public/images/hero-bg.svg b/public/images/hero-bg.svg
new file mode 100644
index 0000000..5fe0eaf
--- /dev/null
+++ b/public/images/hero-bg.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/public/images/stats-bg.svg b/public/images/stats-bg.svg
new file mode 100644
index 0000000..ec590c6
--- /dev/null
+++ b/public/images/stats-bg.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/src/base/BaseController.js b/src/base/BaseController.js
index 853fa86..a464c97 100644
--- a/src/base/BaseController.js
+++ b/src/base/BaseController.js
@@ -255,6 +255,25 @@ class BaseController {
}
/**
+ * 检查用户是否已登录
+ * @param {*} ctx - Koa上下文
+ * @returns {boolean} 是否已登录
+ */
+ isLoggedIn(ctx) {
+ return !!ctx.state.user
+ }
+
+ /**
+ * 获取用户ID
+ * @param {*} ctx - Koa上下文
+ * @returns {string|number|null} 用户ID
+ */
+ getCurrentUserId(ctx) {
+ const user = this.getCurrentUser(ctx)
+ return user ? (user.id || user._id || null) : null
+ }
+
+ /**
* 检查用户权限
* @param {*} ctx - Koa上下文
* @param {string|Array} permission - 权限名或权限数组
@@ -286,7 +305,8 @@ class BaseController {
throw new CommonError("用户未登录")
}
- if (resource[ownerField] !== user.id && resource[ownerField] !== user.username) {
+ const userId = this.getCurrentUserId(ctx)
+ if (resource[ownerField] !== userId && resource[ownerField] !== user.username) {
throw new CommonError("无权限操作此资源")
}
}
diff --git a/src/controllers/Api/JobController.js b/src/controllers/Api/JobController.js
index 1f9cf6d..a691f3c 100644
--- a/src/controllers/Api/JobController.js
+++ b/src/controllers/Api/JobController.js
@@ -34,7 +34,8 @@ class JobController {
static createRoutes() {
const controller = new JobController()
- const router = new Router({ prefix: "/api/jobs" })
+ const router = new Router({ prefix: "/api/jobs", auth: true })
+ router.get("/", controller.list.bind(controller))
router.get("/", controller.list.bind(controller))
router.post("/start/:id", controller.start.bind(controller))
router.post("/stop/:id", controller.stop.bind(controller))
diff --git a/src/controllers/Page/CommonController.js b/src/controllers/Page/CommonController.js
index 118b693..a9e77a2 100644
--- a/src/controllers/Page/CommonController.js
+++ b/src/controllers/Page/CommonController.js
@@ -1,17 +1,39 @@
import Router from "utils/router.js"
import { logger } from "@/logger.js"
import BaseController from "@/base/BaseController.js"
-
+import SiteConfigModel from '@/db/models/SiteConfigModel.js'
export default class CommonController extends BaseController {
constructor() {
super()
}
+ pageGet(...args) {
+ return (ctx) => {
+ return ctx.render(...args)
+ }
+ }
+
// 首页
async indexGet(ctx) {
+ // 可以在这里添加一些需要用户信息的逻辑
+ // 例如获取用户相关的统计数据等
+ const user = ctx.state.user || null;
+
+ // 示例数据,实际项目中可以从数据库获取
+ const stats = {
+ articles: 1234,
+ users: 567,
+ categories: 89,
+ responseTime: "24h"
+ };
+
return await ctx.render(
- "page/index/index", {}, { includeSite: true, includeUser: true }
+ "page/index/index",
+ {
+ stats,
+ // 其他需要传递给模板的数据
+ }
)
}
@@ -24,8 +46,16 @@ export default class CommonController extends BaseController {
const router = new Router({ auth: "try" })
// 首页
- router.get("", controller.handleRequest(controller.indexGet), { auth: false })
- router.get("/", controller.handleRequest(controller.indexGet), { auth: false })
+ router.get("", controller.handleRequest(controller.indexGet))
+ router.get("/", controller.handleRequest(controller.indexGet))
+ // router.get("/about", controller.handleRequest(controller.pageGet("page/about/index")))
+ router.get("/contact", controller.handleRequest(controller.pageGet("page/extra/contact")))
+ router.get("/faq", controller.handleRequest(controller.pageGet("page/extra/faq")))
+ router.get("/feedback", controller.handleRequest(controller.pageGet("page/extra/feedback")))
+ router.get("/help", controller.handleRequest(controller.pageGet("page/extra/help")))
+ router.get("/privacy", controller.handleRequest(controller.pageGet("page/extra/privacy")))
+ router.get("/terms", controller.handleRequest(controller.pageGet("page/extra/terms")))
+ router.get("/no-auth", controller.handleRequest(controller.pageGet("page/auth/no-auth")))
return router
}
diff --git a/src/db/index.js b/src/db/index.js
index bf78b42..9a23f47 100644
--- a/src/db/index.js
+++ b/src/db/index.js
@@ -122,108 +122,98 @@ export const DbQueryCache = {
// QueryBuilder 扩展
// 1) cache(ttlMs?): 读取缓存,不存在则执行并写入
-buildKnex.QueryBuilder.extend("cache", async function (ttlMs) {
- const key = getCacheKeyForBuilder(this)
- const entry = queryCache.get(key)
- if (entry && !isExpired(entry)) {
- return entry.value
- }
- const data = await this
- queryCache.set(key, { value: data, expiresAt: computeExpiresAt(ttlMs) })
- return data
-})
-
-// 2) cacheAs(customKey): 设置自定义 key
-buildKnex.QueryBuilder.extend("cacheAs", function (customKey) {
- this._customCacheKey = String(customKey)
- return this
-})
-
-// 3) cacheSet(value, ttlMs?): 手动设置当前查询 key 的缓存
-buildKnex.QueryBuilder.extend("cacheSet", function (value, ttlMs) {
- const key = getCacheKeyForBuilder(this)
- queryCache.set(key, { value, expiresAt: computeExpiresAt(ttlMs) })
- return value
-})
-
-// 4) cacheGet(): 仅从缓存读取当前查询 key 的值
-buildKnex.QueryBuilder.extend("cacheGet", function () {
- const key = getCacheKeyForBuilder(this)
- const entry = queryCache.get(key)
- if (!entry || isExpired(entry)) return undefined
- return entry.value
-})
+if (buildKnex.QueryBuilder && typeof buildKnex.QueryBuilder.extend === 'function') {
+ buildKnex.QueryBuilder.extend("cache", async function (ttlMs) {
+ const key = getCacheKeyForBuilder(this)
+ const entry = queryCache.get(key)
+ if (entry && !isExpired(entry)) {
+ return entry.value
+ }
+ const data = await this
+ queryCache.set(key, { value: data, expiresAt: computeExpiresAt(ttlMs) })
+ return data
+ })
+
+ // 2) cacheAs(customKey): 设置自定义 key
+ buildKnex.QueryBuilder.extend("cacheAs", function (customKey) {
+ this._customCacheKey = String(customKey)
+ return this
+ })
+
+ // 3) cacheSet(value, ttlMs?): 手动设置当前查询 key 的缓存
+ buildKnex.QueryBuilder.extend("cacheSet", function (value, ttlMs) {
+ const key = getCacheKeyForBuilder(this)
+ queryCache.set(key, { value, expiresAt: computeExpiresAt(ttlMs) })
+ return value
+ })
-// 5) cacheInvalidate(): 使当前查询 key 的缓存失效
-buildKnex.QueryBuilder.extend("cacheInvalidate", function () {
- const key = getCacheKeyForBuilder(this)
- queryCache.delete(key)
- return this
-})
+ // 4) cacheGet(): 仅从缓存读取当前查询 key 的值
+ buildKnex.QueryBuilder.extend("cacheGet", function () {
+ const key = getCacheKeyForBuilder(this)
+ const entry = queryCache.get(key)
+ if (!entry || isExpired(entry)) return undefined
+ return entry.value
+ })
-// 6) cacheInvalidateByPrefix(prefix): 按前缀清理
-buildKnex.QueryBuilder.extend("cacheInvalidateByPrefix", function (prefix) {
- const p = String(prefix)
- for (const k of queryCache.keys()) {
- if (k.startsWith(p)) queryCache.delete(k)
- }
- return this
-})
+ // 5) cacheInvalidate(): 使当前查询 key 的缓存失效
+ buildKnex.QueryBuilder.extend("cacheInvalidate", function () {
+ const key = getCacheKeyForBuilder(this)
+ queryCache.delete(key)
+ return this
+ })
-// 7) 数据变更时自动清理相关缓存
-buildKnex.QueryBuilder.extend("invalidateCache", function() {
- const tableName = this._single?.table
- if (tableName) {
- DbQueryCache.invalidateByTable(tableName)
- logger.debug(`清理表 ${tableName} 的缓存`)
- }
- return this
-})
+ // 6) cacheInvalidateByPrefix(prefix): 按前缀清理
+ buildKnex.QueryBuilder.extend("cacheInvalidateByPrefix", function (prefix) {
+ const p = String(prefix)
+ for (const k of queryCache.keys()) {
+ if (k.startsWith(p)) queryCache.delete(k)
+ }
+ return this
+ })
-// 8) 为 CUD 操作添加自动缓存失效
-const originalInsert = buildKnex.QueryBuilder.prototype.insert
-buildKnex.QueryBuilder.prototype.insert = function(...args) {
- const tableName = this._single?.table
- const result = originalInsert.apply(this, args)
- if (tableName) {
- // 在操作完成后清理缓存
- result.then(() => {
+ // 7) 数据变更时自动清理相关缓存
+ buildKnex.QueryBuilder.extend("invalidateCache", function() {
+ const tableName = this._single?.table
+ if (tableName) {
DbQueryCache.invalidateByTable(tableName)
- }).catch(() => {
- // 即使失败也清理缓存,保证一致性
- DbQueryCache.invalidateByTable(tableName)
- })
- }
- return result
+ logger.debug(`清理表 ${tableName} 的缓存`)
+ }
+ return this
+ })
}
-const originalUpdate = buildKnex.QueryBuilder.prototype.update
-buildKnex.QueryBuilder.prototype.update = function(...args) {
- const tableName = this._single?.table
- const result = originalUpdate.apply(this, args)
- if (tableName) {
- result.then(() => {
- DbQueryCache.invalidateByTable(tableName)
- }).catch(() => {
- DbQueryCache.invalidateByTable(tableName)
- })
+// 8) 为 CUD 操作添加自动缓存失效
+// 使用更安全的方式扩展 QueryBuilder 方法
+const addCacheInvalidation = (methodName) => {
+ if (buildKnex.QueryBuilder && buildKnex.QueryBuilder.prototype && buildKnex.QueryBuilder.prototype[methodName]) {
+ const originalMethod = buildKnex.QueryBuilder.prototype[methodName];
+ buildKnex.QueryBuilder.prototype[methodName] = function(...args) {
+ const result = originalMethod.apply(this, args);
+ const tableName = this._single?.table;
+
+ if (tableName && result && typeof result.then === 'function') {
+ // 在操作完成后清理缓存
+ const originalThen = result.then;
+ result.then = function(...thenArgs) {
+ const promise = originalThen.apply(this, thenArgs);
+ promise.then(() => {
+ DbQueryCache.invalidateByTable(tableName);
+ }).catch(() => {
+ DbQueryCache.invalidateByTable(tableName);
+ });
+ return promise;
+ };
+ }
+
+ return result;
+ };
}
- return result
-}
+};
-const originalDel = buildKnex.QueryBuilder.prototype.del
-buildKnex.QueryBuilder.prototype.del = function(...args) {
- const tableName = this._single?.table
- const result = originalDel.apply(this, args)
- if (tableName) {
- result.then(() => {
- DbQueryCache.invalidateByTable(tableName)
- }).catch(() => {
- DbQueryCache.invalidateByTable(tableName)
- })
- }
- return result
-}
+// 安全地扩展 CUD 方法
+addCacheInvalidation('insert');
+addCacheInvalidation('update');
+addCacheInvalidation('del');
const environment = process.env.NODE_ENV || "development"
const db = buildKnex(knexConfig[environment])
diff --git a/src/db/models/SiteConfigModel.js b/src/db/models/SiteConfigModel.js
index 1d291b4..d8340ae 100644
--- a/src/db/models/SiteConfigModel.js
+++ b/src/db/models/SiteConfigModel.js
@@ -39,7 +39,7 @@ class SiteConfigModel extends BaseModel {
// 获取所有配置
static async getAll() {
- const rows = await db(this.tableName).select("key", "value")
+ const rows = await db(this.tableName).select("key", "value").cache()
const result = {}
rows.forEach(row => {
result[row.key] = row.value
diff --git a/src/logger.js b/src/logger.js
index 06392df..9dacabe 100644
--- a/src/logger.js
+++ b/src/logger.js
@@ -52,12 +52,11 @@ log4js.configure({
},
categories: {
jobs: { appenders: ["console", "jobs"], level: "info" },
- error: { appenders: ["console", "error"], level: "error" },
- default: { appenders: ["console", "all", "error"], level: "all" },
+ // error: { appenders: ["console", "error"], level: "error" },
+ default: { appenders: ["console", "all"], level: "all" },
},
});
// 导出常用 logger 实例,便于直接引用
export const logger = log4js.getLogger(); // default
export const jobLogger = log4js.getLogger('jobs');
-export const errorLogger = log4js.getLogger('error');
diff --git a/src/main.js b/src/main.js
index 7f27c89..07f5261 100644
--- a/src/main.js
+++ b/src/main.js
@@ -9,33 +9,36 @@ import os from "os"
// 应用插件与自动路由
import LoadMiddlewares from "./middlewares/install.js"
-// 注册插件
-LoadMiddlewares(app)
-
-const PORT = process.env.PORT || 3000
-
-const server = app.listen(PORT, () => {
- const port = server.address().port
- // 获取本地 IP
- const getLocalIP = () => {
- const interfaces = os.networkInterfaces()
- for (const name of Object.keys(interfaces)) {
- for (const iface of interfaces[name]) {
- if (iface.family === "IPv4" && !iface.internal) {
- return iface.address
+const PORT = process.env.PORT || 3000;
+
+; (async () => {
+
+ // 注册插件
+ await LoadMiddlewares(app)
+
+ const server = app.listen(PORT, () => {
+ const port = server.address().port
+ // 获取本地 IP
+ const getLocalIP = () => {
+ const interfaces = os.networkInterfaces()
+ for (const name of Object.keys(interfaces)) {
+ for (const iface of interfaces[name]) {
+ if (iface.family === "IPv4" && !iface.internal) {
+ return iface.address
+ }
}
}
+ return "localhost"
}
- return "localhost"
- }
- const localIP = getLocalIP()
- logger.trace(`──────────────────── 服务器已启动 ────────────────────`)
- logger.trace(` `)
- logger.trace(` 本地访问: http://localhost:${port} `)
- logger.trace(` 局域网: http://${localIP}:${port} `)
- logger.trace(` `)
- logger.trace(` 服务启动时间: ${new Date().toLocaleString()} `)
- logger.trace(`──────────────────────────────────────────────────────\n`)
-})
-
-export default app
+ const localIP = getLocalIP()
+ logger.trace(`──────────────────── 服务器已启动 ────────────────────`)
+ logger.trace(` `)
+ logger.trace(` 本地访问: http://localhost:${port} `)
+ logger.trace(` 局域网: http://${localIP}:${port} `)
+ logger.trace(` `)
+ logger.trace(` 服务启动时间: ${new Date().toLocaleString()} `)
+ logger.trace(`──────────────────────────────────────────────────────\n`)
+ })
+
+
+})()
diff --git a/src/middlewares/Auth/index.js b/src/middlewares/Auth/index.js
index 4f2d4e8..96dbdd3 100644
--- a/src/middlewares/Auth/index.js
+++ b/src/middlewares/Auth/index.js
@@ -49,16 +49,16 @@ export function AuthMiddleware(options = {
export function VerifyUserMiddleware() {
return (ctx, next) => {
if (ctx.session.user) {
- ctx.user = ctx.session.user
+ ctx.state.user = ctx.session.user
} else {
const authorizationString = ctx.headers["authorization"]
if (authorizationString) {
const token = authorizationString.replace(/^Bearer\s/, "")
- ctx.user = jwt.verify(token, process.env.JWT_SECRET)
+ ctx.state.user = jwt.verify(token, process.env.JWT_SECRET)
}
}
if (ctx.authType === false) {
- if (ctx.user) {
+ if (ctx.state.user) {
throw new CommonError("该接口不能登录查看")
}
return next()
@@ -66,7 +66,7 @@ export function VerifyUserMiddleware() {
if (ctx.authType === "try") {
return next()
}
- if (!ctx.user && ctx.authType === true) {
+ if (!ctx.state.user && ctx.authType === true) {
throw new CommonError("请登录")
}
return next()
diff --git a/src/middlewares/Views/index.js b/src/middlewares/Views/index.js
index 9508101..e323d43 100644
--- a/src/middlewares/Views/index.js
+++ b/src/middlewares/Views/index.js
@@ -4,10 +4,8 @@ import consolidate from "consolidate"
import send from "../Send"
import getPaths from "get-paths"
import pretty from "pretty"
-// import { logger } from "@/logger"
-// import SiteConfigService from "services/SiteConfigService.js"
import assign from "lodash/assign"
-// import config from "config/index.js"
+import { logger } from "@/logger"
export default viewsMiddleware
@@ -19,22 +17,27 @@ function viewsMiddleware(path, { engineSource = consolidate, extension = "html",
// 将 render 注入到 context 和 response 对象中
ctx.response.render = ctx.render = function (relPath, locals = {}, renderOptions) {
- renderOptions = assign({ includeSite: true, includeUser: true }, renderOptions || {})
+ renderOptions = assign({}, renderOptions || {})
return getPaths(path, relPath, extension).then(async paths => {
const suffix = paths.ext
- // const site = await siteConfigService.getAll()
const otherData = {
currentPath: ctx.path,
- // $config: config,
- // isLogin: !!ctx.session && !!ctx.session.user,
}
- // if (renderOptions.includeSite) {
- // otherData.$site = site
- // }
- // if (renderOptions.includeUser && ctx.session && ctx.session.user) {
- // otherData.$user = ctx.session.user
- // }
- const state = assign({}, otherData, locals, options, ctx.state || {})
+ const state = assign(
+ {
+ filters: {
+ "my-own-filter": function (text, options) {
+ if (options.addStart) text = "Start\n" + text
+ if (options.addEnd) text = text + "\nEnd"
+ return text
+ },
+ },
+ },
+ otherData,
+ locals,
+ options,
+ ctx.state || {}
+ )
// deep copy partials
state.partials = assign({}, options.partials || {})
// logger.debug("render `%s` with %j", paths.rel, state)
diff --git a/src/middlewares/errorHandler/index.js b/src/middlewares/errorHandler/index.js
index 816dce4..6023895 100644
--- a/src/middlewares/errorHandler/index.js
+++ b/src/middlewares/errorHandler/index.js
@@ -1,43 +1,102 @@
import { logger } from "@/logger"
-// src/plugins/errorHandler.js
-// 错误处理中间件插件
+import AuthError from "@/utils/error/AuthError"
+import BaseError from "@/utils/error/BaseError.js"
+import CommonError from "@/utils/error/CommonError.js"
+/**
+ * 格式化错误响应
+ * @param {Object} ctx - Koa上下文
+ * @param {number} status - HTTP状态码
+ * @param {string} message - 错误消息
+ * @param {string} stack - 错误堆栈(仅开发环境)
+ * @returns {Promise}
+ */
async function formatError(ctx, status, message, stack) {
const accept = ctx.accepts("json", "html", "text")
const isDev = process.env.NODE_ENV === "development"
+
+ // 确保状态码在合理范围内
+ status = status >= 100 && status < 600 ? status : 500
+
if (accept === "json") {
ctx.type = "application/json"
- ctx.body = isDev && stack ? { success: false, error: message, stack } : { success: false, error: message }
+ ctx.body = isDev && stack ?
+ { success: false, error: message, stack, status } :
+ { success: false, error: message, status }
} else if (accept === "html") {
ctx.type = "html"
await ctx.render("error/index", { status, message, stack, isDev })
} else {
ctx.type = "text"
- ctx.body = isDev && stack ? `${status} - ${message}\n${stack}` : `${status} - ${message}`
+ ctx.body = isDev && stack ?
+ `${status} - ${message}\n${stack}` :
+ `${status} - ${message}`
}
ctx.status = status
}
-export default function errorHandler() {
+/**
+ * 错误处理中间件
+ * @returns {Function} Koa中间件函数
+ */
+export default function () {
return async (ctx, next) => {
- // 拦截 Chrome DevTools 探测请求,直接返回 204
- if (ctx.path === "/.well-known/appspecific/com.chrome.devtools.json") {
- ctx.status = 204
- ctx.body = ""
- return
- }
try {
await next()
- if (ctx.status === 404) {
+ // 处理404情况 - 只有在没有设置body且状态码为404时才处理
+ if (ctx.status === 404 && !ctx.body) {
await formatError(ctx, 404, "Resource not found")
}
} catch (err) {
- logger.error(err)
+ if (err instanceof AuthError) {
+ ctx.redirect('/no-auth?from=' + err.ctx.url)
+ return
+ }
+ // 记录错误日志,包含更多上下文信息
+ logger.error({
+ message: "Unhandled error occurred",
+ error: err.message,
+ stack: err.stack,
+ url: ctx.url,
+ method: ctx.method,
+ ip: ctx.ip,
+ userAgent: ctx.headers['user-agent']
+ })
+
const isDev = process.env.NODE_ENV === "development"
- if (isDev && err.stack) {
- console.error(err.stack)
+
+ // 开发环境下在控制台输出错误堆栈
+ // if (isDev && err.stack) {
+ // console.error("\x1b[31m%s\x1b[0m", err.stack)
+ // }
+
+ // 根据错误类型设置适当的状态码和消息
+ let status = 500
+ let message = "Internal server error"
+
+ // 处理自定义错误类型
+ if (err instanceof BaseError) {
+ status = err.statusCode || 500
+ message = err.message || message
+ } else if (err.status) {
+ // 处理Koa内置错误对象
+ status = err.status
+ message = err.message || message
+ } else if (err.statusCode) {
+ // 处理其他带有状态码的错误对象
+ status = err.statusCode
+ message = err.message || message
}
- await formatError(ctx, err.statusCode || 500, err.message || err || "Internal server error", isDev ? err.stack : undefined)
+
+ // 确保状态码在合理范围内
+ status = status >= 100 && status < 600 ? status : 500
+
+ await formatError(
+ ctx,
+ status,
+ message,
+ isDev ? err.stack : undefined
+ )
}
}
-}
+}
\ No newline at end of file
diff --git a/src/middlewares/install.js b/src/middlewares/install.js
index e1b021d..7a494f0 100644
--- a/src/middlewares/install.js
+++ b/src/middlewares/install.js
@@ -14,6 +14,9 @@ import { autoRegisterControllers } from "@/utils/ForRegister.js"
import performanceMonitor from "./RoutePerformance/index.js"
import app from "@/global"
+import { SiteConfigService } from "services/SiteConfigService.js"
+import config from "config/index.js"
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const publicPath = resolve(__dirname, "../../public")
@@ -21,11 +24,36 @@ const publicPath = resolve(__dirname, "../../public")
* 注册中间件
* @param {app} app
*/
-export default app => {
- // 错误处理
- app.use(ErrorHandler())
+export default async app => {
// 响应时间
app.use(ResponseTime)
+ // 拦截 Chrome DevTools 探测请求,直接返回 204
+ app.use((ctx, next) => {
+ if (ctx.path === "/.well-known/appspecific/com.chrome.devtools.json") {
+ ctx.status = 204
+ ctx.body = ""
+ return
+ }
+ return next()
+ })
+ app.use(async (ctx, next) => {
+ ctx.set("Access-Control-Allow-Origin", "*")
+ ctx.set("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS")
+ ctx.set("Access-Control-Allow-Headers", "Content-Type,Content-Length, Authorization, Accept,X-Requested-With")
+ ctx.set("Access-Control-Allow-Credentials", true)
+ if (ctx.method == "OPTIONS") {
+ ctx.status = 200
+ }
+ return await next()
+ })
+ app.use(async (ctx, next) => {
+ // 提供全局数据
+ ctx.state.siteConfig = await SiteConfigService.getAll()
+ ctx.state.base = config.base
+ return await next()
+ })
+ // 错误处理,主要处理运行中抛出的错误
+ app.use(ErrorHandler())
// 路由性能监控(在路由处理之前)
app.use(performanceMonitor.middleware())
// session设置
@@ -49,9 +77,9 @@ export default app => {
],
blackList: [
// 禁用api请求
- "/api",
- "/api/",
- "/api/**/*",
+ // "/api",
+ // "/api/",
+ // "/api/**/*",
],
})
)
@@ -60,7 +88,7 @@ export default app => {
// 请求体解析
app.use(bodyParser())
// 自动注册控制器
- autoRegisterControllers(app, path.resolve(__dirname, "../controllers"))
+ await autoRegisterControllers(app, path.resolve(__dirname, "../controllers"))
// 注册完成之后静态资源设置
app.use(async (ctx, next) => {
if (ctx.body) return await next()
diff --git a/src/services/ArticleService.js b/src/services/ArticleService.js
new file mode 100644
index 0000000..1e7d8b0
--- /dev/null
+++ b/src/services/ArticleService.js
@@ -0,0 +1,563 @@
+import ArticleModel from "../db/models/ArticleModel.js"
+import { logger } from "../logger.js"
+
+/**
+ * 文章服务类
+ * 提供文章相关的业务逻辑
+ */
+class ArticleService {
+ /**
+ * 创建新文章
+ * @param {Object} articleData - 文章数据
+ * @returns {Promise