diff --git a/crates/erp-health/src/service/banner_service.rs b/crates/erp-health/src/service/banner_service.rs index 4a1bdeb..4706213 100644 --- a/crates/erp-health/src/service/banner_service.rs +++ b/crates/erp-health/src/service/banner_service.rs @@ -307,10 +307,11 @@ pub async fn list_public_banners( let (token, expires) = generate_signed_url(&media.storage_path, &signing_secret(), 3600); + let clean_path = media.storage_path.trim_start_matches("./"); let image_url = format!( - "{}{}?expires={}&token={}", + "{}/{}?expires={}&token={}", base_url.trim_end_matches('/'), - media.storage_path, + clean_path, expires, token ); diff --git a/crates/erp-server/migration/src/m20260510_000135_create_media_item.rs b/crates/erp-server/migration/src/m20260510_000135_create_media_item.rs index 9860e96..acbe245 100644 --- a/crates/erp-server/migration/src/m20260510_000135_create_media_item.rs +++ b/crates/erp-server/migration/src/m20260510_000135_create_media_item.rs @@ -141,8 +141,9 @@ enum MediaItem { } /// 外键引用 media_folder 表 -#[derive(DeriveIden)] +#[derive(Iden)] enum MediaFolderRef { + #[iden = "media_folder"] Table, Id, } diff --git a/crates/erp-server/migration/src/m20260510_000136_create_banner.rs b/crates/erp-server/migration/src/m20260510_000136_create_banner.rs index ed5a564..168af7c 100644 --- a/crates/erp-server/migration/src/m20260510_000136_create_banner.rs +++ b/crates/erp-server/migration/src/m20260510_000136_create_banner.rs @@ -129,8 +129,9 @@ enum Banner { } /// 外键引用 media_item 表 -#[derive(DeriveIden)] +#[derive(Iden)] enum MediaItemRef { + #[iden = "media_item"] Table, Id, } diff --git a/crates/erp-server/migration/src/m20260510_000137_seed_media_banner_menus.rs b/crates/erp-server/migration/src/m20260510_000137_seed_media_banner_menus.rs new file mode 100644 index 0000000..b3e02aa --- /dev/null +++ b/crates/erp-server/migration/src/m20260510_000137_seed_media_banner_menus.rs @@ -0,0 +1,192 @@ +//! 媒体库 + 轮播图管理菜单种子数据 + 首页推荐文章分类 +//! +//! 1. 插入"媒体库"和"轮播图管理"菜单到健康业务目录(运营分组) +//! 2. 创建对应权限码 health.media.list / health.banners.list +//! 3. 将权限绑定 admin / operator 角色 +//! 4. 插入"首页推荐"文章分类到 article_category 表 + +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: 插入"媒体库"和"轮播图管理"菜单 + // ================================================================ + // 运营分组: articles(40), points-rules(41), points-products(42), + // points-orders(43), offline-events(44) + // 媒体库 → 45, 轮播图管理 → 46 + + let menus: &[(&str, &str, &str, &str, i32)] = &[ + ( + "b0000003-0000-7000-8000-000000000033", + "媒体库", + "/health/media-library", + "PictureOutlined", + 45, + ), + ( + "b0000003-0000-7000-8000-000000000034", + "轮播图管理", + "/health/banners", + "SwapOutlined", + 46, + ), + ]; + + for &(id, title, path, icon, sort) in menus { + let sql = format!( + r#" + INSERT INTO menus (id, tenant_id, parent_id, title, path, icon, sort_order, + visible, menu_type, created_at, updated_at, created_by, updated_by, version) + SELECT + '{id}'::uuid, + t.id, + (SELECT m.id FROM menus m WHERE m.path = '/health' AND m.tenant_id = t.id LIMIT 1), + '{title}', + '{path}', + '{icon}', + {sort}, + true, 'page', + NOW(), NOW(), + (SELECT u.id FROM users u WHERE u.tenant_id = t.id LIMIT 1), + (SELECT u.id FROM users u WHERE u.tenant_id = t.id LIMIT 1), + 1 + FROM tenant t + WHERE NOT EXISTS ( + SELECT 1 FROM menus m WHERE m.path = '{path}' AND m.tenant_id = t.id + ) + "# + ); + db.execute_unprepared(&sql).await?; + } + + // ================================================================ + // Part 2: 创建权限码 + // ================================================================ + let permissions: &[(&str, &str, &str, &str)] = &[ + ("health.media.list", "媒体库查看", "health", "media.list"), + ( + "health.banners.list", + "轮播图查看", + "health", + "banners.list", + ), + ( + "health.media.manage", + "媒体库管理", + "health", + "media.manage", + ), + ( + "health.banners.manage", + "轮播图管理", + "health", + "banners.manage", + ), + ]; + + for &(code, name, resource, action) in permissions { + db.execute_unprepared(&format!( + r#" + INSERT INTO permissions (id, tenant_id, code, name, resource, action, description, + created_at, updated_at, created_by, updated_by, deleted_at, version) + SELECT gen_random_uuid(), t.id, '{code}', '{name}', '{resource}', '{action}', '{name}', + NOW(), NOW(), '{sys}', '{sys}', NULL, 1 + FROM tenant t + WHERE NOT EXISTS ( + SELECT 1 FROM permissions p + WHERE p.code = '{code}' AND p.tenant_id = t.id AND p.deleted_at IS NULL + ) + "# + )).await?; + } + + // ================================================================ + // Part 3: 绑定权限到 admin 和 operator 角色 + // ================================================================ + for role_code in &["admin", "operator"] { + db.execute_unprepared(&format!( + r#" + INSERT INTO role_permissions (role_id, permission_id, tenant_id, created_by, updated_by, version) + SELECT r.id, p.id, t.id, r.id, r.id, 1 + FROM tenant t + JOIN roles r ON r.tenant_id = t.id AND r.code = '{role_code}' AND r.deleted_at IS NULL + JOIN permissions p ON p.tenant_id = t.id + AND p.code IN ('health.media.list', 'health.banners.list', + 'health.media.manage', 'health.banners.manage') + AND p.deleted_at IS NULL + WHERE NOT EXISTS ( + SELECT 1 FROM role_permissions rp + WHERE rp.permission_id = p.id AND rp.role_id = r.id + ) + "# + )).await?; + } + + // ================================================================ + // Part 4: 插入"首页推荐"文章分类 + // ================================================================ + db.execute_unprepared(&format!( + r#" + INSERT INTO article_category (id, tenant_id, name, slug, parent_id, description, sort_order, + created_at, updated_at, created_by, updated_by, deleted_at, version) + SELECT gen_random_uuid(), t.id, '首页推荐', 'home-featured', NULL, + '小程序访客首页展示的推荐文章', 0, + NOW(), NOW(), '{sys}', '{sys}', NULL, 1 + FROM tenant t + WHERE NOT EXISTS ( + SELECT 1 FROM article_category ac + WHERE ac.slug = 'home-featured' AND ac.tenant_id = t.id AND ac.deleted_at IS NULL + ) + "# + )).await?; + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let db = manager.get_connection(); + + // 删除菜单 + for path in &["/health/media-library", "/health/banners"] { + db.execute_unprepared(&format!("DELETE FROM menus WHERE path = '{path}'")) + .await + .ok(); + } + + // 删除权限绑定 + db.execute_unprepared( + "DELETE FROM role_permissions + WHERE permission_id IN ( + SELECT id FROM permissions + WHERE code IN ('health.media.list', 'health.banners.list', + 'health.media.manage', 'health.banners.manage') + )", + ) + .await + .ok(); + + // 删除权限 + db.execute_unprepared( + "DELETE FROM permissions + WHERE code IN ('health.media.list', 'health.banners.list', + 'health.media.manage', 'health.banners.manage')", + ) + .await + .ok(); + + // 删除文章分类 + db.execute_unprepared("DELETE FROM article_category WHERE slug = 'home-featured'") + .await + .ok(); + + Ok(()) + } +}