docs: 全局文档梳理归档 — 删除过期文件 + 归档 V1/早期设计 + wiki 数据校正 + CLAUDE.md 规则优化
**根目录清理:** - 删除 CLAUDE-1.md(ZCLAW 旧项目配置,HMS 已完全脱离) - 移动 DESIGN.md → docs/archive/(ERP 旧设计系统) - 删除 plans/ 98 个临时会话计划文件 **归档重组:** - V1 审计(12 文件)→ docs/archive/audits-v1/ - 早期 CRM/插件迭代设计(13 文件)→ docs/archive/superpowers-early/ - 已完成/已取代设计(28 文件)→ docs/archive/superpowers-completed/ - 早期讨论/测试报告 → docs/archive/discussions-early/ + test-reports-early/ - QA 重复文件清理(3 个旧版 result 文件) **wiki 数据校正:** - 迁移数 137→145,源文件 599→649,提交数 720→800+ - 小程序文件 124→163,Web 前端 297→332 - 后端测试 999→943(实际统计),权限码 75+→128 - 文档索引新增归档目录说明 **CLAUDE.md 规则优化:** - §2.5 闭环工作法:提交+文档+推送三合一 + wiki 更新触发条件 - §2.6 Feature DoD:新增文档一致性检查项 - §6 反模式:新增 wiki 更新滞后/推送不及时警告
This commit is contained in:
@@ -1,618 +0,0 @@
|
||||
# ERP Platform Base - Design Specification
|
||||
|
||||
**Date:** 2026-04-10
|
||||
**Status:** Draft (Review Round 2)
|
||||
**Author:** Claude + User
|
||||
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Build a commercial SaaS ERP product from scratch using a "platform base + industry plugins" architecture. The base provides core infrastructure (auth, workflow, messaging, configuration), enabling rapid deployment of industry-specific modules (inventory, manufacturing, finance, HR, etc.) on top.
|
||||
|
||||
The system targets progressive scaling: start with small businesses, expand to mid and large enterprises. Multi-tenant SaaS deployment is the default, with private deployment as an option.
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
### Overall: Modular Monolith (Progressive)
|
||||
|
||||
Start as a single Rust backend service with well-defined module boundaries. Modules communicate through an internal event bus and shared traits. When a module needs independent scaling, it can be extracted into a standalone service without changing interfaces.
|
||||
|
||||
### System Layers
|
||||
|
||||
```
|
||||
Web Frontend (Vite + React 18 + Ant Design 5)
|
||||
├── Shell / Layout / Navigation
|
||||
└── Module UI (dynamically loaded per tenant config)
|
||||
│
|
||||
API Layer (REST + WebSocket)
|
||||
│
|
||||
Rust Backend Service (Axum + Tokio)
|
||||
├── Auth Module (identity, roles, permissions, tenants)
|
||||
├── Workflow Engine (BPMN processes, tasks, approvals)
|
||||
├── Message Center (notifications, templates, channels)
|
||||
└── Config Module (menus, dictionaries, settings, numbering)
|
||||
│
|
||||
Core Shared Layer (tenant context, audit, events, caching)
|
||||
│
|
||||
PostgreSQL (primary) + Redis (cache + session + pub/sub)
|
||||
```
|
||||
|
||||
> **Note:** Tauri 桌面端为可选方案,未来行业模块(如工厂仓库)需要硬件集成时启用。主力前端为 Web SPA。
|
||||
|
||||
### Design Principles
|
||||
|
||||
1. **Module isolation**: Each business module is an independent Rust crate, interfaces defined via traits
|
||||
2. **Multi-tenant built-in**: All data tables include `tenant_id`, middleware auto-injects tenant context
|
||||
3. **Event-driven**: Modules communicate via event bus, no direct coupling
|
||||
4. **Plugin extensibility**: Industry modules register through standard interfaces, support dynamic enable/disable
|
||||
|
||||
### Error Handling Strategy
|
||||
|
||||
```rust
|
||||
// erp-core defines the unified error hierarchy
|
||||
// Uses thiserror for typed errors across crate boundaries
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum AppError {
|
||||
#[error("Not found: {0}")]
|
||||
NotFound(String),
|
||||
|
||||
#[error("Validation error: {0}")]
|
||||
Validation(String),
|
||||
|
||||
#[error("Unauthorized")]
|
||||
Unauthorized,
|
||||
|
||||
#[error("Forbidden: {0}")]
|
||||
Forbidden(String),
|
||||
|
||||
#[error("Conflict: {0}")]
|
||||
Conflict(String),
|
||||
|
||||
#[error("Internal error: {0}")]
|
||||
Internal(String),
|
||||
}
|
||||
|
||||
// Axum IntoResponse impl maps to HTTP status codes
|
||||
// Validation errors include field-level detail for UI rendering
|
||||
```
|
||||
|
||||
**Decision**: `thiserror` for crate boundaries (typed, catchable), `anyhow` never crosses crate boundaries (internal use only for prototyping). Each module defines its own error variants that wrap `AppError`.
|
||||
|
||||
### Event Bus Specification
|
||||
|
||||
```
|
||||
EventBus (tokio::sync::broadcast based, in-process)
|
||||
|
||||
Event {
|
||||
id: UUID v7
|
||||
event_type: String (e.g., "user.created", "workflow.task.completed")
|
||||
tenant_id: UUID
|
||||
payload: serde_json::Value
|
||||
timestamp: DateTime<Utc>
|
||||
correlation_id: UUID (for tracing)
|
||||
}
|
||||
|
||||
Delivery Guarantees:
|
||||
- At-least-once delivery within the process
|
||||
- Events persisted to `domain_events` table before dispatch (outbox pattern)
|
||||
- Failed handlers log to dead-letter storage, trigger alert
|
||||
- No cross-process delivery in Phase 1 (single binary)
|
||||
```
|
||||
|
||||
### Plugin / Module Registration Interface
|
||||
|
||||
```rust
|
||||
// erp-core defines the plugin trait
|
||||
pub trait ErpModule: Send + Sync {
|
||||
fn name(&self) -> &str;
|
||||
fn version(&self) -> &str;
|
||||
fn dependencies(&self) -> Vec<&str>; // required modules
|
||||
|
||||
fn register_routes(&self, router: Router) -> Router;
|
||||
fn register_event_handlers(&self, bus: &EventBus);
|
||||
fn on_tenant_created(&self, tenant_id: Uuid) -> Result<()>;
|
||||
fn on_tenant_deleted(&self, tenant_id: Uuid) -> Result<()>;
|
||||
}
|
||||
|
||||
// erp-server assembles modules at startup
|
||||
fn build_app(modules: Vec<Box<dyn ErpModule>>) -> Router { ... }
|
||||
```
|
||||
|
||||
Industry modules implement `ErpModule` and are discovered via configuration, not compile-time.
|
||||
|
||||
### API Versioning & Contract Governance
|
||||
|
||||
- Code-first with utoipa: derive OpenAPI from Rust types
|
||||
- Auto-generated Swagger UI at `/docs` in development
|
||||
- `/api/v1/` prefix for all endpoints; v2 only when breaking changes needed
|
||||
- Client sends `X-API-Version: 1` header; server rejects unsupported versions
|
||||
- Tauri client version and server version must be compatible (checked on connect)
|
||||
|
||||
### Concurrency & Transaction Strategy
|
||||
|
||||
- **Optimistic locking**: All mutable entities carry `version` column; updates fail on mismatch
|
||||
- **Idempotency**: Write endpoints accept optional `Idempotency-Key` header
|
||||
- **Cross-module transactions**: Avoided by design; event bus + saga pattern for consistency
|
||||
- **Numbering sequences**: PostgreSQL sequences with `advisory_lock` per tenant per rule
|
||||
|
||||
### Audit Logging
|
||||
|
||||
```
|
||||
AuditLog {
|
||||
id: UUID v7
|
||||
tenant_id: UUID
|
||||
user_id: UUID
|
||||
action: String (e.g., "user.update", "role.create")
|
||||
resource_type: String
|
||||
resource_id: UUID
|
||||
changes: JSONB { before: {}, after: {} }
|
||||
ip_address: String
|
||||
user_agent: String
|
||||
timestamp: DateTime<Utc>
|
||||
}
|
||||
|
||||
Retention: 90 days hot, archive to cold storage after
|
||||
Query API: GET /api/v1/audit-logs?resource_type=user&from=...
|
||||
```
|
||||
|
||||
### Frontend-Backend Communication
|
||||
|
||||
| Aspect | Decision |
|
||||
|--------|----------|
|
||||
| Auth flow | Login via REST → JWT stored in httpOnly cookie (web) → sent as Bearer header |
|
||||
| REST calls | Standard fetch/axios from browser to backend |
|
||||
| WebSocket | Connect on page load, auth via first message with JWT, auto-reconnect with exponential backoff |
|
||||
| File upload/download | Standard HTTP multipart + blob download |
|
||||
| CORS | Whitelist per tenant, deny by default |
|
||||
|
||||
### Security Measures
|
||||
|
||||
- CORS: Whitelist per tenant, deny by default
|
||||
- Rate limiting: Per-IP + per-user via Redis token bucket
|
||||
- Secret management: Environment variables + vault (HashiCorp Vault for production)
|
||||
- Data encryption: TLS in transit, AES-256 at rest for PII fields (optional per tenant config)
|
||||
- Input validation: Schema-based (JSON Schema for complex inputs, types for simple)
|
||||
- SQL injection: Prevented by SeaORM parameterized queries
|
||||
|
||||
---
|
||||
|
||||
## Tech Stack
|
||||
|
||||
### Backend (Rust)
|
||||
|
||||
| Component | Choice | Rationale |
|
||||
|-----------|--------|-----------|
|
||||
| Web framework | Axum 0.8 | Tokio-team maintained, best ecosystem |
|
||||
| Async runtime | Tokio | Rust async standard |
|
||||
| ORM | SeaORM | Async, type-safe, migration support |
|
||||
| DB migration | SeaORM Migration | Versioned schema management |
|
||||
| Cache | redis-rs | Official Redis client |
|
||||
| JWT | jsonwebtoken | Lightweight, reliable |
|
||||
| Serialization | serde + serde_json | Rust standard |
|
||||
| Logging | tracing + tracing-subscriber | Structured logging |
|
||||
| Config | config-rs | Multi-format support |
|
||||
| API docs | utoipa (OpenAPI 3) | Auto-generate Swagger |
|
||||
| Testing | Built-in + tokio-test | Unit + integration |
|
||||
|
||||
### Web Frontend
|
||||
|
||||
| Layer | Technology |
|
||||
|-------|-----------|
|
||||
| Build tool | Vite 6 |
|
||||
| UI framework | React 18 + TypeScript |
|
||||
| Component library | Ant Design 5 |
|
||||
| State management | Zustand |
|
||||
| Routing | React Router 7 |
|
||||
| Styling | TailwindCSS + CSS Variables |
|
||||
|
||||
### Infrastructure
|
||||
|
||||
| Component | Technology |
|
||||
|-----------|-----------|
|
||||
| Primary database | PostgreSQL 16+ |
|
||||
| Cache / Session / PubSub | Redis 7+ |
|
||||
| Containerization | Docker + Docker Compose (dev) |
|
||||
|
||||
---
|
||||
|
||||
## Crate Structure
|
||||
|
||||
```
|
||||
erp/
|
||||
├── crates/
|
||||
│ ├── erp-core/ # Shared: error handling, types, traits, events
|
||||
│ ├── erp-auth/ # Identity & permissions module
|
||||
│ ├── erp-workflow/ # Workflow engine module
|
||||
│ ├── erp-message/ # Message center module
|
||||
│ ├── erp-config/ # System configuration module
|
||||
│ ├── erp-server/ # Axum server entry, assembles all modules
|
||||
│ └── erp-common/ # Shared utilities, macros
|
||||
├── apps/
|
||||
│ └── web/ # Vite + React SPA (primary frontend)
|
||||
├── desktop/ # (Optional) Tauri desktop, enabled per industry need
|
||||
├── packages/
|
||||
│ └── ui-components/ # React shared component library
|
||||
├── migrations/ # Database migrations
|
||||
├── docs/ # Documentation
|
||||
└── docker/ # Docker configurations
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Module 1: Identity & Permissions (Auth)
|
||||
|
||||
### Data Model
|
||||
|
||||
```
|
||||
Tenant
|
||||
├── Organization
|
||||
│ └── Department
|
||||
│ └── Position
|
||||
├── User
|
||||
│ ├── UserCredential (password / OAuth / SSO)
|
||||
│ ├── UserProfile
|
||||
│ └── UserToken (session)
|
||||
├── Role
|
||||
│ └── Permission
|
||||
└── Policy (ABAC rules)
|
||||
```
|
||||
|
||||
### Permission Model: RBAC + ABAC Hybrid
|
||||
|
||||
- **RBAC**: User -> Role -> Permission, for standard scenarios
|
||||
- **ABAC**: Attribute-based rules (e.g., "department manager can only approve own department's requests")
|
||||
- **Data-level**: Row filtering (e.g., "only see own department's data")
|
||||
|
||||
### Authentication Methods
|
||||
|
||||
| Method | Description |
|
||||
|--------|------------|
|
||||
| Username/Password | Basic auth, Argon2 hash |
|
||||
| OAuth 2.0 | Third-party login (WeChat, DingTalk, WeCom) |
|
||||
| SSO (SAML/OIDC) | Enterprise SSO, required for private deployment |
|
||||
| TOTP | Two-factor authentication |
|
||||
|
||||
### Key APIs
|
||||
|
||||
```
|
||||
POST /api/v1/auth/login
|
||||
POST /api/v1/auth/logout
|
||||
POST /api/v1/auth/refresh
|
||||
POST /api/v1/auth/revoke # Revoke a specific token
|
||||
GET /api/v1/users
|
||||
POST /api/v1/users
|
||||
PUT /api/v1/users/:id
|
||||
DELETE /api/v1/users/:id (soft delete)
|
||||
GET /api/v1/roles
|
||||
POST /api/v1/roles
|
||||
PUT /api/v1/roles/:id
|
||||
DELETE /api/v1/roles/:id
|
||||
POST /api/v1/roles/:id/permissions
|
||||
GET /api/v1/permissions # List all available permissions
|
||||
GET /api/v1/tenants/:id/users
|
||||
GET /api/v1/organizations
|
||||
POST /api/v1/organizations
|
||||
PUT /api/v1/organizations/:id
|
||||
DELETE /api/v1/organizations/:id
|
||||
GET /api/v1/organizations/:id/departments
|
||||
POST /api/v1/organizations/:id/departments
|
||||
GET /api/v1/positions
|
||||
POST /api/v1/positions
|
||||
GET /api/v1/policies
|
||||
POST /api/v1/policies
|
||||
PUT /api/v1/policies/:id
|
||||
DELETE /api/v1/policies/:id
|
||||
```
|
||||
|
||||
### Multi-tenant Isolation
|
||||
|
||||
- **Default**: Shared database + `tenant_id` column isolation (cost-optimal)
|
||||
- **Switchable**: Independent schema per tenant (for private deployment)
|
||||
- Middleware auto-injects `tenant_id`, application code is tenant-agnostic
|
||||
|
||||
### Multi-tenant Migration Strategy
|
||||
|
||||
- Schema migrations run once globally, affect all tenants' rows
|
||||
- New tenant provisioning: seed data script (default roles, admin user, org structure, menus)
|
||||
- Migrations are versioned and idempotent; failed migrations halt startup
|
||||
- Per-tenant data migrations (e.g., adding default config) trigger on `on_tenant_created` hook
|
||||
|
||||
---
|
||||
|
||||
## Module 2: Workflow Engine
|
||||
|
||||
### Design Goals
|
||||
|
||||
- BPMN 2.0 **subset** compatible visual process designer
|
||||
- Low latency, high throughput (Rust advantage)
|
||||
- Support conditional branches, parallel gateways, sub-processes
|
||||
- Embeddable into any business module
|
||||
|
||||
### BPMN Subset Scope (Phase 4)
|
||||
|
||||
**Included in Phase 4:**
|
||||
- Start/End events
|
||||
- User Tasks (with assignee, candidate groups)
|
||||
- Service Tasks (HTTP call, script execution)
|
||||
- Exclusive Gateways (conditional branching)
|
||||
- Parallel Gateways (fork/join)
|
||||
- Sequence Flows with conditions
|
||||
- Process variables (basic types: string, number, boolean, date)
|
||||
|
||||
**Deferred to later phases:**
|
||||
- Inclusive Gateways
|
||||
- Sub-Processes (call activity)
|
||||
- Timer events (intermediate, boundary)
|
||||
- Signal/Message events
|
||||
- Error boundary events
|
||||
- Multi-instance (loop) activities
|
||||
- Data objects and stores
|
||||
|
||||
### Core Concepts
|
||||
|
||||
```
|
||||
ProcessDefinition
|
||||
├── Node Types
|
||||
│ ├── StartNode
|
||||
│ ├── EndNode
|
||||
│ ├── UserTask (human task)
|
||||
│ ├── ServiceTask (system task)
|
||||
│ ├── Gateway (exclusive / parallel / inclusive)
|
||||
│ └── SubProcess
|
||||
├── Flow (connections)
|
||||
│ └── Condition (expressions)
|
||||
└── ProcessInstance
|
||||
├── Token (tracks execution position)
|
||||
├── Task (pending tasks)
|
||||
└── Variable (process variables)
|
||||
```
|
||||
|
||||
### Key Features
|
||||
|
||||
| Feature | Description |
|
||||
|---------|------------|
|
||||
| Visual designer | React flowchart editor, drag-and-drop |
|
||||
| Condition expressions | EL expressions: `amount > 10000 && dept == "finance"` |
|
||||
| Countersign / Or-sign | Multi-person approval: all approve / any approve |
|
||||
| Delegate / Transfer | Tasks can be delegated to others |
|
||||
| Reminder / Timeout | Auto-remind, auto-handle on timeout |
|
||||
| Version management | Process definitions versioned, running instances use old version |
|
||||
|
||||
### Key APIs
|
||||
|
||||
```
|
||||
POST /api/v1/workflow/definitions
|
||||
GET /api/v1/workflow/definitions/:id
|
||||
PUT /api/v1/workflow/definitions/:id
|
||||
POST /api/v1/workflow/instances
|
||||
GET /api/v1/workflow/instances/:id
|
||||
GET /api/v1/workflow/tasks (my pending)
|
||||
POST /api/v1/workflow/tasks/:id/approve
|
||||
POST /api/v1/workflow/tasks/:id/reject
|
||||
POST /api/v1/workflow/tasks/:id/delegate
|
||||
GET /api/v1/workflow/instances/:id/diagram (highlighted)
|
||||
```
|
||||
|
||||
### Integration Points
|
||||
|
||||
- **Auth**: Task assignment based on roles/org structure
|
||||
- **Message**: Pending task notifications, reminders, approval results
|
||||
- **Config**: Process categories, numbering rules
|
||||
|
||||
---
|
||||
|
||||
## Module 3: Message Center
|
||||
|
||||
### Message Channels
|
||||
|
||||
| Channel | Use Case |
|
||||
|---------|----------|
|
||||
| In-app notifications | Foundation for all messages |
|
||||
| WebSocket | Real-time push, instant desktop alerts |
|
||||
| Email | Important approvals, scheduled reports |
|
||||
| SMS | Verification codes, urgent alerts |
|
||||
| WeCom / DingTalk | Enterprise messaging integration |
|
||||
|
||||
### Data Model
|
||||
|
||||
```
|
||||
MessageTemplate
|
||||
├── Channel type
|
||||
├── Template content (variable interpolation: {{user_name}})
|
||||
└── Multi-language versions
|
||||
|
||||
Message
|
||||
├── Sender (system / user)
|
||||
├── Recipient (user / role / department / all)
|
||||
├── Priority (normal / important / urgent)
|
||||
├── Read status
|
||||
└── Business reference (deep link to specific page)
|
||||
|
||||
MessageSubscription
|
||||
├── User notification preferences
|
||||
├── Do-not-disturb periods
|
||||
└── Channel preferences (e.g., approvals via in-app + WeCom, reports via email)
|
||||
```
|
||||
|
||||
### Key Features
|
||||
|
||||
- **Message aggregation**: Group similar messages (e.g., "You have 5 pending approvals")
|
||||
- **Read/unread**: Read receipts, unread count query
|
||||
- **Message recall**: Sender can recall unread messages
|
||||
- **Scheduled sending**: Set delivery time
|
||||
- **Message archive**: Auto-archive history, searchable
|
||||
|
||||
### Key APIs
|
||||
|
||||
```
|
||||
GET /api/v1/messages (list with pagination)
|
||||
GET /api/v1/messages/unread-count
|
||||
PUT /api/v1/messages/:id/read
|
||||
PUT /api/v1/messages/read-all
|
||||
DELETE /api/v1/messages/:id
|
||||
POST /api/v1/messages/send
|
||||
GET /api/v1/message-templates
|
||||
POST /api/v1/message-templates
|
||||
PUT /api/v1/message-subscriptions (update preferences)
|
||||
WS /ws/v1/messages (real-time push)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Module 4: System Configuration
|
||||
|
||||
### Configuration Hierarchy
|
||||
|
||||
```
|
||||
Platform (global)
|
||||
└── Tenant
|
||||
└── Organization
|
||||
└── User
|
||||
```
|
||||
|
||||
Lower-level overrides higher-level. Priority: User > Organization > Tenant > Platform.
|
||||
|
||||
### Capabilities
|
||||
|
||||
| Capability | Description |
|
||||
|-----------|------------|
|
||||
| Dynamic menus | Tenants customize menu structure, display by role |
|
||||
| Data dictionaries | System-level and tenant-level enum management |
|
||||
| Numbering rules | Document number generation with concurrency-safe sequences |
|
||||
| Multi-language | i18n resource management, runtime switching |
|
||||
| System parameters | Key-value general configuration |
|
||||
| Theme customization | Tenant-level UI theme (colors, logo) |
|
||||
|
||||
### Key APIs
|
||||
|
||||
```
|
||||
GET /api/v1/config/menus # Tenant from middleware
|
||||
PUT /api/v1/config/menus
|
||||
GET /api/v1/config/dictionaries
|
||||
POST /api/v1/config/dictionaries
|
||||
PUT /api/v1/config/dictionaries/:id
|
||||
GET /api/v1/config/settings/:key
|
||||
PUT /api/v1/config/settings/:key
|
||||
GET /api/v1/config/numbering-rules
|
||||
POST /api/v1/config/numbering-rules
|
||||
PUT /api/v1/config/numbering-rules/:id
|
||||
GET /api/v1/config/languages
|
||||
PUT /api/v1/config/languages/:code
|
||||
GET /api/v1/config/themes # Tenant theme
|
||||
PUT /api/v1/config/themes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Database Design Principles
|
||||
|
||||
- All tables include: `tenant_id`, `created_at`, `updated_at`, `created_by`, `updated_by`
|
||||
- Soft delete via `deleted_at` (no hard deletes)
|
||||
- UUID v7 as primary keys (time-sortable + unique)
|
||||
- JSONB columns for flexible extension data
|
||||
- Indexes on `tenant_id` + business keys for multi-tenant queries
|
||||
|
||||
---
|
||||
|
||||
## Web UI Design
|
||||
|
||||
### Layout
|
||||
|
||||
Classic SaaS admin panel layout (responsive, mobile-friendly):
|
||||
|
||||
```
|
||||
+----------------------------------------------+
|
||||
| LOGO Search... 🔔 5 👤 Admin ▾ | ← Top nav bar
|
||||
+--------+-------------------------------------+
|
||||
| Home | |
|
||||
| Users | Main Content Area |
|
||||
| Roles | (Dynamic per menu selection) |
|
||||
| Flows | Multi-tab support |
|
||||
| Messages| |
|
||||
| Settings| |
|
||||
|--------| |
|
||||
| Inv. | |
|
||||
| Mfg. | |
|
||||
| Finance| |
|
||||
|--------| |
|
||||
| More > | |
|
||||
+--------+-------------------------------------+
|
||||
```
|
||||
|
||||
### Key UI Features
|
||||
|
||||
- **Collapsible sidebar**: Multi-level menus, grouped (base modules / industry modules)
|
||||
- **Multi-tab content**: Switch between open pages like browser tabs
|
||||
- **Global search**: Search menus, users, documents
|
||||
- **Notification panel**: Click bell icon to expand message list
|
||||
- **Dark/Light theme**: Toggle support, follow system preference
|
||||
- **Responsive**: Mobile/tablet adaptive layout
|
||||
- **Browser notifications**: Web Notification API for real-time alerts
|
||||
|
||||
---
|
||||
|
||||
## Development Roadmap
|
||||
|
||||
### Phase 1 - Foundation (2-3 weeks)
|
||||
- Rust workspace scaffolding + Vite + React setup
|
||||
- erp-core: error types, shared types, trait definitions, event bus
|
||||
- ErpModule trait + module registration system
|
||||
- Database migration framework (SeaORM) with tenant provisioning
|
||||
- Docker dev environment (PostgreSQL + Redis)
|
||||
- CI/CD pipeline setup
|
||||
|
||||
### Phase 2 - Identity & Permissions (2-3 weeks)
|
||||
- User, Role, Organization, Department, Position CRUD
|
||||
- RBAC + ABAC permission model
|
||||
- JWT auth (access + refresh tokens, token revocation)
|
||||
- httpOnly cookie for web JWT storage
|
||||
- Multi-tenant middleware
|
||||
- Login page UI + user management pages
|
||||
|
||||
### Phase 3 - System Configuration (1-2 weeks)
|
||||
- Data dictionaries
|
||||
- Dynamic menus
|
||||
- System parameters (hierarchical override)
|
||||
- Numbering rules (concurrency-safe PostgreSQL sequences)
|
||||
- i18n framework
|
||||
- Settings pages UI
|
||||
|
||||
### Phase 4 - Workflow Engine (4-6 weeks)
|
||||
- Process definition storage and versioning
|
||||
- BPMN subset parser (start/end, user/service tasks, exclusive/parallel gateways)
|
||||
- Execution engine with token tracking
|
||||
- Task assignment, countersign, delegation
|
||||
- Condition expression evaluator
|
||||
- React visual flowchart designer
|
||||
- Process diagram viewer (highlighted current node)
|
||||
- Reminder and timeout handling
|
||||
|
||||
### Phase 5 - Message Center (2 weeks)
|
||||
- Message templates with variable interpolation
|
||||
- In-app notification CRUD
|
||||
- WebSocket real-time push (auth, reconnect)
|
||||
- Notification panel UI
|
||||
- Message aggregation and read tracking
|
||||
|
||||
### Phase 6 - Integration & Polish (2-3 weeks)
|
||||
- Cross-module integration testing
|
||||
- Audit logging verification
|
||||
- Web app deployment and optimization
|
||||
- Performance optimization
|
||||
- Documentation
|
||||
|
||||
---
|
||||
|
||||
## Verification Plan
|
||||
|
||||
1. **Unit tests**: Each module has comprehensive unit tests (80%+ coverage target)
|
||||
2. **Integration tests**: API endpoint tests against real PostgreSQL/Redis
|
||||
3. **E2E tests**: Desktop client test automation via Tauri WebDriver
|
||||
4. **Multi-tenant tests**: Verify data isolation between tenants
|
||||
5. **Workflow tests**: Full process lifecycle (define -> start -> approve -> complete)
|
||||
6. **Performance benchmarks**: API response time < 100ms (p99), WebSocket push < 50ms
|
||||
7. **Security audit**: OWASP top 10 check before release
|
||||
@@ -1,985 +0,0 @@
|
||||
# WASM 插件系统设计规格
|
||||
|
||||
> 日期:2026-04-13
|
||||
> 状态:审核通过 (v2 — 修复安全/多租户/迁移问题)
|
||||
> 关联:`docs/superpowers/specs/2026-04-10-erp-platform-base-design.md`
|
||||
> Review 历史:v1 首次审核 → 修复 C1-C4 关键问题 + I1-I5 重要问题 → v2 审核通过
|
||||
|
||||
## 1. 背景与动机
|
||||
|
||||
ERP 平台底座 Phase 1-6 已全部完成,包含 auth、config、workflow、message 四大基础模块。
|
||||
当前系统是一个"模块化形状的单体"——模块以独立 crate 存在,但集成方式是编译时硬编码(main.rs 手动注册路由、合并迁移、启动后台任务)。
|
||||
|
||||
**核心矛盾:** Rust 的静态编译特性不支持运行时热插拔,但产品目标是"通用基座 + 行业插件"架构。
|
||||
|
||||
**本设计的目标:** 引入 WASM 运行时插件系统,使行业模块(进销存、生产、财务等)可以动态安装、启用、停用,无需修改基座代码。
|
||||
|
||||
## 2. 设计决策
|
||||
|
||||
| 决策点 | 选择 | 理由 |
|
||||
|--------|------|------|
|
||||
| 插件范围 | 仅行业模块动态化,基础模块保持 Rust 编译时 | 基础模块变更频率低、可靠性要求高,适合编译时保证 |
|
||||
| 插件技术 | WebAssembly (Wasmtime) | Rust 原生运行时,性能接近原生,沙箱安全 |
|
||||
| 数据库访问 | 宿主代理 API | 宿主自动注入 tenant_id、软删除、审计日志,插件无法绕过 |
|
||||
| 前端 UI | 配置驱动 | ERP 80% 页面是 CRUD,配置驱动覆盖大部分场景 |
|
||||
| 插件管理 | 内置插件商店 | 类似 WordPress 模型,管理后台上传 WASM 包 |
|
||||
| WASM 运行时 | Wasmtime | Bytecode Alliance 维护,Rust 原生,Cranelift JIT |
|
||||
|
||||
## 3. 架构总览
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ erp-server │
|
||||
│ │
|
||||
│ ┌────────────────────────────────────────────────────┐ │
|
||||
│ │ ModuleRegistry v2 │ │
|
||||
│ │ ┌─────────────────┐ ┌──────────────────────────┐│ │
|
||||
│ │ │ Native Modules │ │ Wasmtime Runtime ││ │
|
||||
│ │ │ ┌──────┐┌──────┐│ │ ┌──────┐┌──────┐┌──────┐││ │
|
||||
│ │ │ │ auth ││config ││ │ │进销存 ││ 生产 ││ 财务 │││ │
|
||||
│ │ │ ├──────┤├──────┤│ │ └──┬───┘└──┬───┘└──┬───┘││ │
|
||||
│ │ │ │workflow│msg ││ │ └────────┼────────┘ ││ │
|
||||
│ │ │ └──────┘└──────┘│ │ Host API Layer ││ │
|
||||
│ │ └─────────────────┘ └──────────────────────────┘│ │
|
||||
│ └────────────────────────────────────────────────────┘ │
|
||||
│ ↕ EventBus │
|
||||
│ ┌────────────────────────────────────────────────────┐ │
|
||||
│ │ 统一 Axum Router │ │
|
||||
│ │ /api/v1/auth/* /api/v1/plugins/{id}/* │ │
|
||||
│ └────────────────────────────────────────────────────┘ │
|
||||
└──────────────────────────────────────────────────────────┘
|
||||
↕
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ Frontend (React SPA) │
|
||||
│ ┌──────────────┐ ┌──────────────────────────────────┐ │
|
||||
│ │ 固定路由 │ │ 动态路由 (PluginRegistry Store) │ │
|
||||
│ │ /users /roles │ │ /inventory/* /production/* │ │
|
||||
│ └──────────────┘ └──────────────────────────────────┘ │
|
||||
│ ┌──────────────────────────────────────────────────────┐│
|
||||
│ │ PluginCRUDPage — 配置驱动的通用 CRUD 渲染引擎 ││
|
||||
│ └──────────────────────────────────────────────────────┘│
|
||||
└──────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 4. 插件清单 (Plugin Manifest)
|
||||
|
||||
每个 WASM 插件包含一个 `plugin.toml` 清单文件:
|
||||
|
||||
```toml
|
||||
[plugin]
|
||||
id = "erp-inventory" # 全局唯一 ID,kebab-case
|
||||
name = "进销存管理" # 显示名称
|
||||
version = "1.0.0" # 语义化版本
|
||||
description = "商品/采购/销售/库存管理"
|
||||
author = "ERP Team"
|
||||
min_platform_version = "1.0.0" # 最低基座版本要求
|
||||
|
||||
[dependencies]
|
||||
modules = ["auth", "workflow"] # 依赖的基础模块 ID 列表
|
||||
|
||||
[permissions]
|
||||
database = true # 需要数据库访问
|
||||
events = true # 需要发布/订阅事件
|
||||
config = true # 需要读取系统配置
|
||||
files = false # 是否需要文件存储
|
||||
|
||||
[schema]
|
||||
[[schema.entities]]
|
||||
name = "inventory_item"
|
||||
fields = [
|
||||
{ name = "sku", type = "string", required = true, unique = true },
|
||||
{ name = "name", type = "string", required = true },
|
||||
{ name = "quantity", type = "integer", default = 0 },
|
||||
{ name = "unit", type = "string", default = "个" },
|
||||
{ name = "category_id", type = "uuid", nullable = true },
|
||||
{ name = "unit_price", type = "decimal", precision = 10, scale = 2 },
|
||||
]
|
||||
indexes = [["sku"], ["category_id"]]
|
||||
|
||||
[[schema.entities]]
|
||||
name = "purchase_order"
|
||||
fields = [
|
||||
{ name = "order_no", type = "string", required = true, unique = true },
|
||||
{ name = "supplier_id", type = "uuid" },
|
||||
{ name = "status", type = "string", default = "draft" },
|
||||
{ name = "total_amount", type = "decimal", precision = 12, scale = 2 },
|
||||
{ name = "order_date", type = "date" },
|
||||
]
|
||||
|
||||
[events]
|
||||
published = ["inventory.stock.low", "purchase_order.created", "purchase_order.approved"]
|
||||
subscribed = ["workflow.task.completed"]
|
||||
|
||||
[ui]
|
||||
[[ui.pages]]
|
||||
name = "商品管理"
|
||||
path = "/inventory/items"
|
||||
entity = "inventory_item"
|
||||
type = "crud"
|
||||
icon = "ShoppingOutlined"
|
||||
menu_group = "进销存"
|
||||
|
||||
[[ui.pages]]
|
||||
name = "采购管理"
|
||||
path = "/inventory/purchase"
|
||||
entity = "purchase_order"
|
||||
type = "crud"
|
||||
icon = "ShoppingCartOutlined"
|
||||
menu_group = "进销存"
|
||||
|
||||
[[ui.pages]]
|
||||
name = "库存盘点"
|
||||
path = "/inventory/stocktaking"
|
||||
type = "custom"
|
||||
menu_group = "进销存"
|
||||
```
|
||||
|
||||
**规则:**
|
||||
- `schema.entities` 声明的表自动注入标准字段:`id`, `tenant_id`, `created_at`, `updated_at`, `created_by`, `updated_by`, `deleted_at`, `version`
|
||||
- `permissions` 控制插件可调用的宿主 API 范围(最小权限原则)
|
||||
- `ui.pages.type` 为 `crud` 时由通用渲染引擎自动生成页面,`custom` 时由插件处理渲染逻辑
|
||||
- 插件事件命名使用 `{plugin_id}.{entity}.{action}` 三段式,避免与基础模块的 `{module}.{action}` 二段式冲突
|
||||
- 动态创建的表使用 `plugin_{entity_name}` 格式,所有租户共享同一张表,通过 `tenant_id` 列实现行级隔离(与现有表模式一致)
|
||||
|
||||
## 5. 宿主 API (Host Functions)
|
||||
|
||||
WASM 插件通过宿主暴露的函数访问系统资源,这是插件与外部世界的唯一通道:
|
||||
|
||||
### 5.1 API 定义
|
||||
|
||||
```rust
|
||||
/// 宿主暴露给 WASM 插件的 API 接口
|
||||
/// 通过 Wasmtime Linker 注册为 host functions
|
||||
trait PluginHostApi {
|
||||
// === 数据库操作 ===
|
||||
|
||||
/// 插入记录(宿主自动注入 tenant_id, id, created_at 等标准字段)
|
||||
fn db_insert(&mut self, entity: &str, data: &[u8]) -> Result<Vec<u8>>;
|
||||
|
||||
/// 查询记录(自动注入 tenant_id 过滤 + 软删除过滤)
|
||||
fn db_query(&mut self, entity: &str, filter: &[u8], pagination: &[u8]) -> Result<Vec<u8]>;
|
||||
|
||||
/// 更新记录(自动检查 version 乐观锁)
|
||||
fn db_update(&mut self, entity: &str, id: &str, data: &[u8], version: i64) -> Result<Vec<u8]>;
|
||||
|
||||
/// 软删除记录
|
||||
fn db_delete(&mut self, entity: &str, id: &str) -> Result<()>;
|
||||
|
||||
/// 原始查询(仅允许 SELECT,自动注入 tenant_id 过滤)
|
||||
fn db_raw_query(&mut self, sql: &str, params: &[u8]) -> Result<Vec<u8]>;
|
||||
|
||||
// === 事件总线 ===
|
||||
|
||||
/// 发布领域事件
|
||||
fn event_publish(&mut self, event_type: &str, payload: &[u8]) -> Result<()>;
|
||||
|
||||
// === 配置 ===
|
||||
|
||||
/// 读取系统配置(插件作用域内)
|
||||
fn config_get(&mut self, key: &str) -> Result<Vec<u8]>;
|
||||
|
||||
// === 日志 ===
|
||||
|
||||
/// 写日志(自动关联 tenant_id + plugin_id)
|
||||
fn log_write(&mut self, level: &str, message: &str);
|
||||
|
||||
// === 用户/权限 ===
|
||||
|
||||
/// 获取当前用户信息
|
||||
fn current_user(&mut self) -> Result<Vec<u8]>;
|
||||
|
||||
/// 检查当前用户权限
|
||||
fn check_permission(&mut self, permission: &str) -> Result<bool>;
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 安全边界
|
||||
|
||||
插件运行在 WASM 沙箱中,安全策略如下:
|
||||
|
||||
1. **权限校验** — 插件只能调用清单 `permissions` 中声明的宿主函数,未声明的调用在加载时被拦截
|
||||
2. **租户隔离** — 所有 `db_*` 操作自动注入 `tenant_id`,插件无法绕过多租户隔离。使用行级隔离(共享表 + tenant_id 过滤),与现有基础模块保持一致
|
||||
3. **资源限制** — 每个插件有独立的资源配额(内存上限、CPU 时间、API 调用频率)
|
||||
4. **审计记录** — 所有写操作自动记录审计日志
|
||||
5. **SQL 安全** — 不暴露原始 SQL 接口,`db_aggregate` 使用结构化查询对象,宿主层安全构建参数化 SQL
|
||||
6. **文件/网络隔离** — 插件不能直接访问文件系统或网络
|
||||
|
||||
### 5.3 数据流
|
||||
|
||||
```
|
||||
WASM 插件 宿主安全层 PostgreSQL
|
||||
┌──────────┐ ┌───────────────┐ ┌──────────┐
|
||||
│ 调用 │ ── Host Call ──→ │ 1. 权限校验 │ │ │
|
||||
│ db_insert │ │ 2. 注入标准字段 │ ── SQL ──→ │ INSERT │
|
||||
│ │ │ 3. 注入 tenant │ │ INTO │
|
||||
│ │ ←─ JSON 结果 ── │ 4. 写审计日志 │ │ │
|
||||
└──────────┘ └───────────────┘ └──────────┘
|
||||
```
|
||||
|
||||
## 6. 插件生命周期
|
||||
|
||||
### 6.1 状态机
|
||||
|
||||
```
|
||||
上传 WASM 包
|
||||
│
|
||||
▼
|
||||
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
|
||||
│ uploaded │───→│ installed │───→│ enabled │───→│ running │
|
||||
└──────────┘ └──────────┘ └──────────┘ └──────────┘
|
||||
│ │
|
||||
│ ┌──────────┘
|
||||
│ ▼
|
||||
┌──────────┐
|
||||
│ disabled │←── 运行时错误自动停用
|
||||
└──────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────┐
|
||||
│uninstalled│ ── 软删除插件记录,保留数据表和数据
|
||||
└──────────┘
|
||||
│
|
||||
▼ (可选,需管理员二次确认)
|
||||
┌──────────┐
|
||||
│ purged │ ── 真正删除数据表 + 数据导出备份
|
||||
└──────────┘
|
||||
```
|
||||
|
||||
### 6.2 各阶段操作
|
||||
|
||||
| 阶段 | 操作 |
|
||||
|------|------|
|
||||
| uploaded → installed | 校验清单格式、验证依赖模块存在、检查 min_platform_version |
|
||||
| installed → enabled | 根据 `schema.entities` 创建数据表(带 `plugin_` 前缀)、写入启用状态 |
|
||||
| enabled → running | 服务启动时:Wasmtime 实例化、注册 Host Functions、调用 `init()`、注册事件处理器、注册前端路由 |
|
||||
| running → disabled | 停止 WASM 实例、注销事件处理器、注销路由 |
|
||||
| disabled → uninstalled | 软删除插件记录(设置 `deleted_at`),**保留数据表和数据不变**,清理事件订阅记录 |
|
||||
| uninstalled → purged | 数据导出备份后,删除 `plugin_*` 数据表。**需要管理员二次确认 + 数据导出完成** |
|
||||
|
||||
### 6.3 启动加载流程
|
||||
|
||||
```rust
|
||||
async fn load_plugins(db: &DatabaseConnection) -> Vec<LoadedPlugin> {
|
||||
// 1. 查询所有 enabled 状态的插件
|
||||
let plugins = Plugin::find()
|
||||
.filter(status.eq("enabled"))
|
||||
.filter(deleted_at.is_null())
|
||||
.all(db).await?;
|
||||
|
||||
let mut loaded = Vec::new();
|
||||
for plugin in plugins {
|
||||
// 2. 初始化 Wasmtime Engine(复用全局 Engine)
|
||||
let module = Module::from_binary(&engine, &plugin.wasm_binary)?;
|
||||
|
||||
// 3. 创建 Linker,根据 permissions 注册对应的 Host Functions
|
||||
let mut linker = Linker::new(&engine);
|
||||
register_host_functions(&mut linker, &plugin.permissions)?;
|
||||
|
||||
// 4. 实例化
|
||||
let instance = linker.instantiate_async(&mut store, &module).await?;
|
||||
|
||||
// 5. 调用插件的 init() 入口函数
|
||||
if let Ok(init) = instance.get_typed_func::<(), ()>(&mut store, "init") {
|
||||
init.call_async(&mut store, ()).await?;
|
||||
}
|
||||
|
||||
// 6. 注册事件处理器
|
||||
for sub in &plugin.manifest.events.subscribed {
|
||||
event_bus.subscribe_filtered(sub, plugin_handler(plugin.id, instance.clone()));
|
||||
}
|
||||
|
||||
loaded.push(LoadedPlugin { plugin, instance, store });
|
||||
}
|
||||
|
||||
// 7. 依赖排序验证
|
||||
validate_dependencies(&loaded)?;
|
||||
|
||||
Ok(loaded)
|
||||
}
|
||||
```
|
||||
|
||||
## 7. 数据库 Schema
|
||||
|
||||
### 7.1 新增表
|
||||
|
||||
```sql
|
||||
-- 插件注册表
|
||||
CREATE TABLE plugins (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL,
|
||||
plugin_id VARCHAR(100) NOT NULL, -- 清单中的唯一 ID
|
||||
name VARCHAR(200) NOT NULL,
|
||||
plugin_version VARCHAR(20) NOT NULL, -- 插件语义化版本(避免与乐观锁 version 混淆)
|
||||
description TEXT,
|
||||
manifest JSONB NOT NULL, -- 完整清单 JSON
|
||||
wasm_binary BYTEA NOT NULL, -- 编译后的 WASM 二进制
|
||||
status VARCHAR(20) DEFAULT 'installed',
|
||||
-- uploaded / installed / enabled / disabled / error
|
||||
permissions JSONB NOT NULL,
|
||||
error_message TEXT,
|
||||
schema_version INTEGER DEFAULT 1, -- 插件数据 schema 版本
|
||||
config JSONB DEFAULT '{}', -- 插件配置
|
||||
-- 标准字段
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
created_by UUID,
|
||||
updated_by UUID,
|
||||
deleted_at TIMESTAMPTZ, -- 软删除(卸载不删数据)
|
||||
row_version INTEGER NOT NULL DEFAULT 1, -- 乐观锁版本
|
||||
UNIQUE(tenant_id, plugin_id)
|
||||
);
|
||||
|
||||
-- 插件 schema 版本跟踪(用于动态表的版本管理)
|
||||
CREATE TABLE plugin_schema_versions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
plugin_id VARCHAR(100) NOT NULL, -- 全局唯一的插件 ID
|
||||
entity_name VARCHAR(100) NOT NULL, -- 实体名
|
||||
schema_version INTEGER NOT NULL DEFAULT 1, -- 当前 schema 版本
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
UNIQUE(plugin_id, entity_name)
|
||||
);
|
||||
|
||||
-- 插件事件订阅记录
|
||||
CREATE TABLE plugin_event_subscriptions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL,
|
||||
plugin_id VARCHAR(100) NOT NULL,
|
||||
event_type VARCHAR(200) NOT NULL,
|
||||
handler_name VARCHAR(100) NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
```
|
||||
|
||||
### 7.2 动态数据表
|
||||
|
||||
插件安装时根据 `manifest.schema.entities` 自动创建数据表:
|
||||
|
||||
- 表名格式:`plugin_{entity_name}`
|
||||
- **行级隔离模式**:所有租户共享同一张 `plugin_*` 表,通过 `tenant_id` 列过滤实现隔离(与现有基础模块的表保持一致)
|
||||
- 首次创建表时使用 `IF NOT EXISTS`(幂等),后续租户安装同一插件时复用已有表
|
||||
- 自动包含标准字段:`id`, `tenant_id`, `created_at`, `updated_at`, `created_by`, `updated_by`, `deleted_at`, `version`
|
||||
- 索引自动创建:主键 + `tenant_id`(必选)+ 清单中声明的自定义索引
|
||||
- **注意**:此方式绕过 SeaORM Migration 系统,属于合理偏差——插件是运行时动态加载的,其 schema 无法在编译时通过静态迁移管理。宿主维护 `plugin_schema_versions` 表跟踪每个插件的 schema 版本
|
||||
|
||||
## 8. 配置驱动 UI
|
||||
|
||||
### 8.1 前端架构
|
||||
|
||||
```
|
||||
插件 manifest.ui.pages
|
||||
│
|
||||
▼
|
||||
┌───────────────────┐
|
||||
│ PluginStore │ Zustand Store,从 /api/v1/plugins/:id/pages 加载
|
||||
│ (前端插件注册表) │ 缓存所有已启用插件的页面配置
|
||||
└───────┬───────────┘
|
||||
│
|
||||
▼
|
||||
┌───────────────────┐
|
||||
│ DynamicRouter │ React Router,根据 PluginStore 自动生成路由
|
||||
│ (动态路由层) │ 懒加载 PluginCRUDPage / PluginDashboard
|
||||
└───────┬───────────┘
|
||||
│
|
||||
▼
|
||||
┌───────────────────┐
|
||||
│ PluginCRUDPage │ 通用 CRUD 页面组件
|
||||
│ │
|
||||
│ ┌─────────────┐ │
|
||||
│ │ SearchBar │ │ 根据 filters 配置自动生成搜索条件
|
||||
│ └─────────────┘ │
|
||||
│ ┌─────────────┐ │
|
||||
│ │ DataTable │ │ 根据 columns 配置渲染 Ant Design Table
|
||||
│ └─────────────┘ │
|
||||
│ ┌─────────────┐ │
|
||||
│ │ FormDialog │ │ 根据 form 配置渲染新建/编辑表单
|
||||
│ └─────────────┘ │
|
||||
│ ┌─────────────┐ │
|
||||
│ │ ActionBar │ │ 根据 actions 配置渲染操作按钮
|
||||
│ └─────────────┘ │
|
||||
└───────────────────┘
|
||||
```
|
||||
|
||||
### 8.2 页面配置类型
|
||||
|
||||
```typescript
|
||||
interface PluginPageConfig {
|
||||
name: string;
|
||||
path: string;
|
||||
entity: string;
|
||||
type: "crud" | "dashboard" | "custom";
|
||||
icon?: string;
|
||||
menu_group: string;
|
||||
|
||||
// CRUD 配置(可选,不提供时从 schema.entities 自动推导)
|
||||
// columns 未指定时:从 entity 的 fields 生成,type=select 需显式指定 options
|
||||
// form 未指定时:从 entity 的 fields 生成表单,required 字段为必填
|
||||
columns?: ColumnDef[];
|
||||
filters?: FilterDef[];
|
||||
actions?: ActionDef[];
|
||||
form?: FormDef;
|
||||
}
|
||||
|
||||
interface ColumnDef {
|
||||
field: string;
|
||||
label: string;
|
||||
type: "text" | "number" | "date" | "datetime" | "select"
|
||||
| "multiselect" | "currency" | "status" | "link";
|
||||
width?: number;
|
||||
sortable?: boolean;
|
||||
hidden?: boolean;
|
||||
options?: { label: string; value: string; color?: string }[];
|
||||
}
|
||||
|
||||
interface FormDef {
|
||||
groups?: FormGroup[];
|
||||
fields: FormField[];
|
||||
rules?: ValidationRule[];
|
||||
}
|
||||
```
|
||||
|
||||
### 8.3 动态菜单生成
|
||||
|
||||
前端侧边栏从 PluginStore 动态生成菜单项:
|
||||
|
||||
- 基础模块菜单固定(用户、权限、组织、工作流、消息、设置)
|
||||
- 插件菜单按 `menu_group` 分组,动态追加到侧边栏
|
||||
- 菜单数据来自 `/api/v1/plugins/installed` API,启动时加载
|
||||
|
||||
### 8.4 插件 API 路由
|
||||
|
||||
插件的 CRUD API 由宿主自动生成:
|
||||
|
||||
```
|
||||
GET /api/v1/plugins/{plugin_id}/{entity} # 列表查询
|
||||
GET /api/v1/plugins/{plugin_id}/{entity}/{id} # 详情
|
||||
POST /api/v1/plugins/{plugin_id}/{entity} # 新建
|
||||
PUT /api/v1/plugins/{plugin_id}/{entity}/{id} # 更新
|
||||
DELETE /api/v1/plugins/{plugin_id}/{entity}/{id} # 删除
|
||||
```
|
||||
|
||||
宿主自动注入 tenant_id、处理分页、乐观锁、软删除。
|
||||
|
||||
### 8.5 自定义页面
|
||||
|
||||
`type: "custom"` 的页面需要额外的渲染指令:
|
||||
|
||||
- 插件 WASM 可以导出 `render_page` 函数,返回 UI 指令 JSON
|
||||
- 宿主前端解析指令并渲染(支持:条件显示、自定义操作、复杂布局)
|
||||
- 复杂交互(如库存盘点)通过事件驱动:前端发送 action → 后端 WASM 处理 → 返回新的 UI 状态
|
||||
|
||||
## 9. 升级后的模块注册系统
|
||||
|
||||
### 9.1 ErpModule trait v2
|
||||
|
||||
```rust
|
||||
#[async_trait]
|
||||
pub trait ErpModule: Send + Sync {
|
||||
fn id(&self) -> &str;
|
||||
fn name(&self) -> &str;
|
||||
fn version(&self) -> &str { env!("CARGO_PKG_VERSION") }
|
||||
fn dependencies(&self) -> Vec<&str> { vec![] }
|
||||
fn module_type(&self) -> ModuleType;
|
||||
|
||||
// 生命周期
|
||||
async fn on_startup(&self, ctx: &ModuleContext) -> AppResult<()> { Ok(()) }
|
||||
async fn on_shutdown(&self) -> AppResult<()> { Ok(()) }
|
||||
async fn health_check(&self) -> AppResult<ModuleHealth> {
|
||||
Ok(ModuleHealth { status: "ok".into(), details: None })
|
||||
}
|
||||
|
||||
// 路由
|
||||
fn public_routes(&self) -> Option<Router> { None }
|
||||
fn protected_routes(&self) -> Option<Router> { None }
|
||||
|
||||
// 数据库
|
||||
fn migrations(&self) -> Vec<Box<dyn MigrationTrait>> { vec![] }
|
||||
|
||||
// 事件
|
||||
fn register_event_handlers(&self, bus: &EventBus) {}
|
||||
|
||||
// 租户
|
||||
async fn on_tenant_created(&self, tenant_id: Uuid, ctx: &ModuleContext) -> AppResult<()> { Ok(()) }
|
||||
async fn on_tenant_deleted(&self, tenant_id: Uuid, ctx: &ModuleContext) -> AppResult<()> { Ok(()) }
|
||||
|
||||
// 配置
|
||||
fn config_schema(&self) -> Option<serde_json::Value> { None }
|
||||
|
||||
fn as_any(&self) -> &dyn Any;
|
||||
}
|
||||
|
||||
pub enum ModuleType { Native, Wasm }
|
||||
|
||||
pub struct ModuleHealth {
|
||||
pub status: String,
|
||||
pub details: Option<String>,
|
||||
}
|
||||
|
||||
pub struct ModuleContext {
|
||||
pub db: DatabaseConnection,
|
||||
pub event_bus: EventBus,
|
||||
pub config: Arc<AppConfig>,
|
||||
}
|
||||
```
|
||||
|
||||
### 9.2 ModuleRegistry v2
|
||||
|
||||
```rust
|
||||
pub struct ModuleRegistry {
|
||||
modules: Arc<Vec<Arc<dyn ErpModule>>>,
|
||||
wasm_runtime: Arc<WasmPluginRuntime>,
|
||||
index: Arc<HashMap<String, usize>>,
|
||||
}
|
||||
|
||||
impl ModuleRegistry {
|
||||
pub fn new() -> Self;
|
||||
|
||||
// 注册 Rust 原生模块
|
||||
pub fn register(self, module: impl ErpModule + 'static) -> Self;
|
||||
|
||||
// 从数据库加载 WASM 插件
|
||||
pub async fn load_wasm_plugins(&mut self, db: &DatabaseConnection) -> AppResult<()>;
|
||||
|
||||
// 按依赖顺序启动所有模块
|
||||
pub async fn startup_all(&self, ctx: &ModuleContext) -> AppResult<()>;
|
||||
|
||||
// 聚合健康状态
|
||||
pub async fn health_check_all(&self) -> HashMap<String, ModuleHealth>;
|
||||
|
||||
// 自动收集所有路由
|
||||
pub fn build_routes(&self) -> (Router, Router);
|
||||
|
||||
// 自动收集所有迁移
|
||||
pub fn collect_migrations(&self) -> Vec<Box<dyn MigrationTrait>>;
|
||||
|
||||
// 拓扑排序(基于 dependencies)
|
||||
fn topological_sort(&self) -> AppResult<Vec<Arc<dyn ErpModule>>>;
|
||||
|
||||
// 按 ID 查找模块
|
||||
pub fn get_module(&self, id: &str) -> Option<&Arc<dyn ErpModule>>;
|
||||
}
|
||||
```
|
||||
|
||||
### 9.3 升级后的 main.rs
|
||||
|
||||
```rust
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
// 初始化 DB、Config、EventBus ...
|
||||
|
||||
// 1. 注册 Rust 原生模块
|
||||
let mut registry = ModuleRegistry::new()
|
||||
.register(AuthModule::new())
|
||||
.register(ConfigModule::new())
|
||||
.register(WorkflowModule::new())
|
||||
.register(MessageModule::new());
|
||||
|
||||
// 2. 从数据库加载 WASM 插件
|
||||
registry.load_wasm_plugins(&db).await?;
|
||||
|
||||
// 3. 依赖排序 + 启动所有模块
|
||||
let ctx = ModuleContext { db: db.clone(), event_bus: event_bus.clone(), config: config.clone() };
|
||||
registry.startup_all(&ctx).await?;
|
||||
|
||||
// 4. 自动收集路由(无需手动 merge)
|
||||
let (public, protected) = registry.build_routes();
|
||||
|
||||
// 5. 构建 Axum 服务
|
||||
let app = Router::new()
|
||||
.nest("/api/v1", public.merge(protected))
|
||||
.with_state(app_state);
|
||||
|
||||
// 启动服务 ...
|
||||
}
|
||||
```
|
||||
|
||||
## 10. 插件开发体验
|
||||
|
||||
### 10.1 插件项目结构
|
||||
|
||||
```
|
||||
erp-plugin-inventory/
|
||||
├── Cargo.toml # crate 类型为 cdylib (WASM)
|
||||
├── plugin.toml # 插件清单
|
||||
└── src/
|
||||
└── lib.rs # 插件入口
|
||||
```
|
||||
|
||||
### 10.2 插件 Cargo.toml
|
||||
|
||||
```toml
|
||||
[package]
|
||||
name = "erp-plugin-inventory"
|
||||
version = "1.0.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
wit-bindgen = "0.24" # WIT 接口绑定生成
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
```
|
||||
|
||||
### 10.3 插件代码示例
|
||||
|
||||
```rust
|
||||
use wit_bindgen::generate::Guest;
|
||||
|
||||
// 自动生成宿主 API 绑定
|
||||
export!(Plugin);
|
||||
|
||||
struct Plugin;
|
||||
|
||||
impl Guest for Plugin {
|
||||
fn init() -> Result<(), String> {
|
||||
host::log_write("info", "进销存插件初始化完成");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn on_tenant_created(tenant_id: String) -> Result<(), String> {
|
||||
// 初始化默认商品分类等
|
||||
host::db_insert("inventory_category", br#"{"name": "默认分类"}"#)
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_event(event_type: String, payload: Vec<u8>) -> Result<(), String> {
|
||||
match event_type.as_str() {
|
||||
"workflow.task.completed" => {
|
||||
// 采购审批通过,更新采购单状态
|
||||
let data: serde_json::Value = serde_json::from_slice(&payload)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let order_id = data["business_id"].as_str().unwrap();
|
||||
host::db_update("purchase_order", order_id,
|
||||
br#"{"status": "approved"}"#, 1)
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 10.4 构建与发布
|
||||
|
||||
```bash
|
||||
# 编译为 WASM
|
||||
cargo build --target wasm32-unknown-unknown --release
|
||||
|
||||
# 打包(WASM 二进制 + 清单文件)
|
||||
erp-plugin pack ./target/wasm32-unknown-unknown/release/erp_plugin_inventory.wasm \
|
||||
--manifest ./plugin.toml \
|
||||
--output ./erp-inventory-1.0.0.erp-plugin
|
||||
|
||||
# 上传到平台(通过管理后台或 API)
|
||||
curl -X POST /api/v1/admin/plugins/upload \
|
||||
-F "plugin=@./erp-inventory-1.0.0.erp-plugin"
|
||||
```
|
||||
|
||||
## 11. 管理后台 API
|
||||
|
||||
### 11.1 插件管理接口
|
||||
|
||||
```
|
||||
POST /api/v1/admin/plugins/upload # 上传插件包
|
||||
GET /api/v1/admin/plugins # 列出所有插件
|
||||
GET /api/v1/admin/plugins/{plugin_id} # 插件详情
|
||||
POST /api/v1/admin/plugins/{plugin_id}/enable # 启用插件
|
||||
POST /api/v1/admin/plugins/{plugin_id}/disable # 停用插件
|
||||
DELETE /api/v1/admin/plugins/{plugin_id} # 卸载插件
|
||||
GET /api/v1/admin/plugins/{plugin_id}/health # 插件健康检查
|
||||
PUT /api/v1/admin/plugins/{plugin_id}/config # 更新插件配置
|
||||
POST /api/v1/admin/plugins/{plugin_id}/upgrade # 升级插件版本
|
||||
```
|
||||
|
||||
### 11.2 插件数据接口(自动生成)
|
||||
|
||||
```
|
||||
GET /api/v1/plugins/{plugin_id}/{entity} # 列表查询
|
||||
GET /api/v1/plugins/{plugin_id}/{entity}/{id} # 详情
|
||||
POST /api/v1/plugins/{plugin_id}/{entity} # 新建
|
||||
PUT /api/v1/plugins/{plugin_id}/{entity}/{id} # 更新
|
||||
DELETE /api/v1/plugins/{plugin_id}/{entity}/{id} # 删除
|
||||
```
|
||||
|
||||
## 12. 实施路径
|
||||
|
||||
### Phase 7: 插件系统核心
|
||||
|
||||
1. **引入 Wasmtime 依赖**,创建 `erp-plugin-runtime` crate
|
||||
2. **定义 WIT 接口文件**,描述宿主-插件合约
|
||||
3. **实现 Host API 层** — db_insert/query/update/delete、event_publish、config_get 等
|
||||
4. **实现插件加载器** — 从数据库读取 WASM 二进制、实例化、注册路由
|
||||
5. **升级 ErpModule trait** — 添加 lifecycle hooks、routes、migrations 方法
|
||||
6. **升级 ModuleRegistry** — 拓扑排序、自动路由收集、WASM 插件注册
|
||||
7. **插件管理 API** — 上传、启用、停用、卸载
|
||||
8. **插件数据库表** — plugins、plugin_event_subscriptions + 动态建表逻辑
|
||||
|
||||
### Phase 8: 前端配置驱动 UI
|
||||
|
||||
1. **PluginStore** (Zustand) — 管理已安装插件的页面配置
|
||||
2. **DynamicRouter** — 根据 PluginStore 自动生成 React Router 路由
|
||||
3. **PluginCRUDPage** — 通用 CRUD 渲染引擎(表格 + 搜索 + 表单 + 操作)
|
||||
4. **动态菜单** — 从 PluginStore 生成侧边栏菜单
|
||||
5. **插件管理页面** — 上传、启用/停用、配置的管理后台
|
||||
|
||||
### Phase 9: 第一个行业插件(进销存)
|
||||
|
||||
1. 创建 `erp-plugin-inventory` 作为参考实现
|
||||
2. 实现商品、采购、库存管理的核心业务逻辑
|
||||
3. 配置驱动页面覆盖 80% 的 CRUD 场景
|
||||
4. 验证端到端流程:安装 → 启用 → 使用 → 停用 → 卸载
|
||||
|
||||
## 13. 风险与缓解
|
||||
|
||||
| 风险 | 概率 | 影响 | 缓解措施 |
|
||||
|------|------|------|----------|
|
||||
| WASM 插件性能不足 | 低 | 高 | 性能基准测试,关键路径保留 Rust 原生 |
|
||||
| 插件安全问题 | 中 | 高 | 沙箱隔离 + 最小权限 + 审计日志 |
|
||||
| 配置驱动 UI 覆盖不足 | 中 | 中 | 保留 custom 页面类型作为兜底 |
|
||||
| 插件间依赖冲突 | 中 | 中 | 拓扑排序 + 版本约束 + 冲突检测 |
|
||||
| Wasmtime 版本兼容性 | 低 | 中 | 锁定 Wasmtime 大版本,CI 验证 |
|
||||
|
||||
## 附录 A: ErpModule Trait 迁移策略
|
||||
|
||||
### A.1 向后兼容原则
|
||||
|
||||
`ErpModule` trait v2 的所有新增方法均提供**默认实现(no-op)**,确保现有四个模块(AuthModule、ConfigModule、WorkflowModule、MessageModule)无需修改即可编译通过。
|
||||
|
||||
### A.2 迁移清单
|
||||
|
||||
| 现有方法 | v2 变化 | 迁移操作 |
|
||||
|----------|---------|----------|
|
||||
| `fn name(&self) -> &str` | 保留不变,新增 `fn id()` 返回相同值 | 在各模块 impl 中添加 `fn id()` |
|
||||
| `fn version()` | 保留不变 | 无需改动 |
|
||||
| `fn dependencies()` | 保留不变 | 无需改动 |
|
||||
| `fn register_event_handlers()` | 签名不变 | 无需改动 |
|
||||
| `fn on_tenant_created(tenant_id)` | 签名变为 `on_tenant_created(tenant_id, ctx)` | 更新签名,添加 `ctx` 参数 |
|
||||
| `fn on_tenant_deleted(tenant_id)` | 签名变为 `on_tenant_deleted(tenant_id, ctx)` | 更新签名,添加 `ctx` 参数 |
|
||||
| `fn as_any()` | 保留不变 | 无需改动 |
|
||||
| (新增)`fn module_type()` | 默认返回 `ModuleType::Native` | 无需改动 |
|
||||
| (新增)`fn on_startup()` | 默认 no-op | 可选实现 |
|
||||
| (新增)`fn on_shutdown()` | 默认 no-op | 可选实现 |
|
||||
| (新增)`fn health_check()` | 默认返回 ok | 可选实现 |
|
||||
| (新增)`fn public_routes()` | 默认 None | 将现有关联函数迁移到此方法 |
|
||||
| (新增)`fn protected_routes()` | 默认 None | 将现有关联函数迁移到此方法 |
|
||||
| (新增)`fn migrations()` | 默认空 vec | 可选实现 |
|
||||
| (新增)`fn config_schema()` | 默认 None | 可选实现 |
|
||||
|
||||
### A.3 迁移后的 main.rs 变化
|
||||
|
||||
迁移后,main.rs 从手动路由合并变为自动收集:
|
||||
|
||||
```rust
|
||||
// 迁移前(手动)
|
||||
let protected_routes = erp_auth::AuthModule::protected_routes()
|
||||
.merge(erp_config::ConfigModule::protected_routes())
|
||||
.merge(erp_workflow::WorkflowModule::protected_routes())
|
||||
.merge(erp_message::MessageModule::protected_routes());
|
||||
|
||||
// 迁移后(自动)
|
||||
let (public, protected) = registry.build_routes();
|
||||
```
|
||||
|
||||
## 附录 B: EventBus 类型化订阅扩展
|
||||
|
||||
### B.1 现有 EventBus 扩展
|
||||
|
||||
现有的 `EventBus`(`erp-core/src/events.rs`)只有 `subscribe()` 方法返回全部事件的 `Receiver`。需要添加类型化过滤订阅:
|
||||
|
||||
```rust
|
||||
impl EventBus {
|
||||
/// 订阅特定事件类型
|
||||
/// 内部使用 mpmc 通道,为每个事件类型维护独立的分发器
|
||||
pub fn subscribe_filtered(
|
||||
&self,
|
||||
event_type: &str,
|
||||
handler: Box<dyn Fn(DomainEvent) + Send + Sync>,
|
||||
) -> SubscriptionHandle {
|
||||
// 在内部 HashMap<String, Vec<Handler>> 中注册
|
||||
// publish() 时根据 event_type 分发到匹配的 handler
|
||||
}
|
||||
|
||||
/// 取消订阅(用于插件停用时清理)
|
||||
pub fn unsubscribe(&self, handle: SubscriptionHandle) { /* ... */ }
|
||||
}
|
||||
```
|
||||
|
||||
### B.2 插件事件处理器包装
|
||||
|
||||
```rust
|
||||
struct PluginEventHandler {
|
||||
plugin_id: String,
|
||||
handler_fn: Box<dyn Fn(DomainEvent) + Send + Sync>,
|
||||
}
|
||||
|
||||
impl PluginEventHandler {
|
||||
fn handle(&self, event: DomainEvent) {
|
||||
// 捕获 panic,防止插件崩溃影响宿主
|
||||
let result = std::panic::catch_unwind(|| {
|
||||
(self.handler_fn)(event)
|
||||
});
|
||||
if let Err(_) = result {
|
||||
tracing::error!("插件 {} 事件处理器崩溃", self.plugin_id);
|
||||
// 通知 PluginManager 标记插件为 error 状态
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 附录 C: 管理后台 API 权限控制
|
||||
|
||||
### C.1 权限模型
|
||||
|
||||
| API 端点 | 所需权限 | 角色范围 |
|
||||
|----------|---------|----------|
|
||||
| `POST /admin/plugins/upload` | `plugin:admin` | 仅平台超级管理员 |
|
||||
| `POST /admin/plugins/{id}/enable` | `plugin:manage` | 平台管理员或租户管理员(仅限自己租户的插件) |
|
||||
| `POST /admin/plugins/{id}/disable` | `plugin:manage` | 平台管理员或租户管理员 |
|
||||
| `DELETE /admin/plugins/{id}` | `plugin:manage` | 租户管理员(软删除) |
|
||||
| `DELETE /admin/plugins/{id}/purge` | `plugin:admin` | 仅平台超级管理员 |
|
||||
| `GET /admin/plugins` | `plugin:view` | 租户管理员(仅看到自己租户的插件) |
|
||||
| `PUT /admin/plugins/{id}/config` | `plugin:configure` | 租户管理员 |
|
||||
| `GET /admin/plugins/{id}/health` | `plugin:view` | 租户管理员 |
|
||||
|
||||
### C.2 租户隔离
|
||||
|
||||
- 插件管理 API 自动注入 `tenant_id` 过滤(从 JWT 中提取)
|
||||
- 平台超级管理员可以通过 `/admin/platform/plugins` 查看所有租户的插件
|
||||
- 租户管理员只能管理自己租户安装的插件
|
||||
- 插件上传为平台级操作(所有租户共享同一个 WASM 二进制),但启用/配置为租户级操作
|
||||
|
||||
## 附录 D: WIT 接口定义
|
||||
|
||||
### D.1 插件接口 (`plugin.wit`)
|
||||
|
||||
```wit
|
||||
package erp:plugin;
|
||||
|
||||
interface host {
|
||||
/// 数据库操作
|
||||
db-insert: func(entity: string, data: list<u8>) -> result<list<u8>, string>;
|
||||
db-query: func(entity: string, filter: list<u8>, pagination: list<u8>) -> result<list<u8>, string>;
|
||||
db-update: func(entity: string, id: string, data: list<u8>, version: s64) -> result<list<u8>, string>;
|
||||
db-delete: func(entity: string, id: string) -> result<_, string>;
|
||||
db-aggregate: func(entity: string, query: list<u8>) -> result<list<u8>, string>;
|
||||
|
||||
/// 事件总线
|
||||
event-publish: func(event-type: string, payload: list<u8>) -> result<_, string>;
|
||||
|
||||
/// 配置
|
||||
config-get: func(key: string) -> result<list<u8>, string>;
|
||||
|
||||
/// 日志
|
||||
log-write: func(level: string, message: string);
|
||||
|
||||
/// 用户/权限
|
||||
current-user: func() -> result<list<u8>, string>;
|
||||
check-permission: func(permission: string) -> result<bool, string>;
|
||||
}
|
||||
|
||||
interface plugin {
|
||||
/// 插件初始化(加载时调用一次)
|
||||
init: func() -> result<_, string>;
|
||||
|
||||
/// 租户创建时调用
|
||||
on-tenant-created: func(tenant-id: string) -> result<_, string>;
|
||||
|
||||
/// 处理订阅的事件
|
||||
handle-event: func(event-type: string, payload: list<u8>) -> result<_, string>;
|
||||
|
||||
/// 自定义页面渲染(仅 type=custom 页面)
|
||||
render-page: func(page-path: string, params: list<u8>) -> result<list<u8>, string>;
|
||||
|
||||
/// 自定义页面操作处理
|
||||
handle-action: func(page-path: string, action: string, data: list<u8>) -> result<list<u8>, string>;
|
||||
}
|
||||
|
||||
world plugin-world {
|
||||
import host;
|
||||
export plugin;
|
||||
}
|
||||
```
|
||||
|
||||
### D.2 使用方式
|
||||
|
||||
插件开发者使用 `wit-bindgen` 生成绑定代码:
|
||||
|
||||
```bash
|
||||
# 生成 Rust 插件绑定
|
||||
wit-bindgen rust ./plugin.wit --out-dir ./src/generated
|
||||
```
|
||||
|
||||
宿主使用 `wasmtime` 的 `bindgen!` 宏生成调用端代码:
|
||||
|
||||
```rust
|
||||
// 在 erp-plugin-runtime crate 中
|
||||
wasmtime::component::bindgen!({
|
||||
path: "./plugin.wit",
|
||||
world: "plugin-world",
|
||||
async: true,
|
||||
});
|
||||
```
|
||||
|
||||
## 附录 E: 插件崩溃恢复策略
|
||||
|
||||
### E.1 崩溃检测与恢复
|
||||
|
||||
| 场景 | 检测方式 | 恢复策略 |
|
||||
|------|---------|----------|
|
||||
| WASM 执行 panic | `catch_unwind` 捕获 | 记录错误日志,该请求返回 500,插件继续运行 |
|
||||
| 插件 init() 失败 | 返回 Err | 标记插件为 `error` 状态,不加载 |
|
||||
| 事件处理器崩溃 | `catch_unwind` 捕获 | 记录错误日志,事件丢弃(不重试) |
|
||||
| 连续崩溃(>5次/分钟) | 计数器检测 | 自动停用插件,标记 `error`,通知管理员 |
|
||||
| 服务重启 | 启动流程 | 重新加载所有 `enabled` 状态的插件 |
|
||||
|
||||
### E.2 僵尸状态处理
|
||||
|
||||
插件在数据库中为 `enabled` 但实际未运行的情况:
|
||||
|
||||
1. 服务启动时,所有 `enabled` 插件尝试加载
|
||||
2. 加载失败的插件自动标记为 `error`,`error_message` 记录原因
|
||||
3. 管理后台显示 `error` 状态的插件,提供"重试"按钮
|
||||
4. 重试成功后恢复为 `enabled`,重试失败保持 `error`
|
||||
|
||||
### E.3 插件健康检查
|
||||
|
||||
```rust
|
||||
/// 定期健康检查(每 60 秒)
|
||||
async fn health_check_loop(registry: &ModuleRegistry) {
|
||||
let mut interval = tokio::time::interval(Duration::from_secs(60));
|
||||
loop {
|
||||
interval.tick().await;
|
||||
let results = registry.health_check_all().await;
|
||||
for (id, health) in results {
|
||||
if health.status != "ok" {
|
||||
tracing::warn!("模块 {} 健康检查异常: {:?}", id, health.details);
|
||||
// 通知管理后台
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 附录 F: Crate 依赖图更新
|
||||
|
||||
```
|
||||
erp-core (无业务依赖)
|
||||
erp-common (无业务依赖)
|
||||
↑
|
||||
erp-auth (→ core)
|
||||
erp-config (→ core)
|
||||
erp-workflow (→ core)
|
||||
erp-message (→ core)
|
||||
erp-plugin-runtime (→ core, wasmtime) ← 新增
|
||||
↑
|
||||
erp-server (→ 所有 crate,组装入口)
|
||||
```
|
||||
|
||||
**规则:**
|
||||
- `erp-plugin-runtime` 依赖 `erp-core`(使用 EventBus、ErpModule trait、AppError)
|
||||
- `erp-plugin-runtime` 依赖 `wasmtime`(WASM 运行时)
|
||||
- `erp-plugin-runtime` 不依赖任何业务 crate(auth/config/workflow/message)
|
||||
- `erp-server` 在组装时引入 `erp-plugin-runtime`
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,767 +0,0 @@
|
||||
# CRM 插件基座升级设计规格 v1.0
|
||||
|
||||
> **文档状态:** v1.1 — 已修复评审问题
|
||||
> **创建日期:** 2026-04-17
|
||||
> **范围:** JSONB 存储优化 + 数据完整性框架 + 行级数据权限 + 前端页面能力增强
|
||||
> **评审记录:** code-reviewer 子代理评审通过一轮修复(3 Critical + 7 Important)
|
||||
|
||||
---
|
||||
|
||||
## 1. 背景与动机
|
||||
|
||||
CRM 插件是 ERP 平台的第一个 WASM 行业插件,已完成 3 阶段 24 任务,包含 5 实体、9 权限、7 页面类型。经 6 个专家组深度评审,发现以下结构性问题需要优先解决:
|
||||
|
||||
| 问题 | 严重级别 | 影响 |
|
||||
|------|---------|------|
|
||||
| JSONB 动态表类型安全缺失、排序全表扫描 | High | 万级数据以上性能崩溃 |
|
||||
| JSONB 零外键完整性、零级联策略 | High | 数据"脏"掉,引用断裂 |
|
||||
| 行级数据权限缺失 | Critical | 销售A能看到销售B的所有客户 |
|
||||
| plugin.admin 权限 fallback 过宽 | Critical | 超级用户权限泄露 |
|
||||
| 无关联选择器 (entity_select) | High | UX 极差,客户ID手动输入 |
|
||||
| 无看板/批量操作/图表等页面能力 | Medium | CRM 功能不完整 |
|
||||
|
||||
**核心原则:** 基座优先。所有改进沉淀为插件平台通用能力,CRM 作为第一受益者而非唯一受益者。
|
||||
|
||||
---
|
||||
|
||||
## 2. 设计目标
|
||||
|
||||
1. **JSONB 存储优化** — 百万级数据下列表查询 p95 < 200ms,搜索 p95 < 300ms
|
||||
2. **数据完整性框架** — 应用层外键校验、级联策略、字段校验、循环引用检测
|
||||
3. **行级数据权限** — 支持 self/department/department_tree/all 四级数据范围
|
||||
4. **前端页面能力增强** — 关联选择器、看板页面、批量操作、Dashboard 图表、visible_when 增强
|
||||
|
||||
---
|
||||
|
||||
## 3. JSONB 存储优化
|
||||
|
||||
### 3.1 Generated Column 混合存储
|
||||
|
||||
利用 PostgreSQL 12+ 的 `GENERATED ALWAYS AS ... STORED` 列,自动从 JSONB `data` 列提取高频字段到独立列。数据只存一份(在 JSONB 中),Generated Column 是自动派生的,零维护成本。
|
||||
|
||||
**提取规则(在 `dynamic_table.rs` 的 `create_table` 中自动判断):**
|
||||
|
||||
| 字段特征 | 提取策略 | 原因 |
|
||||
|----------|---------|------|
|
||||
| `unique == true` | Generated Column + UNIQUE INDEX | 需要精确唯一性约束 |
|
||||
| `required == true && (sortable \|\| filterable)` | Generated Column + INDEX | 需要类型化排序/筛选 |
|
||||
| `sortable == true` | Generated Column + INDEX | ORDER BY 走 B-tree |
|
||||
| `filterable == true` | Generated Column + INDEX | WHERE 走索引扫描 |
|
||||
| `searchable == true` | 保留 JSONB + pg_trgm GIN 索引 | 模糊搜索用三元组索引 |
|
||||
| 其他字段 | 保留 JSONB | 无需索引 |
|
||||
|
||||
**生成的 DDL 示例:**
|
||||
|
||||
```sql
|
||||
CREATE TABLE plugin_erp_crm_customer (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL,
|
||||
data JSONB NOT NULL DEFAULT '{}',
|
||||
-- Generated Columns
|
||||
_f_code TEXT GENERATED ALWAYS AS (data->>'code') STORED,
|
||||
_f_name TEXT GENERATED ALWAYS AS (data->>'name') STORED,
|
||||
_f_customer_type TEXT GENERATED ALWAYS AS (data->>'customer_type') STORED,
|
||||
_f_status TEXT GENERATED ALWAYS AS (data->>'status') STORED,
|
||||
_f_level TEXT GENERATED ALWAYS AS (data->>'level') STORED,
|
||||
-- 标准字段
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
created_by UUID,
|
||||
updated_by UUID,
|
||||
deleted_at TIMESTAMPTZ,
|
||||
version INT NOT NULL DEFAULT 1
|
||||
);
|
||||
|
||||
-- 复合索引(tenant_id 在前,支持多租户过滤)
|
||||
CREATE INDEX IF NOT EXISTS idx_{t}_tenant_cover
|
||||
ON "{t}" (tenant_id, created_at DESC)
|
||||
INCLUDE (id, data, updated_at, version)
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_{t}_f_name_sort
|
||||
ON "{t}" (tenant_id, _f_name)
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_{t}_f_code_uniq
|
||||
ON "{t}" (tenant_id, _f_code)
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_{t}_f_type_filter
|
||||
ON "{t}" (tenant_id, _f_customer_type)
|
||||
WHERE deleted_at IS NULL;
|
||||
```
|
||||
|
||||
**SQL 查询路由:** 在 `dynamic_table.rs` 中新增 `GeneratedColumnInfo` 结构,记录哪些字段被提取为 Generated Column。`build_filtered_query_sql` 和 `build_aggregate_sql` 检测到对应 Generated Column 存在时,自动将 `data->>'field'` 替换为 `_f_{field}`。
|
||||
|
||||
**类型映射:** `data->>'field'` 始终返回 TEXT。对于非字符串类型,Generated Column 需要类型转换以支持正确的排序和比较:
|
||||
|
||||
| field_type | SQL 类型 | Generated Column 表达式 |
|
||||
|------------|---------|------------------------|
|
||||
| String | TEXT | `data->>'field'` |
|
||||
| Integer | INTEGER | `(data->>'field')::INTEGER` |
|
||||
| Float | DOUBLE PRECISION | `(data->>'field')::DOUBLE PRECISION` |
|
||||
| Decimal | NUMERIC(18,4) | `(data->>'field')::NUMERIC` |
|
||||
| Boolean | BOOLEAN | `(data->>'field')::BOOLEAN` |
|
||||
| Date | DATE | `(data->>'field')::DATE` |
|
||||
| DateTime | TIMESTAMPTZ | `(data->>'field')::TIMESTAMPTZ` |
|
||||
| Uuid | UUID | `(data->>'field')::UUID` |
|
||||
|
||||
`dynamic_table.rs` 的 `create_table` 根据 `PluginField.field_type` 自动选择正确的 SQL 类型和类型转换表达式。
|
||||
|
||||
**元数据表:**
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS plugin_entity_columns (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL, -- 多租户标准字段
|
||||
plugin_entity_id UUID NOT NULL REFERENCES plugin_entities(id),
|
||||
field_name VARCHAR(100) NOT NULL,
|
||||
column_name VARCHAR(100) NOT NULL, -- 如 _f_name
|
||||
sql_type VARCHAR(50) NOT NULL, -- 如 TEXT, INTEGER, UUID
|
||||
is_generated BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
```
|
||||
|
||||
**Schema 演变策略(重新安装/字段变更):**
|
||||
|
||||
当前 `service.rs` 的 `install` 使用 `CREATE TABLE IF NOT EXISTS`。引入 Generated Column 后,安装流程改为:
|
||||
|
||||
1. **首次安装**:`CREATE TABLE` 包含所有 Generated Column。
|
||||
2. **重新安装(同版本)**:`IF NOT EXISTS` 跳过表创建。比对 `plugin_entity_columns` 元数据与当前 manifest 的字段列表,执行增量 ALTER:
|
||||
- 新增字段:`ALTER TABLE ADD COLUMN _f_{name} {type} GENERATED ALWAYS AS (...) STORED`
|
||||
- 删除字段:`ALTER TABLE DROP COLUMN _f_{name}`(仅删除 Generated Column,JSONB data 中的原始值保留)
|
||||
- 类型变更:PostgreSQL 不支持 ALTER GENERATED COLUMN 的表达式,需 DROP + ADD
|
||||
3. **插件卸载时**:表被删除,元数据自动清理。
|
||||
|
||||
`dynamic_table.rs` 新增 `migrate_table` 方法,接受已有列列表和目标列列表,生成增量 DDL。
|
||||
|
||||
### 3.2 pg_trgm 模糊搜索加速
|
||||
|
||||
**迁移文件:** 在 `erp-server/migration` 中新增迁移启用 pg_trgm 扩展:
|
||||
|
||||
```sql
|
||||
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
||||
```
|
||||
|
||||
**索引创建:** `create_table` 中 searchable 字段的索引从普通 B-tree 改为 GIN 三元组:
|
||||
|
||||
```sql
|
||||
CREATE INDEX IF NOT EXISTS idx_{t}_{f}_trgm
|
||||
ON "{t}" USING GIN ((data->>'{f}') gin_trgm_ops)
|
||||
WHERE deleted_at IS NULL;
|
||||
```
|
||||
|
||||
启用后 `ILIKE '%keyword%'` 从全表扫描退化为索引扫描,百万级数据搜索从 2-5s 降至 50-200ms。
|
||||
|
||||
### 3.3 Keyset Pagination
|
||||
|
||||
**向后兼容设计:** API 同时支持 OFFSET 和 cursor 两种分页模式。
|
||||
|
||||
`data_dto.rs` 中 `PluginDataListParams` 新增 `cursor` 字段:
|
||||
|
||||
```rust
|
||||
pub struct PluginDataListParams {
|
||||
pub page: Option<u64>, // 保留,向后兼容
|
||||
pub page_size: Option<u64>,
|
||||
pub cursor: Option<String>, // 新增:Base64 编码的游标
|
||||
pub search: Option<String>,
|
||||
pub filter: Option<String>,
|
||||
pub sort_by: Option<String>,
|
||||
pub sort_order: Option<String>,
|
||||
}
|
||||
```
|
||||
|
||||
`dynamic_table.rs` 中 SQL 构建逻辑:当 `cursor` 存在时使用 keyset 分页:
|
||||
|
||||
**游标编码格式:** JSON 结构 `{ "v": [value1, value2, ...], "id": "uuid" }`,Base64 编码。`v` 数组存储排序字段的值(与 sort_by 顺序一致),`id` 是记录主键作为最终 tiebreaker。多列排序时 `v` 包含多个值。字段值为 null 时存储 JSON null。
|
||||
|
||||
客户端必须在每次请求中同时发送 `cursor` 和 `sort_by`/`sort_order`(游标不嵌入排序信息,保持无状态)。
|
||||
|
||||
```sql
|
||||
-- 第一页
|
||||
SELECT ... ORDER BY _f_name ASC, id ASC LIMIT 20;
|
||||
|
||||
-- 后续页(cursor 解码后)
|
||||
SELECT ... WHERE (_f_name, id) > ($cursor_sort_val, $cursor_id)
|
||||
ORDER BY _f_name ASC, id ASC LIMIT 20;
|
||||
```
|
||||
|
||||
### 3.4 Schema 缓存
|
||||
|
||||
在 `PluginState` 中添加 `moka` LRU 缓存,消除每次数据请求的 `resolve_entity_info` 查库:
|
||||
|
||||
```rust
|
||||
pub entity_cache: Cache<String, EntityInfo>, // key: "{plugin_id}:{entity_name}:{tenant_id}"
|
||||
```
|
||||
|
||||
TTL 5 分钟,容量 1000 条。
|
||||
|
||||
### 3.5 聚合 Redis 缓存
|
||||
|
||||
`data_service.rs` 的 create/update/delete 成功后增量更新 Redis 统计:
|
||||
|
||||
```
|
||||
plugin:{plugin_id}:{entity}:count:{tenant_id} → 计数值
|
||||
plugin:{plugin_id}:{entity}:agg:{field}:{tenant_id} → JSON {key: count}
|
||||
```
|
||||
|
||||
Dashboard 查询直接从 Redis 读取,TTL 5 分钟兜底。
|
||||
|
||||
### 3.6 性能 SLA 目标
|
||||
|
||||
**测试条件:** PostgreSQL 与应用同机部署(Redis localhost 延迟 < 1ms)。SLA 包含 Redis 往返(schema 缓存 + 部门缓存)。冷启动(Redis 缓存未命中)首次查询允许 3x SLA 宽限。
|
||||
|
||||
| 查询场景 | 数据量 | p50 | p95 | p99 |
|
||||
|----------|--------|-----|-----|-----|
|
||||
| 按 ID 获取单条 | 100万 | < 5ms | < 10ms | < 20ms |
|
||||
| 列表查询(默认排序) | 100万 | < 20ms | < 50ms | < 100ms |
|
||||
| 列表查询(字段排序) | 100万 | < 30ms | < 100ms | < 200ms |
|
||||
| 搜索(ILIKE) | 100万 | < 50ms | < 100ms | < 300ms |
|
||||
| 聚合查询 | 100万 | < 50ms (缓存) | < 500ms (实时) | - |
|
||||
| Dashboard 全量加载 | 100万 | < 200ms | < 500ms | - |
|
||||
|
||||
### 3.7 涉及文件
|
||||
|
||||
| 文件 | 改动类型 |
|
||||
|------|---------|
|
||||
| `crates/erp-plugin/src/dynamic_table.rs` | 主要改动 — Generated Column DDL、索引策略、SQL 路由、keyset 分页 |
|
||||
| `crates/erp-plugin/src/data_service.rs` | 缓存逻辑、聚合 Redis 缓存 |
|
||||
| `crates/erp-plugin/src/data_dto.rs` | 新增 cursor 参数 |
|
||||
| `crates/erp-plugin/src/state.rs` | 新增 entity_cache |
|
||||
| `crates/erp-plugin/src/manifest.rs` | PluginEntityColumns 元数据 |
|
||||
| `crates/erp-server/migration/src/` | pg_trgm 扩展 + plugin_entity_columns 表 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 数据完整性框架
|
||||
|
||||
### 4.1 外键引用声明
|
||||
|
||||
`manifest.rs` 的 `PluginField` 新增 `ref_entity` 字段:
|
||||
|
||||
```rust
|
||||
pub struct PluginField {
|
||||
pub name: String,
|
||||
pub field_type: PluginFieldType,
|
||||
// ...已有字段...
|
||||
pub ref_entity: Option<String>, // 新增:引用的实体名
|
||||
}
|
||||
```
|
||||
|
||||
manifest TOML 中的使用方式:
|
||||
|
||||
```toml
|
||||
[[schema.entities.fields]]
|
||||
name = "customer_id"
|
||||
field_type = "uuid"
|
||||
required = true
|
||||
display_name = "所属客户"
|
||||
ref_entity = "customer" # 声明外键引用
|
||||
```
|
||||
|
||||
### 4.2 应用层外键校验
|
||||
|
||||
在 `data_service.rs` 的 `validate_data` 函数中扩展:
|
||||
|
||||
```
|
||||
create/update 时:
|
||||
遍历 fields,如果 field.ref_entity 存在:
|
||||
1. 从 data 中取出该字段的 UUID 值
|
||||
2. 如果值为 null 或空字符串且 required == false → 跳过校验
|
||||
3. 如果是自引用(ref_entity == 当前实体名)且为 create 操作:
|
||||
a. 如果引用的是自身 ID → 跳过(记录尚不存在,无法校验)
|
||||
b. 如果引用的是其他记录 → 正常校验
|
||||
4. 查询 ref_entity 对应的动态表,验证该记录存在且未删除
|
||||
5. 不存在则返回 ValidationError
|
||||
|
||||
TOCTOU 竞态说明:
|
||||
外键校验与引用记录删除之间存在理论上的竞态窗口。
|
||||
对于 JSONB 动态表,这是可接受的风险——应用层校验已大幅降低孤立引用概率。
|
||||
如果未来需要严格保证,可在 flush_ops 中增加二次校验(事务内 SELECT FOR UPDATE)。
|
||||
```
|
||||
|
||||
### 4.3 级联删除策略
|
||||
|
||||
`manifest.rs` 新增 `PluginRelation` 结构:
|
||||
|
||||
```rust
|
||||
pub struct PluginRelation {
|
||||
pub entity: String, // 关联实体名
|
||||
pub foreign_key: String, // 关联实体中的外键字段名
|
||||
pub on_delete: OnDeleteStrategy, // 级联策略
|
||||
}
|
||||
|
||||
pub enum OnDeleteStrategy {
|
||||
Nullify, // 置空外键字段
|
||||
Cascade, // 级联软删除
|
||||
Restrict, // 存在关联时拒绝删除
|
||||
}
|
||||
```
|
||||
|
||||
manifest TOML 中的使用方式:
|
||||
|
||||
```toml
|
||||
[[schema.entities.relations]]
|
||||
entity = "contact"
|
||||
foreign_key = "customer_id"
|
||||
on_delete = "nullify"
|
||||
|
||||
[[schema.entities.relations]]
|
||||
entity = "communication"
|
||||
foreign_key = "customer_id"
|
||||
on_delete = "cascade"
|
||||
|
||||
[[schema.entities.relations]]
|
||||
entity = "customer_tag"
|
||||
foreign_key = "customer_id"
|
||||
on_delete = "cascade"
|
||||
```
|
||||
|
||||
`data_service.rs` 的 `delete` 方法中,在软删除记录之前:
|
||||
|
||||
```
|
||||
1. 从 manifest 中查找该实体声明的所有 relations
|
||||
2. 对每个 relation:
|
||||
- Restrict: 查询关联实体是否有引用 → 有则拒绝删除
|
||||
- Nullify: 批量 UPDATE 关联记录,将 foreign_key 设为 null
|
||||
- Cascade: 批量软删除关联记录(级联深度上限 3 层,防止 A→B→C→D 无限递归)
|
||||
```
|
||||
|
||||
### 4.4 字段校验规则
|
||||
|
||||
`manifest.rs` 的 `PluginField` 新增 `validation` 子结构:
|
||||
|
||||
```rust
|
||||
pub struct FieldValidation {
|
||||
pub pattern: Option<String>, // 正则表达式
|
||||
pub message: Option<String>, // 校验失败提示
|
||||
}
|
||||
```
|
||||
|
||||
manifest TOML 中的使用方式:
|
||||
|
||||
```toml
|
||||
[[schema.entities.fields]]
|
||||
name = "phone"
|
||||
field_type = "string"
|
||||
display_name = "手机号"
|
||||
validation = { pattern = "^1[3-9]\\d{9}$", message = "手机号格式不正确" }
|
||||
|
||||
[[schema.entities.fields]]
|
||||
name = "email"
|
||||
field_type = "string"
|
||||
display_name = "邮箱"
|
||||
validation = { pattern = "^[\\w.-]+@[\\w.-]+\\.\\w+$", message = "邮箱格式不正确" }
|
||||
```
|
||||
|
||||
`validate_data` 扩展:对有 `validation.pattern` 的字段,使用 `regex` crate 做正则匹配。
|
||||
|
||||
### 4.5 循环引用检测
|
||||
|
||||
`manifest.rs` 的 `PluginField` 新增 `no_cycle` 字段:
|
||||
|
||||
```toml
|
||||
[[schema.entities.fields]]
|
||||
name = "parent_id"
|
||||
field_type = "uuid"
|
||||
ref_entity = "customer"
|
||||
no_cycle = true # 声明不允许循环引用
|
||||
```
|
||||
|
||||
`data_service.rs` 的 `update` 方法中,当 `no_cycle == true` 的字段被修改时:
|
||||
|
||||
```
|
||||
1. 从 data 中取出新值 (new_parent_id)
|
||||
2. 初始化 visited = {record_id}
|
||||
3. 循环:查询 current 的 parent_id → 如果在 visited 中则报错 → 加入 visited
|
||||
4. 直到 parent_id 为 null 或到达根节点
|
||||
```
|
||||
|
||||
### 4.6 涉及文件
|
||||
|
||||
| 文件 | 改动类型 |
|
||||
|------|---------|
|
||||
| `crates/erp-plugin/src/manifest.rs` | 新增 ref_entity / PluginRelation / FieldValidation / no_cycle |
|
||||
| `crates/erp-plugin/src/data_service.rs` | 外键校验 / 级联删除 / 字段校验 / 循环检测 |
|
||||
| `crates/erp-plugin-crm/plugin.toml` | 为现有字段添加 ref_entity / relations / validation / no_cycle 声明 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 行级数据权限
|
||||
|
||||
### 5.1 数据范围模型
|
||||
|
||||
在实体级别声明是否启用行级数据权限,在权限级别声明数据范围等级。
|
||||
|
||||
**manifest 扩展:**
|
||||
|
||||
```toml
|
||||
[[schema.entities]]
|
||||
name = "customer"
|
||||
display_name = "客户"
|
||||
data_scope = true # 启用行级数据权限
|
||||
|
||||
[[schema.entities.fields]]
|
||||
name = "owner_id"
|
||||
field_type = "uuid"
|
||||
display_name = "负责人"
|
||||
scope_role = "owner" # 标记为数据权限的"所有者"字段
|
||||
```
|
||||
|
||||
**权限声明扩展:**
|
||||
|
||||
```toml
|
||||
[[permissions]]
|
||||
code = "customer.list"
|
||||
name = "查看客户"
|
||||
data_scope_levels = ["self", "department", "department_tree", "all"]
|
||||
```
|
||||
|
||||
### 5.2 数据范围等级定义
|
||||
|
||||
| 等级 | 含义 | SQL 条件 |
|
||||
|------|------|---------|
|
||||
| `self` | 只看自己负责/创建的 | `data->>'owner_id' = current_user_id OR created_by = current_user_id` |
|
||||
| `department` | 看本部门所有人的 | `data->>'owner_id' IN (部门用户列表)` |
|
||||
| `department_tree` | 看本部门及下级部门 | `data->>'owner_id' IN (部门树用户列表)` |
|
||||
| `all` | 看全部 | 无额外条件 |
|
||||
|
||||
### 5.3 实现路径
|
||||
|
||||
**TenantContext 扩展:** `erp-core` 的 `TenantContext` 结构新增 `department_ids: Vec<Uuid>` 字段(注意:用户可通过岗位属于多个部门)。JWT claims 中新增 `dept_ids` 字段,JWT 中间件在构造 TenantContext 时填充。
|
||||
|
||||
**多部门用户处理:** 用户通过 Position 关联到多个 Department。`department` 级别取所有所属部门的并集;`department_tree` 取所有所属部门及其下级部门的并集。没有岗位/部门的用户在 `department` 和 `department_tree` 级别下只能看到自己创建的数据(降级为 self)。
|
||||
|
||||
**角色权限表扩展:** `role_permissions` 表新增 `data_scope` 字段(VARCHAR(32),默认值 `'all'`)。新增迁移文件 `m20260418_*_add_data_scope_to_role_permissions.rs`:
|
||||
|
||||
```sql
|
||||
ALTER TABLE role_permissions ADD COLUMN IF NOT EXISTS data_scope VARCHAR(32) NOT NULL DEFAULT 'all';
|
||||
```
|
||||
|
||||
**管理界面适配:** 角色权限分配界面新增"数据范围"下拉选项,管理员为每个权限分配时选择 self/department/department_tree/all。
|
||||
|
||||
**查询注入:** `data_service.rs` 的 `list` / `count` / `aggregate` 方法中:
|
||||
|
||||
```
|
||||
1. 从权限检查结果中获取该权限对应的 data_scope 等级
|
||||
2. 如果实体启用了 data_scope:
|
||||
- self: 注入 owner_id / created_by 过滤条件
|
||||
- department: 查询用户所在部门的所有用户 ID,注入 IN 条件
|
||||
- department_tree: 递归查询部门树,注入 IN 条件
|
||||
- all: 无额外条件
|
||||
3. 将条件追加到 dynamic_table 的 SQL 构建中
|
||||
```
|
||||
|
||||
**部门用户缓存:** 使用 Redis 缓存部门-用户映射关系,TTL 10 分钟,避免每次查询都递归查部门树。当部门分配变更时通过 EventBus 事件 (`department.member_changed`) 失效缓存。
|
||||
|
||||
### 5.4 权限 fallback 收紧
|
||||
|
||||
**当前行为(危险):** `data_handler.rs` 中,如果没有实体级权限,fallback 到 `plugin.admin`,获得所有数据访问权。
|
||||
|
||||
**修改后:** 移除 fallback 逻辑。权限检查链改为:
|
||||
|
||||
```
|
||||
1. 检查实体级权限 ({manifest_id}.{entity}.{action})
|
||||
2. 存在 → 通过,附带 data_scope
|
||||
3. 不存在 → 拒绝 (403)
|
||||
```
|
||||
|
||||
`plugin.admin` 只管理插件生命周期(上传/安装/启用/禁用/卸载),不自动获得数据访问权。需要显式分配实体级权限。
|
||||
|
||||
**迁移策略(避免现有管理员失去访问):** 在收紧 fallback 的迁移中,同时执行以下补偿:
|
||||
|
||||
```sql
|
||||
-- 为所有拥有 plugin.admin 权限的角色,自动分配所有已安装插件的实体级权限
|
||||
-- data_scope 默认设为 'all'(管理员级别)
|
||||
INSERT INTO role_permissions (id, role_id, permission_id, tenant_id, data_scope, ...)
|
||||
SELECT gen_random_uuid(), rp.role_id, p.id, rp.tenant_id, 'all', ...
|
||||
FROM role_permissions rp
|
||||
JOIN permissions p ON p.tenant_id = rp.tenant_id
|
||||
WHERE rp.permission_id = (SELECT id FROM permissions WHERE code = 'plugin.admin')
|
||||
AND p.code LIKE 'erp-%' -- 所有插件实体权限
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM role_permissions rp2
|
||||
WHERE rp2.role_id = rp.role_id AND rp2.permission_id = p.id
|
||||
);
|
||||
```
|
||||
|
||||
这确保现有管理员在 fallback 收紧后仍保持完整的数据访问能力。
|
||||
|
||||
### 5.5 涉及文件
|
||||
|
||||
| 文件 | 改动类型 |
|
||||
|------|---------|
|
||||
| `crates/erp-core/src/types.rs` | TenantContext 新增 department_ids 字段 |
|
||||
| `crates/erp-auth/src/middleware/jwt_auth.rs` | JWT claims 解析 department_ids |
|
||||
| `crates/erp-plugin/src/manifest.rs` | data_scope / scope_role / data_scope_levels |
|
||||
| `crates/erp-plugin/src/data_service.rs` | 查询条件注入 |
|
||||
| `crates/erp-plugin/src/handler/data_handler.rs` | 移除权限 fallback |
|
||||
| `crates/erp-plugin/src/dynamic_table.rs` | SQL 构建支持数据范围条件 |
|
||||
| `crates/erp-plugin-crm/plugin.toml` | customer 实体添加 data_scope / owner_id |
|
||||
| `crates/erp-server/migration/src/` | 新增 data_scope 列 + 权限补偿迁移 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 前端页面能力增强
|
||||
|
||||
### 6.1 关联选择器 (entity_select)
|
||||
|
||||
**Schema 扩展:** `PluginFieldSchema` 新增字段:
|
||||
|
||||
```typescript
|
||||
interface PluginFieldSchema {
|
||||
// ...已有字段...
|
||||
ref_entity?: string; // 引用的实体名
|
||||
ref_label_field?: string; // 显示字段
|
||||
ref_search_fields?: string[]; // 搜索字段
|
||||
cascade_from?: string; // 级联过滤来源字段
|
||||
cascade_filter?: string; // 级联过滤目标字段
|
||||
}
|
||||
```
|
||||
|
||||
**新增组件:** `EntitySelect.tsx` — 通用远程搜索选择器
|
||||
|
||||
```
|
||||
Props: pluginId, entity, labelField, searchFields, cascadeFrom?, cascadeFilter?, value?, onChange?
|
||||
内部: listPluginData(pluginId, entity, {search, filter}) → Ant Design Select + showSearch
|
||||
```
|
||||
|
||||
**manifest TOML:**
|
||||
|
||||
```toml
|
||||
[[schema.entities.fields]]
|
||||
name = "customer_id"
|
||||
field_type = "uuid"
|
||||
display_name = "所属客户"
|
||||
ui_widget = "entity_select"
|
||||
ref_entity = "customer"
|
||||
ref_label_field = "name"
|
||||
ref_search_fields = ["name", "code"]
|
||||
|
||||
[[schema.entities.fields]]
|
||||
name = "contact_id"
|
||||
field_type = "uuid"
|
||||
display_name = "关联联系人"
|
||||
ui_widget = "entity_select"
|
||||
ref_entity = "contact"
|
||||
ref_label_field = "name"
|
||||
ref_search_fields = ["name"]
|
||||
cascade_from = "customer_id" # 选了客户后自动过滤
|
||||
cascade_filter = "customer_id"
|
||||
```
|
||||
|
||||
### 6.2 Kanban 看板页面
|
||||
|
||||
**Schema 扩展:** `PluginPageType` 新增 `Kanban` 变体。
|
||||
|
||||
**manifest TOML:**
|
||||
|
||||
```toml
|
||||
[[ui.pages]]
|
||||
type = "kanban"
|
||||
entity = "customer"
|
||||
label = "销售漏斗"
|
||||
icon = "swap"
|
||||
lane_field = "level"
|
||||
lane_order = ["potential", "normal", "vip", "svip"]
|
||||
card_title_field = "name"
|
||||
card_subtitle_field = "code"
|
||||
card_fields = ["name", "code", "region", "status"]
|
||||
enable_drag = true
|
||||
```
|
||||
|
||||
**新增组件:** `PluginKanbanPage.tsx`
|
||||
|
||||
- 使用 `@dnd-kit/core` + `@dnd-kit/sortable` 实现跨列拖拽
|
||||
- 每列使用 Ant Design Card 渲染卡片
|
||||
- 每列内支持虚拟滚动(节点数 > 50 时)
|
||||
- 拖拽结束调用 `PATCH /plugins/{id}/{entity}/{recordId}` 更新 lane_field 值
|
||||
|
||||
**后端新增:** `PATCH` 部分更新端点(当前只有 PUT 全量更新):
|
||||
|
||||
```
|
||||
PATCH /api/v1/plugins/{plugin_id}/{entity}/{id}
|
||||
Body: { "data": { "level": "vip" }, "version": 3 }
|
||||
```
|
||||
|
||||
与 PUT 的区别:PATCH 只更新 data 中提供的字段,未提供的字段保持不变。
|
||||
|
||||
### 6.3 批量操作
|
||||
|
||||
**CRUD 页面增强:** `PluginCRUDPage.tsx` 新增 `rowSelection` 和批量操作栏。
|
||||
|
||||
**manifest TOML:**
|
||||
|
||||
```toml
|
||||
[[ui.pages]]
|
||||
type = "crud"
|
||||
entity = "customer"
|
||||
enable_batch = true
|
||||
|
||||
[[ui.pages.batch_actions]]
|
||||
label = "批量删除"
|
||||
action = "batch_delete"
|
||||
permission = "customer.manage"
|
||||
confirm = true
|
||||
|
||||
[[ui.pages.batch_actions]]
|
||||
label = "批量修改状态"
|
||||
action = "batch_update"
|
||||
update_field = "status"
|
||||
permission = "customer.manage"
|
||||
```
|
||||
|
||||
**后端新增:** `POST /api/v1/plugins/{id}/{entity}/batch`
|
||||
|
||||
```rust
|
||||
pub enum BatchAction {
|
||||
BatchDelete { ids: Vec<Uuid> },
|
||||
BatchUpdate { ids: Vec<Uuid>, data: serde_json::Value },
|
||||
}
|
||||
```
|
||||
|
||||
批量操作在单个事务中执行,有上限(默认 100 条)。
|
||||
|
||||
### 6.4 visible_when 表达式增强
|
||||
|
||||
**当前:** 只支持 `field == 'value'` 单一等式。
|
||||
|
||||
**增强后支持:**
|
||||
|
||||
```toml
|
||||
visible_when = "customer_type == 'enterprise'"
|
||||
visible_when = "customer_type == 'enterprise' AND level == 'vip'"
|
||||
visible_when = "status == 'active' OR status == 'pending'"
|
||||
visible_when = "NOT status == 'blacklist'"
|
||||
visible_when = "customer_type == 'enterprise' AND (level == 'vip' OR level == 'svip')"
|
||||
```
|
||||
|
||||
**前端实现:** 新建 `exprEvaluator.ts`,约 100 行递归下降表达式解析器:
|
||||
|
||||
```typescript
|
||||
interface ExprNode {
|
||||
type: 'eq' | 'and' | 'or' | 'not';
|
||||
field?: string;
|
||||
value?: string;
|
||||
left?: ExprNode;
|
||||
right?: ExprNode;
|
||||
operand?: ExprNode;
|
||||
}
|
||||
|
||||
function parseExpr(input: string): ExprNode;
|
||||
function evaluateExpr(node: ExprNode, values: Record<string, unknown>): boolean;
|
||||
```
|
||||
|
||||
不引入外部依赖,不使用 eval。
|
||||
|
||||
### 6.5 Dashboard 图表增强
|
||||
|
||||
**Schema 扩展:** Dashboard 页面支持 widgets 声明:
|
||||
|
||||
```toml
|
||||
[[ui.pages]]
|
||||
type = "dashboard"
|
||||
label = "统计概览"
|
||||
|
||||
[[ui.pages.widgets]]
|
||||
type = "stat_card"
|
||||
entity = "customer"
|
||||
title = "客户总数"
|
||||
icon = "team"
|
||||
color = "#4F46E5"
|
||||
|
||||
[[ui.pages.widgets]]
|
||||
type = "bar_chart"
|
||||
entity = "customer"
|
||||
title = "客户地区分布"
|
||||
dimension_field = "region"
|
||||
metric = "count"
|
||||
|
||||
[[ui.pages.widgets]]
|
||||
type = "pie_chart"
|
||||
entity = "customer"
|
||||
title = "客户类型分布"
|
||||
dimension_field = "customer_type"
|
||||
metric = "count"
|
||||
|
||||
[[ui.pages.widgets]]
|
||||
type = "funnel_chart"
|
||||
entity = "customer"
|
||||
title = "客户等级漏斗"
|
||||
dimension_field = "level"
|
||||
dimension_order = ["potential", "normal", "vip", "svip"]
|
||||
metric = "count"
|
||||
```
|
||||
|
||||
**图表库:** 使用 `@ant-design/charts`(Ant Design 生态一致,支持按需引入)。
|
||||
|
||||
**后端新增:** timeseries 聚合 API:
|
||||
|
||||
```
|
||||
GET /api/v1/plugins/{id}/{entity}/timeseries?time_field=occurred_at&time_grain=week&start=2026-01-01&end=2026-04-17
|
||||
|
||||
响应:{ "data": [{ "period": "2026-W01", "count": 12 }, ...] }
|
||||
```
|
||||
|
||||
SQL 实现:`date_trunc('week', (data->>'occurred_at')::timestamp)`
|
||||
|
||||
**数据钻取:** 图表点击维度值时跳转到 CRUD 页面并自动带上筛选条件。`PluginCRUDPage` 支持从 URL query 参数初始化筛选。
|
||||
|
||||
### 6.6 前端文件拆分
|
||||
|
||||
| 当前文件 | 行数 | 拆分方案 |
|
||||
|---------|------|---------|
|
||||
| `PluginGraphPage.tsx` | 1081 | → `graphRenderer.ts` + `graphLayout.ts` + `graphInteraction.ts` |
|
||||
| `PluginCRUDPage.tsx` | 617 | → `CrudTable.tsx` + `CrudForm.tsx` + `CrudDetail.tsx` |
|
||||
| `PluginDashboardPage.tsx` | 647 | → `DashboardWidgets.tsx` + `dashboardTypes.ts` |
|
||||
|
||||
拆分后每个文件控制在 400 行以内。
|
||||
|
||||
### 6.7 涉及文件
|
||||
|
||||
| 文件 | 改动类型 |
|
||||
|------|---------|
|
||||
| `apps/web/src/components/EntitySelect.tsx` | 新增 |
|
||||
| `apps/web/src/pages/PluginKanbanPage.tsx` | 新增 |
|
||||
| `apps/web/src/utils/exprEvaluator.ts` | 新增 |
|
||||
| `apps/web/src/pages/PluginCRUDPage.tsx` | 重构 — 拆分 + 批量操作 + entity_select + visible_when |
|
||||
| `apps/web/src/pages/PluginGraphPage.tsx` | 重构 — 拆分 |
|
||||
| `apps/web/src/pages/PluginDashboardPage.tsx` | 重构 — 图表 + 拆分 |
|
||||
| `apps/web/src/pages/PluginTreePage.tsx` | 优化 — 懒加载 |
|
||||
| `apps/web/src/api/plugins.ts` | Schema 类型扩展 |
|
||||
| `apps/web/src/api/pluginData.ts` | 新增 batch / timeseries / cursor API |
|
||||
| `apps/web/src/App.tsx` | Kanban 路由注册 |
|
||||
| `crates/erp-plugin/src/handler/data_handler.rs` | 新增 PATCH / batch 端点 |
|
||||
| `crates/erp-plugin/src/data_service.rs` | batch / timeseries / partial update(PATCH 只合并 data 中的字段) |
|
||||
| `crates/erp-plugin/src/dynamic_table.rs` | 新增 `build_patch_sql` 部分更新 SQL 构建器 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 风险与缓解
|
||||
|
||||
| 风险 | 概率 | 影响 | 缓解措施 |
|
||||
|------|------|------|---------|
|
||||
| Generated Column 的 ALTER TABLE 锁表 | 中 | 中 | 插件安装时在低峰期执行;万级数据以内锁表时间 < 1s |
|
||||
| pg_trgm 索引空间开销(约 2-3x 原始文本) | 低 | 低 | 只为 searchable 的短文本字段创建 |
|
||||
| 行级权限的部门查询性能 | 中 | 中 | Redis 缓存部门树,TTL 10 分钟 |
|
||||
| 批量操作事务过大 | 低 | 中 | 上限 100 条;超过则分批执行 |
|
||||
| 前端重构引入回归 | 中 | 高 | 逐文件拆分,每步验证现有功能不变 |
|
||||
|
||||
---
|
||||
|
||||
## 8. 不在范围内(后续版本)
|
||||
|
||||
以下内容在本次设计中**不涉及**,记录为已知需求:
|
||||
|
||||
- WASM Guest 业务逻辑增强 (L2/L3 插件模型)
|
||||
- 插件版本升级迁移框架
|
||||
- 跨插件通信 (事件契约 + 只读查询)
|
||||
- 插件间 RPC / 自定义 API 端点
|
||||
- 插件市场 / 分发架构
|
||||
- CRM 新增实体 (lead / opportunity / activity)
|
||||
- WIT 接口版本化
|
||||
- 图谱 LOD + WebGL 渲染
|
||||
- Iframe / Web Component 自定义 UI
|
||||
|
||||
这些将在后续的设计规格中详细展开。
|
||||
@@ -1,456 +0,0 @@
|
||||
# ERP 平台底座 — 全面成熟度提升路线图
|
||||
|
||||
> 创建日期:2026-04-17
|
||||
> 状态:审查修订完成
|
||||
> 范围:安全、架构、测试、前端体验、插件生态 — 3 季度分层推进
|
||||
|
||||
---
|
||||
|
||||
## 1. 背景与目标
|
||||
|
||||
### 1.1 项目现状
|
||||
|
||||
ERP 平台底座已完成 Phase 1-6 基础设施建设 + WASM 插件系统集成 + CRM 客户管理插件。当前具备:
|
||||
|
||||
- 6 个业务模块(auth, config, workflow, message, plugin, server)
|
||||
- 36 个数据库迁移
|
||||
- 完整的 WASM 插件运行时
|
||||
- Schema 驱动的动态前端(6 种页面类型)
|
||||
- React 19 + Ant Design 6 + Zustand 5 前端 SPA
|
||||
|
||||
### 1.2 分析发现摘要
|
||||
|
||||
| 维度 | 评分 | 关键问题 |
|
||||
|------|------|---------|
|
||||
| 架构健壮性 | 8/10 | ErpModule trait 死代码、路由注册未自动化 |
|
||||
| 代码质量 | 7/10 | N+1 查询、错误映射过宽、 oversized 组件 |
|
||||
| 安全性 | 5/10 | 3 个 CRITICAL(硬编码密钥/密码)、4 个 HIGH |
|
||||
| 测试覆盖 | 4/10 | 零数据库集成测试、关键流程未覆盖 |
|
||||
| 前端体验 | 7/10 | 无 i18n、无 Error Boundary、无虚拟滚动 |
|
||||
| 基础设施 | 4/10 | 无 CI/CD、Wiki 过时、大量未跟踪文件 |
|
||||
|
||||
### 1.3 目标
|
||||
|
||||
通过 3 个季度的分层改进,将平台从"功能完整"推进到"生产就绪":
|
||||
|
||||
- **Q2(4-5月)**:消除安全风险,建立自动化质量门
|
||||
- **Q3(6-8月)**:强化架构,提升前端工程化水平
|
||||
- **Q4(9-11月)**:补齐测试覆盖,扩展插件生态
|
||||
|
||||
### 1.4 约束
|
||||
|
||||
- **独立开发者** + Claude 辅助 — 每季度聚焦单一维度
|
||||
- **SaaS 优先**部署 — 多租户安全是硬性要求
|
||||
- **不破坏现有功能** — 所有改进必须向后兼容
|
||||
|
||||
---
|
||||
|
||||
## 2. Q2:安全地基 + CI/CD(4-5月)
|
||||
|
||||
### 2.1 密钥外部化与启动强制检查
|
||||
|
||||
**问题:**
|
||||
- JWT 密钥 `"change-me-in-production"` 硬编码在 `crates/erp-server/config/default.toml`
|
||||
- 管理员密码 `"Admin@2026"` 硬编码 + fallback
|
||||
- 数据库凭据 `postgres://erp:erp_dev_2024@...` 硬编码
|
||||
- `.test_token` 含有效 admin JWT 提交到仓库
|
||||
|
||||
**方案:**
|
||||
|
||||
1. **配置强制化**:`default.toml` 只保留开发环境默认值。生产敏感值通过环境变量 `ERP__` 前缀注入(已有机制)
|
||||
2. **启动检查**:服务启动时检测 JWT 密钥是否为默认值,若是则 **拒绝启动**(返回错误退出码,不只是警告)
|
||||
3. **密码初始化**:`seed_tenant_auth` 从环境变量 `ERP__SUPER_ADMIN_PASSWORD` 读取初始密码(与现有 `module.rs:149` 中的变量名一致),未设置则拒绝初始化(移除 fallback 到硬编码值的逻辑)
|
||||
4. **清理 `.test_token`**:立即加入 `.gitignore`。验证该文件是否曾被提交到 git 历史 — 如果曾提交,需使用 BFG Repo-Cleaner 清理历史(因包含用硬编码密钥签名的 admin JWT,等同于密钥泄露)
|
||||
5. **`default.toml` 占位符**:敏感字段改为 `"__MUST_SET_VIA_ENV__"` 之类的明显占位值
|
||||
|
||||
**验证标准:**
|
||||
- 默认配置启动时服务拒绝运行
|
||||
- 环境变量设置后正常启动
|
||||
- `.test_token` 不再出现在仓库中
|
||||
|
||||
### 2.2 Gitea Actions CI/CD
|
||||
|
||||
**流水线设计:**
|
||||
|
||||
```yaml
|
||||
name: CI
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
rust-check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- run: cargo fmt --check --all
|
||||
- run: cargo clippy -- -D warnings
|
||||
|
||||
rust-test:
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16
|
||||
env: { POSTGRES_DB: erp_test, POSTGRES_USER: test, POSTGRES_PASSWORD: test }
|
||||
ports: ["5432:5432"]
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- run: cargo test --workspace
|
||||
|
||||
frontend-build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with: { node-version: 20 }
|
||||
- run: cd apps/web && pnpm install && pnpm build
|
||||
|
||||
security-audit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- run: cargo audit
|
||||
- uses: actions/setup-node@v4
|
||||
with: { node-version: 20 }
|
||||
- run: cd apps/web && pnpm audit
|
||||
```
|
||||
|
||||
**关键决策:**
|
||||
- 使用 Gitea Actions(与 GitHub Actions 语法兼容)
|
||||
- 每个 job 包含 `actions/checkout@v4` + 对应语言 toolchain setup
|
||||
- Rust 使用 `Swatinem/rust-cache@v2` 缓存编译产物,避免每次全量编译
|
||||
- PostgreSQL 通过 service 容器提供
|
||||
- 四个 job 并行运行,互不依赖
|
||||
- 后续可扩展:Redis service、Playwright E2E、Docker 镜像构建推送
|
||||
|
||||
### 2.3 审计日志补全
|
||||
|
||||
**当前缺口与改进:**
|
||||
|
||||
| 缺口 | 改进方案 |
|
||||
|------|---------|
|
||||
| 登录/登出只发 DomainEvent,不写审计日志 | 在 `auth_service` 的 login/logout/change_password 中调用 `audit_service::record()` |
|
||||
| 审计日志缺少 `old_value`/`new_value` | 关键实体(user/role/permission/org)的 update 操作添加 `.with_changes(old, new)`。序列化完整的旧模型和新模型为 JSON,由审计日志消费者计算 diff — 比应用层计算细粒度 diff 更简单健壮 |
|
||||
| 缺少 IP 地址和 User-Agent | `AuditLogBuilder::with_request_info()` 在 handler 层传入请求上下文 |
|
||||
| 插件 CRUD 无审计 | `data_service` 的 create/update/delete 操作添加审计日志记录 |
|
||||
| 登录失败无记录 | 添加失败登录审计(含尝试的用户名/IP),用于入侵检测 |
|
||||
|
||||
**验证标准:**
|
||||
- 登录成功/失败均写入审计日志
|
||||
- 用户更新操作记录变更前后值
|
||||
- 审计日志包含 IP 和 User-Agent
|
||||
|
||||
### 2.4 Docker 生产化
|
||||
|
||||
| 改进项 | 当前 | 目标 |
|
||||
|--------|------|------|
|
||||
| PostgreSQL 端口 | `ports: "5432:5432"` 暴露到宿主机 | 移除 `ports:`,使用 Docker 网络内部通信 |
|
||||
| Redis 端口 | `ports: "6379:6379"` 无认证 | 移除 `ports:`,添加 `--requirepass` |
|
||||
| 容器资源限制 | 无 | CPU 1核 / 内存 512MB |
|
||||
| 应用镜像 | 无 Dockerfile | 多阶段构建:Rust build → 精简 runtime 镜像 |
|
||||
| Redis 宕机时限流 | fail-open(无限流) | fail-closed(拒绝请求) |
|
||||
|
||||
**限流 fail-closed 改动:**
|
||||
`crates/erp-server/src/middleware/rate_limit.rs` 中 Redis 不可用时,返回 `429 Too Many Requests` 而非放行。
|
||||
|
||||
### 2.5 多租户安全加固
|
||||
|
||||
| 问题 | 改进方案 |
|
||||
|------|---------|
|
||||
| 登录使用硬编码 `default_tenant_id` | 登录接口增加租户解析(从子域名/请求头 `X-Tenant-ID`) |
|
||||
| `auth_service::refresh()` 用户查询缺少 tenant_id(`auth_service.rs:177`) | `find_by_id` 添加 `.filter(user::Column::TenantId.eq(claims.tenant_id))` |
|
||||
| 内存级 tenant_id 过滤(`user_service.rs` 的 `get_by_id`/`update`/`delete`) | 改为数据库级 `.filter(Column::TenantId.eq(tenant_id))` 查询。注意:`login`/`list`/`assign_roles` 已正确使用数据库级过滤,无需修改 |
|
||||
|
||||
**涉及文件:**
|
||||
- `crates/erp-auth/src/handler/auth_handler.rs`
|
||||
- `crates/erp-auth/src/service/auth_service.rs`
|
||||
- `crates/erp-auth/src/service/user_service.rs`
|
||||
- `crates/erp-auth/src/middleware/jwt_auth.rs`
|
||||
|
||||
---
|
||||
|
||||
## 3. Q3:架构强化 + 前端体验(6-8月)
|
||||
|
||||
### 3.1 ErpModule Trait 重构
|
||||
|
||||
**当前问题:**
|
||||
- `register_event_handlers` 是死代码 — 所有模块实现为空操作
|
||||
- 路由注册需在 `main.rs` 手动编辑两处
|
||||
- 事件订阅在 `main.rs` 中手动调用,绕过 trait
|
||||
|
||||
**改进方案:**
|
||||
|
||||
基于当前 trait 签名(`erp-core/src/module.rs`),新增双路由注册和权限声明。保持与现有 `ModuleContext` 参数一致,不引入 `AppState` 依赖(避免 `erp-core` → `erp-server` 反向依赖):
|
||||
|
||||
```rust
|
||||
pub trait ErpModule: Send + Sync + 'static {
|
||||
// 保留已有方法
|
||||
fn name(&self) -> &str;
|
||||
fn version(&self) -> &str;
|
||||
fn module_type(&self) -> &str { "business" }
|
||||
fn dependencies(&self) -> Vec<&str> { vec![] }
|
||||
fn id(&self) -> Uuid { /* 默认实现 */ }
|
||||
fn as_any(&self) -> &dyn Any;
|
||||
|
||||
// 新增:双路由注册(匹配现有 public/protected 分离模式)
|
||||
fn register_public_routes(&self, router: Router) -> Router { router }
|
||||
fn register_protected_routes(&self, router: Router) -> Router { router }
|
||||
|
||||
// 重构:事件订阅真正生效(当前所有模块实现为空操作)
|
||||
fn register_event_handlers(&self, bus: &EventBus) {}
|
||||
|
||||
// 新增:模块权限声明
|
||||
fn permissions(&self) -> Vec<PermissionDef> { vec![] }
|
||||
|
||||
// 保留已有生命周期钩子(保持 ModuleContext 参数签名)
|
||||
async fn on_startup(&self, _ctx: &ModuleContext) -> AppResult<()> { Ok(()) }
|
||||
async fn on_shutdown(&self) -> AppResult<()> { Ok(()) }
|
||||
async fn on_tenant_created(&self, _tenant_id: Uuid, _db: &DatabaseConnection, _bus: &EventBus) -> AppResult<()> { Ok(()) }
|
||||
async fn on_tenant_deleted(&self, _tenant_id: Uuid, _db: &DatabaseConnection, _bus: &EventBus) -> AppResult<()> { Ok(()) }
|
||||
async fn health_check(&self) -> AppResult<serde_json::Value> { Ok(serde_json::json!({})) }
|
||||
}
|
||||
```
|
||||
|
||||
`ModuleRegistry::build()` 自动收集路由、事件处理器和权限,`main.rs` 简化为:
|
||||
|
||||
```rust
|
||||
let (registry, public_routes, protected_routes) = ModuleRegistry::new()
|
||||
.register(auth_module)
|
||||
.register(config_module)
|
||||
.register(workflow_module)
|
||||
.register(message_module)
|
||||
.register(plugin_module)
|
||||
.build();
|
||||
|
||||
// 自动组合:public_routes 直接挂载,protected_routes 包裹 JWT 中间件
|
||||
let app = Router::new()
|
||||
.merge(public_routes)
|
||||
.merge(protected_routes.layer(jwt_middleware));
|
||||
```
|
||||
|
||||
**迁移策略:** 逐模块迁移 — 每个模块从静态 `public_routes()`/`protected_routes()` 函数改为 trait 方法实现,`main.rs` 逐步简化。
|
||||
|
||||
**已知例外:** PluginModule 的两阶段初始化(先注册再启动事件监听器)在初期保持独立处理,不强行纳入自动化。`MessageModule::start_event_listener`、`WorkflowModule::start_timeout_checker`、`outbox::start_outbox_relay` 等独立生命周期钩子作为范围排除项,后续迭代再统一。
|
||||
|
||||
**迁移策略:** 逐模块迁移 — 每个模块从静态函数改为 trait 方法实现,`main.rs` 逐步简化。
|
||||
|
||||
### 3.2 错误映射修正 + N+1 查询优化
|
||||
|
||||
**错误映射修正:**
|
||||
|
||||
当前 `erp-auth` 服务中直接 `.map_err(|e| AuthError::Validation(e.to_string()))` 将所有 `DbErr` 映射为 `Validation`,绕过了 `erp-core` 中已有的 `From<DbErr> for AppError` 语义映射(该映射已正确处理 `RecordNotFound` → `NotFound`、重复键 → `Conflict`)。
|
||||
|
||||
**修复策略:** `erp-auth` 服务层停止手动包装 `DbErr`,改为通过 `?` 操作符依赖 `DbErr → AppError` 的核心映射,通过现有的 `From<AuthError> for AppError` 转换传播。这样数据库连接错误会正确显示为 `Internal`,唯一约束冲突会正确显示为 `Conflict`。
|
||||
|
||||
移除 `From<AppError> for AuthError` 的反向映射(当前是 lossy wrapping — `AppError::NotFound` 变为 `AuthError::Validation`,丢失语义信息)。
|
||||
|
||||
**N+1 查询优化:**
|
||||
|
||||
`user_service.rs` 的 `list()` 方法改为批量查询:
|
||||
1. 先查询当前页用户列表
|
||||
2. 收集所有 `user_id`
|
||||
3. 一次 `WHERE user_id IN (...)` 查询 `user_role` + `role`
|
||||
4. 内存中按 `user_id` 分组组装
|
||||
|
||||
从 N+1 查询降为 3 次固定查询(用户列表 + 角色关联 + 角色详情)。
|
||||
|
||||
### 3.3 前端 Error Boundary + hooks 提取
|
||||
|
||||
**Error Boundary:**
|
||||
- `App.tsx` 根组件包裹全局 Error Boundary(捕获未预期崩溃)
|
||||
- 每个懒加载页面外包裹页面级 Error Boundary(隔离单页面崩溃)
|
||||
- 失败时展示友好错误页面 + 重试按钮
|
||||
|
||||
**hooks 提取:**
|
||||
|
||||
| Hook | 提取来源 | 用途 |
|
||||
|------|---------|------|
|
||||
| `usePaginatedData<T>` | 6+ 页面的分页加载逻辑 | 统一分页/搜索/加载状态 |
|
||||
| `useDarkMode` | 8+ 文件的 `token.colorBgContainer` 字符串比较 | 提供可靠的 boolean 暗色模式判断 |
|
||||
| `useCountUp` | Home.tsx + DashboardWidgets 重复实现 | 计数动画复用 |
|
||||
| `useDebouncedValue` | Users.tsx 等搜索输入 | 防抖搜索,避免每次按键触发 API |
|
||||
| `useApiRequest` | 所有页面的 try/catch + message.error | 统一 API 错误处理和消息提示 |
|
||||
|
||||
### 3.4 i18n 基础设施搭建
|
||||
|
||||
**方案:react-i18next**
|
||||
|
||||
- 安装 `react-i18next` + `i18next`
|
||||
- 创建 `locales/zh-CN.json`,提取所有硬编码中文为 key
|
||||
- 配置 i18next 初始化,默认 `zh-CN`
|
||||
- 用 `useTranslation()` hook 替换硬编码字符串
|
||||
|
||||
**实施策略:** 增量式 — 新页面强制使用 i18n,旧页面按模块逐步迁移。不强求一次性替换。
|
||||
|
||||
**命名规范:**
|
||||
- 页面文案:`{module}.{page}.{element}` 如 `auth.login.username`
|
||||
- 通用文案:`common.{action}` 如 `common.save`, `common.cancel`
|
||||
- 错误消息:`error.{type}` 如 `error.network`, `error.unauthorized`
|
||||
|
||||
### 3.5 行级数据权限接线
|
||||
|
||||
**当前状态:** 数据库列、SQL 条件构建器、manifest 声明已就绪,handler 层有 TODO 未实现。
|
||||
|
||||
**完成步骤:**
|
||||
1. JWT 中间件注入 `department_ids`(完成 `jwt_auth.rs:50` 的 TODO)
|
||||
2. `data_handler` 查询接口注入 data scope 条件
|
||||
3. 前端角色权限编辑页添加 `data_scope` 选择控件
|
||||
4. 端到端验证:创建测试角色 → 设置数据范围 → 验证查询过滤
|
||||
|
||||
### 3.6 前端共享类型统一
|
||||
|
||||
- `PaginatedResponse<T>` 从 `users.ts` 提取到 `api/types.ts`
|
||||
- 错误提取工具函数 `extractErrorMessage(err: unknown): string` → `api/errors.ts`
|
||||
- 插件 Schema 类型定义集中到 `types/plugin.ts`
|
||||
- 移除 `api/client.ts` 中已废弃的 `CancelToken`,改用 `AbortController`
|
||||
|
||||
---
|
||||
|
||||
## 4. Q4:测试覆盖 + 插件生态(9-11月)
|
||||
|
||||
### 4.1 Q4 范围调整说明
|
||||
|
||||
Q4 原始范围较大(Testcontainers + Playwright + 进销存插件 + 热更新 + 文档清理)。调整为两个子阶段:
|
||||
|
||||
- **Q4a(9-10月)**:测试基础设施 — Testcontainers 集成测试框架 + Playwright E2E + 文档清理
|
||||
- **Q4b(11月+)**:插件生态 — 进销存插件 + 热更新
|
||||
|
||||
热更新功能可视 Q4a 进度推迟到 Q1 2027,避免在单季度内承载过多工作。
|
||||
|
||||
### 4.2 数据库集成测试框架
|
||||
|
||||
**方案:Testcontainers + PostgreSQL**
|
||||
|
||||
创建 `crates/erp-server/tests/integration/` 目录,使用 `testcontainers` crate 启动真实 PostgreSQL 容器。
|
||||
|
||||
**测试基座:**
|
||||
- 每个测试套件共享一个 PostgreSQL 容器
|
||||
- 自动运行所有迁移
|
||||
- 提供 `setup_test_db()` 辅助函数返回连接池
|
||||
- 测试结束自动清理
|
||||
|
||||
**覆盖优先级:**
|
||||
|
||||
| 优先级 | 模块 | 测试场景 |
|
||||
|--------|------|---------|
|
||||
| P0 | erp-auth | 用户 CRUD、角色权限分配、登录/JWT 完整流程 |
|
||||
| P0 | erp-auth | 多租户隔离 — 租户 A 数据对租户 B 不可见 |
|
||||
| P0 | erp-plugin | 插件生命周期(install→enable→disable→uninstall) |
|
||||
| P1 | erp-auth | 乐观锁并发冲突、软删除恢复 |
|
||||
| P1 | erp-plugin | 行级数据权限过滤、JSONB 查询/索引 |
|
||||
| P1 | erp-plugin | 动态表 DDL 正确性(generated column、pg_trgm 索引) |
|
||||
| P1 | erp-workflow | 流程实例启动、任务完成、网关分支 |
|
||||
| P1 | erp-core | 事件总线发布/订阅端到端、outbox relay 补偿 |
|
||||
|
||||
### 4.2 核心流程 E2E 测试
|
||||
|
||||
**方案:Playwright**
|
||||
|
||||
放在 `apps/web/e2e/` 目录,CI 中作为独立 job 运行。
|
||||
|
||||
**覆盖场景(4 个关键旅程):**
|
||||
|
||||
| 场景 | 步骤 |
|
||||
|------|------|
|
||||
| 完整登录流程 | 打开登录页 → 输入密码 → 验证 token → 刷新 token → 登出 → 验证跳转 |
|
||||
| 用户管理闭环 | 创建用户 → 分配角色 → 搜索用户 → 编辑 → 软删除 → 验证列表不显示 |
|
||||
| 插件安装流程 | 上传 WASM → 安装 → 验证菜单出现 → 数据 CRUD → 卸载 → 验证菜单消失 |
|
||||
| 多租户隔离 | 租户 A 创建用户 → 切换租户 B → 验证查询结果为空 |
|
||||
|
||||
### 4.3 第二个行业插件 — 进销存(Inventory)
|
||||
|
||||
**选择理由:**
|
||||
- 与 CRM 有天然关联(客户 → 订单 → 出库)
|
||||
- 实体数量适中(5-8 个),复杂度可控
|
||||
- 能验证插件系统的复用性和跨实体关联能力
|
||||
- 为后续财务模块铺垫
|
||||
|
||||
**实体设计:**
|
||||
|
||||
| 实体 | 字段 | 关联 |
|
||||
|------|------|------|
|
||||
| product 商品 | 名称/编码/规格/单位/分类/售价/成本价 | — |
|
||||
| warehouse 仓库 | 名称/地址/负责人/状态 | — |
|
||||
| stock 库存 | 商品/仓库/数量/成本/预警线 | → product, warehouse |
|
||||
| purchase_order 采购单 | 供应商/总金额/状态/日期 | → supplier(CRM), stock |
|
||||
| sales_order 销售单 | 客户/总金额/状态/日期 | → customer(CRM), stock |
|
||||
| supplier 供应商 | 名称/编码/联系方式/地址 | — |
|
||||
|
||||
**需要验证的插件能力:**
|
||||
- 跨实体关联(订单 → 商品 → 库存联动)
|
||||
- 事务性事件(库存扣减在订单确认时原子执行)
|
||||
- 页面间导航(从订单跳转客户详情)
|
||||
- 报表/统计页面(库存汇总、进销存明细)
|
||||
|
||||
### 4.4 插件热更新能力
|
||||
|
||||
**当前限制:** 更新插件需要完整 uninstall/reinstall。
|
||||
|
||||
**改进方案:**
|
||||
- 新增 `POST /api/v1/admin/plugins/{id}/upgrade` 端点
|
||||
- 升级流程:上传新 WASM → 对比 manifest schema → 增量 DDL(ADD COLUMN 等) → 热替换 WASM 模块
|
||||
- 数据安全:`tenant_id` 数据不丢失
|
||||
- 版本兼容性检查:新版本必须向后兼容或提供迁移脚本
|
||||
|
||||
**回滚策略:** 升级前创建 schema 备份点。升级流程分两步执行:
|
||||
1. 先暂存新 WASM 并尝试验证初始化(不应用 DDL)
|
||||
2. 初始化成功后,在单事务中执行 DDL 变更 + 状态转换
|
||||
3. 如果新 WASM 初始化失败,保持旧 WASM 继续运行,回滚暂存状态
|
||||
4. DDL 已应用但 WASM 运行异常时,保留旧 WASM 可加载作为 fallback
|
||||
|
||||
### 4.5 文档更新与清理
|
||||
|
||||
| 项目 | 改进 |
|
||||
|------|------|
|
||||
| Wiki 文档 | 全面更新到当前状态(前端路由、测试数量、模块能力、插件系统) |
|
||||
| CLAUDE.md | 版本号修正(React 19 / Ant Design 6) |
|
||||
| 根目录清理 | 删除未跟踪的开发临时文件(截图、heap dump、perf trace、agent plan 文件) |
|
||||
| integration-tests/ | 验证现有测试是否能编译。若已失效则删除,用新的 Testcontainers 框架替代;若仍有效则纳入 Cargo workspace |
|
||||
| N+1 查询(plugin) | `plugin_service.rs` 的列表查询也存在 N+1 问题(每条插件单独查询 entities),需一并优化 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 风险与缓解
|
||||
|
||||
| 风险 | 概率 | 影响 | 缓解措施 |
|
||||
|------|------|------|---------|
|
||||
| 安全修复引入新 bug | 中 | 高 | 每个修复配有对应的测试用例 |
|
||||
| ErpModule trait 重构影响所有模块 | 高 | 中 | 逐模块迁移,每步验证 `cargo test` |
|
||||
| i18n 迁移工作量大 | 中 | 低 | 增量式,不追求一次性完成 |
|
||||
| Testcontainers 在 CI 环境不稳定 | 低 | 中 | 本地开发可跳过集成测试,CI 用 service container 兜底 |
|
||||
| Testcontainers 在 Windows (WSL2) 上兼容性 | 中 | 中 | 主开发环境为 Windows 11,Testcontainers 对 Windows 支持有限。本地开发可依赖 CI service container 运行集成测试,或使用 WSL2 环境 |
|
||||
| 进销存插件实体设计变更 | 中 | 低 | 先完成最小实体集,后续迭代扩展 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 成功标准
|
||||
|
||||
**Q2 完成标准:**
|
||||
- [ ] 3 个 CRITICAL 安全问题全部修复
|
||||
- [ ] Gitea Actions CI/CD 流水线运行通过
|
||||
- [ ] 默认配置启动被拒绝
|
||||
- [ ] 登录/登出写入审计日志
|
||||
- [ ] Docker 生产化配置就绪
|
||||
|
||||
**Q3 完成标准:**
|
||||
- [ ] ErpModule trait 路由注册自动化
|
||||
- [ ] N+1 查询优化,用户列表查询次数固定为 3
|
||||
- [ ] 前端 Error Boundary 覆盖全局 + 页面级
|
||||
- [ ] 5 个自定义 hooks 提取完成
|
||||
- [ ] i18n 基础设施可用,至少 1 个页面完成迁移
|
||||
- [ ] 行级数据权限端到端验证通过
|
||||
|
||||
**Q4 完成标准:**
|
||||
- [ ] 集成测试覆盖 auth + plugin 核心流程
|
||||
- [ ] 4 个 E2E 测试场景通过
|
||||
- [ ] 进销存插件 6 个实体可用
|
||||
- [ ] 插件热更新功能可用
|
||||
- [ ] Wiki 文档与代码同步
|
||||
@@ -1,604 +0,0 @@
|
||||
# CRM 插件平台标杆 — P0 基础能力设计规格
|
||||
|
||||
> **版本**: v1.1 (修正版 — 基于代码审查发现,对齐现有实现)
|
||||
> **日期**: 2026-04-18
|
||||
> **状态**: Draft
|
||||
> **定位**: 插件平台标杆 — CRM 是试金石,打磨通用能力
|
||||
|
||||
---
|
||||
|
||||
## 1. 背景与动机
|
||||
|
||||
### 1.1 为什么要做这个
|
||||
|
||||
CRM 插件是 ERP 平台的第一个行业插件,当前状态是"客户通讯录 + 标签 + 关系图谱",距离一流 CRM(Salesforce/HubSpot/Pipedrive)有显著差距。但更大的问题是:**CRM 暴露的差距不在于 CRM 本身,而在于插件平台的基础能力缺失。**
|
||||
|
||||
具体来说:
|
||||
- ~~5 个实体之间有明确的 FK 关系,但 manifest 无法声明~~ → **已有 `PluginRelation` + 级联删除**,但缺少 `name`/`display_field`/关系类型等前端渲染信息
|
||||
- 35+ 字段有 required/unique/pattern 校验,但缺少 `min_length`/`max_length`/`min_value`/`max_value` 扩展校验
|
||||
- Dashboard/Graph 页面硬编码了 CRM 专属颜色和标题,第二个插件无法复用
|
||||
- CRM 的 `plugin.toml` 没有声明 `relations`,导致现有级联能力未被使用
|
||||
- 批量删除和 PATCH 部分更新绕过了现有校验
|
||||
|
||||
如果不在 P0 阶段补齐这些基础,所有后续业务功能(商机、合同、报价)都会建在不稳固的地基上。
|
||||
|
||||
### 1.2 设计原则
|
||||
|
||||
| 原则 | 含义 |
|
||||
|------|------|
|
||||
| **平台优先** | 每个能力都是平台层的,CRM 只是第一个使用者 |
|
||||
| **零改动复用** | inventory/生产/财务插件不应为这些能力写任何额外代码 |
|
||||
| **Manifest 驱动** | 所有行为由 plugin.toml 声明驱动,不写硬编码 |
|
||||
| **双层保障** | 前端即时反馈 + 后端最终防线,缺一不可 |
|
||||
|
||||
### 1.3 一流 CRM 差距分析摘要
|
||||
|
||||
| 类别 | 差距 | 本规格是否覆盖 |
|
||||
|------|------|--------------|
|
||||
| 实体关系 + 级联删除 | 致命 — 删除客户产生孤儿数据 | **P0-1 覆盖** |
|
||||
| 字段校验 + FK 完整性 | 严重 — 数据质量无保障 | **P0-2 覆盖** |
|
||||
| 前端通用化 | 中等 — 第二个插件无法复用 Dashboard/Graph | **P0-3 覆盖** |
|
||||
| 商机/漏斗/合同 | 严重 — 核心业务缺失 | P2(本规格不覆盖) |
|
||||
| 导入导出/批量操作 | 中等 — ERP 刚需 | P1(后续规格) |
|
||||
| 全局搜索/保存视图 | 中等 — UX 缺失 | P1(后续规格) |
|
||||
| WASM 活化 | 低 — 当前空操作不影响功能 | P2(后续规格) |
|
||||
|
||||
---
|
||||
|
||||
## 2. P0-1: 实体关系声明 + ref_entity + 级联策略
|
||||
|
||||
### 2.1 Manifest Schema 扩展
|
||||
|
||||
**现有基础**:`PluginRelation` 已存在(`manifest.rs:184-189`),包含 `entity`、`foreign_key`、`on_delete` 三个字段。级联删除已在 `data_service.rs:330-395` 中实现。
|
||||
|
||||
**扩展方向**:在现有结构上新增字段,保持向后兼容。
|
||||
|
||||
```toml
|
||||
# === 一对多关系 (customer → contacts) ===
|
||||
[[schema.entities.relations]]
|
||||
entity = "contact" # 目标实体 (已有字段)
|
||||
foreign_key = "customer_id" # FK 字段 (已有字段)
|
||||
on_delete = "cascade" # cascade | nullify | restrict (已有枚举)
|
||||
# ↓ 新增字段 (可选,向后兼容)
|
||||
name = "contacts" # 关系显示名,用于前端标签
|
||||
type = "one_to_many" # 关系类型 (one_to_many | many_to_one | many_to_many)
|
||||
display_field = "name" # EntitySelect 下拉显示字段
|
||||
|
||||
# === 多对一关系 (contact → customer,含自引用) ===
|
||||
[[schema.entities.relations]]
|
||||
entity = "customer"
|
||||
foreign_key = "parent_id"
|
||||
on_delete = "nullify"
|
||||
name = "parent"
|
||||
type = "many_to_one"
|
||||
display_field = "name"
|
||||
|
||||
# === 多对多关系 (customer ↔ customer,通过中间表) ===
|
||||
[[schema.entities.relations]]
|
||||
entity = "customer"
|
||||
foreign_key = "from_customer_id" # 中间表中的源 FK
|
||||
on_delete = "nullify"
|
||||
name = "related_customers"
|
||||
type = "many_to_many"
|
||||
through_entity = "customer_relationship"
|
||||
through_source_field = "from_customer_id"
|
||||
through_target_field = "to_customer_id"
|
||||
```
|
||||
|
||||
#### 关系类型定义 (新增 `type` 字段)
|
||||
|
||||
| 类型 | 含义 | foreign_key 位置 | CRM 场景 |
|
||||
|------|------|-----------------|---------|
|
||||
| `one_to_many` | 一个父 → 多个子 | 子实体上 | customer → contacts |
|
||||
| `many_to_one` | 多个子 → 一个父 | 本实体上 | contact → customer |
|
||||
| `many_to_many` | 双向多对多 | 中间表上 | customer ↔ customer |
|
||||
|
||||
> `type` 字段为 `Option<RelationType>`,默认 `OneToMany`。不声明则现有行为不变。
|
||||
|
||||
#### 级联策略 (保持现有枚举不变)
|
||||
|
||||
| 策略 | TOML 值 | 行为 | 适用场景 |
|
||||
|------|---------|------|---------|
|
||||
| `Cascade` | `"cascade"` | 子记录 `deleted_at = now()` | 强所有权:客户→联系人 |
|
||||
| `Nullify` | `"nullify"` | FK 字段设 NULL | 弱引用:联系人→上级客户 |
|
||||
| `Restrict` | `"restrict"` | 有子记录时阻止删除(409) | 关键数据:不允许孤立 |
|
||||
|
||||
### 2.2 后端实现
|
||||
|
||||
#### 数据结构扩展 (`manifest.rs`)
|
||||
|
||||
**在现有 `PluginRelation` 上新增字段**(不替换):
|
||||
|
||||
```rust
|
||||
// 现有字段保持不变
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PluginRelation {
|
||||
pub entity: String, // 已有
|
||||
pub foreign_key: String, // 已有
|
||||
pub on_delete: OnDeleteStrategy, // 已有 (Cascade | Nullify | Restrict)
|
||||
// ↓ 新增可选字段
|
||||
#[serde(default)]
|
||||
pub name: Option<String>,
|
||||
#[serde(default, rename = "type")]
|
||||
pub relation_type: Option<RelationType>,
|
||||
#[serde(default)]
|
||||
pub display_field: Option<String>,
|
||||
// many_to_many 专属
|
||||
#[serde(default)]
|
||||
pub through_entity: Option<String>,
|
||||
#[serde(default)]
|
||||
pub through_source_field: Option<String>,
|
||||
#[serde(default)]
|
||||
pub through_target_field: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum RelationType {
|
||||
#[default]
|
||||
OneToMany,
|
||||
ManyToOne,
|
||||
ManyToMany,
|
||||
}
|
||||
```
|
||||
|
||||
#### 级联删除 (已有,需增强)
|
||||
|
||||
`data_service.rs:330-395` 已实现 `Restrict`/`Nullify`/`Cascade` 三种策略。需增强:
|
||||
|
||||
1. **级联影响信息返回**:Restrict 时返回 `affected_count` 和 `relation.name`,方便前端展示
|
||||
2. **批量删除级联**:`batch_delete` (data_service.rs:417-520) 当前绕过级联,需补充
|
||||
3. **many_to_many 级联**:基于 `through_entity` 清理中间表记录
|
||||
|
||||
#### 级联策略执行 (已有,需增强错误信息)
|
||||
|
||||
现有 `data_service.rs:330-395` 已实现。增强点:
|
||||
|
||||
1. **Restrict 错误增强**:返回 `affected_count` 和 `relation.name`
|
||||
2. **批量删除级联**:`batch_delete` (data_service.rs:417-520) 当前绕过级联,需补充
|
||||
3. **PATCH 校验**:`partial_update` (data_service.rs:291-327) 当前绕过 `validate_data`,需补充
|
||||
4. **many_to_many 级联**:基于 `through_entity` 清理中间表记录
|
||||
|
||||
#### FK 存在性校验 (已有 `validate_ref_entities`)
|
||||
|
||||
`data_service.rs:834-899` 已实现 `validate_ref_entities`。需确保 `partial_update` (PATCH) 也调用此函数。
|
||||
|
||||
### 2.3 前端实现
|
||||
|
||||
#### 前端类型扩展
|
||||
|
||||
`apps/web/src/api/plugins.ts` 需更新:
|
||||
|
||||
```typescript
|
||||
// PluginEntitySchema 新增
|
||||
interface PluginEntitySchema {
|
||||
// ... existing fields
|
||||
relations?: PluginRelationSchema[];
|
||||
}
|
||||
|
||||
interface PluginRelationSchema {
|
||||
entity: string;
|
||||
foreign_key: string;
|
||||
on_delete: 'cascade' | 'nullify' | 'restrict';
|
||||
name?: string;
|
||||
type?: 'one_to_many' | 'many_to_one' | 'many_to_many';
|
||||
display_field?: string;
|
||||
}
|
||||
|
||||
// PluginFieldSchema 新增 validation 属性
|
||||
interface PluginFieldSchema {
|
||||
// ... existing fields
|
||||
validation?: {
|
||||
pattern?: string;
|
||||
message?: string;
|
||||
min_length?: number;
|
||||
max_length?: number;
|
||||
min_value?: number;
|
||||
max_value?: number;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
#### EntitySelect 增强 (已有基础)
|
||||
|
||||
字段有 `ref_entity` 属性时,CRUD 表单已自动渲染为 EntitySelect。增强点:
|
||||
- 优先使用 `relation.display_field` 作为下拉显示字段(fallback 到现有 `ref_label_field`)
|
||||
- 关联子表标题使用 `relation.name`
|
||||
|
||||
#### 详情页关联子表自动渲染
|
||||
|
||||
Entity 的 `one_to_many` relations 自动在详情页渲染为内嵌 CRUD 表格:
|
||||
- Compact 模式 + 自动过滤 `fk = parent_record.id`
|
||||
- 支持新增/编辑/删除子记录
|
||||
- 标题使用 `relation.name`
|
||||
|
||||
#### 级联删除确认
|
||||
|
||||
删除有 incoming relations 的记录时,弹出确认:
|
||||
```
|
||||
确定删除客户「{name}」?
|
||||
此操作将同时删除:
|
||||
- 3 条联系人记录
|
||||
- 5 条沟通记录
|
||||
- 2 条标签记录
|
||||
```
|
||||
|
||||
### 2.4 CRM plugin.toml 改造
|
||||
|
||||
为 customer 实体补充 relations:
|
||||
|
||||
```toml
|
||||
[[schema.entities.relations]]
|
||||
entity = "contact"
|
||||
foreign_key = "customer_id"
|
||||
on_delete = "cascade"
|
||||
name = "contacts"
|
||||
type = "one_to_many"
|
||||
display_field = "name"
|
||||
|
||||
[[schema.entities.relations]]
|
||||
entity = "communication"
|
||||
foreign_key = "customer_id"
|
||||
on_delete = "cascade"
|
||||
name = "communications"
|
||||
type = "one_to_many"
|
||||
display_field = "subject"
|
||||
|
||||
[[schema.entities.relations]]
|
||||
entity = "customer_tag"
|
||||
foreign_key = "customer_id"
|
||||
on_delete = "cascade"
|
||||
name = "tags"
|
||||
type = "one_to_many"
|
||||
display_field = "tag_name"
|
||||
|
||||
[[schema.entities.relations]]
|
||||
entity = "customer"
|
||||
foreign_key = "parent_id"
|
||||
on_delete = "nullify"
|
||||
name = "parent"
|
||||
type = "many_to_one"
|
||||
display_field = "name"
|
||||
```
|
||||
|
||||
为 contact 实体补充 relations:
|
||||
|
||||
```toml
|
||||
[[schema.entities.relations]]
|
||||
entity = "communication"
|
||||
foreign_key = "contact_id"
|
||||
on_delete = "cascade"
|
||||
name = "communications"
|
||||
type = "one_to_many"
|
||||
display_field = "subject"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. P0-2: 字段校验层
|
||||
|
||||
### 3.1 现有基础
|
||||
|
||||
**已有实现**:
|
||||
- `validate_data` (`data_service.rs:797-831`): required + pattern 正则校验
|
||||
- `validate_ref_entities` (`data_service.rs:834-899`): FK 引用存在性校验
|
||||
- `FieldValidation` (`manifest.rs:53-57`): `pattern` + `message` 字段
|
||||
- unique 检查已在 `create`/`update` 流程中实现
|
||||
|
||||
**缺失部分**:
|
||||
- `min_length` / `max_length` 校验器
|
||||
- `min_value` / `max_value` 校验器
|
||||
- PATCH (partial_update) 绕过所有校验
|
||||
- 前端 TypeScript 类型缺少 `validation` 属性
|
||||
|
||||
### 3.2 Manifest Schema 扩展
|
||||
|
||||
在现有 `[validation]` 上新增字段(`manifest.rs:53-57` 已有 `pattern` + `message`):
|
||||
|
||||
```toml
|
||||
[[schema.entities.fields]]
|
||||
name = "phone"
|
||||
field_type = "string"
|
||||
display_name = "手机号"
|
||||
|
||||
[schema.entities.fields.validation]
|
||||
pattern = "^1[3-9]\\d{9}$"
|
||||
message = "请输入有效的手机号码"
|
||||
min_length = 11
|
||||
max_length = 11
|
||||
|
||||
[[schema.entities.fields]]
|
||||
name = "credit_limit"
|
||||
field_type = "decimal"
|
||||
|
||||
[schema.entities.fields.validation]
|
||||
min_value = 0
|
||||
max_value = 99999999
|
||||
message = "信用额度必须在 0-99999999 之间"
|
||||
```
|
||||
|
||||
#### 校验类型定义
|
||||
|
||||
| 校验器 | manifest 字段 | 状态 | 说明 |
|
||||
|--------|-------------|------|------|
|
||||
| `required` | `field.required` | **已有** | 值不能为 null/空字符串 |
|
||||
| `unique` | `field.unique` | **已有** | 同 tenant 内值唯一 |
|
||||
| `pattern` | `validation.pattern` + `validation.message` | **已有** | 正则匹配 |
|
||||
| `ref_exists` | `field.ref_entity` | **已有** | FK 指向的记录存在且未删除 |
|
||||
| `min_length` / `max_length` | `validation.min_length` / `validation.max_length` | **新增** | 字符串长度范围 |
|
||||
| `min_value` / `max_value` | `validation.min_value` / `validation.max_value` | **新增** | 数值范围 |
|
||||
|
||||
### 3.3 后端实现
|
||||
|
||||
#### 扩展 `FieldValidation` (`manifest.rs:53-57`)
|
||||
|
||||
在现有结构上新增 4 个可选字段:
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FieldValidation {
|
||||
pub pattern: Option<String>, // 已有
|
||||
pub message: Option<String>, // 已有
|
||||
// ↓ 新增
|
||||
pub min_length: Option<usize>,
|
||||
pub max_length: Option<usize>,
|
||||
pub min_value: Option<f64>,
|
||||
pub max_value: Option<f64>,
|
||||
}
|
||||
```
|
||||
|
||||
#### 扩展 `validate_data` (`data_service.rs:797-831`)
|
||||
|
||||
在现有函数中追加 min_length/max_length/min_value/max_value 检查:
|
||||
|
||||
```rust
|
||||
// 现有: required + pattern 检查 (已实现)
|
||||
// 新增:
|
||||
if let Some(validation) = &field.validation {
|
||||
// min_length / max_length
|
||||
if let Some(str_val) = val.as_str() {
|
||||
if let Some(min) = validation.min_length {
|
||||
if str_val.len() < min { return Err(...); }
|
||||
}
|
||||
if let Some(max) = validation.max_length {
|
||||
if str_val.len() > max { return Err(...); }
|
||||
}
|
||||
}
|
||||
// min_value / max_value (适用于 number/integer/decimal)
|
||||
if let Some(num_val) = val.as_f64() {
|
||||
if let Some(min) = validation.min_value {
|
||||
if num_val < min { return Err(...); }
|
||||
}
|
||||
if let Some(max) = validation.max_value {
|
||||
if num_val > max { return Err(...); }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 修复 PATCH 校验缺失
|
||||
|
||||
`partial_update` (`data_service.rs:291-327`) 需要添加 `validate_data` 和 `validate_ref_entities` 调用,与 `update` 保持一致。
|
||||
|
||||
**执行位置:** `data_service.rs` 的 `create_record` 和 `update_record` 方法中,数据写入前调用 `validate_record`。
|
||||
|
||||
**错误响应格式:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "数据验证失败",
|
||||
"details": [
|
||||
{ "field": "phone", "message": "请输入有效的手机号码" },
|
||||
{ "field": "customer_id", "message": "引用的客户不存在" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 3.4 前端实现
|
||||
|
||||
从 schema 自动生成 Ant Design Form rules(需先修复 TypeScript 类型缺失):
|
||||
|
||||
```typescript
|
||||
function generateFormRules(field: PluginFieldSchema): Rule[] {
|
||||
const rules: Rule[] = [];
|
||||
|
||||
if (field.required) {
|
||||
rules.push({ required: true, message: `${field.display_name}不能为空` });
|
||||
}
|
||||
|
||||
if (field.validation?.pattern) {
|
||||
rules.push({
|
||||
pattern: new RegExp(field.validation.pattern),
|
||||
message: field.validation.message || `${field.display_name}格式不正确`,
|
||||
});
|
||||
}
|
||||
|
||||
if (field.validation?.min_length || field.validation?.max_length) {
|
||||
rules.push({
|
||||
min: field.validation.min_length,
|
||||
max: field.validation.max_length,
|
||||
message: field.validation.message || `${field.display_name}长度不正确`,
|
||||
});
|
||||
}
|
||||
|
||||
return rules;
|
||||
}
|
||||
```
|
||||
|
||||
### 3.5 CRM plugin.toml 补充校验
|
||||
|
||||
```toml
|
||||
# phone 字段
|
||||
[schema.entities.fields.validation]
|
||||
pattern = "^1[3-9]\\d{9}$"
|
||||
message = "请输入有效的手机号码"
|
||||
|
||||
# email 字段
|
||||
[schema.entities.fields.validation]
|
||||
pattern = "^[\\w.+-]+@[\\w.-]+\\.[a-zA-Z]{2,}$"
|
||||
message = "请输入有效的邮箱地址"
|
||||
|
||||
# credit_code 字段
|
||||
[schema.entities.fields.validation]
|
||||
pattern = "^[0-9A-HJ-NP-RTUW-Y]{2}\\d{6}[0-9A-HJ-NP-RTUW-Y]{10}$"
|
||||
message = "请输入有效的统一社会信用代码"
|
||||
|
||||
# website 字段
|
||||
[schema.entities.fields.validation]
|
||||
pattern = "^https?://[\\w.-]+(?:\\.[\\w.-]+)+[/#?]?.*$"
|
||||
message = "请输入有效的网址"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. P0-3: 前端去硬编码
|
||||
|
||||
### 4.1 Dashboard 通用化
|
||||
|
||||
**涉及文件:**
|
||||
- `apps/web/src/pages/dashboard/dashboardConstants.tsx`
|
||||
- `apps/web/src/pages/dashboard/DashboardWidgets.tsx`
|
||||
- `apps/web/src/pages/PluginDashboardPage.tsx`
|
||||
|
||||
**改造方案:**
|
||||
|
||||
| 当前硬编码 | 通用化方案 |
|
||||
|-----------|-----------|
|
||||
| `ENTITY_COLORS`: customer→indigo, contact→green, ... | 8 色调色板按 entity 顺序自动分配 |
|
||||
| `ENTITY_ICONS`: customer→TeamOutlined, ... | 从 page schema 的 icon 字段读取 |
|
||||
| 标题 "CRM 数据全景视图" | `{manifest.name} 统计概览` |
|
||||
| 副标题 "实时掌握业务动态" | `{manifest.description}` 截取前 50 字 |
|
||||
|
||||
**通用调色板:**
|
||||
|
||||
```typescript
|
||||
const UNIVERSAL_PALETTE = [
|
||||
'#6366f1', // indigo
|
||||
'#22c55e', // green
|
||||
'#f59e0b', // amber
|
||||
'#8b5cf6', // violet
|
||||
'#ef4444', // red
|
||||
'#06b6d4', // cyan
|
||||
'#f97316', // orange
|
||||
'#ec4899', // pink
|
||||
];
|
||||
```
|
||||
|
||||
### 4.2 Graph 通用化
|
||||
|
||||
**涉及文件:** `apps/web/src/pages/plugins/graph/graphConstants.ts`
|
||||
|
||||
**改造方案:**
|
||||
|
||||
| 当前硬编码 | 通用化方案 |
|
||||
|-----------|-----------|
|
||||
| `RELATIONSHIP_COLORS`: parent_child→indigo, ... | 调色板按 option 顺序循环 |
|
||||
| `RELATIONSHIP_LABELS`: parent_child→"母子", ... | 从 field.options[].label 读取 |
|
||||
| `RELATIONSHIP_TYPES` 固定 5 种 | 从 schema 动态生成 |
|
||||
|
||||
### 4.3 CRUD 表格列可配置
|
||||
|
||||
**涉及文件:** `apps/web/src/pages/PluginCRUDPage.tsx`
|
||||
|
||||
**改造方案:**
|
||||
|
||||
manifest page 新增可选字段 `table_columns`:
|
||||
|
||||
```toml
|
||||
[[ui.pages]]
|
||||
type = "crud"
|
||||
entity = "customer"
|
||||
table_columns = ["code", "name", "customer_type", "level", "status", "owner_id", "region", "industry"]
|
||||
```
|
||||
|
||||
不声明时默认行为:
|
||||
- 取前 8 个非 hidden 非 FK 字段
|
||||
- 替换当前 `fields.slice(0, 5)` 硬编码
|
||||
|
||||
### 4.4 验证标准
|
||||
|
||||
> **测试: 将 CRM 插件替换为 inventory 插件,Dashboard/Graph/CRUD 页面应零改动正确渲染。**
|
||||
|
||||
具体验证:
|
||||
1. Dashboard 显示 inventory 的 6 个实体统计,颜色按顺序分配
|
||||
2. Graph 如果 inventory 有关系数据,渲染正确(无数据则显示空状态)
|
||||
3. CRUD 表格按 `table_columns` 或默认 8 列显示
|
||||
|
||||
---
|
||||
|
||||
## 5. 关键文件清单
|
||||
|
||||
### 后端 Rust
|
||||
|
||||
| 文件 | 改动类型 | 说明 |
|
||||
|------|---------|------|
|
||||
| `crates/erp-plugin/src/manifest.rs` | 修改 | `PluginRelation` 新增 name/type/display_field/through_* 字段;`FieldValidation` 新增 min_length/max_length/min_value/max_value |
|
||||
| `crates/erp-plugin/src/data_service.rs` | 修改 | 扩展 `validate_data` 增加 min/max 校验;`partial_update` 补充校验调用;`batch_delete` 补充级联 |
|
||||
| `crates/erp-plugin-crm/plugin.toml` | 修改 | 补充 relations 声明 + validation 规则 |
|
||||
|
||||
> 注意:不新建 `validation.rs`,直接扩展现有 `validate_data` 和 `validate_ref_entities`。
|
||||
|
||||
### 前端 TypeScript
|
||||
|
||||
| 文件 | 改动类型 | 说明 |
|
||||
|------|---------|------|
|
||||
| `apps/web/src/api/plugins.ts` | 修改 | `PluginEntitySchema` 新增 `relations`;`PluginFieldSchema` 新增 `validation` |
|
||||
| `apps/web/src/pages/dashboard/dashboardConstants.tsx` | 修改 | 去硬编码,通用调色板自动分配 |
|
||||
| `apps/web/src/pages/dashboard/DashboardWidgets.tsx` | 修改 | schema 驱动颜色/图标 |
|
||||
| `apps/web/src/pages/PluginDashboardPage.tsx` | 修改 | 通用标题/副标题 |
|
||||
| `apps/web/src/pages/plugins/graph/graphConstants.ts` | 修改 | 关系类型从 options 动态读取 |
|
||||
| `apps/web/src/pages/PluginCRUDPage.tsx` | 修改 | 可配置列数 + Form rules 自动生成 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 验证方案
|
||||
|
||||
### 6.1 编译与测试
|
||||
|
||||
```bash
|
||||
cargo check # 全 workspace 编译
|
||||
cargo test --workspace # 全量测试
|
||||
```
|
||||
|
||||
### 6.2 单元测试
|
||||
|
||||
- `validation.rs`: 每种校验器独立测试 (required/unique/pattern/ref_exists/length/value range)
|
||||
- `data_service.rs`: 级联策略测试 (cascade_soft_delete/set_null/restrict)
|
||||
|
||||
### 6.3 集成测试 (Testcontainers)
|
||||
|
||||
- 删除客户 → 验证联系人/沟通记录/标签级联软删除
|
||||
- 删除有 restrict 关系的记录 → 验证 409 响应
|
||||
- 创建联系人 → customer_id 不存在时验证 400
|
||||
- 创建客户 → phone 格式不正确时验证 400 + 错误详情
|
||||
- 创建客户 → code 已存在时验证 409
|
||||
|
||||
### 6.4 功能验证
|
||||
|
||||
1. 重新安装 CRM 插件,确认 5 个 relation 正确注册到 entity metadata
|
||||
2. 删除客户 → 确认关联数据正确级联
|
||||
3. 手机号/邮箱格式校验 → 确认前后端双重拦截
|
||||
4. Dashboard → 确认标题/颜色从 schema 动态生成
|
||||
5. 切换 inventory 插件 → Dashboard/Graph 零改动渲染
|
||||
|
||||
### 6.5 前端验证
|
||||
|
||||
```bash
|
||||
cd apps/web && pnpm dev
|
||||
```
|
||||
|
||||
手动测试所有 CRM 页面,确认无回归。
|
||||
|
||||
---
|
||||
|
||||
## 7. 不在本规格范围内
|
||||
|
||||
| 项 | 原因 | 计划 |
|
||||
|----|------|------|
|
||||
| 商机 (Opportunity) / 销售漏斗 | CRM 业务功能,P2 范畴 | 后续规格 |
|
||||
| 数据导入导出 (Excel) | 平台能力但工作量大 | P1 规格 |
|
||||
| 通知规则 + 消息中心联动 | 需要跨模块协作 | P1 规格 |
|
||||
| WASM 校验/计算 Hook | 平台能力但依赖 WASM 运行时增强 | P2 规格 |
|
||||
| 全局搜索 / 保存视图 | UX 增强 | P1 规格 |
|
||||
| Lead 线索实体 | CRM 业务功能 | P2 规格 |
|
||||
@@ -1,337 +0,0 @@
|
||||
# ERP 插件平台演进路线图 — 设计规格
|
||||
|
||||
> 日期: 2026-04-18
|
||||
> 来源: 无主题发散式互动探讨
|
||||
> 状态: Draft
|
||||
|
||||
---
|
||||
|
||||
## 1. 背景与动机
|
||||
|
||||
ERP 平台已完成 Phase 1-6 核心开发和 Q2-Q4 成熟度路线图。当前有两个行业插件(CRM + 进销存)运行在 WASM 插件系统上。通过分析发现四大系统性缺口:
|
||||
|
||||
1. **跨插件数据引用完全不支持** — 进销存的 `customer_id` 只能存裸 UUID
|
||||
2. **插件无通用业务能力** — 导入导出/打印/配置/视图每个插件都要自己实现
|
||||
3. **无质量保障机制** — 第三方插件的安全性和性能无法保证
|
||||
4. **无发现和分发渠道** — 用户无法自助发现和安装插件
|
||||
|
||||
**目标:** 通过搭建财务/应收插件来验证和推动这些平台能力的实现。
|
||||
|
||||
**核心设计原则:**
|
||||
- 插件间**完全独立**,任何插件可自由安装/卸载,不受其他插件影响
|
||||
- 跨插件引用**声明式**,通过 plugin.toml 零代码实现
|
||||
- 通用业务能力**平台层提供**,插件声明式接入
|
||||
- 外部引用问题永远是**软警告**,永不硬阻塞用户操作
|
||||
|
||||
---
|
||||
|
||||
## 2. 跨插件数据引用系统
|
||||
|
||||
### 2.1 Entity Registry (平台实体注册表)
|
||||
|
||||
插件安装时将其所有实体注册到平台级 Entity Registry,其他插件通过 registry 动态发现和引用。
|
||||
|
||||
**数据结构:**
|
||||
|
||||
```
|
||||
entity_registry:
|
||||
- entity_name: string # 实体名 (如 "customer")
|
||||
- plugin_id: string # 注册该实体的插件 ID
|
||||
- display_fields: string[] # 用于下拉显示的字段列表
|
||||
- search_fields: string[] # 用于搜索的字段列表
|
||||
- status: active | inactive # 插件卸载时标记 inactive
|
||||
- registered_at: timestamp
|
||||
- tenant_id: uuid # 多租户隔离
|
||||
```
|
||||
|
||||
**生命周期:**
|
||||
- 插件安装 → 注册所有 entities 到 registry
|
||||
- 插件启用 → status = active
|
||||
- 插件禁用 → status = inactive(数据保留)
|
||||
- 插件卸载 → status = inactive + 标记为 orphaned
|
||||
|
||||
### 2.2 plugin.toml 扩展
|
||||
|
||||
```toml
|
||||
# 可选依赖声明
|
||||
[dependencies.crm]
|
||||
optional = true
|
||||
description = "客户管理 — 自动关联客户数据,未安装时客户字段为手动输入"
|
||||
|
||||
[dependencies.inventory]
|
||||
optional = true
|
||||
description = "进销存 — 自动关联商品数据"
|
||||
|
||||
# 跨插件引用字段
|
||||
[[schema.entities.fields]]
|
||||
name = "customer_id"
|
||||
field_type = "uuid"
|
||||
display_name = "客户"
|
||||
ref_entity = "customer" # 目标实体名
|
||||
ref_scope = "external" # "internal" (默认) | "external"
|
||||
ref_display_field = "name" # 下拉框显示字段
|
||||
ref_search_fields = ["name", "phone"] # 搜索字段
|
||||
ref_fallback_label = "外部客户" # 降级时显示文本
|
||||
```
|
||||
|
||||
### 2.3 运行时行为
|
||||
|
||||
**写入时校验:**
|
||||
|
||||
| 源插件状态 | 写入行为 | 读取行为 | 前端展示 |
|
||||
|-----------|---------|---------|---------|
|
||||
| 已安装 (active) | 强校验 UUID 存在性 | JOIN 富化 display_field | ✅ 绿色链接 "张三" |
|
||||
| 未安装 (inactive) | 无校验,接受任意 UUID | 返回原始 UUID | ⬜ 灰色 "外部客户" |
|
||||
| 刚重新启用 | 新写入强校验,不回溯已有 | 后台对账扫描 | ⚠️ 黄色警告 (悬空) |
|
||||
|
||||
**悬空引用处理 (插件重新启用时):**
|
||||
1. 后台扫描所有 `ref_scope=external` 且指向本插件实体的字段
|
||||
2. 验证每个 UUID 是否存在于本插件表中
|
||||
3. 生成对账报告: `{ valid: N, dangling: M, details: [...] }`
|
||||
4. 前端展示对账结果,用户逐条处理(映射/清空/忽略)
|
||||
5. 永不硬阻塞用户操作
|
||||
|
||||
### 2.4 需要改造的文件
|
||||
|
||||
| 文件 | 改动 | 复杂度 |
|
||||
|------|------|--------|
|
||||
| `crates/erp-plugin/src/manifest.rs` | 新增 `ref_scope`, `ref_display_field`, `ref_search_fields`, `ref_fallback_label`; 新增 `DependenciesSection` | 低 |
|
||||
| `crates/erp-plugin/src/entity_registry.rs` (新) | 实体注册/发现/inactive 标记/对账 | 中 |
|
||||
| `crates/erp-plugin/src/data_service.rs` | `validate_ref_entities` 支持运行时发现外部引用 | 中 |
|
||||
| `crates/erp-plugin/src/host.rs` | 新增 `resolve_ref_entity` Host API | 中 |
|
||||
| `crates/erp-plugin/wit/plugin.wit` | 新增 `resolve-ref-entity` 接口 | 低 |
|
||||
| `crates/erp-plugin/src/service.rs` | 插件安装/卸载时维护 Entity Registry | 中 |
|
||||
| `apps/web/src/` 前端 | entity_select 组件支持跨插件数据源 + 降级显示 + 对账 UI | 高 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 插件平台通用服务层 (P1)
|
||||
|
||||
### 3.1 数据导入导出服务
|
||||
|
||||
插件在 plugin.toml 中声明哪些实体支持导入导出,平台提供统一的导入导出 UI 和引擎。
|
||||
|
||||
```toml
|
||||
[[schema.entities]]
|
||||
name = "invoice"
|
||||
display_name = "发票"
|
||||
importable = true
|
||||
exportable = true
|
||||
import_template = "invoice_import_template.xlsx"
|
||||
```
|
||||
|
||||
**平台能力:**
|
||||
- 自动生成导入模板(基于 schema entities fields)
|
||||
- Excel/CSV 解析 + schema 字段校验
|
||||
- 批量写入(支持事务 + 错误行级报告)
|
||||
- 导出为 Excel/CSV(支持筛选条件)
|
||||
- 导入历史记录 + 回滚
|
||||
|
||||
**实现位置:** `crates/erp-plugin/src/import_export.rs` + 前端 `ImportExportModal` 通用组件
|
||||
|
||||
### 3.2 打印模板引擎
|
||||
|
||||
平台提供 HTML → PDF 的模板渲染能力,插件定义模板和字段映射。
|
||||
|
||||
```toml
|
||||
[[templates]]
|
||||
name = "invoice_pdf"
|
||||
display_name = "发票"
|
||||
entity = "invoice"
|
||||
format = "pdf"
|
||||
template_file = "templates/invoice.html"
|
||||
```
|
||||
|
||||
**平台能力:**
|
||||
- HTML 模板渲染 → PDF 下载
|
||||
- 模板变量替换(基于实体字段)
|
||||
- 租户级模板自定义(覆盖默认模板)
|
||||
- 打印预览
|
||||
|
||||
### 3.3 插件配置 UI
|
||||
|
||||
插件在 plugin.toml 中声明配置项,平台自动生成配置页面。
|
||||
|
||||
```toml
|
||||
[settings]
|
||||
[[settings.fields]]
|
||||
name = "default_tax_rate"
|
||||
display_name = "默认税率"
|
||||
field_type = "number"
|
||||
default_value = 0.13
|
||||
|
||||
[[settings.fields]]
|
||||
name = "invoice_prefix"
|
||||
display_name = "发票前缀"
|
||||
field_type = "text"
|
||||
default_value = "INV"
|
||||
```
|
||||
|
||||
**平台能力:**
|
||||
- 根据 settings 声明自动生成配置表单
|
||||
- 配置数据存储在 `plugin_settings` 表(tenant_id + plugin_id + key/value)
|
||||
- 配置变更时通知插件(通过事件)
|
||||
- 支持配置权限控制(仅管理员可改)
|
||||
|
||||
### 3.4 自定义视图
|
||||
|
||||
用户可以保存列表页的列配置和筛选条件。
|
||||
|
||||
```
|
||||
user_views:
|
||||
- id: uuid
|
||||
- user_id: uuid
|
||||
- plugin_id: string
|
||||
- entity_name: string
|
||||
- view_name: string
|
||||
- columns: string[]
|
||||
- filters: json
|
||||
- sort: json
|
||||
- is_default: boolean
|
||||
```
|
||||
|
||||
### 3.5 通知规则
|
||||
|
||||
插件在 plugin.toml 中声明可触发的事件,平台提供通知规则配置 UI。
|
||||
|
||||
```toml
|
||||
[[trigger_events]]
|
||||
name = "invoice.overdue"
|
||||
display_name = "发票逾期"
|
||||
description = "发票超过付款期限未收款"
|
||||
```
|
||||
|
||||
**平台能力:**
|
||||
- 规则引擎: WHEN event THEN notify [user/role/department]
|
||||
- 复用 erp-message 的通知渠道
|
||||
- 租户级规则配置
|
||||
|
||||
### 3.6 编号规则 (已有基础扩展)
|
||||
|
||||
复用 erp-config 的编号规则服务,扩展为插件可接入。
|
||||
|
||||
```toml
|
||||
[[numbering]]
|
||||
entity = "invoice"
|
||||
prefix = "INV"
|
||||
format = "{PREFIX}-{YEAR}-{SEQ:4}"
|
||||
reset_rule = "yearly"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 插件质量保障
|
||||
|
||||
### 4.1 上传时校验
|
||||
|
||||
```
|
||||
插件上传 → Schema 校验 → WASM 二进制验证 → 安全扫描 → 性能基准 → 发布/拒绝
|
||||
```
|
||||
|
||||
| 阶段 | 校验内容 | 现状 |
|
||||
|------|---------|------|
|
||||
| Schema 校验 | plugin.toml 格式、字段类型、权限码一致性 | 部分已有 |
|
||||
| WASM 验证 | 二进制格式、WIT 兼容性、导出函数检查 | 已有 |
|
||||
| 安全扫描 | 动态表 SQL 注入风险、Fuel 耗尽、内存泄漏 | 缺失 |
|
||||
| 性能基准 | 标准 CRUD 操作在 N 条数据下的响应时间 | 缺失 |
|
||||
| 兼容性 | 平台版本匹配、依赖插件版本兼容 | 缺失 |
|
||||
|
||||
### 4.2 运行时监控
|
||||
|
||||
```
|
||||
plugin_runtime_metrics:
|
||||
- plugin_id: string
|
||||
- error_rate: float
|
||||
- avg_response_ms: float
|
||||
- fuel_consumption: float
|
||||
- memory_peak_mb: float
|
||||
- active_instances: int
|
||||
```
|
||||
|
||||
**告警规则:** 错误率 > 5% / 平均响应 > 2s / Fuel 消耗异常 / 内存持续增长
|
||||
|
||||
---
|
||||
|
||||
## 5. 插件市场/商店
|
||||
|
||||
| 功能 | 说明 |
|
||||
|------|------|
|
||||
| 插件目录 | 按行业/功能分类浏览 |
|
||||
| 搜索 | 按名称/标签/行业搜索 |
|
||||
| 详情页 | 截图、演示、功能描述、权限说明 |
|
||||
| 一键安装 | 上传 → 自动安装 → 配置 → 启用 |
|
||||
| 评分/评论 | 用户评分和使用反馈 |
|
||||
| 版本管理 | 版本列表、更新日志、回滚 |
|
||||
| 依赖提示 | 安装时提示可选依赖 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 验证计划 — 财务/应收插件
|
||||
|
||||
### 6.1 实体设计
|
||||
|
||||
| 实体 | 字段概要 | 跨插件引用 |
|
||||
|------|---------|-----------|
|
||||
| invoice (发票) | 编号/客户/金额/税额/状态/到期日 | customer_id → CRM.customer |
|
||||
| invoice_line (发票行) | 发票/商品/数量/单价/税额 | product_id → Inventory.product |
|
||||
| payment (收款) | 发票/金额/方式/日期/状态 | invoice_id → 本插件内部 |
|
||||
| quote (报价单) | 编号/客户/有效期/状态 | customer_id → CRM.customer |
|
||||
| quote_line (报价行) | 报价单/商品/数量/单价 | product_id → Inventory.product |
|
||||
|
||||
### 6.2 验证矩阵
|
||||
|
||||
| 能力 | 验证方式 | 预期结果 |
|
||||
|------|---------|---------|
|
||||
| 跨插件引用 (CRM 安装) | 创建发票时选择客户 | entity_select 下拉显示 CRM 客户列表 |
|
||||
| 跨插件引用 (CRM 卸载) | 创建发票时输入客户 | 降级为文本输入,不阻塞 |
|
||||
| 悬空引用对账 | CRM 卸载→创建发票→重新安装 CRM | 对账报告显示悬空引用,用户可修复 |
|
||||
| 数据导入 | 导入 Excel 客户清单 | 解析+校验+批量写入 |
|
||||
| 数据导出 | 导出发票列表为 Excel | 筛选+下载 |
|
||||
| 打印模板 | 打印发票 PDF | HTML→PDF 渲染 |
|
||||
| 插件配置 | 设置税率/发票前缀 | 自动生成的配置页面 |
|
||||
| 编号规则 | 创建发票自动编号 | INV-2026-0001 |
|
||||
| 通知规则 | 发票逾期通知 | 规则引擎触发通知 |
|
||||
| 独立安装 | 不安装 CRM 单独安装财务 | 所有功能正常,客户字段降级 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 实施优先级
|
||||
|
||||
```
|
||||
P0 (已完成/进行中): P0 平台能力升级 + 插件系统增强
|
||||
|
||||
P1 (跨插件引用): Entity Registry + ref_scope 扩展 + 前端 entity_select 改造
|
||||
这是所有后续能力的基础
|
||||
|
||||
P2 (平台通用服务): 数据导入导出 → 插件配置 UI → 编号规则扩展 → 通知规则
|
||||
|
||||
P3 (质量保障): 上传时安全扫描 → 性能基准 → 运行时监控
|
||||
|
||||
P4 (插件市场): 插件目录 → 一键安装 → 版本管理 → 评分评论
|
||||
|
||||
验证: 财务/应收插件贯穿 P1-P2,每完成一个 P 就用财务插件验证
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 风险与缓解
|
||||
|
||||
| 风险 | 影响 | 缓解措施 |
|
||||
|------|------|---------|
|
||||
| Entity Registry 查询性能 | 每次数据操作都要查注册表 | 内存缓存 + DashMap,注册表数据量极小 |
|
||||
| 悬空引用数据量过大 | 对账扫描耗时长 | 异步后台任务 + 分批处理 + 进度条 |
|
||||
| Excel 导入内存占用 | 大文件解析 OOM | 流式解析 + 批量提交 + 文件大小限制 |
|
||||
| 打印模板安全 | 模板注入攻击 | 沙箱渲染 + 变量白名单 |
|
||||
| 插件市场审核成本 | 人工审核效率低 | 自动化扫描 + 人工抽查 + 社区举报 |
|
||||
|
||||
---
|
||||
|
||||
## 9. 讨论溯源
|
||||
|
||||
本文档基于 2026-04-18 的无主题发散式互动探讨产出,完整讨论过程记录在 `plans/skill-cosmic-pancake.md`。
|
||||
|
||||
关键决策历程:
|
||||
- **Round 1:** 发现跨插件数据引用完全不支持(进销存的 customer_id 是裸 UUID)
|
||||
- **Round 2:** 确定声明式引用 + 完全独立(无硬依赖)+ 软警告对账方案
|
||||
- **Round 3:** 确定导入导出/打印/配置/视图/通知应为平台通用服务
|
||||
- **Round 4:** 收敛为统一设计规格,以财务插件为验证载体
|
||||
@@ -1,183 +0,0 @@
|
||||
# 插件系统增强设计规格
|
||||
|
||||
## Context
|
||||
|
||||
插件系统是 ERP 平台的核心差异化能力,当前声明式层面(manifest schema、动态表、前端页面)已达 90% 成熟度。但 WASM 逻辑层存在根本性限制:
|
||||
|
||||
1. **插件无法自主查询数据** — `db_query` 的 filter/pagination 参数被忽略,只能使用预填充结果
|
||||
2. **无读后写一致性** — 延迟刷新模型导致插件在一次调用中无法读取自己刚写入的数据
|
||||
3. **聚合只有 COUNT** — 缺少 SUM/AVG/MAX/MIN,无法支撑财务、统计类场景
|
||||
4. **热更新无原子回滚** — 旧版本先卸载再加载新版本,中间失败无保障
|
||||
5. **Schema 变更只支持新增实体** — 不支持已有实体的字段演进
|
||||
|
||||
这些限制使插件系统只能支撑"数据管理+展示"型轻量场景(CRM、简单进销存),无法支撑需要复杂业务逻辑的行业(财务、制造、电商)。
|
||||
|
||||
本次增强的目标:**让插件逻辑层从 40% 提升到 80%+,使系统能真正承载不同行业的定制化需求。**
|
||||
|
||||
---
|
||||
|
||||
## 改动 1:混合执行模型(解决查询和读后写一致性)
|
||||
|
||||
### 问题
|
||||
|
||||
`host.rs:99-109` — `db_query` 忽略 `_filter` 和 `_pagination` 参数,只从 `query_results` 预填充缓存取数据。插件无法自主构造查询。
|
||||
|
||||
### 方案:读操作走实时 SQL + 写操作保持延迟批量 + 读前自动 flush
|
||||
|
||||
核心流程变更:
|
||||
|
||||
```
|
||||
当前:
|
||||
WASM 调用 db_insert() → 入队 pending_ops
|
||||
WASM 调用 db_query() → 从预填充缓存读(忽略 filter/pagination)
|
||||
WASM 结束 → flush 全部 pending_ops
|
||||
|
||||
改为:
|
||||
WASM 调用 db_insert() → 入队 pending_ops
|
||||
WASM 调用 db_query() → 先 flush pending_ops → 执行真实 SQL 查询 → 返回结果
|
||||
WASM 结束 → flush 剩余 pending_ops
|
||||
```
|
||||
|
||||
### 改动文件
|
||||
|
||||
#### 1. `crates/erp-plugin/src/host.rs`
|
||||
|
||||
HostState 新增字段:
|
||||
|
||||
```rust
|
||||
pub struct HostState {
|
||||
// ... 现有字段保留 ...
|
||||
pub(crate) db: Option<DatabaseConnection>,
|
||||
pub(crate) event_bus: Option<EventBus>,
|
||||
}
|
||||
```
|
||||
|
||||
db_query 实现变更 — 使用 `tokio::runtime::Handle::current()` 在 `spawn_blocking` 内执行异步 DB 操作:
|
||||
|
||||
1. 先 `block_on(flush_ops(...))` 清空 pending writes
|
||||
2. 解析 filter/pagination 参数
|
||||
3. 调用 `DynamicTableManager::build_query_sql()` 构建查询
|
||||
4. `block_on` 执行查询并返回结果
|
||||
|
||||
向后兼容:`db = None` 时走旧的预填充路径。
|
||||
|
||||
#### 2. `crates/erp-plugin/src/dynamic_table.rs`
|
||||
|
||||
新增 `build_query_sql` 方法,复用 `data_service.rs` 中的查询构建逻辑。
|
||||
|
||||
### 向后兼容
|
||||
|
||||
- `HostState::new()` 不传 db → 走旧的预填充路径
|
||||
- `execute_wasm()` 传 db → 走新的实时查询路径
|
||||
- 现有 WASM 插件无需修改
|
||||
|
||||
---
|
||||
|
||||
## 改动 2:扩展聚合查询
|
||||
|
||||
### 问题
|
||||
|
||||
`data_service.rs:655` 的 `aggregate` 方法只支持 `GROUP BY + COUNT(*)`。
|
||||
|
||||
### 方案
|
||||
|
||||
新增 `aggregate_multi` 方法支持 SUM/AVG/MAX/MIN。
|
||||
|
||||
改动文件:
|
||||
|
||||
1. `data_service.rs` — 新增 `AggregateDef`、`AggregateFunc`、`AggregateResult` 类型和 `aggregate_multi` 方法
|
||||
2. `dynamic_table.rs` — 新增 `build_aggregate_multi_sql` 方法
|
||||
3. `data_handler.rs` — 扩展聚合 API 端点
|
||||
4. 前端 Dashboard Widget 适配多聚合返回格式
|
||||
|
||||
SQL 示例:
|
||||
```sql
|
||||
SELECT _f_status as key,
|
||||
COUNT(*) as count,
|
||||
COALESCE(SUM(_f_amount), 0) as sum_amount,
|
||||
COALESCE(AVG(_f_price), 0) as avg_price
|
||||
FROM plugin_erp_crm__order
|
||||
WHERE tenant_id = $1 AND deleted_at IS NULL
|
||||
GROUP BY _f_status
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 改动 3:热更新原子回滚
|
||||
|
||||
### 问题
|
||||
|
||||
`service.rs:578-585` — 先 `unload(old)` 再 `load(new)`,中间失败无回滚。
|
||||
|
||||
### 方案:先加载新版本到临时 key,成功后原子替换
|
||||
|
||||
改动文件:
|
||||
|
||||
1. `service.rs` — upgrade 方法改用临时 key 加载新版本
|
||||
2. `engine.rs` — 新增 `rename_plugin` 方法
|
||||
|
||||
安全保证:新版本加载失败 → 旧版本仍在运行,零停机。
|
||||
|
||||
---
|
||||
|
||||
## 改动 4:Schema 演进(ALTER TABLE 支持)
|
||||
|
||||
### 问题
|
||||
|
||||
升级时只处理新增实体(CREATE TABLE),不处理已有实体的字段变更。
|
||||
|
||||
### 方案:利用 JSONB 特性实现轻量级 Schema 演进
|
||||
|
||||
大部分字段变更不需要 DDL(JSONB 天然支持),仅新增 filterable/sortable 字段需 ALTER TABLE ADD Generated Column + 索引。
|
||||
|
||||
改动文件:
|
||||
|
||||
1. `service.rs` — upgrade 方法增加 schema diff 逻辑
|
||||
2. `dynamic_table.rs` — 新增 `FieldDiff`、`diff_entity_fields`、`alter_add_generated_columns`
|
||||
|
||||
---
|
||||
|
||||
## 实施顺序
|
||||
|
||||
| 阶段 | 改动 | 复杂度 | 影响范围 |
|
||||
|------|------|--------|---------|
|
||||
| 1 | 热更新原子回滚 | 低 | engine.rs + service.rs |
|
||||
| 2 | Schema 演进(ALTER TABLE) | 中低 | service.rs + dynamic_table.rs |
|
||||
| 3 | 扩展聚合查询 | 中 | data_service.rs + data_handler.rs + dynamic_table.rs |
|
||||
| 4 | 混合执行模型(查询能力) | 高 | host.rs + engine.rs + dynamic_table.rs |
|
||||
|
||||
---
|
||||
|
||||
## 验证方案
|
||||
|
||||
### 阶段 1:热更新回滚
|
||||
1. 上传损坏的 WASM 二进制 → 验证旧版本仍在运行
|
||||
2. 上传正确的新版本 → 验证成功切换
|
||||
|
||||
### 阶段 2:Schema 演进
|
||||
1. 升级插件增加 filterable 字段 → 验证 ALTER TABLE 正确执行
|
||||
2. 旧数据上新 Generated Column 值正确填充
|
||||
|
||||
### 阶段 3:聚合查询
|
||||
1. 创建测试数据,调用聚合 API → 验证 SUM/AVG 结果正确
|
||||
2. 前端 Dashboard 展示正确
|
||||
|
||||
### 阶段 4:混合执行模型
|
||||
1. 插件 WASM 中 db_insert 后立即 db_query → 读后写一致性
|
||||
2. 带 filter 的 db_query → 过滤结果正确
|
||||
3. 旧插件(预填充模式)仍能正常工作
|
||||
4. 多次连续 db_query 不超过 Fuel 限制
|
||||
|
||||
---
|
||||
|
||||
## 关键文件清单
|
||||
|
||||
| 文件 | 改动类型 |
|
||||
|------|---------|
|
||||
| `crates/erp-plugin/src/host.rs` | 重构 db_query + 新增 db/事件总线字段 |
|
||||
| `crates/erp-plugin/src/engine.rs` | 调整 execute_wasm + 新增 rename_plugin |
|
||||
| `crates/erp-plugin/src/service.rs` | 升级流程回滚安全 + schema diff |
|
||||
| `crates/erp-plugin/src/dynamic_table.rs` | 新增 build_query_sql + alter_add_generated_columns + diff_entity_fields |
|
||||
| `crates/erp-plugin/src/data_service.rs` | 新增 aggregate_multi + AggregateDef |
|
||||
| `crates/erp-plugin/src/data_handler.rs` | 扩展聚合 API |
|
||||
| `apps/web/src/pages/PluginDashboardPage.tsx` | 适配多聚合返回格式 |
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,624 +0,0 @@
|
||||
# freelance + itops 插件增强设计规格
|
||||
|
||||
> 日期: 2026-04-20
|
||||
> 来源: 多专家头脑风暴(UX专家 + 业务顾问 + 运维专家 + 财务专家)
|
||||
> 状态: Draft
|
||||
> 前置: `docs/superpowers/specs/2026-04-19-shantou-zhijie-it-services-plugins-design.md`
|
||||
|
||||
---
|
||||
|
||||
## 1. 背景与动机
|
||||
|
||||
当前插件是「数据录入系统」,不是「赚钱工具」。一人 IT 服务公司的核心痛点:
|
||||
|
||||
1. **钱从哪里来?** — 商机跟进靠人记,没有自动提醒、没有漏斗分析
|
||||
2. **项目做到哪了?** — 任务状态和工时手动填,跟合同金额/应收款脱节
|
||||
3. **钱收回来了吗?** — 报价→合同→开票→收款割裂,没有串联
|
||||
4. **运维服务会不会忘?** — 巡检计划写了没人催,SLA 超时了才知道
|
||||
5. **税和利润算不清?** — 收支分散在不同表里,月底做账要手动汇总
|
||||
|
||||
**问题根因:** 平台已有 trigger_events、settings、templates、cascade_from、visible_when、validation 六大能力,但两个插件完全没有使用。
|
||||
|
||||
**改进目标:** 纯插件层增强,三层递进:
|
||||
- Layer 1: 智能业务引擎 — 让系统主动驱动用户做事
|
||||
- Layer 2: 仪表盘重构 — 一个页面掌控全局
|
||||
- Layer 3: 专业输出 — 一键生成报价单/发票/合同 PDF
|
||||
|
||||
---
|
||||
|
||||
## 2. Layer 1: 智能业务引擎 — freelance 插件
|
||||
|
||||
### 2.1 Settings(插件配置页)
|
||||
|
||||
一次性配置公司信息和业务偏好,后续自动生效:
|
||||
|
||||
```toml
|
||||
[settings]
|
||||
|
||||
# ── 基本信息 ──
|
||||
[[settings.fields]]
|
||||
name = "company_name"
|
||||
display_name = "公司名称"
|
||||
field_type = "text"
|
||||
required = true
|
||||
group = "基本信息"
|
||||
|
||||
[[settings.fields]]
|
||||
name = "currency_symbol"
|
||||
display_name = "货币符号"
|
||||
field_type = "text"
|
||||
default_value = "¥"
|
||||
group = "基本信息"
|
||||
|
||||
# ── 财务 ──
|
||||
[[settings.fields]]
|
||||
name = "default_tax_rate"
|
||||
display_name = "默认税率(%)"
|
||||
field_type = "number"
|
||||
default_value = 6
|
||||
range = [0.0, 100.0]
|
||||
group = "财务"
|
||||
|
||||
# ── 提醒 ──
|
||||
[[settings.fields]]
|
||||
name = "payment_reminder_days"
|
||||
display_name = "收款提前提醒(天)"
|
||||
field_type = "number"
|
||||
default_value = 3
|
||||
range = [1.0, 30.0]
|
||||
group = "提醒"
|
||||
|
||||
[[settings.fields]]
|
||||
name = "notify_contract_expiring"
|
||||
display_name = "合同到期提醒"
|
||||
field_type = "boolean"
|
||||
default_value = true
|
||||
group = "提醒"
|
||||
|
||||
[[settings.fields]]
|
||||
name = "notify_payment_overdue"
|
||||
display_name = "逾期收款提醒"
|
||||
field_type = "boolean"
|
||||
default_value = true
|
||||
group = "提醒"
|
||||
|
||||
[[settings.fields]]
|
||||
name = "notify_opportunity_followup"
|
||||
display_name = "商机跟进提醒"
|
||||
field_type = "boolean"
|
||||
default_value = true
|
||||
group = "提醒"
|
||||
```
|
||||
|
||||
### 2.2 Trigger Events(自动事件驱动)
|
||||
|
||||
关键操作时自动发通知,把"人找事"变"事找人":
|
||||
|
||||
```toml
|
||||
[[trigger_events]]
|
||||
name = "opportunity_stage_changed"
|
||||
display_name = "商机阶段变更"
|
||||
description = "商机阶段发生变化时通知,特别是成交或失败"
|
||||
entity = "opportunity"
|
||||
on = "update"
|
||||
|
||||
[[trigger_events]]
|
||||
name = "contract_status_changed"
|
||||
display_name = "合同状态变更"
|
||||
description = "合同状态变化时检查到期预警"
|
||||
entity = "contract"
|
||||
on = "update"
|
||||
|
||||
[[trigger_events]]
|
||||
name = "invoice_status_changed"
|
||||
display_name = "发票状态变更"
|
||||
description = "发票状态变化时检查逾期收款"
|
||||
entity = "invoice"
|
||||
on = "update"
|
||||
|
||||
[[trigger_events]]
|
||||
name = "task_status_changed"
|
||||
display_name = "任务状态变更"
|
||||
description = "任务完成或取消时通知"
|
||||
entity = "task"
|
||||
on = "update"
|
||||
|
||||
[[trigger_events]]
|
||||
name = "expense_created"
|
||||
display_name = "新支出记录"
|
||||
description = "记录新支出时通知"
|
||||
entity = "expense"
|
||||
on = "create"
|
||||
```
|
||||
|
||||
### 2.3 Cascade(智能联动下拉)
|
||||
|
||||
选客户后自动过滤其关联数据。以下均为**已有字段追加 cascade 属性**,不是新增字段:
|
||||
|
||||
**contract 实体 — 已有 opportunity_id 字段追加:**
|
||||
```toml
|
||||
cascade_from = "client_id"
|
||||
cascade_filter = "client_id"
|
||||
```
|
||||
|
||||
**contract 实体 — 已有 quote_id 字段追加:**
|
||||
```toml
|
||||
cascade_from = "client_id"
|
||||
cascade_filter = "client_id"
|
||||
```
|
||||
|
||||
**invoice 实体 — 已有 project_id 字段追加:**
|
||||
```toml
|
||||
cascade_from = "client_id"
|
||||
cascade_filter = "client_id"
|
||||
```
|
||||
|
||||
**invoice 实体 — 已有 contract_id 字段追加:**
|
||||
```toml
|
||||
cascade_from = "client_id"
|
||||
cascade_filter = "client_id"
|
||||
```
|
||||
|
||||
**time_entry 实体 — 已有 task_id 字段追加:**
|
||||
```toml
|
||||
cascade_from = "project_id"
|
||||
cascade_filter = "project_id"
|
||||
```
|
||||
|
||||
### 2.4 Visible When(条件显示)
|
||||
|
||||
只在有意义时才显示字段。以下为**已有字段追加 visible_when 属性**:
|
||||
|
||||
**invoice 实体 — 已有 payment_date 字段追加:**
|
||||
```toml
|
||||
visible_when = "status == 'paid' || status == 'partial'"
|
||||
```
|
||||
|
||||
**contract 实体 — 已有 paid_amount 字段追加:**
|
||||
```toml
|
||||
visible_when = "status != 'drafting'"
|
||||
```
|
||||
|
||||
**task 实体 — 已有 actual_hours 字段追加:**
|
||||
```toml
|
||||
visible_when = "status != 'todo'"
|
||||
```
|
||||
|
||||
**quote 实体 — 已有 total_amount 字段追加:**
|
||||
```toml
|
||||
visible_when = "status != 'draft'"
|
||||
```
|
||||
|
||||
### 2.5 Validation(字段校验)
|
||||
|
||||
**已有字段追加 validation 属性**,不是新增字段:
|
||||
|
||||
**client 实体 — 已有 email 字段追加:**
|
||||
```toml
|
||||
validation = { pattern = "^[^@]+@[^@]+\\.[^@]+$", message = "请输入有效的邮箱地址" }
|
||||
```
|
||||
|
||||
**client 实体 — 已有 phone 字段追加:**
|
||||
```toml
|
||||
validation = { pattern = "^1[3-9]\\d{9}$", message = "请输入有效的手机号" }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Layer 2: 仪表盘重构 — freelance 插件
|
||||
|
||||
将占位符仪表盘升级为真正的指挥中心。通过 `widgets` 声明告诉平台该展示什么。
|
||||
|
||||
> **平台依赖:** 仪表盘 widgets 需要平台层配合:
|
||||
> 1. `manifest.rs` 的 `PluginPageType::Dashboard` 需要新增 `widgets: Option<Vec<PluginWidget>>` 字段
|
||||
> 2. 定义 `PluginWidget` 枚举(stat_cards/action_list/funnel/card_list 类型)
|
||||
> 3. 更新 TOML 解析和验证逻辑
|
||||
> 4. 前端解析 `widgets` 声明并渲染对应组件
|
||||
>
|
||||
> 因此 P5/P6 **不是纯 plugin.toml 改动**,需要平台+前端联合实施。以下 widgets 声明作为设计参考,实施时需先完成平台侧支持。
|
||||
|
||||
```toml
|
||||
[[ui.pages]]
|
||||
type = "dashboard"
|
||||
label = "工作台"
|
||||
icon = "DashboardOutlined"
|
||||
|
||||
# ── 财务概览卡片 ──
|
||||
[[ui.pages.widgets]]
|
||||
type = "stat_cards"
|
||||
label = "财务概览"
|
||||
cards = [
|
||||
{ entity = "invoice", aggregate = "sum", field = "amount", filter = "type == 'payment' && status != 'overdue'", label = "本月收入", icon = "rise", color = "green" },
|
||||
{ entity = "expense", aggregate = "sum", field = "amount", label = "本月支出", icon = "fall", color = "red" },
|
||||
{ entity = "invoice", aggregate = "sum", field = "amount", filter = "status == 'overdue' || status == 'pending'", label = "应收总额", icon = "dollar", color = "orange" },
|
||||
{ entity = "invoice", aggregate = "count", filter = "status == 'overdue'", label = "逾期笔数", icon = "warning", color = "red" }
|
||||
]
|
||||
|
||||
# ── 紧急待办 ──
|
||||
[[ui.pages.widgets]]
|
||||
type = "action_list"
|
||||
label = "紧急待办"
|
||||
max_items = 5
|
||||
queries = [
|
||||
{ entity = "invoice", filter = "status == 'overdue'", label_field = "invoice_number", subtitle_field = "amount", action = "查看", icon = "warning" },
|
||||
{ entity = "task", filter = "status != 'done' && status != 'cancelled'", sort = "due_date asc", label_field = "title", subtitle_field = "due_date", action = "处理", icon = "clock" },
|
||||
{ entity = "contract", filter = "status == 'active'", sort = "end_date asc", label_field = "title", subtitle_field = "end_date", action = "续约", icon = "file-text" },
|
||||
{ entity = "opportunity", filter = "next_follow_up <= today", label_field = "title", subtitle_field = "next_follow_up", action = "跟进", icon = "phone" }
|
||||
]
|
||||
|
||||
# ── 商机漏斗 ──
|
||||
[[ui.pages.widgets]]
|
||||
type = "funnel"
|
||||
label = "商机漏斗"
|
||||
entity = "opportunity"
|
||||
lane_field = "stage"
|
||||
value_field = "estimated_amount"
|
||||
lane_order = ["visit", "requirement", "quote", "negotiation", "won", "lost"]
|
||||
|
||||
# ── 活跃项目卡片 ──
|
||||
[[ui.pages.widgets]]
|
||||
type = "card_list"
|
||||
label = "活跃项目"
|
||||
entity = "project"
|
||||
filter = "status == 'in_progress'"
|
||||
max_items = 4
|
||||
title_field = "name"
|
||||
subtitle_field = "contract_amount"
|
||||
tags = ["business_type", "status"]
|
||||
```
|
||||
|
||||
**依赖:** 数据源来自平台已有的聚合 API(`/count`、`/aggregate`)。Filter 表达式使用平台过滤 DSL(`==`, `!=`, `||`, `&&`, `<=`)。
|
||||
|
||||
---
|
||||
|
||||
## 4. Layer 3: 专业输出 — freelance 插件
|
||||
|
||||
一键生成专业 PDF,替代手动排 Word。
|
||||
|
||||
> **模板引擎说明:**
|
||||
> - 语法基于 Handlebars(`{{field}}`, `{{#each relation}}...{{/each}}`)
|
||||
> - 当前实体字段直接可用:`{{amount}}`, `{{status}}`
|
||||
> - 关系字段解析:`{{client.name}}` 表示通过 `client_id` 引用的 client 实体的 name 字段,渲染器需自动解析
|
||||
> - `{{#each lines}}` 用于一对多关系(如 quote → quote_line),渲染器查询子实体并遍历
|
||||
> - 平台需要实现 PDF 渲染管道:TOML 模板 → Handlebars 渲染(注入数据)→ HTML → wkhtmltopdf/浏览器打印 → PDF
|
||||
|
||||
### 4.1 报价单模板
|
||||
|
||||
```toml
|
||||
[[templates]]
|
||||
name = "quote_pdf"
|
||||
display_name = "报价单"
|
||||
entity = "quote"
|
||||
format = "pdf"
|
||||
template_html = """
|
||||
<html>
|
||||
<head><style>
|
||||
body { font-family: 'Microsoft YaHei', sans-serif; margin: 40px; }
|
||||
h1 { text-align: center; border-bottom: 2px solid #333; padding-bottom: 10px; }
|
||||
table { width: 100%; border-collapse: collapse; margin: 20px 0; }
|
||||
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
|
||||
th { background-color: #f5f5f5; }
|
||||
.total { text-align: right; font-size: 18px; font-weight: bold; }
|
||||
.footer { margin-top: 40px; color: #666; font-size: 12px; }
|
||||
</style></head>
|
||||
<body>
|
||||
<h1>报价单 {{quote_number}}</h1>
|
||||
<p>客户:{{client.name}} | 有效期至:{{valid_until}}</p>
|
||||
<table>
|
||||
<tr><th>项目</th><th>描述</th><th>数量</th><th>单价</th><th>金额</th></tr>
|
||||
{{#each lines}}
|
||||
<tr><td>{{item_name}}</td><td>{{description}}</td><td>{{quantity}}</td><td>{{unit_price}}</td><td>{{amount}}</td></tr>
|
||||
{{/each}}
|
||||
</table>
|
||||
<p class="total">小计:{{subtotal}} | 税率:{{tax_rate}}% | 总计:{{total_amount}}</p>
|
||||
<div class="footer">备注:{{notes}}</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
```
|
||||
|
||||
### 4.2 发票模板
|
||||
|
||||
```toml
|
||||
[[templates]]
|
||||
name = "invoice_pdf"
|
||||
display_name = "发票"
|
||||
entity = "invoice"
|
||||
format = "pdf"
|
||||
template_html = """
|
||||
<html>
|
||||
<head><style>
|
||||
body { font-family: 'Microsoft YaHei', sans-serif; margin: 40px; }
|
||||
h1 { text-align: center; color: #1890ff; border-bottom: 2px solid #1890ff; padding-bottom: 10px; }
|
||||
.info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin: 20px 0; }
|
||||
.info-item { padding: 8px; background: #fafafa; }
|
||||
.amount { font-size: 24px; font-weight: bold; text-align: center; color: #f5222d; margin: 20px 0; }
|
||||
.status-badge { display: inline-block; padding: 4px 12px; border-radius: 4px; background: #f0f0f0; }
|
||||
</style></head>
|
||||
<body>
|
||||
<h1>发票 {{invoice_number}}</h1>
|
||||
<div class="info-grid">
|
||||
<div class="info-item">客户:{{client.name}}</div>
|
||||
<div class="info-item">类型:{{type}}</div>
|
||||
<div class="info-item">开票日期:{{issue_date}}</div>
|
||||
<div class="info-item">到期日:{{due_date}}</div>
|
||||
</div>
|
||||
<div class="amount">¥{{amount}}</div>
|
||||
<p>状态:<span class="status-badge">{{status}}</span></p>
|
||||
<p>备注:{{notes}}</p>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
```
|
||||
|
||||
### 4.3 合同模板
|
||||
|
||||
```toml
|
||||
[[templates]]
|
||||
name = "contract_pdf"
|
||||
display_name = "合同"
|
||||
entity = "contract"
|
||||
format = "pdf"
|
||||
template_html = """
|
||||
<html>
|
||||
<head><style>
|
||||
body { font-family: 'Microsoft YaHei', sans-serif; margin: 40px; }
|
||||
h1 { text-align: center; border-bottom: 3px double #333; padding-bottom: 10px; }
|
||||
.parties { margin: 20px 0; padding: 15px; background: #fafafa; border-left: 4px solid #1890ff; }
|
||||
.signature { margin-top: 60px; display: grid; grid-template-columns: 1fr 1fr; gap: 40px; }
|
||||
.sig-box { border-top: 1px solid #333; padding-top: 10px; text-align: center; }
|
||||
</style></head>
|
||||
<body>
|
||||
<h1>{{title}}</h1>
|
||||
<p>合同编号:{{contract_number}}</p>
|
||||
<div class="parties">
|
||||
<p>甲方:{{client.name}}</p>
|
||||
<p>合同金额:¥{{amount}} | 已付:¥{{paid_amount}}</p>
|
||||
<p>期限:{{start_date}} 至 {{end_date}}</p>
|
||||
<p>付款条款:{{payment_terms}}</p>
|
||||
</div>
|
||||
<p>备注:{{notes}}</p>
|
||||
<div class="signature">
|
||||
<div class="sig-box">甲方签章</div>
|
||||
<div class="sig-box">乙方签章</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. itops 插件增强
|
||||
|
||||
### 5.1 Settings
|
||||
|
||||
```toml
|
||||
[settings]
|
||||
|
||||
[[settings.fields]]
|
||||
name = "default_sla_response"
|
||||
display_name = "默认SLA响应时间(小时)"
|
||||
field_type = "number"
|
||||
default_value = 8
|
||||
range = [1.0, 72.0]
|
||||
group = "SLA"
|
||||
|
||||
[[settings.fields]]
|
||||
name = "default_sla_resolve"
|
||||
display_name = "默认SLA解决时间(小时)"
|
||||
field_type = "number"
|
||||
default_value = 48
|
||||
range = [1.0, 168.0]
|
||||
group = "SLA"
|
||||
|
||||
[[settings.fields]]
|
||||
name = "notify_sla_breach"
|
||||
display_name = "SLA超标提醒"
|
||||
field_type = "boolean"
|
||||
default_value = true
|
||||
group = "提醒"
|
||||
|
||||
[[settings.fields]]
|
||||
name = "notify_check_due"
|
||||
display_name = "巡检到期提醒"
|
||||
field_type = "boolean"
|
||||
default_value = true
|
||||
group = "提醒"
|
||||
```
|
||||
|
||||
### 5.2 Trigger Events
|
||||
|
||||
```toml
|
||||
[[trigger_events]]
|
||||
name = "ticket_created"
|
||||
display_name = "新工单"
|
||||
description = "创建工单时开始SLA计时并通知"
|
||||
entity = "ticket"
|
||||
on = "create"
|
||||
|
||||
[[trigger_events]]
|
||||
name = "ticket_status_changed"
|
||||
display_name = "工单状态变更"
|
||||
description = "工单状态变化时检查SLA是否达标"
|
||||
entity = "ticket"
|
||||
on = "update"
|
||||
|
||||
[[trigger_events]]
|
||||
name = "contract_status_changed"
|
||||
display_name = "维保合同状态变更"
|
||||
description = "合同状态变化时检查到期预警"
|
||||
entity = "service_contract"
|
||||
on = "update"
|
||||
|
||||
[[trigger_events]]
|
||||
name = "check_plan_updated"
|
||||
display_name = "巡检计划更新"
|
||||
description = "巡检计划更新时检查下次巡检日期"
|
||||
entity = "check_plan"
|
||||
on = "update"
|
||||
```
|
||||
|
||||
### 5.3 Cascade
|
||||
|
||||
**已有字段追加 cascade 属性**,不是新增字段:
|
||||
|
||||
**ticket 实体 — 已有 contract_id 字段追加:**
|
||||
```toml
|
||||
cascade_from = "client_id"
|
||||
cascade_filter = "client_id"
|
||||
```
|
||||
|
||||
**check_record 实体 — 已有 contract_id 字段追加:**
|
||||
```toml
|
||||
cascade_from = "plan_id"
|
||||
cascade_filter = "contract_id"
|
||||
```
|
||||
|
||||
### 5.4 Visible When
|
||||
|
||||
**已有字段追加 visible_when 属性**:
|
||||
|
||||
**ticket 实体 — 已有 resolution 字段追加:**
|
||||
```toml
|
||||
visible_when = "status == 'resolved' || status == 'closed'"
|
||||
```
|
||||
|
||||
**ticket 实体 — 已有 responded_at 字段追加:**
|
||||
```toml
|
||||
visible_when = "status != 'open'"
|
||||
```
|
||||
|
||||
**ticket 实体 — 已有 resolved_at 字段追加:**
|
||||
```toml
|
||||
visible_when = "status == 'resolved' || status == 'closed'"
|
||||
```
|
||||
|
||||
**ticket 实体 — 已有 closed_at 字段追加:**
|
||||
```toml
|
||||
visible_when = "status == 'closed'"
|
||||
```
|
||||
|
||||
**check_record 实体 — 已有 issues_found 字段追加:**
|
||||
```toml
|
||||
visible_when = "result == 'abnormal'"
|
||||
```
|
||||
|
||||
**check_record 实体 — 已有 actions_taken 字段追加:**
|
||||
```toml
|
||||
visible_when = "result == 'abnormal'"
|
||||
```
|
||||
|
||||
### 5.5 Validation
|
||||
|
||||
**已有字段追加 validation 属性**:
|
||||
|
||||
**service_contract 实体 — 已有 contract_number 字段追加:**
|
||||
```toml
|
||||
validation = { pattern = "^SC-\\d{4}-\\d{4}$", message = "格式:SC-YYYY-NNNN" }
|
||||
```
|
||||
|
||||
### 5.6 Dashboard
|
||||
|
||||
> **同 Layer 2 说明:** widgets 需要平台层配合(manifest.rs 扩展 + 前端渲染),非纯 plugin.toml 改动。此仪表盘页面**插入到现有页面列表最前面**,现有 4 个页面保持不变。
|
||||
|
||||
```toml
|
||||
[[ui.pages]]
|
||||
type = "dashboard"
|
||||
label = "运维概览"
|
||||
icon = "DashboardOutlined"
|
||||
|
||||
[[ui.pages.widgets]]
|
||||
type = "stat_cards"
|
||||
label = "运维概览"
|
||||
cards = [
|
||||
{ entity = "service_contract", aggregate = "count", filter = "status == 'active'", label = "活跃合同", icon = "file-text", color = "blue" },
|
||||
{ entity = "ticket", aggregate = "count", filter = "status == 'open' || status == 'in_progress'", label = "待处理工单", icon = "tool", color = "orange" },
|
||||
{ entity = "ticket", aggregate = "count", filter = "status == 'resolved'", label = "已解决工单", icon = "check-circle", color = "green" },
|
||||
{ entity = "check_plan", aggregate = "count", filter = "status == 'active'", label = "活跃巡检", icon = "schedule", color = "blue" }
|
||||
]
|
||||
|
||||
[[ui.pages.widgets]]
|
||||
type = "action_list"
|
||||
label = "紧急待办"
|
||||
max_items = 5
|
||||
queries = [
|
||||
{ entity = "ticket", filter = "status == 'open'", sort = "priority asc", label_field = "title", subtitle_field = "type", action = "处理", icon = "warning" },
|
||||
{ entity = "service_contract", filter = "status == 'active'", sort = "end_date asc", label_field = "name", subtitle_field = "end_date", action = "续约", icon = "file-text" },
|
||||
{ entity = "check_plan", filter = "status == 'active'", sort = "next_check_date asc", label_field = "name", subtitle_field = "next_check_date", action = "巡检", icon = "schedule" }
|
||||
]
|
||||
```
|
||||
|
||||
### 5.7 Template(维保合同 PDF)
|
||||
|
||||
```toml
|
||||
[[templates]]
|
||||
name = "service_contract_pdf"
|
||||
display_name = "维保合同"
|
||||
entity = "service_contract"
|
||||
format = "pdf"
|
||||
template_html = """
|
||||
<html>
|
||||
<head><style>
|
||||
body { font-family: 'Microsoft YaHei', sans-serif; margin: 40px; }
|
||||
h1 { text-align: center; border-bottom: 3px double #1890ff; padding-bottom: 10px; color: #1890ff; }
|
||||
.sla-box { margin: 20px 0; padding: 15px; background: #e6f7ff; border: 1px solid #91d5ff; border-radius: 4px; }
|
||||
.info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin: 20px 0; }
|
||||
.info-item { padding: 8px; background: #fafafa; }
|
||||
.signature { margin-top: 60px; display: grid; grid-template-columns: 1fr 1fr; gap: 40px; }
|
||||
.sig-box { border-top: 1px solid #333; padding-top: 10px; text-align: center; }
|
||||
</style></head>
|
||||
<body>
|
||||
<h1>{{name}}</h1>
|
||||
<p>合同编号:{{contract_number}}</p>
|
||||
<div class="info-grid">
|
||||
<div class="info-item">客户:{{client.name}}</div>
|
||||
<div class="info-item">合同金额:¥{{amount}}</div>
|
||||
<div class="info-item">期限:{{start_date}} 至 {{end_date}}</div>
|
||||
<div class="info-item">状态:{{status}}</div>
|
||||
</div>
|
||||
<div class="sla-box">
|
||||
<strong>SLA 承诺:</strong>响应 {{sla_response_hours}} 小时内 / 解决 {{sla_resolve_hours}} 小时内
|
||||
</div>
|
||||
<p>服务范围:{{service_scope}}</p>
|
||||
<p>付款条款:{{payment_terms}}</p>
|
||||
<p>备注:{{notes}}</p>
|
||||
<div class="signature">
|
||||
<div class="sig-box">甲方签章</div>
|
||||
<div class="sig-box">乙方签章</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 改进汇总
|
||||
|
||||
| 层次 | 能力 | freelance | itops |
|
||||
|------|------|-----------|-------|
|
||||
| Layer 1 | settings | 7 个配置项(公司名/税率/提醒偏好) | 4 个配置项(SLA默认值/提醒偏好) |
|
||||
| Layer 1 | trigger_events | 5 个事件(商机/合同/发票/任务/支出) | 4 个事件(工单/合同/巡检) |
|
||||
| Layer 1 | cascade | 4 处联动(合同/发票/工时表单) | 2 处联动(工单/巡检记录) |
|
||||
| Layer 1 | visible_when | 4 个条件字段 | 6 个条件字段 |
|
||||
| Layer 1 | validation | 2 个校验(邮箱/手机) | 1 个校验(合同编号格式) |
|
||||
| Layer 2 | dashboard widgets | 财务卡片+紧急待办+商机漏斗+项目卡片 | 运维卡片+紧急待办 |
|
||||
| Layer 3 | templates | 3 个 PDF(报价单/发票/合同) | 1 个 PDF(维保合同) |
|
||||
|
||||
**总计:** 2 个插件 × 3 层增强,从「数据录入」升级为「赚钱工具」。
|
||||
|
||||
---
|
||||
|
||||
## 7. 实施优先级
|
||||
|
||||
```
|
||||
P1: freelance Layer 1(settings + trigger_events + cascade + visible_when + validation)
|
||||
P2: itops Layer 1(settings + trigger_events + cascade + visible_when + validation)
|
||||
P3: freelance Layer 3(3 个 PDF 模板)
|
||||
P4: itops Layer 3(维保合同 PDF 模板)
|
||||
P5: freelance Layer 2(仪表盘 widgets)
|
||||
P6: itops Layer 2(仪表盘 widgets)
|
||||
```
|
||||
|
||||
P1-P4 是纯 plugin.toml 改动(给已有字段追加 cascade/visible_when/validation 属性,以及新增 settings/trigger_events/templates 段落),可立即实施。P5-P6 的仪表盘 widgets 需要平台层配合:扩展 `manifest.rs` 的 `PluginPageType::Dashboard` 支持 `widgets` 字段 + 前端渲染组件。
|
||||
@@ -1,710 +0,0 @@
|
||||
# 健康管理系统 — erp-health 模块设计规格
|
||||
|
||||
> **文档版本**: 1.0
|
||||
> **日期**: 2026-04-23
|
||||
> **状态**: 已确认
|
||||
> **范围**: V1 — 患者管理 + 健康数据 + 预约排班 + 随访管理 + 咨询管理
|
||||
|
||||
---
|
||||
|
||||
## 1. 项目背景
|
||||
|
||||
### 1.1 产品定位
|
||||
|
||||
构建一个面向体检中心/医疗机构的**综合型健康管理平台**,以体检中心为数据源,汇集不同情况的患者,提供全生命周期的健康管理服务。
|
||||
|
||||
本系统从 ERP 平台底座分叉独立,作为 **Health Management System (HMS)** 产品演进。ERP 底座提供身份权限、工作流、消息通知、系统配置等基础能力,`erp-health` 作为原生 Rust 模块承载所有医疗业务逻辑。
|
||||
|
||||
### 1.2 系统架构
|
||||
|
||||
```
|
||||
📱 患者端(微信小程序) ──┐
|
||||
├──→ 🔀 API 网关 ──→ 🖥️ ERP 后端(HMS)
|
||||
👨⚕️ 医护端(小程序/H5) ──┘ │ │
|
||||
│ ├── erp-auth(用户/角色/权限)
|
||||
│ ├── erp-workflow(工作流引擎)
|
||||
│ ├── erp-message(消息通知)
|
||||
│ ├── erp-config(字典/配置)
|
||||
│ └── erp-health(健康管理)★ 新增
|
||||
│
|
||||
└──→ 💾 PostgreSQL + Redis
|
||||
```
|
||||
|
||||
**关键决策:**
|
||||
- ERP 只负责 **PC 管理后台**功能
|
||||
- 小程序(患者端/医护端)作为**独立系统**开发
|
||||
- 数据共享通过 **API 网关**实现
|
||||
- 健康管理使用**原生 Rust 模块**(非 WASM 插件),获得完整的数据库访问和自定义 API 能力
|
||||
|
||||
### 1.3 为什么不用 WASM 插件
|
||||
|
||||
| 限制 | 影响 |
|
||||
|------|------|
|
||||
| 实体上限 20 个 | 综合健康平台轻松超过 |
|
||||
| JSONB 存储 | 医疗数据需要强类型、索引、关联 |
|
||||
| 无自定义 API | 趋势分析、统计报表需要专用端点 |
|
||||
| 无文件上传 | 化验单、体检报告无法存储 |
|
||||
| WASM 沙箱限制 | 无法引入加密、AI、外部 API |
|
||||
|
||||
原生模块遵循现有模式(如 erp-auth、erp-workflow)。**注意:**`ErpModule` trait 没有 `register_routes` 方法。模块通过固有方法 `public_routes()` 和 `protected_routes()` 暴露路由,在 `erp-server` 的 `main.rs` 中通过 `.nest("/api/v1/health", HealthModule::protected_routes())` 集成。通过 EventBus 通信,未来可平滑拆分为独立微服务。
|
||||
|
||||
---
|
||||
|
||||
## 2. V1 功能范围
|
||||
|
||||
| 模块 | 功能 | 页面数 |
|
||||
|------|------|--------|
|
||||
| ① 患者与医护管理 | 患者档案、家庭成员、医护档案、患者标签 | 3 |
|
||||
| ② 健康数据管理 | 体检记录、日常监测、化验报告、趋势分析 | 3 |
|
||||
| ③ 预约与排班 | 预约管理、医生排班、日历视图 | 2 |
|
||||
| ④ 随访管理 | 随访任务、随访记录台账 | 2 |
|
||||
| ⑤ 咨询管理 | 会话管理、对话记录查看/导出 | 2 |
|
||||
| ⑥ 医护管理 | 医护人员列表 | 1 |
|
||||
| **合计** | | **13** |
|
||||
|
||||
**V2 预留:** 积分商城、数据统计中心、内容管理增强。
|
||||
|
||||
---
|
||||
|
||||
## 3. 实体模型
|
||||
|
||||
### 3.1 设计原则
|
||||
|
||||
- 患者和医护的**账号**走 `erp-auth` 的 `users` 表,`erp-health` 只存医疗业务扩展字段
|
||||
- 通过 `user_id` 外键关联 `users` 表
|
||||
- 所有表含 `tenant_id`(多租户隔离)、`id`(UUIDv7)、`created_at`、`updated_at`、`created_by`、`updated_by`、`deleted_at`、`version`
|
||||
- 多对多关系使用中间表
|
||||
|
||||
### 3.2 实体定义
|
||||
|
||||
#### ① 患者与医护管理
|
||||
|
||||
**patient — 患者档案**
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | UUID PK | UUIDv7 |
|
||||
| tenant_id | UUID NOT NULL | 租户 ID |
|
||||
| user_id | UUID FK → users | 关联 erp-auth 账号 |
|
||||
| name | VARCHAR(100) | 姓名 |
|
||||
| gender | VARCHAR(10) | 性别 (male/female/other) |
|
||||
| birth_date | DATE | 出生日期 |
|
||||
| blood_type | VARCHAR(10) | 血型 (A/B/AB/O/RH-/RH+) |
|
||||
| id_number | VARCHAR(20) | 身份证号 |
|
||||
| allergy_history | TEXT | 过敏史 |
|
||||
| medical_history_summary | TEXT | 病史摘要 |
|
||||
| emergency_contact_name | VARCHAR(100) | 紧急联系人姓名 |
|
||||
| emergency_contact_phone | VARCHAR(20) | 紧急联系人电话 |
|
||||
| status | VARCHAR(20) | 状态 (active/inactive/deceased) |
|
||||
| verification_status | VARCHAR(20) | 实名认证 (pending/verified/rejected) |
|
||||
| source | VARCHAR(100) | 来源(体检中心名称) |
|
||||
| notes | TEXT | 备注 |
|
||||
| created_at, updated_at, created_by, updated_by, deleted_at, version | — | 标准字段 |
|
||||
|
||||
索引:`(tenant_id, name)`, `(tenant_id, status)`, `(tenant_id, id_number) UNIQUE WHERE deleted_at IS NULL`
|
||||
|
||||
**patient_family_member — 家庭成员**
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | UUID PK | |
|
||||
| tenant_id | UUID NOT NULL | |
|
||||
| patient_id | UUID FK → patient | 患者关联 |
|
||||
| name | VARCHAR(100) | 姓名 |
|
||||
| relationship | VARCHAR(50) | 关系(父亲/母亲/配偶/子女等) |
|
||||
| phone | VARCHAR(20) | 电话 |
|
||||
| birth_date | DATE | 出生日期 |
|
||||
| notes | TEXT | 备注 |
|
||||
| 标准 ERP 字段 | — | |
|
||||
|
||||
**doctor_profile — 医护档案**
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | UUID PK | |
|
||||
| tenant_id | UUID NOT NULL | |
|
||||
| user_id | UUID FK → users | 关联 erp-auth 账号 |
|
||||
| department | VARCHAR(100) | 科室 |
|
||||
| title | VARCHAR(50) | 职称(主任医师/副主任医师/主治医师等) |
|
||||
| specialty | VARCHAR(200) | 专长 |
|
||||
| license_number | VARCHAR(50) | 执业证号 |
|
||||
| bio | TEXT | 简介 |
|
||||
| online_status | VARCHAR(20) | 在线状态 (online/offline/busy) |
|
||||
| 标准 ERP 字段 | — | |
|
||||
|
||||
索引:`(tenant_id, patient_id)`
|
||||
|
||||
**patient_tag — 患者标签**
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | UUID PK | |
|
||||
| tenant_id | UUID NOT NULL | |
|
||||
| name | VARCHAR(50) | 标签名 |
|
||||
| color | VARCHAR(20) | 颜色值 |
|
||||
| description | TEXT | 描述 |
|
||||
| is_system | BOOLEAN | 系统标签(不可删除) |
|
||||
| 标准 ERP 字段 | — | |
|
||||
|
||||
索引:`UNIQUE (tenant_id, name) WHERE deleted_at IS NULL`
|
||||
|
||||
**patient_tag_relation — 患者-标签关联**
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | UUID PK | |
|
||||
| tenant_id | UUID NOT NULL | |
|
||||
| patient_id | UUID FK → patient | |
|
||||
| tag_id | UUID FK → patient_tag | |
|
||||
| created_at | TIMESTAMPTZ | |
|
||||
| updated_at | TIMESTAMPTZ | |
|
||||
| created_by | UUID | |
|
||||
| updated_by | UUID | |
|
||||
| deleted_at | TIMESTAMPTZ | 软删除 |
|
||||
|
||||
索引:`(tenant_id, patient_id)`, `(tenant_id, tag_id)`
|
||||
|
||||
**patient_doctor_relation — 医患关系**
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | UUID PK | |
|
||||
| tenant_id | UUID NOT NULL | |
|
||||
| patient_id | UUID FK → patient | |
|
||||
| doctor_id | UUID FK → doctor_profile | |
|
||||
| relationship_type | VARCHAR(20) | 类型 (primary/consulting) |
|
||||
| created_at | TIMESTAMPTZ | |
|
||||
| updated_at | TIMESTAMPTZ | |
|
||||
| created_by | UUID | |
|
||||
| updated_by | UUID | |
|
||||
| deleted_at | TIMESTAMPTZ | 软删除 |
|
||||
|
||||
索引:`(tenant_id, patient_id)`, `(tenant_id, doctor_id)`
|
||||
|
||||
#### ② 健康数据管理
|
||||
|
||||
**health_record — 体检/就诊记录**
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | UUID PK | |
|
||||
| tenant_id | UUID NOT NULL | |
|
||||
| patient_id | UUID FK → patient | |
|
||||
| record_type | VARCHAR(20) | 类型 (checkup/outpatient/inpatient) |
|
||||
| record_date | DATE | 记录日期 |
|
||||
| source | VARCHAR(200) | 来源(体检中心/医院名称) |
|
||||
| overall_assessment | TEXT | 总体评估 |
|
||||
| report_file_url | VARCHAR(500) | 报告文件 URL |
|
||||
| notes | TEXT | 备注 |
|
||||
| 标准 ERP 字段 | — | |
|
||||
|
||||
索引:`(tenant_id, patient_id, record_date DESC)`
|
||||
|
||||
**vital_signs — 日常监测数据**
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | UUID PK | |
|
||||
| tenant_id | UUID NOT NULL | |
|
||||
| patient_id | UUID FK → patient | |
|
||||
| record_date | DATE | 记录日期 |
|
||||
| systolic_bp_morning | INTEGER | 晨起收缩压 |
|
||||
| diastolic_bp_morning | INTEGER | 晨起舒张压 |
|
||||
| systolic_bp_evening | INTEGER | 晚间收缩压 |
|
||||
| diastolic_bp_evening | INTEGER | 晚间舒张压 |
|
||||
| heart_rate | INTEGER | 心率 |
|
||||
| weight | DECIMAL(5,1) | 体重 (kg) |
|
||||
| blood_sugar | DECIMAL(5,1) | 血糖 (mmol/L) |
|
||||
| water_intake_ml | INTEGER | 饮水量 (ml) |
|
||||
| urine_output_ml | INTEGER | 尿量 (ml) |
|
||||
| notes | TEXT | 备注 |
|
||||
| 标准 ERP 字段 | — | |
|
||||
|
||||
索引:`(tenant_id, patient_id, record_date DESC)`
|
||||
|
||||
**lab_report — 化验报告**
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | UUID PK | |
|
||||
| tenant_id | UUID NOT NULL | |
|
||||
| patient_id | UUID FK → patient | |
|
||||
| report_date | DATE | 报告日期 |
|
||||
| report_type | VARCHAR(50) | 报告类型(肾功能/血常规/尿常规等) |
|
||||
| indicators | JSONB | 指标数据 [{name, value, unit, ref_range, is_abnormal}] |
|
||||
| image_urls | JSONB | 图片 URLs [url1, url2, ...] |
|
||||
| doctor_interpretation | TEXT | 医生解读 |
|
||||
| 标准 ERP 字段 | — | |
|
||||
|
||||
索引:`(tenant_id, patient_id, report_date DESC)`, GIN on `indicators`, `(tenant_id, report_type)`
|
||||
|
||||
**health_trend — 健康趋势报告**
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | UUID PK | |
|
||||
| tenant_id | UUID NOT NULL | |
|
||||
| patient_id | UUID FK → patient | |
|
||||
| period_start | DATE | 周期开始 |
|
||||
| period_end | DATE | 周期结束 |
|
||||
| indicator_summary | JSONB | 指标摘要 |
|
||||
| abnormal_items | JSONB | 异常项 |
|
||||
| generation_type | VARCHAR(20) | 生成方式 (auto/manual) |
|
||||
| report_file_url | VARCHAR(500) | 报告文件 URL |
|
||||
| 标准 ERP 字段 | — | |
|
||||
|
||||
索引:`(tenant_id, patient_id, period_start DESC)`
|
||||
|
||||
#### ③ 预约排班
|
||||
|
||||
**appointment — 预约记录**
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | UUID PK | |
|
||||
| tenant_id | UUID NOT NULL | |
|
||||
| patient_id | UUID FK → patient | |
|
||||
| doctor_id | UUID FK → doctor_profile | |
|
||||
| appointment_type | VARCHAR(20) | 类型 (dialysis/recheck/outpatient) |
|
||||
| appointment_date | DATE | 预约日期 |
|
||||
| start_time | TIME | 开始时间 |
|
||||
| end_time | TIME | 结束时间 |
|
||||
| status | VARCHAR(20) | 状态 (pending/confirmed/cancelled/completed/no_show) |
|
||||
| cancel_reason | TEXT | 取消原因 |
|
||||
| notes | TEXT | 备注 |
|
||||
| 标准 ERP 字段 | — | |
|
||||
|
||||
索引:`(tenant_id, appointment_date, status)`, `(tenant_id, doctor_id, appointment_date)`
|
||||
|
||||
**doctor_schedule — 医生排班**
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | UUID PK | |
|
||||
| tenant_id | UUID NOT NULL | |
|
||||
| doctor_id | UUID FK → doctor_profile | |
|
||||
| schedule_date | DATE | 排班日期 |
|
||||
| period_type | VARCHAR(20) | 时段 (am/pm/night/full_day) |
|
||||
| start_time | TIME | 开始时间 |
|
||||
| end_time | TIME | 结束时间 |
|
||||
| max_appointments | INTEGER | 最大预约数 |
|
||||
| current_appointments | INTEGER | 已预约数(默认 0) |
|
||||
| status | VARCHAR(20) | 状态 (enabled/disabled) |
|
||||
| 标准 ERP 字段 | — |
|
||||
|
||||
索引:`(tenant_id, doctor_id, schedule_date)`, `UNIQUE (tenant_id, doctor_id, schedule_date, period_type) WHERE deleted_at IS NULL`
|
||||
|
||||
**预约并发控制:** 创建预约时使用原子 CAS 操作 `UPDATE doctor_schedule SET current_appointments = current_appointments + 1 WHERE id = $1 AND current_appointments < max_appointments RETURNING *`,防止超额预约。
|
||||
|
||||
#### ④ 随访管理
|
||||
|
||||
**follow_up_task — 随访任务**
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | UUID PK | |
|
||||
| tenant_id | UUID NOT NULL | |
|
||||
| patient_id | UUID FK → patient | |
|
||||
| assigned_to | UUID FK → users | 负责医护 |
|
||||
| follow_up_type | VARCHAR(20) | 类型 (phone/face_to_face/online) |
|
||||
| planned_date | DATE | 计划日期 |
|
||||
| status | VARCHAR(20) | 状态 (pending/in_progress/completed/overdue/cancelled) |
|
||||
| content_template | TEXT | 随访内容模板 |
|
||||
| related_appointment_id | UUID FK → appointment | 关联预约 |
|
||||
| 标准 ERP 字段 | — | |
|
||||
|
||||
索引:`(tenant_id, assigned_to, status)`, `(tenant_id, planned_date, status)`
|
||||
|
||||
**follow_up_record — 随访记录**
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | UUID PK | |
|
||||
| tenant_id | UUID NOT NULL | |
|
||||
| task_id | UUID FK → follow_up_task | |
|
||||
| executed_by | UUID FK → users | 执行医护 |
|
||||
| executed_date | DATE | 执行日期 |
|
||||
| result | VARCHAR(20) | 结果 (followed_up/unreachable/refused/other) |
|
||||
| patient_condition | TEXT | 患者状况 |
|
||||
| medical_advice | TEXT | 医嘱建议 |
|
||||
| next_follow_up_date | DATE | 下次随访日期 |
|
||||
| 标准 ERP 字段 | — | |
|
||||
|
||||
索引:`(tenant_id, task_id)`, `(tenant_id, executed_date)`
|
||||
|
||||
#### ⑤ 咨询管理
|
||||
|
||||
**consultation_session — 咨询会话**
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | UUID PK | |
|
||||
| tenant_id | UUID NOT NULL | |
|
||||
| patient_id | UUID FK → patient | |
|
||||
| doctor_id | UUID FK → doctor_profile | |
|
||||
| type | VARCHAR(20) | 类型 (customer_service/doctor) |
|
||||
| status | VARCHAR(20) | 状态 (waiting/active/closed) |
|
||||
| last_message_at | TIMESTAMPTZ | 最后消息时间 |
|
||||
| unread_count_patient | INTEGER | 患者未读数 |
|
||||
| unread_count_doctor | INTEGER | 医生未读数 |
|
||||
| 标准 ERP 字段 | — | |
|
||||
|
||||
索引:`(tenant_id, doctor_id, status)`, `(tenant_id, patient_id, status)`
|
||||
|
||||
**consultation_message — 咨询消息**
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | UUID PK | |
|
||||
| tenant_id | UUID NOT NULL | |
|
||||
| session_id | UUID FK → consultation_session | |
|
||||
| sender_id | UUID | 发送者 ID |
|
||||
| sender_role | VARCHAR(20) | 角色 (patient/doctor/system) |
|
||||
| content_type | VARCHAR(20) | 类型 (text/image/voice/file) |
|
||||
| content | TEXT | 内容 |
|
||||
| is_read | BOOLEAN | 已读状态(默认 false) |
|
||||
| created_at | TIMESTAMPTZ | 发送时间 |
|
||||
| updated_at | TIMESTAMPTZ | |
|
||||
| created_by | UUID | |
|
||||
| updated_by | UUID | |
|
||||
| deleted_at | TIMESTAMPTZ | 软删除(内容审核用) |
|
||||
| version | INT NOT NULL DEFAULT 1 | 乐观锁 |
|
||||
|
||||
索引:`(tenant_id, session_id, created_at)`
|
||||
|
||||
**数据增长策略:** 对 `created_at` 按月分区(PostgreSQL table partitioning),超过 1 年的已关闭会话消息归档到冷存储。
|
||||
|
||||
**说明:**
|
||||
- `patient.user_id` 允许 NULL — 患者可先创建档案(如体检中心导入),后续再绑定 erp-auth 账号
|
||||
- `consultation_message.sender_id` 引用 `users.id` — 统一使用 erp-auth 用户体系标识发送者
|
||||
|
||||
---
|
||||
|
||||
## 3.3 状态机定义
|
||||
|
||||
### appointment.status 转换
|
||||
|
||||
```
|
||||
pending ──→ confirmed ──→ completed
|
||||
│ │
|
||||
│ └──→ no_show(预约时间过后,系统自动或前台手动触发)
|
||||
│
|
||||
└──→ cancelled(任意时刻可取消,需填 cancel_reason)
|
||||
```
|
||||
|
||||
### follow_up_task.status 转换
|
||||
|
||||
```
|
||||
pending ──→ in_progress ──→ completed
|
||||
│ │
|
||||
└──→ cancelled └──→ overdue(系统定时任务:planned_date 已过且仍 pending 自动标记)
|
||||
```
|
||||
|
||||
### consultation_session.status 转换
|
||||
|
||||
```
|
||||
waiting ──→ active(第一条消息发送时自动触发)──→ closed(手动关闭或超时自动关闭)
|
||||
```
|
||||
|
||||
### patient.status 转换
|
||||
|
||||
```
|
||||
active ──→ inactive(手动停用)
|
||||
active ──→ deceased(标记死亡,不可逆)
|
||||
inactive ──→ active(重新激活)
|
||||
```
|
||||
|
||||
### patient.verification_status 转换
|
||||
|
||||
```
|
||||
pending ──→ verified(实名认证通过)
|
||||
pending ──→ rejected(认证被拒)
|
||||
rejected ──→ pending(重新提交认证)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. API 设计
|
||||
|
||||
所有端点前缀: `/api/v1/health/`
|
||||
|
||||
### 4.1 患者管理
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | `/patients` | 患者列表(分页、搜索、标签筛选) |
|
||||
| POST | `/patients` | 创建患者 |
|
||||
| GET | `/patients/:id` | 患者详情 |
|
||||
| PUT | `/patients/:id` | 更新患者 |
|
||||
| DELETE | `/patients/:id` | 软删除 |
|
||||
| POST | `/patients/:id/tags` | 管理标签(批量设置) |
|
||||
| GET | `/patients/:id/health-summary` | 健康摘要 |
|
||||
| GET | `/patients/:id/family-members` | 家庭成员列表 |
|
||||
| POST | `/patients/:id/family-members` | 新增家庭成员 |
|
||||
| PUT | `/patients/:id/family-members/:fid` | 更新家庭成员 |
|
||||
| DELETE | `/patients/:id/family-members/:fid` | 删除家庭成员 |
|
||||
| POST | `/patients/:id/doctors` | 分配主治医生 |
|
||||
| DELETE | `/patients/:id/doctors/:did` | 移除医患关系 |
|
||||
|
||||
### 4.2 健康数据
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | `/patients/:id/vital-signs` | 日常监测列表 |
|
||||
| POST | `/patients/:id/vital-signs` | 新增监测数据 |
|
||||
| GET | `/patients/:id/lab-reports` | 化验报告列表 |
|
||||
| POST | `/patients/:id/lab-reports` | 新增化验报告 |
|
||||
| GET | `/patients/:id/health-records` | 体检/就诊记录 |
|
||||
| POST | `/patients/:id/health-records` | 新增记录 |
|
||||
| GET | `/patients/:id/trends` | 趋势报告 |
|
||||
| POST | `/patients/:id/trends/generate` | 生成趋势报告 |
|
||||
| GET | `/patients/:id/trends/:indicator` | 单指标时序数据 |
|
||||
| PUT | `/patients/:id/vital-signs/:vid` | 更新监测数据 |
|
||||
| DELETE | `/patients/:id/vital-signs/:vid` | 删除监测数据 |
|
||||
| PUT | `/patients/:id/lab-reports/:rid` | 更新化验报告 |
|
||||
| DELETE | `/patients/:id/lab-reports/:rid` | 删除化验报告 |
|
||||
| PUT | `/patients/:id/health-records/:rid` | 更新体检记录 |
|
||||
| DELETE | `/patients/:id/health-records/:rid` | 删除体检记录 |
|
||||
|
||||
### 4.3 预约排班
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | `/appointments` | 预约列表 |
|
||||
| POST | `/appointments` | 创建预约 |
|
||||
| PUT | `/appointments/:id/status` | 更新状态 |
|
||||
| GET | `/doctor-schedules` | 排班列表 |
|
||||
| POST | `/doctor-schedules` | 创建排班 |
|
||||
| PUT | `/doctor-schedules/:id` | 更新排班 |
|
||||
| GET | `/doctor-schedules/calendar` | 日历视图 |
|
||||
|
||||
### 4.4 随访管理
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | `/follow-up-tasks` | 任务列表 |
|
||||
| POST | `/follow-up-tasks` | 创建任务 |
|
||||
| PUT | `/follow-up-tasks/:id` | 更新任务 |
|
||||
| DELETE | `/follow-up-tasks/:id` | 删除任务 |
|
||||
| POST | `/follow-up-tasks/:id/records` | 填写随访记录 |
|
||||
| GET | `/follow-up-records` | 随访台账 |
|
||||
|
||||
### 4.5 咨询管理
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | `/consultation-sessions` | 会话列表 |
|
||||
| GET | `/consultation-sessions/:id/messages` | 消息记录 |
|
||||
| PUT | `/consultation-sessions/:id/close` | 关闭会话 |
|
||||
| POST | `/consultation-messages` | 写入消息(API 网关用) |
|
||||
| GET | `/consultation-sessions/export` | 导出 |
|
||||
|
||||
### 4.6 医护管理
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | `/doctors` | 医护列表 |
|
||||
| POST | `/doctors` | 创建医护档案 |
|
||||
| GET | `/doctors/:id` | 医护详情 |
|
||||
| PUT | `/doctors/:id` | 更新医护档案 |
|
||||
| DELETE | `/doctors/:id` | 软删除医护档案 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 前端页面设计
|
||||
|
||||
文件位置: `apps/web/src/pages/health/`
|
||||
|
||||
### 5.1 页面清单
|
||||
|
||||
| # | 页面 | 文件名 | 类型 |
|
||||
|---|------|--------|------|
|
||||
| 1 | 患者列表 | PatientList.tsx | 表格+搜索+标签筛选+导出 |
|
||||
| 2 | 患者详情 | PatientDetail.tsx | Tab布局:基本信息/健康趋势/化验报告/就诊记录/随访记录 |
|
||||
| 3 | 标签管理 | PatientTagManage.tsx | CRUD+颜色+批量打标 |
|
||||
| 4 | 日常监测 | VitalSignsList.tsx | 按患者+日期+ECharts趋势折线图 |
|
||||
| 5 | 化验报告 | LabReportList.tsx | 列表+图片预览+指标详情+解读 |
|
||||
| 6 | 体检记录 | HealthRecordList.tsx | 类型筛选+报告文件查看/上传 |
|
||||
| 7 | 预约管理 | AppointmentList.tsx | 列表/日历切换+状态流转 |
|
||||
| 8 | 排班管理 | DoctorSchedule.tsx | 周/月日历+排班模板 |
|
||||
| 9 | 随访任务 | FollowUpTaskList.tsx | 任务CRUD+分配+关联工作流 |
|
||||
| 10 | 随访台账 | FollowUpRecordList.tsx | 按患者/医护/日期筛选+导出 |
|
||||
| 11 | 会话管理 | ConsultationList.tsx | 列表+未回复统计 |
|
||||
| 12 | 对话记录 | ConsultationDetail.tsx | 聊天气泡+图片/语音查看+导出 |
|
||||
| 13 | 医护列表 | DoctorList.tsx | 列表+科室筛选+在线状态 |
|
||||
|
||||
### 5.2 技术要点
|
||||
|
||||
- **ECharts 趋势图** — 血压/体重/血糖曲线图,按日期范围展示
|
||||
- **文件上传/预览** — 化验单图片、体检报告 PDF(需新增基础能力)
|
||||
- **日历组件** — Ant Design Calendar 用于排班和预约视图
|
||||
- **聊天 UI** — 消息气泡展示(只读,非实时聊天)
|
||||
- **导出** — 随访台账、咨询记录导出为 Excel
|
||||
|
||||
---
|
||||
|
||||
## 6. 事件集成
|
||||
|
||||
### 6.1 发布事件
|
||||
|
||||
| 事件类型 | 触发时机 | 载荷 |
|
||||
|----------|----------|------|
|
||||
| `patient.created` | 创建患者 | `{patient_id, name, tenant_id}` |
|
||||
| `patient.updated` | 更新患者信息 | `{patient_id, changed_fields}` |
|
||||
| `appointment.created` | 创建预约 | `{appointment_id, patient_id, doctor_id, date}` |
|
||||
| `appointment.confirmed` | 确认预约 | `{appointment_id}` |
|
||||
| `appointment.cancelled` | 取消预约 | `{appointment_id, cancel_reason}` |
|
||||
| `appointment.completed` | 完成就诊 | `{appointment_id}` |
|
||||
| `follow_up.created` | 创建随访任务 | `{task_id, patient_id, assigned_to, planned_date}` |
|
||||
| `follow_up.completed` | 完成随访 | `{task_id, record_id, result}` |
|
||||
| `lab_report.uploaded` | 上传化验报告 | `{report_id, patient_id, report_type, abnormal_count}` |
|
||||
| `consultation.opened` | 开启咨询 | `{session_id, patient_id, doctor_id}` |
|
||||
| `consultation.closed` | 关闭咨询 | `{session_id}` |
|
||||
| `patient.deceased` | 患者死亡标记 | `{patient_id}` |
|
||||
| `patient.verified` | 实名认证通过 | `{patient_id, id_number}` |
|
||||
| `follow_up.overdue` | 随访任务逾期 | `{task_id, patient_id, planned_date}` |
|
||||
| `doctor.online_status_changed` | 医护在线状态变更 | `{doctor_id, old_status, new_status}` |
|
||||
|
||||
**随访记录自动创建后续任务:** 当 `follow_up_record.next_follow_up_date` 不为空时,服务层自动创建新的 `follow_up_task`(planned_date = next_follow_up_date,assigned_to 沿用当前医护)。
|
||||
|
||||
### 6.2 订阅事件
|
||||
|
||||
| 事件类型 | 处理逻辑 |
|
||||
|----------|----------|
|
||||
| `workflow.task.completed` | 工作流任务完成时更新随访任务状态 |
|
||||
| `message.sent` | 消息发送时联动咨询会话的 last_message_at |
|
||||
|
||||
---
|
||||
|
||||
## 7. 模块结构
|
||||
|
||||
```
|
||||
crates/erp-health/
|
||||
├── Cargo.toml
|
||||
├── src/
|
||||
│ ├── lib.rs ← ErpModule trait + public_routes() / protected_routes()
|
||||
│ ├── error.rs ← HealthError → AppError
|
||||
│ ├── state.rs ← HealthState (共享状态)
|
||||
│ ├── entity/ ← SeaORM Entity
|
||||
│ │ ├── mod.rs
|
||||
│ │ ├── patient.rs
|
||||
│ │ ├── patient_family_member.rs
|
||||
│ │ ├── patient_tag.rs
|
||||
│ │ ├── patient_tag_relation.rs
|
||||
│ │ ├── patient_doctor_relation.rs
|
||||
│ │ ├── doctor_profile.rs
|
||||
│ │ ├── health_record.rs
|
||||
│ │ ├── vital_signs.rs
|
||||
│ │ ├── lab_report.rs
|
||||
│ │ ├── health_trend.rs
|
||||
│ │ ├── appointment.rs
|
||||
│ │ ├── doctor_schedule.rs
|
||||
│ │ ├── follow_up_task.rs
|
||||
│ │ ├── follow_up_record.rs
|
||||
│ │ ├── consultation_session.rs
|
||||
│ │ └── consultation_message.rs
|
||||
│ ├── service/ ← 业务逻辑
|
||||
│ │ ├── mod.rs
|
||||
│ │ ├── patient_service.rs
|
||||
│ │ ├── health_data_service.rs
|
||||
│ │ ├── appointment_service.rs
|
||||
│ │ ├── follow_up_service.rs
|
||||
│ │ └── consultation_service.rs
|
||||
│ ├── handler/ ← Axum 路由
|
||||
│ │ ├── mod.rs
|
||||
│ │ ├── patient_handler.rs
|
||||
│ │ ├── health_data_handler.rs
|
||||
│ │ ├── appointment_handler.rs
|
||||
│ │ ├── follow_up_handler.rs
|
||||
│ │ └── consultation_handler.rs
|
||||
│ ├── dto/ ← 请求/响应结构体
|
||||
│ │ ├── mod.rs
|
||||
│ │ ├── patient_dto.rs
|
||||
│ │ ├── health_data_dto.rs
|
||||
│ │ ├── appointment_dto.rs
|
||||
│ │ ├── follow_up_dto.rs
|
||||
│ │ └── consultation_dto.rs
|
||||
│ └── event.rs ← 事件定义和处理器
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 权限定义
|
||||
|
||||
### 8.1 权限码
|
||||
|
||||
| 权限码 | 名称 | 说明 |
|
||||
|--------|------|------|
|
||||
| `health.patient.list` | 查看患者列表 | 查看和搜索患者列表、详情 |
|
||||
| `health.patient.manage` | 管理患者 | 创建、编辑、删除患者 |
|
||||
| `health.health-data.list` | 查看健康数据 | 查看体检记录、监测数据、化验报告 |
|
||||
| `health.health-data.manage` | 管理健康数据 | 录入、编辑、删除健康数据 |
|
||||
| `health.appointment.list` | 查看预约 | 查看预约列表和排班 |
|
||||
| `health.appointment.manage` | 管理预约 | 创建、确认、取消预约 |
|
||||
| `health.follow-up.list` | 查看随访 | 查看随访任务和记录 |
|
||||
| `health.follow-up.manage` | 管理随访 | 创建、分配、完成随访任务 |
|
||||
| `health.consultation.list` | 查看咨询 | 查看咨询会话和消息记录 |
|
||||
| `health.consultation.manage` | 管理咨询 | 关闭会话、导出记录 |
|
||||
| `health.doctor.list` | 查看医护 | 查看医护列表和详情 |
|
||||
| `health.doctor.manage` | 管理医护 | 创建、编辑医护档案、排班 |
|
||||
|
||||
### 8.2 数据范围
|
||||
|
||||
| 实体 | 支持的数据范围级别 | 说明 |
|
||||
|------|-------------------|------|
|
||||
| patient | self, department, department_tree, all | 医生只能看自己负责的患者或本科室患者 |
|
||||
| follow_up_task | self, department, department_tree, all | 医护只能看分配给自己的随访任务 |
|
||||
| appointment | self, department, department_tree, all | 按科室隔离预约数据 |
|
||||
|
||||
### 8.3 角色模板
|
||||
|
||||
| 角色 | 权限 |
|
||||
|------|------|
|
||||
| health_admin | 全部 health.* 权限 |
|
||||
| doctor | health.patient.list, health.health-data.*, health.appointment.list, health.follow-up.*, health.consultation.list, health.doctor.list |
|
||||
| nurse | health.patient.list, health.health-data.*, health.follow-up.*, health.appointment.list |
|
||||
| receptionist | health.patient.*, health.appointment.*, health.doctor.list |
|
||||
|
||||
---
|
||||
|
||||
## 9. 能力扩展
|
||||
|
||||
V1 需要新增以下基础能力(在 erp-core 或独立模块中):
|
||||
|
||||
1. **文件上传服务** — 文件存储(本地/OSS)、URL 生成、图片缩略图
|
||||
2. **趋势分析** — 时序数据聚合、异常检测逻辑
|
||||
3. **报告批注** — 医生对化验报告的解读/批注能力
|
||||
4. **导出增强** — 健康数据导出为 Excel/PDF
|
||||
|
||||
---
|
||||
|
||||
## 10. 实施步骤
|
||||
|
||||
### Phase 1: 项目初始化
|
||||
- 拷贝 ERP 到 hms
|
||||
- 验证编译和构建
|
||||
|
||||
### Phase 2: erp-health 骨架
|
||||
- 创建 crate 结构
|
||||
- 实现 ErpModule trait + `public_routes()` / `protected_routes()` 固有方法
|
||||
- 注册到 workspace
|
||||
|
||||
### Phase 3: 数据库迁移
|
||||
- 16 张表(14 业务实体 + 2 关联表)的迁移文件
|
||||
- 索引创建、唯一约束
|
||||
|
||||
### Phase 4: 业务逻辑(按域迭代)
|
||||
- ① 患者与医护管理
|
||||
- ② 健康数据管理
|
||||
- ③ 预约排班
|
||||
- ④ 随访管理
|
||||
- ⑤ 咨询管理
|
||||
|
||||
### Phase 5: 前端页面
|
||||
- 13 个自定义 React 页面
|
||||
- 路由注册和侧边栏菜单
|
||||
|
||||
### Phase 6: 集成测试
|
||||
- API 端点测试
|
||||
- 多租户隔离验证
|
||||
- 端到端功能验证
|
||||
@@ -1,509 +0,0 @@
|
||||
# HMS 患者小程序设计规格
|
||||
|
||||
> **版本**: v1.0
|
||||
> **日期**: 2026-04-23
|
||||
> **状态**: 草案
|
||||
> **关联**: 健康模块设计规格 `2026-04-23-health-management-module-design.md`
|
||||
|
||||
---
|
||||
|
||||
## 1. 概述
|
||||
|
||||
### 1.1 产品定位
|
||||
|
||||
HMS 患者小程序是**综合健康管理入口**,面向体检中心/医疗机构的患者。覆盖体检预约、报告查询、健康数据长期监测、随访管理、家庭健康管理等场景。
|
||||
|
||||
医护端以 PC 管理后台(`apps/web/`)为主力,小程序聚焦患者体验。医护端小程序可在后续按需补一个轻量版(随访提醒、排班查看),不在本规格范围内。
|
||||
|
||||
### 1.2 核心决策
|
||||
|
||||
| 维度 | 决策 | 原因 |
|
||||
|------|------|------|
|
||||
| 技术选型 | Taro 4 + React 19 | 与 Web 端 React 技能复用,支持多端编译 |
|
||||
| 架构方案 | 直连后端 | MVP 阶段最务实,复用 erp-server API |
|
||||
| 登录方式 | 微信授权 + 手机号补充 | 降低门槛 + 确保身份可靠 |
|
||||
| 代码位置 | `apps/miniprogram/`(Monorepo) | 方便接口同步,共享类型定义 |
|
||||
| 目标平台 | 微信小程序优先 | 覆盖最广泛用户,后续可扩展 |
|
||||
| 数据录入 | 手动 + 蓝牙预留接口 | MVP 快速交付,后续对接设备 |
|
||||
| 视觉风格 | 医疗清新(青色主调) | 专业可靠,沿用现有 HTML 原型风格 |
|
||||
|
||||
### 1.3 MVP 功能范围
|
||||
|
||||
**MVP 包含(7 个功能模块):**
|
||||
1. 登录 + 个人中心
|
||||
2. 健康数据录入 + 趋势图
|
||||
3. 预约挂号
|
||||
4. 报告查询
|
||||
5. 随访管理
|
||||
6. 家庭健康管理(就诊人切换)
|
||||
7. 健康资讯 + 用药提醒
|
||||
|
||||
**后续版本:**
|
||||
- 在线咨询(即时通讯,WebSocket 长连接)
|
||||
|
||||
---
|
||||
|
||||
## 2. 项目结构
|
||||
|
||||
```
|
||||
apps/miniprogram/
|
||||
├── config/ # Taro 编译配置
|
||||
│ ├── index.ts # 通用配置
|
||||
│ ├── dev.ts # 开发环境
|
||||
│ └── prod.ts # 生产环境
|
||||
├── project.config.json # 微信小程序项目配置
|
||||
├── src/
|
||||
│ ├── app.config.ts # Taro 全局配置(TabBar、页面路由)
|
||||
│ ├── app.tsx # 入口组件
|
||||
│ ├── app.scss # 全局样式(医疗清新主题变量)
|
||||
│ ├── components/ # 通用组件
|
||||
│ │ ├── HealthCard/ # 健康指标卡片(血压/血糖/体重)
|
||||
│ │ ├── AppointmentCard/ # 预约卡片
|
||||
│ │ ├── ReportItem/ # 报告列表项
|
||||
│ │ ├── FamilyPicker/ # 就诊人切换器
|
||||
│ │ ├── EmptyState/ # 空状态占位
|
||||
│ │ └── TrendChart/ # 趋势图(echarts-taro3-react)
|
||||
│ ├── pages/
|
||||
│ │ ├── index/ # 首页(今日健康+快捷入口+待办)
|
||||
│ │ ├── health/ # 健康数据(录入+趋势图)
|
||||
│ │ ├── appointment/ # 预约(列表+新建预约)
|
||||
│ │ ├── report/ # 报告(体检报告+化验单)
|
||||
│ │ ├── followup/ # 随访(任务+问卷填写)
|
||||
│ │ ├── article/ # 健康资讯(文章列表+详情)
|
||||
│ │ ├── profile/ # 我的(个人信息+就诊人管理+设置)
|
||||
│ │ └── login/ # 登录(微信授权+手机号)
|
||||
│ ├── services/ # API 调用层
|
||||
│ │ ├── request.ts # 封装 Taro.request(JWT 注入、错误处理)
|
||||
│ │ ├── auth.ts # 登录/刷新 token
|
||||
│ │ ├── health.ts # 健康数据 CRUD
|
||||
│ │ ├── appointment.ts # 预约 CRUD
|
||||
│ │ ├── report.ts # 报告查询
|
||||
│ │ ├── followup.ts # 随访任务/记录
|
||||
│ │ └── article.ts # 资讯/科普
|
||||
│ ├── stores/ # Zustand 状态管理
|
||||
│ │ ├── auth.ts # 登录态、用户信息、就诊人列表
|
||||
│ │ └── health.ts # 健康数据缓存
|
||||
│ ├── utils/
|
||||
│ │ ├── bluetooth.ts # 蓝牙接口预留(MVP 不实现)
|
||||
│ │ ├── format.ts # 日期/数值格式化
|
||||
│ │ └── constants.ts # 常量定义
|
||||
│ └── styles/
|
||||
│ ├── variables.scss # 主题变量(青色主调)
|
||||
│ └── mixins.scss # 常用样式混入
|
||||
├── package.json
|
||||
└── tsconfig.json
|
||||
```
|
||||
|
||||
**设计原则:**
|
||||
- services 层与 Web 端 `apps/web/src/api/` 职责对齐,用 Taro.request 替代 fetch
|
||||
- stores 复用 Zustand 模式,与 Web 端保持一致的状态管理风格
|
||||
- 组件命名 PascalCase 目录,与 Web 端风格统一
|
||||
- MVP 阶段不强抽取 `packages/shared/`,等两端跑起来后根据重复度决定
|
||||
|
||||
---
|
||||
|
||||
## 3. 认证流程
|
||||
|
||||
### 3.1 整体流程
|
||||
|
||||
```
|
||||
用户打开小程序
|
||||
↓
|
||||
检查本地 storage 有无有效 JWT
|
||||
├── 有且未过期 → 直接进入首页
|
||||
└── 无或已过期 ↓
|
||||
|
||||
Step 1: 微信静默登录
|
||||
wx.login() → code
|
||||
→ POST /api/v1/auth/wechat/login { code }
|
||||
→ 后端用 code 换 openid,查找绑定用户
|
||||
├── 已绑定 → 签发 JWT { token, user }
|
||||
└── 未绑定 → 返回 { need_bind: true, openid }
|
||||
|
||||
Step 2: 手机号绑定(仅新用户)
|
||||
wx.getPhoneNumber 按钮组件 → encryptedData + iv
|
||||
→ POST /api/v1/auth/wechat/bind-phone { openid, encryptedData, iv }
|
||||
→ 后端解密手机号,创建/关联 user + patient 档案
|
||||
→ 签发 JWT { token, user, patient }
|
||||
|
||||
Step 3: 补充档案(首次绑定后)
|
||||
→ 引导填写姓名、性别、出生日期、身份证号(可选)
|
||||
→ PUT /api/v1/health/patients/me { name, gender, birthday }
|
||||
```
|
||||
|
||||
### 3.2 后端新增内容
|
||||
|
||||
**`erp-auth` 新增:**
|
||||
|
||||
| 新增 | 说明 |
|
||||
|------|------|
|
||||
| `wechat_users` 表 | `id, openid, union_id, user_id, phone, created_at, updated_at` |
|
||||
| `POST /api/v1/auth/wechat/login` | code → openid 查询,返回绑定状态 |
|
||||
| `POST /api/v1/auth/wechat/bind-phone` | 绑定手机号,创建 user + patient |
|
||||
| `GET /api/v1/auth/wechat/qrcode` | 生成带参数小程序码(PC 端扫码登录场景) |
|
||||
|
||||
`wechat_users` 表必须包含 `tenant_id`(多租户隔离)和标准审计字段(`created_at`, `updated_at`, `deleted_at`)。
|
||||
|
||||
### 3.3 Token 策略
|
||||
|
||||
| Token | 有效期 | 存储 |
|
||||
|-------|--------|------|
|
||||
| Access Token (JWT) | 15 分钟 | 内存 + Taro.setStorage |
|
||||
| Refresh Token | 7 天 | Taro.setStorage |
|
||||
|
||||
自动刷新机制:`services/request.ts` 拦截 401 → 调用 `POST /auth/refresh` → 重试原请求。刷新失败则跳转登录页。
|
||||
|
||||
### 3.4 多就诊人
|
||||
|
||||
- 一个微信账号可管理多个 patient(本人 + 家人)
|
||||
- 切换就诊人时请求 header 带 `X-Patient-Id`
|
||||
- 后端校验该 patient 属于当前 user
|
||||
|
||||
---
|
||||
|
||||
## 4. 页面结构与导航
|
||||
|
||||
### 4.1 Tab Bar
|
||||
|
||||
底部导航栏 5 个入口:
|
||||
|
||||
| Tab | 图标 | 页面路径 |
|
||||
|-----|------|----------|
|
||||
| 首页 | 🏠 | /pages/index/index |
|
||||
| 健康 | 📊 | /pages/health/index |
|
||||
| 预约 | 📅 | /pages/appointment/index |
|
||||
| 资讯 | 📰 | /pages/article/index |
|
||||
| 我的 | 👤 | /pages/profile/index |
|
||||
|
||||
### 4.2 页面层级
|
||||
|
||||
```
|
||||
Tab: 首页 /pages/index
|
||||
├── /pages/notifications/index # 通知列表
|
||||
└── /pages/followup/detail/index # 随访任务详情
|
||||
|
||||
Tab: 健康 /pages/health
|
||||
├── /pages/health/input/index # 录入数据
|
||||
├── /pages/health/trend/index # 指标趋势
|
||||
└── /pages/health/history/index # 历史记录
|
||||
|
||||
Tab: 预约 /pages/appointment
|
||||
├── /pages/appointment/create/index # 新建预约
|
||||
└── /pages/appointment/detail/index # 预约详情
|
||||
|
||||
Tab: 资讯 /pages/article
|
||||
└── /pages/article/detail/index # 文章详情
|
||||
|
||||
Tab: 我的 /pages/profile
|
||||
├── /pages/profile/family/index # 就诊人管理
|
||||
├── /pages/profile/family-add/index # 添加就诊人
|
||||
├── /pages/profile/reports/index # 我的报告
|
||||
├── /pages/profile/followups/index # 我的随访
|
||||
├── /pages/profile/medication/index # 用药提醒
|
||||
└── /pages/profile/settings/index # 设置
|
||||
|
||||
独立页面(不在 Tab 内):
|
||||
├── /pages/login/index # 登录
|
||||
└── /pages/login/profile/index # 档案补全
|
||||
```
|
||||
|
||||
### 4.3 首页布局
|
||||
|
||||
```
|
||||
┌─────────────────────────────┐
|
||||
│ 问候栏(渐变青色背景) │ 用户名 + 日期 + 通知铃铛
|
||||
├─────────────────────────────┤
|
||||
│ 今日健康卡片(上浮 -20px) │ 血压/心率/血糖/体重 2×2 网格
|
||||
├─────────────────────────────┤
|
||||
│ 快捷服务(4 宫格) │ 录数据/预约/报告/随访
|
||||
├─────────────────────────────┤
|
||||
│ 即将到来 │ 最近 1 条预约卡片
|
||||
├─────────────────────────────┤
|
||||
│ 待办随访 │ 最多 2 条待办 + 查看全部
|
||||
├─────────────────────────────┤
|
||||
│ [ 首页 ] [ 健康 ] [ 预约 ] [ 资讯 ] [ 我的 ] │
|
||||
└─────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 核心功能数据流
|
||||
|
||||
### 5.1 健康数据录入
|
||||
|
||||
```
|
||||
选择指标类型 → 输入数值 + 测量时间 → 添加备注(可选)→ POST /vital-signs
|
||||
↓
|
||||
成功 → 更新首页卡片 + 趋势缓存
|
||||
失败 → Toast + 本地暂存
|
||||
```
|
||||
|
||||
**MVP 支持的指标类型:**
|
||||
|
||||
| 指标 | 单位 | 输入控件 |
|
||||
|------|------|---------|
|
||||
| 收缩压 / 舒张压 | mmHg | 两个数字输入框 |
|
||||
| 心率 | bpm | 数字输入框 |
|
||||
| 空腹血糖 | mmol/L | 数字输入框 |
|
||||
| 餐后血糖 | mmol/L | 数字输入框 |
|
||||
| 体重 | kg | 数字输入框(1 位小数) |
|
||||
| 体温 | ℃ | 数字输入框(1 位小数) |
|
||||
|
||||
每次可同时填多项或只填一项。录入时间默认当前,可手动调整为当天任意时间。
|
||||
|
||||
### 5.2 预约挂号
|
||||
|
||||
```
|
||||
选择科室 → 选择医生 → 选择日期(排班日历)→ 选择时段 → 确认预约
|
||||
↓
|
||||
POST /appointments
|
||||
↓
|
||||
成功 → 订阅消息通知 + 日历同步
|
||||
满员 → 提示"该时段已满"
|
||||
```
|
||||
|
||||
**关键交互:**
|
||||
- 排班日历用周视图,有排班的日期标绿点
|
||||
- 点击日期后展示该日可用时段
|
||||
- 时段显示"剩余 X 位"
|
||||
- 预约成功后支持微信订阅消息提醒
|
||||
|
||||
### 5.3 报告查询
|
||||
|
||||
```
|
||||
报告列表(分页,时间倒序)
|
||||
↓ 点击某份报告
|
||||
报告详情 → 基本信息卡 + 指标列表 + PDF/图片附件预览
|
||||
```
|
||||
|
||||
**指标状态标记:**
|
||||
- 异常偏高:红色 + ↑ 箭头
|
||||
- 异常偏低:红色 + ↓ 箭头
|
||||
- 正常范围:灰色
|
||||
|
||||
### 5.4 随访管理
|
||||
|
||||
```
|
||||
待办列表(按截止日期排序)
|
||||
↓ 支持"待完成/已完成/已过期"筛选
|
||||
点击任务 → 动态表单(后端定义字段)→ 提交 → 标记完成
|
||||
```
|
||||
|
||||
问卷由 PC 端医护创建(follow_up_task),小程序负责展示和填写。提交后创建 follow_up_record。
|
||||
|
||||
### 5.5 家庭健康管理
|
||||
|
||||
```
|
||||
就诊人列表(本人 + 已添加家属)
|
||||
↓ 点击头像或下拉切换
|
||||
切换就诊人 → 全局 X-Patient-Id 更新 → 所有页面数据刷新
|
||||
↓ 添加家属
|
||||
填写信息 → 姓名 + 关系 + 身份证号(可选)→ POST /patients
|
||||
```
|
||||
|
||||
切换就诊人通过全局 store 更新,所有 service 请求自动携带新 `X-Patient-Id`。
|
||||
|
||||
### 5.6 健康资讯 + 用药提醒
|
||||
|
||||
**资讯:**
|
||||
- `GET /articles` → 分页列表(缩略图 + 标题 + 摘要 + 时间)
|
||||
- 文章详情使用 Taro `RichText` 组件渲染富文本
|
||||
|
||||
**用药提醒(MVP):**
|
||||
- 小程序本地 storage 存储提醒规则(药品名 + 频率 + 时间)
|
||||
- 每日触发检查
|
||||
- 通过微信订阅消息推送提醒
|
||||
- 不依赖后端新表
|
||||
|
||||
---
|
||||
|
||||
## 6. API 集成与状态管理
|
||||
|
||||
### 6.1 请求层封装
|
||||
|
||||
`services/request.ts` 职责:
|
||||
|
||||
| 拦截点 | 行为 |
|
||||
|--------|------|
|
||||
| 请求拦截 | 自动注入 `Authorization: Bearer {token}` |
|
||||
| 请求拦截 | 自动注入 `X-Patient-Id`(当前选中就诊人) |
|
||||
| 请求拦截 | 自动注入 `X-Tenant-Id`(从登录信息获取) |
|
||||
| 响应拦截 | 401 → 静默刷新 token → 重试原请求 |
|
||||
| 响应拦截 | 刷新失败 → 跳转登录页 |
|
||||
| 错误处理 | 网络错误 / 业务错误 / 超时统一处理 |
|
||||
|
||||
多租户处理:患者只属于一个租户。登录时后端返回 `tenant_id`,前端每次请求带上。不走 `tenant_id` 中间件自动注入。
|
||||
|
||||
### 6.2 Zustand Stores
|
||||
|
||||
**auth store:**
|
||||
|
||||
```typescript
|
||||
interface AuthState {
|
||||
token: string | null
|
||||
refreshToken: string | null
|
||||
user: { id: string; name: string; phone: string; avatar: string } | null
|
||||
currentPatient: Patient | null
|
||||
patients: Patient[]
|
||||
setCurrentPatient: (id: string) => void
|
||||
login: (code: string) => Promise<void>
|
||||
bindPhone: (data: BindPhoneData) => Promise<void>
|
||||
logout: () => void
|
||||
}
|
||||
```
|
||||
|
||||
**health store:**
|
||||
|
||||
```typescript
|
||||
interface HealthState {
|
||||
todaySummary: VitalSigns | null
|
||||
trendData: Record<string, TrendPoint[]>
|
||||
refreshToday: () => Promise<void>
|
||||
getTrend: (type: string, range: '7d' | '30d' | '90d') => Promise<TrendPoint[]>
|
||||
}
|
||||
```
|
||||
|
||||
### 6.3 API 端点对应表
|
||||
|
||||
| 小程序 service | 后端端点 | 方法 |
|
||||
|----------------|----------|------|
|
||||
| `auth.login(code)` | `/api/v1/auth/wechat/login` | POST |
|
||||
| `auth.bindPhone(data)` | `/api/v1/auth/wechat/bind-phone` | POST |
|
||||
| `auth.refresh()` | `/api/v1/auth/refresh` | POST |
|
||||
| `health.getToday()` | `/api/v1/health/vital-signs?date=today` | GET |
|
||||
| `health.input(data)` | `/api/v1/health/vital-signs` | POST |
|
||||
| `health.getTrend(type, range)` | `/api/v1/health/vital-signs/trend` | GET |
|
||||
| `appointment.list()` | `/api/v1/health/appointments` | GET |
|
||||
| `appointment.create(data)` | `/api/v1/health/appointments` | POST |
|
||||
| `appointment.cancel(id)` | `/api/v1/health/appointments/:id/cancel` | PUT |
|
||||
| `schedule.getByDoctor(id)` | `/api/v1/health/doctor-schedules` | GET |
|
||||
| `report.list()` | `/api/v1/health/lab-reports` | GET |
|
||||
| `report.detail(id)` | `/api/v1/health/lab-reports/:id` | GET |
|
||||
| `followup.list()` | `/api/v1/health/follow-up-tasks` | GET |
|
||||
| `followup.submit(id, data)` | `/api/v1/health/follow-up-records` | POST |
|
||||
| `patient.list()` | `/api/v1/health/patients` | GET |
|
||||
| `patient.create(data)` | `/api/v1/health/patients` | POST |
|
||||
| `patient.update(id, data)` | `/api/v1/health/patients/:id` | PUT |
|
||||
|
||||
**后端需新增的端点(尚未实现):**
|
||||
|
||||
| 端点 | 说明 |
|
||||
|------|------|
|
||||
| `POST /auth/wechat/login` | 微信登录 |
|
||||
| `POST /auth/wechat/bind-phone` | 手机号绑定 |
|
||||
| `GET /vital-signs/trend` | 趋势聚合查询 |
|
||||
| `GET /doctor-schedules` | 按科室/医生查询排班 |
|
||||
| `GET /articles` | 健康资讯列表 |
|
||||
| `GET /articles/:id` | 资讯详情 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 视觉设计
|
||||
|
||||
### 7.1 主题色
|
||||
|
||||
沿用现有 HTML 原型的医疗清新风格:
|
||||
|
||||
| 用途 | 色值 | 说明 |
|
||||
|------|------|------|
|
||||
| 主色 | `#0891B2` | 青色,按钮、导航、强调 |
|
||||
| 主色浅 | `#E0F7FA` | 背景、卡片高亮 |
|
||||
| 主色深 | `#065A73` | 渐变、按压态 |
|
||||
| 辅助色 | `#059669` | 绿色,成功、正常指标 |
|
||||
| 危险色 | `#DC2626` | 红色,异常指标、删除 |
|
||||
| 警告色 | `#D97706` | 琥珀,待办、提醒 |
|
||||
| 背景色 | `#F0FDFA` | 页面底色 |
|
||||
| 卡片色 | `#FFFFFF` | 卡片背景 |
|
||||
| 主文字 | `#134E4A` | 标题、正文 |
|
||||
| 副文字 | `#6B7280` | 说明、标签 |
|
||||
| 轻文字 | `#94A3B8` | 时间戳、占位符 |
|
||||
|
||||
### 7.2 圆角规范
|
||||
|
||||
| 元素 | 圆角 |
|
||||
|------|------|
|
||||
| 卡片 | 12px |
|
||||
| 按钮 | 8px |
|
||||
| 输入框 | 8px |
|
||||
| 头像 | 50% |
|
||||
| 快捷图标 | 14px |
|
||||
|
||||
### 7.3 阴影规范
|
||||
|
||||
| 层级 | 值 |
|
||||
|------|---|
|
||||
| 轻阴影 | `0 1px 3px rgba(0,0,0,.04)` |
|
||||
| 标准阴影 | `0 2px 8px rgba(0,0,0,.06)` |
|
||||
| 中阴影 | `0 4px 16px rgba(0,0,0,.08)` |
|
||||
| 重阴影 | `0 8px 32px rgba(0,0,0,.12)` |
|
||||
|
||||
---
|
||||
|
||||
## 8. 开发工作流
|
||||
|
||||
### 8.1 开发环境
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
cd apps/miniprogram && pnpm install
|
||||
|
||||
# 开发模式(需配合微信开发者工具)
|
||||
pnpm dev:weapp # Taro 编译 + watch → dist/
|
||||
# 用微信开发者工具打开 dist/ 目录预览
|
||||
|
||||
# 生产构建
|
||||
pnpm build:weapp # 压缩 + tree-shaking
|
||||
|
||||
# 后端联调
|
||||
# 需同时运行 erp-server (port 3000)
|
||||
# 小程序开发设置中关闭域名校验(开发阶段)
|
||||
```
|
||||
|
||||
### 8.2 与 Web 端的代码复用
|
||||
|
||||
| 复用内容 | 方式 | 说明 |
|
||||
|---------|------|------|
|
||||
| TypeScript 类型 | 按需引用 Web 端 DTO 类型 | API 请求/响应结构一致 |
|
||||
| 主题变量值 | Web CSS 变量 → SCSS 变量 | 青色主调色值保持一致 |
|
||||
| Zustand 模式 | 相同 store 设计模式 | 各自独立实现 |
|
||||
| API 接口定义 | service 层函数签名对齐 | Web 用 fetch,小程序用 Taro.request |
|
||||
|
||||
### 8.3 后端需同步开发的内容
|
||||
|
||||
| 优先级 | 内容 | 涉及 crate |
|
||||
|--------|------|-----------|
|
||||
| P0 | `wechat_users` 表 + 微信登录/绑定 API | erp-auth |
|
||||
| P0 | `vital_signs` 趋势查询 API | erp-health |
|
||||
| P0 | `doctor_schedules` 按科室/医生查询 API | erp-health |
|
||||
| P1 | `lab_reports` 指标异常标注字段 | erp-health |
|
||||
| P1 | `follow_up_tasks` 动态问卷字段扩展 | erp-health |
|
||||
| P2 | `articles` 表 + CRUD | erp-health |
|
||||
| P2 | 微信订阅消息模板注册 | erp-server |
|
||||
|
||||
---
|
||||
|
||||
## 9. 分期交付计划
|
||||
|
||||
| 阶段 | 内容 | 目标 |
|
||||
|------|------|------|
|
||||
| Phase 1 | 项目骨架 + 登录流程 + 首页(静态数据) | 基础搭建 |
|
||||
| Phase 2 | 健康数据录入 + 趋势图 | 核心功能 |
|
||||
| Phase 3 | 预约挂号 + 排班日历 | 核心功能 |
|
||||
| Phase 4 | 报告查询 + 家庭管理 | 扩展功能 |
|
||||
| Phase 5 | 随访 + 资讯 + 用药提醒 | 扩展功能 |
|
||||
| Phase 6 | 打磨 + 真机测试 + 提审 | 上线准备 |
|
||||
|
||||
每个 Phase 内部遵循:先对接后端 API → 再实现 UI → 真机验证 → 提交。
|
||||
|
||||
---
|
||||
|
||||
## 10. 约束与风险
|
||||
|
||||
| 约束/风险 | 应对策略 |
|
||||
|-----------|---------|
|
||||
| 小程序包体积限制(2MB 主包) | 按功能分包加载,图表库按需引入 |
|
||||
| 微信审核周期(3-7 天) | Phase 6 预留充足审核时间 |
|
||||
| 后端 API 部分未实现 | 小程序开发与后端同步推进,优先实现 P0 端点 |
|
||||
| 微信订阅消息需用户主动触发 | 在预约成功、随访提交等场景引导用户订阅 |
|
||||
| 蓝牙设备适配复杂 | MVP 预留接口不实现,后续按设备型号逐一对接 |
|
||||
| 多就诊人数据隔离 | 后端严格校验 user-patient 归属关系 |
|
||||
@@ -1,671 +0,0 @@
|
||||
# 健康管理模块全面迭代设计
|
||||
|
||||
> **文档版本**: 1.0
|
||||
> **日期**: 2026-04-24
|
||||
> **状态**: 待评审
|
||||
> **基于**: 5 位专家(后端架构/前端架构/医疗业务/安全质量/产品策略)深度审查
|
||||
|
||||
---
|
||||
|
||||
## 0. 审查发现总览
|
||||
|
||||
### 0.1 V1 发布阻塞项
|
||||
|
||||
| # | 阻塞项 | 来源 | 影响 |
|
||||
|---|--------|------|------|
|
||||
| B1 | Web 健康模块 10 页面未实现 | 前端架构/产品策略 | 无法演示和交付 |
|
||||
| B2 | 医疗数据安全不合规 | 安全质量 | 零 sanitize / 零审计 / 身证明文 / 零测试 |
|
||||
| B3 | 数据一致性缺陷 | 医疗业务/后端架构 | 排班可超额 / 名额释放可能失败 / 随访逾期未实现 |
|
||||
| B4 | 事件处理器空壳 | 后端架构 | 随访状态/咨询消息不联动 |
|
||||
|
||||
### 0.2 当前完成度
|
||||
|
||||
| 层级 | 模块 | 完成度 |
|
||||
|------|------|--------|
|
||||
| 后端 | erp-health(16 实体/8 服务/7 handler/40+ API) | 95% |
|
||||
| 后端 | 事件处理器业务逻辑 | 0%(框架已搭建,需填充 db 操作) |
|
||||
| 后端 | sanitize / 审计 / 加密 | 0% |
|
||||
| 后端 | 测试覆盖 | 0% |
|
||||
| Web 前端 | 健康模块页面 | 0% |
|
||||
| Web 前端 | 健康模块 API 服务层 | 0% |
|
||||
| 小程序 | 初版 21 页面 | 85% |
|
||||
|
||||
---
|
||||
|
||||
## 1. 安全省基(阶段 1,1.5-2 周)
|
||||
|
||||
### 1.1 sanitize 全覆盖
|
||||
|
||||
**问题**: erp-health 模块没有任何对 `strip_html_tags` 的调用,攻击者可在患者姓名、病史等字段注入 XSS payload。
|
||||
|
||||
**参考实现**: `crates/erp-auth/src/dto.rs` 第 96-118 行,`CreateUserReq` 和 `UpdateUserReq` 已实现 `sanitize()` 方法。
|
||||
|
||||
**修复方案**: 为每个 DTO 的字符串输入字段添加 sanitize。
|
||||
|
||||
**覆盖字段清单**:
|
||||
|
||||
| DTO 文件 | 字段 |
|
||||
|----------|------|
|
||||
| `patient_dto.rs` CreatePatientReq / UpdatePatientReq | name, notes, allergy_history, medical_history_summary, emergency_contact_name, source |
|
||||
| `patient_dto.rs` FamilyMemberReq(create + update 共用) | name, notes |
|
||||
| `patient_handler.rs` AssignDoctorReq(位于 handler 非 dto) | — (无字符串字段) |
|
||||
| `health_data_dto.rs` CreateVitalSignsReq | notes |
|
||||
| `health_data_dto.rs` CreateLabReportReq | doctor_interpretation |
|
||||
| `health_data_dto.rs` CreateHealthRecordReq | source, overall_assessment, notes |
|
||||
| `appointment_dto.rs` CreateAppointmentReq | notes, cancel_reason |
|
||||
| `follow_up_dto.rs` CreateFollowUpTaskReq / UpdateFollowUpTaskReq | content_template |
|
||||
| `follow_up_dto.rs` CreateFollowUpRecordReq | patient_condition, medical_advice |
|
||||
| `consultation_dto.rs` CreateMessageReq | content |
|
||||
| `consultation_dto.rs` CreateSessionReq | — (无字符串字段) |
|
||||
| `doctor_dto.rs` CreateDoctorReq / UpdateDoctorReq | department, title, specialty, bio |
|
||||
|
||||
**实现模式**:
|
||||
|
||||
```rust
|
||||
// 封装 sanitize 辅助函数(与 erp-auth 的 sanitize_option 模式一致)
|
||||
fn sanitize_option_string(opt: Option<String>) -> Option<String> {
|
||||
opt.map(|s| strip_html_tags(&s))
|
||||
}
|
||||
|
||||
// 在每个 DTO 的 impl 中添加 sanitize 方法
|
||||
impl CreatePatientReq {
|
||||
pub fn sanitize(&mut self) {
|
||||
self.name = strip_html_tags(&self.name);
|
||||
self.notes = sanitize_option_string(self.notes.take());
|
||||
self.allergy_history = sanitize_option_string(self.allergy_history.take());
|
||||
self.medical_history_summary = sanitize_option_string(self.medical_history_summary.take());
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
// 在 handler 调用 service 前执行
|
||||
async fn create_patient(/* ... */) -> AppResult<Json<ApiResponse<PatientResp>>> {
|
||||
let mut req: CreatePatientReq = Json(req).0;
|
||||
req.sanitize();
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**前端安全**: ChatBubble 组件必须使用 React 默认 JSX 转义渲染文本内容(不使用 `dangerouslySetInnerHTML`),图片消息 URL 需做白名单校验。
|
||||
|
||||
### 1.2 审计日志注入
|
||||
|
||||
**问题**: erp-health 整个模块没有任何对 `audit_service::record` 的调用。
|
||||
|
||||
**参考实现**: `crates/erp-auth/src/service/auth_service.rs` 第 168-177 行。
|
||||
|
||||
**修复方案**: 在所有写入操作的 service 层添加审计记录。
|
||||
|
||||
**覆盖操作清单**:
|
||||
|
||||
| Service | 操作 | 审计 action |
|
||||
|---------|------|------------|
|
||||
| patient_service | create_patient | `patient.created` |
|
||||
| patient_service | update_patient | `patient.updated` |
|
||||
| patient_service | delete_patient | `patient.deleted` |
|
||||
| patient_service | manage_patient_tags | `patient.tags_updated` |
|
||||
| health_data_service | create_vital_signs | `vital_signs.created` |
|
||||
| health_data_service | create_lab_report | `lab_report.created` |
|
||||
| health_data_service | create_health_record | `health_record.created` |
|
||||
| appointment_service | create_appointment | `appointment.created` |
|
||||
| appointment_service | update_appointment_status | `appointment.status_changed` |
|
||||
| follow_up_service | create_task | `follow_up_task.created` |
|
||||
| follow_up_service | create_record | `follow_up_record.created` |
|
||||
| consultation_service | create_session | `consultation.opened` |
|
||||
| consultation_service | close_session | `consultation.closed` |
|
||||
| consultation_service | create_message | `consultation.message_sent` |
|
||||
| doctor_service | create/update/delete_doctor | `doctor.*` |
|
||||
|
||||
**审计日志内容**: tenant_id、user_id、action、resource_type、resource_id、变更前后值摘要。
|
||||
|
||||
**注意**: 当前 `audit_service::record` 是 fire-and-forget,审计日志丢失对医疗合规不可接受。修复方案:
|
||||
1. 新增 `record_in_txn(log: AuditLog, txn: &DatabaseTransaction)` 方法,在事务内 await 写入
|
||||
2. 保留原 `record` 方法用于不要求事务保证的场景
|
||||
3. erp-health 的关键写入操作使用 `record_in_txn`,失败时回滚整个事务
|
||||
4. 需要改为事务包裹的 service 方法:create_patient、update_patient、delete_patient、create_appointment、update_appointment_status、create_record(随访)、create_message(咨询)
|
||||
|
||||
### 1.3 身份证号加密存储
|
||||
|
||||
**问题**: `patient.id_number` 明文存储在数据库中,违反《个人信息保护法》。
|
||||
|
||||
**方案**: AES-256-GCM 应用层加密。
|
||||
|
||||
**新增文件**: `crates/erp-health/src/crypto.rs`
|
||||
|
||||
```rust
|
||||
pub struct HealthCrypto { key: [u8; 32] }
|
||||
|
||||
impl HealthCrypto {
|
||||
pub fn from_env() -> Self { /* 从 ERP__HEALTH__ENCRYPTION_KEY 读取 */ }
|
||||
pub fn encrypt(&self, plaintext: &str) -> AppResult<String> { /* AES-256-GCM + Base64 */ }
|
||||
pub fn decrypt(&self, ciphertext: &str) -> AppResult<String> { /* 解密 */ }
|
||||
}
|
||||
```
|
||||
|
||||
**集成点**:
|
||||
- `patient_service::create_patient` — 加密 id_number 后存储
|
||||
- `patient_service::update_patient` — 同上
|
||||
- `patient_service::get_patient` — 解密后返回
|
||||
- `patient_service::list_patients` — 列表不返回 id_number(脱敏)
|
||||
|
||||
**密钥管理**: 环境变量 `ERP__HEALTH__ENCRYPTION_KEY`(32 字节 hex),必须在 `default.toml` 中标记为 `__MUST_SET_VIA_ENV__`。
|
||||
|
||||
**搜索兼容**: `patient.id_number` 的模糊搜索(`contains`)改为精确匹配(`eq`),在加密后使用 HMAC 索引做等值查询。
|
||||
|
||||
**HMAC 索引详情**:
|
||||
- 新增数据库列 `id_number_hash VARCHAR(64)`,存储 HMAC-SHA256 哈希
|
||||
- HMAC 密钥独立于 AES 密钥,从环境变量 `ERP__HEALTH__HMAC_KEY` 读取
|
||||
- 创建/更新患者时同时写入 hash 列,等值查询使用 `WHERE id_number_hash = hmac(输入值)`
|
||||
- 迁移 SQL:新增列 → 批量加密现有明文 → 删除原明文列(可选)
|
||||
|
||||
**数据迁移方案**:
|
||||
1. 停机窗口(预估 1-2 小时,视数据量)
|
||||
2. 迁移脚本:`SELECT id, id_number FROM patients WHERE id_number IS NOT NULL AND deleted_at IS NULL` → 批量加密 → `UPDATE patients SET id_number = $encrypted WHERE id = $id`
|
||||
3. 同步写入 `id_number_hash` 列
|
||||
4. 验证脚本:抽样解密比对原值
|
||||
5. 回滚方案:保留明文备份表 `patients_id_number_backup`,72 小时后确认无误再删除
|
||||
|
||||
**问题**: 列表接口直接返回完整身份证号、病史等敏感字段。
|
||||
|
||||
**修复方案**: 拆分响应 DTO。
|
||||
|
||||
```rust
|
||||
// 列表用 — 不含敏感字段
|
||||
pub struct PatientListResp {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
pub gender: Option<String>,
|
||||
pub birth_date: Option<NaiveDate>,
|
||||
pub status: String,
|
||||
pub tags: Vec<TagResp>,
|
||||
// 无 id_number, allergy_history, medical_history_summary, emergency_contact_phone 等
|
||||
}
|
||||
|
||||
// 详情用 — 敏感字段掩码
|
||||
pub struct PatientDetailResp {
|
||||
// ... 全部字段
|
||||
pub id_number: Option<String>, // "320***********1234"
|
||||
pub emergency_contact_phone: Option<String>, // "138****1234"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 后端补完(阶段 2,1.5 周)
|
||||
|
||||
### 2.1 事件处理器实现
|
||||
|
||||
**问题**: `event.rs` 中两个事件处理器只有 `tracing::info`,无实际业务逻辑。且 handler 中没有 `DatabaseConnection`,无法执行数据库操作。
|
||||
|
||||
**方案**: 在 `HealthModule::on_startup` 中创建 `HealthState` 并注册需要数据库访问的事件处理器。将现有 `register_event_handlers` 中的空壳代码迁移到 `on_startup`,`register_event_handlers` 改为空实现。
|
||||
|
||||
**修改 `crates/erp-health/src/module.rs`**:
|
||||
|
||||
```rust
|
||||
// register_event_handlers 改为空实现
|
||||
fn register_event_handlers(&self, _bus: &EventBus) {
|
||||
// 事件处理器迁移到 on_startup,此处不再注册
|
||||
}
|
||||
|
||||
// on_startup 中注册带 db 的事件处理器
|
||||
async fn on_startup(&self, ctx: &ModuleContext) -> AppResult<()> {
|
||||
let state = HealthState {
|
||||
db: ctx.db.clone(),
|
||||
event_bus: ctx.event_bus.clone(),
|
||||
};
|
||||
crate::event::register_handlers_with_state(state);
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
**修改 `crates/erp-health/src/event.rs`**:
|
||||
|
||||
新增 `register_handlers_with_state(state: HealthState)` 函数替代原有 `register_handlers`。
|
||||
|
||||
**事件处理器业务逻辑**:
|
||||
|
||||
`workflow.task.completed`:
|
||||
1. 从 payload 中提取 `task_id`
|
||||
2. 查询 `follow_up_task WHERE related_appointment_id` 或通过 payload 映射
|
||||
3. 更新随访任务状态为 `completed`
|
||||
|
||||
`message.sent`:
|
||||
1. 从 payload 中提取 `session_id`(或通过 sender/recipient 关联)
|
||||
2. 更新 `consultation_session SET last_message_at = NOW(), unread_count = unread_count + 1`
|
||||
3. 使用 `check_version` 乐观锁
|
||||
|
||||
### 2.2 数据一致性修复
|
||||
|
||||
#### 2.2.1 排班名额保护
|
||||
|
||||
**问题**: `update_schedule` 可以将 `max_appointments` 改为小于 `current_appointments` 的值。
|
||||
|
||||
**修复**: 在 `appointment_service.rs` 的 `update_schedule` 方法中增加校验:
|
||||
|
||||
```rust
|
||||
if req.max_appointments < model.current_appointments {
|
||||
return Err(HealthError::Validation(
|
||||
"max_appointments 不能小于当前已预约数".into()
|
||||
).into());
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.2.2 取消预约名额释放
|
||||
|
||||
**问题**: `update_appointment_status` 中取消时名额释放失败只 log error 不回滚。
|
||||
|
||||
**修复**: 将名额释放作为事务的一部分,失败时回滚整个操作(包括状态更新)。
|
||||
|
||||
#### 2.2.3 咨询消息原子性
|
||||
|
||||
**问题**: `create_message` 中消息已插入,但后续 CAS 更新 session 失败时返回错误 — 消息已持久化但 session 元数据未更新。
|
||||
|
||||
**修复**: 将消息 INSERT + session CAS 更新放在同一个事务中。
|
||||
|
||||
### 2.3 随访逾期定时任务
|
||||
|
||||
**问题**: 设计规格定义了 `overdue` 状态和定时任务自动标记,但代码中:
|
||||
- `validation.rs` 不允许转换到 `overdue`
|
||||
- 没有后台定时任务
|
||||
|
||||
**修复**:
|
||||
|
||||
1. 在 `validation.rs` 中添加 `overdue` 转换规则:`pending -> overdue`(仅限系统自动触发)
|
||||
2. 在 `erp-server/src/main.rs` 后台任务区增加逾期检查器,使用与现有 `start_timeout_checker` 一致的 `tokio::spawn` + `loop` + `tokio::time::interval` 模式(每 6 小时执行一次,非 cron 表达式):
|
||||
|
||||
```rust
|
||||
// erp-server/src/main.rs 后台任务区
|
||||
tokio::spawn(async move {
|
||||
let mut interval = tokio::time::interval(Duration::from_secs(6 * 3600));
|
||||
loop {
|
||||
interval.tick().await;
|
||||
// 调用 health module 的 check_overdue_tasks
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
3. 在 `erp-health` module 中添加一个公开方法 `check_overdue_tasks` 供定时任务调用。
|
||||
|
||||
### 2.4 article 管理 CRUD
|
||||
|
||||
**问题**: 权限声明中有 `health.articles.manage`,但 service/handler 只有 list 和 get。
|
||||
|
||||
**修复**: 在 `article_service.rs` 和 `article_handler.rs` 中补充 create/update/delete 方法。在 `module.rs` 中添加路由。**工时估算**: 0.5 天。
|
||||
|
||||
---
|
||||
|
||||
## 3. Web 前端 10 页面(阶段 3,3.5-4 周)
|
||||
|
||||
### 3.1 页面文件组织
|
||||
|
||||
```
|
||||
apps/web/src/
|
||||
├── api/health/
|
||||
│ ├── patients.ts # 12 端点
|
||||
│ ├── healthData.ts # 13 端点
|
||||
│ ├── appointments.ts # 6 端点
|
||||
│ ├── followUp.ts # 6 端点
|
||||
│ ├── consultations.ts # 6 端点
|
||||
│ └── doctors.ts # 4 端点
|
||||
├── pages/health/
|
||||
│ ├── PatientList.tsx # 患者列表
|
||||
│ ├── PatientDetail.tsx # 患者详情(5 Tab)
|
||||
│ ├── PatientTagManage.tsx # 标签管理
|
||||
│ ├── DoctorList.tsx # 医护列表
|
||||
│ ├── AppointmentList.tsx # 预约管理
|
||||
│ ├── DoctorSchedule.tsx # 排班管理
|
||||
│ ├── FollowUpTaskList.tsx # 随访任务
|
||||
│ ├── FollowUpRecordList.tsx # 随访台账
|
||||
│ ├── ConsultationList.tsx # 会话管理
|
||||
│ ├── ConsultationDetail.tsx # 对话详情
|
||||
│ └── components/
|
||||
│ ├── StatusTag.tsx # 通用状态标签
|
||||
│ ├── PatientSelect.tsx # 患者搜索选择器
|
||||
│ ├── DoctorSelect.tsx # 医护选择器
|
||||
│ ├── VitalSignsChart.tsx # ECharts 趋势图
|
||||
│ ├── CalendarView.tsx # 日历视图
|
||||
│ ├── ChatBubble.tsx # 聊天气泡
|
||||
│ ├── ImagePreview.tsx # 图片预览
|
||||
│ └── ExportButton.tsx # 导出按钮
|
||||
```
|
||||
|
||||
### 3.2 API 服务层设计
|
||||
|
||||
每个 service 文件遵循现有 `api/users.ts` 的解构模式:
|
||||
|
||||
```typescript
|
||||
// api/health/patients.ts
|
||||
import client from '../client';
|
||||
|
||||
export interface Patient {
|
||||
id: string;
|
||||
name: string;
|
||||
gender?: string;
|
||||
birth_date?: string;
|
||||
status: string;
|
||||
tags: Tag[];
|
||||
// ...
|
||||
}
|
||||
|
||||
export interface CreatePatientReq {
|
||||
name: string;
|
||||
gender?: string;
|
||||
// ...
|
||||
}
|
||||
|
||||
export const patientApi = {
|
||||
list: async (params: ListParams) => {
|
||||
const { data } = await client.get<{ success: boolean; data: PaginatedResponse<Patient> }>(
|
||||
'/health/patients', { params }
|
||||
);
|
||||
return data.data;
|
||||
},
|
||||
get: async (id: string) => {
|
||||
const { data } = await client.get<{ success: boolean; data: Patient }>(
|
||||
`/health/patients/${id}`
|
||||
);
|
||||
return data.data;
|
||||
},
|
||||
create: async (req: CreatePatientReq) => {
|
||||
const { data } = await client.post<{ success: boolean; data: Patient }>(
|
||||
'/health/patients', req
|
||||
);
|
||||
return data.data;
|
||||
},
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
### 3.3 路由注册
|
||||
|
||||
在 `App.tsx` 中新增:
|
||||
|
||||
```typescript
|
||||
// lazy imports
|
||||
const PatientList = lazy(() => import('./pages/health/PatientList'));
|
||||
const PatientDetail = lazy(() => import('./pages/health/PatientDetail'));
|
||||
// ... 共 10 个路由组件
|
||||
|
||||
// Routes 内
|
||||
<Route path="/health/patients" element={<PatientList />} />
|
||||
<Route path="/health/patients/:id" element={<PatientDetail />} />
|
||||
<Route path="/health/tags" element={<PatientTagManage />} />
|
||||
<Route path="/health/doctors" element={<DoctorList />} />
|
||||
<Route path="/health/appointments" element={<AppointmentList />} />
|
||||
<Route path="/health/schedules" element={<DoctorSchedule />} />
|
||||
<Route path="/health/follow-up-tasks" element={<FollowUpTaskList />} />
|
||||
<Route path="/health/follow-up-records" element={<FollowUpRecordList />} />
|
||||
<Route path="/health/consultations" element={<ConsultationList />} />
|
||||
<Route path="/health/consultations/:id" element={<ConsultationDetail />} />
|
||||
```
|
||||
|
||||
### 3.4 侧边栏菜单
|
||||
|
||||
在 `MainLayout.tsx` 中新增 `healthMenuItems` 数组(参照现有 `bizMenuItems` 模式),使用 `@ant-design/icons` 图标(如 `MedicineBoxOutlined`、`HeartOutlined`、`CalendarOutlined`、`PhoneOutlined`、`CommentOutlined`、`TagsOutlined`):
|
||||
|
||||
```
|
||||
侧边栏布局:
|
||||
├── 首页 (HomeOutlined)
|
||||
├── 用户管理 (UserOutlined)
|
||||
├── 权限管理 (SafetyOutlined)
|
||||
├── 工作流 (ApartmentOutlined)
|
||||
├── 消息中心 (BellOutlined)
|
||||
├── ─────────
|
||||
├── 健康管理 (MedicineBoxOutlined) ← 新增组
|
||||
│ ├── 患者管理 (TeamOutlined)
|
||||
│ ├── 医护管理 (HeartOutlined)
|
||||
│ ├── 预约排班 (CalendarOutlined)
|
||||
│ ├── 随访管理 (PhoneOutlined)
|
||||
│ ├── 咨询管理 (CommentOutlined)
|
||||
│ └── 标签管理 (TagsOutlined)
|
||||
├── ─────────
|
||||
├── 插件管理 (AppstoreOutlined)
|
||||
├── 系统设置 (SettingOutlined)
|
||||
```
|
||||
|
||||
### 3.5 前端权限集成
|
||||
|
||||
后端已有完整权限体系(14 个权限码),前端 V1 阶段采用以下策略:
|
||||
|
||||
1. **路由级权限**: 所有健康模块路由在 `PrivateRoute` 内(已实现),后端 `require_permission` 拦截无权限请求返回 403
|
||||
2. **按钮级权限(V1 简化)**: 不做前端按钮级权限控制,依赖后端 403 响应。后续可扩展 `usePermission` hook
|
||||
3. **菜单可见性**: 健康模块菜单组始终显示,但无权限用户点击任何页面会收到 403 提示
|
||||
|
||||
### 3.5 13 页面逐一设计
|
||||
|
||||
#### PatientList.tsx(中复杂度,1.5 天)
|
||||
|
||||
- Ant Design `Table` 组件(与 Users.tsx 模式一致,不使用 ProTable)
|
||||
- 搜索:姓名模糊 + 状态筛选 + 标签多选筛选
|
||||
- 每行显示患者标签为 `Tag` 组件列表
|
||||
- 行点击跳转 `/health/patients/:id`
|
||||
- 批量操作:批量打标
|
||||
- 导出功能
|
||||
|
||||
#### PatientDetail.tsx(高复杂度,3 天)
|
||||
|
||||
- 顶部:患者摘要卡片(姓名/性别/年龄/状态/标签)
|
||||
- Ant Design `Tabs` 5 个 Tab:
|
||||
1. **基本信息** — `Descriptions` 展示 + 编辑 Modal
|
||||
2. **健康趋势** — `VitalSignsChart` 组件 + 时间范围选择器
|
||||
3. **化验报告** — 报告卡片列表 + `ImagePreview` 指标详情
|
||||
4. **就诊记录** — 嵌套列表(体检/门诊/住院)
|
||||
5. **随访记录** — 嵌套列表 + 关联的随访记录
|
||||
|
||||
#### PatientTagManage.tsx(低复杂度,0.5 天)
|
||||
|
||||
- 标准 CRUD 表格
|
||||
- 颜色选择器(Ant Design `ColorPicker`)
|
||||
- 批量打标功能
|
||||
|
||||
#### DoctorList.tsx(低复杂度,0.5 天)
|
||||
|
||||
- 标准 CRUD 表格
|
||||
- 科室筛选 + 在线状态 Badge(online=绿/busy=黄/offline=灰)
|
||||
- 详情 Drawer
|
||||
|
||||
#### AppointmentList.tsx(中复杂度,2 天)
|
||||
|
||||
- `Segmented` 切换列表/日历视图
|
||||
- 列表模式:表格 + 状态筛选 + 日期筛选
|
||||
- 日历模式:`Calendar` + `cellRender` 显示当日预约数
|
||||
- 状态流转 Dropdown(pending → confirmed → completed/no_show/cancelled)
|
||||
- 创建预约 Modal(选择患者 + 医生 + 日期时段 + 检查排班余量)
|
||||
|
||||
#### DoctorSchedule.tsx(高复杂度,2.5 天)
|
||||
|
||||
- 选择医生后展示其排班
|
||||
- 周视图(自定义 7 列网格,每列显示一天的排班时段)
|
||||
- 月视图(Ant Design Calendar)
|
||||
- 批量创建排班(选择日期范围 + 时段模板)
|
||||
- 显示已预约/最大预约数
|
||||
|
||||
#### FollowUpTaskList.tsx(中复杂度,1.5 天)
|
||||
|
||||
- 表格 + 状态筛选(pending/in_progress/completed/overdue/cancelled)
|
||||
- 分配给医护(`DoctorSelect`)
|
||||
- 创建任务 Modal
|
||||
- 快捷"填写随访记录"按钮打开子 Modal
|
||||
|
||||
#### FollowUpRecordList.tsx(低复杂度,0.5 天)
|
||||
|
||||
- 纯只读台账
|
||||
- 筛选:日期范围、患者、任务、结果
|
||||
- 导出功能(`ExportButton`)
|
||||
|
||||
#### ConsultationList.tsx(中复杂度,1 天)
|
||||
|
||||
- 表格 + 状态筛选(waiting/active/closed)
|
||||
- 未读消息数 Badge
|
||||
- 最后消息时间
|
||||
- 关闭会话操作
|
||||
- 点击跳转 `/health/consultations/:id`
|
||||
|
||||
#### ConsultationDetail.tsx(高复杂度,2 天)
|
||||
|
||||
- `ChatBubble` 组件渲染聊天气泡
|
||||
- 根据 `sender_role` 区分左右对齐
|
||||
- 支持内容类型:text / image(`ImagePreview`)/ voice / file
|
||||
- 消息按时间排列,支持滚动加载更多(分页)
|
||||
- 导出按钮
|
||||
|
||||
### 3.6 技术难点方案
|
||||
|
||||
#### ECharts 趋势图
|
||||
|
||||
使用已安装的 `@ant-design/charts` 的 `Line` 组件。
|
||||
|
||||
- 后端 API `/patients/:id/trends/:indicator` 返回时序数据
|
||||
- 前端转换为 `{ date: string, value: number }[]`
|
||||
- 支持多指标叠加(血压收缩压/舒张压双线)
|
||||
- 封装为 `VitalSignsChart`,接收 `patientId` + `indicators` 参数
|
||||
- 时间范围选择器(7天/30天/90天)
|
||||
|
||||
#### 日历视图
|
||||
|
||||
Ant Design `Calendar` + 自定义 `cellRender`:
|
||||
- DoctorSchedule:每个日期格显示排班时段标签
|
||||
- AppointmentList:每个日期格显示预约数量气泡
|
||||
|
||||
#### 聊天 UI
|
||||
|
||||
自定义 `ChatBubble` 组件,基于 Ant Design `Typography.Paragraph` + `Avatar`:
|
||||
- 根据 `sender_role` 区分样式
|
||||
- 只读模式(PC 后台只查看不发送)
|
||||
- 图片消息使用 `Image.PreviewGroup`
|
||||
|
||||
#### 导出
|
||||
|
||||
后端 blob 导出 + 前端触发下载,参照 `PluginCRUDPage` 中已有的 `exportPluginDataAsBlob` 模式。
|
||||
|
||||
#### 文件上传/预览
|
||||
|
||||
- 上传:Ant Design `Upload.Dragger`,上传到后端文件接口
|
||||
- 图片预览:Ant Design `Image.PreviewGroup`
|
||||
- PDF 预览:新窗口打开(V1 简化方案)
|
||||
|
||||
### 3.7 开发顺序
|
||||
|
||||
| Phase | 内容 | 天数 | 依赖 |
|
||||
|-------|------|------|------|
|
||||
| 1 | API 层 6 文件 + 通用组件 + 路由菜单 | 1.5 | 无 |
|
||||
| 2 | PatientList + PatientTagManage + PatientDetail 基本信息Tab | 2 | Phase 1 |
|
||||
| 3 | VitalSignsChart + 健康趋势 Tab + LabReportList + HealthRecordList | 3 | Phase 2 |
|
||||
| 4 | DoctorList + AppointmentList + DoctorSchedule | 3 | Phase 1 |
|
||||
| 5 | FollowUpTaskList + FollowUpRecordList + ConsultationList + ConsultationDetail | 3 | Phase 1 |
|
||||
| 6 | 打磨(暗色主题 + 响应式 + 联调) | 1 | Phase 2-5 |
|
||||
| **合计** | | **13.5 天** | |
|
||||
|
||||
---
|
||||
|
||||
## 4. 测试策略(阶段 2-3 交叉进行)
|
||||
|
||||
### 4.1 优先级排序
|
||||
|
||||
| 优先级 | 测试目标 | 预估用例数 | 工作量 |
|
||||
|--------|---------|-----------|--------|
|
||||
| P0 | `validation.rs` 纯函数 | 20-30 | 1 天 |
|
||||
| P0 | `appointment_service` CAS + 状态流转 | 15-20 | 2 天 |
|
||||
| P0 | `patient_service` CRUD + 状态机 | 15-20 | 2 天 |
|
||||
| P1 | `consultation_service` 消息原子性 | 10-15 | 2 天 |
|
||||
| P1 | `health_data_service` 指标数据 | 10-15 | 1 天 |
|
||||
| P2 | `follow_up_service` 链式任务 | 10 | 1 天 |
|
||||
|
||||
### 4.2 测试基础设施
|
||||
|
||||
在 `erp-health/Cargo.toml` 中添加 `[dev-dependencies]`:
|
||||
- `tokio` 的 `test` 和 `macros` feature
|
||||
- `sea-orm` 的 `mock` feature(用于简单单元测试,如 validation 纯函数)
|
||||
|
||||
对于涉及事务和 CAS 的集成测试(预约并发、消息原子性),使用 testcontainers-postgreSQL 做真实数据库测试,因为 SeaORM 的 `MockDatabaseConnection` 不支持复杂事务模拟。
|
||||
|
||||
创建 `tests/test_helpers.rs` 提供:
|
||||
- `create_test_health_state()` — 带 mock db 的 HealthState(单元测试用)
|
||||
- `create_integration_db()` — testcontainers PostgreSQL 实例(集成测试用)
|
||||
- 共享 fixture 工厂
|
||||
|
||||
### 4.3 关键测试场景
|
||||
|
||||
**预约 CAS 并发**:
|
||||
- 排班已满 → 创建预约失败
|
||||
- 排班有余 → CAS 成功 + 名额减 1
|
||||
- 并发创建 → 只有 max_appointments 个成功
|
||||
|
||||
**状态机转换**:
|
||||
- 合法转换:pending → confirmed → completed
|
||||
- 非法转换:completed → pending → 拒绝
|
||||
- 取消:任意状态 → cancelled(填 cancel_reason)
|
||||
|
||||
**随访链式任务**:
|
||||
- next_follow_up_date 不为空 → 自动创建新任务
|
||||
- 新任务的 assigned_to 沿用当前医护
|
||||
- next_follow_up_date 为空 → 不创建新任务
|
||||
|
||||
---
|
||||
|
||||
## 5. 实施路线图
|
||||
|
||||
### 5.1 总时间线(调整为 7 周)
|
||||
|
||||
```
|
||||
Week 1-2 | 安全地基(1.5-2 周)
|
||||
| ├── sanitize 全覆盖(2 天)
|
||||
| ├── 审计日志注入(2 天)
|
||||
| ├── 身份证号加密 + HMAC 索引 + 数据迁移(3-4 天)
|
||||
| └── 字段级脱敏(1-2 天)
|
||||
|
||||
Week 2-4 | 后端补完 + 测试(1.5-2 周)
|
||||
| ├── 事件处理器实现(2 天)
|
||||
| ├── 数据一致性修复(2 天)
|
||||
| ├── 随访逾期定时任务(1 天)
|
||||
| ├── article CRUD(0.5 天)
|
||||
| └── 核心路径测试(5-6 天)
|
||||
|
||||
Week 4-7 | Web 前端(3.5-4 周)
|
||||
| ├── Phase 1: API 层 + 通用组件 + 路由菜单(1.5 天)
|
||||
| ├── Phase 2: 核心入口页面(2 天)
|
||||
| ├── Phase 3: 健康数据页面(3 天)
|
||||
| ├── Phase 4: 预约排班页面(3 天)
|
||||
| ├── Phase 5: 随访咨询页面(3 天)
|
||||
| └── Phase 6: 打磨联调(1 天)
|
||||
|
||||
Week 7-8 | 端到端验证(1 周)
|
||||
| ├── 小程序联调
|
||||
| ├── 种子数据填充
|
||||
| ├── Docker 演示环境
|
||||
| └── 文档更新
|
||||
```
|
||||
|
||||
### 5.2 里程碑
|
||||
|
||||
| 里程碑 | 交付物 | 验收标准 |
|
||||
|--------|--------|---------|
|
||||
| M1 | 安全省基完成 | sanitize + 审计 + 加密 + 脱敏全部到位,cargo test 通过 |
|
||||
| M2 | 后端功能完整 | 事件处理器 + 数据一致性 + 测试覆盖,cargo test 通过 |
|
||||
| M3 | Web 3 核心页面 | PatientList + AppointmentList + DoctorSchedule 可操作 |
|
||||
| M4 | Web 10 页面完成 | 所有页面功能可用,pnpm build 通过 |
|
||||
| M5 | 端到端验证 | Web + 小程序 + 后端全链路可演示 |
|
||||
|
||||
### 5.3 风险和缓解
|
||||
|
||||
| 风险 | 概率 | 缓解 |
|
||||
|------|------|------|
|
||||
| ECharts 集成复杂度高 | 中 | 使用 @ant-design/charts 已安装,降低自研成本 |
|
||||
| 身份证加密影响现有查询 | 中 | HMAC 索引 + 数据迁移脚本 + 备份表 + 回滚方案 |
|
||||
| 10 页面开发时间超预期 | 高 | 按优先级裁剪,MVP 先做 3 核心页面 |
|
||||
| 文件上传能力未就绪 | 中 | V1 先支持 URL 存储,文件上传推迟到 V1.1 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 不在本设计范围内(推迟到 V2)
|
||||
|
||||
- 积分商城
|
||||
- 数据统计中心 / 运营驾驶舱
|
||||
- AI 辅助诊断/报告解读
|
||||
- 实时 WebSocket 在线咨询
|
||||
- 咨询消息按月分区
|
||||
- 事件幂等性(processed_events 去重表)
|
||||
- Polling Outbox 重试机制
|
||||
- HealthState 扩展 Redis 缓存
|
||||
- 国际化(英文等多语言)
|
||||
- 小程序医护端
|
||||
@@ -1,469 +0,0 @@
|
||||
# HMS 患者小程序迭代设计规格
|
||||
|
||||
> **版本**: v1.0
|
||||
> **日期**: 2026-04-24
|
||||
> **状态**: 草案
|
||||
> **关联**: 小程序初版设计 `2026-04-23-hms-miniprogram-design.md`
|
||||
|
||||
---
|
||||
|
||||
## 1. 概述
|
||||
|
||||
### 1.1 背景
|
||||
|
||||
小程序初版已完成 21 个页面、7 个 API service 的基础实现,覆盖登录、健康数据、预约挂号、检验报告、随访管理、用药提醒、健康资讯、个人中心。当前处于**开发阶段**,工程质量和用户体验存在明显短板,距测试阶段尚有差距。
|
||||
|
||||
### 1.2 问题全景
|
||||
|
||||
| 优先级 | 问题 | 影响 |
|
||||
|--------|------|------|
|
||||
| P0 | 大量重复代码(profile/reports ≈ report/index, profile/followups ≈ followup/index) | 维护成本翻倍 |
|
||||
| P0 | 预约详情通过 Storage 缓存传递而非 API 获取 | 数据不一致 |
|
||||
| P0 | EmptyState 导入方式不一致导致运行时报错 | 页面崩溃 |
|
||||
| P0 | 手机号绑定后端硬编码 `"13800000000"` | 无法上线 |
|
||||
| P0 | `getTodaySummary()` 调用的后端端点不存在 | 首页/健康页数据无法加载 |
|
||||
| P1 | ErrorState 组件定义但未使用 | 错误处理不统一 |
|
||||
| P1 | mixins.scss 定义但未使用 | 样式重复内联 |
|
||||
| P1 | 无全局错误边界 | 页面崩溃无兜底 |
|
||||
| P1 | tryRefreshToken 静默吞异常 | 调试困难 |
|
||||
| P1 | 趋势图缓存永不过期 | 数据过时 |
|
||||
| P1 | 随访详情获取低效(listTasks().find()) | 性能浪费 |
|
||||
| P1 | 首页/健康页缺少 loading 状态 | 体验空白 |
|
||||
| P1 | 用药提醒纯本地 Storage | 换设备即丢失(后续版本解决) |
|
||||
| P2 | 路径别名 @/* 未使用 | 代码可读性差 |
|
||||
| P2 | 无 schema 验证库 | 表单验证脆弱 |
|
||||
| P2 | 趋势图纯 CSS 柱状图 | 无交互能力 |
|
||||
| P2 | 用药提醒时间选择器未实现 | 功能不完整 |
|
||||
| P2 | 无日志/埋点/上报 | 无法追踪问题 |
|
||||
|
||||
### 1.3 迭代策略:混合策略
|
||||
|
||||
采用**先基建再模块**的混合策略,分 4 个 Sprint 交付:
|
||||
|
||||
```
|
||||
Sprint 0 (2-3天) Sprint 1 (3-4天) Sprint 2 (3-4天) Sprint 3 (4-5天)
|
||||
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
||||
│ 工程基础修复 │ → │ 健康数据打磨 │ → │ 预约+通知 │ → │ 报告/随访/ │
|
||||
│ │ │ │ │ │ │ 安全+增长 │
|
||||
│ · 消除重复代码│ │ · ECharts图表│ │ · 步骤指示器 │ │ · 指标卡片 │
|
||||
│ · 统一错误处理│ │ · 缓存TTL │ │ · 周视图日历 │ │ · Token加密 │
|
||||
│ · 修复数据传递│ │ · zod验证 │ │ · 订阅消息 │ │ · 手机号解密 │
|
||||
│ · 统一Loading │ │ · 状态色卡片 │ │ · 时段可视化 │ │ · 埋点+分享 │
|
||||
└──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘
|
||||
```
|
||||
|
||||
**原则**:Sprint 0 铺路,后续每个 Sprint 都受益于基础设施改善。Sprint 0 只修不建,不引入新依赖。
|
||||
|
||||
---
|
||||
|
||||
## 2. Sprint 0:工程基础修复
|
||||
|
||||
**目标**:消除最痛的工程问题,为后续所有 Sprint 铺路。约束 2-3 天完成。
|
||||
|
||||
### 2.1 修复阻断性 API 端点缺失
|
||||
|
||||
**现状**:前端 `services/health.ts` 的 `getTodaySummary()` 调用 `GET /health/vital-signs?date=today`,但后端路由中**不存在此端点**。后端仅有 `GET /health/patients/{id}/vital-signs`(需 patient_id 路径参数)。这意味着首页"今日健康"卡片和健康页的数据从一开始就**无法加载**。
|
||||
|
||||
**方案**:
|
||||
|
||||
- 后端在 `erp-health` 新增小程序专用端点 `GET /health/vital-signs/today`,通过 JWT `user_id` 自动关联 patient(类似已有的 `GET /health/vital-signs/trend` 模式)
|
||||
- 前端 `services/health.ts` 的 `getTodaySummary()` 调整为调用新端点
|
||||
- 此项为 **Sprint 0 最高优先级**,阻塞首页和健康页基本功能
|
||||
|
||||
**涉及文件**:
|
||||
- 后端新增:`erp-health` handler + 路由注册
|
||||
- 修改:`services/health.ts`
|
||||
|
||||
### 2.2 消除重复页面
|
||||
|
||||
**现状**:`pages/report/index` 与 `pages/profile/reports/index` 几乎完全重复,`pages/followup/index` 与 `pages/profile/followups/index` 同理。且 `report/index` 和 `followup/index` 没有明确的导航入口。
|
||||
|
||||
**方案**:
|
||||
|
||||
1. 删除 `pages/report/index` 和 `pages/followup/index` 及其 SCSS 文件
|
||||
2. 从 `app.config.ts` 移除对应路由注册
|
||||
3. 首页快捷入口和 profile 菜单统一指向 `profile/reports` 和 `profile/followups`
|
||||
4. 如果后续需要独立入口,则抽取共享组件 `components/ReportList` 和 `components/FollowupList`,两个页面只做薄壳路由
|
||||
|
||||
**涉及文件**:
|
||||
- 删除:`pages/report/index.tsx`、`pages/report/index.scss`
|
||||
- 删除:`pages/followup/index.tsx`、`pages/followup/index.scss`
|
||||
- 修改:`app.config.ts`(移除路由)
|
||||
- 修改:`pages/index/index.tsx`(快捷入口路径)
|
||||
|
||||
### 2.2 统一错误处理
|
||||
|
||||
**现状**:`ErrorState` 组件已定义但未被任何页面使用,各页面内联 `showToast` 错误提示。无全局错误边界。
|
||||
|
||||
**方案**:
|
||||
|
||||
1. 所有列表页、详情页统一使用 `ErrorState` 组件,替换内联错误提示
|
||||
2. 在 `app.tsx` 添加 React Error Boundary 组件,兜底页面崩溃
|
||||
3. 新建 `components/ErrorBoundary/index.tsx`
|
||||
4. 修复 `tryRefreshToken` 的 catch 块,添加 `console.error` 日志
|
||||
|
||||
**涉及文件**:
|
||||
- 新增:`components/ErrorBoundary/index.tsx`
|
||||
- 修改:`app.tsx`(包裹 ErrorBoundary)
|
||||
- 修改:所有列表页和详情页(替换内联错误处理为 ErrorState)
|
||||
- 修改:`services/request.ts`(tryRefreshToken 日志)
|
||||
|
||||
### 2.3 修复数据传递问题
|
||||
|
||||
**预约详情**:
|
||||
- 移除 `appointment_detail_cache` Storage 传递
|
||||
- 改为进入页面时通过 `GET /health/appointments/:id` 获取数据
|
||||
- **后端需新增此端点**(当前仅有列表 `GET`、创建 `POST`、状态更新 `PUT`,缺少单条查询 `GET`)
|
||||
|
||||
**随访详情**:
|
||||
- 后端**需新增** `GET /health/follow-up-tasks/:id` 单条查询端点(当前 `{id}` 路由仅注册了 `PUT` 和 `DELETE`,缺少 `GET`)
|
||||
- 前端替换 `listTasks().find()` 为直接按 ID 查询
|
||||
|
||||
> **注意**:以上后端新增端点为 Sprint 0 前置阻塞项。如果后端资源有限,前端先做"调用端点"的准备代码,后端并行实现。
|
||||
|
||||
**涉及文件**:
|
||||
- 修改:`pages/appointment/detail/index.tsx`
|
||||
- 修改:`services/appointment.ts`(新增 getDetail 方法)
|
||||
- 修改:`services/followup.ts`(新增 getTaskDetail 方法)
|
||||
- 后端新增:`erp-health` 预约单条查询 + 随访单条查询端点
|
||||
|
||||
### 2.4 统一 Loading 状态
|
||||
|
||||
**现状**:首页和健康页的 `loading` 状态已在 store 中定义但未在 UI 层消费。详情页使用内联 `<Text>加载中...</Text>`。
|
||||
|
||||
**方案**:
|
||||
|
||||
1. 首页和健康页在数据加载时展示 `Loading` 组件
|
||||
2. 所有详情页统一使用 `Loading` 组件替换内联文字
|
||||
3. 预约创建页三步骤切换时也展示 loading
|
||||
|
||||
**涉及文件**:
|
||||
- 修改:`pages/index/index.tsx`(消费 loading 状态)
|
||||
- 修改:`pages/health/index.tsx`(消费 loading 状态)
|
||||
- 修改:所有详情页 tsx(替换内联加载文字)
|
||||
|
||||
### 2.5 杂项修复
|
||||
|
||||
| 项目 | 方案 |
|
||||
|------|------|
|
||||
| EmptyState 导入 bug | 首页 `import { EmptyState }` 改为 `import EmptyState`(默认导入) |
|
||||
| 路径别名启用 | `services/` 和 `stores/` 层的 import 逐步改为 `@/` 别名 |
|
||||
| mixins.scss 复用 | 新写的页面样式使用 `@include card`、`@include flex-center`、`@include safe-bottom` |
|
||||
|
||||
---
|
||||
|
||||
## 3. Sprint 1:健康数据模块打磨
|
||||
|
||||
**目标**:升级健康数据录入、展示和趋势分析体验,从"能用"到"好用"。
|
||||
|
||||
### 3.1 健康卡片状态色
|
||||
|
||||
**现状**:四张健康卡片(血压/心率/血糖/体重)样式统一灰色,无状态区分。
|
||||
|
||||
**方案**:
|
||||
|
||||
每张卡片根据指标状态着色:
|
||||
- **正常**:左侧绿色边条 + 绿色"正常 ─"标签
|
||||
- **偏高**:左侧红色边条 + 红色"偏高 ▲{差值}"标签
|
||||
- **偏低**:左侧红色边条 + 红色"偏低 ▼{差值}"标签
|
||||
- **无数据**:灰色,保持现状
|
||||
|
||||
异常指标数值变红,卡片底部显示参考范围。
|
||||
|
||||
**后端配合**:后端需在新增的 `GET /health/vital-signs/today` 端点中返回 `status`(normal/high/low)和 `reference_range`。前端 `TodaySummary` 类型同步新增 `reference_range` 字段(当前已有 `status` 字段但后端无对应返回)。
|
||||
|
||||
**涉及文件**:
|
||||
- 修改:`pages/health/index.tsx`(卡片样式逻辑)
|
||||
- 修改:`pages/index/index.tsx`(首页健康卡片同步更新)
|
||||
- 修改:`services/health.ts`(类型定义增加 status 字段)
|
||||
|
||||
### 3.2 ECharts 趋势图
|
||||
|
||||
**现状**:纯 CSS div 柱状图,无交互、无缩放、无 tooltip。
|
||||
|
||||
**方案**:
|
||||
|
||||
引入 `echarts-taro3-react`(设计规格中已规划)。**前置条件**:Sprint 1 开始前需做技术预研(spike),验证 `echarts-taro3-react` 在 Taro 4.2.0 + webpack5 下的兼容性。如果不可用,备选方案为 `echarts-for-weixin` + 手动封装为 React 组件。
|
||||
|
||||
实现:
|
||||
|
||||
- **折线图**:数据点连线,异常点标红放大
|
||||
- **参考范围色带**:正常值区间以半透明绿色背景显示
|
||||
- **Tooltip**:长按/点击显示具体数值和日期
|
||||
- **时间范围切换**:7天/30天/90天 三个 tab
|
||||
- **缓存 TTL**:趋势数据缓存 5 分钟后自动过期,强制重新请求
|
||||
|
||||
**涉及文件**:
|
||||
- 新增:`components/TrendChart/index.tsx`、`components/TrendChart/index.scss`
|
||||
- 重写:`pages/health/trend/index.tsx`
|
||||
- 修改:`stores/health.ts`(缓存 TTL 机制)
|
||||
- 新增依赖:`echarts-taro3-react`
|
||||
|
||||
### 3.3 表单验证升级
|
||||
|
||||
**现状**:所有表单验证为手动 if 判断,无 schema 约束。
|
||||
|
||||
**方案**:
|
||||
|
||||
引入 `zod`(~3KB gzip),为每个表单定义验证 schema:
|
||||
|
||||
```typescript
|
||||
// 示例:体征录入验证
|
||||
const vitalSignSchema = z.object({
|
||||
indicator_type: z.enum(['blood_pressure', 'heart_rate', 'blood_sugar', 'weight']),
|
||||
value: z.number().positive(),
|
||||
extra: z.object({ systolic: z.number().min(60).max(250).optional(),
|
||||
diastolic: z.number().min(40).max(150).optional() }).optional(),
|
||||
measured_at: z.string().datetime().optional(),
|
||||
note: z.string().max(200).optional()
|
||||
});
|
||||
```
|
||||
|
||||
异常值即时警告(如收缩压 > 180 显示红色提示"请及时就医")。
|
||||
|
||||
录入成功后自动刷新首页卡片 + 清除趋势缓存。
|
||||
|
||||
**涉及文件**:
|
||||
- 修改:`pages/health/input/index.tsx`(zod schema 验证)
|
||||
- 修改:`stores/health.ts`(录入成功后清除缓存)
|
||||
- 新增依赖:`zod`
|
||||
|
||||
---
|
||||
|
||||
## 4. Sprint 2:预约挂号 + 通知触达
|
||||
|
||||
**目标**:优化预约三步流程体验,建立微信订阅消息通知机制。
|
||||
|
||||
### 4.1 三步流程升级
|
||||
|
||||
**现状**:三个步骤(选科室 → 选医生 → 选日期时段)无进度指示,排班信息为纯文字列表。
|
||||
|
||||
**方案**:
|
||||
|
||||
**步骤指示器**:
|
||||
- 新增 `components/StepIndicator/index.tsx`
|
||||
- 顶部固定 1→2→3 步骤条,当前步骤高亮,已完成步骤可点击回退
|
||||
- 步骤间切换带过渡动画
|
||||
|
||||
**科室选择**:
|
||||
- 从文字列表改为宫格卡片(图标 + 科室名 + 医生数)
|
||||
- 每个科室卡片可点击,选中后高亮边框
|
||||
|
||||
**排班日历**:
|
||||
- 新增 `components/WeekCalendar/index.tsx` 周视图日历
|
||||
- 有排班的日期标记绿点,无排班的日期灰色
|
||||
- 点击日期展示该日可用时段卡片
|
||||
- 时段卡片按剩余名额着色:>3 绿色、1-3 橙色、0 灰色不可选
|
||||
|
||||
**涉及文件**:
|
||||
- 新增:`components/StepIndicator/index.tsx`
|
||||
- 新增:`components/WeekCalendar/index.tsx`
|
||||
- 重写:`pages/appointment/create/index.tsx`
|
||||
|
||||
### 4.2 微信订阅消息
|
||||
|
||||
**现状**:无任何推送通知机制。
|
||||
|
||||
**方案**:
|
||||
|
||||
1. 后端在微信公众平台注册订阅消息模板:
|
||||
- 预约就诊提醒(就诊前 1 天推送)
|
||||
- 随访任务提醒(截止前 1 天推送)
|
||||
- 报告出具通知(新报告发布时推送)
|
||||
|
||||
2. 前端在关键场景引导用户订阅:
|
||||
- 预约成功后弹出订阅授权
|
||||
- 随访提交后引导订阅下次提醒
|
||||
|
||||
3. 后端定时任务检查待推送消息并触发
|
||||
|
||||
4. **降级设计**:用户拒绝订阅时,消息仍写入 `erp-message` 消息中心。小程序"我的"页面顶部显示未读消息数量红点,作为消息触达的备选渠道。
|
||||
|
||||
**涉及文件**:
|
||||
- 修改:`pages/appointment/detail/index.tsx`(预约成功后订阅引导)
|
||||
- 修改:`pages/followup/detail/index.tsx`(随访提交后订阅引导)
|
||||
- 后端新增:`erp-server` 订阅消息模板注册 + 定时推送任务
|
||||
|
||||
---
|
||||
|
||||
## 5. Sprint 3:报告/随访/个人中心 + 安全 + 增长
|
||||
|
||||
**目标**:打磨剩余模块,完成安全加固和增长基础建设,达到可测试状态。
|
||||
|
||||
### 5.1 报告详情页升级
|
||||
|
||||
**现状**:所有指标卡片样式相同,无法一眼区分正常/异常。
|
||||
|
||||
**方案**:
|
||||
|
||||
指标卡片按状态着色:
|
||||
- **正常**:绿色背景 + 绿色"✓ 正常"标签 + 绿色数值
|
||||
- **偏高**:红色背景 + 红色"↑ 偏高"标签 + 红色数值
|
||||
- **偏低**:红色背景 + 红色"↓ 偏低"标签 + 红色数值
|
||||
|
||||
顶部汇总标签:`2 项异常 · 1 项正常`,一眼掌握整体状况。
|
||||
|
||||
**涉及文件**:
|
||||
- 修改:`pages/report/detail/index.tsx`
|
||||
- 修改:`pages/profile/reports/index.tsx`(如果仍独立存在)
|
||||
|
||||
### 5.2 随访 UX 细节
|
||||
|
||||
- 任务卡片增加截止日期倒计时("还剩 2 天",红色紧迫)
|
||||
- 过期任务灰色标记
|
||||
- 提交记录后增加"提交成功"确认动画(checkmark 缩放)
|
||||
|
||||
**涉及文件**:
|
||||
- 修改:`pages/profile/followups/index.tsx`
|
||||
- 修改:`pages/followup/detail/index.tsx`
|
||||
|
||||
### 5.3 个人中心改进
|
||||
|
||||
**用药提醒**:
|
||||
- 实现时间选择器 Picker(替换当前静态文本)
|
||||
- 增加"提醒开关"(enabled/disabled)
|
||||
- 注:用药提醒数据仍为本地 Storage 存储,**后端同步作为后续版本事项**。MVP 阶段接受"换设备即丢失"的限制。
|
||||
|
||||
**就诊人管理**:
|
||||
- 增加编辑功能(当前只能添加不能编辑)
|
||||
- 复用 `family-add` 页面,传入已有数据进入编辑模式
|
||||
|
||||
**涉及文件**:
|
||||
- 修改:`pages/profile/medication/index.tsx`(时间 Picker)
|
||||
- 修改:`pages/profile/family/index.tsx`(编辑入口)
|
||||
- 修改:`pages/profile/family-add/index.tsx`(编辑模式支持)
|
||||
|
||||
### 5.4 安全加固
|
||||
|
||||
#### 5.4.1 Token 安全
|
||||
|
||||
**现状**:Access Token 和 Refresh Token 明文存储在 `Taro.setStorageSync`。
|
||||
|
||||
**方案**:
|
||||
|
||||
MVP 阶段采用简化方案:微信小程序的 Storage 本身有沙箱隔离,明文存储的边际风险有限。做以下最低成本改进:
|
||||
|
||||
- 使用 `wx.getRandomValues()` 生成随机密钥,单独 key 存储
|
||||
- Token 存储时用此密钥做简单混淆(XOR 或 AES-ECB 单块加密)
|
||||
- 目的:防止 Storage 被直接明文读取,非追求密码学安全级别
|
||||
|
||||
> **后续版本**:如果合规要求提高,再升级为完整的 AES-GCM 方案。
|
||||
|
||||
**涉及文件**:
|
||||
- 新增:`utils/crypto.ts`(轻量混淆工具)
|
||||
- 修改:`stores/auth.ts`(Storage 读写走混淆层)
|
||||
|
||||
#### 5.4.2 手机号真实解密
|
||||
|
||||
**现状**:`wechat_service.rs` 第 82 行硬编码 `"13800000000"`。
|
||||
|
||||
**方案**:
|
||||
- 后端接入微信 `phonenumber.getPhoneNumber` 接口
|
||||
- 使用 `encryptedData` + `iv` + `session_key` 解密真实手机号
|
||||
- 前端无需改动(已传递正确的 encryptedData 和 iv)
|
||||
|
||||
**涉及文件**:
|
||||
- 修改:`crates/erp-auth/src/service/wechat_service.rs`
|
||||
|
||||
#### 5.4.3 用户协议与隐私政策
|
||||
|
||||
- 新增 `pages/agreement/index.tsx` 页面
|
||||
- 登录页增加"阅读并同意《用户协议》和《隐私政策》"勾选
|
||||
- 权限使用说明文案(获取手机号用途声明)
|
||||
|
||||
**涉及文件**:
|
||||
- 新增:`pages/agreement/index.tsx`
|
||||
- 修改:`pages/login/index.tsx`(协议勾选)
|
||||
- 修改:`app.config.ts`(新增路由)
|
||||
|
||||
### 5.5 增长基础
|
||||
|
||||
#### 5.5.1 数据埋点
|
||||
|
||||
新增 `services/analytics.ts`,轻量事件记录:
|
||||
|
||||
```typescript
|
||||
// 核心事件类型
|
||||
type AnalyticsEvent =
|
||||
| { type: 'page_view'; page: string; duration_ms?: number }
|
||||
| { type: 'feature_use'; feature: string; action: string }
|
||||
| { type: 'error'; message: string; stack?: string }
|
||||
```
|
||||
|
||||
- 页面进入/离开自动记录 `page_view`
|
||||
- 关键操作(录入数据、创建预约、提交随访)记录 `feature_use`
|
||||
- 捕获的错误记录 `error`
|
||||
- MVP 阶段:事件写入本地 `console.info` + Taro Storage 缓存(最近 100 条)
|
||||
- 后续版本:批量上报到后端 `POST /api/v1/analytics/events`
|
||||
|
||||
**涉及文件**:
|
||||
- 新增:`services/analytics.ts`
|
||||
- 修改:`app.tsx`(全局页面进入/离开监听)
|
||||
|
||||
#### 5.5.2 分享能力
|
||||
|
||||
- 文章详情页支持分享到微信好友/朋友圈
|
||||
- 自定义分享卡片(标题 + 摘要 + 封面图)通过 `onShareAppMessage` 和 `onShareTimeline` 实现
|
||||
|
||||
> **后续版本**:健康报告 Canvas 分享图片生成、PC 扫码登录。
|
||||
|
||||
**涉及文件**:
|
||||
- 修改:`pages/article/detail/index.tsx`(onShareAppMessage + onShareTimeline)
|
||||
|
||||
---
|
||||
|
||||
## 6. 文件变更总览
|
||||
|
||||
### 新增文件
|
||||
|
||||
| 文件 | 说明 | Sprint |
|
||||
|------|------|--------|
|
||||
| `components/ErrorBoundary/index.tsx` | 全局错误边界 | 0 |
|
||||
| `components/TrendChart/index.tsx` | ECharts 趋势图 | 1 |
|
||||
| `components/StepIndicator/index.tsx` | 步骤指示器 | 2 |
|
||||
| `components/WeekCalendar/index.tsx` | 周视图日历 | 2 |
|
||||
| `pages/agreement/index.tsx` | 用户协议/隐私政策 | 3 |
|
||||
| `utils/crypto.ts` | Token 加密工具 | 3 |
|
||||
| `services/analytics.ts` | 数据埋点 | 3 |
|
||||
|
||||
### 删除文件
|
||||
|
||||
| 文件 | 原因 | Sprint |
|
||||
|------|------|--------|
|
||||
| `pages/report/index.tsx` | 与 profile/reports 重复 | 0 |
|
||||
| `pages/report/index.scss` | 同上 | 0 |
|
||||
| `pages/followup/index.tsx` | 与 profile/followups 重复 | 0 |
|
||||
| `pages/followup/index.scss` | 同上 | 0 |
|
||||
|
||||
### 新增依赖
|
||||
|
||||
| 依赖 | 用途 | 体积 | Sprint |
|
||||
|------|------|------|--------|
|
||||
| `echarts-taro3-react` | 交互式图表 | 封装层 ~5KB + echarts 按需 ~100-200KB (gzip) | 1 |
|
||||
| `zod` | 表单 schema 验证(长期投资) | ~3KB (gzip) | 1 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 约束与风险
|
||||
|
||||
| 风险 | 应对策略 |
|
||||
|------|---------|
|
||||
| ECharts 增大包体积 | 按需引入 echarts 模块,不引入全量包;监控主包大小不超过 2MB |
|
||||
| 微信订阅消息需用户主动触发 | 在预约成功、随访提交等高意愿场景引导订阅 |
|
||||
| Token 加密增加启动耗时 | AES-GCM 加解密 < 1ms,可忽略 |
|
||||
| zod 增加包体积 | 3KB gzip,远小于自写验证代码量 |
|
||||
| Sprint 0 范围膨胀 | 严格只修不建,不引入新依赖,不重构架构 |
|
||||
| 后端端点未实现阻塞前端 | Sprint 0/1 的端点基本已实现;Sprint 2 订阅消息、Sprint 3 analytics 需后端配合 |
|
||||
|
||||
---
|
||||
|
||||
## 8. 验收标准
|
||||
|
||||
每个 Sprint 完成时必须满足:
|
||||
|
||||
- [ ] `pnpm build:weapp` 生产构建通过
|
||||
- [ ] 微信开发者工具无编译错误
|
||||
- [ ] 所有涉及页面真机预览功能正常
|
||||
- [ ] 无 console.error 或未捕获异常
|
||||
- [ ] 已修改的页面 loading/error/empty 三态完整
|
||||
- [ ] 所有代码已提交
|
||||
@@ -1,313 +0,0 @@
|
||||
# HMS 功能完善迭代设计规格
|
||||
|
||||
> 日期: 2026-04-25
|
||||
> 状态: 已确认(审查修正版)
|
||||
> 关联: `docs/superpowers/specs/2026-04-25-erp-ai-module-design.md`
|
||||
|
||||
## 1. 背景与目标
|
||||
|
||||
### 1.1 项目现状
|
||||
|
||||
HMS 健康管理平台已完成核心业务开发(237 次提交、57k 行 Rust + 174 前端文件),但存在以下功能缺口:
|
||||
|
||||
- **按钮级权限控制缺失** — 路由守卫已有,但操作按钮(新增/编辑/删除)未做权限过滤;前端缺少权限数据源(`UserInfo` 接口不含 `permissions` 字段)
|
||||
- **AI 模块管理端空白** — 后端 6 个 API 端点中 4 个 SSE 流式端点可用,但 Prompt CRUD、分析历史查询、用量统计端点均为空壳或缺失
|
||||
- **小程序端 AI 不可见** — 患者无法查看 AI 分析报告
|
||||
|
||||
### 1.2 目标
|
||||
|
||||
通过纵向切片方式逐步交付三个功能域,每个切片从前到后完整打通:
|
||||
|
||||
1. **切片 1:按钮级权限** — 基础设施,后续页面的前置依赖
|
||||
2. **切片 2:AI 管理端** — 3 个 PC 管理页面
|
||||
3. **切片 3:小程序报告** — 患者端只读查看
|
||||
|
||||
---
|
||||
|
||||
## 2. 切片 1:按钮级权限控制
|
||||
|
||||
### 2.1 架构
|
||||
|
||||
```
|
||||
后端新增 /api/v1/auth/me/permissions → 返回当前用户权限码列表
|
||||
↓
|
||||
auth store (permissions: string[]) ← 登录时从新端点加载
|
||||
↓
|
||||
usePermission(code) → { hasPermission: boolean }
|
||||
↓
|
||||
<AuthButton code="health.patient.manage"> ... </AuthButton>
|
||||
↓
|
||||
无权限 → 不渲染(hidden 模式)
|
||||
有权限 → 正常渲染子元素
|
||||
```
|
||||
|
||||
**前置依赖:** 当前 `UserInfo` 接口不含 `permissions` 字段(仅含 `roles`)。需后端新增 `/api/v1/auth/me/permissions` 端点,返回当前用户所有权限码的扁平列表(从角色 → 权限关联表聚合)。超级管理员(`is_system: true`)默认返回全部权限码。
|
||||
|
||||
### 2.2 组件设计
|
||||
|
||||
**usePermission hook**
|
||||
|
||||
位置: `apps/web/src/hooks/usePermission.ts`
|
||||
|
||||
```typescript
|
||||
function usePermission(code: string): { hasPermission: boolean }
|
||||
```
|
||||
|
||||
- 从 auth store 读取当前用户 permissions 数组
|
||||
- 返回 code 是否在权限列表中
|
||||
- 权限数据加载失败时默认无权限(安全降级)
|
||||
|
||||
**AuthButton 组件**
|
||||
|
||||
位置: `apps/web/src/components/AuthButton.tsx`
|
||||
|
||||
Props:
|
||||
- `code: string` — 权限码(如 `health.patient.manage`)
|
||||
- `children: ReactNode` — 受保护的按钮内容
|
||||
|
||||
行为: 无权限时不渲染 children(hidden 模式)。
|
||||
|
||||
**AuthGuard 组件**
|
||||
|
||||
位置: `apps/web/src/components/AuthGuard.tsx`
|
||||
|
||||
Props:
|
||||
- `code: string` — 权限码
|
||||
- `children: ReactNode` — 受保护的内容块
|
||||
|
||||
行为: 同 AuthButton,用于包裹非按钮内容(如整个 Tab、区块)。
|
||||
|
||||
### 2.3 改造范围
|
||||
|
||||
优先改造健康模块 15 个页面中的操作按钮:
|
||||
|
||||
| 页面 | 按钮权限码 |
|
||||
|------|-----------|
|
||||
| PatientList | health.patient.manage |
|
||||
| PatientDetail | health.patient.manage |
|
||||
| AppointmentList | health.appointment.manage |
|
||||
| DoctorList | health.doctor.manage |
|
||||
| DoctorSchedule | health.doctor.manage |
|
||||
| FollowUpTaskList | health.follow-up.manage |
|
||||
| FollowUpRecordList | health.follow-up.manage |
|
||||
| ConsultationList | health.consultation.manage |
|
||||
| ConsultationDetail | health.consultation.manage |
|
||||
| OfflineEventList | health.articles.manage |
|
||||
| PatientTagManage | health.patient.manage |
|
||||
| StatisticsDashboard | health.health-data.list (只读) |
|
||||
| PointsProductList | health.points.manage |
|
||||
| PointsOrderList | health.points.list |
|
||||
| PointsRuleList | health.points.manage |
|
||||
|
||||
扩展到基础模块页面(Users, Roles, Organizations, Workflow 等)。
|
||||
|
||||
### 2.4 验证标准
|
||||
|
||||
- [ ] 无权限用户看不到操作按钮
|
||||
- [ ] 有权限用户操作正常
|
||||
- [ ] 权限变更后界面实时更新(无需刷新)
|
||||
|
||||
---
|
||||
|
||||
## 3. 切片 2:AI 管理端 3 页面
|
||||
|
||||
### 3.1 路由设计
|
||||
|
||||
```
|
||||
/health/ai/prompts → Prompt 管理
|
||||
/health/ai/analysis → 分析历史
|
||||
/health/ai/usage → 用量统计
|
||||
```
|
||||
|
||||
### 3.2 页面 A — Prompt 管理
|
||||
|
||||
位置: `apps/web/src/pages/health/AiPromptList.tsx`
|
||||
|
||||
**功能清单:**
|
||||
|
||||
| 功能 | 说明 |
|
||||
|------|------|
|
||||
| 列表展示 | 表格:名称/类型(化验单解读、趋势分析、体检方案、报告摘要)/版本号/状态(active/draft)/更新时间 |
|
||||
| 新建 Prompt | Modal 表单:名称、类型(下拉)、系统提示词、用户提示词模板(支持 `{{variable}}` 占位符) |
|
||||
| 编辑 Prompt | 同新建,自动递增版本号 |
|
||||
| 激活/停用 | 切换按钮,激活时停用同类型旧模板 |
|
||||
| 版本历史 | 展开行显示所有历史版本,支持一键回滚 |
|
||||
|
||||
**API 封装:**
|
||||
|
||||
位置: `apps/web/src/api/ai/prompts.ts`
|
||||
|
||||
```typescript
|
||||
// 后端需新增 Prompt CRUD 端点(当前仅有 service 层的 get_active_prompt + create_prompt)
|
||||
getPrompts(params: ListParams): Promise<PaginatedResponse<Prompt>> // GET /api/v1/ai/prompts
|
||||
createPrompt(data: CreatePromptDto): Promise<Prompt> // POST /api/v1/ai/prompts
|
||||
updatePrompt(id: string, data: UpdatePromptDto): Promise<Prompt> // PUT /api/v1/ai/prompts/{id}
|
||||
activatePrompt(id: string): Promise<Prompt> // POST /api/v1/ai/prompts/{id}/activate
|
||||
rollbackPrompt(id: string): Promise<Prompt> // POST /api/v1/ai/prompts/{id}/rollback
|
||||
```
|
||||
|
||||
**版本回滚机制:** 每次编辑 Prompt 创建新记录(递增 version),回滚 = 将目标旧版本 `is_active` 设为 `true` 并将当前激活版本 `is_active` 设为 `false`。不删除任何版本记录。
|
||||
|
||||
**权限码:** `ai.prompt.list`(查看)、`ai.prompt.manage`(编辑/激活/回滚)
|
||||
|
||||
### 3.3 页面 B — 分析历史
|
||||
|
||||
位置: `apps/web/src/pages/health/AiAnalysisList.tsx`
|
||||
|
||||
**功能清单:**
|
||||
|
||||
| 功能 | 说明 |
|
||||
|------|------|
|
||||
| 列表展示 | 表格:分析类型/患者姓名/状态(streaming/completed/failed)/创建时间/token 用量 |
|
||||
| 详情查看 | 点击行展开/Modal 展示完整分析结果(Markdown 渲染) |
|
||||
| 筛选 | 按类型(4 种)、时间范围(DateRangePicker)、患者(PatientSelect 组件复用) |
|
||||
| 重新分析 | 对 failed 记录支持重新发起分析 |
|
||||
|
||||
**API 封装:**
|
||||
|
||||
位置: `apps/web/src/api/ai/analysis.ts`
|
||||
|
||||
```typescript
|
||||
// 后端当前 list_analysis/get_analysis 为空壳(返回 ApiResponse::ok(())),需实现真实查询
|
||||
getAnalysisHistory(params: AnalysisQueryParams): Promise<PaginatedResponse<Analysis>> // GET /api/v1/ai/analysis/history
|
||||
getAnalysisDetail(id: string): Promise<Analysis> // GET /api/v1/ai/analysis/{id}
|
||||
```
|
||||
|
||||
**权限码:** `ai.analysis.list`(查看)、`ai.analysis.manage`(重新分析)
|
||||
|
||||
### 3.4 页面 C — 用量统计
|
||||
|
||||
位置: `apps/web/src/pages/health/AiUsageDashboard.tsx`
|
||||
|
||||
**功能清单:**
|
||||
|
||||
| 功能 | 说明 |
|
||||
|------|------|
|
||||
| 概览卡片 | 4 张 StatCard:总用量/本月/今日/平均 token |
|
||||
| 趋势图 | Ant Design Charts 折线图,按日/周/月切换 |
|
||||
| 类型分布 | 饼图展示 4 种分析类型的占比 |
|
||||
| 用户排行 | 表格展示用户维度用量排名 |
|
||||
|
||||
**API 封装:**
|
||||
|
||||
位置: `apps/web/src/api/ai/usage.ts`
|
||||
|
||||
```typescript
|
||||
// 后端需完全新增:路由、handler、聚合 service
|
||||
// ai_usage_logs 表需增加 created_by 列(当前缺失 user_id),或复用 ai_analysis_results.created_by 做用户排行
|
||||
getUsageOverview(): Promise<UsageOverview> // GET /api/v1/ai/usage/overview
|
||||
getUsageTrend(params: TrendParams): Promise<TrendData[]> // GET /api/v1/ai/usage/trend
|
||||
getUsageByType(): Promise<TypeDistribution[]> // GET /api/v1/ai/usage/by-type
|
||||
getUsageByUser(params: UserRankingParams): Promise<PaginatedResponse<UserUsage>> // GET /api/v1/ai/usage/by-user
|
||||
```
|
||||
|
||||
**后端补充:** 需在 erp-ai 中新增用量统计聚合端点。优先方案:复用 `ai_analysis_results` 表的 `created_by` 字段做用户维度排行,避免修改 `ai_usage_logs` 表结构。如需精确 token 统计,后续可加迁移增加 `user_id` 列。
|
||||
|
||||
**权限码:** `ai.usage.list`
|
||||
|
||||
### 3.5 菜单注册
|
||||
|
||||
在 `apps/web/src/layouts/MainLayout.tsx` 健康管理菜单组下新增 AI 分析入口。当前菜单为扁平结构(无子菜单折叠),新增项直接追加为同级菜单项:
|
||||
|
||||
```
|
||||
健康管理
|
||||
├── 患者管理
|
||||
├── 医护管理
|
||||
├── 预约管理
|
||||
├── 随访管理
|
||||
├── 咨询管理
|
||||
├── 积分商城
|
||||
├── 统计看板
|
||||
├── AI Prompt 管理 ← 新增(扁平)
|
||||
├── AI 分析历史 ← 新增(扁平)
|
||||
└── AI 用量统计 ← 新增(扁平)
|
||||
```
|
||||
|
||||
### 3.6 验证标准
|
||||
|
||||
- [ ] Prompt CRUD 全流程可用(创建/编辑/激活/回滚)
|
||||
- [ ] 分析历史可筛选、可查看详情(Markdown 正确渲染)
|
||||
- [ ] 用量统计图表数据正确
|
||||
- [ ] 所有操作按钮受 AuthButton 权限控制
|
||||
- [ ] 页面响应式布局正常
|
||||
|
||||
---
|
||||
|
||||
## 4. 切片 3:小程序 AI 报告查看
|
||||
|
||||
### 4.1 新增页面
|
||||
|
||||
**AI 报告列表页**
|
||||
|
||||
位置: `apps/miniprogram/src/pages/ai-report/list/index.tsx`
|
||||
|
||||
- 调用 `GET /api/v1/ai/analysis/history` (后端需实现,根据 JWT user_id → patient_id 自动过滤)
|
||||
- 列表展示分析记录(类型图标 + 时间 + 状态标签)
|
||||
- 点击进入详情
|
||||
|
||||
**AI 报告详情页**
|
||||
|
||||
位置: `apps/miniprogram/src/pages/ai-report/detail/index.tsx`
|
||||
|
||||
- 调用 `GET /api/v1/ai/analysis/{id}`
|
||||
- 使用 `taro-markdown` 组件渲染 Markdown 格式的分析结果(需先验证兼容性)
|
||||
- 底部展示分析时间和 token 用量信息
|
||||
|
||||
### 4.2 路由集成
|
||||
|
||||
在首页(`pages/index/index.tsx`)健康数据区域增加"AI 报告"入口卡片。
|
||||
|
||||
### 4.3 后端依赖
|
||||
|
||||
后端 `list_analysis` 和 `get_analysis` 当前为空壳(仅验证权限后返回空值),需实现:
|
||||
- 根据 JWT 中的 user_id 查找关联 patient_id
|
||||
- 从 `ai_analysis_results` 表查询该患者的分析记录
|
||||
- 返回不含 PII 的脱敏结果
|
||||
|
||||
### 4.4 验证标准
|
||||
|
||||
- [ ] 患者可查看自己的 AI 分析历史
|
||||
- [ ] 详情页 Markdown 正确渲染
|
||||
- [ ] 无法查看其他患者的报告
|
||||
- [ ] 无报告时显示空状态提示
|
||||
|
||||
---
|
||||
|
||||
## 5. 实施顺序
|
||||
|
||||
| 阶段 | 内容 | 依赖 | 预计工作量 |
|
||||
|------|------|------|-----------|
|
||||
| P0a | 后端:新增 `/api/v1/auth/me/permissions` 端点 | 无 | erp-auth handler + service |
|
||||
| P0b | 后端:实现 Prompt CRUD 端点(list/create/update/activate/rollback) | 无 | erp-ai handler + service |
|
||||
| P0c | 后端:实现分析历史查询(list_analysis/get_analysis 从空壳到真实查询) | 无 | erp-ai handler + service |
|
||||
| P0d | 后端:新增用量统计聚合端点(overview/trend/by-type/by-user) | 无 | erp-ai handler + service + 可能迁移 |
|
||||
| P1 | 前端:usePermission hook + AuthButton/AuthGuard 组件 + auth store 加载 permissions | P0a | 3 文件 |
|
||||
| P2 | 前端:健康模块页面按钮权限改造 | P1 | 15 文件 |
|
||||
| P3 | 前端:AI API 封装(3 个 service 文件) | P0b-d | 3 文件 |
|
||||
| P4 | 前端:AI Prompt 管理页面 | P1, P3 | 1 文件 |
|
||||
| P5 | 前端:AI 分析历史页面 | P1, P3 | 1 文件 |
|
||||
| P6 | 前端:AI 用量统计页面 | P1, P3 | 1 文件 |
|
||||
| P7 | 前端:菜单注册 + 路由配置 | P4-P6 | 2 文件 |
|
||||
| P8 | 小程序:验证 taro-markdown 兼容性 + AI 报告列表/详情页 | P0c | 3 文件 |
|
||||
| P9 | 小程序:首页入口集成 | P8 | 1 文件 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 非目标(明确排除)
|
||||
|
||||
- 不涉及 CI/CD 流水线建设(属于安全与稳定性方向)
|
||||
- 不涉及 erp-plugin unwrap 修复(属于代码质量方向)
|
||||
- 不涉及 TypeScript strict 模式开启(属于质量方向,单独处理)
|
||||
- 不涉及新的 AI 提供商接入(仅使用现有 Claude 提供商)
|
||||
- 不涉及用量配额/计费功能(后续迭代)
|
||||
|
||||
---
|
||||
|
||||
## 7. 技术约束
|
||||
|
||||
- 前端组件使用 Ant Design 6 现有组件
|
||||
- 图表使用 Ant Design Charts(项目已有依赖)
|
||||
- 小程序 Markdown 渲染使用 taro-markdown 组件(P8 开始前验证兼容性)
|
||||
- 后端新增端点遵循现有 handler/service/entity 模式
|
||||
- 所有新页面使用 i18n key(前缀约定:`health.ai.*`),不硬编码中文
|
||||
- 权限数据加载失败时默认无权限(安全降级,宁可少显示按钮也不暴露越权操作)
|
||||
@@ -1,450 +0,0 @@
|
||||
# HMS 健康管理模块业务流程合理性分析
|
||||
|
||||
> 日期: 2026-04-25
|
||||
> 状态: Draft
|
||||
> 分析方法: 三专家组并行深度审查(临床业务 + 运营管理 + 产品架构)
|
||||
|
||||
---
|
||||
|
||||
## 执行摘要
|
||||
|
||||
HMS 健康管理模块已完成初始实现,包含 27 个数据库实体、14 个权限描述符(7 组 .list/.manage)、16 个 Web 页面、21 个小程序页面。本次分析从**临床业务流程、运营管理有效性、产品架构竞争力**三个维度进行深度审查。
|
||||
|
||||
### 核心结论
|
||||
|
||||
**工程基础设施达到生产级水准。** 多租户隔离、CAS 并发控制、乐观锁、事件驱动架构、数据加密(AES-256 + HMAC)、审计日志等能力一应俱全。
|
||||
|
||||
**业务深度存在三个核心缺口:**
|
||||
|
||||
| 缺口 | 影响 | 紧急度 |
|
||||
|------|------|--------|
|
||||
| 诊断编码缺失 | 随访/趋势分析/统计报表缺乏医学语义锚点,与 HIS/LIS 无法对接 | P0 |
|
||||
| 统计数据造假 | Dashboard 硬编码 0 值,管理者无法做运营决策 | P0 |
|
||||
| 消息推送未集成 | 设计规格定义 11 种事件,代码发布 9 种(缺失 `follow_up.overdue` 等),患者触达手段严重不足 | P0 |
|
||||
|
||||
### 发现总计
|
||||
|
||||
- **P0 关键问题**: 6 个(产品可信度 + 患者安全)
|
||||
- **P1 核心缺失**: 8 个(业务能力补全)
|
||||
- **P2 运营增强**: 7 个(差异化竞争力)
|
||||
- **P3 长期规划**: 7 个(市场准入 + 技术领先)
|
||||
|
||||
### 差异化竞争优势
|
||||
|
||||
1. **Rust 技术性能壁垒** — 高并发预约场景显著优势,单体二进制部署比竞品微服务架构简单一个数量级
|
||||
2. **血透专科深度** — 竞品中少有的专科化设计,补全方案/通路/充分性后更具竞争力
|
||||
3. **患者运营闭环** — 积分商城+签到+活动+资讯,医疗 SaaS 中罕见的互联网运营思维
|
||||
4. **多租户原生设计** — 从第一天内置隔离,竞品多为后期改造
|
||||
|
||||
---
|
||||
|
||||
## 1. 临床业务流程评估
|
||||
|
||||
> 专家角色: 资深临床医疗信息化专家(15 年医院信息系统设计经验)
|
||||
|
||||
### 1.1 患者全生命周期
|
||||
|
||||
**评分: 6/10 — "两头有、中间空"**
|
||||
|
||||
已覆盖的环节:
|
||||
- 建档阶段扎实:加密存储、标签分类、实名认证状态流转、家庭关系、医患关联
|
||||
- 健康摘要 API (`get_health_summary`) 聚合最新体征/化验/预约/随访,提供一站式概览
|
||||
|
||||
缺失的关键环节:
|
||||
|
||||
| 环节 | 说明 | 影响 |
|
||||
|------|------|------|
|
||||
| 分诊 (Triage) | 无主诉、分诊科室、紧急程度的记录 | 预约直接绑定医生,跳过分诊 |
|
||||
| 诊疗记录 (EMR) | `health_record` 仅含 `overall_assessment` + 文件 URL | 缺主诉、现病史、体格检查、诊断、处方 |
|
||||
| 诊断编码 (ICD) | 无 ICD-10/ICD-11 支持 | 随访/趋势分析缺乏医学语义锚点 |
|
||||
| 转诊流程 | 无科室间/院间转诊记录和追踪 | 多学科协作无法支撑 |
|
||||
| 入组/出组管理 | 只有标签这一非结构化方式 | 健康管理项目无法规范化 |
|
||||
|
||||
### 1.2 医疗数据管理
|
||||
|
||||
**评分: 5/10 — 基本可用但临床精细度不足**
|
||||
|
||||
优点:
|
||||
- 体征数据晨/晚血压区分符合慢病管理实践
|
||||
- 化验报告 V2 JSON 结构 `[{name, value, unit, reference_low, reference_high, is_abnormal}]` 灵活实用
|
||||
- 透析记录覆盖干体重、超滤量、血流量、透析类型 (HD/HDF/HF)
|
||||
|
||||
关键缺失:
|
||||
|
||||
| 问题 | 说明 |
|
||||
|------|------|
|
||||
| `vital_signs` 与 `daily_monitoring` 字段 91% 重叠 | 数据冗余 + 命名不一致(`systolic_bp_morning` vs `morning_bp_systolic`),趋势分析只查 `vital_signs` 表,`daily_monitoring` 数据完全被忽略 |
|
||||
| 缺乏采集时间精度 | 只有 `record_date`,血压需精确到分钟,血糖需标注空腹/餐后 |
|
||||
| 缺乏体温和 SpO2 | 透析感染监测和呼吸系统疾病管理的必备指标 |
|
||||
| 血糖无类型标记 | 空腹/餐后/随机/OGTT 混在一起,参考范围完全不同 |
|
||||
| 无用药记录 | 小程序有 `profile/medication` 页面但后端无对应实体 |
|
||||
| 化验指标未标准化 | "肌酐"/"CREA"/"Creatinine" 多种写法影响趋势分析 |
|
||||
| 出入量记录过于简单 | 仅有饮水量和尿量,临床需区分口服/静脉/引流/失血 |
|
||||
|
||||
### 1.3 预约排班
|
||||
|
||||
**评分: 7/10 — 核心流程优秀,运营辅助不足**
|
||||
|
||||
优点:
|
||||
- CAS 原子操作防超额预约,取消时自动释放名额,事务保证一致性
|
||||
- 状态机完整: pending → confirmed → completed/no_show/cancelled
|
||||
- 5 种预约类型覆盖主要场景(透析/复诊/门诊/体检/咨询)
|
||||
|
||||
缺失:
|
||||
- 无资源维度绑定(透析机位、检查室)
|
||||
- 无批量排班/排班模板(透析中心需周期性排班)
|
||||
- 无候补机制(号源满后直接拒绝)
|
||||
- no_show 无自动触发(定时任务缺失)
|
||||
- 排班不区分节假日
|
||||
|
||||
### 1.4 随访管理
|
||||
|
||||
**评分: 6/10 — 基础流程完整,临床实用性不足**
|
||||
|
||||
优点:
|
||||
- 逾期自动检查 (`check_overdue_tasks`) + 自动创建后续任务(`next_follow_up_date` 机制)
|
||||
- 乐观锁保护所有更新操作
|
||||
|
||||
缺失:
|
||||
- 无结构化随访模板(`content_template` 是纯文本)
|
||||
- 随访结果无结构化(`result`/`patient_condition`/`medical_advice` 均为自由文本)
|
||||
- 无随访到期提醒通知
|
||||
- 无随访方案模板(一次性生成 1/3/6/12 月任务组)
|
||||
- 无优先级字段
|
||||
- **前后端类型不一致**: 后端 `phone`/`face_to_face`/`online`,前端 `phone`/`outpatient`/`home_visit`/`wechat`
|
||||
|
||||
### 1.5 透析管理
|
||||
|
||||
**评分: 4/10 — "透析记录本"而非"透析管理系统"**
|
||||
|
||||
已实现: 透析记录覆盖核心物理参数(体重变化/血压/超滤/血流量),审阅流程 (draft → reviewed)。
|
||||
|
||||
三大核心支柱缺失:
|
||||
|
||||
| 支柱 | 说明 | 临床影响 |
|
||||
|------|------|---------|
|
||||
| 透析方案 (Prescription) | 无透析器型号、透析液配方、抗凝方案、目标超滤、血管通路类型 | 每次透析需重新输入相同参数 |
|
||||
| 血管通路管理 | 无通路类型/位置/评估/并发症/手术史 | 通路是透析患者的"生命线" |
|
||||
| 充分性评估 | 无 Kt/V 和 URR 计算 | 无法评估透析质量 |
|
||||
|
||||
其他缺失: 透析中动态监测、透析药物管理 (EPO/铁剂/磷结合剂)、症状/并发症未结构化。此外,entity 注释中定义了 `completed` 状态但无代码路径可达,存在"幽灵状态"问题。
|
||||
|
||||
### 1.6 咨询管理
|
||||
|
||||
**评分: 5/10 — 适合"留言板",无法支撑"在线问诊"**
|
||||
|
||||
优点: 消息模型合理(text/image/voice/file 四种类型、CAS 未读计数、会话状态机 waiting → active → closed)。
|
||||
|
||||
缺失:
|
||||
- **无实时推送**: 只有 HTTP API,无 WebSocket/SSE
|
||||
- 消息已读标记 API 缺失
|
||||
- 无会话超时自动关闭
|
||||
- 无满意度评价
|
||||
- 无智能分配机制(轮转/科室匹配)
|
||||
- 无关联健康数据展示
|
||||
|
||||
### 1.7 临床决策支持
|
||||
|
||||
**评分: 3/10 — "概念验证"阶段**
|
||||
|
||||
已实现: 趋势分析框架、异常值基本检测(心率 60-100、血糖 3.9-11.1、血压 90-140/60-90)、化验 `is_abnormal` 标记。
|
||||
|
||||
关键问题:
|
||||
- **阈值硬编码** — 不考虑患者个体差异(老年/糖尿病/透析/儿童不同目标)
|
||||
- **血糖阈值不一致** — 趋势分析用 3.9-11.1,小程序摘要用 3.9-6.1
|
||||
- **无实时预警** — 趋势分析是手动触发,血压 180/110 不会自动报警
|
||||
- **不整合化验数据** — 肌酐/eGFR/血红蛋白/电解质趋势同样重要
|
||||
- **无风险评分** — Framingham/KDOQI/MNA 等临床评分模型完全缺失
|
||||
|
||||
---
|
||||
|
||||
## 2. 运营管理评估
|
||||
|
||||
> 专家角色: 资深健康管理运营专家(10+ 年体检中心/健康管理机构运营经验)
|
||||
|
||||
### 2.1 积分激励体系
|
||||
|
||||
**评分: 6/10 — 架构优秀,激励有效性不足**
|
||||
|
||||
优点:
|
||||
- 事件驱动积分发放,`daily_cap` 每日上限,FIFO 消费模型,12 个月过期
|
||||
- 连续签到阶梯奖励 (7/14/30 天)
|
||||
- 全链路 CAS 并发安全
|
||||
|
||||
关键问题:
|
||||
|
||||
| 问题 | 影响 |
|
||||
|------|------|
|
||||
| 事件类型有限 | 只有 6 种行为获积分,缺少 `vital_signs_input`/`annual_checkup`/`followup_adherence` 等真正驱动健康行为的激励点 |
|
||||
| 积分通胀无控制 | 无发放/消费比率监控、无月度预算上限 |
|
||||
| 积分过期未清理 | `expires_at` 字段写入后无定时检查,`total_expired` 永远为 0 |
|
||||
| 订单过期未取消 | 兑换订单 30 天过期但无定时任务退还积分 |
|
||||
| 无过期提醒 | 患者无法得知积分即将过期 |
|
||||
| 无补签机制 | 中断一天即重置,缺少积分兑换补签卡 |
|
||||
|
||||
### 2.2 患者参与度
|
||||
|
||||
**评分: 4/10 — 触达手段严重不足**
|
||||
|
||||
已有渠道: 每日打卡、积分商城、线下活动、咨询管理、随访任务、体征上报。
|
||||
|
||||
缺失的关键互动:
|
||||
|
||||
| 互动方式 | 状态 | 影响 |
|
||||
|----------|------|------|
|
||||
| 消息推送 (Push) | 未集成 erp-message | 随访/积分/报告/预约提醒全部缺失 |
|
||||
| 健康目标与挑战 | 无 | 无法设定"30 天降压"目标,无社区排行 |
|
||||
| 成就体系 (Badge) | 无 | 无"连续打卡 30 天"等徽章激励 |
|
||||
| 社交互动 | 无 | 家庭成员表仅记录信息,无家庭健康管理联动 |
|
||||
| 内容运营 | 基础 | 文章有 CRUD 但无推荐/阅读积分/分享积分 |
|
||||
|
||||
### 2.3 随访管理运营
|
||||
|
||||
**评分: 5/10 — 单条操作效率低**
|
||||
|
||||
**核心问题: 不支持批量操作。** 实际体检中心一个护士每天要处理 30-50 个随访任务,逐个操作效率极低。
|
||||
|
||||
缺失:
|
||||
- 批量创建(按患者标签/疾病类型)
|
||||
- 批量分配负责人
|
||||
- 批量标记完成
|
||||
- 随访工作量统计(按护士维度的完成率/响应时间)
|
||||
- 随访计划模板(按疾病类型的标准化方案)
|
||||
|
||||
### 2.4 健康干预闭环
|
||||
|
||||
**评分: 4/10 — 闭环完整度约 40%**
|
||||
|
||||
```
|
||||
数据采集 → 异常识别 → 干预建议 → 执行跟踪 → 效果评估
|
||||
80% 20% 0% 30% 0%
|
||||
```
|
||||
|
||||
- 数据采集基本完整(体征/化验/透析)
|
||||
- 异常识别仅有简单阈值判断,无实时预警
|
||||
- 干预建议完全缺失(`medical_advice` 是自由文本,无标准化方案库)
|
||||
- 执行跟踪仅有随访链式创建
|
||||
- 效果评估完全缺失(无 Health Score、无干预前后对比)
|
||||
|
||||
### 2.5 数据分析与报表
|
||||
|
||||
**评分: 2/10 — 不可用于运营决策**
|
||||
|
||||
**严重问题: Dashboard 数据大部分是假的。**
|
||||
|
||||
| 指标 | 实现方式 | 可信度 |
|
||||
|------|---------|--------|
|
||||
| 患者总数 | `list?page_size=1` 取 total | 部分 |
|
||||
| 本月新增 | 硬编码 `0` | 不可信 |
|
||||
| 本周新增 | 硬编码 `0` | 不可信 |
|
||||
| 本月活跃 | 硬编码 `0` | 不可信 |
|
||||
| 随访完成率 | 硬编码 `0` | 不可信 |
|
||||
| 咨询总量 | `list?page_size=1` 取 total | 部分 |
|
||||
| 积分统计 | SQL 聚合 | 可信 |
|
||||
| 积分排行 | SQL 排序 | 可信 |
|
||||
|
||||
缺失的关键运营指标: 患者覆盖率、随访完成率趋势、积分使用率、活动参与率、患者留存率、医生工作量分布。无时间维度分析,无数据导出能力。
|
||||
|
||||
### 2.6 多角色协作
|
||||
|
||||
**评分: 5/10 — 职责边界模糊**
|
||||
|
||||
| 问题 | 说明 |
|
||||
|------|------|
|
||||
| 护士/健康管理师无独立 profile | 随访 `assigned_to` 指向医生选择器,但护士才是随访主力 |
|
||||
| 职责边界不清 | `patient_doctor_relation` 只有 `primary`/`consulting`,无"健康管理师"角色 |
|
||||
| 前台核销不流畅 | 需手动输入 UUID,无扫码枪集成 |
|
||||
| 无转诊协作机制 | 无医生间转诊流程,无 MDT 任务分配 |
|
||||
| 无智能工作负载分配 | 随访任务完全手动分配 |
|
||||
|
||||
### 2.7 商业变现能力
|
||||
|
||||
**评分: 4/10 — 基础模型在,变现工具缺失**
|
||||
|
||||
积分商城当前只有获取+兑换的基础闭环。缺失:
|
||||
- 积分充值通道(现金购买)
|
||||
- 会员等级体系 (VIP/SVIP)
|
||||
- 营销工具(优惠券/限时活动/邀请有礼/满减)
|
||||
- 供应商管理(商品采购成本/物流)
|
||||
- 数据分析(商品热度/用户分层 RFM)
|
||||
|
||||
---
|
||||
|
||||
## 3. 产品架构评估
|
||||
|
||||
> 专家角色: 资深医疗 SaaS 产品架构师(10+ 年医疗信息化产品设计经验)
|
||||
|
||||
### 3.1 模块边界与耦合
|
||||
|
||||
**评分: 8/10 — 边界清晰,事件契约有进步空间**
|
||||
|
||||
优点:
|
||||
- 对 erp-core 仅依赖共享类型(AppError/EventBus/PaginatedResponse)
|
||||
- 对 erp-auth 通过 `user_id` 外键松耦合,`Option<Uuid>` 允许患者先建档后绑定
|
||||
- 声明 `dependencies() = vec!["auth"]` 语义明确
|
||||
|
||||
关键问题:
|
||||
- **事件发布不完整**: 设计规格定义 11 种事件,代码实际发布 9 种(含 2 种设计规格外的: `doctor.online_status_changed`/`lab_report.uploaded`)。缺失 `follow_up.overdue` 等关键事件
|
||||
- **message.sent 订阅为空操作**: 只打 `tracing::info`,但 `consultation_session.last_message_at` 已在 `create_message` 方法中通过 CAS 直接更新,此事件订阅是预留扩展点而非功能缺失
|
||||
- 缺少密钥轮换机制
|
||||
|
||||
### 3.2 数据模型完整性
|
||||
|
||||
**评分: 6/10 — 覆盖核心域,存在医疗级缺口**
|
||||
|
||||
已覆盖(27 实体): 患者管理(完整)、医护管理(基本完整)、健康数据(完整)、预约排班(完整)、随访管理(完整)、咨询管理(完整)、专科血透(扩展完整)、患者运营(超预期)。
|
||||
|
||||
缺失的核心实体(按重要性排序):
|
||||
|
||||
| 实体 | 优先级 | 理由 |
|
||||
|------|--------|------|
|
||||
| 诊断记录 (Diagnosis) | P0 | ICD-10 编码,所有临床活动的锚点 |
|
||||
| 用药记录 (Medication) | P1 | 慢病管理核心数据,小程序页面已存在 |
|
||||
| 处方/医嘱 (Prescription) | P1 | 结构化处方是慢病管理核心 |
|
||||
| 健康计划 (Care Plan) | P2 | 从"数据记录"到"主动干预"的关键 |
|
||||
| 过敏详细记录 (Allergy) | P2 | 药物过敏交叉检查需要结构化数据 |
|
||||
|
||||
### 3.3 API 设计质量
|
||||
|
||||
**评分: 7/10 — RESTful 规范,高级特性缺失**
|
||||
|
||||
优点: 统一分页 `PaginatedResponse<T>`、乐观锁 `DeleteWithVersion`、状态机校验、管理端/患者端路由分离。
|
||||
|
||||
不足:
|
||||
- 无批量操作 API
|
||||
- 排序参数未暴露(硬编码 `order_by_desc(CreatedAt)`)
|
||||
- 日期范围过滤缺失(预约只有单日过滤)
|
||||
- 统计 API 不专业(用 `list?page_size=1` 模拟)
|
||||
- 咨询无实时机制
|
||||
- 导出功能单一
|
||||
|
||||
### 3.4 前端架构
|
||||
|
||||
**评分: 7/10 — 组件化程度高,全局状态管理缺失**
|
||||
|
||||
Web 前端优点: API 层按领域拆分(8 个 TS 文件)、12 个共享组件、Tab 化详情页、主题适配。
|
||||
|
||||
不足:
|
||||
- 无 Zustand 全局状态(健康模块共享数据如当前患者、名称缓存无法跨页面)
|
||||
- StatisticsDashboard 数据质量低
|
||||
- 日期处理类型不够安全
|
||||
|
||||
小程序优点: 27 页面覆盖全面、服务层与 Web 端一一对应。
|
||||
|
||||
不足: 无离线缓存策略、无统一错误处理/重试。
|
||||
|
||||
### 3.5 多租户适配
|
||||
|
||||
**评分: 8/10 — 基础隔离完整,差异化配置不足**
|
||||
|
||||
已实现: 全实体 `tenant_id` 过滤、租户生命周期钩子(seed/soft_delete)、AES-256-GCM 加密 + HMAC 索引、数据脱敏、行级数据权限。
|
||||
|
||||
缺失: 不同医疗机构类型(体检中心/社区中心/血透中心/专科诊所)的差异化配置能力。`patient` 模型是"大一统"设计,无法自定义采集项、评估量表、报告模板。
|
||||
|
||||
### 3.6 扩展性与可配置性
|
||||
|
||||
**评分: 5/10 — 架构级良好,业务层不足**
|
||||
|
||||
架构级(良好): ErpModule trait 注册、EventBus 解耦、WASM 插件保留、SeaORM Entity + Migration 扩展。
|
||||
|
||||
业务层(不足): 无自定义表单(EAV 或 JSONB + Schema)、无自定义工作流模板、无报告模板。积分规则和标签系统是系统中少有的可配置业务模块。
|
||||
|
||||
### 3.7 竞品对比
|
||||
|
||||
| 维度 | HMS | 杏树林 | 微医 | 平安好医生 |
|
||||
|------|-----|--------|------|-----------|
|
||||
| 技术性能 | 极高 (Rust) | 中等 (Java) | 中等 | 中等 |
|
||||
| 多租户 | 原生支持 | 后期改造 | 有限 | 有限 |
|
||||
| 患者运营 | 有(积分+商城) | 无 | 无 | 有 |
|
||||
| 血透专科 | 有(待完善) | 无 | 无 | 无 |
|
||||
| AI 能力 | 开发中 | 成熟 | 部分 | 成熟 |
|
||||
| 实时通讯 | 无 | 音视频 | 音视频 | 音视频 |
|
||||
| 医疗标准 | 无 | 有 (ICD) | 有 | 有 |
|
||||
| 影像管理 | 无 | 有 | 有 | 无 |
|
||||
| 合规认证 | 无 | 等保三级 | 等保三级 | 等保三级 |
|
||||
|
||||
**核心差距**: AI 能力、实时通讯、医疗数据标准、影像管理、合规认证。
|
||||
|
||||
---
|
||||
|
||||
## 4. 综合改进路线图
|
||||
|
||||
### Phase 1: 产品可信度修复 (P0, 2-3 人天)
|
||||
|
||||
> 影响: 管理者无法决策、患者安全风险、跨模块联动断裂
|
||||
|
||||
| # | 改进项 | 涉及文件 | 复杂度 |
|
||||
|---|--------|---------|--------|
|
||||
| 1 | 修复 Dashboard 统计数据 | `points.ts` + `points_service.rs` + `StatisticsDashboard.tsx` | 中 |
|
||||
| 2 | 补全事件发布(至少 `follow_up.overdue`) | `event.rs` + `follow_up_service.rs` | 中 |
|
||||
| 3 | 合并 vital_signs 和 daily_monitoring | entity + service + DTO + migration | 中 |
|
||||
| 4 | 增加实时异常预警 | `health_data_service.rs` + `trend_service.rs` | 中 |
|
||||
| 5 | 增加 ICD-10 诊断编码支持 | 新建 entity + migration + service | 中 |
|
||||
| 6 | 实现积分过期清理定时任务 | `points_service.rs` + `module.rs` | 低 |
|
||||
|
||||
### Phase 2: 核心业务能力补全 (P1, 5-7 人天)
|
||||
|
||||
> 影响: 临床实用性不足、患者参与度低、运营效率差
|
||||
|
||||
| # | 改进项 | 复杂度 |
|
||||
|---|--------|--------|
|
||||
| 7 | 结构化随访模板系统 | 高 |
|
||||
| 8 | 用药记录实体 | 中 |
|
||||
| 9 | 透析方案管理 | 中 |
|
||||
| 10 | 体征增加体温/SpO2/血糖类型 | 低 |
|
||||
| 11 | 消息推送集成 | 中 |
|
||||
| 12 | 批量随访操作 | 中 |
|
||||
| 13 | 修复随访类型前后端不一致 | 低 |
|
||||
| 14 | 咨询 WebSocket 实时推送 | 高 |
|
||||
|
||||
### Phase 3: 运营增强 (P2, 5-7 人天)
|
||||
|
||||
> 影响: 差异化竞争力、患者留存、商业变现
|
||||
|
||||
| # | 改进项 | 复杂度 |
|
||||
|---|--------|--------|
|
||||
| 15 | 患者健康评分体系 (Health Score) | 中 |
|
||||
| 16 | 会员等级和营销工具 | 中 |
|
||||
| 17 | 预约资源绑定 | 中 |
|
||||
| 18 | 个性化异常阈值配置 | 中 |
|
||||
| 19 | 化验指标标准化字典 (LOINC) | 中 |
|
||||
| 20 | 批量排班/排班模板 | 中 |
|
||||
| 21 | 小程序分析埋点后端 | 低 |
|
||||
|
||||
### Phase 4: 长期竞争力 (P3, 路线图)
|
||||
|
||||
| # | 改进项 |
|
||||
|---|--------|
|
||||
| 22 | 血管通路管理 |
|
||||
| 23 | 疾病风险评分模型 |
|
||||
| 24 | AI 辅助诊断 (erp-ai 集成) |
|
||||
| 25 | 可配置表单能力 |
|
||||
| 26 | 影像管理集成 (DICOM/PACS) |
|
||||
| 27 | 合规认证 (等保三级) |
|
||||
| 28 | HL7 FHIR R4 数据互操作 |
|
||||
|
||||
---
|
||||
|
||||
## 附录 A: 与 QA 审计计划的交叉引用
|
||||
|
||||
本分析与 [QA 审计计划](../../plans/qa-review-brainstorm-floofy-finch.md) 的发现高度重叠,以下是交叉对照:
|
||||
|
||||
| QA 审计编号 | 业务分析对应 | 状态 |
|
||||
|------------|-------------|------|
|
||||
| 1.1 逾期随访检查器未启动 | §1.4 随访管理 — 逾期自动检查已实现但需修复 | 需修复 |
|
||||
| 1.2 积分并发余额损坏 | §2.1 积分激励体系 — CAS 并发安全 | 需修复 |
|
||||
| 2.4 HealthDataProvider 全 stub | §1.7 临床决策支持 — AI 集成依赖 | 需决策 |
|
||||
| 2.6 小程序 DTO 不匹配 | §1.2 医疗数据管理 — 数据模型对齐 | 需修复 |
|
||||
| 3.1 咨询列表显示截断 UUID | §3.4 前端架构 — 名称缓存 | 已修复 |
|
||||
| 3.2 积分订单列表显示 UUID | §3.4 前端架构 — 需名称解析 | 待修复 |
|
||||
| 3.4 预约状态变更无确认 | §1.3 预约排班 — 已在 AppointmentList 中实现 | 已修复 |
|
||||
|
||||
## 附录 B: 工作量估算
|
||||
|
||||
| 阶段 | 人天 | 优先级 |
|
||||
|------|------|--------|
|
||||
| Phase 1: P0 可信度修复 | 2-3 | 立即 |
|
||||
| Phase 2: P1 核心能力 | 5-7 | 本迭代 |
|
||||
| Phase 3: P2 运营增强 | 5-7 | 下迭代 |
|
||||
| Phase 4: P3 长期竞争力 | 路线图 | Q3-Q4 |
|
||||
| **合计** | **12-17 + 路线图** | |
|
||||
@@ -1,670 +0,0 @@
|
||||
# HMS V2 迭代设计 — 血透专科健康管理平台
|
||||
|
||||
> **日期**: 2026-04-25
|
||||
> **状态**: 待评审
|
||||
> **前置文档**: `2026-04-23-health-management-module-design.md`, `2026-04-24-health-module-iteration-design.md`
|
||||
> **需求来源**: 客户功能需求文档 `docs/健康管理/管理系统功能文档(1).xlsx`
|
||||
|
||||
---
|
||||
|
||||
## 1. 背景与动机
|
||||
|
||||
### 1.1 业务定位变化
|
||||
|
||||
V1 的健康管理模块定位为**通用健康管理平台**,覆盖患者管理、健康数据、预约排班、随访管理、咨询管理五大功能。
|
||||
|
||||
客户反馈后,明确业务定位为**血透(透析)专科健康管理平台**,面向肾病/透析患者和医护群体。这带来三个重大变化:
|
||||
|
||||
1. **数据模型专科化** — 从通用健康指标(血压/心率/血糖/体重/体温)扩展为血透专科指标(透析记录、化验报告、日常监测)
|
||||
2. **新增积分商城** — 替代原计划的完整电商,用积分体系驱动患者活跃度
|
||||
3. **新增医护端小程序** — 独立入口,医护可查看患者数据、填写透析记录、回复咨询
|
||||
|
||||
### 1.2 客户需求来源
|
||||
|
||||
客户通过 Excel 功能文档定义了三端需求:
|
||||
|
||||
| 端 | 核心功能 |
|
||||
|---|---|
|
||||
| 患者端小程序 | 首页、数据上报(透析/化验/日常)、在线咨询、积分商城、预约与随访、个人中心 |
|
||||
| 医护端小程序 | 数据概览、患者管理、咨询回复、随访管理、报告解读 |
|
||||
| PC 管理后台 | 系统管理、患者管理、健康数据中心、咨询管理、商城管理、内容管理、统计报表、系统设置 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 需求-实现差距分析
|
||||
|
||||
### 2.1 已有功能匹配度
|
||||
|
||||
| 客户需求 | 已有实现 | 匹配度 | 需要的工作 |
|
||||
|---------|---------|--------|-----------|
|
||||
| 患者列表/档案 | Patient CRUD + 18 实体 | 90% | 微调 |
|
||||
| 在线咨询(图文) | Consultation 模块 | 70% | 加语音/客服通道 |
|
||||
| 预约管理 | Appointment 模块 | 60% | 加透析专项预约 |
|
||||
| 随访任务/台账 | Follow-up 模块 | 80% | 微调 |
|
||||
| 科普文章 | Article 模块 | 90% | 微调 |
|
||||
| 医护管理 | Doctor 模块 | 80% | 微调 |
|
||||
| 账号权限/操作日志 | erp-auth RBAC | 95% | 基本不变 |
|
||||
| 患者标签 | Tag 模块 | 90% | 微调 |
|
||||
| 小程序健康数据 | 6 种指标录入 + ECharts 趋势图 | 80% | 扩展指标 + 透析数据 |
|
||||
|
||||
### 2.2 全新模块
|
||||
|
||||
| 模块 | 说明 | 复杂度 |
|
||||
|------|------|--------|
|
||||
| 血透透析记录 | 透析日期/时间、干体重、透前/后血压、心率、超滤量、症状 | 中 |
|
||||
| 日常监测扩展 | 饮水量、尿量 + 打卡模式 | 低 |
|
||||
| 化验报告上传 | 拍照上传 + 指标录入 | 中 |
|
||||
| 积分商城 | 积分获取、商品兑换、二维码核销 | 高 |
|
||||
| 线下活动 | 活动管理、报名、扫码签到 | 中 |
|
||||
| 在线咨询(IM) | 客服通道、医生通道、图文/语音消息 | 高 |
|
||||
| 医护端小程序 | 独立小程序,医护专属功能 | 高 |
|
||||
| 统计报表中心 | 患者增长、咨询量、随访完成率、商城销售 | 中-高 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 积分商城设计
|
||||
|
||||
### 3.1 核心链路
|
||||
|
||||
**赚积分 → 攒积分 → 花积分 → 核销**
|
||||
|
||||
### 3.2 实现方式
|
||||
|
||||
放在 `erp-health` 原生模块内,直接复用现有患者体系和事件机制。
|
||||
|
||||
### 3.3 积分获取渠道(数据库可配置)
|
||||
|
||||
| 渠道 | 积分 | 说明 |
|
||||
|------|------|------|
|
||||
| 每日健康打卡 | 可配置/天 | 完成日常监测数据填写触发 |
|
||||
| 数据上报 | 可配置/次 | 上传化验单、填透析记录 |
|
||||
| 线下活动签到 | 活动配置 | 到院参加讲座/义诊等 |
|
||||
| 连续打卡奖励 | 阶梯配置 | 连续 7/14/30 天额外加分 |
|
||||
| 医生互动 | 可配置/次 | 完成一次咨询、回复随访问卷 |
|
||||
|
||||
积分获取规则通过 `points_rule` 表配置,管理员可在 PC 端调整积分值、每日上限、连续奖励等参数。
|
||||
|
||||
### 3.4 兑换品类
|
||||
|
||||
| 类型 | 兑换物 | 履约方式 |
|
||||
|------|--------|---------|
|
||||
| 实物 | 血透护理用品、肾病食品、慢病器械 | 到院自提(二维码核销) |
|
||||
| 服务券 | 免费抽血、肾功能检查、营养咨询 | 生成预约券 → 线下二维码核销 |
|
||||
| 权益 | 免费停车券、优先预约权 | 虚拟权益即时生效 |
|
||||
|
||||
### 3.5 核销方式
|
||||
|
||||
用户兑换后生成二维码(UUID),到院后工作人员扫码核销。
|
||||
|
||||
服务券类型核销后可自动关联预约系统创建预约。
|
||||
|
||||
### 3.6 积分过期机制
|
||||
|
||||
**滚动 12 个月过期,FIFO 先进先出结算。**
|
||||
|
||||
- 每笔积分(earn)有独立 `expires_at`(创建时间 + 12 个月)
|
||||
- 每日后台任务扫描并标记过期积分
|
||||
- 消费时从最老的未过期积分开始扣减
|
||||
- 每笔积分支持部分消费(`remaining_amount` 字段)
|
||||
|
||||
### 3.7 数据模型(8 张新表)
|
||||
|
||||
#### points_account(积分账户)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | UUID v7 | PK |
|
||||
| patient_id | UUID | FK → patient,唯一约束 |
|
||||
| balance | i32 | 当前可用积分 |
|
||||
| total_earned | i32 | 累计获得 |
|
||||
| total_spent | i32 | 累计消耗 |
|
||||
| total_expired | i32 | 累计过期 |
|
||||
| version | i32 | 乐观锁 |
|
||||
| tenant_id | UUID | 租户 |
|
||||
| created_at / updated_at / created_by / updated_by / deleted_at | — | 标准字段 |
|
||||
|
||||
#### points_rule(积分规则,数据库可配置)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | UUID v7 | PK |
|
||||
| event_type | String | 触发事件:daily_checkin / data_report / lab_upload / event_checkin / consultation_complete / followup_complete |
|
||||
| name | String | 规则名称(展示用) |
|
||||
| description | String | 规则描述 |
|
||||
| points_value | i32 | 单次获得积分 |
|
||||
| daily_cap | i32 | 每日上限(0 = 无限制) |
|
||||
| streak_7d_bonus | i32 | 连续 7 天额外奖励 |
|
||||
| streak_14d_bonus | i32 | 连续 14 天额外奖励 |
|
||||
| streak_30d_bonus | i32 | 连续 30 天额外奖励 |
|
||||
| is_active | bool | 是否启用 |
|
||||
| tenant_id | UUID | 租户 |
|
||||
| 标准字段 | — | — |
|
||||
|
||||
#### points_transaction(积分流水,FIFO 桶模型)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | UUID v7 | PK |
|
||||
| account_id | UUID | FK → points_account |
|
||||
| type | Enum | earn / spend / expired / refund |
|
||||
| amount | i32 | 正数=获得,负数=消耗 |
|
||||
| remaining_amount | i32 | 该笔积分剩余可用量(earn 类型) |
|
||||
| status | Enum | active / expired / consumed |
|
||||
| expires_at | DateTime | 过期时间(earn 类型:创建 + 12 个月) |
|
||||
| balance_after | i32 | 操作后账户余额快照 |
|
||||
| rule_id | UUID | FK → points_rule(earn 类型) |
|
||||
| order_id | UUID | FK → points_order(spend 类型,可空) |
|
||||
| description | String | 流水描述 |
|
||||
| tenant_id | UUID | 租户 |
|
||||
| 标准字段 | — | — |
|
||||
|
||||
#### points_product(兑换商品)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | UUID v7 | PK |
|
||||
| name | String | 商品名称 |
|
||||
| type | Enum | physical / service / privilege |
|
||||
| points_cost | i32 | 兑换所需积分 |
|
||||
| stock | i32 | 库存数量(-1 = 无限) |
|
||||
| image_url | String | 商品图片 |
|
||||
| description | Text | 商品描述 |
|
||||
| service_config | JSON | 服务类型配置(service 类型:关联检查项目、有效期等) |
|
||||
| is_active | bool | 是否上架 |
|
||||
| sort_order | i32 | 排序权重 |
|
||||
| tenant_id | UUID | 租户 |
|
||||
| 标准字段 | — | — |
|
||||
|
||||
#### points_order(兑换订单)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | UUID v7 | PK |
|
||||
| patient_id | UUID | FK → patient |
|
||||
| product_id | UUID | FK → points_product |
|
||||
| points_cost | i32 | 消耗积分(冗余,防商品价格变化) |
|
||||
| status | Enum | pending / verified / cancelled / expired |
|
||||
| qr_code | UUID | 核销二维码(UUID v4) |
|
||||
| verified_by | UUID | 核销人 FK → user |
|
||||
| verified_at | DateTime | 核销时间 |
|
||||
| expires_at | DateTime | 订单过期时间(实物/服务券有效期) |
|
||||
| notes | String | 备注 |
|
||||
| tenant_id | UUID | 租户 |
|
||||
| 标准字段 | — | — |
|
||||
|
||||
#### points_checkin(每日打卡)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | UUID v7 | PK |
|
||||
| patient_id | UUID | FK → patient |
|
||||
| checkin_date | Date | 打卡日期 |
|
||||
| consecutive_days | i32 | 连续打卡天数(计算值) |
|
||||
| tenant_id | UUID | 租户 |
|
||||
| created_at | DateTime | — |
|
||||
|
||||
唯一约束:(patient_id, checkin_date)
|
||||
|
||||
#### offline_event(线下活动)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | UUID v7 | PK |
|
||||
| title | String | 活动标题 |
|
||||
| description | Text | 活动描述 |
|
||||
| event_date | Date | 活动日期 |
|
||||
| start_time / end_time | Time | 活动时间 |
|
||||
| location | String | 活动地点 |
|
||||
| points_reward | i32 | 参与奖励积分 |
|
||||
| max_participants | i32 | 最大参与人数(0 = 无限制) |
|
||||
| current_participants | i32 | 已报名人数 |
|
||||
| status | Enum | draft / published / ongoing / completed / cancelled |
|
||||
| image_url | String | 活动封面图 |
|
||||
| tenant_id | UUID | 租户 |
|
||||
| 标准字段 | — | — |
|
||||
|
||||
#### offline_event_registration(活动报名 + 签到)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | UUID v7 | PK |
|
||||
| event_id | UUID | FK → offline_event |
|
||||
| patient_id | UUID | FK → patient |
|
||||
| status | Enum | registered / checked_in / cancelled |
|
||||
| checked_in_at | DateTime | 签到时间 |
|
||||
| checked_in_by | UUID | 签到确认人 FK → user |
|
||||
| points_granted | bool | 是否已发放积分 |
|
||||
| tenant_id | UUID | 租户 |
|
||||
| 标准字段 | — | — |
|
||||
|
||||
唯一约束:(event_id, patient_id)
|
||||
|
||||
### 3.8 积分获取链路
|
||||
|
||||
```
|
||||
健康打卡 → 触发事件 → points_rule 匹配规则 → 写入 points_transaction → 更新 points_account.balance
|
||||
数据上报 → 同上 ↓
|
||||
咨询完成 → 同上 连续打卡检查
|
||||
线下签到 → 同上 → 达到 7/14/30 天?
|
||||
→ 额外 bonus transaction
|
||||
```
|
||||
|
||||
### 3.9 兑换核销链路
|
||||
|
||||
```
|
||||
用户浏览商品 → 兑换(扣积分)→ 生成 points_order + QR(UUID v4)
|
||||
↓
|
||||
用户到院出示二维码
|
||||
↓
|
||||
工作人员扫码(小程序/PC)→ 状态变 verified
|
||||
↓
|
||||
type=service → 自动创建预约券
|
||||
type=physical → 库存扣减
|
||||
```
|
||||
|
||||
### 3.10 后台任务
|
||||
|
||||
| 任务 | 频率 | 说明 |
|
||||
|------|------|------|
|
||||
| 积分过期扫描 | 每日 | 扫描 expires_at < now 的 earn 记录,标记 expired,扣减 balance |
|
||||
| 订单过期扫描 | 每日 | 扫描未核销且过期的订单,标记 expired,退还积分 |
|
||||
| 连续打卡计算 | 每日 | 计算各患者连续打卡天数,触发阶梯奖励 |
|
||||
|
||||
### 3.11 API 端点
|
||||
|
||||
**患者端:**
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | /api/v1/points/account | 查看我的积分账户 |
|
||||
| POST | /api/v1/points/checkin | 每日打卡 |
|
||||
| GET | /api/v1/points/checkin/status | 打卡状态(连续天数等) |
|
||||
| GET | /api/v1/points/transactions | 积分流水(分页) |
|
||||
| GET | /api/v1/points/products | 商品列表(分页、按类型筛选) |
|
||||
| GET | /api/v1/points/products/{id} | 商品详情 |
|
||||
| POST | /api/v1/points/exchange | 兑换商品 |
|
||||
| GET | /api/v1/points/orders | 我的兑换订单 |
|
||||
| GET | /api/v1/points/orders/{id} | 订单详情(含二维码) |
|
||||
| GET | /api/v1/offline-events | 线下活动列表 |
|
||||
| GET | /api/v1/offline-events/{id} | 活动详情 |
|
||||
| POST | /api/v1/offline-events/{id}/register | 报名活动 |
|
||||
|
||||
**医护/管理员端:**
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| POST | /api/v1/points/verify | 扫码核销 |
|
||||
| CRUD | /api/v1/admin/points/rules | 积分规则管理 |
|
||||
| CRUD | /api/v1/admin/points/products | 商品管理 |
|
||||
| GET | /api/v1/admin/points/orders | 订单管理(导出) |
|
||||
| GET | /api/v1/admin/points/statistics | 积分统计 |
|
||||
| CRUD | /api/v1/admin/offline-events | 线下活动管理 |
|
||||
| POST | /api/v1/admin/offline-events/{id}/checkin | 活动扫码签到 |
|
||||
|
||||
### 3.12 PC 管理后台新增页面
|
||||
|
||||
| 页面 | 功能 |
|
||||
|------|------|
|
||||
| 积分规则管理 | 增删改规则、启用/禁用、调整积分值和上限 |
|
||||
| 商品管理 | 增删改兑换品、设置库存、上传图片、设置积分价格 |
|
||||
| 订单管理 | 查看兑换记录、手动核销、导出 |
|
||||
| 线下活动管理 | 创建活动、设置积分奖励、查看报名/签到名单 |
|
||||
| 积分统计 | 总发放/总消耗/活跃用户排行、积分流水查询 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 血透专科数据模型
|
||||
|
||||
### 4.1 设计决策
|
||||
|
||||
- **独立实体表**:不复用现有 health_data 键值对结构,每类数据一张独立表
|
||||
- **医护+患者协同上报**:透析记录由医护填写,化验报告由患者上传/医护审阅,日常监测由患者填写
|
||||
|
||||
### 4.2 权限矩阵
|
||||
|
||||
| 数据 | 患者端 | 医护端 | PC 管理后台 |
|
||||
|------|--------|--------|------------|
|
||||
| 透析记录 | 查看(只读) | 创建/编辑 | 查看/导出/统计 |
|
||||
| 化验报告 | 上传照片/查看 | 审阅/标注/解读 | 查看/导出 |
|
||||
| 日常监测 | 创建/查看 | 查看/预警 | 查看/统计 |
|
||||
| AI 健康报告 | 查看 | 查看/编辑 | 查看/导出 |
|
||||
|
||||
### 4.3 新增实体
|
||||
|
||||
#### dialysis_record(透析记录)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | UUID v7 | PK |
|
||||
| patient_id | UUID | FK → patient |
|
||||
| dialysis_date | Date | 透析日期 |
|
||||
| start_time | Time | 开始时间 |
|
||||
| end_time | Time | 结束时间 |
|
||||
| dry_weight | Decimal(5,1) | 干体重 (kg) |
|
||||
| pre_weight | Decimal(5,1) | 透前体重 (kg) |
|
||||
| post_weight | Decimal(5,1) | 透后体重 (kg) |
|
||||
| pre_bp_systolic | i32 | 透前收缩压 |
|
||||
| pre_bp_diastolic | i32 | 透前舒张压 |
|
||||
| post_bp_systolic | i32 | 透后收缩压 |
|
||||
| post_bp_diastolic | i32 | 透后舒张压 |
|
||||
| pre_heart_rate | i32 | 透前心率 |
|
||||
| post_heart_rate | i32 | 透后心率 |
|
||||
| ultrafiltration_volume | i32 | 超滤量 (ml) |
|
||||
| dialysis_duration | i32 | 透析时长 (min) |
|
||||
| blood_flow_rate | i32 | 血流量 (ml/min) |
|
||||
| dialysis_type | Enum | HD / HDF / HF |
|
||||
| symptoms | JSON | 不适症状数组 ["低血压","恶心","抽筋"] |
|
||||
| complication_notes | Text | 并发症备注 |
|
||||
| status | Enum | draft / completed / reviewed |
|
||||
| reviewed_by | UUID | FK → user,审阅医生 |
|
||||
| reviewed_at | DateTime | 审阅时间 |
|
||||
| tenant_id | UUID | 租户 |
|
||||
| 标准字段 | — | created_at / updated_at / created_by / updated_by / deleted_at / version |
|
||||
|
||||
#### lab_report(化验报告)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | UUID v7 | PK |
|
||||
| patient_id | UUID | FK → patient |
|
||||
| report_date | Date | 化验日期 |
|
||||
| report_type | Enum | kidney_function / blood_routine / electrolyte / liver_function / other |
|
||||
| source | Enum | manual_input / photo_upload |
|
||||
| image_urls | JSON | 化验单照片 URL 数组 |
|
||||
| items | JSON | 指标数据数组(见下方结构) |
|
||||
| doctor_notes | Text | 医生解读/批注 |
|
||||
| reviewed_by | UUID | FK → user,审阅医生 |
|
||||
| reviewed_at | DateTime | 审阅时间 |
|
||||
| status | Enum | pending / reviewed |
|
||||
| tenant_id | UUID | 租户 |
|
||||
| 标准字段 | — | — |
|
||||
|
||||
**items JSON 结构:**
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "肌酐",
|
||||
"value": "856",
|
||||
"unit": "μmol/L",
|
||||
"reference_low": 44,
|
||||
"reference_high": 133,
|
||||
"is_abnormal": true
|
||||
},
|
||||
{
|
||||
"name": "血钾",
|
||||
"value": "6.2",
|
||||
"unit": "mmol/L",
|
||||
"reference_low": 3.5,
|
||||
"reference_high": 5.3,
|
||||
"is_abnormal": true
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
#### daily_monitoring(日常监测)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | UUID v7 | PK |
|
||||
| patient_id | UUID | FK → patient |
|
||||
| record_date | Date | 记录日期 |
|
||||
| morning_bp_systolic | i32 | 晨起收缩压 |
|
||||
| morning_bp_diastolic | i32 | 晨起舒张压 |
|
||||
| evening_bp_systolic | i32 | 晚间收缩压 |
|
||||
| evening_bp_diastolic | i32 | 晚间舒张压 |
|
||||
| weight | Decimal(5,1) | 体重 (kg) |
|
||||
| blood_sugar | Decimal(4,1) | 血糖 (mmol/L) |
|
||||
| fluid_intake | i32 | 饮水量 (ml) |
|
||||
| urine_output | i32 | 尿量 (ml) |
|
||||
| notes | Text | 备注 |
|
||||
| tenant_id | UUID | 租户 |
|
||||
| 标准字段 | — | — |
|
||||
|
||||
唯一约束:(patient_id, record_date)
|
||||
|
||||
### 4.4 与现有模块的关系
|
||||
|
||||
```
|
||||
patient(已有)
|
||||
├── dialysis_record(新增) 1:N 每次透析一条
|
||||
├── lab_report(新增) 1:N 每次化验一份
|
||||
├── daily_monitoring(新增) 1:N 每天一条
|
||||
├── health_data(已有,保留) 1:N 通用指标仍可用
|
||||
├── appointment(已有) 1:N 透析预约走这里
|
||||
└── follow_up(已有) 1:N 随访任务
|
||||
```
|
||||
|
||||
### 4.5 化验报告 items 用 JSON 的原因
|
||||
|
||||
- 化验项目数量和类型因报告而异(肾功能 8 项 vs 血常规 20+ 项)
|
||||
- 不需要按单个指标做复杂查询(都是按患者+日期范围查整份报告)
|
||||
- JSON 内含 `is_abnormal` 标记,前端直接渲染异常标红
|
||||
- PostgreSQL JSONB 支持按单个指标查询趋势(未来可加物化视图展平)
|
||||
|
||||
### 4.6 数据上报协同流程
|
||||
|
||||
**透析记录**:医护在医护端小程序/PC 填写 → 患者端只读查看
|
||||
|
||||
**化验报告**:患者拍照上传照片 + 手动填写指标 → 医护审阅/标注/解读 → 患者查看异常标红+医生解读
|
||||
|
||||
**日常监测**:患者每日填写(血压/体重/血糖/饮水量/尿量)→ 触发积分获取 → 医护端查看趋势+异常预警
|
||||
|
||||
### 4.7 新增 API 端点
|
||||
|
||||
**透析记录:**
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | /api/v1/health/patients/{id}/dialysis-records | 患者透析记录列表 |
|
||||
| GET | /api/v1/health/dialysis-records/{id} | 透析记录详情 |
|
||||
| POST | /api/v1/health/dialysis-records | 创建透析记录(医护) |
|
||||
| PUT | /api/v1/health/dialysis-records/{id} | 更新透析记录(医护) |
|
||||
|
||||
**化验报告:**
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | /api/v1/health/patients/{id}/lab-reports | 患者化验报告列表 |
|
||||
| GET | /api/v1/health/lab-reports/{id} | 报告详情 |
|
||||
| POST | /api/v1/health/lab-reports | 上传化验报告(患者/医护) |
|
||||
| PUT | /api/v1/health/lab-reports/{id}/review | 医生审阅(标注+解读) |
|
||||
|
||||
**日常监测:**
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | /api/v1/health/patients/{id}/daily-monitoring | 患者日常监测列表 |
|
||||
| POST | /api/v1/health/daily-monitoring | 上报日常监测(患者) |
|
||||
| GET | /api/v1/health/daily-monitoring/trend | 趋势数据查询 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 小程序迭代设计
|
||||
|
||||
### 5.1 患者端小程序
|
||||
|
||||
#### 新 TabBar 结构
|
||||
|
||||
```
|
||||
首页 | 上报 | 咨询 | 商城 | 我的
|
||||
```
|
||||
|
||||
替换现有:~~首页 | 健康 | 预约 | 资讯 | 我的~~
|
||||
|
||||
#### 首页(改造)
|
||||
|
||||
- 公告轮播(Banner 组件)
|
||||
- 功能入口 Grid:数据上报、我的医生、在线咨询、血透预约、积分商城
|
||||
- 今日健康打卡入口(未打卡时醒目提示)
|
||||
- 健康概览卡片:血压、体重、最近透析记录
|
||||
- 今日提醒列表:透析预约、血压测量、用药提醒
|
||||
|
||||
#### 上报 Tab(改造自"健康")
|
||||
|
||||
- 打卡区:血压/体重/血糖/饮水/尿量 快捷填写入口
|
||||
- 数据类型切换:日常监测 / 透析记录(只读)/ 化验报告
|
||||
- 趋势图(ECharts):血压/体重/血糖 折线图
|
||||
- 子页面:
|
||||
- 日常监测填写表单
|
||||
- 化验报告上传(拍照 + 指标填写)
|
||||
- 透析记录详情(只读)
|
||||
- 化验报告详情(含医生批注)
|
||||
- 趋势分析详情(大图 + AI 解读)
|
||||
|
||||
#### 咨询 Tab(全新)
|
||||
|
||||
- 咨询类型切换:医生 / 客服
|
||||
- 最近对话列表(未读红点)
|
||||
- 子页面:
|
||||
- 选择医生
|
||||
- 聊天界面(图文/语音 + 上传报告)
|
||||
- 客服对话
|
||||
|
||||
#### 商城 Tab(全新)
|
||||
|
||||
- 我的积分 + 签到按钮
|
||||
- 商品分类:全部 / 实物 / 检查 / 权益
|
||||
- 商品列表(网格)
|
||||
- 线下活动入口
|
||||
- 子页面:
|
||||
- 商品详情 + 兑换
|
||||
- 兑换确认(生成二维码)
|
||||
- 我的订单(待核销/已核销/已过期)
|
||||
- 线下活动详情 + 报名
|
||||
- 积分明细
|
||||
|
||||
#### 我的 Tab(改造)
|
||||
|
||||
- 个人信息 + 积分展示 + 连续打卡天数
|
||||
- 就诊人管理(已有)
|
||||
- 健康档案
|
||||
- 我的医生
|
||||
- 我的预约(从 TabBar 降级为菜单入口)
|
||||
- 我的报告(已有)
|
||||
- 我的随访(已有)
|
||||
- 我的订单(积分商城)
|
||||
- 消息通知(新增)
|
||||
- 用药提醒(已有,需接入后端)
|
||||
- 设置(已有)
|
||||
|
||||
### 5.2 医护端小程序(V2)
|
||||
|
||||
医护端为独立小程序(独立 AppID),V2 实现。V1 阶段医护功能通过 PC 管理后台覆盖。
|
||||
|
||||
#### V2 页面结构(约 18 页)
|
||||
|
||||
TabBar:概览 | 患者 | 咨询 | 随访 | 我的
|
||||
|
||||
- **概览**:今日待回复咨询数、异常预警列表、今日透析患者数、随访任务数
|
||||
- **患者**:患者列表(按透析/高危/标签筛选)、患者详情(档案+透析记录+化验+趋势图+标签)、填写透析记录、报告解读
|
||||
- **咨询**:未读消息列表、图文/语音回复、发送科普文章
|
||||
- **随访**:随访任务列表、填写记录、台账导出
|
||||
- **我的**:医生信息、我的患者、排班日历、科普文章管理
|
||||
|
||||
### 5.3 页面工作量统计
|
||||
|
||||
| 端 | 改造页 | 新增页 | 总计 |
|
||||
|----|--------|--------|------|
|
||||
| 患者端 | 5(首页/健康/我的/预约/资讯改造) | ~15(咨询3+商城6+上报子页4+通知2) | ~20 页 |
|
||||
| 医护端(V2) | 0 | 18 | 18 页 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 在线咨询设计(V2 详情待补充)
|
||||
|
||||
### 6.1 核心需求
|
||||
|
||||
- 客服通道:订单/物流/使用问题
|
||||
- 医生通道:选择医生、发送问题、上传报告、语音/文字沟通
|
||||
- 留言功能:医生离线时留存问题
|
||||
|
||||
### 6.2 技术方向
|
||||
|
||||
- V1 可先实现基于轮询的图文消息(复用现有 Consultation 模块)
|
||||
- V2 升级为 WebSocket 实时通信 + 语音消息
|
||||
- 客服通道可对接第三方客服系统(如美洽、智齿)
|
||||
|
||||
---
|
||||
|
||||
## 7. PC 管理后台新增页面
|
||||
|
||||
### 7.1 健康数据中心(新增)
|
||||
|
||||
| 页面 | 功能 |
|
||||
|------|------|
|
||||
| 透析数据统计 | 透析次数趋势、干体重变化、超滤量统计 |
|
||||
| 异常指标排行 | 血钾/血磷/肌酐异常患者排行 |
|
||||
| 上报率统计 | 患者数据上报活跃度、打卡率 |
|
||||
|
||||
### 7.2 商城管理(新增)
|
||||
|
||||
| 页面 | 功能 |
|
||||
|------|------|
|
||||
| 积分规则管理 | 增删改积分获取规则 |
|
||||
| 商品管理 | 增删改兑换商品、库存管理 |
|
||||
| 订单管理 | 兑换记录查看、手动核销、导出 |
|
||||
| 线下活动管理 | 活动创建、报名/签到管理 |
|
||||
| 积分统计 | 发放/消耗/活跃排行 |
|
||||
|
||||
### 7.3 统计报表(新增)
|
||||
|
||||
| 页面 | 功能 |
|
||||
|------|------|
|
||||
| 患者增长 | 新增患者趋势、活跃度 |
|
||||
| 咨询量 | 咨询次数/回复率/满意度 |
|
||||
| 随访完成率 | 随访任务执行统计 |
|
||||
| 商城数据 | 兑换排行、库存周转 |
|
||||
|
||||
---
|
||||
|
||||
## 8. AI 分析能力(V2)
|
||||
|
||||
### 8.1 V1 预留
|
||||
|
||||
- 趋势图已通过 ECharts 实现(现有 TrendChart 组件)
|
||||
- 异常指标通过 `is_abnormal` 标记和阈值校验实现
|
||||
- 健康报告可生成基础版(数据汇总 + 异常标注)
|
||||
|
||||
### 8.2 V2 增强
|
||||
|
||||
- LLM 集成:自然语言健康报告生成
|
||||
- AI 辅助:化验单 OCR 自动识别
|
||||
- 智能预警:基于历史数据的异常趋势预测
|
||||
|
||||
---
|
||||
|
||||
## 9. 分期建议
|
||||
|
||||
### V1.1(建议优先)
|
||||
|
||||
1. 血透专科数据模型(3 张新表 + API)
|
||||
2. 患者端小程序改造(TabBar + 首页 + 上报扩展)
|
||||
3. 积分商城后端 + 小程序前端
|
||||
4. 线下活动管理
|
||||
|
||||
### V1.2
|
||||
|
||||
1. 在线咨询(轮询版)
|
||||
2. 化验报告上传 + 审阅流程
|
||||
3. PC 管理后台新增页面
|
||||
4. 统计报表
|
||||
|
||||
### V2
|
||||
|
||||
1. 医护端小程序
|
||||
2. 在线咨询升级(WebSocket + 语音)
|
||||
3. AI 分析增强(LLM + OCR)
|
||||
4. Redis 缓存层
|
||||
|
||||
---
|
||||
|
||||
## 10. 风险与缓解
|
||||
|
||||
| 风险 | 影响 | 缓解措施 |
|
||||
|------|------|---------|
|
||||
| 积分 FIFO 结算复杂度 | 并发消费时积分桶冲突 | 使用数据库事务 + 乐观锁(account.version) |
|
||||
| 在线咨询工作量超预期 | V1.2 延期 | V1 先做轮询图文,WebSocket 推迟到 V2 |
|
||||
| 医护端小程序工作量大 | V2 延期 | V1 阶段用 PC 后台替代医护端功能 |
|
||||
| 化验单 OCR 准确率 | 用户体验差 | V1 先做手动填写,OCR 作为 V2 AI 增量 |
|
||||
| 积分通胀 | 积分价值稀释 | 可配置每日上限 + 过期机制 + 运营调整积分价格 |
|
||||
@@ -1,406 +0,0 @@
|
||||
# HMS 平台基座回顾与演进设计
|
||||
|
||||
> 日期: 2026-04-26 | 状态: Draft | 方法: 三专家多视角评审
|
||||
|
||||
---
|
||||
|
||||
## 1. 概述
|
||||
|
||||
### 1.1 回顾目的
|
||||
|
||||
HMS 健康管理平台经过 17 天密集开发(2026-04-10 ~ 2026-04-26),从 ERP 底座演进到包含 16 个 Rust crate、62 个前端页面、27 个小程序页面的综合医疗 SaaS 平台。本次回顾旨在:
|
||||
|
||||
- **验证基座设计** — 星形依赖拓扑、ErpModule trait、事件总线、多租户策略是否经得起实践检验
|
||||
- **评估演进路径** — 从插件开发模式到原生模块开发的决策是否正确
|
||||
- **识别缺口与风险** — 通过多专家视角发现盲点
|
||||
- **制定演进路线** — 基于 P0/P1/P2 优先级指导后续迭代
|
||||
|
||||
### 1.2 评审方法
|
||||
|
||||
采用三专家独立评审,每个专家从不同视角分析相同的诊断和建议:
|
||||
|
||||
| 专家 | 视角 | 关注点 |
|
||||
|------|------|--------|
|
||||
| 高级系统架构师 | 架构可持续性 | 模块边界、事件可靠性、技术债 |
|
||||
| 医疗信息化专家 | 临床安全与合规 | 患者安全、PIPL 合规、领域模型 |
|
||||
| 产品策略专家 | ROI 与开发节奏 | 优先级、技术债量化、路线图现实性 |
|
||||
|
||||
### 1.3 核心结论
|
||||
|
||||
**基座设计方向正确,但深度不足。** 星形依赖、trait 抽象、事件总线等基础架构经受住了实践检验。但在临床安全(危急值告警未闭环)、合规(知情同意缺失)、事件可靠性(无重放机制)方面存在需立即修复的缺口。插件系统已验证可行性但对 HMS 核心业务贡献有限,建议有条件冻结。
|
||||
|
||||
---
|
||||
|
||||
## 2. 基座设计验证
|
||||
|
||||
### 2.1 评分总览
|
||||
|
||||
| 维度 | 评分 | 说明 |
|
||||
|------|------|------|
|
||||
| 模块边界 | ★★★★ | 星形拓扑零循环依赖,trait 契约清晰 |
|
||||
| ErpModule trait | ★★★★ | 生命周期/权限/事件/健康检查统一接口 |
|
||||
| 事件总线 | ★★★☆ | 基础设施扎实(broadcast+outbox),但无重放机制,消费侧不完整 |
|
||||
| 多租户 | ★★★☆ | JWT→TenantContext 全链路贯通,但缺 RLS 兜底和集成测试 |
|
||||
| 权限体系 | ★★★★ | RBAC + 行级数据权限 + 按钮级控制 |
|
||||
| 插件系统 | ★★★☆ | CRUD 场景验证通过,医疗场景天花板明显 |
|
||||
| API 一致性 | ★★★★ | 统一 envelope、分页、OpenAPI 自动文档 |
|
||||
| 数据库迁移 | ★★★★ | 59 个迁移,幂等、可回滚、fixup 模式健康 |
|
||||
| 测试覆盖 | ★☆☆☆ | 36 后端 + 3 前端,覆盖率 < 5% |
|
||||
| 合规性 | ★☆☆☆ | 知情同意缺失,审计不完整,PIE 加密范围不足 |
|
||||
|
||||
### 2.2 星形依赖拓扑
|
||||
|
||||
```
|
||||
erp-core (L1)
|
||||
/ | \ \ \ \
|
||||
erp-auth workflow message config erp-health erp-plugin erp-ai
|
||||
\ | / / / / /
|
||||
erp-server (L3, 组装入口)
|
||||
```
|
||||
|
||||
- `erp-core`:零业务依赖,纯净基础层
|
||||
- 7 个业务 crate:各只依赖 `erp-core`,兄弟间无横向依赖
|
||||
- `erp-server`:唯一组装点,负责路由合并和模块初始化
|
||||
- **无循环依赖** — 架构师验证通过
|
||||
|
||||
### 2.3 ErpModule trait
|
||||
|
||||
当前 trait 提供统一的模块接口:
|
||||
|
||||
- **身份**:`name()` / `id()` / `version()`
|
||||
- **依赖声明**:`dependencies()` — 用于拓扑排序启动顺序
|
||||
- **生命周期**:`on_startup()` / `on_shutdown()` / `health_check()`
|
||||
- **多租户**:`on_tenant_created()` / `on_tenant_deleted()`
|
||||
- **权限自描述**:`permissions()` — 模块声明自己需要的权限码
|
||||
- **事件订阅**:`register_event_handlers()` / `as_any()`
|
||||
|
||||
**已知张力**:路由注册不在 trait 中,而是通过各模块的 inherent method (`public_routes()` / `protected_routes()`) 手动在 `main.rs` 中合并。原因是 Axum 的 `Router<S>` 泛型约束不适合 trait object。这是务实的妥协,但在添加新模块时有 boilerplate 成本。
|
||||
|
||||
### 2.4 事件总线
|
||||
|
||||
**实现机制**:`tokio::sync::broadcast` (容量 1024) + `domain_events` 表持久化(best-effort)+ Outbox relay (5秒轮询,3次重试)
|
||||
|
||||
**发布侧**(已识别的事件类型):
|
||||
|
||||
| 模块 | 事件类型数 | 示例 |
|
||||
|------|-----------|------|
|
||||
| erp-auth | 10 | `user.login`, `user.created`, `role.created` |
|
||||
| erp-workflow | 4 | `process_instance.started`, `task.completed` |
|
||||
| erp-message | 1 | `message.sent` |
|
||||
| erp-health | 13 | `patient.created`, `health_data.critical_alert`, `follow_up.overdue` |
|
||||
| erp-plugin | 2+ | `plugin.config.updated`, `plugin.trigger.*` |
|
||||
|
||||
**消费侧**(已识别的订阅者):
|
||||
|
||||
| 订阅者 | 订阅方式 | 处理的事件 |
|
||||
|--------|---------|-----------|
|
||||
| erp-message | `subscribe()` 全量 | `appointment.*`, `process_instance.*`, `task.*` |
|
||||
| erp-health | `register_handlers_with_state` | `workflow.task.completed` |
|
||||
| erp-plugin 通知 | `subscribe_filtered("plugin.trigger.*")` | 插件触发通知 |
|
||||
| outbox relay | 轮询 DB | 重发 pending 事件 |
|
||||
|
||||
**已识别缺陷**:
|
||||
1. **无重放机制** — 内存 broadcast,服务重启后未消费的事件丢失
|
||||
2. **无幂等保护** — `follow_up.overdue` 每 6 小时检查会重复发布同一条逾期事件
|
||||
3. **全量订阅** — erp-message 使用 `subscribe()` 而非 `subscribe_filtered()`,所有事件都经过消息模块
|
||||
|
||||
### 2.5 多租户
|
||||
|
||||
**已实现**:
|
||||
- JWT claims 提取 `tenant_id` → `TenantContext` 注入请求扩展
|
||||
- 所有 Entity 含 `tenant_id` 字段,BaseFields 统一
|
||||
- 所有 DomainEvent 携带 `tenant_id`
|
||||
- `on_tenant_created()` / `on_tenant_deleted()` 钩子(auth 和 health 已实现)
|
||||
- 部门级数据范围(`department_ids` 在 TenantContext 中)
|
||||
|
||||
**缺失**:
|
||||
- 无 PostgreSQL RLS policy 作为兜底层
|
||||
- 无强制 tenant_id 过滤的查询层机制 — 依赖每个 service 手动 `.filter()`
|
||||
- 当前实际只有 default_tenant,微信登录硬编码使用 `default_tenant_id`
|
||||
- 无多租户管理 API(创建/配置/迁移)
|
||||
|
||||
---
|
||||
|
||||
## 3. 演进路径回顾
|
||||
|
||||
### 3.1 时间线
|
||||
|
||||
```
|
||||
4/10-4/16 基座搭建 (Phase 1-6)
|
||||
→ core → auth → config → workflow → message
|
||||
→ 全部原生 Rust 模块,30+ 数据库表
|
||||
|
||||
4/13-4/18 WASM 插件实验
|
||||
→ 插件系统设计与实现 (Wasmtime + WIT bindgen)
|
||||
→ CRM (5实体) → Inventory (6实体) → Freelance → ITOps
|
||||
→ 证明:CRUD 密集型领域可行,沙盒隔离有效
|
||||
→ 跨插件数据引用未解决
|
||||
|
||||
4/23-4/26 HMS 分叉 — 健康模块原生开发
|
||||
→ 18+ 强类型实体 (患者/家属/医生/预约/排班/随访/咨询/体征/化验/透析/诊断/积分...)
|
||||
→ PII 加密 (AES-256-GCM)、脱敏管道
|
||||
→ AI 模块 (4 SSE 流式端点 + 3 REST 端点)
|
||||
→ 微信小程序 (27 页面)
|
||||
→ 按钮级权限控制
|
||||
```
|
||||
|
||||
### 3.2 从插件到原生的决策链
|
||||
|
||||
**原始插件愿景**(设计规格 2026-04-13):
|
||||
|
||||
- 平台模块原生,行业模块 WASM 插件
|
||||
- 插件通过 9 个 Host API 函数通信(db_insert/query/update/delete、event_publish、config_get 等)
|
||||
- 数据存 JSONB 动态表,路由自动生成
|
||||
- UI 配置驱动,通用 PluginCRUDPage 组件
|
||||
|
||||
**健康模块原生的 5 个硬限制**(设计规格 2026-04-23 §1.3):
|
||||
|
||||
| 限制 | 影响 | 不可妥协原因 |
|
||||
|------|------|-------------|
|
||||
| 20 实体上限 | 健康平台轻松超过 | 18+ 实体已是最低合理粒度 |
|
||||
| JSONB 存储 | 无强类型、无外键约束 | 医疗数据需要引用完整性和精确索引 |
|
||||
| 无自定义 API | 只有自动 CRUD | 趋势分析/统计报表/日历视图无法实现 |
|
||||
| 无文件上传 | 沙盒阻止文件系统访问 | 化验单/体检报告需要文件存储 |
|
||||
| WASM 沙盒限制 | 无 native crypto/外部 API/后台任务 | PII 加密、微信集成、定时任务全部需要 |
|
||||
|
||||
### 3.3 得失评估
|
||||
|
||||
**得 — 正确的决策:**
|
||||
|
||||
| 决策 | 收益 |
|
||||
|------|------|
|
||||
| 星形依赖拓扑 | 模块独立性强,可独立测试和替换 |
|
||||
| ErpModule 统一接口 | 新模块注册流程标准化 |
|
||||
| 事件总线 | 跨模块解耦通信的基础设施已就绪 |
|
||||
| JWT→TenantContext | 多租户全链路贯通 |
|
||||
| 健康模块原生 | 不受沙盒限制,加密/文件/后台任务全部可用 |
|
||||
| 插件实验 | 验证了平台灵活性,CRM/库存可正常使用 |
|
||||
|
||||
**失 — 需要修正的问题:**
|
||||
|
||||
| 决策 | 代价 |
|
||||
|------|------|
|
||||
| 插件系统投入过大 | 22,000 行代码(41% Rust 总量),对 HMS 核心业务贡献接近零 |
|
||||
| 积分系统混入 health | 8 实体/12+ 路由,增加合规复杂度和数据泄露面 |
|
||||
| 事件消费侧忽视 | 13 个事件只有 3 个被消费,危急告警和逾期通知空转 |
|
||||
| 测试覆盖极薄 | 36 后端 + 3 前端测试,覆盖率 < 5% |
|
||||
| 合规意识不足 | 知情同意缺失、审计不完整、PIE 加密范围不足 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 三专家评审摘要
|
||||
|
||||
### 4.1 高级系统架构师
|
||||
|
||||
**诊断准确度:7/10** — 四个张力都真实存在,但优先级和细节有偏差。
|
||||
|
||||
关键补充:
|
||||
|
||||
| 发现 | 严重程度 |
|
||||
|------|---------|
|
||||
| WIT 接口是同步调用(阻塞),WASM 运行时嵌入主进程(故障隔离不足) | 架构隐患 |
|
||||
| EventBus 内存 broadcast 无重放机制,服务重启丢事件 | P1 |
|
||||
| `follow_up.overdue` 无幂等保护,每 6h 检查重复发布 | P0 |
|
||||
| erp-message 用 `subscribe()` 全量订阅,性能隐患 | P1 |
|
||||
| RLS 不是 P0,多租户集成测试才是 | 观点 |
|
||||
| 积分系统(8 独立实体、12+ 路由)不应在 erp-health 内 | 共识 |
|
||||
| 缺监控/可观测性、数据备份策略、API 版本升级路线图 | 盲点 |
|
||||
|
||||
核心原则:**先补测试再重构,先修事件再上功能,先验证再加固。**
|
||||
|
||||
### 4.2 医疗信息化专家
|
||||
|
||||
**发现了比原始诊断更深层的临床安全风险。**
|
||||
|
||||
| 新发现 | 严重程度 |
|
||||
|--------|---------|
|
||||
| 危急值阈值全部硬编码(收缩压 180/80、心率 150/40),不可配置 | P0 |
|
||||
| `daily_monitoring` 表体征数据不经过危急值检测(合并前遗留) | P0 |
|
||||
| 过敏史更新直接覆盖,无变更历史 | P0 |
|
||||
| 知情同意完全缺失(搜索 consent/同意/授权/隐私 零结果) | P0 — PIPL 违规 |
|
||||
| 只有身份证号存储加密,姓名/过敏史/诊断/咨询内容明文 | P1 |
|
||||
| 审计日志不完整 — 只有预约状态变更记录前后值 | P1 |
|
||||
| `ip_address` 和 `user_agent` 从未被填充 | P1 |
|
||||
| 读操作(查看患者详情/化验报告)完全没有审计记录 | P1 |
|
||||
| 诊断记录 `icd_code` 只做字符串约束,无格式校验,无同行审核 | P1 |
|
||||
|
||||
合规评估:PIPL 第 29 条要求处理敏感个人信息须取得单独同意。医疗数据属于敏感个人信息。知情同意缺失是法律红线。
|
||||
|
||||
领域模型建议:积分系统(6 实体 + 2 线下活动实体)应拆分为独立 `erp-points` 或 `erp-engagement` 模块,与健康数据分离以降低合规复杂度。
|
||||
|
||||
### 4.3 产品策略专家
|
||||
|
||||
**开发节奏不可持续但不必恐慌。**
|
||||
|
||||
| 分析 | 结论 |
|
||||
|------|------|
|
||||
| 峰值 68 提交/天,fix 提交占 21.6% | 短期冲刺可以,长期人会耗竭 |
|
||||
| 41% Rust 代码在插件系统,对核心业务贡献接近零 | 最大 ROI 失衡 |
|
||||
| 单人 + AI 的"速度幻觉" | 68 提交/天 = 审查不足,积分混入 health 就是例证 |
|
||||
| 测试覆盖 < 5% | 正确水位不是 80%,而是关键路径不回退(目标 50-80 用例,3-4 天) |
|
||||
|
||||
关键风险缓解建议:
|
||||
- ADR(架构决策记录)强制化
|
||||
- 医疗安全代码双人外部 review
|
||||
- 每日提交上限 15 次
|
||||
- 每月需求裁剪
|
||||
|
||||
V2 血透路线图评估:技术储备已够(`dialysis_service` 286 行骨架在),但缺市场验证。建议先做 3-5 家目标客户调研,确认需求后再做 2 周 MVP 试运行。
|
||||
|
||||
---
|
||||
|
||||
## 5. 共识优先级
|
||||
|
||||
### 5.1 三专家加权共识矩阵
|
||||
|
||||
| 议题 | 架构师 | 医疗专家 | 产品策略 | 共识等级 |
|
||||
|------|--------|---------|---------|---------|
|
||||
| 危急值告警闭环 | P0 | P0 + 硬编码 | P0 | 三方一致 |
|
||||
| 知情同意 (PIPL) | 未涉及 | P0 | P0 | 两方一致 |
|
||||
| 审计日志补全 | 未涉及 | P1 | P0 | P0-P1 |
|
||||
| EventBus 可靠性 | P1 | 未涉及 | P0 | P0-P1 |
|
||||
| 随访逾期通知 | P0 | P0 | P0 | 三方一致 |
|
||||
| 积分系统拆分 | 应拆 | 应拆(合规) | 占 19.5% | 三方一致 |
|
||||
| RLS | 不是 P0 | P1 | P0 | 有分歧 |
|
||||
| 插件系统 | 有条件冻结 | 未涉及 | 冻结 | 两方一致 |
|
||||
| 测试覆盖 | 先补测试 | 上线前必修 | 50-80 用例 | 三方一致 |
|
||||
| V2 血透 | 未涉及 | 缺标准流程 | 先调研 | 两方一致 |
|
||||
|
||||
### 5.2 P0 — 上线前必修(估计 2-3 周)
|
||||
|
||||
| 序号 | 项 | 工作量 | 负责 crate | 说明 |
|
||||
|------|---|--------|-----------|------|
|
||||
| 1 | 危急值告警消费者 | 1 天 | erp-health + erp-message | `health_data.critical_alert` → 推送通知给责任医护 |
|
||||
| 2 | 危急值阈值可配置化 | 2 天 | erp-health | 硬编码阈值改为数据库配置,支持科室/年龄差异化 |
|
||||
| 3 | daily_monitoring 合并后告警验证 | 1 天 | erp-health | 确认合并到 vital_signs 后所有体征数据都经过告警检测 |
|
||||
| 4 | 随访逾期通知 | 1 天 | erp-health + erp-message | `follow_up.overdue` → 催办通知 + 幂等保护 |
|
||||
| 5 | 知情同意记录 | 3 天 | erp-health | 患者数据处理同意获取和记录机制 |
|
||||
| 6 | 审计日志补全 | 3 天 | erp-core + erp-health | 临床数据变更记录前后值、读操作审计、IP/UA 填充 |
|
||||
| 7 | EventBus 持久化增强 | 2 天 | erp-core | 服务重启不丢事件 + overdue 事件幂等 |
|
||||
|
||||
### 5.3 P1 — 治理(2-4 周)
|
||||
|
||||
| 序号 | 项 | 工作量 | 说明 |
|
||||
|------|---|--------|------|
|
||||
| 8 | 积分系统剥离 | 5 天 | 从 erp-health 拆分为独立 erp-engagement crate |
|
||||
| 9 | 关键路径测试 | 4 天 | 多租户隔离、患者安全路径、预约并发(50-80 用例) |
|
||||
| 10 | 插件系统冻结声明 | 0.5 天 | 保留代码,README 声明实验性,不再投入 |
|
||||
| 11 | erp-message 改用 `subscribe_filtered` | 1 天 | 减少无效事件传递 |
|
||||
| 12 | 统一事件消费模式 | 2 天 | 消除 `register_event_handlers` vs `on_startup` 双路径 |
|
||||
| 13 | 过敏史变更历史 | 1 天 | 更新时记录旧值 |
|
||||
|
||||
### 5.4 P2 — 扩展(后续迭代)
|
||||
|
||||
| 序号 | 项 | 前置条件 |
|
||||
|------|---|---------|
|
||||
| 14 | PostgreSQL RLS | P1 测试覆盖完成 |
|
||||
| 15 | 血透专科 | 3-5 家客户调研完成 |
|
||||
| 16 | OCR 化验单提取 | 血透验证后 |
|
||||
| 17 | IM 咨询 | 血透验证后 |
|
||||
| 18 | health 模块按子域重组目录 | P1 测试覆盖完成 |
|
||||
| 19 | 前端测试覆盖提升 | P1 后端测试完成 |
|
||||
| 20 | 动态菜单系统 | 现有计划可用 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 风险与缓解
|
||||
|
||||
### 6.1 开发模式风险
|
||||
|
||||
| 风险 | 影响 | 缓解措施 |
|
||||
|------|------|---------|
|
||||
| 单人认知单点 | 一人理解 16 个 crate,bus factor = 1 | ADR 强制化,关键决策留文档 |
|
||||
| AI 生成"编译对但逻辑错" | 危急值阈值硬编码、积分混入 health 就是例证 | 医疗安全代码双人外部 review |
|
||||
| 速度幻觉 | 68 提交/天 = 审查不足 | 每日提交上限 15 次 |
|
||||
| AI 回音壁 | AI 不质疑需求合理性 | 每月需求裁剪,引入真实用户反馈 |
|
||||
|
||||
### 6.2 临床安全风险
|
||||
|
||||
| 风险 | 影响 | 缓解措施 |
|
||||
|------|------|---------|
|
||||
| 危急值告警未闭环 | 危急体征值无人响应,可致患者安全事故 | P0-1:实现消费者 + 阈值可配置 |
|
||||
| 逾期随访无催办 | 患者失访,影响医疗质量指标 | P0-4:实现通知 + 幂等保护 |
|
||||
| 过敏史无变更记录 | 无法追溯过敏史变更,用药风险 | P1-13:添加变更历史 |
|
||||
| 告警阈值硬编码 | 无法适应儿科/老年科/血透科不同范围 | P0-2:数据库配置 |
|
||||
|
||||
### 6.3 合规风险
|
||||
|
||||
| 风险 | 法规依据 | 缓解措施 |
|
||||
|------|---------|---------|
|
||||
| 知情同意缺失 | PIPL 第 29 条 | P0-5:实现同意记录机制 |
|
||||
| 审计不完整 | 医疗机构信息化建设要求 | P0-6:补全审计日志 |
|
||||
| PIE 加密范围不足 | PIPL 第 51 条 | P1:扩展加密到姓名/过敏史/诊断 |
|
||||
| 数据删除权缺失 | PIPL 第 47 条 | P2:实现患者数据导出/删除 |
|
||||
|
||||
### 6.4 架构风险
|
||||
|
||||
| 风险 | 影响 | 缓解措施 |
|
||||
|------|------|---------|
|
||||
| EventBus 无重放 | 服务重启丢事件 | P0-7:增强持久化 |
|
||||
| 全量订阅 | 性能隐患,所有事件经消息模块 | P1-11:改用过滤订阅 |
|
||||
| 路由手动合并 | 新模块 boilerplate 成本 | 长期:ErpModule trait v2 |
|
||||
| erp-health 过大 | 18+ 实体,维护复杂度上升 | P2-18:按子域重组 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 附录
|
||||
|
||||
### 7.1 关键文件索引
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `crates/erp-core/src/module.rs` | ErpModule trait + ModuleRegistry (拓扑排序) |
|
||||
| `crates/erp-core/src/events.rs` | EventBus 实现 (broadcast + outbox) |
|
||||
| `crates/erp-core/src/types.rs` | TenantContext, BaseFields, Pagination |
|
||||
| `crates/erp-core/src/rbac.rs` | 权限/角色检查 |
|
||||
| `crates/erp-server/src/main.rs` | 服务组装和手动路由合并 |
|
||||
| `crates/erp-server/src/state.rs` | AppState + FromRef 桥接 |
|
||||
| `crates/erp-server/src/outbox.rs` | Outbox relay (5s 轮询, 3 次重试) |
|
||||
| `crates/erp-auth/src/middleware/jwt_auth.rs` | JWT 认证 + TenantContext 注入 |
|
||||
| `crates/erp-health/src/module.rs` | HealthModule (ErpModule 实现 + 后台任务) |
|
||||
| `crates/erp-health/src/event.rs` | 健康模块事件订阅 |
|
||||
| `crates/erp-health/src/crypto.rs` | AES-256-GCM 加密 |
|
||||
| `crates/erp-health/src/service/masking.rs` | PII 脱敏管道 |
|
||||
| `crates/erp-plugin/src/engine.rs` | WASM 插件引擎 |
|
||||
| `docs/superpowers/specs/2026-04-13-wasm-plugin-system-design.md` | 插件系统设计规格 |
|
||||
| `docs/superpowers/specs/2026-04-23-health-management-module-design.md` | 健康模块设计规格 |
|
||||
| `docs/discussions/2026-04-18-plugin-platform-brainstorm.md` | 插件平台演进讨论 |
|
||||
|
||||
### 7.2 迁移历史时间线
|
||||
|
||||
| 日期 | 迁移范围 | 说明 |
|
||||
|------|---------|------|
|
||||
| 4/10-11 | 核心平台 | 租户、用户、凭证、角色、权限、组织、部门、岗位 |
|
||||
| 4/12 | 配置 + 工作流 | 字典、菜单、设置、编号规则 + 流程定义/实例/令牌/任务 |
|
||||
| 4/13 | 消息 + 审计 | 模板、消息、订阅 + 审计日志 |
|
||||
| 4/14 | 修复 | 唯一索引与软删除冲突、标准字段补全 |
|
||||
| 4/16 | 领域事件 | domain_events 表 |
|
||||
| 4/17 | 插件系统 | 插件表、动态表 |
|
||||
| 4/18 | 搜索 + 权限 | pg_trgm、实体注册表、数据范围 |
|
||||
| 4/19 | 关联修复 | 用户部门、CRM 修复、插件市场 |
|
||||
| 4/23 | 健康表 | 患者、微信用户、文章 |
|
||||
| 4/24 | 索引修复 | 3 个 fixup 迁移 |
|
||||
| 4/25 | 健康扩展 | 患者ID哈希、医生名、透析/化验增强、AI 表、积分 |
|
||||
| 4/26 | 业务改进 | 诊断、列重命名、daily_monitoring 合并、菜单种子 |
|
||||
|
||||
**总计:59 个迁移,17 天内。** fixup 迁移模式健康(不编辑旧迁移,单独修复)。
|
||||
|
||||
### 7.3 项目统计快照 (2026-04-26)
|
||||
|
||||
| 指标 | 值 |
|
||||
|------|-----|
|
||||
| Rust crate 数 | 16 |
|
||||
| Rust 代码行 | ~57,000 |
|
||||
| 前端文件数 | 174 (TSX/TS) |
|
||||
| 前端页面 | 62 |
|
||||
| 小程序页面 | 27 |
|
||||
| 数据库迁移 | 59 |
|
||||
| 数据库表 | 30 基础 + 18 健康 + 3 AI |
|
||||
| 后端测试 | 36 |
|
||||
| 前端单元测试 | 3 |
|
||||
| Git 提交 | 237 |
|
||||
| 开发周期 | 17 天 |
|
||||
|
||||
---
|
||||
|
||||
*本文档由三专家多视角评审生成,作为 HMS 平台基座演进的参考基准。后续实施计划将基于本文档的优先级排序展开。*
|
||||
@@ -1,346 +0,0 @@
|
||||
# V1 客户演示方案设计规格
|
||||
|
||||
> 日期: 2026-05-09 | 状态: Draft v2 | 类型: 演示方案
|
||||
|
||||
## 1. 背景与目标
|
||||
|
||||
### 1.1 为什么做这次演示
|
||||
|
||||
HMS 健康管理平台已完成核心功能开发(700+ 次提交,V2 审计 85% 完成度),进入 V1 发布阶段。需要面向潜在客户(体检中心/血透中心)进行产品演示,目标是:
|
||||
|
||||
- **打动决策者签约** — 展示业务价值,而非功能清单
|
||||
- **收集真实反馈** — 了解客户实际工作流中的痛点,指导 V2 迭代
|
||||
- **验证产品定位** — 确认「AI 驱动主动关怀引擎」的定位是否与客户需求匹配
|
||||
|
||||
### 1.2 当前系统状态
|
||||
|
||||
| 指标 | 状态 |
|
||||
|------|------|
|
||||
| 核心链路 | 11 条端到端链路已验证通过 |
|
||||
| 已知 CRITICAL | 1 个未修复(Token 刷新竞态);其余 CRITICAL(告警权限码拼写、晚间血压丢失、仪表盘 500)均已修复 |
|
||||
| 角色测试通过率 | 84.6%(R01-R05) |
|
||||
| Web 前端 | 55 路由,283 文件,最完整的端 |
|
||||
| 小程序 | 59 页面,118 文件,代码完整 |
|
||||
| AI 模块 | 已对接 Ollama qwen3:4b,SSE 分析可用 |
|
||||
|
||||
### 1.3 演示策略
|
||||
|
||||
- **质量优先** — 修完所有已知问题再发布
|
||||
- **故事线驱动** — 用一个患者的 30 天管理历程展示完整闭环
|
||||
- **单患者深度** — 而非多角色广度,降低演示事故风险
|
||||
|
||||
## 2. 演示信息
|
||||
|
||||
| 项 | 值 |
|
||||
|------|------|
|
||||
| 受众 | 机构决策层 + 医疗团队 |
|
||||
| 时长 | 30-40 分钟 |
|
||||
| 视角 | 患者旅程(张大爷的 30 天) |
|
||||
| 涉及端 | Web 管理端(主力)+ 微信小程序(辅助) |
|
||||
| 涉及角色 | 护士、AI、医生、患者、健康管理师、管理员 |
|
||||
|
||||
## 3. 准备清单
|
||||
|
||||
### 3.1 测试账号
|
||||
|
||||
| 账号 | 角色 | 密码 | 用途 |
|
||||
|------|------|------|------|
|
||||
| `admin` | 管理员 | `Admin@2026` | 场景 7 仪表盘 |
|
||||
| `doctor1` | 医生 | `Admin@2026` | 场景 3 医生审批 |
|
||||
| `nurse1` | 护士 | `Admin@2026` | 场景 1 建档 + 场景 5 告警处理 |
|
||||
| `health_mgr` | 健康管理师 | `Admin@2026` | 场景 6 随访执行 |
|
||||
| `zhang_daye` | 患者(小程序) | 微信登录 | 场景 4/5 患者端操作 |
|
||||
|
||||
### 3.2 预置测试数据
|
||||
|
||||
| 数据 | 说明 | 目的 |
|
||||
|------|------|------|
|
||||
| 患者档案(张大爷) | 张建国,65岁,男,CKD 3期 | 主角 |
|
||||
| 历史化验单 ×2 | 肌酐 88→102 μmol/L 的趋势 | AI 分析需要历史对比发现趋势 |
|
||||
| 随访模板 | "慢性肾病定期随访"模板 | 场景 3 医生一键生成随访 |
|
||||
| 告警规则 | 肌酐>120 或 收缩压>160 | 场景 5 触发告警 |
|
||||
| 健康科普文章 ×3 | CKD 饮食/运动/用药 | 场景 4 小程序内容展示 |
|
||||
|
||||
### 3.3 环境检查
|
||||
|
||||
| 检查项 | 方法 | 通过标准 |
|
||||
|--------|------|----------|
|
||||
| 后端服务 | `cargo run` | 无 panic,Swagger 可访问 |
|
||||
| Web 前端 | `pnpm dev` | 登录页正常加载 |
|
||||
| 小程序 | 微信开发者工具 | 真机预览可扫码 |
|
||||
| 数据库 | 迁移已执行 | 预置数据查询无空结果 |
|
||||
| AI 模块 | Ollama 运行中 | SSE 分析端点可返回结果 |
|
||||
| 浏览器 | Chrome 无痕模式 | 干净环境,无缓存干扰 |
|
||||
|
||||
### 3.4 风险预案
|
||||
|
||||
| 风险 | 应对措施 |
|
||||
|------|----------|
|
||||
| AI 分析响应慢/失败 | 预先跑一次分析,截图备用;口头说明"云端大模型更快" |
|
||||
| 小程序真机扫码失败 | 准备 15 秒录屏视频展示关键页面 |
|
||||
| 后端服务崩溃 | 演示前重启一次确保干净状态 |
|
||||
| 数据库连接断开 | 提前验证 Docker PostgreSQL 健康状态 |
|
||||
| 告警权限码 bug | 演示前验证 AlertDashboard.tsx 权限码已修复(`health.alerts.manage`) |
|
||||
| SSE 长连接断开 | 录制 30 秒 AI 分析过程视频备用 |
|
||||
| Ollama 模型未加载 | 环境检查清单加入 `ollama list` 确认 qwen3:4b 已就绪 |
|
||||
| 多角色登录冲突 | 使用多个 Chrome Profile,每个角色一个独立 Profile |
|
||||
| 演示超时 | 标注可跳过场景(场景 6 可一句话带过) |
|
||||
|
||||
### 3.5 硬件与网络要求
|
||||
|
||||
| 项 | 要求 |
|
||||
|------|------|
|
||||
| 投影仪/大屏 | 分辨率 ≥ 1920x1080 |
|
||||
| 网络 | 演示机器与服务器在同一局域网,延迟 < 10ms |
|
||||
| 浏览器 | Chrome ×2 个 Profile(Web 端两个角色并行),或双屏方案 |
|
||||
| 手机 | 安装微信,可扫小程序码(备用:开发者工具投屏) |
|
||||
| 服务器 | 后端 + PostgreSQL + Ollama 运行在同一台机器,避免网络依赖 |
|
||||
|
||||
### 3.6 角色切换指引
|
||||
|
||||
| 切换点 | 操作 | 预计耗时 |
|
||||
|--------|------|----------|
|
||||
| 场景 1→2 | nurse1 退出 → admin 登录 | 15 秒 |
|
||||
| 场景 2→3 | admin 退出 → doctor1 登录 | 15 秒 |
|
||||
| 场景 3→4 | Web → 微信开发者工具/手机 | 10 秒 |
|
||||
| 场景 4→5 | 小程序录入 → Web nurse1 告警 | 10 秒 |
|
||||
| 场景 5→6 | nurse1 退出 → health_mgr 登录 | 15 秒 |
|
||||
| 场景 6→7 | health_mgr 退出 → admin 登录 | 15 秒 |
|
||||
|
||||
**建议:** 准备 2 个 Chrome Profile(Profile A: nurse1/admin,Profile B: doctor1/health_mgr),减少登录切换。场景 4/5 用独立手机或开发者工具。总切换时间约 1-1.5 分钟。
|
||||
|
||||
## 4. 演示脚本
|
||||
|
||||
### 开场(2 分钟)
|
||||
|
||||
**话术:**
|
||||
> "体检中心最大的痛点是什么?患者体检完,就走了。没有后续管理,没有随访跟进,体检数据躺在系统里没人看。今天给大家演示 HMS 健康管理平台如何解决这个问题——用一个真实场景:张大爷来体检后,系统如何帮他做 30 天的持续健康管理。"
|
||||
|
||||
---
|
||||
|
||||
### 场景 1:张大爷来体检(Day 1 上午)— 护士视角
|
||||
|
||||
**登录:** Web 端 `nurse1` | **时长:** ~4 分钟
|
||||
|
||||
**操作步骤:**
|
||||
1. 登录后展示护士工作台首页 — 一眼看到今日待办
|
||||
2. 点击「患者管理」→「新建患者」
|
||||
3. 填入:张建国 / 男 / 65岁 / 手机号 / 慢性肾病3期(诊断标签)
|
||||
4. 保存 → 跳转患者详情页
|
||||
5. 在患者详情页点击「体征录入」→ 录入血压 142/88、心率 72、空腹血糖 5.8
|
||||
6. 点击「化验报告」→ 上传预置的化验单图片,显示肌酐值 102 μmol/L
|
||||
|
||||
**话术:**
|
||||
> "张大爷第一次来体检中心。以前护士拿纸质表格登记,现在 30 秒建档。体征数据和化验报告立刻进入系统。"
|
||||
|
||||
**突出能力:** 快速建档、结构化体征录入、化验单数字化
|
||||
|
||||
---
|
||||
|
||||
### 场景 2:AI 自动分析(Day 1 下午)— 系统自动触发
|
||||
|
||||
**登录:** `admin` 或任意管理端账号 | **时长:** ~4 分钟
|
||||
|
||||
**操作步骤:**
|
||||
1. 展示「AI 分析」页面 → 显示张大爷的分析结果
|
||||
2. 点击分析详情 → 展示 AI 输出:
|
||||
- "肌酐值 88→102 μmol/L,3 个月持续上升趋势"
|
||||
- "建议:加做肾功能全套检查,排除 CKD 进展"
|
||||
- 风险等级:中风险(黄色标签)
|
||||
3. 切到 AI 建议列表 → 展示系统自动生成的「建议加做肾功能检查」行动项
|
||||
|
||||
**话术:**
|
||||
> "护士录入完数据,系统后台自动跑 AI 分析。不需要医生手动触发。AI 发现张大爷肌酐 3 个月在涨,主动建议进一步检查。这就是我们说的「主动关怀」——不是等患者出问题才看,是系统帮你盯着。"
|
||||
|
||||
**突出能力:** AI 自动分析、趋势发现、主动建议生成
|
||||
|
||||
**重要说明:** 当前 Web 端 AI 分析触发入口有限(审计报告指出"仅历史查看有 UI,分析触发无入口")。演示前**必须**执行以下操作之一:
|
||||
- 方案 A:演示前通过 API 手动触发一次分析(`POST /api/v1/ai/analysis/...`),演示时展示已生成的结果
|
||||
- 方案 B:为演示临时添加一个「触发分析」按钮到患者详情页
|
||||
- 推荐方案 A,配合话术调整:"这是系统刚才自动生成的分析结果"
|
||||
|
||||
**预案:** AI 分析慢或失败 → 展示预置截图,口头说明"接入云端大模型后速度更快"
|
||||
|
||||
---
|
||||
|
||||
### 场景 3:医生一秒决策(Day 3)— 医生视角
|
||||
|
||||
**登录:** Web 端 `doctor1` | **时长:** ~5 分钟
|
||||
|
||||
**操作步骤:**
|
||||
1. 展示医生工作台 → 待办区域显示"1 条 AI 建议待审批"
|
||||
2. 点击进入 → 查看 AI 分析详情 + 患者历史数据
|
||||
3. 点击「同意建议」→ 系统自动:
|
||||
- 生成随访任务("肾功能复查随访",2 周后到期)
|
||||
- 推送小程序消息给患者
|
||||
4. 展示随访任务列表 → 新任务已创建
|
||||
5. 点击「预约管理」→ 演示为张大爷预约复查(选医生、选时间段、确认)
|
||||
|
||||
**话术:**
|
||||
> "李医生早上打开系统,看到 AI 昨天的分析建议。以前要翻纸质报告、手动比对数据,现在 AI 已经帮你分析好了,医生只需要做一个决策:同意还是不同意。点一下,系统自动安排随访、自动通知患者。"
|
||||
|
||||
**突出能力:** AI 辅助决策、一键生成随访、自动通知患者
|
||||
|
||||
---
|
||||
|
||||
### 场景 4:张大爷在家收到提醒(Day 7)— 小程序视角
|
||||
|
||||
**操作:** 微信开发者工具或真机预览 | **时长:** ~4 分钟
|
||||
|
||||
**操作步骤:**
|
||||
1. 打开小程序首页 → 展示今日摘要:1 条随访待办 + 1 篇健康科普
|
||||
2. 点击「消息」Tab → 显示"您有一条新的随访任务"
|
||||
3. 点击进入随访详情 → 显示随访问卷(饮食情况、用药依从性、症状变化)
|
||||
4. 快速填写 2-3 项 → 提交
|
||||
5. 切回「健康」Tab → 展示张大爷的体征趋势图(血压曲线、肌酐趋势)
|
||||
6. 展示 AI 建议卡片:"您的血压近一周有上升趋势,建议减少盐分摄入"
|
||||
|
||||
**话术:**
|
||||
> "张大爷在家打开手机,不用打电话、不用跑医院,系统自动提醒他有随访要完成。填个问卷 2 分钟,医生那边就能看到。趋势图也让他自己看到身体变化,比口头解释直观得多。"
|
||||
|
||||
**突出能力:** 小程序主动提醒、随访问卷、趋势可视化、AI 健康建议触达
|
||||
|
||||
**预案:** 真机失败 → 播放 15 秒小程序录屏,重点展示随访提醒和趋势图
|
||||
|
||||
---
|
||||
|
||||
### 场景 5:危急值告警(Day 14)— 护士 + 系统联动
|
||||
|
||||
**操作:** 先小程序,再切 Web 端 | **时长:** ~4 分钟
|
||||
|
||||
**操作步骤:**
|
||||
1. **小程序端**(快速操作):张大爷录入血压 168/95 → 提交
|
||||
2. **切到 Web 端**(`nurse1` 登录):
|
||||
- 顶部弹出告警通知 "危急值告警:张建国 收缩压 168mmHg"
|
||||
- 点击进入告警列表 → 红色高亮显示
|
||||
- 点击告警详情 → 展示:触发规则(收缩压>160)、当前值、历史趋势
|
||||
- 点击「确认」→ 状态变为"已确认"
|
||||
- 点击「处理」→ 录入处理备注:"已电话通知患者,建议立即到门诊"
|
||||
- 状态变为"已处理"
|
||||
3. **回到小程序端**:张大爷收到消息"您的血压偏高,李医生建议您尽快来院检查"
|
||||
|
||||
**话术:**
|
||||
> "张大爷在家量了个血压,168。以前这种情况没人知道,可能拖到下次复诊才发现。现在数据一传上来,护士工作站立刻弹告警。护士确认后打电话给患者,15 分钟内完成从发现到处理。这才是真正的「主动关怀」。"
|
||||
|
||||
**突出能力:** 实时告警、分级处理、跨端联动(小程序录入→Web 告警→小程序反馈)
|
||||
|
||||
---
|
||||
|
||||
### 场景 6:随访闭环(Day 21)— 健康管理师视角
|
||||
|
||||
**登录:** Web 端 `health_mgr` | **时长:** ~4 分钟
|
||||
|
||||
**操作步骤:**
|
||||
1. 展示健康管理师工作台 → 随访任务列表显示"张建国 - 肾功能复查随访 - 即将到期"
|
||||
2. 点击执行随访 → 选择"电话随访"
|
||||
3. 录入随访记录:
|
||||
- 患者状态:"已完成肾功能检查,肌酐降至 98"
|
||||
- 遵医行为:"按时服药,控制饮食"
|
||||
- 下一步:"继续观察,3 个月后复查"
|
||||
4. 提交 → 随访状态变为"已完成"
|
||||
5. 展示随访历史时间线 → Day 3 创建 → Day 7 问卷 → Day 21 电话随访,完整记录
|
||||
|
||||
**话术:**
|
||||
> "30 天的管理周期里,每一步都有记录。从 AI 发现问题、医生决策、患者问卷、到健康管理师电话回访,全部可追溯。卫健委来检查,一导出就是完整的健康管理档案。"
|
||||
|
||||
**突出能力:** 随访全流程记录、可追溯、健康管理闭环
|
||||
|
||||
---
|
||||
|
||||
### 场景 7:数据说话(Day 30)— 管理员视角
|
||||
|
||||
**登录:** Web 端 `admin` | **时长:** ~3 分钟
|
||||
|
||||
**操作步骤:**
|
||||
1. 展示运营仪表盘:
|
||||
- 本月管理患者数
|
||||
- 随访完成率
|
||||
- AI 分析覆盖率
|
||||
- 告警响应平均时间
|
||||
2. 展示趋势图:患者增长曲线、随访完成率趋势
|
||||
3. 切到「内容管理」→ 展示已发布的健康科普文章(阅读量、转发量)
|
||||
4. 切到「积分商城」→ 展示患者积分排行、兑换记录
|
||||
|
||||
**话术:**
|
||||
> "张大爷的故事不是个例。系统帮你管每一个患者,而且每一步都有数据。随访完成率从手工追踪的 40% 提升到系统化管理后能做到 80% 以上。这些数据就是你们向卫健委、向患者证明管理质量的最好证据。"
|
||||
|
||||
**突出能力:** 运营数据可视化、管理质量量化、内容运营
|
||||
|
||||
**重要说明:** 单个患者(张大爷)的数据不足以支撑仪表盘的说服力。演示前**必须**预置 20-30 个背景患者数据 + 若干随访/告警记录,让仪表盘显示有意义的统计数字。在数据预置脚本中一并处理。
|
||||
|
||||
---
|
||||
|
||||
### 收尾(5 分钟)
|
||||
|
||||
**总结话术:**
|
||||
> "总结一下 HMS 带来的三个核心变化:
|
||||
> 1. **从被动到主动** — AI 帮你看数据,系统帮你盯着患者
|
||||
> 2. **从纸质到数字** — 每一步可追溯,检查随时可导出
|
||||
> 3. **从单点到闭环** — 体检不是终点,30 天持续管理才是"
|
||||
|
||||
**收集反馈(3 个问题):**
|
||||
1. "您刚才看到的流程中,哪些环节对您机构最有价值?"
|
||||
2. "有没有我们没覆盖到、但您实际工作中很重要的场景?"
|
||||
3. "您更关心 Web 管理端还是患者小程序端的能力?"
|
||||
|
||||
---
|
||||
|
||||
## 5. V1 发布前必修项
|
||||
|
||||
### 5.1 必修(阻塞发布)
|
||||
|
||||
| # | 问题 | 修复方案 | 工作量估计 |
|
||||
|---|------|----------|-----------|
|
||||
| 1 | Token 刷新并发竞态 | refresh 流程加事务 + SELECT FOR UPDATE | 0.5 天 |
|
||||
|
||||
### 5.2 建议修(提升演示体验)
|
||||
|
||||
| # | 问题 | 说明 |
|
||||
|---|------|------|
|
||||
| 1 | AI 分析预置截图 | 演示前手动跑一次分析,截图备用 |
|
||||
| 2 | 小程序录屏视频 | 15 秒展示随访提醒 + 趋势图 |
|
||||
| 3 | 测试数据脚本 | 一键预置张大爷的完整数据 |
|
||||
| 4 | 演示前全链路冒烟 | 跑一遍 7 个场景确认无阻塞 |
|
||||
|
||||
## 6. 下一步演化方向(演示后收集)
|
||||
|
||||
| 方向 | 来源 | 说明 |
|
||||
|------|------|------|
|
||||
| HIS 系统集成 | 场景 1 | 演示后可能被问"能不能对接我们现有 HIS" |
|
||||
| 报告导出 | 场景 6 | 卫健委检查需要标准格式报告 |
|
||||
| 多科室支持 | 客户反馈 | 当前以肾病/体检为主,其他科室扩展 |
|
||||
| 微信服务号推送 | 场景 4 | 小程序消息触达有限,服务号更灵活 |
|
||||
| 设备直连 | 场景 5 | 血压计/血糖仪 BLE 直连小程序 |
|
||||
|
||||
## 7. Q&A 异议处理
|
||||
|
||||
### 客户可能提出的问题及建议回答
|
||||
|
||||
**Q: 能不能对接我们现有的 HIS/EMR 系统?**
|
||||
> HMS 提供标准 FHIR R4 接口和 RESTful API,支持 HL7 标准数据交换。具体集成方案需要了解贵院 HIS 的品牌和版本,我们可以安排技术团队做接口评估。通常 2-4 周可以完成基础对接。
|
||||
|
||||
**Q: 患者数据安全如何保障?**
|
||||
> 数据存储采用 PII 加密(姓名/身份证/手机号等敏感字段加密存储),多租户隔离确保不同机构数据完全独立。系统支持私有化部署,数据不出院。后端使用 Rust 语言开发,天然免疫内存安全漏洞。
|
||||
|
||||
**Q: AI 分析的准确率如何?**
|
||||
> 当前 AI 模块定位是「辅助筛查」,发现异常趋势后由医生做最终决策。不是替代医生诊断,而是帮医生从海量数据中找到需要关注的患者。所有 AI 建议都需要医生审批才生效。
|
||||
|
||||
**Q: 部署方式有哪些?**
|
||||
> 支持 SaaS(按年付费,我们运维)和私有化部署(一次性 + 年维护费,部署在客户服务器)。SaaS 适合快速上线,私有化适合数据合规要求高的机构。
|
||||
|
||||
**Q: 价格怎么算?**
|
||||
> 根据机构规模(管理患者数、医护账号数)定制方案。演示后我们可以根据贵院的具体需求出一份详细报价。
|
||||
|
||||
**Q: 医护人员需要培训多久?**
|
||||
> 系统设计遵循「零培训」理念——医生工作台只展示待办,护士录入界面跟纸质表单一样直观。通常 30 分钟上手,1 天熟练。我们提供远程培训和操作手册。
|
||||
|
||||
## 8. DRY RUN 计划
|
||||
|
||||
| 阶段 | 时间 | 内容 |
|
||||
|------|------|------|
|
||||
| D-7 | 演示前 7 天 | 修完 P0 问题(Token 刷新、AI 触发入口验证) |
|
||||
| D-5 | 演示前 5 天 | 编写数据预置脚本,预置张大爷完整数据 |
|
||||
| D-3 | 演示前 3 天 | 第一次 DRY RUN:完整走 7 个场景,记录阻塞点 |
|
||||
| D-2 | 演示前 2 天 | 修复 DRY RUN 发现的问题,预置 20-30 个背景患者数据 |
|
||||
| D-1 | 演示前 1 天 | 第二次 DRY RUN(带投影/网络),确认全链路无阻塞 |
|
||||
| D-Day | 演示当天 | 提前 1 小时启动环境,30 分钟前最终冒烟 |
|
||||
Reference in New Issue
Block a user