368 lines
15 KiB
C#
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")
|
|
};
|
|
}
|
|
}
|