npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

knight-server

v1.2.4

Published

Knight server architecture library — Express/WebSocket server core with database, auth, cache support

Downloads

2,036

Readme

Knight-Server 服务插件使用手册

概述

Knight-Server 是一个基于 Express + WebSocket 的 Node.js 服务端框架,集成了 MySQLRedis 两大一等模块,通过装饰器 @OnRegister 实现路由自动注册,提供开箱即用的 HTTP/WebSocket/Database/Redis 全栈开发体验。

  • 包名: knight-server
  • 入口: dist/index.js
  • 类型声明: dist/index.d.ts

一、安装与启动

安装

npm install knight-server

最小启动示例

import { Server } from "knight-server";

const server = Server.Instance;
await server.OnStart({
    database: { /* ... */ },
    redis:    { /* ... */ },
    http:     { port: 3000 },
    websocket: {
        port: 6666,
        heartbeat_timeout_interval: 30000,
        router_timeout_interval: 60000,
    },
    serverHost: "127.0.0.1",
});

Server 是单例类,构造函数私有,通过 Server.Instance 访问。OnStartdatabase → redis → http → websocket 顺序启动各模块,重复调用会被忽略。


二、服务配置 (ServerConfig)

export interface ServerConfig {
    database:  DataBaseConfig;
    redis:     RedisConfig;
    http:      HttpConfig;
    websocket: WebSocketConfig;
    token?:    TokenConfig;   // 可选,JWT 配置
    serverHost: string;        // 服务器对外地址,如 "127.0.0.1" 或域名
}

2.1 DataBaseConfig

interface DataBaseConfig {
    host: string;
    port: number;
    user: string;
    password: string;
    database: string;
    connectionLimit: number;    // 连接池大小
    tableRelativePath: string;  // .sql 表定义文件目录(相对于项目根目录)
}

模块启动时会自动校验必填字段,并调用 SmartTable 根据 .sql 文件同步数据库表结构。

2.2 RedisConfig

interface RedisConfig {
    host: string;
    port: number;
    password?: string;
    db?: number;
    url?: string;           // 连接 URL,优先级高于 host/port
    keyPrefix?: string;     // 键前缀
    connectTimeout?: number; // 连接超时(ms)
    heartbeatInterval?: number; // 心跳间隔(ms),默认 240000(4分钟)
}

模块启动后维护两个客户端:主客户端执行命令,订阅客户端处理 Pub/Sub。内置心跳机制,每隔 heartbeatInterval 毫秒向主客户端和订阅客户端发送 PING 命令,防止服务端因空闲超时断开连接(如 Redis timeout 配置)。重连时自动恢复所有频道和模式订阅。

2.3 HttpConfig

interface HttpConfig {
    port: number;
}

2.4 WebSocketConfig

interface WebSocketConfig {
    port: number;
    heartbeat_timeout_interval: number;  // 心跳超时间隔(ms),超时客户端会被关闭(code 1001)
    router_timeout_interval: number;     // 非持久路由闲置检测间隔(ms),无客户端时自动注销
}

2.5 TokenConfig (可选)

interface TokenConfig {
    secret: string;
    accessExpiresIn?: string;   // 默认 "15m"
    refreshExpiresIn?: string;  // 默认 "7d"
}

此配置存放在 server.config.token 中供业务代码使用,框架本身不自动接入鉴权逻辑——你需要在 Router 的 OnCheck 中自行调用 TokenUtility.verify()


三、路由注册机制

3.1 @OnRegister 装饰器

import { OnRegister } from "knight-server";

@OnRegister("/api/users")
export class UserRouter extends HttpRouter { /* ... */ }

行为:

  • 将路由元数据 { path, type, permanent: true } 推入全局 RouteRegistry 数组
  • path 自动规范化为小写且以 / 开头
  • 各 Module 在 OnStart 时通过 instanceof 过滤出归属自己的 Router 并实例化
  • 装饰器标记的路由 permanent 默认为 true,不会被自动清理

3.2 手动注册 / 注销

// 动态注册
server.http.OnRegistRouter({
    path: "/api/dynamic",
    type: DynamicRouter,
    permanent: false,
});

// 动态注销
server.http.OnDegistRouter("/api/dynamic");

四、HTTP 模块

4.1 HttpRouter 抽象类

继承 HttpRouter 后需实现以下方法:

