feat: 初始化后台管理模块与基础业务框架

1.  新增依赖注入生命周期标记接口、基础仓储与管理员仓储实现
2.  新增管理员认证与用户服务接口,补充认证相关DTO
3.  重构实体审计字段命名,统一Created/UpdatedAt规范
4.  新增大量业务实体类与API版本枚举配置
5.  集成Autofac依赖注入、JWT自动刷新与跨域配置
6.  替换原有微信小程序与旧认证服务为后台管理系统架构
7.  完善Swagger文档配置与项目基础部署配置
This commit is contained in:
glz
2026-06-01 17:59:23 +08:00
parent ab53492cc4
commit 831f8ba7f5
51 changed files with 2417 additions and 596 deletions

View File

@ -0,0 +1,96 @@
using QYZH.InteractiveMagazine.Models.Dto;
namespace QYZH.InteractiveMagazine.IService.Dto;
/// <summary>
/// 管理员创建/更新输入
/// </summary>
public class AdminUserInput
{
/// <summary>
/// 用户名
/// </summary>
public string UserName { get; set; } = string.Empty;
/// <summary>
/// 密码(创建时必填,更新时为空表示不修改密码)
/// </summary>
public string? Password { get; set; }
/// <summary>
/// 管理员类型: SuperAdmin, Editor
/// </summary>
public string Type { get; set; } = "Editor";
/// <summary>
/// 状态: Active, Inactive
/// </summary>
public string Status { get; set; } = "Active";
}
/// <summary>
/// 管理员输出
/// </summary>
public class AdminUserOutput
{
/// <summary>
/// 主键ID
/// </summary>
public long Id { get; set; }
/// <summary>
/// 用户名
/// </summary>
public string UserName { get; set; } = string.Empty;
/// <summary>
/// 管理员类型
/// </summary>
public string Type { get; set; } = string.Empty;
/// <summary>
/// 状态
/// </summary>
public string Status { get; set; } = string.Empty;
/// <summary>
/// 创建人
/// </summary>
public string? CreatedBy { get; set; }
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreatedAt { get; set; }
/// <summary>
/// 更新人
/// </summary>
public string? UpdatedBy { get; set; }
/// <summary>
/// 更新时间
/// </summary>
public DateTime? UpdatedAt { get; set; }
}
/// <summary>
/// 管理员分页查询输入
/// </summary>
public class AdminUserQueryInput : PageQueryModel
{
/// <summary>
/// 用户名(模糊查询)
/// </summary>
public string? UserName { get; set; }
/// <summary>
/// 管理员类型
/// </summary>
public string? Type { get; set; }
/// <summary>
/// 状态
/// </summary>
public string? Status { get; set; }
}

View File

@ -62,3 +62,32 @@ public class LoginOutput
/// </summary>
public string RefreshToken { get; set; } = string.Empty;
}
public class AdminLoginInput
{
public string UserName { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
}
public class AdminLoginOutput
{
public string Token { get; set; } = string.Empty;
public long UserId { get; set; }
public string UserName { get; set; } = string.Empty;
public string Type { get; set; } = string.Empty;
}
public class AdminUserInfoOutput
{
public long UserId { get; set; }
public string UserName { get; set; } = string.Empty;
public string Type { get; set; } = string.Empty;
public string Status { get; set; } = string.Empty;
}

View File

@ -0,0 +1,14 @@
using QYZH.InteractiveMagazine.IService.Dto;
namespace QYZH.InteractiveMagazine.IService;
public interface IAdminAuthService
{
Task<AdminLoginOutput> LoginAsync(AdminLoginInput input);
Task LogoutAsync(long userId);
Task<AdminUserInfoOutput> GetAdminInfoAsync(long userId);
Task ChangePasswordAsync(long userId, string oldPassword, string newPassword);
}

View File

@ -0,0 +1,45 @@
using QYZH.InteractiveMagazine.IService.Dto;
using QYZH.InteractiveMagazine.Models.Dto;
namespace QYZH.InteractiveMagazine.IService;
/// <summary>
/// 管理员用户服务接口
/// </summary>
public interface IAdminUserService
{
/// <summary>
/// 创建管理员
/// </summary>
/// <param name="input">管理员输入</param>
/// <returns>创建的管理员信息</returns>
Task<AdminUserOutput> CreateAsync(AdminUserInput input);
/// <summary>
/// 更新管理员
/// </summary>
/// <param name="id">管理员ID</param>
/// <param name="input">管理员输入</param>
/// <returns>更新后的管理员信息</returns>
Task<AdminUserOutput> UpdateAsync(long id, AdminUserInput input);
/// <summary>
/// 删除管理员(软删除)
/// </summary>
/// <param name="id">管理员ID</param>
Task DeleteAsync(long id);
/// <summary>
/// 根据ID获取管理员
/// </summary>
/// <param name="id">管理员ID</param>
/// <returns>管理员信息</returns>
Task<AdminUserOutput> GetByIdAsync(long id);
/// <summary>
/// 分页查询管理员列表
/// </summary>
/// <param name="input">查询条件</param>
/// <returns>分页结果</returns>
Task<PageListModel<AdminUserOutput>> GetListAsync(AdminUserQueryInput input);
}

View File

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

View File

@ -0,0 +1,15 @@
namespace QYZH.InteractiveMagazine.IService;
/// <summary>
/// 单例生命周期标记接口
/// </summary>
public interface ISingletonDependency
{
}
/// <summary>
/// 瞬时生命周期标记接口
/// </summary>
public interface ITransientDependency
{
}

View File

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

View File

@ -44,28 +44,51 @@ public static class JwtHelper
}
/// <summary>
/// 验证JWT令牌
/// 获取Token过期时间
/// </summary>
/// <param name="token">JWT令牌</param>
/// <param name="settings">JWT配置</param>
/// <returns>ClaimsPrincipal对象</returns>
public static ClaimsPrincipal ValidateToken(string token, JwtSettings settings)
/// <returns>过期时间</returns>
public static DateTime? GetTokenExpiry(string token)
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.UTF8.GetBytes(settings.SecretKey!);
var validationParameters = new TokenValidationParameters
if (tokenHandler.ReadToken(token) is JwtSecurityToken jwtToken)
{
ValidateIssuer = true,
ValidIssuer = settings.Issuer,
ValidateAudience = true,
ValidAudience = settings.Audience,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
};
return jwtToken.ValidTo;
}
return null;
}
return tokenHandler.ValidateToken(token, validationParameters, out _);
/// <summary>
/// 从Token中获取用户ID
/// </summary>
/// <param name="token">JWT令牌</param>
/// <returns>用户ID</returns>
public static long? GetUserIdFromToken(string token)
{
var tokenHandler = new JwtSecurityTokenHandler();
if (tokenHandler.ReadToken(token) is JwtSecurityToken jwtToken)
{
var userIdClaim = jwtToken.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier);
if (long.TryParse(userIdClaim?.Value, out long userId))
{
return userId;
}
}
return null;
}
/// <summary>
/// 从Token中获取用户名
/// </summary>
/// <param name="token">JWT令牌</param>
/// <returns>用户名</returns>
public static string GetUserNameFromToken(string token)
{
var tokenHandler = new JwtSecurityTokenHandler();
if (tokenHandler.ReadToken(token) is JwtSecurityToken jwtToken)
{
return jwtToken.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Name)?.Value ?? string.Empty;
}
return string.Empty;
}
}

View File

