refactor: 拆分微信API到独立项目并迁移相关代码

1.  新增WeChatApi独立项目,将原WebApi中的微信相关控制器迁移至新项目
2.  新增后台权限管理相关实体、服务接口和基础控制器
3.  完善管理员用户服务,增加角色关联查询和赋值逻辑
4.  修复WebApi Swagger文档过滤微信API版本的问题
5.  更新解决方案文件,添加新的微信API项目引用
6.  新增微信API基础配置文件和项目属性配置
This commit is contained in:
glz
2026-07-06 14:21:21 +08:00
parent f8874ff60c
commit a04b4be7e5
32 changed files with 1669 additions and 158 deletions

View File

@ -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);

View File

@ -0,0 +1,80 @@
using QYZH.InteractiveMagazine.Models.Dto;
using QYZH.InteractiveMagazine.Models.Entity;
namespace QYZH.InteractiveMagazine.IService;
/// <summary>
/// 后台权限管理服务接口
/// </summary>
public interface IAdminPermissionService : IBaseService<AdminRole>
{
/// <summary>
/// 创建菜单
/// </summary>
Task<AdminMenuOutput> CreateMenuAsync(AdminMenuInput input);
/// <summary>
/// 更新菜单
/// </summary>
Task<AdminMenuOutput> UpdateMenuAsync(long id, AdminMenuInput input);
/// <summary>
/// 删除菜单
/// </summary>
Task DeleteMenuAsync(long id);
/// <summary>
/// 获取菜单树
/// </summary>
Task<List<AdminMenuOutput>> GetMenuTreeAsync(AdminMenuQueryInput input);
/// <summary>
/// 创建角色
/// </summary>
Task<AdminRoleOutput> CreateRoleAsync(AdminRoleInput input);
/// <summary>
/// 更新角色
/// </summary>
Task<AdminRoleOutput> UpdateRoleAsync(long id, AdminRoleInput input);
/// <summary>
/// 删除角色
/// </summary>
Task DeleteRoleAsync(long id);
/// <summary>
/// 获取角色详情
/// </summary>
Task<AdminRoleOutput> GetRoleByIdAsync(long id);
/// <summary>
/// 获取角色分页列表
/// </summary>
Task<PageListModel<AdminRoleOutput>> GetRoleListAsync(AdminRoleQueryInput input);
/// <summary>
/// 分配角色菜单
/// </summary>
Task AssignRoleMenusAsync(long roleId, AssignRoleMenusInput input);
/// <summary>
/// 分配管理员角色
/// </summary>
Task AssignAdminUserRolesAsync(long adminUserId, AssignAdminUserRolesInput input);
/// <summary>
/// 获取管理员菜单树
/// </summary>
Task<List<AdminMenuOutput>> GetAdminUserMenuTreeAsync(long adminUserId);
/// <summary>
/// 获取管理员角色
/// </summary>
Task<List<AdminRoleSimpleOutput>> GetAdminUserRolesAsync(long adminUserId);
/// <summary>
/// 获取管理员权限编码
/// </summary>
Task<List<string>> GetAdminUserPermissionCodesAsync(long adminUserId);
}

View File

