Files
OrpaonVision/OrpaonVision.ConfigApp/Infrastructure/Persistence/SqlUserStore.cs
2026-04-06 22:04:05 +08:00

368 lines
15 KiB
C#

using Microsoft.Extensions.Logging;
using Microsoft.Data.SqlClient;
using OrpaonVision.Core.Results;
using OrpaonVision.Core.Security;
using OrpaonVision.Model.Security;
using System.Data;
using System.Text.Json;
namespace OrpaonVision.ConfigApp.Infrastructure.Persistence;
/// <summary>
/// SQL Server 用户仓储实现。
/// </summary>
public sealed class SqlUserStore : IUserStore
{
private readonly ILogger<SqlUserStore> _logger;
private readonly string _connectionString;
/// <summary>
/// 构造函数。
/// </summary>
public SqlUserStore(ILogger<SqlUserStore> logger, string connectionString)
{
_logger = logger;
_connectionString = connectionString;
}
/// <inheritdoc />
public async Task<Result<UserModel?>> GetByIdAsync(Guid id)
{
try
{
const string sql = @"
SELECT Id, Username, DisplayName, Email, PhoneNumber, PasswordHash, PasswordSalt,
Status, LastLoginAtUtc, LastLoginIp, LoginFailedCount, LockedUntilUtc, IsFirstLogin,
CreatedAtUtc, UpdatedAtUtc, CreatedBy, UpdatedBy, Remark
FROM sec_users
WHERE Id = @Id AND Status != @DeletedStatus";
using var connection = new SqlConnection(_connectionString);
await connection.OpenAsync();
using var command = new SqlCommand(sql, connection);
command.Parameters.AddWithValue("@Id", id);
command.Parameters.AddWithValue("@DeletedStatus", (int)UserStatus.Deleted);
using var reader = await command.ExecuteReaderAsync();
if (await reader.ReadAsync())
{
return Result<UserModel?>.Success(MapReaderToUser(reader));
}
return Result<UserModel?>.Success(null);
}
catch (Exception ex)
{
_logger.LogError(ex, "根据ID获取用户失败: {UserId}", id);
return Result<UserModel?>.Fail("USER_GET_BY_ID_FAILED", "获取用户失败");
}
}
/// <inheritdoc />
public async Task<Result<UserModel?>> GetByUsernameAsync(string username)
{
try
{
const string sql = @"
SELECT Id, Username, DisplayName, Email, PhoneNumber, PasswordHash, PasswordSalt,
Status, LastLoginAtUtc, LastLoginIp, LoginFailedCount, LockedUntilUtc, IsFirstLogin,
CreatedAtUtc, UpdatedAtUtc, CreatedBy, UpdatedBy, Remark
FROM sec_users
WHERE Username = @Username AND Status != @DeletedStatus";
using var connection = new SqlConnection(_connectionString);
await connection.OpenAsync();
using var command = new SqlCommand(sql, connection);
command.Parameters.AddWithValue("@Username", username);
command.Parameters.AddWithValue("@DeletedStatus", (int)UserStatus.Deleted);
using var reader = await command.ExecuteReaderAsync();
if (await reader.ReadAsync())
{
return Result<UserModel?>.Success(MapReaderToUser(reader));
}
return Result<UserModel?>.Success(null);
}
catch (Exception ex)
{
_logger.LogError(ex, "根据用户名获取用户失败: {Username}", username);
return Result<UserModel?>.Fail("USER_GET_BY_USERNAME_FAILED", "获取用户失败");
}
}
/// <inheritdoc />
public async Task<Result<UserModel>> CreateAsync(UserModel user)
{
try
{
const string sql = @"
INSERT INTO sec_users (
Id, Username, DisplayName, Email, PhoneNumber, PasswordHash, PasswordSalt,
Status, CreatedAtUtc, UpdatedAtUtc, CreatedBy, UpdatedBy, Remark
) VALUES (
@Id, @Username, @DisplayName, @Email, @PhoneNumber, @PasswordHash, @PasswordSalt,
@Status, @CreatedAtUtc, @UpdatedAtUtc, @CreatedBy, @UpdatedBy, @Remark
)";
using var connection = new SqlConnection(_connectionString);
await connection.OpenAsync();
using var command = new SqlCommand(sql, connection);
command.Parameters.AddWithValue("@Id", user.Id);
command.Parameters.AddWithValue("@Username", user.Username);
command.Parameters.AddWithValue("@DisplayName", user.DisplayName);
command.Parameters.AddWithValue("@Email", user.Email);
command.Parameters.AddWithValue("@PhoneNumber", (object?)user.PhoneNumber ?? DBNull.Value);
command.Parameters.AddWithValue("@PasswordHash", user.PasswordHash);
command.Parameters.AddWithValue("@PasswordSalt", user.PasswordSalt);
command.Parameters.AddWithValue("@Status", (int)user.Status);
command.Parameters.AddWithValue("@CreatedAtUtc", user.CreatedAtUtc);
command.Parameters.AddWithValue("@UpdatedAtUtc", user.UpdatedAtUtc);
command.Parameters.AddWithValue("@CreatedBy", user.CreatedBy);
command.Parameters.AddWithValue("@UpdatedBy", user.UpdatedBy);
command.Parameters.AddWithValue("@Remark", (object?)user.Remark ?? DBNull.Value);
await command.ExecuteNonQueryAsync();
_logger.LogInformation("用户创建成功: {UserId} - {Username}", user.Id, user.Username);
return Result<UserModel>.Success(user);
}
catch (Exception ex)
{
_logger.LogError(ex, "创建用户失败: {Username}", user.Username);
return Result<UserModel>.Fail("USER_CREATE_FAILED", "创建用户失败");
}
}
/// <inheritdoc />
public async Task<Result<UserModel>> UpdateAsync(UserModel user)
{
try
{
const string sql = @"
UPDATE sec_users SET
DisplayName = @DisplayName, Email = @Email, PhoneNumber = @PhoneNumber,
Status = @Status, UpdatedAtUtc = @UpdatedAtUtc, UpdatedBy = @UpdatedBy, Remark = @Remark
WHERE Id = @Id";
using var connection = new SqlConnection(_connectionString);
await connection.OpenAsync();
using var command = new SqlCommand(sql, connection);
command.Parameters.AddWithValue("@Id", user.Id);
command.Parameters.AddWithValue("@DisplayName", user.DisplayName);
command.Parameters.AddWithValue("@Email", user.Email);
command.Parameters.AddWithValue("@PhoneNumber", (object?)user.PhoneNumber ?? DBNull.Value);
command.Parameters.AddWithValue("@Status", (int)user.Status);
command.Parameters.AddWithValue("@UpdatedAtUtc", user.UpdatedAtUtc);
command.Parameters.AddWithValue("@UpdatedBy", user.UpdatedBy);
command.Parameters.AddWithValue("@Remark", (object?)user.Remark ?? DBNull.Value);
var rowsAffected = await command.ExecuteNonQueryAsync();
if (rowsAffected == 0)
{
return Result<UserModel>.Fail("USER_NOT_FOUND", "用户不存在");
}
_logger.LogInformation("用户更新成功: {UserId} - {Username}", user.Id, user.Username);
return Result<UserModel>.Success(user);
}
catch (Exception ex)
{
_logger.LogError(ex, "更新用户失败: {UserId}", user.Id);
return Result<UserModel>.Fail("USER_UPDATE_FAILED", "更新用户失败");
}
}
/// <inheritdoc />
public async Task<Result> DeleteAsync(Guid id)
{
try
{
const string sql = "UPDATE sec_users SET Status = @DeletedStatus, UpdatedAtUtc = @UpdatedAtUtc WHERE Id = @Id";
using var connection = new SqlConnection(_connectionString);
await connection.OpenAsync();
using var command = new SqlCommand(sql, connection);
command.Parameters.AddWithValue("@Id", id);
command.Parameters.AddWithValue("@DeletedStatus", (int)UserStatus.Deleted);
command.Parameters.AddWithValue("@UpdatedAtUtc", DateTime.UtcNow);
await command.ExecuteNonQueryAsync();
_logger.LogInformation("用户删除成功: {UserId}", id);
return Result.Success("用户删除成功");
}
catch (Exception ex)
{
_logger.LogError(ex, "删除用户失败: {UserId}", id);
return Result.Fail("USER_DELETE_FAILED", "删除用户失败");
}
}
/// <inheritdoc />
public async Task<Result<bool>> UsernameExistsAsync(string username)
{
try
{
const string sql = "SELECT COUNT(1) FROM sec_users WHERE Username = @Username AND Status != @DeletedStatus";
using var connection = new SqlConnection(_connectionString);
await connection.OpenAsync();
using var command = new SqlCommand(sql, connection);
command.Parameters.AddWithValue("@Username", username);
command.Parameters.AddWithValue("@DeletedStatus", (int)UserStatus.Deleted);
var exists = (int)await command.ExecuteScalarAsync() > 0;
return Result<bool>.Success(exists);
}
catch (Exception ex)
{
_logger.LogError(ex, "检查用户名是否存在失败: {Username}", username);
return Result<bool>.Fail("USER_CHECK_USERNAME_FAILED", "检查用户名失败");
}
}
/// <inheritdoc />
public async Task<Result<(IReadOnlyList<UserModel> users, int totalCount)>> GetPagedListAsync(
int pageIndex,
int pageSize,
string? keyword = null,
UserStatus? status = null)
{
try
{
var offset = pageIndex * pageSize;
var whereConditions = new List<string> { "Status != @DeletedStatus" };
var parameters = new List<SqlParameter>
{
new("@DeletedStatus", (int)UserStatus.Deleted),
new("@Offset", offset),
new("@PageSize", pageSize)
};
if (!string.IsNullOrWhiteSpace(keyword))
{
whereConditions.Add("(Username LIKE @Keyword OR DisplayName LIKE @Keyword OR Email LIKE @Keyword)");
parameters.Add(new SqlParameter("@Keyword", $"%{keyword}%"));
}
if (status.HasValue)
{
whereConditions.Add("Status = @Status");
parameters.Add(new SqlParameter("@Status", (int)status.Value));
}
var whereClause = string.Join(" AND ", whereConditions);
// 查询总数
var countSql = $"SELECT COUNT(1) FROM sec_users WHERE {whereClause}";
// 查询数据
var dataSql = $@"
SELECT Id, Username, DisplayName, Email, PhoneNumber, PasswordHash, PasswordSalt,
Status, LastLoginAtUtc, LastLoginIp, LoginFailedCount, LockedUntilUtc, IsFirstLogin,
CreatedAtUtc, UpdatedAtUtc, CreatedBy, UpdatedBy, Remark
FROM sec_users
WHERE {whereClause}
ORDER BY CreatedAtUtc DESC
OFFSET @Offset ROWS FETCH NEXT @PageSize ROWS ONLY";
using var connection = new SqlConnection(_connectionString);
await connection.OpenAsync();
// 获取总数
using var countCommand = new SqlCommand(countSql, connection);
countCommand.Parameters.AddRange(parameters.ToArray());
var totalCount = (int)await countCommand.ExecuteScalarAsync();
// 获取数据
var users = new List<UserModel>();
using var dataCommand = new SqlCommand(dataSql, connection);
dataCommand.Parameters.AddRange(parameters.ToArray());
using var reader = await dataCommand.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
users.Add(MapReaderToUser(reader));
}
return Result<(IReadOnlyList<UserModel>, int)>.Success((users.AsReadOnly(), totalCount));
}
catch (Exception ex)
{
_logger.LogError(ex, "获取用户分页列表失败");
return Result<(IReadOnlyList<UserModel>, int)>.Fail("USER_GET_PAGED_LIST_FAILED", "获取用户列表失败");
}
}
/// <inheritdoc />
public async Task<Result> UpdateLastLoginAsync(Guid userId, string ipAddress)
{
try
{
const string sql = @"
UPDATE sec_users SET
LastLoginAtUtc = @LastLoginAtUtc,
LastLoginIp = @IpAddress,
LoginFailedCount = 0,
IsFirstLogin = 0,
UpdatedAtUtc = @UpdatedAtUtc
WHERE Id = @Id";
using var connection = new SqlConnection(_connectionString);
await connection.OpenAsync();
using var command = new SqlCommand(sql, connection);
command.Parameters.AddWithValue("@Id", userId);
command.Parameters.AddWithValue("@LastLoginAtUtc", DateTime.UtcNow);
command.Parameters.AddWithValue("@IpAddress", ipAddress);
command.Parameters.AddWithValue("@UpdatedAtUtc", DateTime.UtcNow);
await command.ExecuteNonQueryAsync();
_logger.LogInformation("更新用户最后登录信息成功: {UserId}", userId);
return Result.Success("更新登录信息成功");
}
catch (Exception ex)
{
_logger.LogError(ex, "更新用户最后登录信息失败: {UserId}", userId);
return Result.Fail("USER_UPDATE_LOGIN_FAILED", "更新登录信息失败");
}
}
/// <summary>
/// 将DataReader映射到UserModel。
/// </summary>
private static UserModel MapReaderToUser(SqlDataReader reader)
{
return new UserModel
{
Id = reader.GetGuid("Id"),
Username = reader.GetString("Username"),
DisplayName = reader.GetString("DisplayName"),
Email = reader.GetString("Email"),
PhoneNumber = reader.IsDBNull("PhoneNumber") ? null : reader.GetString("PhoneNumber"),
PasswordHash = reader.GetString("PasswordHash"),
PasswordSalt = reader.GetString("PasswordSalt"),
Status = (UserStatus)reader.GetInt32("Status"),
LastLoginAtUtc = reader.IsDBNull("LastLoginAtUtc") ? null : reader.GetDateTime("LastLoginAtUtc"),
LastLoginIp = reader.IsDBNull("LastLoginIp") ? null : reader.GetString("LastLoginIp"),
LoginFailedCount = reader.GetInt32("LoginFailedCount"),
LockedUntilUtc = reader.IsDBNull("LockedUntilUtc") ? null : reader.GetDateTime("LockedUntilUtc"),
IsFirstLogin = reader.GetBoolean("IsFirstLogin"),
CreatedAtUtc = reader.GetDateTime("CreatedAtUtc"),
UpdatedAtUtc = reader.GetDateTime("UpdatedAtUtc"),
CreatedBy = reader.GetString("CreatedBy"),
UpdatedBy = reader.GetString("UpdatedBy"),
Remark = reader.IsDBNull("Remark") ? null : reader.GetString("Remark")
};
}
}