- 更新 .env.local 中的域名配置 - 更新 infra/deploy/.env.local - 更新 compose.local.yaml 中的环境变量 - 更新 admin-web 构建参数 VITE_API_BASE_URL - 更新小程序 app.ts 中的 apiBaseUrl
120 lines
4.7 KiB
TypeScript
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',
|
|
};
|
|
}
|
|
}
|