@ -0,0 +1,270 @@
namespace QYZH.InteractiveMagazine.Models.Dto;
/// <summary>
/// 后台菜单输入
/// </summary>
public class AdminMenuInput
{
/// <summary>
/// 父级菜单ID根节点为0
/// </summary>
public long ParentId { get; set; }
/// <summary>
/// 菜单名称
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// 权限编码
/// </summary>
public string Code { get; set; } = string.Empty;
/// <summary>
/// 前端路由路径
/// </summary>
public string? Path { get; set; }
/// <summary>
/// 前端组件路径
/// </summary>
public string? Component { get; set; }
/// <summary>
/// 菜单图标
/// </summary>
public string? Icon { get; set; }
/// <summary>
/// 排序值
/// </summary>
public int Sort { get; set; }
/// <summary>
/// 是否显示在菜单
/// </summary>
public bool IsVisible { get; set; } = true;
/// <summary>
/// 状态
/// </summary>
public int Status { get; set; } = 1;
}
/// <summary>
/// 后台菜单输出
/// </summary>
public class AdminMenuOutput
{
/// <summary>
/// 菜单ID
/// </summary>
public long Id { get; set; }
/// <summary>
/// 父级菜单ID
/// </summary>
public long ParentId { get; set; }
/// <summary>
/// 菜单名称
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// 权限编码
/// </summary>
public string Code { get; set; } = string.Empty;
/// <summary>
/// 前端路由路径
/// </summary>
public string? Path { get; set; }
/// <summary>
/// 前端组件路径
/// </summary>
public string? Component { get; set; }
/// <summary>
/// 菜单图标
/// </summary>
public string? Icon { get; set; }
/// <summary>
/// 排序值
/// </summary>
public int Sort { get; set; }
/// <summary>
/// 是否显示在菜单
/// </summary>
public bool IsVisible { get; set; }
/// <summary>
/// 状态
/// </summary>
public int Status { get; set; }
/// <summary>
/// 子菜单
/// </summary>
public List<AdminMenuOutput> Children { get; set; } = [];
}
/// <summary>
/// 后台菜单查询输入
/// </summary>
public class AdminMenuQueryInput
{
/// <summary>
/// 菜单名称
/// </summary>
public string? Name { get; set; }
/// <summary>
/// 权限编码
/// </summary>
public string? Code { get; set; }
/// <summary>
/// 状态
/// </summary>
public int? Status { get; set; }
}
/// <summary>
/// 后台角色输入
/// </summary>
public class AdminRoleInput
{
/// <summary>
/// 角色名称
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// 角色编码
/// </summary>
public string Code { get; set; } = string.Empty;
/// <summary>
/// 备注
/// </summary>
public string? Remark { get; set; }
/// <summary>
/// 状态
/// </summary>
public int Status { get; set; } = 1;
/// <summary>
/// 菜单ID集合
/// </summary>
public List<long> MenuIds { get; set; } = [];
}
/// <summary>
/// 后台角色输出
/// </summary>
public class AdminRoleOutput
{
/// <summary>
/// 角色ID
/// </summary>
public long Id { get; set; }
/// <summary>
/// 角色名称
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// 角色编码
/// </summary>
public string Code { get; set; } = string.Empty;
/// <summary>
/// 备注
/// </summary>
public string? Remark { get; set; }
/// <summary>
/// 状态
/// </summary>
public int Status { get; set; }
/// <summary>
/// 菜单ID集合
/// </summary>
public List<long> MenuIds { get; set; } = [];
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreatedAt { get; set; }
}
/// <summary>
/// 后台角色简要输出
/// </summary>
public class AdminRoleSimpleOutput
{
/// <summary>
/// 角色ID
/// </summary>
public long Id { get; set; }
/// <summary>
/// 角色名称
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// 角色编码
/// </summary>
public string Code { get; set; } = string.Empty;
}
/// <summary>
/// 后台角色查询输入
/// </summary>
public class AdminRoleQueryInput : PageQueryModel
{
/// <summary>
/// 角色名称
/// </summary>
public string? Name { get; set; }
/// <summary>
/// 角色编码
/// </summary>
public string? Code { get; set; }
/// <summary>
/// 状态
/// </summary>
public int? Status { get; set; }
}
/// <summary>
/// 分配角色菜单输入
/// </summary>
public class AssignRoleMenusInput
{
/// <summary>
/// 菜单ID集合
/// </summary>
public List<long> MenuIds { get; set; } = [];
}
/// <summary>
/// 分配用户角色输入
/// </summary>
public class AssignAdminUserRolesInput
{
/// <summary>
/// 角色ID集合
/// </summary>
public List<long> RoleIds { get; set; } = [];
}

View File

@ -27,6 +27,11 @@ public class AdminUserInput
/// 状态: Active, Inactive /// 状态: Active, Inactive
/// </summary> /// </summary>
public int Status { get; set; } = 1; public int Status { get; set; } = 1;
/// <summary>
/// 角色ID集合更新时为null表示不调整角色
/// </summary>
public List<long>? RoleIds { get; set; }
} }
/// <summary> /// <summary>
@ -73,6 +78,16 @@ public class AdminUserOutput
/// 更新时间 /// 更新时间
/// </summary> /// </summary>
public DateTime? UpdatedAt { get; set; } public DateTime? UpdatedAt { get; set; }
/// <summary>
/// 角色ID集合
/// </summary>
public List<long> RoleIds { get; set; } = [];
/// <summary>
/// 角色集合
/// </summary>
public List<AdminRoleSimpleOutput> Roles { get; set; } = [];
} }
/// <summary> /// <summary>

View File

@ -26,6 +26,26 @@ public class AdminLoginOutput
public string UserName { get; set; } = string.Empty; public string UserName { get; set; } = string.Empty;
public string Type { get; set; } = string.Empty; public string Type { get; set; } = string.Empty;
/// <summary>
/// 角色ID集合
/// </summary>
public List<long> RoleIds { get; set; } = [];
/// <summary>
/// 角色集合
/// </summary>
public List<AdminRoleSimpleOutput> Roles { get; set; } = [];
/// <summary>
/// 菜单树
/// </summary>
public List<AdminMenuOutput> Menus { get; set; } = [];
/// <summary>
/// 权限编码集合
/// </summary>
public List<string> PermissionCodes { get; set; } = [];
} }
public class AdminUserInfoOutput public class AdminUserInfoOutput
@ -37,4 +57,24 @@ public class AdminUserInfoOutput
public string Type { get; set; } = string.Empty; public string Type { get; set; } = string.Empty;
public int Status { get; set; } public int Status { get; set; }
/// <summary>
/// 角色ID集合
/// </summary>
public List<long> RoleIds { get; set; } = [];
/// <summary>
/// 角色集合
/// </summary>
public List<AdminRoleSimpleOutput> Roles { get; set; } = [];
/// <summary>
/// 菜单树
/// </summary>
public List<AdminMenuOutput> Menus { get; set; } = [];
/// <summary>
/// 权限编码集合
/// </summary>
public List<string> PermissionCodes { get; set; } = [];
} }

