docs: 基本完成初版数据库设计

This commit is contained in:
R524809
2026-10-15 10:09:34 +08:00
parent 63a6993276
commit e6ded00a4c
41 changed files with 2597 additions and 180 deletions
+154
View File
@@ -0,0 +1,154 @@
import { DynamicModule, Global, Module } from '@nestjs/common';
import {
getRuntimeOptions,
type RuntimeOptions,
} from '../common/runtime-options';
/** 注入已经校验的配置,业务模块无需直接读取 process.env。 */
export const SERVER_CONFIG = Symbol('SERVER_CONFIG');
/** API 契约的主版本前缀,与服务端发布版本、小程序版本分别维护。 */
export const API_PREFIX = 'api/v1';
export type LogLevel = 'error' | 'warn' | 'info' | 'debug';
/** 规范化的启动配置;仅在服务端使用,包含不得下发或记录的 JWT 密钥。 */
export interface ServerConfig extends RuntimeOptions {
nodeEnv: 'development' | 'test' | 'production';
logLevel: LogLevel;
bodyLimitBytes: number;
swaggerEnabled: boolean;
corsOrigins: string[];
trustedProxyCidrs: string[];
jwtSecret: string;
jwtIssuer: string;
jwtAudience: string;
jwtExpiresInSeconds: number;
}
/** 按十进制整数字符串解析并检查范围,防止空串、科学计数法等被 Number 宽松接受。 */
function integer(
env: NodeJS.ProcessEnv,
key: string,
fallback: number,
min: number,
max: number,
): number {
const raw = env[key] ?? String(fallback);
const value = Number(raw);
if (
!/^\d+$/.test(raw) ||
!Number.isSafeInteger(value) ||
value < min ||
value > max
) {
throw new Error(`${key} must be an integer between ${min} and ${max}`);
}
return value;
}
/** 启动前一次性校验环境变量;错误消息只包含配置项要求,不回显实际配置值。 */
export function getServerConfig(
env: NodeJS.ProcessEnv = process.env,
): ServerConfig {
const nodeEnv = env.NODE_ENV ?? 'development';
if (!['development', 'test', 'production'].includes(nodeEnv)) {
throw new Error('NODE_ENV must be development, test or production');
}
const logLevel = env.LOG_LEVEL ?? 'info';
if (!['error', 'warn', 'info', 'debug'].includes(logLevel)) {
throw new Error('LOG_LEVEL must be error, warn, info or debug');
}
// 未显式配置时生产关闭文档,避免沿用开发环境的公开文档入口。
const rawSwagger =
env.SWAGGER_ENABLED ?? (nodeEnv === 'production' ? 'false' : 'true');
if (!['true', 'false'].includes(rawSwagger)) {
throw new Error('SWAGGER_ENABLED must be true or false');
}
// 密钥没有默认值;按字节检查长度,并拒绝示例中的占位密码。
const jwtSecret = env.JWT_SECRET ?? '';
if (
Buffer.byteLength(jwtSecret.trim()) < 32 ||
jwtSecret.includes('change-me')
) {
throw new Error(
'JWT_SECRET must be a configured secret of at least 32 bytes',
);
}
const jwtIssuer = (env.JWT_ISSUER ?? 'doodle-store').trim();
const jwtAudience = (env.JWT_AUDIENCE ?? 'doodle-mini').trim();
if (!jwtIssuer || !jwtAudience) {
throw new Error('JWT_ISSUER and JWT_AUDIENCE must not be empty');
}
// 只接受可直接比较的完整 origin;路径、凭证、通配符不属于白名单项。
const corsOrigins = (env.CORS_ORIGINS ?? '')
.split(',')
.map((origin) => origin.trim())
.filter(Boolean);
for (const origin of corsOrigins) {
let url: URL;
try {
url = new URL(origin);
} catch {
throw new Error('CORS_ORIGINS must contain HTTP(S) origins');
}
if (
!['http:', 'https:'].includes(url.protocol) ||
url.origin !== origin
) {
throw new Error(
'CORS_ORIGINS must contain exact HTTP(S) origins without paths',
);
}
}
const trustedProxyCidrs = (env.TRUSTED_PROXY_CIDRS ?? '')
.split(',')
.map((cidr) => cidr.trim())
.filter(Boolean);
// Express 在安装 trust proxy 时校验具体网段;不接受 true 或 hop count。
if (
trustedProxyCidrs.some(
(value) => ['true', 'false'].includes(value) || /^\d+$/.test(value),
)
) {
throw new Error(
'TRUSTED_PROXY_CIDRS must contain explicit proxy addresses or CIDRs',
);
}
return {
...getRuntimeOptions(env),
nodeEnv: nodeEnv as ServerConfig['nodeEnv'],
logLevel: logLevel as LogLevel,
bodyLimitBytes: integer(
env,
'API_BODY_LIMIT_BYTES',
1_048_576,
1024,
10 * 1024 * 1024,
),
swaggerEnabled: rawSwagger === 'true',
corsOrigins,
trustedProxyCidrs,
jwtSecret,
jwtIssuer,
jwtAudience,
jwtExpiresInSeconds: integer(
env,
'JWT_EXPIRES_IN_SECONDS',
86_400,
60,
604_800,
),
};
}
@Global()
@Module({})
/** 将同一份配置提供给所有模块;测试可传入独立配置,避免修改进程环境。 */
export class ServerConfigModule {
static forRoot(config: ServerConfig): DynamicModule {
return {
module: ServerConfigModule,
providers: [{ provide: SERVER_CONFIG, useValue: config }],
exports: [SERVER_CONFIG],
};
}
}