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:
iven
2026-05-15 09:29:04 +08:00
parent dc983945ff
commit 18fa6ce6d4
92 changed files with 53 additions and 10253 deletions

View File

@@ -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

View File

@@ -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" # 全局唯一 IDkebab-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` 不依赖任何业务 crateauth/config/workflow/message
- `erp-server` 在组装时引入 `erp-plugin-runtime`

File diff suppressed because it is too large Load Diff

View File

@@ -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 ColumnJSONB 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 updatePATCH 只合并 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
这些将在后续的设计规格中详细展开。

View File

@@ -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 个季度的分层改进,将平台从"功能完整"推进到"生产就绪"
- **Q24-5月**:消除安全风险,建立自动化质量门
- **Q36-8月**:强化架构,提升前端工程化水平
- **Q49-11月**:补齐测试覆盖,扩展插件生态
### 1.4 约束
- **独立开发者** + Claude 辅助 — 每季度聚焦单一维度
- **SaaS 优先**部署 — 多租户安全是硬性要求
- **不破坏现有功能** — 所有改进必须向后兼容
---
## 2. Q2安全地基 + CI/CD4-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 + 进销存插件 + 热更新 + 文档清理)。调整为两个子阶段:
- **Q4a9-10月**:测试基础设施 — Testcontainers 集成测试框架 + Playwright E2E + 文档清理
- **Q4b11月+**:插件生态 — 进销存插件 + 热更新
热更新功能可视 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 → 增量 DDLADD 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 11Testcontainers 对 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 文档与代码同步

View File

@@ -1,604 +0,0 @@
# CRM 插件平台标杆 — P0 基础能力设计规格
> **版本**: v1.1 (修正版 — 基于代码审查发现,对齐现有实现)
> **日期**: 2026-04-18
> **状态**: Draft
> **定位**: 插件平台标杆 — CRM 是试金石,打磨通用能力
---
## 1. 背景与动机
### 1.1 为什么要做这个
CRM 插件是 ERP 平台的第一个行业插件,当前状态是"客户通讯录 + 标签 + 关系图谱",距离一流 CRMSalesforce/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 规格 |

View File

@@ -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:** 收敛为统一设计规格,以财务插件为验证载体

View File

@@ -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` 方法
安全保证:新版本加载失败 → 旧版本仍在运行,零停机。
---
## 改动 4Schema 演进ALTER TABLE 支持)
### 问题
升级时只处理新增实体CREATE TABLE不处理已有实体的字段变更。
### 方案:利用 JSONB 特性实现轻量级 Schema 演进
大部分字段变更不需要 DDLJSONB 天然支持),仅新增 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. 上传正确的新版本 → 验证成功切换
### 阶段 2Schema 演进
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` | 适配多聚合返回格式 |

View File

@@ -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 1settings + trigger_events + cascade + visible_when + validation
P2: itops Layer 1settings + trigger_events + cascade + visible_when + validation
P3: freelance Layer 33 个 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` 字段 + 前端渲染组件。

View File

@@ -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_dateassigned_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 端点测试
- 多租户隔离验证
- 端到端功能验证

View File

@@ -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.requestJWT 注入、错误处理)
│ │ ├── 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 归属关系 |

View File

@@ -1,671 +0,0 @@
# 健康管理模块全面迭代设计
> **文档版本**: 1.0
> **日期**: 2026-04-24
> **状态**: 待评审
> **基于**: 5 位专家(后端架构/前端架构/医疗业务/安全质量/产品策略)深度审查
---
## 0. 审查发现总览
### 0.1 V1 发布阻塞项
| # | 阻塞项 | 来源 | 影响 |
|---|--------|------|------|
| B1 | Web 健康模块 10 页面未实现 | 前端架构/产品策略 | 无法演示和交付 |
| B2 | 医疗数据安全不合规 | 安全质量 | 零 sanitize / 零审计 / 身证明文 / 零测试 |
| B3 | 数据一致性缺陷 | 医疗业务/后端架构 | 排班可超额 / 名额释放可能失败 / 随访逾期未实现 |
| B4 | 事件处理器空壳 | 后端架构 | 随访状态/咨询消息不联动 |
### 0.2 当前完成度
| 层级 | 模块 | 完成度 |
|------|------|--------|
| 后端 | erp-health16 实体/8 服务/7 handler/40+ API | 95% |
| 后端 | 事件处理器业务逻辑 | 0%(框架已搭建,需填充 db 操作) |
| 后端 | sanitize / 审计 / 加密 | 0% |
| 后端 | 测试覆盖 | 0% |
| Web 前端 | 健康模块页面 | 0% |
| Web 前端 | 健康模块 API 服务层 | 0% |
| 小程序 | 初版 21 页面 | 85% |
---
## 1. 安全省基(阶段 11.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` FamilyMemberReqcreate + 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. 后端补完(阶段 21.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 页面(阶段 33.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 表格
- 科室筛选 + 在线状态 Badgeonline=绿/busy=黄/offline=灰)
- 详情 Drawer
#### AppointmentList.tsx中复杂度2 天)
- `Segmented` 切换列表/日历视图
- 列表模式:表格 + 状态筛选 + 日期筛选
- 日历模式:`Calendar` + `cellRender` 显示当日预约数
- 状态流转 Dropdownpending → 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 CRUD0.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 缓存
- 国际化(英文等多语言)
- 小程序医护端

View File

@@ -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 三态完整
- [ ] 所有代码已提交

View File

@@ -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. **切片 2AI 管理端** — 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` — 受保护的按钮内容
行为: 无权限时不渲染 childrenhidden 模式)。
**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. 切片 2AI 管理端 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.*`),不硬编码中文
- 权限数据加载失败时默认无权限(安全降级,宁可少显示按钮也不暴露越权操作)

View File

@@ -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 + 路线图** | |

View File

@@ -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_ruleearn 类型) |
| order_id | UUID | FK → points_orderspend 类型,可空) |
| 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 + QRUUID 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
医护端为独立小程序(独立 AppIDV2 实现。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 增量 |
| 积分通胀 | 积分价值稀释 | 可配置每日上限 + 过期机制 + 运营调整积分价格 |

View File

@@ -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 个 cratebus 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 平台基座演进的参考基准。后续实施计划将基于本文档的优先级排序展开。*

View File

@@ -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:4bSSE 分析可用 |
### 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` | 无 panicSwagger 可访问 |
| 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 个 ProfileWeb 端两个角色并行),或双屏方案 |
| 手机 | 安装微信,可扫小程序码(备用:开发者工具投屏) |
| 服务器 | 后端 + 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 ProfileProfile A: nurse1/adminProfile 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 秒建档。体征数据和化验报告立刻进入系统。"
**突出能力:** 快速建档、结构化体征录入、化验单数字化
---
### 场景 2AI 自动分析Day 1 下午)— 系统自动触发
**登录:** `admin` 或任意管理端账号 | **时长:** ~4 分钟
**操作步骤:**
1. 展示「AI 分析」页面 → 显示张大爷的分析结果
2. 点击分析详情 → 展示 AI 输出:
- "肌酐值 88→102 μmol/L3 个月持续上升趋势"
- "建议:加做肾功能全套检查,排除 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 分钟前最终冒烟 |