Compare commits
105 Commits
de769ccf12
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| cbdee5068a | |||
| 685a8aeaec | |||
| 766be485d0 | |||
| faf40d6e27 | |||
| 57e40440ab | |||
| 4bb03aa0df | |||
| de9a0c2978 | |||
| 195792ec66 | |||
| a8b19cbacc | |||
| a64d1b459a | |||
| f7f505d109 | |||
| 1d467a5fc4 | |||
| 24059dfebb | |||
| edc0122a3b | |||
| 593f95ffaa | |||
| 0b829b72cb | |||
| 2deb0279bb | |||
| 2b444abe10 | |||
| 32612ca6d4 | |||
| 7f78795135 | |||
| 91752baa3b | |||
| a04b4be7e5 | |||
| f8874ff60c | |||
| 88e5fd23be | |||
| 12d488a5ca | |||
| 519428c52d | |||
| 72c7a38f1d | |||
| 5700da58d9 | |||
| bfdfad14d8 | |||
| cc2276fe27 | |||
| 7c53adc804 | |||
| b95c5029f1 | |||
| 1285ac4648 | |||
| 0227f06658 | |||
| 07614b5fe6 | |||
| eb4f09e391 | |||
| 87bc5c0e0c | |||
| b822489111 | |||
| 2ca2a4c114 | |||
| c43517969d | |||
| 85a58123be | |||
| ab09c24338 | |||
| 6ec47220c2 | |||
| 6985a4af42 | |||
| fa1acec8af | |||
| c342f543f7 | |||
| db6aaaec84 | |||
| 5481c091f2 | |||
| 2fcff63ee2 | |||
| ddf2b6316d | |||
| a6db06c6cb | |||
| 6b09006071 | |||
| c0bd6b8200 | |||
| bdbef21fa8 | |||
| bdaa6a0dc8 | |||
| 45b5e6f71e | |||
| 4f33d81aa3 | |||
| 4579fefef9 | |||
| d64b68abdd | |||
| f29802eb5a | |||
| c5db5d773b | |||
| 21051dd199 | |||
| 1eb986177d | |||
| 8b773cbfe1 | |||
| 3c3453668f | |||
| 4fb861d9e6 | |||
| 00fa7a21f6 | |||
| bf333cd718 | |||
| 28a0c3cfe4 | |||
| 16ad80f2a8 | |||
| 2d3fceb265 | |||
| d9933e4537 | |||
| 6ece22d23d | |||
| 8e17fef32f | |||
| f2f7ec9100 | |||
| dbf77c7d50 | |||
| 94799ee189 | |||
| eb1a677612 | |||
| 4142b621c3 | |||
| e493c85d08 | |||
| ae4cd627df | |||
| 028e3dfb34 | |||
| 296d87b245 | |||
| badf68a8f8 | |||
| eb8f40f2ab | |||
| 77778f67c1 | |||
| 1d86580db7 | |||
| 0126c6b097 | |||
| 78b686f9e5 | |||
| 698c404b72 | |||
| 866549873a | |||
| 73e4f8e001 | |||
| 5eb856ed73 | |||
| 8291b82362 | |||
| eab84747a5 | |||
| f9b79417a0 | |||
| 634c4eccfd | |||
| ae2c6ddfc7 | |||
| 903ccd3073 | |||
| 018d7e1733 | |||
| 32989e1fa0 | |||
| 0a32754740 | |||
| 8ed012bba8 | |||
| 831f8ba7f5 | |||
| ab53492cc4 |
10
.editorconfig
Normal file
10
.editorconfig
Normal file
@ -0,0 +1,10 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = crlf
|
||||
insert_final_newline = true
|
||||
|
||||
[*.cs]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -364,3 +364,6 @@ FodyWeavers.xsd
|
||||
|
||||
# Trae IDE related files
|
||||
.trae/
|
||||
PROJECT_STRUCTURE.md
|
||||
/SqlMigrations
|
||||
/QYZH.InteractiveMagazine.WebApi/DataProtection/key-48a0c7fb-e7b5-436a-bee8-3c40690512cb.xml
|
||||
|
||||
125
AdminMenu_RouteSeed.sql
Normal file
125
AdminMenu_RouteSeed.sql
Normal file
@ -0,0 +1,125 @@
|
||||
SET @now = NOW();
|
||||
SET @createdBy = 'System';
|
||||
SET @shortSnowflakeEpoch = 1577836800000;
|
||||
SET @shortSnowflakeTimestamp = CAST(UNIX_TIMESTAMP(CURRENT_TIMESTAMP(3)) * 1000 AS UNSIGNED) - @shortSnowflakeEpoch;
|
||||
SET @shortSnowflakeSequence = -1;
|
||||
|
||||
SET @ai_config_id = COALESCE((SELECT `Id` FROM `System_AdminMenu` WHERE `Code` = 'ai-config' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
|
||||
INSERT INTO `System_AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
|
||||
VALUES (@ai_config_id, 0, 'AI配置中心', 'ai-config', '/ai-config', 'layout.base', 'ri:robot-line', 0, b'1', 1, b'0', @createdBy, @now, @createdBy, @now)
|
||||
ON DUPLICATE KEY UPDATE `ParentId` = VALUES(`ParentId`), `Name` = VALUES(`Name`), `Path` = VALUES(`Path`), `Component` = VALUES(`Component`), `Icon` = VALUES(`Icon`), `Sort` = VALUES(`Sort`), `IsVisible` = VALUES(`IsVisible`), `Status` = VALUES(`Status`), `IsDeleted` = b'0', `UpdatedBy` = @createdBy, `UpdatedAt` = @now;
|
||||
|
||||
SET @ai_config_base_prompt_id = COALESCE((SELECT `Id` FROM `System_AdminMenu` WHERE `Code` = 'ai-config:base-prompt' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
|
||||
INSERT INTO `System_AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
|
||||
VALUES (@ai_config_base_prompt_id, @ai_config_id, '基础提示词配置', 'ai-config:base-prompt', '/ai-config/base-prompt', 'view.ai-config_base-prompt', 'ri:file-text-line', 0, b'1', 1, b'0', @createdBy, @now, @createdBy, @now)
|
||||
ON DUPLICATE KEY UPDATE `ParentId` = VALUES(`ParentId`), `Name` = VALUES(`Name`), `Path` = VALUES(`Path`), `Component` = VALUES(`Component`), `Icon` = VALUES(`Icon`), `Sort` = VALUES(`Sort`), `IsVisible` = VALUES(`IsVisible`), `Status` = VALUES(`Status`), `IsDeleted` = b'0', `UpdatedBy` = @createdBy, `UpdatedAt` = @now;
|
||||
|
||||
SET @main_nav_id = COALESCE((SELECT `Id` FROM `System_AdminMenu` WHERE `Code` = 'main-nav' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
|
||||
INSERT INTO `System_AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
|
||||
VALUES (@main_nav_id, 0, '主导航', 'main-nav', '/main-nav', 'layout.base', 'ri:organization-chart', 1, b'1', 1, b'0', @createdBy, @now, @createdBy, @now)
|
||||
ON DUPLICATE KEY UPDATE `ParentId` = VALUES(`ParentId`), `Name` = VALUES(`Name`), `Path` = VALUES(`Path`), `Component` = VALUES(`Component`), `Icon` = VALUES(`Icon`), `Sort` = VALUES(`Sort`), `IsVisible` = VALUES(`IsVisible`), `Status` = VALUES(`Status`), `IsDeleted` = b'0', `UpdatedBy` = @createdBy, `UpdatedAt` = @now;
|
||||
|
||||
SET @main_nav_home_id = COALESCE((SELECT `Id` FROM `System_AdminMenu` WHERE `Code` = 'main-nav:home' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
|
||||
INSERT INTO `System_AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
|
||||
VALUES (@main_nav_home_id, @main_nav_id, '仪表盘', 'main-nav:home', '/main-nav/home', 'view.main-nav_home', 'ri:home-2-line', 1, b'1', 1, b'0', @createdBy, @now, @createdBy, @now)
|
||||
ON DUPLICATE KEY UPDATE `ParentId` = VALUES(`ParentId`), `Name` = VALUES(`Name`), `Path` = VALUES(`Path`), `Component` = VALUES(`Component`), `Icon` = VALUES(`Icon`), `Sort` = VALUES(`Sort`), `IsVisible` = VALUES(`IsVisible`), `Status` = VALUES(`Status`), `IsDeleted` = b'0', `UpdatedBy` = @createdBy, `UpdatedAt` = @now;
|
||||
|
||||
SET @main_nav_journal_qr_code_id = COALESCE((SELECT `Id` FROM `System_AdminMenu` WHERE `Code` = 'main-nav:journal-qr-code' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
|
||||
INSERT INTO `System_AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
|
||||
VALUES (@main_nav_journal_qr_code_id, @main_nav_id, '期刊二维码管理', 'main-nav:journal-qr-code', '/main-nav/journal-qr-code', 'view.main-nav_journal-qr-code', 'ri:qr-code-line', 2, b'1', 1, b'0', @createdBy, @now, @createdBy, @now)
|
||||
ON DUPLICATE KEY UPDATE `ParentId` = VALUES(`ParentId`), `Name` = VALUES(`Name`), `Path` = VALUES(`Path`), `Component` = VALUES(`Component`), `Icon` = VALUES(`Icon`), `Sort` = VALUES(`Sort`), `IsVisible` = VALUES(`IsVisible`), `Status` = VALUES(`Status`), `IsDeleted` = b'0', `UpdatedBy` = @createdBy, `UpdatedAt` = @now;
|
||||
|
||||
SET @main_nav_book_management_id = COALESCE((SELECT `Id` FROM `System_AdminMenu` WHERE `Code` = 'main-nav:book-management' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
|
||||
INSERT INTO `System_AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
|
||||
VALUES (@main_nav_book_management_id, @main_nav_id, '书籍管理', 'main-nav:book-management', '/main-nav/book-management', 'ayout.base', 'ri:book-open-line', 5, b'1', 1, b'0', @createdBy, @now, @createdBy, @now)
|
||||
ON DUPLICATE KEY UPDATE `ParentId` = VALUES(`ParentId`), `Name` = VALUES(`Name`), `Path` = VALUES(`Path`), `Component` = VALUES(`Component`), `Icon` = VALUES(`Icon`), `Sort` = VALUES(`Sort`), `IsVisible` = VALUES(`IsVisible`), `Status` = VALUES(`Status`), `IsDeleted` = b'0', `UpdatedBy` = @createdBy, `UpdatedAt` = @now;
|
||||
|
||||
SET @main_nav_community_message_management_id = COALESCE((SELECT `Id` FROM `System_AdminMenu` WHERE `Code` = 'main-nav:community-message-management' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
|
||||
INSERT INTO `System_AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
|
||||
VALUES (@main_nav_community_message_management_id, @main_nav_id, '社区消息管理', 'main-nav:community-message-management', '/main-nav/community-message-management', 'view.main-nav_community-message-management', 'ic:baseline-mark-unread-chat-alt', 6, b'1', 1, b'0', @createdBy, @now, @createdBy, @now)
|
||||
ON DUPLICATE KEY UPDATE `ParentId` = VALUES(`ParentId`), `Name` = VALUES(`Name`), `Path` = VALUES(`Path`), `Component` = VALUES(`Component`), `Icon` = VALUES(`Icon`), `Sort` = VALUES(`Sort`), `IsVisible` = VALUES(`IsVisible`), `Status` = VALUES(`Status`), `IsDeleted` = b'0', `UpdatedBy` = @createdBy, `UpdatedAt` = @now;
|
||||
|
||||
SET @main_nav_medal_management_id = COALESCE((SELECT `Id` FROM `System_AdminMenu` WHERE `Code` = 'main-nav:medal-management' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
|
||||
INSERT INTO `System_AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
|
||||
VALUES (@main_nav_medal_management_id, @main_nav_id, '勋章管理', 'main-nav:medal-management', '/main-nav/medal-management', 'view.main-nav_medal-management', 'ri:medal-line', 7, b'1', 1, b'0', @createdBy, @now, @createdBy, @now)
|
||||
ON DUPLICATE KEY UPDATE `ParentId` = VALUES(`ParentId`), `Name` = VALUES(`Name`), `Path` = VALUES(`Path`), `Component` = VALUES(`Component`), `Icon` = VALUES(`Icon`), `Sort` = VALUES(`Sort`), `IsVisible` = VALUES(`IsVisible`), `Status` = VALUES(`Status`), `IsDeleted` = b'0', `UpdatedBy` = @createdBy, `UpdatedAt` = @now;
|
||||
|
||||
SET @main_nav_pet_management_id = COALESCE((SELECT `Id` FROM `System_AdminMenu` WHERE `Code` = 'main-nav:pet-management' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
|
||||
INSERT INTO `System_AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
|
||||
VALUES (@main_nav_pet_management_id, @main_nav_id, '宠物管理', 'main-nav:pet-management', '/main-nav/pet-management', 'view.main-nav_pet-management', 'mdi:paw', 8, b'1', 1, b'0', @createdBy, @now, @createdBy, @now)
|
||||
ON DUPLICATE KEY UPDATE `ParentId` = VALUES(`ParentId`), `Name` = VALUES(`Name`), `Path` = VALUES(`Path`), `Component` = VALUES(`Component`), `Icon` = VALUES(`Icon`), `Sort` = VALUES(`Sort`), `IsVisible` = VALUES(`IsVisible`), `Status` = VALUES(`Status`), `IsDeleted` = b'0', `UpdatedBy` = @createdBy, `UpdatedAt` = @now;
|
||||
|
||||
SET @main_nav_product_management_id = COALESCE((SELECT `Id` FROM `System_AdminMenu` WHERE `Code` = 'main-nav:product-management' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
|
||||
INSERT INTO `System_AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
|
||||
VALUES (@main_nav_product_management_id, @main_nav_id, '商品管理', 'main-nav:product-management', '/main-nav/product-management', 'view.main-nav_product-management', 'icon-park-solid:health-products', 9, b'1', 1, b'0', @createdBy, @now, @createdBy, @now)
|
||||
ON DUPLICATE KEY UPDATE `ParentId` = VALUES(`ParentId`), `Name` = VALUES(`Name`), `Path` = VALUES(`Path`), `Component` = VALUES(`Component`), `Icon` = VALUES(`Icon`), `Sort` = VALUES(`Sort`), `IsVisible` = VALUES(`IsVisible`), `Status` = VALUES(`Status`), `IsDeleted` = b'0', `UpdatedBy` = @createdBy, `UpdatedAt` = @now;
|
||||
|
||||
SET @main_nav_check_in_config_management_id = COALESCE((SELECT `Id` FROM `System_AdminMenu` WHERE `Code` = 'main-nav:check-in-config-management' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
|
||||
INSERT INTO `System_AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
|
||||
VALUES (@main_nav_check_in_config_management_id, @main_nav_id, '签到配置管理', 'main-nav:check-in-config-management', '/main-nav/check-in-config-management', 'view.main-nav_check-in-config-management', 'ri:check-line', 10, b'1', 1, b'0', @createdBy, @now, @createdBy, @now)
|
||||
ON DUPLICATE KEY UPDATE `ParentId` = VALUES(`ParentId`), `Name` = VALUES(`Name`), `Path` = VALUES(`Path`), `Component` = VALUES(`Component`), `Icon` = VALUES(`Icon`), `Sort` = VALUES(`Sort`), `IsVisible` = VALUES(`IsVisible`), `Status` = VALUES(`Status`), `IsDeleted` = b'0', `UpdatedBy` = @createdBy, `UpdatedAt` = @now;
|
||||
|
||||
SET @data_statistics_id = COALESCE((SELECT `Id` FROM `System_AdminMenu` WHERE `Code` = 'data-statistics' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
|
||||
INSERT INTO `System_AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
|
||||
VALUES (@data_statistics_id, 0, '数据统计', 'data-statistics', '/data-statistics', 'layout.base', 'ri:bar-chart-2-line', 2, b'1', 1, b'0', @createdBy, @now, @createdBy, @now)
|
||||
ON DUPLICATE KEY UPDATE `ParentId` = VALUES(`ParentId`), `Name` = VALUES(`Name`), `Path` = VALUES(`Path`), `Component` = VALUES(`Component`), `Icon` = VALUES(`Icon`), `Sort` = VALUES(`Sort`), `IsVisible` = VALUES(`IsVisible`), `Status` = VALUES(`Status`), `IsDeleted` = b'0', `UpdatedBy` = @createdBy, `UpdatedAt` = @now;
|
||||
|
||||
SET @data_statistics_statistics_id = COALESCE((SELECT `Id` FROM `System_AdminMenu` WHERE `Code` = 'data-statistics:statistics' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
|
||||
INSERT INTO `System_AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
|
||||
VALUES (@data_statistics_statistics_id, @data_statistics_id, '数据统计', 'data-statistics:statistics', '/data-statistics/statistics', 'view.data-statistics_statistics', 'ri:bar-chart-2-line', 0, b'1', 1, b'0', @createdBy, @now, @createdBy, @now)
|
||||
ON DUPLICATE KEY UPDATE `ParentId` = VALUES(`ParentId`), `Name` = VALUES(`Name`), `Path` = VALUES(`Path`), `Component` = VALUES(`Component`), `Icon` = VALUES(`Icon`), `Sort` = VALUES(`Sort`), `IsVisible` = VALUES(`IsVisible`), `Status` = VALUES(`Status`), `IsDeleted` = b'0', `UpdatedBy` = @createdBy, `UpdatedAt` = @now;
|
||||
|
||||
SET @system_manage_id = COALESCE((SELECT `Id` FROM `System_AdminMenu` WHERE `Code` = 'system-manage' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
|
||||
INSERT INTO `System_AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
|
||||
VALUES (@system_manage_id, 0, '系统管理', 'system-manage', '/system-manage', 'layout.base', 'ri:settings-2-line', 3, b'1', 1, b'0', @createdBy, @now, @createdBy, @now)
|
||||
ON DUPLICATE KEY UPDATE `ParentId` = VALUES(`ParentId`), `Name` = VALUES(`Name`), `Path` = VALUES(`Path`), `Component` = VALUES(`Component`), `Icon` = VALUES(`Icon`), `Sort` = VALUES(`Sort`), `IsVisible` = VALUES(`IsVisible`), `Status` = VALUES(`Status`), `IsDeleted` = b'0', `UpdatedBy` = @createdBy, `UpdatedAt` = @now;
|
||||
|
||||
SET @system_manage_menu_id = COALESCE((SELECT `Id` FROM `System_AdminMenu` WHERE `Code` = 'system-manage:menu' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
|
||||
INSERT INTO `System_AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
|
||||
VALUES (@system_manage_menu_id, @system_manage_id, '菜单管理', 'system-manage:menu', '/system-manage/menu-manage', 'view.system-manage_menu-manage', 'mingcute:list-collapse-fill', 3, b'1', 1, b'0', @createdBy, @now, @createdBy, @now)
|
||||
ON DUPLICATE KEY UPDATE `ParentId` = VALUES(`ParentId`), `Name` = VALUES(`Name`), `Path` = VALUES(`Path`), `Component` = VALUES(`Component`), `Icon` = VALUES(`Icon`), `Sort` = VALUES(`Sort`), `IsVisible` = VALUES(`IsVisible`), `Status` = VALUES(`Status`), `IsDeleted` = b'0', `UpdatedBy` = @createdBy, `UpdatedAt` = @now;
|
||||
|
||||
SET @system_manage_operation_log_id = COALESCE((SELECT `Id` FROM `System_AdminMenu` WHERE `Code` = 'system-manage:operation-log' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
|
||||
INSERT INTO `System_AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
|
||||
VALUES (@system_manage_operation_log_id, @system_manage_id, 'system-manage_operation-log-management', 'system-manage:operation-log', '/system-manage/operation-log-management', 'view.system-manage_operation-log-management', 'ri:file-list-3-line', 4, b'1', 1, b'0', @createdBy, @now, @createdBy, @now)
|
||||
ON DUPLICATE KEY UPDATE `ParentId` = VALUES(`ParentId`), `Name` = VALUES(`Name`), `Path` = VALUES(`Path`), `Component` = VALUES(`Component`), `Icon` = VALUES(`Icon`), `Sort` = VALUES(`Sort`), `IsVisible` = VALUES(`IsVisible`), `Status` = VALUES(`Status`), `IsDeleted` = b'0', `UpdatedBy` = @createdBy, `UpdatedAt` = @now;
|
||||
|
||||
SET @system_manage_user_id = COALESCE((SELECT `Id` FROM `System_AdminMenu` WHERE `Code` = 'system-manage:user' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
|
||||
INSERT INTO `System_AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
|
||||
VALUES (@system_manage_user_id, @system_manage_id, 'admin管理', 'system-manage:user', '/system-manage/user-manage', 'view.system-manage_user-manage', 'ic:sharp-admin-panel-settings', 5, b'1', 1, b'0', @createdBy, @now, @createdBy, @now)
|
||||
ON DUPLICATE KEY UPDATE `ParentId` = VALUES(`ParentId`), `Name` = VALUES(`Name`), `Path` = VALUES(`Path`), `Component` = VALUES(`Component`), `Icon` = VALUES(`Icon`), `Sort` = VALUES(`Sort`), `IsVisible` = VALUES(`IsVisible`), `Status` = VALUES(`Status`), `IsDeleted` = b'0', `UpdatedBy` = @createdBy, `UpdatedAt` = @now;
|
||||
|
||||
SET @system_manage_user_management_id = COALESCE((SELECT `Id` FROM `System_AdminMenu` WHERE `Code` = 'system-manage:user-management' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
|
||||
INSERT INTO `System_AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
|
||||
VALUES (@system_manage_user_management_id, @system_manage_id, '用户管理', 'system-manage:user-management', '/system-manage/users-management', 'view.system-manage_users-management', 'ri:user-2-line', 6, b'1', 1, b'0', @createdBy, @now, @createdBy, @now)
|
||||
ON DUPLICATE KEY UPDATE `ParentId` = VALUES(`ParentId`), `Name` = VALUES(`Name`), `Path` = VALUES(`Path`), `Component` = VALUES(`Component`), `Icon` = VALUES(`Icon`), `Sort` = VALUES(`Sort`), `IsVisible` = VALUES(`IsVisible`), `Status` = VALUES(`Status`), `IsDeleted` = b'0', `UpdatedBy` = @createdBy, `UpdatedAt` = @now;
|
||||
|
||||
SET @help_center_id = COALESCE((SELECT `Id` FROM `System_AdminMenu` WHERE `Code` = 'help-center' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
|
||||
INSERT INTO `System_AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
|
||||
VALUES (@help_center_id, 0, '帮助中心', 'help-center', '/help-center', 'layout.base', 'ic:baseline-support-agent', 4, b'1', 1, b'0', @createdBy, @now, @createdBy, @now)
|
||||
ON DUPLICATE KEY UPDATE `ParentId` = VALUES(`ParentId`), `Name` = VALUES(`Name`), `Path` = VALUES(`Path`), `Component` = VALUES(`Component`), `Icon` = VALUES(`Icon`), `Sort` = VALUES(`Sort`), `IsVisible` = VALUES(`IsVisible`), `Status` = VALUES(`Status`), `IsDeleted` = b'0', `UpdatedBy` = @createdBy, `UpdatedAt` = @now;
|
||||
|
||||
SET @help_center_contact_support_id = COALESCE((SELECT `Id` FROM `System_AdminMenu` WHERE `Code` = 'help-center:contact-support' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
|
||||
INSERT INTO `System_AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
|
||||
VALUES (@help_center_contact_support_id, @help_center_id, '联系客服', 'help-center:contact-support', '/help-center/contact-support', 'view.help-center_contact-support', 'ri:customer-service-2-line', 0, b'1', 1, b'0', @createdBy, @now, @createdBy, @now)
|
||||
ON DUPLICATE KEY UPDATE `ParentId` = VALUES(`ParentId`), `Name` = VALUES(`Name`), `Path` = VALUES(`Path`), `Component` = VALUES(`Component`), `Icon` = VALUES(`Icon`), `Sort` = VALUES(`Sort`), `IsVisible` = VALUES(`IsVisible`), `Status` = VALUES(`Status`), `IsDeleted` = b'0', `UpdatedBy` = @createdBy, `UpdatedAt` = @now;
|
||||
|
||||
SET @help_center_usage_help_id = COALESCE((SELECT `Id` FROM `System_AdminMenu` WHERE `Code` = 'help-center:usage-help' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
|
||||
INSERT INTO `System_AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
|
||||
VALUES (@help_center_usage_help_id, @help_center_id, '使用帮助', 'help-center:usage-help', '/help-center/usage-help', 'view.help-center_usage-help', 'ic:baseline-support-agent', 1, b'1', 1, b'0', @createdBy, @now, @createdBy, @now)
|
||||
ON DUPLICATE KEY UPDATE `ParentId` = VALUES(`ParentId`), `Name` = VALUES(`Name`), `Path` = VALUES(`Path`), `Component` = VALUES(`Component`), `Icon` = VALUES(`Icon`), `Sort` = VALUES(`Sort`), `IsVisible` = VALUES(`IsVisible`), `Status` = VALUES(`Status`), `IsDeleted` = b'0', `UpdatedBy` = @createdBy, `UpdatedAt` = @now;
|
||||
|
||||
SET @pen_management_id = COALESCE((SELECT `Id` FROM `System_AdminMenu` WHERE `Code` = 'pen-management' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
|
||||
INSERT INTO `System_AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
|
||||
VALUES (@pen_management_id, 0, '点阵笔管理', 'pen-management', '/pen-management', 'layout.base', 'ic:round-edit-off', 8, b'1', 1, b'0', @createdBy, @now, @createdBy, @now)
|
||||
ON DUPLICATE KEY UPDATE `ParentId` = VALUES(`ParentId`), `Name` = VALUES(`Name`), `Path` = VALUES(`Path`), `Component` = VALUES(`Component`), `Icon` = VALUES(`Icon`), `Sort` = VALUES(`Sort`), `IsVisible` = VALUES(`IsVisible`), `Status` = VALUES(`Status`), `IsDeleted` = b'0', `UpdatedBy` = @createdBy, `UpdatedAt` = @now;
|
||||
|
||||
SET @pen_management_pen_list_id = COALESCE((SELECT `Id` FROM `System_AdminMenu` WHERE `Code` = 'pen-management:list' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
|
||||
INSERT INTO `System_AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
|
||||
VALUES (@pen_management_pen_list_id, @pen_management_id, '点阵笔列表', 'pen-management:list', '/pen-management/pen-list', 'view.pen-management_pen-list', 'ri:bar-chart-2-line', 0, b'1', 1, b'0', @createdBy, @now, @createdBy, @now)
|
||||
ON DUPLICATE KEY UPDATE `ParentId` = VALUES(`ParentId`), `Name` = VALUES(`Name`), `Path` = VALUES(`Path`), `Component` = VALUES(`Component`), `Icon` = VALUES(`Icon`), `Sort` = VALUES(`Sort`), `IsVisible` = VALUES(`IsVisible`), `Status` = VALUES(`Status`), `IsDeleted` = b'0', `UpdatedBy` = @createdBy, `UpdatedAt` = @now;
|
||||
|
||||
SET @pen_management_coordinate_data_id = COALESCE((SELECT `Id` FROM `System_AdminMenu` WHERE `Code` = 'pen-management:coordinate-data' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
|
||||
INSERT INTO `System_AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
|
||||
VALUES (@pen_management_coordinate_data_id, @pen_management_id, '点阵笔坐标数据', 'pen-management:coordinate-data', '/pen-management/coordinate-data', 'view.pen-management_coordinate-data', 'ic:baseline-edit', 1, b'1', 1, b'0', @createdBy, @now, @createdBy, @now)
|
||||
ON DUPLICATE KEY UPDATE `ParentId` = VALUES(`ParentId`), `Name` = VALUES(`Name`), `Path` = VALUES(`Path`), `Component` = VALUES(`Component`), `Icon` = VALUES(`Icon`), `Sort` = VALUES(`Sort`), `IsVisible` = VALUES(`IsVisible`), `Status` = VALUES(`Status`), `IsDeleted` = b'0', `UpdatedBy` = @createdBy, `UpdatedAt` = @now;
|
||||
22
DatabaseScripts/message_outbox.sql
Normal file
22
DatabaseScripts/message_outbox.sql
Normal file
@ -0,0 +1,22 @@
|
||||
CREATE TABLE IF NOT EXISTS `Message_Outbox` (
|
||||
`Id` bigint NOT NULL,
|
||||
`Exchange` varchar(200) NOT NULL,
|
||||
`Queue` varchar(200) NOT NULL,
|
||||
`RoutingKey` varchar(200) NOT NULL,
|
||||
`Payload` json NOT NULL,
|
||||
`RetryCount` int NOT NULL DEFAULT 0,
|
||||
`NextRetryAt` datetime NULL,
|
||||
`SentAt` datetime NULL,
|
||||
`LastError` varchar(2000) NULL,
|
||||
`BusinessType` varchar(100) NOT NULL,
|
||||
`BusinessId` bigint NOT NULL,
|
||||
`Status` int NOT NULL DEFAULT 0,
|
||||
`IsDeleted` bit NOT NULL DEFAULT b'0',
|
||||
`CreatedBy` varchar(100) NOT NULL,
|
||||
`CreatedAt` datetime NOT NULL,
|
||||
`UpdatedBy` varchar(100) NULL,
|
||||
`UpdatedAt` datetime NULL,
|
||||
PRIMARY KEY (`Id`),
|
||||
KEY `idx_message_outbox_status_next_retry` (`Status`, `NextRetryAt`, `CreatedAt`),
|
||||
KEY `idx_message_outbox_business` (`BusinessType`, `BusinessId`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
5
DatabaseScripts/stability_unique_constraints.sql
Normal file
5
DatabaseScripts/stability_unique_constraints.sql
Normal file
@ -0,0 +1,5 @@
|
||||
ALTER TABLE `CheckIn_Record`
|
||||
ADD UNIQUE KEY `uk_checkin_record_user_date_type_deleted` (`UserId`, `CheckInDate`, `Type`, `IsDeleted`);
|
||||
|
||||
ALTER TABLE `User_Bag`
|
||||
ADD UNIQUE KEY `uk_user_bag_available_item` (`UserId`, `ItemId`, `Status`, `IsDeleted`);
|
||||
5
Directory.Build.props
Normal file
5
Directory.Build.props
Normal file
@ -0,0 +1,5 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<NoWarn>$(NoWarn);CS0105;CS0108;CS0168;CS0169;CS0618;CS1570;CS1572;CS1573;CS1591;CS8600;CS8601;CS8602;CS8603;CS8604;CS8618;CS8625;CS8629;CS8634;CS8714;CS9113</NoWarn>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@ -7,6 +7,10 @@ namespace QYZH.InteractiveMagazine.Common.Extensions
|
||||
/// </summary>
|
||||
public static class DateTimeExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// 时间戳起始日期
|
||||
/// </summary>
|
||||
public static readonly DateTime TimestampStart = new(1970, 1, 1, 0, 0, 0, 0);
|
||||
/// <summary>
|
||||
/// 转换为Unix时间戳(秒)
|
||||
/// </summary>
|
||||
@ -84,5 +88,15 @@ namespace QYZH.InteractiveMagazine.Common.Extensions
|
||||
{
|
||||
return dateTime.Date.AddDays(1).AddTicks(-1);
|
||||
}
|
||||
|
||||
public static long ToTimestamp(this DateTime dateTime, bool milliseconds = false)
|
||||
{
|
||||
var timestamp = dateTime.ToUniversalTime() - TimestampStart;
|
||||
return (long)(milliseconds ? timestamp.TotalMilliseconds : timestamp.TotalSeconds);
|
||||
}
|
||||
public static long ToMillisecondsTimestamp(this DateTime dateTime)
|
||||
{
|
||||
return ToTimestamp(dateTime, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
441
QYZH.InteractiveMagazine.Common/Extensions/Extension.Convert.cs
Normal file
441
QYZH.InteractiveMagazine.Common/Extensions/Extension.Convert.cs
Normal file
@ -0,0 +1,441 @@
|
||||
using System.Collections;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.Common.Extensions
|
||||
{
|
||||
public static partial class Extensions
|
||||
{
|
||||
#region 转换为long
|
||||
/// <summary>
|
||||
/// 将object转换为long,若转换失败,则返回0。不抛出异常。
|
||||
/// </summary>
|
||||
/// <param name="str"></param>
|
||||
/// <returns></returns>
|
||||
public static long ParseToLong(this object obj)
|
||||
{
|
||||
try
|
||||
{
|
||||
return long.Parse(obj.ToString());
|
||||
}
|
||||
catch
|
||||
{
|
||||
return 0L;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将object转换为long,若转换失败,则返回指定值。不抛出异常。
|
||||
/// </summary>
|
||||
/// <param name="str"></param>
|
||||
/// <param name="defaultValue"></param>
|
||||
/// <returns></returns>
|
||||
public static long ParseToLong(this string str, long defaultValue)
|
||||
{
|
||||
try
|
||||
{
|
||||
return long.Parse(str);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 转换为int
|
||||
/// <summary>
|
||||
/// 将object转换为int,若转换失败,则返回0。不抛出异常。
|
||||
/// </summary>
|
||||
/// <param name="str"></param>
|
||||
/// <returns></returns>
|
||||
public static int ParseToInt(this object str)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Convert.ToInt32(str);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将object转换为int,若转换失败,则返回指定值。不抛出异常。
|
||||
/// null返回默认值
|
||||
/// </summary>
|
||||
/// <param name="str"></param>
|
||||
/// <param name="defaultValue"></param>
|
||||
/// <returns></returns>
|
||||
public static int ParseToInt(this object str, int defaultValue)
|
||||
{
|
||||
if (str == null)
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
try
|
||||
{
|
||||
return Convert.ToInt32(str);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 转换为short
|
||||
/// <summary>
|
||||
/// 将object转换为short,若转换失败,则返回0。不抛出异常。
|
||||
/// </summary>
|
||||
/// <param name="str"></param>
|
||||
/// <returns></returns>
|
||||
public static short ParseToShort(this object obj)
|
||||
{
|
||||
try
|
||||
{
|
||||
return short.Parse(obj.ToString());
|
||||
}
|
||||
catch
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将object转换为short,若转换失败,则返回指定值。不抛出异常。
|
||||
/// </summary>
|
||||
/// <param name="str"></param>
|
||||
/// <returns></returns>
|
||||
public static short ParseToShort(this object str, short defaultValue)
|
||||
{
|
||||
try
|
||||
{
|
||||
return short.Parse(str.ToString());
|
||||
}
|
||||
catch
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 转换为demical
|
||||
/// <summary>
|
||||
/// 将object转换为demical,若转换失败,则返回指定值。不抛出异常。
|
||||
/// </summary>
|
||||
/// <param name="str"></param>
|
||||
/// <returns></returns>
|
||||
public static decimal ParseToDecimal(this object str, decimal defaultValue)
|
||||
{
|
||||
try
|
||||
{
|
||||
return decimal.Parse(str.ToString());
|
||||
}
|
||||
catch
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将object转换为demical,若转换失败,则返回0。不抛出异常。
|
||||
/// </summary>
|
||||
/// <param name="str"></param>
|
||||
/// <returns></returns>
|
||||
public static decimal ParseToDecimal(this object str)
|
||||
{
|
||||
try
|
||||
{
|
||||
return decimal.Parse(str.ToString());
|
||||
}
|
||||
catch
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 转化为bool
|
||||
/// <summary>
|
||||
/// 将object转换为bool,若转换失败,则返回false。不抛出异常。
|
||||
/// </summary>
|
||||
/// <param name="str"></param>
|
||||
/// <returns></returns>
|
||||
public static bool ParseToBool(this object str)
|
||||
{
|
||||
try
|
||||
{
|
||||
return bool.Parse(str.ToString());
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将object转换为bool,若转换失败,则返回指定值。不抛出异常。
|
||||
/// </summary>
|
||||
/// <param name="str"></param>
|
||||
/// <returns></returns>
|
||||
public static bool ParseToBool(this object str, bool result)
|
||||
{
|
||||
try
|
||||
{
|
||||
return bool.Parse(str.ToString());
|
||||
}
|
||||
catch
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 转换为float
|
||||
/// <summary>
|
||||
/// 将object转换为float,若转换失败,则返回0。不抛出异常。
|
||||
/// </summary>
|
||||
/// <param name="str"></param>
|
||||
/// <returns></returns>
|
||||
public static float ParseToFloat(this object str)
|
||||
{
|
||||
try
|
||||
{
|
||||
return float.Parse(str.ToString());
|
||||
}
|
||||
catch
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将object转换为float,若转换失败,则返回指定值。不抛出异常。
|
||||
/// </summary>
|
||||
/// <param name="str"></param>
|
||||
/// <returns></returns>
|
||||
public static float ParseToFloat(this object str, float result)
|
||||
{
|
||||
try
|
||||
{
|
||||
return float.Parse(str.ToString());
|
||||
}
|
||||
catch
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 转换为Guid
|
||||
/// <summary>
|
||||
/// 将string转换为Guid,若转换失败,则返回Guid.Empty。不抛出异常。
|
||||
/// </summary>
|
||||
/// <param name="str"></param>
|
||||
/// <returns></returns>
|
||||
public static Guid ParseToGuid(this string str)
|
||||
{
|
||||
try
|
||||
{
|
||||
return new Guid(str);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return Guid.Empty;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 转换为DateTime
|
||||
/// <summary>
|
||||
/// 将string转换为DateTime,若转换失败,则返回日期最小值。不抛出异常。
|
||||
/// </summary>
|
||||
/// <param name="str"></param>
|
||||
/// <returns></returns>
|
||||
public static DateTime ParseToDateTime(this string str)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(str))
|
||||
{
|
||||
return DateTime.MinValue;
|
||||
}
|
||||
if (str.Contains("-") || str.Contains("/"))
|
||||
{
|
||||
return DateTime.Parse(str);
|
||||
}
|
||||
else
|
||||
{
|
||||
int length = str.Length;
|
||||
switch (length)
|
||||
{
|
||||
case 4:
|
||||
return DateTime.ParseExact(str, "yyyy", System.Globalization.CultureInfo.CurrentCulture);
|
||||
case 6:
|
||||
return DateTime.ParseExact(str, "yyyyMM", System.Globalization.CultureInfo.CurrentCulture);
|
||||
case 8:
|
||||
return DateTime.ParseExact(str, "yyyyMMdd", System.Globalization.CultureInfo.CurrentCulture);
|
||||
case 10:
|
||||
return DateTime.ParseExact(str, "yyyyMMddHH", System.Globalization.CultureInfo.CurrentCulture);
|
||||
case 12:
|
||||
return DateTime.ParseExact(str, "yyyyMMddHHmm", System.Globalization.CultureInfo.CurrentCulture);
|
||||
case 14:
|
||||
return DateTime.ParseExact(str, "yyyyMMddHHmmss", System.Globalization.CultureInfo.CurrentCulture);
|
||||
default:
|
||||
return DateTime.ParseExact(str, "yyyyMMddHHmmss", System.Globalization.CultureInfo.CurrentCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return DateTime.MinValue;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将string转换为DateTime,若转换失败,则返回默认值。
|
||||
/// </summary>
|
||||
/// <param name="str"></param>
|
||||
/// <param name="defaultValue"></param>
|
||||
/// <returns></returns>
|
||||
public static DateTime ParseToDateTime(this string str, DateTime? defaultValue)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(str))
|
||||
{
|
||||
return defaultValue.GetValueOrDefault();
|
||||
}
|
||||
if (str.Contains("-") || str.Contains("/"))
|
||||
{
|
||||
return DateTime.Parse(str);
|
||||
}
|
||||
else
|
||||
{
|
||||
int length = str.Length;
|
||||
switch (length)
|
||||
{
|
||||
case 4:
|
||||
return DateTime.ParseExact(str, "yyyy", System.Globalization.CultureInfo.CurrentCulture);
|
||||
case 6:
|
||||
return DateTime.ParseExact(str, "yyyyMM", System.Globalization.CultureInfo.CurrentCulture);
|
||||
case 8:
|
||||
return DateTime.ParseExact(str, "yyyyMMdd", System.Globalization.CultureInfo.CurrentCulture);
|
||||
case 10:
|
||||
return DateTime.ParseExact(str, "yyyyMMddHH", System.Globalization.CultureInfo.CurrentCulture);
|
||||
case 12:
|
||||
return DateTime.ParseExact(str, "yyyyMMddHHmm", System.Globalization.CultureInfo.CurrentCulture);
|
||||
case 14:
|
||||
return DateTime.ParseExact(str, "yyyyMMddHHmmss", System.Globalization.CultureInfo.CurrentCulture);
|
||||
default:
|
||||
return DateTime.ParseExact(str, "yyyyMMddHHmmss", System.Globalization.CultureInfo.CurrentCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return defaultValue.GetValueOrDefault();
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 转换为string
|
||||
/// <summary>
|
||||
/// 将object转换为string,若转换失败,则返回""。不抛出异常。
|
||||
/// </summary>
|
||||
/// <param name="str"></param>
|
||||
/// <returns></returns>
|
||||
public static string ParseToString(this object obj)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (obj == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
return obj.ToString();
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
public static string ParseToStrings<T>(this object obj)
|
||||
{
|
||||
try
|
||||
{
|
||||
var list = obj as IEnumerable<T>;
|
||||
if (list != null)
|
||||
{
|
||||
return string.Join(",", list);
|
||||
}
|
||||
else
|
||||
{
|
||||
return obj.ToString();
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 转换为double
|
||||
/// <summary>
|
||||
/// 将object转换为double,若转换失败,则返回0。不抛出异常。
|
||||
/// </summary>
|
||||
/// <param name="obj"></param>
|
||||
/// <returns></returns>
|
||||
public static double ParseToDouble(this object obj)
|
||||
{
|
||||
try
|
||||
{
|
||||
return double.Parse(obj.ToString());
|
||||
}
|
||||
catch
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将object转换为double,若转换失败,则返回指定值。不抛出异常。
|
||||
/// </summary>
|
||||
/// <param name="str"></param>
|
||||
/// <param name="defaultValue"></param>
|
||||
/// <returns></returns>
|
||||
public static double ParseToDouble(this object str, double defaultValue)
|
||||
{
|
||||
try
|
||||
{
|
||||
return double.Parse(str.ToString());
|
||||
}
|
||||
catch
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 强制转换类型
|
||||
/// <summary>
|
||||
/// 强制转换类型
|
||||
/// </summary>
|
||||
/// <typeparam name="TResult"></typeparam>
|
||||
/// <param name="source"></param>
|
||||
/// <returns></returns>
|
||||
public static IEnumerable<TResult> CastSuper<TResult>(this IEnumerable source)
|
||||
{
|
||||
foreach (object item in source)
|
||||
{
|
||||
yield return (TResult)Convert.ChangeType(item, typeof(TResult));
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
//using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.Common.Extensions
|
||||
{
|
||||
public static partial class Extensions
|
||||
{
|
||||
public static bool IsEmpty(this object value)
|
||||
{
|
||||
if (value != null && !string.IsNullOrEmpty(value.ParseToString()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
public static bool IsNotEmpty(this object value)
|
||||
{
|
||||
return !IsEmpty(value);
|
||||
}
|
||||
public static bool IsNullOrZero(this object value)
|
||||
{
|
||||
if (value == null || value.ParseToString().Trim() == "0")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
//public static bool IsAjaxRequest(this HttpRequest request)
|
||||
//{
|
||||
// if (request == null)
|
||||
// throw new ArgumentNullException("request");
|
||||
|
||||
// if (request.Headers != null)
|
||||
// return request.Headers["X-Requested-With"] == "XMLHttpRequest";
|
||||
// return false;
|
||||
//}
|
||||
}
|
||||
}
|
||||
@ -90,5 +90,15 @@ namespace QYZH.InteractiveMagazine.Common.Extensions
|
||||
|
||||
return dictionary;
|
||||
}
|
||||
|
||||
public static bool IsNull<T>(this IList<T>? s)
|
||||
{
|
||||
return s == null || s?.Count() < 1;
|
||||
}
|
||||
|
||||
public static bool IsNull(this object? s)
|
||||
{
|
||||
return s == null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -146,5 +146,72 @@ namespace QYZH.InteractiveMagazine.Common.Extensions
|
||||
|
||||
return Regex.IsMatch(input, pattern);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断字符串是否为Null、空
|
||||
/// </summary>
|
||||
/// <param name="s"></param>
|
||||
/// <returns></returns>
|
||||
public static bool IsNull(this string s)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(s);
|
||||
}
|
||||
|
||||
public static string RemoveDomain(this string str)
|
||||
{
|
||||
if (str.IsNull()) return str;
|
||||
// 匹配域名后的路径部分(包括第一个/)
|
||||
return Regex.Replace(str, @"https?://[^/]+/([^""\s<>]*)", "$1");
|
||||
}
|
||||
|
||||
public static string AddDomain(this string str, string domainUrl)
|
||||
{
|
||||
if (string.IsNullOrEmpty(str) || string.IsNullOrEmpty(domainUrl)) return str;
|
||||
|
||||
string pattern = @"src=""([^""]+)""";
|
||||
return Regex.Replace(str, pattern, match =>
|
||||
{
|
||||
string originalUrl = match.Groups[1].Value;
|
||||
// 忽略base64图片
|
||||
if (originalUrl.StartsWith("data:", StringComparison.OrdinalIgnoreCase)) return match.Value; // 返回原样
|
||||
// 如果已经是完整URL则不做处理
|
||||
if (Uri.IsWellFormedUriString(originalUrl, UriKind.Absolute)) return match.Value; // 返回原样
|
||||
|
||||
|
||||
// 处理URL开头可能存在的斜杠
|
||||
var trimmedUrl = originalUrl.StartsWith("/") ? originalUrl.Substring(1) : originalUrl;
|
||||
var trimmedDomain = domainUrl.EndsWith("/") ? domainUrl : domainUrl + "/";
|
||||
|
||||
string newUrl = $"{trimmedDomain}{trimmedUrl}";
|
||||
return $"src=\"{newUrl}\""; // 返回替换后的属性
|
||||
});
|
||||
}
|
||||
|
||||
public static string ToExtension(this string str)
|
||||
{
|
||||
return Regex.Match(str, @"(?<=\.)[a-zA-Z0-9]+$").Value;
|
||||
}
|
||||
|
||||
public static bool NotNull(this string s)
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(s);
|
||||
}
|
||||
|
||||
public static List<string> AllUrl(this string str, string url = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(str)) return new List<string>();
|
||||
|
||||
string pattern = @"src=""([^""]+)""";
|
||||
|
||||
var matches = Regex.Matches(str, pattern, RegexOptions.IgnoreCase);
|
||||
|
||||
return matches
|
||||
.Cast<Match>()
|
||||
.Select(m => m.Groups[1].Value)
|
||||
.Where(u => !string.IsNullOrEmpty(u) && !u.StartsWith("data:", StringComparison.OrdinalIgnoreCase)) // 排除base64
|
||||
.Where(url => !string.IsNullOrEmpty(url) && url.StartsWith(url, StringComparison.OrdinalIgnoreCase))
|
||||
//.Where(url => url.StartsWith("https://example.com")) // 二次验证
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
26
QYZH.InteractiveMagazine.Common/Helpers/DomainHelper.cs
Normal file
26
QYZH.InteractiveMagazine.Common/Helpers/DomainHelper.cs
Normal file
@ -0,0 +1,26 @@
|
||||
namespace QYZH.InteractiveMagazine.Common.Helpers
|
||||
{
|
||||
public static class DomainHelper
|
||||
{
|
||||
public static string OssDomain { get; set; }
|
||||
|
||||
public static string VodDomain { get; set; }
|
||||
|
||||
public static string OssFullUrl(string url) => GetFullUrl(url, OssDomain);
|
||||
|
||||
public static string VodFullUrl(string url) => GetFullUrl(url, VodDomain);
|
||||
|
||||
private static string GetFullUrl(string url, string domain)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(url)) return string.Empty;
|
||||
|
||||
if (url.StartsWith("http")) return url;
|
||||
|
||||
// 确保域名末尾没有斜杠,url开头有斜杠
|
||||
var cleanDomain = domain.TrimEnd('/');
|
||||
var cleanUrl = url.StartsWith("/") ? url : "/" + url;
|
||||
|
||||
return $"{cleanDomain}{cleanUrl}";
|
||||
}
|
||||
}
|
||||
}
|
||||
37
QYZH.InteractiveMagazine.Common/Helpers/JsonConverterUtil.cs
Normal file
37
QYZH.InteractiveMagazine.Common/Helpers/JsonConverterUtil.cs
Normal file
@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.Common.Helpers
|
||||
{
|
||||
public class JsonConverterUtil
|
||||
{
|
||||
public class DateTimeNullConverter : JsonConverter<DateTime?>
|
||||
{
|
||||
public override DateTime? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
=> string.IsNullOrEmpty(reader.GetString()) ? default : ParseDateTime(reader.GetString());
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, DateTime? value, JsonSerializerOptions options)
|
||||
=> writer.WriteStringValue(value?.ToString("yyyy-MM-dd HH:mm:ss"));
|
||||
}
|
||||
|
||||
public class DateTimeConverter : JsonConverter<DateTime>
|
||||
{
|
||||
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
var dateTime = ParseDateTime(reader.GetString());
|
||||
return dateTime == null ? DateTime.MinValue : dateTime.Value;
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
|
||||
=> writer.WriteStringValue(value.ToString("yyyy-MM-dd HH:mm:ss"));
|
||||
}
|
||||
|
||||
public static DateTime? ParseDateTime(string dateStr)
|
||||
{
|
||||
if (System.Text.RegularExpressions.Regex.IsMatch(dateStr, @"^\d{4}[/-]") && DateTime.TryParse(dateStr, null, System.Globalization.DateTimeStyles.AssumeLocal, out var dateVal))
|
||||
return dateVal;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
473
QYZH.InteractiveMagazine.Common/Helpers/QrCodeHelper.cs
Normal file
473
QYZH.InteractiveMagazine.Common/Helpers/QrCodeHelper.cs
Normal file
@ -0,0 +1,473 @@
|
||||
using System.IO.Compression;
|
||||
using System.Text;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.Common.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// 二维码帮助类
|
||||
/// </summary>
|
||||
public static class QrCodeHelper
|
||||
{
|
||||
private const int Version = 4;
|
||||
private const int Size = Version * 4 + 17;
|
||||
private const int DataCodewordCount = 80;
|
||||
private const int ErrorCorrectionCodewordCount = 20;
|
||||
private const int QuietZone = 4;
|
||||
private const int Scale = 10;
|
||||
private static readonly int[] AlignmentPatternPositions = [6, 26];
|
||||
|
||||
/// <summary>
|
||||
/// 生成二维码PNG图片字节
|
||||
/// </summary>
|
||||
/// <param name="content">二维码内容</param>
|
||||
/// <returns>PNG图片字节</returns>
|
||||
public static byte[] GeneratePng(string content)
|
||||
{
|
||||
var dataCodewords = CreateDataCodewords(content);
|
||||
var allCodewords = dataCodewords.Concat(CreateErrorCorrectionCodewords(dataCodewords)).ToArray();
|
||||
var modules = BuildModules(allCodewords);
|
||||
var imageSize = (Size + QuietZone * 2) * Scale;
|
||||
var pixels = new byte[imageSize * imageSize * 4];
|
||||
|
||||
for (var i = 0; i < pixels.Length; i += 4)
|
||||
{
|
||||
pixels[i] = 255;
|
||||
pixels[i + 1] = 255;
|
||||
pixels[i + 2] = 255;
|
||||
pixels[i + 3] = 255;
|
||||
}
|
||||
|
||||
for (var y = 0; y < Size; y++)
|
||||
{
|
||||
for (var x = 0; x < Size; x++)
|
||||
{
|
||||
if (modules[y, x])
|
||||
{
|
||||
FillModule(pixels, imageSize, x + QuietZone, y + QuietZone);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return EncodePng(imageSize, imageSize, pixels);
|
||||
}
|
||||
|
||||
private static byte[] CreateDataCodewords(string content)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(content);
|
||||
if (bytes.Length > 78)
|
||||
{
|
||||
throw new InvalidOperationException("二维码内容过长");
|
||||
}
|
||||
|
||||
var bits = new List<bool>();
|
||||
AppendBits(bits, 0b0100, 4);
|
||||
AppendBits(bits, bytes.Length, 8);
|
||||
foreach (var value in bytes)
|
||||
{
|
||||
AppendBits(bits, value, 8);
|
||||
}
|
||||
|
||||
var remaining = DataCodewordCount * 8 - bits.Count;
|
||||
AppendBits(bits, 0, Math.Min(4, remaining));
|
||||
|
||||
while (bits.Count % 8 != 0)
|
||||
{
|
||||
bits.Add(false);
|
||||
}
|
||||
|
||||
var data = BitsToBytes(bits);
|
||||
var pad = true;
|
||||
while (data.Count < DataCodewordCount)
|
||||
{
|
||||
data.Add((byte)(pad ? 0xEC : 0x11));
|
||||
pad = !pad;
|
||||
}
|
||||
|
||||
return data.ToArray();
|
||||
}
|
||||
|
||||
private static bool[,] BuildModules(byte[] codewords)
|
||||
{
|
||||
var modules = new bool[Size, Size];
|
||||
var reserved = new bool[Size, Size];
|
||||
|
||||
AddFinder(modules, reserved, 0, 0);
|
||||
AddFinder(modules, reserved, Size - 7, 0);
|
||||
AddFinder(modules, reserved, 0, Size - 7);
|
||||
AddTimingPatterns(modules, reserved);
|
||||
AddAlignmentPatterns(modules, reserved);
|
||||
ReserveFormatAreas(reserved);
|
||||
|
||||
modules[Version * 4 + 9, 8] = true;
|
||||
reserved[Version * 4 + 9, 8] = true;
|
||||
|
||||
AddDataModules(modules, reserved, codewords);
|
||||
AddFormatBits(modules);
|
||||
|
||||
return modules;
|
||||
}
|
||||
|
||||
private static void AddFinder(bool[,] modules, bool[,] reserved, int left, int top)
|
||||
{
|
||||
for (var y = -1; y <= 7; y++)
|
||||
{
|
||||
for (var x = -1; x <= 7; x++)
|
||||
{
|
||||
var px = left + x;
|
||||
var py = top + y;
|
||||
if (px < 0 || py < 0 || px >= Size || py >= Size)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
reserved[py, px] = true;
|
||||
modules[py, px] = x >= 0 && x <= 6 && y >= 0 && y <= 6
|
||||
&& (x == 0 || x == 6 || y == 0 || y == 6 || (x >= 2 && x <= 4 && y >= 2 && y <= 4));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddTimingPatterns(bool[,] modules, bool[,] reserved)
|
||||
{
|
||||
for (var i = 8; i < Size - 8; i++)
|
||||
{
|
||||
var isDark = i % 2 == 0;
|
||||
modules[6, i] = isDark;
|
||||
modules[i, 6] = isDark;
|
||||
reserved[6, i] = true;
|
||||
reserved[i, 6] = true;
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddAlignmentPatterns(bool[,] modules, bool[,] reserved)
|
||||
{
|
||||
foreach (var centerY in AlignmentPatternPositions)
|
||||
{
|
||||
foreach (var centerX in AlignmentPatternPositions)
|
||||
{
|
||||
if (reserved[centerY, centerX])
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for (var y = -2; y <= 2; y++)
|
||||
{
|
||||
for (var x = -2; x <= 2; x++)
|
||||
{
|
||||
var px = centerX + x;
|
||||
var py = centerY + y;
|
||||
reserved[py, px] = true;
|
||||
modules[py, px] = Math.Max(Math.Abs(x), Math.Abs(y)) != 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ReserveFormatAreas(bool[,] reserved)
|
||||
{
|
||||
for (var i = 0; i <= 8; i++)
|
||||
{
|
||||
reserved[8, i] = true;
|
||||
reserved[i, 8] = true;
|
||||
}
|
||||
|
||||
for (var i = 0; i < 8; i++)
|
||||
{
|
||||
reserved[8, Size - 1 - i] = true;
|
||||
}
|
||||
|
||||
for (var i = 0; i < 7; i++)
|
||||
{
|
||||
reserved[Size - 1 - i, 8] = true;
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddDataModules(bool[,] modules, bool[,] reserved, byte[] codewords)
|
||||
{
|
||||
var bits = new List<bool>();
|
||||
foreach (var codeword in codewords)
|
||||
{
|
||||
AppendBits(bits, codeword, 8);
|
||||
}
|
||||
|
||||
var bitIndex = 0;
|
||||
var upward = true;
|
||||
for (var right = Size - 1; right >= 1; right -= 2)
|
||||
{
|
||||
if (right == 6)
|
||||
{
|
||||
right--;
|
||||
}
|
||||
|
||||
for (var i = 0; i < Size; i++)
|
||||
{
|
||||
var y = upward ? Size - 1 - i : i;
|
||||
for (var j = 0; j < 2; j++)
|
||||
{
|
||||
var x = right - j;
|
||||
if (reserved[y, x])
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var bit = bitIndex < bits.Count && bits[bitIndex++];
|
||||
if ((x + y) % 2 == 0)
|
||||
{
|
||||
bit = !bit;
|
||||
}
|
||||
|
||||
modules[y, x] = bit;
|
||||
}
|
||||
}
|
||||
|
||||
upward = !upward;
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddFormatBits(bool[,] modules)
|
||||
{
|
||||
var format = GetFormatBits();
|
||||
|
||||
for (var i = 0; i <= 5; i++)
|
||||
{
|
||||
modules[i, 8] = GetBit(format, i);
|
||||
}
|
||||
|
||||
modules[7, 8] = GetBit(format, 6);
|
||||
modules[8, 8] = GetBit(format, 7);
|
||||
modules[8, 7] = GetBit(format, 8);
|
||||
|
||||
for (var i = 9; i < 15; i++)
|
||||
{
|
||||
modules[8, 14 - i] = GetBit(format, i);
|
||||
}
|
||||
|
||||
for (var i = 0; i < 8; i++)
|
||||
{
|
||||
modules[8, Size - 1 - i] = GetBit(format, i);
|
||||
}
|
||||
|
||||
for (var i = 8; i < 15; i++)
|
||||
{
|
||||
modules[Size - 15 + i, 8] = GetBit(format, i);
|
||||
}
|
||||
|
||||
modules[Size - 8, 8] = true;
|
||||
}
|
||||
|
||||
private static int GetFormatBits()
|
||||
{
|
||||
const int errorCorrectionLevelBits = 1;
|
||||
const int mask = 0;
|
||||
var data = (errorCorrectionLevelBits << 3) | mask;
|
||||
var value = data << 10;
|
||||
const int generator = 0x537;
|
||||
|
||||
for (var i = 14; i >= 10; i--)
|
||||
{
|
||||
if (((value >> i) & 1) != 0)
|
||||
{
|
||||
value ^= generator << (i - 10);
|
||||
}
|
||||
}
|
||||
|
||||
return ((data << 10) | (value & 0x3FF)) ^ 0x5412;
|
||||
}
|
||||
|
||||
private static byte[] CreateErrorCorrectionCodewords(byte[] data)
|
||||
{
|
||||
var generator = CreateGeneratorPolynomial(ErrorCorrectionCodewordCount);
|
||||
var result = new byte[ErrorCorrectionCodewordCount];
|
||||
|
||||
foreach (var b in data)
|
||||
{
|
||||
var factor = b ^ result[0];
|
||||
Array.Copy(result, 1, result, 0, result.Length - 1);
|
||||
result[^1] = 0;
|
||||
|
||||
for (var i = 0; i < result.Length; i++)
|
||||
{
|
||||
result[i] ^= GaloisMultiply(generator[i + 1], factor);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static byte[] CreateGeneratorPolynomial(int degree)
|
||||
{
|
||||
var result = new List<byte> { 1 };
|
||||
for (var i = 0; i < degree; i++)
|
||||
{
|
||||
var next = new byte[result.Count + 1];
|
||||
for (var j = 0; j < result.Count; j++)
|
||||
{
|
||||
next[j] ^= result[j];
|
||||
next[j + 1] ^= GaloisMultiply(result[j], GaloisPower(i));
|
||||
}
|
||||
|
||||
result = next.ToList();
|
||||
}
|
||||
|
||||
return result.ToArray();
|
||||
}
|
||||
|
||||
private static byte GaloisPower(int exponent)
|
||||
{
|
||||
var value = 1;
|
||||
for (var i = 0; i < exponent; i++)
|
||||
{
|
||||
value <<= 1;
|
||||
if ((value & 0x100) != 0)
|
||||
{
|
||||
value ^= 0x11D;
|
||||
}
|
||||
}
|
||||
|
||||
return (byte)value;
|
||||
}
|
||||
|
||||
private static byte GaloisMultiply(int x, int y)
|
||||
{
|
||||
var result = 0;
|
||||
while (y != 0)
|
||||
{
|
||||
if ((y & 1) != 0)
|
||||
{
|
||||
result ^= x;
|
||||
}
|
||||
|
||||
x <<= 1;
|
||||
if ((x & 0x100) != 0)
|
||||
{
|
||||
x ^= 0x11D;
|
||||
}
|
||||
|
||||
y >>= 1;
|
||||
}
|
||||
|
||||
return (byte)result;
|
||||
}
|
||||
|
||||
private static void AppendBits(List<bool> bits, int value, int count)
|
||||
{
|
||||
for (var i = count - 1; i >= 0; i--)
|
||||
{
|
||||
bits.Add(((value >> i) & 1) != 0);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<byte> BitsToBytes(List<bool> bits)
|
||||
{
|
||||
var result = new List<byte>();
|
||||
for (var i = 0; i < bits.Count; i += 8)
|
||||
{
|
||||
var value = 0;
|
||||
for (var j = 0; j < 8; j++)
|
||||
{
|
||||
value = (value << 1) | (bits[i + j] ? 1 : 0);
|
||||
}
|
||||
|
||||
result.Add((byte)value);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static bool GetBit(int value, int index)
|
||||
{
|
||||
return ((value >> index) & 1) != 0;
|
||||
}
|
||||
|
||||
private static void FillModule(byte[] pixels, int imageSize, int moduleX, int moduleY)
|
||||
{
|
||||
var startX = moduleX * Scale;
|
||||
var startY = moduleY * Scale;
|
||||
|
||||
for (var y = 0; y < Scale; y++)
|
||||
{
|
||||
for (var x = 0; x < Scale; x++)
|
||||
{
|
||||
var index = ((startY + y) * imageSize + startX + x) * 4;
|
||||
pixels[index] = 0;
|
||||
pixels[index + 1] = 0;
|
||||
pixels[index + 2] = 0;
|
||||
pixels[index + 3] = 255;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] EncodePng(int width, int height, byte[] rgba)
|
||||
{
|
||||
using var output = new MemoryStream();
|
||||
WriteUInt(output, 0x89504E47);
|
||||
WriteUInt(output, 0x0D0A1A0A);
|
||||
|
||||
using (var ihdr = new MemoryStream())
|
||||
{
|
||||
WriteUInt(ihdr, (uint)width);
|
||||
WriteUInt(ihdr, (uint)height);
|
||||
ihdr.WriteByte(8);
|
||||
ihdr.WriteByte(6);
|
||||
ihdr.WriteByte(0);
|
||||
ihdr.WriteByte(0);
|
||||
ihdr.WriteByte(0);
|
||||
WriteChunk(output, "IHDR", ihdr.ToArray());
|
||||
}
|
||||
|
||||
var stride = width * 4;
|
||||
using (var raw = new MemoryStream())
|
||||
{
|
||||
for (var y = 0; y < height; y++)
|
||||
{
|
||||
raw.WriteByte(0);
|
||||
raw.Write(rgba, y * stride, stride);
|
||||
}
|
||||
|
||||
using var compressed = new MemoryStream();
|
||||
using (var zlib = new ZLibStream(compressed, CompressionLevel.SmallestSize, true))
|
||||
{
|
||||
raw.Position = 0;
|
||||
raw.CopyTo(zlib);
|
||||
}
|
||||
|
||||
WriteChunk(output, "IDAT", compressed.ToArray());
|
||||
}
|
||||
|
||||
WriteChunk(output, "IEND", []);
|
||||
return output.ToArray();
|
||||
}
|
||||
|
||||
private static void WriteChunk(Stream stream, string type, byte[] data)
|
||||
{
|
||||
WriteUInt(stream, (uint)data.Length);
|
||||
var typeBytes = Encoding.ASCII.GetBytes(type);
|
||||
stream.Write(typeBytes);
|
||||
stream.Write(data);
|
||||
WriteUInt(stream, Crc32(typeBytes.Concat(data).ToArray()));
|
||||
}
|
||||
|
||||
private static void WriteUInt(Stream stream, uint value)
|
||||
{
|
||||
stream.WriteByte((byte)(value >> 24));
|
||||
stream.WriteByte((byte)(value >> 16));
|
||||
stream.WriteByte((byte)(value >> 8));
|
||||
stream.WriteByte((byte)value);
|
||||
}
|
||||
|
||||
private static uint Crc32(byte[] bytes)
|
||||
{
|
||||
var crc = 0xFFFFFFFFu;
|
||||
foreach (var b in bytes)
|
||||
{
|
||||
crc ^= b;
|
||||
for (var i = 0; i < 8; i++)
|
||||
{
|
||||
crc = (crc & 1) == 1 ? (crc >> 1) ^ 0xEDB88320u : crc >> 1;
|
||||
}
|
||||
}
|
||||
|
||||
return ~crc;
|
||||
}
|
||||
}
|
||||
37
QYZH.InteractiveMagazine.Common/Helpers/RandomIdHelper.cs
Normal file
37
QYZH.InteractiveMagazine.Common/Helpers/RandomIdHelper.cs
Normal file
@ -0,0 +1,37 @@
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.Common.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// 随机ID帮助类
|
||||
/// </summary>
|
||||
public static class RandomIdHelper
|
||||
{
|
||||
private const long DefaultMinValue = 1_000_000_000_000L;
|
||||
private const long DefaultMaxValue = 9_000_000_000_000_000L;
|
||||
|
||||
/// <summary>
|
||||
/// 生成不可预测的正数长整型ID
|
||||
/// </summary>
|
||||
public static long GenerateLongId(long minValue = DefaultMinValue, long maxValue = DefaultMaxValue)
|
||||
{
|
||||
if (minValue <= 0 || minValue >= maxValue)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(minValue), "随机ID范围配置错误");
|
||||
}
|
||||
|
||||
var range = (ulong)(maxValue - minValue);
|
||||
var limit = ulong.MaxValue - (ulong.MaxValue % range);
|
||||
|
||||
Span<byte> bytes = stackalloc byte[sizeof(ulong)];
|
||||
while (true)
|
||||
{
|
||||
RandomNumberGenerator.Fill(bytes);
|
||||
var value = BitConverter.ToUInt64(bytes);
|
||||
if (value < limit)
|
||||
{
|
||||
return minValue + (long)(value % range);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,9 +1,5 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\QYZH.InteractiveMagazine.Models\QYZH.InteractiveMagazine.Models.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AutoMapper" Version="16.1.1" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
|
||||
|
||||
@ -1,64 +0,0 @@
|
||||
namespace QYZH.InteractiveMagazine.IService.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// 登录输入
|
||||
/// </summary>
|
||||
public class LoginInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 账号
|
||||
/// </summary>
|
||||
public string Account { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 密码
|
||||
/// </summary>
|
||||
public string Password { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册输入
|
||||
/// </summary>
|
||||
public class RegisterInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 账号
|
||||
/// </summary>
|
||||
public string Account { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 密码
|
||||
/// </summary>
|
||||
public string Password { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 用户名
|
||||
/// </summary>
|
||||
public string UserName { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 登录输出
|
||||
/// </summary>
|
||||
public class LoginOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// 访问令牌
|
||||
/// </summary>
|
||||
public string Token { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 用户ID
|
||||
/// </summary>
|
||||
public long UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户名
|
||||
/// </summary>
|
||||
public string UserName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 刷新令牌
|
||||
/// </summary>
|
||||
public string RefreshToken { get; set; } = string.Empty;
|
||||
}
|
||||
@ -1,38 +0,0 @@
|
||||
namespace QYZH.InteractiveMagazine.IService.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// 微信登录输出
|
||||
/// </summary>
|
||||
public class WeChatLoginOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// 访问令牌
|
||||
/// </summary>
|
||||
public string Token { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 用户ID
|
||||
/// </summary>
|
||||
public long UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户名
|
||||
/// </summary>
|
||||
public string UserName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 微信OpenId
|
||||
/// </summary>
|
||||
public string OpenId { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 微信手机号获取输出
|
||||
/// </summary>
|
||||
public class WeChatPhoneNumberOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// 手机号
|
||||
/// </summary>
|
||||
public string PhoneNumber { get; set; } = string.Empty;
|
||||
}
|
||||
16
QYZH.InteractiveMagazine.IService/IAdminAuthService.cs
Normal file
16
QYZH.InteractiveMagazine.IService/IAdminAuthService.cs
Normal file
@ -0,0 +1,16 @@
|
||||
|
||||
using QYZH.InteractiveMagazine.Models.Dto;
|
||||
using QYZH.InteractiveMagazine.Models.Entity;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.IService;
|
||||
|
||||
public interface IAdminAuthService : IBaseService<AdminUser>
|
||||
{
|
||||
Task<AdminLoginOutput> LoginAsync(AdminLoginInput input);
|
||||
|
||||
Task LogoutAsync(long userId);
|
||||
|
||||
Task<AdminUserInfoOutput> GetAdminInfoAsync(long userId);
|
||||
|
||||
Task ChangePasswordAsync(long userId, string oldPassword, string newPassword);
|
||||
}
|
||||
80
QYZH.InteractiveMagazine.IService/IAdminPermissionService.cs
Normal file
80
QYZH.InteractiveMagazine.IService/IAdminPermissionService.cs
Normal file
@ -0,0 +1,80 @@
|
||||
using QYZH.InteractiveMagazine.Models.Dto;
|
||||
using QYZH.InteractiveMagazine.Models.Entity;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.IService;
|
||||
|
||||
/// <summary>
|
||||
/// 后台权限管理服务接口
|
||||
/// </summary>
|
||||
public interface IAdminPermissionService : IBaseService<AdminRole>
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建菜单
|
||||
/// </summary>
|
||||
Task<AdminMenuOutput> CreateMenuAsync(AdminMenuInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 更新菜单
|
||||
/// </summary>
|
||||
Task<AdminMenuOutput> UpdateMenuAsync(long id, AdminMenuInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 删除菜单
|
||||
/// </summary>
|
||||
Task DeleteMenuAsync(long id);
|
||||
|
||||
/// <summary>
|
||||
/// 获取菜单树
|
||||
/// </summary>
|
||||
Task<List<AdminMenuOutput>> GetMenuTreeAsync(AdminMenuQueryInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 创建角色
|
||||
/// </summary>
|
||||
Task<AdminRoleOutput> CreateRoleAsync(AdminRoleInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 更新角色
|
||||
/// </summary>
|
||||
Task<AdminRoleOutput> UpdateRoleAsync(long id, AdminRoleInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 删除角色
|
||||
/// </summary>
|
||||
Task DeleteRoleAsync(long id);
|
||||
|
||||
/// <summary>
|
||||
/// 获取角色详情
|
||||
/// </summary>
|
||||
Task<AdminRoleOutput> GetRoleByIdAsync(long id);
|
||||
|
||||
/// <summary>
|
||||
/// 获取角色分页列表
|
||||
/// </summary>
|
||||
Task<PageListModel<AdminRoleOutput>> GetRoleListAsync(AdminRoleQueryInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 分配角色菜单
|
||||
/// </summary>
|
||||
Task AssignRoleMenusAsync(long roleId, AssignRoleMenusInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 分配管理员角色
|
||||
/// </summary>
|
||||
Task AssignAdminUserRolesAsync(long adminUserId, AssignAdminUserRolesInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 获取管理员菜单树
|
||||
/// </summary>
|
||||
Task<List<AdminMenuOutput>> GetAdminUserMenuTreeAsync(long adminUserId);
|
||||
|
||||
/// <summary>
|
||||
/// 获取管理员角色
|
||||
/// </summary>
|
||||
Task<List<AdminRoleSimpleOutput>> GetAdminUserRolesAsync(long adminUserId);
|
||||
|
||||
/// <summary>
|
||||
/// 获取管理员权限编码
|
||||
/// </summary>
|
||||
Task<List<string>> GetAdminUserPermissionCodesAsync(long adminUserId);
|
||||
}
|
||||
53
QYZH.InteractiveMagazine.IService/IAdminUserService.cs
Normal file
53
QYZH.InteractiveMagazine.IService/IAdminUserService.cs
Normal file
@ -0,0 +1,53 @@
|
||||
|
||||
using QYZH.InteractiveMagazine.Models.Dto;
|
||||
using QYZH.InteractiveMagazine.Models.Entity;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.IService;
|
||||
|
||||
/// <summary>
|
||||
/// 管理员用户服务接口
|
||||
/// </summary>
|
||||
public interface IAdminUserService : IBaseService<AdminUser>
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建管理员
|
||||
/// </summary>
|
||||
/// <param name="input">管理员输入</param>
|
||||
/// <returns>创建的管理员信息</returns>
|
||||
Task<AdminUserOutput> CreateAsync(AdminUserInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 更新管理员
|
||||
/// </summary>
|
||||
/// <param name="id">管理员ID</param>
|
||||
/// <param name="input">管理员输入</param>
|
||||
/// <returns>更新后的管理员信息</returns>
|
||||
Task<AdminUserOutput> UpdateAsync(long id, AdminUserInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 删除管理员(软删除)
|
||||
/// </summary>
|
||||
/// <param name="id">管理员ID</param>
|
||||
Task DeleteAsync(long id);
|
||||
|
||||
/// <summary>
|
||||
/// 根据ID获取管理员
|
||||
/// </summary>
|
||||
/// <param name="id">管理员ID</param>
|
||||
/// <returns>管理员信息</returns>
|
||||
Task<AdminUserOutput> GetByIdAsync(long id);
|
||||
|
||||
/// <summary>
|
||||
/// 分页查询管理员列表
|
||||
/// </summary>
|
||||
/// <param name="input">查询条件</param>
|
||||
/// <returns>分页结果</returns>
|
||||
Task<PageListModel<AdminUserOutput>> GetListAsync(AdminUserQueryInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 切换管理员状态(激活/冻结)
|
||||
/// </summary>
|
||||
/// <param name="id">管理员ID</param>
|
||||
/// <param name="status">目标状态:1=激活,0=冻结</param>
|
||||
Task ToggleStatusAsync(long id);
|
||||
}
|
||||
57
QYZH.InteractiveMagazine.IService/IAiBasePromptService.cs
Normal file
57
QYZH.InteractiveMagazine.IService/IAiBasePromptService.cs
Normal file
@ -0,0 +1,57 @@
|
||||
using QYZH.InteractiveMagazine.Models.Dto;
|
||||
using QYZH.InteractiveMagazine.Models.Entity;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.IService;
|
||||
|
||||
/// <summary>
|
||||
/// AIPrompt配置服务接口
|
||||
/// </summary>
|
||||
public interface IAiBasePromptService : IBaseService<AiBasePrompt>
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建Prompt配置
|
||||
/// </summary>
|
||||
/// <param name="input">Prompt配置输入</param>
|
||||
/// <returns>创建的Prompt配置信息</returns>
|
||||
Task<AiBasePromptOutput> CreateAsync(AiBasePromptInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 更新Prompt配置
|
||||
/// </summary>
|
||||
/// <param name="id">Prompt配置ID</param>
|
||||
/// <param name="input">Prompt配置输入</param>
|
||||
/// <returns>更新后的Prompt配置信息</returns>
|
||||
Task<AiBasePromptOutput> UpdateAsync(long id, AiBasePromptInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 删除Prompt配置(软删除)
|
||||
/// </summary>
|
||||
/// <param name="id">Prompt配置ID</param>
|
||||
Task DeleteAsync(long id);
|
||||
|
||||
/// <summary>
|
||||
/// 根据ID获取Prompt配置
|
||||
/// </summary>
|
||||
/// <param name="id">Prompt配置ID</param>
|
||||
/// <returns>Prompt配置信息</returns>
|
||||
Task<AiBasePromptOutput> GetByIdAsync(long id);
|
||||
|
||||
/// <summary>
|
||||
/// 分页查询Prompt配置列表
|
||||
/// </summary>
|
||||
/// <param name="input">查询条件</param>
|
||||
/// <returns>分页结果</returns>
|
||||
Task<PageListModel<AiBasePromptOutput>> GetListAsync(AiBasePromptQueryInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 获取全部Prompt配置(无分页)
|
||||
/// </summary>
|
||||
/// <returns>全部Prompt配置列表</returns>
|
||||
Task<List<AiBasePromptOutput>> GetAllAsync();
|
||||
|
||||
/// <summary>
|
||||
/// 切换Prompt启用/禁用状态
|
||||
/// </summary>
|
||||
/// <param name="id">Prompt配置ID</param>
|
||||
Task ToggleStatusAsync(long id);
|
||||
}
|
||||
17
QYZH.InteractiveMagazine.IService/IAiChatService.cs
Normal file
17
QYZH.InteractiveMagazine.IService/IAiChatService.cs
Normal file
@ -0,0 +1,17 @@
|
||||
using QYZH.InteractiveMagazine.Models.Dto;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.IService;
|
||||
|
||||
/// <summary>
|
||||
/// AI聊天服务接口
|
||||
/// </summary>
|
||||
public interface IAiChatService
|
||||
{
|
||||
/// <summary>
|
||||
/// 流式AI对话,逐块返回AI生成的内容
|
||||
/// </summary>
|
||||
/// <param name="input">聊天输入</param>
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
/// <returns>AI生成的内容片段流</returns>
|
||||
IAsyncEnumerable<string> ChatAsync(AiChatInput input, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@ -1,31 +0,0 @@
|
||||
using QYZH.InteractiveMagazine.IService.Dto;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.IService;
|
||||
|
||||
/// <summary>
|
||||
/// 认证服务接口
|
||||
/// </summary>
|
||||
public interface IAuthService
|
||||
{
|
||||
/// <summary>
|
||||
/// 用户登录
|
||||
/// </summary>
|
||||
/// <param name="account">账号</param>
|
||||
/// <param name="password">密码</param>
|
||||
/// <returns>登录结果</returns>
|
||||
Task<LoginOutput> LoginAsync(string account, string password);
|
||||
|
||||
/// <summary>
|
||||
/// 用户注册
|
||||
/// </summary>
|
||||
/// <param name="input">注册输入参数</param>
|
||||
/// <returns>是否成功</returns>
|
||||
Task<bool> RegisterAsync(RegisterInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 刷新令牌
|
||||
/// </summary>
|
||||
/// <param name="refreshToken">刷新令牌</param>
|
||||
/// <returns>新的登录结果</returns>
|
||||
Task<LoginOutput> RefreshTokenAsync(string refreshToken);
|
||||
}
|
||||
61
QYZH.InteractiveMagazine.IService/IBannerService.cs
Normal file
61
QYZH.InteractiveMagazine.IService/IBannerService.cs
Normal file
@ -0,0 +1,61 @@
|
||||
using QYZH.InteractiveMagazine.Models.Dto;
|
||||
using QYZH.InteractiveMagazine.Models.Dto.Banner;
|
||||
using QYZH.InteractiveMagazine.Models.Entity;
|
||||
using QYZH.InteractiveMagazine.Models.Enum;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.IService;
|
||||
|
||||
/// <summary>
|
||||
/// 轮播图服务接口
|
||||
/// </summary>
|
||||
public interface IBannerService : IBaseService<Banner>
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建轮播图
|
||||
/// </summary>
|
||||
/// <param name="input">轮播图信息</param>
|
||||
/// <returns>创建后的轮播图信息</returns>
|
||||
Task<BannerOutput> CreateAsync(BannerInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 更新轮播图
|
||||
/// </summary>
|
||||
/// <param name="id">轮播图ID</param>
|
||||
/// <param name="input">轮播图信息</param>
|
||||
/// <returns>更新后的轮播图信息</returns>
|
||||
Task<BannerOutput> UpdateAsync(long id, BannerInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 删除轮播图
|
||||
/// </summary>
|
||||
/// <param name="id">轮播图ID</param>
|
||||
Task DeleteAsync(long id);
|
||||
|
||||
/// <summary>
|
||||
/// 根据ID获取轮播图
|
||||
/// </summary>
|
||||
/// <param name="id">轮播图ID</param>
|
||||
/// <returns>轮播图信息</returns>
|
||||
Task<BannerOutput> GetByIdAsync(long id);
|
||||
|
||||
/// <summary>
|
||||
/// 分页查询轮播图列表
|
||||
/// </summary>
|
||||
/// <param name="input">查询条件</param>
|
||||
/// <returns>分页结果</returns>
|
||||
Task<PageListModel<BannerOutput>> GetListAsync(BannerQueryInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 更新轮播图启用/禁用状态
|
||||
/// </summary>
|
||||
/// <param name="id">轮播图ID</param>
|
||||
/// <returns>操作结果</returns>
|
||||
Task<bool> UpdateStatusAsync(long id);
|
||||
|
||||
/// <summary>
|
||||
/// 获取启用轮播图列表
|
||||
/// </summary>
|
||||
/// <param name="position">展示位置</param>
|
||||
/// <returns>轮播图列表</returns>
|
||||
Task<List<BannerOutput>> GetEnabledListAsync(BannerPositionEnum position);
|
||||
}
|
||||
@ -1,51 +1,39 @@
|
||||
using QYZH.InteractiveMagazine.Models.Dto;
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.IService;
|
||||
|
||||
/// <summary>
|
||||
/// 基础服务接口
|
||||
/// 基础服务定义
|
||||
/// </summary>
|
||||
/// <typeparam name="T">实体类型</typeparam>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
public interface IBaseService<T> where T : class, new()
|
||||
{
|
||||
/// <summary>
|
||||
/// 根据ID获取实体
|
||||
/// </summary>
|
||||
/// <param name="id">实体ID</param>
|
||||
/// <returns>实体对象</returns>
|
||||
Task<T?> GetByIdAsync(long id);
|
||||
Task<T> GetFirstAsync(Expression<Func<T, bool>> whereExpression);
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有实体列表
|
||||
/// 根据条件表达式查询单条数据
|
||||
/// </summary>
|
||||
/// <returns>实体列表</returns>
|
||||
Task<List<T>> GetListAsync();
|
||||
/// <param name="expression">表达式</param>
|
||||
/// <returns>泛型实体</returns>
|
||||
Task<R> GetByIdAsync<R>(Expression<Func<T, bool>> expression) where R : class;
|
||||
|
||||
/// <summary>
|
||||
/// 获取分页列表
|
||||
/// </summary>
|
||||
/// <param name="pageQuery">分页查询参数</param>
|
||||
/// <returns>分页数据</returns>
|
||||
Task<PageListModel<T>> GetPageListAsync(PageQueryModel pageQuery);
|
||||
Task<R> GetByExpressionAsync<R>(Expression<Func<T, bool>> expression) where R : class;
|
||||
|
||||
/// <summary>
|
||||
/// 新增实体
|
||||
/// </summary>
|
||||
/// <param name="entity">实体对象</param>
|
||||
/// <returns>是否成功</returns>
|
||||
Task<bool> InsertAsync(T entity);
|
||||
Task<List<T2>> GetListByExpression<T2>(Expression<Func<T, bool>> expression) where T2 : class;
|
||||
|
||||
/// <summary>
|
||||
/// 更新实体
|
||||
/// </summary>
|
||||
/// <param name="entity">实体对象</param>
|
||||
/// <returns>是否成功</returns>
|
||||
Task<bool> UpdateAsync(T entity);
|
||||
Task<bool> UpdateAsync(T updateObj);
|
||||
|
||||
/// <summary>
|
||||
/// 根据ID删除实体
|
||||
/// </summary>
|
||||
/// <param name="id">实体ID</param>
|
||||
/// <returns>是否成功</returns>
|
||||
Task<bool> DeleteByIdAsync(long id);
|
||||
///// <summary>
|
||||
///// 根据指定条件更新指定列 eg:Update(new SysUser(){ Status = 1 }, it => new { it.Status }, f => f.Userid == 1));
|
||||
///// 只更新Status列,条件是包含
|
||||
///// </summary>
|
||||
///// <param name="entity">实体类</param>
|
||||
///// <param name="expression">要更新列的表达式</param>
|
||||
///// <param name="where">where表达式</param>
|
||||
///// <returns></returns>
|
||||
//Task<bool> UpdateAsync(T entity, Expression<Func<T, object>> expression, Expression<Func<T, bool>> where);
|
||||
|
||||
Task<bool> UpdateAsync(Expression<Func<T, T>> columns, Expression<Func<T, bool>> whereExpression);
|
||||
|
||||
Task<bool> UpdateAsync(Expression<Func<T, bool>> columns, Expression<Func<T, bool>> where);
|
||||
}
|
||||
|
||||
52
QYZH.InteractiveMagazine.IService/ICheckInConfigService.cs
Normal file
52
QYZH.InteractiveMagazine.IService/ICheckInConfigService.cs
Normal file
@ -0,0 +1,52 @@
|
||||
using QYZH.InteractiveMagazine.Models.Dto;
|
||||
using QYZH.InteractiveMagazine.Models.Dto.CheckIn;
|
||||
using QYZH.InteractiveMagazine.Models.Entity;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.IService;
|
||||
|
||||
/// <summary>
|
||||
/// 签到配置服务接口
|
||||
/// </summary>
|
||||
public interface ICheckInConfigService : IBaseService<CheckInConfig>
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建签到配置
|
||||
/// </summary>
|
||||
/// <param name="input">签到配置输入</param>
|
||||
/// <returns>创建的签到配置信息</returns>
|
||||
Task<CheckInConfigOutput> CreateAsync(CheckInConfigInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 更新签到配置
|
||||
/// </summary>
|
||||
/// <param name="id">签到配置ID</param>
|
||||
/// <param name="input">签到配置输入</param>
|
||||
/// <returns>更新后的签到配置信息</returns>
|
||||
Task<CheckInConfigOutput> UpdateAsync(long id, CheckInConfigInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 删除签到配置(软删除)
|
||||
/// </summary>
|
||||
/// <param name="id">签到配置ID</param>
|
||||
Task DeleteAsync(long id);
|
||||
|
||||
/// <summary>
|
||||
/// 根据ID获取签到配置
|
||||
/// </summary>
|
||||
/// <param name="id">签到配置ID</param>
|
||||
/// <returns>签到配置信息</returns>
|
||||
Task<CheckInConfigOutput> GetByIdAsync(long id);
|
||||
|
||||
/// <summary>
|
||||
/// 分页查询签到配置列表
|
||||
/// </summary>
|
||||
/// <param name="input">查询条件</param>
|
||||
/// <returns>分页结果</returns>
|
||||
Task<PageListModel<CheckInConfigOutput>> GetListAsync(CheckInConfigQueryInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 更新签到配置启用/禁用状态
|
||||
/// </summary>
|
||||
/// <param name="id">签到配置ID</param>
|
||||
Task<bool> UpdateStatusAsync(long id);
|
||||
}
|
||||
40
QYZH.InteractiveMagazine.IService/ICheckInService.cs
Normal file
40
QYZH.InteractiveMagazine.IService/ICheckInService.cs
Normal file
@ -0,0 +1,40 @@
|
||||
using QYZH.InteractiveMagazine.Models.Dto.CheckIn;
|
||||
using QYZH.InteractiveMagazine.Models.Entity;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.IService;
|
||||
|
||||
/// <summary>
|
||||
/// 签到服务接口
|
||||
/// </summary>
|
||||
public interface ICheckInService : IBaseService<CheckInRecord>
|
||||
{
|
||||
/// <summary>
|
||||
/// 用户签到
|
||||
/// </summary>
|
||||
/// <param name="userId">用户Id</param>
|
||||
/// <returns>签到结果</returns>
|
||||
Task<CheckInOutput> CheckInAsync(long userId);
|
||||
|
||||
/// <summary>
|
||||
/// 获取用户签到信息(今日状态 + 连续天数 + 最近记录)
|
||||
/// </summary>
|
||||
/// <param name="userId">用户Id</param>
|
||||
/// <returns>签到信息</returns>
|
||||
Task<CheckInInfoOutput> GetCheckInInfoAsync(long userId);
|
||||
|
||||
/// <summary>
|
||||
/// 补签(消耗补签卡,补签历史漏签日期)
|
||||
/// </summary>
|
||||
/// <param name="userId">用户Id</param>
|
||||
/// <param name="targetDate">补签目标日期</param>
|
||||
/// <returns>补签结果</returns>
|
||||
Task<CheckInOutput> MakeUpCheckInAsync(long userId, DateTime targetDate);
|
||||
|
||||
/// <summary>
|
||||
/// 获取用户可补签的日期列表(历史漏签日期)
|
||||
/// </summary>
|
||||
/// <param name="userId">用户Id</param>
|
||||
/// <param name="days">往前查看天数(默认30天)</param>
|
||||
/// <returns>漏签日期列表</returns>
|
||||
Task<List<DateTime>> GetMissedDatesAsync(long userId, int days = 30);
|
||||
}
|
||||
@ -0,0 +1,50 @@
|
||||
using QYZH.InteractiveMagazine.Models.Dto;
|
||||
using QYZH.InteractiveMagazine.Models.Entity;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.IService;
|
||||
|
||||
/// <summary>
|
||||
/// 社区消息后台管理服务接口
|
||||
/// </summary>
|
||||
public interface ICommunityMessageService : IBaseService<CommunityMessage>
|
||||
{
|
||||
/// <summary>
|
||||
/// 分页查询消息列表
|
||||
/// </summary>
|
||||
Task<PageListModel<AdminMessageDetailOutput>> GetListAsync(AdminMessageQueryInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 查看消息详情
|
||||
/// </summary>
|
||||
Task<AdminMessageDetailOutput> GetDetailAsync(long id);
|
||||
|
||||
/// <summary>
|
||||
/// 软删除消息
|
||||
/// </summary>
|
||||
Task DeleteAsync(long id);
|
||||
|
||||
/// <summary>
|
||||
/// 冻结/解冻消息
|
||||
/// </summary>
|
||||
/// <param name="id">消息ID</param>
|
||||
/// <param name="status">状态: 1=解冻(通过), 2=冻结</param>
|
||||
Task FreezeAsync(long id, int status);
|
||||
|
||||
/// <summary>
|
||||
/// 设置/取消精选
|
||||
/// </summary>
|
||||
/// <param name="id">消息ID</param>
|
||||
/// <param name="isFeatured">0=取消, 1=精选</param>
|
||||
Task SetFeaturedAsync(long id, int isFeatured);
|
||||
|
||||
/// <summary>
|
||||
/// 设置排序权重
|
||||
/// </summary>
|
||||
Task SetSortOrderAsync(long id, int sortOrder);
|
||||
|
||||
/// <summary>
|
||||
/// 批量发布消息(IsActive false => true)
|
||||
/// </summary>
|
||||
/// <param name="ids">消息ID列表</param>
|
||||
Task BatchPublishAsync(List<long> ids);
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
using QYZH.InteractiveMagazine.Models.Dto;
|
||||
using QYZH.InteractiveMagazine.Models.Dto.Compensation;
|
||||
using QYZH.InteractiveMagazine.Models.Entity;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.IService;
|
||||
|
||||
/// <summary>
|
||||
/// 补偿任务管理服务接口
|
||||
/// </summary>
|
||||
public interface ICompensationManageService : IBaseService<CompensationTask>
|
||||
{
|
||||
/// <summary>
|
||||
/// 分页查询补偿任务
|
||||
/// </summary>
|
||||
/// <param name="input">查询参数</param>
|
||||
/// <returns>分页结果</returns>
|
||||
Task<PageListModel<CompensationManageOutput>> GetListAsync(CompensationTaskQueryInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 手动重试补偿任务(将状态改为 Pending,重置 RetryCount)
|
||||
/// </summary>
|
||||
/// <param name="taskId">任务Id</param>
|
||||
/// <param name="operatorId">操作人Id</param>
|
||||
/// <param name="operatorName">操作人用户名</param>
|
||||
/// <param name="input">重试参数</param>
|
||||
/// <param name="ipAddress">IP地址</param>
|
||||
Task RetryAsync(long taskId, long operatorId, string operatorName, CompensationRetryInput input, string? ipAddress = null);
|
||||
|
||||
/// <summary>
|
||||
/// 标记补偿任务为已解决
|
||||
/// </summary>
|
||||
/// <param name="taskId">任务Id</param>
|
||||
/// <param name="operatorId">操作人Id</param>
|
||||
/// <param name="operatorName">操作人用户名</param>
|
||||
/// <param name="input">解决参数</param>
|
||||
/// <param name="ipAddress">IP地址</param>
|
||||
Task ResolveAsync(long taskId, long operatorId, string operatorName, CompensationResolveInput input, string? ipAddress = null);
|
||||
|
||||
/// <summary>
|
||||
/// 获取补偿任务详情
|
||||
/// </summary>
|
||||
/// <param name="taskId">任务Id</param>
|
||||
/// <returns>任务详情</returns>
|
||||
Task<CompensationManageDetailOutput> GetDetailAsync(long taskId);
|
||||
}
|
||||
@ -0,0 +1,46 @@
|
||||
using QYZH.InteractiveMagazine.Models.Dto.Compensation;
|
||||
using QYZH.InteractiveMagazine.Models.Entity;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.IService;
|
||||
|
||||
/// <summary>
|
||||
/// 补偿任务服务接口(仅负责记录和状态管理,处理逻辑由外部项目实现)
|
||||
/// </summary>
|
||||
public interface ICompensationTaskService : IBaseService<CompensationTask>
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建补偿任务(公共调用入口)
|
||||
/// 当业务操作部分成功但某个后续步骤失败时调用,记录失败操作以便后续重试
|
||||
/// </summary>
|
||||
/// <param name="input">补偿任务创建参数</param>
|
||||
/// <returns>补偿任务Id</returns>
|
||||
Task<long> CreateTaskAsync(CreateCompensationTaskInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 获取待处理的补偿任务列表(Pending + 已过 ScheduledAt 的任务)
|
||||
/// </summary>
|
||||
/// <param name="limit">每次获取数量上限</param>
|
||||
/// <returns>待处理任务列表</returns>
|
||||
Task<List<CompensationTaskOutput>> GetPendingTasksAsync(int limit = 50);
|
||||
|
||||
/// <summary>
|
||||
/// 按条件查询补偿任务(供外部项目按类型/状态/来源拉取)
|
||||
/// </summary>
|
||||
/// <param name="input">查询条件</param>
|
||||
/// <returns>任务列表</returns>
|
||||
Task<List<CompensationTaskOutput>> GetTasksAsync(GetCompensationTasksInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 更新补偿任务状态(供外部处理项目回调更新处理结果)
|
||||
/// </summary>
|
||||
/// <param name="taskId">任务Id</param>
|
||||
/// <param name="input">状态更新参数</param>
|
||||
Task UpdateTaskStatusAsync(long taskId, UpdateCompensationStatusInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 取消补偿任务
|
||||
/// </summary>
|
||||
/// <param name="taskId">任务Id</param>
|
||||
/// <param name="reason">取消原因</param>
|
||||
Task CancelTaskAsync(long taskId, string reason);
|
||||
}
|
||||
45
QYZH.InteractiveMagazine.IService/IJournalCatalogService.cs
Normal file
45
QYZH.InteractiveMagazine.IService/IJournalCatalogService.cs
Normal file
@ -0,0 +1,45 @@
|
||||
using QYZH.InteractiveMagazine.Models;
|
||||
using QYZH.InteractiveMagazine.Models.Base;
|
||||
using QYZH.InteractiveMagazine.Models.Dto.Journal;
|
||||
using QYZH.InteractiveMagazine.Models.Entity;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.IService;
|
||||
|
||||
public interface IJournalCatalogService : IBaseService<JournalCatalog>
|
||||
{
|
||||
Task<List<JournalCatalog>> DetailAsync(long JournalId);
|
||||
|
||||
Task<long> InsertAsync(JournalCatalogInput input);
|
||||
|
||||
Task<long> ImportAsync(JournalImportDto input);
|
||||
|
||||
Task<bool> UpdateAsync(JournalCatalogUpdateInput input);
|
||||
|
||||
Task<bool> DeleteAsync(long id);
|
||||
|
||||
Task<bool> MoveAsync(MoveInput input);
|
||||
|
||||
//Task<bool> CopyAsync(CopyInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 获取书分类及页码
|
||||
/// </summary>
|
||||
/// <param name="JournalId"></param>
|
||||
/// <returns></returns>
|
||||
Task<List<JournalCatalogTreeListDto>> GetJournalCatalogListAsync(long JournalId);
|
||||
|
||||
/// <summary>
|
||||
/// 获取书分类及页码
|
||||
/// </summary>
|
||||
/// <param name="JournalId"></param>
|
||||
/// <returns></returns>
|
||||
Task<List<IcrJournalCatalogTreeDto>> GetJournalCataloTreeAsync(long JournalId);
|
||||
|
||||
/// <summary>
|
||||
/// 导入书籍目录
|
||||
/// </summary>
|
||||
/// <param name="JournalId"></param>
|
||||
/// <param name="dtos"></param>
|
||||
/// <returns></returns>
|
||||
Task<bool> ImportCatalogAsync(long JournalId, List<JournalCatalogTreeListDto> dtos);
|
||||
}
|
||||
30
QYZH.InteractiveMagazine.IService/IJournalPageService.cs
Normal file
30
QYZH.InteractiveMagazine.IService/IJournalPageService.cs
Normal file
@ -0,0 +1,30 @@
|
||||
|
||||
|
||||
using QYZH.InteractiveMagazine.Models.Dto.Journal;
|
||||
using QYZH.InteractiveMagazine.Models.Entity;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.IService;
|
||||
|
||||
public interface IJournalPageService: IBaseService<JournalPage>
|
||||
{
|
||||
Task<long> InsertAsync(JournalAddV2Input input);
|
||||
|
||||
Task<bool> UpdateAsync(PageLayoutInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 修改书页的点阵码
|
||||
/// </summary>
|
||||
/// <param name="JournalId"></param>
|
||||
/// <returns></returns>
|
||||
Task<bool> UpdatePageNoAsync(long JournalId);
|
||||
|
||||
|
||||
Task<JournalPageV2Output> DetailAsync(long id);
|
||||
Task<bool> DeleteAsync(long id);
|
||||
/// <summary>
|
||||
/// 回调接口-自动铺码, 书页铺码后回调接口,更新书页的点阵码
|
||||
/// </summary>
|
||||
/// <param name="bookPagePrintDto"></param>
|
||||
/// <returns></returns>
|
||||
Task<bool> CallbackPageUpdatePageNo(JournalPagePrintDto journalPagePrintDto);
|
||||
}
|
||||
20
QYZH.InteractiveMagazine.IService/IJournalPageTaskService.cs
Normal file
20
QYZH.InteractiveMagazine.IService/IJournalPageTaskService.cs
Normal file
@ -0,0 +1,20 @@
|
||||
|
||||
|
||||
using QYZH.InteractiveMagazine.Models.Dto.Journal;
|
||||
using QYZH.InteractiveMagazine.Models.Entity;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.IService;
|
||||
|
||||
public interface IJournalPageTaskService : IBaseService<JournalPageTask>
|
||||
{
|
||||
Task<long?> InsertAsync(JournalPageTaskAddInput input);
|
||||
Task<bool> UpdateAsync(JournalPageTaskUpdateInput input);
|
||||
Task<JournalPageTaskOutput> DetailAsync(long id);
|
||||
|
||||
Task<bool> DeleteAsync(long id);
|
||||
Task<bool> ComplementAsync(JournalPageTaskComplementInput input);
|
||||
|
||||
Task<long> AddAnswerAsync(JournalTaskAnswerAddInput input);
|
||||
Task<bool> UpdateAnswerAsync(JournalTaskAnswerUpdateInput input);
|
||||
Task<bool> DeleteAnswerAsync(long id);
|
||||
}
|
||||
57
QYZH.InteractiveMagazine.IService/IJournalService.cs
Normal file
57
QYZH.InteractiveMagazine.IService/IJournalService.cs
Normal file
@ -0,0 +1,57 @@
|
||||
|
||||
using QYZH.InteractiveMagazine.Models.Dto.Journal;
|
||||
using QYZH.InteractiveMagazine.Models.Dto.DotMatrix;
|
||||
using QYZH.InteractiveMagazine.Models.Enum;
|
||||
using QYZH.InteractiveMagazine.Models.Dto;
|
||||
using QYZH.InteractiveMagazine.Models.Entity;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.IService;
|
||||
|
||||
public interface IJournalService : IBaseService<Journal>
|
||||
{
|
||||
/// <summary>
|
||||
/// 查询List
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
Task<List<JournalDto>> GetListAsync(JournalQueryDto dto);
|
||||
|
||||
/// <summary>
|
||||
/// 分页查询
|
||||
/// </summary>
|
||||
/// <param name="search"></param>
|
||||
/// <returns></returns>
|
||||
Task<PageListModel<JournalDto>> GetPageListAsync(PageQueryModel<JournalQueryDto> search);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 创建杂志
|
||||
/// </summary>
|
||||
/// <param name="input">杂志信息</param>
|
||||
/// <returns>新杂志ID</returns>
|
||||
Task<BaseResponse<long>> AddAsync(JournalAddDto input);
|
||||
|
||||
/// <summary>
|
||||
/// 编辑
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
Task<BaseResponse<bool>> EditAsync(JournalEditDto input);
|
||||
|
||||
Task<List<JournalTaskOutput>> Tasks(long id);
|
||||
|
||||
Task<JournalDto> DetailAsync(long id);
|
||||
|
||||
Task<bool> DeleteAsync(List<long> id);
|
||||
|
||||
|
||||
Task<bool> StartPageAsync(long Id, int Index);
|
||||
|
||||
Task<bool> StatusAsync(long id, JournalStatusEnum status);
|
||||
|
||||
Task<bool> PublishAsync(long id);
|
||||
|
||||
Task<DotMatrixOutput> PrintCodeAsync(long id);
|
||||
|
||||
Task<bool> ResultReportAsync(DotMatrixNoteJournalReportInput input);
|
||||
}
|
||||
59
QYZH.InteractiveMagazine.IService/IMaterialService.cs
Normal file
59
QYZH.InteractiveMagazine.IService/IMaterialService.cs
Normal file
@ -0,0 +1,59 @@
|
||||
using QYZH.InteractiveMagazine.Models.Dto;
|
||||
using QYZH.InteractiveMagazine.Models.Dto.Material;
|
||||
using MaterialEntity = QYZH.InteractiveMagazine.Models.Entity.Material;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.IService;
|
||||
|
||||
/// <summary>
|
||||
/// 资料服务接口
|
||||
/// </summary>
|
||||
public interface IMaterialService : IBaseService<MaterialEntity>
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建资料
|
||||
/// </summary>
|
||||
/// <param name="input">资料信息</param>
|
||||
/// <returns>创建后的资料信息</returns>
|
||||
Task<MaterialOutput> CreateAsync(MaterialInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 更新资料
|
||||
/// </summary>
|
||||
/// <param name="id">资料ID</param>
|
||||
/// <param name="input">资料信息</param>
|
||||
/// <returns>更新后的资料信息</returns>
|
||||
Task<MaterialOutput> UpdateAsync(long id, MaterialInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 删除资料
|
||||
/// </summary>
|
||||
/// <param name="id">资料ID</param>
|
||||
Task DeleteAsync(long id);
|
||||
|
||||
/// <summary>
|
||||
/// 根据ID获取资料
|
||||
/// </summary>
|
||||
/// <param name="id">资料ID</param>
|
||||
/// <returns>资料信息</returns>
|
||||
Task<MaterialOutput> GetByIdAsync(long id);
|
||||
|
||||
/// <summary>
|
||||
/// 分页查询资料列表
|
||||
/// </summary>
|
||||
/// <param name="input">查询条件</param>
|
||||
/// <returns>分页结果</returns>
|
||||
Task<PageListModel<MaterialOutput>> GetListAsync(MaterialQueryInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 更新资料启用/禁用状态
|
||||
/// </summary>
|
||||
/// <param name="id">资料ID</param>
|
||||
/// <returns>操作结果</returns>
|
||||
Task<bool> UpdateStatusAsync(long id);
|
||||
|
||||
/// <summary>
|
||||
/// 获取小程序可见资料列表
|
||||
/// </summary>
|
||||
/// <returns>资料列表</returns>
|
||||
Task<List<MaterialOutput>> GetEnabledListAsync();
|
||||
}
|
||||
91
QYZH.InteractiveMagazine.IService/IMedalService.cs
Normal file
91
QYZH.InteractiveMagazine.IService/IMedalService.cs
Normal file
@ -0,0 +1,91 @@
|
||||
using QYZH.InteractiveMagazine.Models.Dto;
|
||||
using QYZH.InteractiveMagazine.Models.Entity;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.IService;
|
||||
|
||||
/// <summary>
|
||||
/// 勋章服务接口
|
||||
/// </summary>
|
||||
public interface IMedalService : IBaseService<Medal>
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建勋章
|
||||
/// </summary>
|
||||
/// <param name="input">勋章输入</param>
|
||||
/// <returns>创建的勋章信息</returns>
|
||||
Task<MedalOutput> CreateAsync(MedalInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 更新勋章
|
||||
/// </summary>
|
||||
/// <param name="id">勋章ID</param>
|
||||
/// <param name="input">勋章输入</param>
|
||||
/// <returns>更新后的勋章信息</returns>
|
||||
Task<MedalOutput> UpdateAsync(long id, MedalInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 删除勋章(软删除)
|
||||
/// </summary>
|
||||
/// <param name="id">勋章ID</param>
|
||||
Task DeleteAsync(long id);
|
||||
|
||||
/// <summary>
|
||||
/// 根据ID获取勋章
|
||||
/// </summary>
|
||||
/// <param name="id">勋章ID</param>
|
||||
/// <returns>勋章信息</returns>
|
||||
Task<MedalOutput> GetByIdAsync(long id);
|
||||
|
||||
/// <summary>
|
||||
/// 分页查询勋章列表
|
||||
/// </summary>
|
||||
/// <param name="input">查询条件</param>
|
||||
/// <returns>分页结果</returns>
|
||||
Task<PageListModel<MedalOutput>> GetListAsync(MedalQueryInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 更新勋章启用/禁用状态
|
||||
/// </summary>
|
||||
/// <param name="id">勋章ID</param>
|
||||
/// <param name="status">状态: 0=禁用, 1=启用</param>
|
||||
Task<bool> UpdateStatusAsync(long id);
|
||||
|
||||
/// <summary>
|
||||
/// 获取勋章的规则列表
|
||||
/// </summary>
|
||||
/// <param name="medalId">勋章ID</param>
|
||||
/// <returns>规则列表</returns>
|
||||
Task<List<MedalRuleOutput>> GetRulesByMedalIdAsync(long medalId);
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有启用勋章列表(含当前用户拥有状态)
|
||||
/// </summary>
|
||||
/// <param name="userId">当前用户ID</param>
|
||||
/// <returns>勋章列表</returns>
|
||||
Task<List<WxMedalListOutput>> GetAllMedalsAsync(long userId);
|
||||
|
||||
/// <summary>
|
||||
/// 获取用户已拥有的勋章列表
|
||||
/// </summary>
|
||||
/// <param name="userId">用户ID</param>
|
||||
/// <returns>用户勋章列表</returns>
|
||||
Task<List<WxUserMedalOutput>> GetUserMedalsAsync(long userId);
|
||||
|
||||
/// <summary>
|
||||
/// 激活/获得勋章
|
||||
/// </summary>
|
||||
/// <param name="userId">用户ID</param>
|
||||
/// <param name="input">激活输入</param>
|
||||
Task ActivateMedalAsync(long userId, WxMedalActivateInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 获取规则配置中可用的目标表列表
|
||||
/// </summary>
|
||||
Task<List<MedalRuleTableOptionOutput>> GetAvailableTablesAsync();
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定表的可用字段列表
|
||||
/// </summary>
|
||||
/// <param name="tableName">表名</param>
|
||||
Task<List<MedalRuleFieldOptionOutput>> GetTableFieldsAsync(string tableName);
|
||||
}
|
||||
25
QYZH.InteractiveMagazine.IService/IMessagePublishService.cs
Normal file
25
QYZH.InteractiveMagazine.IService/IMessagePublishService.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using QYZH.InteractiveMagazine.Models.Dto.RabbitMQ;
|
||||
using QYZH.InteractiveMagazine.Models.Entity;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.IService;
|
||||
|
||||
/// <summary>
|
||||
/// MQ消息发布服务。
|
||||
/// </summary>
|
||||
public interface IMessagePublishService : IBaseService<MessageOutbox>
|
||||
{
|
||||
/// <summary>
|
||||
/// 可靠发布消息,默认写入Outbox。
|
||||
/// </summary>
|
||||
Task<MessagePublishResult> PublishAsync<T>(MessagePublishInput<T> input, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 批量可靠发布消息,默认写入Outbox。
|
||||
/// </summary>
|
||||
Task<MessagePublishResult> PublishBatchAsync<T>(IEnumerable<MessagePublishInput<T>> inputs, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 直接发布消息,不写Outbox。
|
||||
/// </summary>
|
||||
Task<MessagePublishResult> PublishDirectAsync<T>(MessagePublishInput<T> input, CancellationToken cancellationToken = default);
|
||||
}
|
||||
43
QYZH.InteractiveMagazine.IService/IOperationLogService.cs
Normal file
43
QYZH.InteractiveMagazine.IService/IOperationLogService.cs
Normal file
@ -0,0 +1,43 @@
|
||||
using QYZH.InteractiveMagazine.Models.Dto;
|
||||
using QYZH.InteractiveMagazine.Models.Entity;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.IService;
|
||||
|
||||
/// <summary>
|
||||
/// 操作日志服务接口
|
||||
/// </summary>
|
||||
public interface IOperationLogService : IBaseService<OperationLog>
|
||||
{
|
||||
/// <summary>
|
||||
/// 记录操作日志
|
||||
/// </summary>
|
||||
/// <param name="operatorId">操作人Id</param>
|
||||
/// <param name="operatorName">操作人用户名</param>
|
||||
/// <param name="actionType">操作类型(使用 OperationLogActionType 常量)</param>
|
||||
/// <param name="targetType">目标类型(使用 OperationLogTargetType 常量)</param>
|
||||
/// <param name="targetId">目标记录Id</param>
|
||||
/// <param name="targetName">目标名称</param>
|
||||
/// <param name="detail">操作详情(可选,JSON格式)</param>
|
||||
/// <param name="ipAddress">IP地址(可选)</param>
|
||||
Task LogAsync(long operatorId, string operatorName, string actionType, string targetType, long targetId, string? targetName = null, string? detail = null, string? ipAddress = null);
|
||||
|
||||
/// <summary>
|
||||
/// 记录操作日志
|
||||
/// </summary>
|
||||
/// <param name="input">操作日志记录输入</param>
|
||||
Task LogAsync(OperationLogRecordInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 分页查询操作日志
|
||||
/// </summary>
|
||||
/// <param name="input">查询参数</param>
|
||||
/// <returns>分页结果</returns>
|
||||
Task<PageListModel<OperationLogOutput>> GetListAsync(OperationLogQueryInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 获取操作日志详情
|
||||
/// </summary>
|
||||
/// <param name="id">日志Id</param>
|
||||
/// <returns>日志详情</returns>
|
||||
Task<OperationLogDetailOutput> GetDetailAsync(long id);
|
||||
}
|
||||
186
QYZH.InteractiveMagazine.IService/IPetService.cs
Normal file
186
QYZH.InteractiveMagazine.IService/IPetService.cs
Normal file
@ -0,0 +1,186 @@
|
||||
using QYZH.InteractiveMagazine.Models.Common;
|
||||
using QYZH.InteractiveMagazine.Models.Dto;
|
||||
using QYZH.InteractiveMagazine.Models.Dto.Pet;
|
||||
using QYZH.InteractiveMagazine.Models.Entity;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.IService;
|
||||
|
||||
/// <summary>
|
||||
/// 宠物服务接口
|
||||
/// </summary>
|
||||
public interface IPetService : IBaseService<UserPet>
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取用户宠物信息
|
||||
/// </summary>
|
||||
/// <param name="userId">用户Id</param>
|
||||
/// <returns>宠物信息</returns>
|
||||
Task<PetOutput?> GetPetByUserIdAsync(long userId);
|
||||
|
||||
/// <summary>
|
||||
/// 为用户创建默认宠物(最低形态、成长值为0、未激活状态)
|
||||
/// </summary>
|
||||
/// <param name="userId">用户Id</param>
|
||||
Task CreateDefaultPetAsync(long userId);
|
||||
|
||||
/// <summary>
|
||||
/// 激活宠物(将状态从 Inactive 改为 Active)
|
||||
/// </summary>
|
||||
/// <param name="userId">用户Id</param>
|
||||
Task ActivatePetAsync(long userId);
|
||||
|
||||
/// <summary>
|
||||
/// 喂养宠物(增加成长值 + 记录喂养记录 + 触发进化检查)
|
||||
/// </summary>
|
||||
/// <param name="userId">用户Id</param>
|
||||
/// <param name="input">喂养输入</param>
|
||||
/// <returns>喂养结果</returns>
|
||||
Task<FeedPetOutput> FeedPetAsync(long userId, FeedPetInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 喂养宠物(无事务,需在外部事务中调用)
|
||||
/// </summary>
|
||||
/// <param name="userId">用户Id</param>
|
||||
/// <param name="input">喂养输入</param>
|
||||
/// <returns>喂养结果</returns>
|
||||
Task<FeedPetOutput> FeedPetInTranAsync(long userId, FeedPetInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 获取宠物喂养记录列表
|
||||
/// </summary>
|
||||
/// <param name="userId">用户Id</param>
|
||||
/// <param name="petId">宠物Id</param>
|
||||
/// <returns>喂养记录列表</returns>
|
||||
Task<PageListModel<FeedingRecordOutput>> GetFeedingRecordsAsync(long userId, long petId, PageQueryModel pageQuery);
|
||||
|
||||
// ==================== 后台管理:宠物模板 ====================
|
||||
|
||||
/// <summary>
|
||||
/// 创建宠物模板
|
||||
/// </summary>
|
||||
Task<PetTemplateOutput> CreateTemplateAsync(PetTemplateInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 更新宠物模板
|
||||
/// </summary>
|
||||
Task<PetTemplateOutput> UpdateTemplateAsync(long id, PetTemplateInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 删除宠物模板(软删除)
|
||||
/// </summary>
|
||||
Task DeleteTemplateAsync(long id);
|
||||
|
||||
/// <summary>
|
||||
/// 获取单个宠物模板
|
||||
/// </summary>
|
||||
Task<PetTemplateOutput> GetTemplateByIdAsync(long id);
|
||||
|
||||
/// <summary>
|
||||
/// 分页查询宠物模板列表
|
||||
/// </summary>
|
||||
Task<PageListModel<PetTemplateOutput>> GetTemplatesAsync(PetTemplateQueryInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 更新模板状态(启用/禁用)
|
||||
/// </summary>
|
||||
Task UpdateTemplateStatusAsync(long id);
|
||||
|
||||
// ==================== 后台管理:进化链 ====================
|
||||
|
||||
/// <summary>
|
||||
/// 创建进化阶段
|
||||
/// </summary>
|
||||
Task<PetEvolutionOutput> CreateEvolutionAsync(PetEvolutionInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 更新进化阶段
|
||||
/// </summary>
|
||||
Task<PetEvolutionOutput> UpdateEvolutionAsync(long id, PetEvolutionInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 删除进化阶段(软删除)
|
||||
/// </summary>
|
||||
Task DeleteEvolutionAsync(long id);
|
||||
|
||||
/// <summary>
|
||||
/// 获取单个进化阶段
|
||||
/// </summary>
|
||||
Task<PetEvolutionOutput> GetEvolutionByIdAsync(long id);
|
||||
|
||||
/// <summary>
|
||||
/// 分页查询进化阶段列表
|
||||
/// </summary>
|
||||
Task<PageListModel<PetEvolutionOutput>> GetEvolutionsAsync(PetEvolutionQueryInput input);
|
||||
|
||||
// ==================== 后台管理:皮肤 ====================
|
||||
|
||||
/// <summary>
|
||||
/// 创建皮肤
|
||||
/// </summary>
|
||||
Task<PetSkinOutput> CreateSkinAsync(PetSkinInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 更新皮肤
|
||||
/// </summary>
|
||||
Task<PetSkinOutput> UpdateSkinAsync(long id, PetSkinInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 删除皮肤(软删除)
|
||||
/// </summary>
|
||||
Task DeleteSkinAsync(long id);
|
||||
|
||||
/// <summary>
|
||||
/// 获取单个皮肤(含图片列表)
|
||||
/// </summary>
|
||||
Task<PetSkinOutput> GetSkinByIdAsync(long id);
|
||||
|
||||
/// <summary>
|
||||
/// 分页查询皮肤列表
|
||||
/// </summary>
|
||||
Task<PageListModel<PetSkinOutput>> GetSkinsAsync(PetSkinQueryInput input);
|
||||
|
||||
// ==================== 后台管理:皮肤图片 ====================
|
||||
|
||||
/// <summary>
|
||||
/// 创建皮肤图片
|
||||
/// </summary>
|
||||
Task<PetSkinImageOutput> CreateSkinImageAsync(PetSkinImageInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 更新皮肤图片
|
||||
/// </summary>
|
||||
Task<PetSkinImageOutput> UpdateSkinImageAsync(long id, PetSkinImageInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 删除皮肤图片(软删除)
|
||||
/// </summary>
|
||||
Task DeleteSkinImageAsync(long id);
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定皮肤的所有图片
|
||||
/// </summary>
|
||||
Task<List<PetSkinImageOutput>> GetSkinImagesBySkinIdAsync(long skinId);
|
||||
|
||||
// ==================== 便捷查询接口 ====================
|
||||
|
||||
/// <summary>
|
||||
/// 根据模板Id获取进化链列表(非分页,按StageLevel排序)
|
||||
/// </summary>
|
||||
Task<List<PetEvolutionOutput>> GetEvolutionsByTemplateIdAsync(long templateId);
|
||||
|
||||
/// <summary>
|
||||
/// 根据模板Id获取皮肤列表(非分页,按SortOrder排序)
|
||||
/// </summary>
|
||||
Task<List<PetSkinOutput>> GetSkinsByTemplateIdAsync(long templateId);
|
||||
|
||||
/// <summary>
|
||||
/// 根据皮肤Id获取各阶段图片列表(按进化阶段分组,含阶段基本信息)
|
||||
/// </summary>
|
||||
Task<List<SkinEvolutionStageOutput>> GetSkinImagesGroupedBySkinIdAsync(long skinId);
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有皮肤下拉列表(仅返回Id和Name,按SortOrder排序)
|
||||
/// </summary>
|
||||
/// <param name="types">皮肤类型列表(可选)</param>
|
||||
Task<List<SelectOptionDto>> GetAllSkinsForSelectAsync(List<string>? types = null);
|
||||
}
|
||||
66
QYZH.InteractiveMagazine.IService/IPointsService.cs
Normal file
66
QYZH.InteractiveMagazine.IService/IPointsService.cs
Normal file
@ -0,0 +1,66 @@
|
||||
using QYZH.InteractiveMagazine.Models.Dto;
|
||||
using QYZH.InteractiveMagazine.Models.Dto.Points;
|
||||
using QYZH.InteractiveMagazine.Models.Entity;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.IService;
|
||||
|
||||
/// <summary>
|
||||
/// 积分服务接口
|
||||
/// </summary>
|
||||
public interface IPointsService : IBaseService<PointsRecord>
|
||||
{
|
||||
// ==================== 带事务版本(独立调用) ====================
|
||||
|
||||
/// <summary>
|
||||
/// 增加积分(带事务)
|
||||
/// </summary>
|
||||
/// <param name="input">增加积分参数</param>
|
||||
/// <returns>增加结果</returns>
|
||||
Task<AddPointsOutput> AddPointsAsync(AddPointsInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 扣除积分(带事务,含余额不足校验)
|
||||
/// </summary>
|
||||
/// <param name="input">扣除积分参数</param>
|
||||
/// <returns>扣除结果</returns>
|
||||
Task<DeductPointsOutput> DeductPointsAsync(DeductPointsInput input);
|
||||
|
||||
// ==================== 无事务版本(供外部事务调用) ====================
|
||||
|
||||
/// <summary>
|
||||
/// 增加积分(无事务,需在外部事务中调用)
|
||||
/// </summary>
|
||||
/// <param name="input">增加积分参数</param>
|
||||
/// <returns>增加结果</returns>
|
||||
Task<AddPointsOutput> AddPointsInTranAsync(AddPointsInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 扣除积分(无事务,需在外部事务中调用,含余额不足校验)
|
||||
/// </summary>
|
||||
/// <param name="input">扣除积分参数</param>
|
||||
/// <returns>扣除结果</returns>
|
||||
Task<DeductPointsOutput> DeductPointsInTranAsync(DeductPointsInput input);
|
||||
|
||||
// ==================== 查询 ====================
|
||||
|
||||
/// <summary>
|
||||
/// 查询用户当前积分余额
|
||||
/// </summary>
|
||||
/// <param name="userId">用户Id</param>
|
||||
/// <returns>当前积分余额</returns>
|
||||
Task<int> GetUserPointsAsync(long userId);
|
||||
|
||||
/// <summary>
|
||||
/// 获取用户积分概览(当前余额 + 累计收入 + 累计支出)
|
||||
/// </summary>
|
||||
/// <param name="userId">用户Id</param>
|
||||
/// <returns>积分概览</returns>
|
||||
Task<PointsSummaryOutput> GetPointsSummaryAsync(long userId);
|
||||
|
||||
/// <summary>
|
||||
/// 分页查询积分流水
|
||||
/// </summary>
|
||||
/// <param name="input">查询参数</param>
|
||||
/// <returns>分页流水列表</returns>
|
||||
Task<PageListModel<PointsRecordOutput>> GetPointsRecordsAsync(PointsRecordQueryInput input);
|
||||
}
|
||||
51
QYZH.InteractiveMagazine.IService/IProductService.cs
Normal file
51
QYZH.InteractiveMagazine.IService/IProductService.cs
Normal file
@ -0,0 +1,51 @@
|
||||
using QYZH.InteractiveMagazine.Models.Dto;
|
||||
using QYZH.InteractiveMagazine.Models.Entity;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.IService;
|
||||
|
||||
/// <summary>
|
||||
/// 商品服务接口
|
||||
/// </summary>
|
||||
public interface IProductService : IBaseService<Product>
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建商品
|
||||
/// </summary>
|
||||
/// <param name="input">商品输入</param>
|
||||
/// <returns>创建的商品信息</returns>
|
||||
Task<ProductOutput> CreateAsync(ProductInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 更新商品
|
||||
/// </summary>
|
||||
/// <param name="id">商品ID</param>
|
||||
/// <param name="input">商品输入</param>
|
||||
/// <returns>更新后的商品信息</returns>
|
||||
Task<ProductOutput> UpdateAsync(long id, ProductInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 删除商品(软删除)
|
||||
/// </summary>
|
||||
/// <param name="id">商品ID</param>
|
||||
Task DeleteAsync(long id);
|
||||
|
||||
/// <summary>
|
||||
/// 根据ID获取商品
|
||||
/// </summary>
|
||||
/// <param name="id">商品ID</param>
|
||||
/// <returns>商品信息</returns>
|
||||
Task<ProductOutput> GetByIdAsync(long id);
|
||||
|
||||
/// <summary>
|
||||
/// 分页查询商品列表
|
||||
/// </summary>
|
||||
/// <param name="input">查询条件</param>
|
||||
/// <returns>分页结果</returns>
|
||||
Task<PageListModel<ProductOutput>> GetListAsync(ProductQueryInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 更新商品上架/下架状态(取反)
|
||||
/// </summary>
|
||||
/// <param name="id">商品ID</param>
|
||||
Task UpdateSaleStatusAsync(long id);
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
using QYZH.InteractiveMagazine.Models.Dto.SystemManagement;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.IService;
|
||||
|
||||
/// <summary>
|
||||
/// 系统管理服务接口
|
||||
/// </summary>
|
||||
public interface ISystemManagementService
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取所有枚举信息
|
||||
/// </summary>
|
||||
/// <returns>枚举信息列表</returns>
|
||||
Task<List<EnumInfoOutput>> GetAllEnumsAsync();
|
||||
}
|
||||
73
QYZH.InteractiveMagazine.IService/IUserAnswerTaskService.cs
Normal file
73
QYZH.InteractiveMagazine.IService/IUserAnswerTaskService.cs
Normal file
@ -0,0 +1,73 @@
|
||||
using QYZH.InteractiveMagazine.Models.Dto.UserAnswerTaskService;
|
||||
using QYZH.InteractiveMagazine.Models.Entity;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.IService;
|
||||
|
||||
/// <summary>
|
||||
/// 用户作答任务服务接口
|
||||
/// </summary>
|
||||
public interface IUserAnswerTaskService : IBaseService<JournalPageTaskUserAnswer>
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取用户期刊学习进度列表(按期刊分组)
|
||||
/// </summary>
|
||||
/// <param name="userId">用户Id</param>
|
||||
/// <returns>按期刊分组的学习进度数据</returns>
|
||||
Task<List<JournalProgressOutput>> GetJournalProgressAsync(long userId);
|
||||
|
||||
/// <summary>
|
||||
/// 获取目录列表(按一级目录分组,二级目录含完成状态)
|
||||
/// </summary>
|
||||
/// <param name="journalId">期刊Id</param>
|
||||
/// <param name="userId">用户Id</param>
|
||||
/// <returns>按一级目录分组的目录列表</returns>
|
||||
Task<List<CatalogListOutput>> GetCatalogListAsync(long journalId, long userId);
|
||||
|
||||
/// <summary>
|
||||
/// 获取一级目录概览(含二级目录详情、页码范围、任务完成状态)
|
||||
/// </summary>
|
||||
/// <param name="catalogId">一级目录Id(JournalCatalogId)</param>
|
||||
/// <param name="userId">用户Id</param>
|
||||
/// <returns>一级目录概览数据</returns>
|
||||
Task<CatalogOverviewOutput> GetCatalogOverviewAsync(long catalogId, long userId);
|
||||
|
||||
/// <summary>
|
||||
/// 获取用户答题任务列表
|
||||
/// </summary>
|
||||
/// <param name="catalogId">二级目录Id(CatalogId)</param>
|
||||
/// <param name="userId">用户Id</param>
|
||||
/// <returns>答题任务列表</returns>
|
||||
Task<UserAnswerTaskListOutput> GetUserAnswerTaskListAsync(long catalogId, long userId);
|
||||
|
||||
/// <summary>
|
||||
/// 获取跨页题目详情
|
||||
/// </summary>
|
||||
/// <param name="groupId">任务分组ID(JournalPageTaskGroupId)</param>
|
||||
/// <param name="userId">用户Id</param>
|
||||
/// <returns>跨页题目详情</returns>
|
||||
Task<CrossPageTaskOutput> GetCrossPageTaskAsync(long groupId, long userId);
|
||||
|
||||
/// <summary>
|
||||
/// 领取任务积分
|
||||
/// </summary>
|
||||
/// <param name="groupId">任务分组Id(JournalPageTask.GroupId)</param>
|
||||
/// <param name="userId">用户Id</param>
|
||||
/// <returns>领取结果</returns>
|
||||
Task<ClaimTaskPointsOutput> ClaimTaskPointsAsync(long groupId, long userId);
|
||||
|
||||
/// <summary>
|
||||
/// 批量领取任务积分(一键领取)
|
||||
/// </summary>
|
||||
/// <param name="input">批量领取参数</param>
|
||||
/// <param name="userId">用户Id</param>
|
||||
/// <returns>批量领取结果</returns>
|
||||
Task<BatchClaimPointsOutput> BatchClaimPointsAsync(BatchClaimPointsInput input, long userId);
|
||||
|
||||
/// <summary>
|
||||
/// 获取目录树形结构(一级目录和二级目录的完整树形结构,包含任务详情)
|
||||
/// </summary>
|
||||
/// <param name="journalId">期刊Id</param>
|
||||
/// <param name="userId">用户Id</param>
|
||||
/// <returns>目录树形结构数据</returns>
|
||||
Task<CatalogTreeOutput> GetCatalogTreeAsync(long journalId, long userId);
|
||||
}
|
||||
70
QYZH.InteractiveMagazine.IService/IUserJournalService.cs
Normal file
70
QYZH.InteractiveMagazine.IService/IUserJournalService.cs
Normal file
@ -0,0 +1,70 @@
|
||||
using QYZH.InteractiveMagazine.Models.Dto;
|
||||
using QYZH.InteractiveMagazine.Models.Entity;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.IService;
|
||||
|
||||
/// <summary>
|
||||
/// 用户期刊关联服务接口
|
||||
/// </summary>
|
||||
public interface IUserJournalService : IBaseService<UserJournal>
|
||||
{
|
||||
/// <summary>
|
||||
/// 用户绑定期刊(扫码绑定)
|
||||
/// </summary>
|
||||
/// <param name="userId">当前用户Id</param>
|
||||
/// <param name="input">绑定输入</param>
|
||||
/// <returns>绑定结果</returns>
|
||||
Task<BindJournalOutput> BindJournalAsync(long userId, BindJournalInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 获取用户的期刊绑定列表
|
||||
/// </summary>
|
||||
/// <param name="userId">用户Id</param>
|
||||
/// <param name="input">查询条件</param>
|
||||
/// <returns>分页结果</returns>
|
||||
Task<PageListModel<BindJournalOutput>> GetUserJournalsAsync(long userId, UserJournalQueryInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 取消期刊绑定
|
||||
/// </summary>
|
||||
/// <param name="userId">用户Id</param>
|
||||
/// <param name="id">绑定记录Id</param>
|
||||
Task UnbindJournalAsync(long userId, long id);
|
||||
|
||||
/// <summary>
|
||||
/// 批量提交期刊二维码生成任务
|
||||
/// </summary>
|
||||
/// <param name="input">生成输入</param>
|
||||
/// <param name="operatorName">操作人名称</param>
|
||||
/// <returns>提交结果</returns>
|
||||
Task<CreateUserJournalQrCodeOutput> CreateQrCodesAsync(CreateUserJournalQrCodeInput input, string operatorName);
|
||||
|
||||
/// <summary>
|
||||
/// 分页查询期刊二维码记录
|
||||
/// </summary>
|
||||
/// <param name="input">查询条件</param>
|
||||
/// <returns>分页结果</returns>
|
||||
Task<PageListModel<UserJournalQrCodeOutput>> GetQrCodePageListAsync(UserJournalQrCodeQueryInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 获取期刊二维码详情
|
||||
/// </summary>
|
||||
/// <param name="id">二维码记录Id</param>
|
||||
/// <returns>二维码记录</returns>
|
||||
Task<UserJournalQrCodeOutput> GetQrCodeDetailAsync(long id);
|
||||
|
||||
/// <summary>
|
||||
/// 根据期刊Id获取未绑定用户的二维码列表
|
||||
/// </summary>
|
||||
/// <param name="journalId">期刊Id</param>
|
||||
/// <returns>未绑定二维码列表</returns>
|
||||
Task<List<UserJournalQrCodeOutput>> GetUnboundQrCodesByJournalIdAsync(long journalId);
|
||||
|
||||
/// <summary>
|
||||
/// 删除未绑定的期刊二维码记录
|
||||
/// </summary>
|
||||
/// <param name="input">删除输入</param>
|
||||
/// <param name="operatorName">操作人名称</param>
|
||||
/// <returns>是否成功</returns>
|
||||
Task<bool> DeleteQrCodeAsync(DeleteUserJournalQrCodeInput input, string operatorName);
|
||||
}
|
||||
55
QYZH.InteractiveMagazine.IService/IUsersService.cs
Normal file
55
QYZH.InteractiveMagazine.IService/IUsersService.cs
Normal file
@ -0,0 +1,55 @@
|
||||
using QYZH.InteractiveMagazine.Models.Dto;
|
||||
using QYZH.InteractiveMagazine.Models.Dto.CheckIn;
|
||||
using QYZH.InteractiveMagazine.Models.Dto.Compensation;
|
||||
using QYZH.InteractiveMagazine.Models.Dto.Points;
|
||||
using QYZH.InteractiveMagazine.Models.Entity;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.IService;
|
||||
|
||||
public interface IUsersService : IBaseService<Users>
|
||||
{
|
||||
/// <summary>
|
||||
/// 分页查询用户列表
|
||||
/// </summary>
|
||||
Task<BaseResponse<PageListModel<UsersOutput>>> GetListAsync(UsersQueryInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 获取用户详情(仅基本信息)
|
||||
/// </summary>
|
||||
Task<BaseResponse<UserDetailOutput>> GetDetailAsync(long id);
|
||||
|
||||
/// <summary>
|
||||
/// 更新用户状态
|
||||
/// </summary>
|
||||
Task<BaseResponse> UpdateStatusAsync(long id);
|
||||
|
||||
/// <summary>
|
||||
/// 手动增加用户积分
|
||||
/// </summary>
|
||||
Task<ManualPointsOutput> ManualAddPointsAsync(long userId, ManualAddPointsInput input, long operatorId, string operatorName, string? ipAddress = null);
|
||||
|
||||
/// <summary>
|
||||
/// 手动扣除用户积分
|
||||
/// </summary>
|
||||
Task<ManualPointsOutput> ManualDeductPointsAsync(long userId, ManualDeductPointsInput input, long operatorId, string operatorName, string? ipAddress = null);
|
||||
|
||||
/// <summary>
|
||||
/// 分页查询用户积分记录
|
||||
/// </summary>
|
||||
Task<BaseResponse<PageListModel<PointsRecordOutput>>> GetUserPointsRecordsAsync(long userId, PointsRecordQueryInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 分页查询用户签到记录
|
||||
/// </summary>
|
||||
Task<BaseResponse<PageListModel<CheckInRecordOutput>>> GetUserCheckInRecordsAsync(long userId, PageQueryModel input);
|
||||
|
||||
/// <summary>
|
||||
/// 分页查询用户补偿任务
|
||||
/// </summary>
|
||||
Task<BaseResponse<PageListModel<CompensationTaskOutput>>> GetUserCompensationTasksAsync(long userId, CompensationTaskQueryInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 分页查询用户期刊列表(含期刊详情)
|
||||
/// </summary>
|
||||
Task<BaseResponse<PageListModel<UserJournalItemOutput>>> GetUserJournalsAsync(long userId, UserJournalQueryInput input);
|
||||
}
|
||||
51
QYZH.InteractiveMagazine.IService/IWeChatAuthService.cs
Normal file
51
QYZH.InteractiveMagazine.IService/IWeChatAuthService.cs
Normal file
@ -0,0 +1,51 @@
|
||||
using QYZH.InteractiveMagazine.Models.Entity;
|
||||
using QYZH.InteractiveMagazine.Models.WeChat;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.IService;
|
||||
|
||||
/// <summary>
|
||||
/// 微信小程序认证服务
|
||||
/// </summary>
|
||||
public interface IWeChatAuthService : IBaseService<WxUser>
|
||||
{
|
||||
/// <summary>
|
||||
/// 微信小程序登录(首次仅创建 WxUser,不自动创建 User)
|
||||
/// </summary>
|
||||
Task<WeChatLoginOutput> LoginAsync(WeChatLoginInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 微信小程序快捷登录(通过 OpenId 直接登录)
|
||||
/// </summary>
|
||||
Task<WeChatLoginOutput> QuickLoginAsync(WeChatQuickLoginInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 切换用户(同一 WxUser 下切换 User 身份,重新生成 Token)
|
||||
/// </summary>
|
||||
/// <param name="wxUserId">当前 WxUser.Id(来自 JWT)</param>
|
||||
/// <param name="currentUserId">当前激活 Users.Id(来自 JWT,用于清除旧 Redis Token)</param>
|
||||
/// <param name="input">切换目标用户参数</param>
|
||||
Task<WeChatSwitchUserOutput> SwitchUserAsync(long wxUserId, long currentUserId, WeChatSwitchUserInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前 WxUser 下所有用户列表
|
||||
/// </summary>
|
||||
/// <param name="wxUserId">当前登录的 WxUser.Id(来自 JWT)</param>
|
||||
/// <returns>用户列表</returns>
|
||||
Task<List<WxUserOutput>> GetUsersAsync(long wxUserId);
|
||||
|
||||
/// <summary>
|
||||
/// 在当前 WxUser 下新增用户(子用户/角色)
|
||||
/// </summary>
|
||||
/// <param name="wxUserId">当前登录的 WxUser.Id(来自 JWT)</param>
|
||||
/// <param name="input">新增用户参数</param>
|
||||
/// <returns>新创建的用户信息</returns>
|
||||
Task<WxUserOutput> CreateUserAsync(long wxUserId, CreateChildUserInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 修改家长名字(WxUser.Name)
|
||||
/// </summary>
|
||||
/// <param name="wxUserId">当前登录的 WxUser.Id(来自 JWT)</param>
|
||||
/// <param name="input">修改名字参数</param>
|
||||
/// <returns>更新后的微信用户信息</returns>
|
||||
Task<WxUserInfoOutput> UpdateWxUserNameAsync(long wxUserId, UpdateWxUserNameInput input);
|
||||
}
|
||||
31
QYZH.InteractiveMagazine.IService/IWeChatCommunityService.cs
Normal file
31
QYZH.InteractiveMagazine.IService/IWeChatCommunityService.cs
Normal file
@ -0,0 +1,31 @@
|
||||
using QYZH.InteractiveMagazine.Models.Dto;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.IService;
|
||||
|
||||
/// <summary>
|
||||
/// 社区微信端服务接口
|
||||
/// </summary>
|
||||
public interface IWeChatCommunityService
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取社区Feed流(翻页)
|
||||
/// </summary>
|
||||
/// <param name="userId">当前用户ID</param>
|
||||
/// <param name="cursor">游标(上一页最后一条消息的ID,首次传null)</param>
|
||||
Task<WxFeedOutput> GetFeedAsync(long userId, long? cursor);
|
||||
|
||||
/// <summary>
|
||||
/// 下拉刷新(返回最新列表)
|
||||
/// </summary>
|
||||
Task<WxFeedOutput> RefreshFeedAsync(long userId);
|
||||
|
||||
/// <summary>
|
||||
/// 点赞
|
||||
/// </summary>
|
||||
Task<WxLikeOutput> LikeAsync(long userId, WxLikeInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 取消点赞
|
||||
/// </summary>
|
||||
Task<WxLikeOutput> UnlikeAsync(long userId, long messageId);
|
||||
}
|
||||
@ -1,23 +0,0 @@
|
||||
using QYZH.InteractiveMagazine.IService.Dto;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.IService;
|
||||
|
||||
/// <summary>
|
||||
/// 微信小程序服务接口
|
||||
/// </summary>
|
||||
public interface IWeChatMiniProgramService
|
||||
{
|
||||
/// <summary>
|
||||
/// 微信登录
|
||||
/// </summary>
|
||||
/// <param name="code">微信登录凭证</param>
|
||||
/// <returns>微信登录结果</returns>
|
||||
Task<WeChatLoginOutput> WeChatLoginAsync(string code);
|
||||
|
||||
/// <summary>
|
||||
/// 获取手机号
|
||||
/// </summary>
|
||||
/// <param name="code">获取手机号凭证</param>
|
||||
/// <returns>手机号信息</returns>
|
||||
Task<WeChatPhoneNumberOutput> GetPhoneNumberAsync(string code);
|
||||
}
|
||||
60
QYZH.InteractiveMagazine.IService/IWxMallService.cs
Normal file
60
QYZH.InteractiveMagazine.IService/IWxMallService.cs
Normal file
@ -0,0 +1,60 @@
|
||||
using QYZH.InteractiveMagazine.Models.Dto.Bag;
|
||||
using QYZH.InteractiveMagazine.Models.Dto.Mall;
|
||||
using QYZH.InteractiveMagazine.Models.Entity;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.IService;
|
||||
|
||||
/// <summary>
|
||||
/// 小程序商城服务接口
|
||||
/// </summary>
|
||||
public interface IWxMallService : IBaseService<ExchangeRecord>
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取商城商品列表(仅上架 + 在售商品)
|
||||
/// </summary>
|
||||
/// <param name="userId">当前用户Id(用于判断是否已拥有)</param>
|
||||
/// <param name="type">按商品类型筛选(可选)</param>
|
||||
Task<List<WxProductOutput>> GetProductsAsync(long userId, string? type = null);
|
||||
|
||||
/// <summary>
|
||||
/// 获取商品详情
|
||||
/// </summary>
|
||||
/// <param name="userId">当前用户Id</param>
|
||||
/// <param name="productId">商品Id</param>
|
||||
Task<WxProductOutput?> GetProductDetailAsync(long userId, long productId);
|
||||
|
||||
/// <summary>
|
||||
/// 积分兑换商品
|
||||
/// </summary>
|
||||
/// <param name="userId">用户Id</param>
|
||||
/// <param name="input">兑换参数</param>
|
||||
Task<ExchangeOutput> ExchangeAsync(long userId, ExchangeInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 获取用户兑换记录
|
||||
/// </summary>
|
||||
/// <param name="userId">用户Id</param>
|
||||
/// <param name="limit">返回数量</param>
|
||||
Task<List<ExchangeRecordOutput>> GetExchangeRecordsAsync(long userId, int limit = 20);
|
||||
|
||||
/// <summary>
|
||||
/// 获取用户背包物品列表
|
||||
/// </summary>
|
||||
/// <param name="userId">用户Id</param>
|
||||
/// <param name="itemType">按物品类型筛选(可选)</param>
|
||||
Task<List<UserBagOutput>> GetBagItemsAsync(long userId, string? itemType = null);
|
||||
|
||||
/// <summary>
|
||||
/// 使用背包物品(补签卡等消耗品)
|
||||
/// </summary>
|
||||
/// <param name="userId">用户Id</param>
|
||||
/// <param name="input">使用参数</param>
|
||||
Task<UseItemOutput> UseItemAsync(long userId, UseItemInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 宠物换肤
|
||||
/// </summary>
|
||||
/// <param name="userId">用户Id</param>
|
||||
/// <param name="input">换肤参数</param>
|
||||
Task EquipSkinAsync(long userId, EquipSkinInput input);
|
||||
}
|
||||
@ -11,8 +11,11 @@ namespace QYZH.InteractiveMagazine.Infrastructure.Auth;
|
||||
/// </summary>
|
||||
public static class JwtHelper
|
||||
{
|
||||
public const string AdminTokenKeyPrefix = "InteractiveMagazine:AdminAuth:Token";
|
||||
public const string WeChatTokenKeyPrefix = "InteractiveMagazine:WeChatAuth:Token";
|
||||
|
||||
/// <summary>
|
||||
/// 生成JWT令牌
|
||||
/// 生成JWT令牌(管理端 / 单用户场景)
|
||||
/// </summary>
|
||||
/// <param name="userId">用户ID</param>
|
||||
/// <param name="userName">用户名</param>
|
||||
@ -29,6 +32,54 @@ public static class JwtHelper
|
||||
new Claim(ClaimTypes.Name, userName)
|
||||
};
|
||||
|
||||
return BuildToken(claims, settings);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成JWT令牌(小程序场景,同时携带 WxUserId 和 UserId)
|
||||
/// </summary>
|
||||
/// <param name="wxUserId">微信用户ID(WxUser.Id)</param>
|
||||
/// <param name="userId">当前激活用户ID(Users.Id,无用户时为 0)</param>
|
||||
/// <param name="userName">用户名</param>
|
||||
/// <param name="settings">JWT配置</param>
|
||||
/// <returns>JWT令牌字符串</returns>
|
||||
public static string GenerateToken(long wxUserId, long userId, string userName, JwtSettings settings)
|
||||
{
|
||||
var claims = new[]
|
||||
{
|
||||
new Claim(JwtRegisteredClaimNames.Sub, userId.ToString()),
|
||||
new Claim(JwtRegisteredClaimNames.Name, userName),
|
||||
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
|
||||
new Claim(ClaimTypes.NameIdentifier, userId.ToString()),
|
||||
new Claim(ClaimTypes.Name, userName),
|
||||
new Claim(WxUserIdClaimType, wxUserId.ToString())
|
||||
};
|
||||
|
||||
return BuildToken(claims, settings);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 自定义 Claim 类型:WxUserId
|
||||
/// </summary>
|
||||
public const string WxUserIdClaimType = "WxUserId";
|
||||
|
||||
public static string BuildAdminTokenKey(string userId)
|
||||
{
|
||||
return $"{AdminTokenKeyPrefix}:{userId}";
|
||||
}
|
||||
|
||||
public static string BuildWeChatTokenKey(string wxUserId, string userId)
|
||||
{
|
||||
return $"{WeChatTokenKeyPrefix}:{wxUserId}:{userId}";
|
||||
}
|
||||
|
||||
public static string BuildWeChatTokenKey(long wxUserId, long userId)
|
||||
{
|
||||
return BuildWeChatTokenKey(wxUserId.ToString(), userId.ToString());
|
||||
}
|
||||
|
||||
private static string BuildToken(Claim[] claims, JwtSettings settings)
|
||||
{
|
||||
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(settings.SecretKey!));
|
||||
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
|
||||
|
||||
@ -36,7 +87,7 @@ public static class JwtHelper
|
||||
issuer: settings.Issuer,
|
||||
audience: settings.Audience,
|
||||
claims: claims,
|
||||
expires: DateTime.Now.AddMinutes(settings.ExpiryMinutes),
|
||||
expires: DateTime.Now.AddDays(settings.JwtTokenExpiryDays),
|
||||
signingCredentials: credentials
|
||||
);
|
||||
|
||||
@ -44,28 +95,75 @@ public static class JwtHelper
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证JWT令牌
|
||||
/// 获取Token过期时间
|
||||
/// </summary>
|
||||
/// <param name="token">JWT令牌</param>
|
||||
/// <param name="settings">JWT配置</param>
|
||||
/// <returns>ClaimsPrincipal对象</returns>
|
||||
public static ClaimsPrincipal ValidateToken(string token, JwtSettings settings)
|
||||
/// <returns>过期时间</returns>
|
||||
public static DateTime? GetTokenExpiry(string token)
|
||||
{
|
||||
var tokenHandler = new JwtSecurityTokenHandler();
|
||||
var key = Encoding.UTF8.GetBytes(settings.SecretKey!);
|
||||
|
||||
var validationParameters = new TokenValidationParameters
|
||||
if (tokenHandler.ReadToken(token) is JwtSecurityToken jwtToken)
|
||||
{
|
||||
ValidateIssuer = true,
|
||||
ValidIssuer = settings.Issuer,
|
||||
ValidateAudience = true,
|
||||
ValidAudience = settings.Audience,
|
||||
ValidateIssuerSigningKey = true,
|
||||
IssuerSigningKey = new SymmetricSecurityKey(key),
|
||||
ValidateLifetime = true,
|
||||
ClockSkew = TimeSpan.Zero
|
||||
};
|
||||
return jwtToken.ValidTo;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return tokenHandler.ValidateToken(token, validationParameters, out _);
|
||||
/// <summary>
|
||||
/// 从Token中获取用户ID
|
||||
/// </summary>
|
||||
/// <param name="token">JWT令牌</param>
|
||||
/// <returns>用户ID</returns>
|
||||
public static long? GetUserIdFromToken(string token)
|
||||
{
|
||||
var tokenHandler = new JwtSecurityTokenHandler();
|
||||
if (tokenHandler.ReadToken(token) is JwtSecurityToken jwtToken)
|
||||
{
|
||||
var userIdClaim = GetClaim(jwtToken, ClaimTypes.NameIdentifier, JwtRegisteredClaimNames.NameId, JwtRegisteredClaimNames.Sub);
|
||||
if (long.TryParse(userIdClaim?.Value, out long userId))
|
||||
{
|
||||
return userId;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从Token中获取微信用户ID(WxUserId)
|
||||
/// </summary>
|
||||
/// <param name="token">JWT令牌</param>
|
||||
/// <returns>WxUserId</returns>
|
||||
public static long? GetWxUserIdFromToken(string token)
|
||||
{
|
||||
var tokenHandler = new JwtSecurityTokenHandler();
|
||||
if (tokenHandler.ReadToken(token) is JwtSecurityToken jwtToken)
|
||||
{
|
||||
var wxUserIdClaim = jwtToken.Claims.FirstOrDefault(c => c.Type == WxUserIdClaimType);
|
||||
if (long.TryParse(wxUserIdClaim?.Value, out long wxUserId))
|
||||
{
|
||||
return wxUserId;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从Token中获取用户名
|
||||
/// </summary>
|
||||
/// <param name="token">JWT令牌</param>
|
||||
/// <returns>用户名</returns>
|
||||
public static string GetUserNameFromToken(string token)
|
||||
{
|
||||
var tokenHandler = new JwtSecurityTokenHandler();
|
||||
if (tokenHandler.ReadToken(token) is JwtSecurityToken jwtToken)
|
||||
{
|
||||
return GetClaim(jwtToken, ClaimTypes.Name, JwtRegisteredClaimNames.UniqueName, JwtRegisteredClaimNames.Name)?.Value ?? string.Empty;
|
||||
}
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private static Claim? GetClaim(JwtSecurityToken jwtToken, params string[] claimTypes)
|
||||
{
|
||||
return jwtToken.Claims.FirstOrDefault(c => claimTypes.Contains(c.Type));
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,30 @@
|
||||
using Autofac;
|
||||
using Autofac.Extensions.DependencyInjection;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using QYZH.InteractiveMagazine.Models.Settings;
|
||||
using RabbitMQ.Client;
|
||||
using StackExchange.Redis;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.Infrastructure.Autofacs
|
||||
{
|
||||
public static class AutofacExtension
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// 根据程序集名称获取程序集
|
||||
/// </summary>
|
||||
/// <param name="builder">Web应用程序构建器</param>
|
||||
public static void UseAutofac(this WebApplicationBuilder builder)
|
||||
{
|
||||
builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory())
|
||||
.ConfigureContainer<ContainerBuilder>((c, containerBuilder) =>
|
||||
{
|
||||
var friendlyName = AppDomain.CurrentDomain.FriendlyName;
|
||||
var source = friendlyName.Split('.');
|
||||
var assemblyNames = string.Join(".", source.Take(source.Length - 1));
|
||||
containerBuilder.RegisterModule(new AutofacModuleRegister(assemblyNames));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
using Autofac;
|
||||
using System.Reflection;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.Infrastructure.Autofacs
|
||||
{
|
||||
public class AutofacModuleRegister : Autofac.Module
|
||||
{
|
||||
private readonly string _assemblyName;
|
||||
public AutofacModuleRegister(string assemblyNames)
|
||||
{
|
||||
_assemblyName = assemblyNames;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 加在程序集
|
||||
/// </summary>
|
||||
/// <param name="builder"></param>
|
||||
protected override void Load(ContainerBuilder builder)
|
||||
{
|
||||
//注册Service
|
||||
builder.RegisterAssemblyTypes(GetAssemblyByName($"{_assemblyName}.Service")).Where(t => t.Name.EndsWith("Service")).AsImplementedInterfaces().InstancePerLifetimeScope();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据程序集名称获取程序集
|
||||
/// </summary>
|
||||
/// <param name="AssemblyName">程序集名称</param>
|
||||
public static Assembly GetAssemblyByName(string AssemblyName)
|
||||
{
|
||||
return Assembly.Load(AssemblyName);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,119 +0,0 @@
|
||||
using StackExchange.Redis;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.Infrastructure.Cache;
|
||||
|
||||
/// <summary>
|
||||
/// Redis操作封装
|
||||
/// </summary>
|
||||
public static class RedisHelper
|
||||
{
|
||||
private static string? _keyPrefix;
|
||||
|
||||
/// <summary>
|
||||
/// Redis连接实例
|
||||
/// </summary>
|
||||
public static IConnectionMultiplexer? Connection { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 设置键前缀
|
||||
/// </summary>
|
||||
/// <param name="prefix">前缀字符串</param>
|
||||
public static void SetKeyPrefix(string prefix)
|
||||
{
|
||||
_keyPrefix = prefix;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取Redis数据库实例
|
||||
/// </summary>
|
||||
/// <param name="db">数据库索引</param>
|
||||
/// <returns>IDatabase实例</returns>
|
||||
public static IDatabase GetDatabase(int db = -1)
|
||||
{
|
||||
if (Connection == null || !Connection.IsConnected)
|
||||
{
|
||||
throw new InvalidOperationException("Redis连接未初始化或已断开");
|
||||
}
|
||||
|
||||
return Connection.GetDatabase(db);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置字符串值
|
||||
/// </summary>
|
||||
/// <param name="key">键</param>
|
||||
/// <param name="value">值</param>
|
||||
/// <param name="expiry">过期时间</param>
|
||||
/// <returns>是否成功</returns>
|
||||
public static async Task<bool> StringSetAsync(string key, string value, TimeSpan? expiry = null)
|
||||
{
|
||||
var db = GetDatabase();
|
||||
var prefixedKey = GetPrefixedKey(key);
|
||||
|
||||
if (expiry.HasValue)
|
||||
{
|
||||
return await db.StringSetAsync(prefixedKey, value, (Expiration)expiry.Value);
|
||||
}
|
||||
|
||||
return await db.StringSetAsync(prefixedKey, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取字符串值
|
||||
/// </summary>
|
||||
/// <param name="key">键</param>
|
||||
/// <returns>值</returns>
|
||||
public static async Task<string?> StringGetAsync(string key)
|
||||
{
|
||||
var db = GetDatabase();
|
||||
var prefixedKey = GetPrefixedKey(key);
|
||||
return await db.StringGetAsync(prefixedKey);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除键
|
||||
/// </summary>
|
||||
/// <param name="key">键</param>
|
||||
/// <returns>是否成功</returns>
|
||||
public static async Task<bool> KeyDeleteAsync(string key)
|
||||
{
|
||||
var db = GetDatabase();
|
||||
var prefixedKey = GetPrefixedKey(key);
|
||||
return await db.KeyDeleteAsync(prefixedKey);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查键是否存在
|
||||
/// </summary>
|
||||
/// <param name="key">键</param>
|
||||
/// <returns>是否存在</returns>
|
||||
public static async Task<bool> KeyExistsAsync(string key)
|
||||
{
|
||||
var db = GetDatabase();
|
||||
var prefixedKey = GetPrefixedKey(key);
|
||||
return await db.KeyExistsAsync(prefixedKey);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置键的过期时间
|
||||
/// </summary>
|
||||
/// <param name="key">键</param>
|
||||
/// <param name="expiry">过期时间</param>
|
||||
/// <returns>是否成功</returns>
|
||||
public static async Task<bool> KeyExpireAsync(string key, TimeSpan expiry)
|
||||
{
|
||||
var db = GetDatabase();
|
||||
var prefixedKey = GetPrefixedKey(key);
|
||||
return await db.KeyExpireAsync(prefixedKey, expiry);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取带前缀的键
|
||||
/// </summary>
|
||||
/// <param name="key">原始键</param>
|
||||
/// <returns>带前缀的键</returns>
|
||||
private static string GetPrefixedKey(string key)
|
||||
{
|
||||
return string.IsNullOrEmpty(_keyPrefix) ? key : $"{_keyPrefix}:{key}";
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,75 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.Infrastructure.Context
|
||||
{
|
||||
/// <summary>
|
||||
/// 应用程序上下文
|
||||
/// </summary>
|
||||
public static class ServiceContext
|
||||
{
|
||||
public static IServiceProvider ServiceProvider;
|
||||
|
||||
public static void UseServiceContext(this IApplicationBuilder applicationBuilder)
|
||||
{
|
||||
ServiceProvider = applicationBuilder.ApplicationServices;
|
||||
}
|
||||
|
||||
public static void UseServiceContext(this IServiceCollection services)
|
||||
{
|
||||
ServiceProvider = services.BuildServiceProvider();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取对象实例
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public static T GetService<T>()
|
||||
{
|
||||
return ServiceProvider.GetService<T>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取Scope对象实例
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public static T GetScopeService<T>()
|
||||
{
|
||||
var scope = ServiceProvider.CreateScope();
|
||||
return scope.ServiceProvider.GetService<T>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取对象实例
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public static T GetRequiredService<T>()
|
||||
{
|
||||
return ServiceProvider.GetRequiredService<T>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取对象实例
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public static T GetOptionsMonitor<T>()
|
||||
{
|
||||
return ServiceProvider.GetService<IOptionsMonitor<T>>().CurrentValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取对象实例
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public static T GetOptions<T>() where T : class, new()
|
||||
{
|
||||
return ServiceProvider.GetService<IOptions<T>>().Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,50 +1,74 @@
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using QYZH.InteractiveMagazine.Infrastructure.Cache;
|
||||
using QYZH.InteractiveMagazine.Infrastructure.MessageQueue;
|
||||
using QYZH.InteractiveMagazine.Infrastructure.Auth;
|
||||
using QYZH.InteractiveMagazine.Infrastructure.Middleware;
|
||||
using QYZH.InteractiveMagazine.Models.Settings;
|
||||
using RabbitMQ.Client;
|
||||
using StackExchange.Redis;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.Infrastructure.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// 统一服务注册扩展
|
||||
/// Infrastructure service registration extensions.
|
||||
/// </summary>
|
||||
public static class DependencyInjectionExtensions
|
||||
{
|
||||
private const string NoAuthScheme = "NoAuth";
|
||||
private const string DevelopmentAuthScheme = "DevelopmentSmartAuth";
|
||||
|
||||
/// <summary>
|
||||
/// 注册基础设施服务
|
||||
/// Registers infrastructure services.
|
||||
/// </summary>
|
||||
/// <param name="services">服务集合</param>
|
||||
/// <param name="configuration">配置</param>
|
||||
public static void AddInfrastructureServices(this IServiceCollection services, IConfiguration configuration)
|
||||
public static void AddInfrastructureServices(this IServiceCollection services, IConfiguration configuration, IWebHostEnvironment? environment = null)
|
||||
{
|
||||
AddJwtAuthentication(services, configuration);
|
||||
AddRedisCache(services, configuration);
|
||||
AddRabbitMQ(services, configuration);
|
||||
AddJwtAuthentication(services, configuration, environment);
|
||||
|
||||
services.AddTransient<GlobalExceptionMiddleware>();
|
||||
services.AddTransient<OperationLogMiddleware>();
|
||||
services.AddScoped<OperationLogActionFilter>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 配置JWT认证
|
||||
/// </summary>
|
||||
/// <param name="services">服务集合</param>
|
||||
/// <param name="configuration">配置</param>
|
||||
private static void AddJwtAuthentication(IServiceCollection services, IConfiguration configuration)
|
||||
private static void AddJwtAuthentication(IServiceCollection services, IConfiguration configuration, IWebHostEnvironment? environment = null)
|
||||
{
|
||||
var jwtSettings = configuration.GetSection("JwtSettings").Get<JwtSettings>()!;
|
||||
|
||||
services.AddSingleton(jwtSettings);
|
||||
|
||||
if (environment?.IsDevelopment() == true)
|
||||
{
|
||||
services.AddAuthentication(options =>
|
||||
{
|
||||
options.DefaultScheme = DevelopmentAuthScheme;
|
||||
options.DefaultChallengeScheme = DevelopmentAuthScheme;
|
||||
})
|
||||
.AddPolicyScheme(DevelopmentAuthScheme, null, options =>
|
||||
{
|
||||
options.ForwardDefaultSelector = context =>
|
||||
{
|
||||
var authHeader = context.Request.Headers.Authorization.FirstOrDefault();
|
||||
return !string.IsNullOrWhiteSpace(authHeader) &&
|
||||
authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)
|
||||
? JwtBearerDefaults.AuthenticationScheme
|
||||
: NoAuthScheme;
|
||||
};
|
||||
})
|
||||
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options => ConfigureJwtBearer(options, jwtSettings))
|
||||
.AddScheme<AuthenticationSchemeOptions, NoAuthHandler>(NoAuthScheme, options => { });
|
||||
return;
|
||||
}
|
||||
|
||||
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||
.AddJwtBearer(options =>
|
||||
.AddJwtBearer(options => ConfigureJwtBearer(options, jwtSettings));
|
||||
}
|
||||
|
||||
private static void ConfigureJwtBearer(JwtBearerOptions options, JwtSettings jwtSettings)
|
||||
{
|
||||
options.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
@ -57,54 +81,92 @@ public static class DependencyInjectionExtensions
|
||||
ValidateLifetime = true,
|
||||
ClockSkew = TimeSpan.Zero
|
||||
};
|
||||
});
|
||||
|
||||
options.Events = new JwtBearerEvents
|
||||
{
|
||||
OnTokenValidated = async context =>
|
||||
{
|
||||
var currentToken = GetBearerToken(context);
|
||||
if (string.IsNullOrEmpty(currentToken))
|
||||
{
|
||||
context.Fail("Invalid token");
|
||||
return;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册Redis缓存
|
||||
/// </summary>
|
||||
/// <param name="services">服务集合</param>
|
||||
/// <param name="configuration">配置</param>
|
||||
private static void AddRedisCache(IServiceCollection services, IConfiguration configuration)
|
||||
var userId = context.Principal?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
if (string.IsNullOrEmpty(userId))
|
||||
{
|
||||
var redisSettings = configuration.GetSection("RedisSettings").Get<RedisSettings>()!;
|
||||
|
||||
services.AddSingleton(redisSettings);
|
||||
|
||||
services.AddSingleton<IConnectionMultiplexer>(sp =>
|
||||
{
|
||||
var multiplexer = ConnectionMultiplexer.Connect(redisSettings.ConnectionString!);
|
||||
RedisHelper.Connection = multiplexer;
|
||||
RedisHelper.SetKeyPrefix(redisSettings.InstanceName!);
|
||||
return multiplexer;
|
||||
});
|
||||
context.Fail("Invalid token");
|
||||
return;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册RabbitMQ
|
||||
/// </summary>
|
||||
/// <param name="services">服务集合</param>
|
||||
/// <param name="configuration">配置</param>
|
||||
private static void AddRabbitMQ(IServiceCollection services, IConfiguration configuration)
|
||||
var wxUserId = context.Principal?.FindFirst(JwtHelper.WxUserIdClaimType)?.Value;
|
||||
if (string.IsNullOrEmpty(wxUserId))
|
||||
{
|
||||
var rabbitMQSettings = configuration.GetSection("RabbitMQSettings").Get<RabbitMQSettings>()!;
|
||||
var adminToken = await RedisHelper.GetAsync(JwtHelper.BuildAdminTokenKey(userId));
|
||||
if (adminToken == currentToken)
|
||||
{
|
||||
await RefreshRedisTokenExpiryAsync(JwtHelper.BuildAdminTokenKey(userId), currentToken, jwtSettings);
|
||||
return;
|
||||
}
|
||||
|
||||
services.AddSingleton(rabbitMQSettings);
|
||||
context.Fail("Token expired, please login again");
|
||||
return;
|
||||
}
|
||||
|
||||
services.AddSingleton<IConnection>(sp =>
|
||||
var wechatToken = await RedisHelper.GetAsync(JwtHelper.BuildWeChatTokenKey(wxUserId, userId));
|
||||
if (wechatToken == currentToken)
|
||||
{
|
||||
var factory = new ConnectionFactory
|
||||
{
|
||||
HostName = rabbitMQSettings.HostName!,
|
||||
Port = rabbitMQSettings.Port,
|
||||
UserName = rabbitMQSettings.UserName!,
|
||||
Password = rabbitMQSettings.Password!,
|
||||
VirtualHost = rabbitMQSettings.VirtualHost!
|
||||
await RefreshRedisTokenExpiryAsync(JwtHelper.BuildWeChatTokenKey(wxUserId, userId), currentToken, jwtSettings);
|
||||
return;
|
||||
}
|
||||
|
||||
context.Fail("Token expired, please login again");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return factory.CreateConnectionAsync().GetAwaiter().GetResult();
|
||||
});
|
||||
private static async Task RefreshRedisTokenExpiryAsync(string tokenKey, string currentToken, JwtSettings jwtSettings)
|
||||
{
|
||||
if (jwtSettings.ExpiryMinutes > 0)
|
||||
{
|
||||
await RedisHelper.SetAsync(tokenKey, currentToken, TimeSpan.FromMinutes(jwtSettings.ExpiryMinutes));
|
||||
}
|
||||
}
|
||||
|
||||
services.AddTransient<RabbitMQPublisher>();
|
||||
private static string? GetBearerToken(TokenValidatedContext context)
|
||||
{
|
||||
var authHeader = context.HttpContext.Request.Headers.Authorization.FirstOrDefault();
|
||||
if (string.IsNullOrWhiteSpace(authHeader) || !authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return authHeader.Substring("Bearer ".Length).Trim();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Authentication handler used only in development.
|
||||
/// </summary>
|
||||
public class NoAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
|
||||
{
|
||||
public NoAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
|
||||
: base(options, logger, encoder, clock)
|
||||
{
|
||||
}
|
||||
|
||||
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||
{
|
||||
var claims = new[]
|
||||
{
|
||||
new Claim(ClaimTypes.Name, "DevUser"),
|
||||
new Claim(ClaimTypes.NameIdentifier, "0")
|
||||
};
|
||||
var identity = new ClaimsIdentity(claims, Scheme.Name);
|
||||
var principal = new ClaimsPrincipal(identity);
|
||||
var ticket = new AuthenticationTicket(principal, Scheme.Name);
|
||||
|
||||
return Task.FromResult(AuthenticateResult.Success(ticket));
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,86 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.ResponseCompression;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using QYZH.InteractiveMagazine.Common.Helpers;
|
||||
using QYZH.InteractiveMagazine.Infrastructure.Middleware;
|
||||
using QYZH.InteractiveMagazine.Infrastructure.RabbitMQ;
|
||||
using QYZH.InteractiveMagazine.Infrastructure.Redis;
|
||||
using QYZH.InteractiveMagazine.Infrastructure.SDK;
|
||||
using QYZH.InteractiveMagazine.Models.Settings;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.Infrastructure.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// API 默认服务注册扩展。
|
||||
/// </summary>
|
||||
public static class InteractiveMagazineApiDefaultsExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 注册 WebApi 与 WeChatApi 共享的默认服务。
|
||||
/// </summary>
|
||||
/// <param name="builder">应用构建器。</param>
|
||||
/// <returns>应用构建器。</returns>
|
||||
public static WebApplicationBuilder AddInteractiveMagazineApiDefaults(this WebApplicationBuilder builder)
|
||||
{
|
||||
builder.Services.AddDataProtection()
|
||||
.PersistKeysToFileSystem(new DirectoryInfo(Path.Combine(Directory.GetCurrentDirectory(), "DataProtection")));
|
||||
|
||||
builder.Services.AddCSRedisCacheExtension(builder.Configuration.GetSection("RedisSettings"));
|
||||
builder.Services.AddRabbitMQ(builder.Configuration);
|
||||
|
||||
builder.Services.AddControllers(options =>
|
||||
{
|
||||
options.SuppressImplicitRequiredAttributeForNonNullableReferenceTypes = true;
|
||||
options.Filters.Add<ModelValidActionFilterAttribute>();
|
||||
options.Filters.AddService<OperationLogActionFilter>();
|
||||
})
|
||||
.AddJsonOptions(options =>
|
||||
{
|
||||
options.JsonSerializerOptions.PropertyNameCaseInsensitive = true;
|
||||
options.JsonSerializerOptions.Converters.Add(new JsonConverterUtil.DateTimeConverter());
|
||||
options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
|
||||
})
|
||||
.ConfigureApiBehaviorOptions(opt => opt.SuppressModelStateInvalidFilter = true);
|
||||
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.AddCorsRegister();
|
||||
builder.Services.AddHttpClient();
|
||||
builder.Services.AddHttpContextAccessor();
|
||||
builder.Services.AddInfrastructureServices(builder.Configuration, builder.Environment);
|
||||
builder.Services.AddSDKService(builder.Configuration);
|
||||
builder.Services.Configure<MedalRuleConfigSettings>(builder.Configuration.GetSection("MedalRuleConfig"));
|
||||
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddPolicy("AllowAll", policy =>
|
||||
{
|
||||
policy.AllowAnyOrigin()
|
||||
.AllowAnyMethod()
|
||||
.AllowAnyHeader();
|
||||
});
|
||||
});
|
||||
|
||||
builder.Services.Configure<BrotliCompressionProviderOptions>(options =>
|
||||
{
|
||||
options.Level = System.IO.Compression.CompressionLevel.Optimal;
|
||||
});
|
||||
builder.Services.Configure<GzipCompressionProviderOptions>(options =>
|
||||
{
|
||||
options.Level = System.IO.Compression.CompressionLevel.Fastest;
|
||||
});
|
||||
builder.Services.AddResponseCompression(options =>
|
||||
{
|
||||
options.EnableForHttps = true;
|
||||
options.Providers.Add<BrotliCompressionProvider>();
|
||||
options.Providers.Add<GzipCompressionProvider>();
|
||||
options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(["image/svg+xml", "application/json", "text/plain"]);
|
||||
});
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
@ -1,72 +0,0 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using RabbitMQ.Client;
|
||||
using RabbitMQ.Client.Events;
|
||||
using System.Text;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.Infrastructure.MessageQueue;
|
||||
|
||||
/// <summary>
|
||||
/// RabbitMQ消息消费者基类
|
||||
/// </summary>
|
||||
public abstract class RabbitMQConsumer : IDisposable
|
||||
{
|
||||
private readonly IConnection _connection;
|
||||
private readonly ILogger<RabbitMQConsumer> _logger;
|
||||
private IChannel? _channel;
|
||||
private AsyncEventingBasicConsumer? _consumer;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
/// <param name="connection">RabbitMQ连接</param>
|
||||
/// <param name="logger">日志记录器</param>
|
||||
protected RabbitMQConsumer(IConnection connection, ILogger<RabbitMQConsumer> logger)
|
||||
{
|
||||
_connection = connection;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启动消费
|
||||
/// </summary>
|
||||
/// <param name="queueName">队列名称</param>
|
||||
/// <param name="handleMessage">消息处理委托</param>
|
||||
public async Task StartConsume(string queueName, Func<string, Task> handleMessage)
|
||||
{
|
||||
_channel = await _connection.CreateChannelAsync();
|
||||
|
||||
_consumer = new AsyncEventingBasicConsumer(_channel);
|
||||
_consumer.ReceivedAsync += async (model, ea) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var body = ea.Body.ToArray();
|
||||
var message = Encoding.UTF8.GetString(body);
|
||||
|
||||
await handleMessage(message);
|
||||
|
||||
await _channel.BasicAckAsync(ea.DeliveryTag, false);
|
||||
|
||||
_logger.LogInformation("消息消费成功 | 队列: {QueueName} | 消息: {Message}", queueName, message);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "消息消费失败 | 队列: {QueueName}", queueName);
|
||||
await _channel.BasicNackAsync(ea.DeliveryTag, false, true);
|
||||
}
|
||||
};
|
||||
|
||||
await _channel.BasicConsumeAsync(queue: queueName, autoAck: false, consumer: _consumer);
|
||||
|
||||
_logger.LogInformation("开始消费消息 | 队列: {QueueName}", queueName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放资源
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_channel?.DisposeAsync().GetAwaiter().GetResult();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
@ -1,47 +0,0 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using RabbitMQ.Client;
|
||||
using System.Text;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.Infrastructure.MessageQueue;
|
||||
|
||||
/// <summary>
|
||||
/// RabbitMQ消息发布器
|
||||
/// </summary>
|
||||
public class RabbitMQPublisher
|
||||
{
|
||||
private readonly IConnection _connection;
|
||||
private readonly ILogger<RabbitMQPublisher> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
/// <param name="connection">RabbitMQ连接</param>
|
||||
/// <param name="logger">日志记录器</param>
|
||||
public RabbitMQPublisher(IConnection connection, ILogger<RabbitMQPublisher> logger)
|
||||
{
|
||||
_connection = connection;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发布消息
|
||||
/// </summary>
|
||||
/// <param name="exchange">交换机名称</param>
|
||||
/// <param name="routingKey">路由键</param>
|
||||
/// <param name="message">消息内容</param>
|
||||
public async Task PublishMessage(string exchange, string routingKey, string message)
|
||||
{
|
||||
await using var channel = await _connection.CreateChannelAsync();
|
||||
|
||||
var body = Encoding.UTF8.GetBytes(message);
|
||||
|
||||
await channel.BasicPublishAsync(
|
||||
exchange: exchange,
|
||||
routingKey: routingKey,
|
||||
body: body
|
||||
);
|
||||
|
||||
_logger.LogInformation("消息已发布 | 交换机: {Exchange} | 路由键: {RoutingKey} | 消息: {Message}",
|
||||
exchange, routingKey, message);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,42 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.Infrastructure.Middleware
|
||||
{
|
||||
/// <summary>
|
||||
/// 跨域扩展
|
||||
/// </summary>
|
||||
public static class CorsExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// 跨域配置
|
||||
/// </summary>
|
||||
/// <param name="services"></param>
|
||||
/// <param name="configuration"></param>
|
||||
public static void AddCorsRegister(this WebApplicationBuilder builder)
|
||||
{
|
||||
bool isCorsAll = builder.Configuration["corsUrls"] == "*";
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddPolicy("defaultCors", policy =>
|
||||
{
|
||||
if (isCorsAll)
|
||||
{
|
||||
policy.SetIsOriginAllowed(origin => true);
|
||||
}
|
||||
else
|
||||
{
|
||||
var corsUrls = builder.Configuration.GetSection("corsUrls").Get<string[]>();
|
||||
policy.WithOrigins(corsUrls ?? Array.Empty<string>());
|
||||
}
|
||||
policy.AllowAnyHeader()
|
||||
.AllowAnyMethod()
|
||||
.AllowCredentials()
|
||||
.WithExposedHeaders("X-New-Token");
|
||||
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -13,6 +13,14 @@ public class GlobalExceptionMiddleware : IMiddleware
|
||||
{
|
||||
private readonly ILogger<GlobalExceptionMiddleware> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// JSON序列化选项(camelCase,与Controller保持一致)
|
||||
/// </summary>
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
@ -35,7 +43,7 @@ public class GlobalExceptionMiddleware : IMiddleware
|
||||
}
|
||||
catch (BusinessException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "业务异常:{Message}", ex.Message);
|
||||
_logger.LogWarning(ex, "业务异常:{Message},状态码:{ResultCode}", ex.Message, ex.ResultCode);
|
||||
await HandleBusinessExceptionAsync(context, ex);
|
||||
}
|
||||
catch (Exception ex)
|
||||
@ -46,17 +54,17 @@ public class GlobalExceptionMiddleware : IMiddleware
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理业务异常
|
||||
/// 处理业务异常(按业务状态码映射HTTP状态码)
|
||||
/// </summary>
|
||||
/// <param name="context">HTTP上下文</param>
|
||||
/// <param name="ex">业务异常</param>
|
||||
private static async Task HandleBusinessExceptionAsync(HttpContext context, BusinessException ex)
|
||||
{
|
||||
context.Response.ContentType = "application/json";
|
||||
context.Response.StatusCode = StatusCodes.Status400BadRequest;
|
||||
context.Response.StatusCode = GetHttpStatusCode(ex.ResultCode);
|
||||
|
||||
var response = BaseResponse<object>.Fail(ex.Message, ex.Code);
|
||||
var json = JsonSerializer.Serialize(response);
|
||||
var response = BaseResponse<object>.Fail(ex.ResultCode, ex.Message);
|
||||
var json = JsonSerializer.Serialize(response, JsonOptions);
|
||||
|
||||
await context.Response.WriteAsync(json);
|
||||
}
|
||||
@ -71,9 +79,32 @@ public class GlobalExceptionMiddleware : IMiddleware
|
||||
context.Response.ContentType = "application/json";
|
||||
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
|
||||
|
||||
var response = BaseResponse<object>.Fail("系统内部错误,请稍后重试");
|
||||
var json = JsonSerializer.Serialize(response);
|
||||
var response = BaseResponse<object>.Fail(ResultCode.GLOBAL_ERROR, "系统内部错误,请稍后重试");
|
||||
var json = JsonSerializer.Serialize(response, JsonOptions);
|
||||
|
||||
await context.Response.WriteAsync(json);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据业务状态码获取对应的HTTP状态码
|
||||
/// </summary>
|
||||
/// <param name="resultCode">业务状态码</param>
|
||||
/// <returns>HTTP状态码</returns>
|
||||
private static int GetHttpStatusCode(ResultCode resultCode)
|
||||
{
|
||||
return resultCode switch
|
||||
{
|
||||
ResultCode.DENY => StatusCodes.Status401Unauthorized,
|
||||
ResultCode.FORBIDDEN => StatusCodes.Status403Forbidden,
|
||||
ResultCode.NOT_FOUND => StatusCodes.Status404NotFound,
|
||||
ResultCode.BAD_REQUEST => StatusCodes.Status400BadRequest,
|
||||
ResultCode.PARAM_ERROR => StatusCodes.Status400BadRequest,
|
||||
ResultCode.CONFLICT => StatusCodes.Status409Conflict,
|
||||
ResultCode.UNPROCESSABLE_ENTITY => StatusCodes.Status422UnprocessableEntity,
|
||||
ResultCode.TOO_MANY_REQUESTS => StatusCodes.Status429TooManyRequests,
|
||||
ResultCode.BAD_GATEWAY => StatusCodes.Status502BadGateway,
|
||||
ResultCode.SERVICE_UNAVAILABLE => StatusCodes.Status503ServiceUnavailable,
|
||||
_ => StatusCodes.Status400BadRequest
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,21 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.Infrastructure.Middleware;
|
||||
|
||||
/// <summary>
|
||||
/// 保留兼容的JWT自动刷新中间件,实际刷新在认证成功后执行。
|
||||
/// </summary>
|
||||
public class JwtAutoRefreshMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
|
||||
public JwtAutoRefreshMiddleware(RequestDelegate next)
|
||||
{
|
||||
_next = next;
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
await _next(context);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using QYZH.InteractiveMagazine.Models.Dto;
|
||||
|
||||
|
||||
namespace QYZH.InteractiveMagazine.Infrastructure.Middleware;
|
||||
|
||||
/// <summary>
|
||||
/// 数据验证过滤器
|
||||
/// </summary>
|
||||
public class ModelValidActionFilterAttribute : ActionFilterAttribute
|
||||
{
|
||||
public override void OnActionExecuting(ActionExecutingContext context)
|
||||
{
|
||||
if (!context.ModelState.IsValid)
|
||||
{
|
||||
Dictionary<string, string> errorDic = new Dictionary<string, string>();
|
||||
foreach (var key in context.ModelState.Keys)
|
||||
{
|
||||
var modelstate = context.ModelState[key];
|
||||
if (modelstate.Errors.Any())
|
||||
{
|
||||
string errorStr = string.Join(",", modelstate.Errors.Select(e => e.ErrorMessage).ToList());
|
||||
errorDic.Add(key, errorStr);
|
||||
}
|
||||
}
|
||||
var result = new BaseResponse<Dictionary<string, string>>() { code = ResultCode.BAD_REQUEST };
|
||||
|
||||
result.message = string.Join("|", errorDic.Select(e => e.Value).Distinct());
|
||||
result.result = errorDic;
|
||||
context.Result = new JsonResult(result);
|
||||
}
|
||||
}
|
||||
|
||||
//public override void OnActionExecuted(ActionExecutedContext context)
|
||||
//{
|
||||
// if (context.Exception != null)
|
||||
// {
|
||||
// BaseResponse result = new BaseResponse() { Code = ResultCode.Fail };
|
||||
// result.Message = context.Exception.Message;
|
||||
// context.Result = new JsonResult(result);
|
||||
// }
|
||||
//}
|
||||
}
|
||||
|
||||
@ -0,0 +1,397 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using QYZH.InteractiveMagazine.IService;
|
||||
using QYZH.InteractiveMagazine.Models.Dto;
|
||||
using System.Collections;
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.Infrastructure.Middleware;
|
||||
|
||||
/// <summary>
|
||||
/// 操作日志过滤器
|
||||
/// </summary>
|
||||
public class OperationLogActionFilter(
|
||||
IOperationLogService operationLogService,
|
||||
ILogger<OperationLogActionFilter> logger) : IAsyncActionFilter
|
||||
{
|
||||
private static readonly HashSet<string> SensitiveNames = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"password",
|
||||
"oldPassword",
|
||||
"newPassword",
|
||||
"token",
|
||||
"secret",
|
||||
"authorization"
|
||||
};
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
ReferenceHandler = ReferenceHandler.IgnoreCycles
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 执行操作日志过滤器
|
||||
/// </summary>
|
||||
/// <param name="context">Action 执行上下文</param>
|
||||
/// <param name="next">后续执行委托</param>
|
||||
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
|
||||
{
|
||||
var attribute = context.ActionDescriptor.EndpointMetadata
|
||||
.OfType<OperationLogAttribute>()
|
||||
.FirstOrDefault();
|
||||
|
||||
if (attribute == null)
|
||||
{
|
||||
await next();
|
||||
return;
|
||||
}
|
||||
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
var executedContext = await next();
|
||||
stopwatch.Stop();
|
||||
|
||||
if (executedContext.Exception != null && !executedContext.ExceptionHandled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IsSuccessResult(executedContext.Result, context.HttpContext.Response.StatusCode))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var operatorId = GetOperatorId(context);
|
||||
if (!operatorId.HasValue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var operatorName = GetOperatorName(context);
|
||||
var responseValue = GetResponseValue(executedContext.Result);
|
||||
var responseResult = GetResponseResult(responseValue);
|
||||
var targetId = ResolveTargetId(attribute, context, responseResult, operatorId.Value);
|
||||
var targetName = ResolveTargetName(attribute, context, responseResult);
|
||||
|
||||
var detail = BuildDetail(attribute, context, responseValue, stopwatch.ElapsedMilliseconds);
|
||||
|
||||
await operationLogService.LogAsync(new OperationLogRecordInput
|
||||
{
|
||||
OperatorId = operatorId.Value,
|
||||
OperatorName = operatorName,
|
||||
ActionType = attribute.ActionType,
|
||||
TargetType = attribute.TargetType,
|
||||
TargetId = targetId,
|
||||
TargetName = targetName,
|
||||
Detail = detail,
|
||||
IpAddress = GetClientIp(context)
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "自动记录操作日志失败,Path: {Path}", context.HttpContext.Request.Path);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsSuccessResult(IActionResult? result, int responseStatusCode)
|
||||
{
|
||||
var responseValue = GetResponseValue(result);
|
||||
if (responseValue is BaseResponse response)
|
||||
{
|
||||
return response.isSuccess;
|
||||
}
|
||||
|
||||
if (result is ObjectResult objectResult && objectResult.StatusCode.HasValue)
|
||||
{
|
||||
return IsSuccessStatusCode(objectResult.StatusCode.Value);
|
||||
}
|
||||
|
||||
if (result is StatusCodeResult statusCodeResult)
|
||||
{
|
||||
return IsSuccessStatusCode(statusCodeResult.StatusCode);
|
||||
}
|
||||
|
||||
return IsSuccessStatusCode(responseStatusCode == 0 ? StatusCodes.Status200OK : responseStatusCode);
|
||||
}
|
||||
|
||||
private static bool IsSuccessStatusCode(int statusCode)
|
||||
{
|
||||
return statusCode >= StatusCodes.Status200OK && statusCode < StatusCodes.Status300MultipleChoices;
|
||||
}
|
||||
|
||||
private static long? GetOperatorId(ActionExecutingContext context)
|
||||
{
|
||||
var value = context.HttpContext.User.Claims
|
||||
.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)
|
||||
?.Value;
|
||||
|
||||
return long.TryParse(value, out var operatorId) ? operatorId : null;
|
||||
}
|
||||
|
||||
private static string GetOperatorName(ActionExecutingContext context)
|
||||
{
|
||||
return context.HttpContext.User.Claims
|
||||
.FirstOrDefault(c => c.Type == ClaimTypes.Name)
|
||||
?.Value ?? string.Empty;
|
||||
}
|
||||
|
||||
private static string? GetClientIp(ActionExecutingContext context)
|
||||
{
|
||||
var request = context.HttpContext.Request;
|
||||
var forwardedFor = request.Headers["X-Forwarded-For"].FirstOrDefault();
|
||||
if (!string.IsNullOrWhiteSpace(forwardedFor))
|
||||
{
|
||||
return forwardedFor.Split(',')[0].Trim();
|
||||
}
|
||||
|
||||
var realIp = request.Headers["X-Real-IP"].FirstOrDefault();
|
||||
if (!string.IsNullOrWhiteSpace(realIp))
|
||||
{
|
||||
return realIp;
|
||||
}
|
||||
|
||||
return context.HttpContext.Connection.RemoteIpAddress?.ToString();
|
||||
}
|
||||
|
||||
private static long ResolveTargetId(OperationLogAttribute attribute, ActionExecutingContext context, object? responseResult, long operatorId)
|
||||
{
|
||||
if (attribute.UseOperatorAsTargetId)
|
||||
{
|
||||
return operatorId;
|
||||
}
|
||||
|
||||
if (TryGetLongRouteValue(context, attribute.TargetIdRouteKey, out var routeId))
|
||||
{
|
||||
return routeId;
|
||||
}
|
||||
|
||||
if (TryGetLongArgumentValue(context, attribute.TargetIdArgumentName, out var argumentId))
|
||||
{
|
||||
return argumentId;
|
||||
}
|
||||
|
||||
foreach (var key in new[] { "id", "userId", "adminUserId", "roleId", "taskId", "recordId" })
|
||||
{
|
||||
if (TryGetLongRouteValue(context, key, out routeId) || TryGetLongArgumentValue(context, key, out argumentId))
|
||||
{
|
||||
return routeId != 0 ? routeId : argumentId;
|
||||
}
|
||||
}
|
||||
|
||||
if (TryConvertToLong(responseResult, out var responseId))
|
||||
{
|
||||
return responseId;
|
||||
}
|
||||
|
||||
return TryGetLongPropertyValue(responseResult, "Id", out responseId) ? responseId : 0;
|
||||
}
|
||||
|
||||
private static string? ResolveTargetName(OperationLogAttribute attribute, ActionExecutingContext context, object? responseResult)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(attribute.TargetNameArgumentName)
|
||||
&& context.ActionArguments.TryGetValue(attribute.TargetNameArgumentName, out var nameArgument))
|
||||
{
|
||||
if (nameArgument is string name)
|
||||
{
|
||||
return name;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(attribute.TargetNameProperty)
|
||||
&& TryGetStringPropertyValue(nameArgument, attribute.TargetNameProperty, out name))
|
||||
{
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var value in context.ActionArguments.Values)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(attribute.TargetNameProperty)
|
||||
&& TryGetStringPropertyValue(value, attribute.TargetNameProperty, out var configuredName))
|
||||
{
|
||||
return configuredName;
|
||||
}
|
||||
|
||||
if (TryGetStringPropertyValue(value, "Name", out var name)
|
||||
|| TryGetStringPropertyValue(value, "Title", out name))
|
||||
{
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(attribute.TargetNameProperty)
|
||||
&& TryGetStringPropertyValue(responseResult, attribute.TargetNameProperty, out var responseName))
|
||||
{
|
||||
return responseName;
|
||||
}
|
||||
|
||||
return TryGetStringPropertyValue(responseResult, "Name", out var defaultName)
|
||||
|| TryGetStringPropertyValue(responseResult, "Title", out defaultName)
|
||||
? defaultName
|
||||
: null;
|
||||
}
|
||||
|
||||
private static string BuildDetail(OperationLogAttribute attribute, ActionExecutingContext context, object? responseValue, long elapsedMilliseconds)
|
||||
{
|
||||
var detail = new Dictionary<string, object?>
|
||||
{
|
||||
["method"] = context.HttpContext.Request.Method,
|
||||
["path"] = context.HttpContext.Request.Path.Value,
|
||||
["routeValues"] = context.RouteData.Values.ToDictionary(k => k.Key, v => v.Value?.ToString()),
|
||||
["arguments"] = attribute.LogArguments ? SanitizeValue(context.ActionArguments, 0) : null,
|
||||
["responseMessage"] = GetResponseMessage(responseValue),
|
||||
["elapsedMilliseconds"] = elapsedMilliseconds
|
||||
};
|
||||
|
||||
return JsonSerializer.Serialize(detail, JsonOptions);
|
||||
}
|
||||
|
||||
private static string? GetResponseMessage(object? responseValue)
|
||||
{
|
||||
return responseValue is BaseResponse response ? response.message : null;
|
||||
}
|
||||
|
||||
private static object? GetResponseValue(IActionResult? result)
|
||||
{
|
||||
return result switch
|
||||
{
|
||||
ObjectResult objectResult => objectResult.Value,
|
||||
JsonResult jsonResult => jsonResult.Value,
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
private static object? GetResponseResult(object? responseValue)
|
||||
{
|
||||
if (responseValue == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return responseValue.GetType()
|
||||
.GetProperty("result", BindingFlags.Instance | BindingFlags.Public | BindingFlags.IgnoreCase)
|
||||
?.GetValue(responseValue);
|
||||
}
|
||||
|
||||
private static object? SanitizeValue(object? value, int depth)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (depth > 4)
|
||||
{
|
||||
return value.ToString();
|
||||
}
|
||||
|
||||
var type = value.GetType();
|
||||
if (type.IsPrimitive || value is string or decimal or DateTime or DateTimeOffset or Guid || type.IsEnum)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
if (value is IDictionary dictionary)
|
||||
{
|
||||
var result = new Dictionary<string, object?>();
|
||||
foreach (DictionaryEntry item in dictionary)
|
||||
{
|
||||
var key = item.Key?.ToString() ?? string.Empty;
|
||||
result[key] = SensitiveNames.Contains(key) ? "***" : SanitizeValue(item.Value, depth + 1);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
if (value is IEnumerable enumerable && value is not string)
|
||||
{
|
||||
return enumerable.Cast<object?>()
|
||||
.Take(20)
|
||||
.Select(item => SanitizeValue(item, depth + 1))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
return type.GetProperties(BindingFlags.Instance | BindingFlags.Public)
|
||||
.Where(p => p.GetIndexParameters().Length == 0)
|
||||
.ToDictionary(
|
||||
p => p.Name,
|
||||
p => SensitiveNames.Contains(p.Name) ? "***" : SanitizeValue(p.GetValue(value), depth + 1));
|
||||
}
|
||||
|
||||
private static bool TryGetLongRouteValue(ActionExecutingContext context, string? key, out long value)
|
||||
{
|
||||
value = 0;
|
||||
if (string.IsNullOrWhiteSpace(key) || !context.RouteData.Values.TryGetValue(key, out var routeValue))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return long.TryParse(routeValue?.ToString(), out value);
|
||||
}
|
||||
|
||||
private static bool TryGetLongArgumentValue(ActionExecutingContext context, string? key, out long value)
|
||||
{
|
||||
value = 0;
|
||||
if (string.IsNullOrWhiteSpace(key) || !context.ActionArguments.TryGetValue(key, out var argumentValue))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (TryConvertToLong(argumentValue, out value))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return TryGetLongPropertyValue(argumentValue, "Id", out value);
|
||||
}
|
||||
|
||||
private static bool TryGetLongPropertyValue(object? source, string propertyName, out long value)
|
||||
{
|
||||
value = 0;
|
||||
if (source == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var property = source.GetType().GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.IgnoreCase);
|
||||
return property != null && TryConvertToLong(property.GetValue(source), out value);
|
||||
}
|
||||
|
||||
private static bool TryConvertToLong(object? source, out long value)
|
||||
{
|
||||
value = 0;
|
||||
return source switch
|
||||
{
|
||||
long longValue => SetValue(longValue, out value),
|
||||
int intValue => SetValue(intValue, out value),
|
||||
string stringValue => long.TryParse(stringValue, out value),
|
||||
_ => long.TryParse(source?.ToString(), out value)
|
||||
};
|
||||
}
|
||||
|
||||
private static bool TryGetStringPropertyValue(object? source, string propertyName, out string? value)
|
||||
{
|
||||
value = null;
|
||||
if (source == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var property = source.GetType().GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.IgnoreCase);
|
||||
value = property?.GetValue(source)?.ToString();
|
||||
return !string.IsNullOrWhiteSpace(value);
|
||||
}
|
||||
|
||||
private static bool SetValue(long source, out long value)
|
||||
{
|
||||
value = source;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,59 @@
|
||||
namespace QYZH.InteractiveMagazine.Infrastructure.Middleware;
|
||||
|
||||
/// <summary>
|
||||
/// 操作日志标记
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public sealed class OperationLogAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
/// <param name="actionType">操作类型</param>
|
||||
/// <param name="targetType">目标类型</param>
|
||||
public OperationLogAttribute(string actionType, string targetType)
|
||||
{
|
||||
ActionType = actionType;
|
||||
TargetType = targetType;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 操作类型
|
||||
/// </summary>
|
||||
public string ActionType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 目标类型
|
||||
/// </summary>
|
||||
public string TargetType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 目标Id路由键
|
||||
/// </summary>
|
||||
public string? TargetIdRouteKey { get; set; } = "id";
|
||||
|
||||
/// <summary>
|
||||
/// 目标Id参数名
|
||||
/// </summary>
|
||||
public string? TargetIdArgumentName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 目标名称参数名
|
||||
/// </summary>
|
||||
public string? TargetNameArgumentName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 目标名称属性名
|
||||
/// </summary>
|
||||
public string? TargetNameProperty { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 使用当前操作人Id作为目标Id
|
||||
/// </summary>
|
||||
public bool UseOperatorAsTargetId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否记录参数摘要
|
||||
/// </summary>
|
||||
public bool LogArguments { get; set; } = true;
|
||||
}
|
||||
62
QYZH.InteractiveMagazine.Infrastructure/OSS/OSSOptions.cs
Normal file
62
QYZH.InteractiveMagazine.Infrastructure/OSS/OSSOptions.cs
Normal file
@ -0,0 +1,62 @@
|
||||
|
||||
namespace QYZH.InteractiveMagazine.Infrastructure.OSS
|
||||
{
|
||||
/// <summary>
|
||||
/// OSS配置
|
||||
/// </summary>
|
||||
public class OSSOptions
|
||||
{
|
||||
public const string Section = "AliyunOSSConfigs";
|
||||
|
||||
/// <summary>
|
||||
/// ID
|
||||
/// </summary>
|
||||
public string AccessKeyID { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 密钥
|
||||
/// </summary>
|
||||
public string AccessKeySecret { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 地区
|
||||
/// </summary>
|
||||
public string Region { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 存储桶
|
||||
/// </summary>
|
||||
public string BucketName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 视频点播储存桶
|
||||
/// </summary>
|
||||
public string VodBucketName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 存储桶
|
||||
/// </summary>
|
||||
public string RoleArn { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 过期时间
|
||||
/// </summary>
|
||||
public int DurationSeconds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 终结点
|
||||
/// </summary>
|
||||
public string Endpoint { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// IMM转换项目名称
|
||||
/// </summary>
|
||||
public string ProjectName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 访问域名
|
||||
/// </summary>
|
||||
public string Domain { get; set; }
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
namespace QYZH.InteractiveMagazine.Infrastructure.OSS
|
||||
{
|
||||
public class OssCredentials
|
||||
{
|
||||
public string AccessKeyId { get; set; }
|
||||
|
||||
public string AccessKeySecret { get; set; }
|
||||
|
||||
public string SecurityToken { get; set; }
|
||||
|
||||
public long ExpireTime { get; set; } // 格式化为 "yyyy-MM-dd HH:mm:ss"
|
||||
|
||||
public string BucketName { get; set; }
|
||||
|
||||
public string Region { get; set; } // 格式如 "oss-cn-hangzhou"
|
||||
|
||||
public string Domain { get; set; }
|
||||
}
|
||||
}
|
||||
96
QYZH.InteractiveMagazine.Infrastructure/OSS/OssIMMService.cs
Normal file
96
QYZH.InteractiveMagazine.Infrastructure/OSS/OssIMMService.cs
Normal file
@ -0,0 +1,96 @@
|
||||
using Aliyun.Credentials.Models;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Web;
|
||||
using Tea;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.Infrastructure.OSS
|
||||
{
|
||||
public class OssIMMService
|
||||
{
|
||||
private readonly AlibabaCloud.SDK.Imm20200930.Client _client;
|
||||
private readonly OSSOptions _oSSOptions;
|
||||
private readonly IConfiguration _configuration;
|
||||
public OssIMMService(IConfiguration configuration,IOptionsMonitor<OSSOptions> ossOptions)
|
||||
{
|
||||
this._configuration = configuration;
|
||||
_oSSOptions = ossOptions.CurrentValue;
|
||||
Config config = new Config()
|
||||
{
|
||||
Type = "access_key",
|
||||
AccessKeyId = _oSSOptions.AccessKeyID,
|
||||
AccessKeySecret = _oSSOptions.AccessKeySecret,
|
||||
};
|
||||
|
||||
Aliyun.Credentials.Client credential = new Aliyun.Credentials.Client(config);
|
||||
AlibabaCloud.OpenApiClient.Models.Config config1 = new AlibabaCloud.OpenApiClient.Models.Config
|
||||
{
|
||||
Credential = credential,
|
||||
};
|
||||
// Endpoint 请参考 https://api.aliyun.com/product/imm
|
||||
config1.Endpoint = $"imm.cn-{_oSSOptions.Region}.aliyuncs.com";
|
||||
_client = new AlibabaCloud.SDK.Imm20200930.Client(config1);
|
||||
}
|
||||
|
||||
|
||||
public string DocConvertPng(string source, string prefix)
|
||||
{
|
||||
var createOfficeConversionTaskRequest = new AlibabaCloud.SDK.Imm20200930.Models.CreateOfficeConversionTaskRequest
|
||||
{
|
||||
ProjectName = _oSSOptions.ProjectName,
|
||||
TargetType = "png",
|
||||
SourceURI = $"oss://{_oSSOptions.BucketName}/{HttpUtility.UrlDecode(source)}",
|
||||
TargetURIPrefix = $"oss://{_oSSOptions.BucketName}/{prefix}",
|
||||
//TargetURI = "oss://returnzero/output1/{dirname}/{barename}/{index}.{autoext}",//oss://examplebucket/outputprefix/
|
||||
};
|
||||
var runtime = new AlibabaCloud.TeaUtil.Models.RuntimeOptions();
|
||||
try
|
||||
{
|
||||
// 复制代码运行请自行打印 API 的返回值
|
||||
var data = _client.CreateOfficeConversionTaskWithOptions(createOfficeConversionTaskRequest, runtime);
|
||||
if (data?.StatusCode == 200)
|
||||
{
|
||||
return data.Body.TaskId;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
catch (Exception _error)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public bool GetTask(string taskId)
|
||||
{
|
||||
AlibabaCloud.SDK.Imm20200930.Models.GetTaskRequest getTaskRequest = new()
|
||||
{
|
||||
ProjectName = _oSSOptions.ProjectName,
|
||||
TaskType = "OfficeConversion",
|
||||
TaskId = taskId,
|
||||
};
|
||||
var runtime = new AlibabaCloud.TeaUtil.Models.RuntimeOptions();
|
||||
try
|
||||
{
|
||||
var data = _client.GetTaskWithOptions(getTaskRequest, runtime);
|
||||
if (data?.StatusCode == 200)
|
||||
{
|
||||
if (data.Body.Status == "Running") return GetTask(taskId);
|
||||
if (data.Body.Status == "Succeeded") return true;
|
||||
|
||||
}
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
catch (Exception _error)
|
||||
{
|
||||
TeaException error = new TeaException(new Dictionary<string, object>
|
||||
{
|
||||
{ "message", _error.Message }
|
||||
});
|
||||
Console.WriteLine(error.Message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
111
QYZH.InteractiveMagazine.Infrastructure/OSS/OssImageHelper.cs
Normal file
111
QYZH.InteractiveMagazine.Infrastructure/OSS/OssImageHelper.cs
Normal file
@ -0,0 +1,111 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using QYZH.InteractiveMagazine.Common.Extensions;
|
||||
using QYZH.InteractiveMagazine.Models.Common;
|
||||
using QYZH.InteractiveMagazine.Models.Dto;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.Infrastructure.OSS;
|
||||
|
||||
/// <summary>
|
||||
/// OSS 图片搬运辅助类
|
||||
/// 负责将 temp 临时目录下的文件搬运到正式目录
|
||||
/// </summary>
|
||||
public class OssImageHelper
|
||||
{
|
||||
private readonly OssService _ossService;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public OssImageHelper(OssService ossService, ILogger logger)
|
||||
{
|
||||
_ossService = ossService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断图片是否在 temp 临时目录
|
||||
/// </summary>
|
||||
public static bool IsTempImage(string? imageUrl)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(imageUrl)) return false;
|
||||
return imageUrl.Contains("/temp/", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 temp 目录下的图片搬运到正式目录,返回正式路径
|
||||
/// 如果图片不在 temp 目录,原样返回
|
||||
/// </summary>
|
||||
/// <param name="imageUrl">原始图片 URL(可能是完整 URL 或相对路径)</param>
|
||||
/// <param name="formalFolder">正式目录前缀,如 "product/makeupcard/123" 或 "pet/skin/456"</param>
|
||||
/// <returns>正式路径(相对路径),搬运失败时抛出异常</returns>
|
||||
public async Task<string> MoveToFormalAsync(string imageUrl, string formalFolder)
|
||||
{
|
||||
if (!IsTempImage(imageUrl))
|
||||
return imageUrl;
|
||||
|
||||
// 提取 OSS 相对路径(去掉域名前缀)
|
||||
var sourcePath = imageUrl.RemoveDomain();
|
||||
if (string.IsNullOrWhiteSpace(sourcePath))
|
||||
{
|
||||
_logger.LogWarning("图片路径解析失败,ImageUrl: {ImageUrl}", imageUrl);
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
// URL 解码(处理中文文件名)
|
||||
sourcePath = Uri.UnescapeDataString(sourcePath);
|
||||
|
||||
// 提取文件名
|
||||
var fileName = Path.GetFileName(sourcePath);
|
||||
if (string.IsNullOrWhiteSpace(fileName))
|
||||
{
|
||||
_logger.LogWarning("无法从路径中提取文件名,Path: {Path}", sourcePath);
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
// 生成正式路径
|
||||
var targetPath = $"{formalFolder}/{fileName}";
|
||||
|
||||
_logger.LogInformation("开始搬运图片,Source: {Source} -> Target: {Target}", sourcePath, targetPath);
|
||||
|
||||
try
|
||||
{
|
||||
// 拷贝文件到正式目录
|
||||
var copyResult = _ossService.CopyObject(sourcePath, targetPath);
|
||||
if (!copyResult)
|
||||
{
|
||||
_logger.LogError("OSS 图片拷贝失败,Source: {Source}, Target: {Target}", sourcePath, targetPath);
|
||||
throw new Exception("OSS 图片拷贝失败");
|
||||
}
|
||||
|
||||
// 删除临时文件(失败不影响主流程)
|
||||
var deleteResult = _ossService.DeleteObject(sourcePath);
|
||||
if (!deleteResult)
|
||||
{
|
||||
_logger.LogWarning("OSS 临时文件删除失败(不影响主流程),Source: {Source}", sourcePath);
|
||||
}
|
||||
|
||||
_logger.LogInformation("图片搬运成功,Target: {Target}", targetPath);
|
||||
return targetPath;
|
||||
}
|
||||
catch (Exception ex) when (ex is not BusinessException)
|
||||
{
|
||||
_logger.LogError(ex, "OSS 图片搬运异常,Source: {Source}, Target: {Target}", sourcePath, targetPath);
|
||||
throw new BusinessException("图片搬运失败,请重试", ResultCode.GLOBAL_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量搬运 temp 图片到正式目录
|
||||
/// </summary>
|
||||
/// <param name="imageUrls">原始图片 URL 列表</param>
|
||||
/// <param name="formalFolder">正式目录前缀</param>
|
||||
/// <returns>正式路径列表(顺序与输入一致)</returns>
|
||||
public async Task<List<string>> MoveBatchToFormalAsync(IEnumerable<string> imageUrls, string formalFolder)
|
||||
{
|
||||
var results = new List<string>();
|
||||
foreach (var url in imageUrls)
|
||||
{
|
||||
var formal = await MoveToFormalAsync(url, formalFolder);
|
||||
results.Add(formal);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
}
|
||||
356
QYZH.InteractiveMagazine.Infrastructure/OSS/OssService.cs
Normal file
356
QYZH.InteractiveMagazine.Infrastructure/OSS/OssService.cs
Normal file
@ -0,0 +1,356 @@
|
||||
using Aliyun.Acs.Core;
|
||||
using Aliyun.Acs.Core.Auth.Sts;
|
||||
using Aliyun.Acs.Core.Profile;
|
||||
using Aliyun.OSS;
|
||||
using Aliyun.OSS.Common;
|
||||
using CSRedis;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using QYZH.InteractiveMagazine.Common.Extensions;
|
||||
using QYZH.InteractiveMagazine.Models.Common;
|
||||
using QYZH.InteractiveMagazine.Models.Dto;
|
||||
using System.Text.Json;
|
||||
using System.Web;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.Infrastructure.OSS
|
||||
{
|
||||
public class OssService
|
||||
{
|
||||
private readonly OssClient _ossClient;
|
||||
private readonly OSSOptions _ossOption;
|
||||
private readonly CSRedisClient _redis;
|
||||
private readonly ILogger<OssService> _logger;
|
||||
//private readonly IOssResourceRepository _ossResourceRepository;
|
||||
public OssService(IOptionsMonitor<OSSOptions> options, CSRedisClient redis, ILogger<OssService> logger)
|
||||
{
|
||||
// _ossOption = configuration.GetSection("AliyunOSSConfigs").Get<OSSOptions>();
|
||||
_ossOption = options.CurrentValue;
|
||||
_ossClient = new OssClient($"oss-cn-{_ossOption.Region}.aliyuncs.com", _ossOption.AccessKeyID, _ossOption.AccessKeySecret);
|
||||
_ossClient.SetRegion($"cn-{_ossOption.Region}");
|
||||
_redis = redis;
|
||||
_logger = logger;
|
||||
//_ossResourceRepository = ossResourceRepository;
|
||||
}
|
||||
|
||||
public async Task<OssCredentials> GetSTSTokenAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var stsJson = await _redis.GetAsync("STS-InteractiveMagazine-OssCredentials");
|
||||
if (string.IsNullOrWhiteSpace(stsJson))
|
||||
{
|
||||
// ststoken
|
||||
IClientProfile profile = DefaultProfile.GetProfile($"oss-cn-{_ossOption.Region}", _ossOption.AccessKeyID, _ossOption.AccessKeySecret);
|
||||
DefaultAcsClient client = new DefaultAcsClient(profile);
|
||||
|
||||
var policy = new
|
||||
{
|
||||
Version = "1",
|
||||
Statement = new List<dynamic>
|
||||
{
|
||||
new
|
||||
{
|
||||
Action = new List<string> { "oss:PutObject", "oss:GetObject","oss:DeleteObject" },
|
||||
Resource = new List<string> { $"acs:oss:*:*:{_ossOption.BucketName}/temp/*" },
|
||||
//Condition = new
|
||||
//{
|
||||
// NumericLessThanEquals = new Dictionary<string, object>()
|
||||
// {
|
||||
// {"oss:ContentLength",104857600 }
|
||||
// }
|
||||
//},
|
||||
Effect = "Allow",
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
var assumeRoleRequest = new AssumeRoleRequest();
|
||||
assumeRoleRequest.Policy = JsonSerializer.Serialize(policy);
|
||||
assumeRoleRequest.RoleArn = _ossOption.RoleArn;
|
||||
assumeRoleRequest.RoleSessionName = "oss-services";//这里的名字随便写
|
||||
assumeRoleRequest.DurationSeconds = 3600;//过期时间
|
||||
var response = client.GetAcsResponse(assumeRoleRequest);
|
||||
|
||||
var expiration = DateTime.Parse(response.Credentials.Expiration).AddMinutes(5);
|
||||
var result = new OssCredentials
|
||||
{
|
||||
AccessKeyId = response.Credentials.AccessKeyId,
|
||||
AccessKeySecret = response.Credentials.AccessKeySecret,
|
||||
SecurityToken = response.Credentials.SecurityToken,
|
||||
ExpireTime = expiration.ToMillisecondsTimestamp(),
|
||||
BucketName = _ossOption.BucketName,
|
||||
Region = $"oss-cn-{_ossOption.Region}",
|
||||
Domain = _ossOption.Domain
|
||||
};
|
||||
|
||||
await _redis.SetAsync("STS-InteractiveMagazine-OssCredentials", result, expiration - DateTime.Now);
|
||||
return result;
|
||||
}
|
||||
else
|
||||
{
|
||||
return JsonSerializer.Deserialize<OssCredentials>(stsJson);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Dictionary<string, string>> GetSTSToken()
|
||||
{
|
||||
var stsJson = await _redis.GetAsync("QYZH.InteractiveMagazine.STS");
|
||||
if (stsJson.IsNull())
|
||||
{
|
||||
// ststoken
|
||||
IClientProfile profile = DefaultProfile.GetProfile($"oss-cn-{_ossOption.Region}", _ossOption.AccessKeyID, _ossOption.AccessKeySecret);
|
||||
DefaultAcsClient client = new DefaultAcsClient(profile);
|
||||
|
||||
var policy = new
|
||||
{
|
||||
Version = "1",
|
||||
Statement = new List<dynamic>
|
||||
{
|
||||
new
|
||||
{
|
||||
Action = new List<string> { "oss:PutObject", "oss:GetObject","oss:DeleteObject" },
|
||||
Resource = new List<string> { $"acs:oss:*:*:{_ossOption.BucketName}/temp/*" },
|
||||
//Condition = new
|
||||
//{
|
||||
// NumericLessThanEquals = new Dictionary<string, object>()
|
||||
// {
|
||||
// {"oss:ContentLength",104857600 }
|
||||
// }
|
||||
//},
|
||||
Effect = "Allow",
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
var request = new AssumeRoleRequest();
|
||||
request.Policy = JsonSerializer.Serialize(policy);
|
||||
request.RoleArn = _ossOption.RoleArn;
|
||||
request.RoleSessionName = "oss-services";//这里的名字随便写
|
||||
request.DurationSeconds = 3600;//过期时间
|
||||
var response = client.GetAcsResponse(request);
|
||||
|
||||
var expiration = DateTime.Parse(response.Credentials.Expiration);
|
||||
var result = new Dictionary<string, string>
|
||||
{
|
||||
{"AccessKeyId", response.Credentials.AccessKeyId},
|
||||
{"AccessKeySecret",response.Credentials.AccessKeySecret },
|
||||
{"SecurityToken",response.Credentials.SecurityToken },
|
||||
{"Expiration", DateTime.Parse(response.Credentials.Expiration).ToString("yyyy-MM-dd HH:mm:ss") },
|
||||
{"BucketName",_ossOption.BucketName },
|
||||
{"Region",$"oss-cn-{_ossOption.Region }"},
|
||||
{"Domain",_ossOption.Domain }
|
||||
};
|
||||
|
||||
var seconds = (expiration - DateTime.Now).TotalSeconds - 300;
|
||||
await _redis.SetAsync("QYZH.InteractiveMagazine.STS", result, TimeSpan.FromSeconds(seconds));
|
||||
return result;
|
||||
}
|
||||
return JsonSerializer.Deserialize<Dictionary<string, string>>(stsJson);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 上传单个
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <returns></returns>
|
||||
public string PutObject(string key, Stream content)
|
||||
{
|
||||
//try
|
||||
//{
|
||||
var result = _ossClient.PutObject(_ossOption.BucketName, key, content);
|
||||
if (result.HttpStatusCode == System.Net.HttpStatusCode.OK)
|
||||
{
|
||||
return key;
|
||||
}
|
||||
else return null;
|
||||
//}
|
||||
//catch (Exception ex)
|
||||
//{
|
||||
// return null;
|
||||
//}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除单个
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <returns></returns>
|
||||
public bool DeleteObject(string key)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
if (key.IsNull()) return false;
|
||||
|
||||
key = HttpUtility.UrlDecode(key);
|
||||
//_ossResourceRepository.UpdateDiy.Set(s => s.Status, -1).Where(w => w.Path == key).ExecuteAffrows();
|
||||
|
||||
var result = _ossClient.DeleteObject(_ossOption.BucketName, key);
|
||||
return result.HttpStatusCode == System.Net.HttpStatusCode.OK;
|
||||
}
|
||||
catch (OssException ex)
|
||||
{
|
||||
_logger.LogError(ex.Message + ex.StackTrace);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断对象是否存在
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <returns></returns>
|
||||
public bool DoesObjectExist(string key)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (key.IsNull()) return false;
|
||||
|
||||
key = key.RemoveDomain();
|
||||
key = HttpUtility.UrlDecode(key);
|
||||
|
||||
return _ossClient.DoesObjectExist(_ossOption.BucketName, key);
|
||||
}
|
||||
catch (OssException ex)
|
||||
{
|
||||
_logger.LogError(ex.Message + ex.StackTrace);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取对象内容流
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <returns></returns>
|
||||
public Stream? GetObjectStream(string key)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (key.IsNull()) return null;
|
||||
|
||||
key = key.RemoveDomain();
|
||||
key = HttpUtility.UrlDecode(key);
|
||||
|
||||
var ossObject = _ossClient.GetObject(_ossOption.BucketName, key);
|
||||
return ossObject.Content;
|
||||
}
|
||||
catch (OssException ex)
|
||||
{
|
||||
_logger.LogError(ex.Message + ex.StackTrace);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除多个
|
||||
/// </summary>
|
||||
/// <param name="keys"></param>
|
||||
/// <returns></returns>
|
||||
public bool DeleteObjects(List<string>? keys)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (keys.IsNull()) return false;
|
||||
|
||||
// 设置为详细模式,返回所有删除的文件列表。
|
||||
var quietMode = false;
|
||||
|
||||
keys = keys.Select(it => it.RemoveDomain()).ToList();
|
||||
|
||||
//_ossResourceRepository.UpdateDiy.Set(s => s.Status, -1).Where(w => keys.Contains(w.Path)).ExecuteAffrows();
|
||||
|
||||
var request = new DeleteObjectsRequest(_ossOption.BucketName, keys, quietMode);
|
||||
// 删除多个文件。
|
||||
var result = _ossClient.DeleteObjects(request);
|
||||
|
||||
|
||||
//var del = _ossResourceRepository.Delete(w => keys.Contains(w.Path));
|
||||
//BusinessException.ThrowIf(!del, "删除资源失败");
|
||||
|
||||
|
||||
return result.Keys.Count() == keys.Count;
|
||||
}
|
||||
catch (OssException ex)
|
||||
{
|
||||
_logger.LogError(ex.Message + ex.StackTrace);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public List<string> ListObjects(string prefix)
|
||||
{
|
||||
try
|
||||
{
|
||||
var keys = new List<string>();
|
||||
ObjectListing result = null;
|
||||
string nextMarker = string.Empty;
|
||||
do
|
||||
{
|
||||
var listObjectsRequest = new ListObjectsRequest(_ossOption.BucketName)
|
||||
{
|
||||
Marker = nextMarker,
|
||||
MaxKeys = 100,
|
||||
Prefix = prefix,
|
||||
};
|
||||
result = _ossClient.ListObjects(listObjectsRequest);
|
||||
foreach (var summary in result.ObjectSummaries)
|
||||
{
|
||||
keys.Add(summary.Key);
|
||||
}
|
||||
nextMarker = result.NextMarker;
|
||||
} while (result.IsTruncated);
|
||||
return keys;
|
||||
}
|
||||
catch (OssException ex)
|
||||
{
|
||||
_logger.LogError(ex.Message + ex.StackTrace);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 复制文件
|
||||
/// </summary>
|
||||
/// <param name="sourceObject"></param>
|
||||
/// <param name="targetObject"></param>
|
||||
/// <returns></returns>
|
||||
public bool CopyObject(string sourceObject, string targetObject, long? id = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
//var metadata = new ObjectMetadata();
|
||||
// 设置自定义元数据。自定义元数据为键值对(Key-Value)的形式,例如Key为mk1,Value为mv1。
|
||||
//metadata.AddHeader("mk1", "mv1");
|
||||
//metadata.AddHeader("mk2", "mv2");
|
||||
sourceObject = sourceObject.RemoveDomain();
|
||||
sourceObject = Uri.UnescapeDataString(sourceObject);
|
||||
|
||||
//var uow = _unitOfWorkManager.Begin();
|
||||
//var data = _ossResourceRepository.Insert(new OssResource
|
||||
//{
|
||||
// MainId = id,
|
||||
// SoucePath = sourceObject,
|
||||
// Path = targetObject,
|
||||
|
||||
//});
|
||||
//BusinessException.ThrowIf(data.IsNull(), "资源添加失败");
|
||||
|
||||
var req = new CopyObjectRequest(_ossOption.BucketName, sourceObject, _ossOption.BucketName, targetObject)
|
||||
{
|
||||
// 如果NewObjectMetadata为null则为COPY模式(即拷贝源文件的元数据),非null则为REPLACE模式(覆盖源文件的元数据)。
|
||||
NewObjectMetadata = null
|
||||
};
|
||||
var result = _ossClient.CopyObject(req);
|
||||
return result.HttpStatusCode == System.Net.HttpStatusCode.OK;
|
||||
}
|
||||
catch (OssException ex)
|
||||
{
|
||||
_logger.LogError(ex.Message + ex.StackTrace);
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2,14 +2,22 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\QYZH.InteractiveMagazine.Common\QYZH.InteractiveMagazine.Common.csproj" />
|
||||
<ProjectReference Include="..\QYZH.InteractiveMagazine.IService\QYZH.InteractiveMagazine.IService.csproj" />
|
||||
<ProjectReference Include="..\QYZH.InteractiveMagazine.Models\QYZH.InteractiveMagazine.Models.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AlibabaCloud.SDK.Dysmsapi20170525" Version="4.0.0" />
|
||||
<PackageReference Include="AlibabaCloud.SDK.Imm20200930" Version="4.6.4" />
|
||||
<PackageReference Include="AlibabaCloud.SDK.Vod20170321" Version="3.8.2" />
|
||||
<PackageReference Include="aliyun-net-sdk-sts" Version="3.1.2" />
|
||||
<PackageReference Include="Aliyun.OSS.SDK.NetCore" Version="2.14.1" />
|
||||
<PackageReference Include="Autofac" Version="9.1.0" />
|
||||
<PackageReference Include="Autofac.Extensions.DependencyInjection" Version="11.0.0" />
|
||||
<PackageReference Include="CSRedisCore" Version="3.8.807" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.0" />
|
||||
<PackageReference Include="RabbitMQ.Client" Version="7.2.1" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
|
||||
<PackageReference Include="SqlSugar" Version="5.1.4.207" />
|
||||
<PackageReference Include="StackExchange.Redis" Version="2.13.17" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.18.0" />
|
||||
</ItemGroup>
|
||||
|
||||
@ -0,0 +1,14 @@
|
||||
using RabbitMQ.Client;
|
||||
using RabbitMQ.Client.Events;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.Infrastructure.RabbitMQ
|
||||
{
|
||||
public interface IRabbitMQService
|
||||
{
|
||||
Task<bool> SendAsync(RabbitMQSendParam param, CancellationToken cancellationToken = default);
|
||||
|
||||
Task<bool> SendBatchAsync(IEnumerable<RabbitMQSendParam> @params, CancellationToken cancellationToken = default);
|
||||
|
||||
Task ReceiveAsync(string exchange, string queueName, string routingKey, Func<IChannel, BasicDeliverEventArgs, Task> callback, CancellationToken cancellationToken = default);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,62 @@
|
||||
using RabbitMQ.Client;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.Infrastructure.RabbitMQ
|
||||
{
|
||||
public interface IRabbitMQConnection : IDisposable
|
||||
{
|
||||
Task<IChannel> CreateChannel();
|
||||
}
|
||||
|
||||
public class RabbitMQConnection : IRabbitMQConnection
|
||||
{
|
||||
private readonly ConnectionFactory _factory;
|
||||
private readonly IConnection _connection;
|
||||
private bool _isDisposed;
|
||||
|
||||
public RabbitMQConnection(ConnectionFactory factory)
|
||||
{
|
||||
_factory = factory ?? throw new ArgumentNullException(nameof(factory));
|
||||
_connection = factory.CreateConnectionAsync().Result;
|
||||
}
|
||||
|
||||
public async Task<IChannel> CreateChannel()
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
return await _connection.CreateChannelAsync();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (_isDisposed) return;
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
// Free any other managed objects here.
|
||||
}
|
||||
|
||||
// Free any unmanaged objects here.
|
||||
_connection.Dispose();
|
||||
|
||||
_isDisposed = true;
|
||||
}
|
||||
|
||||
~RabbitMQConnection()
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
|
||||
private void EnsureNotDisposed()
|
||||
{
|
||||
if (_isDisposed)
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(RabbitMQConnection));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
namespace QYZH.InteractiveMagazine.Infrastructure.RabbitMQ
|
||||
{
|
||||
public class RabbitMQOptions
|
||||
{
|
||||
public string UserName { get; set; }
|
||||
public string Password { get; set; }
|
||||
public string HostName { get; set; }
|
||||
public int Port { get; set; }
|
||||
public string ExchangeName { get; set; } = string.Empty;
|
||||
|
||||
public string VirtualHost { get; set; } = "/";
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
namespace QYZH.InteractiveMagazine.Infrastructure.RabbitMQ
|
||||
{
|
||||
public class RabbitMQSendParam
|
||||
{
|
||||
/// <summary>
|
||||
/// 交换机 默认空
|
||||
/// </summary>
|
||||
public string Exchange { get; set; } = "";
|
||||
public string Queue { get; set; }
|
||||
/// <summary>
|
||||
/// 路由键
|
||||
/// </summary>
|
||||
public string RoutingKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 消息数据
|
||||
/// </summary>
|
||||
public object Data { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否清空队列
|
||||
/// </summary>
|
||||
public bool Purge { get; set; } = false;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,178 @@
|
||||
using RabbitMQ.Client;
|
||||
using RabbitMQ.Client.Events;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.Infrastructure.RabbitMQ
|
||||
{
|
||||
public class RabbitMQService : IRabbitMQService
|
||||
{
|
||||
private readonly IRabbitMQConnection _connection;
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly ILogger<RabbitMQService> _logger;
|
||||
private readonly JsonSerializerOptions options = new JsonSerializerOptions
|
||||
{
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||||
};
|
||||
public RabbitMQService(IRabbitMQConnection connection, IConfiguration configuration, ILogger<RabbitMQService> logger)
|
||||
{
|
||||
_connection = connection ?? throw new ArgumentNullException(nameof(connection));
|
||||
_configuration = configuration;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
|
||||
public async Task<bool> SendAsync(RabbitMQSendParam param, CancellationToken cancellationToken = default)
|
||||
{
|
||||
|
||||
try
|
||||
{
|
||||
using var channel = await _connection.CreateChannel();
|
||||
|
||||
// 声明 Exchange(持久化)
|
||||
await channel.ExchangeDeclareAsync(exchange: param.Exchange, type: "direct", durable: true, autoDelete: false, arguments: null);
|
||||
|
||||
// 声明队列(持久化)
|
||||
await channel.QueueDeclareAsync(queue: param.Queue, durable: true, exclusive: false, autoDelete: false, arguments: null);
|
||||
|
||||
// 绑定队列到 Exchange
|
||||
await channel.QueueBindAsync(queue: param.Queue, exchange: param.Exchange, routingKey: param.RoutingKey, arguments: null);
|
||||
|
||||
// 清空队列
|
||||
if (param.Purge) await channel.QueuePurgeAsync(param.Queue);
|
||||
// 消息序列化
|
||||
var mesjson = JsonSerializer.Serialize(param.Data, options);
|
||||
|
||||
var body = Encoding.UTF8.GetBytes(mesjson);
|
||||
var properties = new BasicProperties
|
||||
{
|
||||
Persistent = true // 设置消息持久化
|
||||
};
|
||||
await channel.BasicPublishAsync(param.Exchange, param.RoutingKey, false, properties, body, cancellationToken);
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
catch (OperationCanceledException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "RabbitMQ消息发送已取消,Exchange: {Exchange}, Queue: {Queue}, RoutingKey: {RoutingKey}",
|
||||
param.Exchange, param.Queue, param.RoutingKey);
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "RabbitMQ消息发送失败,Exchange: {Exchange}, Queue: {Queue}, RoutingKey: {RoutingKey}",
|
||||
param.Exchange, param.Queue, param.RoutingKey);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public async Task<bool> SendBatchAsync(IEnumerable<RabbitMQSendParam> @params, CancellationToken cancellationToken = default)
|
||||
{
|
||||
IChannel channel = null;
|
||||
try
|
||||
{
|
||||
channel = await _connection.CreateChannel();
|
||||
|
||||
// 开启事务
|
||||
await channel.TxSelectAsync();
|
||||
|
||||
var properties = new BasicProperties
|
||||
{
|
||||
Persistent = true // 设置消息持久化
|
||||
};
|
||||
|
||||
// 批量发送消息到不同的 routingKey
|
||||
var declaredExchanges = new HashSet<string>();
|
||||
foreach (var param in @params)
|
||||
{
|
||||
// 声明 Exchange(持久化)
|
||||
if (declaredExchanges.Add(param.Exchange))
|
||||
{
|
||||
await channel.ExchangeDeclareAsync(exchange: param.Exchange, type: "direct", durable: true, autoDelete: false, arguments: null);
|
||||
}
|
||||
|
||||
// 声明队列(持久化)
|
||||
await channel.QueueDeclareAsync(queue: param.Queue, durable: true, exclusive: false, autoDelete: false, arguments: null);
|
||||
|
||||
// 绑定队列到 Exchange
|
||||
await channel.QueueBindAsync(queue: param.Queue, exchange: param.Exchange, routingKey: param.RoutingKey, arguments: null);
|
||||
|
||||
// 清空队列
|
||||
if (param.Purge) await channel.QueuePurgeAsync(param.Queue);
|
||||
|
||||
// 消息序列化
|
||||
var mesjson = JsonSerializer.Serialize(param.Data, options);
|
||||
var body = Encoding.UTF8.GetBytes(mesjson);
|
||||
|
||||
// 发布消息
|
||||
await channel.BasicPublishAsync(param.Exchange, param.RoutingKey, false, properties, body, cancellationToken);
|
||||
}
|
||||
|
||||
// 提交事务 - 确保所有消息都发送成功
|
||||
await channel.TxCommitAsync();
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "RabbitMQ批量消息发送失败");
|
||||
// 回滚事务
|
||||
try { await channel?.TxRollbackAsync(); } catch { /* 忽略回滚异常 */ }
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (channel != null && !channel.IsClosed)
|
||||
{
|
||||
await channel.CloseAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public async Task ReceiveAsync(string exchange, string queueName, string routingKey, Func<IChannel, BasicDeliverEventArgs, Task> callback, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var channel = await _connection.CreateChannel();
|
||||
var prefetchCount = _configuration.GetValue<ushort>("RabbitMq:PrefetchCount");
|
||||
if (prefetchCount == 0)
|
||||
{
|
||||
prefetchCount = 1;
|
||||
}
|
||||
|
||||
await channel.BasicQosAsync(0, prefetchCount, false, cancellationToken);
|
||||
|
||||
// 声明 Exchange(持久化)
|
||||
await channel.ExchangeDeclareAsync(exchange: exchange, type: "direct", durable: true, autoDelete: false, arguments: null);
|
||||
|
||||
// 声明队列(持久化)
|
||||
await channel.QueueDeclareAsync(queue: queueName, durable: true, exclusive: false, autoDelete: false, arguments: null);
|
||||
|
||||
// 绑定队列到 Exchange
|
||||
await channel.QueueBindAsync(queue: queueName, exchange: exchange, routingKey: routingKey, arguments: null);
|
||||
|
||||
var consumer = new AsyncEventingBasicConsumer(channel);
|
||||
consumer.ReceivedAsync += async (model, ea) =>
|
||||
{
|
||||
//var body = ea.Body.ToArray();
|
||||
try
|
||||
{
|
||||
// 直接传递 model 和 body 给 callback,不需要转换
|
||||
await callback(channel, ea);
|
||||
}
|
||||
|
||||
finally
|
||||
{
|
||||
//await channel.BasicAckAsync(ea.DeliveryTag, false, cancellationToken);
|
||||
}
|
||||
};
|
||||
await channel.BasicConsumeAsync(queue: queueName, autoAck: false, consumer: consumer, cancellationToken: cancellationToken);
|
||||
// Prevent the method from returning immediately
|
||||
await Task.Delay(-1, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,56 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using RabbitMQ.Client;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.Infrastructure.RabbitMQ
|
||||
{
|
||||
public static class RabbiteMQExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化消息队列,并添加Publisher到IoC容器
|
||||
/// </summary>
|
||||
/// <remarks>从Configuration读取"RabbbitMQOptions配置项"</remarks>
|
||||
public static IServiceCollection AddRabbitMQ(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
var rabbitMqSection = configuration.GetSection("RabbitMq");
|
||||
|
||||
if (rabbitMqSection.Exists())
|
||||
{
|
||||
// 绑定RabbitMQ配置
|
||||
services.Configure<RabbitMQOptions>(rabbitMqSection);
|
||||
// 注册RabbitMQ连接工厂
|
||||
services.AddSingleton<IRabbitMQConnection, RabbitMQConnection>(sp =>
|
||||
{
|
||||
var options = sp.GetRequiredService<IOptions<RabbitMQOptions>>().Value;
|
||||
var factory = new ConnectionFactory()
|
||||
{
|
||||
HostName = options.HostName,
|
||||
Port = options.Port,
|
||||
UserName = options.UserName,
|
||||
Password = options.Password,
|
||||
VirtualHost = options.VirtualHost,
|
||||
|
||||
// 自动恢复配置
|
||||
AutomaticRecoveryEnabled = true, // 启用自动恢复
|
||||
NetworkRecoveryInterval = TimeSpan.FromSeconds(10), // 每10秒尝试重连
|
||||
// 心跳检测
|
||||
RequestedHeartbeat = TimeSpan.FromSeconds(10), // 60秒心跳
|
||||
// 其他重要配置
|
||||
TopologyRecoveryEnabled = true, // 恢复交换机、队列等拓扑结构
|
||||
RequestedConnectionTimeout = TimeSpan.FromSeconds(30), // 连接超时
|
||||
SocketReadTimeout = TimeSpan.FromSeconds(30), // 读取超时
|
||||
SocketWriteTimeout = TimeSpan.FromSeconds(30) // 写入超时
|
||||
};
|
||||
return new RabbitMQConnection(factory);
|
||||
});
|
||||
|
||||
// 添加RabbitMQService的服务注册
|
||||
services.AddSingleton<IRabbitMQService, RabbitMQService>();
|
||||
//services.AddHostedService<TerminalReportService>();
|
||||
}
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
}
|
||||
106
QYZH.InteractiveMagazine.Infrastructure/Redis/RedisExtensions.cs
Normal file
106
QYZH.InteractiveMagazine.Infrastructure/Redis/RedisExtensions.cs
Normal file
@ -0,0 +1,106 @@
|
||||
using CSRedis;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.Infrastructure.Redis
|
||||
{
|
||||
public static class RedisExtensions
|
||||
{
|
||||
public static TimeSpan expireSecond = TimeSpan.FromMinutes(20);
|
||||
|
||||
/// <summary>
|
||||
/// AddCSRedisCacheExtension
|
||||
/// </summary>
|
||||
/// <param name="services"></param>
|
||||
/// <param name="setupAction"></param>
|
||||
/// <returns></returns>
|
||||
public static IServiceCollection AddCSRedisCacheExtension(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
if (services == null)
|
||||
throw new ArgumentNullException("services");
|
||||
|
||||
if (configuration == null)
|
||||
throw new ArgumentNullException("configuration");
|
||||
|
||||
var redisOptions = configuration.Get<RedisCacheOption>();
|
||||
services.AddSingleton(redisOptions);
|
||||
var csredis = new CSRedisClient(redisOptions.ConnectionString, redisOptions.Sentinels, redisOptions.ReadOnly);
|
||||
RedisHelper.Initialization(csredis);
|
||||
services.AddSingleton(csredis);//csredis注册为单例
|
||||
//services.AddSingleton<IDistributedCache>(new Microsoft.Extensions.Caching.Redis.CSRedisCache(RedisHelper.Instance));
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 获取缓存,如果换成不存在则执行方法生成缓存
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="factory"></param>
|
||||
/// <param name="validFor"></param>
|
||||
/// <returns></returns>
|
||||
public static T GetOrSet<T>(this CSRedisClient redisCache, string key, Func<T> factory, TimeSpan? expireTime = null) where T : class
|
||||
{
|
||||
var result = redisCache.Get<T>(key);
|
||||
if (factory != null && result == null)
|
||||
{
|
||||
result = factory();
|
||||
if (expireTime == null || !expireTime.HasValue)
|
||||
expireTime = expireSecond;
|
||||
|
||||
redisCache.Set(key, result, expireTime.Value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取缓存,如果缓存不存在则执行方法生成缓存
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="factory"></param>
|
||||
/// <param name="validFor"></param>
|
||||
/// <returns></returns>
|
||||
public static async Task<T> GetOrSetAsync<T>(this CSRedisClient redisCache, string key, Func<T> factory, TimeSpan? expireTime = null)
|
||||
{
|
||||
var result = await redisCache.GetAsync<T>(key);
|
||||
if (result == null && factory != null)
|
||||
{
|
||||
result = factory();
|
||||
|
||||
if (expireTime == null || !expireTime.HasValue)
|
||||
expireTime = expireSecond;
|
||||
|
||||
if (result != null)
|
||||
await redisCache.SetAsync(key, result, expireTime.Value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public class RedisCacheOption
|
||||
{
|
||||
/// <summary>
|
||||
/// 连接字符串
|
||||
/// </summary>
|
||||
public string ConnectionString { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 哨兵设置
|
||||
/// </summary>
|
||||
public string[] Sentinels { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否只读
|
||||
/// </summary>
|
||||
public bool ReadOnly { get; set; } = false;
|
||||
|
||||
public int[] ExpireSecondRange { get; set; }
|
||||
|
||||
public int? ExpireSecond { get; set; } = 3600;
|
||||
}
|
||||
}
|
||||
25
QYZH.InteractiveMagazine.Infrastructure/SDK/SDKExtensions.cs
Normal file
25
QYZH.InteractiveMagazine.Infrastructure/SDK/SDKExtensions.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using QYZH.InteractiveMagazine.Common.Helpers;
|
||||
using QYZH.InteractiveMagazine.Infrastructure.OSS;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.Infrastructure.SDK
|
||||
{
|
||||
public static class SDKExtensions
|
||||
{
|
||||
public static void AddSDKService(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
services.Configure<OSSOptions>(configuration.GetSection(OSSOptions.Section));
|
||||
|
||||
DomainHelper.OssDomain = configuration.GetSection(OSSOptions.Section).Get<OSSOptions>()!.Domain;
|
||||
|
||||
services.AddSingleton<OssService>();
|
||||
services.AddSingleton<OssIMMService>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,3 +1,5 @@
|
||||
using QYZH.InteractiveMagazine.Models.Dto;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.Models.Common;
|
||||
|
||||
/// <summary>
|
||||
@ -10,6 +12,11 @@ public class BusinessException : Exception
|
||||
/// </summary>
|
||||
public int Code { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 业务状态码
|
||||
/// </summary>
|
||||
public ResultCode ResultCode { get; set; } = ResultCode.FAIL;
|
||||
|
||||
/// <summary>
|
||||
/// 无参构造函数
|
||||
/// </summary>
|
||||
@ -33,6 +40,18 @@ public class BusinessException : Exception
|
||||
public BusinessException(string message, int code) : base(message)
|
||||
{
|
||||
Code = code;
|
||||
ResultCode = System.Enum.IsDefined(typeof(ResultCode), code) ? (ResultCode)code : ResultCode.FAIL;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数(按业务状态码)
|
||||
/// </summary>
|
||||
/// <param name="message">异常消息</param>
|
||||
/// <param name="resultCode">业务状态码</param>
|
||||
public BusinessException(string message, ResultCode resultCode) : base(message)
|
||||
{
|
||||
ResultCode = resultCode;
|
||||
Code = (int)resultCode;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -43,4 +62,34 @@ public class BusinessException : Exception
|
||||
public BusinessException(string message, Exception innerException) : base(message, innerException)
|
||||
{
|
||||
}
|
||||
|
||||
public static void ThrowIf(bool isTrue, string message)
|
||||
{
|
||||
if (isTrue) throw new BusinessException(message);
|
||||
}
|
||||
|
||||
public static void ThrowIf(bool isTrue, Func<string> action)
|
||||
{
|
||||
if (isTrue) throw new BusinessException(action.Invoke());
|
||||
}
|
||||
|
||||
public static void ThrowIf(bool isTrue, string message, Action action)
|
||||
{
|
||||
if (isTrue)
|
||||
{
|
||||
action();
|
||||
throw new BusinessException(message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 条件抛出业务异常(带业务状态码)
|
||||
/// </summary>
|
||||
/// <param name="isTrue">是否触发</param>
|
||||
/// <param name="message">异常消息</param>
|
||||
/// <param name="resultCode">业务状态码</param>
|
||||
public static void ThrowIf(bool isTrue, string message, ResultCode resultCode)
|
||||
{
|
||||
if (isTrue) throw new BusinessException(message, resultCode);
|
||||
}
|
||||
}
|
||||
|
||||
292
QYZH.InteractiveMagazine.Models/Dto/Admin/AdminPermissionDto.cs
Normal file
292
QYZH.InteractiveMagazine.Models/Dto/Admin/AdminPermissionDto.cs
Normal file
@ -0,0 +1,292 @@
|
||||
using QYZH.InteractiveMagazine.Models.Enum;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.Models.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// 后台菜单输入
|
||||
/// </summary>
|
||||
public class AdminMenuInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 父级菜单ID,根节点为0
|
||||
/// </summary>
|
||||
public long ParentId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 菜单名称
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 权限编码
|
||||
/// </summary>
|
||||
public string Code { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 前端路由路径
|
||||
/// </summary>
|
||||
public string? Path { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 前端组件路径
|
||||
/// </summary>
|
||||
public string? Component { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 菜单图标
|
||||
/// </summary>
|
||||
public string? Icon { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 排序值
|
||||
/// </summary>
|
||||
public int Sort { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否显示在菜单
|
||||
/// </summary>
|
||||
public bool IsVisible { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 状态
|
||||
/// </summary>
|
||||
public int Status { get; set; } = 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 后台菜单输出
|
||||
/// </summary>
|
||||
public class AdminMenuOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// 菜单ID
|
||||
/// </summary>
|
||||
public long Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 父级菜单ID
|
||||
/// </summary>
|
||||
public long ParentId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 菜单名称
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 权限编码
|
||||
/// </summary>
|
||||
public string Code { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 前端路由路径
|
||||
/// </summary>
|
||||
public string? Path { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 前端组件路径
|
||||
/// </summary>
|
||||
public string? Component { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 菜单图标
|
||||
/// </summary>
|
||||
public string? Icon { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 排序值
|
||||
/// </summary>
|
||||
public int Sort { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否显示在菜单
|
||||
/// </summary>
|
||||
public bool IsVisible { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态
|
||||
/// </summary>
|
||||
public int Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 子菜单
|
||||
/// </summary>
|
||||
public List<AdminMenuOutput> Children { get; set; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 后台菜单查询输入
|
||||
/// </summary>
|
||||
public class AdminMenuQueryInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 菜单名称
|
||||
/// </summary>
|
||||
public string? Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 权限编码
|
||||
/// </summary>
|
||||
public string? Code { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态
|
||||
/// </summary>
|
||||
public int? Status { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 后台角色输入
|
||||
/// </summary>
|
||||
public class AdminRoleInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 角色名称
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 角色编码
|
||||
/// </summary>
|
||||
public string Code { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 角色类型
|
||||
/// </summary>
|
||||
public AdminRoleTypeEnum Type { get; set; } = AdminRoleTypeEnum.Normal;
|
||||
|
||||
/// <summary>
|
||||
/// 备注
|
||||
/// </summary>
|
||||
public string? Remark { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态
|
||||
/// </summary>
|
||||
public int Status { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 菜单ID集合
|
||||
/// </summary>
|
||||
public List<long> MenuIds { get; set; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 后台角色输出
|
||||
/// </summary>
|
||||
public class AdminRoleOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// 角色ID
|
||||
/// </summary>
|
||||
public long Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 角色名称
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 角色编码
|
||||
/// </summary>
|
||||
public string Code { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 角色类型
|
||||
/// </summary>
|
||||
public AdminRoleTypeEnum Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 备注
|
||||
/// </summary>
|
||||
public string? Remark { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态
|
||||
/// </summary>
|
||||
public int Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 菜单ID集合
|
||||
/// </summary>
|
||||
public List<long> MenuIds { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 创建时间
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 后台角色简要输出
|
||||
/// </summary>
|
||||
public class AdminRoleSimpleOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// 角色ID
|
||||
/// </summary>
|
||||
public long Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 角色名称
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 角色编码
|
||||
/// </summary>
|
||||
public string Code { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 角色类型
|
||||
/// </summary>
|
||||
public AdminRoleTypeEnum Type { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 后台角色查询输入
|
||||
/// </summary>
|
||||
public class AdminRoleQueryInput : PageQueryModel
|
||||
{
|
||||
/// <summary>
|
||||
/// 角色名称
|
||||
/// </summary>
|
||||
public string? Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 角色编码
|
||||
/// </summary>
|
||||
public string? Code { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 角色类型
|
||||
/// </summary>
|
||||
public AdminRoleTypeEnum? Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态
|
||||
/// </summary>
|
||||
public int? Status { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 分配角色菜单输入
|
||||
/// </summary>
|
||||
public class AssignRoleMenusInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 菜单ID集合
|
||||
/// </summary>
|
||||
public List<long> MenuIds { get; set; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 分配用户角色输入
|
||||
/// </summary>
|
||||
public class AssignAdminUserRolesInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 角色ID集合
|
||||
/// </summary>
|
||||
public List<long> RoleIds { get; set; } = [];
|
||||
}
|
||||
112
QYZH.InteractiveMagazine.Models/Dto/Admin/AdminUserDto.cs
Normal file
112
QYZH.InteractiveMagazine.Models/Dto/Admin/AdminUserDto.cs
Normal file
@ -0,0 +1,112 @@
|
||||
using QYZH.InteractiveMagazine.Models.Dto;
|
||||
using QYZH.InteractiveMagazine.Models.Enum;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.Models.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// 管理员创建/更新输入
|
||||
/// </summary>
|
||||
public class AdminUserInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 用户名
|
||||
/// </summary>
|
||||
public string UserName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 密码(创建时必填,更新时为空表示不修改密码)
|
||||
/// </summary>
|
||||
public string? Password { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 管理员类型: SuperAdmin, Editor
|
||||
/// </summary>
|
||||
public AdminUserTypeEnum Type { get; set; } = AdminUserTypeEnum.Editor;
|
||||
|
||||
/// <summary>
|
||||
/// 状态: Active, Inactive
|
||||
/// </summary>
|
||||
public int Status { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 角色ID集合,更新时为null表示不调整角色
|
||||
/// </summary>
|
||||
public List<long>? RoleIds { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 管理员输出
|
||||
/// </summary>
|
||||
public class AdminUserOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// 主键ID
|
||||
/// </summary>
|
||||
public long Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户名
|
||||
/// </summary>
|
||||
public string UserName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 管理员类型
|
||||
/// </summary>
|
||||
public string Type { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 状态
|
||||
/// </summary>
|
||||
public int Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建人
|
||||
/// </summary>
|
||||
public string? CreatedBy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建时间
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 更新人
|
||||
/// </summary>
|
||||
public string? UpdatedBy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 更新时间
|
||||
/// </summary>
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 角色ID集合
|
||||
/// </summary>
|
||||
public List<long> RoleIds { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 角色集合
|
||||
/// </summary>
|
||||
public List<AdminRoleSimpleOutput> Roles { get; set; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 管理员分页查询输入
|
||||
/// </summary>
|
||||
public class AdminUserQueryInput : PageQueryModel
|
||||
{
|
||||
/// <summary>
|
||||
/// 用户名(模糊查询)
|
||||
/// </summary>
|
||||
public string? UserName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 管理员类型
|
||||
/// </summary>
|
||||
public string? Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态
|
||||
/// </summary>
|
||||
public int? Status { get; set; }
|
||||
}
|
||||
80
QYZH.InteractiveMagazine.Models/Dto/Admin/AuthDto.cs
Normal file
80
QYZH.InteractiveMagazine.Models/Dto/Admin/AuthDto.cs
Normal file
@ -0,0 +1,80 @@
|
||||
namespace QYZH.InteractiveMagazine.Models.Dto;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 管理员登录输入
|
||||
/// </summary>
|
||||
public class AdminLoginInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 用户名
|
||||
/// </summary>
|
||||
public string UserName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 密码
|
||||
/// </summary>
|
||||
public string Password { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class AdminLoginOutput
|
||||
{
|
||||
public string Token { get; set; } = string.Empty;
|
||||
|
||||
public long UserId { get; set; }
|
||||
|
||||
public string UserName { get; set; } = string.Empty;
|
||||
|
||||
public string Type { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 角色ID集合
|
||||
/// </summary>
|
||||
public List<long> RoleIds { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 角色集合
|
||||
/// </summary>
|
||||
public List<AdminRoleSimpleOutput> Roles { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 菜单树
|
||||
/// </summary>
|
||||
public List<AdminMenuOutput> Menus { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 权限编码集合
|
||||
/// </summary>
|
||||
public List<string> PermissionCodes { get; set; } = [];
|
||||
}
|
||||
|
||||
public class AdminUserInfoOutput
|
||||
{
|
||||
public long UserId { get; set; }
|
||||
|
||||
public string UserName { get; set; } = string.Empty;
|
||||
|
||||
public string Type { get; set; } = string.Empty;
|
||||
|
||||
public int Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 角色ID集合
|
||||
/// </summary>
|
||||
public List<long> RoleIds { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 角色集合
|
||||
/// </summary>
|
||||
public List<AdminRoleSimpleOutput> Roles { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 菜单树
|
||||
/// </summary>
|
||||
public List<AdminMenuOutput> Menus { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 权限编码集合
|
||||
/// </summary>
|
||||
public List<string> PermissionCodes { get; set; } = [];
|
||||
}
|
||||
121
QYZH.InteractiveMagazine.Models/Dto/Ai/AiBasePromptDto.cs
Normal file
121
QYZH.InteractiveMagazine.Models/Dto/Ai/AiBasePromptDto.cs
Normal file
@ -0,0 +1,121 @@
|
||||
using QYZH.InteractiveMagazine.Models.Dto;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.Models.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// AIPrompt配置创建/更新输入
|
||||
/// </summary>
|
||||
public class AiBasePromptInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 配置标识(如:DEFAULT, READING, COMPREHENSION)
|
||||
/// </summary>
|
||||
public string PromptKey { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 配置名称(如:默认Prompt、阅读理解专用)
|
||||
/// </summary>
|
||||
public string PromptName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Prompt模板内容
|
||||
/// </summary>
|
||||
public string PromptTemplate { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 配置说明
|
||||
/// </summary>
|
||||
public string? Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 优先级(数值越小优先级越高)
|
||||
/// </summary>
|
||||
public int Priority { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否为默认模板
|
||||
/// </summary>
|
||||
public bool IsDefault { get; set; } = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AIPrompt配置输出
|
||||
/// </summary>
|
||||
public class AiBasePromptOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// 主键ID
|
||||
/// </summary>
|
||||
public long Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 配置标识
|
||||
/// </summary>
|
||||
public string PromptKey { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 配置名称
|
||||
/// </summary>
|
||||
public string PromptName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Prompt模板内容
|
||||
/// </summary>
|
||||
public string PromptTemplate { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 配置说明
|
||||
/// </summary>
|
||||
public string? Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 优先级
|
||||
/// </summary>
|
||||
public int Priority { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否为默认模板
|
||||
/// </summary>
|
||||
public bool IsDefault { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态(1=启用, 0=禁用)
|
||||
/// </summary>
|
||||
public int Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建人
|
||||
/// </summary>
|
||||
public string? CreatedBy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建时间
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 更新人
|
||||
/// </summary>
|
||||
public string? UpdatedBy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 更新时间
|
||||
/// </summary>
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AIPrompt配置分页查询输入
|
||||
/// </summary>
|
||||
public class AiBasePromptQueryInput : PageQueryModel
|
||||
{
|
||||
/// <summary>
|
||||
/// 配置标识(精确匹配)
|
||||
/// </summary>
|
||||
public string? PromptKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 配置名称(模糊查询)
|
||||
/// </summary>
|
||||
public string? PromptName { get; set; }
|
||||
}
|
||||
23
QYZH.InteractiveMagazine.Models/Dto/Ai/AiChatDto.cs
Normal file
23
QYZH.InteractiveMagazine.Models/Dto/Ai/AiChatDto.cs
Normal file
@ -0,0 +1,23 @@
|
||||
namespace QYZH.InteractiveMagazine.Models.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// AI聊天输入
|
||||
/// </summary>
|
||||
public class AiChatInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 用户需求文本
|
||||
/// </summary>
|
||||
public string Message { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AI聊天输出
|
||||
/// </summary>
|
||||
public class AiChatOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// AI返回的内容(Markdown格式提示词)
|
||||
/// </summary>
|
||||
public string Content { get; set; } = string.Empty;
|
||||
}
|
||||
91
QYZH.InteractiveMagazine.Models/Dto/Bag/UserBagDto.cs
Normal file
91
QYZH.InteractiveMagazine.Models/Dto/Bag/UserBagDto.cs
Normal file
@ -0,0 +1,91 @@
|
||||
namespace QYZH.InteractiveMagazine.Models.Dto.Bag;
|
||||
|
||||
/// <summary>
|
||||
/// 背包物品输出
|
||||
/// </summary>
|
||||
public class UserBagOutput
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public long ItemId { get; set; }
|
||||
public string ItemType { get; set; } = string.Empty;
|
||||
public int Quantity { get; set; }
|
||||
public string Status { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 关联商品信息(如果有)
|
||||
/// </summary>
|
||||
public BagProductBrief? Product { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 关联皮肤信息(PetBg 类型时)
|
||||
/// </summary>
|
||||
public BagSkinBrief? Skin { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 背包中的商品简要
|
||||
/// </summary>
|
||||
public class BagProductBrief
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string? ImageUrl { get; set; }
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 背包中的皮肤简要
|
||||
/// </summary>
|
||||
public class BagSkinBrief
|
||||
{
|
||||
public long SkinId { get; set; }
|
||||
public string SkinName { get; set; } = string.Empty;
|
||||
public string Rarity { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 皮肤预览图(取第一张图片)
|
||||
/// </summary>
|
||||
public string? PreviewImage { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用物品输入(补签卡)
|
||||
/// </summary>
|
||||
public class UseItemInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 背包物品Id
|
||||
/// </summary>
|
||||
public long BagItemId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 补签目标日期(yyyy-MM-dd,仅补签卡使用)
|
||||
/// </summary>
|
||||
public string? TargetDate { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 换肤输入
|
||||
/// </summary>
|
||||
public class EquipSkinInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 皮肤Id(传0则恢复默认皮肤)
|
||||
/// </summary>
|
||||
public long SkinId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用物品输出
|
||||
/// </summary>
|
||||
public class UseItemOutput
|
||||
{
|
||||
public bool IsSuccess { get; set; }
|
||||
public string Message { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 补签时返回的签到结果
|
||||
/// </summary>
|
||||
public object? Extra { get; set; }
|
||||
}
|
||||
163
QYZH.InteractiveMagazine.Models/Dto/Banner/BannerDto.cs
Normal file
163
QYZH.InteractiveMagazine.Models/Dto/Banner/BannerDto.cs
Normal file
@ -0,0 +1,163 @@
|
||||
using QYZH.InteractiveMagazine.Common.Helpers;
|
||||
using QYZH.InteractiveMagazine.Models.Enum;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.Models.Dto.Banner;
|
||||
|
||||
/// <summary>
|
||||
/// 轮播图创建/更新输入
|
||||
/// </summary>
|
||||
public class BannerInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 标题
|
||||
/// </summary>
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 轮播图OSS路径
|
||||
/// </summary>
|
||||
public string ImageUrl { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 展示位置
|
||||
/// </summary>
|
||||
public BannerPositionEnum Position { get; set; } = BannerPositionEnum.Home;
|
||||
|
||||
/// <summary>
|
||||
/// 跳转类型
|
||||
/// </summary>
|
||||
public BannerTargetTypeEnum TargetType { get; set; } = BannerTargetTypeEnum.None;
|
||||
|
||||
/// <summary>
|
||||
/// 跳转值
|
||||
/// </summary>
|
||||
public string? TargetValue { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 排序值,越小越靠前
|
||||
/// </summary>
|
||||
public int Sort { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 生效开始时间
|
||||
/// </summary>
|
||||
public DateTime? StartTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 生效结束时间
|
||||
/// </summary>
|
||||
public DateTime? EndTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 备注
|
||||
/// </summary>
|
||||
public string? Remark { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 轮播图输出
|
||||
/// </summary>
|
||||
public class BannerOutput
|
||||
{
|
||||
private string _imageUrl = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 主键ID
|
||||
/// </summary>
|
||||
public long Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 标题
|
||||
/// </summary>
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 轮播图完整地址
|
||||
/// </summary>
|
||||
public string ImageUrl
|
||||
{
|
||||
get => DomainHelper.OssFullUrl(_imageUrl);
|
||||
set => _imageUrl = value ?? string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 展示位置
|
||||
/// </summary>
|
||||
public BannerPositionEnum Position { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 跳转类型
|
||||
/// </summary>
|
||||
public BannerTargetTypeEnum TargetType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 跳转值
|
||||
/// </summary>
|
||||
public string? TargetValue { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 排序值,越小越靠前
|
||||
/// </summary>
|
||||
public int Sort { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 生效开始时间
|
||||
/// </summary>
|
||||
public DateTime? StartTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 生效结束时间
|
||||
/// </summary>
|
||||
public DateTime? EndTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 备注
|
||||
/// </summary>
|
||||
public string? Remark { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态
|
||||
/// </summary>
|
||||
public int Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建人
|
||||
/// </summary>
|
||||
public string? CreatedBy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建时间
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 修改人
|
||||
/// </summary>
|
||||
public string? UpdatedBy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 修改时间
|
||||
/// </summary>
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 轮播图分页查询输入
|
||||
/// </summary>
|
||||
public class BannerQueryInput : PageQueryModel
|
||||
{
|
||||
/// <summary>
|
||||
/// 标题
|
||||
/// </summary>
|
||||
public string? Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 展示位置
|
||||
/// </summary>
|
||||
public BannerPositionEnum? Position { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态
|
||||
/// </summary>
|
||||
public int? Status { get; set; }
|
||||
}
|
||||
@ -1,55 +1,217 @@
|
||||
using System.ComponentModel;
|
||||
using System.DirectoryServices.Protocols;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.Models.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// 统一响应模型
|
||||
/// 操作响应基类
|
||||
/// </summary>
|
||||
/// <typeparam name="T">数据类型</typeparam>
|
||||
public class BaseResponse<T>
|
||||
public class BaseResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// 状态码
|
||||
/// 构造函数,初始化操作响应基类
|
||||
/// </summary>
|
||||
public int Code { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 提示信息
|
||||
/// </summary>
|
||||
public string? Message { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 数据
|
||||
/// </summary>
|
||||
public T? Data { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 成功响应
|
||||
/// </summary>
|
||||
/// <param name="data">数据</param>
|
||||
/// <param name="message">提示信息</param>
|
||||
/// <returns>统一响应对象</returns>
|
||||
public static BaseResponse<T> Success(T data, string message = "操作成功")
|
||||
public BaseResponse()
|
||||
{
|
||||
return new BaseResponse<T>
|
||||
code = ResultCode.SUCCESS;
|
||||
}
|
||||
|
||||
#region 公共属性
|
||||
|
||||
/// <summary>
|
||||
/// 操作描述
|
||||
/// </summary>
|
||||
public string message { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 结果Code
|
||||
/// </summary>
|
||||
public ResultCode code { get; set; }
|
||||
|
||||
#endregion 公共属性
|
||||
|
||||
#region 公用方法
|
||||
|
||||
/// <summary>
|
||||
/// 成功
|
||||
/// </summary>
|
||||
public static BaseResponse Success()
|
||||
{
|
||||
Code = 200,
|
||||
Message = message,
|
||||
Data = data
|
||||
};
|
||||
return new BaseResponse { code = ResultCode.SUCCESS, message = "操作成功。" };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 失败响应
|
||||
/// 成功
|
||||
/// </summary>
|
||||
/// <param name="message">提示信息</param>
|
||||
/// <param name="code">状态码</param>
|
||||
/// <returns>统一响应对象</returns>
|
||||
public static BaseResponse<T> Fail(string message, int code = 500)
|
||||
public static BaseResponse Success(string message = "操作成功。")
|
||||
{
|
||||
return new BaseResponse<T>
|
||||
{
|
||||
Code = code,
|
||||
Message = message,
|
||||
Data = default
|
||||
};
|
||||
return new BaseResponse { code = ResultCode.SUCCESS, message = message };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 失败
|
||||
/// </summary>
|
||||
public static BaseResponse Fail(string message, ResultCode code = ResultCode.FAIL)
|
||||
{
|
||||
return new BaseResponse { code = code, message = message };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 权限验证失败
|
||||
/// </summary>
|
||||
public static BaseResponse AuthFail(string message, ResultCode code = ResultCode.FAIL)
|
||||
{
|
||||
return new BaseResponse { code = code, message = message };
|
||||
}
|
||||
|
||||
#endregion 公用方法
|
||||
|
||||
/// <summary>
|
||||
/// 操作结果
|
||||
/// </summary>
|
||||
public bool isSuccess => code == ResultCode.SUCCESS;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 响应结果类型
|
||||
/// </summary>
|
||||
/// <typeparam name="T">
|
||||
/// </typeparam>
|
||||
public class BaseResponse<T> : BaseResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// 操作结果
|
||||
/// </summary>
|
||||
public T? result { get; set; }
|
||||
|
||||
#region 公用方法
|
||||
|
||||
/// <summary>
|
||||
/// 成功
|
||||
/// </summary>
|
||||
public static BaseResponse<T> Success(T result)
|
||||
{
|
||||
return new BaseResponse<T> { code = ResultCode.SUCCESS, message = "", result = result };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 成功
|
||||
/// </summary>
|
||||
public static BaseResponse<T> Success(T result, string message = "操作成功。")
|
||||
{
|
||||
return new BaseResponse<T> { code = ResultCode.SUCCESS, message = message, result = result };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 失败
|
||||
/// </summary>
|
||||
public static BaseResponse<T> Fail(string message = "fail")
|
||||
{
|
||||
return new BaseResponse<T> { code = ResultCode.FAIL, message = message };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 失败 自定义结果
|
||||
/// </summary>
|
||||
public static BaseResponse<T> Fail(ResultCode code, string message = "fail")
|
||||
{
|
||||
return new BaseResponse<T> { code = code, message = message };
|
||||
}
|
||||
|
||||
public static BaseResponse<T> Fail(T result, string message = "操作失败,请稍后再试")
|
||||
{
|
||||
return new BaseResponse<T> { code = ResultCode.FAIL, message = message, result = result };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 权限验证失败
|
||||
/// </summary>
|
||||
public static BaseResponse<T> AuthFail(string message = "Permission authentication failed")
|
||||
{
|
||||
return new BaseResponse<T> { code = ResultCode.OAUTH_FAIL, message = message };
|
||||
}
|
||||
|
||||
///// <summary>
|
||||
///// 成功
|
||||
///// </summary>
|
||||
//public static BaseResponse<T> Assert(T result, string message = "")
|
||||
//{
|
||||
// if (result is bool b)
|
||||
// return b ? BaseResponse<T>.Success(result) : BaseResponse<T>.Fail(message);
|
||||
// if (result is object o)
|
||||
// return (data == null || data.IsNull() == true) ? BaseResponse<T>.Success(result) : BaseResponse<T>.Fail(message);
|
||||
// return new BaseResponse<T> { Code = ResultCode.SUCCESS, Message = message, Result = result };
|
||||
//}
|
||||
|
||||
|
||||
#endregion 公用方法
|
||||
|
||||
/// <summary>
|
||||
/// 操作结果
|
||||
/// </summary>
|
||||
public bool isSuccess => code == ResultCode.SUCCESS;
|
||||
}
|
||||
|
||||
|
||||
public enum ResultCode
|
||||
{
|
||||
[Description("success")]
|
||||
SUCCESS = 200,
|
||||
|
||||
[Description("没有更多数据")]
|
||||
NO_DATA = 210,
|
||||
|
||||
[Description("参数错误")]
|
||||
PARAM_ERROR = 101,
|
||||
|
||||
[Description("验证码错误")]
|
||||
CAPTCHA_ERROR = 103,
|
||||
|
||||
[Description("登录错误")]
|
||||
LOGIN_ERROR = 105,
|
||||
|
||||
[Description("操作失败")]
|
||||
FAIL = 1,
|
||||
|
||||
[Description("服务端出错啦")]
|
||||
GLOBAL_ERROR = 500,
|
||||
|
||||
[Description("自定义异常")]
|
||||
CUSTOM_ERROR = 110,
|
||||
|
||||
[Description("非法请求")]
|
||||
INVALID_REQUEST = 116,
|
||||
|
||||
[Description("授权失败")]
|
||||
OAUTH_FAIL = 201,
|
||||
|
||||
[Description("请先绑定手机号")]
|
||||
PHONE_BIND = 202,
|
||||
|
||||
[Description("未授权")]
|
||||
DENY = 401,
|
||||
|
||||
[Description("授权访问失败")]
|
||||
FORBIDDEN = 403,
|
||||
|
||||
[Description("Bad Request")]
|
||||
BAD_REQUEST = 400,
|
||||
|
||||
[Description("资源不存在")]
|
||||
NOT_FOUND = 404,
|
||||
|
||||
[Description("冲突/重复提交")]
|
||||
CONFLICT = 409,
|
||||
|
||||
[Description("业务规则校验失败")]
|
||||
UNPROCESSABLE_ENTITY = 422,
|
||||
|
||||
[Description("请求过于频繁")]
|
||||
TOO_MANY_REQUESTS = 429,
|
||||
|
||||
[Description("第三方服务异常")]
|
||||
BAD_GATEWAY = 502,
|
||||
|
||||
[Description("服务不可用")]
|
||||
SERVICE_UNAVAILABLE = 503
|
||||
}
|
||||
|
||||
@ -0,0 +1,96 @@
|
||||
using QYZH.InteractiveMagazine.Models.Enum;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.Models.Dto.CheckIn;
|
||||
|
||||
/// <summary>
|
||||
/// 签到配置创建/更新输入
|
||||
/// </summary>
|
||||
public class CheckInConfigInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 连续签到天数
|
||||
/// </summary>
|
||||
public int DayNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 奖励积分数
|
||||
/// </summary>
|
||||
public int RewardPoints { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 额外奖励积分
|
||||
/// </summary>
|
||||
public int BonusPoints { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 配置类型: Daily, Streak
|
||||
/// </summary>
|
||||
public CheckInConfigTypeEnum Type { get; set; } = CheckInConfigTypeEnum.Daily;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 签到配置输出
|
||||
/// </summary>
|
||||
public class CheckInConfigOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// 主键ID
|
||||
/// </summary>
|
||||
public long Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 连续签到天数
|
||||
/// </summary>
|
||||
public int DayNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 奖励积分数
|
||||
/// </summary>
|
||||
public int RewardPoints { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 额外奖励积分
|
||||
/// </summary>
|
||||
public int BonusPoints { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 配置类型: Daily, Streak
|
||||
/// </summary>
|
||||
public CheckInConfigTypeEnum Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态
|
||||
/// </summary>
|
||||
public int Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建人
|
||||
/// </summary>
|
||||
public string? CreatedBy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建时间
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 更新人
|
||||
/// </summary>
|
||||
public string? UpdatedBy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 更新时间
|
||||
/// </summary>
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 签到配置分页查询输入
|
||||
/// </summary>
|
||||
public class CheckInConfigQueryInput : PageQueryModel
|
||||
{
|
||||
/// <summary>
|
||||
/// 配置类型筛选: Daily, Streak
|
||||
/// </summary>
|
||||
public CheckInConfigTypeEnum? Type { get; set; }
|
||||
}
|
||||
151
QYZH.InteractiveMagazine.Models/Dto/CheckIn/CheckInDto.cs
Normal file
151
QYZH.InteractiveMagazine.Models/Dto/CheckIn/CheckInDto.cs
Normal file
@ -0,0 +1,151 @@
|
||||
using QYZH.InteractiveMagazine.Models.Dto;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.Models.Dto.CheckIn;
|
||||
|
||||
/// <summary>
|
||||
/// 签到输出
|
||||
/// </summary>
|
||||
public class CheckInOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// 签到记录Id
|
||||
/// </summary>
|
||||
public long RecordId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 签到日期
|
||||
/// </summary>
|
||||
public DateTime CheckInDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 连续签到天数
|
||||
/// </summary>
|
||||
public int ConsecutiveDays { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 本次获得积分
|
||||
/// </summary>
|
||||
public int PointsAwarded { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 本次获得成长值
|
||||
/// </summary>
|
||||
public int GrowthPointsAwarded { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 签到后用户积分余额
|
||||
/// </summary>
|
||||
public int PointsBalance { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 签到后用户成长值余额
|
||||
/// </summary>
|
||||
public int GrowthPointsBalance { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否有宠物(成长值是否喂养到宠物)
|
||||
/// </summary>
|
||||
public bool HasPet { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 宠物是否触发进化
|
||||
/// </summary>
|
||||
public bool HasEvolved { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 进化后的阶段名称(未进化则为空)
|
||||
/// </summary>
|
||||
public string? EvolvedStageName { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 签到信息输出(查询用)
|
||||
/// </summary>
|
||||
public class CheckInInfoOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// 今日是否已签到
|
||||
/// </summary>
|
||||
public bool HasCheckedInToday { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 当前连续签到天数
|
||||
/// </summary>
|
||||
public int ConsecutiveDays { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 累计签到天数
|
||||
/// </summary>
|
||||
public int TotalCheckInDays { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户当前积分余额
|
||||
/// </summary>
|
||||
public int PointsBalance { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户当前成长值余额
|
||||
/// </summary>
|
||||
public int GrowthPointsBalance { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 最近签到记录
|
||||
/// </summary>
|
||||
public List<CheckInRecordOutput> RecentRecords { get; set; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 补签输入
|
||||
/// </summary>
|
||||
public class MakeUpCheckInInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 补签目标日期(yyyy-MM-dd)
|
||||
/// </summary>
|
||||
public DateTime TargetDate { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 签到记录输出
|
||||
/// </summary>
|
||||
public class CheckInRecordOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// 记录Id
|
||||
/// </summary>
|
||||
public long Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 签到日期
|
||||
/// </summary>
|
||||
public DateTime CheckInDate { get; set; }
|
||||
/// <summary>
|
||||
/// 签到时间
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 连续签到天数
|
||||
/// </summary>
|
||||
public int ConsecutiveDays { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 本次获得积分
|
||||
/// </summary>
|
||||
public int PointsAwarded { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 本次获得成长值
|
||||
/// </summary>
|
||||
public int GrowthPointsAwarded { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 签到类型: Normal, MakeUp
|
||||
/// </summary>
|
||||
public string Type { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 状态
|
||||
/// </summary>
|
||||
public string Status { get; set; } = string.Empty;
|
||||
}
|
||||
295
QYZH.InteractiveMagazine.Models/Dto/Community/CommunityDto.cs
Normal file
295
QYZH.InteractiveMagazine.Models/Dto/Community/CommunityDto.cs
Normal file
@ -0,0 +1,295 @@
|
||||
namespace QYZH.InteractiveMagazine.Models.Dto;
|
||||
|
||||
#region 后台管理 DTO
|
||||
|
||||
/// <summary>
|
||||
/// 社区消息分页查询输入(后台)
|
||||
/// </summary>
|
||||
public class AdminMessageQueryInput : PageQueryModel
|
||||
{
|
||||
/// <summary>
|
||||
/// 期刊ID
|
||||
/// </summary>
|
||||
public long? JournalId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 消息类型: Article, Quote, Announcement
|
||||
/// </summary>
|
||||
public string? Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态: 0=审核中, 1=已通过, 2=已冻结
|
||||
/// </summary>
|
||||
public int? Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否精选: 0=否, 1=是
|
||||
/// </summary>
|
||||
public int? IsFeatured { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用
|
||||
/// </summary>
|
||||
public bool? IsActive { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 用户ID
|
||||
/// </summary>
|
||||
public long? UserId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 社区消息详情输出(后台)
|
||||
/// </summary>
|
||||
public class AdminMessageDetailOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// 主键ID
|
||||
/// </summary>
|
||||
public long Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 期刊ID
|
||||
/// </summary>
|
||||
public long JournalId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户ID
|
||||
/// </summary>
|
||||
public long UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户实例期刊ID
|
||||
/// </summary>
|
||||
public long UserJournalId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 期刊任务ID
|
||||
/// </summary>
|
||||
public long JournalTaskId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户期刊任务回答ID
|
||||
/// </summary>
|
||||
public long JournalTaskAnswerId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 消息内容
|
||||
/// </summary>
|
||||
public string Content { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 配图
|
||||
/// </summary>
|
||||
public string? ImageUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 排序权重
|
||||
/// </summary>
|
||||
public int SortOrder { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用
|
||||
/// </summary>
|
||||
public bool IsActive { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 消息类型
|
||||
/// </summary>
|
||||
public string Type { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 点赞数
|
||||
/// </summary>
|
||||
public int LikeCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否精选
|
||||
/// </summary>
|
||||
public int IsFeatured { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态: 0=审核中, 1=已通过, 2=已冻结
|
||||
/// </summary>
|
||||
public int Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建人
|
||||
/// </summary>
|
||||
public string? CreatedBy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建时间
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 更新人
|
||||
/// </summary>
|
||||
public string? UpdatedBy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 更新时间
|
||||
/// </summary>
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置精选入参
|
||||
/// </summary>
|
||||
public class AdminSetFeaturedInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 是否精选: 0=否, 1=是
|
||||
/// </summary>
|
||||
public int IsFeatured { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置排序权重入参
|
||||
/// </summary>
|
||||
public class AdminSetSortOrderInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 排序权重
|
||||
/// </summary>
|
||||
public int SortOrder { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 冻结/解冻入参
|
||||
/// </summary>
|
||||
public class AdminFreezeInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 状态: 1=解冻(通过), 2=冻结
|
||||
/// </summary>
|
||||
public int Status { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量发布消息入参
|
||||
/// </summary>
|
||||
public class AdminBatchPublishInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 消息ID列表
|
||||
/// </summary>
|
||||
public List<long> Ids { get; set; } = new();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 微信端 DTO
|
||||
|
||||
/// <summary>
|
||||
/// 微信端社区Feed消息输出
|
||||
/// </summary>
|
||||
public class WxFeedMessageOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// 消息ID
|
||||
/// </summary>
|
||||
public long Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 期刊ID
|
||||
/// </summary>
|
||||
public long JournalId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 作者用户ID
|
||||
/// </summary>
|
||||
public long UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 消息内容
|
||||
/// </summary>
|
||||
public string Content { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 配图
|
||||
/// </summary>
|
||||
public string? ImageUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 消息类型
|
||||
/// </summary>
|
||||
public string Type { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 点赞数
|
||||
/// </summary>
|
||||
public int LikeCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否精选
|
||||
/// </summary>
|
||||
public int IsFeatured { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 当前用户是否已点赞
|
||||
/// </summary>
|
||||
public bool IsLiked { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建时间
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 微信端社区Feed输出(含游标)
|
||||
/// </summary>
|
||||
public class WxFeedOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// 消息列表
|
||||
/// </summary>
|
||||
public List<WxFeedMessageOutput> Messages { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 下一页游标(为null表示没有更多数据)
|
||||
/// </summary>
|
||||
public long? NextCursor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否还有更多数据
|
||||
/// </summary>
|
||||
public bool HasMore { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 微信端点赞入参
|
||||
/// </summary>
|
||||
public class WxLikeInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 消息ID
|
||||
/// </summary>
|
||||
public long MessageId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 微信端点赞输出
|
||||
/// </summary>
|
||||
public class WxLikeOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// 消息ID
|
||||
/// </summary>
|
||||
public long MessageId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 当前点赞数
|
||||
/// </summary>
|
||||
public int LikeCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否已点赞
|
||||
/// </summary>
|
||||
public bool IsLiked { get; set; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
@ -0,0 +1,241 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using QYZH.InteractiveMagazine.Models.Enum;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.Models.Dto.Compensation;
|
||||
|
||||
/// <summary>
|
||||
/// 补偿任务分页查询输入
|
||||
/// </summary>
|
||||
public class CompensationTaskQueryInput : PageQueryModel
|
||||
{
|
||||
/// <summary>
|
||||
/// 按用户Id筛选
|
||||
/// </summary>
|
||||
public long? UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 按任务类型筛选(PetFeeding/UserPoints/UserGrowth/SendNotification)
|
||||
/// </summary>
|
||||
public CompensationTaskTypeEnum? TaskType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 按状态筛选(Pending/Processing/Success/Failed/Cancelled)
|
||||
/// </summary>
|
||||
public CompensationTaskStatusEnum? Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 按业务来源筛选(CheckIn/Purchase/Activity等)
|
||||
/// </summary>
|
||||
public string? BusinessSource { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 手动重试补偿任务输入
|
||||
/// </summary>
|
||||
public class CompensationRetryInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 重试原因
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "重试原因不能为空")]
|
||||
public string Reason { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 标记补偿任务已解决输入
|
||||
/// </summary>
|
||||
public class CompensationResolveInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 处理说明
|
||||
/// </summary>
|
||||
[Required(ErrorMessage = "处理说明不能为空")]
|
||||
public string ResolveNote { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 补偿任务管理输出(含用户昵称)
|
||||
/// </summary>
|
||||
public class CompensationManageOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// 任务Id
|
||||
/// </summary>
|
||||
public long Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 任务类型
|
||||
/// </summary>
|
||||
public CompensationTaskTypeEnum TaskType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 业务来源
|
||||
/// </summary>
|
||||
public string BusinessSource { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 关联业务Id
|
||||
/// </summary>
|
||||
public string? BusinessId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户Id
|
||||
/// </summary>
|
||||
public long UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户昵称
|
||||
/// </summary>
|
||||
public string? UserName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 处理参数(JSON)
|
||||
/// </summary>
|
||||
public string Payload { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 异常消息
|
||||
/// </summary>
|
||||
public string ErrorMessage { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 异常来源
|
||||
/// </summary>
|
||||
public string ErrorSource { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 已重试次数
|
||||
/// </summary>
|
||||
public int RetryCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 最大重试次数
|
||||
/// </summary>
|
||||
public int MaxRetries { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态
|
||||
/// </summary>
|
||||
public CompensationTaskStatusEnum Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 最后处理时间
|
||||
/// </summary>
|
||||
public DateTime? ProcessedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 计划执行时间
|
||||
/// </summary>
|
||||
public DateTime? ScheduledAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 处理结果
|
||||
/// </summary>
|
||||
public string? ResultMessage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建时间
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 补偿任务详情输出(含审计字段)
|
||||
/// </summary>
|
||||
public class CompensationManageDetailOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// 任务Id
|
||||
/// </summary>
|
||||
public long Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 任务类型
|
||||
/// </summary>
|
||||
public CompensationTaskTypeEnum TaskType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 业务来源
|
||||
/// </summary>
|
||||
public string BusinessSource { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 关联业务Id
|
||||
/// </summary>
|
||||
public string? BusinessId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户Id
|
||||
/// </summary>
|
||||
public long UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户昵称
|
||||
/// </summary>
|
||||
public string? UserName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 处理参数(JSON)
|
||||
/// </summary>
|
||||
public string Payload { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 异常消息
|
||||
/// </summary>
|
||||
public string ErrorMessage { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 异常来源
|
||||
/// </summary>
|
||||
public string ErrorSource { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 已重试次数
|
||||
/// </summary>
|
||||
public int RetryCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 最大重试次数
|
||||
/// </summary>
|
||||
public int MaxRetries { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态
|
||||
/// </summary>
|
||||
public CompensationTaskStatusEnum Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 最后处理时间
|
||||
/// </summary>
|
||||
public DateTime? ProcessedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 计划执行时间
|
||||
/// </summary>
|
||||
public DateTime? ScheduledAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 处理结果
|
||||
/// </summary>
|
||||
public string? ResultMessage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建时间
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建人
|
||||
/// </summary>
|
||||
public string? CreatedBy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 修改人
|
||||
/// </summary>
|
||||
public string? UpdatedBy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 修改时间
|
||||
/// </summary>
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
}
|
||||
@ -0,0 +1,128 @@
|
||||
using QYZH.InteractiveMagazine.Models.Enum;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.Models.Dto.Compensation;
|
||||
|
||||
/// <summary>
|
||||
/// 创建补偿任务输入
|
||||
/// </summary>
|
||||
public class CreateCompensationTaskInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 任务类型(使用 CompensationTaskTypeEnum 枚举)
|
||||
/// </summary>
|
||||
public CompensationTaskTypeEnum TaskType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 业务来源(如 CheckIn、Purchase)
|
||||
/// </summary>
|
||||
public string BusinessSource { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 关联业务记录Id
|
||||
/// </summary>
|
||||
public string? BusinessId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 关联用户Id
|
||||
/// </summary>
|
||||
public long UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 处理参数(匿名对象或字典,会自动序列化为JSON)
|
||||
/// </summary>
|
||||
public object Payload { get; set; } = new { };
|
||||
|
||||
/// <summary>
|
||||
/// 异常消息
|
||||
/// </summary>
|
||||
public string ErrorMessage { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 异常来源(类名.方法名,如 CheckInService.CheckInAsync)
|
||||
/// </summary>
|
||||
public string ErrorSource { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 最大重试次数(默认3次)
|
||||
/// </summary>
|
||||
public int MaxRetries { get; set; } = 3;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 补偿任务输出
|
||||
/// </summary>
|
||||
public class CompensationTaskOutput
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public CompensationTaskTypeEnum TaskType { get; set; }
|
||||
public string BusinessSource { get; set; } = string.Empty;
|
||||
public string? BusinessId { get; set; }
|
||||
public long UserId { get; set; }
|
||||
public string Payload { get; set; } = string.Empty;
|
||||
public string ErrorMessage { get; set; } = string.Empty;
|
||||
public string ErrorSource { get; set; } = string.Empty;
|
||||
public int RetryCount { get; set; }
|
||||
public int MaxRetries { get; set; }
|
||||
public CompensationTaskStatusEnum Status { get; set; }
|
||||
public DateTime? ProcessedAt { get; set; }
|
||||
public DateTime? ScheduledAt { get; set; }
|
||||
public string? ResultMessage { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询补偿任务输入(供外部项目按条件拉取)
|
||||
/// </summary>
|
||||
public class GetCompensationTasksInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 按用户Id筛选
|
||||
/// </summary>
|
||||
public long? UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 按状态筛选
|
||||
/// </summary>
|
||||
public CompensationTaskStatusEnum? Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 按任务类型筛选
|
||||
/// </summary>
|
||||
public CompensationTaskTypeEnum? TaskType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 按业务来源筛选
|
||||
/// </summary>
|
||||
public string? BusinessSource { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取数量上限(默认50)
|
||||
/// </summary>
|
||||
public int Limit { get; set; } = 50;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新补偿任务状态输入(供外部处理项目回调)
|
||||
/// </summary>
|
||||
public class UpdateCompensationStatusInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 目标状态(使用 CompensationTaskStatusEnum 枚举)
|
||||
/// </summary>
|
||||
public CompensationTaskStatusEnum Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 处理结果描述
|
||||
/// </summary>
|
||||
public string? ResultMessage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 更新重试次数(可选)
|
||||
/// </summary>
|
||||
public int? RetryCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 下次计划执行时间(用于退避重试,可选)
|
||||
/// </summary>
|
||||
public DateTime? ScheduledAt { get; set; }
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.Models.Dto.DotMatrix
|
||||
{
|
||||
public class DotMatrixNoteJournalReportInput
|
||||
{
|
||||
public long Id { get; set; }
|
||||
|
||||
public bool Success { get; set; }
|
||||
|
||||
public string FileKey { get; set; }
|
||||
|
||||
public string FileUrl { get; set; }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,73 @@
|
||||
|
||||
using QYZH.InteractiveMagazine.Common.Helpers;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.Models.Dto.DotMatrix
|
||||
{
|
||||
public class DotMatrixOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// 方法
|
||||
/// </summary>
|
||||
public string Method { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 名称
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// id
|
||||
/// </summary>
|
||||
public long NoteId { get; set; }
|
||||
|
||||
private string _pdfUrl;
|
||||
/// <summary>
|
||||
/// pdf地址
|
||||
/// </summary>
|
||||
public string PdfUrl
|
||||
{
|
||||
get => DomainHelper.OssFullUrl(_pdfUrl);
|
||||
set => _pdfUrl = value;
|
||||
}
|
||||
|
||||
public List<DotMatrixMqOutput> DotMatrixs { get; set; } = new List<DotMatrixMqOutput>();
|
||||
}
|
||||
|
||||
public class DotMatrixMqOutput
|
||||
{
|
||||
public long DotId { get; set; }
|
||||
|
||||
private string _fileAddress;
|
||||
/// <summary>
|
||||
/// 点阵xml地址
|
||||
/// </summary>
|
||||
public string FileAddress
|
||||
{
|
||||
get => DomainHelper.OssFullUrl(_fileAddress);
|
||||
set => _fileAddress = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 页
|
||||
/// </summary>
|
||||
public List<DotMatrixPageDto> Pages { get; set; } = new List<DotMatrixPageDto>();
|
||||
}
|
||||
|
||||
public class DotMatrixPageDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 页码
|
||||
/// </summary>
|
||||
public string PageNo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 连续数量
|
||||
/// </summary>
|
||||
public int PageNum { get; set; }
|
||||
}
|
||||
}
|
||||
280
QYZH.InteractiveMagazine.Models/Dto/Journal/BindJournalDto.cs
Normal file
280
QYZH.InteractiveMagazine.Models/Dto/Journal/BindJournalDto.cs
Normal file
@ -0,0 +1,280 @@
|
||||
using QYZH.InteractiveMagazine.Models.Enum;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.Models.Dto;
|
||||
|
||||
/// <summary>
|
||||
/// 绑定期刊输入DTO
|
||||
/// </summary>
|
||||
public class BindJournalInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 期刊模板Id(扫码解析的期刊定义Id)
|
||||
/// </summary>
|
||||
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)]
|
||||
public long JournalId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 实例化期刊Id(扫码解析的具体期刊实例Id,可选)
|
||||
/// </summary>
|
||||
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)]
|
||||
public long Id { get; set; }
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 绑定期刊输出DTO
|
||||
/// </summary>
|
||||
public class BindJournalOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// 绑定记录Id
|
||||
/// </summary>
|
||||
public long Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户Id
|
||||
/// </summary>
|
||||
public long UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 期刊模板Id
|
||||
/// </summary>
|
||||
public long JournalId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 关联类型
|
||||
/// </summary>
|
||||
public string Type { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 状态
|
||||
/// </summary>
|
||||
public string Status { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 绑定时间
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用户绑定期刊消息
|
||||
/// </summary>
|
||||
public class BindJournalMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// 用户Id
|
||||
/// </summary>
|
||||
public long UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 期刊Id
|
||||
/// </summary>
|
||||
public long JournalId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 期刊开始时间
|
||||
/// </summary>
|
||||
public DateTime? StartTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 期刊结束时间
|
||||
/// </summary>
|
||||
public DateTime? EndTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户作答信息上传地址
|
||||
/// </summary>
|
||||
public string UploadDomain { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用户期刊关联查询输入DTO
|
||||
/// </summary>
|
||||
public class UserJournalQueryInput : PageQueryModel
|
||||
{
|
||||
/// <summary>
|
||||
/// 期刊模板Id
|
||||
/// </summary>
|
||||
public long? JournalId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 实例化期刊Id
|
||||
/// </summary>
|
||||
public long Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 关联类型: Read, Favorite, Subscribe
|
||||
/// </summary>
|
||||
public string? Type { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成期刊二维码输入DTO
|
||||
/// </summary>
|
||||
public class CreateUserJournalQrCodeInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 期刊Id
|
||||
/// </summary>
|
||||
public long JournalId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 生成数量
|
||||
/// </summary>
|
||||
public int Count { get; set; } = 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成期刊二维码提交结果
|
||||
/// </summary>
|
||||
public class CreateUserJournalQrCodeOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// 期刊Id
|
||||
/// </summary>
|
||||
public long JournalId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 请求生成数量
|
||||
/// </summary>
|
||||
public int RequestedCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 已提交生成数量
|
||||
/// </summary>
|
||||
public int AcceptedCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 二维码记录Id列表
|
||||
/// </summary>
|
||||
[JsonNumberHandling(JsonNumberHandling.WriteAsString)]
|
||||
public List<long> RecordIds { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 是否后台生成
|
||||
/// </summary>
|
||||
public bool IsAsync { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 提示信息
|
||||
/// </summary>
|
||||
public string Message { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 期刊二维码查询输入DTO
|
||||
/// </summary>
|
||||
public class GenerateUserJournalQrCodeMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// 二维码记录Id列表
|
||||
/// </summary>
|
||||
public List<long> RecordIds { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 操作人名称
|
||||
/// </summary>
|
||||
public string OperatorName { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 期刊二维码查询输入DTO
|
||||
/// </summary>
|
||||
public class UserJournalQrCodeQueryInput : PageQueryModel
|
||||
{
|
||||
/// <summary>
|
||||
/// 期刊Id
|
||||
/// </summary>
|
||||
public long? JournalId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户Id
|
||||
/// </summary>
|
||||
public long? UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否已绑定用户
|
||||
/// </summary>
|
||||
public bool? IsBound { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态
|
||||
/// </summary>
|
||||
public int? Status { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 期刊二维码输出DTO
|
||||
/// </summary>
|
||||
public class UserJournalQrCodeOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// 二维码记录Id
|
||||
/// </summary>
|
||||
[JsonNumberHandling(JsonNumberHandling.WriteAsString)]
|
||||
public long Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 期刊Id
|
||||
/// </summary>
|
||||
public long JournalId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 期刊名称
|
||||
/// </summary>
|
||||
public string JournalName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 用户Id
|
||||
/// </summary>
|
||||
public long? UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户昵称
|
||||
/// </summary>
|
||||
public string? UserName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 关联类型
|
||||
/// </summary>
|
||||
public string Type { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 状态
|
||||
/// </summary>
|
||||
public string Status { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 是否已绑定用户
|
||||
/// </summary>
|
||||
public bool IsBound { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 二维码内容
|
||||
/// </summary>
|
||||
public string QrCodeUrl { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 创建时间
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 绑定时间
|
||||
/// </summary>
|
||||
public DateTime? BoundAt { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除期刊二维码输入DTO
|
||||
/// </summary>
|
||||
public class DeleteUserJournalQrCodeInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 二维码记录Id列表
|
||||
/// </summary>
|
||||
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)]
|
||||
public List<long> Ids { get; set; } = [];
|
||||
}
|
||||
@ -0,0 +1,112 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.Models.Dto.Journal
|
||||
{
|
||||
|
||||
public class JournalImportDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 书Id
|
||||
/// </summary>
|
||||
public long JournalId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// pdf地址
|
||||
/// </summary>
|
||||
public string PdfUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 从多少页开始
|
||||
/// </summary>
|
||||
public int Index { get; set; }
|
||||
|
||||
public List<JournalCatalogAddInput> JournalCatalogs { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 目录导入
|
||||
/// </summary>
|
||||
public class JournalCatalogImportDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 一级目录
|
||||
/// </summary>
|
||||
public string ParentName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 二级目录
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 页码
|
||||
/// </summary>
|
||||
public int PageNum { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public long PagePageId { get; set; }
|
||||
}
|
||||
|
||||
public class JournalCatalogAddInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 目录名称
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 级别
|
||||
/// </summary>
|
||||
public int Level { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 类型0-目录 1-页
|
||||
/// </summary>
|
||||
public int Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// url
|
||||
/// </summary>
|
||||
public string Url { get; set; }
|
||||
}
|
||||
|
||||
public class JournalCatalogInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 书Id
|
||||
/// </summary>
|
||||
public long JournalId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 目录名称
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 父Id;无限级别
|
||||
/// </summary>
|
||||
public long ParentId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 等级
|
||||
/// </summary>
|
||||
public int Level { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 指定插入位置的索引(从0开始)不传或传null时默认追加到末尾
|
||||
/// </summary>
|
||||
public int? Position { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 类型0-目录 1-页
|
||||
/// </summary>
|
||||
public int Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// url
|
||||
/// </summary>
|
||||
public string Url { get; set; }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,71 @@
|
||||
using MiniExcelLibs.Attributes;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.Models.Dto.Journal
|
||||
{
|
||||
public partial class JournalCatalogTreeListDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Desc:主键id
|
||||
/// </summary>
|
||||
[ExcelIgnore]
|
||||
public long Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Desc:书id
|
||||
/// </summary>
|
||||
[ExcelIgnore]
|
||||
public long JournalId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Desc:一级目录名称
|
||||
/// </summary>
|
||||
[DisplayName("目录名称")]
|
||||
[Required]
|
||||
public string? ParentName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Desc:目录名称
|
||||
/// </summary>
|
||||
[DisplayName("二级目录")]
|
||||
[Required]
|
||||
public string Name { get; set; }
|
||||
|
||||
[ExcelIgnore]
|
||||
public long PageId { get; set; }
|
||||
|
||||
[DisplayName("页码")]
|
||||
[Required]
|
||||
public int PageNum { get; set; }
|
||||
|
||||
[ExcelIgnore]
|
||||
public long? ParentId { get; set; }
|
||||
}
|
||||
|
||||
|
||||
public partial class IcrJournalCatalogTreeDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Desc:主键id
|
||||
/// </summary>
|
||||
public long Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 名称
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 层级
|
||||
/// </summary>
|
||||
public int Level { get; set; }
|
||||
|
||||
public List<IcrJournalCatalogTreeDto> Child { get; set; }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.Models.Dto.Journal
|
||||
{
|
||||
public class JournalCatalogUpdateInput
|
||||
{
|
||||
public long Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 目录名称
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
}
|
||||
}
|
||||
154
QYZH.InteractiveMagazine.Models/Dto/Journal/JournalDto.cs
Normal file
154
QYZH.InteractiveMagazine.Models/Dto/Journal/JournalDto.cs
Normal file
@ -0,0 +1,154 @@
|
||||
using QYZH.InteractiveMagazine.Common.Extensions;
|
||||
using QYZH.InteractiveMagazine.Common.Helpers;
|
||||
using QYZH.InteractiveMagazine.Models.Enum;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace QYZH.InteractiveMagazine.Models.Dto.Journal
|
||||
{
|
||||
public class JournalDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Desc:主键id
|
||||
/// </summary>
|
||||
public long Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Desc:书籍名称
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Desc:标题
|
||||
/// </summary>
|
||||
public string? Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Desc:总页数
|
||||
/// </summary>
|
||||
public int TotalPage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Desc:校验页数
|
||||
/// </summary>
|
||||
public int VerifyPage { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Desc:状态
|
||||
/// </summary>
|
||||
public JournalStatusEnum Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态
|
||||
/// </summary>
|
||||
public string ShowStatus => Status.GetDescription();
|
||||
|
||||
private string _pdfUrl;
|
||||
/// <summary>
|
||||
/// pdf预览地址
|
||||
/// </summary>
|
||||
public string PdfUrl
|
||||
{
|
||||
get => DomainHelper.OssFullUrl(_pdfUrl);
|
||||
set => _pdfUrl = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Desc:宽度
|
||||
/// </summary>
|
||||
public float Width { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Desc:高度
|
||||
/// </summary>
|
||||
public float Height { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Desc:乐观锁
|
||||
/// </summary>
|
||||
public int Revision { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Desc:审查结果(默认0 1通过 -1驳回)
|
||||
/// </summary>
|
||||
public int Result { get; set; }
|
||||
|
||||
private string _cover;
|
||||
/// <summary>
|
||||
/// 封面
|
||||
/// </summary>
|
||||
public string Cover
|
||||
{
|
||||
get => DomainHelper.OssFullUrl(_cover);
|
||||
set => _cover = value;
|
||||
}
|
||||
|
||||
private string _backCover;
|
||||
/// <summary>
|
||||
/// 封底
|
||||
/// </summary>
|
||||
public string BackCover
|
||||
{
|
||||
get => DomainHelper.OssFullUrl(_backCover);
|
||||
set => _backCover = value;
|
||||
}
|
||||
|
||||
|
||||
private string _pdfPreviewUrl;
|
||||
|
||||
/// <summary>
|
||||
/// pdf预览地址
|
||||
/// </summary>
|
||||
public string PdfPreviewUrl
|
||||
{
|
||||
get => DomainHelper.OssFullUrl(_pdfPreviewUrl);
|
||||
set => _pdfPreviewUrl = value;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Desc:
|
||||
/// </summary>
|
||||
public string? Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Desc:下载书籍页码点阵码PDF文件名称
|
||||
/// </summary>
|
||||
public string? DownloadJournalPagePdfName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 发布开始时间
|
||||
/// </summary>
|
||||
public DateTime? StartTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 发布结束时间
|
||||
/// </summary>
|
||||
public DateTime? EndTime { get; set; }
|
||||
|
||||
|
||||
public string CreatedBy { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
||||
|
||||
public class JournalPublishBookMessage
|
||||
{
|
||||
public long BookId { get; set; }
|
||||
public DateTime StartTime { get; set; }
|
||||
public DateTime EndTime { get; set; }
|
||||
}
|
||||
|
||||
public class JournalPublishBookPageMessage
|
||||
{
|
||||
public long BookId { get; set; }
|
||||
public long PageId { get; set; }
|
||||
public string Url { get; set; }
|
||||
public string? PageNo { get; set; }
|
||||
public string? Layout { get; set; }
|
||||
public string QuestionNo { get; set; }
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user