@ -0,0 +1,67 @@
using Autofac;
using Autofac.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using QYZH.InteractiveMagazine.Infrastructure.Cache;
using QYZH.InteractiveMagazine.Infrastructure.MessageQueue;
using QYZH.InteractiveMagazine.Models.Settings;
using RabbitMQ.Client;
using StackExchange.Redis;
namespace QYZH.InteractiveMagazine.Infrastructure.Autofacs
{
public static class AutofacExtension
{
/// <summary>
/// 根据程序集名称获取程序集
/// </summary>
/// <param name="builder">Web应用程序构建器</param>
public static void UseAutofac(this WebApplicationBuilder builder)
{
builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory())
.ConfigureContainer<ContainerBuilder>((c, containerBuilder) =>
{
var friendlyName = AppDomain.CurrentDomain.FriendlyName;
var source = friendlyName.Split('.');
var assemblyNames = string.Join(".", source.Take(source.Length - 1));
containerBuilder.RegisterModule(new AutofacModuleRegister(assemblyNames));
InitializeRedis(c.Configuration);
InitializeRabbitMQ(c.Configuration, containerBuilder);
});
}
private static void InitializeRedis(IConfiguration configuration)
{
var redisSettings = configuration.GetSection("RedisSettings").Get<RedisSettings>();
if (redisSettings != null && !string.IsNullOrWhiteSpace(redisSettings.ConnectionString))
{
var multiplexer = ConnectionMultiplexer.Connect(redisSettings.ConnectionString);
RedisHelper.Connection = multiplexer;
RedisHelper.SetKeyPrefix(redisSettings.InstanceName ?? string.Empty);
}
}
private static void InitializeRabbitMQ(IConfiguration configuration, ContainerBuilder containerBuilder)
{
var rabbitMQSettings = configuration.GetSection("RabbitMQSettings").Get<RabbitMQSettings>();
if (rabbitMQSettings != null && !string.IsNullOrWhiteSpace(rabbitMQSettings.HostName))
{
var factory = new ConnectionFactory
{
HostName = rabbitMQSettings.HostName,
Port = rabbitMQSettings.Port,
UserName = rabbitMQSettings.UserName ?? string.Empty,
Password = rabbitMQSettings.Password ?? string.Empty,
VirtualHost = rabbitMQSettings.VirtualHost ?? string.Empty
};
var connection = factory.CreateConnectionAsync().GetAwaiter().GetResult();
containerBuilder.RegisterInstance(connection).As<IConnection>().SingleInstance();
containerBuilder.RegisterType<RabbitMQPublisher>().InstancePerDependency();
}
}
}
}

View File

@ -0,0 +1,42 @@
using Autofac;
using System.Reflection;
namespace QYZH.InteractiveMagazine.Infrastructure.Autofacs
{
public class AutofacModuleRegister : Autofac.Module
{
private readonly string _assemblyName;
public AutofacModuleRegister(string assemblyNames)
{
_assemblyName = assemblyNames;
}
/// <summary>
/// 加在程序集
/// </summary>
/// <param name="builder"></param>
protected override void Load(ContainerBuilder builder)
{
//注册Repository(只注册接口,遵循依赖倒置原则)
builder.RegisterAssemblyTypes(GetAssemblyByName($"{_assemblyName}.Repository"))
.Where(t => t.Name.EndsWith("Repository") && !t.IsAbstract)
.AsImplementedInterfaces()
.InstancePerLifetimeScope();
//注册Service
builder.RegisterAssemblyTypes(GetAssemblyByName($"{_assemblyName}.Service"))
.Where(t => t.Name.EndsWith("Service") && !t.IsAbstract)
.AsImplementedInterfaces()
.InstancePerLifetimeScope();
}
/// <summary>
/// 根据程序集名称获取程序集
/// </summary>
/// <param name="AssemblyName">程序集名称</param>
public static Assembly GetAssemblyByName(string AssemblyName)
{
return Assembly.Load(AssemblyName);
}
}
}

View File

@ -2,12 +2,8 @@ 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;
@ -25,11 +21,10 @@ public static class DependencyInjectionExtensions
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>
@ -59,52 +54,4 @@ public static class DependencyInjectionExtensions
};
});
}
/// <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>();
}
}

View File

@ -0,0 +1,42 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace QYZH.InteractiveMagazine.Infrastructure.Middleware
{
/// <summary>
/// 跨域扩展
/// </summary>
public static class CorsExtension
{
/// <summary>
/// 跨域配置
/// </summary>
/// <param name="services"></param>
/// <param name="configuration"></param>
public static void AddCorsRegister(this WebApplicationBuilder builder)
{
bool isCorsAll = builder.Configuration["corsUrls"] == "*";
builder.Services.AddCors(options =>
{
options.AddPolicy("defaultCors", policy =>
{
if (isCorsAll)
{
policy.SetIsOriginAllowed(origin => true);
}
else
{
var corsUrls = builder.Configuration.GetSection("corsUrls").Get<string[]>();
policy.WithOrigins(corsUrls ?? Array.Empty<string>());
}
policy.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials()
.WithExposedHeaders("X-New-Token");
});
});
}
}
}

View File

@ -0,0 +1,79 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using QYZH.InteractiveMagazine.Infrastructure.Cache;
using QYZH.InteractiveMagazine.Models.Settings;
using System.IdentityModel.Tokens.Jwt;
namespace QYZH.InteractiveMagazine.Infrastructure.Middleware;
public class JwtAutoRefreshMiddleware
{
private readonly RequestDelegate _next;
private readonly IConfiguration _configuration;
private const string TokenKeyPrefix = "InteractiveMagazine:AdminAuth:Token";
private const int RefreshThresholdMinutes = 10;
public JwtAutoRefreshMiddleware(RequestDelegate next, IConfiguration configuration)
{
_next = next;
_configuration = configuration;
}
public async Task InvokeAsync(HttpContext context)
{
var authHeader = context.Request.Headers.Authorization.FirstOrDefault();
if (!string.IsNullOrEmpty(authHeader) && authHeader.StartsWith("Bearer "))
{
var token = authHeader.Substring("Bearer ".Length).Trim();
await TryAutoRefreshTokenAsync(context, token);
}
await _next(context);
}
private async Task TryAutoRefreshTokenAsync(HttpContext context, string token)
{
try
{
var tokenHandler = new JwtSecurityTokenHandler();
if (tokenHandler.ReadToken(token) is not JwtSecurityToken jwtToken)
{
return;
}
var expiryTime = jwtToken.ValidTo;
var remainingTime = expiryTime - DateTime.Now;
if (remainingTime <= TimeSpan.FromMinutes(RefreshThresholdMinutes) && remainingTime > TimeSpan.Zero)
{
var userId = jwtToken.Claims.FirstOrDefault(c => c.Type == System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
var userName = jwtToken.Claims.FirstOrDefault(c => c.Type == System.Security.Claims.ClaimTypes.Name)?.Value;
if (string.IsNullOrEmpty(userId) || string.IsNullOrEmpty(userName))
{
return;
}
var jwtSettings = _configuration.GetSection("JwtSettings").Get<JwtSettings>();
if (jwtSettings == null)
{
return;
}
var newToken = QYZH.InteractiveMagazine.Infrastructure.Auth.JwtHelper.GenerateToken(
long.Parse(userId), userName, jwtSettings);
await RedisHelper.StringSetAsync(
$"{TokenKeyPrefix}:{userId}",
newToken,
TimeSpan.FromMinutes(jwtSettings.ExpiryMinutes));
context.Response.Headers["X-New-Token"] = newToken;
}
}
catch
{
// 忽略自动刷新异常,由后续认证中间件处理
}
}
}

View File

@ -1,11 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\QYZH.InteractiveMagazine.Common\QYZH.InteractiveMagazine.Common.csproj" />
<ProjectReference Include="..\QYZH.InteractiveMagazine.IService\QYZH.InteractiveMagazine.IService.csproj" />
<ProjectReference Include="..\QYZH.InteractiveMagazine.Models\QYZH.InteractiveMagazine.Models.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Autofac" Version="9.1.0" />
<PackageReference Include="Autofac.Extensions.DependencyInjection" Version="11.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.0" />
<PackageReference Include="RabbitMQ.Client" Version="7.2.1" />
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />

View File

@ -0,0 +1,51 @@
using SqlSugar;
namespace QYZH.InteractiveMagazine.Models.Entity
{
///<summary>
///后台管理员表
///</summary>
[SugarTable("AdminUser")]
public partial class AdminUser : BaseEntity
{
public AdminUser(){
}
/// <summary>
/// Desc:主键
/// Default:
/// Nullable:False
/// </summary>
[SugarColumn(IsPrimaryKey=true,IsIdentity=true)]
public new int Id { get; set; }
/// <summary>
/// Desc:用户名
/// Default:
/// Nullable:False
/// </summary>
public string UserName {get;set;}
/// <summary>
/// Desc:密码哈希
/// Default:
/// Nullable:False
/// </summary>
public string PasswordHash {get;set;}
/// <summary>
/// Desc:管理员类型: SuperAdmin, Editor
/// Default:Editor
/// Nullable:False
/// </summary>
public string Type {get;set;}
/// <summary>
/// Desc:状态: Active, Inactive
/// Default:Active
/// Nullable:False
/// </summary>
public string Status {get;set;}
}
}

View File

@ -23,21 +23,21 @@ public abstract class BaseEntity
/// 创建人
/// </summary>
[SugarColumn(Length = 50)]
public string? CreatedBy { get; set; }
public string? CreatedBy { get; set; } = "System";
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreatedTime { get; set; } = DateTime.Now;
public DateTime CreatedAt { get; set; } = DateTime.Now;
/// <summary>
/// 更新人
/// </summary>
[SugarColumn(Length = 50)]
public string? UpdatedBy { get; set; }
public string? UpdatedBy { get; set; } = "System";
/// <summary>
/// 更新时间
/// </summary>
public DateTime UpdatedTime { get; set; } = DateTime.Now;
public DateTime? UpdatedAt { get; set; } = DateTime.Now;
}

