feat(p4): content analysis service with Neo4j knowledge graph and ClickHouse analytics

P4 阶段交付物:
- services/content: 内容资源服务(NestJS)
  - textbooks: 教材 CRUD + 知识图谱绑定
  - config/neo4j.ts: Neo4j driver 单例
  - textbooks.service.ts: MySQL CRUD + Neo4j 知识图谱(createKnowledgeGraph/getPrerequisites)
  - package.json: 补充 @opentelemetry/sdk-node + exporter-trace-otlp-http
- services/data-ana: 数据分析服务(Python FastAPI)
  - main.py: FastAPI + /healthz + class_performance + student_weakness 骨架
  - clickhouse_client.py: ClickHouse 客户端封装
  - config.py: 环境变量配置
- packages/shared-proto/proto/content.proto: TextbookService + KnowledgeGraphService 契约
- packages/shared-proto/proto/analytics.proto: AnalyticsService 契约(class_performance/student_weakness)
This commit is contained in:
SpecialX
2026-07-08 01:38:35 +08:00
parent 23246ade6d
commit 9850bfcfd1
28 changed files with 936 additions and 0 deletions

View File

@@ -0,0 +1,96 @@
export type ErrorType =
| 'validation'
| 'not_found'
| 'permission_denied'
| 'conflict'
| 'business'
| 'database'
| 'internal';
export interface ErrorDetails {
[key: string]: unknown;
}
export abstract class ApplicationError extends Error {
abstract readonly type: ErrorType;
abstract readonly statusCode: number;
readonly code: string;
readonly details?: ErrorDetails;
// FIX #1: traceId 改为可写,以便 GlobalErrorFilter 注入请求级 traceId
traceId?: string;
constructor(message: string, code: string, details?: ErrorDetails) {
super(message);
this.name = this.constructor.name;
this.code = code;
this.details = details;
}
toJSON(): Record<string, unknown> {
return {
success: false,
error: {
code: this.code,
message: this.message,
details: this.details,
traceId: this.traceId,
},
};
}
}
export class ValidationError extends ApplicationError {
readonly type = 'validation' as const;
readonly statusCode = 400;
constructor(message: string, details?: ErrorDetails) {
super(message, 'CONTENT_VALIDATION_ERROR', details);
}
}
export class NotFoundError extends ApplicationError {
readonly type = 'not_found' as const;
readonly statusCode = 404;
constructor(resource: string, id: string) {
super(`${resource} not found: ${id}`, 'CONTENT_NOT_FOUND', { resource, id });
}
}
export class PermissionDeniedError extends ApplicationError {
readonly type = 'permission_denied' as const;
readonly statusCode = 403;
constructor(permission: string) {
super(`Permission denied: ${permission}`, 'CONTENT_PERMISSION_DENIED', { permission });
}
}
export class ConflictError extends ApplicationError {
readonly type = 'conflict' as const;
readonly statusCode = 409;
constructor(message: string, details?: ErrorDetails) {
super(message, 'CONTENT_CONFLICT', details);
}
}
export class BusinessError extends ApplicationError {
readonly type = 'business' as const;
readonly statusCode = 422;
constructor(message: string, details?: ErrorDetails) {
super(message, 'CONTENT_BUSINESS_ERROR', details);
}
}
export class DatabaseError extends ApplicationError {
readonly type = 'database' as const;
readonly statusCode = 500;
constructor(message: string, details?: ErrorDetails) {
super(message, 'CONTENT_DATABASE_ERROR', details);
}
}
export class InternalError extends ApplicationError {
readonly type = 'internal' as const;
readonly statusCode = 500;
constructor(message: string, details?: ErrorDetails) {
super(message, 'CONTENT_INTERNAL_ERROR', details);
}
}