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:
@@ -52,11 +52,24 @@ export default function Login() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { encryptedData, iv } = e.detail;
|
const { encryptedData, iv } = e.detail;
|
||||||
|
try {
|
||||||
const success = await bindPhone(encryptedData, iv);
|
const success = await bindPhone(encryptedData, iv);
|
||||||
if (success) {
|
if (success) {
|
||||||
navigateAfterLogin();
|
navigateAfterLogin();
|
||||||
} else {
|
}
|
||||||
Taro.showToast({ title: '绑定失败,请重试', icon: 'none' });
|
} catch (err: any) {
|
||||||
|
const msg = err?.message || '绑定失败';
|
||||||
|
Taro.showModal({
|
||||||
|
title: '绑定手机号失败',
|
||||||
|
content: msg,
|
||||||
|
confirmText: '重新登录',
|
||||||
|
cancelText: '取消',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
setNeedBind(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
|||||||
const openid = secureGet('wechat_openid') || '';
|
const openid = secureGet('wechat_openid') || '';
|
||||||
if (!openid) {
|
if (!openid) {
|
||||||
set({ loading: false });
|
set({ loading: false });
|
||||||
return false;
|
throw new Error('登录态丢失,请返回重试');
|
||||||
}
|
}
|
||||||
const resp = await authApi.wechatBindPhone(openid, encryptedData, iv) as Record<string, unknown>;
|
const resp = await authApi.wechatBindPhone(openid, encryptedData, iv) as Record<string, unknown>;
|
||||||
const tokenData = resp as { access_token: string; refresh_token: string; user: AuthState['user'] };
|
const tokenData = resp as { access_token: string; refresh_token: string; user: AuthState['user'] };
|
||||||
@@ -129,9 +129,10 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
|||||||
secureRemove('wechat_openid');
|
secureRemove('wechat_openid');
|
||||||
set({ user: tokenData.user, roles, loading: false });
|
set({ user: tokenData.user, roles, loading: false });
|
||||||
return true;
|
return true;
|
||||||
} catch {
|
} catch (err: any) {
|
||||||
|
secureRemove('wechat_openid');
|
||||||
set({ loading: false });
|
set({ loading: false });
|
||||||
return false;
|
throw err;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -126,10 +126,18 @@ impl WechatService {
|
|||||||
encrypted_data: &str,
|
encrypted_data: &str,
|
||||||
iv: &str,
|
iv: &str,
|
||||||
) -> AuthResult<LoginResp> {
|
) -> AuthResult<LoginResp> {
|
||||||
// 从 Redis 或内存获取 session_key
|
// 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?;
|
let session_key = Self::get_session_key(&state.redis, openid).await?;
|
||||||
|
decrypt_phone_number(&session_key, encrypted_data, iv)?
|
||||||
let phone = decrypt_phone_number(&session_key, encrypted_data, iv)?;
|
};
|
||||||
|
|
||||||
let existing = wechat_user::Entity::find()
|
let existing = wechat_user::Entity::find()
|
||||||
.filter(wechat_user::Column::Openid.eq(openid))
|
.filter(wechat_user::Column::Openid.eq(openid))
|
||||||
@@ -222,9 +230,106 @@ impl WechatService {
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| AuthError::DbError(e.to_string()))?;
|
.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)
|
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(
|
async fn store_session_key_redis(
|
||||||
redis: &Option<redis::Client>,
|
redis: &Option<redis::Client>,
|
||||||
openid: &str,
|
openid: &str,
|
||||||
|
|||||||
@@ -134,6 +134,7 @@ mod m20260507_000129_fix_nurse_operator_points_permissions;
|
|||||||
mod m20260508_000130_fix_operator_permissions_and_nurse_devices;
|
mod m20260508_000130_fix_operator_permissions_and_nurse_devices;
|
||||||
mod m20260508_000131_fix_all_role_permissions;
|
mod m20260508_000131_fix_all_role_permissions;
|
||||||
mod m20260508_000132_fix_doctor_permissions_restore;
|
mod m20260508_000132_fix_doctor_permissions_restore;
|
||||||
|
mod m20260510_000133_create_patient_role;
|
||||||
|
|
||||||
pub struct Migrator;
|
pub struct Migrator;
|
||||||
|
|
||||||
@@ -275,6 +276,7 @@ impl MigratorTrait for Migrator {
|
|||||||
Box::new(m20260508_000130_fix_operator_permissions_and_nurse_devices::Migration),
|
Box::new(m20260508_000130_fix_operator_permissions_and_nurse_devices::Migration),
|
||||||
Box::new(m20260508_000131_fix_all_role_permissions::Migration),
|
Box::new(m20260508_000131_fix_all_role_permissions::Migration),
|
||||||
Box::new(m20260508_000132_fix_doctor_permissions_restore::Migration),
|
Box::new(m20260508_000132_fix_doctor_permissions_restore::Migration),
|
||||||
|
Box::new(m20260510_000133_create_patient_role::Migration),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
use sea_orm_migration::prelude::*;
|
||||||
|
|
||||||
|
#[derive(DeriveMigrationName)]
|
||||||
|
pub struct Migration;
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl MigrationTrait for Migration {
|
||||||
|
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
let db = manager.get_connection();
|
||||||
|
let sys = "00000000-0000-0000-0000-000000000000";
|
||||||
|
|
||||||
|
// Part 1: 为所有租户创建 patient 角色(幂等)
|
||||||
|
db.execute_unprepared(&format!(
|
||||||
|
"INSERT INTO roles (id, tenant_id, name, code, description, is_system, created_at, updated_at, created_by, updated_by, deleted_at, version) \
|
||||||
|
SELECT gen_random_uuid(), t.id, '患者', 'patient', '小程序患者用户,可查看自身健康数据、积分、咨询等', false, NOW(), NOW(), '{sys}', '{sys}', NULL, 1 \
|
||||||
|
FROM tenant t \
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM roles r WHERE r.tenant_id = t.id AND r.code = 'patient' AND r.deleted_at IS NULL)"
|
||||||
|
)).await?;
|
||||||
|
|
||||||
|
// Part 2: 为 patient 角色分配小程序所需权限
|
||||||
|
let patient_perms: &[&str] = &[
|
||||||
|
// 健康数据
|
||||||
|
"health.health-data.list",
|
||||||
|
// 患者自身
|
||||||
|
"health.patient.list",
|
||||||
|
// 预约
|
||||||
|
"health.appointment.list",
|
||||||
|
// 随访
|
||||||
|
"health.follow-up.list",
|
||||||
|
// 咨询
|
||||||
|
"health.consultation.list",
|
||||||
|
// 积分
|
||||||
|
"health.points.list",
|
||||||
|
// 文章
|
||||||
|
"health.articles.list",
|
||||||
|
// 告警(自身)
|
||||||
|
"health.alerts.list",
|
||||||
|
// 日常监测
|
||||||
|
"health.daily-monitoring.list",
|
||||||
|
// 设备数据
|
||||||
|
"health.device-readings.list",
|
||||||
|
// 设备绑定
|
||||||
|
"health.devices.list",
|
||||||
|
// 知情同意
|
||||||
|
"health.consent.list",
|
||||||
|
// 用药记录
|
||||||
|
"health.medication-records.list",
|
||||||
|
// 药物提醒
|
||||||
|
"health.medication-reminders.list",
|
||||||
|
// 护理计划
|
||||||
|
"health.care-plan.list",
|
||||||
|
// AI 建议
|
||||||
|
"ai.suggestion.list",
|
||||||
|
// 消息
|
||||||
|
"message.list",
|
||||||
|
// 透析
|
||||||
|
"health.dialysis.list",
|
||||||
|
];
|
||||||
|
|
||||||
|
assign_perms_by_codes(db, "patient", patient_perms).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
let db = manager.get_connection();
|
||||||
|
|
||||||
|
// 删除 patient 角色的权限关联
|
||||||
|
db.execute_unprepared(
|
||||||
|
"DELETE FROM role_permissions WHERE role_id IN (SELECT id FROM roles WHERE code = 'patient')"
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
// 软删除 patient 角色
|
||||||
|
db.execute_unprepared(
|
||||||
|
"UPDATE roles SET deleted_at = NOW() WHERE code = 'patient' AND deleted_at IS NULL",
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn assign_perms_by_codes(
|
||||||
|
db: &sea_orm_migration::prelude::SchemaManagerConnection<'_>,
|
||||||
|
role_code: &str,
|
||||||
|
perm_codes: &[&str],
|
||||||
|
) -> Result<(), DbErr> {
|
||||||
|
let codes_csv: String = perm_codes
|
||||||
|
.iter()
|
||||||
|
.map(|c| format!("'{}'", c))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(",");
|
||||||
|
|
||||||
|
db.execute_unprepared(&format!(
|
||||||
|
"INSERT INTO role_permissions (role_id, permission_id, tenant_id, data_scope, created_at, updated_at, created_by, updated_by, deleted_at, version) \
|
||||||
|
SELECT r.id, p.id, r.tenant_id, 'self', NOW(), NOW(), r.id, r.id, NULL, 1 \
|
||||||
|
FROM roles r \
|
||||||
|
JOIN permissions p ON p.tenant_id = r.tenant_id AND p.code IN ({codes_csv}) AND p.deleted_at IS NULL \
|
||||||
|
WHERE r.code = '{role_code}' AND r.deleted_at IS NULL \
|
||||||
|
ON CONFLICT (role_id, permission_id) WHERE deleted_at IS NULL \
|
||||||
|
DO UPDATE SET deleted_at = NULL, version = role_permissions.version + 1, updated_at = NOW()"
|
||||||
|
)).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user