View File

@ -0,0 +1,58 @@
using SqlSugar;
namespace QYZH.InteractiveMagazine.Models.Entity
{
///<summary>
///签到配置表
///</summary>
[SugarTable("CheckInConfig")]
public partial class CheckInConfig : BaseEntity
{
public CheckInConfig(){
}
/// <summary>
/// Desc:主键
/// Default:
/// Nullable:False
/// </summary>
[SugarColumn(IsPrimaryKey=true,IsIdentity=true)]
public new int Id {get;set;}
/// <summary>
/// Desc:连续签到天数
/// Default:
/// Nullable:False
/// </summary>
public int DayNumber {get;set;}
/// <summary>
/// Desc:奖励积分数
/// Default:
/// Nullable:False
/// </summary>
public int RewardPoints {get;set;}
/// <summary>
/// Desc:额外奖励积分
/// Default:0
/// Nullable:False
/// </summary>
public int BonusPoints {get;set;}
/// <summary>
/// Desc:配置类型: Daily, Streak
/// Default:Daily
/// Nullable:False
/// </summary>
public string Type {get;set;}
/// <summary>
/// Desc:状态: Active, Inactive
/// Default:Active
/// Nullable:False
/// </summary>
public string Status {get;set;}
}
}

View File

@ -0,0 +1,57 @@
using SqlSugar;
namespace QYZH.InteractiveMagazine.Models.Entity
{
///<summary>
///签到记录表
///</summary>
[SugarTable("CheckInRecord")]
public partial class CheckInRecord : BaseEntity
{
public CheckInRecord(){
}
/// <summary>
/// Desc:用户Id
/// Default:
/// Nullable:False
/// </summary>
public long UserId {get;set;}
/// <summary>
/// Desc:签到日期
/// Default:
/// Nullable:False
/// </summary>
public DateTime CheckInDate {get;set;}
/// <summary>
/// Desc:本次签到获得积分
/// Default:
/// Nullable:False
/// </summary>
public int PointsAwarded {get;set;}
/// <summary>
/// Desc:连续签到天数
/// Default:
/// Nullable:False
/// </summary>
public int ConsecutiveDays {get;set;}
/// <summary>
/// Desc:签到类型: Normal, MakeUp
/// Default:Normal
/// Nullable:False
/// </summary>
public string Type {get;set;}
/// <summary>
/// Desc:状态: Success
/// Default:Success
/// Nullable:False
/// </summary>
public string Status {get;set;}
}
}

View File

@ -0,0 +1,64 @@
using SqlSugar;
namespace QYZH.InteractiveMagazine.Models.Entity
{
///<summary>
///社区预置消息表
///</summary>
[SugarTable("CommunityMessage")]
public partial class CommunityMessage : BaseEntity
{
public CommunityMessage(){
}
/// <summary>
/// Desc:期刊Id
/// Default:
/// Nullable:False
/// </summary>
public long JournalId {get;set;}
/// <summary>
/// Desc:消息内容
/// Default:
/// Nullable:False
/// </summary>
public string Content {get;set;}
/// <summary>
/// Desc:配图
/// Default:
/// Nullable:True
/// </summary>
public string ImageUrl {get;set;}
/// <summary>
/// Desc:排序
/// Default:0
/// Nullable:False
/// </summary>
public int SortOrder {get;set;}
/// <summary>
/// Desc:是否启用
/// Default:b'1'
/// Nullable:False
/// </summary>
public bool IsActive {get;set;}
/// <summary>
/// Desc:消息类型: Article, Quote, Announcement
/// Default:Article
/// Nullable:False
/// </summary>
public string Type {get;set;}
/// <summary>
/// Desc:状态: Draft, Published
/// Default:Draft
/// Nullable:False
/// </summary>
public string Status {get;set;}
}
}

View File

@ -0,0 +1,92 @@
using SqlSugar;
namespace QYZH.InteractiveMagazine.Models.Entity
{
///<summary>
///期刊表
///</summary>
[SugarTable("Journal")]
public partial class Journal : BaseEntity
{
public Journal(){
}
/// <summary>
/// Desc:期刊号
/// Default:
/// Nullable:False
/// </summary>
public int IssueNumber {get;set;}
/// <summary>
/// Desc:期刊标题
/// Default:
/// Nullable:False
/// </summary>
public string Title {get;set;}
/// <summary>
/// Desc:封面图地址
/// Default:
/// Nullable:True
/// </summary>
public string CoverImageUrl {get;set;}
/// <summary>
/// Desc:摘要
/// Default:
/// Nullable:True
/// </summary>
public string Summary {get;set;}
/// <summary>
/// Desc:发布时间
/// Default:
/// Nullable:True
/// </summary>
public DateTime? PublishDate {get;set;}
/// <summary>
/// Desc:是否启用
/// Default:b'1'
/// Nullable:False
/// </summary>
public bool IsActive {get;set;}
/// <summary>
/// Desc:排序权重
/// Default:0
/// Nullable:False
/// </summary>
public int SortOrder {get;set;}
/// <summary>
/// Desc:编者寄语
/// Default:
/// Nullable:True
/// </summary>
public string EditorNote {get;set;}
/// <summary>
/// Desc:主题色
/// Default:
/// Nullable:True
/// </summary>
public string ThemeColor {get;set;}
/// <summary>
/// Desc:期刊类型: Normal, Special
/// Default:Normal
/// Nullable:False
/// </summary>
public string Type {get;set;}
/// <summary>
/// Desc:状态: Draft, Published, Archived
/// Default:Draft
/// Nullable:False
/// </summary>
public string Status {get;set;}
}
}

View File

@ -0,0 +1,79 @@
using SqlSugar;
namespace QYZH.InteractiveMagazine.Models.Entity
{
///<summary>
///勋章定义表
///</summary>
[SugarTable("Medal")]
public partial class Medal : BaseEntity
{
public Medal(){
}
/// <summary>
/// Desc:主键
/// Default:
/// Nullable:False
/// </summary>
[SugarColumn(IsPrimaryKey=true,IsIdentity=true)]
public new int Id {get;set;}
/// <summary>
/// Desc:勋章名称
/// Default:
/// Nullable:False
/// </summary>
public string Name {get;set;}
/// <summary>
/// Desc:获得条件描述
/// Default:
/// Nullable:True
/// </summary>
public string Description {get;set;}
/// <summary>
/// Desc:图片地址
/// Default:
/// Nullable:True
/// </summary>
public string ImageUrl {get;set;}
/// <summary>
/// Desc:条件类型: EvolutionCount, FeedingCount等
/// Default:
/// Nullable:False
/// </summary>
public string ConditionType {get;set;}
/// <summary>
/// Desc:条件阈值
/// Default:
/// Nullable:False
/// </summary>
public int ConditionValue {get;set;}
/// <summary>
/// Desc:排序
/// Default:0
/// Nullable:False
/// </summary>
public int SortOrder {get;set;}
/// <summary>
/// Desc:勋章的类型: Pet, Community
/// Default:Pet
/// Nullable:False
/// </summary>
public string Type {get;set;}
/// <summary>
/// Desc:状态: Active, Inactive
/// Default:Active
/// Nullable:False
/// </summary>
public string Status {get;set;}
}
}

View File

@ -0,0 +1,50 @@
using SqlSugar;
namespace QYZH.InteractiveMagazine.Models.Entity
{
///<summary>
///模板评论表
///</summary>
[SugarTable("MessageComment")]
public partial class MessageComment : BaseEntity
{
public MessageComment(){
}
/// <summary>
/// Desc:用户Id
/// Default:
/// Nullable:False
/// </summary>
public long UserId {get;set;}
/// <summary>
/// Desc:消息Id
/// Default:
/// Nullable:False
/// </summary>
public long MessageId {get;set;}
/// <summary>
/// Desc:模板Id
/// Default:
/// Nullable:False
/// </summary>
public int TemplateId {get;set;}
/// <summary>
/// Desc:评论类型
/// Default:TemplateComment
/// Nullable:False
/// </summary>
public string Type {get;set;}
/// <summary>
/// Desc:状态: Published, Hidden
/// Default:Published
/// Nullable:False
/// </summary>
public string Status {get;set;}
}
}

View File

