选择:微服务架构 + 云原生设计
选择理由:
架构特点:
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Web前端 │ │ 移动端App │ │ 管理后台 │
│ (Vue.js) │ │ (React Native)│ │ (React) │
└─────────┬───────┘ └─────────┬───────┘ └─────────┬───────┘
│ │ │
└──────────────────────┼──────────────────────┘
│
┌─────────────┴─────────────┐
│ API网关 │
│ (Kong/Nginx Gateway) │
└─────────────┬─────────────┘
│
┌────────────────────────┼────────────────────────┐
│ │ │
┌───────▼───────┐ ┌─────────▼─────────┐ ┌───────▼───────┐
│ 用户服务 │ │ 面试服务 │ │ 企业服务 │
│ (User Service) │ │(Interview Service)│ │(Company Service)│
└───────┬───────┘ └─────────┬─────────┘ └───────┬───────┘
│ │ │
┌───────▼───────┐ ┌─────────▼─────────┐ ┌───────▼───────┐
│ 简历服务 │ │ AI分析服务 │ │ 通知服务 │
│(Resume Service)│ │ (AI Analysis) │ │(Notification) │
└───────┬───────┘ └─────────┬─────────┘ └───────┬───────┘
│ │ │
└──────────────────────┼──────────────────────┘
│
┌─────────▼─────────┐
│ 消息队列 │
│ (Apache Kafka) │
└───────────────────┘
核心服务说明:
外部服务集成:
选择:Vue.js 3 + TypeScript
选择理由:
技术栈:
// 核心框架
Vue.js 3.3+
TypeScript 5.0+
Vite 4.0+
// 路由和状态管理
Vue Router 4
Pinia
// UI组件库
Element Plus
Tailwind CSS
// 工具库
Axios (HTTP客户端)
Day.js (日期处理)
Lodash (工具函数)
选择:Pinia
优势:
状态结构:
// stores/user.ts
export const useUserStore = defineStore('user', {
state: () => ({
currentUser: null,
isAuthenticated: false,
permissions: []
})
})
// stores/interview.ts
export const useInterviewStore = defineStore('interview', {
state: () => ({
currentInterview: null,
questions: [],
answers: [],
status: 'idle'
})
})
主要选择:Element Plus + Tailwind CSS
理由:
项目结构:
src/
├── components/ # 通用组件
│ ├── common/ # 基础组件
│ ├── business/ # 业务组件
│ └── layout/ # 布局组件
├── views/ # 页面组件
│ ├── auth/ # 认证相关
│ ├── interview/ # 面试相关
│ ├── resume/ # 简历相关
│ └── dashboard/ # 仪表板
├── stores/ # 状态管理
├── composables/ # 组合式函数
├── utils/ # 工具函数
├── types/ # TypeScript类型
└── assets/ # 静态资源
性能优化措施:
选择:Node.js + NestJS + TypeScript
选择理由:
技术栈详情:
// 核心框架
Node.js 18+
NestJS 10+
TypeScript 5.0+
// 数据库ORM
TypeORM / Prisma
// 验证和安全
class-validator
Passport.js
JWT
// 消息队列
Bull (Redis-based)
// 文档和测试
Swagger
Jest
备选方案:
选择:RESTful API + GraphQL混合
RESTful API用于:
GraphQL用于:
API设计规范:
// RESTful API示例
GET /api/v1/users/:id
POST /api/v1/users
PUT /api/v1/users/:id
DELETE /api/v1/users/:id
// GraphQL Schema示例
type User {
id: ID!
email: String!
profile: UserProfile
interviews: [Interview!]!
}
type Query {
user(id: ID!): User
interviews(filter: InterviewFilter): [Interview!]!
}
设计方案:JWT + RBAC
认证流程:
// JWT Token结构
interface JWTPayload {
userId: string
email: string
roles: string[]
permissions: string[]
exp: number
}
// 权限控制装饰器
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('hr', 'admin')
@Controller('interviews')
export class InterviewController {
@Post()
@RequirePermissions('interview:create')
createInterview(@Body() dto: CreateInterviewDto) {
// 创建面试逻辑
}
}
安全措施:
采用:领域驱动设计(DDD) + 分层架构
分层结构:
// 领域层 (Domain Layer)
export class Interview {
constructor(
private readonly id: InterviewId,
private readonly candidateId: UserId,
private readonly jobId: JobId,
private status: InterviewStatus
) {}
public start(): void {
if (this.status !== InterviewStatus.SCHEDULED) {
throw new Error('Interview cannot be started');
}
this.status = InterviewStatus.IN_PROGRESS;
}
}
// 应用层 (Application Layer)
@Injectable()
export class InterviewService {
constructor(
private readonly interviewRepo: InterviewRepository,
private readonly aiService: AIAnalysisService
) {}
async conductInterview(command: ConductInterviewCommand): Promise<void> {
const interview = await this.interviewRepo.findById(command.interviewId);
interview.start();
await this.interviewRepo.save(interview);
// 发布领域事件
this.eventBus.publish(new InterviewStartedEvent(interview.id));
}
}
// 基础设施层 (Infrastructure Layer)
@Injectable()
export class TypeOrmInterviewRepository implements InterviewRepository {
constructor(
@InjectRepository(InterviewEntity)
private readonly repo: Repository<InterviewEntity>
) {}
async findById(id: InterviewId): Promise<Interview> {
const entity = await this.repo.findOne({ where: { id: id.value } });
return InterviewMapper.toDomain(entity);
}
}
选择:Redis + Bull队列
队列设计:
// 队列定义
export enum QueueNames {
RESUME_PROCESSING = 'resume-processing',
VIDEO_ANALYSIS = 'video-analysis',
EMAIL_NOTIFICATION = 'email-notification',
REPORT_GENERATION = 'report-generation'
}
// 任务处理器
@Processor(QueueNames.RESUME_PROCESSING)
export class ResumeProcessor {
@Process('parse-resume')
async parseResume(job: Job<ParseResumeData>) {
const { resumeId, fileUrl } = job.data;
// 1. 下载文件
const fileBuffer = await this.fileService.download(fileUrl);
// 2. AI解析
const parsedData = await this.aiService.parseResume(fileBuffer);
// 3. 保存结果
await this.resumeService.updateParsedData(resumeId, parsedData);
// 4. 更新进度
job.progress(100);
}
}
// 任务调度
@Injectable()
export class TaskScheduler {
constructor(
@InjectQueue(QueueNames.RESUME_PROCESSING)
private resumeQueue: Queue
) {}
async scheduleResumeProcessing(resumeId: string, fileUrl: string) {
await this.resumeQueue.add('parse-resume', {
resumeId,
fileUrl
}, {
attempts: 3,
backoff: 'exponential',
delay: 2000
});
}
}
混合架构设计
关系型数据库:PostgreSQL
文档数据库:MongoDB
搜索引擎:Elasticsearch
缓存数据库:Redis
CAP理论权衡:
PostgreSQL核心实体设计:
-- 用户表
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
user_type VARCHAR(20) NOT NULL CHECK (user_type IN ('candidate', 'hr', 'admin')),
status VARCHAR(20) DEFAULT 'active',
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
last_login_at TIMESTAMP WITH TIME ZONE
);
-- 企业表
CREATE TABLE companies (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
industry VARCHAR(100),
size_range VARCHAR(50),
website VARCHAR(255),
description TEXT,
logo_url VARCHAR(500),
status VARCHAR(20) DEFAULT 'active',
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- 职位表
CREATE TABLE jobs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
company_id UUID NOT NULL REFERENCES companies(id),
title VARCHAR(255) NOT NULL,
description TEXT NOT NULL,
requirements TEXT,
salary_min INTEGER,
salary_max INTEGER,
location VARCHAR(255),
employment_type VARCHAR(50),
experience_level VARCHAR(50),
skills JSONB,
status VARCHAR(20) DEFAULT 'active',
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- 简历表
CREATE TABLE resumes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id),
original_filename VARCHAR(255),
file_url VARCHAR(500),
file_size INTEGER,
mime_type VARCHAR(100),
parsing_status VARCHAR(20) DEFAULT 'pending',
parsed_data_id VARCHAR(100), -- MongoDB文档ID
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- 面试表
CREATE TABLE interviews (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
job_id UUID NOT NULL REFERENCES jobs(id),
candidate_id UUID NOT NULL REFERENCES users(id),
hr_id UUID NOT NULL REFERENCES users(id),
type VARCHAR(20) NOT NULL CHECK (type IN ('ai', 'human', 'hybrid')),
status VARCHAR(20) DEFAULT 'scheduled',
scheduled_at TIMESTAMP WITH TIME ZONE,
started_at TIMESTAMP WITH TIME ZONE,
completed_at TIMESTAMP WITH TIME ZONE,
duration_minutes INTEGER,
video_url VARCHAR(500),
analysis_result_id VARCHAR(100), -- MongoDB文档ID
score INTEGER CHECK (score >= 0 AND score <= 100),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- 面试问题表
CREATE TABLE interview_questions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
interview_id UUID NOT NULL REFERENCES interviews(id),
question_text TEXT NOT NULL,
question_type VARCHAR(50),
order_index INTEGER NOT NULL,
asked_at TIMESTAMP WITH TIME ZONE,
answer_text TEXT,
answer_duration_seconds INTEGER,
answer_score INTEGER CHECK (answer_score >= 0 AND answer_score <= 100),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
MongoDB文档结构:
// 简历解析结果集合
db.parsed_resumes.insertOne({
_id: ObjectId(),
resume_id: "uuid",
parsing_version: "1.0",
parsed_at: new Date(),
personal_info: {
name: "张三",
email: "zhangsan@example.com",
phone: "+86-138-0000-0000",
location: "北京市朝阳区",
birth_date: "1990-01-01",
gender: "male"
},
education: [{
institution: "清华大学",
degree: "本科",
major: "计算机科学与技术",
start_date: "2008-09",
end_date: "2012-06",
gpa: 3.8
}],
work_experience: [{
company: "阿里巴巴",
position: "高级软件工程师",
start_date: "2020-03",
end_date: "2023-08",
description: "负责电商平台后端开发",
achievements: ["优化系统性能提升30%", "主导微服务架构改造"]
}],
skills: {
programming_languages: ["Java", "Python", "JavaScript"],
frameworks: ["Spring Boot", "React", "Vue.js"],
databases: ["MySQL", "Redis", "MongoDB"],
tools: ["Git", "Docker", "Kubernetes"]
},
projects: [{
name: "电商推荐系统",
description: "基于机器学习的商品推荐系统",
technologies: ["Python", "TensorFlow", "Redis"],
start_date: "2022-01",
end_date: "2022-06"
}],
certifications: [{
name: "AWS认证解决方案架构师",
issuer: "Amazon",
issue_date: "2022-03",
expiry_date: "2025-03"
}],
languages: [{
language: "英语",
proficiency: "流利"
}],
extracted_keywords: ["Java", "微服务", "高并发", "分布式系统"],
confidence_score: 0.95
});
// 面试分析结果集合
db.interview_analysis.insertOne({
_id: ObjectId(),
interview_id: "uuid",
analysis_version: "2.0",
analyzed_at: new Date(),
overall_score: 85,
dimensions: {
technical_skills: {
score: 88,
details: {
programming_knowledge: 90,
system_design: 85,
problem_solving: 90
}
},
communication: {
score: 82,
details: {
clarity: 85,
fluency: 80,
confidence: 80
}
},
behavioral: {
score: 85,
details: {
teamwork: 88,
leadership: 82,
adaptability: 85
}
}
},
question_analysis: [{
question_id: "uuid",
question_text: "请介绍一下你的项目经验",
answer_analysis: {
content_relevance: 0.9,
technical_depth: 0.85,
communication_clarity: 0.8,
keywords_mentioned: ["微服务", "高并发", "Redis"],
sentiment_score: 0.7,
confidence_level: 0.8
},
transcription: "我在阿里巴巴主要负责...",
duration_seconds: 120
}],
video_analysis: {
facial_expressions: {
confidence_avg: 0.75,
engagement_avg: 0.8,
stress_indicators: 0.2
},
voice_analysis: {
pace_consistency: 0.8,
volume_stability: 0.85,
filler_words_count: 5
}
},
recommendations: [
"候选人技术能力强,建议进入下一轮",
"沟通表达可以更加简洁明了",
"对系统设计有深入理解"
],
red_flags: [],
processing_metadata: {
ai_model_version: "gpt-4",
processing_time_ms: 15000,
confidence_threshold: 0.7
}
});
关系型数据规范化(第三范式):
性能优化的反规范化:
-- 在jobs表中冗余company_name,避免频繁JOIN
ALTER TABLE jobs ADD COLUMN company_name VARCHAR(255);
-- 在interviews表中冗余candidate_name和job_title
ALTER TABLE interviews ADD COLUMN candidate_name VARCHAR(255);
ALTER TABLE interviews ADD COLUMN job_title VARCHAR(255);
-- 创建物化视图用于复杂查询
CREATE MATERIALIZED VIEW interview_summary AS
SELECT
i.id,
i.status,
i.score,
u.email as candidate_email,
j.title as job_title,
c.name as company_name,
i.completed_at
FROM interviews i
JOIN users u ON i.candidate_id = u.id
JOIN jobs j ON i.job_id = j.id
JOIN companies c ON j.company_id = c.id;
索引策略:
-- 复合索引用于常见查询
CREATE INDEX idx_interviews_status_date ON interviews(status, created_at);
CREATE INDEX idx_jobs_company_status ON jobs(company_id, status);
CREATE INDEX idx_resumes_user_status ON resumes(user_id, parsing_status);
-- 部分索引用于特定条件
CREATE INDEX idx_active_jobs ON jobs(created_at) WHERE status = 'active';
-- 表达式索引
CREATE INDEX idx_users_email_lower ON users(LOWER(email));
-- JSONB索引用于技能搜索
CREATE INDEX idx_jobs_skills_gin ON jobs USING GIN(skills);
多级缓存策略:
// L1缓存:应用内存缓存
@Injectable()
export class CacheService {
private readonly memoryCache = new Map<string, any>();
// L2缓存:Redis缓存
constructor(
@Inject('REDIS_CLIENT') private redis: Redis
) {}
async get<T>(key: string): Promise<T | null> {
// 1. 检查内存缓存
if (this.memoryCache.has(key)) {
return this.memoryCache.get(key);
}
// 2. 检查Redis缓存
const cached = await this.redis.get(key);
if (cached) {
const data = JSON.parse(cached);
this.memoryCache.set(key, data);
return data;
}
return null;
}
async set(key: string, value: any, ttl: number = 3600): Promise<void> {
// 同时设置内存和Redis缓存
this.memoryCache.set(key, value);
await this.redis.setex(key, ttl, JSON.stringify(value));
}
}
// 缓存装饰器
export function Cacheable(ttl: number = 3600) {
return function (target: any, propertyName: string, descriptor: PropertyDescriptor) {
const method = descriptor.value;
descriptor.value = async function (...args: any[]) {
const cacheKey = `${target.constructor.name}:${propertyName}:${JSON.stringify(args)}`;
const cacheService = this.cacheService;
let result = await cacheService.get(cacheKey);
if (!result) {
result = await method.apply(this, args);
await cacheService.set(cacheKey, result, ttl);
}
return result;
};
};
}
// 使用示例
@Injectable()
export class JobService {
@Cacheable(1800) // 30分钟缓存
async getActiveJobs(companyId: string): Promise<Job[]> {
return this.jobRepository.findActiveByCompany(companyId);
}
}
数据库分片策略:
// 用户数据按用户ID分片
class UserShardingStrategy {
getShardKey(userId: string): string {
const hash = crypto.createHash('md5').update(userId).digest('hex');
const shardIndex = parseInt(hash.substring(0, 2), 16) % 4;
return `user_shard_${shardIndex}`;
}
}
// 面试数据按时间分片
class InterviewShardingStrategy {
getShardKey(date: Date): string {
const year = date.getFullYear();
const month = date.getMonth() + 1;
return `interview_${year}_${month.toString().padStart(2, '0')}`;
}
}
// 分片路由器
@Injectable()
export class ShardingRouter {
private readonly shards = new Map<string, DataSource>();
constructor(
private userStrategy: UserShardingStrategy,
private interviewStrategy: InterviewShardingStrategy
) {
this.initializeShards();
}
getUserShard(userId: string): DataSource {
const shardKey = this.userStrategy.getShardKey(userId);
return this.shards.get(shardKey)!;
}
getInterviewShard(date: Date): DataSource {
const shardKey = this.interviewStrategy.getShardKey(date);
return this.shards.get(shardKey)!;
}
}
读写分离配置:
// 数据源配置
@Module({
imports: [
TypeOrmModule.forRoot({
name: 'master',
type: 'postgres',
host: 'master.db.example.com',
replication: {
master: {
host: 'master.db.example.com',
port: 5432,
username: 'master_user',
password: 'master_pass',
database: 'interview_platform'
},
slaves: [
{
host: 'slave1.db.example.com',
port: 5432,
username: 'slave_user',
password: 'slave_pass',
database: 'interview_platform'
},
{
host: 'slave2.db.example.com',
port: 5432,
username: 'slave_user',
password: 'slave_pass',
database: 'interview_platform'
}
]
}
})
]
})
export class DatabaseModule {}
备份策略:
#!/bin/bash
# 全量备份脚本
BACKUP_DIR="/backup/$(date +%Y%m%d)"
mkdir -p $BACKUP_DIR
# PostgreSQL备份
pg_dump -h $PG_HOST -U $PG_USER -d $PG_DATABASE | gzip > $BACKUP_DIR/postgres_full.sql.gz
# MongoDB备份
mongodump --host $MONGO_HOST --db $MONGO_DATABASE --out $BACKUP_DIR/mongodb/
# Redis备份
redis-cli --rdb $BACKUP_DIR/redis_dump.rdb
# 上传到云存储
aws s3 sync $BACKUP_DIR s3://backup-bucket/$(date +%Y%m%d)/
# 清理本地备份(保留7天)
find /backup -type d -mtime +7 -exec rm -rf {} +
增量备份:
#!/bin/bash
# PostgreSQL WAL归档
archive_command = 'aws s3 cp %p s3://wal-archive-bucket/%f'
# MongoDB Oplog备份
mongodump --host $MONGO_HOST --db local --collection oplog.rs --query '{"ts": {"$gte": Timestamp('$(date -d "1 hour ago" +%s)', 0)}}' --out $BACKUP_DIR/oplog/
恢复策略:
#!/bin/bash
# 点时间恢复(PITR)
function restore_to_point_in_time() {
local target_time=$1
# 1. 恢复基础备份
pg_restore -h $PG_HOST -U $PG_USER -d $PG_DATABASE $BACKUP_DIR/postgres_full.sql.gz
# 2. 应用WAL日志到指定时间点
pg_ctl start -D $PGDATA
psql -c "SELECT pg_wal_replay_resume();"
# 3. 验证数据一致性
psql -c "SELECT count(*) FROM users;"
}
推荐:阿里云 + Kubernetes
选择理由:
云服务选择:
# 核心服务
Compute:
- ECS (弹性计算服务)
- ACK (容器服务Kubernetes版)
- Serverless App Engine
Storage:
- OSS (对象存储)
- NAS (文件存储)
- CPFS (并行文件系统)
Database:
- RDS PostgreSQL
- MongoDB Atlas
- Redis Enterprise
- AnalyticDB (数据仓库)
Networking:
- VPC (专有网络)
- SLB (负载均衡)
- CDN (内容分发)
- API Gateway
Security:
- WAF (Web应用防火墙)
- Anti-DDoS
- SSL证书服务
- RAM (访问控制)
Monitoring:
- CloudMonitor
- Log Service
- Application Real-Time Monitoring Service (ARMS)
Docker容器化:
# Node.js应用Dockerfile
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
FROM node:18-alpine AS runtime
WORKDIR /app
# 创建非root用户
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001
COPY --from=builder /app/node_modules ./node_modules
COPY --chown=nextjs:nodejs . .
USER nextjs
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1
CMD ["npm", "start"]
Kubernetes部署配置:
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: interview-api
namespace: production
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
app: interview-api
template:
metadata:
labels:
app: interview-api
spec:
containers:
- name: api
image: registry.cn-hangzhou.aliyuncs.com/interview/api:v1.0.0
ports:
- containerPort: 3000
env:
- name: NODE_ENV
value: "production"
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: db-secret
key: url
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 3000
initialDelaySeconds: 5
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: interview-api-service
spec:
selector:
app: interview-api
ports:
- protocol: TCP
port: 80
targetPort: 3000
type: ClusterIP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: interview-api-ingress
annotations:
kubernetes.io/ingress.class: nginx
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/rate-limit: "100"
spec:
tls:
- hosts:
- api.interview-platform.com
secretName: api-tls
rules:
- host: api.interview-platform.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: interview-api-service
port:
number: 80
Helm Chart配置:
# values.yaml
replicaCount: 3
image:
repository: registry.cn-hangzhou.aliyuncs.com/interview/api
tag: "v1.0.0"
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 80
ingress:
enabled: true
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
hosts:
- host: api.interview-platform.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: api-tls
hosts:
- api.interview-platform.com
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 250m
memory: 256Mi
autoscaling:
enabled: true
minReplicas: 3
maxReplicas: 10
targetCPUUtilizationPercentage: 80
targetMemoryUtilizationPercentage: 80
env:
NODE_ENV: production
LOG_LEVEL: info
secrets:
database:
url: "postgresql://user:pass@host:5432/db"
redis:
url: "redis://redis-cluster:6379"
jwt:
secret: "your-jwt-secret"
GitLab CI/CD Pipeline:
# .gitlab-ci.yml
stages:
- test
- build
- security
- deploy-staging
- deploy-production
variables:
DOCKER_REGISTRY: registry.cn-hangzhou.aliyuncs.com
IMAGE_NAME: interview/api
KUBECONFIG_FILE: $KUBECONFIG_STAGING
# 测试阶段
test:unit:
stage: test
image: node:18-alpine
script:
- npm ci
- npm run test:unit
- npm run test:coverage
coverage: '/Lines\s*:\s*(\d+\.?\d*)%/'
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
test:integration:
stage: test
image: node:18-alpine
services:
- postgres:13
- redis:7-alpine
variables:
POSTGRES_DB: test_db
POSTGRES_USER: test_user
POSTGRES_PASSWORD: test_pass
script:
- npm ci
- npm run test:integration
# 构建阶段
build:docker:
stage: build
image: docker:20.10.16
services:
- docker:20.10.16-dind
before_script:
- echo $DOCKER_PASSWORD | docker login -u $DOCKER_USERNAME --password-stdin $DOCKER_REGISTRY
script:
- docker build -t $DOCKER_REGISTRY/$IMAGE_NAME:$CI_COMMIT_SHA .
- docker build -t $DOCKER_REGISTRY/$IMAGE_NAME:latest .
- docker push $DOCKER_REGISTRY/$IMAGE_NAME:$CI_COMMIT_SHA
- docker push $DOCKER_REGISTRY/$IMAGE_NAME:latest
only:
- main
- develop
# 安全扫描
security:container-scan:
stage: security
image: aquasec/trivy:latest
script:
- trivy image --exit-code 0 --severity HIGH,CRITICAL $DOCKER_REGISTRY/$IMAGE_NAME:$CI_COMMIT_SHA
allow_failure: true
security:sast:
stage: security
image: node:18-alpine
script:
- npm ci
- npm audit --audit-level high
- npx eslint . --ext .ts,.js --format gitlab
artifacts:
reports:
sast: gl-sast-report.json
# 部署到测试环境
deploy:staging:
stage: deploy-staging
image: bitnami/kubectl:latest
script:
- echo $KUBECONFIG_STAGING | base64 -d > kubeconfig
- export KUBECONFIG=kubeconfig
- helm upgrade --install interview-api-staging ./helm-chart \
--namespace staging \
--set image.tag=$CI_COMMIT_SHA \
--set ingress.hosts[0].host=staging-api.interview-platform.com \
--values ./helm-chart/values-staging.yaml
environment:
name: staging
url: https://staging-api.interview-platform.com
only:
- develop
# 部署到生产环境
deploy:production:
stage: deploy-production
image: bitnami/kubectl:latest
script:
- echo $KUBECONFIG_PRODUCTION | base64 -d > kubeconfig
- export KUBECONFIG=kubeconfig
- helm upgrade --install interview-api ./helm-chart \
--namespace production \
--set image.tag=$CI_COMMIT_SHA \
--values ./helm-chart/values-production.yaml
environment:
name: production
url: https://api.interview-platform.com
when: manual
only:
- main
Prometheus + Grafana监控栈:
# prometheus-config.yaml
global:
scrape_interval: 15s
evaluation_interval: 15s
rule_files:
- "alert_rules.yml"
alerting:
alertmanagers:
- static_configs:
- targets:
- alertmanager:9093
scrape_configs:
- job_name: 'kubernetes-apiservers'
kubernetes_sd_configs:
- role: endpoints
scheme: https
tls_config:
ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
relabel_configs:
- source_labels: [__meta_kubernetes_namespace, __meta_kubernetes_service_name, __meta_kubernetes_endpoint_port_name]
action: keep
regex: default;kubernetes;https
- job_name: 'interview-api'
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_pod_label_app]
action: keep
regex: interview-api
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
action: keep
regex: true
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
action: replace
target_label: __metrics_path__
regex: (.+)
告警规则:
# alert_rules.yml
groups:
- name: interview-platform
rules:
- alert: HighErrorRate
expr: rate(http_requests_total{status=~"5.."}[5m]) > 0.1
for: 5m
labels:
severity: critical
annotations:
summary: "High error rate detected"
description: "Error rate is {{ $value }} errors per second"
- alert: HighResponseTime
expr: histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m])) > 1
for: 5m
labels:
severity: warning
annotations:
summary: "High response time detected"
description: "95th percentile response time is {{ $value }} seconds"
- alert: DatabaseConnectionHigh
expr: pg_stat_activity_count > 80
for: 2m
labels:
severity: warning
annotations:
summary: "High database connection count"
description: "Database has {{ $value }} active connections"
- alert: PodCrashLooping
expr: rate(kube_pod_container_status_restarts_total[15m]) > 0
for: 5m
labels:
severity: critical
annotations:
summary: "Pod is crash looping"
description: "Pod {{ $labels.pod }} in namespace {{ $labels.namespace }} is crash looping"
Grafana仪表板配置:
{
"dashboard": {
"title": "Interview Platform Overview",
"panels": [
{
"title": "Request Rate",
"type": "graph",
"targets": [
{
"expr": "rate(http_requests_total[5m])",
"legendFormat": "{{method}} {{status}}"
}
]
},
{
"title": "Response Time",
"type": "graph",
"targets": [
{
"expr": "histogram_quantile(0.50, rate(http_request_duration_seconds_bucket[5m]))",
"legendFormat": "50th percentile"
},
{
"expr": "histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))",
"legendFormat": "95th percentile"
}
]
},
{
"title": "Database Performance",
"type": "graph",
"targets": [
{
"expr": "pg_stat_database_tup_fetched",
"legendFormat": "Rows fetched"
},
{
"expr": "pg_stat_database_tup_inserted",
"legendFormat": "Rows inserted"
}
]
}
]
}
}
传输加密(TLS):
# Nginx TLS配置
server {
listen 443 ssl http2;
server_name api.interview-platform.com;
# TLS证书配置
ssl_certificate /etc/ssl/certs/api.interview-platform.com.crt;
ssl_certificate_key /etc/ssl/private/api.interview-platform.com.key;
# TLS安全配置
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# HSTS
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# 其他安全头
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header Referrer-Policy "strict-origin-when-cross-origin";
location / {
proxy_pass http://interview-api-service;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
存储加密:
// 数据库字段加密
@Entity()
export class User {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
email: string;
@Column({ transformer: new EncryptionTransformer() })
phone: string; // 加密存储手机号
@Column({ transformer: new EncryptionTransformer() })
idCard: string; // 加密存储身份证号
}
// 加密转换器
export class EncryptionTransformer implements ValueTransformer {
private readonly algorithm = 'aes-256-gcm';
private readonly key = Buffer.from(process.env.ENCRYPTION_KEY!, 'hex');
to(value: string): string {
if (!value) return value;
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipher(this.algorithm, this.key);
cipher.setAAD(Buffer.from('interview-platform'));
let encrypted = cipher.update(value, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
return iv.toString('hex') + ':' + authTag.toString('hex') + ':' + encrypted;
}
from(value: string): string {
if (!value) return value;
const [ivHex, authTagHex, encrypted] = value.split(':');
const iv = Buffer.from(ivHex, 'hex');
const authTag = Buffer.from(authTagHex, 'hex');
const decipher = crypto.createDecipher(this.algorithm, this.key);
decipher.setAAD(Buffer.from('interview-platform'));
decipher.setAuthTag(authTag);
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
}
API安全中间件:
// 速率限制
@Injectable()
export class RateLimitMiddleware implements NestMiddleware {
private readonly limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15分钟
max: 100, // 最多100个请求
message: 'Too many requests from this IP',
standardHeaders: true,
legacyHeaders: false,
});
use(req: Request, res: Response, next: NextFunction) {
this.limiter(req, res, next);
}
}
// SQL注入防护
@Injectable()
export class SqlInjectionGuard implements CanActivate {
private readonly sqlInjectionPatterns = [
/('|(\-\-)|(;)|(\||\|)|(\*|\*))/i,
/(exec(\s|\+)+(s|x)p\w+)/i,
/union.*select/i,
/insert.*into/i,
/delete.*from/i,
/update.*set/i,
/drop.*table/i
];
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
const { query, body, params } = request;
const allInputs = { ...query, ...body, ...params };
for (const [key, value] of Object.entries(allInputs)) {
if (typeof value === 'string' && this.containsSqlInjection(value)) {
throw new BadRequestException(`Potential SQL injection detected in ${key}`);
}
}
return true;
}
private containsSqlInjection(input: string): boolean {
return this.sqlInjectionPatterns.some(pattern => pattern.test(input));
}
}
// XSS防护
@Injectable()
export class XssProtectionInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
// 清理请求数据
this.sanitizeObject(request.body);
this.sanitizeObject(request.query);
return next.handle().pipe(
map(data => {
// 清理响应数据
return this.sanitizeObject(data);
})
);
}
private sanitizeObject(obj: any): any {
if (typeof obj === 'string') {
return DOMPurify.sanitize(obj);
}
if (Array.isArray(obj)) {
return obj.map(item => this.sanitizeObject(item));
}
if (obj && typeof obj === 'object') {
const sanitized = {};
for (const [key, value] of Object.entries(obj)) {
sanitized[key] = this.sanitizeObject(value);
}
return sanitized;
}
return obj;
}
}
// CSRF防护
@Injectable()
export class CsrfProtectionMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
// 验证CSRF Token
const token = req.headers['x-csrf-token'] || req.body._token;
const sessionToken = req.session?.csrfToken;
if (req.method !== 'GET' && token !== sessionToken) {
throw new ForbiddenException('Invalid CSRF token');
}
next();
}
}
API访问控制:
// API密钥验证
@Injectable()
export class ApiKeyGuard implements CanActivate {
constructor(private readonly apiKeyService: ApiKeyService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const apiKey = request.headers['x-api-key'];
if (!apiKey) {
throw new UnauthorizedException('API key required');
}
const isValid = await this.apiKeyService.validateKey(apiKey);
if (!isValid) {
throw new UnauthorizedException('Invalid API key');
}
// 记录API使用情况
await this.apiKeyService.recordUsage(apiKey, request.path);
return true;
}
}
// IP白名单
@Injectable()
export class IpWhitelistGuard implements CanActivate {
private readonly allowedIps = new Set([
'192.168.1.0/24',
'10.0.0.0/8',
'172.16.0.0/12'
]);
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
const clientIp = request.ip || request.connection.remoteAddress;
if (!this.isIpAllowed(clientIp)) {
throw new ForbiddenException('IP address not allowed');
}
return true;
}
private isIpAllowed(ip: string): boolean {
return Array.from(this.allowedIps).some(range => {
return this.ipInRange(ip, range);
});
}
}
数据隐私保护:
// 个人信息保护法合规
@Injectable()
export class PrivacyComplianceService {
// 数据最小化原则
async collectUserData(userData: Partial<User>): Promise<User> {
// 只收集必要的数据字段
const allowedFields = ['email', 'name', 'phone'];
const filteredData = Object.keys(userData)
.filter(key => allowedFields.includes(key))
.reduce((obj, key) => {
obj[key] = userData[key];
return obj;
}, {});
return this.userService.create(filteredData);
}
// 用户同意管理
async recordConsent(userId: string, consentType: string): Promise<void> {
await this.consentService.record({
userId,
consentType,
timestamp: new Date(),
ipAddress: this.request.ip,
userAgent: this.request.headers['user-agent']
});
}
// 数据删除权(被遗忘权)
async deleteUserData(userId: string): Promise<void> {
// 1. 删除个人身份信息
await this.userService.anonymize(userId);
// 2. 删除关联数据
await this.resumeService.deleteByUser(userId);
await this.interviewService.anonymizeByUser(userId);
// 3. 记录删除操作
await this.auditService.logDataDeletion(userId);
}
// 数据导出权
async exportUserData(userId: string): Promise<any> {
const userData = await this.userService.findById(userId);
const resumes = await this.resumeService.findByUser(userId);
const interviews = await this.interviewService.findByUser(userId);
return {
personal_info: userData,
resumes: resumes,
interviews: interviews,
exported_at: new Date().toISOString()
};
}
}
审计日志:
@Injectable()
export class AuditService {
async logUserAction(action: AuditAction): Promise<void> {
await this.auditRepository.save({
userId: action.userId,
action: action.type,
resource: action.resource,
details: action.details,
ipAddress: action.ipAddress,
userAgent: action.userAgent,
timestamp: new Date()
});
}
// 敏感操作审计
@AuditLog('DATA_ACCESS')
async accessSensitiveData(userId: string, dataType: string): Promise<any> {
// 记录敏感数据访问
return this.dataService.getSensitiveData(userId, dataType);
}
}
// 审计装饰器
export function AuditLog(actionType: string) {
return function (target: any, propertyName: string, descriptor: PropertyDescriptor) {
const method = descriptor.value;
descriptor.value = async function (...args: any[]) {
const result = await method.apply(this, args);
// 记录审计日志
await this.auditService.logUserAction({
type: actionType,
resource: `${target.constructor.name}.${propertyName}`,
details: { args: args.slice(0, 2) }, // 只记录前两个参数
userId: this.getCurrentUserId(),
ipAddress: this.getClientIp(),
userAgent: this.getUserAgent()
});
return result;
};
};
}
层级 | 技术选择 | 版本 | 用途 | 备选方案 |
---|---|---|---|---|
前端框架 | Vue.js | 3.3+ | 用户界面开发 | React 18, Angular 16 |
前端语言 | TypeScript | 5.0+ | 类型安全开发 | JavaScript ES2022 |
前端构建 | Vite | 4.0+ | 快速构建打包 | Webpack 5, Rollup |
状态管理 | Pinia | 2.1+ | 应用状态管理 | Vuex 4, Zustand |
UI组件库 | Element Plus | 2.3+ | 企业级组件 | Ant Design Vue, Vuetify |
CSS框架 | Tailwind CSS | 3.3+ | 原子化样式 | Bootstrap 5, Bulma |
后端框架 | NestJS | 10+ | 企业级Node.js框架 | Express.js, Fastify |
后端语言 | Node.js | 18+ | 服务端JavaScript | Java 17, Python 3.11, Go 1.20 |
ORM框架 | TypeORM | 0.3+ | 数据库对象映射 | Prisma, Sequelize |
主数据库 | PostgreSQL | 15+ | 关系型数据存储 | MySQL 8.0, MariaDB 10.6 |
文档数据库 | MongoDB | 6.0+ | 非结构化数据 | CouchDB, Amazon DocumentDB |
搜索引擎 | Elasticsearch | 8.0+ | 全文搜索分析 | Apache Solr, Algolia |
缓存数据库 | Redis | 7.0+ | 高性能缓存 | Memcached, Hazelcast |
消息队列 | Apache Kafka | 3.4+ | 分布式消息流 | RabbitMQ, Apache Pulsar |
任务队列 | Bull | 4.10+ | 异步任务处理 | Agenda, Kue |
容器化 | Docker | 20.10+ | 应用容器化 | Podman, containerd |
容器编排 | Kubernetes | 1.27+ | 容器集群管理 | Docker Swarm, Nomad |
服务网格 | Istio | 1.18+ | 微服务通信 | Linkerd, Consul Connect |
API网关 | Kong | 3.3+ | API统一入口 | Nginx Gateway, Zuul |
监控系统 | Prometheus | 2.45+ | 指标监控收集 | InfluxDB, Datadog |
可视化 | Grafana | 10.0+ | 监控数据可视化 | Kibana, New Relic |
日志管理 | ELK Stack | 8.8+ | 日志收集分析 | Fluentd + InfluxDB |
CI/CD | GitLab CI | 16.0+ | 持续集成部署 | Jenkins, GitHub Actions |
云服务商 | 阿里云 | - | 云基础设施 | AWS, Azure, 腾讯云 |
CDN | 阿里云CDN | - | 内容分发网络 | CloudFlare, AWS CloudFront |
对象存储 | 阿里云OSS | - | 文件对象存储 | AWS S3, 腾讯云COS |
核心技术选择分析:
Vue.js (推荐)
React (备选)
Angular (备选)
Node.js (推荐)
Java Spring Boot (备选)
Python FastAPI (备选)
Go (备选)
PostgreSQL (推荐)
MySQL (备选)
MongoDB (混合使用)
最终推荐架构组合:
前端:Vue.js 3 + TypeScript + Vite + Pinia + Element Plus
后端:Node.js + NestJS + TypeScript + TypeORM
数据:PostgreSQL + MongoDB + Redis + Elasticsearch
基础设施:Docker + Kubernetes + 阿里云
监控:Prometheus + Grafana + ELK
适用场景:
不适用场景:
本架构设计为AI智能面试平台提供了一套完整、可扩展、安全的技术解决方案。通过微服务架构、云原生部署、多数据库混合使用等现代化技术栈,能够支撑百万级用户的业务需求,同时保证系统的高可用性、高性能和高安全性。
关键优势:
实施建议:
该架构设计为AI智能面试平台的成功实施提供了坚实的技术基础。