perf(health): 随访列表内联负责人名称 — 消除 N+1 查询
follow_up list_tasks 批量查询 users 表获取 assigned_to_name, 前端移除 doctorLabels 逐条请求缓存,直接使用后端内联字段。
This commit is contained in:
@@ -7,6 +7,7 @@ export interface FollowUpTask {
|
|||||||
patient_id: string;
|
patient_id: string;
|
||||||
assigned_to?: string;
|
assigned_to?: string;
|
||||||
patient_name?: string;
|
patient_name?: string;
|
||||||
|
assigned_to_name?: string;
|
||||||
follow_up_type: string;
|
follow_up_type: string;
|
||||||
planned_date: string;
|
planned_date: string;
|
||||||
status: string;
|
status: string;
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import { PlusOutlined, EditOutlined, SwapOutlined, DeleteOutlined } from '@ant-d
|
|||||||
import type { ColumnsType, TablePaginationConfig } from 'antd/es/table';
|
import type { ColumnsType, TablePaginationConfig } from 'antd/es/table';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { followUpApi, type FollowUpTask, type CreateFollowUpTaskReq, type UpdateFollowUpTaskReq } from '../../api/health/followUp';
|
import { followUpApi, type FollowUpTask, type CreateFollowUpTaskReq, type UpdateFollowUpTaskReq } from '../../api/health/followUp';
|
||||||
import { getUser } from '../../api/users';
|
|
||||||
import { StatusTag } from './components/StatusTag';
|
import { StatusTag } from './components/StatusTag';
|
||||||
import { PatientSelect } from './components/PatientSelect';
|
import { PatientSelect } from './components/PatientSelect';
|
||||||
import { DoctorSelect } from './components/DoctorSelect';
|
import { DoctorSelect } from './components/DoctorSelect';
|
||||||
@@ -94,9 +93,6 @@ export default function FollowUpTaskList() {
|
|||||||
const [assignForm] = Form.useForm<AssignFormValues>();
|
const [assignForm] = Form.useForm<AssignFormValues>();
|
||||||
const [assignTask, setAssignTask] = useState<FollowUpTask | null>(null);
|
const [assignTask, setAssignTask] = useState<FollowUpTask | null>(null);
|
||||||
|
|
||||||
// Doctor label cache (for assignee display from users table)
|
|
||||||
const [doctorLabels, setDoctorLabels] = useState<Record<string, string>>({});
|
|
||||||
|
|
||||||
const isDark = useThemeMode();
|
const isDark = useThemeMode();
|
||||||
|
|
||||||
// --- Data fetching ---
|
// --- Data fetching ---
|
||||||
@@ -106,30 +102,12 @@ export default function FollowUpTaskList() {
|
|||||||
const result = await followUpApi.listTasks(params);
|
const result = await followUpApi.listTasks(params);
|
||||||
setTasks(result.data);
|
setTasks(result.data);
|
||||||
setTotal(result.total);
|
setTotal(result.total);
|
||||||
|
|
||||||
// Batch resolve assignee names (from users table, not inlined by backend)
|
|
||||||
const assigneeIds = [...new Set(result.data.map((t: FollowUpTask) => t.assigned_to).filter((x): x is string => !!x))];
|
|
||||||
const missingIds = assigneeIds.filter((id) => !doctorLabels[id]);
|
|
||||||
if (missingIds.length > 0) {
|
|
||||||
const newDoctorLabels: Record<string, string> = {};
|
|
||||||
await Promise.allSettled(
|
|
||||||
missingIds.map(async (id) => {
|
|
||||||
try {
|
|
||||||
const u = await getUser(id);
|
|
||||||
newDoctorLabels[id] = u.display_name || u.username;
|
|
||||||
} catch { /* skip */ }
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
if (Object.keys(newDoctorLabels).length > 0) {
|
|
||||||
setDoctorLabels((prev) => ({ ...prev, ...newDoctorLabels }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
message.error('加载随访任务失败');
|
message.error('加载随访任务失败');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [doctorLabels]);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchTasks(query);
|
fetchTasks(query);
|
||||||
@@ -247,9 +225,11 @@ export default function FollowUpTaskList() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Store labels from selects
|
// Store labels from selects for immediate display
|
||||||
const handleDoctorLabel = (id: string, label: string) => {
|
const handleDoctorLabel = (id: string, label: string) => {
|
||||||
setDoctorLabels((prev) => ({ ...prev, [id]: label }));
|
setTasks((prev) =>
|
||||||
|
prev.map((t) => (t.assigned_to === id ? { ...t, assigned_to_name: label } : t)),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Columns ---
|
// --- Columns ---
|
||||||
@@ -288,8 +268,8 @@ export default function FollowUpTaskList() {
|
|||||||
dataIndex: 'assigned_to',
|
dataIndex: 'assigned_to',
|
||||||
key: 'assigned_to',
|
key: 'assigned_to',
|
||||||
width: 140,
|
width: 140,
|
||||||
render: (id: string | undefined) =>
|
render: (_: unknown, record: FollowUpTask) =>
|
||||||
id ? doctorLabels[id] || id.slice(0, 8) : '-',
|
record.assigned_to ? record.assigned_to_name || record.assigned_to.slice(0, 8) : '-',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '创建时间',
|
title: '创建时间',
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ pub struct FollowUpTaskResp {
|
|||||||
pub patient_id: Uuid,
|
pub patient_id: Uuid,
|
||||||
pub assigned_to: Option<Uuid>,
|
pub assigned_to: Option<Uuid>,
|
||||||
pub patient_name: Option<String>,
|
pub patient_name: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub assigned_to_name: Option<String>,
|
||||||
pub follow_up_type: String,
|
pub follow_up_type: String,
|
||||||
pub planned_date: NaiveDate,
|
pub planned_date: NaiveDate,
|
||||||
pub status: String,
|
pub status: String,
|
||||||
|
|||||||
@@ -68,9 +68,32 @@ pub async fn list_tasks(
|
|||||||
HashMap::new()
|
HashMap::new()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 批量查询 assigned_to_name(从 users 表)
|
||||||
|
let assigned_ids: HashSet<Uuid> = models.iter().filter_map(|m| m.assigned_to).collect();
|
||||||
|
let assigned_names: HashMap<Uuid, String> = if !assigned_ids.is_empty() {
|
||||||
|
let ids_csv = assigned_ids.iter().map(|id| format!("'{}'", id)).collect::<Vec<_>>().join(",");
|
||||||
|
let sql = format!(
|
||||||
|
"SELECT id, COALESCE(display_name, username) AS name FROM users WHERE id IN ({}) AND tenant_id = '{}'",
|
||||||
|
ids_csv, tenant_id
|
||||||
|
);
|
||||||
|
let rows = state.db.query_all(sea_orm::Statement::from_string(
|
||||||
|
sea_orm::DatabaseBackend::Postgres, sql,
|
||||||
|
)).await?;
|
||||||
|
rows.into_iter()
|
||||||
|
.filter_map(|row| {
|
||||||
|
let id: Uuid = row.try_get_by_index(0).ok()?;
|
||||||
|
let name: String = row.try_get_by_index(1).ok()?;
|
||||||
|
Some((id, name))
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
HashMap::new()
|
||||||
|
};
|
||||||
|
|
||||||
let data = models.into_iter().map(|m| FollowUpTaskResp {
|
let data = models.into_iter().map(|m| FollowUpTaskResp {
|
||||||
id: m.id, patient_id: m.patient_id, assigned_to: m.assigned_to,
|
id: m.id, patient_id: m.patient_id, assigned_to: m.assigned_to,
|
||||||
patient_name: patient_names.get(&m.patient_id).cloned(),
|
patient_name: patient_names.get(&m.patient_id).cloned(),
|
||||||
|
assigned_to_name: m.assigned_to.and_then(|uid| assigned_names.get(&uid).cloned()),
|
||||||
follow_up_type: m.follow_up_type, planned_date: m.planned_date,
|
follow_up_type: m.follow_up_type, planned_date: m.planned_date,
|
||||||
status: m.status, content_template: m.content_template,
|
status: m.status, content_template: m.content_template,
|
||||||
related_appointment_id: m.related_appointment_id,
|
related_appointment_id: m.related_appointment_id,
|
||||||
@@ -95,7 +118,7 @@ pub async fn get_task(
|
|||||||
|
|
||||||
Ok(FollowUpTaskResp {
|
Ok(FollowUpTaskResp {
|
||||||
id: m.id, patient_id: m.patient_id, assigned_to: m.assigned_to,
|
id: m.id, patient_id: m.patient_id, assigned_to: m.assigned_to,
|
||||||
patient_name: None,
|
patient_name: None, assigned_to_name: None,
|
||||||
follow_up_type: m.follow_up_type, planned_date: m.planned_date,
|
follow_up_type: m.follow_up_type, planned_date: m.planned_date,
|
||||||
status: m.status, content_template: m.content_template,
|
status: m.status, content_template: m.content_template,
|
||||||
related_appointment_id: m.related_appointment_id,
|
related_appointment_id: m.related_appointment_id,
|
||||||
@@ -156,7 +179,7 @@ pub async fn create_task(
|
|||||||
|
|
||||||
Ok(FollowUpTaskResp {
|
Ok(FollowUpTaskResp {
|
||||||
id: m.id, patient_id: m.patient_id, assigned_to: m.assigned_to,
|
id: m.id, patient_id: m.patient_id, assigned_to: m.assigned_to,
|
||||||
patient_name: None,
|
patient_name: None, assigned_to_name: None,
|
||||||
follow_up_type: m.follow_up_type, planned_date: m.planned_date,
|
follow_up_type: m.follow_up_type, planned_date: m.planned_date,
|
||||||
status: m.status, content_template: m.content_template,
|
status: m.status, content_template: m.content_template,
|
||||||
related_appointment_id: m.related_appointment_id,
|
related_appointment_id: m.related_appointment_id,
|
||||||
@@ -227,7 +250,7 @@ pub async fn update_task(
|
|||||||
|
|
||||||
Ok(FollowUpTaskResp {
|
Ok(FollowUpTaskResp {
|
||||||
id: m.id, patient_id: m.patient_id, assigned_to: m.assigned_to,
|
id: m.id, patient_id: m.patient_id, assigned_to: m.assigned_to,
|
||||||
patient_name: None,
|
patient_name: None, assigned_to_name: None,
|
||||||
follow_up_type: m.follow_up_type, planned_date: m.planned_date,
|
follow_up_type: m.follow_up_type, planned_date: m.planned_date,
|
||||||
status: m.status, content_template: m.content_template,
|
status: m.status, content_template: m.content_template,
|
||||||
related_appointment_id: m.related_appointment_id,
|
related_appointment_id: m.related_appointment_id,
|
||||||
|
|||||||
Reference in New Issue
Block a user