添加项目文件。
This commit is contained in:
329
.trae/documents/企业级框架搭建计划.md
Normal file
329
.trae/documents/企业级框架搭建计划.md
Normal file
@ -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<T>
|
||||||
|
{
|
||||||
|
public int Code { get; set; }
|
||||||
|
public string Message { get; set; }
|
||||||
|
public T Data { get; set; }
|
||||||
|
|
||||||
|
public static BaseResponse<T> Success(T data, string message = "操作成功")
|
||||||
|
public static BaseResponse<T> 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<T>`
|
||||||
|
- 增删改查基础方法
|
||||||
|
- 分页查询方法
|
||||||
|
|
||||||
|
#### 5.3 创建基础仓储实现 `BaseRepository<T>`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 阶段六:构建基础设施层 (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<T>`
|
||||||
|
- 通用业务操作方法
|
||||||
|
|
||||||
|
#### 7.2 创建基础服务实现 `BaseService<T>`
|
||||||
|
- 事务管理
|
||||||
|
- 业务逻辑封装
|
||||||
|
|
||||||
|
#### 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<T> | 统一格式,前端易处理 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、最终项目文件树(核心文件)
|
||||||
|
|
||||||
|
```
|
||||||
|
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
|
||||||
|
```
|
||||||
6
QYZH.InteractiveMagazine.Common/Class1.cs
Normal file
6
QYZH.InteractiveMagazine.Common/Class1.cs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
namespace QYZH.InteractiveMagazine.Common;
|
||||||
|
|
||||||
|
public class Class1
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,88 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace QYZH.InteractiveMagazine.Common.Extensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 日期时间扩展方法
|
||||||
|
/// </summary>
|
||||||
|
public static class DateTimeExtension
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 转换为Unix时间戳(秒)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dateTime">日期时间</param>
|
||||||
|
/// <returns>Unix时间戳</returns>
|
||||||
|
public static long ToTimestamp(this DateTime dateTime)
|
||||||
|
{
|
||||||
|
return new DateTimeOffset(dateTime.ToUniversalTime()).ToUnixTimeSeconds();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从Unix时间戳(秒)转换为DateTime
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="timestamp">Unix时间戳</param>
|
||||||
|
/// <returns>日期时间</returns>
|
||||||
|
public static DateTime FromTimestamp(long timestamp)
|
||||||
|
{
|
||||||
|
return DateTimeOffset.FromUnixTimeSeconds(timestamp).LocalDateTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 转换为指定格式的日期时间字符串
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dateTime">日期时间</param>
|
||||||
|
/// <param name="format">格式字符串,默认为"yyyy-MM-dd HH:mm:ss"</param>
|
||||||
|
/// <returns>格式化的日期时间字符串</returns>
|
||||||
|
public static string ToDateTimeString(this DateTime dateTime, string format = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
{
|
||||||
|
return dateTime.ToString(format);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从生日计算年龄
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="birthday">生日日期</param>
|
||||||
|
/// <returns>年龄</returns>
|
||||||
|
public static int GetAge(this DateTime birthday)
|
||||||
|
{
|
||||||
|
int age = DateTime.Now.Year - birthday.Year;
|
||||||
|
|
||||||
|
if (DateTime.Now.DayOfYear < birthday.DayOfYear)
|
||||||
|
{
|
||||||
|
age--;
|
||||||
|
}
|
||||||
|
|
||||||
|
return age;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断是否是今天
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dateTime">日期时间</param>
|
||||||
|
/// <returns>是否是今天</returns>
|
||||||
|
public static bool IsToday(this DateTime dateTime)
|
||||||
|
{
|
||||||
|
return dateTime.Date == DateTime.Today;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取当天开始时间(00:00:00)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dateTime">日期时间</param>
|
||||||
|
/// <returns>当天开始时间</returns>
|
||||||
|
public static DateTime ToStartOfDay(this DateTime dateTime)
|
||||||
|
{
|
||||||
|
return dateTime.Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取当天结束时间(23:59:59.999)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dateTime">日期时间</param>
|
||||||
|
/// <returns>当天结束时间</returns>
|
||||||
|
public static DateTime ToEndOfDay(this DateTime dateTime)
|
||||||
|
{
|
||||||
|
return dateTime.Date.AddDays(1).AddTicks(-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
79
QYZH.InteractiveMagazine.Common/Extensions/EnumExtension.cs
Normal file
79
QYZH.InteractiveMagazine.Common/Extensions/EnumExtension.cs
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace QYZH.InteractiveMagazine.Common.Extensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 枚举扩展方法
|
||||||
|
/// </summary>
|
||||||
|
public static class EnumExtension
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取枚举的DescriptionAttribute描述
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">枚举类型</typeparam>
|
||||||
|
/// <param name="enumValue">枚举值</param>
|
||||||
|
/// <returns>描述文本,如果没有DescriptionAttribute则返回枚举名称</returns>
|
||||||
|
public static string GetDescription<T>(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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取枚举名称
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">枚举类型</typeparam>
|
||||||
|
/// <param name="enumValue">枚举值</param>
|
||||||
|
/// <returns>枚举名称</returns>
|
||||||
|
public static string GetName<T>(this T enumValue) where T : Enum
|
||||||
|
{
|
||||||
|
return enumValue.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取所有枚举名称列表
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">枚举类型</typeparam>
|
||||||
|
/// <returns>枚举名称列表</returns>
|
||||||
|
public static List<string> GetNames<T>() where T : Enum
|
||||||
|
{
|
||||||
|
return Enum.GetNames(typeof(T)).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取所有枚举描述列表
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">枚举类型</typeparam>
|
||||||
|
/// <returns>枚举描述列表</returns>
|
||||||
|
public static List<string> GetDescriptions<T>() where T : Enum
|
||||||
|
{
|
||||||
|
return Enum.GetValues(typeof(T))
|
||||||
|
.Cast<T>()
|
||||||
|
.Select(e => e.GetDescription())
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将枚举转换为字典(名称,值)
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">枚举类型</typeparam>
|
||||||
|
/// <returns>枚举字典</returns>
|
||||||
|
public static Dictionary<string, int> ToDictionary<T>() where T : Enum
|
||||||
|
{
|
||||||
|
return Enum.GetValues(typeof(T))
|
||||||
|
.Cast<T>()
|
||||||
|
.ToDictionary(e => e.GetDescription(), e => Convert.ToInt32(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,94 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Reflection;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace QYZH.InteractiveMagazine.Common.Extensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 对象扩展方法
|
||||||
|
/// </summary>
|
||||||
|
public static class ObjectExtension
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 将源对象的属性值拷贝到目标对象
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">目标对象类型</typeparam>
|
||||||
|
/// <param name="source">源对象</param>
|
||||||
|
/// <param name="target">目标对象</param>
|
||||||
|
/// <returns>目标对象</returns>
|
||||||
|
public static T CopyTo<T>(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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将对象转换为JSON字符串
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="obj">对象</param>
|
||||||
|
/// <param name="formatting">格式化选项</param>
|
||||||
|
/// <returns>JSON字符串</returns>
|
||||||
|
public static string ToJson(this object obj, Formatting formatting = Formatting.None)
|
||||||
|
{
|
||||||
|
if (obj == null)
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
return JsonConvert.SerializeObject(obj, formatting);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将对象转换为字典
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="obj">对象</param>
|
||||||
|
/// <returns>字典</returns>
|
||||||
|
public static Dictionary<string, object?> ToDictionary(this object obj)
|
||||||
|
{
|
||||||
|
if (obj == null)
|
||||||
|
{
|
||||||
|
return new Dictionary<string, object?>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var dictionary = new Dictionary<string, object?>();
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
150
QYZH.InteractiveMagazine.Common/Extensions/StringExtension.cs
Normal file
150
QYZH.InteractiveMagazine.Common/Extensions/StringExtension.cs
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
using System;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace QYZH.InteractiveMagazine.Common.Extensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 字符串扩展方法
|
||||||
|
/// </summary>
|
||||||
|
public static class StringExtension
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// MD5加密
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">原始字符串</param>
|
||||||
|
/// <returns>MD5哈希值(32位小写)</returns>
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// SHA256加密
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">原始字符串</param>
|
||||||
|
/// <returns>SHA256哈希值(64位小写)</returns>
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Base64编码
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">原始字符串</param>
|
||||||
|
/// <returns>Base64编码后的字符串</returns>
|
||||||
|
public static string ToBase64(this string input)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(input))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] bytes = Encoding.UTF8.GetBytes(input);
|
||||||
|
return Convert.ToBase64String(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Base64解码
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">Base64编码的字符串</param>
|
||||||
|
/// <returns>解码后的原始字符串</returns>
|
||||||
|
public static string FromBase64(this string input)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(input))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] bytes = Convert.FromBase64String(input);
|
||||||
|
return Encoding.UTF8.GetString(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断字符串是否为空或空白
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">待检查的字符串</param>
|
||||||
|
/// <returns>是否为空或空白</returns>
|
||||||
|
public static bool IsNullOrWhiteSpace(this string input)
|
||||||
|
{
|
||||||
|
return string.IsNullOrWhiteSpace(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 手机号脱敏
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="phone">手机号</param>
|
||||||
|
/// <returns>脱敏后的手机号(中间4位用*替换)</returns>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 身份证脱敏
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="idCard">身份证号</param>
|
||||||
|
/// <returns>脱敏后的身份证号(保留前3位和后4位)</returns>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 正则匹配
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">待匹配的字符串</param>
|
||||||
|
/// <param name="pattern">正则表达式</param>
|
||||||
|
/// <returns>是否匹配</returns>
|
||||||
|
public static bool IsMatchRegex(this string input, string pattern)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(input) || string.IsNullOrEmpty(pattern))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Regex.IsMatch(input, pattern);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
110
QYZH.InteractiveMagazine.Common/Helpers/EnumHelper.cs
Normal file
110
QYZH.InteractiveMagazine.Common/Helpers/EnumHelper.cs
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace QYZH.InteractiveMagazine.Common.Helpers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 枚举工具类
|
||||||
|
/// </summary>
|
||||||
|
public static class EnumHelper
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 根据枚举名称获取枚举值
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TEnum">枚举类型</typeparam>
|
||||||
|
/// <param name="name">枚举名称</param>
|
||||||
|
/// <returns>枚举值</returns>
|
||||||
|
public static TEnum GetValue<TEnum>(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}' 的值");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据枚举值获取枚举名称
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TEnum">枚举类型</typeparam>
|
||||||
|
/// <param name="value">枚举值</param>
|
||||||
|
/// <returns>枚举名称</returns>
|
||||||
|
public static string GetName<TEnum>(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}'");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取枚举的所有值
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TEnum">枚举类型</typeparam>
|
||||||
|
/// <returns>枚举值列表</returns>
|
||||||
|
public static List<TEnum> GetAllValues<TEnum>() where TEnum : Enum
|
||||||
|
{
|
||||||
|
return Enum.GetValues(typeof(TEnum)).Cast<TEnum>().ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据Description获取枚举值
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TEnum">枚举类型</typeparam>
|
||||||
|
/// <param name="description">描述文本</param>
|
||||||
|
/// <returns>枚举值</returns>
|
||||||
|
public static TEnum GetValueByDescription<TEnum>(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}' 的值");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取枚举的DescriptionAttribute描述
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TEnum">枚举类型</typeparam>
|
||||||
|
/// <param name="value">枚举值</param>
|
||||||
|
/// <returns>描述文本</returns>
|
||||||
|
public static string GetDescription<TEnum>(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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
103
QYZH.InteractiveMagazine.Common/Helpers/HttpHelper.cs
Normal file
103
QYZH.InteractiveMagazine.Common/Helpers/HttpHelper.cs
Normal file
@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// HTTP请求工具类
|
||||||
|
/// </summary>
|
||||||
|
public static class HttpHelper
|
||||||
|
{
|
||||||
|
private static readonly HttpClient _httpClient = new HttpClient();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 设置默认请求头
|
||||||
|
/// </summary>
|
||||||
|
static HttpHelper()
|
||||||
|
{
|
||||||
|
_httpClient.Timeout = TimeSpan.FromSeconds(30);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 发送GET请求
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="url">请求地址</param>
|
||||||
|
/// <returns>响应内容字符串</returns>
|
||||||
|
public static async Task<string> GetAsync(string url)
|
||||||
|
{
|
||||||
|
HttpResponseMessage response = await _httpClient.GetAsync(url);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
return await response.Content.ReadAsStringAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 发送GET请求并反序列化为指定类型
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">响应类型</typeparam>
|
||||||
|
/// <param name="url">请求地址</param>
|
||||||
|
/// <returns>反序列化后的对象</returns>
|
||||||
|
public static async Task<T?> GetAsync<T>(string url)
|
||||||
|
{
|
||||||
|
string json = await GetAsync(url);
|
||||||
|
return JsonConvert.DeserializeObject<T>(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 发送POST请求
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="url">请求地址</param>
|
||||||
|
/// <param name="data">请求数据</param>
|
||||||
|
/// <returns>响应内容字符串</returns>
|
||||||
|
public static async Task<string> 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 发送POST请求并反序列化为指定类型
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">响应类型</typeparam>
|
||||||
|
/// <param name="url">请求地址</param>
|
||||||
|
/// <param name="data">请求数据</param>
|
||||||
|
/// <returns>反序列化后的对象</returns>
|
||||||
|
public static async Task<T?> PostAsync<T>(string url, object data)
|
||||||
|
{
|
||||||
|
string json = await PostAsync(url, data);
|
||||||
|
return JsonConvert.DeserializeObject<T>(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 发送DELETE请求
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="url">请求地址</param>
|
||||||
|
/// <returns>响应内容字符串</returns>
|
||||||
|
public static async Task<string> DeleteAsync(string url)
|
||||||
|
{
|
||||||
|
HttpResponseMessage response = await _httpClient.DeleteAsync(url);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
return await response.Content.ReadAsStringAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 发送PUT请求
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="url">请求地址</param>
|
||||||
|
/// <param name="data">请求数据</param>
|
||||||
|
/// <returns>响应内容字符串</returns>
|
||||||
|
public static async Task<string> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
91
QYZH.InteractiveMagazine.Common/Helpers/JsonHelper.cs
Normal file
91
QYZH.InteractiveMagazine.Common/Helpers/JsonHelper.cs
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace QYZH.InteractiveMagazine.Common.Helpers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// JSON工具类
|
||||||
|
/// </summary>
|
||||||
|
public static class JsonHelper
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// JSON序列化设置
|
||||||
|
/// </summary>
|
||||||
|
private static readonly JsonSerializerSettings _settings = new JsonSerializerSettings
|
||||||
|
{
|
||||||
|
NullValueHandling = NullValueHandling.Ignore,
|
||||||
|
DateFormatString = "yyyy-MM-dd HH:mm:ss",
|
||||||
|
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将对象序列化为JSON字符串
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="obj">对象</param>
|
||||||
|
/// <returns>JSON字符串</returns>
|
||||||
|
public static string Serialize(object obj)
|
||||||
|
{
|
||||||
|
if (obj == null)
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
return JsonConvert.SerializeObject(obj, _settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将对象序列化为格式化的JSON字符串
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="obj">对象</param>
|
||||||
|
/// <returns>格式化的JSON字符串</returns>
|
||||||
|
public static string SerializePretty(object obj)
|
||||||
|
{
|
||||||
|
if (obj == null)
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
return JsonConvert.SerializeObject(obj, Formatting.Indented, _settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将JSON字符串反序列化为指定类型
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">目标类型</typeparam>
|
||||||
|
/// <param name="json">JSON字符串</param>
|
||||||
|
/// <returns>反序列化后的对象</returns>
|
||||||
|
public static T? Deserialize<T>(string json)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(json))
|
||||||
|
{
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
return JsonConvert.DeserializeObject<T>(json, _settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将JSON字符串反序列化为指定类型(带异常处理)
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">目标类型</typeparam>
|
||||||
|
/// <param name="json">JSON字符串</param>
|
||||||
|
/// <param name="defaultValue">反序列化失败时的默认值</param>
|
||||||
|
/// <returns>反序列化后的对象或默认值</returns>
|
||||||
|
public static T? TryDeserialize<T>(string json, T? defaultValue = default)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(json))
|
||||||
|
{
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
T? result = JsonConvert.DeserializeObject<T>(json, _settings);
|
||||||
|
return result ?? defaultValue;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
180
QYZH.InteractiveMagazine.Common/Helpers/SnowflakeIdHelper.cs
Normal file
180
QYZH.InteractiveMagazine.Common/Helpers/SnowflakeIdHelper.cs
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace QYZH.InteractiveMagazine.Common.Helpers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 雪花ID生成器
|
||||||
|
/// 基于Twitter Snowflake算法实现
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 静态构造函数,初始化机器ID和数据中心ID
|
||||||
|
/// </summary>
|
||||||
|
static SnowflakeIdHelper()
|
||||||
|
{
|
||||||
|
_workerId = GetWorkerId();
|
||||||
|
_dataCenterId = GetDataCenterId();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 初始化雪花ID生成器
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="workerId">机器ID (0-31)</param>
|
||||||
|
/// <param name="dataCenterId">数据中心ID (0-31)</param>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 生成雪花ID
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>唯一的雪花ID</returns>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取当前毫秒数
|
||||||
|
/// </summary>
|
||||||
|
private static long GetCurrentMilliseconds()
|
||||||
|
{
|
||||||
|
return DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 等待下一毫秒
|
||||||
|
/// </summary>
|
||||||
|
private static long WaitNextMillis(long lastTimestamp)
|
||||||
|
{
|
||||||
|
long timestamp = GetCurrentMilliseconds();
|
||||||
|
while (timestamp <= lastTimestamp)
|
||||||
|
{
|
||||||
|
timestamp = GetCurrentMilliseconds();
|
||||||
|
}
|
||||||
|
return timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取机器ID(基于MAC地址简单计算)
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取数据中心ID(基于机器名简单计算)
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
102
QYZH.InteractiveMagazine.Common/Helpers/ValidateHelper.cs
Normal file
102
QYZH.InteractiveMagazine.Common/Helpers/ValidateHelper.cs
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace QYZH.InteractiveMagazine.Common.Helpers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 参数校验工具类
|
||||||
|
/// </summary>
|
||||||
|
public static class ValidateHelper
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 邮箱正则表达式
|
||||||
|
/// </summary>
|
||||||
|
private const string EmailPattern = @"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 手机号正则表达式(中国大陆)
|
||||||
|
/// </summary>
|
||||||
|
private const string PhonePattern = @"^1[3-9]\d{9}$";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 身份证号正则表达式(中国大陆)
|
||||||
|
/// </summary>
|
||||||
|
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])$)$";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 校验邮箱格式
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="email">邮箱地址</param>
|
||||||
|
/// <returns>是否有效</returns>
|
||||||
|
public static bool IsEmail(string email)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(email))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Regex.IsMatch(email, EmailPattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 校验手机号格式
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="phone">手机号</param>
|
||||||
|
/// <returns>是否有效</returns>
|
||||||
|
public static bool IsPhone(string phone)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(phone))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Regex.IsMatch(phone, PhonePattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 校验身份证号格式
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="idCard">身份证号</param>
|
||||||
|
/// <returns>是否有效</returns>
|
||||||
|
public static bool IsIdCard(string idCard)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(idCard))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Regex.IsMatch(idCard, IdCardPattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 校验URL格式
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="url">URL地址</param>
|
||||||
|
/// <returns>是否有效</returns>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 校验邮政编码格式
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="zipCode">邮政编码</param>
|
||||||
|
/// <returns>是否有效</returns>
|
||||||
|
public static bool IsZipCode(string zipCode)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(zipCode))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const string zipPattern = @"^\d{6}$";
|
||||||
|
return Regex.IsMatch(zipCode, zipPattern);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
<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" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
6
QYZH.InteractiveMagazine.IService/Class1.cs
Normal file
6
QYZH.InteractiveMagazine.IService/Class1.cs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
namespace QYZH.InteractiveMagazine.IService;
|
||||||
|
|
||||||
|
public class Class1
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
64
QYZH.InteractiveMagazine.IService/Dto/AuthDto.cs
Normal file
64
QYZH.InteractiveMagazine.IService/Dto/AuthDto.cs
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
namespace QYZH.InteractiveMagazine.IService.Dto;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 登录输入
|
||||||
|
/// </summary>
|
||||||
|
public class LoginInput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 账号
|
||||||
|
/// </summary>
|
||||||
|
public string Account { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 密码
|
||||||
|
/// </summary>
|
||||||
|
public string Password { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 注册输入
|
||||||
|
/// </summary>
|
||||||
|
public class RegisterInput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 账号
|
||||||
|
/// </summary>
|
||||||
|
public string Account { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 密码
|
||||||
|
/// </summary>
|
||||||
|
public string Password { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用户名
|
||||||
|
/// </summary>
|
||||||
|
public string UserName { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 登录输出
|
||||||
|
/// </summary>
|
||||||
|
public class LoginOutput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 访问令牌
|
||||||
|
/// </summary>
|
||||||
|
public string Token { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用户ID
|
||||||
|
/// </summary>
|
||||||
|
public long UserId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用户名
|
||||||
|
/// </summary>
|
||||||
|
public string UserName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 刷新令牌
|
||||||
|
/// </summary>
|
||||||
|
public string RefreshToken { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
38
QYZH.InteractiveMagazine.IService/Dto/WeChatDto.cs
Normal file
38
QYZH.InteractiveMagazine.IService/Dto/WeChatDto.cs
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
namespace QYZH.InteractiveMagazine.IService.Dto;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 微信登录输出
|
||||||
|
/// </summary>
|
||||||
|
public class WeChatLoginOutput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 访问令牌
|
||||||
|
/// </summary>
|
||||||
|
public string Token { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用户ID
|
||||||
|
/// </summary>
|
||||||
|
public long UserId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用户名
|
||||||
|
/// </summary>
|
||||||
|
public string UserName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 微信OpenId
|
||||||
|
/// </summary>
|
||||||
|
public string OpenId { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 微信手机号获取输出
|
||||||
|
/// </summary>
|
||||||
|
public class WeChatPhoneNumberOutput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 手机号
|
||||||
|
/// </summary>
|
||||||
|
public string PhoneNumber { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
31
QYZH.InteractiveMagazine.IService/IAuthService.cs
Normal file
31
QYZH.InteractiveMagazine.IService/IAuthService.cs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
using QYZH.InteractiveMagazine.IService.Dto;
|
||||||
|
|
||||||
|
namespace QYZH.InteractiveMagazine.IService;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 认证服务接口
|
||||||
|
/// </summary>
|
||||||
|
public interface IAuthService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 用户登录
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="account">账号</param>
|
||||||
|
/// <param name="password">密码</param>
|
||||||
|
/// <returns>登录结果</returns>
|
||||||
|
Task<LoginOutput> LoginAsync(string account, string password);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用户注册
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">注册输入参数</param>
|
||||||
|
/// <returns>是否成功</returns>
|
||||||
|
Task<bool> RegisterAsync(RegisterInput input);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 刷新令牌
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="refreshToken">刷新令牌</param>
|
||||||
|
/// <returns>新的登录结果</returns>
|
||||||
|
Task<LoginOutput> RefreshTokenAsync(string refreshToken);
|
||||||
|
}
|
||||||
51
QYZH.InteractiveMagazine.IService/IBaseService.cs
Normal file
51
QYZH.InteractiveMagazine.IService/IBaseService.cs
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
using QYZH.InteractiveMagazine.Models.Dto;
|
||||||
|
|
||||||
|
namespace QYZH.InteractiveMagazine.IService;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 基础服务接口
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">实体类型</typeparam>
|
||||||
|
public interface IBaseService<T> where T : class, new()
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 根据ID获取实体
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">实体ID</param>
|
||||||
|
/// <returns>实体对象</returns>
|
||||||
|
Task<T?> GetByIdAsync(long id);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取所有实体列表
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>实体列表</returns>
|
||||||
|
Task<List<T>> GetListAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取分页列表
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="pageQuery">分页查询参数</param>
|
||||||
|
/// <returns>分页数据</returns>
|
||||||
|
Task<PageListModel<T>> GetPageListAsync(PageQueryModel pageQuery);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新增实体
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="entity">实体对象</param>
|
||||||
|
/// <returns>是否成功</returns>
|
||||||
|
Task<bool> InsertAsync(T entity);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新实体
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="entity">实体对象</param>
|
||||||
|
/// <returns>是否成功</returns>
|
||||||
|
Task<bool> UpdateAsync(T entity);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据ID删除实体
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">实体ID</param>
|
||||||
|
/// <returns>是否成功</returns>
|
||||||
|
Task<bool> DeleteByIdAsync(long id);
|
||||||
|
}
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
using QYZH.InteractiveMagazine.IService.Dto;
|
||||||
|
|
||||||
|
namespace QYZH.InteractiveMagazine.IService;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 微信小程序服务接口
|
||||||
|
/// </summary>
|
||||||
|
public interface IWeChatMiniProgramService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 微信登录
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="code">微信登录凭证</param>
|
||||||
|
/// <returns>微信登录结果</returns>
|
||||||
|
Task<WeChatLoginOutput> WeChatLoginAsync(string code);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取手机号
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="code">获取手机号凭证</param>
|
||||||
|
/// <returns>手机号信息</returns>
|
||||||
|
Task<WeChatPhoneNumberOutput> GetPhoneNumberAsync(string code);
|
||||||
|
}
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\QYZH.InteractiveMagazine.Common\QYZH.InteractiveMagazine.Common.csproj" />
|
||||||
|
<ProjectReference Include="..\QYZH.InteractiveMagazine.Models\QYZH.InteractiveMagazine.Models.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
71
QYZH.InteractiveMagazine.Infrastructure/Auth/JwtHelper.cs
Normal file
71
QYZH.InteractiveMagazine.Infrastructure/Auth/JwtHelper.cs
Normal file
@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// JWT工具类
|
||||||
|
/// </summary>
|
||||||
|
public static class JwtHelper
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 生成JWT令牌
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userId">用户ID</param>
|
||||||
|
/// <param name="userName">用户名</param>
|
||||||
|
/// <param name="settings">JWT配置</param>
|
||||||
|
/// <returns>JWT令牌字符串</returns>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证JWT令牌
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="token">JWT令牌</param>
|
||||||
|
/// <param name="settings">JWT配置</param>
|
||||||
|
/// <returns>ClaimsPrincipal对象</returns>
|
||||||
|
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 _);
|
||||||
|
}
|
||||||
|
}
|
||||||
119
QYZH.InteractiveMagazine.Infrastructure/Cache/RedisHelper.cs
Normal file
119
QYZH.InteractiveMagazine.Infrastructure/Cache/RedisHelper.cs
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
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}";
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 统一服务注册扩展
|
||||||
|
/// </summary>
|
||||||
|
public static class DependencyInjectionExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 注册基础设施服务
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="services">服务集合</param>
|
||||||
|
/// <param name="configuration">配置</param>
|
||||||
|
public static void AddInfrastructureServices(this IServiceCollection services, IConfiguration configuration)
|
||||||
|
{
|
||||||
|
AddJwtAuthentication(services, configuration);
|
||||||
|
AddRedisCache(services, configuration);
|
||||||
|
AddRabbitMQ(services, configuration);
|
||||||
|
|
||||||
|
services.AddTransient<GlobalExceptionMiddleware>();
|
||||||
|
services.AddTransient<OperationLogMiddleware>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 配置JWT认证
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="services">服务集合</param>
|
||||||
|
/// <param name="configuration">配置</param>
|
||||||
|
private static void AddJwtAuthentication(IServiceCollection services, IConfiguration configuration)
|
||||||
|
{
|
||||||
|
var jwtSettings = configuration.GetSection("JwtSettings").Get<JwtSettings>()!;
|
||||||
|
|
||||||
|
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
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 注册Redis缓存
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="services">服务集合</param>
|
||||||
|
/// <param name="configuration">配置</param>
|
||||||
|
private static void AddRedisCache(IServiceCollection services, IConfiguration configuration)
|
||||||
|
{
|
||||||
|
var redisSettings = configuration.GetSection("RedisSettings").Get<RedisSettings>()!;
|
||||||
|
|
||||||
|
services.AddSingleton(redisSettings);
|
||||||
|
|
||||||
|
services.AddSingleton<IConnectionMultiplexer>(sp =>
|
||||||
|
{
|
||||||
|
var multiplexer = ConnectionMultiplexer.Connect(redisSettings.ConnectionString!);
|
||||||
|
RedisHelper.Connection = multiplexer;
|
||||||
|
RedisHelper.SetKeyPrefix(redisSettings.InstanceName!);
|
||||||
|
return multiplexer;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 注册RabbitMQ
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="services">服务集合</param>
|
||||||
|
/// <param name="configuration">配置</param>
|
||||||
|
private static void AddRabbitMQ(IServiceCollection services, IConfiguration configuration)
|
||||||
|
{
|
||||||
|
var rabbitMQSettings = configuration.GetSection("RabbitMQSettings").Get<RabbitMQSettings>()!;
|
||||||
|
|
||||||
|
services.AddSingleton(rabbitMQSettings);
|
||||||
|
|
||||||
|
services.AddSingleton<IConnection>(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<RabbitMQPublisher>();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,72 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 全局异常中间件
|
||||||
|
/// </summary>
|
||||||
|
public class GlobalExceptionMiddleware : IMiddleware
|
||||||
|
{
|
||||||
|
private readonly ILogger<GlobalExceptionMiddleware> _logger;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 构造函数
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="logger">日志记录器</param>
|
||||||
|
public GlobalExceptionMiddleware(ILogger<GlobalExceptionMiddleware> logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 执行中间件
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">HTTP上下文</param>
|
||||||
|
/// <param name="next">下一个中间件委托</param>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 处理业务异常
|
||||||
|
/// </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;
|
||||||
|
|
||||||
|
var response = BaseResponse<object>.Fail(ex.Message, ex.Code);
|
||||||
|
var json = JsonSerializer.Serialize(response);
|
||||||
|
|
||||||
|
await context.Response.WriteAsync(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 处理系统异常
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">HTTP上下文</param>
|
||||||
|
/// <param name="ex">系统异常</param>
|
||||||
|
private static async Task HandleSystemExceptionAsync(HttpContext context, Exception ex)
|
||||||
|
{
|
||||||
|
context.Response.ContentType = "application/json";
|
||||||
|
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
|
||||||
|
|
||||||
|
var response = BaseResponse<object>.Fail("系统内部错误,请稍后重试");
|
||||||
|
var json = JsonSerializer.Serialize(response);
|
||||||
|
|
||||||
|
await context.Response.WriteAsync(json);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,53 @@
|
|||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace QYZH.InteractiveMagazine.Infrastructure.Middleware;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 操作日志中间件
|
||||||
|
/// </summary>
|
||||||
|
public class OperationLogMiddleware : IMiddleware
|
||||||
|
{
|
||||||
|
private readonly ILogger<OperationLogMiddleware> _logger;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 构造函数
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="logger">日志记录器</param>
|
||||||
|
public OperationLogMiddleware(ILogger<OperationLogMiddleware> logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 执行中间件
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">HTTP上下文</param>
|
||||||
|
/// <param name="next">下一个中间件委托</param>
|
||||||
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\QYZH.InteractiveMagazine.Common\QYZH.InteractiveMagazine.Common.csproj" />
|
||||||
|
<ProjectReference Include="..\QYZH.InteractiveMagazine.Models\QYZH.InteractiveMagazine.Models.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<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" />
|
||||||
|
<PackageReference Include="SqlSugar" Version="5.1.4.207" />
|
||||||
|
<PackageReference Include="StackExchange.Redis" Version="2.13.17" />
|
||||||
|
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.18.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
46
QYZH.InteractiveMagazine.Models/Common/BusinessException.cs
Normal file
46
QYZH.InteractiveMagazine.Models/Common/BusinessException.cs
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
namespace QYZH.InteractiveMagazine.Models.Common;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 业务异常类
|
||||||
|
/// </summary>
|
||||||
|
public class BusinessException : Exception
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 错误码
|
||||||
|
/// </summary>
|
||||||
|
public int Code { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 无参构造函数
|
||||||
|
/// </summary>
|
||||||
|
public BusinessException() : base()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 构造函数
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">异常消息</param>
|
||||||
|
public BusinessException(string message) : base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 构造函数
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">异常消息</param>
|
||||||
|
/// <param name="code">错误码</param>
|
||||||
|
public BusinessException(string message, int code) : base(message)
|
||||||
|
{
|
||||||
|
Code = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 构造函数
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">异常消息</param>
|
||||||
|
/// <param name="innerException">内部异常</param>
|
||||||
|
public BusinessException(string message, Exception innerException) : base(message, innerException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
55
QYZH.InteractiveMagazine.Models/Dto/BaseResponse.cs
Normal file
55
QYZH.InteractiveMagazine.Models/Dto/BaseResponse.cs
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
namespace QYZH.InteractiveMagazine.Models.Dto;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 统一响应模型
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">数据类型</typeparam>
|
||||||
|
public class BaseResponse<T>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 状态码
|
||||||
|
/// </summary>
|
||||||
|
public int Code { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 提示信息
|
||||||
|
/// </summary>
|
||||||
|
public string? Message { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 数据
|
||||||
|
/// </summary>
|
||||||
|
public T? Data { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 成功响应
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">数据</param>
|
||||||
|
/// <param name="message">提示信息</param>
|
||||||
|
/// <returns>统一响应对象</returns>
|
||||||
|
public static BaseResponse<T> Success(T data, string message = "操作成功")
|
||||||
|
{
|
||||||
|
return new BaseResponse<T>
|
||||||
|
{
|
||||||
|
Code = 200,
|
||||||
|
Message = message,
|
||||||
|
Data = data
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 失败响应
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">提示信息</param>
|
||||||
|
/// <param name="code">状态码</param>
|
||||||
|
/// <returns>统一响应对象</returns>
|
||||||
|
public static BaseResponse<T> Fail(string message, int code = 500)
|
||||||
|
{
|
||||||
|
return new BaseResponse<T>
|
||||||
|
{
|
||||||
|
Code = code,
|
||||||
|
Message = message,
|
||||||
|
Data = default
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
33
QYZH.InteractiveMagazine.Models/Dto/PageListModel.cs
Normal file
33
QYZH.InteractiveMagazine.Models/Dto/PageListModel.cs
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
namespace QYZH.InteractiveMagazine.Models.Dto;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 分页输出
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">数据类型</typeparam>
|
||||||
|
public class PageListModel<T>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 页码
|
||||||
|
/// </summary>
|
||||||
|
public int PageIndex { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 每页条数
|
||||||
|
/// </summary>
|
||||||
|
public int PageSize { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 总记录数
|
||||||
|
/// </summary>
|
||||||
|
public long TotalCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 总页数
|
||||||
|
/// </summary>
|
||||||
|
public int TotalPages => PageSize > 0 ? (int)Math.Ceiling((double)TotalCount / PageSize) : 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 数据列表
|
||||||
|
/// </summary>
|
||||||
|
public List<T>? List { get; set; }
|
||||||
|
}
|
||||||
27
QYZH.InteractiveMagazine.Models/Dto/PageQueryModel.cs
Normal file
27
QYZH.InteractiveMagazine.Models/Dto/PageQueryModel.cs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
namespace QYZH.InteractiveMagazine.Models.Dto;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 分页查询输入
|
||||||
|
/// </summary>
|
||||||
|
public class PageQueryModel
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 页码,默认1
|
||||||
|
/// </summary>
|
||||||
|
public int PageIndex { get; set; } = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 每页条数,默认10
|
||||||
|
/// </summary>
|
||||||
|
public int PageSize { get; set; } = 10;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 排序字段
|
||||||
|
/// </summary>
|
||||||
|
public string? SortField { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 排序方式(asc/desc)
|
||||||
|
/// </summary>
|
||||||
|
public string? SortOrder { get; set; }
|
||||||
|
}
|
||||||
43
QYZH.InteractiveMagazine.Models/Entity/BaseEntity.cs
Normal file
43
QYZH.InteractiveMagazine.Models/Entity/BaseEntity.cs
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
using SqlSugar;
|
||||||
|
|
||||||
|
namespace QYZH.InteractiveMagazine.Models.Entity;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 基础实体类
|
||||||
|
/// </summary>
|
||||||
|
public abstract class BaseEntity
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 主键Id
|
||||||
|
/// </summary>
|
||||||
|
[SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
|
||||||
|
public long Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否已删除
|
||||||
|
/// </summary>
|
||||||
|
[SugarColumn(IsIgnore = false)]
|
||||||
|
public bool IsDeleted { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建人
|
||||||
|
/// </summary>
|
||||||
|
[SugarColumn(Length = 50)]
|
||||||
|
public string? CreatedBy { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建时间
|
||||||
|
/// </summary>
|
||||||
|
public DateTime CreatedTime { get; set; } = DateTime.Now;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新人
|
||||||
|
/// </summary>
|
||||||
|
[SugarColumn(Length = 50)]
|
||||||
|
public string? UpdatedBy { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新时间
|
||||||
|
/// </summary>
|
||||||
|
public DateTime UpdatedTime { get; set; } = DateTime.Now;
|
||||||
|
}
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="SqlSugarCore" Version="5.1.4.166" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
27
QYZH.InteractiveMagazine.Models/Settings/JwtSettings.cs
Normal file
27
QYZH.InteractiveMagazine.Models/Settings/JwtSettings.cs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
namespace QYZH.InteractiveMagazine.Models.Settings;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// JWT配置
|
||||||
|
/// </summary>
|
||||||
|
public class JwtSettings
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 颁发者
|
||||||
|
/// </summary>
|
||||||
|
public string? Issuer { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 接收者
|
||||||
|
/// </summary>
|
||||||
|
public string? Audience { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 密钥
|
||||||
|
/// </summary>
|
||||||
|
public string? SecretKey { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 过期时间(分钟)
|
||||||
|
/// </summary>
|
||||||
|
public int ExpiryMinutes { get; set; }
|
||||||
|
}
|
||||||
32
QYZH.InteractiveMagazine.Models/Settings/RabbitMQSettings.cs
Normal file
32
QYZH.InteractiveMagazine.Models/Settings/RabbitMQSettings.cs
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
namespace QYZH.InteractiveMagazine.Models.Settings;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// RabbitMQ配置
|
||||||
|
/// </summary>
|
||||||
|
public class RabbitMQSettings
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 主机地址
|
||||||
|
/// </summary>
|
||||||
|
public string? HostName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 端口
|
||||||
|
/// </summary>
|
||||||
|
public int Port { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用户名
|
||||||
|
/// </summary>
|
||||||
|
public string? UserName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 密码
|
||||||
|
/// </summary>
|
||||||
|
public string? Password { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 虚拟主机
|
||||||
|
/// </summary>
|
||||||
|
public string? VirtualHost { get; set; }
|
||||||
|
}
|
||||||
17
QYZH.InteractiveMagazine.Models/Settings/RedisSettings.cs
Normal file
17
QYZH.InteractiveMagazine.Models/Settings/RedisSettings.cs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
namespace QYZH.InteractiveMagazine.Models.Settings;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Redis配置
|
||||||
|
/// </summary>
|
||||||
|
public class RedisSettings
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 连接字符串
|
||||||
|
/// </summary>
|
||||||
|
public string? ConnectionString { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 实例名称
|
||||||
|
/// </summary>
|
||||||
|
public string? InstanceName { get; set; }
|
||||||
|
}
|
||||||
17
QYZH.InteractiveMagazine.Models/Settings/WeChatSettings.cs
Normal file
17
QYZH.InteractiveMagazine.Models/Settings/WeChatSettings.cs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
namespace QYZH.InteractiveMagazine.Models.Settings;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 微信小程序配置
|
||||||
|
/// </summary>
|
||||||
|
public class WeChatSettings
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 应用Id
|
||||||
|
/// </summary>
|
||||||
|
public string? AppId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 应用密钥
|
||||||
|
/// </summary>
|
||||||
|
public string? AppSecret { get; set; }
|
||||||
|
}
|
||||||
149
QYZH.InteractiveMagazine.Repository/BaseRepository.cs
Normal file
149
QYZH.InteractiveMagazine.Repository/BaseRepository.cs
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
using QYZH.InteractiveMagazine.Models.Dto;
|
||||||
|
using SqlSugar;
|
||||||
|
using System.Linq.Expressions;
|
||||||
|
|
||||||
|
namespace QYZH.InteractiveMagazine.Repository;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 基础仓储实现
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">实体类型</typeparam>
|
||||||
|
public class BaseRepository<T> : IBaseRepository<T> where T : class, new()
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// SqlSugar 数据库实例
|
||||||
|
/// </summary>
|
||||||
|
protected SqlSugarClient Db => SqlSugarDbContext.GetDb();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据Id获取实体(自动过滤已删除数据)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">主键Id</param>
|
||||||
|
/// <returns>实体对象</returns>
|
||||||
|
public async Task<T?> GetByIdAsync(long id)
|
||||||
|
{
|
||||||
|
return await Db.Queryable<T>().In(id).FirstAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取所有列表(自动过滤已删除数据)
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>实体列表</returns>
|
||||||
|
public async Task<List<T>> GetListAsync()
|
||||||
|
{
|
||||||
|
return await Db.Queryable<T>().ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据条件获取列表(自动过滤已删除数据)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="where">查询条件</param>
|
||||||
|
/// <returns>实体列表</returns>
|
||||||
|
public async Task<List<T>> GetListByWhereAsync(Expression<Func<T, bool>> where)
|
||||||
|
{
|
||||||
|
return await Db.Queryable<T>().Where(where).ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 分页查询(自动过滤已删除数据)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="where">查询条件</param>
|
||||||
|
/// <param name="pageQuery">分页参数</param>
|
||||||
|
/// <returns>分页结果</returns>
|
||||||
|
public async Task<PageListModel<T>> GetPageListAsync(Expression<Func<T, bool>> where, PageQueryModel pageQuery)
|
||||||
|
{
|
||||||
|
RefAsync<int> total = 0;
|
||||||
|
|
||||||
|
var query = Db.Queryable<T>().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<T>
|
||||||
|
{
|
||||||
|
PageIndex = pageQuery.PageIndex,
|
||||||
|
PageSize = pageQuery.PageSize,
|
||||||
|
TotalCount = total,
|
||||||
|
List = list
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 插入单条记录
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="entity">实体对象</param>
|
||||||
|
/// <returns>是否成功</returns>
|
||||||
|
public async Task<bool> InsertAsync(T entity)
|
||||||
|
{
|
||||||
|
return await Db.Insertable(entity).ExecuteCommandAsync() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 批量插入记录
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="entities">实体列表</param>
|
||||||
|
/// <returns>是否成功</returns>
|
||||||
|
public async Task<bool> InsertRangeAsync(List<T> entities)
|
||||||
|
{
|
||||||
|
return await Db.Insertable(entities).ExecuteCommandAsync() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新单条记录
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="entity">实体对象</param>
|
||||||
|
/// <returns>是否成功</returns>
|
||||||
|
public async Task<bool> UpdateAsync(T entity)
|
||||||
|
{
|
||||||
|
return await Db.Updateable(entity).ExecuteCommandAsync() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 批量更新记录
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="entities">实体列表</param>
|
||||||
|
/// <returns>是否成功</returns>
|
||||||
|
public async Task<bool> UpdateRangeAsync(List<T> entities)
|
||||||
|
{
|
||||||
|
return await Db.Updateable(entities).ExecuteCommandAsync() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据Id删除记录(软删除,设置 IsDeleted = true)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">主键Id</param>
|
||||||
|
/// <returns>是否成功</returns>
|
||||||
|
public async Task<bool> DeleteByIdAsync(long id)
|
||||||
|
{
|
||||||
|
return await Db.Deleteable<T>().In(id).IsLogic().ExecuteCommandAsync() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据条件删除记录(软删除,设置 IsDeleted = true)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="where">删除条件</param>
|
||||||
|
/// <returns>是否成功</returns>
|
||||||
|
public async Task<bool> DeleteByWhereAsync(Expression<Func<T, bool>> where)
|
||||||
|
{
|
||||||
|
return await Db.Deleteable<T>().Where(where).IsLogic().ExecuteCommandAsync() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据条件获取记录数(自动过滤已删除数据)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="where">查询条件</param>
|
||||||
|
/// <returns>记录数</returns>
|
||||||
|
public async Task<int> GetCountAsync(Expression<Func<T, bool>> where)
|
||||||
|
{
|
||||||
|
return await Db.Queryable<T>().Where(where).CountAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
88
QYZH.InteractiveMagazine.Repository/IBaseRepository.cs
Normal file
88
QYZH.InteractiveMagazine.Repository/IBaseRepository.cs
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
using QYZH.InteractiveMagazine.Models.Dto;
|
||||||
|
using System.Linq.Expressions;
|
||||||
|
|
||||||
|
namespace QYZH.InteractiveMagazine.Repository;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 基础仓储接口
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">实体类型</typeparam>
|
||||||
|
public interface IBaseRepository<T> where T : class, new()
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 根据Id获取实体
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">主键Id</param>
|
||||||
|
/// <returns>实体对象</returns>
|
||||||
|
Task<T?> GetByIdAsync(long id);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取所有列表
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>实体列表</returns>
|
||||||
|
Task<List<T>> GetListAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据条件获取列表
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="where">查询条件</param>
|
||||||
|
/// <returns>实体列表</returns>
|
||||||
|
Task<List<T>> GetListByWhereAsync(Expression<Func<T, bool>> where);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 分页查询
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="where">查询条件</param>
|
||||||
|
/// <param name="pageQuery">分页参数</param>
|
||||||
|
/// <returns>分页结果</returns>
|
||||||
|
Task<PageListModel<T>> GetPageListAsync(Expression<Func<T, bool>> where, PageQueryModel pageQuery);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 插入单条记录
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="entity">实体对象</param>
|
||||||
|
/// <returns>是否成功</returns>
|
||||||
|
Task<bool> InsertAsync(T entity);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 批量插入记录
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="entities">实体列表</param>
|
||||||
|
/// <returns>是否成功</returns>
|
||||||
|
Task<bool> InsertRangeAsync(List<T> entities);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新单条记录
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="entity">实体对象</param>
|
||||||
|
/// <returns>是否成功</returns>
|
||||||
|
Task<bool> UpdateAsync(T entity);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 批量更新记录
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="entities">实体列表</param>
|
||||||
|
/// <returns>是否成功</returns>
|
||||||
|
Task<bool> UpdateRangeAsync(List<T> entities);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据Id删除记录(软删除)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">主键Id</param>
|
||||||
|
/// <returns>是否成功</returns>
|
||||||
|
Task<bool> DeleteByIdAsync(long id);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据条件删除记录(软删除)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="where">删除条件</param>
|
||||||
|
/// <returns>是否成功</returns>
|
||||||
|
Task<bool> DeleteByWhereAsync(Expression<Func<T, bool>> where);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据条件获取记录数
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="where">查询条件</param>
|
||||||
|
/// <returns>记录数</returns>
|
||||||
|
Task<int> GetCountAsync(Expression<Func<T, bool>> where);
|
||||||
|
}
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\QYZH.InteractiveMagazine.Common\QYZH.InteractiveMagazine.Common.csproj" />
|
||||||
|
<ProjectReference Include="..\QYZH.InteractiveMagazine.Models\QYZH.InteractiveMagazine.Models.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
|
||||||
|
<PackageReference Include="MySqlConnector" Version="2.5.0" />
|
||||||
|
<PackageReference Include="SqlSugar" Version="5.1.4.207" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
99
QYZH.InteractiveMagazine.Repository/SqlSugarDbContext.cs
Normal file
99
QYZH.InteractiveMagazine.Repository/SqlSugarDbContext.cs
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using QYZH.InteractiveMagazine.Models.Entity;
|
||||||
|
using SqlSugar;
|
||||||
|
|
||||||
|
namespace QYZH.InteractiveMagazine.Repository;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// SqlSugar 数据库上下文封装(静态类)
|
||||||
|
/// </summary>
|
||||||
|
public static class SqlSugarDbContext
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// SqlSugarClient 实例
|
||||||
|
/// </summary>
|
||||||
|
private static SqlSugarClient? _db;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 配置对象
|
||||||
|
/// </summary>
|
||||||
|
private static IConfiguration? _configuration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 初始化数据库上下文(在应用启动时调用一次)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="configuration">配置对象</param>
|
||||||
|
public static void Init(IConfiguration configuration)
|
||||||
|
{
|
||||||
|
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取 SqlSugarClient 实例
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>SqlSugarClient 实例</returns>
|
||||||
|
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<BaseEntity>(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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 代码优先初始化(可选)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="entityTypes">实体类型数组</param>
|
||||||
|
public static void InitializeCodeFirst(params Type[] entityTypes)
|
||||||
|
{
|
||||||
|
var db = GetDb();
|
||||||
|
|
||||||
|
// 创建数据库(如果不存在)
|
||||||
|
db.DbMaintenance.CreateDatabase();
|
||||||
|
|
||||||
|
// 初始化表结构
|
||||||
|
if (entityTypes != null && entityTypes.Length > 0)
|
||||||
|
{
|
||||||
|
db.CodeFirst.InitTables(entityTypes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
173
QYZH.InteractiveMagazine.Service/AuthService.cs
Normal file
173
QYZH.InteractiveMagazine.Service/AuthService.cs
Normal file
@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 认证服务实现
|
||||||
|
/// </summary>
|
||||||
|
public class AuthService : IAuthService
|
||||||
|
{
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
|
private readonly ILogger<AuthService> _logger;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 演示用管理员账号
|
||||||
|
/// </summary>
|
||||||
|
private const string DemoAccount = "admin";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 演示用管理员密码
|
||||||
|
/// </summary>
|
||||||
|
private const string DemoPassword = "123456";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 构造函数
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="configuration">配置</param>
|
||||||
|
/// <param name="logger">日志记录器</param>
|
||||||
|
public AuthService(IConfiguration configuration, ILogger<AuthService> logger)
|
||||||
|
{
|
||||||
|
_configuration = configuration;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用户登录
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="account">账号</param>
|
||||||
|
/// <param name="password">密码</param>
|
||||||
|
/// <returns>登录结果</returns>
|
||||||
|
public async Task<LoginOutput> 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 = "管理员"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用户注册
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">注册输入参数</param>
|
||||||
|
/// <returns>是否成功</returns>
|
||||||
|
public async Task<bool> 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 刷新令牌
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="refreshToken">刷新令牌</param>
|
||||||
|
/// <returns>新的登录结果</returns>
|
||||||
|
public async Task<LoginOutput> 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 = "管理员"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取JWT配置
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>JWT配置对象</returns>
|
||||||
|
private JwtSettings GetJwtSettings()
|
||||||
|
{
|
||||||
|
var jwtSettings = _configuration.GetSection("JwtSettings").Get<JwtSettings>()
|
||||||
|
?? 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
205
QYZH.InteractiveMagazine.Service/BaseService.cs
Normal file
205
QYZH.InteractiveMagazine.Service/BaseService.cs
Normal file
@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 基础服务实现
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">实体类型</typeparam>
|
||||||
|
public class BaseService<T> : IBaseService<T> where T : class, new()
|
||||||
|
{
|
||||||
|
protected readonly IBaseRepository<T> _repository;
|
||||||
|
protected readonly ILogger<BaseService<T>> _logger;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 构造函数
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="repository">基础仓储</param>
|
||||||
|
/// <param name="logger">日志记录器</param>
|
||||||
|
public BaseService(IBaseRepository<T> repository, ILogger<BaseService<T>> logger)
|
||||||
|
{
|
||||||
|
_repository = repository;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据ID获取实体
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">实体ID</param>
|
||||||
|
/// <returns>实体对象</returns>
|
||||||
|
public async Task<T?> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取所有实体列表
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>实体列表</returns>
|
||||||
|
public async Task<List<T>> GetListAsync()
|
||||||
|
{
|
||||||
|
_logger.LogInformation("正在获取所有实体列表");
|
||||||
|
return await _repository.GetListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取分页列表
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="pageQuery">分页查询参数</param>
|
||||||
|
/// <returns>分页数据</returns>
|
||||||
|
public async Task<PageListModel<T>> 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新增实体
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="entity">实体对象</param>
|
||||||
|
/// <returns>是否成功</returns>
|
||||||
|
public async Task<bool> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新实体
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="entity">实体对象</param>
|
||||||
|
/// <returns>是否成功</returns>
|
||||||
|
public async Task<bool> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据ID删除实体
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">实体ID</param>
|
||||||
|
/// <returns>是否成功</returns>
|
||||||
|
public async Task<bool> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 设置插入时的审计字段
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="entity">实体对象</param>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 设置更新时的审计字段
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="entity">实体对象</param>
|
||||||
|
private static void SetAuditFieldsOnUpdate(T entity)
|
||||||
|
{
|
||||||
|
if (entity is BaseEntity baseEntity)
|
||||||
|
{
|
||||||
|
baseEntity.UpdatedTime = DateTime.Now;
|
||||||
|
baseEntity.UpdatedBy = baseEntity.UpdatedBy ?? "system";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\QYZH.InteractiveMagazine.IService\QYZH.InteractiveMagazine.IService.csproj" />
|
||||||
|
<ProjectReference Include="..\QYZH.InteractiveMagazine.Repository\QYZH.InteractiveMagazine.Repository.csproj" />
|
||||||
|
<ProjectReference Include="..\QYZH.InteractiveMagazine.Common\QYZH.InteractiveMagazine.Common.csproj" />
|
||||||
|
<ProjectReference Include="..\QYZH.InteractiveMagazine.Models\QYZH.InteractiveMagazine.Models.csproj" />
|
||||||
|
<ProjectReference Include="..\QYZH.InteractiveMagazine.Infrastructure\QYZH.InteractiveMagazine.Infrastructure.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="AutoMapper" Version="16.1.1" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.8" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
167
QYZH.InteractiveMagazine.Service/WeChatMiniProgramService.cs
Normal file
167
QYZH.InteractiveMagazine.Service/WeChatMiniProgramService.cs
Normal file
@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 微信小程序服务实现
|
||||||
|
/// </summary>
|
||||||
|
public class WeChatMiniProgramService : IWeChatMiniProgramService
|
||||||
|
{
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
|
private readonly ILogger<WeChatMiniProgramService> _logger;
|
||||||
|
private const string Code2SessionUrl = "https://api.weixin.qq.com/sns/jscode2session";
|
||||||
|
private const string GetPhoneNumberUrl = "https://api.weixin.qq.com/wxa/business/getuserphonenumber";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 构造函数
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="configuration">配置</param>
|
||||||
|
/// <param name="logger">日志记录器</param>
|
||||||
|
public WeChatMiniProgramService(IConfiguration configuration, ILogger<WeChatMiniProgramService> logger)
|
||||||
|
{
|
||||||
|
_configuration = configuration;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 微信登录
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="code">微信登录凭证</param>
|
||||||
|
/// <returns>微信登录结果</returns>
|
||||||
|
public async Task<WeChatLoginOutput> 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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取手机号
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="code">获取手机号凭证</param>
|
||||||
|
/// <returns>手机号信息</returns>
|
||||||
|
public async Task<WeChatPhoneNumberOutput> 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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 模拟调用微信code2session接口
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="code">登录凭证</param>
|
||||||
|
/// <param name="weChatSettings">微信配置</param>
|
||||||
|
/// <returns>openId</returns>
|
||||||
|
private async Task<string> 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<dynamic>(url);
|
||||||
|
// if (response?.errcode == 0) return response.openid;
|
||||||
|
|
||||||
|
await Task.Delay(100); // 模拟网络请求延迟
|
||||||
|
|
||||||
|
return $"demo_openid_{code.GetHashCode():X}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 模拟调用微信获取手机号接口
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="code">获取手机号凭证</param>
|
||||||
|
/// <returns>手机号</returns>
|
||||||
|
private async Task<string> 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<dynamic>(tokenUrl);
|
||||||
|
// var accessToken = tokenResponse.access_token;
|
||||||
|
// var phoneUrl = $"{GetPhoneNumberUrl}?access_token={accessToken}";
|
||||||
|
// var phoneResponse = await HttpHelper.PostAsync<dynamic>(phoneUrl, new { code });
|
||||||
|
|
||||||
|
await Task.Delay(100); // 模拟网络请求延迟
|
||||||
|
|
||||||
|
return "13800138000";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取微信配置
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>微信配置对象</returns>
|
||||||
|
private WeChatSettings GetWeChatSettings()
|
||||||
|
{
|
||||||
|
return _configuration.GetSection("WeChatSettings").Get<WeChatSettings>()
|
||||||
|
?? new WeChatSettings
|
||||||
|
{
|
||||||
|
AppId = "demo_app_id",
|
||||||
|
AppSecret = "demo_app_secret"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取JWT配置
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>JWT配置对象</returns>
|
||||||
|
private JwtSettings GetJwtSettings()
|
||||||
|
{
|
||||||
|
return _configuration.GetSection("JwtSettings").Get<JwtSettings>()
|
||||||
|
?? new JwtSettings
|
||||||
|
{
|
||||||
|
Issuer = "QYZH.InteractiveMagazine",
|
||||||
|
Audience = "QYZH.InteractiveMagazine.Client",
|
||||||
|
SecretKey = "QYZH_InteractiveMagazine_SecretKey_2024",
|
||||||
|
ExpiryMinutes = 120
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 认证控制器
|
||||||
|
/// </summary>
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
[ApiController]
|
||||||
|
public class AuthController : BaseController
|
||||||
|
{
|
||||||
|
private readonly IAuthService _authService;
|
||||||
|
|
||||||
|
public AuthController(IAuthService authService)
|
||||||
|
{
|
||||||
|
_authService = authService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用户登录
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">登录信息</param>
|
||||||
|
/// <returns>登录结果</returns>
|
||||||
|
[AllowAnonymous]
|
||||||
|
[HttpPost("login")]
|
||||||
|
public async Task<BaseResponse<LoginOutput>> LoginAsync([FromBody] LoginInput input)
|
||||||
|
{
|
||||||
|
var result = await _authService.LoginAsync(input.Account, input.Password);
|
||||||
|
return Success(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用户注册
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">注册信息</param>
|
||||||
|
/// <returns>是否成功</returns>
|
||||||
|
[AllowAnonymous]
|
||||||
|
[HttpPost("register")]
|
||||||
|
public async Task<BaseResponse<bool>> RegisterAsync([FromBody] RegisterInput input)
|
||||||
|
{
|
||||||
|
var result = await _authService.RegisterAsync(input);
|
||||||
|
return Success(result, "注册成功");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 刷新令牌
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="refreshToken">刷新令牌</param>
|
||||||
|
/// <returns>新的登录结果</returns>
|
||||||
|
[AllowAnonymous]
|
||||||
|
[HttpPost("refreshToken")]
|
||||||
|
public async Task<BaseResponse<LoginOutput>> RefreshTokenAsync([FromQuery] string refreshToken)
|
||||||
|
{
|
||||||
|
var result = await _authService.RefreshTokenAsync(refreshToken);
|
||||||
|
return Success(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,58 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using QYZH.InteractiveMagazine.Models.Dto;
|
||||||
|
|
||||||
|
namespace QYZH.InteractiveMagazine.WebApi.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 基础控制器
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
public abstract class BaseController : ControllerBase
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取当前用户ID
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>用户ID</returns>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取当前用户名
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>用户名</returns>
|
||||||
|
protected string? GetCurrentUserName()
|
||||||
|
{
|
||||||
|
return User.Claims.FirstOrDefault(c => c.Type == "userName")?.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 成功响应
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">数据类型</typeparam>
|
||||||
|
/// <param name="data">数据</param>
|
||||||
|
/// <param name="message">提示信息</param>
|
||||||
|
/// <returns>统一响应对象</returns>
|
||||||
|
protected BaseResponse<T> Success<T>(T data, string message = "操作成功")
|
||||||
|
{
|
||||||
|
return BaseResponse<T>.Success(data, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 失败响应
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">提示信息</param>
|
||||||
|
/// <param name="code">状态码</param>
|
||||||
|
/// <returns>统一响应对象</returns>
|
||||||
|
protected BaseResponse<object> Fail(string message, int code = 500)
|
||||||
|
{
|
||||||
|
return BaseResponse<object>.Fail(message, code);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using QYZH.InteractiveMagazine.Models.Dto;
|
||||||
|
|
||||||
|
namespace QYZH.InteractiveMagazine.WebApi.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 健康检查控制器
|
||||||
|
/// </summary>
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
[ApiController]
|
||||||
|
public class HealthController : BaseController
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 健康检查
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>pong</returns>
|
||||||
|
[HttpGet("ping")]
|
||||||
|
public BaseResponse<string> Ping()
|
||||||
|
{
|
||||||
|
return Success("pong");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 微信小程序控制器
|
||||||
|
/// </summary>
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
[ApiController]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public class WeChatController : BaseController
|
||||||
|
{
|
||||||
|
private readonly IWeChatMiniProgramService _weChatService;
|
||||||
|
|
||||||
|
public WeChatController(IWeChatMiniProgramService weChatService)
|
||||||
|
{
|
||||||
|
_weChatService = weChatService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 微信登录
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="code">微信登录凭证</param>
|
||||||
|
/// <returns>微信登录结果</returns>
|
||||||
|
[HttpPost("login")]
|
||||||
|
public async Task<BaseResponse<WeChatLoginOutput>> WeChatLoginAsync([FromQuery] string code)
|
||||||
|
{
|
||||||
|
var result = await _weChatService.WeChatLoginAsync(code);
|
||||||
|
return Success(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
64
QYZH.InteractiveMagazine.WebApi/Program.cs
Normal file
64
QYZH.InteractiveMagazine.WebApi/Program.cs
Normal file
@ -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<GlobalExceptionMiddleware>();
|
||||||
|
builder.Services.AddTransient<OperationLogMiddleware>();
|
||||||
|
|
||||||
|
// 注册业务服务
|
||||||
|
builder.Services.AddScoped<QYZH.InteractiveMagazine.IService.IAuthService, QYZH.InteractiveMagazine.Service.AuthService>();
|
||||||
|
builder.Services.AddScoped<QYZH.InteractiveMagazine.IService.IWeChatMiniProgramService, QYZH.InteractiveMagazine.Service.WeChatMiniProgramService>();
|
||||||
|
|
||||||
|
// 初始化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<GlobalExceptionMiddleware>();
|
||||||
|
app.UseMiddleware<OperationLogMiddleware>();
|
||||||
|
app.UseAuthentication();
|
||||||
|
app.UseAuthorization();
|
||||||
|
app.MapControllers();
|
||||||
|
|
||||||
|
app.Run();
|
||||||
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.27" />
|
||||||
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.2.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\QYZH.InteractiveMagazine.IService\QYZH.InteractiveMagazine.IService.csproj" />
|
||||||
|
<ProjectReference Include="..\QYZH.InteractiveMagazine.Service\QYZH.InteractiveMagazine.Service.csproj" />
|
||||||
|
<ProjectReference Include="..\QYZH.InteractiveMagazine.Infrastructure\QYZH.InteractiveMagazine.Infrastructure.csproj" />
|
||||||
|
<ProjectReference Include="..\QYZH.InteractiveMagazine.Common\QYZH.InteractiveMagazine.Common.csproj" />
|
||||||
|
<ProjectReference Include="..\QYZH.InteractiveMagazine.Models\QYZH.InteractiveMagazine.Models.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
@QYZH.InteractiveMagazine.WebApi_HostAddress = http://localhost:5197
|
||||||
|
|
||||||
|
GET {{QYZH.InteractiveMagazine.WebApi_HostAddress}}/weatherforecast/
|
||||||
|
Accept: application/json
|
||||||
|
|
||||||
|
###
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
48
QYZH.InteractiveMagazine.WebApi/appsettings.json
Normal file
48
QYZH.InteractiveMagazine.WebApi/appsettings.json
Normal file
@ -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": "*"
|
||||||
|
}
|
||||||
9
QYZH.InteractiveMagazine.slnx
Normal file
9
QYZH.InteractiveMagazine.slnx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<Solution>
|
||||||
|
<Project Path="QYZH.InteractiveMagazine.Common/QYZH.InteractiveMagazine.Common.csproj" />
|
||||||
|
<Project Path="QYZH.InteractiveMagazine.Infrastructure/QYZH.InteractiveMagazine.Infrastructure.csproj" />
|
||||||
|
<Project Path="QYZH.InteractiveMagazine.IService/QYZH.InteractiveMagazine.IService.csproj" />
|
||||||
|
<Project Path="QYZH.InteractiveMagazine.Models/QYZH.InteractiveMagazine.Models.csproj" />
|
||||||
|
<Project Path="QYZH.InteractiveMagazine.Repository/QYZH.InteractiveMagazine.Repository.csproj" />
|
||||||
|
<Project Path="QYZH.InteractiveMagazine.Service/QYZH.InteractiveMagazine.Service.csproj" />
|
||||||
|
<Project Path="QYZH.InteractiveMagazine.WebApi/QYZH.InteractiveMagazine.WebApi.csproj" />
|
||||||
|
</Solution>
|
||||||
Reference in New Issue
Block a user