Features - User Management (Phase 4.1): - Database: Add user_modules table for fine-grained module permissions - Database: Add 4 user permissions (view/create/edit/delete) to role_permissions - Backend: UserService (780 lines) - CRUD with tenant isolation - Backend: UserController + UserRoutes (648 lines) - 13 API endpoints - Backend: Batch import users from Excel - Frontend: UserListPage (412 lines) - list/filter/search/pagination - Frontend: UserFormPage (341 lines) - create/edit with module config - Frontend: UserDetailPage (393 lines) - details/tenant/module management - Frontend: 3 modal components (592 lines) - import/assign/configure - API: GET/POST/PUT/DELETE /api/admin/users/* endpoints Architecture Upgrade - Module Permission System: - Backend: Add getUserModules() method in auth.service - Backend: Login API returns modules array in user object - Frontend: AuthContext adds hasModule() method - Frontend: Navigation filters modules based on user.modules - Frontend: RouteGuard checks requiredModule instead of requiredVersion - Frontend: Remove deprecated version-based permission system - UX: Only show accessible modules in navigation (clean UI) - UX: Smart redirect after login (avoid 403 for regular users) Fixes: - Fix UTF-8 encoding corruption in ~100 docs files - Fix pageSize type conversion in userService (String to Number) - Fix authUser undefined error in TopNavigation - Fix login redirect logic with role-based access check - Update Git commit guidelines v1.2 with UTF-8 safety rules Database Changes: - CREATE TABLE user_modules (user_id, tenant_id, module_code, is_enabled) - ADD UNIQUE CONSTRAINT (user_id, tenant_id, module_code) - INSERT 4 permissions + role assignments - UPDATE PUBLIC tenant with 8 module subscriptions Technical: - Backend: 5 new files (~2400 lines) - Frontend: 10 new files (~2500 lines) - Docs: 1 development record + 2 status updates + 1 guideline update - Total: ~4900 lines of code Status: User management 100% complete, module permission system operational
42 KiB
42 KiB
模块独立部署与单机版方案
文档版本: v1.0
创建日期: 2025-11-06
最后更新: 2025-11-06
文档状态: 架构设计
作者: 技术架构师
📋 核心问题
-
如何实现每个模块独立部署?
- 模块依赖平台层和通用能力层,如何独立?
- 如何变成独立产品销售?
- 如何实现本地化部署?
-
如何开发单机版?
- 如何利用Electron框架?
- 如何复用现有代码?
- 架构如何设计?
🎯 Part 1:模块独立部署方案
核心理念:独立部署 ≠ 完全孤立
关键洞察:
独立部署的真正含义:
✅ 可以单独打包
✅ 可以单独运行
✅ 可以单独销售
✅ 可以单独升级
但不是:
❌ 完全不依赖其他代码
❌ 完全不共享基础设施
类比:
就像一个独立的手机App:
- 它依赖操作系统(Android/iOS)
- 它依赖系统API(摄像头、定位等)
- 但它可以独立下载、安装、运行、卸载
模块独立部署也是一样:
- 它依赖平台层(用户认证、存储等)
- 它依赖通用能力层(LLM网关、文档处理等)
- 但它可以独立打包、部署、销售
📦 方案一:完整打包(推荐用于独立产品)
适用场景
- ✅ 模块作为独立产品销售(如审稿系统)
- ✅ 客户要求完全本地化部署
- ✅ 不依赖其他模块或平台
核心思路
将依赖的平台层和通用能力层一起打包
审稿系统独立产品包:
┌─────────────────────────────────────────┐
│ RVW审稿系统独立产品 │
│ │
│ ┌────────────────────────────────────┐ │
│ │ 业务模块层 │ │
│ │ RVW(稿件审查) │ │
│ └────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────┐ │
│ │ 通用能力层(必需部分) │ │
│ │ - LLM网关 │ │
│ │ - 文档处理引擎 │ │
│ └────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────┐ │
│ │ 平台基础层(必需部分) │ │
│ │ - 用户认证(简化版) │ │
│ │ - 存储服务 │ │
│ │ - 监控日志 │ │
│ └────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────┐ │
│ │ 独立数据库 │ │
│ │ PostgreSQL(Docker) │ │
│ └────────────────────────────────────┘ │
└─────────────────────────────────────────┘
技术实现
Step 1: 代码组织(Monorepo)
AIclinicalresearch/
├── packages/ # Monorepo包管理
│ │
│ ├── platform-core/ # 平台核心(可复用)
│ │ ├── auth/ # 用户认证
│ │ ├── storage/ # 存储服务
│ │ ├── monitoring/ # 监控日志
│ │ └── index.ts
│ │
│ ├── capabilities-core/ # 通用能力核心(可复用)
│ │ ├── llm-gateway/ # LLM网关
│ │ ├── document-processor/ # 文档处理
│ │ ├── rag-engine/ # RAG引擎
│ │ └── index.ts
│ │
│ ├── module-aia/ # AI问答模块
│ │ ├── backend/
│ │ ├── frontend/
│ │ └── package.json
│ │
│ ├── module-asl/ # AI文献模块
│ │ ├── backend/
│ │ ├── frontend/
│ │ └── package.json
│ │
│ ├── module-review/ # 审稿系统模块
│ │ ├── backend/
│ │ ├── frontend/
│ │ └── package.json
│ │
│ └── ...
│
├── products/ # 独立产品打包
│ │
│ ├── review-system/ # 审稿系统独立产品 ⭐
│ │ ├── docker-compose.yml
│ │ ├── deploy.sh
│ │ ├── README.md
│ │ └── package.json # 依赖关系
│ │
│ ├── literature-system/ # AI文献独立产品 ⭐
│ │ └── ...
│ │
│ └── full-platform/ # 完整平台
│ └── ...
│
└── ...
Step 2: 依赖管理(package.json)
审稿系统独立产品的依赖:
// products/review-system/package.json
{
"name": "@yizhengxun/review-system",
"version": "1.0.0",
"private": true,
"dependencies": {
// 平台核心(必需)
"@yizhengxun/platform-core": "workspace:*",
// 通用能力(必需)
"@yizhengxun/capabilities-core": "workspace:*",
// 业务模块
"@yizhengxun/module-review": "workspace:*"
},
"scripts": {
"build": "node build.js",
"deploy": "sh deploy.sh"
}
}
平台核心的选择性导出:
// packages/platform-core/index.ts
// 完整导出(用于完整平台)
export * from './auth';
export * from './storage';
export * from './monitoring';
export * from './notification';
// 精简导出(用于独立产品)
export {
// 只导出必需的认证功能
authMiddleware,
jwtService
} from './auth';
export {
// 只导出必需的存储功能
uploadFile,
downloadFile
} from './storage';
export {
// 只导出必需的监控功能
logError,
logAudit
} from './monitoring';
Step 3: 打包脚本
审稿系统独立打包:
// products/review-system/build.js
const { build } = require('esbuild');
const { dependencies } = require('./package.json');
async function buildReviewSystem() {
console.log('构建审稿系统独立产品...');
// 1. 构建后端
await build({
entryPoints: ['../../packages/module-review/backend/src/index.ts'],
bundle: true,
platform: 'node',
target: 'node18',
outfile: 'dist/backend/index.js',
external: ['pg', 'fastify', 'prisma'], // 不打包这些大库
// 自动包含依赖
plugins: [
// 自动解析workspace依赖
resolveWorkspaceDependencies()
]
});
// 2. 构建前端
await build({
entryPoints: ['../../packages/module-review/frontend/src/main.tsx'],
bundle: true,
platform: 'browser',
outfile: 'dist/frontend/index.js',
loader: { '.tsx': 'tsx' }
});
// 3. 复制必要文件
copyFiles([
'docker-compose.yml',
'README.md',
'deploy.sh'
]);
console.log('构建完成!');
}
buildReviewSystem();
Step 4: Docker部署配置
审稿系统独立部署:
# products/review-system/docker-compose.yml
version: '3.8'
services:
# PostgreSQL数据库(独立)
postgres:
image: postgres:15-alpine
container_name: review-system-postgres
environment:
POSTGRES_DB: review_system
POSTGRES_USER: review
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- review-network
# 后端服务(包含平台层和通用能力层)
backend:
build:
context: ./dist/backend
container_name: review-system-backend
environment:
DATABASE_URL: postgresql://review:${DB_PASSWORD}@postgres:5432/review_system
LLM_API_KEY: ${LLM_API_KEY}
depends_on:
- postgres
networks:
- review-network
# 前端服务
frontend:
build:
context: ./dist/frontend
container_name: review-system-frontend
ports:
- "80:80"
depends_on:
- backend
networks:
- review-network
volumes:
postgres_data:
networks:
review-network:
driver: bridge
Step 5: 一键部署脚本
#!/bin/bash
# products/review-system/deploy.sh
echo "======================================"
echo " 审稿系统独立部署脚本"
echo "======================================"
# 1. 检查Docker
if ! command -v docker &> /dev/null; then
echo "错误:未安装Docker"
exit 1
fi
# 2. 设置环境变量
read -p "请输入数据库密码: " DB_PASSWORD
read -p "请输入LLM API密钥: " LLM_API_KEY
export DB_PASSWORD
export LLM_API_KEY
# 3. 启动服务
docker-compose up -d
# 4. 等待服务启动
echo "等待服务启动..."
sleep 10
# 5. 初始化数据库
docker exec review-system-backend npx prisma migrate deploy
# 6. 创建默认管理员
docker exec review-system-backend node scripts/create-admin.js
echo "======================================"
echo " 部署完成!"
echo " 访问地址:http://localhost"
echo " 管理员账号:admin@review.com"
echo " 密码:admin123(请及时修改)"
echo "======================================"
关键技术点
1. 平台层的精简和适配
完整平台的用户认证(复杂):
// 完整平台
- 多租户支持
- RBAC权限控制
- SSO单点登录
- 第三方登录(微信、企业微信等)
- 复杂的权限树
审稿系统的用户认证(简化):
// 审稿系统独立产品
- 简化的用户认证(邮箱+密码)
- 简单的角色(管理员、编辑、审稿人)
- 基于JWT的Token认证
- 无SSO、无多租户
实现方式:
// packages/platform-core/auth/simple-auth.ts
// 为独立产品提供简化版认证
export class SimpleAuthService {
// 只保留核心功能
async login(email: string, password: string) { }
async register(email: string, password: string) { }
async verifyToken(token: string) { }
async logout(userId: string) { }
// 移除复杂功能
// ❌ async loginWithWeChat()
// ❌ async setupSSO()
// ❌ async multiTenantCheck()
}
2. 通用能力层的精简和适配
完整平台的LLM网关(复杂):
// 完整平台
- 支持10+种模型
- Feature Flag控制
- 配额管理
- 成本分析
- A/B测试
审稿系统的LLM网关(简化):
// 审稿系统独立产品
- 只支持1-2种模型(DeepSeek + Claude)
- 简单的配额检查
- 无Feature Flag
- 无成本分析
实现方式:
// packages/capabilities-core/llm-gateway/simple-llm.ts
export class SimpleLLMGateway {
// 只保留核心功能
async chat(params: ChatParams) { }
async checkQuota(userId: string) { }
// 移除复杂功能
// ❌ async getCostStats()
// ❌ async selectModelByVersion()
// ❌ async runABTest()
}
3. 数据库的隔离
完整平台的数据库(复杂):
-- 平台层Schema
platform_schema.users
platform_schema.tenants
platform_schema.feature_flags
-- ...
-- 所有模块Schema
aia_schema.*
asl_schema.*
review_schema.*
-- ...
审稿系统独立产品的数据库(简化):
-- 只包含必需的表
CREATE DATABASE review_system;
-- 简化的用户表
CREATE TABLE users (
id UUID PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
role VARCHAR(50) NOT NULL, -- admin/editor/reviewer
created_at TIMESTAMP DEFAULT NOW()
);
-- 审稿系统的业务表
CREATE TABLE review_tasks (...);
CREATE TABLE review_journals (...);
CREATE TABLE review_reviewers (...);
-- ...
独立产品的优势
| 优势 | 说明 |
|---|---|
| 独立销售 | 可以单独定价、单独销售 |
| 独立部署 | 客户可以本地化部署,数据完全隔离 |
| 独立升级 | 不影响其他模块 |
| 轻量化 | 只包含必需功能,包体积小 |
| 定制化 | 可以针对特定客户定制 |
📦 方案二:共享服务(推荐用于平台内模块)
适用场景
- ✅ 同一客户购买多个模块
- ✅ 云端SaaS部署
- ✅ 模块之间需要共享用户和数据
核心思路
平台层和通用能力层作为共享服务
完整平台架构(微服务版):
┌─────────────────────────────────────────────────────────┐
│ API网关(Kong/Traefik) │
│ 路由:/aia/* /asl/* /review/* │
└─────────────────────────────────────────────────────────┘
│ │ │
↓ ↓ ↓
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ AIA模块服务 │ │ ASL模块服务 │ │ RVW模块服务 │
│ (独立部署) │ │ (独立部署) │ │ (独立部署) │
└─────────────┘ └─────────────┘ └─────────────┘
│ │ │
└──────────────┴──────────────┘
↓
┌───────────────────────────┐
│ 共享服务(平台层) │
│ - 用户认证服务 │
│ - 存储服务 │
│ - 监控服务 │
└───────────────────────────┘
↓
┌───────────────────────────┐
│ 共享服务(通用能力层) │
│ - LLM网关服务 │
│ - 文档处理服务 │
│ - RAG引擎服务 │
└───────────────────────────┘
↓
┌───────────────────────────┐
│ 共享数据库 │
│ - PostgreSQL (多Schema) │
└───────────────────────────┘
技术实现
Step 1: 服务拆分
平台基础服务(独立部署):
# services/platform/docker-compose.yml
services:
# 用户认证服务
auth-service:
image: yizhengxun/auth-service:latest
ports:
- "3001:3000"
environment:
DATABASE_URL: ${DATABASE_URL}
JWT_SECRET: ${JWT_SECRET}
# 存储服务
storage-service:
image: yizhengxun/storage-service:latest
ports:
- "3002:3000"
environment:
OSS_ENDPOINT: ${OSS_ENDPOINT}
# 监控服务
monitoring-service:
image: yizhengxun/monitoring-service:latest
ports:
- "3003:3000"
通用能力服务(独立部署):
# services/capabilities/docker-compose.yml
services:
# LLM网关服务
llm-gateway:
image: yizhengxun/llm-gateway:latest
ports:
- "3010:3000"
environment:
DEEPSEEK_API_KEY: ${DEEPSEEK_API_KEY}
QWEN_API_KEY: ${QWEN_API_KEY}
# 文档处理服务
document-processor:
image: yizhengxun/document-processor:latest
ports:
- "3011:3000"
# RAG引擎服务
rag-engine:
image: yizhengxun/rag-engine:latest
ports:
- "3012:3000"
业务模块服务(独立部署):
# services/modules/review-system/docker-compose.yml
services:
review-system:
image: yizhengxun/review-system:latest
ports:
- "4001:3000"
environment:
# 依赖共享服务
AUTH_SERVICE_URL: http://auth-service:3000
STORAGE_SERVICE_URL: http://storage-service:3000
LLM_GATEWAY_URL: http://llm-gateway:3000
DOCUMENT_PROCESSOR_URL: http://document-processor:3000
# 自己的数据库Schema
DATABASE_URL: postgresql://user:pass@postgres:5432/platform?schema=review_schema
Step 2: API网关配置
Kong API网关配置:
# api-gateway/kong.yml
_format_version: "3.0"
services:
# 用户认证服务
- name: auth-service
url: http://auth-service:3000
routes:
- name: auth-routes
paths:
- /api/auth
# 审稿系统模块
- name: review-system
url: http://review-system:3000
routes:
- name: review-routes
paths:
- /api/review
plugins:
- name: jwt
config:
secret_is_base64: false
key_claim_name: kid
# AI文献模块
- name: literature-system
url: http://literature-system:3000
routes:
- name: literature-routes
paths:
- /api/literature
plugins:
- name: jwt
Step 3: 服务间通信
业务模块调用共享服务:
// packages/module-review/backend/src/services/review.service.ts
import { AuthClient } from '@yizhengxun/platform-core/clients';
import { LLMClient } from '@yizhengxun/capabilities-core/clients';
export class ReviewService {
private authClient: AuthClient;
private llmClient: LLMClient;
constructor() {
// 连接到共享服务
this.authClient = new AuthClient({
baseURL: process.env.AUTH_SERVICE_URL
});
this.llmClient = new LLMClient({
baseURL: process.env.LLM_GATEWAY_URL
});
}
async createReviewTask(userId: string, file: File) {
// 1. 验证用户(调用共享认证服务)
const user = await this.authClient.verifyUser(userId);
// 2. 上传文件(调用共享存储服务)
const fileUrl = await this.storageClient.upload(file);
// 3. AI分析(调用共享LLM网关)
const analysis = await this.llmClient.chat({
messages: [{ role: 'user', content: `分析这篇稿件: ${fileUrl}` }]
});
// 4. 保存到自己的数据库
const task = await prisma.reviewTask.create({
data: {
userId,
fileUrl,
analysis: analysis.content
}
});
return task;
}
}
模块独立部署的步骤
1. 部署共享服务(一次性):
# 部署平台基础服务
cd services/platform
docker-compose up -d
# 部署通用能力服务
cd services/capabilities
docker-compose up -d
# 部署API网关
cd services/api-gateway
docker-compose up -d
2. 部署业务模块(按需):
# 只部署审稿系统模块
cd services/modules/review-system
docker-compose up -d
# 客户A购买了审稿系统,只需部署这一个模块
# 其他模块不需要部署
3. 新客户购买其他模块:
# 客户B购买了AI文献模块,再部署这个模块
cd services/modules/literature-system
docker-compose up -d
# 共享服务已经部署,不需要重复部署
# 只需增加新的业务模块
🖥️ Part 2:Electron单机版方案
为什么需要单机版?
核心需求:
- 数据隐私:医生个人数据不能上传云端
- 离线使用:无需网络连接
- 便携性:可以在任何电脑上安装使用
- 独立运行:不依赖外部服务器
目标用户:
- 个人医生
- 数据敏感的研究者
- 无网络环境的场景
Electron架构与现有代码的对应关系
现有架构(云端版)
┌─────────────────────────────────────────┐
│ 浏览器 (Chrome) │
│ ┌───────────────────────────────────┐ │
│ │ 前端 (React + Vite) │ │
│ │ http://localhost:5173 │ │
│ └───────────────────────────────────┘ │
└─────────────────────────────────────────┘
↓ HTTP请求
┌─────────────────────────────────────────┐
│ 后端 (Node.js + Fastify) │
│ http://localhost:3001 │
│ ┌───────────────────────────────────┐ │
│ │ API路由、业务逻辑、Prisma ORM │ │
│ └───────────────────────────────────┘ │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ PostgreSQL (Docker) │
│ localhost:5432 │
└─────────────────────────────────────────┘
Electron架构(单机版)
┌───────────────────────────────────────────────────────────┐
│ Electron 应用 │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 渲染进程 (Chromium) │ │
│ │ ┌───────────────────────────────────────────────┐ │ │
│ │ │ 前端 (React) ⭐ 复用现有代码 │ │ │
│ │ │ file://app/index.html │ │ │
│ │ └───────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ ↓ IPC通信 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 主进程 (Node.js) │ │
│ │ ┌───────────────────────────────────────────────┐ │ │
│ │ │ API逻辑、业务逻辑 ⭐ 复用现有代码 │ │ │
│ │ │ Prisma ORM、本地文件系统 │ │ │
│ │ └───────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ SQLite (嵌入式数据库) │ │
│ │ ~/Documents/YizhengxunData/app.db │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 子进程 (Python/R) │ │
│ │ 文档提取、数据分析 │ │
│ └─────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────┘
核心技术方案
1. 前端代码复用(90%+ 复用)
现有前端代码(Web版):
// frontend/src/api/client.ts
const API_BASE_URL = 'http://localhost:3001';
export const apiClient = {
async get(url: string) {
return fetch(`${API_BASE_URL}${url}`);
},
async post(url: string, data: any) {
return fetch(`${API_BASE_URL}${url}`, {
method: 'POST',
body: JSON.stringify(data)
});
}
};
Electron版前端(修改API调用方式):
// electron-frontend/src/api/client.ts
// 使用Electron的IPC通信,而不是HTTP
const { ipcRenderer } = window.require('electron');
export const apiClient = {
async get(url: string) {
// 通过IPC发送到主进程
return ipcRenderer.invoke('api-request', {
method: 'GET',
url
});
},
async post(url: string, data: any) {
return ipcRenderer.invoke('api-request', {
method: 'POST',
url,
data
});
}
};
React组件完全不需要改:
// 业务组件完全不变 ✅
function UserList() {
const [users, setUsers] = useState([]);
useEffect(() => {
// API调用方式不变,底层自动适配
apiClient.get('/api/users').then(setUsers);
}, []);
return <div>{/* ... */}</div>;
}
复用率:90%+ ✅
- ✅ 所有React组件
- ✅ 所有UI库(Ant Design)
- ✅ 所有状态管理(Zustand)
- ✅ 所有路由(React Router)
- ⚠️ 只需修改API调用层(1个文件)
2. 后端代码复用(80%+ 复用)
现有后端代码(Web版):
// backend/src/routes/users.ts
import { FastifyInstance } from 'fastify';
export async function userRoutes(fastify: FastifyInstance) {
// GET /api/users
fastify.get('/api/users', async (request, reply) => {
const users = await prisma.user.findMany();
return users;
});
// POST /api/users
fastify.post('/api/users', async (request, reply) => {
const user = await prisma.user.create({
data: request.body
});
return user;
});
}
Electron版后端(主进程):
// electron-backend/src/ipc-handlers.ts
import { ipcMain } from 'electron';
import { prisma } from './prisma-client';
// 复用业务逻辑 ⭐
import { UserService } from '../../../backend/src/services/user.service';
export function setupIpcHandlers() {
const userService = new UserService(prisma);
// 将HTTP路由转换为IPC Handler
ipcMain.handle('api-request', async (event, { method, url, data }) => {
// 路由分发(简化版的Fastify)
if (url === '/api/users' && method === 'GET') {
return userService.findAll();
}
if (url === '/api/users' && method === 'POST') {
return userService.create(data);
}
// ... 其他路由
});
}
核心业务逻辑完全复用:
// backend/src/services/user.service.ts
// 这个文件在Web版和Electron版完全共享 ✅
export class UserService {
constructor(private prisma: PrismaClient) {}
async findAll() {
return this.prisma.user.findMany();
}
async create(data: CreateUserDto) {
// 业务逻辑
const hashedPassword = await bcrypt.hash(data.password, 10);
return this.prisma.user.create({
data: {
...data,
password: hashedPassword
}
});
}
}
复用率:80%+ ✅
- ✅ 所有Service层(业务逻辑)
- ✅ 所有Prisma Model和查询
- ✅ 所有工具函数
- ⚠️ 需要适配:HTTP路由 → IPC Handler
- ⚠️ 需要替换:PostgreSQL → SQLite
3. 数据库适配(PostgreSQL → SQLite)
Prisma Schema修改(最小改动):
// electron-backend/prisma/schema.prisma
datasource db {
// Web版:PostgreSQL
// provider = "postgresql"
// url = env("DATABASE_URL")
// Electron版:SQLite ⭐
provider = "sqlite"
url = "file:./app.db"
}
// Model定义完全不变 ✅
model User {
id String @id @default(uuid())
email String @unique
password String
name String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("users")
}
注意事项:
// SQLite不支持某些PostgreSQL特性
// ❌ PostgreSQL支持,SQLite不支持:
- JSON列类型(需要用TEXT存储)
- 复杂的全文搜索
- 某些聚合函数
// ✅ 解决方案:
// 1. 用TEXT存储JSON(手动序列化)
model Config {
id String @id
data String // JSON.stringify(data)
}
// 2. 简化查询(避免复杂SQL)
// 3. 在应用层实现某些功能
完整项目结构
AIclinicalresearch/
├── packages/ # 共享代码(Monorepo)
│ ├── frontend-core/ # 前端核心(Web + Electron复用)
│ ├── backend-core/ # 后端核心(Web + Electron复用)
│ └── shared-types/ # 共享类型定义
│
├── apps/
│ ├── web/ # Web版(当前)
│ │ ├── frontend/
│ │ └── backend/
│ │
│ └── electron/ # Electron版 ⭐
│ ├── main/ # 主进程(Node.js后端)
│ │ ├── src/
│ │ │ ├── main.ts # Electron入口
│ │ │ ├── ipc-handlers.ts # IPC处理器
│ │ │ ├── services/ # 复用backend-core
│ │ │ └── prisma/
│ │ │ └── schema.prisma # SQLite版本
│ │ └── package.json
│ │
│ ├── renderer/ # 渲染进程(React前端)
│ │ ├── src/
│ │ │ ├── main.tsx
│ │ │ ├── api/ # IPC客户端
│ │ │ ├── pages/ # 复用frontend-core
│ │ │ └── components/ # 复用frontend-core
│ │ └── package.json
│ │
│ ├── resources/ # 打包资源
│ │ ├── python/ # Python运行时
│ │ ├── r/ # R运行时(如需要)
│ │ └── assets/
│ │
│ └── electron-builder.yml # 打包配置
│
└── ...
关键实现细节
1. Electron主进程(main.ts)
// apps/electron/main/src/main.ts
import { app, BrowserWindow, ipcMain } from 'electron';
import path from 'path';
import { setupIpcHandlers } from './ipc-handlers';
import { initDatabase } from './database';
import { startPythonService } from './python-service';
let mainWindow: BrowserWindow | null = null;
async function createWindow() {
// 1. 创建浏览器窗口
mainWindow = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: path.join(__dirname, 'preload.js') // 安全的IPC桥接
}
});
// 2. 加载前端(生产环境)
if (app.isPackaged) {
mainWindow.loadFile(path.join(__dirname, '../renderer/index.html'));
} else {
// 开发环境:连接到Vite开发服务器
mainWindow.loadURL('http://localhost:5173');
}
// 3. 初始化数据库
await initDatabase();
// 4. 启动Python文档处理服务
await startPythonService();
// 5. 设置IPC处理器
setupIpcHandlers();
}
// Electron生命周期
app.whenReady().then(createWindow);
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
2. IPC处理器(ipc-handlers.ts)
// apps/electron/main/src/ipc-handlers.ts
import { ipcMain } from 'electron';
import { prisma } from './prisma-client';
// 复用业务逻辑 ⭐
import { UserService } from '@yizhengxun/backend-core/services';
import { ProjectService } from '@yizhengxun/backend-core/services';
import { ConversationService } from '@yizhengxun/backend-core/services';
export function setupIpcHandlers() {
// 初始化服务
const userService = new UserService(prisma);
const projectService = new ProjectService(prisma);
const conversationService = new ConversationService(prisma);
// 用户相关API
ipcMain.handle('api:users:list', async () => {
return userService.findAll();
});
ipcMain.handle('api:users:create', async (event, data) => {
return userService.create(data);
});
// 项目相关API
ipcMain.handle('api:projects:list', async (event, userId) => {
return projectService.findByUser(userId);
});
ipcMain.handle('api:projects:create', async (event, data) => {
return projectService.create(data);
});
// 对话相关API
ipcMain.handle('api:conversations:create', async (event, data) => {
return conversationService.create(data);
});
ipcMain.handle('api:conversations:chat', async (event, data) => {
// 调用LLM(本地或云端API)
return conversationService.chat(data);
});
// 文件操作
ipcMain.handle('api:files:upload', async (event, file) => {
// 保存到本地文件系统
const filePath = await saveFile(file);
return { url: filePath };
});
}
3. 前端IPC客户端(api-client.ts)
// apps/electron/renderer/src/api/client.ts
const { ipcRenderer } = window.require('electron');
export const apiClient = {
// 用户API
users: {
async list() {
return ipcRenderer.invoke('api:users:list');
},
async create(data: any) {
return ipcRenderer.invoke('api:users:create', data);
}
},
// 项目API
projects: {
async list(userId: string) {
return ipcRenderer.invoke('api:projects:list', userId);
},
async create(data: any) {
return ipcRenderer.invoke('api:projects:create', data);
}
},
// 对话API
conversations: {
async create(data: any) {
return ipcRenderer.invoke('api:conversations:create', data);
},
async chat(data: any) {
return ipcRenderer.invoke('api:conversations:chat', data);
}
},
// 文件上传
files: {
async upload(file: File) {
// 将File转换为Buffer
const buffer = await file.arrayBuffer();
return ipcRenderer.invoke('api:files:upload', {
name: file.name,
data: Buffer.from(buffer)
});
}
}
};
4. Python子进程管理
// apps/electron/main/src/python-service.ts
import { spawn, ChildProcess } from 'child_process';
import path from 'path';
import { app } from 'electron';
let pythonProcess: ChildProcess | null = null;
export async function startPythonService(): Promise<void> {
return new Promise((resolve, reject) => {
// Python运行时路径(打包时包含)
const pythonPath = app.isPackaged
? path.join(process.resourcesPath, 'python', 'python.exe')
: 'python'; // 开发环境使用系统Python
// Python脚本路径
const scriptPath = app.isPackaged
? path.join(process.resourcesPath, 'python', 'extraction_service', 'main.py')
: path.join(__dirname, '../../../../extraction_service/main.py');
// 启动Python进程
pythonProcess = spawn(pythonPath, [
'-m', 'uvicorn',
'main:app',
'--host', '127.0.0.1',
'--port', '8000'
], {
cwd: path.dirname(scriptPath),
stdio: 'pipe'
});
// 监听输出
pythonProcess.stdout?.on('data', (data) => {
console.log(`Python: ${data}`);
if (data.toString().includes('Application startup complete')) {
resolve();
}
});
pythonProcess.stderr?.on('data', (data) => {
console.error(`Python Error: ${data}`);
});
pythonProcess.on('error', reject);
});
}
export function stopPythonService(): void {
if (pythonProcess) {
pythonProcess.kill();
pythonProcess = null;
}
}
// 应用退出时关闭Python服务
app.on('will-quit', stopPythonService);
5. 打包配置(electron-builder.yml)
# apps/electron/electron-builder.yml
appId: com.yizhengxun.aiclinical
productName: 壹证循AI科研助手
# 打包目录
directories:
output: dist
buildResources: resources
# Windows配置
win:
target:
- nsis
- portable
icon: resources/icon.ico
# NSIS安装程序配置
nsis:
oneClick: false
allowToChangeInstallationDirectory: true
createDesktopShortcut: always
createStartMenuShortcut: true
# Mac配置
mac:
target:
- dmg
- zip
icon: resources/icon.icns
category: public.app-category.productivity
# 打包包含的文件
files:
- "main/**/*"
- "renderer/**/*"
- "!**/*.ts"
- "!**/*.map"
# 额外资源(Python运行时等)
extraResources:
- from: resources/python
to: python
- from: ../../../../extraction_service
to: python/extraction_service
# 打包后大小
asar: true
asarUnpack:
- "**/*.node"
- "resources/**/*"
单机版的关键技术挑战
挑战1:打包体积(500MB+)
包含内容:
安装包组成:
- Electron框架:~100MB
- Node.js运行时:包含在Electron中
- 前端代码(编译后):~10MB
- 后端代码(编译后):~5MB
- SQLite数据库:~5MB(空库)
- Python运行时:~150MB
- Python依赖:~100MB
- R运行时(可选):~200MB
- R依赖(可选):~100MB
总计:500-700MB
优化方案:
- ✅ 使用ASAR压缩(Electron默认)
- ✅ 不包含R运行时(仅云端版需要)
- ✅ Python依赖精简(只包含必需的包)
- ✅ 增量更新(只下载变化的部分)
挑战2:跨平台兼容性
需要分别打包:
- Windows x64
- Windows ARM64(Surface等设备)
- macOS x64
- macOS ARM64(Apple Silicon)
- Linux x64
测试工作量:
- 每个平台都需要独立测试
- 安装、升级、卸载流程
- 权限问题、文件路径问题
挑战3:自动更新
electron-updater配置:
// apps/electron/main/src/updater.ts
import { autoUpdater } from 'electron-updater';
import { dialog } from 'electron';
export function setupAutoUpdater() {
// 配置更新服务器
autoUpdater.setFeedURL({
provider: 'generic',
url: 'https://releases.yizhengxun.com'
});
// 检查更新
autoUpdater.checkForUpdatesAndNotify();
// 发现新版本
autoUpdater.on('update-available', () => {
dialog.showMessageBox({
type: 'info',
title: '发现新版本',
message: '检测到新版本,是否立即下载?',
buttons: ['是', '否']
}).then((result) => {
if (result.response === 0) {
autoUpdater.downloadUpdate();
}
});
});
// 下载完成
autoUpdater.on('update-downloaded', () => {
dialog.showMessageBox({
type: 'info',
title: '更新已就绪',
message: '新版本已下载,是否立即安装?',
buttons: ['立即安装', '稍后']
}).then((result) => {
if (result.response === 0) {
autoUpdater.quitAndInstall();
}
});
});
}
📊 总结对比
三种部署方式对比
| 维度 | 云端SaaS | 独立产品包 | Electron单机版 |
|---|---|---|---|
| 部署方式 | 云端服务器 | Docker容器 | 本地安装 |
| 数据存储 | 云端PostgreSQL | 本地PostgreSQL | 本地SQLite |
| 网络需求 | 必须联网 | 可离线(LLM除外) | 完全离线 |
| 更新方式 | 无感知更新 | Docker镜像更新 | 应用内更新 |
| 安装难度 | 无需安装 | Docker部署 | 一键安装 |
| 代码复用 | 100% | 80%(精简版) | 90% |
| 适用客户 | 个人、小机构 | 医院、大机构 | 个人医生 |
| 商业模式 | 订阅制 | 一次性License | 一次性购买 |
| 维护成本 | 低 | 中 | 高 |
🎯 最终建议
分阶段实施
阶段一(当前-6个月):
- ✅ 专注云端SaaS版
- ✅ 完善7个业务模块
- ✅ 建立Monorepo架构(为未来打基础)
阶段二(6-12个月):
- ✅ 开发独立产品包(审稿系统、AI文献)
- ✅ 支持Docker本地化部署
- ✅ 验证商业模式
阶段三(12-18个月):
- ✅ 开发Electron单机版(如有需求)
- ✅ 支持完全离线使用
- ✅ 扩展个人用户市场
您想先深入哪个方案?
- 模块独立部署的详细实施?
- Electron单机版的开发计划?
- Monorepo架构的设计?