@ -0,0 +1,43 @@
using SqlSugar;
namespace QYZH.InteractiveMagazine.Models.Entity
{
///<summary>
///点赞记录表
///</summary>
[SugarTable("MessageLike")]
public partial class MessageLike : BaseEntity
{
public MessageLike(){
}
/// <summary>
/// Desc:用户Id
/// Default:
/// Nullable:False
/// </summary>
public long UserId {get;set;}
/// <summary>
/// Desc:消息Id
/// Default:
/// Nullable:False
/// </summary>
public long MessageId {get;set;}
/// <summary>
/// Desc:点赞类型
/// Default:Like
/// Nullable:False
/// </summary>
public string Type {get;set;}
/// <summary>
/// Desc:状态: Liked, Cancelled
/// Default:Liked
/// Nullable:False
/// </summary>
public string Status {get;set;}
}
}

View File

@ -0,0 +1,64 @@
using SqlSugar;
namespace QYZH.InteractiveMagazine.Models.Entity
{
///<summary>
///宠物实例表
///</summary>
[SugarTable("Pet")]
public partial class Pet : BaseEntity
{
public Pet(){
}
/// <summary>
/// Desc:用户Id
/// Default:
/// Nullable:False
/// </summary>
public long UserId {get;set;}
/// <summary>
/// Desc:宠物昵称
/// Default:
/// Nullable:True
/// </summary>
public string Name {get;set;}
/// <summary>
/// Desc:当前进化形态Id
/// Default:
/// Nullable:False
/// </summary>
public int CurrentEvolutionId {get;set;}
/// <summary>
/// Desc:当前成长值
/// Default:0
/// Nullable:False
/// </summary>
public int GrowthPoints {get;set;}
/// <summary>
/// Desc:累计喂养次数
/// Default:0
/// Nullable:False
/// </summary>
public int FeedingCount {get;set;}
/// <summary>
/// Desc:宠物类型
/// Default:Normal
/// Nullable:False
/// </summary>
public string Type {get;set;}
/// <summary>
/// Desc:状态: Active, Sleeping
/// Default:Active
/// Nullable:False
/// </summary>
public string Status {get;set;}
}
}

View File

@ -0,0 +1,100 @@
using SqlSugar;
namespace QYZH.InteractiveMagazine.Models.Entity
{
///<summary>
///宠物进化链定义表
///</summary>
[SugarTable("PetEvolution")]
public partial class PetEvolution : BaseEntity
{
public PetEvolution(){
}
/// <summary>
/// Desc:主键
/// Default:
/// Nullable:False
/// </summary>
[SugarColumn(IsPrimaryKey=true,IsIdentity=true)]
public new int Id {get;set;}
/// <summary>
/// Desc:阶段名称
/// Default:
/// Nullable:False
/// </summary>
public string StageName {get;set;}
/// <summary>
/// Desc:阶段等级
/// Default:
/// Nullable:False
/// </summary>
public int StageLevel {get;set;}
/// <summary>
/// Desc:进化所需成长值
/// Default:
/// Nullable:False
/// </summary>
public int RequiredGrowth {get;set;}
/// <summary>
/// Desc:形态图片
/// Default:
/// Nullable:True
/// </summary>
public string ImageUrl {get;set;}
/// <summary>
/// Desc:前一形态Id
/// Default:
/// Nullable:True
/// </summary>
public int? PreviousEvolutionId {get;set;}
/// <summary>
/// Desc:基础力量
/// Default:0
/// Nullable:False
/// </summary>
public int BaseStrength {get;set;}
/// <summary>
/// Desc:基础敏捷
/// Default:0
/// Nullable:False
/// </summary>
public int BaseAgility {get;set;}
/// <summary>
/// Desc:基础智力
/// Default:0
/// Nullable:False
/// </summary>
public int BaseIntelligence {get;set;}
/// <summary>
/// Desc:基础魅力
/// Default:0
/// Nullable:False
/// </summary>
public int BaseCharm {get;set;}
/// <summary>
/// Desc:进化类型: Normal, Special
/// Default:Normal
/// Nullable:False
/// </summary>
public string Type {get;set;}
/// <summary>
/// Desc:状态: Active, Inactive
/// Default:Active
/// Nullable:False
/// </summary>
public string Status {get;set;}
}
}

View File

@ -0,0 +1,71 @@
using SqlSugar;
namespace QYZH.InteractiveMagazine.Models.Entity
{
///<summary>
///宠物喂养记录表
///</summary>
[SugarTable("PetFeedingRecord")]
public partial class PetFeedingRecord : BaseEntity
{
public PetFeedingRecord(){
}
/// <summary>
/// Desc:宠物Id
/// Default:
/// Nullable:False
/// </summary>
public long PetId {get;set;}
/// <summary>
/// Desc:用户Id
/// Default:
/// Nullable:False
/// </summary>
public long UserId {get;set;}
/// <summary>
/// Desc:消耗积分
/// Default:
/// Nullable:False
/// </summary>
public int PointsUsed {get;set;}
/// <summary>
/// Desc:成长值变化量
/// Default:
/// Nullable:False
/// </summary>
public int GrowthChange {get;set;}
/// <summary>
/// Desc:喂养前成长值
/// Default:
/// Nullable:False
/// </summary>
public int GrowthBefore {get;set;}
/// <summary>
/// Desc:喂养后成长值
/// Default:
/// Nullable:False
/// </summary>
public int GrowthAfter {get;set;}
/// <summary>
/// Desc:喂养类型: Normal, Special
/// Default:Normal
/// Nullable:False
/// </summary>
public string Type {get;set;}
/// <summary>
/// Desc:状态
/// Default:Success
/// Nullable:False
/// </summary>
public string Status {get;set;}
}
}

View File

@ -0,0 +1,71 @@
using SqlSugar;
namespace QYZH.InteractiveMagazine.Models.Entity
{
///<summary>
///积分流水记录表
///</summary>
[SugarTable("PointsRecord")]
public partial class PointsRecord : BaseEntity
{
public PointsRecord(){
}
/// <summary>
/// Desc:用户Id
/// Default:
/// Nullable:False
/// </summary>
public long UserId {get;set;}
/// <summary>
/// Desc:变动数值
/// Default:
/// Nullable:False
/// </summary>
public int ChangeAmount {get;set;}
/// <summary>
/// Desc:变动后余额
/// Default:
/// Nullable:False
/// </summary>
public int BalanceAfter {get;set;}
/// <summary>
/// Desc:变动类型: Exchange, FeedPet, SignIn, TaskReward
/// Default:
/// Nullable:False
/// </summary>
public string ChangeType {get;set;}
/// <summary>
/// Desc:关联业务Id
/// Default:
/// Nullable:True
/// </summary>
public long? RelatedId {get;set;}
/// <summary>
/// Desc:备注
/// Default:
/// Nullable:True
/// </summary>
public string Description {get;set;}
/// <summary>
/// Desc:流水分类
/// Default:
/// Nullable:True
/// </summary>
public string Type {get;set;}
/// <summary>
/// Desc:状态: Success, Failed, Pending
/// Default:Success
/// Nullable:False
/// </summary>
public string Status {get;set;}
}
}

View File

@ -0,0 +1,86 @@
using SqlSugar;
namespace QYZH.InteractiveMagazine.Models.Entity
{
///<summary>
///虚拟商品表
///</summary>
[SugarTable("Product")]
public partial class Product : BaseEntity
{
public Product(){
}
/// <summary>
/// Desc:主键
/// Default:
/// Nullable:False
/// </summary>
[SugarColumn(IsPrimaryKey=true,IsIdentity=true)]
public new int Id {get;set;}
/// <summary>
/// Desc:商品名称
/// Default:
/// Nullable:False
/// </summary>
public string Name {get;set;}
/// <summary>
/// Desc:描述
/// Default:
/// Nullable:True
/// </summary>
public string Description {get;set;}
/// <summary>
/// Desc:商品图片
/// Default:
/// Nullable:True
/// </summary>
public string ImageUrl {get;set;}
/// <summary>
/// Desc:所需积分
/// Default:
/// Nullable:False
/// </summary>
public int Price {get;set;}
/// <summary>
/// Desc:商品类型: MakeUpCard, PetBg
/// Default:
/// Nullable:False
/// </summary>
public string Type {get;set;}
/// <summary>
/// Desc:状态: OnSale, OffSale
/// Default:OnSale
/// Nullable:False
/// </summary>
public string Status {get;set;}
/// <summary>
/// Desc:扩展数据
/// Default:
/// Nullable:True
/// </summary>
public string MetaData {get;set;}
/// <summary>
/// Desc:是否上架
/// Default:b'1'
/// Nullable:False
/// </summary>
public bool IsActive {get;set;}
/// <summary>
/// Desc:库存(-1无限)
/// Default:-1
/// Nullable:False
/// </summary>
public int Stock {get;set;}
}
}

