Compare commits

...

78 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
glz
fa1acec8af Merge branch 'master' of http://8.137.159.94:3000/glz/QYZH.InteractiveMagazine 2026-06-30 09:16:09 +08:00
glz
c342f543f7 fix 登录授权的问题 2026-06-30 09:15:52 +08:00
db6aaaec84 Merge branch 'master' of http://8.137.159.94:3000/glz/QYZH.InteractiveMagazine 2026-06-29 17:49:41 +08:00
5481c091f2 删除方法 2026-06-29 17:49:39 +08:00
glz
2fcff63ee2 fix RedisHelper wxTokenKeyPrefix 2026-06-29 17:49:28 +08:00
ddf2b6316d Merge branch 'master' of http://8.137.159.94:3000/glz/QYZH.InteractiveMagazine 2026-06-29 17:16:40 +08:00
a6db06c6cb 接口新增任务参数 2026-06-29 17:16:38 +08:00
glz
6b09006071 fix AliyunOSSConfigs Domain 2026-06-29 17:07:49 +08:00
glz
c0bd6b8200 fix pdf文件夹名称 2026-06-29 17:04:23 +08:00
glz
bdbef21fa8 Merge branch 'master' of http://8.137.159.94:3000/glz/QYZH.InteractiveMagazine 2026-06-29 16:34:29 +08:00
glz
bdaa6a0dc8 refactor: 统一业务异常处理,标准化结果码和错误响应
1.  新增并完善ResultCode枚举,补充标准HTTP状态码对应的业务状态码
2.  重构BusinessException,新增基于ResultCode的构造函数和ThrowIf扩展方法
3.  替换所有硬编码的HTTP状态码为统一的ResultCode枚举
4.  优化全局异常中间件,根据业务状态码映射对应HTTP状态码并规范化JSON响应
5.  修复OssImageHelper和AutoDotCodeConsumer中的OSS文件处理逻辑
6.  新增用户答题快照实体类
7.  清理废弃的宠物模块迁移脚本
2026-06-29 16:34:26 +08:00
45b5e6f71e 修改 2026-06-29 15:10:32 +08:00
4f33d81aa3 修改代码逻辑,修改配置文件 2026-06-29 15:09:23 +08:00
glz
4579fefef9 chore: update wechat config and add print tool dependencies
1. update wechat appid and appsecret in webapi config
2. add print tool related files to copy to output directory for work service
2026-06-29 13:52:47 +08:00
glz
d64b68abdd fix: 更新微信配置、点阵ID及打印工具相关配置
1. 更新WebApi和WorkService的配置文件中的微信AppId、AppSecret
2. 修改WorkService的DotId配置为新的点阵ID
3. 新增打印工具所需的授权xml文件
4. 重构打印工具调用逻辑,修复路径问题并优化命令执行方式
2026-06-29 10:39:59 +08:00
glz
f29802eb5a Merge branch 'master' of http://8.137.159.94:3000/glz/QYZH.InteractiveMagazine 2026-06-25 18:01:44 +08:00
glz
c5db5d773b feat: 新增铺码回调接口与相关配置,优化模板校验逻辑
1. 调整OSS访问域名从HTTPS改为HTTP
2. 新增宠物默认模板枚举并限制删除默认模板
3. 添加书页铺码回调接口与WebAPI端点
4. 配置Redis、HttpClient与OSS SDK服务
5. 新增打印配置项与回调地址
6. 优化PetService代码格式与宠物模板启停逻辑
7. 完善UpdatePageNoAsync的校验与处理逻辑
2026-06-25 18:01:37 +08:00
21051dd199 Merge branch 'master' of http://8.137.159.94:3000/glz/QYZH.InteractiveMagazine 2026-06-25 14:53:55 +08:00
1eb986177d 修改方法逻辑、以及业务调整 2026-06-25 14:53:45 +08:00
glz
8b773cbfe1 Merge branch 'master' of http://8.137.159.94:3000/glz/QYZH.InteractiveMagazine 2026-06-24 18:25:30 +08:00
glz
3c3453668f refactor: 重构AI聊天服务为流式返回,优化OOS路径与仓库初始化逻辑
1. 重构AI聊天接口与实现为流式返回,支持SSE协议
2. 修正BaseRepository的数据库上下文初始化逻辑
3. 更新AutoDotCodeConsumer的OOS存储路径
4. 新增阿里云OSS配置项到appsettings
5. 优化AiChatService的日志与代码注释
2026-06-24 18:25:25 +08:00
4fb861d9e6 Merge branch 'master' of http://8.137.159.94:3000/glz/QYZH.InteractiveMagazine 2026-06-24 16:33:09 +08:00
00fa7a21f6 新增微信接口以及一些问天 2026-06-24 16:32:59 +08:00
glz
bf333cd718 feat: 新增AI聊天服务,重构任务提示词相关逻辑
1.  新增AI聊天服务接口、实现、控制器及相关DTO、配置类
2.  调整JournalPageTaskTypeEnum枚举值修正
3.  合并任务的基础Prompt和自定义Prompt为单个Prompt字段
4.  精简任务输出DTO冗余属性
2026-06-24 15:06:40 +08:00
glz
28a0c3cfe4 refactor(service): 补充期刊相关实体的关联删除与prompt字段映射
1. 为JournalPageTaskService新增BasePrompt和CustomPrompt字段的赋值映射
2. 为JournalService注入JournalPageTaskAnswerRepository并完善关联删除逻辑,先删除任务答案再删除任务本身
2026-06-23 17:55:48 +08:00
glz
16ad80f2a8 refactor(work-service): 重构自动铺码消费者,新增微信登录逻辑与配置更新
1. 更新微信公众号配置AppId和AppSecret
2. 修复微信授权控制器临时返回逻辑,启用真实登录流程
3. 新增期刊打印DTO类,定义铺码数据结构
4. 重构AutoDotCodeConsumer,新增依赖注入与完整铺码业务逻辑
5. 调整队列消费者接口与实现,添加取消令牌参数
6. 新增铺码回调响应实体类,完善异常处理与事务管理
2026-06-23 16:53:18 +08:00
glz
2d3fceb265 feat(journal task): add crud operations for task answers
重构了任务答案的处理逻辑,将原有的先删后插从任务更新中拆分出来,新增独立的答案增删改查接口:
1. 移除了JournalPageTaskUpdateInput中的TaskAnswer属性
2. 新增JournalTaskAnswerAddInput/UpdateInput DTO
3. 在服务层实现答案的增删改业务逻辑,包含OSS文件处理
4. 在控制器中添加对应的API接口
2026-06-22 17:49:37 +08:00
glz
d9933e4537 refactor: 重构RabbitMQ消费者体系,新增自动铺码功能
1. 新增AutoDotCodeConsumer自动铺码消费者,实现统一的交换机路由绑定
2. 重构RabbitMQ接收方法,支持交换机、路由键配置,添加死信队列处理
3. 重构现有JournalTaskReceiveConsumer,使用标准交换机路由配置
4. 更新JournalPageService,将打印逻辑替换为自动铺码消息发送
5. 调整枚举类型,重构任务类型命名
6. 更新实体和DTO,新增Prompt相关字段
7. 添加PrintToolV2.7配套工具和文档
8. 清理冗余的PrintJournalPageAsync接口
2026-06-22 16:56:05 +08:00
glz
6ece22d23d refactor(journal task): 重构期刊任务模块,优化答案数据结构与流程
1. 移除废弃的KeywordAnalysis相关接口与代码
2. 重构任务答案存储逻辑,改用一对多关联存储多组答案
3. 调整实体与DTO字段,统一评分、时长等任务配置字段位置
4. 优化任务查询与返回结构,使用列表统一管理答案数据
5. 修复微信授权控制器默认返回逻辑
2026-06-18 13:48:31 +08:00
glz
8e17fef32f fix upload and download 2026-06-17 17:42:40 +08:00
glz
f2f7ec9100 feat: 完成多模块功能迭代与代码重构
本次提交包含多方面更新:
1. 新增消息类型枚举、杂志标题字段与AI提示词状态管理
2. 重构杂志服务,新增创建杂志接口并统一OSS路径命名规范
3. 调整题库任务结构,将答案解析迁移至独立表并优化关联查询
4. 新增AI提示词无分页查询与状态切换接口
5. 精简任务添加输入DTO,移除冗余字段
6. 修复控制器路由命名大小写不一致问题
2026-06-16 16:54:02 +08:00
glz
dbf77c7d50 refactor: 调整期刊实体字段与用户期刊输出映射
1. 移除Journal实体冗余的Title字段,统一使用Name字段
2. 更新UserDto和UsersService中的期刊标题映射逻辑
3. 新增JournalPageTask的四项能力评分字段
4. 新增AI提示词管理的完整服务、控制器与相关DTO
5. 新增用户作答记录实体JournalPageTaskUserAnswer
2026-06-16 08:37:15 +08:00
glz
94799ee189 fix: 优化微信授权服务的用户创建逻辑
移除冗余的用户ID赋值步骤,直接使用插入后返回的主键值填充日志,简化代码逻辑
2026-06-15 09:38:15 +08:00
glz
eb1a677612 fix EntityName 2026-06-12 16:26:40 +08:00
227 changed files with 15120 additions and 1519 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

