Compare commits

...

88 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
glz
4142b621c3 feat: 新增工作服务项目并调整部分接口路由
1. 新增QYZH.InteractiveMagazine.WorkService工作服务项目,包含队列消费者框架、Hangfire定时任务支持
2. 修复PetService中未定义FeedPetOutput变量的问题
3. 调整JournalController的路由前缀从api/icr改为api
4. 将工作服务项目添加到解决方案中
2026-06-11 18:01:07 +08:00
glz
e493c85d08 refactor,feat: 批量代码重构与新增业务模块
1. 重命名枚举项与实体字段,修正类型引用
2. 新增IsNull扩展方法与多项字符串处理扩展
3. 新增大量业务DTO、服务接口与枚举定义
4. 重构RabbitMQ服务实现,替换旧版消息队列组件
5. 优化签到服务的宠物喂养事务逻辑
6. 移除冗余的项目引用与旧版消息队列代码
7. 新增Excel导出、导入模板相关工具方法
2026-06-11 17:01:24 +08:00
glz
ae4cd627df feat: 完成多模块功能迭代与优化
1.  新增宠物皮肤初始枚举值、下拉列表接口及相关DTO
2.  完善商品实体与DTO,新增虚拟商品标记和类型ID字段
3.  重构用户认证体系,支持多用户切换并重新生成JWT令牌
4.  新增签到配置管理全套功能,包括增删改查和状态管理
5.  优化模型验证过滤器和基础响应类的命名规范
6.  新增补签功能,完善签到服务逻辑
7.  拆分用户详情DTO,新增各子数据分页查询接口
8.  重构微信控制器的用户ID获取逻辑,统一使用激活用户ID
9.  修复背包服务中补签卡的扣减逻辑
10. 新增家长姓名修改接口和相关服务实现
2026-06-10 17:53:38 +08:00
glz
028e3dfb34 refactor: 重构用户与勋章体系,统一状态管理与数据结构
1.  新增通用默认状态枚举 DefaultStatusEnum,替换原有分散的状态枚举
2.  重构用户体系:拆分 WxUser 独立表存储微信身份,Users 表改为角色子用户表并关联 WxUser
3.  重构勋章模块:新增系统/期刊勋章类型,调整 JournalId 为可空,新增勋章状态字段
4.  重构微信认证流程:基于 WxUser 生成 Token,支持多子用户管理
5.  清理冗余枚举文件,重构多处业务逻辑适配新的数据结构
6.  修复用户手机号关联逻辑,迁移手机号字段至 WxUser 表
2026-06-10 13:47:13 +08:00
glz
296d87b245 feat: 新增宠物进化分组接口、OSS图片搬运功能及配置更新
1. 新增皮肤按进化阶段分组的输出DTO
2. 新增宠物相关便捷查询接口并实现对应控制器
3. 新增OSS图片搬运辅助类,实现临时文件迁移逻辑
4. 为商品、宠物相关服务添加OSS图片自动迁移功能
5. 调整微信授权同步逻辑移除异步等待
6. 更新配置文件中的微信密钥和OSS存储信息
7. 注册SDK服务到DI容器
2026-06-09 18:02:26 +08:00
glz
badf68a8f8 feat: 新增宠物封面图支持,优化补偿任务查询与宠物模板创建流程
1.  新增PetSkinImageTypeEnum.Cover枚举,添加封面图类型支持
2.  为PetSkin、PetTemplate实体添加封面图片地址字段
3.  新增补偿任务按用户ID筛选功能,优化UsersService查询逻辑
4.  重构宠物模板创建流程,支持批量创建进化链与图片
5.  移除PetEvolution实体的基础属性字段,简化进化形态结构
6.  新增OSS域名辅助工具与SDK注册扩展类
7.  优化宠物皮肤、进化阶段的DTO与服务层逻辑
2026-06-09 16:15:22 +08:00
glz
eb8f40f2ab refactor(auth): 优化JWT认证和Token刷新逻辑
1. 调整JWT令牌过期时间配置,新增JwtTokenExpiryDays配置项
2. 分离Token存储前缀,区分管理后台和微信端Token
3. 新增Redis Token有效性校验逻辑,拦截无效/已注销的Token
4. 简化自动刷新逻辑,改为每次请求刷新Redis Token过期时间
5. 完善相关注释和代码结构
2026-06-09 15:17:16 +08:00
glz
77778f67c1 feat: 新增SDK模块、OSS相关功能并优化Redis与代码扩展
1.  新增API版本枚举SDK项,创建SDK基础控制器
2.  添加IList、string扩展方法,新增时间戳扩展工具
3.  新增OSS配置、STS凭证服务、文件处理相关代码
4.  重构Redis缓存组件,替换StackExchange.Redis为CSRedisCore
5.  修复原有Redis操作方法调用,新增基础控制器快捷返回方法
6.  新增阿里云OSS、短信、VOD相关SDK依赖
7.  配置阿里云OSS相关参数到appsettings
2026-06-09 14:36:46 +08:00
glz
1d86580db7 refactor: 重构状态更新接口,新增商品管理与勋章规则配置
1. 重构用户、宠物模板、勋章、管理员状态更新接口,移除冗余参数改为自动切换状态
2. 重命名宠物背景枚举为宠物皮肤,调整商品状态字段为枚举类型
3. 新增商品上下架状态管理接口与对应逻辑
4. 新增勋章规则配置系统,支持动态加载可用表和字段
5. 优化商品查询与展示逻辑,适配新的状态字段
2026-06-09 11:41:28 +08:00
glz
0126c6b097 refactor: 完善枚举系统与状态类型转换,新增枚举管理接口
1. 为所有枚举添加Description特性用于中文描述
2. 移除实体类中冗余的状态字段注释与定义
3. 将所有枚举状态参数改为int类型转换,统一数据交互格式
4. 新增系统管理枚举查询接口与实现,支持获取所有枚举元数据
5. 调整宠物服务模板状态更新接口参数类型
2026-06-08 17:54:36 +08:00
335 changed files with 23379 additions and 2341 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

@ -7,6 +7,10 @@ namespace QYZH.InteractiveMagazine.Common.Extensions
/// </summary>
public static class DateTimeExtension
{
/// <summary>
/// 时间戳起始日期
/// </summary>
public static readonly DateTime TimestampStart = new(1970, 1, 1, 0, 0, 0, 0);
/// <summary>
/// 转换为Unix时间戳(秒)
/// </summary>
@ -84,5 +88,15 @@ namespace QYZH.InteractiveMagazine.Common.Extensions
{
return dateTime.Date.AddDays(1).AddTicks(-1);
}
public static long ToTimestamp(this DateTime dateTime, bool milliseconds = false)
{
var timestamp = dateTime.ToUniversalTime() - TimestampStart;
return (long)(milliseconds ? timestamp.TotalMilliseconds : timestamp.TotalSeconds);
}
public static long ToMillisecondsTimestamp(this DateTime dateTime)
{
return ToTimestamp(dateTime, true);
}
}
}

View File

@ -0,0 +1,441 @@
using System.Collections;
namespace QYZH.InteractiveMagazine.Common.Extensions
{
public static partial class Extensions
{
#region long
/// <summary>
/// 将object转换为long若转换失败则返回0。不抛出异常。
/// </summary>
/// <param name="str"></param>
/// <returns></returns>
public static long ParseToLong(this object obj)
{
try
{
return long.Parse(obj.ToString());
}
catch
{
return 0L;
}
}
/// <summary>
/// 将object转换为long若转换失败则返回指定值。不抛出异常。
/// </summary>
/// <param name="str"></param>
/// <param name="defaultValue"></param>
/// <returns></returns>
public static long ParseToLong(this string str, long defaultValue)
{
try
{
return long.Parse(str);
}
catch
{
return defaultValue;
}
}
#endregion
#region int
/// <summary>
/// 将object转换为int若转换失败则返回0。不抛出异常。
/// </summary>
/// <param name="str"></param>
/// <returns></returns>
public static int ParseToInt(this object str)
{
try
{
return Convert.ToInt32(str);
}
catch
{
return 0;
}
}
/// <summary>
/// 将object转换为int若转换失败则返回指定值。不抛出异常。
/// null返回默认值
/// </summary>
/// <param name="str"></param>
/// <param name="defaultValue"></param>
/// <returns></returns>
public static int ParseToInt(this object str, int defaultValue)
{
if (str == null)
{
return defaultValue;
}
try
{
return Convert.ToInt32(str);
}
catch
{
return defaultValue;
}
}
#endregion
#region short
/// <summary>
/// 将object转换为short若转换失败则返回0。不抛出异常。
/// </summary>
/// <param name="str"></param>
/// <returns></returns>
public static short ParseToShort(this object obj)
{
try
{
return short.Parse(obj.ToString());
}
catch
{
return 0;
}
}
/// <summary>
/// 将object转换为short若转换失败则返回指定值。不抛出异常。
/// </summary>
/// <param name="str"></param>
/// <returns></returns>
public static short ParseToShort(this object str, short defaultValue)
{
try
{
return short.Parse(str.ToString());
}
catch
{
return defaultValue;
}
}
#endregion
#region demical
/// <summary>
/// 将object转换为demical若转换失败则返回指定值。不抛出异常。
/// </summary>
/// <param name="str"></param>
/// <returns></returns>
public static decimal ParseToDecimal(this object str, decimal defaultValue)
{
try
{
return decimal.Parse(str.ToString());
}
catch
{
return defaultValue;
}
}
/// <summary>
/// 将object转换为demical若转换失败则返回0。不抛出异常。
/// </summary>
/// <param name="str"></param>
/// <returns></returns>
public static decimal ParseToDecimal(this object str)
{
try
{
return decimal.Parse(str.ToString());
}
catch
{
return 0;
}
}
#endregion
#region bool
/// <summary>
/// 将object转换为bool若转换失败则返回false。不抛出异常。
/// </summary>
/// <param name="str"></param>
/// <returns></returns>
public static bool ParseToBool(this object str)
{
try
{
return bool.Parse(str.ToString());
}
catch
{
return false;
}
}
/// <summary>
/// 将object转换为bool若转换失败则返回指定值。不抛出异常。
/// </summary>
/// <param name="str"></param>
/// <returns></returns>
public static bool ParseToBool(this object str, bool result)
{
try
{
return bool.Parse(str.ToString());
}
catch
{
return result;
}
}
#endregion
#region float
/// <summary>
/// 将object转换为float若转换失败则返回0。不抛出异常。
/// </summary>
/// <param name="str"></param>
/// <returns></returns>
public static float ParseToFloat(this object str)
{
try
{
return float.Parse(str.ToString());
}
catch
{
return 0;
}
}
/// <summary>
/// 将object转换为float若转换失败则返回指定值。不抛出异常。
/// </summary>
/// <param name="str"></param>
/// <returns></returns>
public static float ParseToFloat(this object str, float result)
{
try
{
return float.Parse(str.ToString());
}
catch
{
return result;
}
}
#endregion
#region Guid
/// <summary>
/// 将string转换为Guid若转换失败则返回Guid.Empty。不抛出异常。
/// </summary>
/// <param name="str"></param>
/// <returns></returns>
public static Guid ParseToGuid(this string str)
{
try
{
return new Guid(str);
}
catch
{
return Guid.Empty;
}
}
#endregion
#region DateTime
/// <summary>
/// 将string转换为DateTime若转换失败则返回日期最小值。不抛出异常。
/// </summary>
/// <param name="str"></param>
/// <returns></returns>
public static DateTime ParseToDateTime(this string str)
{
try
{
if (string.IsNullOrWhiteSpace(str))
{
return DateTime.MinValue;
}
if (str.Contains("-") || str.Contains("/"))
{
return DateTime.Parse(str);
}
else
{
int length = str.Length;
switch (length)
{
case 4:
return DateTime.ParseExact(str, "yyyy", System.Globalization.CultureInfo.CurrentCulture);
case 6:
return DateTime.ParseExact(str, "yyyyMM", System.Globalization.CultureInfo.CurrentCulture);
case 8:
return DateTime.ParseExact(str, "yyyyMMdd", System.Globalization.CultureInfo.CurrentCulture);
case 10:
return DateTime.ParseExact(str, "yyyyMMddHH", System.Globalization.CultureInfo.CurrentCulture);
case 12:
return DateTime.ParseExact(str, "yyyyMMddHHmm", System.Globalization.CultureInfo.CurrentCulture);
case 14:
return DateTime.ParseExact(str, "yyyyMMddHHmmss", System.Globalization.CultureInfo.CurrentCulture);
default:
return DateTime.ParseExact(str, "yyyyMMddHHmmss", System.Globalization.CultureInfo.CurrentCulture);
}
}
}
catch
{
return DateTime.MinValue;
}
}
/// <summary>
/// 将string转换为DateTime若转换失败则返回默认值。
/// </summary>
/// <param name="str"></param>
/// <param name="defaultValue"></param>
/// <returns></returns>
public static DateTime ParseToDateTime(this string str, DateTime? defaultValue)
{
try
{
if (string.IsNullOrWhiteSpace(str))
{
return defaultValue.GetValueOrDefault();
}
if (str.Contains("-") || str.Contains("/"))
{
return DateTime.Parse(str);
}
else
{
int length = str.Length;
switch (length)
{
case 4:
return DateTime.ParseExact(str, "yyyy", System.Globalization.CultureInfo.CurrentCulture);
case 6:
return DateTime.ParseExact(str, "yyyyMM", System.Globalization.CultureInfo.CurrentCulture);
case 8:
return DateTime.ParseExact(str, "yyyyMMdd", System.Globalization.CultureInfo.CurrentCulture);
case 10:
return DateTime.ParseExact(str, "yyyyMMddHH", System.Globalization.CultureInfo.CurrentCulture);
case 12:
return DateTime.ParseExact(str, "yyyyMMddHHmm", System.Globalization.CultureInfo.CurrentCulture);
case 14:
return DateTime.ParseExact(str, "yyyyMMddHHmmss", System.Globalization.CultureInfo.CurrentCulture);
default:
return DateTime.ParseExact(str, "yyyyMMddHHmmss", System.Globalization.CultureInfo.CurrentCulture);
}
}
}
catch
{
return defaultValue.GetValueOrDefault();
}
}
#endregion
#region string
/// <summary>
/// 将object转换为string若转换失败则返回""。不抛出异常。
/// </summary>
/// <param name="str"></param>
/// <returns></returns>
public static string ParseToString(this object obj)
{
try
{
if (obj == null)
{
return string.Empty;
}
else
{
return obj.ToString();
}
}
catch
{
return string.Empty;
}
}
public static string ParseToStrings<T>(this object obj)
{
try
{
var list = obj as IEnumerable<T>;
if (list != null)
{
return string.Join(",", list);
}
else
{
return obj.ToString();
}
}
catch
{
return string.Empty;
}
}
#endregion
#region double
/// <summary>
/// 将object转换为double若转换失败则返回0。不抛出异常。
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
public static double ParseToDouble(this object obj)
{
try
{
return double.Parse(obj.ToString());
}
catch
{
return 0;
}
}
/// <summary>
/// 将object转换为double若转换失败则返回指定值。不抛出异常。
/// </summary>
/// <param name="str"></param>
/// <param name="defaultValue"></param>
/// <returns></returns>
public static double ParseToDouble(this object str, double defaultValue)
{
try
{
return double.Parse(str.ToString());
}
catch
{
return defaultValue;
}
}
#endregion
#region
/// <summary>
/// 强制转换类型
/// </summary>
/// <typeparam name="TResult"></typeparam>
/// <param name="source"></param>
/// <returns></returns>
public static IEnumerable<TResult> CastSuper<TResult>(this IEnumerable source)
{
foreach (object item in source)
{
yield return (TResult)Convert.ChangeType(item, typeof(TResult));
}
}
#endregion
}
}

