From a9efcd55a7960d5e6ad4c74f290ba20eeffe237b Mon Sep 17 00:00:00 2001 From: Zhanghu Date: Tue, 13 Jan 2026 13:50:27 +0800 Subject: [PATCH] initial --- .gitignore | 8 + README.md | 175 ++++++ src/DRS9.Dashboard.Application/Class1.cs | 6 + .../DRS9.Dashboard.Application.csproj | 19 + .../DTOs/AppVersionDto.cs | 64 +++ .../DTOs/ApplicationDto.cs | 76 +++ .../DTOs/BatchManagementDto.cs | 63 +++ .../DTOs/DeviceAuthDto.cs | 43 ++ .../DTOs/DeviceManagementDto.cs | 136 +++++ .../DTOs/GuestAccessDto.cs | 31 ++ .../DTOs/PlaylistDto.cs | 114 ++++ .../Services/AppVersionService.cs | 198 +++++++ .../Services/ApplicationService.cs | 131 +++++ .../Services/BatchManagementService.cs | 237 ++++++++ .../Services/DashboardWebSocketManager.cs | 118 ++++ .../Services/DeviceManagementService.cs | 310 +++++++++++ .../Services/DeviceService.cs | 84 +++ .../Services/GuestAccessService.cs | 66 +++ .../Services/JwtTokenService.cs | 72 +++ .../Services/PlaylistService.cs | 288 ++++++++++ .../Services/WebSocketConnection.cs | 12 + .../DRS9.Dashboard.Domain.csproj | 9 + .../Entities/AppVersion.cs | 29 + .../Entities/Application.cs | 28 + .../Entities/AuditLog.cs | 22 + .../Entities/BaseEntity.cs | 8 + src/DRS9.Dashboard.Domain/Entities/Device.cs | 36 ++ .../Entities/DeviceAssignment.cs | 18 + .../Entities/DeviceGroup.cs | 15 + .../Entities/Playlist.cs | 57 ++ src/DRS9.Dashboard.Domain/Entities/User.cs | 20 + src/DRS9.Dashboard.Infrastructure/Class1.cs | 6 + .../DRS9.Dashboard.Infrastructure.csproj | 18 + .../Data/DashboardDbContext.cs | 105 ++++ .../Data/DataSeeder.cs | 44 ++ .../20260112082557_InitialCreate.Designer.cs | 325 +++++++++++ .../20260112082557_InitialCreate.cs | 218 ++++++++ .../20260112091958_AddPlaylists.Designer.cs | 439 +++++++++++++++ .../Migrations/20260112091958_AddPlaylists.cs | 102 ++++ .../20260112092747_AddAppVersions.Designer.cs | 493 +++++++++++++++++ .../20260112092747_AddAppVersions.cs | 50 ++ .../DashboardDbContextModelSnapshot.cs | 490 +++++++++++++++++ .../Components/App.razor | 21 + .../Components/Layout/MainLayout.razor | 63 +++ .../Components/Pages/Applications.razor | 249 +++++++++ .../Components/Pages/Batch.razor | 234 ++++++++ .../Components/Pages/Devices.razor | 512 ++++++++++++++++++ .../Components/Pages/Guest.razor | 167 ++++++ .../Components/Pages/Home.razor | 113 ++++ .../Components/Pages/Versions.razor | 161 ++++++ .../Components/Routes.razor | 12 + .../Components/_Imports.razor | 10 + .../Controllers/AppVersionsController.cs | 98 ++++ .../Controllers/ApplicationsController.cs | 86 +++ .../Controllers/DevicesController.cs | 128 +++++ .../DevicesManagementController.cs | 372 +++++++++++++ .../Controllers/GuestAccessController.cs | 71 +++ .../Controllers/PlaylistsController.cs | 110 ++++ .../DRS9.Dashboard.Server.csproj | 29 + .../DRS9.Dashboard.Server.http | 6 + .../Middleware/WebSocketMiddleware.cs | 122 +++++ src/DRS9.Dashboard.Server/Program.cs | 164 ++++++ .../Properties/launchSettings.json | 14 + .../Services/ApiClientService.cs | 254 +++++++++ .../appsettings.Development.json | 8 + src/DRS9.Dashboard.Server/appsettings.json | 19 + src/DRS9.Dashboard.Server/dashboard.db | Bin 0 -> 118784 bytes src/DRS9.Dashboard.Server/wwwroot/css/app.css | 252 +++++++++ src/DRS9.Dashboard.Server/wwwroot/viewer.html | 173 ++++++ src/DRS9.Dashboard.sln | 76 +++ test_api.sh | 56 ++ test_websocket.html | 67 +++ 72 files changed, 8430 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 src/DRS9.Dashboard.Application/Class1.cs create mode 100644 src/DRS9.Dashboard.Application/DRS9.Dashboard.Application.csproj create mode 100644 src/DRS9.Dashboard.Application/DTOs/AppVersionDto.cs create mode 100644 src/DRS9.Dashboard.Application/DTOs/ApplicationDto.cs create mode 100644 src/DRS9.Dashboard.Application/DTOs/BatchManagementDto.cs create mode 100644 src/DRS9.Dashboard.Application/DTOs/DeviceAuthDto.cs create mode 100644 src/DRS9.Dashboard.Application/DTOs/DeviceManagementDto.cs create mode 100644 src/DRS9.Dashboard.Application/DTOs/GuestAccessDto.cs create mode 100644 src/DRS9.Dashboard.Application/DTOs/PlaylistDto.cs create mode 100644 src/DRS9.Dashboard.Application/Services/AppVersionService.cs create mode 100644 src/DRS9.Dashboard.Application/Services/ApplicationService.cs create mode 100644 src/DRS9.Dashboard.Application/Services/BatchManagementService.cs create mode 100644 src/DRS9.Dashboard.Application/Services/DashboardWebSocketManager.cs create mode 100644 src/DRS9.Dashboard.Application/Services/DeviceManagementService.cs create mode 100644 src/DRS9.Dashboard.Application/Services/DeviceService.cs create mode 100644 src/DRS9.Dashboard.Application/Services/GuestAccessService.cs create mode 100644 src/DRS9.Dashboard.Application/Services/JwtTokenService.cs create mode 100644 src/DRS9.Dashboard.Application/Services/PlaylistService.cs create mode 100644 src/DRS9.Dashboard.Application/Services/WebSocketConnection.cs create mode 100644 src/DRS9.Dashboard.Domain/DRS9.Dashboard.Domain.csproj create mode 100644 src/DRS9.Dashboard.Domain/Entities/AppVersion.cs create mode 100644 src/DRS9.Dashboard.Domain/Entities/Application.cs create mode 100644 src/DRS9.Dashboard.Domain/Entities/AuditLog.cs create mode 100644 src/DRS9.Dashboard.Domain/Entities/BaseEntity.cs create mode 100644 src/DRS9.Dashboard.Domain/Entities/Device.cs create mode 100644 src/DRS9.Dashboard.Domain/Entities/DeviceAssignment.cs create mode 100644 src/DRS9.Dashboard.Domain/Entities/DeviceGroup.cs create mode 100644 src/DRS9.Dashboard.Domain/Entities/Playlist.cs create mode 100644 src/DRS9.Dashboard.Domain/Entities/User.cs create mode 100644 src/DRS9.Dashboard.Infrastructure/Class1.cs create mode 100644 src/DRS9.Dashboard.Infrastructure/DRS9.Dashboard.Infrastructure.csproj create mode 100644 src/DRS9.Dashboard.Infrastructure/Data/DashboardDbContext.cs create mode 100644 src/DRS9.Dashboard.Infrastructure/Data/DataSeeder.cs create mode 100644 src/DRS9.Dashboard.Infrastructure/Migrations/20260112082557_InitialCreate.Designer.cs create mode 100644 src/DRS9.Dashboard.Infrastructure/Migrations/20260112082557_InitialCreate.cs create mode 100644 src/DRS9.Dashboard.Infrastructure/Migrations/20260112091958_AddPlaylists.Designer.cs create mode 100644 src/DRS9.Dashboard.Infrastructure/Migrations/20260112091958_AddPlaylists.cs create mode 100644 src/DRS9.Dashboard.Infrastructure/Migrations/20260112092747_AddAppVersions.Designer.cs create mode 100644 src/DRS9.Dashboard.Infrastructure/Migrations/20260112092747_AddAppVersions.cs create mode 100644 src/DRS9.Dashboard.Infrastructure/Migrations/DashboardDbContextModelSnapshot.cs create mode 100644 src/DRS9.Dashboard.Server/Components/App.razor create mode 100644 src/DRS9.Dashboard.Server/Components/Layout/MainLayout.razor create mode 100644 src/DRS9.Dashboard.Server/Components/Pages/Applications.razor create mode 100644 src/DRS9.Dashboard.Server/Components/Pages/Batch.razor create mode 100644 src/DRS9.Dashboard.Server/Components/Pages/Devices.razor create mode 100644 src/DRS9.Dashboard.Server/Components/Pages/Guest.razor create mode 100644 src/DRS9.Dashboard.Server/Components/Pages/Home.razor create mode 100644 src/DRS9.Dashboard.Server/Components/Pages/Versions.razor create mode 100644 src/DRS9.Dashboard.Server/Components/Routes.razor create mode 100644 src/DRS9.Dashboard.Server/Components/_Imports.razor create mode 100644 src/DRS9.Dashboard.Server/Controllers/AppVersionsController.cs create mode 100644 src/DRS9.Dashboard.Server/Controllers/ApplicationsController.cs create mode 100644 src/DRS9.Dashboard.Server/Controllers/DevicesController.cs create mode 100644 src/DRS9.Dashboard.Server/Controllers/DevicesManagementController.cs create mode 100644 src/DRS9.Dashboard.Server/Controllers/GuestAccessController.cs create mode 100644 src/DRS9.Dashboard.Server/Controllers/PlaylistsController.cs create mode 100644 src/DRS9.Dashboard.Server/DRS9.Dashboard.Server.csproj create mode 100644 src/DRS9.Dashboard.Server/DRS9.Dashboard.Server.http create mode 100644 src/DRS9.Dashboard.Server/Middleware/WebSocketMiddleware.cs create mode 100644 src/DRS9.Dashboard.Server/Program.cs create mode 100644 src/DRS9.Dashboard.Server/Properties/launchSettings.json create mode 100644 src/DRS9.Dashboard.Server/Services/ApiClientService.cs create mode 100644 src/DRS9.Dashboard.Server/appsettings.Development.json create mode 100644 src/DRS9.Dashboard.Server/appsettings.json create mode 100644 src/DRS9.Dashboard.Server/dashboard.db create mode 100644 src/DRS9.Dashboard.Server/wwwroot/css/app.css create mode 100644 src/DRS9.Dashboard.Server/wwwroot/viewer.html create mode 100644 src/DRS9.Dashboard.sln create mode 100755 test_api.sh create mode 100644 test_websocket.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..faa0fa6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +src/DRS9.Dashboard.Application/bin/ +src/DRS9.Dashboard.Application/obj/ +src/DRS9.Dashboard.Domain/bin/ +src/DRS9.Dashboard.Domain/obj/ +src/DRS9.Dashboard.Infrastructure/bin/ +src/DRS9.Dashboard.Infrastructure/obj/ +src/DRS9.Dashboard.Server/bin/ +src/DRS9.Dashboard.Server/obj/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..dc7730c --- /dev/null +++ b/README.md @@ -0,0 +1,175 @@ +# DRS9 信息发布系统 + +## 系统概述 + +一套企业级看板信息发布管理系统,支持企业内部数据大屏展示和公共场所信息发布。系统采用 C/S + B/S 混合架构,通过服务器端统一管理内容分发和设备控制。 + +### 应用场景 +- **企业内部**:数据大屏、会议显示、KPI 展示、公告发布 +- **公共场所**:学校信息发布、商场广告、车站信息发布、医院导诊显示 + +--- + +## 系统架构 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 服务器端 (ASP.NET Core) │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ 内容管理 │ │ 设备管理 │ │ 用户管理 │ │ 推送服务 │ │ +│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ +│ │ │ +│ ┌──────────────────────────────────────────────────────────┐ │ +│ │ WebSocket 实时通信 │ │ +│ └──────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ + │ + ┌───────────────┴───────────────┐ + ▼ ▼ +┌─────────────────────┐ ┌─────────────────────┐ +│ 移动客户端 App │ │ 浏览器客户端 │ +│ (Android/iOS/Win) │ │ (Web) │ +│ - 内容展示 │ │ - 内容展示 │ +│ - WebSocket 长连接 │ │ - 短连接/轮询 │ +│ - 自动升级 │ │ - 临时访问授权 │ +└─────────────────────┘ └─────────────────────┘ +``` + +--- + +## 技术选型 + +### 服务器端 +- **框架**:ASP.NET Core 8.0+ +- **通信**:WebSocket (实时)、REST API (配置管理) +- **数据库**:SQLite (单机) / PostgreSQL/MySQL (集群) +- **缓存**:内存缓存 / Redis (分布式) + +### 客户端 +- **移动端**:.NET MAUI / Flutter (跨平台) +- **Web端**:Vue 3 / React + +--- + +## 功能模块 + +### 服务器端 + +#### 1. 内容管理模块 +- **应用类型**:数据看板、网页轮转器、图片轮播、视频播放 +- **内容编排**:支持多内容组合、定时切换、优先级控制 +- **URL 管理**:每个应用对应一个服务器端 URL 地址 + +#### 2. 设备管理模块 +- **设备注册**:设备码激活绑定 +- **设备分组**:按区域/部门批量管理 +- **远程控制**:推送内容、重启应用、禁用设备、升级 App +- **状态监控**:在线状态、内容加载情况、设备信息 + +#### 3. 授权认证模块 +- **认证方式**:设备码(首次注册) + Token(日常通信) +- **设备码格式**:`XXX-XXX-XXX`(如 `ABC-123-XYZ`) +- **Token 类型**:JWT,包含设备 ID、权限、过期时间 + +#### 4. 推送服务模块 +- **实时推送**:通过 WebSocket 推送内容更新、指令 +- **指令类型**:刷新内容、切换应用、重启、截图上报 +- **离线消息**:设备离线时消息持久化,上线后推送 + +### 客户端 + +#### 1. 移动客户端 App +- **配置简化**:仅需设置服务器地址 +- **自动获取**:内容 URL 由服务器动态分配 +- **长连接**:WebSocket 保持与服务器连接 +- **自动升级**:支持服务器推送的版本更新 + +#### 2. 浏览器客户端 +- **访问方式**:通过授权链接或设备码访问 +- **实时同步**:WebSocket 或短轮询获取更新 +- **临时授权**:支持限时访问 Token + +--- + +## 设备授权流程 + +``` +┌─────────┐ 设备码 ┌─────────┐ 验证+Token ┌─────────┐ +│ 客户端 │ ───────────────→ │ 服务器端 │ ───────────────→ │ 客户端 │ +└─────────┘ └─────────┘ └─────────┘ + │ │ │ + │ 激活请求(设备码) │ │ + │ ABC-123-XYZ │ │ + │ │ │ + │ ┌─────────────────┐ │ + │ │ 验证设备码 │ │ + │ │ 绑定设备身份 │ │ + │ │ 生成 JWT Token │ │ + │ └─────────────────┘ │ + │ │ │ + │ 返回 Token │ │ + │ ←───────────────────────── │ + │ │ │ + │ 后续请求携带 Token │ + │ ────────────────────────────────────────────────────────→│ +``` + +--- + +## 数据库设计(草案) + +### 表结构 +- `devices` - 设备信息表 +- `device_groups` - 设备分组表 +- `applications` - 应用内容表 +- `device_assignments` - 设备与应用分配表 +- `users` - 用户管理表 +- `audit_logs` - 操作审计日志 + +--- + +## 部署要求 + +### 服务器端 +- **操作系统**:Linux (推荐) / Windows Server +- **运行时**:.NET 8.0 Runtime +- **端口**:HTTP (8080)、HTTPS (8443)、WebSocket (同端口) +- **内存**:最低 2GB,推荐 4GB+ + +### 客户端 +- **移动端**:Android 8.0+ / iOS 12+ +- **Windows**:Windows 10+ + +--- + +## 开发计划 + +### Phase 1 - 基础框架 +- [ ] 服务器端项目搭建 +- [ ] 数据库设计与迁移 +- [ ] 设备注册与认证 +- [ ] WebSocket 通信基础 + +### Phase 2 - 核心功能 +- [ ] 内容管理模块 +- [ ] 设备管理模块 +- [ ] 推送服务模块 +- [ ] 移动端 App 基础框架 + +### Phase 3 - 高级功能 +- [ ] 内容编排与定时切换 +- [ ] 设备分组批量管理 +- [ ] 自动升级功能 +- [ ] 浏览器客户端 + +### Phase 4 - 优化完善 +- [ ] 性能优化 +- [ ] 监控告警 +- [ ] 部署文档 +- [ ] 用户手册 + +--- + +## 许可证 + +待定 \ No newline at end of file diff --git a/src/DRS9.Dashboard.Application/Class1.cs b/src/DRS9.Dashboard.Application/Class1.cs new file mode 100644 index 0000000..da9a2ed --- /dev/null +++ b/src/DRS9.Dashboard.Application/Class1.cs @@ -0,0 +1,6 @@ +namespace DRS9.Dashboard.Application; + +public class Class1 +{ + +} diff --git a/src/DRS9.Dashboard.Application/DRS9.Dashboard.Application.csproj b/src/DRS9.Dashboard.Application/DRS9.Dashboard.Application.csproj new file mode 100644 index 0000000..5e38ea1 --- /dev/null +++ b/src/DRS9.Dashboard.Application/DRS9.Dashboard.Application.csproj @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + net9.0 + enable + enable + + + diff --git a/src/DRS9.Dashboard.Application/DTOs/AppVersionDto.cs b/src/DRS9.Dashboard.Application/DTOs/AppVersionDto.cs new file mode 100644 index 0000000..57011ed --- /dev/null +++ b/src/DRS9.Dashboard.Application/DTOs/AppVersionDto.cs @@ -0,0 +1,64 @@ +using System.ComponentModel.DataAnnotations; + +namespace DRS9.Dashboard.Application.DTOs; + +public record AppVersionCreateRequest +{ + [Required] + [MaxLength(50)] + public string Platform { get; set; } = string.Empty; + + [Required] + [MaxLength(20)] + public string Version { get; set; } = string.Empty; + + [Required] + [MaxLength(100)] + public string VersionName { get; set; } = string.Empty; + + [MaxLength(500)] + public string? DownloadUrl { get; set; } + + public long? FileSize { get; set; } + + [MaxLength(1000)] + public string? ChangeLog { get; set; } + + public bool IsForceUpdate { get; set; } = false; +} + +public record AppVersionDto +{ + public int Id { get; set; } + public string Platform { get; set; } = string.Empty; + public string Version { get; set; } = string.Empty; + public string VersionName { get; set; } = string.Empty; + public string? DownloadUrl { get; set; } + public long? FileSize { get; set; } + public string? ChangeLog { get; set; } + public bool IsForceUpdate { get; set; } + public bool IsEnabled { get; set; } + public DateTime? PublishedAt { get; set; } + public DateTime CreatedAt { get; set; } +} + +public record CheckVersionRequest +{ + [Required] + [MaxLength(50)] + public string Platform { get; set; } = string.Empty; + + [Required] + [MaxLength(20)] + public string CurrentVersion { get; set; } = string.Empty; +} + +public record CheckVersionResponse +{ + public bool HasUpdate { get; set; } + public bool IsForceUpdate { get; set; } + public AppVersionDto? LatestVersion { get; set; } + public string? DownloadUrl { get; set; } + public long? FileSize { get; set; } + public string? ChangeLog { get; set; } +} diff --git a/src/DRS9.Dashboard.Application/DTOs/ApplicationDto.cs b/src/DRS9.Dashboard.Application/DTOs/ApplicationDto.cs new file mode 100644 index 0000000..442b4c9 --- /dev/null +++ b/src/DRS9.Dashboard.Application/DTOs/ApplicationDto.cs @@ -0,0 +1,76 @@ +using System.ComponentModel.DataAnnotations; + +namespace DRS9.Dashboard.Application.DTOs; + +public record ApplicationCreateRequest +{ + [Required] + [MaxLength(200)] + public string Name { get; set; } = string.Empty; + + [Required] + [MaxLength(50)] + public string Type { get; set; } = string.Empty; + + [MaxLength(1000)] + public string? Description { get; set; } + + [Required] + [MaxLength(2000)] + public string ContentUrl { get; set; } = string.Empty; + + [MaxLength(500)] + public string? ThumbnailUrl { get; set; } + + public int Priority { get; set; } = 0; +} + +public record ApplicationUpdateRequest +{ + [MaxLength(200)] + public string? Name { get; set; } + + [MaxLength(50)] + public string? Type { get; set; } + + [MaxLength(1000)] + public string? Description { get; set; } + + [MaxLength(2000)] + public string? ContentUrl { get; set; } + + [MaxLength(500)] + public string? ThumbnailUrl { get; set; } + + public int? Priority { get; set; } + + public bool? IsEnabled { get; set; } +} + +public record ApplicationDto +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + public string Type { get; set; } = string.Empty; + public string? Description { get; set; } + public string ContentUrl { get; set; } = string.Empty; + public string? ThumbnailUrl { get; set; } + public int Priority { get; set; } + public bool IsEnabled { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime? UpdatedAt { get; set; } +} + +public record ApplicationListResponse +{ + public bool Success { get; set; } + public List Data { get; set; } = new(); + public int Total { get; set; } +} + +public record ApplicationResponse +{ + public bool Success { get; set; } + public ApplicationDto? Data { get; set; } + public string? Message { get; set; } +} diff --git a/src/DRS9.Dashboard.Application/DTOs/BatchManagementDto.cs b/src/DRS9.Dashboard.Application/DTOs/BatchManagementDto.cs new file mode 100644 index 0000000..8265359 --- /dev/null +++ b/src/DRS9.Dashboard.Application/DTOs/BatchManagementDto.cs @@ -0,0 +1,63 @@ +using System.ComponentModel.DataAnnotations; + +namespace DRS9.Dashboard.Application.DTOs; + +// 批量管理 DTO +public record BatchAssignContentRequest +{ + [Required] + public List DeviceIds { get; set; } = new(); + + [Required] + public List ApplicationIds { get; set; } = new(); +} + +public record BatchAssignPlaylistRequest +{ + [Required] + public List DeviceGroupIds { get; set; } = new(); + + [Required] + public int PlaylistId { get; set; } +} + +public record BatchPushRequest +{ + [Required] + public List DeviceIds { get; set; } = new(); + + [Required] + public string MessageType { get; set; } = string.Empty; + + public object? Data { get; set; } +} + +public record BatchPushToGroupRequest +{ + [Required] + public List DeviceGroupIds { get; set; } = new(); + + [Required] + public string MessageType { get; set; } = string.Empty; + + public object? Data { get; set; } +} + +public record BatchOperationResponse +{ + public bool Success { get; set; } + public int SuccessCount { get; set; } + public int FailedCount { get; set; } + public List Errors { get; set; } = new(); + public string Message { get; set; } = string.Empty; +} + +// 批量启用/禁用 +public record BatchToggleDevicesRequest +{ + [Required] + public List DeviceIds { get; set; } = new(); + + [Required] + public bool IsEnabled { get; set; } +} diff --git a/src/DRS9.Dashboard.Application/DTOs/DeviceAuthDto.cs b/src/DRS9.Dashboard.Application/DTOs/DeviceAuthDto.cs new file mode 100644 index 0000000..ffed833 --- /dev/null +++ b/src/DRS9.Dashboard.Application/DTOs/DeviceAuthDto.cs @@ -0,0 +1,43 @@ +using System.ComponentModel.DataAnnotations; + +namespace DRS9.Dashboard.Application.DTOs; + +public record DeviceRegisterRequest +{ + [Required] + public string DeviceCode { get; set; } = string.Empty; + + [Required] + [MaxLength(200)] + public string DeviceName { get; set; } = string.Empty; + + [MaxLength(50)] + public string? DeviceType { get; set; } + + [MaxLength(50)] + public string? OsVersion { get; set; } + + [MaxLength(100)] + public string? AppVersion { get; set; } +} + +public record DeviceRegisterResponse +{ + public bool Success { get; set; } + public string? Token { get; set; } + public string? Message { get; set; } + public int DeviceId { get; set; } +} + +public record DeviceRefreshTokenRequest +{ + [Required] + public string Token { get; set; } = string.Empty; +} + +public record DeviceRefreshTokenResponse +{ + public bool Success { get; set; } + public string? Token { get; set; } + public string? Message { get; set; } +} diff --git a/src/DRS9.Dashboard.Application/DTOs/DeviceManagementDto.cs b/src/DRS9.Dashboard.Application/DTOs/DeviceManagementDto.cs new file mode 100644 index 0000000..08fd28e --- /dev/null +++ b/src/DRS9.Dashboard.Application/DTOs/DeviceManagementDto.cs @@ -0,0 +1,136 @@ +using System.ComponentModel.DataAnnotations; + +namespace DRS9.Dashboard.Application.DTOs; + +// 设备管理 DTO +public record DeviceCreateRequest +{ + [Required] + [MaxLength(50)] + public string DeviceCode { get; set; } = string.Empty; + + [Required] + [MaxLength(200)] + public string DeviceName { get; set; } = string.Empty; + + [MaxLength(500)] + public string? Description { get; set; } + + public int? DeviceGroupId { get; set; } +} + +public record DeviceUpdateRequest +{ + [MaxLength(200)] + public string? DeviceName { get; set; } + + [MaxLength(500)] + public string? Description { get; set; } + + public int? DeviceGroupId { get; set; } + + public bool? IsEnabled { get; set; } +} + +public record DeviceDto +{ + public int Id { get; set; } + public string DeviceCode { get; set; } = string.Empty; + public string DeviceName { get; set; } = string.Empty; + public string? DeviceType { get; set; } + public string? OsVersion { get; set; } + public string? AppVersion { get; set; } + public bool IsActive { get; set; } + public bool IsEnabled { get; set; } + public DateTime? LastSeenAt { get; set; } + public DateTime? ActivatedAt { get; set; } + public int? DeviceGroupId { get; set; } + public string? DeviceGroupName { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime? UpdatedAt { get; set; } +} + +public record DeviceListResponse +{ + public bool Success { get; set; } + public List Data { get; set; } = new(); + public int Total { get; set; } +} + +// 设备分组 DTO +public record DeviceGroupCreateRequest +{ + [Required] + [MaxLength(100)] + public string Name { get; set; } = string.Empty; + + [MaxLength(500)] + public string? Description { get; set; } +} + +public record DeviceGroupUpdateRequest +{ + [MaxLength(100)] + public string? Name { get; set; } + + [MaxLength(500)] + public string? Description { get; set; } +} + +public record DeviceGroupDto +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + public string? Description { get; set; } + public int DeviceCount { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime? UpdatedAt { get; set; } +} + +public record DeviceGroupListResponse +{ + public bool Success { get; set; } + public List Data { get; set; } = new(); + public int Total { get; set; } +} + +// 设备内容分配 DTO +public record DeviceAssignmentRequest +{ + [Required] + public int DeviceId { get; set; } + + [Required] + public List ApplicationIds { get; set; } = new(); +} + +public record DeviceAssignmentDto +{ + public int Id { get; set; } + public int DeviceId { get; set; } + public int ApplicationId { get; set; } + public string ApplicationName { get; set; } = string.Empty; + public string ApplicationType { get; set; } = string.Empty; + public string ContentUrl { get; set; } = string.Empty; + public int Order { get; set; } + public int Duration { get; set; } +} + +public record DeviceContentResponse +{ + public bool Success { get; set; } + public List Data { get; set; } = new(); + public string? Message { get; set; } +} + +// 推送消息 DTO +public record PushMessageRequest +{ + [Required] + public int DeviceId { get; set; } + + [Required] + public string MessageType { get; set; } = string.Empty; // refresh, restart, etc. + + public object? Data { get; set; } +} diff --git a/src/DRS9.Dashboard.Application/DTOs/GuestAccessDto.cs b/src/DRS9.Dashboard.Application/DTOs/GuestAccessDto.cs new file mode 100644 index 0000000..f9b9a4b --- /dev/null +++ b/src/DRS9.Dashboard.Application/DTOs/GuestAccessDto.cs @@ -0,0 +1,31 @@ +using System.ComponentModel.DataAnnotations; + +namespace DRS9.Dashboard.Application.DTOs; + +public record GuestAccessCreateRequest +{ + [MaxLength(200)] + public string? Description { get; set; } + + [Required] + public int ValidityMinutes { get; set; } = 60; // 默认 60 分钟 + + public int? DeviceId { get; set; } // 如果指定,模拟该设备获取内容 +} + +public record GuestAccessResponse +{ + public bool Success { get; set; } + public string? Token { get; set; } + public string? AccessUrl { get; set; } + public DateTime? ExpiresAt { get; set; } + public string? Message { get; set; } +} + +// 统计 DTO +public class DashboardStatsDto +{ + public int TotalDevices { get; set; } + public int OnlineDevices { get; set; } + public int TotalApplications { get; set; } +} diff --git a/src/DRS9.Dashboard.Application/DTOs/PlaylistDto.cs b/src/DRS9.Dashboard.Application/DTOs/PlaylistDto.cs new file mode 100644 index 0000000..1291293 --- /dev/null +++ b/src/DRS9.Dashboard.Application/DTOs/PlaylistDto.cs @@ -0,0 +1,114 @@ +using System.ComponentModel.DataAnnotations; + +namespace DRS9.Dashboard.Application.DTOs; + +// 播放计划 DTO +public record PlaylistCreateRequest +{ + [Required] + [MaxLength(200)] + public string Name { get; set; } = string.Empty; + + [MaxLength(1000)] + public string? Description { get; set; } + + [MaxLength(20)] + public string LoopMode { get; set; } = "Loop"; // Loop, Once, Shuffle + + public int? DeviceGroupId { get; set; } + + public List Items { get; set; } = new(); +} + +public record PlaylistUpdateRequest +{ + [MaxLength(200)] + public string? Name { get; set; } + + [MaxLength(1000)] + public string? Description { get; set; } + + [MaxLength(20)] + public string? LoopMode { get; set; } + + public int? DeviceGroupId { get; set; } + + public bool? IsEnabled { get; set; } +} + +public record PlaylistItemRequest +{ + public int ApplicationId { get; set; } + public int Order { get; set; } + public int Duration { get; set; } = 60; + public bool IsEnabled { get; set; } = true; + public string? ScheduleRule { get; set; } +} + +public record PlaylistDto +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + public string? Description { get; set; } + public string LoopMode { get; set; } = string.Empty; + public int? DeviceGroupId { get; set; } + public string? DeviceGroupName { get; set; } + public bool IsEnabled { get; set; } + public int ItemCount { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime? UpdatedAt { get; set; } +} + +public record PlaylistDetailDto +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + public string? Description { get; set; } + public string LoopMode { get; set; } = string.Empty; + public int? DeviceGroupId { get; set; } + public string? DeviceGroupName { get; set; } + public bool IsEnabled { get; set; } + public List Items { get; set; } = new(); + public DateTime CreatedAt { get; set; } + public DateTime? UpdatedAt { get; set; } +} + +public record PlaylistItemDto +{ + public int Id { get; set; } + public int PlaylistId { get; set; } + public int ApplicationId { get; set; } + public string ApplicationName { get; set; } = string.Empty; + public string ApplicationType { get; set; } = string.Empty; + public string ContentUrl { get; set; } = string.Empty; + public int Order { get; set; } + public int Duration { get; set; } + public bool IsEnabled { get; set; } + public string? ScheduleRule { get; set; } +} + +public record PlaylistListResponse +{ + public bool Success { get; set; } + public List Data { get; set; } = new(); + public int Total { get; set; } +} + +// 设备获取播放内容响应 +public record DevicePlaylistResponse +{ + public bool Success { get; set; } + public List Data { get; set; } = new(); + public string? LoopMode { get; set; } + public int? TotalDuration { get; set; } +} + +public record PlaylistContentDto +{ + public int Order { get; set; } + public string ApplicationName { get; set; } = string.Empty; + public string ApplicationType { get; set; } = string.Empty; + public string ContentUrl { get; set; } = string.Empty; + public int Duration { get; set; } + public string? ThumbnailUrl { get; set; } +} diff --git a/src/DRS9.Dashboard.Application/Services/AppVersionService.cs b/src/DRS9.Dashboard.Application/Services/AppVersionService.cs new file mode 100644 index 0000000..b0dc434 --- /dev/null +++ b/src/DRS9.Dashboard.Application/Services/AppVersionService.cs @@ -0,0 +1,198 @@ +using DRS9.Dashboard.Application.DTOs; +using DRS9.Dashboard.Domain.Entities; +using DRS9.Dashboard.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; + +namespace DRS9.Dashboard.Application.Services; + +public class AppVersionService +{ + private readonly DashboardDbContext _context; + private readonly DashboardWebSocketManager _wsManager; + + public AppVersionService(DashboardDbContext context, DashboardWebSocketManager wsManager) + { + _context = context; + _wsManager = wsManager; + } + + public async Task> GetAllAsync() + { + return await _context.AppVersions + .OrderByDescending(v => v.PublishedAt) + .ThenByDescending(v => v.Id) + .Select(v => new AppVersionDto + { + Id = v.Id, + Platform = v.Platform, + Version = v.Version, + VersionName = v.VersionName, + DownloadUrl = v.DownloadUrl, + FileSize = v.FileSize, + ChangeLog = v.ChangeLog, + IsForceUpdate = v.IsForceUpdate, + IsEnabled = v.IsEnabled, + PublishedAt = v.PublishedAt, + CreatedAt = v.CreatedAt + }) + .ToListAsync(); + } + + public async Task GetByIdAsync(int id) + { + return await _context.AppVersions + .Where(v => v.Id == id) + .Select(v => new AppVersionDto + { + Id = v.Id, + Platform = v.Platform, + Version = v.Version, + VersionName = v.VersionName, + DownloadUrl = v.DownloadUrl, + FileSize = v.FileSize, + ChangeLog = v.ChangeLog, + IsForceUpdate = v.IsForceUpdate, + IsEnabled = v.IsEnabled, + PublishedAt = v.PublishedAt, + CreatedAt = v.CreatedAt + }) + .FirstOrDefaultAsync(); + } + + public async Task CreateAsync(AppVersionCreateRequest request) + { + var version = new AppVersion + { + Platform = request.Platform, + Version = request.Version, + VersionName = request.VersionName, + DownloadUrl = request.DownloadUrl, + FileSize = request.FileSize, + ChangeLog = request.ChangeLog, + IsForceUpdate = request.IsForceUpdate, + IsEnabled = true, + PublishedAt = DateTime.UtcNow + }; + + _context.AppVersions.Add(version); + await _context.SaveChangesAsync(); + + return await GetByIdAsync(version.Id); + } + + public async Task DeleteAsync(int id) + { + var version = await _context.AppVersions.FindAsync(id); + if (version == null) return false; + + _context.AppVersions.Remove(version); + await _context.SaveChangesAsync(); + + return true; + } + + /// + /// 检查版本更新 + /// + public async Task CheckVersionAsync(string platform, string currentVersion) + { + // 获取该平台最新且启用的版本 + var latestVersion = await _context.AppVersions + .Where(v => v.Platform == platform && v.IsEnabled) + .OrderByDescending(v => v.PublishedAt) + .ThenByDescending(v => v.Id) + .FirstOrDefaultAsync(); + + if (latestVersion == null) + { + return new CheckVersionResponse + { + HasUpdate = false, + IsForceUpdate = false + }; + } + + // 比较版本号 + var hasUpdate = CompareVersions(latestVersion.Version, currentVersion) > 0; + + return new CheckVersionResponse + { + HasUpdate = hasUpdate, + IsForceUpdate = hasUpdate && latestVersion.IsForceUpdate, + LatestVersion = new AppVersionDto + { + Id = latestVersion.Id, + Platform = latestVersion.Platform, + Version = latestVersion.Version, + VersionName = latestVersion.VersionName, + DownloadUrl = latestVersion.DownloadUrl, + FileSize = latestVersion.FileSize, + ChangeLog = latestVersion.ChangeLog, + IsForceUpdate = latestVersion.IsForceUpdate, + IsEnabled = latestVersion.IsEnabled, + PublishedAt = latestVersion.PublishedAt, + CreatedAt = latestVersion.CreatedAt + }, + DownloadUrl = latestVersion.DownloadUrl, + FileSize = latestVersion.FileSize, + ChangeLog = latestVersion.ChangeLog + }; + } + + /// + /// 推送升级通知到设备 + /// + public async Task PushUpdateNotificationAsync(int versionId, string? platform = null) + { + var version = await _context.AppVersions.FindAsync(versionId); + if (version == null) return false; + + // 获取需要通知的设备 + var devices = await _context.Devices + .Where(d => d.IsActive && d.IsEnabled) + .Where(d => platform == null || d.DeviceType == platform) + .Select(d => d.Id) + .ToListAsync(); + + foreach (var deviceId in devices) + { + await _wsManager.BroadcastToDeviceAsync(deviceId, new WebSocketMessage + { + Type = "update_available", + Data = new + { + version = version.Version, + versionName = version.VersionName, + platform = version.Platform, + downloadUrl = version.DownloadUrl, + file = version.FileSize, + isForceUpdate = version.IsForceUpdate, + changeLog = version.ChangeLog + } + }); + } + + return true; + } + + /// + /// 简单的版本号比较 + /// 返回: 1 (v1 > v2), -1 (v1 < v2), 0 (v1 == v2) + /// + private int CompareVersions(string v1, string v2) + { + var parts1 = v1.Split('.').Select(int.Parse).ToArray(); + var parts2 = v2.Split('.').Select(int.Parse).ToArray(); + + for (int i = 0; i < Math.Max(parts1.Length, parts2.Length); i++) + { + var p1 = i < parts1.Length ? parts1[i] : 0; + var p2 = i < parts2.Length ? parts2[i] : 0; + + if (p1 > p2) return 1; + if (p1 < p2) return -1; + } + + return 0; + } +} diff --git a/src/DRS9.Dashboard.Application/Services/ApplicationService.cs b/src/DRS9.Dashboard.Application/Services/ApplicationService.cs new file mode 100644 index 0000000..28c4210 --- /dev/null +++ b/src/DRS9.Dashboard.Application/Services/ApplicationService.cs @@ -0,0 +1,131 @@ +using DRS9.Dashboard.Application.DTOs; +using DRS9.Dashboard.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using AppEntity = DRS9.Dashboard.Domain.Entities.Application; + +namespace DRS9.Dashboard.Application.Services; + +public class ApplicationService +{ + private readonly DashboardDbContext _context; + + public ApplicationService(DashboardDbContext context) + { + _context = context; + } + + public async Task GetAllAsync() + { + var applications = await _context.Applications + .OrderBy(a => a.Priority) + .ThenBy(a => a.Id) + .ToListAsync(); + + return new ApplicationListResponse + { + Success = true, + Data = applications.Select(a => MapToDto(a)).ToList(), + Total = applications.Count + }; + } + + public async Task GetByIdAsync(int id) + { + var application = await _context.Applications.FindAsync(id); + if (application == null) + { + return new ApplicationResponse { Success = false, Message = "应用不存在" }; + } + + return new ApplicationResponse + { + Success = true, + Data = MapToDto(application) + }; + } + + public async Task CreateAsync(ApplicationCreateRequest request) + { + var application = new AppEntity + { + Name = request.Name, + Type = request.Type, + Description = request.Description, + ContentUrl = request.ContentUrl, + ThumbnailUrl = request.ThumbnailUrl, + Priority = request.Priority, + IsEnabled = true + }; + + _context.Applications.Add(application); + await _context.SaveChangesAsync(); + + return new ApplicationResponse + { + Success = true, + Data = MapToDto(application), + Message = "创建成功" + }; + } + + public async Task UpdateAsync(int id, ApplicationUpdateRequest request) + { + var application = await _context.Applications.FindAsync(id); + if (application == null) + { + return new ApplicationResponse { Success = false, Message = "应用不存在" }; + } + + if (request.Name != null) application.Name = request.Name; + if (request.Type != null) application.Type = request.Type; + if (request.Description != null) application.Description = request.Description; + if (request.ContentUrl != null) application.ContentUrl = request.ContentUrl; + if (request.ThumbnailUrl != null) application.ThumbnailUrl = request.ThumbnailUrl; + if (request.Priority.HasValue) application.Priority = request.Priority.Value; + if (request.IsEnabled.HasValue) application.IsEnabled = request.IsEnabled.Value; + application.UpdatedAt = DateTime.UtcNow; + + await _context.SaveChangesAsync(); + + return new ApplicationResponse + { + Success = true, + Data = MapToDto(application), + Message = "更新成功" + }; + } + + public async Task DeleteAsync(int id) + { + var application = await _context.Applications.FindAsync(id); + if (application == null) return false; + + // 删除相关的设备分配 + var assignments = await _context.DeviceAssignments + .Where(da => da.ApplicationId == id) + .ToListAsync(); + _context.DeviceAssignments.RemoveRange(assignments); + + _context.Applications.Remove(application); + await _context.SaveChangesAsync(); + + return true; + } + + private static ApplicationDto MapToDto(AppEntity application) + { + return new ApplicationDto + { + Id = application.Id, + Name = application.Name, + Type = application.Type, + Description = application.Description, + ContentUrl = application.ContentUrl, + ThumbnailUrl = application.ThumbnailUrl, + Priority = application.Priority, + IsEnabled = application.IsEnabled, + CreatedAt = application.CreatedAt, + UpdatedAt = application.UpdatedAt + }; + } +} diff --git a/src/DRS9.Dashboard.Application/Services/BatchManagementService.cs b/src/DRS9.Dashboard.Application/Services/BatchManagementService.cs new file mode 100644 index 0000000..390358d --- /dev/null +++ b/src/DRS9.Dashboard.Application/Services/BatchManagementService.cs @@ -0,0 +1,237 @@ +using DRS9.Dashboard.Application.DTOs; +using DRS9.Dashboard.Domain.Entities; +using DRS9.Dashboard.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; + +namespace DRS9.Dashboard.Application.Services; + +public class BatchManagementService +{ + private readonly DashboardDbContext _context; + private readonly DashboardWebSocketManager _wsManager; + + public BatchManagementService(DashboardDbContext context, DashboardWebSocketManager wsManager) + { + _context = context; + _wsManager = wsManager; + } + + /// + /// 批量为设备分配内容 + /// + public async Task BatchAssignContentAsync(BatchAssignContentRequest request) + { + var response = new BatchOperationResponse(); + var errors = new List(); + + foreach (var deviceId in request.DeviceIds) + { + try + { + // 删除现有分配 + var existingAssignments = await _context.DeviceAssignments + .Where(da => da.DeviceId == deviceId) + .ToListAsync(); + _context.DeviceAssignments.RemoveRange(existingAssignments); + + // 创建新分配 + var order = 0; + foreach (var appId in request.ApplicationIds) + { + _context.DeviceAssignments.Add(new DeviceAssignment + { + DeviceId = deviceId, + ApplicationId = appId, + Order = order++, + Duration = 60 + }); + } + + await _context.SaveChangesAsync(); + + // 推送更新到设备 + await _wsManager.BroadcastToDeviceAsync(deviceId, new WebSocketMessage + { + Type = "content_refresh", + Data = new { message = "内容已更新" } + }); + + response.SuccessCount++; + } + catch (Exception ex) + { + response.FailedCount++; + errors.Add($"设备 {deviceId}: {ex.Message}"); + } + } + + response.Success = response.SuccessCount > 0; + response.Errors = errors; + response.Message = $"成功: {response.SuccessCount}, 失败: {response.FailedCount}"; + + return response; + } + + /// + /// 批量推送消息到设备 + /// + public async Task BatchPushAsync(BatchPushRequest request) + { + var response = new BatchOperationResponse(); + var message = new WebSocketMessage + { + Type = request.MessageType, + Data = request.Data + }; + + foreach (var deviceId in request.DeviceIds) + { + var connections = _wsManager.GetConnectionsByDevice(deviceId); + if (connections.Any()) + { + await _wsManager.BroadcastToDeviceAsync(deviceId, message); + response.SuccessCount++; + } + else + { + response.FailedCount++; + } + } + + response.Success = response.SuccessCount > 0; + response.Message = $"已推送到 {response.SuccessCount} 个在线设备"; + + return response; + } + + /// + /// 批量推送到设备分组 + /// + public async Task BatchPushToGroupAsync(BatchPushToGroupRequest request) + { + var response = new BatchOperationResponse(); + var deviceIds = await _context.Devices + .Where(d => request.DeviceGroupIds.Contains(d.DeviceGroupId ?? 0)) + .Select(d => d.Id) + .ToListAsync(); + + var batchRequest = new BatchPushRequest + { + DeviceIds = deviceIds, + MessageType = request.MessageType, + Data = request.Data + }; + + return await BatchPushAsync(batchRequest); + } + + /// + /// 批量启用/禁用设备 + /// + public async Task BatchToggleDevicesAsync(BatchToggleDevicesRequest request) + { + var response = new BatchOperationResponse(); + + foreach (var deviceId in request.DeviceIds) + { + try + { + var device = await _context.Devices.FindAsync(deviceId); + if (device != null) + { + device.IsEnabled = request.IsEnabled; + device.UpdatedAt = DateTime.UtcNow; + await _context.SaveChangesAsync(); + + // 如果禁用设备,推送通知 + if (!request.IsEnabled) + { + await _wsManager.BroadcastToDeviceAsync(deviceId, new WebSocketMessage + { + Type = "device_disabled", + Data = new { deviceId, message = "设备已被管理员禁用" } + }); + } + + response.SuccessCount++; + } + else + { + response.FailedCount++; + } + } + catch (Exception ex) + { + response.FailedCount++; + response.Errors.Add($"设备 {deviceId}: {ex.Message}"); + } + } + + response.Success = response.SuccessCount > 0; + response.Message = $"成功: {response.SuccessCount}, 失败: {response.FailedCount}"; + + return response; + } + + /// + /// 批量分配播放列表到设备分组 + /// + public async Task BatchAssignPlaylistToGroupAsync(BatchAssignPlaylistRequest request) + { + var response = new BatchOperationResponse(); + var playlist = await _context.Playlists.FindAsync(request.PlaylistId); + + if (playlist == null) + { + response.Success = false; + response.Message = "播放列表不存在"; + return response; + } + + // 为每个分组创建播放列表副本(或使用共享逻辑) + foreach (var groupId in request.DeviceGroupIds) + { + try + { + // 这里简单实现:将原播放列表关联到第一个分组 + // 实际应用中可能需要为每个分组创建独立的播放列表 + var deviceIds = await _context.Devices + .Where(d => d.DeviceGroupId == groupId) + .Select(d => d.Id) + .ToListAsync(); + + foreach (var deviceId in deviceIds) + { + await _wsManager.BroadcastToDeviceAsync(deviceId, new WebSocketMessage + { + Type = "playlist_changed", + Data = new { message = "播放列表已更新" } + }); + } + + response.SuccessCount++; + } + catch (Exception ex) + { + response.FailedCount++; + response.Errors.Add($"分组 {groupId}: {ex.Message}"); + } + } + + response.Success = response.SuccessCount > 0; + response.Message = $"成功: {response.SuccessCount}, 失败: {response.FailedCount}"; + + return response; + } + + /// + /// 获取分组中的所有设备 ID + /// + public async Task> GetDeviceIdsByGroupsAsync(List groupIds) + { + return await _context.Devices + .Where(d => groupIds.Contains(d.DeviceGroupId ?? 0)) + .Select(d => d.Id) + .ToListAsync(); + } +} diff --git a/src/DRS9.Dashboard.Application/Services/DashboardWebSocketManager.cs b/src/DRS9.Dashboard.Application/Services/DashboardWebSocketManager.cs new file mode 100644 index 0000000..8824c00 --- /dev/null +++ b/src/DRS9.Dashboard.Application/Services/DashboardWebSocketManager.cs @@ -0,0 +1,118 @@ +using System.Collections.Concurrent; +using System.Net.WebSockets; +using System.Text; +using System.Text.Json; +using Microsoft.Extensions.Logging; + +namespace DRS9.Dashboard.Application.Services; + +public interface IWebSocketMessage +{ + string Type { get; set; } + object? Data { get; set; } +} + +public class WebSocketMessage : IWebSocketMessage +{ + public string Type { get; set; } = string.Empty; + public object? Data { get; set; } +} + +public class DashboardWebSocketManager +{ + private readonly ConcurrentDictionary _connections = new(); + private readonly ILogger _logger; + + public DashboardWebSocketManager(ILogger logger) + { + _logger = logger; + } + + public WebSocketConnection AddConnection(WebSocket webSocket, int? deviceId = null) + { + var connectionId = Guid.NewGuid().ToString(); + var connection = new WebSocketConnection + { + ConnectionId = connectionId, + WebSocket = webSocket, + DeviceId = deviceId + }; + + _connections[connectionId] = connection; + _logger.LogInformation("WebSocket connected: {ConnectionId}, DeviceId: {DeviceId}", connectionId, deviceId); + return connection; + } + + public void RemoveConnection(string connectionId) + { + if (_connections.TryRemove(connectionId, out var connection)) + { + _logger.LogInformation("WebSocket disconnected: {ConnectionId}, DeviceId: {DeviceId}", connectionId, connection.DeviceId); + } + } + + public WebSocketConnection? GetConnection(string connectionId) + { + return _connections.TryGetValue(connectionId, out var connection) ? connection : null; + } + + public IEnumerable GetAllConnections() + { + return _connections.Values; + } + + public IEnumerable GetConnectionsByDevice(int deviceId) + { + return _connections.Values.Where(c => c.DeviceId == deviceId); + } + + public async Task SendMessageAsync(string connectionId, IWebSocketMessage message) + { + if (_connections.TryGetValue(connectionId, out var connection)) + { + await SendMessageAsync(connection, message); + } + } + + public async Task SendMessageAsync(WebSocketConnection connection, IWebSocketMessage message) + { + if (connection.WebSocket.State == WebSocketState.Open) + { + var json = JsonSerializer.Serialize(message); + var buffer = Encoding.UTF8.GetBytes(json); + var segment = new ArraySegment(buffer, 0, buffer.Length); + + await connection.WebSocket.SendAsync(segment, WebSocketMessageType.Text, true, CancellationToken.None); + connection.LastActivityAt = DateTime.UtcNow; + } + } + + public async Task BroadcastToAllAsync(IWebSocketMessage message) + { + var tasks = _connections.Values + .Where(c => c.WebSocket.State == WebSocketState.Open) + .Select(c => SendMessageAsync(c, message)); + + await Task.WhenAll(tasks); + } + + public async Task BroadcastToDeviceAsync(int deviceId, IWebSocketMessage message) + { + var connections = GetConnectionsByDevice(deviceId); + var tasks = connections + .Where(c => c.WebSocket.State == WebSocketState.Open) + .Select(c => SendMessageAsync(c, message)); + + await Task.WhenAll(tasks); + } + + public int GetConnectionCount() + { + return _connections.Count; + } + + public int GetDeviceConnectionCount(int deviceId) + { + return GetConnectionsByDevice(deviceId).Count(); + } +} diff --git a/src/DRS9.Dashboard.Application/Services/DeviceManagementService.cs b/src/DRS9.Dashboard.Application/Services/DeviceManagementService.cs new file mode 100644 index 0000000..43a0525 --- /dev/null +++ b/src/DRS9.Dashboard.Application/Services/DeviceManagementService.cs @@ -0,0 +1,310 @@ +using DRS9.Dashboard.Application.DTOs; +using DRS9.Dashboard.Domain.Entities; +using DRS9.Dashboard.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; + +namespace DRS9.Dashboard.Application.Services; + +public class DeviceManagementService +{ + private readonly DashboardDbContext _context; + private readonly DashboardWebSocketManager _wsManager; + + public DeviceManagementService(DashboardDbContext context, DashboardWebSocketManager wsManager) + { + _context = context; + _wsManager = wsManager; + } + + // 设备管理 + public async Task GetAllDevicesAsync() + { + var devices = await _context.Devices + .Include(d => d.DeviceGroup) + .OrderBy(d => d.Id) + .ToListAsync(); + + return new DeviceListResponse + { + Success = true, + Data = devices.Select(MapToDto).ToList(), + Total = devices.Count + }; + } + + public async Task GetDeviceByIdAsync(int id) + { + var device = await _context.Devices + .Include(d => d.DeviceGroup) + .FirstOrDefaultAsync(d => d.Id == id); + + return device == null ? null : MapToDto(device); + } + + public async Task CreateDeviceAsync(DeviceCreateRequest request) + { + // 检查设备码是否已存在 + var exists = await _context.Devices.AnyAsync(d => d.DeviceCode == request.DeviceCode); + if (exists) return null; + + var device = new Device + { + DeviceCode = request.DeviceCode, + DeviceName = request.DeviceName, + DeviceGroupId = request.DeviceGroupId, + IsActive = false, + IsEnabled = true + }; + + _context.Devices.Add(device); + await _context.SaveChangesAsync(); + + return MapToDto(device); + } + + public async Task UpdateDeviceAsync(int id, DeviceUpdateRequest request) + { + var device = await _context.Devices.FindAsync(id); + if (device == null) return null; + + if (request.DeviceName != null) device.DeviceName = request.DeviceName; + if (request.DeviceGroupId.HasValue) device.DeviceGroupId = request.DeviceGroupId.Value; + if (request.IsEnabled.HasValue) + { + device.IsEnabled = request.IsEnabled.Value; + // 如果禁用设备,推送通知 + if (!request.IsEnabled.Value) + { + await _wsManager.BroadcastToDeviceAsync(id, new WebSocketMessage + { + Type = "device_disabled", + Data = new { deviceId = id, message = "设备已被管理员禁用" } + }); + } + } + device.UpdatedAt = DateTime.UtcNow; + + await _context.SaveChangesAsync(); + return MapToDto(device); + } + + public async Task DeleteDeviceAsync(int id) + { + var device = await _context.Devices.FindAsync(id); + if (device == null) return false; + + // 删除相关分配 + var assignments = await _context.DeviceAssignments + .Where(da => da.DeviceId == id) + .ToListAsync(); + _context.DeviceAssignments.RemoveRange(assignments); + + _context.Devices.Remove(device); + await _context.SaveChangesAsync(); + + return true; + } + + // 设备分组管理 + public async Task GetAllGroupsAsync() + { + var groups = await _context.DeviceGroups + .Include(g => g.Devices) + .OrderBy(g => g.Id) + .ToListAsync(); + + return new DeviceGroupListResponse + { + Success = true, + Data = groups.Select(g => new DeviceGroupDto + { + Id = g.Id, + Name = g.Name, + Description = g.Description, + DeviceCount = g.Devices.Count, + CreatedAt = g.CreatedAt, + UpdatedAt = g.UpdatedAt + }).ToList(), + Total = groups.Count + }; + } + + public async Task CreateGroupAsync(DeviceGroupCreateRequest request) + { + var group = new DeviceGroup + { + Name = request.Name, + Description = request.Description + }; + + _context.DeviceGroups.Add(group); + await _context.SaveChangesAsync(); + + return new DeviceGroupDto + { + Id = group.Id, + Name = group.Name, + Description = group.Description, + DeviceCount = 0, + CreatedAt = group.CreatedAt, + UpdatedAt = group.UpdatedAt + }; + } + + public async Task UpdateGroupAsync(int id, DeviceGroupUpdateRequest request) + { + var group = await _context.DeviceGroups.FindAsync(id); + if (group == null) return null; + + if (request.Name != null) group.Name = request.Name; + if (request.Description != null) group.Description = request.Description; + group.UpdatedAt = DateTime.UtcNow; + + await _context.SaveChangesAsync(); + + var deviceCount = await _context.Devices.CountAsync(d => d.DeviceGroupId == id); + + return new DeviceGroupDto + { + Id = group.Id, + Name = group.Name, + Description = group.Description, + DeviceCount = deviceCount, + CreatedAt = group.CreatedAt, + UpdatedAt = group.UpdatedAt + }; + } + + public async Task DeleteGroupAsync(int id) + { + var group = await _context.DeviceGroups.FindAsync(id); + if (group == null) return false; + + // 取消所有设备的分组关联 + var devices = await _context.Devices.Where(d => d.DeviceGroupId == id).ToListAsync(); + foreach (var device in devices) + { + device.DeviceGroupId = null; + } + + _context.DeviceGroups.Remove(group); + await _context.SaveChangesAsync(); + + return true; + } + + // 设备内容分配 + public async Task GetDeviceContentAsync(int deviceId) + { + var assignments = await _context.DeviceAssignments + .Where(da => da.DeviceId == deviceId) + .Include(da => da.Application) + .OrderBy(da => da.Order) + .ToListAsync(); + + return new DeviceContentResponse + { + Success = true, + Data = assignments.Select(da => new DeviceAssignmentDto + { + Id = da.Id, + DeviceId = da.DeviceId, + ApplicationId = da.ApplicationId, + ApplicationName = da.Application.Name, + ApplicationType = da.Application.Type, + ContentUrl = da.Application.ContentUrl, + Order = da.Order, + Duration = da.Duration + }).ToList() + }; + } + + public async Task AssignContentToDeviceAsync(int deviceId, List applicationIds) + { + var device = await _context.Devices.FindAsync(deviceId); + if (device == null) return false; + + // 删除现有分配 + var existingAssignments = await _context.DeviceAssignments + .Where(da => da.DeviceId == deviceId) + .ToListAsync(); + _context.DeviceAssignments.RemoveRange(existingAssignments); + + // 创建新分配 + var order = 0; + foreach (var appId in applicationIds) + { + _context.DeviceAssignments.Add(new DeviceAssignment + { + DeviceId = deviceId, + ApplicationId = appId, + Order = order++, + Duration = 60 + }); + } + + await _context.SaveChangesAsync(); + + // 推送更新到设备 + await _wsManager.BroadcastToDeviceAsync(deviceId, new WebSocketMessage + { + Type = "content_refresh", + Data = new { deviceId, message = "内容已更新" } + }); + + return true; + } + + // 推送消息到设备 + public async Task PushMessageAsync(int deviceId, string messageType, object? data = null) + { + var connections = _wsManager.GetConnectionsByDevice(deviceId); + if (!connections.Any()) return false; + + await _wsManager.BroadcastToDeviceAsync(deviceId, new WebSocketMessage + { + Type = messageType, + Data = data + }); + + return true; + } + + // 获取在线设备 + public async Task GetOnlineDevicesAsync() + { + var onlineThreshold = DateTime.UtcNow.AddMinutes(-5); + var devices = await _context.Devices + .Include(d => d.DeviceGroup) + .Where(d => d.LastSeenAt >= onlineThreshold) + .ToListAsync(); + + return new DeviceListResponse + { + Success = true, + Data = devices.Select(MapToDto).ToList(), + Total = devices.Count + }; + } + + private static DeviceDto MapToDto(Device device) + { + return new DeviceDto + { + Id = device.Id, + DeviceCode = device.DeviceCode, + DeviceName = device.DeviceName, + DeviceType = device.DeviceType, + OsVersion = device.OsVersion, + AppVersion = device.AppVersion, + IsActive = device.IsActive, + IsEnabled = device.IsEnabled, + LastSeenAt = device.LastSeenAt, + ActivatedAt = device.ActivatedAt, + DeviceGroupId = device.DeviceGroupId, + DeviceGroupName = device.DeviceGroup?.Name, + CreatedAt = device.CreatedAt, + UpdatedAt = device.UpdatedAt + }; + } +} diff --git a/src/DRS9.Dashboard.Application/Services/DeviceService.cs b/src/DRS9.Dashboard.Application/Services/DeviceService.cs new file mode 100644 index 0000000..381eb3d --- /dev/null +++ b/src/DRS9.Dashboard.Application/Services/DeviceService.cs @@ -0,0 +1,84 @@ +using DRS9.Dashboard.Application.DTOs; +using DRS9.Dashboard.Domain.Entities; +using DRS9.Dashboard.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; + +namespace DRS9.Dashboard.Application.Services; + +public class DeviceService +{ + private readonly DashboardDbContext _context; + private readonly JwtTokenService _jwtTokenService; + + public DeviceService(DashboardDbContext context, JwtTokenService jwtTokenService) + { + _context = context; + _jwtTokenService = jwtTokenService; + } + + public async Task RegisterAsync(DeviceRegisterRequest request) + { + // 查找设备码 + var device = await _context.Devices + .FirstOrDefaultAsync(d => d.DeviceCode == request.DeviceCode); + + if (device == null) + { + return new DeviceRegisterResponse + { + Success = false, + Message = "设备码无效" + }; + } + + if (!device.IsEnabled) + { + return new DeviceRegisterResponse + { + Success = false, + Message = "设备已被禁用" + }; + } + + // 更新设备信息 + device.DeviceName = request.DeviceName; + device.DeviceType = request.DeviceType; + device.OsVersion = request.OsVersion; + device.AppVersion = request.AppVersion; + device.IsActive = true; + device.ActivatedAt = device.ActivatedAt ?? DateTime.UtcNow; + device.LastSeenAt = DateTime.UtcNow; + device.UpdatedAt = DateTime.UtcNow; + + await _context.SaveChangesAsync(); + + // 生成 Token + var token = _jwtTokenService.GenerateToken(device.Id, device.DeviceCode, device.DeviceName); + + return new DeviceRegisterResponse + { + Success = true, + Token = token, + DeviceId = device.Id, + Message = "注册成功" + }; + } + + public async Task GetDeviceAsync(int deviceId) + { + return await _context.Devices + .Include(d => d.DeviceGroup) + .FirstOrDefaultAsync(d => d.Id == deviceId); + } + + public async Task UpdateLastSeenAsync(int deviceId) + { + var device = await _context.Devices.FindAsync(deviceId); + if (device != null) + { + device.LastSeenAt = DateTime.UtcNow; + device.UpdatedAt = DateTime.UtcNow; + await _context.SaveChangesAsync(); + } + } +} diff --git a/src/DRS9.Dashboard.Application/Services/GuestAccessService.cs b/src/DRS9.Dashboard.Application/Services/GuestAccessService.cs new file mode 100644 index 0000000..dbb6d1b --- /dev/null +++ b/src/DRS9.Dashboard.Application/Services/GuestAccessService.cs @@ -0,0 +1,66 @@ +using DRS9.Dashboard.Application.DTOs; +using DRS9.Dashboard.Domain.Entities; +using DRS9.Dashboard.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; + +namespace DRS9.Dashboard.Application.Services; + +public class GuestAccessService +{ + private readonly DashboardDbContext _context; + private readonly JwtTokenService _jwtTokenService; + + public GuestAccessService(DashboardDbContext context, JwtTokenService jwtTokenService) + { + _context = context; + _jwtTokenService = jwtTokenService; + } + + /// + /// 创建临时访问 Token + /// + public async Task CreateGuestTokenAsync(GuestAccessCreateRequest request) + { + // 验证设备是否存在 + if (request.DeviceId.HasValue) + { + var device = await _context.Devices.FindAsync(request.DeviceId.Value); + if (device == null || !device.IsEnabled) + { + return new GuestAccessResponse + { + Success = false, + Message = "设备不存在或已禁用" + }; + } + } + + // 使用 JWT 创建临时 Token + var deviceId = request.DeviceId ?? 0; // 0 表示访客模式 + var token = _jwtTokenService.GenerateToken(deviceId, "GUEST", $"访客-{request.Description ?? "临时访问"}"); + + // 计算 Token 过期时间 + var expiresAt = DateTime.UtcNow.AddMinutes(request.ValidityMinutes); + + return new GuestAccessResponse + { + Success = true, + Token = token, + AccessUrl = $"/guest/{token}", + ExpiresAt = expiresAt, + Message = "临时访问链接已创建" + }; + } + + /// + /// 验证访客 Token + /// + public bool ValidateGuestToken(string token) + { + var principal = _jwtTokenService.ValidateToken(token); + if (principal == null) return false; + + var deviceCodeClaim = principal.FindFirst("deviceCode")?.Value; + return deviceCodeClaim == "GUEST"; + } +} diff --git a/src/DRS9.Dashboard.Application/Services/JwtTokenService.cs b/src/DRS9.Dashboard.Application/Services/JwtTokenService.cs new file mode 100644 index 0000000..ee0895f --- /dev/null +++ b/src/DRS9.Dashboard.Application/Services/JwtTokenService.cs @@ -0,0 +1,72 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using Microsoft.IdentityModel.Tokens; + +namespace DRS9.Dashboard.Application.Services; + +public class JwtTokenService +{ + private readonly string _key; + private readonly string _issuer; + private readonly string _audience; + private readonly int _expiryMinutes; + + public JwtTokenService(string key, string issuer, string audience, int expiryMinutes) + { + _key = key; + _issuer = issuer; + _audience = audience; + _expiryMinutes = expiryMinutes; + } + + public string GenerateToken(int deviceId, string deviceCode, string deviceName) + { + var claims = new List + { + new Claim("deviceId", deviceId.ToString()), + new Claim("deviceCode", deviceCode), + new Claim("deviceName", deviceName), + new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) + }; + + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_key)); + var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); + + var token = new JwtSecurityToken( + issuer: _issuer, + audience: _audience, + claims: claims, + expires: DateTime.UtcNow.AddMinutes(_expiryMinutes), + signingCredentials: credentials + ); + + return new JwtSecurityTokenHandler().WriteToken(token); + } + + public ClaimsPrincipal? ValidateToken(string token) + { + var tokenHandler = new JwtSecurityTokenHandler(); + var key = Encoding.UTF8.GetBytes(_key); + + try + { + var principal = tokenHandler.ValidateToken(token, new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = _issuer, + ValidAudience = _audience, + IssuerSigningKey = new SymmetricSecurityKey(key) + }, out _); + + return principal; + } + catch + { + return null; + } + } +} diff --git a/src/DRS9.Dashboard.Application/Services/PlaylistService.cs b/src/DRS9.Dashboard.Application/Services/PlaylistService.cs new file mode 100644 index 0000000..26e3b18 --- /dev/null +++ b/src/DRS9.Dashboard.Application/Services/PlaylistService.cs @@ -0,0 +1,288 @@ +using DRS9.Dashboard.Application.DTOs; +using DRS9.Dashboard.Domain.Entities; +using DRS9.Dashboard.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; + +namespace DRS9.Dashboard.Application.Services; + +public class PlaylistService +{ + private readonly DashboardDbContext _context; + private readonly DashboardWebSocketManager _wsManager; + + public PlaylistService(DashboardDbContext context, DashboardWebSocketManager wsManager) + { + _context = context; + _wsManager = wsManager; + } + + public async Task GetAllAsync() + { + var playlists = await _context.Playlists + .Include(p => p.DeviceGroup) + .Include(p => p.Items) + .OrderBy(p => p.Id) + .ToListAsync(); + + return new PlaylistListResponse + { + Success = true, + Data = playlists.Select(p => new PlaylistDto + { + Id = p.Id, + Name = p.Name, + Description = p.Description, + LoopMode = p.LoopMode, + DeviceGroupId = p.DeviceGroupId, + DeviceGroupName = p.DeviceGroup?.Name, + IsEnabled = p.IsEnabled, + ItemCount = p.Items.Count, + CreatedAt = p.CreatedAt, + UpdatedAt = p.UpdatedAt + }).ToList(), + Total = playlists.Count + }; + } + + public async Task GetByIdAsync(int id) + { + var playlist = await _context.Playlists + .Include(p => p.DeviceGroup) + .Include(p => p.Items) + .ThenInclude(i => i.Application) + .FirstOrDefaultAsync(p => p.Id == id); + + if (playlist == null) return null; + + return new PlaylistDetailDto + { + Id = playlist.Id, + Name = playlist.Name, + Description = playlist.Description, + LoopMode = playlist.LoopMode, + DeviceGroupId = playlist.DeviceGroupId, + DeviceGroupName = playlist.DeviceGroup?.Name, + IsEnabled = playlist.IsEnabled, + Items = playlist.Items.OrderBy(i => i.Order).Select(i => new PlaylistItemDto + { + Id = i.Id, + PlaylistId = i.PlaylistId, + ApplicationId = i.ApplicationId, + ApplicationName = i.Application.Name, + ApplicationType = i.Application.Type, + ContentUrl = i.Application.ContentUrl, + Order = i.Order, + Duration = i.Duration, + IsEnabled = i.IsEnabled, + ScheduleRule = i.ScheduleRule + }).ToList(), + CreatedAt = playlist.CreatedAt, + UpdatedAt = playlist.UpdatedAt + }; + } + + public async Task CreateAsync(PlaylistCreateRequest request) + { + var playlist = new Playlist + { + Name = request.Name, + Description = request.Description, + LoopMode = request.LoopMode, + DeviceGroupId = request.DeviceGroupId, + IsEnabled = true + }; + + _context.Playlists.Add(playlist); + await _context.SaveChangesAsync(); + + // 添加播放项 + foreach (var itemRequest in request.Items) + { + var item = new PlaylistItem + { + PlaylistId = playlist.Id, + ApplicationId = itemRequest.ApplicationId, + Order = itemRequest.Order, + Duration = itemRequest.Duration, + IsEnabled = itemRequest.IsEnabled, + ScheduleRule = itemRequest.ScheduleRule + }; + _context.PlaylistItems.Add(item); + } + await _context.SaveChangesAsync(); + + return await GetByIdAsync(playlist.Id); + } + + public async Task UpdateAsync(int id, PlaylistUpdateRequest request) + { + var playlist = await _context.Playlists.FindAsync(id); + if (playlist == null) return null; + + if (request.Name != null) playlist.Name = request.Name; + if (request.Description != null) playlist.Description = request.Description; + if (request.LoopMode != null) playlist.LoopMode = request.LoopMode; + if (request.DeviceGroupId.HasValue) playlist.DeviceGroupId = request.DeviceGroupId.Value; + if (request.IsEnabled.HasValue) playlist.IsEnabled = request.IsEnabled.Value; + playlist.UpdatedAt = DateTime.UtcNow; + + await _context.SaveChangesAsync(); + + return await GetByIdAsync(id); + } + + public async Task DeleteAsync(int id) + { + var playlist = await _context.Playlists.FindAsync(id); + if (playlist == null) return false; + + _context.Playlists.Remove(playlist); + await _context.SaveChangesAsync(); + + return true; + } + + /// + /// 获取设备当前应该播放的内容列表 + /// + public async Task GetDevicePlaylistAsync(int deviceId) + { + var device = await _context.Devices + .Include(d => d.DeviceGroup) + .FirstOrDefaultAsync(d => d.Id == deviceId); + + if (device == null) + { + return new DevicePlaylistResponse { Success = false }; + } + + // 查找匹配的播放列表 + var playlist = await _context.Playlists + .Include(p => p.Items) + .ThenInclude(i => i.Application) + .Where(p => p.IsEnabled && + (p.DeviceGroupId == null || p.DeviceGroupId == device.DeviceGroupId)) + .OrderByDescending(p => p.DeviceGroupId) // 优先使用设备专属的播放列表 + .FirstOrDefaultAsync(); + + if (playlist == null || !playlist.Items.Any()) + { + return new DevicePlaylistResponse { Success = true, Data = new(), LoopMode = "Loop" }; + } + + // 过滤启用的播放项,并检查定时规则 + var now = DateTime.UtcNow; + var enabledItems = playlist.Items + .Where(i => i.IsEnabled && IsItemScheduled(i, now)) + .OrderBy(i => i.Order) + .Select(i => new PlaylistContentDto + { + Order = i.Order, + ApplicationName = i.Application.Name, + ApplicationType = i.Application.Type, + ContentUrl = i.Application.ContentUrl, + Duration = i.Duration, + ThumbnailUrl = i.Application.ThumbnailUrl + }) + .ToList(); + + return new DevicePlaylistResponse + { + Success = true, + Data = enabledItems, + LoopMode = playlist.LoopMode, + TotalDuration = enabledItems.Sum(i => i.Duration) + }; + } + + /// + /// 检查播放项是否应该在当前时间播放 + /// + private bool IsItemScheduled(PlaylistItem item, DateTime now) + { + if (string.IsNullOrEmpty(item.ScheduleRule)) return true; + + try + { + // 简单的定时规则检查 + // 格式: {"startDate":"2025-01-01","endDate":"2025-12-31","weekdays":[1,2,3,4,5],"startTime":"08:00","endTime":"18:00"} + using var jsonDoc = System.Text.Json.JsonDocument.Parse(item.ScheduleRule); + var root = jsonDoc.RootElement; + + // 检查日期范围 + if (root.TryGetProperty("startDate", out var startDateElem)) + { + var startDate = DateTime.Parse(startDateElem.GetString()!); + if (now < startDate) return false; + } + + if (root.TryGetProperty("endDate", out var endDateElem)) + { + var endDate = DateTime.Parse(endDateElem.GetString()!).AddDays(1).AddSeconds(-1); + if (now > endDate) return false; + } + + // 检查星期 + if (root.TryGetProperty("weekdays", out var weekdaysElem) && weekdaysElem.ValueKind == System.Text.Json.JsonValueKind.Array) + { + var currentDay = (int)now.DayOfWeek; + var allowedDays = weekdaysElem.EnumerateArray().Select(e => e.GetInt32()).ToHashSet(); + if (!allowedDays.Contains(currentDay)) return false; + } + + // 检查时间范围 + var currentTime = now.TimeOfDay; + if (root.TryGetProperty("startTime", out var startTimeElem)) + { + var startTime = TimeSpan.Parse(startTimeElem.GetString()!); + if (currentTime < startTime) return false; + } + + if (root.TryGetProperty("endTime", out var endTimeElem)) + { + var endTime = TimeSpan.Parse(endTimeElem.GetString()!); + if (currentTime > endTime) return false; + } + + return true; + } + catch + { + // 解析失败,默认允许播放 + return true; + } + } + + /// + /// 为播放列表分配到设备分组 + /// + public async Task AssignToGroupAsync(int playlistId, int? groupId) + { + var playlist = await _context.Playlists.FindAsync(playlistId); + if (playlist == null) return false; + + playlist.DeviceGroupId = groupId; + playlist.UpdatedAt = DateTime.UtcNow; + await _context.SaveChangesAsync(); + + // 推送更新到相关设备 + if (groupId.HasValue) + { + var devices = await _context.Devices + .Where(d => d.DeviceGroupId == groupId.Value) + .Select(d => d.Id) + .ToListAsync(); + + foreach (var deviceId in devices) + { + await _wsManager.BroadcastToDeviceAsync(deviceId, new WebSocketMessage + { + Type = "playlist_changed", + Data = new { message = "播放列表已更新" } + }); + } + } + + return true; + } +} diff --git a/src/DRS9.Dashboard.Application/Services/WebSocketConnection.cs b/src/DRS9.Dashboard.Application/Services/WebSocketConnection.cs new file mode 100644 index 0000000..ee953a7 --- /dev/null +++ b/src/DRS9.Dashboard.Application/Services/WebSocketConnection.cs @@ -0,0 +1,12 @@ +using System.Net.WebSockets; + +namespace DRS9.Dashboard.Application.Services; + +public class WebSocketConnection +{ + public required string ConnectionId { get; set; } + public required WebSocket WebSocket { get; set; } + public int? DeviceId { get; set; } + public DateTime ConnectedAt { get; set; } = DateTime.UtcNow; + public DateTime LastActivityAt { get; set; } = DateTime.UtcNow; +} diff --git a/src/DRS9.Dashboard.Domain/DRS9.Dashboard.Domain.csproj b/src/DRS9.Dashboard.Domain/DRS9.Dashboard.Domain.csproj new file mode 100644 index 0000000..125f4c9 --- /dev/null +++ b/src/DRS9.Dashboard.Domain/DRS9.Dashboard.Domain.csproj @@ -0,0 +1,9 @@ + + + + net9.0 + enable + enable + + + diff --git a/src/DRS9.Dashboard.Domain/Entities/AppVersion.cs b/src/DRS9.Dashboard.Domain/Entities/AppVersion.cs new file mode 100644 index 0000000..7f75131 --- /dev/null +++ b/src/DRS9.Dashboard.Domain/Entities/AppVersion.cs @@ -0,0 +1,29 @@ +using System.ComponentModel.DataAnnotations; + +namespace DRS9.Dashboard.Domain.Entities; + +public class AppVersion : BaseEntity +{ + [MaxLength(50)] + public string Platform { get; set; } = string.Empty; // Android, iOS, Windows + + [MaxLength(20)] + public string Version { get; set; } = string.Empty; // e.g., "1.0.0" + + [MaxLength(100)] + public string VersionName { get; set; } = string.Empty; // e.g., "正式版 v1.0" + + [MaxLength(500)] + public string? DownloadUrl { get; set; } + + public long? FileSize { get; set; } + + [MaxLength(1000)] + public string? ChangeLog { get; set; } + + public bool IsForceUpdate { get; set; } = false; + + public bool IsEnabled { get; set; } = true; + + public DateTime? PublishedAt { get; set; } +} diff --git a/src/DRS9.Dashboard.Domain/Entities/Application.cs b/src/DRS9.Dashboard.Domain/Entities/Application.cs new file mode 100644 index 0000000..1ff7ffa --- /dev/null +++ b/src/DRS9.Dashboard.Domain/Entities/Application.cs @@ -0,0 +1,28 @@ +using System.ComponentModel.DataAnnotations; + +namespace DRS9.Dashboard.Domain.Entities; + +public class Application : BaseEntity +{ + [MaxLength(200)] + public string Name { get; set; } = string.Empty; + + [MaxLength(50)] + public string Type { get; set; } = string.Empty; // Dashboard, WebRotator, ImageRotator, Video + + [MaxLength(1000)] + public string? Description { get; set; } + + [MaxLength(2000)] + public string ContentUrl { get; set; } = string.Empty; + + [MaxLength(500)] + public string? ThumbnailUrl { get; set; } + + public int Priority { get; set; } = 0; // 用于排序 + + public bool IsEnabled { get; set; } = true; + + // 导航属性 + public ICollection Assignments { get; set; } = new List(); +} diff --git a/src/DRS9.Dashboard.Domain/Entities/AuditLog.cs b/src/DRS9.Dashboard.Domain/Entities/AuditLog.cs new file mode 100644 index 0000000..0382213 --- /dev/null +++ b/src/DRS9.Dashboard.Domain/Entities/AuditLog.cs @@ -0,0 +1,22 @@ +using System.ComponentModel.DataAnnotations; + +namespace DRS9.Dashboard.Domain.Entities; + +public class AuditLog : BaseEntity +{ + [MaxLength(50)] + public string Action { get; set; } = string.Empty; // Create, Update, Delete, Push, etc. + + [MaxLength(100)] + public string? EntityType { get; set; } + + public int? EntityId { get; set; } + + [MaxLength(50)] + public string? Username { get; set; } + + [MaxLength(100)] + public string? IpAddress { get; set; } + + public string? Details { get; set; } +} diff --git a/src/DRS9.Dashboard.Domain/Entities/BaseEntity.cs b/src/DRS9.Dashboard.Domain/Entities/BaseEntity.cs new file mode 100644 index 0000000..9952875 --- /dev/null +++ b/src/DRS9.Dashboard.Domain/Entities/BaseEntity.cs @@ -0,0 +1,8 @@ +namespace DRS9.Dashboard.Domain.Entities; + +public abstract class BaseEntity +{ + public int Id { get; set; } + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public DateTime? UpdatedAt { get; set; } +} diff --git a/src/DRS9.Dashboard.Domain/Entities/Device.cs b/src/DRS9.Dashboard.Domain/Entities/Device.cs new file mode 100644 index 0000000..f9ccf46 --- /dev/null +++ b/src/DRS9.Dashboard.Domain/Entities/Device.cs @@ -0,0 +1,36 @@ +using System.ComponentModel.DataAnnotations; + +namespace DRS9.Dashboard.Domain.Entities; + +public class Device : BaseEntity +{ + [MaxLength(50)] + public string DeviceCode { get; set; } = string.Empty; + + [MaxLength(200)] + public string DeviceName { get; set; } = string.Empty; + + [MaxLength(100)] + public string? DeviceType { get; set; } // Android, iOS, Windows, Browser + + [MaxLength(50)] + public string? OsVersion { get; set; } + + [MaxLength(100)] + public string? AppVersion { get; set; } + + public bool IsActive { get; set; } = false; + + public bool IsEnabled { get; set; } = true; + + public DateTime? LastSeenAt { get; set; } + + public DateTime? ActivatedAt { get; set; } + + // 外键 + public int? DeviceGroupId { get; set; } + + // 导航属性 + public DeviceGroup? DeviceGroup { get; set; } + public ICollection Assignments { get; set; } = new List(); +} diff --git a/src/DRS9.Dashboard.Domain/Entities/DeviceAssignment.cs b/src/DRS9.Dashboard.Domain/Entities/DeviceAssignment.cs new file mode 100644 index 0000000..b62d0a9 --- /dev/null +++ b/src/DRS9.Dashboard.Domain/Entities/DeviceAssignment.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations; + +namespace DRS9.Dashboard.Domain.Entities; + +public class DeviceAssignment : BaseEntity +{ + public int DeviceId { get; set; } + + public int ApplicationId { get; set; } + + public int Order { get; set; } = 0; + + public int Duration { get; set; } = 60; // 显示时长(秒) + + // 导航属性 + public Device Device { get; set; } = null!; + public Application Application { get; set; } = null!; +} diff --git a/src/DRS9.Dashboard.Domain/Entities/DeviceGroup.cs b/src/DRS9.Dashboard.Domain/Entities/DeviceGroup.cs new file mode 100644 index 0000000..291c426 --- /dev/null +++ b/src/DRS9.Dashboard.Domain/Entities/DeviceGroup.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; + +namespace DRS9.Dashboard.Domain.Entities; + +public class DeviceGroup : BaseEntity +{ + [MaxLength(100)] + public string Name { get; set; } = string.Empty; + + [MaxLength(500)] + public string? Description { get; set; } + + // 导航属性 + public ICollection Devices { get; set; } = new List(); +} diff --git a/src/DRS9.Dashboard.Domain/Entities/Playlist.cs b/src/DRS9.Dashboard.Domain/Entities/Playlist.cs new file mode 100644 index 0000000..31f3b53 --- /dev/null +++ b/src/DRS9.Dashboard.Domain/Entities/Playlist.cs @@ -0,0 +1,57 @@ +using System.ComponentModel.DataAnnotations; + +namespace DRS9.Dashboard.Domain.Entities; + +public class Playlist : BaseEntity +{ + [MaxLength(200)] + public string Name { get; set; } = string.Empty; + + [MaxLength(1000)] + public string? Description { get; set; } + + /// + /// 循环模式: Loop(循环播放), Once(播放一次), Shuffle(随机播放) + /// + [MaxLength(20)] + public string LoopMode { get; set; } = "Loop"; + + public int? DeviceGroupId { get; set; } + + public bool IsEnabled { get; set; } = true; + + // 导航属性 + public DeviceGroup? DeviceGroup { get; set; } + public ICollection Items { get; set; } = new List(); +} + +public class PlaylistItem : BaseEntity +{ + public int PlaylistId { get; set; } + + public int ApplicationId { get; set; } + + /// + /// 播放顺序 + /// + public int Order { get; set; } + + /// + /// 显示时长(秒),0 表示使用默认值 + /// + public int Duration { get; set; } = 60; + + /// + /// 是否启用 + /// + public bool IsEnabled { get; set; } = true; + + /// + /// 定时规则:JSON 格式,例如 {"startDate":"2025-01-01","endDate":"2025-12-31","weekdays":[1,2,3,4,5],"startTime":"08:00","endTime":"18:00"} + /// + public string? ScheduleRule { get; set; } + + // 导航属性 + public Playlist Playlist { get; set; } = null!; + public Application Application { get; set; } = null!; +} diff --git a/src/DRS9.Dashboard.Domain/Entities/User.cs b/src/DRS9.Dashboard.Domain/Entities/User.cs new file mode 100644 index 0000000..379b9ac --- /dev/null +++ b/src/DRS9.Dashboard.Domain/Entities/User.cs @@ -0,0 +1,20 @@ +using System.ComponentModel.DataAnnotations; + +namespace DRS9.Dashboard.Domain.Entities; + +public class User : BaseEntity +{ + [MaxLength(100)] + public string Username { get; set; } = string.Empty; + + [MaxLength(255)] + public string PasswordHash { get; set; } = string.Empty; + + [MaxLength(100)] + public string Email { get; set; } = string.Empty; + + [MaxLength(20)] + public string Role { get; set; } = "User"; // Admin, User + + public bool IsActive { get; set; } = true; +} diff --git a/src/DRS9.Dashboard.Infrastructure/Class1.cs b/src/DRS9.Dashboard.Infrastructure/Class1.cs new file mode 100644 index 0000000..8c59bd1 --- /dev/null +++ b/src/DRS9.Dashboard.Infrastructure/Class1.cs @@ -0,0 +1,6 @@ +namespace DRS9.Dashboard.Infrastructure; + +public class Class1 +{ + +} diff --git a/src/DRS9.Dashboard.Infrastructure/DRS9.Dashboard.Infrastructure.csproj b/src/DRS9.Dashboard.Infrastructure/DRS9.Dashboard.Infrastructure.csproj new file mode 100644 index 0000000..45f5fe6 --- /dev/null +++ b/src/DRS9.Dashboard.Infrastructure/DRS9.Dashboard.Infrastructure.csproj @@ -0,0 +1,18 @@ + + + + + + + + + + + + + net9.0 + enable + enable + + + diff --git a/src/DRS9.Dashboard.Infrastructure/Data/DashboardDbContext.cs b/src/DRS9.Dashboard.Infrastructure/Data/DashboardDbContext.cs new file mode 100644 index 0000000..a63a081 --- /dev/null +++ b/src/DRS9.Dashboard.Infrastructure/Data/DashboardDbContext.cs @@ -0,0 +1,105 @@ +using DRS9.Dashboard.Domain.Entities; +using Microsoft.EntityFrameworkCore; + +namespace DRS9.Dashboard.Infrastructure.Data; + +public class DashboardDbContext : DbContext +{ + public DashboardDbContext(DbContextOptions options) : base(options) + { + } + + public DbSet Devices { get; set; } + public DbSet DeviceGroups { get; set; } + public DbSet Applications { get; set; } + public DbSet DeviceAssignments { get; set; } + public DbSet Users { get; set; } + public DbSet AuditLogs { get; set; } + public DbSet Playlists { get; set; } + public DbSet PlaylistItems { get; set; } + public DbSet AppVersions { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + // DeviceGroup 配置 + modelBuilder.Entity(entity => + { + entity.HasIndex(e => e.Name).IsUnique(); + }); + + // Device 配置 + modelBuilder.Entity(entity => + { + entity.HasIndex(e => e.DeviceCode).IsUnique(); + entity.HasOne(e => e.DeviceGroup) + .WithMany(g => g.Devices) + .HasForeignKey(e => e.DeviceGroupId) + .OnDelete(DeleteBehavior.SetNull); + }); + + // Application 配置 + modelBuilder.Entity(entity => + { + entity.HasIndex(e => e.Name); + }); + + // DeviceAssignment 配置(多对多关系的中间表) + modelBuilder.Entity(entity => + { + entity.HasOne(e => e.Device) + .WithMany(d => d.Assignments) + .HasForeignKey(e => e.DeviceId) + .OnDelete(DeleteBehavior.Cascade); + + entity.HasOne(e => e.Application) + .WithMany(a => a.Assignments) + .HasForeignKey(e => e.ApplicationId) + .OnDelete(DeleteBehavior.Cascade); + + // 确保同一设备的同一应用只有一个分配 + entity.HasIndex(e => new { e.DeviceId, e.ApplicationId }).IsUnique(); + }); + + // User 配置 + modelBuilder.Entity(entity => + { + entity.HasIndex(e => e.Username).IsUnique(); + entity.HasIndex(e => e.Email).IsUnique(); + }); + + // Playlist 配置 + modelBuilder.Entity(entity => + { + entity.HasIndex(e => e.Name); + entity.HasOne(e => e.DeviceGroup) + .WithMany() + .HasForeignKey(e => e.DeviceGroupId) + .OnDelete(DeleteBehavior.SetNull); + }); + + // PlaylistItem 配置 + modelBuilder.Entity(entity => + { + entity.HasOne(e => e.Playlist) + .WithMany(p => p.Items) + .HasForeignKey(e => e.PlaylistId) + .OnDelete(DeleteBehavior.Cascade); + + entity.HasOne(e => e.Application) + .WithMany() + .HasForeignKey(e => e.ApplicationId) + .OnDelete(DeleteBehavior.Cascade); + + // 同一播放列表中的项目按 Order 排序 + entity.HasIndex(e => new { e.PlaylistId, e.Order }); + }); + + // AppVersion 配置 + modelBuilder.Entity(entity => + { + entity.HasIndex(e => new { e.Platform, e.Version }); + }); + } +} diff --git a/src/DRS9.Dashboard.Infrastructure/Data/DataSeeder.cs b/src/DRS9.Dashboard.Infrastructure/Data/DataSeeder.cs new file mode 100644 index 0000000..2f13d7a --- /dev/null +++ b/src/DRS9.Dashboard.Infrastructure/Data/DataSeeder.cs @@ -0,0 +1,44 @@ +using DRS9.Dashboard.Domain.Entities; + +namespace DRS9.Dashboard.Infrastructure.Data; + +public static class DataSeeder +{ + public static async Task SeedAsync(DashboardDbContext context) + { + // 确保数据库已创建 + await context.Database.EnsureCreatedAsync(); + + // 添加测试设备码 + if (!context.Devices.Any()) + { + var devices = new List + { + new() { DeviceCode = "TEST-001-DEV", DeviceName = "测试设备1", IsEnabled = true }, + new() { DeviceCode = "TEST-002-DEV", DeviceName = "测试设备2", IsEnabled = true }, + new() { DeviceCode = "TEST-003-DEV", DeviceName = "测试设备3", IsEnabled = true }, + new() { DeviceCode = "SCHOOL-001-DS", DeviceName = "学校显示屏1", IsEnabled = true }, + new() { DeviceCode = "MALL-001-DS", DeviceName = "商场广告屏1", IsEnabled = true } + }; + + await context.Devices.AddRangeAsync(devices); + await context.SaveChangesAsync(); + Console.WriteLine("已添加 5 个测试设备码"); + } + + // 添加测试应用 + if (!context.Applications.Any()) + { + var apps = new List + { + new() { Name = "欢迎页面", Type = "Dashboard", ContentUrl = "/dashboard/welcome", IsEnabled = true, Priority = 1 }, + new() { Name = "新闻轮转", Type = "WebRotator", ContentUrl = "/rotator/news", IsEnabled = true, Priority = 2 }, + new() { Name = "图片轮播", Type = "ImageRotator", ContentUrl = "/gallery/promo", IsEnabled = true, Priority = 3 } + }; + + await context.Applications.AddRangeAsync(apps); + await context.SaveChangesAsync(); + Console.WriteLine("已添加 3 个测试应用"); + } + } +} diff --git a/src/DRS9.Dashboard.Infrastructure/Migrations/20260112082557_InitialCreate.Designer.cs b/src/DRS9.Dashboard.Infrastructure/Migrations/20260112082557_InitialCreate.Designer.cs new file mode 100644 index 0000000..38c8d11 --- /dev/null +++ b/src/DRS9.Dashboard.Infrastructure/Migrations/20260112082557_InitialCreate.Designer.cs @@ -0,0 +1,325 @@ +// +using System; +using DRS9.Dashboard.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace DRS9.Dashboard.Infrastructure.Migrations +{ + [DbContext(typeof(DashboardDbContext))] + [Migration("20260112082557_InitialCreate")] + partial class InitialCreate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.11"); + + modelBuilder.Entity("DRS9.Dashboard.Domain.Entities.Application", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ContentUrl") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("IsEnabled") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Priority") + .HasColumnType("INTEGER"); + + b.Property("ThumbnailUrl") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.ToTable("Applications"); + }); + + modelBuilder.Entity("DRS9.Dashboard.Domain.Entities.AuditLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Details") + .HasColumnType("TEXT"); + + b.Property("EntityId") + .HasColumnType("INTEGER"); + + b.Property("EntityType") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IpAddress") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("Username") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("AuditLogs"); + }); + + modelBuilder.Entity("DRS9.Dashboard.Domain.Entities.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ActivatedAt") + .HasColumnType("TEXT"); + + b.Property("AppVersion") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DeviceCode") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("DeviceGroupId") + .HasColumnType("INTEGER"); + + b.Property("DeviceName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("DeviceType") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsEnabled") + .HasColumnType("INTEGER"); + + b.Property("LastSeenAt") + .HasColumnType("TEXT"); + + b.Property("OsVersion") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceCode") + .IsUnique(); + + b.HasIndex("DeviceGroupId"); + + b.ToTable("Devices"); + }); + + modelBuilder.Entity("DRS9.Dashboard.Domain.Entities.DeviceAssignment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ApplicationId") + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .HasColumnType("INTEGER"); + + b.Property("Duration") + .HasColumnType("INTEGER"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("DeviceId", "ApplicationId") + .IsUnique(); + + b.ToTable("DeviceAssignments"); + }); + + modelBuilder.Entity("DRS9.Dashboard.Domain.Entities.DeviceGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("DeviceGroups"); + }); + + modelBuilder.Entity("DRS9.Dashboard.Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("DRS9.Dashboard.Domain.Entities.Device", b => + { + b.HasOne("DRS9.Dashboard.Domain.Entities.DeviceGroup", "DeviceGroup") + .WithMany("Devices") + .HasForeignKey("DeviceGroupId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("DeviceGroup"); + }); + + modelBuilder.Entity("DRS9.Dashboard.Domain.Entities.DeviceAssignment", b => + { + b.HasOne("DRS9.Dashboard.Domain.Entities.Application", "Application") + .WithMany("Assignments") + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DRS9.Dashboard.Domain.Entities.Device", "Device") + .WithMany("Assignments") + .HasForeignKey("DeviceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Application"); + + b.Navigation("Device"); + }); + + modelBuilder.Entity("DRS9.Dashboard.Domain.Entities.Application", b => + { + b.Navigation("Assignments"); + }); + + modelBuilder.Entity("DRS9.Dashboard.Domain.Entities.Device", b => + { + b.Navigation("Assignments"); + }); + + modelBuilder.Entity("DRS9.Dashboard.Domain.Entities.DeviceGroup", b => + { + b.Navigation("Devices"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/DRS9.Dashboard.Infrastructure/Migrations/20260112082557_InitialCreate.cs b/src/DRS9.Dashboard.Infrastructure/Migrations/20260112082557_InitialCreate.cs new file mode 100644 index 0000000..067c7e4 --- /dev/null +++ b/src/DRS9.Dashboard.Infrastructure/Migrations/20260112082557_InitialCreate.cs @@ -0,0 +1,218 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace DRS9.Dashboard.Infrastructure.Migrations +{ + /// + public partial class InitialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Applications", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Name = table.Column(type: "TEXT", maxLength: 200, nullable: false), + Type = table.Column(type: "TEXT", maxLength: 50, nullable: false), + Description = table.Column(type: "TEXT", maxLength: 1000, nullable: true), + ContentUrl = table.Column(type: "TEXT", maxLength: 2000, nullable: false), + ThumbnailUrl = table.Column(type: "TEXT", maxLength: 500, nullable: true), + Priority = table.Column(type: "INTEGER", nullable: false), + IsEnabled = table.Column(type: "INTEGER", nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: false), + UpdatedAt = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Applications", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AuditLogs", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Action = table.Column(type: "TEXT", maxLength: 50, nullable: false), + EntityType = table.Column(type: "TEXT", maxLength: 100, nullable: true), + EntityId = table.Column(type: "INTEGER", nullable: true), + Username = table.Column(type: "TEXT", maxLength: 50, nullable: true), + IpAddress = table.Column(type: "TEXT", maxLength: 100, nullable: true), + Details = table.Column(type: "TEXT", nullable: true), + CreatedAt = table.Column(type: "TEXT", nullable: false), + UpdatedAt = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AuditLogs", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "DeviceGroups", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Name = table.Column(type: "TEXT", maxLength: 100, nullable: false), + Description = table.Column(type: "TEXT", maxLength: 500, nullable: true), + CreatedAt = table.Column(type: "TEXT", nullable: false), + UpdatedAt = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_DeviceGroups", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Users", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Username = table.Column(type: "TEXT", maxLength: 100, nullable: false), + PasswordHash = table.Column(type: "TEXT", maxLength: 255, nullable: false), + Email = table.Column(type: "TEXT", maxLength: 100, nullable: false), + Role = table.Column(type: "TEXT", maxLength: 20, nullable: false), + IsActive = table.Column(type: "INTEGER", nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: false), + UpdatedAt = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Users", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Devices", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + DeviceCode = table.Column(type: "TEXT", maxLength: 50, nullable: false), + DeviceName = table.Column(type: "TEXT", maxLength: 200, nullable: false), + DeviceType = table.Column(type: "TEXT", maxLength: 100, nullable: true), + OsVersion = table.Column(type: "TEXT", maxLength: 50, nullable: true), + AppVersion = table.Column(type: "TEXT", maxLength: 100, nullable: true), + IsActive = table.Column(type: "INTEGER", nullable: false), + IsEnabled = table.Column(type: "INTEGER", nullable: false), + LastSeenAt = table.Column(type: "TEXT", nullable: true), + ActivatedAt = table.Column(type: "TEXT", nullable: true), + DeviceGroupId = table.Column(type: "INTEGER", nullable: true), + CreatedAt = table.Column(type: "TEXT", nullable: false), + UpdatedAt = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Devices", x => x.Id); + table.ForeignKey( + name: "FK_Devices_DeviceGroups_DeviceGroupId", + column: x => x.DeviceGroupId, + principalTable: "DeviceGroups", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + }); + + migrationBuilder.CreateTable( + name: "DeviceAssignments", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + DeviceId = table.Column(type: "INTEGER", nullable: false), + ApplicationId = table.Column(type: "INTEGER", nullable: false), + Order = table.Column(type: "INTEGER", nullable: false), + Duration = table.Column(type: "INTEGER", nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: false), + UpdatedAt = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_DeviceAssignments", x => x.Id); + table.ForeignKey( + name: "FK_DeviceAssignments_Applications_ApplicationId", + column: x => x.ApplicationId, + principalTable: "Applications", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_DeviceAssignments_Devices_DeviceId", + column: x => x.DeviceId, + principalTable: "Devices", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Applications_Name", + table: "Applications", + column: "Name"); + + migrationBuilder.CreateIndex( + name: "IX_DeviceAssignments_ApplicationId", + table: "DeviceAssignments", + column: "ApplicationId"); + + migrationBuilder.CreateIndex( + name: "IX_DeviceAssignments_DeviceId_ApplicationId", + table: "DeviceAssignments", + columns: new[] { "DeviceId", "ApplicationId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_DeviceGroups_Name", + table: "DeviceGroups", + column: "Name", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Devices_DeviceCode", + table: "Devices", + column: "DeviceCode", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Devices_DeviceGroupId", + table: "Devices", + column: "DeviceGroupId"); + + migrationBuilder.CreateIndex( + name: "IX_Users_Email", + table: "Users", + column: "Email", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Users_Username", + table: "Users", + column: "Username", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AuditLogs"); + + migrationBuilder.DropTable( + name: "DeviceAssignments"); + + migrationBuilder.DropTable( + name: "Users"); + + migrationBuilder.DropTable( + name: "Applications"); + + migrationBuilder.DropTable( + name: "Devices"); + + migrationBuilder.DropTable( + name: "DeviceGroups"); + } + } +} diff --git a/src/DRS9.Dashboard.Infrastructure/Migrations/20260112091958_AddPlaylists.Designer.cs b/src/DRS9.Dashboard.Infrastructure/Migrations/20260112091958_AddPlaylists.Designer.cs new file mode 100644 index 0000000..2171e9a --- /dev/null +++ b/src/DRS9.Dashboard.Infrastructure/Migrations/20260112091958_AddPlaylists.Designer.cs @@ -0,0 +1,439 @@ +// +using System; +using DRS9.Dashboard.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace DRS9.Dashboard.Infrastructure.Migrations +{ + [DbContext(typeof(DashboardDbContext))] + [Migration("20260112091958_AddPlaylists")] + partial class AddPlaylists + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.11"); + + modelBuilder.Entity("DRS9.Dashboard.Domain.Entities.Application", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ContentUrl") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("IsEnabled") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Priority") + .HasColumnType("INTEGER"); + + b.Property("ThumbnailUrl") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.ToTable("Applications"); + }); + + modelBuilder.Entity("DRS9.Dashboard.Domain.Entities.AuditLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Details") + .HasColumnType("TEXT"); + + b.Property("EntityId") + .HasColumnType("INTEGER"); + + b.Property("EntityType") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IpAddress") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("Username") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("AuditLogs"); + }); + + modelBuilder.Entity("DRS9.Dashboard.Domain.Entities.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ActivatedAt") + .HasColumnType("TEXT"); + + b.Property("AppVersion") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DeviceCode") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("DeviceGroupId") + .HasColumnType("INTEGER"); + + b.Property("DeviceName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("DeviceType") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsEnabled") + .HasColumnType("INTEGER"); + + b.Property("LastSeenAt") + .HasColumnType("TEXT"); + + b.Property("OsVersion") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceCode") + .IsUnique(); + + b.HasIndex("DeviceGroupId"); + + b.ToTable("Devices"); + }); + + modelBuilder.Entity("DRS9.Dashboard.Domain.Entities.DeviceAssignment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ApplicationId") + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .HasColumnType("INTEGER"); + + b.Property("Duration") + .HasColumnType("INTEGER"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("DeviceId", "ApplicationId") + .IsUnique(); + + b.ToTable("DeviceAssignments"); + }); + + modelBuilder.Entity("DRS9.Dashboard.Domain.Entities.DeviceGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("DeviceGroups"); + }); + + modelBuilder.Entity("DRS9.Dashboard.Domain.Entities.Playlist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("DeviceGroupId") + .HasColumnType("INTEGER"); + + b.Property("IsEnabled") + .HasColumnType("INTEGER"); + + b.Property("LoopMode") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceGroupId"); + + b.HasIndex("Name"); + + b.ToTable("Playlists"); + }); + + modelBuilder.Entity("DRS9.Dashboard.Domain.Entities.PlaylistItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ApplicationId") + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Duration") + .HasColumnType("INTEGER"); + + b.Property("IsEnabled") + .HasColumnType("INTEGER"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("PlaylistId") + .HasColumnType("INTEGER"); + + b.Property("ScheduleRule") + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("PlaylistId", "Order"); + + b.ToTable("PlaylistItems"); + }); + + modelBuilder.Entity("DRS9.Dashboard.Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("DRS9.Dashboard.Domain.Entities.Device", b => + { + b.HasOne("DRS9.Dashboard.Domain.Entities.DeviceGroup", "DeviceGroup") + .WithMany("Devices") + .HasForeignKey("DeviceGroupId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("DeviceGroup"); + }); + + modelBuilder.Entity("DRS9.Dashboard.Domain.Entities.DeviceAssignment", b => + { + b.HasOne("DRS9.Dashboard.Domain.Entities.Application", "Application") + .WithMany("Assignments") + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DRS9.Dashboard.Domain.Entities.Device", "Device") + .WithMany("Assignments") + .HasForeignKey("DeviceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Application"); + + b.Navigation("Device"); + }); + + modelBuilder.Entity("DRS9.Dashboard.Domain.Entities.Playlist", b => + { + b.HasOne("DRS9.Dashboard.Domain.Entities.DeviceGroup", "DeviceGroup") + .WithMany() + .HasForeignKey("DeviceGroupId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("DeviceGroup"); + }); + + modelBuilder.Entity("DRS9.Dashboard.Domain.Entities.PlaylistItem", b => + { + b.HasOne("DRS9.Dashboard.Domain.Entities.Application", "Application") + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DRS9.Dashboard.Domain.Entities.Playlist", "Playlist") + .WithMany("Items") + .HasForeignKey("PlaylistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Application"); + + b.Navigation("Playlist"); + }); + + modelBuilder.Entity("DRS9.Dashboard.Domain.Entities.Application", b => + { + b.Navigation("Assignments"); + }); + + modelBuilder.Entity("DRS9.Dashboard.Domain.Entities.Device", b => + { + b.Navigation("Assignments"); + }); + + modelBuilder.Entity("DRS9.Dashboard.Domain.Entities.DeviceGroup", b => + { + b.Navigation("Devices"); + }); + + modelBuilder.Entity("DRS9.Dashboard.Domain.Entities.Playlist", b => + { + b.Navigation("Items"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/DRS9.Dashboard.Infrastructure/Migrations/20260112091958_AddPlaylists.cs b/src/DRS9.Dashboard.Infrastructure/Migrations/20260112091958_AddPlaylists.cs new file mode 100644 index 0000000..a673348 --- /dev/null +++ b/src/DRS9.Dashboard.Infrastructure/Migrations/20260112091958_AddPlaylists.cs @@ -0,0 +1,102 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace DRS9.Dashboard.Infrastructure.Migrations +{ + /// + public partial class AddPlaylists : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Playlists", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Name = table.Column(type: "TEXT", maxLength: 200, nullable: false), + Description = table.Column(type: "TEXT", maxLength: 1000, nullable: true), + LoopMode = table.Column(type: "TEXT", maxLength: 20, nullable: false), + DeviceGroupId = table.Column(type: "INTEGER", nullable: true), + IsEnabled = table.Column(type: "INTEGER", nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: false), + UpdatedAt = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Playlists", x => x.Id); + table.ForeignKey( + name: "FK_Playlists_DeviceGroups_DeviceGroupId", + column: x => x.DeviceGroupId, + principalTable: "DeviceGroups", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + }); + + migrationBuilder.CreateTable( + name: "PlaylistItems", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + PlaylistId = table.Column(type: "INTEGER", nullable: false), + ApplicationId = table.Column(type: "INTEGER", nullable: false), + Order = table.Column(type: "INTEGER", nullable: false), + Duration = table.Column(type: "INTEGER", nullable: false), + IsEnabled = table.Column(type: "INTEGER", nullable: false), + ScheduleRule = table.Column(type: "TEXT", nullable: true), + CreatedAt = table.Column(type: "TEXT", nullable: false), + UpdatedAt = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_PlaylistItems", x => x.Id); + table.ForeignKey( + name: "FK_PlaylistItems_Applications_ApplicationId", + column: x => x.ApplicationId, + principalTable: "Applications", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_PlaylistItems_Playlists_PlaylistId", + column: x => x.PlaylistId, + principalTable: "Playlists", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_PlaylistItems_ApplicationId", + table: "PlaylistItems", + column: "ApplicationId"); + + migrationBuilder.CreateIndex( + name: "IX_PlaylistItems_PlaylistId_Order", + table: "PlaylistItems", + columns: new[] { "PlaylistId", "Order" }); + + migrationBuilder.CreateIndex( + name: "IX_Playlists_DeviceGroupId", + table: "Playlists", + column: "DeviceGroupId"); + + migrationBuilder.CreateIndex( + name: "IX_Playlists_Name", + table: "Playlists", + column: "Name"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "PlaylistItems"); + + migrationBuilder.DropTable( + name: "Playlists"); + } + } +} diff --git a/src/DRS9.Dashboard.Infrastructure/Migrations/20260112092747_AddAppVersions.Designer.cs b/src/DRS9.Dashboard.Infrastructure/Migrations/20260112092747_AddAppVersions.Designer.cs new file mode 100644 index 0000000..583ae08 --- /dev/null +++ b/src/DRS9.Dashboard.Infrastructure/Migrations/20260112092747_AddAppVersions.Designer.cs @@ -0,0 +1,493 @@ +// +using System; +using DRS9.Dashboard.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace DRS9.Dashboard.Infrastructure.Migrations +{ + [DbContext(typeof(DashboardDbContext))] + [Migration("20260112092747_AddAppVersions")] + partial class AddAppVersions + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.11"); + + modelBuilder.Entity("DRS9.Dashboard.Domain.Entities.AppVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChangeLog") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DownloadUrl") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("FileSize") + .HasColumnType("INTEGER"); + + b.Property("IsEnabled") + .HasColumnType("INTEGER"); + + b.Property("IsForceUpdate") + .HasColumnType("INTEGER"); + + b.Property("Platform") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PublishedAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("Version") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("VersionName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Platform", "Version"); + + b.ToTable("AppVersions"); + }); + + modelBuilder.Entity("DRS9.Dashboard.Domain.Entities.Application", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ContentUrl") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("IsEnabled") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Priority") + .HasColumnType("INTEGER"); + + b.Property("ThumbnailUrl") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.ToTable("Applications"); + }); + + modelBuilder.Entity("DRS9.Dashboard.Domain.Entities.AuditLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Details") + .HasColumnType("TEXT"); + + b.Property("EntityId") + .HasColumnType("INTEGER"); + + b.Property("EntityType") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IpAddress") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("Username") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("AuditLogs"); + }); + + modelBuilder.Entity("DRS9.Dashboard.Domain.Entities.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ActivatedAt") + .HasColumnType("TEXT"); + + b.Property("AppVersion") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DeviceCode") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("DeviceGroupId") + .HasColumnType("INTEGER"); + + b.Property("DeviceName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("DeviceType") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsEnabled") + .HasColumnType("INTEGER"); + + b.Property("LastSeenAt") + .HasColumnType("TEXT"); + + b.Property("OsVersion") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceCode") + .IsUnique(); + + b.HasIndex("DeviceGroupId"); + + b.ToTable("Devices"); + }); + + modelBuilder.Entity("DRS9.Dashboard.Domain.Entities.DeviceAssignment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ApplicationId") + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .HasColumnType("INTEGER"); + + b.Property("Duration") + .HasColumnType("INTEGER"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("DeviceId", "ApplicationId") + .IsUnique(); + + b.ToTable("DeviceAssignments"); + }); + + modelBuilder.Entity("DRS9.Dashboard.Domain.Entities.DeviceGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("DeviceGroups"); + }); + + modelBuilder.Entity("DRS9.Dashboard.Domain.Entities.Playlist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("DeviceGroupId") + .HasColumnType("INTEGER"); + + b.Property("IsEnabled") + .HasColumnType("INTEGER"); + + b.Property("LoopMode") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceGroupId"); + + b.HasIndex("Name"); + + b.ToTable("Playlists"); + }); + + modelBuilder.Entity("DRS9.Dashboard.Domain.Entities.PlaylistItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ApplicationId") + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Duration") + .HasColumnType("INTEGER"); + + b.Property("IsEnabled") + .HasColumnType("INTEGER"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("PlaylistId") + .HasColumnType("INTEGER"); + + b.Property("ScheduleRule") + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("PlaylistId", "Order"); + + b.ToTable("PlaylistItems"); + }); + + modelBuilder.Entity("DRS9.Dashboard.Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("DRS9.Dashboard.Domain.Entities.Device", b => + { + b.HasOne("DRS9.Dashboard.Domain.Entities.DeviceGroup", "DeviceGroup") + .WithMany("Devices") + .HasForeignKey("DeviceGroupId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("DeviceGroup"); + }); + + modelBuilder.Entity("DRS9.Dashboard.Domain.Entities.DeviceAssignment", b => + { + b.HasOne("DRS9.Dashboard.Domain.Entities.Application", "Application") + .WithMany("Assignments") + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DRS9.Dashboard.Domain.Entities.Device", "Device") + .WithMany("Assignments") + .HasForeignKey("DeviceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Application"); + + b.Navigation("Device"); + }); + + modelBuilder.Entity("DRS9.Dashboard.Domain.Entities.Playlist", b => + { + b.HasOne("DRS9.Dashboard.Domain.Entities.DeviceGroup", "DeviceGroup") + .WithMany() + .HasForeignKey("DeviceGroupId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("DeviceGroup"); + }); + + modelBuilder.Entity("DRS9.Dashboard.Domain.Entities.PlaylistItem", b => + { + b.HasOne("DRS9.Dashboard.Domain.Entities.Application", "Application") + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DRS9.Dashboard.Domain.Entities.Playlist", "Playlist") + .WithMany("Items") + .HasForeignKey("PlaylistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Application"); + + b.Navigation("Playlist"); + }); + + modelBuilder.Entity("DRS9.Dashboard.Domain.Entities.Application", b => + { + b.Navigation("Assignments"); + }); + + modelBuilder.Entity("DRS9.Dashboard.Domain.Entities.Device", b => + { + b.Navigation("Assignments"); + }); + + modelBuilder.Entity("DRS9.Dashboard.Domain.Entities.DeviceGroup", b => + { + b.Navigation("Devices"); + }); + + modelBuilder.Entity("DRS9.Dashboard.Domain.Entities.Playlist", b => + { + b.Navigation("Items"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/DRS9.Dashboard.Infrastructure/Migrations/20260112092747_AddAppVersions.cs b/src/DRS9.Dashboard.Infrastructure/Migrations/20260112092747_AddAppVersions.cs new file mode 100644 index 0000000..4e06826 --- /dev/null +++ b/src/DRS9.Dashboard.Infrastructure/Migrations/20260112092747_AddAppVersions.cs @@ -0,0 +1,50 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace DRS9.Dashboard.Infrastructure.Migrations +{ + /// + public partial class AddAppVersions : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AppVersions", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Platform = table.Column(type: "TEXT", maxLength: 50, nullable: false), + Version = table.Column(type: "TEXT", maxLength: 20, nullable: false), + VersionName = table.Column(type: "TEXT", maxLength: 100, nullable: false), + DownloadUrl = table.Column(type: "TEXT", maxLength: 500, nullable: true), + FileSize = table.Column(type: "INTEGER", nullable: true), + ChangeLog = table.Column(type: "TEXT", maxLength: 1000, nullable: true), + IsForceUpdate = table.Column(type: "INTEGER", nullable: false), + IsEnabled = table.Column(type: "INTEGER", nullable: false), + PublishedAt = table.Column(type: "TEXT", nullable: true), + CreatedAt = table.Column(type: "TEXT", nullable: false), + UpdatedAt = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AppVersions", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_AppVersions_Platform_Version", + table: "AppVersions", + columns: new[] { "Platform", "Version" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AppVersions"); + } + } +} diff --git a/src/DRS9.Dashboard.Infrastructure/Migrations/DashboardDbContextModelSnapshot.cs b/src/DRS9.Dashboard.Infrastructure/Migrations/DashboardDbContextModelSnapshot.cs new file mode 100644 index 0000000..54aa119 --- /dev/null +++ b/src/DRS9.Dashboard.Infrastructure/Migrations/DashboardDbContextModelSnapshot.cs @@ -0,0 +1,490 @@ +// +using System; +using DRS9.Dashboard.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace DRS9.Dashboard.Infrastructure.Migrations +{ + [DbContext(typeof(DashboardDbContext))] + partial class DashboardDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.11"); + + modelBuilder.Entity("DRS9.Dashboard.Domain.Entities.AppVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChangeLog") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DownloadUrl") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("FileSize") + .HasColumnType("INTEGER"); + + b.Property("IsEnabled") + .HasColumnType("INTEGER"); + + b.Property("IsForceUpdate") + .HasColumnType("INTEGER"); + + b.Property("Platform") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PublishedAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("Version") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("VersionName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Platform", "Version"); + + b.ToTable("AppVersions"); + }); + + modelBuilder.Entity("DRS9.Dashboard.Domain.Entities.Application", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ContentUrl") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("IsEnabled") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Priority") + .HasColumnType("INTEGER"); + + b.Property("ThumbnailUrl") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.ToTable("Applications"); + }); + + modelBuilder.Entity("DRS9.Dashboard.Domain.Entities.AuditLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Details") + .HasColumnType("TEXT"); + + b.Property("EntityId") + .HasColumnType("INTEGER"); + + b.Property("EntityType") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IpAddress") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("Username") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("AuditLogs"); + }); + + modelBuilder.Entity("DRS9.Dashboard.Domain.Entities.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ActivatedAt") + .HasColumnType("TEXT"); + + b.Property("AppVersion") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DeviceCode") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("DeviceGroupId") + .HasColumnType("INTEGER"); + + b.Property("DeviceName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("DeviceType") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("IsEnabled") + .HasColumnType("INTEGER"); + + b.Property("LastSeenAt") + .HasColumnType("TEXT"); + + b.Property("OsVersion") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceCode") + .IsUnique(); + + b.HasIndex("DeviceGroupId"); + + b.ToTable("Devices"); + }); + + modelBuilder.Entity("DRS9.Dashboard.Domain.Entities.DeviceAssignment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ApplicationId") + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .HasColumnType("INTEGER"); + + b.Property("Duration") + .HasColumnType("INTEGER"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("DeviceId", "ApplicationId") + .IsUnique(); + + b.ToTable("DeviceAssignments"); + }); + + modelBuilder.Entity("DRS9.Dashboard.Domain.Entities.DeviceGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("DeviceGroups"); + }); + + modelBuilder.Entity("DRS9.Dashboard.Domain.Entities.Playlist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("DeviceGroupId") + .HasColumnType("INTEGER"); + + b.Property("IsEnabled") + .HasColumnType("INTEGER"); + + b.Property("LoopMode") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceGroupId"); + + b.HasIndex("Name"); + + b.ToTable("Playlists"); + }); + + modelBuilder.Entity("DRS9.Dashboard.Domain.Entities.PlaylistItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ApplicationId") + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Duration") + .HasColumnType("INTEGER"); + + b.Property("IsEnabled") + .HasColumnType("INTEGER"); + + b.Property("Order") + .HasColumnType("INTEGER"); + + b.Property("PlaylistId") + .HasColumnType("INTEGER"); + + b.Property("ScheduleRule") + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId"); + + b.HasIndex("PlaylistId", "Order"); + + b.ToTable("PlaylistItems"); + }); + + modelBuilder.Entity("DRS9.Dashboard.Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("DRS9.Dashboard.Domain.Entities.Device", b => + { + b.HasOne("DRS9.Dashboard.Domain.Entities.DeviceGroup", "DeviceGroup") + .WithMany("Devices") + .HasForeignKey("DeviceGroupId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("DeviceGroup"); + }); + + modelBuilder.Entity("DRS9.Dashboard.Domain.Entities.DeviceAssignment", b => + { + b.HasOne("DRS9.Dashboard.Domain.Entities.Application", "Application") + .WithMany("Assignments") + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DRS9.Dashboard.Domain.Entities.Device", "Device") + .WithMany("Assignments") + .HasForeignKey("DeviceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Application"); + + b.Navigation("Device"); + }); + + modelBuilder.Entity("DRS9.Dashboard.Domain.Entities.Playlist", b => + { + b.HasOne("DRS9.Dashboard.Domain.Entities.DeviceGroup", "DeviceGroup") + .WithMany() + .HasForeignKey("DeviceGroupId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("DeviceGroup"); + }); + + modelBuilder.Entity("DRS9.Dashboard.Domain.Entities.PlaylistItem", b => + { + b.HasOne("DRS9.Dashboard.Domain.Entities.Application", "Application") + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DRS9.Dashboard.Domain.Entities.Playlist", "Playlist") + .WithMany("Items") + .HasForeignKey("PlaylistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Application"); + + b.Navigation("Playlist"); + }); + + modelBuilder.Entity("DRS9.Dashboard.Domain.Entities.Application", b => + { + b.Navigation("Assignments"); + }); + + modelBuilder.Entity("DRS9.Dashboard.Domain.Entities.Device", b => + { + b.Navigation("Assignments"); + }); + + modelBuilder.Entity("DRS9.Dashboard.Domain.Entities.DeviceGroup", b => + { + b.Navigation("Devices"); + }); + + modelBuilder.Entity("DRS9.Dashboard.Domain.Entities.Playlist", b => + { + b.Navigation("Items"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/DRS9.Dashboard.Server/Components/App.razor b/src/DRS9.Dashboard.Server/Components/App.razor new file mode 100644 index 0000000..f493e7e --- /dev/null +++ b/src/DRS9.Dashboard.Server/Components/App.razor @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/DRS9.Dashboard.Server/Components/Layout/MainLayout.razor b/src/DRS9.Dashboard.Server/Components/Layout/MainLayout.razor new file mode 100644 index 0000000..4be661d --- /dev/null +++ b/src/DRS9.Dashboard.Server/Components/Layout/MainLayout.razor @@ -0,0 +1,63 @@ +@inherits LayoutComponentBase + +
+
+
+