@ -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,57 @@
using QYZH.InteractiveMagazine.Models.Dto;
using QYZH.InteractiveMagazine.Models.Entity;
namespace QYZH.InteractiveMagazine.IService;
/// <summary>
/// AIPrompt配置服务接口
/// </summary>
public interface IAiBasePromptService : IBaseService<AiBasePrompt>
{
/// <summary>
/// 创建Prompt配置
/// </summary>
/// <param name="input">Prompt配置输入</param>
/// <returns>创建的Prompt配置信息</returns>
Task<AiBasePromptOutput> CreateAsync(AiBasePromptInput input);
/// <summary>
/// 更新Prompt配置
/// </summary>
/// <param name="id">Prompt配置ID</param>
/// <param name="input">Prompt配置输入</param>
/// <returns>更新后的Prompt配置信息</returns>
Task<AiBasePromptOutput> UpdateAsync(long id, AiBasePromptInput input);
/// <summary>
/// 删除Prompt配置软删除
/// </summary>
/// <param name="id">Prompt配置ID</param>
Task DeleteAsync(long id);
/// <summary>
/// 根据ID获取Prompt配置
/// </summary>
/// <param name="id">Prompt配置ID</param>
/// <returns>Prompt配置信息</returns>
Task<AiBasePromptOutput> GetByIdAsync(long id);
/// <summary>
/// 分页查询Prompt配置列表
/// </summary>
/// <param name="input">查询条件</param>
/// <returns>分页结果</returns>
Task<PageListModel<AiBasePromptOutput>> GetListAsync(AiBasePromptQueryInput input);
/// <summary>
/// 获取全部Prompt配置无分页
/// </summary>
/// <returns>全部Prompt配置列表</returns>
Task<List<AiBasePromptOutput>> GetAllAsync();
/// <summary>
/// 切换Prompt启用/禁用状态
/// </summary>
/// <param name="id">Prompt配置ID</param>
Task ToggleStatusAsync(long id);
}

View File

@ -0,0 +1,17 @@
using QYZH.InteractiveMagazine.Models.Dto;
namespace QYZH.InteractiveMagazine.IService;
/// <summary>
/// AI聊天服务接口
/// </summary>
public interface IAiChatService
{
/// <summary>
/// 流式AI对话逐块返回AI生成的内容
/// </summary>
/// <param name="input">聊天输入</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>AI生成的内容片段流</returns>
IAsyncEnumerable<string> ChatAsync(AiChatInput input, CancellationToken cancellationToken = default);
}

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

@ -18,15 +18,13 @@ public interface IJournalPageService: IBaseService<JournalPage>
/// <returns></returns>
Task<bool> UpdatePageNoAsync(long JournalId);
/// <summary>
/// 打印书页(全部)
/// </summary>
/// <param name="JournalId"></param>
/// <returns></returns>
Task<bool> PrintJournalPageAsync(long JournalId);
Task<JournalPageV2Output> DetailAsync(long id);
Task<bool> DeleteAsync(long id);
//Task<List<JournalPageNoArticleOutput>> PageNoArticleAsync(long id);
/// <summary>
/// 回调接口-自动铺码, 书页铺码后回调接口,更新书页的点阵码
/// </summary>
/// <param name="bookPagePrintDto"></param>
/// <returns></returns>
Task<bool> CallbackPageUpdatePageNo(JournalPagePrintDto journalPagePrintDto);
}

View File

@ -9,9 +9,12 @@ public interface IJournalPageTaskService : IBaseService<JournalPageTask>
{
Task<long?> InsertAsync(JournalPageTaskAddInput input);
Task<bool> UpdateAsync(JournalPageTaskUpdateInput input);
Task<bool> KeywordAnalysisAsync(JournalPageTaskKeywordAnalysis input);
Task<JournalPageTaskOutput> DetailAsync(long id);
Task<bool> DeleteAsync(long id);
Task<bool> ComplementAsync(JournalPageTaskComplementInput input);
Task<long> AddAnswerAsync(JournalTaskAnswerAddInput input);
Task<bool> UpdateAnswerAsync(JournalTaskAnswerUpdateInput input);
Task<bool> DeleteAnswerAsync(long id);
}

View File

@ -1,4 +1,4 @@

using QYZH.InteractiveMagazine.Models.Dto.Journal;
using QYZH.InteractiveMagazine.Models.Dto.DotMatrix;
using QYZH.InteractiveMagazine.Models.Enum;
@ -24,6 +24,13 @@ public interface IJournalService : IBaseService<Journal>
Task<PageListModel<JournalDto>> GetPageListAsync(PageQueryModel<JournalQueryDto> search);
/// <summary>
/// 创建杂志
/// </summary>
/// <param name="input">杂志信息</param>
/// <returns>新杂志ID</returns>
Task<BaseResponse<long>> AddAsync(JournalAddDto input);
/// <summary>
/// 编辑
/// </summary>
@ -42,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

@ -0,0 +1,73 @@
using QYZH.InteractiveMagazine.Models.Dto.UserAnswerTaskService;
using QYZH.InteractiveMagazine.Models.Entity;
namespace QYZH.InteractiveMagazine.IService;
/// <summary>
/// 用户作答任务服务接口
/// </summary>
public interface IUserAnswerTaskService : IBaseService<JournalPageTaskUserAnswer>
{
/// <summary>
/// 获取用户期刊学习进度列表(按期刊分组)
/// </summary>
/// <param name="userId">用户Id</param>
/// <returns>按期刊分组的学习进度数据</returns>
Task<List<JournalProgressOutput>> GetJournalProgressAsync(long userId);
/// <summary>
/// 获取目录列表(按一级目录分组,二级目录含完成状态)
/// </summary>
/// <param name="journalId">期刊Id</param>
/// <param name="userId">用户Id</param>
/// <returns>按一级目录分组的目录列表</returns>
Task<List<CatalogListOutput>> GetCatalogListAsync(long journalId, long userId);
/// <summary>
/// 获取一级目录概览(含二级目录详情、页码范围、任务完成状态)
/// </summary>
/// <param name="catalogId">一级目录IdJournalCatalogId</param>
/// <param name="userId">用户Id</param>
/// <returns>一级目录概览数据</returns>
Task<CatalogOverviewOutput> GetCatalogOverviewAsync(long catalogId, long userId);
/// <summary>
/// 获取用户答题任务列表
/// </summary>
/// <param name="catalogId">二级目录IdCatalogId</param>
/// <param name="userId">用户Id</param>
/// <returns>答题任务列表</returns>
Task<UserAnswerTaskListOutput> GetUserAnswerTaskListAsync(long catalogId, long userId);
/// <summary>
/// 获取跨页题目详情
/// </summary>
/// <param name="groupId">任务分组IDJournalPageTaskGroupId</param>
/// <param name="userId">用户Id</param>
/// <returns>跨页题目详情</returns>
Task<CrossPageTaskOutput> GetCrossPageTaskAsync(long groupId, long userId);
/// <summary>
/// 领取任务积分
/// </summary>
/// <param name="groupId">任务分组IdJournalPageTask.GroupId</param>
/// <param name="userId">用户Id</param>
/// <returns>领取结果</returns>
Task<ClaimTaskPointsOutput> ClaimTaskPointsAsync(long groupId, long userId);
/// <summary>
/// 批量领取任务积分(一键领取)
/// </summary>
/// <param name="input">批量领取参数</param>
/// <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

@ -11,6 +11,9 @@ namespace QYZH.InteractiveMagazine.Infrastructure.Auth;
/// </summary>
public static class JwtHelper
{
public const string AdminTokenKeyPrefix = "InteractiveMagazine:AdminAuth:Token";
public const string WeChatTokenKeyPrefix = "InteractiveMagazine:WeChatAuth:Token";
/// <summary>
/// 生成JWT令牌管理端 / 单用户场景)
/// </summary>
@ -60,6 +63,21 @@ public static class JwtHelper
/// </summary>
public const string WxUserIdClaimType = "WxUserId";
public static string BuildAdminTokenKey(string userId)
{
return $"{AdminTokenKeyPrefix}:{userId}";
}
public static string BuildWeChatTokenKey(string wxUserId, string userId)
{
return $"{WeChatTokenKeyPrefix}:{wxUserId}:{userId}";
}
public static string BuildWeChatTokenKey(long wxUserId, long userId)
{
return BuildWeChatTokenKey(wxUserId.ToString(), userId.ToString());
}
private static string BuildToken(Claim[] claims, JwtSettings settings)
{
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(settings.SecretKey!));
@ -101,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;
@ -139,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

@ -7,6 +7,7 @@ using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using QYZH.InteractiveMagazine.Infrastructure.Auth;
using QYZH.InteractiveMagazine.Infrastructure.Middleware;
using QYZH.InteractiveMagazine.Models.Settings;
using System.Security.Claims;
@ -16,95 +17,137 @@ using System.Text.Encodings.Web;
namespace QYZH.InteractiveMagazine.Infrastructure.Extensions;
/// <summary>
/// 统一服务注册扩展
/// Infrastructure service registration extensions.
/// </summary>
public static class DependencyInjectionExtensions
{
private const string NoAuthScheme = "NoAuth";
private const string DevelopmentAuthScheme = "DevelopmentSmartAuth";
/// <summary>
/// 注册基础设施服务
/// Registers infrastructure services.
/// </summary>
/// <param name="services">服务集合</param>
/// <param name="configuration">配置</param>
/// <param name="environment">运行环境</param>
public static void AddInfrastructureServices(this IServiceCollection services, IConfiguration configuration, IWebHostEnvironment? environment = null)
{
AddJwtAuthentication(services, configuration, environment);
services.AddTransient<GlobalExceptionMiddleware>();
services.AddTransient<OperationLogMiddleware>();
services.AddScoped<OperationLogActionFilter>();
}
/// <summary>
/// 配置JWT认证
/// </summary>
/// <param name="services">服务集合</param>
/// <param name="configuration">配置</param>
/// <param name="environment">运行环境</param>
private static void AddJwtAuthentication(IServiceCollection services, IConfiguration configuration, IWebHostEnvironment? environment = null)
{
// 开发环境下跳过 JWT 验证
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 userId = context.Principal?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId))
{
context.Fail("无效的 Token");
return;
}
// 检查管理后台 Token
var adminToken = await RedisHelper.GetAsync($"InteractiveMagazine:AdminAuth:Token:{userId}");
if (!string.IsNullOrEmpty(adminToken))
{
return;
}
// 检查微信端 Token
var wechatToken = await RedisHelper.GetAsync($"InteractiveMagazine:WeChatAuth:Token:{userId}");
if (!string.IsNullOrEmpty(wechatToken))
{
return;
}
// Redis 中不存在任何 Token认证失败
context.Fail("Token 已失效,请重新登录");
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)
{
var authHeader = context.HttpContext.Request.Headers.Authorization.FirstOrDefault();
if (string.IsNullOrWhiteSpace(authHeader) || !authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
{
return null;
}
return authHeader.Substring("Bearer ".Length).Trim();
}
}
/// <summary>
/// 开发环境免认证处理器
/// Authentication handler used only in development.
/// </summary>
public class NoAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
@ -115,14 +158,13 @@ public class NoAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
// 开发环境下始终认证成功
var claims = new[]
{
new System.Security.Claims.Claim(System.Security.Claims.ClaimTypes.Name, "DevUser"),
new System.Security.Claims.Claim(System.Security.Claims.ClaimTypes.NameIdentifier, "0")
new Claim(ClaimTypes.Name, "DevUser"),
new Claim(ClaimTypes.NameIdentifier, "0")
};
var identity = new System.Security.Claims.ClaimsIdentity(claims, Scheme.Name);
var principal = new System.Security.Claims.ClaimsPrincipal(identity);
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
return Task.FromResult(AuthenticateResult.Success(ticket));

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

@ -13,6 +13,14 @@ public class GlobalExceptionMiddleware : IMiddleware
{
private readonly ILogger<GlobalExceptionMiddleware> _logger;
/// <summary>
/// JSON序列化选项camelCase与Controller保持一致
/// </summary>
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
/// <summary>
/// 构造函数
/// </summary>
@ -35,7 +43,7 @@ public class GlobalExceptionMiddleware : IMiddleware
}
catch (BusinessException ex)
{
_logger.LogWarning(ex, "业务异常:{Message}", ex.Message);
_logger.LogWarning(ex, "业务异常:{Message},状态码:{ResultCode}", ex.Message, ex.ResultCode);
await HandleBusinessExceptionAsync(context, ex);
}
catch (Exception ex)
@ -46,17 +54,17 @@ public class GlobalExceptionMiddleware : IMiddleware
}
/// <summary>
/// 处理业务异常
/// 处理业务异常按业务状态码映射HTTP状态码
/// </summary>
/// <param name="context">HTTP上下文</param>
/// <param name="ex">业务异常</param>
private static async Task HandleBusinessExceptionAsync(HttpContext context, BusinessException ex)
{
context.Response.ContentType = "application/json";
context.Response.StatusCode = StatusCodes.Status400BadRequest;
context.Response.StatusCode = GetHttpStatusCode(ex.ResultCode);
var response = BaseResponse<object>.Fail(ResultCode.FAIL,ex.Message);
var json = JsonSerializer.Serialize(response);
var response = BaseResponse<object>.Fail(ex.ResultCode, ex.Message);
var json = JsonSerializer.Serialize(response, JsonOptions);
await context.Response.WriteAsync(json);
}
@ -71,9 +79,32 @@ public class GlobalExceptionMiddleware : IMiddleware
context.Response.ContentType = "application/json";
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
var response = BaseResponse<object>.Fail("系统内部错误,请稍后重试");
var json = JsonSerializer.Serialize(response);
var response = BaseResponse<object>.Fail(ResultCode.GLOBAL_ERROR, "系统内部错误,请稍后重试");
var json = JsonSerializer.Serialize(response, JsonOptions);
await context.Response.WriteAsync(json);
}
/// <summary>
/// 根据业务状态码获取对应的HTTP状态码
/// </summary>
/// <param name="resultCode">业务状态码</param>
/// <returns>HTTP状态码</returns>
private static int GetHttpStatusCode(ResultCode resultCode)
{
return resultCode switch
{
ResultCode.DENY => StatusCodes.Status401Unauthorized,
ResultCode.FORBIDDEN => StatusCodes.Status403Forbidden,
ResultCode.NOT_FOUND => StatusCodes.Status404NotFound,
ResultCode.BAD_REQUEST => StatusCodes.Status400BadRequest,
ResultCode.PARAM_ERROR => StatusCodes.Status400BadRequest,
ResultCode.CONFLICT => StatusCodes.Status409Conflict,
ResultCode.UNPROCESSABLE_ENTITY => StatusCodes.Status422UnprocessableEntity,
ResultCode.TOO_MANY_REQUESTS => StatusCodes.Status429TooManyRequests,
ResultCode.BAD_GATEWAY => StatusCodes.Status502BadGateway,
ResultCode.SERVICE_UNAVAILABLE => StatusCodes.Status503ServiceUnavailable,
_ => StatusCodes.Status400BadRequest
};
}
}

