commit a7add8ff9093480434246d16ee75f340651d4fd6 Author: hailin Date: Fri Jan 9 00:01:12 2026 -0800 Initial commit: iConsulting 香港移民咨询智能客服系统 项目架构: - Monorepo (pnpm + Turborepo) - 后端: NestJS 微服务 + Claude Agent SDK - 前端: React + Vite + Ant Design 包含服务: - conversation-service: 对话服务 (Claude AI) - user-service: 用户认证服务 - payment-service: 支付服务 (支付宝/微信/Stripe) - knowledge-service: 知识库服务 (RAG + Neo4j) - evolution-service: 自我进化服务 - web-client: 用户前端 - admin-client: 管理后台 基础设施: - PostgreSQL + Redis + Neo4j - Kong API Gateway - Nginx 反向代理 - Docker Compose 部署配置 Co-Authored-By: Claude Opus 4.5 diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..13d5074 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,13 @@ +{ + "permissions": { + "allow": [ + "Bash(pnpm install:*)", + "Bash(npm install:*)", + "Bash(npx tsc:*)", + "Bash(pnpm add:*)", + "Bash(git init:*)", + "Bash(git checkout:*)", + "Bash(git add:*)" + ] + } +} diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..6ef0c67 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,72 @@ +# =========================================== +# Docker 忽略文件 +# =========================================== + +# 依赖 +node_modules +**/node_modules + +# 构建产物 +dist +**/dist +.next +.nuxt + +# 日志 +logs +*.log +npm-debug.log* +pnpm-debug.log* + +# 环境配置 +.env +.env.* +!.env.example + +# IDE +.idea +.vscode +*.swp +*.swo + +# 系统文件 +.DS_Store +Thumbs.db + +# Git +.git +.gitignore + +# 测试 +coverage +.nyc_output +test +tests +**/*.test.ts +**/*.spec.ts + +# 文档 +docs +*.md +!README.md + +# 临时文件 +tmp +temp +.tmp +.temp + +# 备份 +backups +*.bak + +# 证书 (敏感) +*.pem +*.key +*.crt +nginx/ssl/* + +# 其他 +.turbo +.cache +.parcel-cache diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..006850c --- /dev/null +++ b/.env.example @@ -0,0 +1,151 @@ +# =========================================== +# iConsulting - Environment Configuration +# =========================================== +# 使用方法: 复制此文件为 .env 并填入实际值 +# cp .env.example .env +# =========================================== + +# Application +NODE_ENV=production +APP_NAME=iConsulting + +# =========================================== +# 服务器网络配置 +# =========================================== +# 对外服务 IP (用户访问) +SERVER_PUBLIC_IP=14.215.128.96 +# Claude API 出口 IP +CLAUDE_API_OUTBOUND_IP=154.84.135.121 +# Claude API 服务器 +CLAUDE_API_SERVER=67.223.119.33 + +# =========================================== +# Anthropic Claude API +# =========================================== +ANTHROPIC_API_KEY=sk-ant-api03-xxx +ANTHROPIC_BASE_URL=https://api.anthropic.com + +# =========================================== +# OpenAI API (用于 Embedding) +# =========================================== +OPENAI_API_KEY=sk-xxx + +# =========================================== +# PostgreSQL Database +# =========================================== +POSTGRES_HOST=localhost +POSTGRES_PORT=5432 +POSTGRES_USER=postgres +POSTGRES_PASSWORD=your_secure_password +POSTGRES_DB=iconsulting +DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB} + +# =========================================== +# Neo4j Graph Database +# =========================================== +NEO4J_URI=bolt://localhost:7687 +NEO4J_USER=neo4j +NEO4J_PASSWORD=your_secure_password + +# =========================================== +# Redis Cache +# =========================================== +REDIS_HOST=localhost +REDIS_PORT=6379 +REDIS_PASSWORD=your_redis_password +REDIS_URL=redis://:${REDIS_PASSWORD}@${REDIS_HOST}:${REDIS_PORT} + +# =========================================== +# Payment - Alipay +# =========================================== +ALIPAY_APP_ID=your_app_id +ALIPAY_PRIVATE_KEY=your_private_key +ALIPAY_PUBLIC_KEY=alipay_public_key +ALIPAY_GATEWAY=https://openapi.alipay.com/gateway.do +ALIPAY_NOTIFY_URL=https://your-domain.com/api/v1/payments/alipay/notify + +# =========================================== +# Payment - WeChat Pay +# =========================================== +WECHAT_APP_ID=your_app_id +WECHAT_MCH_ID=your_merchant_id +WECHAT_API_KEY=your_api_key +WECHAT_CERT_PATH=/path/to/wechat/cert +WECHAT_NOTIFY_URL=https://your-domain.com/api/v1/payments/wechat/notify + +# =========================================== +# Payment - Stripe (Credit Card) +# =========================================== +STRIPE_SECRET_KEY=sk_test_xxx +STRIPE_PUBLISHABLE_KEY=pk_test_xxx +STRIPE_WEBHOOK_SECRET=whsec_xxx + +# =========================================== +# Payment Callback URLs +# =========================================== +PAYMENT_CALLBACK_BASE_URL=https://your-domain.com +PAYMENT_SUCCESS_REDIRECT_URL=https://your-domain.com/payment/success +PAYMENT_CANCEL_REDIRECT_URL=https://your-domain.com/payment/cancel + +# =========================================== +# JWT Authentication +# =========================================== +JWT_SECRET=your_super_secret_jwt_key_change_in_production +JWT_EXPIRES_IN=7d +JWT_REFRESH_EXPIRES_IN=30d + +# =========================================== +# SMS Service (for phone verification) +# =========================================== +SMS_PROVIDER=aliyun +ALIYUN_SMS_ACCESS_KEY_ID=your_access_key_id +ALIYUN_SMS_ACCESS_KEY_SECRET=your_access_key_secret +ALIYUN_SMS_SIGN_NAME=iConsulting +ALIYUN_SMS_TEMPLATE_CODE=SMS_xxx + +# =========================================== +# Service Ports +# =========================================== +USER_SERVICE_PORT=3001 +PAYMENT_SERVICE_PORT=3002 +KNOWLEDGE_SERVICE_PORT=3003 +CONVERSATION_SERVICE_PORT=3004 +EVOLUTION_SERVICE_PORT=3005 + +# =========================================== +# 服务间通信 URL +# =========================================== +USER_SERVICE_URL=http://localhost:3001 +PAYMENT_SERVICE_URL=http://localhost:3002 +KNOWLEDGE_SERVICE_URL=http://localhost:3003 +CONVERSATION_SERVICE_URL=http://localhost:3004 +EVOLUTION_SERVICE_URL=http://localhost:3005 + +# =========================================== +# Kong API Gateway +# =========================================== +KONG_PROXY_PORT=8000 +KONG_ADMIN_PORT=8001 + +# =========================================== +# Frontend URLs +# =========================================== +WEB_CLIENT_URL=http://localhost +ADMIN_CLIENT_URL=http://localhost/admin + +# =========================================== +# CORS +# =========================================== +CORS_ORIGINS=http://localhost,http://14.215.128.96 + +# =========================================== +# Rate Limiting +# =========================================== +RATE_LIMIT_TTL=60 +RATE_LIMIT_MAX=100 + +# =========================================== +# Logging +# =========================================== +LOG_LEVEL=info +LOG_FORMAT=json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..99c4da2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,97 @@ +# =========================================== +# iConsulting Git 忽略配置 +# =========================================== + +# 依赖目录 +node_modules/ +**/node_modules/ +.pnpm-store/ + +# 构建产物 +dist/ +**/dist/ +build/ +.next/ +.nuxt/ +.output/ +out/ + +# 环境变量 (包含敏感信息) +.env +.env.local +.env.*.local +!.env.example + +# 日志文件 +logs/ +*.log +npm-debug.log* +pnpm-debug.log* +yarn-debug.log* +yarn-error.log* + +# 运行时数据 +pids/ +*.pid +*.seed +*.pid.lock + +# IDE 和编辑器 +.idea/ +.vscode/ +*.swp +*.swo +*.sublime-workspace +*.sublime-project + +# 系统文件 +.DS_Store +Thumbs.db +desktop.ini + +# 测试覆盖率 +coverage/ +.nyc_output/ +*.lcov + +# 缓存 +.cache/ +.parcel-cache/ +.turbo/ +.eslintcache +.stylelintcache +*.tsbuildinfo + +# 临时文件 +tmp/ +temp/ +.tmp/ +.temp/ +*.tmp + +# 备份文件 +backups/ +*.bak +*~ + +# SSL 证书 (敏感) +*.pem +*.key +*.crt +*.p12 +nginx/ssl/* +!nginx/ssl/.gitkeep + +# 数据库文件 +*.sqlite +*.db + +# Docker +docker-compose.override.yml + +# PM2 +.pm2/ + +# 打包文件 +*.tar.gz +*.zip diff --git a/DEVELOPMENT_GUIDE.md b/DEVELOPMENT_GUIDE.md new file mode 100644 index 0000000..e060070 --- /dev/null +++ b/DEVELOPMENT_GUIDE.md @@ -0,0 +1,1322 @@ +# iConsulting - 香港移民在线咨询系统开发指导 + +## 项目概述 + +iConsulting 是一个基于 Claude Agent SDK 的智能在线客服系统,专注于提供香港移民咨询服务。系统具备自我进化能力,能够从用户对话中学习经验,并根据管理员意图持续优化服务。 + +### 核心特性 + +- **智能咨询**: 基于 Claude Agent SDK 的自然语言对话 +- **付费评估**: 移民资格评估服务,支持多种支付方式 +- **知识增强**: RAG + 知识图谱,提供准确专业的回答 +- **自我进化**: 从对话中学习,根据管理员指令调整 +- **长期记忆**: 基于时间线的知识图谱记录 + +--- + +## 技术架构 + +### 整体架构图 + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ 客户端层 (Client Layer) │ +├─────────────────────────────────────────────────────────────────────┤ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────────┐ │ +│ │ PC Web │ │ H5 Mobile │ │ Admin Dashboard │ │ +│ │ (React+TS) │ │ (React+TS) │ │ (React+TS) │ │ +│ └──────────────┘ └──────────────┘ └──────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ API 网关层 (API Gateway) │ +├─────────────────────────────────────────────────────────────────────┤ +│ - 路由分发 - 认证授权 - 限流熔断 - 日志追踪 │ +│ - WebSocket 代理 - 请求聚合 │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ 微服务层 (Microservices) │ +├─────────────────────────────────────────────────────────────────────┤ +│ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ +│ │ User │ │ Conversation│ │ Knowledge │ │ Payment │ │ +│ │ Service │ │ Service │ │ Service │ │ Service │ │ +│ │ │ │ │ │ │ │ │ │ +│ │ - 匿名用户 │ │ - Claude │ │ - RAG引擎 │ │ - 支付宝 │ │ +│ │ - 手机验证 │ │ Agent │ │ - 向量检索 │ │ - 微信支付 │ │ +│ │ - 会话管理 │ │ - 对话管理 │ │ - 知识图谱 │ │ - 信用卡 │ │ +│ │ - 用户画像 │ │ - 流式输出 │ │ - 长期记忆 │ │ - 订单管理 │ │ +│ └────────────┘ └────────────┘ └────────────┘ └────────────┘ │ +│ │ +│ ┌────────────┐ ┌────────────┐ │ +│ │ Admin │ │ Evolution │ │ +│ │ Service │ │ Service │ │ +│ │ │ │ │ │ +│ │ - 后台管理 │ │ - 经验提取 │ │ +│ │ - 角色权限 │ │ - 模式识别 │ │ +│ │ - 系统配置 │ │ - 自我调整 │ │ +│ │ - 数据统计 │ │ - 管理员交互│ │ +│ └────────────┘ └────────────┘ │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ 基础设施层 (Infrastructure) │ +├─────────────────────────────────────────────────────────────────────┤ +│ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ +│ │ PostgreSQL │ │ Neo4j │ │ Redis │ │ Kafka │ │ +│ │ │ │ │ │ │ │ │ │ +│ │ - 用户数据 │ │ - 知识图谱 │ │ - 会话缓存 │ │ - 事件驱动 │ │ +│ │ - 订单数据 │ │ - 长期记忆 │ │ - 分布式锁 │ │ - Outbox │ │ +│ │ - 配置数据 │ │ - 关系网络 │ │ - 限流计数 │ │ - 消息队列 │ │ +│ └────────────┘ └────────────┘ └────────────┘ └────────────┘ │ +│ │ +│ ┌────────────┐ ┌────────────┐ │ +│ │ Anthropic │ │ 向量数据库 │ │ +│ │ Claude API │ │ (pgvector) │ │ +│ └────────────┘ └────────────┘ │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +### 技术栈详情 + +| 层次 | 技术选型 | 说明 | +|------|---------|------| +| 前端 | React 18 + TypeScript | Clean Architecture | +| 前端UI | TailwindCSS + Radix UI | 响应式设计 | +| 前端状态 | Zustand + React Query | 轻量高效 | +| 后端框架 | NestJS | DDD + Hexagonal Architecture | +| API协议 | REST + WebSocket | 实时对话支持 | +| 主数据库 | PostgreSQL 15 | 结构化数据存储 | +| 向量数据库 | pgvector | RAG 向量检索 | +| 图数据库 | Neo4j | 知识图谱 + 长期记忆 | +| 缓存 | Redis | 会话 + 缓存 | +| 消息队列 | Kafka | 事件驱动 + Outbox模式 | +| AI引擎 | Claude Agent SDK | 对话 + 推理 | + +--- + +## 项目目录结构 + +``` +iconsulting/ +├── docs/ # 项目文档 +│ ├── architecture/ # 架构设计文档 +│ ├── api/ # API 文档 +│ └── deployment/ # 部署文档 +│ +├── packages/ # Monorepo 包管理 +│ ├── shared/ # 共享代码 +│ │ ├── types/ # TypeScript 类型定义 +│ │ ├── utils/ # 工具函数 +│ │ └── constants/ # 常量定义 +│ │ +│ ├── web-client/ # 用户端 Web 应用 (PC/H5) +│ │ ├── src/ +│ │ │ ├── app/ # 应用层 +│ │ │ │ ├── providers/ # Context Providers +│ │ │ │ ├── routes/ # 路由配置 +│ │ │ │ └── App.tsx +│ │ │ ├── features/ # 功能模块 (Clean Architecture) +│ │ │ │ ├── chat/ # 聊天功能 +│ │ │ │ │ ├── domain/ # 领域层 +│ │ │ │ │ ├── data/ # 数据层 +│ │ │ │ │ └── presentation/# 表现层 +│ │ │ │ ├── payment/ # 支付功能 +│ │ │ │ └── user/ # 用户功能 +│ │ │ ├── shared/ # 共享组件 +│ │ │ │ ├── components/ # UI 组件 +│ │ │ │ ├── hooks/ # 自定义 Hooks +│ │ │ │ └── utils/ # 工具函数 +│ │ │ └── main.tsx +│ │ ├── package.json +│ │ └── vite.config.ts +│ │ +│ ├── admin-client/ # 管理端 Web 应用 +│ │ ├── src/ +│ │ │ ├── app/ +│ │ │ ├── features/ +│ │ │ │ ├── dashboard/ # 仪表盘 +│ │ │ │ ├── knowledge/ # 知识库管理 +│ │ │ │ ├── evolution/ # 进化管理 +│ │ │ │ ├── users/ # 用户管理 +│ │ │ │ ├── orders/ # 订单管理 +│ │ │ │ └── settings/ # 系统设置 +│ │ │ └── shared/ +│ │ └── package.json +│ │ +│ └── services/ # 后端微服务 +│ ├── api-gateway/ # API 网关 +│ │ ├── src/ +│ │ │ ├── main.ts +│ │ │ ├── gateway.module.ts +│ │ │ └── middleware/ +│ │ └── package.json +│ │ +│ ├── user-service/ # 用户服务 +│ │ ├── src/ +│ │ │ ├── domain/ # 领域层 +│ │ │ │ ├── entities/ +│ │ │ │ ├── value-objects/ +│ │ │ │ ├── repositories/ +│ │ │ │ └── services/ +│ │ │ ├── application/ # 应用层 +│ │ │ │ ├── commands/ +│ │ │ │ ├── queries/ +│ │ │ │ └── services/ +│ │ │ ├── infrastructure/ # 基础设施层 +│ │ │ │ ├── persistence/ +│ │ │ │ ├── messaging/ +│ │ │ │ └── external/ +│ │ │ └── interfaces/ # 接口层 +│ │ │ ├── http/ +│ │ │ └── grpc/ +│ │ └── package.json +│ │ +│ ├── conversation-service/ # 对话服务 (核心) +│ │ ├── src/ +│ │ │ ├── domain/ +│ │ │ │ ├── entities/ +│ │ │ │ │ ├── conversation.entity.ts +│ │ │ │ │ ├── message.entity.ts +│ │ │ │ │ └── session.entity.ts +│ │ │ │ ├── value-objects/ +│ │ │ │ ├── repositories/ +│ │ │ │ └── services/ +│ │ │ │ └── claude-agent.service.ts +│ │ │ ├── application/ +│ │ │ │ ├── commands/ +│ │ │ │ │ ├── send-message.command.ts +│ │ │ │ │ └── start-conversation.command.ts +│ │ │ │ └── services/ +│ │ │ │ └── conversation.service.ts +│ │ │ ├── infrastructure/ +│ │ │ │ ├── claude/ # Claude Agent SDK 集成 +│ │ │ │ │ ├── claude-client.ts +│ │ │ │ │ ├── tools/ # Agent Tools +│ │ │ │ │ └── prompts/ +│ │ │ │ └── persistence/ +│ │ │ └── interfaces/ +│ │ │ ├── http/ +│ │ │ └── websocket/ +│ │ └── package.json +│ │ +│ ├── knowledge-service/ # 知识服务 +│ │ ├── src/ +│ │ │ ├── domain/ +│ │ │ │ ├── entities/ +│ │ │ │ │ ├── document.entity.ts +│ │ │ │ │ ├── knowledge-node.entity.ts +│ │ │ │ │ └── memory.entity.ts +│ │ │ │ └── services/ +│ │ │ │ ├── rag.service.ts +│ │ │ │ └── graph.service.ts +│ │ │ ├── application/ +│ │ │ ├── infrastructure/ +│ │ │ │ ├── vector-store/ # pgvector +│ │ │ │ ├── graph-db/ # Neo4j +│ │ │ │ └── embedding/ # 向量化 +│ │ │ └── interfaces/ +│ │ └── package.json +│ │ +│ ├── payment-service/ # 支付服务 +│ │ ├── src/ +│ │ │ ├── domain/ +│ │ │ │ ├── entities/ +│ │ │ │ │ ├── order.entity.ts +│ │ │ │ │ └── payment.entity.ts +│ │ │ │ └── services/ +│ │ │ ├── application/ +│ │ │ ├── infrastructure/ +│ │ │ │ ├── alipay/ # 支付宝 +│ │ │ │ ├── wechat-pay/ # 微信支付 +│ │ │ │ └── stripe/ # 信用卡(Stripe) +│ │ │ └── interfaces/ +│ │ └── package.json +│ │ +│ ├── admin-service/ # 管理服务 +│ │ ├── src/ +│ │ │ ├── domain/ +│ │ │ │ ├── entities/ +│ │ │ │ │ ├── admin-user.entity.ts +│ │ │ │ │ ├── role.entity.ts +│ │ │ │ │ └── system-config.entity.ts +│ │ │ │ └── services/ +│ │ │ ├── application/ +│ │ │ ├── infrastructure/ +│ │ │ └── interfaces/ +│ │ └── package.json +│ │ +│ └── evolution-service/ # 进化服务 (核心) +│ ├── src/ +│ │ ├── domain/ +│ │ │ ├── entities/ +│ │ │ │ ├── experience.entity.ts +│ │ │ │ ├── pattern.entity.ts +│ │ │ │ └── evolution-log.entity.ts +│ │ │ └── services/ +│ │ │ ├── experience-extractor.service.ts +│ │ │ ├── pattern-recognizer.service.ts +│ │ │ └── evolution-engine.service.ts +│ │ ├── application/ +│ │ │ ├── commands/ +│ │ │ │ ├── extract-experience.command.ts +│ │ │ │ └── apply-evolution.command.ts +│ │ │ └── services/ +│ │ ├── infrastructure/ +│ │ │ ├── analyzers/ # 分析器 +│ │ │ └── adapters/ # 适配器 +│ │ └── interfaces/ +│ └── package.json +│ +├── infrastructure/ # 基础设施配置 +│ ├── docker/ +│ │ ├── docker-compose.yml +│ │ ├── docker-compose.dev.yml +│ │ └── services/ +│ │ ├── postgres/ +│ │ ├── neo4j/ +│ │ ├── redis/ +│ │ └── kafka/ +│ ├── k8s/ # Kubernetes 配置 (可选) +│ └── scripts/ # 部署脚本 +│ +├── package.json # Root package.json +├── pnpm-workspace.yaml # pnpm workspace 配置 +├── turbo.json # Turborepo 配置 +├── tsconfig.base.json # TypeScript 基础配置 +├── .env.example # 环境变量示例 +└── README.md # 项目说明 +``` + +--- + +## 核心模块设计 + +### 1. 对话服务 (Conversation Service) + +#### Claude Agent SDK 集成 + +```typescript +// conversation-service/src/infrastructure/claude/claude-client.ts + +import Anthropic from '@anthropic-ai/sdk'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class ClaudeAgentClient { + private client: Anthropic; + + constructor(private configService: ConfigService) { + this.client = new Anthropic({ + apiKey: this.configService.get('ANTHROPIC_API_KEY'), + }); + } + + async createConversation( + systemPrompt: string, + tools: Tool[], + ): Promise { + // 创建带有工具的对话会话 + } + + async sendMessage( + sessionId: string, + message: string, + context: ConversationContext, + ): AsyncGenerator { + // 流式发送消息并获取响应 + } +} +``` + +#### 对话工具定义 + +系统需要定义以下 Agent Tools: + +1. **知识检索工具** - 从 RAG 系统检索相关知识 +2. **评估工具** - 收集用户信息进行移民评估 +3. **支付工具** - 生成支付二维码 +4. **记忆工具** - 读写长期记忆 + +```typescript +// conversation-service/src/infrastructure/claude/tools/index.ts + +export const immigrationTools = [ + { + name: 'search_knowledge', + description: '搜索香港移民相关知识库', + input_schema: { + type: 'object', + properties: { + query: { type: 'string', description: '搜索查询' }, + category: { + type: 'string', + enum: ['优才计划', '专才计划', '留学IANG', '高才通', '投资移民', '科技人才'], + description: '移民类别' + } + }, + required: ['query'] + } + }, + { + name: 'start_assessment', + description: '开始移民资格评估(付费服务)', + input_schema: { + type: 'object', + properties: { + category: { type: 'string', description: '评估的移民类别' }, + user_info: { type: 'object', description: '用户基本信息' } + }, + required: ['category'] + } + }, + { + name: 'generate_payment', + description: '生成支付二维码', + input_schema: { + type: 'object', + properties: { + amount: { type: 'number', description: '支付金额' }, + service_type: { type: 'string', description: '服务类型' }, + payment_method: { + type: 'string', + enum: ['alipay', 'wechat', 'credit_card'], + description: '支付方式' + } + }, + required: ['amount', 'service_type', 'payment_method'] + } + }, + { + name: 'check_off_topic', + description: '检查问题是否与移民相关', + input_schema: { + type: 'object', + properties: { + question: { type: 'string', description: '用户问题' } + }, + required: ['question'] + } + } +]; +``` + +#### 系统提示词设计 + +```typescript +// conversation-service/src/infrastructure/claude/prompts/system-prompt.ts + +export const buildSystemPrompt = (config: SystemConfig): string => ` +你是 iConsulting 的香港移民咨询顾问,专门为用户提供香港各类移民政策的咨询服务。 + +## 身份定位 +${config.identity || '专业、友善、耐心的移民顾问'} + +## 服务范围 +你只回答与香港移民相关的问题,包括以下6大类别: + +### 1. 优才计划 (QMAS - Quality Migrant Admission Scheme) +- **目的**: 吸纳优秀人才来港定居 +- **审核标准**: 年龄、教育经历、工作经验、语言能力、年收入证明、良好品格、是否持有公司等 +- **人才类型**: 行业翘楚、精英人士 +- **名额**: 暂无名额限制 +- **申请要点**: 年龄、教育经历、工作经验、年收入证明、良好品格,是否符合等方面的要求(共12项),满足其中6项为基本门槛,择优批准 + +### 2. 专才计划 (GEP - General Employment Policy) +- **目的**: 吸引专业人才来港就业 +- **审核标准**: 雇佣企业的行业背景、行业内的地位、政策扶持力度、所属行业经济地位 +- **人才类型**: 专业人士(≥200万年薪,属于人才清单) +- **名额**: 无配额限制(不限行业) +- **申请要点**: 必须有雇主担保、要保持工作连续性(被裁3个月内)、工作和居住在非粤港澳、申请前至少有一年半社保记录、提供所在公司配套宣传图 + +### 3. 留学IANG (Immigration Arrangements for Non-local Graduates) +- **目的**: 吸引优秀学生人才来港续读 +- **审核标准**: 有无完成学业、是否有雇主担保 +- **人才类型**: 成绩优异、拥有国际化视野的学生 +- **名额**: 扩展到本港大学大湾区校区毕业生,为期2年、1年后续签 +- **申请要点**: 必须有雇主担保、要保持工作连续性 + +### 4. 高才通计划 (TTPS - Top Talent Pass Scheme) +- **目的**: 吸引高端人才来港 +- **审核标准**: + - A类: 年薪≥250万港币 + - B类: 毕业于世界前100名大学,5年内有3年以上工作经验 + - C类: 毕业于世界前100名大学,5年内有少于3年工作经验(年度限额10,000名) +- **人才类型**: 顶尖人才 +- **名额**: 无限期计划(推出后制一进行检讨) + +### 5. 投资移民 (新资本投资者入境计划) +- **目的**: 吸引投资者 +- **审核标准**: 投资不少于3,000万港币(或等值外币)净资产于其指定对实益拥有者的许可投资 +- **人才类型**: 投资者 +- **名额**: 无名额限制 +- **申请要点**: 资产审查新政3月1日起,申请人只须证明在提出申请六个月前已期限内持有该资产(1,000万元额度可获考虑) + +### 6. 科技人才入境计划 (TechTAS) +- **目的**: 吸引科技人才来港就业 +- **审核标准**: 申请人主要从事先进通讯技术、人工智能、生物科技、网络安全、数据分析、数码娱乐、金融科技、绿色科技、集成电路设计、物联网、材料科学、微电子、量子科技、机械人技术等研究工作;申请薪酬应不低于香港特区该业务的市场薪酬水平 +- **人才类型**: 科技人才 +- **名额**: 需用公司高新创新科技营配出有效配额 +- **申请要点**: 由符合创新科技署发出的获配证出有效配额 + +## 行为准则 +1. 如果用户询问与移民无关的问题,礼貌地引导回移民话题 +2. 对于复杂的评估需求,建议使用付费评估服务 +3. 提供信息时注明信息来源和更新时间 +4. 不做任何法律承诺或申请成功率的保证 + +## 付费服务 +当用户需要个性化移民评估时,介绍付费评估服务: +- 说明服务内容和价值 +- 确认用户意愿后生成支付码 + +## 对话风格 +${config.conversationStyle || '专业但不生硬,用简洁明了的语言解答'} + +## 已积累的经验 +${config.accumulatedExperience || '暂无'} + +## 管理员特别指示 +${config.adminInstructions || '暂无'} +`; +``` + +### 2. 知识服务 (Knowledge Service) + +#### RAG 架构 + +```typescript +// knowledge-service/src/domain/services/rag.service.ts + +@Injectable() +export class RAGService { + constructor( + private vectorStore: VectorStoreRepository, + private embeddingService: EmbeddingService, + private graphService: GraphService, + ) {} + + async search(query: string, options: SearchOptions): Promise { + // 1. 向量相似度搜索 + const embedding = await this.embeddingService.embed(query); + const vectorResults = await this.vectorStore.search(embedding, options.limit); + + // 2. 图谱增强(可选) + if (options.useGraph) { + const graphContext = await this.graphService.getRelatedNodes( + vectorResults.map(r => r.nodeId) + ); + return this.mergeResults(vectorResults, graphContext); + } + + return vectorResults; + } + + async addDocument(doc: Document): Promise { + // 1. 文档分块 + const chunks = await this.chunkDocument(doc); + + // 2. 向量化并存储 + for (const chunk of chunks) { + const embedding = await this.embeddingService.embed(chunk.content); + await this.vectorStore.insert({ + content: chunk.content, + embedding, + metadata: chunk.metadata, + }); + } + + // 3. 更新知识图谱 + await this.graphService.addDocumentNodes(doc, chunks); + } +} +``` + +#### 知识图谱 (Neo4j) + +```typescript +// knowledge-service/src/infrastructure/graph-db/neo4j.repository.ts + +@Injectable() +export class Neo4jRepository { + constructor(private neo4jService: Neo4jService) {} + + // 添加用户记忆节点 + async addMemory(memory: Memory): Promise { + await this.neo4jService.write(` + MERGE (u:User {id: $userId}) + CREATE (m:Memory { + id: $memoryId, + content: $content, + timestamp: datetime($timestamp), + type: $type + }) + CREATE (u)-[:HAS_MEMORY {timestamp: datetime($timestamp)}]->(m) + `, { + userId: memory.userId, + memoryId: memory.id, + content: memory.content, + timestamp: memory.timestamp.toISOString(), + type: memory.type, + }); + } + + // 获取用户时间线记忆 + async getUserTimeline(userId: string, limit: number = 50): Promise { + const result = await this.neo4jService.read(` + MATCH (u:User {id: $userId})-[r:HAS_MEMORY]->(m:Memory) + RETURN m + ORDER BY m.timestamp DESC + LIMIT $limit + `, { userId, limit }); + + return result.records.map(r => this.mapToMemory(r.get('m'))); + } + + // 知识关联查询 + async getRelatedKnowledge(topic: string): Promise { + const result = await this.neo4jService.read(` + MATCH (n:Knowledge)-[r:RELATED_TO*1..2]-(related) + WHERE n.topic CONTAINS $topic + RETURN n, related, r + `, { topic }); + + return this.mapToKnowledgeNodes(result); + } +} +``` + +### 3. 进化服务 (Evolution Service) + +这是系统的核心创新点,实现自我学习和进化。 + +#### 经验提取器 + +```typescript +// evolution-service/src/domain/services/experience-extractor.service.ts + +@Injectable() +export class ExperienceExtractorService { + constructor( + private claudeClient: ClaudeAgentClient, + private experienceRepo: ExperienceRepository, + ) {} + + // 从对话中提取经验 + async extractFromConversation(conversation: Conversation): Promise { + const prompt = ` + 分析以下对话,提取有价值的经验: + 1. 用户常见问题模式 + 2. 有效的回答策略 + 3. 用户满意度信号 + 4. 需要改进的地方 + + 对话内容: + ${JSON.stringify(conversation.messages)} + + 请以JSON格式返回经验列表。 + `; + + const response = await this.claudeClient.analyze(prompt); + return this.parseExperiences(response); + } + + // 定期批量分析 + async batchAnalyze(): Promise { + const conversations = await this.getRecentConversations(); + + const prompt = ` + 分析以下 ${conversations.length} 个对话,总结: + 1. 高频问题 TOP 10 + 2. 用户最关心的移民类别 + 3. 转化率较高的话术 + 4. 常见的用户困惑点 + 5. 建议的系统改进方向 + + 请生成详细的分析报告。 + `; + + return await this.claudeClient.analyze(prompt); + } +} +``` + +#### 进化引擎 + +```typescript +// evolution-service/src/domain/services/evolution-engine.service.ts + +@Injectable() +export class EvolutionEngineService { + constructor( + private experienceRepo: ExperienceRepository, + private configService: SystemConfigService, + private claudeClient: ClaudeAgentClient, + ) {} + + // 与管理员对话,理解意图 + async interactWithAdmin( + adminMessage: string, + context: AdminContext, + ): Promise { + const experiences = await this.experienceRepo.getRecentExperiences(); + const currentConfig = await this.configService.getCurrentConfig(); + + const prompt = ` + 你是 iConsulting 系统的进化引擎。 + + ## 当前系统配置 + ${JSON.stringify(currentConfig)} + + ## 近期积累的经验 + ${JSON.stringify(experiences)} + + ## 管理员指令 + ${adminMessage} + + ## 任务 + 1. 理解管理员的意图 + 2. 基于积累的经验,提出具体的调整方案 + 3. 生成新的配置或行为规则 + + 请返回: + - 对管理员意图的理解 + - 建议的调整方案 + - 需要确认的事项(如有) + `; + + return await this.claudeClient.analyze(prompt); + } + + // 应用进化 + async applyEvolution(evolution: Evolution): Promise { + // 更新系统提示词 + if (evolution.promptChanges) { + await this.configService.updatePrompt(evolution.promptChanges); + } + + // 更新知识库 + if (evolution.knowledgeChanges) { + await this.knowledgeService.applyChanges(evolution.knowledgeChanges); + } + + // 更新业务规则 + if (evolution.ruleChanges) { + await this.configService.updateRules(evolution.ruleChanges); + } + + // 记录进化日志 + await this.evolutionLogRepo.save({ + timestamp: new Date(), + changes: evolution, + triggeredBy: evolution.adminId, + }); + } +} +``` + +### 4. 支付服务 (Payment Service) + +```typescript +// payment-service/src/infrastructure/alipay/alipay.adapter.ts + +@Injectable() +export class AlipayAdapter implements PaymentGateway { + constructor(private configService: ConfigService) { + this.alipay = new AlipaySdk({ + appId: this.configService.get('ALIPAY_APP_ID'), + privateKey: this.configService.get('ALIPAY_PRIVATE_KEY'), + // ... + }); + } + + async createPayment(order: Order): Promise { + const result = await this.alipay.exec('alipay.trade.precreate', { + bizContent: { + out_trade_no: order.id, + total_amount: order.amount.toString(), + subject: order.subject, + } + }); + + return { + qrCode: result.qrCode, + orderId: order.id, + expireAt: new Date(Date.now() + 30 * 60 * 1000), // 30分钟有效 + }; + } + + async handleCallback(payload: AlipayCallback): Promise { + // 验签并处理回调 + } +} +``` + +### 5. 用户服务 (User Service) + +#### 匿名用户处理 + +```typescript +// user-service/src/domain/entities/user.entity.ts + +export class User extends AggregateRoot { + id: UserId; + type: UserType; // ANONYMOUS | REGISTERED + fingerprint?: string; + phone?: string; + createdAt: Date; + lastActiveAt: Date; + + static createAnonymous(fingerprint: string): User { + return new User({ + id: UserId.generate(), + type: UserType.ANONYMOUS, + fingerprint, + createdAt: new Date(), + lastActiveAt: new Date(), + }); + } + + upgradeToRegistered(phone: string): void { + this.phone = phone; + this.type = UserType.REGISTERED; + this.addDomainEvent(new UserUpgradedEvent(this.id, phone)); + } +} +``` + +--- + +## 数据模型设计 + +### PostgreSQL 表结构 + +```sql +-- 用户表 +CREATE TABLE users ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + type VARCHAR(20) NOT NULL DEFAULT 'ANONYMOUS', + fingerprint VARCHAR(255), + phone VARCHAR(20), + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + last_active_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- 对话表 +CREATE TABLE conversations ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID REFERENCES users(id), + status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE', + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + ended_at TIMESTAMP WITH TIME ZONE +); + +-- 消息表 +CREATE TABLE messages ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + conversation_id UUID REFERENCES conversations(id), + role VARCHAR(20) NOT NULL, -- user, assistant, system + content TEXT NOT NULL, + metadata JSONB, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- 订单表 +CREATE TABLE orders ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID REFERENCES users(id), + service_type VARCHAR(50) NOT NULL, + amount DECIMAL(10, 2) NOT NULL, + status VARCHAR(20) NOT NULL DEFAULT 'PENDING', + payment_method VARCHAR(20), + paid_at TIMESTAMP WITH TIME ZONE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- 发件箱表 (Outbox Pattern) +CREATE TABLE outbox ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + aggregate_type VARCHAR(100) NOT NULL, + aggregate_id UUID NOT NULL, + event_type VARCHAR(100) NOT NULL, + payload JSONB NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + processed_at TIMESTAMP WITH TIME ZONE +); + +-- 知识文档表 +CREATE TABLE documents ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + title VARCHAR(255) NOT NULL, + content TEXT NOT NULL, + category VARCHAR(50), + metadata JSONB, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- 向量存储表 (pgvector) +CREATE TABLE document_embeddings ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + document_id UUID REFERENCES documents(id), + chunk_index INT NOT NULL, + content TEXT NOT NULL, + embedding vector(1536), + metadata JSONB, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- 系统配置表 +CREATE TABLE system_configs ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + key VARCHAR(100) UNIQUE NOT NULL, + value JSONB NOT NULL, + updated_by UUID, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- 管理员表 +CREATE TABLE admin_users ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + username VARCHAR(100) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, + role VARCHAR(50) NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- 进化日志表 +CREATE TABLE evolution_logs ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + triggered_by UUID REFERENCES admin_users(id), + change_type VARCHAR(50) NOT NULL, + changes JSONB NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); +``` + +### Neo4j 图模型 + +```cypher +// 用户节点 +(:User { + id: string, + type: string, + createdAt: datetime +}) + +// 记忆节点 +(:Memory { + id: string, + content: string, + type: string, // FACT, PREFERENCE, INTERACTION + timestamp: datetime +}) + +// 知识节点 +(:Knowledge { + id: string, + topic: string, + content: string, + category: string, + updatedAt: datetime +}) + +// 经验节点 +(:Experience { + id: string, + pattern: string, + insight: string, + frequency: int, + createdAt: datetime +}) + +// 关系定义 +(User)-[:HAS_MEMORY {timestamp}]->(Memory) +(User)-[:ASKED_ABOUT]->(Knowledge) +(Knowledge)-[:RELATED_TO {weight}]->(Knowledge) +(Experience)-[:DERIVED_FROM]->(Conversation) +(Experience)-[:INFLUENCES]->(SystemConfig) +``` + +--- + +## API 设计 + +### REST API + +```yaml +# 用户端 API +/api/v1: + /auth: + /anonymous: + POST: 创建匿名会话 + /verify-phone: + POST: 发送验证码 + /login: + POST: 手机号登录 + + /conversations: + POST: 创建新对话 + GET: 获取对话列表 + /{id}: + GET: 获取对话详情 + + /messages: + POST: 发送消息 (HTTP) + + /payments: + POST: 创建支付订单 + /{id}/status: + GET: 查询支付状态 + +# 管理端 API +/admin/api/v1: + /auth: + /login: + POST: 管理员登录 + + /knowledge: + GET: 获取知识列表 + POST: 添加知识 + /{id}: + PUT: 更新知识 + DELETE: 删除知识 + /upload: + POST: 上传文档 + + /evolution: + /chat: + POST: 与系统进化引擎对话 + /experiences: + GET: 获取经验总结 + /apply: + POST: 应用进化变更 + + /configs: + GET: 获取配置 + PUT: 更新配置 + + /analytics: + /dashboard: + GET: 获取仪表盘数据 + /conversations: + GET: 对话统计 + /payments: + GET: 支付统计 +``` + +### WebSocket API + +```typescript +// 对话 WebSocket +ws://host/ws/conversation + +// 客户端 -> 服务端 +{ + type: 'message', + conversationId: 'uuid', + content: '用户消息' +} + +// 服务端 -> 客户端 (流式) +{ + type: 'stream_start', + messageId: 'uuid' +} +{ + type: 'stream_chunk', + messageId: 'uuid', + content: '部分内容', + index: 0 +} +{ + type: 'stream_end', + messageId: 'uuid' +} +{ + type: 'tool_call', + tool: 'generate_payment', + result: { qrCode: '...', orderId: '...' } +} +``` + +--- + +## 前端架构 (Clean Architecture) + +### 目录结构详解 + +``` +features/chat/ +├── domain/ # 领域层 +│ ├── entities/ +│ │ ├── Message.ts # 消息实体 +│ │ └── Conversation.ts # 对话实体 +│ ├── repositories/ +│ │ └── IChatRepository.ts # 仓储接口 +│ └── usecases/ +│ ├── SendMessageUseCase.ts +│ └── GetConversationUseCase.ts +│ +├── data/ # 数据层 +│ ├── repositories/ +│ │ └── ChatRepositoryImpl.ts +│ ├── datasources/ +│ │ ├── ChatApiDataSource.ts +│ │ └── ChatWebSocketDataSource.ts +│ └── models/ +│ ├── MessageModel.ts +│ └── ConversationModel.ts +│ +└── presentation/ # 表现层 + ├── components/ + │ ├── ChatWindow.tsx + │ ├── MessageBubble.tsx + │ ├── InputArea.tsx + │ └── PaymentModal.tsx + ├── hooks/ + │ ├── useChat.ts + │ └── useWebSocket.ts + ├── stores/ + │ └── chatStore.ts + └── pages/ + └── ChatPage.tsx +``` + +### 核心组件示例 + +```tsx +// features/chat/presentation/components/ChatWindow.tsx + +import { useChat } from '../hooks/useChat'; +import { MessageBubble } from './MessageBubble'; +import { InputArea } from './InputArea'; + +export const ChatWindow: React.FC = () => { + const { + messages, + isStreaming, + sendMessage, + currentStreamContent + } = useChat(); + + return ( +
+
+ {messages.map((msg) => ( + + ))} + {isStreaming && ( + + )} +
+ +
+ ); +}; +``` + +--- + +## 部署方案 + +### Docker Compose (开发环境) + +```yaml +# infrastructure/docker/docker-compose.dev.yml +version: '3.8' + +services: + postgres: + image: pgvector/pgvector:pg15 + environment: + POSTGRES_USER: iconsulting + POSTGRES_PASSWORD: dev_password + POSTGRES_DB: iconsulting + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + + neo4j: + image: neo4j:5 + environment: + NEO4J_AUTH: neo4j/dev_password + ports: + - "7474:7474" + - "7687:7687" + volumes: + - neo4j_data:/data + + redis: + image: redis:7-alpine + ports: + - "6379:6379" + + zookeeper: + image: confluentinc/cp-zookeeper:7.4.0 + environment: + ZOOKEEPER_CLIENT_PORT: 2181 + ports: + - "2181:2181" + + kafka: + image: confluentinc/cp-kafka:7.4.0 + depends_on: + - zookeeper + ports: + - "9092:9092" + environment: + KAFKA_BROKER_ID: 1 + KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092 + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + +volumes: + postgres_data: + neo4j_data: +``` + +--- + +## 开发计划 + +### Phase 1: 基础架构 (核心) +1. 项目初始化和工程配置 +2. 数据库 Schema 和连接 +3. 基础微服务框架搭建 +4. API 网关配置 + +### Phase 2: 核心对话功能 +1. Claude Agent SDK 集成 +2. 对话服务完整实现 +3. WebSocket 实时通信 +4. 基础知识检索 (RAG) + +### Phase 3: 支付和用户 +1. 支付服务对接 +2. 用户认证体系 +3. 订单管理 + +### Phase 4: 知识和记忆 +1. Neo4j 知识图谱 +2. 长期记忆系统 +3. RAG 优化 + +### Phase 5: 进化系统 +1. 经验提取引擎 +2. 管理员交互界面 +3. 自动进化机制 + +### Phase 6: 管理后台 +1. 管理员认证 +2. 知识库管理 +3. 系统配置 +4. 数据统计 + +### Phase 7: 前端完善 +1. 用户端 UI 完善 +2. 管理端 UI 完善 +3. 移动端适配 + +### Phase 8: 测试和优化 +1. 单元测试 +2. 集成测试 +3. 性能优化 +4. 安全审计 + +--- + +## 环境变量配置 + +```env +# .env.example + +# Anthropic +ANTHROPIC_API_KEY=sk-ant-xxx + +# PostgreSQL +POSTGRES_HOST=localhost +POSTGRES_PORT=5432 +POSTGRES_USER=iconsulting +POSTGRES_PASSWORD=your_password +POSTGRES_DB=iconsulting + +# Neo4j +NEO4J_URI=bolt://localhost:7687 +NEO4J_USER=neo4j +NEO4J_PASSWORD=your_password + +# Redis +REDIS_HOST=localhost +REDIS_PORT=6379 + +# Kafka +KAFKA_BROKERS=localhost:9092 + +# 支付宝 +ALIPAY_APP_ID=your_app_id +ALIPAY_PRIVATE_KEY=your_private_key +ALIPAY_PUBLIC_KEY=alipay_public_key + +# 微信支付 +WECHAT_APP_ID=your_app_id +WECHAT_MCH_ID=your_mch_id +WECHAT_API_KEY=your_api_key + +# JWT +JWT_SECRET=your_jwt_secret +JWT_EXPIRES_IN=7d + +# 服务端口 +API_GATEWAY_PORT=3000 +USER_SERVICE_PORT=3001 +CONVERSATION_SERVICE_PORT=3002 +KNOWLEDGE_SERVICE_PORT=3003 +PAYMENT_SERVICE_PORT=3004 +ADMIN_SERVICE_PORT=3005 +EVOLUTION_SERVICE_PORT=3006 +``` + +--- + +## 安全考虑 + +1. **API 安全** + - JWT 认证 + - 请求限流 + - 输入验证 + - SQL 注入防护 + +2. **支付安全** + - 签名验证 + - 回调白名单 + - 金额校验 + +3. **数据安全** + - 敏感数据加密 + - 日志脱敏 + - 定期备份 + +4. **Claude API 安全** + - API Key 安全存储 + - 输入内容过滤 + - 输出内容审核 + +--- + +## 下一步行动 + +1. 确认移民类别列表(等待图片) +2. 创建项目目录结构 +3. 初始化 Monorepo 配置 +4. 搭建基础微服务框架 +5. 集成 Claude Agent SDK + +准备好后请告知,我将开始创建项目结构! diff --git a/README.md b/README.md new file mode 100644 index 0000000..08098c1 --- /dev/null +++ b/README.md @@ -0,0 +1,176 @@ +# iConsulting - 香港移民在线咨询系统 + +基于 Claude Agent SDK 的智能在线客服系统,专注于提供香港移民咨询服务。 + +## 功能特性 + +- **智能咨询**: 基于 Claude Agent SDK 的自然语言对话 +- **付费评估**: 移民资格评估服务,支持支付宝/微信/信用卡 +- **知识增强**: RAG + Neo4j 知识图谱 +- **自我进化**: 从对话中学习,根据管理员指令调整 +- **长期记忆**: 基于时间线的知识图谱记录 +- **多端支持**: PC Web / H5 响应式设计 + +## 支持的移民类别 + +1. **优才计划 (QMAS)** - 行业翘楚、精英人士 +2. **专才计划 (GEP)** - 专业人才 +3. **留学IANG** - 非本地毕业生 +4. **高才通 (TTPS)** - 高端人才 +5. **投资移民 (CIES)** - 投资者 +6. **科技人才 (TechTAS)** - 科技领域人才 + +## 技术架构 + +``` +├── 前端 (Clean Architecture) +│ ├── React 18 + TypeScript +│ ├── TailwindCSS + Radix UI +│ └── Zustand + React Query +│ +├── 后端 (DDD + Hexagonal + 微服务) +│ ├── NestJS +│ ├── Claude Agent SDK +│ └── TypeORM +│ +└── 基础设施 + ├── PostgreSQL + pgvector (RAG) + ├── Neo4j (知识图谱) + ├── Redis (缓存) + └── Kafka (消息队列) +``` + +## 项目结构 + +``` +iconsulting/ +├── packages/ +│ ├── shared/ # 共享类型、常量、工具 +│ ├── web-client/ # 用户端 Web 应用 +│ ├── admin-client/ # 管理端 Web 应用 +│ └── services/ # 后端微服务 +│ ├── api-gateway/ # API 网关 +│ ├── user-service/ # 用户服务 +│ ├── conversation-service/ # 对话服务 (核心) +│ ├── knowledge-service/ # 知识服务 +│ ├── payment-service/ # 支付服务 +│ ├── admin-service/ # 管理服务 +│ └── evolution-service/ # 进化服务 +│ +├── infrastructure/ +│ └── docker/ # Docker 配置 +│ +├── DEVELOPMENT_GUIDE.md # 详细开发指导 +└── README.md +``` + +## 快速开始 + +### 1. 安装依赖 + +```bash +# 安装 pnpm (如果没有) +npm install -g pnpm + +# 安装项目依赖 +pnpm install +``` + +### 2. 启动基础设施 + +```bash +# 启动 Docker 容器 (PostgreSQL, Neo4j, Redis, Kafka) +pnpm docker:dev +``` + +### 3. 配置环境变量 + +```bash +# 复制环境变量示例文件 +cp .env.example .env + +# 编辑 .env 文件,填入必要的配置 +# 特别是 ANTHROPIC_API_KEY +``` + +### 4. 运行数据库迁移 + +```bash +pnpm db:migrate +``` + +### 5. 启动开发服务器 + +```bash +# 启动所有服务 +pnpm dev +``` + +访问: +- 用户端: http://localhost:5173 +- 管理端: http://localhost:5174 +- API: http://localhost:3000 + +## 环境变量 + +关键配置项: + +```env +# Claude API +ANTHROPIC_API_KEY=sk-ant-xxx + +# 数据库 +POSTGRES_HOST=localhost +POSTGRES_PORT=5432 +POSTGRES_USER=iconsulting +POSTGRES_PASSWORD=your_password +POSTGRES_DB=iconsulting + +# Neo4j +NEO4J_URI=bolt://localhost:7687 +NEO4J_USER=neo4j +NEO4J_PASSWORD=your_password + +# 支付 (支付宝/微信) +ALIPAY_APP_ID=xxx +WECHAT_APP_ID=xxx +``` + +完整配置请参考 `.env.example` + +## 开发指南 + +详细的开发指导请参考 [DEVELOPMENT_GUIDE.md](./DEVELOPMENT_GUIDE.md) + +## 开发进度 + +### 已完成 + +- [x] 项目架构设计 +- [x] 开发指导文档 +- [x] Monorepo 配置 +- [x] 共享类型定义 +- [x] Docker 基础设施配置 +- [x] 数据库 Schema +- [x] 对话服务 (Claude Agent SDK 集成) +- [x] 用户端前端基础框架 + +### 进行中 + +- [ ] 用户服务 +- [ ] 知识服务 (RAG + Neo4j) +- [ ] 支付服务 +- [ ] 管理服务 (自我进化) +- [ ] 管理后台前端 + +## 贡献指南 + +1. Fork 本仓库 +2. 创建特性分支 (`git checkout -b feature/amazing-feature`) +3. 提交更改 (`git commit -m 'Add some amazing feature'`) +4. 推送到分支 (`git push origin feature/amazing-feature`) +5. 创建 Pull Request + +## 许可证 + +私有项目,保留所有权利。 diff --git a/deploy.sh b/deploy.sh new file mode 100644 index 0000000..90810e8 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,903 @@ +#!/bin/bash + +#=============================================================================== +# iConsulting 部署管理脚本 +# +# 用法: ./deploy.sh [service] [options] +# +# 命令: +# build - 编译构建 +# start - 启动服务 +# stop - 停止服务 +# restart - 重启服务 +# status - 查看状态 +# logs - 查看日志 +# clean - 清理构建产物 +# deploy - 完整部署(构建+启动) +# db - 数据库操作 +# help - 显示帮助 +# +# 服务: +# all - 所有服务 +# web-client - 用户前端 +# admin-client - 管理后台前端 +# conversation - 对话服务 +# user - 用户服务 +# payment - 支付服务 +# knowledge - 知识库服务 +# evolution - 进化服务 +# kong - API网关 +# postgres - PostgreSQL数据库 +# redis - Redis缓存 +# neo4j - Neo4j图数据库 +# nginx - Nginx静态服务 +# +#=============================================================================== + +set -e + +# 颜色定义 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +PURPLE='\033[0;35m' +CYAN='\033[0;36m' +NC='\033[0m' # No Color + +# 项目根目录 +PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$PROJECT_ROOT" + +# 配置 +COMPOSE_FILE="docker-compose.yml" +ENV_FILE=".env" + +# 服务端口配置 +declare -A SERVICE_PORTS=( + ["conversation"]=3004 + ["user"]=3001 + ["payment"]=3002 + ["knowledge"]=3003 + ["evolution"]=3005 + ["kong"]=8000 + ["postgres"]=5432 + ["redis"]=6379 + ["neo4j"]=7474 + ["nginx"]=80 +) + +# 服务目录映射 +declare -A SERVICE_DIRS=( + ["conversation"]="packages/services/conversation-service" + ["user"]="packages/services/user-service" + ["payment"]="packages/services/payment-service" + ["knowledge"]="packages/services/knowledge-service" + ["evolution"]="packages/services/evolution-service" + ["web-client"]="packages/web-client" + ["admin-client"]="packages/admin-client" + ["shared"]="packages/shared" +) + +# Docker服务名映射 +declare -A DOCKER_SERVICES=( + ["conversation"]="conversation-service" + ["user"]="user-service" + ["payment"]="payment-service" + ["knowledge"]="knowledge-service" + ["evolution"]="evolution-service" + ["web-client"]="web-client" + ["admin-client"]="admin-client" + ["kong"]="kong" + ["postgres"]="postgres" + ["redis"]="redis" + ["neo4j"]="neo4j" + ["nginx"]="nginx" +) + +#=============================================================================== +# 工具函数 +#=============================================================================== + +log_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +log_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +log_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +log_step() { + echo -e "${PURPLE}[STEP]${NC} $1" +} + +# 检查命令是否存在 +check_command() { + if ! command -v "$1" &> /dev/null; then + log_error "$1 未安装,请先安装" + exit 1 + fi +} + +# 检查环境 +check_environment() { + log_step "检查运行环境..." + + check_command "node" + check_command "pnpm" + check_command "docker" + check_command "docker-compose" + + # 检查 Node 版本 + NODE_VERSION=$(node -v | cut -d'v' -f2 | cut -d'.' -f1) + if [ "$NODE_VERSION" -lt 18 ]; then + log_error "Node.js 版本需要 >= 18,当前版本: $(node -v)" + exit 1 + fi + + log_success "环境检查通过" +} + +# 加载环境变量 +load_env() { + if [ -f "$ENV_FILE" ]; then + export $(grep -v '^#' "$ENV_FILE" | xargs) + fi +} + +# 等待服务就绪 +wait_for_service() { + local host=$1 + local port=$2 + local service=$3 + local max_attempts=${4:-30} + local attempt=1 + + log_info "等待 $service ($host:$port) 就绪..." + + while [ $attempt -le $max_attempts ]; do + if nc -z "$host" "$port" 2>/dev/null; then + log_success "$service 已就绪" + return 0 + fi + echo -n "." + sleep 2 + attempt=$((attempt + 1)) + done + + echo "" + log_error "$service 启动超时" + return 1 +} + +#=============================================================================== +# 构建函数 +#=============================================================================== + +# 安装依赖 +install_deps() { + log_step "安装项目依赖..." + pnpm install + log_success "依赖安装完成" +} + +# 构建共享包 +build_shared() { + log_step "构建 shared 包..." + cd "$PROJECT_ROOT/${SERVICE_DIRS[shared]}" + pnpm run build + cd "$PROJECT_ROOT" + log_success "shared 构建完成" +} + +# 构建单个后端服务 +build_backend_service() { + local service=$1 + local dir="${SERVICE_DIRS[$service]}" + + if [ -z "$dir" ]; then + log_error "未知服务: $service" + return 1 + fi + + log_step "构建 $service..." + cd "$PROJECT_ROOT/$dir" + + # 清理旧构建 + rm -rf dist + + # TypeScript 编译 + pnpm run build + + cd "$PROJECT_ROOT" + log_success "$service 构建完成" +} + +# 构建单个前端 +build_frontend() { + local service=$1 + local dir="${SERVICE_DIRS[$service]}" + + if [ -z "$dir" ]; then + log_error "未知服务: $service" + return 1 + fi + + log_step "构建 $service..." + cd "$PROJECT_ROOT/$dir" + + # 清理旧构建 + rm -rf dist + + # Vite 构建 + pnpm run build + + cd "$PROJECT_ROOT" + log_success "$service 构建完成" +} + +# 构建所有后端服务 +build_all_backend() { + build_shared + + for service in conversation user payment knowledge evolution; do + build_backend_service "$service" + done +} + +# 构建所有前端 +build_all_frontend() { + for service in web-client admin-client; do + build_frontend "$service" + done +} + +# 构建所有 +build_all() { + log_info "开始构建所有服务..." + install_deps + build_all_backend + build_all_frontend + log_success "所有服务构建完成" +} + +# 构建入口 +do_build() { + local target=${1:-all} + + case $target in + all) + build_all + ;; + shared) + build_shared + ;; + backend) + build_all_backend + ;; + frontend) + build_all_frontend + ;; + web-client|admin-client) + build_frontend "$target" + ;; + conversation|user|payment|knowledge|evolution) + build_backend_service "$target" + ;; + *) + log_error "未知构建目标: $target" + exit 1 + ;; + esac +} + +#=============================================================================== +# Docker 操作函数 +#=============================================================================== + +# 构建 Docker 镜像 +build_docker_images() { + local service=${1:-} + + log_step "构建 Docker 镜像..." + + if [ -n "$service" ] && [ "$service" != "all" ]; then + local docker_service="${DOCKER_SERVICES[$service]}" + if [ -n "$docker_service" ]; then + docker-compose build "$docker_service" + else + log_error "未知服务: $service" + return 1 + fi + else + docker-compose build + fi + + log_success "Docker 镜像构建完成" +} + +# 启动基础设施 +start_infrastructure() { + log_step "启动基础设施服务..." + + docker-compose up -d postgres redis neo4j + + # 等待数据库就绪 + wait_for_service localhost 5432 "PostgreSQL" + wait_for_service localhost 6379 "Redis" + wait_for_service localhost 7474 "Neo4j" + + log_success "基础设施启动完成" +} + +# 启动 Kong 网关 +start_kong() { + log_step "启动 Kong API 网关..." + + docker-compose up -d kong-database + sleep 5 + + # Kong 数据库迁移 + docker-compose run --rm kong kong migrations bootstrap || true + + docker-compose up -d kong + wait_for_service localhost 8000 "Kong" + + log_success "Kong 启动完成" +} + +# 启动后端服务 (非 Docker 模式) +start_backend_service_local() { + local service=$1 + local dir="${SERVICE_DIRS[$service]}" + local port="${SERVICE_PORTS[$service]}" + + if [ -z "$dir" ]; then + log_error "未知服务: $service" + return 1 + fi + + log_step "启动 $service (端口: $port)..." + + cd "$PROJECT_ROOT/$dir" + + # 检查是否已构建 + if [ ! -d "dist" ]; then + log_warning "$service 未构建,先进行构建..." + pnpm run build + fi + + # 使用 PM2 或直接启动 + if command -v pm2 &> /dev/null; then + pm2 start dist/main.js --name "iconsulting-$service" --cwd "$PROJECT_ROOT/$dir" + else + # 后台启动 + nohup node dist/main.js > "$PROJECT_ROOT/logs/$service.log" 2>&1 & + echo $! > "$PROJECT_ROOT/pids/$service.pid" + fi + + cd "$PROJECT_ROOT" + + sleep 2 + wait_for_service localhost "$port" "$service" 15 +} + +# 启动后端服务 (Docker 模式) +start_backend_service_docker() { + local service=$1 + local docker_service="${DOCKER_SERVICES[$service]}" + + log_step "启动 $service (Docker)..." + docker-compose up -d "$docker_service" + + local port="${SERVICE_PORTS[$service]}" + wait_for_service localhost "$port" "$service" +} + +# 启动所有后端服务 +start_all_backend() { + local mode=${1:-docker} + + for service in user payment knowledge conversation evolution; do + if [ "$mode" = "docker" ]; then + start_backend_service_docker "$service" + else + start_backend_service_local "$service" + fi + done +} + +# 启动 Nginx (静态文件服务) +start_nginx() { + log_step "启动 Nginx..." + docker-compose up -d nginx + wait_for_service localhost 80 "Nginx" + log_success "Nginx 启动完成" +} + +# 启动所有服务 +start_all() { + local mode=${1:-docker} + + log_info "开始启动所有服务 (模式: $mode)..." + + # 创建必要目录 + mkdir -p "$PROJECT_ROOT/logs" + mkdir -p "$PROJECT_ROOT/pids" + + start_infrastructure + start_kong + start_all_backend "$mode" + start_nginx + + log_success "所有服务启动完成" + do_status +} + +# 启动入口 +do_start() { + local target=${1:-all} + local mode=${2:-docker} + + load_env + + case $target in + all) + start_all "$mode" + ;; + infra|infrastructure) + start_infrastructure + ;; + kong) + start_kong + ;; + nginx) + start_nginx + ;; + postgres|redis|neo4j) + docker-compose up -d "$target" + ;; + conversation|user|payment|knowledge|evolution) + if [ "$mode" = "docker" ]; then + start_backend_service_docker "$target" + else + start_backend_service_local "$target" + fi + ;; + backend) + start_all_backend "$mode" + ;; + *) + log_error "未知启动目标: $target" + exit 1 + ;; + esac +} + +#=============================================================================== +# 停止函数 +#=============================================================================== + +# 停止单个服务 (本地模式) +stop_service_local() { + local service=$1 + + log_step "停止 $service..." + + if command -v pm2 &> /dev/null; then + pm2 stop "iconsulting-$service" 2>/dev/null || true + pm2 delete "iconsulting-$service" 2>/dev/null || true + else + local pid_file="$PROJECT_ROOT/pids/$service.pid" + if [ -f "$pid_file" ]; then + kill $(cat "$pid_file") 2>/dev/null || true + rm -f "$pid_file" + fi + fi + + log_success "$service 已停止" +} + +# 停止单个服务 (Docker 模式) +stop_service_docker() { + local service=$1 + local docker_service="${DOCKER_SERVICES[$service]}" + + if [ -n "$docker_service" ]; then + log_step "停止 $service..." + docker-compose stop "$docker_service" + log_success "$service 已停止" + fi +} + +# 停止所有服务 +stop_all() { + local mode=${1:-docker} + + log_info "停止所有服务..." + + if [ "$mode" = "docker" ]; then + docker-compose down + else + for service in conversation user payment knowledge evolution; do + stop_service_local "$service" + done + docker-compose down + fi + + log_success "所有服务已停止" +} + +# 停止入口 +do_stop() { + local target=${1:-all} + local mode=${2:-docker} + + case $target in + all) + stop_all "$mode" + ;; + infra|infrastructure) + docker-compose stop postgres redis neo4j + ;; + conversation|user|payment|knowledge|evolution) + if [ "$mode" = "docker" ]; then + stop_service_docker "$target" + else + stop_service_local "$target" + fi + ;; + kong|postgres|redis|neo4j|nginx) + docker-compose stop "$target" + ;; + *) + log_error "未知停止目标: $target" + exit 1 + ;; + esac +} + +#=============================================================================== +# 重启函数 +#=============================================================================== + +do_restart() { + local target=${1:-all} + local mode=${2:-docker} + + log_info "重启 $target..." + do_stop "$target" "$mode" + sleep 2 + do_start "$target" "$mode" +} + +#=============================================================================== +# 状态查看 +#=============================================================================== + +do_status() { + echo "" + echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" + echo -e "${CYAN} iConsulting 服务状态 ${NC}" + echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" + echo "" + + # Docker 服务状态 + echo -e "${PURPLE}Docker 容器状态:${NC}" + docker-compose ps + echo "" + + # 端口检查 + echo -e "${PURPLE}服务端口检查:${NC}" + printf "%-20s %-10s %-10s\n" "服务" "端口" "状态" + echo "----------------------------------------" + + for service in "${!SERVICE_PORTS[@]}"; do + local port="${SERVICE_PORTS[$service]}" + if nc -z localhost "$port" 2>/dev/null; then + printf "%-20s %-10s ${GREEN}%-10s${NC}\n" "$service" "$port" "运行中" + else + printf "%-20s %-10s ${RED}%-10s${NC}\n" "$service" "$port" "未运行" + fi + done + + echo "" + + # PM2 状态 (如果使用) + if command -v pm2 &> /dev/null; then + echo -e "${PURPLE}PM2 进程状态:${NC}" + pm2 list 2>/dev/null | grep iconsulting || echo "无 PM2 管理的服务" + echo "" + fi +} + +#=============================================================================== +# 日志查看 +#=============================================================================== + +do_logs() { + local service=${1:-all} + local lines=${2:-100} + + if [ "$service" = "all" ]; then + docker-compose logs -f --tail="$lines" + else + local docker_service="${DOCKER_SERVICES[$service]}" + if [ -n "$docker_service" ]; then + docker-compose logs -f --tail="$lines" "$docker_service" + else + # 本地日志 + local log_file="$PROJECT_ROOT/logs/$service.log" + if [ -f "$log_file" ]; then + tail -f -n "$lines" "$log_file" + else + log_error "日志文件不存在: $log_file" + fi + fi + fi +} + +#=============================================================================== +# 清理函数 +#=============================================================================== + +do_clean() { + local target=${1:-build} + + case $target in + build) + log_step "清理构建产物..." + for dir in "${SERVICE_DIRS[@]}"; do + rm -rf "$PROJECT_ROOT/$dir/dist" + done + log_success "构建产物已清理" + ;; + deps) + log_step "清理依赖..." + rm -rf node_modules + for dir in "${SERVICE_DIRS[@]}"; do + rm -rf "$PROJECT_ROOT/$dir/node_modules" + done + log_success "依赖已清理" + ;; + docker) + log_step "清理 Docker 资源..." + docker-compose down -v --rmi local + docker system prune -f + log_success "Docker 资源已清理" + ;; + logs) + log_step "清理日志..." + rm -rf "$PROJECT_ROOT/logs/*" + log_success "日志已清理" + ;; + all) + do_clean build + do_clean deps + do_clean docker + do_clean logs + ;; + *) + log_error "未知清理目标: $target (可选: build, deps, docker, logs, all)" + exit 1 + ;; + esac +} + +#=============================================================================== +# 完整部署 +#=============================================================================== + +do_deploy() { + local mode=${1:-docker} + + log_info "开始完整部署 (模式: $mode)..." + + check_environment + + # 构建 + do_build all + + # 如果是 Docker 模式,构建镜像 + if [ "$mode" = "docker" ]; then + build_docker_images + fi + + # 启动 + do_start all "$mode" + + log_success "部署完成!" + echo "" + echo -e "${CYAN}访问地址:${NC}" + echo " 用户前端: http://localhost" + echo " 管理后台: http://localhost/admin" + echo " API 网关: http://localhost:8000" + echo " Kong 管理: http://localhost:8001" + echo "" +} + +#=============================================================================== +# 数据库操作 +#=============================================================================== + +do_db() { + local action=${1:-status} + + case $action in + migrate) + log_step "执行数据库迁移..." + # 可以添加 TypeORM 迁移命令 + for service in user payment knowledge conversation evolution; do + local dir="${SERVICE_DIRS[$service]}" + cd "$PROJECT_ROOT/$dir" + pnpm run migration:run 2>/dev/null || log_warning "$service 无迁移或迁移失败" + cd "$PROJECT_ROOT" + done + log_success "数据库迁移完成" + ;; + seed) + log_step "初始化种子数据..." + # 添加种子数据脚本 + log_success "种子数据初始化完成" + ;; + backup) + local backup_dir="$PROJECT_ROOT/backups/$(date +%Y%m%d_%H%M%S)" + mkdir -p "$backup_dir" + + log_step "备份数据库..." + docker-compose exec -T postgres pg_dump -U postgres iconsulting > "$backup_dir/postgres.sql" + log_success "数据库备份到: $backup_dir" + ;; + restore) + local backup_file=$2 + if [ -z "$backup_file" ]; then + log_error "请指定备份文件: ./deploy.sh db restore " + exit 1 + fi + + log_step "恢复数据库..." + docker-compose exec -T postgres psql -U postgres iconsulting < "$backup_file" + log_success "数据库恢复完成" + ;; + reset) + log_warning "这将删除所有数据!" + read -p "确认继续? (y/N) " confirm + if [ "$confirm" = "y" ] || [ "$confirm" = "Y" ]; then + docker-compose down -v + docker-compose up -d postgres redis neo4j + wait_for_service localhost 5432 "PostgreSQL" + do_db migrate + log_success "数据库已重置" + fi + ;; + status) + echo -e "${PURPLE}数据库状态:${NC}" + docker-compose exec postgres psql -U postgres -c "SELECT version();" 2>/dev/null || echo "PostgreSQL 未运行" + docker-compose exec redis redis-cli ping 2>/dev/null || echo "Redis 未运行" + curl -s http://localhost:7474 > /dev/null && echo "Neo4j 运行中" || echo "Neo4j 未运行" + ;; + *) + log_error "未知数据库操作: $action (可选: migrate, seed, backup, restore, reset, status)" + exit 1 + ;; + esac +} + +#=============================================================================== +# 帮助信息 +#=============================================================================== + +show_help() { + cat << 'EOF' + +╔═══════════════════════════════════════════════════════════════════════════════╗ +║ iConsulting 部署管理脚本 ║ +╚═══════════════════════════════════════════════════════════════════════════════╝ + +用法: ./deploy.sh [target] [options] + +命令: + build [target] 编译构建 + target: all, shared, backend, frontend, + conversation, user, payment, knowledge, evolution, + web-client, admin-client + + start [target] [mode] 启动服务 + target: all, infra, kong, nginx, backend, + conversation, user, payment, knowledge, evolution, + postgres, redis, neo4j + mode: docker (默认), local + + stop [target] [mode] 停止服务 + (target 同上) + + restart [target] [mode] 重启服务 + (target 同上) + + status 查看所有服务状态 + + logs [service] [lines] 查看日志 + service: 服务名或 all + lines: 显示行数 (默认 100) + + clean [target] 清理 + target: build, deps, docker, logs, all + + deploy [mode] 完整部署 (构建 + 启动) + mode: docker (默认), local + + db 数据库操作 + action: migrate, seed, backup, restore, reset, status + + help 显示此帮助信息 + +示例: + ./deploy.sh deploy # 完整部署 + ./deploy.sh build conversation # 只构建对话服务 + ./deploy.sh start backend local # 本地模式启动所有后端 + ./deploy.sh restart user docker # 重启用户服务 (Docker) + ./deploy.sh logs conversation 200 # 查看对话服务最近200行日志 + ./deploy.sh clean all # 清理所有构建产物和依赖 + ./deploy.sh db backup # 备份数据库 + ./deploy.sh db migrate # 执行数据库迁移 + +EOF +} + +#=============================================================================== +# 主入口 +#=============================================================================== + +main() { + local command=${1:-help} + shift || true + + case $command in + build) + do_build "$@" + ;; + start) + do_start "$@" + ;; + stop) + do_stop "$@" + ;; + restart) + do_restart "$@" + ;; + status) + do_status + ;; + logs) + do_logs "$@" + ;; + clean) + do_clean "$@" + ;; + deploy) + do_deploy "$@" + ;; + db) + do_db "$@" + ;; + help|--help|-h) + show_help + ;; + *) + log_error "未知命令: $command" + show_help + exit 1 + ;; + esac +} + +# 执行主函数 +main "$@" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..8994414 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,319 @@ +#=============================================================================== +# iConsulting Docker Compose 配置 +# +# 服务架构: +# - 基础设施: PostgreSQL, Redis, Neo4j +# - API网关: Kong +# - 后端服务: conversation, user, payment, knowledge, evolution +# - 前端服务: nginx (托管 web-client 和 admin-client) +# +# 网络配置: +# - 对外网卡: 14.215.128.96 (用户访问) +# - 出口网卡: 154.84.135.121 (Claude API 调用) +# +#=============================================================================== + +version: '3.8' + +services: + #============================================================================= + # 基础设施服务 + #============================================================================= + + postgres: + image: postgres:15-alpine + container_name: iconsulting-postgres + restart: unless-stopped + environment: + POSTGRES_USER: ${POSTGRES_USER:-postgres} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres} + POSTGRES_DB: ${POSTGRES_DB:-iconsulting} + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + - ./scripts/init-db.sql:/docker-entrypoint-initdb.d/init.sql:ro + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - iconsulting-network + + redis: + image: redis:7-alpine + container_name: iconsulting-redis + restart: unless-stopped + command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD:-redis123} + ports: + - "6379:6379" + volumes: + - redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - iconsulting-network + + neo4j: + image: neo4j:5-community + container_name: iconsulting-neo4j + restart: unless-stopped + environment: + NEO4J_AUTH: ${NEO4J_USER:-neo4j}/${NEO4J_PASSWORD:-neo4j123} + NEO4J_PLUGINS: '["apoc"]' + NEO4J_dbms_memory_heap_max__size: 1G + ports: + - "7474:7474" # HTTP + - "7687:7687" # Bolt + volumes: + - neo4j_data:/data + - neo4j_logs:/logs + healthcheck: + test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:7474 || exit 1"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - iconsulting-network + + #============================================================================= + # Kong API 网关 + #============================================================================= + + kong-database: + image: postgres:15-alpine + container_name: iconsulting-kong-db + restart: unless-stopped + environment: + POSTGRES_USER: kong + POSTGRES_PASSWORD: kong + POSTGRES_DB: kong + volumes: + - kong_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U kong"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - iconsulting-network + + kong: + image: kong:3.4-alpine + container_name: iconsulting-kong + restart: unless-stopped + depends_on: + kong-database: + condition: service_healthy + environment: + KONG_DATABASE: postgres + KONG_PG_HOST: kong-database + KONG_PG_USER: kong + KONG_PG_PASSWORD: kong + KONG_PG_DATABASE: kong + KONG_PROXY_ACCESS_LOG: /dev/stdout + KONG_ADMIN_ACCESS_LOG: /dev/stdout + KONG_PROXY_ERROR_LOG: /dev/stderr + KONG_ADMIN_ERROR_LOG: /dev/stderr + KONG_ADMIN_LISTEN: 0.0.0.0:8001 + KONG_PROXY_LISTEN: 0.0.0.0:8000, 0.0.0.0:8443 ssl + ports: + - "8000:8000" # Proxy + - "8443:8443" # Proxy SSL + - "8001:8001" # Admin API + healthcheck: + test: ["CMD", "kong", "health"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - iconsulting-network + + #============================================================================= + # 后端微服务 + #============================================================================= + + user-service: + build: + context: . + dockerfile: packages/services/user-service/Dockerfile + container_name: iconsulting-user + restart: unless-stopped + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + environment: + NODE_ENV: production + PORT: 3001 + DATABASE_URL: postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@postgres:5432/${POSTGRES_DB:-iconsulting} + REDIS_URL: redis://:${REDIS_PASSWORD:-redis123}@redis:6379 + JWT_SECRET: ${JWT_SECRET:-your-jwt-secret-key} + JWT_EXPIRES_IN: ${JWT_EXPIRES_IN:-7d} + ports: + - "3001:3001" + networks: + - iconsulting-network + + payment-service: + build: + context: . + dockerfile: packages/services/payment-service/Dockerfile + container_name: iconsulting-payment + restart: unless-stopped + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + environment: + NODE_ENV: production + PORT: 3002 + DATABASE_URL: postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@postgres:5432/${POSTGRES_DB:-iconsulting} + REDIS_URL: redis://:${REDIS_PASSWORD:-redis123}@redis:6379 + ALIPAY_APP_ID: ${ALIPAY_APP_ID} + ALIPAY_PRIVATE_KEY: ${ALIPAY_PRIVATE_KEY} + WECHAT_APP_ID: ${WECHAT_APP_ID} + WECHAT_MCH_ID: ${WECHAT_MCH_ID} + WECHAT_API_KEY: ${WECHAT_API_KEY} + STRIPE_SECRET_KEY: ${STRIPE_SECRET_KEY} + ports: + - "3002:3002" + networks: + - iconsulting-network + + knowledge-service: + build: + context: . + dockerfile: packages/services/knowledge-service/Dockerfile + container_name: iconsulting-knowledge + restart: unless-stopped + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + neo4j: + condition: service_healthy + environment: + NODE_ENV: production + PORT: 3003 + DATABASE_URL: postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@postgres:5432/${POSTGRES_DB:-iconsulting} + REDIS_URL: redis://:${REDIS_PASSWORD:-redis123}@redis:6379 + NEO4J_URI: bolt://neo4j:7687 + NEO4J_USER: ${NEO4J_USER:-neo4j} + NEO4J_PASSWORD: ${NEO4J_PASSWORD:-neo4j123} + OPENAI_API_KEY: ${OPENAI_API_KEY} + ports: + - "3003:3003" + networks: + - iconsulting-network + + conversation-service: + build: + context: . + dockerfile: packages/services/conversation-service/Dockerfile + container_name: iconsulting-conversation + restart: unless-stopped + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + knowledge-service: + condition: service_started + environment: + NODE_ENV: production + PORT: 3004 + DATABASE_URL: postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@postgres:5432/${POSTGRES_DB:-iconsulting} + REDIS_URL: redis://:${REDIS_PASSWORD:-redis123}@redis:6379 + ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY} + ANTHROPIC_BASE_URL: ${ANTHROPIC_BASE_URL:-https://api.anthropic.com} + KNOWLEDGE_SERVICE_URL: http://knowledge-service:3003 + # Claude API 出口配置 (如需指定出口IP,在宿主机配置路由) + ports: + - "3004:3004" + networks: + - iconsulting-network + + evolution-service: + build: + context: . + dockerfile: packages/services/evolution-service/Dockerfile + container_name: iconsulting-evolution + restart: unless-stopped + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + environment: + NODE_ENV: production + PORT: 3005 + DATABASE_URL: postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@postgres:5432/${POSTGRES_DB:-iconsulting} + REDIS_URL: redis://:${REDIS_PASSWORD:-redis123}@redis:6379 + ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY} + ANTHROPIC_BASE_URL: ${ANTHROPIC_BASE_URL:-https://api.anthropic.com} + ports: + - "3005:3005" + networks: + - iconsulting-network + + #============================================================================= + # 前端 Nginx + #============================================================================= + + nginx: + image: nginx:alpine + container_name: iconsulting-nginx + restart: unless-stopped + depends_on: + - kong + ports: + - "80:80" + - "443:443" + volumes: + - ./packages/web-client/dist:/usr/share/nginx/html/web:ro + - ./packages/admin-client/dist:/usr/share/nginx/html/admin:ro + - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro + - ./nginx/conf.d:/etc/nginx/conf.d:ro + - ./nginx/ssl:/etc/nginx/ssl:ro + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost/health"] + interval: 10s + timeout: 5s + retries: 3 + networks: + - iconsulting-network + +#=============================================================================== +# 网络配置 +#=============================================================================== + +networks: + iconsulting-network: + driver: bridge + ipam: + config: + - subnet: 172.20.0.0/16 + +#=============================================================================== +# 数据卷 +#=============================================================================== + +volumes: + postgres_data: + driver: local + redis_data: + driver: local + neo4j_data: + driver: local + neo4j_logs: + driver: local + kong_data: + driver: local diff --git a/iconsulting部署架构.jpg b/iconsulting部署架构.jpg new file mode 100644 index 0000000..e20b5a8 Binary files /dev/null and b/iconsulting部署架构.jpg differ diff --git a/infrastructure/docker/docker-compose.dev.yml b/infrastructure/docker/docker-compose.dev.yml new file mode 100644 index 0000000..208a625 --- /dev/null +++ b/infrastructure/docker/docker-compose.dev.yml @@ -0,0 +1,134 @@ +version: '3.8' + +services: + # PostgreSQL with pgvector extension + postgres: + image: pgvector/pgvector:pg15 + container_name: iconsulting-postgres + environment: + POSTGRES_USER: iconsulting + POSTGRES_PASSWORD: dev_password_123 + POSTGRES_DB: iconsulting + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + - ./services/postgres/init.sql:/docker-entrypoint-initdb.d/init.sql + healthcheck: + test: ["CMD-SHELL", "pg_isready -U iconsulting"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - iconsulting-network + + # Neo4j Graph Database + neo4j: + image: neo4j:5 + container_name: iconsulting-neo4j + environment: + NEO4J_AUTH: neo4j/dev_password_123 + NEO4J_PLUGINS: '["apoc"]' + NEO4J_dbms_security_procedures_unrestricted: apoc.* + ports: + - "7474:7474" # HTTP + - "7687:7687" # Bolt + volumes: + - neo4j_data:/data + - neo4j_logs:/logs + healthcheck: + test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:7474 || exit 1"] + interval: 15s + timeout: 10s + retries: 5 + networks: + - iconsulting-network + + # Redis Cache + redis: + image: redis:7-alpine + container_name: iconsulting-redis + ports: + - "6379:6379" + volumes: + - redis_data:/data + command: redis-server --appendonly yes + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - iconsulting-network + + # Zookeeper for Kafka + zookeeper: + image: confluentinc/cp-zookeeper:7.4.0 + container_name: iconsulting-zookeeper + environment: + ZOOKEEPER_CLIENT_PORT: 2181 + ZOOKEEPER_TICK_TIME: 2000 + ports: + - "2181:2181" + volumes: + - zookeeper_data:/var/lib/zookeeper/data + - zookeeper_logs:/var/lib/zookeeper/log + networks: + - iconsulting-network + + # Kafka Message Broker + kafka: + image: confluentinc/cp-kafka:7.4.0 + container_name: iconsulting-kafka + depends_on: + - zookeeper + ports: + - "9092:9092" + - "29092:29092" + environment: + KAFKA_BROKER_ID: 1 + KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092 + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 + KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 + KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0 + KAFKA_AUTO_CREATE_TOPICS_ENABLE: 'true' + volumes: + - kafka_data:/var/lib/kafka/data + healthcheck: + test: ["CMD-SHELL", "kafka-broker-api-versions --bootstrap-server localhost:9092"] + interval: 30s + timeout: 10s + retries: 5 + networks: + - iconsulting-network + + # Kafka UI (optional, for debugging) + kafka-ui: + image: provectuslabs/kafka-ui:latest + container_name: iconsulting-kafka-ui + depends_on: + - kafka + ports: + - "8080:8080" + environment: + KAFKA_CLUSTERS_0_NAME: iconsulting + KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:29092 + KAFKA_CLUSTERS_0_ZOOKEEPER: zookeeper:2181 + networks: + - iconsulting-network + +networks: + iconsulting-network: + driver: bridge + +volumes: + postgres_data: + neo4j_data: + neo4j_logs: + redis_data: + zookeeper_data: + zookeeper_logs: + kafka_data: diff --git a/infrastructure/docker/services/postgres/init.sql b/infrastructure/docker/services/postgres/init.sql new file mode 100644 index 0000000..c5c4ce1 --- /dev/null +++ b/infrastructure/docker/services/postgres/init.sql @@ -0,0 +1,1450 @@ +-- =========================================== +-- iConsulting 数据库初始化脚本 +-- 香港移民在线咨询系统 +-- =========================================== + +-- 启用必要的扩展 +CREATE EXTENSION IF NOT EXISTS vector; -- pgvector: 向量存储和相似度搜索 +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -- UUID生成函数 + +-- =========================================== +-- 用户表 (users) +-- 存储系统用户信息,支持匿名用户和注册用户 +-- =========================================== +CREATE TABLE users ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + -- 用户类型: ANONYMOUS(匿名访客), REGISTERED(已注册用户) + type VARCHAR(20) NOT NULL DEFAULT 'ANONYMOUS' + CHECK (type IN ('ANONYMOUS', 'REGISTERED')), + -- 浏览器指纹,用于识别匿名用户 + fingerprint VARCHAR(255), + -- 手机号码(注册用户必填) + phone VARCHAR(20), + -- 用户昵称 + nickname VARCHAR(100), + -- 头像URL + avatar VARCHAR(500), + -- 用户来源渠道: WEB, H5, WECHAT_MP, ALIPAY_MP, LINE, WHATSAPP + source_channel VARCHAR(50) DEFAULT 'WEB', + -- 用户来源标识(如推广码、活动ID等) + source_ref VARCHAR(100), + -- 用户标签(JSON数组,如["高净值", "技术人才"]) + tags JSONB DEFAULT '[]', + -- 用户偏好设置 + preferences JSONB DEFAULT '{}', + -- 备注信息(管理员可编辑) + notes TEXT, + -- 创建时间 + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + -- 更新时间 + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + -- 最后活跃时间 + last_active_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +COMMENT ON TABLE users IS '用户表 - 存储系统所有用户信息'; +COMMENT ON COLUMN users.type IS '用户类型: ANONYMOUS=匿名访客, REGISTERED=已注册用户'; +COMMENT ON COLUMN users.fingerprint IS '浏览器指纹,用于识别和关联匿名用户'; +COMMENT ON COLUMN users.source_channel IS '用户来源渠道,用于统计分析'; +COMMENT ON COLUMN users.tags IS '用户标签,JSON数组格式,用于用户分群'; + +CREATE INDEX idx_users_fingerprint ON users(fingerprint); +CREATE INDEX idx_users_phone ON users(phone); +CREATE INDEX idx_users_type ON users(type); +CREATE INDEX idx_users_source_channel ON users(source_channel); +CREATE INDEX idx_users_created_at ON users(created_at); + +-- =========================================== +-- 对话表 (conversations) +-- 存储用户与AI助手的对话会话 +-- =========================================== +CREATE TABLE conversations ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + -- 所属用户ID + user_id UUID REFERENCES users(id) ON DELETE CASCADE, + -- 对话状态: ACTIVE(进行中), ENDED(已结束), ARCHIVED(已归档) + status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE' + CHECK (status IN ('ACTIVE', 'ENDED', 'ARCHIVED')), + -- 对话标题(自动生成或用户设置) + title VARCHAR(255), + -- 对话摘要(AI生成) + summary TEXT, + -- 主要咨询的移民类别: QMAS, GEP, IANG, TTPS, CIES, TECHTAS + category VARCHAR(50), + -- 消息数量(通过触发器自动更新) + message_count INT DEFAULT 0, + -- 用户消息数量 + user_message_count INT DEFAULT 0, + -- AI消息数量 + assistant_message_count INT DEFAULT 0, + -- Token消耗统计 + total_input_tokens INT DEFAULT 0, + total_output_tokens INT DEFAULT 0, + -- 对话评分(用户反馈,1-5分) + rating SMALLINT CHECK (rating >= 1 AND rating <= 5), + -- 用户反馈内容 + feedback TEXT, + -- 是否转化为付费(用于统计转化率) + has_converted BOOLEAN DEFAULT FALSE, + -- 创建时间 + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + -- 更新时间 + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + -- 结束时间 + ended_at TIMESTAMP WITH TIME ZONE +); + +COMMENT ON TABLE conversations IS '对话表 - 存储用户与AI助手的对话会话'; +COMMENT ON COLUMN conversations.category IS '主要咨询的移民类别,用于分析用户兴趣分布'; +COMMENT ON COLUMN conversations.has_converted IS '是否产生付费转化,用于计算转化率'; +COMMENT ON COLUMN conversations.rating IS '用户对对话的评分,1-5分'; + +CREATE INDEX idx_conversations_user_id ON conversations(user_id); +CREATE INDEX idx_conversations_status ON conversations(status); +CREATE INDEX idx_conversations_category ON conversations(category); +CREATE INDEX idx_conversations_created_at ON conversations(created_at DESC); +CREATE INDEX idx_conversations_has_converted ON conversations(has_converted) WHERE has_converted = TRUE; + +-- =========================================== +-- 消息表 (messages) +-- 存储对话中的每条消息 +-- =========================================== +CREATE TABLE messages ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + -- 所属对话ID + conversation_id UUID REFERENCES conversations(id) ON DELETE CASCADE, + -- 消息角色: user(用户), assistant(AI助手), system(系统) + role VARCHAR(20) NOT NULL CHECK (role IN ('user', 'assistant', 'system')), + -- 消息类型 + type VARCHAR(30) NOT NULL DEFAULT 'TEXT' + CHECK (type IN ('TEXT', 'TOOL_CALL', 'TOOL_RESULT', 'PAYMENT_REQUEST', 'ASSESSMENT_START', 'ASSESSMENT_RESULT')), + -- 消息内容 + content TEXT NOT NULL, + -- 元数据(工具调用信息、Token使用等) + metadata JSONB, + -- Token使用量 + input_tokens INT, + output_tokens INT, + -- 创建时间 + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +COMMENT ON TABLE messages IS '消息表 - 存储对话中的每条消息'; +COMMENT ON COLUMN messages.role IS '消息角色: user=用户发送, assistant=AI回复, system=系统消息'; +COMMENT ON COLUMN messages.type IS '消息类型,用于区分普通文本、工具调用、支付请求等'; +COMMENT ON COLUMN messages.metadata IS '元数据,存储工具调用参数、结果等扩展信息'; + +CREATE INDEX idx_messages_conversation_id ON messages(conversation_id); +CREATE INDEX idx_messages_role ON messages(role); +CREATE INDEX idx_messages_created_at ON messages(created_at); + +-- =========================================== +-- 订单表 (orders) +-- 存储用户购买的服务订单 +-- =========================================== +CREATE TABLE orders ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + -- 订单号(用于展示,格式:ORD + 年月日 + 序号) + order_no VARCHAR(50) UNIQUE, + -- 所属用户ID + user_id UUID REFERENCES users(id) ON DELETE SET NULL, + -- 关联的对话ID(订单从哪个对话产生) + conversation_id UUID REFERENCES conversations(id) ON DELETE SET NULL, + -- 服务类型: ASSESSMENT(评估), CONSULTATION(咨询), DOCUMENT_REVIEW(文档审核) + service_type VARCHAR(50) NOT NULL + CHECK (service_type IN ('ASSESSMENT', 'CONSULTATION', 'DOCUMENT_REVIEW')), + -- 服务对应的移民类别 + service_category VARCHAR(50), + -- 订单金额 + amount DECIMAL(10, 2) NOT NULL, + -- 货币类型 + currency VARCHAR(10) NOT NULL DEFAULT 'CNY', + -- 订单状态 + status VARCHAR(20) NOT NULL DEFAULT 'CREATED' + CHECK (status IN ('CREATED', 'PENDING_PAYMENT', 'PAID', 'PROCESSING', 'COMPLETED', 'CANCELLED', 'REFUNDED')), + -- 支付方式: ALIPAY, WECHAT, CREDIT_CARD + payment_method VARCHAR(20), + -- 关联的支付ID + payment_id UUID, + -- 优惠券ID + coupon_id UUID, + -- 优惠金额 + discount_amount DECIMAL(10, 2) DEFAULT 0, + -- 实付金额 + paid_amount DECIMAL(10, 2), + -- 支付时间 + paid_at TIMESTAMP WITH TIME ZONE, + -- 完成时间 + completed_at TIMESTAMP WITH TIME ZONE, + -- 取消时间 + cancelled_at TIMESTAMP WITH TIME ZONE, + -- 取消原因 + cancel_reason TEXT, + -- 退款时间 + refunded_at TIMESTAMP WITH TIME ZONE, + -- 退款原因 + refund_reason TEXT, + -- 订单备注 + notes TEXT, + -- 扩展元数据(评估结果等) + metadata JSONB, + -- 创建时间 + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + -- 更新时间 + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +COMMENT ON TABLE orders IS '订单表 - 存储用户购买的服务订单'; +COMMENT ON COLUMN orders.order_no IS '订单号,用于对外展示,格式:ORD + 年月日 + 序号'; +COMMENT ON COLUMN orders.service_type IS '服务类型: ASSESSMENT=移民评估, CONSULTATION=付费咨询, DOCUMENT_REVIEW=文档审核'; +COMMENT ON COLUMN orders.metadata IS '扩展数据,如评估结果详情等'; + +CREATE INDEX idx_orders_order_no ON orders(order_no); +CREATE INDEX idx_orders_user_id ON orders(user_id); +CREATE INDEX idx_orders_status ON orders(status); +CREATE INDEX idx_orders_service_type ON orders(service_type); +CREATE INDEX idx_orders_created_at ON orders(created_at DESC); +CREATE INDEX idx_orders_paid_at ON orders(paid_at) WHERE paid_at IS NOT NULL; + +-- =========================================== +-- 支付表 (payments) +-- 存储支付交易记录 +-- =========================================== +CREATE TABLE payments ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + -- 关联的订单ID + order_id UUID REFERENCES orders(id) ON DELETE CASCADE, + -- 支付方式 + method VARCHAR(20) NOT NULL CHECK (method IN ('ALIPAY', 'WECHAT', 'CREDIT_CARD')), + -- 支付金额 + amount DECIMAL(10, 2) NOT NULL, + -- 货币类型 + currency VARCHAR(10) NOT NULL DEFAULT 'CNY', + -- 支付状态 + status VARCHAR(20) NOT NULL DEFAULT 'PENDING' + CHECK (status IN ('PENDING', 'PROCESSING', 'COMPLETED', 'FAILED', 'REFUNDED', 'CANCELLED')), + -- 第三方交易号(支付宝/微信/Stripe的交易ID) + transaction_id VARCHAR(255), + -- 支付二维码URL(支付宝/微信) + qr_code_url TEXT, + -- 支付页面URL(Stripe) + payment_url TEXT, + -- 支付过期时间 + expires_at TIMESTAMP WITH TIME ZONE NOT NULL, + -- 实际支付时间 + paid_at TIMESTAMP WITH TIME ZONE, + -- 失败原因 + failed_reason TEXT, + -- 第三方回调原始数据(用于对账和排查问题) + callback_payload JSONB, + -- 创建时间 + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + -- 更新时间 + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +COMMENT ON TABLE payments IS '支付表 - 存储支付交易记录'; +COMMENT ON COLUMN payments.transaction_id IS '第三方支付平台的交易号,用于对账'; +COMMENT ON COLUMN payments.callback_payload IS '支付回调的原始数据,用于问题排查和对账'; + +CREATE INDEX idx_payments_order_id ON payments(order_id); +CREATE INDEX idx_payments_status ON payments(status); +CREATE INDEX idx_payments_method ON payments(method); +CREATE INDEX idx_payments_transaction_id ON payments(transaction_id); +CREATE INDEX idx_payments_created_at ON payments(created_at); + +-- =========================================== +-- 分类账/财务流水表 (ledger_entries) +-- 记录所有资金流动,支持财务对账和报表 +-- =========================================== +CREATE TABLE ledger_entries ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + -- 流水号(唯一标识) + entry_no VARCHAR(50) UNIQUE NOT NULL, + -- 流水类型 + entry_type VARCHAR(30) NOT NULL + CHECK (entry_type IN ('INCOME', 'REFUND', 'COMMISSION', 'WITHDRAWAL', 'ADJUSTMENT')), + -- 关联订单ID + order_id UUID REFERENCES orders(id) ON DELETE SET NULL, + -- 关联支付ID + payment_id UUID REFERENCES payments(id) ON DELETE SET NULL, + -- 关联用户ID + user_id UUID REFERENCES users(id) ON DELETE SET NULL, + -- 金额(正数为收入,负数为支出) + amount DECIMAL(12, 2) NOT NULL, + -- 货币类型 + currency VARCHAR(10) NOT NULL DEFAULT 'CNY', + -- 账户余额(流水后的账户余额快照) + balance_after DECIMAL(12, 2), + -- 交易渠道: ALIPAY, WECHAT, STRIPE, BANK, MANUAL + channel VARCHAR(30), + -- 第三方交易号 + transaction_id VARCHAR(255), + -- 业务类型: ASSESSMENT, CONSULTATION, DOCUMENT_REVIEW + business_type VARCHAR(50), + -- 业务类别: QMAS, GEP, IANG, TTPS, CIES, TECHTAS + business_category VARCHAR(50), + -- 摘要/描述 + description TEXT, + -- 备注 + notes TEXT, + -- 状态: PENDING(待确认), CONFIRMED(已确认), CANCELLED(已取消) + status VARCHAR(20) NOT NULL DEFAULT 'CONFIRMED' + CHECK (status IN ('PENDING', 'CONFIRMED', 'CANCELLED')), + -- 确认时间 + confirmed_at TIMESTAMP WITH TIME ZONE, + -- 确认人(管理员ID) + confirmed_by UUID, + -- 会计期间(格式:YYYY-MM) + accounting_period VARCHAR(7), + -- 创建时间(流水发生时间) + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + -- 更新时间 + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +COMMENT ON TABLE ledger_entries IS '分类账/财务流水表 - 记录所有资金流动,支持财务对账和报表生成'; +COMMENT ON COLUMN ledger_entries.entry_no IS '流水号,格式:LE + 年月日 + 序号'; +COMMENT ON COLUMN ledger_entries.entry_type IS '流水类型: INCOME=收入, REFUND=退款, COMMISSION=佣金, WITHDRAWAL=提现, ADJUSTMENT=调整'; +COMMENT ON COLUMN ledger_entries.balance_after IS '该笔流水后的账户余额快照,用于对账'; +COMMENT ON COLUMN ledger_entries.accounting_period IS '会计期间,格式YYYY-MM,用于月度报表'; + +CREATE INDEX idx_ledger_entries_entry_no ON ledger_entries(entry_no); +CREATE INDEX idx_ledger_entries_entry_type ON ledger_entries(entry_type); +CREATE INDEX idx_ledger_entries_order_id ON ledger_entries(order_id); +CREATE INDEX idx_ledger_entries_user_id ON ledger_entries(user_id); +CREATE INDEX idx_ledger_entries_status ON ledger_entries(status); +CREATE INDEX idx_ledger_entries_accounting_period ON ledger_entries(accounting_period); +CREATE INDEX idx_ledger_entries_created_at ON ledger_entries(created_at DESC); +CREATE INDEX idx_ledger_entries_business_type ON ledger_entries(business_type); + +-- =========================================== +-- 日统计表 (daily_statistics) +-- 预聚合的每日统计数据,用于快速报表查询 +-- =========================================== +CREATE TABLE daily_statistics ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + -- 统计日期 + stat_date DATE NOT NULL, + -- 统计维度: OVERALL(总体), CHANNEL(渠道), CATEGORY(移民类别) + dimension VARCHAR(30) NOT NULL DEFAULT 'OVERALL', + -- 维度值(如渠道名称、类别代码) + dimension_value VARCHAR(50), + + -- ===== 用户统计 ===== + -- 新增用户数 + new_users INT DEFAULT 0, + -- 新增注册用户数 + new_registered_users INT DEFAULT 0, + -- 活跃用户数 + active_users INT DEFAULT 0, + + -- ===== 对话统计 ===== + -- 新增对话数 + new_conversations INT DEFAULT 0, + -- 总消息数 + total_messages INT DEFAULT 0, + -- 用户消息数 + user_messages INT DEFAULT 0, + -- AI消息数 + assistant_messages INT DEFAULT 0, + -- 平均对话轮次 + avg_conversation_turns DECIMAL(10, 2) DEFAULT 0, + + -- ===== 订单统计 ===== + -- 新增订单数 + new_orders INT DEFAULT 0, + -- 支付成功订单数 + paid_orders INT DEFAULT 0, + -- 订单总金额 + total_order_amount DECIMAL(12, 2) DEFAULT 0, + -- 实收金额 + total_paid_amount DECIMAL(12, 2) DEFAULT 0, + -- 退款订单数 + refunded_orders INT DEFAULT 0, + -- 退款金额 + refund_amount DECIMAL(12, 2) DEFAULT 0, + + -- ===== 转化统计 ===== + -- 咨询转化数(对话转订单) + conversion_count INT DEFAULT 0, + -- 转化率 + conversion_rate DECIMAL(5, 4) DEFAULT 0, + + -- ===== Token消耗统计 ===== + -- 输入Token总数 + total_input_tokens BIGINT DEFAULT 0, + -- 输出Token总数 + total_output_tokens BIGINT DEFAULT 0, + -- 预估API成本(美元) + estimated_api_cost DECIMAL(10, 4) DEFAULT 0, + + -- 创建时间 + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + -- 更新时间 + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + + -- 唯一约束:同一天同一维度同一维度值只有一条记录 + UNIQUE(stat_date, dimension, dimension_value) +); + +COMMENT ON TABLE daily_statistics IS '日统计表 - 预聚合的每日统计数据,支持多维度分析'; +COMMENT ON COLUMN daily_statistics.dimension IS '统计维度: OVERALL=总体统计, CHANNEL=按渠道, CATEGORY=按移民类别'; +COMMENT ON COLUMN daily_statistics.conversion_rate IS '转化率 = 支付订单数 / 新对话数'; +COMMENT ON COLUMN daily_statistics.estimated_api_cost IS '预估Claude API成本,基于Token消耗计算'; + +CREATE INDEX idx_daily_statistics_stat_date ON daily_statistics(stat_date DESC); +CREATE INDEX idx_daily_statistics_dimension ON daily_statistics(dimension, dimension_value); + +-- =========================================== +-- 月度财务报表 (monthly_financial_reports) +-- 月度汇总的财务数据 +-- =========================================== +CREATE TABLE monthly_financial_reports ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + -- 报表月份(格式:YYYY-MM) + report_month VARCHAR(7) NOT NULL UNIQUE, + + -- ===== 收入统计 ===== + -- 总收入 + total_revenue DECIMAL(12, 2) DEFAULT 0, + -- 评估服务收入 + assessment_revenue DECIMAL(12, 2) DEFAULT 0, + -- 咨询服务收入 + consultation_revenue DECIMAL(12, 2) DEFAULT 0, + -- 其他收入 + other_revenue DECIMAL(12, 2) DEFAULT 0, + + -- ===== 退款统计 ===== + -- 总退款 + total_refunds DECIMAL(12, 2) DEFAULT 0, + -- 净收入(总收入 - 退款) + net_revenue DECIMAL(12, 2) DEFAULT 0, + + -- ===== 成本统计 ===== + -- API成本(Claude) + api_cost DECIMAL(10, 2) DEFAULT 0, + -- 支付手续费 + payment_fees DECIMAL(10, 2) DEFAULT 0, + -- 其他成本 + other_costs DECIMAL(10, 2) DEFAULT 0, + -- 总成本 + total_costs DECIMAL(10, 2) DEFAULT 0, + + -- ===== 利润统计 ===== + -- 毛利润 + gross_profit DECIMAL(12, 2) DEFAULT 0, + -- 毛利率 + gross_margin DECIMAL(5, 4) DEFAULT 0, + + -- ===== 订单统计 ===== + -- 总订单数 + total_orders INT DEFAULT 0, + -- 成功订单数 + successful_orders INT DEFAULT 0, + -- 平均订单金额 + avg_order_amount DECIMAL(10, 2) DEFAULT 0, + + -- ===== 按类别收入明细(JSONB) ===== + revenue_by_category JSONB DEFAULT '{}', + + -- ===== 按渠道收入明细(JSONB) ===== + revenue_by_channel JSONB DEFAULT '{}', + + -- 报表状态: DRAFT(草稿), CONFIRMED(已确认), LOCKED(已锁定) + status VARCHAR(20) NOT NULL DEFAULT 'DRAFT' + CHECK (status IN ('DRAFT', 'CONFIRMED', 'LOCKED')), + -- 确认人 + confirmed_by UUID, + -- 确认时间 + confirmed_at TIMESTAMP WITH TIME ZONE, + -- 备注 + notes TEXT, + + -- 创建时间 + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + -- 更新时间 + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +COMMENT ON TABLE monthly_financial_reports IS '月度财务报表 - 月度汇总的财务数据,支持确认和锁定'; +COMMENT ON COLUMN monthly_financial_reports.report_month IS '报表月份,格式YYYY-MM'; +COMMENT ON COLUMN monthly_financial_reports.revenue_by_category IS '按移民类别的收入明细,JSON格式'; +COMMENT ON COLUMN monthly_financial_reports.status IS 'DRAFT=草稿可修改, CONFIRMED=已确认, LOCKED=已锁定不可修改'; + +CREATE INDEX idx_monthly_financial_reports_month ON monthly_financial_reports(report_month DESC); +CREATE INDEX idx_monthly_financial_reports_status ON monthly_financial_reports(status); + +-- =========================================== +-- 审计日志表 (audit_logs) +-- 记录所有重要操作,用于安全审计和问题追踪 +-- =========================================== +CREATE TABLE audit_logs ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + -- 操作者ID(用户或管理员) + actor_id UUID, + -- 操作者类型: USER, ADMIN, SYSTEM + actor_type VARCHAR(20) NOT NULL CHECK (actor_type IN ('USER', 'ADMIN', 'SYSTEM')), + -- 操作者名称/标识 + actor_name VARCHAR(100), + -- 操作类型 + action VARCHAR(50) NOT NULL, + -- 操作对象类型(表名) + entity_type VARCHAR(50) NOT NULL, + -- 操作对象ID + entity_id UUID, + -- 操作前的数据快照 + old_values JSONB, + -- 操作后的数据快照 + new_values JSONB, + -- 变更的字段列表 + changed_fields TEXT[], + -- 操作描述 + description TEXT, + -- 客户端IP地址 + ip_address INET, + -- 用户代理(浏览器信息) + user_agent TEXT, + -- 请求ID(用于追踪) + request_id VARCHAR(100), + -- 操作结果: SUCCESS, FAILED + result VARCHAR(20) DEFAULT 'SUCCESS' CHECK (result IN ('SUCCESS', 'FAILED')), + -- 错误信息(如果失败) + error_message TEXT, + -- 创建时间 + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +COMMENT ON TABLE audit_logs IS '审计日志表 - 记录所有重要操作,用于安全审计'; +COMMENT ON COLUMN audit_logs.action IS '操作类型,如: CREATE, UPDATE, DELETE, LOGIN, LOGOUT, PAYMENT等'; +COMMENT ON COLUMN audit_logs.old_values IS '操作前的数据快照,用于审计和回滚'; +COMMENT ON COLUMN audit_logs.new_values IS '操作后的数据快照'; + +CREATE INDEX idx_audit_logs_actor_id ON audit_logs(actor_id); +CREATE INDEX idx_audit_logs_actor_type ON audit_logs(actor_type); +CREATE INDEX idx_audit_logs_action ON audit_logs(action); +CREATE INDEX idx_audit_logs_entity_type ON audit_logs(entity_type); +CREATE INDEX idx_audit_logs_entity_id ON audit_logs(entity_id); +CREATE INDEX idx_audit_logs_created_at ON audit_logs(created_at DESC); +-- 按时间范围查询优化 +CREATE INDEX idx_audit_logs_created_at_brin ON audit_logs USING BRIN(created_at); + +-- =========================================== +-- 发件箱表 (outbox) +-- 事务发件箱模式,确保消息可靠发送到Kafka +-- =========================================== +CREATE TABLE outbox ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + -- 聚合根类型(如:Order, Payment, User) + aggregate_type VARCHAR(100) NOT NULL, + -- 聚合根ID + aggregate_id UUID NOT NULL, + -- 事件类型(如:OrderCreated, PaymentCompleted) + event_type VARCHAR(100) NOT NULL, + -- 事件负载(JSON格式的事件数据) + payload JSONB NOT NULL, + -- 创建时间 + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + -- 处理时间(消息发送到Kafka的时间) + processed_at TIMESTAMP WITH TIME ZONE, + -- 处理次数(用于重试机制) + retry_count INT DEFAULT 0, + -- 最后错误信息 + last_error TEXT +); + +COMMENT ON TABLE outbox IS '发件箱表 - 事务发件箱模式,确保事件消息可靠发送'; +COMMENT ON COLUMN outbox.aggregate_type IS '聚合根类型,对应领域模型'; +COMMENT ON COLUMN outbox.event_type IS '领域事件类型'; + +CREATE INDEX idx_outbox_unprocessed ON outbox(created_at) WHERE processed_at IS NULL; +CREATE INDEX idx_outbox_aggregate ON outbox(aggregate_type, aggregate_id); + +-- =========================================== +-- 知识文档表 (documents) +-- 存储移民知识库文档,用于RAG检索 +-- =========================================== +CREATE TABLE documents ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + -- 文档标题 + title VARCHAR(255) NOT NULL, + -- 文档内容(原始文本) + content TEXT NOT NULL, + -- 移民类别: QMAS, GEP, IANG, TTPS, CIES, TECHTAS, GENERAL + category VARCHAR(50), + -- 标签数组 + tags TEXT[], + -- 来源名称 + source VARCHAR(255), + -- 来源URL + source_url VARCHAR(500), + -- 文档元数据 + metadata JSONB, + -- 文档版本 + version INT DEFAULT 1, + -- 是否启用 + is_active BOOLEAN DEFAULT TRUE, + -- 最后验证时间(确认信息仍然有效) + last_verified_at TIMESTAMP WITH TIME ZONE, + -- 验证人 + verified_by UUID, + -- 创建人 + created_by UUID, + -- 创建时间 + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + -- 更新时间 + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +COMMENT ON TABLE documents IS '知识文档表 - 存储移民知识库文档,用于RAG检索增强'; +COMMENT ON COLUMN documents.category IS '移民类别,GENERAL表示通用知识'; +COMMENT ON COLUMN documents.last_verified_at IS '最后验证时间,确保信息的时效性'; + +CREATE INDEX idx_documents_category ON documents(category); +CREATE INDEX idx_documents_is_active ON documents(is_active); +CREATE INDEX idx_documents_tags ON documents USING GIN(tags); +CREATE INDEX idx_documents_created_at ON documents(created_at); + +-- =========================================== +-- 文档向量嵌入表 (document_embeddings) +-- 存储文档分块的向量嵌入,用于相似度搜索 +-- =========================================== +CREATE TABLE document_embeddings ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + -- 所属文档ID + document_id UUID REFERENCES documents(id) ON DELETE CASCADE, + -- 分块序号 + chunk_index INT NOT NULL, + -- 分块内容 + content TEXT NOT NULL, + -- 向量嵌入(1536维,对应text-embedding-3-small) + embedding vector(1536), + -- 分块元数据 + metadata JSONB, + -- 创建时间 + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +COMMENT ON TABLE document_embeddings IS '文档向量嵌入表 - 存储文档分块的向量,用于语义搜索'; +COMMENT ON COLUMN document_embeddings.embedding IS '1536维向量,使用text-embedding-3-small模型生成'; + +CREATE INDEX idx_document_embeddings_document_id ON document_embeddings(document_id); +-- IVFFlat索引,用于快速向量相似度搜索 +CREATE INDEX idx_document_embeddings_embedding ON document_embeddings + USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100); + +-- =========================================== +-- 系统配置表 (system_configs) +-- 存储系统配置项,支持动态调整 +-- =========================================== +CREATE TABLE system_configs ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + -- 配置键(唯一) + key VARCHAR(100) UNIQUE NOT NULL, + -- 配置值(JSON格式) + value JSONB NOT NULL, + -- 配置分组: SYSTEM, PROMPT, PAYMENT, NOTIFICATION, FEATURE + config_group VARCHAR(50) DEFAULT 'SYSTEM', + -- 配置描述 + description TEXT, + -- 是否敏感信息(敏感信息不在日志中显示完整值) + is_sensitive BOOLEAN DEFAULT FALSE, + -- 更新人 + updated_by UUID, + -- 创建时间 + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + -- 更新时间 + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +COMMENT ON TABLE system_configs IS '系统配置表 - 存储可动态调整的系统配置'; +COMMENT ON COLUMN system_configs.config_group IS '配置分组,便于管理和查询'; +COMMENT ON COLUMN system_configs.is_sensitive IS '敏感配置在日志中脱敏显示'; + +CREATE INDEX idx_system_configs_group ON system_configs(config_group); + +-- =========================================== +-- 管理员用户表 (admin_users) +-- 存储后台管理员信息 +-- =========================================== +CREATE TABLE admin_users ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + -- 用户名(唯一) + username VARCHAR(100) UNIQUE NOT NULL, + -- 邮箱 + email VARCHAR(255), + -- 密码哈希(bcrypt) + password_hash VARCHAR(255) NOT NULL, + -- 角色: SUPER_ADMIN, ADMIN, OPERATOR, VIEWER + role VARCHAR(50) NOT NULL CHECK (role IN ('SUPER_ADMIN', 'ADMIN', 'OPERATOR', 'VIEWER')), + -- 姓名 + display_name VARCHAR(100), + -- 手机号 + phone VARCHAR(20), + -- 是否启用 + is_active BOOLEAN DEFAULT TRUE, + -- 最后登录时间 + last_login_at TIMESTAMP WITH TIME ZONE, + -- 最后登录IP + last_login_ip INET, + -- 登录失败次数(用于账户锁定) + failed_login_attempts INT DEFAULT 0, + -- 账户锁定时间 + locked_until TIMESTAMP WITH TIME ZONE, + -- 备注 + notes TEXT, + -- 创建时间 + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + -- 更新时间 + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +COMMENT ON TABLE admin_users IS '管理员用户表 - 存储后台管理员信息'; +COMMENT ON COLUMN admin_users.role IS 'SUPER_ADMIN=超级管理员, ADMIN=管理员, OPERATOR=操作员, VIEWER=只读用户'; +COMMENT ON COLUMN admin_users.failed_login_attempts IS '连续登录失败次数,超过阈值锁定账户'; + +CREATE INDEX idx_admin_users_username ON admin_users(username); +CREATE INDEX idx_admin_users_email ON admin_users(email); +CREATE INDEX idx_admin_users_role ON admin_users(role); +CREATE INDEX idx_admin_users_is_active ON admin_users(is_active); + +-- =========================================== +-- 经验库表 (experiences) +-- 存储从对话中提取的经验,用于系统自我进化 +-- =========================================== +CREATE TABLE experiences ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + -- 经验类型 + type VARCHAR(50) NOT NULL + CHECK (type IN ('FAQ_PATTERN', 'USER_CONCERN', 'EFFECTIVE_RESPONSE', 'CONVERSION_PATTERN', 'CONFUSION_POINT')), + -- 模式描述 + pattern TEXT NOT NULL, + -- 洞察/经验总结 + insight TEXT NOT NULL, + -- 出现频率 + frequency INT DEFAULT 1, + -- 置信度(0-1) + confidence DECIMAL(3, 2) DEFAULT 0.5 CHECK (confidence >= 0 AND confidence <= 1), + -- 相关移民类别 + category VARCHAR(50), + -- 来源对话ID列表 + source_conversation_ids UUID[], + -- 示例问题 + sample_questions TEXT[], + -- 示例回答 + sample_responses TEXT[], + -- 元数据 + metadata JSONB, + -- 是否已应用 + is_applied BOOLEAN DEFAULT FALSE, + -- 应用时间 + applied_at TIMESTAMP WITH TIME ZONE, + -- 创建时间 + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + -- 更新时间 + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +COMMENT ON TABLE experiences IS '经验库表 - 存储从对话中提取的经验,支持系统自我进化'; +COMMENT ON COLUMN experiences.type IS '经验类型: FAQ_PATTERN=常见问题模式, USER_CONCERN=用户关注点, EFFECTIVE_RESPONSE=有效回答, CONVERSION_PATTERN=转化模式, CONFUSION_POINT=困惑点'; +COMMENT ON COLUMN experiences.confidence IS '置信度,值越高表示该经验越可靠'; + +CREATE INDEX idx_experiences_type ON experiences(type); +CREATE INDEX idx_experiences_category ON experiences(category); +CREATE INDEX idx_experiences_confidence ON experiences(confidence DESC); +CREATE INDEX idx_experiences_is_applied ON experiences(is_applied); + +-- =========================================== +-- 进化日志表 (evolution_logs) +-- 记录系统配置和行为的变更历史 +-- =========================================== +CREATE TABLE evolution_logs ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + -- 触发人(管理员ID) + triggered_by UUID REFERENCES admin_users(id), + -- 变更类型 + type VARCHAR(50) NOT NULL + CHECK (type IN ('PROMPT_UPDATE', 'KNOWLEDGE_UPDATE', 'RULE_UPDATE', 'BEHAVIOR_UPDATE')), + -- 变更状态 + status VARCHAR(20) NOT NULL DEFAULT 'PROPOSED' + CHECK (status IN ('PROPOSED', 'APPROVED', 'APPLIED', 'ROLLED_BACK', 'REJECTED')), + -- 变更内容 + changes JSONB NOT NULL, + -- 变更原因/说明 + reason TEXT, + -- 关联的经验ID列表 + related_experience_ids UUID[], + -- 审批人 + approved_by UUID REFERENCES admin_users(id), + -- 审批时间 + approved_at TIMESTAMP WITH TIME ZONE, + -- 应用时间 + applied_at TIMESTAMP WITH TIME ZONE, + -- 回滚时间 + rollback_at TIMESTAMP WITH TIME ZONE, + -- 回滚原因 + rollback_reason TEXT, + -- 创建时间 + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +COMMENT ON TABLE evolution_logs IS '进化日志表 - 记录系统配置和行为的变更历史'; +COMMENT ON COLUMN evolution_logs.type IS '变更类型: PROMPT_UPDATE=提示词更新, KNOWLEDGE_UPDATE=知识库更新, RULE_UPDATE=规则更新, BEHAVIOR_UPDATE=行为更新'; +COMMENT ON COLUMN evolution_logs.status IS '变更状态流转: PROPOSED -> APPROVED -> APPLIED, 可回滚为ROLLED_BACK'; + +CREATE INDEX idx_evolution_logs_status ON evolution_logs(status); +CREATE INDEX idx_evolution_logs_type ON evolution_logs(type); +CREATE INDEX idx_evolution_logs_triggered_by ON evolution_logs(triggered_by); +CREATE INDEX idx_evolution_logs_created_at ON evolution_logs(created_at DESC); + +-- =========================================== +-- 验证码表 (verification_codes) +-- 存储手机验证码 +-- =========================================== +CREATE TABLE verification_codes ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + -- 手机号 + phone VARCHAR(20) NOT NULL, + -- 验证码 + code VARCHAR(10) NOT NULL, + -- 过期时间 + expires_at TIMESTAMP WITH TIME ZONE NOT NULL, + -- 是否已使用 + is_used BOOLEAN DEFAULT FALSE, + -- 使用时间 + used_at TIMESTAMP WITH TIME ZONE, + -- IP地址(用于防刷) + ip_address INET, + -- 创建时间 + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +COMMENT ON TABLE verification_codes IS '验证码表 - 存储手机验证码,支持防刷和过期机制'; + +CREATE INDEX idx_verification_codes_phone ON verification_codes(phone); +CREATE INDEX idx_verification_codes_expires_at ON verification_codes(expires_at); +CREATE INDEX idx_verification_codes_ip ON verification_codes(ip_address); + +-- =========================================== +-- 服务定价表 (service_pricing) +-- 存储各类服务的定价信息 +-- =========================================== +CREATE TABLE service_pricing ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + -- 服务类型 + service_type VARCHAR(50) NOT NULL + CHECK (service_type IN ('ASSESSMENT', 'CONSULTATION', 'DOCUMENT_REVIEW')), + -- 移民类别 + category VARCHAR(50), + -- 价格 + price DECIMAL(10, 2) NOT NULL, + -- 原价(用于显示折扣) + original_price DECIMAL(10, 2), + -- 货币 + currency VARCHAR(10) NOT NULL DEFAULT 'CNY', + -- 服务描述 + description TEXT, + -- 服务详情(JSON格式) + details JSONB, + -- 是否启用 + is_active BOOLEAN DEFAULT TRUE, + -- 生效开始时间 + effective_from TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + -- 生效结束时间 + effective_until TIMESTAMP WITH TIME ZONE, + -- 创建时间 + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + -- 更新时间 + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + -- 唯一约束 + UNIQUE(service_type, category) +); + +COMMENT ON TABLE service_pricing IS '服务定价表 - 存储各类服务的定价信息'; +COMMENT ON COLUMN service_pricing.original_price IS '原价,用于显示折扣效果'; +COMMENT ON COLUMN service_pricing.effective_from IS '价格生效开始时间,支持定时调价'; + +CREATE INDEX idx_service_pricing_service_type ON service_pricing(service_type); +CREATE INDEX idx_service_pricing_is_active ON service_pricing(is_active); + +-- =========================================== +-- 优惠券表 (coupons) +-- 存储优惠券信息 +-- =========================================== +CREATE TABLE coupons ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + -- 优惠券码 + code VARCHAR(50) UNIQUE NOT NULL, + -- 优惠券名称 + name VARCHAR(100) NOT NULL, + -- 优惠类型: FIXED(固定金额), PERCENTAGE(百分比) + discount_type VARCHAR(20) NOT NULL CHECK (discount_type IN ('FIXED', 'PERCENTAGE')), + -- 优惠值(固定金额或百分比) + discount_value DECIMAL(10, 2) NOT NULL, + -- 最低消费金额 + min_amount DECIMAL(10, 2) DEFAULT 0, + -- 最高优惠金额(百分比优惠时的上限) + max_discount DECIMAL(10, 2), + -- 适用服务类型(为空表示全部适用) + applicable_services TEXT[], + -- 适用移民类别(为空表示全部适用) + applicable_categories TEXT[], + -- 总发行量 + total_quantity INT, + -- 已使用数量 + used_quantity INT DEFAULT 0, + -- 每人限用次数 + per_user_limit INT DEFAULT 1, + -- 生效时间 + valid_from TIMESTAMP WITH TIME ZONE NOT NULL, + -- 失效时间 + valid_until TIMESTAMP WITH TIME ZONE NOT NULL, + -- 是否启用 + is_active BOOLEAN DEFAULT TRUE, + -- 备注 + notes TEXT, + -- 创建时间 + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + -- 更新时间 + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +COMMENT ON TABLE coupons IS '优惠券表 - 存储优惠券信息,支持固定金额和百分比折扣'; + +CREATE INDEX idx_coupons_code ON coupons(code); +CREATE INDEX idx_coupons_is_active ON coupons(is_active); +CREATE INDEX idx_coupons_valid_until ON coupons(valid_until); + +-- =========================================== +-- 用户优惠券表 (user_coupons) +-- 存储用户领取/使用的优惠券 +-- =========================================== +CREATE TABLE user_coupons ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + -- 用户ID + user_id UUID REFERENCES users(id) ON DELETE CASCADE, + -- 优惠券ID + coupon_id UUID REFERENCES coupons(id) ON DELETE CASCADE, + -- 状态: AVAILABLE(可用), USED(已使用), EXPIRED(已过期) + status VARCHAR(20) NOT NULL DEFAULT 'AVAILABLE' + CHECK (status IN ('AVAILABLE', 'USED', 'EXPIRED')), + -- 使用的订单ID + used_order_id UUID REFERENCES orders(id) ON DELETE SET NULL, + -- 使用时间 + used_at TIMESTAMP WITH TIME ZONE, + -- 领取时间 + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +COMMENT ON TABLE user_coupons IS '用户优惠券表 - 记录用户领取和使用优惠券的情况'; + +CREATE INDEX idx_user_coupons_user_id ON user_coupons(user_id); +CREATE INDEX idx_user_coupons_coupon_id ON user_coupons(coupon_id); +CREATE INDEX idx_user_coupons_status ON user_coupons(status); + +-- =========================================== +-- 插入默认数据 +-- =========================================== + +-- 默认管理员用户 (密码: admin123) +INSERT INTO admin_users (username, email, password_hash, role, display_name) +VALUES ('admin', 'admin@iconsulting.com', '$2b$10$rqKu.wB1E8z9vQzKK0FZMuXz.B7xPc5S7BUTr6G9TkVdEX5C0Q.Wy', 'SUPER_ADMIN', '系统管理员'); + +-- 默认系统配置 +INSERT INTO system_configs (key, value, config_group, description) VALUES +('system_prompt_identity', '"专业、友善、耐心的香港移民顾问"', 'PROMPT', '系统身份定位'), +('system_prompt_style', '"专业但不生硬,用简洁明了的语言解答"', 'PROMPT', '对话风格'), +('system_prompt_rules', '["只回答移民相关问题", "复杂评估建议付费服务", "不做法律承诺"]', 'PROMPT', '系统行为规则'), +('max_free_messages_per_day', '50', 'SYSTEM', '每日免费消息数限制'), +('max_conversation_context_messages', '20', 'SYSTEM', '对话上下文最大消息数'), +('assessment_price_default', '99', 'PAYMENT', '默认评估价格(元)'), +('payment_timeout_minutes', '30', 'PAYMENT', '支付超时时间(分钟)'), +('sms_rate_limit_per_hour', '5', 'SYSTEM', '每小时短信发送限制'), +('enable_anonymous_chat', 'true', 'FEATURE', '是否允许匿名用户聊天'), +('require_phone_for_payment', 'true', 'FEATURE', '支付时是否要求手机验证'); + +-- 默认服务定价 +INSERT INTO service_pricing (service_type, category, price, original_price, currency, description) VALUES +('ASSESSMENT', 'QMAS', 99.00, 199.00, 'CNY', '优才计划移民资格评估'), +('ASSESSMENT', 'GEP', 99.00, 199.00, 'CNY', '专才计划移民资格评估'), +('ASSESSMENT', 'IANG', 79.00, 149.00, 'CNY', '留学IANG移民资格评估'), +('ASSESSMENT', 'TTPS', 99.00, 199.00, 'CNY', '高才通移民资格评估'), +('ASSESSMENT', 'CIES', 199.00, 399.00, 'CNY', '投资移民资格评估'), +('ASSESSMENT', 'TECHTAS', 99.00, 199.00, 'CNY', '科技人才移民资格评估'); + +-- =========================================== +-- 创建函数和触发器 +-- =========================================== + +-- 更新 updated_at 时间戳的通用函数 +CREATE OR REPLACE FUNCTION update_updated_at_column() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$ language 'plpgsql'; + +-- 为所有需要的表添加更新时间触发器 +CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON users FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); +CREATE TRIGGER update_conversations_updated_at BEFORE UPDATE ON conversations FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); +CREATE TRIGGER update_orders_updated_at BEFORE UPDATE ON orders FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); +CREATE TRIGGER update_payments_updated_at BEFORE UPDATE ON payments FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); +CREATE TRIGGER update_documents_updated_at BEFORE UPDATE ON documents FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); +CREATE TRIGGER update_system_configs_updated_at BEFORE UPDATE ON system_configs FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); +CREATE TRIGGER update_admin_users_updated_at BEFORE UPDATE ON admin_users FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); +CREATE TRIGGER update_experiences_updated_at BEFORE UPDATE ON experiences FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); +CREATE TRIGGER update_service_pricing_updated_at BEFORE UPDATE ON service_pricing FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); +CREATE TRIGGER update_coupons_updated_at BEFORE UPDATE ON coupons FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); +CREATE TRIGGER update_ledger_entries_updated_at BEFORE UPDATE ON ledger_entries FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); +CREATE TRIGGER update_daily_statistics_updated_at BEFORE UPDATE ON daily_statistics FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); +CREATE TRIGGER update_monthly_financial_reports_updated_at BEFORE UPDATE ON monthly_financial_reports FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +-- 更新对话消息计数的函数 +CREATE OR REPLACE FUNCTION update_conversation_message_count() +RETURNS TRIGGER AS $$ +BEGIN + UPDATE conversations + SET + message_count = message_count + 1, + user_message_count = user_message_count + CASE WHEN NEW.role = 'user' THEN 1 ELSE 0 END, + assistant_message_count = assistant_message_count + CASE WHEN NEW.role = 'assistant' THEN 1 ELSE 0 END, + total_input_tokens = total_input_tokens + COALESCE(NEW.input_tokens, 0), + total_output_tokens = total_output_tokens + COALESCE(NEW.output_tokens, 0) + WHERE id = NEW.conversation_id; + RETURN NEW; +END; +$$ language 'plpgsql'; + +CREATE TRIGGER increment_conversation_message_count + AFTER INSERT ON messages + FOR EACH ROW + EXECUTE FUNCTION update_conversation_message_count(); + +-- 生成订单号的函数 +CREATE OR REPLACE FUNCTION generate_order_no() +RETURNS TRIGGER AS $$ +DECLARE + seq_val INT; +BEGIN + -- 获取当天的序号 + SELECT COALESCE(MAX(CAST(SUBSTRING(order_no FROM 12) AS INT)), 0) + 1 INTO seq_val + FROM orders + WHERE order_no LIKE 'ORD' || TO_CHAR(NOW(), 'YYYYMMDD') || '%'; + + NEW.order_no := 'ORD' || TO_CHAR(NOW(), 'YYYYMMDD') || LPAD(seq_val::TEXT, 6, '0'); + RETURN NEW; +END; +$$ language 'plpgsql'; + +CREATE TRIGGER generate_order_no_trigger + BEFORE INSERT ON orders + FOR EACH ROW + WHEN (NEW.order_no IS NULL) + EXECUTE FUNCTION generate_order_no(); + +-- 生成分类账流水号的函数 +CREATE OR REPLACE FUNCTION generate_ledger_entry_no() +RETURNS TRIGGER AS $$ +DECLARE + seq_val INT; +BEGIN + SELECT COALESCE(MAX(CAST(SUBSTRING(entry_no FROM 11) AS INT)), 0) + 1 INTO seq_val + FROM ledger_entries + WHERE entry_no LIKE 'LE' || TO_CHAR(NOW(), 'YYYYMMDD') || '%'; + + NEW.entry_no := 'LE' || TO_CHAR(NOW(), 'YYYYMMDD') || LPAD(seq_val::TEXT, 6, '0'); + NEW.accounting_period := TO_CHAR(NOW(), 'YYYY-MM'); + RETURN NEW; +END; +$$ language 'plpgsql'; + +CREATE TRIGGER generate_ledger_entry_no_trigger + BEFORE INSERT ON ledger_entries + FOR EACH ROW + WHEN (NEW.entry_no IS NULL) + EXECUTE FUNCTION generate_ledger_entry_no(); + +-- 支付成功后自动创建分类账条目的函数 +CREATE OR REPLACE FUNCTION create_ledger_entry_on_payment() +RETURNS TRIGGER AS $$ +BEGIN + -- 仅当支付状态变为COMPLETED时创建收入流水 + IF NEW.status = 'COMPLETED' AND (OLD.status IS NULL OR OLD.status != 'COMPLETED') THEN + INSERT INTO ledger_entries ( + entry_type, + order_id, + payment_id, + user_id, + amount, + currency, + channel, + transaction_id, + description, + status + ) + SELECT + 'INCOME', + NEW.order_id, + NEW.id, + o.user_id, + NEW.amount, + NEW.currency, + NEW.method, + NEW.transaction_id, + '订单支付: ' || o.order_no || ' - ' || o.service_type || COALESCE(' (' || o.service_category || ')', ''), + 'CONFIRMED' + FROM orders o + WHERE o.id = NEW.order_id; + END IF; + + -- 退款时创建退款流水 + IF NEW.status = 'REFUNDED' AND OLD.status != 'REFUNDED' THEN + INSERT INTO ledger_entries ( + entry_type, + order_id, + payment_id, + user_id, + amount, + currency, + channel, + transaction_id, + description, + status + ) + SELECT + 'REFUND', + NEW.order_id, + NEW.id, + o.user_id, + -NEW.amount, -- 退款为负数 + NEW.currency, + NEW.method, + NEW.transaction_id, + '订单退款: ' || o.order_no, + 'CONFIRMED' + FROM orders o + WHERE o.id = NEW.order_id; + END IF; + + RETURN NEW; +END; +$$ language 'plpgsql'; + +CREATE TRIGGER create_ledger_entry_on_payment_trigger + AFTER UPDATE ON payments + FOR EACH ROW + EXECUTE FUNCTION create_ledger_entry_on_payment(); + +-- =========================================== +-- 创建统计视图 +-- =========================================== + +-- 今日实时统计视图 +CREATE OR REPLACE VIEW v_today_statistics AS +SELECT + COUNT(DISTINCT CASE WHEN u.created_at >= CURRENT_DATE THEN u.id END) AS new_users_today, + COUNT(DISTINCT CASE WHEN u.created_at >= CURRENT_DATE AND u.type = 'REGISTERED' THEN u.id END) AS new_registered_today, + COUNT(DISTINCT CASE WHEN c.created_at >= CURRENT_DATE THEN c.id END) AS new_conversations_today, + COUNT(CASE WHEN m.created_at >= CURRENT_DATE THEN 1 END) AS messages_today, + COUNT(DISTINCT CASE WHEN o.created_at >= CURRENT_DATE THEN o.id END) AS new_orders_today, + COUNT(DISTINCT CASE WHEN o.paid_at >= CURRENT_DATE THEN o.id END) AS paid_orders_today, + COALESCE(SUM(CASE WHEN o.paid_at >= CURRENT_DATE THEN o.paid_amount END), 0) AS revenue_today +FROM users u +FULL OUTER JOIN conversations c ON TRUE +FULL OUTER JOIN messages m ON TRUE +FULL OUTER JOIN orders o ON TRUE; + +COMMENT ON VIEW v_today_statistics IS '今日实时统计视图 - 展示当天的关键指标'; + +-- 移民类别统计视图 +CREATE OR REPLACE VIEW v_category_statistics AS +SELECT + c.category, + COUNT(DISTINCT c.id) AS total_conversations, + COUNT(DISTINCT c.user_id) AS unique_users, + COUNT(DISTINCT CASE WHEN c.has_converted THEN c.id END) AS converted_conversations, + ROUND(COUNT(DISTINCT CASE WHEN c.has_converted THEN c.id END)::NUMERIC / + NULLIF(COUNT(DISTINCT c.id), 0) * 100, 2) AS conversion_rate, + COALESCE(SUM(o.paid_amount), 0) AS total_revenue +FROM conversations c +LEFT JOIN orders o ON c.id = o.conversation_id AND o.status = 'PAID' +WHERE c.category IS NOT NULL +GROUP BY c.category +ORDER BY total_conversations DESC; + +COMMENT ON VIEW v_category_statistics IS '移民类别统计视图 - 按类别汇总对话和转化数据'; + +-- 渠道统计视图 +CREATE OR REPLACE VIEW v_channel_statistics AS +SELECT + u.source_channel, + COUNT(DISTINCT u.id) AS total_users, + COUNT(DISTINCT CASE WHEN u.type = 'REGISTERED' THEN u.id END) AS registered_users, + COUNT(DISTINCT c.id) AS total_conversations, + COALESCE(SUM(o.paid_amount), 0) AS total_revenue, + COUNT(DISTINCT CASE WHEN o.status = 'PAID' THEN o.id END) AS paid_orders +FROM users u +LEFT JOIN conversations c ON u.id = c.user_id +LEFT JOIN orders o ON u.id = o.user_id AND o.status = 'PAID' +GROUP BY u.source_channel +ORDER BY total_users DESC; + +COMMENT ON VIEW v_channel_statistics IS '渠道统计视图 - 按来源渠道汇总用户和收入数据'; + +-- =========================================== +-- 知识文章表 (knowledge_articles) +-- 存储移民相关的知识内容,支持RAG检索 +-- =========================================== +CREATE TABLE knowledge_articles ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + -- 文章标题 + title VARCHAR(500) NOT NULL, + -- 文章内容(纯文本或Markdown) + content TEXT NOT NULL, + -- 内容摘要(用于预览) + summary TEXT, + -- 移民类别: QMAS, GEP, IANG, TTPS, CIES, TechTAS, GENERAL + category VARCHAR(50) NOT NULL, + -- 内容标签(JSON数组) + tags TEXT[] DEFAULT '{}', + -- 来源: MANUAL(手动), CRAWL(爬取), EXTRACT(对话提取), IMPORT(批量导入) + source VARCHAR(20) NOT NULL DEFAULT 'MANUAL' + CHECK (source IN ('MANUAL', 'CRAWL', 'EXTRACT', 'IMPORT')), + -- 来源URL + source_url VARCHAR(1000), + -- 内容向量(用于语义搜索) + embedding VECTOR(1536), + -- 是否已发布 + is_published BOOLEAN DEFAULT FALSE, + -- 引用次数(被对话引用) + citation_count INT DEFAULT 0, + -- 点赞数 + helpful_count INT DEFAULT 0, + -- 点踩数 + unhelpful_count INT DEFAULT 0, + -- 质量评分 0-100 + quality_score INT DEFAULT 50 CHECK (quality_score >= 0 AND quality_score <= 100), + -- 创建者ID + created_by UUID, + -- 更新者ID + updated_by UUID, + -- 创建时间 + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + -- 更新时间 + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +COMMENT ON TABLE knowledge_articles IS '知识文章表 - 存储移民相关知识,支持RAG语义检索'; +COMMENT ON COLUMN knowledge_articles.category IS '移民类别,用于分类筛选'; +COMMENT ON COLUMN knowledge_articles.source IS '内容来源:MANUAL手动添加,CRAWL网页爬取,EXTRACT对话提取,IMPORT批量导入'; +COMMENT ON COLUMN knowledge_articles.embedding IS '文章向量,1536维,用于pgvector语义搜索'; +COMMENT ON COLUMN knowledge_articles.quality_score IS '质量评分,根据引用和反馈自动计算'; + +CREATE INDEX idx_knowledge_articles_category ON knowledge_articles(category); +CREATE INDEX idx_knowledge_articles_published ON knowledge_articles(is_published); +CREATE INDEX idx_knowledge_articles_quality ON knowledge_articles(quality_score DESC); +CREATE INDEX idx_knowledge_articles_embedding ON knowledge_articles USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100); + +-- =========================================== +-- 知识块表 (knowledge_chunks) +-- 将文章拆分为更小的检索单元,提高RAG精确度 +-- =========================================== +CREATE TABLE knowledge_chunks ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + -- 所属文章ID + article_id UUID NOT NULL REFERENCES knowledge_articles(id) ON DELETE CASCADE, + -- 块内容 + content TEXT NOT NULL, + -- 块序号(在文章中的位置) + chunk_index INT NOT NULL, + -- 块类型: TITLE(标题), PARAGRAPH(段落), LIST(列表), TABLE(表格), CODE(代码), FAQ(问答) + chunk_type VARCHAR(20) NOT NULL DEFAULT 'PARAGRAPH' + CHECK (chunk_type IN ('TITLE', 'PARAGRAPH', 'LIST', 'TABLE', 'CODE', 'FAQ')), + -- 内容向量 + embedding VECTOR(1536), + -- 元数据(如章节标题、前后块链接等) + metadata JSONB DEFAULT '{}', + -- Token数量(估算) + token_count INT DEFAULT 0, + -- 创建时间 + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +COMMENT ON TABLE knowledge_chunks IS '知识块表 - 文章分块,用于精确RAG检索'; +COMMENT ON COLUMN knowledge_chunks.chunk_type IS '块类型,用于理解内容结构'; +COMMENT ON COLUMN knowledge_chunks.metadata IS '元数据,包含章节标题、前后块链接等'; + +CREATE INDEX idx_knowledge_chunks_article ON knowledge_chunks(article_id); +CREATE INDEX idx_knowledge_chunks_embedding ON knowledge_chunks USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100); + +-- =========================================== +-- 用户记忆表 (user_memories) +-- 存储用户的长期记忆,用于个性化对话 +-- =========================================== +CREATE TABLE user_memories ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + -- 用户ID + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + -- 记忆类型 + memory_type VARCHAR(30) NOT NULL + CHECK (memory_type IN ( + 'PERSONAL_INFO', -- 个人信息 + 'WORK_EXPERIENCE', -- 工作经历 + 'EDUCATION', -- 教育背景 + 'LANGUAGE', -- 语言能力 + 'IMMIGRATION_INTENT', -- 移民意向 + 'PREFERRED_CATEGORY', -- 倾向类别 + 'ASSESSMENT_RESULT', -- 评估结果 + 'QUESTION_ASKED', -- 问过的问题 + 'CONCERN', -- 关注点 + 'PREFERENCE', -- 偏好设置 + 'CUSTOM' -- 自定义 + )), + -- 记忆内容 + content TEXT NOT NULL, + -- 重要性 0-100 + importance INT DEFAULT 50 CHECK (importance >= 0 AND importance <= 100), + -- 来源对话ID + source_conversation_id UUID, + -- 相关移民类别 + related_category VARCHAR(50), + -- 内容向量 + embedding VECTOR(1536), + -- 访问次数 + access_count INT DEFAULT 0, + -- 最后访问时间 + last_accessed_at TIMESTAMP WITH TIME ZONE, + -- 是否已过期 + is_expired BOOLEAN DEFAULT FALSE, + -- 创建时间 + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + -- 更新时间 + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +COMMENT ON TABLE user_memories IS '用户记忆表 - 存储用户长期记忆,支持个性化对话'; +COMMENT ON COLUMN user_memories.memory_type IS '记忆类型,用于分类管理用户信息'; +COMMENT ON COLUMN user_memories.importance IS '重要性评分,影响检索优先级'; +COMMENT ON COLUMN user_memories.is_expired IS '是否过期,用户情况变化时标记'; + +CREATE INDEX idx_user_memories_user ON user_memories(user_id); +CREATE INDEX idx_user_memories_type ON user_memories(memory_type); +CREATE INDEX idx_user_memories_importance ON user_memories(user_id, importance DESC); +CREATE INDEX idx_user_memories_embedding ON user_memories USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100); + +-- =========================================== +-- 系统经验表 (system_experiences) +-- 存储系统从对话中学习到的经验,用于自我进化 +-- =========================================== +CREATE TABLE system_experiences ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + -- 经验类型 + experience_type VARCHAR(30) NOT NULL + CHECK (experience_type IN ( + 'COMMON_QUESTION', -- 常见问题 + 'ANSWER_TEMPLATE', -- 回答模板 + 'CLARIFICATION', -- 澄清方式 + 'USER_PATTERN', -- 用户行为模式 + 'CONVERSION_TRIGGER', -- 转化触发点 + 'KNOWLEDGE_GAP', -- 知识缺口 + 'KNOWLEDGE_UPDATE', -- 知识更新 + 'CONVERSATION_SKILL', -- 对话技巧 + 'OBJECTION_HANDLING', -- 异议处理 + 'CUSTOM' -- 自定义 + )), + -- 经验内容 + content TEXT NOT NULL, + -- 置信度 0-100 + confidence INT DEFAULT 50 CHECK (confidence >= 0 AND confidence <= 100), + -- 应用场景描述 + scenario TEXT NOT NULL, + -- 相关移民类别 + related_category VARCHAR(50), + -- 来源对话ID列表 + source_conversation_ids UUID[] DEFAULT '{}', + -- 验证状态: PENDING(待验证), APPROVED(已通过), REJECTED(已拒绝), DEPRECATED(已弃用) + verification_status VARCHAR(20) NOT NULL DEFAULT 'PENDING' + CHECK (verification_status IN ('PENDING', 'APPROVED', 'REJECTED', 'DEPRECATED')), + -- 验证者ID + verified_by UUID, + -- 验证时间 + verified_at TIMESTAMP WITH TIME ZONE, + -- 使用次数 + usage_count INT DEFAULT 0, + -- 正面反馈次数 + positive_count INT DEFAULT 0, + -- 负面反馈次数 + negative_count INT DEFAULT 0, + -- 内容向量 + embedding VECTOR(1536), + -- 是否激活使用 + is_active BOOLEAN DEFAULT FALSE, + -- 创建时间 + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + -- 更新时间 + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +COMMENT ON TABLE system_experiences IS '系统经验表 - 从对话中学习的经验,支持系统自我进化'; +COMMENT ON COLUMN system_experiences.experience_type IS '经验类型,用于分类应用'; +COMMENT ON COLUMN system_experiences.confidence IS '置信度,根据来源数量和反馈计算'; +COMMENT ON COLUMN system_experiences.verification_status IS '验证状态,需管理员审核后才能激活'; +COMMENT ON COLUMN system_experiences.is_active IS '是否激活,只有通过审核才能在对话中使用'; + +CREATE INDEX idx_system_experiences_type ON system_experiences(experience_type); +CREATE INDEX idx_system_experiences_status ON system_experiences(verification_status); +CREATE INDEX idx_system_experiences_active ON system_experiences(is_active); +CREATE INDEX idx_system_experiences_confidence ON system_experiences(confidence DESC); +CREATE INDEX idx_system_experiences_embedding ON system_experiences USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100); + +-- =========================================== +-- 管理员表 (admins) +-- 管理后台用户,支持多角色权限 +-- =========================================== +CREATE TABLE admins ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + -- 用户名(登录用) + username VARCHAR(50) NOT NULL UNIQUE, + -- 密码哈希 + password_hash VARCHAR(255) NOT NULL, + -- 姓名 + name VARCHAR(100) NOT NULL, + -- 邮箱 + email VARCHAR(255), + -- 手机号 + phone VARCHAR(20), + -- 角色: SUPER_ADMIN(超级管理员), ADMIN(管理员), OPERATOR(运营), VIEWER(只读) + role VARCHAR(20) NOT NULL DEFAULT 'OPERATOR' + CHECK (role IN ('SUPER_ADMIN', 'ADMIN', 'OPERATOR', 'VIEWER')), + -- 权限列表(细粒度权限控制) + permissions JSONB DEFAULT '[]', + -- 头像URL + avatar VARCHAR(500), + -- 最后登录时间 + last_login_at TIMESTAMP WITH TIME ZONE, + -- 最后登录IP + last_login_ip VARCHAR(50), + -- 是否启用 + is_active BOOLEAN DEFAULT TRUE, + -- 创建时间 + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + -- 更新时间 + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +COMMENT ON TABLE admins IS '管理员表 - 管理后台用户,支持多角色权限'; +COMMENT ON COLUMN admins.role IS '角色:SUPER_ADMIN超管,ADMIN管理员,OPERATOR运营,VIEWER只读'; +COMMENT ON COLUMN admins.permissions IS '细粒度权限列表,JSON数组格式'; + +CREATE INDEX idx_admins_username ON admins(username); +CREATE INDEX idx_admins_role ON admins(role); +CREATE INDEX idx_admins_active ON admins(is_active); + +-- 插入默认超级管理员(密码: admin123,实际生产环境需要修改) +INSERT INTO admins (username, password_hash, name, role, permissions) VALUES +('admin', '$2b$10$rQNDjKwYXOw8FNrFcD3e0.T8KCqVJLqDQT9gQR2KPnDqPvqK8VpKi', '系统管理员', 'SUPER_ADMIN', '["*"]'); + +-- =========================================== +-- 结束 +-- =========================================== diff --git a/nginx/conf.d/default.conf b/nginx/conf.d/default.conf new file mode 100644 index 0000000..ebd9d9a --- /dev/null +++ b/nginx/conf.d/default.conf @@ -0,0 +1,130 @@ +#=============================================================================== +# iConsulting Nginx 配置 +# +# 路由规则: +# / -> web-client (用户前端) +# /admin -> admin-client (管理后台) +# /api/v1/* -> Kong API Gateway +# /ws/* -> WebSocket (conversation-service) +# +#=============================================================================== + +server { + listen 80; + server_name _; + + # 健康检查端点 + location /health { + access_log off; + return 200 'OK'; + add_header Content-Type text/plain; + } + + # 用户前端 (web-client) + location / { + root /usr/share/nginx/html/web; + index index.html; + try_files $uri $uri/ /index.html; + + # 缓存静态资源 + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + } + + # 管理后台 (admin-client) + location /admin { + alias /usr/share/nginx/html/admin; + index index.html; + try_files $uri $uri/ /admin/index.html; + + # 缓存静态资源 + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + } + + # API 请求代理到 Kong + location /api/ { + proxy_pass http://kong_upstream/; + proxy_http_version 1.1; + 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; + + # 超时设置 + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + + # 缓冲设置 + proxy_buffering on; + proxy_buffer_size 4k; + proxy_buffers 8 4k; + } + + # WebSocket 代理 + location /ws/ { + proxy_pass http://websocket_upstream/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + 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; + + # WebSocket 超时 (保持长连接) + proxy_connect_timeout 7d; + proxy_send_timeout 7d; + proxy_read_timeout 7d; + } + + # Socket.IO 专用路径 + location /socket.io/ { + proxy_pass http://websocket_upstream/socket.io/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + 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; + + proxy_connect_timeout 7d; + proxy_send_timeout 7d; + proxy_read_timeout 7d; + } + + # 禁止访问隐藏文件 + location ~ /\. { + deny all; + access_log off; + log_not_found off; + } +} + +# HTTPS 配置 (如有SSL证书,取消注释) +# server { +# listen 443 ssl http2; +# server_name _; +# +# ssl_certificate /etc/nginx/ssl/fullchain.pem; +# ssl_certificate_key /etc/nginx/ssl/privkey.pem; +# ssl_session_timeout 1d; +# ssl_session_cache shared:SSL:50m; +# ssl_session_tickets off; +# +# ssl_protocols TLSv1.2 TLSv1.3; +# ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256; +# ssl_prefer_server_ciphers off; +# +# # HSTS +# add_header Strict-Transport-Security "max-age=63072000" always; +# +# # 其他配置同上... +# include /etc/nginx/conf.d/locations.conf; +# } diff --git a/nginx/nginx.conf b/nginx/nginx.conf new file mode 100644 index 0000000..ee23ea3 --- /dev/null +++ b/nginx/nginx.conf @@ -0,0 +1,51 @@ +user nginx; +worker_processes auto; + +error_log /var/log/nginx/error.log notice; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; + use epoll; + multi_accept on; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + + keepalive_timeout 65; + + # Gzip 压缩 + gzip on; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml; + + # 请求大小限制 + client_max_body_size 50M; + + # 上游服务定义 + upstream kong_upstream { + server kong:8000; + keepalive 32; + } + + upstream websocket_upstream { + server conversation-service:3004; + keepalive 32; + } + + include /etc/nginx/conf.d/*.conf; +} diff --git a/nginx/ssl/.gitkeep b/nginx/ssl/.gitkeep new file mode 100644 index 0000000..1477133 --- /dev/null +++ b/nginx/ssl/.gitkeep @@ -0,0 +1,2 @@ +# 此目录用于存放 SSL 证书 +# 请将 fullchain.pem 和 privkey.pem 放置于此 diff --git a/package.json b/package.json new file mode 100644 index 0000000..71a6ee5 --- /dev/null +++ b/package.json @@ -0,0 +1,32 @@ +{ + "name": "iconsulting", + "version": "1.0.0", + "description": "Hong Kong Immigration Consulting System based on Claude Agent SDK", + "private": true, + "workspaces": [ + "packages/*", + "packages/services/*" + ], + "scripts": { + "dev": "turbo run dev", + "build": "turbo run build", + "lint": "turbo run lint", + "test": "turbo run test", + "clean": "turbo run clean && rm -rf node_modules", + "db:migrate": "turbo run db:migrate", + "docker:dev": "docker-compose -f infrastructure/docker/docker-compose.dev.yml up -d", + "docker:down": "docker-compose -f infrastructure/docker/docker-compose.dev.yml down" + }, + "devDependencies": { + "turbo": "^2.0.0", + "typescript": "^5.3.0", + "@types/node": "^20.10.0", + "prettier": "^3.1.0", + "eslint": "^8.55.0" + }, + "engines": { + "node": ">=18.0.0", + "pnpm": ">=8.0.0" + }, + "packageManager": "pnpm@8.15.0" +} diff --git a/packages/admin-client/index.html b/packages/admin-client/index.html new file mode 100644 index 0000000..f0b7317 --- /dev/null +++ b/packages/admin-client/index.html @@ -0,0 +1,13 @@ + + + + + + + iConsulting 管理后台 + + +
+ + + diff --git a/packages/admin-client/package.json b/packages/admin-client/package.json new file mode 100644 index 0000000..9743343 --- /dev/null +++ b/packages/admin-client/package.json @@ -0,0 +1,40 @@ +{ + "name": "@iconsulting/admin-client", + "version": "0.1.0", + "description": "iConsulting 管理后台", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0" + }, + "dependencies": { + "@tanstack/react-query": "^5.17.0", + "antd": "^5.12.8", + "@ant-design/icons": "^5.2.6", + "axios": "^1.6.5", + "dayjs": "^1.11.10", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.21.1", + "zustand": "^4.4.7", + "recharts": "^2.10.4" + }, + "devDependencies": { + "@types/react": "^18.2.47", + "@types/react-dom": "^18.2.18", + "@typescript-eslint/eslint-plugin": "^6.18.1", + "@typescript-eslint/parser": "^6.18.1", + "@vitejs/plugin-react": "^4.2.1", + "autoprefixer": "^10.4.16", + "eslint": "^8.56.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.5", + "postcss": "^8.4.33", + "tailwindcss": "^3.4.1", + "typescript": "^5.3.3", + "vite": "^5.0.11" + } +} diff --git a/packages/admin-client/postcss.config.js b/packages/admin-client/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/packages/admin-client/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/packages/admin-client/src/App.tsx b/packages/admin-client/src/App.tsx new file mode 100644 index 0000000..0b1d36a --- /dev/null +++ b/packages/admin-client/src/App.tsx @@ -0,0 +1,37 @@ +import { Routes, Route, Navigate } from 'react-router-dom'; +import { MainLayout } from './shared/components/MainLayout'; +import { ProtectedRoute } from './shared/components/ProtectedRoute'; +import { LoginPage } from './features/auth/presentation/pages/LoginPage'; +import { DashboardPage } from './features/dashboard/presentation/pages/DashboardPage'; +import { KnowledgePage } from './features/knowledge/presentation/pages/KnowledgePage'; +import { ExperiencePage } from './features/experience/presentation/pages/ExperiencePage'; + +function App() { + return ( + + {/* 登录页 */} + } /> + + {/* 需要认证的路由 */} + + + + } + > + } /> + } /> + } /> + 用户管理(开发中)} /> + 系统设置(开发中)} /> + + + {/* 未匹配路由重定向 */} + } /> + + ); +} + +export default App; diff --git a/packages/admin-client/src/features/auth/presentation/pages/LoginPage.tsx b/packages/admin-client/src/features/auth/presentation/pages/LoginPage.tsx new file mode 100644 index 0000000..be6f949 --- /dev/null +++ b/packages/admin-client/src/features/auth/presentation/pages/LoginPage.tsx @@ -0,0 +1,82 @@ +import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Form, Input, Button, Card, message } from 'antd'; +import { UserOutlined, LockOutlined } from '@ant-design/icons'; +import { useAuth } from '../../../../shared/hooks/useAuth'; + +interface LoginFormValues { + username: string; + password: string; +} + +export function LoginPage() { + const [loading, setLoading] = useState(false); + const navigate = useNavigate(); + const login = useAuth((state) => state.login); + + const onFinish = async (values: LoginFormValues) => { + setLoading(true); + try { + await login(values.username, values.password); + message.success('登录成功'); + navigate('/'); + } catch (error) { + message.error('用户名或密码错误'); + } finally { + setLoading(false); + } + }; + + return ( +
+ +
+

iConsulting

+

管理后台

+
+ +
+ + } + placeholder="用户名" + /> + + + + } + placeholder="密码" + /> + + + + + +
+ +
+ 默认账号: admin / admin123 +
+
+
+ ); +} diff --git a/packages/admin-client/src/features/dashboard/presentation/pages/DashboardPage.tsx b/packages/admin-client/src/features/dashboard/presentation/pages/DashboardPage.tsx new file mode 100644 index 0000000..f2bd1db --- /dev/null +++ b/packages/admin-client/src/features/dashboard/presentation/pages/DashboardPage.tsx @@ -0,0 +1,298 @@ +import { useQuery } from '@tanstack/react-query'; +import { Card, Row, Col, Statistic, Tag, Progress, List, Typography } from 'antd'; +import { + UserOutlined, + MessageOutlined, + DollarOutlined, + RobotOutlined, + CheckCircleOutlined, + ClockCircleOutlined, +} from '@ant-design/icons'; +import { + LineChart, + Line, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, + PieChart, + Pie, + Cell, +} from 'recharts'; +import api from '../../../../shared/utils/api'; + +const { Title, Text } = Typography; + +// Mock数据 - 实际应该从API获取 +const mockTrendData = [ + { date: '01-01', conversations: 120, users: 45 }, + { date: '01-02', conversations: 150, users: 52 }, + { date: '01-03', conversations: 180, users: 68 }, + { date: '01-04', conversations: 145, users: 55 }, + { date: '01-05', conversations: 200, users: 75 }, + { date: '01-06', conversations: 230, users: 88 }, + { date: '01-07', conversations: 210, users: 82 }, +]; + +const mockCategoryData = [ + { name: 'QMAS', value: 35, color: '#1890ff' }, + { name: 'GEP', value: 25, color: '#52c41a' }, + { name: 'IANG', value: 20, color: '#faad14' }, + { name: 'TTPS', value: 10, color: '#722ed1' }, + { name: 'CIES', value: 7, color: '#eb2f96' }, + { name: 'TechTAS', value: 3, color: '#13c2c2' }, +]; + +export function DashboardPage() { + const { data: evolutionStats } = useQuery({ + queryKey: ['evolution-stats'], + queryFn: async () => { + const response = await api.get('/evolution/statistics'); + return response.data.data; + }, + }); + + const { data: healthReport } = useQuery({ + queryKey: ['system-health'], + queryFn: async () => { + const response = await api.get('/evolution/health'); + return response.data.data; + }, + }); + + const getHealthColor = (status: string) => { + switch (status) { + case 'healthy': + return 'success'; + case 'warning': + return 'warning'; + case 'critical': + return 'error'; + default: + return 'default'; + } + }; + + return ( +
+ 仪表盘 + + {/* 核心指标 */} + + + + } + valueStyle={{ color: '#1890ff' }} + /> +
+ 较昨日 +12% +
+
+ + + + } + valueStyle={{ color: '#52c41a' }} + /> +
+ 较昨日 +8% +
+
+ + + + } + suffix="元" + valueStyle={{ color: '#faad14' }} + /> +
+ 较昨日 +15% +
+
+ + + + } + valueStyle={{ color: '#722ed1' }} + /> +
+ 较昨日 -2% +
+
+ +
+ + {/* 趋势图表 */} + + + + + + + + + + + + + + + + + + + + + `${name} ${(percent * 100).toFixed(0)}%` + } + > + {mockCategoryData.map((entry, index) => ( + + ))} + + + + + + + + + {/* 系统状态 */} + + + + 系统健康 + + {healthReport?.overall === 'healthy' + ? '健康' + : healthReport?.overall === 'warning' + ? '警告' + : '异常'} + +
+ } + > + ( + +
+
+ {item.name} + + {item.value} / {item.threshold} + +
+ +
+
+ )} + /> + {healthReport?.recommendations?.length > 0 && ( +
+ 建议: +
    + {healthReport.recommendations.map((rec: string, i: number) => ( +
  • {rec}
  • + ))} +
+
+ )} + + + + + + + } + /> + + + } + valueStyle={{ color: '#52c41a' }} + /> + + + } + valueStyle={{ color: '#faad14' }} + /> + + + } + /> + + +
+ 经验类型分布 +
+ {evolutionStats?.topExperienceTypes?.map( + (item: { type: string; count: number }) => ( + + {item.type}: {item.count} + + ) + )} +
+
+
+ + + + ); +} diff --git a/packages/admin-client/src/features/experience/presentation/pages/ExperiencePage.tsx b/packages/admin-client/src/features/experience/presentation/pages/ExperiencePage.tsx new file mode 100644 index 0000000..9ec84fc --- /dev/null +++ b/packages/admin-client/src/features/experience/presentation/pages/ExperiencePage.tsx @@ -0,0 +1,393 @@ +import { useState } from 'react'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { + Card, + Table, + Button, + Select, + Tag, + Space, + Modal, + message, + Tabs, + Typography, + Statistic, + Row, + Col, +} from 'antd'; +import { + CheckOutlined, + CloseOutlined, + EyeOutlined, + PlayCircleOutlined, +} from '@ant-design/icons'; +import api from '../../../../shared/utils/api'; +import { useAuth } from '../../../../shared/hooks/useAuth'; + +const { Title, Text, Paragraph } = Typography; + +const EXPERIENCE_TYPES = [ + { value: 'COMMON_QUESTION', label: '常见问题' }, + { value: 'ANSWER_TEMPLATE', label: '回答模板' }, + { value: 'CLARIFICATION', label: '澄清方式' }, + { value: 'USER_PATTERN', label: '用户模式' }, + { value: 'CONVERSION_TRIGGER', label: '转化触发' }, + { value: 'KNOWLEDGE_GAP', label: '知识缺口' }, + { value: 'CONVERSATION_SKILL', label: '对话技巧' }, + { value: 'OBJECTION_HANDLING', label: '异议处理' }, +]; + +interface Experience { + id: string; + experienceType: string; + content: string; + scenario: string; + confidence: number; + relatedCategory: string; + sourceConversationIds: string[]; + verificationStatus: string; + usageCount: number; + positiveCount: number; + negativeCount: number; + isActive: boolean; + createdAt: string; +} + +export function ExperiencePage() { + const [activeTab, setActiveTab] = useState('pending'); + const [typeFilter, setTypeFilter] = useState(); + const [selectedExperience, setSelectedExperience] = useState(null); + const [isModalOpen, setIsModalOpen] = useState(false); + const queryClient = useQueryClient(); + const admin = useAuth((state) => state.admin); + + const { data: pendingData, isLoading: pendingLoading } = useQuery({ + queryKey: ['pending-experiences', typeFilter], + queryFn: async () => { + const params = new URLSearchParams(); + if (typeFilter) params.append('type', typeFilter); + const response = await api.get(`/memory/experience/pending?${params}`); + return response.data.data; + }, + enabled: activeTab === 'pending', + }); + + const { data: stats } = useQuery({ + queryKey: ['experience-stats'], + queryFn: async () => { + const response = await api.get('/memory/experience/statistics'); + return response.data.data; + }, + }); + + const approveMutation = useMutation({ + mutationFn: (id: string) => + api.post(`/memory/experience/${id}/approve`, { adminId: admin?.id }), + onSuccess: () => { + message.success('经验已批准'); + queryClient.invalidateQueries({ queryKey: ['pending-experiences'] }); + queryClient.invalidateQueries({ queryKey: ['experience-stats'] }); + }, + }); + + const rejectMutation = useMutation({ + mutationFn: (id: string) => + api.post(`/memory/experience/${id}/reject`, { adminId: admin?.id }), + onSuccess: () => { + message.success('经验已拒绝'); + queryClient.invalidateQueries({ queryKey: ['pending-experiences'] }); + queryClient.invalidateQueries({ queryKey: ['experience-stats'] }); + }, + }); + + const runEvolutionMutation = useMutation({ + mutationFn: () => api.post('/evolution/run', { hoursBack: 24, limit: 50 }), + onSuccess: (response) => { + const result = response.data.data; + message.success( + `进化任务完成:分析了${result.conversationsAnalyzed}个对话,提取了${result.experiencesExtracted}条经验` + ); + queryClient.invalidateQueries({ queryKey: ['pending-experiences'] }); + queryClient.invalidateQueries({ queryKey: ['experience-stats'] }); + }, + }); + + const handleView = (exp: Experience) => { + setSelectedExperience(exp); + setIsModalOpen(true); + }; + + const getTypeLabel = (type: string) => { + return EXPERIENCE_TYPES.find((t) => t.value === type)?.label || type; + }; + + const getStatusTag = (status: string) => { + const statusMap: Record = { + PENDING: { color: 'orange', label: '待审核' }, + APPROVED: { color: 'green', label: '已通过' }, + REJECTED: { color: 'red', label: '已拒绝' }, + DEPRECATED: { color: 'default', label: '已弃用' }, + }; + const s = statusMap[status] || { color: 'default', label: status }; + return {s.label}; + }; + + const columns = [ + { + title: '类型', + dataIndex: 'experienceType', + key: 'experienceType', + render: (type: string) => {getTypeLabel(type)}, + }, + { + title: '场景', + dataIndex: 'scenario', + key: 'scenario', + ellipsis: true, + }, + { + title: '内容', + dataIndex: 'content', + key: 'content', + ellipsis: true, + render: (text: string) => ( + + {text} + + ), + }, + { + title: '置信度', + dataIndex: 'confidence', + key: 'confidence', + render: (confidence: number) => ( + = 70 + ? 'text-green-600' + : confidence >= 40 + ? 'text-yellow-600' + : 'text-red-600' + } + > + {confidence}% + + ), + }, + { + title: '来源对话', + dataIndex: 'sourceConversationIds', + key: 'sources', + render: (ids: string[]) => {ids?.length || 0}个, + }, + { + title: '状态', + dataIndex: 'verificationStatus', + key: 'status', + render: getStatusTag, + }, + { + title: '操作', + key: 'action', + render: (_: unknown, record: Experience) => ( + + + + + {/* 统计卡片 */} + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ } + value={searchText} + onChange={(e) => setSearchText(e.target.value)} + style={{ width: 200 }} + /> + + + + + + + + +