View File

@ -0,0 +1,51 @@
using SqlSugar;
namespace QYZH.InteractiveMagazine.Models.Entity
{
///<summary>
///固定模板言论表
///</summary>
[SugarTable("TemplateSentence")]
public partial class TemplateSentence : BaseEntity
{
public TemplateSentence(){
}
/// <summary>
/// Desc:主键
/// Default:
/// Nullable:False
/// </summary>
[SugarColumn(IsPrimaryKey=true,IsIdentity=true)]
public new int Id {get;set;}
/// <summary>
/// Desc:模板内容
/// Default:
/// Nullable:False
/// </summary>
public string Sentence {get;set;}
/// <summary>
/// Desc:是否启用
/// Default:b'1'
/// Nullable:False
/// </summary>
public bool IsActive {get;set;}
/// <summary>
/// Desc:分类: Positive, Neutral, Negative
/// Default:Neutral
/// Nullable:False
/// </summary>
public string Type {get;set;}
/// <summary>
/// Desc:状态: Active, Inactive
/// Default:Active
/// Nullable:False
/// </summary>
public string Status {get;set;}
}
}

View File

@ -0,0 +1,71 @@
using SqlSugar;
namespace QYZH.InteractiveMagazine.Models.Entity
{
///<summary>
///用户表
///</summary>
[SugarTable("User")]
public partial class User : BaseEntity
{
public User(){
}
/// <summary>
/// Desc:微信OpenId
/// Default:
/// Nullable:False
/// </summary>
public string OpenId {get;set;}
/// <summary>
/// Desc:微信UnionId
/// Default:
/// Nullable:True
/// </summary>
public string UnionId {get;set;}
/// <summary>
/// Desc:昵称
/// Default:
/// Nullable:True
/// </summary>
public string NickName {get;set;}
/// <summary>
/// Desc:头像地址
/// Default:
/// Nullable:True
/// </summary>
public string AvatarUrl {get;set;}
/// <summary>
/// Desc:手机号
/// Default:
/// Nullable:True
/// </summary>
public string Phone {get;set;}
/// <summary>
/// Desc:积分余额
/// Default:0
/// Nullable:False
/// </summary>
public int Points {get;set;}
/// <summary>
/// Desc:用户类型: Normal, VIP
/// Default:Normal
/// Nullable:False
/// </summary>
public string Type {get;set;}
/// <summary>
/// Desc:状态: Active, Disabled
/// Default:Active
/// Nullable:False
/// </summary>
public string Status {get;set;}
}
}

View File

@ -0,0 +1,64 @@
using SqlSugar;
namespace QYZH.InteractiveMagazine.Models.Entity
{
///<summary>
///用户背包表
///</summary>
[SugarTable("UserBag")]
public partial class UserBag : BaseEntity
{
public UserBag(){
}
/// <summary>
/// Desc:用户Id
/// Default:
/// Nullable:False
/// </summary>
public long UserId {get;set;}
/// <summary>
/// Desc:物品类型: MakeUpCard, PetBg
/// Default:
/// Nullable:False
/// </summary>
public string ItemType {get;set;}
/// <summary>
/// Desc:关联商品Id或资源Id
/// Default:
/// Nullable:False
/// </summary>
public int ItemId {get;set;}
/// <summary>
/// Desc:数量
/// Default:1
/// Nullable:False
/// </summary>
public int Quantity {get;set;}
/// <summary>
/// Desc:扩展信息
/// Default:
/// Nullable:True
/// </summary>
public string MetaData {get;set;}
/// <summary>
/// Desc:背包物品分类
/// Default:
/// Nullable:True
/// </summary>
public string Type {get;set;}
/// <summary>
/// Desc:状态: Available, Expired
/// Default:Available
/// Nullable:False
/// </summary>
public string Status {get;set;}
}
}

View File

@ -0,0 +1,50 @@
using SqlSugar;
namespace QYZH.InteractiveMagazine.Models.Entity
{
///<summary>
///用户勋章表
///</summary>
[SugarTable("UserMedal")]
public partial class UserMedal : BaseEntity
{
public UserMedal(){
}
/// <summary>
/// Desc:用户Id
/// Default:
/// Nullable:False
/// </summary>
public long UserId {get;set;}
/// <summary>
/// Desc:勋章Id
/// Default:
/// Nullable:False
/// </summary>
public int MedalId {get;set;}
/// <summary>
/// Desc:获得时间
/// Default:
/// Nullable:True
/// </summary>
public DateTime? AwardedAt {get;set;}
/// <summary>
/// Desc:勋章记录类型
/// Default:
/// Nullable:True
/// </summary>
public string Type {get;set;}
/// <summary>
/// Desc:状态: Awarded, Revoked
/// Default:Awarded
/// Nullable:False
/// </summary>
public string Status {get;set;}
}
}

View File

@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace QYZH.InteractiveMagazine.Models.Enum
{
public enum ApiVersionEnum
{
/// <summary>
/// Platform
/// </summary>
[Description("平台管理")]
Platform = 10,
/// <summary>
/// Wechat
/// </summary>
[Description("小程序管理")]
Wechat = 20,
}
}

View File

@ -1,9 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>

View File

@ -0,0 +1,13 @@
using QYZH.InteractiveMagazine.Models.Entity;
namespace QYZH.InteractiveMagazine.Repository;
public class AdminUserRepository : BaseRepository<AdminUser>, IAdminUserRepository
{
public async Task<AdminUser?> GetByUserNameAsync(string userName)
{
return await Db.Queryable<AdminUser>()
.Where(x => x.UserName == userName)
.FirstAsync();
}
}

View File

@ -146,4 +146,9 @@ public class BaseRepository<T> : IBaseRepository<T> where T : class, new()
{
return await Db.Queryable<T>().Where(where).CountAsync();
}
public ISugarQueryable<T> Queryable()
{
return Db.Queryable<T>();
}
}

View File

@ -0,0 +1,8 @@
using QYZH.InteractiveMagazine.Models.Entity;
namespace QYZH.InteractiveMagazine.Repository;
public interface IAdminUserRepository : IBaseRepository<AdminUser>
{
Task<AdminUser?> GetByUserNameAsync(string userName);
}

View File