View File

@ -0,0 +1,66 @@
using SqlSugar;
namespace QYZH.InteractiveMagazine.Models.Entity;
/// <summary>
/// 后台菜单表
/// </summary>
[SugarTable("AdminMenu")]
public partial class AdminMenu : SqlSugarBaseEntity
{
/// <summary>
/// Desc:父级菜单ID根节点为0
/// Default:0
/// Nullable:False
/// </summary>
public long ParentId { get; set; }
/// <summary>
/// Desc:菜单名称
/// Default:
/// Nullable:False
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// Desc:权限编码
/// Default:
/// Nullable:False
/// </summary>
public string Code { get; set; } = string.Empty;
/// <summary>
/// Desc:前端路由路径
/// Default:
/// Nullable:True
/// </summary>
public string? Path { get; set; }
/// <summary>
/// Desc:前端组件路径
/// Default:
/// Nullable:True
/// </summary>
public string? Component { get; set; }
/// <summary>
/// Desc:菜单图标
/// Default:
/// Nullable:True
/// </summary>
public string? Icon { get; set; }
/// <summary>
/// Desc:排序值
/// Default:0
/// Nullable:False
/// </summary>
public int Sort { get; set; }
/// <summary>
/// Desc:是否显示在菜单
/// Default:b'1'
/// Nullable:False
/// </summary>
public bool IsVisible { get; set; } = true;
}

View File

@ -0,0 +1,31 @@
using SqlSugar;
namespace QYZH.InteractiveMagazine.Models.Entity;
/// <summary>
/// 后台角色表
/// </summary>
[SugarTable("AdminRole")]
public partial class AdminRole : SqlSugarBaseEntity
{
/// <summary>
/// Desc:角色名称
/// Default:
/// Nullable:False
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// Desc:角色编码
/// Default:
/// Nullable:False
/// </summary>
public string Code { get; set; } = string.Empty;
/// <summary>
/// Desc:备注
/// Default:
/// Nullable:True
/// </summary>
public string? Remark { get; set; }
}

View File

@ -0,0 +1,24 @@
using SqlSugar;
namespace QYZH.InteractiveMagazine.Models.Entity;
/// <summary>
/// 后台角色菜单关系表
/// </summary>
[SugarTable("AdminRoleMenu")]
public partial class AdminRoleMenu : SqlSugarBaseEntity
{
/// <summary>
/// Desc:角色ID
/// Default:
/// Nullable:False
/// </summary>
public long RoleId { get; set; }
/// <summary>
/// Desc:菜单ID
/// Default:
/// Nullable:False
/// </summary>
public long MenuId { get; set; }
}

View File

@ -0,0 +1,24 @@
using SqlSugar;
namespace QYZH.InteractiveMagazine.Models.Entity;
/// <summary>
/// 后台管理员角色关系表
/// </summary>
[SugarTable("AdminUserRole")]
public partial class AdminUserRole : SqlSugarBaseEntity
{
/// <summary>
/// Desc:管理员ID
/// Default:
/// Nullable:False
/// </summary>
public long AdminUserId { get; set; }
/// <summary>
/// Desc:角色ID
/// Default:
/// Nullable:False
/// </summary>
public long RoleId { get; set; }
}

View File