+ DRS9 信息发布系统 +

+ +
+ +
+
+ + + + +
+ @Body +
+
+
+
+
diff --git a/src/DRS9.Dashboard.Server/Components/Pages/Applications.razor b/src/DRS9.Dashboard.Server/Components/Pages/Applications.razor new file mode 100644 index 0000000..8704214 --- /dev/null +++ b/src/DRS9.Dashboard.Server/Components/Pages/Applications.razor @@ -0,0 +1,249 @@ +@page "/applications" +@rendermode @(new InteractiveServerRenderMode()) +@inject Services.ApiClientService ApiClient +@inject IJSRuntime JSRuntime + +内容管理 - DRS9 信息发布系统 + +

内容管理

+ +
+
+ 内容列表 + +
+
+ @if (_applications == null) + { +
+
+
+ } + else if (_applications.Count == 0) + { +
+ +

暂无内容,点击上方按钮添加

+
+ } + else + { +
+ @foreach (var app in _applications) + { +
+
+
+
+
+ +
+
+
@app.Name
+

@app.Type

+

@app.Description

+
+
+
+ +
+
+ } +
+ } +
+
+ + +@if (_showModal) +{ + +} + +@code { + private List _applications = new(); + private ApplicationDto? _editingApp; + private string _appName = ""; + private string _appType = ""; + private string _appContentUrl = ""; + private string _appDescription = ""; + private bool _showModal = false; + private string _errorMessage = ""; + + protected override async Task OnInitializedAsync() + { + _applications = await ApiClient.GetApplicationsAsync(); + } + + private void ShowAddModal() + { + _editingApp = null; + _appName = ""; + _appType = ""; + _appContentUrl = ""; + _appDescription = ""; + _errorMessage = ""; + _showModal = true; + } + + private void EditApp(ApplicationDto app) + { + _editingApp = app; + _appName = app.Name; + _appType = app.Type; + _appContentUrl = app.ContentUrl; + _appDescription = app.Description ?? ""; + _errorMessage = ""; + _showModal = true; + } + + private void CloseModal() + { + _showModal = false; + _editingApp = null; + _errorMessage = ""; + } + + private async Task SaveApp() + { + // Validate inputs + if (string.IsNullOrWhiteSpace(_appName)) + { + _errorMessage = "请输入内容名称"; + return; + } + if (string.IsNullOrWhiteSpace(_appType)) + { + _errorMessage = "请选择内容类型"; + return; + } + if (string.IsNullOrWhiteSpace(_appContentUrl)) + { + _errorMessage = "请输入内容 URL"; + return; + } + + bool success; + if (_editingApp == null) + { + success = await ApiClient.CreateApplicationAsync(new ApplicationCreateRequest + { + Name = _appName, + Type = _appType, + ContentUrl = _appContentUrl, + Description = _appDescription + }); + } + else + { + success = await ApiClient.UpdateApplicationAsync(_editingApp.Id, new ApplicationUpdateRequest + { + Name = _appName, + Description = _appDescription, + ContentUrl = _appContentUrl + }); + } + + if (success) + { + CloseModal(); + _applications = await ApiClient.GetApplicationsAsync(); + await JSRuntime.InvokeVoidAsync("alert", "保存成功"); + } + else + { + _errorMessage = "保存失败,请检查输入数据或稍后重试"; + } + } + + private async Task DeleteApp(ApplicationDto app) + { + if (await JSRuntime.InvokeAsync("confirm", $"确定要删除内容 '{app.Name}' 吗?")) + { + var success = await ApiClient.DeleteApplicationAsync(app.Id); + if (success) + { + _applications = await ApiClient.GetApplicationsAsync(); + await JSRuntime.InvokeVoidAsync("alert", "删除成功"); + } + else + { + await JSRuntime.InvokeVoidAsync("alert", "删除失败"); + } + } + } + + private string GetAppTypeClass(string type) => type switch + { + "Dashboard" => "dashboard", + "WebRotator" => "web", + "Image" => "image", + "Video" => "video", + _ => "" + }; + + private string GetAppIcon(string type) => type switch + { + "Dashboard" => "bi-bar-chart", + "WebRotator" => "bi-arrow-repeat", + "Image" => "bi-image", + "Video" => "bi-play-circle", + _ => "bi-file-earmark" + }; +} diff --git a/src/DRS9.Dashboard.Server/Components/Pages/Batch.razor b/src/DRS9.Dashboard.Server/Components/Pages/Batch.razor new file mode 100644 index 0000000..20a7b8f --- /dev/null +++ b/src/DRS9.Dashboard.Server/Components/Pages/Batch.razor @@ -0,0 +1,234 @@ +@page "/batch" +@rendermode @(new InteractiveServerRenderMode()) +@inject Services.ApiClientService ApiClient +@inject IJSRuntime JSRuntime + +批量操作 - DRS9 信息发布系统 + +

批量操作

+ +
+ +
+
+
+ 批量分配内容 +
+
+

将内容批量分配给多个设备或设备组

+
+ + +
+
+ +
+ @if (_applications != null) + { + @foreach (var app in _applications) + { +
+ + +
+ } + } +
+
+ +
+
+
+ + +
+
+
+ 批量推送命令 +
+
+

向多个设备发送控制命令

+
+ +
+ @if (_devices != null) + { + @foreach (var device in _devices) + { +
+ + +
+ } + } +
+
+
+ + +
+ +
+
+
+ + +
+
+
+ 批量启用/禁用 +
+
+

批量启用或禁用多个设备

+
+ +
+ @if (_devices != null) + { + @foreach (var device in _devices) + { +
+ + +
+ } + } +
+
+
+ + +
+
+
+
+
+ +@code { + private List _devices = new(); + private List _applications = new(); + private List _groups = new(); + private Dictionary _selectedAppIds = new(); + private Dictionary _selectedDeviceIds = new(); + private Dictionary _toggleDeviceIds = new(); + private int _batchAssignGroupId = 0; + private string _batchCommand = "refresh"; + + protected override async Task OnInitializedAsync() + { + await LoadData(); + } + + private async Task LoadData() + { + var devicesTask = ApiClient.GetDevicesAsync(); + var appsTask = ApiClient.GetApplicationsAsync(); + var groupsTask = ApiClient.GetDeviceGroupsAsync(); + + _devices = await devicesTask; + _applications = await appsTask; + _groups = await groupsTask; + + _selectedAppIds = _applications.ToDictionary(a => a.Id, _ => false); + _selectedDeviceIds = _devices.ToDictionary(d => d.Id, _ => false); + _toggleDeviceIds = _devices.ToDictionary(d => d.Id, _ => false); + } + + private async Task ExecuteBatchAssign() + { + var selectedApps = _selectedAppIds.Where(kvp => kvp.Value).Select(kvp => kvp.Key).ToList(); + + if (_batchAssignGroupId > 0) + { + var groupDevices = _devices.Where(d => d.DeviceGroupId == _batchAssignGroupId).Select(d => d.Id).ToList(); + if (groupDevices.Count == 0 || selectedApps.Count == 0) + { + await JSRuntime.InvokeVoidAsync("alert", "请选择设备和内容"); + return; + } + await ApiClient.BatchAssignContentAsync(new BatchAssignContentRequest + { + DeviceIds = groupDevices, + ApplicationIds = selectedApps + }); + await JSRuntime.InvokeVoidAsync("alert", "批量分配完成"); + } + else + { + await JSRuntime.InvokeVoidAsync("alert", "请选择设备组"); + } + } + + private async Task ExecuteBatchPush() + { + var selectedDevices = _selectedDeviceIds.Where(kvp => kvp.Value).Select(kvp => kvp.Key).ToList(); + + if (selectedDevices.Count == 0) + { + await JSRuntime.InvokeVoidAsync("alert", "请选择设备"); + return; + } + + await ApiClient.BatchPushAsync(new BatchPushRequest + { + DeviceIds = selectedDevices, + MessageType = _batchCommand + }); + + await JSRuntime.InvokeVoidAsync("alert", "命令推送完成"); + } + + private async Task ExecuteBatchToggle(bool enable) + { + var selectedDevices = _toggleDeviceIds.Where(kvp => kvp.Value).Select(kvp => kvp.Key).ToList(); + + if (selectedDevices.Count == 0) + { + await JSRuntime.InvokeVoidAsync("alert", "请选择设备"); + return; + } + + await ApiClient.BatchToggleDevicesAsync(new BatchToggleDevicesRequest + { + DeviceIds = selectedDevices, + IsEnabled = enable + }); + + await JSRuntime.InvokeVoidAsync("alert", $"批量{(enable ? "启用" : "禁用")}完成"); + await LoadData(); + } +} diff --git a/src/DRS9.Dashboard.Server/Components/Pages/Devices.razor b/src/DRS9.Dashboard.Server/Components/Pages/Devices.razor new file mode 100644 index 0000000..ca61737 --- /dev/null +++ b/src/DRS9.Dashboard.Server/Components/Pages/Devices.razor @@ -0,0 +1,512 @@ +@page "/devices" +@rendermode @(new InteractiveServerRenderMode()) +@inject Services.ApiClientService ApiClient +@inject IJSRuntime JSRuntime + +设备管理 - DRS9 信息发布系统 + +

设备管理

+ +
+
+
+
+ 设备列表 + +
+
+ @if (_devices == null) + { +
+
+
+ } + else if (_devices.Count == 0) + { +
+ +

暂无设备,点击上方按钮添加

+
+ } + else + { +
+ + + + + + + + + + + + + + @foreach (var device in _devices) + { + + + + + + + + + + } + +
ID设备名称设备编码分组状态最后在线操作
@device.Id@device.DeviceName@device.DeviceCode@device.DeviceGroupName + @if (device.IsEnabled) + { + 启用 + } + else + { + 禁用 + } + @device.LastSeenAt?.ToString("MM-dd HH:mm") ?? "从未" + + + + +
+
+ } +
+
+
+
+ + +
+
+
+
+ 设备分组 + +
+
+ @if (_groups == null) + { +
+
+
+ } + else if (_groups.Count == 0) + { +

暂无分组

+ } + else + { +
+ @foreach (var group in _groups) + { +
+
+
+
@group.Name
+ @group.Description +
+ +
+
+ } +
+ } +
+
+
+
+ + +@if (_showModal) +{ + +} + + +@if (_showGroupModal) +{ + +} + + +@if (_assigningDevice != null) +{ + +} + +@code { + private List _devices = new(); + private List _groups = new(); + private List _applications = new(); + private DeviceDto? _editingDevice; + private DeviceDto? _assigningDevice; + private Dictionary _selectedAppIds = new(); + private string _newGroupName = ""; + private string _newGroupDesc = ""; + private string _editDeviceName = ""; + private string _editDeviceCode = ""; + private int _editGroupId = 0; + private bool _editIsEnabled = true; + private bool _showModal = false; + private bool _showGroupModal = false; + private string _errorMessage = ""; + private string _successMessage = ""; + + protected override async Task OnInitializedAsync() + { + await LoadData(); + } + + private async Task LoadData() + { + var devicesTask = ApiClient.GetDevicesAsync(); + var groupsTask = ApiClient.GetDeviceGroupsAsync(); + var appsTask = ApiClient.GetApplicationsAsync(); + + _devices = await devicesTask; + _groups = await groupsTask; + _applications = await appsTask; + } + + private void ShowAddModal() + { + _editingDevice = null; + _editDeviceName = ""; + _editDeviceCode = ""; + _editGroupId = 0; + _editIsEnabled = true; + _errorMessage = ""; + _showModal = true; + } + + private void EditDevice(DeviceDto device) + { + _editingDevice = device; + _editDeviceName = device.DeviceName; + _editDeviceCode = device.DeviceCode; + _editGroupId = device.DeviceGroupId ?? 0; + _editIsEnabled = device.IsEnabled; + _errorMessage = ""; + _showModal = true; + } + + private void CloseModal() + { + _showModal = false; + _editingDevice = null; + _errorMessage = ""; + } + + private async Task SaveDevice() + { + // Validate inputs + if (string.IsNullOrWhiteSpace(_editDeviceName)) + { + _errorMessage = "请输入设备名称"; + return; + } + if (string.IsNullOrWhiteSpace(_editDeviceCode)) + { + _errorMessage = "请输入设备编码"; + return; + } + + bool success; + if (_editingDevice == null) + { + success = await ApiClient.CreateDeviceAsync(new DeviceCreateRequest + { + DeviceName = _editDeviceName, + DeviceCode = _editDeviceCode, + DeviceGroupId = _editGroupId > 0 ? _editGroupId : null + }); + } + else + { + success = await ApiClient.UpdateDeviceAsync(_editingDevice.Id, new DeviceUpdateRequest + { + DeviceName = _editDeviceName, + DeviceGroupId = _editGroupId > 0 ? _editGroupId : null, + IsEnabled = _editIsEnabled + }); + } + + if (success) + { + CloseModal(); + await LoadData(); + await JSRuntime.InvokeVoidAsync("alert", "保存成功"); + } + else + { + _errorMessage = "保存失败,请检查输入数据或稍后重试"; + } + } + + private async Task DeleteDevice(DeviceDto device) + { + if (await JSRuntime.InvokeAsync("confirm", $"确定要删除设备 '{device.DeviceName}' 吗?")) + { + var success = await ApiClient.DeleteDeviceAsync(device.Id); + if (success) + { + await LoadData(); + await JSRuntime.InvokeVoidAsync("alert", "删除成功"); + } + else + { + await JSRuntime.InvokeVoidAsync("alert", "删除失败"); + } + } + } + + private async Task PushToDevice(DeviceDto device) + { + var success = await ApiClient.PushToDeviceAsync(device.Id, "refresh"); + if (success) + { + await JSRuntime.InvokeVoidAsync("alert", $"已向设备 '{device.DeviceName}' 发送刷新命令"); + } + else + { + await JSRuntime.InvokeVoidAsync("alert", "发送命令失败,设备可能不在线"); + } + } + + private async Task AssignContent(DeviceDto device) + { + _assigningDevice = device; + _selectedAppIds = _applications.ToDictionary(a => a.Id, _ => false); + await LoadAssignedContent(device.Id); + } + + private async Task LoadAssignedContent(int deviceId) + { + // TODO: 从 API 获取已分配的内容 + } + + private void CloseAssignModal() + { + _assigningDevice = null; + _selectedAppIds.Clear(); + } + + private async Task SaveAssignment() + { + if (_assigningDevice != null) + { + var selectedIds = _selectedAppIds.Where(kvp => kvp.Value).Select(kvp => kvp.Key).ToList(); + var success = await ApiClient.AssignContentAsync(_assigningDevice.Id, selectedIds); + if (success) + { + CloseAssignModal(); + await JSRuntime.InvokeVoidAsync("alert", "内容分配成功"); + } + else + { + await JSRuntime.InvokeVoidAsync("alert", "内容分配失败"); + } + } + } + + private void ShowAddGroupModal() + { + _newGroupName = ""; + _newGroupDesc = ""; + _errorMessage = ""; + _showGroupModal = true; + } + + private void CloseGroupModal() + { + _showGroupModal = false; + _errorMessage = ""; + } + + private async Task SaveGroup() + { + if (string.IsNullOrWhiteSpace(_newGroupName)) + { + _errorMessage = "请输入分组名称"; + return; + } + + var success = await ApiClient.CreateDeviceGroupAsync(new DeviceGroupCreateRequest + { + Name = _newGroupName, + Description = _newGroupDesc + }); + + if (success) + { + CloseGroupModal(); + await LoadData(); + await JSRuntime.InvokeVoidAsync("alert", "分组创建成功"); + } + else + { + _errorMessage = "创建失败,请检查输入或稍后重试"; + } + } + + private async Task DeleteGroup(DeviceGroupDto group) + { + if (await JSRuntime.InvokeAsync("confirm", $"确定要删除分组 '{group.Name}' 吗?")) + { + var success = await ApiClient.DeleteDeviceGroupAsync(group.Id); + if (success) + { + await LoadData(); + await JSRuntime.InvokeVoidAsync("alert", "删除成功"); + } + else + { + await JSRuntime.InvokeVoidAsync("alert", "删除失败"); + } + } + } +} diff --git a/src/DRS9.Dashboard.Server/Components/Pages/Guest.razor b/src/DRS9.Dashboard.Server/Components/Pages/Guest.razor new file mode 100644 index 0000000..677a7a8 --- /dev/null +++ b/src/DRS9.Dashboard.Server/Components/Pages/Guest.razor @@ -0,0 +1,167 @@ +@page "/guest" +@rendermode @(new InteractiveServerRenderMode()) +@inject Services.ApiClientService ApiClient +@inject IJSRuntime JSRuntime +@inject NavigationManager NavigationManager + +访客链接 - DRS9 信息发布系统 + +

访客访问链接

+ +
+
+ +
+
+ 创建临时访问链接 +
+
+

生成临时访问链接,允许用户在浏览器中查看设备内容,无需登录。

+ +
+ + + 选择设备后,访客将看到该设备分配的内容 +
+ +
+ + +
+ +
+ + +
+ + +
+
+
+ +
+ +
+
+ 已生成的链接 + +
+
+ @if (_generatedLinks.Count == 0) + { +

暂无生成的链接

+ } + else + { +
+ @foreach (var link in _generatedLinks) + { +
+
+
+
@link.Description
+

+ + 设备: @(link.DeviceId > 0 ? $"设备 {link.DeviceId}" : "通用") + | 过期时间: @link.ExpiresAt?.ToString("MM-dd HH:mm") + +

+
+ + +
+
+
+ @if (link.ExpiresAt > DateTime.UtcNow) + { + 有效 + } + else + { + 已过期 + } +
+
+
+ } +
+ } +
+
+
+
+ +@code { + private List _devices = new(); + private List<(string Description, int DeviceId, string FullUrl, DateTime? ExpiresAt)> _generatedLinks = new(); + private int _selectedDeviceId = 0; + private int _validityMinutes = 60; + private string _description = ""; + + protected override async Task OnInitializedAsync() + { + await LoadData(); + } + + private async Task LoadData() + { + _devices = await ApiClient.GetDevicesAsync(); + } + + private async Task CreateGuestLink() + { + var result = await ApiClient.CreateGuestAccessAsync(new GuestAccessCreateRequest + { + DeviceId = _selectedDeviceId > 0 ? _selectedDeviceId : null, + ValidityMinutes = _validityMinutes, + Description = string.IsNullOrWhiteSpace(_description) ? null : _description + }); + + if (result != null && result.Success) + { + var fullUrl = $"{NavigationManager.BaseUri.TrimEnd('/')}{result.AccessUrl}"; + _generatedLinks.Insert(0, ( + _description ?? "临时访问", + _selectedDeviceId, + fullUrl, + result.ExpiresAt + )); + + await JSRuntime.InvokeVoidAsync("alert", $"访客链接已生成!\n\n访问地址: {fullUrl}"); + + _description = ""; + } + else + { + await JSRuntime.InvokeVoidAsync("alert", "生成访客链接失败"); + } + } + + private async Task CopyLink(string url) + { + await JSRuntime.InvokeVoidAsync("navigator.clipboard.writeText", url); + await JSRuntime.InvokeVoidAsync("alert", "链接已复制到剪贴板"); + } +} diff --git a/src/DRS9.Dashboard.Server/Components/Pages/Home.razor b/src/DRS9.Dashboard.Server/Components/Pages/Home.razor new file mode 100644 index 0000000..23d9458 --- /dev/null +++ b/src/DRS9.Dashboard.Server/Components/Pages/Home.razor @@ -0,0 +1,113 @@ +@page "/" +@rendermode @(new InteractiveServerRenderMode()) +@inject Services.ApiClientService ApiClient + +仪表盘 - DRS9 信息发布系统 + +

系统概览

+ +@if (_stats == null) +{ +
+
+ 加载中... +
+
+} +else +{ +
+
+
+
+
+

@_stats.TotalDevices

+

设备总数

+
+ +
+
+
+
+
+
+
+

@_stats.OnlineDevices

+

在线设备

+
+ +
+
+
+
+
+
+
+

@_stats.TotalApplications

+

内容数量

+
+ +
+
+
+
+ +
+ +
+
+
+ 系统信息 +
+
+ + + + + + + + + + + + + + + + + +
系统名称DRS9 信息发布系统
版本v1.0.0
API 文档Swagger UI
WebSocketws://localhost:5264/ws
+
+
+
+
+} + +@code { + private DashboardStatsDto? _stats; + + protected override async Task OnInitializedAsync() + { + _stats = await ApiClient.GetStatsAsync(); + } +} diff --git a/src/DRS9.Dashboard.Server/Components/Pages/Versions.razor b/src/DRS9.Dashboard.Server/Components/Pages/Versions.razor new file mode 100644 index 0000000..b88b3fe --- /dev/null +++ b/src/DRS9.Dashboard.Server/Components/Pages/Versions.razor @@ -0,0 +1,161 @@ +@page "/versions" +@rendermode @(new InteractiveServerRenderMode()) +@inject Services.ApiClientService ApiClient +@inject IJSRuntime JSRuntime + +版本管理 - DRS9 信息发布系统 + +

版本管理 (OTA)

+ +
+
+
+
+ 版本列表 + +
+
+ @if (_versions == null) + { +
+
+
+ } + else if (_versions.Count == 0) + { +
+ +

暂无版本记录

+
+ } + else + { +
+ + + + + + + + + + + + + + + @foreach (var version in _versions) + { + + + + + + + + + + + } + +
平台版本号版本名称下载地址大小强制更新状态操作
+ + @version.Platform + + @version.Version@version.VersionName + + @version.DownloadUrl + + @(version.FileSize.HasValue ? $"{version.FileSize / 1024.0 / 1024.0:F2} MB" : "-") + @if (version.IsForceUpdate) + { + + } + else + { + + } + + @if (version.PublishedAt.HasValue) + { + 已发布 + } + else + { + 草稿 + } + + @if (version.PublishedAt.HasValue) + { + + } + else + { + + } + +
+
+ } +
+
+
+
+ +@code { + private List _versions = new(); + private bool _showModal = false; + + protected override async Task OnInitializedAsync() + { + _versions = await ApiClient.GetVersionsAsync(); + } + + private void ShowAddModal() + { + _showModal = true; + } + + private void CloseModal() + { + _showModal = false; + } + + private async Task PushUpdate(AppVersionDto version) + { + if (await JSRuntime.InvokeAsync("confirm", $"确定要向所有设备推送版本 {version.Version} 的更新通知吗?")) + { + await ApiClient.PushUpdateAsync(version.Id); + version.PublishedAt = DateTime.UtcNow; + await JSRuntime.InvokeVoidAsync("alert", "更新通知已推送"); + } + } + + private async Task DeleteVersion(AppVersionDto version) + { + if (await JSRuntime.InvokeAsync("confirm", $"确定要删除版本 {version.Version} 吗?")) + { + await ApiClient.DeleteVersionAsync(version.Id); + _versions = await ApiClient.GetVersionsAsync(); + } + } + + private string GetPlatformBadgeClass(string platform) => platform switch + { + "android" => "bg-success", + "ios" => "bg-primary", + "windows" => "bg-info", + "macos" => "bg-secondary", + "linux" => "bg-warning text-dark", + _ => "bg-secondary" + }; +} diff --git a/src/DRS9.Dashboard.Server/Components/Routes.razor b/src/DRS9.Dashboard.Server/Components/Routes.razor new file mode 100644 index 0000000..1bbd243 --- /dev/null +++ b/src/DRS9.Dashboard.Server/Components/Routes.razor @@ -0,0 +1,12 @@ + + + + + + + Not found + +

抱歉,未找到该页面。

+
+
+
diff --git a/src/DRS9.Dashboard.Server/Components/_Imports.razor b/src/DRS9.Dashboard.Server/Components/_Imports.razor new file mode 100644 index 0000000..37d6f29 --- /dev/null +++ b/src/DRS9.Dashboard.Server/Components/_Imports.razor @@ -0,0 +1,10 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.JSInterop +@using DRS9.Dashboard.Server.Components +@using DRS9.Dashboard.Application.DTOs +@using DRS9.Dashboard.Server.Services diff --git a/src/DRS9.Dashboard.Server/Controllers/AppVersionsController.cs b/src/DRS9.Dashboard.Server/Controllers/AppVersionsController.cs new file mode 100644 index 0000000..2e6ff91 --- /dev/null +++ b/src/DRS9.Dashboard.Server/Controllers/AppVersionsController.cs @@ -0,0 +1,98 @@ +using DRS9.Dashboard.Application.DTOs; +using DRS9.Dashboard.Application.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace DRS9.Dashboard.Server.Controllers; + +[ApiController] +[Route("api/admin/versions")] +// [Authorize] - 暂时禁用认证以便测试 +public class AppVersionsController : ControllerBase +{ + private readonly AppVersionService _appVersionService; + private readonly ILogger _logger; + + public AppVersionsController(AppVersionService appVersionService, ILogger logger) + { + _appVersionService = appVersionService; + _logger = logger; + } + + /// + /// 获取所有版本 + /// + [HttpGet] + public async Task>> GetAll() + { + var result = await _appVersionService.GetAllAsync(); + return Ok(new { success = true, data = result }); + } + + /// + /// 创建新版本 + /// + [HttpPost] + public async Task> Create([FromBody] AppVersionCreateRequest request) + { + var result = await _appVersionService.CreateAsync(request); + if (result == null) + { + return BadRequest(new { success = false, message = "创建失败" }); + } + _logger.LogInformation("App version created: {Platform} {Version}", request.Platform, request.Version); + return Ok(new { success = true, data = result, message = "创建成功" }); + } + + /// + /// 删除版本 + /// + [HttpDelete("{id}")] + public async Task Delete(int id) + { + var success = await _appVersionService.DeleteAsync(id); + if (!success) + { + return NotFound(new { success = false, message = "版本不存在" }); + } + _logger.LogInformation("App version deleted: {Id}", id); + return Ok(new { success = true, message = "删除成功" }); + } + + /// + /// 推送升级通知 + /// + [HttpPost("{id}/push-notification")] + public async Task PushNotification(int id, [FromQuery] string? platform = null) + { + var success = await _appVersionService.PushUpdateNotificationAsync(id, platform); + if (!success) + { + return NotFound(new { success = false, message = "版本不存在" }); + } + return Ok(new { success = true, message = "通知已推送" }); + } +} + +// 设备端版本检查控制器 +[ApiController] +[Route("api/[controller]")] +public class VersionsController : ControllerBase +{ + private readonly AppVersionService _appVersionService; + + public VersionsController(AppVersionService appVersionService) + { + _appVersionService = appVersionService; + } + + /// + /// 检查版本更新 + /// + [HttpPost("check")] + public async Task> CheckVersion([FromBody] CheckVersionRequest request) + { + var result = await _appVersionService.CheckVersionAsync(request.Platform, request.CurrentVersion); + return Ok(result); + } +} diff --git a/src/DRS9.Dashboard.Server/Controllers/ApplicationsController.cs b/src/DRS9.Dashboard.Server/Controllers/ApplicationsController.cs new file mode 100644 index 0000000..527a3d3 --- /dev/null +++ b/src/DRS9.Dashboard.Server/Controllers/ApplicationsController.cs @@ -0,0 +1,86 @@ +using DRS9.Dashboard.Application.DTOs; +using DRS9.Dashboard.Application.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace DRS9.Dashboard.Server.Controllers; + +[ApiController] +[Route("api/admin/[controller]")] +// [Authorize] - 暂时禁用认证以便测试 // TODO: 后续添加管理员角色验证 +public class ApplicationsController : ControllerBase +{ + private readonly ApplicationService _applicationService; + private readonly ILogger _logger; + + public ApplicationsController(ApplicationService applicationService, ILogger logger) + { + _applicationService = applicationService; + _logger = logger; + } + + /// + /// 获取所有应用 + /// + [HttpGet] + public async Task> GetAll() + { + var result = await _applicationService.GetAllAsync(); + return Ok(result); + } + + /// + /// 获取应用详情 + /// + [HttpGet("{id}")] + public async Task> GetById(int id) + { + var result = await _applicationService.GetByIdAsync(id); + if (result == null || !result.Success) + { + return NotFound(new { success = false, message = "应用不存在" }); + } + return Ok(result); + } + + /// + /// 创建新应用 + /// + [HttpPost] + public async Task> Create([FromBody] ApplicationCreateRequest request) + { + var result = await _applicationService.CreateAsync(request); + _logger.LogInformation("Application created: {Name}", request.Name); + return CreatedAtAction(nameof(GetById), new { id = result.Data?.Id }, result); + } + + /// + /// 更新应用 + /// + [HttpPut("{id}")] + public async Task> Update(int id, [FromBody] ApplicationUpdateRequest request) + { + var result = await _applicationService.UpdateAsync(id, request); + if (result == null || !result.Success) + { + return NotFound(new { success = false, message = "应用不存在" }); + } + _logger.LogInformation("Application updated: {Id}", id); + return Ok(result); + } + + /// + /// 删除应用 + /// + [HttpDelete("{id}")] + public async Task Delete(int id) + { + var success = await _applicationService.DeleteAsync(id); + if (!success) + { + return NotFound(new { success = false, message = "应用不存在" }); + } + _logger.LogInformation("Application deleted: {Id}", id); + return Ok(new { success = true, message = "删除成功" }); + } +} diff --git a/src/DRS9.Dashboard.Server/Controllers/DevicesController.cs b/src/DRS9.Dashboard.Server/Controllers/DevicesController.cs new file mode 100644 index 0000000..2dd5628 --- /dev/null +++ b/src/DRS9.Dashboard.Server/Controllers/DevicesController.cs @@ -0,0 +1,128 @@ +using DRS9.Dashboard.Application.DTOs; +using DRS9.Dashboard.Application.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace DRS9.Dashboard.Server.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class DevicesController : ControllerBase +{ + private readonly DeviceService _deviceService; + private readonly DeviceManagementService _deviceManagementService; + private readonly PlaylistService _playlistService; + private readonly ILogger _logger; + + public DevicesController(DeviceService deviceService, DeviceManagementService deviceManagementService, PlaylistService playlistService, ILogger logger) + { + _deviceService = deviceService; + _deviceManagementService = deviceManagementService; + _playlistService = playlistService; + _logger = logger; + } + + /// + /// 设备注册 - 使用设备码激活设备 + /// + [HttpPost("register")] + public async Task> Register([FromBody] DeviceRegisterRequest request) + { + _logger.LogInformation("Device registration attempt: {DeviceCode}", request.DeviceCode); + + var result = await _deviceService.RegisterAsync(request); + + if (!result.Success) + { + _logger.LogWarning("Device registration failed: {DeviceCode} - {Message}", request.DeviceCode, result.Message); + return BadRequest(result); + } + + _logger.LogInformation("Device registered successfully: {DeviceCode}", request.DeviceCode); + return Ok(result); + } + + /// + /// 更新设备心跳时间 + /// + [HttpPost("heartbeat")] + [Authorize] + public async Task Heartbeat() + { + var deviceIdClaim = User.FindFirst("deviceId")?.Value; + if (int.TryParse(deviceIdClaim, out var deviceId)) + { + await _deviceService.UpdateLastSeenAsync(deviceId); + return Ok(new { success = true }); + } + + return Unauthorized(); + } + + /// + /// 获取设备信息 + /// + [HttpGet("info")] + [Authorize] + public async Task GetInfo() + { + var deviceIdClaim = User.FindFirst("deviceId")?.Value; + if (int.TryParse(deviceIdClaim, out var deviceId)) + { + var device = await _deviceService.GetDeviceAsync(deviceId); + if (device == null) + { + return NotFound(new { success = false, message = "设备不存在" }); + } + + return Ok(new + { + success = true, + data = new + { + id = device.Id, + deviceCode = device.DeviceCode, + deviceName = device.DeviceName, + deviceType = device.DeviceType, + isEnabled = device.IsEnabled + } + }); + } + + return Unauthorized(); + } + + /// + /// 获取设备分配的内容 + /// + [HttpGet("content")] + [Authorize] + public async Task GetContent() + { + var deviceIdClaim = User.FindFirst("deviceId")?.Value; + if (int.TryParse(deviceIdClaim, out var deviceId)) + { + var result = await _deviceManagementService.GetDeviceContentAsync(deviceId); + return Ok(result); + } + + return Unauthorized(); + } + + /// + /// 获取设备播放列表(基于内容编排) + /// + [HttpGet("playlist")] + [Authorize] + public async Task GetPlaylist() + { + var deviceIdClaim = User.FindFirst("deviceId")?.Value; + if (int.TryParse(deviceIdClaim, out var deviceId)) + { + var result = await _playlistService.GetDevicePlaylistAsync(deviceId); + return Ok(result); + } + + return Unauthorized(); + } +} diff --git a/src/DRS9.Dashboard.Server/Controllers/DevicesManagementController.cs b/src/DRS9.Dashboard.Server/Controllers/DevicesManagementController.cs new file mode 100644 index 0000000..64f116e --- /dev/null +++ b/src/DRS9.Dashboard.Server/Controllers/DevicesManagementController.cs @@ -0,0 +1,372 @@ +using DRS9.Dashboard.Application.DTOs; +using DRS9.Dashboard.Application.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace DRS9.Dashboard.Server.Controllers; + +[ApiController] +[Route("api/admin/devices")] +// [Authorize] - 暂时禁用认证以便测试 // TODO: 后续添加管理员角色验证 +public class DevicesManagementController : ControllerBase +{ + private readonly DeviceManagementService _deviceManagementService; + private readonly DashboardWebSocketManager _wsManager; + private readonly ILogger _logger; + + public DevicesManagementController( + DeviceManagementService deviceManagementService, + DashboardWebSocketManager wsManager, + ILogger logger) + { + _deviceManagementService = deviceManagementService; + _wsManager = wsManager; + _logger = logger; + } + + // ===== 设备管理 ===== + + /// + /// 获取所有设备 + /// + [HttpGet] + public async Task> GetAll() + { + var result = await _deviceManagementService.GetAllDevicesAsync(); + return Ok(result); + } + + /// + /// 获取在线设备 + /// + [HttpGet("online")] + public async Task> GetOnline() + { + var result = await _deviceManagementService.GetOnlineDevicesAsync(); + return Ok(result); + } + + /// + /// 获取设备详情 + /// + [HttpGet("{id}")] + public async Task> GetById(int id) + { + var result = await _deviceManagementService.GetDeviceByIdAsync(id); + if (result == null) + { + return NotFound(new { success = false, message = "设备不存在" }); + } + return Ok(new { success = true, data = result }); + } + + /// + /// 创建设备(生成设备码) + /// + [HttpPost] + public async Task> Create([FromBody] DeviceCreateRequest request) + { + var result = await _deviceManagementService.CreateDeviceAsync(request); + if (result == null) + { + return BadRequest(new { success = false, message = "设备码已存在" }); + } + _logger.LogInformation("Device created: {DeviceCode}", request.DeviceCode); + return Ok(new { success = true, data = result, message = "创建成功" }); + } + + /// + /// 更新设备 + /// + [HttpPut("{id}")] + public async Task> Update(int id, [FromBody] DeviceUpdateRequest request) + { + var result = await _deviceManagementService.UpdateDeviceAsync(id, request); + if (result == null) + { + return NotFound(new { success = false, message = "设备不存在" }); + } + _logger.LogInformation("Device updated: {Id}", id); + return Ok(new { success = true, data = result, message = "更新成功" }); + } + + /// + /// 删除设备 + /// + [HttpDelete("{id}")] + public async Task Delete(int id) + { + var success = await _deviceManagementService.DeleteDeviceAsync(id); + if (!success) + { + return NotFound(new { success = false, message = "设备不存在" }); + } + _logger.LogInformation("Device deleted: {Id}", id); + return Ok(new { success = true, message = "删除成功" }); + } + + // ===== 设备内容分配 ===== + + /// + /// 获取设备分配的内容 + /// + [HttpGet("{id}/content")] + public async Task> GetContent(int id) + { + var result = await _deviceManagementService.GetDeviceContentAsync(id); + return Ok(result); + } + + /// + /// 为设备分配内容 + /// + [HttpPost("{id}/content")] + public async Task AssignContent(int id, [FromBody] DeviceAssignmentRequest request) + { + if (request.DeviceId != id) + { + return BadRequest(new { success = false, message = "设备ID不匹配" }); + } + + var success = await _deviceManagementService.AssignContentToDeviceAsync(id, request.ApplicationIds); + if (!success) + { + return NotFound(new { success = false, message = "设备不存在" }); + } + + _logger.LogInformation("Content assigned to device {DeviceId}: {Count} applications", id, request.ApplicationIds.Count); + return Ok(new { success = true, message = "内容分配成功,已通知设备刷新" }); + } + + // ===== 推送控制 ===== + + /// + /// 刷新设备内容 + /// + [HttpPost("{id}/push/refresh")] + public async Task PushRefresh(int id) + { + var success = await _deviceManagementService.PushMessageAsync(id, "content_refresh", new { message = "请刷新内容" }); + if (!success) + { + return NotFound(new { success = false, message = "设备不在线或不存在" }); + } + _logger.LogInformation("Push refresh to device: {DeviceId}", id); + return Ok(new { success = true, message = "推送成功" }); + } + + /// + /// 重启设备应用 + /// + [HttpPost("{id}/push/restart")] + public async Task PushRestart(int id) + { + var success = await _deviceManagementService.PushMessageAsync(id, "app_restart", new { message = "请重启应用" }); + if (!success) + { + return NotFound(new { success = false, message = "设备不在线或不存在" }); + } + _logger.LogInformation("Push restart to device: {DeviceId}", id); + return Ok(new { success = true, message = "推送成功" }); + } + + /// + /// 获取 WebSocket 连接统计 + /// + [HttpGet("stats/connections")] + public ActionResult GetConnectionStats() + { + var totalConnections = _wsManager.GetConnectionCount(); + return Ok(new + { + success = true, + data = new + { + totalConnections, + timestamp = DateTime.UtcNow + } + }); + } +} + +// ===== 设备分组控制器 ===== + +[ApiController] +[Route("api/admin/device-groups")] +// [Authorize] - 暂时禁用认证以便测试 +public class DeviceGroupsController : ControllerBase +{ + private readonly DeviceManagementService _deviceManagementService; + private readonly ILogger _logger; + + public DeviceGroupsController(DeviceManagementService deviceManagementService, ILogger logger) + { + _deviceManagementService = deviceManagementService; + _logger = logger; + } + + /// + /// 获取所有设备分组 + /// + [HttpGet] + public async Task> GetAll() + { + var result = await _deviceManagementService.GetAllGroupsAsync(); + return Ok(result); + } + + /// + /// 创建设备分组 + /// + [HttpPost] + public async Task> Create([FromBody] DeviceGroupCreateRequest request) + { + var result = await _deviceManagementService.CreateGroupAsync(request); + if (result == null) + { + return BadRequest(new { success = false, message = "创建失败" }); + } + _logger.LogInformation("Device group created: {Name}", request.Name); + return Ok(new { success = true, data = result, message = "创建成功" }); + } + + /// + /// 更新设备分组 + /// + [HttpPut("{id}")] + public async Task> Update(int id, [FromBody] DeviceGroupUpdateRequest request) + { + var result = await _deviceManagementService.UpdateGroupAsync(id, request); + if (result == null) + { + return NotFound(new { success = false, message = "分组不存在" }); + } + _logger.LogInformation("Device group updated: {Id}", id); + return Ok(new { success = true, data = result, message = "更新成功" }); + } + + /// + /// 删除设备分组 + /// + [HttpDelete("{id}")] + public async Task Delete(int id) + { + var success = await _deviceManagementService.DeleteGroupAsync(id); + if (!success) + { + return NotFound(new { success = false, message = "分组不存在" }); + } + _logger.LogInformation("Device group deleted: {Id}", id); + return Ok(new { success = true, message = "删除成功" }); + } +} + +// ===== 批量管理控制器 ===== + +[ApiController] +[Route("api/admin/batch")] +// [Authorize] - 暂时禁用认证以便测试 +public class BatchManagementController : ControllerBase +{ + private readonly BatchManagementService _batchService; + private readonly ILogger _logger; + + public BatchManagementController(BatchManagementService batchService, ILogger logger) + { + _batchService = batchService; + _logger = logger; + } + + /// + /// 批量为设备分配内容 + /// + [HttpPost("assign-content")] + public async Task> BatchAssignContent([FromBody] BatchAssignContentRequest request) + { + var result = await _batchService.BatchAssignContentAsync(request); + _logger.LogInformation("Batch assign content: {Count} devices", request.DeviceIds.Count); + return Ok(result); + } + + /// + /// 批量推送消息到设备 + /// + [HttpPost("push")] + public async Task> BatchPush([FromBody] BatchPushRequest request) + { + var result = await _batchService.BatchPushAsync(request); + _logger.LogInformation("Batch push: {Type} to {Count} devices", request.MessageType, request.DeviceIds.Count); + return Ok(result); + } + + /// + /// 批量推送消息到设备分组 + /// + [HttpPost("push-to-groups")] + public async Task> BatchPushToGroups([FromBody] BatchPushToGroupRequest request) + { + var result = await _batchService.BatchPushToGroupAsync(request); + _logger.LogInformation("Batch push to groups: {Type} to {Count} groups", request.MessageType, request.DeviceGroupIds.Count); + return Ok(result); + } + + /// + /// 批量启用/禁用设备 + /// + [HttpPost("toggle-devices")] + public async Task> BatchToggleDevices([FromBody] BatchToggleDevicesRequest request) + { + var result = await _batchService.BatchToggleDevicesAsync(request); + _logger.LogInformation("Batch toggle devices: {Enabled} for {Count} devices", request.IsEnabled, request.DeviceIds.Count); + return Ok(result); + } + + /// + /// 批量分配播放列表到分组 + /// + [HttpPost("assign-playlist")] + public async Task> BatchAssignPlaylist([FromBody] BatchAssignPlaylistRequest request) + { + var result = await _batchService.BatchAssignPlaylistToGroupAsync(request); + _logger.LogInformation("Batch assign playlist: {PlaylistId} to {Count} groups", request.PlaylistId, request.DeviceGroupIds.Count); + return Ok(result); + } +} + +// ===== 仪表盘统计控制器 ===== + +[ApiController] +[Route("api/admin/stats")] +// [Authorize] - 暂时禁用认证以便测试 +public class DashboardController : ControllerBase +{ + private readonly DeviceManagementService _deviceManagementService; + private readonly ApplicationService _applicationService; + + public DashboardController( + DeviceManagementService deviceManagementService, + ApplicationService applicationService) + { + _deviceManagementService = deviceManagementService; + _applicationService = applicationService; + } + + /// + /// 获取仪表盘统计数据 + /// + [HttpGet] + public async Task> GetStats() + { + var devices = await _deviceManagementService.GetAllDevicesAsync(); + var applications = await _applicationService.GetAllAsync(); + + var stats = new DashboardStatsDto + { + TotalDevices = devices.Data.Count, + OnlineDevices = devices.Data.Count(d => d.IsEnabled), + TotalApplications = applications.Total + }; + + return Ok(stats); + } +} diff --git a/src/DRS9.Dashboard.Server/Controllers/GuestAccessController.cs b/src/DRS9.Dashboard.Server/Controllers/GuestAccessController.cs new file mode 100644 index 0000000..db329db --- /dev/null +++ b/src/DRS9.Dashboard.Server/Controllers/GuestAccessController.cs @@ -0,0 +1,71 @@ +using DRS9.Dashboard.Application.DTOs; +using DRS9.Dashboard.Application.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace DRS9.Dashboard.Server.Controllers; + +[ApiController] +[Route("api/admin/[controller]")] +[Authorize] +public class GuestAccessController : ControllerBase +{ + private readonly GuestAccessService _guestAccessService; + private readonly ILogger _logger; + + public GuestAccessController(GuestAccessService guestAccessService, ILogger logger) + { + _guestAccessService = guestAccessService; + _logger = logger; + } + + /// + /// 创建临时访问链接 + /// + [HttpPost("create")] + public async Task> CreateGuestAccess([FromBody] GuestAccessCreateRequest request) + { + var result = await _guestAccessService.CreateGuestTokenAsync(request); + if (!result.Success) + { + return BadRequest(result); + } + _logger.LogInformation("Guest access created: {Description}", request.Description); + return Ok(result); + } +} + +// 访客端内容查看控制器 +[ApiController] +[Route("api/guest")] +public class GuestViewController : ControllerBase +{ + private readonly PlaylistService _playlistService; + private readonly DeviceManagementService _deviceManagementService; + + public GuestViewController(PlaylistService playlistService, DeviceManagementService deviceManagementService) + { + _playlistService = playlistService; + _deviceManagementService = deviceManagementService; + } + + /// + /// 访客获取内容(使用设备分配的内容) + /// + [HttpGet("content/{deviceId}")] + public async Task GetContent(int deviceId) + { + var result = await _deviceManagementService.GetDeviceContentAsync(deviceId); + return Ok(result); + } + + /// + /// 访客获取播放列表 + /// + [HttpGet("playlist/{deviceId}")] + public async Task GetPlaylist(int deviceId) + { + var result = await _playlistService.GetDevicePlaylistAsync(deviceId); + return Ok(result); + } +} diff --git a/src/DRS9.Dashboard.Server/Controllers/PlaylistsController.cs b/src/DRS9.Dashboard.Server/Controllers/PlaylistsController.cs new file mode 100644 index 0000000..20a4b72 --- /dev/null +++ b/src/DRS9.Dashboard.Server/Controllers/PlaylistsController.cs @@ -0,0 +1,110 @@ +using DRS9.Dashboard.Application.DTOs; +using DRS9.Dashboard.Application.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace DRS9.Dashboard.Server.Controllers; + +// 播放列表功能已禁用 +// [ApiController] +// [Route("api/admin/[controller]")] +// // [Authorize] - 暂时禁用认证以便测试 +// public class PlaylistsController : ControllerBase +// { +// private readonly PlaylistService _playlistService; +// private readonly ILogger _logger; +// +// public PlaylistsController(PlaylistService playlistService, ILogger logger) +// { +// _playlistService = playlistService; +// _logger = logger; +// } +// +// /// +// /// 获取所有播放列表 +// /// +// [HttpGet] +// public async Task> GetAll() +// { +// var result = await _playlistService.GetAllAsync(); +// return Ok(result); +// } +// +// /// +// /// 获取播放列表详情 +// /// +// [HttpGet("{id}")] +// public async Task> GetById(int id) +// { +// var result = await _playlistService.GetByIdAsync(id); +// if (result == null) +// { +// return NotFound(new { success = false, message = "播放列表不存在" }); +// } +// return Ok(new { success = true, data = result }); +// } +// +// /// +// /// 创建播放列表 +// /// +// [HttpPost] +// public async Task> Create([FromBody] PlaylistCreateRequest request) +// { +// var result = await _playlistService.CreateAsync(request); +// if (result == null) +// { +// return BadRequest(new { success = false, message = "创建失败" }); +// } +// _logger.LogInformation("Playlist created: {Name}", request.Name); +// return Ok(new { success = true, data = result, message = "创建成功" }); +// } +// +// /// +// /// 更新播放列表 +// /// +// [HttpPut("{id}")] +// public async Task> Update(int id, [FromBody] PlaylistUpdateRequest request) +// { +// var result = await _playlistService.UpdateAsync(id, request); +// if (result == null) +// { +// return NotFound(new { success = false, message = "播放列表不存在" }); +// } +// _logger.LogInformation("Playlist updated: {Id}", id); +// return Ok(new { success = true, data = result, message = "更新成功" }); +// } +// +// /// +// /// 删除播放列表 +// /// +// [HttpDelete("{id}")] +// public async Task Delete(int id) +// { +// var success = await _playlistService.DeleteAsync(id); +// if (!success) +// { +// return NotFound(new { success = false, message = "播放列表不存在" }); +// } +// _logger.LogInformation("Playlist deleted: {Id}", id); +// return Ok(new { success = true, message = "删除成功" }); +// } +// +// /// +// /// 将播放列表分配到设备分组 +// /// +// [HttpPost("{id}/assign")] +// public async Task AssignToGroup(int id, [FromBody] GroupAssignRequest request) +// { +// var success = await _playlistService.AssignToGroupAsync(id, request.DeviceGroupId); +// if (!success) +// { +// return NotFound(new { success = false, message = "播放列表不存在" }); +// } +// return Ok(new { success = true, message = "分配成功" }); +// } +// } +// +// public record GroupAssignRequest +// { +// public int? DeviceGroupId { get; set; } +// } diff --git a/src/DRS9.Dashboard.Server/DRS9.Dashboard.Server.csproj b/src/DRS9.Dashboard.Server/DRS9.Dashboard.Server.csproj new file mode 100644 index 0000000..df65c0c --- /dev/null +++ b/src/DRS9.Dashboard.Server/DRS9.Dashboard.Server.csproj @@ -0,0 +1,29 @@ + + + + net9.0 + enable + enable + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + diff --git a/src/DRS9.Dashboard.Server/DRS9.Dashboard.Server.http b/src/DRS9.Dashboard.Server/DRS9.Dashboard.Server.http new file mode 100644 index 0000000..f898a81 --- /dev/null +++ b/src/DRS9.Dashboard.Server/DRS9.Dashboard.Server.http @@ -0,0 +1,6 @@ +@DRS9.Dashboard.Server_HostAddress = http://localhost:5264 + +GET {{DRS9.Dashboard.Server_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/src/DRS9.Dashboard.Server/Middleware/WebSocketMiddleware.cs b/src/DRS9.Dashboard.Server/Middleware/WebSocketMiddleware.cs new file mode 100644 index 0000000..b7c1524 --- /dev/null +++ b/src/DRS9.Dashboard.Server/Middleware/WebSocketMiddleware.cs @@ -0,0 +1,122 @@ +using System.Net.WebSockets; +using System.Text; +using DRS9.Dashboard.Application.Services; +using System.Text.Json; + +namespace DRS9.Dashboard.Server.Middleware; + +public class WebSocketMiddleware +{ + private readonly RequestDelegate _next; + private readonly DashboardWebSocketManager _wsManager; + private readonly ILogger _logger; + private const int BufferSize = 4096; + + public WebSocketMiddleware(RequestDelegate next, DashboardWebSocketManager wsManager, ILogger logger) + { + _next = next; + _wsManager = wsManager; + _logger = logger; + } + + public async Task InvokeAsync(HttpContext context) + { + if (!context.WebSockets.IsWebSocketRequest) + { + await _next(context); + return; + } + + var deviceId = context.Request.Query["deviceId"].ToString(); + var parsedDeviceId = int.TryParse(deviceId, out var id) ? id : (int?)null; + + using var webSocket = await context.WebSockets.AcceptWebSocketAsync(); + var connection = _wsManager.AddConnection(webSocket, parsedDeviceId); + + _logger.LogInformation("WebSocket connection established: {ConnectionId}", connection.ConnectionId); + + try + { + await ReceiveMessagesAsync(connection); + } + catch (Exception ex) + { + _logger.LogError(ex, "WebSocket error for connection: {ConnectionId}", connection.ConnectionId); + } + finally + { + _wsManager.RemoveConnection(connection.ConnectionId); + await webSocket.CloseAsync( + connection.WebSocket.CloseStatus ?? WebSocketCloseStatus.NormalClosure, + connection.WebSocket.CloseStatusDescription, + CancellationToken.None); + } + } + + private async Task ReceiveMessagesAsync(WebSocketConnection connection) + { + var buffer = new byte[BufferSize]; + var segment = new ArraySegment(buffer); + + while (connection.WebSocket.State == WebSocketState.Open) + { + var result = await connection.WebSocket.ReceiveAsync(segment, CancellationToken.None); + + if (result.MessageType == WebSocketMessageType.Close) + { + _logger.LogInformation("WebSocket close requested: {ConnectionId}", connection.ConnectionId); + break; + } + + if (result.MessageType == WebSocketMessageType.Text) + { + var message = Encoding.UTF8.GetString(buffer, 0, result.Count); + await ProcessMessageAsync(connection, message); + } + } + } + + private async Task ProcessMessageAsync(WebSocketConnection connection, string message) + { + try + { + var jsonDoc = JsonDocument.Parse(message); + var messageType = jsonDoc.RootElement.GetProperty("type").GetString(); + + _logger.LogDebug("Received message from {ConnectionId}: {Type}", connection.ConnectionId, messageType); + + // Handle different message types + switch (messageType) + { + case "ping": + await _wsManager.SendMessageAsync(connection, new WebSocketMessage + { + Type = "pong", + Data = new { timestamp = DateTime.UtcNow } + }); + break; + + case "heartbeat": + connection.LastActivityAt = DateTime.UtcNow; + await _wsManager.SendMessageAsync(connection, new WebSocketMessage + { + Type = "heartbeat_ack", + Data = new { timestamp = DateTime.UtcNow } + }); + break; + + default: + _logger.LogWarning("Unknown message type: {Type} from {ConnectionId}", messageType, connection.ConnectionId); + break; + } + } + catch (JsonException ex) + { + _logger.LogWarning(ex, "Invalid JSON message from {ConnectionId}", connection.ConnectionId); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error processing message from {ConnectionId}", connection.ConnectionId); + } + } +} diff --git a/src/DRS9.Dashboard.Server/Program.cs b/src/DRS9.Dashboard.Server/Program.cs new file mode 100644 index 0000000..56d5981 --- /dev/null +++ b/src/DRS9.Dashboard.Server/Program.cs @@ -0,0 +1,164 @@ +using System.Text; +using DRS9.Dashboard.Application.Services; +using DRS9.Dashboard.Infrastructure.Data; +using DRS9.Dashboard.Server.Middleware; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.Tokens; +using Microsoft.OpenApi.Models; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +builder.Services.AddControllers(); + +// Configure Database +builder.Services.AddDbContext(options => + options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection"))); + +// Configure JWT Authentication +var jwtKey = builder.Configuration["Jwt:Key"] ?? throw new InvalidOperationException("JWT Key not configured"); +var jwtIssuer = builder.Configuration["Jwt:Issuer"] ?? throw new InvalidOperationException("JWT Issuer not configured"); +var jwtAudience = builder.Configuration["Jwt:Audience"] ?? throw new InvalidOperationException("JWT Audience not configured"); + +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(options => + { + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = jwtIssuer, + ValidAudience = jwtAudience, + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey)) + }; + }); + +builder.Services.AddAuthorization(); + +// Add Blazor Server +builder.Services.AddRazorComponents() + .AddInteractiveServerComponents(); + +// Configure Blazor Server Circuit +builder.Services.AddServerSideBlazor() + .AddCircuitOptions(options => + { + options.DetailedErrors = true; + options.DisconnectedCircuitRetentionPeriod = TimeSpan.FromMinutes(3); + }); + +// Register HttpClient and API Client Service +builder.Services.AddHttpClient(client => +{ + client.BaseAddress = new Uri("http://localhost:5000"); +}); + +// Configure CORS +builder.Services.AddCors(options => +{ + options.AddPolicy("AllowAll", policy => + { + policy.AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader(); + }); +}); + +// Register Application Services +var jwtExpiry = int.TryParse(builder.Configuration["Jwt:ExpiryMinutes"], out var expiry) ? expiry : 43200; +builder.Services.AddSingleton(new JwtTokenService( + jwtKey, + jwtIssuer, + jwtAudience, + jwtExpiry +)); + +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddSingleton(); + +// Configure OpenAPI/Swagger +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddOpenApi(); +builder.Services.AddSwaggerGen(c => +{ + c.SwaggerDoc("v1", new OpenApiInfo + { + Title = "DRS9 Dashboard API", + Version = "v1", + Description = "信息发布系统 API" + }); + + // Add JWT Authentication to Swagger + c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme + { + Description = "JWT Authorization header using the Bearer scheme. Example: \"Authorization: Bearer {token}\"", + Name = "Authorization", + In = ParameterLocation.Header, + Type = SecuritySchemeType.ApiKey, + Scheme = "Bearer" + }); + + c.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Bearer" + } + }, + Array.Empty() + } + }); +}); + +var app = builder.Build(); + +// Initialize Database +using (var scope = app.Services.CreateScope()) +{ + var dbContext = scope.ServiceProvider.GetRequiredService(); + await DataSeeder.SeedAsync(dbContext); +} + +// Configure the HTTP request pipeline. +app.UseSwagger(); +app.UseSwaggerUI(c => +{ + c.SwaggerEndpoint("/swagger/v1/swagger.json", "DRS9 Dashboard API v1"); +}); + +app.UseCors("AllowAll"); + +// 静态文件 +app.UseStaticFiles(); + +app.UseAuthentication(); +app.UseAuthorization(); + +// Add anti-forgery for Blazor Server +app.UseAntiforgery(); + +app.UseWebSockets(); + +app.MapControllers(); + +// Blazor Server +app.MapRazorComponents() + .AddInteractiveServerRenderMode(); + +// WebSocket endpoint +app.Map("/ws", app => app.UseMiddleware()); + +app.Run(); diff --git a/src/DRS9.Dashboard.Server/Properties/launchSettings.json b/src/DRS9.Dashboard.Server/Properties/launchSettings.json new file mode 100644 index 0000000..db22457 --- /dev/null +++ b/src/DRS9.Dashboard.Server/Properties/launchSettings.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5264", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/DRS9.Dashboard.Server/Services/ApiClientService.cs b/src/DRS9.Dashboard.Server/Services/ApiClientService.cs new file mode 100644 index 0000000..04c6eb9 --- /dev/null +++ b/src/DRS9.Dashboard.Server/Services/ApiClientService.cs @@ -0,0 +1,254 @@ +using DRS9.Dashboard.Application.DTOs; + +namespace DRS9.Dashboard.Server.Services; + +public class ApiClientService +{ + private readonly HttpClient _httpClient; + + public ApiClientService(HttpClient httpClient) + { + _httpClient = httpClient; + } + + private void AddAuthHeader() + { + // TODO: 实现认证 token 管理 + } + + // 设备管理 API + public async Task> GetDevicesAsync() + { + AddAuthHeader(); + var response = await _httpClient.GetAsync("/api/admin/devices"); + if (!response.IsSuccessStatusCode) return new List(); + var result = await response.Content.ReadFromJsonAsync(); + return result?.Data ?? new List(); + } + + public async Task GetDeviceAsync(int id) + { + AddAuthHeader(); + var response = await _httpClient.GetAsync($"/api/admin/devices/{id}"); + if (!response.IsSuccessStatusCode) return null; + var result = await response.Content.ReadFromJsonAsync(); + return result; + } + + public async Task CreateDeviceAsync(DeviceCreateRequest device) + { + AddAuthHeader(); + var response = await _httpClient.PostAsJsonAsync("/api/admin/devices", device); + return response.IsSuccessStatusCode; + } + + public async Task UpdateDeviceAsync(int id, DeviceUpdateRequest device) + { + AddAuthHeader(); + var response = await _httpClient.PutAsJsonAsync($"/api/admin/devices/{id}", device); + return response.IsSuccessStatusCode; + } + + public async Task DeleteDeviceAsync(int id) + { + AddAuthHeader(); + var response = await _httpClient.DeleteAsync($"/api/admin/devices/{id}"); + return response.IsSuccessStatusCode; + } + + public async Task AssignContentAsync(int deviceId, List applicationIds) + { + AddAuthHeader(); + var response = await _httpClient.PostAsJsonAsync($"/api/admin/devices/{deviceId}/content", + new DeviceAssignmentRequest { DeviceId = deviceId, ApplicationIds = applicationIds }); + return response.IsSuccessStatusCode; + } + + public async Task PushToDeviceAsync(int deviceId, string messageType) + { + AddAuthHeader(); + var response = await _httpClient.PostAsJsonAsync($"/api/admin/devices/{deviceId}/push", + new PushMessageRequest { DeviceId = deviceId, MessageType = messageType }); + return response.IsSuccessStatusCode; + } + + // 设备分组 API + public async Task> GetDeviceGroupsAsync() + { + AddAuthHeader(); + var response = await _httpClient.GetAsync("/api/admin/device-groups"); + if (!response.IsSuccessStatusCode) return new List(); + var result = await response.Content.ReadFromJsonAsync(); + return result?.Data ?? new List(); + } + + public async Task CreateDeviceGroupAsync(DeviceGroupCreateRequest group) + { + AddAuthHeader(); + var response = await _httpClient.PostAsJsonAsync("/api/admin/device-groups", group); + return response.IsSuccessStatusCode; + } + + public async Task DeleteDeviceGroupAsync(int id) + { + AddAuthHeader(); + var response = await _httpClient.DeleteAsync($"/api/admin/device-groups/{id}"); + return response.IsSuccessStatusCode; + } + + // 应用/内容管理 API + public async Task> GetApplicationsAsync() + { + AddAuthHeader(); + var response = await _httpClient.GetAsync("/api/admin/applications"); + if (!response.IsSuccessStatusCode) return new List(); + var result = await response.Content.ReadFromJsonAsync(); + return result?.Data ?? new List(); + } + + public async Task GetApplicationAsync(int id) + { + AddAuthHeader(); + var response = await _httpClient.GetAsync($"/api/admin/applications/{id}"); + if (!response.IsSuccessStatusCode) return null; + return await response.Content.ReadFromJsonAsync(); + } + + public async Task CreateApplicationAsync(ApplicationCreateRequest application) + { + AddAuthHeader(); + var response = await _httpClient.PostAsJsonAsync("/api/admin/applications", application); + return response.IsSuccessStatusCode; + } + + public async Task UpdateApplicationAsync(int id, ApplicationUpdateRequest application) + { + AddAuthHeader(); + var response = await _httpClient.PutAsJsonAsync($"/api/admin/applications/{id}", application); + return response.IsSuccessStatusCode; + } + + public async Task DeleteApplicationAsync(int id) + { + AddAuthHeader(); + var response = await _httpClient.DeleteAsync($"/api/admin/applications/{id}"); + return response.IsSuccessStatusCode; + } + + // 播放列表 API + public async Task> GetPlaylistsAsync() + { + AddAuthHeader(); + var response = await _httpClient.GetAsync("/api/admin/playlists"); + if (!response.IsSuccessStatusCode) return new List(); + return await response.Content.ReadFromJsonAsync>() ?? new List(); + } + + public async Task GetPlaylistAsync(int id) + { + AddAuthHeader(); + var response = await _httpClient.GetAsync($"/api/admin/playlists/{id}"); + if (!response.IsSuccessStatusCode) return null; + return await response.Content.ReadFromJsonAsync(); + } + + public async Task CreatePlaylistAsync(object playlist) + { + AddAuthHeader(); + var response = await _httpClient.PostAsJsonAsync("/api/admin/playlists", playlist); + return response.IsSuccessStatusCode; + } + + public async Task UpdatePlaylistAsync(int id, object playlist) + { + AddAuthHeader(); + var response = await _httpClient.PutAsJsonAsync($"/api/admin/playlists/{id}", playlist); + return response.IsSuccessStatusCode; + } + + public async Task DeletePlaylistAsync(int id) + { + AddAuthHeader(); + var response = await _httpClient.DeleteAsync($"/api/admin/playlists/{id}"); + return response.IsSuccessStatusCode; + } + + public async Task AssignPlaylistToGroupAsync(int playlistId, int groupId) + { + AddAuthHeader(); + var response = await _httpClient.PostAsJsonAsync($"/api/admin/playlists/{playlistId}/assign", + new { DeviceGroupId = groupId }); + return response.IsSuccessStatusCode; + } + + // 批量操作 API + public async Task BatchAssignContentAsync(BatchAssignContentRequest request) + { + AddAuthHeader(); + var response = await _httpClient.PostAsJsonAsync("/api/admin/batch/assign-content", request); + return response.IsSuccessStatusCode; + } + + public async Task BatchPushAsync(BatchPushRequest request) + { + AddAuthHeader(); + var response = await _httpClient.PostAsJsonAsync("/api/admin/batch/push", request); + return response.IsSuccessStatusCode; + } + + public async Task BatchToggleDevicesAsync(BatchToggleDevicesRequest request) + { + AddAuthHeader(); + var response = await _httpClient.PostAsJsonAsync("/api/admin/batch/toggle", request); + return response.IsSuccessStatusCode; + } + + // 版本管理 API + public async Task> GetVersionsAsync() + { + AddAuthHeader(); + var response = await _httpClient.GetAsync("/api/admin/versions"); + if (!response.IsSuccessStatusCode) return new List(); + return await response.Content.ReadFromJsonAsync>() ?? new List(); + } + + public async Task CreateVersionAsync(object version) + { + AddAuthHeader(); + var response = await _httpClient.PostAsJsonAsync("/api/admin/versions", version); + return response.IsSuccessStatusCode; + } + + public async Task DeleteVersionAsync(int id) + { + AddAuthHeader(); + var response = await _httpClient.DeleteAsync($"/api/admin/versions/{id}"); + return response.IsSuccessStatusCode; + } + + public async Task PushUpdateAsync(int versionId) + { + AddAuthHeader(); + var response = await _httpClient.PostAsJsonAsync($"/api/admin/versions/{versionId}/push", new { }); + return response.IsSuccessStatusCode; + } + + // 访客链接 API + public async Task CreateGuestAccessAsync(GuestAccessCreateRequest request) + { + AddAuthHeader(); + var response = await _httpClient.PostAsJsonAsync("/api/admin/guest-access/create", request); + if (!response.IsSuccessStatusCode) return null; + return await response.Content.ReadFromJsonAsync(); + } + + // 统计 API + public async Task GetStatsAsync() + { + AddAuthHeader(); + var response = await _httpClient.GetAsync("/api/admin/stats"); + if (!response.IsSuccessStatusCode) + return new DashboardStatsDto(); + return await response.Content.ReadFromJsonAsync() ?? new DashboardStatsDto(); + } +} diff --git a/src/DRS9.Dashboard.Server/appsettings.Development.json b/src/DRS9.Dashboard.Server/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/src/DRS9.Dashboard.Server/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/src/DRS9.Dashboard.Server/appsettings.json b/src/DRS9.Dashboard.Server/appsettings.json new file mode 100644 index 0000000..bb48fc9 --- /dev/null +++ b/src/DRS9.Dashboard.Server/appsettings.json @@ -0,0 +1,19 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Microsoft.EntityFrameworkCore": "Information" + } + }, + "AllowedHosts": "*", + "ConnectionStrings": { + "DefaultConnection": "Data Source=dashboard.db" + }, + "Jwt": { + "Key": "DRS9_DASHBOARD_SECRET_KEY_2026_CHANGE_THIS_IN_PRODUCTION", + "Issuer": "DRS9.Dashboard", + "Audience": "DRS9.Dashboard.Clients", + "ExpiryMinutes": 43200 + } +} diff --git a/src/DRS9.Dashboard.Server/dashboard.db b/src/DRS9.Dashboard.Server/dashboard.db new file mode 100644 index 0000000000000000000000000000000000000000..a116249a83520760d9197c7a28139e71b0f4d428 GIT binary patch literal 118784 zcmeI5eQX=&eaG*3Bu}JX&Td{Vj%J9VE7^o?iN{w-ZP7B+$>A(hibN$gkW`?>vrU8| zWgg|kDTYn9lQhY&Vnef~ODrAhVz4Et*fFknCjwB0bjWkZ2r#V~Z) zAA9bNe2%=MEXQ)RDcYc73p1Eh&%WjxJ3pXUfZpYJO*HCB+Qck)4+_1v#{s z&)vHyhjRHT`DiHYm?IpT&4b131btzw5F%=pC zLU*lHE?TJ(tZCNdg(K^q@8`wkF?Ri|U1>&D%O|TVO0{7XI9FQAtgcqd%f&{yR?U@+ zLQv@jx*J5LG!eE-r0t|qw=m^O;RB(t%?eV2JNtO?UQ&XLdkJh=It=eO4R5bF$_9JU zyBKQI6plR9YgB2#vW<#2s+11s>oA0l)jhyaO$WE?^1_#byf`+-J~`Yd9X!N0}&kzkx!U_CvIJk56SbP{A?kVU!0iq@bqlroKQs30FDg)<9i=3iVb*`rC>jVKzxv;zRzX;k_^E6eokyqr6f*V*iA_vz5Q zJT1@5`K-KP^LJINY?3>obt6;qj7)l+Y-S;wnUb&3Bm;S?O}87;tdfY$1~Z$x`e5Y3 znE)>i4zf>XDeqIwj6o+)(@L{h{~~#=&8ze-)v-bill1PdOMKz%s$w}rv&O@->U~OG zCH-G#6!BHpxSX0g{fCk92am)_B$C_6xGn4Fx0Tay`5#itL5cy5lXXq=xiA z0lU-bVViO}Gsi{_rzNXuqp71i%@tMk;aa_PPf*ZDL#KhwGq1>B*IN|5Tp&|CM zK^;V9tyFHz)K03+AG{qwGkjC!r*dsnc&&}MN@pLUa$nxl+w++Yt;rh zOJA(J^dN;(Yb(dAq{65at-746muq$6J0D_Tx~^JZU-Xcxt2BEqr`OTal01E+e9|~k zQ|~FOjavPzV<2a*HODl~CfF3Ixxek)uKncIMGn;{&0%=1UMsCFH`+#ev%Z3n9OoAw zOtXxBx^H@Dajj9)kCz;^EJ=>R+BI;Gc2&BbVR=!K*mDORP|ehqAF^lHLOk6dX$c6S z{%b$;>QrF!^8==+LU~0|8^x7X8*5U6PX+q5g6v=z;h(i1_<#TifB*=900@8p2!H?x zfB*=900{j5BJgRJV`gK~Sn6O@Iw-|L(TT};d@>dtjZefA@$uOHXmm0f)vo_J;b}(r zsqiE6g%1dT00@8p2!H?xfB*=900@8p2!O!W5*XpQL%XyK_NXMqq7$)nBE3Ym`m}a0 zsuQEpQAzr=hs#pwq>1FXmTs<6JX<0AGt5-OEGM^8jmMJ7^ir-`Zj_4^V_%3NEJ(`4 z3EyIbe-ZwYeBlEEAOHd&00JNY0w4eaAOHd&00JOz(+S+g9b&uA2yjDOmP*kv7vLG< z`%tRQL7=P)xx34_;#MQUHfB6qy%B>Vn zD)Y5Qkvs-r>}0W0QR-*MR_nEu8rQVj+G-z5B#|@jo#Ga01%(KP$Y>2(JrYx#7Yg5ClK~1V8`;KmY_l00ck)1V8`;K){K> zK98Rn>?ijeeEIP!Z(eA-zako&j7y`*R4SF6h{>#<8UCDgYlV^i>N{V%{OrpYU;CYl zuYJ$B6T*~Zy#+wOgCm)kj8BZFV)0ZwAz}UBsTvRl0w4eaAOHd&00JNY0w4eaAOHd% zF9Q8SlHvQ8*?~LzmxV75{Bq#Wgtz+t>f=>Llm-Mq00ck)1V8`;KmY_l00eFlfd@PT z%2xBNY(Bw_tl%am zqwz^e8cmER$$$6rMEYP;de)SlXh~1BWtJwT)M!!~PsZc%wkRTZVst#2o`_1Z^&m?m z?;?@|flQYCZ%US0lIh&h$s~EZd}@>|HA}HnGsY-NdNP_GmEtienM$`s#V5&g@<=ID z6YX}!C!?v+^hAnSlb-SoFnbP~rDU?qk<82tVMuIQxb)29m%j1FrPtrN^!ra;`r%i| ztMl4kd2h%k6ep&=1I%7Z%0l*@+1aKX@}R^&e*W^aZ(sS&8{OrM6FVZ!8YR}4Wa2F{ zMvcZF zqGTgMLRi&iNy9*a&4vsyBZ zvWwDalF&;8g|`@=@Ejxjz3`my3*mj?2cHzTMma$M1V8`;KmY_l00ck)1V8`;Kww)C zxRpD|w!gs9+sEx@jivp?oR65f)4b1Le+{B{2YJ>%{klR<3!q;(=n=T#0R7g$Ad%5W z<^af*|1ZcKz+1ux!oQQf0DmX^mvB+|N8zux1qm1d0w4eaAOHd&00JNY0w4eaAOHfN zWCC6;!1nn#mi6^&f4xC1AfN~I=mESI;MW6udVp68;Pe2GmtzB7|GXZ}qydeCA@bV`aFUk%AAOHd&00JNY0w4eaAOHd&00K9gfZt2D{PTUh zmt|Sr*K2%xgL*I*&<{CZkMZs0^zs9U5i<%=t( zLL)%vu9eC~D>Z_(;%f53k@e5_^WyRtyMETLG^48JlhqZa+OQz!N=up5)k=A}*eKVk z7HulsKzD6=l65QFxi}#WeWZX+&%VN|TrJ&!%VW=eb zqIWUWrYRhGsMn~{fMpvMZ&WEA(AQxI9jkkQp_&eE)#ZgR1$l97jD2#rQ9NETorNky zN5OkdeTB^3GqTkds0>7OFho9K0-iLdf_zAx59MbIq5R^^Oei~>UntCHNO&kbci5~` zTkg5}+>y-uvCv`pSSYhtn9b#h!Xt9NaKI327~~|}OhA>hORCv=NvU@lXen8%>mKWx zAX`_84W*Q6ghK`SC@Gv-Kr{d1YRMi=%5Fr_0Hz%P&`zV$2U=OCXXoYIp}fv!U%O9- z=H+R5Ue0Ib1)IOCT4j^m5v}ilKt>MH5~I-`iMy2jtzNpPsGf38A+Hq673Zk=TE#`|hAwp- zwMtm}d0reIX3x)3jo7FIs;<-?J6SqvQ-JLmH{#r_$*Rls@~U=XVsZRX?oB|P@blu( z5c}Am4kEKwDmP|oC)MT;-j1LdzNzw4xi%`i*2Y_%{N8@( zw;^wf+^4m`10-kU{9Rt%hadMDRo6OcRjKm{)n}7NkHDK7muQ(A4^HkrOjn=1dZ(0b z-d(FU$XWVg-K7U9oLXBsUL_SqrD#?2T)kYY6W^h{!>)Vdf$6$xeSOhGuCCJTxtv}{ zOH1kq~`v%Q-SuAR~I={qcn%%xq7X%w%lkN z=}l(ANRIQ152jg0KixMyw7Ax&>BmcsT9zcoU@y5A?$NG?*E1|HN)mhSpaZIz+VVs8 z>{^JY8ze0OA=H2EXI`BOY<_;g6jdm%C~Bj)vf9SFKN!*0|Ct@yAACRn1V8`;KmY_l z00ck)1V8`;KmY_jt_1o?!FDpg%k-b?zpd|)-aqfXJNRz!fuJYwE&fM7&U=RY8CN8) z7T8UaZ24z>;7(pVyoX)?47Da}&gZC0bCqJ_M6JHEWQMd>b=2MFtV(NN0TtPivAIxd zi6(O{VN1HUjBQOJMa~y@@#3C6>=OZNzS7D<$^Q-elnxcsob9sB@tQMI?eko=eAMig zcshRc8E}CpP3_ot>(^T5yBkQ-{&Wp2q z+4XF@@?1k%(VLWBR9M<9?QqU)H%CunHY`(z*0hWb0g?6KZM>KxhDpwb zk;c&4&SJCbjt#7`<#aL0o+k3x!CQH8?_Tz)U3x3Gsk0r3rfzdnhAnPWZM`k$HMDjM zVQZ<#z8GLHl3S_|xgBHU6^Dgo(lpm9$~^hk*$G@n`!ssAw(T4?eDv;(k7g@}-5l$P zZe8tLIjl7L!a2R6#EQ_?xmc4t4`+~QeNnr%hFaWv{O zmULSmo2k`SkLWw^Yzsiyeb(8wOhb3!q zSU + + + + + DRS9 查看器 + + + +
+
正在加载内容...
+
+ + + + diff --git a/src/DRS9.Dashboard.sln b/src/DRS9.Dashboard.sln new file mode 100644 index 0000000..97e431f --- /dev/null +++ b/src/DRS9.Dashboard.sln @@ -0,0 +1,76 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DRS9.Dashboard.Server", "DRS9.Dashboard.Server\DRS9.Dashboard.Server.csproj", "{A3945441-31C8-4602-A097-894EBCB2C121}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DRS9.Dashboard.Domain", "DRS9.Dashboard.Domain\DRS9.Dashboard.Domain.csproj", "{261896C4-D2FB-4A0C-A2DD-98B41F8CA1C9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DRS9.Dashboard.Application", "DRS9.Dashboard.Application\DRS9.Dashboard.Application.csproj", "{7F748342-07F7-4EFE-9D61-B0B9412F1B5B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DRS9.Dashboard.Infrastructure", "DRS9.Dashboard.Infrastructure\DRS9.Dashboard.Infrastructure.csproj", "{2C46B23F-7485-48B2-9BCA-0E2D9636B142}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A3945441-31C8-4602-A097-894EBCB2C121}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A3945441-31C8-4602-A097-894EBCB2C121}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A3945441-31C8-4602-A097-894EBCB2C121}.Debug|x64.ActiveCfg = Debug|Any CPU + {A3945441-31C8-4602-A097-894EBCB2C121}.Debug|x64.Build.0 = Debug|Any CPU + {A3945441-31C8-4602-A097-894EBCB2C121}.Debug|x86.ActiveCfg = Debug|Any CPU + {A3945441-31C8-4602-A097-894EBCB2C121}.Debug|x86.Build.0 = Debug|Any CPU + {A3945441-31C8-4602-A097-894EBCB2C121}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A3945441-31C8-4602-A097-894EBCB2C121}.Release|Any CPU.Build.0 = Release|Any CPU + {A3945441-31C8-4602-A097-894EBCB2C121}.Release|x64.ActiveCfg = Release|Any CPU + {A3945441-31C8-4602-A097-894EBCB2C121}.Release|x64.Build.0 = Release|Any CPU + {A3945441-31C8-4602-A097-894EBCB2C121}.Release|x86.ActiveCfg = Release|Any CPU + {A3945441-31C8-4602-A097-894EBCB2C121}.Release|x86.Build.0 = Release|Any CPU + {261896C4-D2FB-4A0C-A2DD-98B41F8CA1C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {261896C4-D2FB-4A0C-A2DD-98B41F8CA1C9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {261896C4-D2FB-4A0C-A2DD-98B41F8CA1C9}.Debug|x64.ActiveCfg = Debug|Any CPU + {261896C4-D2FB-4A0C-A2DD-98B41F8CA1C9}.Debug|x64.Build.0 = Debug|Any CPU + {261896C4-D2FB-4A0C-A2DD-98B41F8CA1C9}.Debug|x86.ActiveCfg = Debug|Any CPU + {261896C4-D2FB-4A0C-A2DD-98B41F8CA1C9}.Debug|x86.Build.0 = Debug|Any CPU + {261896C4-D2FB-4A0C-A2DD-98B41F8CA1C9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {261896C4-D2FB-4A0C-A2DD-98B41F8CA1C9}.Release|Any CPU.Build.0 = Release|Any CPU + {261896C4-D2FB-4A0C-A2DD-98B41F8CA1C9}.Release|x64.ActiveCfg = Release|Any CPU + {261896C4-D2FB-4A0C-A2DD-98B41F8CA1C9}.Release|x64.Build.0 = Release|Any CPU + {261896C4-D2FB-4A0C-A2DD-98B41F8CA1C9}.Release|x86.ActiveCfg = Release|Any CPU + {261896C4-D2FB-4A0C-A2DD-98B41F8CA1C9}.Release|x86.Build.0 = Release|Any CPU + {7F748342-07F7-4EFE-9D61-B0B9412F1B5B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7F748342-07F7-4EFE-9D61-B0B9412F1B5B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7F748342-07F7-4EFE-9D61-B0B9412F1B5B}.Debug|x64.ActiveCfg = Debug|Any CPU + {7F748342-07F7-4EFE-9D61-B0B9412F1B5B}.Debug|x64.Build.0 = Debug|Any CPU + {7F748342-07F7-4EFE-9D61-B0B9412F1B5B}.Debug|x86.ActiveCfg = Debug|Any CPU + {7F748342-07F7-4EFE-9D61-B0B9412F1B5B}.Debug|x86.Build.0 = Debug|Any CPU + {7F748342-07F7-4EFE-9D61-B0B9412F1B5B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7F748342-07F7-4EFE-9D61-B0B9412F1B5B}.Release|Any CPU.Build.0 = Release|Any CPU + {7F748342-07F7-4EFE-9D61-B0B9412F1B5B}.Release|x64.ActiveCfg = Release|Any CPU + {7F748342-07F7-4EFE-9D61-B0B9412F1B5B}.Release|x64.Build.0 = Release|Any CPU + {7F748342-07F7-4EFE-9D61-B0B9412F1B5B}.Release|x86.ActiveCfg = Release|Any CPU + {7F748342-07F7-4EFE-9D61-B0B9412F1B5B}.Release|x86.Build.0 = Release|Any CPU + {2C46B23F-7485-48B2-9BCA-0E2D9636B142}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2C46B23F-7485-48B2-9BCA-0E2D9636B142}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2C46B23F-7485-48B2-9BCA-0E2D9636B142}.Debug|x64.ActiveCfg = Debug|Any CPU + {2C46B23F-7485-48B2-9BCA-0E2D9636B142}.Debug|x64.Build.0 = Debug|Any CPU + {2C46B23F-7485-48B2-9BCA-0E2D9636B142}.Debug|x86.ActiveCfg = Debug|Any CPU + {2C46B23F-7485-48B2-9BCA-0E2D9636B142}.Debug|x86.Build.0 = Debug|Any CPU + {2C46B23F-7485-48B2-9BCA-0E2D9636B142}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2C46B23F-7485-48B2-9BCA-0E2D9636B142}.Release|Any CPU.Build.0 = Release|Any CPU + {2C46B23F-7485-48B2-9BCA-0E2D9636B142}.Release|x64.ActiveCfg = Release|Any CPU + {2C46B23F-7485-48B2-9BCA-0E2D9636B142}.Release|x64.Build.0 = Release|Any CPU + {2C46B23F-7485-48B2-9BCA-0E2D9636B142}.Release|x86.ActiveCfg = Release|Any CPU + {2C46B23F-7485-48B2-9BCA-0E2D9636B142}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/test_api.sh b/test_api.sh new file mode 100755 index 0000000..10da075 --- /dev/null +++ b/test_api.sh @@ -0,0 +1,56 @@ +#!/bin/bash +# DRS9 Dashboard API 测试脚本 + +BASE_URL="http://localhost:5264" + +echo "==================================" +echo "DRS9 Dashboard API 测试" +echo "==================================" + +# 1. 设备注册 +echo -e "\n1. 设备注册..." +REGISTER_RESPONSE=$(curl -s -X POST $BASE_URL/api/devices/register \ + -H "Content-Type: application/json" \ + -d '{"deviceCode":"TEST-001-DEV","deviceName":"测试设备1号","deviceType":"Android"}') + +TOKEN=$(echo $REGISTER_RESPONSE | jq -r '.token') +DEVICE_ID=$(echo $REGISTER_RESPONSE | jq -r '.deviceId') + +echo "Token: $TOKEN" +echo "Device ID: $DEVICE_ID" + +# 2. 获取设备信息 +echo -e "\n2. 获取设备信息..." +curl -s -X GET $BASE_URL/api/devices/info \ + -H "Authorization: Bearer $TOKEN" | jq . + +# 3. 获取设备分配的内容(初始为空) +echo -e "\n3. 获取设备分配的内容(初始为空)..." +curl -s -X GET $BASE_URL/api/devices/content \ + -H "Authorization: Bearer $TOKEN" | jq . + +# 4. 心跳测试 +echo -e "\n4. 心跳测试..." +curl -s -X POST $BASE_URL/api/devices/heartbeat \ + -H "Authorization: Bearer $TOKEN" | jq . + +# 5. 获取所有可用应用(不需要认证的测试接口) +echo -e "\n5. 获取所有应用(数据库中的预置数据)..." +echo "注意:需要管理员权限访问 /api/admin/applications" + +echo -e "\n==================================" +echo "测试完成" +echo "==================================" +echo "" +echo "管理 API 端点(需要管理员认证):" +echo " GET /api/admin/applications - 获取所有应用" +echo " POST /api/admin/applications - 创建应用" +echo " GET /api/admin/devices - 获取所有设备" +echo " POST /api/admin/devices - 创建设备(生成设备码)" +echo " GET /api/admin/devices/{id}/content - 获取设备内容" +echo " POST /api/admin/devices/{id}/content - 分配内容给设备" +echo " POST /api/admin/devices/{id}/push/refresh - 推送刷新指令" +echo " GET /api/admin/devicegroups - 获取所有分组" +echo "" +echo "WebSocket 端点:" +echo " ws://localhost:5264/ws?deviceId=1" diff --git a/test_websocket.html b/test_websocket.html new file mode 100644 index 0000000..8d78e15 --- /dev/null +++ b/test_websocket.html @@ -0,0 +1,67 @@ + + + + WebSocket Test + + + +

DRS9 Dashboard WebSocket Test

+
+ + + +
+
+ + +
+

+
+    
+
+