| 方法 | 签名 | 说明 | |------|------|------| | OnCheck | (server, request: Request, response: Response): Promise<CheckResult> | 鉴权/校验 | | OnGetHandler | (server, checkResult, request, response): Promise<void> | GET 请求 | | OnPostHandler | 同上 | POST 请求 | | OnPutHandler | 同上 | PUT 请求 | | OnDeleteHandler | 同上 | DELETE 请求 | | OnOptionsHandler | 同上 | OPTIONS 请求 | | OnHeadHandler | 同上 | HEAD 请求 | | OnPatchHandler | 同上 | PATCH 请求 | | OnTraceHandler | 同上 | TRACE 请求 |

4.2 调度流程

HTTP 请求 → express.all(path) 代理层 → 查找 router → OnCheck
  ├─ valid=false → 返回 401
  └─ valid=true  → 按 method 分发到对应 OnXxxHandler

4.3 示例

import { HttpRouter, OnRegister, CheckResult, TokenUtility } from "knight-server";

@OnRegister("/api/user/info")
export class UserInfoRouter extends HttpRouter {

    async OnCheck(server, request, response): Promise<CheckResult> {
        const token = request.headers.authorization?.replace("Bearer ", "");
        if (!token) return { valid: false, error: "请先登录" };
        try {
            const payload = TokenUtility.verify(token, server.config.token.secret);
            return { valid: true, payload };
        } catch {
            return { valid: false, error: "令牌无效" };
        }
    }

    async OnGetHandler(server, checkResult, request, response) {
        const users = await server.database.find("users", "id = ?", [checkResult.payload.uid]);
        response.json({ success: true, data: users.data });
    }

    async OnPostHandler(server, checkResult, request, response) {
        const result = await server.database.insert("users", request.body);
        response.json(result);
    }

    // 未实现的 method handler 会导致请求无响应,建议至少实现 OnOptionsHandler
    async OnOptionsHandler(server, checkResult, request, response) {
        response.status(204).end();
    }
}

五、WebSocket 模块

5.1 WebSocketRouter 抽象类

abstract class WebSocketRouter {
    // 鉴权检查
    abstract OnCheck(server: Server, url: URL): Promise<CheckResult>;

    // 客户端连接
    abstract OnConnect(server: Server, checkResult: CheckResult, client: WebSocketClient): Promise<void>;

    // 客户端断开
    abstract OnDisconnect(server: Server, checkResult: CheckResult, client: WebSocketClient): Promise<void>;

    // 收到消息
    abstract OnMessage(server: Server, checkResult: CheckResult, client: WebSocketClient, data: string): Promise<void>;
}

5.2 WebSocketClient 关键方法

| 方法 | 说明 | |------|------| | OnSend(data) | 向该客户端发送消息 | | OnBroadcast(data, excludeSelf?) | 向同路由下所有客户端广播 | | OnUpdateHeartbeat() | 刷新心跳时间戳(收到消息时调用;协议层 Ping 帧会自动刷新) | | OnChangeRouter(router) | 将客户端迁移到另一个路由 | | OnGetSearchParams(key) | 获取连接 URL 上的查询参数 | | OnGetCheckResultPayload<T>() | 获取鉴权时写入的 payload |

5.3 关键属性

| 属性 | 说明 | |------|------| | client.key | "ip:port" 格式的唯一标识 | | client.checkResult | 鉴权结果 |

5.4 WebSocketRouter 客户端管理

| 方法 | 说明 | |------|------| | OnGetClient(key) | 按 key 获取客户端 | | OnGetClients() | 获取所有客户端 | | OnGetClientBySearchParams(key, value) | 按 URL 参数查找 | | OnGetClientByCheckResultPayload(key, value) | 按鉴权 payload 字段查找 |

5.5 连接流程

WebSocket 连接 → 按 pathname 找到 router → router.OnHandler
  → 去重检查(ip:port)
  → OnCheck(鉴权)
  → 创建 WebSocketClient → 注册到 client pool
  → OnConnect
  → 启动心跳检测定时器

客户端发送 Ping 帧时,服务端自动回复 Pong 并刷新心跳时间戳;此外在 OnMessage 中调用 client.OnUpdateHeartbeat() 也可维持心跳。

5.6 示例

import { WebSocketRouter, WebSocketClient, OnRegister, CheckResult, Server } from "knight-server";

@OnRegister("/ws/chat")
export class ChatRouter extends WebSocketRouter {