@ -12,7 +12,11 @@ using QYZH.InteractiveMagazine.Repository;
namespace QYZH.InteractiveMagazine.Service; namespace QYZH.InteractiveMagazine.Service;
public class AdminAuthService(BaseRepository<AdminUser> adminUserRepository, IConfiguration configuration, ILogger<AdminAuthService> logger) : BaseRepository<AdminUser>, IAdminAuthService public class AdminAuthService(
BaseRepository<AdminUser> adminUserRepository,
IAdminPermissionService adminPermissionService,
IConfiguration configuration,
ILogger<AdminAuthService> logger) : BaseRepository<AdminUser>, IAdminAuthService
{ {
private const string TokenKeyPrefix = "InteractiveMagazine:AdminAuth:Token"; private const string TokenKeyPrefix = "InteractiveMagazine:AdminAuth:Token";
@ -60,12 +64,20 @@ public class AdminAuthService(BaseRepository<AdminUser> adminUserRepository, ICo
logger.LogInformation("管理员登录成功,用户名: {UserName}, ID: {UserId}", input.UserName, adminUser.Id); 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 return new AdminLoginOutput
{ {
Token = token, Token = token,
UserId = (long)adminUser.Id, UserId = (long)adminUser.Id,
UserName = adminUser.UserName, UserName = adminUser.UserName,
Type = adminUser.Type.ToString(), 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<AdminUser> adminUserRepository, ICo
throw new BusinessException("用户不存在", ResultCode.NOT_FOUND); 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 return new AdminUserInfoOutput
{ {
UserId = adminUser.Id, UserId = adminUser.Id,
UserName = adminUser.UserName, UserName = adminUser.UserName,
Type = adminUser.Type.ToString(), Type = adminUser.Type.ToString(),
Status = adminUser.Status Status = adminUser.Status,
RoleIds = roles.Select(x => x.Id).ToList(),
Roles = roles,
Menus = menus,
PermissionCodes = permissionCodes
}; };
} }

View File

@ -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;
/// <summary>
/// 后台权限管理服务实现
/// </summary>
public class AdminPermissionService(
BaseRepository<AdminRole> adminRoleRepository,
BaseRepository<AdminMenu> adminMenuRepository,
BaseRepository<AdminRoleMenu> adminRoleMenuRepository,
BaseRepository<AdminUserRole> adminUserRoleRepository,
BaseRepository<AdminUser> adminUserRepository,
ILogger<AdminPermissionService> logger) : BaseRepository<AdminRole>, IAdminPermissionService
{
/// <summary>
/// 创建菜单
/// </summary>
public async Task<AdminMenuOutput> 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);
}
/// <summary>
/// 更新菜单
/// </summary>
public async Task<AdminMenuOutput> 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);
}
/// <summary>
/// 删除菜单
/// </summary>
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);
}
/// <summary>
/// 获取菜单树
/// </summary>
public async Task<List<AdminMenuOutput>> 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);
}
/// <summary>
/// 创建角色
/// </summary>
public async Task<AdminRoleOutput> 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);
}
/// <summary>
/// 更新角色
/// </summary>
public async Task<AdminRoleOutput> 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);
}
/// <summary>
/// 删除角色
/// </summary>
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);
});
}
/// <summary>
/// 获取角色详情
/// </summary>
public async Task<AdminRoleOutput> 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);
}
/// <summary>
/// 获取角色分页列表
/// </summary>
public async Task<PageListModel<AdminRoleOutput>> GetRoleListAsync(AdminRoleQueryInput input)
{
RefAsync<int> 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<AdminRoleOutput>(outputs, input.PageIndex, input.PageSize, totalNumber);
}
/// <summary>
/// 分配角色菜单
/// </summary>
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));
}
/// <summary>
/// 分配管理员角色
/// </summary>
public async Task AssignAdminUserRolesAsync(long adminUserId, AssignAdminUserRolesInput input)
{
await ReplaceAdminUserRolesAsync(adminUserId, input.RoleIds);
}
/// <summary>
/// 获取管理员菜单树
/// </summary>
public async Task<List<AdminMenuOutput>> GetAdminUserMenuTreeAsync(long adminUserId)
{
var menus = await GetAdminUserMenusAsync(adminUserId, true);
return BuildMenuTree(menus);
}
/// <summary>
/// 获取管理员角色
/// </summary>
public async Task<List<AdminRoleSimpleOutput>> GetAdminUserRolesAsync(long adminUserId)
{
var adminUser = await adminUserRepository.GetByIdAsync(adminUserId);
BusinessException.ThrowIf(adminUser == null, "管理员不存在", ResultCode.NOT_FOUND);
return await adminRoleRepository.Queryable()
.InnerJoin<AdminUserRole>((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();
}
/// <summary>
/// 获取管理员权限编码
/// </summary>
public async Task<List<string>> 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<long> 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<long> 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<long> 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<long> 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<List<AdminMenu>> 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<AdminRoleMenu>((menu, roleMenu) => menu.Id == roleMenu.MenuId)
.InnerJoin<AdminRole>((menu, roleMenu, role) => roleMenu.RoleId == role.Id)
.InnerJoin<AdminUserRole>((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<AdminMenuOutput> BuildMenuTree(List<AdminMenu> 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<long> menuIds)
{
return new AdminRoleOutput
{
Id = role.Id,
Name = role.Name,
Code = role.Code,
Remark = role.Remark,
Status = role.Status,
MenuIds = menuIds,
CreatedAt = role.CreatedAt
};
}
}

View File

@ -15,7 +15,10 @@ namespace QYZH.InteractiveMagazine.Service;
/// <summary> /// <summary>
/// 管理员用户服务实现 /// 管理员用户服务实现
/// </summary> /// </summary>
public class AdminUserService(BaseRepository<AdminUser> adminUserRepository, ILogger<AdminUserService> logger) : BaseRepository<AdminUser>, IAdminUserService public class AdminUserService(
BaseRepository<AdminUser> adminUserRepository,
IAdminPermissionService adminPermissionService,
ILogger<AdminUserService> logger) : BaseRepository<AdminUser>, IAdminUserService
{ {
/// <summary> /// <summary>
@ -63,9 +66,14 @@ public class AdminUserService(BaseRepository<AdminUser> adminUserRepository, ILo
throw new BusinessException("创建管理员失败", ResultCode.GLOBAL_ERROR); 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); 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);
} }
/// <summary> /// <summary>
@ -114,9 +122,14 @@ public class AdminUserService(BaseRepository<AdminUser> adminUserRepository, ILo
throw new BusinessException("更新管理员失败", ResultCode.GLOBAL_ERROR); 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); 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);
} }
/// <summary> /// <summary>
@ -156,17 +169,7 @@ public class AdminUserService(BaseRepository<AdminUser> adminUserRepository, ILo
logger.LogWarning("未找到管理员ID: {Id}", id); logger.LogWarning("未找到管理员ID: {Id}", id);
throw new BusinessException("管理员不存在", ResultCode.NOT_FOUND); throw new BusinessException("管理员不存在", ResultCode.NOT_FOUND);
} }
return new AdminUserOutput return await ToAdminUserOutputAsync(adminUser);
{
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
};
} }
/// <summary> /// <summary>
@ -201,6 +204,12 @@ public class AdminUserService(BaseRepository<AdminUser> adminUserRepository, ILo
UpdatedAt = a.UpdatedAt UpdatedAt = a.UpdatedAt
}, true) }, true)
.ToPageListAsync(input.PageIndex, input.PageSize, totalNumber); .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<AdminUserOutput>(pageResult, input.PageIndex, input.PageSize, totalNumber); return new PageListModel<AdminUserOutput>(pageResult, input.PageIndex, input.PageSize, totalNumber);
} }
@ -231,4 +240,22 @@ public class AdminUserService(BaseRepository<AdminUser> adminUserRepository, ILo
logger.LogInformation("管理员状态更新成功ID: {Id}", id); logger.LogInformation("管理员状态更新成功ID: {Id}", id);
} }
private async Task<AdminUserOutput> 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
};
}
} }

