Compare commits

...

44 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
glz
0b829b72cb Merge branch 'master' of http://8.137.159.94:3000/glz/QYZH.InteractiveMagazine 2026-07-06 17:45:45 +08:00
glz
2deb0279bb refactor(AdminPermissionService): 优化权限和角色编码重复校验逻辑
1.  将链式查询拆分为分步Where调用,使用WhereIF简化条件判断
2.  修复空值处理问题,使用id!.Value替代原值访问
3.  新增数据库菜单初始化脚本,补充Dockerfile和dotnet工具配置
2026-07-06 17:45:42 +08:00
2b444abe10 修改错误 2026-07-06 15:47:44 +08:00
32612ca6d4 修改这个 2026-07-06 14:30:37 +08:00
7f78795135 临时数据 2026-07-06 14:27:56 +08:00
91752baa3b 接口修改 2026-07-06 14:26:16 +08:00
glz
a04b4be7e5 refactor: 拆分微信API到独立项目并迁移相关代码
1.  新增WeChatApi独立项目,将原WebApi中的微信相关控制器迁移至新项目
2.  新增后台权限管理相关实体、服务接口和基础控制器
3.  完善管理员用户服务,增加角色关联查询和赋值逻辑
4.  修复WebApi Swagger文档过滤微信API版本的问题
5.  更新解决方案文件,添加新的微信API项目引用
6.  新增微信API基础配置文件和项目属性配置
2026-07-06 14:21:21 +08:00
glz
f8874ff60c feat: 新增微信用户名字段并优化用户数据查询及接口请求方式
1. 新增WxUserName字段到UsersOutput Dto用于存储微信用户名
2. 关联WxUser表查询以获取微信用户名数据
3. 将用户相关的GET接口修改为POST接口,使用FromBody接收参数
2026-07-03 11:24:28 +08:00
glz
88e5fd23be refactor(common&service): adjust id range and remove unused qr code method
1. 调整RandomIdHelper的默认ID生成范围从16位改为13位
2. 从IUserJournalService和UserJournalService中移除废弃的单例生成二维码方法
3. 为相关DTO添加JSON数字序列化/反序列化配置,处理前后端数字类型兼容问题
4. 修改二维码内容序列化逻辑,将数字转为字符串避免精度丢失
2026-07-02 16:16:24 +08:00
glz
12d488a5ca refactor: 优化AI评分与二维码ID生成逻辑,调整RabbitMQ配置
1. 调整RabbitMQ预取计数配置,支持从配置读取
2. 新增随机ID帮助类,生成唯一长整型ID
3. 重构二维码ID生成逻辑,新增重试机制避免重复
4. 优化AI评分配置,调整温度系数与并发限制
5. 重构跨页题评分逻辑,支持分组评分与结果去重
6. 新增AI评分异常分类与结果校验逻辑
7. 优化评分提示词与结果归一化处理
2026-07-02 16:05:13 +08:00
glz
519428c52d fix(userJournalService): 移除冗余的类型过滤条件
移除了不必要的Type参数过滤判断,简化用户日志列表查询逻辑
2026-07-02 14:28:39 +08:00
glz
72c7a38f1d refactor(UserJournal): 优化期刊绑定逻辑并完善校验
1. 重构用户查询逻辑,增加未删除过滤
2. 将枚举常量提取复用,统一状态判断
3. 新增数据库事务包裹绑定操作
4. 增加重复绑定同一期刊的校验逻辑
5. 统一绑定时间变量复用
6. 完善更新条件,增加期刊ID和状态校验
2026-07-02 11:25:24 +08:00
glz
5700da58d9 feat: 新增期刊二维码批量生成功能及相关配套
1. 新增UserJournalQrCodeGenerateConsumer消费者处理二维码生成队列任务
2. 新增用户期刊状态枚举的生成中、失败状态
3. 新增批量生成二维码服务方法和相关DTO
4. 优化SqlSugar自动填充创建/更新人字段逻辑
5. 调整接口参数从操作人ID改为操作人名称
6. 新增获取未绑定二维码的API接口和服务方法
2026-07-02 09:46:34 +08:00
glz
bfdfad14d8 refactor(auth): 重构JWT认证相关代码,优化声明获取逻辑
1. 提取通用的GetClaim方法简化多声明类型查找逻辑
2. 重构JWT认证配置代码,拆分配置逻辑到单独方法
3. 优化开发环境下的认证策略,支持无认证和JWT认证自动切换
2026-07-02 08:52:41 +08:00
glz
cc2276fe27 refactor(user-journal): 重构用户期刊关联逻辑,移除枚举依赖并新增二维码功能
1.  删除不再使用的UserJournalTypeEnum枚举
2.  将UserJournal实体的Type字段改为int类型并移除枚举依赖
3.  移除BindJournal相关接口的类型参数校验和赋值逻辑
4.  新增二维码生成和上传功能,为用户期刊绑定QrCodeUrl字段
5.  修复UserAnswerTaskController的用户ID获取逻辑
6.  调整UserJournalQrCodeOutput的字段映射,替换QrCodeContent为QrCodeUrl
2026-07-01 17:59:35 +08:00
glz
7c53adc804 Merge branch 'master' of http://8.137.159.94:3000/glz/QYZH.InteractiveMagazine 2026-07-01 17:05:41 +08:00
glz
b95c5029f1 feat: 添加期刊二维码管理功能,调整UserJournal的UserId为可空类型
1.  新增数据库脚本修改User表UserId字段为可空
2.  新增期刊二维码CRUD接口与控制器
3.  重构绑定期刊逻辑,优化重复绑定校验
4.  删除冗余的JwtHelper重载方法
2026-07-01 17:05:38 +08:00
1285ac4648 Merge branch 'master' of http://8.137.159.94:3000/glz/QYZH.InteractiveMagazine 2026-07-01 15:04:45 +08:00
0227f06658 修改字段类型,以及统计修改 2026-07-01 15:04:42 +08:00
glz
07614b5fe6 feat: 新增上传地址管理、答题评分及社区消息功能
1. 新增UploadDomain实体与上传地址分配逻辑,为用户分配可用上传域名
2. 新增绑定期刊消息推送,通过RabbitMQ传递绑定信息
3. 优化答题评分逻辑,新增完成度阈值判定与社区消息插入
4. 新增配置项用于评分阈值配置
5. 补充相关DTO与实体类字段,完善数据传输与存储
2026-07-01 14:50:37 +08:00
glz
eb4f09e391 feat: 新增打印任务相关功能并完善服务配置
1. 调整回调响应的Code字段类型为object以兼容更多返回值
2. 为JournalDto新增Url和QuestionNo属性
3. 为JournalPageTaskOutput新增Task字段并完善注释
4. 新增打印工作服务的发布、安装卸载脚本与README文档
5. 调整项目配置,添加运行时标识与资源拷贝配置
6. 优化程序启动逻辑,修复工作目录与日志路径问题
7. 简化日志配置,移除多余的Hangfire与AI配置项
8. 完善期刊删除逻辑,关联删除相关子数据
9. 新增期刊发布时的书页任务排序与回调地址补全逻辑
2026-07-01 10:56:18 +08:00
87bc5c0e0c 修改GrowthPoint不为int?的类型 2026-07-01 10:27:19 +08:00
b822489111 删除AnswerStatus字段 2026-07-01 09:20:07 +08:00
2ca2a4c114 解决冲突 2026-07-01 08:51:41 +08:00
c43517969d 临时数据提交 2026-07-01 08:18:51 +08:00
85a58123be 接口逻辑修改 2026-07-01 08:18:07 +08:00
glz
ab09c24338 refactor: 拆分自动铺码服务为独立PrintWorker项目
1.  将原WorkService中的AutoDotCodeConsumer、RabbitMQHostedService、IQueueConsumer迁移至新的PrintWorker项目
2.  移除WorkService中冗余的PrintTool依赖文件复制配置
3.  将PrintToolV2.7相关资源移动到PrintWorker项目中统一管理
4.  从WorkService中移除AutoDotCodeConsumer的服务注册
5.  新增PrintWorker项目的完整宿主程序与配置文件
2026-06-30 17:41:00 +08:00
glz
6ec47220c2 feat: 新增AI自动批改功能,完善任务评分体系
1.  添加AI评分配置节点到appsettings.json
2.  新增NeedAiProcess字段控制是否使用AI批改
3.  扩展答题记录实体,新增理解力、判断力、表达力、说服力评分字段
4.  实现期刊任务AI自动批改消费者逻辑,支持重试机制和图片处理
5.  优化实体类注释和代码结构
6.  新增Dockerfile用于容器化部署
2026-06-30 16:16:12 +08:00
glz
6985a4af42 feat(journal): add journal publish feature
1. add PublishAsync method to IJournalService and implement it in JournalService
2. add publish start/end time fields to Journal entity, DTOs and edit form
3. add journal publish API endpoint
4. add rabbitMQ message sending for book and page publish events
5. clean up some commented-out old controller methods
2026-06-30 11:03:39 +08:00
183 changed files with 10467 additions and 1832 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

125
AdminMenu_RouteSeed.sql Normal file
View File

@ -0,0 +1,125 @@
SET @now = NOW();
SET @createdBy = 'System';
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 `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 `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 `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 `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 `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 `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 `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 `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 `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 `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 `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 `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 `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 `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 `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 `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 `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 `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 `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 `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 `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 `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 `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 `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