View File

@ -0,0 +1,44 @@
//using Microsoft.AspNetCore.Http;
namespace QYZH.InteractiveMagazine.Common.Extensions
{
public static partial class Extensions
{
public static bool IsEmpty(this object value)
{
if (value != null && !string.IsNullOrEmpty(value.ParseToString()))
{
return false;
}
else
{
return true;
}
}
public static bool IsNotEmpty(this object value)
{
return !IsEmpty(value);
}
public static bool IsNullOrZero(this object value)
{
if (value == null || value.ParseToString().Trim() == "0")
{
return true;
}
else
{
return false;
}
}
//public static bool IsAjaxRequest(this HttpRequest request)
//{
// if (request == null)
// throw new ArgumentNullException("request");
// if (request.Headers != null)
// return request.Headers["X-Requested-With"] == "XMLHttpRequest";
// return false;
//}
}
}

View File

@ -90,5 +90,15 @@ namespace QYZH.InteractiveMagazine.Common.Extensions
return dictionary;
}
public static bool IsNull<T>(this IList<T>? s)
{
return s == null || s?.Count() < 1;
}
public static bool IsNull(this object? s)
{
return s == null;
}
}
}

View File

@ -146,5 +146,72 @@ namespace QYZH.InteractiveMagazine.Common.Extensions
return Regex.IsMatch(input, pattern);
}
/// <summary>
/// 判断字符串是否为Null、空
/// </summary>
/// <param name="s"></param>
/// <returns></returns>
public static bool IsNull(this string s)
{
return string.IsNullOrWhiteSpace(s);
}
public static string RemoveDomain(this string str)
{
if (str.IsNull()) return str;
// 匹配域名后的路径部分(包括第一个/
return Regex.Replace(str, @"https?://[^/]+/([^""\s<>]*)", "$1");
}
public static string AddDomain(this string str, string domainUrl)
{
if (string.IsNullOrEmpty(str) || string.IsNullOrEmpty(domainUrl)) return str;
string pattern = @"src=""([^""]+)""";
return Regex.Replace(str, pattern, match =>
{
string originalUrl = match.Groups[1].Value;
// 忽略base64图片
if (originalUrl.StartsWith("data:", StringComparison.OrdinalIgnoreCase)) return match.Value; // 返回原样
// 如果已经是完整URL则不做处理
if (Uri.IsWellFormedUriString(originalUrl, UriKind.Absolute)) return match.Value; // 返回原样
// 处理URL开头可能存在的斜杠
var trimmedUrl = originalUrl.StartsWith("/") ? originalUrl.Substring(1) : originalUrl;
var trimmedDomain = domainUrl.EndsWith("/") ? domainUrl : domainUrl + "/";
string newUrl = $"{trimmedDomain}{trimmedUrl}";
return $"src=\"{newUrl}\""; // 返回替换后的属性
});
}
public static string ToExtension(this string str)
{
return Regex.Match(str, @"(?<=\.)[a-zA-Z0-9]+$").Value;
}
public static bool NotNull(this string s)
{
return !string.IsNullOrWhiteSpace(s);
}
public static List<string> AllUrl(this string str, string url = null)
{
if (string.IsNullOrEmpty(str)) return new List<string>();
string pattern = @"src=""([^""]+)""";
var matches = Regex.Matches(str, pattern, RegexOptions.IgnoreCase);
return matches
.Cast<Match>()
.Select(m => m.Groups[1].Value)
.Where(u => !string.IsNullOrEmpty(u) && !u.StartsWith("data:", StringComparison.OrdinalIgnoreCase)) // 排除base64
.Where(url => !string.IsNullOrEmpty(url) && url.StartsWith(url, StringComparison.OrdinalIgnoreCase))
//.Where(url => url.StartsWith("https://example.com")) // 二次验证
.ToList();
}
}
}

View File

@ -0,0 +1,26 @@
namespace QYZH.InteractiveMagazine.Common.Helpers
{
public static class DomainHelper
{
public static string OssDomain { get; set; }
public static string VodDomain { get; set; }
public static string OssFullUrl(string url) => GetFullUrl(url, OssDomain);
public static string VodFullUrl(string url) => GetFullUrl(url, VodDomain);
private static string GetFullUrl(string url, string domain)
{
if (string.IsNullOrWhiteSpace(url)) return string.Empty;
if (url.StartsWith("http")) return url;
// 确保域名末尾没有斜杠url开头有斜杠
var cleanDomain = domain.TrimEnd('/');
var cleanUrl = url.StartsWith("/") ? url : "/" + url;
return $"{cleanDomain}{cleanUrl}";
}
}
}

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

@ -1,9 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\QYZH.InteractiveMagazine.Models\QYZH.InteractiveMagazine.Models.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="AutoMapper" Version="16.1.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />

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

@ -43,4 +43,11 @@ public interface IAdminUserService : IBaseService<AdminUser>
/// <param name="input">查询条件</param>
/// <returns>分页结果</returns>
Task<PageListModel<AdminUserOutput>> GetListAsync(AdminUserQueryInput input);
/// <summary>
/// 切换管理员状态(激活/冻结)
/// </summary>
/// <param name="id">管理员ID</param>
/// <param name="status">目标状态1=激活0=冻结</param>
Task ToggleStatusAsync(long id);
}

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

@ -0,0 +1,52 @@
using QYZH.InteractiveMagazine.Models.Dto;
using QYZH.InteractiveMagazine.Models.Dto.CheckIn;
using QYZH.InteractiveMagazine.Models.Entity;
namespace QYZH.InteractiveMagazine.IService;
/// <summary>
/// 签到配置服务接口
/// </summary>
public interface ICheckInConfigService : IBaseService<CheckInConfig>
{
/// <summary>
/// 创建签到配置
/// </summary>
/// <param name="input">签到配置输入</param>
/// <returns>创建的签到配置信息</returns>
Task<CheckInConfigOutput> CreateAsync(CheckInConfigInput input);
/// <summary>
/// 更新签到配置
/// </summary>
/// <param name="id">签到配置ID</param>
/// <param name="input">签到配置输入</param>
/// <returns>更新后的签到配置信息</returns>
Task<CheckInConfigOutput> UpdateAsync(long id, CheckInConfigInput input);
/// <summary>
/// 删除签到配置(软删除)
/// </summary>
/// <param name="id">签到配置ID</param>
Task DeleteAsync(long id);
/// <summary>
/// 根据ID获取签到配置
/// </summary>
/// <param name="id">签到配置ID</param>
/// <returns>签到配置信息</returns>
Task<CheckInConfigOutput> GetByIdAsync(long id);
/// <summary>
/// 分页查询签到配置列表
/// </summary>
/// <param name="input">查询条件</param>
/// <returns>分页结果</returns>
Task<PageListModel<CheckInConfigOutput>> GetListAsync(CheckInConfigQueryInput input);
/// <summary>
/// 更新签到配置启用/禁用状态
/// </summary>
/// <param name="id">签到配置ID</param>
Task<bool> UpdateStatusAsync(long id);
}

View File

@ -0,0 +1,45 @@
using QYZH.InteractiveMagazine.Models;
using QYZH.InteractiveMagazine.Models.Base;
using QYZH.InteractiveMagazine.Models.Dto.Journal;
using QYZH.InteractiveMagazine.Models.Entity;
namespace QYZH.InteractiveMagazine.IService;
public interface IJournalCatalogService : IBaseService<JournalCatalog>
{
Task<List<JournalCatalog>> DetailAsync(long JournalId);
Task<long> InsertAsync(JournalCatalogInput input);
Task<long> ImportAsync(JournalImportDto input);
Task<bool> UpdateAsync(JournalCatalogUpdateInput input);
Task<bool> DeleteAsync(long id);
Task<bool> MoveAsync(MoveInput input);
//Task<bool> CopyAsync(CopyInput input);
/// <summary>
/// 获取书分类及页码
/// </summary>
/// <param name="JournalId"></param>
/// <returns></returns>
Task<List<JournalCatalogTreeListDto>> GetJournalCatalogListAsync(long JournalId);
/// <summary>
/// 获取书分类及页码
/// </summary>
/// <param name="JournalId"></param>
/// <returns></returns>
Task<List<IcrJournalCatalogTreeDto>> GetJournalCataloTreeAsync(long JournalId);
/// <summary>
/// 导入书籍目录
/// </summary>
/// <param name="JournalId"></param>
/// <param name="dtos"></param>
/// <returns></returns>
Task<bool> ImportCatalogAsync(long JournalId, List<JournalCatalogTreeListDto> dtos);
}

View File

@ -0,0 +1,30 @@

using QYZH.InteractiveMagazine.Models.Dto.Journal;
using QYZH.InteractiveMagazine.Models.Entity;
namespace QYZH.InteractiveMagazine.IService;
public interface IJournalPageService: IBaseService<JournalPage>
{
Task<long> InsertAsync(JournalAddV2Input input);
Task<bool> UpdateAsync(PageLayoutInput input);
/// <summary>
/// 修改书页的点阵码
/// </summary>
/// <param name="JournalId"></param>
/// <returns></returns>
Task<bool> UpdatePageNoAsync(long JournalId);
Task<JournalPageV2Output> DetailAsync(long id);
Task<bool> DeleteAsync(long id);
/// <summary>
/// 回调接口-自动铺码, 书页铺码后回调接口,更新书页的点阵码
/// </summary>
/// <param name="bookPagePrintDto"></param>
/// <returns></returns>
Task<bool> CallbackPageUpdatePageNo(JournalPagePrintDto journalPagePrintDto);
}

View File

@ -0,0 +1,20 @@

using QYZH.InteractiveMagazine.Models.Dto.Journal;
using QYZH.InteractiveMagazine.Models.Entity;
namespace QYZH.InteractiveMagazine.IService;
public interface IJournalPageTaskService : IBaseService<JournalPageTask>
{
Task<long?> InsertAsync(JournalPageTaskAddInput input);
Task<bool> UpdateAsync(JournalPageTaskUpdateInput 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

@ -0,0 +1,57 @@
using QYZH.InteractiveMagazine.Models.Dto.Journal;
using QYZH.InteractiveMagazine.Models.Dto.DotMatrix;
using QYZH.InteractiveMagazine.Models.Enum;
using QYZH.InteractiveMagazine.Models.Dto;
using QYZH.InteractiveMagazine.Models.Entity;
namespace QYZH.InteractiveMagazine.IService;
public interface IJournalService : IBaseService<Journal>
{
/// <summary>
/// 查询List
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
Task<List<JournalDto>> GetListAsync(JournalQueryDto dto);
/// <summary>
/// 分页查询
/// </summary>
/// <param name="search"></param>
/// <returns></returns>
Task<PageListModel<JournalDto>> GetPageListAsync(PageQueryModel<JournalQueryDto> search);
/// <summary>
/// 创建杂志
/// </summary>
/// <param name="input">杂志信息</param>
/// <returns>新杂志ID</returns>
Task<BaseResponse<long>> AddAsync(JournalAddDto input);
/// <summary>
/// 编辑
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
Task<BaseResponse<bool>> EditAsync(JournalEditDto input);
Task<List<JournalTaskOutput>> Tasks(long id);
Task<JournalDto> DetailAsync(long id);
Task<bool> DeleteAsync(List<long> id);
Task<bool> StartPageAsync(long Id, int Index);
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

@ -48,7 +48,7 @@ public interface IMedalService : IBaseService<Medal>
/// </summary>
/// <param name="id">勋章ID</param>
/// <param name="status">状态: 0=禁用, 1=启用</param>
Task UpdateStatusAsync(long id, int status);
Task<bool> UpdateStatusAsync(long id);
/// <summary>
/// 获取勋章的规则列表
@ -77,4 +77,15 @@ public interface IMedalService : IBaseService<Medal>
/// <param name="userId">用户ID</param>
/// <param name="input">激活输入</param>
Task ActivateMedalAsync(long userId, WxMedalActivateInput input);
/// <summary>
/// 获取规则配置中可用的目标表列表
/// </summary>
Task<List<MedalRuleTableOptionOutput>> GetAvailableTablesAsync();
/// <summary>
/// 获取指定表的可用字段列表
/// </summary>
/// <param name="tableName">表名</param>
Task<List<MedalRuleFieldOptionOutput>> GetTableFieldsAsync(string tableName);
}

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

@ -37,6 +37,14 @@ public interface IPetService : IBaseService<UserPet>
/// <returns>喂养结果</returns>
Task<FeedPetOutput> FeedPetAsync(long userId, FeedPetInput input);
/// <summary>
/// 喂养宠物(无事务,需在外部事务中调用)
/// </summary>
/// <param name="userId">用户Id</param>
/// <param name="input">喂养输入</param>
/// <returns>喂养结果</returns>
Task<FeedPetOutput> FeedPetInTranAsync(long userId, FeedPetInput input);
/// <summary>
/// 获取宠物喂养记录列表
/// </summary>
@ -75,7 +83,7 @@ public interface IPetService : IBaseService<UserPet>
/// <summary>
/// 更新模板状态(启用/禁用)
/// </summary>
Task UpdateTemplateStatusAsync(long id, string status);
Task UpdateTemplateStatusAsync(long id);
// ==================== 后台管理:进化链 ====================
@ -152,4 +160,27 @@ public interface IPetService : IBaseService<UserPet>
/// 获取指定皮肤的所有图片
/// </summary>
Task<List<PetSkinImageOutput>> GetSkinImagesBySkinIdAsync(long skinId);
// ==================== 便捷查询接口 ====================
/// <summary>
/// 根据模板Id获取进化链列表非分页按StageLevel排序
/// </summary>
Task<List<PetEvolutionOutput>> GetEvolutionsByTemplateIdAsync(long templateId);
/// <summary>
/// 根据模板Id获取皮肤列表非分页按SortOrder排序
/// </summary>
Task<List<PetSkinOutput>> GetSkinsByTemplateIdAsync(long templateId);
/// <summary>
/// 根据皮肤Id获取各阶段图片列表按进化阶段分组含阶段基本信息
/// </summary>
Task<List<SkinEvolutionStageOutput>> GetSkinImagesGroupedBySkinIdAsync(long skinId);
/// <summary>
/// 获取所有皮肤下拉列表仅返回Id和Name按SortOrder排序
/// </summary>
/// <param name="types">皮肤类型列表(可选)</param>
Task<List<SelectOptionDto>> GetAllSkinsForSelectAsync(List<string>? types = null);
}

View File

@ -42,4 +42,10 @@ public interface IProductService : IBaseService<Product>
/// <param name="input">查询条件</param>
/// <returns>分页结果</returns>
Task<PageListModel<ProductOutput>> GetListAsync(ProductQueryInput input);
/// <summary>
/// 更新商品上架/下架状态(取反)
/// </summary>
/// <param name="id">商品ID</param>
Task UpdateSaleStatusAsync(long id);
}