View File

@ -1,101 +1,21 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using QYZH.InteractiveMagazine.Models.Settings;
using System.IdentityModel.Tokens.Jwt;
namespace QYZH.InteractiveMagazine.Infrastructure.Middleware;
/// <summary>
/// JWT 自动刷新中间件
/// 功能:
/// 每次请求都刷新 Redis 中 Token 的过期时间(保持会话活跃)
/// 前端永远使用同一个 Token无需处理 Token 刷新
/// 保留兼容的JWT自动刷新中间件,实际刷新在认证成功后执行。
/// </summary>
public class JwtAutoRefreshMiddleware
{
private readonly RequestDelegate _next;
private readonly IConfiguration _configuration;
private const string AdminTokenKeyPrefix = "InteractiveMagazine:AdminAuth:Token";
private const string WeChatTokenKeyPrefix = "InteractiveMagazine:WeChatAuth:Token";
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(context, token);
}
await _next(context);
}
private async Task TryRefreshRedisTokenExpiryAsync(HttpContext context, string token)
{
try
{
var tokenHandler = new JwtSecurityTokenHandler();
if (tokenHandler.ReadToken(token) is not JwtSecurityToken jwtToken)
{
return;
}
var expiryTime = jwtToken.ValidTo;
var remainingTime = expiryTime - DateTime.Now;
// JWT 已过期,不处理
if (remainingTime <= TimeSpan.Zero)
{
return;
}
var userId = jwtToken.Claims.FirstOrDefault(c => c.Type == System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId))
{
return;
}
var jwtSettings = _configuration.GetSection("JwtSettings").Get<JwtSettings>();
if (jwtSettings == null || jwtSettings.ExpiryMinutes <= 0)
{
return;
}
// 每次请求都刷新 Redis 中 Token 的过期时间
await RefreshRedisTokenExpiryAsync(userId, token, jwtSettings.ExpiryMinutes);
}
catch
{
// 忽略自动刷新异常,由后续认证中间件处理
}
}
/// <summary>
/// 刷新 Redis 中已存在 Token 的过期时间(保持会话活跃)
/// </summary>
private async Task RefreshRedisTokenExpiryAsync(string userId, string currentToken, int expiryMinutes)
{
// 检查 Admin Token
var adminTokenKey = $"{AdminTokenKeyPrefix}:{userId}";
var adminToken = await RedisHelper.GetAsync(adminTokenKey);
if (!string.IsNullOrEmpty(adminToken))
{
await RedisHelper.SetAsync(adminTokenKey, currentToken, TimeSpan.FromMinutes(expiryMinutes));
return;
}
// 检查 WeChat Token
var wechatTokenKey = $"{WeChatTokenKeyPrefix}:{userId}";
var wechatToken = await RedisHelper.GetAsync(wechatTokenKey);
if (!string.IsNullOrEmpty(wechatToken))
{
await RedisHelper.SetAsync(wechatTokenKey, currentToken, TimeSpan.FromMinutes(expiryMinutes));
}
}
}

View File

@ -24,7 +24,7 @@ public class ModelValidActionFilterAttribute : ActionFilterAttribute
errorDic.Add(key, errorStr);
}
}
var result = new BaseResponse<Dictionary<string, string>>() { code = ResultCode.FAIL };
var result = new BaseResponse<Dictionary<string, string>>() { code = ResultCode.BAD_REQUEST };
result.message = string.Join("|", errorDic.Select(e => e.Value).Distinct());
result.result = errorDic;

View File

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

View File

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

View File

@ -1,6 +1,7 @@
using Microsoft.Extensions.Logging;
using QYZH.InteractiveMagazine.Common.Extensions;
using QYZH.InteractiveMagazine.Models.Common;
using QYZH.InteractiveMagazine.Models.Dto;
namespace QYZH.InteractiveMagazine.Infrastructure.OSS;
@ -87,7 +88,7 @@ public class OssImageHelper
catch (Exception ex) when (ex is not BusinessException)
{
_logger.LogError(ex, "OSS 图片搬运异常Source: {Source}, Target: {Target}", sourcePath, targetPath);
throw new BusinessException("图片搬运失败,请重试", 500);
throw new BusinessException("图片搬运失败,请重试", ResultCode.GLOBAL_ERROR);
}
}

View File

