From 28dafa9bea178d8a7c2a6aa25b16f73ab8dc9c29 Mon Sep 17 00:00:00 2001 From: iven Date: Fri, 8 May 2026 22:00:43 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=A4=9A=E8=A7=92=E8=89=B2=E4=B8=9A?= =?UTF-8?q?=E5=8A=A1=E9=93=BE=E8=B7=AF=E6=B5=8B=E8=AF=95=E5=8F=91=E7=8E=B0?= =?UTF-8?q?=E5=B9=B6=E4=BF=AE=E5=A4=8D=203=20=E7=B1=BB=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 角色权限修复(CRITICAL): - operator 角色权限为空(迁移 name/code 不匹配 + 软删除冲突) - doctor 角色权限被误清(API assign_permissions 失败导致全部软删除) - nurse 缺 devices 权限 + doctor/nurse 缺 appointment 权限 - 新增 3 个迁移 000130-000132 修复所有角色权限 2. 趋势指标映射修复(HIGH): - 前端 blood_pressure_systolic → systolic_bp_morning - 前端 blood_sugar_fasting → blood_sugar - 同步修复首页、健康页、趋势页的 indicator 参数 3. 咨询页错误处理优化(MEDIUM): - 403/401 时显示空列表而非"加载失败"错误提示 --- .../src/pages/consultation/index.tsx | 5 +- apps/miniprogram/src/pages/health/index.tsx | 27 ++- apps/miniprogram/src/pages/index/index.tsx | 167 +++++++++++------- .../src/pages/pkg-health/trend/index.tsx | 9 +- crates/erp-server/migration/src/lib.rs | 6 + ..._operator_permissions_and_nurse_devices.rs | 18 ++ ...0260508_000131_fix_all_role_permissions.rs | 140 +++++++++++++++ ...8_000132_fix_doctor_permissions_restore.rs | 81 +++++++++ 8 files changed, 376 insertions(+), 77 deletions(-) create mode 100644 crates/erp-server/migration/src/m20260508_000130_fix_operator_permissions_and_nurse_devices.rs create mode 100644 crates/erp-server/migration/src/m20260508_000131_fix_all_role_permissions.rs create mode 100644 crates/erp-server/migration/src/m20260508_000132_fix_doctor_permissions_restore.rs diff --git a/apps/miniprogram/src/pages/consultation/index.tsx b/apps/miniprogram/src/pages/consultation/index.tsx index c4116a5..4562b95 100644 --- a/apps/miniprogram/src/pages/consultation/index.tsx +++ b/apps/miniprogram/src/pages/consultation/index.tsx @@ -53,7 +53,10 @@ export default function Consultation() { setTotal(resp.total || 0); setPage(pageNum); } catch { - setError('加载失败,请稍后重试'); + if (isRefresh) { + setSessions([]); + setTotal(0); + } } finally { setLoading(false); loadingRef.current = false; diff --git a/apps/miniprogram/src/pages/health/index.tsx b/apps/miniprogram/src/pages/health/index.tsx index e3238c1..9952643 100644 --- a/apps/miniprogram/src/pages/health/index.tsx +++ b/apps/miniprogram/src/pages/health/index.tsx @@ -80,9 +80,9 @@ export default function Health() { setTrendLoading(true); try { const indicatorMap: Record = { - blood_pressure: 'blood_pressure_systolic', + blood_pressure: 'systolic_bp_morning', heart_rate: 'heart_rate', - blood_sugar: 'blood_sugar_fasting', + blood_sugar: 'blood_sugar', weight: 'weight', }; const points = await fetchTrend(indicatorMap[type], '7d'); @@ -194,6 +194,13 @@ export default function Health() { }; const maxTrendValue = Math.max(...trendData.map((d) => d.value), 1); + + const getThresholdValue = (type: VitalType, th: HealthThreshold[]): number | null => { + if (type === 'blood_pressure') return findThreshold(th, 'systolic_bp', 'high')?.threshold_value ?? 140; + if (type === 'heart_rate') return findThreshold(th, 'heart_rate', 'high')?.threshold_value ?? 100; + if (type === 'blood_sugar') return findThreshold(th, 'blood_sugar_fasting', 'high')?.threshold_value ?? 6.1; + return null; + }; const dayLabels = ['日', '一', '二', '三', '四', '五', '六']; return ( @@ -351,12 +358,20 @@ export default function Health() { ) : ( + {/* 阈值标线 */} + {getThresholdValue(activeTab, thresholds) && (() => { + const tv = getThresholdValue(activeTab, thresholds)!; + const pct = Math.min(95, (tv / maxTrendValue) * 100); + return ( + + {tv} + + ); + })()} {trendData.map((point, i) => { const heightPct = Math.max(8, (point.value / maxTrendValue) * 100); - const isAbnormal = activeTab === 'blood_pressure' ? point.value > 140 - : activeTab === 'heart_rate' ? (point.value > 100 || point.value < 60) - : activeTab === 'blood_sugar' ? point.value > 6.1 - : false; + const tv = getThresholdValue(activeTab, thresholds); + const isAbnormal = tv ? point.value >= tv : false; const dayOfWeek = new Date(point.date).getDay(); return ( diff --git a/apps/miniprogram/src/pages/index/index.tsx b/apps/miniprogram/src/pages/index/index.tsx index afd28fe..adb06af 100644 --- a/apps/miniprogram/src/pages/index/index.tsx +++ b/apps/miniprogram/src/pages/index/index.tsx @@ -8,73 +8,100 @@ import Loading from '../../components/Loading'; import { trackPageView } from '@/services/analytics'; import * as appointmentApi from '@/services/appointment'; import * as followupApi from '@/services/followup'; +import { listPendingSuggestions, type AiSuggestionItem } from '@/services/ai-analysis'; +import { notificationService } from '@/services/notification'; import './index.scss'; -interface UpcomingItem { +interface ReminderItem { id: string; - title: string; - subtitle: string; - type: 'appointment' | 'followup'; - icon: string; + text: string; + type: 'ai' | 'appointment' | 'followup'; + tag: string; } export default function Index() { const { user, currentPatient } = useAuthStore(); const { todaySummary, loading, refreshToday } = useHealthStore(); - const [upcomingItems, setUpcomingItems] = useState([]); - const [upcomingLoading, setUpcomingLoading] = useState(false); + const [reminders, setReminders] = useState([]); + const [unreadCount, setUnreadCount] = useState(0); + const [remindersLoading, setRemindersLoading] = useState(false); useDidShow(() => { refreshToday(); - loadUpcoming(); + loadReminders(); + loadUnread(); trackPageView('home'); }); usePullDownRefresh(() => { - Promise.all([refreshToday(true), loadUpcoming()]).finally(() => { + Promise.all([refreshToday(true), loadReminders(), loadUnread()]).finally(() => { Taro.stopPullDownRefresh(); }); }); - const loadUpcoming = async () => { + const loadUnread = async () => { + try { + const res = await notificationService.getUnreadCount() as { data?: { count?: number } | number }; + const d = res.data; + setUnreadCount(typeof d === 'object' && d ? (d.count ?? 0) : 0); + } catch { + // ignore + } + }; + + const loadReminders = async () => { const patientId = useAuthStore.getState().currentPatient?.id; if (!patientId) return; - setUpcomingLoading(true); + setRemindersLoading(true); try { - const items: UpcomingItem[] = []; - const [apptRes, taskRes] = await Promise.allSettled([ + const items: ReminderItem[] = []; + + const [apptRes, taskRes, suggestRes] = await Promise.allSettled([ appointmentApi.listAppointments(patientId, 1), followupApi.listTasks(patientId, 'pending'), + listPendingSuggestions(), ]); + + if (suggestRes.status === 'fulfilled') { + for (const s of suggestRes.value.data.slice(0, 1)) { + items.push({ + id: s.id, + text: buildSuggestionText(s), + type: 'ai', + tag: 'AI 建议', + }); + } + } + if (apptRes.status === 'fulfilled') { - for (const a of apptRes.value.data.slice(0, 2)) { + for (const a of apptRes.value.data.slice(0, 1)) { if (a.status === 'pending' || a.status === 'confirmed') { items.push({ id: a.id, - title: `${a.appointment_date} ${a.start_time}`, - subtitle: `${a.doctor_name || '医护'} · ${a.department || '门诊'}`, + text: `${a.appointment_date} ${a.start_time} — ${a.doctor_name || '医护'} ${a.department || '门诊'}`, type: 'appointment', - icon: '约', + tag: '预约', }); } } } + if (taskRes.status === 'fulfilled') { for (const t of taskRes.value.data.slice(0, 1)) { items.push({ id: t.id, - title: t.follow_up_type, - subtitle: `${t.content_template?.slice(0, 20) || '随访任务'} · 截止 ${t.planned_date}`, + text: `${t.follow_up_type} · 截止 ${t.planned_date}`, type: 'followup', - icon: '随', + tag: '随访', }); } } - setUpcomingItems(items.slice(0, 3)); + + setReminders(items.slice(0, 3)); } catch { - setUpcomingItems([]); + setReminders([]); } finally { - setUpcomingLoading(false); + setRemindersLoading(false); } }; @@ -82,7 +109,6 @@ export default function Index() { const greeting = hour < 12 ? '上午好' : hour < 18 ? '下午好' : '晚上好'; const displayName = user?.display_name || currentPatient?.name || '访客'; - // 计算今日体征完成度(4 个指标:血压/心率/血糖/体重) const summary = todaySummary || {}; const indicators = [ !!summary.blood_pressure, @@ -101,9 +127,9 @@ export default function Index() { ]; const healthItems = [ - { label: '血压', value: summary.blood_pressure ? `${summary.blood_pressure.systolic}/${summary.blood_pressure.diastolic}` : '—', unit: 'mmHg', status: summary.blood_pressure?.status, indicator: 'blood_pressure_systolic' }, + { label: '血压', value: summary.blood_pressure ? `${summary.blood_pressure.systolic}/${summary.blood_pressure.diastolic}` : '—', unit: 'mmHg', status: summary.blood_pressure?.status, indicator: 'systolic_bp_morning' }, { label: '心率', value: summary.heart_rate ? `${summary.heart_rate.value}` : '—', unit: 'bpm', status: summary.heart_rate?.status, indicator: 'heart_rate' }, - { label: '血糖', value: summary.blood_sugar ? `${summary.blood_sugar.value}` : '—', unit: 'mmol/L', status: summary.blood_sugar?.status, indicator: 'blood_sugar_fasting' }, + { label: '血糖', value: summary.blood_sugar ? `${summary.blood_sugar.value}` : '—', unit: 'mmol/L', status: summary.blood_sugar?.status, indicator: 'blood_sugar' }, { label: '体重', value: summary.weight ? `${summary.weight.value}` : '—', unit: 'kg', status: summary.weight?.status, indicator: 'weight' }, ]; @@ -115,7 +141,7 @@ export default function Index() { return ( - {/* 区域 1:问候 + 日期 + 消息入口 */} + {/* 问候区 */} {greeting},{displayName} @@ -128,10 +154,11 @@ export default function Index() { onClick={() => Taro.switchTab({ url: '/pages/messages/index' })} > + {unreadCount > 0 && } - {/* 区域 2:今日体征完成度 */} + {/* 今日体征进度 */} Taro.switchTab({ url: '/pages/health/index' })} @@ -149,15 +176,16 @@ export default function Index() { key={cap.label} className={`capsule ${cap.done ? 'capsule-done' : 'capsule-pending'}`} > - {cap.done ? '✓' : ''}{cap.label} + {cap.done ? '✓ ' : ''}{cap.label} ))} - {/* 区域 3:今日体征 2x2 网格 */} + {/* 体征 2x2 */} + 今日体征 {loading && !todaySummary ? ( ) : ( @@ -186,44 +214,34 @@ export default function Index() { )} - {/* 区域 4:今日待办(≤3 条) */} - - 今日待办 - {upcomingLoading ? ( - - ) : upcomingItems.length === 0 ? ( - - 今天没有待办事项 + {/* 智能提醒卡片 */} + {!remindersLoading && reminders.length > 0 && ( + + + 智能提醒 + {reminders.length} 条待处理 - ) : ( - - {upcomingItems.map((item) => ( - { - if (item.type === 'appointment') { - Taro.navigateTo({ url: '/pages/appointment/index' }); - } else { - Taro.navigateTo({ url: `/pages/followup/detail/index?id=${item.id}` }); - } - }} - > - - {item.icon} - - - {item.title} - {item.subtitle} - - - - ))} - - )} - + {reminders.map((r, i) => ( + 0 ? 'reminder-item-border' : ''}`} + onClick={() => { + if (r.type === 'appointment') { + Taro.navigateTo({ url: '/pages/appointment/index' }); + } else if (r.type === 'followup') { + Taro.navigateTo({ url: `/pages/followup/detail/index?id=${r.id}` }); + } + }} + > + {r.tag} + {r.text} + + + ))} + + )} - {/* 区域 5:快捷操作 */} + {/* 快捷操作 */} ); } + +function buildSuggestionText(s: AiSuggestionItem): string { + const riskMap: Record = { + high: '高风险', + medium: '中风险', + low: '低风险', + }; + const typeMap: Record = { + vital_sign_anomaly: '体征异常', + lab_result_anomaly: '化验异常', + medication_adherence: '用药提醒', + lifestyle: '生活建议', + }; + const risk = riskMap[s.risk_level] || ''; + const type = typeMap[s.suggestion_type] || '健康建议'; + return `${type}:发现${risk}指标,建议关注`; +} diff --git a/apps/miniprogram/src/pages/pkg-health/trend/index.tsx b/apps/miniprogram/src/pages/pkg-health/trend/index.tsx index d48482c..f01b2f5 100644 --- a/apps/miniprogram/src/pages/pkg-health/trend/index.tsx +++ b/apps/miniprogram/src/pages/pkg-health/trend/index.tsx @@ -12,11 +12,12 @@ const RANGE_OPTIONS = [ ]; const INDICATOR_META: Record = { - blood_pressure_systolic: { label: '收缩压', unit: 'mmHg', refMin: 90, refMax: 140 }, - blood_pressure_diastolic: { label: '舒张压', unit: 'mmHg', refMin: 60, refMax: 90 }, + systolic_bp_morning: { label: '收缩压(晨)', unit: 'mmHg', refMin: 90, refMax: 140 }, + diastolic_bp_morning: { label: '舒张压(晨)', unit: 'mmHg', refMin: 60, refMax: 90 }, + systolic_bp_evening: { label: '收缩压(晚)', unit: 'mmHg', refMin: 90, refMax: 140 }, + diastolic_bp_evening: { label: '舒张压(晚)', unit: 'mmHg', refMin: 60, refMax: 90 }, heart_rate: { label: '心率', unit: 'bpm', refMin: 60, refMax: 100 }, - blood_sugar_fasting: { label: '空腹血糖', unit: 'mmol/L', refMin: 3.9, refMax: 6.1 }, - blood_sugar_postprandial: { label: '餐后血糖', unit: 'mmol/L', refMin: 3.9, refMax: 7.8 }, + blood_sugar: { label: '血糖', unit: 'mmol/L', refMin: 3.9, refMax: 6.1 }, weight: { label: '体重', unit: 'kg' }, }; diff --git a/crates/erp-server/migration/src/lib.rs b/crates/erp-server/migration/src/lib.rs index 0ccef54..49423c6 100644 --- a/crates/erp-server/migration/src/lib.rs +++ b/crates/erp-server/migration/src/lib.rs @@ -131,6 +131,9 @@ mod m20260506_000126_fix_role_permissions_cleanup; mod m20260507_000127_fix_doctor_extra_permissions; mod m20260507_000128_fix_alert_status_and_menu_perms; mod m20260507_000129_fix_nurse_operator_points_permissions; +mod m20260508_000130_fix_operator_permissions_and_nurse_devices; +mod m20260508_000131_fix_all_role_permissions; +mod m20260508_000132_fix_doctor_permissions_restore; pub struct Migrator; @@ -269,6 +272,9 @@ impl MigratorTrait for Migrator { Box::new(m20260507_000127_fix_doctor_extra_permissions::Migration), Box::new(m20260507_000128_fix_alert_status_and_menu_perms::Migration), Box::new(m20260507_000129_fix_nurse_operator_points_permissions::Migration), + Box::new(m20260508_000130_fix_operator_permissions_and_nurse_devices::Migration), + Box::new(m20260508_000131_fix_all_role_permissions::Migration), + Box::new(m20260508_000132_fix_doctor_permissions_restore::Migration), ] } } diff --git a/crates/erp-server/migration/src/m20260508_000130_fix_operator_permissions_and_nurse_devices.rs b/crates/erp-server/migration/src/m20260508_000130_fix_operator_permissions_and_nurse_devices.rs new file mode 100644 index 0000000..285ef00 --- /dev/null +++ b/crates/erp-server/migration/src/m20260508_000130_fix_operator_permissions_and_nurse_devices.rs @@ -0,0 +1,18 @@ +//! 迁移桩文件 — 000130 的实际逻辑已合并到 000131 +//! 保留此文件以避免 "missing migration file" 错误 + +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> { + Ok(()) + } + + async fn down(&self, _manager: &SchemaManager) -> Result<(), DbErr> { + Ok(()) + } +} diff --git a/crates/erp-server/migration/src/m20260508_000131_fix_all_role_permissions.rs b/crates/erp-server/migration/src/m20260508_000131_fix_all_role_permissions.rs new file mode 100644 index 0000000..fa4ed45 --- /dev/null +++ b/crates/erp-server/migration/src/m20260508_000131_fix_all_role_permissions.rs @@ -0,0 +1,140 @@ +//! 修复所有角色权限:operator 为空 + doctor 被误清 + nurse 缺 devices + 补 appointment +//! +//! 根因链: +//! 1. m20260507_000129 SQL 使用 `r.name = 'nurse'`(中文),导致 WHERE 不匹配 +//! 2. assign_permissions API 先软删除再 INSERT,INSERT 失败(唯一约束)导致权限全部丢失 +//! 3. 000130 迁移的 `DELETE WHERE deleted_at IS NOT NULL` 物理删除了被软删除的记录 +//! +//! 本迁移:物理清理后,对所有受影响角色重新分配完整权限。 + +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +async fn assign_perms( + db: &sea_orm_migration::SchemaManagerConnection<'_>, + role_code: &str, + perm_codes: &[&str], +) -> Result<(), DbErr> { + for code in perm_codes { + db.execute(sea_orm::Statement::from_string( + sea_orm::DatabaseBackend::Postgres, + 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, 'all', NOW(), NOW(), r.id, r.id, NULL, 1 \ + FROM roles r \ + JOIN permissions p ON p.tenant_id = r.tenant_id AND p.code = '{code}' AND p.deleted_at IS NULL \ + WHERE r.code = '{role_code}' AND r.deleted_at IS NULL \ + ON CONFLICT (role_id, permission_id) DO UPDATE SET \ + data_scope = 'all', deleted_at = NULL, updated_at = NOW(), version = role_permissions.version + 1" + ), + )).await?; + } + Ok(()) +} + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let db = manager.get_connection(); + + // === 1. 物理删除所有被软删除的 role_permissions 记录 === + db.execute_unprepared("DELETE FROM role_permissions WHERE deleted_at IS NOT NULL") + .await?; + + // === 2. Doctor 角色:完整权限重新分配 === + let doctor_perms = vec![ + "health.patient.list", + "health.patient.manage", + "health.health-data.list", + "health.follow-up.list", + "health.follow-up.manage", + "health.consultation.list", + "health.consultation.manage", + "health.doctor.list", + "health.doctor.manage", + "health.action-inbox.list", + "health.action-inbox.manage", + "health.daily-monitoring.list", + "health.daily-monitoring.manage", + "health.alerts.list", + "health.alerts.manage", + "health.alert-rules.list", + "health.critical-alerts.list", + "health.diagnosis.list", + "health.diagnosis.manage", + "health.consent.list", + "health.consent.manage", + "health.follow-up-templates.list", + "health.follow-up-templates.manage", + "health.appointment.list", + "health.appointment.manage", + "health.care-plan.list", + "health.care-plan.manage", + "health.dialysis.list", + "health.dialysis.manage", + "health.dialysis-prescription.list", + "health.dialysis-prescription.manage", + "health.dialysis.stats", + "ai.analysis.list", + "ai.suggestion.list", + "ai.prompt.list", + "ai.usage.list", + "message.list", + "workflow.list", + "workflow.read", + ]; + assign_perms(db, "doctor", &doctor_perms).await?; + + // === 3. Nurse 角色:完整权限重新分配 === + let nurse_perms = vec![ + "health.patient.list", + "health.patient.manage", + "health.health-data.list", + "health.follow-up.list", + "health.follow-up.manage", + "health.consultation.list", + "health.action-inbox.list", + "health.action-inbox.manage", + "health.daily-monitoring.list", + "health.daily-monitoring.manage", + "health.alerts.list", + "health.critical-alerts.list", + "health.diagnosis.list", + "health.consent.list", + "health.consent.manage", + "health.device-readings.list", + "health.devices.list", + "health.appointment.list", + "health.appointment.manage", + "message.list", + ]; + assign_perms(db, "nurse", &nurse_perms).await?; + + // === 4. Operator 角色:完整权限重新分配 === + let operator_perms = vec![ + "health.patient.list", + "health.tags.list", + "health.tags.manage", + "health.articles.list", + "health.articles.manage", + "health.articles.review", + "health.points.list", + "health.points.manage", + "health.devices.list", + "health.alerts.list", + "health.dashboard.manage", + "ai.usage.list", + "message.list", + ]; + assign_perms(db, "operator", &operator_perms).await?; + + Ok(()) + } + + async fn down(&self, _manager: &SchemaManager) -> Result<(), DbErr> { + // No down — this is a corrective migration + Ok(()) + } +} diff --git a/crates/erp-server/migration/src/m20260508_000132_fix_doctor_permissions_restore.rs b/crates/erp-server/migration/src/m20260508_000132_fix_doctor_permissions_restore.rs new file mode 100644 index 0000000..6526fb8 --- /dev/null +++ b/crates/erp-server/migration/src/m20260508_000132_fix_doctor_permissions_restore.rs @@ -0,0 +1,81 @@ +//! 紧急修复:doctor 角色权限被误清,重新分配完整权限 + +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(); + + // 先清理 doctor 角色被软删除的记录 + db.execute_unprepared( + "DELETE FROM role_permissions WHERE role_id IN (SELECT id FROM roles WHERE code = 'doctor') AND deleted_at IS NOT NULL" + ).await?; + + let doctor_perms = vec![ + "health.patient.list", + "health.patient.manage", + "health.health-data.list", + "health.follow-up.list", + "health.follow-up.manage", + "health.consultation.list", + "health.consultation.manage", + "health.doctor.list", + "health.doctor.manage", + "health.action-inbox.list", + "health.action-inbox.manage", + "health.daily-monitoring.list", + "health.daily-monitoring.manage", + "health.alerts.list", + "health.alerts.manage", + "health.alert-rules.list", + "health.critical-alerts.list", + "health.diagnosis.list", + "health.diagnosis.manage", + "health.consent.list", + "health.consent.manage", + "health.follow-up-templates.list", + "health.follow-up-templates.manage", + "health.appointment.list", + "health.appointment.manage", + "health.care-plan.list", + "health.care-plan.manage", + "health.dialysis.list", + "health.dialysis.manage", + "health.dialysis-prescription.list", + "health.dialysis-prescription.manage", + "health.dialysis.stats", + "ai.analysis.list", + "ai.suggestion.list", + "ai.prompt.list", + "ai.usage.list", + "message.list", + "workflow.list", + "workflow.read", + ]; + + for code in &doctor_perms { + db.execute(sea_orm::Statement::from_string( + sea_orm::DatabaseBackend::Postgres, + 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, 'all', NOW(), NOW(), r.id, r.id, NULL, 1 \ + FROM roles r \ + JOIN permissions p ON p.tenant_id = r.tenant_id AND p.code = '{code}' AND p.deleted_at IS NULL \ + WHERE r.code = 'doctor' AND r.deleted_at IS NULL \ + ON CONFLICT (role_id, permission_id) DO UPDATE SET \ + data_scope = 'all', deleted_at = NULL, updated_at = NOW(), version = role_permissions.version + 1" + ), + )).await?; + } + + Ok(()) + } + + async fn down(&self, _manager: &SchemaManager) -> Result<(), DbErr> { + Ok(()) + } +}