View File

@ -3,7 +3,7 @@ using QYZH.InteractiveMagazine.IService;
using QYZH.InteractiveMagazine.Models.Dto; using QYZH.InteractiveMagazine.Models.Dto;
using QYZH.InteractiveMagazine.Models.Dto.Bag; using QYZH.InteractiveMagazine.Models.Dto.Bag;
namespace QYZH.InteractiveMagazine.WebApi.Controllers.WeChat; namespace QYZH.InteractiveMagazine.WeChatApi.Controllers;
/// <summary> /// <summary>
/// 小程序背包控制器 /// 小程序背包控制器

View File

@ -4,7 +4,7 @@ using QYZH.InteractiveMagazine.Models.Common;
using QYZH.InteractiveMagazine.Models.Dto; using QYZH.InteractiveMagazine.Models.Dto;
using QYZH.InteractiveMagazine.Models.Dto.CheckIn; using QYZH.InteractiveMagazine.Models.Dto.CheckIn;
namespace QYZH.InteractiveMagazine.WebApi.Controllers.WeChat; namespace QYZH.InteractiveMagazine.WeChatApi.Controllers;
/// <summary> /// <summary>
/// 签到控制器 /// 签到控制器

View File

@ -3,7 +3,7 @@ using QYZH.InteractiveMagazine.IService;
using QYZH.InteractiveMagazine.Models.Common; using QYZH.InteractiveMagazine.Models.Common;
using QYZH.InteractiveMagazine.Models.Dto; using QYZH.InteractiveMagazine.Models.Dto;
namespace QYZH.InteractiveMagazine.WebApi.Controllers.WeChat; namespace QYZH.InteractiveMagazine.WeChatApi.Controllers;
/// <summary> /// <summary>
/// 小程序社区控制器 /// 小程序社区控制器

View File

@ -3,7 +3,7 @@ using QYZH.InteractiveMagazine.IService;
using QYZH.InteractiveMagazine.Models.Common; using QYZH.InteractiveMagazine.Models.Common;
using QYZH.InteractiveMagazine.Models.Dto; using QYZH.InteractiveMagazine.Models.Dto;
namespace QYZH.InteractiveMagazine.WebApi.Controllers.WeChat; namespace QYZH.InteractiveMagazine.WeChatApi.Controllers;
/// <summary> /// <summary>
/// 小程序期刊管理控制器 /// 小程序期刊管理控制器