@ -7,6 +7,8 @@ using CSRedis;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using QYZH.InteractiveMagazine.Common.Extensions;
using QYZH.InteractiveMagazine.Models.Common;
using QYZH.InteractiveMagazine.Models.Dto;
using System.Text.Json;
using System.Web;
@ -193,6 +195,53 @@ namespace QYZH.InteractiveMagazine.Infrastructure.OSS
}
}
/// <summary>
/// 判断对象是否存在
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
public bool DoesObjectExist(string key)
{
try
{
if (key.IsNull()) return false;
key = key.RemoveDomain();
key = HttpUtility.UrlDecode(key);
return _ossClient.DoesObjectExist(_ossOption.BucketName, key);
}
catch (OssException ex)
{
_logger.LogError(ex.Message + ex.StackTrace);
return false;
}
}
/// <summary>
/// 获取对象内容流
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
public Stream? GetObjectStream(string key)
{
try
{
if (key.IsNull()) return null;
key = key.RemoveDomain();
key = HttpUtility.UrlDecode(key);
var ossObject = _ossClient.GetObject(_ossOption.BucketName, key);
return ossObject.Content;
}
catch (OssException ex)
{
_logger.LogError(ex.Message + ex.StackTrace);
return null;
}
}
/// <summary>
/// 删除多个
/// </summary>

View File

@ -9,6 +9,6 @@ namespace QYZH.InteractiveMagazine.Infrastructure.RabbitMQ
Task<bool> SendBatchAsync(IEnumerable<RabbitMQSendParam> @params, CancellationToken cancellationToken = default);
Task ReceiveAsync(string queueName, Func<IChannel, BasicDeliverEventArgs, Task> callback, CancellationToken cancellationToken = default);
Task ReceiveAsync(string exchange, string queueName, string routingKey, Func<IChannel, BasicDeliverEventArgs, Task> callback, CancellationToken cancellationToken = default);
}
}

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;
}
@ -26,18 +32,17 @@ namespace QYZH.InteractiveMagazine.Infrastructure.RabbitMQ
{
using var channel = await _connection.CreateChannel();
// 启用发布者确认
//var channelOpts = new CreateChannelOptions
//(
// publisherConfirmationsEnabled: true,
// publisherConfirmationTrackingEnabled: true,
// outstandingPublisherConfirmationsRateLimiter: new ThrottlingRateLimiter(MAX_OUTSTANDING_CONFIRMS)
//);
// 声明 Exchange持久化
await channel.ExchangeDeclareAsync(exchange: param.Exchange, type: "direct", durable: true, autoDelete: false, arguments: null);
// 声明队列(持久化)
await channel.QueueDeclareAsync(queue: param.RoutingKey, durable: true, exclusive: false, autoDelete: false, arguments: null);
await channel.QueueDeclareAsync(queue: param.Queue, durable: true, exclusive: false, autoDelete: false, arguments: null);
// 绑定队列到 Exchange
await channel.QueueBindAsync(queue: param.Queue, exchange: param.Exchange, routingKey: param.RoutingKey, arguments: null);
// 清空队列
if (param.Purge) await channel.QueuePurgeAsync(param.RoutingKey);
if (param.Purge) await channel.QueuePurgeAsync(param.Queue);
// 消息序列化
var mesjson = JsonSerializer.Serialize(param.Data, options);
@ -53,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;
}
}
@ -113,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;
@ -128,12 +135,26 @@ namespace QYZH.InteractiveMagazine.Infrastructure.RabbitMQ
}
public async Task ReceiveAsync(string queueName, Func<IChannel, BasicDeliverEventArgs, Task> callback, CancellationToken cancellationToken = default)
public async Task ReceiveAsync(string exchange, string queueName, string routingKey, Func<IChannel, BasicDeliverEventArgs, Task> callback, CancellationToken cancellationToken = default)
{
var channel = await _connection.CreateChannel();
//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);
// 声明队列(持久化)
await channel.QueueDeclareAsync(queue: queueName, durable: true, exclusive: false, autoDelete: false, arguments: null);
// 绑定队列到 Exchange
await channel.QueueBindAsync(queue: queueName, exchange: exchange, routingKey: routingKey, arguments: null);
var consumer = new AsyncEventingBasicConsumer(channel);
consumer.ReceivedAsync += async (model, ea) =>
{

View File

@ -1,3 +1,5 @@
using QYZH.InteractiveMagazine.Models.Dto;
namespace QYZH.InteractiveMagazine.Models.Common;
/// <summary>
@ -10,6 +12,11 @@ public class BusinessException : Exception
/// </summary>
public int Code { get; set; }
/// <summary>
/// 业务状态码
/// </summary>
public ResultCode ResultCode { get; set; } = ResultCode.FAIL;
/// <summary>
/// 无参构造函数
/// </summary>
@ -33,6 +40,18 @@ public class BusinessException : Exception
public BusinessException(string message, int code) : base(message)
{
Code = code;
ResultCode = System.Enum.IsDefined(typeof(ResultCode), code) ? (ResultCode)code : ResultCode.FAIL;
}
/// <summary>
/// 构造函数(按业务状态码)
/// </summary>
/// <param name="message">异常消息</param>
/// <param name="resultCode">业务状态码</param>
public BusinessException(string message, ResultCode resultCode) : base(message)
{
ResultCode = resultCode;
Code = (int)resultCode;
}
/// <summary>
@ -62,4 +81,15 @@ public class BusinessException : Exception
throw new BusinessException(message);
}
}
/// <summary>
/// 条件抛出业务异常(带业务状态码)
/// </summary>
/// <param name="isTrue">是否触发</param>
/// <param name="message">异常消息</param>
/// <param name="resultCode">业务状态码</param>
public static void ThrowIf(bool isTrue, string message, ResultCode resultCode)
{
if (isTrue) throw new BusinessException(message, resultCode);
}
}

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,121 @@
using QYZH.InteractiveMagazine.Models.Dto;
namespace QYZH.InteractiveMagazine.Models.Dto;
/// <summary>
/// AIPrompt配置创建/更新输入
/// </summary>
public class AiBasePromptInput
{
/// <summary>
/// 配置标识DEFAULT, READING, COMPREHENSION
/// </summary>
public string PromptKey { get; set; } = string.Empty;
/// <summary>
/// 配置名称默认Prompt、阅读理解专用
/// </summary>
public string PromptName { get; set; } = string.Empty;
/// <summary>
/// Prompt模板内容
/// </summary>
public string PromptTemplate { get; set; } = string.Empty;
/// <summary>
/// 配置说明
/// </summary>
public string? Description { get; set; }
/// <summary>
/// 优先级(数值越小优先级越高)
/// </summary>
public int Priority { get; set; }
/// <summary>
/// 是否为默认模板
/// </summary>
public bool IsDefault { get; set; } = true;
}
/// <summary>
/// AIPrompt配置输出
/// </summary>
public class AiBasePromptOutput
{
/// <summary>
/// 主键ID
/// </summary>
public long Id { get; set; }
/// <summary>
/// 配置标识
/// </summary>
public string PromptKey { get; set; } = string.Empty;
/// <summary>
/// 配置名称
/// </summary>
public string PromptName { get; set; } = string.Empty;
/// <summary>
/// Prompt模板内容
/// </summary>
public string PromptTemplate { get; set; } = string.Empty;
/// <summary>
/// 配置说明
/// </summary>
public string? Description { get; set; }
/// <summary>
/// 优先级
/// </summary>
public int Priority { get; set; }
/// <summary>
/// 是否为默认模板
/// </summary>
public bool IsDefault { get; set; }
/// <summary>
/// 状态1=启用, 0=禁用)
/// </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>
/// AIPrompt配置分页查询输入
/// </summary>
public class AiBasePromptQueryInput : PageQueryModel
{
/// <summary>
/// 配置标识(精确匹配)
/// </summary>
public string? PromptKey { get; set; }
/// <summary>
/// 配置名称(模糊查询)
/// </summary>
public string? PromptName { get; set; }
}

View File

@ -0,0 +1,23 @@
namespace QYZH.InteractiveMagazine.Models.Dto;
/// <summary>
/// AI聊天输入
/// </summary>
public class AiChatInput
{
/// <summary>
/// 用户需求文本
/// </summary>
public string Message { get; set; } = string.Empty;
}
/// <summary>
/// AI聊天输出
/// </summary>
public class AiChatOutput
{
/// <summary>
/// AI返回的内容Markdown格式提示词
/// </summary>
public string Content { get; set; } = string.Empty;
}

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

@ -195,5 +195,23 @@ public enum ResultCode
FORBIDDEN = 403,
[Description("Bad Request")]
BAD_REQUEST = 400
BAD_REQUEST = 400,
[Description("资源不存在")]
NOT_FOUND = 404,
[Description("冲突/重复提交")]
CONFLICT = 409,
[Description("业务规则校验失败")]
UNPROCESSABLE_ENTITY = 422,
[Description("请求过于频繁")]
TOO_MANY_REQUESTS = 429,
[Description("第三方服务异常")]
BAD_GATEWAY = 502,
[Description("服务不可用")]
SERVICE_UNAVAILABLE = 503
}

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

@ -21,6 +21,11 @@ namespace QYZH.InteractiveMagazine.Models.Dto.Journal
/// </summary>
public string Name { get; set; }
/// <summary>
/// Desc:标题
/// </summary>
public string? Title { get; set; }
/// <summary>
/// Desc:总页数
/// </summary>
@ -31,7 +36,6 @@ namespace QYZH.InteractiveMagazine.Models.Dto.Journal
/// </summary>
public int VerifyPage { get; set; }
/// <summary>
/// Desc:状态
@ -105,41 +109,46 @@ namespace QYZH.InteractiveMagazine.Models.Dto.Journal
set => _pdfPreviewUrl = value;
}
/// <summary>
/// Desc:结论json
/// </summary>
public object? Conclusion { get; set; }
/// <summary>
/// Desc:思路导读
/// Desc:
/// </summary>
public string? Guide { get; set; }
/// <summary>
/// Desc:家长指导
/// </summary>
public string? Tutelage { get; set; }
/// <summary>
/// Desc:所属机构列表
/// </summary>
public Dictionary<long, string>? Organization { get; set; }
//public List<Dictionary<long, string>> Organizations => Organization.Select(c => new Dictionary<long, string>() { { c.Key, c.Value } }).ToList();
/// <summary>
/// Desc:副标题
/// </summary>
public string? Title { get; set; }
/// <summary>
/// 年级
/// </summary>
public int Grade { get; set; }
public string? Description { get; set; }
/// <summary>
/// Desc:下载书籍页码点阵码PDF文件名称
/// </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

