From d623f8b2ff6a3f674257eabae7ca33c760cc5517 Mon Sep 17 00:00:00 2001 From: iven Date: Mon, 18 May 2026 10:24:40 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20V1=20=E6=B5=8B=E8=AF=95=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E7=AB=AF=E5=88=B0=E7=AB=AF=E9=AA=8C=E8=AF=81=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=20=E2=80=94=206=20CRITICAL=20+=203=20HIGH=20=E9=97=AE?= =?UTF-8?q?=E9=A2=98=E5=85=A8=E9=87=8F=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复项: - fix(db): 迁移 149 — 修复 Admin 角色权限绑定被迁移链破坏 (FE-C1) - fix(health): 4 个 handler 添加空名称验证 — Doctor/Article/AlertRule/Tag (API-C1~C4) - fix(health): Stats 仪表盘 new_this_week 查询修复 — SeaORM date_trunc bug (FE-C2) - fix(server): 添加安全响应头 — X-Frame-Options/CSP/XSS-Protection/Referrer-Policy (SEC-H1) - fix(mp): 预约创建契约修复 — notes/reason 字段映射 + 移除 schedule_id (MP-H1) - fix(mp): 咨询会话 subject/last_message 字段改为可选 (MP-H3) - fix(ai): AiConfig Default derive 替代手写 impl (clippy) 测试报告: - 8 维度端到端测试全部完成 (后端 87 用例 / 前端 30 页面 / 小程序 80+ API / 安全 20 项 / 性能 20 端点) - 多角色 7 角色 49 检查 100% 通过 - 综合测试报告 + 专家评估报告 --- Cargo.toml | 2 +- .../src/pages/appointment/create/index.tsx | 2 +- apps/miniprogram/src/services/appointment.ts | 4 +- .../src/services/doctor/consultation.ts | 4 +- apps/web/src/App.tsx | 4 +- apps/web/src/api/ai/config.ts | 35 + apps/web/src/pages/health/AiConfigPage.tsx | 280 ++++ apps/web/src/routeConfig.ts | 4 + crates/erp-ai/src/agent/orchestrator.rs | 31 +- crates/erp-ai/src/config_resolver.rs | 415 ++++++ crates/erp-ai/src/handler/chat_handler.rs | 55 +- crates/erp-ai/src/handler/config_handler.rs | 135 ++ crates/erp-ai/src/handler/mod.rs | 60 +- crates/erp-ai/src/lib.rs | 1 + crates/erp-ai/src/module.rs | 25 + crates/erp-ai/tests/agent_test.rs | 5 +- crates/erp-core/src/aggregate.rs | 5 +- .../src/handler/alert_rule_handler.rs | 3 + .../erp-health/src/handler/article_handler.rs | 3 + .../src/handler/article_tag_handler.rs | 3 + .../erp-health/src/handler/doctor_handler.rs | 3 + .../src/service/stats_service/health.rs | 201 ++- .../src/service/stats_service/operations.rs | 116 +- crates/erp-server/migration/src/lib.rs | 4 + .../m20260518_000149_fix_admin_permissions.rs | 78 + ...260518_000150_seed_ai_config_permission.rs | 94 ++ crates/erp-server/src/main.rs | 27 +- .../qa/e2e-test-report-v1-release-deep-api.md | 349 +++++ docs/qa/e2e-web-frontend-report.md | 445 ++++++ ...iniprogram-contract-verification-report.md | 357 +++++ docs/qa/performance-baseline-report.md | 285 ++++ .../multi-role-scenario-results.md | 96 ++ .../role-test-results/run_multi_role_test.py | 793 +++++++++++ docs/qa/role-test-results/test_results.json | 1256 +++++++++++++++++ docs/qa/security-deep-verification-report.md | 117 ++ .../v1-release-comprehensive-test-report.md | 456 ++++++ 36 files changed, 5564 insertions(+), 189 deletions(-) create mode 100644 apps/web/src/api/ai/config.ts create mode 100644 apps/web/src/pages/health/AiConfigPage.tsx create mode 100644 crates/erp-ai/src/config_resolver.rs create mode 100644 crates/erp-ai/src/handler/config_handler.rs create mode 100644 crates/erp-server/migration/src/m20260518_000149_fix_admin_permissions.rs create mode 100644 crates/erp-server/migration/src/m20260518_000150_seed_ai_config_permission.rs create mode 100644 docs/qa/e2e-test-report-v1-release-deep-api.md create mode 100644 docs/qa/e2e-web-frontend-report.md create mode 100644 docs/qa/miniprogram-contract-verification-report.md create mode 100644 docs/qa/performance-baseline-report.md create mode 100644 docs/qa/role-test-results/multi-role-scenario-results.md create mode 100644 docs/qa/role-test-results/run_multi_role_test.py create mode 100644 docs/qa/role-test-results/test_results.json create mode 100644 docs/qa/security-deep-verification-report.md create mode 100644 docs/qa/v1-release-comprehensive-test-report.md diff --git a/Cargo.toml b/Cargo.toml index 8b42093..4a85b6c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,7 @@ tokio = { version = "1", features = ["full"] } # Web axum = { version = "0.8", features = ["multipart"] } tower = "0.5" -tower-http = { version = "0.6", features = ["cors", "trace", "compression-gzip", "fs"] } +tower-http = { version = "0.6", features = ["cors", "trace", "compression-gzip", "fs", "set-header"] } # Database sea-orm = { version = "1.1", features = [ diff --git a/apps/miniprogram/src/pages/appointment/create/index.tsx b/apps/miniprogram/src/pages/appointment/create/index.tsx index 4635837..790c481 100644 --- a/apps/miniprogram/src/pages/appointment/create/index.tsx +++ b/apps/miniprogram/src/pages/appointment/create/index.tsx @@ -121,7 +121,7 @@ export default function AppointmentCreate() { appointment_date: appointmentDate, start_time: selectedSlot?.start_time || timeSlot, end_time: selectedSlot?.end_time || timeSlot, - reason: reason.trim() || undefined, + notes: reason.trim() || undefined, }); Taro.showToast({ title: '预约成功', icon: 'success' }); trackEvent('appointment_create', { doctor_id: selectedDoctor.id, date: appointmentDate }); diff --git a/apps/miniprogram/src/services/appointment.ts b/apps/miniprogram/src/services/appointment.ts index 48104a6..a1f0951 100644 --- a/apps/miniprogram/src/services/appointment.ts +++ b/apps/miniprogram/src/services/appointment.ts @@ -45,11 +45,11 @@ export async function getAppointment(id: string) { export async function createAppointment(data: { patient_id: string; doctor_id: string; - schedule_id?: string; appointment_date: string; start_time: string; end_time: string; - reason?: string; + notes?: string; + appointment_type?: string; }) { return api.post('/health/appointments', data); } diff --git a/apps/miniprogram/src/services/doctor/consultation.ts b/apps/miniprogram/src/services/doctor/consultation.ts index c43f250..76ca2bf 100644 --- a/apps/miniprogram/src/services/doctor/consultation.ts +++ b/apps/miniprogram/src/services/doctor/consultation.ts @@ -9,8 +9,8 @@ export interface ConsultationSession { doctor_id: string | null; consultation_type: string; status: string; - subject: string | null; - last_message: string | null; + subject?: string | null; + last_message?: string | null; last_message_at: string | null; unread_count_doctor?: number; created_at: string; diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index e64c434..cbaf68c 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -45,6 +45,7 @@ const StatisticsDashboard = lazy(() => import('./pages/health/StatisticsDashboar const AiPromptList = lazy(() => import('./pages/health/AiPromptList')); const AiAnalysisList = lazy(() => import('./pages/health/AiAnalysisList')); const AiUsageDashboard = lazy(() => import('./pages/health/AiUsageDashboard')); +const AiConfigPage = lazy(() => import('./pages/health/AiConfigPage')); const AlertList = lazy(() => import('./pages/health/AlertList')); const AlertDashboard = lazy(() => import('./pages/health/AlertDashboard')); const AlertRuleList = lazy(() => import('./pages/health/AlertRuleList')); @@ -254,7 +255,7 @@ export default function App() { "/health/follow-up-records", "/health/consultations", "/health/points-rules", "/health/points-products", "/health/points-orders", "/health/offline-events", "/health/ai-prompts", "/health/ai-analysis", - "/health/ai-usage", "/health/alerts", "/health/alert-dashboard", + "/health/ai-usage", "/health/ai-config", "/health/alerts", "/health/alert-dashboard", "/health/alert-rules", "/health/devices", "/health/realtime-monitor", "/health/oauth-clients", "/health/dialysis", "/health/action-inbox", "/health/follow-up-templates", "/health/care-plans", "/health/shifts", @@ -325,6 +326,7 @@ export default function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/apps/web/src/api/ai/config.ts b/apps/web/src/api/ai/config.ts new file mode 100644 index 0000000..99055e3 --- /dev/null +++ b/apps/web/src/api/ai/config.ts @@ -0,0 +1,35 @@ +import client from '../client'; + +export interface AiAgentConfig { + model: string; + temperature: number; + max_tokens: number; + max_iterations: number; + system_prompt: string; +} + +export interface AiAnalysisDefaults { + model: string; + temperature: number; + max_tokens: number; +} + +export interface AiConfig { + agent: AiAgentConfig; + analysis_defaults: AiAnalysisDefaults; +} + +export const aiConfigApi = { + get: async () => { + const resp = await client.get('/ai/config'); + return resp.data.data as AiConfig; + }, + getDefaults: async () => { + const resp = await client.get('/ai/config/defaults'); + return resp.data.data as AiConfig; + }, + update: async (config: AiConfig) => { + const resp = await client.put('/ai/config', { config }); + return resp.data.data as AiConfig; + }, +}; diff --git a/apps/web/src/pages/health/AiConfigPage.tsx b/apps/web/src/pages/health/AiConfigPage.tsx new file mode 100644 index 0000000..cc5419d --- /dev/null +++ b/apps/web/src/pages/health/AiConfigPage.tsx @@ -0,0 +1,280 @@ +import { useEffect, useState, useCallback } from 'react'; +import { + Card, + Form, + Input, + InputNumber, + Button, + Space, + message, + Divider, + Spin, + Tabs, +} from 'antd'; +import { SaveOutlined, UndoOutlined } from '@ant-design/icons'; +import { aiConfigApi, type AiConfig } from '../../api/ai/config'; +import { AuthButton } from '../../components/AuthButton'; +import { useThemeMode } from '../../hooks/useThemeMode'; + +const { TextArea } = Input; + +export default function AiConfigPage() { + const [config, setConfig] = useState(null); + const [loading, setLoading] = useState(false); + const [saving, setSaving] = useState(false); + const [form] = Form.useForm(); + const isDark = useThemeMode(); + + const fetchConfig = useCallback(async () => { + setLoading(true); + try { + const data = await aiConfigApi.get(); + setConfig(data); + form.setFieldsValue({ + agentModel: data.agent.model, + agentTemperature: data.agent.temperature, + agentMaxTokens: data.agent.max_tokens, + agentMaxIterations: data.agent.max_iterations, + agentSystemPrompt: data.agent.system_prompt, + analysisModel: data.analysis_defaults.model, + analysisTemperature: data.analysis_defaults.temperature, + analysisMaxTokens: data.analysis_defaults.max_tokens, + }); + } catch { + message.error('加载 AI 配置失败'); + } finally { + setLoading(false); + } + }, [form]); + + useEffect(() => { + fetchConfig(); + }, [fetchConfig]); + + const handleSave = async () => { + try { + const values = await form.validateFields(); + setSaving(true); + + const updated: AiConfig = { + agent: { + model: values.agentModel, + temperature: values.agentTemperature, + max_tokens: values.agentMaxTokens, + max_iterations: values.agentMaxIterations, + system_prompt: values.agentSystemPrompt, + }, + analysis_defaults: { + model: values.analysisModel, + temperature: values.analysisTemperature, + max_tokens: values.analysisMaxTokens, + }, + }; + + const result = await aiConfigApi.update(updated); + setConfig(result); + message.success('AI 配置已保存'); + } catch (err: unknown) { + if (err && typeof err === 'object' && 'errorFields' in err) { + return; + } + message.error('保存 AI 配置失败'); + } finally { + setSaving(false); + } + }; + + const handleReset = async () => { + try { + const defaults = await aiConfigApi.getDefaults(); + form.setFieldsValue({ + agentModel: defaults.agent.model, + agentTemperature: defaults.agent.temperature, + agentMaxTokens: defaults.agent.max_tokens, + agentMaxIterations: defaults.agent.max_iterations, + agentSystemPrompt: defaults.agent.system_prompt, + analysisModel: defaults.analysis_defaults.model, + analysisTemperature: defaults.analysis_defaults.temperature, + analysisMaxTokens: defaults.analysis_defaults.max_tokens, + }); + message.info('已恢复为系统默认值(未保存)'); + } catch { + message.error('加载默认配置失败'); + } + }; + + if (loading && !config) { + return ( +
+ +
+ ); + } + + const cardStyle = isDark + ? { background: '#1f1f1f', borderColor: '#333' } + : {}; + + return ( +
+

AI 配置管理

+ +
+ + + + + + + + + + + + + + + + + + + + + + +