feat: 后端架构重构 — Endpoint→Service→Repository分层 + AI确认机制 + 异步任务持久化

- 核心业务拆分为 Endpoint → Application Service → Repository 三层
- AI写入操作必须用户确认后才写库(确认卡片机制)
- 报告/饮食/用药分析改为持久化任务队列(原子领取/重试/重启恢复)
- 运动计划修复: 连续真实日期替代周模板
- 用药提醒去重 + 通知Outbox预留
- 认证收拢到AuthService, 管理员收拢到AdminService
- AI会话加用户归属校验防串号
- 提示词调整为患者视角
- 开发假数据已关闭
- 21/21测试通过, 0警告0错误
This commit is contained in:
MingNian
2026-06-20 20:41:42 +08:00
parent c610417e29
commit 4d213b5a44
132 changed files with 6733 additions and 2856 deletions

View File

@@ -19,7 +19,7 @@ class DoctorConsultationsPage extends ConsumerWidget {
final list = ref.watch(_consListProvider);
return list.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (_, __) => const Center(child: Text('加载失败')),
error: (_, _) => const Center(child: Text('加载失败')),
data: (items) => items.isEmpty
? const Center(
child: Text('暂无问诊', style: TextStyle(color: AppColors.textHint)),

View File

@@ -63,7 +63,7 @@ class DoctorDashboardPage extends ConsumerWidget {
dash.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (_, __) => const Center(child: Text('加载失败')),
error: (_, _) => const Center(child: Text('加载失败')),
data: (data) => _buildContent(context, ref, data),
),
],

View File

@@ -82,7 +82,7 @@ class _DoctorFollowUpEditPageState
const SizedBox(height: 8),
pts.when(
loading: () => const LinearProgressIndicator(),
error: (_, __) => const Text('加载失败'),
error: (_, _) => const Text('加载失败'),
data: (list) => _dropdown(list),
),
const SizedBox(height: 16),

View File

@@ -45,10 +45,11 @@ class _DoctorFollowupsPageState extends ConsumerState<DoctorFollowupsPage> {
@override
Widget build(BuildContext context) {
var items = ref.watch(_fupList);
if (_filter == 'Upcoming')
if (_filter == 'Upcoming') {
items = items.where((f) => f['status'] == 'Upcoming').toList();
else if (_filter == 'Completed')
} else if (_filter == 'Completed') {
items = items.where((f) => f['status'] == 'Completed').toList();
}
return Column(
children: [

View File

@@ -35,7 +35,7 @@ class DoctorPatientDetailPage extends ConsumerWidget {
),
body: detail.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (_, __) => const Center(child: Text('加载失败')),
error: (_, _) => const Center(child: Text('加载失败')),
data: (data) => data == null
? const Center(child: Text('患者不存在'))
: _buildBody(data),
@@ -50,9 +50,6 @@ class DoctorPatientDetailPage extends ConsumerWidget {
final meds = data['medications'] as List? ?? [];
final reports = data['reports'] as List? ?? [];
final followUps = data['followUps'] as List? ?? [];
final trends = data['trendRecords'] as List? ?? [];
final diet = data['dietRecords'] as List? ?? [];
final exercise = data['exercisePlan'] as Map<String, dynamic>?;
return ListView(
padding: const EdgeInsets.all(16),
@@ -159,11 +156,12 @@ class DoctorPatientDetailPage extends ConsumerWidget {
...latest.map((r) {
final type = r['metricType']?.toString() ?? '';
String val = '';
if (type == 'BloodPressure')
if (type == 'BloodPressure') {
val =
'${r['systolic'] ?? '--'}/${r['diastolic'] ?? '--'} mmHg';
else
} else {
val = '${r['value'] ?? '--'} ${r['unit'] ?? ''}';
}
return Padding(
padding: const EdgeInsets.only(bottom: 4),
child: Row(

View File

@@ -51,10 +51,11 @@ class _DoctorPatientsPageState extends ConsumerState<DoctorPatientsPage> {
final items =
(data['items'] as List?)?.cast<Map<String, dynamic>>() ?? [];
setState(() {
if (reset)
if (reset) {
_patients = items;
else
} else {
_patients.addAll(items);
}
_total = data['total'] ?? 0;
_hasMore = _patients.length < _total;
_page++;

View File

@@ -53,7 +53,7 @@ class _DoctorProfileEditPageState extends ConsumerState<DoctorProfileEditPage> {
),
body: prof.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (_, __) => const Center(child: Text('加载失败')),
error: (_, _) => const Center(child: Text('加载失败')),
data: (d) {
if (d != null) {
_name.text = d['name'] ?? '';
@@ -111,21 +111,23 @@ class _DoctorProfileEditPageState extends ConsumerState<DoctorProfileEditPage> {
},
);
ref.invalidate(_docProfileProvider);
if (mounted)
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('保存成功'),
backgroundColor: AppColors.success,
),
);
}
} catch (_) {
if (mounted)
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('保存失败'),
backgroundColor: AppColors.error,
),
);
}
} finally {
if (mounted) setState(() => _saving = false);
}

View File

@@ -72,18 +72,20 @@ class _DoctorReportDetailPageState
);
setState(() => _submitted = true);
ref.invalidate(_reportDetailProvider(widget.id));
if (mounted)
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('审核提交成功'),
backgroundColor: AppColors.success,
),
);
}
} catch (e) {
if (mounted)
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('提交失败: $e'), backgroundColor: AppColors.error),
);
}
} finally {
if (mounted) setState(() => _submitting = false);
}
@@ -110,7 +112,7 @@ class _DoctorReportDetailPageState
),
body: detail.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (_, __) => const Center(child: Text('加载失败')),
error: (_, _) => const Center(child: Text('加载失败')),
data: (data) => data == null
? const Center(child: Text('报告不存在'))
: _buildBody(data),
@@ -253,7 +255,7 @@ class _DoctorReportDetailPageState
),
),
Text(
'${ind['value'] ?? ''}',
ind['value'] ?? '',
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
@@ -528,11 +530,11 @@ class _DoctorReportDetailPageState
),
const SizedBox(height: 12),
if (severity != null)
_InfoRow('严重程度', _severityLabels[severity] ?? severity),
_infoRow('严重程度', _severityLabels[severity] ?? severity),
if (doctorRec != null && doctorRec.isNotEmpty)
_InfoRow('建议', doctorRec),
_infoRow('建议', doctorRec),
if (doctorComment != null && doctorComment.isNotEmpty)
_InfoRow('评语', doctorComment),
_infoRow('评语', doctorComment),
],
),
),
@@ -546,8 +548,9 @@ class _DoctorReportDetailPageState
if (raw == null || raw.isEmpty) return [];
try {
final list = jsonDecode(raw);
if (list is List)
if (list is List) {
return list.map((e) => Map<String, String>.from(e as Map)).toList();
}
} catch (_) {}
return [];
}
@@ -559,7 +562,7 @@ class _DoctorReportDetailPageState
_ => AppColors.textHint,
};
Widget _InfoRow(String label, String value) => Padding(
Widget _infoRow(String label, String value) => Padding(
padding: const EdgeInsets.only(bottom: 6),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,

View File

@@ -63,7 +63,7 @@ class _DoctorReportsPageState extends ConsumerState<DoctorReportsPage> {
Expanded(
child: reports.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (_, __) => const Center(child: Text('加载失败')),
error: (_, _) => const Center(child: Text('加载失败')),
data: (items) => items.isEmpty
? const Center(
child: Text(