Compare commits

..

15 Commits

Author SHA1 Message Date
glz
cbdee5068a feat: 新增消息Outbox机制、雪花ID配置优化及多项功能完善
1.  新增数据库唯一约束和Message_Outbox表脚本
2.  新增雪花ID、Hangfire存储、MQ重试等配置实体
3.  重构各项目雪花ID生成逻辑,改为从配置读取WorkerId
4.  优化积分服务分页查询、用户背包更新逻辑
5.  新增JWT令牌Redis过期刷新逻辑
6.  完善RabbitMQ死信队列消息头信息
7.  新增可靠MQ消息发布服务和Outbox派发后台服务
8.  替换原有RabbitMQ直接发送为Outbox可靠发布
9.  优化签到服务逻辑,新增重复签到校验和补签卡扣减逻辑
10. 修复自动铺码消费逻辑,新增点阵页预占和释放机制
2026-07-10 10:44:00 +08:00
glz
685a8aeaec Merge branch 'master' of http://8.137.159.94:3000/glz/QYZH.InteractiveMagazine 2026-07-09 18:01:41 +08:00
glz
766be485d0 feat: 新增操作日志功能并优化定时任务配置
1.  新增OperationLogAttribute与OperationLogActionFilter,实现自动化操作日志记录
2.  新增OperationLogRecordInput输入模型,重构IOperationLogService日志接口
3.  为所有业务控制器接口添加操作日志注解
4.  调整期刊AI批改任务的执行周期与方法适配异步调用
5.  优化Hangfire定时任务注册逻辑,支持异步任务
6.  补充完善操作日志类型与目标类型枚举
2026-07-09 18:01:37 +08:00
faf40d6e27 去掉调试使用的UerId 2026-07-09 15:44:05 +08:00
57e40440ab 解决统计总成长值和积分问题 2026-07-09 15:24:49 +08:00
glz
4bb03aa0df refactor(workservice): 迁移AI批改逻辑到定时任务,移除消费端评分逻辑
1. 新增JournalTaskAiScoreJob定时任务,实现周期性扫描处理期刊答题记录AI批改
2. 调整appsettings.json配置,新增定时任务配置与相关参数
3. 重构JournalTaskReceiveConsumer,移除原有的AI评分相关代码,仅保留答题记录保存逻辑
4. 优化任务查询条件,支持分组任务查询
2026-07-08 18:00:44 +08:00
glz
de9a0c2978 refactor(work-service,print-worker): 优化代码结构与错误处理
1. 提取评分prompt构建逻辑减少重复代码
2. 移除冗余的参考答案图片循环逻辑
3. 重构打印服务的错误回调逻辑,统一失败处理流程
4. 新增打印服务的任务取消处理逻辑
2026-07-08 17:07:41 +08:00
glz
195792ec66 refactor: 统一数据库表名前缀,按业务模块拆分表名
对所有实体类的SugarTable特性以及菜单初始化SQL中的表名进行统一修改,将原有的零散表名调整为带业务前缀的规范命名,例如将系统管理相关表加上System_前缀、内容相关表加上Content_前缀、商城相关表加上Mall_前缀等,统一数据库表命名规范
2026-07-08 10:37:03 +08:00
glz
a8b19cbacc feat(admin): add admin role type management
1. 新增后台角色类型枚举AdminRoleTypeEnum
2. 为AdminRole实体添加角色类型字段并设置默认值
3. 新增/更新管理员角色相关DTO的角色类型属性
4. 完善权限服务中的角色增删改查逻辑,增加系统角色保护校验
5. 修复任务消费中角色类型存储为枚举字符串的问题,改为存储枚举值
6. 新增初始化角色类型的SQL脚本
2026-07-08 09:55:17 +08:00
glz
a64d1b459a refactor(JournalTaskConsumer): 优化AI评分prompt与图片加载逻辑
1. 新增无效作答固定返回文案常量
2. 调整prompt提示词,补充无效作答评分规则与返回要求
3. 移除冗余的参考答案无结果提示与题目基础信息打印
4. 封装图片流获取方法,兼容OSS和HTTP链接
5. 新增结果标准化处理,统一无效作答返回格式
2026-07-08 09:13:48 +08:00
glz
f7f505d109 refactor(api): 提取公共API默认配置并修复积分并发问题
1.  新增InteractiveMagazineApiDefaultsExtensions统一注册共享服务,简化WeChatApi和WebApi的Program.cs代码
2.  修复积分增减的并发问题,改用原子更新操作,添加余额校验避免超扣
3.  修复注释乱码问题,将简体中文注释替换为正确文本
4.  新增.editorconfig配置统一代码格式
2026-07-07 18:01:17 +08:00
glz
1d467a5fc4 Merge branch 'master' of http://8.137.159.94:3000/glz/QYZH.InteractiveMagazine 2026-07-07 15:53:29 +08:00
glz
24059dfebb feat: 新增轮播图和资料管理模块
此提交完整实现了小程序端和管理后台的轮播图、资料管理功能:
1. 新增3个枚举类型:轮播图展示位置、跳转类型、资料文件类型
2. 新增数据库表结构SQL脚本,包含Banner、Material、MaterialFile表
3. 新增实体类定义,适配SqlSugar ORM
4. 新增服务接口与实现,包含CRUD、状态切换、列表查询等完整业务逻辑
5. 新增管理后台API控制器和小程序端API控制器
6. 新增数据传输对象,包含输入、输出、查询参数等类型
7. 实现了OSS临时文件迁移、参数校验、分页查询、软删除等基础功能
2026-07-07 15:53:25 +08:00
edc0122a3b 统一AssignmentStatus字段类型,新建枚举,对应状态 2026-07-07 10:36:14 +08:00
593f95ffaa 这个是启动自动生成的文件 2026-07-07 10:22:47 +08:00
122 changed files with 4524 additions and 1238 deletions

10
.editorconfig Normal file
View File

@ -0,0 +1,10 @@
root = true
[*]
charset = utf-8
end_of_line = crlf
insert_final_newline = true
[*.cs]
indent_style = space
indent_size = 4

View File