@ -0,0 +1,168 @@
using BCrypt.Net;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using QYZH.InteractiveMagazine.Infrastructure.Auth;
using QYZH.InteractiveMagazine.Infrastructure.Cache;
using QYZH.InteractiveMagazine.IService;
using QYZH.InteractiveMagazine.IService.Dto;
using QYZH.InteractiveMagazine.Models.Common;
using QYZH.InteractiveMagazine.Models.Entity;
using QYZH.InteractiveMagazine.Models.Settings;
using QYZH.InteractiveMagazine.Repository;
namespace QYZH.InteractiveMagazine.Service;
public class AdminAuthService : IAdminAuthService
{
private readonly IAdminUserRepository _adminUserRepository;
private readonly IConfiguration _configuration;
private readonly ILogger<AdminAuthService> _logger;
private const string TokenKeyPrefix = "InteractiveMagazine:AdminAuth:Token";
private const string UserInfoKeyPrefix = "InteractiveMagazine:AdminAuth:UserInfo";
public AdminAuthService(IAdminUserRepository adminUserRepository, IConfiguration configuration, ILogger<AdminAuthService> logger)
{
_adminUserRepository = adminUserRepository;
_configuration = configuration;
_logger = logger;
}
public async Task<AdminLoginOutput> LoginAsync(AdminLoginInput input)
{
_logger.LogInformation("管理员登录尝试,用户名: {UserName}", input.UserName);
if (string.IsNullOrWhiteSpace(input.UserName))
{
throw new BusinessException("用户名不能为空", 400);
}
if (string.IsNullOrWhiteSpace(input.Password))
{
throw new BusinessException("密码不能为空", 400);
}
var adminUser = await _adminUserRepository.GetByUserNameAsync(input.UserName);
if (adminUser == null)
{
_logger.LogWarning("管理员登录失败,用户名不存在: {UserName}", input.UserName);
throw new BusinessException("用户名或密码错误", 401);
}
if (!BCrypt.Net.BCrypt.Verify(input.Password, adminUser.PasswordHash))
{
_logger.LogWarning("管理员登录失败,密码错误: {UserName}", input.UserName);
throw new BusinessException("用户名或密码错误", 401);
}
if (adminUser.Status != "Active")
{
_logger.LogWarning("管理员登录失败,账号已禁用: {UserName}", input.UserName);
throw new BusinessException("账号已被禁用,请联系系统管理员", 403);
}
var jwtSettings = GetJwtSettings();
var token = JwtHelper.GenerateToken((long)adminUser.Id, adminUser.UserName, jwtSettings);
await RedisHelper.StringSetAsync($"{TokenKeyPrefix}:{adminUser.Id}", token, TimeSpan.FromMinutes(jwtSettings.ExpiryMinutes));
_logger.LogInformation("管理员登录成功,用户名: {UserName}, ID: {UserId}", input.UserName, adminUser.Id);
return new AdminLoginOutput
{
Token = token,
UserId = (long)adminUser.Id,
UserName = adminUser.UserName,
Type = adminUser.Type
};
}
public async Task LogoutAsync(long userId)
{
_logger.LogInformation("管理员登出ID: {UserId}", userId);
await RedisHelper.KeyDeleteAsync($"{TokenKeyPrefix}:{userId}");
_logger.LogInformation("管理员登出成功ID: {UserId}", userId);
}
public async Task<AdminUserInfoOutput> GetAdminInfoAsync(long userId)
{
_logger.LogInformation("获取管理员信息ID: {UserId}", userId);
var adminUser = await _adminUserRepository.GetByIdAsync(userId);
if (adminUser == null)
{
_logger.LogWarning("未找到管理员ID: {UserId}", userId);
throw new BusinessException("用户不存在", 404);
}
return new AdminUserInfoOutput
{
UserId = adminUser.Id,
UserName = adminUser.UserName,
Type = adminUser.Type,
Status = adminUser.Status
};
}
public async Task ChangePasswordAsync(long userId, string oldPassword, string newPassword)
{
_logger.LogInformation("管理员修改密码尝试ID: {UserId}", userId);
if (string.IsNullOrWhiteSpace(oldPassword))
{
throw new BusinessException("原密码不能为空", 400);
}
if (string.IsNullOrWhiteSpace(newPassword))
{
throw new BusinessException("新密码不能为空", 400);
}
var adminUser = await _adminUserRepository.GetByIdAsync(userId);
if (adminUser == null)
{
_logger.LogWarning("未找到管理员ID: {UserId}", userId);
throw new BusinessException("用户不存在", 404);
}
if (!BCrypt.Net.BCrypt.Verify(oldPassword, adminUser.PasswordHash))
{
_logger.LogWarning("管理员修改密码失败原密码错误ID: {UserId}", userId);
throw new BusinessException("原密码错误", 400);
}
adminUser.PasswordHash = BCrypt.Net.BCrypt.HashPassword(newPassword);
var result = await _adminUserRepository.UpdateAsync(adminUser);
if (!result)
{
throw new BusinessException("修改密码失败", 500);
}
await RedisHelper.KeyDeleteAsync($"{TokenKeyPrefix}:{userId}");
_logger.LogInformation("管理员修改密码成功ID: {UserId}", userId);
}
private JwtSettings GetJwtSettings()
{
var jwtSettings = _configuration.GetSection("JwtSettings").Get<JwtSettings>()
?? new JwtSettings
{
Issuer = "QYZH.InteractiveMagazine",
Audience = "QYZH.InteractiveMagazine",
SecretKey = "your-256-bit-secret-key-here-change-in-production",
ExpiryMinutes = 120
};
if (string.IsNullOrWhiteSpace(jwtSettings.SecretKey))
{
throw new BusinessException("JWT 配置不完整", 500);
}
return jwtSettings;
}
}

View File

@ -0,0 +1,267 @@
using System.Linq.Expressions;
using BCrypt.Net;
using Microsoft.Extensions.Logging;
using QYZH.InteractiveMagazine.IService;
using QYZH.InteractiveMagazine.IService.Dto;
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>
public class AdminUserService : IAdminUserService
{
private readonly IAdminUserRepository _adminUserRepository;
private readonly ILogger<AdminUserService> _logger;
public AdminUserService(IAdminUserRepository adminUserRepository, ILogger<AdminUserService> logger)
{
_adminUserRepository = adminUserRepository;
_logger = logger;
}
/// <summary>
/// 创建管理员
/// </summary>
public async Task<AdminUserOutput> CreateAsync(AdminUserInput input)
{
_logger.LogInformation("正在创建管理员,用户名: {UserName}", input.UserName);
if (string.IsNullOrWhiteSpace(input.UserName))
{
throw new BusinessException("用户名不能为空", 400);
}
if (string.IsNullOrWhiteSpace(input.Password))
{
throw new BusinessException("密码不能为空", 400);
}
// 检查用户名是否已存在
var existingUser = await _adminUserRepository.GetByUserNameAsync(input.UserName);
if (existingUser != null)
{
_logger.LogWarning("创建管理员失败,用户名已存在: {UserName}", input.UserName);
throw new BusinessException("用户名已存在", 400);
}
var adminUser = new AdminUser
{
UserName = input.UserName.Trim(),
PasswordHash = BCrypt.Net.BCrypt.HashPassword(input.Password),
Type = input.Type,
Status = input.Status,
CreatedBy = "System",
UpdatedBy = "System",
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now,
IsDeleted = false
};
var result = await _adminUserRepository.InsertAsync(adminUser);
if (!result)
{
_logger.LogError("管理员创建失败,用户名: {UserName}", input.UserName);
throw new BusinessException("创建管理员失败", 500);
}
_logger.LogInformation("管理员创建成功,用户名: {UserName}, ID: {Id}", input.UserName, adminUser.Id);
return MapToOutput(adminUser);
}
/// <summary>
/// 更新管理员
/// </summary>
public async Task<AdminUserOutput> UpdateAsync(long id, AdminUserInput input)
{
_logger.LogInformation("正在更新管理员ID: {Id}", id);
var adminUser = await _adminUserRepository.GetByIdAsync(id);
if (adminUser == null)
{
_logger.LogWarning("未找到要更新的管理员ID: {Id}", id);
throw new BusinessException("管理员不存在", 404);
}
// 如果用户名有变更,检查是否与其他用户重复
if (!string.IsNullOrWhiteSpace(input.UserName) && input.UserName != adminUser.UserName)
{
var existingUser = await _adminUserRepository.GetByUserNameAsync(input.UserName.Trim());
if (existingUser != null && existingUser.Id != id)
{
_logger.LogWarning("更新管理员失败,用户名已存在: {UserName}", input.UserName);
throw new BusinessException("用户名已存在", 400);
}
adminUser.UserName = input.UserName.Trim();
}
// 如果提供了密码,则更新密码
if (!string.IsNullOrWhiteSpace(input.Password))
{
adminUser.PasswordHash = BCrypt.Net.BCrypt.HashPassword(input.Password);
}
if (!string.IsNullOrWhiteSpace(input.Type))
{
adminUser.Type = input.Type;
}
if (!string.IsNullOrWhiteSpace(input.Status))
{
adminUser.Status = input.Status;
}
adminUser.UpdatedBy = "System";
adminUser.UpdatedAt = DateTime.Now;
var result = await _adminUserRepository.UpdateAsync(adminUser);
if (!result)
{
_logger.LogError("管理员更新失败ID: {Id}", id);
throw new BusinessException("更新管理员失败", 500);
}
_logger.LogInformation("管理员更新成功ID: {Id}", id);
return MapToOutput(adminUser);
}
/// <summary>
/// 删除管理员(软删除)
/// </summary>
public async Task DeleteAsync(long id)
{
_logger.LogInformation("正在删除管理员ID: {Id}", id);
var adminUser = await _adminUserRepository.GetByIdAsync(id);
if (adminUser == null)
{
_logger.LogWarning("未找到要删除的管理员ID: {Id}", id);
throw new BusinessException("管理员不存在", 404);
}
var result = await _adminUserRepository.DeleteByIdAsync(id);
if (!result)
{
_logger.LogError("管理员删除失败ID: {Id}", id);
throw new BusinessException("删除管理员失败", 500);
}
_logger.LogInformation("管理员删除成功ID: {Id}", id);
}
/// <summary>
/// 根据ID获取管理员
/// </summary>
public async Task<AdminUserOutput> GetByIdAsync(long id)
{
_logger.LogInformation("正在获取管理员信息ID: {Id}", id);
var adminUser = await _adminUserRepository.GetByIdAsync(id);
if (adminUser == null)
{
_logger.LogWarning("未找到管理员ID: {Id}", id);
throw new BusinessException("管理员不存在", 404);
}
return MapToOutput(adminUser);
}
/// <summary>
/// 分页查询管理员列表
/// </summary>
public async Task<PageListModel<AdminUserOutput>> GetListAsync(AdminUserQueryInput input)
{
_logger.LogInformation("正在查询管理员列表,页码: {PageIndex}, 每页条数: {PageSize}", input.PageIndex, input.PageSize);
if (input.PageIndex <= 0)
{
throw new BusinessException("页码必须大于0", 400);
}
if (input.PageSize <= 0 || input.PageSize > 100)
{
throw new BusinessException("每页条数必须在1-100之间", 400);
}
var pageResult = await _adminUserRepository.GetPageListAsync(
BuildQueryExpression(input),
input
);
// 转换为输出DTO
var outputList = pageResult.List?.Select(MapToOutput).ToList() ?? new List<AdminUserOutput>();
return new PageListModel<AdminUserOutput>
{
List = outputList,
TotalCount = pageResult.TotalCount,
PageIndex = pageResult.PageIndex,
PageSize = pageResult.PageSize
};
}
/// <summary>
/// 构建查询表达式
/// </summary>
private static Expression<Func<AdminUser, bool>> BuildQueryExpression(AdminUserQueryInput input)
{
Expression<Func<AdminUser, bool>> where = x => !x.IsDeleted;
if (!string.IsNullOrWhiteSpace(input.UserName))
{
var userName = input.UserName;
Expression<Func<AdminUser, bool>> userNameCondition = x => x.UserName.Contains(userName);
where = Expression.Lambda<Func<AdminUser, bool>>(
Expression.AndAlso(where.Body,
Expression.Invoke(userNameCondition, where.Parameters[0])),
where.Parameters);
}
if (!string.IsNullOrWhiteSpace(input.Type))
{
var type = input.Type;
Expression<Func<AdminUser, bool>> typeCondition = x => x.Type == type;
where = Expression.Lambda<Func<AdminUser, bool>>(
Expression.AndAlso(where.Body,
Expression.Invoke(typeCondition, where.Parameters[0])),
where.Parameters);
}
if (!string.IsNullOrWhiteSpace(input.Status))
{
var status = input.Status;
Expression<Func<AdminUser, bool>> statusCondition = x => x.Status == status;
where = Expression.Lambda<Func<AdminUser, bool>>(
Expression.AndAlso(where.Body,
Expression.Invoke(statusCondition, where.Parameters[0])),
where.Parameters);
}
return where;
}
/// <summary>
/// 将实体映射为输出DTO
/// </summary>
private static AdminUserOutput MapToOutput(AdminUser adminUser)
{
return new AdminUserOutput
{
Id = adminUser.Id,
UserName = adminUser.UserName,
Type = adminUser.Type,
Status = adminUser.Status,
CreatedBy = adminUser.CreatedBy,
CreatedAt = adminUser.CreatedAt,
UpdatedBy = adminUser.UpdatedBy,
UpdatedAt = adminUser.UpdatedAt
};
}
}

