using System.Data.Common;
using System.Security.Cryptography;
using System.Text;
namespace SimpleAdmin.Application;
///
/// 所有修改同一任务的入口共用 MySQL 命名锁,适用于同一主库上的多个后端实例。
/// 锁使用独立、非池化连接,不在第三方请求期间持有业务数据库事务。
///
internal static class OnboardingConcurrency
{
public static Task RunTask(SqlSugarScope db, long taskId, Func action,
bool transactional = false, bool protectIdentity = false)
=> RunTask(db, taskId, async () => { await action(); return true; },
transactional, protectIdentity);
public static Task RunTask(SqlSugarScope db, long taskId, Func> action,
bool transactional = false, bool protectIdentity = false)
=> Run(db, $"task:{taskId}", () => protectIdentity
? RunPerson(db, taskId, () => InTransaction(db, action, transactional))
: InTransaction(db, action, transactional));
public static Task RunEmployee(SqlSugarScope db, long employeeId, Func action)
=> Run(db, $"employee:{employeeId}", async () => { await action(); return true; });
public static Task RunPublic(SqlSugarScope db, string token, Func action,
bool transactional = false, bool protectIdentity = false)
=> RunPublic(db, token, async () => { await action(); return true; },
transactional, protectIdentity);
public static async Task RunPublic(SqlSugarScope db, string token, Func> action,
bool transactional = false, bool protectIdentity = false)
{
if (string.IsNullOrWhiteSpace(token))
throw Oops.Bah("入职链接 Token 不能为空");
var hash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(token)));
var link = await db.Queryable().FirstAsync(item => item.TokenHash == hash)
?? throw Oops.Bah("链接不存在");
// 此处只定位锁;被保护的方法必须在锁内重新检查链接、任务状态和归属。
return await RunTask(db, link.TaskId, action, transactional, protectIdentity);
}
public static Task RunContract(SqlSugarScope db, long contractId, Func action,
bool transactional = false)
=> RunContract(db, contractId, async () => { await action(); return true; }, transactional);
public static async Task RunContract(SqlSugarScope db, long contractId, Func> action,
bool transactional = false)
{
var contract = await db.Queryable().InSingleAsync(contractId)
?? throw Oops.Bah("任务合同不存在");
return await RunTask(db, contract.TaskId, action, transactional);
}
public static async Task RunMaterial(SqlSugarScope db, long materialId, Func action)
{
var material = await db.Queryable().InSingleAsync(materialId)
?? throw Oops.Bah("任务资料不存在");
await RunTask(db, material.TaskId, action, transactional: true);
}
private static async Task RunPerson(SqlSugarScope db, long taskId, Func> action)
{
var info = await db.Queryable().FirstAsync(item => item.TaskId == taskId)
?? throw Oops.Bah("请先完整填写基本信息");
// 统一顺序:任务 -> 身份证 -> 手机;跨任务提交/建档的查重与写入也串行。
return await Run(db, "identity:" + info.IdCardNumber?.Trim().ToUpperInvariant(),
() => Run(db, "phone:" + info.Phone?.Trim(), action));
}
private static async Task InTransaction(SqlSugarScope db, Func> action, bool enabled)
{
if (!enabled || db.Ado.Transaction != null)
return await action();
await db.Ado.BeginTranAsync();
try
{
var result = await action();
await db.Ado.CommitTranAsync();
return result;
}
catch
{
await db.Ado.RollbackTranAsync();
throw;
}
}
private static Task Run(SqlSugarScope db, string resource, Func> action)
{
if (db.CurrentConnectionConfig.DbType != global::SqlSugar.DbType.MySql)
throw Oops.Bah("入职并发保护需要 MySQL 数据库");
// 使用已选择的业务主库,不能在只读副本/不同主库上分别申请锁。
var database = db.Ado.Connection.Database;
var key = Convert.ToHexString(SHA256.HashData(
Encoding.UTF8.GetBytes($"onboardx:{database}:{resource}")));
return OnboardingOperationGate.Run(key, () => Acquire(db, key), action);
}
private static async Task Acquire(SqlSugarScope db, string key)
{
var connection = (DbConnection)Activator.CreateInstance(db.Ado.Connection.GetType());
var builder = new DbConnectionStringBuilder
{
ConnectionString = db.CurrentConnectionConfig.ConnectionString
};
// 确保异常关闭时会真正关闭持锁会话,不把命名锁留在连接池里。
builder["Pooling"] = false;
connection.ConnectionString = builder.ConnectionString;
try
{
await connection.OpenAsync();
await using var command = connection.CreateCommand();
command.CommandText = "SELECT GET_LOCK(@lockName, 0)";
command.CommandTimeout = 5;
AddKey(command, key);
var result = await command.ExecuteScalarAsync();
if (result == null || result == DBNull.Value || Convert.ToInt32(result) != 1)
throw Oops.Bah("当前数据正在处理,请稍后刷新重试");
return new NamedLock(connection, key);
}
catch
{
await connection.DisposeAsync();
throw;
}
}
private static void AddKey(DbCommand command, string key)
{
var parameter = command.CreateParameter();
parameter.ParameterName = "@lockName";
parameter.Value = key;
command.Parameters.Add(parameter);
}
private sealed class NamedLock : IAsyncDisposable
{
private readonly DbConnection _connection;
private readonly string _key;
public NamedLock(DbConnection connection, string key)
{
_connection = connection;
_key = key;
}
public async ValueTask DisposeAsync()
{
try
{
await using var command = _connection.CreateCommand();
command.CommandText = "SELECT RELEASE_LOCK(@lockName)";
command.CommandTimeout = 5;
AddKey(command, _key);
await command.ExecuteScalarAsync();
}
catch
{
// 非池化物理连接关闭会释放会话锁;释放错误不覆盖业务原异常。
}
finally
{
await _connection.DisposeAsync();
}
}
}
}