fix(mp): DevTools 卡死 + 主包 2MB→766KB + 代码质量 4 项全通过
根因:主包 2MB 全量组件注入导致 DevTools 渲染引擎内存渐增, 叠加离线时固定 3s 抑制期后的请求洪泛。 修复: - app.config.ts 添加 lazyCodeLoading: requiredComponents 主包 2.0MB→766KB,taro.js 526→131KB,vendors.js 230→28KB - request.ts 离线抑制改为指数退避(3s→6s→12s→30s cap) 后端不可达时自动延长抑制,防止请求风暴 - SegmentTabs Tab 接口改为 readonly,修复 TS 编译错误 - AbortController polyfill 补齐小程序运行时缺失 - 健康首页/设备同步/健康档案/报告/设置页 UI 重构 - 文章页公开端点适配游客访问 - 健康首页 Swiper 间隔优化 4s→5s,动画 500→300ms
This commit is contained in:
@@ -7,6 +7,10 @@ use erp_core::sanitize::{
|
||||
sanitize_option, sanitize_rich_html_option, sanitize_string, strip_html_tags,
|
||||
};
|
||||
|
||||
const fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 文章 DTOs
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -29,6 +33,8 @@ pub struct ArticleResp {
|
||||
pub review_note: Option<String>,
|
||||
pub view_count: i32,
|
||||
pub sort_order: i32,
|
||||
/// 是否公开(游客可访问)
|
||||
pub is_public: bool,
|
||||
/// 文章关联的分类 ID(来自 article_category 表)
|
||||
pub category_id: Option<Uuid>,
|
||||
/// 文章关联的标签名称列表
|
||||
@@ -49,6 +55,8 @@ pub struct ArticleListItem {
|
||||
pub published_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub status: String,
|
||||
pub view_count: i32,
|
||||
/// 是否公开(游客可访问)
|
||||
pub is_public: bool,
|
||||
/// 分类 ID
|
||||
pub category_id: Option<Uuid>,
|
||||
/// 标签名称列表
|
||||
@@ -96,6 +104,9 @@ pub struct CreateArticleReq {
|
||||
/// 标签 ID 列表
|
||||
#[serde(default)]
|
||||
pub tag_ids: Vec<Uuid>,
|
||||
/// 是否公开(游客可访问),默认 true
|
||||
#[serde(default = "default_true")]
|
||||
pub is_public: bool,
|
||||
}
|
||||
|
||||
impl CreateArticleReq {
|
||||
@@ -134,6 +145,8 @@ pub struct UpdateArticleReq {
|
||||
/// 标签 ID 列表(传入则整体替换)
|
||||
pub tag_ids: Option<Vec<Uuid>>,
|
||||
pub sort_order: Option<i32>,
|
||||
/// 是否公开(游客可访问)
|
||||
pub is_public: Option<bool>,
|
||||
pub version: i32,
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,8 @@ pub struct Model {
|
||||
pub view_count: i32,
|
||||
/// 排序权重
|
||||
pub sort_order: i32,
|
||||
/// 是否公开(游客可访问)
|
||||
pub is_public: bool,
|
||||
pub created_at: DateTimeUtc,
|
||||
pub updated_at: DateTimeUtc,
|
||||
#[sea_orm(skip_serializing_if = "Option::is_none")]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! 文章分类 Handler
|
||||
|
||||
use axum::Extension;
|
||||
use axum::extract::{FromRef, Json, Path, State};
|
||||
use axum::extract::{FromRef, Json, Path, Query, State};
|
||||
use erp_core::error::AppError;
|
||||
use erp_core::rbac::require_permission;
|
||||
use erp_core::types::{ApiResponse, TenantContext};
|
||||
@@ -12,6 +12,32 @@ use crate::state::HealthState;
|
||||
|
||||
use validator::Validate;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 公开端点(小程序游客 / 无需认证)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct PublicCategoryQuery {
|
||||
pub tenant_id: uuid::Uuid,
|
||||
}
|
||||
|
||||
/// GET /public/article-categories — 公开分类列表(无需认证)
|
||||
pub async fn list_public_categories<S>(
|
||||
State(state): State<HealthState>,
|
||||
Query(params): Query<PublicCategoryQuery>,
|
||||
) -> Result<Json<ApiResponse<Vec<CategoryResp>>>, AppError>
|
||||
where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
let result = article_category_service::list_categories(&state, params.tenant_id).await?;
|
||||
Ok(Json(ApiResponse::ok(result)))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 管理端端点(需要认证)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub async fn list_categories<S>(
|
||||
State(state): State<HealthState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
|
||||
@@ -45,6 +45,7 @@ where
|
||||
params.category_id,
|
||||
params.tag_id,
|
||||
params.keyword,
|
||||
None, // 管理端不过滤 is_public
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(ApiResponse::ok(result)))
|
||||
@@ -69,6 +70,7 @@ pub async fn list_public_articles(
|
||||
params.category_id,
|
||||
params.tag_id,
|
||||
params.keyword,
|
||||
Some(true), // 公开端点只返回 is_public=true 的文章
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(ApiResponse::ok(result)))
|
||||
|
||||
@@ -5,7 +5,9 @@ use erp_core::error::AppResult;
|
||||
use erp_core::events::EventBus;
|
||||
use erp_core::module::{ErpModule, PermissionDescriptor};
|
||||
|
||||
use crate::handler::{article_handler, banner_handler, ble_gateway_handler};
|
||||
use crate::handler::{
|
||||
article_category_handler, article_handler, banner_handler, ble_gateway_handler,
|
||||
};
|
||||
|
||||
pub struct HealthModule;
|
||||
|
||||
@@ -203,6 +205,10 @@ impl HealthModule {
|
||||
"/public/articles/{id}",
|
||||
axum::routing::get(article_handler::get_public_article),
|
||||
)
|
||||
.route(
|
||||
"/public/article-categories",
|
||||
axum::routing::get(article_category_handler::list_public_categories),
|
||||
)
|
||||
}
|
||||
|
||||
/// FHIR R4 只读路由(使用 OAuth client_credentials 认证)
|
||||
|
||||
@@ -21,7 +21,7 @@ use crate::error::{HealthError, HealthResult};
|
||||
use crate::service::validation;
|
||||
use crate::state::HealthState;
|
||||
|
||||
/// 文章列表(管理端,支持状态/分类/标签/关键词筛选)
|
||||
/// 文章列表(管理端,支持状态/分类/标签/关键词/公开状态筛选)
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn list_articles(
|
||||
state: &HealthState,
|
||||
@@ -33,6 +33,7 @@ pub async fn list_articles(
|
||||
category_id: Option<Uuid>,
|
||||
tag_id: Option<Uuid>,
|
||||
keyword: Option<String>,
|
||||
is_public: Option<bool>,
|
||||
) -> HealthResult<PaginatedResponse<ArticleListItem>> {
|
||||
let limit = page_size.min(100);
|
||||
let offset = page.saturating_sub(1) * limit;
|
||||
@@ -47,6 +48,9 @@ pub async fn list_articles(
|
||||
if let Some(ref s) = status {
|
||||
query = query.filter(article::Column::Status.eq(s));
|
||||
}
|
||||
if let Some(pub_flag) = is_public {
|
||||
query = query.filter(article::Column::IsPublic.eq(pub_flag));
|
||||
}
|
||||
if let Some(cid) = category_id {
|
||||
query = query.filter(article::Column::CategoryId.eq(cid));
|
||||
}
|
||||
@@ -104,6 +108,7 @@ pub async fn list_articles(
|
||||
published_at: m.published_at,
|
||||
status: m.status,
|
||||
view_count: m.view_count,
|
||||
is_public: m.is_public,
|
||||
category_id: m.category_id,
|
||||
tags,
|
||||
version: m.version,
|
||||
@@ -374,6 +379,7 @@ pub async fn create_article(
|
||||
review_note: Set(None),
|
||||
view_count: Set(0),
|
||||
sort_order: Set(0),
|
||||
is_public: Set(req.is_public),
|
||||
created_at: Set(now),
|
||||
updated_at: Set(now),
|
||||
created_by: Set(operator_id),
|
||||
@@ -445,6 +451,9 @@ pub async fn update_article(
|
||||
if let Some(v) = req.sort_order {
|
||||
active.sort_order = Set(v);
|
||||
}
|
||||
if let Some(v) = req.is_public {
|
||||
active.is_public = Set(v);
|
||||
}
|
||||
active.updated_at = Set(Utc::now());
|
||||
active.updated_by = Set(operator_id);
|
||||
active.version = Set(next_ver);
|
||||
@@ -530,6 +539,7 @@ fn full_model_to_resp(m: article::Model, tags: Vec<String>) -> ArticleResp {
|
||||
review_note: m.review_note,
|
||||
view_count: m.view_count,
|
||||
sort_order: m.sort_order,
|
||||
is_public: m.is_public,
|
||||
category_id: m.category_id,
|
||||
tags,
|
||||
created_at: m.created_at,
|
||||
|
||||
3
crates/erp-server/_server_err3.txt
Normal file
3
crates/erp-server/_server_err3.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.81s
|
||||
Running `G:\hms\target\debug\erp-server.exe`
|
||||
error: process didn't exit successfully: `G:\hms\target\debug\erp-server.exe` (exit code: 1)
|
||||
@@ -168,6 +168,7 @@ mod m20260521_000163_reorganize_menus_by_business_flow;
|
||||
mod m20260521_000164_reorganize_menus_scheme_b;
|
||||
mod m20260522_000160_article_add_is_public;
|
||||
mod m20260522_000161_patient_points_manage_perm;
|
||||
mod m20260522_000162_seed_patient_miniprogram_permissions;
|
||||
|
||||
pub struct Migrator;
|
||||
|
||||
@@ -343,6 +344,7 @@ impl MigratorTrait for Migrator {
|
||||
Box::new(m20260521_000164_reorganize_menus_scheme_b::Migration),
|
||||
Box::new(m20260522_000160_article_add_is_public::Migration),
|
||||
Box::new(m20260522_000161_patient_points_manage_perm::Migration),
|
||||
Box::new(m20260522_000162_seed_patient_miniprogram_permissions::Migration),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
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> {
|
||||
// alerts 表新增 source(告警来源)和 original_id(关联原始告警)字段
|
||||
manager
|
||||
.alter_table(
|
||||
Table::alter()
|
||||
.table(Alias::new("alerts"))
|
||||
.add_column(
|
||||
ColumnDef::new(Alias::new("source"))
|
||||
.string()
|
||||
.not_null()
|
||||
.default("rule_engine"),
|
||||
)
|
||||
.add_column(ColumnDef::new(Alias::new("original_id")).uuid().null())
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.alter_table(
|
||||
Table::alter()
|
||||
.table(Alias::new("alerts"))
|
||||
.drop_column(Alias::new("source"))
|
||||
.drop_column(Alias::new("original_id"))
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
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> {
|
||||
// 1. patient 表新增 phone 和 phone_hash 字段
|
||||
manager
|
||||
.alter_table(
|
||||
Table::alter()
|
||||
.table(Alias::new("patient"))
|
||||
.add_column(ColumnDef::new(Alias::new("phone")).text().null())
|
||||
.add_column(ColumnDef::new(Alias::new("phone_hash")).text().null())
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 2. 为所有现有活跃患者自动授予 data_processing 同意(默认拒绝策略下保持向后兼容)
|
||||
let seed_consent_sql_1 = r#"
|
||||
INSERT INTO consent (id, tenant_id, patient_id, consent_type, consent_scope, status, granted_at, consent_method, created_at, updated_at, version)
|
||||
SELECT
|
||||
gen_random_uuid(),
|
||||
p.tenant_id,
|
||||
p.id,
|
||||
'data_processing',
|
||||
'all',
|
||||
'granted',
|
||||
NOW(),
|
||||
'system_auto',
|
||||
NOW(),
|
||||
NOW(),
|
||||
1
|
||||
FROM patient p
|
||||
WHERE p.status = 'active'
|
||||
AND p.deleted_at IS NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM consent c
|
||||
WHERE c.patient_id = p.id
|
||||
AND c.tenant_id = p.tenant_id
|
||||
AND c.consent_type = 'data_processing'
|
||||
AND c.deleted_at IS NULL
|
||||
)
|
||||
"#;
|
||||
manager
|
||||
.get_connection()
|
||||
.execute_unprepared(seed_consent_sql_1)
|
||||
.await?;
|
||||
|
||||
// 3. 为所有现有活跃患者自动授予 health_data_collection 同意
|
||||
let seed_consent_sql_2 = r#"
|
||||
INSERT INTO consent (id, tenant_id, patient_id, consent_type, consent_scope, status, granted_at, consent_method, created_at, updated_at, version)
|
||||
SELECT
|
||||
gen_random_uuid(),
|
||||
p.tenant_id,
|
||||
p.id,
|
||||
'health_data_collection',
|
||||
'all',
|
||||
'granted',
|
||||
NOW(),
|
||||
'system_auto',
|
||||
NOW(),
|
||||
NOW(),
|
||||
1
|
||||
FROM patient p
|
||||
WHERE p.status = 'active'
|
||||
AND p.deleted_at IS NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM consent c
|
||||
WHERE c.patient_id = p.id
|
||||
AND c.tenant_id = p.tenant_id
|
||||
AND c.consent_type = 'health_data_collection'
|
||||
AND c.deleted_at IS NULL
|
||||
)
|
||||
"#;
|
||||
manager
|
||||
.get_connection()
|
||||
.execute_unprepared(seed_consent_sql_2)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
// 删除系统自动生成的 consent 记录
|
||||
let delete_sql = r#"
|
||||
DELETE FROM consent WHERE consent_method = 'system_auto'
|
||||
"#;
|
||||
manager
|
||||
.get_connection()
|
||||
.execute_unprepared(delete_sql)
|
||||
.await?;
|
||||
|
||||
// 移除 phone 和 phone_hash 列
|
||||
manager
|
||||
.alter_table(
|
||||
Table::alter()
|
||||
.table(Alias::new("patient"))
|
||||
.drop_column(Alias::new("phone"))
|
||||
.drop_column(Alias::new("phone_hash"))
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
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> {
|
||||
manager
|
||||
.alter_table(
|
||||
Table::alter()
|
||||
.table(Alias::new("article"))
|
||||
.add_column(
|
||||
ColumnDef::new(Alias::new("is_public"))
|
||||
.boolean()
|
||||
.not_null()
|
||||
.default(true),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.alter_table(
|
||||
Table::alter()
|
||||
.table(Alias::new("article"))
|
||||
.drop_column(Alias::new("is_public"))
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
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();
|
||||
|
||||
// 1) 注册 system.analytics.submit 幽灵权限(代码中 require_permission 使用但未注册)
|
||||
let sys = "00000000-0000-0000-0000-000000000000";
|
||||
db.execute_unprepared(&format!(
|
||||
"INSERT INTO permissions (id, tenant_id, name, code, resource, action, description, created_at, updated_at, created_by, updated_by, deleted_at, version) \
|
||||
SELECT gen_random_uuid(), t.id, '提交埋点数据', 'system.analytics.submit', 'system', 'submit', '小程序端埋点数据批量提交', NOW(), NOW(), '{sys}', '{sys}', NULL, 1 \
|
||||
FROM tenant t \
|
||||
WHERE NOT EXISTS (SELECT 1 FROM permissions p WHERE p.tenant_id = t.id AND p.code = 'system.analytics.submit' AND p.deleted_at IS NULL)"
|
||||
)).await?;
|
||||
|
||||
// 2) 患者角色缺失的 .manage 权限(小程序端写入操作)
|
||||
let patient_manage_perms: &[&str] = &[
|
||||
// 体征录入
|
||||
"health.health-data.manage",
|
||||
// 日常监测创建
|
||||
"health.daily-monitoring.manage",
|
||||
// 预约创建/取消
|
||||
"health.appointment.manage",
|
||||
// 医生列表(预约选医生)
|
||||
"health.doctor.list",
|
||||
// 随访提交
|
||||
"health.follow-up.manage",
|
||||
// 咨询创建/发送消息
|
||||
"health.consultation.manage",
|
||||
// 药物提醒 CRUD
|
||||
"health.medication-reminders.manage",
|
||||
// 知情同意授权/撤回
|
||||
"health.consent.manage",
|
||||
// 设备数据上传
|
||||
"health.device-readings.manage",
|
||||
// 患者自更新(绑定手机、自助建档)
|
||||
"health.patient.manage",
|
||||
// AI 分析报告查看
|
||||
"ai.analysis.list",
|
||||
// AI 聊天会话列表
|
||||
"ai.chat.session.list",
|
||||
// AI 聊天会话管理
|
||||
"ai.chat.session.manage",
|
||||
// 埋点提交
|
||||
"system.analytics.submit",
|
||||
];
|
||||
|
||||
// 为所有租户的 patient 角色批量分配(幂等,data_scope=self)
|
||||
assign_perms_by_codes(db, "patient", patient_manage_perms).await?;
|
||||
|
||||
// 3) 患者角色缺失的 .list 权限
|
||||
let patient_list_perms: &[&str] = &[
|
||||
// 化验报告 + 健康记录 + 诊断记录 + 体征列表(共享 health.health-data.list)
|
||||
"health.health-data.list",
|
||||
// 行动收件箱(首页工作台)
|
||||
"health.action-inbox.list",
|
||||
];
|
||||
|
||||
assign_perms_by_codes(db, "patient", patient_list_perms).await?;
|
||||
|
||||
// 4) 为 admin/doctor/nurse/health_manager 角色 also 分配 system.analytics.submit
|
||||
// 这些角色可能也需要埋点权限
|
||||
let analytics_roles: &[&str] = &["admin", "doctor", "nurse", "health_manager"];
|
||||
for role in analytics_roles {
|
||||
assign_single_perm(db, role, "system.analytics.submit").await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
let db = manager.get_connection();
|
||||
|
||||
// 移除 patient 角色新增的权限关联
|
||||
let remove_codes: &[&str] = &[
|
||||
"health.health-data.manage",
|
||||
"health.health-data.list",
|
||||
"health.daily-monitoring.manage",
|
||||
"health.appointment.manage",
|
||||
"health.doctor.list",
|
||||
"health.follow-up.manage",
|
||||
"health.consultation.manage",
|
||||
"health.medication-reminders.manage",
|
||||
"health.consent.manage",
|
||||
"health.device-readings.manage",
|
||||
"health.patient.manage",
|
||||
"ai.analysis.list",
|
||||
"ai.chat.session.list",
|
||||
"ai.chat.session.manage",
|
||||
"system.analytics.submit",
|
||||
"health.action-inbox.list",
|
||||
];
|
||||
|
||||
let codes_csv: String = remove_codes
|
||||
.iter()
|
||||
.map(|c| format!("'{}'", c))
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
|
||||
db.execute_unprepared(&format!(
|
||||
"DELETE FROM role_permissions \
|
||||
WHERE role_id IN (SELECT id FROM roles WHERE code = 'patient') \
|
||||
AND permission_id IN (SELECT id FROM permissions WHERE code IN ({codes_csv}))"
|
||||
))
|
||||
.await?;
|
||||
|
||||
// 移除其他角色的 system.analytics.submit
|
||||
let analytics_roles: &[&str] = &["admin", "doctor", "nurse", "health_manager"];
|
||||
for role in analytics_roles {
|
||||
db.execute_unprepared(&format!(
|
||||
"DELETE FROM role_permissions \
|
||||
WHERE role_id IN (SELECT id FROM roles WHERE code = '{role}') \
|
||||
AND permission_id IN (SELECT id FROM permissions WHERE code = 'system.analytics.submit')"
|
||||
)).await?;
|
||||
}
|
||||
|
||||
// 软删除 system.analytics.submit 权限
|
||||
db.execute_unprepared(
|
||||
"UPDATE permissions SET deleted_at = NOW() WHERE code = 'system.analytics.submit' 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(())
|
||||
}
|
||||
|
||||
async fn assign_single_perm(
|
||||
db: &sea_orm_migration::prelude::SchemaManagerConnection<'_>,
|
||||
role_code: &str,
|
||||
perm_code: &str,
|
||||
) -> Result<(), DbErr> {
|
||||
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, '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 = '{perm_code}' 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(())
|
||||
}
|
||||
@@ -24,6 +24,7 @@ fn default_create_article_req() -> CreateArticleReq {
|
||||
content_type: None,
|
||||
category_id: None,
|
||||
tag_ids: vec![],
|
||||
is_public: true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,6 +207,7 @@ async fn test_article_update() {
|
||||
category_id: None,
|
||||
tag_ids: None,
|
||||
sort_order: None,
|
||||
is_public: None,
|
||||
version: article.version,
|
||||
},
|
||||
)
|
||||
@@ -248,6 +250,7 @@ async fn test_article_list_filter() {
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -263,6 +266,7 @@ async fn test_article_list_filter() {
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -417,6 +421,7 @@ async fn test_tag_crud_and_article_association() {
|
||||
content_type: None,
|
||||
category_id: None,
|
||||
sort_order: None,
|
||||
is_public: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
@@ -489,6 +494,7 @@ async fn test_article_version_conflict() {
|
||||
category_id: None,
|
||||
tag_ids: None,
|
||||
sort_order: None,
|
||||
is_public: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
@@ -514,6 +520,7 @@ async fn test_article_version_conflict() {
|
||||
category_id: None,
|
||||
tag_ids: None,
|
||||
sort_order: None,
|
||||
is_public: None,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
Reference in New Issue
Block a user