@ -8,28 +8,24 @@ using System.Threading.Tasks;
namespace QYZH.InteractiveMagazine.Models.Dto.Journal
{
public partial class JournalEditDto
public partial class JournalAddDto
{
/// <summary>
/// Desc:主键id
/// </summary>
public long Id { get; set; }
/// <summary>
/// Desc:书籍名称
/// </summary>
public string Name { get; set; }
/// <summary>
/// Desc:标题
/// </summary>
public string? Title { get; set; }
/// <summary>
/// Desc:总页数
/// </summary>
public int TotalPage { get; set; }
/// <summary>
/// Desc:校验页数
/// </summary>
public int VerifyPage { get; set; }
/// <summary>
/// Desc:状态
@ -57,10 +53,6 @@ namespace QYZH.InteractiveMagazine.Models.Dto.Journal
[JsonIgnore]
public int Revision { get; set; }
/// <summary>
/// Desc:审查结果(默认0 1通过 -1驳回)
/// </summary>
public int Result { get; set; }
/// <summary>
/// Desc:封面
@ -75,42 +67,30 @@ namespace QYZH.InteractiveMagazine.Models.Dto.Journal
/// <summary>
/// Desc:pdf预览地址
/// </summary>
public string PdfPreviewUrl { get; set; }
public string? PdfPreviewUrl { get; set; }
/// <summary>
/// Desc:结论json
/// Desc:发布开始时间
/// </summary>
public object Conclusion { get; set; }
public DateTime? StartTime { get; set; }
/// <summary>
/// Desc:思路导读
/// Desc:发布结束时间
/// </summary>
public string Guide { get; set; }
public DateTime? EndTime { get; set; }
}
public partial class JournalEditDto: JournalAddDto
{
/// <summary>
/// Desc:主键id
/// </summary>
public long Id { get; set; }
/// <summary>
/// Desc:家长指导
/// Desc:状态
/// </summary>
[JsonIgnore]
public string Tutelage { get; set; }
public JournalStatusEnum Status { get; set; }
/// <summary>
/// Desc:所属机构
/// </summary>
public long[] OrganizationIds { get; set; }
///// <summary>
///// Desc:所属机构
///// </summary>
//public string OrganizationName { get; set; }
/// <summary>
/// Desc:副标题
/// </summary>
public string Title { get; set; }
/// <summary>
/// 年级
/// </summary>
public int Grade { get; set; }
}
}

View File

@ -0,0 +1,42 @@
using QYZH.InteractiveMagazine.Models.Enum;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace QYZH.InteractiveMagazine.Models.Dto.Journal
{
public class JournalPagePrintDto
{
/// <summary>
/// 书Id
/// </summary>
public long JournalId { get; set; }
/// <summary>
/// 状态
/// </summary>
public JournalStatusEnum? Status { get; set; }
/// <summary>
/// 书PDF URL这里带有 OSS 域名 https://oss.qyzhjy.com/
/// </summary>
public string? JournalPdfUrl { get; set; }
/// <summary>
/// 页码数组
/// </summary>
public int[] PageNum { get; set; } = [];
/// <summary>
/// 点阵码打印页面数组
/// </summary>
public string[] PageNo { get; set; } = [];
/// <summary>
/// 下载书页PDF的文件名铺码程序已经添加好这里带有 OSS 域名 https://oss.qyzhjy.com/
/// </summary>
public string? DownloadJournalPagePdfName { get; set; }
}
}

View File

@ -30,33 +30,9 @@ namespace QYZH.InteractiveMagazine.Models.Dto.Journal
public TaskBankTypeEnum Type { get; set; }
/// <summary>
/// 学科Id
/// 是否需要AI批改默认需要
/// </summary>
public long SubjectId { get; set; }
public bool NeedAiProcess { get; set; } = true;
/// <summary>
/// 分数
/// </summary>
public float Score { get; set; }
/// <summary>
/// 回答时间(秒)
/// </summary>
public int AnswerTime { get; set; }
/// <summary>
/// 关联的知识点ids
/// </summary>
public string KnowledgePointIds { get; set; }
/// <summary>
/// 关键解析
/// </summary>
public string KeywordAnalysis { get; set; }
/// <summary>
/// 问题内容
/// </summary>
public string Task { get; set; }
}
}

View File

@ -1,4 +1,4 @@
using QYZH.InteractiveMagazine.Models.Enum;
using QYZH.InteractiveMagazine.Models.Enum;
using QYZH.InteractiveMagazine.Common.Extensions;
using QYZH.InteractiveMagazine.Common.Helpers;
using QYZH.InteractiveMagazine.Models.Enum;
@ -18,12 +18,10 @@ namespace QYZH.InteractiveMagazine.Models.Dto.Journal
/// 书页Id
/// </summary>
public long JournalPageId { get; set; }
/// <summary>
/// 学科Id
/// 任务信息
/// </summary>
public long SubjectId { get; set; }
public string Task { get; set; }
/// <summary>
/// 题号
/// </summary>
@ -36,24 +34,66 @@ namespace QYZH.InteractiveMagazine.Models.Dto.Journal
public string ShowType => Type.GetDescription();
private string _task;
/// <summary>
/// 问题
/// 答案列表
/// </summary>
public string Task
{
get { return _task.AddDomain(DomainHelper.OssDomain); }
set { _task = value; }
}
public List<JournalTaskAnswerOutput> Answers { get; set; }
/// <summary>
/// 积分
/// </summary>
public float Points { get; set; }
/// <summary>
/// 选项
/// 成长值
/// </summary>
public string Options { get; set; }
public float GrowthPoint { get; set; }
/// <summary>
/// 理解力评分
/// </summary>
public float Comprehension { get; set; }
/// <summary>
/// 判断力评分
/// </summary>
public float Judgment { get; set; }
/// <summary>
/// 表达力评分
/// </summary>
public float Expression { get; set; }
/// <summary>
/// 说服力评分
/// </summary>
public float Persuasiveness { get; set; }
/// <summary>
/// 作答参考时间(秒)
/// </summary>
public int AnswerTime { get; set; }
/// <summary>
/// Prompt配置
/// </summary>
public string Prompt { get; set; }
/// <summary>
/// 是否需要AI批改
/// </summary>
public bool NeedAiProcess { get; set; }
}
/// <summary>
/// 答案输出
/// </summary>
public class JournalTaskAnswerOutput
{
public long Id { get; set; }
private string _answer;
/// <summary>
/// 答案
/// 参考答案
/// </summary>
public string Answer
{
@ -61,98 +101,14 @@ namespace QYZH.InteractiveMagazine.Models.Dto.Journal
set { _answer = value; }
}
private string _analysis;
private string _answerUrl;
/// <summary>
/// 解析
/// 参考答案图片
/// </summary>
public string Analysis
public string AnswerUrl
{
get { return _analysis.AddDomain(DomainHelper.OssDomain); }
set { _analysis = value; }
get { return _answerUrl.AddDomain(DomainHelper.OssDomain); }
set { _answerUrl = value; }
}
public string AnalysisUrl { get; set; }
/// <summary>
/// 问题分数
/// </summary>
public float TaskScore { get; set; }
private string _audioUrl;
/// <summary>
/// 音频地址
/// </summary>
public string AudioUrl
{
get { return DomainHelper.OssFullUrl(_audioUrl); }
set { _audioUrl = value; }
}
private string _videoUrl;
/// <summary>
/// 视频地址
/// </summary>
public string VideoUrl
{
get { return DomainHelper.OssFullUrl(_videoUrl); }
set { _videoUrl = value; }
}
private string _pointsUrl;
/// <summary>
/// 点位数据地址
/// </summary>
public string PointsUrl
{
get { return DomainHelper.OssFullUrl(_pointsUrl); }
set { _pointsUrl = value; }
}
private string _imageUrl;
/// <summary>
/// 图片地址
/// </summary>
public string ImageUrl
{
get { return DomainHelper.OssFullUrl(_imageUrl); }
set { _imageUrl = value; }
}
public List<string> ImageUrls { get; set; }
/// <summary>
/// 问题分数
/// </summary>
public float Score { get; set; }
/// <summary>
/// 回答时间(秒)
/// </summary>
public int AnswerTime { get; set; }
/// <summary>
/// 音频开始时间
/// </summary>
public long AudioStartTime { get; set; }
/// <summary>
/// 音频结束时间
/// </summary>
public long AudioEndTime { get; set; }
/// <summary>
/// 知识点
/// </summary>
public string KnowledgePointIds { get; set; }
/// <summary>
/// 回答项解析
/// </summary>
public string KeywordAnalysis { get; set; }
/// <summary>
/// 年级
/// </summary>
public int Grade { get; set; }
}
}

View File

