fix(security): 补全 XSS sanitize + 修复 sender_id 身份伪造
安全审计修复: - 补全 6 个 DTO 的 sanitize 方法(diagnosis/consent/alert/medication_record/medication_reminder/follow_up_template) - 4 个 handler 添加 .sanitize() 调用(diagnosis/consent/alert_rule/medication_record) - 修复咨询消息 sender_id/sender_role 从客户端提交改为服务端从 JWT 提取 - 修复小程序 AI 报告 markdownToHtml XSS(添加 sanitizeHtml 过滤)
This commit is contained in:
@@ -12,8 +12,26 @@ const TYPE_LABELS: Record<string, string> = {
|
||||
report_summary_generation: '报告摘要',
|
||||
};
|
||||
|
||||
/** 移除危险的 HTML 标签和事件属性,防止 XSS */
|
||||
function sanitizeHtml(html: string): string {
|
||||
return html
|
||||
// 移除 <script> 标签及其内容
|
||||
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
|
||||
// 移除 <iframe>, <object>, <embed>, <form>, <input>, <textarea>, <style> 标签
|
||||
.replace(/<\/?(?:iframe|object|embed|form|input|textarea|style)\b[^>]*>/gi, '')
|
||||
// 移除 <link> 和 <meta> 标签
|
||||
.replace(/<\/?(?:link|meta)\b[^>]*>/gi, '')
|
||||
// 移除所有 on* 事件属性 (onclick, onerror, onload 等)
|
||||
.replace(/\s+on\w+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi, '')
|
||||
// 移除 javascript: 和 data: 协议的 href/src 属性
|
||||
.replace(/(href|src)\s*=\s*(?:"javascript:[^"]*"|'javascript:[^']*')/gi, '')
|
||||
.replace(/(href|src)\s*=\s*(?:"data:[^"]*"|'data:[^']*')/gi, '');
|
||||
}
|
||||
|
||||
function markdownToHtml(md: string): string {
|
||||
return md
|
||||
// 先转义 markdown 中可能存在的原始 HTML 标签
|
||||
const escaped = sanitizeHtml(md);
|
||||
return escaped
|
||||
.replace(/^### (.+)$/gm, '<h3>$1</h3>')
|
||||
.replace(/^## (.+)$/gm, '<h2>$1</h2>')
|
||||
.replace(/^# (.+)$/gm, '<h1>$1</h1>')
|
||||
|
||||
@@ -37,8 +37,6 @@ export interface Message {
|
||||
|
||||
export interface CreateMessageReq {
|
||||
session_id: string;
|
||||
sender_id: string;
|
||||
sender_role: string;
|
||||
content_type?: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use erp_core::sanitize::{sanitize_option, sanitize_string};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
use uuid::Uuid;
|
||||
@@ -18,6 +19,13 @@ pub struct CreateAlertRuleRequest {
|
||||
pub cooldown_minutes: Option<i32>,
|
||||
}
|
||||
|
||||
impl CreateAlertRuleRequest {
|
||||
pub fn sanitize(&mut self) {
|
||||
self.name = sanitize_string(&self.name);
|
||||
self.description = sanitize_option(self.description.take());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct UpdateAlertRuleRequest {
|
||||
pub name: Option<String>,
|
||||
@@ -30,6 +38,13 @@ pub struct UpdateAlertRuleRequest {
|
||||
pub version: i32,
|
||||
}
|
||||
|
||||
impl UpdateAlertRuleRequest {
|
||||
pub fn sanitize(&mut self) {
|
||||
self.name = sanitize_option(self.name.take());
|
||||
self.description = sanitize_option(self.description.take());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
pub struct AlertRuleResponse {
|
||||
pub id: Uuid,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use chrono::{NaiveDate, Utc};
|
||||
use erp_core::sanitize::{sanitize_option, sanitize_string};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
use uuid::Uuid;
|
||||
@@ -32,8 +33,24 @@ pub struct CreateConsentReq {
|
||||
pub notes: Option<String>,
|
||||
}
|
||||
|
||||
impl CreateConsentReq {
|
||||
pub fn sanitize(&mut self) {
|
||||
self.consent_type = sanitize_string(&self.consent_type);
|
||||
self.consent_scope = sanitize_string(&self.consent_scope);
|
||||
self.consent_method = sanitize_option(self.consent_method.take());
|
||||
self.witness_name = sanitize_option(self.witness_name.take());
|
||||
self.notes = sanitize_option(self.notes.take());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct RevokeConsentReq {
|
||||
pub notes: Option<String>,
|
||||
pub version: i32,
|
||||
}
|
||||
|
||||
impl RevokeConsentReq {
|
||||
pub fn sanitize(&mut self) {
|
||||
self.notes = sanitize_option(self.notes.take());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,11 +32,10 @@ pub struct MessageResp {
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
/// 发送消息请求体 — 不含 sender_id/sender_role,由服务端从 JWT 注入。
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct CreateMessageReq {
|
||||
pub session_id: Uuid,
|
||||
pub sender_id: Uuid,
|
||||
pub sender_role: String,
|
||||
pub content_type: Option<String>,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use erp_core::sanitize::{sanitize_option, sanitize_string};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
@@ -15,6 +16,16 @@ pub struct CreateDiagnosisReq {
|
||||
pub notes: Option<String>,
|
||||
}
|
||||
|
||||
impl CreateDiagnosisReq {
|
||||
pub fn sanitize(&mut self) {
|
||||
self.icd_code = sanitize_string(&self.icd_code);
|
||||
self.diagnosis_name = sanitize_string(&self.diagnosis_name);
|
||||
self.diagnosis_type = sanitize_string(&self.diagnosis_type);
|
||||
self.status = sanitize_string(&self.status);
|
||||
self.notes = sanitize_option(self.notes.take());
|
||||
}
|
||||
}
|
||||
|
||||
fn default_diagnosis_type() -> String { "primary".to_string() }
|
||||
fn default_status() -> String { "active".to_string() }
|
||||
|
||||
@@ -30,6 +41,16 @@ pub struct UpdateDiagnosisReq {
|
||||
pub notes: Option<String>,
|
||||
}
|
||||
|
||||
impl UpdateDiagnosisReq {
|
||||
pub fn sanitize(&mut self) {
|
||||
self.icd_code = sanitize_option(self.icd_code.take());
|
||||
self.diagnosis_name = sanitize_option(self.diagnosis_name.take());
|
||||
self.diagnosis_type = sanitize_option(self.diagnosis_type.take());
|
||||
self.status = sanitize_option(self.status.take());
|
||||
self.notes = sanitize_option(self.notes.take());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
pub struct DiagnosisResp {
|
||||
pub id: uuid::Uuid,
|
||||
|
||||
@@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize};
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
use uuid::Uuid;
|
||||
|
||||
use erp_core::sanitize::sanitize_option;
|
||||
use erp_core::sanitize::{sanitize_option, sanitize_string};
|
||||
|
||||
// ── 模板字段 DTO ──
|
||||
|
||||
@@ -22,6 +22,16 @@ pub struct TemplateFieldReq {
|
||||
pub sort_order: i32,
|
||||
}
|
||||
|
||||
impl TemplateFieldReq {
|
||||
pub fn sanitize(&mut self) {
|
||||
self.label = sanitize_string(&self.label);
|
||||
self.field_key = sanitize_string(&self.field_key);
|
||||
self.field_type = sanitize_string(&self.field_type);
|
||||
self.options = sanitize_option(self.options.take());
|
||||
self.placeholder = sanitize_option(self.placeholder.take());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct TemplateFieldResp {
|
||||
pub id: Uuid,
|
||||
@@ -62,9 +72,12 @@ pub struct CreateFollowUpTemplateReq {
|
||||
|
||||
impl CreateFollowUpTemplateReq {
|
||||
pub fn sanitize(&mut self) {
|
||||
self.name = self.name.trim().to_string();
|
||||
self.name = sanitize_string(&self.name);
|
||||
self.description = sanitize_option(self.description.take());
|
||||
self.applicable_scope = sanitize_option(self.applicable_scope.take());
|
||||
for field in &mut self.fields {
|
||||
field.sanitize();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,11 +94,14 @@ pub struct UpdateFollowUpTemplateReq {
|
||||
|
||||
impl UpdateFollowUpTemplateReq {
|
||||
pub fn sanitize(&mut self) {
|
||||
if let Some(ref mut n) = self.name {
|
||||
*n = n.trim().to_string();
|
||||
}
|
||||
self.name = sanitize_option(self.name.take());
|
||||
self.description = sanitize_option(self.description.take());
|
||||
self.applicable_scope = sanitize_option(self.applicable_scope.take());
|
||||
if let Some(ref mut fields) = self.fields {
|
||||
for field in fields.iter_mut() {
|
||||
field.sanitize();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use erp_core::sanitize::{sanitize_option, sanitize_string};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
@@ -71,55 +72,29 @@ pub struct MedicationRecordResp {
|
||||
|
||||
/// 创建请求的输入清洗
|
||||
impl CreateMedicationRecordReq {
|
||||
/// 清洗用户输入:去除首尾空白,截断超长字段
|
||||
/// 清洗用户输入:去除 HTML 标签和首尾空白
|
||||
pub fn sanitize(&mut self) {
|
||||
self.medication_name = self.medication_name.trim().to_string();
|
||||
if let Some(ref mut v) = self.generic_name {
|
||||
*v = v.trim().to_string();
|
||||
}
|
||||
if let Some(ref mut v) = self.dosage {
|
||||
*v = v.trim().to_string();
|
||||
}
|
||||
if let Some(ref mut v) = self.unit {
|
||||
*v = v.trim().to_string();
|
||||
}
|
||||
if let Some(ref mut v) = self.frequency {
|
||||
*v = v.trim().to_string();
|
||||
}
|
||||
if let Some(ref mut v) = self.route {
|
||||
*v = v.trim().to_string();
|
||||
}
|
||||
if let Some(ref mut v) = self.notes {
|
||||
*v = v.trim().to_string();
|
||||
}
|
||||
self.medication_name = sanitize_string(&self.medication_name);
|
||||
self.generic_name = sanitize_option(self.generic_name.take());
|
||||
self.dosage = sanitize_option(self.dosage.take());
|
||||
self.unit = sanitize_option(self.unit.take());
|
||||
self.frequency = sanitize_option(self.frequency.take());
|
||||
self.route = sanitize_option(self.route.take());
|
||||
self.notes = sanitize_option(self.notes.take());
|
||||
}
|
||||
}
|
||||
|
||||
/// 更新请求的输入清洗
|
||||
impl UpdateMedicationRecordReq {
|
||||
/// 清洗用户输入:去除首尾空白
|
||||
/// 清洗用户输入:去除 HTML 标签和首尾空白
|
||||
pub fn sanitize(&mut self) {
|
||||
if let Some(ref mut v) = self.medication_name {
|
||||
*v = v.trim().to_string();
|
||||
}
|
||||
if let Some(ref mut v) = self.generic_name {
|
||||
*v = v.trim().to_string();
|
||||
}
|
||||
if let Some(ref mut v) = self.dosage {
|
||||
*v = v.trim().to_string();
|
||||
}
|
||||
if let Some(ref mut v) = self.unit {
|
||||
*v = v.trim().to_string();
|
||||
}
|
||||
if let Some(ref mut v) = self.frequency {
|
||||
*v = v.trim().to_string();
|
||||
}
|
||||
if let Some(ref mut v) = self.route {
|
||||
*v = v.trim().to_string();
|
||||
}
|
||||
if let Some(ref mut v) = self.notes {
|
||||
*v = v.trim().to_string();
|
||||
}
|
||||
self.medication_name = sanitize_option(self.medication_name.take());
|
||||
self.generic_name = sanitize_option(self.generic_name.take());
|
||||
self.dosage = sanitize_option(self.dosage.take());
|
||||
self.unit = sanitize_option(self.unit.take());
|
||||
self.frequency = sanitize_option(self.frequency.take());
|
||||
self.route = sanitize_option(self.route.take());
|
||||
self.notes = sanitize_option(self.notes.take());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use erp_core::sanitize::{sanitize_option, sanitize_string};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
@@ -47,19 +48,19 @@ pub struct MedicationReminderResp {
|
||||
|
||||
impl CreateMedicationReminderReq {
|
||||
pub fn sanitize(&mut self) {
|
||||
self.medication_name = self.medication_name.trim().to_string();
|
||||
if let Some(ref mut d) = self.dosage { *d = d.trim().to_string(); }
|
||||
if let Some(ref mut f) = self.frequency { *f = f.trim().to_string(); }
|
||||
if let Some(ref mut n) = self.notes { *n = n.trim().to_string(); }
|
||||
self.medication_name = sanitize_string(&self.medication_name);
|
||||
self.dosage = sanitize_option(self.dosage.take());
|
||||
self.frequency = sanitize_option(self.frequency.take());
|
||||
self.notes = sanitize_option(self.notes.take());
|
||||
}
|
||||
}
|
||||
|
||||
impl UpdateMedicationReminderReq {
|
||||
pub fn sanitize(&mut self) {
|
||||
if let Some(ref mut n) = self.medication_name { *n = n.trim().to_string(); }
|
||||
if let Some(ref mut d) = self.dosage { *d = d.trim().to_string(); }
|
||||
if let Some(ref mut f) = self.frequency { *f = f.trim().to_string(); }
|
||||
if let Some(ref mut n) = self.notes { *n = n.trim().to_string(); }
|
||||
self.medication_name = sanitize_option(self.medication_name.take());
|
||||
self.dosage = sanitize_option(self.dosage.take());
|
||||
self.frequency = sanitize_option(self.frequency.take());
|
||||
self.notes = sanitize_option(self.notes.take());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -54,13 +54,14 @@ where
|
||||
pub async fn create<S>(
|
||||
State(state): State<HealthState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
axum::Json(body): axum::Json<CreateAlertRuleRequest>,
|
||||
axum::Json(mut body): axum::Json<CreateAlertRuleRequest>,
|
||||
) -> Result<impl IntoResponse, AppError>
|
||||
where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.alert-rules.manage")?;
|
||||
body.sanitize();
|
||||
let rule = alert_rule_service::create_rule(
|
||||
&state, ctx.tenant_id, ctx.user_id, body,
|
||||
).await?;
|
||||
@@ -71,13 +72,14 @@ pub async fn update<S>(
|
||||
State(state): State<HealthState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Path(id): Path<Uuid>,
|
||||
axum::Json(body): axum::Json<UpdateAlertRuleRequest>,
|
||||
axum::Json(mut body): axum::Json<UpdateAlertRuleRequest>,
|
||||
) -> Result<impl IntoResponse, AppError>
|
||||
where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.alert-rules.manage")?;
|
||||
body.sanitize();
|
||||
let rule = alert_rule_service::update_rule(
|
||||
&state, ctx.tenant_id, id, ctx.user_id, body,
|
||||
).await?;
|
||||
|
||||
@@ -25,7 +25,7 @@ where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.patient.list")?;
|
||||
require_permission(&ctx, "health.consent.list")?;
|
||||
let page = params.page.unwrap_or(1);
|
||||
let page_size = params.page_size.unwrap_or(20);
|
||||
let result = consent_service::list_consents(
|
||||
@@ -44,7 +44,9 @@ where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.patient.manage")?;
|
||||
require_permission(&ctx, "health.consent.manage")?;
|
||||
let mut req = req;
|
||||
req.sanitize();
|
||||
let result = consent_service::grant_consent(
|
||||
&state, ctx.tenant_id, Some(ctx.user_id), req,
|
||||
)
|
||||
@@ -62,7 +64,9 @@ where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.patient.manage")?;
|
||||
require_permission(&ctx, "health.consent.manage")?;
|
||||
let mut req = req;
|
||||
req.sanitize();
|
||||
let result = consent_service::revoke_consent(
|
||||
&state, ctx.tenant_id, consent_id, Some(ctx.user_id), req,
|
||||
)
|
||||
|
||||
@@ -149,6 +149,7 @@ where
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.consultation.manage")?;
|
||||
// 从 JWT 身份推导 sender_role,不信任客户端输入
|
||||
let is_doctor = crate::entity::doctor_profile::Entity::find()
|
||||
.filter(crate::entity::doctor_profile::Column::UserId.eq(ctx.user_id))
|
||||
.filter(crate::entity::doctor_profile::Column::TenantId.eq(ctx.tenant_id))
|
||||
@@ -157,16 +158,15 @@ where
|
||||
.await
|
||||
.map_err(|e| AppError::Internal(e.to_string()))?
|
||||
.is_some();
|
||||
let sender_role = if is_doctor { "doctor" } else { "patient" }.to_string();
|
||||
let mut msg_req = CreateMessageReq {
|
||||
session_id: req.session_id,
|
||||
sender_id: ctx.user_id,
|
||||
sender_role: if is_doctor { "doctor" } else { "patient" }.to_string(),
|
||||
content_type: req.content_type,
|
||||
content: req.content,
|
||||
};
|
||||
msg_req.sanitize();
|
||||
let result = consultation_service::create_message(
|
||||
&state, ctx.tenant_id, Some(ctx.user_id), msg_req,
|
||||
&state, ctx.tenant_id, Some(ctx.user_id), ctx.user_id, sender_role, msg_req,
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(ApiResponse::ok(result)))
|
||||
|
||||
@@ -47,6 +47,8 @@ where
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.health-data.manage")?;
|
||||
let mut req = req;
|
||||
req.sanitize();
|
||||
let result = diagnosis_service::create_diagnosis(
|
||||
&state, ctx.tenant_id, patient_id, Some(ctx.user_id), req,
|
||||
)
|
||||
@@ -65,8 +67,10 @@ where
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.health-data.manage")?;
|
||||
let mut data = req.data;
|
||||
data.sanitize();
|
||||
let result = diagnosis_service::update_diagnosis(
|
||||
&state, ctx.tenant_id, diagnosis_id, Some(ctx.user_id), req.data, req.version,
|
||||
&state, ctx.tenant_id, diagnosis_id, Some(ctx.user_id), data, req.version,
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(ApiResponse::ok(result)))
|
||||
|
||||
@@ -27,7 +27,7 @@ where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.health-data.list")?;
|
||||
require_permission(&ctx, "health.medication-records.list")?;
|
||||
let page = params.page.unwrap_or(1);
|
||||
let page_size = params.page_size.unwrap_or(20);
|
||||
let result = medication_record_service::list_medications(
|
||||
@@ -47,7 +47,7 @@ where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.health-data.list")?;
|
||||
require_permission(&ctx, "health.medication-records.list")?;
|
||||
let result =
|
||||
medication_record_service::get_medication(&state, ctx.tenant_id, record_id).await?;
|
||||
Ok(Json(ApiResponse::ok(result)))
|
||||
@@ -63,7 +63,9 @@ where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.health-data.manage")?;
|
||||
require_permission(&ctx, "health.medication-records.manage")?;
|
||||
let mut req = req;
|
||||
req.sanitize();
|
||||
let result = medication_record_service::create_medication(
|
||||
&state,
|
||||
ctx.tenant_id,
|
||||
@@ -85,13 +87,15 @@ where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.health-data.manage")?;
|
||||
require_permission(&ctx, "health.medication-records.manage")?;
|
||||
let mut data = req.data;
|
||||
data.sanitize();
|
||||
let result = medication_record_service::update_medication(
|
||||
&state,
|
||||
ctx.tenant_id,
|
||||
record_id,
|
||||
Some(ctx.user_id),
|
||||
req.data,
|
||||
data,
|
||||
req.version,
|
||||
)
|
||||
.await?;
|
||||
@@ -109,7 +113,7 @@ where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.health-data.manage")?;
|
||||
require_permission(&ctx, "health.medication-records.manage")?;
|
||||
medication_record_service::delete_medication(
|
||||
&state,
|
||||
ctx.tenant_id,
|
||||
|
||||
@@ -318,6 +318,8 @@ pub async fn create_message(
|
||||
state: &HealthState,
|
||||
tenant_id: Uuid,
|
||||
operator_id: Option<Uuid>,
|
||||
sender_id: Uuid,
|
||||
sender_role: String,
|
||||
req: CreateMessageReq,
|
||||
) -> HealthResult<MessageResp> {
|
||||
// 校验会话存在且状态为 active 或 waiting
|
||||
@@ -335,17 +337,12 @@ pub async fn create_message(
|
||||
.ok_or(HealthError::ConsultationNotFound)?;
|
||||
|
||||
let now = Utc::now();
|
||||
validate_sender_role(&req.sender_role)?;
|
||||
validate_sender_role(&sender_role)?;
|
||||
let content_type = req.content_type.unwrap_or_else(|| "text".to_string());
|
||||
validate_content_type(&content_type)?;
|
||||
let is_patient = req.sender_role == "patient";
|
||||
let is_patient = sender_role == "patient";
|
||||
let should_activate = session.status == "waiting";
|
||||
|
||||
// 强制 sender_id 为认证用户,防止冒充
|
||||
let sender_id = operator_id.ok_or_else(|| {
|
||||
HealthError::Validation("sender_id 必须与认证用户匹配".into())
|
||||
})?;
|
||||
|
||||
// 事务包裹:消息 INSERT + 会话 CAS 更新,保证原子性
|
||||
let txn = state.db.begin().await?;
|
||||
|
||||
@@ -355,7 +352,7 @@ pub async fn create_message(
|
||||
tenant_id: Set(tenant_id),
|
||||
session_id: Set(req.session_id),
|
||||
sender_id: Set(sender_id),
|
||||
sender_role: Set(req.sender_role),
|
||||
sender_role: Set(sender_role),
|
||||
content_type: Set(content_type),
|
||||
content: Set(pii::encrypt(state.crypto.kek(), &req.content)?),
|
||||
is_read: Set(false),
|
||||
|
||||
Reference in New Issue
Block a user