@ -4,122 +4,122 @@ SET @shortSnowflakeEpoch = 1577836800000;
SET @shortSnowflakeTimestamp = CAST(UNIX_TIMESTAMP(CURRENT_TIMESTAMP(3)) * 1000 AS UNSIGNED) - @shortSnowflakeEpoch;
SET @shortSnowflakeSequence = -1;
SET @ai_config_id = COALESCE((SELECT `Id` FROM `AdminMenu` WHERE `Code` = 'ai-config' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
INSERT INTO `AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
SET @ai_config_id = COALESCE((SELECT `Id` FROM `System_AdminMenu` WHERE `Code` = 'ai-config' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
INSERT INTO `System_AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
VALUES (@ai_config_id, 0, 'AI配置中心', 'ai-config', '/ai-config', 'layout.base', 'ri:robot-line', 0, b'1', 1, b'0', @createdBy, @now, @createdBy, @now)
ON DUPLICATE KEY UPDATE `ParentId` = VALUES(`ParentId`), `Name` = VALUES(`Name`), `Path` = VALUES(`Path`), `Component` = VALUES(`Component`), `Icon` = VALUES(`Icon`), `Sort` = VALUES(`Sort`), `IsVisible` = VALUES(`IsVisible`), `Status` = VALUES(`Status`), `IsDeleted` = b'0', `UpdatedBy` = @createdBy, `UpdatedAt` = @now;
SET @ai_config_base_prompt_id = COALESCE((SELECT `Id` FROM `AdminMenu` WHERE `Code` = 'ai-config:base-prompt' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
INSERT INTO `AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
SET @ai_config_base_prompt_id = COALESCE((SELECT `Id` FROM `System_AdminMenu` WHERE `Code` = 'ai-config:base-prompt' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
INSERT INTO `System_AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
VALUES (@ai_config_base_prompt_id, @ai_config_id, '基础提示词配置', 'ai-config:base-prompt', '/ai-config/base-prompt', 'view.ai-config_base-prompt', 'ri:file-text-line', 0, b'1', 1, b'0', @createdBy, @now, @createdBy, @now)
ON DUPLICATE KEY UPDATE `ParentId` = VALUES(`ParentId`), `Name` = VALUES(`Name`), `Path` = VALUES(`Path`), `Component` = VALUES(`Component`), `Icon` = VALUES(`Icon`), `Sort` = VALUES(`Sort`), `IsVisible` = VALUES(`IsVisible`), `Status` = VALUES(`Status`), `IsDeleted` = b'0', `UpdatedBy` = @createdBy, `UpdatedAt` = @now;
SET @main_nav_id = COALESCE((SELECT `Id` FROM `AdminMenu` WHERE `Code` = 'main-nav' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
INSERT INTO `AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
SET @main_nav_id = COALESCE((SELECT `Id` FROM `System_AdminMenu` WHERE `Code` = 'main-nav' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
INSERT INTO `System_AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
VALUES (@main_nav_id, 0, '主导航', 'main-nav', '/main-nav', 'layout.base', 'ri:organization-chart', 1, b'1', 1, b'0', @createdBy, @now, @createdBy, @now)
ON DUPLICATE KEY UPDATE `ParentId` = VALUES(`ParentId`), `Name` = VALUES(`Name`), `Path` = VALUES(`Path`), `Component` = VALUES(`Component`), `Icon` = VALUES(`Icon`), `Sort` = VALUES(`Sort`), `IsVisible` = VALUES(`IsVisible`), `Status` = VALUES(`Status`), `IsDeleted` = b'0', `UpdatedBy` = @createdBy, `UpdatedAt` = @now;
SET @main_nav_home_id = COALESCE((SELECT `Id` FROM `AdminMenu` WHERE `Code` = 'main-nav:home' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
INSERT INTO `AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
SET @main_nav_home_id = COALESCE((SELECT `Id` FROM `System_AdminMenu` WHERE `Code` = 'main-nav:home' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
INSERT INTO `System_AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
VALUES (@main_nav_home_id, @main_nav_id, '仪表盘', 'main-nav:home', '/main-nav/home', 'view.main-nav_home', 'ri:home-2-line', 1, b'1', 1, b'0', @createdBy, @now, @createdBy, @now)
ON DUPLICATE KEY UPDATE `ParentId` = VALUES(`ParentId`), `Name` = VALUES(`Name`), `Path` = VALUES(`Path`), `Component` = VALUES(`Component`), `Icon` = VALUES(`Icon`), `Sort` = VALUES(`Sort`), `IsVisible` = VALUES(`IsVisible`), `Status` = VALUES(`Status`), `IsDeleted` = b'0', `UpdatedBy` = @createdBy, `UpdatedAt` = @now;
SET @main_nav_journal_qr_code_id = COALESCE((SELECT `Id` FROM `AdminMenu` WHERE `Code` = 'main-nav:journal-qr-code' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
INSERT INTO `AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
SET @main_nav_journal_qr_code_id = COALESCE((SELECT `Id` FROM `System_AdminMenu` WHERE `Code` = 'main-nav:journal-qr-code' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
INSERT INTO `System_AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
VALUES (@main_nav_journal_qr_code_id, @main_nav_id, '期刊二维码管理', 'main-nav:journal-qr-code', '/main-nav/journal-qr-code', 'view.main-nav_journal-qr-code', 'ri:qr-code-line', 2, b'1', 1, b'0', @createdBy, @now, @createdBy, @now)
ON DUPLICATE KEY UPDATE `ParentId` = VALUES(`ParentId`), `Name` = VALUES(`Name`), `Path` = VALUES(`Path`), `Component` = VALUES(`Component`), `Icon` = VALUES(`Icon`), `Sort` = VALUES(`Sort`), `IsVisible` = VALUES(`IsVisible`), `Status` = VALUES(`Status`), `IsDeleted` = b'0', `UpdatedBy` = @createdBy, `UpdatedAt` = @now;
SET @main_nav_book_management_id = COALESCE((SELECT `Id` FROM `AdminMenu` WHERE `Code` = 'main-nav:book-management' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
INSERT INTO `AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
SET @main_nav_book_management_id = COALESCE((SELECT `Id` FROM `System_AdminMenu` WHERE `Code` = 'main-nav:book-management' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
INSERT INTO `System_AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
VALUES (@main_nav_book_management_id, @main_nav_id, '书籍管理', 'main-nav:book-management', '/main-nav/book-management', 'ayout.base', 'ri:book-open-line', 5, b'1', 1, b'0', @createdBy, @now, @createdBy, @now)
ON DUPLICATE KEY UPDATE `ParentId` = VALUES(`ParentId`), `Name` = VALUES(`Name`), `Path` = VALUES(`Path`), `Component` = VALUES(`Component`), `Icon` = VALUES(`Icon`), `Sort` = VALUES(`Sort`), `IsVisible` = VALUES(`IsVisible`), `Status` = VALUES(`Status`), `IsDeleted` = b'0', `UpdatedBy` = @createdBy, `UpdatedAt` = @now;
SET @main_nav_community_message_management_id = COALESCE((SELECT `Id` FROM `AdminMenu` WHERE `Code` = 'main-nav:community-message-management' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
INSERT INTO `AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
SET @main_nav_community_message_management_id = COALESCE((SELECT `Id` FROM `System_AdminMenu` WHERE `Code` = 'main-nav:community-message-management' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
INSERT INTO `System_AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
VALUES (@main_nav_community_message_management_id, @main_nav_id, '社区消息管理', 'main-nav:community-message-management', '/main-nav/community-message-management', 'view.main-nav_community-message-management', 'ic:baseline-mark-unread-chat-alt', 6, b'1', 1, b'0', @createdBy, @now, @createdBy, @now)
ON DUPLICATE KEY UPDATE `ParentId` = VALUES(`ParentId`), `Name` = VALUES(`Name`), `Path` = VALUES(`Path`), `Component` = VALUES(`Component`), `Icon` = VALUES(`Icon`), `Sort` = VALUES(`Sort`), `IsVisible` = VALUES(`IsVisible`), `Status` = VALUES(`Status`), `IsDeleted` = b'0', `UpdatedBy` = @createdBy, `UpdatedAt` = @now;
SET @main_nav_medal_management_id = COALESCE((SELECT `Id` FROM `AdminMenu` WHERE `Code` = 'main-nav:medal-management' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
INSERT INTO `AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
SET @main_nav_medal_management_id = COALESCE((SELECT `Id` FROM `System_AdminMenu` WHERE `Code` = 'main-nav:medal-management' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
INSERT INTO `System_AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
VALUES (@main_nav_medal_management_id, @main_nav_id, '勋章管理', 'main-nav:medal-management', '/main-nav/medal-management', 'view.main-nav_medal-management', 'ri:medal-line', 7, b'1', 1, b'0', @createdBy, @now, @createdBy, @now)
ON DUPLICATE KEY UPDATE `ParentId` = VALUES(`ParentId`), `Name` = VALUES(`Name`), `Path` = VALUES(`Path`), `Component` = VALUES(`Component`), `Icon` = VALUES(`Icon`), `Sort` = VALUES(`Sort`), `IsVisible` = VALUES(`IsVisible`), `Status` = VALUES(`Status`), `IsDeleted` = b'0', `UpdatedBy` = @createdBy, `UpdatedAt` = @now;
SET @main_nav_pet_management_id = COALESCE((SELECT `Id` FROM `AdminMenu` WHERE `Code` = 'main-nav:pet-management' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
INSERT INTO `AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
SET @main_nav_pet_management_id = COALESCE((SELECT `Id` FROM `System_AdminMenu` WHERE `Code` = 'main-nav:pet-management' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
INSERT INTO `System_AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
VALUES (@main_nav_pet_management_id, @main_nav_id, '宠物管理', 'main-nav:pet-management', '/main-nav/pet-management', 'view.main-nav_pet-management', 'mdi:paw', 8, b'1', 1, b'0', @createdBy, @now, @createdBy, @now)
ON DUPLICATE KEY UPDATE `ParentId` = VALUES(`ParentId`), `Name` = VALUES(`Name`), `Path` = VALUES(`Path`), `Component` = VALUES(`Component`), `Icon` = VALUES(`Icon`), `Sort` = VALUES(`Sort`), `IsVisible` = VALUES(`IsVisible`), `Status` = VALUES(`Status`), `IsDeleted` = b'0', `UpdatedBy` = @createdBy, `UpdatedAt` = @now;
SET @main_nav_product_management_id = COALESCE((SELECT `Id` FROM `AdminMenu` WHERE `Code` = 'main-nav:product-management' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
INSERT INTO `AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
SET @main_nav_product_management_id = COALESCE((SELECT `Id` FROM `System_AdminMenu` WHERE `Code` = 'main-nav:product-management' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
INSERT INTO `System_AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
VALUES (@main_nav_product_management_id, @main_nav_id, '商品管理', 'main-nav:product-management', '/main-nav/product-management', 'view.main-nav_product-management', 'icon-park-solid:health-products', 9, b'1', 1, b'0', @createdBy, @now, @createdBy, @now)
ON DUPLICATE KEY UPDATE `ParentId` = VALUES(`ParentId`), `Name` = VALUES(`Name`), `Path` = VALUES(`Path`), `Component` = VALUES(`Component`), `Icon` = VALUES(`Icon`), `Sort` = VALUES(`Sort`), `IsVisible` = VALUES(`IsVisible`), `Status` = VALUES(`Status`), `IsDeleted` = b'0', `UpdatedBy` = @createdBy, `UpdatedAt` = @now;
SET @main_nav_check_in_config_management_id = COALESCE((SELECT `Id` FROM `AdminMenu` WHERE `Code` = 'main-nav:check-in-config-management' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
INSERT INTO `AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
SET @main_nav_check_in_config_management_id = COALESCE((SELECT `Id` FROM `System_AdminMenu` WHERE `Code` = 'main-nav:check-in-config-management' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
INSERT INTO `System_AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
VALUES (@main_nav_check_in_config_management_id, @main_nav_id, '签到配置管理', 'main-nav:check-in-config-management', '/main-nav/check-in-config-management', 'view.main-nav_check-in-config-management', 'ri:check-line', 10, b'1', 1, b'0', @createdBy, @now, @createdBy, @now)
ON DUPLICATE KEY UPDATE `ParentId` = VALUES(`ParentId`), `Name` = VALUES(`Name`), `Path` = VALUES(`Path`), `Component` = VALUES(`Component`), `Icon` = VALUES(`Icon`), `Sort` = VALUES(`Sort`), `IsVisible` = VALUES(`IsVisible`), `Status` = VALUES(`Status`), `IsDeleted` = b'0', `UpdatedBy` = @createdBy, `UpdatedAt` = @now;
SET @data_statistics_id = COALESCE((SELECT `Id` FROM `AdminMenu` WHERE `Code` = 'data-statistics' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
INSERT INTO `AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
SET @data_statistics_id = COALESCE((SELECT `Id` FROM `System_AdminMenu` WHERE `Code` = 'data-statistics' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
INSERT INTO `System_AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
VALUES (@data_statistics_id, 0, '数据统计', 'data-statistics', '/data-statistics', 'layout.base', 'ri:bar-chart-2-line', 2, b'1', 1, b'0', @createdBy, @now, @createdBy, @now)
ON DUPLICATE KEY UPDATE `ParentId` = VALUES(`ParentId`), `Name` = VALUES(`Name`), `Path` = VALUES(`Path`), `Component` = VALUES(`Component`), `Icon` = VALUES(`Icon`), `Sort` = VALUES(`Sort`), `IsVisible` = VALUES(`IsVisible`), `Status` = VALUES(`Status`), `IsDeleted` = b'0', `UpdatedBy` = @createdBy, `UpdatedAt` = @now;
SET @data_statistics_statistics_id = COALESCE((SELECT `Id` FROM `AdminMenu` WHERE `Code` = 'data-statistics:statistics' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
INSERT INTO `AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
SET @data_statistics_statistics_id = COALESCE((SELECT `Id` FROM `System_AdminMenu` WHERE `Code` = 'data-statistics:statistics' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
INSERT INTO `System_AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
VALUES (@data_statistics_statistics_id, @data_statistics_id, '数据统计', 'data-statistics:statistics', '/data-statistics/statistics', 'view.data-statistics_statistics', 'ri:bar-chart-2-line', 0, b'1', 1, b'0', @createdBy, @now, @createdBy, @now)
ON DUPLICATE KEY UPDATE `ParentId` = VALUES(`ParentId`), `Name` = VALUES(`Name`), `Path` = VALUES(`Path`), `Component` = VALUES(`Component`), `Icon` = VALUES(`Icon`), `Sort` = VALUES(`Sort`), `IsVisible` = VALUES(`IsVisible`), `Status` = VALUES(`Status`), `IsDeleted` = b'0', `UpdatedBy` = @createdBy, `UpdatedAt` = @now;
SET @system_manage_id = COALESCE((SELECT `Id` FROM `AdminMenu` WHERE `Code` = 'system-manage' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
INSERT INTO `AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
SET @system_manage_id = COALESCE((SELECT `Id` FROM `System_AdminMenu` WHERE `Code` = 'system-manage' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
INSERT INTO `System_AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
VALUES (@system_manage_id, 0, '系统管理', 'system-manage', '/system-manage', 'layout.base', 'ri:settings-2-line', 3, b'1', 1, b'0', @createdBy, @now, @createdBy, @now)
ON DUPLICATE KEY UPDATE `ParentId` = VALUES(`ParentId`), `Name` = VALUES(`Name`), `Path` = VALUES(`Path`), `Component` = VALUES(`Component`), `Icon` = VALUES(`Icon`), `Sort` = VALUES(`Sort`), `IsVisible` = VALUES(`IsVisible`), `Status` = VALUES(`Status`), `IsDeleted` = b'0', `UpdatedBy` = @createdBy, `UpdatedAt` = @now;
SET @system_manage_menu_id = COALESCE((SELECT `Id` FROM `AdminMenu` WHERE `Code` = 'system-manage:menu' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
INSERT INTO `AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
SET @system_manage_menu_id = COALESCE((SELECT `Id` FROM `System_AdminMenu` WHERE `Code` = 'system-manage:menu' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
INSERT INTO `System_AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
VALUES (@system_manage_menu_id, @system_manage_id, '菜单管理', 'system-manage:menu', '/system-manage/menu-manage', 'view.system-manage_menu-manage', 'mingcute:list-collapse-fill', 3, b'1', 1, b'0', @createdBy, @now, @createdBy, @now)
ON DUPLICATE KEY UPDATE `ParentId` = VALUES(`ParentId`), `Name` = VALUES(`Name`), `Path` = VALUES(`Path`), `Component` = VALUES(`Component`), `Icon` = VALUES(`Icon`), `Sort` = VALUES(`Sort`), `IsVisible` = VALUES(`IsVisible`), `Status` = VALUES(`Status`), `IsDeleted` = b'0', `UpdatedBy` = @createdBy, `UpdatedAt` = @now;
SET @system_manage_operation_log_id = COALESCE((SELECT `Id` FROM `AdminMenu` WHERE `Code` = 'system-manage:operation-log' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
INSERT INTO `AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
SET @system_manage_operation_log_id = COALESCE((SELECT `Id` FROM `System_AdminMenu` WHERE `Code` = 'system-manage:operation-log' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
INSERT INTO `System_AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
VALUES (@system_manage_operation_log_id, @system_manage_id, 'system-manage_operation-log-management', 'system-manage:operation-log', '/system-manage/operation-log-management', 'view.system-manage_operation-log-management', 'ri:file-list-3-line', 4, b'1', 1, b'0', @createdBy, @now, @createdBy, @now)
ON DUPLICATE KEY UPDATE `ParentId` = VALUES(`ParentId`), `Name` = VALUES(`Name`), `Path` = VALUES(`Path`), `Component` = VALUES(`Component`), `Icon` = VALUES(`Icon`), `Sort` = VALUES(`Sort`), `IsVisible` = VALUES(`IsVisible`), `Status` = VALUES(`Status`), `IsDeleted` = b'0', `UpdatedBy` = @createdBy, `UpdatedAt` = @now;
SET @system_manage_user_id = COALESCE((SELECT `Id` FROM `AdminMenu` WHERE `Code` = 'system-manage:user' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
INSERT INTO `AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
SET @system_manage_user_id = COALESCE((SELECT `Id` FROM `System_AdminMenu` WHERE `Code` = 'system-manage:user' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
INSERT INTO `System_AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
VALUES (@system_manage_user_id, @system_manage_id, 'admin管理', 'system-manage:user', '/system-manage/user-manage', 'view.system-manage_user-manage', 'ic:sharp-admin-panel-settings', 5, b'1', 1, b'0', @createdBy, @now, @createdBy, @now)
ON DUPLICATE KEY UPDATE `ParentId` = VALUES(`ParentId`), `Name` = VALUES(`Name`), `Path` = VALUES(`Path`), `Component` = VALUES(`Component`), `Icon` = VALUES(`Icon`), `Sort` = VALUES(`Sort`), `IsVisible` = VALUES(`IsVisible`), `Status` = VALUES(`Status`), `IsDeleted` = b'0', `UpdatedBy` = @createdBy, `UpdatedAt` = @now;
SET @system_manage_user_management_id = COALESCE((SELECT `Id` FROM `AdminMenu` WHERE `Code` = 'system-manage:user-management' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
INSERT INTO `AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
SET @system_manage_user_management_id = COALESCE((SELECT `Id` FROM `System_AdminMenu` WHERE `Code` = 'system-manage:user-management' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
INSERT INTO `System_AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
VALUES (@system_manage_user_management_id, @system_manage_id, '用户管理', 'system-manage:user-management', '/system-manage/users-management', 'view.system-manage_users-management', 'ri:user-2-line', 6, b'1', 1, b'0', @createdBy, @now, @createdBy, @now)
ON DUPLICATE KEY UPDATE `ParentId` = VALUES(`ParentId`), `Name` = VALUES(`Name`), `Path` = VALUES(`Path`), `Component` = VALUES(`Component`), `Icon` = VALUES(`Icon`), `Sort` = VALUES(`Sort`), `IsVisible` = VALUES(`IsVisible`), `Status` = VALUES(`Status`), `IsDeleted` = b'0', `UpdatedBy` = @createdBy, `UpdatedAt` = @now;
SET @help_center_id = COALESCE((SELECT `Id` FROM `AdminMenu` WHERE `Code` = 'help-center' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
INSERT INTO `AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
SET @help_center_id = COALESCE((SELECT `Id` FROM `System_AdminMenu` WHERE `Code` = 'help-center' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
INSERT INTO `System_AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
VALUES (@help_center_id, 0, '帮助中心', 'help-center', '/help-center', 'layout.base', 'ic:baseline-support-agent', 4, b'1', 1, b'0', @createdBy, @now, @createdBy, @now)
ON DUPLICATE KEY UPDATE `ParentId` = VALUES(`ParentId`), `Name` = VALUES(`Name`), `Path` = VALUES(`Path`), `Component` = VALUES(`Component`), `Icon` = VALUES(`Icon`), `Sort` = VALUES(`Sort`), `IsVisible` = VALUES(`IsVisible`), `Status` = VALUES(`Status`), `IsDeleted` = b'0', `UpdatedBy` = @createdBy, `UpdatedAt` = @now;
SET @help_center_contact_support_id = COALESCE((SELECT `Id` FROM `AdminMenu` WHERE `Code` = 'help-center:contact-support' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
INSERT INTO `AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
SET @help_center_contact_support_id = COALESCE((SELECT `Id` FROM `System_AdminMenu` WHERE `Code` = 'help-center:contact-support' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
INSERT INTO `System_AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
VALUES (@help_center_contact_support_id, @help_center_id, '联系客服', 'help-center:contact-support', '/help-center/contact-support', 'view.help-center_contact-support', 'ri:customer-service-2-line', 0, b'1', 1, b'0', @createdBy, @now, @createdBy, @now)
ON DUPLICATE KEY UPDATE `ParentId` = VALUES(`ParentId`), `Name` = VALUES(`Name`), `Path` = VALUES(`Path`), `Component` = VALUES(`Component`), `Icon` = VALUES(`Icon`), `Sort` = VALUES(`Sort`), `IsVisible` = VALUES(`IsVisible`), `Status` = VALUES(`Status`), `IsDeleted` = b'0', `UpdatedBy` = @createdBy, `UpdatedAt` = @now;
SET @help_center_usage_help_id = COALESCE((SELECT `Id` FROM `AdminMenu` WHERE `Code` = 'help-center:usage-help' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
INSERT INTO `AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
SET @help_center_usage_help_id = COALESCE((SELECT `Id` FROM `System_AdminMenu` WHERE `Code` = 'help-center:usage-help' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
INSERT INTO `System_AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
VALUES (@help_center_usage_help_id, @help_center_id, '使用帮助', 'help-center:usage-help', '/help-center/usage-help', 'view.help-center_usage-help', 'ic:baseline-support-agent', 1, b'1', 1, b'0', @createdBy, @now, @createdBy, @now)
ON DUPLICATE KEY UPDATE `ParentId` = VALUES(`ParentId`), `Name` = VALUES(`Name`), `Path` = VALUES(`Path`), `Component` = VALUES(`Component`), `Icon` = VALUES(`Icon`), `Sort` = VALUES(`Sort`), `IsVisible` = VALUES(`IsVisible`), `Status` = VALUES(`Status`), `IsDeleted` = b'0', `UpdatedBy` = @createdBy, `UpdatedAt` = @now;
SET @pen_management_id = COALESCE((SELECT `Id` FROM `AdminMenu` WHERE `Code` = 'pen-management' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
INSERT INTO `AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
SET @pen_management_id = COALESCE((SELECT `Id` FROM `System_AdminMenu` WHERE `Code` = 'pen-management' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
INSERT INTO `System_AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
VALUES (@pen_management_id, 0, '点阵笔管理', 'pen-management', '/pen-management', 'layout.base', 'ic:round-edit-off', 8, b'1', 1, b'0', @createdBy, @now, @createdBy, @now)
ON DUPLICATE KEY UPDATE `ParentId` = VALUES(`ParentId`), `Name` = VALUES(`Name`), `Path` = VALUES(`Path`), `Component` = VALUES(`Component`), `Icon` = VALUES(`Icon`), `Sort` = VALUES(`Sort`), `IsVisible` = VALUES(`IsVisible`), `Status` = VALUES(`Status`), `IsDeleted` = b'0', `UpdatedBy` = @createdBy, `UpdatedAt` = @now;
SET @pen_management_pen_list_id = COALESCE((SELECT `Id` FROM `AdminMenu` WHERE `Code` = 'pen-management:list' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
INSERT INTO `AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
SET @pen_management_pen_list_id = COALESCE((SELECT `Id` FROM `System_AdminMenu` WHERE `Code` = 'pen-management:list' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
INSERT INTO `System_AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
VALUES (@pen_management_pen_list_id, @pen_management_id, '点阵笔列表', 'pen-management:list', '/pen-management/pen-list', 'view.pen-management_pen-list', 'ri:bar-chart-2-line', 0, b'1', 1, b'0', @createdBy, @now, @createdBy, @now)
ON DUPLICATE KEY UPDATE `ParentId` = VALUES(`ParentId`), `Name` = VALUES(`Name`), `Path` = VALUES(`Path`), `Component` = VALUES(`Component`), `Icon` = VALUES(`Icon`), `Sort` = VALUES(`Sort`), `IsVisible` = VALUES(`IsVisible`), `Status` = VALUES(`Status`), `IsDeleted` = b'0', `UpdatedBy` = @createdBy, `UpdatedAt` = @now;
SET @pen_management_coordinate_data_id = COALESCE((SELECT `Id` FROM `AdminMenu` WHERE `Code` = 'pen-management:coordinate-data' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
INSERT INTO `AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
SET @pen_management_coordinate_data_id = COALESCE((SELECT `Id` FROM `System_AdminMenu` WHERE `Code` = 'pen-management:coordinate-data' LIMIT 1), (@shortSnowflakeTimestamp * 1000 + (@shortSnowflakeSequence := @shortSnowflakeSequence + 1)));
INSERT INTO `System_AdminMenu` (`Id`, `ParentId`, `Name`, `Code`, `Path`, `Component`, `Icon`, `Sort`, `IsVisible`, `Status`, `IsDeleted`, `CreatedBy`, `CreatedAt`, `UpdatedBy`, `UpdatedAt`)
VALUES (@pen_management_coordinate_data_id, @pen_management_id, '点阵笔坐标数据', 'pen-management:coordinate-data', '/pen-management/coordinate-data', 'view.pen-management_coordinate-data', 'ic:baseline-edit', 1, b'1', 1, b'0', @createdBy, @now, @createdBy, @now)
ON DUPLICATE KEY UPDATE `ParentId` = VALUES(`ParentId`), `Name` = VALUES(`Name`), `Path` = VALUES(`Path`), `Component` = VALUES(`Component`), `Icon` = VALUES(`Icon`), `Sort` = VALUES(`Sort`), `IsVisible` = VALUES(`IsVisible`), `Status` = VALUES(`Status`), `IsDeleted` = b'0', `UpdatedBy` = @createdBy, `UpdatedAt` = @now;

View File

@ -0,0 +1,22 @@
CREATE TABLE IF NOT EXISTS `Message_Outbox` (
`Id` bigint NOT NULL,
`Exchange` varchar(200) NOT NULL,
`Queue` varchar(200) NOT NULL,
`RoutingKey` varchar(200) NOT NULL,
`Payload` json NOT NULL,
`RetryCount` int NOT NULL DEFAULT 0,
`NextRetryAt` datetime NULL,
`SentAt` datetime NULL,
`LastError` varchar(2000) NULL,
`BusinessType` varchar(100) NOT NULL,
`BusinessId` bigint NOT NULL,
`Status` int NOT NULL DEFAULT 0,
`IsDeleted` bit NOT NULL DEFAULT b'0',
`CreatedBy` varchar(100) NOT NULL,
`CreatedAt` datetime NOT NULL,
`UpdatedBy` varchar(100) NULL,
`UpdatedAt` datetime NULL,
PRIMARY KEY (`Id`),
KEY `idx_message_outbox_status_next_retry` (`Status`, `NextRetryAt`, `CreatedAt`),
KEY `idx_message_outbox_business` (`BusinessType`, `BusinessId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

View File

@ -0,0 +1,5 @@
ALTER TABLE `CheckIn_Record`
ADD UNIQUE KEY `uk_checkin_record_user_date_type_deleted` (`UserId`, `CheckInDate`, `Type`, `IsDeleted`);
ALTER TABLE `User_Bag`
ADD UNIQUE KEY `uk_user_bag_available_item` (`UserId`, `ItemId`, `Status`, `IsDeleted`);

View File

@ -0,0 +1,61 @@
using QYZH.InteractiveMagazine.Models.Dto;
using QYZH.InteractiveMagazine.Models.Dto.Banner;
using QYZH.InteractiveMagazine.Models.Entity;
using QYZH.InteractiveMagazine.Models.Enum;
namespace QYZH.InteractiveMagazine.IService;
/// <summary>
/// 轮播图服务接口
/// </summary>
public interface IBannerService : IBaseService<Banner>
{
/// <summary>
/// 创建轮播图
/// </summary>
/// <param name="input">轮播图信息</param>
/// <returns>创建后的轮播图信息</returns>
Task<BannerOutput> CreateAsync(BannerInput input);
/// <summary>
/// 更新轮播图
/// </summary>
/// <param name="id">轮播图ID</param>
/// <param name="input">轮播图信息</param>
/// <returns>更新后的轮播图信息</returns>
Task<BannerOutput> UpdateAsync(long id, BannerInput input);
/// <summary>
/// 删除轮播图
/// </summary>
/// <param name="id">轮播图ID</param>
Task DeleteAsync(long id);
/// <summary>
/// 根据ID获取轮播图
/// </summary>
/// <param name="id">轮播图ID</param>
/// <returns>轮播图信息</returns>
Task<BannerOutput> GetByIdAsync(long id);
/// <summary>
/// 分页查询轮播图列表
/// </summary>
/// <param name="input">查询条件</param>
/// <returns>分页结果</returns>
Task<PageListModel<BannerOutput>> GetListAsync(BannerQueryInput input);
/// <summary>
/// 更新轮播图启用/禁用状态
/// </summary>
/// <param name="id">轮播图ID</param>
/// <returns>操作结果</returns>
Task<bool> UpdateStatusAsync(long id);
/// <summary>
/// 获取启用轮播图列表
/// </summary>
/// <param name="position">展示位置</param>
/// <returns>轮播图列表</returns>
Task<List<BannerOutput>> GetEnabledListAsync(BannerPositionEnum position);
}

View File

@ -0,0 +1,59 @@
using QYZH.InteractiveMagazine.Models.Dto;
using QYZH.InteractiveMagazine.Models.Dto.Material;
using MaterialEntity = QYZH.InteractiveMagazine.Models.Entity.Material;
namespace QYZH.InteractiveMagazine.IService;
/// <summary>
/// 资料服务接口
/// </summary>
public interface IMaterialService : IBaseService<MaterialEntity>
{
/// <summary>
/// 创建资料
/// </summary>
/// <param name="input">资料信息</param>
/// <returns>创建后的资料信息</returns>
Task<MaterialOutput> CreateAsync(MaterialInput input);
/// <summary>
/// 更新资料
/// </summary>
/// <param name="id">资料ID</param>
/// <param name="input">资料信息</param>
/// <returns>更新后的资料信息</returns>
Task<MaterialOutput> UpdateAsync(long id, MaterialInput input);
/// <summary>
/// 删除资料
/// </summary>
/// <param name="id">资料ID</param>
Task DeleteAsync(long id);
/// <summary>
/// 根据ID获取资料
/// </summary>
/// <param name="id">资料ID</param>
/// <returns>资料信息</returns>
Task<MaterialOutput> GetByIdAsync(long id);
/// <summary>
/// 分页查询资料列表
/// </summary>
/// <param name="input">查询条件</param>
/// <returns>分页结果</returns>
Task<PageListModel<MaterialOutput>> GetListAsync(MaterialQueryInput input);
/// <summary>
/// 更新资料启用/禁用状态
/// </summary>
/// <param name="id">资料ID</param>
/// <returns>操作结果</returns>
Task<bool> UpdateStatusAsync(long id);
/// <summary>
/// 获取小程序可见资料列表
/// </summary>
/// <returns>资料列表</returns>
Task<List<MaterialOutput>> GetEnabledListAsync();
}

View File

@ -0,0 +1,25 @@
using QYZH.InteractiveMagazine.Models.Dto.RabbitMQ;
using QYZH.InteractiveMagazine.Models.Entity;
namespace QYZH.InteractiveMagazine.IService;
/// <summary>
/// MQ消息发布服务。
/// </summary>
public interface IMessagePublishService : IBaseService<MessageOutbox>
{
/// <summary>
/// 可靠发布消息默认写入Outbox。
/// </summary>
Task<MessagePublishResult> PublishAsync<T>(MessagePublishInput<T> input, CancellationToken cancellationToken = default);
/// <summary>
/// 批量可靠发布消息默认写入Outbox。
/// </summary>
Task<MessagePublishResult> PublishBatchAsync<T>(IEnumerable<MessagePublishInput<T>> inputs, CancellationToken cancellationToken = default);
/// <summary>
/// 直接发布消息不写Outbox。
/// </summary>
Task<MessagePublishResult> PublishDirectAsync<T>(MessagePublishInput<T> input, CancellationToken cancellationToken = default);
}

View File

@ -21,6 +21,12 @@ public interface IOperationLogService : IBaseService<OperationLog>
/// <param name="ipAddress">IP地址可选</param>
Task LogAsync(long operatorId, string operatorName, string actionType, string targetType, long targetId, string? targetName = null, string? detail = null, string? ipAddress = null);
/// <summary>
/// 记录操作日志
/// </summary>
/// <param name="input">操作日志记录输入</param>
Task LogAsync(OperationLogRecordInput input);
/// <summary>
/// 分页查询操作日志
/// </summary>

View File

@ -33,6 +33,7 @@ public static class DependencyInjectionExtensions
services.AddTransient<GlobalExceptionMiddleware>();
services.AddTransient<OperationLogMiddleware>();
services.AddScoped<OperationLogActionFilter>();
}
private static void AddJwtAuthentication(IServiceCollection services, IConfiguration configuration, IWebHostEnvironment? environment = null)
@ -105,6 +106,7 @@ public static class DependencyInjectionExtensions
var adminToken = await RedisHelper.GetAsync(JwtHelper.BuildAdminTokenKey(userId));
if (adminToken == currentToken)
{
await RefreshRedisTokenExpiryAsync(JwtHelper.BuildAdminTokenKey(userId), currentToken, jwtSettings);
return;
}
@ -115,6 +117,7 @@ public static class DependencyInjectionExtensions
var wechatToken = await RedisHelper.GetAsync(JwtHelper.BuildWeChatTokenKey(wxUserId, userId));
if (wechatToken == currentToken)
{
await RefreshRedisTokenExpiryAsync(JwtHelper.BuildWeChatTokenKey(wxUserId, userId), currentToken, jwtSettings);
return;
}
@ -123,6 +126,14 @@ public static class DependencyInjectionExtensions
};
}
private static async Task RefreshRedisTokenExpiryAsync(string tokenKey, string currentToken, JwtSettings jwtSettings)
{
if (jwtSettings.ExpiryMinutes > 0)
{
await RedisHelper.SetAsync(tokenKey, currentToken, TimeSpan.FromMinutes(jwtSettings.ExpiryMinutes));
}
}
private static string? GetBearerToken(TokenValidatedContext context)
{
var authHeader = context.HttpContext.Request.Headers.Authorization.FirstOrDefault();

View File

@ -0,0 +1,86 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.ResponseCompression;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using QYZH.InteractiveMagazine.Common.Helpers;
using QYZH.InteractiveMagazine.Infrastructure.Middleware;
using QYZH.InteractiveMagazine.Infrastructure.RabbitMQ;
using QYZH.InteractiveMagazine.Infrastructure.Redis;
using QYZH.InteractiveMagazine.Infrastructure.SDK;
using QYZH.InteractiveMagazine.Models.Settings;
using System.Text.Json.Serialization;
namespace QYZH.InteractiveMagazine.Infrastructure.Extensions;
/// <summary>
/// API 默认服务注册扩展。
/// </summary>
public static class InteractiveMagazineApiDefaultsExtensions
{
/// <summary>
/// 注册 WebApi 与 WeChatApi 共享的默认服务。
/// </summary>
/// <param name="builder">应用构建器。</param>
/// <returns>应用构建器。</returns>
public static WebApplicationBuilder AddInteractiveMagazineApiDefaults(this WebApplicationBuilder builder)
{
builder.Services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo(Path.Combine(Directory.GetCurrentDirectory(), "DataProtection")));
builder.Services.AddCSRedisCacheExtension(builder.Configuration.GetSection("RedisSettings"));
builder.Services.AddRabbitMQ(builder.Configuration);
builder.Services.AddControllers(options =>
{
options.SuppressImplicitRequiredAttributeForNonNullableReferenceTypes = true;
options.Filters.Add<ModelValidActionFilterAttribute>();
options.Filters.AddService<OperationLogActionFilter>();
})
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.PropertyNameCaseInsensitive = true;
options.JsonSerializerOptions.Converters.Add(new JsonConverterUtil.DateTimeConverter());
options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
})
.ConfigureApiBehaviorOptions(opt => opt.SuppressModelStateInvalidFilter = true);
builder.Services.AddEndpointsApiExplorer();
builder.AddCorsRegister();
builder.Services.AddHttpClient();
builder.Services.AddHttpContextAccessor();
builder.Services.AddInfrastructureServices(builder.Configuration, builder.Environment);
builder.Services.AddSDKService(builder.Configuration);
builder.Services.Configure<MedalRuleConfigSettings>(builder.Configuration.GetSection("MedalRuleConfig"));
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowAll", policy =>
{
policy.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
});
});
builder.Services.Configure<BrotliCompressionProviderOptions>(options =>
{
options.Level = System.IO.Compression.CompressionLevel.Optimal;
});
builder.Services.Configure<GzipCompressionProviderOptions>(options =>
{
options.Level = System.IO.Compression.CompressionLevel.Fastest;
});
builder.Services.AddResponseCompression(options =>
{
options.EnableForHttps = true;
options.Providers.Add<BrotliCompressionProvider>();
options.Providers.Add<GzipCompressionProvider>();
options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(["image/svg+xml", "application/json", "text/plain"]);
});
return builder;
}
}

View File

@ -1,99 +1,21 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using QYZH.InteractiveMagazine.Infrastructure.Auth;
using QYZH.InteractiveMagazine.Models.Settings;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
namespace QYZH.InteractiveMagazine.Infrastructure.Middleware;
/// <summary>
/// Refreshes the Redis session TTL for the current JWT when it is still the active token.
/// 保留兼容的JWT自动刷新中间件实际刷新在认证成功后执行。
/// </summary>
public class JwtAutoRefreshMiddleware
{
private readonly RequestDelegate _next;
private readonly IConfiguration _configuration;
public JwtAutoRefreshMiddleware(RequestDelegate next, IConfiguration configuration)
public JwtAutoRefreshMiddleware(RequestDelegate next)
{
_next = next;
_configuration = configuration;
}
public async Task InvokeAsync(HttpContext context)
{
var authHeader = context.Request.Headers.Authorization.FirstOrDefault();
if (!string.IsNullOrEmpty(authHeader) && authHeader.StartsWith("Bearer "))
{
var token = authHeader.Substring("Bearer ".Length).Trim();
await TryRefreshRedisTokenExpiryAsync(token);
}
await _next(context);
}
private async Task TryRefreshRedisTokenExpiryAsync(string token)
{
try
{
var tokenHandler = new JwtSecurityTokenHandler();
if (tokenHandler.ReadToken(token) is not JwtSecurityToken jwtToken)
{
return;
}
if (jwtToken.ValidTo <= DateTime.UtcNow)
{
return;
}
var userId = GetClaimValue(jwtToken, ClaimTypes.NameIdentifier, JwtRegisteredClaimNames.NameId);
if (string.IsNullOrEmpty(userId))
{
return;
}
var jwtSettings = _configuration.GetSection("JwtSettings").Get<JwtSettings>();
if (jwtSettings == null || jwtSettings.ExpiryMinutes <= 0)
{
return;
}
var wxUserId = GetClaimValue(jwtToken, JwtHelper.WxUserIdClaimType);
await RefreshRedisTokenExpiryAsync(wxUserId, userId, token, jwtSettings.ExpiryMinutes);
}
catch
{
// Ignore refresh failures. Authentication middleware will validate the request later.
}
}
private static async Task RefreshRedisTokenExpiryAsync(string? wxUserId, string userId, string currentToken, int expiryMinutes)
{
var adminTokenKey = JwtHelper.BuildAdminTokenKey(userId);
var adminToken = await RedisHelper.GetAsync(adminTokenKey);
if (string.IsNullOrEmpty(wxUserId) && !string.IsNullOrEmpty(adminToken))
{
await RedisHelper.SetAsync(adminTokenKey, adminToken, TimeSpan.FromMinutes(expiryMinutes));
return;
}
if (string.IsNullOrEmpty(wxUserId))
{
return;
}
var wechatTokenKey = JwtHelper.BuildWeChatTokenKey(wxUserId, userId);
var wechatToken = await RedisHelper.GetAsync(wechatTokenKey);
if (wechatToken == currentToken)
{
await RedisHelper.SetAsync(wechatTokenKey, currentToken, TimeSpan.FromMinutes(expiryMinutes));
}
}
private static string? GetClaimValue(JwtSecurityToken jwtToken, params string[] claimTypes)
{
return jwtToken.Claims.FirstOrDefault(c => claimTypes.Contains(c.Type))?.Value;
}
}

View File

@ -0,0 +1,397 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using QYZH.InteractiveMagazine.IService;
using QYZH.InteractiveMagazine.Models.Dto;
using System.Collections;
using System.Diagnostics;
using System.Reflection;
using System.Security.Claims;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace QYZH.InteractiveMagazine.Infrastructure.Middleware;
/// <summary>
/// 操作日志过滤器
/// </summary>
public class OperationLogActionFilter(
IOperationLogService operationLogService,
ILogger<OperationLogActionFilter> logger) : IAsyncActionFilter
{
private static readonly HashSet<string> SensitiveNames = new(StringComparer.OrdinalIgnoreCase)
{
"password",
"oldPassword",
"newPassword",
"token",
"secret",
"authorization"
};
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
ReferenceHandler = ReferenceHandler.IgnoreCycles
};
/// <summary>
/// 执行操作日志过滤器
/// </summary>
/// <param name="context">Action 执行上下文</param>
/// <param name="next">后续执行委托</param>
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
var attribute = context.ActionDescriptor.EndpointMetadata
.OfType<OperationLogAttribute>()
.FirstOrDefault();
if (attribute == null)
{
await next();
return;
}
var stopwatch = Stopwatch.StartNew();
var executedContext = await next();
stopwatch.Stop();
if (executedContext.Exception != null && !executedContext.ExceptionHandled)
{
return;
}
if (!IsSuccessResult(executedContext.Result, context.HttpContext.Response.StatusCode))
{
return;
}
try
{
var operatorId = GetOperatorId(context);
if (!operatorId.HasValue)
{
return;
}
var operatorName = GetOperatorName(context);
var responseValue = GetResponseValue(executedContext.Result);
var responseResult = GetResponseResult(responseValue);
var targetId = ResolveTargetId(attribute, context, responseResult, operatorId.Value);
var targetName = ResolveTargetName(attribute, context, responseResult);
var detail = BuildDetail(attribute, context, responseValue, stopwatch.ElapsedMilliseconds);
await operationLogService.LogAsync(new OperationLogRecordInput
{
OperatorId = operatorId.Value,
OperatorName = operatorName,
ActionType = attribute.ActionType,
TargetType = attribute.TargetType,
TargetId = targetId,
TargetName = targetName,
Detail = detail,
IpAddress = GetClientIp(context)
});
}
catch (Exception ex)
{
logger.LogError(ex, "自动记录操作日志失败Path: {Path}", context.HttpContext.Request.Path);
}
}
private static bool IsSuccessResult(IActionResult? result, int responseStatusCode)
{
var responseValue = GetResponseValue(result);
if (responseValue is BaseResponse response)
{
return response.isSuccess;
}
if (result is ObjectResult objectResult && objectResult.StatusCode.HasValue)
{
return IsSuccessStatusCode(objectResult.StatusCode.Value);
}
if (result is StatusCodeResult statusCodeResult)
{
return IsSuccessStatusCode(statusCodeResult.StatusCode);
}
return IsSuccessStatusCode(responseStatusCode == 0 ? StatusCodes.Status200OK : responseStatusCode);
}
private static bool IsSuccessStatusCode(int statusCode)
{
return statusCode >= StatusCodes.Status200OK && statusCode < StatusCodes.Status300MultipleChoices;
}
private static long? GetOperatorId(ActionExecutingContext context)
{
var value = context.HttpContext.User.Claims
.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)
?.Value;
return long.TryParse(value, out var operatorId) ? operatorId : null;
}
private static string GetOperatorName(ActionExecutingContext context)
{
return context.HttpContext.User.Claims
.FirstOrDefault(c => c.Type == ClaimTypes.Name)
?.Value ?? string.Empty;
}
private static string? GetClientIp(ActionExecutingContext context)
{
var request = context.HttpContext.Request;
var forwardedFor = request.Headers["X-Forwarded-For"].FirstOrDefault();
if (!string.IsNullOrWhiteSpace(forwardedFor))
{
return forwardedFor.Split(',')[0].Trim();
}
var realIp = request.Headers["X-Real-IP"].FirstOrDefault();
if (!string.IsNullOrWhiteSpace(realIp))
{
return realIp;
}
return context.HttpContext.Connection.RemoteIpAddress?.ToString();
}
private static long ResolveTargetId(OperationLogAttribute attribute, ActionExecutingContext context, object? responseResult, long operatorId)
{
if (attribute.UseOperatorAsTargetId)
{
return operatorId;
}
if (TryGetLongRouteValue(context, attribute.TargetIdRouteKey, out var routeId))
{
return routeId;
}
if (TryGetLongArgumentValue(context, attribute.TargetIdArgumentName, out var argumentId))
{
return argumentId;
}
foreach (var key in new[] { "id", "userId", "adminUserId", "roleId", "taskId", "recordId" })
{
if (TryGetLongRouteValue(context, key, out routeId) || TryGetLongArgumentValue(context, key, out argumentId))
{
return routeId != 0 ? routeId : argumentId;
}
}
if (TryConvertToLong(responseResult, out var responseId))
{
return responseId;
}
return TryGetLongPropertyValue(responseResult, "Id", out responseId) ? responseId : 0;
}
private static string? ResolveTargetName(OperationLogAttribute attribute, ActionExecutingContext context, object? responseResult)
{
if (!string.IsNullOrWhiteSpace(attribute.TargetNameArgumentName)
&& context.ActionArguments.TryGetValue(attribute.TargetNameArgumentName, out var nameArgument))
{
if (nameArgument is string name)
{
return name;
}
if (!string.IsNullOrWhiteSpace(attribute.TargetNameProperty)
&& TryGetStringPropertyValue(nameArgument, attribute.TargetNameProperty, out name))
{
return name;
}
}
foreach (var value in context.ActionArguments.Values)
{
if (!string.IsNullOrWhiteSpace(attribute.TargetNameProperty)
&& TryGetStringPropertyValue(value, attribute.TargetNameProperty, out var configuredName))
{
return configuredName;
}
if (TryGetStringPropertyValue(value, "Name", out var name)
|| TryGetStringPropertyValue(value, "Title", out name))
{
return name;
}
}
if (!string.IsNullOrWhiteSpace(attribute.TargetNameProperty)
&& TryGetStringPropertyValue(responseResult, attribute.TargetNameProperty, out var responseName))
{
return responseName;
}
return TryGetStringPropertyValue(responseResult, "Name", out var defaultName)
|| TryGetStringPropertyValue(responseResult, "Title", out defaultName)
? defaultName
: null;
}
private static string BuildDetail(OperationLogAttribute attribute, ActionExecutingContext context, object? responseValue, long elapsedMilliseconds)
{
var detail = new Dictionary<string, object?>
{
["method"] = context.HttpContext.Request.Method,
["path"] = context.HttpContext.Request.Path.Value,
["routeValues"] = context.RouteData.Values.ToDictionary(k => k.Key, v => v.Value?.ToString()),
["arguments"] = attribute.LogArguments ? SanitizeValue(context.ActionArguments, 0) : null,
["responseMessage"] = GetResponseMessage(responseValue),
["elapsedMilliseconds"] = elapsedMilliseconds
};
return JsonSerializer.Serialize(detail, JsonOptions);
}
private static string? GetResponseMessage(object? responseValue)
{
return responseValue is BaseResponse response ? response.message : null;
}
private static object? GetResponseValue(IActionResult? result)
{
return result switch
{
ObjectResult objectResult => objectResult.Value,
JsonResult jsonResult => jsonResult.Value,
_ => null
};
}
private static object? GetResponseResult(object? responseValue)
{
if (responseValue == null)
{
return null;
}
return responseValue.GetType()
.GetProperty("result", BindingFlags.Instance | BindingFlags.Public | BindingFlags.IgnoreCase)
?.GetValue(responseValue);
}
private static object? SanitizeValue(object? value, int depth)
{
if (value == null)
{
return null;
}
if (depth > 4)
{
return value.ToString();
}
var type = value.GetType();
if (type.IsPrimitive || value is string or decimal or DateTime or DateTimeOffset or Guid || type.IsEnum)
{
return value;
}
if (value is IDictionary dictionary)
{
var result = new Dictionary<string, object?>();
foreach (DictionaryEntry item in dictionary)
{
var key = item.Key?.ToString() ?? string.Empty;
result[key] = SensitiveNames.Contains(key) ? "***" : SanitizeValue(item.Value, depth + 1);
}
return result;
}
if (value is IEnumerable enumerable && value is not string)
{
return enumerable.Cast<object?>()
.Take(20)
.Select(item => SanitizeValue(item, depth + 1))
.ToList();
}
return type.GetProperties(BindingFlags.Instance | BindingFlags.Public)
.Where(p => p.GetIndexParameters().Length == 0)
.ToDictionary(
p => p.Name,
p => SensitiveNames.Contains(p.Name) ? "***" : SanitizeValue(p.GetValue(value), depth + 1));
}
private static bool TryGetLongRouteValue(ActionExecutingContext context, string? key, out long value)
{
value = 0;
if (string.IsNullOrWhiteSpace(key) || !context.RouteData.Values.TryGetValue(key, out var routeValue))
{
return false;
}
return long.TryParse(routeValue?.ToString(), out value);
}
private static bool TryGetLongArgumentValue(ActionExecutingContext context, string? key, out long value)
{
value = 0;
if (string.IsNullOrWhiteSpace(key) || !context.ActionArguments.TryGetValue(key, out var argumentValue))
{
return false;
}
if (TryConvertToLong(argumentValue, out value))
{
return true;
}
return TryGetLongPropertyValue(argumentValue, "Id", out value);
}
private static bool TryGetLongPropertyValue(object? source, string propertyName, out long value)
{
value = 0;
if (source == null)
{
return false;
}
var property = source.GetType().GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.IgnoreCase);
return property != null && TryConvertToLong(property.GetValue(source), out value);
}
private static bool TryConvertToLong(object? source, out long value)
{
value = 0;
return source switch
{
long longValue => SetValue(longValue, out value),
int intValue => SetValue(intValue, out value),
string stringValue => long.TryParse(stringValue, out value),
_ => long.TryParse(source?.ToString(), out value)
};
}
private static bool TryGetStringPropertyValue(object? source, string propertyName, out string? value)
{
value = null;
if (source == null)
{
return false;
}
var property = source.GetType().GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.IgnoreCase);
value = property?.GetValue(source)?.ToString();
return !string.IsNullOrWhiteSpace(value);
}
private static bool SetValue(long source, out long value)
{
value = source;
return true;
}
}

View File

@ -0,0 +1,59 @@
namespace QYZH.InteractiveMagazine.Infrastructure.Middleware;
/// <summary>
/// 操作日志标记
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public sealed class OperationLogAttribute : Attribute
{
/// <summary>
/// 构造函数
/// </summary>
/// <param name="actionType">操作类型</param>
/// <param name="targetType">目标类型</param>
public OperationLogAttribute(string actionType, string targetType)
{
ActionType = actionType;
TargetType = targetType;
}
/// <summary>
/// 操作类型
/// </summary>
public string ActionType { get; }
/// <summary>
/// 目标类型
/// </summary>
public string TargetType { get; }
/// <summary>
/// 目标Id路由键
/// </summary>
public string? TargetIdRouteKey { get; set; } = "id";
/// <summary>
/// 目标Id参数名
/// </summary>
public string? TargetIdArgumentName { get; set; }
/// <summary>
/// 目标名称参数名
/// </summary>
public string? TargetNameArgumentName { get; set; }
/// <summary>
/// 目标名称属性名
/// </summary>
public string? TargetNameProperty { get; set; }
/// <summary>
/// 使用当前操作人Id作为目标Id
/// </summary>
public bool UseOperatorAsTargetId { get; set; }
/// <summary>
/// 是否记录参数摘要
/// </summary>
public bool LogArguments { get; set; } = true;
}

View File

@ -1,6 +1,7 @@
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
@ -11,14 +12,16 @@ namespace QYZH.InteractiveMagazine.Infrastructure.RabbitMQ
{
private readonly IRabbitMQConnection _connection;
private readonly IConfiguration _configuration;
private readonly ILogger<RabbitMQService> _logger;
private readonly JsonSerializerOptions options = new JsonSerializerOptions
{
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
public RabbitMQService(IRabbitMQConnection connection, IConfiguration configuration)
public RabbitMQService(IRabbitMQConnection connection, IConfiguration configuration, ILogger<RabbitMQService> logger)
{
_connection = connection ?? throw new ArgumentNullException(nameof(connection));
_configuration = configuration;
_logger = logger;
}
@ -55,12 +58,14 @@ namespace QYZH.InteractiveMagazine.Infrastructure.RabbitMQ
}
catch (OperationCanceledException ex)
{
Console.WriteLine($"Operation was canceled: {ex.Message}");
_logger.LogWarning(ex, "RabbitMQ消息发送已取消Exchange: {Exchange}, Queue: {Queue}, RoutingKey: {RoutingKey}",
param.Exchange, param.Queue, param.RoutingKey);
return false;
}
catch (Exception ex)
{
Console.WriteLine($"An error occurred: {ex.Message}");
_logger.LogError(ex, "RabbitMQ消息发送失败Exchange: {Exchange}, Queue: {Queue}, RoutingKey: {RoutingKey}",
param.Exchange, param.Queue, param.RoutingKey);
return false;
}
}
@ -115,7 +120,7 @@ namespace QYZH.InteractiveMagazine.Infrastructure.RabbitMQ
}
catch (Exception ex)
{
Console.WriteLine($"An error occurred: {ex.Message}");
_logger.LogError(ex, "RabbitMQ批量消息发送失败");
// 回滚事务
try { await channel?.TxRollbackAsync(); } catch { /* 忽略回滚异常 */ }
return false;

View File

@ -1,3 +1,5 @@
using QYZH.InteractiveMagazine.Models.Enum;
namespace QYZH.InteractiveMagazine.Models.Dto;
/// <summary>
@ -148,6 +150,11 @@ public class AdminRoleInput
/// </summary>
public string Code { get; set; } = string.Empty;
/// <summary>
/// 角色类型
/// </summary>
public AdminRoleTypeEnum Type { get; set; } = AdminRoleTypeEnum.Normal;
/// <summary>
/// 备注
/// </summary>
@ -184,6 +191,11 @@ public class AdminRoleOutput
/// </summary>
public string Code { get; set; } = string.Empty;
/// <summary>
/// 角色类型
/// </summary>
public AdminRoleTypeEnum Type { get; set; }
/// <summary>
/// 备注
/// </summary>
@ -224,6 +236,11 @@ public class AdminRoleSimpleOutput
/// 角色编码
/// </summary>
public string Code { get; set; } = string.Empty;
/// <summary>
/// 角色类型
/// </summary>
public AdminRoleTypeEnum Type { get; set; }
}
/// <summary>
@ -241,6 +258,11 @@ public class AdminRoleQueryInput : PageQueryModel
/// </summary>
public string? Code { get; set; }
/// <summary>
/// 角色类型
/// </summary>
public AdminRoleTypeEnum? Type { get; set; }
/// <summary>
/// 状态
/// </summary>

View File

@ -0,0 +1,163 @@
using QYZH.InteractiveMagazine.Common.Helpers;
using QYZH.InteractiveMagazine.Models.Enum;
namespace QYZH.InteractiveMagazine.Models.Dto.Banner;
/// <summary>
/// 轮播图创建/更新输入
/// </summary>
public class BannerInput
{
/// <summary>
/// 标题
/// </summary>
public string Title { get; set; } = string.Empty;
/// <summary>
/// 轮播图OSS路径
/// </summary>
public string ImageUrl { get; set; } = string.Empty;
/// <summary>
/// 展示位置
/// </summary>
public BannerPositionEnum Position { get; set; } = BannerPositionEnum.Home;
/// <summary>
/// 跳转类型
/// </summary>
public BannerTargetTypeEnum TargetType { get; set; } = BannerTargetTypeEnum.None;
/// <summary>
/// 跳转值
/// </summary>
public string? TargetValue { get; set; }
/// <summary>
/// 排序值,越小越靠前
/// </summary>
public int Sort { get; set; }
/// <summary>
/// 生效开始时间
/// </summary>
public DateTime? StartTime { get; set; }
/// <summary>
/// 生效结束时间
/// </summary>
public DateTime? EndTime { get; set; }
/// <summary>
/// 备注
/// </summary>
public string? Remark { get; set; }
}
/// <summary>
/// 轮播图输出
/// </summary>
public class BannerOutput
{
private string _imageUrl = string.Empty;
/// <summary>
/// 主键ID
/// </summary>
public long Id { get; set; }
/// <summary>
/// 标题
/// </summary>
public string Title { get; set; } = string.Empty;
/// <summary>
/// 轮播图完整地址
/// </summary>
public string ImageUrl
{
get => DomainHelper.OssFullUrl(_imageUrl);
set => _imageUrl = value ?? string.Empty;
}
/// <summary>
/// 展示位置
/// </summary>
public BannerPositionEnum Position { get; set; }
/// <summary>
/// 跳转类型
/// </summary>
public BannerTargetTypeEnum TargetType { get; set; }
/// <summary>
/// 跳转值
/// </summary>
public string? TargetValue { get; set; }
/// <summary>
/// 排序值,越小越靠前
/// </summary>
public int Sort { get; set; }
/// <summary>
/// 生效开始时间
/// </summary>
public DateTime? StartTime { get; set; }
/// <summary>
/// 生效结束时间
/// </summary>
public DateTime? EndTime { get; set; }
/// <summary>
/// 备注
/// </summary>
public string? Remark { get; set; }
/// <summary>
/// 状态
/// </summary>
public int Status { get; set; }
/// <summary>
/// 创建人
/// </summary>
public string? CreatedBy { get; set; }
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreatedAt { get; set; }
/// <summary>
/// 修改人
/// </summary>
public string? UpdatedBy { get; set; }
/// <summary>
/// 修改时间
/// </summary>
public DateTime? UpdatedAt { get; set; }
}
/// <summary>
/// 轮播图分页查询输入
/// </summary>
public class BannerQueryInput : PageQueryModel
{
/// <summary>
/// 标题
/// </summary>
public string? Title { get; set; }
/// <summary>
/// 展示位置
/// </summary>
public BannerPositionEnum? Position { get; set; }
/// <summary>
/// 状态
/// </summary>
public int? Status { get; set; }
}

View File

@ -180,7 +180,7 @@ public class GenerateUserJournalQrCodeMessage
}
/// <summary>
/// 鏈熷垔浜岀淮鐮佹煡璇㈣緭鍏TO
/// 期刊二维码查询输入DTO
/// </summary>
public class UserJournalQrCodeQueryInput : PageQueryModel
{

View File

@ -0,0 +1,200 @@
using QYZH.InteractiveMagazine.Common.Helpers;
using QYZH.InteractiveMagazine.Models.Enum;
namespace QYZH.InteractiveMagazine.Models.Dto.Material;
/// <summary>
/// 资料创建/更新输入
/// </summary>
public class MaterialInput
{
/// <summary>
/// 资料标题
/// </summary>
public string Title { get; set; } = string.Empty;
/// <summary>
/// 资料名称
/// </summary>
public string? Name { get; set; }
/// <summary>
/// 资料描述
/// </summary>
public string? Description { get; set; }
/// <summary>
/// 排序值,越小越靠前
/// </summary>
public int Sort { get; set; }
/// <summary>
/// 生效开始时间
/// </summary>
public DateTime? StartTime { get; set; }
/// <summary>
/// 生效结束时间
/// </summary>
public DateTime? EndTime { get; set; }
/// <summary>
/// 资料文件列表
/// </summary>
public List<MaterialFileInput> Files { get; set; } = new();
}
/// <summary>
/// 资料文件输入
/// </summary>
public class MaterialFileInput
{
/// <summary>
/// 文件类型
/// </summary>
public MaterialFileTypeEnum FileType { get; set; }
/// <summary>
/// 文件OSS路径
/// </summary>
public string FileUrl { get; set; } = string.Empty;
/// <summary>
/// 文件名称
/// </summary>
public string? FileName { get; set; }
/// <summary>
/// 排序值,越小越靠前
/// </summary>
public int Sort { get; set; }
}
/// <summary>
/// 资料输出
/// </summary>
public class MaterialOutput
{
/// <summary>
/// 主键ID
/// </summary>
public long Id { get; set; }
/// <summary>
/// 资料标题
/// </summary>
public string Title { get; set; } = string.Empty;
/// <summary>
/// 资料名称
/// </summary>
public string? Name { get; set; }
/// <summary>
/// 资料描述
/// </summary>
public string? Description { get; set; }
/// <summary>
/// 排序值,越小越靠前
/// </summary>
public int Sort { get; set; }
/// <summary>
/// 生效开始时间
/// </summary>
public DateTime? StartTime { get; set; }
/// <summary>
/// 生效结束时间
/// </summary>
public DateTime? EndTime { get; set; }
/// <summary>
/// 状态
/// </summary>
public int Status { get; set; }
/// <summary>
/// 创建人
/// </summary>
public string? CreatedBy { get; set; }
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreatedAt { get; set; }
/// <summary>
/// 修改人
/// </summary>
public string? UpdatedBy { get; set; }
/// <summary>
/// 修改时间
/// </summary>
public DateTime? UpdatedAt { get; set; }
/// <summary>
/// 资料文件列表
/// </summary>
public List<MaterialFileOutput> Files { get; set; } = new();
}
/// <summary>
/// 资料文件输出
/// </summary>
public class MaterialFileOutput
{
private string _fileUrl = string.Empty;
/// <summary>
/// 主键ID
/// </summary>
public long Id { get; set; }
/// <summary>
/// 文件类型
/// </summary>
public MaterialFileTypeEnum FileType { get; set; }
/// <summary>
/// 文件完整地址
/// </summary>
public string FileUrl
{
get => DomainHelper.OssFullUrl(_fileUrl);
set => _fileUrl = value ?? string.Empty;
}
/// <summary>
/// 文件名称
/// </summary>
public string? FileName { get; set; }
/// <summary>
/// 排序值,越小越靠前
/// </summary>
public int Sort { get; set; }
}
/// <summary>
/// 资料分页查询输入
/// </summary>
public class MaterialQueryInput : PageQueryModel
{
/// <summary>
/// 标题
/// </summary>
public string? Title { get; set; }
/// <summary>
/// 名称
/// </summary>
public string? Name { get; set; }
/// <summary>
/// 状态
/// </summary>
public int? Status { get; set; }
}

View File

@ -5,6 +5,36 @@ namespace QYZH.InteractiveMagazine.Models.Dto;
/// </summary>
public static class OperationLogActionType
{
/// <summary>
/// 新增
/// </summary>
public const string Create = "Create";
/// <summary>
/// 修改
/// </summary>
public const string Update = "Update";
/// <summary>
/// 删除
/// </summary>
public const string Delete = "Delete";
/// <summary>
/// 状态变更
/// </summary>
public const string StatusChange = "StatusChange";
/// <summary>
/// 分配
/// </summary>
public const string Assign = "Assign";
/// <summary>
/// 修改密码
/// </summary>
public const string ChangePassword = "ChangePassword";
/// <summary>
/// 手动增加积分
/// </summary>
@ -31,17 +61,163 @@ public static class OperationLogActionType
/// </summary>
public static class OperationLogTargetType
{
/// <summary>
/// 管理员
/// </summary>
public const string AdminUser = "AdminUser";
/// <summary>
/// 权限菜单
/// </summary>
public const string AdminMenu = "AdminMenu";
/// <summary>
/// 管理员角色
/// </summary>
public const string AdminRole = "AdminRole";
/// <summary>
/// 用户
/// </summary>
public const string User = "User";
/// <summary>
/// 社区留言
/// </summary>
public const string CommunityMessage = "CommunityMessage";
/// <summary>
/// 勋章
/// </summary>
public const string Medal = "Medal";
/// <summary>
/// AI 基础提示词
/// </summary>
public const string AiBasePrompt = "AiBasePrompt";
/// <summary>
/// 签到配置
/// </summary>
public const string CheckInConfig = "CheckInConfig";
/// <summary>
/// Banner
/// </summary>
public const string Banner = "Banner";
/// <summary>
/// 素材
/// </summary>
public const string Material = "Material";
/// <summary>
/// 商品
/// </summary>
public const string Product = "Product";
/// <summary>
/// 宠物
/// </summary>
public const string Pet = "Pet";
/// <summary>
/// 宠物进化
/// </summary>
public const string PetEvolution = "PetEvolution";
/// <summary>
/// 宠物皮肤
/// </summary>
public const string PetSkin = "PetSkin";
/// <summary>
/// 宠物皮肤图片
/// </summary>
public const string PetSkinImage = "PetSkinImage";
/// <summary>
/// 用户期刊二维码
/// </summary>
public const string UserJournalQrCode = "UserJournalQrCode";
/// <summary>
/// 期刊
/// </summary>
public const string Journal = "Journal";
/// <summary>
/// 期刊目录
/// </summary>
public const string JournalCatalog = "JournalCatalog";
/// <summary>
/// 期刊书页
/// </summary>
public const string JournalPage = "JournalPage";
/// <summary>
/// 期刊书页任务
/// </summary>
public const string JournalPageTask = "JournalPageTask";
/// <summary>
/// 期刊任务答案
/// </summary>
public const string JournalTaskAnswer = "JournalTaskAnswer";
/// <summary>
/// 补偿任务
/// </summary>
public const string CompensationTask = "CompensationTask";
}
/// <summary>
/// 操作日志记录输入
/// </summary>
public class OperationLogRecordInput
{
/// <summary>
/// 操作人Id
/// </summary>
public long OperatorId { get; set; }
/// <summary>
/// 操作人用户名
/// </summary>
public string OperatorName { get; set; } = string.Empty;
/// <summary>
/// 操作类型
/// </summary>
public string ActionType { get; set; } = string.Empty;
/// <summary>
/// 目标类型
/// </summary>
public string TargetType { get; set; } = string.Empty;
/// <summary>
/// 目标记录Id
/// </summary>
public long TargetId { get; set; }
/// <summary>
/// 目标名称
/// </summary>
public string? TargetName { get; set; }
/// <summary>
/// 操作详情 JSON
/// </summary>
public string? Detail { get; set; }
/// <summary>
/// IP地址
/// </summary>
public string? IpAddress { get; set; }
}
/// <summary>
/// 操作日志分页查询输入
/// </summary>

View File

@ -0,0 +1,38 @@
namespace QYZH.InteractiveMagazine.Models.Dto.RabbitMQ;
/// <summary>
/// MQ消息发布入参。
/// </summary>
/// <typeparam name="T">消息数据类型。</typeparam>
public class MessagePublishInput<T>
{
/// <summary>
/// 交换机。
/// </summary>
public string Exchange { get; set; } = string.Empty;
/// <summary>
/// 队列。
/// </summary>
public string Queue { get; set; } = string.Empty;
/// <summary>
/// 路由键。
/// </summary>
public string RoutingKey { get; set; } = string.Empty;
/// <summary>
/// 消息数据。
/// </summary>
public T Data { get; set; } = default!;
/// <summary>
/// 业务类型。
/// </summary>
public string BusinessType { get; set; } = string.Empty;
/// <summary>
/// 业务ID。
/// </summary>
public long BusinessId { get; set; }
}

View File

@ -0,0 +1,22 @@
namespace QYZH.InteractiveMagazine.Models.Dto.RabbitMQ;
/// <summary>
/// MQ消息发布结果。
/// </summary>
public class MessagePublishResult
{
/// <summary>
/// 是否成功。
/// </summary>
public bool Success { get; set; }
/// <summary>
/// Outbox消息ID。
/// </summary>
public List<long> OutboxIds { get; set; } = [];
/// <summary>
/// 结果消息。
/// </summary>
public string Message { get; set; } = string.Empty;
}

View File

@ -5,7 +5,7 @@ namespace QYZH.InteractiveMagazine.Models.Entity;
/// <summary>
/// 后台菜单表
/// </summary>
[SugarTable("AdminMenu")]
[SugarTable("System_AdminMenu")]
public partial class AdminMenu : SqlSugarBaseEntity
{
/// <summary>

View File

@ -1,11 +1,12 @@
using SqlSugar;
using QYZH.InteractiveMagazine.Models.Enum;
namespace QYZH.InteractiveMagazine.Models.Entity;
/// <summary>
/// 后台角色表
/// </summary>
[SugarTable("AdminRole")]
[SugarTable("System_AdminRole")]
public partial class AdminRole : SqlSugarBaseEntity
{
/// <summary>
@ -22,6 +23,13 @@ public partial class AdminRole : SqlSugarBaseEntity
/// </summary>
public string Code { get; set; } = string.Empty;
/// <summary>
/// Desc:角色类型0=普通角色1=系统角色
/// Default:0
/// Nullable:False
/// </summary>
public AdminRoleTypeEnum Type { get; set; } = AdminRoleTypeEnum.Normal;
/// <summary>
/// Desc:备注
/// Default:

View File

@ -5,7 +5,7 @@ namespace QYZH.InteractiveMagazine.Models.Entity;
/// <summary>
/// 后台角色菜单关系表
/// </summary>
[SugarTable("AdminRoleMenu")]
[SugarTable("System_AdminRoleMenu")]
public partial class AdminRoleMenu : SqlSugarBaseEntity
{
/// <summary>

View File

@ -6,7 +6,7 @@ namespace QYZH.InteractiveMagazine.Models.Entity
///<summary>
///后台管理员表
///</summary>
[SugarTable("AdminUser")]
[SugarTable("System_AdminUser")]
public partial class AdminUser : SqlSugarBaseEntity
{
public AdminUser(){

View File

@ -5,7 +5,7 @@ namespace QYZH.InteractiveMagazine.Models.Entity;
/// <summary>
/// 后台管理员角色关系表
/// </summary>
[SugarTable("AdminUserRole")]
[SugarTable("System_AdminUserRole")]
public partial class AdminUserRole : SqlSugarBaseEntity
{
/// <summary>

View File

@ -8,7 +8,7 @@ namespace QYZH.InteractiveMagazine.Models.Entity
///<summary>
///AIPrompt配置表
///</summary>
[SugarTable("AiBasePrompt")]
[SugarTable("Ai_BasePrompt")]
public partial class AiBasePrompt : SqlSugarBaseEntity
{
public AiBasePrompt(){

View File

@ -0,0 +1,74 @@
using QYZH.InteractiveMagazine.Models.Enum;
using SqlSugar;
namespace QYZH.InteractiveMagazine.Models.Entity;
/// <summary>
/// 轮播图表
/// </summary>
[SugarTable("Content_Banner")]
public partial class Banner : SqlSugarBaseEntity
{
/// <summary>
/// Desc:标题
/// Default:
/// Nullable:False
/// </summary>
public string Title { get; set; } = string.Empty;
/// <summary>
/// Desc:轮播图OSS路径
/// Default:
/// Nullable:False
/// </summary>
public string ImageUrl { get; set; } = string.Empty;
/// <summary>
/// Desc:展示位置
/// Default:1
/// Nullable:False
/// </summary>
public BannerPositionEnum Position { get; set; } = BannerPositionEnum.Home;
/// <summary>
/// Desc:跳转类型
/// Default:0
/// Nullable:False
/// </summary>
public BannerTargetTypeEnum TargetType { get; set; } = BannerTargetTypeEnum.None;
/// <summary>
/// Desc:跳转值
/// Default:
/// Nullable:True
/// </summary>
public string? TargetValue { get; set; }
/// <summary>
/// Desc:排序值,越小越靠前
/// Default:0
/// Nullable:False
/// </summary>
public int Sort { get; set; }
/// <summary>
/// Desc:生效开始时间
/// Default:
/// Nullable:True
/// </summary>
public DateTime? StartTime { get; set; }
/// <summary>
/// Desc:生效结束时间
/// Default:
/// Nullable:True
/// </summary>
public DateTime? EndTime { get; set; }
/// <summary>
/// Desc:备注
/// Default:
/// Nullable:True
/// </summary>
public string? Remark { get; set; }
}

View File

@ -6,7 +6,7 @@ namespace QYZH.InteractiveMagazine.Models.Entity
///<summary>
///签到配置表
///</summary>
[SugarTable("CheckInConfig")]
[SugarTable("CheckIn_Config")]
public partial class CheckInConfig : SqlSugarBaseEntity
{
public CheckInConfig(){

View File

@ -6,7 +6,7 @@ namespace QYZH.InteractiveMagazine.Models.Entity
///<summary>
///签到记录表
///</summary>
[SugarTable("CheckInRecord")]
[SugarTable("CheckIn_Record")]
public partial class CheckInRecord : SqlSugarBaseEntity
{
public CheckInRecord(){

View File

@ -6,7 +6,7 @@ namespace QYZH.InteractiveMagazine.Models.Entity
///<summary>
///社区预置消息表
///</summary>
[SugarTable("CommunityMessage")]
[SugarTable("Community_Message")]
public partial class CommunityMessage : SqlSugarBaseEntity
{
public CommunityMessage()

View File

@ -6,7 +6,7 @@ namespace QYZH.InteractiveMagazine.Models.Entity
///<summary>
///模板评论表
///</summary>
[SugarTable("CommunityMessageComment")]
[SugarTable("Community_MessageComment")]
public partial class CommunityMessageComment : SqlSugarBaseEntity
{
public CommunityMessageComment(){

View File

@ -6,7 +6,7 @@ namespace QYZH.InteractiveMagazine.Models.Entity
///<summary>
///点赞记录表
///</summary>
[SugarTable("CommunityMessageLike")]
[SugarTable("Community_MessageLike")]
public partial class CommunityMessageLike : SqlSugarBaseEntity
{
public CommunityMessageLike()

View File

@ -6,7 +6,7 @@ namespace QYZH.InteractiveMagazine.Models.Entity
///<summary>
///固定模板言论表
///</summary>
[SugarTable("CommunityTemplateSentence")]
[SugarTable("Community_TemplateSentence")]
public partial class CommunityTemplateSentence : SqlSugarBaseEntity
{
public CommunityTemplateSentence(){

View File

@ -6,7 +6,7 @@ namespace QYZH.InteractiveMagazine.Models.Entity
/// <summary>
/// 补偿任务表 — 记录业务执行失败后需要异步重试的操作
/// </summary>
[SugarTable("CompensationTask")]
[SugarTable("Compensation_Task")]
public partial class CompensationTask : SqlSugarBaseEntity
{
public CompensationTask() { }

View File

@ -9,7 +9,7 @@ namespace QYZH.InteractiveMagazine.Models.Entity
///<summary>
///
///</summary>
[SugarTable("DotFile")]
[SugarTable("Journal_DotFile")]
public partial class DotFile : SqlSugarBaseEntity
{
public DotFile(){

View File

@ -9,7 +9,7 @@ namespace QYZH.InteractiveMagazine.Models.Entity
///<summary>
///
///</summary>
[SugarTable("DotFileDetail")]
[SugarTable("Journal_DotFileDetail")]
public partial class DotFileDetail : SqlSugarBaseEntity
{
public DotFileDetail(){

View File

@ -6,7 +6,7 @@ namespace QYZH.InteractiveMagazine.Models.Entity
/// <summary>
/// 商品兑换记录表
/// </summary>
[SugarTable("ExchangeRecord")]
[SugarTable("Mall_ExchangeRecord")]
public partial class ExchangeRecord : SqlSugarBaseEntity
{
public ExchangeRecord() { }

View File

@ -6,7 +6,7 @@ namespace QYZH.InteractiveMagazine.Models.Entity
///<summary>
///期刊表
///</summary>
[SugarTable("Journal")]
[SugarTable("Journal_Book")]
public partial class Journal : SqlSugarBaseEntity
{
public Journal(){

View File

@ -9,7 +9,7 @@ namespace QYZH.InteractiveMagazine.Models.Entity
///<summary>
///
///</summary>
[SugarTable("JournalCatalog")]
[SugarTable("Journal_Catalog")]
public partial class JournalCatalog : SqlSugarBaseEntity
{
public JournalCatalog(){

View File

@ -8,7 +8,7 @@ namespace QYZH.InteractiveMagazine.Models.Entity
///<summary>
///书页;
///</summary>
[SugarTable("JournalPage")]
[SugarTable("Journal_Page")]
public partial class JournalPage : SqlSugarBaseEntity
{
public JournalPage(){

View File

@ -6,7 +6,7 @@ namespace QYZH.InteractiveMagazine.Models.Entity
///<summary>
///杂志任务;
///</summary>
[SugarTable("JournalPageTask")]
[SugarTable("Journal_PageTask")]
public class JournalPageTask : SqlSugarBaseEntity
{
/// <summary>

View File

@ -8,7 +8,7 @@ namespace QYZH.InteractiveMagazine.Models.Entity
///<summary>
///杂志任务答案表;
///</summary>
[SugarTable("JournalPageTaskAnswer")]
[SugarTable("Journal_PageTaskAnswer")]
public partial class JournalPageTaskAnswer : SqlSugarBaseEntity
{
public JournalPageTaskAnswer(){

View File

@ -6,7 +6,7 @@ namespace QYZH.InteractiveMagazine.Models.Entity
/// <summary>
/// 杂志任务用户作答记录
/// </summary>
[SugarTable("JournalPageTaskUserAnswer")]
[SugarTable("Journal_PageTaskUserAnswer")]
public class JournalPageTaskUserAnswer : SqlSugarBaseEntity
{
/// <summary>
@ -231,6 +231,6 @@ namespace QYZH.InteractiveMagazine.Models.Entity
/// Default:
/// Nullable:True
/// </summary>
public string AssignmentStatus { get; set; }
public int AssignmentStatus { get; set; }
}
}

View File

@ -5,7 +5,7 @@ namespace QYZH.InteractiveMagazine.Models.Entity
/// <summary>
/// Snapshot record for a user answer.
/// </summary>
[SugarTable("JournalPageTaskUserAnswerSnapshot")]
[SugarTable("Journal_PageTaskUserAnswerSnapshot")]
public class JournalPageTaskUserAnswerSnapshot : SqlSugarBaseEntity
{
/// <summary>
@ -171,6 +171,6 @@ namespace QYZH.InteractiveMagazine.Models.Entity
/// <summary>
/// Assignment status.
/// </summary>
public string? AssignmentStatus { get; set; }
public int? AssignmentStatus { get; set; }
}
}

View File

@ -0,0 +1,52 @@
using SqlSugar;
namespace QYZH.InteractiveMagazine.Models.Entity;
/// <summary>
/// 资料表
/// </summary>
[SugarTable("Content_Material")]
public partial class Material : SqlSugarBaseEntity
{
/// <summary>
/// Desc:资料标题
/// Default:
/// Nullable:False
/// </summary>
public string Title { get; set; } = string.Empty;
/// <summary>
/// Desc:资料名称
/// Default:
/// Nullable:True
/// </summary>
public string? Name { get; set; }
/// <summary>
/// Desc:资料描述
/// Default:
/// Nullable:True
/// </summary>
public string? Description { get; set; }
/// <summary>
/// Desc:排序值,越小越靠前
/// Default:0
/// Nullable:False
/// </summary>
public int Sort { get; set; }
/// <summary>
/// Desc:生效开始时间
/// Default:
/// Nullable:True
/// </summary>
public DateTime? StartTime { get; set; }
/// <summary>
/// Desc:生效结束时间
/// Default:
/// Nullable:True
/// </summary>
public DateTime? EndTime { get; set; }
}

View File

@ -0,0 +1,46 @@
using QYZH.InteractiveMagazine.Models.Enum;
using SqlSugar;
namespace QYZH.InteractiveMagazine.Models.Entity;
/// <summary>
/// 资料文件表
/// </summary>
[SugarTable("Content_MaterialFile")]
public partial class MaterialFile : SqlSugarBaseEntity
{
/// <summary>
/// Desc:资料ID
/// Default:
/// Nullable:False
/// </summary>
public long MaterialId { get; set; }
/// <summary>
/// Desc:文件类型
/// Default:
/// Nullable:False
/// </summary>
public MaterialFileTypeEnum FileType { get; set; }
/// <summary>
/// Desc:文件OSS路径
/// Default:
/// Nullable:False
/// </summary>
public string FileUrl { get; set; } = string.Empty;
/// <summary>
/// Desc:文件名称
/// Default:
/// Nullable:True
/// </summary>
public string? FileName { get; set; }
/// <summary>
/// Desc:排序值,越小越靠前
/// Default:0
/// Nullable:False
/// </summary>
public int Sort { get; set; }
}

View File

@ -6,7 +6,7 @@ namespace QYZH.InteractiveMagazine.Models.Entity
///<summary>
///勋章定义表
///</summary>
[SugarTable("Medal")]
[SugarTable("Medal_Definition")]
public partial class Medal : SqlSugarBaseEntity
{
public Medal()

View File

@ -8,7 +8,7 @@ namespace QYZH.InteractiveMagazine.Models.Entity
///<summary>
///勋章获取规则配置表
///</summary>
[SugarTable("MedalRule")]
[SugarTable("Medal_Rule")]
public partial class MedalRule : SqlSugarBaseEntity
{

View File

@ -0,0 +1,60 @@
using SqlSugar;
namespace QYZH.InteractiveMagazine.Models.Entity;
/// <summary>
/// 消息Outbox。
/// </summary>
[SugarTable("Message_Outbox")]
public partial class MessageOutbox : SqlSugarBaseEntity
{
/// <summary>
/// 交换机。
/// </summary>
public string Exchange { get; set; } = string.Empty;
/// <summary>
/// 队列。
/// </summary>
public string Queue { get; set; } = string.Empty;
/// <summary>
/// 路由键。
/// </summary>
public string RoutingKey { get; set; } = string.Empty;
/// <summary>
/// 消息内容JSON。
/// </summary>
public string Payload { get; set; } = string.Empty;
/// <summary>
/// 重试次数。
/// </summary>
public int RetryCount { get; set; }
/// <summary>
/// 下次重试时间。
/// </summary>
public DateTime? NextRetryAt { get; set; }
/// <summary>
/// 发送成功时间。
/// </summary>
public DateTime? SentAt { get; set; }
/// <summary>
/// 最后错误。
/// </summary>
public string? LastError { get; set; }
/// <summary>
/// 业务类型。
/// </summary>
public string BusinessType { get; set; } = string.Empty;
/// <summary>
/// 业务ID。
/// </summary>
public long BusinessId { get; set; }
}

View File

@ -5,7 +5,7 @@ namespace QYZH.InteractiveMagazine.Models.Entity;
/// <summary>
/// 操作日志表 — 记录后台管理员的所有手动操作
/// </summary>
[SugarTable("OperationLog")]
[SugarTable("System_OperationLog")]
public partial class OperationLog : SqlSugarBaseEntity
{
public OperationLog() { }

View File

@ -6,7 +6,7 @@ namespace QYZH.InteractiveMagazine.Models.Entity
///<summary>
///宠物进化链定义表
///</summary>
[SugarTable("PetEvolution")]
[SugarTable("Pet_Evolution")]
public partial class PetEvolution : SqlSugarBaseEntity
{
public PetEvolution() { }

View File

@ -6,7 +6,7 @@ namespace QYZH.InteractiveMagazine.Models.Entity
///<summary>
///宠物喂养记录表
///</summary>
[SugarTable("PetFeedingRecord")]
[SugarTable("Pet_FeedingRecord")]
public partial class PetFeedingRecord : SqlSugarBaseEntity
{
public PetFeedingRecord(){

View File

@ -6,7 +6,7 @@ namespace QYZH.InteractiveMagazine.Models.Entity
/// <summary>
/// 宠物皮肤定义表
/// </summary>
[SugarTable("PetSkin")]
[SugarTable("Pet_Skin")]
public partial class PetSkin : SqlSugarBaseEntity
{
public PetSkin() { }

View File

@ -6,7 +6,7 @@ namespace QYZH.InteractiveMagazine.Models.Entity
/// <summary>
/// 宠物皮肤图片表(每个皮肤在每个进化阶段下有一组有序图片,用于动态表示)
/// </summary>
[SugarTable("PetSkinImage")]
[SugarTable("Pet_SkinImage")]
public partial class PetSkinImage : SqlSugarBaseEntity
{
public PetSkinImage() { }

View File

@ -6,7 +6,7 @@ namespace QYZH.InteractiveMagazine.Models.Entity
/// <summary>
/// 宠物模板表(定义不同宠物种类,支持后期多宠物扩展)
/// </summary>
[SugarTable("PetTemplate")]
[SugarTable("Pet_Template")]
public partial class PetTemplate : SqlSugarBaseEntity
{
public PetTemplate() { }

View File

@ -6,7 +6,7 @@ namespace QYZH.InteractiveMagazine.Models.Entity
///<summary>
///积分流水记录表
///</summary>
[SugarTable("PointsRecord")]
[SugarTable("Points_Record")]
public partial class PointsRecord : SqlSugarBaseEntity
{
public PointsRecord(){

View File

@ -6,7 +6,7 @@ namespace QYZH.InteractiveMagazine.Models.Entity
///<summary>
///虚拟商品表
///</summary>
[SugarTable("Product")]
[SugarTable("Mall_Product")]
public partial class Product : SqlSugarBaseEntity
{
public Product()

View File

@ -5,7 +5,7 @@ namespace QYZH.InteractiveMagazine.Models.Entity
/// <summary>
/// 上传地址管理表
/// </summary>
[SugarTable("UploadDomain")]
[SugarTable("System_UploadDomain")]
public partial class UploadDomain : SqlSugarBaseEntity
{
/// <summary>

View File

@ -6,7 +6,7 @@ namespace QYZH.InteractiveMagazine.Models.Entity
///<summary>
///用户背包表
///</summary>
[SugarTable("UserBag")]
[SugarTable("User_Bag")]
public partial class UserBag : SqlSugarBaseEntity
{
public UserBag(){

View File

@ -6,7 +6,7 @@ namespace QYZH.InteractiveMagazine.Models.Entity
///<summary>
///用户与期刊关联表
///</summary>
[SugarTable("UserJournal")]
[SugarTable("User_Journal")]
public partial class UserJournal : SqlSugarBaseEntity
{
public UserJournal()

View File

@ -6,7 +6,7 @@ namespace QYZH.InteractiveMagazine.Models.Entity
///<summary>
///用户勋章表
///</summary>
[SugarTable("UserMedal")]
[SugarTable("User_Medal")]
public partial class UserMedal : SqlSugarBaseEntity
{
public UserMedal(){

View File

@ -6,7 +6,7 @@ namespace QYZH.InteractiveMagazine.Models.Entity
///<summary>
///用户宠物实例表
///</summary>
[SugarTable("UserPet")]
[SugarTable("User_Pet")]
public partial class UserPet : SqlSugarBaseEntity
{
public UserPet()

View File

@ -6,7 +6,7 @@ namespace QYZH.InteractiveMagazine.Models.Entity
///<summary>
///用户表 — 游戏/角色数据,关联 WxUser
///</summary>
[SugarTable("Users")]
[SugarTable("User_Account")]
public partial class Users : SqlSugarBaseEntity
{
/// <summary>

View File

@ -6,7 +6,7 @@ namespace QYZH.InteractiveMagazine.Models.Entity
/// 微信用户表 — 存储微信身份信息和基本资料
/// 一个 WxUser 可关联多个 Users多角色/多用户)
/// </summary>
[SugarTable("WxUser")]
[SugarTable("User_WechatAccount")]
public partial class WxUser : SqlSugarBaseEntity
{
public WxUser() { }

View File

@ -0,0 +1,21 @@
using System.ComponentModel;
namespace QYZH.InteractiveMagazine.Models.Enum;
/// <summary>
/// 后台角色类型枚举
/// </summary>
public enum AdminRoleTypeEnum
{
/// <summary>
/// 普通角色
/// </summary>
[Description("普通角色")]
Normal = 0,
/// <summary>
/// 系统角色
/// </summary>
[Description("系统角色")]
System = 1
}

View File

@ -0,0 +1,22 @@
using System.ComponentModel;
namespace QYZH.InteractiveMagazine.Models.Enum
{
/// <summary>
/// 作业分配审核状态枚举: 0-未审核 1-已审核
/// </summary>
public enum AssignmentStatusEnum
{
/// <summary>
/// 未审核
/// </summary>
[Description("未审核")]
UnAssignmented = 0,
/// <summary>
/// 已审核
/// </summary>
[Description("已审核")]
Assignmented = 1
}
}

View File

@ -0,0 +1,15 @@
using System.ComponentModel;
namespace QYZH.InteractiveMagazine.Models.Enum;
/// <summary>
/// 轮播图展示位置枚举
/// </summary>
public enum BannerPositionEnum
{
/// <summary>
/// 首页
/// </summary>
[Description("首页")]
Home = 1
}

View File

@ -0,0 +1,33 @@
using System.ComponentModel;
namespace QYZH.InteractiveMagazine.Models.Enum;
/// <summary>
/// 轮播图跳转类型枚举
/// </summary>
public enum BannerTargetTypeEnum
{
/// <summary>
/// 无跳转
/// </summary>
[Description("无跳转")]
None = 0,
/// <summary>
/// 链接
/// </summary>
[Description("链接")]
Url = 1,
/// <summary>
/// 期刊
/// </summary>
[Description("期刊")]
Journal = 2,
/// <summary>
/// 商品
/// </summary>
[Description("商品")]
Product = 3
}

View File

@ -0,0 +1,27 @@
using System.ComponentModel;
namespace QYZH.InteractiveMagazine.Models.Enum;
/// <summary>
/// 资料文件类型枚举
/// </summary>
public enum MaterialFileTypeEnum
{
/// <summary>
/// 视频
/// </summary>
[Description("视频")]
Video = 1,
/// <summary>
/// 图片
/// </summary>
[Description("图片")]
Image = 2,
/// <summary>
/// 音频
/// </summary>
[Description("音频")]
Audio = 3
}

View File

@ -0,0 +1,33 @@
using System.ComponentModel;
namespace QYZH.InteractiveMagazine.Models.Enum;
/// <summary>
/// 消息Outbox状态。
/// </summary>
public enum MessageOutboxStatusEnum
{
/// <summary>
/// 待发送。
/// </summary>
[Description("待发送")]
Pending = 0,
/// <summary>
/// 已发送。
/// </summary>
[Description("已发送")]
Sent = 1,
/// <summary>
/// 发送失败待重试。
/// </summary>
[Description("发送失败待重试")]
Failed = 2,
/// <summary>
/// 已放弃。
/// </summary>
[Description("已放弃")]
Abandoned = 3
}

View File

@ -0,0 +1,12 @@
namespace QYZH.InteractiveMagazine.Models.Settings;
/// <summary>
/// Hangfire存储配置。
/// </summary>
public class HangfireStorageSettings
{
/// <summary>
/// 存储类型,当前默认 Memory可配置为 Redis。
/// </summary>
public string StorageType { get; set; } = "Memory";
}

View File

@ -0,0 +1,17 @@
namespace QYZH.InteractiveMagazine.Models.Settings;
/// <summary>
/// RabbitMQ重试配置。
/// </summary>
public class RabbitMQRetrySettings
{
/// <summary>
/// 最大重试次数。
/// </summary>
public int MaxRetryCount { get; set; } = 3;
/// <summary>
/// 重试延迟毫秒数。
/// </summary>
public int RetryDelayMilliseconds { get; set; } = 30000;
}

View File

@ -0,0 +1,12 @@
namespace QYZH.InteractiveMagazine.Models.Settings;
/// <summary>
/// 雪花ID配置。
/// </summary>
public class SnowflakeSettings
{
/// <summary>
/// WorkerId生产部署时每个写库进程必须唯一。
/// </summary>
public ushort WorkerId { get; set; }
}

View File

@ -47,10 +47,13 @@ public class AutoDotCodeConsumer(
using var scope = scopeFactory.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<ISqlSugarClient>();
JournalPagePrintDto? request = null;
List<long> reservedDotDetailIds = [];
var dotPagesCommitted = false;
try
{
var request = JsonSerializer.Deserialize<JournalPagePrintDto>(message, new JsonSerializerOptions
request = JsonSerializer.Deserialize<JournalPagePrintDto>(message, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
@ -72,6 +75,7 @@ public class AutoDotCodeConsumer(
if (string.IsNullOrWhiteSpace(journalPdfKey))
{
logger.LogError("书籍上传 PDF 文件地址为空JournalId: {JournalId}", request.JournalId);
await TryCallbackCodeFailAsync(request, cancellationToken);
return;
}
@ -79,6 +83,7 @@ public class AutoDotCodeConsumer(
if (pdfStream == null)
{
logger.LogError("获取书籍上传 PDF 文件失败OSS Key: {OssKey}", journalPdfKey);
await TryCallbackCodeFailAsync(request, cancellationToken);
return;
}
@ -93,6 +98,7 @@ public class AutoDotCodeConsumer(
if (dotFile == null)
{
logger.LogError("配置打印数据错误点阵文件不存在DotId: {DotId}", dotId);
await TryCallbackCodeFailAsync(request, cancellationToken);
return;
}
@ -101,7 +107,7 @@ public class AutoDotCodeConsumer(
if (!File.Exists(exePath))
{
logger.LogError("PrintTool.exe 不存在,路径:{ExePath}", exePath);
await ExecuteUpdateJournalStatusAsync(BuildFailResponse(request), cancellationToken);
await TryCallbackCodeFailAsync(request, cancellationToken);
return;
}
@ -109,7 +115,7 @@ public class AutoDotCodeConsumer(
if (!File.Exists(xmlPath))
{
logger.LogError("铺码授权文件不存在,路径:{XmlPath}", xmlPath);
await ExecuteUpdateJournalStatusAsync(BuildFailResponse(request), cancellationToken);
await TryCallbackCodeFailAsync(request, cancellationToken);
return;
}
@ -126,11 +132,26 @@ public class AutoDotCodeConsumer(
if (dotFileDetailList.Count < pageNumMax)
{
logger.LogError("点阵页码余量不足DotId: {DotId}, Need: {Need}, Available: {Available}", dotId, pageNumMax, dotFileDetailList.Count);
await ExecuteUpdateJournalStatusAsync(BuildFailResponse(request), cancellationToken);
await TryCallbackCodeFailAsync(request, cancellationToken);
return;
}
var dotFileDetailPageName = dotFileDetailList.Select(x => x.PageName).OrderBy(x => x).ToArray();
reservedDotDetailIds = dotFileDetailList.Select(x => x.Id).ToList();
var reservedRows = await dbContext.Updateable<DotFileDetail>()
.SetColumns(x => x.IsUse == true)
.SetColumns(x => x.UpdatedAt == DateTime.Now)
.Where(x => reservedDotDetailIds.Contains(x.Id) && !x.IsUse)
.ExecuteCommandAsync(cancellationToken);
if (reservedRows != reservedDotDetailIds.Count)
{
logger.LogError("点阵页码预占失败DotId: {DotId}, Need: {Need}, Reserved: {Reserved}", dotId, reservedDotDetailIds.Count, reservedRows);
await ReleaseReservedDotPagesAsync(dbContext, reservedDotDetailIds, cancellationToken);
reservedDotDetailIds.Clear();
await TryCallbackCodeFailAsync(request, cancellationToken, dotFileDetailPageName);
return;
}
var pageStr = "{" + $"[{dotFileDetailList[0].PageName},{pageNumMax}]" + "}";
var arguments = $"-sMode=Generate -sPDF=\"{uploadFilePath}\" -sLIC=\"{xmlPath}\" -pStart=1 -oPDF=\"{downloadFilePath}\" -dPageAddr=1 -dPrint={dPrint} -dDotSize=40 -dType=0 -dOutFile=0 -dControlPageNum={pageStr}";
var printResult = await ExecutePrintToolAsync(exePath, printToolDirectory, arguments, cancellationToken);
@ -138,7 +159,9 @@ public class AutoDotCodeConsumer(
if (printResult.Timeout || printResult.ExitCode != 0)
{
logger.LogError("执行 PrintTool.exe 失败,退出码:{ExitCode},错误信息:{ErrorMessage}", printResult.ExitCode, printResult.Error);
await ExecuteUpdateJournalStatusAsync(BuildFailResponse(request, dotFileDetailPageName), cancellationToken);
await ReleaseReservedDotPagesAsync(dbContext, reservedDotDetailIds, cancellationToken);
reservedDotDetailIds.Clear();
await TryCallbackCodeFailAsync(request, cancellationToken, dotFileDetailPageName);
return;
}
@ -150,8 +173,9 @@ public class AutoDotCodeConsumer(
if (!ValidatePrintOutput(printResult.Output, dotFileDetailPageName, dPrint))
{
statusModel.Status = JournalStatusEnum.CodeFail;
await ExecuteUpdateJournalStatusAsync(statusModel, cancellationToken);
await ReleaseReservedDotPagesAsync(dbContext, reservedDotDetailIds, cancellationToken);
reservedDotDetailIds.Clear();
await TryCallbackCodeFailAsync(request, cancellationToken, dotFileDetailPageName);
return;
}
@ -186,16 +210,32 @@ public class AutoDotCodeConsumer(
.Where(x => x.Id == dotId)
.ExecuteCommandAsync();
});
dotPagesCommitted = true;
}
else
{
await ReleaseReservedDotPagesAsync(dbContext, reservedDotDetailIds, cancellationToken);
reservedDotDetailIds.Clear();
logger.LogError("回调接口修改书籍状态失败JournalId: {JournalId},接口返回消息:{Message}", statusModel.JournalId, callbackResponse.Message);
}
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
logger.LogWarning("自动铺码处理已取消JournalId: {JournalId}", request?.JournalId);
throw;
}
catch (Exception ex)
{
logger.LogError(ex, "自动铺码处理失败");
throw;
if (request != null)
{
if (!dotPagesCommitted && reservedDotDetailIds.Count > 0)
{
await ReleaseReservedDotPagesAsync(dbContext, reservedDotDetailIds, CancellationToken.None);
}
await TryCallbackCodeFailAsync(request, CancellationToken.None);
}
}
}
@ -298,6 +338,40 @@ public class AutoDotCodeConsumer(
return new CallbackUpdateJournalStatusResponse();
}
private async Task TryCallbackCodeFailAsync(JournalPagePrintDto request, CancellationToken cancellationToken, string[]? pageNo = null)
{
try
{
var callbackResponse = await ExecuteUpdateJournalStatusAsync(BuildFailResponse(request, pageNo), cancellationToken);
if (!callbackResponse.IsSuccess)
{
logger.LogError("回调接口修改书籍为铺码失败状态失败JournalId: {JournalId},接口返回消息:{Message}", request.JournalId, callbackResponse.Message);
}
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
throw;
}
catch (Exception callbackEx)
{
logger.LogError(callbackEx, "铺码失败后请求回调接口失败JournalId: {JournalId}", request.JournalId);
}
}
private static async Task ReleaseReservedDotPagesAsync(ISqlSugarClient dbContext, List<long> dotDetailIds, CancellationToken cancellationToken)
{
if (dotDetailIds.Count == 0)
{
return;
}
await dbContext.Updateable<DotFileDetail>()
.SetColumns(x => x.IsUse == false)
.SetColumns(x => x.UpdatedAt == DateTime.Now)
.Where(x => dotDetailIds.Contains(x.Id))
.ExecuteCommandAsync(cancellationToken);
}
private static JournalPagePrintDto BuildFailResponse(JournalPagePrintDto request, string[]? pageNo = null)
{
return new JournalPagePrintDto

View File

@ -55,7 +55,7 @@ public class RabbitMQHostedService(IServiceProvider serviceProvider, ILogger<Rab
catch (Exception ex)
{
logger.LogError(ex, "消费者 {QueueName} 处理消息异常", queueName);
var dlqSent = await SendToDeadLetterQueueAsync(exchange, dlqName, dlqRoutingKey, ea.Body.ToArray(), stoppingToken);
var dlqSent = await SendToDeadLetterQueueAsync(exchange, dlqName, dlqRoutingKey, queueName, ea.Body.ToArray(), ex, stoppingToken);
await channel.BasicNackAsync(ea.DeliveryTag, false, !dlqSent, stoppingToken);
try
@ -79,7 +79,7 @@ public class RabbitMQHostedService(IServiceProvider serviceProvider, ILogger<Rab
}
}
private async Task<bool> SendToDeadLetterQueueAsync(string exchange, string dlqName, string dlqRoutingKey, byte[] body, CancellationToken cancellationToken)
private async Task<bool> SendToDeadLetterQueueAsync(string exchange, string dlqName, string dlqRoutingKey, string originalQueueName, byte[] body, Exception exception, CancellationToken cancellationToken)
{
try
{
@ -90,7 +90,14 @@ public class RabbitMQHostedService(IServiceProvider serviceProvider, ILogger<Rab
var properties = new RabbitMQ.Client.BasicProperties
{
Persistent = true
Persistent = true,
Headers = new Dictionary<string, object?>
{
["x-original-queue"] = originalQueueName,
["x-error-type"] = exception.GetType().FullName,
["x-error-message"] = exception.Message,
["x-failed-at"] = DateTimeOffset.UtcNow.ToString("O")
}
};
await channel.BasicPublishAsync(exchange, dlqRoutingKey, false, properties, body, cancellationToken);
logger.LogInformation("消息已发送到死信队列: {DlqName}", dlqName);

View File

@ -1,6 +1,7 @@
using QYZH.InteractiveMagazine.Infrastructure.RabbitMQ;
using QYZH.InteractiveMagazine.Infrastructure.Redis;
using QYZH.InteractiveMagazine.Infrastructure.SDK;
using QYZH.InteractiveMagazine.Models.Settings;
using QYZH.InteractiveMagazine.PrintWorker.Consumers;
using Serilog;
using SqlSugar;
@ -34,7 +35,8 @@ Log.Logger = new LoggerConfiguration()
builder.Services.AddSerilog();
builder.Services.AddWindowsService(options => options.ServiceName = serviceName);
YitIdHelper.SetIdGenerator(new IdGeneratorOptions { WorkerId = 3 });
var snowflakeSettings = builder.Configuration.GetSection("SnowflakeSettings").Get<SnowflakeSettings>() ?? new SnowflakeSettings { WorkerId = 3 };
YitIdHelper.SetIdGenerator(new IdGeneratorOptions { WorkerId = snowflakeSettings.WorkerId });
builder.Services.AddSqlSugar(new IocConfig
{

View File

@ -2,6 +2,9 @@
"ConnectionStrings": {
"DefaultConnection": "server=192.168.20.150;port=13306;database=InteractiveMagazine;user=user;password=n68792bu!y99r905;charset=utf8mb4;"
},
"SnowflakeSettings": {
"WorkerId": 3
},
"RedisSettings": {
"ConnectionString": "192.168.20.150:16379,defaultDatabase=5",
"Sentinels": [],
@ -14,6 +17,10 @@
"Password": "@ss%&*otz%d*pq2S",
"VirtualHost": "InteractiveMagazine"
},
"RabbitMQRetrySettings": {
"MaxRetryCount": 3,
"RetryDelayMilliseconds": 30000
},
"Serilog": {
"MinimumLevel": {
"Default": "Information",

View File

@ -253,9 +253,9 @@ namespace QYZH.InteractiveMagazine.Repository
var total = 0;
page.PageSize = parm.PageSize;
page.PageIndex = parm.PageIndex;
if (string.IsNullOrEmpty(parm.Sort))
if (!string.IsNullOrWhiteSpace(parm.Sort))
{
source.OrderByPropertyName(parm.Sort, parm.SortType.Contains("desc") ? OrderByType.Desc : OrderByType.Asc);
source.OrderByPropertyName(parm.Sort, parm.SortType?.Contains("desc", StringComparison.OrdinalIgnoreCase) == true ? OrderByType.Desc : OrderByType.Asc);
}
page.Result = source
//.OrderByIF(parm.Sort.IsNotEmpty(), $"{parm.Sort.ToSqlFilter()} {(!string.IsNullOrWhiteSpace(parm.SortType) && parm.SortType.Contains("desc") ? "desc" : "asc")}")
@ -278,9 +278,9 @@ namespace QYZH.InteractiveMagazine.Repository
var total = 0;
page.PageSize = parm.PageSize;
page.PageIndex = parm.PageIndex;
if (string.IsNullOrEmpty(parm.Sort))
if (!string.IsNullOrWhiteSpace(parm.Sort))
{
source.OrderByPropertyName(parm.Sort, parm.SortType.Contains("desc") ? OrderByType.Desc : OrderByType.Asc);
source.OrderByPropertyName(parm.Sort, parm.SortType?.Contains("desc", StringComparison.OrdinalIgnoreCase) == true ? OrderByType.Desc : OrderByType.Asc);
}
var result = source
//.OrderByIF(parm.Sort.IsNotEmpty(), $"{parm.Sort.ToSqlFilter()} {(!string.IsNullOrWhiteSpace(parm.SortType) && parm.SortType.Contains("desc") ? "desc" : "asc")}")

View File

@ -128,6 +128,7 @@ public class AdminPermissionService(
{
Name = input.Name.Trim(),
Code = input.Code.Trim(),
Type = input.Type,
Remark = input.Remark?.Trim(),
Status = input.Status,
CreatedBy = "System",
@ -157,9 +158,11 @@ public class AdminPermissionService(
BusinessException.ThrowIf(role == null, "角色不存在", ResultCode.NOT_FOUND);
await ValidateRoleInputAsync(input, id);
BusinessException.ThrowIf(role!.Type == AdminRoleTypeEnum.System && input.Type != AdminRoleTypeEnum.System, "系统角色不能修改为普通角色", ResultCode.FORBIDDEN);
role!.Name = input.Name.Trim();
role.Name = input.Name.Trim();
role.Code = input.Code.Trim();
role.Type = input.Type;
role.Remark = input.Remark?.Trim();
role.Status = input.Status;
role.UpdatedBy = "System";
@ -182,6 +185,7 @@ public class AdminPermissionService(
public async Task DeleteRoleAsync(long id)
{
var role = await adminRoleRepository.GetByIdAsync(id);
BusinessException.ThrowIf(role?.Type == AdminRoleTypeEnum.System, "系统角色不允许删除", ResultCode.FORBIDDEN);
BusinessException.ThrowIf(role == null, "角色不存在", ResultCode.NOT_FOUND);
var usedByUser = await adminUserRoleRepository.Queryable().AnyAsync(x => x.RoleId == id && !x.IsDeleted);
@ -221,6 +225,7 @@ public class AdminPermissionService(
.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.Type.HasValue, x => x.Type == input.Type!.Value)
.WhereIF(input.Status.HasValue, x => x.Status == input.Status!.Value)
.OrderByDescending(x => x.CreatedAt)
.ToPageListAsync(input.PageIndex, input.PageSize, totalNumber);
@ -283,7 +288,8 @@ public class AdminPermissionService(
{
Id = role.Id,
Name = role.Name,
Code = role.Code
Code = role.Code,
Type = role.Type
})
.ToListAsync();
}
@ -490,6 +496,7 @@ public class AdminPermissionService(
Id = role.Id,
Name = role.Name,
Code = role.Code,
Type = role.Type,
Remark = role.Remark,
Status = role.Status,
MenuIds = menuIds,

View File

@ -0,0 +1,244 @@
using Microsoft.Extensions.Logging;
using QYZH.InteractiveMagazine.Infrastructure.OSS;
using QYZH.InteractiveMagazine.IService;
using QYZH.InteractiveMagazine.Models.Common;
using QYZH.InteractiveMagazine.Models.Dto;
using QYZH.InteractiveMagazine.Models.Dto.Banner;
using QYZH.InteractiveMagazine.Models.Entity;
using QYZH.InteractiveMagazine.Models.Enum;
using QYZH.InteractiveMagazine.Repository;
using SqlSugar;
namespace QYZH.InteractiveMagazine.Service;
/// <summary>
/// 轮播图服务实现
/// </summary>
public class BannerService(
BaseRepository<Banner> bannerRepository,
OssService ossService,
ILogger<BannerService> logger) : BaseRepository<Banner>, IBannerService
{
/// <summary>
/// 创建轮播图
/// </summary>
public async Task<BannerOutput> CreateAsync(BannerInput input)
{
ValidateInput(input);
var banner = new Banner
{
Title = input.Title.Trim(),
ImageUrl = input.ImageUrl.Trim(),
Position = input.Position,
TargetType = input.TargetType,
TargetValue = NormalizeTargetValue(input),
Sort = input.Sort,
StartTime = input.StartTime,
EndTime = input.EndTime,
Remark = input.Remark,
Status = (int)DefaultStatusEnum.Active,
IsDeleted = false,
CreatedBy = "System",
CreatedAt = DateTime.Now,
UpdatedBy = "System",
UpdatedAt = DateTime.Now
};
var result = await bannerRepository.InsertAsync(banner);
BusinessException.ThrowIf(!result, "创建轮播图失败", ResultCode.GLOBAL_ERROR);
if (OssImageHelper.IsTempImage(banner.ImageUrl))
{
var helper = CreateImageHelper();
banner.ImageUrl = await helper.MoveToFormalAsync(banner.ImageUrl, GetBannerFormalFolder(banner.Position, banner.Id));
await bannerRepository.UpdateAsync(banner);
}
logger.LogInformation("轮播图创建成功:{Id}", banner.Id);
return MapToOutput(banner);
}
/// <summary>
/// 更新轮播图
/// </summary>
public async Task<BannerOutput> UpdateAsync(long id, BannerInput input)
{
ValidateInput(input);
var banner = await GetValidBannerAsync(id);
var newImageUrl = input.ImageUrl.Trim();
if (OssImageHelper.IsTempImage(newImageUrl))
{
var helper = CreateImageHelper();
newImageUrl = await helper.MoveToFormalAsync(newImageUrl, GetBannerFormalFolder(input.Position, banner.Id));
}
banner.Title = input.Title.Trim();
banner.ImageUrl = newImageUrl;
banner.Position = input.Position;
banner.TargetType = input.TargetType;
banner.TargetValue = NormalizeTargetValue(input);
banner.Sort = input.Sort;
banner.StartTime = input.StartTime;
banner.EndTime = input.EndTime;
banner.Remark = input.Remark;
banner.UpdatedBy = "System";
banner.UpdatedAt = DateTime.Now;
var result = await bannerRepository.UpdateAsync(banner);
BusinessException.ThrowIf(!result, "更新轮播图失败", ResultCode.GLOBAL_ERROR);
logger.LogInformation("轮播图更新成功:{Id}", banner.Id);
return MapToOutput(banner);
}
/// <summary>
/// 删除轮播图
/// </summary>
public async Task DeleteAsync(long id)
{
await GetValidBannerAsync(id);
var result = await bannerRepository.Context.Updateable<Banner>()
.SetColumns(b => new Banner
{
IsDeleted = true,
UpdatedBy = "System",
UpdatedAt = DateTime.Now
})
.Where(b => b.Id == id && !b.IsDeleted)
.ExecuteCommandAsync();
BusinessException.ThrowIf(result <= 0, "删除轮播图失败", ResultCode.GLOBAL_ERROR);
logger.LogInformation("轮播图删除成功:{Id}", id);
}
/// <summary>
/// 根据ID获取轮播图
/// </summary>
public async Task<BannerOutput> GetByIdAsync(long id)
{
var banner = await GetValidBannerAsync(id);
return MapToOutput(banner);
}
/// <summary>
/// 分页查询轮播图列表
/// </summary>
public async Task<PageListModel<BannerOutput>> GetListAsync(BannerQueryInput input)
{
BusinessException.ThrowIf(input.PageIndex <= 0, "页码必须大于0", ResultCode.BAD_REQUEST);
BusinessException.ThrowIf(input.PageSize <= 0 || input.PageSize > 100, "每页条数必须在1-100之间", ResultCode.BAD_REQUEST);
RefAsync<int> totalNumber = 0;
var banners = await bannerRepository.Queryable()
.Where(b => !b.IsDeleted)
.WhereIF(!string.IsNullOrWhiteSpace(input.Title), b => b.Title.Contains(input.Title!.Trim()))
.WhereIF(input.Position.HasValue, b => b.Position == input.Position!.Value)
.WhereIF(input.Status.HasValue, b => b.Status == input.Status!.Value)
.OrderBy(b => b.Sort)
.OrderByDescending(b => b.CreatedAt)
.ToPageListAsync(input.PageIndex, input.PageSize, totalNumber);
var result = banners.Select(MapToOutput).ToList();
return new PageListModel<BannerOutput>(result, input.PageIndex, input.PageSize, totalNumber);
}
/// <summary>
/// 更新轮播图启用/禁用状态
/// </summary>
public async Task<bool> UpdateStatusAsync(long id)
{
var banner = await GetValidBannerAsync(id);
banner.Status = banner.Status == (int)DefaultStatusEnum.Active
? (int)DefaultStatusEnum.Inactive
: (int)DefaultStatusEnum.Active;
banner.UpdatedBy = "System";
banner.UpdatedAt = DateTime.Now;
var result = await bannerRepository.UpdateAsync(banner);
BusinessException.ThrowIf(!result, "更新轮播图状态失败", ResultCode.GLOBAL_ERROR);
logger.LogInformation("轮播图状态更新成功:{Id},状态:{Status}", id, banner.Status);
return result;
}
/// <summary>
/// 获取启用轮播图列表
/// </summary>
public async Task<List<BannerOutput>> GetEnabledListAsync(BannerPositionEnum position)
{
var now = DateTime.Now;
var banners = await bannerRepository.Queryable()
.Where(b => !b.IsDeleted)
.Where(b => b.Status == (int)DefaultStatusEnum.Active)
.Where(b => b.Position == position)
.Where(b => b.StartTime == null || b.StartTime <= now)
.Where(b => b.EndTime == null || b.EndTime >= now)
.OrderBy(b => b.Sort)
.OrderByDescending(b => b.CreatedAt)
.ToListAsync();
return banners.Select(MapToOutput).ToList();
}
private async Task<Banner> GetValidBannerAsync(long id)
{
var banner = await bannerRepository.Queryable()
.Where(b => b.Id == id && !b.IsDeleted)
.FirstAsync();
BusinessException.ThrowIf(banner == null, "轮播图不存在", ResultCode.NOT_FOUND);
return banner;
}
private static void ValidateInput(BannerInput input)
{
BusinessException.ThrowIf(string.IsNullOrWhiteSpace(input.Title), "标题不能为空", ResultCode.BAD_REQUEST);
BusinessException.ThrowIf(input.Title.Trim().Length > 100, "标题长度不能超过100个字符", ResultCode.BAD_REQUEST);
BusinessException.ThrowIf(string.IsNullOrWhiteSpace(input.ImageUrl), "轮播图不能为空", ResultCode.BAD_REQUEST);
BusinessException.ThrowIf(input.ImageUrl.Trim().Length > 500, "轮播图路径长度不能超过500个字符", ResultCode.BAD_REQUEST);
BusinessException.ThrowIf(input.Sort < 0, "排序值不能小于0", ResultCode.BAD_REQUEST);
BusinessException.ThrowIf(input.StartTime.HasValue && input.EndTime.HasValue && input.EndTime <= input.StartTime, "结束时间必须大于开始时间", ResultCode.BAD_REQUEST);
BusinessException.ThrowIf(input.TargetType != BannerTargetTypeEnum.None && string.IsNullOrWhiteSpace(input.TargetValue), "跳转值不能为空", ResultCode.BAD_REQUEST);
BusinessException.ThrowIf(!string.IsNullOrWhiteSpace(input.TargetValue) && input.TargetValue.Length > 500, "跳转值长度不能超过500个字符", ResultCode.BAD_REQUEST);
BusinessException.ThrowIf(!string.IsNullOrWhiteSpace(input.Remark) && input.Remark.Length > 500, "备注长度不能超过500个字符", ResultCode.BAD_REQUEST);
}
private static string? NormalizeTargetValue(BannerInput input)
{
return input.TargetType == BannerTargetTypeEnum.None
? null
: input.TargetValue?.Trim();
}
private OssImageHelper CreateImageHelper() => new(ossService, logger);
private static string GetBannerFormalFolder(BannerPositionEnum position, long bannerId)
{
return $"banner/{position.ToString().ToLowerInvariant()}/{bannerId}";
}
private static BannerOutput MapToOutput(Banner banner)
{
return new BannerOutput
{
Id = banner.Id,
Title = banner.Title,
ImageUrl = banner.ImageUrl,
Position = banner.Position,
TargetType = banner.TargetType,
TargetValue = banner.TargetValue,
Sort = banner.Sort,
StartTime = banner.StartTime,
EndTime = banner.EndTime,
Remark = banner.Remark,
Status = banner.Status,
CreatedBy = banner.CreatedBy,
CreatedAt = banner.CreatedAt,
UpdatedBy = banner.UpdatedBy,
UpdatedAt = banner.UpdatedAt
};
}
}

View File

@ -74,6 +74,14 @@ public class CheckInService(
await checkInRecordRepository.UseTranAsync(async () =>
{
var duplicate = await checkInRecordRepository.Context.Queryable<CheckInRecord>()
.Where(r => r.UserId == userId && !r.IsDeleted && r.CheckInDate >= today && r.CheckInDate < today.AddDays(1))
.AnyAsync();
if (duplicate)
{
throw new BusinessException("今日已签到,请明天再来", ResultCode.CONFLICT);
}
// 6a. 创建签到记录
var checkInRecord = new CheckInRecord
{
@ -94,10 +102,16 @@ public class CheckInService(
var newGrowthBalance = user.GrowthPoints + growthReward;
await checkInRecordRepository.Context.Updateable<Users>()
.SetColumns(u => u.GrowthPoints == newGrowthBalance)
.SetColumns(u => u.GrowthPoints == u.GrowthPoints + growthReward)
.SetColumns(u => u.UpdatedAt == DateTime.Now)
.Where(u => u.Id == userId && !u.IsDeleted)
.ExecuteCommandAsync();
newGrowthBalance = await checkInRecordRepository.Context.Queryable<Users>()
.Where(u => u.Id == userId && !u.IsDeleted)
.Select(u => u.GrowthPoints)
.FirstAsync();
// 6c. 通过积分服务增加积分
var pointsResult = await pointsService.AddPointsInTranAsync(new AddPointsInput
{
@ -271,6 +285,14 @@ public class CheckInService(
await checkInRecordRepository.UseTranAsync(async () =>
{
var duplicate = await checkInRecordRepository.Context.Queryable<CheckInRecord>()
.Where(r => r.UserId == userId && !r.IsDeleted && r.CheckInDate >= targetDate && r.CheckInDate < targetDate.AddDays(1))
.AnyAsync();
if (duplicate)
{
throw new BusinessException($"{targetDate:yyyy-MM-dd} 已签到,无需补签", ResultCode.CONFLICT);
}
// 创建补签记录
var checkInRecord = new CheckInRecord
{
@ -290,14 +312,17 @@ public class CheckInService(
var recordId = await checkInRecordRepository.Insertable(checkInRecord).ExecuteReturnIdentityAsync();
checkInRecord.Id = recordId;
// 更新用户成长值
var newGrowthBalance = user.GrowthPoints + growthReward;
await checkInRecordRepository.Context.Updateable<Users>()
.SetColumns(u => u.GrowthPoints == newGrowthBalance)
.SetColumns(u => u.GrowthPoints == u.GrowthPoints + growthReward)
.SetColumns(u => u.UpdatedAt == DateTime.Now)
.Where(u => u.Id == userId && !u.IsDeleted)
.ExecuteCommandAsync();
var newGrowthBalance = await checkInRecordRepository.Context.Queryable<Users>()
.Where(u => u.Id == userId && !u.IsDeleted)
.Select(u => u.GrowthPoints)
.FirstAsync();
// 通过积分服务增加积分
var pointsResult = await pointsService.AddPointsInTranAsync(new AddPointsInput
{
@ -311,19 +336,20 @@ public class CheckInService(
// 扣减补签卡
if (makeUpCard.Quantity <= 1)
{
var cardAffectedRows = await checkInRecordRepository.Context.Updateable<UserBag>()
.SetColumns(b => b.Quantity == b.Quantity - 1)
.SetColumns(b => b.UpdatedAt == DateTime.Now)
.Where(b => b.Id == makeUpCard.Id && b.UserId == userId && b.Quantity > 0 && b.Status == (int)UserBagStatusEnum.Available && !b.IsDeleted)
.ExecuteCommandAsync();
if (cardAffectedRows <= 0)
{
throw new BusinessException("补签卡不足,无法补签", ResultCode.CONFLICT);
}
await checkInRecordRepository.Context.Updateable<UserBag>()
.SetColumns(b => b.Status == (int)UserBagStatusEnum.Expired)
.SetColumns(b => b.Quantity == 0)
.SetColumns(b => b.UpdatedAt == DateTime.Now)
.Where(b => b.Id == makeUpCard.Id)
.ExecuteCommandAsync();
}
else
{
await checkInRecordRepository.Context.Updateable<UserBag>()
.SetColumns(b => b.Quantity == makeUpCard.Quantity - 1)
.SetColumns(b => b.UpdatedAt == DateTime.Now)
.Where(b => b.Id == makeUpCard.Id)
.Where(b => b.Id == makeUpCard.Id && b.Quantity <= 0 && !b.IsDeleted)
.ExecuteCommandAsync();
}

View File

@ -98,11 +98,16 @@ public class CompensationManageService(
UserId = task.UserId
});
await operationLogService.LogAsync(
operatorId, operatorName,
OperationLogActionType.CompensationRetry,
OperationLogTargetType.CompensationTask,
taskId, null, detail, ipAddress);
await operationLogService.LogAsync(new OperationLogRecordInput
{
OperatorId = operatorId,
OperatorName = operatorName,
ActionType = OperationLogActionType.CompensationRetry,
TargetType = OperationLogTargetType.CompensationTask,
TargetId = taskId,
Detail = detail,
IpAddress = ipAddress
});
logger.LogInformation("补偿任务手动重试成功TaskId: {TaskId}", taskId);
}
@ -140,11 +145,16 @@ public class CompensationManageService(
UserId = task.UserId
});
await operationLogService.LogAsync(
operatorId, operatorName,
OperationLogActionType.CompensationResolve,
OperationLogTargetType.CompensationTask,
taskId, null, detail, ipAddress);
await operationLogService.LogAsync(new OperationLogRecordInput
{
OperatorId = operatorId,
OperatorName = operatorName,
ActionType = OperationLogActionType.CompensationResolve,
TargetType = OperationLogTargetType.CompensationTask,
TargetId = taskId,
Detail = detail,
IpAddress = ipAddress
});
logger.LogInformation("补偿任务标记已解决成功TaskId: {TaskId}", taskId);
}

View File

@ -11,6 +11,7 @@ using QYZH.InteractiveMagazine.IService;
using QYZH.InteractiveMagazine.Models.Common;
using QYZH.InteractiveMagazine.Models.Dto;
using QYZH.InteractiveMagazine.Models.Dto.Journal;
using QYZH.InteractiveMagazine.Models.Dto.RabbitMQ;
using QYZH.InteractiveMagazine.Models.Entity;
using QYZH.InteractiveMagazine.Models.Enum;
using QYZH.InteractiveMagazine.Repository;
@ -24,7 +25,7 @@ public class JournalPageService(BaseRepository<Journal> JournalRepository,
OssService ossService,
IHttpClientFactory httpClientFactory,
IConfiguration configuration,
IRabbitMQService rabbitMqService,
IMessagePublishService messagePublishService,
ILogger<AiBasePromptService> logger,
BaseRepository<JournalPage> JournalPageRepository,
BaseRepository<JournalPageTask> JournalPageTaskRepository,
@ -135,18 +136,29 @@ public class JournalPageService(BaseRepository<Journal> JournalRepository,
var isPdfExists = ossService.DoesObjectExist(uploadPdfKey);
BusinessException.ThrowIf(!isPdfExists, "获取期刊上传的PDF文件失败请检查PDF文件是否上传成功", ResultCode.GLOBAL_ERROR);
journalEntity.Status = (int)JournalStatusEnum.Codeing;
await JournalRepository.Updateable(journalEntity).UpdateColumns(x => new { x.Status, x.UpdatedAt }).ExecuteCommandAsync();
var data = new JournalPagePrintDto
{
JournalId = JournalId,
JournalPdfUrl = uploadPdfUrl,
PageNum = [.. journalPageList.Select(x => x.PageNum)]
};
var msRes = await rabbitMqService.SendAsync(new RabbitMQSendParam { Exchange = "ex.journal", Queue = "mq.journal.dotcode.auto", RoutingKey = "rk.journal.dotcode.auto", Data = data });//发生消息
return msRes;
return await UseTranAsync(async () =>
{
journalEntity.Status = (int)JournalStatusEnum.Codeing;
journalEntity.UpdatedAt = DateTime.Now;
await JournalRepository.Updateable(journalEntity).UpdateColumns(x => new { x.Status, x.UpdatedAt }).ExecuteCommandAsync();
await messagePublishService.PublishAsync(new MessagePublishInput<JournalPagePrintDto>
{
Exchange = "ex.journal",
Queue = "mq.journal.dotcode.auto",
RoutingKey = "rk.journal.dotcode.auto",
Data = data,
BusinessType = "JournalDotCode",
BusinessId = JournalId
});
return true;
});
}
/// <summary>
/// 回调接口-自动铺码, 书页铺码后回调接口,更新书页的点阵码

View File

@ -9,10 +9,12 @@ using QYZH.InteractiveMagazine.Models.Common;
using QYZH.InteractiveMagazine.Models.Dto;
using QYZH.InteractiveMagazine.Models.Dto.DotMatrix;
using QYZH.InteractiveMagazine.Models.Dto.Journal;
using QYZH.InteractiveMagazine.Models.Dto.RabbitMQ;
using QYZH.InteractiveMagazine.Models.Entity;
using QYZH.InteractiveMagazine.Models.Enum;
using QYZH.InteractiveMagazine.Repository;
using SqlSugar;
using System.Text.Json;
using Yitter.IdGenerator;
namespace QYZH.InteractiveMagazine.Service;
@ -24,7 +26,7 @@ public class JournalService(BaseRepository<JournalPage> JournalPageRepository,
BaseRepository<DotFile> dotFileRepository,
BaseRepository<DotFileDetail> dotFileDetailRepository,
OssService ossService,
IRabbitMQService rabbitMqService,
IMessagePublishService messagePublishService,
ILogger<JournalService> logger) : BaseRepository<Journal>, IJournalService
{
private const string JournalExchange = "ex.journal";
@ -375,23 +377,27 @@ public class JournalService(BaseRepository<JournalPage> JournalPageRepository,
x => x.Key,
x => string.Join(',', x.OrderBy(q => ParseTaskNo(q.No)).Select(q => q.Id)));
var bookMessageSent = await rabbitMqService.SendAsync(new RabbitMQSendParam
var publishMessages = new List<MessagePublishInput<object>>
{
Exchange = JournalExchange,
Queue = PublishBookQueue,
RoutingKey = PublishBookRoutingKey,
Data = new JournalPublishBookMessage
new()
{
BookId = id,
StartTime = book.StartTime.Value,
EndTime = book.EndTime.Value
Exchange = JournalExchange,
Queue = PublishBookQueue,
RoutingKey = PublishBookRoutingKey,
Data = new JournalPublishBookMessage
{
BookId = id,
StartTime = book.StartTime.Value,
EndTime = book.EndTime.Value
},
BusinessType = "JournalPublish",
BusinessId = id
}
});
BusinessException.ThrowIf(!bookMessageSent, "发布书籍消息发送失败", ResultCode.GLOBAL_ERROR);
};
foreach (var page in pages)
{
var pageMessageSent = await rabbitMqService.SendAsync(new RabbitMQSendParam
publishMessages.Add(new MessagePublishInput<object>
{
Exchange = JournalExchange,
Queue = PublishBookPageQueue,
@ -404,16 +410,21 @@ public class JournalService(BaseRepository<JournalPage> JournalPageRepository,
Layout = page.Layout,
Url = DomainHelper.OssFullUrl(page.Url),
QuestionNo = questionIdsByPageId.GetValueOrDefault(page.Id) ?? string.Empty
}
},
BusinessType = "JournalPublish",
BusinessId = id
});
BusinessException.ThrowIf(!pageMessageSent, $"发布书页消息发送失败PageId: {page.Id}", ResultCode.GLOBAL_ERROR);
}
return await base.Updateable()
.SetColumns(s => s.Status, JournalStatusEnum.Published)
.SetColumns(s => s.UpdatedAt, DateTime.Now)
.Where(w => w.Id == id)
.ExecuteCommandAsync() > 0;
return await UseTranAsync(async () =>
{
await messagePublishService.PublishBatchAsync(publishMessages);
return await base.Updateable()
.SetColumns(s => s.Status, JournalStatusEnum.Published)
.SetColumns(s => s.UpdatedAt, DateTime.Now)
.Where(w => w.Id == id)
.ExecuteCommandAsync() > 0;
});
static (int First, int Second, int Third, int Fourth) ParseTaskNo(string? no)
{

View File

@ -0,0 +1,334 @@
using Microsoft.Extensions.Logging;
using QYZH.InteractiveMagazine.Infrastructure.OSS;
using QYZH.InteractiveMagazine.IService;
using QYZH.InteractiveMagazine.Models.Common;
using QYZH.InteractiveMagazine.Models.Dto;
using QYZH.InteractiveMagazine.Models.Dto.Material;
using QYZH.InteractiveMagazine.Models.Enum;
using QYZH.InteractiveMagazine.Repository;
using SqlSugar;
using MaterialEntity = QYZH.InteractiveMagazine.Models.Entity.Material;
using MaterialFileEntity = QYZH.InteractiveMagazine.Models.Entity.MaterialFile;
namespace QYZH.InteractiveMagazine.Service;
/// <summary>
/// 资料服务实现
/// </summary>
public class MaterialService(
BaseRepository<MaterialEntity> materialRepository,
BaseRepository<MaterialFileEntity> materialFileRepository,
OssService ossService,
ILogger<MaterialService> logger) : BaseRepository<MaterialEntity>, IMaterialService
{
/// <summary>
/// 创建资料
/// </summary>
public async Task<MaterialOutput> CreateAsync(MaterialInput input)
{
ValidateInput(input);
var material = new MaterialEntity
{
Title = input.Title.Trim(),
Name = Normalize(input.Name),
Description = Normalize(input.Description),
Sort = input.Sort,
StartTime = input.StartTime,
EndTime = input.EndTime,
Status = (int)DefaultStatusEnum.Active,
IsDeleted = false,
CreatedBy = "System",
CreatedAt = DateTime.Now,
UpdatedBy = "System",
UpdatedAt = DateTime.Now
};
await materialRepository.UseTranAsync(async () =>
{
var materialResult = await materialRepository.InsertAsync(material);
BusinessException.ThrowIf(!materialResult, "创建资料失败", ResultCode.GLOBAL_ERROR);
var files = await BuildFileEntitiesAsync(material.Id, input.Files);
var fileResult = await materialFileRepository.Context.Insertable(files).ExecuteCommandAsync();
BusinessException.ThrowIf(fileResult <= 0, "创建资料文件失败", ResultCode.GLOBAL_ERROR);
});
logger.LogInformation("资料创建成功:{Id}", material.Id);
return await GetByIdAsync(material.Id);
}
/// <summary>
/// 更新资料
/// </summary>
public async Task<MaterialOutput> UpdateAsync(long id, MaterialInput input)
{
ValidateInput(input);
var material = await GetValidMaterialAsync(id);
material.Title = input.Title.Trim();
material.Name = Normalize(input.Name);
material.Description = Normalize(input.Description);
material.Sort = input.Sort;
material.StartTime = input.StartTime;
material.EndTime = input.EndTime;
material.UpdatedBy = "System";
material.UpdatedAt = DateTime.Now;
await materialRepository.UseTranAsync(async () =>
{
var materialResult = await materialRepository.UpdateAsync(material);
BusinessException.ThrowIf(!materialResult, "更新资料失败", ResultCode.GLOBAL_ERROR);
await SoftDeleteFilesAsync(material.Id);
var files = await BuildFileEntitiesAsync(material.Id, input.Files);
var fileResult = await materialFileRepository.Context.Insertable(files).ExecuteCommandAsync();
BusinessException.ThrowIf(fileResult <= 0, "更新资料文件失败", ResultCode.GLOBAL_ERROR);
});
logger.LogInformation("资料更新成功:{Id}", material.Id);
return await GetByIdAsync(material.Id);
}
/// <summary>
/// 删除资料
/// </summary>
public async Task DeleteAsync(long id)
{
await GetValidMaterialAsync(id);
await materialRepository.UseTranAsync(async () =>
{
var materialResult = await materialRepository.Context.Updateable<MaterialEntity>()
.SetColumns(m => new MaterialEntity
{
IsDeleted = true,
UpdatedBy = "System",
UpdatedAt = DateTime.Now
})
.Where(m => m.Id == id && !m.IsDeleted)
.ExecuteCommandAsync();
BusinessException.ThrowIf(materialResult <= 0, "删除资料失败", ResultCode.GLOBAL_ERROR);
await SoftDeleteFilesAsync(id);
});
logger.LogInformation("资料删除成功:{Id}", id);
}
/// <summary>
/// 根据ID获取资料
/// </summary>
public async Task<MaterialOutput> GetByIdAsync(long id)
{
var material = await GetValidMaterialAsync(id);
var files = await GetFilesAsync(material.Id);
return MapToOutput(material, files);
}
/// <summary>
/// 分页查询资料列表
/// </summary>
public async Task<PageListModel<MaterialOutput>> GetListAsync(MaterialQueryInput input)
{
BusinessException.ThrowIf(input.PageIndex <= 0, "页码必须大于0", ResultCode.BAD_REQUEST);
BusinessException.ThrowIf(input.PageSize <= 0 || input.PageSize > 100, "每页条数必须在1-100之间", ResultCode.BAD_REQUEST);
RefAsync<int> totalNumber = 0;
var materials = await materialRepository.Queryable()
.Where(m => !m.IsDeleted)
.WhereIF(!string.IsNullOrWhiteSpace(input.Title), m => m.Title.Contains(input.Title!.Trim()))
.WhereIF(!string.IsNullOrWhiteSpace(input.Name), m => m.Name != null && m.Name.Contains(input.Name!.Trim()))
.WhereIF(input.Status.HasValue, m => m.Status == input.Status!.Value)
.OrderBy(m => m.Sort)
.OrderByDescending(m => m.CreatedAt)
.ToPageListAsync(input.PageIndex, input.PageSize, totalNumber);
var result = await BuildOutputsAsync(materials);
return new PageListModel<MaterialOutput>(result, input.PageIndex, input.PageSize, totalNumber);
}
/// <summary>
/// 更新资料启用/禁用状态
/// </summary>
public async Task<bool> UpdateStatusAsync(long id)
{
var material = await GetValidMaterialAsync(id);
material.Status = material.Status == (int)DefaultStatusEnum.Active
? (int)DefaultStatusEnum.Inactive
: (int)DefaultStatusEnum.Active;
material.UpdatedBy = "System";
material.UpdatedAt = DateTime.Now;
var result = await materialRepository.UpdateAsync(material);
BusinessException.ThrowIf(!result, "更新资料状态失败", ResultCode.GLOBAL_ERROR);
logger.LogInformation("资料状态更新成功:{Id},状态:{Status}", id, material.Status);
return result;
}
/// <summary>
/// 获取小程序可见资料列表
/// </summary>
public async Task<List<MaterialOutput>> GetEnabledListAsync()
{
var now = DateTime.Now;
var materials = await materialRepository.Queryable()
.Where(m => !m.IsDeleted)
.Where(m => m.Status == (int)DefaultStatusEnum.Active)
.Where(m => m.StartTime == null || m.StartTime <= now)
.Where(m => m.EndTime == null || m.EndTime >= now)
.OrderBy(m => m.Sort)
.OrderByDescending(m => m.CreatedAt)
.ToListAsync();
return await BuildOutputsAsync(materials);
}
private async Task<MaterialEntity> GetValidMaterialAsync(long id)
{
var material = await materialRepository.Queryable()
.Where(m => m.Id == id && !m.IsDeleted)
.FirstAsync();
BusinessException.ThrowIf(material == null, "资料不存在", ResultCode.NOT_FOUND);
return material;
}
private async Task<List<MaterialFileEntity>> BuildFileEntitiesAsync(long materialId, List<MaterialFileInput> inputs)
{
var helper = CreateImageHelper();
var files = new List<MaterialFileEntity>();
foreach (var input in inputs.OrderBy(f => f.Sort))
{
var file = new MaterialFileEntity
{
MaterialId = materialId,
FileType = input.FileType,
FileUrl = input.FileUrl.Trim(),
FileName = Normalize(input.FileName),
Sort = input.Sort,
Status = (int)DefaultStatusEnum.Active,
IsDeleted = false,
CreatedBy = "System",
CreatedAt = DateTime.Now,
UpdatedBy = "System",
UpdatedAt = DateTime.Now
};
if (OssImageHelper.IsTempImage(file.FileUrl))
{
file.FileUrl = await helper.MoveToFormalAsync(file.FileUrl, GetMaterialFormalFolder(materialId, file.FileType, file.Id));
}
files.Add(file);
}
return files;
}
private async Task SoftDeleteFilesAsync(long materialId)
{
await materialFileRepository.Context.Updateable<MaterialFileEntity>()
.SetColumns(f => new MaterialFileEntity
{
IsDeleted = true,
UpdatedBy = "System",
UpdatedAt = DateTime.Now
})
.Where(f => f.MaterialId == materialId && !f.IsDeleted)
.ExecuteCommandAsync();
}
private async Task<List<MaterialFileEntity>> GetFilesAsync(long materialId)
{
return await materialFileRepository.Queryable()
.Where(f => f.MaterialId == materialId && !f.IsDeleted)
.OrderBy(f => f.Sort)
.OrderByDescending(f => f.CreatedAt)
.ToListAsync();
}
private async Task<List<MaterialOutput>> BuildOutputsAsync(List<MaterialEntity> materials)
{
if (materials.Count == 0)
{
return new List<MaterialOutput>();
}
var materialIds = materials.Select(m => m.Id).ToList();
var files = await materialFileRepository.Queryable()
.Where(f => materialIds.Contains(f.MaterialId) && !f.IsDeleted)
.OrderBy(f => f.Sort)
.OrderByDescending(f => f.CreatedAt)
.ToListAsync();
var fileMap = files.GroupBy(f => f.MaterialId).ToDictionary(g => g.Key, g => g.ToList());
return materials.Select(m => MapToOutput(m, fileMap.GetValueOrDefault(m.Id) ?? new List<MaterialFileEntity>())).ToList();
}
private static void ValidateInput(MaterialInput input)
{
BusinessException.ThrowIf(string.IsNullOrWhiteSpace(input.Title), "资料标题不能为空", ResultCode.BAD_REQUEST);
BusinessException.ThrowIf(input.Title.Trim().Length > 100, "资料标题长度不能超过100个字符", ResultCode.BAD_REQUEST);
BusinessException.ThrowIf(!string.IsNullOrWhiteSpace(input.Name) && input.Name.Length > 100, "资料名称长度不能超过100个字符", ResultCode.BAD_REQUEST);
BusinessException.ThrowIf(!string.IsNullOrWhiteSpace(input.Description) && input.Description.Length > 1000, "资料描述长度不能超过1000个字符", ResultCode.BAD_REQUEST);
BusinessException.ThrowIf(input.Sort < 0, "排序值不能小于0", ResultCode.BAD_REQUEST);
BusinessException.ThrowIf(input.StartTime.HasValue && input.EndTime.HasValue && input.EndTime <= input.StartTime, "结束时间必须大于开始时间", ResultCode.BAD_REQUEST);
BusinessException.ThrowIf(input.Files == null || input.Files.Count == 0, "资料文件不能为空", ResultCode.BAD_REQUEST);
foreach (var file in input.Files)
{
BusinessException.ThrowIf(!Enum.IsDefined(typeof(MaterialFileTypeEnum), file.FileType), "资料文件类型不正确", ResultCode.BAD_REQUEST);
BusinessException.ThrowIf(string.IsNullOrWhiteSpace(file.FileUrl), "资料文件地址不能为空", ResultCode.BAD_REQUEST);
BusinessException.ThrowIf(file.FileUrl.Trim().Length > 500, "资料文件地址长度不能超过500个字符", ResultCode.BAD_REQUEST);
BusinessException.ThrowIf(!string.IsNullOrWhiteSpace(file.FileName) && file.FileName.Length > 100, "资料文件名称长度不能超过100个字符", ResultCode.BAD_REQUEST);
BusinessException.ThrowIf(file.Sort < 0, "资料文件排序值不能小于0", ResultCode.BAD_REQUEST);
}
}
private OssImageHelper CreateImageHelper() => new(ossService, logger);
private static string GetMaterialFormalFolder(long materialId, MaterialFileTypeEnum fileType, long fileId)
{
return $"material/{materialId}/{fileType.ToString().ToLowerInvariant()}/{fileId}";
}
private static string? Normalize(string? value)
{
return string.IsNullOrWhiteSpace(value) ? null : value.Trim();
}
private static MaterialOutput MapToOutput(MaterialEntity material, List<MaterialFileEntity> files)
{
return new MaterialOutput
{
Id = material.Id,
Title = material.Title,
Name = material.Name,
Description = material.Description,
Sort = material.Sort,
StartTime = material.StartTime,
EndTime = material.EndTime,
Status = material.Status,
CreatedBy = material.CreatedBy,
CreatedAt = material.CreatedAt,
UpdatedBy = material.UpdatedBy,
UpdatedAt = material.UpdatedAt,
Files = files.Select(MapFileToOutput).ToList()
};
}
private static MaterialFileOutput MapFileToOutput(MaterialFileEntity file)
{
return new MaterialFileOutput
{
Id = file.Id,
FileType = file.FileType,
FileUrl = file.FileUrl,
FileName = file.FileName,
Sort = file.Sort
};
}
}

View File

@ -0,0 +1,112 @@
using Microsoft.Extensions.Logging;
using QYZH.InteractiveMagazine.Infrastructure.RabbitMQ;
using QYZH.InteractiveMagazine.IService;
using QYZH.InteractiveMagazine.Models.Common;
using QYZH.InteractiveMagazine.Models.Dto;
using QYZH.InteractiveMagazine.Models.Dto.RabbitMQ;
using QYZH.InteractiveMagazine.Models.Entity;
using QYZH.InteractiveMagazine.Models.Enum;
using QYZH.InteractiveMagazine.Repository;
using System.Text.Json;
namespace QYZH.InteractiveMagazine.Service;
/// <summary>
/// MQ消息发布服务。
/// </summary>
public class MessagePublishService(
IRabbitMQService rabbitMQService,
ILogger<MessagePublishService> logger) : BaseRepository<MessageOutbox>, IMessagePublishService
{
/// <summary>
/// 可靠发布消息默认写入Outbox。
/// </summary>
public async Task<MessagePublishResult> PublishAsync<T>(MessagePublishInput<T> input, CancellationToken cancellationToken = default)
{
ValidateInput(input);
var message = BuildOutboxMessage(input);
await Context.Insertable(message).ExecuteCommandAsync(cancellationToken);
return new MessagePublishResult
{
Success = true,
OutboxIds = [message.Id],
Message = "消息已写入Outbox"
};
}
/// <summary>
/// 批量可靠发布消息默认写入Outbox。
/// </summary>
public async Task<MessagePublishResult> PublishBatchAsync<T>(IEnumerable<MessagePublishInput<T>> inputs, CancellationToken cancellationToken = default)
{
var inputList = inputs.ToList();
BusinessException.ThrowIf(inputList.Count == 0, "消息发布列表不能为空", ResultCode.BAD_REQUEST);
inputList.ForEach(ValidateInput);
var messages = inputList.Select(BuildOutboxMessage).ToList();
await Context.Insertable(messages).ExecuteCommandAsync(cancellationToken);
return new MessagePublishResult
{
Success = true,
OutboxIds = messages.Select(x => x.Id).ToList(),
Message = "消息已批量写入Outbox"
};
}
/// <summary>
/// 直接发布消息不写Outbox。
/// </summary>
public async Task<MessagePublishResult> PublishDirectAsync<T>(MessagePublishInput<T> input, CancellationToken cancellationToken = default)
{
ValidateInput(input);
var sent = await rabbitMQService.SendAsync(new RabbitMQSendParam
{
Exchange = input.Exchange,
Queue = input.Queue,
RoutingKey = input.RoutingKey,
Data = input.Data!
}, cancellationToken);
if (!sent)
{
logger.LogError("MQ直接发布失败Exchange: {Exchange}, Queue: {Queue}, RoutingKey: {RoutingKey}, BusinessType: {BusinessType}, BusinessId: {BusinessId}",
input.Exchange, input.Queue, input.RoutingKey, input.BusinessType, input.BusinessId);
throw new BusinessException("MQ消息发送失败", ResultCode.GLOBAL_ERROR);
}
return new MessagePublishResult
{
Success = true,
Message = "消息已直接发送"
};
}
private static MessageOutbox BuildOutboxMessage<T>(MessagePublishInput<T> input)
{
var now = DateTime.Now;
return new MessageOutbox
{
Exchange = input.Exchange,
Queue = input.Queue,
RoutingKey = input.RoutingKey,
Payload = JsonSerializer.Serialize(input.Data),
BusinessType = input.BusinessType,
BusinessId = input.BusinessId,
Status = (int)MessageOutboxStatusEnum.Pending,
CreatedBy = "System",
CreatedAt = now,
UpdatedBy = "System",
UpdatedAt = now
};
}
private static void ValidateInput<T>(MessagePublishInput<T> input)
{
BusinessException.ThrowIf(string.IsNullOrWhiteSpace(input.Exchange), "MQ交换机不能为空", ResultCode.BAD_REQUEST);
BusinessException.ThrowIf(string.IsNullOrWhiteSpace(input.Queue), "MQ队列不能为空", ResultCode.BAD_REQUEST);
BusinessException.ThrowIf(string.IsNullOrWhiteSpace(input.RoutingKey), "MQ路由键不能为空", ResultCode.BAD_REQUEST);
BusinessException.ThrowIf(string.IsNullOrWhiteSpace(input.BusinessType), "MQ业务类型不能为空", ResultCode.BAD_REQUEST);
BusinessException.ThrowIf(input.BusinessId <= 0, "MQ业务ID无效", ResultCode.BAD_REQUEST);
BusinessException.ThrowIf(input.Data == null, "MQ消息数据不能为空", ResultCode.BAD_REQUEST);
}
}

View File

@ -24,23 +24,41 @@ public class OperationLogService(
/// 记录操作日志
/// </summary>
public async Task LogAsync(long operatorId, string operatorName, string actionType, string targetType, long targetId, string? targetName = null, string? detail = null, string? ipAddress = null)
{
await LogAsync(new OperationLogRecordInput
{
OperatorId = operatorId,
OperatorName = operatorName,
ActionType = actionType,
TargetType = targetType,
TargetId = targetId,
TargetName = targetName,
Detail = detail,
IpAddress = ipAddress
});
}
/// <summary>
/// 记录操作日志
/// </summary>
public async Task LogAsync(OperationLogRecordInput input)
{
try
{
var log = new OperationLog
{
OperatorId = operatorId,
OperatorName = operatorName,
ActionType = actionType,
TargetType = targetType,
TargetId = targetId,
TargetName = targetName,
Detail = detail,
IpAddress = ipAddress,
OperatorId = input.OperatorId,
OperatorName = input.OperatorName,
ActionType = input.ActionType,
TargetType = input.TargetType,
TargetId = input.TargetId,
TargetName = input.TargetName,
Detail = input.Detail,
IpAddress = input.IpAddress,
IsDeleted = false,
CreatedBy = operatorName,
CreatedBy = input.OperatorName,
CreatedAt = DateTime.Now,
UpdatedBy = operatorName,
UpdatedBy = input.OperatorName,
UpdatedAt = DateTime.Now
};
@ -48,12 +66,12 @@ public class OperationLogService(
logger.LogInformation(
"记录操作日志Operator: {Operator}, Action: {Action}, Target: {TargetType}/{TargetId}",
operatorName, actionType, targetType, targetId);
input.OperatorName, input.ActionType, input.TargetType, input.TargetId);
}
catch (Exception ex)
{
// 日志记录不应影响主业务流程
logger.LogError(ex, "记录操作日志失败Operator: {Operator}, Action: {Action}", operatorName, actionType);
logger.LogError(ex, "记录操作日志失败Operator: {Operator}, Action: {Action}", input.OperatorName, input.ActionType);
}
}

View File

@ -6,6 +6,7 @@ using QYZH.InteractiveMagazine.Models.Dto.Points;
using QYZH.InteractiveMagazine.Models.Entity;
using QYZH.InteractiveMagazine.Models.Enum;
using QYZH.InteractiveMagazine.Repository;
using SqlSugar;
namespace QYZH.InteractiveMagazine.Service;
@ -73,7 +74,18 @@ public class PointsService(
if (input.Amount <= 0)
throw new BusinessException("增加积分数量必须大于0", ResultCode.BAD_REQUEST);
// 查询用户当前积分
var now = DateTime.Now;
// 原子增加积分,避免并发写回旧余额覆盖新余额
var affectedRows = await Context.Updateable<Users>()
.SetColumns(u => u.Points == u.Points + input.Amount)
.SetColumns(u => u.UpdatedAt == now)
.Where(u => u.Id == input.UserId && !u.IsDeleted)
.ExecuteCommandAsync();
if (affectedRows <= 0)
throw new BusinessException("用户不存在", ResultCode.NOT_FOUND);
var user = await Context.Queryable<Users>()
.Where(u => u.Id == input.UserId && !u.IsDeleted)
.FirstAsync();
@ -81,15 +93,8 @@ public class PointsService(
if (user == null)
throw new BusinessException("用户不存在", ResultCode.NOT_FOUND);
var previousBalance = user.Points;
var newBalance = previousBalance + input.Amount;
// 更新用户积分
await Context.Updateable<Users>()
.SetColumns(u => u.Points == newBalance)
.SetColumns(u => u.UpdatedAt == DateTime.Now)
.Where(u => u.Id == input.UserId && !u.IsDeleted)
.ExecuteCommandAsync();
var newBalance = user.Points;
var previousBalance = newBalance - input.Amount;
// 插入积分流水记录
var record = new PointsRecord
@ -104,9 +109,9 @@ public class PointsService(
Status = (int)PointsRecordStatusEnum.Success,
IsDeleted = false,
CreatedBy = input.OperatorName ?? user.Name ?? input.UserId.ToString(),
CreatedAt = DateTime.Now,
CreatedAt = now,
UpdatedBy = input.OperatorName ?? user.Name ?? input.UserId.ToString(),
UpdatedAt = DateTime.Now
UpdatedAt = now
};
var recordEntity = await InsertReturnEntityAsync(record);
@ -131,7 +136,27 @@ public class PointsService(
if (input.Amount <= 0)
throw new BusinessException("扣除积分数量必须大于0", ResultCode.BAD_REQUEST);
// 查询用户当前积分
var now = DateTime.Now;
// 带余额条件的原子扣减,避免并发扣减时超扣或覆盖余额
var affectedRows = await Context.Updateable<Users>()
.SetColumns(u => u.Points == u.Points - input.Amount)
.SetColumns(u => u.UpdatedAt == now)
.Where(u => u.Id == input.UserId && !u.IsDeleted && u.Points >= input.Amount)
.ExecuteCommandAsync();
if (affectedRows <= 0)
{
var currentUser = await Context.Queryable<Users>()
.Where(u => u.Id == input.UserId && !u.IsDeleted)
.FirstAsync();
if (currentUser == null)
throw new BusinessException("用户不存在", ResultCode.NOT_FOUND);
throw new BusinessException($"积分不足,需要 {input.Amount} 积分,当前余额 {currentUser.Points}", ResultCode.BAD_REQUEST);
}
var user = await Context.Queryable<Users>()
.Where(u => u.Id == input.UserId && !u.IsDeleted)
.FirstAsync();
@ -139,20 +164,8 @@ public class PointsService(
if (user == null)
throw new BusinessException("用户不存在", ResultCode.NOT_FOUND);
var previousBalance = user.Points;
// 余额不足校验
if (previousBalance < input.Amount)
throw new BusinessException($"积分不足,需要 {input.Amount} 积分,当前余额 {previousBalance}", ResultCode.BAD_REQUEST);
var newBalance = previousBalance - input.Amount;
// 更新用户积分
await Context.Updateable<Users>()
.SetColumns(u => u.Points == newBalance)
.SetColumns(u => u.UpdatedAt == DateTime.Now)
.Where(u => u.Id == input.UserId && !u.IsDeleted)
.ExecuteCommandAsync();
var newBalance = user.Points;
var previousBalance = newBalance + input.Amount;
// 插入积分流水记录
var record = new PointsRecord
@ -167,9 +180,9 @@ public class PointsService(
Status = (int)PointsRecordStatusEnum.Success,
IsDeleted = false,
CreatedBy = input.OperatorName ?? user.Name ?? input.UserId.ToString(),
CreatedAt = DateTime.Now,
CreatedAt = now,
UpdatedBy = input.OperatorName ?? user.Name ?? input.UserId.ToString(),
UpdatedAt = DateTime.Now
UpdatedAt = now
};
var recordEntity = await InsertReturnEntityAsync(record);
@ -253,7 +266,7 @@ public class PointsService(
.WhereIF(!string.IsNullOrEmpty(input.Status), r => r.Status.ToString() == input.Status)
.OrderByDescending(r => r.CreatedAt);
var total = 0;
RefAsync<int> total = 0;
var records = await query
.Select(r => new PointsRecordOutput
{

View File

@ -172,29 +172,18 @@ public class UserAnswerTaskService(
// ============================================
// 【积分和成长值计算】
// 1. totalGrowthPoints从已完成作答记录中,按 JournalPageTaskGroupId 查询主任务Id=GroupId GrowthPoint
// 2. totalPoints统计该期刊下已完成任务的 Points 总数
// totalGrowthPoints当前期刊下所有主任务Id == GroupId成长值总和
// totalPoints当前期刊下所有主任务Id == GroupId的积分总和
// ============================================
// 该期刊下用户已完成的作答记录
// 【已完成判断逻辑】
// Status: 0=未批阅1=已批阅
var completedAnswers = userAnswers
.Where(a => a.JournalId == journal.Id && a.Status == (int)UserAnswerStatusEnum.Complete)
.ToList();
// 筛选当前期刊下的主任务Id == GroupId
var allMainTasks = journalTasks.Where(t => t.Id == t.GroupId).ToList();
// 按 JournalPageTaskGroupId 去重,获取已完成的主任务 ID 列表
var completedGroupIdsSet = completedAnswers.Select(a => a.JournalPageTaskGroupId).Distinct().ToList();
// 累计成长值(所有主任务的 GrowthPoint 总和)
var totalGrowthPoints = allMainTasks.Sum(t => t.GrowthPoint);
// 从主任务表查询这些 GroupId 对应的 GrowthPoint 和 Points
var mainTasks = allTasks
.Where(t => completedGroupIdsSet.Contains(t.GroupId) && t.Id == t.GroupId)
.ToList();
// 累计成长值(从主任务的 GrowthPoint 累加)
var totalGrowthPoints = mainTasks.Sum(t => t.GrowthPoint);
// 累计积分(从已完成任务的 Points 累加,每个 GroupId 只计算一次)
var totalPoints = mainTasks.Sum(t => t.Points);
// 累计积分(所有主任务的 Points 总和)
var totalPoints = allMainTasks.Sum(t => t.Points);
// 【进度百分比计算】按篇数统计
// 只计算有题目的页面,进度 = 已答篇数 / 有题目的总篇数 × 100

View File

@ -5,6 +5,8 @@ using QYZH.InteractiveMagazine.Infrastructure.RabbitMQ;
using QYZH.InteractiveMagazine.IService;
using QYZH.InteractiveMagazine.Models.Common;
using QYZH.InteractiveMagazine.Models.Dto;
using QYZH.InteractiveMagazine.Models.Dto.Journal;
using QYZH.InteractiveMagazine.Models.Dto.RabbitMQ;
using QYZH.InteractiveMagazine.Models.Entity;
using QYZH.InteractiveMagazine.Models.Enum;
using QYZH.InteractiveMagazine.Repository;
@ -21,7 +23,7 @@ public class UserJournalService(
BaseRepository<Users> usersRepository,
BaseRepository<Journal> journalRepository,
ILogger<UserJournalService> logger,
IRabbitMQService rabbitMqService,
IMessagePublishService messagePublishService,
OssService ossService,
IPetService petService)
: BaseRepository<UserJournal>, IUserJournalService
@ -214,7 +216,7 @@ public class UserJournalService(
}
var recordIds = records.Select(r => r.Id).ToList();
var messageSent = await rabbitMqService.SendAsync(new RabbitMQSendParam
var messageSent = (await messagePublishService.PublishAsync(new MessagePublishInput<GenerateUserJournalQrCodeMessage>
{
Exchange = JournalExchange,
Queue = QrCodeGenerateQueue,
@ -223,8 +225,10 @@ public class UserJournalService(
{
RecordIds = recordIds,
OperatorName = operatorName
}
});
},
BusinessType = "UserJournalQrCodeGenerate",
BusinessId = input.JournalId
})).Success;
if (!messageSent)
{
@ -457,7 +461,7 @@ public class UserJournalService(
{
try
{
var messageSent = await rabbitMqService.SendAsync(new RabbitMQSendParam
var messageSent = (await messagePublishService.PublishAsync(new MessagePublishInput<BindJournalMessage>
{
Exchange = JournalExchange,
Queue = BindJournalQueue,
@ -469,8 +473,10 @@ public class UserJournalService(
StartTime = journal.StartTime,
EndTime = journal.EndTime,
UploadDomain = user.UploadDomain
}
});
},
BusinessType = "UserJournalBind",
BusinessId = user.Id
})).Success;
if (!messageSent)
{

View File

@ -252,11 +252,17 @@ public class UsersService(
RecordId = result.RecordId
});
await operationLogService.LogAsync(
operatorId, operatorName,
OperationLogActionType.ManualAddPoints,
OperationLogTargetType.User,
userId, user.Name, detail, ipAddress);
await operationLogService.LogAsync(new OperationLogRecordInput
{
OperatorId = operatorId,
OperatorName = operatorName,
ActionType = OperationLogActionType.ManualAddPoints,
TargetType = OperationLogTargetType.User,
TargetId = userId,
TargetName = user.Name,
Detail = detail,
IpAddress = ipAddress
});
return new ManualPointsOutput
{
@ -302,11 +308,17 @@ public class UsersService(
RecordId = result.RecordId
});
await operationLogService.LogAsync(
operatorId, operatorName,
OperationLogActionType.ManualDeductPoints,
OperationLogTargetType.User,
userId, user.Name, detail, ipAddress);
await operationLogService.LogAsync(new OperationLogRecordInput
{
OperatorId = operatorId,
OperatorName = operatorName,
ActionType = OperationLogActionType.ManualDeductPoints,
TargetType = OperationLogTargetType.User,
TargetId = userId,
TargetName = user.Name,
Detail = detail,
IpAddress = ipAddress
});
return new ManualPointsOutput
{

View File

@ -253,9 +253,9 @@ public class WxMallService(
{
// 已有同类物品,累加数量
await exchangeRecordRepository.Context.Updateable<UserBag>()
.SetColumns(b => b.Quantity == existingBag.Quantity + input.Quantity)
.SetColumns(b => b.Quantity == b.Quantity + input.Quantity)
.SetColumns(b => b.UpdatedAt == DateTime.Now)
.Where(b => b.Id == existingBag.Id)
.Where(b => b.Id == existingBag.Id && !b.IsDeleted && b.Status == (int)UserBagStatusEnum.Available)
.ExecuteCommandAsync();
}
else

View File

@ -0,0 +1,25 @@
using Microsoft.AspNetCore.Mvc;
using QYZH.InteractiveMagazine.IService;
using QYZH.InteractiveMagazine.Models.Dto;
using QYZH.InteractiveMagazine.Models.Dto.Banner;
using QYZH.InteractiveMagazine.Models.Enum;
namespace QYZH.InteractiveMagazine.WeChatApi.Controllers;
/// <summary>
/// 小程序轮播图控制器
/// </summary>
public class BannerController(IBannerService bannerService) : WeChatBaseController
{
/// <summary>
/// 获取启用轮播图列表
/// </summary>
/// <param name="position">展示位置</param>
/// <returns>轮播图列表</returns>
[HttpGet]
public async Task<BaseResponse<List<BannerOutput>>> GetListAsync([FromQuery] BannerPositionEnum position = BannerPositionEnum.Home)
{
var result = await bannerService.GetEnabledListAsync(position);
return Success(result);
}
}

View File

@ -0,0 +1,23 @@
using Microsoft.AspNetCore.Mvc;
using QYZH.InteractiveMagazine.IService;
using QYZH.InteractiveMagazine.Models.Dto;
using QYZH.InteractiveMagazine.Models.Dto.Material;
namespace QYZH.InteractiveMagazine.WeChatApi.Controllers;
/// <summary>
/// 小程序资料控制器
/// </summary>
public class MaterialController(IMaterialService materialService) : WeChatBaseController
{
/// <summary>
/// 获取可见资料列表
/// </summary>
/// <returns>资料列表</returns>
[HttpGet]
public async Task<BaseResponse<List<MaterialOutput>>> GetListAsync()
{
var result = await materialService.GetEnabledListAsync();
return Success(result);
}
}

View File

@ -26,7 +26,7 @@ public class UserAnswerTaskController : WeChatBaseController
_logger = logger;
}
private const long ConstUserId = 821752253853766;
private const long ConstUserId = 821752253853766; //ConstUserId;//
/// <summary>
/// 获取当前用户的期刊学习进度列表
@ -275,7 +275,7 @@ public class UserAnswerTaskController : WeChatBaseController
{
try
{
var userId = ConstUserId;//GetCurrentUserId();
var userId = GetCurrentUserId();
if (userId == 0)
{
return BaseResponse<CatalogTreeOutput>.Fail(ResultCode.DENY, "未获取到用户信息");

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<key id="464a114e-8722-43ec-b421-bc39d97f6a6d" version="1">
<creationDate>2026-07-06T07:54:57.8414657Z</creationDate>
<activationDate>2026-07-06T07:54:57.8247502Z</activationDate>
<expirationDate>2026-10-04T07:54:57.8247502Z</expirationDate>
<descriptor deserializerType="Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.AuthenticatedEncryptorDescriptorDeserializer, Microsoft.AspNetCore.DataProtection, Version=8.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60">
<descriptor>
<encryption algorithm="AES_256_CBC" />
<validation algorithm="HMACSHA256" />
<masterKey p4:requiresEncryption="true" xmlns:p4="http://schemas.asp.net/2015/03/dataProtection">
<!-- Warning: the key below is in an unencrypted form. -->
<value>eHtVAu0iXhzOZG+D+OtGwyuEUuxMSOeJb0EYcpcAcjGfaK316jIbOZuIeLU0X8nhnvFPigbBOHobSu7KDSiQ1Q==</value>
</masterKey>
</descriptor>
</descriptor>
</key>

View File

@ -26,7 +26,8 @@ var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddJsonFile("medal-rule-config.json", optional: false, reloadOnChange: true);
YitIdHelper.SetIdGenerator(new IdGeneratorOptions { WorkerId = 1 });
var snowflakeSettings = builder.Configuration.GetSection("SnowflakeSettings").Get<SnowflakeSettings>() ?? new SnowflakeSettings { WorkerId = 1 };
YitIdHelper.SetIdGenerator(new IdGeneratorOptions { WorkerId = snowflakeSettings.WorkerId });
builder.UseAutofac();
@ -38,66 +39,14 @@ builder.InitSqlSugarDb(new IocConfig
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.AddInteractiveMagazineApiDefaults();
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 =>
{

View File

@ -9,6 +9,9 @@
"ExpiryMinutes": 120,
"JwtTokenExpiryDays": 30
},
"SnowflakeSettings": {
"WorkerId": 1
},
"RedisSettings": {
"ConnectionString": "192.168.20.150:16379,defaultDatabase=5",
"Sentinels": [],

View File

@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using QYZH.InteractiveMagazine.Infrastructure.Middleware;
using QYZH.InteractiveMagazine.IService;
using QYZH.InteractiveMagazine.Models.Common;
@ -76,6 +77,7 @@ public class AdminController : BaseController
/// </summary>
/// <param name="input">修改密码输入</param>
/// <returns>修改密码结果</returns>
[OperationLog(OperationLogActionType.ChangePassword, OperationLogTargetType.AdminUser, UseOperatorAsTargetId = true, LogArguments = false)]
[HttpPost("changePassword")]
public async Task<BaseResponse<object>> ChangePasswordAsync([FromBody] ChangePasswordInput input)
{
@ -94,6 +96,7 @@ public class AdminController : BaseController
/// </summary>
/// <param name="input">管理员输入</param>
/// <returns>创建的管理员信息</returns>
[OperationLog(OperationLogActionType.Create, OperationLogTargetType.AdminUser, TargetNameProperty = "UserName")]
[HttpPost("users")]
public async Task<BaseResponse<AdminUserOutput>> CreateUserAsync([FromBody] AdminUserInput input)
{
@ -120,6 +123,7 @@ public class AdminController : BaseController
/// <param name="id">管理员ID</param>
/// <param name="input">管理员输入</param>
/// <returns>更新后的管理员信息</returns>
[OperationLog(OperationLogActionType.Update, OperationLogTargetType.AdminUser, TargetNameProperty = "UserName")]
[HttpPut("users/{id}")]
public async Task<BaseResponse<AdminUserOutput>> UpdateUserAsync(long id, [FromBody] AdminUserInput input)
{
@ -145,6 +149,7 @@ public class AdminController : BaseController
/// </summary>
/// <param name="id">管理员ID</param>
/// <returns>操作结果</returns>
[OperationLog(OperationLogActionType.Delete, OperationLogTargetType.AdminUser)]
[HttpDelete("users/{id}")]
public async Task<BaseResponse<object>> DeleteUserAsync(long id)
{
@ -220,6 +225,7 @@ public class AdminController : BaseController
/// </summary>
/// <param name="id">管理员ID</param>
/// <returns>操作结果</returns>
[OperationLog(OperationLogActionType.StatusChange, OperationLogTargetType.AdminUser)]
[HttpPut("users/{id}/status")]
public async Task<BaseResponse<object>> ToggleUserStatusAsync(long id)
{

Some files were not shown because too many files have changed in this diff Show More