From a04b4be7e5f793b55c12c9e85dfffee9dba69532 Mon Sep 17 00:00:00 2001 From: glz <694770232@qq.com> Date: Mon, 6 Jul 2026 14:21:21 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=8B=86=E5=88=86=E5=BE=AE?= =?UTF-8?q?=E4=BF=A1API=E5=88=B0=E7=8B=AC=E7=AB=8B=E9=A1=B9=E7=9B=AE?= =?UTF-8?q?=E5=B9=B6=E8=BF=81=E7=A7=BB=E7=9B=B8=E5=85=B3=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 新增WeChatApi独立项目,将原WebApi中的微信相关控制器迁移至新项目 2. 新增后台权限管理相关实体、服务接口和基础控制器 3. 完善管理员用户服务,增加角色关联查询和赋值逻辑 4. 修复WebApi Swagger文档过滤微信API版本的问题 5. 更新解决方案文件,添加新的微信API项目引用 6. 新增微信API基础配置文件和项目属性配置 --- CommunityMessage_TestData.sql | 129 ----- .../IAdminPermissionService.cs | 80 +++ .../Dto/Admin/AdminPermissionDto.cs | 270 ++++++++++ .../Dto/Admin/AdminUserDto.cs | 15 + .../Dto/Admin/AuthDto.cs | 40 ++ .../Entity/AdminMenu.cs | 66 +++ .../Entity/AdminRole.cs | 31 ++ .../Entity/AdminRoleMenu.cs | 24 + .../Entity/AdminUserRole.cs | 24 + .../AdminAuthService.cs | 24 +- .../AdminPermissionService.cs | 495 ++++++++++++++++++ .../AdminUserService.cs | 55 +- .../Controllers}/BagController.cs | 2 +- .../Controllers}/CheckInController.cs | 2 +- .../Controllers}/CommunityController.cs | 2 +- .../Controllers}/JournalController.cs | 2 +- .../Controllers}/MallController.cs | 2 +- .../Controllers}/MedalController.cs | 2 +- .../Controllers}/PetController.cs | 2 +- .../Controllers}/UserAnswerTaskController.cs | 2 +- .../Controllers}/WeChatAuthController.cs | 2 +- .../Controllers}/WeChatBaseController.cs | 2 +- QYZH.InteractiveMagazine.WeChatApi/Program.cs | 189 +++++++ .../Properties/launchSettings.json | 41 ++ .../QYZH.InteractiveMagazine.WeChatApi.csproj | 25 + .../appsettings.Development.json | 8 + .../appsettings.json | 71 +++ .../medal-rule-config.json | 43 ++ .../Controllers/PermissionController.cs | 139 +++++ QYZH.InteractiveMagazine.WebApi/Program.cs | 5 +- .../Consumers/JournalTaskReceiveConsumer.cs | 32 ++ QYZH.InteractiveMagazine.slnx | 1 + 32 files changed, 1669 insertions(+), 158 deletions(-) delete mode 100644 CommunityMessage_TestData.sql create mode 100644 QYZH.InteractiveMagazine.IService/IAdminPermissionService.cs create mode 100644 QYZH.InteractiveMagazine.Models/Dto/Admin/AdminPermissionDto.cs create mode 100644 QYZH.InteractiveMagazine.Models/Entity/AdminMenu.cs create mode 100644 QYZH.InteractiveMagazine.Models/Entity/AdminRole.cs create mode 100644 QYZH.InteractiveMagazine.Models/Entity/AdminRoleMenu.cs create mode 100644 QYZH.InteractiveMagazine.Models/Entity/AdminUserRole.cs create mode 100644 QYZH.InteractiveMagazine.Service/AdminPermissionService.cs rename {QYZH.InteractiveMagazine.WebApi/Controllers/WeChat => QYZH.InteractiveMagazine.WeChatApi/Controllers}/BagController.cs (96%) rename {QYZH.InteractiveMagazine.WebApi/Controllers/WeChat => QYZH.InteractiveMagazine.WeChatApi/Controllers}/CheckInController.cs (98%) rename {QYZH.InteractiveMagazine.WebApi/Controllers/WeChat => QYZH.InteractiveMagazine.WeChatApi/Controllers}/CommunityController.cs (98%) rename {QYZH.InteractiveMagazine.WebApi/Controllers/WeChat => QYZH.InteractiveMagazine.WeChatApi/Controllers}/JournalController.cs (98%) rename {QYZH.InteractiveMagazine.WebApi/Controllers/WeChat => QYZH.InteractiveMagazine.WeChatApi/Controllers}/MallController.cs (97%) rename {QYZH.InteractiveMagazine.WebApi/Controllers/WeChat => QYZH.InteractiveMagazine.WeChatApi/Controllers}/MedalController.cs (98%) rename {QYZH.InteractiveMagazine.WebApi/Controllers/WeChat => QYZH.InteractiveMagazine.WeChatApi/Controllers}/PetController.cs (98%) rename {QYZH.InteractiveMagazine.WebApi/Controllers/WeChat => QYZH.InteractiveMagazine.WeChatApi/Controllers}/UserAnswerTaskController.cs (99%) rename {QYZH.InteractiveMagazine.WebApi/Controllers/WeChat => QYZH.InteractiveMagazine.WeChatApi/Controllers}/WeChatAuthController.cs (99%) rename {QYZH.InteractiveMagazine.WebApi/Controllers/WeChat => QYZH.InteractiveMagazine.WeChatApi/Controllers}/WeChatBaseController.cs (97%) create mode 100644 QYZH.InteractiveMagazine.WeChatApi/Program.cs create mode 100644 QYZH.InteractiveMagazine.WeChatApi/Properties/launchSettings.json create mode 100644 QYZH.InteractiveMagazine.WeChatApi/QYZH.InteractiveMagazine.WeChatApi.csproj create mode 100644 QYZH.InteractiveMagazine.WeChatApi/appsettings.Development.json create mode 100644 QYZH.InteractiveMagazine.WeChatApi/appsettings.json create mode 100644 QYZH.InteractiveMagazine.WeChatApi/medal-rule-config.json create mode 100644 QYZH.InteractiveMagazine.WebApi/Controllers/PermissionController.cs diff --git a/CommunityMessage_TestData.sql b/CommunityMessage_TestData.sql deleted file mode 100644 index 375122f..0000000 --- a/CommunityMessage_TestData.sql +++ /dev/null @@ -1,129 +0,0 @@ --- ===================================================== --- CommunityMessage 社区预置消息表 - 20条模拟测试数据 --- 生成时间: 2026-06-12 --- ===================================================== - -INSERT INTO [dbo].[CommunityMessage] - ([Id], [Status], [IsDeleted], [CreatedBy], [CreatedAt], [UpdatedBy], [UpdatedAt], - [JournalId], [UserId], [UserJournalId], [JournalTaskId], [JournalTaskAnswerId], - [Content], [ImageUrl], [SortOrder], [IsActive], [Type], [LikeCount], [IsFeatured]) -VALUES --- 1. 文章类型 - 期刊导读 -(1916234567890001, 1, 0, N'系统管理员', '2026-05-01 09:00:00', NULL, NULL, - 1001, 5001, 8001, 3001, 6001, - N'本期期刊聚焦人工智能在教育领域的创新应用,欢迎各位读者踊跃讨论。', - NULL, 1, 1, 1, 42, 1), - --- 2. 引用类型 - 名人名言 -(1916234567890002, 1, 0, N'编辑小王', '2026-05-02 10:30:00', NULL, NULL, - 1001, 5002, 8002, 3001, 6002, - N'"教育不是灌输,而是点燃火焰。" —— 苏格拉底', - NULL, 2, 1, 2, 88, 1), - --- 3. 公告类型 - 活动通知 -(1916234567890003, 1, 0, N'系统管理员', '2026-05-03 14:00:00', N'系统管理员', '2026-05-03 15:00:00', - 1001, 5003, 8003, 3002, 6003, - N'【活动通知】第十二期线上读书分享会将于本周六晚8点举行,主题为"科技与人文的对话",欢迎大家报名参加!', - N'/uploads/images/activity_notice_202605.png', 1, 1, 3, 156, 1), - --- 4. 文章类型 - 读者投稿 -(1916234567890004, 1, 0, N'读者张三', '2026-05-04 08:15:00', NULL, NULL, - 1002, 5004, 8004, 3003, 6004, - N'阅读了本期的深度报道《数字时代的阅读习惯变迁》,深有感触。作为一个从纸质书时代走过来的人,既感慨科技带来的便利,也怀念翻页时的触感。', - NULL, 3, 1, 1, 35, 0), - --- 5. 引用类型 - 期刊摘录 -(1916234567890005, 1, 0, N'编辑小李', '2026-05-05 11:20:00', NULL, NULL, - 1002, 5005, 8005, 3003, 6005, - N'摘自本期特稿:"在信息爆炸的时代,深度阅读能力正在成为一种稀缺的核心竞争力。"', - N'/uploads/images/quote_bg_01.png', 4, 1, 2, 67, 1), - --- 6. 文章类型 - 互动讨论 -(1916234567890006, 1, 0, N'读者李四', '2026-05-06 16:45:00', NULL, NULL, - 1001, 5006, 8006, 3001, 6006, - N'关于本期讨论话题"AI是否会取代教师",我认为技术只是工具,教育的本质是人与人之间灵魂的碰撞,这是机器无法替代的。', - NULL, 5, 1, 1, 92, 1), - --- 7. 公告类型 - 系统维护 -(1916234567890007, 1, 0, N'系统管理员', '2026-05-07 09:00:00', N'系统管理员', '2026-05-07 09:30:00', - 1001, 5001, 8001, 3001, 6001, - N'【系统公告】平台将于5月10日凌晨2:00-5:00进行系统升级维护,届时服务将短暂中断,请各位读者提前保存阅读进度。', - NULL, 1, 1, 3, 23, 0), - --- 8. 文章类型 - 编辑寄语 -(1916234567890008, 1, 0, N'主编老陈', '2026-05-08 07:30:00', NULL, NULL, - 1003, 5007, 8007, 3004, 6007, - N'新一期杂志如期而至。本期我们特别策划了"青年创作者专栏",收录了十位90后作家的原创作品,期待听到你们的声音。', - N'/uploads/images/editor_letter_05.jpg', 1, 1, 1, 210, 1), - --- 9. 引用类型 - 读者金句 -(1916234567890009, 1, 0, N'读者王五', '2026-05-09 20:10:00', NULL, NULL, - 1002, 5008, 8008, 3003, 6008, - N'"每一本杂志都是一扇窗,让我们看到不同的人生风景。" 感谢社区里每一位分享者。', - NULL, 6, 1, 2, 54, 0), - --- 10. 文章类型 - 话题互动 -(1916234567890010, 1, 0, N'读者赵六', '2026-05-10 13:55:00', NULL, NULL, - 1003, 5009, 8009, 3004, 6009, - N'看完"城市记忆"系列第三篇,想起了小时候胡同里的叫卖声和邻居家的饭菜香。城市化进程中,我们该如何留住这些温暖的记忆?', - N'/uploads/images/city_memory_03.jpg', 7, 1, 1, 78, 1), - --- 11. 公告类型 - 征稿启事 -(1916234567890011, 1, 0, N'编辑小王', '2026-05-11 10:00:00', N'编辑小王', '2026-05-12 08:00:00', - 1003, 5010, 8010, 3005, 6010, - N'【征稿启事】第十三期"我的阅读故事"主题征稿正式启动!征稿时间:5月11日-6月11日,优秀作品将刊登在杂志特辑中,期待您的来稿。', - N'/uploads/images/call_for_papers_2026.png', 2, 1, 3, 189, 1), - --- 12. 文章类型 - 读书笔记 -(1916234567890012, 1, 0, N'读者孙七', '2026-05-12 19:30:00', NULL, NULL, - 1001, 5011, 8011, 3001, 6011, - N'本期推荐的三本书我都读完了,最喜欢《思考,快与慢》。作者对直觉思维和理性思维的分析让人醍醐灌顶,强烈推荐给所有社区朋友。', - NULL, 8, 1, 1, 45, 0), - --- 13. 引用类型 - 经典回顾 -(1916234567890013, 1, 0, N'编辑小李', '2026-05-13 15:00:00', NULL, NULL, - 1002, 5002, 8002, 3003, 6012, - N'回顾创刊号寄语:"我们相信,好的内容值得被慢慢品味。在这个快节奏的时代,让我们一起做慢阅读的实践者。"', - N'/uploads/images/anniversary_retro.png', 3, 1, 2, 134, 1), - --- 14. 文章类型 - 作者互动 -(1916234567890014, 1, 0, N'作者林清', '2026-05-14 21:45:00', NULL, NULL, - 1003, 5012, 8012, 3004, 6013, - N'感谢大家对《城市记忆》系列的支持和反馈!每一条评论我都认真阅读了。你们的共鸣是我继续创作的最大动力,下一篇将聚焦"消失的声音"。', - NULL, 9, 1, 1, 167, 1), - --- 15. 公告类型 - 获奖公告 -(1916234567890015, 1, 0, N'系统管理员', '2026-05-15 12:00:00', N'系统管理员', '2026-05-15 14:00:00', - 1001, 5001, 8001, 3002, 6001, - N'【获奖公告】第十一期"最佳读者评论"评选结果揭晓!恭喜读者@书香满园、@夜读人、@思考者三位获得本期精选评论奖,奖品将于一周内寄送。', - N'/uploads/images/award_11.jpg', 4, 1, 3, 302, 1), - --- 16. 文章类型 - 生活随笔 -(1916234567890016, 1, 0, N'读者周八', '2026-05-16 07:00:00', NULL, NULL, - 1002, 5013, 8013, 3003, 6014, - N'清晨六点,一杯咖啡,一本杂志。这已经成了我三年来雷打不动的习惯。今天的这篇关于极简生活的文章,正好呼应了我最近的生活理念——少即是多。', - N'/uploads/images/morning_read.jpg', 10, 1, 1, 29, 0), - --- 17. 引用类型 - 编辑精选 -(1916234567890017, 1, 0, N'主编老陈', '2026-05-17 16:30:00', NULL, NULL, - 1003, 5007, 8007, 3004, 6015, - N'本期编辑精选语录:"真正的知识不在于拥有多少信息,而在于能用多少方式去理解世界。" —— 选自专栏文章《认知的边界》', - NULL, 5, 1, 2, 95, 1), - --- 18. 文章类型 - 问题探讨 -(1916234567890018, 1, 0, N'读者吴九', '2026-05-18 22:15:00', NULL, NULL, - 1001, 5014, 8014, 3001, 6016, - N'想请教大家,本期提到的"元宇宙图书馆"概念,你们觉得在技术层面还有多远?VR阅读体验是否真的能超越传统阅读?期待和大家探讨。', - NULL, 11, 1, 1, 61, 0), - --- 19. 公告类型 - 订阅优惠 -(1916234567890019, 1, 0, N'系统管理员', '2026-05-19 09:30:00', NULL, NULL, - 1002, 5003, 8003, 3002, 6003, - N'【限时优惠】杂志年度订阅特惠活动开启!5月20日-5月31日期间订阅全年杂志享7折优惠,老用户续费额外赠送限量版精装合集一本。', - N'/uploads/images/subscription_promo_2026.png', 6, 1, 3, 278, 0), - --- 20. 文章类型 - 社区感言 -(1916234567890020, 1, 0, N'读者郑十', '2026-05-20 18:00:00', NULL, NULL, - 1003, 5015, 8015, 3004, 6017, - N'加入这个阅读社区整整一年了,从最初的潜水到现在的积极互动,在这里认识了很多志同道合的朋友。感谢平台的用心运营,让我们一起在文字中找到归属。', - N'/uploads/images/community_anniversary.jpg', 12, 1, 1, 445, 1); diff --git a/QYZH.InteractiveMagazine.IService/IAdminPermissionService.cs b/QYZH.InteractiveMagazine.IService/IAdminPermissionService.cs new file mode 100644 index 0000000..fa8931c --- /dev/null +++ b/QYZH.InteractiveMagazine.IService/IAdminPermissionService.cs @@ -0,0 +1,80 @@ +using QYZH.InteractiveMagazine.Models.Dto; +using QYZH.InteractiveMagazine.Models.Entity; + +namespace QYZH.InteractiveMagazine.IService; + +/// +/// 后台权限管理服务接口 +/// +public interface IAdminPermissionService : IBaseService +{ + /// + /// 创建菜单 + /// + Task CreateMenuAsync(AdminMenuInput input); + + /// + /// 更新菜单 + /// + Task UpdateMenuAsync(long id, AdminMenuInput input); + + /// + /// 删除菜单 + /// + Task DeleteMenuAsync(long id); + + /// + /// 获取菜单树 + /// + Task> GetMenuTreeAsync(AdminMenuQueryInput input); + + /// + /// 创建角色 + /// + Task CreateRoleAsync(AdminRoleInput input); + + /// + /// 更新角色 + /// + Task UpdateRoleAsync(long id, AdminRoleInput input); + + /// + /// 删除角色 + /// + Task DeleteRoleAsync(long id); + + /// + /// 获取角色详情 + /// + Task GetRoleByIdAsync(long id); + + /// + /// 获取角色分页列表 + /// + Task> GetRoleListAsync(AdminRoleQueryInput input); + + /// + /// 分配角色菜单 + /// + Task AssignRoleMenusAsync(long roleId, AssignRoleMenusInput input); + + /// + /// 分配管理员角色 + /// + Task AssignAdminUserRolesAsync(long adminUserId, AssignAdminUserRolesInput input); + + /// + /// 获取管理员菜单树 + /// + Task> GetAdminUserMenuTreeAsync(long adminUserId); + + /// + /// 获取管理员角色 + /// + Task> GetAdminUserRolesAsync(long adminUserId); + + /// + /// 获取管理员权限编码 + /// + Task> GetAdminUserPermissionCodesAsync(long adminUserId); +} diff --git a/QYZH.InteractiveMagazine.Models/Dto/Admin/AdminPermissionDto.cs b/QYZH.InteractiveMagazine.Models/Dto/Admin/AdminPermissionDto.cs new file mode 100644 index 0000000..08855c8 --- /dev/null +++ b/QYZH.InteractiveMagazine.Models/Dto/Admin/AdminPermissionDto.cs @@ -0,0 +1,270 @@ +namespace QYZH.InteractiveMagazine.Models.Dto; + +/// +/// 后台菜单输入 +/// +public class AdminMenuInput +{ + /// + /// 父级菜单ID,根节点为0 + /// + public long ParentId { get; set; } + + /// + /// 菜单名称 + /// + public string Name { get; set; } = string.Empty; + + /// + /// 权限编码 + /// + public string Code { get; set; } = string.Empty; + + /// + /// 前端路由路径 + /// + public string? Path { get; set; } + + /// + /// 前端组件路径 + /// + public string? Component { get; set; } + + /// + /// 菜单图标 + /// + public string? Icon { get; set; } + + /// + /// 排序值 + /// + public int Sort { get; set; } + + /// + /// 是否显示在菜单 + /// + public bool IsVisible { get; set; } = true; + + /// + /// 状态 + /// + public int Status { get; set; } = 1; +} + +/// +/// 后台菜单输出 +/// +public class AdminMenuOutput +{ + /// + /// 菜单ID + /// + public long Id { get; set; } + + /// + /// 父级菜单ID + /// + public long ParentId { get; set; } + + /// + /// 菜单名称 + /// + public string Name { get; set; } = string.Empty; + + /// + /// 权限编码 + /// + public string Code { get; set; } = string.Empty; + + /// + /// 前端路由路径 + /// + public string? Path { get; set; } + + /// + /// 前端组件路径 + /// + public string? Component { get; set; } + + /// + /// 菜单图标 + /// + public string? Icon { get; set; } + + /// + /// 排序值 + /// + public int Sort { get; set; } + + /// + /// 是否显示在菜单 + /// + public bool IsVisible { get; set; } + + /// + /// 状态 + /// + public int Status { get; set; } + + /// + /// 子菜单 + /// + public List Children { get; set; } = []; +} + +/// +/// 后台菜单查询输入 +/// +public class AdminMenuQueryInput +{ + /// + /// 菜单名称 + /// + public string? Name { get; set; } + + /// + /// 权限编码 + /// + public string? Code { get; set; } + + /// + /// 状态 + /// + public int? Status { get; set; } +} + +/// +/// 后台角色输入 +/// +public class AdminRoleInput +{ + /// + /// 角色名称 + /// + public string Name { get; set; } = string.Empty; + + /// + /// 角色编码 + /// + public string Code { get; set; } = string.Empty; + + /// + /// 备注 + /// + public string? Remark { get; set; } + + /// + /// 状态 + /// + public int Status { get; set; } = 1; + + /// + /// 菜单ID集合 + /// + public List MenuIds { get; set; } = []; +} + +/// +/// 后台角色输出 +/// +public class AdminRoleOutput +{ + /// + /// 角色ID + /// + public long Id { get; set; } + + /// + /// 角色名称 + /// + public string Name { get; set; } = string.Empty; + + /// + /// 角色编码 + /// + public string Code { get; set; } = string.Empty; + + /// + /// 备注 + /// + public string? Remark { get; set; } + + /// + /// 状态 + /// + public int Status { get; set; } + + /// + /// 菜单ID集合 + /// + public List MenuIds { get; set; } = []; + + /// + /// 创建时间 + /// + public DateTime CreatedAt { get; set; } +} + +/// +/// 后台角色简要输出 +/// +public class AdminRoleSimpleOutput +{ + /// + /// 角色ID + /// + public long Id { get; set; } + + /// + /// 角色名称 + /// + public string Name { get; set; } = string.Empty; + + /// + /// 角色编码 + /// + public string Code { get; set; } = string.Empty; +} + +/// +/// 后台角色查询输入 +/// +public class AdminRoleQueryInput : PageQueryModel +{ + /// + /// 角色名称 + /// + public string? Name { get; set; } + + /// + /// 角色编码 + /// + public string? Code { get; set; } + + /// + /// 状态 + /// + public int? Status { get; set; } +} + +/// +/// 分配角色菜单输入 +/// +public class AssignRoleMenusInput +{ + /// + /// 菜单ID集合 + /// + public List MenuIds { get; set; } = []; +} + +/// +/// 分配用户角色输入 +/// +public class AssignAdminUserRolesInput +{ + /// + /// 角色ID集合 + /// + public List RoleIds { get; set; } = []; +} diff --git a/QYZH.InteractiveMagazine.Models/Dto/Admin/AdminUserDto.cs b/QYZH.InteractiveMagazine.Models/Dto/Admin/AdminUserDto.cs index 8186c97..4df5a7e 100644 --- a/QYZH.InteractiveMagazine.Models/Dto/Admin/AdminUserDto.cs +++ b/QYZH.InteractiveMagazine.Models/Dto/Admin/AdminUserDto.cs @@ -27,6 +27,11 @@ public class AdminUserInput /// 状态: Active, Inactive /// public int Status { get; set; } = 1; + + /// + /// 角色ID集合,更新时为null表示不调整角色 + /// + public List? RoleIds { get; set; } } /// @@ -73,6 +78,16 @@ public class AdminUserOutput /// 更新时间 /// public DateTime? UpdatedAt { get; set; } + + /// + /// 角色ID集合 + /// + public List RoleIds { get; set; } = []; + + /// + /// 角色集合 + /// + public List Roles { get; set; } = []; } /// diff --git a/QYZH.InteractiveMagazine.Models/Dto/Admin/AuthDto.cs b/QYZH.InteractiveMagazine.Models/Dto/Admin/AuthDto.cs index 1335868..640ec76 100644 --- a/QYZH.InteractiveMagazine.Models/Dto/Admin/AuthDto.cs +++ b/QYZH.InteractiveMagazine.Models/Dto/Admin/AuthDto.cs @@ -26,6 +26,26 @@ public class AdminLoginOutput public string UserName { get; set; } = string.Empty; public string Type { get; set; } = string.Empty; + + /// + /// 角色ID集合 + /// + public List RoleIds { get; set; } = []; + + /// + /// 角色集合 + /// + public List Roles { get; set; } = []; + + /// + /// 菜单树 + /// + public List Menus { get; set; } = []; + + /// + /// 权限编码集合 + /// + public List PermissionCodes { get; set; } = []; } public class AdminUserInfoOutput @@ -37,4 +57,24 @@ public class AdminUserInfoOutput public string Type { get; set; } = string.Empty; public int Status { get; set; } + + /// + /// 角色ID集合 + /// + public List RoleIds { get; set; } = []; + + /// + /// 角色集合 + /// + public List Roles { get; set; } = []; + + /// + /// 菜单树 + /// + public List Menus { get; set; } = []; + + /// + /// 权限编码集合 + /// + public List PermissionCodes { get; set; } = []; } diff --git a/QYZH.InteractiveMagazine.Models/Entity/AdminMenu.cs b/QYZH.InteractiveMagazine.Models/Entity/AdminMenu.cs new file mode 100644 index 0000000..8e033b6 --- /dev/null +++ b/QYZH.InteractiveMagazine.Models/Entity/AdminMenu.cs @@ -0,0 +1,66 @@ +using SqlSugar; + +namespace QYZH.InteractiveMagazine.Models.Entity; + +/// +/// 后台菜单表 +/// +[SugarTable("AdminMenu")] +public partial class AdminMenu : SqlSugarBaseEntity +{ + /// + /// Desc:父级菜单ID,根节点为0 + /// Default:0 + /// Nullable:False + /// + public long ParentId { get; set; } + + /// + /// Desc:菜单名称 + /// Default: + /// Nullable:False + /// + public string Name { get; set; } = string.Empty; + + /// + /// Desc:权限编码 + /// Default: + /// Nullable:False + /// + public string Code { get; set; } = string.Empty; + + /// + /// Desc:前端路由路径 + /// Default: + /// Nullable:True + /// + public string? Path { get; set; } + + /// + /// Desc:前端组件路径 + /// Default: + /// Nullable:True + /// + public string? Component { get; set; } + + /// + /// Desc:菜单图标 + /// Default: + /// Nullable:True + /// + public string? Icon { get; set; } + + /// + /// Desc:排序值 + /// Default:0 + /// Nullable:False + /// + public int Sort { get; set; } + + /// + /// Desc:是否显示在菜单 + /// Default:b'1' + /// Nullable:False + /// + public bool IsVisible { get; set; } = true; +} diff --git a/QYZH.InteractiveMagazine.Models/Entity/AdminRole.cs b/QYZH.InteractiveMagazine.Models/Entity/AdminRole.cs new file mode 100644 index 0000000..9ffd19a --- /dev/null +++ b/QYZH.InteractiveMagazine.Models/Entity/AdminRole.cs @@ -0,0 +1,31 @@ +using SqlSugar; + +namespace QYZH.InteractiveMagazine.Models.Entity; + +/// +/// 后台角色表 +/// +[SugarTable("AdminRole")] +public partial class AdminRole : SqlSugarBaseEntity +{ + /// + /// Desc:角色名称 + /// Default: + /// Nullable:False + /// + public string Name { get; set; } = string.Empty; + + /// + /// Desc:角色编码 + /// Default: + /// Nullable:False + /// + public string Code { get; set; } = string.Empty; + + /// + /// Desc:备注 + /// Default: + /// Nullable:True + /// + public string? Remark { get; set; } +} diff --git a/QYZH.InteractiveMagazine.Models/Entity/AdminRoleMenu.cs b/QYZH.InteractiveMagazine.Models/Entity/AdminRoleMenu.cs new file mode 100644 index 0000000..b5d06ac --- /dev/null +++ b/QYZH.InteractiveMagazine.Models/Entity/AdminRoleMenu.cs @@ -0,0 +1,24 @@ +using SqlSugar; + +namespace QYZH.InteractiveMagazine.Models.Entity; + +/// +/// 后台角色菜单关系表 +/// +[SugarTable("AdminRoleMenu")] +public partial class AdminRoleMenu : SqlSugarBaseEntity +{ + /// + /// Desc:角色ID + /// Default: + /// Nullable:False + /// + public long RoleId { get; set; } + + /// + /// Desc:菜单ID + /// Default: + /// Nullable:False + /// + public long MenuId { get; set; } +} diff --git a/QYZH.InteractiveMagazine.Models/Entity/AdminUserRole.cs b/QYZH.InteractiveMagazine.Models/Entity/AdminUserRole.cs new file mode 100644 index 0000000..4399da0 --- /dev/null +++ b/QYZH.InteractiveMagazine.Models/Entity/AdminUserRole.cs @@ -0,0 +1,24 @@ +using SqlSugar; + +namespace QYZH.InteractiveMagazine.Models.Entity; + +/// +/// 后台管理员角色关系表 +/// +[SugarTable("AdminUserRole")] +public partial class AdminUserRole : SqlSugarBaseEntity +{ + /// + /// Desc:管理员ID + /// Default: + /// Nullable:False + /// + public long AdminUserId { get; set; } + + /// + /// Desc:角色ID + /// Default: + /// Nullable:False + /// + public long RoleId { get; set; } +} diff --git a/QYZH.InteractiveMagazine.Service/AdminAuthService.cs b/QYZH.InteractiveMagazine.Service/AdminAuthService.cs index 1aaa911..0ae9d13 100644 --- a/QYZH.InteractiveMagazine.Service/AdminAuthService.cs +++ b/QYZH.InteractiveMagazine.Service/AdminAuthService.cs @@ -12,7 +12,11 @@ using QYZH.InteractiveMagazine.Repository; namespace QYZH.InteractiveMagazine.Service; -public class AdminAuthService(BaseRepository adminUserRepository, IConfiguration configuration, ILogger logger) : BaseRepository, IAdminAuthService +public class AdminAuthService( + BaseRepository adminUserRepository, + IAdminPermissionService adminPermissionService, + IConfiguration configuration, + ILogger logger) : BaseRepository, IAdminAuthService { private const string TokenKeyPrefix = "InteractiveMagazine:AdminAuth:Token"; @@ -60,12 +64,20 @@ public class AdminAuthService(BaseRepository adminUserRepository, ICo logger.LogInformation("管理员登录成功,用户名: {UserName}, ID: {UserId}", input.UserName, adminUser.Id); + var roles = await adminPermissionService.GetAdminUserRolesAsync(adminUser.Id); + var menus = await adminPermissionService.GetAdminUserMenuTreeAsync(adminUser.Id); + var permissionCodes = await adminPermissionService.GetAdminUserPermissionCodesAsync(adminUser.Id); + return new AdminLoginOutput { Token = token, UserId = (long)adminUser.Id, UserName = adminUser.UserName, Type = adminUser.Type.ToString(), + RoleIds = roles.Select(x => x.Id).ToList(), + Roles = roles, + Menus = menus, + PermissionCodes = permissionCodes }; } @@ -89,12 +101,20 @@ public class AdminAuthService(BaseRepository adminUserRepository, ICo throw new BusinessException("用户不存在", ResultCode.NOT_FOUND); } + var roles = await adminPermissionService.GetAdminUserRolesAsync(adminUser.Id); + var menus = await adminPermissionService.GetAdminUserMenuTreeAsync(adminUser.Id); + var permissionCodes = await adminPermissionService.GetAdminUserPermissionCodesAsync(adminUser.Id); + return new AdminUserInfoOutput { UserId = adminUser.Id, UserName = adminUser.UserName, Type = adminUser.Type.ToString(), - Status = adminUser.Status + Status = adminUser.Status, + RoleIds = roles.Select(x => x.Id).ToList(), + Roles = roles, + Menus = menus, + PermissionCodes = permissionCodes }; } diff --git a/QYZH.InteractiveMagazine.Service/AdminPermissionService.cs b/QYZH.InteractiveMagazine.Service/AdminPermissionService.cs new file mode 100644 index 0000000..edb804b --- /dev/null +++ b/QYZH.InteractiveMagazine.Service/AdminPermissionService.cs @@ -0,0 +1,495 @@ +using Microsoft.Extensions.Logging; +using QYZH.InteractiveMagazine.IService; +using QYZH.InteractiveMagazine.Models.Common; +using QYZH.InteractiveMagazine.Models.Dto; +using QYZH.InteractiveMagazine.Models.Entity; +using QYZH.InteractiveMagazine.Models.Enum; +using QYZH.InteractiveMagazine.Repository; +using SqlSugar; + +namespace QYZH.InteractiveMagazine.Service; + +/// +/// 后台权限管理服务实现 +/// +public class AdminPermissionService( + BaseRepository adminRoleRepository, + BaseRepository adminMenuRepository, + BaseRepository adminRoleMenuRepository, + BaseRepository adminUserRoleRepository, + BaseRepository adminUserRepository, + ILogger logger) : BaseRepository, IAdminPermissionService +{ + /// + /// 创建菜单 + /// + public async Task CreateMenuAsync(AdminMenuInput input) + { + await ValidateMenuInputAsync(input); + + var menu = new AdminMenu + { + ParentId = input.ParentId, + Name = input.Name.Trim(), + Code = input.Code.Trim(), + Path = input.Path?.Trim(), + Component = input.Component?.Trim(), + Icon = input.Icon?.Trim(), + Sort = input.Sort, + IsVisible = input.IsVisible, + Status = input.Status, + CreatedBy = "System", + CreatedAt = DateTime.Now, + UpdatedBy = "System", + UpdatedAt = DateTime.Now, + IsDeleted = false + }; + + var result = await adminMenuRepository.InsertAsync(menu); + BusinessException.ThrowIf(!result, "创建菜单失败", ResultCode.GLOBAL_ERROR); + + logger.LogInformation("创建后台菜单成功:{Code},ID:{Id}", menu.Code, menu.Id); + return ToMenuOutput(menu); + } + + /// + /// 更新菜单 + /// + public async Task UpdateMenuAsync(long id, AdminMenuInput input) + { + var menu = await adminMenuRepository.GetByIdAsync(id); + BusinessException.ThrowIf(menu == null, "菜单不存在", ResultCode.NOT_FOUND); + BusinessException.ThrowIf(input.ParentId == id, "父级菜单不能选择自身", ResultCode.BAD_REQUEST); + + await ValidateMenuInputAsync(input, id); + + menu!.ParentId = input.ParentId; + menu.Name = input.Name.Trim(); + menu.Code = input.Code.Trim(); + menu.Path = input.Path?.Trim(); + menu.Component = input.Component?.Trim(); + menu.Icon = input.Icon?.Trim(); + menu.Sort = input.Sort; + menu.IsVisible = input.IsVisible; + menu.Status = input.Status; + menu.UpdatedBy = "System"; + menu.UpdatedAt = DateTime.Now; + + var result = await adminMenuRepository.UpdateAsync(menu); + BusinessException.ThrowIf(!result, "更新菜单失败", ResultCode.GLOBAL_ERROR); + + logger.LogInformation("更新后台菜单成功:{Code},ID:{Id}", menu.Code, menu.Id); + return ToMenuOutput(menu); + } + + /// + /// 删除菜单 + /// + public async Task DeleteMenuAsync(long id) + { + var menu = await adminMenuRepository.GetByIdAsync(id); + BusinessException.ThrowIf(menu == null, "菜单不存在", ResultCode.NOT_FOUND); + + var hasChildren = await adminMenuRepository.Queryable().AnyAsync(x => x.ParentId == id && !x.IsDeleted); + BusinessException.ThrowIf(hasChildren, "请先删除子菜单", ResultCode.CONFLICT); + + var usedByRole = await adminRoleMenuRepository.Queryable().AnyAsync(x => x.MenuId == id && !x.IsDeleted); + BusinessException.ThrowIf(usedByRole, "菜单已被角色使用,不能删除", ResultCode.CONFLICT); + + var result = await adminMenuRepository.DeleteByIdAsync(id); + BusinessException.ThrowIf(!result, "删除菜单失败", ResultCode.GLOBAL_ERROR); + } + + /// + /// 获取菜单树 + /// + public async Task> GetMenuTreeAsync(AdminMenuQueryInput input) + { + var menus = await adminMenuRepository.Queryable() + .Where(x => !x.IsDeleted) + .WhereIF(!string.IsNullOrWhiteSpace(input.Name), x => x.Name.Contains(input.Name!)) + .WhereIF(!string.IsNullOrWhiteSpace(input.Code), x => x.Code.Contains(input.Code!)) + .WhereIF(input.Status.HasValue, x => x.Status == input.Status!.Value) + .OrderBy(x => x.Sort) + .OrderBy(x => x.Id) + .ToListAsync(); + + return BuildMenuTree(menus); + } + + /// + /// 创建角色 + /// + public async Task CreateRoleAsync(AdminRoleInput input) + { + await ValidateRoleInputAsync(input); + + var role = new AdminRole + { + Name = input.Name.Trim(), + Code = input.Code.Trim(), + Remark = input.Remark?.Trim(), + Status = input.Status, + CreatedBy = "System", + CreatedAt = DateTime.Now, + UpdatedBy = "System", + UpdatedAt = DateTime.Now, + IsDeleted = false + }; + + await UseTranAsync(async () => + { + var inserted = await adminRoleRepository.InsertAsync(role); + BusinessException.ThrowIf(!inserted, "创建角色失败", ResultCode.GLOBAL_ERROR); + await ReplaceRoleMenusAsync(role.Id, input.MenuIds); + }); + + logger.LogInformation("创建后台角色成功:{Code},ID:{Id}", role.Code, role.Id); + return await GetRoleByIdAsync(role.Id); + } + + /// + /// 更新角色 + /// + public async Task UpdateRoleAsync(long id, AdminRoleInput input) + { + var role = await adminRoleRepository.GetByIdAsync(id); + BusinessException.ThrowIf(role == null, "角色不存在", ResultCode.NOT_FOUND); + + await ValidateRoleInputAsync(input, id); + + role!.Name = input.Name.Trim(); + role.Code = input.Code.Trim(); + role.Remark = input.Remark?.Trim(); + role.Status = input.Status; + role.UpdatedBy = "System"; + role.UpdatedAt = DateTime.Now; + + await UseTranAsync(async () => + { + var updated = await adminRoleRepository.UpdateAsync(role); + BusinessException.ThrowIf(!updated, "更新角色失败", ResultCode.GLOBAL_ERROR); + await ReplaceRoleMenusAsync(role.Id, input.MenuIds); + }); + + logger.LogInformation("更新后台角色成功:{Code},ID:{Id}", role.Code, role.Id); + return await GetRoleByIdAsync(role.Id); + } + + /// + /// 删除角色 + /// + public async Task DeleteRoleAsync(long id) + { + var role = await adminRoleRepository.GetByIdAsync(id); + BusinessException.ThrowIf(role == null, "角色不存在", ResultCode.NOT_FOUND); + + var usedByUser = await adminUserRoleRepository.Queryable().AnyAsync(x => x.RoleId == id && !x.IsDeleted); + BusinessException.ThrowIf(usedByUser, "角色已分配给管理员,不能删除", ResultCode.CONFLICT); + + await UseTranAsync(async () => + { + await adminRoleMenuRepository.Deleteable().Where(x => x.RoleId == id).ExecuteCommandAsync(); + var deleted = await adminRoleRepository.DeleteByIdAsync(id); + BusinessException.ThrowIf(!deleted, "删除角色失败", ResultCode.GLOBAL_ERROR); + }); + } + + /// + /// 获取角色详情 + /// + public async Task GetRoleByIdAsync(long id) + { + var role = await adminRoleRepository.GetByIdAsync(id); + BusinessException.ThrowIf(role == null, "角色不存在", ResultCode.NOT_FOUND); + + var menuIds = await adminRoleMenuRepository.Queryable() + .Where(x => x.RoleId == id && !x.IsDeleted) + .Select(x => x.MenuId) + .ToListAsync(); + + return ToRoleOutput(role!, menuIds); + } + + /// + /// 获取角色分页列表 + /// + public async Task> GetRoleListAsync(AdminRoleQueryInput input) + { + RefAsync totalNumber = 0; + var roles = await adminRoleRepository.Queryable() + .Where(x => !x.IsDeleted) + .WhereIF(!string.IsNullOrWhiteSpace(input.Name), x => x.Name.Contains(input.Name!)) + .WhereIF(!string.IsNullOrWhiteSpace(input.Code), x => x.Code.Contains(input.Code!)) + .WhereIF(input.Status.HasValue, x => x.Status == input.Status!.Value) + .OrderByDescending(x => x.CreatedAt) + .ToPageListAsync(input.PageIndex, input.PageSize, totalNumber); + + var roleIds = roles.Select(x => x.Id).ToList(); + var roleMenus = roleIds.Count == 0 + ? [] + : await adminRoleMenuRepository.Queryable() + .Where(x => roleIds.Contains(x.RoleId) && !x.IsDeleted) + .ToListAsync(); + + var outputs = roles + .Select(x => ToRoleOutput(x, roleMenus.Where(rm => rm.RoleId == x.Id).Select(rm => rm.MenuId).ToList())) + .ToList(); + + return new PageListModel(outputs, input.PageIndex, input.PageSize, totalNumber); + } + + /// + /// 分配角色菜单 + /// + public async Task AssignRoleMenusAsync(long roleId, AssignRoleMenusInput input) + { + var role = await adminRoleRepository.GetByIdAsync(roleId); + BusinessException.ThrowIf(role == null, "角色不存在", ResultCode.NOT_FOUND); + + await ValidateMenuIdsAsync(input.MenuIds); + await UseTranAsync(async () => await ReplaceRoleMenusAsync(roleId, input.MenuIds)); + } + + /// + /// 分配管理员角色 + /// + public async Task AssignAdminUserRolesAsync(long adminUserId, AssignAdminUserRolesInput input) + { + await ReplaceAdminUserRolesAsync(adminUserId, input.RoleIds); + } + + /// + /// 获取管理员菜单树 + /// + public async Task> GetAdminUserMenuTreeAsync(long adminUserId) + { + var menus = await GetAdminUserMenusAsync(adminUserId, true); + return BuildMenuTree(menus); + } + + /// + /// 获取管理员角色 + /// + public async Task> GetAdminUserRolesAsync(long adminUserId) + { + var adminUser = await adminUserRepository.GetByIdAsync(adminUserId); + BusinessException.ThrowIf(adminUser == null, "管理员不存在", ResultCode.NOT_FOUND); + + return await adminRoleRepository.Queryable() + .InnerJoin((role, userRole) => role.Id == userRole.RoleId) + .Where((role, userRole) => userRole.AdminUserId == adminUserId && !role.IsDeleted && !userRole.IsDeleted) + .Select((role, userRole) => new AdminRoleSimpleOutput + { + Id = role.Id, + Name = role.Name, + Code = role.Code + }) + .ToListAsync(); + } + + /// + /// 获取管理员权限编码 + /// + public async Task> GetAdminUserPermissionCodesAsync(long adminUserId) + { + var menus = await GetAdminUserMenusAsync(adminUserId, false); + return menus.Select(x => x.Code).Where(x => !string.IsNullOrWhiteSpace(x)).Distinct().ToList(); + } + + private async Task ValidateMenuInputAsync(AdminMenuInput input, long? id = null) + { + BusinessException.ThrowIf(string.IsNullOrWhiteSpace(input.Name), "菜单名称不能为空", ResultCode.BAD_REQUEST); + BusinessException.ThrowIf(string.IsNullOrWhiteSpace(input.Code), "权限编码不能为空", ResultCode.BAD_REQUEST); + + if (input.ParentId > 0) + { + var parentExists = await adminMenuRepository.Queryable().AnyAsync(x => x.Id == input.ParentId && !x.IsDeleted); + BusinessException.ThrowIf(!parentExists, "父级菜单不存在", ResultCode.NOT_FOUND); + } + + var code = input.Code.Trim(); + var codeExists = await adminMenuRepository.Queryable() + .AnyAsync(x => x.Code == code && !x.IsDeleted && (!id.HasValue || x.Id != id.Value)); + BusinessException.ThrowIf(codeExists, "权限编码已存在", ResultCode.CONFLICT); + } + + private async Task ValidateRoleInputAsync(AdminRoleInput input, long? id = null) + { + BusinessException.ThrowIf(string.IsNullOrWhiteSpace(input.Name), "角色名称不能为空", ResultCode.BAD_REQUEST); + BusinessException.ThrowIf(string.IsNullOrWhiteSpace(input.Code), "角色编码不能为空", ResultCode.BAD_REQUEST); + + var code = input.Code.Trim(); + var codeExists = await adminRoleRepository.Queryable() + .AnyAsync(x => x.Code == code && !x.IsDeleted && (!id.HasValue || x.Id != id.Value)); + BusinessException.ThrowIf(codeExists, "角色编码已存在", ResultCode.CONFLICT); + + await ValidateMenuIdsAsync(input.MenuIds); + } + + private async Task ValidateMenuIdsAsync(List menuIds) + { + var ids = menuIds.Distinct().ToList(); + if (ids.Count == 0) + { + return; + } + + var existsCount = await adminMenuRepository.Queryable() + .Where(x => ids.Contains(x.Id) && !x.IsDeleted) + .CountAsync(); + BusinessException.ThrowIf(existsCount != ids.Count, "包含不存在的菜单", ResultCode.BAD_REQUEST); + } + + private async Task ValidateRoleIdsAsync(List roleIds) + { + var ids = roleIds.Distinct().ToList(); + if (ids.Count == 0) + { + return; + } + + var existsCount = await adminRoleRepository.Queryable() + .Where(x => ids.Contains(x.Id) && !x.IsDeleted) + .CountAsync(); + BusinessException.ThrowIf(existsCount != ids.Count, "包含不存在的角色", ResultCode.BAD_REQUEST); + } + + private async Task ReplaceRoleMenusAsync(long roleId, List menuIds) + { + await adminRoleMenuRepository.Deleteable().Where(x => x.RoleId == roleId).ExecuteCommandAsync(); + + var now = DateTime.Now; + var items = menuIds.Distinct().Select(menuId => new AdminRoleMenu + { + RoleId = roleId, + MenuId = menuId, + Status = (int)DefaultStatusEnum.Active, + CreatedBy = "System", + CreatedAt = now, + UpdatedBy = "System", + UpdatedAt = now, + IsDeleted = false + }).ToList(); + + if (items.Count > 0) + { + await adminRoleMenuRepository.Context.Insertable(items).ExecuteCommandAsync(); + } + } + + private async Task ReplaceAdminUserRolesAsync(long adminUserId, List roleIds) + { + var adminUser = await adminUserRepository.GetByIdAsync(adminUserId); + BusinessException.ThrowIf(adminUser == null, "管理员不存在", ResultCode.NOT_FOUND); + + await ValidateRoleIdsAsync(roleIds); + + await UseTranAsync(async () => + { + await adminUserRoleRepository.Deleteable().Where(x => x.AdminUserId == adminUserId).ExecuteCommandAsync(); + + var now = DateTime.Now; + var items = roleIds.Distinct().Select(roleId => new AdminUserRole + { + AdminUserId = adminUserId, + RoleId = roleId, + Status = (int)DefaultStatusEnum.Active, + CreatedBy = "System", + CreatedAt = now, + UpdatedBy = "System", + UpdatedAt = now, + IsDeleted = false + }).ToList(); + + if (items.Count > 0) + { + await adminUserRoleRepository.Context.Insertable(items).ExecuteCommandAsync(); + } + }); + } + + private async Task> GetAdminUserMenusAsync(long adminUserId, bool visibleOnly) + { + var adminUser = await adminUserRepository.GetByIdAsync(adminUserId); + BusinessException.ThrowIf(adminUser == null, "管理员不存在", ResultCode.NOT_FOUND); + + if (adminUser!.Type == AdminUserTypeEnum.SuperAdmin) + { + return await adminMenuRepository.Queryable() + .Where(x => !x.IsDeleted && x.Status == (int)DefaultStatusEnum.Active) + .WhereIF(visibleOnly, x => x.IsVisible) + .OrderBy(x => x.Sort) + .OrderBy(x => x.Id) + .ToListAsync(); + } + + var menus = await adminMenuRepository.Queryable() + .InnerJoin((menu, roleMenu) => menu.Id == roleMenu.MenuId) + .InnerJoin((menu, roleMenu, role) => roleMenu.RoleId == role.Id) + .InnerJoin((menu, roleMenu, role, userRole) => role.Id == userRole.RoleId) + .Where((menu, roleMenu, role, userRole) => + userRole.AdminUserId == adminUserId + && !menu.IsDeleted + && !roleMenu.IsDeleted + && !role.IsDeleted + && !userRole.IsDeleted + && menu.Status == (int)DefaultStatusEnum.Active + && role.Status == (int)DefaultStatusEnum.Active) + .WhereIF(visibleOnly, (menu, roleMenu, role, userRole) => menu.IsVisible) + .OrderBy((menu, roleMenu, role, userRole) => menu.Sort) + .OrderBy((menu, roleMenu, role, userRole) => menu.Id) + .Select((menu, roleMenu, role, userRole) => menu) + .ToListAsync(); + + return menus.DistinctBy(x => x.Id).OrderBy(x => x.Sort).ThenBy(x => x.Id).ToList(); + } + + private static List BuildMenuTree(List menus) + { + var outputs = menus.Select(ToMenuOutput).ToList(); + var lookup = outputs.ToLookup(x => x.ParentId); + + foreach (var item in outputs) + { + item.Children = lookup[item.Id].OrderBy(x => x.Sort).ThenBy(x => x.Id).ToList(); + } + + return outputs + .Where(x => x.ParentId == 0 || outputs.All(item => item.Id != x.ParentId)) + .OrderBy(x => x.Sort) + .ThenBy(x => x.Id) + .ToList(); + } + + private static AdminMenuOutput ToMenuOutput(AdminMenu menu) + { + return new AdminMenuOutput + { + Id = menu.Id, + ParentId = menu.ParentId, + Name = menu.Name, + Code = menu.Code, + Path = menu.Path, + Component = menu.Component, + Icon = menu.Icon, + Sort = menu.Sort, + IsVisible = menu.IsVisible, + Status = menu.Status + }; + } + + private static AdminRoleOutput ToRoleOutput(AdminRole role, List menuIds) + { + return new AdminRoleOutput + { + Id = role.Id, + Name = role.Name, + Code = role.Code, + Remark = role.Remark, + Status = role.Status, + MenuIds = menuIds, + CreatedAt = role.CreatedAt + }; + } +} diff --git a/QYZH.InteractiveMagazine.Service/AdminUserService.cs b/QYZH.InteractiveMagazine.Service/AdminUserService.cs index 9b61b01..bd5731e 100644 --- a/QYZH.InteractiveMagazine.Service/AdminUserService.cs +++ b/QYZH.InteractiveMagazine.Service/AdminUserService.cs @@ -15,7 +15,10 @@ namespace QYZH.InteractiveMagazine.Service; /// /// 管理员用户服务实现 /// -public class AdminUserService(BaseRepository adminUserRepository, ILogger logger) : BaseRepository, IAdminUserService +public class AdminUserService( + BaseRepository adminUserRepository, + IAdminPermissionService adminPermissionService, + ILogger logger) : BaseRepository, IAdminUserService { /// @@ -63,9 +66,14 @@ public class AdminUserService(BaseRepository adminUserRepository, ILo throw new BusinessException("创建管理员失败", ResultCode.GLOBAL_ERROR); } + if (input.RoleIds != null) + { + await adminPermissionService.AssignAdminUserRolesAsync(adminUser.Id, new AssignAdminUserRolesInput { RoleIds = input.RoleIds }); + } + logger.LogInformation("管理员创建成功,用户名: {UserName}, ID: {Id}", input.UserName, adminUser.Id); - return new AdminUserOutput { Id = adminUser.Id, UserName = adminUser.UserName, Type = adminUser.Type.ToString(), Status = adminUser.Status, CreatedBy = adminUser.CreatedBy, CreatedAt = adminUser.CreatedAt, UpdatedBy = adminUser.UpdatedBy, UpdatedAt = adminUser.UpdatedAt }; + return await ToAdminUserOutputAsync(adminUser); } /// @@ -114,9 +122,14 @@ public class AdminUserService(BaseRepository adminUserRepository, ILo throw new BusinessException("更新管理员失败", ResultCode.GLOBAL_ERROR); } + if (input.RoleIds != null) + { + await adminPermissionService.AssignAdminUserRolesAsync(adminUser.Id, new AssignAdminUserRolesInput { RoleIds = input.RoleIds }); + } + logger.LogInformation("管理员更新成功,ID: {Id}", id); - return new AdminUserOutput { Id = adminUser.Id, UserName = adminUser.UserName, Type = adminUser.Type.ToString(), Status = adminUser.Status, CreatedBy = adminUser.CreatedBy, CreatedAt = adminUser.CreatedAt, UpdatedBy = adminUser.UpdatedBy, UpdatedAt = adminUser.UpdatedAt }; + return await ToAdminUserOutputAsync(adminUser); } /// @@ -156,17 +169,7 @@ public class AdminUserService(BaseRepository adminUserRepository, ILo logger.LogWarning("未找到管理员,ID: {Id}", id); throw new BusinessException("管理员不存在", ResultCode.NOT_FOUND); } - return new AdminUserOutput - { - Id = adminUser.Id, - UserName = adminUser.UserName, - Type = adminUser.Type.ToString(), - Status = adminUser.Status, - CreatedBy = adminUser.CreatedBy, - CreatedAt = adminUser.CreatedAt, - UpdatedBy = adminUser.UpdatedBy, - UpdatedAt = adminUser.UpdatedAt - }; + return await ToAdminUserOutputAsync(adminUser); } /// @@ -201,6 +204,12 @@ public class AdminUserService(BaseRepository adminUserRepository, ILo UpdatedAt = a.UpdatedAt }, true) .ToPageListAsync(input.PageIndex, input.PageSize, totalNumber); + foreach (var item in pageResult) + { + item.Roles = await adminPermissionService.GetAdminUserRolesAsync(item.Id); + item.RoleIds = item.Roles.Select(x => x.Id).ToList(); + } + return new PageListModel(pageResult, input.PageIndex, input.PageSize, totalNumber); } @@ -231,4 +240,22 @@ public class AdminUserService(BaseRepository adminUserRepository, ILo logger.LogInformation("管理员状态更新成功,ID: {Id}", id); } + + private async Task ToAdminUserOutputAsync(AdminUser adminUser) + { + var roles = await adminPermissionService.GetAdminUserRolesAsync(adminUser.Id); + return new AdminUserOutput + { + Id = adminUser.Id, + UserName = adminUser.UserName, + Type = adminUser.Type.ToString(), + Status = adminUser.Status, + CreatedBy = adminUser.CreatedBy, + CreatedAt = adminUser.CreatedAt, + UpdatedBy = adminUser.UpdatedBy, + UpdatedAt = adminUser.UpdatedAt, + RoleIds = roles.Select(x => x.Id).ToList(), + Roles = roles + }; + } } diff --git a/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/BagController.cs b/QYZH.InteractiveMagazine.WeChatApi/Controllers/BagController.cs similarity index 96% rename from QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/BagController.cs rename to QYZH.InteractiveMagazine.WeChatApi/Controllers/BagController.cs index 8133d3b..024d48b 100644 --- a/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/BagController.cs +++ b/QYZH.InteractiveMagazine.WeChatApi/Controllers/BagController.cs @@ -3,7 +3,7 @@ using QYZH.InteractiveMagazine.IService; using QYZH.InteractiveMagazine.Models.Dto; using QYZH.InteractiveMagazine.Models.Dto.Bag; -namespace QYZH.InteractiveMagazine.WebApi.Controllers.WeChat; +namespace QYZH.InteractiveMagazine.WeChatApi.Controllers; /// /// 小程序背包控制器 diff --git a/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/CheckInController.cs b/QYZH.InteractiveMagazine.WeChatApi/Controllers/CheckInController.cs similarity index 98% rename from QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/CheckInController.cs rename to QYZH.InteractiveMagazine.WeChatApi/Controllers/CheckInController.cs index a03ee23..3c4938c 100644 --- a/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/CheckInController.cs +++ b/QYZH.InteractiveMagazine.WeChatApi/Controllers/CheckInController.cs @@ -4,7 +4,7 @@ using QYZH.InteractiveMagazine.Models.Common; using QYZH.InteractiveMagazine.Models.Dto; using QYZH.InteractiveMagazine.Models.Dto.CheckIn; -namespace QYZH.InteractiveMagazine.WebApi.Controllers.WeChat; +namespace QYZH.InteractiveMagazine.WeChatApi.Controllers; /// /// 签到控制器 diff --git a/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/CommunityController.cs b/QYZH.InteractiveMagazine.WeChatApi/Controllers/CommunityController.cs similarity index 98% rename from QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/CommunityController.cs rename to QYZH.InteractiveMagazine.WeChatApi/Controllers/CommunityController.cs index 8ca4973..d81be05 100644 --- a/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/CommunityController.cs +++ b/QYZH.InteractiveMagazine.WeChatApi/Controllers/CommunityController.cs @@ -3,7 +3,7 @@ using QYZH.InteractiveMagazine.IService; using QYZH.InteractiveMagazine.Models.Common; using QYZH.InteractiveMagazine.Models.Dto; -namespace QYZH.InteractiveMagazine.WebApi.Controllers.WeChat; +namespace QYZH.InteractiveMagazine.WeChatApi.Controllers; /// /// 小程序社区控制器 diff --git a/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/JournalController.cs b/QYZH.InteractiveMagazine.WeChatApi/Controllers/JournalController.cs similarity index 98% rename from QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/JournalController.cs rename to QYZH.InteractiveMagazine.WeChatApi/Controllers/JournalController.cs index 44e260e..ec8d514 100644 --- a/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/JournalController.cs +++ b/QYZH.InteractiveMagazine.WeChatApi/Controllers/JournalController.cs @@ -3,7 +3,7 @@ using QYZH.InteractiveMagazine.IService; using QYZH.InteractiveMagazine.Models.Common; using QYZH.InteractiveMagazine.Models.Dto; -namespace QYZH.InteractiveMagazine.WebApi.Controllers.WeChat; +namespace QYZH.InteractiveMagazine.WeChatApi.Controllers; /// /// 小程序期刊管理控制器 diff --git a/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/MallController.cs b/QYZH.InteractiveMagazine.WeChatApi/Controllers/MallController.cs similarity index 97% rename from QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/MallController.cs rename to QYZH.InteractiveMagazine.WeChatApi/Controllers/MallController.cs index 90e2f77..6f97519 100644 --- a/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/MallController.cs +++ b/QYZH.InteractiveMagazine.WeChatApi/Controllers/MallController.cs @@ -3,7 +3,7 @@ using QYZH.InteractiveMagazine.IService; using QYZH.InteractiveMagazine.Models.Dto; using QYZH.InteractiveMagazine.Models.Dto.Mall; -namespace QYZH.InteractiveMagazine.WebApi.Controllers.WeChat; +namespace QYZH.InteractiveMagazine.WeChatApi.Controllers; /// /// 小程序商城控制器 diff --git a/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/MedalController.cs b/QYZH.InteractiveMagazine.WeChatApi/Controllers/MedalController.cs similarity index 98% rename from QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/MedalController.cs rename to QYZH.InteractiveMagazine.WeChatApi/Controllers/MedalController.cs index 42ffac6..2819ae3 100644 --- a/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/MedalController.cs +++ b/QYZH.InteractiveMagazine.WeChatApi/Controllers/MedalController.cs @@ -3,7 +3,7 @@ using QYZH.InteractiveMagazine.IService; using QYZH.InteractiveMagazine.Models.Common; using QYZH.InteractiveMagazine.Models.Dto; -namespace QYZH.InteractiveMagazine.WebApi.Controllers.WeChat; +namespace QYZH.InteractiveMagazine.WeChatApi.Controllers; /// /// 小程序勋章控制器 diff --git a/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/PetController.cs b/QYZH.InteractiveMagazine.WeChatApi/Controllers/PetController.cs similarity index 98% rename from QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/PetController.cs rename to QYZH.InteractiveMagazine.WeChatApi/Controllers/PetController.cs index 894f79b..8e7b35e 100644 --- a/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/PetController.cs +++ b/QYZH.InteractiveMagazine.WeChatApi/Controllers/PetController.cs @@ -4,7 +4,7 @@ using QYZH.InteractiveMagazine.Models.Common; using QYZH.InteractiveMagazine.Models.Dto; using QYZH.InteractiveMagazine.Models.Dto.Pet; -namespace QYZH.InteractiveMagazine.WebApi.Controllers.WeChat; +namespace QYZH.InteractiveMagazine.WeChatApi.Controllers; /// /// 小程序宠物管理控制器 diff --git a/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/UserAnswerTaskController.cs b/QYZH.InteractiveMagazine.WeChatApi/Controllers/UserAnswerTaskController.cs similarity index 99% rename from QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/UserAnswerTaskController.cs rename to QYZH.InteractiveMagazine.WeChatApi/Controllers/UserAnswerTaskController.cs index 62e2211..c87c2fb 100644 --- a/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/UserAnswerTaskController.cs +++ b/QYZH.InteractiveMagazine.WeChatApi/Controllers/UserAnswerTaskController.cs @@ -5,7 +5,7 @@ using QYZH.InteractiveMagazine.Models.Dto; using QYZH.InteractiveMagazine.Models.Dto.UserAnswerTaskService; using System.ComponentModel.DataAnnotations; -namespace QYZH.InteractiveMagazine.WebApi.Controllers.WeChat; +namespace QYZH.InteractiveMagazine.WeChatApi.Controllers; /// /// 小程序首页控制器 diff --git a/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/WeChatAuthController.cs b/QYZH.InteractiveMagazine.WeChatApi/Controllers/WeChatAuthController.cs similarity index 99% rename from QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/WeChatAuthController.cs rename to QYZH.InteractiveMagazine.WeChatApi/Controllers/WeChatAuthController.cs index 3e0bcae..3ce8d32 100644 --- a/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/WeChatAuthController.cs +++ b/QYZH.InteractiveMagazine.WeChatApi/Controllers/WeChatAuthController.cs @@ -6,7 +6,7 @@ using QYZH.InteractiveMagazine.Models.Common; using QYZH.InteractiveMagazine.Models.Dto; using QYZH.InteractiveMagazine.Models.WeChat; -namespace QYZH.InteractiveMagazine.WebApi.Controllers.WeChat; +namespace QYZH.InteractiveMagazine.WeChatApi.Controllers; /// /// 微信小程序认证控制器 diff --git a/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/WeChatBaseController.cs b/QYZH.InteractiveMagazine.WeChatApi/Controllers/WeChatBaseController.cs similarity index 97% rename from QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/WeChatBaseController.cs rename to QYZH.InteractiveMagazine.WeChatApi/Controllers/WeChatBaseController.cs index ae97bc9..989677d 100644 --- a/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/WeChatBaseController.cs +++ b/QYZH.InteractiveMagazine.WeChatApi/Controllers/WeChatBaseController.cs @@ -5,7 +5,7 @@ using QYZH.InteractiveMagazine.Models.Dto; using QYZH.InteractiveMagazine.Models.Enum; using System.Security.Claims; -namespace QYZH.InteractiveMagazine.WebApi.Controllers.WeChat; +namespace QYZH.InteractiveMagazine.WeChatApi.Controllers; /// /// 小程序基础控制器 diff --git a/QYZH.InteractiveMagazine.WeChatApi/Program.cs b/QYZH.InteractiveMagazine.WeChatApi/Program.cs new file mode 100644 index 0000000..5144dfe --- /dev/null +++ b/QYZH.InteractiveMagazine.WeChatApi/Program.cs @@ -0,0 +1,189 @@ +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.ResponseCompression; +using Microsoft.OpenApi.Models; +using QYZH.InteractiveMagazine.Common.Extensions; +using QYZH.InteractiveMagazine.Common.Helpers; +using QYZH.InteractiveMagazine.Infrastructure.Autofacs; +using QYZH.InteractiveMagazine.Infrastructure.Context; +using QYZH.InteractiveMagazine.Infrastructure.Extensions; +using QYZH.InteractiveMagazine.Infrastructure.Middleware; +using QYZH.InteractiveMagazine.Infrastructure.RabbitMQ; +using QYZH.InteractiveMagazine.Infrastructure.Redis; +using QYZH.InteractiveMagazine.Infrastructure.SDK; +using QYZH.InteractiveMagazine.Models.Enum; +using QYZH.InteractiveMagazine.Models.Settings; +using QYZH.InteractiveMagazine.Repository; +using QYZH.InteractiveMagazine.Repository.Core; +using Serilog; +using SqlSugar.IOC; +using Swashbuckle.AspNetCore.SwaggerGen; +using Swashbuckle.AspNetCore.SwaggerUI; +using System.Text.Json.Serialization; +using Yitter.IdGenerator; + +var builder = WebApplication.CreateBuilder(args); + +builder.Configuration.AddJsonFile("medal-rule-config.json", optional: false, reloadOnChange: true); + +YitIdHelper.SetIdGenerator(new IdGeneratorOptions { WorkerId = 1 }); + +builder.UseAutofac(); + +builder.InitSqlSugarDb(new IocConfig +{ + ConfigId = 0, + DbType = IocDbType.MySql, + ConnectionString = builder.Configuration.GetConnectionString("DefaultConnection"), + IsAutoCloseConnection = true, +}); + +builder.Services.AddDataProtection() + .PersistKeysToFileSystem(new DirectoryInfo(Path.Combine(Directory.GetCurrentDirectory(), "DataProtection"))); + +builder.Services.AddCSRedisCacheExtension(builder.Configuration.GetSection("RedisSettings")); +builder.Services.AddRabbitMQ(builder.Configuration); + +Log.Logger = new LoggerConfiguration() + .ReadFrom.Configuration(builder.Configuration) + .Enrich.FromLogContext() + .CreateLogger(); + +builder.Host.UseSerilog(); + +builder.Services.AddControllers(options => +{ + options.SuppressImplicitRequiredAttributeForNonNullableReferenceTypes = true; + options.Filters.Add(); +}) +.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.AddScoped(typeof(BaseRepository<>)); +builder.Services.AddInfrastructureServices(builder.Configuration, builder.Environment); +builder.Services.AddSDKService(builder.Configuration); +builder.Services.Configure(builder.Configuration.GetSection("MedalRuleConfig")); + +builder.Services.AddCors(options => +{ + options.AddPolicy("AllowAll", policy => + { + policy.AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader(); + }); +}); + +builder.Services.Configure(options => +{ + options.Level = System.IO.Compression.CompressionLevel.Optimal; +}); +builder.Services.Configure(options => +{ + options.Level = System.IO.Compression.CompressionLevel.Fastest; +}); +builder.Services.AddResponseCompression(options => +{ + options.EnableForHttps = true; + options.Providers.Add(); + options.Providers.Add(); + options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(["image/svg+xml", "application/json", "text/plain"]); +}); + +builder.Services.AddSwaggerGen(option => +{ + var xmlFile = $"{AppDomain.CurrentDomain.FriendlyName}.xml"; + var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); + var modelXml = Path.Combine(AppContext.BaseDirectory, "QYZH.InteractiveMagazine.Models.xml"); + var version = ApiVersionEnum.Wechat; + + option.SwaggerDoc(version.ToString(), new OpenApiInfo + { + Title = AppDomain.CurrentDomain.FriendlyName, + Version = "互动期刊接口文档", + Description = $"{version.GetDescription()}接口,Last Modify Time:{new FileInfo(xmlPath).LastWriteTime:yyyy-MM-dd HH:mm:ss}" + }); + + option.OrderActionsBy(o => o.RelativePath); + + if (File.Exists(xmlPath)) + { + option.IncludeXmlComments(xmlPath, true); + } + + if (File.Exists(modelXml)) + { + option.IncludeXmlComments(modelXml, true); + } + + option.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme + { + Description = "请输入 Token,格式为 Bearer Token", + Name = "Authorization", + In = ParameterLocation.Header, + Type = SecuritySchemeType.ApiKey, + BearerFormat = "JWT", + Scheme = "Bearer" + }); + option.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Bearer" + } + }, + [] + } + }); + + option.DocInclusionPredicate((docName, apiDesc) => + { + if (!apiDesc.TryGetMethodInfo(out var methodInfo)) + { + return false; + } + + var groupName = methodInfo.DeclaringType? + .GetCustomAttributes(true) + .OfType() + .FirstOrDefault()? + .GroupName; + + return groupName == docName; + }); +}); + +var app = builder.Build(); + +app.UseSwagger(); +app.UseSwaggerUI(c => +{ + var version = ApiVersionEnum.Wechat; + c.SwaggerEndpoint($"/swagger/{version}/swagger.json", $"{version.GetDescription()}接口"); + c.DocExpansion(DocExpansion.None); +}); + +app.UseServiceContext(); +app.UseHttpsRedirection(); +app.UseCors("AllowAll"); +app.UseMiddleware(); +app.UseMiddleware(); +app.UseMiddleware(); +app.UseAuthentication(); +app.UseAuthorization(); +app.MapControllers(); + +app.Run(); diff --git a/QYZH.InteractiveMagazine.WeChatApi/Properties/launchSettings.json b/QYZH.InteractiveMagazine.WeChatApi/Properties/launchSettings.json new file mode 100644 index 0000000..c171209 --- /dev/null +++ b/QYZH.InteractiveMagazine.WeChatApi/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:51131", + "sslPort": 44367 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5198", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7253;http://localhost:5198", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/QYZH.InteractiveMagazine.WeChatApi/QYZH.InteractiveMagazine.WeChatApi.csproj b/QYZH.InteractiveMagazine.WeChatApi/QYZH.InteractiveMagazine.WeChatApi.csproj new file mode 100644 index 0000000..000665b --- /dev/null +++ b/QYZH.InteractiveMagazine.WeChatApi/QYZH.InteractiveMagazine.WeChatApi.csproj @@ -0,0 +1,25 @@ + + + + net8.0 + enable + enable + True + + + + + + + + + + + + + + + + + + diff --git a/QYZH.InteractiveMagazine.WeChatApi/appsettings.Development.json b/QYZH.InteractiveMagazine.WeChatApi/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/QYZH.InteractiveMagazine.WeChatApi/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/QYZH.InteractiveMagazine.WeChatApi/appsettings.json b/QYZH.InteractiveMagazine.WeChatApi/appsettings.json new file mode 100644 index 0000000..87c9381 --- /dev/null +++ b/QYZH.InteractiveMagazine.WeChatApi/appsettings.json @@ -0,0 +1,71 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "server=192.168.20.150;port=13306;database=InteractiveMagazine;user=user;password=n68792bu!y99r905;charset=utf8mb4;" + }, + "JwtSettings": { + "Issuer": "QYZH.InteractiveMagazine", + "Audience": "QYZH.InteractiveMagazine", + "SecretKey": "zG7pLqR9xVw2bN8fYtHk3mPc5sA1dF6eUjW4gXhC7vB", + "ExpiryMinutes": 120, + "JwtTokenExpiryDays": 30 + }, + "RedisSettings": { + "ConnectionString": "192.168.20.150:16379,defaultDatabase=5", + "Sentinels": [], + "ExpireSecondRange": [ 3600, 7200 ] + }, + "RabbitMq": { + "HostName": "192.168.20.150", + "Port": 5672, + "UserName": "smartschool", + "Password": "@ss%&*otz%d*pq2S", + "VirtualHost": "InteractiveMagazine", + "ClientProvidedName": "Custom connection name" + }, + "WeChatSettings": { + "AppId": "wx8c08da60bd207e64", + "AppSecret": "76fa314c34762c0347bd613e488c6872" + }, + "Serilog": { + "MinimumLevel": { + "Default": "Information", + "Override": { + "Microsoft": "Warning", + "System": "Warning" + } + }, + "WriteTo": [ + { + "Name": "Console" + }, + { + "Name": "File", + "Args": { + "path": "logs/log-.txt", + "rollingInterval": "Day" + } + } + ] + }, + "AllowedHosts": "*", + "AiChat": { + "ApiKey": "Ollama", + "BaseUrl": "http://172.16.10.130:11434/v1/", + "Model": "qwen2.5vl:7b", + "TimeoutSeconds": 300, + "MaxTokens": 2000, + "Temperature": 0.5 + }, + "AliyunOSSConfigs": { + "AccessKeyID": "LTAI5tEBXGewpHSLiSxyx6Bf", + "AccessKeySecret": "w29b8wkw6XQVL8GWXgp3ZesgYeDKvf", + "VodBucketName": "outin-5277bbb52bec11f08dbd00163e169e2b.oss-cn-beijing.aliyuncs.com", + "BucketName": "qyzh2025test", + "Region": "beijing", + "RoleArn": "acs:ram::1064745380176636:role/aliyunosstokengeneratorrole", + "DurationSeconds": 3600, //过期时间(秒) + "Endpoint": "oss-cn-beijing.aliyuncs.com", + "ProjectName": "InteractiveMagazine", + "Domain": "http://oss-test.qyzhjy.com/" + } +} diff --git a/QYZH.InteractiveMagazine.WeChatApi/medal-rule-config.json b/QYZH.InteractiveMagazine.WeChatApi/medal-rule-config.json new file mode 100644 index 0000000..bc290ad --- /dev/null +++ b/QYZH.InteractiveMagazine.WeChatApi/medal-rule-config.json @@ -0,0 +1,43 @@ +{ + "MedalRuleConfig": { + "Tables": [ + { + "TableName": "CheckInRecord", + "DisplayName": "签到记录", + "Fields": [ + { "FieldName": "ContinuousDays", "DisplayName": "连续天数" } + ] + }, + { + "TableName": "UserPet", + "DisplayName": "宠物", + "Fields": [ + { + "FieldName": "GrowthPoints", + "DisplayName": "成长值" + }, + { + "FieldName": "FeedingCount", + "DisplayName": "喂养次数" + }, + { + "FieldName": "Comprehension", + "DisplayName": "理解力" + }, + { + "FieldName": "Judgment", + "DisplayName": "判断力" + }, + { + "FieldName": "Expression", + "DisplayName": "表达力" + }, + { + "FieldName": "Persuasiveness", + "DisplayName": "说服力" + } + ] + } + ] + } +} diff --git a/QYZH.InteractiveMagazine.WebApi/Controllers/PermissionController.cs b/QYZH.InteractiveMagazine.WebApi/Controllers/PermissionController.cs new file mode 100644 index 0000000..05bd1a4 --- /dev/null +++ b/QYZH.InteractiveMagazine.WebApi/Controllers/PermissionController.cs @@ -0,0 +1,139 @@ +using Microsoft.AspNetCore.Mvc; +using QYZH.InteractiveMagazine.IService; +using QYZH.InteractiveMagazine.Models.Common; +using QYZH.InteractiveMagazine.Models.Dto; +using QYZH.InteractiveMagazine.Models.Enum; + +namespace QYZH.InteractiveMagazine.WebApi.Controllers; + +/// +/// 后台权限管理 +/// +[ApiController] +[Route("api/[controller]")] +[ApiExplorerSettings(GroupName = nameof(ApiVersionEnum.Platform))] +public class PermissionController(IAdminPermissionService adminPermissionService) : BaseController +{ + /// + /// 创建菜单 + /// + [HttpPost("menus")] + public async Task> CreateMenuAsync([FromBody] AdminMenuInput input) + { + var result = await adminPermissionService.CreateMenuAsync(input); + return Success(result); + } + + /// + /// 更新菜单 + /// + [HttpPut("menus/{id}")] + public async Task> UpdateMenuAsync(long id, [FromBody] AdminMenuInput input) + { + var result = await adminPermissionService.UpdateMenuAsync(id, input); + return Success(result); + } + + /// + /// 删除菜单 + /// + [HttpDelete("menus/{id}")] + public async Task> DeleteMenuAsync(long id) + { + await adminPermissionService.DeleteMenuAsync(id); + return Success(new object()); + } + + /// + /// 获取菜单树 + /// + [HttpPost("menus/tree")] + public async Task>> GetMenuTreeAsync([FromBody] AdminMenuQueryInput input) + { + var result = await adminPermissionService.GetMenuTreeAsync(input); + return Success(result); + } + + /// + /// 获取当前管理员菜单树 + /// + [HttpGet("menus/current")] + public async Task>> GetCurrentMenuTreeAsync() + { + var userId = GetCurrentUserId(); + BusinessException.ThrowIf(userId == null, "未获取到用户信息", ResultCode.DENY); + + var result = await adminPermissionService.GetAdminUserMenuTreeAsync(userId!.Value); + return Success(result); + } + + /// + /// 创建角色 + /// + [HttpPost("roles")] + public async Task> CreateRoleAsync([FromBody] AdminRoleInput input) + { + var result = await adminPermissionService.CreateRoleAsync(input); + return Success(result); + } + + /// + /// 更新角色 + /// + [HttpPut("roles/{id}")] + public async Task> UpdateRoleAsync(long id, [FromBody] AdminRoleInput input) + { + var result = await adminPermissionService.UpdateRoleAsync(id, input); + return Success(result); + } + + /// + /// 删除角色 + /// + [HttpDelete("roles/{id}")] + public async Task> DeleteRoleAsync(long id) + { + await adminPermissionService.DeleteRoleAsync(id); + return Success(new object()); + } + + /// + /// 获取角色详情 + /// + [HttpGet("roles/{id}")] + public async Task> GetRoleByIdAsync(long id) + { + var result = await adminPermissionService.GetRoleByIdAsync(id); + return Success(result); + } + + /// + /// 获取角色分页列表 + /// + [HttpPost("roles/list")] + public async Task>> GetRoleListAsync([FromBody] AdminRoleQueryInput input) + { + var result = await adminPermissionService.GetRoleListAsync(input); + return Success(result); + } + + /// + /// 分配角色菜单 + /// + [HttpPut("roles/{roleId}/menus")] + public async Task> AssignRoleMenusAsync(long roleId, [FromBody] AssignRoleMenusInput input) + { + await adminPermissionService.AssignRoleMenusAsync(roleId, input); + return Success(new object()); + } + + /// + /// 分配管理员角色 + /// + [HttpPut("users/{adminUserId}/roles")] + public async Task> AssignAdminUserRolesAsync(long adminUserId, [FromBody] AssignAdminUserRolesInput input) + { + await adminPermissionService.AssignAdminUserRolesAsync(adminUserId, input); + return Success(new object()); + } +} diff --git a/QYZH.InteractiveMagazine.WebApi/Program.cs b/QYZH.InteractiveMagazine.WebApi/Program.cs index 02d1683..392de5b 100644 --- a/QYZH.InteractiveMagazine.WebApi/Program.cs +++ b/QYZH.InteractiveMagazine.WebApi/Program.cs @@ -90,7 +90,7 @@ builder.Services.AddSwaggerGen(option => var modelXml = Path.Combine(AppContext.BaseDirectory, $"QYZH.InteractiveMagazine.Models.xml"); - Enum.GetValues().ToList().ForEach(version => + Enum.GetValues().Where(version => version != ApiVersionEnum.Wechat).ToList().ForEach(version => { // 配置文档信息 option.SwaggerDoc(version.ToString(), new OpenApiInfo @@ -201,7 +201,7 @@ var app = builder.Build(); app.UseSwaggerUI(c => { // 根据版本名称倒序 遍历展示 - Enum.GetValues().OrderBy(e => e).ToList().ForEach(version => + Enum.GetValues().Where(version => version != ApiVersionEnum.Wechat).OrderBy(e => e).ToList().ForEach(version => { c.SwaggerEndpoint($"/swagger/{version}/swagger.json", $"{version.GetDescription()}接口"); }); @@ -220,4 +220,3 @@ app.UseAuthorization(); app.MapControllers(); app.Run(); - diff --git a/QYZH.InteractiveMagazine.WorkService/Consumers/JournalTaskReceiveConsumer.cs b/QYZH.InteractiveMagazine.WorkService/Consumers/JournalTaskReceiveConsumer.cs index eac5b1b..0a5cd1d 100644 --- a/QYZH.InteractiveMagazine.WorkService/Consumers/JournalTaskReceiveConsumer.cs +++ b/QYZH.InteractiveMagazine.WorkService/Consumers/JournalTaskReceiveConsumer.cs @@ -47,6 +47,7 @@ public class JournalTaskReceiveConsumer( { PropertyNameCaseInsensitive = true }) ?? throw new InvalidOperationException("期刊任务消息内容为空"); + data.Normalize(); if (data.Questions == null || data.Questions.Length == 0) { @@ -1093,11 +1094,26 @@ public class QuestionData /// public long UserId { get; set; } + /// + /// 学生ID + /// + public long StudentId { get; set; } + /// /// 期刊ID /// public long JournalId { get; set; } + /// + /// 作业ID + /// + public long HomeworkId { get; set; } + + /// + /// 书籍ID + /// + public long BookId { get; set; } + /// /// 页ID /// @@ -1117,6 +1133,22 @@ public class QuestionData /// 创建时间 /// public DateTime CreatedTime { get; set; } + + /// + /// 兼容新版作答消息字段 + /// + public void Normalize() + { + if (UserId <= 0) + { + UserId = StudentId; + } + + if (JournalId <= 0) + { + JournalId = BookId > 0 ? BookId : HomeworkId; + } + } } /// diff --git a/QYZH.InteractiveMagazine.slnx b/QYZH.InteractiveMagazine.slnx index 6320146..3a2a8cf 100644 --- a/QYZH.InteractiveMagazine.slnx +++ b/QYZH.InteractiveMagazine.slnx @@ -9,5 +9,6 @@ +