feat: 后端架构重构 — Endpoint→Service→Repository分层 + AI确认机制 + 异步任务持久化
- 核心业务拆分为 Endpoint → Application Service → Repository 三层 - AI写入操作必须用户确认后才写库(确认卡片机制) - 报告/饮食/用药分析改为持久化任务队列(原子领取/重试/重启恢复) - 运动计划修复: 连续真实日期替代周模板 - 用药提醒去重 + 通知Outbox预留 - 认证收拢到AuthService, 管理员收拢到AdminService - AI会话加用户归属校验防串号 - 提示词调整为患者视角 - 开发假数据已关闭 - 21/21测试通过, 0警告0错误
This commit is contained in:
@@ -6,7 +6,6 @@ import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
import '../../../core/app_colors.dart';
|
||||
import '../../../core/app_theme.dart';
|
||||
import '../../../core/navigation_provider.dart';
|
||||
import '../../../providers/auth_provider.dart';
|
||||
import '../../../providers/chat_provider.dart';
|
||||
import '../../../providers/data_providers.dart';
|
||||
|
||||
@@ -503,21 +502,21 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
if (time.isNotEmpty) {
|
||||
final timeValue = meta['服药时间'] as String? ?? time;
|
||||
fields.add(
|
||||
_ConfirmField(label: '服药时间', value: timeValue, editable: true),
|
||||
_ConfirmField(label: '服药时间', value: timeValue),
|
||||
);
|
||||
}
|
||||
final frequency = meta['frequency'] as String? ?? '';
|
||||
if (frequency.isNotEmpty) {
|
||||
final freqValue = meta['频率'] as String? ?? _freqLabel(frequency);
|
||||
fields.add(
|
||||
_ConfirmField(label: '频率', value: freqValue, editable: true),
|
||||
_ConfirmField(label: '频率', value: freqValue),
|
||||
);
|
||||
}
|
||||
final duration = meta['duration_days'] as int?;
|
||||
if (duration != null && duration > 0) {
|
||||
final durationValue = meta['服用天数'] as String? ?? '$duration 天';
|
||||
fields.add(
|
||||
_ConfirmField(label: '服用天数', value: durationValue, editable: true),
|
||||
_ConfirmField(label: '服用天数', value: durationValue),
|
||||
);
|
||||
}
|
||||
} else if (isExercise) {
|
||||
@@ -549,18 +548,18 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
|
||||
if (recordTime.isEmpty) recordTime = _formatTime(msg.createdAt);
|
||||
if (dur.isNotEmpty) {
|
||||
fields.add(_ConfirmField(label: '运动时长', value: dur, editable: true));
|
||||
fields.add(_ConfirmField(label: '运动时长', value: dur));
|
||||
}
|
||||
if (freq.isNotEmpty) {
|
||||
fields.add(_ConfirmField(label: '频率', value: freq));
|
||||
}
|
||||
if (exDays > 0) {
|
||||
fields.add(_ConfirmField(label: '计划天数', value: '${exDays}天'));
|
||||
fields.add(_ConfirmField(label: '计划天数', value: '$exDays天'));
|
||||
}
|
||||
final intensity = (meta['intensity'] ?? meta['运动强度'] ?? '').toString();
|
||||
if (intensity.isNotEmpty) {
|
||||
fields.add(
|
||||
_ConfirmField(label: '运动强度', value: intensity, editable: true),
|
||||
_ConfirmField(label: '运动强度', value: intensity),
|
||||
);
|
||||
}
|
||||
final calories =
|
||||
@@ -568,7 +567,7 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
.toString();
|
||||
if (calories.isNotEmpty) {
|
||||
fields.add(
|
||||
_ConfirmField(label: '消耗热量', value: '$calories kcal', editable: true),
|
||||
_ConfirmField(label: '消耗热量', value: '$calories kcal'),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
@@ -582,13 +581,13 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
if (recordTime.isEmpty) recordTime = _formatTime(msg.createdAt);
|
||||
final timeValue = meta['记录时间'] as String? ?? recordTime;
|
||||
fields.add(
|
||||
_ConfirmField(label: '记录时间', value: timeValue, editable: true),
|
||||
_ConfirmField(label: '记录时间', value: timeValue),
|
||||
);
|
||||
final note = meta['note'] as String? ?? '';
|
||||
if (note.isNotEmpty) {
|
||||
final noteValue = meta['备注'] as String? ?? note;
|
||||
fields.add(
|
||||
_ConfirmField(label: '备注', value: noteValue, editable: true),
|
||||
_ConfirmField(label: '备注', value: noteValue),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -851,7 +850,7 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
horizontal: 16,
|
||||
),
|
||||
),
|
||||
_buildFieldRow(fields[i], msg, ref),
|
||||
_buildFieldRow(fields[i]),
|
||||
],
|
||||
],
|
||||
),
|
||||
@@ -907,10 +906,15 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
ref
|
||||
onTap: () async {
|
||||
final error = await ref
|
||||
.read(chatProvider.notifier)
|
||||
.confirmMessage(msg.id);
|
||||
if (error != null && context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(error)),
|
||||
);
|
||||
}
|
||||
},
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
child: Container(
|
||||
@@ -958,9 +962,7 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFieldRow(_ConfirmField field, ChatMessage msg, WidgetRef ref) {
|
||||
final isEditing = field.label == msg.metadata?['_editingField'];
|
||||
|
||||
Widget _buildFieldRow(_ConfirmField field) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Row(
|
||||
@@ -975,67 +977,20 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: !field.editable
|
||||
? Text(
|
||||
field.value.isNotEmpty ? field.value : '未设置',
|
||||
style: const TextStyle(
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
)
|
||||
: isEditing
|
||||
? _buildEditableTextField(field, msg, ref)
|
||||
: InkWell(
|
||||
onTap: () => ref
|
||||
.read(chatProvider.notifier)
|
||||
.startEditingField(msg.id, field.label),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
field.value.isNotEmpty ? field.value : '未设置',
|
||||
style: const TextStyle(
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Icons.edit_outlined,
|
||||
size: 18,
|
||||
color: AppColors.border,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
field.value.isNotEmpty ? field.value : '未设置',
|
||||
style: const TextStyle(
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEditableTextField(
|
||||
_ConfirmField field,
|
||||
ChatMessage msg,
|
||||
WidgetRef ref,
|
||||
) {
|
||||
return _EditFieldCell(
|
||||
key: ValueKey('editing_${msg.id}_${field.label}'),
|
||||
initialValue: field.value,
|
||||
onDone: (value) {
|
||||
ref
|
||||
.read(chatProvider.notifier)
|
||||
.finishEditingField(msg.id, field.label, value);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _editField(_ConfirmField field, ChatMessage msg, WidgetRef ref) {
|
||||
// 字段可点击修改的入口,后续可接入 showDialog 实现
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 3. ThinkingBubble — 思考动画
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
@@ -1243,57 +1198,6 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 公共组件:通用按钮
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
Widget _cardFilledBtn(String label, IconData icon, {VoidCallback? onTap}) {
|
||||
return ElevatedButton(
|
||||
onPressed: onTap ?? () {},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.primary,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
padding: const EdgeInsets.symmetric(vertical: 11),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(icon, size: 19),
|
||||
const SizedBox(width: 5),
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _cardOutlineBtn(String label, IconData icon, {VoidCallback? onTap}) {
|
||||
return OutlinedButton(
|
||||
onPressed: onTap ?? () {},
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: AppTheme.primary,
|
||||
side: const BorderSide(color: AppTheme.primary, width: 1.2),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
padding: const EdgeInsets.symmetric(vertical: 11),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(icon, size: 18),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 工具方法
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
@@ -1428,45 +1332,6 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
static void _medicationCheckIn(WidgetRef ref, BuildContext context) async {
|
||||
try {
|
||||
final api = ref.read(apiClientProvider);
|
||||
final reminders = await ref.read(medicationReminderProvider.future);
|
||||
if (reminders.isEmpty) {
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('暂无待服药记录'),
|
||||
backgroundColor: AppColors.warning,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
for (final m in reminders) {
|
||||
final id = m['id']?.toString() ?? '';
|
||||
final time = m['scheduledTime']?.toString() ?? '';
|
||||
if (id.isEmpty) continue;
|
||||
await api.post(
|
||||
'/api/medications/$id/confirm-dose',
|
||||
data: {'scheduledTime': time, 'status': 'taken'},
|
||||
);
|
||||
}
|
||||
ref.invalidate(medicationReminderProvider);
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('打卡成功 已记录 ${reminders.length} 项服药'),
|
||||
backgroundColor: AppTheme.success,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('打卡失败:$e'), backgroundColor: Colors.red),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static _AgentColors _agentColors(ActiveAgent agent) {
|
||||
return switch (agent) {
|
||||
ActiveAgent.consultation => _AgentColors(
|
||||
@@ -1637,8 +1502,9 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
} else if (hasAbnormal) {
|
||||
// 有异常时显示异常指标
|
||||
final abnormalParts = <String>[];
|
||||
if (bpAbnormal)
|
||||
if (bpAbnormal) {
|
||||
abnormalParts.add('血压 ${bp['systolic']}/${bp['diastolic']} 偏高');
|
||||
}
|
||||
tasks.add(
|
||||
_taskRow(
|
||||
context,
|
||||
@@ -1672,14 +1538,14 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
const exIconBg = Color(0xFFFEF3C7);
|
||||
final exercisePlan = ref.watch(currentExercisePlanProvider);
|
||||
var hasExercise = false;
|
||||
exercisePlan.whenOrNull(
|
||||
exercisePlan.when(
|
||||
data: (plan) {
|
||||
if (plan != null) {
|
||||
final items =
|
||||
(plan['items'] as List?)?.cast<Map<String, dynamic>>() ?? [];
|
||||
final today = now.weekday % 7;
|
||||
final today = '${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')}';
|
||||
final todayItem = items.cast<Map<String, dynamic>?>().firstWhere(
|
||||
(i) => i?['dayOfWeek'] == today,
|
||||
(i) => i?['scheduledDate']?.toString() == today,
|
||||
orElse: () => null,
|
||||
);
|
||||
if (todayItem != null && todayItem['isRestDay'] != true) {
|
||||
@@ -1707,6 +1573,36 @@ class ChatMessagesView extends ConsumerWidget {
|
||||
}
|
||||
}
|
||||
},
|
||||
loading: () {
|
||||
hasExercise = true;
|
||||
tasks.add(
|
||||
_taskRow(
|
||||
context,
|
||||
Icons.directions_run,
|
||||
'运动',
|
||||
trailing: '正在加载运动计划',
|
||||
status: 'pending',
|
||||
iconColor: exIconColor,
|
||||
iconBg: exIconBg,
|
||||
onTap: () => pushRoute(ref, 'exercisePlan'),
|
||||
),
|
||||
);
|
||||
},
|
||||
error: (_, _) {
|
||||
hasExercise = true;
|
||||
tasks.add(
|
||||
_taskRow(
|
||||
context,
|
||||
Icons.directions_run,
|
||||
'运动',
|
||||
trailing: '运动计划加载失败',
|
||||
status: 'overdue',
|
||||
iconColor: exIconColor,
|
||||
iconBg: exIconBg,
|
||||
onTap: () => pushRoute(ref, 'exercisePlan'),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
if (!hasExercise) {
|
||||
tasks.add(
|
||||
@@ -2057,13 +1953,9 @@ class _AgentAction {
|
||||
class _ConfirmField {
|
||||
final String label;
|
||||
final String value;
|
||||
final IconData icon;
|
||||
final bool editable;
|
||||
const _ConfirmField({
|
||||
required this.label,
|
||||
required this.value,
|
||||
this.icon = Icons.info_outline,
|
||||
this.editable = false,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2215,58 +2107,3 @@ class _ExpandableAdviceState extends State<_ExpandableAdvice> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _EditFieldCell extends StatefulWidget {
|
||||
final String initialValue;
|
||||
final ValueChanged<String> onDone;
|
||||
const _EditFieldCell({
|
||||
super.key,
|
||||
required this.initialValue,
|
||||
required this.onDone,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_EditFieldCell> createState() => _EditFieldCellState();
|
||||
}
|
||||
|
||||
class _EditFieldCellState extends State<_EditFieldCell> {
|
||||
late final TextEditingController _ctrl;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_ctrl = TextEditingController(text: widget.initialValue);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_ctrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => TextField(
|
||||
controller: _ctrl,
|
||||
autofocus: true,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF1E293B),
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(color: const Color(0xFF6366F1).withAlpha(80)),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(color: Color(0xFF6366F1), width: 1.5),
|
||||
),
|
||||
),
|
||||
onSubmitted: widget.onDone,
|
||||
onTapOutside: (_) => widget.onDone(_ctrl.text),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user