View File

@ -3,7 +3,7 @@ using QYZH.InteractiveMagazine.IService;
using QYZH.InteractiveMagazine.Models.Dto; using QYZH.InteractiveMagazine.Models.Dto;
using QYZH.InteractiveMagazine.Models.Dto.Mall; using QYZH.InteractiveMagazine.Models.Dto.Mall;
namespace QYZH.InteractiveMagazine.WebApi.Controllers.WeChat; namespace QYZH.InteractiveMagazine.WeChatApi.Controllers;
/// <summary> /// <summary>
/// 小程序商城控制器 /// 小程序商城控制器

View File

@ -3,7 +3,7 @@ using QYZH.InteractiveMagazine.IService;
using QYZH.InteractiveMagazine.Models.Common; using QYZH.InteractiveMagazine.Models.Common;
using QYZH.InteractiveMagazine.Models.Dto; using QYZH.InteractiveMagazine.Models.Dto;
namespace QYZH.InteractiveMagazine.WebApi.Controllers.WeChat; namespace QYZH.InteractiveMagazine.WeChatApi.Controllers;
/// <summary> /// <summary>
/// 小程序勋章控制器 /// 小程序勋章控制器

View File

@ -4,7 +4,7 @@ using QYZH.InteractiveMagazine.Models.Common;
using QYZH.InteractiveMagazine.Models.Dto; using QYZH.InteractiveMagazine.Models.Dto;
using QYZH.InteractiveMagazine.Models.Dto.Pet; using QYZH.InteractiveMagazine.Models.Dto.Pet;
namespace QYZH.InteractiveMagazine.WebApi.Controllers.WeChat; namespace QYZH.InteractiveMagazine.WeChatApi.Controllers;
/// <summary> /// <summary>
/// 小程序宠物管理控制器 /// 小程序宠物管理控制器

View File

@ -5,7 +5,7 @@ using QYZH.InteractiveMagazine.Models.Dto;
using QYZH.InteractiveMagazine.Models.Dto.UserAnswerTaskService; using QYZH.InteractiveMagazine.Models.Dto.UserAnswerTaskService;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
namespace QYZH.InteractiveMagazine.WebApi.Controllers.WeChat; namespace QYZH.InteractiveMagazine.WeChatApi.Controllers;
/// <summary> /// <summary>
/// 小程序首页控制器 /// 小程序首页控制器

View File

@ -6,7 +6,7 @@ using QYZH.InteractiveMagazine.Models.Common;
using QYZH.InteractiveMagazine.Models.Dto; using QYZH.InteractiveMagazine.Models.Dto;
using QYZH.InteractiveMagazine.Models.WeChat; using QYZH.InteractiveMagazine.Models.WeChat;
namespace QYZH.InteractiveMagazine.WebApi.Controllers.WeChat; namespace QYZH.InteractiveMagazine.WeChatApi.Controllers;
/// <summary> /// <summary>
/// 微信小程序认证控制器 /// 微信小程序认证控制器

View File

@ -5,7 +5,7 @@ using QYZH.InteractiveMagazine.Models.Dto;
using QYZH.InteractiveMagazine.Models.Enum; using QYZH.InteractiveMagazine.Models.Enum;
using System.Security.Claims; using System.Security.Claims;
namespace QYZH.InteractiveMagazine.WebApi.Controllers.WeChat; namespace QYZH.InteractiveMagazine.WeChatApi.Controllers;
/// <summary> /// <summary>
/// 小程序基础控制器 /// 小程序基础控制器

View File

@ -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<ModelValidActionFilterAttribute>();
})
.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<MedalRuleConfigSettings>(builder.Configuration.GetSection("MedalRuleConfig"));
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowAll", policy =>
{
policy.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
});
});
builder.Services.Configure<BrotliCompressionProviderOptions>(options =>
{
options.Level = System.IO.Compression.CompressionLevel.Optimal;
});
builder.Services.Configure<GzipCompressionProviderOptions>(options =>
{
options.Level = System.IO.Compression.CompressionLevel.Fastest;
});
builder.Services.AddResponseCompression(options =>
{
options.EnableForHttps = true;
options.Providers.Add<BrotliCompressionProvider>();
options.Providers.Add<GzipCompressionProvider>();
options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(["image/svg+xml", "application/json", "text/plain"]);
});
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<ApiExplorerSettingsAttribute>()
.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<GlobalExceptionMiddleware>();
app.UseMiddleware<OperationLogMiddleware>();
app.UseMiddleware<JwtAutoRefreshMiddleware>();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();

View File

@ -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"
}
}
}
}

View File

