feat: 文件存储安全加固 + 认证增强 + 媒体URL保护 + provider 重构
## 后端安全加固 - 新增 UserUploadPathResolver: 用户上传文件路径安全解析, 防目录穿越 - LocalReportFileStorage: 文件存储路径安全加固 - local_account_file_cleanup: 账号删除时文件清理逻辑增强 - AuthService: 认证逻辑增强 - file_endpoints / report_endpoints: 文件访问接口安全加固 - ai_chat_endpoints / doctor_endpoints: 接口安全调整 - Program.cs: 服务注册调整 ## 前端认证与媒体 - 新增 authenticated_network_image.dart: 带认证的图片加载组件 - auth_provider: 认证状态管理大幅增强(+173) - api_client: 网络客户端增强(+124) - chat_provider: 聊天 provider 重构(+76) - omron_device_provider: 蓝牙设备 provider 增强(+53) - sse_handler: SSE 处理增强(+35) - consultation_provider / data_providers / conversation_history_provider: 调整 ## 页面调整 - remaining_pages: 健康档案/饮食记录等页面增强(+115) - home_page / chat_messages_view: 主页微调 - doctor 端多页微调(consultations/dashboard/followups/patient_detail/profile/report_detail/reports) - report_pages / settings_pages / notification_prefs_page: 微调 - device_scan_page / diet_capture_page / admin_home_page: 微调 ## 测试 - 新增 file_path_security_tests: 文件路径安全测试 - 新增 protected_media_url_test: 媒体URL保护测试 - 新增 user_session_identity_test: 用户会话身份测试 - account_deletion_tests / application_service_tests / auth_tests: 更新
This commit is contained in:
@@ -27,6 +27,7 @@ class DietRecordListPage extends ConsumerStatefulWidget {
|
||||
class _DietRecordListPageState extends ConsumerState<DietRecordListPage> {
|
||||
List<Map<String, dynamic>> _data = [];
|
||||
bool _loading = true;
|
||||
String? _loadError;
|
||||
int _trendDays = 7;
|
||||
DateTime _selectedDate = DateTime.now();
|
||||
|
||||
@@ -37,12 +38,22 @@ class _DietRecordListPageState extends ConsumerState<DietRecordListPage> {
|
||||
}
|
||||
|
||||
Future<void> _refresh() async {
|
||||
final records = await ref.read(dietServiceProvider).getRecords();
|
||||
if (mounted) {
|
||||
try {
|
||||
final records = await ref.read(dietServiceProvider).getRecords();
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_data = records;
|
||||
_loading = false;
|
||||
_loadError = null;
|
||||
});
|
||||
} catch (_) {
|
||||
if (!mounted) return;
|
||||
if (_data.isEmpty) {
|
||||
setState(() => _loadError = '网络异常或服务暂时不可用,请稍后重试');
|
||||
} else {
|
||||
AppToast.show(context, '刷新失败,已保留当前记录', type: AppToastType.error);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _loading = false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +115,19 @@ class _DietRecordListPageState extends ConsumerState<DietRecordListPage> {
|
||||
onPressed: () => popRoute(ref),
|
||||
),
|
||||
),
|
||||
body: const Center(child: CircularProgressIndicator()),
|
||||
body: _loadError == null
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: AppErrorState(
|
||||
title: '饮食记录加载失败',
|
||||
subtitle: _loadError,
|
||||
onRetry: () {
|
||||
setState(() {
|
||||
_loading = true;
|
||||
_loadError = null;
|
||||
});
|
||||
_refresh();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -390,33 +413,67 @@ class _DietRecordListPageState extends ConsumerState<DietRecordListPage> {
|
||||
|
||||
void _showEditDialog(String id, num cal) {
|
||||
final ctrl = TextEditingController(text: '$cal');
|
||||
showDialog(
|
||||
var saving = false;
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('修改热量'),
|
||||
content: TextField(
|
||||
controller: ctrl,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(labelText: '热量(千卡)'),
|
||||
barrierDismissible: !saving,
|
||||
builder: (ctx) => StatefulBuilder(
|
||||
builder: (context, setDialogState) => AlertDialog(
|
||||
title: const Text('修改热量'),
|
||||
content: TextField(
|
||||
controller: ctrl,
|
||||
enabled: !saving,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(labelText: '热量(千卡)'),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: saving ? null : () => Navigator.pop(ctx),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: saving
|
||||
? null
|
||||
: () async {
|
||||
final calories = int.tryParse(ctrl.text.trim());
|
||||
if (calories == null || calories < 0) {
|
||||
AppToast.show(
|
||||
context,
|
||||
'请输入正确的热量',
|
||||
type: AppToastType.warning,
|
||||
);
|
||||
return;
|
||||
}
|
||||
setDialogState(() => saving = true);
|
||||
try {
|
||||
await ref.read(dietServiceProvider).updateRecord(id, {
|
||||
'totalCalories': calories,
|
||||
});
|
||||
if (!ctx.mounted) return;
|
||||
Navigator.pop(ctx);
|
||||
await _refresh();
|
||||
} catch (_) {
|
||||
if (!ctx.mounted) return;
|
||||
setDialogState(() => saving = false);
|
||||
AppToast.show(
|
||||
context,
|
||||
'保存失败,请检查网络后重试',
|
||||
type: AppToastType.error,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: saving
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Text('保存'),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
Navigator.pop(ctx);
|
||||
await ref.read(dietServiceProvider).updateRecord(id, {
|
||||
'totalCalories': int.tryParse(ctrl.text) ?? 0,
|
||||
});
|
||||
_refresh();
|
||||
},
|
||||
child: const Text('保存'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
).whenComplete(ctrl.dispose);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2546,7 +2603,7 @@ class _FollowUpItem extends StatelessWidget {
|
||||
|
||||
String _formatDateTime(String? iso) {
|
||||
if (iso == null) return '';
|
||||
final dt = DateTime.tryParse(iso);
|
||||
final dt = DateTime.tryParse(iso)?.toLocal();
|
||||
if (dt == null) return iso;
|
||||
return '${dt.year}-${dt.month.toString().padLeft(2, '0')}-${dt.day.toString().padLeft(2, '0')} ${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user