View File

@ -0,0 +1,15 @@
using QYZH.InteractiveMagazine.Models.Dto.SystemManagement;
namespace QYZH.InteractiveMagazine.IService;
/// <summary>
/// 系统管理服务接口
/// </summary>
public interface ISystemManagementService
{
/// <summary>
/// 获取所有枚举信息
/// </summary>
/// <returns>枚举信息列表</returns>
Task<List<EnumInfoOutput>> GetAllEnumsAsync();
}

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

@ -1,4 +1,6 @@
using QYZH.InteractiveMagazine.Models.Dto;
using QYZH.InteractiveMagazine.Models.Dto.CheckIn;
using QYZH.InteractiveMagazine.Models.Dto.Compensation;
using QYZH.InteractiveMagazine.Models.Dto.Points;
using QYZH.InteractiveMagazine.Models.Entity;
@ -12,14 +14,14 @@ public interface IUsersService : IBaseService<Users>
Task<BaseResponse<PageListModel<UsersOutput>>> GetListAsync(UsersQueryInput input);
/// <summary>
/// 获取用户详情(包含积分记录、签到记录、补偿任务、期刊列表
/// 获取用户详情(仅基本信息
/// </summary>
Task<BaseResponse<UserDetailOutput>> GetDetailAsync(long id);
/// <summary>
/// 更新用户状态
/// </summary>
Task<BaseResponse> UpdateStatusAsync(long id, UpdateUserStatusInput input);
Task<BaseResponse> UpdateStatusAsync(long id);
/// <summary>
/// 手动增加用户积分
@ -30,4 +32,24 @@ public interface IUsersService : IBaseService<Users>
/// 手动扣除用户积分
/// </summary>
Task<ManualPointsOutput> ManualDeductPointsAsync(long userId, ManualDeductPointsInput input, long operatorId, string operatorName, string? ipAddress = null);
/// <summary>
/// 分页查询用户积分记录
/// </summary>
Task<BaseResponse<PageListModel<PointsRecordOutput>>> GetUserPointsRecordsAsync(long userId, PointsRecordQueryInput input);
/// <summary>
/// 分页查询用户签到记录
/// </summary>
Task<BaseResponse<PageListModel<CheckInRecordOutput>>> GetUserCheckInRecordsAsync(long userId, PageQueryModel input);
/// <summary>
/// 分页查询用户补偿任务
/// </summary>
Task<BaseResponse<PageListModel<CompensationTaskOutput>>> GetUserCompensationTasksAsync(long userId, CompensationTaskQueryInput input);
/// <summary>
/// 分页查询用户期刊列表(含期刊详情)
/// </summary>
Task<BaseResponse<PageListModel<UserJournalItemOutput>>> GetUserJournalsAsync(long userId, UserJournalQueryInput input);
}

View File

@ -1,4 +1,3 @@
using QYZH.InteractiveMagazine.Models.Entity;
using QYZH.InteractiveMagazine.Models.WeChat;
@ -7,27 +6,46 @@ namespace QYZH.InteractiveMagazine.IService;
/// <summary>
/// 微信小程序认证服务
/// </summary>
public interface IWeChatAuthService : IBaseService<Users>
public interface IWeChatAuthService : IBaseService<WxUser>
{
/// <summary>
/// 微信小程序登录(首次创建用户,非首次直接登录
/// 微信小程序登录(首次创建 WxUser不自动创建 User
/// </summary>
/// <param name="input">登录输入(含微信 code 和可选的手机号 code</param>
/// <returns>登录结果(含 Token 和用户信息)</returns>
Task<WeChatLoginOutput> LoginAsync(WeChatLoginInput input);
/// <summary>
/// 微信小程序快捷登录(通过 OpenId 直接登录,用户需已存在
/// 微信小程序快捷登录(通过 OpenId 直接登录)
/// </summary>
/// <param name="input">快捷登录输入(含 OpenId</param>
/// <returns>登录结果(含 Token 和用户列表)</returns>
Task<WeChatLoginOutput> QuickLoginAsync(WeChatQuickLoginInput input);
/// <summary>
/// 切换用户(同一 OpenId 下切换身份
/// 切换用户(同一 WxUser 下切换 User 身份,重新生成 Token
/// </summary>
/// <param name="currentUserId">当前登录用户ID</param>
/// <param name="input">切换用户输入</param>
/// <returns>切换结果(含新 Token 和目标用户详情)</returns>
Task<WeChatSwitchUserOutput> SwitchUserAsync(long currentUserId, WeChatSwitchUserInput input);
/// <param name="wxUserId">当前 WxUser.Id来自 JWT</param>
/// <param name="currentUserId">当前激活 Users.Id来自 JWT用于清除旧 Redis Token</param>
/// <param name="input">切换目标用户参数</param>
Task<WeChatSwitchUserOutput> SwitchUserAsync(long wxUserId, long currentUserId, WeChatSwitchUserInput input);
/// <summary>
/// 获取当前 WxUser 下所有用户列表
/// </summary>
/// <param name="wxUserId">当前登录的 WxUser.Id来自 JWT</param>
/// <returns>用户列表</returns>
Task<List<WxUserOutput>> GetUsersAsync(long wxUserId);
/// <summary>
/// 在当前 WxUser 下新增用户(子用户/角色)
/// </summary>
/// <param name="wxUserId">当前登录的 WxUser.Id来自 JWT</param>
/// <param name="input">新增用户参数</param>
/// <returns>新创建的用户信息</returns>
Task<WxUserOutput> CreateUserAsync(long wxUserId, CreateChildUserInput input);
/// <summary>
/// 修改家长名字WxUser.Name
/// </summary>
/// <param name="wxUserId">当前登录的 WxUser.Id来自 JWT</param>
/// <param name="input">修改名字参数</param>
/// <returns>更新后的微信用户信息</returns>
Task<WxUserInfoOutput> UpdateWxUserNameAsync(long wxUserId, UpdateWxUserNameInput input);
}

View File

@ -11,8 +11,11 @@ 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令牌
/// 生成JWT令牌(管理端 / 单用户场景)
/// </summary>
/// <param name="userId">用户ID</param>
/// <param name="userName">用户名</param>
@ -29,6 +32,54 @@ public static class JwtHelper
new Claim(ClaimTypes.Name, userName)
};
return BuildToken(claims, settings);
}
/// <summary>
/// 生成JWT令牌小程序场景同时携带 WxUserId 和 UserId
/// </summary>
/// <param name="wxUserId">微信用户IDWxUser.Id</param>
/// <param name="userId">当前激活用户IDUsers.Id无用户时为 0</param>
/// <param name="userName">用户名</param>
/// <param name="settings">JWT配置</param>
/// <returns>JWT令牌字符串</returns>
public static string GenerateToken(long wxUserId, long userId, string userName, JwtSettings settings)
{
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Sub, userId.ToString()),
new Claim(JwtRegisteredClaimNames.Name, userName),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(ClaimTypes.NameIdentifier, userId.ToString()),
new Claim(ClaimTypes.Name, userName),
new Claim(WxUserIdClaimType, wxUserId.ToString())
};
return BuildToken(claims, settings);
}
/// <summary>
/// 自定义 Claim 类型WxUserId
/// </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!));
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
@ -36,7 +87,7 @@ public static class JwtHelper
issuer: settings.Issuer,
audience: settings.Audience,
claims: claims,
expires: DateTime.Now.AddMinutes(settings.ExpiryMinutes),
expires: DateTime.Now.AddDays(settings.JwtTokenExpiryDays),
signingCredentials: credentials
);
@ -68,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;
@ -77,6 +128,25 @@ public static class JwtHelper
return null;
}
/// <summary>
/// 从Token中获取微信用户IDWxUserId
/// </summary>
/// <param name="token">JWT令牌</param>
/// <returns>WxUserId</returns>
public static long? GetWxUserIdFromToken(string token)
{
var tokenHandler = new JwtSecurityTokenHandler();
if (tokenHandler.ReadToken(token) is JwtSecurityToken jwtToken)
{
var wxUserIdClaim = jwtToken.Claims.FirstOrDefault(c => c.Type == WxUserIdClaimType);
if (long.TryParse(wxUserIdClaim?.Value, out long wxUserId))
{
return wxUserId;
}
}
return null;
}
/// <summary>
/// 从Token中获取用户名
/// </summary>
@ -87,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

@ -2,8 +2,6 @@
using Autofac.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using QYZH.InteractiveMagazine.Infrastructure.Cache;
using QYZH.InteractiveMagazine.Infrastructure.MessageQueue;
using QYZH.InteractiveMagazine.Models.Settings;
using RabbitMQ.Client;
using StackExchange.Redis;
@ -26,42 +24,7 @@ namespace QYZH.InteractiveMagazine.Infrastructure.Autofacs
var source = friendlyName.Split('.');
var assemblyNames = string.Join(".", source.Take(source.Length - 1));
containerBuilder.RegisterModule(new AutofacModuleRegister(assemblyNames));
InitializeRedis(c.Configuration);
InitializeRabbitMQ(c.Configuration, containerBuilder);
});
}
private static void InitializeRedis(IConfiguration configuration)
{
var redisSettings = configuration.GetSection("RedisSettings").Get<RedisSettings>();
if (redisSettings != null && !string.IsNullOrWhiteSpace(redisSettings.ConnectionString))
{
var multiplexer = ConnectionMultiplexer.Connect(redisSettings.ConnectionString);
RedisHelper.Connection = multiplexer;
RedisHelper.SetKeyPrefix(redisSettings.InstanceName ?? string.Empty);
}
}
private static void InitializeRabbitMQ(IConfiguration configuration, ContainerBuilder containerBuilder)
{
var rabbitMQSettings = configuration.GetSection("RabbitMQSettings").Get<RabbitMQSettings>();
if (rabbitMQSettings != null && !string.IsNullOrWhiteSpace(rabbitMQSettings.HostName))
{
var factory = new ConnectionFactory
{
HostName = rabbitMQSettings.HostName,
Port = rabbitMQSettings.Port,
UserName = rabbitMQSettings.UserName ?? string.Empty,
Password = rabbitMQSettings.Password ?? string.Empty,
VirtualHost = rabbitMQSettings.VirtualHost ?? string.Empty
};
var connection = factory.CreateConnectionAsync().GetAwaiter().GetResult();
containerBuilder.RegisterInstance(connection).As<IConnection>().SingleInstance();
containerBuilder.RegisterType<RabbitMQPublisher>().InstancePerDependency();
}
}
}
}

View File