@ -1,129 +0,0 @@
-- =====================================================
-- CommunityMessage 社区预置消息表 - 20条模拟测试数据
-- 生成时间: 2026-06-12
-- =====================================================
INSERT INTO [dbo].[CommunityMessage]
([Id], [Status], [IsDeleted], [CreatedBy], [CreatedAt], [UpdatedBy], [UpdatedAt],
[JournalId], [UserId], [UserJournalId], [JournalTaskId], [JournalTaskAnswerId],
[Content], [ImageUrl], [SortOrder], [IsActive], [Type], [LikeCount], [IsFeatured])
VALUES
-- 1. 文章类型 - 期刊导读
(1916234567890001, 1, 0, N'系统管理员', '2026-05-01 09:00:00', NULL, NULL,
1001, 5001, 8001, 3001, 6001,
N'本期期刊聚焦人工智能在教育领域的创新应用,欢迎各位读者踊跃讨论。',
NULL, 1, 1, 1, 42, 1),
-- 2. 引用类型 - 名人名言
(1916234567890002, 1, 0, N'编辑小王', '2026-05-02 10:30:00', NULL, NULL,
1001, 5002, 8002, 3001, 6002,
N'"教育不是灌输,而是点燃火焰。" —— 苏格拉底',
NULL, 2, 1, 2, 88, 1),
-- 3. 公告类型 - 活动通知
(1916234567890003, 1, 0, N'系统管理员', '2026-05-03 14:00:00', N'系统管理员', '2026-05-03 15:00:00',
1001, 5003, 8003, 3002, 6003,
N'【活动通知】第十二期线上读书分享会将于本周六晚8点举行主题为"科技与人文的对话",欢迎大家报名参加!',
N'/uploads/images/activity_notice_202605.png', 1, 1, 3, 156, 1),
-- 4. 文章类型 - 读者投稿
(1916234567890004, 1, 0, N'读者张三', '2026-05-04 08:15:00', NULL, NULL,
1002, 5004, 8004, 3003, 6004,
N'阅读了本期的深度报道《数字时代的阅读习惯变迁》,深有感触。作为一个从纸质书时代走过来的人,既感慨科技带来的便利,也怀念翻页时的触感。',
NULL, 3, 1, 1, 35, 0),
-- 5. 引用类型 - 期刊摘录
(1916234567890005, 1, 0, N'编辑小李', '2026-05-05 11:20:00', NULL, NULL,
1002, 5005, 8005, 3003, 6005,
N'摘自本期特稿:"在信息爆炸的时代,深度阅读能力正在成为一种稀缺的核心竞争力。"',
N'/uploads/images/quote_bg_01.png', 4, 1, 2, 67, 1),
-- 6. 文章类型 - 互动讨论
(1916234567890006, 1, 0, N'读者李四', '2026-05-06 16:45:00', NULL, NULL,
1001, 5006, 8006, 3001, 6006,
N'关于本期讨论话题"AI是否会取代教师",我认为技术只是工具,教育的本质是人与人之间灵魂的碰撞,这是机器无法替代的。',
NULL, 5, 1, 1, 92, 1),
-- 7. 公告类型 - 系统维护
(1916234567890007, 1, 0, N'系统管理员', '2026-05-07 09:00:00', N'系统管理员', '2026-05-07 09:30:00',
1001, 5001, 8001, 3001, 6001,
N'【系统公告】平台将于5月10日凌晨2:00-5:00进行系统升级维护届时服务将短暂中断请各位读者提前保存阅读进度。',
NULL, 1, 1, 3, 23, 0),
-- 8. 文章类型 - 编辑寄语
(1916234567890008, 1, 0, N'主编老陈', '2026-05-08 07:30:00', NULL, NULL,
1003, 5007, 8007, 3004, 6007,
N'新一期杂志如期而至。本期我们特别策划了"青年创作者专栏"收录了十位90后作家的原创作品期待听到你们的声音。',
N'/uploads/images/editor_letter_05.jpg', 1, 1, 1, 210, 1),
-- 9. 引用类型 - 读者金句
(1916234567890009, 1, 0, N'读者王五', '2026-05-09 20:10:00', NULL, NULL,
1002, 5008, 8008, 3003, 6008,
N'"每一本杂志都是一扇窗,让我们看到不同的人生风景。" 感谢社区里每一位分享者。',
NULL, 6, 1, 2, 54, 0),
-- 10. 文章类型 - 话题互动
(1916234567890010, 1, 0, N'读者赵六', '2026-05-10 13:55:00', NULL, NULL,
1003, 5009, 8009, 3004, 6009,
N'看完"城市记忆"系列第三篇,想起了小时候胡同里的叫卖声和邻居家的饭菜香。城市化进程中,我们该如何留住这些温暖的记忆?',
N'/uploads/images/city_memory_03.jpg', 7, 1, 1, 78, 1),
-- 11. 公告类型 - 征稿启事
(1916234567890011, 1, 0, N'编辑小王', '2026-05-11 10:00:00', N'编辑小王', '2026-05-12 08:00:00',
1003, 5010, 8010, 3005, 6010,
N'【征稿启事】第十三期"我的阅读故事"主题征稿正式启动征稿时间5月11日-6月11日优秀作品将刊登在杂志特辑中期待您的来稿。',
N'/uploads/images/call_for_papers_2026.png', 2, 1, 3, 189, 1),
-- 12. 文章类型 - 读书笔记
(1916234567890012, 1, 0, N'读者孙七', '2026-05-12 19:30:00', NULL, NULL,
1001, 5011, 8011, 3001, 6011,
N'本期推荐的三本书我都读完了,最喜欢《思考,快与慢》。作者对直觉思维和理性思维的分析让人醍醐灌顶,强烈推荐给所有社区朋友。',
NULL, 8, 1, 1, 45, 0),
-- 13. 引用类型 - 经典回顾
(1916234567890013, 1, 0, N'编辑小李', '2026-05-13 15:00:00', NULL, NULL,
1002, 5002, 8002, 3003, 6012,
N'回顾创刊号寄语:"我们相信,好的内容值得被慢慢品味。在这个快节奏的时代,让我们一起做慢阅读的实践者。"',
N'/uploads/images/anniversary_retro.png', 3, 1, 2, 134, 1),
-- 14. 文章类型 - 作者互动
(1916234567890014, 1, 0, N'作者林清', '2026-05-14 21:45:00', NULL, NULL,
1003, 5012, 8012, 3004, 6013,
N'感谢大家对《城市记忆》系列的支持和反馈!每一条评论我都认真阅读了。你们的共鸣是我继续创作的最大动力,下一篇将聚焦"消失的声音"。',
NULL, 9, 1, 1, 167, 1),
-- 15. 公告类型 - 获奖公告
(1916234567890015, 1, 0, N'系统管理员', '2026-05-15 12:00:00', N'系统管理员', '2026-05-15 14:00:00',
1001, 5001, 8001, 3002, 6001,
N'【获奖公告】第十一期"最佳读者评论"评选结果揭晓!恭喜读者@书香满园、@夜读人、@思考者三位获得本期精选评论奖,奖品将于一周内寄送。',
N'/uploads/images/award_11.jpg', 4, 1, 3, 302, 1),
-- 16. 文章类型 - 生活随笔
(1916234567890016, 1, 0, N'读者周八', '2026-05-16 07:00:00', NULL, NULL,
1002, 5013, 8013, 3003, 6014,
N'清晨六点,一杯咖啡,一本杂志。这已经成了我三年来雷打不动的习惯。今天的这篇关于极简生活的文章,正好呼应了我最近的生活理念——少即是多。',
N'/uploads/images/morning_read.jpg', 10, 1, 1, 29, 0),
-- 17. 引用类型 - 编辑精选
(1916234567890017, 1, 0, N'主编老陈', '2026-05-17 16:30:00', NULL, NULL,
1003, 5007, 8007, 3004, 6015,
N'本期编辑精选语录:"真正的知识不在于拥有多少信息,而在于能用多少方式去理解世界。" —— 选自专栏文章《认知的边界》',
NULL, 5, 1, 2, 95, 1),
-- 18. 文章类型 - 问题探讨
(1916234567890018, 1, 0, N'读者吴九', '2026-05-18 22:15:00', NULL, NULL,
1001, 5014, 8014, 3001, 6016,
N'想请教大家,本期提到的"元宇宙图书馆"概念你们觉得在技术层面还有多远VR阅读体验是否真的能超越传统阅读期待和大家探讨。',
NULL, 11, 1, 1, 61, 0),
-- 19. 公告类型 - 订阅优惠
(1916234567890019, 1, 0, N'系统管理员', '2026-05-19 09:30:00', NULL, NULL,
1002, 5003, 8003, 3002, 6003,
N'【限时优惠】杂志年度订阅特惠活动开启5月20日-5月31日期间订阅全年杂志享7折优惠老用户续费额外赠送限量版精装合集一本。',
N'/uploads/images/subscription_promo_2026.png', 6, 1, 3, 278, 0),
-- 20. 文章类型 - 社区感言
(1916234567890020, 1, 0, N'读者郑十', '2026-05-20 18:00:00', NULL, NULL,
1003, 5015, 8015, 3004, 6017,
N'加入这个阅读社区整整一年了,从最初的潜水到现在的积极互动,在这里认识了很多志同道合的朋友。感谢平台的用心运营,让我们一起在文字中找到归属。',
N'/uploads/images/community_anniversary.jpg', 12, 1, 1, 445, 1);

View File

