feat: 后端架构重构 — Endpoint→Service→Repository分层 + AI确认机制 + 异步任务持久化

- 核心业务拆分为 Endpoint → Application Service → Repository 三层
- AI写入操作必须用户确认后才写库(确认卡片机制)
- 报告/饮食/用药分析改为持久化任务队列(原子领取/重试/重启恢复)
- 运动计划修复: 连续真实日期替代周模板
- 用药提醒去重 + 通知Outbox预留
- 认证收拢到AuthService, 管理员收拢到AdminService
- AI会话加用户归属校验防串号
- 提示词调整为患者视角
- 开发假数据已关闭
- 21/21测试通过, 0警告0错误
This commit is contained in:
MingNian
2026-06-20 20:41:42 +08:00
parent c610417e29
commit 4d213b5a44
132 changed files with 6733 additions and 2856 deletions

View File

@@ -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(