@ -1,119 +0,0 @@
using StackExchange.Redis;
namespace QYZH.InteractiveMagazine.Infrastructure.Cache;
/// <summary>
/// Redis操作封装
/// </summary>
public static class RedisHelper
{
private static string? _keyPrefix;
/// <summary>
/// Redis连接实例
/// </summary>
public static IConnectionMultiplexer? Connection { get; set; }
/// <summary>
/// 设置键前缀
/// </summary>
/// <param name="prefix">前缀字符串</param>
public static void SetKeyPrefix(string prefix)
{
_keyPrefix = prefix;
}
/// <summary>
/// 获取Redis数据库实例
/// </summary>
/// <param name="db">数据库索引</param>
/// <returns>IDatabase实例</returns>
public static IDatabase GetDatabase(int db = -1)
{
if (Connection == null || !Connection.IsConnected)
{
throw new InvalidOperationException("Redis连接未初始化或已断开");
}
return Connection.GetDatabase(db);
}
/// <summary>
/// 设置字符串值
/// </summary>
/// <param name="key">键</param>
/// <param name="value">值</param>
/// <param name="expiry">过期时间</param>
/// <returns>是否成功</returns>
public static async Task<bool> StringSetAsync(string key, string value, TimeSpan? expiry = null)
{
var db = GetDatabase();
var prefixedKey = GetPrefixedKey(key);
if (expiry.HasValue)
{
return await db.StringSetAsync(prefixedKey, value, (Expiration)expiry.Value);
}
return await db.StringSetAsync(prefixedKey, value);
}
/// <summary>
/// 获取字符串值
/// </summary>
/// <param name="key">键</param>
/// <returns>值</returns>
public static async Task<string?> StringGetAsync(string key)
{
var db = GetDatabase();
var prefixedKey = GetPrefixedKey(key);
return await db.StringGetAsync(prefixedKey);
}
/// <summary>
/// 删除键
/// </summary>
/// <param name="key">键</param>
/// <returns>是否成功</returns>
public static async Task<bool> KeyDeleteAsync(string key)
{
var db = GetDatabase();
var prefixedKey = GetPrefixedKey(key);
return await db.KeyDeleteAsync(prefixedKey);
}
/// <summary>
/// 检查键是否存在
/// </summary>
/// <param name="key">键</param>
/// <returns>是否存在</returns>
public static async Task<bool> KeyExistsAsync(string key)
{
var db = GetDatabase();
var prefixedKey = GetPrefixedKey(key);
return await db.KeyExistsAsync(prefixedKey);
}
/// <summary>
/// 设置键的过期时间
/// </summary>
/// <param name="key">键</param>
/// <param name="expiry">过期时间</param>
/// <returns>是否成功</returns>
public static async Task<bool> KeyExpireAsync(string key, TimeSpan expiry)
{
var db = GetDatabase();
var prefixedKey = GetPrefixedKey(key);
return await db.KeyExpireAsync(prefixedKey, expiry);
}
/// <summary>
/// 获取带前缀的键
/// </summary>
/// <param name="key">原始键</param>
/// <returns>带前缀的键</returns>
private static string GetPrefixedKey(string key)
{
return string.IsNullOrEmpty(_keyPrefix) ? key : $"{_keyPrefix}:{key}";
}
}

View File

@ -7,73 +7,147 @@ 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;
using System.Text;
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;
}
var userId = context.Principal?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId))
{
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)
{
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>
{
@ -84,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

@ -1,72 +0,0 @@
using Microsoft.Extensions.Logging;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using System.Text;
namespace QYZH.InteractiveMagazine.Infrastructure.MessageQueue;
/// <summary>
/// RabbitMQ消息消费者基类
/// </summary>
public abstract class RabbitMQConsumer : IDisposable
{
private readonly IConnection _connection;
private readonly ILogger<RabbitMQConsumer> _logger;
private IChannel? _channel;
private AsyncEventingBasicConsumer? _consumer;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="connection">RabbitMQ连接</param>
/// <param name="logger">日志记录器</param>
protected RabbitMQConsumer(IConnection connection, ILogger<RabbitMQConsumer> logger)
{
_connection = connection;
_logger = logger;
}
/// <summary>
/// 启动消费
/// </summary>
/// <param name="queueName">队列名称</param>
/// <param name="handleMessage">消息处理委托</param>
public async Task StartConsume(string queueName, Func<string, Task> handleMessage)
{
_channel = await _connection.CreateChannelAsync();
_consumer = new AsyncEventingBasicConsumer(_channel);
_consumer.ReceivedAsync += async (model, ea) =>
{
try
{
var body = ea.Body.ToArray();
var message = Encoding.UTF8.GetString(body);
await handleMessage(message);
await _channel.BasicAckAsync(ea.DeliveryTag, false);
_logger.LogInformation("消息消费成功 | 队列: {QueueName} | 消息: {Message}", queueName, message);
}
catch (Exception ex)
{
_logger.LogError(ex, "消息消费失败 | 队列: {QueueName}", queueName);
await _channel.BasicNackAsync(ea.DeliveryTag, false, true);
}
};
await _channel.BasicConsumeAsync(queue: queueName, autoAck: false, consumer: _consumer);
_logger.LogInformation("开始消费消息 | 队列: {QueueName}", queueName);
}
/// <summary>
/// 释放资源
/// </summary>
public void Dispose()
{
_channel?.DisposeAsync().GetAwaiter().GetResult();
GC.SuppressFinalize(this);
}
}

View File

@ -1,47 +0,0 @@
using Microsoft.Extensions.Logging;
using RabbitMQ.Client;
using System.Text;
namespace QYZH.InteractiveMagazine.Infrastructure.MessageQueue;
/// <summary>
/// RabbitMQ消息发布器
/// </summary>
public class RabbitMQPublisher
{
private readonly IConnection _connection;
private readonly ILogger<RabbitMQPublisher> _logger;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="connection">RabbitMQ连接</param>
/// <param name="logger">日志记录器</param>
public RabbitMQPublisher(IConnection connection, ILogger<RabbitMQPublisher> logger)
{
_connection = connection;
_logger = logger;
}
/// <summary>
/// 发布消息
/// </summary>
/// <param name="exchange">交换机名称</param>
/// <param name="routingKey">路由键</param>
/// <param name="message">消息内容</param>
public async Task PublishMessage(string exchange, string routingKey, string message)
{
await using var channel = await _connection.CreateChannelAsync();
var body = Encoding.UTF8.GetBytes(message);
await channel.BasicPublishAsync(
exchange: exchange,
routingKey: routingKey,
body: body
);
_logger.LogInformation("消息已发布 | 交换机: {Exchange} | 路由键: {RoutingKey} | 消息: {Message}",
exchange, routingKey, message);
}
}

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,79 +1,21 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using QYZH.InteractiveMagazine.Infrastructure.Cache;
using QYZH.InteractiveMagazine.Models.Settings;
using System.IdentityModel.Tokens.Jwt;
namespace QYZH.InteractiveMagazine.Infrastructure.Middleware;
/// <summary>
/// 保留兼容的JWT自动刷新中间件实际刷新在认证成功后执行。
/// </summary>
public class JwtAutoRefreshMiddleware
{
private readonly RequestDelegate _next;
private readonly IConfiguration _configuration;
private const string TokenKeyPrefix = "InteractiveMagazine:AdminAuth:Token";
private const int RefreshThresholdMinutes = 10;
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 TryAutoRefreshTokenAsync(context, token);
}
await _next(context);
}
private async Task TryAutoRefreshTokenAsync(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;
if (remainingTime <= TimeSpan.FromMinutes(RefreshThresholdMinutes) && remainingTime > TimeSpan.Zero)
{
var userId = jwtToken.Claims.FirstOrDefault(c => c.Type == System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
var userName = jwtToken.Claims.FirstOrDefault(c => c.Type == System.Security.Claims.ClaimTypes.Name)?.Value;
if (string.IsNullOrEmpty(userId) || string.IsNullOrEmpty(userName))
{
return;
}
var jwtSettings = _configuration.GetSection("JwtSettings").Get<JwtSettings>();
if (jwtSettings == null)
{
return;
}
var newToken = QYZH.InteractiveMagazine.Infrastructure.Auth.JwtHelper.GenerateToken(
long.Parse(userId), userName, jwtSettings);
await RedisHelper.StringSetAsync(
$"{TokenKeyPrefix}:{userId}",
newToken,
TimeSpan.FromMinutes(jwtSettings.ExpiryMinutes));
context.Response.Headers["X-New-Token"] = newToken;
}
}
catch
{
// 忽略自动刷新异常,由后续认证中间件处理
}
}
}

View File

@ -24,10 +24,10 @@ 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;
result.message = string.Join("|", errorDic.Select(e => e.Value).Distinct());
result.result = errorDic;
context.Result = new JsonResult(result);
}
}

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

@ -0,0 +1,62 @@

namespace QYZH.InteractiveMagazine.Infrastructure.OSS
{
/// <summary>
/// OSS配置
/// </summary>
public class OSSOptions
{
public const string Section = "AliyunOSSConfigs";
/// <summary>
/// ID
/// </summary>
public string AccessKeyID { get; set; }
/// <summary>
/// 密钥
/// </summary>
public string AccessKeySecret { get; set; }
/// <summary>
/// 地区
/// </summary>
public string Region { get; set; }
/// <summary>
/// 存储桶
/// </summary>
public string BucketName { get; set; }
/// <summary>
/// 视频点播储存桶
/// </summary>
public string VodBucketName { get; set; }
/// <summary>
/// 存储桶
/// </summary>
public string RoleArn { get; set; }
/// <summary>
/// 过期时间
/// </summary>
public int DurationSeconds { get; set; }
/// <summary>
/// 终结点
/// </summary>
public string Endpoint { get; set; }
/// <summary>
/// IMM转换项目名称
/// </summary>
public string ProjectName { get; set; }
/// <summary>
/// 访问域名
/// </summary>
public string Domain { get; set; }
}
}

View File

@ -0,0 +1,19 @@
namespace QYZH.InteractiveMagazine.Infrastructure.OSS
{
public class OssCredentials
{
public string AccessKeyId { get; set; }
public string AccessKeySecret { get; set; }
public string SecurityToken { get; set; }
public long ExpireTime { get; set; } // 格式化为 "yyyy-MM-dd HH:mm:ss"
public string BucketName { get; set; }
public string Region { get; set; } // 格式如 "oss-cn-hangzhou"
public string Domain { get; set; }
}
}

View File

@ -0,0 +1,96 @@
using Aliyun.Credentials.Models;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
using System.Web;
using Tea;
namespace QYZH.InteractiveMagazine.Infrastructure.OSS
{
public class OssIMMService
{
private readonly AlibabaCloud.SDK.Imm20200930.Client _client;
private readonly OSSOptions _oSSOptions;
private readonly IConfiguration _configuration;
public OssIMMService(IConfiguration configuration,IOptionsMonitor<OSSOptions> ossOptions)
{
this._configuration = configuration;
_oSSOptions = ossOptions.CurrentValue;
Config config = new Config()
{
Type = "access_key",
AccessKeyId = _oSSOptions.AccessKeyID,
AccessKeySecret = _oSSOptions.AccessKeySecret,
};
Aliyun.Credentials.Client credential = new Aliyun.Credentials.Client(config);
AlibabaCloud.OpenApiClient.Models.Config config1 = new AlibabaCloud.OpenApiClient.Models.Config
{
Credential = credential,
};
// Endpoint 请参考 https://api.aliyun.com/product/imm
config1.Endpoint = $"imm.cn-{_oSSOptions.Region}.aliyuncs.com";
_client = new AlibabaCloud.SDK.Imm20200930.Client(config1);
}
public string DocConvertPng(string source, string prefix)
{
var createOfficeConversionTaskRequest = new AlibabaCloud.SDK.Imm20200930.Models.CreateOfficeConversionTaskRequest
{
ProjectName = _oSSOptions.ProjectName,
TargetType = "png",
SourceURI = $"oss://{_oSSOptions.BucketName}/{HttpUtility.UrlDecode(source)}",
TargetURIPrefix = $"oss://{_oSSOptions.BucketName}/{prefix}",
//TargetURI = "oss://returnzero/output1/{dirname}/{barename}/{index}.{autoext}",//oss://examplebucket/outputprefix/
};
var runtime = new AlibabaCloud.TeaUtil.Models.RuntimeOptions();
try
{
// 复制代码运行请自行打印 API 的返回值
var data = _client.CreateOfficeConversionTaskWithOptions(createOfficeConversionTaskRequest, runtime);
if (data?.StatusCode == 200)
{
return data.Body.TaskId;
}
return null;
}
catch (Exception _error)
{
return null;
}
}
public bool GetTask(string taskId)
{
AlibabaCloud.SDK.Imm20200930.Models.GetTaskRequest getTaskRequest = new()
{
ProjectName = _oSSOptions.ProjectName,
TaskType = "OfficeConversion",
TaskId = taskId,
};
var runtime = new AlibabaCloud.TeaUtil.Models.RuntimeOptions();
try
{
var data = _client.GetTaskWithOptions(getTaskRequest, runtime);
if (data?.StatusCode == 200)
{
if (data.Body.Status == "Running") return GetTask(taskId);
if (data.Body.Status == "Succeeded") return true;
}
return false;
}
catch (Exception _error)
{
TeaException error = new TeaException(new Dictionary<string, object>
{
{ "message", _error.Message }
});
Console.WriteLine(error.Message);
return false;
}
}
}
}

View File

