diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..fec5140 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,29 @@ +# 使用官方 Bun 运行时的轻量级镜像 +FROM oven/bun:alpine as base + +WORKDIR /app + +# 仅复制生产依赖相关文件 +COPY package.json bun.lockb knexfile.mjs .npmrc ./ + +# 安装依赖(生产环境) +RUN bun install --production + +# 复制应用代码和静态资源 +COPY src ./src +COPY public ./public + +COPY entrypoint.sh ./entrypoint.sh +RUN chmod +x ./entrypoint.sh + +# 如需数据库文件(如 SQLite),可挂载到宿主机 +VOLUME /app/database + +# 启动命令(如有端口需求可暴露端口) +EXPOSE 3000 + +# 健康检查:每30秒检查一次服务端口,3次失败则容器为unhealthy +HEALTHCHECK --interval=30s --timeout=5s --retries=3 \ + CMD wget --spider -q http://localhost:3000/ || exit 1 + +ENTRYPOINT ["./entrypoint.sh"] diff --git a/bun.lockb b/bun.lockb index 2739946..7be0543 100644 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/database/.gitkeep b/database/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/database/development.sqlite3 b/database/development.sqlite3 index 5238449..3f03792 100644 Binary files a/database/development.sqlite3 and b/database/development.sqlite3 differ diff --git a/database/development.sqlite3-shm b/database/development.sqlite3-shm index 2553477..30b5938 100644 Binary files a/database/development.sqlite3-shm and b/database/development.sqlite3-shm differ diff --git a/database/development.sqlite3-wal b/database/development.sqlite3-wal index bea45f3..8e6f511 100644 Binary files a/database/development.sqlite3-wal and b/database/development.sqlite3-wal differ diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..57d99b5 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,18 @@ +#!/bin/sh +set -e + +# 数据库文件路径(可根据实际环境调整) +DB_FILE=./database/db.sqlite3 + +# 如果数据库文件不存在,先 migrate 再 seed +if [ ! -f "$DB_FILE" ]; then + echo "Database not found, running migration and seed..." + bun run npx knex migrate:latest + bun run npx knex seed:run +else + echo "Database exists, running migration only..." + bun run npx knex migrate:latest +fi + +# 启动主服务 +exec bun src/main.js diff --git a/package.json b/package.json index 20f4fe0..12ba189 100644 --- a/package.json +++ b/package.json @@ -12,12 +12,16 @@ }, "devDependencies": { "@types/bun": "latest", - "@types/node": "^24.0.1", - "knex": "^3.1.0" + "@types/node": "^24.0.1" }, "dependencies": { + "bcryptjs": "^3.0.2", + "jsonwebtoken": "^9.0.0", + "knex": "^3.1.0", "koa": "^3.0.0", + "koa-bodyparser": "^4.4.1", "log4js": "^6.9.1", + "minimatch": "^9.0.0", "module-alias": "^2.2.3", "node-cron": "^4.1.0", "path-to-regexp": "^8.2.0", diff --git a/public/aa.txt b/public/aa.txt deleted file mode 100644 index 6a12326..0000000 --- a/public/aa.txt +++ /dev/null @@ -1 +0,0 @@ -sada \ No newline at end of file diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..535e0ac --- /dev/null +++ b/public/index.html @@ -0,0 +1,101 @@ + + + + + + 登录 / 注册 + + + +
+
+
登录
+
注册
+
+
+
+
+ + +
+
+ + +
+ +
+ +
+ + + diff --git a/src/controllers/JobController.js b/src/controllers/JobController.js index be3cb2b..3348cf8 100644 --- a/src/controllers/JobController.js +++ b/src/controllers/JobController.js @@ -1,41 +1,56 @@ // Job Controller 示例:如何调用 service 层动态控制和查询定时任务 import JobService from "services/JobService.js" -import Router from "utils/router.js" +import { formatResponse } from "utils/helper.js" -class JobController { - static routes() { - const router = new Router({ prefix: "/api/jobs" }) - router.get("/", JobController.list) - router.post("/start/:id", JobController.start) - router.post("/stop/:id", JobController.stop) - router.post("/update/:id", JobController.updateCron) - return router - } +const jobService = new JobService() - static async list(ctx) { - ctx.body = JobService.listJobs() +export const list = async (ctx) => { + try { + const data = jobService.listJobs() + ctx.body = formatResponse(true, data) + } catch (err) { + ctx.body = formatResponse(false, null, err.message || "获取任务列表失败") } +} - static async start(ctx) { - const { id } = ctx.params - JobService.startJob(id) - ctx.body = { success: true, message: `${id} 任务已启动` } +export const start = async (ctx) => { + const { id } = ctx.params + try { + jobService.startJob(id) + ctx.body = formatResponse(true, null, null, `${id} 任务已启动`) + } catch (err) { + ctx.body = formatResponse(false, null, err.message || "启动任务失败") } +} - static async stop(ctx) { - const { id } = ctx.params - JobService.stopJob(id) - ctx.body = { success: true, message: `${id} 任务已停止` } +export const stop = async (ctx) => { + const { id } = ctx.params + try { + jobService.stopJob(id) + ctx.body = formatResponse(true, null, null, `${id} 任务已停止`) + } catch (err) { + ctx.body = formatResponse(false, null, err.message || "停止任务失败") } +} - static async updateCron(ctx) { - const { id } = ctx.params - const { cronTime } = ctx.request.body - JobService.updateJobCron(id, cronTime) - ctx.body = { success: true, message: `${id} 任务频率已修改` } +export const updateCron = async (ctx) => { + const { id } = ctx.params + const { cronTime } = ctx.request.body + try { + jobService.updateJobCron(id, cronTime) + ctx.body = formatResponse(true, null, null, `${id} 任务频率已修改`) + } catch (err) { + ctx.body = formatResponse(false, null, err.message || "修改任务频率失败") } } -export default JobController - -// 你可以在路由中引入这些 controller 方法,实现接口调用 +// 路由注册示例 +import Router from "utils/router.js" +export function createRoutes() { + const router = new Router({ prefix: "/api/jobs" }) + router.get("/", list) + router.post("/start/:id", start) + router.post("/stop/:id", stop) + router.post("/update/:id", updateCron) + return router +} diff --git a/src/controllers/StatusController.js b/src/controllers/StatusController.js index f6775ed..5239ec0 100644 --- a/src/controllers/StatusController.js +++ b/src/controllers/StatusController.js @@ -1,22 +1,16 @@ -import Router from 'utils/router.js'; +import { formatResponse } from "utils/helper.js" -class StatusController { - static routes() { - const v1 = new Router({ prefix: "/api/v1" }) - - // 组内中间件 - v1.use((ctx, next) => { - ctx.set("X-API-Version", "v1") - return next() - }) - - v1.get("/status", StatusController.status) - return v1 - } - - static async status(ctx) { - ctx.body = "OK" - } +export const status = async (ctx) => { + ctx.body = "OK" } -export default StatusController +import Router from "utils/router.js" +export function createRoutes() { + const v1 = new Router({ prefix: "/api/v1" }) + v1.use((ctx, next) => { + ctx.set("X-API-Version", "v1") + return next() + }) + v1.get("/status", status) + return v1 +} diff --git a/src/controllers/userController.js b/src/controllers/userController.js index 91a9527..b00549d 100644 --- a/src/controllers/userController.js +++ b/src/controllers/userController.js @@ -1,23 +1,44 @@ -import UserService from 'services/UserService.js'; -import Router from 'utils/router.js'; +import UserService from "services/UserService.js" +import { formatResponse } from "utils/helper.js" -class UserController { - static routes() { - let router = new Router({ prefix: '/api' }); - router.get('/hello', UserController.hello); - router.get('/user/:id', UserController.getUser); - return router; - } +const userService = new UserService() - static async hello(ctx) { - ctx.body = 'Hello World'; - } +export const hello = async (ctx) => { + ctx.body = formatResponse(true, "Hello World") +} + +export const getUser = async (ctx) => { + const user = await userService.getUserById(ctx.params.id) + ctx.body = formatResponse(true, user) +} - static async getUser(ctx) { - // 调用 service 层获取用户 - const user = await UserService.getUserById(ctx.params.id); - ctx.body = user; - } +export const register = async (ctx) => { + try { + const { username, email, password } = ctx.request.body + const user = await userService.register({ username, email, password }) + ctx.body = formatResponse(true, user) + } catch (err) { + ctx.body = formatResponse(false, null, err.message) + } } -export default UserController; \ No newline at end of file +export const login = async (ctx) => { + try { + const { username, email, password } = ctx.request.body + const result = await userService.login({ username, email, password }) + ctx.body = formatResponse(true, result) + } catch (err) { + ctx.body = formatResponse(false, null, err.message) + } +} + +// 路由注册示例 +import Router from "utils/router.js" +export function createRoutes() { + const router = new Router({ prefix: "/api" }) + router.get("/hello", hello) + router.get("/user/:id", getUser) + router.post("/register", register) + router.post("/login", login) + return router +} diff --git a/src/db/migrations/20250616065041_create_users_table.mjs b/src/db/migrations/20250616065041_create_users_table.mjs index 56f7418..f73fcae 100644 --- a/src/db/migrations/20250616065041_create_users_table.mjs +++ b/src/db/migrations/20250616065041_create_users_table.mjs @@ -7,6 +7,7 @@ export const up = async knex => { table.increments("id").primary() // 自增主键 table.string("username", 100).notNullable() // 字符串字段(最大长度100) table.string("email", 100).unique().notNullable() // 唯一邮箱 + table.string("password", 100).unique() // 密码 table.integer("age").unsigned() // 无符号整数 table.timestamp("created_at").defaultTo(knex.fn.now()) // 创建时间 table.timestamp("updated_at").defaultTo(knex.fn.now()) // 更新时间 diff --git a/src/db/models/UserModel.js b/src/db/models/UserModel.js index f263586..19de0ac 100644 --- a/src/db/models/UserModel.js +++ b/src/db/models/UserModel.js @@ -20,6 +20,13 @@ class UserModel { static async delete(id) { return db("users").where("id", id).del() } + + static async findByUsername(username) { + return db("users").where("username", username).first() + } + static async findByEmail(email) { + return db("users").where("email", email).first() + } } export default UserModel diff --git a/src/main.js b/src/main.js index b6f4296..65e5d4b 100644 --- a/src/main.js +++ b/src/main.js @@ -8,14 +8,16 @@ import os from "os" import log4js from "log4js" // 应用插件与自动路由 -import LoadPlugins from "./plugins/install.js" +import LoadMiddlewares from "./middlewares/install.js" import { autoRegisterControllers } from "utils/autoRegister.js" +import bodyParser from "koa-bodyparser" const logger = log4js.getLogger() const app = new Koa() +app.use(bodyParser()); // 注册插件 -LoadPlugins(app) +LoadMiddlewares(app) // 自动注册所有 controller autoRegisterControllers(app) diff --git a/src/middlewares/Auth/auth.js b/src/middlewares/Auth/auth.js new file mode 100644 index 0000000..779d3fd --- /dev/null +++ b/src/middlewares/Auth/auth.js @@ -0,0 +1,68 @@ +// JWT 鉴权中间件,支持白名单和黑名单,白名单/黑名单支持glob语法,白名单可指定是否校验权限(auth: true/false/"try") +import jwt from "./jwt" +import { minimatch } from "minimatch" + +export const JWT_SECRET = process.env.JWT_SECRET || "jwt-demo-secret" + +function matchList(list, path) { + for (const item of list) { + if (typeof item === "string" && minimatch(path, item)) { + return { matched: true, auth: false } + } + if (typeof item === "object" && minimatch(path, item.pattern)) { + return { matched: true, auth: item.auth } + } + } + return { matched: false } +} + +function verifyToken(ctx) { + const token = ctx.headers["authorization"]?.replace(/^Bearer\s/, "") + if (!token) return { ok: false } + try { + ctx.state.user = jwt.verify(token, JWT_SECRET) + return { ok: true } + } catch { + ctx.state.user = undefined + return { ok: false } + } +} + +export default function authMiddleware(options = { + whiteList: [], + blackList: [] +}) { + return async (ctx, next) => { + // 黑名单优先生效 + if (matchList(options.blackList, ctx.path).matched) { + ctx.status = 403 + ctx.body = { success: false, error: "禁止访问" } + return + } + // 白名单处理 + const white = matchList(options.whiteList, ctx.path) + if (white.matched) { + if (white.auth === false) { + return await next() + } + if (white.auth === "try") { + verifyToken(ctx) // token可选,校验失败不报错 + return await next() + } + // true 或其他情况,必须有token + if (!verifyToken(ctx).ok) { + ctx.status = 401 + ctx.body = { success: false, error: "未登录或token缺失或无效" } + return + } + return await next() + } + // 非白名单,必须有token + if (!verifyToken(ctx).ok) { + ctx.status = 401 + ctx.body = { success: false, error: "未登录或token缺失或无效" } + return + } + await next() + } +} diff --git a/src/middlewares/Auth/index.js b/src/middlewares/Auth/index.js new file mode 100644 index 0000000..7e8009b --- /dev/null +++ b/src/middlewares/Auth/index.js @@ -0,0 +1,3 @@ +// 统一导出所有中间件 +import auth from "./auth.js" +export { auth } diff --git a/src/middlewares/Auth/jwt.js b/src/middlewares/Auth/jwt.js new file mode 100644 index 0000000..0af32e5 --- /dev/null +++ b/src/middlewares/Auth/jwt.js @@ -0,0 +1,3 @@ +// 兼容性导出,便于后续扩展 +import jwt from "jsonwebtoken" +export default jwt diff --git a/src/plugins/ResponseTime/index.js b/src/middlewares/ResponseTime/index.js similarity index 100% rename from src/plugins/ResponseTime/index.js rename to src/middlewares/ResponseTime/index.js diff --git a/src/plugins/Send/index.js b/src/middlewares/Send/index.js similarity index 100% rename from src/plugins/Send/index.js rename to src/middlewares/Send/index.js diff --git a/src/plugins/Send/resolve-path.js b/src/middlewares/Send/resolve-path.js similarity index 100% rename from src/plugins/Send/resolve-path.js rename to src/middlewares/Send/resolve-path.js diff --git a/src/plugins/errorHandler/index.js b/src/middlewares/errorHandler/index.js similarity index 100% rename from src/plugins/errorHandler/index.js rename to src/middlewares/errorHandler/index.js diff --git a/src/middlewares/install.js b/src/middlewares/install.js new file mode 100644 index 0000000..0db2696 --- /dev/null +++ b/src/middlewares/install.js @@ -0,0 +1,33 @@ +import ResponseTime from "./ResponseTime" +import Send from "./Send" +import { resolve } from "path" +import { fileURLToPath } from "url" +import path from "path" +import errorHandler from "./errorHandler" +import { auth } from "./Auth" + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const publicPath = resolve(__dirname, "../../public") + +export default app => { + app.use(errorHandler()) + app.use(ResponseTime) + app.use( + auth({ + whiteList: [ + { pattern: "/", auth: "try" }, + "/api/login", + "/api/register" + ], + blackList: [], + }) + ) + app.use(async (ctx, next) => { + try { + await Send(ctx, ctx.path, { root: publicPath }) + } catch (err) { + if (err.status !== 404) throw err + } + await next() + }) +} diff --git a/src/plugins/install.js b/src/plugins/install.js deleted file mode 100644 index 7ef8498..0000000 --- a/src/plugins/install.js +++ /dev/null @@ -1,23 +0,0 @@ -import ResponseTime from "./ResponseTime"; -import Send from "./Send"; -import { resolve } from 'path'; -import { fileURLToPath } from 'url'; -import path from "path"; -import errorHandler from './errorHandler'; - - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const publicPath = resolve(__dirname, '../../public'); - -export default (app)=>{ - app.use(errorHandler()); - app.use(ResponseTime) - app.use(async (ctx, next) => { - try { - await Send(ctx, ctx.path, { root: publicPath }); - } catch (err) { - if (err.status !== 404) throw err; - } - await next(); - }) -} \ No newline at end of file diff --git a/src/services/JobService.js b/src/services/JobService.js index 1649aeb..35a04a3 100644 --- a/src/services/JobService.js +++ b/src/services/JobService.js @@ -1,18 +1,18 @@ -import jobs from '../jobs'; +import jobs from "../jobs" class JobService { - static startJob(id) { - return jobs.start(id); - } - static stopJob(id) { - return jobs.stop(id); - } - static updateJobCron(id, cronTime) { - return jobs.updateCronTime(id, cronTime); - } - static listJobs() { - return jobs.list(); - } + startJob(id) { + return jobs.start(id) + } + stopJob(id) { + return jobs.stop(id) + } + updateJobCron(id, cronTime) { + return jobs.updateCronTime(id, cronTime) + } + listJobs() { + return jobs.list() + } } -export default JobService; \ No newline at end of file +export default JobService diff --git a/src/services/userService.js b/src/services/userService.js index 1bd0a77..960ff1d 100644 --- a/src/services/userService.js +++ b/src/services/userService.js @@ -1,39 +1,74 @@ -// src/services/userService.js -// 用户相关业务逻辑 - -import UserModel from 'db/models/UserModel.js'; +import UserModel from "db/models/UserModel.js" +import { hashPassword, comparePassword } from "utils/bcrypt.js" +import { JWT_SECRET } from "@/middlewares/Auth/auth.js" +import jwt from "@/middlewares/Auth/jwt.js" class UserService { - static async getUserById(id) { - // 这里可以调用数据库模型 - // 示例返回 - return { id, name: `User_${id}` }; - } - - // 获取所有用户 - static async getAllUsers() { - return await UserModel.findAll(); - } - - // 创建新用户 - static async createUser(data) { - if (!data.name) throw new Error('用户名不能为空'); - return await UserModel.create(data); - } - - // 更新用户 - static async updateUser(id, data) { - const user = await UserModel.findById(id); - if (!user) throw new Error('用户不存在'); - return await UserModel.update(id, data); - } - - // 删除用户 - static async deleteUser(id) { - const user = await UserModel.findById(id); - if (!user) throw new Error('用户不存在'); - return await UserModel.delete(id); - } + async getUserById(id) { + // 这里可以调用数据库模型 + // 示例返回 + return { id, name: `User_${id}` } + } + + // 获取所有用户 + async getAllUsers() { + return await UserModel.findAll() + } + + // 创建新用户 + async createUser(data) { + if (!data.name) throw new Error("用户名不能为空") + return await UserModel.create(data) + } + + // 更新用户 + async updateUser(id, data) { + const user = await UserModel.findById(id) + if (!user) throw new Error("用户不存在") + return await UserModel.update(id, data) + } + + // 删除用户 + async deleteUser(id) { + const user = await UserModel.findById(id) + if (!user) throw new Error("用户不存在") + return await UserModel.delete(id) + } + + // 注册新用户 + async register(data) { + if (!data.username || !data.email || !data.password) throw new Error("用户名、邮箱和密码不能为空") + const existUser = await UserModel.findByUsername(data.username) + if (existUser) throw new Error("用户名已存在") + const existEmail = await UserModel.findByEmail(data.email) + if (existEmail) throw new Error("邮箱已被注册") + // 密码加密 + const hashed = await hashPassword(data.password) + + const user = await UserModel.create({ ...data, password: hashed }) + // 返回脱敏信息 + const { password, ...userInfo } = Array.isArray(user) ? user[0] : user + return userInfo + } + + // 登录 + async login({ username, email, password }) { + let user + if (username) { + user = await UserModel.findByUsername(username) + } else if (email) { + user = await UserModel.findByEmail(email) + } + if (!user) throw new Error("用户不存在") + // 校验密码 + const ok = await comparePassword(password, user.password) + if (!ok) throw new Error("密码错误") + // 生成token + const token = jwt.sign({ id: user.id, username: user.username }, JWT_SECRET, { expiresIn: "2h" }) + // 返回token和用户信息 + const { password: pwd, ...userInfo } = user + return { token, user: userInfo } + } } -export default UserService; +export default UserService diff --git a/src/utils/BaseSingleton.js b/src/utils/BaseSingleton.js new file mode 100644 index 0000000..9705647 --- /dev/null +++ b/src/utils/BaseSingleton.js @@ -0,0 +1,37 @@ +// 抽象基类,使用泛型来正确推导子类类型 +class BaseSingleton { + static _instance + + constructor() { + if (this.constructor === BaseSingleton) { + throw new Error("禁止直接实例化 BaseOne 抽象类") + } + + if (this.constructor._instance) { + throw new Error("构造函数私有化失败,禁止重复 new") + } + + // this.constructor 是子类,所以这里设为 instance + this.constructor._instance = this + } + + static getInstance() { + const clazz = this + if (!clazz._instance) { + const self = new this() + const handler = { + get: function (target, prop) { + const value = Reflect.get(target, prop) + if (typeof value === "function") { + return value.bind(target) + } + return Reflect.get(target, prop) + }, + } + clazz._instance = new Proxy(self, handler) + } + return clazz._instance + } +} + +export { BaseSingleton } diff --git a/src/utils/autoRegister.js b/src/utils/autoRegister.js index 4390aba..64b7b30 100644 --- a/src/utils/autoRegister.js +++ b/src/utils/autoRegister.js @@ -26,7 +26,7 @@ export function autoRegisterControllers(app, controllersDir = path.resolve(__dir } catch (e) { controller = (await import(fullPath)).default } - const routes = controller.routes || controller.default?.routes || controller.default || controller + const routes = controller.createRoutes || controller.default?.createRoutes || controller.default || controller // 判断 routes 方法参数个数,支持自动适配 if (typeof routes === "function") { allRouter.push(routes()) diff --git a/src/utils/bcrypt.js b/src/utils/bcrypt.js new file mode 100644 index 0000000..4c26d52 --- /dev/null +++ b/src/utils/bcrypt.js @@ -0,0 +1,11 @@ +// 密码加密与校验工具 +import bcrypt from "bcryptjs" + +export async function hashPassword(password) { + const salt = await bcrypt.genSalt(10) + return bcrypt.hash(password, salt) +} + +export async function comparePassword(password, hash) { + return bcrypt.compare(password, hash) +} diff --git a/src/utils/helper.js b/src/utils/helper.js new file mode 100644 index 0000000..a1903ee --- /dev/null +++ b/src/utils/helper.js @@ -0,0 +1,4 @@ + +export function formatResponse(success, data = null, error = null) { + return { success, error, data } +}