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
+84
View File
@@ -0,0 +1,84 @@
/**
* OperationLogModule
* 操作日志子表 - 挂在 userModule 下,记录"某用户做了什么"
* - 默认只记 mutation(add/edit/delete/clear/import) + login
* - args 自动脱敏 + JSON.stringify + 截断到 2000 字符
* - 日志失败不影响主调用
* - 只暴露 list/get,不开 add/edit/delete 防止误改
*/
import { IFieldConfig, IOperationLog, WithId, ILabel } from "../IShrimpType"
import { Shrimp } from "./Shrimp"
import { ShrimpModule } from "./ShrimpModule"
import { ShrimpTableModule } from "./ShrimpTableModule"
import type { ShrimpCrud } from "./ShrimpCrud"
const SENSITIVE_KEY_RE = /(password|pwd|token|secret)/i
const ARGS_MAX_LEN = 2000
const operationLogFields: IFieldConfig[] = [
{ key: "ip", label: { t: "operationLog.ip" }, type: "string", searchable: true, editable: "readonly" },
{ key: "method", label: { t: "operationLog.method" }, type: "string", searchable: true, filterable: true, editable: "readonly" },
{ key: "args", label: { t: "operationLog.args" }, type: "textarea", editable: "readonly" },
{ key: "success", label: { t: "operationLog.success" }, type: "boolean", filterable: true, editable: "readonly" },
{ key: "errorMsg", label: { t: "operationLog.errorMsg" }, type: "string", editable: "readonly" },
{ key: "createdAt", label: { t: "operationLog.createdAt" }, type: "date", editable: "readonly", sortable: true, filterable: true },
]
export class OperationLogModule extends ShrimpTableModule<WithId<IOperationLog>> {
logEvents: Set<string> = new Set(['add', 'edit', 'delete', 'clear', 'import', 'login'])
constructor(parent: Shrimp | ShrimpModule, dataSource: ShrimpCrud<WithId<IOperationLog>>, fields?: IFieldConfig[], label?: ILabel) {
super(parent, "operationLog", dataSource, fields || operationLogFields, label || { t: "user.operationLog" }, undefined, {
icon: "Tickets",
ownerKey: "operatorId",
})
this.addEvent("list", this.power)
this.addEvent("get", this.power)
}
shouldLog(path: string[]): boolean {
if (path.length === 0) return false
if (path.includes('operationLog')) return false
const last = path[path.length - 1]
return this.logEvents.has(last)
}
sanitizeArgs(args: any[]): string {
const walk = (val: any): any => {
if (val == null) return val
if (Array.isArray(val)) return val.map(walk)
if (typeof val === 'object') {
const out: Record<string, any> = {}
for (const [k, v] of Object.entries(val)) {
out[k] = SENSITIVE_KEY_RE.test(k) ? '***' : walk(v)
}
return out
}
return val
}
let str: string
try {
str = JSON.stringify(walk(args))
} catch {
str = '[unserializable]'
}
return str.length > ARGS_MAX_LEN ? str.substring(0, ARGS_MAX_LEN) + '...' : str
}
async record(operatorId: any, ip: string, path: string[], args: any[], success: boolean, errorMsg: string = ''): Promise<void> {
try {
await this.dataSource.add({
operatorId: operatorId == null ? null : String(operatorId),
ip,
method: path.join('.'),
args: this.sanitizeArgs(args),
success,
errorMsg,
createdAt: Date.now(),
} as Omit<WithId<IOperationLog>, '_id'>)
} catch (e: any) {
console.warn('[OperationLog] record failed', e?.message || e)
}
}
}