@ -0,0 +1,111 @@
using Microsoft.Extensions.Logging;
using QYZH.InteractiveMagazine.Common.Extensions;
using QYZH.InteractiveMagazine.Models.Common;
using QYZH.InteractiveMagazine.Models.Dto;
namespace QYZH.InteractiveMagazine.Infrastructure.OSS;
/// <summary>
/// OSS 图片搬运辅助类
/// 负责将 temp 临时目录下的文件搬运到正式目录
/// </summary>
public class OssImageHelper
{
private readonly OssService _ossService;
private readonly ILogger _logger;
public OssImageHelper(OssService ossService, ILogger logger)
{
_ossService = ossService;
_logger = logger;
}
/// <summary>
/// 判断图片是否在 temp 临时目录
/// </summary>
public static bool IsTempImage(string? imageUrl)
{
if (string.IsNullOrWhiteSpace(imageUrl)) return false;
return imageUrl.Contains("/temp/", StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// 将 temp 目录下的图片搬运到正式目录,返回正式路径
/// 如果图片不在 temp 目录,原样返回
/// </summary>
/// <param name="imageUrl">原始图片 URL可能是完整 URL 或相对路径)</param>
/// <param name="formalFolder">正式目录前缀,如 "product/makeupcard/123" 或 "pet/skin/456"</param>
/// <returns>正式路径(相对路径),搬运失败时抛出异常</returns>
public async Task<string> MoveToFormalAsync(string imageUrl, string formalFolder)
{
if (!IsTempImage(imageUrl))
return imageUrl;
// 提取 OSS 相对路径(去掉域名前缀)
var sourcePath = imageUrl.RemoveDomain();
if (string.IsNullOrWhiteSpace(sourcePath))
{
_logger.LogWarning("图片路径解析失败ImageUrl: {ImageUrl}", imageUrl);
return imageUrl;
}
// URL 解码(处理中文文件名)
sourcePath = Uri.UnescapeDataString(sourcePath);
// 提取文件名
var fileName = Path.GetFileName(sourcePath);
if (string.IsNullOrWhiteSpace(fileName))
{
_logger.LogWarning("无法从路径中提取文件名Path: {Path}", sourcePath);
return imageUrl;
}
// 生成正式路径
var targetPath = $"{formalFolder}/{fileName}";
_logger.LogInformation("开始搬运图片Source: {Source} -> Target: {Target}", sourcePath, targetPath);
try
{
// 拷贝文件到正式目录
var copyResult = _ossService.CopyObject(sourcePath, targetPath);
if (!copyResult)
{
_logger.LogError("OSS 图片拷贝失败Source: {Source}, Target: {Target}", sourcePath, targetPath);
throw new Exception("OSS 图片拷贝失败");
}
// 删除临时文件(失败不影响主流程)
var deleteResult = _ossService.DeleteObject(sourcePath);
if (!deleteResult)
{
_logger.LogWarning("OSS 临时文件删除失败不影响主流程Source: {Source}", sourcePath);
}
_logger.LogInformation("图片搬运成功Target: {Target}", targetPath);
return targetPath;
}
catch (Exception ex) when (ex is not BusinessException)
{
_logger.LogError(ex, "OSS 图片搬运异常Source: {Source}, Target: {Target}", sourcePath, targetPath);
throw new BusinessException("图片搬运失败,请重试", ResultCode.GLOBAL_ERROR);
}
}
/// <summary>
/// 批量搬运 temp 图片到正式目录
/// </summary>
/// <param name="imageUrls">原始图片 URL 列表</param>
/// <param name="formalFolder">正式目录前缀</param>
/// <returns>正式路径列表(顺序与输入一致)</returns>
public async Task<List<string>> MoveBatchToFormalAsync(IEnumerable<string> imageUrls, string formalFolder)
{
var results = new List<string>();
foreach (var url in imageUrls)
{
var formal = await MoveToFormalAsync(url, formalFolder);
results.Add(formal);
}
return results;
}
}

View File

@ -0,0 +1,356 @@
using Aliyun.Acs.Core;
using Aliyun.Acs.Core.Auth.Sts;
using Aliyun.Acs.Core.Profile;
using Aliyun.OSS;
using Aliyun.OSS.Common;
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;
namespace QYZH.InteractiveMagazine.Infrastructure.OSS
{
public class OssService
{
private readonly OssClient _ossClient;
private readonly OSSOptions _ossOption;
private readonly CSRedisClient _redis;
private readonly ILogger<OssService> _logger;
//private readonly IOssResourceRepository _ossResourceRepository;
public OssService(IOptionsMonitor<OSSOptions> options, CSRedisClient redis, ILogger<OssService> logger)
{
// _ossOption = configuration.GetSection("AliyunOSSConfigs").Get<OSSOptions>();
_ossOption = options.CurrentValue;
_ossClient = new OssClient($"oss-cn-{_ossOption.Region}.aliyuncs.com", _ossOption.AccessKeyID, _ossOption.AccessKeySecret);
_ossClient.SetRegion($"cn-{_ossOption.Region}");
_redis = redis;
_logger = logger;
//_ossResourceRepository = ossResourceRepository;
}
public async Task<OssCredentials> GetSTSTokenAsync(CancellationToken cancellationToken)
{
var stsJson = await _redis.GetAsync("STS-InteractiveMagazine-OssCredentials");
if (string.IsNullOrWhiteSpace(stsJson))
{
// ststoken
IClientProfile profile = DefaultProfile.GetProfile($"oss-cn-{_ossOption.Region}", _ossOption.AccessKeyID, _ossOption.AccessKeySecret);
DefaultAcsClient client = new DefaultAcsClient(profile);
var policy = new
{
Version = "1",
Statement = new List<dynamic>
{
new
{
Action = new List<string> { "oss:PutObject", "oss:GetObject","oss:DeleteObject" },
Resource = new List<string> { $"acs:oss:*:*:{_ossOption.BucketName}/temp/*" },
//Condition = new
//{
// NumericLessThanEquals = new Dictionary<string, object>()
// {
// {"oss:ContentLength",104857600 }
// }
//},
Effect = "Allow",
}
}
};
var assumeRoleRequest = new AssumeRoleRequest();
assumeRoleRequest.Policy = JsonSerializer.Serialize(policy);
assumeRoleRequest.RoleArn = _ossOption.RoleArn;
assumeRoleRequest.RoleSessionName = "oss-services";//这里的名字随便写
assumeRoleRequest.DurationSeconds = 3600;//过期时间
var response = client.GetAcsResponse(assumeRoleRequest);
var expiration = DateTime.Parse(response.Credentials.Expiration).AddMinutes(5);
var result = new OssCredentials
{
AccessKeyId = response.Credentials.AccessKeyId,
AccessKeySecret = response.Credentials.AccessKeySecret,
SecurityToken = response.Credentials.SecurityToken,
ExpireTime = expiration.ToMillisecondsTimestamp(),
BucketName = _ossOption.BucketName,
Region = $"oss-cn-{_ossOption.Region}",
Domain = _ossOption.Domain
};
await _redis.SetAsync("STS-InteractiveMagazine-OssCredentials", result, expiration - DateTime.Now);
return result;
}
else
{
return JsonSerializer.Deserialize<OssCredentials>(stsJson);
}
}
public async Task<Dictionary<string, string>> GetSTSToken()
{
var stsJson = await _redis.GetAsync("QYZH.InteractiveMagazine.STS");
if (stsJson.IsNull())
{
// ststoken
IClientProfile profile = DefaultProfile.GetProfile($"oss-cn-{_ossOption.Region}", _ossOption.AccessKeyID, _ossOption.AccessKeySecret);
DefaultAcsClient client = new DefaultAcsClient(profile);
var policy = new
{
Version = "1",
Statement = new List<dynamic>
{
new
{
Action = new List<string> { "oss:PutObject", "oss:GetObject","oss:DeleteObject" },
Resource = new List<string> { $"acs:oss:*:*:{_ossOption.BucketName}/temp/*" },
//Condition = new
//{
// NumericLessThanEquals = new Dictionary<string, object>()
// {
// {"oss:ContentLength",104857600 }
// }
//},
Effect = "Allow",
}
}
};
var request = new AssumeRoleRequest();
request.Policy = JsonSerializer.Serialize(policy);
request.RoleArn = _ossOption.RoleArn;
request.RoleSessionName = "oss-services";//这里的名字随便写
request.DurationSeconds = 3600;//过期时间
var response = client.GetAcsResponse(request);
var expiration = DateTime.Parse(response.Credentials.Expiration);
var result = new Dictionary<string, string>
{
{"AccessKeyId", response.Credentials.AccessKeyId},
{"AccessKeySecret",response.Credentials.AccessKeySecret },
{"SecurityToken",response.Credentials.SecurityToken },
{"Expiration", DateTime.Parse(response.Credentials.Expiration).ToString("yyyy-MM-dd HH:mm:ss") },
{"BucketName",_ossOption.BucketName },
{"Region",$"oss-cn-{_ossOption.Region }"},
{"Domain",_ossOption.Domain }
};
var seconds = (expiration - DateTime.Now).TotalSeconds - 300;
await _redis.SetAsync("QYZH.InteractiveMagazine.STS", result, TimeSpan.FromSeconds(seconds));
return result;
}
return JsonSerializer.Deserialize<Dictionary<string, string>>(stsJson);
}
/// <summary>
/// 上传单个
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
public string PutObject(string key, Stream content)
{
//try
//{
var result = _ossClient.PutObject(_ossOption.BucketName, key, content);
if (result.HttpStatusCode == System.Net.HttpStatusCode.OK)
{
return key;
}
else return null;
//}
//catch (Exception ex)
//{
// return null;
//}
}
/// <summary>
/// 删除单个
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
public bool DeleteObject(string key)
{
try
{
if (key.IsNull()) return false;
key = HttpUtility.UrlDecode(key);
//_ossResourceRepository.UpdateDiy.Set(s => s.Status, -1).Where(w => w.Path == key).ExecuteAffrows();
var result = _ossClient.DeleteObject(_ossOption.BucketName, key);
return result.HttpStatusCode == System.Net.HttpStatusCode.OK;
}
catch (OssException ex)
{
_logger.LogError(ex.Message + ex.StackTrace);
return false;
}
}
/// <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>
/// <param name="keys"></param>
/// <returns></returns>
public bool DeleteObjects(List<string>? keys)
{
try
{
if (keys.IsNull()) return false;
// 设置为详细模式,返回所有删除的文件列表。
var quietMode = false;
keys = keys.Select(it => it.RemoveDomain()).ToList();
//_ossResourceRepository.UpdateDiy.Set(s => s.Status, -1).Where(w => keys.Contains(w.Path)).ExecuteAffrows();
var request = new DeleteObjectsRequest(_ossOption.BucketName, keys, quietMode);
// 删除多个文件。
var result = _ossClient.DeleteObjects(request);
//var del = _ossResourceRepository.Delete(w => keys.Contains(w.Path));
//BusinessException.ThrowIf(!del, "删除资源失败");
return result.Keys.Count() == keys.Count;
}
catch (OssException ex)
{
_logger.LogError(ex.Message + ex.StackTrace);
return false;
}
}
public List<string> ListObjects(string prefix)
{
try
{
var keys = new List<string>();
ObjectListing result = null;
string nextMarker = string.Empty;
do
{
var listObjectsRequest = new ListObjectsRequest(_ossOption.BucketName)
{
Marker = nextMarker,
MaxKeys = 100,
Prefix = prefix,
};
result = _ossClient.ListObjects(listObjectsRequest);
foreach (var summary in result.ObjectSummaries)
{
keys.Add(summary.Key);
}
nextMarker = result.NextMarker;
} while (result.IsTruncated);
return keys;
}
catch (OssException ex)
{
_logger.LogError(ex.Message + ex.StackTrace);
return null;
}
}
/// <summary>
/// 复制文件
/// </summary>
/// <param name="sourceObject"></param>
/// <param name="targetObject"></param>
/// <returns></returns>
public bool CopyObject(string sourceObject, string targetObject, long? id = null)
{
try
{
//var metadata = new ObjectMetadata();
// 设置自定义元数据。自定义元数据为键值对Key-Value的形式例如Key为mk1Value为mv1。
//metadata.AddHeader("mk1", "mv1");
//metadata.AddHeader("mk2", "mv2");
sourceObject = sourceObject.RemoveDomain();
sourceObject = Uri.UnescapeDataString(sourceObject);
//var uow = _unitOfWorkManager.Begin();
//var data = _ossResourceRepository.Insert(new OssResource
//{
// MainId = id,
// SoucePath = sourceObject,
// Path = targetObject,
//});
//BusinessException.ThrowIf(data.IsNull(), "资源添加失败");
var req = new CopyObjectRequest(_ossOption.BucketName, sourceObject, _ossOption.BucketName, targetObject)
{
// 如果NewObjectMetadata为null则为COPY模式即拷贝源文件的元数据非null则为REPLACE模式覆盖源文件的元数据
NewObjectMetadata = null
};
var result = _ossClient.CopyObject(req);
return result.HttpStatusCode == System.Net.HttpStatusCode.OK;
}
catch (OssException ex)
{
_logger.LogError(ex.Message + ex.StackTrace);
return false;
}
}
}
}

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\QYZH.InteractiveMagazine.Common\QYZH.InteractiveMagazine.Common.csproj" />
@ -7,8 +7,14 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="AlibabaCloud.SDK.Dysmsapi20170525" Version="4.0.0" />
<PackageReference Include="AlibabaCloud.SDK.Imm20200930" Version="4.6.4" />
<PackageReference Include="AlibabaCloud.SDK.Vod20170321" Version="3.8.2" />
<PackageReference Include="aliyun-net-sdk-sts" Version="3.1.2" />
<PackageReference Include="Aliyun.OSS.SDK.NetCore" Version="2.14.1" />
<PackageReference Include="Autofac" Version="9.1.0" />
<PackageReference Include="Autofac.Extensions.DependencyInjection" Version="11.0.0" />
<PackageReference Include="CSRedisCore" Version="3.8.807" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.0" />
<PackageReference Include="RabbitMQ.Client" Version="7.2.1" />
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />

View File

@ -0,0 +1,14 @@
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
namespace QYZH.InteractiveMagazine.Infrastructure.RabbitMQ
{
public interface IRabbitMQService
{
Task<bool> SendAsync(RabbitMQSendParam param, CancellationToken cancellationToken = default);
Task<bool> SendBatchAsync(IEnumerable<RabbitMQSendParam> @params, CancellationToken cancellationToken = default);
Task ReceiveAsync(string exchange, string queueName, string routingKey, Func<IChannel, BasicDeliverEventArgs, Task> callback, CancellationToken cancellationToken = default);
}
}

View File

@ -0,0 +1,62 @@
using RabbitMQ.Client;
namespace QYZH.InteractiveMagazine.Infrastructure.RabbitMQ
{
public interface IRabbitMQConnection : IDisposable
{
Task<IChannel> CreateChannel();
}
public class RabbitMQConnection : IRabbitMQConnection
{
private readonly ConnectionFactory _factory;
private readonly IConnection _connection;
private bool _isDisposed;
public RabbitMQConnection(ConnectionFactory factory)
{
_factory = factory ?? throw new ArgumentNullException(nameof(factory));
_connection = factory.CreateConnectionAsync().Result;
}
public async Task<IChannel> CreateChannel()
{
EnsureNotDisposed();
return await _connection.CreateChannelAsync();
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (_isDisposed) return;
if (disposing)
{
// Free any other managed objects here.
}
// Free any unmanaged objects here.
_connection.Dispose();
_isDisposed = true;
}
~RabbitMQConnection()
{
Dispose(false);
}
private void EnsureNotDisposed()
{
if (_isDisposed)
{
throw new ObjectDisposedException(nameof(RabbitMQConnection));
}
}
}
}

View File

@ -0,0 +1,13 @@
namespace QYZH.InteractiveMagazine.Infrastructure.RabbitMQ
{
public class RabbitMQOptions
{
public string UserName { get; set; }
public string Password { get; set; }
public string HostName { get; set; }
public int Port { get; set; }
public string ExchangeName { get; set; } = string.Empty;
public string VirtualHost { get; set; } = "/";
}
}

View File

@ -0,0 +1,25 @@
namespace QYZH.InteractiveMagazine.Infrastructure.RabbitMQ
{
public class RabbitMQSendParam
{
/// <summary>
/// 交换机 默认空
/// </summary>
public string Exchange { get; set; } = "";
public string Queue { get; set; }
/// <summary>
/// 路由键
/// </summary>
public string RoutingKey { get; set; }
/// <summary>
/// 消息数据
/// </summary>
public object Data { get; set; }
/// <summary>
/// 是否清空队列
/// </summary>
public bool Purge { get; set; } = false;
}
}

View File

@ -0,0 +1,178 @@
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;
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, IConfiguration configuration, ILogger<RabbitMQService> logger)
{
_connection = connection ?? throw new ArgumentNullException(nameof(connection));
_configuration = configuration;
_logger = logger;
}
public async Task<bool> SendAsync(RabbitMQSendParam param, CancellationToken cancellationToken = default)
{
try
{
using var channel = await _connection.CreateChannel();
// 声明 Exchange持久化
await channel.ExchangeDeclareAsync(exchange: param.Exchange, type: "direct", durable: true, 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.Queue);
// 消息序列化
var mesjson = JsonSerializer.Serialize(param.Data, options);
var body = Encoding.UTF8.GetBytes(mesjson);
var properties = new BasicProperties
{
Persistent = true // 设置消息持久化
};
await channel.BasicPublishAsync(param.Exchange, param.RoutingKey, false, properties, body, cancellationToken);
return true;
}
catch (OperationCanceledException ex)
{
_logger.LogWarning(ex, "RabbitMQ消息发送已取消Exchange: {Exchange}, Queue: {Queue}, RoutingKey: {RoutingKey}",
param.Exchange, param.Queue, param.RoutingKey);
return false;
}
catch (Exception ex)
{
_logger.LogError(ex, "RabbitMQ消息发送失败Exchange: {Exchange}, Queue: {Queue}, RoutingKey: {RoutingKey}",
param.Exchange, param.Queue, param.RoutingKey);
return false;
}
}
public async Task<bool> SendBatchAsync(IEnumerable<RabbitMQSendParam> @params, CancellationToken cancellationToken = default)
{
IChannel channel = null;
try
{
channel = await _connection.CreateChannel();
// 开启事务
await channel.TxSelectAsync();
var properties = new BasicProperties
{
Persistent = true // 设置消息持久化
};
// 批量发送消息到不同的 routingKey
var declaredExchanges = new HashSet<string>();
foreach (var param in @params)
{
// 声明 Exchange持久化
if (declaredExchanges.Add(param.Exchange))
{
await channel.ExchangeDeclareAsync(exchange: param.Exchange, type: "direct", durable: true, 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.Queue);
// 消息序列化
var mesjson = JsonSerializer.Serialize(param.Data, options);
var body = Encoding.UTF8.GetBytes(mesjson);
// 发布消息
await channel.BasicPublishAsync(param.Exchange, param.RoutingKey, false, properties, body, cancellationToken);
}
// 提交事务 - 确保所有消息都发送成功
await channel.TxCommitAsync();
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "RabbitMQ批量消息发送失败");
// 回滚事务
try { await channel?.TxRollbackAsync(); } catch { /* 忽略回滚异常 */ }
return false;
}
finally
{
if (channel != null && !channel.IsClosed)
{
await channel.CloseAsync();
}
}
}
public async Task ReceiveAsync(string exchange, string queueName, string routingKey, Func<IChannel, BasicDeliverEventArgs, Task> callback, CancellationToken cancellationToken = default)
{
var channel = await _connection.CreateChannel();
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) =>
{
//var body = ea.Body.ToArray();
try
{
// 直接传递 model 和 body 给 callback不需要转换
await callback(channel, ea);
}
finally
{
//await channel.BasicAckAsync(ea.DeliveryTag, false, cancellationToken);
}
};
await channel.BasicConsumeAsync(queue: queueName, autoAck: false, consumer: consumer, cancellationToken: cancellationToken);
// Prevent the method from returning immediately
await Task.Delay(-1, cancellationToken);
}
}
}

