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:
MingNian
2026-07-20 10:19:01 +08:00
parent 0d4fd88ce7
commit 9cea41705e
48 changed files with 1181 additions and 212 deletions

View File

@@ -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')}';
}