feat(auth): 微信登录自动分配 patient 角色 + 创建患者档案
- 新增迁移 m20260510_000133:为所有租户创建 patient 角色并分配 19 个权限 - wechat_service: bind_phone 自动 assign_patient_role + ensure_patient_record - find_or_create_user_by_phone 新用户自动获得 patient 角色和患者档案 - 小程序 auth store: bindPhone 抛出异常而非静默返回 false - 小程序登录页: 捕获绑定错误并显示可操作的对话框
This commit is contained in:
@@ -126,10 +126,18 @@ impl WechatService {
|
||||
encrypted_data: &str,
|
||||
iv: &str,
|
||||
) -> AuthResult<LoginResp> {
|
||||
// 从 Redis 或内存获取 session_key
|
||||
let session_key = Self::get_session_key(&state.redis, openid).await?;
|
||||
|
||||
let phone = decrypt_phone_number(&session_key, encrypted_data, iv)?;
|
||||
// Dev 模式:mock session_key 无法解密真实微信加密数据,直接使用 mock 手机号
|
||||
let phone = if state.wechat_dev_mode {
|
||||
let hash = openid
|
||||
.bytes()
|
||||
.fold(0u32, |acc, b| acc.wrapping_mul(31).wrapping_add(b as u32));
|
||||
let suffix = hash % 10000;
|
||||
tracing::warn!(%openid, mock_phone = format!("1380000{suffix:04}"), "开发模式:跳过手机号解密,使用 mock 手机号");
|
||||
format!("1380000{suffix:04}")
|
||||
} else {
|
||||
let session_key = Self::get_session_key(&state.redis, openid).await?;
|
||||
decrypt_phone_number(&session_key, encrypted_data, iv)?
|
||||
};
|
||||
|
||||
let existing = wechat_user::Entity::find()
|
||||
.filter(wechat_user::Column::Openid.eq(openid))
|
||||
@@ -222,9 +230,106 @@ impl WechatService {
|
||||
.await
|
||||
.map_err(|e| AuthError::DbError(e.to_string()))?;
|
||||
|
||||
// 自动分配 patient 角色
|
||||
Self::assign_patient_role(db, tenant_id, user_id).await?;
|
||||
|
||||
// 自动创建或关联 patient 记录
|
||||
Self::ensure_patient_record(db, tenant_id, user_id, phone).await?;
|
||||
|
||||
Ok(user_id)
|
||||
}
|
||||
|
||||
async fn assign_patient_role(
|
||||
db: &sea_orm::DatabaseConnection,
|
||||
tenant_id: Uuid,
|
||||
user_id: Uuid,
|
||||
) -> AuthResult<()> {
|
||||
use crate::entity::role;
|
||||
use crate::entity::user_role;
|
||||
|
||||
let patient_role = role::Entity::find()
|
||||
.filter(role::Column::Code.eq("patient"))
|
||||
.filter(role::Column::TenantId.eq(tenant_id))
|
||||
.filter(role::Column::DeletedAt.is_null())
|
||||
.one(db)
|
||||
.await
|
||||
.map_err(|e| AuthError::DbError(e.to_string()))?;
|
||||
|
||||
if let Some(r) = patient_role {
|
||||
let now = Utc::now();
|
||||
let ur = user_role::ActiveModel {
|
||||
user_id: Set(user_id),
|
||||
role_id: Set(r.id),
|
||||
tenant_id: Set(tenant_id),
|
||||
created_at: Set(now),
|
||||
updated_at: Set(now),
|
||||
created_by: Set(user_id),
|
||||
updated_by: Set(user_id),
|
||||
deleted_at: Set(None),
|
||||
version: Set(1),
|
||||
};
|
||||
ur.insert(db)
|
||||
.await
|
||||
.map_err(|e| AuthError::DbError(e.to_string()))?;
|
||||
tracing::info!(%user_id, role_id = %r.id, "已为新用户分配 patient 角色");
|
||||
} else {
|
||||
tracing::warn!(%tenant_id, "patient 角色不存在,跳过角色分配");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 自动创建或关联 patient 记录。
|
||||
///
|
||||
/// 1. 如果已有 user_id 关联的 patient → 跳过
|
||||
/// 2. 否则 → 创建新的 patient 记录
|
||||
async fn ensure_patient_record(
|
||||
db: &sea_orm::DatabaseConnection,
|
||||
tenant_id: Uuid,
|
||||
user_id: Uuid,
|
||||
phone: &str,
|
||||
) -> AuthResult<()> {
|
||||
use sea_orm::{ConnectionTrait, Statement};
|
||||
|
||||
// 使用 raw SQL 避免跨 crate 依赖 erp-health 的 entity
|
||||
let result: Option<sea_orm::QueryResult> = db
|
||||
.query_one(Statement::from_sql_and_values(
|
||||
sea_orm::DatabaseBackend::Postgres,
|
||||
"SELECT id FROM patient WHERE user_id = $1 AND tenant_id = $2 AND deleted_at IS NULL LIMIT 1",
|
||||
[user_id.into(), tenant_id.into()],
|
||||
))
|
||||
.await
|
||||
.map_err(|e| AuthError::DbError(e.to_string()))?;
|
||||
|
||||
if result.is_some() {
|
||||
tracing::debug!(%user_id, "patient 记录已存在,跳过创建");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let suffix = &phone[phone.len().saturating_sub(4)..];
|
||||
let patient_id = Uuid::now_v7();
|
||||
let now = Utc::now();
|
||||
|
||||
db.execute(Statement::from_sql_and_values(
|
||||
sea_orm::DatabaseBackend::Postgres,
|
||||
r#"INSERT INTO patient (id, tenant_id, user_id, name, gender, status, verification_status, source, created_at, updated_at, created_by, updated_by, deleted_at, version)
|
||||
VALUES ($1, $2, $3, $4, NULL, 'active', 'pending', 'wechat_miniprogram', $5, $5, $3, $3, NULL, 1)
|
||||
ON CONFLICT DO NOTHING"#,
|
||||
[
|
||||
patient_id.into(),
|
||||
tenant_id.into(),
|
||||
user_id.into(),
|
||||
sanitize_string(&format!("微信用户{}", suffix)).into(),
|
||||
now.into(),
|
||||
],
|
||||
))
|
||||
.await
|
||||
.map_err(|e| AuthError::DbError(e.to_string()))?;
|
||||
|
||||
tracing::info!(%user_id, %patient_id, "已自动创建 patient 记录");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn store_session_key_redis(
|
||||
redis: &Option<redis::Client>,
|
||||
openid: &str,
|
||||
|
||||
Reference in New Issue
Block a user