@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Autofac" Version="9.1.0" />
<PackageReference Include="Autofac.Extensions.DependencyInjection" Version="11.0.0" />
<PackageReference Include="BCrypt.Net-Next" Version="4.2.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\QYZH.InteractiveMagazine.IService\QYZH.InteractiveMagazine.IService.csproj" />
<ProjectReference Include="..\QYZH.InteractiveMagazine.Service\QYZH.InteractiveMagazine.Service.csproj" />
<ProjectReference Include="..\QYZH.InteractiveMagazine.Infrastructure\QYZH.InteractiveMagazine.Infrastructure.csproj" />
<ProjectReference Include="..\QYZH.InteractiveMagazine.Common\QYZH.InteractiveMagazine.Common.csproj" />
<ProjectReference Include="..\QYZH.InteractiveMagazine.Models\QYZH.InteractiveMagazine.Models.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@ -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/"
}
}

View File

@ -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": "说服力"
}
]
}
]
}
}

View File

@ -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;
/// <summary>
/// 后台权限管理
/// </summary>
[ApiController]
[Route("api/[controller]")]
[ApiExplorerSettings(GroupName = nameof(ApiVersionEnum.Platform))]
public class PermissionController(IAdminPermissionService adminPermissionService) : BaseController
{
/// <summary>
/// 创建菜单
/// </summary>
[HttpPost("menus")]
public async Task<BaseResponse<AdminMenuOutput>> CreateMenuAsync([FromBody] AdminMenuInput input)
{
var result = await adminPermissionService.CreateMenuAsync(input);
return Success(result);
}
/// <summary>
/// 更新菜单
/// </summary>
[HttpPut("menus/{id}")]
public async Task<BaseResponse<AdminMenuOutput>> UpdateMenuAsync(long id, [FromBody] AdminMenuInput input)
{
var result = await adminPermissionService.UpdateMenuAsync(id, input);
return Success(result);
}
/// <summary>
/// 删除菜单
/// </summary>
[HttpDelete("menus/{id}")]
public async Task<BaseResponse<object>> DeleteMenuAsync(long id)
{
await adminPermissionService.DeleteMenuAsync(id);
return Success(new object());
}
/// <summary>
/// 获取菜单树
/// </summary>
[HttpPost("menus/tree")]
public async Task<BaseResponse<List<AdminMenuOutput>>> GetMenuTreeAsync([FromBody] AdminMenuQueryInput input)
{
var result = await adminPermissionService.GetMenuTreeAsync(input);
return Success(result);
}
/// <summary>
/// 获取当前管理员菜单树
/// </summary>
[HttpGet("menus/current")]
public async Task<BaseResponse<List<AdminMenuOutput>>> GetCurrentMenuTreeAsync()
{
var userId = GetCurrentUserId();
BusinessException.ThrowIf(userId == null, "未获取到用户信息", ResultCode.DENY);
var result = await adminPermissionService.GetAdminUserMenuTreeAsync(userId!.Value);
return Success(result);
}
/// <summary>
/// 创建角色
/// </summary>
[HttpPost("roles")]
public async Task<BaseResponse<AdminRoleOutput>> CreateRoleAsync([FromBody] AdminRoleInput input)
{
var result = await adminPermissionService.CreateRoleAsync(input);
return Success(result);
}
/// <summary>
/// 更新角色
/// </summary>
[HttpPut("roles/{id}")]
public async Task<BaseResponse<AdminRoleOutput>> UpdateRoleAsync(long id, [FromBody] AdminRoleInput input)
{
var result = await adminPermissionService.UpdateRoleAsync(id, input);
return Success(result);
}
/// <summary>
/// 删除角色
/// </summary>
[HttpDelete("roles/{id}")]
public async Task<BaseResponse<object>> DeleteRoleAsync(long id)
{
await adminPermissionService.DeleteRoleAsync(id);
return Success(new object());
}
/// <summary>
/// 获取角色详情
/// </summary>
[HttpGet("roles/{id}")]
public async Task<BaseResponse<AdminRoleOutput>> GetRoleByIdAsync(long id)
{
var result = await adminPermissionService.GetRoleByIdAsync(id);
return Success(result);
}
/// <summary>
/// 获取角色分页列表
/// </summary>
[HttpPost("roles/list")]
public async Task<BaseResponse<PageListModel<AdminRoleOutput>>> GetRoleListAsync([FromBody] AdminRoleQueryInput input)
{
var result = await adminPermissionService.GetRoleListAsync(input);
return Success(result);
}
/// <summary>
/// 分配角色菜单
/// </summary>
[HttpPut("roles/{roleId}/menus")]
public async Task<BaseResponse<object>> AssignRoleMenusAsync(long roleId, [FromBody] AssignRoleMenusInput input)
{
await adminPermissionService.AssignRoleMenusAsync(roleId, input);
return Success(new object());
}
/// <summary>
/// 分配管理员角色
/// </summary>
[HttpPut("users/{adminUserId}/roles")]
public async Task<BaseResponse<object>> AssignAdminUserRolesAsync(long adminUserId, [FromBody] AssignAdminUserRolesInput input)
{
await adminPermissionService.AssignAdminUserRolesAsync(adminUserId, input);
return Success(new object());
}
}

