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');
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) => {
secureSet('current_patient_id', patient.id);
secureSet('current_patient', JSON.stringify(patient));
setCachedPatientId(patient.id);
const safePatient: authApi.PatientInfo = {
id: 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();
set({ currentPatient: patient });
set({ currentPatient: safePatient });
},
loadPatients: async () => {
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 });
if (patients.length > 0 && !get().currentPatient) {
get().setCurrentPatient(patients[0]);

View File

@@ -246,3 +246,13 @@ pub struct ReferResultResp {
pub from_doctor_id: Option<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::patient_dto::{
BatchImportPatientReq, BatchResultResp, BindByPhoneReq, BindResultResp, CreatePatientReq,
FamilyMemberReq, FamilyMemberResp, ManageTagsReq, PatientResp, ReferPatientReq,
FamilyMemberReq, FamilyMemberResp, ManageTagsReq, PatientResp, PatientSummary, ReferPatientReq,
ReferResultResp, UpdatePatientReq,
};
use crate::service::patient_service;
@@ -57,6 +57,23 @@ where
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>(
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,

View File

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

View File

@@ -550,3 +550,47 @@ pub async fn bind_by_phone(
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 层调用路径不变
pub use crud::{
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::{
assign_doctor, create_family_member, delete_family_member, get_health_summary,