    async OnCheck(server: Server, url: URL): Promise<CheckResult> {
        const token = url.searchParams.get("token");
        if (!token) return { valid: false, error: "缺少令牌" };
        // ... 验证 token
        return { valid: true, payload: { userId: "xxx" } };
    }

    async OnConnect(server, checkResult, client) {
        client.OnBroadcast(JSON.stringify({
            type: "join",
            userId: (checkResult.payload as any).userId,
        }));
    }

    async OnDisconnect(server, checkResult, client) {
        client.OnBroadcast(JSON.stringify({
            type: "leave",
            userId: (checkResult.payload as any).userId,
        }));
    }

    async OnMessage(server, checkResult, client, data) {
        client.OnUpdateHeartbeat();      // 刷新心跳
        client.OnBroadcast(data, true);  // 广播给同路由其他人
    }
}

六、Database 模块

6.1 访问方式

// 直接通过 server.database 调用
const result = await server.database.find("users", "age > ?", [18]);

6.2 DataBaseRouter 钩子

@OnRegister("/users")
export class UsersRouter extends DataBaseRouter {

    // 访问控制
    async OnCheck(server): Promise<CheckResult> {
        if (!server.database.isConnected()) return { valid: false, error: "数据库未连接" };
        return { valid: true };
    }

    // 插入前钩子
    async OnBeforeInsert(data: any): Promise<HookResult> {
        return { proceed: true, data };
    }

    // 更新前钩子
    async OnBeforeUpdate(data: any, where: string, params: any[]): Promise<HookResult> {
        return { proceed: true, data };
    }

    // 删除前钩子
    async OnBeforeDelete(where: string, params: any[]): Promise<HookResult> {
        return { proceed: true };
    }

    // 查询后钩子(脱敏、转换等)
    async OnAfterSelect(rows: any[]): Promise<any[]> {
        return rows.map(({ password, ...rest }) => rest);
    }
}

6.3 HookResult

interface HookResult<T = any> {
    proceed: boolean;   // true=继续, false=阻止
    data?: T;           // 修改后的数据
    error?: string;     // 阻止时的错误信息
}

6.4 CRUD 方法一览

所有方法返回 Promise<QueryResult<T>>

interface QueryResult<T = any> {
    success: boolean;
    data?: T[];           // 多行结果
    item?: T;             // 单行结果 (findOne, findById)
    affectedRows?: number; // insert/update/delete
    insertId?: number;    // insert
    error?: string;       // 失败时的错误信息
}

查询:

| 方法 | 说明 | |------|------| | find<T>(table, where?, params?) | 查询多条 | | findOne<T>(table, where, params?) | 查询单条 (含 .item) | | findById<T>(table, id, idField?) | 按 ID 查询 | | paginate<T>(table, page?, pageSize?, where?, params?, orderBy?) | 分页查询 |

写入:

| 方法 | 说明 | |------|------| | insert<T>(table, data) | 插入单条 | | insertBatch<T>(table, dataArray) | 批量插入 | | update<T>(table, data, where, whereParams?) | 条件更新 | | updateById<T>(table, id, data, idField?) | 按 ID 更新 | | delete(table, where, params?) | 条件删除 | | deleteById(table, id, idField?) | 按 ID 删除 |

高级:

| 方法 | 说明 | |------|------| | query<T>(sql, params?) | 执行自定义 SQL(绕过钩子与访问控制) | | transaction<T>(callback) | 事务执行,自动 begin/commit/rollback/release |

6.5 事务示例

const result = await server.database.transaction(async (connection) => {
    await connection.execute("UPDATE accounts SET balance = balance - 100 WHERE id = ?", [1]);
    await connection.execute("UPDATE accounts SET balance = balance + 100 WHERE id = ?", [2]);
    return { msg: "转账成功" };
});
if (!result.success) {
    console.error(result.error);  // 事务已自动回滚
}

七、Redis 模块

7.1 访问方式

await server.redis.set("key", "value");
const val = await server.redis.get("key");

7.2 心跳保活

模块启动后自动对主客户端和订阅客户端定时发送 PING 保活,防止 Redis 服务端因空闲超时(timeout)断开连接。心跳间隔由 RedisConfig.heartbeatInterval 控制(默认 4 分钟)。连接意外断开重连后,自动恢复所有频道和模式订阅,心跳也会自动恢复。

7.3 KV 操作