View File

@ -90,7 +90,7 @@ builder.Services.AddSwaggerGen(option =>
var modelXml = Path.Combine(AppContext.BaseDirectory, $"QYZH.InteractiveMagazine.Models.xml"); var modelXml = Path.Combine(AppContext.BaseDirectory, $"QYZH.InteractiveMagazine.Models.xml");
Enum.GetValues<ApiVersionEnum>().ToList().ForEach(version => Enum.GetValues<ApiVersionEnum>().Where(version => version != ApiVersionEnum.Wechat).ToList().ForEach(version =>
{ {
// 配置文档信息 // 配置文档信息
option.SwaggerDoc(version.ToString(), new OpenApiInfo option.SwaggerDoc(version.ToString(), new OpenApiInfo
@ -201,7 +201,7 @@ var app = builder.Build();
app.UseSwaggerUI(c => app.UseSwaggerUI(c =>
{ {
// 根据版本名称倒序 遍历展示 // 根据版本名称倒序 遍历展示
Enum.GetValues<ApiVersionEnum>().OrderBy(e => e).ToList().ForEach(version => Enum.GetValues<ApiVersionEnum>().Where(version => version != ApiVersionEnum.Wechat).OrderBy(e => e).ToList().ForEach(version =>
{ {
c.SwaggerEndpoint($"/swagger/{version}/swagger.json", $"{version.GetDescription()}接口"); c.SwaggerEndpoint($"/swagger/{version}/swagger.json", $"{version.GetDescription()}接口");
}); });
@ -220,4 +220,3 @@ app.UseAuthorization();
app.MapControllers(); app.MapControllers();
app.Run(); app.Run();

View File

@ -47,6 +47,7 @@ public class JournalTaskReceiveConsumer(
{ {
PropertyNameCaseInsensitive = true PropertyNameCaseInsensitive = true
}) ?? throw new InvalidOperationException("期刊任务消息内容为空"); }) ?? throw new InvalidOperationException("期刊任务消息内容为空");
data.Normalize();
if (data.Questions == null || data.Questions.Length == 0) if (data.Questions == null || data.Questions.Length == 0)
{ {
@ -1093,11 +1094,26 @@ public class QuestionData
/// </summary> /// </summary>
public long UserId { get; set; } public long UserId { get; set; }
/// <summary>
/// 学生ID
/// </summary>
public long StudentId { get; set; }
/// <summary> /// <summary>
/// 期刊ID /// 期刊ID
/// </summary> /// </summary>
public long JournalId { get; set; } public long JournalId { get; set; }
/// <summary>
/// 作业ID
/// </summary>
public long HomeworkId { get; set; }
/// <summary>
/// 书籍ID
/// </summary>
public long BookId { get; set; }
/// <summary> /// <summary>
/// 页ID /// 页ID
/// </summary> /// </summary>
@ -1117,6 +1133,22 @@ public class QuestionData
/// 创建时间 /// 创建时间
/// </summary> /// </summary>
public DateTime CreatedTime { get; set; } public DateTime CreatedTime { get; set; }
/// <summary>
/// 兼容新版作答消息字段
/// </summary>
public void Normalize()
{
if (UserId <= 0)
{
UserId = StudentId;
}
if (JournalId <= 0)
{
JournalId = BookId > 0 ? BookId : HomeworkId;
}
}
} }
/// <summary> /// <summary>

View File

@ -9,5 +9,6 @@
<Project Path="QYZH.InteractiveMagazine.Repository/QYZH.InteractiveMagazine.Repository.csproj" /> <Project Path="QYZH.InteractiveMagazine.Repository/QYZH.InteractiveMagazine.Repository.csproj" />
<Project Path="QYZH.InteractiveMagazine.Service/QYZH.InteractiveMagazine.Service.csproj" /> <Project Path="QYZH.InteractiveMagazine.Service/QYZH.InteractiveMagazine.Service.csproj" />
<Project Path="QYZH.InteractiveMagazine.WebApi/QYZH.InteractiveMagazine.WebApi.csproj" /> <Project Path="QYZH.InteractiveMagazine.WebApi/QYZH.InteractiveMagazine.WebApi.csproj" />
<Project Path="QYZH.InteractiveMagazine.WeChatApi/QYZH.InteractiveMagazine.WeChatApi.csproj" />
<Project Path="QYZH.InteractiveMagazine.WorkService/QYZH.InteractiveMagazine.WorkService.csproj" /> <Project Path="QYZH.InteractiveMagazine.WorkService/QYZH.InteractiveMagazine.WorkService.csproj" />
</Solution> </Solution>