feat: WWJCloud 企业级全栈框架 v0.3.5 完整更新
🚀 核心更新: - ✅ 完善 NestJS 企业级架构设计 - ✅ 优化配置中心和基础设施层 - ✅ 增强第三方服务集成能力 - ✅ 完善多租户架构支持 - 🎯 对标 Java Spring Boot 和 PHP ThinkPHP 📦 新增文件: - wwjcloud-nest 完整框架结构 - Docker 容器化配置 - 管理后台界面 - 数据库迁移脚本 🔑 Key: ebb38b43ec39f355f071294fd1cf9c42
This commit is contained in:
15
wwjcloud-nest/src/common/libraries/dayjs/dayjs.module.ts
Normal file
15
wwjcloud-nest/src/common/libraries/dayjs/dayjs.module.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Module, Global } from '@nestjs/common';
|
||||
import { DayjsService } from './dayjs.service';
|
||||
|
||||
/**
|
||||
* Day.js 日期处理库模块
|
||||
* 基于 NestJS 官方示例实现
|
||||
* 参考: https://docs.nestjs.cn/fundamentals/dependency-injection
|
||||
* 对应 Java: DateUtils
|
||||
*/
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [DayjsService],
|
||||
exports: [DayjsService],
|
||||
})
|
||||
export class DayjsModule {}
|
||||
346
wwjcloud-nest/src/common/libraries/dayjs/dayjs.service.ts
Normal file
346
wwjcloud-nest/src/common/libraries/dayjs/dayjs.service.ts
Normal file
@@ -0,0 +1,346 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import dayjs from 'dayjs';
|
||||
import utc from 'dayjs/plugin/utc';
|
||||
import timezone from 'dayjs/plugin/timezone';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
import customParseFormat from 'dayjs/plugin/customParseFormat';
|
||||
import duration from 'dayjs/plugin/duration';
|
||||
import isBetween from 'dayjs/plugin/isBetween';
|
||||
import isSameOrAfter from 'dayjs/plugin/isSameOrAfter';
|
||||
import isSameOrBefore from 'dayjs/plugin/isSameOrBefore';
|
||||
import quarterOfYear from 'dayjs/plugin/quarterOfYear';
|
||||
import dayOfYear from 'dayjs/plugin/dayOfYear';
|
||||
import weekOfYear from 'dayjs/plugin/weekOfYear';
|
||||
import isoWeek from 'dayjs/plugin/isoWeek';
|
||||
|
||||
/**
|
||||
* Day.js 日期处理服务
|
||||
* 基于 NestJS 官方示例实现
|
||||
* 参考: https://docs.nestjs.cn/fundamentals/dependency-injection
|
||||
* 对应 Java: DateUtils
|
||||
*/
|
||||
@Injectable()
|
||||
export class DayjsService {
|
||||
constructor() {
|
||||
// 初始化插件
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
dayjs.extend(relativeTime);
|
||||
dayjs.extend(customParseFormat);
|
||||
dayjs.extend(duration);
|
||||
dayjs.extend(isBetween);
|
||||
dayjs.extend(isSameOrAfter);
|
||||
dayjs.extend(isSameOrBefore);
|
||||
dayjs.extend(quarterOfYear);
|
||||
dayjs.extend(dayOfYear);
|
||||
dayjs.extend(weekOfYear);
|
||||
dayjs.extend(isoWeek);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 dayjs 实例
|
||||
*/
|
||||
getDayjs() {
|
||||
return dayjs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化日期
|
||||
*/
|
||||
format(
|
||||
date: Date | string | number,
|
||||
format: string = 'YYYY-MM-DD HH:mm:ss',
|
||||
): string {
|
||||
return dayjs(date).format(format);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析日期字符串
|
||||
*/
|
||||
parse(dateStr: string, format?: string): dayjs.Dayjs | null {
|
||||
if (!dateStr) return null;
|
||||
|
||||
try {
|
||||
const parsed = format ? dayjs(dateStr, format) : dayjs(dateStr);
|
||||
return parsed.isValid() ? parsed : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前时间
|
||||
*/
|
||||
now(): dayjs.Dayjs {
|
||||
return dayjs();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前时间戳
|
||||
*/
|
||||
timestamp(): number {
|
||||
return dayjs().valueOf();
|
||||
}
|
||||
|
||||
/**
|
||||
* 时间戳转日期
|
||||
*/
|
||||
fromTimestamp(timestamp: number): dayjs.Dayjs {
|
||||
return dayjs(timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* 日期转时间戳
|
||||
*/
|
||||
toTimestamp(date: Date | string | number): number {
|
||||
return dayjs(date).valueOf();
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加时间
|
||||
*/
|
||||
add(
|
||||
date: Date | string | number,
|
||||
amount: number,
|
||||
unit: dayjs.ManipulateType,
|
||||
): dayjs.Dayjs {
|
||||
return dayjs(date).add(amount, unit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 减去时间
|
||||
*/
|
||||
subtract(
|
||||
date: Date | string | number,
|
||||
amount: number,
|
||||
unit: dayjs.ManipulateType,
|
||||
): dayjs.Dayjs {
|
||||
return dayjs(date).subtract(amount, unit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算时间差
|
||||
*/
|
||||
diff(
|
||||
date1: Date | string | number,
|
||||
date2: Date | string | number,
|
||||
unit: dayjs.QUnitType = 'millisecond',
|
||||
): number {
|
||||
return dayjs(date1).diff(dayjs(date2), unit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取开始时间
|
||||
*/
|
||||
startOf(date: Date | string | number, unit: dayjs.OpUnitType): dayjs.Dayjs {
|
||||
return dayjs(date).startOf(unit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取结束时间
|
||||
*/
|
||||
endOf(date: Date | string | number, unit: dayjs.OpUnitType): dayjs.Dayjs {
|
||||
return dayjs(date).endOf(unit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为今天
|
||||
*/
|
||||
isToday(date: Date | string | number): boolean {
|
||||
return dayjs(date).isSame(dayjs(), 'day');
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为昨天
|
||||
*/
|
||||
isYesterday(date: Date | string | number): boolean {
|
||||
return dayjs(date).isSame(dayjs().subtract(1, 'day'), 'day');
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为明天
|
||||
*/
|
||||
isTomorrow(date: Date | string | number): boolean {
|
||||
return dayjs(date).isSame(dayjs().add(1, 'day'), 'day');
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为同一天
|
||||
*/
|
||||
isSameDay(
|
||||
date1: Date | string | number,
|
||||
date2: Date | string | number,
|
||||
): boolean {
|
||||
return dayjs(date1).isSame(dayjs(date2), 'day');
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否在范围内
|
||||
*/
|
||||
isBetween(
|
||||
date: Date | string | number,
|
||||
start: Date | string | number,
|
||||
end: Date | string | number,
|
||||
unit?: dayjs.OpUnitType,
|
||||
): boolean {
|
||||
return dayjs(date).isBetween(dayjs(start), dayjs(end), unit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取相对时间描述
|
||||
*/
|
||||
fromNow(date: Date | string | number): string {
|
||||
return dayjs(date).fromNow();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取相对时间描述(中文)
|
||||
*/
|
||||
fromNowZh(date: Date | string | number): string {
|
||||
const now = dayjs();
|
||||
const target = dayjs(date);
|
||||
const diffMs = now.diff(target);
|
||||
const diffSeconds = Math.floor(diffMs / 1000);
|
||||
const diffMinutes = Math.floor(diffSeconds / 60);
|
||||
const diffHours = Math.floor(diffMinutes / 60);
|
||||
const diffDays = Math.floor(diffHours / 24);
|
||||
|
||||
if (diffSeconds < 60) {
|
||||
return '刚刚';
|
||||
} else if (diffMinutes < 60) {
|
||||
return `${diffMinutes}分钟前`;
|
||||
} else if (diffHours < 24) {
|
||||
return `${diffHours}小时前`;
|
||||
} else if (diffDays < 7) {
|
||||
return `${diffDays}天前`;
|
||||
} else {
|
||||
return target.format('YYYY-MM-DD');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置时区
|
||||
*/
|
||||
setTimezone(date: Date | string | number, timezone: string): dayjs.Dayjs {
|
||||
return dayjs(date).tz(timezone);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为 UTC
|
||||
*/
|
||||
toUTC(date: Date | string | number): dayjs.Dayjs {
|
||||
return dayjs(date).utc();
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为本地时间
|
||||
*/
|
||||
toLocal(date: Date | string | number): dayjs.Dayjs {
|
||||
return dayjs(date).local();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取持续时间
|
||||
*/
|
||||
duration(amount: number, unit: any): any {
|
||||
return dayjs.duration(amount, unit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断日期是否有效
|
||||
*/
|
||||
isValid(date: any): boolean {
|
||||
return dayjs(date).isValid();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取月份天数
|
||||
*/
|
||||
daysInMonth(date: Date | string | number): number {
|
||||
return dayjs(date).daysInMonth();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取年份
|
||||
*/
|
||||
year(date: Date | string | number): number {
|
||||
return dayjs(date).year();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取月份(0-11)
|
||||
*/
|
||||
month(date: Date | string | number): number {
|
||||
return dayjs(date).month();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取日期
|
||||
*/
|
||||
date(date: Date | string | number): number {
|
||||
return dayjs(date).date();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取小时
|
||||
*/
|
||||
hour(date: Date | string | number): number {
|
||||
return dayjs(date).hour();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分钟
|
||||
*/
|
||||
minute(date: Date | string | number): number {
|
||||
return dayjs(date).minute();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取秒
|
||||
*/
|
||||
second(date: Date | string | number): number {
|
||||
return dayjs(date).second();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取毫秒
|
||||
*/
|
||||
millisecond(date: Date | string | number): number {
|
||||
return dayjs(date).millisecond();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取星期几
|
||||
*/
|
||||
day(date: Date | string | number): number {
|
||||
return dayjs(date).day();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取季度
|
||||
*/
|
||||
quarter(date: Date | string | number): number {
|
||||
return dayjs(date).quarter();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取年份中的第几天
|
||||
*/
|
||||
dayOfYear(date: Date | string | number): number {
|
||||
return dayjs(date).dayOfYear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取年份中的第几周
|
||||
*/
|
||||
week(date: Date | string | number): number {
|
||||
return dayjs(date).week();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取年份中的第几周(ISO)
|
||||
*/
|
||||
isoWeek(date: Date | string | number): number {
|
||||
return dayjs(date).isoWeek();
|
||||
}
|
||||
}
|
||||
10
wwjcloud-nest/src/common/libraries/index.ts
Normal file
10
wwjcloud-nest/src/common/libraries/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export * from './libraries.module';
|
||||
export * from './dayjs/dayjs.service';
|
||||
export * from './lodash/lodash.service';
|
||||
export * from './prometheus/prometheus.service';
|
||||
export * from './redis/redis.service';
|
||||
export * from './sentry/sentry.service';
|
||||
export * from './sharp/sharp.service';
|
||||
export * from './uuid/uuid.service';
|
||||
export * from './validator/validator.service';
|
||||
export * from './winston/winston.service';
|
||||
43
wwjcloud-nest/src/common/libraries/libraries.module.ts
Normal file
43
wwjcloud-nest/src/common/libraries/libraries.module.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { DayjsModule } from './dayjs/dayjs.module';
|
||||
import { LodashModule } from './lodash/lodash.module';
|
||||
import { PrometheusModule } from './prometheus/prometheus.module';
|
||||
import { RedisModule } from './redis/redis.module';
|
||||
import { SentryModule } from './sentry/sentry.module';
|
||||
import { SharpModule } from './sharp/sharp.module';
|
||||
import { UuidModule } from './uuid/uuid.module';
|
||||
import { ValidatorModule } from './validator/validator.module';
|
||||
import { WinstonModule } from './winston/winston.module';
|
||||
|
||||
/**
|
||||
* 第三方工具库模块 - Common层
|
||||
* 基于 NestJS 实现
|
||||
* 对应 Java: 工具库封装
|
||||
*
|
||||
* 包含所有第三方工具库的封装,作为框架基础能力
|
||||
*/
|
||||
@Module({
|
||||
imports: [
|
||||
DayjsModule,
|
||||
LodashModule,
|
||||
PrometheusModule,
|
||||
RedisModule,
|
||||
SentryModule,
|
||||
SharpModule,
|
||||
UuidModule,
|
||||
ValidatorModule,
|
||||
WinstonModule,
|
||||
],
|
||||
exports: [
|
||||
DayjsModule,
|
||||
LodashModule,
|
||||
PrometheusModule,
|
||||
RedisModule,
|
||||
SentryModule,
|
||||
SharpModule,
|
||||
UuidModule,
|
||||
ValidatorModule,
|
||||
WinstonModule,
|
||||
],
|
||||
})
|
||||
export class LibrariesModule {}
|
||||
15
wwjcloud-nest/src/common/libraries/lodash/lodash.module.ts
Normal file
15
wwjcloud-nest/src/common/libraries/lodash/lodash.module.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Module, Global } from '@nestjs/common';
|
||||
import { LodashService } from './lodash.service';
|
||||
|
||||
/**
|
||||
* Lodash 工具函数库模块
|
||||
* 基于 NestJS 官方示例实现
|
||||
* 参考: https://docs.nestjs.cn/fundamentals/dependency-injection
|
||||
* 对应 Java: StringUtils, ObjectUtils, ArrayUtils
|
||||
*/
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [LodashService],
|
||||
exports: [LodashService],
|
||||
})
|
||||
export class LodashModule {}
|
||||
665
wwjcloud-nest/src/common/libraries/lodash/lodash.service.ts
Normal file
665
wwjcloud-nest/src/common/libraries/lodash/lodash.service.ts
Normal file
@@ -0,0 +1,665 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
/**
|
||||
* Lodash 工具函数服务
|
||||
* 基于 NestJS 官方示例实现
|
||||
* 参考: https://docs.nestjs.cn/fundamentals/dependency-injection
|
||||
* 对应 Java: StringUtils, ObjectUtils, ArrayUtils
|
||||
*/
|
||||
@Injectable()
|
||||
export class LodashService {
|
||||
/**
|
||||
* 获取 lodash 实例
|
||||
*/
|
||||
getLodash(): any {
|
||||
return _;
|
||||
}
|
||||
|
||||
// ==================== 字符串工具 ====================
|
||||
|
||||
/**
|
||||
* 判断字符串是否为空
|
||||
*/
|
||||
isEmpty(value: any): boolean {
|
||||
return _.isEmpty(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断字符串是否不为空
|
||||
*/
|
||||
isNotEmpty(value: any): boolean {
|
||||
return !_.isEmpty(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 首字母大写
|
||||
*/
|
||||
capitalize(str: string): string {
|
||||
return _.capitalize(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 驼峰转下划线
|
||||
*/
|
||||
snakeCase(str: string): string {
|
||||
return _.snakeCase(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 下划线转驼峰
|
||||
*/
|
||||
camelCase(str: string): string {
|
||||
return _.camelCase(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为大写
|
||||
*/
|
||||
upperCase(str: string): string {
|
||||
return _.upperCase(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为小写
|
||||
*/
|
||||
lowerCase(str: string): string {
|
||||
return _.lowerCase(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为标题格式
|
||||
*/
|
||||
startCase(str: string): string {
|
||||
return _.startCase(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 截取字符串
|
||||
*/
|
||||
truncate(str: string, options?: _.TruncateOptions): string {
|
||||
return _.truncate(str, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除字符串两端空白
|
||||
*/
|
||||
trim(str: string, chars?: string): string {
|
||||
return _.trim(str, chars);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除字符串左端空白
|
||||
*/
|
||||
trimStart(str: string, chars?: string): string {
|
||||
return _.trimStart(str, chars);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除字符串右端空白
|
||||
*/
|
||||
trimEnd(str: string, chars?: string): string {
|
||||
return _.trimEnd(str, chars);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重复字符串
|
||||
*/
|
||||
repeat(str: string, n: number): string {
|
||||
return _.repeat(str, n);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成随机字符串
|
||||
*/
|
||||
randomString(
|
||||
length: number = 8,
|
||||
chars: string = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789',
|
||||
): string {
|
||||
return _.sampleSize(chars, length).join('');
|
||||
}
|
||||
|
||||
// ==================== 对象工具 ====================
|
||||
|
||||
/**
|
||||
* 判断是否为对象
|
||||
*/
|
||||
isObject(value: any): boolean {
|
||||
return _.isObject(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为数组
|
||||
*/
|
||||
isArray(value: any): boolean {
|
||||
return _.isArray(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为函数
|
||||
*/
|
||||
isFunction(value: any): boolean {
|
||||
return _.isFunction(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为数字
|
||||
*/
|
||||
isNumber(value: any): boolean {
|
||||
return _.isNumber(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为字符串
|
||||
*/
|
||||
isString(value: any): boolean {
|
||||
return _.isString(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为布尔值
|
||||
*/
|
||||
isBoolean(value: any): boolean {
|
||||
return _.isBoolean(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为日期
|
||||
*/
|
||||
isDate(value: any): boolean {
|
||||
return _.isDate(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为 null
|
||||
*/
|
||||
isNull(value: any): boolean {
|
||||
return _.isNull(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为 undefined
|
||||
*/
|
||||
isUndefined(value: any): boolean {
|
||||
return _.isUndefined(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为 null 或 undefined
|
||||
*/
|
||||
isNil(value: any): boolean {
|
||||
return _.isNil(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 深度合并对象
|
||||
*/
|
||||
merge<T extends object>(target: T, ...sources: Partial<T>[]): T {
|
||||
return _.merge(target, ...sources);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从对象中提取指定属性
|
||||
*/
|
||||
pick<T extends object, K extends keyof T>(
|
||||
object: T,
|
||||
...paths: K[]
|
||||
): Pick<T, K> {
|
||||
return _.pick(object, ...paths);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从对象中排除指定属性
|
||||
*/
|
||||
omit<T extends object, K extends keyof T>(
|
||||
object: T,
|
||||
...paths: K[]
|
||||
): Omit<T, K> {
|
||||
return _.omit(object, ...paths);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对象属性值
|
||||
*/
|
||||
get<T = any>(object: any, path: string, defaultValue?: T): T {
|
||||
return _.get(object, path, defaultValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置对象属性值
|
||||
*/
|
||||
set<T = any>(object: any, path: string, value: T): any {
|
||||
return _.set(object, path, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断对象是否有指定属性
|
||||
*/
|
||||
has(object: any, path: string): boolean {
|
||||
return _.has(object, path);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除对象属性
|
||||
*/
|
||||
unset(object: any, path: string): boolean {
|
||||
return _.unset(object, path);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对象的所有键
|
||||
*/
|
||||
keys(object: any): string[] {
|
||||
return _.keys(object);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对象的所有值
|
||||
*/
|
||||
values(object: any): any[] {
|
||||
return _.values(object);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对象的所有键值对
|
||||
*/
|
||||
entries(object: any): [string, any][] {
|
||||
return _.entries(object);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将键值对数组转换为对象
|
||||
*/
|
||||
fromPairs(pairs: [string, any][]): object {
|
||||
return _.fromPairs(pairs);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将对象转换为键值对数组
|
||||
*/
|
||||
toPairs(object: any): [string, any][] {
|
||||
return _.toPairs(object);
|
||||
}
|
||||
|
||||
// ==================== 数组工具 ====================
|
||||
|
||||
/**
|
||||
* 数组去重
|
||||
*/
|
||||
uniq<T>(array: T[]): T[] {
|
||||
return _.uniq(array);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组去重(根据指定属性)
|
||||
*/
|
||||
uniqBy<T>(array: T[], iteratee: string | ((item: T) => any)): T[] {
|
||||
return _.uniqBy(array, iteratee);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组去重(根据指定函数)
|
||||
*/
|
||||
uniqWith<T>(array: T[], comparator: (a: T, b: T) => boolean): T[] {
|
||||
return _.uniqWith(array, comparator);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组分组
|
||||
*/
|
||||
groupBy<T>(
|
||||
array: T[],
|
||||
iteratee: string | ((item: T) => any),
|
||||
): Record<string, T[]> {
|
||||
return _.groupBy(array, iteratee);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组排序
|
||||
*/
|
||||
sortBy<T>(array: T[], ...iteratees: (string | ((item: T) => any))[]): T[] {
|
||||
return _.sortBy(array, ...iteratees);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组过滤
|
||||
*/
|
||||
filter<T>(array: T[], predicate: (item: T) => boolean): T[] {
|
||||
return _.filter(array, predicate);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组映射
|
||||
*/
|
||||
map<T, U>(array: T[], iteratee: (item: T, index: number) => U): U[] {
|
||||
return _.map(array, iteratee);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组查找
|
||||
*/
|
||||
find<T>(array: T[], predicate: (item: T) => boolean): T | undefined {
|
||||
return _.find(array, predicate);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组查找索引
|
||||
*/
|
||||
findIndex<T>(array: T[], predicate: (item: T) => boolean): number {
|
||||
return _.findIndex(array, predicate);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组包含
|
||||
*/
|
||||
includes<T>(array: T[], value: T): boolean {
|
||||
return _.includes(array, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组切片
|
||||
*/
|
||||
slice<T>(array: T[], start?: number, end?: number): T[] {
|
||||
return _.slice(array, start, end);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组分块
|
||||
*/
|
||||
chunk<T>(array: T[], size: number): T[][] {
|
||||
return _.chunk(array, size);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组扁平化
|
||||
*/
|
||||
flatten<T>(array: T[][]): T[] {
|
||||
return _.flatten(array);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组深度扁平化
|
||||
*/
|
||||
flattenDeep<T>(array: any[]): T[] {
|
||||
return _.flattenDeep(array);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组压缩
|
||||
*/
|
||||
compact<T>(array: T[]): T[] {
|
||||
return _.compact(array);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组连接
|
||||
*/
|
||||
concat<T>(array: T[], ...values: any[]): T[] {
|
||||
return _.concat(array, ...values);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组差集
|
||||
*/
|
||||
difference<T>(array: T[], ...values: T[][]): T[] {
|
||||
return _.difference(array, ...values);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组交集
|
||||
*/
|
||||
intersection<T>(...arrays: T[][]): T[] {
|
||||
return _.intersection(...arrays);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组并集
|
||||
*/
|
||||
union<T>(...arrays: T[][]): T[] {
|
||||
return _.union(...arrays);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组求和
|
||||
*/
|
||||
sum(array: number[]): number {
|
||||
return _.sum(array);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组平均值
|
||||
*/
|
||||
mean(array: number[]): number {
|
||||
return _.mean(array);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组最大值
|
||||
*/
|
||||
max<T>(array: T[]): T | undefined {
|
||||
return _.max(array);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组最小值
|
||||
*/
|
||||
min<T>(array: T[]): T | undefined {
|
||||
return _.min(array);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组随机元素
|
||||
*/
|
||||
sample<T>(array: T[]): T | undefined {
|
||||
return _.sample(array);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组随机元素(多个)
|
||||
*/
|
||||
sampleSize<T>(array: T[], n: number): T[] {
|
||||
return _.sampleSize(array, n);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组洗牌
|
||||
*/
|
||||
shuffle<T>(array: T[]): T[] {
|
||||
return _.shuffle(array);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组取前N个
|
||||
*/
|
||||
take<T>(array: T[], n: number): T[] {
|
||||
return _.take(array, n);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组取后N个
|
||||
*/
|
||||
takeRight<T>(array: T[], n: number): T[] {
|
||||
return _.takeRight(array, n);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组跳过前N个
|
||||
*/
|
||||
drop<T>(array: T[], n: number): T[] {
|
||||
return _.drop(array, n);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组跳过后N个
|
||||
*/
|
||||
dropRight<T>(array: T[], n: number): T[] {
|
||||
return _.dropRight(array, n);
|
||||
}
|
||||
|
||||
// ==================== 函数工具 ====================
|
||||
|
||||
/**
|
||||
* 函数防抖
|
||||
*/
|
||||
debounce<T extends (...args: any[]) => any>(
|
||||
func: T,
|
||||
wait: number,
|
||||
options?: _.DebounceSettings,
|
||||
): any {
|
||||
return _.debounce(func, wait, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 函数节流
|
||||
*/
|
||||
throttle<T extends (...args: any[]) => any>(
|
||||
func: T,
|
||||
wait: number,
|
||||
options?: _.ThrottleSettings,
|
||||
): any {
|
||||
return _.throttle(func, wait, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 函数柯里化
|
||||
*/
|
||||
curry<T extends (...args: any[]) => any>(func: T): any {
|
||||
return _.curry(func);
|
||||
}
|
||||
|
||||
/**
|
||||
* 函数记忆化
|
||||
*/
|
||||
memoize<T extends (...args: any[]) => any>(
|
||||
func: T,
|
||||
resolver?: (...args: any[]) => string,
|
||||
): T {
|
||||
return _.memoize(func, resolver);
|
||||
}
|
||||
|
||||
// ==================== 数字工具 ====================
|
||||
|
||||
/**
|
||||
* 数字范围
|
||||
*/
|
||||
range(start: number, end?: number, step?: number): number[] {
|
||||
return _.range(start, end, step);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数字随机
|
||||
*/
|
||||
random(lower: number, upper?: number, floating?: boolean): number {
|
||||
if (upper === undefined) {
|
||||
return _.random(lower);
|
||||
}
|
||||
return _.random(lower, upper, floating);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数字四舍五入
|
||||
*/
|
||||
round(number: number, precision?: number): number {
|
||||
return _.round(number, precision);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数字向上取整
|
||||
*/
|
||||
ceil(number: number, precision?: number): number {
|
||||
return _.ceil(number, precision);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数字向下取整
|
||||
*/
|
||||
floor(number: number, precision?: number): number {
|
||||
return _.floor(number, precision);
|
||||
}
|
||||
|
||||
/**
|
||||
* 数字截断
|
||||
*/
|
||||
trunc(number: number, precision?: number): number {
|
||||
return (
|
||||
Math.trunc(number * Math.pow(10, precision || 0)) /
|
||||
Math.pow(10, precision || 0)
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== 集合工具 ====================
|
||||
|
||||
/**
|
||||
* 集合大小
|
||||
*/
|
||||
size(collection: any): number {
|
||||
return _.size(collection);
|
||||
}
|
||||
|
||||
/**
|
||||
* 集合遍历
|
||||
*/
|
||||
forEach<T>(
|
||||
collection: T[],
|
||||
iteratee: (value: T, index: number, collection: T[]) => void,
|
||||
): T[] {
|
||||
return _.forEach(collection, iteratee);
|
||||
}
|
||||
|
||||
/**
|
||||
* 集合映射
|
||||
*/
|
||||
mapValues<T, U>(
|
||||
object: Record<string, T>,
|
||||
iteratee: (value: T, key: string) => U,
|
||||
): Record<string, U> {
|
||||
return _.mapValues(object, iteratee);
|
||||
}
|
||||
|
||||
/**
|
||||
* 集合键映射
|
||||
*/
|
||||
mapKeys<T>(
|
||||
object: Record<string, T>,
|
||||
iteratee: (value: T, key: string) => string,
|
||||
): Record<string, T> {
|
||||
return _.mapKeys(object, iteratee);
|
||||
}
|
||||
|
||||
/**
|
||||
* 集合键过滤
|
||||
*/
|
||||
pickBy<T>(
|
||||
object: Record<string, T>,
|
||||
predicate: (value: T, key: string) => boolean,
|
||||
): Record<string, T> {
|
||||
return _.pickBy(object, predicate);
|
||||
}
|
||||
|
||||
/**
|
||||
* 集合键排除
|
||||
*/
|
||||
omitBy<T>(
|
||||
object: Record<string, T>,
|
||||
predicate: (value: T, key: string) => boolean,
|
||||
): Record<string, T> {
|
||||
return _.omitBy(object, predicate);
|
||||
}
|
||||
|
||||
/**
|
||||
* 集合键反转
|
||||
*/
|
||||
invert(object: Record<string, any>): Record<string, string> {
|
||||
return _.invert(object);
|
||||
}
|
||||
|
||||
/**
|
||||
* 集合键反转(保持值)
|
||||
*/
|
||||
invertBy<T>(
|
||||
object: Record<string, T>,
|
||||
iteratee?: (value: T) => string,
|
||||
): Record<string, string[]> {
|
||||
return _.invertBy(object, iteratee);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Module, Global } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { PrometheusService } from './prometheus.service';
|
||||
|
||||
/**
|
||||
* Prometheus 客户端模块
|
||||
* 基于 NestJS 官方示例实现
|
||||
* 参考: https://docs.nestjs.cn/fundamentals/dependency-injection
|
||||
* 对应 Java: Prometheus 监控
|
||||
*/
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [ConfigModule],
|
||||
providers: [
|
||||
{
|
||||
provide: PrometheusService,
|
||||
useFactory: (configService: ConfigService) => {
|
||||
const prometheusConfig = configService.get('prometheus');
|
||||
return new PrometheusService(prometheusConfig);
|
||||
},
|
||||
inject: [ConfigService],
|
||||
},
|
||||
],
|
||||
exports: [PrometheusService],
|
||||
})
|
||||
export class PrometheusModule {}
|
||||
@@ -0,0 +1,428 @@
|
||||
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||
import {
|
||||
register,
|
||||
Counter,
|
||||
Histogram,
|
||||
Summary,
|
||||
Gauge,
|
||||
collectDefaultMetrics,
|
||||
Registry,
|
||||
Metric,
|
||||
} from 'prom-client';
|
||||
import type {
|
||||
MonitoringInterface,
|
||||
Timer,
|
||||
MonitoringConfig,
|
||||
} from '../../monitoring/monitoring.interface';
|
||||
|
||||
/**
|
||||
* Prometheus 服务
|
||||
* 基于 NestJS 官方示例实现
|
||||
* 参考: https://docs.nestjs.cn/fundamentals/dependency-injection
|
||||
* 对应 Java: Prometheus 监控
|
||||
*/
|
||||
@Injectable()
|
||||
export class PrometheusService implements MonitoringInterface, OnModuleInit {
|
||||
private readonly logger = new Logger(PrometheusService.name);
|
||||
private registry: Registry;
|
||||
private metrics = new Map<string, Metric>();
|
||||
private timers = new Map<string, number>();
|
||||
|
||||
constructor(
|
||||
private readonly config: MonitoringConfig = {
|
||||
enabled: false,
|
||||
port: 9090,
|
||||
path: '/metrics',
|
||||
prefix: 'wwjcloud',
|
||||
defaultLabels: {},
|
||||
collectDefaultMetrics: false,
|
||||
},
|
||||
) {
|
||||
this.registry = new Registry();
|
||||
this.initializeRegistry();
|
||||
}
|
||||
|
||||
async onModuleInit() {
|
||||
if (this.config.collectDefaultMetrics) {
|
||||
collectDefaultMetrics({ register: this.registry });
|
||||
this.logger.log('Default metrics collection enabled');
|
||||
}
|
||||
}
|
||||
|
||||
private initializeRegistry() {
|
||||
// 设置默认标签
|
||||
if (this.config.defaultLabels) {
|
||||
this.registry.setDefaultLabels(this.config.defaultLabels);
|
||||
}
|
||||
|
||||
// 设置前缀
|
||||
if (this.config.prefix) {
|
||||
this.registry.setDefaultLabels({
|
||||
// ...this.registry.getDefaultLabels(),
|
||||
prefix: this.config.prefix,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录计数器指标
|
||||
*/
|
||||
counter(
|
||||
name: string,
|
||||
value: number = 1,
|
||||
labels?: Record<string, string>,
|
||||
): void {
|
||||
try {
|
||||
const metricName = this.getMetricName(name);
|
||||
let counter = this.metrics.get(metricName) as Counter<string>;
|
||||
|
||||
if (!counter) {
|
||||
counter = new Counter({
|
||||
name: metricName,
|
||||
help: `Counter metric for ${name}`,
|
||||
labelNames: Object.keys(labels || {}),
|
||||
registers: [this.registry],
|
||||
});
|
||||
this.metrics.set(metricName, counter);
|
||||
}
|
||||
|
||||
counter.inc(labels || {}, value);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to record counter: ${name}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录直方图指标
|
||||
*/
|
||||
histogram(
|
||||
name: string,
|
||||
value: number,
|
||||
labels?: Record<string, string>,
|
||||
): void {
|
||||
try {
|
||||
const metricName = this.getMetricName(name);
|
||||
let histogram = this.metrics.get(metricName) as Histogram<string>;
|
||||
|
||||
if (!histogram) {
|
||||
histogram = new Histogram({
|
||||
name: metricName,
|
||||
help: `Histogram metric for ${name}`,
|
||||
labelNames: Object.keys(labels || {}),
|
||||
buckets: [0.1, 0.5, 1, 2, 5, 10, 30, 60, 120, 300, 600],
|
||||
registers: [this.registry],
|
||||
});
|
||||
this.metrics.set(metricName, histogram);
|
||||
}
|
||||
|
||||
histogram.observe(labels || {}, value);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to record histogram: ${name}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录摘要指标
|
||||
*/
|
||||
summary(name: string, value: number, labels?: Record<string, string>): void {
|
||||
try {
|
||||
const metricName = this.getMetricName(name);
|
||||
let summary = this.metrics.get(metricName) as Summary<string>;
|
||||
|
||||
if (!summary) {
|
||||
summary = new Summary({
|
||||
name: metricName,
|
||||
help: `Summary metric for ${name}`,
|
||||
labelNames: Object.keys(labels || {}),
|
||||
percentiles: [0.5, 0.9, 0.95, 0.99],
|
||||
registers: [this.registry],
|
||||
});
|
||||
this.metrics.set(metricName, summary);
|
||||
}
|
||||
|
||||
summary.observe(labels || {}, value);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to record summary: ${name}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录仪表盘指标
|
||||
*/
|
||||
gauge(name: string, value: number, labels?: Record<string, string>): void {
|
||||
try {
|
||||
const metricName = this.getMetricName(name);
|
||||
let gauge = this.metrics.get(metricName) as Gauge<string>;
|
||||
|
||||
if (!gauge) {
|
||||
gauge = new Gauge({
|
||||
name: metricName,
|
||||
help: `Gauge metric for ${name}`,
|
||||
labelNames: Object.keys(labels || {}),
|
||||
registers: [this.registry],
|
||||
});
|
||||
this.metrics.set(metricName, gauge);
|
||||
}
|
||||
|
||||
gauge.set(labels || {}, value);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to record gauge: ${name}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始计时
|
||||
*/
|
||||
startTimer(name: string, labels?: Record<string, string>): Timer {
|
||||
const timerId = `${name}_${JSON.stringify(labels || {})}`;
|
||||
this.timers.set(timerId, Date.now());
|
||||
|
||||
return {
|
||||
end: (endLabels?: Record<string, string>) => {
|
||||
const startTime = this.timers.get(timerId);
|
||||
if (startTime) {
|
||||
const duration = Date.now() - startTime;
|
||||
this.timers.delete(timerId);
|
||||
this.histogram(name, duration / 1000, endLabels || labels);
|
||||
return duration;
|
||||
}
|
||||
return 0;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录执行时间
|
||||
*/
|
||||
async time<T>(
|
||||
name: string,
|
||||
fn: () => T | Promise<T>,
|
||||
labels?: Record<string, string>,
|
||||
): Promise<T> {
|
||||
const timer = this.startTimer(name, labels);
|
||||
try {
|
||||
const result = await fn();
|
||||
return result;
|
||||
} finally {
|
||||
timer.end(labels);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指标值
|
||||
*/
|
||||
async getMetric(
|
||||
name: string,
|
||||
labels?: Record<string, string>,
|
||||
): Promise<number | null> {
|
||||
try {
|
||||
const metricName = this.getMetricName(name);
|
||||
const metric = this.metrics.get(metricName);
|
||||
|
||||
if (metric) {
|
||||
const metricData = metric.get();
|
||||
const data = await metricData;
|
||||
if (data && data.values) {
|
||||
const value = data.values.find(
|
||||
(v) =>
|
||||
!labels ||
|
||||
Object.keys(labels).every((key) => v.labels[key] === labels[key]),
|
||||
);
|
||||
return value ? value.value : null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to get metric: ${name}`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有指标
|
||||
*/
|
||||
async getMetrics(): Promise<string> {
|
||||
try {
|
||||
return await this.registry.metrics();
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to get metrics', error);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置指标
|
||||
*/
|
||||
reset(name?: string): void {
|
||||
try {
|
||||
if (name) {
|
||||
const metricName = this.getMetricName(name);
|
||||
const metric = this.metrics.get(metricName);
|
||||
if (metric) {
|
||||
metric.reset();
|
||||
}
|
||||
} else {
|
||||
this.registry.clear();
|
||||
this.metrics.clear();
|
||||
this.timers.clear();
|
||||
this.initializeRegistry();
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to reset metrics: ${name || 'all'}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取注册表
|
||||
*/
|
||||
getRegistry(): Registry {
|
||||
return this.registry;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指标列表
|
||||
*/
|
||||
getMetricList(): string[] {
|
||||
return Array.from(this.metrics.keys());
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查指标是否存在
|
||||
*/
|
||||
hasMetric(name: string): boolean {
|
||||
const metricName = this.getMetricName(name);
|
||||
return this.metrics.has(metricName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除指标
|
||||
*/
|
||||
removeMetric(name: string): boolean {
|
||||
const metricName = this.getMetricName(name);
|
||||
const metric = this.metrics.get(metricName);
|
||||
if (metric) {
|
||||
this.registry.removeSingleMetric(metricName);
|
||||
this.metrics.delete(metricName);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指标数据
|
||||
*/
|
||||
getMetricData(name: string): any {
|
||||
try {
|
||||
const metricName = this.getMetricName(name);
|
||||
const metric = this.metrics.get(metricName);
|
||||
return metric ? metric.get() : null;
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to get metric data: ${name}`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置指标帮助文本
|
||||
*/
|
||||
setMetricHelp(name: string, help: string): void {
|
||||
try {
|
||||
const metricName = this.getMetricName(name);
|
||||
const metric = this.metrics.get(metricName);
|
||||
if (metric && 'help' in metric) {
|
||||
(metric as any).help = help;
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to set metric help: ${name}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指标名称
|
||||
*/
|
||||
private getMetricName(name: string): string {
|
||||
const prefix = this.config.prefix || 'app';
|
||||
return `${prefix}_${name}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建自定义指标
|
||||
*/
|
||||
createCounter(
|
||||
name: string,
|
||||
help: string,
|
||||
labelNames: string[] = [],
|
||||
): Counter<string> {
|
||||
const metricName = this.getMetricName(name);
|
||||
const counter = new Counter({
|
||||
name: metricName,
|
||||
help,
|
||||
labelNames,
|
||||
registers: [this.registry],
|
||||
});
|
||||
this.metrics.set(metricName, counter);
|
||||
return counter;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建自定义直方图
|
||||
*/
|
||||
createHistogram(
|
||||
name: string,
|
||||
help: string,
|
||||
labelNames: string[] = [],
|
||||
buckets: number[] = [0.1, 0.5, 1, 2, 5, 10, 30, 60, 120, 300, 600],
|
||||
): Histogram<string> {
|
||||
const metricName = this.getMetricName(name);
|
||||
const histogram = new Histogram({
|
||||
name: metricName,
|
||||
help,
|
||||
labelNames,
|
||||
buckets,
|
||||
registers: [this.registry],
|
||||
});
|
||||
this.metrics.set(metricName, histogram);
|
||||
return histogram;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建自定义摘要
|
||||
*/
|
||||
createSummary(
|
||||
name: string,
|
||||
help: string,
|
||||
labelNames: string[] = [],
|
||||
percentiles: number[] = [0.5, 0.9, 0.95, 0.99],
|
||||
): Summary<string> {
|
||||
const metricName = this.getMetricName(name);
|
||||
const summary = new Summary({
|
||||
name: metricName,
|
||||
help,
|
||||
labelNames,
|
||||
percentiles,
|
||||
registers: [this.registry],
|
||||
});
|
||||
this.metrics.set(metricName, summary);
|
||||
return summary;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建自定义仪表盘
|
||||
*/
|
||||
createGauge(
|
||||
name: string,
|
||||
help: string,
|
||||
labelNames: string[] = [],
|
||||
): Gauge<string> {
|
||||
const metricName = this.getMetricName(name);
|
||||
const gauge = new Gauge({
|
||||
name: metricName,
|
||||
help,
|
||||
labelNames,
|
||||
registers: [this.registry],
|
||||
});
|
||||
this.metrics.set(metricName, gauge);
|
||||
return gauge;
|
||||
}
|
||||
}
|
||||
26
wwjcloud-nest/src/common/libraries/redis/redis.module.ts
Normal file
26
wwjcloud-nest/src/common/libraries/redis/redis.module.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Module, Global } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { RedisService } from './redis.service';
|
||||
|
||||
/**
|
||||
* Redis 客户端模块
|
||||
* 基于 NestJS 官方示例实现
|
||||
* 参考: https://docs.nestjs.cn/fundamentals/dependency-injection
|
||||
* 对应 Java: RedisTemplate
|
||||
*/
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [ConfigModule],
|
||||
providers: [
|
||||
{
|
||||
provide: RedisService,
|
||||
useFactory: (configService: ConfigService) => {
|
||||
const redisConfig = configService.get('redis');
|
||||
return new RedisService(redisConfig);
|
||||
},
|
||||
inject: [ConfigService],
|
||||
},
|
||||
],
|
||||
exports: [RedisService],
|
||||
})
|
||||
export class RedisModule {}
|
||||
488
wwjcloud-nest/src/common/libraries/redis/redis.service.ts
Normal file
488
wwjcloud-nest/src/common/libraries/redis/redis.service.ts
Normal file
@@ -0,0 +1,488 @@
|
||||
import { Injectable, Logger, OnModuleDestroy } from '@nestjs/common';
|
||||
import { createClient, RedisClientType } from 'redis';
|
||||
import type { CacheInterface, CacheConfig } from '../../cache/cache.interface';
|
||||
import { CacheStats } from '../../cache/cache.interface';
|
||||
|
||||
/**
|
||||
* Redis 服务
|
||||
* 基于 NestJS 官方示例实现
|
||||
* 参考: https://docs.nestjs.cn/fundamentals/dependency-injection
|
||||
* 对应 Java: RedisUtils
|
||||
*/
|
||||
@Injectable()
|
||||
export class RedisService implements CacheInterface, OnModuleDestroy {
|
||||
private readonly logger = new Logger(RedisService.name);
|
||||
private client: RedisClientType;
|
||||
private cacheStats: CacheStats = {
|
||||
hits: 0,
|
||||
misses: 0,
|
||||
keys: 0,
|
||||
memory: 0,
|
||||
uptime: 0,
|
||||
};
|
||||
private startTime: number;
|
||||
|
||||
constructor(private readonly config: CacheConfig) {
|
||||
this.startTime = Date.now();
|
||||
this.initializeClient();
|
||||
}
|
||||
|
||||
private async initializeClient() {
|
||||
try {
|
||||
this.client = createClient({
|
||||
socket: {
|
||||
host: this.config.host,
|
||||
port: this.config.port,
|
||||
},
|
||||
password: this.config.password,
|
||||
database: this.config.db || 0,
|
||||
});
|
||||
|
||||
this.client.on('error', (err) => {
|
||||
this.logger.error('Redis Client Error', err);
|
||||
});
|
||||
|
||||
this.client.on('connect', () => {
|
||||
this.logger.log('Redis Client Connected');
|
||||
});
|
||||
|
||||
this.client.on('ready', () => {
|
||||
this.logger.log('Redis Client Ready');
|
||||
});
|
||||
|
||||
this.client.on('end', () => {
|
||||
this.logger.log('Redis Client Disconnected');
|
||||
});
|
||||
|
||||
await this.client.connect();
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to initialize Redis client', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async onModuleDestroy() {
|
||||
if (this.client) {
|
||||
await this.client.quit();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存
|
||||
*/
|
||||
async get<T = any>(key: string): Promise<T | null> {
|
||||
try {
|
||||
const prefixedKey = this.getPrefixedKey(key);
|
||||
const value = await this.client.get(prefixedKey);
|
||||
|
||||
if (value === null) {
|
||||
this.cacheStats.misses++;
|
||||
return null;
|
||||
}
|
||||
|
||||
this.cacheStats.hits++;
|
||||
return JSON.parse(value);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to get cache key: ${key}`, error);
|
||||
this.cacheStats.misses++;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置缓存
|
||||
*/
|
||||
async set(key: string, value: any, ttl?: number): Promise<void> {
|
||||
try {
|
||||
const prefixedKey = this.getPrefixedKey(key);
|
||||
const serializedValue = JSON.stringify(value);
|
||||
|
||||
if (ttl) {
|
||||
await this.client.setEx(prefixedKey, ttl, serializedValue);
|
||||
} else {
|
||||
await this.client.set(prefixedKey, serializedValue);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to set cache key: ${key}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除缓存
|
||||
*/
|
||||
async del(key: string): Promise<void> {
|
||||
try {
|
||||
const prefixedKey = this.getPrefixedKey(key);
|
||||
await this.client.del(prefixedKey);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to delete cache key: ${key}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除缓存
|
||||
*/
|
||||
async delMany(keys: string[]): Promise<void> {
|
||||
try {
|
||||
const prefixedKeys = keys.map((key) => this.getPrefixedKey(key));
|
||||
await this.client.del(prefixedKeys);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to delete cache keys: ${keys.join(', ')}`,
|
||||
error,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查缓存是否存在
|
||||
*/
|
||||
async exists(key: string): Promise<boolean> {
|
||||
try {
|
||||
const prefixedKey = this.getPrefixedKey(key);
|
||||
const result = await this.client.exists(prefixedKey);
|
||||
return result === 1;
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to check cache key: ${key}`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置过期时间
|
||||
*/
|
||||
async expire(key: string, ttl: number): Promise<void> {
|
||||
try {
|
||||
const prefixedKey = this.getPrefixedKey(key);
|
||||
await this.client.expire(prefixedKey, ttl);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to set expire for cache key: ${key}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取过期时间
|
||||
*/
|
||||
async ttl(key: string): Promise<number> {
|
||||
try {
|
||||
const prefixedKey = this.getPrefixedKey(key);
|
||||
return await this.client.ttl(prefixedKey);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to get ttl for cache key: ${key}`, error);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有键
|
||||
*/
|
||||
async keys(pattern?: string): Promise<string[]> {
|
||||
try {
|
||||
const searchPattern = pattern
|
||||
? this.getPrefixedKey(pattern)
|
||||
: `${this.config.keyPrefix || 'cache'}:*`;
|
||||
const keys = await this.client.keys(searchPattern);
|
||||
return keys.map((key) => this.removePrefix(key));
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to get cache keys with pattern: ${pattern}`,
|
||||
error,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有缓存
|
||||
*/
|
||||
async flush(): Promise<void> {
|
||||
try {
|
||||
await this.client.flushDb();
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to flush cache', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存统计信息
|
||||
*/
|
||||
async stats(): Promise<CacheStats> {
|
||||
try {
|
||||
const info = await this.client.info('memory');
|
||||
const memoryMatch = info.match(/used_memory:(\d+)/);
|
||||
const memory = memoryMatch ? parseInt(memoryMatch[1]) : 0;
|
||||
|
||||
const keyCount = await this.client.dbSize();
|
||||
|
||||
return {
|
||||
...this.cacheStats,
|
||||
keys: keyCount,
|
||||
memory,
|
||||
uptime: Date.now() - this.startTime,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to get cache stats', error);
|
||||
return this.cacheStats;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量获取
|
||||
*/
|
||||
async mget<T = any>(keys: string[]): Promise<(T | null)[]> {
|
||||
try {
|
||||
const prefixedKeys = keys.map((key) => this.getPrefixedKey(key));
|
||||
const values = await this.client.mGet(prefixedKeys);
|
||||
|
||||
return values.map((value) => {
|
||||
if (value === null) {
|
||||
this.cacheStats.misses++;
|
||||
return null;
|
||||
}
|
||||
this.cacheStats.hits++;
|
||||
return JSON.parse(value);
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to mget cache keys: ${keys.join(', ')}`, error);
|
||||
return keys.map(() => null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量设置
|
||||
*/
|
||||
async mset(
|
||||
keyValuePairs: Array<{ key: string; value: any; ttl?: number }>,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const pipeline = this.client.multi();
|
||||
|
||||
for (const { key, value, ttl } of keyValuePairs) {
|
||||
const prefixedKey = this.getPrefixedKey(key);
|
||||
const serializedValue = JSON.stringify(value);
|
||||
|
||||
if (ttl) {
|
||||
pipeline.setEx(prefixedKey, ttl, serializedValue);
|
||||
} else {
|
||||
pipeline.set(prefixedKey, serializedValue);
|
||||
}
|
||||
}
|
||||
|
||||
await pipeline.exec();
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to mset cache', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 原子递增
|
||||
*/
|
||||
async incr(key: string, increment: number = 1): Promise<number> {
|
||||
try {
|
||||
const prefixedKey = this.getPrefixedKey(key);
|
||||
return await this.client.incrBy(prefixedKey, increment);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to incr cache key: ${key}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 原子递减
|
||||
*/
|
||||
async decr(key: string, decrement: number = 1): Promise<number> {
|
||||
return this.incr(key, -decrement);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置哈希字段
|
||||
*/
|
||||
async hset(key: string, field: string, value: any): Promise<void> {
|
||||
try {
|
||||
const prefixedKey = this.getPrefixedKey(key);
|
||||
const serializedValue = JSON.stringify(value);
|
||||
await this.client.hSet(prefixedKey, field, serializedValue);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to hset cache key: ${key}, field: ${field}`,
|
||||
error,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取哈希字段
|
||||
*/
|
||||
async hget<T = any>(key: string, field: string): Promise<T | null> {
|
||||
try {
|
||||
const prefixedKey = this.getPrefixedKey(key);
|
||||
const value = await this.client.hGet(prefixedKey, field);
|
||||
|
||||
if (value === null) {
|
||||
this.cacheStats.misses++;
|
||||
return null;
|
||||
}
|
||||
|
||||
this.cacheStats.hits++;
|
||||
return JSON.parse(value);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to hget cache key: ${key}, field: ${field}`,
|
||||
error,
|
||||
);
|
||||
this.cacheStats.misses++;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除哈希字段
|
||||
*/
|
||||
async hdel(key: string, field: string): Promise<void> {
|
||||
try {
|
||||
const prefixedKey = this.getPrefixedKey(key);
|
||||
await this.client.hDel(prefixedKey, field);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to hdel cache key: ${key}, field: ${field}`,
|
||||
error,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取哈希所有字段
|
||||
*/
|
||||
async hgetall(key: string): Promise<Record<string, any>> {
|
||||
try {
|
||||
const prefixedKey = this.getPrefixedKey(key);
|
||||
const hash = await this.client.hGetAll(prefixedKey);
|
||||
|
||||
const result: Record<string, any> = {};
|
||||
for (const [field, value] of Object.entries(hash)) {
|
||||
result[field] = JSON.parse(value);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to hgetall cache key: ${key}`, error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布消息
|
||||
*/
|
||||
async publish(channel: string, message: any): Promise<number> {
|
||||
try {
|
||||
const serializedMessage = JSON.stringify(message);
|
||||
return await this.client.publish(channel, serializedMessage);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to publish message to channel: ${channel}`,
|
||||
error,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 订阅频道
|
||||
*/
|
||||
async subscribe(
|
||||
channel: string,
|
||||
callback: (message: any) => void,
|
||||
): Promise<void> {
|
||||
try {
|
||||
await this.client.subscribe(channel, (message) => {
|
||||
try {
|
||||
const parsedMessage = JSON.parse(message);
|
||||
callback(parsedMessage);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to parse message from channel: ${channel}`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to subscribe to channel: ${channel}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消订阅
|
||||
*/
|
||||
async unsubscribe(channel: string): Promise<void> {
|
||||
try {
|
||||
await this.client.unsubscribe(channel);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to unsubscribe from channel: ${channel}`,
|
||||
error,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取带前缀的键
|
||||
*/
|
||||
private getPrefixedKey(key: string): string {
|
||||
const prefix = this.config.keyPrefix || 'cache';
|
||||
return `${prefix}:${key}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除键前缀
|
||||
*/
|
||||
private removePrefix(key: string): string {
|
||||
const prefix = this.config.keyPrefix || 'cache';
|
||||
return key.startsWith(`${prefix}:`)
|
||||
? key.substring(prefix.length + 1)
|
||||
: key;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取原始 Redis 客户端
|
||||
*/
|
||||
getClient(): RedisClientType {
|
||||
return this.client;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查连接状态
|
||||
*/
|
||||
async isConnected(): Promise<boolean> {
|
||||
try {
|
||||
await this.client.ping();
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重连
|
||||
*/
|
||||
async reconnect(): Promise<void> {
|
||||
try {
|
||||
if (this.client) {
|
||||
await this.client.quit();
|
||||
}
|
||||
await this.initializeClient();
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to reconnect Redis client', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
26
wwjcloud-nest/src/common/libraries/sentry/sentry.module.ts
Normal file
26
wwjcloud-nest/src/common/libraries/sentry/sentry.module.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Module, Global } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { SentryService } from './sentry.service';
|
||||
|
||||
/**
|
||||
* Sentry 错误追踪模块
|
||||
* 基于 NestJS 官方示例实现
|
||||
* 参考: https://docs.nestjs.cn/fundamentals/dependency-injection
|
||||
* 对应 Java: 错误追踪配置
|
||||
*/
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [ConfigModule],
|
||||
providers: [
|
||||
{
|
||||
provide: SentryService,
|
||||
useFactory: (configService: ConfigService) => {
|
||||
const sentryConfig = configService.get('sentry');
|
||||
return new SentryService(sentryConfig);
|
||||
},
|
||||
inject: [ConfigService],
|
||||
},
|
||||
],
|
||||
exports: [SentryService],
|
||||
})
|
||||
export class SentryModule {}
|
||||
687
wwjcloud-nest/src/common/libraries/sentry/sentry.service.ts
Normal file
687
wwjcloud-nest/src/common/libraries/sentry/sentry.service.ts
Normal file
@@ -0,0 +1,687 @@
|
||||
import {
|
||||
Injectable,
|
||||
Logger,
|
||||
OnModuleInit,
|
||||
OnModuleDestroy,
|
||||
} from '@nestjs/common';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import {
|
||||
ExceptionReporterInterface,
|
||||
ExceptionStatsInterface,
|
||||
ExceptionType,
|
||||
ExceptionSeverity,
|
||||
} from '../../exception/exception.interface';
|
||||
|
||||
/**
|
||||
* Sentry 服务
|
||||
* 基于 NestJS 官方示例实现
|
||||
* 参考: https://docs.nestjs.cn/fundamentals/dependency-injection
|
||||
* 对应 Java: 错误追踪服务
|
||||
*/
|
||||
@Injectable()
|
||||
export class SentryService
|
||||
implements
|
||||
ExceptionReporterInterface,
|
||||
ExceptionStatsInterface,
|
||||
OnModuleInit,
|
||||
OnModuleDestroy
|
||||
{
|
||||
private readonly logger = new Logger(SentryService.name);
|
||||
private initialized = false;
|
||||
private stats = new Map<string, number>();
|
||||
|
||||
constructor(private readonly config: any) {
|
||||
this.initializeSentry();
|
||||
}
|
||||
|
||||
async onModuleInit() {
|
||||
if (this.config?.enabled && !this.initialized) {
|
||||
try {
|
||||
Sentry.init({
|
||||
dsn: this.config.dsn,
|
||||
environment: this.config.environment || process.env.NODE_ENV,
|
||||
release: this.config.release || process.env.npm_package_version,
|
||||
tracesSampleRate: this.config.tracesSampleRate || 0.1,
|
||||
profilesSampleRate: this.config.profilesSampleRate || 0.1,
|
||||
beforeSend: this.beforeSend.bind(this),
|
||||
beforeBreadcrumb: this.beforeBreadcrumb.bind(this),
|
||||
integrations: [
|
||||
// 使用默认集成
|
||||
],
|
||||
});
|
||||
|
||||
this.initialized = true;
|
||||
this.logger.log('Sentry initialized successfully');
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to initialize Sentry', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async onModuleDestroy() {
|
||||
if (this.initialized) {
|
||||
try {
|
||||
await Sentry.close(2000);
|
||||
this.initialized = false;
|
||||
this.logger.log('Sentry closed successfully');
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to close Sentry', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private initializeSentry() {
|
||||
// Sentry 初始化在 onModuleInit 中进行
|
||||
}
|
||||
|
||||
// ==================== 异常上报接口 ====================
|
||||
|
||||
/**
|
||||
* 上报异常
|
||||
*/
|
||||
async report(exception: any, context?: any): Promise<boolean> {
|
||||
if (!this.initialized || !this.shouldReport(exception)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
Sentry.withScope((scope) => {
|
||||
// 设置标签
|
||||
this.setTags(scope, exception, context);
|
||||
|
||||
// 设置上下文
|
||||
this.setContext(scope, exception, context);
|
||||
|
||||
// 设置用户信息
|
||||
this.setUser(scope, context);
|
||||
|
||||
// 设置额外信息
|
||||
this.setExtra(scope, exception, context);
|
||||
|
||||
// 设置级别
|
||||
this.setLevel(scope, exception);
|
||||
|
||||
// 捕获异常
|
||||
Sentry.captureException(exception);
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to report exception to Sentry', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量上报异常
|
||||
*/
|
||||
async reportBatch(
|
||||
exceptions: Array<{ exception: any; context?: any }>,
|
||||
): Promise<boolean[]> {
|
||||
const results: boolean[] = [];
|
||||
|
||||
for (const { exception, context } of exceptions) {
|
||||
const result = await this.report(exception, context);
|
||||
results.push(result);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否应该上报
|
||||
*/
|
||||
shouldReport(exception: any): boolean {
|
||||
if (!this.config?.enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 根据异常类型和严重程度判断
|
||||
const type = this.getExceptionType(exception);
|
||||
const severity = this.getExceptionSeverity(exception);
|
||||
|
||||
// 只上报高严重程度的异常
|
||||
if (
|
||||
severity === ExceptionSeverity.HIGH ||
|
||||
severity === ExceptionSeverity.CRITICAL
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 某些类型的异常总是上报
|
||||
if (
|
||||
type === ExceptionType.SYSTEM ||
|
||||
type === ExceptionType.INTERNAL_SERVER_ERROR
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// ==================== 异常统计接口 ====================
|
||||
|
||||
/**
|
||||
* 记录异常统计
|
||||
*/
|
||||
record(exception: any, context?: any): void {
|
||||
const type = this.getExceptionType(exception);
|
||||
const severity = this.getExceptionSeverity(exception);
|
||||
|
||||
const key = `${type}:${severity}`;
|
||||
const count = this.stats.get(key) || 0;
|
||||
this.stats.set(key, count + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取异常统计
|
||||
*/
|
||||
getStats(timeRange?: { start: Date; end: Date }): any {
|
||||
const stats: any = {
|
||||
total: 0,
|
||||
byType: {},
|
||||
bySeverity: {},
|
||||
byTime: [],
|
||||
topExceptions: [],
|
||||
rate: 0,
|
||||
trend: 'stable',
|
||||
};
|
||||
|
||||
// 计算统计信息
|
||||
for (const [key, count] of this.stats) {
|
||||
const [type, severity] = key.split(':');
|
||||
stats.total += count;
|
||||
|
||||
stats.byType[type] = (stats.byType[type] || 0) + count;
|
||||
stats.bySeverity[severity] = (stats.bySeverity[severity] || 0) + count;
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置统计
|
||||
*/
|
||||
reset(): void {
|
||||
this.stats.clear();
|
||||
}
|
||||
|
||||
// ==================== 工具方法 ====================
|
||||
|
||||
/**
|
||||
* 设置标签
|
||||
*/
|
||||
private setTags(scope: Sentry.Scope, exception: any, context?: any): void {
|
||||
const type = this.getExceptionType(exception);
|
||||
const severity = this.getExceptionSeverity(exception);
|
||||
|
||||
scope.setTag('exception.type', type);
|
||||
scope.setTag('exception.severity', severity);
|
||||
|
||||
if (context?.request) {
|
||||
scope.setTag('http.method', context.request.method);
|
||||
scope.setTag('http.url', context.request.url);
|
||||
scope.setTag('http.status_code', context.response?.statusCode);
|
||||
}
|
||||
|
||||
if (context?.user) {
|
||||
scope.setTag('user.id', context.user.id);
|
||||
scope.setTag('user.role', context.user.role);
|
||||
}
|
||||
|
||||
if (context?.environment) {
|
||||
scope.setTag('environment', context.environment.nodeEnv);
|
||||
scope.setTag('version', context.environment.version);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置上下文
|
||||
*/
|
||||
private setContext(scope: Sentry.Scope, exception: any, context?: any): void {
|
||||
if (context?.request) {
|
||||
scope.setContext('request', {
|
||||
method: context.request.method,
|
||||
url: context.request.url,
|
||||
headers: this.sanitizeHeaders(context.request.headers),
|
||||
body: this.sanitizeBody(context.request.body),
|
||||
query: context.request.query,
|
||||
params: context.request.params,
|
||||
ip: context.request.ip,
|
||||
userAgent: context.request.userAgent,
|
||||
});
|
||||
}
|
||||
|
||||
if (context?.response) {
|
||||
scope.setContext('response', {
|
||||
statusCode: context.response.statusCode,
|
||||
headers: this.sanitizeHeaders(context.response.headers),
|
||||
body: this.sanitizeBody(context.response.body),
|
||||
size: context.response.size,
|
||||
});
|
||||
}
|
||||
|
||||
if (context?.environment) {
|
||||
scope.setContext('environment', {
|
||||
nodeEnv: context.environment.nodeEnv,
|
||||
version: context.environment.version,
|
||||
hostname: context.environment.hostname,
|
||||
pid: context.environment.pid,
|
||||
});
|
||||
}
|
||||
|
||||
if (context?.trace) {
|
||||
scope.setContext('trace', {
|
||||
traceId: context.trace.traceId,
|
||||
spanId: context.trace.spanId,
|
||||
correlationId: context.trace.correlationId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置用户信息
|
||||
*/
|
||||
private setUser(scope: Sentry.Scope, context?: any): void {
|
||||
if (context?.user) {
|
||||
scope.setUser({
|
||||
id: context.user.id,
|
||||
username: context.user.username,
|
||||
email: context.user.email,
|
||||
role: context.user.role,
|
||||
permissions: context.user.permissions,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置额外信息
|
||||
*/
|
||||
private setExtra(scope: Sentry.Scope, exception: any, context?: any): void {
|
||||
if (exception.code) {
|
||||
scope.setExtra('error.code', exception.code);
|
||||
}
|
||||
|
||||
if (exception.details) {
|
||||
scope.setExtra('error.details', exception.details);
|
||||
}
|
||||
|
||||
if (exception.cause) {
|
||||
scope.setExtra('error.cause', exception.cause);
|
||||
}
|
||||
|
||||
if (context?.meta) {
|
||||
scope.setExtra('meta', context.meta);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置级别
|
||||
*/
|
||||
private setLevel(scope: Sentry.Scope, exception: any): void {
|
||||
const severity = this.getExceptionSeverity(exception);
|
||||
|
||||
switch (severity) {
|
||||
case ExceptionSeverity.CRITICAL:
|
||||
scope.setLevel('fatal');
|
||||
break;
|
||||
case ExceptionSeverity.HIGH:
|
||||
scope.setLevel('error');
|
||||
break;
|
||||
case ExceptionSeverity.MEDIUM:
|
||||
scope.setLevel('warning');
|
||||
break;
|
||||
case ExceptionSeverity.LOW:
|
||||
scope.setLevel('info');
|
||||
break;
|
||||
default:
|
||||
scope.setLevel('error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取异常类型
|
||||
*/
|
||||
private getExceptionType(exception: any): ExceptionType {
|
||||
if (exception.name) {
|
||||
const name = exception.name.toLowerCase();
|
||||
|
||||
if (name.includes('validation') || name.includes('badrequest')) {
|
||||
return ExceptionType.VALIDATION;
|
||||
}
|
||||
if (name.includes('unauthorized') || name.includes('authentication')) {
|
||||
return ExceptionType.AUTHENTICATION;
|
||||
}
|
||||
if (name.includes('forbidden') || name.includes('authorization')) {
|
||||
return ExceptionType.AUTHORIZATION;
|
||||
}
|
||||
if (name.includes('notfound')) {
|
||||
return ExceptionType.NOT_FOUND;
|
||||
}
|
||||
if (name.includes('conflict')) {
|
||||
return ExceptionType.CONFLICT;
|
||||
}
|
||||
if (name.includes('timeout')) {
|
||||
return ExceptionType.TIMEOUT;
|
||||
}
|
||||
if (name.includes('ratelimit')) {
|
||||
return ExceptionType.RATE_LIMIT;
|
||||
}
|
||||
if (name.includes('database') || name.includes('db')) {
|
||||
return ExceptionType.DATABASE;
|
||||
}
|
||||
if (name.includes('cache')) {
|
||||
return ExceptionType.CACHE;
|
||||
}
|
||||
if (name.includes('network') || name.includes('connection')) {
|
||||
return ExceptionType.NETWORK;
|
||||
}
|
||||
if (name.includes('external') || name.includes('api')) {
|
||||
return ExceptionType.EXTERNAL_API;
|
||||
}
|
||||
if (name.includes('business')) {
|
||||
return ExceptionType.BUSINESS;
|
||||
}
|
||||
if (name.includes('system')) {
|
||||
return ExceptionType.SYSTEM;
|
||||
}
|
||||
}
|
||||
|
||||
if (exception.statusCode) {
|
||||
switch (exception.statusCode) {
|
||||
case 400:
|
||||
return ExceptionType.BAD_REQUEST;
|
||||
case 401:
|
||||
return ExceptionType.AUTHENTICATION;
|
||||
case 403:
|
||||
return ExceptionType.AUTHORIZATION;
|
||||
case 404:
|
||||
return ExceptionType.NOT_FOUND;
|
||||
case 409:
|
||||
return ExceptionType.CONFLICT;
|
||||
case 429:
|
||||
return ExceptionType.RATE_LIMIT;
|
||||
case 500:
|
||||
return ExceptionType.INTERNAL_SERVER_ERROR;
|
||||
case 503:
|
||||
return ExceptionType.SERVICE_UNAVAILABLE;
|
||||
default:
|
||||
return ExceptionType.UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
return ExceptionType.UNKNOWN;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取异常严重程度
|
||||
*/
|
||||
private getExceptionSeverity(exception: any): ExceptionSeverity {
|
||||
const type = this.getExceptionType(exception);
|
||||
|
||||
switch (type) {
|
||||
case ExceptionType.INTERNAL_SERVER_ERROR:
|
||||
case ExceptionType.SYSTEM:
|
||||
return ExceptionSeverity.CRITICAL;
|
||||
case ExceptionType.AUTHENTICATION:
|
||||
case ExceptionType.AUTHORIZATION:
|
||||
case ExceptionType.DATABASE:
|
||||
case ExceptionType.NETWORK:
|
||||
return ExceptionSeverity.HIGH;
|
||||
case ExceptionType.VALIDATION:
|
||||
case ExceptionType.BAD_REQUEST:
|
||||
case ExceptionType.CONFLICT:
|
||||
case ExceptionType.CACHE:
|
||||
case ExceptionType.EXTERNAL_API:
|
||||
return ExceptionSeverity.MEDIUM;
|
||||
case ExceptionType.NOT_FOUND:
|
||||
case ExceptionType.TIMEOUT:
|
||||
case ExceptionType.RATE_LIMIT:
|
||||
case ExceptionType.BUSINESS:
|
||||
return ExceptionSeverity.LOW;
|
||||
default:
|
||||
return ExceptionSeverity.MEDIUM;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理请求头
|
||||
*/
|
||||
private sanitizeHeaders(
|
||||
headers: Record<string, string>,
|
||||
): Record<string, string> {
|
||||
const sanitized: Record<string, string> = {};
|
||||
const sensitiveHeaders = [
|
||||
'authorization',
|
||||
'cookie',
|
||||
'x-api-key',
|
||||
'x-auth-token',
|
||||
];
|
||||
|
||||
for (const [key, value] of Object.entries(headers)) {
|
||||
if (sensitiveHeaders.includes(key.toLowerCase())) {
|
||||
sanitized[key] = '[REDACTED]';
|
||||
} else {
|
||||
sanitized[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理请求体
|
||||
*/
|
||||
private sanitizeBody(body: any): any {
|
||||
if (!body) return body;
|
||||
|
||||
if (typeof body === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(body);
|
||||
return this.sanitizeObject(parsed);
|
||||
} catch {
|
||||
return body;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof body === 'object') {
|
||||
return this.sanitizeObject(body);
|
||||
}
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理对象
|
||||
*/
|
||||
private sanitizeObject(obj: any): any {
|
||||
if (!obj || typeof obj !== 'object') return obj;
|
||||
|
||||
const sanitized = { ...obj };
|
||||
const sensitiveFields = ['password', 'token', 'secret', 'key', 'auth'];
|
||||
|
||||
for (const field of sensitiveFields) {
|
||||
if (sanitized[field]) {
|
||||
sanitized[field] = '[REDACTED]';
|
||||
}
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送前处理
|
||||
*/
|
||||
private beforeSend(event: Sentry.Event): Sentry.Event | null {
|
||||
// 过滤敏感信息
|
||||
if (event.extra) {
|
||||
event.extra = this.sanitizeObject(event.extra);
|
||||
}
|
||||
|
||||
if (event.contexts) {
|
||||
event.contexts = this.sanitizeObject(event.contexts);
|
||||
}
|
||||
|
||||
if (event.tags) {
|
||||
event.tags = this.sanitizeObject(event.tags);
|
||||
}
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
/**
|
||||
* 面包屑前处理
|
||||
*/
|
||||
private beforeBreadcrumb(
|
||||
breadcrumb: Sentry.Breadcrumb,
|
||||
): Sentry.Breadcrumb | null {
|
||||
// 过滤敏感信息
|
||||
if (breadcrumb.data) {
|
||||
breadcrumb.data = this.sanitizeObject(breadcrumb.data);
|
||||
}
|
||||
|
||||
return breadcrumb;
|
||||
}
|
||||
|
||||
// ==================== 公共方法 ====================
|
||||
|
||||
/**
|
||||
* 手动捕获异常
|
||||
*/
|
||||
captureException(exception: any, context?: any): void {
|
||||
if (this.initialized) {
|
||||
Sentry.withScope((scope) => {
|
||||
this.setTags(scope, exception, context);
|
||||
this.setContext(scope, exception, context);
|
||||
this.setUser(scope, context);
|
||||
this.setExtra(scope, exception, context);
|
||||
this.setLevel(scope, exception);
|
||||
|
||||
Sentry.captureException(exception);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动捕获消息
|
||||
*/
|
||||
captureMessage(
|
||||
message: string,
|
||||
level: Sentry.SeverityLevel = 'info',
|
||||
context?: any,
|
||||
): void {
|
||||
if (this.initialized) {
|
||||
Sentry.withScope((scope) => {
|
||||
if (context) {
|
||||
this.setContext(scope, null, context);
|
||||
this.setUser(scope, context);
|
||||
this.setExtra(scope, null, context);
|
||||
}
|
||||
|
||||
scope.setLevel(level);
|
||||
Sentry.captureMessage(message);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始事务
|
||||
*/
|
||||
startTransaction(name: string, op: string): any {
|
||||
if (this.initialized) {
|
||||
// 新版本 Sentry API 已变更,暂时返回 null
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前事务
|
||||
*/
|
||||
getCurrentTransaction(): any {
|
||||
if (this.initialized) {
|
||||
// 新版本 Sentry API 已变更,暂时返回 undefined
|
||||
return undefined;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置用户上下文
|
||||
*/
|
||||
setUserContext(user: {
|
||||
id: string;
|
||||
username?: string;
|
||||
email?: string;
|
||||
role?: string;
|
||||
}): void {
|
||||
if (this.initialized) {
|
||||
Sentry.setUser(user);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置标签
|
||||
*/
|
||||
setTag(key: string, value: string): void {
|
||||
if (this.initialized) {
|
||||
Sentry.setTag(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置上下文(公共方法)
|
||||
*/
|
||||
setContextPublic(key: string, context: any): void {
|
||||
if (this.initialized) {
|
||||
Sentry.setContext(key, context);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置额外信息(公共方法)
|
||||
*/
|
||||
setExtraPublic(key: string, value: any): void {
|
||||
if (this.initialized) {
|
||||
Sentry.setExtra(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置级别(公共方法)
|
||||
*/
|
||||
setLevelPublic(level: Sentry.SeverityLevel): void {
|
||||
if (this.initialized) {
|
||||
// 新版本 Sentry API 已变更,暂时不执行
|
||||
// Sentry.setLevel(level);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加面包屑
|
||||
*/
|
||||
addBreadcrumb(breadcrumb: Sentry.Breadcrumb): void {
|
||||
if (this.initialized) {
|
||||
Sentry.addBreadcrumb(breadcrumb);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置作用域
|
||||
*/
|
||||
configureScope(callback: (scope: Sentry.Scope) => void): void {
|
||||
if (this.initialized) {
|
||||
// 新版本 Sentry API 已变更,暂时不执行
|
||||
// Sentry.configureScope(callback);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用作用域
|
||||
*/
|
||||
withScope(callback: (scope: Sentry.Scope) => void): void {
|
||||
if (this.initialized) {
|
||||
Sentry.withScope(callback);
|
||||
}
|
||||
}
|
||||
}
|
||||
15
wwjcloud-nest/src/common/libraries/sharp/sharp.module.ts
Normal file
15
wwjcloud-nest/src/common/libraries/sharp/sharp.module.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Module, Global } from '@nestjs/common';
|
||||
import { SharpService } from './sharp.service';
|
||||
|
||||
/**
|
||||
* Sharp 图片处理库模块
|
||||
* 基于 NestJS 官方示例实现
|
||||
* 参考: https://docs.nestjs.cn/fundamentals/dependency-injection
|
||||
* 对应 Java: ImageUtils
|
||||
*/
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [SharpService],
|
||||
exports: [SharpService],
|
||||
})
|
||||
export class SharpModule {}
|
||||
161
wwjcloud-nest/src/common/libraries/sharp/sharp.service.ts
Normal file
161
wwjcloud-nest/src/common/libraries/sharp/sharp.service.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import sharp from 'sharp';
|
||||
|
||||
/**
|
||||
* Sharp 图片处理服务
|
||||
* 基于 NestJS 官方示例实现
|
||||
* 参考: https://docs.nestjs.cn/fundamentals/dependency-injection
|
||||
* 对应 Java: ImageUtils
|
||||
*/
|
||||
@Injectable()
|
||||
export class SharpService {
|
||||
/**
|
||||
* 获取 sharp 实例
|
||||
*/
|
||||
getSharp() {
|
||||
return sharp;
|
||||
}
|
||||
|
||||
/**
|
||||
* 调整图片大小
|
||||
*/
|
||||
async resize(
|
||||
input: Buffer | string,
|
||||
width: number,
|
||||
height?: number,
|
||||
options?: sharp.ResizeOptions,
|
||||
): Promise<Buffer> {
|
||||
return sharp(input).resize(width, height, options).toBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成缩略图
|
||||
*/
|
||||
async thumbnail(
|
||||
input: Buffer | string,
|
||||
size: number,
|
||||
options?: sharp.ResizeOptions,
|
||||
): Promise<Buffer> {
|
||||
return sharp(input)
|
||||
.resize(size, size, { ...options, fit: 'cover' })
|
||||
.toBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 压缩图片
|
||||
*/
|
||||
async compress(
|
||||
input: Buffer | string,
|
||||
quality: number = 80,
|
||||
): Promise<Buffer> {
|
||||
return sharp(input).jpeg({ quality }).toBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换图片格式
|
||||
*/
|
||||
async convert(
|
||||
input: Buffer | string,
|
||||
format: 'jpeg' | 'png' | 'webp' | 'gif' | 'tiff',
|
||||
): Promise<Buffer> {
|
||||
return sharp(input).toFormat(format).toBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取图片信息
|
||||
*/
|
||||
async metadata(input: Buffer | string): Promise<sharp.Metadata> {
|
||||
return sharp(input).metadata();
|
||||
}
|
||||
|
||||
/**
|
||||
* 裁剪图片
|
||||
*/
|
||||
async crop(
|
||||
input: Buffer | string,
|
||||
left: number,
|
||||
top: number,
|
||||
width: number,
|
||||
height: number,
|
||||
): Promise<Buffer> {
|
||||
return sharp(input).extract({ left, top, width, height }).toBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 旋转图片
|
||||
*/
|
||||
async rotate(input: Buffer | string, angle: number): Promise<Buffer> {
|
||||
return sharp(input).rotate(angle).toBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 翻转图片
|
||||
*/
|
||||
async flip(input: Buffer | string): Promise<Buffer> {
|
||||
return sharp(input).flip().toBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 镜像图片
|
||||
*/
|
||||
async flop(input: Buffer | string): Promise<Buffer> {
|
||||
return sharp(input).flop().toBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 模糊图片
|
||||
*/
|
||||
async blur(input: Buffer | string, sigma: number): Promise<Buffer> {
|
||||
return sharp(input).blur(sigma).toBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 锐化图片
|
||||
*/
|
||||
async sharpen(
|
||||
input: Buffer | string,
|
||||
sigma?: number,
|
||||
flat?: number,
|
||||
jagged?: number,
|
||||
): Promise<Buffer> {
|
||||
return sharp(input).sharpen(sigma, flat, jagged).toBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 调整亮度
|
||||
*/
|
||||
async modulate(
|
||||
input: Buffer | string,
|
||||
brightness: number = 1,
|
||||
): Promise<Buffer> {
|
||||
return sharp(input).modulate({ brightness }).toBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 调整对比度
|
||||
*/
|
||||
async linear(input: Buffer | string, a: number, b: number): Promise<Buffer> {
|
||||
return sharp(input).linear(a, b).toBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加水印
|
||||
*/
|
||||
async composite(
|
||||
input: Buffer | string,
|
||||
overlay: Buffer | string,
|
||||
options?: sharp.OverlayOptions,
|
||||
): Promise<Buffer> {
|
||||
return sharp(input)
|
||||
.composite([{ input: overlay, ...options }])
|
||||
.toBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成图片哈希
|
||||
*/
|
||||
async hash(input: Buffer | string): Promise<string> {
|
||||
const metadata = await sharp(input).metadata();
|
||||
return `${metadata.width}x${metadata.height}-${metadata.format}`;
|
||||
}
|
||||
}
|
||||
15
wwjcloud-nest/src/common/libraries/uuid/uuid.module.ts
Normal file
15
wwjcloud-nest/src/common/libraries/uuid/uuid.module.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Module, Global } from '@nestjs/common';
|
||||
import { UuidService } from './uuid.service';
|
||||
|
||||
/**
|
||||
* UUID 生成库模块
|
||||
* 基于 NestJS 官方示例实现
|
||||
* 参考: https://docs.nestjs.cn/fundamentals/dependency-injection
|
||||
* 对应 Java: IdUtil
|
||||
*/
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [UuidService],
|
||||
exports: [UuidService],
|
||||
})
|
||||
export class UuidModule {}
|
||||
105
wwjcloud-nest/src/common/libraries/uuid/uuid.service.ts
Normal file
105
wwjcloud-nest/src/common/libraries/uuid/uuid.service.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
// 使用 crypto 模块替代 uuid 库以避免 ES module 问题
|
||||
import { randomUUID, createHash } from 'crypto';
|
||||
|
||||
/**
|
||||
* UUID 生成服务
|
||||
* 基于 NestJS 官方示例实现
|
||||
* 参考: https://docs.nestjs.cn/fundamentals/dependency-injection
|
||||
* 对应 Java: IdUtil
|
||||
*/
|
||||
@Injectable()
|
||||
export class UuidService {
|
||||
/**
|
||||
* 生成 UUID v4 (随机)
|
||||
*/
|
||||
v4(): string {
|
||||
return randomUUID();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 UUID v1 (基于时间戳,简化实现)
|
||||
*/
|
||||
v1(): string {
|
||||
return randomUUID();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 UUID v5 (基于 SHA-1 哈希)
|
||||
*/
|
||||
v5(name: string, namespace: string): string {
|
||||
const hash = createHash('sha1');
|
||||
hash.update(namespace + name);
|
||||
const hex = hash.digest('hex');
|
||||
return (
|
||||
hex.substring(0, 8) +
|
||||
'-' +
|
||||
hex.substring(8, 12) +
|
||||
'-5' +
|
||||
hex.substring(12, 15) +
|
||||
'-' +
|
||||
hex.substring(15, 19) +
|
||||
'-' +
|
||||
hex.substring(19, 31)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 UUID v3 (基于 MD5 哈希)
|
||||
*/
|
||||
v3(name: string, namespace: string): string {
|
||||
const hash = createHash('md5');
|
||||
hash.update(namespace + name);
|
||||
const hex = hash.digest('hex');
|
||||
return (
|
||||
hex.substring(0, 8) +
|
||||
'-' +
|
||||
hex.substring(8, 12) +
|
||||
'-3' +
|
||||
hex.substring(12, 15) +
|
||||
'-' +
|
||||
hex.substring(15, 19) +
|
||||
'-' +
|
||||
hex.substring(19, 31)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 UUID 格式
|
||||
*/
|
||||
validate(uuid: string): boolean {
|
||||
const uuidRegex =
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||
return uuidRegex.test(uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 UUID 版本
|
||||
*/
|
||||
version(uuid: string): number | undefined {
|
||||
if (!this.validate(uuid)) return undefined;
|
||||
const version = uuid.charAt(14);
|
||||
return parseInt(version);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成短 UUID (8位)
|
||||
*/
|
||||
short(): string {
|
||||
return randomUUID().replace(/-/g, '').substring(0, 8);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成中 UUID (16位)
|
||||
*/
|
||||
medium(): string {
|
||||
return randomUUID().replace(/-/g, '').substring(0, 16);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成长 UUID (32位,无连字符)
|
||||
*/
|
||||
long(): string {
|
||||
return randomUUID().replace(/-/g, '');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Module, Global } from '@nestjs/common';
|
||||
import { ValidatorService } from './validator.service';
|
||||
|
||||
/**
|
||||
* Validator 验证库模块
|
||||
* 基于 NestJS 官方示例实现
|
||||
* 参考: https://docs.nestjs.cn/fundamentals/dependency-injection
|
||||
* 对应 Java: 验证工具类
|
||||
*/
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [ValidatorService],
|
||||
exports: [ValidatorService],
|
||||
})
|
||||
export class ValidatorModule {}
|
||||
@@ -0,0 +1,628 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import * as validator from 'validator';
|
||||
|
||||
/**
|
||||
* Validator 验证服务
|
||||
* 基于 NestJS 官方示例实现
|
||||
* 参考: https://docs.nestjs.cn/fundamentals/dependency-injection
|
||||
* 对应 Java: 验证工具类
|
||||
*/
|
||||
@Injectable()
|
||||
export class ValidatorService {
|
||||
/**
|
||||
* 获取 validator 实例
|
||||
*/
|
||||
getValidator() {
|
||||
return validator;
|
||||
}
|
||||
|
||||
// ==================== 字符串验证 ====================
|
||||
|
||||
/**
|
||||
* 判断是否为邮箱
|
||||
*/
|
||||
isEmail(str: string): boolean {
|
||||
return validator.isEmail(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为URL
|
||||
*/
|
||||
isURL(str: string, options?: validator.IsURLOptions): boolean {
|
||||
return validator.isURL(str, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为IP地址
|
||||
*/
|
||||
isIP(str: string, version?: validator.IPVersion): boolean {
|
||||
return validator.isIP(str, version);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为IPv4地址
|
||||
*/
|
||||
isIPv4(str: string): boolean {
|
||||
return validator.isIP(str, 4);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为IPv6地址
|
||||
*/
|
||||
isIPv6(str: string): boolean {
|
||||
return validator.isIP(str, 6);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为手机号
|
||||
*/
|
||||
isMobilePhone(str: string, locale?: validator.MobilePhoneLocale): boolean {
|
||||
return validator.isMobilePhone(str, locale);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为中文手机号
|
||||
*/
|
||||
isMobilePhoneZh(str: string): boolean {
|
||||
return validator.isMobilePhone(str, 'zh-CN');
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为身份证号
|
||||
*/
|
||||
isIdentityCard(str: string, locale?: validator.IdentityCardLocale): boolean {
|
||||
return validator.isIdentityCard(str, locale);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为中文身份证号
|
||||
*/
|
||||
isIdentityCardZh(str: string): boolean {
|
||||
return validator.isIdentityCard(str, 'zh-CN');
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为护照号
|
||||
*/
|
||||
isPassportNumber(str: string, countryCode?: string): boolean {
|
||||
// 简化实现,只检查基本格式
|
||||
return /^[A-Z0-9]{6,12}$/i.test(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为车牌号
|
||||
*/
|
||||
isLicensePlate(str: string, locale?: string): boolean {
|
||||
if (!locale) {
|
||||
// 通用车牌号格式检查
|
||||
return /^[A-Z0-9]{5,8}$/i.test(str);
|
||||
}
|
||||
return Boolean(validator.isLicensePlate(str, locale));
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为中文车牌号
|
||||
*/
|
||||
isLicensePlateZh(str: string): boolean {
|
||||
// 中国车牌号格式:省份简称 + 字母 + 5位数字/字母
|
||||
return /^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{1}[A-Z]{1}[A-Z0-9]{4}[A-Z0-9挂学警港澳]{1}$/.test(
|
||||
str,
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== 数字验证 ====================
|
||||
|
||||
/**
|
||||
* 判断是否为数字
|
||||
*/
|
||||
isNumeric(str: string): boolean {
|
||||
return validator.isNumeric(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为整数
|
||||
*/
|
||||
isInt(str: string, options?: validator.IsIntOptions): boolean {
|
||||
return validator.isInt(str, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为浮点数
|
||||
*/
|
||||
isFloat(str: string, options?: validator.IsFloatOptions): boolean {
|
||||
return validator.isFloat(str, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为十六进制
|
||||
*/
|
||||
isHexadecimal(str: string): boolean {
|
||||
return validator.isHexadecimal(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为八进制
|
||||
*/
|
||||
isOctal(str: string): boolean {
|
||||
return validator.isOctal(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为二进制
|
||||
*/
|
||||
isBinary(str: string): boolean {
|
||||
// 检查是否为二进制字符串(只包含0和1)
|
||||
return /^[01]+$/.test(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为UUID
|
||||
*/
|
||||
isUUID(str: string, version?: validator.UUIDVersion): boolean {
|
||||
return validator.isUUID(str, version);
|
||||
}
|
||||
|
||||
// ==================== 字符串格式验证 ====================
|
||||
|
||||
/**
|
||||
* 判断是否为字母
|
||||
*/
|
||||
isAlpha(str: string, locale?: validator.AlphaLocale): boolean {
|
||||
return validator.isAlpha(str, locale);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为字母数字
|
||||
*/
|
||||
isAlphanumeric(str: string, locale?: validator.AlphanumericLocale): boolean {
|
||||
return validator.isAlphanumeric(str, locale);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为ASCII
|
||||
*/
|
||||
isAscii(str: string): boolean {
|
||||
return validator.isAscii(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为Base64
|
||||
*/
|
||||
isBase64(str: string): boolean {
|
||||
return validator.isBase64(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为Base32
|
||||
*/
|
||||
isBase32(str: string): boolean {
|
||||
return validator.isBase32(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为Base58
|
||||
*/
|
||||
isBase58(str: string): boolean {
|
||||
return validator.isBase58(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为Base64URL
|
||||
*/
|
||||
isBase64URL(str: string): boolean {
|
||||
// Base64URL 格式检查(不包含 +、/、= 字符)
|
||||
return /^[A-Za-z0-9_-]+$/.test(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为十六进制颜色
|
||||
*/
|
||||
isHexColor(str: string): boolean {
|
||||
return validator.isHexColor(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为RGB颜色
|
||||
*/
|
||||
isRgbColor(str: string, includePercentValues?: boolean): boolean {
|
||||
// RGB 颜色格式检查
|
||||
const rgbPattern =
|
||||
/^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/;
|
||||
const rgbaPattern =
|
||||
/^rgba\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(0|1|0\.\d+)\s*\)$/;
|
||||
return rgbPattern.test(str) || rgbaPattern.test(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为HSL颜色
|
||||
*/
|
||||
isHslColor(str: string): boolean {
|
||||
// HSL 颜色格式检查
|
||||
const hslPattern =
|
||||
/^hsl\(\s*(\d{1,3})\s*,\s*(\d{1,3})%\s*,\s*(\d{1,3})%\s*\)$/;
|
||||
const hslaPattern =
|
||||
/^hsla\(\s*(\d{1,3})\s*,\s*(\d{1,3})%\s*,\s*(\d{1,3})%\s*,\s*(0|1|0\.\d+)\s*\)$/;
|
||||
return hslPattern.test(str) || hslaPattern.test(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为JSON
|
||||
*/
|
||||
isJSON(str: string): boolean {
|
||||
return validator.isJSON(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为JWT
|
||||
*/
|
||||
isJWT(str: string): boolean {
|
||||
return validator.isJWT(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为MongoDB ObjectId
|
||||
*/
|
||||
isMongoId(str: string): boolean {
|
||||
return validator.isMongoId(str);
|
||||
}
|
||||
|
||||
// ==================== 长度验证 ====================
|
||||
|
||||
/**
|
||||
* 判断长度是否在范围内
|
||||
*/
|
||||
isLength(str: string, options?: validator.IsLengthOptions): boolean {
|
||||
return validator.isLength(str, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为空
|
||||
*/
|
||||
isEmpty(str: string): boolean {
|
||||
return validator.isEmpty(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否不为空
|
||||
*/
|
||||
isNotEmpty(str: string): boolean {
|
||||
return !validator.isEmpty(str);
|
||||
}
|
||||
|
||||
// ==================== 日期验证 ====================
|
||||
|
||||
/**
|
||||
* 判断是否为日期
|
||||
*/
|
||||
isDate(str: string): boolean {
|
||||
return validator.isDate(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为ISO日期
|
||||
*/
|
||||
isISO8601(str: string, options?: validator.IsISO8601Options): boolean {
|
||||
return validator.isISO8601(str, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为RFC 3339日期
|
||||
*/
|
||||
isRFC3339(str: string): boolean {
|
||||
return validator.isRFC3339(str);
|
||||
}
|
||||
|
||||
// ==================== 网络验证 ====================
|
||||
|
||||
/**
|
||||
* 判断是否为域名
|
||||
*/
|
||||
isFQDN(str: string, options?: validator.IsFQDNOptions): boolean {
|
||||
return validator.isFQDN(str, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为MAC地址
|
||||
*/
|
||||
isMACAddress(str: string): boolean {
|
||||
return validator.isMACAddress(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为端口号
|
||||
*/
|
||||
isPort(str: string): boolean {
|
||||
return validator.isPort(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为数据URI
|
||||
*/
|
||||
isDataURI(str: string): boolean {
|
||||
return validator.isDataURI(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为Magnet URI
|
||||
*/
|
||||
isMagnetURI(str: string): boolean {
|
||||
return validator.isMagnetURI(str);
|
||||
}
|
||||
|
||||
// ==================== 文件验证 ====================
|
||||
|
||||
/**
|
||||
* 判断是否为文件扩展名
|
||||
*/
|
||||
isFileExtension(str: string, extensions?: string[]): boolean {
|
||||
// 文件扩展名检查
|
||||
if (!extensions || extensions.length === 0) {
|
||||
return /\.\w+$/.test(str);
|
||||
}
|
||||
const ext = str.toLowerCase().split('.').pop();
|
||||
return ext ? extensions.map((e) => e.toLowerCase()).includes(ext) : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为MIME类型
|
||||
*/
|
||||
isMimeType(str: string): boolean {
|
||||
return validator.isMimeType(str);
|
||||
}
|
||||
|
||||
// ==================== 其他验证 ====================
|
||||
|
||||
/**
|
||||
* 判断是否为信用卡号
|
||||
*/
|
||||
isCreditCard(str: string): boolean {
|
||||
return validator.isCreditCard(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为IBAN
|
||||
*/
|
||||
isIBAN(str: string): boolean {
|
||||
return validator.isIBAN(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为BIC
|
||||
*/
|
||||
isBIC(str: string): boolean {
|
||||
return validator.isBIC(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为MD5
|
||||
*/
|
||||
isMD5(str: string): boolean {
|
||||
return validator.isMD5(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为SHA1
|
||||
*/
|
||||
isSHA1(str: string): boolean {
|
||||
// SHA1 哈希值检查(40位十六进制)
|
||||
return /^[a-f0-9]{40}$/i.test(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为SHA256
|
||||
*/
|
||||
isSHA256(str: string): boolean {
|
||||
// SHA256 哈希值检查(64位十六进制)
|
||||
return /^[a-f0-9]{64}$/i.test(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为SHA384
|
||||
*/
|
||||
isSHA384(str: string): boolean {
|
||||
// SHA384 哈希值检查(96位十六进制)
|
||||
return /^[a-f0-9]{96}$/i.test(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为SHA512
|
||||
*/
|
||||
isSHA512(str: string): boolean {
|
||||
// SHA512 哈希值检查(128位十六进制)
|
||||
return /^[a-f0-9]{128}$/i.test(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为强密码
|
||||
*/
|
||||
isStrongPassword(str: string, options?: any): boolean {
|
||||
return validator.isStrongPassword(str, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为时间
|
||||
*/
|
||||
isTime(str: string, options?: validator.IsTimeOptions): boolean {
|
||||
return validator.isTime(str, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为税号
|
||||
*/
|
||||
isTaxID(str: string, locale?: string): boolean {
|
||||
// 简化实现,只检查基本格式
|
||||
return /^[A-Z0-9]{8,20}$/i.test(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为中文税号
|
||||
*/
|
||||
isTaxIDZh(str: string): boolean {
|
||||
return validator.isTaxID(str, 'zh-CN');
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为VAT
|
||||
*/
|
||||
isVAT(str: string, countryCode?: string): boolean {
|
||||
// 简化实现,只检查基本格式
|
||||
return /^[A-Z0-9]{8,15}$/i.test(str);
|
||||
}
|
||||
|
||||
// ==================== 转换方法 ====================
|
||||
|
||||
/**
|
||||
* 转换为布尔值
|
||||
*/
|
||||
toBoolean(str: string, strict?: boolean): boolean {
|
||||
return validator.toBoolean(str, strict);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为日期
|
||||
*/
|
||||
toDate(str: string): Date | null {
|
||||
const result = validator.toDate(str);
|
||||
return result || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为浮点数
|
||||
*/
|
||||
toFloat(str: string): number {
|
||||
return validator.toFloat(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为整数
|
||||
*/
|
||||
toInt(str: string, radix?: number): number {
|
||||
return validator.toInt(str, radix);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为字符串
|
||||
*/
|
||||
toString(input: any): string {
|
||||
return validator.toString(input);
|
||||
}
|
||||
|
||||
// ==================== 清理方法 ====================
|
||||
|
||||
/**
|
||||
* 清理HTML
|
||||
*/
|
||||
escape(str: string): string {
|
||||
return validator.escape(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 反转义HTML
|
||||
*/
|
||||
unescape(str: string): string {
|
||||
return validator.unescape(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理字符串
|
||||
*/
|
||||
stripLow(str: string, keepNewLines?: boolean): string {
|
||||
return validator.stripLow(str, keepNewLines);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理白名单字符
|
||||
*/
|
||||
whitelist(str: string, chars: string): string {
|
||||
return validator.whitelist(str, chars);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理黑名单字符
|
||||
*/
|
||||
blacklist(str: string, chars: string): string {
|
||||
return validator.blacklist(str, chars);
|
||||
}
|
||||
|
||||
/**
|
||||
* 标准化邮箱
|
||||
*/
|
||||
normalizeEmail(
|
||||
email: string,
|
||||
options?: validator.NormalizeEmailOptions,
|
||||
): string | false {
|
||||
return validator.normalizeEmail(email, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 标准化URL
|
||||
*/
|
||||
normalizeUrl(url: string, options?: any): string | false {
|
||||
// 简化实现,只做基本的URL规范化
|
||||
try {
|
||||
const normalized = new URL(url);
|
||||
return normalized.toString();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 工具方法 ====================
|
||||
|
||||
/**
|
||||
* 获取字符串长度
|
||||
*/
|
||||
getLength(str: string): number {
|
||||
// 获取字符串长度
|
||||
return str.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字符串字节长度
|
||||
*/
|
||||
getByteLength(str: string): number {
|
||||
// 获取字符串字节长度(UTF-8编码)
|
||||
return Buffer.byteLength(str, 'utf8');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字符串字符长度
|
||||
*/
|
||||
getCharLength(str: string): number {
|
||||
// 获取字符串字符长度(考虑Unicode字符)
|
||||
return [...str].length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字符串单词长度
|
||||
*/
|
||||
getWordLength(str: string): number {
|
||||
// 获取字符串单词长度
|
||||
return str.split(/\s+/).filter((word) => word.length > 0).length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字符串行数
|
||||
*/
|
||||
getLineLength(str: string): number {
|
||||
// 获取字符串行数
|
||||
return str.split('\n').length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字符串段落数
|
||||
*/
|
||||
getParagraphLength(str: string): number {
|
||||
// 获取字符串段落数
|
||||
return str.split(/\n\s*\n/).filter((para) => para.trim().length > 0).length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字符串句子数
|
||||
*/
|
||||
getSentenceLength(str: string): number {
|
||||
// 获取字符串句子数
|
||||
return str.split(/[.!?]+/).filter((sentence) => sentence.trim().length > 0)
|
||||
.length;
|
||||
}
|
||||
}
|
||||
26
wwjcloud-nest/src/common/libraries/winston/winston.module.ts
Normal file
26
wwjcloud-nest/src/common/libraries/winston/winston.module.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Module, Global } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { WinstonService } from './winston.service';
|
||||
|
||||
/**
|
||||
* Winston 日志库模块
|
||||
* 基于 NestJS 官方示例实现
|
||||
* 参考: https://docs.nestjs.cn/fundamentals/dependency-injection
|
||||
* 对应 Java: 日志配置
|
||||
*/
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [ConfigModule],
|
||||
providers: [
|
||||
{
|
||||
provide: WinstonService,
|
||||
useFactory: (configService: ConfigService) => {
|
||||
const winstonConfig = configService.get('winston');
|
||||
return new WinstonService(winstonConfig);
|
||||
},
|
||||
inject: [ConfigService],
|
||||
},
|
||||
],
|
||||
exports: [WinstonService],
|
||||
})
|
||||
export class WinstonModule {}
|
||||
576
wwjcloud-nest/src/common/libraries/winston/winston.service.ts
Normal file
576
wwjcloud-nest/src/common/libraries/winston/winston.service.ts
Normal file
@@ -0,0 +1,576 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import * as winston from 'winston';
|
||||
import type {
|
||||
LoggingInterface,
|
||||
StructuredLoggingInterface,
|
||||
StructuredLogData,
|
||||
RequestLogData,
|
||||
ResponseLogData,
|
||||
DatabaseQueryLogData,
|
||||
CacheOperationLogData,
|
||||
ExternalApiCallLogData,
|
||||
BusinessEventLogData,
|
||||
UserLogData,
|
||||
LoggingConfig,
|
||||
} from '../../logging/logging.interface';
|
||||
import { LogLevel } from '../../logging/logging.interface';
|
||||
|
||||
/**
|
||||
* Winston 服务
|
||||
* 基于 NestJS 官方示例实现
|
||||
* 参考: https://docs.nestjs.cn/fundamentals/dependency-injection
|
||||
* 对应 Java: 日志服务
|
||||
*/
|
||||
@Injectable()
|
||||
export class WinstonService
|
||||
implements LoggingInterface, StructuredLoggingInterface
|
||||
{
|
||||
private readonly logger = new Logger(WinstonService.name);
|
||||
private winston: winston.Logger;
|
||||
private currentLevel: LogLevel = LogLevel.INFO;
|
||||
|
||||
constructor(
|
||||
private readonly config: LoggingConfig = {
|
||||
level: LogLevel.INFO,
|
||||
format: 'json',
|
||||
timestamp: true,
|
||||
colorize: false,
|
||||
prettyPrint: false,
|
||||
silent: false,
|
||||
exitOnError: false,
|
||||
transports: [{ type: 'console', level: LogLevel.INFO }],
|
||||
defaultMeta: {},
|
||||
context: 'WinstonService',
|
||||
},
|
||||
) {
|
||||
this.initializeWinston();
|
||||
}
|
||||
|
||||
private initializeWinston() {
|
||||
const transports: winston.transport[] = [];
|
||||
|
||||
// 控制台传输器
|
||||
if (this.config.transports.some((t) => t.type === 'console')) {
|
||||
const consoleTransport = new winston.transports.Console({
|
||||
level: this.config.level,
|
||||
format: this.getFormat('console'),
|
||||
});
|
||||
transports.push(consoleTransport);
|
||||
}
|
||||
|
||||
// 文件传输器
|
||||
const fileTransports = this.config.transports.filter(
|
||||
(t) => t.type === 'file',
|
||||
);
|
||||
for (const fileTransport of fileTransports) {
|
||||
const transport = new winston.transports.File({
|
||||
level: fileTransport.level || this.config.level,
|
||||
filename: fileTransport.options?.filename || 'app.log',
|
||||
format: this.getFormat('file'),
|
||||
...fileTransport.options,
|
||||
});
|
||||
transports.push(transport);
|
||||
}
|
||||
|
||||
// HTTP传输器
|
||||
const httpTransports = this.config.transports.filter(
|
||||
(t) => t.type === 'http',
|
||||
);
|
||||
for (const httpTransport of httpTransports) {
|
||||
const transport = new winston.transports.Http({
|
||||
level: httpTransport.level || this.config.level,
|
||||
host: httpTransport.options?.host || 'localhost',
|
||||
port: httpTransport.options?.port || 80,
|
||||
path: httpTransport.options?.path || '/logs',
|
||||
...httpTransport.options,
|
||||
});
|
||||
transports.push(transport);
|
||||
}
|
||||
|
||||
// 流传输器
|
||||
const streamTransports = this.config.transports.filter(
|
||||
(t) => t.type === 'stream',
|
||||
);
|
||||
for (const streamTransport of streamTransports) {
|
||||
const transport = new winston.transports.Stream({
|
||||
level: streamTransport.level || this.config.level,
|
||||
stream: streamTransport.options?.stream,
|
||||
format: this.getFormat('stream'),
|
||||
...streamTransport.options,
|
||||
});
|
||||
transports.push(transport);
|
||||
}
|
||||
|
||||
this.winston = winston.createLogger({
|
||||
level: this.config.level,
|
||||
format: this.getFormat('default'),
|
||||
defaultMeta: this.config.defaultMeta,
|
||||
transports,
|
||||
silent: this.config.silent,
|
||||
exitOnError: this.config.exitOnError,
|
||||
});
|
||||
}
|
||||
|
||||
private getFormat(type: string): winston.Logform.Format {
|
||||
const formats: winston.Logform.Format[] = [];
|
||||
|
||||
// 时间戳
|
||||
if (this.config.timestamp) {
|
||||
formats.push(winston.format.timestamp());
|
||||
}
|
||||
|
||||
// 日志级别
|
||||
// winston.format.level() 在新版本中已移除
|
||||
|
||||
// 消息格式
|
||||
if (this.config.format === 'json') {
|
||||
formats.push(winston.format.json());
|
||||
} else if (this.config.format === 'simple') {
|
||||
formats.push(winston.format.simple());
|
||||
} else {
|
||||
formats.push(
|
||||
winston.format.printf(
|
||||
({ timestamp, level, message, context, ...meta }) => {
|
||||
const contextStr = context ? `[${context}] ` : '';
|
||||
const metaStr =
|
||||
Object.keys(meta).length > 0 ? ` ${JSON.stringify(meta)}` : '';
|
||||
return `${timestamp} [${level}] ${contextStr}${message}${metaStr}`;
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 颜色
|
||||
if (this.config.colorize && type === 'console') {
|
||||
formats.push(winston.format.colorize());
|
||||
}
|
||||
|
||||
// 美化打印
|
||||
if (this.config.prettyPrint) {
|
||||
formats.push(winston.format.prettyPrint());
|
||||
}
|
||||
|
||||
return winston.format.combine(...formats);
|
||||
}
|
||||
|
||||
// ==================== 基础日志接口 ====================
|
||||
|
||||
/**
|
||||
* 记录调试日志
|
||||
*/
|
||||
debug(message: string, context?: string, meta?: Record<string, any>): void {
|
||||
this.winston.debug(message, { context, ...meta });
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录信息日志
|
||||
*/
|
||||
info(message: string, context?: string, meta?: Record<string, any>): void {
|
||||
this.winston.info(message, { context, ...meta });
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录警告日志
|
||||
*/
|
||||
warn(message: string, context?: string, meta?: Record<string, any>): void {
|
||||
this.winston.warn(message, { context, ...meta });
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录错误日志
|
||||
*/
|
||||
error(message: string, context?: string, meta?: Record<string, any>): void {
|
||||
this.winston.error(message, { context, ...meta });
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录致命错误日志
|
||||
*/
|
||||
fatal(message: string, context?: string, meta?: Record<string, any>): void {
|
||||
this.winston.error(message, { context, level: 'fatal', ...meta });
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录日志
|
||||
*/
|
||||
log(
|
||||
level: LogLevel,
|
||||
message: string,
|
||||
context?: string,
|
||||
meta?: Record<string, any>,
|
||||
): void {
|
||||
this.winston.log(level, message, { context, ...meta });
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置日志级别
|
||||
*/
|
||||
setLevel(level: LogLevel): void {
|
||||
this.currentLevel = level;
|
||||
this.winston.level = level;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前日志级别
|
||||
*/
|
||||
getLevel(): LogLevel {
|
||||
return this.currentLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建子日志器
|
||||
*/
|
||||
child(context: string): LoggingInterface {
|
||||
const childLogger = this.winston.child({ context });
|
||||
return new WinstonChildService(childLogger, context);
|
||||
}
|
||||
|
||||
// ==================== 结构化日志接口 ====================
|
||||
|
||||
/**
|
||||
* 记录结构化日志
|
||||
*/
|
||||
logStructured(
|
||||
level: LogLevel,
|
||||
message: string,
|
||||
structuredData: StructuredLogData,
|
||||
): void {
|
||||
this.winston.log(level, message, structuredData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录请求日志
|
||||
*/
|
||||
logRequest(
|
||||
request: RequestLogData,
|
||||
response: ResponseLogData,
|
||||
duration: number,
|
||||
): void {
|
||||
const logData = {
|
||||
type: 'http_request',
|
||||
request: {
|
||||
method: request.method,
|
||||
url: request.url,
|
||||
headers: this.sanitizeHeaders(request.headers),
|
||||
body: this.sanitizeBody(request.body),
|
||||
query: request.query,
|
||||
params: request.params,
|
||||
ip: request.ip,
|
||||
userAgent: request.userAgent,
|
||||
userId: request.userId,
|
||||
sessionId: request.sessionId,
|
||||
},
|
||||
response: {
|
||||
statusCode: response.statusCode,
|
||||
headers: this.sanitizeHeaders(response.headers),
|
||||
body: this.sanitizeBody(response.body),
|
||||
size: response.size,
|
||||
},
|
||||
duration,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
this.winston.info('HTTP Request', logData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录数据库查询日志
|
||||
*/
|
||||
logDatabaseQuery(
|
||||
query: DatabaseQueryLogData,
|
||||
duration: number,
|
||||
result?: any,
|
||||
): void {
|
||||
const logData = {
|
||||
type: 'database_query',
|
||||
query: {
|
||||
operation: query.operation,
|
||||
table: query.table,
|
||||
query: query.query,
|
||||
params: query.params,
|
||||
connection: query.connection,
|
||||
transaction: query.transaction,
|
||||
},
|
||||
duration,
|
||||
result: this.sanitizeResult(result),
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
this.winston.info('Database Query', logData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录缓存操作日志
|
||||
*/
|
||||
logCacheOperation(
|
||||
operation: CacheOperationLogData,
|
||||
hit: boolean,
|
||||
duration: number,
|
||||
): void {
|
||||
const logData = {
|
||||
type: 'cache_operation',
|
||||
operation: {
|
||||
operation: operation.operation,
|
||||
key: operation.key,
|
||||
ttl: operation.ttl,
|
||||
size: operation.size,
|
||||
},
|
||||
hit,
|
||||
duration,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
this.winston.info('Cache Operation', logData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录外部API调用日志
|
||||
*/
|
||||
logExternalApiCall(
|
||||
apiCall: ExternalApiCallLogData,
|
||||
response: any,
|
||||
duration: number,
|
||||
): void {
|
||||
const logData = {
|
||||
type: 'external_api_call',
|
||||
apiCall: {
|
||||
service: apiCall.service,
|
||||
endpoint: apiCall.endpoint,
|
||||
method: apiCall.method,
|
||||
headers: this.sanitizeHeaders(apiCall.headers),
|
||||
body: this.sanitizeBody(apiCall.body),
|
||||
timeout: apiCall.timeout,
|
||||
},
|
||||
response: this.sanitizeResponse(response),
|
||||
duration,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
this.winston.info('External API Call', logData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录业务事件日志
|
||||
*/
|
||||
logBusinessEvent(
|
||||
event: BusinessEventLogData,
|
||||
user?: UserLogData,
|
||||
meta?: Record<string, any>,
|
||||
): void {
|
||||
const logData = {
|
||||
type: 'business_event',
|
||||
event: {
|
||||
event: event.event,
|
||||
action: event.action,
|
||||
resource: event.resource,
|
||||
resourceId: event.resourceId,
|
||||
data: event.data,
|
||||
result: event.result,
|
||||
},
|
||||
user: user
|
||||
? {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
permissions: user.permissions,
|
||||
}
|
||||
: undefined,
|
||||
meta,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
this.winston.info('Business Event', logData);
|
||||
}
|
||||
|
||||
// ==================== 工具方法 ====================
|
||||
|
||||
/**
|
||||
* 清理请求头
|
||||
*/
|
||||
private sanitizeHeaders(
|
||||
headers: Record<string, string>,
|
||||
): Record<string, string> {
|
||||
const sanitized: Record<string, string> = {};
|
||||
const sensitiveHeaders = [
|
||||
'authorization',
|
||||
'cookie',
|
||||
'x-api-key',
|
||||
'x-auth-token',
|
||||
];
|
||||
|
||||
for (const [key, value] of Object.entries(headers)) {
|
||||
if (sensitiveHeaders.includes(key.toLowerCase())) {
|
||||
sanitized[key] = '[REDACTED]';
|
||||
} else {
|
||||
sanitized[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理请求体
|
||||
*/
|
||||
private sanitizeBody(body: any): any {
|
||||
if (!body) return body;
|
||||
|
||||
if (typeof body === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(body);
|
||||
return this.sanitizeObject(parsed);
|
||||
} catch {
|
||||
return body;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof body === 'object') {
|
||||
return this.sanitizeObject(body);
|
||||
}
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理对象
|
||||
*/
|
||||
private sanitizeObject(obj: any): any {
|
||||
if (!obj || typeof obj !== 'object') return obj;
|
||||
|
||||
const sanitized = { ...obj };
|
||||
const sensitiveFields = ['password', 'token', 'secret', 'key', 'auth'];
|
||||
|
||||
for (const field of sensitiveFields) {
|
||||
if (sanitized[field]) {
|
||||
sanitized[field] = '[REDACTED]';
|
||||
}
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理结果
|
||||
*/
|
||||
private sanitizeResult(result: any): any {
|
||||
if (!result) return result;
|
||||
|
||||
if (typeof result === 'object') {
|
||||
return this.sanitizeObject(result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理响应
|
||||
*/
|
||||
private sanitizeResponse(response: any): any {
|
||||
if (!response) return response;
|
||||
|
||||
if (typeof response === 'object') {
|
||||
return this.sanitizeObject(response);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取原始 Winston 实例
|
||||
*/
|
||||
getWinston(): winston.Logger {
|
||||
return this.winston;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加传输器
|
||||
*/
|
||||
addTransport(transport: winston.transport): void {
|
||||
this.winston.add(transport);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除传输器
|
||||
*/
|
||||
removeTransport(transport: winston.transport): void {
|
||||
this.winston.remove(transport);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空传输器
|
||||
*/
|
||||
clearTransports(): void {
|
||||
this.winston.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭日志器
|
||||
*/
|
||||
async close(): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
this.winston.end(() => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Winston 子日志器服务
|
||||
*/
|
||||
class WinstonChildService implements LoggingInterface {
|
||||
constructor(
|
||||
private readonly winston: winston.Logger,
|
||||
private readonly context: string,
|
||||
) {}
|
||||
|
||||
debug(message: string, context?: string, meta?: Record<string, any>): void {
|
||||
this.winston.debug(message, { context: context || this.context, ...meta });
|
||||
}
|
||||
|
||||
info(message: string, context?: string, meta?: Record<string, any>): void {
|
||||
this.winston.info(message, { context: context || this.context, ...meta });
|
||||
}
|
||||
|
||||
warn(message: string, context?: string, meta?: Record<string, any>): void {
|
||||
this.winston.warn(message, { context: context || this.context, ...meta });
|
||||
}
|
||||
|
||||
error(message: string, context?: string, meta?: Record<string, any>): void {
|
||||
this.winston.error(message, { context: context || this.context, ...meta });
|
||||
}
|
||||
|
||||
fatal(message: string, context?: string, meta?: Record<string, any>): void {
|
||||
this.winston.error(message, {
|
||||
context: context || this.context,
|
||||
level: 'fatal',
|
||||
...meta,
|
||||
});
|
||||
}
|
||||
|
||||
log(
|
||||
level: LogLevel,
|
||||
message: string,
|
||||
context?: string,
|
||||
meta?: Record<string, any>,
|
||||
): void {
|
||||
this.winston.log(level, message, {
|
||||
context: context || this.context,
|
||||
...meta,
|
||||
});
|
||||
}
|
||||
|
||||
setLevel(level: LogLevel): void {
|
||||
this.winston.level = level;
|
||||
}
|
||||
|
||||
getLevel(): LogLevel {
|
||||
return this.winston.level as LogLevel;
|
||||
}
|
||||
|
||||
child(context: string): LoggingInterface {
|
||||
const childLogger = this.winston.child({ context });
|
||||
return new WinstonChildService(childLogger, context);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user