View File

@ -0,0 +1,56 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using RabbitMQ.Client;
namespace QYZH.InteractiveMagazine.Infrastructure.RabbitMQ
{
public static class RabbiteMQExtensions
{
/// <summary>
/// 初始化消息队列并添加Publisher到IoC容器
/// </summary>
/// <remarks>从Configuration读取"RabbbitMQOptions配置项"</remarks>
public static IServiceCollection AddRabbitMQ(this IServiceCollection services, IConfiguration configuration)
{
var rabbitMqSection = configuration.GetSection("RabbitMq");
if (rabbitMqSection.Exists())
{
// 绑定RabbitMQ配置
services.Configure<RabbitMQOptions>(rabbitMqSection);
// 注册RabbitMQ连接工厂
services.AddSingleton<IRabbitMQConnection, RabbitMQConnection>(sp =>
{
var options = sp.GetRequiredService<IOptions<RabbitMQOptions>>().Value;
var factory = new ConnectionFactory()
{
HostName = options.HostName,
Port = options.Port,
UserName = options.UserName,
Password = options.Password,
VirtualHost = options.VirtualHost,
// 自动恢复配置
AutomaticRecoveryEnabled = true, // 启用自动恢复
NetworkRecoveryInterval = TimeSpan.FromSeconds(10), // 每10秒尝试重连
// 心跳检测
RequestedHeartbeat = TimeSpan.FromSeconds(10), // 60秒心跳
// 其他重要配置
TopologyRecoveryEnabled = true, // 恢复交换机、队列等拓扑结构
RequestedConnectionTimeout = TimeSpan.FromSeconds(30), // 连接超时
SocketReadTimeout = TimeSpan.FromSeconds(30), // 读取超时
SocketWriteTimeout = TimeSpan.FromSeconds(30) // 写入超时
};
return new RabbitMQConnection(factory);
});
// 添加RabbitMQService的服务注册
services.AddSingleton<IRabbitMQService, RabbitMQService>();
//services.AddHostedService<TerminalReportService>();
}
return services;
}
}
}

View File

@ -0,0 +1,106 @@
using CSRedis;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace QYZH.InteractiveMagazine.Infrastructure.Redis
{
public static class RedisExtensions
{
public static TimeSpan expireSecond = TimeSpan.FromMinutes(20);
/// <summary>
/// AddCSRedisCacheExtension
/// </summary>
/// <param name="services"></param>
/// <param name="setupAction"></param>
/// <returns></returns>
public static IServiceCollection AddCSRedisCacheExtension(this IServiceCollection services, IConfiguration configuration)
{
if (services == null)
throw new ArgumentNullException("services");
if (configuration == null)
throw new ArgumentNullException("configuration");
var redisOptions = configuration.Get<RedisCacheOption>();
services.AddSingleton(redisOptions);
var csredis = new CSRedisClient(redisOptions.ConnectionString, redisOptions.Sentinels, redisOptions.ReadOnly);
RedisHelper.Initialization(csredis);
services.AddSingleton(csredis);//csredis注册为单例
//services.AddSingleton<IDistributedCache>(new Microsoft.Extensions.Caching.Redis.CSRedisCache(RedisHelper.Instance));
return services;
}
/// <summary>
/// 获取缓存,如果换成不存在则执行方法生成缓存
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="key"></param>
/// <param name="factory"></param>
/// <param name="validFor"></param>
/// <returns></returns>
public static T GetOrSet<T>(this CSRedisClient redisCache, string key, Func<T> factory, TimeSpan? expireTime = null) where T : class
{
var result = redisCache.Get<T>(key);
if (factory != null && result == null)
{
result = factory();
if (expireTime == null || !expireTime.HasValue)
expireTime = expireSecond;
redisCache.Set(key, result, expireTime.Value);
}
return result;
}
/// <summary>
/// 获取缓存,如果缓存不存在则执行方法生成缓存
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="key"></param>
/// <param name="factory"></param>
/// <param name="validFor"></param>
/// <returns></returns>
public static async Task<T> GetOrSetAsync<T>(this CSRedisClient redisCache, string key, Func<T> factory, TimeSpan? expireTime = null)
{
var result = await redisCache.GetAsync<T>(key);
if (result == null && factory != null)
{
result = factory();
if (expireTime == null || !expireTime.HasValue)
expireTime = expireSecond;
if (result != null)
await redisCache.SetAsync(key, result, expireTime.Value);
}
return result;
}
}
public class RedisCacheOption
{
/// <summary>
/// 连接字符串
/// </summary>
public string ConnectionString { get; set; }
/// <summary>
/// 哨兵设置
/// </summary>
public string[] Sentinels { get; set; }
/// <summary>
/// 是否只读
/// </summary>
public bool ReadOnly { get; set; } = false;
public int[] ExpireSecondRange { get; set; }
public int? ExpireSecond { get; set; } = 3600;
}
}

View File

@ -0,0 +1,25 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using QYZH.InteractiveMagazine.Common.Helpers;
using QYZH.InteractiveMagazine.Infrastructure.OSS;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace QYZH.InteractiveMagazine.Infrastructure.SDK
{
public static class SDKExtensions
{
public static void AddSDKService(this IServiceCollection services, IConfiguration configuration)
{
services.Configure<OSSOptions>(configuration.GetSection(OSSOptions.Section));
DomainHelper.OssDomain = configuration.GetSection(OSSOptions.Section).Get<OSSOptions>()!.Domain;
services.AddSingleton<OssService>();
services.AddSingleton<OssIMMService>();
}
}
}

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>
@ -43,4 +62,34 @@ public class BusinessException : Exception
public BusinessException(string message, Exception innerException) : base(message, innerException)
{
}
public static void ThrowIf(bool isTrue, string message)
{
if (isTrue) throw new BusinessException(message);
}
public static void ThrowIf(bool isTrue, Func<string> action)
{
if (isTrue) throw new BusinessException(action.Invoke());
}
public static void ThrowIf(bool isTrue, string message, Action action)
{
if (isTrue)
{
action();
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

@ -13,7 +13,7 @@ public class BaseResponse
/// </summary>
public BaseResponse()
{
Code = ResultCode.SUCCESS;
code = ResultCode.SUCCESS;
}
#region
@ -21,12 +21,12 @@ public class BaseResponse
/// <summary>
/// 操作描述
/// </summary>
public string Message { get; set; }
public string message { get; set; }
/// <summary>
/// 结果Code
/// </summary>
public ResultCode Code { get; set; }
public ResultCode code { get; set; }
#endregion
@ -37,7 +37,7 @@ public class BaseResponse
/// </summary>
public static BaseResponse Success()
{
return new BaseResponse { Code = ResultCode.SUCCESS, Message = "操作成功。" };
return new BaseResponse { code = ResultCode.SUCCESS, message = "操作成功。" };
}
/// <summary>
@ -45,7 +45,7 @@ public class BaseResponse
/// </summary>
public static BaseResponse Success(string message = "操作成功。")
{
return new BaseResponse { Code = ResultCode.SUCCESS, Message = message };
return new BaseResponse { code = ResultCode.SUCCESS, message = message };
}
/// <summary>
@ -53,7 +53,7 @@ public class BaseResponse
/// </summary>
public static BaseResponse Fail(string message, ResultCode code = ResultCode.FAIL)
{
return new BaseResponse { Code = code, Message = message };
return new BaseResponse { code = code, message = message };
}
/// <summary>
@ -61,7 +61,7 @@ public class BaseResponse
/// </summary>
public static BaseResponse AuthFail(string message, ResultCode code = ResultCode.FAIL)
{
return new BaseResponse { Code = code, Message = message };
return new BaseResponse { code = code, message = message };
}
#endregion
@ -69,7 +69,7 @@ public class BaseResponse
/// <summary>
/// 操作结果
/// </summary>
public bool IsSuccess => Code == ResultCode.SUCCESS;
public bool isSuccess => code == ResultCode.SUCCESS;
}
/// <summary>
@ -82,7 +82,7 @@ public class BaseResponse<T> : BaseResponse
/// <summary>
/// 操作结果
/// </summary>
public T? Result { get; set; }
public T? result { get; set; }
#region
@ -91,7 +91,7 @@ public class BaseResponse<T> : BaseResponse
/// </summary>
public static BaseResponse<T> Success(T result)
{
return new BaseResponse<T> { Code = ResultCode.SUCCESS, Message = "", Result = result };
return new BaseResponse<T> { code = ResultCode.SUCCESS, message = "", result = result };
}
/// <summary>
@ -99,7 +99,7 @@ public class BaseResponse<T> : BaseResponse
/// </summary>
public static BaseResponse<T> Success(T result, string message = "操作成功。")
{
return new BaseResponse<T> { Code = ResultCode.SUCCESS, Message = message, Result = result };
return new BaseResponse<T> { code = ResultCode.SUCCESS, message = message, result = result };
}
/// <summary>
@ -107,7 +107,7 @@ public class BaseResponse<T> : BaseResponse
/// </summary>
public static BaseResponse<T> Fail(string message = "fail")
{
return new BaseResponse<T> { Code = ResultCode.FAIL, Message = message };
return new BaseResponse<T> { code = ResultCode.FAIL, message = message };
}
/// <summary>
@ -115,12 +115,12 @@ public class BaseResponse<T> : BaseResponse
/// </summary>
public static BaseResponse<T> Fail(ResultCode code, string message = "fail")
{
return new BaseResponse<T> { Code = code, Message = message };
return new BaseResponse<T> { code = code, message = message };
}
public static BaseResponse<T> Fail(T result, string message = "操作失败,请稍后再试")
{
return new BaseResponse<T> { Code = ResultCode.FAIL, Message = message, Result = result };
return new BaseResponse<T> { code = ResultCode.FAIL, message = message, result = result };
}
/// <summary>
@ -128,7 +128,7 @@ public class BaseResponse<T> : BaseResponse
/// </summary>
public static BaseResponse<T> AuthFail(string message = "Permission authentication failed")
{
return new BaseResponse<T> { Code = ResultCode.OAUTH_FAIL, Message = message };
return new BaseResponse<T> { code = ResultCode.OAUTH_FAIL, message = message };
}
///// <summary>
@ -149,7 +149,7 @@ public class BaseResponse<T> : BaseResponse
/// <summary>
/// 操作结果
/// </summary>
public bool IsSuccess => Code == ResultCode.SUCCESS;
public bool isSuccess => code == ResultCode.SUCCESS;
}
@ -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