@ -0,0 +1,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`);

5
Directory.Build.props Normal file
View File

@ -0,0 +1,5 @@
<Project>
<PropertyGroup>
<NoWarn>$(NoWarn);CS0105;CS0108;CS0168;CS0169;CS0618;CS1570;CS1572;CS1573;CS1591;CS8600;CS8601;CS8602;CS8603;CS8604;CS8618;CS8625;CS8629;CS8634;CS8714;CS9113</NoWarn>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,473 @@
using System.IO.Compression;
using System.Text;
namespace QYZH.InteractiveMagazine.Common.Helpers;
/// <summary>
/// 二维码帮助类
/// </summary>
public static class QrCodeHelper
{
private const int Version = 4;
private const int Size = Version * 4 + 17;
private const int DataCodewordCount = 80;
private const int ErrorCorrectionCodewordCount = 20;
private const int QuietZone = 4;
private const int Scale = 10;
private static readonly int[] AlignmentPatternPositions = [6, 26];
/// <summary>
/// 生成二维码PNG图片字节
/// </summary>
/// <param name="content">二维码内容</param>
/// <returns>PNG图片字节</returns>
public static byte[] GeneratePng(string content)
{
var dataCodewords = CreateDataCodewords(content);
var allCodewords = dataCodewords.Concat(CreateErrorCorrectionCodewords(dataCodewords)).ToArray();
var modules = BuildModules(allCodewords);
var imageSize = (Size + QuietZone * 2) * Scale;
var pixels = new byte[imageSize * imageSize * 4];
for (var i = 0; i < pixels.Length; i += 4)
{
pixels[i] = 255;
pixels[i + 1] = 255;
pixels[i + 2] = 255;
pixels[i + 3] = 255;
}
for (var y = 0; y < Size; y++)
{
for (var x = 0; x < Size; x++)
{
if (modules[y, x])
{
FillModule(pixels, imageSize, x + QuietZone, y + QuietZone);
}
}
}
return EncodePng(imageSize, imageSize, pixels);
}
private static byte[] CreateDataCodewords(string content)
{
var bytes = Encoding.UTF8.GetBytes(content);
if (bytes.Length > 78)
{
throw new InvalidOperationException("二维码内容过长");
}
var bits = new List<bool>();
AppendBits(bits, 0b0100, 4);
AppendBits(bits, bytes.Length, 8);
foreach (var value in bytes)
{
AppendBits(bits, value, 8);
}
var remaining = DataCodewordCount * 8 - bits.Count;
AppendBits(bits, 0, Math.Min(4, remaining));
while (bits.Count % 8 != 0)
{
bits.Add(false);
}
var data = BitsToBytes(bits);
var pad = true;
while (data.Count < DataCodewordCount)
{
data.Add((byte)(pad ? 0xEC : 0x11));
pad = !pad;
}
return data.ToArray();
}
private static bool[,] BuildModules(byte[] codewords)
{
var modules = new bool[Size, Size];
var reserved = new bool[Size, Size];
AddFinder(modules, reserved, 0, 0);
AddFinder(modules, reserved, Size - 7, 0);
AddFinder(modules, reserved, 0, Size - 7);
AddTimingPatterns(modules, reserved);
AddAlignmentPatterns(modules, reserved);
ReserveFormatAreas(reserved);
modules[Version * 4 + 9, 8] = true;
reserved[Version * 4 + 9, 8] = true;
AddDataModules(modules, reserved, codewords);
AddFormatBits(modules);
return modules;
}
private static void AddFinder(bool[,] modules, bool[,] reserved, int left, int top)
{
for (var y = -1; y <= 7; y++)
{
for (var x = -1; x <= 7; x++)
{
var px = left + x;
var py = top + y;
if (px < 0 || py < 0 || px >= Size || py >= Size)
{
continue;
}
reserved[py, px] = true;
modules[py, px] = x >= 0 && x <= 6 && y >= 0 && y <= 6
&& (x == 0 || x == 6 || y == 0 || y == 6 || (x >= 2 && x <= 4 && y >= 2 && y <= 4));
}
}
}
private static void AddTimingPatterns(bool[,] modules, bool[,] reserved)
{
for (var i = 8; i < Size - 8; i++)
{
var isDark = i % 2 == 0;
modules[6, i] = isDark;
modules[i, 6] = isDark;
reserved[6, i] = true;
reserved[i, 6] = true;
}
}
private static void AddAlignmentPatterns(bool[,] modules, bool[,] reserved)
{
foreach (var centerY in AlignmentPatternPositions)
{
foreach (var centerX in AlignmentPatternPositions)
{
if (reserved[centerY, centerX])
{
continue;
}
for (var y = -2; y <= 2; y++)
{
for (var x = -2; x <= 2; x++)
{
var px = centerX + x;
var py = centerY + y;
reserved[py, px] = true;
modules[py, px] = Math.Max(Math.Abs(x), Math.Abs(y)) != 1;
}
}
}
}
}
private static void ReserveFormatAreas(bool[,] reserved)
{
for (var i = 0; i <= 8; i++)
{
reserved[8, i] = true;
reserved[i, 8] = true;
}
for (var i = 0; i < 8; i++)
{
reserved[8, Size - 1 - i] = true;
}
for (var i = 0; i < 7; i++)
{
reserved[Size - 1 - i, 8] = true;
}
}
private static void AddDataModules(bool[,] modules, bool[,] reserved, byte[] codewords)
{
var bits = new List<bool>();
foreach (var codeword in codewords)
{
AppendBits(bits, codeword, 8);
}
var bitIndex = 0;
var upward = true;
for (var right = Size - 1; right >= 1; right -= 2)
{
if (right == 6)
{
right--;
}
for (var i = 0; i < Size; i++)
{
var y = upward ? Size - 1 - i : i;
for (var j = 0; j < 2; j++)
{
var x = right - j;
if (reserved[y, x])
{
continue;
}
var bit = bitIndex < bits.Count && bits[bitIndex++];
if ((x + y) % 2 == 0)
{
bit = !bit;
}
modules[y, x] = bit;
}
}
upward = !upward;
}
}
private static void AddFormatBits(bool[,] modules)
{
var format = GetFormatBits();
for (var i = 0; i <= 5; i++)
{
modules[i, 8] = GetBit(format, i);
}
modules[7, 8] = GetBit(format, 6);
modules[8, 8] = GetBit(format, 7);
modules[8, 7] = GetBit(format, 8);
for (var i = 9; i < 15; i++)
{
modules[8, 14 - i] = GetBit(format, i);
}
for (var i = 0; i < 8; i++)
{
modules[8, Size - 1 - i] = GetBit(format, i);
}
for (var i = 8; i < 15; i++)
{
modules[Size - 15 + i, 8] = GetBit(format, i);
}
modules[Size - 8, 8] = true;
}
private static int GetFormatBits()
{
const int errorCorrectionLevelBits = 1;
const int mask = 0;
var data = (errorCorrectionLevelBits << 3) | mask;
var value = data << 10;
const int generator = 0x537;
for (var i = 14; i >= 10; i--)
{
if (((value >> i) & 1) != 0)
{
value ^= generator << (i - 10);
}
}
return ((data << 10) | (value & 0x3FF)) ^ 0x5412;
}
private static byte[] CreateErrorCorrectionCodewords(byte[] data)
{
var generator = CreateGeneratorPolynomial(ErrorCorrectionCodewordCount);
var result = new byte[ErrorCorrectionCodewordCount];
foreach (var b in data)
{
var factor = b ^ result[0];
Array.Copy(result, 1, result, 0, result.Length - 1);
result[^1] = 0;
for (var i = 0; i < result.Length; i++)
{
result[i] ^= GaloisMultiply(generator[i + 1], factor);
}
}
return result;
}
private static byte[] CreateGeneratorPolynomial(int degree)
{
var result = new List<byte> { 1 };
for (var i = 0; i < degree; i++)
{
var next = new byte[result.Count + 1];
for (var j = 0; j < result.Count; j++)
{
next[j] ^= result[j];
next[j + 1] ^= GaloisMultiply(result[j], GaloisPower(i));
}
result = next.ToList();
}
return result.ToArray();
}
private static byte GaloisPower(int exponent)
{
var value = 1;
for (var i = 0; i < exponent; i++)
{
value <<= 1;
if ((value & 0x100) != 0)
{
value ^= 0x11D;
}
}
return (byte)value;
}
private static byte GaloisMultiply(int x, int y)
{
var result = 0;
while (y != 0)
{
if ((y & 1) != 0)
{
result ^= x;
}
x <<= 1;
if ((x & 0x100) != 0)
{
x ^= 0x11D;
}
y >>= 1;
}
return (byte)result;
}
private static void AppendBits(List<bool> bits, int value, int count)
{
for (var i = count - 1; i >= 0; i--)
{
bits.Add(((value >> i) & 1) != 0);
}
}
private static List<byte> BitsToBytes(List<bool> bits)
{
var result = new List<byte>();
for (var i = 0; i < bits.Count; i += 8)
{
var value = 0;
for (var j = 0; j < 8; j++)
{
value = (value << 1) | (bits[i + j] ? 1 : 0);
}
result.Add((byte)value);
}
return result;
}
private static bool GetBit(int value, int index)
{
return ((value >> index) & 1) != 0;
}
private static void FillModule(byte[] pixels, int imageSize, int moduleX, int moduleY)
{
var startX = moduleX * Scale;
var startY = moduleY * Scale;
for (var y = 0; y < Scale; y++)
{
for (var x = 0; x < Scale; x++)
{
var index = ((startY + y) * imageSize + startX + x) * 4;
pixels[index] = 0;
pixels[index + 1] = 0;
pixels[index + 2] = 0;
pixels[index + 3] = 255;
}
}
}
private static byte[] EncodePng(int width, int height, byte[] rgba)
{
using var output = new MemoryStream();
WriteUInt(output, 0x89504E47);
WriteUInt(output, 0x0D0A1A0A);
using (var ihdr = new MemoryStream())
{
WriteUInt(ihdr, (uint)width);
WriteUInt(ihdr, (uint)height);
ihdr.WriteByte(8);
ihdr.WriteByte(6);
ihdr.WriteByte(0);
ihdr.WriteByte(0);
ihdr.WriteByte(0);
WriteChunk(output, "IHDR", ihdr.ToArray());
}
var stride = width * 4;
using (var raw = new MemoryStream())
{
for (var y = 0; y < height; y++)
{
raw.WriteByte(0);
raw.Write(rgba, y * stride, stride);
}
using var compressed = new MemoryStream();
using (var zlib = new ZLibStream(compressed, CompressionLevel.SmallestSize, true))
{
raw.Position = 0;
raw.CopyTo(zlib);
}
WriteChunk(output, "IDAT", compressed.ToArray());
}
WriteChunk(output, "IEND", []);
return output.ToArray();
}
private static void WriteChunk(Stream stream, string type, byte[] data)
{
WriteUInt(stream, (uint)data.Length);
var typeBytes = Encoding.ASCII.GetBytes(type);
stream.Write(typeBytes);
stream.Write(data);
WriteUInt(stream, Crc32(typeBytes.Concat(data).ToArray()));
}
private static void WriteUInt(Stream stream, uint value)
{
stream.WriteByte((byte)(value >> 24));
stream.WriteByte((byte)(value >> 16));
stream.WriteByte((byte)(value >> 8));
stream.WriteByte((byte)value);
}
private static uint Crc32(byte[] bytes)
{
var crc = 0xFFFFFFFFu;
foreach (var b in bytes)
{
crc ^= b;
for (var i = 0; i < 8; i++)
{
crc = (crc & 1) == 1 ? (crc >> 1) ^ 0xEDB88320u : crc >> 1;
}
}
return ~crc;
}
}

View File

@ -0,0 +1,37 @@
using System.Security.Cryptography;
namespace QYZH.InteractiveMagazine.Common.Helpers;
/// <summary>
/// 随机ID帮助类
/// </summary>
public static class RandomIdHelper
{
private const long DefaultMinValue = 1_000_000_000_000L;
private const long DefaultMaxValue = 9_000_000_000_000_000L;
/// <summary>
/// 生成不可预测的正数长整型ID
/// </summary>
public static long GenerateLongId(long minValue = DefaultMinValue, long maxValue = DefaultMaxValue)
{
if (minValue <= 0 || minValue >= maxValue)
{
throw new ArgumentOutOfRangeException(nameof(minValue), "随机ID范围配置错误");
}
var range = (ulong)(maxValue - minValue);
var limit = ulong.MaxValue - (ulong.MaxValue % range);
Span<byte> bytes = stackalloc byte[sizeof(ulong)];
while (true)
{
RandomNumberGenerator.Fill(bytes);
var value = BitConverter.ToUInt64(bytes);
if (value < limit)
{
return minValue + (long)(value % range);
}
}
}
}

View File

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

View File

@ -0,0 +1,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

@ -49,6 +49,8 @@ public interface IJournalService : IBaseService<Journal>
Task<bool> StatusAsync(long id, JournalStatusEnum status);
Task<bool> PublishAsync(long id);
Task<DotMatrixOutput> PrintCodeAsync(long id);
Task<bool> ResultReportAsync(DotMatrixNoteJournalReportInput input);

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

@ -42,10 +42,10 @@ public interface IUserAnswerTaskService : IBaseService<JournalPageTaskUserAnswer
/// <summary>
/// 获取跨页题目详情
/// </summary>
/// <param name="journalPageTaskId">任务Id</param>
/// <param name="groupId">任务分组IDJournalPageTaskGroupId</param>
/// <param name="userId">用户Id</param>
/// <returns>跨页题目详情</returns>
Task<CrossPageTaskOutput> GetCrossPageTaskAsync(long journalPageTaskId, long userId);
Task<CrossPageTaskOutput> GetCrossPageTaskAsync(long groupId, long userId);
/// <summary>
/// 领取任务积分
@ -62,4 +62,12 @@ public interface IUserAnswerTaskService : IBaseService<JournalPageTaskUserAnswer
/// <param name="userId">用户Id</param>
/// <returns>批量领取结果</returns>
Task<BatchClaimPointsOutput> BatchClaimPointsAsync(BatchClaimPointsInput input, long userId);
/// <summary>
/// 获取目录树形结构(一级目录和二级目录的完整树形结构,包含任务详情)
/// </summary>
/// <param name="journalId">期刊Id</param>
/// <param name="userId">用户Id</param>
/// <returns>目录树形结构数据</returns>
Task<CatalogTreeOutput> GetCatalogTreeAsync(long journalId, long userId);
}

View File

@ -30,4 +30,41 @@ public interface IUserJournalService : IBaseService<UserJournal>
/// <param name="userId">用户Id</param>
/// <param name="id">绑定记录Id</param>
Task UnbindJournalAsync(long userId, long id);
/// <summary>
/// 批量提交期刊二维码生成任务
/// </summary>
/// <param name="input">生成输入</param>
/// <param name="operatorName">操作人名称</param>
/// <returns>提交结果</returns>
Task<CreateUserJournalQrCodeOutput> CreateQrCodesAsync(CreateUserJournalQrCodeInput input, string operatorName);
/// <summary>
/// 分页查询期刊二维码记录
/// </summary>
/// <param name="input">查询条件</param>
/// <returns>分页结果</returns>
Task<PageListModel<UserJournalQrCodeOutput>> GetQrCodePageListAsync(UserJournalQrCodeQueryInput input);
/// <summary>
/// 获取期刊二维码详情
/// </summary>
/// <param name="id">二维码记录Id</param>
/// <returns>二维码记录</returns>
Task<UserJournalQrCodeOutput> GetQrCodeDetailAsync(long id);
/// <summary>
/// 根据期刊Id获取未绑定用户的二维码列表
/// </summary>
/// <param name="journalId">期刊Id</param>
/// <returns>未绑定二维码列表</returns>
Task<List<UserJournalQrCodeOutput>> GetUnboundQrCodesByJournalIdAsync(long journalId);
/// <summary>
/// 删除未绑定的期刊二维码记录
/// </summary>
/// <param name="input">删除输入</param>
/// <param name="operatorName">操作人名称</param>
/// <returns>是否成功</returns>
Task<bool> DeleteQrCodeAsync(DeleteUserJournalQrCodeInput input, string operatorName);
}

View File

@ -68,11 +68,6 @@ public static class JwtHelper
return $"{AdminTokenKeyPrefix}:{userId}";
}
public static string BuildAdminTokenKey(long userId)
{
return BuildAdminTokenKey(userId.ToString());
}
public static string BuildWeChatTokenKey(string wxUserId, string userId)
{
return $"{WeChatTokenKeyPrefix}:{wxUserId}:{userId}";
@ -124,7 +119,7 @@ public static class JwtHelper
var tokenHandler = new JwtSecurityTokenHandler();
if (tokenHandler.ReadToken(token) is JwtSecurityToken jwtToken)
{
var userIdClaim = jwtToken.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier);
var userIdClaim = GetClaim(jwtToken, ClaimTypes.NameIdentifier, JwtRegisteredClaimNames.NameId, JwtRegisteredClaimNames.Sub);
if (long.TryParse(userIdClaim?.Value, out long userId))
{
return userId;
@ -162,8 +157,13 @@ public static class JwtHelper
var tokenHandler = new JwtSecurityTokenHandler();
if (tokenHandler.ReadToken(token) is JwtSecurityToken jwtToken)
{
return jwtToken.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Name)?.Value ?? string.Empty;
return GetClaim(jwtToken, ClaimTypes.Name, JwtRegisteredClaimNames.UniqueName, JwtRegisteredClaimNames.Name)?.Value ?? string.Empty;
}
return string.Empty;
}
private static Claim? GetClaim(JwtSecurityToken jwtToken, params string[] claimTypes)
{
return jwtToken.Claims.FirstOrDefault(c => claimTypes.Contains(c.Type));
}
}

View File

@ -21,6 +21,9 @@ namespace QYZH.InteractiveMagazine.Infrastructure.Extensions;
/// </summary>
public static class DependencyInjectionExtensions
{
private const string NoAuthScheme = "NoAuth";
private const string DevelopmentAuthScheme = "DevelopmentSmartAuth";
/// <summary>
/// Registers infrastructure services.
/// </summary>
@ -30,74 +33,105 @@ public static class DependencyInjectionExtensions
services.AddTransient<GlobalExceptionMiddleware>();
services.AddTransient<OperationLogMiddleware>();
services.AddScoped<OperationLogActionFilter>();
}
private static void AddJwtAuthentication(IServiceCollection services, IConfiguration configuration, IWebHostEnvironment? environment = null)
{
var jwtSettings = configuration.GetSection("JwtSettings").Get<JwtSettings>()!;
services.AddSingleton(jwtSettings);
if (environment?.IsDevelopment() == true)
{
services.AddAuthentication("NoAuth")
.AddScheme<AuthenticationSchemeOptions, NoAuthHandler>("NoAuth", options => { });
services.AddAuthentication(options =>
{
options.DefaultScheme = DevelopmentAuthScheme;
options.DefaultChallengeScheme = DevelopmentAuthScheme;
})
.AddPolicyScheme(DevelopmentAuthScheme, null, options =>
{
options.ForwardDefaultSelector = context =>
{
var authHeader = context.Request.Headers.Authorization.FirstOrDefault();
return !string.IsNullOrWhiteSpace(authHeader) &&
authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)
? JwtBearerDefaults.AuthenticationScheme
: NoAuthScheme;
};
})
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options => ConfigureJwtBearer(options, jwtSettings))
.AddScheme<AuthenticationSchemeOptions, NoAuthHandler>(NoAuthScheme, options => { });
return;
}
var jwtSettings = configuration.GetSection("JwtSettings").Get<JwtSettings>()!;
services.AddSingleton(jwtSettings);
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
.AddJwtBearer(options => ConfigureJwtBearer(options, jwtSettings));
}
private static void ConfigureJwtBearer(JwtBearerOptions options, JwtSettings jwtSettings)
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = jwtSettings.Issuer,
ValidateAudience = true,
ValidAudience = jwtSettings.Audience,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.SecretKey!)),
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
};
options.Events = new JwtBearerEvents
{
OnTokenValidated = async context =>
{
options.TokenValidationParameters = new TokenValidationParameters
var currentToken = GetBearerToken(context);
if (string.IsNullOrEmpty(currentToken))
{
ValidateIssuer = true,
ValidIssuer = jwtSettings.Issuer,
ValidateAudience = true,
ValidAudience = jwtSettings.Audience,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.SecretKey!)),
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
};
context.Fail("Invalid token");
return;
}
options.Events = new JwtBearerEvents
var userId = context.Principal?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId))
{
OnTokenValidated = async context =>
context.Fail("Invalid token");
return;
}
var wxUserId = context.Principal?.FindFirst(JwtHelper.WxUserIdClaimType)?.Value;
if (string.IsNullOrEmpty(wxUserId))
{
var adminToken = await RedisHelper.GetAsync(JwtHelper.BuildAdminTokenKey(userId));
if (adminToken == currentToken)
{
var currentToken = GetBearerToken(context);
if (string.IsNullOrEmpty(currentToken))
{
context.Fail("Invalid token");
return;
}
var userId = context.Principal?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId))
{
context.Fail("Invalid token");
return;
}
var adminToken = await RedisHelper.GetAsync(JwtHelper.BuildAdminTokenKey(userId));
var wxUserId = context.Principal?.FindFirst(JwtHelper.WxUserIdClaimType)?.Value;
if (string.IsNullOrEmpty(wxUserId) && !string.IsNullOrEmpty(adminToken))
{
return;
}
if (!string.IsNullOrEmpty(wxUserId))
{
var wechatToken = await RedisHelper.GetAsync(JwtHelper.BuildWeChatTokenKey(wxUserId, userId));
if (wechatToken == currentToken)
{
return;
}
}
context.Fail("Token expired, please login again");
await RefreshRedisTokenExpiryAsync(JwtHelper.BuildAdminTokenKey(userId), currentToken, jwtSettings);
return;
}
};
});
context.Fail("Token expired, please login again");
return;
}
var wechatToken = await RedisHelper.GetAsync(JwtHelper.BuildWeChatTokenKey(wxUserId, userId));
if (wechatToken == currentToken)
{
await RefreshRedisTokenExpiryAsync(JwtHelper.BuildWeChatTokenKey(wxUserId, userId), currentToken, jwtSettings);
return;
}
context.Fail("Token expired, please login again");
}
};
}
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)

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,5 +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;
@ -9,13 +11,17 @@ namespace QYZH.InteractiveMagazine.Infrastructure.RabbitMQ
public class RabbitMQService : IRabbitMQService
{
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)
public RabbitMQService(IRabbitMQConnection connection, IConfiguration configuration, ILogger<RabbitMQService> logger)
{
_connection = connection ?? throw new ArgumentNullException(nameof(connection));
_configuration = configuration;
_logger = logger;
}
@ -52,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;
}
}
@ -112,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;
@ -130,7 +138,13 @@ namespace QYZH.InteractiveMagazine.Infrastructure.RabbitMQ
public async Task ReceiveAsync(string exchange, string queueName, string routingKey, Func<IChannel, BasicDeliverEventArgs, Task> callback, CancellationToken cancellationToken = default)
{
var channel = await _connection.CreateChannel();
//await channel.BasicQosAsync(0, 10, false); // 一次最多接收10条未确认的消息
var prefetchCount = _configuration.GetValue<ushort>("RabbitMq:PrefetchCount");
if (prefetchCount == 0)
{
prefetchCount = 1;
}
await channel.BasicQosAsync(0, prefetchCount, false, cancellationToken);
// 声明 Exchange持久化
await channel.ExchangeDeclareAsync(exchange: exchange, type: "direct", durable: true, autoDelete: false, arguments: null);

View File

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

View File

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

View File

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

View File

@ -0,0 +1,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

@ -1,4 +1,5 @@
using QYZH.InteractiveMagazine.Models.Enum;
using System.Text.Json.Serialization;
namespace QYZH.InteractiveMagazine.Models.Dto;
@ -10,17 +11,15 @@ public class BindJournalInput
/// <summary>
/// 期刊模板Id扫码解析的期刊定义Id
/// </summary>
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)]
public long JournalId { get; set; }
/// <summary>
/// 实例化期刊Id扫码解析的具体期刊实例Id可选
/// </summary>
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)]
public long Id { get; set; }
/// <summary>
/// 关联类型: Read(已读), Favorite(收藏), Subscribe(订阅),默认 Subscribe
/// </summary>
public string Type { get; set; } = UserJournalTypeEnum.Subscribe.ToString();
}
/// <summary>
@ -59,6 +58,37 @@ public class BindJournalOutput
public DateTime CreatedAt { get; set; }
}
/// <summary>
/// 用户绑定期刊消息
/// </summary>
public class BindJournalMessage
{
/// <summary>
/// 用户Id
/// </summary>
public long UserId { get; set; }
/// <summary>
/// 期刊Id
/// </summary>
public long JournalId { get; set; }
/// <summary>
/// 期刊开始时间
/// </summary>
public DateTime? StartTime { get; set; }
/// <summary>
/// 期刊结束时间
/// </summary>
public DateTime? EndTime { get; set; }
/// <summary>
/// 用户作答信息上传地址
/// </summary>
public string UploadDomain { get; set; } = string.Empty;
}
/// <summary>
/// 用户期刊关联查询输入DTO
/// </summary>
@ -79,3 +109,172 @@ public class UserJournalQueryInput : PageQueryModel
/// </summary>
public string? Type { get; set; }
}
/// <summary>
/// 生成期刊二维码输入DTO
/// </summary>
public class CreateUserJournalQrCodeInput
{
/// <summary>
/// 期刊Id
/// </summary>
public long JournalId { get; set; }
/// <summary>
/// 生成数量
/// </summary>
public int Count { get; set; } = 1;
}
/// <summary>
/// 生成期刊二维码提交结果
/// </summary>
public class CreateUserJournalQrCodeOutput
{
/// <summary>
/// 期刊Id
/// </summary>
public long JournalId { get; set; }
/// <summary>
/// 请求生成数量
/// </summary>
public int RequestedCount { get; set; }
/// <summary>
/// 已提交生成数量
/// </summary>
public int AcceptedCount { get; set; }
/// <summary>
/// 二维码记录Id列表
/// </summary>
[JsonNumberHandling(JsonNumberHandling.WriteAsString)]
public List<long> RecordIds { get; set; } = [];
/// <summary>
/// 是否后台生成
/// </summary>
public bool IsAsync { get; set; }
/// <summary>
/// 提示信息
/// </summary>
public string Message { get; set; } = string.Empty;
}
/// <summary>
/// 期刊二维码查询输入DTO
/// </summary>
public class GenerateUserJournalQrCodeMessage
{
/// <summary>
/// 二维码记录Id列表
/// </summary>
public List<long> RecordIds { get; set; } = [];
/// <summary>
/// 操作人名称
/// </summary>
public string OperatorName { get; set; } = string.Empty;
}
/// <summary>
/// 期刊二维码查询输入DTO
/// </summary>
public class UserJournalQrCodeQueryInput : PageQueryModel
{
/// <summary>
/// 期刊Id
/// </summary>
public long? JournalId { get; set; }
/// <summary>
/// 用户Id
/// </summary>
public long? UserId { get; set; }
/// <summary>
/// 是否已绑定用户
/// </summary>
public bool? IsBound { get; set; }
/// <summary>
/// 状态
/// </summary>
public int? Status { get; set; }
}
/// <summary>
/// 期刊二维码输出DTO
/// </summary>
public class UserJournalQrCodeOutput
{
/// <summary>
/// 二维码记录Id
/// </summary>
[JsonNumberHandling(JsonNumberHandling.WriteAsString)]
public long Id { get; set; }
/// <summary>
/// 期刊Id
/// </summary>
public long JournalId { get; set; }
/// <summary>
/// 期刊名称
/// </summary>
public string JournalName { get; set; } = string.Empty;
/// <summary>
/// 用户Id
/// </summary>
public long? UserId { get; set; }
/// <summary>
/// 用户昵称
/// </summary>
public string? UserName { get; set; }
/// <summary>
/// 关联类型
/// </summary>
public string Type { get; set; } = string.Empty;
/// <summary>
/// 状态
/// </summary>
public string Status { get; set; } = string.Empty;
/// <summary>
/// 是否已绑定用户
/// </summary>
public bool IsBound { get; set; }
/// <summary>
/// 二维码内容
/// </summary>
public string QrCodeUrl { get; set; } = string.Empty;
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreatedAt { get; set; }
/// <summary>
/// 绑定时间
/// </summary>
public DateTime? BoundAt { get; set; }
}
/// <summary>
/// 删除期刊二维码输入DTO
/// </summary>
public class DeleteUserJournalQrCodeInput
{
/// <summary>
/// 二维码记录Id列表
/// </summary>
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)]
public List<long> Ids { get; set; } = [];
}

View File

@ -120,8 +120,35 @@ namespace QYZH.InteractiveMagazine.Models.Dto.Journal
/// </summary>
public string? DownloadJournalPagePdfName { get; set; }
/// <summary>
/// 发布开始时间
/// </summary>
public DateTime? StartTime { get; set; }
/// <summary>
/// 发布结束时间
/// </summary>
public DateTime? EndTime { get; set; }
public string CreatedBy { get; set; }
public DateTime CreatedAt { get; set; }
}
public class JournalPublishBookMessage
{
public long BookId { get; set; }
public DateTime StartTime { get; set; }
public DateTime EndTime { get; set; }
}
public class JournalPublishBookPageMessage
{
public long BookId { get; set; }
public long PageId { get; set; }
public string Url { get; set; }
public string? PageNo { get; set; }
public string? Layout { get; set; }
public string QuestionNo { get; set; }
}
}

View File

@ -69,6 +69,16 @@ namespace QYZH.InteractiveMagazine.Models.Dto.Journal
/// </summary>
public string? PdfPreviewUrl { get; set; }
/// <summary>
/// Desc:发布开始时间
/// </summary>
public DateTime? StartTime { get; set; }
/// <summary>
/// Desc:发布结束时间
/// </summary>
public DateTime? EndTime { get; set; }
}
public partial class JournalEditDto: JournalAddDto
{

View File

@ -29,5 +29,10 @@ namespace QYZH.InteractiveMagazine.Models.Dto.Journal
/// </summary>
public TaskBankTypeEnum Type { get; set; }
/// <summary>
/// 是否需要AI批改默认需要
/// </summary>
public bool NeedAiProcess { get; set; } = true;
}
}

View File

@ -18,7 +18,10 @@ namespace QYZH.InteractiveMagazine.Models.Dto.Journal
/// 书页Id
/// </summary>
public long JournalPageId { get; set; }
/// <summary>
/// 任务信息
/// </summary>
public string Task { get; set; }
/// <summary>
/// 题号
/// </summary>
@ -74,6 +77,11 @@ namespace QYZH.InteractiveMagazine.Models.Dto.Journal
/// Prompt配置
/// </summary>
public string Prompt { get; set; }
/// <summary>
/// 是否需要AI批改
/// </summary>
public bool NeedAiProcess { get; set; }
}
/// <summary>

View File

@ -44,7 +44,7 @@ namespace QYZH.InteractiveMagazine.Models.Dto.Journal
/// <summary>
/// 成长值
/// </summary>
public float GrowthPoint { get; set; }
public int GrowthPoint { get; set; }
/// <summary>
/// 理解力评分
/// </summary>
@ -71,6 +71,11 @@ namespace QYZH.InteractiveMagazine.Models.Dto.Journal
/// </summary>
public string Prompt { get; set; }
/// <summary>
/// 是否需要AI批改false时由人工批改
/// </summary>
public bool NeedAiProcess { get; set; } = true;
}
public class JournalPageTaskComplementInput

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

@ -8,7 +8,7 @@ public class BatchClaimPointsInput
/// <summary>
/// 任务分组Id列表JournalPageTaskGroupId
/// </summary>
public List<long> GroupIds { get; set; } = [];
public HashSet<long> GroupIds { get; set; } = [];
}
/// <summary>
@ -26,21 +26,6 @@ public class BatchClaimPointsOutput
/// </summary>
public int FailCount { get; set; }
/// <summary>
/// 累计获得积分
/// </summary>
public int TotalPoints { get; set; }
/// <summary>
/// 累计获得成长值
/// </summary>
public int TotalGrowthPoints { get; set; }
/// <summary>
/// 领取后积分余额
/// </summary>
public int BalanceAfter { get; set; }
/// <summary>
/// 成功领取详情列表
/// </summary>
@ -58,17 +43,7 @@ public class BatchClaimPointsOutput
public class BatchClaimSuccessItem
{
/// <summary>
/// 任务分组Id
/// </summary>
public long GroupId { get; set; }
/// <summary>
/// 任务名称
/// </summary>
public string TaskName { get; set; }
/// <summary>
/// 任务编号(从 JournalPageTask.No 字段按 - 分割取索引2
/// 任务编号(从 JournalPageTask.No 字段按 - 分割取索引2前面加"任务"
/// </summary>
public string TaskNo { get; set; }
@ -83,7 +58,7 @@ public class BatchClaimSuccessItem
public int GrowthPoints { get; set; }
/// <summary>
/// 题目得分(从 JournalPageTaskUserAnswer 表获取,该 GroupId 下所有相关任务得分之和)
/// 题目得分
/// </summary>
public float Score { get; set; }
}
@ -98,6 +73,11 @@ public class BatchClaimFailItem
/// </summary>
public long GroupId { get; set; }
/// <summary>
/// 任务编号(从 JournalPageTask.No 字段按 - 分割取索引2前面加"任务"
/// </summary>
public string TaskNo { get; set; }
/// <summary>
/// 失败原因
/// </summary>

View File

@ -0,0 +1,130 @@
namespace QYZH.InteractiveMagazine.Models.Dto.UserAnswerTaskService;
/// <summary>
/// 目录树形结构输出(包含一级目录列表)
/// </summary>
public class CatalogTreeOutput
{
/// <summary>
/// 一级目录列表
/// </summary>
public List<FirstLevelCatalogItem> FirstLevelCatalogs { get; set; } = [];
}
/// <summary>
/// 一级目录项输出
/// </summary>
public class FirstLevelCatalogItem
{
/// <summary>
/// 一级目录ID
/// </summary>
public long CatalogId { get; set; }
/// <summary>
/// 一级目录名称
/// </summary>
public string CatalogName { get; set; }
/// <summary>
/// 文章篇数(来自二级目录对应的所有有题目的页面数总和)
/// </summary>
public int ArticleCount { get; set; }
/// <summary>
/// 任务数量来自二级目录对应的所有任务按GroupId去重后的总和
/// </summary>
public int TaskCount { get; set; }
/// <summary>
/// 二级目录列表
/// </summary>
public List<SecondLevelCatalogItem> Children { get; set; } = [];
}
/// <summary>
/// 二级目录项输出(含页码范围、任务数量、状态、总成长值、总积分及任务列表)
/// </summary>
public class SecondLevelCatalogItem
{
/// <summary>
/// 二级目录ID
/// </summary>
public long CatalogId { get; set; }
/// <summary>
/// 二级目录名称
/// </summary>
public string CatalogName { get; set; }
/// <summary>
/// 起始页码
/// </summary>
public int StartPage { get; set; }
/// <summary>
/// 结束页码
/// </summary>
public int EndPage { get; set; }
/// <summary>
/// 任务数量按GroupId去重后的数量
/// </summary>
public int TaskCount { get; set; }
/// <summary>
/// 状态格式如1/2 进行中、0/2 未开始、2/2 已完成)
/// </summary>
public string Status { get; set; }
/// <summary>
/// 总成长值该二级目录下所有任务的成长值总和按GroupId去重
/// </summary>
public int TotalGrowthPoints { get; set; }
/// <summary>
/// 总积分该二级目录下所有任务的积分总和按GroupId去重
/// </summary>
public int TotalPoints { get; set; }
/// <summary>
/// 任务列表(每个任务的详细信息)
/// </summary>
public List<CatalogTaskItem> Tasks { get; set; } = [];
}
/// <summary>
/// 目录任务项输出(任务详情)
/// </summary>
public class CatalogTaskItem
{
/// <summary>
/// 任务分组IDGroupId
/// </summary>
public long GroupId { get; set; }
/// <summary>
/// 任务名称取主任务的Task字段
/// </summary>
public string TaskName { get; set; }
/// <summary>
/// 任务状态(未开始、进行中、已完成)
/// </summary>
public string Status { get; set; }
/// <summary>
/// 成长值取主任务的GrowthPoint
/// </summary>
public int GrowthPoints { get; set; }
/// <summary>
/// 积分值取主任务的Points
/// </summary>
public int Points { get; set; }
/// <summary>
/// 答题时长分钟该GroupId下所有任务的AnswerSeconds累加后转换为分钟
/// </summary>
public double AnswerMinutes { get; set; }
}

View File

@ -6,37 +6,22 @@ namespace QYZH.InteractiveMagazine.Models.Dto.UserAnswerTaskService;
public class ClaimTaskPointsOutput
{
/// <summary>
/// 积分流水记录Id
/// 总成长值(主任务的 GrowthPoint
/// </summary>
public long RecordId { get; set; }
public int TotalGrowthPoints { get; set; }
/// <summary>
/// 操作前积分余额
/// 总积分(主任务的 Points
/// </summary>
public int PreviousBalance { get; set; }
public int TotalPoints { get; set; }
/// <summary>
/// 操作后积分余额
/// </summary>
public int NewBalance { get; set; }
/// <summary>
/// 实际增加数量
/// </summary>
public int AddedAmount { get; set; }
/// <summary>
/// 任务名称
/// </summary>
public string TaskName { get; set; }
/// <summary>
/// 任务编号(从 JournalPageTask.No 字段按 - 分割取索引2
/// </summary>
public string TaskNo { get; set; }
/// <summary>
/// 题目得分(从 JournalPageTaskUserAnswer 表获取JournalPageTaskId == GroupId 的 Score
/// 得分(从 JournalPageTaskUserAnswer 表获取JournalPageTaskId == GroupId 的 Score
/// </summary>
public float Score { get; set; }
/// <summary>
/// 任务编号(从 JournalPageTask.No 字段按 - 分割取索引2前面加"任务"
/// </summary>
public string TaskNo { get; set; }
}

View File

@ -26,6 +26,11 @@ public class UsersOutput
/// </summary>
public string WxUserId { get; set; } = string.Empty;
/// <summary>
/// 微信用户名
/// </summary>
public string? WxUserName { get; set; }
/// <summary>
/// 昵称
/// </summary>
@ -55,6 +60,11 @@ public class UsersOutput
/// 当前成长值
/// </summary>
public int GrowthPoints { get; set; }
/// <summary>
/// 用户作答信息上传地址
/// </summary>
public string UploadDomain { get; set; } = string.Empty;
}
/// <summary>

View File

@ -146,6 +146,11 @@ public class WxUserOutput
/// 创建时间
/// </summary>
public DateTime CreatedAt { get; set; }
/// <summary>
/// 用户作答信息上传地址
/// </summary>
public string UploadDomain { get; set; } = string.Empty;
}
/// <summary>

View File

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

View File

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

View File

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

View File

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

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

View File

@ -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(){
@ -106,6 +106,20 @@ namespace QYZH.InteractiveMagazine.Models.Entity
/// </summary>
public string DownloadJournalPagePdfName { 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:书籍描述/简介

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

@ -1,23 +1,14 @@
using QYZH.InteractiveMagazine.Models.Enum;
using SqlSugar;
using System;
using System.Linq;
using System.Text;
namespace QYZH.InteractiveMagazine.Models.Entity
{
///<summary>
///杂志任务;
///</summary>
[SugarTable("JournalPageTask")]
public partial class JournalPageTask : SqlSugarBaseEntity
{
public JournalPageTask()
{
}
[SugarTable("Journal_PageTask")]
public class JournalPageTask : SqlSugarBaseEntity
{
/// <summary>
/// Desc:书id
/// Default:
@ -64,7 +55,7 @@ namespace QYZH.InteractiveMagazine.Models.Entity
/// Default:0
/// Nullable:True
/// </summary>
public int? GrowthPoint { get; set; }
public int GrowthPoint { get; set; }
/// <summary>
/// Desc:积分设置

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

@ -1,215 +1,236 @@
using QYZH.InteractiveMagazine.Models.Enum;
using SqlSugar;
namespace QYZH.InteractiveMagazine.Models.Entity
{
///<summary>
///杂志任务用户作答记录;
///</summary>
[SugarTable("JournalPageTaskUserAnswer")]
/// <summary>
/// 杂志任务用户作答记录
/// </summary>
[SugarTable("Journal_PageTaskUserAnswer")]
public class JournalPageTaskUserAnswer : SqlSugarBaseEntity
{
/// <summary>
/// Desc:书id
/// Default:
/// Nullable:False
/// </summary>
/// </summary>
public long JournalId { get; set; }
/// <summary>
/// Desc:书页id
/// Default:
/// Nullable:False
/// </summary>
/// </summary>
public long JournalPageId { get; set; }
/// <summary>
/// Desc:任务id
/// Default:
/// Nullable:False
/// </summary>
/// </summary>
public long JournalPageTaskId { get; set; }
/// <summary>
/// Desc:任务分组id
/// Default:
/// Nullable:False
/// </summary>
/// </summary>
public long JournalPageTaskGroupId { get; set; }
/// <summary>
/// Desc:用户id
/// Default:
/// Nullable:False
/// </summary>
/// </summary>
public long UserId { get; set; }
/// <summary>
/// Desc:作答结果
/// Default:
/// Nullable:True
/// </summary>
/// </summary>
public string Result { get; set; }
/// <summary>
/// Desc:积分
/// Default:0
/// Nullable:False
/// </summary>
public float Points { get; set; }
/// </summary>
public int Points { get; set; }
/// <summary>
/// Desc:成长值
/// Default:0
/// Nullable:False
/// </summary>
public float GrowthPoints { get; set; }
/// </summary>
public int GrowthPoints { get; set; }
/// <summary>
/// Desc:题目得分
/// Default:0
/// Nullable:False
/// </summary>
/// </summary>
public float Score { get; set; }
/// <summary>
/// Desc:理解力评分
/// Default:0
/// Nullable:False
/// </summary>
public float Comprehension { get; set; }
/// <summary>
/// Desc:判断力评分
/// Default:0
/// Nullable:False
/// </summary>
public float Judgment { get; set; }
/// <summary>
/// Desc:表达力评分
/// Default:0
/// Nullable:False
/// </summary>
public float Expression { get; set; }
/// <summary>
/// Desc:说服力评分
/// Default:0
/// Nullable:False
/// </summary>
public float Persuasiveness { get; set; }
/// <summary>
/// Desc:题目作答图片地址
/// Default:
/// Nullable:True
/// </summary>
/// </summary>
public string QuestionAnswerUrl { get; set; }
/// <summary>
/// Desc:答案图片地址
/// Default:
/// Nullable:True
/// </summary>
/// </summary>
public string AnswerUrl { get; set; }
/// <summary>
/// Desc:页面作答图片地址
/// Default:
/// Nullable:True
/// </summary>
/// </summary>
public string PageAnswerUrl { get; set; }
/// <summary>
/// Desc:乐观锁
/// Default:
/// Nullable:False
/// </summary>
/// </summary>
public int Revision { get; set; }
/// <summary>
/// Desc:作答状态
/// Default:
/// Nullable:False
/// </summary>
public int AnswerStatus { get; set; }
/// <summary>
/// Desc:作答开始时间
/// Default:
/// Nullable:True
/// </summary>
/// </summary>
public DateTime AnswerStartTime { get; set; }
/// <summary>
/// Desc:作答结束时间
/// Default:
/// Nullable:True
/// </summary>
/// </summary>
public DateTime AnswerEndTime { get; set; }
/// <summary>
/// Desc:作答耗时(秒)
/// Default:0
/// Nullable:False
/// </summary>
/// </summary>
public int AnswerSeconds { get; set; }
/// <summary>
/// Desc:图像识别结果
/// Default:
/// Nullable:False
/// </summary>
/// </summary>
public int ImageRecognition { get; set; }
/// <summary>
/// Desc:书页序号
/// Default:
/// Nullable:False
/// </summary>
/// </summary>
public int JournalPageNum { get; set; }
/// <summary>
/// Desc:修改次数
/// Default:
/// Nullable:False
/// </summary>
/// </summary>
public int Modify { get; set; }
/// <summary>
/// Desc:最后标签id
/// Default:
/// Nullable:False
/// </summary>
/// </summary>
public long LastTag { get; set; }
/// <summary>
/// Desc:点读书页序号
/// Default:
/// Nullable:False
/// </summary>
/// </summary>
public int DotPageNum { get; set; }
/// <summary>
/// Desc:页面结果图片地址
/// Default:
/// Nullable:True
/// </summary>
/// </summary>
public string PageResultUrl { get; set; }
/// <summary>
/// Desc:题型
/// Default:
/// Nullable:True
/// </summary>
/// </summary>
public string Type { get; set; }
/// <summary>
/// Desc:点读书页编号
/// Default:
/// Nullable:True
/// </summary>
/// </summary>
public string DotPageNo { get; set; }
/// <summary>
/// Desc:页面作答点读图片地址
/// Default:
/// Nullable:True
/// </summary>
/// </summary>
public string PageAnswerDotUrl { get; set; }
/// <summary>
/// Desc:中断次数
/// Default:0
/// Nullable:False
/// </summary>
/// </summary>
public int BreakCount { get; set; }
/// <summary>
/// Desc:中断时间记录
/// Default:
/// Nullable:True
/// </summary>
/// </summary>
public string BreakTimes { get; set; }
/// <summary>
/// Desc:作业状态
/// Default:
/// Nullable:True
/// </summary>
public string AssignmentStatus { get; set; }
/// </summary>
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>
@ -46,7 +46,7 @@ namespace QYZH.InteractiveMagazine.Models.Entity
/// <summary>
/// Points awarded.
/// </summary>
public float Points { get; set; }
public int Points { get; set; }
/// <summary>
/// Growth points awarded.
@ -58,6 +58,26 @@ namespace QYZH.InteractiveMagazine.Models.Entity
/// </summary>
public float Score { get; set; }
/// <summary>
/// Comprehension score.
/// </summary>
public float Comprehension { get; set; }
/// <summary>
/// Judgment score.
/// </summary>
public float Judgment { get; set; }
/// <summary>
/// Expression score.
/// </summary>
public float Expression { get; set; }
/// <summary>
/// Persuasiveness score.
/// </summary>
public float Persuasiveness { get; set; }
/// <summary>
/// Question answer image URL.
/// </summary>
@ -76,12 +96,7 @@ namespace QYZH.InteractiveMagazine.Models.Entity
/// <summary>
/// Optimistic concurrency revision.
/// </summary>
public int Revision { get; set; }
/// <summary>
/// Answer status.
/// </summary>
public int AnswerStatus { get; set; }
public int Revision { get; set; }
/// <summary>
/// Answer start time.
@ -156,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

@ -0,0 +1,25 @@
using SqlSugar;
namespace QYZH.InteractiveMagazine.Models.Entity
{
/// <summary>
/// 上传地址管理表
/// </summary>
[SugarTable("System_UploadDomain")]
public partial class UploadDomain : SqlSugarBaseEntity
{
/// <summary>
/// Desc:上传地址
/// Default:
/// Nullable:False
/// </summary>
public string Domain { get; set; } = string.Empty;
/// <summary>
/// Desc:已分配人数
/// Default:0
/// Nullable:False
/// </summary>
public int AssignedCount { get; set; }
}
}

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()
@ -20,7 +20,7 @@ namespace QYZH.InteractiveMagazine.Models.Entity
/// Nullable:False
/// </summary>
[SugarColumn(ColumnName = "UserId")]
public long UserId { get; set; }
public long? UserId { get; set; }
/// <summary>
/// Desc:期刊模板Id对应Journal表的期刊定义
@ -31,12 +31,18 @@ namespace QYZH.InteractiveMagazine.Models.Entity
public long JournalId { get; set; }
/// <summary>
/// Desc:关联类型: Read(已读), Favorite(收藏), Subscribe(订阅)
/// Default:Read
/// Desc:关联类型: 默认0
/// Nullable:False
/// </summary>
[SugarColumn(ColumnName = "Type")]
public UserJournalTypeEnum Type { get; set; }
public int Type { get; set; } = 0;
/// <summary>
/// Desc:二维码地址
/// Default:
/// Nullable:True
/// </summary>
[SugarColumn(ColumnName = "QrCodeUrl")]
public string? QrCodeUrl { get; set; }
}
}

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>
@ -57,5 +57,12 @@ namespace QYZH.InteractiveMagazine.Models.Entity
/// Nullable:False
/// </summary>
public bool IsLastOnline { get; set; }
/// <summary>
/// Desc:用户作答信息上传地址
/// Default:
/// Nullable:False
/// </summary>
public string UploadDomain { get; set; } = string.Empty;
}
}

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

@ -17,5 +17,17 @@ public enum UserJournalStatusEnum
/// 正常
/// </summary>
[Description("正常")]
Active = 1
Active = 1,
/// <summary>
/// 生成中
/// </summary>
[Description("生成中")]
Generating = 2,
/// <summary>
/// 生成失败
/// </summary>
[Description("生成失败")]
Failed = 3
}

View File

@ -1,25 +0,0 @@
using System.ComponentModel;
namespace QYZH.InteractiveMagazine.Models.Enum;
/// <summary>
/// 用户期刊关联类型枚举
/// </summary>
public enum UserJournalTypeEnum
{
/// <summary>
/// 已读
/// </summary>
[Description("已读")]
Read = 1,
/// <summary>
/// 收藏
/// </summary>
[Description("收藏")]
Favorite = 2,
/// <summary>
/// 订阅
/// </summary>
[Description("订阅")]
Subscribe = 3
}

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

@ -0,0 +1,412 @@
using QYZH.InteractiveMagazine.Common.Extensions;
using QYZH.InteractiveMagazine.Infrastructure.OSS;
using QYZH.InteractiveMagazine.Models.Dto.Journal;
using QYZH.InteractiveMagazine.Models.Entity;
using QYZH.InteractiveMagazine.Models.Enum;
using SqlSugar;
using System.Diagnostics;
using System.Net;
using System.Text;
using System.Text.Json;
namespace QYZH.InteractiveMagazine.PrintWorker.Consumers;
/// <summary>
/// 自动铺码消费者。
/// </summary>
public class AutoDotCodeConsumer(
IConfiguration configuration,
IServiceScopeFactory scopeFactory,
IHostEnvironment hostEnvironment,
ILogger<AutoDotCodeConsumer> logger,
IHttpClientFactory httpClientFactory,
OssService ossService) : IQueueConsumer
{
/// <summary>
/// 交换机名称。
/// </summary>
public string Exchange => "ex.journal";
/// <summary>
/// 队列名称。
/// </summary>
public string QueueName => "mq.journal.dotcode.auto";
/// <summary>
/// 路由键。
/// </summary>
public string RoutingKey => "rk.journal.dotcode.auto";
/// <summary>
/// 处理自动铺码消息。
/// </summary>
public async Task HandleAsync(byte[] body, CancellationToken cancellationToken = default)
{
var message = Encoding.UTF8.GetString(body);
logger.LogInformation("收到自动铺码消息: {Message}", message);
using var scope = scopeFactory.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<ISqlSugarClient>();
JournalPagePrintDto? request = null;
List<long> reservedDotDetailIds = [];
var dotPagesCommitted = false;
try
{
request = JsonSerializer.Deserialize<JournalPagePrintDto>(message, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
if (request == null)
{
return;
}
var workDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "JournalPagePdf");
var uploadDirectory = Path.Combine(workDirectory, "upload");
var downloadDirectory = Path.Combine(workDirectory, "download");
Directory.CreateDirectory(uploadDirectory);
Directory.CreateDirectory(downloadDirectory);
var uploadFileName = $"upload_{request.JournalId}_{DateTime.Now:yyyyMMddHHmmssffffff}{Random.Shared.Next(1000, 9999)}.pdf";
var uploadFilePath = Path.Combine(uploadDirectory, uploadFileName);
var journalPdfKey = request.JournalPdfUrl?.RemoveDomain();
if (string.IsNullOrWhiteSpace(journalPdfKey))
{
logger.LogError("书籍上传 PDF 文件地址为空JournalId: {JournalId}", request.JournalId);
await TryCallbackCodeFailAsync(request, cancellationToken);
return;
}
await using var pdfStream = ossService.GetObjectStream(journalPdfKey);
if (pdfStream == null)
{
logger.LogError("获取书籍上传 PDF 文件失败OSS Key: {OssKey}", journalPdfKey);
await TryCallbackCodeFailAsync(request, cancellationToken);
return;
}
await using (var fs = new FileStream(uploadFilePath, FileMode.CreateNew, FileAccess.Write))
{
await pdfStream.CopyToAsync(fs, cancellationToken);
logger.LogInformation("书籍 PDF 已下载到本地,长度:{Length}", fs.Length);
}
var dotId = configuration.GetValue("PrintConfig:DotId", 765837655859269);
var dotFile = await dbContext.Queryable<DotFile>().FirstAsync(x => x.Id == dotId, cancellationToken);
if (dotFile == null)
{
logger.LogError("配置打印数据错误点阵文件不存在DotId: {DotId}", dotId);
await TryCallbackCodeFailAsync(request, cancellationToken);
return;
}
var printToolDirectory = Path.Combine(hostEnvironment.ContentRootPath, "PrintToolV2.7");
var exePath = Path.Combine(printToolDirectory, "PrintTool.exe");
if (!File.Exists(exePath))
{
logger.LogError("PrintTool.exe 不存在,路径:{ExePath}", exePath);
await TryCallbackCodeFailAsync(request, cancellationToken);
return;
}
var xmlPath = Path.Combine(printToolDirectory, dotFile.FileName);
if (!File.Exists(xmlPath))
{
logger.LogError("铺码授权文件不存在,路径:{XmlPath}", xmlPath);
await TryCallbackCodeFailAsync(request, cancellationToken);
return;
}
var downloadFileName = $"download_{request.JournalId}_{DateTime.Now:yyyyMMddHHmmssffffff}{Random.Shared.Next(1000, 9999)}_dot.pdf";
var downloadFilePath = Path.Combine(downloadDirectory, downloadFileName);
var dPrint = configuration.GetValue("PrintConfig:DPrint", 0);
var pageNumMax = request.PageNum.Max(x => x);
var dotFileDetailList = await dbContext.Queryable<DotFileDetail>()
.Where(x => x.DotId == dotId && !x.IsUse)
.OrderBy(x => x.Id)
.Take(pageNumMax)
.ToListAsync(cancellationToken);
if (dotFileDetailList.Count < pageNumMax)
{
logger.LogError("点阵页码余量不足DotId: {DotId}, Need: {Need}, Available: {Available}", dotId, pageNumMax, dotFileDetailList.Count);
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);
if (printResult.Timeout || printResult.ExitCode != 0)
{
logger.LogError("执行 PrintTool.exe 失败,退出码:{ExitCode},错误信息:{ErrorMessage}", printResult.ExitCode, printResult.Error);
await ReleaseReservedDotPagesAsync(dbContext, reservedDotDetailIds, cancellationToken);
reservedDotDetailIds.Clear();
await TryCallbackCodeFailAsync(request, cancellationToken, dotFileDetailPageName);
return;
}
var statusModel = new JournalPagePrintDto
{
JournalId = request.JournalId,
PageNo = dotFileDetailPageName
};
if (!ValidatePrintOutput(printResult.Output, dotFileDetailPageName, dPrint))
{
await ReleaseReservedDotPagesAsync(dbContext, reservedDotDetailIds, cancellationToken);
reservedDotDetailIds.Clear();
await TryCallbackCodeFailAsync(request, cancellationToken, dotFileDetailPageName);
return;
}
var tempOssDownloadKey = "journal/download/" + downloadFileName;
const int bufferSize = 1 * 1024 * 1024;
await using var downloadFs = new FileStream(downloadFilePath, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize, FileOptions.SequentialScan | FileOptions.Asynchronous);
var downloadBookPagePdfName = ossService.PutObject(tempOssDownloadKey, downloadFs);
var ossDomain = configuration.GetSection("AliyunOSSConfigs:Domain").Get<string>() ?? string.Empty;
statusModel.DownloadJournalPagePdfName = ossDomain + downloadBookPagePdfName;
await downloadFs.DisposeAsync();
TryDeleteFile(uploadFilePath);
TryDeleteFile(downloadFilePath);
statusModel.Status = JournalStatusEnum.CodeSuccess;
var callbackResponse = await ExecuteUpdateJournalStatusAsync(statusModel, cancellationToken);
if (callbackResponse.IsSuccess)
{
await dbContext.Ado.UseTranAsync(async () =>
{
foreach (var dotFileDetail in dotFileDetailList)
{
dotFileDetail.IsUse = true;
dotFileDetail.UpdatedAt = DateTime.Now;
}
await dbContext.Updateable(dotFileDetailList).ExecuteCommandAsync();
await dbContext.Updateable<DotFile>()
.SetColumns(x => x.TotalUse == x.TotalUse + dotFileDetailList.Count)
.SetColumns(x => x.UpdatedAt == DateTime.Now)
.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, "自动铺码处理失败");
if (request != null)
{
if (!dotPagesCommitted && reservedDotDetailIds.Count > 0)
{
await ReleaseReservedDotPagesAsync(dbContext, reservedDotDetailIds, CancellationToken.None);
}
await TryCallbackCodeFailAsync(request, CancellationToken.None);
}
}
}
/// <summary>
/// 处理消费异常。
/// </summary>
public Task OnErrorAsync(byte[] message, Exception exception)
{
var body = Encoding.UTF8.GetString(message);
logger.LogError(exception, "处理自动铺码消息失败: {Message}", body);
return Task.CompletedTask;
}
private async Task<PrintToolResult> ExecutePrintToolAsync(string exePath, string workingDirectory, string arguments, CancellationToken cancellationToken)
{
var printToolTimeoutSeconds = configuration.GetValue("PrintConfig:TimeoutSeconds", 300);
var printToolTimeout = TimeSpan.FromSeconds(printToolTimeoutSeconds);
var cmd = $"\"{exePath}\" {arguments}";
logger.LogInformation("执行 PrintTool.exe 的命令: {Command},超时时间:{TimeoutSeconds} 秒", cmd, printToolTimeoutSeconds);
using var process = new Process();
process.StartInfo = new ProcessStartInfo
{
WorkingDirectory = workingDirectory,
FileName = exePath,
Arguments = arguments,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
process.Start();
var outputTask = process.StandardOutput.ReadToEndAsync(cancellationToken);
var errorTask = process.StandardError.ReadToEndAsync(cancellationToken);
var timeout = false;
try
{
await process.WaitForExitAsync(cancellationToken).WaitAsync(printToolTimeout, cancellationToken);
}
catch (TimeoutException)
{
timeout = true;
logger.LogError("执行 PrintTool.exe 超时,准备终止进程树,超时时间:{TimeoutSeconds} 秒,命令:{Command}", printToolTimeoutSeconds, cmd);
process.Kill(entireProcessTree: true);
await process.WaitForExitAsync(CancellationToken.None);
}
var output = await outputTask;
var error = await errorTask;
var exitCode = process.ExitCode;
logger.LogInformation("执行 PrintTool.exe 后,退出码:{ExitCode}stdout{Output}", exitCode, output);
if (!string.IsNullOrWhiteSpace(error))
{
logger.LogWarning("执行 PrintTool.exe 后stderr{ErrorMessage}", error);
}
return new PrintToolResult(output, error, exitCode, timeout);
}
private bool ValidatePrintOutput(string output, string[] dotFileDetailPageName, int dPrint)
{
if (string.IsNullOrWhiteSpace(output))
{
logger.LogInformation("执行铺码程序没有任何输出");
return false;
}
if (dPrint == 0)
{
var pageNoList = JsonSerializer.Deserialize<string[]>(output) ?? [];
return pageNoList.OrderBy(x => x).SequenceEqual(dotFileDetailPageName.OrderBy(x => x), StringComparer.Ordinal);
}
return dotFileDetailPageName.AsParallel().Any(ip => output.Contains(ip, StringComparison.Ordinal));
}
private async Task<CallbackUpdateJournalStatusResponse> ExecuteUpdateJournalStatusAsync(JournalPagePrintDto request, CancellationToken cancellationToken)
{
var client = httpClientFactory.CreateClient();
var callbackUrl = configuration.GetValue<string>("PrintConfig:CallBackApiUrl");
var callbackRequest = new HttpRequestMessage(HttpMethod.Post, callbackUrl)
{
Content = new StringContent(JsonSerializer.Serialize(request), Encoding.UTF8, "application/json")
};
var response = await client.SendAsync(callbackRequest, cancellationToken);
if (response.StatusCode == HttpStatusCode.OK)
{
var responseContent = await response.Content.ReadAsStringAsync(cancellationToken);
logger.LogInformation("回调接口返回内容:{ResponseContent}", responseContent);
return JsonSerializer.Deserialize<CallbackUpdateJournalStatusResponse>(responseContent, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
}) ?? new CallbackUpdateJournalStatusResponse();
}
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
{
JournalId = request.JournalId,
PageNo = pageNo ?? [],
Status = JournalStatusEnum.CodeFail
};
}
private void TryDeleteFile(string filePath)
{
try
{
if (File.Exists(filePath))
{
File.Delete(filePath);
}
}
catch (Exception ex)
{
logger.LogWarning(ex, "删除临时文件失败,路径:{FilePath}", filePath);
}
}
}
internal sealed record PrintToolResult(string Output, string Error, int ExitCode, bool Timeout);
internal class CallbackUpdateJournalStatusResponse
{
public string? Message { get; set; }
public object? Code { get; set; }
public bool Result { get; set; }
public bool IsSuccess { get; set; }
}

View File

@ -0,0 +1,32 @@
namespace QYZH.InteractiveMagazine.PrintWorker.Consumers;
/// <summary>
/// 队列消费者接口。
/// </summary>
public interface IQueueConsumer
{
/// <summary>
/// 交换机名称。
/// </summary>
string Exchange { get; }
/// <summary>
/// 队列名称。
/// </summary>
string QueueName { get; }
/// <summary>
/// 路由键。
/// </summary>
string RoutingKey { get; }
/// <summary>
/// 处理消息。
/// </summary>
Task HandleAsync(byte[] message, CancellationToken cancellationToken = default);
/// <summary>
/// 处理消费异常。
/// </summary>
Task OnErrorAsync(byte[] message, Exception exception);
}

View File

@ -0,0 +1,112 @@
using QYZH.InteractiveMagazine.Infrastructure.RabbitMQ;
namespace QYZH.InteractiveMagazine.PrintWorker.Consumers;
/// <summary>
/// RabbitMQ 消费者后台服务。
/// </summary>
public class RabbitMQHostedService(IServiceProvider serviceProvider, ILogger<RabbitMQHostedService> logger) : BackgroundService
{
/// <summary>
/// 启动所有已注册队列消费者。
/// </summary>
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using var scope = serviceProvider.CreateScope();
var consumers = scope.ServiceProvider.GetServices<IQueueConsumer>().ToList();
if (consumers.Count == 0)
{
logger.LogWarning("未注册任何队列消费者");
return;
}
var tasks = consumers.Select(c => StartConsumerAsync(c, stoppingToken)).ToList();
await Task.WhenAll(tasks);
}
private async Task StartConsumerAsync(IQueueConsumer queueConsumer, CancellationToken stoppingToken)
{
var queueName = queueConsumer.QueueName;
var exchange = queueConsumer.Exchange;
var routingKey = queueConsumer.RoutingKey;
logger.LogInformation("正在启动消费者 {QueueName}, Exchange: {Exchange}, RoutingKey: {RoutingKey}", queueName, exchange, routingKey);
try
{
var rabbitMQService = serviceProvider.GetRequiredService<IRabbitMQService>();
await rabbitMQService.ReceiveAsync(exchange, queueName, routingKey, async (channel, ea) =>
{
var dlqName = $"{queueName}.dlq";
var dlqRoutingKey = $"{routingKey}.dlq";
await channel.QueueDeclareAsync(queue: dlqName, durable: true, exclusive: false, autoDelete: false, arguments: null);
await channel.QueueBindAsync(queue: dlqName, exchange: exchange, routingKey: dlqRoutingKey, arguments: null);
using var messageScope = serviceProvider.CreateScope();
var consumer = messageScope.ServiceProvider
.GetServices<IQueueConsumer>()
.First(c => c.QueueName == queueName);
try
{
await consumer.HandleAsync(ea.Body.ToArray(), stoppingToken);
await channel.BasicAckAsync(ea.DeliveryTag, false, stoppingToken);
}
catch (Exception ex)
{
logger.LogError(ex, "消费者 {QueueName} 处理消息异常", queueName);
var dlqSent = await SendToDeadLetterQueueAsync(exchange, dlqName, dlqRoutingKey, queueName, ea.Body.ToArray(), ex, stoppingToken);
await channel.BasicNackAsync(ea.DeliveryTag, false, !dlqSent, stoppingToken);
try
{
await consumer.OnErrorAsync(ea.Body.ToArray(), ex);
}
catch (Exception errorEx)
{
logger.LogError(errorEx, "消费者 {QueueName} OnErrorAsync 执行异常", queueName);
}
}
}, stoppingToken);
}
catch (OperationCanceledException)
{
logger.LogInformation("消费者 {QueueName} 已停止", queueName);
}
catch (Exception ex)
{
logger.LogError(ex, "消费者 {QueueName} 启动失败", queueName);
}
}
private async Task<bool> SendToDeadLetterQueueAsync(string exchange, string dlqName, string dlqRoutingKey, string originalQueueName, byte[] body, Exception exception, CancellationToken cancellationToken)
{
try
{
var rabbitMQConnection = serviceProvider.GetRequiredService<IRabbitMQConnection>();
using var channel = await rabbitMQConnection.CreateChannel();
await channel.QueueDeclareAsync(queue: dlqName, durable: true, exclusive: false, autoDelete: false, arguments: null);
await channel.QueueBindAsync(queue: dlqName, exchange: exchange, routingKey: dlqRoutingKey, arguments: null);
var properties = new RabbitMQ.Client.BasicProperties
{
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);
return true;
}
catch (Exception ex)
{
logger.LogError(ex, "发送消息到死信队列 {DlqName} 失败", dlqName);
return false;
}
}
}

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