@ -1,4 +1,4 @@
using QYZH.InteractiveMagazine.Models.Enum;
using QYZH.InteractiveMagazine.Models.Enum;
using QYZH.InteractiveMagazine.Models.Enum;
using System;
using System.Collections.Generic;
@ -13,11 +13,10 @@ namespace QYZH.InteractiveMagazine.Models.Dto.Journal
public long Id { get; set; }
/// <summary>
/// Id
/// 杂志Id
/// </summary>
public long JournalId { get; set; }
/// <summary>
/// 书页id
/// </summary>
@ -39,59 +38,44 @@ namespace QYZH.InteractiveMagazine.Models.Dto.Journal
public string Task { get; set; }
/// <summary>
/// 选项
/// 积分
/// </summary>
public string Options { get; set; }
public int Points { get; set; }
/// <summary>
/// 答案
/// 成长值
/// </summary>
public string Answer { get; set; }
public int GrowthPoint { get; set; }
/// <summary>
/// 答案图片
/// 理解力评分
/// </summary>
public string AnswerUrl { get; set; }
public float Comprehension { get; set; }
/// <summary>
/// 解析
/// 判断力评分
/// </summary>
public string Analysis { get; set; }
public float Judgment { get; set; }
/// <summary>
/// 解析图片地址
/// 表达力评分
/// </summary>
public string AnalysisUrl { get; set; }
public float Expression { get; set; }
/// <summary>
/// 问题分数
/// 说服力评分
/// </summary>
public float Score { get; set; }
public float Persuasiveness { get; set; }
/// <summary>
/// 回答时间秒
/// </summary>
public int AnswerTime { get; set; }
/// <summary>
/// Prompt配置
/// </summary>
public string Prompt { get; set; }
/// <summary>
/// 图片
/// 是否需要AI批改false时由人工批改
/// </summary>
public string ImageUrl { get; set; }
public bool NeedAiProcess { get; set; } = true;
/// <summary>
/// 视频
/// </summary>
public string VideoUrl { get; set; }
/// <summary>
/// 学科Id
/// </summary>
public long SubjectId { get; set; }
/// <summary>
/// 关联的知识点ids
/// </summary>
public string KnowledgepointIds { get; set; }
}
public class JournalPageTaskComplementInput

View File

@ -61,19 +61,9 @@ namespace QYZH.InteractiveMagazine.Models.Dto.Journal
public string ShowType => Type.GetDescription();
/// <summary>
/// 选项
/// 答案列表
/// </summary>
public string Options { get; set; }
/// <summary>
/// 答案
/// </summary>
public string Answers { get; set; }
/// <summary>
/// 解析
/// </summary>
public string Analysis { get; set; }
public List<JournalTaskAnswerOutput> Answers { get; set; }
/// <summary>
/// 是否分配

View File

@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace QYZH.InteractiveMagazine.Models.Dto.Journal
{
public class JournalTaskAnswerAddInput
{
/// <summary>
/// 题目Id
/// </summary>
public long JournalPageTaskId { get; set; }
/// <summary>
/// 参考答案
/// </summary>
public string Answer { get; set; }
/// <summary>
/// 参考答案图片
/// </summary>
public string AnswerUrl { get; set; }
}
}

View File

@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace QYZH.InteractiveMagazine.Models.Dto.Journal
{
public class JournalTaskAnswerInput
{
/// <summary>
/// 参考答案
/// </summary>
public string Answer { get; set; }
/// <summary>
/// 参考答案图片
/// </summary>
public string AnswerUrl { get; set; }
}
}

View File

@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace QYZH.InteractiveMagazine.Models.Dto.Journal
{
public class JournalTaskAnswerUpdateInput
{
/// <summary>
/// 答案Id
/// </summary>
public long Id { get; set; }
/// <summary>
/// 参考答案
/// </summary>
public string Answer { get; set; }
/// <summary>
/// 参考答案图片
/// </summary>
public string AnswerUrl { get; set; }
}
}

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

@ -0,0 +1,85 @@
namespace QYZH.InteractiveMagazine.Models.Dto.UserAnswerTaskService;
/// <summary>
/// 批量领取任务积分输入
/// </summary>
public class BatchClaimPointsInput
{
/// <summary>
/// 任务分组Id列表JournalPageTaskGroupId
/// </summary>
public HashSet<long> GroupIds { get; set; } = [];
}
/// <summary>
/// 批量领取任务积分输出
/// </summary>
public class BatchClaimPointsOutput
{
/// <summary>
/// 成功领取数量
/// </summary>
public int SuccessCount { get; set; }
/// <summary>
/// 失败数量
/// </summary>
public int FailCount { get; set; }
/// <summary>
/// 成功领取详情列表
/// </summary>
public List<BatchClaimSuccessItem> SuccessItems { get; set; } = [];
/// <summary>
/// 失败详情
/// </summary>
public List<BatchClaimFailItem> FailItems { get; set; } = [];
}
/// <summary>
/// 批量领取成功项
/// </summary>
public class BatchClaimSuccessItem
{
/// <summary>
/// 任务编号(从 JournalPageTask.No 字段按 - 分割取索引2前面加"任务"
/// </summary>
public string TaskNo { get; set; }
/// <summary>
/// 积分值
/// </summary>
public int Points { get; set; }
/// <summary>
/// 成长值
/// </summary>
public int GrowthPoints { get; set; }
/// <summary>
/// 题目得分
/// </summary>
public float Score { get; set; }
}
/// <summary>
/// 批量领取失败项
/// </summary>
public class BatchClaimFailItem
{
/// <summary>
/// 任务分组Id
/// </summary>
public long GroupId { get; set; }
/// <summary>
/// 任务编号(从 JournalPageTask.No 字段按 - 分割取索引2前面加"任务"
/// </summary>
public string TaskNo { get; set; }
/// <summary>
/// 失败原因
/// </summary>
public string Message { get; set; }
}

View File

@ -0,0 +1,32 @@
namespace QYZH.InteractiveMagazine.Models.Dto.UserAnswerTaskService;
/// <summary>
/// 目录列表输出(按一级目录分组)
/// </summary>
public class CatalogListOutput
{
/// <summary>
/// 一级目录ID
/// </summary>
public long CatalogId { get; set; }
/// <summary>
/// 一级目录名称
/// </summary>
public string CatalogName { get; set; }
/// <summary>
/// 杂志名称
/// </summary>
public string JournalName { get; set; }
/// <summary>
/// 文章篇数
/// </summary>
public int ArticleCount { get; set; }
/// <summary>
/// 二级栏目列表
/// </summary>
public List<SecondCatalogOutput> Children { get; set; } = [];
}

View File

@ -0,0 +1,32 @@
namespace QYZH.InteractiveMagazine.Models.Dto.UserAnswerTaskService;
/// <summary>
/// 一级目录概览输出(含二级目录详情列表)
/// </summary>
public class CatalogOverviewOutput
{
/// <summary>
/// 一级目录ID
/// </summary>
public long CatalogId { get; set; }
/// <summary>
/// 一级目录名称
/// </summary>
public string FirstLevelCatalogName { get; set; }
/// <summary>
/// 文章篇数(该一级目录下 JournalPage 的个数)
/// </summary>
public int PageCount { get; set; }
/// <summary>
/// 总任务数(该一级目录下所有 JournalPageTask 的条数)
/// </summary>
public int TotalTaskCount { get; set; }
/// <summary>
/// 二级目录详情列表
/// </summary>
public List<SubCatalogDetailOutput> SubCatalogs { get; set; } = new();
}

View File

@ -0,0 +1,37 @@
namespace QYZH.InteractiveMagazine.Models.Dto.UserAnswerTaskService;
/// <summary>
/// 目录任务详情输出(含任务列表和徽章数)
/// </summary>
public class CatalogTaskDetailOutput
{
/// <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>
/// 徽章个数
/// </summary>
public int MedalCount { get; set; }
/// <summary>
/// 任务列表
/// </summary>
public List<TaskStatusOutput> Tasks { get; set; } = new();
}

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

@ -0,0 +1,27 @@
namespace QYZH.InteractiveMagazine.Models.Dto.UserAnswerTaskService;
/// <summary>
/// 领取任务积分输出
/// </summary>
public class ClaimTaskPointsOutput
{
/// <summary>
/// 总成长值(主任务的 GrowthPoint
/// </summary>
public int TotalGrowthPoints { get; set; }
/// <summary>
/// 总积分(主任务的 Points
/// </summary>
public int TotalPoints { get; set; }
/// <summary>
/// 得分(从 JournalPageTaskUserAnswer 表获取JournalPageTaskId == GroupId 的 Score
/// </summary>
public float Score { get; set; }
/// <summary>
/// 任务编号(从 JournalPageTask.No 字段按 - 分割取索引2前面加"任务"
/// </summary>
public string TaskNo { get; set; }
}

View File

@ -0,0 +1,42 @@
namespace QYZH.InteractiveMagazine.Models.Dto.UserAnswerTaskService;
/// <summary>
/// 跨页题目详情输出
/// </summary>
public class CrossPageTaskOutput
{
/// <summary>
/// 一级栏目名称
/// </summary>
public string FirstLevelCatalogName { get; set; }
/// <summary>
/// 栏目名称(二级栏目/文章名称)
/// </summary>
public string CatalogName { get; set; }
/// <summary>
/// 任务名称
/// </summary>
public string TaskName { get; set; }
/// <summary>
/// 图片数组已答题用PageAnswerUrl未答题用TaskUrl
/// </summary>
public List<string> Images { get; set; } = [];
/// <summary>
/// 页面作答点读图片地址集合(已答题返回,未答题返回空集合)
/// </summary>
public List<string> PageAnswerDotUrl { get; set; } = [];
/// <summary>
/// 是否跨页题目
/// </summary>
public bool IsCrossPage { get; set; }
/// <summary>
/// 是否可以领取积分(任务已完成且未领取过积分)
/// </summary>
public bool CanClaimPoints { get; set; }
}

View File

