Files
lehuo/apps/api/src/config/app-config.service.ts
T
climber 4412a416ed fix: 更新域名从 lhgy.net 到 www.lhgy.net
- 更新 .env.local 中的域名配置
- 更新 infra/deploy/.env.local
- 更新 compose.local.yaml 中的环境变量
- 更新 admin-web 构建参数 VITE_API_BASE_URL
- 更新小程序 app.ts 中的 apiBaseUrl
2026-10-24 19:49:59 +08:00

120 lines
4.7 KiB
TypeScript

import { Injectable } from '@nestjs/common';
export interface DatabaseConnectionOptions {
host: string;
port: number;
user: string;
password: string;
database: string;
allowPublicKeyRetrieval?: boolean;
ssl?: boolean;
}
function requiredEnvironmentValue(name: string): string {
const value = process.env[name]?.trim();
if (!value) {
throw new Error(`Missing required environment variable: ${name}`);
}
return value;
}
function positiveInteger(name: string, fallback: number): number {
const raw = process.env[name]?.trim();
if (!raw) return fallback;
const value = Number(raw);
if (!Number.isSafeInteger(value) || value <= 0) {
throw new Error(`${name} must be a positive integer`);
}
return value;
}
function parseBoolean(name: string, fallback: boolean): boolean {
const raw = process.env[name]?.trim().toLowerCase();
if (!raw) return fallback;
if (raw === 'true') return true;
if (raw === 'false') return false;
throw new Error(`${name} must be true or false`);
}
@Injectable()
export class AppConfigService {
readonly appEnvironment = process.env.APP_ENV?.trim() || 'development';
readonly apiPort = positiveInteger('API_PORT', 3000);
readonly databaseUrl = requiredEnvironmentValue('DATABASE_URL');
readonly sessionTokenPepper = requiredEnvironmentValue('SESSION_TOKEN_PEPPER');
readonly auditIpHashKey = requiredEnvironmentValue('AUDIT_IP_HASH_KEY');
readonly adminWebOrigin = requiredEnvironmentValue('ADMIN_WEB_ORIGIN');
readonly adminCookieSecure = parseBoolean('ADMIN_COOKIE_SECURE', true);
readonly adminSessionTtlSeconds = positiveInteger('AUTH_SESSION_TTL_SECONDS', 28_800);
readonly wechatSessionTtlSeconds = positiveInteger('WECHAT_SESSION_TTL_SECONDS', 7_200);
readonly wechatAppId = process.env.WECHAT_APP_ID?.trim() || null;
readonly wechatAppSecret = process.env.WECHAT_APP_SECRET?.trim() || null;
readonly wechatNotificationTemplates = {
APPLICATION_SUBMITTED: process.env.WECHAT_TEMPLATE_APPLICATION_SUBMITTED?.trim() || null,
APPLICATION_ACCEPTED: process.env.WECHAT_TEMPLATE_APPLICATION_ACCEPTED?.trim() || null,
SERVICE_ARRANGED: process.env.WECHAT_TEMPLATE_SERVICE_ARRANGED?.trim() || null,
SERVICE_COMPLETED: process.env.WECHAT_TEMPLATE_SERVICE_COMPLETED?.trim() || null,
FOLLOW_UP_PENDING: process.env.WECHAT_TEMPLATE_FOLLOW_UP_PENDING?.trim() || null,
ACTIVITY_REGISTRATION_RESULT:
process.env.WECHAT_TEMPLATE_ACTIVITY_REGISTRATION_RESULT?.trim() || null,
} as const;
readonly notificationWorkerEnabled = parseBoolean('NOTIFICATION_WORKER_ENABLED', false);
readonly objectStorageEndpoint = process.env.OBJECT_STORAGE_ENDPOINT?.trim() || null;
readonly objectStorageRegion = process.env.OBJECT_STORAGE_REGION?.trim() || 'auto';
readonly objectStorageBucket = process.env.OBJECT_STORAGE_BUCKET?.trim() || null;
readonly objectStorageAccessKey = process.env.OBJECT_STORAGE_ACCESS_KEY?.trim() || null;
readonly objectStorageSecretKey = process.env.OBJECT_STORAGE_SECRET_KEY?.trim() || null;
readonly objectStorageForcePathStyle = parseBoolean('OBJECT_STORAGE_FORCE_PATH_STYLE', true);
readonly fieldEncryptionKey = this.parseEncryptionKey();
readonly database = this.parseDatabaseUrl();
get isProduction(): boolean {
return this.appEnvironment === 'production';
}
get objectStorageConfigured(): boolean {
return Boolean(
this.objectStorageEndpoint &&
this.objectStorageBucket &&
this.objectStorageAccessKey &&
this.objectStorageSecretKey,
);
}
get wechatNotificationConfigured(): boolean {
return Boolean(
this.wechatAppId &&
this.wechatAppSecret &&
Object.values(this.wechatNotificationTemplates).every(Boolean),
);
}
private parseEncryptionKey(): Buffer {
const encoded = requiredEnvironmentValue('FIELD_ENCRYPTION_KEY');
const key = Buffer.from(encoded, 'base64');
if (key.length !== 32) {
throw new Error('FIELD_ENCRYPTION_KEY must be a base64-encoded 32-byte key');
}
return key;
}
private parseDatabaseUrl(): DatabaseConnectionOptions {
const url = new URL(this.databaseUrl);
if (url.protocol !== 'mysql:') {
throw new Error('DATABASE_URL must use the mysql protocol');
}
const database = url.pathname.replace(/^\//, '');
if (!url.hostname || !url.username || !database) {
throw new Error('DATABASE_URL must include host, user, and database');
}
return {
host: url.hostname,
port: url.port ? Number(url.port) : 3306,
user: decodeURIComponent(url.username),
password: decodeURIComponent(url.password),
database,
allowPublicKeyRetrieval: url.searchParams.get('allowPublicKeyRetrieval') === 'true',
ssl: url.searchParams.get('useSSL') === 'true',
};
}
}