diff --git a/.trae/documents/企业级框架搭建计划.md b/.trae/documents/企业级框架搭建计划.md new file mode 100644 index 0000000..95daead --- /dev/null +++ b/.trae/documents/企业级框架搭建计划.md @@ -0,0 +1,329 @@ +# QYZH.InteractiveMagazine 企业级框架搭建计划 + +## 一、技术栈 + +| 技术 | 用途 | +|------|------| +| .NET 8 Web API | 后端框架 | +| MySQL | 主数据库 | +| SugarSql (SqlSugar) | ORM 框架 | +| JWT | 身份认证 | +| RabbitMQ | 消息队列 | +| Redis | 缓存 | +| Serilog | 日志记录 | +| 微信小程序 | 前端客户端 | + +--- + +## 二、项目分层结构 + +``` +QYZH.InteractiveMagazine/ +├── QYZH.InteractiveMagazine.WebApi/ # API 控制器、启动配置 +├── QYZH.InteractiveMagazine.IService/ # 服务接口定义 +├── QYZH.InteractiveMagazine.Service/ # 业务逻辑实现 +├── QYZH.InteractiveMagazine.Repository/ # 数据访问层 +├── QYZH.InteractiveMagazine.Models/ # 实体、DTO、枚举、基础模型 +├── QYZH.InteractiveMagazine.Infrastructure/ # 中间件、JWT、缓存、消息队列等 +├── QYZH.InteractiveMagazine.Common/ # 公共工具类、扩展方法 +└── QYZH.InteractiveMagazine.Test/ # 单元测试(可选) +``` + +### 项目引用关系 + +``` +WebApi → IService, Service, Infrastructure, Common, Models +Service → IService, Repository, Common, Models +Repository → Common, Models +Infrastructure → Common, Models +IService → Common, Models +Common → Models +Models → 不依赖任何其他项目 +``` + +--- + +## 三、实施步骤 + +### 阶段一:创建解决方案与项目结构 + +1. 清理现有项目文件,创建新的解决方案文件 `.sln` +2. 创建以下类库项目: + - `QYZH.InteractiveMagazine.Models` - .NET 8 类库 + - `QYZH.InteractiveMagazine.Common` - .NET 8 类库 + - `QYZH.InteractiveMagazine.Repository` - .NET 8 类库 + - `QYZH.InteractiveMagazine.IService` - .NET 8 类库 + - `QYZH.InteractiveMagazine.Service` - .NET 8 类库 + - `QYZH.InteractiveMagazine.Infrastructure` - .NET 8 类库 +3. 将现有的 `QYZH.InteractiveMagazine` 项目重命名为 `QYZH.InteractiveMagazine.WebApi` +4. 设置项目引用关系 + +--- + +### 阶段二:安装 NuGet 依赖包 + +| 项目 | 依赖包 | +|------|--------| +| **Models** | 无外部依赖 | +| **Common** | Newtonsoft.Json, AutoMapper | +| **Repository** | SqlSugar, MySqlConnector, Microsoft.Extensions.Configuration.Abstractions | +| **IService** | 无外部依赖 | +| **Service** | AutoMapper, Microsoft.Extensions.Logging.Abstractions | +| **Infrastructure** | Microsoft.AspNetCore.Authentication.JwtBearer, System.IdentityModel.Tokens.Jwt, StackExchange.Redis, RabbitMQ.Client, Serilog.AspNetCore, SqlSugar | +| **WebApi** | Microsoft.AspNetCore.OpenApi, Swashbuckle.AspNetCore, Serilog.AspNetCore | + +--- + +### 阶段三:构建基础模型层 (Models) + +#### 3.1 创建基础实体类 `BaseEntity.cs`(仅包含字段定义,不包含具体业务实体) + +#### 3.2 创建响应模型 `BaseResponse.cs` +```csharp +public class BaseResponse +{ + public int Code { get; set; } + public string Message { get; set; } + public T Data { get; set; } + + public static BaseResponse Success(T data, string message = "操作成功") + public static BaseResponse Fail(string message, int code = 500) +} +``` + +#### 3.3 创建分页模型 `PageListModel.cs` 和 `PageQueryModel.cs` + +#### 3.4 创建业务异常类 `BusinessException.cs` + +#### 3.5 创建 JWT 配置模型 `JwtSettings.cs` + +#### 3.6 创建 RabbitMQ 配置模型 `RabbitMQSettings.cs` + +#### 3.7 创建 Redis 配置模型 `RedisSettings.cs` + +--- + +### 阶段四:构建公共工具层 (Common) + +#### 4.1 创建雪花 ID 生成器 `SnowflakeIdHelper.cs` + +#### 4.2 创建扩展方法类: +- `ObjectExtension.cs` - 对象拷贝、深拷贝等 +- `DateTimeExtension.cs` - 日期时间扩展(格式转换、时间戳转换、年龄计算等) +- `StringExtension.cs` - 字符串扩展(脱敏、MD5、SHA256、Base64、正则校验等) +- `EnumExtension.cs` - 枚举扩展(描述获取、值转换、名称列表等) + +#### 4.3 创建公共工具类: +- `JsonHelper.cs` - JSON 序列化/反序列化 +- `HttpHelper.cs` - HTTP 请求工具 +- `ValidateHelper.cs` - 参数校验工具 +- `EnumHelper.cs` - 枚举工具类 + +--- + +### 阶段五:构建数据访问层 (Repository) + +#### 5.1 创建 SqlSugar 数据库上下文封装 `SqlSugarDbContext.cs` +- 连接字符串读取 +- 数据库初始化 +- 软删除全局过滤 + +#### 5.2 创建基础仓储接口 `IBaseRepository` +- 增删改查基础方法 +- 分页查询方法 + +#### 5.3 创建基础仓储实现 `BaseRepository` + +--- + +### 阶段六:构建基础设施层 (Infrastructure) + +#### 6.1 JWT 认证模块 +- `JwtHelper.cs` - JWT Token 生成/验证 +- `JwtServiceExtensions.cs` - JWT 服务注册扩展 + +#### 6.2 Redis 缓存模块 +- `RedisHelper.cs` - Redis 操作封装 +- `CacheServiceExtensions.cs` - 缓存服务注册扩展 + +#### 6.3 RabbitMQ 消息队列模块 +- `RabbitMQPublisher.cs` - 消息发布器 +- `RabbitMQConsumer.cs` - 消息消费者 +- `RabbitMQServiceExtensions.cs` - RabbitMQ 服务注册扩展 + +#### 6.4 全局异常处理中间件 +- `GlobalExceptionMiddleware.cs` - 全局异常捕获与统一响应 + +#### 6.5 JWT 认证过滤器 +- `JwtAuthorizationFilter.cs` - 自定义 JWT 授权过滤 + +#### 6.6 操作日志中间件 +- `OperationLogMiddleware.cs` - 请求日志记录 + +#### 6.7 服务注册扩展 +- `DependencyInjectionExtensions.cs` - 统一服务注册入口 + +--- + +### 阶段七:构建服务层 (IService & Service) + +#### 7.1 创建基础服务接口 `IBaseService` +- 通用业务操作方法 + +#### 7.2 创建基础服务实现 `BaseService` +- 事务管理 +- 业务逻辑封装 + +#### 7.3 创建用户服务示例(演示分层调用) +- `IAuthService.cs` - 认证服务接口 +- `AuthService.cs` - 认证服务实现(登录、注册、Token 刷新) + +--- + +### 阶段八:构建 API 层 (WebApi) + +#### 8.1 创建基础控制器 `BaseController.cs` +- 获取当前用户信息 +- 统一响应返回 + +#### 8.2 创建认证控制器 `AuthController.cs` +- `POST api/auth/login` - 用户登录 +- `POST api/auth/register` - 用户注册 +- `POST api/auth/refreshToken` - Token 刷新 + +#### 8.3 创建健康检查控制器 `HealthController.cs` +- `GET api/health/ping` - 健康检查 + +#### 8.4 配置 Program.cs +- Serilog 日志配置 +- 依赖注入配置 +- 中间件管道配置(日志 → 异常处理 → 认证 → 授权 → 路由) +- JWT 认证配置 +- Swagger 配置 +- CORS 配置 +- RabbitMQ 消费者启动 + +#### 8.5 配置 appsettings.json +- 数据库连接字符串 +- JWT 配置 +- Redis 配置 +- RabbitMQ 配置 +- Serilog 日志配置 + +--- + +### 阶段九:微信小程序支持 + +#### 9.1 创建微信小程序专用服务 +- `IWeChatMiniProgramService.cs` - 微信服务接口 +- `WeChatMiniProgramService.cs` - 微信服务实现 +- 微信登录(code 换取 openid/session_key) +- 用户信息解密 + +#### 9.2 创建微信小程序控制器 +- `WeChatController.cs` - 微信相关 API +- `POST api/wechat/login` - 微信登录 + +#### 9.3 微信小程序配置项 +- AppId、AppSecret 配置 + +--- + +### 阶段十:配置与脚本 + +#### 10.1 创建数据库初始化脚本 +- `init_database.sql` - 创建数据库和基础表 + +#### 10.2 创建 Docker 配置(可选) +- `Dockerfile` +- `docker-compose.yml` + +#### 10.3 创建 .gitignore + +#### 10.4 创建 launchSettings.json 配置 + +--- + +## 四、关键设计决策 + +| 决策点 | 选择 | 原因 | +|--------|------|------| +| ORM 框架 | SqlSugar | 功能全面,API 简洁,支持代码优先 | +| 主键策略 | long 雪花 ID | 分布式友好,高性能 | +| 软删除 | IsDeleted 字段 | 数据安全,可恢复 | +| 认证方式 | JWT | 无状态,适合小程序 | +| 日志框架 | Serilog | 结构化日志,易扩展 | +| 响应格式 | BaseResponse | 统一格式,前端易处理 | + +--- + +## 五、最终项目文件树(核心文件) + +``` +QYZH.InteractiveMagazine/ +├── QYZH.InteractiveMagazine.sln +├── QYZH.InteractiveMagazine.WebApi/ +│ ├── Controllers/ +│ │ ├── BaseController.cs +│ │ ├── AuthController.cs +│ │ ├── HealthController.cs +│ │ └── WeChatController.cs +│ ├── Program.cs +│ ├── appsettings.json +│ └── QYZH.InteractiveMagazine.WebApi.csproj +├── QYZH.InteractiveMagazine.IService/ +│ ├── IBaseService.cs +│ ├── IAuthService.cs +│ └── QYZH.InteractiveMagazine.IService.csproj +├── QYZH.InteractiveMagazine.Service/ +│ ├── BaseService.cs +│ ├── AuthService.cs +│ └── QYZH.InteractiveMagazine.Service.csproj +├── QYZH.InteractiveMagazine.Repository/ +│ ├── IBaseRepository.cs +│ ├── BaseRepository.cs +│ ├── SqlSugarDbContext.cs +│ └── QYZH.InteractiveMagazine.Repository.csproj +├── QYZH.InteractiveMagazine.Models/ +│ ├── Entity/ +│ │ └── BaseEntity.cs +│ ├── Dto/ +│ │ ├── BaseResponse.cs +│ │ ├── PageListModel.cs +│ │ └── PageQueryModel.cs +│ ├── Settings/ +│ │ ├── JwtSettings.cs +│ │ ├── RedisSettings.cs +│ │ └── RabbitMQSettings.cs +│ ├── Common/ +│ │ └── BusinessException.cs +│ └── QYZH.InteractiveMagazine.Models.csproj +├── QYZH.InteractiveMagazine.Infrastructure/ +│ ├── Extensions/ +│ │ └── DependencyInjectionExtensions.cs +│ ├── Middleware/ +│ │ ├── GlobalExceptionMiddleware.cs +│ │ └── OperationLogMiddleware.cs +│ ├── Auth/ +│ │ └── JwtHelper.cs +│ ├── Cache/ +│ │ └── RedisHelper.cs +│ ├── MessageQueue/ +│ │ └── RabbitMQPublisher.cs +│ └── QYZH.InteractiveMagazine.Infrastructure.csproj +├── QYZH.InteractiveMagazine.Common/ +│ ├── Extensions/ +│ │ ├── ObjectExtension.cs +│ │ ├── DateTimeExtension.cs +│ │ ├── StringExtension.cs +│ │ └── EnumExtension.cs +│ ├── Helpers/ +│ │ ├── JsonHelper.cs +│ │ ├── HttpHelper.cs +│ │ ├── ValidateHelper.cs +│ │ ├── EnumHelper.cs +│ │ └── SnowflakeIdHelper.cs +│ └── QYZH.InteractiveMagazine.Common.csproj +└── scripts/ + └── init_database.sql +``` diff --git a/QYZH.InteractiveMagazine.Common/Class1.cs b/QYZH.InteractiveMagazine.Common/Class1.cs new file mode 100644 index 0000000..ec7c50c --- /dev/null +++ b/QYZH.InteractiveMagazine.Common/Class1.cs @@ -0,0 +1,6 @@ +namespace QYZH.InteractiveMagazine.Common; + +public class Class1 +{ + +} diff --git a/QYZH.InteractiveMagazine.Common/Extensions/DateTimeExtension.cs b/QYZH.InteractiveMagazine.Common/Extensions/DateTimeExtension.cs new file mode 100644 index 0000000..4973374 --- /dev/null +++ b/QYZH.InteractiveMagazine.Common/Extensions/DateTimeExtension.cs @@ -0,0 +1,88 @@ +using System; + +namespace QYZH.InteractiveMagazine.Common.Extensions +{ + /// + /// 日期时间扩展方法 + /// + public static class DateTimeExtension + { + /// + /// 转换为Unix时间戳(秒) + /// + /// 日期时间 + /// Unix时间戳 + public static long ToTimestamp(this DateTime dateTime) + { + return new DateTimeOffset(dateTime.ToUniversalTime()).ToUnixTimeSeconds(); + } + + /// + /// 从Unix时间戳(秒)转换为DateTime + /// + /// Unix时间戳 + /// 日期时间 + public static DateTime FromTimestamp(long timestamp) + { + return DateTimeOffset.FromUnixTimeSeconds(timestamp).LocalDateTime; + } + + /// + /// 转换为指定格式的日期时间字符串 + /// + /// 日期时间 + /// 格式字符串,默认为"yyyy-MM-dd HH:mm:ss" + /// 格式化的日期时间字符串 + public static string ToDateTimeString(this DateTime dateTime, string format = "yyyy-MM-dd HH:mm:ss") + { + return dateTime.ToString(format); + } + + /// + /// 从生日计算年龄 + /// + /// 生日日期 + /// 年龄 + public static int GetAge(this DateTime birthday) + { + int age = DateTime.Now.Year - birthday.Year; + + if (DateTime.Now.DayOfYear < birthday.DayOfYear) + { + age--; + } + + return age; + } + + /// + /// 判断是否是今天 + /// + /// 日期时间 + /// 是否是今天 + public static bool IsToday(this DateTime dateTime) + { + return dateTime.Date == DateTime.Today; + } + + /// + /// 获取当天开始时间(00:00:00) + /// + /// 日期时间 + /// 当天开始时间 + public static DateTime ToStartOfDay(this DateTime dateTime) + { + return dateTime.Date; + } + + /// + /// 获取当天结束时间(23:59:59.999) + /// + /// 日期时间 + /// 当天结束时间 + public static DateTime ToEndOfDay(this DateTime dateTime) + { + return dateTime.Date.AddDays(1).AddTicks(-1); + } + } +} diff --git a/QYZH.InteractiveMagazine.Common/Extensions/EnumExtension.cs b/QYZH.InteractiveMagazine.Common/Extensions/EnumExtension.cs new file mode 100644 index 0000000..dbedfed --- /dev/null +++ b/QYZH.InteractiveMagazine.Common/Extensions/EnumExtension.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; + +namespace QYZH.InteractiveMagazine.Common.Extensions +{ + /// + /// 枚举扩展方法 + /// + public static class EnumExtension + { + /// + /// 获取枚举的DescriptionAttribute描述 + /// + /// 枚举类型 + /// 枚举值 + /// 描述文本,如果没有DescriptionAttribute则返回枚举名称 + public static string GetDescription(this T enumValue) where T : Enum + { + System.Reflection.FieldInfo? field = enumValue.GetType().GetField(enumValue.ToString()); + if (field == null) + { + return enumValue.ToString(); + } + + var attribute = (DescriptionAttribute?)Attribute.GetCustomAttribute( + field, typeof(DescriptionAttribute)); + + return attribute?.Description ?? enumValue.ToString(); + } + + /// + /// 获取枚举名称 + /// + /// 枚举类型 + /// 枚举值 + /// 枚举名称 + public static string GetName(this T enumValue) where T : Enum + { + return enumValue.ToString(); + } + + /// + /// 获取所有枚举名称列表 + /// + /// 枚举类型 + /// 枚举名称列表 + public static List GetNames() where T : Enum + { + return Enum.GetNames(typeof(T)).ToList(); + } + + /// + /// 获取所有枚举描述列表 + /// + /// 枚举类型 + /// 枚举描述列表 + public static List GetDescriptions() where T : Enum + { + return Enum.GetValues(typeof(T)) + .Cast() + .Select(e => e.GetDescription()) + .ToList(); + } + + /// + /// 将枚举转换为字典(名称,值) + /// + /// 枚举类型 + /// 枚举字典 + public static Dictionary ToDictionary() where T : Enum + { + return Enum.GetValues(typeof(T)) + .Cast() + .ToDictionary(e => e.GetDescription(), e => Convert.ToInt32(e)); + } + } +} diff --git a/QYZH.InteractiveMagazine.Common/Extensions/ObjectExtension.cs b/QYZH.InteractiveMagazine.Common/Extensions/ObjectExtension.cs new file mode 100644 index 0000000..d6b81a4 --- /dev/null +++ b/QYZH.InteractiveMagazine.Common/Extensions/ObjectExtension.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using Newtonsoft.Json; + +namespace QYZH.InteractiveMagazine.Common.Extensions +{ + /// + /// 对象扩展方法 + /// + public static class ObjectExtension + { + /// + /// 将源对象的属性值拷贝到目标对象 + /// + /// 目标对象类型 + /// 源对象 + /// 目标对象 + /// 目标对象 + public static T CopyTo(this object source, T target) + { + if (source == null || target == null) + { + return target; + } + + Type sourceType = source.GetType(); + Type targetType = target.GetType(); + + PropertyInfo[] sourceProperties = sourceType.GetProperties(BindingFlags.Public | BindingFlags.Instance); + + foreach (PropertyInfo sourceProp in sourceProperties) + { + if (!sourceProp.CanRead) + { + continue; + } + + PropertyInfo? targetProp = targetType.GetProperty(sourceProp.Name); + + if (targetProp != null && targetProp.CanWrite && + targetProp.PropertyType == sourceProp.PropertyType) + { + object? value = sourceProp.GetValue(source); + targetProp.SetValue(target, value); + } + } + + return target; + } + + /// + /// 将对象转换为JSON字符串 + /// + /// 对象 + /// 格式化选项 + /// JSON字符串 + public static string ToJson(this object obj, Formatting formatting = Formatting.None) + { + if (obj == null) + { + return string.Empty; + } + + return JsonConvert.SerializeObject(obj, formatting); + } + + /// + /// 将对象转换为字典 + /// + /// 对象 + /// 字典 + public static Dictionary ToDictionary(this object obj) + { + if (obj == null) + { + return new Dictionary(); + } + + var dictionary = new Dictionary(); + PropertyInfo[] properties = obj.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance); + + foreach (PropertyInfo property in properties) + { + if (property.CanRead) + { + dictionary[property.Name] = property.GetValue(obj); + } + } + + return dictionary; + } + } +} diff --git a/QYZH.InteractiveMagazine.Common/Extensions/StringExtension.cs b/QYZH.InteractiveMagazine.Common/Extensions/StringExtension.cs new file mode 100644 index 0000000..116cc88 --- /dev/null +++ b/QYZH.InteractiveMagazine.Common/Extensions/StringExtension.cs @@ -0,0 +1,150 @@ +using System; +using System.Security.Cryptography; +using System.Text; +using System.Text.RegularExpressions; + +namespace QYZH.InteractiveMagazine.Common.Extensions +{ + /// + /// 字符串扩展方法 + /// + public static class StringExtension + { + /// + /// MD5加密 + /// + /// 原始字符串 + /// MD5哈希值(32位小写) + public static string ToMd5(this string input) + { + if (string.IsNullOrEmpty(input)) + { + return string.Empty; + } + + using (var md5 = MD5.Create()) + { + byte[] bytes = md5.ComputeHash(Encoding.UTF8.GetBytes(input)); + var sb = new StringBuilder(); + foreach (byte b in bytes) + { + sb.Append(b.ToString("x2")); + } + return sb.ToString(); + } + } + + /// + /// SHA256加密 + /// + /// 原始字符串 + /// SHA256哈希值(64位小写) + public static string ToSha256(this string input) + { + if (string.IsNullOrEmpty(input)) + { + return string.Empty; + } + + using (var sha256 = SHA256.Create()) + { + byte[] bytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(input)); + var sb = new StringBuilder(); + foreach (byte b in bytes) + { + sb.Append(b.ToString("x2")); + } + return sb.ToString(); + } + } + + /// + /// Base64编码 + /// + /// 原始字符串 + /// Base64编码后的字符串 + public static string ToBase64(this string input) + { + if (string.IsNullOrEmpty(input)) + { + return string.Empty; + } + + byte[] bytes = Encoding.UTF8.GetBytes(input); + return Convert.ToBase64String(bytes); + } + + /// + /// Base64解码 + /// + /// Base64编码的字符串 + /// 解码后的原始字符串 + public static string FromBase64(this string input) + { + if (string.IsNullOrEmpty(input)) + { + return string.Empty; + } + + byte[] bytes = Convert.FromBase64String(input); + return Encoding.UTF8.GetString(bytes); + } + + /// + /// 判断字符串是否为空或空白 + /// + /// 待检查的字符串 + /// 是否为空或空白 + public static bool IsNullOrWhiteSpace(this string input) + { + return string.IsNullOrWhiteSpace(input); + } + + /// + /// 手机号脱敏 + /// + /// 手机号 + /// 脱敏后的手机号(中间4位用*替换) + public static string MaskPhone(this string phone) + { + if (string.IsNullOrEmpty(phone) || phone.Length < 7) + { + return phone; + } + + return phone.Substring(0, 3) + "****" + phone.Substring(phone.Length - 4); + } + + /// + /// 身份证脱敏 + /// + /// 身份证号 + /// 脱敏后的身份证号(保留前3位和后4位) + public static string MaskIdCard(this string idCard) + { + if (string.IsNullOrEmpty(idCard) || idCard.Length < 7) + { + return idCard; + } + + int length = idCard.Length; + return idCard.Substring(0, 3) + new string('*', length - 7) + idCard.Substring(length - 4); + } + + /// + /// 正则匹配 + /// + /// 待匹配的字符串 + /// 正则表达式 + /// 是否匹配 + public static bool IsMatchRegex(this string input, string pattern) + { + if (string.IsNullOrEmpty(input) || string.IsNullOrEmpty(pattern)) + { + return false; + } + + return Regex.IsMatch(input, pattern); + } + } +} diff --git a/QYZH.InteractiveMagazine.Common/Helpers/EnumHelper.cs b/QYZH.InteractiveMagazine.Common/Helpers/EnumHelper.cs new file mode 100644 index 0000000..323b440 --- /dev/null +++ b/QYZH.InteractiveMagazine.Common/Helpers/EnumHelper.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; + +namespace QYZH.InteractiveMagazine.Common.Helpers +{ + /// + /// 枚举工具类 + /// + public static class EnumHelper + { + /// + /// 根据枚举名称获取枚举值 + /// + /// 枚举类型 + /// 枚举名称 + /// 枚举值 + public static TEnum GetValue(string name) where TEnum : Enum + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentException("枚举名称不能为空", nameof(name)); + } + + if (Enum.TryParse(typeof(TEnum), name, true, out object? result)) + { + return (TEnum)result; + } + + throw new ArgumentException($"枚举 '{typeof(TEnum).Name}' 中不存在名称为 '{name}' 的值"); + } + + /// + /// 根据枚举值获取枚举名称 + /// + /// 枚举类型 + /// 枚举值 + /// 枚举名称 + public static string GetName(object value) where TEnum : Enum + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + if (Enum.IsDefined(typeof(TEnum), value)) + { + return Enum.GetName(typeof(TEnum), value)!; + } + + throw new ArgumentException($"枚举 '{typeof(TEnum).Name}' 中不存在值 '{value}'"); + } + + /// + /// 获取枚举的所有值 + /// + /// 枚举类型 + /// 枚举值列表 + public static List GetAllValues() where TEnum : Enum + { + return Enum.GetValues(typeof(TEnum)).Cast().ToList(); + } + + /// + /// 根据Description获取枚举值 + /// + /// 枚举类型 + /// 描述文本 + /// 枚举值 + public static TEnum GetValueByDescription(string description) where TEnum : Enum + { + if (string.IsNullOrWhiteSpace(description)) + { + throw new ArgumentException("描述不能为空", nameof(description)); + } + + foreach (TEnum value in Enum.GetValues(typeof(TEnum))) + { + string desc = GetDescription(value); + if (desc.Equals(description, StringComparison.OrdinalIgnoreCase)) + { + return value; + } + } + + throw new ArgumentException($"枚举 '{typeof(TEnum).Name}' 中不存在描述为 '{description}' 的值"); + } + + /// + /// 获取枚举的DescriptionAttribute描述 + /// + /// 枚举类型 + /// 枚举值 + /// 描述文本 + public static string GetDescription(TEnum value) where TEnum : Enum + { + System.Reflection.FieldInfo? field = value.GetType().GetField(value.ToString()); + if (field == null) + { + return value.ToString(); + } + + var attribute = (DescriptionAttribute?)Attribute.GetCustomAttribute( + field, typeof(DescriptionAttribute)); + + return attribute?.Description ?? value.ToString(); + } + } +} diff --git a/QYZH.InteractiveMagazine.Common/Helpers/HttpHelper.cs b/QYZH.InteractiveMagazine.Common/Helpers/HttpHelper.cs new file mode 100644 index 0000000..5b4d1d6 --- /dev/null +++ b/QYZH.InteractiveMagazine.Common/Helpers/HttpHelper.cs @@ -0,0 +1,103 @@ +using System; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; + +namespace QYZH.InteractiveMagazine.Common.Helpers +{ + /// + /// HTTP请求工具类 + /// + public static class HttpHelper + { + private static readonly HttpClient _httpClient = new HttpClient(); + + /// + /// 设置默认请求头 + /// + static HttpHelper() + { + _httpClient.Timeout = TimeSpan.FromSeconds(30); + } + + /// + /// 发送GET请求 + /// + /// 请求地址 + /// 响应内容字符串 + public static async Task GetAsync(string url) + { + HttpResponseMessage response = await _httpClient.GetAsync(url); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadAsStringAsync(); + } + + /// + /// 发送GET请求并反序列化为指定类型 + /// + /// 响应类型 + /// 请求地址 + /// 反序列化后的对象 + public static async Task GetAsync(string url) + { + string json = await GetAsync(url); + return JsonConvert.DeserializeObject(json); + } + + /// + /// 发送POST请求 + /// + /// 请求地址 + /// 请求数据 + /// 响应内容字符串 + public static async Task PostAsync(string url, object data) + { + string json = JsonConvert.SerializeObject(data); + var content = new StringContent(json, Encoding.UTF8, "application/json"); + HttpResponseMessage response = await _httpClient.PostAsync(url, content); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadAsStringAsync(); + } + + /// + /// 发送POST请求并反序列化为指定类型 + /// + /// 响应类型 + /// 请求地址 + /// 请求数据 + /// 反序列化后的对象 + public static async Task PostAsync(string url, object data) + { + string json = await PostAsync(url, data); + return JsonConvert.DeserializeObject(json); + } + + /// + /// 发送DELETE请求 + /// + /// 请求地址 + /// 响应内容字符串 + public static async Task DeleteAsync(string url) + { + HttpResponseMessage response = await _httpClient.DeleteAsync(url); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadAsStringAsync(); + } + + /// + /// 发送PUT请求 + /// + /// 请求地址 + /// 请求数据 + /// 响应内容字符串 + public static async Task PutAsync(string url, object data) + { + string json = JsonConvert.SerializeObject(data); + var content = new StringContent(json, Encoding.UTF8, "application/json"); + HttpResponseMessage response = await _httpClient.PutAsync(url, content); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadAsStringAsync(); + } + } +} diff --git a/QYZH.InteractiveMagazine.Common/Helpers/JsonHelper.cs b/QYZH.InteractiveMagazine.Common/Helpers/JsonHelper.cs new file mode 100644 index 0000000..7f515a3 --- /dev/null +++ b/QYZH.InteractiveMagazine.Common/Helpers/JsonHelper.cs @@ -0,0 +1,91 @@ +using Newtonsoft.Json; + +namespace QYZH.InteractiveMagazine.Common.Helpers +{ + /// + /// JSON工具类 + /// + public static class JsonHelper + { + /// + /// JSON序列化设置 + /// + private static readonly JsonSerializerSettings _settings = new JsonSerializerSettings + { + NullValueHandling = NullValueHandling.Ignore, + DateFormatString = "yyyy-MM-dd HH:mm:ss", + ReferenceLoopHandling = ReferenceLoopHandling.Ignore + }; + + /// + /// 将对象序列化为JSON字符串 + /// + /// 对象 + /// JSON字符串 + public static string Serialize(object obj) + { + if (obj == null) + { + return string.Empty; + } + + return JsonConvert.SerializeObject(obj, _settings); + } + + /// + /// 将对象序列化为格式化的JSON字符串 + /// + /// 对象 + /// 格式化的JSON字符串 + public static string SerializePretty(object obj) + { + if (obj == null) + { + return string.Empty; + } + + return JsonConvert.SerializeObject(obj, Formatting.Indented, _settings); + } + + /// + /// 将JSON字符串反序列化为指定类型 + /// + /// 目标类型 + /// JSON字符串 + /// 反序列化后的对象 + public static T? Deserialize(string json) + { + if (string.IsNullOrWhiteSpace(json)) + { + return default; + } + + return JsonConvert.DeserializeObject(json, _settings); + } + + /// + /// 将JSON字符串反序列化为指定类型(带异常处理) + /// + /// 目标类型 + /// JSON字符串 + /// 反序列化失败时的默认值 + /// 反序列化后的对象或默认值 + public static T? TryDeserialize(string json, T? defaultValue = default) + { + if (string.IsNullOrWhiteSpace(json)) + { + return defaultValue; + } + + try + { + T? result = JsonConvert.DeserializeObject(json, _settings); + return result ?? defaultValue; + } + catch + { + return defaultValue; + } + } + } +} diff --git a/QYZH.InteractiveMagazine.Common/Helpers/SnowflakeIdHelper.cs b/QYZH.InteractiveMagazine.Common/Helpers/SnowflakeIdHelper.cs new file mode 100644 index 0000000..99ee975 --- /dev/null +++ b/QYZH.InteractiveMagazine.Common/Helpers/SnowflakeIdHelper.cs @@ -0,0 +1,180 @@ +using System; +using System.Linq; + +namespace QYZH.InteractiveMagazine.Common.Helpers +{ + /// + /// 雪花ID生成器 + /// 基于Twitter Snowflake算法实现 + /// + public static class SnowflakeIdHelper + { + private static long _sequence = 0L; + private static long _lastTimestamp = -1L; + private static readonly object _lock = new object(); + + // 基础时间戳 (2020-01-01 00:00:00 UTC) + private const long TwEpoch = 1577836800000L; + + // 机器ID位数 + private const int WorkerIdBits = 5; + + // 数据中心ID位数 + private const int DataCenterIdBits = 5; + + // 序列号位数 + private const int SequenceBits = 12; + + // 最大值计算 + private const long MaxWorkerId = -1L ^ (-1L << WorkerIdBits); + private const long MaxDataCenterId = -1L ^ (-1L << DataCenterIdBits); + private const long MaxSequence = -1L ^ (-1L << SequenceBits); + + // 位移偏移量 + private const int WorkerIdShift = SequenceBits; + private const int DataCenterIdShift = SequenceBits + WorkerIdBits; + private const int TimestampLeftShift = SequenceBits + WorkerIdBits + DataCenterIdBits; + + private static long _workerId; + private static long _dataCenterId; + + /// + /// 静态构造函数,初始化机器ID和数据中心ID + /// + static SnowflakeIdHelper() + { + _workerId = GetWorkerId(); + _dataCenterId = GetDataCenterId(); + } + + /// + /// 初始化雪花ID生成器 + /// + /// 机器ID (0-31) + /// 数据中心ID (0-31) + public static void Initialize(long workerId, long dataCenterId) + { + if (workerId < 0 || workerId > MaxWorkerId) + { + throw new ArgumentException($"机器ID必须在0-{MaxWorkerId}范围内", nameof(workerId)); + } + + if (dataCenterId < 0 || dataCenterId > MaxDataCenterId) + { + throw new ArgumentException($"数据中心ID必须在0-{MaxDataCenterId}范围内", nameof(dataCenterId)); + } + + _workerId = workerId; + _dataCenterId = dataCenterId; + } + + /// + /// 生成雪花ID + /// + /// 唯一的雪花ID + public static long GenerateId() + { + lock (_lock) + { + long timestamp = GetCurrentMilliseconds(); + + // 时钟回拨检测 + if (timestamp < _lastTimestamp) + { + throw new InvalidOperationException("时钟回拨异常,拒绝生成ID"); + } + + // 同一毫秒内,序列号递增 + if (timestamp == _lastTimestamp) + { + _sequence = (_sequence + 1) & MaxSequence; + + // 序列号溢出,等待下一毫秒 + if (_sequence == 0) + { + timestamp = WaitNextMillis(_lastTimestamp); + } + } + else + { + _sequence = 0L; + } + + _lastTimestamp = timestamp; + + // 组装ID: 时间戳 + 数据中心ID + 机器ID + 序列号 + return ((timestamp - TwEpoch) << TimestampLeftShift) | + (_dataCenterId << DataCenterIdShift) | + (_workerId << WorkerIdShift) | + _sequence; + } + } + + /// + /// 获取当前毫秒数 + /// + private static long GetCurrentMilliseconds() + { + return DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + } + + /// + /// 等待下一毫秒 + /// + private static long WaitNextMillis(long lastTimestamp) + { + long timestamp = GetCurrentMilliseconds(); + while (timestamp <= lastTimestamp) + { + timestamp = GetCurrentMilliseconds(); + } + return timestamp; + } + + /// + /// 获取机器ID(基于MAC地址简单计算) + /// + private static long GetWorkerId() + { + try + { + string macAddress = System.Net.NetworkInformation.NetworkInterface.GetAllNetworkInterfaces() + .FirstOrDefault(n => n.OperationalStatus == System.Net.NetworkInformation.OperationalStatus.Up && + n.NetworkInterfaceType != System.Net.NetworkInformation.NetworkInterfaceType.Loopback)? + .GetPhysicalAddress().ToString() ?? "0"; + + long hash = 0; + foreach (char c in macAddress) + { + hash = (hash * 31 + c) & MaxWorkerId; + } + return hash; + } + catch + { + return 1; + } + } + + /// + /// 获取数据中心ID(基于机器名简单计算) + /// + private static long GetDataCenterId() + { + try + { + string machineName = Environment.MachineName; + long hash = 0; + foreach (char c in machineName) + { + hash = (hash * 31 + c) & MaxDataCenterId; + } + return hash; + } + catch + { + return 1; + } + } + } +} diff --git a/QYZH.InteractiveMagazine.Common/Helpers/ValidateHelper.cs b/QYZH.InteractiveMagazine.Common/Helpers/ValidateHelper.cs new file mode 100644 index 0000000..d106723 --- /dev/null +++ b/QYZH.InteractiveMagazine.Common/Helpers/ValidateHelper.cs @@ -0,0 +1,102 @@ +using System.Text.RegularExpressions; + +namespace QYZH.InteractiveMagazine.Common.Helpers +{ + /// + /// 参数校验工具类 + /// + public static class ValidateHelper + { + /// + /// 邮箱正则表达式 + /// + private const string EmailPattern = @"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"; + + /// + /// 手机号正则表达式(中国大陆) + /// + private const string PhonePattern = @"^1[3-9]\d{9}$"; + + /// + /// 身份证号正则表达式(中国大陆) + /// + private const string IdCardPattern = @"^(^[1-9]\d{7}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{3}$)|(^[1-9]\d{5}[1-9]\d{3}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])((\d{4})|\d{3}[Xx])$)$"; + + /// + /// 校验邮箱格式 + /// + /// 邮箱地址 + /// 是否有效 + public static bool IsEmail(string email) + { + if (string.IsNullOrWhiteSpace(email)) + { + return false; + } + + return Regex.IsMatch(email, EmailPattern); + } + + /// + /// 校验手机号格式 + /// + /// 手机号 + /// 是否有效 + public static bool IsPhone(string phone) + { + if (string.IsNullOrWhiteSpace(phone)) + { + return false; + } + + return Regex.IsMatch(phone, PhonePattern); + } + + /// + /// 校验身份证号格式 + /// + /// 身份证号 + /// 是否有效 + public static bool IsIdCard(string idCard) + { + if (string.IsNullOrWhiteSpace(idCard)) + { + return false; + } + + return Regex.IsMatch(idCard, IdCardPattern); + } + + /// + /// 校验URL格式 + /// + /// URL地址 + /// 是否有效 + public static bool IsUrl(string url) + { + if (string.IsNullOrWhiteSpace(url)) + { + return false; + } + + const string urlPattern = @"^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$"; + return Regex.IsMatch(url, urlPattern); + } + + /// + /// 校验邮政编码格式 + /// + /// 邮政编码 + /// 是否有效 + public static bool IsZipCode(string zipCode) + { + if (string.IsNullOrWhiteSpace(zipCode)) + { + return false; + } + + const string zipPattern = @"^\d{6}$"; + return Regex.IsMatch(zipCode, zipPattern); + } + } +} diff --git a/QYZH.InteractiveMagazine.Common/QYZH.InteractiveMagazine.Common.csproj b/QYZH.InteractiveMagazine.Common/QYZH.InteractiveMagazine.Common.csproj new file mode 100644 index 0000000..b44c444 --- /dev/null +++ b/QYZH.InteractiveMagazine.Common/QYZH.InteractiveMagazine.Common.csproj @@ -0,0 +1,18 @@ + + + + + + + + + + + + + net8.0 + enable + enable + + + diff --git a/QYZH.InteractiveMagazine.IService/Class1.cs b/QYZH.InteractiveMagazine.IService/Class1.cs new file mode 100644 index 0000000..0d5ecc8 --- /dev/null +++ b/QYZH.InteractiveMagazine.IService/Class1.cs @@ -0,0 +1,6 @@ +namespace QYZH.InteractiveMagazine.IService; + +public class Class1 +{ + +} diff --git a/QYZH.InteractiveMagazine.IService/Dto/AuthDto.cs b/QYZH.InteractiveMagazine.IService/Dto/AuthDto.cs new file mode 100644 index 0000000..08a0d41 --- /dev/null +++ b/QYZH.InteractiveMagazine.IService/Dto/AuthDto.cs @@ -0,0 +1,64 @@ +namespace QYZH.InteractiveMagazine.IService.Dto; + +/// +/// 登录输入 +/// +public class LoginInput +{ + /// + /// 账号 + /// + public string Account { get; set; } = string.Empty; + + /// + /// 密码 + /// + public string Password { get; set; } = string.Empty; +} + +/// +/// 注册输入 +/// +public class RegisterInput +{ + /// + /// 账号 + /// + public string Account { get; set; } = string.Empty; + + /// + /// 密码 + /// + public string Password { get; set; } = string.Empty; + + /// + /// 用户名 + /// + public string UserName { get; set; } = string.Empty; +} + +/// +/// 登录输出 +/// +public class LoginOutput +{ + /// + /// 访问令牌 + /// + public string Token { get; set; } = string.Empty; + + /// + /// 用户ID + /// + public long UserId { get; set; } + + /// + /// 用户名 + /// + public string UserName { get; set; } = string.Empty; + + /// + /// 刷新令牌 + /// + public string RefreshToken { get; set; } = string.Empty; +} diff --git a/QYZH.InteractiveMagazine.IService/Dto/WeChatDto.cs b/QYZH.InteractiveMagazine.IService/Dto/WeChatDto.cs new file mode 100644 index 0000000..f2f3709 --- /dev/null +++ b/QYZH.InteractiveMagazine.IService/Dto/WeChatDto.cs @@ -0,0 +1,38 @@ +namespace QYZH.InteractiveMagazine.IService.Dto; + +/// +/// 微信登录输出 +/// +public class WeChatLoginOutput +{ + /// + /// 访问令牌 + /// + public string Token { get; set; } = string.Empty; + + /// + /// 用户ID + /// + public long UserId { get; set; } + + /// + /// 用户名 + /// + public string UserName { get; set; } = string.Empty; + + /// + /// 微信OpenId + /// + public string OpenId { get; set; } = string.Empty; +} + +/// +/// 微信手机号获取输出 +/// +public class WeChatPhoneNumberOutput +{ + /// + /// 手机号 + /// + public string PhoneNumber { get; set; } = string.Empty; +} diff --git a/QYZH.InteractiveMagazine.IService/IAuthService.cs b/QYZH.InteractiveMagazine.IService/IAuthService.cs new file mode 100644 index 0000000..f1ea5a2 --- /dev/null +++ b/QYZH.InteractiveMagazine.IService/IAuthService.cs @@ -0,0 +1,31 @@ +using QYZH.InteractiveMagazine.IService.Dto; + +namespace QYZH.InteractiveMagazine.IService; + +/// +/// 认证服务接口 +/// +public interface IAuthService +{ + /// + /// 用户登录 + /// + /// 账号 + /// 密码 + /// 登录结果 + Task LoginAsync(string account, string password); + + /// + /// 用户注册 + /// + /// 注册输入参数 + /// 是否成功 + Task RegisterAsync(RegisterInput input); + + /// + /// 刷新令牌 + /// + /// 刷新令牌 + /// 新的登录结果 + Task RefreshTokenAsync(string refreshToken); +} diff --git a/QYZH.InteractiveMagazine.IService/IBaseService.cs b/QYZH.InteractiveMagazine.IService/IBaseService.cs new file mode 100644 index 0000000..ee50497 --- /dev/null +++ b/QYZH.InteractiveMagazine.IService/IBaseService.cs @@ -0,0 +1,51 @@ +using QYZH.InteractiveMagazine.Models.Dto; + +namespace QYZH.InteractiveMagazine.IService; + +/// +/// 基础服务接口 +/// +/// 实体类型 +public interface IBaseService where T : class, new() +{ + /// + /// 根据ID获取实体 + /// + /// 实体ID + /// 实体对象 + Task GetByIdAsync(long id); + + /// + /// 获取所有实体列表 + /// + /// 实体列表 + Task> GetListAsync(); + + /// + /// 获取分页列表 + /// + /// 分页查询参数 + /// 分页数据 + Task> GetPageListAsync(PageQueryModel pageQuery); + + /// + /// 新增实体 + /// + /// 实体对象 + /// 是否成功 + Task InsertAsync(T entity); + + /// + /// 更新实体 + /// + /// 实体对象 + /// 是否成功 + Task UpdateAsync(T entity); + + /// + /// 根据ID删除实体 + /// + /// 实体ID + /// 是否成功 + Task DeleteByIdAsync(long id); +} diff --git a/QYZH.InteractiveMagazine.IService/IWeChatMiniProgramService.cs b/QYZH.InteractiveMagazine.IService/IWeChatMiniProgramService.cs new file mode 100644 index 0000000..6559d8c --- /dev/null +++ b/QYZH.InteractiveMagazine.IService/IWeChatMiniProgramService.cs @@ -0,0 +1,23 @@ +using QYZH.InteractiveMagazine.IService.Dto; + +namespace QYZH.InteractiveMagazine.IService; + +/// +/// 微信小程序服务接口 +/// +public interface IWeChatMiniProgramService +{ + /// + /// 微信登录 + /// + /// 微信登录凭证 + /// 微信登录结果 + Task WeChatLoginAsync(string code); + + /// + /// 获取手机号 + /// + /// 获取手机号凭证 + /// 手机号信息 + Task GetPhoneNumberAsync(string code); +} diff --git a/QYZH.InteractiveMagazine.IService/QYZH.InteractiveMagazine.IService.csproj b/QYZH.InteractiveMagazine.IService/QYZH.InteractiveMagazine.IService.csproj new file mode 100644 index 0000000..a890765 --- /dev/null +++ b/QYZH.InteractiveMagazine.IService/QYZH.InteractiveMagazine.IService.csproj @@ -0,0 +1,14 @@ + + + + + + + + + net8.0 + enable + enable + + + diff --git a/QYZH.InteractiveMagazine.Infrastructure/Auth/JwtHelper.cs b/QYZH.InteractiveMagazine.Infrastructure/Auth/JwtHelper.cs new file mode 100644 index 0000000..683426f --- /dev/null +++ b/QYZH.InteractiveMagazine.Infrastructure/Auth/JwtHelper.cs @@ -0,0 +1,71 @@ +using Microsoft.IdentityModel.Tokens; +using QYZH.InteractiveMagazine.Models.Settings; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; + +namespace QYZH.InteractiveMagazine.Infrastructure.Auth; + +/// +/// JWT工具类 +/// +public static class JwtHelper +{ + /// + /// 生成JWT令牌 + /// + /// 用户ID + /// 用户名 + /// JWT配置 + /// JWT令牌字符串 + public static string GenerateToken(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) + }; + + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(settings.SecretKey!)); + var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); + + var token = new JwtSecurityToken( + issuer: settings.Issuer, + audience: settings.Audience, + claims: claims, + expires: DateTime.Now.AddMinutes(settings.ExpiryMinutes), + signingCredentials: credentials + ); + + return new JwtSecurityTokenHandler().WriteToken(token); + } + + /// + /// 验证JWT令牌 + /// + /// JWT令牌 + /// JWT配置 + /// ClaimsPrincipal对象 + public static ClaimsPrincipal ValidateToken(string token, JwtSettings settings) + { + var tokenHandler = new JwtSecurityTokenHandler(); + var key = Encoding.UTF8.GetBytes(settings.SecretKey!); + + var validationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidIssuer = settings.Issuer, + ValidateAudience = true, + ValidAudience = settings.Audience, + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(key), + ValidateLifetime = true, + ClockSkew = TimeSpan.Zero + }; + + return tokenHandler.ValidateToken(token, validationParameters, out _); + } +} diff --git a/QYZH.InteractiveMagazine.Infrastructure/Cache/RedisHelper.cs b/QYZH.InteractiveMagazine.Infrastructure/Cache/RedisHelper.cs new file mode 100644 index 0000000..9fc9409 --- /dev/null +++ b/QYZH.InteractiveMagazine.Infrastructure/Cache/RedisHelper.cs @@ -0,0 +1,119 @@ +using StackExchange.Redis; + +namespace QYZH.InteractiveMagazine.Infrastructure.Cache; + +/// +/// Redis操作封装 +/// +public static class RedisHelper +{ + private static string? _keyPrefix; + + /// + /// Redis连接实例 + /// + public static IConnectionMultiplexer? Connection { get; set; } + + /// + /// 设置键前缀 + /// + /// 前缀字符串 + public static void SetKeyPrefix(string prefix) + { + _keyPrefix = prefix; + } + + /// + /// 获取Redis数据库实例 + /// + /// 数据库索引 + /// IDatabase实例 + public static IDatabase GetDatabase(int db = -1) + { + if (Connection == null || !Connection.IsConnected) + { + throw new InvalidOperationException("Redis连接未初始化或已断开"); + } + + return Connection.GetDatabase(db); + } + + /// + /// 设置字符串值 + /// + /// 键 + /// 值 + /// 过期时间 + /// 是否成功 + public static async Task 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); + } + + /// + /// 获取字符串值 + /// + /// 键 + /// + public static async Task StringGetAsync(string key) + { + var db = GetDatabase(); + var prefixedKey = GetPrefixedKey(key); + return await db.StringGetAsync(prefixedKey); + } + + /// + /// 删除键 + /// + /// 键 + /// 是否成功 + public static async Task KeyDeleteAsync(string key) + { + var db = GetDatabase(); + var prefixedKey = GetPrefixedKey(key); + return await db.KeyDeleteAsync(prefixedKey); + } + + /// + /// 检查键是否存在 + /// + /// 键 + /// 是否存在 + public static async Task KeyExistsAsync(string key) + { + var db = GetDatabase(); + var prefixedKey = GetPrefixedKey(key); + return await db.KeyExistsAsync(prefixedKey); + } + + /// + /// 设置键的过期时间 + /// + /// 键 + /// 过期时间 + /// 是否成功 + public static async Task KeyExpireAsync(string key, TimeSpan expiry) + { + var db = GetDatabase(); + var prefixedKey = GetPrefixedKey(key); + return await db.KeyExpireAsync(prefixedKey, expiry); + } + + /// + /// 获取带前缀的键 + /// + /// 原始键 + /// 带前缀的键 + private static string GetPrefixedKey(string key) + { + return string.IsNullOrEmpty(_keyPrefix) ? key : $"{_keyPrefix}:{key}"; + } +} diff --git a/QYZH.InteractiveMagazine.Infrastructure/Extensions/DependencyInjectionExtensions.cs b/QYZH.InteractiveMagazine.Infrastructure/Extensions/DependencyInjectionExtensions.cs new file mode 100644 index 0000000..a172a40 --- /dev/null +++ b/QYZH.InteractiveMagazine.Infrastructure/Extensions/DependencyInjectionExtensions.cs @@ -0,0 +1,110 @@ +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.IdentityModel.Tokens; +using QYZH.InteractiveMagazine.Infrastructure.Cache; +using QYZH.InteractiveMagazine.Infrastructure.MessageQueue; +using QYZH.InteractiveMagazine.Infrastructure.Middleware; +using QYZH.InteractiveMagazine.Models.Settings; +using RabbitMQ.Client; +using StackExchange.Redis; +using System.Text; + +namespace QYZH.InteractiveMagazine.Infrastructure.Extensions; + +/// +/// 统一服务注册扩展 +/// +public static class DependencyInjectionExtensions +{ + /// + /// 注册基础设施服务 + /// + /// 服务集合 + /// 配置 + public static void AddInfrastructureServices(this IServiceCollection services, IConfiguration configuration) + { + AddJwtAuthentication(services, configuration); + AddRedisCache(services, configuration); + AddRabbitMQ(services, configuration); + + services.AddTransient(); + services.AddTransient(); + } + + /// + /// 配置JWT认证 + /// + /// 服务集合 + /// 配置 + private static void AddJwtAuthentication(IServiceCollection services, IConfiguration configuration) + { + var jwtSettings = configuration.GetSection("JwtSettings").Get()!; + + services.AddSingleton(jwtSettings); + + services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(options => + { + 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 + }; + }); + } + + /// + /// 注册Redis缓存 + /// + /// 服务集合 + /// 配置 + private static void AddRedisCache(IServiceCollection services, IConfiguration configuration) + { + var redisSettings = configuration.GetSection("RedisSettings").Get()!; + + services.AddSingleton(redisSettings); + + services.AddSingleton(sp => + { + var multiplexer = ConnectionMultiplexer.Connect(redisSettings.ConnectionString!); + RedisHelper.Connection = multiplexer; + RedisHelper.SetKeyPrefix(redisSettings.InstanceName!); + return multiplexer; + }); + } + + /// + /// 注册RabbitMQ + /// + /// 服务集合 + /// 配置 + private static void AddRabbitMQ(IServiceCollection services, IConfiguration configuration) + { + var rabbitMQSettings = configuration.GetSection("RabbitMQSettings").Get()!; + + services.AddSingleton(rabbitMQSettings); + + services.AddSingleton(sp => + { + var factory = new ConnectionFactory + { + HostName = rabbitMQSettings.HostName!, + Port = rabbitMQSettings.Port, + UserName = rabbitMQSettings.UserName!, + Password = rabbitMQSettings.Password!, + VirtualHost = rabbitMQSettings.VirtualHost! + }; + + return factory.CreateConnectionAsync().GetAwaiter().GetResult(); + }); + + services.AddTransient(); + } +} diff --git a/QYZH.InteractiveMagazine.Infrastructure/MessageQueue/RabbitMQConsumer.cs b/QYZH.InteractiveMagazine.Infrastructure/MessageQueue/RabbitMQConsumer.cs new file mode 100644 index 0000000..18021c8 --- /dev/null +++ b/QYZH.InteractiveMagazine.Infrastructure/MessageQueue/RabbitMQConsumer.cs @@ -0,0 +1,72 @@ +using Microsoft.Extensions.Logging; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; +using System.Text; + +namespace QYZH.InteractiveMagazine.Infrastructure.MessageQueue; + +/// +/// RabbitMQ消息消费者基类 +/// +public abstract class RabbitMQConsumer : IDisposable +{ + private readonly IConnection _connection; + private readonly ILogger _logger; + private IChannel? _channel; + private AsyncEventingBasicConsumer? _consumer; + + /// + /// 构造函数 + /// + /// RabbitMQ连接 + /// 日志记录器 + protected RabbitMQConsumer(IConnection connection, ILogger logger) + { + _connection = connection; + _logger = logger; + } + + /// + /// 启动消费 + /// + /// 队列名称 + /// 消息处理委托 + public async Task StartConsume(string queueName, Func 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); + } + + /// + /// 释放资源 + /// + public void Dispose() + { + _channel?.DisposeAsync().GetAwaiter().GetResult(); + GC.SuppressFinalize(this); + } +} diff --git a/QYZH.InteractiveMagazine.Infrastructure/MessageQueue/RabbitMQPublisher.cs b/QYZH.InteractiveMagazine.Infrastructure/MessageQueue/RabbitMQPublisher.cs new file mode 100644 index 0000000..a84f184 --- /dev/null +++ b/QYZH.InteractiveMagazine.Infrastructure/MessageQueue/RabbitMQPublisher.cs @@ -0,0 +1,47 @@ +using Microsoft.Extensions.Logging; +using RabbitMQ.Client; +using System.Text; + +namespace QYZH.InteractiveMagazine.Infrastructure.MessageQueue; + +/// +/// RabbitMQ消息发布器 +/// +public class RabbitMQPublisher +{ + private readonly IConnection _connection; + private readonly ILogger _logger; + + /// + /// 构造函数 + /// + /// RabbitMQ连接 + /// 日志记录器 + public RabbitMQPublisher(IConnection connection, ILogger logger) + { + _connection = connection; + _logger = logger; + } + + /// + /// 发布消息 + /// + /// 交换机名称 + /// 路由键 + /// 消息内容 + 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); + } +} diff --git a/QYZH.InteractiveMagazine.Infrastructure/Middleware/GlobalExceptionMiddleware.cs b/QYZH.InteractiveMagazine.Infrastructure/Middleware/GlobalExceptionMiddleware.cs new file mode 100644 index 0000000..4f0bef2 --- /dev/null +++ b/QYZH.InteractiveMagazine.Infrastructure/Middleware/GlobalExceptionMiddleware.cs @@ -0,0 +1,79 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using QYZH.InteractiveMagazine.Models.Common; +using QYZH.InteractiveMagazine.Models.Dto; +using System.Text.Json; + +namespace QYZH.InteractiveMagazine.Infrastructure.Middleware; + +/// +/// 全局异常中间件 +/// +public class GlobalExceptionMiddleware : IMiddleware +{ + private readonly ILogger _logger; + + /// + /// 构造函数 + /// + /// 日志记录器 + public GlobalExceptionMiddleware(ILogger logger) + { + _logger = logger; + } + + /// + /// 执行中间件 + /// + /// HTTP上下文 + /// 下一个中间件委托 + public async Task InvokeAsync(HttpContext context, RequestDelegate next) + { + try + { + await next(context); + } + catch (BusinessException ex) + { + _logger.LogWarning(ex, "业务异常:{Message}", ex.Message); + await HandleBusinessExceptionAsync(context, ex); + } + catch (Exception ex) + { + _logger.LogError(ex, "系统异常:{Message}", ex.Message); + await HandleSystemExceptionAsync(context, ex); + } + } + + /// + /// 处理业务异常 + /// + /// HTTP上下文 + /// 业务异常 + private static async Task HandleBusinessExceptionAsync(HttpContext context, BusinessException ex) + { + context.Response.ContentType = "application/json"; + context.Response.StatusCode = StatusCodes.Status400BadRequest; + + var response = BaseResponse.Fail(ex.Message, ex.Code); + var json = JsonSerializer.Serialize(response); + + await context.Response.WriteAsync(json); + } + + /// + /// 处理系统异常 + /// + /// HTTP上下文 + /// 系统异常 + private static async Task HandleSystemExceptionAsync(HttpContext context, Exception ex) + { + context.Response.ContentType = "application/json"; + context.Response.StatusCode = StatusCodes.Status500InternalServerError; + + var response = BaseResponse.Fail("系统内部错误,请稍后重试"); + var json = JsonSerializer.Serialize(response); + + await context.Response.WriteAsync(json); + } +} diff --git a/QYZH.InteractiveMagazine.Infrastructure/Middleware/OperationLogMiddleware.cs b/QYZH.InteractiveMagazine.Infrastructure/Middleware/OperationLogMiddleware.cs new file mode 100644 index 0000000..894fb2b --- /dev/null +++ b/QYZH.InteractiveMagazine.Infrastructure/Middleware/OperationLogMiddleware.cs @@ -0,0 +1,53 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using System.Diagnostics; + +namespace QYZH.InteractiveMagazine.Infrastructure.Middleware; + +/// +/// 操作日志中间件 +/// +public class OperationLogMiddleware : IMiddleware +{ + private readonly ILogger _logger; + + /// + /// 构造函数 + /// + /// 日志记录器 + public OperationLogMiddleware(ILogger logger) + { + _logger = logger; + } + + /// + /// 执行中间件 + /// + /// HTTP上下文 + /// 下一个中间件委托 + public async Task InvokeAsync(HttpContext context, RequestDelegate next) + { + var stopwatch = Stopwatch.StartNew(); + + var requestMethod = context.Request.Method; + var requestUrl = context.Request.Path.ToString(); + + try + { + await next(context); + } + finally + { + stopwatch.Stop(); + var elapsedMilliseconds = stopwatch.ElapsedMilliseconds; + + _logger.LogInformation( + "请求完成 | {Method} {Url} | 状态码: {StatusCode} | 耗时: {ElapsedMilliseconds}ms", + requestMethod, + requestUrl, + context.Response.StatusCode, + elapsedMilliseconds + ); + } + } +} diff --git a/QYZH.InteractiveMagazine.Infrastructure/QYZH.InteractiveMagazine.Infrastructure.csproj b/QYZH.InteractiveMagazine.Infrastructure/QYZH.InteractiveMagazine.Infrastructure.csproj new file mode 100644 index 0000000..efe3da9 --- /dev/null +++ b/QYZH.InteractiveMagazine.Infrastructure/QYZH.InteractiveMagazine.Infrastructure.csproj @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + net8.0 + enable + enable + + + diff --git a/QYZH.InteractiveMagazine.Models/Common/BusinessException.cs b/QYZH.InteractiveMagazine.Models/Common/BusinessException.cs new file mode 100644 index 0000000..39b7b20 --- /dev/null +++ b/QYZH.InteractiveMagazine.Models/Common/BusinessException.cs @@ -0,0 +1,46 @@ +namespace QYZH.InteractiveMagazine.Models.Common; + +/// +/// 业务异常类 +/// +public class BusinessException : Exception +{ + /// + /// 错误码 + /// + public int Code { get; set; } + + /// + /// 无参构造函数 + /// + public BusinessException() : base() + { + } + + /// + /// 构造函数 + /// + /// 异常消息 + public BusinessException(string message) : base(message) + { + } + + /// + /// 构造函数 + /// + /// 异常消息 + /// 错误码 + public BusinessException(string message, int code) : base(message) + { + Code = code; + } + + /// + /// 构造函数 + /// + /// 异常消息 + /// 内部异常 + public BusinessException(string message, Exception innerException) : base(message, innerException) + { + } +} diff --git a/QYZH.InteractiveMagazine.Models/Dto/BaseResponse.cs b/QYZH.InteractiveMagazine.Models/Dto/BaseResponse.cs new file mode 100644 index 0000000..23e569a --- /dev/null +++ b/QYZH.InteractiveMagazine.Models/Dto/BaseResponse.cs @@ -0,0 +1,55 @@ +namespace QYZH.InteractiveMagazine.Models.Dto; + +/// +/// 统一响应模型 +/// +/// 数据类型 +public class BaseResponse +{ + /// + /// 状态码 + /// + public int Code { get; set; } + + /// + /// 提示信息 + /// + public string? Message { get; set; } + + /// + /// 数据 + /// + public T? Data { get; set; } + + /// + /// 成功响应 + /// + /// 数据 + /// 提示信息 + /// 统一响应对象 + public static BaseResponse Success(T data, string message = "操作成功") + { + return new BaseResponse + { + Code = 200, + Message = message, + Data = data + }; + } + + /// + /// 失败响应 + /// + /// 提示信息 + /// 状态码 + /// 统一响应对象 + public static BaseResponse Fail(string message, int code = 500) + { + return new BaseResponse + { + Code = code, + Message = message, + Data = default + }; + } +} diff --git a/QYZH.InteractiveMagazine.Models/Dto/PageListModel.cs b/QYZH.InteractiveMagazine.Models/Dto/PageListModel.cs new file mode 100644 index 0000000..5c44d88 --- /dev/null +++ b/QYZH.InteractiveMagazine.Models/Dto/PageListModel.cs @@ -0,0 +1,33 @@ +namespace QYZH.InteractiveMagazine.Models.Dto; + +/// +/// 分页输出 +/// +/// 数据类型 +public class PageListModel +{ + /// + /// 页码 + /// + public int PageIndex { get; set; } + + /// + /// 每页条数 + /// + public int PageSize { get; set; } + + /// + /// 总记录数 + /// + public long TotalCount { get; set; } + + /// + /// 总页数 + /// + public int TotalPages => PageSize > 0 ? (int)Math.Ceiling((double)TotalCount / PageSize) : 0; + + /// + /// 数据列表 + /// + public List? List { get; set; } +} diff --git a/QYZH.InteractiveMagazine.Models/Dto/PageQueryModel.cs b/QYZH.InteractiveMagazine.Models/Dto/PageQueryModel.cs new file mode 100644 index 0000000..3231c4c --- /dev/null +++ b/QYZH.InteractiveMagazine.Models/Dto/PageQueryModel.cs @@ -0,0 +1,27 @@ +namespace QYZH.InteractiveMagazine.Models.Dto; + +/// +/// 分页查询输入 +/// +public class PageQueryModel +{ + /// + /// 页码,默认1 + /// + public int PageIndex { get; set; } = 1; + + /// + /// 每页条数,默认10 + /// + public int PageSize { get; set; } = 10; + + /// + /// 排序字段 + /// + public string? SortField { get; set; } + + /// + /// 排序方式(asc/desc) + /// + public string? SortOrder { get; set; } +} diff --git a/QYZH.InteractiveMagazine.Models/Entity/BaseEntity.cs b/QYZH.InteractiveMagazine.Models/Entity/BaseEntity.cs new file mode 100644 index 0000000..8d0678f --- /dev/null +++ b/QYZH.InteractiveMagazine.Models/Entity/BaseEntity.cs @@ -0,0 +1,43 @@ +using SqlSugar; + +namespace QYZH.InteractiveMagazine.Models.Entity; + +/// +/// 基础实体类 +/// +public abstract class BaseEntity +{ + /// + /// 主键Id + /// + [SugarColumn(IsPrimaryKey = true, IsIdentity = true)] + public long Id { get; set; } + + /// + /// 是否已删除 + /// + [SugarColumn(IsIgnore = false)] + public bool IsDeleted { get; set; } = false; + + /// + /// 创建人 + /// + [SugarColumn(Length = 50)] + public string? CreatedBy { get; set; } + + /// + /// 创建时间 + /// + public DateTime CreatedTime { get; set; } = DateTime.Now; + + /// + /// 更新人 + /// + [SugarColumn(Length = 50)] + public string? UpdatedBy { get; set; } + + /// + /// 更新时间 + /// + public DateTime UpdatedTime { get; set; } = DateTime.Now; +} diff --git a/QYZH.InteractiveMagazine.Models/QYZH.InteractiveMagazine.Models.csproj b/QYZH.InteractiveMagazine.Models/QYZH.InteractiveMagazine.Models.csproj new file mode 100644 index 0000000..33a2e64 --- /dev/null +++ b/QYZH.InteractiveMagazine.Models/QYZH.InteractiveMagazine.Models.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/QYZH.InteractiveMagazine.Models/Settings/JwtSettings.cs b/QYZH.InteractiveMagazine.Models/Settings/JwtSettings.cs new file mode 100644 index 0000000..ca8c2e9 --- /dev/null +++ b/QYZH.InteractiveMagazine.Models/Settings/JwtSettings.cs @@ -0,0 +1,27 @@ +namespace QYZH.InteractiveMagazine.Models.Settings; + +/// +/// JWT配置 +/// +public class JwtSettings +{ + /// + /// 颁发者 + /// + public string? Issuer { get; set; } + + /// + /// 接收者 + /// + public string? Audience { get; set; } + + /// + /// 密钥 + /// + public string? SecretKey { get; set; } + + /// + /// 过期时间(分钟) + /// + public int ExpiryMinutes { get; set; } +} diff --git a/QYZH.InteractiveMagazine.Models/Settings/RabbitMQSettings.cs b/QYZH.InteractiveMagazine.Models/Settings/RabbitMQSettings.cs new file mode 100644 index 0000000..b84332b --- /dev/null +++ b/QYZH.InteractiveMagazine.Models/Settings/RabbitMQSettings.cs @@ -0,0 +1,32 @@ +namespace QYZH.InteractiveMagazine.Models.Settings; + +/// +/// RabbitMQ配置 +/// +public class RabbitMQSettings +{ + /// + /// 主机地址 + /// + public string? HostName { get; set; } + + /// + /// 端口 + /// + public int Port { get; set; } + + /// + /// 用户名 + /// + public string? UserName { get; set; } + + /// + /// 密码 + /// + public string? Password { get; set; } + + /// + /// 虚拟主机 + /// + public string? VirtualHost { get; set; } +} diff --git a/QYZH.InteractiveMagazine.Models/Settings/RedisSettings.cs b/QYZH.InteractiveMagazine.Models/Settings/RedisSettings.cs new file mode 100644 index 0000000..c1871e4 --- /dev/null +++ b/QYZH.InteractiveMagazine.Models/Settings/RedisSettings.cs @@ -0,0 +1,17 @@ +namespace QYZH.InteractiveMagazine.Models.Settings; + +/// +/// Redis配置 +/// +public class RedisSettings +{ + /// + /// 连接字符串 + /// + public string? ConnectionString { get; set; } + + /// + /// 实例名称 + /// + public string? InstanceName { get; set; } +} diff --git a/QYZH.InteractiveMagazine.Models/Settings/WeChatSettings.cs b/QYZH.InteractiveMagazine.Models/Settings/WeChatSettings.cs new file mode 100644 index 0000000..94a827c --- /dev/null +++ b/QYZH.InteractiveMagazine.Models/Settings/WeChatSettings.cs @@ -0,0 +1,17 @@ +namespace QYZH.InteractiveMagazine.Models.Settings; + +/// +/// 微信小程序配置 +/// +public class WeChatSettings +{ + /// + /// 应用Id + /// + public string? AppId { get; set; } + + /// + /// 应用密钥 + /// + public string? AppSecret { get; set; } +} diff --git a/QYZH.InteractiveMagazine.Repository/BaseRepository.cs b/QYZH.InteractiveMagazine.Repository/BaseRepository.cs new file mode 100644 index 0000000..4045146 --- /dev/null +++ b/QYZH.InteractiveMagazine.Repository/BaseRepository.cs @@ -0,0 +1,149 @@ +using QYZH.InteractiveMagazine.Models.Dto; +using SqlSugar; +using System.Linq.Expressions; + +namespace QYZH.InteractiveMagazine.Repository; + +/// +/// 基础仓储实现 +/// +/// 实体类型 +public class BaseRepository : IBaseRepository where T : class, new() +{ + /// + /// SqlSugar 数据库实例 + /// + protected SqlSugarClient Db => SqlSugarDbContext.GetDb(); + + /// + /// 根据Id获取实体(自动过滤已删除数据) + /// + /// 主键Id + /// 实体对象 + public async Task GetByIdAsync(long id) + { + return await Db.Queryable().In(id).FirstAsync(); + } + + /// + /// 获取所有列表(自动过滤已删除数据) + /// + /// 实体列表 + public async Task> GetListAsync() + { + return await Db.Queryable().ToListAsync(); + } + + /// + /// 根据条件获取列表(自动过滤已删除数据) + /// + /// 查询条件 + /// 实体列表 + public async Task> GetListByWhereAsync(Expression> where) + { + return await Db.Queryable().Where(where).ToListAsync(); + } + + /// + /// 分页查询(自动过滤已删除数据) + /// + /// 查询条件 + /// 分页参数 + /// 分页结果 + public async Task> GetPageListAsync(Expression> where, PageQueryModel pageQuery) + { + RefAsync total = 0; + + var query = Db.Queryable().Where(where); + + // 处理排序 + if (!string.IsNullOrEmpty(pageQuery.SortField)) + { + var isAsc = string.IsNullOrEmpty(pageQuery.SortOrder) || + pageQuery.SortOrder.ToLower() == "asc"; + query = isAsc + ? query.OrderBy($"{pageQuery.SortField} asc") + : query.OrderBy($"{pageQuery.SortField} desc"); + } + + var list = await query.ToPageListAsync(pageQuery.PageIndex, pageQuery.PageSize, total); + + return new PageListModel + { + PageIndex = pageQuery.PageIndex, + PageSize = pageQuery.PageSize, + TotalCount = total, + List = list + }; + } + + /// + /// 插入单条记录 + /// + /// 实体对象 + /// 是否成功 + public async Task InsertAsync(T entity) + { + return await Db.Insertable(entity).ExecuteCommandAsync() > 0; + } + + /// + /// 批量插入记录 + /// + /// 实体列表 + /// 是否成功 + public async Task InsertRangeAsync(List entities) + { + return await Db.Insertable(entities).ExecuteCommandAsync() > 0; + } + + /// + /// 更新单条记录 + /// + /// 实体对象 + /// 是否成功 + public async Task UpdateAsync(T entity) + { + return await Db.Updateable(entity).ExecuteCommandAsync() > 0; + } + + /// + /// 批量更新记录 + /// + /// 实体列表 + /// 是否成功 + public async Task UpdateRangeAsync(List entities) + { + return await Db.Updateable(entities).ExecuteCommandAsync() > 0; + } + + /// + /// 根据Id删除记录(软删除,设置 IsDeleted = true) + /// + /// 主键Id + /// 是否成功 + public async Task DeleteByIdAsync(long id) + { + return await Db.Deleteable().In(id).IsLogic().ExecuteCommandAsync() > 0; + } + + /// + /// 根据条件删除记录(软删除,设置 IsDeleted = true) + /// + /// 删除条件 + /// 是否成功 + public async Task DeleteByWhereAsync(Expression> where) + { + return await Db.Deleteable().Where(where).IsLogic().ExecuteCommandAsync() > 0; + } + + /// + /// 根据条件获取记录数(自动过滤已删除数据) + /// + /// 查询条件 + /// 记录数 + public async Task GetCountAsync(Expression> where) + { + return await Db.Queryable().Where(where).CountAsync(); + } +} diff --git a/QYZH.InteractiveMagazine.Repository/IBaseRepository.cs b/QYZH.InteractiveMagazine.Repository/IBaseRepository.cs new file mode 100644 index 0000000..6366ab9 --- /dev/null +++ b/QYZH.InteractiveMagazine.Repository/IBaseRepository.cs @@ -0,0 +1,88 @@ +using QYZH.InteractiveMagazine.Models.Dto; +using System.Linq.Expressions; + +namespace QYZH.InteractiveMagazine.Repository; + +/// +/// 基础仓储接口 +/// +/// 实体类型 +public interface IBaseRepository where T : class, new() +{ + /// + /// 根据Id获取实体 + /// + /// 主键Id + /// 实体对象 + Task GetByIdAsync(long id); + + /// + /// 获取所有列表 + /// + /// 实体列表 + Task> GetListAsync(); + + /// + /// 根据条件获取列表 + /// + /// 查询条件 + /// 实体列表 + Task> GetListByWhereAsync(Expression> where); + + /// + /// 分页查询 + /// + /// 查询条件 + /// 分页参数 + /// 分页结果 + Task> GetPageListAsync(Expression> where, PageQueryModel pageQuery); + + /// + /// 插入单条记录 + /// + /// 实体对象 + /// 是否成功 + Task InsertAsync(T entity); + + /// + /// 批量插入记录 + /// + /// 实体列表 + /// 是否成功 + Task InsertRangeAsync(List entities); + + /// + /// 更新单条记录 + /// + /// 实体对象 + /// 是否成功 + Task UpdateAsync(T entity); + + /// + /// 批量更新记录 + /// + /// 实体列表 + /// 是否成功 + Task UpdateRangeAsync(List entities); + + /// + /// 根据Id删除记录(软删除) + /// + /// 主键Id + /// 是否成功 + Task DeleteByIdAsync(long id); + + /// + /// 根据条件删除记录(软删除) + /// + /// 删除条件 + /// 是否成功 + Task DeleteByWhereAsync(Expression> where); + + /// + /// 根据条件获取记录数 + /// + /// 查询条件 + /// 记录数 + Task GetCountAsync(Expression> where); +} diff --git a/QYZH.InteractiveMagazine.Repository/QYZH.InteractiveMagazine.Repository.csproj b/QYZH.InteractiveMagazine.Repository/QYZH.InteractiveMagazine.Repository.csproj new file mode 100644 index 0000000..0ed1119 --- /dev/null +++ b/QYZH.InteractiveMagazine.Repository/QYZH.InteractiveMagazine.Repository.csproj @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + net8.0 + enable + enable + + + diff --git a/QYZH.InteractiveMagazine.Repository/SqlSugarDbContext.cs b/QYZH.InteractiveMagazine.Repository/SqlSugarDbContext.cs new file mode 100644 index 0000000..b88ddb3 --- /dev/null +++ b/QYZH.InteractiveMagazine.Repository/SqlSugarDbContext.cs @@ -0,0 +1,99 @@ +using Microsoft.Extensions.Configuration; +using QYZH.InteractiveMagazine.Models.Entity; +using SqlSugar; + +namespace QYZH.InteractiveMagazine.Repository; + +/// +/// SqlSugar 数据库上下文封装(静态类) +/// +public static class SqlSugarDbContext +{ + /// + /// SqlSugarClient 实例 + /// + private static SqlSugarClient? _db; + + /// + /// 配置对象 + /// + private static IConfiguration? _configuration; + + /// + /// 初始化数据库上下文(在应用启动时调用一次) + /// + /// 配置对象 + public static void Init(IConfiguration configuration) + { + _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); + } + + /// + /// 获取 SqlSugarClient 实例 + /// + /// SqlSugarClient 实例 + public static SqlSugarClient GetDb() + { + if (_configuration == null) + { + throw new InvalidOperationException("请先调用 SqlSugarDbContext.Init(configuration) 进行初始化"); + } + + if (_db == null) + { + lock (typeof(SqlSugarDbContext)) + { + if (_db == null) + { + var connectionString = _configuration.GetConnectionString("DefaultConnection"); + if (string.IsNullOrWhiteSpace(connectionString)) + { + throw new InvalidOperationException("未找到连接字符串 DefaultConnection"); + } + + _db = new SqlSugarClient(new ConnectionConfig + { + ConnectionString = connectionString, + DbType = DbType.MySql, + IsAutoCloseConnection = true, + InitKeyType = InitKeyType.Attribute + }, + db => + { + // 配置软删除全局过滤(继承 BaseEntity 的实体都有效) + db.QueryFilter.AddTableFilter(it => it.IsDeleted == false); + + // 开启日志打印 + db.Aop.OnLogExecuting = (sql, pars) => + { + Console.WriteLine($"[SQL执行] {DateTime.Now:yyyy-MM-dd HH:mm:ss}"); + Console.WriteLine($"[SQL语句] {sql}"); + Console.WriteLine($"[SQL参数] {string.Join(", ", pars.Select(p => $"{p.ParameterName}={p.Value}"))}"); + Console.WriteLine(new string('-', 50)); + }; + }); + } + } + } + + return _db; + } + + /// + /// 代码优先初始化(可选) + /// + /// 实体类型数组 + public static void InitializeCodeFirst(params Type[] entityTypes) + { + var db = GetDb(); + + // 创建数据库(如果不存在) + db.DbMaintenance.CreateDatabase(); + + // 初始化表结构 + if (entityTypes != null && entityTypes.Length > 0) + { + db.CodeFirst.InitTables(entityTypes); + } + } +} diff --git a/QYZH.InteractiveMagazine.Service/AuthService.cs b/QYZH.InteractiveMagazine.Service/AuthService.cs new file mode 100644 index 0000000..eb4b569 --- /dev/null +++ b/QYZH.InteractiveMagazine.Service/AuthService.cs @@ -0,0 +1,173 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using QYZH.InteractiveMagazine.IService; +using QYZH.InteractiveMagazine.IService.Dto; +using QYZH.InteractiveMagazine.Models.Common; +using QYZH.InteractiveMagazine.Models.Settings; +using QYZH.InteractiveMagazine.Infrastructure.Auth; + +namespace QYZH.InteractiveMagazine.Service; + +/// +/// 认证服务实现 +/// +public class AuthService : IAuthService +{ + private readonly IConfiguration _configuration; + private readonly ILogger _logger; + + /// + /// 演示用管理员账号 + /// + private const string DemoAccount = "admin"; + + /// + /// 演示用管理员密码 + /// + private const string DemoPassword = "123456"; + + /// + /// 构造函数 + /// + /// 配置 + /// 日志记录器 + public AuthService(IConfiguration configuration, ILogger logger) + { + _configuration = configuration; + _logger = logger; + } + + /// + /// 用户登录 + /// + /// 账号 + /// 密码 + /// 登录结果 + public async Task LoginAsync(string account, string password) + { + _logger.LogInformation("用户登录尝试,账号: {Account}", account); + + if (string.IsNullOrWhiteSpace(account)) + { + throw new BusinessException("账号不能为空", 400); + } + + if (string.IsNullOrWhiteSpace(password)) + { + throw new BusinessException("密码不能为空", 400); + } + + // 演示版本:硬编码验证账号密码 + if (account != DemoAccount || password != DemoPassword) + { + _logger.LogWarning("用户登录失败,账号: {Account}", account); + throw new BusinessException("账号或密码错误", 401); + } + + // 获取JWT配置 + var jwtSettings = GetJwtSettings(); + + // 生成令牌 + var token = JwtHelper.GenerateToken(1, "管理员", jwtSettings); + var refreshToken = JwtHelper.GenerateToken(1, "管理员", jwtSettings); + + _logger.LogInformation("用户登录成功,账号: {Account}", account); + + return new LoginOutput + { + Token = token, + RefreshToken = refreshToken, + UserId = 1, + UserName = "管理员" + }; + } + + /// + /// 用户注册 + /// + /// 注册输入参数 + /// 是否成功 + public async Task RegisterAsync(RegisterInput input) + { + _logger.LogInformation("用户注册尝试,账号: {Account}", input?.Account); + + if (input == null) + { + throw new BusinessException("注册参数不能为空", 400); + } + + if (string.IsNullOrWhiteSpace(input.Account)) + { + throw new BusinessException("账号不能为空", 400); + } + + if (string.IsNullOrWhiteSpace(input.Password)) + { + throw new BusinessException("密码不能为空", 400); + } + + if (string.IsNullOrWhiteSpace(input.UserName)) + { + throw new BusinessException("用户名不能为空", 400); + } + + // 演示版本:模拟注册成功 + _logger.LogInformation("用户注册成功,账号: {Account}, 用户名: {UserName}", input.Account, input.UserName); + + return await Task.FromResult(true); + } + + /// + /// 刷新令牌 + /// + /// 刷新令牌 + /// 新的登录结果 + public async Task RefreshTokenAsync(string refreshToken) + { + _logger.LogInformation("令牌刷新尝试"); + + if (string.IsNullOrWhiteSpace(refreshToken)) + { + throw new BusinessException("刷新令牌不能为空", 400); + } + + var jwtSettings = GetJwtSettings(); + + // 生成新的令牌 + var newToken = JwtHelper.GenerateToken(1, "管理员", jwtSettings); + var newRefreshToken = JwtHelper.GenerateToken(1, "管理员", jwtSettings); + + _logger.LogInformation("令牌刷新成功"); + + return new LoginOutput + { + Token = newToken, + RefreshToken = newRefreshToken, + UserId = 1, + UserName = "管理员" + }; + } + + /// + /// 获取JWT配置 + /// + /// JWT配置对象 + private JwtSettings GetJwtSettings() + { + var jwtSettings = _configuration.GetSection("JwtSettings").Get() + ?? new JwtSettings + { + Issuer = "QYZH.InteractiveMagazine", + Audience = "QYZH.InteractiveMagazine.Client", + SecretKey = "QYZH_InteractiveMagazine_SecretKey_2024", + ExpiryMinutes = 120 + }; + + if (string.IsNullOrWhiteSpace(jwtSettings.SecretKey)) + { + throw new BusinessException("JWT配置不完整", 500); + } + + return jwtSettings; + } +} diff --git a/QYZH.InteractiveMagazine.Service/BaseService.cs b/QYZH.InteractiveMagazine.Service/BaseService.cs new file mode 100644 index 0000000..b4685f2 --- /dev/null +++ b/QYZH.InteractiveMagazine.Service/BaseService.cs @@ -0,0 +1,205 @@ +using Microsoft.Extensions.Logging; +using QYZH.InteractiveMagazine.IService; +using QYZH.InteractiveMagazine.Models.Common; +using QYZH.InteractiveMagazine.Models.Dto; +using QYZH.InteractiveMagazine.Models.Entity; +using QYZH.InteractiveMagazine.Repository; + +namespace QYZH.InteractiveMagazine.Service; + +/// +/// 基础服务实现 +/// +/// 实体类型 +public class BaseService : IBaseService where T : class, new() +{ + protected readonly IBaseRepository _repository; + protected readonly ILogger> _logger; + + /// + /// 构造函数 + /// + /// 基础仓储 + /// 日志记录器 + public BaseService(IBaseRepository repository, ILogger> logger) + { + _repository = repository; + _logger = logger; + } + + /// + /// 根据ID获取实体 + /// + /// 实体ID + /// 实体对象 + public async Task GetByIdAsync(long id) + { + _logger.LogInformation("正在获取实体,ID: {Id}", id); + + var entity = await _repository.GetByIdAsync(id); + + if (entity == null) + { + _logger.LogWarning("未找到实体,ID: {Id}", id); + throw new BusinessException($"未找到ID为{id}的记录", 404); + } + + return entity; + } + + /// + /// 获取所有实体列表 + /// + /// 实体列表 + public async Task> GetListAsync() + { + _logger.LogInformation("正在获取所有实体列表"); + return await _repository.GetListAsync(); + } + + /// + /// 获取分页列表 + /// + /// 分页查询参数 + /// 分页数据 + public async Task> GetPageListAsync(PageQueryModel pageQuery) + { + _logger.LogInformation("正在获取分页列表,页码: {PageIndex}, 每页条数: {PageSize}", pageQuery.PageIndex, pageQuery.PageSize); + + if (pageQuery.PageIndex <= 0) + { + throw new BusinessException("页码必须大于0", 400); + } + + if (pageQuery.PageSize <= 0 || pageQuery.PageSize > 100) + { + throw new BusinessException("每页条数必须在1-100之间", 400); + } + + return await _repository.GetPageListAsync(x => true, pageQuery); + } + + /// + /// 新增实体 + /// + /// 实体对象 + /// 是否成功 + public async Task InsertAsync(T entity) + { + if (entity == null) + { + throw new BusinessException("实体对象不能为空", 400); + } + + _logger.LogInformation("正在新增实体,类型: {EntityType}", typeof(T).Name); + + SetAuditFieldsOnInsert(entity); + + var result = await _repository.InsertAsync(entity); + + if (result) + { + _logger.LogInformation("实体新增成功"); + } + else + { + _logger.LogError("实体新增失败"); + throw new BusinessException("新增记录失败", 500); + } + + return result; + } + + /// + /// 更新实体 + /// + /// 实体对象 + /// 是否成功 + public async Task UpdateAsync(T entity) + { + if (entity == null) + { + throw new BusinessException("实体对象不能为空", 400); + } + + _logger.LogInformation("正在更新实体,类型: {EntityType}", typeof(T).Name); + + SetAuditFieldsOnUpdate(entity); + + var result = await _repository.UpdateAsync(entity); + + if (result) + { + _logger.LogInformation("实体更新成功"); + } + else + { + _logger.LogError("实体更新失败"); + throw new BusinessException("更新记录失败", 500); + } + + return result; + } + + /// + /// 根据ID删除实体 + /// + /// 实体ID + /// 是否成功 + public async Task DeleteByIdAsync(long id) + { + _logger.LogInformation("正在删除实体,ID: {Id}", id); + + var entity = await _repository.GetByIdAsync(id); + + if (entity == null) + { + _logger.LogWarning("未找到要删除的实体,ID: {Id}", id); + throw new BusinessException($"未找到ID为{id}的记录", 404); + } + + var result = await _repository.DeleteByIdAsync(id); + + if (result) + { + _logger.LogInformation("实体删除成功,ID: {Id}", id); + } + else + { + _logger.LogError("实体删除失败,ID: {Id}", id); + throw new BusinessException("删除记录失败", 500); + } + + return result; + } + + /// + /// 设置插入时的审计字段 + /// + /// 实体对象 + private static void SetAuditFieldsOnInsert(T entity) + { + if (entity is BaseEntity baseEntity) + { + var now = DateTime.Now; + baseEntity.CreatedTime = now; + baseEntity.UpdatedTime = now; + baseEntity.CreatedBy = baseEntity.CreatedBy ?? "system"; + baseEntity.UpdatedBy = baseEntity.UpdatedBy ?? "system"; + baseEntity.IsDeleted = false; + } + } + + /// + /// 设置更新时的审计字段 + /// + /// 实体对象 + private static void SetAuditFieldsOnUpdate(T entity) + { + if (entity is BaseEntity baseEntity) + { + baseEntity.UpdatedTime = DateTime.Now; + baseEntity.UpdatedBy = baseEntity.UpdatedBy ?? "system"; + } + } +} diff --git a/QYZH.InteractiveMagazine.Service/QYZH.InteractiveMagazine.Service.csproj b/QYZH.InteractiveMagazine.Service/QYZH.InteractiveMagazine.Service.csproj new file mode 100644 index 0000000..c4f7b10 --- /dev/null +++ b/QYZH.InteractiveMagazine.Service/QYZH.InteractiveMagazine.Service.csproj @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + net8.0 + enable + enable + + + diff --git a/QYZH.InteractiveMagazine.Service/WeChatMiniProgramService.cs b/QYZH.InteractiveMagazine.Service/WeChatMiniProgramService.cs new file mode 100644 index 0000000..2f586d1 --- /dev/null +++ b/QYZH.InteractiveMagazine.Service/WeChatMiniProgramService.cs @@ -0,0 +1,167 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using QYZH.InteractiveMagazine.Common.Helpers; +using QYZH.InteractiveMagazine.IService; +using QYZH.InteractiveMagazine.IService.Dto; +using QYZH.InteractiveMagazine.Models.Common; +using QYZH.InteractiveMagazine.Models.Settings; +using QYZH.InteractiveMagazine.Infrastructure.Auth; + +namespace QYZH.InteractiveMagazine.Service; + +/// +/// 微信小程序服务实现 +/// +public class WeChatMiniProgramService : IWeChatMiniProgramService +{ + private readonly IConfiguration _configuration; + private readonly ILogger _logger; + private const string Code2SessionUrl = "https://api.weixin.qq.com/sns/jscode2session"; + private const string GetPhoneNumberUrl = "https://api.weixin.qq.com/wxa/business/getuserphonenumber"; + + /// + /// 构造函数 + /// + /// 配置 + /// 日志记录器 + public WeChatMiniProgramService(IConfiguration configuration, ILogger logger) + { + _configuration = configuration; + _logger = logger; + } + + /// + /// 微信登录 + /// + /// 微信登录凭证 + /// 微信登录结果 + public async Task WeChatLoginAsync(string code) + { + _logger.LogInformation("微信登录尝试,code: {Code}", code); + + if (string.IsNullOrWhiteSpace(code)) + { + throw new BusinessException("登录凭证不能为空", 400); + } + + // 获取微信配置 + var weChatSettings = GetWeChatSettings(); + + if (string.IsNullOrWhiteSpace(weChatSettings.AppId) || string.IsNullOrWhiteSpace(weChatSettings.AppSecret)) + { + throw new BusinessException("微信配置不完整", 500); + } + + // 演示版本:模拟调用微信code2session接口 + var openId = await SimulateCode2SessionAsync(code, weChatSettings); + + // 获取JWT配置并生成令牌 + var jwtSettings = GetJwtSettings(); + var token = JwtHelper.GenerateToken(1, "微信用户", jwtSettings); + + _logger.LogInformation("微信登录成功,openId: {OpenId}", openId); + + return new WeChatLoginOutput + { + Token = token, + UserId = 1, + UserName = "微信用户", + OpenId = openId + }; + } + + /// + /// 获取手机号 + /// + /// 获取手机号凭证 + /// 手机号信息 + public async Task GetPhoneNumberAsync(string code) + { + _logger.LogInformation("获取微信手机号尝试,code: {Code}", code); + + if (string.IsNullOrWhiteSpace(code)) + { + throw new BusinessException("获取手机号凭证不能为空", 400); + } + + // 演示版本:模拟返回手机号 + var phoneNumber = await SimulateGetPhoneNumberAsync(code); + + _logger.LogInformation("获取微信手机号成功"); + + return new WeChatPhoneNumberOutput + { + PhoneNumber = phoneNumber + }; + } + + /// + /// 模拟调用微信code2session接口 + /// + /// 登录凭证 + /// 微信配置 + /// openId + private async Task SimulateCode2SessionAsync(string code, WeChatSettings weChatSettings) + { + // 演示版本:模拟返回openId + // 实际实现应调用微信API: + // var url = $"{Code2SessionUrl}?appid={weChatSettings.AppId}&secret={weChatSettings.AppSecret}&js_code={code}&grant_type=authorization_code"; + // var response = await HttpHelper.GetAsync(url); + // if (response?.errcode == 0) return response.openid; + + await Task.Delay(100); // 模拟网络请求延迟 + + return $"demo_openid_{code.GetHashCode():X}"; + } + + /// + /// 模拟调用微信获取手机号接口 + /// + /// 获取手机号凭证 + /// 手机号 + private async Task SimulateGetPhoneNumberAsync(string code) + { + // 演示版本:模拟返回手机号 + // 实际实现应调用微信API获取access_token,然后调用获取手机号接口: + // var tokenUrl = $"https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={appId}&secret={appSecret}"; + // var tokenResponse = await HttpHelper.GetAsync(tokenUrl); + // var accessToken = tokenResponse.access_token; + // var phoneUrl = $"{GetPhoneNumberUrl}?access_token={accessToken}"; + // var phoneResponse = await HttpHelper.PostAsync(phoneUrl, new { code }); + + await Task.Delay(100); // 模拟网络请求延迟 + + return "13800138000"; + } + + /// + /// 获取微信配置 + /// + /// 微信配置对象 + private WeChatSettings GetWeChatSettings() + { + return _configuration.GetSection("WeChatSettings").Get() + ?? new WeChatSettings + { + AppId = "demo_app_id", + AppSecret = "demo_app_secret" + }; + } + + /// + /// 获取JWT配置 + /// + /// JWT配置对象 + private JwtSettings GetJwtSettings() + { + return _configuration.GetSection("JwtSettings").Get() + ?? new JwtSettings + { + Issuer = "QYZH.InteractiveMagazine", + Audience = "QYZH.InteractiveMagazine.Client", + SecretKey = "QYZH_InteractiveMagazine_SecretKey_2024", + ExpiryMinutes = 120 + }; + } +} diff --git a/QYZH.InteractiveMagazine.WebApi/Controllers/AuthController.cs b/QYZH.InteractiveMagazine.WebApi/Controllers/AuthController.cs new file mode 100644 index 0000000..b174f22 --- /dev/null +++ b/QYZH.InteractiveMagazine.WebApi/Controllers/AuthController.cs @@ -0,0 +1,61 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using QYZH.InteractiveMagazine.IService; +using QYZH.InteractiveMagazine.IService.Dto; +using QYZH.InteractiveMagazine.Models.Dto; + +namespace QYZH.InteractiveMagazine.WebApi.Controllers; + +/// +/// 认证控制器 +/// +[Route("api/[controller]")] +[ApiController] +public class AuthController : BaseController +{ + private readonly IAuthService _authService; + + public AuthController(IAuthService authService) + { + _authService = authService; + } + + /// + /// 用户登录 + /// + /// 登录信息 + /// 登录结果 + [AllowAnonymous] + [HttpPost("login")] + public async Task> LoginAsync([FromBody] LoginInput input) + { + var result = await _authService.LoginAsync(input.Account, input.Password); + return Success(result); + } + + /// + /// 用户注册 + /// + /// 注册信息 + /// 是否成功 + [AllowAnonymous] + [HttpPost("register")] + public async Task> RegisterAsync([FromBody] RegisterInput input) + { + var result = await _authService.RegisterAsync(input); + return Success(result, "注册成功"); + } + + /// + /// 刷新令牌 + /// + /// 刷新令牌 + /// 新的登录结果 + [AllowAnonymous] + [HttpPost("refreshToken")] + public async Task> RefreshTokenAsync([FromQuery] string refreshToken) + { + var result = await _authService.RefreshTokenAsync(refreshToken); + return Success(result); + } +} diff --git a/QYZH.InteractiveMagazine.WebApi/Controllers/BaseController.cs b/QYZH.InteractiveMagazine.WebApi/Controllers/BaseController.cs new file mode 100644 index 0000000..e551629 --- /dev/null +++ b/QYZH.InteractiveMagazine.WebApi/Controllers/BaseController.cs @@ -0,0 +1,58 @@ +using Microsoft.AspNetCore.Mvc; +using QYZH.InteractiveMagazine.Models.Dto; + +namespace QYZH.InteractiveMagazine.WebApi.Controllers; + +/// +/// 基础控制器 +/// +[ApiController] +[Route("api/[controller]")] +public abstract class BaseController : ControllerBase +{ + /// + /// 获取当前用户ID + /// + /// 用户ID + protected long? GetCurrentUserId() + { + var userIdClaim = User.Claims.FirstOrDefault(c => c.Type == "userId"); + if (userIdClaim != null && long.TryParse(userIdClaim.Value, out var userId)) + { + return userId; + } + return null; + } + + /// + /// 获取当前用户名 + /// + /// 用户名 + protected string? GetCurrentUserName() + { + return User.Claims.FirstOrDefault(c => c.Type == "userName")?.Value; + } + + /// + /// 成功响应 + /// + /// 数据类型 + /// 数据 + /// 提示信息 + /// 统一响应对象 + protected BaseResponse Success(T data, string message = "操作成功") + { + return BaseResponse.Success(data, message); + } + + /// + /// 失败响应 + /// + /// 提示信息 + /// 状态码 + /// 统一响应对象 + protected BaseResponse Fail(string message, int code = 500) + { + return BaseResponse.Fail(message, code); + } +} diff --git a/QYZH.InteractiveMagazine.WebApi/Controllers/HealthController.cs b/QYZH.InteractiveMagazine.WebApi/Controllers/HealthController.cs new file mode 100644 index 0000000..3d07fee --- /dev/null +++ b/QYZH.InteractiveMagazine.WebApi/Controllers/HealthController.cs @@ -0,0 +1,22 @@ +using Microsoft.AspNetCore.Mvc; +using QYZH.InteractiveMagazine.Models.Dto; + +namespace QYZH.InteractiveMagazine.WebApi.Controllers; + +/// +/// 健康检查控制器 +/// +[Route("api/[controller]")] +[ApiController] +public class HealthController : BaseController +{ + /// + /// 健康检查 + /// + /// pong + [HttpGet("ping")] + public BaseResponse Ping() + { + return Success("pong"); + } +} diff --git a/QYZH.InteractiveMagazine.WebApi/Controllers/WeChatController.cs b/QYZH.InteractiveMagazine.WebApi/Controllers/WeChatController.cs new file mode 100644 index 0000000..a468cc9 --- /dev/null +++ b/QYZH.InteractiveMagazine.WebApi/Controllers/WeChatController.cs @@ -0,0 +1,35 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using QYZH.InteractiveMagazine.IService; +using QYZH.InteractiveMagazine.IService.Dto; +using QYZH.InteractiveMagazine.Models.Dto; + +namespace QYZH.InteractiveMagazine.WebApi.Controllers; + +/// +/// 微信小程序控制器 +/// +[Route("api/[controller]")] +[ApiController] +[AllowAnonymous] +public class WeChatController : BaseController +{ + private readonly IWeChatMiniProgramService _weChatService; + + public WeChatController(IWeChatMiniProgramService weChatService) + { + _weChatService = weChatService; + } + + /// + /// 微信登录 + /// + /// 微信登录凭证 + /// 微信登录结果 + [HttpPost("login")] + public async Task> WeChatLoginAsync([FromQuery] string code) + { + var result = await _weChatService.WeChatLoginAsync(code); + return Success(result); + } +} diff --git a/QYZH.InteractiveMagazine.WebApi/Program.cs b/QYZH.InteractiveMagazine.WebApi/Program.cs new file mode 100644 index 0000000..4267f96 --- /dev/null +++ b/QYZH.InteractiveMagazine.WebApi/Program.cs @@ -0,0 +1,64 @@ +using QYZH.InteractiveMagazine.Infrastructure.Extensions; +using QYZH.InteractiveMagazine.Infrastructure.Middleware; +using QYZH.InteractiveMagazine.Repository; +using Serilog; + +var builder = WebApplication.CreateBuilder(args); + +// 配置Serilog +Log.Logger = new LoggerConfiguration() + .ReadFrom.Configuration(builder.Configuration) + .Enrich.FromLogContext() + .WriteTo.Console() + .CreateLogger(); + +builder.Host.UseSerilog(); + +// 注册服务 +builder.Services.AddControllers(); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +// 基础设施服务注册(JWT、Redis、RabbitMQ) +builder.Services.AddInfrastructureServices(builder.Configuration); + +// 注册中间件 +builder.Services.AddTransient(); +builder.Services.AddTransient(); + +// 注册业务服务 +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +// 初始化SqlSugar +SqlSugarDbContext.Init(builder.Configuration); + +// 添加CORS +builder.Services.AddCors(options => +{ + options.AddPolicy("AllowAll", policy => + { + policy.AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader(); + }); +}); + +var app = builder.Build(); + +// 配置HTTP请求管道 +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); +app.UseCors("AllowAll"); +app.UseMiddleware(); +app.UseMiddleware(); +app.UseAuthentication(); +app.UseAuthorization(); +app.MapControllers(); + +app.Run(); diff --git a/QYZH.InteractiveMagazine.WebApi/Properties/launchSettings.json b/QYZH.InteractiveMagazine.WebApi/Properties/launchSettings.json new file mode 100644 index 0000000..8560a71 --- /dev/null +++ b/QYZH.InteractiveMagazine.WebApi/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:51130", + "sslPort": 44366 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5197", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7252;http://localhost:5197", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/QYZH.InteractiveMagazine.WebApi/QYZH.InteractiveMagazine.WebApi.csproj b/QYZH.InteractiveMagazine.WebApi/QYZH.InteractiveMagazine.WebApi.csproj new file mode 100644 index 0000000..ffb94cf --- /dev/null +++ b/QYZH.InteractiveMagazine.WebApi/QYZH.InteractiveMagazine.WebApi.csproj @@ -0,0 +1,22 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + + + diff --git a/QYZH.InteractiveMagazine.WebApi/QYZH.InteractiveMagazine.WebApi.http b/QYZH.InteractiveMagazine.WebApi/QYZH.InteractiveMagazine.WebApi.http new file mode 100644 index 0000000..6633dbc --- /dev/null +++ b/QYZH.InteractiveMagazine.WebApi/QYZH.InteractiveMagazine.WebApi.http @@ -0,0 +1,6 @@ +@QYZH.InteractiveMagazine.WebApi_HostAddress = http://localhost:5197 + +GET {{QYZH.InteractiveMagazine.WebApi_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/QYZH.InteractiveMagazine.WebApi/appsettings.Development.json b/QYZH.InteractiveMagazine.WebApi/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/QYZH.InteractiveMagazine.WebApi/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/QYZH.InteractiveMagazine.WebApi/appsettings.json b/QYZH.InteractiveMagazine.WebApi/appsettings.json new file mode 100644 index 0000000..c6c73cc --- /dev/null +++ b/QYZH.InteractiveMagazine.WebApi/appsettings.json @@ -0,0 +1,48 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Server=localhost;Database=interactive_magazine;Uid=root;Pwd=root;Charset=utf8mb4;" + }, + "JwtSettings": { + "Issuer": "QYZH.InteractiveMagazine", + "Audience": "QYZH.InteractiveMagazine.Client", + "SecretKey": "your-256-bit-secret-key-here-change-in-production", + "ExpiryMinutes": 120 + }, + "RedisSettings": { + "ConnectionString": "localhost:6379", + "InstanceName": "interactive_magazine" + }, + "RabbitMQSettings": { + "HostName": "localhost", + "Port": 5672, + "UserName": "guest", + "Password": "guest", + "VirtualHost": "/" + }, + "WeChatSettings": { + "AppId": "your-wechat-appid", + "AppSecret": "your-wechat-appsecret" + }, + "Serilog": { + "MinimumLevel": { + "Default": "Information", + "Override": { + "Microsoft": "Warning", + "System": "Warning" + } + }, + "WriteTo": [ + { + "Name": "Console" + }, + { + "Name": "File", + "Args": { + "path": "logs/log-.txt", + "rollingInterval": "Day" + } + } + ] + }, + "AllowedHosts": "*" +} diff --git a/QYZH.InteractiveMagazine.slnx b/QYZH.InteractiveMagazine.slnx new file mode 100644 index 0000000..76e4735 --- /dev/null +++ b/QYZH.InteractiveMagazine.slnx @@ -0,0 +1,9 @@ + + + + + + + + +