@ -0,0 +1,57 @@
namespace QYZH.InteractiveMagazine.Models.Dto.UserAnswerTaskService;
/// <summary>
/// 用户期刊学习进度输出(按期刊分组)
/// </summary>
public class JournalProgressOutput
{
/// <summary>
/// 期刊ID
/// </summary>
public long JournalId { get; set; }
/// <summary>
/// 期刊名称
/// </summary>
public string JournalName { get; set; }
/// <summary>
/// 栏目数(一级目录个数)
/// </summary>
public int CatalogCount { get; set; }
/// <summary>
/// 任务总数(该期刊下 JournalPageTask 的条数)
/// </summary>
public int TotalTaskCount { get; set; }
/// <summary>
/// 已完成任务数(按 JournalPageTaskGroupId 去重同一题跨页算1个
/// </summary>
public int CompletedTaskCount { get; set; }
/// <summary>
/// 成长值(已完成任务的 GrowthPoint 累加)
/// </summary>
public int TotalGrowthPoints { get; set; }
/// <summary>
/// 积分值(已完成任务的 Points 累加)
/// </summary>
public int TotalPoints { get; set; }
/// <summary>
/// 进度百分比已完成任务ID数 / 总任务数 × 100
/// </summary>
public double Progress { get; set; }
/// <summary>
/// 剩余篇数(该期刊总页数 - 用户已作答页数)
/// </summary>
public int RemainingPages { get; set; }
/// <summary>
/// 连续打卡天数
/// </summary>
public int ConsecutiveDays { get; set; }
}

View File

@ -0,0 +1,32 @@
namespace QYZH.InteractiveMagazine.Models.Dto.UserAnswerTaskService;
/// <summary>
/// 二级栏目输出
/// </summary>
public class SecondCatalogOutput
{
/// <summary>
/// 二级栏目ID
/// </summary>
public long CatalogId { get; set; }
/// <summary>
/// 二级栏目名称
/// </summary>
public string CatalogName { get; set; }
/// <summary>
/// 总积分值
/// </summary>
public int TotalPoints { get; set; }
/// <summary>
/// 总成长值
/// </summary>
public int TotalGrowthPoints { get; set; }
/// <summary>
/// 状态:未开始 / 进行中 / 已完成
/// </summary>
public string Status { get; set; }
}

View File

@ -0,0 +1,37 @@
namespace QYZH.InteractiveMagazine.Models.Dto.UserAnswerTaskService;
/// <summary>
/// 二级目录详情输出(含页码范围、任务数、完成状态)
/// </summary>
public class SubCatalogDetailOutput
{
/// <summary>
/// 二级目录ID
/// </summary>
public long CatalogId { get; set; }
/// <summary>
/// 二级目录名字
/// </summary>
public string SecondLevelCatalogName { get; set; }
/// <summary>
/// 起始页码(杂志第几页开始)
/// </summary>
public int StartPage { get; set; }
/// <summary>
/// 结束页码(杂志第几页结束)
/// </summary>
public int EndPage { get; set; }
/// <summary>
/// 任务数量
/// </summary>
public int TaskCount { get; set; }
/// <summary>
/// 状态描述格式如1/2 进行中、2/2 已完成、0/2 未开始)
/// </summary>
public string Status { get; set; }
}

View File

@ -0,0 +1,27 @@
namespace QYZH.InteractiveMagazine.Models.Dto.UserAnswerTaskService;
/// <summary>
/// 任务状态输出
/// </summary>
public class TaskStatusOutput
{
/// <summary>
/// 任务ID
/// </summary>
public long TaskId { get; set; }
/// <summary>
/// 任务名字
/// </summary>
public string TaskName { get; set; }
/// <summary>
/// 任务状态:未开始 / 进行中 / 已完成
/// </summary>
public string Status { get; set; }
/// <summary>
/// 答题时间(分钟,由 AnswerSeconds 累加后转换)
/// </summary>
public double AnswerTime { get; set; }
}

View File