| 方法 | Redis 命令 | 返回值 | |------|------------|--------| | get(key) | GET | Promise<string \| {}> | | set(key, value) | SET | Promise<string \| {}> | | deleteKeys(...keys) | DEL | Promise<number> | | expire(key, seconds) | EXPIRE | Promise<number> | | exists(...keys) | EXISTS | Promise<number> | | increment(key) | INCR | Promise<number> | | incrementBy(key, n) | INCRBY | Promise<number> | | decrement(key) | DECR | Promise<number> | | decrementBy(key, n) | DECRBY | Promise<number> | | timeToLive(key) | TTL | Promise<number> | | keys(pattern) | KEYS | Promise<string[]> | | setIfNotExists(key, value) | SETNX | Promise<number> | | getAndSet(key, value) | GETSET | Promise<string \| {}> | | multiGet(...keys) | MGET | Promise<(string \| {})[]> | | multiSet(keyValues) | MSET | Promise<string \| null> |

7.4 Hash 操作

| 方法 | Redis 命令 | |------|------------| | hashSet(key, field, value) | HSET | | hashGet(key, field) | HGET | | hashGetAll(key) | HGETALL | | hashDelete(key, ...fields) | HDEL | | hashExists(key, field) | HEXISTS | | hashKeys(key) | HKEYS | | hashValues(key) | HVALS | | hashLength(key) | HLEN | | hashMultiSet(key, fieldValues) | HSET (批量) |

7.5 List 操作

| 方法 | Redis 命令 | |------|------------| | listLeftPush(key, ...elements) | LPUSH | | listRightPush(key, ...elements) | RPUSH | | listLeftPop(key) | LPOP | | listRightPop(key) | RPOP | | listRange(key, start, stop) | LRANGE | | listLength(key) | LLEN | | listRemove(key, count, element) | LREM |

7.6 Set 操作

| 方法 | Redis 命令 | |------|------------| | setAdd(key, ...members) | SADD | | setRemove(key, ...members) | SREM | | setMembers(key) | SMEMBERS | | setIsMember(key, member) | SISMEMBER | | setCardinality(key) | SCARD | | setIntersect(...keys) | SINTER | | setUnion(...keys) | SUNION | | setDifference(...keys) | SDIFF |

7.7 SortedSet 操作

| 方法 | Redis 命令 | |------|------------| | sortedSetAdd(key, ...{score, value}[]) | ZADD | | sortedSetRange(key, start, stop, withScores?) | ZRANGE | | sortedSetRangeByScore(key, min, max, withScores?) | ZRANGEBYSCORE | | sortedSetRemove(key, ...members) | ZREM | | sortedSetCardinality(key) | ZCARD | | sortedSetScore(key, member) | ZSCORE | | sortedSetRank(key, member) | ZRANK | | sortedSetReverseRank(key, member) | ZREVRANK |

7.8 Pub/Sub 操作

// 订阅频道
await server.redis.subscribe("game:events", (message, channel) => {
    console.log(`[${channel}] ${message}`);
});

// 发布消息
await server.redis.publish("game:events", JSON.stringify({ type: "start" }));

// 退订
await server.redis.unsubscribe("game:events");

// 模式订阅(通配符)
await server.redis.patternSubscribe("game:*", (message, channel) => { /* ... */ });
await server.redis.patternUnsubscribe("game:*");

7.9 RedisRouter 自动订阅

@OnRegister 的路径即频道名,RedisModule 启动后自动订阅,消息到达时调用 OnHandler

@OnRegister("/channel/notifications")
export class NotificationRouter extends RedisRouter {

    // 有人 publish 到 "/channel/notifications" 时自动触发
    async OnHandler(channel: string, message: string): Promise<void> {
        const data = JSON.parse(message);
        console.log(`收到频道 ${channel} 的消息:`, data);
    }
}

7.10 Pipeline 操作

const pipeline = server.redis.pipeline();
pipeline.set("key1", "val1")
       .get("key1")
       .deleteKeys("key2")
       .listLeftPush("queue", "item1", "item2");

const results = await pipeline.exec(server.redis.getNativeClient());

7.11 Lua 脚本

// 执行脚本
const result = await server.redis.evalScript("return redis.call('GET', KEYS[1])", ["mykey"]);

// 加载 + 缓存执行
const sha = await server.redis.scriptLoad("return redis.call('INCR', KEYS[1])");
const count = await server.redis.evalSha(sha, ["counter"]);

7.12 底层客户端访问

