fix: V1 测试版本端到端验证修复 — 6 CRITICAL + 3 HIGH 问题全量修复

修复项:
- 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% 通过
- 综合测试报告 + 专家评估报告
This commit is contained in:
iven
2026-05-18 10:24:40 +08:00
parent 38b0d91407
commit d623f8b2ff
36 changed files with 5564 additions and 189 deletions

View File

@@ -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() {
<Route path="/health/ai-prompts" element={<AiPromptList />} />
<Route path="/health/ai-analysis" element={<AiAnalysisList />} />
<Route path="/health/ai-usage" element={<AiUsageDashboard />} />
<Route path="/health/ai-config" element={<AiConfigPage />} />
<Route path="/health/alerts" element={<AlertList />} />
<Route path="/health/alert-dashboard" element={<AlertDashboard />} />
<Route path="/health/alert-rules" element={<AlertRuleList />} />

View File

@@ -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;
},
};

View File

@@ -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<AiConfig | null>(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 (
<div style={{ textAlign: 'center', padding: 48 }}>
<Spin size="large" />
</div>
);
}
const cardStyle = isDark
? { background: '#1f1f1f', borderColor: '#333' }
: {};
return (
<div style={{ padding: 24, maxWidth: 960, margin: '0 auto' }}>
<h2 style={{ marginBottom: 24 }}>AI </h2>
<Form form={form} layout="vertical">
<Tabs
items={[
{
key: 'agent',
label: 'Agent 对话配置',
children: (
<Card
title="AI 客服 Agent 参数"
size="small"
style={cardStyle}
>
<Form.Item
label="模型名称"
name="agentModel"
rules={[{ required: true, message: '请输入模型名称' }]}
extra="如 claude-sonnet-4-6、gpt-4o 等"
>
<Input placeholder="claude-sonnet-4-6" />
</Form.Item>
<Space style={{ width: '100%' }} size="large">
<Form.Item
label="温度 (Temperature)"
name="agentTemperature"
rules={[{ required: true, message: '请输入温度参数' }]}
extra="0.0 ~ 2.0,越高越随机"
style={{ width: 200 }}
>
<InputNumber
min={0}
max={2}
step={0.1}
style={{ width: '100%' }}
/>
</Form.Item>
<Form.Item
label="最大 Token"
name="agentMaxTokens"
rules={[{ required: true, message: '请输入最大 Token' }]}
extra="1 ~ 65536"
style={{ width: 200 }}
>
<InputNumber
min={1}
max={65536}
step={256}
style={{ width: '100%' }}
/>
</Form.Item>
<Form.Item
label="最大迭代次数"
name="agentMaxIterations"
rules={[
{ required: true, message: '请输入最大迭代次数' },
]}
extra="1 ~ 20Agent ReAct 循环上限"
style={{ width: 200 }}
>
<InputNumber
min={1}
max={20}
style={{ width: '100%' }}
/>
</Form.Item>
</Space>
<Divider style={{ margin: '8px 0 16px' }} />
<Form.Item
label="系统提示词 (System Prompt)"
name="agentSystemPrompt"
rules={[{ required: true, message: '请输入系统提示词' }]}
extra="定义 AI 客服的角色和行为策略"
>
<TextArea rows={12} placeholder="你是 HMS 健康管理平台的 AI 健康顾问..." />
</Form.Item>
</Card>
),
},
{
key: 'analysis',
label: '分析任务默认配置',
children: (
<Card
title="AI 分析任务默认参数"
size="small"
style={cardStyle}
extra="当 Prompt 模板未指定模型参数时使用的默认值"
>
<Form.Item
label="默认模型"
name="analysisModel"
rules={[{ required: true, message: '请输入默认模型' }]}
extra="化验单解读、趋势分析等分析任务的默认模型"
>
<Input placeholder="claude-sonnet-4-6" />
</Form.Item>
<Space style={{ width: '100%' }} size="large">
<Form.Item
label="默认温度"
name="analysisTemperature"
rules={[{ required: true, message: '请输入默认温度' }]}
extra="分析任务建议用较低温度"
style={{ width: 200 }}
>
<InputNumber
min={0}
max={2}
step={0.1}
style={{ width: '100%' }}
/>
</Form.Item>
<Form.Item
label="默认最大 Token"
name="analysisMaxTokens"
rules={[
{ required: true, message: '请输入默认最大 Token' },
]}
style={{ width: 200 }}
>
<InputNumber
min={1}
max={65536}
step={256}
style={{ width: '100%' }}
/>
</Form.Item>
</Space>
</Card>
),
},
]}
/>
<div style={{ marginTop: 16, textAlign: 'right' }}>
<Space>
<Button icon={<UndoOutlined />} onClick={handleReset}>
</Button>
<AuthButton
permission="ai.config.manage"
type="primary"
icon={<SaveOutlined />}
loading={saving}
onClick={handleSave}
>
</AuthButton>
</Space>
</div>
</Form>
</div>
);
}

View File

@@ -143,6 +143,10 @@ const ENTRIES: RoutePermissionEntry[] = [
permissions: ["ai.analysis.list", "ai.analysis.manage"],
},
{ path: "/health/ai-usage", permissions: ["ai.usage.list"] },
{
path: "/health/ai-config",
permissions: ["ai.config.read", "ai.config.manage"],
},
// ===== 健康管理 — 积分商城 =====
{