Compare commits

...

8 Commits

Author SHA1 Message Date
iven
c6c94ebb84 docs: HMS 功能思维导图 + 系统设计文档 + 演进路线图
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled
患者端/医护端/管理后台/平台技术能力思维导图 +
系统功能文档 + 演进路线图 + 设计思路
2026-05-03 19:32:39 +08:00
iven
ec87ae85cf docs(wiki): 全量 wiki 更新 — C1 晚间血压已修复标记 + 数据校正
- miniprogram.md: 晚间血压 CRITICAL 标记为已修复
- index.md: 症状导航表更新
- architecture/database/erp-core/erp-health/frontend/testing: 同步更新
2026-05-03 19:32:30 +08:00
iven
c208dcc6f5 docs(specs): 7 份设计规格 — 工作台/适老化/硬编码清理/项目分析
新增: 适老化小程序/Action Inbox/统一工作台/医生操作台/
硬编码清理/健康管理台/全项目深度分析报告
2026-05-03 19:32:25 +08:00
iven
d712ad78c3 docs: 审计报告(8 份) + 讨论记录(4 份)
审计报告: 基线快照/功能清单/后端完整性/事件系统/参数配置/
差距模式/错误处理/测试覆盖/审计总结报告
讨论记录: 设备管线/端到端测试/三端审计/工作台重构
2026-05-03 19:32:15 +08:00
iven
78c783d332 feat(miniprogram): 配置更新 + 家庭成员/设置页面优化
- dev.ts: 开发环境配置调整
- project.config.json: 自动化审计配置
- family-add: 添加家庭成员页面优化
- settings: 设置页面优化
- config/: 新增项目配置文件
2026-05-03 19:32:09 +08:00
iven
3e4baa38a6 feat(web): 透析 API + 积分账户组件 + 工作台 store + 统计页修复
- dialysis.ts: 新增透析管理 API 模块
- PointsAccountTab.tsx: 积分账户标签页组件
- workbenchStore.ts: 工作台状态管理
- StatisticsDashboard.tsx: 统计页空列表修复
- auth.test.ts: 修复权限码拼写 health.alert → health.alerts
- api.test.ts: API 契约测试
2026-05-03 19:32:00 +08:00
iven
70322e4132 feat(miniprogram): 医生端 API 服务层 — 7 个模块
新增医生端完整 API 调用层:alerts / appointment / consultation /
dashboard / followup / labReport / patient
2026-05-03 19:31:51 +08:00
iven
3412d807e3 fix(core): 跨 crate 小修复 — dto 合并、tracing 补全、死代码清理
- erp-ai: 删除孤立 dto.rs(已合并到子模块)
- erp-core: audit_service tracing 优化
- erp-health: points_handler 补充返回值、alert_engine 修正日志级别
- erp-plugin: host/data_handler/market_handler tracing 统一
- erp-dialysis/event: 移除无用 import
- erp-workflow/executor: tracing 格式统一
2026-05-03 19:31:46 +08:00
67 changed files with 7523 additions and 338 deletions

View File

@@ -2,6 +2,10 @@ import type { UserConfigExport } from '@tarojs/cli';
export default {
logger: { quiet: false },
mini: {},
mini: {
miniCssExtractPluginOption: {
ignoreOrder: true,
},
},
h5: {},
} satisfies UserConfigExport;

View File

@@ -3,13 +3,13 @@
"miniprogramRoot": "dist/",
"compileType": "miniprogram",
"setting": {
"urlCheck": false,
"urlCheck": true,
"automationAudits": true,
"es6": false,
"enhance": false,
"compileHotReLoad": true,
"postcss": false,
"minified": false,
"minified": true,
"bundle": false,
"minifyWXML": true
}

View File

@@ -83,7 +83,7 @@ export default function FamilyAdd() {
>
<View className='form-picker'>
<Text className='form-picker-text'>{RELATION_OPTIONS[relationIdx]}</Text>
<Text className='form-picker-arrow'>></Text>
<Text className='form-picker-arrow'>{'>'}</Text>
</View>
</Picker>
</View>
@@ -98,7 +98,7 @@ export default function FamilyAdd() {
>
<View className='form-picker'>
<Text className='form-picker-text'>{GENDER_OPTIONS[genderIdx]}</Text>
<Text className='form-picker-arrow'>></Text>
<Text className='form-picker-arrow'>{'>'}</Text>
</View>
</Picker>
</View>
@@ -114,7 +114,7 @@ export default function FamilyAdd() {
<Text className={`form-picker-text ${!birthDate ? 'placeholder' : ''}`}>
{birthDate || '请选择'}
</Text>
<Text className='form-picker-arrow'>></Text>
<Text className='form-picker-arrow'>{'>'}</Text>
</View>
</Picker>
</View>

View File

@@ -68,21 +68,21 @@ export default function Settings() {
<Text className='settings-icon-text'></Text>
</View>
<Text className='settings-label'></Text>
<Text className='settings-arrow'>></Text>
<Text className='settings-arrow'>{'>'}</Text>
</View>
<View className='settings-item' onClick={handleAbout}>
<View className='settings-icon'>
<Text className='settings-icon-text'></Text>
</View>
<Text className='settings-label'></Text>
<Text className='settings-arrow'>></Text>
<Text className='settings-arrow'>{'>'}</Text>
</View>
<View className='settings-item' onClick={handlePrivacy}>
<View className='settings-icon'>
<Text className='settings-icon-text'></Text>
</View>
<Text className='settings-label'></Text>
<Text className='settings-arrow'>></Text>
<Text className='settings-arrow'>{'>'}</Text>
</View>
</View>

View File

@@ -0,0 +1,39 @@
import { api } from '../request';
// ── Alerts (doctor view) ────────────────────────────
export interface Alert {
id: string;
patient_id: string;
rule_id: string;
severity: string;
title: string;
detail?: Record<string, unknown>;
status: string;
acknowledged_by?: string;
acknowledged_at?: string;
resolved_at?: string;
created_at: string;
version: number;
}
export async function listAlerts(params?: {
patient_id?: string;
status?: string;
page?: number;
page_size?: number;
}) {
return api.get<{ data: Alert[]; total: number }>('/health/alerts', params);
}
export async function acknowledgeAlert(id: string, version: number) {
return api.put<Alert>(`/health/alerts/${id}/acknowledge`, { version });
}
export async function dismissAlert(id: string, version: number) {
return api.put<Alert>(`/health/alerts/${id}/dismiss`, { version });
}
export async function resolveAlert(id: string, version: number) {
return api.put<Alert>(`/health/alerts/${id}/resolve`, { version });
}

View File

@@ -0,0 +1,12 @@
import { api } from '../request';
// ── Appointments (doctor view) ─────────────────────
export async function listAppointments(params?: {
page?: number;
page_size?: number;
status?: string;
date?: string;
}) {
return api.get<{ data: any[]; total: number }>('/health/appointments', params);
}

View File

@@ -0,0 +1,76 @@
import { api } from '../request';
// ── Consultation (doctor view) ─────────────────────
export interface ConsultationSession {
id: string;
patient_id: string;
patient_name?: string;
doctor_id: string | null;
consultation_type: string;
status: string;
subject: string | null;
last_message: string | null;
last_message_at: string | null;
unread_count_doctor?: number;
created_at: string;
}
export interface ConsultationMessage {
id: string;
session_id: string;
sender_id: string;
sender_role: string;
content_type: string;
content: string;
created_at: string;
}
export async function listSessions(params?: {
page?: number;
page_size?: number;
status?: string;
}) {
return api.get<{ data: ConsultationSession[]; total: number }>(
'/health/consultation-sessions',
params,
);
}
export async function getSession(id: string) {
return api.get<ConsultationSession>(`/health/consultation-sessions/${id}`);
}
export async function listMessages(sessionId: string, params?: { page?: number; page_size?: number; after_id?: string }) {
return api.get<{ data: ConsultationMessage[]; total: number }>(
`/health/consultation-sessions/${sessionId}/messages`,
params,
);
}
export async function sendMessage(sessionId: string, content: string, contentType = 'text') {
return api.post<ConsultationMessage>('/health/consultation-messages', {
session_id: sessionId,
content_type: contentType,
content,
});
}
export async function markSessionRead(sessionId: string) {
return api.put<void>(`/health/consultation-sessions/${sessionId}/read`);
}
export async function closeSession(sessionId: string) {
return api.put<void>(`/health/consultation-sessions/${sessionId}/close`);
}
export interface ConsultationStats {
total_sessions: number;
pending_reply: number;
avg_response_time_minutes?: number | null;
this_month: number;
}
export async function getConsultationStats() {
return api.get<ConsultationStats>('/health/admin/statistics/consultations');
}

View File

@@ -0,0 +1,17 @@
import { api } from '../request';
// ── Dashboard ──────────────────────────────────────
export interface DoctorDashboard {
total_patients: number;
active_sessions: number;
unread_messages: number;
pending_follow_ups: number;
today_consultations: number;
pending_lab_review: number;
today_appointments: number;
}
export async function getDashboard() {
return api.get<DoctorDashboard>('/health/doctor/dashboard');
}

View File

@@ -0,0 +1,70 @@
import { api } from '../request';
// ── Follow-up (doctor view) ────────────────────────
export interface FollowUpTask {
id: string;
patient_id: string;
patient_name?: string;
assigned_to?: string;
follow_up_type: string;
planned_date: string;
content_template?: string;
status: string;
created_at: string;
version: number;
}
export interface FollowUpRecord {
id: string;
task_id: string;
executed_by?: string;
executed_date: string;
result?: string;
patient_condition?: string;
medical_advice?: string;
next_follow_up_date?: string;
created_at: string;
}
export async function listFollowUpTasks(params?: {
page?: number;
page_size?: number;
status?: string;
patient_id?: string;
}) {
return api.get<{ data: FollowUpTask[]; total: number }>('/health/follow-up-tasks', params);
}
export async function getFollowUpTask(id: string) {
return api.get<FollowUpTask>(`/health/follow-up-tasks/${id}`);
}
export async function updateFollowUpTask(id: string, data: Record<string, unknown>, version: number) {
return api.put<FollowUpTask>(`/health/follow-up-tasks/${id}`, { ...data, version });
}
export async function createFollowUpRecord(taskId: string, data: {
result?: string;
patient_condition?: string;
medical_advice?: string;
next_follow_up_date?: string;
}) {
return api.post<FollowUpRecord>(`/health/follow-up-tasks/${taskId}/records`, { task_id: taskId, ...data });
}
export async function listFollowUpRecords(params?: { task_id?: string; page?: number }) {
return api.get<{ data: FollowUpRecord[]; total: number }>('/health/follow-up-records', params);
}
export interface FollowUpStats {
total_tasks: number;
completed: number;
pending: number;
overdue: number;
completion_rate?: number;
}
export async function getFollowUpStats() {
return api.get<FollowUpStats>('/health/admin/statistics/follow-ups');
}

View File

@@ -0,0 +1,49 @@
import { api } from '../request';
// ── Lab Report (doctor view) ───────────────────────
export interface LabReportItem {
id: string;
report_date: string;
report_type: string;
status: string;
abnormal_count?: number;
reviewed_by?: string;
reviewed_at?: string;
doctor_notes?: string;
version: number;
}
export interface LabReportDetail extends LabReportItem {
items?: {
name: string;
value: number;
unit?: string;
reference_min?: number;
reference_max?: number;
is_abnormal?: boolean;
}[];
image_urls?: string[];
}
export async function listLabReports(patientId: string, params?: { page?: number; page_size?: number }) {
return api.get<{ data: LabReportItem[]; total: number }>(
`/health/patients/${patientId}/lab-reports`,
params,
);
}
export async function getLabReport(patientId: string, reportId: string) {
return api.get<LabReportDetail>(`/health/patients/${patientId}/lab-reports/${reportId}`);
}
export async function reviewLabReport(
patientId: string,
reportId: string,
data: { doctor_notes?: string; version: number },
) {
return api.put<LabReportDetail>(
`/health/patients/${patientId}/lab-reports/${reportId}/review`,
data,
);
}

View File

@@ -0,0 +1,84 @@
import { api } from '../request';
// ── Patient (doctor view) ──────────────────────────
export interface PatientItem {
id: string;
name: string;
gender?: string;
birth_date?: string;
phone?: string;
status?: string;
tags?: { id: string; name: string; color?: string }[];
last_visit_date?: string;
version: number;
}
export interface PatientDetail extends PatientItem {
blood_type?: string;
allergy_history?: string;
medical_history_summary?: string;
emergency_contact_name?: string;
emergency_contact_phone?: string;
source?: string;
notes?: string;
}
export interface HealthSummary {
patient_id: string;
latest_vital_signs?: {
record_date: string;
systolic_bp?: number;
diastolic_bp?: number;
heart_rate?: number;
weight?: number;
blood_sugar?: number;
} | null;
latest_lab_report?: {
id: string;
report_date: string;
report_type: string;
abnormal_count?: number;
} | null;
pending_follow_ups?: number;
upcoming_appointments?: number;
}
export interface PatientTag {
id: string;
name: string;
color?: string;
is_system?: boolean;
}
export async function listPatients(params?: {
page?: number;
page_size?: number;
search?: string;
tag_id?: string;
}) {
return api.get<{ data: PatientItem[]; total: number }>('/health/patients', params);
}
export async function getPatient(id: string) {
return api.get<PatientDetail>(`/health/patients/${id}`);
}
export async function getHealthSummary(patientId: string) {
return api.get<HealthSummary>(`/health/patients/${patientId}/health-summary`);
}
export async function listPatientTags() {
return api.get<{ data: PatientTag[]; total: number }>('/health/patient-tags');
}
export interface PatientStats {
total_patients: number;
new_this_month: number;
new_this_week: number;
active_this_month: number;
}
export async function getPatientStats() {
return api.get<PatientStats>('/health/admin/statistics/patients');
}

View File

@@ -0,0 +1,172 @@
/**
* 健康模块新增 API 函数的契约测试
*
* 验证 dialysisApi / pointsAdminApi / healthDataApi 的日常监测与报告审核函数
* 是否调用了正确的 HTTP 方法、URL 路径和参数。
*/
import { describe, it, expect, vi, beforeEach } from 'vitest'
// --- Mock axios client ---
// 三个被测文件都 import client from '../client',相对路径一致
const mockGet = vi.fn()
const mockPost = vi.fn()
const mockPut = vi.fn()
const mockDelete = vi.fn()
vi.mock('../client', () => ({
default: {
get: (...args: unknown[]) => mockGet(...args),
post: (...args: unknown[]) => mockPost(...args),
put: (...args: unknown[]) => mockPut(...args),
delete: (...args: unknown[]) => mockDelete(...args),
},
}))
// 在 mock 生效后导入被测模块
import { dialysisApi } from './dialysis'
import { pointsAdminApi } from './points'
import { healthDataApi } from './healthData'
beforeEach(() => {
vi.clearAllMocks()
})
// ============================================================
// dialysisApi
// ============================================================
describe('dialysisApi', () => {
const fakeResponse = { data: { success: true, data: {} } }
it('listRecords 应调用 GET /health/patients/:id/dialysis-records 并传递分页参数', async () => {
mockGet.mockResolvedValue(fakeResponse)
await dialysisApi.listRecords('p-001', { page: 2, page_size: 20 })
expect(mockGet).toHaveBeenCalledTimes(1)
expect(mockGet).toHaveBeenCalledWith(
'/health/patients/p-001/dialysis-records',
{ params: { page: 2, page_size: 20 } },
)
})
it('getRecord 应调用 GET /health/dialysis-records/:id', async () => {
mockGet.mockResolvedValue(fakeResponse)
await dialysisApi.getRecord('rec-123')
expect(mockGet).toHaveBeenCalledWith('/health/dialysis-records/rec-123')
})
it('createRecord 应调用 POST /health/dialysis-records 并传递请求体', async () => {
mockPost.mockResolvedValue(fakeResponse)
const req = { patient_id: 'p-001', dialysis_date: '2026-04-30', dialysis_type: 'hemodialysis' }
await dialysisApi.createRecord(req)
expect(mockPost).toHaveBeenCalledWith('/health/dialysis-records', req)
})
it('updateRecord 应调用 PUT /health/dialysis-records/:id 并传递请求体', async () => {
mockPut.mockResolvedValue(fakeResponse)
const req = { dry_weight: 65.0, version: 3 }
await dialysisApi.updateRecord('rec-123', req)
expect(mockPut).toHaveBeenCalledWith('/health/dialysis-records/rec-123', req)
})
it('deleteRecord 应调用 DELETE /health/dialysis-records/:id 并在 body 中传递 version', async () => {
mockDelete.mockResolvedValue(undefined)
await dialysisApi.deleteRecord('rec-123', 3)
expect(mockDelete).toHaveBeenCalledWith('/health/dialysis-records/rec-123', {
data: { version: 3 },
})
})
it('reviewRecord 应调用 PUT /health/dialysis-records/:id/review', async () => {
mockPut.mockResolvedValue(fakeResponse)
const req = { version: 2, doctor_notes: '指标正常' }
await dialysisApi.reviewRecord('rec-456', req)
expect(mockPut).toHaveBeenCalledWith('/health/dialysis-records/rec-456/review', req)
})
})
// ============================================================
// pointsAdminApi
// ============================================================
describe('pointsAdminApi', () => {
const fakeResponse = { data: { success: true, data: {} } }
it('getPatientAccount 应调用 GET /health/admin/points/patients/:id/account', async () => {
mockGet.mockResolvedValue(fakeResponse)
await pointsAdminApi.getPatientAccount('p-001')
expect(mockGet).toHaveBeenCalledWith('/health/admin/points/patients/p-001/account')
})
it('listPatientTransactions 应调用 GET 并传递分页参数', async () => {
mockGet.mockResolvedValue(fakeResponse)
await pointsAdminApi.listPatientTransactions('p-001', { page: 1, page_size: 10 })
expect(mockGet).toHaveBeenCalledWith(
'/health/admin/points/patients/p-001/transactions',
{ params: { page: 1, page_size: 10 } },
)
})
})
// ============================================================
// healthDataApi — 日常监测 + 报告审核
// ============================================================
describe('healthDataApi 日常监测', () => {
const fakeResponse = { data: { success: true, data: {} } }
it('listDailyMonitoring 应调用 GET /health/patients/:id/daily-monitoring 并传递分页参数', async () => {
mockGet.mockResolvedValue(fakeResponse)
await healthDataApi.listDailyMonitoring('p-001', { page: 1, page_size: 15 })
expect(mockGet).toHaveBeenCalledWith(
'/health/patients/p-001/daily-monitoring',
{ params: { page: 1, page_size: 15 } },
)
})
it('createDailyMonitoring 应调用 POST /health/daily-monitoring 并传递请求体', async () => {
mockPost.mockResolvedValue(fakeResponse)
const req = {
patient_id: 'p-001',
record_date: '2026-04-30',
weight: 70.5,
blood_sugar: 5.2,
}
await healthDataApi.createDailyMonitoring(req)
expect(mockPost).toHaveBeenCalledWith('/health/daily-monitoring', req)
})
it('updateDailyMonitoring 应调用 PUT /health/daily-monitoring/:id 并传递请求体', async () => {
mockPut.mockResolvedValue(fakeResponse)
const req = { weight: 71.0, version: 1 }
await healthDataApi.updateDailyMonitoring('dm-123', req)
expect(mockPut).toHaveBeenCalledWith('/health/daily-monitoring/dm-123', req)
})
it('deleteDailyMonitoring 应调用 DELETE /health/daily-monitoring/:id 并在 body 中传递 version', async () => {
mockDelete.mockResolvedValue(undefined)
await healthDataApi.deleteDailyMonitoring('dm-123', 2)
expect(mockDelete).toHaveBeenCalledWith('/health/daily-monitoring/dm-123', {
data: { version: 2 },
})
})
it('reviewLabReport 应调用 PUT /health/patients/:pid/lab-reports/:rid/review', async () => {
mockPut.mockResolvedValue(fakeResponse)
const req = { version: 1, doctor_notes: '指标略有异常,建议复查' }
await healthDataApi.reviewLabReport('p-001', 'lr-456', req)
expect(mockPut).toHaveBeenCalledWith(
'/health/patients/p-001/lab-reports/lr-456/review',
req,
)
})
})

View File

@@ -0,0 +1,108 @@
import client from '../client';
import type { PaginatedResponse } from '../types';
// --- Types ---
export interface DialysisRecord {
id: string;
patient_id: string;
dialysis_date: string;
start_time?: string;
end_time?: string;
dry_weight?: number;
pre_weight?: number;
post_weight?: number;
pre_bp_systolic?: number;
pre_bp_diastolic?: number;
post_bp_systolic?: number;
post_bp_diastolic?: number;
pre_heart_rate?: number;
post_heart_rate?: number;
ultrafiltration_volume?: number;
dialysis_duration?: number;
blood_flow_rate?: number;
dialysis_type: string;
symptoms?: Record<string, unknown>;
complication_notes?: string;
status: string;
reviewed_by?: string;
reviewed_at?: string;
created_at: string;
updated_at: string;
version: number;
}
export interface CreateDialysisRecordReq {
patient_id: string;
dialysis_date: string;
start_time?: string;
end_time?: string;
dry_weight?: number;
pre_weight?: number;
post_weight?: number;
pre_bp_systolic?: number;
pre_bp_diastolic?: number;
post_bp_systolic?: number;
post_bp_diastolic?: number;
pre_heart_rate?: number;
post_heart_rate?: number;
ultrafiltration_volume?: number;
dialysis_duration?: number;
blood_flow_rate?: number;
dialysis_type?: string;
complication_notes?: string;
}
// --- API ---
export const dialysisApi = {
listRecords: async (
patientId: string,
params: { page?: number; page_size?: number },
) => {
const { data } = await client.get<{
success: boolean;
data: PaginatedResponse<DialysisRecord>;
}>(`/health/patients/${patientId}/dialysis-records`, { params });
return data.data;
},
getRecord: async (id: string) => {
const { data } = await client.get<{
success: boolean;
data: DialysisRecord;
}>(`/health/dialysis-records/${id}`);
return data.data;
},
createRecord: async (req: CreateDialysisRecordReq) => {
const { data } = await client.post<{
success: boolean;
data: DialysisRecord;
}>('/health/dialysis-records', req);
return data.data;
},
updateRecord: async (
id: string,
req: Partial<CreateDialysisRecordReq> & { version: number },
) => {
const { data } = await client.put<{
success: boolean;
data: DialysisRecord;
}>(`/health/dialysis-records/${id}`, req);
return data.data;
},
deleteRecord: async (id: string, version: number) => {
await client.delete(`/health/dialysis-records/${id}`, { data: { version } });
},
reviewRecord: async (id: string, req: { version: number; doctor_notes?: string }) => {
const { data } = await client.put<{
success: boolean;
data: Record<string, unknown>;
}>(`/health/dialysis-records/${id}/review`, req);
return data.data;
},
};

View File

@@ -4,12 +4,13 @@ import { NurseDashboard } from './StatisticsDashboard/NurseDashboard';
import { AdminDashboard } from './StatisticsDashboard/AdminDashboard';
import { OperatorDashboard } from './StatisticsDashboard/OperatorDashboard';
const DASHBOARD_MAP = {
const DASHBOARD_MAP: Record<string, React.FC> = {
doctor: DoctorDashboard,
health_manager: NurseDashboard,
nurse: NurseDashboard,
admin: AdminDashboard,
operator: OperatorDashboard,
} as const;
};
export default function StatisticsDashboard() {
const role = useDashboardRole();

View File

@@ -0,0 +1,141 @@
import { useCallback, useMemo, useState } from 'react';
import { Table, Tag, Statistic, Row, Col, Card, Empty } from 'antd';
import {
pointsAdminApi,
type PointsAccountDetail,
type PointsTransactionDetail,
} from '../../../api/health/points';
import { usePaginatedData } from '../../../hooks/usePaginatedData';
import { handleApiError } from '../../../api/client';
interface Props {
patientId: string;
}
const TYPE_MAP: Record<string, { color: string; label: string }> = {
earn: { color: 'green', label: '获得' },
spend: { color: 'orange', label: '消费' },
expire: { color: 'default', label: '过期' },
adjust: { color: 'blue', label: '调整' },
checkin: { color: 'cyan', label: '签到' },
};
export function PointsAccountTab({ patientId }: Props) {
const [account, setAccount] = useState<PointsAccountDetail | null>(null);
const [accountLoading, setAccountLoading] = useState(true);
const fetchTransactions = useCallback(
async (page: number, pageSize: number) => {
if (!account) {
try {
const acc = await pointsAdminApi.getPatientAccount(patientId);
setAccount(acc);
} catch (err) {
handleApiError(err, '加载积分账户失败');
} finally {
setAccountLoading(false);
}
}
return pointsAdminApi.listPatientTransactions(patientId, { page, page_size: pageSize });
},
[patientId, account],
);
const { data, total, page, loading, refresh } = usePaginatedData<PointsTransactionDetail>(
fetchTransactions,
10,
);
const columns = useMemo(
() => [
{
title: '时间',
dataIndex: 'created_at',
key: 'created_at',
width: 170,
render: (v: string) => new Date(v).toLocaleString('zh-CN'),
},
{
title: '类型',
dataIndex: 'transaction_type',
key: 'transaction_type',
width: 100,
render: (v: string) => {
const m = TYPE_MAP[v];
return m ? <Tag color={m.color}>{m.label}</Tag> : <Tag>{v}</Tag>;
},
},
{
title: '变动',
dataIndex: 'amount',
key: 'amount',
width: 100,
render: (v: number) => (
<span style={{ color: v > 0 ? '#52c41a' : v < 0 ? '#ff4d4f' : undefined }}>
{v > 0 ? `+${v}` : v}
</span>
),
},
{
title: '余额',
dataIndex: 'balance_after',
key: 'balance_after',
width: 80,
},
{
title: '说明',
dataIndex: 'description',
key: 'description',
ellipsis: true,
},
],
[],
);
return (
<div>
<Row gutter={16} style={{ marginBottom: 16 }}>
<Col span={6}>
<Card size="small" loading={accountLoading}>
<Statistic title="当前余额" value={account?.balance ?? 0} suffix="分" />
</Card>
</Col>
<Col span={6}>
<Card size="small" loading={accountLoading}>
<Statistic title="累计获得" value={account?.total_earned ?? 0} suffix="分" />
</Card>
</Col>
<Col span={6}>
<Card size="small" loading={accountLoading}>
<Statistic title="累计消费" value={account?.total_spent ?? 0} suffix="分" />
</Card>
</Col>
<Col span={6}>
<Card size="small" loading={accountLoading}>
<Statistic title="累计过期" value={account?.total_expired ?? 0} suffix="分" />
</Card>
</Col>
</Row>
{!account && !accountLoading ? (
<Empty description="暂无积分记录" />
) : (
<Table
columns={columns}
dataSource={data}
rowKey="id"
loading={loading}
size="small"
pagination={{
current: page,
total,
pageSize: 10,
onChange: (p) => refresh(p),
showTotal: (t) => `${t}`,
style: { margin: 0 },
}}
/>
)}
</div>
);
}

View File

@@ -85,7 +85,7 @@ function createFakeUser(overrides: Partial<UserInfo> = {}): UserInfo {
function createFakeLoginResponse(overrides: Partial<LoginResponse> = {}): LoginResponse {
return {
access_token: createFakeToken(['health.patient.list', 'health.alert.manage']),
access_token: createFakeToken(['health.patient.list', 'health.alerts.manage']),
refresh_token: 'refresh-token-xxx',
expires_in: 3600,
user: createFakeUser(),
@@ -157,7 +157,7 @@ describe('useAuthStore', () => {
expect(state.user).toEqual(fakeUser);
expect(state.isAuthenticated).toBe(true);
expect(state.loading).toBe(false);
expect(state.permissions).toEqual(['health.patient.list', 'health.alert.manage']);
expect(state.permissions).toEqual(['health.patient.list', 'health.alerts.manage']);
// API 被正确调用
expect(mockApiLogin).toHaveBeenCalledWith({ username: 'testuser', password: 'password123' });
@@ -283,7 +283,7 @@ describe('useAuthStore', () => {
// =========================================================================
describe('权限提取', () => {
it('登录后 permissions 应从 JWT token 中正确解析', async () => {
const permissions = ['health.patient.list', 'health.alert.manage', 'health.report.review'];
const permissions = ['health.patient.list', 'health.alerts.manage', 'health.report.review'];
const token = createFakeToken(permissions);
const fakeResponse = createFakeLoginResponse({ access_token: token });
mockApiLogin.mockResolvedValue(fakeResponse);

View File

@@ -0,0 +1,58 @@
import { create } from 'zustand';
import { actionInboxApi, type ActionItem, type WorkbenchStats } from '../api/health/actionInbox';
interface WorkbenchState {
tasks: ActionItem[];
selectedTaskId: string | null;
tab: 'pending' | 'completed';
loading: boolean;
stats: WorkbenchStats | null;
selectTask: (id: string | null) => void;
setTab: (tab: 'pending' | 'completed') => void;
refreshTasks: () => Promise<void>;
refreshStats: () => Promise<void>;
completeTask: (id: string) => void;
}
export const useWorkbenchStore = create<WorkbenchState>((set, get) => ({
tasks: [],
selectedTaskId: null,
tab: 'pending',
loading: false,
stats: null,
selectTask: (id) => set({ selectedTaskId: id }),
setTab: (tab) => {
set({ tab, selectedTaskId: null });
get().refreshTasks();
},
refreshTasks: async () => {
set({ loading: true });
try {
const status = get().tab === 'pending' ? 'pending' : 'completed';
const resp = await actionInboxApi.list({ status, page: 1, page_size: 50 });
const tasks = Array.isArray(resp?.data) ? resp.data : [];
set({ tasks, loading: false });
} catch {
set({ loading: false });
}
},
refreshStats: async () => {
try {
const stats = await actionInboxApi.stats();
set({ stats: stats ?? null });
} catch { /* ignore */ }
},
completeTask: (id) => {
const { tasks } = get();
const remaining = tasks.filter(t => t.id !== id);
const nextId = remaining.length > 0 ? remaining[0].id : null;
set({ tasks: remaining, selectedTaskId: nextId });
get().refreshStats();
},
}));

54
config/default.toml Normal file
View File

@@ -0,0 +1,54 @@
[server]
host = "0.0.0.0"
port = 3000
[database]
url = "__MUST_SET_VIA_ENV__"
max_connections = 20
min_connections = 5
[redis]
url = "__MUST_SET_VIA_ENV__"
[jwt]
secret = "__MUST_SET_VIA_ENV__"
access_token_ttl = "15m"
refresh_token_ttl = "7d"
[auth]
super_admin_password = "__MUST_SET_VIA_ENV__"
[log]
level = "info"
[cors]
# Comma-separated allowed origins. Use "*" for development only.
allowed_origins = "http://localhost:5173,http://localhost:5174,http://localhost:5175,http://localhost:5176,http://localhost:3000"
[wechat]
appid = "__MUST_SET_VIA_ENV__"
secret = "__MUST_SET_VIA_ENV__"
# dev_mode = true 跳过 jscode2session允许微信开发者工具模拟器登录
# 生产环境必须为 false默认
dev_mode = false
[health]
aes_key = "__MUST_SET_VIA_ENV__"
hmac_key = "__MUST_SET_VIA_ENV__"
[crypto]
kek = "__MUST_SET_VIA_ENV__"
[ai]
default_provider = "claude"
api_key = ""
base_url = "https://api.anthropic.com"
model = "claude-sonnet-4-6"
max_tokens = 2048
temperature = 0.3
cache_ttl_seconds = 604800
rate_limit_patient_daily = 10
[storage]
upload_dir = "./uploads"
max_file_size = "10MB"

View File

@@ -1,217 +0,0 @@
use serde::{Deserialize, Serialize};
// === 分析请求 ===
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnalyzeRequest {
pub analysis_type: AnalysisType,
pub source_ref: String,
pub options: AnalyzeOptions,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum AnalysisType {
LabReport,
Trends,
CheckupPlan,
ReportSummary,
}
impl AnalysisType {
pub fn as_str(&self) -> &str {
match self {
Self::LabReport => "lab_report",
Self::Trends => "trend",
Self::CheckupPlan => "checkup_plan",
Self::ReportSummary => "report_summary",
}
}
pub fn prompt_name(&self) -> &str {
match self {
Self::LabReport => "lab_report_interpretation",
Self::Trends => "health_trend_analysis",
Self::CheckupPlan => "personalized_checkup_plan",
Self::ReportSummary => "report_summary_generation",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnalyzeOptions {
pub detail_level: Option<String>,
pub language: Option<String>,
}
impl Default for AnalyzeOptions {
fn default() -> Self {
Self {
detail_level: Some("patient_friendly".into()),
language: Some("zh-CN".into()),
}
}
}
// === AI Provider 请求/响应 ===
#[derive(Debug, Clone)]
pub struct GenerateRequest {
pub system_prompt: String,
pub user_prompt: String,
pub model: String,
pub temperature: f32,
pub max_tokens: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GenerateResponse {
pub content: String,
pub model: String,
pub input_tokens: u32,
pub output_tokens: u32,
pub duration_ms: u64,
}
// === SSE 事件 ===
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TokenUsage {
pub input: u32,
pub output: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum AnalysisSseEvent {
#[serde(rename = "chunk")]
Chunk { content: String, index: u32 },
#[serde(rename = "metadata")]
Metadata {
model: String,
tokens: TokenUsage,
duration_ms: u64,
},
#[serde(rename = "done")]
Done {
analysis_id: uuid::Uuid,
status: String,
},
#[serde(rename = "error")]
Error { message: String },
}
#[cfg(test)]
mod tests {
use super::*;
// ---- AnalysisType::as_str ----
#[test]
fn analysis_type_as_str() {
assert_eq!(AnalysisType::LabReport.as_str(), "lab_report");
assert_eq!(AnalysisType::Trends.as_str(), "trend");
assert_eq!(AnalysisType::CheckupPlan.as_str(), "checkup_plan");
assert_eq!(AnalysisType::ReportSummary.as_str(), "report_summary");
}
// ---- AnalysisType::prompt_name ----
#[test]
fn analysis_type_prompt_name() {
assert_eq!(AnalysisType::LabReport.prompt_name(), "lab_report_interpretation");
assert_eq!(AnalysisType::Trends.prompt_name(), "health_trend_analysis");
assert_eq!(AnalysisType::CheckupPlan.prompt_name(), "personalized_checkup_plan");
assert_eq!(AnalysisType::ReportSummary.prompt_name(), "report_summary_generation");
}
// ---- AnalysisType serde round-trip ----
#[test]
fn analysis_type_serde_roundtrip() {
let types = vec![
AnalysisType::LabReport,
AnalysisType::Trends,
AnalysisType::CheckupPlan,
AnalysisType::ReportSummary,
];
for t in types {
let json = serde_json::to_string(&t).unwrap();
let back: AnalysisType = serde_json::from_str(&json).unwrap();
assert_eq!(t, back);
}
}
#[test]
fn analysis_type_deserialize_snake_case() {
let t: AnalysisType = serde_json::from_str("\"lab_report\"").unwrap();
assert_eq!(t, AnalysisType::LabReport);
let t: AnalysisType = serde_json::from_str("\"trends\"").unwrap();
assert_eq!(t, AnalysisType::Trends);
}
// ---- AnalyzeOptions::default ----
#[test]
fn analyze_options_default() {
let opts = AnalyzeOptions::default();
assert_eq!(opts.detail_level, Some("patient_friendly".to_string()));
assert_eq!(opts.language, Some("zh-CN".to_string()));
}
// ---- AnalysisSseEvent serde round-trip ----
#[test]
fn sse_event_chunk_roundtrip() {
let event = AnalysisSseEvent::Chunk {
content: "血红蛋白偏低".to_string(),
index: 0,
};
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains("\"type\":\"chunk\""));
let back: AnalysisSseEvent = serde_json::from_str(&json).unwrap();
match back {
AnalysisSseEvent::Chunk { content, index } => {
assert_eq!(content, "血红蛋白偏低");
assert_eq!(index, 0);
}
_ => panic!("期望 Chunk 变体"),
}
}
#[test]
fn sse_event_done_roundtrip() {
let id = {
let ts = uuid::Timestamp::now(uuid::NoContext);
uuid::Uuid::new_v7(ts)
};
let event = AnalysisSseEvent::Done {
analysis_id: id,
status: "completed".to_string(),
};
let json = serde_json::to_string(&event).unwrap();
let back: AnalysisSseEvent = serde_json::from_str(&json).unwrap();
match back {
AnalysisSseEvent::Done { analysis_id, status } => {
assert_eq!(analysis_id, id);
assert_eq!(status, "completed");
}
_ => panic!("期望 Done 变体"),
}
}
#[test]
fn sse_event_error_roundtrip() {
let event = AnalysisSseEvent::Error {
message: "超时".to_string(),
};
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains("\"type\":\"error\""));
let back: AnalysisSseEvent = serde_json::from_str(&json).unwrap();
match back {
AnalysisSseEvent::Error { message } => assert_eq!(message, "超时"),
_ => panic!("期望 Error 变体"),
}
}
}

View File

@@ -6,7 +6,7 @@
use std::time::Duration;
use erp_core::health_provider::{HealthDataProvider, TimeRange};
use sea_orm::{ColumnTrait, EntityTrait, FromQueryResult, QueryFilter, Statement};
use sea_orm::{EntityTrait, FromQueryResult, Statement};
use uuid::Uuid;
use crate::dto::AnalysisType;

View File

@@ -1,7 +1,7 @@
use crate::audit::AuditLog;
use crate::entity::audit_log;
use crate::request_info::RequestInfo;
use sea_orm::{ActiveModelTrait, ColumnTrait, ConnectionTrait, EntityTrait, QueryFilter, QueryOrder, Set};
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QueryOrder, Set};
use sha2::{Sha256, Digest};
use tracing;

View File

@@ -1,4 +1,3 @@
use erp_core::events::EventBus;
/// 预留事件处理器注册
pub fn register_handlers_with_state(_state: crate::state::DialysisState) {

View File

@@ -7,7 +7,7 @@ use uuid::Uuid;
use erp_core::error::AppError;
use erp_core::rbac::require_permission;
use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext};
use erp_core::types::{ApiResponse, TenantContext};
use crate::service::device_reading_service;
use crate::state::HealthState;

View File

@@ -452,6 +452,39 @@ where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
Ok(Json(ApiResponse::ok(result)))
}
// ---------------------------------------------------------------------------
// 管理端:按 patient_id 查询积分账户 + 流水
// ---------------------------------------------------------------------------
pub async fn admin_get_patient_account<S>(
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Path(patient_id): Path<Uuid>,
) -> Result<Json<ApiResponse<PointsAccountResp>>, AppError>
where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "health.points.list")?;
let result = points_service::get_account(&state, ctx.tenant_id, patient_id).await?;
Ok(Json(ApiResponse::ok(result)))
}
pub async fn admin_list_patient_transactions<S>(
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Path(patient_id): Path<Uuid>,
Query(params): Query<PaginationParams>,
) -> Result<Json<ApiResponse<PaginatedResponse<PointsTransactionResp>>>, AppError>
where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "health.points.list")?;
let page = params.page.unwrap_or(1);
let page_size = params.page_size.unwrap_or(20);
let result = points_service::list_transactions(
&state, ctx.tenant_id, patient_id, page, page_size,
).await?;
Ok(Json(ApiResponse::ok(result)))
}
// ---------------------------------------------------------------------------
// 辅助:通过 user_id 解析 patient_id
// ---------------------------------------------------------------------------

View File

@@ -1,12 +1,12 @@
use chrono::Utc;
use sea_orm::entity::prelude::*;
use sea_orm::{ActiveValue::Set, QueryOrder, QuerySelect};
use sea_orm::{ActiveValue::Set, QueryOrder};
use serde_json::json;
use std::collections::HashSet;
use uuid::Uuid;
use crate::entity::{alert_rules, alerts, device_readings, vital_signs_hourly};
use crate::error::{HealthError, HealthResult};
use crate::error::HealthResult;
use crate::state::HealthState;
/// 评估所有适用规则,返回触发的告警列表

View File

@@ -32,7 +32,7 @@ pub async fn list_reminders(
.await?
.ok_or(HealthError::PatientNotFound)?;
let mut query = medication_reminder::Entity::find()
let query = medication_reminder::Entity::find()
.filter(medication_reminder::Column::TenantId.eq(tenant_id))
.filter(medication_reminder::Column::PatientId.eq(patient_id))
.filter(medication_reminder::Column::DeletedAt.is_null());

View File

@@ -1061,7 +1061,7 @@ where
pub async fn delete_user_view<S>(
State(state): State<PluginState>,
Extension(ctx): Extension<TenantContext>,
Path((plugin_id, entity, view_id)): Path<(Uuid, String, Uuid)>,
Path((_plugin_id, _entity, view_id)): Path<(Uuid, String, Uuid)>,
) -> Result<Json<ApiResponse<()>>, AppError>
where
PluginState: FromRef<S>,

View File

@@ -210,7 +210,7 @@ where
).await?;
let plugin_id = plugin_resp.id;
let plugin_resp = crate::service::PluginService::install(
let _plugin_resp = crate::service::PluginService::install(
plugin_id,
ctx.tenant_id,
ctx.user_id,

View File

@@ -150,7 +150,7 @@ impl host_api::Host for HostState {
.ok_or_else(|| format!("实体 '{}' 的查询结果未预填充", entity));
}
let db = self.db.clone().unwrap();
let db = self.db.clone().ok_or("数据库连接不可用")?;
let event_bus = self.event_bus.clone()
.ok_or("事件总线不可用")?;
@@ -314,7 +314,7 @@ impl host_api::Host for HostState {
let db = self.db.clone()
.ok_or("编号生成需要数据库连接")?;
let tenant_id = self.tenant_id;
let _tenant_id = self.tenant_id;
let plugin_id = self.plugin_id.clone();
let rt = tokio::runtime::Handle::current();

View File

@@ -426,7 +426,7 @@ impl FlowExecutor {
.await
.map_err(|e| WorkflowError::Validation(e.to_string()))?;
for mut t in consumed_tokens {
for t in consumed_tokens {
let ver = t.version;
let mut active: token::ActiveModel = t.into();
active.status = Set("completed".to_string());

View File

@@ -0,0 +1,109 @@
# HMS 功能审计 — Phase 0: 基线快照
> 日期: 2026-04-30 | 审计范围: 全系统
## 环境信息
| 项目 | 值 |
|------|-----|
| Git HEAD | `84fafb0bc5bf3d742f4136c78288a3d06678fc4d` |
| 最新提交 | `fix(web+health): 修复咨询轮询 temp ID 400 + 健康数据统计 500` |
| 总提交数 | 409 |
## 编译状态
| 指标 | 值 |
|------|-----|
| `cargo check --workspace` | **通过**(有警告) |
| 编译警告总数 | 40 个 |
| 受影响 crate | erp-core(1), erp-plugin(11), erp-health(11), erp-ai(6), erp-dialysis(1), erp-workflow(2), erp-server(1) |
### 警告分类
**未使用字段9 处)**
| 文件 | 字段 | 状态 |
|------|------|------|
| erp-plugin `host.rs` | `chk`×2, `tenant_id`, `user_id` | #[allow(dead_code)] 已抑制 |
| erp-health 多处 | `message`, `usage`, `input_tokens`, `output_tokens`, `check_result`, `total` | 编译器警告 |
| erp-server `analytics.rs` | `timestamp` | 编译器警告 |
| erp-health | `RefRow` struct 从未构造 | 编译器警告 |
**未使用导入18 处)**:分布在 erp-health(7)、erp-plugin(3)、erp-ai(2)、erp-workflow(2)、erp-dialysis(1)、erp-core(1)。
**未使用变量7 处)**`user_id`, `total`, `tenant_id`, `plugin_resp`, `plugin_id`, `final_nodes`, `entity`
## 测试状态
### 总览
| 指标 | 值 |
|------|-----|
| 测试总数 | **772 个函数** |
| 通过 | **75397.5%** |
| 失败 | **91.2%** |
| 跳过 | 0 |
### 失败测试(全部因 `blind_indexes` 表缺失)
| 测试 | 文件 |
|------|------|
| `test_create_patient` | `health_patient_tests.rs` |
| `test_patient_pii_encrypted` | `health_patient_tests.rs` |
| `test_dialysis_create_without_patient_returns_error` | `health_dialysis_tests.rs` |
| `test_cross_tenant_data_integrity` | `health_pii_encryption_tests.rs` |
| `test_patient_detail_returns_decrypted_fields` | `health_pii_encryption_tests.rs` |
| `test_patient_hmac_search_by_phone` | `health_pii_encryption_tests.rs` |
| `test_patient_list_hides_tier1_fields` | `health_pii_encryption_tests.rs` |
| `test_patient_tier1_fields_encrypted_in_db` | `health_pii_encryption_tests.rs` |
| `test_tenant_isolation_encrypted_patient` | `health_pii_encryption_tests.rs` |
**根因**:测试数据库缺少 `blind_indexes` 表(迁移未执行),非代码逻辑错误。
### 模块测试分布
| Crate | 单元测试 | 集成测试 | 总计 | 通过率 |
|-------|---------|---------|------|--------|
| erp-core | 74 | — | 74 | 100% |
| erp-auth | 41 | 3 | 44 | 100% |
| erp-config | 78 | — | 78 | 100% |
| erp-workflow | 63 | 4 | 67 | 100% |
| erp-message | 72 | — | 72 | 100% |
| erp-health | 159 | 144 | 303 | 97% |
| erp-ai | 36 | — | 36 | 100% |
| erp-dialysis | 10 | 15 | 25 | 93% |
| erp-plugin | 78 | 2 | 80 | 100% |
| erp-server | — | 153 | 153 | 94% |
| **合计** | **611** | **153** | **772** | **97.5%** |
## 路由统计
| 模块 | 路由数 |
|------|--------|
| erp-health | 169 |
| erp-plugin | 38 |
| erp-config | 26 |
| erp-auth | 33含 4 个公开路由) |
| erp-workflow | 17 |
| erp-ai | 12 |
| erp-dialysis | 12 |
| erp-message | 13 |
| erp-server | 8含 4 个公开路由) |
| **合计** | **328**8 个公开 + 320 个受保护) |
## 代码规模
| 维度 | 值 |
|------|-----|
| Rust 源文件 | 462 个 |
| Rust 代码行数 | ~77,000 行 |
| Web 前端文件 | 163 个 |
| 小程序文件 | 125 个 |
| 数据库迁移 | 96 个 |
| Rust crate | 18 个 |
## 基线结论
1. **系统整体健康**编译通过97.5% 测试通过
2. **单一阻塞问题**9 个测试因 `blind_indexes` 表缺失失败,需执行迁移
3. **技术债务低**:仅 40 个编译警告(多为未使用导入/变量),无 `unimplemented!``todo!`
4. **规模庞大**233 个后端路由、40 个前端页面,审计工作量较大

View File

@@ -0,0 +1,252 @@
# HMS 功能审计 — Phase 1: 功能清单与路由映射
> 日期: 2026-04-30 | 审计范围: 后端 + Web + 小程序三端
## 总览
| 维度 | 数量 |
|------|------|
| 后端路由 | 328 个8 公开 + 320 受保护) |
| Web 前端 API 调用 | 235 个 |
| 小程序 API 调用 | 76 个 |
| Web 页面路由 | 38 个 |
| 小程序页面 | 40 个31 患者 + 9 医护) |
---
## 1. 模块路由分布
| 模块 | 后端路由 | Web API | 小程序 API | 状态 |
|------|---------|---------|-----------|------|
| erp-auth | 33 | 32 | 3 | 分叉正常Web=管理端MP=微信登录) |
| erp-health | 169 | 124 | 57 | 覆盖广泛 |
| erp-ai | 12 | 8 | 2 | Web 为主 |
| erp-dialysis | 12 | 6 | 0 | **MP 缺失** |
| erp-config | 26 | 26 | 0 | Web 专属(管理功能) |
| erp-workflow | 17 | 14 | 0 | Web 专属(管理功能) |
| erp-message | 13 | 8 | 0 | Web 专属 |
| erp-plugin | 38 | 35 | 0 | Web 专属 |
| erp-server | 8 | 1 | 1 | SSE + 健康检查 |
---
## 2. 三端对齐矩阵 — 健康模块(核心业务)
### 2.1 患者管理
| 后端路由 | Web | 小程序 | 状态 |
|----------|-----|--------|------|
| `GET /health/patients` | ✓ list | ✓ listPatients + getPatients | ALIGNED |
| `POST /health/patients` | ✓ create | ✓ createPatient | ALIGNED |
| `GET /health/patients/{id}` | ✓ get | ✓ getPatient | ALIGNED |
| `PUT /health/patients/{id}` | ✓ update | ✓ updatePatient | ALIGNED |
| `DELETE /health/patients/{id}` | ✓ delete | — | WEB-ONLY预期 |
| `POST /health/patients/{id}/tags` | ✓ manageTags | — | WEB-ONLY预期 |
| `GET /health/patient-tags` | ✓ listTags | ✓ listPatientTags | ALIGNED |
| `POST/PUT/DELETE /health/patient-tags` | ✓ CRUD | — | WEB-ONLY预期 |
| `GET /health/patients/{id}/health-summary` | — | ✓ getHealthSummary | **MP-ONLY** |
| `GET /health/patients/{id}/family-members` | ✓ list | — | WEB-ONLY |
| `POST/PUT/DELETE .../family-members` | ✓ CRUD | ✓ (pages) | MP 有独立页面 |
| `POST .../doctors` (assign) | ✓ | — | WEB-ONLY |
| `DELETE .../doctors/{did}` | ✓ | — | WEB-ONLY |
### 2.2 健康数据
| 后端路由 | Web | 小程序 | 状态 |
|----------|-----|--------|------|
| `GET/POST/PUT/DELETE .../vital-signs` | ✓ 全 CRUD | ✓ 仅 createinputVitalSign | 差异正常 |
| `GET /health/vital-signs/today` | — | ✓ getTodaySummary | **MP-ONLY** |
| `GET /health/vital-signs/trend` | — | ✓ getTrend | **MP-ONLY** |
| `GET .../trends` | ✓ listTrends | — | WEB-ONLY |
| `GET .../trends/{indicator}` | ✓ timeseries | — | WEB-ONLY |
| `POST .../trends/generate` | ✓ | — | **ORPHAN**(未见前端调用) |
| `GET/POST .../lab-reports` | ✓ 全 CRUD | ✓ list + get | MP 只读 |
| `PUT .../lab-reports/{id}/review` | ✓ review | ✓ review医护端 | ALIGNED |
| `GET/POST/PUT/DELETE .../health-records` | ✓ 全 CRUD | — | **WEB-ONLY** |
| `GET/POST/PUT/DELETE .../diagnoses` | ✓ 全 CRUD | — | **WEB-ONLY** |
| `GET/POST/PUT/DELETE .../daily-monitoring` | ✓ 全 CRUD | ✓ create + list | MP 仅创建+列表 |
| `GET/POST/PUT/DELETE .../medications` | ✓ 全 CRUD | — | **WEB-ONLY** |
| `GET/POST/PUT/DELETE .../medication-reminders` | ✓ 全 CRUD | — | **WEB-ONLY** |
### 2.3 预约管理
| 后端路由 | Web | 小程序 | 状态 |
|----------|-----|--------|------|
| `GET /health/appointments` | ✓ list | ✓ list + doctor list | ALIGNED |
| `POST /health/appointments` | ✓ create | ✓ create | ALIGNED |
| `GET /health/appointments/{id}` | ✓ get | ✓ get | ALIGNED |
| `PUT .../status` | ✓ updateStatus | ✓ cancelAppointment | ALIGNED |
| `GET/POST/PUT /health/doctor-schedules` | ✓ | ✓ get + calendar | Web 有管理 CRUD |
| `GET .../calendar` | ✓ calendar | ✓ calendarView | ALIGNED |
| `GET /health/doctors` | ✓ list | ✓ list | ALIGNED |
| `POST/PUT/DELETE /health/doctors` | ✓ CRUD | — | WEB-ONLY预期 |
### 2.4 随访管理
| 后端路由 | Web | 小程序 | 状态 |
|----------|-----|--------|------|
| `GET/POST/PUT/DELETE .../follow-up-tasks` | ✓ 全 CRUD | ✓ list + get | Web 有管理MP 只读 |
| `POST .../batch-create/assign/complete` | ✓ 批量操作 | — | WEB-ONLY |
| `POST .../records` (create) | ✓ | ✓ submit + doctor create | ALIGNED |
| `GET .../follow-up-records` | ✓ list | ✓ list | ALIGNED |
| `GET/POST/PUT/DELETE .../follow-up-templates` | ✓ 全 CRUD | — | WEB-ONLY |
### 2.5 咨询管理
| 后端路由 | Web | 小程序 | 状态 |
|----------|-----|--------|------|
| `GET/POST .../consultation-sessions` | ✓ list + create | ✓ list + doctor list | ALIGNED |
| `GET .../{id}` | ✓ get | ✓ get | ALIGNED |
| `GET .../{id}/messages` | ✓ list | ✓ list + doctor list | ALIGNED |
| `POST .../consultation-messages` | ✓ create | ✓ send + doctor send | ALIGNED |
| `PUT .../{id}/close` | ✓ close | ✓ doctor close | ALIGNED |
| `PUT .../{id}/read` | ✓ markRead | ✓ markRead | ALIGNED |
| `GET .../export` | ✓ export | — | WEB-ONLY预期 |
| `GET .../doctor/dashboard` | — | ✓ doctor dashboard | **MP-ONLY** |
### 2.6 内容管理(文章)
| 后端路由 | Web | 小程序 | 状态 |
|----------|-----|--------|------|
| `GET /health/articles` | ✓ list全状态 | ✓ list仅 published | 角色分叉(正常) |
| `GET /health/articles/{id}` | ✓ get | ✓ getDetail | ALIGNED |
| `POST/PUT/DELETE /health/articles` | ✓ 全 CRUD | — | WEB-ONLY预期 |
| `POST .../submit/approve/reject/unpublish` | ✓ 审核流程 | — | WEB-ONLY预期 |
| `POST .../view` | ✓ view | — | WEB-ONLY |
| `GET .../revisions` | ✓ revisions | — | WEB-ONLY |
| `GET/POST/PUT/DELETE .../article-categories` | ✓ CRUD | ✓ list | MP 只读 |
| `GET/POST/PUT/DELETE .../article-tags` | ✓ CRUD | — | WEB-ONLY |
### 2.7 积分商城
| 后端路由 | Web | 小程序 | 状态 |
|----------|-----|--------|------|
| `GET /health/points/account` | — | ✓ getAccount | **MP-ONLY** |
| `POST /health/points/checkin` | — | ✓ dailyCheckin | **MP-ONLY** |
| `GET /health/points/checkin/status` | — | ✓ getCheckinStatus | **MP-ONLY** |
| `GET .../products` | ✓ admin + patient | ✓ listProducts | ALIGNED |
| `POST .../exchange` | — | ✓ exchangeProduct | **MP-ONLY** |
| `GET .../orders` | ✓ admin list | ✓ listMyOrders | 角色分叉(正常) |
| `GET .../transactions` | — | ✓ listMyTransactions | **MP-ONLY** |
| `POST .../verify` | ✓ verifyOrder | — | WEB-ONLY |
| `GET .../offline-events` | ✓ admin + patient | ✓ list | ALIGNED |
| `POST .../offline-events/{id}/register` | — | ✓ registerEvent | **MP-ONLY** |
| `GET/POST/PUT/DELETE .../admin/points/rules` | ✓ CRUD | — | WEB-ONLY预期 |
| `GET/POST/PUT/DELETE .../admin/points/products` | ✓ CRUD | — | WEB-ONLY预期 |
| `GET .../admin/points/orders` | ✓ list | — | WEB-ONLY预期 |
| `GET .../admin/points/statistics` | ✓ | — | WEB-ONLY预期 |
| `GET .../admin/offline-events` | ✓ admin CRUD | — | WEB-ONLY预期 |
### 2.8 告警系统
| 后端路由 | Web | 小程序 | 状态 |
|----------|-----|--------|------|
| `GET /health/alerts` | ✓ list | ✓ list + doctor list | ALIGNED |
| `PUT .../acknowledge/dismiss/resolve` | ✓ | ✓ doctor acknowledge/dismiss/resolve | ALIGNED |
| `GET/POST/PUT .../alert-rules` | ✓ 全 CRUD | — | WEB-ONLY预期 |
| `GET/POST .../critical-alerts` | ✓ list/get/acknowledge | — | **WEB-ONLY** |
| `GET/POST/PUT/DELETE .../critical-value-thresholds` | ✓ 全 CRUD | — | **WEB-ONLY** |
### 2.9 设备与数据采集
| 后端路由 | Web | 小程序 | 状态 |
|----------|-----|--------|------|
| `GET .../devices` | ✓ list | — | WEB-ONLY |
| `DELETE .../devices/{id}` | ✓ unbind | — | WEB-ONLY |
| `POST .../device-readings/batch` | ✓ batchCreate | ✓ uploadReadings | ALIGNED |
| `GET .../device-readings` | ✓ query | ✓ query | ALIGNED |
| `GET .../device-readings/hourly` | ✓ hourly | ✓ queryHourly | ALIGNED |
### 2.10 AI 分析
| 后端路由 | Web | 小程序 | 状态 |
|----------|-----|--------|------|
| `POST /ai/analyze/*` (4 个 SSE) | — | — | **ORPHAN**(管理端未接入) |
| `GET /ai/analysis/history` | ✓ list | ✓ list | ALIGNED |
| `GET /ai/analysis/{id}` | ✓ get | ✓ getDetail | ALIGNED |
| `GET/POST .../prompts` | ✓ CRUD | — | WEB-ONLY预期 |
| `POST .../prompts/{id}/activate/rollback` | ✓ | — | WEB-ONLY |
| `GET .../usage/overview + by-type` | ✓ | — | WEB-ONLY |
### 2.11 透析管理
| 后端路由 | Web | 小程序 | 状态 |
|----------|-----|--------|------|
| `GET .../dialysis-records` | ✓ list | — | **WEB-ONLY** |
| `POST/PUT/DELETE .../dialysis-records` | ✓ 全 CRUD | — | **WEB-ONLY** |
| `PUT .../dialysis-records/{id}/review` | ✓ review | — | **WEB-ONLY** |
| `GET/POST/PUT/DELETE .../dialysis-prescriptions` | ✓ 全 CRUD | — | **WEB-ONLY** |
| `GET .../admin/statistics/dialysis` | ✓ | — | **WEB-ONLY** |
### 2.12 知情同意
| 后端路由 | Web | 小程序 | 状态 |
|----------|-----|--------|------|
| `GET .../consents` | ✓ list | — | **WEB-ONLY** |
| `POST .../consents` (grant) | ✓ | — | **WEB-ONLY** |
| `PUT .../consents/{id}/revoke` | ✓ | — | **WEB-ONLY** |
### 2.13 统计仪表盘
| 后端路由 | Web | 小程序 | 状态 |
|----------|-----|--------|------|
| `GET .../admin/statistics/*` (9 个) | ✓ 全部 | ✓ 3 个doctor 端) | Web 完整MP 部分覆盖 |
---
## 3. 差距模式摘要
### 3.1 预期分叉(管理员 vs 患者角色)
以下差异是**正常的角色分叉**Web 面向管理员,小程序面向患者/医护:
- 用户/角色/组织管理 → 仅 Web
- 系统配置(字典/菜单/设置/主题/语言/编号) → 仅 Web
- 工作流引擎 → 仅 Web
- 消息管理 → 仅 Web
- 插件系统 → 仅 Web
- 微信登录 → 仅小程序
- 积分签到/兑换 → 仅小程序
- BLE 设备同步 → 仅小程序
- 每日摘要/趋势 → 仅小程序
- 医生仪表盘 → 仅小程序(医护端)
### 3.2 需关注的差距
| 差距 | 影响范围 | 优先级 |
|------|---------|--------|
| 透析管理 — 小程序完全无入口 | 患者无法在移动端查看透析记录 | P1 |
| 知情同意 — 小程序无入口 | 患者无法在移动端管理同意书 | P1 |
| AI 分析 SSE 端点 — 两端都无管理 UI 调用 | AI 功能可能仅通过直接 API 测试使用 | P2 |
| 药物管理 — 小程序有页面但无 API 调用对应 | MP 药物页面可能使用其他 API 或硬编码 | P2 |
| 趋势生成 `POST .../trends/generate` — 无前端调用 | 后台功能可能仅通过定时任务触发 | P3 |
| 危急值告警/阈值管理 — 小程序无入口 | 仅 Web 管理端可操作 | P3 |
| 健康记录 CRUD — 小程序无入口 | 患者移动端无法查看健康档案 | P2 |
| 诊断记录 CRUD — 小程序无入口 | 患者移动端无法查看诊断 | P2 |
### 3.3 后端孤立路由(无任何前端调用者)
| 路由 | 说明 |
|------|------|
| `POST /health/patients/{id}/trends/generate` | 趋势报告生成,可能为内部任务 |
| `POST /ai/analyze/*` (4 个 SSE) | AI 分析接口,可能通过 API 工具直接调用 |
---
## 4. 功能完成度评估
| 功能域 | 后端 | Web | 小程序 | 整体完成度 |
|--------|------|-----|--------|-----------|
| 患者管理 | 100% | 100% | 85%(无删除) | 95% |
| 医生/排班 | 100% | 100% | 40%(只读) | 80% |
| 健康数据 | 100% | 100% | 60%(体征+化验+监测) | 87% |
| 预约管理 | 100% | 100% | 90% | 97% |
| 随访管理 | 100% | 100% | 70%(医护端完善) | 90% |
| 咨询管理 | 100% | 100% | 95% | 98% |
| 内容管理 | 100% | 100% | 50%(只读列表) | 83% |
| 积分商城 | 100% | 80%(管理端) | 100%(患者端) | 93% |
| 告警系统 | 100% | 100% | 60%(仅查看/处理) | 87% |
| AI 分析 | 100% | 70%(无 SSE 调用) | 30%(仅历史查看) | 67% |
| 透析管理 | 100% | 100% | 0% | 67% |
| 知情同意 | 100% | 100% | 0% | 67% |
| 统计仪表盘 | 100% | 100% | 30%(医护端部分) | 77% |

View File

@@ -0,0 +1,191 @@
# HMS 功能审计 — Phase 2: 后端完整性审计
> 日期: 2026-04-30 | 审计范围: 后端 Rust 代码
## 总览
| 指标 | 值 |
|------|-----|
| Handler 函数 | 169 个23 个文件) |
| Service 函数 | 180 个26 个文件) |
| Entity | 45 个46 个文件含 mod.rs |
| 路由 | 121 个erp-health |
| 编译警告 | 40 个 |
| 死代码抑制 | 4 处 |
| TODO 注释 | 4 处 |
| 生产代码 unwrap() | 10 处 |
---
## 1. 死代码分析
### 1.1 `#[allow(dead_code)]` 抑制4 处)
| 文件 | 行号 | 抑制内容 | 是否有调用者 | 判定 |
|------|------|---------|------------|------|
| `erp-auth/src/service/wechat_service.rs` | 43 | `unionid` 字段 | 微信 API 返回但不使用 | 保留合理(未来可能用于 UnionID 登录) |
| `erp-server/src/middleware/rate_limit.rs` | 27 | `RateLimitConfig` 整个 struct | 当前使用环境变量配置 | 保留合理(预留结构化配置) |
| `erp-plugin/src/host.rs` | 42 | `tenant_id` 字段 | HostState 中的 tenant_id | **建议清理**:已在 data_service 中改为函数参数传递 |
| `erp-plugin/src/host.rs` | 44 | `user_id` 字段 | HostState 中的 user_id | **建议清理**:同上 |
### 1.2 编译器死代码警告9 处未抑制)
| 文件 | 警告内容 | 严重性 |
|------|---------|--------|
| erp-health 多处 | `message`, `usage`, `input_tokens`, `output_tokens` 字段 | LOW — DTO 响应字段,前端可能使用 |
| erp-health | `RefRow` struct 从未构造 | MEDIUM — 可能是重构残留 |
| erp-health | `check_result`, `total` 字段 | LOW — 可能用于序列化 |
| erp-server | `AnalyticsEvent.timestamp` 字段 | LOW — 预留字段 |
### 1.3 未使用导入18 处)
分布在 6 个 crate 中,建议运行 `cargo fix` 自动清理:
```bash
cargo fix --lib -p erp-health --allow-dirty
cargo fix --lib -p erp-plugin --allow-dirty
cargo fix --lib -p erp-ai --allow-dirty
```
### 1.4 TODO 注释4 处)
| 文件 | 行号 | 内容 | 优先级 |
|------|------|------|--------|
| `erp-health/src/event.rs` | 50 | PATIENT_VERIFIED/PATIENT_DECEASED 未实现 | KNOWN |
| `erp-auth/src/handler/wechat_handler.rs` | 45 | 多租户微信登录租户解析策略 | P2 |
| `erp-auth/src/handler/wechat_handler.rs` | 76 | 同上 | P2 |
| `erp-plugin/src/data_service.rs` | 1073 | 未来版本添加 Redis 缓存层 | P3 |
---
## 2. 调用链完整性
### 2.1 Handler → Service 覆盖率
| Handler 文件 | Handler 函数数 | 对应 Service | 覆盖率 |
|-------------|--------------|-------------|--------|
| patient_handler | 17 | patient_service | 100% |
| health_data_handler | 18 | health_data_service + trend_service | 100% |
| points_handler | 28 | points_service + stats_service | 100% |
| stats_handler | 9 | stats_service | 100% |
| follow_up_handler | 10 | follow_up_service | 100% |
| article_handler | 11 | article_service | 100% |
| consultation_handler | 9 | consultation_service | 100% |
| doctor_handler | 5 | doctor_service | 100% |
| appointment_handler | 8 | appointment_service | 100% |
| follow_up_template_handler | 5 | follow_up_template_service | 100% |
| medication_record_handler | 5 | medication_record_service | 100% |
| medication_reminder_handler | 4 | medication_reminder_service | 100% |
| daily_monitoring_handler | 5 | daily_monitoring_service | 100% |
| diagnosis_handler | 4 | diagnosis_service | 100% |
| device_reading_handler | 3 | device_reading_service | 100% |
| device_handler | 2 | device_service | 100% |
| consent_handler | 3 | consent_service | 100% |
| alert_handler | 4 | alert_service | 100% |
| alert_rule_handler | 4 | alert_rule_service | 100% |
| critical_alert_handler | 3 | critical_alert_service | 100% |
| critical_value_threshold_handler | 4 | critical_value_threshold_service | 100% |
| article_category_handler | 4 | article_category_service | 100% |
| article_tag_handler | 4 | article_tag_service | 100% |
**结论Handler → Service 覆盖率 100%。每个 handler 都有对应的 service 实现。**
### 2.2 Service → Entity 覆盖率
| Entity | 对应 Service | 状态 |
|--------|-------------|------|
| patient | patient_service | ✓ |
| patient_family_member | patient_service | ✓ |
| patient_tag | patient_service | ✓ |
| patient_tag_relation | patient_service | ✓ |
| patient_doctor_relation | patient_service | ✓ |
| patient_devices | device_service | ✓ |
| blind_index | patient_servicePII 加密) | ✓ |
| consent | consent_service | ✓ |
| doctor_profile | doctor_service | ✓ |
| doctor_schedule | doctor_service | ✓ |
| health_record | health_data_service | ✓ |
| vital_signs | health_data_service | ✓ |
| vital_signs_hourly | health_data_service | ✓ |
| lab_report | health_data_service | ✓ |
| health_trend | trend_service | ✓ |
| diagnosis | diagnosis_service | ✓ |
| medication_record | medication_record_service | ✓ |
| medication_reminder | medication_reminder_service | ✓ |
| device_readings | device_reading_service | ✓ |
| appointment | appointment_service | ✓ |
| follow_up_task | follow_up_service | ✓ |
| follow_up_record | follow_up_service | ✓ |
| follow_up_template | follow_up_template_service | ✓ |
| follow_up_template_field | follow_up_template_service | ✓ |
| consultation_session | consultation_service | ✓ |
| consultation_message | consultation_service | ✓ |
| article | article_service | ✓ |
| article_category | article_category_service | ✓ |
| article_tag | article_tag_service | ✓ |
| article_article_tag | article_service | ✓ |
| article_revision | article_service | ✓ |
| alerts | alert_service | ✓ |
| alert_rules | alert_rule_service | ✓ |
| critical_alert | critical_alert_service | ✓ |
| critical_alert_response | critical_alert_service | ✓ |
| critical_value_threshold | critical_value_threshold_service | ✓ |
| points_account | points_service | ✓ |
| points_rule | points_service | ✓ |
| points_product | points_service | ✓ |
| points_order | points_service | ✓ |
| points_transaction | points_service | ✓ |
| points_checkin | points_service | ✓ |
| offline_event | points_service | ✓ |
| offline_event_registration | points_service | ✓ |
| daily_monitoring | daily_monitoring_service | ✓ |
**结论Entity → Service 覆盖率 100%。45 个实体全部有对应的 service 操作。**
---
## 3. unwrap() 调用审计
### 生产代码中的 unwrap()10 处)
| 模式 | 次数 | 安全性 |
|------|------|--------|
| `active.version.unwrap() + 1` | 9 处 | **安全** — version 从 DB 查询获取SeaORM ActiveModel 保证非 None |
| `existing.unwrap().into()` | 1 处 | **需审查** — device_reading_service.rs:192 |
device_reading_service.rs:192 上下文:在 `update()` 函数中,`existing` 来自 `find_by_id` 查询。如果调用 `update()` 时记录不存在,会 panic。**建议**:改用 `ok_or(AppError::NotFound)` 模式。
### 测试代码中的 unwrap()20+ 处)
全部在 `#[cfg(test)]` 块中,仅用于测试断言,无安全风险。
---
## 4. ErpModule Trait 覆盖率
| 模块 | on_startup | on_tenant_created | on_tenant_deleted | permissions | health_check | 等级 |
|------|-----------|-------------------|-------------------|-------------|-------------|------|
| erp-health | **5 个后台任务 + 事件监听** | **种子数据** | **软删除** | **56 个** | 默认 | **FULL** |
| erp-ai | 默认 | 默认 | 默认 | 6 个 | 默认 | PARTIAL |
| erp-dialysis | 默认 | 默认 | 默认 | 5 个 | 默认 | PARTIAL |
| erp-auth | 默认 | 默认 | 默认 | 默认 | 默认 | MINIMAL |
| erp-config | 默认 | 默认 | 默认 | 默认 | 默认 | MINIMAL |
| erp-workflow | 默认 | 默认 | 默认 | 默认 | 默认 | MINIMAL |
| erp-message | 默认 | 默认 | 默认 | 默认 | 默认 | MINIMAL |
| erp-plugin | 默认 | 默认 | 默认 | 默认 | 默认 | MINIMAL |
**说明**
- MINIMAL 不一定代表缺陷 — auth/config/workflow/message 的权限通过中间件和 JWT claims 管控,不需要声明 PermissionDescriptor
- 只有业务模块health/ai/dialysis需要声明细粒度权限码因为它们的路由使用 `require_permission` 中间件
---
## 5. 后端完整性评分
| 检查项 | 评分 | 说明 |
|--------|------|------|
| 代码存在性 | 100% | 所有实体/服务/处理器 完整 |
| 调用链连通性 | 100% | 处理器→服务→实体 全部连通 |
| 死代码率 | 2% | 4 处抑制 + 9 处警告 / 462 文件 |
| unwrap() 风险 | 98% | 仅 1 处 device_reading_service 可能 panic |
| Trait 实现完整度 | 37.5% | 3/8 模块有实质实现(其余使用默认值) |
| TODO 债务 | LOW | 4 处,均为已知的 P2/P3 预留项 |

View File

@@ -0,0 +1,214 @@
# HMS 功能审计 — Phase 3: 事件系统审计
> 日期: 2026-04-30 | 审计范围: 全系统事件总线
## 总览
| 指标 | 值 |
|------|-----|
| 事件类型常量定义 | 25 个event.rs |
| 事件发布调用 | 44 处 |
| 事件消费者 | 14 个11 个 tokio::spawn 任务) |
| 已知未实现事件 | 2 个PATIENT_VERIFIED / PATIENT_DECEASED标记 KNOWN |
---
## 1. 事件发布方清单
### erp-health 模块30 处发布)
| 事件类型 | 发布者 Service | 函数 | Payload 字段 |
|----------|---------------|------|-------------|
| `patient.created` | patient_service.rs | create_patient | patient_id, name, phone |
| `patient.updated` | patient_service.rs | update_patient | patient_id, updated_fields |
| `appointment.created` | appointment_service.rs | create_appointment | appointment_id, patient_id, doctor_id, scheduled_at |
| `appointment.{status}` | appointment_service.rs | update_appointment_status | appointment_id, patient_id, doctor_id, status |
| `appointment.reminder` | appointment_service.rs | send_reminders | appointment_id, patient_id, doctor_id, scheduled_at |
| `follow_up.created` | follow_up_service.rs | create_task | task_id, patient_id, assigned_to |
| `follow_up.batch_created` | follow_up_service.rs | batch_create_tasks | count, task_ids |
| `follow_up.completed` | follow_up_service.rs | batch_complete_tasks | task_ids, completed_count |
| `follow_up.assigned` | follow_up_service.rs | batch_assign_tasks | task_ids, assigned_to |
| `follow_up.overdue` | follow_up_service.rs | check_overdue_tasks | task_id, assigned_to, patient_id |
| `consultation.opened` | consultation_service.rs | create_session | session_id, patient_id, doctor_id |
| `consultation.closed` | consultation_service.rs | close_session | session_id, patient_id, doctor_id |
| `consultation.new_message` | consultation_service.rs | create_message | session_id, sender_id, sender_type |
| `device.readings.synced` | device_reading_service.rs | sync_readings | patient_id, device_type, reading_count |
| `vital_signs.created` | health_data_service.rs | create_vital_signs | patient_id, record_id, indicators |
| `lab_report.uploaded` | health_data_service.rs | create_lab_report | patient_id, report_id, indicator_count |
| `lab_report.reviewed` | health_data_service.rs | review_lab_report | report_id, reviewer_id, status |
| `health_data.critical_alert` | health_data_service.rs | create_vital_signs | patient_id, alert_type, metric_name, metric_value, threshold_value |
| `daily_monitoring.created` | daily_monitoring_service.rs | create_daily_monitoring | patient_id, monitoring_id, record_date |
| `alert.triggered` | alert_engine.rs | evaluate_rules | patient_id, severity, rule_name, alert_id |
| `article.published` | article_service.rs | publish_article | article_id, title, author_id |
| `article.rejected` | article_service.rs | reject_article | article_id, reviewer_id, reason |
| `consent.granted` | consent_service.rs | grant_consent | patient_id, consent_type |
| `consent.revoked` | consent_service.rs | revoke_consent | patient_id, consent_type, reason |
| `points.earned` | points_service.rs | daily_checkin | patient_id, points, balance |
| `points.exchanged` | points_service.rs | exchange_product | patient_id, product_id, order_id, points |
| `points.expired` | points_service.rs | expire_points | count, expired_points_total |
| `doctor.online_status_changed` | doctor_service.rs | update_online_status | doctor_id, old_status, new_status |
### erp-ai 模块2 处发布)
| 事件类型 | 发布者 | 函数 |
|----------|--------|------|
| `ai.analysis.failed` | handler/mod.rs | stream_lab_report 等 |
| `ai.analysis.completed` | handler/mod.rs | stream_lab_report 等 |
### erp-dialysis 模块1 处发布)
| 事件类型 | 发布者 | 函数 |
|----------|--------|------|
| `dialysis.record.created` | dialysis_service.rs | create_record |
### erp-auth 模块1 处发布)
| 事件类型 | 发布者 | 函数 |
|----------|--------|------|
| `user.login` | auth_service.rs | login |
### erp-workflow 模块2+ 处发布)
| 事件类型 | 发布者 | 函数 |
|----------|--------|------|
| `workflow.instance.*` | instance_service.rs | start_instance 等 |
| `workflow.task.timeout` | module.rs | 后台超时检测 |
### erp-plugin 模块3 处发布)
| 事件类型 | 发布者 | 函数 |
|----------|--------|------|
| `plugin.data.*` | data_service.rs | CRUD 操作 |
| `plugin.trigger.*` | data_service.rs | 通知触发 |
| 动态事件 | engine.rs | 插件自定义事件 |
---
## 2. 事件消费方清单
### erp-health 消费者11 个 tokio::spawn 任务)
| # | 消费者名称 | 订阅前缀 | 处理事件 | 业务动作 |
|---|-----------|---------|---------|---------|
| 1 | workflow_task_consumer | `workflow.task.` | `workflow.task.completed` | 更新随访任务状态为 completed |
| 2 | message_consumer | `message.` | `message.sent` | 日志记录(预留扩展) |
| 3 | device_reading_consumer | `device.readings.` | `device.readings.synced` | 触发告警引擎评估 |
| 4 | alert_notifier | `alert.` | `alert.triggered` | 发送应用内告警通知 |
| 5 | patient_welcome | `patient.` | `patient.created` | 发送欢迎消息 |
| 6 | appointment_notifier | `appointment.` | `appointment.confirmed` | 发送预约确认通知 |
| 7 | appointment_cancel_handler | `appointment.` | `appointment.cancelled` | 日志记录(号源释放) |
| 8 | follow_up_escalator | `follow_up.` | `follow_up.overdue` | 发送逾期升级通知 |
| 9 | critical_alert_consumer | `health_data.` | `health_data.critical_alert` | 创建危急值告警记录 |
| 10 | ai_analysis_notifier | `ai.` | `ai.analysis.completed` | 通知关联医生 |
| 11 | dialysis_notifier | `ai.` | `dialysis.record.created` | 日志记录 |
| 12 | consent_notifier | `consent.` | `consent.granted` | 发送授予通知 |
| 13 | consent_revoked_notifier | `consent.` | `consent.revoked` | 发送撤回通知给医护 |
### 其他模块消费者
| 模块 | 消费者 | 订阅范围 |
|------|--------|---------|
| erp-message | SSE handler | 全部事件(转发给前端) |
| erp-plugin | notification handler | `plugin.trigger.` |
| erp-plugin | engine | 按插件 manifest 的 pattern |
---
## 3. 事件常量 vs 发布方 vs 消费方矩阵
| 事件常量 | 事件类型 | 发布方 | 消费方 | 状态 |
|----------|---------|--------|--------|------|
| APPOINTMENT_CREATED | `appointment.created` | ✓ appointment_service | ✓ appointment_notifier前缀匹配 | **ALIVE** |
| ALERT_TRIGGERED | `alert.triggered` | ✓ alert_engine | ✓ alert_notifier | **ALIVE** |
| CONSENT_GRANTED | `consent.granted` | ✓ consent_service | ✓ consent_notifier | **ALIVE** |
| CONSENT_REVOKED | `consent.revoked` | ✓ consent_service | ✓ consent_revoked_notifier | **ALIVE** |
| ARTICLE_PUBLISHED | `article.published` | ✓ article_service | — | **NO CONSUMER** |
| ARTICLE_REJECTED | `article.rejected` | ✓ article_service | — | **NO CONSUMER** |
| CONSULTATION_OPENED | `consultation.opened` | ✓ consultation_service | — | **NO CONSUMER** |
| CONSULTATION_CLOSED | `consultation.closed` | ✓ consultation_service | — | **NO CONSUMER** |
| CONSULTATION_NEW_MESSAGE | `consultation.new_message` | ✓ consultation_service | — | **NO CONSUMER** |
| DEVICE_READINGS_SYNCED | `device.readings.synced` | ✓ device_reading_service | ✓ device_reading_consumer | **ALIVE** |
| DOCTOR_ONLINE_STATUS_CHANGED | `doctor.online_status_changed` | ✓ doctor_service | — | **NO CONSUMER** |
| FOLLOW_UP_CREATED | `follow_up.created` | ✓ follow_up_service | — | **NO CONSUMER** |
| FOLLOW_UP_COMPLETED | `follow_up.completed` | ✓ follow_up_service | — | **NO CONSUMER** |
| FOLLOW_UP_OVERDUE | `follow_up.overdue` | ✓ follow_up_service | ✓ follow_up_escalator | **ALIVE** |
| DAILY_MONITORING_CREATED | `daily_monitoring.created` | ✓ daily_monitoring_service | — | **NO CONSUMER** |
| LAB_REPORT_UPLOADED | `lab_report.uploaded` | ✓ health_data_service | — | **NO CONSUMER** |
| LAB_REPORT_REVIEWED | `lab_report.reviewed` | ✓ health_data_service | — | **NO CONSUMER** |
| HEALTH_DATA_CRITICAL_ALERT | `health_data.critical_alert` | ✓ health_data_service | ✓ critical_alert_consumer | **ALIVE** |
| PATIENT_CREATED | `patient.created` | ✓ patient_service | ✓ patient_welcome | **ALIVE** |
| PATIENT_UPDATED | `patient.updated` | ✓ patient_service | — | **NO CONSUMER** |
| PATIENT_VERIFIED | `patient.verified` | — | — | **KNOWN** 未实现 |
| PATIENT_DECEASED | `patient.deceased` | — | — | **KNOWN** 未实现 |
| POINTS_EXPIRED | `points.expired` | ✓ points_service | — | **NO CONSUMER** |
| POINTS_EARNED | `points.earned` | ✓ points_service | — | **NO CONSUMER** |
| POINTS_EXCHANGED | `points.exchanged` | ✓ points_service | — | **NO CONSUMER** |
---
## 4. 分析结论
### 4.1 活跃事件(有发布+有消费11 个
这是系统核心业务链路,全部正常:
- `workflow.task.completed` → 随访自动完成
- `device.readings.synced` → 告警评估
- `alert.triggered` → 告警通知
- `patient.created` → 欢迎消息
- `appointment.confirmed/cancelled` → 预约通知
- `follow_up.overdue` → 逾期升级
- `health_data.critical_alert` → 危急值告警
- `ai.analysis.completed` → 医生通知
- `dialysis.record.created` → 日志
- `consent.granted/revoked` → 知情同意通知
### 4.2 有发布无消费事件14 个
这些事件被发布到 EventBus 并持久化到 domain_events 表,但没有业务消费者。它们可能仅用于审计追踪或 SSE 前端推送。
**风险评估**
| 事件 | 风险 | 说明 |
|------|------|------|
| `patient.updated` | LOW | 信息性事件,更新操作已直接处理 |
| `vital_signs.created` | LOW | 数据已写入 DB无需后续触发 |
| `lab_report.uploaded` | LOW | 同上 |
| `lab_report.reviewed` | LOW | 同上 |
| `follow_up.created` | LOW | 任务已创建,无需后续触发 |
| `follow_up.completed` | LOW | 状态已更新,无需后续触发 |
| `consultation.*` | LOW | 会话管理已在 service 内直接处理 |
| `article.published/rejected` | LOW | 状态已更新,无需后续触发 |
| `points.earned/exchanged/expired` | LOW | 积分操作已在 service 内完成 |
| `daily_monitoring.created` | LOW | 数据已写入 |
| `doctor.online_status_changed` | LOW | 状态已更新 |
### 4.3 已知未实现事件KNOWN2 个
- `PATIENT_VERIFIED` — 患者认证流程未实现
- `PATIENT_DECEASED` — 死亡记录流程未实现
### 4.4 潜在问题
1. **message.sent 消费者仅为日志记录** — event.rs:119 的消费者仅做 tracing::info实际业务逻辑如更新 consultation last_message_at已在 service 层直接处理,此消费者预留扩展但当前无实质作用。
2. **SSE 全事件转发** — erp-message 的 SSE handler 监听所有事件,意味着所有事件(包括 points.earned 等低优先级事件)都会被推送到前端,可能导致通知噪音。
---
## 5. Payload Schema 一致性验证(抽样 5 个)
| 事件 | 发布方字段 | 消费方解析 | 一致性 |
|------|-----------|-----------|--------|
| `workflow.task.completed` | `task_id` | `task_id` → Uuid | ✅ |
| `device.readings.synced` | `patient_id` | `patient_id` → Uuid | ✅ |
| `health_data.critical_alert` | `patient_id, alert_type, metric_name, metric_value, threshold_value` | 全部解析 | ✅ |
| `follow_up.overdue` | `task_id, assigned_to` | 全部解析 | ✅ |
| `appointment.confirmed` | `doctor_id, patient_id` | 全部解析 | ✅ |
**结论**:所有抽样的关键事件 payload schema 发布方与消费方完全一致。所有消费者都使用幂等检查(`is_event_processed`),防止重复处理。
---
## 6. 事件系统评分
| 检查项 | 评分 | 说明 |
|--------|------|------|
| 事件定义完整性 | 95% | 25/27 常量有发布者2 个 KNOWN 未实现) |
| 消费者覆盖率 | 44% | 11/25 事件有活跃消费者 |
| Payload 一致性 | 100% | 抽样 5 个全部一致 |
| 幂等性保证 | 100% | 所有消费者使用 `is_event_processed` 检查 |
| 死信处理 | 100% | 消费失败自动进入 dead_letter_event 表 |

View File

@@ -0,0 +1,334 @@
# HMS 功能审计 — Phase 4: 参数传递与配置审计
> 日期: 2026-04-30 | 审计范围: DTO 覆盖率、配置参数、权限码、数据模型映射
## 总览
| 指标 | 值 |
|------|-----|
| DTO 结构体 | 105 个17 文件) |
| 配置参数 | 30 个字段12 配置节) |
| 未使用配置字段 | 6 个(均在 AiConfig |
| 声明权限码 | 50 个3 模块) |
| Handler 实际使用权限码 | ~106 个 |
| 前端 AuthButton 引用 | 13 个唯一码 |
| 小程序未映射字段 | 6 个vital_signs |
---
## 1. DTO 覆盖率检查
### 1.1 DTO 分布统计
| 分类 | 数量 | 说明 |
|------|------|------|
| CreateCreate*Req | 27 | 创建请求 |
| UpdateUpdate*Req | 19 | 更新/审核/撤销/核销请求 |
| Query*Query/*Params | 8 | 列表查询参数 |
| Response*Resp/*Response | 38 | 响应结构体 |
| Other批量操作/辅助) | 13 | BatchReq、ExchangeReq、数据点等 |
| **总计** | **105** | 17 个文件 |
### 1.2 按 Handler 的 DTO 覆盖矩阵
| Handler | Create DTO | Update DTO | Query DTO | Response DTO | 状态 |
|---------|-----------|-----------|----------|-------------|------|
| patient_handler | CreatePatientReq | UpdatePatientReq | PatientListQuery | PatientResp | ✓ |
| doctor_handler | CreateDoctorReq | UpdateDoctorReq | DoctorListQuery | DoctorResp | ✓ |
| appointment_handler | CreateAppointmentReq | UpdateAppointmentStatusReq | AppointmentListQuery/CalendarQuery | AppointmentResp/ScheduleResp | ✓ |
| consultation_handler | CreateSessionReq/CreateMessageReq | — | SessionQuery | SessionResp/MessageResp | ✓ |
| follow_up_handler | CreateFollowUpTaskReq/BatchCreateTasksReq | UpdateFollowUpTaskReq | FollowUpTaskListQuery/FollowUpRecordListQuery | FollowUpTaskResp/FollowUpRecordResp | ✓ |
| follow_up_template_handler | CreateFollowUpTemplateReq | UpdateFollowUpTemplateReq | FollowUpTemplateListQuery | FollowUpTemplateResp | ✓ |
| health_data_handler | CreateVitalSignsReq/CreateLabReportReq/CreateHealthRecordReq | UpdateVitalSignsReq/UpdateLabReportReq/UpdateHealthRecordReq/ReviewLabReportReq | MiniTrendQueryParams | VitalSignsResp/LabReportResp/HealthRecordResp/TrendResp | ✓ |
| daily_monitoring_handler | CreateDailyMonitoringReq | UpdateDailyMonitoringReq | — | DailyMonitoringResp | ✓ |
| diagnosis_handler | CreateDiagnosisReq | UpdateDiagnosisReq | — | DiagnosisResp | ✓ |
| medication_record_handler | CreateMedicationRecordReq | UpdateMedicationRecordReq | — | MedicationRecordResp | ✓ |
| medication_reminder_handler | CreateMedicationReminderReq | UpdateMedicationReminderReq | — | MedicationReminderResp | ✓ |
| alert_handler | CreateAlertRuleRequest | UpdateAlertRuleRequest/AcknowledgeAlertRequest | — | AlertRuleResponse/AlertResponse | ✓ |
| consent_handler | CreateConsentReq | RevokeConsentReq | — | ConsentResp | ✓ |
| points_handler | CreatePointsRuleReq/CreatePointsProductReq/CreateOfflineEventReq | UpdatePointsRuleReq/UpdatePointsProductReq/UpdateOfflineEventWithVersion/VerifyOrderReq | — | PointsRuleResp/PointsProductResp/PointsOrderResp/PointsAccountResp | ✓ |
| article_handler | CreateArticleReq/CreateCategoryReq/CreateTagReq | UpdateArticleReq/UpdateCategoryReq/UpdateTagReq/ReviewArticleReq | ArticleListParams | ArticleResp/CategoryResp/TagResp/ArticleRevisionResp | ✓ |
| stats_handler | — | — | — | DashboardStatsResp/PatientStatisticsResp/...9 个) | ✓ |
**结论:所有 23 个 handler 都有完整的 DTO 覆盖。每个写入端点有 Create/Update DTO列表端点有 Query DTO所有端点有 Response DTO。**
### 1.3 DTO 传递链完整性(抽样验证 5 个)
| 端点 | Handler 输入 DTO | Service 函数签名 | 一致性 |
|------|-----------------|-----------------|--------|
| `POST /patients` | CreatePatientReq | `create_patient(req: CreatePatientReq)` | ✅ |
| `POST /appointments` | CreateAppointmentReq | `create_appointment(req: CreateAppointmentReq)` | ✅ |
| `POST /vital-signs` | CreateVitalSignsReq | `create_vital_signs(patient_id, req: CreateVitalSignsReq)` | ✅ |
| `POST /follow-up/tasks` | CreateFollowUpTaskReq | `create_task(req: CreateFollowUpTaskReq)` | ✅ |
| `POST /consultation-sessions` | CreateSessionReq | `create_session(req: CreateSessionReq)` | ✅ |
---
## 2. 配置参数使用率
### 2.1 配置结构总览
AppConfig 包含 12 个子配置节、30 个字段:
| 配置节 | 字段数 | 用途 |
|--------|-------|------|
| ServerConfig | 3 | 服务端口、指标端口 |
| DatabaseConfig | 3 | 连接字符串、连接池大小 |
| RedisConfig | 1 | 连接字符串 |
| JwtConfig | 3 | 密钥、Token TTL |
| AuthConfig | 1 | 超级管理员密码 |
| LogConfig | 1 | 日志级别 |
| CorsConfig | 1 | 允许的 Origin |
| WechatConfig | 3 | 微信小程序 AppID/Secret/开发模式 |
| HealthConfig | 2 | AES/HMAC 密钥PII 加密) |
| CryptoConfig | 1 | KEK 主密钥 |
| AiConfig | 8 | AI 提供商配置 |
| StorageConfig | 2 | 文件上传目录/大小限制 |
### 2.2 字段使用情况
| 配置节 | 字段 | 使用次数 | 状态 |
|--------|------|---------|------|
| ServerConfig | `host` | 1 | ✓ 正常 |
| ServerConfig | `port` | 1 | ✓ 正常 |
| ServerConfig | `metrics_port` | 2 | ✓ 正常 |
| DatabaseConfig | `url` | 5 | ✓ 正常 |
| DatabaseConfig | `max_connections` | 1 | ✓ 正常 |
| DatabaseConfig | `min_connections` | 1 | ✓ 正常 |
| RedisConfig | `url` | 2 | ✓ 正常 |
| JwtConfig | `secret` | 5 | ✓ 正常 |
| JwtConfig | `access_token_ttl` | 1 | ✓ 正常 |
| JwtConfig | `refresh_token_ttl` | 1 | ✓ 正常 |
| AuthConfig | `super_admin_password` | 1 | ✓ 正常 |
| LogConfig | `level` | 1 | ✓ 正常 |
| CorsConfig | `allowed_origins` | 1 | ✓ 正常 |
| WechatConfig | `appid` | 2 | ✓ 正常 |
| WechatConfig | `secret` | 3 | ✓ 正常 |
| WechatConfig | `dev_mode` | 2 | ✓ 正常 |
| HealthConfig | `aes_key` | 2 | ✓ 正常(启动校验 + CryptoService 初始化) |
| HealthConfig | `hmac_key` | 2 | ✓ 正常(启动校验 + HMAC 盲索引) |
| CryptoConfig | `kek` | 2 | ✓ 正常KEK/DEK 密钥体系) |
| AiConfig | `api_key` | 1 | ✓ 正常 |
| AiConfig | `base_url` | 1 | ✓ 正常 |
| **AiConfig** | **`default_provider`** | **0** | **未使用** |
| **AiConfig** | **`model`** | **0** | **未使用** |
| **AiConfig** | **`max_tokens`** | **0** | **未使用** |
| **AiConfig** | **`temperature`** | **0** | **未使用** |
| **AiConfig** | **`cache_ttl_seconds`** | **0** | **未使用** |
| **AiConfig** | **`rate_limit_patient_daily`** | **0** | **未使用** |
| StorageConfig | `upload_dir` | 3 | ✓ 正常 |
| StorageConfig | `max_file_size` | 1 | ✓ 正常 |
### 2.3 未使用字段分析
6 个未使用字段全部集中在 `AiConfig`
| 字段 | 原设计意图 | 当前替代机制 |
|------|-----------|------------|
| `default_provider` | 全局默认 AI 提供商 | 硬编码使用 Claude |
| `model` | 全局默认模型 | 每个 Prompt 模板通过 `model_config` JSONB 字段配置 |
| `max_tokens` | 全局默认最大 Token 数 | 同上,从 `model_config.max_tokens` 读取,默认 2048 |
| `temperature` | 全局默认温度 | 同上,从 `model_config.temperature` 读取,默认 0.3 |
| `cache_ttl_seconds` | AI 结果缓存时间 | 未实现缓存层(每次为独立 SSE 流) |
| `rate_limit_patient_daily` | 每患者每日 AI 调用限制 | 未实现限流 |
**影响评估**LOW。这些字段是预留的全局默认值实际业务已通过更灵活的 per-prompt `model_config` 实现相同能力。但 `cache_ttl_seconds``rate_limit_patient_daily` 代表缺失的功能(缓存和限流),属于 P3 待实现。
---
## 3. 权限码审计
### 3.1 权限码声明总览
| 模块 | 声明数 | 权限码前缀 |
|------|--------|-----------|
| erp-health | 39 | `health.*` |
| erp-ai | 6 | `ai.*` |
| erp-dialysis | 5 | `health.dialysis*` |
| **合计** | **50** | |
其余 5 个模块auth/config/workflow/message/plugin**未声明任何 PermissionDescriptor**,但 handler 中使用了约 56 个权限码。
### 3.2 声明 vs 使用覆盖矩阵
#### erp-health39 声明 → 全部使用)
| 权限码 | Handler 使用 | 前端 AuthButton |
|--------|-------------|----------------|
| health.patient.list | ✓ | — |
| health.patient.manage | ✓ | ✓ PatientList/PatientDetail/PatientTagManage |
| health.health-data.list | ✓ | — |
| health.health-data.manage | ✓ | ✓ VitalSignsTab/LabReportsTab/HealthRecordsTab/DailyMonitoringTab |
| health.appointment.list | ✓ | — |
| health.appointment.manage | ✓ | ✓ AppointmentList |
| health.follow-up.list | ✓ | — |
| health.follow-up.manage | ✓ | ✓ FollowUpTaskList |
| health.consultation.list | ✓ | — |
| health.consultation.manage | ✓ | ✓ ConsultationList/ConsultationDetail |
| health.doctor.list | ✓ | — |
| health.doctor.manage | ✓ | ✓ DoctorList/DoctorSchedule |
| health.articles.list | ✓ | — |
| health.articles.manage | ✓ | ✓ ArticleManageList/ArticleEditor/ArticleCategoryManage/ArticleTagManage |
| health.articles.review | ✓ | ✓ ArticleManageList |
| health.points.list | ✓ | — |
| health.points.manage | ✓ | ✓ PointsRuleList/PointsProductList/PointsOrderList/OfflineEventList |
| health.device-readings.list | ✓ | — |
| health.device-readings.manage | ✓ | — |
| health.devices.list | ✓ | — |
| health.devices.manage | ✓ | — |
| health.alerts.list | ✓ | — |
| health.alerts.manage | ✓ | ⚠️ 前端拼写错误(见 §3.4 |
| health.alert-rules.list | ✓ | — |
| health.alert-rules.manage | ✓ | — |
| health.critical-alerts.list | ✓ | — |
| health.critical-alerts.manage | ✓ | — |
| health.critical-value-thresholds.list | ✓ | — |
| health.critical-value-thresholds.manage | ✓ | — |
| health.follow-up-templates.list | ✓ | — |
| health.follow-up-templates.manage | ✓ | — |
| health.daily-monitoring.list | ✓ | — |
| health.daily-monitoring.manage | ✓ | — |
| health.consent.list | ✓ | — |
| health.consent.manage | ✓ | — |
| health.medication-records.list | ✓ | — |
| health.medication-records.manage | ✓ | — |
| health.medication-reminders.list | ✓ | — |
| health.medication-reminders.manage | ✓ | — |
#### erp-dialysis5 声明 → 全部使用)
| 权限码 | Handler 使用 | 前端 AuthButton |
|--------|-------------|----------------|
| health.dialysis.list | ✓ | — |
| health.dialysis.manage | ✓ | ✓ DialysisManageList |
| health.dialysis-prescription.list | ✓ | — |
| health.dialysis-prescription.manage | ✓ | — |
| health.dialysis.stats | ✓ | — |
#### erp-ai6 声明 → 5 使用)
| 权限码 | Handler 使用 | 前端 AuthButton |
|--------|-------------|----------------|
| ai.analysis.list | ✓ | — |
| ai.analysis.manage | ✓ | — |
| ai.prompt.list | ✓ | ✓ AiPromptList |
| ai.prompt.manage | ✓ | ✓ AiPromptList |
| ai.usage.list | ✓ | — |
| **ai.provider.manage** | **—** | **已声明但无 Handler 调用** |
### 3.3 未声明权限码(基础模块)
以下 5 个模块的 handler 使用了 `require_permission` 但未实现 `permissions()` 方法,导致权限码不会通过 `sync_module_permissions` 自动注册到数据库:
| 模块 | 未声明权限数 | 权限码示例 |
|------|------------|-----------|
| erp-auth | 23 | user.list, role.create, organization.update, department.delete, position.list |
| erp-config | 18 | dictionary.list, menu.update, setting.read, numbering.generate, theme.update |
| erp-workflow | 8 | workflow.list, workflow.start, workflow.approve, workflow.delegate |
| erp-message | 5 | message.list, message.send, message.template.create |
| erp-plugin | 2 | plugin.admin, plugin.list |
| **合计** | **56** | |
**影响评估**这些权限码通过种子数据super_admin 角色)手动注册到数据库,而非通过 `ErpModule::permissions()` 自动注册。功能上可以正常工作,但:
- 新增权限需要手动 SQL 插入,容易遗漏
- 与 health/ai/dialysis 模块的自动注册机制不一致
- 建议在后续迭代中为这 5 个模块补充 `permissions()` 实现
### 3.4 前端权限码拼写错误BUG
| 文件 | 前端代码 | 后端声明 | 差异 |
|------|---------|---------|------|
| [AlertList.tsx:240](apps/web/src/pages/health/AlertList.tsx#L240) | `health.alert.manage` | `health.alerts.manage` | 缺少 `s` |
**影响**AlertList 页面的"管理"按钮(确认/处置告警)永远不显示,因为 `health.alert.manage` 权限码不存在于系统。用户虽然可以通过 API 直接操作,但 UI 层面无法触发管理动作。
**修复**:将 `health.alert.manage` 改为 `health.alerts.manage`(复数形式)。
### 3.5 前端路由级权限控制
Web 前端通过 `PrivateRoute` 组件做路由守卫,当前仅检查:
1. 是否已认证(`isAuthenticated`
2. `/users``/roles``/organizations` 路径需要 `auth.*` 前缀权限
健康模块的路由**没有前端路由级权限守卫**,依赖后端 API 返回 403 拒绝未授权请求。这在 SPA 架构中是可接受的做法,但用户可能看到空白页面而非友好的"无权限"提示。
---
## 4. 小程序数据模型映射验证
### 4.1 后端 vital_signs Entity 字段24 个,含标准字段)
| # | 字段名 | 类型 | 小程序映射 |
|---|--------|------|-----------|
| 1 | id | Uuid | — |
| 2 | tenant_id | Uuid | — |
| 3 | patient_id | Uuid | — |
| 4 | record_date | NaiveDate | ✓ |
| 5 | systolic_bp_morning | Option\<i32\> | ✓ blood_pressure → extra.systolic |
| 6 | diastolic_bp_morning | Option\<i32\> | ✓ blood_pressure → extra.diastolic |
| 7 | **systolic_bp_evening** | Option\<i32\> | **未映射** |
| 8 | **diastolic_bp_evening** | Option\<i32\> | **未映射** |
| 9 | heart_rate | Option\<i32\> | ✓ heart_rate |
| 10 | weight | Option\<Decimal\> | ✓ weight |
| 11 | blood_sugar | Option\<Decimal\> | ✓ blood_sugar |
| 12 | **body_temperature** | Option\<Decimal\> | **未映射** |
| 13 | **spo2** | Option\<i32\> | **未映射** |
| 14 | **blood_sugar_type** | Option\<String\> | **未映射** |
| 15 | water_intake_ml | Option\<i32\> | ✓ water_intake |
| 16 | urine_output_ml | Option\<i32\> | ✓ urine_output |
| 17 | notes | Option\<String\> | ✓ |
| 18 | source | String | —(后端默认 "manual" |
| 19-24 | 标准字段 | — | — |
### 4.2 小程序 indicator_type 映射表
来源:[health.ts:30-59](apps/miniprogram/src/services/health.ts#L30-L59)
| indicator_type | 映射字段 | 转换逻辑 |
|---------------|---------|---------|
| `blood_pressure` | systolic_bp_morning + diastolic_bp_morning | extra.systolic / extra.diastolic |
| `heart_rate` | heart_rate | Math.round(value) |
| `weight` | weight | value |
| `blood_sugar` | blood_sugar | value |
| `water_intake` | water_intake_ml | Math.round(value) |
| `urine_output` | urine_output_ml | Math.round(value) |
| *(default)* | — | 丢弃(不发送) |
### 4.3 差异清单
#### A. 后端有但小程序未映射6 个业务字段)
| 字段 | 严重性 | 说明 |
|------|--------|------|
| **systolic_bp_evening** | **HIGH** | 后端趋势服务和危急值检测均支持晚间血压,但小程序所有血压数据一律写入 `*_morning` 字段,晚间数据丢失 |
| **diastolic_bp_evening** | **HIGH** | 同上 |
| **body_temperature** | MEDIUM | 后端 entity 和 DTO 均支持体温录入,但小程序无 `body_temperature` indicator_type |
| **spo2** | MEDIUM | 后端支持血氧饱和度,小程序无对应类型 |
| **blood_sugar_type** | LOW | 后端支持区分空腹/餐后/随机/OGTT 血糖类型,小程序仅传数值不传类型 |
| **source** | LOW | 后端默认 "manual",小程序未区分来源标识 |
#### B. 小程序映射了但后端不存在的字段
**无此类差异。** 所有小程序映射的目标字段在后端 CreateVitalSignsReq 中均存在。
### 4.4 根因分析
小程序 `inputVitalSign()``indicator_type` 模型设计为"每种体征一个类型",但血压实际上需要区分时段(晨间/晚间)。当前 `blood_pressure` 类型固定映射到 `*_morning` 字段,没有逻辑可以写入 `*_evening` 字段。
后端设计是完善的——趋势服务([trend_service.rs](crates/erp-health/src/service/trend_service.rs))支持 `systolic_bp_evening`/`diastolic_bp_evening` 指标查询,危急值检测([health_data_service.rs](crates/erp-health/src/service/health_data_service.rs))使用 `morning.or(evening)` 做回退检查。
**修复建议**:新增 `blood_pressure_evening` indicator_type或修改 UI 让用户选择时段。
---
## 5. 评分
| 检查项 | 评分 | 说明 |
|--------|------|------|
| DTO 覆盖率 | 100% | 105 个 DTO 完整覆盖所有 handler传递链一致 |
| 配置参数使用率 | 80% | 24/30 字段活跃使用6 个 AiConfig 字段预留未接入 |
| 权限码声明覆盖率 | 47% | 仅 50/106 个使用的权限码通过 PermissionDescriptor 声明 |
| 权限码一致性 | 98% | 1 处前端拼写错误alert vs alerts |
| 前端权限按钮覆盖 | 26% | 13/50 声明码有 AuthButton其余依赖 API 403 |
| 数据模型映射完整性 | 63% | 6/16 业务字段未映射2 个 HIGH + 2 个 MEDIUM |
| **综合评分** | **69%** | DTO 和配置健壮,权限码和数据模型映射有差距 |

View File

@@ -0,0 +1,243 @@
# HMS 功能审计 — Phase 5: 五种差距模式识别
> 日期: 2026-04-30 | 审计范围: 全系统差距模式分析
## 总览
基于 Phase 0-4 的审计数据,系统性检测五种常见差距模式。
---
## 1. 模式 1"写了没接" — 后端路由无前端调用者
### 检测方法
集合差运算:`backend_routes - web_api_calls - mp_api_calls`
### 发现
| 路由 | 模块 | 说明 | 严重性 |
|------|------|------|--------|
| `POST /health/patients/{id}/trends/generate` | erp-health | 趋势报告生成,无前端调用。可能为后台定时任务触发 | LOW |
| `POST /ai/analyze/vital-signs` | erp-ai | SSE 端点Web 和小程序均无 UI 调用 | MEDIUM |
| `POST /ai/analyze/lab-report` | erp-ai | 同上 | MEDIUM |
| `POST /ai/analyze/health-trend` | erp-ai | 同上 | MEDIUM |
| `POST /ai/analyze/health-summary` | erp-ai | 同上 | MEDIUM |
**AI 分析 SSE 端点说明**4 个 AI 分析端点通过 SSE 流式返回结果。前端未调用是因为 AI 分析功能可能仅通过 API 工具(如 Swagger UI直接测试。这些端点的后端实现完整含权限检查 `ai.analysis.manage`),但缺少前端管理界面的触发入口。
**趋势生成说明**`POST .../trends/generate` 可能设计为后台任务调用(如定时生成趋势报告),而非前端直接触发。
### 统计
| 类别 | 数量 |
|------|------|
| 孤立后端路由(无任何前端调用) | 5 |
| 其中 AI SSE 端点 | 4 |
| 其中后台任务路由 | 1 |
---
## 2. 模式 2"接了没传" — 前端调用但参数传递不完整
### 检测方法
搜索前端 API 调用中空参数传递、缺失 header、字段遗漏。
### 发现
#### 2.1 小程序 vital_signs 字段遗漏6 个)
已在 Phase 4 §4.3 详细分析。核心问题:`indicator_type` 模型无法传递晚间血压、体温、血氧等字段。
#### 2.2 小程序 X-Patient-Id / X-Tenant-Id Header
小程序通过 `request.ts` 拦截器统一注入 `X-Patient-Id``X-Tenant-Id` header。经检查
- 所有患者端 API 调用均通过 `request.ts` 封装header 注入正常
- 医护端 API 调用使用独立的 `doctorRequest` 实例header 注入机制一致
#### 2.3 后端 DTO 字段前端未传递(低风险)
| 端点 | 后端 DTO 字段 | 前端行为 | 风险 |
|------|-------------|---------|------|
| `POST /vital-signs` | blood_sugar_type | 小程序不传,后端默认 NULL | LOW |
| `POST /vital-signs` | source | 小程序不传,后端默认 "manual" | LOW |
| `POST /device-readings/batch` | raw_data | 前端传递完整 JSON | ✓ |
### 统计
| 类别 | 数量 |
|------|------|
| 前端字段遗漏HIGH | 2晚间血压 |
| 前端字段遗漏MEDIUM | 2体温、血氧 |
| 前端字段遗漏LOW | 2血糖类型、来源 |
| Header 注入问题 | 0 |
---
## 3. 模式 3"传了没存" — 数据传递但未完整持久化
### 检测方法
追踪 4 条关键写入路径小程序体征录入、Web 创建预约、BLE 设备同步、每日签到。
### 路径 1小程序体征录入
```
inputVitalSign() → POST /health/patients/{id}/vital-signs
→ health_data_handler::create_vital_signs()
→ health_data_service::create_vital_signs()
→ INSERT vital_signs
```
**结果**:✅ 晨间血压/心率/体重/血糖/饮水量/尿量正常写入。❌ 晚间血压/体温/血氧未写入(前端未传)。
### 路径 2Web 创建预约
```
AppointmentList → POST /health/appointments
→ appointment_handler::create_appointment()
→ appointment_service::create_appointment()
→ CAS 原子检查 slot_available → INSERT appointment
```
**结果**:✅ 完整。含 CAS 并发控制、乐观锁、事务保证。
### 路径 3BLE 设备同步
```
BLE scan → uploadReadings() → POST /health/patients/{id}/device-readings/batch
→ device_reading_handler::batch_create()
→ device_reading_service::sync_readings()
→ INSERT device_readings + UPSERT vital_signs_hourly
```
**结果**:✅ 完整。批量插入 + 小时聚合 + 事件发布。
### 路径 4每日签到
```
checkin() → POST /health/points/checkin
→ points_handler::daily_checkin()
→ points_service::daily_checkin()
→ INSERT/UPDATE points_checkin + INSERT points_transaction
```
**结果**:✅ 完整。幂等检查(同日不可重复签到)+ 事务保证 + 事件发布。
### 统计
| 路径 | 结果 |
|------|------|
| 体征录入 | 部分数据丢失(晚间血压等) |
| 创建预约 | ✅ 完整 |
| BLE 同步 | ✅ 完整 |
| 每日签到 | ✅ 完整 |
---
## 4. 模式 4"存了没用" — 数据持久化但未被消费
### 检测方法
检查写入了但从未被读取或展示的数据。
### 发现
#### 4.1 事件无消费者14 个)
已在 Phase 3 §4.2 详细分析。14 个事件被发布到 EventBus 但没有业务消费者:
- `patient.updated` — 信息性事件,更新已直接处理
- `consultation.*`3 个) — 会话管理已在 service 内直接处理
- `article.published/rejected` — 状态已更新
- `points.earned/exchanged/expired`3 个) — 积分操作已完成
- 其余 5 个同类型
**评估**:这些事件通过 SSE 推送到前端,用于实时通知,不完全是"没用"。但没有专门的后端消费者处理后续业务逻辑。
#### 4.2 数据库字段未读取
| 字段 | 表 | 写入场景 | 读取场景 | 状态 |
|------|-----|---------|---------|------|
| raw_data | device_readings | BLE 同步时写入 | 无专门查询 | MEDIUM |
| revoke_reason | consent | 撤回知情同意时写入 | 列表查询返回 | ✓ 已用 |
| model_config | ai_prompt | 创建/更新 Prompt 时写入 | 分析时读取 model/temperature/max_tokens | ✓ 已用 |
| article_revision | article_revision | 文章编辑时写入 | GET .../revisions 端点返回 | ✓ 已用 |
**raw_data 字段说明**`device_readings.raw_data` 存储 BLE 设备原始 JSONB 数据。当前无专门的读取/分析页面。价值在于数据溯源和异常排查,但在 UI 层面无入口。
#### 4.3 entity 字段从未构造
Phase 2 发现 `RefRow` struct 从未构造(编译器警告),属于重构残留。
### 统计
| 类别 | 数量 | 影响 |
|------|------|------|
| 无消费者事件 | 14 | LOWSSE 推送 + 审计追踪仍有价值) |
| 未读取数据库字段 | 1raw_data | MEDIUM数据溯源需要但无 UI 入口) |
| 死代码 struct | 1RefRow | LOW编译器警告重构残留 |
---
## 5. 模式 5"双系统不同步" — Web 与小程序实现差异
### 检测方法
对比共享功能在 Web 和小程序的实现差异,区分"角色分叉(正常)"和"实现缺陷"。
### 5.1 角色分叉(正常差异)
| 功能 | Web管理端 | 小程序(患者/医护端) | 差异类型 |
|------|-------------|-------------------|---------|
| 积分管理 | 规则 CRUD + 商品管理 + 订单核销 | 签到 + 兑换 + 查看订单 | 角色分叉 ✓ |
| 文章管理 | CMS 全流程(创建/编辑/审核/发布) | 只读列表(仅 published | 角色分叉 ✓ |
| 告警管理 | 规则配置 + 仪表盘 + 处置 | 查看自身告警 + 处理 | 角色分叉 ✓ |
| 设备管理 | 列表 + 解绑 | BLE 扫描 + 同步 | 平台差异 ✓ |
| 预约管理 | 全 CRUD + 排班配置 | 创建/查看/取消预约 | 角色分叉 ✓ |
| 医生管理 | 档案 CRUD + 排班管理 | 查看排班日历 | 角色分叉 ✓ |
### 5.2 实现缺陷(需修复)
| 功能 | Web | 小程序 | 差异类型 | 优先级 |
|------|-----|--------|---------|--------|
| 体征录入 | 结构化表单(所有字段独立输入) | indicator_type 映射(丢失晚间血压/体温/血氧) | **数据模型差异** | P1 |
| 咨询消息 | 列表 + 详情 + 导出 | 列表 + 详情(无导出) | 功能缺失 | P3 |
| 透析管理 | 完整 CRUD + 审阅 | **无任何入口** | **功能缺失** | P1 |
| 知情同意 | 完整 CRUD | **无任何入口** | **功能缺失** | P1 |
| 健康记录 | 完整 CRUD | **无任何入口** | 功能缺失 | P2 |
| 诊断记录 | 完整 CRUD | **无任何入口** | 功能缺失 | P2 |
| 药物管理 | 完整 CRUD | 有页面但 API 调用不明确 | 待验证 | P2 |
| 权限码拼写 | `health.alerts.manage`(正确) | —(前端用 `health.alert.manage` | **拼写错误** | P1 |
### 5.3 透析管理 — 最大差距
透析是唯一一个**后端完整实现但小程序完全空白**的功能域:
- 后端12 个路由(记录 CRUD + 处方 CRUD + 审阅 + 统计)
- Web完整管理界面DialysisManageList
- 小程序0 个 API 调用、0 个页面
**影响**:透析患者无法在移动端查看自己的透析记录和处方,医护无法在小程序端录入透析数据。
### 统计
| 差异类型 | 数量 |
|---------|------|
| 角色分叉(正常) | 6 |
| 数据模型差异 | 1 |
| 功能缺失P1 | 3透析/知情同意/体征映射) |
| 功能缺失P2 | 3健康记录/诊断/药物) |
| 功能缺失P3 | 1咨询导出 |
| 拼写错误 | 1 |
---
## 6. 差距模式汇总评分
| 模式 | 严重程度 | 发现数 | 评分 |
|------|---------|--------|------|
| 模式 1: 写了没接 | LOW-MEDIUM | 5 | 95%(仅 AI SSE 和趋势生成孤立) |
| 模式 2: 接了没传 | HIGH-LOW | 6 | 75%(晚间血压数据丢失是关键缺陷) |
| 模式 3: 传了没存 | LOW | 1 | 95%(仅体征部分字段未传导致未存) |
| 模式 4: 存了没用 | LOW | 16 | 85%(大部分事件有 SSE 消费者raw_data 待利用) |
| 模式 5: 双系统不同步 | HIGH-LOW | 9 | 65%(透析/知情同意完全缺失,体征映射丢失) |

View File

@@ -0,0 +1,227 @@
# HMS 功能审计 — Phase 6: 错误处理与降级审计
> 日期: 2026-04-30 | 审计范围: 错误处理、降级策略、日志完整性
## 总览
| 指标 | 值 |
|------|-----|
| AppError 变体 | 8 个 |
| 领域错误变体 | HealthError 26 + AiError 11 |
| Handler 错误映射机制 | 统一 IntoResponse trait |
| 生产 unwrap() | 18 处13 低风险 + 2 中风险 + 3 无害) |
| 审计日志 | 140 处33 文件)+ SHA256 哈希链 |
| 运行时 tracing 日志 | 11 处health service 层) |
---
## 1. 错误处理覆盖率
### 1.1 AppError 枚举8 变体)
| 变体 | HTTP 状态码 | 触发场景 |
|------|-----------|---------|
| `NotFound(String)` | 404 | 资源不存在 |
| `Validation(String)` | 400 | 输入验证失败 |
| `Unauthorized` | 401 | 未认证 |
| `Forbidden(String)` | 403 | 无权限 |
| `Conflict(String)` | 409 | 唯一约束冲突 |
| `VersionMismatch` | 409 | 乐观锁版本不匹配 |
| `TooManyRequests` | 429 | 速率限制 |
| `Internal(String)` | 500 | 内部错误(消息对外隐藏) |
### 1.2 领域错误映射
**HealthError → AppError**26 变体):
| 映射规则 | 变体示例 | 目标 AppError |
|---------|---------|-------------|
| 资源不存在 | PatientNotFound, DoctorNotFound, ScheduleNotFound | NotFound |
| 状态转换无效 | InvalidStatusTransition, AppointmentAlreadyCancelled | Validation |
| 容量限制 | ScheduleFull | Validation |
| 乐观锁 | VersionMismatch | VersionMismatch (409) |
| 数据库错误 | DbError | Internal |
| 加密错误 | EncryptionError, DecryptionError | Internal |
**AiError → AppError**11 变体):
| 映射规则 | 变体示例 | 目标 AppError |
|---------|---------|-------------|
| 资源不存在 | AnalysisNotFound, PromptNotFound | NotFound |
| 验证失败 | Validation, SanitizationError, TemplateError | Validation |
| 提供商不可用 | ProviderUnavailable | Internal |
| 提供商错误 | ProviderError | Internal |
| 速率限制 | RateLimitExceeded | TooManyRequests (429) |
### 1.3 Handler 错误传播模式
所有 handler 函数统一使用 `?` 运算符自动传播错误:
```
Handler → require_permission()? → service.do_work()? → Ok(Json(ApiResponse::ok(result)))
```
无需手动 match/map`From<DomainError> for AppError` 自动转换。SeaORM 的 `DbErr` 也有智能映射RecordNotFound → 404, duplicate key → 409
### 1.4 PII 加密错误处理
所有加密/解密失败通过 `AppError::Internal` 返回。关键设计:
- 错误细节仅通过 `tracing::error!` 记录到日志
- HTTP 响应体统一替换为 `"内部错误"`,不泄露加密实现细节
- 防止通过错误消息推断加密方案
### 1.5 SSE 端点错误处理
**预流阶段**(连接建立前):
- 权限/验证/数据获取全部通过 `?` 传播
- AI Provider 不可用时返回标准 JSON 错误响应500**不会挂起连接**
**流中阶段**(连接建立后):
- Provider 断连:发送 SSE `error` 事件 → 标记分析记录为 `failed` → 发布 `ai.analysis.failed` 事件 → 优雅终止流
- 序列化错误:使用 `unwrap_or_default()` 避免 panic
**结论SSE 端点不会挂起。** Provider 不可用时有完整的错误传播和清理机制。
### 1.6 生产代码 unwrap() 风险
| 类别 | 数量 | 风险 | 建议 |
|------|------|------|------|
| `active.version.unwrap() + 1` | 13 | LOW | 改用 `expect("version from DB must be set")` |
| `PluginHost::db.unwrap()` | 1 | **MEDIUM** | 改用 `ok_or(AppError::Internal(...))` |
| 信号量 `acquire().unwrap()` | 1 | **MEDIUM** | 改用 `map_err` 处理关闭场景 |
| Response builder `.unwrap()` | 3 | LOW | 可接受,硬编码值不会失败 |
| `unwrap_or`/`unwrap_or_default` | ~20 | NONE | 安全模式 |
---
## 2. 降级策略评估
### 2.1 Redis 不可用
| 场景 | 降级策略 | 评估 |
|------|---------|------|
| IP 限流 | **Fail-Open**Redis 不可达时限流被旁路,请求正常放行 | ✓ 安全(宁可放过不可误杀) |
| 账户锁定 | **可配置**`ERP__RATE_LIMIT__FAIL_CLOSE` 环境变量控制,默认 Fail-Open生产可切 Fail-Close | ✓ 灵活 |
| 微信会话存储 | **优雅降级**`AuthState.redis``Option<redis::Client>`,缺失时降级 | ✓ 安全 |
| 断路器 | 30 秒冷却期,避免重复连接失败阻塞请求 | ✓ 成熟 |
**风险点**`AppState.redis``redis::Client`(非 `Option`),仅 `AuthState``Option` 包裹。但没有全局 Redis 健康检查端点。
### 2.2 AI Provider (Claude) 不可用
| 阶段 | 行为 | 评估 |
|------|------|------|
| 预流(连接前) | 返回 500 JSON 错误,不挂起 | ✓ |
| 流中(连接后) | 发送 SSE error 事件 → 标记失败 → 发布事件 → 终止流 | ✓ |
| 分析记录 | 状态更新为 `failed`,保留错误信息 | ✓ |
**结论AI Provider 不可用不会导致连接挂起。** 有完整的错误传播和清理机制。
### 2.3 EventBus 满载
| 层级 | 机制 | 评估 |
|------|------|------|
| 内存层 | broadcast channel 容量 1024慢消费者收到 `Lagged` 错误 | ✓ |
| 持久化层 | `domain_events` 表 outbox 模式,事件不丢失 | ✓ |
| 兜底轮询 | 30 秒间隔扫描 `pending` 事件 | ✓ |
| 最大重试 | 5 次,超限进入 `dead_letter_events` 表 | ✓ |
**结论:事件不会丢失。** broadcast channel 满载时实时性受影响(最多 30 秒延迟),但 outbox relay 保证最终交付。
### 2.4 前端 SSE 重连
| 维度 | 实现 | 评估 |
|------|------|------|
| 重连机制 | 浏览器原生 EventSource 自动重连 | 基本可用 |
| 指数退避 | 无(浏览器默认 ~3 秒固定间隔) | ⚠️ 缺失 |
| 最大重连次数 | 无(原生无限重试) | ⚠️ 缺失 |
| 连接状态 UI | `useAlertSSE` 暴露 `connected` 状态但未在界面显示 | ⚠️ 缺失 |
| Keep-alive | 后端 `Sse::new().keep_alive(KeepAlive::default())` | ✓ |
---
## 3. 日志完整性
### 3.1 运行时 tracing 日志密度
**erp-health/src/service/ 目录**(核心业务层):
| 日志级别 | 调用次数 | 分布文件 |
|---------|---------|---------|
| `tracing::info!` | 4 | points_service(1), seed(3) |
| `tracing::warn!` | 6 | appointment(1), device_reading(1), health_data(3), points(1), seed(1) |
| `tracing::error!` | 0 | — |
| `tracing::debug!` | 1 | points(1) |
| **总计** | **11** | **5/26 文件** |
**关键缺口**
| 文件 | 行数 | tracing 调用 | 问题 |
|------|------|-------------|------|
| patient_service.rs | 949 | 0 | 患者创建/更新/删除无运行时日志 |
| appointment_service.rs | 590 | 1 | 仅取消预约边缘场景有日志 |
| consultation_service.rs | ~400 | 0 | 咨询会话/消息操作无运行时日志 |
| follow_up_service.rs | ~500 | 0 | 随访任务操作无运行时日志 |
**对比**:基础设施层日志丰富:
- `erp-core/events.rs`EventBus 有完整的生命周期日志
- `erp-server/rate_limit.rs`:限流有 warn/error 日志
- `erp-server/outbox.rs`Outbox relay 有完整的处理日志
### 3.2 审计日志覆盖
审计日志通过 `audit_service::record` 实现,全局 140 处调用分布在 33 个文件中。
**Health 模块审计覆盖**
| 文件 | 审计调用数 | 覆盖操作 |
|------|----------|---------|
| patient_service.rs | 12 | 创建/更新/删除患者、标签管理、家庭成员 CRUD、医生分配 |
| points_service.rs | 12 | 积分发放/兑换/过期 |
| health_data_service.rs | 10 | 体征/化验/体检记录 CRUD + 审核 |
| article_service.rs | 7 | 文章 CRUD |
| follow_up_service.rs | 7 | 随访任务 CRUD |
| appointment_service.rs | 4 | 预约创建/状态变更/排班管理 |
| 其余 7 个文件 | 各 2-3 | 咨询/诊断/同意/医生/透析/设备/告警 |
**审计日志特性**
| 特性 | 实现 |
|------|------|
| 结构化字段 | tenant_id, user_id, action, resource_type, resource_id, ip_address, user_agent |
| 变更追踪 | 更新操作记录 old_value/new_value 快照 |
| 哈希链 | SHA256 链式签名,可验证完整性 |
| 请求来源 | task_local 自动注入 IP + User-Agent |
| 写入模式 | Fire-and-forget失败仅 warn不影响业务 |
**审计日志风险点**
- Fire-and-forget 意味着写入失败是静默的,无重试或告警
- 哈希链在并发写入时可能出现短暂链断裂(查询和写入非原子操作)
### 3.3 日志分层评估
| 层级 | tracing 日志 | 审计日志 | 评估 |
|------|-------------|---------|------|
| Handler 层 | — | — | 依赖 service 层 |
| Service 层health | 11 处 | 70+ 处 | 运行时日志不足,审计日志完善 |
| Service 层infrastructure | 丰富 | — | 日志密度高 |
| EventBus | 完整 | — | 发布/消费全链路可追踪 |
| 加密层 | — | — | 仅错误时 tracing::error |
---
## 4. 评分
| 检查项 | 评分 | 说明 |
|--------|------|------|
| 错误变体覆盖 | 95% | 8 个 AppError + 37 个领域错误,覆盖全面 |
| Handler 错误传播 | 100% | 统一 `?` + IntoResponse无手动 match |
| PII 错误安全 | 100% | Internal 错误消息对外隐藏,仅日志记录 |
| SSE 挂起风险 | 100% | Provider 不可用不会挂起,有完整清理 |
| unwrap() 安全性 | 95% | 2 处中等风险PluginHost::db + 信号量) |
| Redis 降级 | 90% | Fail-Open 断路器 + 可配置,缺健康检查端点 |
| EventBus 降级 | 95% | Outbox 兜底 + 死信队列,事件不丢失 |
| 前端重连 | 60% | 依赖原生 EventSource缺指数退避和 UI 反馈 |
| 运行时日志 | 30% | health service 仅 11 处 tracing运维盲区大 |
| 审计日志 | 95% | 140 处覆盖所有写操作 + 哈希链验证 |
| **综合评分** | **76%** | 错误处理架构优秀,日志和前端重连有差距 |

View File

@@ -0,0 +1,115 @@
# HMS 功能审计 — Phase 7: 测试覆盖率审计
> 日期: 2026-04-30 | 审计范围: 后端 + 前端测试分布与缺口
## 总览
| 指标 | 值 |
|------|-----|
| 测试总数 | 772 个函数 |
| 通过 | 75397.5% |
| 失败 | 9全部因 blind_indexes 表缺失) |
| 跳过 | 0 |
| 后端测试 | 767611 单元 + 153 集成 + 3 多模块集成) |
| Web 前端测试 | 5 单元vitest+ 5 E2Eplaywright |
| 小程序测试 | 0 |
---
## 1. 模块测试分布
### 1.1 后端 Rust 测试
| Crate | 单元测试 | 集成测试 | 总计 | 通过率 | 评估 |
|-------|---------|---------|------|--------|------|
| erp-core | 74 | — | 74 | 100% | 良好 |
| erp-auth | 41 | 3 | 44 | 100% | 良好 |
| erp-config | 78 | — | 78 | 100% | 良好 |
| erp-workflow | 63 | 4 | 67 | 100% | 良好 |
| erp-message | 72 | — | 72 | 100% | 中等(缺集成测试) |
| erp-health | 159 | 144 | 303 | 97% | 良好 |
| erp-ai | 36 | — | 36 | 100% | 中等(缺集成测试) |
| erp-dialysis | 10 | 15 | 25 | 93% | 中等 |
| erp-plugin | 78 | 2 | 80 | 100% | 良好 |
| erp-server | — | 153 | 153 | 94% | 良好API 集成测试) |
| **合计** | **611** | **153** | **767** | **97.5%** | |
### 1.2 前端测试
| 层级 | 框架 | 数量 | 评估 |
|------|------|------|------|
| Web 单元测试 | vitest | 5 | **极低** |
| Web E2E 测试 | playwright | 5 spec | **低** |
| 小程序测试 | — | 0 | **无** |
### 1.3 测试 vs 代码规模比
| 维度 | 代码量 | 测试数 | 比率 |
|------|--------|--------|------|
| Rust 后端 | ~77k 行 | 767 | 1:100 |
| Web 前端 | ~20k 行163 文件) | 10 | 1:2000 |
| 小程序 | ~15k 行125 文件) | 0 | N/A |
---
## 2. 失败测试根因
9 个失败测试全部集中在 erp-health原因一致
| 测试文件 | 失败数 | 根因 |
|---------|--------|------|
| health_patient_tests.rs | 2 | `blind_indexes` 表不存在 |
| health_dialysis_tests.rs | 1 | 同上 |
| health_pii_encryption_tests.rs | 6 | 同上 |
**修复方案**:执行数据库迁移创建 `blind_indexes` 表。这是测试环境配置问题,非代码逻辑错误。
---
## 3. 测试缺口分析
### 3.1 关键缺口(按风险排序)
| 优先级 | 模块 | 缺口 | 影响 |
|--------|------|------|------|
| **P0** | erp-ai | 无集成测试SSE 流 + 外部 API | AI 功能仅通过手动测试验证,无法回归 |
| **P1** | 小程序 | 完全无测试 | 40 个页面全靠手工验证 |
| **P1** | erp-message | 无集成测试SSE 推送) | SSE 连接/重连行为未测试 |
| **P2** | Web 前端 | 仅 10 个测试 | 163 个文件的 API 调用/组件/路由无覆盖 |
| **P2** | erp-dialysis | 93% 通过率 | 2 个测试失败待修复 |
| **P3** | erp-config | 无集成测试 | 简单 CRUD风险较低 |
### 3.2 测试类型覆盖矩阵
| 测试类型 | 后端 | Web 前端 | 小程序 |
|---------|------|---------|--------|
| 单元测试 | ✓ 丰富 | ⚠️ 极少 | ✗ 无 |
| 集成测试 | ✓ health+plugin+server | ✗ 无 | ✗ 无 |
| E2E 测试 | — | ⚠️ 5 spec | ✗ 无 |
| 多租户隔离 | ✓ PII 加密测试 | ✗ 无 | ✗ 无 |
| 并发测试 | ✓ 预约 CAS 测试 | ✗ 无 | ✗ 无 |
| 安全测试 | ⚠️ 部分(权限/注入) | ✗ 无 | ✗ 无 |
### 3.3 测试覆盖良好的领域
| 领域 | 测试特点 |
|------|---------|
| 患者 CRUD | 完整的集成测试覆盖创建/更新/删除/列表 |
| PII 加密 | 独立测试文件验证加密/解密/盲索引/跨租户隔离 |
| 预约并发 | CAS 原子操作测试,验证乐观锁和排班满额 |
| 工作流引擎 | BPMN 解析 + Token 驱动 + 任务分配测试 |
| 权限 RBAC | 角色/权限/菜单关联测试 |
| 插件系统 | WASM 运行时 + 动态表 CRUD + 租户隔离 |
---
## 4. 测试覆盖率评分
| 检查项 | 评分 | 说明 |
|--------|------|------|
| 后端单元测试 | 85% | 611 个,覆盖核心 service 逻辑 |
| 后端集成测试 | 70% | 153 个 API 测试,但 AI/Message 模块缺失 |
| 前端测试 | 5% | 仅 10 个测试覆盖 163 个文件 |
| 小程序测试 | 0% | 完全空白 |
| 测试稳定性 | 98% | 9 个失败因环境配置,非代码缺陷 |
| **综合评分** | **65%** | 后端测试基础扎实,前端和 AI 是主要缺口 |

View File

@@ -0,0 +1,363 @@
# HMS 功能审计报告
> 日期: 2026-04-30 | Git: `84fafb0` | 审计范围: 全系统(后端 + Web + 小程序)
## 执行摘要
| 维度 | 结果 |
|------|------|
| 总体完成度 | **83%** |
| CRITICAL 发现 | **2 项**(小程序晚间血压丢失、告警权限码拼写错误) |
| HIGH 发现 | **3 项**(透析/知情同意小程序缺失、前端日志严重不足) |
| MEDIUM 发现 | **8 项** |
| LOW 发现 | **12 项** |
| 后端健康度 | 优秀97.5% 测试通过100% 调用链连通) |
| 前端覆盖度 | 中等Web 管理端完善,小程序关键功能缺失) |
### 审计范围
- 328 个后端路由8 公开 + 320 受保护)
- 235 个 Web 前端 API 调用
- 76 个小程序 API 调用
- 38 个 Web 页面路由
- 40 个小程序页面
- 45 个数据库 Entity
- 25 个事件类型
- 772 个测试函数
---
## 功能域评分
### 评分方法
每个功能域按 10 项审计清单打分0-100%),加权求和:
| 检查项 | 权重 | 100% 标准 |
|--------|------|----------|
| 代码存在性 | 15% | Handler + Service + Entity 全部存在 |
| 调用链连通性 | 15% | Handler→Service→DB 完整连通 |
| 配置参数传递 | 5% | 所有配置字段被正确读取和使用 |
| 降级策略 | 5% | 外部依赖不可用时有优雅降级 |
| 错误处理 | 10% | 所有错误路径返回适当 HTTP 响应 |
| 性能 | 5% | 列表端点有分页,查询有索引 |
| 安全合规 | 15% | tenant_id 过滤 + 权限检查 + PII 保护 |
| 兼容性 | 5% | Web 和小程序 API 契约一致 |
| 日志完整性 | 10% | 关键操作有日志和审计记录 |
| UX 一致性 | 15% | 两端展示数据一致,交互逻辑统一 |
### 1. 患者管理: 93%
| 检查项 | 评分 | 说明 |
|--------|------|------|
| 代码存在性 | 100% | 17 handler + 完整 service + 6 entity |
| 调用链 | 100% | Handler→Service→Entity 全连通 |
| 配置参数 | 100% | PII 加密密钥正确使用 |
| 降级策略 | 90% | 加密失败有错误处理 |
| 错误处理 | 100% | 26 种领域错误完整映射 |
| 性能 | 90% | 分页查询 + HMAC 盲索引搜索 |
| 安全合规 | 100% | tenant_id + RLS + PII AES-256-GCM + HMAC |
| 兼容性 | 90% | Web/MP 基本对齐MP 无删除(预期) |
| 日志 | 95% | 12 处审计日志(含变更快照),运行时日志不足 |
| UX 一致性 | 70% | 健康摘要仅 MP家庭医生管理仅 Web |
### 2. 医生/排班: 88%
| 检查项 | 评分 | 说明 |
|--------|------|------|
| 代码存在性 | 100% | 5 handler + service + 2 entity |
| 调用链 | 100% | 全连通 |
| 配置参数 | 100% | 正确 |
| 降级策略 | 90% | 标准 |
| 错误处理 | 100% | 领域错误映射 |
| 性能 | 90% | 排班 CAS 并发控制 |
| 安全合规 | 100% | tenant_id + 权限检查 |
| 兼容性 | 60% | MP 仅只读列表,无管理操作 |
| 日志 | 90% | 3 处审计日志 |
| UX 一致性 | 60% | Web 有排班 CRUDMP 仅查看日历 |
### 3. 健康数据: 85%
| 检查项 | 评分 | 说明 |
|--------|------|------|
| 代码存在性 | 100% | 18 handler + service + 7 entity |
| 调用链 | 100% | 全连通 |
| 配置参数 | 100% | AES/HMAC 密钥正确使用 |
| 降级策略 | 90% | 加密失败安全处理 |
| 错误处理 | 100% | 完整错误映射 |
| 性能 | 85% | 分页 + 趋势缓存,但趋势生成可能慢 |
| 安全合规 | 100% | PII 加密 + 权限 + 危急值检测 |
| 兼容性 | 70% | MP 丢失晚间血压/体温/血氧字段 |
| 日志 | 80% | 10 处审计日志,运行时 tracing 不足 |
| UX 一致性 | 60% | 小程序 indicator_type 模型限制数据录入 |
### 4. 预约管理: 95%
| 检查项 | 评分 | 说明 |
|--------|------|------|
| 代码存在性 | 100% | 8 handler + 完整 CRUD |
| 调用链 | 100% | 全连通 |
| 配置参数 | 100% | 正确 |
| 降级策略 | 95% | CAS 原子操作保证 |
| 错误处理 | 100% | 排班满额/状态转换错误 |
| 性能 | 100% | CAS 并发控制 + 分页 |
| 安全合规 | 100% | tenant_id + 权限 |
| 兼容性 | 90% | Web/MP 基本对齐 |
| 日志 | 90% | 4 处审计 + 1 处 tracing |
| UX 一致性 | 85% | Web 排班管理MP 预约创建/查看 |
### 5. 随访管理: 88%
| 检查项 | 评分 | 说明 |
|--------|------|------|
| 代码存在性 | 100% | 10+5 handler + 模板管理 |
| 调用链 | 100% | 全连通 |
| 配置参数 | 100% | 正确 |
| 降级策略 | 90% | 事件驱动逾期检测 |
| 错误处理 | 100% | 完整 |
| 性能 | 90% | 批量操作支持 |
| 安全合规 | 100% | 权限检查 |
| 兼容性 | 80% | MP 仅列表+创建,无管理 |
| 日志 | 90% | 7 处审计 |
| UX 一致性 | 70% | Web 批量操作MP 基础功能 |
### 6. 咨询管理: 94%
| 检查项 | 评分 | 说明 |
|--------|------|------|
| 代码存在性 | 100% | 9 handler |
| 调用链 | 100% | 全连通 |
| 配置参数 | 100% | 正确 |
| 降级策略 | 95% | 消息持久化保证 |
| 错误处理 | 100% | 完整 |
| 性能 | 90% | 分页消息列表 |
| 安全合规 | 100% | 权限 + 数据隔离 |
| 兼容性 | 95% | Web/MP 高度对齐 |
| 日志 | 85% | 3 处审计 |
| UX 一致性 | 90% | Web 有导出MP 无(预期) |
### 7. 内容管理: 86%
| 检查项 | 评分 | 说明 |
|--------|------|------|
| 代码存在性 | 100% | 11+4+4 handler文章+分类+标签) |
| 调用链 | 100% | 全连通 |
| 配置参数 | 100% | 文件上传配置 |
| 降级策略 | 90% | 标准 |
| 错误处理 | 100% | 审核流程错误 |
| 性能 | 90% | 分页 |
| 安全合规 | 100% | 审核权限分离 |
| 兼容性 | 70% | MP 仅只读 published 文章 |
| 日志 | 90% | 7 处审计 |
| UX 一致性 | 65% | Web 完整 CMSMP 仅文章列表 |
### 8. 积分商城: 90%
| 检查项 | 评分 | 说明 |
|--------|------|------|
| 代码存在性 | 100% | 28 handler最复杂的 handler |
| 调用链 | 100% | 全连通 |
| 配置参数 | 100% | 正确 |
| 降级策略 | 90% | 幂等签到检查 |
| 错误处理 | 100% | 完整 |
| 性能 | 90% | 分页 + 统计缓存 |
| 安全合规 | 95% | 权限检查,但积分操作无事务日志 |
| 兼容性 | 90% | Web 管理端 + MP 患者端对齐 |
| 日志 | 85% | 12 处审计 |
| UX 一致性 | 85% | 角色分叉正常 |
### 9. 告警系统: 87%
| 检查项 | 评分 | 说明 |
|--------|------|------|
| 代码存在性 | 100% | 4+4+3+4 handler |
| 调用链 | 100% | 全连通 |
| 配置参数 | 100% | 正确 |
| 降级策略 | 90% | 事件驱动评估 |
| 错误处理 | 100% | 完整 |
| 性能 | 90% | 分页 |
| 安全合规 | 100% | 权限检查 |
| 兼容性 | 70% | MP 仅查看+处理,无规则管理 |
| 日志 | 85% | 审计日志 |
| UX 一致性 | 60% | ⚠️ 前端 `health.alert.manage` 拼写错误导致按钮不显示 |
### 10. AI 分析: 70%
| 检查项 | 评分 | 说明 |
|--------|------|------|
| 代码存在性 | 100% | 12 handler + 3 entity |
| 调用链 | 100% | 全连通 |
| 配置参数 | 40% | 6/8 AiConfig 字段未使用 |
| 降级策略 | 95% | SSE 不挂起Provider 不可用优雅终止 |
| 错误处理 | 100% | 预流/流中完整错误处理 |
| 性能 | 80% | SSE 流式返回,但无缓存层 |
| 安全合规 | 100% | 权限检查 + 输入消毒 |
| 兼容性 | 50% | SSE 端点无前端 UI 调用 |
| 日志 | 70% | 分析成功/失败有事件发布 |
| UX 一致性 | 30% | 仅历史查看有 UI分析触发无入口 |
### 11. 透析管理: 67%
| 检查项 | 评分 | 说明 |
|--------|------|------|
| 代码存在性 | 100% | 12 handler |
| 调用链 | 100% | 全连通 |
| 配置参数 | 100% | 正确 |
| 降级策略 | 90% | 标准 |
| 错误处理 | 100% | 完整 |
| 性能 | 90% | 分页 |
| 安全合规 | 100% | 5 个权限码 |
| 兼容性 | 0% | 小程序完全无入口 |
| 日志 | 90% | 审计 + 事件 |
| UX 一致性 | 0% | 小程序无透析功能 |
### 12. 统计仪表盘: 85%
| 检查项 | 评分 | 说明 |
|--------|------|------|
| 代码存在性 | 100% | 9 handler |
| 调用链 | 100% | 全连通 |
| 配置参数 | 100% | 正确 |
| 降级策略 | 85% | 统计查询失败返回 500 |
| 错误处理 | 90% | 标准 |
| 性能 | 80% | 聚合查询可能慢 |
| 安全合规 | 100% | 权限检查 |
| 兼容性 | 60% | MP 仅 3 个医护端统计 |
| 日志 | 80% | 标准 |
| UX 一致性 | 70% | Web 完整仪表盘MP 部分 |
---
## 发现清单(按严重程度排序)
### CRITICAL2 项)
| # | 发现 | 模块 | 来源 |
|---|------|------|------|
| C1 | **小程序晚间血压数据永久丢失**`inputVitalSign()``blood_pressure` 类型固定映射到 `*_morning` 字段,`systolic_bp_evening`/`diastolic_bp_evening` 从未写入。后端趋势服务和危急值检测支持晚间血压,但数据源缺失 | Phase 4 §4.3 | |
| C2 | **告警管理按钮永远不显示** — 前端 AlertList.tsx 使用 `health.alert.manage`(单数),后端声明 `health.alerts.manage`(复数),权限码不匹配导致 AuthButton 永远隐藏 | Phase 4 §3.4 | |
### HIGH3 项)
| # | 发现 | 模块 | 来源 |
|---|------|------|------|
| H1 | **透析管理小程序完全无入口** — 后端 12 个路由完整实现Web 有管理界面,但小程序 0 个 API 调用、0 个页面。透析患者无法在移动端查看记录和处方 | Phase 1 §2.11, Phase 5 §5.2 | |
| H2 | **知情同意小程序完全无入口** — 同上,后端完整但小程序无任何入口 | Phase 1 §2.12 | |
| H3 | **Health service 层运行时日志极缺** — 26 个 service 文件仅 11 处 tracing 日志patient_service949 行0 处日志,运维排查困难 | Phase 6 §3.1 | |
### MEDIUM8 项)
| # | 发现 | 说明 | 来源 |
|---|------|------|------|
| M1 | 56 个基础模块权限码未通过 PermissionDescriptor 声明 | auth/config/workflow/message/plugin 的权限通过种子数据手动注册 | Phase 4 §3.3 |
| M2 | 4 个 AI 分析 SSE 端点无前端 UI 调用 | 可能仅通过 API 工具直接测试 | Phase 5 §1 |
| M3 | 小程序丢失体温/血氧数据 | body_temperature/spo2 无 indicator_type | Phase 4 §4.3 |
| M4 | 前端 SSE 重连无指数退避 | 依赖浏览器原生 EventSource固定 3 秒间隔 | Phase 6 §2.4 |
| M5 | erp-ai 无集成测试 | SSE 流 + 外部 API 调用无法回归测试 | Phase 7 §3.1 |
| M6 | Web 前端测试仅 10 个 | 163 个文件仅 5 单元 + 5 E2E | Phase 7 §1.2 |
| M7 | 小程序完全无测试 | 40 个页面全靠手工验证 | Phase 7 §1.2 |
| M8 | 健康记录/诊断记录小程序无入口 | 患者移动端无法查看 | Phase 1 §2.2 |
### LOW12 项)
| # | 发现 | 说明 | 来源 |
|---|------|------|------|
| L1 | 14 个事件无业务消费者 | 通过 SSE 推送仍有价值,但无后端消费者 | Phase 3 §4.2 |
| L2 | AiConfig 6 字段声明未使用 | 预留全局默认值per-prompt 已覆盖 | Phase 4 §2.3 |
| L3 | device_readings.raw_data 无读取入口 | 数据溯源需要但无 UI | Phase 5 §4.2 |
| L4 | RefRow struct 从未构造 | 重构残留,编译器警告 | Phase 2 §1.2 |
| L5 | 2 处 unwrap() 中等风险 | PluginHost::db + 信号量 acquire | Phase 6 §1.6 |
| L6 | 审计日志 Fire-and-forget | 写入失败静默,无重试 | Phase 6 §3.2 |
| L7 | health.dialysis.stats 权限无前端引用 | 透析统计页面可能无按钮 | Phase 4 §3.2 |
| L8 | ai.provider.manage 声明但无 Handler | 预留提供商管理功能 | Phase 4 §3.2 |
| L9 | 咨询消息导出仅 Web | 小程序无导出功能(预期) | Phase 1 §2.5 |
| L10 | 趋势生成路由无前端调用 | 可能仅后台任务触发 | Phase 5 §1 |
| L11 | 9 个测试因 blind_indexes 表失败 | 环境配置问题,非代码缺陷 | Phase 0 |
| L12 | 40 个编译警告 | 多为未使用导入/变量 | Phase 0 |
---
## 差距模式分析
基于 Phase 5 的系统性检测,五种差距模式的发生频率和影响:
| 模式 | 发现数 | 最大影响 | 整体评估 |
|------|--------|---------|---------|
| 写了没接 | 5 | AI SSE 端点无 UI 入口 | 低风险(功能完整,缺触发入口) |
| 接了没传 | 6 | 晚间血压数据丢失 | 高风险(数据永久丢失) |
| 传了没存 | 0 | — | 无问题 |
| 存了没用 | 16 | 14 事件无消费者 + raw_data 无入口 | 低风险(审计追踪仍有价值) |
| 双系统不同步 | 9 | 透析/知情同意小程序完全缺失 | 高风险(核心功能移动端空白) |
**根因模式**
1. **后端先行,前端跟进不足** — 后端功能完整度高100% 调用链),但小程序覆盖仅 60%
2. **角色分叉设计正确但执行不彻底** — 透析/知情同意应覆盖小程序医护端但未实现
3. **数据模型差异** — 小程序 indicator_type 简化模型与后端完整字段不匹配
---
## 测试覆盖率总结
| 层级 | 测试数 | 通过率 | 评估 |
|------|--------|--------|------|
| Rust 单元测试 | 611 | 100% | 良好 |
| Rust 集成测试 | 153 | 94% | 良好 |
| Web 前端 | 10 | N/A | **极低** |
| 小程序 | 0 | N/A | **无测试** |
| **合计** | **772** | **97.5%** | |
关键缺口AI 模块无集成测试SSE + 外部 API小程序完全无测试。
---
## 建议与优先级排序
### P0 — 立即修复1-2 天)
| # | 建议 | 影响 | 工作量 |
|---|------|------|--------|
| C2 | 修复前端权限码拼写:`health.alert.manage``health.alerts.manage` | 告警管理按钮恢复可用 | 5 分钟 |
| C1-fix | 小程序新增 `blood_pressure_evening` indicator_type | 晚间血压数据可正常录入 | 2 小时 |
### P1 — 短期优化1-2 周)
| # | 建议 | 影响 | 工作量 |
|---|------|------|--------|
| H1 | 实现小程序透析模块(患者查看记录 + 医护端录入) | 透析患者移动端可用 | 1 周 |
| H2 | 实现小程序知情同意页面 | 患者可管理同意书 | 3 天 |
| H3 | 补充 health service 层 tracing 日志 | 运维可追踪关键操作 | 2 天 |
| M5 | 补充 erp-ai 集成测试 | AI SSE 流可回归测试 | 3 天 |
### P2 — 中期改进1-2 月)
| # | 建议 | 影响 | 工作量 |
|---|------|------|--------|
| M1 | 为 auth/config/workflow/message/plugin 模块补充 PermissionDescriptor 声明 | 权限码自动注册,一致性提升 | 1 周 |
| M3 | 小程序新增 body_temperature/spo2 indicator_type | 体温/血氧可录入 | 1 天 |
| M8 | 实现小程序健康记录/诊断记录查看页面 | 患者可查看完整健康档案 | 1 周 |
| M4/M6/M7 | 补充前端 SSE 指数退避 + 前端/小程序测试 | 系统健壮性提升 | 持续 |
### P3 — 长期规划
| # | 建议 | 影响 | 工作量 |
|---|------|------|--------|
| L1 | 为高价值事件添加消费者(如 consultation.opened 发送通知) | 事件驱动更完善 | 按需 |
| L2 | 清理或接入 AiConfig 未使用字段 | 配置一致性 | 1 天 |
| L5 | 替换 2 处中风险 unwrap() | 消除潜在 panic | 1 小时 |
| L11 | 修复 blind_indexes 迁移使 9 个测试通过 | 测试通过率 100% | 1 小时 |
---
## 审计产出文件索引
| Phase | 文件 | 内容 |
|-------|------|------|
| 0 | [00-baseline-snapshot.md](docs/audits/00-baseline-snapshot.md) | 基线快照 |
| 1 | [01-feature-inventory.md](docs/audits/01-feature-inventory.md) | 功能清单 + 三端映射矩阵 |
| 2 | [02-backend-integrity.md](docs/audits/02-backend-integrity.md) | 死代码 + 调用链 + trait 覆盖率 |
| 3 | [03-event-system.md](docs/audits/03-event-system.md) | 事件发布-消费矩阵 |
| 4 | [04-parameter-config.md](docs/audits/04-parameter-config.md) | DTO + 配置 + 权限码 + 数据映射 |
| 5 | [05-gap-patterns.md](docs/audits/05-gap-patterns.md) | 五种差距模式 |
| 6 | [06-error-handling.md](docs/audits/06-error-handling.md) | 错误处理 + 降级 + 日志 |
| 7 | [07-test-coverage.md](docs/audits/07-test-coverage.md) | 测试分布与缺口 |
| 8 | [08-audit-report-2026-04-30.md](docs/audits/08-audit-report-2026-04-30.md) | 最终评分报告(本文档) |

View File

@@ -0,0 +1,115 @@
# 设备数据全链路发散式探讨
> 日期: 2026-04-28 | 参与者: 用户 + Claude
## 背景
客户提出血压计、血糖仪需要支持自动上传数据。项目已有实时体征管线设计(`docs/superpowers/specs/2026-04-26-realtime-vital-signs-pipeline-design.md`Phase 1手环数据管线和 Phase 2告警引擎 + SSE已基本实现但血压计/血糖仪 BLE 适配器被设计规格明确放在了 Phase 3。
本次探讨从客户需求出发,延伸到 AI 趋势分析、医生端仪表盘、告警通知渠道等方向。
## 讨论要点
### 1. 血压计/血糖仪 BLE 适配器设计
客户接受蓝牙标准协议Bluetooth SIG不需要按品牌逐个对接。
**已完成的底层管线:**
- `device_readings` 分区表 + Entity已实现
- `patient_devices` 设备绑定表(已实现)
- `alert_rules` + `alerts` 告警表(已实现)
- 批量上传 API、小时级降采样、告警引擎3 种条件类型、SSE 推送、EventBus 集成
- 小程序 BLE 基础架构BLEManager + DeviceAdapter 接口 + 小米手环适配器)
- 集成测试8 个)
**6 个关键决策:**
| 决策点 | 结论 | 理由 |
|--------|------|------|
| 设备协议 | Bluetooth SIG 标准协议0x1810 血压 / 0x1808 血糖) | 一个适配器兼容所有标准设备 |
| 数据模型 | 多行拆分 + `metric` 字段 | 一条血压测量 → 3 行systolic/diastolic/map告警引擎直接用 value 比较 |
| BLE 读取 | Indicate实时+ RACP历史读取一起做 | 血糖仪患者一天测多次,批量上传是刚需 |
| 数据路由 | 双写:`device_readings` + 自动写入 `vital_signs` | 设备数据自动成为正式体征记录,患者不需手动录入 |
| 告警阈值 | 临床标准值,租户可自定义覆盖 | 医疗标准 + 灵活性 |
| 患者体验 | 极简式:首页设备卡片,点击连接,测完自动上传 | 类似小米运动体验,降低操作门槛 |
**已知管线缺陷(本次不修但需记录):**
- 批量插入缺少 ON CONFLICT 去重
- SSE 告警是租户广播,未按主治医生过滤
- 小程序无离线缓存(杀进程数据丢失)
- 小米手环适配器只支持实时 notify不支持历史读取
- 无数据保留清理任务90 天分区自动删除)
### 2. AI 趋势分析
设备数据 + AI = 可行动的临床洞察。从"数字"变成"建议"。
**技术路线:混合方案**
- 统计引擎(纯 Rust移动平均、变化率、Z-score 异常检测、基线比较 → 产出确定性结构化中间结果
- LLMClaude API基于统计结果 + 患者档案 + 用药信息 → 生成自然语言医生建议
- 统计层作为 LLM 护栏,防止幻觉
**架构愿景(通用管线):**
```
设备数据 → 统计引擎(通用)→ 结构化中间结果 → LLM 上下文合成 → 自然语言洞察
```
适用于血压、血糖、心电图、体温连续监测、体重变化等所有体征数据。
**erp-ai 模块现状:** 3 个 Entity 已创建(`ai_analysis``ai_report``ai_model_config`),但 Phase 1 化验单解读使用占位数据,需要真实化。
### 3. 医生端告警仪表盘
**核心模式:告警驱动**
- 登录即看到按严重级排序的告警时间线
- 80% 的告警通过侧滑抽屉Drawer处理不需要跳转页面
- 处理完后切换到"患者列表"视图(按风险等级排序)
**三层信息架构:**
1. 全局态势 — 统计卡片(新告警数/高危数/活跃患者/处理率)+ 告警时间线
2. 患者快照 — 最新体征 + AI 摘要 + 用药清单 + 快捷操作
3. 患者详情 — 完整趋势图 + 设备数据 + AI 分析报告 + 病历
**告警处置:结构化处理**
- 医生必须选择处置方式(已电话联系/已调整用药/已转诊/继续观察)+ 备注
- 形成完整医疗记录和审计追溯
- 告警生命周期:触发 → 通知 → 响应 → 处置 → 关闭 → 记录
**与现有 UI/UX 重构的对接:** `2026-04-28-ui-ux-overhaul-design.md` 已有角色自适应仪表盘和表单抽屉策略,可以复用。
### 4. 告警通知渠道
SSE 只在医生浏览器打开时有效。critical/urgent 告警可能出现在凌晨。
**通知链路:**
```
告警触发 (alert_engine)
→ EventBus: alert.triggered
→ SSE: 医生在线时即时推送
→ 微信模板消息: 跳转到小程序告警详情页
→ 短信: critical/urgent 级别兜底通知(需对接短信服务商)
```
## 结论 / 待定
### 达成的共识
1. **设备对接**使用 Bluetooth SIG 标准协议,一套适配器覆盖所有标准血压计/血糖仪
2. **数据管线**复用已有基础设施BLEManager、DeviceAdapter、批量上传 API、告警引擎
3. **AI 分析**采用统计引擎 + LLM 混合方案
4. **医生端**采用告警驱动仪表盘 + 结构化处置
5. **通知渠道**先做微信模板消息 + 短信
### 3 个潜在设计规格方向
1. **血压计/血糖仪 BLE 适配器** — 技术设计,可独立成 spec实现范围最明确
2. **医生端告警仪表盘** — UI/UX + 产品设计,对接现有 UI 重构
3. **AI 趋势分析引擎** — 架构设计,连接 erp-ai + erp-health
### 待定问题
- 血糖采样类型(空腹/餐后/随机)在 `metric` 字段中的编码方式
- 微信模板消息需要申请哪些模板 ID
- 短信服务商选择(阿里云 vs 腾讯云)
- 是否需要修复现有管线缺陷作为本次工作的前置任务
- AI 趋势分析的触发频率和成本控制

View File

@@ -0,0 +1,119 @@
# E2E 跨平台业务联调测试报告
> 日期: 2026-04-30 | 参与者: Claude Code 自动化测试
## 背景
对 HMS 健康管理平台进行端到端业务联调,验证 Web 端(管理员/医生/护士)和小程序端(患者)四个角色的核心业务流程和数据一致性。
## 测试环境
| 组件 | 地址 | 状态 |
|------|------|------|
| 后端 API | `http://localhost:3000/api/v1` | 运行中 |
| Web 前端 | `http://localhost:5174` | 运行中 |
| PostgreSQL | `localhost:5432/erp` | 运行中 |
| Redis | 云端 | 运行中 |
### 测试账号
| 角色 | 用户名 | 密码 |
|------|--------|------|
| 管理员 | admin | Admin@2026 |
| 医生 | doctor1 | Doctor@2026 |
| 护士 | nurse1 | Nurse@2026 |
### 测试数据
| 数据 | ID |
|------|-----|
| 联调测试患者 | `019ddf17-6c67-78b0-b3cc-c41512b76b12` |
| 预约 (2026-05-05) | `019ddf1c-7dd6-7ee3-b012-ef1fd703ef15` |
| 体征记录 1 (空值) | `019ddf2c-91ee-76e0-8fdf-c1a78ee45a5b` |
| 体征记录 2 (血压135/88) | `019ddf2c-cdb3-7f00-9547-21e364720845` |
## 测试结果总览
| 测试项 | 结果 | 备注 |
|--------|------|------|
| Admin 登录 + 仪表盘 | ✅ 通过 | 显示统计概览 |
| Admin 创建患者 | ✅ 通过 | "联调测试患者" 创建成功 |
| Admin 创建预约 | ✅ 通过 | 需先创建排班CAS 匹配 start_time |
| Doctor 登录 + 仪表盘 | ✅ 通过 | 显示医生专属视图 |
| Doctor 查看患者列表 | ✅ 通过 | 可看到 Admin 创建的患者 |
| Nurse 登录 + 仪表盘 | ✅ 通过 | 显示"随访监控台" |
| Nurse 查看告警 | ✅ 通过 | 权限正确限制 |
| 跨角色数据一致性 | ✅ 通过 | 三角色查询同一患者数据一致 |
| 小程序 API - 患者详情 | ✅ 通过 | |
| 小程序 API - 预约列表 | ✅ 通过 | 返回 1 条预约 |
| 小程序 API - 体征数据 | ✅ 通过 | 创建 + 查询链路正常 |
| 小程序 API - 文章列表 | ✅ 通过 | 4 篇已发布文章 |
| 小程序 API - 随访任务 | ✅ 通过 | 0 条(预期,未创建) |
| 小程序 API - 告警列表 | ✅ 通过 | 0 条(预期,未触发) |
| 小程序 API - 积分账户 | ✅ 通过 | 路径 `points/account`(单数) |
## 发现的问题
### 已确认非问题(之前误报)
1. **预约查询返回 0 条** — 实际是 token 中 tenant_id 与数据不一致导致。使用正确角色的 token 查询正常返回数据。
2. **文章列表返回 0 条** — 同上token 问题。Admin/Doctor 角色查询正常返回 5 条4 published + 1 draft
### 已知设计限制
1. **预约 CAS 精确匹配 start_time** — 预约只能预约排班的精确时段,不支持排班时段内的子时间段。例如排班 08:00-12:00预约 start_time 必须是 08:00不能是 09:00。
2. **Nurse 角色文章 403** — 护士无 `health.articles.list` 权限,返回 403 是正确行为。
3. **体征创建字段映射**`systolic`/`diastolic` 请求参数不映射到 `systolic_bp_morning`/`diastolic_bp_morning`,需要使用精确字段名。
### 路径注意事项
| 资源 | 正确 API 路径 | 易错路径 |
|------|---------------|----------|
| 体征数据 | `/health/patients/{id}/vital-signs` | `/health/vital-signs` (404) |
| 设备读数 | `/health/patients/{id}/device-readings` | `/health/device-readings` (404) |
| 积分账户 | `/health/points/account` (单数) | `/health/points/accounts` (404) |
| 积分交易 | `/health/points/transactions` | - |
## 跨角色数据一致性矩阵
| API 端点 | Admin | Doctor | Nurse |
|----------|-------|--------|-------|
| 患者预约 (total=1) | 1 ✅ | 1 ✅ | - |
| 患者体征 (total=2) | 2 ✅ | 2 ✅ | 2 ✅ |
| 已发布文章 (total=4) | 4 ✅ | 4 ✅ | 403 ✅ (无权限) |
| 患者详情 | ✅ | ✅ | ✅ |
**结论:同租户内跨角色数据完全一致,权限隔离正确。**
## 小程序端测试详情
通过 API 层模拟小程序患者视角的完整业务流:
1. **查看个人信息**`GET /health/patients/{id}`
2. **查看我的预约**`GET /health/appointments?patient_id={id}`
3. **查看体征数据**`GET /health/patients/{id}/vital-signs`
4. **查看健康文章**`GET /health/articles?status=published`
5. **查看随访任务**`GET /health/follow-up-tasks?patient_id={id}` ✅ (0 条)
6. **查看告警**`GET /health/alerts` ✅ (0 条)
7. **查看积分**`GET /health/points/account?patient_id={id}`
### 未覆盖项
- MCP 连接微信开发者工具进行 UI 级测试DevTools 未连接)
- 小程序微信登录流程(需要 dev mode 环境)
- 体征数据趋势图 API`/health/vital-signs/trend` 返回 404可能未注册路由
## 结论
**HMS 平台核心业务链路功能正常:**
- Web 端三角色admin/doctor/nurse登录、权限控制、数据访问全部通过
- 跨角色数据一致性验证通过(同租户内数据共享正确)
- 小程序端患者视角 API 全部可达且返回正确
- 权限隔离按设计工作nurse 无法访问文章管理等超出职责的功能)
- 预约 CAS 并发控制工作正常
**建议后续改进:**
1. 体征 API 添加字段别名映射(`systolic``systolic_bp_morning`)降低调用门槛
2. 体征趋势 API 路由确认(`/health/vital-signs/trend` 返回 404
3. 预约 CAS 支持更灵活的时段匹配

View File

@@ -0,0 +1,351 @@
# 三端联调深度审计报告
> 日期: 2026-05-01 | 参与者: Claude + iven
> 目标: 记录项目最真实的状况,从全局/用户/管理者三重角度审视系统
---
## 1. 基础设施状态
### 1.1 编译与构建
| 检查项 | 状态 | 详情 |
|--------|------|------|
| `cargo check` | 通过(修复后) | 修复了 2 个编译错误:`HealthError::DatabaseError``DbError``approval_status` move 问题 |
| `cargo test --workspace` | **失败** | `erp-plugin` 测试编译失败:`parse_manifest` 未导入(`#[cfg(test)]` 缺少 `use crate::manifest::parse_manifest` |
| `pnpm build` (Web) | 通过 | 耗时 33.67santd chunk 1.5MB 过大 |
| `pnpm build:weapp` (小程序) | 通过 | Sass @import deprecation 警告pkg-health 分包 352KB 超限 |
### 1.2 数据库数据分布
| 表 | 记录数 | 评估 |
|----|--------|------|
| patient | 32 | 有测试数据 |
| doctor_profile | 8 | 有测试数据 |
| appointment | 12 | 有测试数据 |
| doctor_schedule | 15 | 有测试数据 |
| follow_up_task | 12 | 有测试数据 |
| follow_up_record | 3 | 较少 |
| consultation_session | 4 | 有测试数据 |
| consultation_message | 7 | 有测试数据 |
| article | 5 | 有测试数据 |
| alert_rules | 10 | 有测试数据 |
| ai_prompt | 4 | 有测试数据 |
| offline_event | 7 | 有测试数据 |
| health_record | 1 | 极少 |
| lab_report | 2 | 较少 |
| ai_analysis | 0 | **空表** |
| ai_suggestion | 0 | **空表** |
| alerts | 0 | **空表** |
| daily_monitoring | 0 | **空表** |
| device_readings | 0 | **空表** |
| critical_alert | 0 | **空表** |
| dialysis_prescription | 0 | **空表** |
| patient_family_member | 0 | **空表** |
| patient_tag_relation | 0 | **空表** |
| medication_reminder | 0 | **空表** |
| medication_record | 0 | **空表** |
| domain_events | 1166 | 大量事件堆积 |
**关键发现**: 32 个表中 12 个完全空表37.5%),说明很多功能从未被端到端使用过。
---
## 2. 后端完整性
### 2.1 API 路由统计
| 模块 | 路由端点数 | CRUD 完整领域数 |
|------|-----------|----------------|
| erp-health | 98 | 15/15 全部完整 |
| erp-ai | 13 | 管理类全覆盖 |
| erp-dialysis | 8 | 2/2 完整 |
| **总计** | **119** | — |
### 2.2 后端已实现但无前端 UI 的端点(功能孤岛)
| 分类 | 端点数 | 具体内容 |
|------|--------|---------|
| AI 分析 SSE 触发 | 4 | lab-report/trends/checkup-plan/report-summary无管理界面入口 |
| 危急值告警体系 | 4 | 列表/详情/确认/阈值配置Web 端完全无页面 |
| 行动收件箱 | 2 | 列表/线程查看,无前端消费 |
| 统计数据 | 5 | dashboard/lab-reports/appointments/vital-signs-report-rate/health-data |
| 趋势生成 | 1 | `POST /trends/generate` 无手动触发入口 |
| **合计** | **16** | **约 13% 的后端路由为"死端点"** |
### 2.3 Service 层健康度
- Service → Handler → Router 映射链路: **100% 完整**
- 内部 Service定时任务/事件消费者/工具函数): 均有合理调用方
- 所有实体领域达到完整 CRUD 覆盖
---
## 3. Web 前端完整性
### 3.1 路由与页面
- 健康管理路由: **30 条**
- 基础 ERP 路由: **17 条**
- 健康模块共享组件: **11 个**
- API 服务文件: **12 个 health + 4 个 AI**
### 3.2 页面功能抽样评审
| 页面 | 完整度 | 关键缺陷 |
|------|--------|---------|
| PatientList | 8/10 | 缺空状态、批量操作未实现 |
| PatientDetail | 9/10 | 作为核心枢纽页表现优秀 |
| AppointmentList | 8/10 | 无法编辑预约内容 |
| AlertList | 7/10 | 搜索未传后端、resolve 无入口、权限码拼写错误已修 |
| FollowUpTaskList | 7/10 | 负责人筛选器空、无法编辑任务 |
| AiAnalysisList | 7/10 | 无患者关联导航、无法触发分析 |
| DialysisManageList | 8/10 | 无 PatientDetail 入口 |
| ConsultationList | 7/10 | 缺患者搜索 |
### 3.3 跨页面导航(功能关联性)
**当前关联度: 中等偏低50%**
| 导航路径 | 状态 |
|----------|------|
| PatientList → PatientDetail | ✅ 行点击 |
| ConsultationList → ConsultationDetail | ✅ 行点击 |
| AlertList → PatientDetail | ✅ 单向 Link |
| PatientDetail → AppointmentList | ❌ 无入口 |
| PatientDetail → ConsultationList | ❌ 无入口 |
| PatientDetail → DialysisManageList | ❌ 无入口 |
| AiAnalysisList → PatientDetail | ❌ 只显示截断 ID |
| PatientDetail → 全局随访任务 | ❌ 无跳转 |
### 3.4 死 API已定义但无页面使用
| API 方法 | 文件 |
|----------|------|
| `patientApi.manageTags` | patients.ts |
| `patientApi.listFamilyMembers/createFamilyMember/updateFamilyMember/deleteFamilyMember` | patients.ts (4个) |
| `alertApi.resolve` | alerts.ts |
| `articleApi.view` | articles.ts |
| `suggestionApi.getComparison` | suggestions.ts |
---
## 4. 微信小程序完整性
### 4.1 规模
- 主包页面: 15 个路由
- 分包页面: 约 47 个路由
- **页面总计: 约 62 个**
- Service 文件: 17 个(患者端 14 + 医护端 7
- TabBar: 4 个(首页/健康/消息/我的)
### 4.2 患者端核心流程覆盖
| 核心流程 | 覆盖度 |
|----------|--------|
| 微信登录 → 手机号绑定 | 100% |
| 建档(就诊人管理) | 100% |
| 体征录入8 种指标) | 95% |
| 趋势图 | 80%Web 有更丰富图表) |
| 蓝牙设备同步BLE | 100%**Web 端没有** |
| 预约挂号 | 100% |
| 随访管理 | 100% |
| 在线咨询 | 100% |
| 积分系统 | 100% |
| AI 分析 | 100% |
| 健康资讯 | 100% |
| 线下活动 | 100% |
| 通知 Tab | **0%(空壳,数据写死空数组)** |
### 4.3 小程序独有功能
| 功能 | 说明 |
|------|------|
| BLE 蓝牙设备同步 | 血压计/血糖仪/小米手环 3 种适配器 |
| 微信一键登录 | wx.login + getPhoneNumber |
| 微信订阅消息 | 5 种模板消息推送 |
---
## 5. 系统统一性分析
### 5.1 功能孤岛效应
从全局角度审视,系统存在以下孤岛效应:
**A. AI 分析 — "有脑无手"**
- 后端 4 个 SSE 分析端点 + 建议审批 + 行动收件箱 全部就绪
- 前端只有"查看历史"能力,无法触发分析
- AI 分析 → 建议 → 审批 → 行动 的闭环在后端完整,前端断裂
- **影响**: AI 模块的投资回报率极低,能力已实现但无法被用户触及
**B. 危急值告警 — "有声无窗"**
- 后端告警引擎 + 规则配置 + 阈值管理 + 危急值确认 全部就绪
- Web 前端只有普通告警列表,危急值体系完全无 UI
- 小程序有告警查看但无危急值专属页面
- **影响**: 医疗安全相关功能缺口,危急值发现后可能无法及时处理
**C. 统计仪表盘 — "有数无图"**
- 后端 9 个统计端点就绪(患者/咨询/随访/化验/预约/体征上报率/透析等)
- Web 前端只有 `StatisticsDashboard` 一个页面,仅消费了部分端点
- 小程序只有医护仪表盘6 指标卡片),无完整统计
- **影响**: 管理者视角缺失,无法通过数据驱动决策
**D. 患者详情 — "有门无路"**
- PatientDetail 作为核心枢纽页有 8 个 Tab数据展示完善
- 但从 PatientDetail 无法跳转到预约、咨询、透析、全局随访等关联功能
- AI 分析列表也不提供到 PatientDetail 的导航
- **影响**: 用户需要在多个页面间手动切换,无法形成工作流闭环
### 5.2 数据一致性
| 问题 | 严重度 | 说明 |
|------|--------|------|
| domain_events 堆积 1166 条 | LOW | outbox 模式未清理已消费事件 |
| 12 个空表 | MEDIUM | 大量功能未被端到端验证 |
| 通知 Tab 空壳 | HIGH | 小程序消息页"通知"Tab 数据写死空数组 |
| erp-plugin 测试编译失败 | MEDIUM | `parse_manifest``#[cfg(test)]` 中未导入 |
### 5.3 过度开发 vs 欠开发
**过度开发(后端就绪但无前端):**
- 危急值告警体系4 端点)— 后端完整,前端零页面
- AI SSE 分析4 端点)— 后端完整,前端零触发
- 行动收件箱2 端点)— 后端完整,前端零消费
- 统计数据5 端点)— 后端完整,前端部分消费
**欠开发(设计目标未达成):**
- 小程序通知系统 — 完全未实现(空壳)
- 患者详情关联导航 — 跨功能跳转缺失
- 家属管理 UI — 4 个 API 就绪但无页面
---
## 6. 三端覆盖度矩阵
| 功能领域 | 后端 | Web 前端 | 小程序 | 完整度 |
|----------|------|---------|--------|--------|
| 患者 CRUD | 100% | 100% | 100% | ✅ 完整 |
| 体征数据 | 100% | 90% | 95% | ✅ 基本完整 |
| 化验报告 | 100% | 100% | 100% | ✅ 完整 |
| 健康档案 | 100% | 100% | 100% | ✅ 完整 |
| 预约排班 | 100% | 95% | 100% | ✅ 基本完整 |
| 随访管理 | 100% | 90% | 100% | ✅ 基本完整 |
| 咨询管理 | 100% | 95% | 100% | ✅ 基本完整 |
| 健康资讯 | 100% | 100% | 100% | ✅ 完整 |
| 积分商城 | 100% | 100% | 100% | ✅ 完整 |
| 线下活动 | 100% | 100% | 100% | ✅ 完整 |
| 告警系统 | 100% | 70% | 80% | ⚠️ Web 缺危急值/resolve |
| AI 分析 | 100% | 40% | 60% | ❌ SSE 无触发入口 |
| 危急值告警 | 100% | 0% | 0% | ❌ 完全无 UI |
| 统计仪表盘 | 100% | 50% | 30% | ⚠️ 端点就绪但消费不足 |
| 行动收件箱 | 100% | 0% | 0% | ❌ 完全无 UI |
| 设备管理 | 100% | 60% | 100% | ⚠️ Web 端缺 BLE 能力 |
| 透析管理 | 100% | 80% | 100% | ✅ 基本完整 |
| 通知系统 | 80% | 50% | 0% | ❌ 小程序空壳 |
| 家属管理 | 100% | 0% | 0% | ❌ API 就绪无 UI |
---
## 7. 优先行动建议
### P0 — 功能断裂(影响核心用户体验)
1. **AI 分析闭环打通** — 在患者详情或独立页面增加"发起 AI 分析"按钮,让 SSE 能力被触及
2. **患者详情关联导航** — 增加"查看该患者预约/咨询/透析"快捷入口
3. **小程序通知 Tab** — 对接后端通知 API不再写死空数据
### P1 — 医疗安全
4. **危急值告警 Web UI** — 至少增加危急值列表页 + 确认操作
5. **erp-plugin 测试修复** — 补充 `use crate::manifest::parse_manifest` 导入
### P2 — 管理者视角
6. **统计仪表盘完善** — 消费已就绪的 5 个统计端点
7. **家属管理 UI** — 在 PatientDetail 增加"家庭成员"Tab
8. **AI 建议前后对比** — 集成 `suggestionApi.getComparison`
---
## 8. 实际验证发现的修正2026-05-01 浏览器 + 代码验证)
### 8.1 报告修正
| 原报告结论 | 实际验证结果 | 修正 |
|-----------|-------------|------|
| 登录页标题"HMS" | 登录页显示 **"HMR Platform"**,与侧边栏 "HMS 健康" 不一致 | 新增:品牌命名不统一 |
| 患者数据 32 条 | 30 条是 E2E 自动化测试生成的 `E2E患者_*`,仅 2 条真实数据 | 修正:系统几乎没有被真实使用过 |
| 统计端点"部分覆盖" | API 验证患者统计、化验报告统计、预约统计、体征上报率、仪表盘统计 **全部返回正常数据** | 修正:后端统计能力完整,仅前端消费不足 |
| 危急值告警"后端完整" | `GET /health/critical-alerts` 返回 **500 Internal Server Error** | 修正:危急值端点有 bug后端也不完整 |
| 透析管理"完整 CRUD" | `GET /health/dialysis-records` 返回 405只接受 POST必须按患者查询 | 修正:透析记录没有全局列表,必须先选患者 |
| 小程序咨询"100% 覆盖" | 首页/健康页/个人中心 **均无入口** 跳转到在线咨询页 | 修正:咨询功能是"页面孤岛",用户无法发现 |
| AI SSE "无触发入口" | `POST /ai/analyze/lab-report` 端点存在且返回验证错误(需真实数据) | 确认:端点可用但前端无调用入口 |
| 行动收件箱"0 数据" | API 返回 total=0 ✅ | 确认 |
### 8.2 新发现(验证前未识别)
**A. 品牌一致性**
- 登录页标题 "HMR Platform",侧边栏 "HMS 健康",版权 "HMR Platform · ©汕头市智界科技有限公司"
- 产品名称在 HMR/HMS 之间摇摆
**B. 数据质量 — 测试污染**
- 32 个患者中 30 个是 E2E 自动化测试创建的 `E2E患者_*`
- domain_events 表堆积 1166 条未清理
- 建议清理测试数据或建立独立测试租户
**C. 小程序 AI 建议跳转错误**
- 健康页 AI 建议卡片识别出 `appointment`/`followup` 类型建议后
- 点击统一跳转到 **设置页**`/pages/pkg-profile/settings/index`
- 而非预期的预约创建页或随访详情页 — 设计缺陷
**D. 小程序咨询功能完全孤立**
- `/pages/consultation/index` 已注册,页面存在
- 但首页/健康页/个人中心均无入口指向它
- 唯一入口在消息 Tab 的"咨询"子 Tab
- 用户极难发现这个功能
**E. Web 告警页面已存在但侧边栏无入口**
- `/health/alerts` 可直接 URL 访问,页面功能完整
- 但侧边栏菜单中 **没有告警管理入口**(未出现在菜单配置中)
- 同理 `/health/alert-dashboard``/health/alert-rules` 也无菜单入口
**F. Web 侧边栏缺少多个页面入口**
| 已注册路由 | 侧边栏入口 | 状态 |
|-----------|-----------|------|
| `/health/alerts` | 无 | 需手动输入 URL |
| `/health/alert-dashboard` | 无 | 需手动输入 URL |
| `/health/alert-rules` | 无 | 需手动输入 URL |
| `/health/dialysis` | 有("透析管理" | ✅ |
| `/health/devices` | 有("设备管理" | ✅ |
| `/health/articles` | 有("资讯管理" | ✅ |
---
## 9. 总结
**项目整体完成度评估: 75%(实际验证后从 78% 下调)**
- **后端** (93%): 119 个路由、CRUD 全覆盖,但危急值告警端点有 500 错误
- **Web 前端** (68%): 核心页面功能完整但存在隐性问题告警页面有路由无菜单入口、AI/统计孤岛、跨模块导航断裂
- **小程序** (80%): 患者端覆盖度高但咨询功能孤立用户无法发现、AI 建议跳转错误、通知空壳
- **数据质量** (35%): 30/32 患者为 E2E 测试数据12/32 表完全空,系统几乎未经真实使用
**最突出的矛盾**: 后端能力远超前端消费速度,约 13% 的路由为"死端点"。而已经暴露给用户的功能中,又有多个存在导航断裂和入口缺失,形成了"有能力但不可达"的双重问题。
**新发现的系统性问题**:
1. **品牌不一致** — HMR/HMS 混用
2. **菜单配置缺失** — 告警相关 3 个页面有路由无菜单
3. **小程序导航断裂** — 咨询功能孤立、AI 建议跳转到设置页
4. **测试数据污染** — E2E 测试未清理导致数据不可信
---
## 变更记录
| 日期 | 变更 |
|------|------|
| 2026-05-01 | 初始版本 — 三端联调深度审计 |
| 2026-05-01 | 实际验证修正 — 浏览器+API+代码分析6 项报告修正 + 6 项新发现,完成度从 78% 下调至 75% |

View File

@@ -0,0 +1,50 @@
# 工作台体验重构 — 发散式探讨
> 日期: 2026-05-01 ~ 2026-05-02 | 参与者: 用户 + Claude
## 背景
HMS 项目已完成全系统审计83% 完成度),工作台原型已设计(医生 A/B/C + 主任 D/E/F 共 6 种方案),最近 5 个提交在推进工作台前端实现。但工作台的设计前提需要修正。
## 讨论要点
### 1. 产品定位重大认知更新
- **首期用户是血透中心/一级综合医院**,不是泛化健康管理机构
- **机构已有 HIS + 专用血透管理系统**HMS 不碰临床透析流程
- **HMS 的真正定位是「患者-机构枢纽」**:日常健康数据采集、随访干预、积分运营、健康科普、潜在患者转化
- **三种角色并行**:健康管家/随访护士、医生(审核/咨询角色)、运营人员
- **产品愿景是持续演化**:健康管理 → 运营引擎 → 血透管理(最终替代专用血透系统)
- **MVP 先独立运行**,不依赖 HIS 对接
### 2. 工作台方案选择
提出三种方案:
- **方案 A「今日全景」仪表盘** — 统计卡片 + 待办列表 + 侧边 AI 洞察
- **方案 B「工作流驱动」流水线** — 任务队列 + 详情处理面板(选中)
- **方案 C「AI 指挥中心」** — AI 助手主卡片主动推送
用户选择**方案 B**,理由:健康管家每天处理 20+ 任务,一件一件处理是最自然的工作节奏。
### 3. 角色与范围决策
- 先只做**健康管家**角色,医生和运营后续再做
- 分两期实施Phase 1 基于现有 3 种数据源告警、AI 建议、随访Phase 2 扩展
### 4. 设计规格评审
规格评审发现 7 个 CRITICAL 问题,核心是设计规格与现有代码库对不上:
- 数据源不匹配7 种任务类型中 4 种没有现成数据源)
- API 路径冲突(现有 action-inbox 路由未处理)
- 表名错误points_redemption → points_order 等)
- "无需新建表"的声明不准确
- 缺少验收标准
v2 已全部修复,采用分两期实施策略。
## 结论
- 方案 B「工作流驱动」健康管家工作台作为本期实施目标
- 设计规格 v2 已通过评审,保存到 docs/superpowers/specs/2026-05-02-health-manager-workbench-design.md
- 原型保存到 _temp/workbench-health-manager-A/B/C.html
- 下一步:编写实施计划并执行

View File

@@ -0,0 +1,213 @@
# HMS 小程序患者端 — 老年友好版本设计规格
> 日期: 2026-04-30 | 状态: 待实施 | 选定方案: A健康 Tab 导航)
## 1. 背景与目标
患者端小程序面向 60+ 慢病患者群体当前版本信息密度高、字号小、导航层级深5 Tab + 40 页老年用户难以独立使用。基于多专家组UX 无障碍、老年医学、产品策略)头脑风暴,选定方案 A 进行全面重设计。
**核心目标:**
- 老年患者能独立完成日常操作(记录体征、查看数据、预约挂号、服药提醒)
- 每个核心操作路径不超过 3 步
- 满足 WCAG 2.1 AA 对比度要求
## 2. 导航结构
**4 Tab首页 / 健康 / 消息 / 我的**
| Tab | 名称 | 核心内容 | 页面文件 |
|-----|------|---------|---------|
| 1 | 首页 | 今日打卡 + 体征 2x2 + 快捷操作 | `pages/index/index.tsx` |
| 2 | 健康 | 体征录入 + 趋势图 + BLE 设备 | `pages/health/index.tsx`(重写) |
| 3 | 消息 | 咨询对话 + 系统通知 | `pages/messages/index.tsx`(新建) |
| 4 | 我的 | 个人信息 + 积分 + 菜单 | `pages/profile/index.tsx`(调整) |
**Tab 迁移映射:**
| 当前 Tab | 操作 | 去向 |
|----------|------|------|
| 首页 (index) | 保留,重写 | — |
| 上报 (health) | 重命名为"健康",重写 | 合并录入功能 |
| 咨询 (consultation) | 从 Tab 移除 | 合并到"消息"Tab 的"咨询"子 Tab |
| 商城 (mall) | 从 Tab 移除 | 降级为"我的"页菜单项,`pkg-mall` 子包保留 |
| 我的 (profile) | 保留,调整间距 | — |
| (新增) 消息 | 新建 Tab + 页面 | `pages/messages/index.tsx` |
**说明:** `pages/consultation/` 目录及 `pages/consultation/detail/` 保留为路由页面(非 Tab从消息页的"咨询"子 Tab 导航进入。`pkg-mall` 子包(兑换/订单/详情)全部保留,从"我的"页菜单入口进入。
## 3. 首页设计
5 个区域,从上到下:
1. **问候 + 日期** — "上午好,张大爷" 24px 粗体 + 日期 + 消息红点
2. **今日体征完成度** — 圆形进度环72px+ 完成百分比 + 4 指标胶囊(血压/心率/血糖/体重)
- **计算逻辑(纯前端):** 查询当日体征数据 API4 个指标(血压/心率/血糖/体重)中当天有记录的计为完成,进度 = 已完成数 / 4
- **与积分签到的区别:** 这是"今日体征记录完成度"(纯展示),不是积分商城的"每日签到"`usePointsStore.checkinStatus`)。两者独立,不耦合。
- **无需新后端 API** 复用现有 `getHealthSummary` 或当日体征查询接口,前端计算百分比。
3. **今日体征 2x2** — 白色卡片网格,数值 32px 衬线体,状态标签(正常/偏高/未记录)
4. **今日待办≤3条** — 预约/随访/异常提醒,带图标和副标题
5. **快捷操作** — 两个大按钮:"记录体征"(实色)+ "预约挂号"(描边)
**首页不放置的内容(从当前首页移除):**
- 设备快捷入口(血压计/血糖仪卡片)→ 移至"健康"Tab
- 6 项快捷服务网格(预约挂号/健康录入/趋势/告警/资讯/AI→ 精简为 2 个核心按钮
- 健康资讯文章列表 → 入口移至"健康"Tab 底部或"我的"页
## 4. 健康页设计
**功能定位:** 体征数据录入 + 趋势查看 + 设备管理
### 4.1 录入区
- 顶部类型 Tab血压 / 心率 / 血糖 / 体重56px 高大 Tab选中项 $pri 实色底白字)
- 血压:收缩压 + 舒张压两个大输入框56px 高,数值 28px 衬线体),上下排列(非左右,防止老年用户填反)
- 心率/体重:单个大输入框
- 血糖:数值 + 时段选择(空腹/餐后 2h大按钮组
- 每个输入框下方显示参考范围和实时状态(正常/偏高/偏低)
- 保存按钮:全宽 56px 高 $pri 实色
- 异常值保存前弹出确认:"血压偏高 (160/95),确认提交?"
### 4.2 趋势图
- 复用现有 `getTrend` API
- 7 天柱状图,异常值用 $wrn 色,正常用 $pri 色
- 底部显示周一到周日标签
### 4.3 BLE 设备
- 从首页移入此区域
- 连接状态卡片:设备图标 + 名称 + 连接状态
- 一键同步按钮
- 设备管理入口(跳转 `pkg-device-sync`
### 4.4 健康资讯入口
- 从首页移入,放在页面底部
- 单行卡片:"最新健康资讯 " 点击跳转文章列表
## 5. 消息页设计
**新页面,合并咨询 + 系统通知。**
- 顶部 Tab 切换:"咨询" | "通知"56px 大 Tab
- 咨询列表:对话卡片 80px 高,文字 28px未读红点 24px
- 通知列表:随访提醒、预约确认、异常告警、用药提醒
## 6. 我的页设计
保留现有结构,调整参数:
- 菜单项高度 48px → 64px
- 文字 28px
- 图标 40px
- 积分 + 连续打卡横排展示
## 7. 设计系统调整
### 7.1 字号
> **单位说明:** 代码库统一使用 `px`Taro 的 `pxtransform` 编译时自动转为 rpx。以下所有值均为 `px`。
| 用途 | 当前(px) | 调整为(px) |
|------|----------|-----------|
| 问候/标题 | 26 | 28 |
| 区块标题 | 24 | 26 |
| 正文 | 22 | 24 |
| 辅助文字 | 20 | 22 |
| 体征数值 | 44 | 48-64 |
| 按钮文字 | 24 | 28 |
| **最小字号** | 20 | **22** |
### 7.2 间距与触控
| 参数 | 当前 | 调整为 |
|------|------|--------|
| 按钮最小高度 | — | 48px120rpx |
| 主按钮高度 | — | 56px140rpx |
| 菜单项高度 | 48px | 64px160rpx |
| 卡片圆角 | 12px | 16px |
| 卡片内间距 | 24-28px | 28-32px |
| 列表项间距 | 16px | 20px |
### 7.3 色彩
> **对比度基于 `$bg` #F5F0EB 计算。** WCAG 2.1 AA 要求:正文(<24px≥ 4.5:1大字≥24px≥ 3:1。
| 变量 | 当前 | 调整为 | 对比度 | WCAG |
|------|------|--------|--------|------|
| `$tx2` | #7A756E | #5A554F | ~5.5:1 | AA 正文 |
| `$tx3` | #A8A29E | #78716C | ~4.6:1 | AA 正文 |
| `$pri` | #C4623A | **不变** | — | — |
| `$bg` | #F5F0EB | **不变** | — | — |
**使用约束:** `$tx3` 仅用于 24px 及以上的文字或装饰性标签。正文一律使用 `$tx2``$tx`
## 8. 功能优先级
| 优先级 | 功能 | 状态 |
|--------|------|------|
| P0 | 体征数据查看2x2 卡片) | 重写首页 |
| P0 | 体征录入(大输入框) | 重写健康页 |
| P0 | 今日体征完成度(进度环) | 新增(纯前端,复用现有 API |
| P1 | 今日待办≤3 条) | 新增 |
| P1 | 消息中心(咨询+通知) | 新建页面 |
| P1 | BLE 设备同步 | 保留调整 |
| P2 | 趋势图简化 | 调整 |
| P2 | 用药提醒 | 保留 |
| P3 | 积分商城 | 降级为菜单项 |
## 9. 页面路由变更
### TabBar 配置app.config.ts
```
5 Tab → 4 Tab
删除上报health、商城mall
新增消息messages
保留首页index、健康health → 重写、我的profile
```
### 页面增减
| 操作 | 页面 |
|------|------|
| 新建 | `pages/messages/index.tsx` |
| 重写 | `pages/index/index.tsx``pages/health/index.tsx` |
| 调整 | `pages/profile/index.tsx` |
| 删除 Tab | `pages/mall/index.tsx`(降级为子页面) |
| 保留 | 所有 `pkg-*` 子包页面 |
## 10. 关键文件
| 文件 | 改动类型 |
|------|---------|
| `apps/miniprogram/src/app.config.ts` | TabBar 5→4路由调整 |
| `apps/miniprogram/src/styles/variables.scss` | 字号/色值/间距调整 |
| `apps/miniprogram/src/styles/mixins.scss` | 新增老年友好 mixin |
| `apps/miniprogram/src/pages/index/index.tsx` | 首页全面重写 |
| `apps/miniprogram/src/pages/index/index.scss` | 首页样式全面重写 |
| `apps/miniprogram/src/pages/health/index.tsx` | 健康页重写 |
| `apps/miniprogram/src/pages/health/index.scss` | 健康页样式重写 |
| `apps/miniprogram/src/pages/messages/index.tsx` | **新建**消息页 |
| `apps/miniprogram/src/pages/messages/index.scss` | **新建**消息页样式 |
| `apps/miniprogram/src/pages/profile/index.tsx` | 调整菜单和间距 |
| `apps/miniprogram/src/services/consultation.ts` | 消息页复用咨询列表 API |
| `apps/miniprogram/src/services/health.ts` | 复用体征数据查询(打卡进度) |
| `apps/miniprogram/src/stores/points.ts` | 签到/积分功能保留,不改动 |
| `apps/miniprogram/src/components/` | 新增 ProgressRing 组件 |
## 11. 验证方式
1. `pnpm build` — 小程序编译通过
2. 微信开发者工具预览 — 布局和字号正确
3. 真机测试 — 在 60+ 患者手机上验证可读性和触控体验
4. WCAG 2.1 AA — 色值对比度合规≥4.5:1 正文≥3:1 大字),使用 axe-core 或微信开发者工具无障碍审计
5. 核心路径验证(每条 ≤ 3 步):
- 记录血压:首页 → 点击"记录体征" → 输入 → 保存3 步)
- 查看趋势:健康 Tab → 查看趋势图1 步)
- 预约挂号:首页 → 点击"预约挂号" → 选择时间 → 确认3 步)
- 查看消息:消息 Tab → 点击对话2 步)
6. 不同屏幕尺寸测试:特别是老年用户常用的小屏 Android 设备
7. 字体回退验证:衬线体在低端 Android 设备上的回退(使用 `Georgia, "PingFang SC", serif`
## 12. 迁移与发布策略
- 一次性全量发布(非 A/B 测试),因为 5 Tab → 4 Tab 是结构性变更,无法共存
- 旧路径 `pages/consultation/``pages/mall/` 保留为路由页面,但不再从 TabBar 进入
- 小程序分享卡片可能指向旧路径——这些页面保留可访问,不受影响
- 发布前在测试环境完整走一遍所有核心路径

View File

@@ -0,0 +1,401 @@
# 行动收件箱与上下文线程设计规格
> 日期: 2026-05-01 | 状态: Draft | 关联: `erp-health`, `erp-ai`, Web 前端, 小程序
## 1. 背景与问题
### 1.1 问题陈述
HMS 平台功能完善328 路由、25 事件类型、97.5% 测试通过率),但用户体验存在**系统性流程断裂**
- **告警 → 行动断裂**:医生看到告警后,无直接路径跳到查看患者→安排随访→记录处置
- **数据录入 → 反馈断裂**:患者录入体征后无即时趋势对比和行动建议
- **AI 建议 → 执行追踪断裂**:审批通过后,后续执行/追踪/结果反馈在 UI 上不可见
**根因**:前端交互模型是"功能陈列式"的——每个功能是死胡同,没有出口指向下一步。
### 1.2 设计目标
建立统一的**行动收件箱 + 上下文线程**机制,让每个"终点"变成"起点"
1. 用户登录后第一眼看到待处理事项(按优先级排列)
2. 每个待办有清晰的时间线展示完整生命周期
3. 时间线每一步都可直接跳转到对应功能页
4. 操作按钮根据当前状态动态变化
5. 架构可扩展——新增行动类型(告警/随访/异常)只需注册聚合器
### 1.3 范围
**Phase 1本次**AI 建议闭环 — 后端已完整实现10 次提交),补齐前端体验
**Future**:告警处理闭环、体征反馈闭环、随访到期提醒
---
## 2. 设计概览
```
事件源 → 行动收件箱服务 → 前端展示
├── Web: 顶栏铃铛 → 列表页 → Drawer
└── 小程序: "待办" Tab → 列表 → 半屏弹窗
```
**核心抽象**`ActionItem` — 统一的待办数据结构,不同类型共用相同接口
**核心交互**`Context Thread` — Drawer/半屏弹窗中的时间线,展示完整处理进度
---
## 3. 数据模型
### 3.1 ActionItem聚合视图不建新表
```typescript
interface ActionItem {
id: string; // 格式: "{type}:{source_id}"
type: "ai_suggestion" | "alert" | "followup" | "data_anomaly";
priority: "urgent" | "high" | "medium" | "low";
status: "pending" | "in_progress" | "completed" | "dismissed";
title: string;
summary: string;
patient_id: string;
patient_name: string;
source_ref: string; // 指向原始实体 ID
created_at: string;
updated_at: string;
}
```
### 3.2 ThreadEvent上下文线程事件
```typescript
interface ThreadEvent {
step: string; // "ai_analysis" | "doctor_approval" | "followup_scheduled" | "followup_completed" | "reanalysis"
label: string; // 展示文本
status: "completed" | "in_progress" | "pending";
detail?: string; // 补充说明
timestamp?: string; // 完成时间
operator?: string; // 操作人
link_to?: string; // 跳转路径
}
```
### 3.3 ActionDefinition可用操作
```typescript
interface ActionDefinition {
key: string; // "approve" | "reject" | "complete_early" | "acknowledge"
label: string; // 按钮文本
variant: "primary" | "danger" | "default";
confirm_message?: string; // 确认弹窗文案
api_endpoint: string; // 调用的 API
}
```
---
## 4. 后端 API 设计
### 4.1 GET /api/v1/action-inbox
获取当前用户的行动收件箱列表。
**Query Params**:
- `status`: `pending` | `in_progress` | `completed` | `dismissed`(可选,默认全部)
- `type`: `ai_suggestion` | `alert` | `followup` | `data_anomaly`(可选,默认全部)
- `page` / `page_size`: 分页(默认 1/20
**Response**: `PaginatedResponse<ActionItem>`
**实现策略**聚合查询不建新表。Phase 1 只查 `ai_suggestion` 表:
```sql
-- 注意patient_id 在 ai_analysis 表上,不在 ai_suggestion 上,需三表 JOIN
SELECT s.id, s.suggestion_type, s.risk_level, s.status, s.params,
s.created_at, s.updated_at,
p.name as patient_name, a.patient_id,
a.result_content, a.analysis_type
FROM ai_suggestion s
JOIN ai_analysis a ON s.analysis_id = a.id
JOIN patient p ON a.patient_id = p.id
WHERE s.tenant_id = $1
AND s.deleted_at IS NULL
ORDER BY
CASE s.risk_level WHEN 'high' THEN 1 WHEN 'medium' THEN 2 ELSE 3 END,
s.created_at DESC
```
### 4.2 GET /api/v1/action-inbox/:source_ref/thread
获取某个行动项的上下文线程。
**Response**:
```typescript
interface ThreadResponse {
action_item: ActionItem;
thread: ThreadEvent[];
available_actions: ActionDefinition[];
}
```
**线程拼装逻辑**(以 AI 建议为例):
| 步骤 | 数据来源 | 状态判断 |
|------|---------|---------|
| AI 分析完成 | `ai_analysis.status = 'completed'` | 始终 completed |
| 医生审批 | `ai_suggestion.status` | Approved→completed, Pending→in_progress |
| 执行安排 | `ai_suggestion.workflow_instance_id IS NOT NULL` | 有值→completed |
| 等待随访 | `follow_up` 关联查询 | 根据随访状态判断 |
| 再分析对比 | `ai_suggestion.reanalysis_id IS NOT NULL` | 有值→completed |
**操作按钮动态生成**
| 建议状态 | 可用操作 |
|---------|---------|
| Pending | 批准(→ `POST /ai/suggestions/:id/approve`)、拒绝 |
| Approved/执行中 | 提前完成随访、标记已知悉 |
| Executed | 查看前后对比报告 |
| Expired/Rejected/ParseFailed | 无操作(只读展示) |
| Executed | 查看前后对比报告 |
| Expired | 无操作 |
### 4.3 代码归属与权限码
**代码位置**
- `ActionInboxService``crates/erp-health/src/service/action_inbox_service.rs`erp-health crate
- `ActionInboxHandler``crates/erp-health/src/handler/action_inbox_handler.rs`
- 路由注册:在 `erp-health` 模块的 `protected_routes()` 中新增
- 跨 crate 查询:通过 raw SQL 直接查 `ai_suggestion` + `ai_analysis` 表(与现有 `ai_suggestion_loader.rs` 模式一致erp-health 已有跨 crate 读 ai 表的先例)
**权限码**:新增独立权限码,注册在 `erp-health` 模块的 `permissions()` 中:
- `health.action_inbox.list` — 查看行动收件箱
- `health.action_inbox.manage` — 执行操作(审批/拒绝/标记已知悉)
理由:`action-inbox` 是聚合端点,未来包含告警/随访/异常等多种类型,不应绑定到 `ai.suggestion.*` 权限。用 `health.` 前缀与 erp-health crate 归属一致。
**用户范围**
- Phase 1AI 建议):返回当前租户下所有有权限查看的待办(`ai_suggestion``assigned_to` 字段)
- Future告警/随访):告警有 `assigned_to` 关联的负责医生,随访有 `assigned_to` 字段,届时 API 增加 `scope` 参数(`all` | `mine`
---
## 5. Web 前端设计
### 5.1 入口改造NotificationPanel 扩展
**现有**`apps/web/src/components/NotificationPanel.tsx`(铃铛 + PopoverSSE 推送消息/告警)
**改造**
- Popover 中增加"待办"区域,显示最近 3 条 ActionItem
- 底部增加"查看全部待办"链接,跳转到 `/action-inbox` 页面
- 角标数字改为:未读消息数 + 待处理 ActionItem 数
### 5.2 新增页面ActionInboxPage
**路由**`/action-inbox`
**组件**`apps/web/src/pages/ActionInboxPage.tsx`
**布局**
- 顶部:筛选栏(状态 Tab + 类型下拉)
- 主体Ant Design List 组件,每项显示:
- 类型标签(颜色区分)
- 标题 + 患者姓名
- 优先级标记
- 创建时间
- 点击 → 打开 Drawer
### 5.3 新增组件ActionThreadDrawer
**组件**`apps/web/src/components/ActionThreadDrawer.tsx`
**实现**Ant Design Drawer + Ant Design Steps/Timeline
**结构**
```
Drawer (width 480px)
├── 头部:标题 + 患者摘要卡(年龄/最近体征/关键指标)
├── 时间线Ant Design Timeline 组件
│ ├── 每个节点:状态图标 + 文本 + 时间 + 跳转链接
│ └── 当前步骤高亮,未来步骤灰色
├── 关联信息:可点击跳转的快捷链接
└── 操作区:动态按钮组
```
**状态设计**
- **加载中**Ant Design Skeleton3 行卡片骨架屏)
- **数据获取失败**Ant Design Result + 重试按钮
- **空列表**:插图 + "暂无待办事项"文案
- **操作按钮请求中**:按钮 loading 状态 + 禁用
### 5.4 实时更新策略
**Phase 1轮询**用户手动刷新或切换页面时重新拉取列表。Drawer 打开时,操作按钮点击后主动调用 `GET /action-inbox/:id/thread` 刷新时间线。
**Phase 2SSE 优化)**:复用 `apps/web/src/stores/message.ts` 中的 SSE 连接。后端在建议状态变更时,通过事件总线发布 → 消息服务推送到 SSE 流。前端监听 `suggestion.status_changed` 事件,触发列表/Drawer 刷新。
Phase 1 选择轮询的理由:现有 SSE 只推送 `message``alert` 两种类型,新增 `suggestion` 事件需要后端消息服务改造,且 Phase 1 的使用场景(医生不频繁审批)对实时性要求不高。
---
## 6. 小程序设计
### 6.1 TabBar 改造
**当前**:首页 | 健康 | 消息 | 我的4 Tab
**新**:首页 | 健康 | 待办 | 消息 | 我的5 Tab
- 在"健康"和"消息"之间插入"待办"Tab
- Tab 图标:`todo-list``check-circle`
- 角标显示待处理数量(调用 `GET /action-inbox?status=pending` 获取计数)
- 修改文件:`apps/miniprogram/src/app.config.ts`
### 6.2 新增页面:待办列表
**路径**`apps/miniprogram/src/pages/action-inbox/index.tsx`
**布局**
- 顶部:状态筛选 Tab全部 | 待处理 | 进行中 | 已完成)
- 列表:按优先级排序的待办卡片
- 每张卡片:类型标签 + 标题 + 患者名 + 时间 + 箭头
- 点击卡片 → 弹出半屏弹窗
### 6.3 新增组件ActionHalfScreenDialog
使用 Taro `half-screen-dialog` 组件或自定义半屏弹窗:
```
半屏弹窗
├── 头部:拖拽条 + 类型标签 + 标题
├── 摘要:关键信息(患者/风险等级/时间)
├── 时间线:简化版(只显示已完成和当前步骤)
├── 操作按钮
└── 底部:跳转详情链接
```
### 6.4 Service 层
**文件**`apps/miniprogram/src/services/action-inbox.ts`
```typescript
// 复用现有 listPendingSuggestions()
// 新增:
listActionItems(params: { status?: string; type?: string; page: number })
GET /api/v1/action-inbox
getActionThread(sourceRef: string)
GET /api/v1/action-inbox/:source_ref/thread
```
---
## 7. 数据映射详细规则
### 7.1 AI 建议 → ActionItem 映射
| ActionItem 字段 | 映射来源 | 规则 |
|----------------|---------|------|
| id | `"ai_suggestion:" + ai_suggestion.id` | 类型前缀 + 原 ID |
| type | 固定 `"ai_suggestion"` | — |
| priority | `ai_suggestion.risk_level` | High→urgent, Medium→high, Low→medium |
| status | `ai_suggestion.status` | Pending→pending, Approved→in_progress, Executed→completed, Expired/Rejected/ParseFailed→dismissed |
| title | `suggestion_type` + `risk_level` 拼接模板 | 如 `suggestion_type=Followup` → "建议安排随访"`suggestion_type=Alert` → "建议关注指标异常";从 `params` JSON 提取 `reason` 字段作为补充(若存在) |
| summary | `ai_analysis.result_content` | 截取前 100 字符 |
| patient_id | `ai_analysis.patient_id`(通过 `analysis_id` JOIN | 三表 JOIN |
| patient_name | `patient.name` | JOIN 查询 |
| source_ref | `ai_suggestion.id` | 原始 ID |
### 7.2 线程步骤映射
| ThreadEvent.step | 触发条件 | 数据来源 |
|-----------------|---------|---------|
| `ai_analysis` | 始终存在 | `ai_analysis` 表 |
| `doctor_approval` | `suggestion.status ∈ {Approved, Rejected}` | `suggestion.updated_at` 作为近似审批时间(无专用 `approved_at` 字段) |
| `followup_scheduled` | `workflow_instance_id IS NOT NULL` | `workflow_instance` 表。注意:`action_dispatched` 步骤已合并到此处——`ai_action_dispatcher` 是事件消费者,不回写时间戳到 `ai_suggestion`,其效果体现为 `workflow_instance_id` 被设置 |
| `followup_completed` | `follow_up.status = completed` | `follow_up` 表 |
| `reanalysis` | `reanalysis_id IS NOT NULL` | `ai_analysis` (reanalysis) 表 |
---
## 8. 实施分阶段
### Phase 1A后端 API2-3 天)
- [ ] 创建 `ActionInboxService``crates/erp-health/src/service/action_inbox_service.rs`
- [ ] 创建 `ActionInboxHandler``crates/erp-health/src/handler/action_inbox_handler.rs`
- [ ] 在 erp-health `protected_routes()` 中注册 2 个新路由
- [ ] 实现三表 JOIN 聚合查询(`ai_suggestion` + `ai_analysis` + `patient`
- [ ] 实现线程拼装逻辑(合并 `action_dispatched``followup_scheduled` 步骤)
- [ ] 实现操作按钮动态生成
- [ ] 在 erp-health `permissions()` 中注册 `health.action_inbox.list` / `health.action_inbox.manage`
### Phase 1BWeb 前端2-3 天)
- [ ] 扩展 `NotificationPanel`,增加待办区域
- [ ] 创建 `ActionInboxPage` 路由页面
- [ ] 创建 `ActionThreadDrawer` 组件
- [ ] SSE 实时更新集成
- [ ] 路由注册 + 菜单入口
### Phase 1C小程序2-3 天)
- [ ] TabBar 改造4 Tab → 5 Tab在"健康"和"消息"间插入"待办"
- [ ] 创建待办列表页面
- [ ] 创建半屏弹窗组件
- [ ] Service 层对接
### Phase 1D验证与收尾1 天)
- [ ] 三端联调测试
- [ ] 更新 wiki 文档
- [ ] 提交并推送
---
## 9. 验证方案
### 9.1 后端验证
```bash
# 1. 启动后端
cd crates/erp-server && cargo run
# 2. 创建 AI 分析 → 产生建议
# 3. 调用 API 验证
curl -H "Authorization: Bearer $TOKEN" \
http://localhost:3000/api/v1/action-inbox
curl -H "Authorization: Bearer $TOKEN" \
http://localhost:3000/api/v1/action-inbox/ai_suggestion:{id}/thread
```
### 9.2 Web 验证
1. 登录 → 顶栏铃铛应显示待办数量
2. 点击铃铛 → Popover 显示最近待办
3. "查看全部" → 进入 ActionInboxPage
4. 点击待办项 → Drawer 打开,时间线正确
5. 点击"批准" → 状态变更,时间线更新
6. SSE 推送 → Drawer 实时更新
### 9.3 小程序验证
1. 底部 TabBar 显示"待办"Tab + 角标
2. 进入待办页 → 列表正确
3. 点击卡片 → 半屏弹窗弹出
4. 操作后 → 状态更新,列表刷新
---
## 10. 未来扩展
新增行动类型只需:
1.`ActionInboxService` 中注册新的聚合查询器
2. 定义 `type → ActionItem` 的映射规则
3. 定义线程步骤模板
4. 前端自动渲染(共用相同组件)
**规划中的扩展类型**
- `alert` — 告警处理闭环(告警→评估→处置→追踪)
- `followup` — 随访到期提醒(到期→执行→记录→再分析)
- `data_anomaly` — 体征异常反馈(异常→趋势对比→建议→操作)

View File

@@ -0,0 +1,499 @@
# 统一工作台设计规格
> 日期: 2026-05-01 | 状态: 草案 | 作者: brainstorming session
## 目录
1. **背景与目标** — 为什么要做、要解决什么问题
2. **设计决策** — 方案选择过程和最终决策
3. **页面布局** — 医生视角和主任视角的详细布局
4. **待办数据模型** — ActionItem 统一结构和类型定义
5. **交互设计** — 弹窗/抽屉混合、筛选、排序、操作按钮
6. **角色感知** — 医生 vs 主任的渲染差异
7. **后端 API** — ActionInboxService 聚合查询接口
8. **与现有模块的关系** — 复用 erp-health / erp-ai 已有能力
9. **实施分步** — Phase 划分和依赖关系
10. **验证标准** — 完成标准
---
## 1. 背景与目标
### 1.1 问题
HMS 平台功能完成度已达 83%,后端 AI 能力成熟4 个 SSE 分析端点、建议系统、自动趋势分析、本地规则引擎),但审计揭示了一个核心矛盾:**后端能力远超前端覆盖**。
具体表现:
- **AI 分析"有脑无手"** — 后端 4 个 SSE 端点就绪AI 建议可自动创建,但前端无统一入口展示和处理
- **告警"有声无窗"** — 后端告警引擎完整Web 端无集中展示
- **待办分散** — AI 建议、告警、随访、数据异常分布在不同页面,医生需要逐一查看
- **主任视角缺失** — 科室主任无法快速掌握团队工作负载和科室风险概况
### 1.2 目标
设计一个**统一工作台**,作为医生和科室主任的默认首页,实现:
1. **一眼看到所有待办** — AI 建议、危急告警、随访、数据异常集中展示
2. **一键操作** — 每条待办可直接处理(审批/联系/安排),不强制跳转
3. **AI 洞察直达** — 右侧面板实时展示最新 AI 分析洞察
4. **主任管理视角** — 团队工作负载、超时升级、科室风险分布一目了然
5. **设计一致性** — 医生和主任使用相同的页面结构,通过角色感知差异化
### 1.3 范围
- **包含**Web 端首页工作台重构、后端 ActionInboxService 聚合 API
- **不包含**小程序端改造独立迭代、BPMN 工作流编排AI 行动闭环 Phase 2+
---
## 2. 设计决策
### 2.1 方案选择过程
通过高保真 HTML 原型对比了三种工作台形态:
| 方案 | 描述 | 优势 | 劣势 |
|------|------|------|------|
| **A · 首页即工作台** | 登录后默认首页,取代现有 Dashboard | 零点击触达待办,信息密度高 | 首页功能增多 |
| **B · 独立工作台** | 侧边栏入口,三栏布局(筛选+列表+详情) | 筛选能力强,详情展示充分 | 需要额外导航,脱离首页语境 |
| **C · 嵌入式体验** | 顶栏气泡+侧边迷你面板+浮动 AI 助手 | 信息无处不在,上下文关联强 | 实现复杂度高,分散注意力 |
**最终选择方案A — 首页即工作台**
理由:
- 医生最核心的需求是"快速知道今天要做什么"方案A 零点击满足
- 信息密度适中(左侧待办 + 右侧 AI 洞察),不会过载
- 与现有 Home.tsx 的角色感知 Dashboard 结构兼容,改动最小
### 2.2 科室主任视角
对比了三种主任工作台方案:
| 方案 | 描述 | 优势 | 劣势 |
|------|------|------|------|
| **D · 个人待办 + 团队概览卡** | 和医生同结构,右侧替换为团队概览 | 设计一致性最高 | 团队管理能力相对有限 |
| **E · 独立管理仪表盘** | 纯管理视角6列统计+团队表+告警线 | 管理信息最全面 | 完全不同的页面,开发成本高 |
| **F · Tab 双视图** | 同一页面内 Tab 切换 | 兼顾两种需求 | Tab 切换增加认知负担 |
**最终选择方案D — 个人待办 + 团队概览卡**
理由:
- 与医生方案保持设计一致性,同一页面结构
- 团队概览卡提供足够的主任管理信息(每人工作负载、处理率、超时数)
- 实现成本最低,角色感知逻辑复用现有模式
### 2.3 AI 建议交互
**决策:弹窗 + 抽屉混合**
| 操作类型 | 交互方式 | 原因 |
|---------|---------|------|
| 批准/拒绝/忽略 | Modal 弹窗确认 | 轻操作,一步完成 |
| 查看完整 AI 分析 | 右侧 Drawer 抽屉 | 需要展示长文本、基线指标、建议详情 |
| 修改后批准 | Drawer 内编辑 | 需要足够空间修改建议内容 |
---
## 3. 页面布局
### 3.1 整体结构
```
┌─────────────────────────────────────────────────────────────┐
│ 顶部 Tab 切换栏(仅原型用)│ 实际MainLayout 侧边栏 + 顶栏 │
├──────┬──────────────────────────────────────────────────────┤
│ │ 欢迎语 + 日期 + 待办总数 │
│ 侧 │ ┌──────┬──────┬──────┬──────┬──────┐ │
│ 边 │ │统计卡1│统计卡2│统计卡3│统计卡4│(主任+1)│ │
│ 导 │ └──────┴──────┴──────┴──────┴──────┘ │
│ 航 │ ┌─────────────────────┬──────────────┐ │
│ │ │ │ │ │
│ 220px│ │ 待办任务列表 │ AI 洞察 / │ │
│ │ │ (按紧急度排序) │ 团队概览 │ │
│ │ │ │ (主任) │ │
│ │ │ 类型筛选条 │ │ │
│ │ │ │ │ │
│ │ └─────────────────────┴──────────────┘ │
│ │ 快捷操作区 │
├──────┴──────────────────────────────────────────────────────┤
```
### 3.2 医生视角Doctor
**统计卡片4 列)**
| 序号 | 标签 | 数据源 | 颜色 |
|------|------|--------|------|
| 1 | 今日待办 | ActionItem count(assigned_to=me, status=pending) | 蓝 #2563EB |
| 2 | AI 建议待审 | ActionItem count(type=ai_suggestion, status=pending) | 紫 #7C3AED |
| 3 | 危急值告警 | ActionItem count(type=alert, severity=urgent) | 红 #DC2626 |
| 4 | 随访到期 | ActionItem count(type=followup, status=pending) | 橙 #D97706 |
**待办列表**
- 默认按紧急度排序urgent > high > normal > low
- 顶部筛选条:全部 / AI 建议 / 告警 / 随访 / 数据异常
- 每条显示:紧急度圆点 + 标题 + 摘要 + 类型标签 + 风险等级 + 时间 + 操作按钮
- 点击条目 → Drawer 展开详情
**右侧 AI 洞察面板340px**
- 顶部AI 图标 + "智能洞察" 标题 + 实时标记
- 最近 3 条 AI 分析洞察,每条包含:患者姓名/年龄/诊断、分析摘要、风险等级标签
- 第一条洞察下方放快捷操作按钮(联系患者/安排急诊/调整用药)
- 底部:快捷操作区(查询患者/新建随访/AI 分析)
### 3.3 科室主任视角Director
与医生共享同一页面结构,差异化:
**统计卡片5 列,多 2 个)**
| 序号 | 标签 | 说明 |
|------|------|------|
| 1 | 科室待办总计 | 全科室 pending 总数,副文本显示"我负责 N 项" |
| 2 | AI 建议待审 | 全科室范围 |
| 3 | 危急值告警 | 全科室范围 |
| 4 | 随访到期 | 全科室范围 |
| 5 | 今日处理率 | 已处理/总数,绿色 |
**待办列表**
- 仅显示分配给主任的待办:超时升级项、高风险 AI 建议审批、随访审核
- 超时升级项带"超时升级"标签和红色高亮
**右侧团队概览面板340px**
- 团队概览卡片:每位医生一行,显示姓名、职称、待办数、处理率进度条、超时数
- 超时医生卡片左侧红色边框 + 红色背景高亮
- 点击医生卡片 → 展开该医生的待办列表
- 底部:科室患者风险分布(高/中/低危统计)
---
## 4. 待办数据模型
### 4.1 ActionItem 统一结构
```typescript
interface ActionItem {
id: string; // UUID v7
tenant_id: string;
type: 'ai_suggestion' | 'alert' | 'followup' | 'data_anomaly';
severity: 'urgent' | 'high' | 'normal' | 'low';
status: 'pending' | 'in_progress' | 'completed' | 'escalated';
title: string; // "张伟 — 血压危急值告警"
summary: string; // 一句话摘要
patient_id: string;
patient_name: string;
assigned_to: string; // user_id
source_id: string; // 来源记录 IDai_suggestion.id / alert.id 等)
source_type: string; // "ai_suggestion" / "alert" / "followup" / "data_anomaly"
created_at: string;
updated_at: string;
due_at?: string; // 超时时间(用于升级判断)
metadata: Record<string, any>; // 类型特定的附加数据
}
```
### 4.2 类型与来源映射
| type | 来源模块 | 触发条件 | 默认 severity |
|------|---------|---------|--------------|
| `ai_suggestion` | erp-ai | AI 分析完成 + structured_output 含 suggestions | 从 AI risk_level 映射 |
| `alert` | erp-health | 告警引擎触发危急值 | `urgent` |
| `followup` | erp-health | 随访计划到期 | `normal` |
| `data_anomaly` | erp-health | 规则引擎检测到异常 | 从规则配置映射 |
### 4.3 严重度与升级规则
| severity | 圆点颜色 | 超时 | 升级行为 |
|----------|---------|------|---------|
| `urgent` | 红 #DC2626 | 4 小时 | 升级至科室主任 |
| `high` | 橙 #D97706 | 24 小时 | 升级至科室主任 |
| `normal` | 蓝 #2563EB | 72 小时 | 标记逾期 |
| `low` | 灰 #94A3B8 | 无 | 不升级 |
### 4.4 不建新表
Phase 1 采用三表 JOIN 聚合查询,不创建独立的 `action_items` 表:
- `ai_suggestions` + `ai_analysis` + `patient` → AI 建议类待办
- `alerts` + `patient` → 告警类待办
- `followup_plans` + `patient` → 随访类待办
- `device_readings`(异常) + `patient` → 数据异常类待办
理由:避免数据冗余和同步问题。各模块已有完整数据,工作台只做聚合展示。未来如需独立表,可作为 Phase 2 引入。
---
## 5. 交互设计
### 5.1 待办列表交互
**筛选**
- 顶部筛选条:全部 / AI 建议 / 告警 / 随访 / 数据异常
- 点击切换,无页面刷新,前端过滤已加载的数据
- 默认显示"全部"
**排序**
- 默认按紧急度排序urgent > high > normal > low
- 同级别按创建时间倒序
- 未来可扩展:按患者、按时间排序
**点击行为**
- 点击待办条目 → 右侧 Drawer 抽屉展开(宽度 480px
- Drawer 内容根据 `type` 不同渲染不同详情组件
### 5.2 Drawer 详情面板
**AI 建议 (ai_suggestion)**
- 顶部:类型标签 + 风险等级 + 触发时间
- 患者信息:姓名、年龄、性别、诊断、科室
- AI 分析摘要Markdown 渲染)
- 建议方案(高亮框)
- 基线指标网格2x2关键数值 + 趋势箭头)
- 操作按钮:批准 / 拒绝 / 修改后批准
**危急告警 (alert)**
- 告警详情:触发指标、阈值、当前值
- 患者基本信息
- 快捷操作:联系患者 / 安排急诊 / 调整用药
- 操作按钮:已处理 / 转交
**随访 (followup)**
- 随访计划详情
- 上次随访摘要
- 操作按钮:开始随访 / 延期 / 标记完成
**数据异常 (data_anomaly)**
- 异常指标详情
- 历史趋势迷你图
- 操作按钮:查看详情 / AI 分析 / 忽略
### 5.3 Modal 弹窗
**批准确认**
- 显示建议摘要
- "确认批准?" + 备注(可选)
- 按钮:确认 / 取消
**拒绝确认**
- 必填拒绝原因
- 按钮:确认拒绝 / 取消
**忽略确认**
- "确认忽略此待办?"
- 按钮:确认 / 取消
### 5.4 右侧面板交互
**医生 — AI 洞察面板**
- 显示最近 3 条 AI 分析洞察(按时间倒序)
- 第一条洞察下方显示快捷操作按钮
- 点击洞察标题 → 打开对应患者的 AI 建议 Drawer
**主任 — 团队概览面板**
- 每位医生一行卡片hover 高亮
- 点击医生卡片 → Drawer 展开该医生的待办列表
- 超时医生卡片:左侧红色边框 + 浅红背景 + 超时数红色高亮
- 底部风险分布:高/中/低危三色统计块
---
## 6. 角色感知
### 6.1 渲染逻辑
```
if user.role contains 'director' or 'department_head':
render DirectorDashboard()
else:
render DoctorDashboard()
```
共享组件:
- `WorkbenchLayout` — 整体布局框架
- `StatCardGrid` — 统计卡片网格
- `TodoList` — 待办列表
- `ActionDrawer` — 详情抽屉
- `ActionModal` — 操作弹窗
差异组件:
- `AiInsightPanel` — 医生专属AI 洞察
- `TeamOverviewPanel` — 主任专属,团队概览
- `RiskDistribution` — 主任专属,风险分布
### 6.2 数据范围差异
| 维度 | 医生 | 主任 |
|------|------|------|
| 待办范围 | `assigned_to = my_id` | `assigned_to = my_id` + 超时升级项 |
| 统计范围 | 仅自己 | 全科室 |
| 团队概览 | 不显示 | 显示本科室所有医生 |
| 风险分布 | 不显示 | 全科室患者 |
| AI 洞察 | 自己负责的患者 | 不显示(用团队概览替代)|
---
## 7. 后端 API
### 7.1 ActionInboxService
归属 crate`erp-health`
**核心方法**
| 方法 | 路由 | 说明 | 权限码 |
|------|------|------|--------|
| `list_action_items` | `GET /api/v1/health/action-inbox` | 聚合查询待办列表 | `health.action_inbox.list` |
| `get_action_thread` | `GET /api/v1/health/action-inbox/:id/thread` | 获取待办处理线程 | `health.action_inbox.list` |
| `get_team_overview` | `GET /api/v1/health/action-inbox/team` | 主任:团队概览 | `health.action_inbox.team` |
| `get_workbench_stats` | `GET /api/v1/health/action-inbox/stats` | 统计卡片数据 | `health.action_inbox.list` |
### 7.2 list_action_items 查询参数
```
GET /api/v1/health/action-inbox?type=ai_suggestion&severity=urgent&page=1&page_size=20
```
| 参数 | 类型 | 说明 |
|------|------|------|
| `type` | string | 可选筛选类型ai_suggestion / alert / followup / data_anomaly |
| `severity` | string | 可选,筛选紧急度 |
| `status` | string | 可选,默认 pending |
| `patient_id` | uuid | 可选,按患者筛选 |
| `page` | number | 页码,默认 1 |
| `page_size` | number | 每页条数,默认 20 |
### 7.3 聚合查询策略
不建独立表,通过 SQL UNION 聚合四类数据源:
```sql
-- AI 建议类
SELECT s.id, 'ai_suggestion' as type, s.risk_level as severity,
s.title, s.summary, s.patient_id, p.name as patient_name,
s.assigned_to, s.created_at, 'ai_suggestion' as source_type
FROM ai_suggestions s JOIN patients p ON s.patient_id = p.id
WHERE s.status = 'pending' AND s.tenant_id = $1
UNION ALL
-- 告警类
SELECT a.id, 'alert' as type, 'urgent' as severity,
a.title, a.summary, a.patient_id, p.name as patient_name,
a.assigned_to, a.created_at, 'alert' as source_type
FROM alerts a JOIN patients p ON a.patient_id = p.id
WHERE a.status = 'active' AND a.tenant_id = $1
-- ... 随访、异常类同理
ORDER BY severity_priority, created_at DESC
```
### 7.4 get_team_overview 响应
```json
{
"members": [
{
"user_id": "uuid",
"name": "李明远",
"title": "副主任医师",
"pending_count": 5,
"completed_count": 3,
"overdue_count": 1,
"completion_rate": 0.6
}
],
"risk_distribution": {
"high": 3,
"medium": 8,
"low": 24
},
"total_pending": 18,
"total_completed": 12
}
```
---
## 8. 与现有模块的关系
### 8.1 复用已有能力
| 能力 | 来源 | 复用方式 |
|------|------|---------|
| AI 建议数据 | `erp-ai` AiSuggestionService | 直接查询 ai_suggestions 表 |
| 告警数据 | `erp-health` AlertService | 直接查询 alerts 表 |
| 随访数据 | `erp-health` FollowupService | 直接查询 followup_plans 表 |
| 角色感知 | `Home.tsx` 已有模式 | 参考现有 role-based Dashboard 分发 |
| Drawer 组件 | `ActionThreadDrawer` 已存在 | 复用并扩展 |
| 建议审批 | `AiSuggestionTab` 已存在 | 复用审批逻辑 |
| SSE 客户端 | `analysisSse.ts` 已存在 | 不涉及(工作台不触发分析) |
### 8.2 新增代码归属
| 文件 | crate/模块 | 说明 |
|------|-----------|------|
| `action_inbox_service.rs` | erp-health/service | 聚合查询服务 |
| `action_inbox_handler.rs` | erp-health/handler | API 路由处理 |
| `WorkbenchHome.tsx` | web/pages | 新首页组件(替代 Home.tsx 中的 Dashboard 部分) |
| `TodoList.tsx` | web/components | 待办列表组件 |
| `AiInsightPanel.tsx` | web/components | AI 洞察面板 |
| `TeamOverviewPanel.tsx` | web/components | 团队概览面板 |
| `ActionDetailDrawer.tsx` | web/components | 操作详情抽屉 |
| `actionInbox.ts` | web/api/health | API 客户端(已存在,需补充) |
### 8.3 权限码
新增权限码(注册在 erp-health
| 权限码 | 说明 |
|--------|------|
| `health.action_inbox.list` | 查看待办列表 |
| `health.action_inbox.manage` | 处理待办(批准/拒绝/转交) |
| `health.action_inbox.team` | 查看团队概览(主任专属) |
---
## 9. 实施分步
### Phase 1A后端聚合 API2-3 天)
1. 创建 `action_inbox_service.rs` — 实现四类数据源 UNION 聚合查询
2. 创建 `action_inbox_handler.rs` — 注册 4 个 API 端点
3. 注册权限码 `health.action_inbox.*`
4. 编写单元测试
### Phase 1BWeb 前端组件2-3 天)
1. 创建 `TodoList.tsx` — 待办列表组件(筛选、排序、点击)
2. 创建 `AiInsightPanel.tsx` — AI 洞察面板
3. 创建 `TeamOverviewPanel.tsx` — 团队概览面板
4. 创建 `ActionDetailDrawer.tsx` — 详情抽屉(按类型渲染不同内容)
5. 补充 `actionInbox.ts` API 客户端
6. 改造 `Home.tsx` — 用新组件替换现有 Dashboard
### Phase 1C联调验证1 天)
1. 前后端联调
2. 角色感知测试(医生视角 vs 主任视角)
3. 四类待办数据验证
4. `pnpm build` 生产构建通过
---
## 10. 验证标准
### 功能验证
- [ ] 医生登录后首页显示工作台4 个统计卡片 + 待办列表 + AI 洞察)
- [ ] 主任登录后首页显示工作台5 个统计卡片 + 我的待办 + 团队概览)
- [ ] 待办列表按紧急度正确排序
- [ ] 类型筛选条正确过滤
- [ ] 点击 AI 建议待办 → Drawer 展示完整分析 + 操作按钮
- [ ] 批准/拒绝操作 → Modal 确认 → 调用后端 API 成功
- [ ] 主任团队概览正确显示每位医生的工作负载
- [ ] 超时升级项正确出现在主任的待办列表
### 技术验证
- [ ] `cargo check` 全 workspace 通过
- [ ] `cargo test` 相关测试通过
- [ ] `pnpm build` 前端生产构建通过
- [ ] API 可通过 Swagger UI 测试
- [ ] 权限码正确拦截未授权访问

View File

@@ -0,0 +1,539 @@
# 医生 & 运营 & 管理员工作台设计规格
> 日期: 2026-05-02 | 状态: 设计中
> 原型参考: `_temp/workbench-doctor-A.html`(医生)、`_temp/workbench-operator-C.html`(运营)、`_temp/workbench-admin.html`(管理员)
> 关联文档: `2026-05-02-health-manager-workbench-design.md`(健康管家方案 B
## 1. 背景与目标
### 1.1 与健康管家工作台的关系
本规格定义健康管家之外的三个角色工作台。四者共享同一平台、同一全局侧边栏、同一路由入口 `/``Home.tsx`),通过 `useDashboardRole()``doctor` / `health_manager` / `operator` / `admin` 角色分发不同的工作台视图。
| 角色 | 方案 | 核心交互模式 | 本期优先级 |
|------|------|-------------|-----------|
| 健康管家 | B 工作流驱动 | 任务队列一件一件处理 | Phase 1已设计 |
| 医生 | A 今日全景 | 仪表盘总览 + AI 建议一键审核 | Phase 1本文档 |
| 运营 | C AI 指挥中心 | AI 洞察驱动运营决策 | Phase 1本文档 |
| 管理员 | 系统管理中心 | 平台运维全局视角 | Phase 1本文档 |
### 1.2 医生角色目标
| 指标 | 当前 | 目标 |
|------|------|------|
| AI 建议审核响应时间 | 无专门入口,散落在 AI 分析列表 | 工作台直接展示,一键采纳/拒绝 |
| 患者咨询回复时间 | 需进入咨询列表才能看到未读 | 工作台右侧实时显示未回复咨询 |
| 关注患者认知负担 | 需逐个翻患者详情 | 聚合告警+AI分析的关注列表 |
### 1.3 运营角色目标
| 指标 | 当前 | 目标 |
|------|------|------|
| 内容运营效率 | 各功能模块独立操作 | AI 推荐热点内容,一键关联发布 |
| 积分异常发现 | 被动审核订单 | AI 主动发现异常兑换模式 |
| 运营决策数据支撑 | 需单独查看统计报表 | 工作台实时展示核心指标 + 趋势 |
### 1.4 范围
**本期Phase 1**
- 医生4 统计卡片 + AI 建议待审 + 重点关注患者 + 日程 + 咨询摘要
- 运营AI 洞察卡片 + 4 数据指标 + 热门内容排行 + 待办列表 + 积分动态 + 内容矩阵
**不在本期:**
- 医生角色的方案 B/C 工作流模式
- 运营角色的方案 A/B 仪表盘模式
- 管理员的实时系统指标监控CPU/内存/磁盘)
- 角色间的任务转交/协作流程Phase 2
- AI 洞察的 SSE 实时推送Phase 2 使用 30 秒轮询)
## 2. 角色定义
### 2.1 医生 — 李明辉
| 维度 | 描述 |
|------|------|
| 姓名 | 李明辉 |
| 职位 | 主治医师 · 肾内科 |
| 年龄 | 42 岁 |
| 工作经验 | 15 年临床8 年肾内科 |
| 技术水平 | 基本操作无障碍,不追求花哨功能 |
| 日均任务量 | AI 建议审核 3-5 项、咨询回复 2-3 条、查看危急值 1-2 次 |
#### 典型工作日
```
07:30 晨间查房,查看住院患者
08:00 门诊开始,打开工作台扫一眼今日概览
08:30 门诊间隙审核 AI 建议(采纳/拒绝)
10:00 回复患者咨询消息
10:30 多学科会诊
14:00 下午门诊
16:00 查看告警中心,确认危急值已处理
17:00 下班
```
#### 核心痛点
1. **AI 建议易遗忘**AI 分析结果在独立列表中,不强制提醒,容易积压
2. **咨询回复不及时**:不知道有新咨询,需要主动刷新咨询列表
3. **患者上下文分散**:看 AI 建议时需要单独翻患者详情了解病史
### 2.2 运营 — 王美玲
| 维度 | 描述 |
|------|------|
| 姓名 | 王美玲 |
| 职位 | 健康运营专员 |
| 年龄 | 28 岁 |
| 工作经验 | 3 年互联网运营1 年健康领域 |
| 技术水平 | 熟练使用各种运营工具,对数据敏感 |
| 日均任务量 | 文章管理 2-3 篇、积分审核 5-10 笔、活动跟进 1-2 个 |
#### 典型工作日
```
08:30 打开工作台,查看 AI 洞察和昨日数据
09:00 审核积分兑换订单AI 标记异常的优先)
10:00 发布新科普文章,关联热门话题
11:00 跟进线下活动报名情况
14:00 查看内容阅读数据,优化推送策略
15:00 整理运营周报
16:30 跟进沉默用户,发送关怀消息
17:30 下班
```
#### 核心痛点
1. **数据分散**:积分、内容、活动数据在不同页面,需来回切换
2. **缺乏数据驱动**:发什么内容凭经验,不知道什么受欢迎
3. **异常发现慢**:积分兑换异常靠人工发现,等到发现时已造成损失
### 2.3 管理员 — 陈建国
| 维度 | 描述 |
|------|------|
| 姓名 | 陈建国 |
| 职位 | 系统管理员 / 平台运维 |
| 年龄 | 35 岁 |
| 工作经验 | 8 年 IT 运维3 年医疗信息化 |
| 技术水平 | 熟悉 Linux/Docker/数据库,能看懂 Rust 错误日志 |
| 日均任务量 | 用户管理 5-10 次、权限配置 1-2 次、系统巡检 3-4 次 |
#### 核心痛点
1. **系统健康不可见**:不知道各模块是否正常运行,出了问题被动发现
2. **操作追溯分散**:用户做了什么操作需要翻审计日志表,没有聚合视图
3. **配置入口分散**:用户管理/角色权限/菜单/字典/插件分布在不同页面,来回切换
## 3. 信息架构
### 3.1 医生:方案 A「今日全景」布局
```
┌──────────────────────────────────────────────────────────────┐
│ 全局侧边栏(已有) │ 医生工作台页面内部 │
│ │ │
│ 🏠 今日全景(高亮) │ ┌──────────────────────────────────────┐ │
│ 🩺 患者管理 │ │ 欢迎栏:上午好,李医生 │ │
│ 💬 患者咨询 │ ├──────────────────────────────────────┤ │
│ 📅 预约排班 │ │ 4 统计卡片(危急值/待审/咨询/预约) │ │
│ 🤖 AI 分析 │ ├──────────────────┬───────────────────┤ │
│ ⚠️ 告警中心 │ │ 左主内容flex │ 右窄栏340px │ │
│ 📊 统计报表 │ │ │ │ │
│ │ │ AI 建议待审列表 │ 今日日程 │ │
│ │ │ (采纳/拒绝按钮) │ 未回复咨询 │ │
│ │ │ │ 快捷操作 │ │
│ │ │ 重点关注患者列表 │ │ │
│ │ │ │ │ │
│ │ └──────────────────┴───────────────────┘ │
└──────────────────────────────────────────────────────────────┘
```
**布局参数**
- 左主内容:`flex: 1`,包含 AI 建议和关注患者两个 Card
- 右窄栏:固定 340px包含日程、咨询、快捷操作
### 3.2 运营:方案 C「AI 指挥中心」布局
```
┌──────────────────────────────────────────────────────────────┐
│ 全局侧边栏(已有) │ 运营工作台页面内部 │
│ │ │
│ 🤖 AI 工作台(高亮) │ ┌──────────────────────────────────────┐ │
│ 📝 内容管理 │ │ AI 洞察卡片(暖橙色渐变 Hero │ │
│ 🎁 积分商城 │ │ 3 条洞察 + 操作按钮 │ │
│ 🎪 线下活动 │ ├──────────────────────────────────────┤ │
│ 👥 患者管理 │ │ 4 数据指标卡片(活跃/阅读/积分/订单) │ │
│ 📊 统计报表 │ ├──────────────────┬───────────────────┤ │
│ ⚙️ 系统设置 │ │ 热门科普排行 │ 今日待办 │ │
│ │ ├──────────────────┼───────────────────┤ │
│ │ │ 积分动态 │ 内容矩阵 │ │
│ │ └──────────────────┴───────────────────┘ │
└──────────────────────────────────────────────────────────────┘
```
**布局参数**
- 顶部AI Hero 卡片(全宽)
- 第二行4 数据指标grid 4 列)
- 第三行起双栏卡片grid 2 列等宽)
- 最大内容宽度 960px`max-width`
### 3.3 管理员:系统管理中心布局
```
┌──────────────────────────────────────────────────────────────┐
│ 全局侧边栏(已有) │ 管理员工作台页面内部 │
│ │ │
│ 🔧 系统管理(高亮) │ ┌──────────────────────────────────────┐ │
│ 👤 用户管理 │ │ 系统健康条6 项状态指示灯) │ │
│ 🔑 角色权限 │ ├──────────────────────────────────────┤ │
│ 📖 菜单管理 │ │ 4 统计卡片(用户/模块/操作/工单) │ │
│ 📋 审计日志 │ ├──────────────────┬───────────────────┤ │
│ ⚙️ 系统配置 │ │ 审计日志 │ 模块状态 │ │
│ 🧩 插件管理 │ ├──────────────────┼───────────────────┤ │
│ 📊 统计报表 │ │ 用户活跃度 │ 系统管理快捷入口 │ │
│ │ └──────────────────┴───────────────────┘ │
└──────────────────────────────────────────────────────────────┘
```
**布局参数**
- 顶部系统健康条全宽flex 横向排列 6 项状态)
- 第二行4 统计卡片grid 4 列)
- 第三行起双栏卡片grid 2 列等宽)
- 最大内容宽度 1080px
## 4. 组件清单
### 4.1 医生组件
| 组件 | 说明 | 数据源 |
|------|------|--------|
| **DoctorStatsRow** | 4 统计卡片:危急值/待审/咨询/预约 | `GET /health/action-inbox/stats` + `GET /health/doctor/dashboard` |
| **AiSuggestionList** | AI 建议待审列表,内嵌采纳/拒绝按钮 | `GET /health/action-inbox`(过滤 `type=ai_suggestion` |
| **FocusedPatientList** | 重点关注患者(聚合告警+AI 分析) | `GET /health/action-inbox`(按患者聚合) |
| **TodaySchedule** | 今日日程(查房/门诊/会诊) | `GET /health/doctor-schedules`(过滤今日) |
| **ConsultSummary** | 未回复咨询摘要(侧栏紧凑版) | `GET /health/consultation-sessions`(过滤未回复) |
| **QuickActions** | 快捷入口AI 分析/告警/患者查询) | 前端静态配置 |
### 4.2 运营组件
| 组件 | 说明 | 数据源 |
|------|------|--------|
| **AiInsightHero** | AI 洞察 Hero 卡片(暖橙色渐变) | Phase 1 前端静态展示Phase 2 接 AI 生成 |
| **OperatorStatsRow** | 4 数据指标(活跃/阅读/积分/订单) | `GET /health/admin/statistics/dashboard` + `GET /health/admin/points/statistics` |
| **HotContentRank** | 热门科普排行(阅读量排名) | `GET /health/articles`(按 view_count 排序) |
| **OperatorTodoList** | 今日待办(积分审核/发布文章/活动跟进等) | 前端状态管理Phase 2 扩展为任务系统) |
| **PointsFeed** | 积分动态流(近期获得/兑换) | `GET /health/admin/points/patients/{id}/transactions`(聚合) |
| **ContentMatrix** | 内容矩阵(分类分布条形图) | `GET /health/articles`(按分类聚合统计) |
### 4.3 共享组件
以下组件在两个角色中复用,与健康管家工作台共享:
| 组件 | 复用场景 |
|------|---------|
| **StatCard** | 统计卡片基础样式(顶部色条 + 数值 + 副文本) |
| **Card** | 通用卡片容器(圆角 + 边框 + header + body |
### 4.4 管理员组件
| 组件 | 说明 | 数据源 |
|------|------|--------|
| **SystemHealthBar** | 6 项系统状态指示灯API/DB/Redis/邮件/存储/定时任务) | Phase 1 前端静态Phase 2 接 `/api/v1/system/health` |
| **AdminStatsRow** | 4 统计卡片(注册用户/业务模块/今日操作/待处理工单) | `GET /api/v1/users`(总数)+ 前端计算 |
| **AuditLogList** | 最近操作记录(操作人+动作+时间) | `GET /api/v1/audit-logs`(最新 6 条) |
| **ModuleStatusList** | 模块状态列表(已启用/未启用) | 前端静态配置(模块列表固定) |
| **UserActivityChart** | 用户活跃度分布(今日/本周/本月/总注册 + 角色分布) | `GET /api/v1/users`(聚合统计) |
| **AdminQuickActions** | 系统管理快捷入口8 个管理功能) | 前端静态配置 |
### 4.5 数据优先级说明
**医生**:核心数据来自 `action-inbox` 系统(与健康管家共享),额外需要 `doctor/dashboard` API 提供预约和日程数据。
**运营**:核心数据来自 `statistics/dashboard` + `articles` + `points` API数据维度完全不同独立于 action-inbox 系统。
## 5. 数据流
### 5.1 医生数据源映射
| 组件 | API | 参数 | 返回 |
|------|-----|------|------|
| DoctorStatsRow | `GET /health/action-inbox/stats` | — | `WorkbenchStats`pending_count 等) |
| DoctorStatsRow | `GET /health/doctor/dashboard` | — | 预约数、日程数 |
| AiSuggestionList | `GET /health/action-inbox` | `type=ai_suggestion, status=pending` | `ActionItem[]` |
| FocusedPatientList | `GET /health/action-inbox` | `status=pending`(前端按 patient_id 聚合) | `ActionItem[]` |
| TodaySchedule | `GET /health/doctor-schedules` | `date=today` | `ScheduleItem[]` |
| ConsultSummary | `GET /health/consultation-sessions` | `status=active, unread=true` | `Consultation[]` |
**并行请求策略**:页面加载时使用 `Promise.allSettled` 并行请求 stats + list + schedule + consultations。
### 5.2 运营数据源映射
| 组件 | API | 参数 | 返回 |
|------|-----|------|------|
| OperatorStatsRow | `GET /health/admin/statistics/dashboard` | — | 综合统计数据 |
| OperatorStatsRow | `GET /health/admin/points/statistics` | — | 积分统计 |
| HotContentRank | `GET /health/articles` | `sort=-view_count, page_size=5` | `Article[]` |
| ContentMatrix | `GET /health/articles` | `page_size=1`(仅需 total | 按分类聚合(前端或后端) |
| PointsFeed | `GET /health/admin/points/patients` | 最近交易(需后端扩展) | 积分流水 |
**AI Hero 洞察**Phase 1 使用前端静态文案硬编码洞察示例。Phase 2 接入 AI 生成接口。
### 5.3 管理员数据源映射
| 组件 | API | 参数 | 返回 |
|------|-----|------|------|
| AdminStatsRow | `GET /api/v1/users` | `page=1, page_size=1` | 用户总数(从 pagination.total |
| AuditLogList | `GET /api/v1/audit-logs` | `page=1, page_size=6` | `AuditLogItem[]` |
| UserActivityChart | `GET /api/v1/users` | 聚合查询 | 角色分布统计 |
| SystemHealthBar | Phase 1 静态 | — | 前端硬编码状态 |
| ModuleStatusList | Phase 1 静态 | — | 前端硬编码模块列表 |
| AdminQuickActions | 前端静态 | — | 路由配置 |
**管理员数据特点**:不依赖 `erp-health` 的任何 API数据来自 ERP 基础模块(用户、审计、配置)。
### 5.4 与 action-inbox 的关系
| 角色 | 使用方式 |
|------|---------|
| 健康管家 | 完全基于 action-inbox任务队列 + 详情处理 |
| 护士 | 复用健康管家工作台(同一任务队列),不做区分 |
| 医生 | 部分使用 action-inbox仅读取 AI 建议和告警),**不做任务处理流程** |
| 运营 | 不使用 action-inbox数据来源独立 |
| 管理员 | 不使用 action-inbox数据来自 ERP 基础模块(用户/审计/配置) |
医生角色的 action-inbox 使用是**只读展示**:在 AI 建议列表中直接嵌入「采纳/拒绝」按钮,调用 `/health/ai-analysis/:id/review`,不走 action-inbox 的 complete 流程。
## 6. 交互流程
### 6.1 医生AI 建议审核
以「张伟 — 血压持续升高建议调整用药」为例:
```
1. 进入工作台
└→ 4 统计卡片显示:待审 5 条、未回复 3 条
2. 浏览 AI 建议列表
└→ 列表按风险等级排序(高→中→低)
└→ 每条建议显示:风险徽章 + 标题 + 摘要 + 采纳/拒绝按钮
3. 审核建议
└→ 点击「采纳」→ 调用 POST /health/ai-analysis/:id/review?action=approve
└→ 或点击「拒绝」→ 弹出拒绝原因输入 → 提交
└→ 操作完成后,该建议从列表移除
4. 查看关注患者
└→ 下方列表聚合了有告警/AI 建议的患者
└→ 点击患者名称 → 跳转患者详情页
5. 查看右侧信息
└→ 日程卡片:了解今日安排
└→ 咨询摘要:点击未回复咨询 → 跳转咨询详情页
```
### 6.2 运营AI 洞察驱动操作
以「透析饮食管理文章爆发」洞察为例:
```
1. 进入工作台
└→ AI Hero 卡片展示 3 条洞察,每条附带操作按钮
2. 响应洞察
└→ 点击「发布关联文章」→ 跳转文章编辑器(预填关联标签)
└→ 或点击「审核积分订单」→ 跳转积分订单列表(标记异常)
3. 查看热门排行
└→ 了解哪些内容受欢迎,指导后续内容策略
4. 处理今日待办
└→ 待办列表展示优先级事项
└→ 点击操作按钮 → 跳转对应管理页面
5. 监控积分动态
└→ 实时查看积分发放/兑换流水
```
### 6.3 管理员:系统巡检
```
1. 进入工作台
└→ 系统健康条显示 6 项服务状态(绿灯/黄灯/红灯)
2. 确认系统正常
└→ 所有绿灯 → 放心继续
└→ 发现黄灯(如邮件队列积压)→ 点击进入邮件服务配置
3. 查看统计卡片
└→ 注册用户/业务模块/今日操作/待处理工单
4. 翻看审计日志
└→ 最近 6 条操作记录(谁做了什么)
└→ 发现异常操作 → 点击进入完整审计日志
5. 检查模块状态
└→ 确认各模块运行中
└→ 发现未启用模块 → 考虑是否启用
6. 用户活跃度
└→ 查看今日/本周/本月活跃数
└→ 角色分布确认人员配置合理
7. 快捷管理入口
└→ 点击用户管理/角色权限/系统配置等 → 跳转对应管理页面
```
### 6.4 空状态处理
| 角色 | 区域 | 空状态展示 |
|------|------|-----------|
| 医生 | AI 建议列表 | 「暂无待审 AI 建议」 |
| 医生 | 关注患者 | 「暂无重点关注患者」 |
| 医生 | 咨询摘要 | 「暂无未回复咨询」 |
| 医生 | 日程 | 「今日暂无安排」 |
| 运营 | AI Hero | 静态内容,不会为空 |
| 运营 | 热门排行 | 「暂无发布内容」 |
| 运营 | 待办列表 | 「今日暂无待办」 |
| 管理员 | 审计日志 | 「暂无操作记录」 |
| 管理员 | 系统健康 | 全绿灯,静态展示 |
### 6.5 前端状态管理
三个角色各自独立数据获取30 秒轮询刷新:
```typescript
// 医生工作台 — 使用现有 workbenchStore + 额外请求
const DoctorDashboard: React.FC = () => {
const { refreshTasks, refreshStats } = useWorkbenchStore();
// 额外请求:日程、咨询
const [schedules, setSchedules] = useState([]);
const [consults, setConsults] = useState([]);
useEffect(() => {
refreshTasks();
refreshStats();
Promise.allSettled([
doctorApi.getDashboard(),
consultationApi.list({ status: 'active', unread: true }),
]).then(...);
}, []);
};
// 运营工作台 — 独立数据获取,不使用 workbenchStore
const OperatorDashboard: React.FC = () => {
const [stats, setStats] = useState(null);
const [articles, setArticles] = useState([]);
useEffect(() => {
Promise.allSettled([
statsApi.getDashboard(),
articlesApi.list({ sort: '-view_count', page_size: 5 }),
pointsApi.getStatistics(),
]).then(...);
}, []);
};
// 管理员工作台 — 独立数据获取,不使用 workbenchStore
const AdminDashboard: React.FC = () => {
const [auditLogs, setAuditLogs] = useState([]);
const [userCount, setUserCount] = useState(0);
useEffect(() => {
Promise.allSettled([
auditLogApi.list({ page: 1, page_size: 6 }),
userApi.list({ page: 1, page_size: 1 }), // 只取 total
]).then(...);
}, []);
};
```
## 7. 与现有系统的关系
### 7.1 角色路由分发
`Home.tsx` 中通过 `useDashboardRole()` 分发不同工作台组件:
```typescript
const HomePage = () => {
const role = useDashboardRole();
switch (role) {
case 'doctor':
return <DoctorDashboard />;
case 'health_manager':
case 'nurse':
return <HealthManagerDashboard />; // 方案 B TaskQueue + TaskDetail
case 'operator':
return <OperatorDashboard />;
case 'admin':
return <AdminDashboard />; // 系统管理中心
default:
return <AdminDashboard />;
}
};
```
### 7.2 涉及文件
| 文件 | 改动类型 | 说明 |
|------|---------|------|
| `apps/web/src/pages/Home.tsx` | **改造** | 角色路由分发 + 导入各角色组件 |
| `apps/web/src/pages/health/components/workbench/DoctorDashboard.tsx` | **新增** | 医生方案 A 仪表盘 |
| `apps/web/src/pages/health/components/workbench/OperatorDashboard.tsx` | **新增** | 运营方案 C AI 指挥中心 |
| `apps/web/src/pages/health/components/workbench/AdminDashboard.tsx` | **新增** | 管理员系统管理中心 |
| `apps/web/src/pages/health/components/workbench/TaskQueue.tsx` | **已有** | 健康管家方案 BPhase 1 已实现) |
| `apps/web/src/pages/health/components/workbench/TaskDetail.tsx` | **已有** | 健康管家方案 BPhase 1 已实现) |
### 7.3 数据库变更
Phase 1 **无需新建表**。所有数据来自现有 API
- 医生action-inbox + doctor/dashboard + consultation-sessions + doctor-schedules
- 运营statistics/dashboard + articles + points/statistics
- 管理员users + audit-logs + 前端静态配置
### 7.4 权限
沿用现有权限码,无需新增:
| 角色 | 所需权限码 |
|------|-----------|
| 医生 | `health.action-inbox.list` + `health.ai-analysis.review` + `health.consultation.manage` + `health.doctor-schedule.list` |
| 运营 | `health.statistics.dashboard` + `health.articles.list` + `health.points.manage` |
| 管理员 | `system.users.list` + `system.audit-logs.list` + `system.settings.read` |
### 7.5 验收标准
**医生工作台:**
- [ ] 工作台显示 4 项统计数据(危急值/待审/咨询/预约)
- [ ] AI 建议列表按风险排序,每条有采纳/拒绝按钮
- [ ] 采纳/拒绝操作调用对应 API 并刷新列表
- [ ] 重点关注患者列表聚合展示(告警+AI 建议)
- [ ] 右侧日程卡片显示今日排班
- [ ] 右侧咨询摘要显示未回复消息
- [ ] 空状态有合理展示
**运营工作台:**
- [ ] AI Hero 卡片展示 3 条洞察 + 操作按钮Phase 1 静态内容)
- [ ] 4 项数据指标展示(活跃用户/阅读量/积分发放/待审订单)
- [ ] 热门科普排行按阅读量排序(前 5 名)
- [ ] 今日待办列表展示优先事项
- [ ] 积分动态流展示近期交易
- [ ] 内容矩阵展示分类分布
- [ ] 空状态有合理展示
**管理员工作台:**
- [ ] 系统健康条展示 6 项服务状态Phase 1 静态)
- [ ] 4 项统计卡片展示(注册用户/业务模块/今日操作/待处理工单)
- [ ] 审计日志展示最近 6 条操作记录
- [ ] 模块状态列表展示已启用/未启用模块
- [ ] 用户活跃度分布展示
- [ ] 8 个系统管理快捷入口可点击跳转
- [ ] 空状态有合理展示
**全局:**
- [ ] `useDashboardRole()` 正确分发五个角色的工作台视图
- [ ] 30 秒自动刷新数据
- [ ] `cargo check` 通过
- [ ] `pnpm build` 通过
- [ ] 浏览器中实际操作验证通过

View File

@@ -0,0 +1,415 @@
# 系统硬编码清理设计规格
> 日期: 2026-05-02 | 状态: DRAFT | 范围: 前端硬编码清理 + 后端 API 补建 + 常量统一 + 医疗阈值配置化
---
## 1. 背景与动机
HMS 健康管理平台已进入关键节点 — 后端 328 条路由、45 个 Entity、772 个测试函数已就位。但前端工作台页面存在大量硬编码假数据,直接影响系统可信度:
- **AdminDashboard** 中 70% 面板数据为假值(系统健康条、用户活跃度、模块状态、角色分布)
- **OperatorWorkbench** 中积分动态使用假姓名、内容矩阵数字硬编码、待办事项不可操作
- 20+ 处状态映射在多个文件中重复定义(严重度映射 5 处、性别映射 4 处、设备类型 3 处)
- 医疗报警阈值(血压/心率/血糖)硬编码在小程序前端,不同患者/年龄段无法差异化
**目标**:清除所有 CRITICAL 级硬编码,确保用户看到的每个数字都来自真实 API同时建立常量管理规范防止回退。
**原则**
- 新建 API 全部遵循现有 `/api/v1/` 前缀 + `ApiResponse<T>` 包装 + 多租户隔离
- 常量统一采用混合策略:静态枚举收敛到 `constants/health.ts`,动态选项对接字典 API
- 医疗阈值复用现有 `critical_value_threshold` 表及 CRUD API补充患者端只读接口
---
## 2. 影响范围与严重度矩阵
### 2.1 CRITICAL — 用户看到虚假数据
| ID | 文件 | 硬编码内容 | 影响面 |
|----|------|-----------|--------|
| C-1 | `AdminDashboard.tsx` 行 108-115 | 系统健康条 6 项全部假数据 | 所有管理员看到的"API服务正常""队列积压12"等均为虚假 |
| C-2 | `AdminDashboard.tsx` 行 230-258 | 用户活跃度 4 项 + 角色分布 5 项全部假数据 | 今日活跃 23 人、医生 12 人等均为虚假 |
| C-3 | `AdminDashboard.tsx` 行 41-50 | 模块状态 8 项硬编码 | "44 实体 · 328 路由"等描述不反映真实状态 |
| C-4 | `AdminDashboard.tsx` 行 87 | 待处理工单硬编码为 5 | 管理员误以为有 5 个工单待处理 |
| C-5 | `OperatorWorkbench.tsx` 行 40-44 | 积分动态 3 人假姓名 | 张伟/王建国/李秀英均为虚构 |
| C-6 | `OperatorWorkbench.tsx` 行 142-147 | 内容矩阵"已发布 24 / 草稿箱 3"硬编码 | 不反映真实文章数量 |
| C-7 | `OperatorWorkbench.tsx` 行 61 | AI Hero 卡片固定文案"3 个运营洞察" | 不随实际数据变化 |
### 2.2 HIGH — 限制扩展性
| ID | 类别 | 数量 | 典型案例 |
|----|------|------|---------|
| H-1 | 重复状态映射 | 20+ 处 | SEVERITY_COLOR 在 5 个文件各自定义 |
| H-2 | 动态选项硬编码 | 6 类 | 科室/职称/设备类型/随访类型/咨询类型/家庭关系 |
| H-3 | 默认角色为 admin | 1 处 | `useDashboardRole.ts` 无角色时返回 admin |
| H-4 | 医疗阈值硬编码 | 2 处 | 血压 140/90、心率 100/60、血糖 6.1/7.8 |
### 2.3 MEDIUM — 代码质量
| ID | 类别 | 说明 |
|----|------|------|
| M-1 | 小程序菜单用中文做 key | `MENU_PATHS` 用中文字符串做映射 key |
| M-2 | 待办模板硬编码 | 5 条固定待办文本,不可操作 |
---
## 3. 轨道 1工作台真实数据化
### 3.1 现有 API 清单与缺口分析
**已有且已被前端调用的 API**
| API | 路径 | 使用者 |
|-----|------|--------|
| 工作台统计 | `GET /health/action-inbox/stats``WorkbenchStats` | DoctorWorkbench, OperatorWorkbench |
| 团队概览 | `GET /health/action-inbox/team``TeamOverview` | DoctorWorkbench |
| 行动收件箱 | `GET /health/action-inbox``ActionItem[]` | DoctorWorkbench |
| 6 类统计 | `useStatsData` → patient/consultation/followup/points/healthData/dialysis | 三个工作台 |
| 个人统计 | `pointsApi.getPersonalStats``PersonalStats` | DoctorWorkbench |
| 审计日志 | `listAuditLogs` | AdminDashboard |
**缺口 — 需要新建的 5 个 API**
### 3.2 新增后端 API 设计
#### API-1: 系统健康检查
```
GET /health/admin/system-health
(完整路径: /api/v1/health/admin/system-health前缀由 erp-server nest 自动添加)
权限: health.dashboard.manage新增需在 module.rs 权限描述符中注册)
响应: ApiResponse<SystemHealth>
```
```typescript
interface SystemHealth {
services: {
name: string; // "API 服务" / "数据库" / "Redis" / ...
status: 'healthy' | 'degraded' | 'down';
message: string; // "正常" / "队列积压 12" / "连接超时"
response_ms?: number;
}[];
checked_at: string; // ISO timestamp
}
```
实现:后端 handler 逐项检查 DB 连接(`SELECT 1`、Redis PING、SMTP 配置状态、存储路径可用性、定时任务心跳。结果缓存 30 秒避免频繁检查。
#### API-2: 用户活跃度统计
```
GET /health/admin/user-activity
权限: health.dashboard.manage同 API-1
响应: ApiResponse<UserActivity>
```
```typescript
interface UserActivity {
daily_active: number;
weekly_active: number;
monthly_active: number;
total_registered: number;
by_role: {
role: string; // "医生" / "护士" / ...
count: number;
}[];
}
```
实现:基于 `users``last_login_at` 字段统计,角色分布通过 `user_roles` JOIN `roles` 聚合。
#### API-3: 模块状态
```
GET /health/admin/modules
权限: health.dashboard.manage同 API-1
响应: ApiResponse<ModuleStatus[]>
```
```typescript
interface ModuleStatus {
name: string; // "erp-health" / "erp-auth" / ...
display_name: string; // "健康管理" / "身份权限" / ...
description: string;
active: boolean;
entity_count?: number;
route_count?: number;
}
```
实现:从 AppState 中读取已注册的 `ErpModule` 列表 + 查询 `plugins` 表补充插件状态。
#### API-4: 积分动态流
```
GET /health/points/recent-activity?limit=10
权限: health.points.list
响应: ApiResponse<PointsActivityItem[]>
```
> **命名约定**TypeScript 接口字段使用 snake_case 镜像后端字段名,与本项目既有模式一致。
```typescript
interface PointsActivityItem {
id: string;
user_name: string; // 患者姓名
detail: string; // "兑换 · 血压计袖带" / "每日上报 · 血压"
amount: string; // "+10" / "-500"
type: 'earn' | 'spend';
created_at: string;
}
```
实现:查询 `points_account_transactions` 最新 N 条JOIN `patients` 获取姓名。
#### API-5: 内容统计
```
GET /health/articles/stats
权限: health.articles.list注意复数形式匹配 module.rs 注册的权限码)
响应: ApiResponse<ArticleStats>
```
```typescript
interface ArticleStats {
published: number;
draft: number;
pending_review: number;
rejected: number;
total_views: number;
}
```
实现:`SELECT status, COUNT(*) FROM articles WHERE tenant_id = $1 AND deleted_at IS NULL GROUP BY status`
### 3.3 工作台页面改造方案
#### AdminDashboard 改造
| 面板 | 当前(假数据) | 改造后(真实数据) |
|------|---------------|-------------------|
| 系统健康条 | 6 项硬编码 | 调用 API-1 `systemHealth` |
| 统计卡片 - 注册用户 | useStatsData (已真实) | 保持不变 |
| 统计卡片 - 业务模块 | 硬编码 MODULES.length | 调用 API-3 计算活跃数 |
| 统计卡片 - 今日操作 | auditLogs.length (已真实) | 保持不变 |
| 统计卡片 - 待处理工单 | 硬编码 5 | 改用 actionInboxApi.stats().total_pending |
| 用户活跃度 | 4 项百分比 + 角色分布全部假 | 调用 API-2 `userActivity` |
| 模块状态 | MODULES 数组硬编码 | 调用 API-3 `modules` |
| 快捷管理 | QUICK_ACTIONS 硬编码 | 保留(属 UI 配置) |
| 问候语 "X主任" | 硬编码"主任" | 移除称呼后缀,只显示姓 |
#### OperatorWorkbench 改造
| 面板 | 当前 | 改造后 |
|------|------|--------|
| AI Hero 卡片 | 固定文案 "3 个运营洞察" | 动态生成文案基于 stats 数据 |
| 统计卡片 | useStatsData + actionInbox (已真实) | 保持不变 |
| 今日待办 | 5 条硬编码 todo 模板 | 改用 actionInboxApi.list 筛选 pending 项 |
| 积分动态 | 3 人假姓名 | 调用 API-4 `recentActivity` |
| 内容矩阵 | "已发布 24 / 草稿箱 3" | 调用 API-5 `articleStats` |
| 问候语 "X美玲" | 硬编码"美玲" | 移除,只显示姓 |
#### DoctorWorkbench
基本已是真实数据80%),仅需微调:
- 问候语 "X医生" 中"医生"后缀 → 移除,只显示姓
- 确认 `personalStats.consultations_this_month` 字段后端已实现(否则用 0 占位)
---
## 4. 轨道 2常量统一
### 4.1 分类原则
| 分类 | 存放位置 | 判断标准 | 例子 |
|------|---------|---------|------|
| 静态映射 | `constants/health.ts` | 枚举固定,后端不可能新增项 | 性别、血型、严重度颜色、状态颜色 |
| 动态选项 | 后端字典 API | 业务运营可能新增 | 科室、职称、设备类型、随访类型 |
| UI 配置 | 各组件内部 | 纯前端行为,无后端对应 | 快捷操作按钮、Tab 标签文案 |
### 4.2 静态映射收敛
统一以下映射组到 `constants/health.ts`,消除所有重复定义:
| 导出名 | 当前分散位置 | 统一后 |
|--------|------------|--------|
| `SEVERITY_CONFIG` | AlertDashboard, AlertList, AlertRuleList, ActionInbox, DoctorDashboard (5处) | 1 处 |
| `GENDER_OPTIONS` | constants/health.ts, PatientDetail, PatientTagManage, PatientSelect (4处) | 1 处 |
| `DEVICE_TYPE_OPTIONS` + `DEVICE_TYPE_COLOR` | DeviceManage, DeviceReadingsTab, AlertRuleList (3处) | 1 处 |
| `ALERT_STATUS_CONFIG` | AlertDashboard, AlertList, StatusTag (3处) | 1 处 |
| `APPOINTMENT_STATUS_CONFIG` | AppointmentList (1处但含复杂流转) | 1 处 |
| `CONSULTATION_STATUS_CONFIG` | ConsultationList (1处) | 1 处 |
| `BLOOD_TYPE_OPTIONS` | constants/health.ts (1处) | 保持 |
| `STATUS_OPTIONS` (患者状态) | constants/health.ts (1处) | 保持 |
每个映射组的统一格式:
```typescript
export const SEVERITY_CONFIG: Record<string, { label: string; color: string; bg: string }> = {
critical: { label: '危急', color: '#DC2626', bg: '#FEF2F2' },
high: { label: '高', color: '#D97706', bg: '#FFFBEB' },
medium: { label: '中', color: '#2563EB', bg: '#EFF6FF' },
low: { label: '低', color: '#6B7280', bg: '#F9FAFB' },
};
```
### 4.3 动态选项字典化
新增以下字典编码到 `erp-config` 的字典系统(后端已有 `GET /config/dictionaries/items?code=xxx` API前端 `apps/web/src/api/dictionaries.ts` 已封装 `listItemsByCode`
| 字典编码 | 用途 | 种子数据来源 | 影响文件 |
|----------|------|-------------|---------|
| `health_department` | 科室列表 | DoctorList 现有 DEPARTMENT_OPTIONS | DoctorList, DoctorSchedule |
| `health_title` | 医护职称 | DoctorList 现有 TITLE_OPTIONS | DoctorList |
| `health_device_type` | 设备类型 | DeviceManage 现有 DEVICE_TYPE_OPTIONS | DeviceManage, DeviceReadingsTab, AlertRuleList |
| `health_follow_up_type` | 随访类型 | FollowUpTaskList 现有 FOLLOW_UP_TYPE_OPTIONS | FollowUpTaskList |
| `health_consultation_type` | 咨询类型 | ConsultationList 现有 CONSULTATION_TYPE_OPTIONS | ConsultationList |
| `health_relationship` | 家庭关系 | FamilyMembersTab 现有 RELATIONSHIP_OPTIONS | FamilyMembersTab, family-add(MP) |
前端新增 `useDictionary(code)` hook 封装字典获取 + 缓存逻辑:
```typescript
export function useDictionary(code: string) {
const [items, setItems] = useState<DictionaryItem[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
listItemsByCode(code)
.then(setItems)
.catch(() => setItems([]))
.finally(() => setLoading(false));
}, [code]);
return { items, loading };
}
```
### 4.4 小程序联动
小程序对应的硬编码同步改为字典 API
| 文件 | 当前硬编码 | 改造 |
|------|-----------|------|
| `pages/pkg-health/input/index.tsx` INDICATORS | 6 个体征指标 | 从字典获取 + 缓存 |
| `pages/health/index.tsx` VITAL_TABS | 4 个体征 tab | 从字典获取 |
| `pages/pkg-profile/family-add/index.tsx` RELATION_OPTIONS | 3 个关系选项 | 从字典获取 |
| `pages/pkg-profile/family-add/index.tsx` GENDER_OPTIONS | 3 个性别选项 | 保留为静态映射(不在字典化范围) |
| `pages/mall/index.tsx` PRODUCT_TYPE_TABS | 商品类型 tab | 从字典获取 |
小程序端新增 `useDict(code)` hook + Taro.storage 本地缓存24h TTL离线时使用缓存或内置默认值。
---
## 5. 轨道 3医疗阈值配置
### 5.1 复用现有 `critical_value_threshold` 表
系统已有完整的危急值阈值基础设施,**无需新建表**
- **表**: `critical_value_threshold`(迁移 `m20260426_000060`
- **Entity**: `crates/erp-health/src/entity/critical_value_threshold.rs`
- **Handler**: `crates/erp-health/src/handler/critical_value_threshold_handler.rs`
- **Service**: `crates/erp-health/src/service/critical_value_threshold_service.rs`
- **路由**: `/health/critical-value-thresholds`CRUD 已注册,见 `module.rs` 行 602-609
- **权限**: `health.critical-value-thresholds.list` + `health.critical-value-thresholds.manage`(已定义)
- **种子数据**: 8 条默认记录已存在
现有表字段:`indicator``direction`(高/低)、`threshold_value``level`(警告/危急)、`department``age_min``age_max``is_active` + 标准审计字段。
### 5.2 需补充的工作
| 工作项 | 说明 |
|--------|------|
| 补充 warning 级别种子数据 | 现有种子仅有 critical 级别,需新增 warning 级别阈值(血压 140/90、心率 100/60、血糖 6.1/7.8 |
| 新增患者端只读 API | `GET /health/critical-value-thresholds/public` — 认证即可,无需管理权限,返回当前租户所有 `is_active` 阈值 |
| Web 端阈值管理 UI | 在告警规则管理页面增加"阈值配置"Tab复用现有 CRUD API |
### 5.3 前端改造
**小程序改造流程:**
1. App 启动时调用 `GET /health/critical-value-thresholds/public` 获取全量阈值
2. 存入 `Taro.storage`key: `health_thresholds`TTL: 24h
3. 体征页 `health/index.tsx` 从缓存读取阈值替代 `REF_RANGES` 和判断逻辑
4. 输入页 `pkg-health/input/index.tsx` 从缓存读取阈值替代 `WARN_THRESHOLDS`
5. 缓存未命中时使用内置默认值(与当前硬编码值一致)
### 5.4 补充种子数据warning 级别)
新增迁移脚本插入 warning 级别阈值(现有 critical 级别已由 `m20260426_000060` 种子覆盖):
| indicator | level | direction | threshold_value | 说明 |
|-----------|-------|-----------|-----------------|------|
| blood_pressure_systolic | warning | high | 140 | 收缩压参考上限 |
| blood_pressure_diastolic | warning | high | 90 | 舒张压参考上限 |
| heart_rate | warning | high | 100 | 心率参考上限 |
| heart_rate | warning | low | 60 | 心率参考下限 |
| blood_sugar_fasting | warning | high | 6.1 | 空腹血糖参考上限 |
| blood_sugar_postprandial | warning | high | 7.8 | 餐后血糖参考上限 |
共 6 条 warning 级别配置,与现有 8 条 critical 级别共同构成完整的阈值体系。
---
## 6. 跨切面关注点
### 6.1 错误处理
- 所有新建 API 遵循现有 `AppError``ApiResponse` 错误链
- 前端调用失败时降级显示:统计卡片显示 "—",列表显示"暂无数据"
- 系统健康检查 API 本身失败时,前端显示"检查中..."而非假数据
### 6.2 测试策略
| 层级 | 测试内容 | 工具 |
|------|---------|------|
| 后端单元测试 | 每个 handler + service 函数 | `#[tokio::test]` |
| 后端集成测试 | 5 个新 API 的完整请求/响应 | Testcontainers |
| 前端单元测试 | `useDictionary` hook + 常量导出一致性 | vitest |
| 前端组件测试 | 工作台页面 loading/empty/data 三态渲染 | vitest + testing-library |
| E2E 测试 | 工作台页面加载无硬编码假数据 | playwright |
### 6.3 迁移与部署顺序
三条轨道互不依赖,但建议按以下顺序部署:
1. **轨道 2常量统一** — 纯前端重构,零后端改动,可立即部署
2. **轨道 3医疗阈值** — 复用现有表,仅需补充种子数据 + 新增患者端只读 API独立部署
3. **轨道 1工作台 API** — 需要新建 5 个 API最后部署
轨道 1 内部的 API 实现顺序:
1. API-5 内容统计(最简单,单表 COUNT
2. API-4 积分动态(单表 JOIN
3. API-3 模块状态(读取 AppState
4. API-2 用户活跃度(多表聚合)
5. API-1 系统健康检查(外部连接检测)
---
## 7. 验证清单
### 轨道 1 验证
- [ ] AdminDashboard 系统健康条数据来自 API不出现假数据
- [ ] AdminDashboard 用户活跃度数据来自 API
- [ ] AdminDashboard 模块状态来自 API
- [ ] OperatorWorkbench 积分动态来自 API
- [ ] OperatorWorkbench 内容矩阵来自 API
- [ ] OperatorWorkbench 待办来自 actionInbox
- [ ] 全部 5 个新 API 在 Swagger UI 可测试
- [ ] `cargo test --workspace` 全部通过
- [ ] `pnpm build` 前端构建通过
### 轨道 2 验证
- [ ] `SEVERITY_CONFIG` 仅在 `constants/health.ts` 定义一次,其余 4 处改为引用
- [ ] `GENDER_OPTIONS` 仅在 `constants/health.ts` 定义一次
- [ ] 6 个字典编码在后端种子数据中存在
- [ ] `useDictionary` hook 可正常获取字典数据
- [ ] 小程序 `useDict` hook + storage 缓存工作正常
### 轨道 3 验证
- [ ] `critical_value_threshold` 表已有 + warning 级别种子数据 6 条正确插入
- [ ] 患者端只读 API `GET /health/critical-value-thresholds/public` 可正常访问
- [ ] 小程序体征页使用 API 阈值(非硬编码)
- [ ] 小程序输入页使用 API 阈值(非硬编码)
- [ ] 离线场景下降级到内置默认值

View File

@@ -0,0 +1,297 @@
# 健康管家工作台 — 方案 B「工作流驱动」设计规格
> 日期: 2026-05-02 | 状态: 设计中(评审后修订 v2
> 原型参考: `_temp/workbench-health-manager-B.html`
> 评审记录: 发现 7 CRITICAL + 5 IMPORTANT已全部修复
## 1. 背景与目标
### 1.1 为什么要重新设计
现有工作台(方案 A/D/E/F基于「临床医生管理透析团队」的假设设计。实际用户场景已明确
- 机构已有 HIS + 专用血透系统HMS 不碰临床透析流程
- HMS 定位是**患者与机构之间的纽带**:日常健康数据采集、随访干预、积分运营、健康科普
- 工作台的第一用户是**健康管家/随访护士**,不是做透析的医生
### 1.2 目标
| 指标 | 当前 | 目标 |
|------|------|------|
| 健康管家每日任务处理效率 | 无专门工作台,散落在各页面 | 统一入口,一件一件处理 |
| 任务遗漏率 | 未知(无追踪) | ≤2%(系统强制排序 + 超时提醒) |
| AI 建议触达率 | 有 AI 洞察面板但无强制处理流程 | 任务流中自动注入100% 触达 |
| 平均任务响应时间 | 无数据 | 危急 ≤30min紧急 ≤2h普通 ≤24h |
### 1.3 范围
**分两期实施:**
| 阶段 | 任务类型 | 数据源状态 |
|------|---------|-----------|
| **Phase 1本期** | 体征告警、AI 建议待审、到期随访 | ✅ 现有 `action_inbox_service.rs` 已聚合 |
| **Phase 2后续** | 患者咨询、积分审批、体征少报提醒、干预评估 | ⏳ 需扩展数据源或新建调度 |
- **本期Phase 1**:健康管家/随访护士角色3 种任务类型,三栏工作流布局
- **不在本期**:医生角色、运营角色、方案 A/C、Phase 2 任务类型
## 2. 角色定义 — 健康管家「刘小燕」
### 2.1 人物画像
| 维度 | 描述 |
|------|------|
| 姓名 | 刘小燕 |
| 职位 | 主管护师 / 健康管家 |
| 年龄 | 32 岁 |
| 工作经验 | 8 年护理3 年健康管理 |
| 技术水平 | 熟练使用手机 AppPC 端基本操作无障碍 |
| 日均任务量 | 20-30 项(随访 8-12、体征审核 4-6、AI 建议 3-5、其他 3-5 |
### 2.2 典型工作日
```
08:00 打开工作台,按优先级处理紧急体征告警
08:30 逐个处理血压/血糖异常告警(电话随访 + 记录)
10:00 处理 AI 建议待审(审核并采纳或转给医生)
14:00 处理到期随访任务(电话/微信随访 + 记录)
16:00 回复患者咨询、处理积分兑换等(跳转对应模块)
17:00 检查今日完成情况,确认无遗漏后下班
```
### 2.3 核心痛点
1. **任务分散**随访、体征、AI 建议散落在不同菜单,频繁切换
2. **优先级不清**:不知道该先处理哪个,容易遗漏紧急告警
3. **上下文缺失**:处理一个告警时需要单独打开患者详情查看历史
4. **AI 建议利用率低**AI 面板在角落,不强制处理,容易被忽略
## 3. 信息架构 — 三栏布局
### 3.1 布局结构
**注意**:侧边栏由全局布局提供(`apps/web/src/layouts/`),工作台页面内部只有两栏(任务队列 + 详情面板)。
```
┌──────────────────────────────────────────────────────────┐
│ 全局侧边栏(已有,不动) │ 工作台页面内部 │
│ │ │
│ ... │ ┌────────────┬──────────────┐ │
│ ⚡ 工作流(高亮) │ │ 任务队列 │ 详情处理面板 │ │
│ 📋 随访管理 │ │ 340px │ flex:1 │ │
│ 🩺 体征监测 │ │ │ │ │
│ 💬 患者咨询 │ │ 头部统计 │ 患者信息栏 │ │
│ ... │ │ Tab 切换 │ 异常数据 │ │
│ │ │ 任务列表 │ AI 建议 │ │
│ │ │ │ 互动记录 │ │
│ │ │ │ 操作按钮 │ │
│ │ └────────────┴──────────────┘ │
└──────────────────────────────────────────────────────────┘
```
### 3.2 两栏职责
| 区域 | 职责 | 交互 |
|------|------|------|
| **任务队列** | 按优先级排列的待处理任务 + 统计 | 点击选中 → 右侧加载详情 |
| **详情面板** | 当前任务的完整上下文 + 操作 | 处理完成后自动跳转下一任务 |
### 3.3 全局侧边栏菜单调整
在现有全局侧边栏中,将「工作台」菜单项更名为「工作流」,点击进入方案 B 的三栏工作台。其他菜单项不变。
## 4. 组件清单
### 4.1 任务队列组件
| 组件 | 说明 | 数据源 |
|------|------|--------|
| **QueueHeader** | 队列统计(待处理/已完成) | `GET /health/action-inbox/stats`(现有) |
| **QueueTabs** | Tab 切换:待处理 / 已完成 | 前端状态 |
| **TaskItem** | 单个任务卡片:优先级圆点 + 标题 + 元信息 + 标签 + 时间 | 现有 `list_action_inbox` API |
| **TaskList** | 可滚动的任务列表容器 | Ant Design `List` + `InfiniteScroll` |
### 4.2 详情面板组件
| 组件 | 适用任务类型 | 说明 | 数据源 |
|------|-------------|------|--------|
| **PatientBar** | 全部 | 患者头像 + 姓名 + 年龄 + 科室 + 病史标签 | `GET /health/patients/:id`(现有) |
| **VitalAlertDetail** | 体征异常 | 异常数值 + 近 7 天趋势图 + 参考范围 | `GET /health/action-inbox/:source_ref/thread`(现有)+ `GET /health/vital-signs/trends`(现有) |
| **AiSuggestionCard** | AI 建议 | 风险评估 + 建议措施 + 采纳/拒绝操作 | thread API现有 |
| **FollowUpForm** | 到期随访 | 随访表单 + 上次随访摘要 | thread API现有 |
| **InteractionTimeline** | 全部 | 患者近期互动记录 | `GET /health/action-inbox/:source_ref/thread`(现有) |
| **ActionBar** | 全部 | 底部操作按钮(因任务类型不同而变化) | — |
### 4.3 任务类型与优先级Phase 1 仅 3 种)
| 优先级 | 类型 | 自动生成规则 | 目标响应时间 | 数据源 |
|--------|------|-------------|-------------|--------|
| **危急 (P0)** | 体征危急值告警 | 告警系统生成 | ≤30 分钟 | `alerts` 表 |
| **紧急 (P1)** | AI 建议待审 | AI 分析引擎生成 | ≤2 小时 | `ai_analysis` 表 |
| **普通 (P2)** | 到期随访 | 随访计划到期 | ≤24 小时 | `follow_up_task` 表 |
### 4.4 任务 ID 格式
沿用现有 `action_inbox_service.rs` 的复合 ID 格式:`"{action_type}:{uuid}"`
- 告警任务:`"alert:550e8400-..."`
- AI 建议:`"ai_suggestion:660e8400-..."`
- 随访任务:`"follow_up:770e8400-..."`
操作分发逻辑:解析 `action_type` 前缀,路由到对应的底层服务。
## 5. 数据流与排序
### 5.1 任务生命周期
```
[自动生成] → [进入队列] → [选中处理] → [执行操作] → [完成/转交]
↑ │
└── 超时未处理 → 升级优先级 ←──────────────┘
```
### 5.2 现有 API 复用策略
不新建 API 路径。**改造现有 `action-inbox` 系列接口**
| 现有 API | 改造内容 |
|---------|---------|
| `GET /health/action-inbox` | 增加按优先级排序(改 SQL ORDER BY |
| `GET /health/action-inbox/stats` | 增加「已完成」计数 |
| `GET /health/action-inbox/:source_ref/thread` | 增加 patient 上下文字段 |
| `POST /health/alerts/:id/acknowledge` | 沿用(完成告警任务) |
| `POST /health/ai-analysis/:id/review` | 沿用(采纳/拒绝 AI 建议) |
| `POST /health/follow-up/:id/complete` | 沿用(完成随访任务) |
详情面板的患者信息、趋势图通过**前端并行调用**现有 API 获取:
- `GET /health/patients/:id` → PatientBar
- `GET /health/vital-signs/trends?patient_id=&days=7` → VitalAlertDetail
- `GET /health/action-inbox/:source_ref/thread` → InteractionTimeline + 核心任务数据
前端用 `Promise.allSettled` 并行请求,加载中显示 Skeleton。
### 5.3 排序规则修复
现有 SQL 使用 `created_at DESC`(新的在前),需改为 `created_at ASC`早的在前FIFO与设计规格一致。优先级映射从 3 级扩展为 4 级:
```sql
ORDER BY
CASE priority_raw
WHEN 'critical' THEN 0 -- P0 新增
WHEN 'high' THEN 1
WHEN 'urgent' THEN 1
WHEN 'medium' THEN 2
ELSE 3
END,
created_at ASC -- 改为 ASC
```
## 6. 交互流程 — 处理一个任务的完整闭环
以「张伟 — 血压危急值 188/102」为例
```
1. 进入工作台
└→ 任务队列自动加载张伟的告警排在最前P0
2. 点击任务
└→ 右侧详情面板加载(并行请求患者信息 + thread + 趋势):
- 患者信息栏:张伟 / 男 / 58岁 / 血透中心 / 高血压3级
- 异常数据:收缩压 188 / 舒张压 102 / 心率 92
- 近 7 天收缩压趋势图(柱状图)
- 近期互动记录
3. 执行操作
└→ 点击「立即电话随访」
└→ 弹出电话随访表单(预设患者电话号码)
└→ 填写随访记录:确认用药 / 伴随症状 / 处理建议
└→ 提交 → 调用 POST /health/alerts/:id/acknowledge
4. 任务完成
└→ 任务从「待处理」移至「已完成」
└→ 队列自动选中下一个任务
└→ 右侧面板加载新任务的详情
5. 可选操作
└→ 「转给医生」:更改 assigned_to → 对应医生(需 migration 加字段Phase 2
└→ 「稍后处理」:本地状态,置底并标记延迟时间
```
### 6.1 操作按钮矩阵Phase 1
| 任务类型 | 主操作 | 次要操作 | 底层 API |
|---------|--------|---------|---------|
| 体征异常告警 | 记录处理结果 + 确认 | 稍后处理 | `POST /health/alerts/:id/acknowledge` |
| AI 建议待审 | 采纳 / 拒绝 | 稍后处理 | `POST /health/ai-analysis/:id/review` |
| 到期随访 | 开始随访(电话/线上/面访) | 稍后处理 | `POST /health/follow-up/:id/complete` |
### 6.2 空状态处理
- 队列为空时:显示「所有任务已处理完毕」+ 今日完成统计
- 详情面板未选中任务时:显示空态引导「从左侧选择一个任务开始处理」
- 加载中Skeleton 占位
### 6.3 前端状态管理
使用 Zustand store `useWorkbenchStore`
```typescript
interface WorkbenchState {
tasks: ActionItem[]; // 任务列表
selectedTaskId: string | null; // 当前选中任务
completedCount: number; // 今日完成数
tab: 'pending' | 'completed'; // 当前 Tab
selectTask: (id: string) => void;
completeTask: (id: string) => Promise<void>;
refreshTasks: () => Promise<void>;
}
```
实时刷新策略Phase 1 使用 30 秒轮询(`setInterval` + `refreshTasks`Phase 2 考虑升级为 SSE。
## 7. 与现有系统的关系
### 7.1 改造策略
改造现有的 `Home.tsx` 工作台页面,替换当前仪表盘布局为方案 B 的任务队列 + 详情面板两栏布局。
**不新建页面**,在现有路由 `/health` 上直接改造。侧边栏不变。
### 7.2 涉及文件
| 文件 | 改动类型 | 说明 |
|------|---------|------|
| `apps/web/src/pages/health/Home.tsx` | **重写** | 从仪表盘布局改为任务队列 + 详情面板 |
| `apps/web/src/pages/health/components/TaskQueue.tsx` | **新增** | 任务队列组件 |
| `apps/web/src/pages/health/components/TaskDetail.tsx` | **新增** | 详情面板组件 |
| `apps/web/src/pages/health/components/PatientBar.tsx` | **新增** | 患者信息栏 |
| `apps/web/src/stores/workbenchStore.ts` | **新增** | Zustand store |
| `crates/erp-health/src/service/action_inbox_service.rs` | **改造** | 修复排序规则 + 扩展优先级 |
| `crates/erp-health/src/handler/device_reading_handler.rs` | **改造** | alert acknowledge 增加处理记录 |
### 7.3 数据库变更
Phase 1 **无需新建表**。任务从现有 3 张表聚合:
- 体征告警 ← `alerts` 表(未确认的告警)
- AI 建议 ← `ai_analysis` 表(状态=pending_review
- 到期随访 ← `follow_up_task` 表(状态=pending + 到期日<=今天)
聚合逻辑沿用现有 `action_inbox_service.rs` 的 UNION ALL 方案。
### 7.4 权限
沿用现有权限码:`health.action-inbox.list` + `health.action-inbox.manage`
### 7.5 验收标准
- [ ] 工作台展示 3 种任务类型告警、AI 建议、随访),按优先级排序
- [ ] 点击任务,右侧详情面板展示完整上下文(患者信息 + 原始数据 + 趋势 + 互动记录)
- [ ] 告警任务可确认并记录处理结果
- [ ] AI 建议可采纳或拒绝
- [ ] 随访任务可完成并填写随访记录
- [ ] 任务完成后自动选中下一个
- [ ] 空状态有合理展示
- [ ] 30 秒自动刷新任务列表
- [ ] `cargo check` 通过
- [ ] 浏览器中实际操作验证通过

View File

@@ -1,8 +1,10 @@
# HMS 健康管理平台 — 全项目深度分析与多专家组头脑风暴
> 日期: 2026-05-03 | 类型: 分析报告 | 数据截止: commit 554 | 状态: 稿
> 日期: 2026-05-03 | 类型: 分析报告 | 数据截止: commit 555 | 状态: 稿
>
> **如何使用本文档:** §1-2 是全景扫描所有人应读。§3-7 是专家组深度分析按需阅读。§8-10 是行动矩阵和风险传导,执行时参考。每个专家组独立成章,可跳读。
**TL;DR:** HMS 项目整体健康度 B+。架构边界满分,安全基础扎实,文档体系完善。核心风险:前端测试 <5%、55% 事件孤立、审计整改率 8%、73 个未提交文件。建议立即止血P0→ 本周补短板P1→ 本月治本P2
**TL;DR:** HMS 项目整体健康度 B+。架构边界满分,安全基础扎实,文档体系完善。核心风险:前端测试 <5%、55% 事件孤立、审计整改率 8%、73 个未提交文件(截至文档编写时)。建议立即止血P0→ 本周补短板P1→ 本月治本P2
---
@@ -10,7 +12,7 @@
### 为什么做这次分析
HMS 项目经过 3 周密集开发554 次提交,41 份设计规格已进入功能基本完整的阶段。2026-04-30 完成的全系统审计发现 25 个问题2 CRITICAL + 3 HIGH + 8 MEDIUM + 12 LOW但审计后 3 天整改率仅 8%。此时需要一个全面的「体检」来:
HMS 项目经过 3 周密集开发555 次提交,86 份设计规格已进入功能基本完整的阶段。2026-04-30 完成的全系统审计发现 25 个问题2 CRITICAL + 3 HIGH + 8 MEDIUM + 12 LOW但审计后 3 天整改率仅 8%。此时需要一个全面的「体检」来:
1. 识别系统性风险而非单点问题
2. 从多维度(架构/安全/前端/质量/管理)交叉验证
@@ -21,10 +23,10 @@ HMS 项目经过 3 周密集开发554 次提交41 份设计规格),已
| 维度 | 覆盖范围 |
|------|---------|
| 后端架构 | 17 Rust crates / ~87k 行 / 484 源文件 |
| 前端 Web | 199 文件113 TSX + 86 TS |
| 微信小程序 | 113 文件 / 40 页面 / 5 TabBar |
| 数据库 | 103 迁移 / 77+ 表 |
| 文档体系 | 12 页 wiki + 41 specs + 38 plans + 18 discussions |
| 前端 Web | 151 文件113 TSX + 38 TS |
| 微信小程序 | 113 文件 / 38 页面 / 5 TabBar |
| 数据库 | 104 迁移 / 77+ 表 |
| 文档体系 | 12 页 wiki + 86 specs + 41 plans + 12 discussions |
### 方法论
@@ -42,14 +44,14 @@ HMS 项目经过 3 周密集开发554 次提交41 份设计规格),已
| 维度 | 数据 | 评估 |
|------|------|------|
| Rust 代码 | ~87k 行 / 17 crates / 484 源文件 | 中大型单体,模块边界清晰 |
| Web 前端 | 199 文件 (113 TSX + 86 TS) | 中等规模API 层完整 |
| 微信小程序 | 113 文件 / 40 页面 / 5 TabBar | 功能丰富,分包合理 |
| 数据库 | 103 迁移 / 77+ 表 | 高频 schema 迭代 |
| Web 前端 | 151 文件 (113 TSX + 38 TS) | 中等规模API 层完整 |
| 微信小程序 | 113 文件 / 38 页面 / 5 TabBar | 功能丰富,分包合理 |
| 数据库 | 104 迁移 / 77+ 表 | 高频 schema 迭代 |
| API 路由 | 328 (8 公开 + 320 受保护) | 全面的业务覆盖 |
| 测试 | 772 后端 + 11 前端 | 后端尚可,前端极低 |
| 事件系统 | 25 类型 / 44 发布 / 14 消费者 | 设计精良但有孤立事件 |
| 设计文档 | 41 specs / 38 plans | 文档驱动,管理有序 |
| Git 历史 | 554 次提交 | AI 辅助密集开发 |
| 设计文档 | 86 specs / 41 plans | 文档驱动,管理有序 |
| Git 历史 | 555 次提交 | AI 辅助密集开发 |
### 2.2 架构评分矩阵
@@ -62,7 +64,7 @@ HMS 项目经过 3 周密集开发554 次提交41 份设计规格),已
| 前端架构 | ⭐⭐⭐⭐☆ | API/Store/Hook 三层分离,但零 i18n、低测试覆盖 |
| 代码质量 | ⭐⭐⭐⭐☆ | 4 个 TODO、无 FIXME/HACK但存在 6 个千行文件 |
| 测试覆盖 | ⭐⭐⭐☆☆ | 后端 772 测试尚可,前端 <5%、小程序 0% |
| 文档完整性 | ⭐⭐⭐⭐☆ | 12 页 wiki + 41 specs但 wiki 数据存在不一致 |
| 文档完整性 | ⭐⭐⭐⭐☆ | 12 页 wiki + 86 specs但 wiki 数据存在不一致 |
| 运维成熟度 | ⭐⭐⭐☆☆ | Docker + 原生双轨但不同步,单分支开发,大量未提交文件 |
---
@@ -108,6 +110,8 @@ HMS 项目经过 3 周密集开发554 次提交41 份设计规格),已
**优先级:** P1 — 事件是模块间通信的核心,孤立事件 = 功能缺失
> **交叉引用:** 安全组(§4.1)指出孤立事件会导致合规声明与实际行为不一致;质量组(§6.1)指出 event.rs 本身也是千行文件,重构时需要测试保障。
## 4. 专家组 2安全与合规
**成员画像:** 安全工程师 + 医疗合规专家 + 隐私保护专家
@@ -133,6 +137,8 @@ HMS 项目经过 3 周密集开发554 次提交41 份设计规格),已
3. **P1 — AI 密钥统一:** 与 JWT 密钥一样使用 `__MUST_SET_VIA_ENV__` 占位符模式
4. **P2 — 审计日志写入:** fire-and-forget 模式可能导致静默丢失,改为带重试队列
> **交叉引用:** 质量组(§6.3)指出 `unwrap()` 调用中 PluginHost::db panic 是同一类问题 — 关键路径缺乏容错。
### 4.2 医疗数据合规性
**已有能力:**
@@ -171,7 +177,9 @@ HMS 项目经过 3 周密集开发554 次提交41 份设计规格),已
- Web13 个 Playwright spec患者旅程、预约、随访、体征、告警等核心流程
- 小程序4 个 automator spec商城、健康查看、积分、体征录入
**核心判断:** 前端测试覆盖率 <5% 是项目最大的质量风险。后端有 772 个测试保障,但前端 163 个文件几乎裸跑。E2E 测试慢且脆弱,不能替代单元测试 — 任何重构都可能引入不可检测的回归。
**核心判断:** 前端测试覆盖率 <5% 是项目最大的质量风险。后端有 772 个测试保障,但前端 151 个文件几乎裸跑。E2E 测试慢且脆弱,不能替代单元测试 — 任何重构都可能引入不可检测的回归。
> **交叉引用:** 4 个专家组独立得出「前端测试是最大短板」的结论 — 架构组(§3)、质量组(§6)、管理组(§7) 均标记。风险传导链(§10)说明低测试覆盖放大了所有其他重构风险。
**建议:**
1. **P1 — Store 测试:** 6 个 Zustand Store 是状态管理核心,每个至少 10 个测试
@@ -235,6 +243,8 @@ HMS 项目经过 3 周密集开发554 次提交41 份设计规格),已
**优先级:** P2 — 在下一次大改动时顺带重构
> **交叉引用:** 前端组(§5.3)独立发现 Web 侧同样存在 5 个 500+ 行大组件。架构组(§3.1)指出 module.rs 路由注册瓶颈。后端 + 前端共 11 个大文件需要拆分,是跨维度共识。
### 6.2 前端错误处理统一化
**重复模式18+ 文件):**
@@ -251,7 +261,9 @@ HMS 项目经过 3 周密集开发554 次提交41 份设计规格),已
2. 批量替换 18 个文件中的内联模式
3. 建立 ESLint 规则禁止该模式
**优先级:** P2
**优先级:** P2 — 依赖 P1 Store 测试完成后再批量重构,避免无安全网的大规模替换
> **交叉引用:** 前端组(§5.1)指出前端测试 <5%,意味着这个 18 文件重构在没有测试保障下执行 = 高风险。应先完成 #7 Store 测试。
### 6.3 TypeScript 类型安全
@@ -296,6 +308,8 @@ HMS 项目经过 3 周密集开发554 次提交41 份设计规格),已
**建议:** 立即执行分类提交 — 按功能 / 文档 / 配置分组,并清理临时文件。
> **交叉引用:** 架构组(§3.1)指出 wiki 文档未提交,质量组(§6.3)指出过时注释未更新 — 三组数据均指向「闭环工作法未执行」的系统性问题。风险传导链(§10.2)说明未提交文件是人员单点故障的放大器。
### 7.2 审计整改进度
**整改率:** 25 个审计发现中仅 1-2 个已处理(~8%3 天后)
@@ -336,53 +350,121 @@ HMS 项目经过 3 周密集开发554 次提交41 份设计规格),已
**建议:** 安排 wiki 刷新会话,逐一校对数据。优先级 P2。
> **交叉引用:** 本文档自身的数据已用代码库实际值校正(见 §1 头部注释)。但 wiki 中仍有 7 处不一致未修。架构组(§3.2)和本组均发现此问题 — 3 组独立验证,可信度高。
## 8. 优先级行动矩阵
### 🔴 P0 — 立即处理(今天)
| # | 行动 | 工作量 | 影响 |
|---|------|--------|------|
| 1 | 提交并推送 73 个未提交文件 | 30 min | 防止工作丢失 |
| 2 | 修复 C2 告警权限码拼写 | 5 min | 消除最后一个 CRITICAL |
| 3 | 确认 C1 晚间血压修复并更新 wiki | 15 min | 关闭审计 CRITICAL |
| # | 行动 | 工作量 | 完成标准 | 阻塞 |
|---|------|--------|---------|------|
| 1 | 提交并推送 73 个未提交文件 | 30 min | `git status` 干净 + `git push` 成功 | 所有后续工作 |
| 2 | 修复 C2 告警权限码拼写 | 5 min | AlertList 页面按钮正常显示 + 无 403 | — |
| 3 | 确认 C1 晚间血压修复并更新 wiki | 15 min | 小程序录入晚间血压 → 数据库有记录 + wiki 症状导航已更新 | — |
### 🟠 P1 — 本周内
| # | 行动 | 工作量 | 影响 |
|---|------|--------|------|
| 4 | 生产限流 fail-close 配置 | 1h | 生产安全 |
| 5 | 补全 erp-health/event.rs 测试 | 4h | 事件系统可靠性 |
| 6 | 孤立事件清理14 个) | 8h | 消除功能断裂风险 |
| 7 | 前端 Store 单元测试6 个 store | 8h | 前端回归安全网 |
| 8 | 小程序透析模块(审计 HIGH H1 | 16h | 医疗完整性 |
| 9 | 小程序知情同意模块(审计 HIGH H2 | 8h | 合规底线 |
| 10 | health_data_service / action_inbox 补充 tracing | 4h | 可观测性 |
| # | 行动 | 工作量 | 完成 standard | 依赖 |
|---|------|--------|-------------|------|
| 4 | 生产限流 fail-close 配置 | 1h | `config/production.toml``fail_close = true` + 集成测试验证 | #1 |
| 5 | 补全 erp-health/event.rs 测试 | 4h | 每个消费者至少 1 个正向 + 1 个异常测试 | — |
| 6 | 孤立事件清理14 个) | 8h | 每个事件有消费者或已删除,孤立率 <10% | #5 |
| 7 | 前端 Store 单元测试6 个 store | 8h | 每个 store ≥10 测试,覆盖核心 action 和 selector | #1 |
| 8 | 小程序透析模块(审计 HIGH H1 | 16h | 透析记录查看/新增/编辑页面可用 + 4 个 automator spec | #1 |
| 9 | 小程序知情同意模块(审计 HIGH H2 | 8h | 知情同意签署页面 + 签署后事件正常发布 | #1 |
| 10 | health_data_service / action_inbox 补充 tracing | 4h | 两个文件的关键路径均有 `tracing::info/error` | — |
### 🟡 P2 — 本月内
| # | 行动 | 工作量 | 影响 |
|---|------|--------|------|
| 11 | 前端错误处理统一化18 个文件) | 4h | 代码一致性 |
| 12 | 大文件拆分(后端 6 个千行文件) | 16h | 可维护性 |
| 13 | 前端 API 契约测试 | 8h | 集成可靠性 |
| 14 | Wiki 数据一致性刷新 | 4h | 文档可信度 |
| 15 | 数据库迁移治理策略 | 8h | 运维安全 |
| 16 | unwrap() 调用替换2 处) | 2h | 生产稳定性 |
| # | 行动 | 工作量 | 完成 standard | 依赖 |
|---|------|--------|-------------|------|
| 11 | 前端错误处理统一化18 个文件) | 4h | 0 处内联错误提取,全部用 `handleApiError` | #7 |
| 12 | 大文件拆分(后端 6 个千行文件) | 16h | 无 >1000 行的 service 文件 | — |
| 13 | 前端 API 契约测试 | 8h | 每个 API 模块 ≥3 测试URL/Method/参数) | #7 |
| 14 | Wiki 数据一致性刷新 | 4h | wiki 中所有计数与代码库一致 | — |
| 15 | 数据库迁移治理策略 | 8h | 新迁移 <500 行 + 有 dry-run 脚本 | — |
| 16 | unwrap() 调用替换2 处) | 2h | `grep -r "unwrap()" service/` 返回 0 结果 | — |
### 🟢 P3 — 季度规划
| # | 行动 | 工作量 | 影响 |
|---|------|--------|------|
| 17 | 编译器警告清理40 个) | 8h | 代码清洁度 |
| 18 | 前端大组件拆分20 个文件) | 16h | 可维护性 |
| 19 | TypeScript any 消除 | 8h | 类型安全 |
| 20 | 小程序测试基础设施 | 16h | 小程序质量保障 |
| 21 | Docker 配置与文档对齐 | 4h | 部署一致性 |
| 22 | 数据保留策略设计 | 16h | 合规准备 |
| # | 行动 | 工作量 | 完成 standard | 依赖 |
|---|------|--------|-------------|------|
| 17 | 编译器警告清理40 个) | 8h | `cargo check 2>&1 | grep warning` 返回 0 | — |
| 18 | 前端大组件拆分20 个文件) | 16h | 无 >500 行的 TSX 文件 | #12 |
| 19 | TypeScript any 消除 | 8h | `grep -r ": any"` Web 返回 0 / 小程序 <5 | — |
| 20 | 小程序测试基础设施 | 16h | BLE + secure-storage + 核心页面单元测试 | #8 |
| 21 | Docker 配置与文档对齐 | 4h | compose 版本号 = wiki 声明 | #14 |
| 22 | 数据保留策略设计 | 16h | 设计规格文档 + 至少 1 个实体的自动化过期 | — |
---
## 9. 总结与建议
## 9. 交叉验证:多组共识与分歧
> 5 个专家组独立分析同一代码库。本节记录各组发现的交叉印证和矛盾,增强结论可信度。
### 9.1 多组共识≥3 组独立发现同一问题)
| 共识问题 | 涉及专家组 | 核心引用 |
|----------|-----------|---------|
| **前端测试空白是最大风险** | 架构(§3) + 前端(§5.1) + 质量(§6) + 管理(§7) | 前端 151 文件仅 11 测试(<5%4 组独立得出"不可接受"结论 |
| **大文件/大组件是可维护性瓶颈** | 架构(§3.1) + 前端(§5.3) + 质量(§6.1) | 后端 6 个千行文件 + 前端 5 个 500+ 行组件3 组独立标记 |
| **Wiki 数据过时** | 架构(§3.2) + 质量(§6.3) + 管理(§7.3) | 迁移数/权限码/entity 计数多处不一致3 组各自发现不同不一致 |
| **审计整改执行纪律缺失** | 管理(§7.2) + 安全(§4.1) + 质量(§6) | C2 五分钟改动 3 天未修,限流 fail-open 未配置,说明流程断裂 |
### 9.2 组间互补A 组发现问题B 组补充影响)
| 问题 | A 组发现 | B 组补充 |
|------|---------|---------|
| 孤立事件 | 架构组(§3.3): 55% 孤立率 | 安全组(§4.1): 事件无消费者 = 功能声明与实际不一致,合规审计时被发现 |
| 限流 fail-open | 安全组(§4.1): Redis 宕机无限流 | 架构组(§3.1): 单体架构下 Redis 是 SPOF影响全局 |
| 前端错误处理重复 | 质量组(§6.2): 18 文件重复模式 | 前端组(§5.1): 无测试覆盖 = 重构时无法验证不破坏 |
| unwrap() 调用 | 质量组(§6.3): 2 处生产风险 | 安全组(§4.1): PluginHost::db panic = 整个插件子系统不可用 |
| 小程序页面空白 | 前端组(§5): 功能缺失 | 安全组(§4.2): 知情同意缺失 = 医疗合规底线问题 |
### 9.3 无重大分歧
5 组之间无结论性矛盾 — 所有发现方向一致,差异仅在优先级判断上:
- i18n前端组判 P4安全组未提及。共识不阻塞
- 编译器警告:质量组判 P3架构组未提及。共识可延后
---
## 10. 风险传导链
> 风险不是孤立的。以下展示风险如何通过依赖关系互相放大,解释为什么某些 P0 看似小改动却必须立即处理。
### 10.1 核心传导路径
```
未提交文件积压 (#1)
├─→ 本地故障 = 工作丢失 → 阻塞所有后续工作
└─→ 审计整改延迟 → C2 五分钟改动被搁置 3 天
孤立事件 55% (#6)
├─→ 功能声明 ≠ 实际行为 → 用户不可见的业务断裂
└─→ 无消费者测试 → 重构 event.rs 时无法验证 (#5)
前端测试 <5% (#7)
├─→ 错误处理统一化 (#11) 无法验证 → 技术债固化
├─→ 大组件拆分 (#18) 风险极高 → 不敢重构
└─→ API 契约测试 (#13) 无回归安全网 → 新功能也可能破坏旧功能
限流 fail-open (#4)
└─→ Redis 宕机 → 无限流 → 恶意请求打垮服务 → 影响所有用户
```
### 10.2 风险放大效应
| 放大器 | 被放大的风险 | 机制 |
|--------|------------|------|
| 低测试覆盖 | 重构风险 | 任何代码变更都无法验证不引入回归 |
| 孤立事件 | 功能验证盲区 | 发布的事件无消费者 = 变更的影响无法观测 |
| 未提交文件 | 人员单点故障 | 唯一开发者本地故障 = 全部工作丢失 |
| Wiki 过时 | 决策误导 | 基于过时 wiki 的新决策可能建立在错误前提上 |
---
## 11. 总结与建议
### 整体评分B+(良好,有明确的提升空间)
@@ -390,7 +472,7 @@ HMS 项目经过 3 周密集开发554 次提交41 份设计规格),已
1. **架构设计出色** — 分层清晰、模块边界严格、事件驱动解耦、零循环依赖
2. **安全基础扎实** — Argon2 + AES-256-GCM + PostgreSQL RLS + 审计哈希链
3. **文档驱动开发**41 份设计规格、18 份讨论记录、12 页 wiki决策有据可查
3. **文档驱动开发**86 份设计规格、12 份讨论记录、12 页 wiki决策有据可查
4. **API 层完整** — 328 路由覆盖所有业务,前端 API 层与后端 1:1 对应
5. **前端架构清晰** — API / Store / Hook 三层分离,无 god store、无硬编码数据、无 console.log 残留
@@ -398,10 +480,26 @@ HMS 项目经过 3 周密集开发554 次提交41 份设计规格),已
1. **前端测试空白** — <5% 覆盖率,任何重构都是冒险
2. **审计整改缓慢** — 3 天仅处理 8% 的审计发现CRITICAL C25 分钟改动)仍未修复
3. **工作积压**55+ 文件未提交,违反闭环工作法
3. **工作积压**73 个文件未提交,违反闭环工作法
4. **孤立事件** — 55% 的事件无消费者,可能是功能断裂的信号
5. **可观测性不足** — 多个核心 service 文件仍缺 tracing 日志patient_service 已补全)
### 成功度量
每个优先级阶段的验收标准:
| 阶段 | 时间窗口 | 核心指标 | 目标 |
|------|---------|---------|------|
| P0 止血 | 今天 | CRITICAL 项关闭率 | 100%2/2 |
| P0 止血 | 今天 | 未提交文件数 | 0 |
| P1 补短板 | 本周 | 审计 HIGH 关闭率 | ≥66%2/3 |
| P1 补短板 | 本周 | 前端 Store 测试数 | ≥60 |
| P1 补短板 | 本周 | 事件孤立率 | <10% |
| P2 治本 | 本月 | 千行 service 文件 | 0 |
| P2 治本 | 本月 | Wiki 数据准确率 | 100% |
| P3 持续 | 季度 | 前端测试覆盖率 | >30% |
| P3 持续 | 季度 | 编译器警告数 | 0 |
### 下一步方向
当前阶段应遵循 **「止血 → 补短板 → 治本」** 路径:

View File

@@ -0,0 +1,82 @@
# HMS 患者端(微信小程序)— 功能思维导图
> 版本: v1.0 | 更新日期: 2026-04-29
> 说明: 可直接导入 XMind / 幻灯片 / 幕布等工具生成图形化思维导图。
---
## 导图结构
```
HMS 患者端(微信小程序)
├── 首页概览
│ ├── 健康数据概览卡片(血压/体重/最近透析记录)
│ ├── 功能快捷入口(数据上报、我的医生、在线咨询、积分商城)
│ ├── 每日健康打卡
│ └── 待办提醒(即将到期检查/治疗/随访)
├── 健康数据
│ ├── 日常监测上报
│ │ ├── 血压记录(早/晚)
│ │ ├── 体重记录
│ │ ├── 血糖记录
│ │ ├── 饮水量记录
│ │ ├── 尿量记录
│ │ └── 趋势图表展示
│ ├── 化验报告
│ │ ├── 化验单拍照上传
│ │ ├── 化验指标手动录入
│ │ ├── AI 自动识别指标
│ │ └── 历史报告对比
│ ├── 设备数据同步
│ │ ├── 蓝牙设备自动采集
│ │ ├── 血压计/血糖仪/体重秤
│ │ └── 数据实时同步至平台
│ └── 透析记录
│ ├── 透析日期与时长
│ ├── 透析前后血压/体重
│ ├── 干体重记录
│ └── 不适症状记录
├── 预约服务
│ ├── 透析预约
│ │ ├── 选择日期时段
│ │ ├── 自动推荐可用时间
│ │ └── 预约状态跟踪
│ ├── 复查/抽血预约
│ │ ├── 检查项目选择
│ │ └── 预约流程引导
│ └── 智能提醒
│ ├── 透析时间提醒
│ ├── 服药提醒
│ ├── 测量血压/体重提醒
│ └── 复查到期提醒
├── 在线咨询
│ ├── 选择医生
│ ├── 图文/语音沟通
│ ├── 上传报告附件
│ ├── 历史对话归档
│ └── 留言功能(医生离线时)
├── 积分商城
│ ├── 商品分类浏览
│ ├── 积分兑换
│ ├── 订单管理
│ └── 配送追踪
├── 健康资讯
│ ├── 科普文章阅读
│ ├── 公告通知查看
│ ├── 按疾病类型分类
│ └── 阅读量统计
├── AI 智能报告
│ ├── 趋势分析图表
│ ├── 异常指标标红
│ ├── 健康风险预警
│ └── 定期健康报告生成
└── 个人中心
├── 个人信息 & 实名认证
├── 多就诊人管理(家属代绑)
├── 健康档案查看
├── 我的医生
├── 我的报告
├── 健康打卡记录
├── 线下活动报名
└── 隐私设置 & 合规声明
```

View File

@@ -0,0 +1,62 @@
# HMS 医护端(微信小程序)— 功能思维导图
> 版本: v1.0 | 更新日期: 2026-04-29
> 说明: 可直接导入 XMind / 幻灯片 / 幕布等工具生成图形化思维导图。
---
## 导图结构
```
HMS 医护端(微信小程序)
├── 工作台
│ ├── 今日待办概览
│ ├── 待回复咨询数
│ ├── 待执行随访任务
│ ├── 异常指标预警摘要
│ └── 患者病情分布统计
├── 患者管理
│ ├── 患者列表
│ │ ├── 按透析/慢病/高危筛选
│ │ ├── 按疾病类型筛选
│ │ ├── 按治疗阶段筛选
│ │ └── 关键字搜索
│ ├── 患者标签管理
│ │ ├── 自定义标签(高钾/高磷/体重超标等)
│ │ ├── 标签分类管理
│ │ └── 批量打标签
│ └── 健康档案查看
│ ├── 病历信息
│ ├── 化验报告
│ ├── 生命体征趋势图
│ ├── 治疗效果对比
│ └── 在线状态 & 最近咨询时间
├── 咨询回复
│ ├── 未读消息提醒
│ ├── 图文/语音回复
│ ├── 预设回复模板
│ └── 科普内容发布
├── 随访管理
│ ├── 随访任务列表
│ │ ├── 指定随访内容和方式
│ │ ├── 修改随访计划
│ │ └── 任务状态管理
│ ├── 随访记录填写
│ │ ├── 详细信息记录
│ │ ├── 医生建议
│ │ └── 随访效果分析
│ └── 随访台账
│ ├── 选择时间范围导出
│ └── 数据类型选择
├── 告警处理
│ ├── 实时告警通知
│ ├── 按严重程度分级
│ ├── 告警确认与处理
│ └── 处理记录追踪
└── 报告解读
├── 化验单批量查看
├── 异常指标高亮
├── 历史结果对比
├── 医生备注标注
└── 随访状态标记
```

View File

@@ -0,0 +1,141 @@
# HMS 管理后台PC Web— 功能思维导图
> 版本: v1.0 | 更新日期: 2026-04-29
> 说明: 可直接导入 XMind / 幻灯片 / 幕布等工具生成图形化思维导图。
---
## 导图结构
```
HMS 管理后台PC Web
├── 工作台仪表盘
│ ├── 角色化首页(管理员/医生/护士/客服不同视图)
│ ├── 关键指标卡片
│ ├── 今日待办汇总
│ └── 最近操作记录
├── 患者管理
│ ├── 患者列表
│ │ ├── 多维度搜索筛选
│ │ ├── 批量操作
│ │ └── 数据导出 Excel
│ ├── 患者详情
│ │ ├── 基本信息/病历信息
│ │ ├── 健康数据趋势
│ │ ├── 预约记录
│ │ ├── 咨询记录
│ │ ├── 随访记录
│ │ └── 标签管理
│ └── 患者标签管理
│ ├── 标签 CRUD
│ └── 标签分类
├── 预约排班
│ ├── 医生排班管理
│ │ ├── 创建排班计划
│ │ ├── 轮班设置
│ │ └── 时间段管理
│ └── 预约列表
│ ├── 预约状态管理
│ ├── 并发控制(防止超额)
│ └── 自动提醒
├── 咨询管理
│ ├── 咨询会话列表
│ ├── 对话记录查看
│ ├── 医生排班关联
│ └── 对话记录导出
├── 随访管理
│ ├── 随访任务列表
│ ├── 随访模板管理
│ │ ├── 自定义字段
│ │ └── 模板 CRUD
│ ├── 随访记录查看
│ └── 随访台账导出
├── 健康数据中心
│ ├── 透析数据统计
│ │ ├── 透析次数/完成率
│ │ └── 透析效果评估
│ ├── 日常监测数据
│ │ ├── 血压/血糖/体重趋势
│ │ └── 异常值统计
│ ├── 化验数据汇总
│ │ ├── 指标异常排行
│ │ └── 上报率统计
│ └── 设备数据管理
│ ├── 设备绑定/解绑
│ └── 数据质量监控
├── 告警中心
│ ├── 告警规则配置
│ │ ├── 阈值设置
│ │ ├── 触发条件
│ │ └── 通知方式
│ ├── 告警列表
│ │ ├── 按严重程度/类型筛选
│ │ └── 告警确认/处理
│ └── 危急值管理
│ ├── 危急值阈值配置
│ ├── 危急值响应记录
│ └── 响应时效统计
├── 内容管理
│ ├── 科普文章
│ │ ├── 富文本编辑器
│ │ ├── 分类/标签管理
│ │ ├── 审核/发布/下架
│ │ └── 阅读量统计
│ ├── 公告管理
│ │ ├── 创建/发布/下架
│ │ └── 定向推送
│ └── 首页轮播配置
│ ├── 轮播图管理
│ └── 排序/定时
├── 积分商城运营
│ ├── 商品管理
│ │ ├── 商品 CRUD
│ │ ├── 分类管理
│ │ └── 上下架
│ ├── 订单管理
│ │ ├── 订单列表/状态流转
│ │ └── 发货/退款处理
│ ├── 积分规则
│ │ ├── 签到积分
│ │ ├── 行为积分
│ │ └── 积分兑换比例
│ └── 积分账户管理
│ ├── 用户积分余额
│ └── 积分明细
├── AI 智能分析
│ ├── AI 分析任务
│ │ ├── 化验单解读
│ │ ├── 趋势分析
│ │ └── 健康报告摘要
│ ├── AI 提示词管理
│ │ ├── 提示词模板 CRUD
│ │ └── 场景分类
│ └── AI 使用统计
│ ├── 调用量/成功率
│ └── 成本分析
├── 统计报表
│ ├── 患者增长分析
│ │ └── 新增/活跃/流失趋势
│ ├── 咨询量统计
│ │ └── 高峰时段/热门主题
│ ├── 随访完成率
│ │ ├── 任务完成情况
│ │ └── 改进建议
│ └── 积分商城销售数据
│ └── 兑换量/积分消耗
└── 系统管理
├── 账号权限
│ ├── 用户/角色/权限管理
│ ├── RBAC + 行级数据权限
│ └── 操作日志审计
├── 组织架构
│ └── 部门/岗位管理
├── 系统设置
│ ├── 字典管理
│ ├── 菜单配置
│ └── 消息推送配置
└── 数据安全
├── 数据备份
├── 合规声明配置
└── 隐私政策管理
```

View File

@@ -0,0 +1,53 @@
# HMS 平台技术能力 — 功能思维导图
> 版本: v1.0 | 更新日期: 2026-04-29
> 说明: 可直接导入 XMind / 幻灯片 / 幕布等工具生成图形化思维导图。
---
## 导图结构
```
HMS 平台技术能力
├── 安全合规
│ ├── JWT 身份认证Access Token 15min + Refresh Token 7天轮换
│ ├── RBAC 权限控制(角色权限 + 行级数据权限 + 按钮级控制)
│ ├── 多租户数据隔离JWT 注入 tenant_id查询自动过滤
│ ├── 操作日志审计(变更前后状态记录,哈希链防篡改)
│ ├── PII 敏感数据加密AES-256-GCM + KEK/DEK 分层密钥管理)
│ ├── 数据脱敏展示(手机号/身份证自动脱敏)
│ ├── 知情同意记录(数据处理前获取明确同意,支持撤回)
│ └── 隐私政策 & 用户协议
├── 多租户架构
│ ├── 共享数据库 + tenant_id 列隔离
│ ├── 租户级配置(独立权限/菜单/字典)
│ ├── 独立部署支持(预留 Schema 隔离能力)
│ ├── 租户自动初始化on_tenant_created 钩子)
│ └── 数据导入/导出
├── 开放集成
│ ├── OpenAPI 文档自动生成Swagger UI 可交互)
│ ├── 蓝牙设备协议对接(血压计/血糖仪/体重秤)
│ ├── 微信生态集成(登录/支付/消息推送)
│ ├── AI 能力扩展接口SSE 流式输出)
│ └── 事件驱动集成EventBus Outbox 持久化)
├── 技术选型
│ ├── 后端: Rust + Axum 0.8(内存安全 + 高性能 + 无 GC 停顿)
│ ├── 数据库: PostgreSQL 18企业级可靠性 + JSON 支持)
│ ├── ORM: SeaORM 1.1(编译期类型检查 + 迁移工具链)
│ ├── 缓存: Redis 7限流 + 热点数据缓存)
│ ├── 前端: React 19 + Ant Design 6企业后台 UI 标配)
│ └── 小程序: Taro 4.2 + React 18跨端兼容 + 微信生态集成)
├── 数据保障
│ ├── UUID v7 主键(时间排序 + 全局唯一 + 分布式友好)
│ ├── 软删除(数据不物理删除,保留审计追溯)
│ ├── 乐观锁version 字段防并发覆盖)
│ ├── 预约防超额(原子 CAS 并发控制)
│ ├── 事件持久化Outbox 模式 + Dead Letter 存储)
│ └── 幂等操作(重复请求安全)
└── 质量保障
├── 225+ 单元测试
├── 159+ 集成测试(真实 PostgreSQL
├── 多租户隔离验证(独立测试 crate
├── E2E 测试Playwright 关键用户流程)
└── 76 个数据库迁移(全部可回滚)
```

View File

@@ -0,0 +1,193 @@
# HMS 健康管理平台 — 后续演化路线图
> 版本: v1.0 | 日期: 2026-04-29 | 面向: 客户产品规划
---
## 1. 当前状态 — 已交付能力
### 1.1 交付总览
| 指标 | 数据 |
|------|------|
| 后端模块 | 9 个 Rust crate5 基础 + 3 业务 + 1 组装) |
| 业务实体 | 47 个44 健康 + 3 AI |
| 数据库迁移 | 76 个,全部可回滚 |
| 管理后台 | 29 个页面25+ 健康路由 + 4 系统) |
| 患者端小程序 | 31 个页面5 个 TabBar |
| 医护端小程序 | 9 个页面 |
| 后端测试 | 225 单元 + 159 集成 |
| API 文档 | OpenAPI 自动生成Swagger UI 可交互 |
### 1.2 已实现的核心能力
| 能力域 | 状态 | 说明 |
|--------|------|------|
| 患者管理(建档/标签/家属/医患关系) | ✅ 已完成 | 含 PII 加密和脱敏 |
| 健康数据(体征/化验/日常监测/设备同步) | ✅ 已完成 | 趋势图、小时聚合 |
| 预约排班(透析预约/医生排班) | ✅ 已完成 | 原子 CAS 并发控制 |
| 随访管理(任务/记录/模板) | ✅ 已完成 | 可自定义字段模板 |
| 咨询管理(会话/消息) | ✅ 已完成 | 图文/语音消息 |
| 告警系统(规则/危急值) | ✅ 已完成 | 阈值可配置,分级告警 |
| 内容管理(文章/分类/标签/公告) | ✅ 已完成 | 富文本编辑,审核流程 |
| 积分商城(账户/规则/商品/订单) | ✅ 已完成 | 积分兑换,签到 |
| AI 分析(化验解读/趋势/报告) | 🔄 Phase 1 MVP | SSE 流式输出 |
| 透析管理(透析记录/透析处方) | 🔄 基础完成 | 已独立拆分为 erp-dialysis |
| 微信小程序(患者端 + 医护端) | ✅ 已完成 | 微信登录,蓝牙设备同步 |
---
## 2. Phase 1加固期2-3 周)— 上线前必修
> **目标:** 补齐临床安全和合规短板,确保系统具备上线条件。
### 2.1 危急值告警闭环
| 项目 | 说明 | 工期 |
|------|------|------|
| 危急值阈值可配置化 | 硬编码阈值改为数据库配置,支持按科室/年龄段差异化设置 | 2 天 |
| 告警通知闭环 | 异常体征 → 自动通知责任医护 → 医护确认处理 → 超时升级 | 1 天 |
| 日常监测告警验证 | 确保所有体征数据(含设备上报)都经过告警检测 | 1 天 |
**为什么是 P0** 危急体征值无人响应可能导致患者安全事故,这是医疗系统的底线。
### 2.2 合规补全
| 项目 | 说明 | 工期 |
|------|------|------|
| 知情同意记录 | 患者数据处理前获取明确同意,记录同意版本和时间,支持撤回 | 3 天 |
| 审计日志补全 | 临床数据变更记录前后值,读操作(查看患者详情/化验报告)纳入审计 | 3 天 |
| 隐私政策 & 用户协议 | 小程序端展示可阅读的隐私政策和用户协议 | 1 天 |
**法规依据:** PIPL个人信息保护法第 29 条要求处理敏感个人信息须取得单独同意。医疗数据属于敏感个人信息。
### 2.3 事件可靠性
| 项目 | 说明 | 工期 |
|------|------|------|
| 事件重放机制 | 服务重启后从 Outbox 恢复未处理事件,不丢失 | 2 天 |
| 随访逾期通知 | 逾期未完成随访自动催办 + 幂等保护(不重复通知) | 1 天 |
---
## 3. Phase 2治理期2-4 周)— 架构治理与质量提升
> **目标:** 提升代码质量和系统稳定性,为规模化运营打基础。
### 3.1 架构治理
| 项目 | 说明 | 工期 |
|------|------|------|
| 积分系统独立拆分 | 从 erp-health 拆分为独立 crate降低健康模块复杂度和合规风险 | 5 天 |
| 事件订阅优化 | 消息模块改用过滤订阅,减少无效事件传递 | 1 天 |
| 统一事件消费模式 | 消除双路径问题,所有事件消费走统一入口 | 2 天 |
### 3.2 质量提升
| 项目 | 说明 | 工期 |
|------|------|------|
| 关键路径测试补全 | 多租户隔离验证、患者安全路径、预约并发测试50-80 用例) | 4 天 |
| 过敏史变更追溯 | 更新过敏史时保留历史记录,支持变更回溯 | 1 天 |
| PII 加密范围扩展 | 从仅身份证/手机号扩展到姓名、过敏史、诊断、咨询内容 | 3 天 |
### 3.3 前端工程化
| 项目 | 说明 | 工期 |
|------|------|------|
| 前端测试覆盖 | 管理后台核心页面单元测试 + E2E 测试 | 3 天 |
| 前端性能优化 | 大列表虚拟滚动、图表懒加载、路由代码分割 | 2 天 |
| 小程序体验优化 | 页面加载性能、离线缓存、弱网降级 | 2 天 |
---
## 4. Phase 3深化期4-6 周)— 专科能力与智能增强
> **目标:** 从通用健康管理深化为肾病/血透专科能力,增强 AI 智能分析。
### 4.1 血透专科能力
| 项目 | 说明 | 前置条件 |
|------|------|---------|
| 透析方案管理 | 透析处方模板、透析器选择、抗凝方案 | 客户需求确认 |
| 透析并发症追踪 | 低血压、失衡综合征等并发症记录与分析 | 透析方案完成 |
| 干体重管理 | 干体重目标设定、变化趋势、达标提醒 | 客户需求确认 |
| 透析充分性评估 | Kt/V 计算、URR 统计、达标率报表 | 客户需求确认 |
| 通路管理 | 血管通路类型、建立时间、使用评估 | 客户需求确认 |
> **建议:** 在开发前先与 3-5 家目标客户做需求调研,确认优先级后再投入开发。
### 4.2 AI 智能增强
| 项目 | 说明 | 预期效果 |
|------|------|---------|
| OCR 化验单识别 | 拍照自动提取化验指标,减少手动录入 | 上报效率提升 80% |
| 个性化健康建议 | 基于患者历史数据生成饮食/用药/运动建议 | 患者依从性提升 |
| 风险预测模型 | 基于体征趋势预测潜在风险,提前预警 | 从被动响应到主动预防 |
| 智能随访计划 | 根据患者病情自动生成随访频率和内容 | 医护工作量降低 |
### 4.3 用户体验升级
| 项目 | 说明 |
|------|------|
| 医护端工作流优化 | 智能分诊、快捷回复模板、语音转文字 |
| 数据可视化增强 | 可交互的图表、自定义时间范围、PDF 报告导出 |
| 消息推送精细化 | 按患者画像定制推送内容和频率,避免打扰 |
---
## 5. Phase 4扩展期按需— 规模化与生态建设
> **目标:** 支撑更多医疗机构接入,建立开放生态。
### 5.1 规模化运营
| 项目 | 说明 |
|------|------|
| 多机构管理后台 | 集团/连锁机构统一管理视图,跨机构数据汇总 |
| 数据分析平台 | 患者群体分析、治疗效果对比、运营效率仪表盘 |
| 数据导入工具 | 历史患者数据批量导入,支持 Excel/CSV |
| 数据备份与恢复 | 自动化备份策略、一键恢复、灾难恢复演练 |
### 5.2 开放生态
| 项目 | 说明 |
|------|------|
| HIS/EMR 集成 | 与医院信息系统对接,患者数据双向同步 |
| 设备厂商接入 | 标准化蓝牙/HTTP 协议,更多品牌设备即插即用 |
| 第三方服务集成 | 电子发票、物流跟踪、在线支付 |
| 开放 API | 合作伙伴可通过 API 接入平台能力 |
### 5.3 移动端扩展
| 项目 | 说明 |
|------|------|
| 医生端独立 App | 更强大的移动工作台,离线支持 |
| 患者端 APP可选 | 不依赖微信的独立应用,覆盖更多用户群 |
| 家属端 | 关注患者健康状况,接收告警通知 |
---
## 6. 版本里程碑总览
```
当前 ───────→ Phase 1 ───────→ Phase 2 ───────→ Phase 3 ───────→ Phase 4
v0.9 加固期 治理期 深化期 扩展期
MVP 2-3 周 2-4 周 4-6 周 按需
│ │ │ │ │
│ ├── 危急值闭环 ├── 积分拆分 ├── 血透专科 ├── 多机构管理
│ ├── 合规补全 ├── 测试补全 ├── AI 增强 ├── HIS 集成
│ └── 事件可靠性 ├── PII 扩展 └── 体验升级 └── 开放 API
│ │ │ │
▼ ▼ ▼ ▼ ▼
内测 安全达标 质量达标 功能达标 规模达标
可上线试运行 可正式运营 可专科化运营 可多机构运营
```
**关键决策点:**
- Phase 1 完成后 → 决定是否启动试运行
- Phase 2 完成后 → 决定是否正式上线
- Phase 3 启动前 → 需完成目标客户需求调研
- Phase 4 按业务增长节奏按需启动

View File

@@ -0,0 +1,356 @@
# HMS 健康管理平台 — 系统功能思维导图
> 版本: v1.0 | 更新日期: 2026-04-29
> 说明: 基于系统已实现能力整理,供客户了解平台全貌。可直接导入 XMind / 幻灯片 / 幕布等工具生成图形化思维导图。
---
## 大纲
```
HMS 健康管理平台
├── 患者端(微信小程序)
│ ├── 首页概览
│ ├── 健康数据
│ ├── 预约服务
│ ├── 在线咨询
│ ├── 积分商城
│ ├── 健康资讯
│ ├── AI 智能报告
│ └── 个人中心
├── 医护端(微信小程序)
│ ├── 工作台
│ ├── 患者管理
│ ├── 咨询回复
│ ├── 随访管理
│ ├── 告警处理
│ └── 报告解读
├── 管理后台PC Web
│ ├── 工作台仪表盘
│ ├── 患者管理
│ ├── 预约排班
│ ├── 咨询管理
│ ├── 随访管理
│ ├── 健康数据中心
│ ├── 告警中心
│ ├── 内容管理
│ ├── 积分商城运营
│ ├── AI 智能分析
│ ├── 统计报表
│ └── 系统管理
└── 平台技术能力
├── 安全合规
├── 多租户架构
└── 开放集成
```
---
## 一、患者端(微信小程序)
- 首页概览
- 健康数据概览卡片(血压/体重/最近透析记录)
- 功能快捷入口(数据上报、我的医生、在线咨询、积分商城)
- 每日健康打卡
- 待办提醒(即将到期检查/治疗/随访)
- 健康数据
- 日常监测上报
- 血压记录(早/晚)
- 体重记录
- 血糖记录
- 饮水量记录
- 尿量记录
- 趋势图表展示
- 化验报告
- 化验单拍照上传
- 化验指标手动录入
- AI 自动识别指标
- 历史报告对比
- 设备数据同步
- 蓝牙设备自动采集
- 血压计/血糖仪/体重秤
- 数据实时同步至平台
- 透析记录
- 透析日期与时长
- 透析前后血压/体重
- 干体重记录
- 不适症状记录
- 预约服务
- 透析预约
- 选择日期时段
- 自动推荐可用时间
- 预约状态跟踪
- 复查/抽血预约
- 检查项目选择
- 预约流程引导
- 智能提醒
- 透析时间提醒
- 服药提醒
- 测量血压/体重提醒
- 复查到期提醒
- 在线咨询
- 选择医生
- 图文/语音沟通
- 上传报告附件
- 历史对话归档
- 留言功能(医生离线时)
- 积分商城
- 商品分类浏览
- 积分兑换
- 订单管理
- 配送追踪
- 健康资讯
- 科普文章阅读
- 公告通知查看
- 按疾病类型分类
- 阅读量统计
- AI 智能报告
- 趋势分析图表
- 异常指标标红
- 健康风险预警
- 定期健康报告生成
- 个人中心
- 个人信息 & 实名认证
- 多就诊人管理(家属代绑)
- 健康档案查看
- 我的医生
- 我的报告
- 健康打卡记录
- 线下活动报名
- 隐私设置 & 合规声明
---
## 二、医护端(微信小程序)
- 工作台
- 今日待办概览
- 待回复咨询数
- 待执行随访任务
- 异常指标预警摘要
- 患者病情分布统计
- 患者管理
- 患者列表
- 按透析/慢病/高危筛选
- 按疾病类型筛选
- 按治疗阶段筛选
- 关键字搜索
- 患者标签管理
- 自定义标签(高钾/高磷/体重超标等)
- 标签分类管理
- 批量打标签
- 健康档案查看
- 病历信息
- 化验报告
- 生命体征趋势图
- 治疗效果对比
- 在线状态 & 最近咨询时间
- 咨询回复
- 未读消息提醒
- 图文/语音回复
- 预设回复模板
- 科普内容发布
- 随访管理
- 随访任务列表
- 指定随访内容和方式
- 修改随访计划
- 任务状态管理
- 随访记录填写
- 详细信息记录
- 医生建议
- 随访效果分析
- 随访台账
- 选择时间范围导出
- 数据类型选择
- 告警处理
- 实时告警通知
- 按严重程度分级
- 告警确认与处理
- 处理记录追踪
- 报告解读
- 化验单批量查看
- 异常指标高亮
- 历史结果对比
- 医生备注标注
- 随访状态标记
---
## 三、管理后台PC Web
- 工作台仪表盘
- 角色化首页(管理员/医生/护士/客服不同视图)
- 关键指标卡片
- 今日待办汇总
- 最近操作记录
- 患者管理
- 患者列表
- 多维度搜索筛选
- 批量操作
- 数据导出 Excel
- 患者详情
- 基本信息/病历信息
- 健康数据趋势
- 预约记录
- 咨询记录
- 随访记录
- 标签管理
- 患者标签管理
- 标签 CRUD
- 标签分类
- 预约排班
- 医生排班管理
- 创建排班计划
- 轮班设置
- 时间段管理
- 预约列表
- 预约状态管理
- 并发控制(防止超额)
- 自动提醒
- 咨询管理
- 咨询会话列表
- 对话记录查看
- 医生排班关联
- 对话记录导出
- 随访管理
- 随访任务列表
- 随访模板管理
- 自定义字段
- 模板 CRUD
- 随访记录查看
- 随访台账导出
- 健康数据中心
- 透析数据统计
- 透析次数/完成率
- 透析效果评估
- 日常监测数据
- 血压/血糖/体重趋势
- 异常值统计
- 化验数据汇总
- 指标异常排行
- 上报率统计
- 设备数据管理
- 设备绑定/解绑
- 数据质量监控
- 告警中心
- 告警规则配置
- 阈值设置
- 触发条件
- 通知方式
- 告警列表
- 按严重程度/类型筛选
- 告警确认/处理
- 危急值管理
- 危急值阈值配置
- 危急值响应记录
- 响应时效统计
- 内容管理
- 科普文章
- 富文本编辑器
- 分类/标签管理
- 审核/发布/下架
- 阅读量统计
- 公告管理
- 创建/发布/下架
- 定向推送
- 首页轮播配置
- 轮播图管理
- 排序/定时
- 积分商城运营
- 商品管理
- 商品 CRUD
- 分类管理
- 上下架
- 订单管理
- 订单列表/状态流转
- 发货/退款处理
- 积分规则
- 签到积分
- 行为积分
- 积分兑换比例
- 积分账户管理
- 用户积分余额
- 积分明细
- AI 智能分析
- AI 分析任务
- 化验单解读
- 趋势分析
- 健康报告摘要
- AI 提示词管理
- 提示词模板 CRUD
- 场景分类
- AI 使用统计
- 调用量/成功率
- 成本分析
- 统计报表
- 患者增长分析
- 新增/活跃/流失趋势
- 咨询量统计
- 高峰时段/热门主题
- 随访完成率
- 任务完成情况
- 改进建议
- 积分商城销售数据
- 兑换量/积分消耗
- 系统管理
- 账号权限
- 用户/角色/权限管理
- RBAC + 行级数据权限
- 操作日志审计
- 组织架构
- 部门/岗位管理
- 系统设置
- 字典管理
- 菜单配置
- 消息推送配置
- 数据安全
- 数据备份
- 合规声明配置
- 隐私政策管理
---
## 四、平台技术能力
- 安全合规
- JWT 身份认证
- RBAC 权限控制
- 多租户数据隔离
- 操作日志审计
- PII 敏感数据加密
- 隐私政策 & 用户协议
- 多租户架构
- 共享数据库 + 租户隔离
- 租户级配置
- 独立部署支持
- 数据导入/导出
- 开放集成
- OpenAPI 文档自动生成
- 蓝牙设备协议对接
- 微信生态集成(登录/支付/消息推送)
- AI 能力扩展接口

Binary file not shown.

View File

@@ -0,0 +1,313 @@
# HMS 健康管理平台 — 系统设计思路
> 版本: v1.0 | 日期: 2026-04-29 | 面向: 客户技术评审
---
## 1. 设计理念
### 1.1 以患者为中心,以数据为驱动
HMS 平台的设计围绕一个核心命题:**让患者的健康数据产生实际价值。**
传统健康管理系统的痛点是"数据沉睡"——患者上报了血压、血糖、化验结果但数据只是存在数据库里没有人看也没有人分析。HMS 从设计之初就要求:每一条数据都必须有明确的用途。
```
患者上报数据 ──→ 实时展示(趋势图/仪表盘)
──→ 智能告警(异常指标自动通知医护)
──→ AI 分析(趋势预测、健康报告)
──→ 随访触发(逾期未上报自动催办)
──→ 统计报表(运营决策依据)
```
### 1.2 三个设计原则
| 原则 | 含义 | 体现 |
|------|------|------|
| **安全先行** | 医疗数据是敏感个人信息,安全不是"加一层",而是架构的内建能力 | PII 字段加密存储、操作审计、知情同意、多租户隔离 |
| **渐进演进** | 不追求一步到位,但每一步都要为下一步打好基础 | 模块化单体架构,可按需拆分为微服务 |
| **开放集成** | 平台不封闭,能接入外部设备和系统 | 蓝牙设备协议、OpenAPI 文档、微信生态集成 |
---
## 2. 整体架构
### 2.1 三端覆盖
```
┌─────────────────────────────────────────────────┐
│ HMS 健康管理平台 │
├──────────────┬──────────────┬───────────────────┤
│ 患者端 │ 医护端 │ 管理后台 │
│ 微信小程序 │ 微信小程序 │ PC Web │
│ │ │ │
│ · 健康数据 │ · 患者管理 │ · 工作台仪表盘 │
│ · 预约服务 │ · 咨询回复 │ · 患者管理 │
│ · 在线咨询 │ · 随访管理 │ · 健康数据中心 │
│ · 积分商城 │ · 告警处理 │ · 内容管理 │
│ · AI 报告 │ · 报告解读 │ · 统计报表 │
│ · 健康资讯 │ │ · 系统管理 │
└──────────────┴──────────────┴───────────────────┘
│ │ │
└──────────────┼───────────────┘
┌─────────┴─────────┐
│ 统一 API 网关 │
│ /api/v1/* │
└─────────┬─────────┘
┌───────────────┼───────────────┐
│ │ │
┌────┴────┐ ┌─────┴─────┐ ┌────┴────┐
│ 基础模块 │ │ 核心业务 │ │ AI 模块 │
│ │ │ │ │ │
│ 认证权限 │ │ 健康管理 │ │ 化验解读 │
│ 工作流 │ │ 预约排班 │ │ 趋势分析 │
│ 消息中心 │ │ 随访管理 │ │ 报告摘要 │
│ 系统配置 │ │ 透析管理 │ │ │
└────┬────┘ └─────┬─────┘ └────┬────┘
│ │ │
└───────────────┼───────────────┘
┌─────────┴─────────┐
│ PostgreSQL 18 │
│ + Redis 7 │
└───────────────────┘
```
### 2.2 三层架构
| 层级 | 职责 | 特点 |
|------|------|------|
| **L1 基础层erp-core** | 错误体系、事件总线、模块 trait、共享类型 | 零业务依赖,所有模块的基础 |
| **L2 业务层** | 认证、配置、工作流、消息、健康管理、AI | 各模块独立,互不依赖,通过事件总线通信 |
| **L3 组装层erp-server** | Axum 入口,注册所有模块,统一路由 | 唯一的组装点,模块可插拔 |
这种分层的设计意图是:**每个业务模块可以独立开发、独立测试、未来按需拆分为独立服务。** 模块之间不存在直接调用关系,所有跨模块通信都通过事件总线异步完成。
---
## 3. 核心设计决策
### 3.1 模块化单体 — 兼顾开发效率与演进弹性
**问题:** SaaS 平台在早期应该选单体还是微服务?
**我们的选择:模块化单体,渐进式拆分。**
微服务的优势(独立部署、故障隔离)在早期团队规模下被运维复杂度完全抵消。模块化单体保留了单进程部署的简单性,同时通过严格的模块边界确保未来可以零成本拆分。
**具体做法:**
- 每个业务模块实现统一的 `ErpModule` trait身份、生命周期、权限、事件订阅
- 模块间零直接依赖,跨模块通信只通过事件总线
- 所有模块在 `erp-server` 唯一组装点注册
```
erp-core基础层
|
+------+-------+-------+------+-------+
| | | | | |
auth config workflow message health ai
| | | | | |
+------+-------+-------+------+-------+
|
erp-server组装层
```
**收益:**
- 新增模块只需实现 trait + 注册,无需修改其他模块
- 未来高流量模块(如健康管理)可独立拆分为微服务
- 开发阶段单进程调试,生产环境按需拆分
### 3.2 多租户架构 — 数据隔离是底线
**问题:** 多个医疗机构使用同一套系统,如何确保数据互不可见?
**我们的选择:共享数据库 + tenant_id 列过滤 + 中间件自动注入。**
```
请求进入 → JWT 中间件提取 tenant_id → 注入 TenantContext
所有数据库查询自动携带 tenant_id 过滤
```
**为什么不用独立数据库?**
- 独立数据库意味着每个新客户都要迁移 schema、管理独立的连接池运维成本指数级增长
- 共享数据库 + 严格过滤在 95% 的场景下足够安全,且运维成本线性增长
- 预留了独立 schema 部署能力,未来有需求的客户可单独隔离
**安全保障链路:**
1. 用户登录 → JWT 签发(含 tenant_id
2. 每次请求 → 中间件从 JWT 提取 tenant_id注入请求上下文
3. 所有数据查询 → 自动过滤 tenant_id应用层无法伪造
4. 所有数据写入 → 自动填充 tenant_id
### 3.3 事件驱动 — 模块解耦的核心机制
**问题:** 预约确认后需要通知医护、触发消息推送、更新统计数据,如何在模块间协调?
**我们的选择:事件总线 + Outbox 持久化。**
传统做法是直接调用:预约模块调用消息模块、统计模块……这会造成模块间网状耦合。事件驱动让每个模块只管"发出事件",不关心谁来消费。
```
预约确认 → 发布 appointment.confirmed 事件
├──→ 消息模块:发送通知给患者
├──→ 随访模块:创建透析后随访任务
└──→ 统计模块:更新预约完成率
```
**可靠性保障:**
- 所有事件持久化到 `domain_events`Outbox 模式)
- 事件处理失败自动进入 Dead Letter 存储,不丢失
- 支持事件重放,服务重启后可恢复未处理事件
### 3.4 安全合规 — 内建而非外挂
**问题:** 医疗数据涉及患者隐私,如何满足合规要求?
安全不是上线前加一层加密就够的,而是从架构层面内建的能力。
| 安全能力 | 实现方式 |
|----------|---------|
| **身份认证** | JWT + Access Token 15 分钟 + Refresh Token 7 天轮换 |
| **权限控制** | RBAC 角色权限 + 行级数据权限 + 按钮级控制 |
| **数据加密** | PII 字段身份证、手机号AES-256-GCM 加密存储 |
| **数据脱敏** | API 返回时自动脱敏(手机号 138****1234、身份证 ****1234 |
| **操作审计** | 关键操作记录变更前后状态,哈希链防篡改 |
| **输入防护** | SQL 注入防护参数化查询、XSS 防护、限流保护 |
| **知情同意** | 患者数据处理前获取明确同意,可随时撤回 |
---
## 4. 数据架构
### 4.1 实体模型44 个业务实体)
平台围绕健康管理场景建模,覆盖从患者建档到长期随访的完整数据链路。
| 业务域 | 实体数 | 核心实体 |
|--------|--------|---------|
| 患者管理 | 8 | 患者、家属、标签、医患关系、设备绑定、知情同意 |
| 医护管理 | 2 | 医生档案、排班 |
| 健康数据 | 9 | 健康档案、体征记录、体征小时聚合、日常监测、化验报告、趋势、诊断、用药记录、设备读数 |
| 透析管理 | 1 | 透析记录(独立 crate可扩展为血透专科模块 |
| 预约排班 | 1 | 预约(原子 CAS 并发控制,防止超额) |
| 随访管理 | 4 | 随访任务、随访记录、随访模板、模板字段 |
| 咨询管理 | 2 | 咨询会话、咨询消息 |
| 内容管理 | 5 | 文章、分类、标签、文章标签关联、文章修订 |
| 告警系统 | 5 | 告警、告警规则、危急值告警、危急值响应、危急值阈值 |
| 积分商城 | 6 | 积分账户、积分规则、商品、订单、积分流水、签到 |
| 线下活动 | 2 | 活动、活动报名 |
### 4.2 数据安全策略
医疗数据的安全性不是单一技术点,而是一个分层防御体系:
```
┌─────────────────────────────────────────────┐
│ 应用层 │
│ · JWT 认证 + RBAC 权限 + 行级数据范围 │
│ · API 统一入口,无直接数据库访问 │
├─────────────────────────────────────────────┤
│ 数据层 │
│ · PII 字段加密AES-256-GCM + KEK/DEK
│ · HMAC 盲索引(加密字段仍可等值查询) │
│ · 脱敏展示API 返回自动脱敏) │
│ · 软删除(数据不物理删除,保留审计追溯) │
├─────────────────────────────────────────────┤
│ 存储层 │
│ · tenant_id 列隔离 │
│ · 乐观锁version 字段防并发覆盖) │
│ · UUID v7 主键(时间排序 + 全局唯一) │
└─────────────────────────────────────────────┘
```
**加密方案说明:**
| 项目 | 方案 | 目的 |
|------|------|------|
| 加密算法 | AES-256-GCM | 认证加密,防篡改 + 保密 |
| 密钥管理 | KEK/DEK 分层 | 每租户独立 DEK主密钥定期轮换 |
| 查询支持 | HMAC-SHA256 盲索引 | 加密后仍支持手机号/身份证精确查询 |
| 脱敏规则 | 手机号 138\*\*\*\*1234、身份证 \*\*\*\*\*1234 | 展示层自动脱敏,原始数据不离开服务端 |
---
## 5. 技术选型
每一项技术选择都经过实际验证,不是纸上谈兵。
| 层次 | 选择 | 选择理由 |
|------|------|---------|
| **后端语言** | Rust | 内存安全 + 高性能,无 GC 停顿,适合医疗系统长期稳定运行 |
| **Web 框架** | Axum 0.8 | Tokio 官方维护类型安全路由tower 中间件生态 |
| **ORM** | SeaORM 1.1 | 异步、编译期类型检查、迁移工具链完善SQL 错误在编译期暴露 |
| **数据库** | PostgreSQL 18 | 企业级可靠性JSON 支持,丰富的索引类型,医疗场景的稳妥选择 |
| **缓存** | Redis 7 | 限流 token bucket + 热点数据缓存 + 会话管理 |
| **前端** | React 19 + Ant Design 6 | 企业后台 UI 标配,组件丰富,开发效率高 |
| **状态管理** | Zustand 5 | 极简 API无 boilerplate适合中等复杂度应用 |
| **小程序** | Taro 4.2 + React 18 | 跨端兼容React 开发体验一致,微信生态集成成熟 |
| **AI 集成** | SSE 流式 + 大模型 API | 化验解读、趋势分析、报告摘要,流式输出实时反馈 |
**为什么选择 Rust**
Rust 在医疗系统中的独特价值:
- **零运行时异常** — 编译期消除空指针、数组越界、数据竞争,医疗系统不能接受运行时崩溃
- **可预测的性能** — 无 GC 停顿,响应时间稳定,告警通知延迟可预测
- **长期维护性** — 编译器强制类型安全,代码重构不会静默引入 bug
- **并发安全** — 编译期保证线程安全,适合处理实时设备数据上报
---
## 6. 质量保障
### 6.1 测试金字塔
| 测试类型 | 数量 | 覆盖目标 |
|----------|------|---------|
| 单元测试 | 225+ | 每个 service 函数、验证逻辑、脱敏逻辑 |
| 集成测试 | 159+ | API 端点 → 数据库完整链路,使用真实 PostgreSQL |
| 多租户测试 | 独立测试 crate | 数据隔离验证,确保跨租户不可见 |
| E2E 测试 | 5 套 | 前端关键用户流程Playwright |
### 6.2 验证机制
每个功能交付前必须通过:
- `cargo check` — 全 workspace 编译无错误
- `cargo test --workspace` — 所有测试通过
- 功能验证 — 启动服务,在浏览器/小程序中实际操作
- 生产构建 — `pnpm build` 前端生产构建通过
- 数据库验证 — 迁移可正向/反向执行
### 6.3 关键数据保障
| 保障项 | 机制 |
|--------|------|
| 预约防超额 | 原子 CAS 操作,并发安全 |
| 数据不丢失 | 软删除 + 事件 Outbox 持久化 + Dead Letter |
| 数据不篡改 | 乐观锁version 字段)+ 审计日志哈希链 |
| 接口一致性 | 统一 `ApiResponse<T>` 包装OpenAPI 文档自动生成 |
---
## 总结
HMS 平台的设计围绕四个关键词展开:
1. **模块化** — 每个业务模块独立自治,通过事件总线协作,可按需拆分演进
2. **安全合规** — 加密存储、操作审计、知情同意、多租户隔离,从架构内建安全能力
3. **数据驱动** — 每条数据都有明确用途:展示、告警、分析、触发、统计
4. **渐进演进** — 当前是模块化单体,未来可按模块独立拆分为微服务,无需重写
平台已完成核心功能开发,包括 44 个业务实体、25+ 管理后台页面、40 个小程序页面、225+ 单元测试。具备投入试运行的基础条件。

Binary file not shown.

View File

@@ -101,14 +101,14 @@ hms/
| 模块 | 状态 | 实体数 | 权限数 | 页面数 | 测试覆盖 |
|------|------|--------|--------|--------|---------|
| erp-auth | ✅ 完成 | 11 表 | - | 用户/角色/组织 | 38 单元 + 3 集成 |
| erp-config | ✅ 完成 | 6 表 | - | 设置/字典/菜单 | ⚠️ 0 测试 |
| erp-workflow | ✅ 完成 | 5 表 | - | 工作流管理 | ⚠️ 0 测试 |
| erp-message | ✅ 完成 | 3 表 | - | 消息中心 | ⚠️ 0 测试 |
| erp-plugin | ✅ 完成 | 4 表 | - | 插件管理/市场 | 31 单元 + 2 集成 |
| erp-health | ✅ 完成 | 44 表 | 22 | 25+ 页面 + 11 组件 | 104 单元 + 159 集成 |
| erp-ai | 🔄 Phase 1 | 3 表 | - | AI 分析/Prompt/用量 | ⚠️ 0 测试 |
| erp-dialysis | 🔄 已拆分 | - | - | - | 10 单元 |
| erp-auth | ✅ 完成 | 11 表 | 23种子数据 | 用户/角色/组织 | 41 单元 + 3 集成 |
| erp-config | ✅ 完成 | 6 表 | 18种子数据 | 设置/字典/菜单 | 78 单元 |
| erp-workflow | ✅ 完成 | 5 表 | 8种子数据 | 工作流管理 | 63 单元 + 4 集成 |
| erp-message | ✅ 完成 | 3 表 | 5种子数据 | 消息中心 | 72 单元 |
| erp-plugin | ✅ 完成 | 4 表 | 2种子数据 | 插件管理/市场 | 78 单元 + 2 集成 |
| erp-health | ✅ 完成 | 45 表 | 39 | 25+ 页面 + 11 组件 | 159 单元 + 144 集成 |
| erp-ai | 🔄 Phase 1 | 3 表 | 6 | AI 分析/Prompt/用量 | 36 单元 |
| erp-dialysis | 🔄 已拆分 | - | 5 | - | 10 单元 + 15 集成 |
### 技术选型
@@ -220,10 +220,21 @@ impl ErpModule for AuthModule {
⚠️ EventBus 内存 broadcast 需 outbox 持久化保障(已通过后台任务实现)
⚠️ 微信登录固定到 default_tenant_id — 多租户场景需设计解析策略
### 2026-04-30 审计发现
| 发现 | 严重性 | 说明 |
|------|--------|------|
| 前端权限码拼写错误 | CRITICAL | `health.alert.manage``health.alerts.manage`(缺 s告警管理按钮永远不显示 |
| 56 个基础模块权限码未通过 PermissionDescriptor 声明 | MEDIUM | auth/config/workflow/message/plugin 通过种子数据手动注册,新增易遗漏 |
| 14 个事件无业务消费者 | LOW | 发布到 EventBus 但无后端消费者SSE 推送仍有价值 |
| Health service 层运行时日志极缺 | HIGH | 26 个 service 文件仅 11 处 tracingpatient_service949 行0 处 |
| 基础模块 ErpModule trait 实现度低 | LOW | 5/8 模块使用默认值auth/config/workflow/message/plugin |
## 7. 变更记录
| 日期 | 变更 |
|------|------|
| 2026-05-01 | 审计数据更新模块状态表刷新772 测试 / 328 路由 / 50 权限码)、审计发现清单 |
| 2026-04-26 | 从 CLAUDE.md 迁移目录结构、模块开发规范§5、安全注意事项§7 |
| 2026-04-25 | 全面更新6 模块已实现状态表、预约 CAS 决策、PII 加密不变量、健康模块集成 |
| 2026-04-23 | 重构为 5 节结构,删除 erp-common 引用,精简技术选型表 |

View File

@@ -24,7 +24,7 @@ tags: [database, seaorm, migration, multi-tenant]
| 文件 | 职责 |
|------|------|
| `crates/erp-server/migration/src/lib.rs` | Migrator 注册所有迁移 |
| `crates/erp-server/migration/src/m*.rs` | 76 个迁移文件 |
| `crates/erp-server/migration/src/m*.rs` | 96 个迁移文件 |
| `crates/erp-core/src/types.rs` | BaseFields 标准字段定义 |
### 迁移命名规则
@@ -34,7 +34,7 @@ m{YYYYMMDD}_{6位序号}_{描述}.rs
例: m20260410_000001_create_tenant.rs
```
### 当前表概览(67+ 张)
### 当前表概览70+ 张)
| 模块 | 表 |
|------|-----|
@@ -145,6 +145,7 @@ m{YYYYMMDD}_{6位序号}_{描述}.rs
| 日期 | 变更 |
|------|------|
| 2026-05-01 | 更新至 96 迁移,刷新迁移文件数 |
| 2026-04-28 | 更新至 76 迁移,新增设备采集/告警/RLS/审计哈希链/盲索引/Dead Letter/透析处方等 19 个迁移 |
| 2026-04-26 | 更新至 72 迁移、67+ 表,新增积分商城/透析/诊断/内容管理/线下活动/PII 加密扩展等 22 个迁移 |
| 2026-04-25 | 更新至 50 迁移、48 表新增健康模块迁移m000042-m000050和 18 张健康业务表 |

View File

@@ -104,9 +104,36 @@ ErpModule trait → ModuleRegistry::register() →
⚠️ EventBus 当前为内存 broadcastoutbox 持久化通过后台任务实现
⚠️ 微信注册路径的 display_name 需调用 `sanitize_string()` 防止 XSS
### 2026-04-30 审计发现
**事件系统评分**
| 检查项 | 评分 | 说明 |
|--------|------|------|
| 事件定义完整性 | 95% | 25/27 常量有发布者2 个 KNOWN 未实现) |
| 消费者覆盖率 | 44% | 11/25 事件有活跃消费者 |
| Payload 一致性 | 100% | 抽样 5 个全部一致 |
| 幂等性保证 | 100% | 所有消费者使用 `is_event_processed` 检查 |
| 死信处理 | 100% | 消费失败自动进入 dead_letter_event 表 |
**错误处理评分**
| 检查项 | 评分 | 说明 |
|--------|------|------|
| 错误变体覆盖 | 95% | 8 AppError + 37 领域错误HealthError 26 + AiError 11 |
| Handler 错误传播 | 100% | 统一 `?` + IntoResponse无手动 match |
| PII 错误安全 | 100% | Internal 错误消息对外隐藏 |
| SSE 挂起风险 | 100% | Provider 不可用不会挂起,有完整清理 |
| 生产 unwrap() 安全性 | 95% | 2 处中等风险PluginHost::db + 信号量 acquire |
**审计日志**140 处调用 / 33 文件 / SHA256 哈希链 / 变更快照
**运行时日志缺口**health service 层仅 11 处 tracing26 个文件),运维排查困难
## 5. 变更记录
| 日期 | 变更 |
|------|------|
| 2026-05-01 | 审计结果更新:事件系统 44% 消费者覆盖率 / 错误处理 95% / 审计日志 140 处 / 运行时日志不足 |
| 2026-04-25 | 添加 erp-health 集成契约、健康模块事件、sanitize 模块引用 |
| 2026-04-23 | 重构为 5 节结构,更新为已完全集成状态 |

View File

@@ -73,7 +73,7 @@ crates/erp-health/
| 积分商城 | points_account, points_rule, points_product, points_order, points_transaction, points_checkin |
| 线下活动 | offline_event, offline_event_registration |
### 权限码(22 个)
### 权限码(39 个)
| 权限码 | 说明 |
|--------|------|
@@ -87,19 +87,16 @@ crates/erp-health/
| `health.articles.review` | 文章审核 |
| `health.points.list` / `health.points.manage` | 积分查看/管理 |
| `health.device-readings.list` / `health.device-readings.manage` | 设备数据查看/管理 |
| `health.devices.list` / `health.devices.manage` | 设备查看/管理 |
| `health.alerts.list` / `health.alerts.manage` | 告警查看/管理 |
| `health.alert-rules.list` / `health.alert-rules.manage` | 告警规则查看/管理 |
| 权限码 | 说明 |
|--------|------|
| `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` | 医护查看/管理 |
| `health.articles.list` / `health.articles.manage` | 文章查看/管理 |
| `health.articles.review` | 文章审核 |
| `health.critical-alerts.list` / `health.critical-alerts.manage` | 危急值告警查看/管理 |
| `health.critical-value-thresholds.list` / `health.critical-value-thresholds.manage` | 危急值阈值查看/管理 |
| `health.follow-up-templates.list` / `health.follow-up-templates.manage` | 随访模板查看/管理 |
| `health.daily-monitoring.list` / `health.daily-monitoring.manage` | 日常监测查看/管理 |
| `health.consent.list` / `health.consent.manage` | 知情同意查看/管理 |
| `health.medication-records.list` / `health.medication-records.manage` | 用药记录查看/管理 |
| `health.medication-reminders.list` / `health.medication-reminders.manage` | 用药提醒查看/管理 |
### 集成契约
@@ -236,6 +233,34 @@ draft → pending_review → published → draft (撤回)
| 健康趋势图 ECharts | P1 | 小程序已有 echarts 集成Web 端待接入 |
| 咨询导出 Excel | P2 | 后端已有 `rust_xlsxwriter` 依赖,导出端点已实现 |
### 2026-04-30 审计发现
**功能域评分**(审计报告 8 个域):
| 域 | 评分 | 关键问题 |
|----|------|---------|
| 患者管理 | 93% | 健康摘要仅 MP家庭医生管理仅 Web |
| 健康数据 | 85% | 小程序丢失晚间血压/体温/血氧字段 |
| 预约管理 | 95% | Web/MP 基本对齐 |
| 随访管理 | 88% | MP 仅列表+创建 |
| 咨询管理 | 94% | 高度对齐 |
| 内容管理 | 86% | MP 仅只读 published 文章 |
| 积分商城 | 90% | 角色分叉正常 |
| 告警系统 | 87% | 前端权限码拼写错误(`alert` vs `alerts` |
**关键审计发现**
| 发现 | 严重性 | 说明 |
|------|--------|------|
| 告警管理按钮不显示 | CRITICAL | 前端 `health.alert.manage` 缺 s应为 `health.alerts.manage` |
| 小程序晚间血压丢失 | CRITICAL | indicator_type 固定映射 `*_morning``*_evening` 从未写入 |
| 小程序透析管理缺失 | HIGH | 后端 12 路由完整,小程序 0 入口 |
| 小程序知情同意缺失 | HIGH | 后端完整,小程序 0 入口 |
| 运行时日志不足 | HIGH | 26 个 service 仅 11 处 tracing |
| 14 事件无消费者 | LOW | 发布到 EventBusSSE 推送仍有价值 |
| DTO 覆盖率 | 100% | 105 个 DTO 完整覆盖 23 个 handler |
| 调用链连通性 | 100% | Handler→Service→Entity 全部连通 |
### 全链路验证结果2026-04-25
| 链路 | API | 前端 UI | 状态 |
@@ -252,6 +277,7 @@ draft → pending_review → published → draft (撤回)
| 日期 | 变更 |
|------|------|
| 2026-05-01 | 审计数据更新:权限码 22→39、审计发现2 CRITICAL + 3 HIGH、功能域评分、DTO/调用链 100% |
| 2026-04-28 | 全面数据刷新44 实体(+10 告警/设备/随访模板/体征小时聚合等、21 handler、22 权限、25+ Web 路由、事件系统完善25 发布/6 消费者、3 个后台任务、PiiCrypto 迁移到 erp-core |
| 2026-04-26 | 全面更新34 实体(+13 积分/透析/诊断/日常监测/线下活动/危急值/知情同意、16 handler、stats 统计端点、validation 统一模块83 测试、PII 加密扩展doctor_profile/dialysis_record/lab_report/diagnosis key_version |
| 2026-04-26 | 新增内容管理article_category/article_tag/article_article_tag/article_revision 4 实体、审核状态机 |

View File

@@ -221,6 +221,19 @@ React 19.2.4 / Ant Design 6.3.5 / React Router 7.14.0 / Zustand 5.0.12 / Vite 8.
| `pages/health/components/StatusTag.test.tsx` | 状态标签渲染 |
| `utils/exprEvaluator.test.ts` | 表达式求值 |
> ⚠️ **审计发现**Web 前端 163 个文件仅 10 个测试5 单元 + 5 E2E测试覆盖率约 5%。小程序完全无测试。详见 `docs/audits/07-test-coverage.md`。
### 2026-04-30 审计发现
| 发现 | 严重性 | 说明 |
|------|--------|------|
| 告警管理按钮永远不显示 | CRITICAL | AlertList.tsx 使用 `health.alert.manage`(单数),后端声明 `health.alerts.manage`复数AuthButton 隐藏 |
| AI 分析 SSE 无 UI 入口 | MEDIUM | 4 个 SSE 端点vital-signs/lab-report/health-trend/health-summary前端未调用 |
| SSE 重连无指数退避 | MEDIUM | 依赖浏览器原生 EventSource固定 3 秒间隔 |
| 前端测试极低 | MEDIUM | 163 文件仅 10 个测试 |
| 前端路由级权限控制缺失 | LOW | 健康模块路由无前端权限守卫,依赖后端 403 |
| AuthButton 覆盖率 26% | LOW | 13/50 声明权限码有 AuthButton其余依赖 API 403 |
### 插件页面系统
插件通过 `plugin.toml` schema 声明页面,前端根据 schema 动态生成:
@@ -259,6 +272,7 @@ ws://localhost:5174/ws/* → ws://localhost:3000/* (WebSocket)
| 日期 | 变更 |
|------|------|
| 2026-05-01 | 审计发现更新CRITICAL 权限码拼写错误alert→alerts、前端测试极低、AI SSE 无入口 |
| 2026-04-28 | UI/UX 重构 Phase 5小程序端 8 项优化):首页健康资讯+空状态引导、Hub sparkline bar+打卡合并、日常监测 3 分组折叠+异常高亮、预约时段灰显、咨询消息日期分组+图片预览、医护异常横幅+搜索、趋势图骨架屏 |
| 2026-04-28 | UI/UX 重构 Phase 44 个表单 Modal→DrawerForm患者 4 分组/预约 3 分组+排班校验/随访 2 分组/积分商品 2 分组) |
| 2026-04-28 | UI/UX 重构 Phase 310 个列表页统一迁移至 PageContainer + usePaginatedData + EntityName + 共享格式化工具,移除手动 isDark 处理 |

View File

@@ -8,17 +8,22 @@
|------|-----|
| Rust crate | 18 个erp-core + 5 基础业务 + erp-health + erp-ai + erp-dialysis + erp-plugin + 5 插件 + erp-plugin-prototype |
| 数据库表 | 30 基础表 + 44 健康业务表 + 3 AI 表(已实现) |
| 数据库迁移 | 76 个 |
| 数据库迁移 | 96 个 |
| 后端路由 | 328 个8 公开 + 320 受保护) |
| 核心模块 | 5 基础 (auth/config/workflow/message/plugin) + 3 业务 (health + ai + dialysis) |
| erp-health 实体 | 44 个 Entity~21k 行 Rust |
| erp-health 实体 | 45 个 Entity~21k 行 Rust |
| erp-ai 实体 | 3 个 Entity1.7k 行 Rust |
| Web 前端 | 92 TSX + 71 TS = 163 个源文件(含 25+ 健康路由页面 + 11 健康组件) |
| 微信小程序 | Taro 4.2 + React 1840 个页面31 患者端 + 9 医护端5 个 TabBar |
| 前端单元测试 | 5 个vitest+ 5 E2E specplaywright |
| 后端测试 | 225 单元 + 159 集成(含 18 个健康模块集成测试文件 |
| 总代码量 | Rust ~77k 行459 源文件)+ Web 前端 163 文件 + 小程序 125 文件 |
| 后端测试 | 611 单元 + 153 集成 = 772 个函数97.5% 通过率 |
| 总代码量 | Rust ~77k 行462 源文件)+ Web 前端 163 文件 + 小程序 125 文件 |
| 事件系统 | 25 事件类型 / 44 发布处 / 14 消费者 |
| DTO | 105 个结构体17 文件) |
| 权限码 | 50 声明health 39 + ai 6 + dialysis 5+ 56 基础模块手动注册 |
| API 文档 | `http://localhost:3000/api/docs/openapi.json` |
| Git 提交 | 409 次 |
| 审计状态 | 2026-04-30 全系统审计完成83% 总体完成度) |
| UI/UX 重构 | Phase 1-5 完成6 共享组件 + 4 角色仪表盘 + 个人统计数据 + 表单抽屉 + 小程序优化) |
## 症状导航
@@ -45,6 +50,9 @@
| MCP token 注入后仍 401 | [[miniprogram]] MCP 联调 §6.1 | 用了生产构建 | dev 构建(`NODE_ENV=development`+ 空密钥 |
| 积分商城 Tab 页空白 | [[miniprogram]] 待优化 | 未关联患者档案 | 需增加降级 UI 引导建档 |
| MCP 批量审计页面栈溢出 | [[miniprogram]] MCP 联调 §6.6 | `navigateTo` 超 10 层 | 改用 `reLaunch` 逐页测试 |
| 告警管理按钮不显示 | [[frontend]] 权限码拼写 | AlertList.tsx | `health.alert.manage``health.alerts.manage`(缺 s |
| 小程序晚间血压丢失 | [[miniprogram]] 体征录入 | indicator_type 映射 | **已修复:** 新增 `blood_pressure_evening` 类型,录入页+日常监测页+后端+测试全覆盖 |
| AI 分析 SSE 无 UI 入口 | [[erp-health]] AI 分析 | 前端未调用 | 4 个 SSE 端点无管理界面触发 |
## 模块导航
@@ -71,7 +79,7 @@
### 基础设施
- [[infrastructure]] — 连接信息 · 环境变量 · 一键启动 (**单一真相源**)
- [[database]] — SeaORM 迁移 · 多租户表结构(76 迁移)
- [[database]] — SeaORM 迁移 · 多租户表结构(96 迁移)
- [[frontend]] — React 19 SPA · 健康管理页面25+ 路由 + 11 组件)
- [[testing]] — 验证清单 · 测试分布 · 性能基准
@@ -99,6 +107,15 @@
| UI/UX 重构设计规格 | `docs/superpowers/specs/2026-04-28-ui-ux-overhaul-design.md` |
| UI/UX 重构实施计划 | `docs/superpowers/plans/2026-04-28-ui-ux-overhaul-plan.md` |
| 实施计划(全量) | `docs/superpowers/plans/` (27 份) |
| 全系统审计报告 | `docs/audits/08-audit-report-2026-04-30.md`83% 总体完成度2 CRITICAL + 3 HIGH |
| 审计基线快照 | `docs/audits/00-baseline-snapshot.md` |
| 审计功能清单 | `docs/audits/01-feature-inventory.md`328 路由三端映射矩阵) |
| 审计后端完整性 | `docs/audits/02-backend-integrity.md`100% 调用链连通) |
| 审计事件系统 | `docs/audits/03-event-system.md`25 事件 / 14 消费者 / 100% payload 一致) |
| 审计参数配置 | `docs/audits/04-parameter-config.md`105 DTO / 50 权限码 / 数据映射缺口) |
| 审计差距模式 | `docs/audits/05-gap-patterns.md`5 种模式,透析/知情同意 MP 缺失) |
| 审计错误处理 | `docs/audits/06-error-handling.md`SSE 不挂起 / 日志 30% 覆盖) |
| 审计测试覆盖 | `docs/audits/07-test-coverage.md`772 测试 / 前端极低 / AI 无集成测试) |
| 讨论记录 | `docs/discussions/` (10 份) |
| 协作规则 | `CLAUDE.md` |
| 插件制作指南 | `.claude/skills/plugin-development/SKILL.md` |

View File

@@ -290,6 +290,35 @@ secret = "<通过环境变量 ERP__WECHAT__SECRET 设置>"
| URL 拼接构建查询参数 | P2 | `request.ts` 已支持 `buildQuery(params)` 但内部使用 |
| 生产配置 | P2 | `urlCheck`/`minified` 需区分环境 |
### 2026-04-30 审计发现
**三端对齐差距**(小程序 API 覆盖 76 个Web 235 个,后端 328 个):
| 发现 | 严重性 | 说明 |
|------|--------|------|
| ~~晚间血压数据永久丢失~~ | ~~CRITICAL~~ **已修复** | 新增 `blood_pressure_evening` indicator_type小程序录入页 + 日常监测页 + 后端 service + 集成测试均已覆盖 |
| 透析管理完全无入口 | HIGH | 后端 12 路由完整,小程序 0 个 API 调用、0 个页面 |
| 知情同意完全无入口 | HIGH | 后端完整,小程序 0 入口 |
| 体温/血氧未映射 | MEDIUM | `body_temperature`/`spo2` 无 indicator_type |
| 健康记录小程序无入口 | MEDIUM | 患者移动端无法查看健康档案 |
| 诊断记录小程序无入口 | MEDIUM | 患者移动端无法查看诊断 |
| 小程序完全无测试 | HIGH | 40 个页面全靠手工验证 |
**功能域完成度**(小程序端):
| 功能域 | 完成度 | 关键差距 |
|--------|--------|---------|
| 咨询管理 | 95% | 无导出(预期) |
| 预约管理 | 90% | 基本对齐 |
| 随访管理 | 70% | 仅列表+创建 |
| 患者管理 | 85% | 无删除(预期) |
| 健康数据 | 60% | 丢失晚间血压/体温/血氧 |
| 告警系统 | 60% | 仅查看+处理 |
| 统计仪表盘 | 30% | 仅医护端 3 个统计 |
| AI 分析 | 30% | 仅历史查看 |
| 透析管理 | 0% | 完全空白 |
| 知情同意 | 0% | 完全空白 |
### 注意事项
- `Taro.login()` 的 code 一次性使用,每次调用会返回新 code
@@ -514,6 +543,7 @@ main();
| 日期 | 变更 |
|------|------|
| 2026-05-01 | 审计发现更新CRITICAL 晚间血压丢失 / HIGH 透析+知情同意完全空白 / 功能域完成度矩阵 |
| 2026-04-28 | **全面性能优化**分包加载6 分包,主包 517KB→275KBvendors 192KB→36KBGET 请求去重+60s TTL 缓存points store 集中积分/签到状态todaySummary 60s TTL7 组件 React.memoTrendChart 双重渲染修复restoreAuth 提升 App 级别prod terser drop_consolecrypto-js 按需引入 |
| 2026-04-27 | **移除 echarts-taro3-react**:内嵌 Taro 3 + React 16 导致 webpack 模块加载失败,改为自定义 `EcCanvas` 组件 + `echarts/core` 按需引入;更新版本说明 + 历史教训 + 组件列表 |
| 2026-04-27 | **MCP 联调全面更新**§6.1 增加 dev 构建前置条件§6.4 重写为明文 token 注入法评估两种方案§6.6 补充 7 条已知限制,新增 §6.7 审计脚本说明 + §6.8 实测审计结果40/40 页面通过§5 补充 4 条审计发现 |

View File

@@ -19,20 +19,22 @@ tags: [testing, verification]
### 测试分布
| Crate | 单元测试 | 集成测试 | 覆盖评估 |
|-------|---------|---------|---------|
| erp-health (validation) | 104 | 18 文件 ~159 | 良好 |
| erp-core | 42 | - | 良好crypto/module/error/rbac/sanitize/types |
| erp-auth | 38 | 3 | 中等 |
| erp-plugin | 31 | 2 | 中等 |
| erp-dialysis | 10 | - | 中等 |
| erp-config | **0** | - | ⚠️ 缺失 |
| erp-message | **0** | - | ⚠️ 缺失 |
| erp-workflow | **0** | - | ⚠️ 缺失 |
| erp-ai | **0** | - | ⚠️ 缺失 |
| **后端总计** | **225** | **159** | |
| 前端 (vitest) | 5 | - | 健康常量/useThemeMode/useDebouncedValue/StatusTag/exprEvaluator |
| E2E (playwright) | - | 5 spec | 登录/用户管理/插件/租户隔离 |
| Crate | 单元测试 | 集成测试 | 总计 | 通过率 | 覆盖评估 |
|-------|---------|---------|------|--------|---------|
| erp-core | 74 | - | 74 | 100% | 良好crypto/module/error/rbac/sanitize/types |
| erp-auth | 41 | 3 | 44 | 100% | 中等 |
| erp-config | 78 | - | 78 | 100% | 良好 |
| erp-workflow | 63 | 4 | 67 | 100% | 良好 |
| erp-message | 72 | - | 72 | 100% | 中等(缺集成测试) |
| erp-health | 159 | 144 | 303 | 97% | 良好9 个因 blind_indexes 表失败) |
| erp-ai | 36 | - | 36 | 100% | 中等(缺集成测试) |
| erp-dialysis | 10 | 15 | 25 | 93% | 中等2 个失败待修复) |
| erp-plugin | 78 | 2 | 80 | 100% | 良好 |
| erp-server | - | 153 | 153 | 94% | 良好API 集成测试) |
| **后端总计** | **611** | **153** | **772** | **97.5%** | |
| 前端 (vitest) | 5 | - | - | - | 健康常量/useThemeMode/useDebouncedValue/StatusTag/exprEvaluator |
| E2E (playwright) | - | 5 spec | - | - | 登录/用户管理/插件/租户隔离 |
| 小程序 | 0 | 0 | 0 | N/A | ⚠️ 完全无测试 |
### 编译 + 测试
@@ -130,12 +132,23 @@ SELECT count(*) FROM patient WHERE deleted_at IS NULL; -- 患者数量
| 领域 | 当前状态 | 优先级 |
|------|---------|--------|
| erp-config 单元测试 | **0 测试** | P1 |
| erp-message 单元测试 | **0 测试** | P1 |
| erp-workflow 单元测试 | **0 测试**BPMN 解析 + Token 驱动 | P1 |
| erp-ai 单元测试 | **0 测试**Prompt 管理 + 脱敏 | P2 |
| 前端健康模块组件测试 | 仅 StatusTag + exprEvaluator | P2 |
| E2E 健康模块测试 | 无 | P2 |
| erp-ai 集成测试 | **0 测试**SSE 流 + 外部 API 无法回归) | P0 |
| 小程序测试 | **0 测试**40 个页面全靠手工验证) | P1 |
| erp-message 集成测试 | **0 测试**SSE 推送连接/重连未测试 | P1 |
| Web 前端测试 | 仅 10 个163 文件的 API/组件/路由无覆盖 | P2 |
| erp-dialysis | 93% 通过率2 个测试失败待修复) | P2 |
| blind_indexes 迁移 | 9 个测试因表缺失失败(环境配置问题) | P3 |
### 测试覆盖良好的领域
| 领域 | 测试特点 |
|------|---------|
| 患者 CRUD | 完整集成测试覆盖创建/更新/删除/列表 |
| PII 加密 | 独立测试文件验证加密/解密/盲索引/跨租户隔离 |
| 预约并发 | CAS 原子操作测试,验证乐观锁和排班满额 |
| 工作流引擎 | BPMN 解析 + Token 驱动 + 任务分配测试 |
| 权限 RBAC | 角色/权限/菜单关联测试 |
| 插件系统 | WASM 运行时 + 动态表 CRUD + 租户隔离 |
### 活跃问题
@@ -158,6 +171,7 @@ SELECT count(*) FROM patient WHERE deleted_at IS NULL; -- 患者数量
| 日期 | 变更 |
|------|------|
| 2026-05-01 | 审计数据全面刷新772 测试611 单元 + 153 集成)/ 97.5% 通过率 / 9 个因 blind_indexes 失败 / 前端仅 10 个测试 / 小程序 0 测试 |
| 2026-04-25 | 全面更新93 后端测试 + 3 前端测试、健康模块全链路验证结果、测试覆盖空白清单 |
| 2026-04-24 | 添加微信小程序验证步骤 |
| 2026-04-23 | 重构为 5 节结构,去除与 infrastructure.md 重复 |