const native = server.redis.getNativeClient();        // 主客户端
const sub = server.redis.getNativeSubscriber();        // 订阅客户端

八、工具集 (Utility)

8.1 LogUtility

import { LogUtility } from "knight-server";

LogUtility.LogTip("提示信息");
LogUtility.LogInfo("信息");
LogUtility.LogWarning("警告");
LogUtility.LogError("错误");
LogUtility.LogSystem("系统信息");

LogUtility.Switch = false;   // 关闭日志

日志格式:[KNIGHT][标签][调用类名][yyyy-MM-dd HH:mm:ss.SSS] 消息...

调用类名通过解析 Error.stack 自动获取,无需手动传入。

8.2 TokenUtility (JWT)

import { TokenUtility } from "knight-server";

// 签发
const accessToken = TokenUtility.sign({ uid: 1 }, server.config.token.secret, "2h");
const refreshToken = TokenUtility.signRefresh({ uid: 1 }, server.config.token.secret, "7d");

// 验证(抛出异常时视为无效)
const payload = TokenUtility.verify<{ uid: number }>(accessToken, server.config.token.secret);

// 解码(不验证签名)
const decoded = TokenUtility.decode<{ uid: number }>(accessToken);

8.3 StringUtility

| 方法 | 说明 | |------|------| | empty(str) | null/undefined/"" → true | | isBlank(str) | 空或纯空白 → true | | trimToEmpty(str) | 安全 trim,null → "" | | uuid() | 生成 UUID | | random(length?) | 随机字符串,默认 16 位 | | randomNumber(length?) | 随机数字串,默认 6 位 | | format(template, params) | "Hello {name}" 模板替换 | | camelToSnake(str) | 驼峰转下划线 | | snakeToCamel(str) | 下划线转驼峰 | | joinUrl(...parts) | URL 路径拼接 |

8.4 TimeUtility

| 方法 | 说明 | |------|------| | nowTimestamp() | Date.now() | | nowUnix() | 秒级时间戳 | | formatDate(date?) | yyyy-MM-dd | | formatDateTime(date?) | yyyy-MM-dd HH:mm:ss | | sleep(ms) | 异步等待 | | startOfDay(date?) | 当天 00:00:00 | | endOfDay(date?) | 当天 23:59:59 | | addDays(date?, n) | 加/减天数 | | diffInDays(d1, d2) | 日期差 | | isExpired(date) | 是否已过期 | | timer(fn) | 测量异步执行耗时 |

8.5 NetworkUtility

import { NetworkUtility } from "knight-server";

// HTTP 请求
const res = await NetworkUtility.HTTP.OnRequest<{ data: any }>("https://api.example.com", {
    method: "post",
    data: { key: "value" },
    params: { page: 1 },
    timeout: 5000,
});

// SSE 流式请求
for await (const chunk of NetworkUtility.HTTP.OnStreamRequest("https://api.example.com/stream")) {
    console.log(chunk);
}

// 客户端 WebSocket
const ws = new NetworkUtility.Websocket();
ws.On("message", (event) => console.log(event.data));
ws.OnConnect("ws://localhost:6666/ws/chat?token=xxx");
ws.OnSend("hello");
ws.OnDisConnect();

常用枚举:

NetworkUtility.WebSocketCloseCode.CONFLICT    // 4000
NetworkUtility.WebSocketCloseCode.AUTH_FAILED // 4001

NetworkUtility.ResponseCode.UNAUTHORIZED      // 401
NetworkUtility.ResponseCode.TOO_MANY_REQUESTS // 429
NetworkUtility.ResponseCode.BUSINESS_ERROR    // 1000

九、CheckResult 与 CheckPayload

interface CheckResult<T extends CheckPayload = CheckPayload> {
    valid: boolean;
    error?: string;       // valid=false 时的错误信息
    payload?: T;          // 携带的业务数据(用户信息等)
}

interface CheckPayload { }

OnCheck 中返回 payload,后续可通过 checkResult.payload 获取,避免重复查询。

扩展 Payload 类型:

interface ChatPayload extends CheckPayload {
    userId: string;
    roomId: string;
}

// 在 OnCheck 中
return { valid: true, payload: { userId: "xxx", roomId: "room1" } as ChatPayload };

十、完整示例

import {
    Server,
    HttpRouter, WebSocketRouter, WebSocketClient,
    DataBaseRouter, RedisRouter,
    OnRegister, CheckResult, CheckPayload,
    TokenUtility, TimeUtility, LogUtility,
} from "knight-server";