@ -0,0 +1,96 @@
using QYZH.InteractiveMagazine.Models.Enum;
namespace QYZH.InteractiveMagazine.Models.Dto.CheckIn;
/// <summary>
/// 签到配置创建/更新输入
/// </summary>
public class CheckInConfigInput
{
/// <summary>
/// 连续签到天数
/// </summary>
public int DayNumber { get; set; }
/// <summary>
/// 奖励积分数
/// </summary>
public int RewardPoints { get; set; }
/// <summary>
/// 额外奖励积分
/// </summary>
public int BonusPoints { get; set; }
/// <summary>
/// 配置类型: Daily, Streak
/// </summary>
public CheckInConfigTypeEnum Type { get; set; } = CheckInConfigTypeEnum.Daily;
}
/// <summary>
/// 签到配置输出
/// </summary>
public class CheckInConfigOutput
{
/// <summary>
/// 主键ID
/// </summary>
public long Id { get; set; }
/// <summary>
/// 连续签到天数
/// </summary>
public int DayNumber { get; set; }
/// <summary>
/// 奖励积分数
/// </summary>
public int RewardPoints { get; set; }
/// <summary>
/// 额外奖励积分
/// </summary>
public int BonusPoints { get; set; }
/// <summary>
/// 配置类型: Daily, Streak
/// </summary>
public CheckInConfigTypeEnum Type { 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 CheckInConfigQueryInput : PageQueryModel
{
/// <summary>
/// 配置类型筛选: Daily, Streak
/// </summary>
public CheckInConfigTypeEnum? Type { get; set; }
}

View File

@ -94,6 +94,17 @@ public class CheckInInfoOutput
public List<CheckInRecordOutput> RecentRecords { get; set; } = [];
}
/// <summary>
/// 补签输入
/// </summary>
public class MakeUpCheckInInput
{
/// <summary>
/// 补签目标日期yyyy-MM-dd
/// </summary>
public DateTime TargetDate { get; set; }
}
/// <summary>
/// 签到记录输出
/// </summary>
@ -108,6 +119,10 @@ public class CheckInRecordOutput
/// 签到日期
/// </summary>
public DateTime CheckInDate { get; set; }
/// <summary>
/// 签到时间
/// </summary>
public DateTime CreatedAt { get; set; }
/// <summary>
/// 连续签到天数

View File

@ -75,6 +75,11 @@ public class CompensationTaskOutput
/// </summary>
public class GetCompensationTasksInput
{
/// <summary>
/// 按用户Id筛选
/// </summary>
public long? UserId { get; set; }
/// <summary>
/// 按状态筛选
/// </summary>

View File

@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace QYZH.InteractiveMagazine.Models.Dto.DotMatrix
{
public class DotMatrixNoteJournalReportInput
{
public long Id { get; set; }
public bool Success { get; set; }
public string FileKey { get; set; }
public string FileUrl { get; set; }
}
}

View File

@ -0,0 +1,73 @@

using QYZH.InteractiveMagazine.Common.Helpers;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace QYZH.InteractiveMagazine.Models.Dto.DotMatrix
{
public class DotMatrixOutput
{
/// <summary>
/// 方法
/// </summary>
public string Method { get; set; }
/// <summary>
/// 名称
/// </summary>
public string Name { get; set; }
/// <summary>
/// id
/// </summary>
public long NoteId { get; set; }
private string _pdfUrl;
/// <summary>
/// pdf地址
/// </summary>
public string PdfUrl
{
get => DomainHelper.OssFullUrl(_pdfUrl);
set => _pdfUrl = value;
}
public List<DotMatrixMqOutput> DotMatrixs { get; set; } = new List<DotMatrixMqOutput>();
}
public class DotMatrixMqOutput
{
public long DotId { get; set; }
private string _fileAddress;
/// <summary>
/// 点阵xml地址
/// </summary>
public string FileAddress
{
get => DomainHelper.OssFullUrl(_fileAddress);
set => _fileAddress = value;
}
/// <summary>
/// 页
/// </summary>
public List<DotMatrixPageDto> Pages { get; set; } = new List<DotMatrixPageDto>();
}
public class DotMatrixPageDto
{
/// <summary>
/// 页码
/// </summary>
public string PageNo { get; set; }
/// <summary>
/// 连续数量
/// </summary>
public int PageNum { get; set; }
}
}

View File

@ -0,0 +1,280 @@
using QYZH.InteractiveMagazine.Models.Enum;
using System.Text.Json.Serialization;
namespace QYZH.InteractiveMagazine.Models.Dto;
/// <summary>
/// 绑定期刊输入DTO
/// </summary>
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>
/// 绑定期刊输出DTO
/// </summary>
public class BindJournalOutput
{
/// <summary>
/// 绑定记录Id
/// </summary>
public long Id { get; set; }
/// <summary>
/// 用户Id
/// </summary>
public long UserId { get; set; }
/// <summary>
/// 期刊模板Id
/// </summary>
public long JournalId { get; set; }
/// <summary>
/// 关联类型
/// </summary>
public string Type { get; set; } = string.Empty;
/// <summary>
/// 状态
/// </summary>
public string Status { get; set; } = string.Empty;
/// <summary>
/// 绑定时间
/// </summary>
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>
public class UserJournalQueryInput : PageQueryModel
{
/// <summary>
/// 期刊模板Id
/// </summary>
public long? JournalId { get; set; }
/// <summary>
/// 实例化期刊Id
/// </summary>
public long Id { get; set; }
/// <summary>
/// 关联类型: Read, Favorite, Subscribe
/// </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

@ -0,0 +1,112 @@
using System;
using System.Linq;
using System.Text.Json.Serialization;
namespace QYZH.InteractiveMagazine.Models.Dto.Journal
{
public class JournalImportDto
{
/// <summary>
/// 书Id
/// </summary>
public long JournalId { get; set; }
/// <summary>
/// pdf地址
/// </summary>
public string PdfUrl { get; set; }
/// <summary>
/// 从多少页开始
/// </summary>
public int Index { get; set; }
public List<JournalCatalogAddInput> JournalCatalogs { get; set; }
}
/// <summary>
/// 目录导入
/// </summary>
public class JournalCatalogImportDto
{
/// <summary>
/// 一级目录
/// </summary>
public string ParentName { get; set; }
/// <summary>
/// 二级目录
/// </summary>
public string Name { get; set; }
/// <summary>
/// 页码
/// </summary>
public int PageNum { get; set; }
[JsonIgnore]
public long PagePageId { get; set; }
}
public class JournalCatalogAddInput
{
/// <summary>
/// 目录名称
/// </summary>
public string Name { get; set; }
/// <summary>
/// 级别
/// </summary>
public int Level { get; set; }
/// <summary>
/// 类型0-目录 1-页
/// </summary>
public int Type { get; set; }
/// <summary>
/// url
/// </summary>
public string Url { get; set; }
}
public class JournalCatalogInput
{
/// <summary>
/// 书Id
/// </summary>
public long JournalId { get; set; }
/// <summary>
/// 目录名称
/// </summary>
public string Name { get; set; }
/// <summary>
/// 父Id;无限级别
/// </summary>
public long ParentId { get; set; }
/// <summary>
/// 等级
/// </summary>
public int Level { get; set; }
/// <summary>
/// 指定插入位置的索引从0开始不传或传null时默认追加到末尾
/// </summary>
public int? Position { get; set; }
/// <summary>
/// 类型0-目录 1-页
/// </summary>
public int Type { get; set; }
/// <summary>
/// url
/// </summary>
public string Url { get; set; }
}
}

View File

@ -0,0 +1,71 @@
using MiniExcelLibs.Attributes;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace QYZH.InteractiveMagazine.Models.Dto.Journal
{
public partial class JournalCatalogTreeListDto
{
/// <summary>
/// Desc:主键id
/// </summary>
[ExcelIgnore]
public long Id { get; set; }
/// <summary>
/// Desc:书id
/// </summary>
[ExcelIgnore]
public long JournalId { get; set; }
/// <summary>
/// Desc:一级目录名称
/// </summary>
[DisplayName("目录名称")]
[Required]
public string? ParentName { get; set; }
/// <summary>
/// Desc:目录名称
/// </summary>
[DisplayName("二级目录")]
[Required]
public string Name { get; set; }
[ExcelIgnore]
public long PageId { get; set; }
[DisplayName("页码")]
[Required]
public int PageNum { get; set; }
[ExcelIgnore]
public long? ParentId { get; set; }
}
public partial class IcrJournalCatalogTreeDto
{
/// <summary>
/// Desc:主键id
/// </summary>
public long Id { get; set; }
/// <summary>
/// 名称
/// </summary>
public string Name { get; set; }
/// <summary>
/// 层级
/// </summary>
public int Level { get; set; }
public List<IcrJournalCatalogTreeDto> Child { get; set; }
}
}

View File

@ -0,0 +1,15 @@
using System;
using System.Linq;
namespace QYZH.InteractiveMagazine.Models.Dto.Journal
{
public class JournalCatalogUpdateInput
{
public long Id { get; set; }
/// <summary>
/// 目录名称
/// </summary>
public string Name { get; set; }
}
}

View File

@ -1,81 +1,154 @@
using QYZH.InteractiveMagazine.Common.Extensions;
using QYZH.InteractiveMagazine.Common.Helpers;
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;
/// <summary>
/// 绑定期刊输入DTO
/// </summary>
public class BindJournalInput
namespace QYZH.InteractiveMagazine.Models.Dto.Journal
{
/// <summary>
/// 期刊模板Id扫码解析的期刊定义Id
/// </summary>
public long JournalId { get; set; }
public class JournalDto
{
/// <summary>
/// Desc:主键id
/// </summary>
public long Id { get; set; }
/// <summary>
/// 实例化期刊Id扫码解析的具体期刊实例Id可选
/// </summary>
public long Id { get; set; }
/// <summary>
/// Desc:书籍名称
/// </summary>
public string Name { get; set; }
/// <summary>
/// 关联类型: Read(已读), Favorite(收藏), Subscribe(订阅),默认 Subscribe
/// </summary>
public string Type { get; set; } = UserJournalTypeEnum.Subscribe.ToString();
}
/// <summary>
/// 绑定期刊输出DTO
/// </summary>
public class BindJournalOutput
{
/// <summary>
/// 绑定记录Id
/// </summary>
public long Id { get; set; }
/// <summary>
/// 用户Id
/// </summary>
public long UserId { get; set; }
/// <summary>
/// 期刊模板Id
/// </summary>
public long JournalId { get; set; }
/// <summary>
/// 关联类型
/// </summary>
public string Type { get; set; } = string.Empty;
/// <summary>
/// 状态
/// </summary>
public string Status { get; set; } = string.Empty;
/// <summary>
/// 绑定时间
/// </summary>
public DateTime CreatedAt { get; set; }
}
/// <summary>
/// 用户期刊关联查询输入DTO
/// </summary>
public class UserJournalQueryInput : PageQueryModel
{
/// <summary>
/// 期刊模板Id
/// </summary>
public long? JournalId { get; set; }
/// <summary>
/// 实例化期刊Id
/// </summary>
public long Id { get; set; }
/// <summary>
/// 关联类型: Read, Favorite, Subscribe
/// </summary>
public string? Type { 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:状态
/// </summary>
public JournalStatusEnum Status { get; set; }
/// <summary>
/// 状态
/// </summary>
public string ShowStatus => Status.GetDescription();
private string _pdfUrl;
/// <summary>
/// pdf预览地址
/// </summary>
public string PdfUrl
{
get => DomainHelper.OssFullUrl(_pdfUrl);
set => _pdfUrl = value;
}
/// <summary>
/// Desc:宽度
/// </summary>
public float Width { get; set; }
/// <summary>
/// Desc:高度
/// </summary>
public float Height { get; set; }
/// <summary>
/// Desc:乐观锁
/// </summary>
public int Revision { get; set; }
/// <summary>
/// Desc:审查结果(默认0 1通过 -1驳回)
/// </summary>
public int Result { get; set; }
private string _cover;
/// <summary>
/// 封面
/// </summary>
public string Cover
{
get => DomainHelper.OssFullUrl(_cover);
set => _cover = value;
}
private string _backCover;
/// <summary>
/// 封底
/// </summary>
public string BackCover
{
get => DomainHelper.OssFullUrl(_backCover);
set => _backCover = value;
}
private string _pdfPreviewUrl;
/// <summary>
/// pdf预览地址
/// </summary>
public string PdfPreviewUrl
{
get => DomainHelper.OssFullUrl(_pdfPreviewUrl);
set => _pdfPreviewUrl = value;
}
/// <summary>
/// Desc:
/// </summary>
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

@ -0,0 +1,96 @@
using QYZH.InteractiveMagazine.Models.Enum;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace QYZH.InteractiveMagazine.Models.Dto.Journal
{
public partial class JournalAddDto
{
/// <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 JournalStatusEnum Status { get; set; } = JournalStatusEnum.Created;
/// <summary>
/// Desc:pdf地址
/// </summary>
public string PdfUrl { get; set; }
/// <summary>
/// Desc:宽度
/// </summary>
public float Width { get; set; }
/// <summary>
/// Desc:高度
/// </summary>
public float Height { get; set; }
/// <summary>
/// Desc:乐观锁
/// </summary>
[JsonIgnore]
public int Revision { get; set; }
/// <summary>
/// Desc:封面
/// </summary>
public string Cover { get; set; }
/// <summary>
/// Desc:封底
/// </summary>
public string BackCover { get; set; }
/// <summary>
/// Desc:pdf预览地址
/// </summary>
public string? PdfPreviewUrl { get; set; }
/// <summary>
/// Desc:发布开始时间
/// </summary>
public DateTime? StartTime { get; set; }
/// <summary>
/// Desc:发布结束时间
/// </summary>
public DateTime? EndTime { get; set; }
}
public partial class JournalEditDto: JournalAddDto
{
/// <summary>
/// Desc:主键id
/// </summary>
public long Id { get; set; }
/// <summary>
/// Desc:状态
/// </summary>
public JournalStatusEnum Status { get; set; }
}
}

View File

@ -0,0 +1,28 @@
using System;
using System.Linq;
namespace QYZH.InteractiveMagazine.Models.Dto.Journal
{
public class JournalAddV2Input
{
/// <summary>
/// 书Id
/// </summary>
public long JournalId { get; set; }
/// <summary>
/// 书目录id
/// </summary>
public long JournalCatalogId { get; set; }
/// <summary>
/// 页码
/// </summary>
public int PageNum { get; set; }
/// <summary>
/// 页图片
/// </summary>
public string Url { 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

@ -0,0 +1,38 @@
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 JournalPageTaskAddInput
{
/// <summary>
/// 书Id
/// </summary>
public long JournalId { get; set; }
/// <summary>
/// 书页id
/// </summary>
public long JournalPageId { get; set; }
/// <summary>
/// 题号
/// </summary>
public string No { get; set; }
/// <summary>
/// 题型
/// </summary>
public TaskBankTypeEnum Type { get; set; }
/// <summary>
/// 是否需要AI批改默认需要
/// </summary>
public bool NeedAiProcess { get; set; } = true;
}
}

View File

@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace QYZH.InteractiveMagazine.Models.Dto.Journal
{
public class JournalPageTaskKeywordAnalysis
{
/// <summary>
/// 问题Id
/// </summary>
public long Id { get; set; }
/// <summary>
/// 单项解析
/// </summary>
public string KeywordAnalysis { get; set; }
}
}

View File

@ -0,0 +1,114 @@
using QYZH.InteractiveMagazine.Models.Enum;
using QYZH.InteractiveMagazine.Common.Extensions;
using QYZH.InteractiveMagazine.Common.Helpers;
using QYZH.InteractiveMagazine.Models.Enum;
namespace QYZH.InteractiveMagazine.Models.Dto.Journal
{
public class JournalPageTaskOutput
{
public long Id { get; set; }
/// <summary>
/// 书Id
/// </summary>
public long JournalId { get; set; }
/// <summary>
/// 书页Id
/// </summary>
public long JournalPageId { get; set; }
/// <summary>
/// 任务信息
/// </summary>
public string Task { get; set; }
/// <summary>
/// 题号
/// </summary>
public string No { get; set; }
/// <summary>
/// 题型
/// </summary>
public TaskBankTypeEnum Type { get; set; }
public string ShowType => Type.GetDescription();
/// <summary>
/// 答案列表
/// </summary>
public List<JournalTaskAnswerOutput> Answers { get; set; }
/// <summary>
/// 积分
/// </summary>
public float Points { get; set; }
/// <summary>
/// 成长值
/// </summary>
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
{
get { return _answer.AddDomain(DomainHelper.OssDomain); }
set { _answer = value; }
}
private string _answerUrl;
/// <summary>
/// 参考答案图片
/// </summary>
public string AnswerUrl
{
get { return _answerUrl.AddDomain(DomainHelper.OssDomain); }
set { _answerUrl = value; }
}
}
}

View File

@ -0,0 +1,110 @@
using QYZH.InteractiveMagazine.Models.Enum;
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 JournalPageTaskUpdateInput
{
public long Id { get; set; }
/// <summary>
/// 杂志Id
/// </summary>
public long JournalId { get; set; }
/// <summary>
/// 书页id
/// </summary>
public long JournalPageId { get; set; }
/// <summary>
/// 题号
/// </summary>
public string No { get; set; }
/// <summary>
/// 题型
/// </summary>
public TaskBankTypeEnum Type { get; set; }
/// <summary>
/// 问题
/// </summary>
public string Task { get; set; }
/// <summary>
/// 积分
/// </summary>
public int Points { get; set; }
/// <summary>
/// 成长值
/// </summary>
public int 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批改false时由人工批改
/// </summary>
public bool NeedAiProcess { get; set; } = true;
}
public class JournalPageTaskComplementInput
{
public long Id { get; set; }
/// <summary>
/// 任务id
/// </summary>
public long AssignTaskId { get; set; }
/// <summary>
/// 音频地址
/// </summary>
public string AudioUrl { get; set; }
/// <summary>
/// 音频开始时间
/// </summary>
public long AudioStartTime { get; set; }
/// <summary>
/// 音频结束时间
/// </summary>
public long AudioEndTime { get; set; }
/// <summary>
/// 点位数据地址
/// </summary>
public string PointsUrl { get; set; }
}
}

View File

@ -0,0 +1,90 @@
using QYZH.InteractiveMagazine.Models.Enum;
using QYZH.InteractiveMagazine.Common.Extensions;
using QYZH.InteractiveMagazine.Common.Helpers;
using QYZH.InteractiveMagazine.Models.Enum;
namespace QYZH.InteractiveMagazine.Models.Dto.Journal
{
public class JournalPageV2Output
{
public long JournalId { get; set; }
public long JournalCatalogId { get; set; }
public long JournalPageId { get; set; }
public string Layout { get; set; }
/// <summary>
/// 页码
/// </summary>
public int PageNum { get; set; }
private string _url;
/// <summary>
/// 图片
/// </summary>
public string Url
{
get { return DomainHelper.OssFullUrl(_url); }
set { _url = value; }
}
/// <summary>
/// 问题
/// </summary>
public List<JournalPageTaskV2Output>? Tasks { get; set; }
public List<JournalPageOtherOutput>? Areas { get; set; }
}
public class JournalPageTaskV2Output
{
public long Id { get; set; }
public long GroupId { get; set; }
/// <summary>
/// 题号
/// </summary>
public string No { get; set; }
//public long test => long.Parse(No.Replace("-", string.Empty));
/// <summary>
/// 题型
/// </summary>
public TaskBankTypeEnum Type { get; set; }
public string ShowType => Type.GetDescription();
/// <summary>
/// 答案列表
/// </summary>
public List<JournalTaskAnswerOutput> Answers { get; set; }
/// <summary>
/// 是否分配
/// </summary>
public bool Assign { get; set; }
}
public class JournalPageOtherOutput
{
public long Id { get; set; }
/// <summary>
/// 题号
/// </summary>
public string No { get; set; }
/// <summary>
/// 题型
/// </summary>
public TaskBankTypeEnum Type { get; set; }
public string ShowType => Type.GetDescription();
}
}

View File

@ -0,0 +1,22 @@
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 JournalQueryDto
{
/// <summary>
/// 书籍名称
/// </summary>
public string? Name { get; set; }
/// <summary>
/// 状态
/// </summary>
public JournalStatusEnum? Status { get; set; }
}
}

View File

@ -0,0 +1,35 @@
using QYZH.InteractiveMagazine.Models.Enum;
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 JournalTaskOutput
{
public long TaskId { get; set; }
public string TaskNo { get; set; }
public TaskTypeEnum TaskType
{
get
{
var type = new TaskBankType();
if (type.ObjectiveTypes.Contains(TaskSubType))
return TaskTypeEnum.Objective;
else if (type.SubjectiveTypes.Contains(TaskSubType))
return TaskTypeEnum.Subjective;
else
throw new InvalidOperationException($"Unknown task sub type: {TaskSubType}");
}
}
/// <summary>
/// 详细类型
/// </summary>
public TaskBankTypeEnum TaskSubType { 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 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,21 @@
using System;
using System.Linq;
namespace QYZH.InteractiveMagazine.Models.Dto.Journal
{
public class PageLayoutInput
{
public long Id { get; set; }
public string Layout { get; set; }
public List<TasksImages> TasksImages { get; set; }
}
public class TasksImages
{
public long TaskId { get; set; }
public string Url { get; set; }
}
}

View File

@ -36,7 +36,7 @@ public class ProductInput
/// <summary>
/// 售卖状态: OnSale, OffSale
/// </summary>
public string SaleStatus { get; set; } = "OnSale";
public ProductStatusEnum Status { get; set; } = ProductStatusEnum.OnSale;
/// <summary>
/// 扩展数据
@ -52,6 +52,16 @@ public class ProductInput
/// 库存(-1无限)
/// </summary>
public int Stock { get; set; } = -1;
/// <summary>
/// 是否是虚拟商品
/// </summary>
public bool IsVirtual { get; set; }
/// <summary>
/// Type对应的Id
/// </summary>
public long TypeId { get; set; }
}
/// <summary>
@ -90,9 +100,9 @@ public class ProductOutput
public string Type { get; set; } = string.Empty;
/// <summary>
/// 售卖状态
/// 状态
/// </summary>
public string SaleStatus { get; set; } = string.Empty;
public string Status { get; set; } = string.Empty;
/// <summary>
/// 扩展数据
@ -128,6 +138,16 @@ public class ProductOutput
/// 更新时间
/// </summary>
public DateTime? UpdatedAt { get; set; }
/// <summary>
/// 是否是虚拟商品
/// </summary>
public bool IsVirtual { get; set; }
/// <summary>
/// Type对应的Id
/// </summary>
public long TypeId { get; set; }
}
/// <summary>
@ -146,9 +166,9 @@ public class ProductQueryInput : PageQueryModel
public string? Type { get; set; }
/// <summary>
/// 售卖状态: OnSale, OffSale
/// 售卖状态
/// </summary>
public string? SaleStatus { get; set; }
public ProductStatusEnum? Status { get; set; }
/// <summary>
/// 是否上架

View File

@ -12,6 +12,16 @@ public class WxProductOutput
public int Price { get; set; }
public string Type { get; set; } = string.Empty;
/// <summary>
/// 是否是虚拟商品
/// </summary>
public bool IsVirtual { get; set; }
/// <summary>
/// Type对应的Id
/// </summary>
public long TypeId { get; set; }
/// <summary>
/// 皮肤信息(仅 PetBg 类型有值)
/// </summary>

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

@ -30,12 +30,12 @@ public class MedalInput
/// <summary>
/// 勋章的类型: Pet, Community
/// </summary>
public string Type { get; set; } = MedalTypeEnum.Pet.ToString();
public string Type { get; set; } = MedalTypeEnum.System.ToString();
/// <summary>
/// id
/// 期刊id
/// </summary>
public long JournalId { get; set; }
public long? JournalId { get; set; }
/// <summary>
/// 勋章规则列表
@ -81,7 +81,11 @@ public class MedalOutput
/// <summary>
/// 书id
/// </summary>
public long JournalId { get; set; }
public long? JournalId { get; set; }
/// <summary>
/// 勋章状态
/// </summary>
public DefaultStatusEnum Status { get; set; }
/// <summary>
/// 勋章规则列表
@ -364,3 +368,35 @@ public class WxMedalActivateInput
/// </summary>
public long MedalId { get; set; }
}
/// <summary>
/// 勋章规则目标表下拉输出
/// </summary>
public class MedalRuleTableOptionOutput
{
/// <summary>
/// 表名
/// </summary>
public string TableName { get; set; } = string.Empty;
/// <summary>
/// 显示名称
/// </summary>
public string DisplayName { get; set; } = string.Empty;
}
/// <summary>
/// 勋章规则字段下拉输出
/// </summary>
public class MedalRuleFieldOptionOutput
{
/// <summary>
/// 字段名
/// </summary>
public string FieldName { get; set; } = string.Empty;
/// <summary>
/// 显示名称
/// </summary>
public string DisplayName { get; set; } = string.Empty;
}

View File

@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace QYZH.InteractiveMagazine.Models.Base
{
public class MoveInput
{
/// <summary>
/// 要移动节点ID (必须)
/// </summary>
[Required]
public long SourceId { get; set; }
/// <summary>
/// 目标父节点ID (0表示移动到根节点)
/// </summary>
public long TargetParentId { get; set; }
/// <summary>
/// 在目标父节点下的插入位置 (可选从0开始,null表示追加到末尾)
/// </summary>
[Range(0, int.MaxValue)]
public int? Position { 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

@ -36,6 +36,11 @@ public class PetSkinInput
/// 皮肤类型: Normal, Limited, Event
/// </summary>
public string Type { get; set; } = PetSkinTypeEnum.Normal.ToString();
/// <summary>
/// 封面图片地址
/// </summary>
public string? CoverImageUrl { get; set; }
}
/// <summary>
@ -50,6 +55,10 @@ public class PetSkinOutput
public string Rarity { get; set; } = string.Empty;
public int SortOrder { get; set; }
public string Type { get; set; } = string.Empty;
/// <summary>
/// 封面图片地址
/// </summary>
public string? CoverImageUrl { get; set; }
public string? CreatedBy { get; set; }
public DateTime CreatedAt { get; set; }
public string? UpdatedBy { get; set; }
@ -129,3 +138,16 @@ public class PetSkinImageOutput
public string? UpdatedBy { get; set; }
public DateTime? UpdatedAt { get; set; }
}
/// <summary>
/// 皮肤按进化阶段分组的图片输出(每阶段包含阶段信息和图片列表)
/// </summary>
public class SkinEvolutionStageOutput
{
public long EvolutionId { get; set; }
public string StageName { get; set; } = string.Empty;
public int StageLevel { get; set; }
public int RequiredGrowth { get; set; }
public long? PreviousEvolutionId { get; set; }
public List<PetSkinImageOutput> Images { get; set; } = new();
}

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