@ -0,0 +1,68 @@
namespace QYZH.InteractiveMagazine.Models.Dto.UserAnswerTaskService;
/// <summary>
/// 用户答题任务列表输出
/// </summary>
public class UserAnswerTaskListOutput
{
/// <summary>
/// 一级栏目名称
/// </summary>
public string FirstLevelCatalogName { get; set; }
/// <summary>
/// 目录名称(二级栏目)
/// </summary>
public string CatalogName { get; set; }
/// <summary>
/// 开始页码
/// </summary>
public int StartPage { get; set; }
/// <summary>
/// 结束页码
/// </summary>
public int EndPage { get; set; }
/// <summary>
/// 任务列表
/// </summary>
public List<UserAnswerTaskItemOutput> Tasks { get; set; } = new();
}
/// <summary>
/// 用户答题任务项输出
/// </summary>
public class UserAnswerTaskItemOutput
{
/// <summary>
/// 任务GroupId
/// </summary>
public long GroupId { get; set; }
/// <summary>
/// 任务名称
/// </summary>
public string TaskName { get; set; }
/// <summary>
/// 总成长值
/// </summary>
public int TotalGrowthPoints { get; set; }
/// <summary>
/// 总积分
/// </summary>
public int TotalPoints { get; set; }
/// <summary>
/// 答题时间(分钟)
/// </summary>
public double AnswerMinutes { get; set; }
/// <summary>
/// 任务状态(进行中、已完成、未开始)
/// </summary>
public string Status { 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>
@ -86,7 +96,7 @@ public class UserJournalItemOutput
/// <summary>
/// 期刊标题
/// </summary>
public string JournalTitle { get; set; } = string.Empty;
public string Name { 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("MessageLike")]
[SugarTable("Community_MessageLike")]
public partial class CommunityMessageLike : SqlSugarBaseEntity
{
public CommunityMessageLike()

View File

@ -6,7 +6,7 @@ namespace QYZH.InteractiveMagazine.Models.Entity
///<summary>
///固定模板言论表
///</summary>
[SugarTable("TemplateSentence")]
[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(){
@ -20,6 +20,13 @@ namespace QYZH.InteractiveMagazine.Models.Entity
/// </summary>
public string Name { get; set; }
/// <summary>
/// Desc:标题
/// Default:
/// Nullable:True
/// </summary>
public string Title { get; set; }
/// <summary>
/// Desc:总页数
/// Default:
@ -92,8 +99,6 @@ namespace QYZH.InteractiveMagazine.Models.Entity
public string PdfPreviewUrl { get; set; }
public string Title { get; set; }
/// <summary>
/// Desc:下载书籍页码点阵码PDF文件名称
/// Default:
@ -101,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,156 +1,130 @@
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:
/// Nullable:False
/// </summary>
public long JournalId { get; set; }
/// <summary>
/// Desc:书页id
/// Default:
/// Nullable:False
/// </summary>
public long JournalPageId { get; set; }
/// <summary>
/// Desc:分组
/// Default:
/// Nullable:False
/// </summary>
public long GroupId { get; set; }
/// <summary>
/// Desc:题号
/// Default:
/// Nullable:True
/// </summary>
public string No { get; set; }
/// <summary>
/// Desc:题型
/// Default:
/// Nullable:False
/// </summary>
public TaskBankTypeEnum Type { get; set; }
/// <summary>
/// Desc:任务
/// Default:
/// Nullable:True
/// </summary>
public string Task { get; set; }
}
/// <summary>
/// Desc:成长值设置
/// Default:0
/// Nullable:True
/// </summary>
public int GrowthPoint { get; set; }
/// <summary>
/// Desc:书id
/// Default:
/// Nullable:False
/// </summary>
public long JournalId {get;set;}
/// <summary>
/// Desc:积分设置
/// Default:0
/// Nullable:True
/// </summary>
public int Points { get; set; }
/// <summary>
/// Desc:理解力评分
/// Default:0
/// Nullable:False
/// </summary>
public float Comprehension { get; set; }
/// <summary>
/// Desc:书页id
/// Default:
/// Nullable:False
/// </summary>
public long JournalPageId {get;set;}
/// <summary>
/// Desc:判断力评分
/// Default:0
/// Nullable:False
/// </summary>
public float Judgment { get; set; }
/// <summary>
/// Desc:题号
/// Default:
/// Nullable:True
/// </summary>
public string No {get;set;}
/// <summary>
/// Desc:表达力评分
/// Default:0
/// Nullable:False
/// </summary>
public float Expression { get; set; }
/// <summary>
/// Desc:题型
/// Default:
/// Nullable:False
/// </summary>
public TaskBankTypeEnum Type {get;set;}
/// <summary>
/// Desc:说服力评分
/// Default:0
/// Nullable:False
/// </summary>
public float Persuasiveness { get; set; }
/// <summary>
/// Desc:乐观锁
/// Default:
/// Nullable:False
/// </summary>
public int Revision { get; set; }
/// <summary>
/// Desc:任务
/// Default:
/// Nullable:True
/// </summary>
public string Task {get;set;}
/// <summary>
/// Desc:答案
/// Default:
/// Nullable:True
/// </summary>
public string Answer {get;set;}
/// <summary>
/// Desc:解析
/// Default:
/// Nullable:True
/// </summary>
public string Analysis {get;set;}
/// <summary>
/// Desc:任务图片地址
/// Default:
/// Nullable:True
/// </summary>
public string TaskUrl { get; set; }
/// <summary>
/// Desc:分组
/// Default:
/// Nullable:False
/// </summary>
public long GroupId {get;set;}
/// <summary>
/// Desc:是否需要AI处理
/// Default:true
/// Nullable:False
/// </summary>
public bool NeedAiProcess { get; set; }
/// <summary>
/// Desc:乐观锁
/// Default:
/// Nullable:False
/// </summary>
public int Revision {get;set;}
/// <summary>
/// Desc:Prompt配置
/// Default:
/// Nullable:True
/// </summary>
public string Prompt { get; set; }
/// <summary>
/// Desc:任务图片地址
/// Default:
/// Nullable:True
/// </summary>
public string TaskUrl {get;set;}
/// <summary>
/// Desc:任务分数
/// Default:0
/// Nullable:False
/// </summary>
public float Score {get;set;}
/// <summary>
/// Desc:选项
/// Default:
/// Nullable:True
/// </summary>
public string Options {get;set;}
/// <summary>
/// Desc:回答时间(秒)
/// Default:0
/// Nullable:False
/// </summary>
public int AnswerTime {get;set;}
/// <summary>
/// Desc:关键解析json
/// Default:
/// Nullable:True
/// </summary>
public string KeywordAnalysis {get;set;}
/// <summary>
/// Desc:成长值设置
/// Default:0
/// Nullable:True
/// </summary>
public int? GrowthPoint {get;set;}
/// <summary>
/// Desc:积分设置
/// Default:0
/// Nullable:True
/// </summary>
public int? Points {get;set;}
/// <summary>
/// Desc:是否需要AI处理
/// Default:true
/// Nullable:False
/// </summary>
public bool NeedAiProcess {get;set;}
/// <summary>
/// Desc:基础Prompt配置ID为空则使用IsDefault=1的模板
/// Default:
/// Nullable:True
/// </summary>
public long? BasePromptId {get;set;}
/// <summary>
/// Desc:个性化Prompt追加到基础Prompt后面可为空
/// Default:
/// Nullable:True
/// </summary>
public string CustomPrompt {get;set;}
/// <summary>
/// Desc:作答参考时间
/// Default:
/// Nullable:True
/// </summary>
public int AnswerTime { get; set; }
}
}

View File

@ -0,0 +1,41 @@
using SqlSugar;
using System;
using System.Linq;
using System.Text;
namespace QYZH.InteractiveMagazine.Models.Entity
{
///<summary>
///杂志任务答案表;
///</summary>
[SugarTable("Journal_PageTaskAnswer")]
public partial class JournalPageTaskAnswer : SqlSugarBaseEntity
{
public JournalPageTaskAnswer(){
}
/// <summary>
/// Desc:任务id
/// Default:
/// Nullable:False
/// </summary>
public long JournalPageTaskId {get;set;}
/// <summary>
/// Desc:答案
/// Default:
/// Nullable:True
/// </summary>
public string Answer {get;set;}
/// <summary>
/// Desc:解析
/// Default:
/// Nullable:True
/// </summary>
public string AnswerUrL { get;set;}
}
}

View File

@ -0,0 +1,236 @@
using QYZH.InteractiveMagazine.Models.Enum;
using SqlSugar;
namespace QYZH.InteractiveMagazine.Models.Entity
{
/// <summary>
/// 杂志任务用户作答记录
/// </summary>
[SugarTable("Journal_PageTaskUserAnswer")]
public class JournalPageTaskUserAnswer : SqlSugarBaseEntity
{
/// <summary>
/// Desc:书id
/// Default:
/// Nullable:False
/// </summary>
public long JournalId { get; set; }
/// <summary>
/// Desc:书页id
/// Default:
/// Nullable:False
/// </summary>
public long JournalPageId { get; set; }
/// <summary>
/// Desc:任务id
/// Default:
/// Nullable:False
/// </summary>
public long JournalPageTaskId { get; set; }
/// <summary>
/// Desc:任务分组id
/// Default:
/// Nullable:False
/// </summary>
public long JournalPageTaskGroupId { get; set; }
/// <summary>
/// Desc:用户id
/// Default:
/// Nullable:False
/// </summary>
public long UserId { get; set; }
/// <summary>
/// Desc:作答结果
/// Default:
/// Nullable:True
/// </summary>
public string Result { get; set; }
/// <summary>
/// Desc:积分
/// Default:0
/// Nullable:False
/// </summary>
public int Points { get; set; }
/// <summary>
/// Desc:成长值
/// Default:0
/// Nullable:False
/// </summary>
public int GrowthPoints { get; set; }
/// <summary>
/// Desc:题目得分
/// Default:0
/// Nullable:False
/// </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>
public string QuestionAnswerUrl { get; set; }
/// <summary>
/// Desc:答案图片地址
/// Default:
/// Nullable:True
/// </summary>
public string AnswerUrl { get; set; }
/// <summary>
/// Desc:页面作答图片地址
/// Default:
/// Nullable:True
/// </summary>
public string PageAnswerUrl { get; set; }
/// <summary>
/// Desc:乐观锁
/// Default:
/// Nullable:False
/// </summary>
public int Revision { get; set; }
/// <summary>
/// Desc:作答开始时间
/// Default:
/// Nullable:True
/// </summary>
public DateTime AnswerStartTime { get; set; }
/// <summary>
/// Desc:作答结束时间
/// Default:
/// Nullable:True
/// </summary>
public DateTime AnswerEndTime { get; set; }
/// <summary>
/// Desc:作答耗时(秒)
/// Default:0
/// Nullable:False
/// </summary>
public int AnswerSeconds { get; set; }
/// <summary>
/// Desc:图像识别结果
/// Default:
/// Nullable:False
/// </summary>
public int ImageRecognition { get; set; }
/// <summary>
/// Desc:书页序号
/// Default:
/// Nullable:False
/// </summary>
public int JournalPageNum { get; set; }
/// <summary>
/// Desc:修改次数
/// Default:
/// Nullable:False
/// </summary>
public int Modify { get; set; }
/// <summary>
/// Desc:最后标签id
/// Default:
/// Nullable:False
/// </summary>
public long LastTag { get; set; }
/// <summary>
/// Desc:点读书页序号
/// Default:
/// Nullable:False
/// </summary>
public int DotPageNum { get; set; }
/// <summary>
/// Desc:页面结果图片地址
/// Default:
/// Nullable:True
/// </summary>
public string PageResultUrl { get; set; }
/// <summary>
/// Desc:题型
/// Default:
/// Nullable:True
/// </summary>
public string Type { get; set; }
/// <summary>
/// Desc:点读书页编号
/// Default:
/// Nullable:True
/// </summary>
public string DotPageNo { get; set; }
/// <summary>
/// Desc:页面作答点读图片地址
/// Default:
/// Nullable:True
/// </summary>
public string PageAnswerDotUrl { get; set; }
/// <summary>
/// Desc:中断次数
/// Default:0
/// Nullable:False
/// </summary>
public int BreakCount { get; set; }
/// <summary>
/// Desc:中断时间记录
/// Default:
/// Nullable:True
/// </summary>
public string BreakTimes { get; set; }
/// <summary>
/// Desc:作业状态
/// Default:
/// Nullable:True
/// </summary>
public int AssignmentStatus { get; set; }
}
}

View File

@ -0,0 +1,176 @@
using SqlSugar;
namespace QYZH.InteractiveMagazine.Models.Entity
{
/// <summary>
/// Snapshot record for a user answer.
/// </summary>
[SugarTable("Journal_PageTaskUserAnswerSnapshot")]
public class JournalPageTaskUserAnswerSnapshot : SqlSugarBaseEntity
{
/// <summary>
/// Source JournalPageTaskUserAnswer record Id.
/// </summary>
public long JournalPageTaskUserAnswerId { get; set; }
/// <summary>
/// Journal Id.
/// </summary>
public long JournalId { get; set; }
/// <summary>
/// Journal page Id.
/// </summary>
public long JournalPageId { get; set; }
/// <summary>
/// Journal page task Id.
/// </summary>
public long JournalPageTaskId { get; set; }
/// <summary>
/// Journal page task group Id.
/// </summary>
public long JournalPageTaskGroupId { get; set; }
/// <summary>
/// User Id.
/// </summary>
public long UserId { get; set; }
/// <summary>
/// Answer result.
/// </summary>
public string? Result { get; set; }
/// <summary>
/// Points awarded.
/// </summary>
public int Points { get; set; }
/// <summary>
/// Growth points awarded.
/// </summary>
public float GrowthPoints { get; set; }
/// <summary>
/// Task score.
/// </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>
public string? QuestionAnswerUrl { get; set; }
/// <summary>
/// Answer image URL.
/// </summary>
public string? AnswerUrl { get; set; }
/// <summary>
/// Page answer image URL.
/// </summary>
public string? PageAnswerUrl { get; set; }
/// <summary>
/// Optimistic concurrency revision.
/// </summary>
public int Revision { get; set; }
/// <summary>
/// Answer start time.
/// </summary>
public DateTime AnswerStartTime { get; set; }
/// <summary>
/// Answer end time.
/// </summary>
public DateTime AnswerEndTime { get; set; }
/// <summary>
/// Answer duration in seconds.
/// </summary>
public int AnswerSeconds { get; set; }
/// <summary>
/// Image recognition result.
/// </summary>
public int ImageRecognition { get; set; }
/// <summary>
/// Journal page number.
/// </summary>
public int JournalPageNum { get; set; }
/// <summary>
/// Modify count.
/// </summary>
public int Modify { get; set; }
/// <summary>
/// Last tag Id.
/// </summary>
public long LastTag { get; set; }
/// <summary>
/// Dot page number.
/// </summary>
public int DotPageNum { get; set; }
/// <summary>
/// Page result image URL.
/// </summary>
public string? PageResultUrl { get; set; }
/// <summary>
/// Question type.
/// </summary>
public string? Type { get; set; }
/// <summary>
/// Dot page number text.
/// </summary>
public string? DotPageNo { get; set; }
/// <summary>
/// Page answer dot image URL.
/// </summary>
public string? PageAnswerDotUrl { get; set; }
/// <summary>
/// Break count.
/// </summary>
public int BreakCount { get; set; }
/// <summary>
/// Break time records.
/// </summary>
public string? BreakTimes { get; set; }
/// <summary>
/// Assignment status.
/// </summary>
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(){

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