fix: 全面修复 - 实时通讯、报告AI流程、健康概览、用药运动提醒
- SignalR Hub消息持久化+防重复+跨端广播 - 报告VLM+LLM真实AI流程(验证→提取→解读) - 医生审核页重构(严重程度/建议模板/综合评语) - 健康概览五合一趋势图(fl_chart+指标切换) - 用药提醒时区修复+跨午夜+过期提醒 - 运动打卡DayOfWeek映射修复+计划覆盖查询 - 体重与其他四指标同级(Abnormal检查/确认卡牌) - AI Agent支持多种类多时段批量录入 - 删除硬编码假数据(张三/假医生/假AI解读) - 随访/报告审核数据链路全部打通
This commit is contained in:
@@ -164,9 +164,13 @@ class ConsultationChatNotifier extends Notifier<ConsultationChatState> {
|
||||
: DateTime.now(),
|
||||
);
|
||||
|
||||
// 去重
|
||||
final existingIds = state.messages.map((m) => m.id).toSet();
|
||||
if (!existingIds.contains(msg.id)) {
|
||||
// 去重:按 ID + 内容+时间窗口(防止本地消息和服务器消息重复)
|
||||
final isDuplicate = state.messages.any((m) =>
|
||||
m.id == msg.id ||
|
||||
(m.senderType == msg.senderType &&
|
||||
m.content == msg.content &&
|
||||
msg.createdAt.difference(m.createdAt).inSeconds.abs() < 3));
|
||||
if (!isDuplicate) {
|
||||
state = state.copyWith(messages: [...state.messages, msg]);
|
||||
}
|
||||
});
|
||||
@@ -220,8 +224,9 @@ class ConsultationChatNotifier extends Notifier<ConsultationChatState> {
|
||||
Future<void> sendMessage(String text) async {
|
||||
if (text.trim().isEmpty || state.isSending || state.consultationId == null) return;
|
||||
|
||||
final localId = '${DateTime.now().millisecondsSinceEpoch}';
|
||||
final userMsg = ConsultationMsg(
|
||||
id: '${DateTime.now().millisecondsSinceEpoch}',
|
||||
id: localId,
|
||||
senderType: 'User',
|
||||
content: text,
|
||||
createdAt: DateTime.now(),
|
||||
@@ -234,13 +239,50 @@ class ConsultationChatNotifier extends Notifier<ConsultationChatState> {
|
||||
try {
|
||||
// 优先通过 SignalR 发送
|
||||
if (_hub != null && _hub!.state == HubConnectionState.Connected) {
|
||||
await _hub!.invoke('SendMessage',
|
||||
final result = await _hub!.invoke('SendMessage',
|
||||
args: <Object>[state.consultationId!, 'User', '', text]);
|
||||
// 用服务器返回的真实 ID 更新本地消息,防止后续轮询重复
|
||||
if (result != null) {
|
||||
final serverId = (result as Map<String, dynamic>)['id']?.toString();
|
||||
if (serverId != null && serverId != localId) {
|
||||
final updated = state.messages.map((m) {
|
||||
if (m.id == localId) {
|
||||
return ConsultationMsg(
|
||||
id: serverId,
|
||||
senderType: m.senderType,
|
||||
senderName: m.senderName,
|
||||
content: m.content,
|
||||
createdAt: m.createdAt,
|
||||
);
|
||||
}
|
||||
return m;
|
||||
}).toList();
|
||||
state = state.copyWith(messages: updated);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 回退到 HTTP
|
||||
final api = ref.read(apiClientProvider);
|
||||
await api.post('/api/consultations/${state.consultationId}/messages',
|
||||
final res = await api.post('/api/consultations/${state.consultationId}/messages',
|
||||
data: {'content': text});
|
||||
// 用服务器返回的真实 ID 更新本地消息
|
||||
final dataMap = res.data['data'] as Map<String, dynamic>?;
|
||||
final serverId = dataMap?['id']?.toString();
|
||||
if (serverId != null && serverId != localId) {
|
||||
final updated = state.messages.map((m) {
|
||||
if (m.id == localId) {
|
||||
return ConsultationMsg(
|
||||
id: serverId,
|
||||
senderType: m.senderType,
|
||||
senderName: m.senderName,
|
||||
content: m.content,
|
||||
createdAt: m.createdAt,
|
||||
);
|
||||
}
|
||||
return m;
|
||||
}).toList();
|
||||
state = state.copyWith(messages: updated);
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
// 静默失败,消息已显示在本地
|
||||
|
||||
@@ -23,6 +23,10 @@ final dietServiceProvider = Provider<DietService>((ref) {
|
||||
return DietService(ref.watch(apiClientProvider));
|
||||
});
|
||||
|
||||
final followUpServiceProvider = Provider<FollowUpService>((ref) {
|
||||
return FollowUpService(ref.watch(apiClientProvider));
|
||||
});
|
||||
|
||||
final consultationServiceProvider = Provider<ConsultationService>((ref) {
|
||||
return ConsultationService(ref.watch(apiClientProvider));
|
||||
});
|
||||
@@ -62,24 +66,24 @@ final doctorListProvider = FutureProvider<List<Map<String, dynamic>>>((ref) asyn
|
||||
|
||||
const _fallbackDoctors = [
|
||||
{
|
||||
'id': 'doc_1',
|
||||
'name': '张医生',
|
||||
'id': '468b82e2-d95a-4436-bff6-a50eecf99a66',
|
||||
'name': '张明',
|
||||
'title': '主任医师',
|
||||
'department': '心内科',
|
||||
'department': '心脏康复科',
|
||||
'introduction': '擅长冠心病、高血压术后管理,20年临床经验',
|
||||
},
|
||||
{
|
||||
'id': 'doc_2',
|
||||
'name': '李医生',
|
||||
'id': 'd4148733-b538-4398-af17-0c7592fc0c2d',
|
||||
'name': '李芳',
|
||||
'title': '副主任医师',
|
||||
'department': '内分泌科',
|
||||
'department': '营养科',
|
||||
'introduction': '擅长糖尿病、甲状腺疾病管理,15年临床经验',
|
||||
},
|
||||
{
|
||||
'id': 'doc_3',
|
||||
'name': '王医生',
|
||||
'title': '主治医师',
|
||||
'department': '营养科',
|
||||
'id': 'ef0953c9-eb63-4d03-b6d7-050a1897d4a3',
|
||||
'name': '王建国',
|
||||
'title': '主任医师',
|
||||
'department': '心血管内科',
|
||||
'introduction': '擅长术后营养指导、饮食方案制定,10年临床经验',
|
||||
},
|
||||
];
|
||||
@@ -102,10 +106,10 @@ final currentExercisePlanProvider = FutureProvider<Map<String, dynamic>?>((ref)
|
||||
'weekStartDate': '${monday.year}-${monday.month.toString().padLeft(2, '0')}-${monday.day.toString().padLeft(2, '0')}',
|
||||
'items': List.generate(7, (i) => {
|
||||
'id': 'local_$i',
|
||||
'dayOfWeek': i,
|
||||
'exerciseType': i == 2 || i == 5 ? '休息' : '散步',
|
||||
'durationMinutes': i == 2 || i == 5 ? 0 : 30,
|
||||
'isRestDay': i == 2 || i == 5,
|
||||
'dayOfWeek': i, // matches C# DayOfWeek: 0=Sun, 1=Mon, ..., 6=Sat
|
||||
'exerciseType': i == 1 || i == 6 ? '休息' : '散步',
|
||||
'durationMinutes': i == 1 || i == 6 ? 0 : 30,
|
||||
'isRestDay': i == 1 || i == 6,
|
||||
'isCompleted': false,
|
||||
}),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user