feat: 后端架构重构 — Endpoint→Service→Repository分层 + AI确认机制 + 异步任务持久化
- 核心业务拆分为 Endpoint → Application Service → Repository 三层 - AI写入操作必须用户确认后才写库(确认卡片机制) - 报告/饮食/用药分析改为持久化任务队列(原子领取/重试/重启恢复) - 运动计划修复: 连续真实日期替代周模板 - 用药提醒去重 + 通知Outbox预留 - 认证收拢到AuthService, 管理员收拢到AdminService - AI会话加用户归属校验防串号 - 提示词调整为患者视角 - 开发假数据已关闭 - 21/21测试通过, 0警告0错误
This commit is contained in:
@@ -39,7 +39,12 @@ class _AiAnalysisPageState extends ConsumerState<AiAnalysisPage> {
|
||||
}
|
||||
|
||||
final displayType = _displayName(analysis.reportType);
|
||||
final isReviewed = reportItem?.status == 'DoctorReviewed';
|
||||
final aiStatus = reportItem?.aiStatus ?? analysis.aiStatus;
|
||||
final reviewStatus = reportItem?.reviewStatus ?? analysis.reviewStatus;
|
||||
final fileUrl = reportItem?.fileUrl ?? analysis.fileUrl;
|
||||
final isReviewed = reviewStatus == 'Reviewed';
|
||||
final isAnalyzing = aiStatus == 'Analyzing';
|
||||
final isFailed = aiStatus == 'Failed';
|
||||
|
||||
return GradientScaffold(
|
||||
appBar: AppBar(
|
||||
@@ -64,8 +69,43 @@ class _AiAnalysisPageState extends ConsumerState<AiAnalysisPage> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (fileUrl != null && fileUrl.isNotEmpty) ...[
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () => pushRoute(
|
||||
ref,
|
||||
'reportOriginal',
|
||||
params: {'url': fileUrl, 'title': displayType},
|
||||
),
|
||||
icon: const Icon(Icons.image_outlined, size: 18),
|
||||
label: const Text('查看原始报告'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: AppColors.primary,
|
||||
side: const BorderSide(color: AppColors.primaryLight),
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
if (isAnalyzing || isFailed) ...[
|
||||
_analysisStateCard(
|
||||
isFailed
|
||||
? (analysis.summary.isNotEmpty ? analysis.summary : 'AI 分析失败,请重新上传或稍后重试')
|
||||
: 'AI 正在分析报告,请稍后刷新查看。',
|
||||
isFailed: isFailed,
|
||||
onRetry: isFailed
|
||||
? () => ref.read(reportProvider.notifier).reanalyzeReport(widget.id)
|
||||
: null,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
// ── 1. 指标分析 ──
|
||||
if (analysis.indicators.isNotEmpty) ...[
|
||||
if (!isAnalyzing && !isFailed && analysis.indicators.isNotEmpty) ...[
|
||||
_sectionTitle('指标分析'),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
@@ -79,72 +119,76 @@ class _AiAnalysisPageState extends ConsumerState<AiAnalysisPage> {
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
// ── 2. 综合解读 ──
|
||||
_sectionTitle('综合解读'),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: _cardDeco(),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
analysis.summary.isNotEmpty
|
||||
? analysis.summary
|
||||
: 'AI 正在分析中,请稍后刷新查看',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
color: AppColors.textPrimary,
|
||||
height: 1.7,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.cardInner,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: const Text(
|
||||
'以上为AI预解读,具体请以医生诊断为准',
|
||||
style: TextStyle(fontSize: 13, color: AppColors.textHint),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
// ── 3. 医生审核意见 ──
|
||||
_sectionTitle('医生审核意见'),
|
||||
const SizedBox(height: 8),
|
||||
if (isReviewed)
|
||||
_doctorReviewCard(reportItem!)
|
||||
else
|
||||
if (!isAnalyzing && !isFailed) ...[
|
||||
_sectionTitle('综合解读'),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: _cardDeco(),
|
||||
child: const Row(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.hourglass_empty,
|
||||
size: 18,
|
||||
color: AppColors.textHint,
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'待审核',
|
||||
style: TextStyle(
|
||||
analysis.summary.isNotEmpty
|
||||
? analysis.summary
|
||||
: 'AI 正在分析中,请稍后刷新查看',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
color: AppColors.textSecondary,
|
||||
color: AppColors.textPrimary,
|
||||
height: 1.7,
|
||||
),
|
||||
),
|
||||
Spacer(),
|
||||
Text(
|
||||
'AI 预解读',
|
||||
style: TextStyle(fontSize: 14, color: AppColors.textHint),
|
||||
const SizedBox(height: 10),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.cardInner,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: const Text(
|
||||
'以上为AI预解读,不能替代医生诊断和治疗建议',
|
||||
style: TextStyle(fontSize: 13, color: AppColors.textHint),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
if (!isAnalyzing && !isFailed) ...[
|
||||
// ── 3. 医生审核意见 ──
|
||||
_sectionTitle('医生审核意见'),
|
||||
const SizedBox(height: 8),
|
||||
if (isReviewed)
|
||||
_doctorReviewCard(reportItem!)
|
||||
else
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: _cardDeco(),
|
||||
child: const Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.hourglass_empty,
|
||||
size: 18,
|
||||
color: AppColors.textHint,
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'待审核',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
Spacer(),
|
||||
Text(
|
||||
'AI 预解读',
|
||||
style: TextStyle(fontSize: 14, color: AppColors.textHint),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 30),
|
||||
],
|
||||
),
|
||||
@@ -159,6 +203,63 @@ class _AiAnalysisPageState extends ConsumerState<AiAnalysisPage> {
|
||||
boxShadow: AppColors.cardShadowLight,
|
||||
);
|
||||
|
||||
Widget _analysisStateCard(
|
||||
String message, {
|
||||
required bool isFailed,
|
||||
VoidCallback? onRetry,
|
||||
}) {
|
||||
final color = isFailed ? AppColors.error : AppColors.primary;
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: _cardDeco(),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(
|
||||
isFailed ? Icons.error_outline : Icons.hourglass_empty,
|
||||
color: color,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
message,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
height: 1.6,
|
||||
color: isFailed ? AppColors.error : AppColors.textPrimary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (onRetry != null) ...[
|
||||
const SizedBox(height: 14),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: onRetry,
|
||||
icon: const Icon(Icons.refresh, size: 18),
|
||||
label: const Text('重新分析'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primary,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _sectionTitle(String text) => Row(
|
||||
children: [
|
||||
Container(
|
||||
|
||||
Reference in New Issue
Block a user