Compare commits
4 Commits
28dafa9bea
...
3e1413aebc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e1413aebc | ||
|
|
36f2ba381a | ||
|
|
a3273ca581 | ||
|
|
f58c60599b |
@@ -189,12 +189,9 @@ impl AuthService {
|
||||
db: &sea_orm::DatabaseConnection,
|
||||
jwt: &JwtConfig<'_>,
|
||||
) -> AuthResult<LoginResp> {
|
||||
// Validate existing refresh token
|
||||
let (old_token_id, claims) =
|
||||
TokenService::validate_refresh_token(refresh_token_str, db, jwt.secret).await?;
|
||||
|
||||
// Revoke the old token (rotation)
|
||||
TokenService::revoke_token(old_token_id, claims.sub, db).await?;
|
||||
// Atomically validate and revoke the old refresh token (prevents TOCTOU race)
|
||||
let claims =
|
||||
TokenService::validate_and_revoke_atomic(refresh_token_str, db, jwt.secret).await?;
|
||||
|
||||
// Fetch fresh roles and permissions
|
||||
let roles: Vec<String> = TokenService::get_user_roles(claims.sub, claims.tid, db).await?;
|
||||
|
||||
@@ -175,6 +175,45 @@ impl TokenService {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Atomically validate and revoke a refresh token by hash.
|
||||
/// This prevents TOCTOU race conditions during concurrent refresh requests.
|
||||
/// Returns the decoded claims on success, or TokenRevoked if already consumed.
|
||||
pub async fn validate_and_revoke_atomic(
|
||||
token: &str,
|
||||
db: &DatabaseConnection,
|
||||
secret: &str,
|
||||
) -> AuthResult<Claims> {
|
||||
let claims = Self::decode_token(token, secret)?;
|
||||
if claims.token_type != "refresh" {
|
||||
return Err(AuthError::Validation("不是 refresh token".to_string()));
|
||||
}
|
||||
|
||||
let hash = sha256_hex(token);
|
||||
let now = Utc::now();
|
||||
let result = user_token::Entity::update_many()
|
||||
.col_expr(
|
||||
user_token::Column::RevokedAt,
|
||||
sea_orm::sea_query::Expr::value(Some(now.naive_utc())),
|
||||
)
|
||||
.col_expr(
|
||||
user_token::Column::UpdatedAt,
|
||||
sea_orm::sea_query::Expr::value(now.naive_utc()),
|
||||
)
|
||||
.filter(user_token::Column::TokenHash.eq(&hash))
|
||||
.filter(user_token::Column::UserId.eq(claims.sub))
|
||||
.filter(user_token::Column::TenantId.eq(claims.tid))
|
||||
.filter(user_token::Column::RevokedAt.is_null())
|
||||
.exec(db)
|
||||
.await
|
||||
.map_err(|e| AuthError::Validation(e.to_string()))?;
|
||||
|
||||
if result.rows_affected == 0 {
|
||||
return Err(AuthError::TokenRevoked);
|
||||
}
|
||||
|
||||
Ok(claims)
|
||||
}
|
||||
|
||||
/// Revoke all non-revoked refresh tokens for a given user within a tenant.
|
||||
pub async fn revoke_all_user_tokens(
|
||||
user_id: Uuid,
|
||||
|
||||
476
docs/superpowers/plans/2026-05-09-v1-demo-preparation-plan.md
Normal file
476
docs/superpowers/plans/2026-05-09-v1-demo-preparation-plan.md
Normal file
@@ -0,0 +1,476 @@
|
||||
# V1 客户演示准备 — 实施计划
|
||||
|
||||
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** 修复已知 CRITICAL 问题,预置演示数据,完成 DRY RUN 验证,确保 V1 客户演示 7 个场景端到端无阻塞。
|
||||
|
||||
**Architecture:** 按依赖关系分 6 个 Task:先修 CRITICAL(Token 竞态),再验证关键链路(告警、AI),然后预置数据,最后全链路冒烟。每个 Task 独立可提交。
|
||||
|
||||
**Tech Stack:** Rust (SeaORM + Axum), TypeScript/React (Web 前端), SQL (数据预置), Taro (小程序)
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-05-09-v1-customer-demo-plan-design.md`
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
| 操作 | 文件 | 职责 |
|
||||
|------|------|------|
|
||||
| Modify | `crates/erp-auth/src/service/token_service.rs:156-176` | revoke 改为原子操作 |
|
||||
| Modify | `crates/erp-auth/src/service/auth_service.rs:187-258` | refresh 流程使用原子 revoke |
|
||||
| Create | `crates/erp-server/tests/integration/auth_concurrent_tests.rs` | 并发刷新测试 |
|
||||
| Create | `scripts/demo-seed.sql` | 演示数据预置脚本 |
|
||||
| Verify | `crates/erp-health/src/service/seed.rs` | 确认告警规则覆盖演示场景 |
|
||||
| Verify | `apps/web/src/pages/health/components/LabReportsTab.tsx:36-57` | 确认 AI 触发按钮可用 |
|
||||
|
||||
---
|
||||
|
||||
## Chunk 1: Token 刷新竞态修复
|
||||
|
||||
### Task 1: 修复 Token 刷新并发竞态(CRITICAL)
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/erp-auth/src/service/token_service.rs` — 新增 `revoke_by_hash_atomic` 方法
|
||||
- Modify: `crates/erp-auth/src/service/auth_service.rs:193-197` — refresh 中改用原子操作
|
||||
- Create: `crates/erp-server/tests/integration/auth_concurrent_tests.rs`
|
||||
|
||||
**设计说明:** JWT claims 中没有 token 数据库 ID(`id` 列),只有 `sub`(user_id) 和 `tid`(tenant_id)。因此原子 CAS 应该使用 `token_hash` 作为匹配条件——先用 JWT 解码获取原始 token,计算 SHA-256 哈希,再用 `UPDATE WHERE token_hash = ? AND revoked_at IS NULL` 做原子操作。这样不需要修改 JWT 结构。
|
||||
|
||||
- [ ] **Step 1: 在 token_service.rs 新增 `revoke_by_hash_atomic` 方法**
|
||||
|
||||
在 `crates/erp-auth/src/service/token_service.rs` 第 176 行(`revoke_token` 方法之后)新增:
|
||||
|
||||
```rust
|
||||
/// 原子操作:通过 token_hash 验证并撤销 refresh token。
|
||||
/// 如果 token 已被撤销(rows_affected == 0),返回 AuthError::TokenRevoked。
|
||||
pub async fn revoke_by_hash_atomic(
|
||||
db: &DatabaseConnection,
|
||||
token_hash: &str,
|
||||
user_id: Uuid,
|
||||
) -> AuthResult<()> {
|
||||
use user_token::Entity as UserToken;
|
||||
let result = UserToken::update_many()
|
||||
.col_expr(
|
||||
user_token::Column::RevokedAt,
|
||||
sea_orm::sea_query::Expr::value(Some(chrono::Utc::now().naive_utc())),
|
||||
)
|
||||
.filter(user_token::Column::TokenHash.eq(token_hash))
|
||||
.filter(user_token::Column::UserId.eq(user_id))
|
||||
.filter(user_token::Column::RevokedAt.is_null())
|
||||
.exec(db)
|
||||
.await
|
||||
.map_err(|e| AuthError::Validation(e.to_string()))?;
|
||||
if result.rows_affected == 0 {
|
||||
return Err(AuthError::TokenRevoked);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
需要新增导入:`use sea_orm::sea_query::Expr;`(参考 `consultation_service.rs:683` 的模式)
|
||||
|
||||
- [ ] **Step 2: 改造 auth_service.rs 的 refresh 流程**
|
||||
|
||||
在 `crates/erp-auth/src/service/auth_service.rs:193-197`,将当前的 validate + revoke 两步替换:
|
||||
|
||||
```rust
|
||||
// 旧代码(第 193-197 行):
|
||||
// let claims = TokenService::validate_refresh_token(&self.token, &self.db).await?;
|
||||
// TokenService::revoke_token(&self.db, &claims.token_id, claims.user_id).await?;
|
||||
|
||||
// 新代码:
|
||||
// 1. JWT 解码获取 claims(不查数据库)
|
||||
let claims = TokenService::decode_refresh_token(&self.token)?;
|
||||
// 2. 计算 token 的 SHA-256 哈希
|
||||
let token_hash = TokenService::hash_token(&self.token);
|
||||
// 3. 原子操作:通过 hash 验证 + 撤销(CAS)
|
||||
TokenService::revoke_by_hash_atomic(&self.db, &token_hash, claims.sub.parse()?).await?;
|
||||
// 4. 后续:查询用户角色权限(第 200-201 行,不变)
|
||||
```
|
||||
|
||||
注意:需要确认 `decode_refresh_token`(仅 JWT 解码)和 `hash_token`(SHA-256 计算)是否已是公开方法。如果 `validate_refresh_token` 内部已有这些逻辑,需要拆分为独立方法。
|
||||
|
||||
- [ ] **Step 3: 编译检查**
|
||||
|
||||
Run: `cargo check --package erp-auth`
|
||||
Expected: 编译通过,无错误
|
||||
|
||||
- [ ] **Step 4: 写并发刷新测试**
|
||||
|
||||
在 `crates/erp-server/tests/integration/auth_concurrent_tests.rs` 中:
|
||||
|
||||
```rust
|
||||
use crate::test_db::TestDb;
|
||||
use erp_auth::service::auth_service::AuthService;
|
||||
use erp_core::config::JwtConfig;
|
||||
|
||||
async fn setup_test_user(db: &TestDb) -> (Uuid, String, String) {
|
||||
// 创建测试用户,返回 (user_id, access_token, refresh_token)
|
||||
// 复用现有集成测试中的用户创建逻辑
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_refresh_rotates_token() {
|
||||
let db = TestDb::new().await;
|
||||
let (_user_id, _, refresh_token) = setup_test_user(&db).await;
|
||||
let jwt_config = JwtConfig::default();
|
||||
|
||||
let svc = AuthService::new(db.conn(), &jwt_config);
|
||||
// 第一次 refresh → 成功
|
||||
let result = svc.refresh(&refresh_token).await;
|
||||
assert!(result.is_ok(), "第一次 refresh 应成功");
|
||||
let new_tokens = result.unwrap();
|
||||
// 用旧 refresh_token 再次 refresh → 必须失败
|
||||
let result2 = svc.refresh(&refresh_token).await;
|
||||
assert!(result2.is_err(), "旧 token 必须不可用");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_concurrent_refresh_token_reuse() {
|
||||
let db = TestDb::new().await;
|
||||
let (_user_id, _, refresh_token) = setup_test_user(&db).await;
|
||||
let jwt_config = JwtConfig::default();
|
||||
|
||||
let svc = AuthService::new(db.conn(), &jwt_config);
|
||||
let token_clone = refresh_token.clone();
|
||||
let svc_clone = // 需要确认 AuthService 是否可 Clone 或用 Arc
|
||||
|
||||
// 使用 tokio::spawn 并发发两个 refresh
|
||||
let handle1 = tokio::spawn(async move { svc.refresh(&refresh_token).await });
|
||||
let handle2 = tokio::spawn(async move { svc_clone.refresh(&token_clone).await });
|
||||
|
||||
let r1 = handle1.await.unwrap();
|
||||
let r2 = handle2.await.unwrap();
|
||||
|
||||
// 恰好一个成功、一个失败
|
||||
let ok_count = [&r1, &r2].iter().filter(|r| r.is_ok()).count();
|
||||
assert_eq!(ok_count, 1, "并发 refresh 中恰好一个成功,另一个失败");
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: 运行全部认证测试**
|
||||
|
||||
Run: `cargo test --package erp-auth`
|
||||
Expected: 全部通过
|
||||
|
||||
Run: `cargo test --package erp-server --test integration auth`
|
||||
Expected: 全部通过
|
||||
|
||||
Run: `cargo test --package erp-server --test integration auth_concurrent -- --nocapture`
|
||||
Expected: 两个测试 PASS
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add crates/erp-auth/src/service/token_service.rs \
|
||||
crates/erp-auth/src/service/auth_service.rs \
|
||||
crates/erp-server/tests/integration/auth_concurrent_tests.rs
|
||||
git commit -m "fix(auth): 修复 Token 刷新并发竞态条件
|
||||
|
||||
使用原子 CAS(UPDATE WHERE token_hash = ? AND revoked_at IS NULL)
|
||||
替代先查后改的非原子操作,防止同一 refresh token 被并发使用两次。"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Chunk 2: 演示链路验证
|
||||
|
||||
### Task 2: 验证告警链路(场景 5 依赖)
|
||||
|
||||
**Files:**
|
||||
- Verify: `crates/erp-health/src/service/seed.rs` — 确认告警规则
|
||||
- Verify: `apps/web/src/pages/health/AlertDashboard.tsx:51` — 确认权限码
|
||||
- Verify: `crates/erp-health/src/handler/alert_handler.rs:82-115` — 确认操作端点
|
||||
|
||||
- [ ] **Step 1: 启动后端服务**
|
||||
|
||||
Run: `cd crates/erp-server && cargo run`
|
||||
Expected: 服务无 panic 启动在 localhost:3000
|
||||
|
||||
- [ ] **Step 2: 查询已有告警规则**
|
||||
|
||||
Run: `curl -s -H "Authorization: Bearer $ADMIN_TOKEN" http://localhost:3000/api/v1/health/alert-rules | jq '.data.items[] | {name, metric, operator, threshold}'`
|
||||
|
||||
Expected: 返回 10 条默认规则,包括:
|
||||
- 收缩压偏高 (>=140)
|
||||
- 收缩压危急 (>=180)
|
||||
|
||||
场景 5 需要"张大爷录入血压 168 触发告警"→ 使用已有的"收缩压危急 >=180"不够,需要调整场景 5 话术用血压 185,或添加一条 >=160 的规则。
|
||||
|
||||
- [ ] **Step 3: 手动测试告警触发**
|
||||
|
||||
```
|
||||
1. 以 nurse1 登录 Web
|
||||
2. 找到张大爷患者详情页
|
||||
3. 录入体征:收缩压 185 / 舒张压 95
|
||||
4. 切到告警仪表盘页面
|
||||
5. 确认出现告警条目
|
||||
6. 点击「确认」→ 状态变为已确认
|
||||
7. 点击「处理」→ 输入备注 → 状态变为已处理
|
||||
```
|
||||
|
||||
Expected: 全流程无 403、无 500
|
||||
|
||||
- [ ] **Step 4: 记录验证结果**
|
||||
|
||||
在文件头部注释验证结果。如果告警权限码正确(`health.alerts.manage`),记录为 ✅。
|
||||
如果发现任何问题,记录具体报错信息,新建 Task 修复。
|
||||
|
||||
---
|
||||
|
||||
### Task 3: 验证 AI 分析触发(场景 2 依赖)
|
||||
|
||||
**Files:**
|
||||
- Verify: `apps/web/src/pages/health/components/LabReportsTab.tsx:177-183`
|
||||
- Verify: `apps/web/src/api/ai/analysisSse.ts`
|
||||
|
||||
- [ ] **Step 1: 确认 Ollama 模型就绪**
|
||||
|
||||
Run: `ollama list`
|
||||
Expected: 输出包含 `qwen3:4b`
|
||||
|
||||
如果没有:`ollama pull qwen3:4b`
|
||||
|
||||
- [ ] **Step 2: 手动触发 AI 分析**
|
||||
|
||||
```
|
||||
1. 以 admin 登录 Web
|
||||
2. 进入张大爷患者详情页
|
||||
3. 切到「化验报告」Tab
|
||||
4. 找到一条化验报告
|
||||
5. 点击「AI 解读」按钮
|
||||
6. 等待 SSE 流式输出
|
||||
```
|
||||
|
||||
Expected: AI 分析结果流式显示,无 500 错误
|
||||
|
||||
如果 AI 解读按钮不存在或化验报告为空 → 使用预案(预置截图),在脚本中标注
|
||||
|
||||
- [ ] **Step 3: 预置 AI 分析截图(预案)**
|
||||
|
||||
如果 AI 分析成功:截图保存到 `docs/demo/screenshots/ai-analysis.png`
|
||||
如果 AI 分析失败:在实施计划中标注使用备用话术
|
||||
|
||||
---
|
||||
|
||||
### Task 4: 验证 health_manager 测试账号
|
||||
|
||||
**Files:**
|
||||
- Verify: 数据库 `users` 表
|
||||
- Reference: `crates/erp-server/migration/src/m20260506_000125_restructure_menus_and_roles.rs:123-126`
|
||||
|
||||
- [ ] **Step 1: 查询 health_manager 角色是否存在**
|
||||
|
||||
Run: `docker exec erp-postgres psql -U erp -c "SELECT id, name, code FROM roles WHERE code = 'health_manager'"`
|
||||
|
||||
Expected: 返回 1 行
|
||||
|
||||
- [ ] **Step 2: 查询是否有测试用户关联此角色**
|
||||
|
||||
Run: `docker exec erp-postgres psql -U erp -c "SELECT u.username, r.code FROM users u JOIN user_roles ur ON u.id = ur.user_id JOIN roles r ON ur.role_id = r.id WHERE r.code = 'health_manager'"`
|
||||
|
||||
Expected: 返回至少 1 个用户
|
||||
|
||||
如果没有用户:需要通过 Web 管理界面创建一个 `health_mgr` 用户并分配 `health_manager` 角色
|
||||
|
||||
- [ ] **Step 3: 验证 health_manager 用户可登录**
|
||||
|
||||
用 health_manager 用户名 + `Admin@2026` 密码尝试登录 Web 端。
|
||||
Expected: 成功登录,工作台显示「任务工作台」
|
||||
|
||||
---
|
||||
|
||||
## Chunk 3: 演示数据预置
|
||||
|
||||
### Task 5: 编写演示数据预置脚本
|
||||
|
||||
**Files:**
|
||||
- Create: `scripts/demo-seed.sql`
|
||||
|
||||
脚本目标:一键预置以下数据(幂等,可重复执行):
|
||||
|
||||
- 张建国患者档案 + 2 份历史化验单(肌酐 88→102)
|
||||
- 20-30 个背景患者(让仪表盘有数据)
|
||||
- 若干随访任务/告警记录(让仪表盘统计有意义)
|
||||
- 3 篇 CKD 健康科普文章
|
||||
- 收缩压 >=160 告警规则(如 seed 中没有)
|
||||
|
||||
- [ ] **Step 1: 编写 SQL 脚本骨架**
|
||||
|
||||
在 `scripts/demo-seed.sql` 中:
|
||||
|
||||
```sql
|
||||
-- HMS V1 Demo Data Seed
|
||||
-- 用法: docker exec -i erp-postgres psql -U erp < scripts/demo-seed.sql
|
||||
-- 幂等:使用 ON CONFLICT DO NOTHING
|
||||
|
||||
-- 1. 确保租户 ID(从现有租户获取)
|
||||
-- 2. 张建国患者档案
|
||||
-- 3. 2 份历史化验单(3 个月前 肌酐 88,1 个月前 肌酐 102)
|
||||
-- 4. 20 个背景患者(随机姓名,基础体征数据)
|
||||
-- 5. 若干随访任务(不同状态:pending/completed)
|
||||
-- 6. 若干告警记录(不同状态:pending/acknowledged/resolved)
|
||||
-- 7. 3 篇 CKD 科普文章
|
||||
-- 8. 收缩压 >=160 告警规则
|
||||
```
|
||||
|
||||
注意:所有 INSERT 需包含 `tenant_id`、`created_at`、`updated_at`、`created_by`、`updated_by`、`version`、`id`(UUID v7)字段。参考现有 Entity 的字段结构。
|
||||
|
||||
- [ ] **Step 2: 编写张建国患者 + 化验单数据**
|
||||
|
||||
```sql
|
||||
-- 患者档案
|
||||
INSERT INTO patients (id, tenant_id, name, gender, birth_date, phone, ...)
|
||||
VALUES (
|
||||
'019dcd34-bc4d-72c1-8c19-77ce1f4839d6', -- 使用已知测试患者 ID
|
||||
(SELECT id FROM tenants LIMIT 1),
|
||||
'张建国', 'male', '1961-03-15', '13800138001', ...
|
||||
) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 化验单 1:3 个月前 肌酐 88
|
||||
INSERT INTO lab_reports (...) VALUES (...) ON CONFLICT (id) DO NOTHING;
|
||||
-- 化验单 1 的 items:肌酐 88 μmol/L
|
||||
INSERT INTO lab_report_items (...) VALUES (...) ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 化验单 2:1 个月前 肌酐 102
|
||||
INSERT INTO lab_reports (...) VALUES (...) ON CONFLICT (id) DO NOTHING;
|
||||
-- 化验单 2 的 items:肌酐 102 μmol/L
|
||||
INSERT INTO lab_report_items (...) VALUES (...) ON CONFLICT (id) DO NOTHING;
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 编写背景患者批量数据**
|
||||
|
||||
使用 SQL generate_series 生成 20-30 个虚拟患者:
|
||||
|
||||
```sql
|
||||
INSERT INTO patients (id, tenant_id, name, gender, birth_date, ...)
|
||||
SELECT
|
||||
gen_random_uuid(),
|
||||
(SELECT id FROM tenants LIMIT 1),
|
||||
'测试患者' || i,
|
||||
CASE WHEN i % 2 = 0 THEN 'male' ELSE 'female' END,
|
||||
CURRENT_DATE - (30 + (i * 37) % 50) * INTERVAL '1 year',
|
||||
...
|
||||
FROM generate_series(1, 25) AS i
|
||||
ON CONFLICT DO NOTHING;
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 编写随访任务和告警记录**
|
||||
|
||||
为背景患者生成不同状态的随访任务和告警记录,让仪表盘统计有意义。
|
||||
|
||||
- [ ] **Step 5: 编写科普文章和告警规则**
|
||||
|
||||
```sql
|
||||
-- 3 篇 CKD 科普文章
|
||||
INSERT INTO articles (title, content, category, status, ...) VALUES
|
||||
('慢性肾病患者的饮食指南', '...', 'nutrition', 'published', ...),
|
||||
('CKD 患者运动建议', '...', 'exercise', 'published', ...),
|
||||
('慢性肾病常用药物说明', '...', 'medication', 'published', ...)
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- 收缩压 >=160 告警规则(如果 seed 中没有)
|
||||
INSERT INTO alert_rules (name, metric, operator, threshold, ...)
|
||||
VALUES ('收缩压偏高(演示用)', 'systolic_bp', '>=', 160, ...)
|
||||
ON CONFLICT DO NOTHING;
|
||||
```
|
||||
|
||||
- [ ] **Step 6: 执行脚本验证**
|
||||
|
||||
Run: `docker exec -i erp-postgres psql -U erp < scripts/demo-seed.sql`
|
||||
Expected: 无错误,所有 INSERT 成功或 ON CONFLICT 跳过
|
||||
|
||||
- [ ] **Step 7: 验证数据完整性**
|
||||
|
||||
```
|
||||
1. 查询张建国患者:SELECT * FROM patients WHERE name = '张建国'
|
||||
2. 查询化验单数量:SELECT count(*) FROM lab_reports WHERE patient_id = ...
|
||||
3. 查询背景患者数:SELECT count(*) FROM patients WHERE name LIKE '测试患者%'
|
||||
4. 查询文章数:SELECT count(*) FROM articles WHERE status = 'published'
|
||||
Expected: 1 张建国 + 2 化验单 + 25 背景患者 + 3 文章
|
||||
```
|
||||
|
||||
- [ ] **Step 8: Commit**
|
||||
|
||||
```bash
|
||||
git add scripts/demo-seed.sql
|
||||
git commit -m "chore(demo): V1 演示数据预置脚本
|
||||
|
||||
一键预置张建国患者+化验单+25背景患者+随访+告警+科普文章。
|
||||
幂等设计,可重复执行。"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Chunk 4: 全链路 DRY RUN
|
||||
|
||||
### Task 6: 端到端 DRY RUN(7 个场景)
|
||||
|
||||
**前置条件:** Task 1-5 全部完成
|
||||
|
||||
- [ ] **Step 1: 环境启动检查**
|
||||
|
||||
```bash
|
||||
# 1. PostgreSQL
|
||||
docker exec erp-postgres pg_isready
|
||||
|
||||
# 2. 后端
|
||||
curl -s http://localhost:3000/api/v1/auth/health | jq .
|
||||
|
||||
# 3. Web 前端
|
||||
curl -s -o /dev/null -w "%{http_code}" http://localhost:5174
|
||||
|
||||
# 4. Ollama
|
||||
ollama list | grep qwen3
|
||||
```
|
||||
|
||||
Expected: 全部 200/ready
|
||||
|
||||
- [ ] **Step 2: 场景 1 — 护士建档**
|
||||
|
||||
登录 nurse1 → 新建患者/查找张建国 → 录入体征 → 查看化验报告
|
||||
Expected: 全流程无报错
|
||||
|
||||
- [ ] **Step 3: 场景 2 — AI 分析**
|
||||
|
||||
进入张建国化验报告 → 点击 AI 解读(或展示预置结果)
|
||||
Expected: AI 输出正常或截图备用
|
||||
|
||||
- [ ] **Step 4: 场景 3 — 医生审批**
|
||||
|
||||
登录 doctor1 → 查看 AI 建议 → 同意 → 查看随访任务
|
||||
Expected: 随访任务自动生成
|
||||
|
||||
- [ ] **Step 5: 场景 4 — 小程序**
|
||||
|
||||
打开小程序(开发者工具)→ 查看消息/随访 → 填写问卷 → 查看趋势
|
||||
Expected: 页面正常渲染,数据正确
|
||||
|
||||
- [ ] **Step 6: 场景 5 — 告警**
|
||||
|
||||
小程序录入血压 185/95 → Web nurse1 查看告警 → 确认 → 处理
|
||||
Expected: 告警实时出现,可操作
|
||||
|
||||
- [ ] **Step 7: 场景 6 — 随访**
|
||||
|
||||
登录 health_manager → 查看随访任务 → 执行 → 录入记录
|
||||
Expected: 随访完成,状态更新
|
||||
|
||||
- [ ] **Step 8: 场景 7 — 仪表盘**
|
||||
|
||||
登录 admin → 查看统计仪表盘 → 查看文章 → 查看积分
|
||||
Expected: 数据有意义(非零)
|
||||
|
||||
- [ ] **Step 9: 记录 DRY RUN 结果**
|
||||
|
||||
在 `docs/qa/demo-dry-run-results.md` 中记录每个场景的结果:
|
||||
- ✅ 通过 / ❌ 失败(附具体错误)
|
||||
- 阻塞问题 → 新建 Task 修复
|
||||
- 可跳过场景标注
|
||||
|
||||
- [ ] **Step 10: Commit DRY RUN 报告**
|
||||
|
||||
```bash
|
||||
git add docs/qa/demo-dry-run-results.md
|
||||
git commit -m "docs: V1 Demo DRY RUN 结果报告"
|
||||
```
|
||||
@@ -0,0 +1,346 @@
|
||||
# V1 客户演示方案设计规格
|
||||
|
||||
> 日期: 2026-05-09 | 状态: Draft v2 | 类型: 演示方案
|
||||
|
||||
## 1. 背景与目标
|
||||
|
||||
### 1.1 为什么做这次演示
|
||||
|
||||
HMS 健康管理平台已完成核心功能开发(700+ 次提交,V2 审计 85% 完成度),进入 V1 发布阶段。需要面向潜在客户(体检中心/血透中心)进行产品演示,目标是:
|
||||
|
||||
- **打动决策者签约** — 展示业务价值,而非功能清单
|
||||
- **收集真实反馈** — 了解客户实际工作流中的痛点,指导 V2 迭代
|
||||
- **验证产品定位** — 确认「AI 驱动主动关怀引擎」的定位是否与客户需求匹配
|
||||
|
||||
### 1.2 当前系统状态
|
||||
|
||||
| 指标 | 状态 |
|
||||
|------|------|
|
||||
| 核心链路 | 11 条端到端链路已验证通过 |
|
||||
| 已知 CRITICAL | 1 个未修复(Token 刷新竞态);其余 CRITICAL(告警权限码拼写、晚间血压丢失、仪表盘 500)均已修复 |
|
||||
| 角色测试通过率 | 84.6%(R01-R05) |
|
||||
| Web 前端 | 55 路由,283 文件,最完整的端 |
|
||||
| 小程序 | 59 页面,118 文件,代码完整 |
|
||||
| AI 模块 | 已对接 Ollama qwen3:4b,SSE 分析可用 |
|
||||
|
||||
### 1.3 演示策略
|
||||
|
||||
- **质量优先** — 修完所有已知问题再发布
|
||||
- **故事线驱动** — 用一个患者的 30 天管理历程展示完整闭环
|
||||
- **单患者深度** — 而非多角色广度,降低演示事故风险
|
||||
|
||||
## 2. 演示信息
|
||||
|
||||
| 项 | 值 |
|
||||
|------|------|
|
||||
| 受众 | 机构决策层 + 医疗团队 |
|
||||
| 时长 | 30-40 分钟 |
|
||||
| 视角 | 患者旅程(张大爷的 30 天) |
|
||||
| 涉及端 | Web 管理端(主力)+ 微信小程序(辅助) |
|
||||
| 涉及角色 | 护士、AI、医生、患者、健康管理师、管理员 |
|
||||
|
||||
## 3. 准备清单
|
||||
|
||||
### 3.1 测试账号
|
||||
|
||||
| 账号 | 角色 | 密码 | 用途 |
|
||||
|------|------|------|------|
|
||||
| `admin` | 管理员 | `Admin@2026` | 场景 7 仪表盘 |
|
||||
| `doctor1` | 医生 | `Admin@2026` | 场景 3 医生审批 |
|
||||
| `nurse1` | 护士 | `Admin@2026` | 场景 1 建档 + 场景 5 告警处理 |
|
||||
| `health_mgr` | 健康管理师 | `Admin@2026` | 场景 6 随访执行 |
|
||||
| `zhang_daye` | 患者(小程序) | 微信登录 | 场景 4/5 患者端操作 |
|
||||
|
||||
### 3.2 预置测试数据
|
||||
|
||||
| 数据 | 说明 | 目的 |
|
||||
|------|------|------|
|
||||
| 患者档案(张大爷) | 张建国,65岁,男,CKD 3期 | 主角 |
|
||||
| 历史化验单 ×2 | 肌酐 88→102 μmol/L 的趋势 | AI 分析需要历史对比发现趋势 |
|
||||
| 随访模板 | "慢性肾病定期随访"模板 | 场景 3 医生一键生成随访 |
|
||||
| 告警规则 | 肌酐>120 或 收缩压>160 | 场景 5 触发告警 |
|
||||
| 健康科普文章 ×3 | CKD 饮食/运动/用药 | 场景 4 小程序内容展示 |
|
||||
|
||||
### 3.3 环境检查
|
||||
|
||||
| 检查项 | 方法 | 通过标准 |
|
||||
|--------|------|----------|
|
||||
| 后端服务 | `cargo run` | 无 panic,Swagger 可访问 |
|
||||
| Web 前端 | `pnpm dev` | 登录页正常加载 |
|
||||
| 小程序 | 微信开发者工具 | 真机预览可扫码 |
|
||||
| 数据库 | 迁移已执行 | 预置数据查询无空结果 |
|
||||
| AI 模块 | Ollama 运行中 | SSE 分析端点可返回结果 |
|
||||
| 浏览器 | Chrome 无痕模式 | 干净环境,无缓存干扰 |
|
||||
|
||||
### 3.4 风险预案
|
||||
|
||||
| 风险 | 应对措施 |
|
||||
|------|----------|
|
||||
| AI 分析响应慢/失败 | 预先跑一次分析,截图备用;口头说明"云端大模型更快" |
|
||||
| 小程序真机扫码失败 | 准备 15 秒录屏视频展示关键页面 |
|
||||
| 后端服务崩溃 | 演示前重启一次确保干净状态 |
|
||||
| 数据库连接断开 | 提前验证 Docker PostgreSQL 健康状态 |
|
||||
| 告警权限码 bug | 演示前验证 AlertDashboard.tsx 权限码已修复(`health.alerts.manage`) |
|
||||
| SSE 长连接断开 | 录制 30 秒 AI 分析过程视频备用 |
|
||||
| Ollama 模型未加载 | 环境检查清单加入 `ollama list` 确认 qwen3:4b 已就绪 |
|
||||
| 多角色登录冲突 | 使用多个 Chrome Profile,每个角色一个独立 Profile |
|
||||
| 演示超时 | 标注可跳过场景(场景 6 可一句话带过) |
|
||||
|
||||
### 3.5 硬件与网络要求
|
||||
|
||||
| 项 | 要求 |
|
||||
|------|------|
|
||||
| 投影仪/大屏 | 分辨率 ≥ 1920x1080 |
|
||||
| 网络 | 演示机器与服务器在同一局域网,延迟 < 10ms |
|
||||
| 浏览器 | Chrome ×2 个 Profile(Web 端两个角色并行),或双屏方案 |
|
||||
| 手机 | 安装微信,可扫小程序码(备用:开发者工具投屏) |
|
||||
| 服务器 | 后端 + PostgreSQL + Ollama 运行在同一台机器,避免网络依赖 |
|
||||
|
||||
### 3.6 角色切换指引
|
||||
|
||||
| 切换点 | 操作 | 预计耗时 |
|
||||
|--------|------|----------|
|
||||
| 场景 1→2 | nurse1 退出 → admin 登录 | 15 秒 |
|
||||
| 场景 2→3 | admin 退出 → doctor1 登录 | 15 秒 |
|
||||
| 场景 3→4 | Web → 微信开发者工具/手机 | 10 秒 |
|
||||
| 场景 4→5 | 小程序录入 → Web nurse1 告警 | 10 秒 |
|
||||
| 场景 5→6 | nurse1 退出 → health_mgr 登录 | 15 秒 |
|
||||
| 场景 6→7 | health_mgr 退出 → admin 登录 | 15 秒 |
|
||||
|
||||
**建议:** 准备 2 个 Chrome Profile(Profile A: nurse1/admin,Profile B: doctor1/health_mgr),减少登录切换。场景 4/5 用独立手机或开发者工具。总切换时间约 1-1.5 分钟。
|
||||
|
||||
## 4. 演示脚本
|
||||
|
||||
### 开场(2 分钟)
|
||||
|
||||
**话术:**
|
||||
> "体检中心最大的痛点是什么?患者体检完,就走了。没有后续管理,没有随访跟进,体检数据躺在系统里没人看。今天给大家演示 HMS 健康管理平台如何解决这个问题——用一个真实场景:张大爷来体检后,系统如何帮他做 30 天的持续健康管理。"
|
||||
|
||||
---
|
||||
|
||||
### 场景 1:张大爷来体检(Day 1 上午)— 护士视角
|
||||
|
||||
**登录:** Web 端 `nurse1` | **时长:** ~4 分钟
|
||||
|
||||
**操作步骤:**
|
||||
1. 登录后展示护士工作台首页 — 一眼看到今日待办
|
||||
2. 点击「患者管理」→「新建患者」
|
||||
3. 填入:张建国 / 男 / 65岁 / 手机号 / 慢性肾病3期(诊断标签)
|
||||
4. 保存 → 跳转患者详情页
|
||||
5. 在患者详情页点击「体征录入」→ 录入血压 142/88、心率 72、空腹血糖 5.8
|
||||
6. 点击「化验报告」→ 上传预置的化验单图片,显示肌酐值 102 μmol/L
|
||||
|
||||
**话术:**
|
||||
> "张大爷第一次来体检中心。以前护士拿纸质表格登记,现在 30 秒建档。体征数据和化验报告立刻进入系统。"
|
||||
|
||||
**突出能力:** 快速建档、结构化体征录入、化验单数字化
|
||||
|
||||
---
|
||||
|
||||
### 场景 2:AI 自动分析(Day 1 下午)— 系统自动触发
|
||||
|
||||
**登录:** `admin` 或任意管理端账号 | **时长:** ~4 分钟
|
||||
|
||||
**操作步骤:**
|
||||
1. 展示「AI 分析」页面 → 显示张大爷的分析结果
|
||||
2. 点击分析详情 → 展示 AI 输出:
|
||||
- "肌酐值 88→102 μmol/L,3 个月持续上升趋势"
|
||||
- "建议:加做肾功能全套检查,排除 CKD 进展"
|
||||
- 风险等级:中风险(黄色标签)
|
||||
3. 切到 AI 建议列表 → 展示系统自动生成的「建议加做肾功能检查」行动项
|
||||
|
||||
**话术:**
|
||||
> "护士录入完数据,系统后台自动跑 AI 分析。不需要医生手动触发。AI 发现张大爷肌酐 3 个月在涨,主动建议进一步检查。这就是我们说的「主动关怀」——不是等患者出问题才看,是系统帮你盯着。"
|
||||
|
||||
**突出能力:** AI 自动分析、趋势发现、主动建议生成
|
||||
|
||||
**重要说明:** 当前 Web 端 AI 分析触发入口有限(审计报告指出"仅历史查看有 UI,分析触发无入口")。演示前**必须**执行以下操作之一:
|
||||
- 方案 A:演示前通过 API 手动触发一次分析(`POST /api/v1/ai/analysis/...`),演示时展示已生成的结果
|
||||
- 方案 B:为演示临时添加一个「触发分析」按钮到患者详情页
|
||||
- 推荐方案 A,配合话术调整:"这是系统刚才自动生成的分析结果"
|
||||
|
||||
**预案:** AI 分析慢或失败 → 展示预置截图,口头说明"接入云端大模型后速度更快"
|
||||
|
||||
---
|
||||
|
||||
### 场景 3:医生一秒决策(Day 3)— 医生视角
|
||||
|
||||
**登录:** Web 端 `doctor1` | **时长:** ~5 分钟
|
||||
|
||||
**操作步骤:**
|
||||
1. 展示医生工作台 → 待办区域显示"1 条 AI 建议待审批"
|
||||
2. 点击进入 → 查看 AI 分析详情 + 患者历史数据
|
||||
3. 点击「同意建议」→ 系统自动:
|
||||
- 生成随访任务("肾功能复查随访",2 周后到期)
|
||||
- 推送小程序消息给患者
|
||||
4. 展示随访任务列表 → 新任务已创建
|
||||
5. 点击「预约管理」→ 演示为张大爷预约复查(选医生、选时间段、确认)
|
||||
|
||||
**话术:**
|
||||
> "李医生早上打开系统,看到 AI 昨天的分析建议。以前要翻纸质报告、手动比对数据,现在 AI 已经帮你分析好了,医生只需要做一个决策:同意还是不同意。点一下,系统自动安排随访、自动通知患者。"
|
||||
|
||||
**突出能力:** AI 辅助决策、一键生成随访、自动通知患者
|
||||
|
||||
---
|
||||
|
||||
### 场景 4:张大爷在家收到提醒(Day 7)— 小程序视角
|
||||
|
||||
**操作:** 微信开发者工具或真机预览 | **时长:** ~4 分钟
|
||||
|
||||
**操作步骤:**
|
||||
1. 打开小程序首页 → 展示今日摘要:1 条随访待办 + 1 篇健康科普
|
||||
2. 点击「消息」Tab → 显示"您有一条新的随访任务"
|
||||
3. 点击进入随访详情 → 显示随访问卷(饮食情况、用药依从性、症状变化)
|
||||
4. 快速填写 2-3 项 → 提交
|
||||
5. 切回「健康」Tab → 展示张大爷的体征趋势图(血压曲线、肌酐趋势)
|
||||
6. 展示 AI 建议卡片:"您的血压近一周有上升趋势,建议减少盐分摄入"
|
||||
|
||||
**话术:**
|
||||
> "张大爷在家打开手机,不用打电话、不用跑医院,系统自动提醒他有随访要完成。填个问卷 2 分钟,医生那边就能看到。趋势图也让他自己看到身体变化,比口头解释直观得多。"
|
||||
|
||||
**突出能力:** 小程序主动提醒、随访问卷、趋势可视化、AI 健康建议触达
|
||||
|
||||
**预案:** 真机失败 → 播放 15 秒小程序录屏,重点展示随访提醒和趋势图
|
||||
|
||||
---
|
||||
|
||||
### 场景 5:危急值告警(Day 14)— 护士 + 系统联动
|
||||
|
||||
**操作:** 先小程序,再切 Web 端 | **时长:** ~4 分钟
|
||||
|
||||
**操作步骤:**
|
||||
1. **小程序端**(快速操作):张大爷录入血压 168/95 → 提交
|
||||
2. **切到 Web 端**(`nurse1` 登录):
|
||||
- 顶部弹出告警通知 "危急值告警:张建国 收缩压 168mmHg"
|
||||
- 点击进入告警列表 → 红色高亮显示
|
||||
- 点击告警详情 → 展示:触发规则(收缩压>160)、当前值、历史趋势
|
||||
- 点击「确认」→ 状态变为"已确认"
|
||||
- 点击「处理」→ 录入处理备注:"已电话通知患者,建议立即到门诊"
|
||||
- 状态变为"已处理"
|
||||
3. **回到小程序端**:张大爷收到消息"您的血压偏高,李医生建议您尽快来院检查"
|
||||
|
||||
**话术:**
|
||||
> "张大爷在家量了个血压,168。以前这种情况没人知道,可能拖到下次复诊才发现。现在数据一传上来,护士工作站立刻弹告警。护士确认后打电话给患者,15 分钟内完成从发现到处理。这才是真正的「主动关怀」。"
|
||||
|
||||
**突出能力:** 实时告警、分级处理、跨端联动(小程序录入→Web 告警→小程序反馈)
|
||||
|
||||
---
|
||||
|
||||
### 场景 6:随访闭环(Day 21)— 健康管理师视角
|
||||
|
||||
**登录:** Web 端 `health_mgr` | **时长:** ~4 分钟
|
||||
|
||||
**操作步骤:**
|
||||
1. 展示健康管理师工作台 → 随访任务列表显示"张建国 - 肾功能复查随访 - 即将到期"
|
||||
2. 点击执行随访 → 选择"电话随访"
|
||||
3. 录入随访记录:
|
||||
- 患者状态:"已完成肾功能检查,肌酐降至 98"
|
||||
- 遵医行为:"按时服药,控制饮食"
|
||||
- 下一步:"继续观察,3 个月后复查"
|
||||
4. 提交 → 随访状态变为"已完成"
|
||||
5. 展示随访历史时间线 → Day 3 创建 → Day 7 问卷 → Day 21 电话随访,完整记录
|
||||
|
||||
**话术:**
|
||||
> "30 天的管理周期里,每一步都有记录。从 AI 发现问题、医生决策、患者问卷、到健康管理师电话回访,全部可追溯。卫健委来检查,一导出就是完整的健康管理档案。"
|
||||
|
||||
**突出能力:** 随访全流程记录、可追溯、健康管理闭环
|
||||
|
||||
---
|
||||
|
||||
### 场景 7:数据说话(Day 30)— 管理员视角
|
||||
|
||||
**登录:** Web 端 `admin` | **时长:** ~3 分钟
|
||||
|
||||
**操作步骤:**
|
||||
1. 展示运营仪表盘:
|
||||
- 本月管理患者数
|
||||
- 随访完成率
|
||||
- AI 分析覆盖率
|
||||
- 告警响应平均时间
|
||||
2. 展示趋势图:患者增长曲线、随访完成率趋势
|
||||
3. 切到「内容管理」→ 展示已发布的健康科普文章(阅读量、转发量)
|
||||
4. 切到「积分商城」→ 展示患者积分排行、兑换记录
|
||||
|
||||
**话术:**
|
||||
> "张大爷的故事不是个例。系统帮你管每一个患者,而且每一步都有数据。随访完成率从手工追踪的 40% 提升到系统化管理后能做到 80% 以上。这些数据就是你们向卫健委、向患者证明管理质量的最好证据。"
|
||||
|
||||
**突出能力:** 运营数据可视化、管理质量量化、内容运营
|
||||
|
||||
**重要说明:** 单个患者(张大爷)的数据不足以支撑仪表盘的说服力。演示前**必须**预置 20-30 个背景患者数据 + 若干随访/告警记录,让仪表盘显示有意义的统计数字。在数据预置脚本中一并处理。
|
||||
|
||||
---
|
||||
|
||||
### 收尾(5 分钟)
|
||||
|
||||
**总结话术:**
|
||||
> "总结一下 HMS 带来的三个核心变化:
|
||||
> 1. **从被动到主动** — AI 帮你看数据,系统帮你盯着患者
|
||||
> 2. **从纸质到数字** — 每一步可追溯,检查随时可导出
|
||||
> 3. **从单点到闭环** — 体检不是终点,30 天持续管理才是"
|
||||
|
||||
**收集反馈(3 个问题):**
|
||||
1. "您刚才看到的流程中,哪些环节对您机构最有价值?"
|
||||
2. "有没有我们没覆盖到、但您实际工作中很重要的场景?"
|
||||
3. "您更关心 Web 管理端还是患者小程序端的能力?"
|
||||
|
||||
---
|
||||
|
||||
## 5. V1 发布前必修项
|
||||
|
||||
### 5.1 必修(阻塞发布)
|
||||
|
||||
| # | 问题 | 修复方案 | 工作量估计 |
|
||||
|---|------|----------|-----------|
|
||||
| 1 | Token 刷新并发竞态 | refresh 流程加事务 + SELECT FOR UPDATE | 0.5 天 |
|
||||
|
||||
### 5.2 建议修(提升演示体验)
|
||||
|
||||
| # | 问题 | 说明 |
|
||||
|---|------|------|
|
||||
| 1 | AI 分析预置截图 | 演示前手动跑一次分析,截图备用 |
|
||||
| 2 | 小程序录屏视频 | 15 秒展示随访提醒 + 趋势图 |
|
||||
| 3 | 测试数据脚本 | 一键预置张大爷的完整数据 |
|
||||
| 4 | 演示前全链路冒烟 | 跑一遍 7 个场景确认无阻塞 |
|
||||
|
||||
## 6. 下一步演化方向(演示后收集)
|
||||
|
||||
| 方向 | 来源 | 说明 |
|
||||
|------|------|------|
|
||||
| HIS 系统集成 | 场景 1 | 演示后可能被问"能不能对接我们现有 HIS" |
|
||||
| 报告导出 | 场景 6 | 卫健委检查需要标准格式报告 |
|
||||
| 多科室支持 | 客户反馈 | 当前以肾病/体检为主,其他科室扩展 |
|
||||
| 微信服务号推送 | 场景 4 | 小程序消息触达有限,服务号更灵活 |
|
||||
| 设备直连 | 场景 5 | 血压计/血糖仪 BLE 直连小程序 |
|
||||
|
||||
## 7. Q&A 异议处理
|
||||
|
||||
### 客户可能提出的问题及建议回答
|
||||
|
||||
**Q: 能不能对接我们现有的 HIS/EMR 系统?**
|
||||
> HMS 提供标准 FHIR R4 接口和 RESTful API,支持 HL7 标准数据交换。具体集成方案需要了解贵院 HIS 的品牌和版本,我们可以安排技术团队做接口评估。通常 2-4 周可以完成基础对接。
|
||||
|
||||
**Q: 患者数据安全如何保障?**
|
||||
> 数据存储采用 PII 加密(姓名/身份证/手机号等敏感字段加密存储),多租户隔离确保不同机构数据完全独立。系统支持私有化部署,数据不出院。后端使用 Rust 语言开发,天然免疫内存安全漏洞。
|
||||
|
||||
**Q: AI 分析的准确率如何?**
|
||||
> 当前 AI 模块定位是「辅助筛查」,发现异常趋势后由医生做最终决策。不是替代医生诊断,而是帮医生从海量数据中找到需要关注的患者。所有 AI 建议都需要医生审批才生效。
|
||||
|
||||
**Q: 部署方式有哪些?**
|
||||
> 支持 SaaS(按年付费,我们运维)和私有化部署(一次性 + 年维护费,部署在客户服务器)。SaaS 适合快速上线,私有化适合数据合规要求高的机构。
|
||||
|
||||
**Q: 价格怎么算?**
|
||||
> 根据机构规模(管理患者数、医护账号数)定制方案。演示后我们可以根据贵院的具体需求出一份详细报价。
|
||||
|
||||
**Q: 医护人员需要培训多久?**
|
||||
> 系统设计遵循「零培训」理念——医生工作台只展示待办,护士录入界面跟纸质表单一样直观。通常 30 分钟上手,1 天熟练。我们提供远程培训和操作手册。
|
||||
|
||||
## 8. DRY RUN 计划
|
||||
|
||||
| 阶段 | 时间 | 内容 |
|
||||
|------|------|------|
|
||||
| D-7 | 演示前 7 天 | 修完 P0 问题(Token 刷新、AI 触发入口验证) |
|
||||
| D-5 | 演示前 5 天 | 编写数据预置脚本,预置张大爷完整数据 |
|
||||
| D-3 | 演示前 3 天 | 第一次 DRY RUN:完整走 7 个场景,记录阻塞点 |
|
||||
| D-2 | 演示前 2 天 | 修复 DRY RUN 发现的问题,预置 20-30 个背景患者数据 |
|
||||
| D-1 | 演示前 1 天 | 第二次 DRY RUN(带投影/网络),确认全链路无阻塞 |
|
||||
| D-Day | 演示当天 | 提前 1 小时启动环境,30 分钟前最终冒烟 |
|
||||
Reference in New Issue
Block a user