v0.5.0: 重新组织目录结构,按职责拆分

- Shrimp.ts (875 行) 按类拆分到 Shrimp/(7 文件: Shrimp, ShrimpModule, ShrimpTableModule, ShrimpPower, ShrimpCrud, UserRoleModule, OperationLogModule)
- forMongo/: MongoShrimpCrud + MongoShrimp(原 ShrimpServer 的 mongo 装配部分,无 express)
- forExpress/: buildShrimpRouter + jwtAuth + types
- main/: index(0 依赖核心) + expmon(express+mongo 整合,导出 ShrimpServer) + types(纯协议)
- 入口子路径 server → expmon(express+mongo 整合方案命名更准确)
- 删除冗余的 index.ts / core.ts(合并为 main/index.ts)
This commit is contained in:
2026-05-23 19:44:02 +08:00
parent e8fc126fa6
commit ca283c6733
27 changed files with 1059 additions and 1128 deletions
+165
View File
@@ -0,0 +1,165 @@
/**
* 把 ShrimpServer 内联的 Express routes 抽成纯函数。
*
* routes 完全不知道:
* - 数据库类型(只通过 dataSource 抽象访问)
* - JWT(只通过 ITokenAuth 注入)
* - 自己的字段名(通过 IUserAccessor 适配)
*/
import { Router, Request, Response, NextFunction } from "express"
import { Shrimp } from "../Shrimp/Shrimp"
import { ShrimpCode, IMeResult, IShrimpOperator, ShrimpError } from "../IShrimpType"
import type { IShrimpRouterDeps } from "./types"
declare global {
namespace Express {
interface Request {
user: { _id: string; name: string; power: Set<string>; roleId: string[]; isSuperAdmin: boolean }
}
}
}
export function buildShrimpRouter(deps: IShrimpRouterDeps): Router {
const {
shrimp, userModule, roleModule, operationLogModule,
tokenAuth, config, globalOptions, forcedOptionAccess, userAccessor,
} = deps
const router = Router()
router.get("/config", (_req: Request, res: Response) => {
res.json(shrimp.success(config))
})
router.post("/login", async (req: Request, res: Response) => {
const ip = req.ip || ""
const { username, password } = req.body
const args = [{ username, password }]
try {
const user = await userModule.getUser(username, password)
const token = tokenAuth.sign(userAccessor.getJwtPayload(user))
operationLogModule.record(user._id, ip, ["login"], args, true)
res.json(shrimp.success({ token }))
} catch (error: any) {
const msg = error instanceof ShrimpError ? error.tip : { t: "shrimp.loginFailed" }
const errMsg = error instanceof ShrimpError ? JSON.stringify(error.tip) : (error?.message || "")
operationLogModule.record(null, ip, ["login"], args, false, errMsg)
res.json(shrimp.fail(ShrimpCode.FAIL, msg))
}
})
router.use(async (req: Request, res: Response, next: NextFunction) => {
try {
const auth = req.headers.authorization
if (!auth || !auth.startsWith("Bearer ")) {
return res.json(shrimp.fail(ShrimpCode.UNAUTHORIZED, { t: "shrimp.relogin" }, "No token"))
}
const token = auth.split(" ")[1]
const decoded = tokenAuth.verify(token)
const user = await userModule.dataSource.get(decoded._id)
if (!user || !userAccessor.isUserActive(user)) {
return res.json(shrimp.fail(ShrimpCode.UNAUTHORIZED, { t: "shrimp.userDisabled" }, "User disabled"))
}
const roleIds = userAccessor.getUserRoleIds(user)
const roles = await Promise.all(roleIds.map(id => roleModule.dataSource.get(id)))
const isSuperAdmin = roles.some(r => userAccessor.isRoleSuperAdmin(r))
const powers = isSuperAdmin
? new Set((await shrimp.allPower()).map(p => p.key))
: new Set(roles.flatMap(r => userAccessor.getRolePowers(r)))
req.user = { _id: user._id, name: userAccessor.getUserDisplayName(user), power: powers, roleId: roleIds, isSuperAdmin }
next()
} catch (error: any) {
const msg = error instanceof ShrimpError ? error.tip : { t: "shrimp.relogin" }
return res.json(shrimp.fail(ShrimpCode.UNAUTHORIZED, msg))
}
})
router.get("/me", async (req: Request, res: Response) => {
try {
const moduleInfo = await userModule.getModule(req.user._id, roleModule)
const user = await userModule.dataSource.get(req.user._id)
if (!user) {
return res.json(shrimp.fail(ShrimpCode.NOT_FOUND, { t: "shrimp.userNotFound" }))
}
const roleIds = userAccessor.getUserRoleIds(user)
const roles = await Promise.all(roleIds.map(id => roleModule.dataSource.get(id)))
const isSuperAdmin = roles.some(r => userAccessor.isRoleSuperAdmin(r))
const powers = isSuperAdmin
? (await shrimp.allPower()).map(p => p.key)
: roles.flatMap(r => userAccessor.getRolePowers(r))
const filteredMenuTree = shrimp.filterMenuTree(shrimp.getMenuTree(), powers)
const neededKeys = new Set<string>()
for (const mod of moduleInfo) {
for (const field of mod.fields || []) {
if (typeof field.options === "string") {
neededKeys.add(field.options)
}
}
}
const filteredOptions: Record<string, any> = {}
for (const key of neededKeys) {
if (globalOptions[key] != null) {
filteredOptions[key] = globalOptions[key]
}
}
for (const [key, access] of forcedOptionAccess) {
if (globalOptions[key] != null) {
if (isSuperAdmin || powers.includes(access.powerKey)) {
filteredOptions[key] = globalOptions[key]
}
}
}
res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate")
res.json(shrimp.success<IMeResult>({
nickname: userAccessor.getUserDisplayName(user),
moduleInfo,
menuTree: filteredMenuTree,
options: filteredOptions,
}))
} catch (error: any) {
if (error instanceof ShrimpError) {
res.json(shrimp.fail(error.code, error.tip))
} else {
res.json(shrimp.fail(ShrimpCode.FAIL, { t: "shrimp.serverError" }, error.message))
}
}
})
router.post("/*path", async (req: Request, res: Response) => {
const raw = req.params.path
const path = (Array.isArray(raw) ? raw.join("/") : raw).split("/").filter(Boolean)
const args = Array.isArray(req.body) ? req.body : [req.body]
const ip = req.ip || ""
const shouldLog = operationLogModule.shouldLog(path)
try {
if (path.length === 0) {
return res.json(shrimp.fail(ShrimpCode.FAIL, { t: "shrimp.emptyPath" }))
}
const powers = req.user.power
if (!shrimp.checkEvent(powers, path)) {
return res.json(shrimp.fail(ShrimpCode.FORBIDDEN, { t: "shrimp.forbidden" }))
}
const operator: IShrimpOperator = {
userId: req.user._id,
roleId: req.user.roleId,
isSuperAdmin: req.user.isSuperAdmin,
}
const result = await Shrimp.asyncLocalStorage.run(operator, () => shrimp.call(path, ...args))
if (shouldLog) operationLogModule.record(req.user._id, ip, path, args, true)
res.json(shrimp.success(result))
} catch (error: any) {
if (shouldLog) {
const errMsg = error instanceof ShrimpError ? JSON.stringify(error.tip) : (error?.message || "")
operationLogModule.record(req.user?._id ?? null, ip, path, args, false, errMsg)
}
if (error instanceof ShrimpError) {
res.json(shrimp.fail(error.code, error.tip))
} else {
res.json(shrimp.fail(ShrimpCode.SERVER_ERROR, { t: "shrimp.serverError" }, error.message))
}
}
})
return router
}
+30
View File
@@ -0,0 +1,30 @@
/**
* JWT 实现 ITokenAuth — buildShrimpRouter 的默认 token 适配器
*
* 通过工厂函数注入 secret 和 expiresIn,routes 完全不需要 import jsonwebtoken。
*/
import jsonwebtoken from "jsonwebtoken"
import type { ITokenAuth } from "./types"
export interface IJwtTokenAuthOptions {
secret: string
/** 过期时间,默认 '7d' */
expiresIn?: string | number
}
export function createJwtTokenAuth(secretOrOptions: string | IJwtTokenAuthOptions): ITokenAuth {
const opts: IJwtTokenAuthOptions = typeof secretOrOptions === "string"
? { secret: secretOrOptions }
: secretOrOptions
const expiresIn = opts.expiresIn ?? "7d"
return {
sign(payload) {
return jsonwebtoken.sign(payload, opts.secret, { expiresIn } as jsonwebtoken.SignOptions)
},
verify(token) {
return jsonwebtoken.verify(token, opts.secret) as Record<string, any>
},
}
}
+49
View File
@@ -0,0 +1,49 @@
/**
* forExpress 子层共用接口
*
* 让 buildShrimpRouter 不直接依赖 ShrimpServer 类。
*/
import type { Router } from "express"
import type { Shrimp } from "../Shrimp/Shrimp"
import type { UserModule, RoleModule } from "../Shrimp/UserRoleModule"
import type { OperationLogModule } from "../Shrimp/OperationLogModule"
import type { RootShrimpPower } from "../Shrimp/ShrimpPower"
import type { IUser, IRole, WithId } from "../IShrimpType"
/**
* Token 鉴权抽象 — 替代 JWT 直接调用
* 默认 createJwtTokenAuth 用 jsonwebtoken;可换成 session / cookie / 自定义签名。
*/
export interface ITokenAuth {
sign(payload: Record<string, any>): string
verify(token: string): Record<string, any>
}
/**
* 用户/角色字段访问器 — 让 routes 不直接知道 user/role 的字段名。
* 默认实现读 IUser / IRole 标准字段;消费方覆盖即可适配自己的 schema。
*/
export interface IUserAccessor {
getJwtPayload(user: WithId<IUser>): Record<string, any>
isUserActive(user: WithId<IUser>): boolean
getUserRoleIds(user: WithId<IUser>): string[]
isRoleSuperAdmin(role: WithId<IRole> | null): boolean
getRolePowers(role: WithId<IRole> | null): string[]
getUserDisplayName(user: WithId<IUser>): string
}
/** buildShrimpRouter 需要的全部协作者 */
export interface IShrimpRouterDeps {
shrimp: Shrimp
userModule: UserModule
roleModule: RoleModule
operationLogModule: OperationLogModule
tokenAuth: ITokenAuth
config: Record<string, any>
globalOptions: Record<string, any>
forcedOptionAccess: Map<string, RootShrimpPower>
userAccessor: IUserAccessor
}
export type { Router }