View File

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

View File

@ -182,23 +182,19 @@ public class BaseService<T> : IBaseService<T> where T : class, new()
if (entity is BaseEntity baseEntity)
{
var now = DateTime.Now;
baseEntity.CreatedTime = now;
baseEntity.UpdatedTime = now;
baseEntity.CreatedAt = now;
baseEntity.UpdatedAt = 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.UpdatedAt = DateTime.Now;
baseEntity.UpdatedBy = baseEntity.UpdatedBy ?? "system";
}
}

View File

@ -10,6 +10,7 @@
<ItemGroup>
<PackageReference Include="AutoMapper" Version="16.1.1" />
<PackageReference Include="BCrypt.Net-Next" Version="4.2.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.8" />
</ItemGroup>

View File

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

View File

@ -0,0 +1,205 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using QYZH.InteractiveMagazine.IService;
using QYZH.InteractiveMagazine.IService.Dto;
using QYZH.InteractiveMagazine.Models.Common;
using QYZH.InteractiveMagazine.Models.Dto;
using QYZH.InteractiveMagazine.Models.Enum;
namespace QYZH.InteractiveMagazine.WebApi.Controllers;
[Route("api/[controller]")]
[ApiController]
public class AdminController : BaseController
{
private readonly IAdminAuthService _adminAuthService;
private readonly IAdminUserService _adminUserService;
private readonly ILogger<AdminController> _logger;
public AdminController(IAdminAuthService adminAuthService, IAdminUserService adminUserService, ILogger<AdminController> logger)
{
_adminAuthService = adminAuthService;
_adminUserService = adminUserService;
_logger = logger;
}
[AllowAnonymous]
[HttpPost("login")]
public async Task<BaseResponse<AdminLoginOutput>> LoginAsync([FromBody] AdminLoginInput input)
{
var result = await _adminAuthService.LoginAsync(input);
return Success(result);
}
[HttpPost("logout")]
public async Task<BaseResponse<object>> LogoutAsync()
{
var userId = GetCurrentUserId();
if (userId == null)
{
return Fail("未获取到用户信息", 401);
}
await _adminAuthService.LogoutAsync(userId.Value);
return Success(new object(), "登出成功");
}
[HttpGet("info")]
public async Task<BaseResponse<AdminUserInfoOutput>> GetAdminInfoAsync()
{
var userId = GetCurrentUserId();
if (userId == null)
{
return BaseResponse<AdminUserInfoOutput>.Fail("未获取到用户信息", 401);
}
var result = await _adminAuthService.GetAdminInfoAsync(userId.Value);
return Success(result);
}
[HttpPost("changePassword")]
public async Task<BaseResponse<object>> ChangePasswordAsync([FromBody] ChangePasswordInput input)
{
var userId = GetCurrentUserId();
if (userId == null)
{
return Fail("未获取到用户信息", 401);
}
await _adminAuthService.ChangePasswordAsync(userId.Value, input.OldPassword, input.NewPassword);
return Success(new object(), "密码修改成功");
}
/// <summary>
/// 创建管理员
/// </summary>
/// <param name="input">管理员输入</param>
/// <returns>创建的管理员信息</returns>
[HttpPost("users")]
public async Task<BaseResponse<AdminUserOutput>> CreateUserAsync([FromBody] AdminUserInput input)
{
try
{
var result = await _adminUserService.CreateAsync(input);
return Success(result, "创建管理员成功");
}
catch (BusinessException ex)
{
_logger.LogWarning(ex, "创建管理员业务异常: {Message}", ex.Message);
return BaseResponse<AdminUserOutput>.Fail(ex.Message, ex.Code);
}
catch (Exception ex)
{
_logger.LogError(ex, "创建管理员系统异常,参数:{Input}", input);
return BaseResponse<AdminUserOutput>.Fail("创建管理员失败,请稍后重试", 500);
}
}
/// <summary>
/// 更新管理员
/// </summary>
/// <param name="id">管理员ID</param>
/// <param name="input">管理员输入</param>
/// <returns>更新后的管理员信息</returns>
[HttpPut("users/{id}")]
public async Task<BaseResponse<AdminUserOutput>> UpdateUserAsync(long id, [FromBody] AdminUserInput input)
{
try
{
var result = await _adminUserService.UpdateAsync(id, input);
return Success(result, "更新管理员成功");
}
catch (BusinessException ex)
{
_logger.LogWarning(ex, "更新管理员业务异常: {Message}", ex.Message);
return BaseResponse<AdminUserOutput>.Fail(ex.Message, ex.Code);
}
catch (Exception ex)
{
_logger.LogError(ex, "更新管理员系统异常ID{Id},参数:{Input}", id, input);
return BaseResponse<AdminUserOutput>.Fail("更新管理员失败,请稍后重试", 500);
}
}
/// <summary>
/// 删除管理员
/// </summary>
/// <param name="id">管理员ID</param>
/// <returns>操作结果</returns>
[HttpDelete("users/{id}")]
public async Task<BaseResponse<object>> DeleteUserAsync(long id)
{
try
{
await _adminUserService.DeleteAsync(id);
return Success(new object(), "删除管理员成功");
}
catch (BusinessException ex)
{
_logger.LogWarning(ex, "删除管理员业务异常: {Message}", ex.Message);
return BaseResponse<object>.Fail(ex.Message, ex.Code);
}
catch (Exception ex)
{
_logger.LogError(ex, "删除管理员系统异常ID{Id}", id);
return BaseResponse<object>.Fail("删除管理员失败,请稍后重试", 500);
}
}
/// <summary>
/// 根据ID获取管理员
/// </summary>
/// <param name="id">管理员ID</param>
/// <returns>管理员信息</returns>
[HttpGet("users/{id}")]
public async Task<BaseResponse<AdminUserOutput>> GetUserByIdAsync(long id)
{
try
{
var result = await _adminUserService.GetByIdAsync(id);
return Success(result);
}
catch (BusinessException ex)
{
_logger.LogWarning(ex, "获取管理员业务异常: {Message}", ex.Message);
return BaseResponse<AdminUserOutput>.Fail(ex.Message, ex.Code);
}
catch (Exception ex)
{
_logger.LogError(ex, "获取管理员系统异常ID{Id}", id);
return BaseResponse<AdminUserOutput>.Fail("获取管理员信息失败,请稍后重试", 500);
}
}
/// <summary>
/// 分页查询管理员列表
/// </summary>
/// <param name="input">查询条件</param>
/// <returns>分页结果</returns>
[HttpPost("users/list")]
public async Task<BaseResponse<PageListModel<AdminUserOutput>>> GetUsersListAsync([FromBody] AdminUserQueryInput input)
{
try
{
var result = await _adminUserService.GetListAsync(input);
return Success(result);
}
catch (BusinessException ex)
{
_logger.LogWarning(ex, "查询管理员列表业务异常: {Message}", ex.Message);
return BaseResponse<PageListModel<AdminUserOutput>>.Fail(ex.Message, ex.Code);
}
catch (Exception ex)
{
_logger.LogError(ex, "查询管理员列表系统异常,参数:{Input}", input);
return BaseResponse<PageListModel<AdminUserOutput>>.Fail("查询管理员列表失败,请稍后重试", 500);
}
}
}
public class ChangePasswordInput
{
public string OldPassword { get; set; } = string.Empty;
public string NewPassword { get; set; } = string.Empty;
}

