临时数据
This commit is contained in:
@ -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);
|
|
||||||
5
Directory.Build.props
Normal file
5
Directory.Build.props
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<Project>
|
||||||
|
<PropertyGroup>
|
||||||
|
<NoWarn>$(NoWarn);CS0105;CS0108;CS0168;CS0169;CS0618;CS1570;CS1572;CS1573;CS1591;CS8600;CS8601;CS8602;CS8603;CS8604;CS8618;CS8625;CS8629;CS8634;CS8714;CS9113</NoWarn>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
473
QYZH.InteractiveMagazine.Common/Helpers/QrCodeHelper.cs
Normal file
473
QYZH.InteractiveMagazine.Common/Helpers/QrCodeHelper.cs
Normal file
@ -0,0 +1,473 @@
|
|||||||
|
using System.IO.Compression;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace QYZH.InteractiveMagazine.Common.Helpers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 二维码帮助类
|
||||||
|
/// </summary>
|
||||||
|
public static class QrCodeHelper
|
||||||
|
{
|
||||||
|
private const int Version = 4;
|
||||||
|
private const int Size = Version * 4 + 17;
|
||||||
|
private const int DataCodewordCount = 80;
|
||||||
|
private const int ErrorCorrectionCodewordCount = 20;
|
||||||
|
private const int QuietZone = 4;
|
||||||
|
private const int Scale = 10;
|
||||||
|
private static readonly int[] AlignmentPatternPositions = [6, 26];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 生成二维码PNG图片字节
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="content">二维码内容</param>
|
||||||
|
/// <returns>PNG图片字节</returns>
|
||||||
|
public static byte[] GeneratePng(string content)
|
||||||
|
{
|
||||||
|
var dataCodewords = CreateDataCodewords(content);
|
||||||
|
var allCodewords = dataCodewords.Concat(CreateErrorCorrectionCodewords(dataCodewords)).ToArray();
|
||||||
|
var modules = BuildModules(allCodewords);
|
||||||
|
var imageSize = (Size + QuietZone * 2) * Scale;
|
||||||
|
var pixels = new byte[imageSize * imageSize * 4];
|
||||||
|
|
||||||
|
for (var i = 0; i < pixels.Length; i += 4)
|
||||||
|
{
|
||||||
|
pixels[i] = 255;
|
||||||
|
pixels[i + 1] = 255;
|
||||||
|
pixels[i + 2] = 255;
|
||||||
|
pixels[i + 3] = 255;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var y = 0; y < Size; y++)
|
||||||
|
{
|
||||||
|
for (var x = 0; x < Size; x++)
|
||||||
|
{
|
||||||
|
if (modules[y, x])
|
||||||
|
{
|
||||||
|
FillModule(pixels, imageSize, x + QuietZone, y + QuietZone);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return EncodePng(imageSize, imageSize, pixels);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] CreateDataCodewords(string content)
|
||||||
|
{
|
||||||
|
var bytes = Encoding.UTF8.GetBytes(content);
|
||||||
|
if (bytes.Length > 78)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("二维码内容过长");
|
||||||
|
}
|
||||||
|
|
||||||
|
var bits = new List<bool>();
|
||||||
|
AppendBits(bits, 0b0100, 4);
|
||||||
|
AppendBits(bits, bytes.Length, 8);
|
||||||
|
foreach (var value in bytes)
|
||||||
|
{
|
||||||
|
AppendBits(bits, value, 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
var remaining = DataCodewordCount * 8 - bits.Count;
|
||||||
|
AppendBits(bits, 0, Math.Min(4, remaining));
|
||||||
|
|
||||||
|
while (bits.Count % 8 != 0)
|
||||||
|
{
|
||||||
|
bits.Add(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
var data = BitsToBytes(bits);
|
||||||
|
var pad = true;
|
||||||
|
while (data.Count < DataCodewordCount)
|
||||||
|
{
|
||||||
|
data.Add((byte)(pad ? 0xEC : 0x11));
|
||||||
|
pad = !pad;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool[,] BuildModules(byte[] codewords)
|
||||||
|
{
|
||||||
|
var modules = new bool[Size, Size];
|
||||||
|
var reserved = new bool[Size, Size];
|
||||||
|
|
||||||
|
AddFinder(modules, reserved, 0, 0);
|
||||||
|
AddFinder(modules, reserved, Size - 7, 0);
|
||||||
|
AddFinder(modules, reserved, 0, Size - 7);
|
||||||
|
AddTimingPatterns(modules, reserved);
|
||||||
|
AddAlignmentPatterns(modules, reserved);
|
||||||
|
ReserveFormatAreas(reserved);
|
||||||
|
|
||||||
|
modules[Version * 4 + 9, 8] = true;
|
||||||
|
reserved[Version * 4 + 9, 8] = true;
|
||||||
|
|
||||||
|
AddDataModules(modules, reserved, codewords);
|
||||||
|
AddFormatBits(modules);
|
||||||
|
|
||||||
|
return modules;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddFinder(bool[,] modules, bool[,] reserved, int left, int top)
|
||||||
|
{
|
||||||
|
for (var y = -1; y <= 7; y++)
|
||||||
|
{
|
||||||
|
for (var x = -1; x <= 7; x++)
|
||||||
|
{
|
||||||
|
var px = left + x;
|
||||||
|
var py = top + y;
|
||||||
|
if (px < 0 || py < 0 || px >= Size || py >= Size)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
reserved[py, px] = true;
|
||||||
|
modules[py, px] = x >= 0 && x <= 6 && y >= 0 && y <= 6
|
||||||
|
&& (x == 0 || x == 6 || y == 0 || y == 6 || (x >= 2 && x <= 4 && y >= 2 && y <= 4));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddTimingPatterns(bool[,] modules, bool[,] reserved)
|
||||||
|
{
|
||||||
|
for (var i = 8; i < Size - 8; i++)
|
||||||
|
{
|
||||||
|
var isDark = i % 2 == 0;
|
||||||
|
modules[6, i] = isDark;
|
||||||
|
modules[i, 6] = isDark;
|
||||||
|
reserved[6, i] = true;
|
||||||
|
reserved[i, 6] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddAlignmentPatterns(bool[,] modules, bool[,] reserved)
|
||||||
|
{
|
||||||
|
foreach (var centerY in AlignmentPatternPositions)
|
||||||
|
{
|
||||||
|
foreach (var centerX in AlignmentPatternPositions)
|
||||||
|
{
|
||||||
|
if (reserved[centerY, centerX])
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var y = -2; y <= 2; y++)
|
||||||
|
{
|
||||||
|
for (var x = -2; x <= 2; x++)
|
||||||
|
{
|
||||||
|
var px = centerX + x;
|
||||||
|
var py = centerY + y;
|
||||||
|
reserved[py, px] = true;
|
||||||
|
modules[py, px] = Math.Max(Math.Abs(x), Math.Abs(y)) != 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ReserveFormatAreas(bool[,] reserved)
|
||||||
|
{
|
||||||
|
for (var i = 0; i <= 8; i++)
|
||||||
|
{
|
||||||
|
reserved[8, i] = true;
|
||||||
|
reserved[i, 8] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < 8; i++)
|
||||||
|
{
|
||||||
|
reserved[8, Size - 1 - i] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < 7; i++)
|
||||||
|
{
|
||||||
|
reserved[Size - 1 - i, 8] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddDataModules(bool[,] modules, bool[,] reserved, byte[] codewords)
|
||||||
|
{
|
||||||
|
var bits = new List<bool>();
|
||||||
|
foreach (var codeword in codewords)
|
||||||
|
{
|
||||||
|
AppendBits(bits, codeword, 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
var bitIndex = 0;
|
||||||
|
var upward = true;
|
||||||
|
for (var right = Size - 1; right >= 1; right -= 2)
|
||||||
|
{
|
||||||
|
if (right == 6)
|
||||||
|
{
|
||||||
|
right--;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < Size; i++)
|
||||||
|
{
|
||||||
|
var y = upward ? Size - 1 - i : i;
|
||||||
|
for (var j = 0; j < 2; j++)
|
||||||
|
{
|
||||||
|
var x = right - j;
|
||||||
|
if (reserved[y, x])
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var bit = bitIndex < bits.Count && bits[bitIndex++];
|
||||||
|
if ((x + y) % 2 == 0)
|
||||||
|
{
|
||||||
|
bit = !bit;
|
||||||
|
}
|
||||||
|
|
||||||
|
modules[y, x] = bit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
upward = !upward;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddFormatBits(bool[,] modules)
|
||||||
|
{
|
||||||
|
var format = GetFormatBits();
|
||||||
|
|
||||||
|
for (var i = 0; i <= 5; i++)
|
||||||
|
{
|
||||||
|
modules[i, 8] = GetBit(format, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
modules[7, 8] = GetBit(format, 6);
|
||||||
|
modules[8, 8] = GetBit(format, 7);
|
||||||
|
modules[8, 7] = GetBit(format, 8);
|
||||||
|
|
||||||
|
for (var i = 9; i < 15; i++)
|
||||||
|
{
|
||||||
|
modules[8, 14 - i] = GetBit(format, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < 8; i++)
|
||||||
|
{
|
||||||
|
modules[8, Size - 1 - i] = GetBit(format, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 8; i < 15; i++)
|
||||||
|
{
|
||||||
|
modules[Size - 15 + i, 8] = GetBit(format, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
modules[Size - 8, 8] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int GetFormatBits()
|
||||||
|
{
|
||||||
|
const int errorCorrectionLevelBits = 1;
|
||||||
|
const int mask = 0;
|
||||||
|
var data = (errorCorrectionLevelBits << 3) | mask;
|
||||||
|
var value = data << 10;
|
||||||
|
const int generator = 0x537;
|
||||||
|
|
||||||
|
for (var i = 14; i >= 10; i--)
|
||||||
|
{
|
||||||
|
if (((value >> i) & 1) != 0)
|
||||||
|
{
|
||||||
|
value ^= generator << (i - 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ((data << 10) | (value & 0x3FF)) ^ 0x5412;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] CreateErrorCorrectionCodewords(byte[] data)
|
||||||
|
{
|
||||||
|
var generator = CreateGeneratorPolynomial(ErrorCorrectionCodewordCount);
|
||||||
|
var result = new byte[ErrorCorrectionCodewordCount];
|
||||||
|
|
||||||
|
foreach (var b in data)
|
||||||
|
{
|
||||||
|
var factor = b ^ result[0];
|
||||||
|
Array.Copy(result, 1, result, 0, result.Length - 1);
|
||||||
|
result[^1] = 0;
|
||||||
|
|
||||||
|
for (var i = 0; i < result.Length; i++)
|
||||||
|
{
|
||||||
|
result[i] ^= GaloisMultiply(generator[i + 1], factor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] CreateGeneratorPolynomial(int degree)
|
||||||
|
{
|
||||||
|
var result = new List<byte> { 1 };
|
||||||
|
for (var i = 0; i < degree; i++)
|
||||||
|
{
|
||||||
|
var next = new byte[result.Count + 1];
|
||||||
|
for (var j = 0; j < result.Count; j++)
|
||||||
|
{
|
||||||
|
next[j] ^= result[j];
|
||||||
|
next[j + 1] ^= GaloisMultiply(result[j], GaloisPower(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
result = next.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte GaloisPower(int exponent)
|
||||||
|
{
|
||||||
|
var value = 1;
|
||||||
|
for (var i = 0; i < exponent; i++)
|
||||||
|
{
|
||||||
|
value <<= 1;
|
||||||
|
if ((value & 0x100) != 0)
|
||||||
|
{
|
||||||
|
value ^= 0x11D;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (byte)value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte GaloisMultiply(int x, int y)
|
||||||
|
{
|
||||||
|
var result = 0;
|
||||||
|
while (y != 0)
|
||||||
|
{
|
||||||
|
if ((y & 1) != 0)
|
||||||
|
{
|
||||||
|
result ^= x;
|
||||||
|
}
|
||||||
|
|
||||||
|
x <<= 1;
|
||||||
|
if ((x & 0x100) != 0)
|
||||||
|
{
|
||||||
|
x ^= 0x11D;
|
||||||
|
}
|
||||||
|
|
||||||
|
y >>= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (byte)result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AppendBits(List<bool> bits, int value, int count)
|
||||||
|
{
|
||||||
|
for (var i = count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
bits.Add(((value >> i) & 1) != 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<byte> BitsToBytes(List<bool> bits)
|
||||||
|
{
|
||||||
|
var result = new List<byte>();
|
||||||
|
for (var i = 0; i < bits.Count; i += 8)
|
||||||
|
{
|
||||||
|
var value = 0;
|
||||||
|
for (var j = 0; j < 8; j++)
|
||||||
|
{
|
||||||
|
value = (value << 1) | (bits[i + j] ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Add((byte)value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool GetBit(int value, int index)
|
||||||
|
{
|
||||||
|
return ((value >> index) & 1) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void FillModule(byte[] pixels, int imageSize, int moduleX, int moduleY)
|
||||||
|
{
|
||||||
|
var startX = moduleX * Scale;
|
||||||
|
var startY = moduleY * Scale;
|
||||||
|
|
||||||
|
for (var y = 0; y < Scale; y++)
|
||||||
|
{
|
||||||
|
for (var x = 0; x < Scale; x++)
|
||||||
|
{
|
||||||
|
var index = ((startY + y) * imageSize + startX + x) * 4;
|
||||||
|
pixels[index] = 0;
|
||||||
|
pixels[index + 1] = 0;
|
||||||
|
pixels[index + 2] = 0;
|
||||||
|
pixels[index + 3] = 255;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] EncodePng(int width, int height, byte[] rgba)
|
||||||
|
{
|
||||||
|
using var output = new MemoryStream();
|
||||||
|
WriteUInt(output, 0x89504E47);
|
||||||
|
WriteUInt(output, 0x0D0A1A0A);
|
||||||
|
|
||||||
|
using (var ihdr = new MemoryStream())
|
||||||
|
{
|
||||||
|
WriteUInt(ihdr, (uint)width);
|
||||||
|
WriteUInt(ihdr, (uint)height);
|
||||||
|
ihdr.WriteByte(8);
|
||||||
|
ihdr.WriteByte(6);
|
||||||
|
ihdr.WriteByte(0);
|
||||||
|
ihdr.WriteByte(0);
|
||||||
|
ihdr.WriteByte(0);
|
||||||
|
WriteChunk(output, "IHDR", ihdr.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
var stride = width * 4;
|
||||||
|
using (var raw = new MemoryStream())
|
||||||
|
{
|
||||||
|
for (var y = 0; y < height; y++)
|
||||||
|
{
|
||||||
|
raw.WriteByte(0);
|
||||||
|
raw.Write(rgba, y * stride, stride);
|
||||||
|
}
|
||||||
|
|
||||||
|
using var compressed = new MemoryStream();
|
||||||
|
using (var zlib = new ZLibStream(compressed, CompressionLevel.SmallestSize, true))
|
||||||
|
{
|
||||||
|
raw.Position = 0;
|
||||||
|
raw.CopyTo(zlib);
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteChunk(output, "IDAT", compressed.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteChunk(output, "IEND", []);
|
||||||
|
return output.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteChunk(Stream stream, string type, byte[] data)
|
||||||
|
{
|
||||||
|
WriteUInt(stream, (uint)data.Length);
|
||||||
|
var typeBytes = Encoding.ASCII.GetBytes(type);
|
||||||
|
stream.Write(typeBytes);
|
||||||
|
stream.Write(data);
|
||||||
|
WriteUInt(stream, Crc32(typeBytes.Concat(data).ToArray()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteUInt(Stream stream, uint value)
|
||||||
|
{
|
||||||
|
stream.WriteByte((byte)(value >> 24));
|
||||||
|
stream.WriteByte((byte)(value >> 16));
|
||||||
|
stream.WriteByte((byte)(value >> 8));
|
||||||
|
stream.WriteByte((byte)value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static uint Crc32(byte[] bytes)
|
||||||
|
{
|
||||||
|
var crc = 0xFFFFFFFFu;
|
||||||
|
foreach (var b in bytes)
|
||||||
|
{
|
||||||
|
crc ^= b;
|
||||||
|
for (var i = 0; i < 8; i++)
|
||||||
|
{
|
||||||
|
crc = (crc & 1) == 1 ? (crc >> 1) ^ 0xEDB88320u : crc >> 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ~crc;
|
||||||
|
}
|
||||||
|
}
|
||||||
37
QYZH.InteractiveMagazine.Common/Helpers/RandomIdHelper.cs
Normal file
37
QYZH.InteractiveMagazine.Common/Helpers/RandomIdHelper.cs
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
using System.Security.Cryptography;
|
||||||
|
|
||||||
|
namespace QYZH.InteractiveMagazine.Common.Helpers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 随机ID帮助类
|
||||||
|
/// </summary>
|
||||||
|
public static class RandomIdHelper
|
||||||
|
{
|
||||||
|
private const long DefaultMinValue = 1_000_000_000_000L;
|
||||||
|
private const long DefaultMaxValue = 9_000_000_000_000_000L;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 生成不可预测的正数长整型ID
|
||||||
|
/// </summary>
|
||||||
|
public static long GenerateLongId(long minValue = DefaultMinValue, long maxValue = DefaultMaxValue)
|
||||||
|
{
|
||||||
|
if (minValue <= 0 || minValue >= maxValue)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(minValue), "随机ID范围配置错误");
|
||||||
|
}
|
||||||
|
|
||||||
|
var range = (ulong)(maxValue - minValue);
|
||||||
|
var limit = ulong.MaxValue - (ulong.MaxValue % range);
|
||||||
|
|
||||||
|
Span<byte> bytes = stackalloc byte[sizeof(ulong)];
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
RandomNumberGenerator.Fill(bytes);
|
||||||
|
var value = BitConverter.ToUInt64(bytes);
|
||||||
|
if (value < limit)
|
||||||
|
{
|
||||||
|
return minValue + (long)(value % range);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
80
QYZH.InteractiveMagazine.IService/IAdminPermissionService.cs
Normal file
80
QYZH.InteractiveMagazine.IService/IAdminPermissionService.cs
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
using QYZH.InteractiveMagazine.Models.Dto;
|
||||||
|
using QYZH.InteractiveMagazine.Models.Entity;
|
||||||
|
|
||||||
|
namespace QYZH.InteractiveMagazine.IService;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 后台权限管理服务接口
|
||||||
|
/// </summary>
|
||||||
|
public interface IAdminPermissionService : IBaseService<AdminRole>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 创建菜单
|
||||||
|
/// </summary>
|
||||||
|
Task<AdminMenuOutput> CreateMenuAsync(AdminMenuInput input);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新菜单
|
||||||
|
/// </summary>
|
||||||
|
Task<AdminMenuOutput> UpdateMenuAsync(long id, AdminMenuInput input);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除菜单
|
||||||
|
/// </summary>
|
||||||
|
Task DeleteMenuAsync(long id);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取菜单树
|
||||||
|
/// </summary>
|
||||||
|
Task<List<AdminMenuOutput>> GetMenuTreeAsync(AdminMenuQueryInput input);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建角色
|
||||||
|
/// </summary>
|
||||||
|
Task<AdminRoleOutput> CreateRoleAsync(AdminRoleInput input);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新角色
|
||||||
|
/// </summary>
|
||||||
|
Task<AdminRoleOutput> UpdateRoleAsync(long id, AdminRoleInput input);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除角色
|
||||||
|
/// </summary>
|
||||||
|
Task DeleteRoleAsync(long id);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取角色详情
|
||||||
|
/// </summary>
|
||||||
|
Task<AdminRoleOutput> GetRoleByIdAsync(long id);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取角色分页列表
|
||||||
|
/// </summary>
|
||||||
|
Task<PageListModel<AdminRoleOutput>> GetRoleListAsync(AdminRoleQueryInput input);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 分配角色菜单
|
||||||
|
/// </summary>
|
||||||
|
Task AssignRoleMenusAsync(long roleId, AssignRoleMenusInput input);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 分配管理员角色
|
||||||
|
/// </summary>
|
||||||
|
Task AssignAdminUserRolesAsync(long adminUserId, AssignAdminUserRolesInput input);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取管理员菜单树
|
||||||
|
/// </summary>
|
||||||
|
Task<List<AdminMenuOutput>> GetAdminUserMenuTreeAsync(long adminUserId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取管理员角色
|
||||||
|
/// </summary>
|
||||||
|
Task<List<AdminRoleSimpleOutput>> GetAdminUserRolesAsync(long adminUserId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取管理员权限编码
|
||||||
|
/// </summary>
|
||||||
|
Task<List<string>> GetAdminUserPermissionCodesAsync(long adminUserId);
|
||||||
|
}
|
||||||
@ -32,12 +32,12 @@ public interface IUserJournalService : IBaseService<UserJournal>
|
|||||||
Task UnbindJournalAsync(long userId, long id);
|
Task UnbindJournalAsync(long userId, long id);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 生成期刊二维码记录
|
/// 批量提交期刊二维码生成任务
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="input">生成输入</param>
|
/// <param name="input">生成输入</param>
|
||||||
/// <param name="operatorId">操作人Id</param>
|
/// <param name="operatorName">操作人名称</param>
|
||||||
/// <returns>二维码记录</returns>
|
/// <returns>提交结果</returns>
|
||||||
Task<UserJournalQrCodeOutput> CreateQrCodeAsync(CreateUserJournalQrCodeInput input, long operatorId);
|
Task<CreateUserJournalQrCodeOutput> CreateQrCodesAsync(CreateUserJournalQrCodeInput input, string operatorName);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 分页查询期刊二维码记录
|
/// 分页查询期刊二维码记录
|
||||||
@ -53,11 +53,18 @@ public interface IUserJournalService : IBaseService<UserJournal>
|
|||||||
/// <returns>二维码记录</returns>
|
/// <returns>二维码记录</returns>
|
||||||
Task<UserJournalQrCodeOutput> GetQrCodeDetailAsync(long id);
|
Task<UserJournalQrCodeOutput> GetQrCodeDetailAsync(long id);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据期刊Id获取未绑定用户的二维码列表
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="journalId">期刊Id</param>
|
||||||
|
/// <returns>未绑定二维码列表</returns>
|
||||||
|
Task<List<UserJournalQrCodeOutput>> GetUnboundQrCodesByJournalIdAsync(long journalId);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 删除未绑定的期刊二维码记录
|
/// 删除未绑定的期刊二维码记录
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="input">删除输入</param>
|
/// <param name="input">删除输入</param>
|
||||||
/// <param name="operatorId">操作人Id</param>
|
/// <param name="operatorName">操作人名称</param>
|
||||||
/// <returns>是否成功</returns>
|
/// <returns>是否成功</returns>
|
||||||
Task<bool> DeleteQrCodeAsync(DeleteUserJournalQrCodeInput input, long operatorId);
|
Task<bool> DeleteQrCodeAsync(DeleteUserJournalQrCodeInput input, string operatorName);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -119,7 +119,7 @@ public static class JwtHelper
|
|||||||
var tokenHandler = new JwtSecurityTokenHandler();
|
var tokenHandler = new JwtSecurityTokenHandler();
|
||||||
if (tokenHandler.ReadToken(token) is JwtSecurityToken jwtToken)
|
if (tokenHandler.ReadToken(token) is JwtSecurityToken jwtToken)
|
||||||
{
|
{
|
||||||
var userIdClaim = jwtToken.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier);
|
var userIdClaim = GetClaim(jwtToken, ClaimTypes.NameIdentifier, JwtRegisteredClaimNames.NameId, JwtRegisteredClaimNames.Sub);
|
||||||
if (long.TryParse(userIdClaim?.Value, out long userId))
|
if (long.TryParse(userIdClaim?.Value, out long userId))
|
||||||
{
|
{
|
||||||
return userId;
|
return userId;
|
||||||
@ -157,8 +157,13 @@ public static class JwtHelper
|
|||||||
var tokenHandler = new JwtSecurityTokenHandler();
|
var tokenHandler = new JwtSecurityTokenHandler();
|
||||||
if (tokenHandler.ReadToken(token) is JwtSecurityToken jwtToken)
|
if (tokenHandler.ReadToken(token) is JwtSecurityToken jwtToken)
|
||||||
{
|
{
|
||||||
return jwtToken.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Name)?.Value ?? string.Empty;
|
return GetClaim(jwtToken, ClaimTypes.Name, JwtRegisteredClaimNames.UniqueName, JwtRegisteredClaimNames.Name)?.Value ?? string.Empty;
|
||||||
}
|
}
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Claim? GetClaim(JwtSecurityToken jwtToken, params string[] claimTypes)
|
||||||
|
{
|
||||||
|
return jwtToken.Claims.FirstOrDefault(c => claimTypes.Contains(c.Type));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,6 +21,9 @@ namespace QYZH.InteractiveMagazine.Infrastructure.Extensions;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static class DependencyInjectionExtensions
|
public static class DependencyInjectionExtensions
|
||||||
{
|
{
|
||||||
|
private const string NoAuthScheme = "NoAuth";
|
||||||
|
private const string DevelopmentAuthScheme = "DevelopmentSmartAuth";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Registers infrastructure services.
|
/// Registers infrastructure services.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -34,19 +37,37 @@ public static class DependencyInjectionExtensions
|
|||||||
|
|
||||||
private static void AddJwtAuthentication(IServiceCollection services, IConfiguration configuration, IWebHostEnvironment? environment = null)
|
private static void AddJwtAuthentication(IServiceCollection services, IConfiguration configuration, IWebHostEnvironment? environment = null)
|
||||||
{
|
{
|
||||||
|
var jwtSettings = configuration.GetSection("JwtSettings").Get<JwtSettings>()!;
|
||||||
|
services.AddSingleton(jwtSettings);
|
||||||
|
|
||||||
if (environment?.IsDevelopment() == true)
|
if (environment?.IsDevelopment() == true)
|
||||||
{
|
{
|
||||||
services.AddAuthentication("NoAuth")
|
services.AddAuthentication(options =>
|
||||||
.AddScheme<AuthenticationSchemeOptions, NoAuthHandler>("NoAuth", options => { });
|
{
|
||||||
|
options.DefaultScheme = DevelopmentAuthScheme;
|
||||||
|
options.DefaultChallengeScheme = DevelopmentAuthScheme;
|
||||||
|
})
|
||||||
|
.AddPolicyScheme(DevelopmentAuthScheme, null, options =>
|
||||||
|
{
|
||||||
|
options.ForwardDefaultSelector = context =>
|
||||||
|
{
|
||||||
|
var authHeader = context.Request.Headers.Authorization.FirstOrDefault();
|
||||||
|
return !string.IsNullOrWhiteSpace(authHeader) &&
|
||||||
|
authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? JwtBearerDefaults.AuthenticationScheme
|
||||||
|
: NoAuthScheme;
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options => ConfigureJwtBearer(options, jwtSettings))
|
||||||
|
.AddScheme<AuthenticationSchemeOptions, NoAuthHandler>(NoAuthScheme, options => { });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var jwtSettings = configuration.GetSection("JwtSettings").Get<JwtSettings>()!;
|
|
||||||
|
|
||||||
services.AddSingleton(jwtSettings);
|
|
||||||
|
|
||||||
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||||
.AddJwtBearer(options =>
|
.AddJwtBearer(options => ConfigureJwtBearer(options, jwtSettings));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ConfigureJwtBearer(JwtBearerOptions options, JwtSettings jwtSettings)
|
||||||
{
|
{
|
||||||
options.TokenValidationParameters = new TokenValidationParameters
|
options.TokenValidationParameters = new TokenValidationParameters
|
||||||
{
|
{
|
||||||
@ -78,26 +99,28 @@ public static class DependencyInjectionExtensions
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var adminToken = await RedisHelper.GetAsync(JwtHelper.BuildAdminTokenKey(userId));
|
|
||||||
var wxUserId = context.Principal?.FindFirst(JwtHelper.WxUserIdClaimType)?.Value;
|
var wxUserId = context.Principal?.FindFirst(JwtHelper.WxUserIdClaimType)?.Value;
|
||||||
if (string.IsNullOrEmpty(wxUserId) && !string.IsNullOrEmpty(adminToken))
|
if (string.IsNullOrEmpty(wxUserId))
|
||||||
|
{
|
||||||
|
var adminToken = await RedisHelper.GetAsync(JwtHelper.BuildAdminTokenKey(userId));
|
||||||
|
if (adminToken == currentToken)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(wxUserId))
|
context.Fail("Token expired, please login again");
|
||||||
{
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var wechatToken = await RedisHelper.GetAsync(JwtHelper.BuildWeChatTokenKey(wxUserId, userId));
|
var wechatToken = await RedisHelper.GetAsync(JwtHelper.BuildWeChatTokenKey(wxUserId, userId));
|
||||||
if (wechatToken == currentToken)
|
if (wechatToken == currentToken)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
context.Fail("Token expired, please login again");
|
context.Fail("Token expired, please login again");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string? GetBearerToken(TokenValidatedContext context)
|
private static string? GetBearerToken(TokenValidatedContext context)
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
using RabbitMQ.Client;
|
using RabbitMQ.Client;
|
||||||
using RabbitMQ.Client.Events;
|
using RabbitMQ.Client.Events;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Encodings.Web;
|
using System.Text.Encodings.Web;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
@ -9,13 +10,15 @@ namespace QYZH.InteractiveMagazine.Infrastructure.RabbitMQ
|
|||||||
public class RabbitMQService : IRabbitMQService
|
public class RabbitMQService : IRabbitMQService
|
||||||
{
|
{
|
||||||
private readonly IRabbitMQConnection _connection;
|
private readonly IRabbitMQConnection _connection;
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
private readonly JsonSerializerOptions options = new JsonSerializerOptions
|
private readonly JsonSerializerOptions options = new JsonSerializerOptions
|
||||||
{
|
{
|
||||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||||||
};
|
};
|
||||||
public RabbitMQService(IRabbitMQConnection connection)
|
public RabbitMQService(IRabbitMQConnection connection, IConfiguration configuration)
|
||||||
{
|
{
|
||||||
_connection = connection ?? throw new ArgumentNullException(nameof(connection));
|
_connection = connection ?? throw new ArgumentNullException(nameof(connection));
|
||||||
|
_configuration = configuration;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -130,7 +133,13 @@ namespace QYZH.InteractiveMagazine.Infrastructure.RabbitMQ
|
|||||||
public async Task ReceiveAsync(string exchange, string queueName, string routingKey, Func<IChannel, BasicDeliverEventArgs, Task> callback, CancellationToken cancellationToken = default)
|
public async Task ReceiveAsync(string exchange, string queueName, string routingKey, Func<IChannel, BasicDeliverEventArgs, Task> callback, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var channel = await _connection.CreateChannel();
|
var channel = await _connection.CreateChannel();
|
||||||
//await channel.BasicQosAsync(0, 10, false); // 一次最多接收10条未确认的消息
|
var prefetchCount = _configuration.GetValue<ushort>("RabbitMq:PrefetchCount");
|
||||||
|
if (prefetchCount == 0)
|
||||||
|
{
|
||||||
|
prefetchCount = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
await channel.BasicQosAsync(0, prefetchCount, false, cancellationToken);
|
||||||
|
|
||||||
// 声明 Exchange(持久化)
|
// 声明 Exchange(持久化)
|
||||||
await channel.ExchangeDeclareAsync(exchange: exchange, type: "direct", durable: true, autoDelete: false, arguments: null);
|
await channel.ExchangeDeclareAsync(exchange: exchange, type: "direct", durable: true, autoDelete: false, arguments: null);
|
||||||
|
|||||||
270
QYZH.InteractiveMagazine.Models/Dto/Admin/AdminPermissionDto.cs
Normal file
270
QYZH.InteractiveMagazine.Models/Dto/Admin/AdminPermissionDto.cs
Normal 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; } = [];
|
||||||
|
}
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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; } = [];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
using QYZH.InteractiveMagazine.Models.Enum;
|
using QYZH.InteractiveMagazine.Models.Enum;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace QYZH.InteractiveMagazine.Models.Dto;
|
namespace QYZH.InteractiveMagazine.Models.Dto;
|
||||||
|
|
||||||
@ -10,17 +11,15 @@ public class BindJournalInput
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 期刊模板Id(扫码解析的期刊定义Id)
|
/// 期刊模板Id(扫码解析的期刊定义Id)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)]
|
||||||
public long JournalId { get; set; }
|
public long JournalId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 实例化期刊Id(扫码解析的具体期刊实例Id,可选)
|
/// 实例化期刊Id(扫码解析的具体期刊实例Id,可选)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)]
|
||||||
public long Id { get; set; }
|
public long Id { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 关联类型: Read(已读), Favorite(收藏), Subscribe(订阅),默认 Subscribe
|
|
||||||
/// </summary>
|
|
||||||
public string Type { get; set; } = UserJournalTypeEnum.Subscribe.ToString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -122,14 +121,67 @@ public class CreateUserJournalQrCodeInput
|
|||||||
public long JournalId { get; set; }
|
public long JournalId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 关联类型: Read, Favorite, Subscribe,默认 Subscribe
|
/// 生成数量
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Type { get; set; } = UserJournalTypeEnum.Subscribe.ToString();
|
public int Count { get; set; } = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 生成期刊二维码提交结果
|
||||||
|
/// </summary>
|
||||||
|
public class CreateUserJournalQrCodeOutput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 期刊Id
|
||||||
|
/// </summary>
|
||||||
|
public long JournalId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 请求生成数量
|
||||||
|
/// </summary>
|
||||||
|
public int RequestedCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 已提交生成数量
|
||||||
|
/// </summary>
|
||||||
|
public int AcceptedCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 二维码记录Id列表
|
||||||
|
/// </summary>
|
||||||
|
[JsonNumberHandling(JsonNumberHandling.WriteAsString)]
|
||||||
|
public List<long> RecordIds { get; set; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否后台生成
|
||||||
|
/// </summary>
|
||||||
|
public bool IsAsync { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 提示信息
|
||||||
|
/// </summary>
|
||||||
|
public string Message { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 期刊二维码查询输入DTO
|
/// 期刊二维码查询输入DTO
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
public class GenerateUserJournalQrCodeMessage
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 二维码记录Id列表
|
||||||
|
/// </summary>
|
||||||
|
public List<long> RecordIds { get; set; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 操作人名称
|
||||||
|
/// </summary>
|
||||||
|
public string OperatorName { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 鏈熷垔浜岀淮鐮佹煡璇㈣緭鍏TO
|
||||||
|
/// </summary>
|
||||||
public class UserJournalQrCodeQueryInput : PageQueryModel
|
public class UserJournalQrCodeQueryInput : PageQueryModel
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -161,6 +213,7 @@ public class UserJournalQrCodeOutput
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 二维码记录Id
|
/// 二维码记录Id
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[JsonNumberHandling(JsonNumberHandling.WriteAsString)]
|
||||||
public long Id { get; set; }
|
public long Id { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -201,7 +254,7 @@ public class UserJournalQrCodeOutput
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 二维码内容
|
/// 二维码内容
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string QrCodeContent { get; set; } = string.Empty;
|
public string QrCodeUrl { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 创建时间
|
/// 创建时间
|
||||||
@ -222,5 +275,6 @@ public class DeleteUserJournalQrCodeInput
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 二维码记录Id列表
|
/// 二维码记录Id列表
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)]
|
||||||
public List<long> Ids { get; set; } = [];
|
public List<long> Ids { get; set; } = [];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,6 +26,11 @@ public class UsersOutput
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string WxUserId { get; set; } = string.Empty;
|
public string WxUserId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 微信用户名
|
||||||
|
/// </summary>
|
||||||
|
public string? WxUserName { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 昵称
|
/// 昵称
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
66
QYZH.InteractiveMagazine.Models/Entity/AdminMenu.cs
Normal file
66
QYZH.InteractiveMagazine.Models/Entity/AdminMenu.cs
Normal 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;
|
||||||
|
}
|
||||||
31
QYZH.InteractiveMagazine.Models/Entity/AdminRole.cs
Normal file
31
QYZH.InteractiveMagazine.Models/Entity/AdminRole.cs
Normal 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; }
|
||||||
|
}
|
||||||
24
QYZH.InteractiveMagazine.Models/Entity/AdminRoleMenu.cs
Normal file
24
QYZH.InteractiveMagazine.Models/Entity/AdminRoleMenu.cs
Normal 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; }
|
||||||
|
}
|
||||||
24
QYZH.InteractiveMagazine.Models/Entity/AdminUserRole.cs
Normal file
24
QYZH.InteractiveMagazine.Models/Entity/AdminUserRole.cs
Normal 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; }
|
||||||
|
}
|
||||||
@ -31,12 +31,18 @@ namespace QYZH.InteractiveMagazine.Models.Entity
|
|||||||
public long JournalId { get; set; }
|
public long JournalId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Desc:关联类型: Read(已读), Favorite(收藏), Subscribe(订阅)
|
/// Desc:关联类型: 默认0
|
||||||
/// Default:Read
|
|
||||||
/// Nullable:False
|
/// Nullable:False
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[SugarColumn(ColumnName = "Type")]
|
[SugarColumn(ColumnName = "Type")]
|
||||||
public UserJournalTypeEnum Type { get; set; }
|
public int Type { get; set; } = 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Desc:二维码地址
|
||||||
|
/// Default:
|
||||||
|
/// Nullable:True
|
||||||
|
/// </summary>
|
||||||
|
[SugarColumn(ColumnName = "QrCodeUrl")]
|
||||||
|
public string? QrCodeUrl { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,5 +17,17 @@ public enum UserJournalStatusEnum
|
|||||||
/// 正常
|
/// 正常
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Description("正常")]
|
[Description("正常")]
|
||||||
Active = 1
|
Active = 1,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 生成中
|
||||||
|
/// </summary>
|
||||||
|
[Description("生成中")]
|
||||||
|
Generating = 2,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 生成失败
|
||||||
|
/// </summary>
|
||||||
|
[Description("生成失败")]
|
||||||
|
Failed = 3
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,25 +0,0 @@
|
|||||||
using System.ComponentModel;
|
|
||||||
|
|
||||||
namespace QYZH.InteractiveMagazine.Models.Enum;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 用户期刊关联类型枚举
|
|
||||||
/// </summary>
|
|
||||||
public enum UserJournalTypeEnum
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 已读
|
|
||||||
/// </summary>
|
|
||||||
[Description("已读")]
|
|
||||||
Read = 1,
|
|
||||||
/// <summary>
|
|
||||||
/// 收藏
|
|
||||||
/// </summary>
|
|
||||||
[Description("收藏")]
|
|
||||||
Favorite = 2,
|
|
||||||
/// <summary>
|
|
||||||
/// 订阅
|
|
||||||
/// </summary>
|
|
||||||
[Description("订阅")]
|
|
||||||
Subscribe = 3
|
|
||||||
}
|
|
||||||
@ -1,6 +1,8 @@
|
|||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using QYZH.InteractiveMagazine.Infrastructure.Context;
|
using QYZH.InteractiveMagazine.Infrastructure.Context;
|
||||||
using QYZH.InteractiveMagazine.Models.Entity;
|
using QYZH.InteractiveMagazine.Models.Entity;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using QYZH.InteractiveMagazine.Infrastructure.Context;
|
||||||
using SqlSugar;
|
using SqlSugar;
|
||||||
using System.Linq.Expressions;
|
using System.Linq.Expressions;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
@ -120,20 +122,14 @@ namespace QYZH.InteractiveMagazine.Repository.Core
|
|||||||
db.Aop.DataExecuting = (oldValue, entityInfo) =>
|
db.Aop.DataExecuting = (oldValue, entityInfo) =>
|
||||||
{
|
{
|
||||||
var entityValue = entityInfo.EntityColumnInfo.PropertyInfo.GetValue(entityInfo.EntityValue)?.ToString();
|
var entityValue = entityInfo.EntityColumnInfo.PropertyInfo.GetValue(entityInfo.EntityValue)?.ToString();
|
||||||
|
var currentUserName = GetCurrentUserName();
|
||||||
//var currnetUserName = "";
|
|
||||||
//var currentNameIdentifier = ServiceContext.GetRequiredService<IHttpContextAccessor>().HttpContext?.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? "";
|
|
||||||
//var currentUserId = string.IsNullOrWhiteSpace(currentNameIdentifier) ? 0 : long.Parse(currentNameIdentifier);
|
|
||||||
|
|
||||||
var currentClaimName = ServiceContext.GetRequiredService<IHttpContextAccessor>().HttpContext?.User?.FindFirst(ClaimTypes.Name)?.Value;
|
|
||||||
|
|
||||||
/*** inset生效 ***/
|
/*** inset生效 ***/
|
||||||
if (entityInfo.OperationType == DataFilterType.InsertByObject)
|
if (entityInfo.OperationType == DataFilterType.InsertByObject)
|
||||||
{
|
{
|
||||||
if (entityInfo.PropertyName == "CreatedAt" && (string.IsNullOrWhiteSpace(entityValue) || entityValue == DateTime.MinValue.ToString()))
|
if (entityInfo.PropertyName == "CreatedAt" && (string.IsNullOrWhiteSpace(entityValue) || entityValue == DateTime.MinValue.ToString()))
|
||||||
entityInfo.SetValue(DateTime.Now);//修改CreateTime字段
|
entityInfo.SetValue(DateTime.Now);//修改CreateTime字段
|
||||||
else if (entityInfo.PropertyName == "CreatedBy" && (string.IsNullOrWhiteSpace(entityValue)))
|
else if (entityInfo.PropertyName == "CreatedBy" && (string.IsNullOrWhiteSpace(entityValue)))
|
||||||
entityInfo.SetValue(currentClaimName);//修改创建人字段
|
entityInfo.SetValue(currnetUserName);//修改创建人字段
|
||||||
else if (entityInfo.PropertyName == "IsDeleted" && string.IsNullOrWhiteSpace(entityValue))
|
else if (entityInfo.PropertyName == "IsDeleted" && string.IsNullOrWhiteSpace(entityValue))
|
||||||
entityInfo.SetValue("0");//修改CreateTime字段
|
entityInfo.SetValue("0");//修改CreateTime字段
|
||||||
}
|
}
|
||||||
@ -144,7 +140,7 @@ namespace QYZH.InteractiveMagazine.Repository.Core
|
|||||||
if (entityInfo.PropertyName == "UpdatedAt" && (string.IsNullOrWhiteSpace(entityValue) || entityValue == DateTime.MinValue.ToString()))
|
if (entityInfo.PropertyName == "UpdatedAt" && (string.IsNullOrWhiteSpace(entityValue) || entityValue == DateTime.MinValue.ToString()))
|
||||||
entityInfo.SetValue(DateTime.Now);//修改UpdatedTime字段
|
entityInfo.SetValue(DateTime.Now);//修改UpdatedTime字段
|
||||||
else if (entityInfo.PropertyName == "UpdatedBy" && (string.IsNullOrWhiteSpace(entityValue) || entityValue == "0"))
|
else if (entityInfo.PropertyName == "UpdatedBy" && (string.IsNullOrWhiteSpace(entityValue) || entityValue == "0"))
|
||||||
entityInfo.SetValue(currentClaimName);//修改更新人字段
|
entityInfo.SetValue(currnetUserName);//修改更新人字段
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return db;
|
return db;
|
||||||
@ -152,6 +148,39 @@ namespace QYZH.InteractiveMagazine.Repository.Core
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static string GetCurrentUserName()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var httpContextAccessor = ServiceContext.ServiceProvider?.GetService(typeof(IHttpContextAccessor)) as IHttpContextAccessor;
|
||||||
|
var user = httpContextAccessor?.HttpContext?.User;
|
||||||
|
var userName = user?.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Name)?.Value
|
||||||
|
?? user?.Identity?.Name;
|
||||||
|
|
||||||
|
return string.IsNullOrWhiteSpace(userName) ? "System" : userName;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return "System";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ShouldSetOperatorName(string? entityValue, string currentUserName)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(entityValue) || entityValue == "0")
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentUserName != "System" && long.TryParse(entityValue, out _))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 把一个字符串转成驼峰规则的字符串
|
/// 把一个字符串转成驼峰规则的字符串
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
495
QYZH.InteractiveMagazine.Service/AdminPermissionService.cs
Normal file
495
QYZH.InteractiveMagazine.Service/AdminPermissionService.cs
Normal 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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using QYZH.InteractiveMagazine.Common.Helpers;
|
||||||
|
using QYZH.InteractiveMagazine.Infrastructure.OSS;
|
||||||
using QYZH.InteractiveMagazine.Infrastructure.RabbitMQ;
|
using QYZH.InteractiveMagazine.Infrastructure.RabbitMQ;
|
||||||
using QYZH.InteractiveMagazine.IService;
|
using QYZH.InteractiveMagazine.IService;
|
||||||
using QYZH.InteractiveMagazine.Models.Common;
|
using QYZH.InteractiveMagazine.Models.Common;
|
||||||
@ -20,20 +22,23 @@ public class UserJournalService(
|
|||||||
BaseRepository<Journal> journalRepository,
|
BaseRepository<Journal> journalRepository,
|
||||||
ILogger<UserJournalService> logger,
|
ILogger<UserJournalService> logger,
|
||||||
IRabbitMQService rabbitMqService,
|
IRabbitMQService rabbitMqService,
|
||||||
|
OssService ossService,
|
||||||
IPetService petService)
|
IPetService petService)
|
||||||
: BaseRepository<UserJournal>, IUserJournalService
|
: BaseRepository<UserJournal>, IUserJournalService
|
||||||
{
|
{
|
||||||
private const string JournalExchange = "ex.journal";
|
private const string JournalExchange = "ex.journal";
|
||||||
private const string BindJournalQueue = "mq.journal.bindUser";
|
private const string BindJournalQueue = "mq.journal.bindUser";
|
||||||
private const string BindJournalRoutingKey = "rk.journal.bindUser";
|
private const string BindJournalRoutingKey = "rk.journal.bindUser";
|
||||||
|
private const string QrCodeGenerateQueue = "mq.journal.qrcode.generate";
|
||||||
|
private const string QrCodeGenerateRoutingKey = "rk.journal.qrcode.generate";
|
||||||
|
private const int MaxBatchQrCodeCount = 500;
|
||||||
|
private const int MaxRandomIdGenerateRetryCount = 5;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 用户绑定期刊(扫码绑定)
|
/// 用户绑定期刊(扫码绑定)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<BindJournalOutput> BindJournalAsync(long userId, BindJournalInput input)
|
public async Task<BindJournalOutput> BindJournalAsync(long userId, BindJournalInput input)
|
||||||
{
|
{
|
||||||
logger.LogInformation("用户绑定期刊,UserId: {UserId}, JournalId: {JournalId}, Id: {Id}, Type: {Type}",
|
|
||||||
userId, input.JournalId, input.Id, input.Type);
|
|
||||||
|
|
||||||
// 校验参数
|
// 校验参数
|
||||||
if (input.JournalId <= 0|| input.Id <= 0)
|
if (input.JournalId <= 0|| input.Id <= 0)
|
||||||
@ -41,7 +46,12 @@ public class UserJournalService(
|
|||||||
throw new BusinessException("参数错误,未获取到期刊", ResultCode.BAD_REQUEST);
|
throw new BusinessException("参数错误,未获取到期刊", ResultCode.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
// 校验用户是否存在
|
// 校验用户是否存在
|
||||||
var user = await usersRepository.GetByIdAsync(userId);
|
var activeStatus = (int)UserJournalStatusEnum.Active;
|
||||||
|
var boundAt = DateTime.Now;
|
||||||
|
|
||||||
|
var user = await usersRepository.Queryable()
|
||||||
|
.Where(u => u.Id == userId && !u.IsDeleted)
|
||||||
|
.FirstAsync();
|
||||||
if (user == null || user.IsDeleted)
|
if (user == null || user.IsDeleted)
|
||||||
{
|
{
|
||||||
logger.LogWarning("绑定期刊失败,用户不存在,UserId: {UserId}", userId);
|
logger.LogWarning("绑定期刊失败,用户不存在,UserId: {UserId}", userId);
|
||||||
@ -63,24 +73,17 @@ public class UserJournalService(
|
|||||||
throw new BusinessException("该期刊暂未发布,无法绑定", ResultCode.UNPROCESSABLE_ENTITY);
|
throw new BusinessException("该期刊暂未发布,无法绑定", ResultCode.UNPROCESSABLE_ENTITY);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 防重复绑定:同一用户 + 期刊 + 实例 + 类型
|
|
||||||
if (!Enum.TryParse<UserJournalTypeEnum>(input.Type, true, out var bindType))
|
|
||||||
{
|
|
||||||
throw new BusinessException("关联类型不正确", ResultCode.BAD_REQUEST);
|
|
||||||
}
|
|
||||||
|
|
||||||
var userJournal = await userJournalRepository.Queryable()
|
var userJournal = await userJournalRepository.Queryable()
|
||||||
.Where(uj => uj.Id == input.Id && uj.JournalId == input.JournalId && !uj.IsDeleted)
|
.Where(uj => uj.Id == input.Id && uj.JournalId == input.JournalId && !uj.IsDeleted)
|
||||||
.FirstAsync();
|
.FirstAsync();
|
||||||
|
|
||||||
// 检查是否为首次绑定期刊(用于激活宠物)
|
|
||||||
if (userJournal == null)
|
if (userJournal == null)
|
||||||
{
|
{
|
||||||
logger.LogWarning("绑定期刊失败,二维码记录不存在,UserId: {UserId}, JournalId: {JournalId}, Id: {Id}", userId, input.JournalId, input.Id);
|
logger.LogWarning("绑定期刊失败,二维码记录不存在,UserId: {UserId}, JournalId: {JournalId}, Id: {Id}", userId, input.JournalId, input.Id);
|
||||||
throw new BusinessException("二维码不存在或已失效", ResultCode.NOT_FOUND);
|
throw new BusinessException("二维码不存在或已失效", ResultCode.NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userJournal.Status != (int)UserJournalStatusEnum.Active)
|
if (userJournal.Status != activeStatus)
|
||||||
{
|
{
|
||||||
throw new BusinessException("二维码已失效", ResultCode.UNPROCESSABLE_ENTITY);
|
throw new BusinessException("二维码已失效", ResultCode.UNPROCESSABLE_ENTITY);
|
||||||
}
|
}
|
||||||
@ -91,16 +94,33 @@ public class UserJournalService(
|
|||||||
throw new BusinessException("该期刊已被绑定", ResultCode.BAD_REQUEST);
|
throw new BusinessException("该期刊已被绑定", ResultCode.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
var isFirstBind = !userJournalRepository.Context.Queryable<UserJournal>()
|
var isFirstBind = false;
|
||||||
.Any(uj => uj.UserId == userId);
|
await userJournalRepository.UseTranAsync(async () =>
|
||||||
|
{
|
||||||
|
var boundJournalIds = await userJournalRepository.Queryable()
|
||||||
|
.Where(uj => uj.UserId == userId && !uj.IsDeleted && uj.Status == activeStatus)
|
||||||
|
.Select(uj => uj.JournalId)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
if (boundJournalIds.Contains(input.JournalId))
|
||||||
|
{
|
||||||
|
logger.LogWarning("用户重复绑定同一期刊,UserId: {UserId}, JournalId: {JournalId}, Id: {Id}", userId, input.JournalId, input.Id);
|
||||||
|
throw new BusinessException("该用户已绑定过该期刊", ResultCode.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
isFirstBind = boundJournalIds.Count == 0;
|
||||||
|
boundAt = DateTime.Now;
|
||||||
|
|
||||||
// 创建绑定记录
|
// 创建绑定记录
|
||||||
var updateCount = await userJournalRepository.Updateable()
|
var updateCount = await userJournalRepository.Updateable()
|
||||||
.SetColumns(uj => uj.UserId == userId)
|
.SetColumns(uj => uj.UserId == userId)
|
||||||
.SetColumns(uj => uj.Type == bindType)
|
|
||||||
.SetColumns(uj => uj.UpdatedBy == userId.ToString())
|
.SetColumns(uj => uj.UpdatedBy == userId.ToString())
|
||||||
.SetColumns(uj => uj.UpdatedAt == DateTime.Now)
|
.SetColumns(uj => uj.UpdatedAt == boundAt)
|
||||||
.Where(uj => uj.Id == input.Id && !uj.IsDeleted && (uj.UserId == null || uj.UserId == 0))
|
.Where(uj => uj.Id == input.Id
|
||||||
|
&& uj.JournalId == input.JournalId
|
||||||
|
&& !uj.IsDeleted
|
||||||
|
&& uj.Status == activeStatus
|
||||||
|
&& (uj.UserId == null || uj.UserId == 0))
|
||||||
.ExecuteCommandAsync();
|
.ExecuteCommandAsync();
|
||||||
|
|
||||||
if (updateCount <= 0)
|
if (updateCount <= 0)
|
||||||
@ -109,11 +129,12 @@ public class UserJournalService(
|
|||||||
throw new BusinessException("绑定期刊失败,请稍后重试", ResultCode.GLOBAL_ERROR);
|
throw new BusinessException("绑定期刊失败,请稍后重试", ResultCode.GLOBAL_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
logger.LogInformation("用户绑定期刊成功,UserId: {UserId}, JournalId: {JournalId}, Id: {Id}", userId, input.JournalId, userJournal.Id);
|
logger.LogInformation("用户绑定期刊成功,UserId: {UserId}, JournalId: {JournalId}, Id: {Id}", userId, input.JournalId, userJournal.Id);
|
||||||
userJournal.UserId = userId;
|
userJournal.UserId = userId;
|
||||||
userJournal.Type = bindType;
|
|
||||||
userJournal.UpdatedBy = userId.ToString();
|
userJournal.UpdatedBy = userId.ToString();
|
||||||
userJournal.UpdatedAt = DateTime.Now;
|
userJournal.UpdatedAt = boundAt;
|
||||||
|
|
||||||
await SendBindJournalMessageAsync(user, journal);
|
await SendBindJournalMessageAsync(user, journal);
|
||||||
|
|
||||||
@ -145,16 +166,16 @@ public class UserJournalService(
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 生成期刊二维码记录
|
/// 生成期刊二维码记录
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<UserJournalQrCodeOutput> CreateQrCodeAsync(CreateUserJournalQrCodeInput input, long operatorId)
|
public async Task<CreateUserJournalQrCodeOutput> CreateQrCodesAsync(CreateUserJournalQrCodeInput input, string operatorName)
|
||||||
{
|
{
|
||||||
if (input.JournalId <= 0)
|
if (input.JournalId <= 0)
|
||||||
{
|
{
|
||||||
throw new BusinessException("期刊Id不能为空", ResultCode.BAD_REQUEST);
|
throw new BusinessException("期刊Id不能为空", ResultCode.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Enum.TryParse<UserJournalTypeEnum>(input.Type, true, out var type))
|
if (input.Count <= 0 || input.Count > MaxBatchQrCodeCount)
|
||||||
{
|
{
|
||||||
throw new BusinessException("关联类型不正确", ResultCode.BAD_REQUEST);
|
throw new BusinessException($"生成数量必须在1-{MaxBatchQrCodeCount}之间", ResultCode.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
var journal = await journalRepository.GetByIdAsync(input.JournalId);
|
var journal = await journalRepository.GetByIdAsync(input.JournalId);
|
||||||
@ -168,31 +189,67 @@ public class UserJournalService(
|
|||||||
throw new BusinessException("该期刊暂未发布,无法生成二维码", ResultCode.UNPROCESSABLE_ENTITY);
|
throw new BusinessException("该期刊暂未发布,无法生成二维码", ResultCode.UNPROCESSABLE_ENTITY);
|
||||||
}
|
}
|
||||||
|
|
||||||
var record = new UserJournal
|
var now = DateTime.Now;
|
||||||
|
var randomIds = await GenerateUniqueQrCodeIdsAsync(input.Count);
|
||||||
|
var records = randomIds
|
||||||
|
.Select(id => new UserJournal
|
||||||
{
|
{
|
||||||
|
Id = id,
|
||||||
UserId = null,
|
UserId = null,
|
||||||
JournalId = input.JournalId,
|
JournalId = input.JournalId,
|
||||||
Type = type,
|
Type = 0,
|
||||||
Status = (int)UserJournalStatusEnum.Active,
|
Status = (int)UserJournalStatusEnum.Generating,
|
||||||
IsDeleted = false,
|
IsDeleted = false,
|
||||||
CreatedBy = operatorId.ToString(),
|
CreatedBy = operatorName,
|
||||||
CreatedAt = DateTime.Now,
|
CreatedAt = now,
|
||||||
UpdatedBy = operatorId.ToString(),
|
UpdatedBy = operatorName,
|
||||||
UpdatedAt = DateTime.Now
|
UpdatedAt = now
|
||||||
};
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
var result = await userJournalRepository.InsertAsync(record);
|
var insertCount = await userJournalRepository.Context.Insertable(records).ExecuteCommandAsync();
|
||||||
if (!result)
|
if (insertCount <= 0)
|
||||||
{
|
{
|
||||||
throw new BusinessException("生成二维码失败,请稍后重试", ResultCode.GLOBAL_ERROR);
|
throw new BusinessException("提交二维码生成任务失败,请稍后重试", ResultCode.GLOBAL_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
return MapQrCodeOutput(record, journal, null);
|
var recordIds = records.Select(r => r.Id).ToList();
|
||||||
|
var messageSent = await rabbitMqService.SendAsync(new RabbitMQSendParam
|
||||||
|
{
|
||||||
|
Exchange = JournalExchange,
|
||||||
|
Queue = QrCodeGenerateQueue,
|
||||||
|
RoutingKey = QrCodeGenerateRoutingKey,
|
||||||
|
Data = new GenerateUserJournalQrCodeMessage
|
||||||
|
{
|
||||||
|
RecordIds = recordIds,
|
||||||
|
OperatorName = operatorName
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!messageSent)
|
||||||
|
{
|
||||||
|
await userJournalRepository.Updateable()
|
||||||
|
.SetColumns(uj => uj.Status == (int)UserJournalStatusEnum.Failed)
|
||||||
|
.SetColumns(uj => uj.UpdatedBy == operatorName)
|
||||||
|
.SetColumns(uj => uj.UpdatedAt == DateTime.Now)
|
||||||
|
.Where(uj => recordIds.Contains(uj.Id) && !uj.IsDeleted && uj.Status == (int)UserJournalStatusEnum.Generating)
|
||||||
|
.ExecuteCommandAsync();
|
||||||
|
|
||||||
|
logger.LogError("发送期刊二维码生成消息失败,RecordIds: {RecordIds}", string.Join(",", recordIds));
|
||||||
|
throw new BusinessException("二维码生成任务提交失败,请稍后重试", ResultCode.GLOBAL_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CreateUserJournalQrCodeOutput
|
||||||
|
{
|
||||||
|
JournalId = input.JournalId,
|
||||||
|
RequestedCount = input.Count,
|
||||||
|
AcceptedCount = insertCount,
|
||||||
|
RecordIds = recordIds,
|
||||||
|
IsAsync = true,
|
||||||
|
Message = "二维码生成任务已提交,请稍后查询未绑定二维码列表"
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 分页查询期刊二维码记录
|
|
||||||
/// </summary>
|
|
||||||
public async Task<PageListModel<UserJournalQrCodeOutput>> GetQrCodePageListAsync(UserJournalQrCodeQueryInput input)
|
public async Task<PageListModel<UserJournalQrCodeOutput>> GetQrCodePageListAsync(UserJournalQrCodeQueryInput input)
|
||||||
{
|
{
|
||||||
if (input.PageIndex <= 0)
|
if (input.PageIndex <= 0)
|
||||||
@ -239,7 +296,7 @@ public class UserJournalService(
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 删除未绑定的期刊二维码记录
|
/// 删除未绑定的期刊二维码记录
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<bool> DeleteQrCodeAsync(DeleteUserJournalQrCodeInput input, long operatorId)
|
public async Task<bool> DeleteQrCodeAsync(DeleteUserJournalQrCodeInput input, string operatorName)
|
||||||
{
|
{
|
||||||
if (input.Ids == null || input.Ids.Count == 0)
|
if (input.Ids == null || input.Ids.Count == 0)
|
||||||
{
|
{
|
||||||
@ -264,7 +321,7 @@ public class UserJournalService(
|
|||||||
var updateCount = await userJournalRepository.Updateable()
|
var updateCount = await userJournalRepository.Updateable()
|
||||||
.SetColumns(uj => uj.IsDeleted == true)
|
.SetColumns(uj => uj.IsDeleted == true)
|
||||||
.SetColumns(uj => uj.Status == (int)UserJournalStatusEnum.Inactive)
|
.SetColumns(uj => uj.Status == (int)UserJournalStatusEnum.Inactive)
|
||||||
.SetColumns(uj => uj.UpdatedBy == operatorId.ToString())
|
.SetColumns(uj => uj.UpdatedBy == operatorName)
|
||||||
.SetColumns(uj => uj.UpdatedAt == DateTime.Now)
|
.SetColumns(uj => uj.UpdatedAt == DateTime.Now)
|
||||||
.Where(uj => ids.Contains(uj.Id) && !uj.IsDeleted && (uj.UserId == null || uj.UserId == 0))
|
.Where(uj => ids.Contains(uj.Id) && !uj.IsDeleted && (uj.UserId == null || uj.UserId == 0))
|
||||||
.ExecuteCommandAsync();
|
.ExecuteCommandAsync();
|
||||||
@ -272,6 +329,24 @@ public class UserJournalService(
|
|||||||
return updateCount == ids.Count;
|
return updateCount == ids.Count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<List<UserJournalQrCodeOutput>> GetUnboundQrCodesByJournalIdAsync(long journalId)
|
||||||
|
{
|
||||||
|
if (journalId <= 0)
|
||||||
|
{
|
||||||
|
throw new BusinessException("期刊Id不能为空", ResultCode.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
var records = await userJournalRepository.Queryable()
|
||||||
|
.Where(uj => uj.JournalId == journalId && !uj.IsDeleted)
|
||||||
|
.Where(uj => uj.UserId == null || uj.UserId == 0)
|
||||||
|
.Where(uj => uj.Status == (int)UserJournalStatusEnum.Active)
|
||||||
|
.Where(uj => !string.IsNullOrEmpty(uj.QrCodeUrl))
|
||||||
|
.OrderByDescending(uj => uj.CreatedAt)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
return await BuildQrCodeOutputsAsync(records);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<List<UserJournalQrCodeOutput>> BuildQrCodeOutputsAsync(List<UserJournal> records)
|
private async Task<List<UserJournalQrCodeOutput>> BuildQrCodeOutputsAsync(List<UserJournal> records)
|
||||||
{
|
{
|
||||||
if (records.Count == 0)
|
if (records.Count == 0)
|
||||||
@ -322,7 +397,7 @@ public class UserJournalService(
|
|||||||
Type = record.Type.ToString(),
|
Type = record.Type.ToString(),
|
||||||
Status = record.Status.ToString(),
|
Status = record.Status.ToString(),
|
||||||
IsBound = record.UserId.HasValue && record.UserId.Value > 0,
|
IsBound = record.UserId.HasValue && record.UserId.Value > 0,
|
||||||
QrCodeContent = BuildQrCodeContent(record.JournalId, record.Id),
|
QrCodeUrl = DomainHelper.OssFullUrl(record.QrCodeUrl ?? string.Empty),
|
||||||
CreatedAt = record.CreatedAt,
|
CreatedAt = record.CreatedAt,
|
||||||
BoundAt = record.UserId.HasValue && record.UserId.Value > 0 ? record.UpdatedAt : null
|
BoundAt = record.UserId.HasValue && record.UserId.Value > 0 ? record.UpdatedAt : null
|
||||||
};
|
};
|
||||||
@ -330,7 +405,52 @@ public class UserJournalService(
|
|||||||
|
|
||||||
private static string BuildQrCodeContent(long journalId, long id)
|
private static string BuildQrCodeContent(long journalId, long id)
|
||||||
{
|
{
|
||||||
return JsonSerializer.Serialize(new { JournalId = journalId, Id = id });
|
return JsonSerializer.Serialize(new { JournalId = journalId.ToString(), Id = id.ToString() });
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<long> GenerateUniqueQrCodeIdAsync()
|
||||||
|
{
|
||||||
|
for (var i = 0; i < MaxRandomIdGenerateRetryCount; i++)
|
||||||
|
{
|
||||||
|
var id = RandomIdHelper.GenerateLongId();
|
||||||
|
var exists = await userJournalRepository.Queryable()
|
||||||
|
.AnyAsync(uj => uj.Id == id);
|
||||||
|
|
||||||
|
if (!exists)
|
||||||
|
{
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new BusinessException("生成二维码ID失败,请稍后重试", ResultCode.GLOBAL_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<List<long>> GenerateUniqueQrCodeIdsAsync(int count)
|
||||||
|
{
|
||||||
|
var ids = new HashSet<long>();
|
||||||
|
|
||||||
|
for (var i = 0; i < MaxRandomIdGenerateRetryCount && ids.Count < count; i++)
|
||||||
|
{
|
||||||
|
while (ids.Count < count)
|
||||||
|
{
|
||||||
|
ids.Add(RandomIdHelper.GenerateLongId());
|
||||||
|
}
|
||||||
|
|
||||||
|
var candidateIds = ids.ToList();
|
||||||
|
var existingIds = await userJournalRepository.Queryable()
|
||||||
|
.Where(uj => candidateIds.Contains(uj.Id))
|
||||||
|
.Select(uj => uj.Id)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
if (existingIds.Count == 0)
|
||||||
|
{
|
||||||
|
return candidateIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
ids.ExceptWith(existingIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new BusinessException("生成二维码ID失败,请稍后重试", ResultCode.GLOBAL_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SendBindJournalMessageAsync(Users user, Journal journal)
|
private async Task SendBindJournalMessageAsync(Users user, Journal journal)
|
||||||
@ -384,7 +504,6 @@ public class UserJournalService(
|
|||||||
RefAsync<int> totalNumber = 0;
|
RefAsync<int> totalNumber = 0;
|
||||||
var pageResult = await userJournalRepository.Queryable()
|
var pageResult = await userJournalRepository.Queryable()
|
||||||
.Where(uj => uj.UserId == userId)
|
.Where(uj => uj.UserId == userId)
|
||||||
.WhereIF(!string.IsNullOrWhiteSpace(input.Type), uj => uj.Type.ToString() == input.Type)
|
|
||||||
.OrderByDescending(uj => uj.CreatedAt)
|
.OrderByDescending(uj => uj.CreatedAt)
|
||||||
.Select(uj => new BindJournalOutput
|
.Select(uj => new BindJournalOutput
|
||||||
{
|
{
|
||||||
|
|||||||
@ -28,10 +28,27 @@ public class UsersService(
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<BaseResponse<PageListModel<UsersOutput>>> GetListAsync(UsersQueryInput input)
|
public async Task<BaseResponse<PageListModel<UsersOutput>>> GetListAsync(UsersQueryInput input)
|
||||||
{
|
{
|
||||||
var page = Queryable()
|
RefAsync<int> totalNumber = 0;
|
||||||
.WhereIF(!string.IsNullOrEmpty(input.WxUserId), u => u.WxUserId.ToString() == input.WxUserId)
|
var list = await Queryable()
|
||||||
.OrderBy(u => u.Id, OrderByType.Desc)
|
.LeftJoin<WxUser>((u, w) => u.WxUserId == w.Id && !w.IsDeleted)
|
||||||
.ToPage<Users, UsersOutput>(input);
|
.WhereIF(!string.IsNullOrEmpty(input.WxUserId), (u, w) => u.WxUserId.ToString() == input.WxUserId)
|
||||||
|
.OrderBy((u, w) => u.Id, OrderByType.Desc)
|
||||||
|
.Select((u, w) => new UsersOutput
|
||||||
|
{
|
||||||
|
Id = u.Id,
|
||||||
|
WxUserId = u.WxUserId.ToString(),
|
||||||
|
WxUserName = w.Name,
|
||||||
|
Name = u.Name,
|
||||||
|
AvatarUrl = u.AvatarUrl,
|
||||||
|
Points = u.Points,
|
||||||
|
Type = u.Type.ToString(),
|
||||||
|
Status = u.Status.ToString(),
|
||||||
|
GrowthPoints = u.GrowthPoints,
|
||||||
|
UploadDomain = u.UploadDomain
|
||||||
|
})
|
||||||
|
.ToPageListAsync(input.PageIndex, input.PageSize, totalNumber);
|
||||||
|
|
||||||
|
var page = new PageListModel<UsersOutput>(list, input.PageIndex, input.PageSize, totalNumber);
|
||||||
|
|
||||||
return BaseResponse<PageListModel<UsersOutput>>.Success(page);
|
return BaseResponse<PageListModel<UsersOutput>>.Success(page);
|
||||||
}
|
}
|
||||||
@ -41,7 +58,23 @@ public class UsersService(
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<BaseResponse<UserDetailOutput>> GetDetailAsync(long id)
|
public async Task<BaseResponse<UserDetailOutput>> GetDetailAsync(long id)
|
||||||
{
|
{
|
||||||
var user = await GetByIdAsync<UsersOutput>(u => u.Id == id);
|
var user = await Queryable()
|
||||||
|
.LeftJoin<WxUser>((u, w) => u.WxUserId == w.Id && !w.IsDeleted)
|
||||||
|
.Where((u, w) => u.Id == id)
|
||||||
|
.Select((u, w) => new UsersOutput
|
||||||
|
{
|
||||||
|
Id = u.Id,
|
||||||
|
WxUserId = u.WxUserId.ToString(),
|
||||||
|
WxUserName = w.Name,
|
||||||
|
Name = u.Name,
|
||||||
|
AvatarUrl = u.AvatarUrl,
|
||||||
|
Points = u.Points,
|
||||||
|
Type = u.Type.ToString(),
|
||||||
|
Status = u.Status.ToString(),
|
||||||
|
GrowthPoints = u.GrowthPoints,
|
||||||
|
UploadDomain = u.UploadDomain
|
||||||
|
})
|
||||||
|
.FirstAsync();
|
||||||
if (user == null)
|
if (user == null)
|
||||||
{
|
{
|
||||||
return BaseResponse<UserDetailOutput>.Fail("用户不存在");
|
return BaseResponse<UserDetailOutput>.Fail("用户不存在");
|
||||||
|
|||||||
@ -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>
|
||||||
/// 小程序背包控制器
|
/// 小程序背包控制器
|
||||||
@ -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>
|
||||||
/// 签到控制器
|
/// 签到控制器
|
||||||
@ -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>
|
||||||
/// 小程序社区控制器
|
/// 小程序社区控制器
|
||||||
@ -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>
|
||||||
/// 小程序期刊管理控制器
|
/// 小程序期刊管理控制器
|
||||||
@ -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>
|
||||||
/// 小程序商城控制器
|
/// 小程序商城控制器
|
||||||
@ -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>
|
||||||
/// 小程序勋章控制器
|
/// 小程序勋章控制器
|
||||||
@ -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>
|
||||||
/// 小程序宠物管理控制器
|
/// 小程序宠物管理控制器
|
||||||
@ -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>
|
||||||
/// 小程序首页控制器
|
/// 小程序首页控制器
|
||||||
@ -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>
|
||||||
/// 微信小程序认证控制器
|
/// 微信小程序认证控制器
|
||||||
@ -6,7 +6,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>
|
||||||
/// 小程序基础控制器
|
/// 小程序基础控制器
|
||||||
189
QYZH.InteractiveMagazine.WeChatApi/Program.cs
Normal file
189
QYZH.InteractiveMagazine.WeChatApi/Program.cs
Normal 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();
|
||||||
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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>
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
71
QYZH.InteractiveMagazine.WeChatApi/appsettings.json
Normal file
71
QYZH.InteractiveMagazine.WeChatApi/appsettings.json
Normal 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/"
|
||||||
|
}
|
||||||
|
}
|
||||||
43
QYZH.InteractiveMagazine.WeChatApi/medal-rule-config.json
Normal file
43
QYZH.InteractiveMagazine.WeChatApi/medal-rule-config.json
Normal 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": "说服力"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -19,10 +19,22 @@ public class UserJournalQrCodeController(IUserJournalService userJournalService)
|
|||||||
/// <param name="input">生成输入</param>
|
/// <param name="input">生成输入</param>
|
||||||
/// <returns>二维码记录</returns>
|
/// <returns>二维码记录</returns>
|
||||||
[HttpPost("add")]
|
[HttpPost("add")]
|
||||||
public async Task<BaseResponse<UserJournalQrCodeOutput>> AddAsync([FromBody] CreateUserJournalQrCodeInput input)
|
public async Task<BaseResponse<CreateUserJournalQrCodeOutput>> AddAsync([FromBody] CreateUserJournalQrCodeInput input)
|
||||||
{
|
{
|
||||||
var result = await userJournalService.CreateQrCodeAsync(input, GetCurrentUserId() ?? 0);
|
var result = await userJournalService.CreateQrCodesAsync(input, GetCurrentUserName() ?? "System");
|
||||||
return BaseResponse<UserJournalQrCodeOutput>.Success(result);
|
return BaseResponse<CreateUserJournalQrCodeOutput>.Success(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据期刊Id获取未绑定用户的二维码地址
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="journalId">期刊Id</param>
|
||||||
|
/// <returns>未绑定二维码列表</returns>
|
||||||
|
[HttpGet("unbound/{journalId:long}")]
|
||||||
|
public async Task<BaseResponse<List<UserJournalQrCodeOutput>>> GetUnboundByJournalIdAsync(long journalId)
|
||||||
|
{
|
||||||
|
var result = await userJournalService.GetUnboundQrCodesByJournalIdAsync(journalId);
|
||||||
|
return BaseResponse<List<UserJournalQrCodeOutput>>.Success(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -57,7 +69,7 @@ public class UserJournalQrCodeController(IUserJournalService userJournalService)
|
|||||||
[HttpPost("delete")]
|
[HttpPost("delete")]
|
||||||
public async Task<BaseResponse<bool>> DeleteAsync([FromBody] DeleteUserJournalQrCodeInput input)
|
public async Task<BaseResponse<bool>> DeleteAsync([FromBody] DeleteUserJournalQrCodeInput input)
|
||||||
{
|
{
|
||||||
var result = await userJournalService.DeleteQrCodeAsync(input, GetCurrentUserId() ?? 0);
|
var result = await userJournalService.DeleteQrCodeAsync(input, GetCurrentUserName() ?? "System");
|
||||||
return BaseResponse<bool>.Success(result);
|
return BaseResponse<bool>.Success(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,8 +28,8 @@ public class UsersController : BaseController
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取用户列表(分页)
|
/// 获取用户列表(分页)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[HttpGet]
|
[HttpPost]
|
||||||
public async Task<BaseResponse<PageListModel<UsersOutput>>> GetList([FromQuery] UsersQueryInput input)
|
public async Task<BaseResponse<PageListModel<UsersOutput>>> GetList([FromBody] UsersQueryInput input)
|
||||||
{
|
{
|
||||||
return await _usersService.GetListAsync(input);
|
return await _usersService.GetListAsync(input);
|
||||||
}
|
}
|
||||||
@ -46,8 +46,8 @@ public class UsersController : BaseController
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 分页查询用户积分记录
|
/// 分页查询用户积分记录
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[HttpGet("{id}/pointsRecords")]
|
[HttpPost("{id}/pointsRecords")]
|
||||||
public async Task<BaseResponse<PageListModel<PointsRecordOutput>>> GetUserPointsRecords(long id, [FromQuery] PointsRecordQueryInput input)
|
public async Task<BaseResponse<PageListModel<PointsRecordOutput>>> GetUserPointsRecords(long id, [FromBody] PointsRecordQueryInput input)
|
||||||
{
|
{
|
||||||
return await _usersService.GetUserPointsRecordsAsync(id, input);
|
return await _usersService.GetUserPointsRecordsAsync(id, input);
|
||||||
}
|
}
|
||||||
@ -55,8 +55,8 @@ public class UsersController : BaseController
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 分页查询用户签到记录
|
/// 分页查询用户签到记录
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[HttpGet("{id}/checkInRecords")]
|
[HttpPost("{id}/checkInRecords")]
|
||||||
public async Task<BaseResponse<PageListModel<CheckInRecordOutput>>> GetUserCheckInRecords(long id, [FromQuery] PageQueryModel input)
|
public async Task<BaseResponse<PageListModel<CheckInRecordOutput>>> GetUserCheckInRecords(long id, [FromBody] PageQueryModel input)
|
||||||
{
|
{
|
||||||
return await _usersService.GetUserCheckInRecordsAsync(id, input);
|
return await _usersService.GetUserCheckInRecordsAsync(id, input);
|
||||||
}
|
}
|
||||||
@ -64,8 +64,8 @@ public class UsersController : BaseController
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 分页查询用户补偿任务
|
/// 分页查询用户补偿任务
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[HttpGet("{id}/compensationTasks")]
|
[HttpPost("{id}/compensationTasks")]
|
||||||
public async Task<BaseResponse<PageListModel<CompensationTaskOutput>>> GetUserCompensationTasks(long id, [FromQuery] CompensationTaskQueryInput input)
|
public async Task<BaseResponse<PageListModel<CompensationTaskOutput>>> GetUserCompensationTasks(long id, [FromBody] CompensationTaskQueryInput input)
|
||||||
{
|
{
|
||||||
return await _usersService.GetUserCompensationTasksAsync(id, input);
|
return await _usersService.GetUserCompensationTasksAsync(id, input);
|
||||||
}
|
}
|
||||||
@ -73,8 +73,8 @@ public class UsersController : BaseController
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 分页查询用户期刊列表
|
/// 分页查询用户期刊列表
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[HttpGet("{id}/journals")]
|
[HttpPost("{id}/journals")]
|
||||||
public async Task<BaseResponse<PageListModel<UserJournalItemOutput>>> GetUserJournals(long id, [FromQuery] UserJournalQueryInput input)
|
public async Task<BaseResponse<PageListModel<UserJournalItemOutput>>> GetUserJournals(long id, [FromBody] UserJournalQueryInput input)
|
||||||
{
|
{
|
||||||
return await _usersService.GetUserJournalsAsync(id, input);
|
return await _usersService.GetUserJournalsAsync(id, input);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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();
|
||||||
|
|
||||||
|
|||||||
@ -21,9 +21,14 @@ public class JournalTaskReceiveConsumer(
|
|||||||
{
|
{
|
||||||
private const int DefaultAiScoreMaxRetryCount = 3;
|
private const int DefaultAiScoreMaxRetryCount = 3;
|
||||||
private const int DefaultAiScoreRetryDelayMilliseconds = 1000;
|
private const int DefaultAiScoreRetryDelayMilliseconds = 1000;
|
||||||
|
private const int DefaultAiMaxConcurrency = 1;
|
||||||
private const long DefaultMaxImageBytes = 10 * 1024 * 1024;
|
private const long DefaultMaxImageBytes = 10 * 1024 * 1024;
|
||||||
private const float DefaultCompletionThreshold = 80;
|
private const float DefaultCompletionThreshold = 80;
|
||||||
private const float DefaultCommunityScoreThreshold = 90;
|
private const float DefaultCommunityScoreThreshold = 90;
|
||||||
|
private const string AiProcessingMessage = "AI批阅中,请稍后";
|
||||||
|
private static readonly object AiSemaphoreLock = new();
|
||||||
|
private static SemaphoreSlim? aiSemaphore;
|
||||||
|
private static int aiSemaphoreLimit;
|
||||||
|
|
||||||
public string Exchange => "ex.journal";
|
public string Exchange => "ex.journal";
|
||||||
|
|
||||||
@ -42,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)
|
||||||
{
|
{
|
||||||
@ -54,17 +60,38 @@ public class JournalTaskReceiveConsumer(
|
|||||||
.Where(t => taskIds.Contains(t.Id) && !t.IsDeleted)
|
.Where(t => taskIds.Contains(t.Id) && !t.IsDeleted)
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
var taskMap = tasks.ToDictionary(t => t.Id);
|
var taskMap = tasks.ToDictionary(t => t.Id);
|
||||||
|
var groupIds = tasks.Select(t => t.GroupId > 0 ? t.GroupId : t.Id).Distinct().ToList();
|
||||||
|
var groupTasks = await client.Queryable<JournalPageTask>()
|
||||||
|
.Where(t => groupIds.Contains(t.GroupId) && !t.IsDeleted)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
var persistedTaskMap = groupTasks
|
||||||
|
.GroupBy(t => t.GroupId > 0 ? t.GroupId : t.Id)
|
||||||
|
.ToDictionary(
|
||||||
|
g => g.Key,
|
||||||
|
g => g.FirstOrDefault(t => t.Id == g.Key)
|
||||||
|
?? g.OrderBy(t => t.Id).First());
|
||||||
|
var groupTaskIds = groupTasks.Select(t => t.Id).Distinct().ToList();
|
||||||
var referenceAnswers = await client.Queryable<JournalPageTaskAnswer>()
|
var referenceAnswers = await client.Queryable<JournalPageTaskAnswer>()
|
||||||
.Where(a => taskIds.Contains(a.JournalPageTaskId) && !a.IsDeleted)
|
.Where(a => groupTaskIds.Contains(a.JournalPageTaskId) && !a.IsDeleted)
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
var referenceAnswerMap = referenceAnswers
|
var referenceAnswerMap = referenceAnswers
|
||||||
.GroupBy(a => a.JournalPageTaskId)
|
.GroupBy(a => a.JournalPageTaskId)
|
||||||
.ToDictionary(g => g.Key, g => g.ToList());
|
.ToDictionary(g => g.Key, g => g.ToList());
|
||||||
|
|
||||||
var page = await client.Queryable<JournalPage>()
|
var pageIds = groupTasks.Select(t => t.JournalPageId).Append(data.PageId).Distinct().ToList();
|
||||||
.Where(p => p.Id == data.PageId && !p.IsDeleted)
|
var pages = await client.Queryable<JournalPage>()
|
||||||
.FirstAsync(cancellationToken);
|
.Where(p => pageIds.Contains(p.Id) && !p.IsDeleted)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
var pageMap = pages.ToDictionary(p => p.Id);
|
||||||
|
|
||||||
|
var existingAnswers = await client.Queryable<JournalPageTaskUserAnswer>()
|
||||||
|
.Where(a => a.UserId == data.UserId && groupTaskIds.Contains(a.JournalPageTaskId) && !a.IsDeleted)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
var existingAnswerMap = existingAnswers
|
||||||
|
.GroupBy(a => a.JournalPageTaskId)
|
||||||
|
.ToDictionary(g => g.Key, g => g.OrderByDescending(a => a.UpdatedAt ?? a.CreatedAt).First());
|
||||||
|
|
||||||
|
var questionContexts = new List<JournalQuestionContext>();
|
||||||
var answerContexts = new List<JournalAnswerContext>();
|
var answerContexts = new List<JournalAnswerContext>();
|
||||||
foreach (var question in data.Questions)
|
foreach (var question in data.Questions)
|
||||||
{
|
{
|
||||||
@ -80,13 +107,55 @@ public class JournalTaskReceiveConsumer(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
referenceAnswerMap.TryGetValue(task.Id, out var taskReferenceAnswers);
|
pageMap.TryGetValue(task.JournalPageId, out var taskPage);
|
||||||
var scoreResult = await ScoreQuestionAsync(task, question, taskReferenceAnswers ?? [], cancellationToken);
|
questionContexts.Add(new JournalQuestionContext(question, task, taskPage));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var scoreUnit in BuildScoreUnits(questionContexts))
|
||||||
|
{
|
||||||
|
var persistedContext = BuildPersistedContext(scoreUnit, persistedTaskMap, pageMap);
|
||||||
|
if (persistedContext == null)
|
||||||
|
{
|
||||||
|
logger.LogWarning("期刊任务评分单元缺少可入库任务,UserId: {UserId}, GroupId: {GroupId}",
|
||||||
|
data.UserId, scoreUnit.GroupId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsCompletedSameAnswer(scoreUnit, persistedContext.Task, data, existingAnswerMap))
|
||||||
|
{
|
||||||
|
logger.LogInformation("期刊任务作答已完成且内容未变化,跳过AI评分,UserId: {UserId}, GroupId: {GroupId}, TaskIds: {TaskIds}",
|
||||||
|
data.UserId, scoreUnit.GroupId, string.Join(",", scoreUnit.Questions.Select(q => q.Task.Id)));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var scoreResult = await ScoreQuestionAsync(scoreUnit, referenceAnswerMap, cancellationToken);
|
||||||
|
var normalizedResult = NormalizeScoreResult(scoreResult, persistedContext.Task);
|
||||||
answerContexts.Add(new JournalAnswerContext(
|
answerContexts.Add(new JournalAnswerContext(
|
||||||
BuildAnswerEntity(data, question, task, page, scoreResult, GetCompletionThreshold()),
|
BuildAnswerEntity(data, scoreUnit, persistedContext.Question, persistedContext.Task, persistedContext.Page, normalizedResult, GetCompletionThreshold()),
|
||||||
question,
|
persistedContext.Question,
|
||||||
task,
|
persistedContext.Task,
|
||||||
scoreResult));
|
normalizedResult));
|
||||||
|
LogSkippedGroupTasks(scoreUnit, persistedContext.Task);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogWarning(ex, "期刊任务AI评分失败,已标记为处理中,UserId: {UserId}, GroupId: {GroupId}, TaskIds: {TaskIds}",
|
||||||
|
data.UserId, scoreUnit.GroupId, string.Join(",", scoreUnit.Questions.Select(q => q.Task.Id)));
|
||||||
|
|
||||||
|
var failureResult = BuildFailureScoreResult();
|
||||||
|
answerContexts.Add(new JournalAnswerContext(
|
||||||
|
BuildAnswerEntity(data, scoreUnit, persistedContext.Question, persistedContext.Task, persistedContext.Page, failureResult, GetCompletionThreshold()),
|
||||||
|
persistedContext.Question,
|
||||||
|
persistedContext.Task,
|
||||||
|
failureResult));
|
||||||
|
LogSkippedGroupTasks(scoreUnit, persistedContext.Task);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (answerContexts.Count == 0)
|
if (answerContexts.Count == 0)
|
||||||
@ -148,9 +217,8 @@ public class JournalTaskReceiveConsumer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async Task<JournalAnswerScoreResult> ScoreQuestionAsync(
|
private async Task<JournalAnswerScoreResult> ScoreQuestionAsync(
|
||||||
JournalPageTask task,
|
JournalScoreUnit scoreUnit,
|
||||||
Question question,
|
Dictionary<long, List<JournalPageTaskAnswer>> referenceAnswerMap,
|
||||||
List<JournalPageTaskAnswer> referenceAnswers,
|
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var apiKey = configuration["AiChat:ApiKey"];
|
var apiKey = configuration["AiChat:ApiKey"];
|
||||||
@ -158,30 +226,33 @@ public class JournalTaskReceiveConsumer(
|
|||||||
var model = configuration["AiChat:Model"];
|
var model = configuration["AiChat:Model"];
|
||||||
var timeoutSeconds = configuration.GetValue<int>("AiChat:TimeoutSeconds");
|
var timeoutSeconds = configuration.GetValue<int>("AiChat:TimeoutSeconds");
|
||||||
var maxTokens = configuration.GetValue<int>("AiChat:MaxTokens");
|
var maxTokens = configuration.GetValue<int>("AiChat:MaxTokens");
|
||||||
var temperature = configuration.GetValue<double>("AiChat:Temperature");
|
var temperature = configuration.GetValue<double?>("AiChat:Temperature");
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(apiKey) || string.IsNullOrWhiteSpace(baseUrl) || string.IsNullOrWhiteSpace(model))
|
if (string.IsNullOrWhiteSpace(apiKey) || string.IsNullOrWhiteSpace(baseUrl) || string.IsNullOrWhiteSpace(model))
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("AI聊天服务配置不完整,请检查 AiChat 配置节点");
|
throw new InvalidOperationException("AI聊天服务配置不完整,请检查 AiChat 配置节点");
|
||||||
}
|
}
|
||||||
|
|
||||||
var answerImages = await BuildAnswerImageContentsAsync(question, cancellationToken);
|
|
||||||
if (answerImages.Count == 0)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException($"题目 {question.Id} 缺少答案图片");
|
|
||||||
}
|
|
||||||
|
|
||||||
var referenceAnswerImages = await BuildReferenceAnswerImageContentsAsync(task.Id, referenceAnswers, cancellationToken);
|
|
||||||
|
|
||||||
var content = new List<object>
|
var content = new List<object>
|
||||||
{
|
{
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
type = "text",
|
type = "text",
|
||||||
text = BuildScorePrompt(task, question, answerImages.Count, referenceAnswers, referenceAnswerImages.Count)
|
text = BuildScorePrompt(scoreUnit, referenceAnswerMap)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var totalAnswerImageCount = 0;
|
||||||
|
foreach (var context in scoreUnit.Questions)
|
||||||
|
{
|
||||||
|
var answerImages = await BuildAnswerImageContentsAsync(context.Question, cancellationToken);
|
||||||
|
totalAnswerImageCount += answerImages.Count;
|
||||||
|
content.Add(new
|
||||||
|
{
|
||||||
|
type = "text",
|
||||||
|
text = $"以下为任务 {context.Task.Id} 的学生作答图片,共 {answerImages.Count} 张。"
|
||||||
|
});
|
||||||
|
|
||||||
foreach (var answerImage in answerImages)
|
foreach (var answerImage in answerImages)
|
||||||
{
|
{
|
||||||
content.Add(new
|
content.Add(new
|
||||||
@ -190,13 +261,26 @@ public class JournalTaskReceiveConsumer(
|
|||||||
image_url = new { url = answerImage.DataUrl }
|
image_url = new { url = answerImage.DataUrl }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (referenceAnswerImages.Count > 0)
|
if (totalAnswerImageCount == 0)
|
||||||
{
|
{
|
||||||
|
throw new InvalidOperationException($"评分单元 {scoreUnit.GroupId} 缺少答案图片");
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var context in scoreUnit.Questions)
|
||||||
|
{
|
||||||
|
referenceAnswerMap.TryGetValue(context.Task.Id, out var referenceAnswers);
|
||||||
|
var referenceAnswerImages = await BuildReferenceAnswerImageContentsAsync(context.Task.Id, referenceAnswers ?? [], cancellationToken);
|
||||||
|
if (referenceAnswerImages.Count == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
content.Add(new
|
content.Add(new
|
||||||
{
|
{
|
||||||
type = "text",
|
type = "text",
|
||||||
text = $"以下为参考答案图片,共 {referenceAnswerImages.Count} 张。参考答案不是必有,评分时以题目Prompt和学生答案为主。"
|
text = $"以下为任务 {context.Task.Id} 的参考答案图片,共 {referenceAnswerImages.Count} 张。参考答案不是必有,评分时以题目Prompt和学生答案为主。"
|
||||||
});
|
});
|
||||||
|
|
||||||
foreach (var referenceAnswerImage in referenceAnswerImages)
|
foreach (var referenceAnswerImage in referenceAnswerImages)
|
||||||
@ -226,7 +310,7 @@ public class JournalTaskReceiveConsumer(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
max_tokens = maxTokens > 0 ? maxTokens : 2000,
|
max_tokens = maxTokens > 0 ? maxTokens : 2000,
|
||||||
temperature = temperature > 0 ? temperature : 0.2,
|
temperature = temperature is >= 0 ? temperature.Value : 0.1,
|
||||||
stream = false,
|
stream = false,
|
||||||
response_format = new { type = "json_object" }
|
response_format = new { type = "json_object" }
|
||||||
};
|
};
|
||||||
@ -236,35 +320,46 @@ public class JournalTaskReceiveConsumer(
|
|||||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||||
});
|
});
|
||||||
|
|
||||||
var responseContent = await SendAiScoreRequestWithRetryAsync(
|
var semaphore = GetAiSemaphore();
|
||||||
task.Id,
|
await semaphore.WaitAsync(cancellationToken);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var scoreResult = await SendAiScoreRequestWithRetryAsync(
|
||||||
|
scoreUnit.GroupId,
|
||||||
$"{baseUrl.TrimEnd('/')}/chat/completions",
|
$"{baseUrl.TrimEnd('/')}/chat/completions",
|
||||||
apiKey,
|
apiKey,
|
||||||
requestJson,
|
requestJson,
|
||||||
timeoutSeconds > 0 ? timeoutSeconds : 300,
|
timeoutSeconds > 0 ? timeoutSeconds : 300,
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
var resultJson = ExtractAssistantContent(responseContent);
|
|
||||||
var scoreResult = ParseScoreResult(resultJson);
|
|
||||||
scoreResult.Result = TrimResult(scoreResult.Result);
|
scoreResult.Result = TrimResult(scoreResult.Result);
|
||||||
return scoreResult;
|
return scoreResult;
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
semaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static string BuildScorePrompt(
|
private static string BuildScorePrompt(
|
||||||
JournalPageTask task,
|
JournalScoreUnit scoreUnit,
|
||||||
Question question,
|
Dictionary<long, List<JournalPageTaskAnswer>> referenceAnswerMap)
|
||||||
int answerImageCount,
|
|
||||||
List<JournalPageTaskAnswer> referenceAnswers,
|
|
||||||
int referenceAnswerImageCount)
|
|
||||||
{
|
{
|
||||||
var referenceAnswerTexts = referenceAnswers
|
var referenceAnswerTexts = scoreUnit.Questions
|
||||||
|
.SelectMany(q =>
|
||||||
|
{
|
||||||
|
referenceAnswerMap.TryGetValue(q.Task.Id, out var answers);
|
||||||
|
return answers ?? [];
|
||||||
|
})
|
||||||
.Select(a => a.Answer?.Trim())
|
.Select(a => a.Answer?.Trim())
|
||||||
.Where(a => !string.IsNullOrWhiteSpace(a))
|
.Where(a => !string.IsNullOrWhiteSpace(a))
|
||||||
.Distinct()
|
.Distinct()
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var prompt = new StringBuilder();
|
var prompt = new StringBuilder();
|
||||||
|
prompt.AppendLine(scoreUnit.Questions.Count > 1
|
||||||
|
? "这是同一跨页题目的多页作答,请综合全部学生作答图片评分。"
|
||||||
|
: "这是单页题目作答,请根据学生作答图片评分。");
|
||||||
prompt.AppendLine("参考答案不是必有;若无参考答案,以题目 Prompt 和学生答案为准评分。");
|
prompt.AppendLine("参考答案不是必有;若无参考答案,以题目 Prompt 和学生答案为准评分。");
|
||||||
prompt.AppendLine($"参考答案图片数量:{referenceAnswerImageCount}");
|
|
||||||
if (referenceAnswerTexts.Count > 0)
|
if (referenceAnswerTexts.Count > 0)
|
||||||
{
|
{
|
||||||
prompt.AppendLine("参考答案文本:");
|
prompt.AppendLine("参考答案文本:");
|
||||||
@ -278,21 +373,27 @@ public class JournalTaskReceiveConsumer(
|
|||||||
prompt.AppendLine("参考答案文本:无");
|
prompt.AppendLine("参考答案文本:无");
|
||||||
}
|
}
|
||||||
prompt.AppendLine();
|
prompt.AppendLine();
|
||||||
prompt.AppendLine("请根据题目评分 Prompt 和学生答案图片进行评分。");
|
prompt.AppendLine("题目信息:");
|
||||||
|
foreach (var context in scoreUnit.Questions)
|
||||||
|
{
|
||||||
|
prompt.AppendLine($"- TaskId:{context.Task.Id}");
|
||||||
|
prompt.AppendLine($" 页码:{context.Page?.PageNum ?? 0}");
|
||||||
|
prompt.AppendLine($" 题号:{context.Task.No}");
|
||||||
|
prompt.AppendLine($" 题目内容:{context.Task.Task}");
|
||||||
|
prompt.AppendLine($" 评分Prompt:{context.Task.Prompt}");
|
||||||
|
prompt.AppendLine($" 成长值上限:{context.Task.GrowthPoint}");
|
||||||
|
prompt.AppendLine($" 积分上限:{context.Task.Points}");
|
||||||
|
prompt.AppendLine($" 理解力上限:{context.Task.Comprehension}");
|
||||||
|
prompt.AppendLine($" 判断力上限:{context.Task.Judgment}");
|
||||||
|
prompt.AppendLine($" 表达力上限:{context.Task.Expression}");
|
||||||
|
prompt.AppendLine($" 说服力上限:{context.Task.Persuasiveness}");
|
||||||
|
}
|
||||||
prompt.AppendLine();
|
prompt.AppendLine();
|
||||||
prompt.AppendLine($"题目内容:{task.Task}");
|
prompt.AppendLine("评分要求:");
|
||||||
prompt.AppendLine("评分Prompt:");
|
prompt.AppendLine("- 不得超过题目配置中的各项上限。");
|
||||||
prompt.AppendLine(task.Prompt);
|
prompt.AppendLine("- 看不清、缺页、无法识别或答案明显不完整时,降低 Completion,不要猜测高分。");
|
||||||
prompt.AppendLine();
|
prompt.AppendLine("- Completion 表示作答完整度,范围 0-100。");
|
||||||
prompt.AppendLine("题目配置:");
|
prompt.AppendLine("- Result 返回 50 字内中文评语。");
|
||||||
prompt.AppendLine($"- 成长值上限:{task.GrowthPoint}");
|
|
||||||
prompt.AppendLine($"- 积分上限:{task.Points}");
|
|
||||||
prompt.AppendLine($"- 理解力上限:{task.Comprehension}");
|
|
||||||
prompt.AppendLine($"- 判断力上限:{task.Judgment}");
|
|
||||||
prompt.AppendLine($"- 表达力上限:{task.Expression}");
|
|
||||||
prompt.AppendLine($"- 说服力上限:{task.Persuasiveness}");
|
|
||||||
prompt.AppendLine();
|
|
||||||
prompt.AppendLine($"学生答案图片数量:{answerImageCount}");
|
|
||||||
prompt.AppendLine();
|
prompt.AppendLine();
|
||||||
prompt.AppendLine("只返回如下 JSON 字段:");
|
prompt.AppendLine("只返回如下 JSON 字段:");
|
||||||
prompt.AppendLine("{");
|
prompt.AppendLine("{");
|
||||||
@ -408,7 +509,7 @@ public class JournalTaskReceiveConsumer(
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<string> SendAiScoreRequestWithRetryAsync(
|
private async Task<JournalAnswerScoreResult> SendAiScoreRequestWithRetryAsync(
|
||||||
long taskId,
|
long taskId,
|
||||||
string requestUrl,
|
string requestUrl,
|
||||||
string apiKey,
|
string apiKey,
|
||||||
@ -446,22 +547,37 @@ public class JournalTaskReceiveConsumer(
|
|||||||
var responseContent = await response.Content.ReadAsStringAsync(cancellationToken);
|
var responseContent = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||||
if (response.IsSuccessStatusCode)
|
if (response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
return responseContent;
|
var resultJson = ExtractAssistantContent(responseContent);
|
||||||
|
return ParseScoreResult(resultJson);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.LogWarning("AI评分调用失败,TaskId: {TaskId}, Attempt: {Attempt}/{MaxRetryCount}, StatusCode: {StatusCode}, Response: {Response}",
|
logger.LogWarning("AI评分调用失败,TaskId: {TaskId}, Attempt: {Attempt}/{MaxRetryCount}, StatusCode: {StatusCode}, Response: {Response}",
|
||||||
taskId, attempt, maxRetryCount, response.StatusCode, responseContent);
|
taskId, attempt, maxRetryCount, response.StatusCode, responseContent);
|
||||||
|
|
||||||
if (!ShouldRetry(response.StatusCode) || attempt == maxRetryCount)
|
if (!ShouldRetry(response.StatusCode))
|
||||||
|
{
|
||||||
|
throw new NonRetryAiScoreException($"AI评分调用失败:{response.StatusCode},响应:{responseContent}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attempt == maxRetryCount)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException($"AI评分调用失败:{response.StatusCode},响应:{responseContent}");
|
throw new InvalidOperationException($"AI评分调用失败:{response.StatusCode},响应:{responseContent}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
catch (Exception ex) when (attempt < maxRetryCount)
|
catch (OperationCanceledException ex) when (attempt < maxRetryCount)
|
||||||
|
{
|
||||||
|
lastException = ex;
|
||||||
|
logger.LogWarning(ex, "AI评分调用超时,准备重试,TaskId: {TaskId}, Attempt: {Attempt}/{MaxRetryCount}", taskId, attempt, maxRetryCount);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException ex)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"AI评分调用超时,TaskId: {taskId}", ex);
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (attempt < maxRetryCount && ex is not NonRetryAiScoreException)
|
||||||
{
|
{
|
||||||
lastException = ex;
|
lastException = ex;
|
||||||
logger.LogWarning(ex, "AI评分调用异常,准备重试,TaskId: {TaskId}, Attempt: {Attempt}/{MaxRetryCount}", taskId, attempt, maxRetryCount);
|
logger.LogWarning(ex, "AI评分调用异常,准备重试,TaskId: {TaskId}, Attempt: {Attempt}/{MaxRetryCount}", taskId, attempt, maxRetryCount);
|
||||||
@ -536,6 +652,7 @@ public class JournalTaskReceiveConsumer(
|
|||||||
private static JournalAnswerScoreResult ParseScoreResult(string resultJson)
|
private static JournalAnswerScoreResult ParseScoreResult(string resultJson)
|
||||||
{
|
{
|
||||||
var cleanedJson = CleanJsonContent(resultJson);
|
var cleanedJson = CleanJsonContent(resultJson);
|
||||||
|
EnsureRequiredScoreFields(cleanedJson);
|
||||||
var result = JsonSerializer.Deserialize<JournalAnswerScoreResult>(cleanedJson, new JsonSerializerOptions
|
var result = JsonSerializer.Deserialize<JournalAnswerScoreResult>(cleanedJson, new JsonSerializerOptions
|
||||||
{
|
{
|
||||||
PropertyNameCaseInsensitive = true
|
PropertyNameCaseInsensitive = true
|
||||||
@ -572,8 +689,219 @@ public class JournalTaskReceiveConsumer(
|
|||||||
return text.Trim();
|
return text.Trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void EnsureRequiredScoreFields(string json)
|
||||||
|
{
|
||||||
|
using var document = JsonDocument.Parse(json);
|
||||||
|
var requiredFields = new[]
|
||||||
|
{
|
||||||
|
nameof(JournalAnswerScoreResult.Score),
|
||||||
|
nameof(JournalAnswerScoreResult.GrowthPoint),
|
||||||
|
nameof(JournalAnswerScoreResult.Points),
|
||||||
|
nameof(JournalAnswerScoreResult.Comprehension),
|
||||||
|
nameof(JournalAnswerScoreResult.Judgment),
|
||||||
|
nameof(JournalAnswerScoreResult.Expression),
|
||||||
|
nameof(JournalAnswerScoreResult.Persuasiveness),
|
||||||
|
nameof(JournalAnswerScoreResult.Completion),
|
||||||
|
nameof(JournalAnswerScoreResult.Result)
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var field in requiredFields)
|
||||||
|
{
|
||||||
|
if (!document.RootElement.EnumerateObject().Any(p => string.Equals(p.Name, field, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"AI评分结果缺少字段:{field}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JournalAnswerScoreResult NormalizeScoreResult(JournalAnswerScoreResult scoreResult, JournalPageTask task)
|
||||||
|
{
|
||||||
|
var scoreMax = task.Comprehension + task.Judgment + task.Expression + task.Persuasiveness;
|
||||||
|
if (scoreMax <= 0)
|
||||||
|
{
|
||||||
|
scoreMax = 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new JournalAnswerScoreResult
|
||||||
|
{
|
||||||
|
Score = Clamp(scoreResult.Score, 0, scoreMax),
|
||||||
|
GrowthPoint = (int)Clamp(scoreResult.GrowthPoint, 0, task.GrowthPoint),
|
||||||
|
Points = (int)Clamp(scoreResult.Points, 0, task.Points),
|
||||||
|
Comprehension = Clamp(scoreResult.Comprehension, 0, task.Comprehension),
|
||||||
|
Judgment = Clamp(scoreResult.Judgment, 0, task.Judgment),
|
||||||
|
Expression = Clamp(scoreResult.Expression, 0, task.Expression),
|
||||||
|
Persuasiveness = Clamp(scoreResult.Persuasiveness, 0, task.Persuasiveness),
|
||||||
|
Completion = Clamp(scoreResult.Completion, 0, 100),
|
||||||
|
Result = TrimResult(scoreResult.Result)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float Clamp(float value, float min, float max)
|
||||||
|
{
|
||||||
|
if (float.IsNaN(value) || float.IsInfinity(value))
|
||||||
|
{
|
||||||
|
return min;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (max < min)
|
||||||
|
{
|
||||||
|
max = min;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.Min(Math.Max(value, min), max);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JournalAnswerScoreResult BuildFailureScoreResult()
|
||||||
|
{
|
||||||
|
return new JournalAnswerScoreResult
|
||||||
|
{
|
||||||
|
Completion = 0,
|
||||||
|
Result = AiProcessingMessage
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<JournalScoreUnit> BuildScoreUnits(List<JournalQuestionContext> contexts)
|
||||||
|
{
|
||||||
|
return contexts
|
||||||
|
.GroupBy(c => c.Task.GroupId > 0 ? c.Task.GroupId : c.Task.Id)
|
||||||
|
.Select(g => new JournalScoreUnit(
|
||||||
|
g.Key,
|
||||||
|
g.OrderBy(c => c.Page?.PageNum ?? 0)
|
||||||
|
.ThenBy(c => ParseTaskNo(c.Task.No))
|
||||||
|
.ThenBy(c => c.Task.Id)
|
||||||
|
.ToList()))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JournalQuestionContext? BuildPersistedContext(
|
||||||
|
JournalScoreUnit scoreUnit,
|
||||||
|
Dictionary<long, JournalPageTask> persistedTaskMap,
|
||||||
|
Dictionary<long, JournalPage> pageMap)
|
||||||
|
{
|
||||||
|
if (scoreUnit.Questions.Count == 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sourceContext = scoreUnit.Questions.FirstOrDefault(q => q.Task.Id == scoreUnit.GroupId)
|
||||||
|
?? scoreUnit.Questions.First();
|
||||||
|
var persistedTask = persistedTaskMap.TryGetValue(scoreUnit.GroupId, out var task)
|
||||||
|
? task
|
||||||
|
: sourceContext.Task;
|
||||||
|
pageMap.TryGetValue(persistedTask.JournalPageId, out var persistedPage);
|
||||||
|
|
||||||
|
return new JournalQuestionContext(sourceContext.Question, persistedTask, persistedPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ParseTaskNo(string? taskNo)
|
||||||
|
{
|
||||||
|
return int.TryParse(taskNo, out var no) ? no : int.MaxValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsCompletedSameAnswer(
|
||||||
|
JournalScoreUnit scoreUnit,
|
||||||
|
JournalPageTask persistedTask,
|
||||||
|
QuestionData data,
|
||||||
|
Dictionary<long, JournalPageTaskUserAnswer> existingAnswerMap)
|
||||||
|
{
|
||||||
|
return scoreUnit.Questions.Count > 0
|
||||||
|
&& existingAnswerMap.TryGetValue(persistedTask.Id, out var existing)
|
||||||
|
&& existing.Status == (int)UserAnswerStatusEnum.Complete
|
||||||
|
&& string.Equals(existing.PageAnswerUrl ?? string.Empty, data.PageAnswerUrl ?? string.Empty, StringComparison.Ordinal)
|
||||||
|
&& string.Equals(existing.AnswerUrl ?? string.Empty, SerializeAnswerUrls(scoreUnit), StringComparison.Ordinal)
|
||||||
|
&& existing.AnswerStartTime == GetAnswerStartTime(scoreUnit)
|
||||||
|
&& existing.AnswerEndTime == GetAnswerEndTime(scoreUnit);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SemaphoreSlim GetAiSemaphore()
|
||||||
|
{
|
||||||
|
var maxConcurrency = configuration.GetValue<int>("AiChat:MaxConcurrency");
|
||||||
|
if (maxConcurrency <= 0)
|
||||||
|
{
|
||||||
|
maxConcurrency = DefaultAiMaxConcurrency;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (AiSemaphoreLock)
|
||||||
|
{
|
||||||
|
if (aiSemaphore == null || aiSemaphoreLimit != maxConcurrency)
|
||||||
|
{
|
||||||
|
aiSemaphore = new SemaphoreSlim(maxConcurrency, maxConcurrency);
|
||||||
|
aiSemaphoreLimit = maxConcurrency;
|
||||||
|
}
|
||||||
|
|
||||||
|
return aiSemaphore;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LogSkippedGroupTasks(JournalScoreUnit scoreUnit, JournalPageTask persistedTask)
|
||||||
|
{
|
||||||
|
var skippedTaskIds = scoreUnit.Questions
|
||||||
|
.Select(q => q.Task.Id)
|
||||||
|
.Where(id => id != persistedTask.Id)
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
if (skippedTaskIds.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.LogInformation("期刊跨页题评分结果仅保存到主任务,GroupId: {GroupId}, PersistedTaskId: {PersistedTaskId}, SkippedTaskIds: {SkippedTaskIds}",
|
||||||
|
scoreUnit.GroupId, persistedTask.Id, string.Join(",", skippedTaskIds));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string SerializeAnswerUrls(JournalScoreUnit scoreUnit)
|
||||||
|
{
|
||||||
|
var answerUrls = scoreUnit.Questions
|
||||||
|
.SelectMany(q => q.Question.AnswerUrl ?? [])
|
||||||
|
.Where(url => !string.IsNullOrWhiteSpace(url))
|
||||||
|
.Distinct()
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
return JsonSerializer.Serialize(answerUrls);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DateTime GetAnswerStartTime(JournalScoreUnit scoreUnit)
|
||||||
|
{
|
||||||
|
var startTimes = scoreUnit.Questions
|
||||||
|
.Select(q => q.Question.AnswerStartTime)
|
||||||
|
.Where(t => t != default)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return startTimes.Count == 0 ? default : startTimes.Min();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DateTime GetAnswerEndTime(JournalScoreUnit scoreUnit)
|
||||||
|
{
|
||||||
|
var endTimes = scoreUnit.Questions
|
||||||
|
.Select(q => q.Question.AnswerEndTime)
|
||||||
|
.Where(t => t != default)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return endTimes.Count == 0 ? default : endTimes.Max();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int GetAnswerSeconds(JournalScoreUnit scoreUnit)
|
||||||
|
{
|
||||||
|
return scoreUnit.Questions.Sum(q => Math.Max(0, q.Question.AnswerTime));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int GetBreakCount(JournalScoreUnit scoreUnit)
|
||||||
|
{
|
||||||
|
return scoreUnit.Questions.Sum(q => Math.Max(0, q.Question.BreakCount));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string SerializeBreakTimes(JournalScoreUnit scoreUnit)
|
||||||
|
{
|
||||||
|
var breakTimes = scoreUnit.Questions
|
||||||
|
.SelectMany(q => q.Question.BreakTimes ?? [])
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return JsonSerializer.Serialize(breakTimes);
|
||||||
|
}
|
||||||
|
|
||||||
private static JournalPageTaskUserAnswer BuildAnswerEntity(
|
private static JournalPageTaskUserAnswer BuildAnswerEntity(
|
||||||
QuestionData data,
|
QuestionData data,
|
||||||
|
JournalScoreUnit scoreUnit,
|
||||||
Question question,
|
Question question,
|
||||||
JournalPageTask task,
|
JournalPageTask task,
|
||||||
JournalPage? page,
|
JournalPage? page,
|
||||||
@ -603,12 +931,12 @@ public class JournalTaskReceiveConsumer(
|
|||||||
Expression = Math.Max(0, scoreResult.Expression),
|
Expression = Math.Max(0, scoreResult.Expression),
|
||||||
Persuasiveness = Math.Max(0, scoreResult.Persuasiveness),
|
Persuasiveness = Math.Max(0, scoreResult.Persuasiveness),
|
||||||
QuestionAnswerUrl = question.Url,
|
QuestionAnswerUrl = question.Url,
|
||||||
AnswerUrl = JsonSerializer.Serialize(question.AnswerUrl ?? []),
|
AnswerUrl = SerializeAnswerUrls(scoreUnit),
|
||||||
PageAnswerUrl = data.PageAnswerUrl,
|
PageAnswerUrl = data.PageAnswerUrl,
|
||||||
Revision = 0,
|
Revision = 0,
|
||||||
AnswerStartTime = question.AnswerStartTime,
|
AnswerStartTime = GetAnswerStartTime(scoreUnit),
|
||||||
AnswerEndTime = question.AnswerEndTime,
|
AnswerEndTime = GetAnswerEndTime(scoreUnit),
|
||||||
AnswerSeconds = question.AnswerTime,
|
AnswerSeconds = GetAnswerSeconds(scoreUnit),
|
||||||
ImageRecognition = 0,
|
ImageRecognition = 0,
|
||||||
JournalPageNum = page?.PageNum ?? 0,
|
JournalPageNum = page?.PageNum ?? 0,
|
||||||
Modify = 0,
|
Modify = 0,
|
||||||
@ -618,8 +946,8 @@ public class JournalTaskReceiveConsumer(
|
|||||||
Type = task.Type.ToString(),
|
Type = task.Type.ToString(),
|
||||||
DotPageNo = page?.PageNo ?? string.Empty,
|
DotPageNo = page?.PageNo ?? string.Empty,
|
||||||
PageAnswerDotUrl = string.Empty,
|
PageAnswerDotUrl = string.Empty,
|
||||||
BreakCount = question.BreakCount,
|
BreakCount = GetBreakCount(scoreUnit),
|
||||||
BreakTimes = JsonSerializer.Serialize(question.BreakTimes ?? []),
|
BreakTimes = SerializeBreakTimes(scoreUnit),
|
||||||
AssignmentStatus = answerStatus.ToString(),
|
AssignmentStatus = answerStatus.ToString(),
|
||||||
Status = (int)answerStatus,
|
Status = (int)answerStatus,
|
||||||
CreatedBy = data.UserId.ToString(),
|
CreatedBy = data.UserId.ToString(),
|
||||||
@ -766,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>
|
||||||
@ -790,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>
|
||||||
@ -911,4 +1270,18 @@ public record JournalAnswerContext(
|
|||||||
JournalPageTask Task,
|
JournalPageTask Task,
|
||||||
JournalAnswerScoreResult ScoreResult);
|
JournalAnswerScoreResult ScoreResult);
|
||||||
|
|
||||||
|
public record JournalQuestionContext(
|
||||||
|
Question Question,
|
||||||
|
JournalPageTask Task,
|
||||||
|
JournalPage? Page);
|
||||||
|
|
||||||
|
public record JournalScoreUnit(
|
||||||
|
long GroupId,
|
||||||
|
List<JournalQuestionContext> Questions);
|
||||||
|
|
||||||
public record AnswerImageContent(string DataUrl);
|
public record AnswerImageContent(string DataUrl);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// AI评分不可重试异常。
|
||||||
|
/// </summary>
|
||||||
|
public class NonRetryAiScoreException(string message) : Exception(message);
|
||||||
|
|||||||
@ -0,0 +1,129 @@
|
|||||||
|
using QYZH.InteractiveMagazine.Common.Helpers;
|
||||||
|
using QYZH.InteractiveMagazine.Infrastructure.OSS;
|
||||||
|
using QYZH.InteractiveMagazine.Models.Common;
|
||||||
|
using QYZH.InteractiveMagazine.Models.Dto;
|
||||||
|
using QYZH.InteractiveMagazine.Models.Entity;
|
||||||
|
using QYZH.InteractiveMagazine.Models.Enum;
|
||||||
|
using SqlSugar;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace QYZH.InteractiveMagazine.WorkService.Consumers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 期刊二维码生成消费者
|
||||||
|
/// </summary>
|
||||||
|
public class UserJournalQrCodeGenerateConsumer(
|
||||||
|
ILogger<UserJournalQrCodeGenerateConsumer> logger,
|
||||||
|
IServiceScopeFactory scopeFactory,
|
||||||
|
OssService ossService) : IQueueConsumer
|
||||||
|
{
|
||||||
|
public string Exchange => "ex.journal";
|
||||||
|
|
||||||
|
public string QueueName => "mq.journal.qrcode.generate";
|
||||||
|
|
||||||
|
public string RoutingKey => "rk.journal.qrcode.generate";
|
||||||
|
|
||||||
|
public async Task HandleAsync(byte[] message, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var content = Encoding.UTF8.GetString(message);
|
||||||
|
logger.LogInformation("收到期刊二维码生成消息: {Message}", content);
|
||||||
|
|
||||||
|
var data = JsonSerializer.Deserialize<GenerateUserJournalQrCodeMessage>(content, new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
PropertyNameCaseInsensitive = true
|
||||||
|
}) ?? throw new InvalidOperationException("期刊二维码生成消息为空");
|
||||||
|
|
||||||
|
if (data.RecordIds.Count == 0)
|
||||||
|
{
|
||||||
|
logger.LogWarning("期刊二维码生成消息缺少记录Id");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var scope = scopeFactory.CreateScope();
|
||||||
|
var client = scope.ServiceProvider.GetRequiredService<ISqlSugarClient>();
|
||||||
|
var operatorName = string.IsNullOrWhiteSpace(data.OperatorName) ? "System" : data.OperatorName;
|
||||||
|
|
||||||
|
foreach (var recordId in data.RecordIds.Distinct())
|
||||||
|
{
|
||||||
|
await GenerateQrCodeImageAsync(client, recordId, operatorName, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task OnErrorAsync(byte[] message, Exception exception)
|
||||||
|
{
|
||||||
|
logger.LogError(exception, "处理期刊二维码生成消息失败: {Message}", Encoding.UTF8.GetString(message));
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var data = JsonSerializer.Deserialize<GenerateUserJournalQrCodeMessage>(Encoding.UTF8.GetString(message), new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
PropertyNameCaseInsensitive = true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data?.RecordIds.Count > 0)
|
||||||
|
{
|
||||||
|
using var scope = scopeFactory.CreateScope();
|
||||||
|
var client = scope.ServiceProvider.GetRequiredService<ISqlSugarClient>();
|
||||||
|
var operatorName = string.IsNullOrWhiteSpace(data.OperatorName) ? "System" : data.OperatorName;
|
||||||
|
await client.Updateable<UserJournal>()
|
||||||
|
.SetColumns(uj => uj.Status == (int)UserJournalStatusEnum.Failed)
|
||||||
|
.SetColumns(uj => uj.UpdatedBy == operatorName)
|
||||||
|
.SetColumns(uj => uj.UpdatedAt == DateTime.Now)
|
||||||
|
.Where(uj => data.RecordIds.Contains(uj.Id) && !uj.IsDeleted && uj.Status == (int)UserJournalStatusEnum.Generating)
|
||||||
|
.ExecuteCommandAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "标记期刊二维码生成失败状态异常");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task GenerateQrCodeImageAsync(ISqlSugarClient client, long recordId, string operatorName, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var record = await client.Queryable<UserJournal>()
|
||||||
|
.Where(uj => uj.Id == recordId && !uj.IsDeleted)
|
||||||
|
.FirstAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (record == null || record.Status != (int)UserJournalStatusEnum.Generating)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var qrCodeContent = BuildQrCodeContent(record.JournalId, record.Id);
|
||||||
|
var qrCodeKey = $"journal/qrcode/{record.JournalId}/{record.Id}.png";
|
||||||
|
using var qrCodeStream = new MemoryStream(QrCodeHelper.GeneratePng(qrCodeContent));
|
||||||
|
var uploadedKey = ossService.PutObject(qrCodeKey, qrCodeStream);
|
||||||
|
if (string.IsNullOrWhiteSpace(uploadedKey))
|
||||||
|
{
|
||||||
|
throw new BusinessException("二维码图片上传失败", ResultCode.GLOBAL_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.Updateable<UserJournal>()
|
||||||
|
.SetColumns(uj => uj.QrCodeUrl == uploadedKey)
|
||||||
|
.SetColumns(uj => uj.Status == (int)UserJournalStatusEnum.Active)
|
||||||
|
.SetColumns(uj => uj.UpdatedBy == operatorName)
|
||||||
|
.SetColumns(uj => uj.UpdatedAt == DateTime.Now)
|
||||||
|
.Where(uj => uj.Id == recordId && !uj.IsDeleted && uj.Status == (int)UserJournalStatusEnum.Generating)
|
||||||
|
.ExecuteCommandAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "生成期刊二维码失败,RecordId: {RecordId}", recordId);
|
||||||
|
await client.Updateable<UserJournal>()
|
||||||
|
.SetColumns(uj => uj.Status == (int)UserJournalStatusEnum.Failed)
|
||||||
|
.SetColumns(uj => uj.UpdatedBy == operatorName)
|
||||||
|
.SetColumns(uj => uj.UpdatedAt == DateTime.Now)
|
||||||
|
.Where(uj => uj.Id == recordId && !uj.IsDeleted && uj.Status == (int)UserJournalStatusEnum.Generating)
|
||||||
|
.ExecuteCommandAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildQrCodeContent(long journalId, long id)
|
||||||
|
{
|
||||||
|
return JsonSerializer.Serialize(new { JournalId = journalId, Id = id });
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -77,6 +77,7 @@ builder.Services.AddRabbitMQ(builder.Configuration);
|
|||||||
|
|
||||||
// 注册队列消费者(新增消费者只需实现 IQueueConsumer 并在此注册)
|
// 注册队列消费者(新增消费者只需实现 IQueueConsumer 并在此注册)
|
||||||
builder.Services.AddScoped<IQueueConsumer, JournalTaskReceiveConsumer>();
|
builder.Services.AddScoped<IQueueConsumer, JournalTaskReceiveConsumer>();
|
||||||
|
builder.Services.AddScoped<IQueueConsumer, UserJournalQrCodeGenerateConsumer>();
|
||||||
|
|
||||||
// 注册消费者后台服务
|
// 注册消费者后台服务
|
||||||
builder.Services.AddHostedService<RabbitMQHostedService>();
|
builder.Services.AddHostedService<RabbitMQHostedService>();
|
||||||
|
|||||||
@ -22,4 +22,10 @@
|
|||||||
<PackageReference Include="Yitter.IdGenerator" Version="1.0.14" />
|
<PackageReference Include="Yitter.IdGenerator" Version="1.0.14" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Update="Dockerfile">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@ -12,7 +12,8 @@
|
|||||||
"Port": 5672,
|
"Port": 5672,
|
||||||
"UserName": "smartschool",
|
"UserName": "smartschool",
|
||||||
"Password": "@ss%&*otz%d*pq2S",
|
"Password": "@ss%&*otz%d*pq2S",
|
||||||
"VirtualHost": "InteractiveMagazine"
|
"VirtualHost": "InteractiveMagazine",
|
||||||
|
"PrefetchCount": 1
|
||||||
},
|
},
|
||||||
"Serilog": {
|
"Serilog": {
|
||||||
"MinimumLevel": {
|
"MinimumLevel": {
|
||||||
@ -55,7 +56,8 @@
|
|||||||
"Model": "qwen2.5vl:7b",
|
"Model": "qwen2.5vl:7b",
|
||||||
"TimeoutSeconds": 300,
|
"TimeoutSeconds": 300,
|
||||||
"MaxTokens": 2000,
|
"MaxTokens": 2000,
|
||||||
"Temperature": 0.5,
|
"Temperature": 0.1,
|
||||||
|
"MaxConcurrency": 1,
|
||||||
"ScoreMaxRetryCount": 3,
|
"ScoreMaxRetryCount": 3,
|
||||||
"ScoreRetryDelayMilliseconds": 1000,
|
"ScoreRetryDelayMilliseconds": 1000,
|
||||||
"MaxImageBytes": 10485760,
|
"MaxImageBytes": 10485760,
|
||||||
|
|||||||
13
QYZH.InteractiveMagazine.WorkService/dotnet-tools.json
Normal file
13
QYZH.InteractiveMagazine.WorkService/dotnet-tools.json
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"isRoot": true,
|
||||||
|
"tools": {
|
||||||
|
"dotnet-ef": {
|
||||||
|
"version": "10.0.9",
|
||||||
|
"commands": [
|
||||||
|
"dotnet-ef"
|
||||||
|
],
|
||||||
|
"rollForward": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user