// ────────── HTTP 路由 ──────────
@OnRegister("/api/user/info")
class UserInfoRouter extends HttpRouter {
    async OnCheck(server, request, response): Promise<CheckResult> {
        const token = request.headers.authorization?.replace("Bearer ", "");
        if (!token) return { valid: false, error: "请先登录" };
        try {
            const payload = TokenUtility.verify(token, server.config.token.secret);
            return { valid: true, payload };
        } catch {
            return { valid: false, error: "令牌无效" };
        }
    }

    async OnGetHandler(server, checkResult, request, response) {
        const uid = (checkResult.payload as any).uid;
        const result = await server.database.findOne("users", "id = ?", [uid]);
        if (!result.item) {
            response.status(404).json({ success: false, error: "用户不存在" });
            return;
        }
        const { password, ...safe } = result.item;
        response.json({ success: true, data: safe });
    }
}

// ────────── WebSocket 路由 ──────────
@OnRegister("/ws/chat")
class ChatRouter extends WebSocketRouter {
    async OnCheck(server, url): Promise<CheckResult> {
        const token = url.searchParams.get("token");
        if (!token) return { valid: false, error: "缺少令牌" };
        try {
            const payload = TokenUtility.verify(token, server.config.token.secret);
            return { valid: true, payload };
        } catch {
            return { valid: false, error: "令牌无效" };
        }
    }

    async OnConnect(server, checkResult, client) {
        const uid = (checkResult.payload as any).uid;
        await server.database.update("users", { online: 1 }, "id = ?", [uid]);
        client.OnBroadcast(JSON.stringify({ type: "join", uid }));
    }

    async OnDisconnect(server, checkResult, client) {
        const uid = (checkResult.payload as any).uid;
        await server.database.update("users", { online: 0, last_seen: TimeUtility.formatDateTime() }, "id = ?", [uid]);
        client.OnBroadcast(JSON.stringify({ type: "leave", uid }));
    }

    async OnMessage(server, checkResult, client, data) {
        client.OnUpdateHeartbeat();
        client.OnBroadcast(data, true);
    }
}

// ────────── DB 路由 (钩子) ──────────
@OnRegister("/users")
class UsersRouter extends DataBaseRouter {
    async OnBeforeInsert(data) {
        data.created_at = TimeUtility.formatDateTime();
        return { proceed: true, data };
    }

    async OnAfterSelect(rows) {
        return rows.map(({ password, ...rest }) => rest);
    }
}

// ────────── Redis 路由 (Pub/Sub) ──────────
@OnRegister("/channel/notifications")
class NotificationRouter extends RedisRouter {
    async OnHandler(channel, message) {
        LogUtility.LogInfo(`[${channel}] 收到消息:`, message);
    }
}

// ────────── 启动 ──────────
(async () => {
    const server = Server.Instance;
    await server.OnStart({
        database: {
            host: "localhost",
            port: 3306,
            user: "root",
            password: "your_password",
            database: "myapp",
            connectionLimit: 10,
            tableRelativePath: "./src/database/tables",
        },
        redis: {
            host: "localhost",
            port: 6379,
        },
        http: {
            port: 3000,
        },
        websocket: {
            port: 6666,
            heartbeat_timeout_interval: 30000,
            router_timeout_interval: 60000,
        },
        token: {
            secret: "your-jwt-secret-key",
            accessExpiresIn: "2h",
        },
        serverHost: "127.0.0.1",
    });

    console.log("[KNIGHT] 服务启动完成");
})();

十一、设计要点

  1. 所有业务 Router 必须用 @OnRegister 装饰,否则 Module 无法发现和注册它
  2. Router 归属由 instanceof 判断,继承正确的基类(HttpRouter / WebSocketRouter / DataBaseRouter / RedisRouter)即可
  3. WebSocket 心跳由协议层 Ping/Pong + 业务层共同维持:客户端发送 Ping 帧时服务端自动回复 Pong 并刷新心跳;在 OnMessage 中调用 client.OnUpdateHeartbeat() 也是备选方案。超时客户端会被自动断开
  4. Database 的钩子可以阻止或改写数据,返回 { proceed: false } 可拦截操作
  5. server.config 在所有 Router 中都可通过 this.server 访问(继承自 AbstractObject
  6. Express 中间件已内置:CORS、JSON body 解析(50MB)、URL-encoded 解析(50MB)