View File

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

View File

@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Mvc;
using QYZH.InteractiveMagazine.Models.Dto;
using QYZH.InteractiveMagazine.Models.Enum;
namespace QYZH.InteractiveMagazine.WebApi.Controllers;
@ -8,6 +9,7 @@ namespace QYZH.InteractiveMagazine.WebApi.Controllers;
/// </summary>
[ApiController]
[Route("api/[controller]")]
[ApiExplorerSettings(GroupName = nameof(ApiVersionEnum.Platform))]
public abstract class BaseController : ControllerBase
{
/// <summary>

View File

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

View File

@ -0,0 +1,19 @@
# 使用 ASP.NET Core 8.0 运行时基础镜像
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
# 设置工作目录
WORKDIR /app
# 将当前目录(发布文件夹)的所有内容复制到容器内的 /app 目录
COPY . .
# 设置时区(可选)
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
# 暴露端口(根据您的 WebApi 实际监听的端口,通常为 80 或 8080
# 注意:这里只是声明,实际映射需要在运行容器时指定
EXPOSE 8090
# 启动应用
ENTRYPOINT ["dotnet", "QYZH.InteractiveMagazine.WebApi.dll"]

View File

@ -1,38 +1,98 @@
using Autofac;
using Autofac.Extensions.DependencyInjection;
using BCrypt.Net;
using Microsoft.AspNetCore.Mvc;
using Microsoft.OpenApi;
using QYZH.InteractiveMagazine.Common.Extensions;
using QYZH.InteractiveMagazine.Infrastructure.Extensions;
using QYZH.InteractiveMagazine.Infrastructure.Middleware;
using QYZH.InteractiveMagazine.Models.Entity;
using QYZH.InteractiveMagazine.Models.Enum;
using QYZH.InteractiveMagazine.Repository;
using Serilog;
using Swashbuckle.AspNetCore.SwaggerGen;
using Swashbuckle.AspNetCore.SwaggerUI;
using QYZH.InteractiveMagazine.Infrastructure.Autofacs;
var builder = WebApplication.CreateBuilder(args);
// autofac注入 允许使用autofac作为DI容器
builder.UseAutofac();
// 配置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.AddCorsRegister();
// 注册 Swagger 文档
builder.Services.AddSwaggerGen(option =>
{
var xmlFile = $"{AppDomain.CurrentDomain.FriendlyName}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
var modelXml = Path.Combine(AppContext.BaseDirectory, $"QYZH.InteractiveMagazine.Models.xml");
Enum.GetValues<ApiVersionEnum>().ToList().ForEach(version =>
{
// 配置文档信息
option.SwaggerDoc(version.ToString(), new OpenApiInfo
{
Title = AppDomain.CurrentDomain.FriendlyName,
Version = "互动期刊接口文档",
Description = $"{version.GetDescription()}接口Last Modify Time{new FileInfo(xmlPath).LastWriteTime.ToString("yyyy-MM-dd HH:mm:ss")}"
});
});
// 配置接口路径排序
option.OrderActionsBy(o => o.RelativePath);
if (File.Exists(xmlPath))
option.IncludeXmlComments(xmlPath, true);
if (File.Exists(modelXml))
option.IncludeXmlComments(modelXml, true);
option.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme()
{
Description = "请输入 Token格式为 Bearer Token",
Name = "Authorization",
In = ParameterLocation.Header,
Type = SecuritySchemeType.ApiKey,
BearerFormat = "JWT",
Scheme = "Bearer"
});
// 过滤文档/路径/方法筛选接口的响应结果
option.DocInclusionPredicate((docName, apiDesc) =>
{
// 方式 1将接口的 [ApiExplorerSettings(GroupName = "xxx")] 特性匹配
if (!apiDesc.TryGetMethodInfo(out var methodInfo)) return false;
var groupName = methodInfo.DeclaringType?
.GetCustomAttributes(true)
.OfType<ApiExplorerSettingsAttribute>()
.FirstOrDefault()?
.GroupName;
// 匹配当前文档(分组)则显示
return groupName == docName;
});
});
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 =>
{
@ -46,19 +106,27 @@ builder.Services.AddCors(options =>
var app = builder.Build();
// 配置HTTP请求管道
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
app.UseSwaggerUI(c =>
{
// 根据版本名称倒序 遍历展示
Enum.GetValues<ApiVersionEnum>().OrderBy(e => e).ToList().ForEach(version =>
{
c.SwaggerEndpoint($"/swagger/{version}/swagger.json", $"{version.GetDescription()}接口");
});
c.DocExpansion(DocExpansion.None); // ->修改界面打开时自动折叠
});
}
app.UseHttpsRedirection();
app.UseCors("AllowAll");
app.UseMiddleware<GlobalExceptionMiddleware>();
app.UseMiddleware<OperationLogMiddleware>();
app.UseMiddleware<JwtAutoRefreshMiddleware>();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();

View File

@ -4,9 +4,13 @@
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Autofac" Version="9.1.0" />
<PackageReference Include="Autofac.Extensions.DependencyInjection" Version="11.0.0" />
<PackageReference Include="BCrypt.Net-Next" Version="4.2.0" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.27" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.2.0" />
</ItemGroup>
@ -19,4 +23,8 @@
<ProjectReference Include="..\QYZH.InteractiveMagazine.Models\QYZH.InteractiveMagazine.Models.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Controllers\WeChat\" />
</ItemGroup>
</Project>

View File

@ -1,23 +1,23 @@
{
"ConnectionStrings": {
"DefaultConnection": "Server=localhost;Database=interactive_magazine;Uid=root;Pwd=root;Charset=utf8mb4;"
"DefaultConnection": "server=192.168.20.150;port=13306;database=InteractiveMagazine;user=user;password=n68792bu!y99r905;charset=utf8mb4;"
},
"JwtSettings": {
"Issuer": "QYZH.InteractiveMagazine",
"Audience": "QYZH.InteractiveMagazine.Client",
"SecretKey": "your-256-bit-secret-key-here-change-in-production",
"Audience": "QYZH.InteractiveMagazine",
"SecretKey": "zG7pLqR9xVw2bN8fYtHk3mPc5sA1dF6eUjW4gXhC7vB",
"ExpiryMinutes": 120
},
"RedisSettings": {
"ConnectionString": "localhost:6379",
"ConnectionString": "192.168.20.150:16379,defaultDatabase=5",
"InstanceName": "interactive_magazine"
},
"RabbitMQSettings": {
"HostName": "localhost",
"HostName": "192.168.20.150",
"Port": 5672,
"UserName": "guest",
"Password": "guest",
"VirtualHost": "/"
"UserName": "smartschool",
"Password": "@ss%&*otz%d*pq2S",
"VirtualHost": "InteractiveMagazine"
},
"WeChatSettings": {
"AppId": "your-wechat-appid",