feat(health+mp): S2-3 Patient DTO 最小化

后端:
- 新增 PatientSummary DTO(id/name/gender/birth_date/status 5 字段)
- 新增 GET /health/patients/summary 端点(权限 health.patient.list)
- patient_service::list_summaries 仅查询非敏感字段

前端:
- 新增 PatientSummary 类型 + getPatientSummaries() API
- auth store loadPatients 改用 summary 端点
- setCurrentPatient 仅存储非敏感字段到 secureSet
This commit is contained in:
iven
2026-05-22 10:56:03 +08:00
parent 437f5d1ae9
commit 490ae075b7
7 changed files with 111 additions and 7 deletions

View File

@@ -57,3 +57,18 @@ export async function getPatients() {
const res = await api.get<PaginatedData<PatientInfo>>('/health/patients'); const res = await api.get<PaginatedData<PatientInfo>>('/health/patients');
return Array.isArray(res?.data) ? res.data : (Array.isArray(res) ? res : []); return Array.isArray(res?.data) ? res.data : (Array.isArray(res) ? res : []);
} }
/** 患者摘要 — 列表用,字段最小化,不含敏感信息 */
export interface PatientSummary {
id: string;
name: string;
gender?: string;
birth_date?: string;
status: string;
}
/** 获取患者摘要列表(字段最小化,替代 getPatients */
export async function getPatientSummaries() {
const res = await api.get<PaginatedData<PatientSummary>>('/health/patients/summary');
return Array.isArray(res?.data) ? res.data : (Array.isArray(res) ? res : []);
}

View File

@@ -222,16 +222,30 @@ export const useAuthStore = create<AuthState>((set, get) => ({
}, },
setCurrentPatient: (patient) => { setCurrentPatient: (patient) => {
secureSet('current_patient_id', patient.id); const safePatient: authApi.PatientInfo = {
secureSet('current_patient', JSON.stringify(patient)); id: patient.id,
setCachedPatientId(patient.id); name: patient.name,
gender: patient.gender,
birth_date: patient.birth_date,
relation: patient.relation,
};
secureSet('current_patient_id', safePatient.id);
secureSet('current_patient', JSON.stringify(safePatient));
setCachedPatientId(safePatient.id);
clearRequestCache(); clearRequestCache();
set({ currentPatient: patient }); set({ currentPatient: safePatient });
}, },
loadPatients: async () => { loadPatients: async () => {
try { try {
const patients = await authApi.getPatients(); const summaries = await authApi.getPatientSummaries();
const patients: authApi.PatientInfo[] = summaries.map((p) => ({
id: p.id,
name: p.name,
gender: p.gender,
birth_date: p.birth_date,
relation: 'self',
}));
set({ patients }); set({ patients });
if (patients.length > 0 && !get().currentPatient) { if (patients.length > 0 && !get().currentPatient) {
get().setCurrentPatient(patients[0]); get().setCurrentPatient(patients[0]);

View File

@@ -246,3 +246,13 @@ pub struct ReferResultResp {
pub from_doctor_id: Option<Uuid>, pub from_doctor_id: Option<Uuid>,
pub to_doctor_id: Uuid, pub to_doctor_id: Uuid,
} }
/// 患者摘要 — 列表/切换用,不含敏感字段
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct PatientSummary {
pub id: Uuid,
pub name: String,
pub gender: Option<String>,
pub birth_date: Option<NaiveDate>,
pub status: String,
}

View File

@@ -12,7 +12,7 @@ use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext};
use crate::dto::DeleteWithVersion; use crate::dto::DeleteWithVersion;
use crate::dto::patient_dto::{ use crate::dto::patient_dto::{
BatchImportPatientReq, BatchResultResp, BindByPhoneReq, BindResultResp, CreatePatientReq, BatchImportPatientReq, BatchResultResp, BindByPhoneReq, BindResultResp, CreatePatientReq,
FamilyMemberReq, FamilyMemberResp, ManageTagsReq, PatientResp, ReferPatientReq, FamilyMemberReq, FamilyMemberResp, ManageTagsReq, PatientResp, PatientSummary, ReferPatientReq,
ReferResultResp, UpdatePatientReq, ReferResultResp, UpdatePatientReq,
}; };
use crate::service::patient_service; use crate::service::patient_service;
@@ -57,6 +57,23 @@ where
Ok(Json(ApiResponse::ok(result))) Ok(Json(ApiResponse::ok(result)))
} }
/// GET /health/patients/summary — 患者摘要列表(字段最小化,不含敏感信息)
pub async fn list_patient_summaries<S>(
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Query(params): Query<PatientListParams>,
) -> Result<Json<ApiResponse<PaginatedResponse<PatientSummary>>>, AppError>
where
HealthState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "health.patient.list")?;
let page = params.page.unwrap_or(1);
let page_size = params.page_size.unwrap_or(20).min(100);
let result = patient_service::list_summaries(&state, ctx.tenant_id, page, page_size).await?;
Ok(Json(ApiResponse::ok(result)))
}
pub async fn create_patient<S>( pub async fn create_patient<S>(
State(state): State<HealthState>, State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>, Extension(ctx): Extension<TenantContext>,

View File

@@ -8,6 +8,10 @@ where
S: Clone + Send + Sync + 'static, S: Clone + Send + Sync + 'static,
{ {
Router::new() Router::new()
.route(
"/health/patients/summary",
axum::routing::get(patient_handler::list_patient_summaries),
)
.route( .route(
"/health/patients", "/health/patients",
axum::routing::get(patient_handler::list_patients) axum::routing::get(patient_handler::list_patients)

View File

@@ -550,3 +550,47 @@ pub async fn bind_by_phone(
patient_name: updated.name, patient_name: updated.name,
}) })
} }
/// 患者摘要列表 — 仅返回非敏感字段,供小程序切换/列表使用
pub async fn list_summaries(
state: &HealthState,
tenant_id: Uuid,
page: u64,
page_size: u64,
) -> HealthResult<PaginatedResponse<PatientSummary>> {
let limit = page_size.min(100);
let offset = page.saturating_sub(1) * limit;
let query = patient::Entity::find()
.filter(patient::Column::TenantId.eq(tenant_id))
.filter(patient::Column::DeletedAt.is_null());
let total = query.clone().count(&state.db).await?;
let models = query
.order_by_desc(patient::Column::CreatedAt)
.offset(offset)
.limit(limit)
.all(&state.db)
.await?;
let total_pages = total.div_ceil(limit.max(1));
let data: Vec<PatientSummary> = models
.into_iter()
.map(|m| PatientSummary {
id: m.id,
name: m.name,
gender: m.gender,
birth_date: m.birth_date,
status: m.status,
})
.collect();
Ok(PaginatedResponse {
data,
total,
page,
page_size: limit,
total_pages,
})
}

View File

@@ -14,7 +14,7 @@ mod tag;
// 从各子模块重新导出所有公开函数,保持 handler 层调用路径不变 // 从各子模块重新导出所有公开函数,保持 handler 层调用路径不变
pub use crud::{ pub use crud::{
batch_import_patients, bind_by_phone, create_patient, delete_patient, get_patient, batch_import_patients, bind_by_phone, create_patient, delete_patient, get_patient,
list_patients, update_patient, list_patients, list_summaries, update_patient,
}; };
pub use relation::{ pub use relation::{
assign_doctor, create_family_member, delete_family_member, get_health_summary, assign_doctor, create_family_member, delete_family_member, get_health_summary,