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 @@
+