Initial_commit_SecMPS_v2

This commit is contained in:
2026-05-15 23:22:48 +08:00
commit 23ea4fe05f
13830 changed files with 298675 additions and 0 deletions

View File

@@ -0,0 +1,438 @@
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
using System.Text.RegularExpressions;
using VolPro.Core.Const;
using VolPro.Core.DBManager;
using VolPro.Core.EFDbContext;
using VolPro.Core.Enums;
using VolPro.Core.Utilities;
namespace VolPro.Builder.Services
{
/// <summary>
/// SQL 表结构信息
/// </summary>
public class SqlColumnSchemaInfo
{
/// <summary>字段名</summary>
public string ColumnName { get; set; } = string.Empty;
/// <summary>备注(说明)</summary>
/// <remarks>对任意 SQL 结果集,通常无法获取扩展属性/注释,此处可能为空</remarks>
public string Comment { get; set; } = string.Empty;
/// <summary>长度/精度</summary>
public int? Length { get; set; }
/// <summary>C# 类型(如 String、Int32、Decimal 等)</summary>
public string DataType { get; set; } = string.Empty;
/// <summary>数据库字段类型(如 nvarchar、int、decimal 等)</summary>
public string DbDataType { get; set; } = string.Empty;
/// <summary>小数位数decimal/numeric 等)</summary>
public int? Scale { get; set; }
/// <summary>是否可为空</summary>
public bool IsNullable { get; set; } = true;
/// <summary>列序号</summary>
public int Ordinal { get; set; }
}
public static class SqlSchemaHelper
{
public static (bool IsValid, string ErrorMessage) IsValidSql(string sql, string dbServer = "SysDbContext")
{
var dbType = DbRelativeCache.GetDbType(dbServer);
var sanitized = sql;
if (ContainsForbiddenKeywords(sanitized, dbType, out var keyword))
{
return (false, $"sql包含禁止操作{keyword}");
}
if (ContainsDbSpecificForbiddenPatterns(sanitized, dbType, out var pattern))
{
return (false, $"sql包含禁止模式{pattern}");
}
if (ContainsSysUserUserPwdReference(sanitized))
{
return (false, "禁止查询 `sys_user`.`UserPwd` 字段。");
}
if (string.IsNullOrWhiteSpace(sql))
return (true, string.Empty);
var connection = GetDbConnection(dbServer);
if (connection.State == ConnectionState.Closed)
{
connection.Open();
}
try
{
using var cmd = connection.CreateCommand();
cmd.CommandText = sql;
using var reader = cmd.ExecuteReader(CommandBehavior.SchemaOnly);
return (true, string.Empty);
}
catch (Exception ex)
{
return (false, $"sql执行未通过,请检查sql,{ex.InnerException?.Message ?? ex.Message}");
}
finally
{
if (connection.State == ConnectionState.Open)
connection.Close();
}
}
private static bool ContainsForbiddenKeywords(string statement, string dbType, out string keyword)
{
keyword = string.Empty;
if (string.IsNullOrWhiteSpace(statement)) return false;
// 全字匹配SQL 任意位置出现关键字即拦截
var forbiddenWords = new (string Word, string Label)[]
{
("delete", "DELETE"),
("drop", "DROP"),
("truncate", "TRUNCATE"),
("alter", "ALTER"),
("create", "CREATE"),
("grant", "GRANT"),
("revoke", "REVOKE"),
("call", "CALL"),
("exec", "EXEC"),
("execute", "EXECUTE"),
("commit", "COMMIT"),
("rollback", "ROLLBACK"),
("use", "USE"),
("into", "INTO")
};
foreach (var (word, label) in forbiddenWords)
{
if (Regex.IsMatch(statement, $@"\b{Regex.Escape(word)}\b", RegexOptions.IgnoreCase))
{
keyword = label;
return true;
}
}
// MySQLSELECT ... INTO OUTFILE|DUMPFILE导出文件/写出)
if (string.Equals(dbType, "MySql", StringComparison.OrdinalIgnoreCase))
{
if (Regex.IsMatch(statement, @"\binto\s+(?:out|dump)file\b", RegexOptions.IgnoreCase))
{
keyword = "INTO OUTFILE/DUMPFILE";
return true;
}
}
return false;
}
private static bool ContainsSysUserUserPwdReference(string statement)
{
if (string.IsNullOrWhiteSpace(statement)) return false;
bool hasSysUser = Regex.IsMatch(statement, @"\bsys_user\b", RegexOptions.IgnoreCase);
if (!hasSysUser) return false;
bool hasUserPwd =
Regex.IsMatch(statement, @"\bUserPwd\b", RegexOptions.IgnoreCase)
|| Regex.IsMatch(statement, @"\[`UserPwd`\]", RegexOptions.IgnoreCase)
|| Regex.IsMatch(statement, @"\[\s*UserPwd\s*\]", RegexOptions.IgnoreCase)
|| Regex.IsMatch(statement, @"""UserPwd""", RegexOptions.IgnoreCase);
return hasUserPwd;
}
private static bool ContainsDbSpecificForbiddenPatterns(string statement, string dbType, out string pattern)
{
pattern = string.Empty;
if (string.IsNullOrWhiteSpace(statement)) return false;
if (string.Equals(dbType, "MsSql", StringComparison.OrdinalIgnoreCase) ||
string.Equals(dbType, "PgSql", StringComparison.OrdinalIgnoreCase))
{
// SELECT ... INTO <table> ... (创建表语义)
// SQL Server允许临时表 INTO #temp拒绝其它 into 创建表
// PostgreSQL允许 INTO TEMP|TEMPORARY拒绝其它 into 创建表
if (string.Equals(dbType, "MsSql", StringComparison.OrdinalIgnoreCase))
{
if (Regex.IsMatch(statement,
@"\bselect\b[\s\S]*?\binto\b\s+(?!#)[^\s]+\s+\bfrom\b",
RegexOptions.IgnoreCase))
{
pattern = "SELECT ... INTO table (non-temp)";
return true;
}
}
else
{
if (Regex.IsMatch(statement,
@"\bselect\b[\s\S]*?\binto\b\s+(?!temp(?:orary)?\b)[^\s]+\s+\bfrom\b",
RegexOptions.IgnoreCase))
{
pattern = "SELECT ... INTO table";
return true;
}
}
}
if (string.Equals(dbType, "PgSql", StringComparison.OrdinalIgnoreCase))
{
// COPY FROM/COPY ... 会修改数据或导入,禁止
if (Regex.IsMatch(statement, @"\bcopy\b", RegexOptions.IgnoreCase))
{
pattern = "COPY";
return true;
}
}
if (string.Equals(dbType, "Oracle", StringComparison.OrdinalIgnoreCase))
{
}
return false;
}
private static IDbConnection GetDbConnection(string dbServer = "SysDbContext")
{
var type = DbRelativeCache.GetDbContextType(dbServer);
object dbObj = HttpContext.Current.RequestServices.GetService(type);
BaseDbContext context = (BaseDbContext)dbObj;
var connection = context.SqlSugarClient.Ado.Connection;
return connection;
}
public static List<SqlColumnSchemaInfo> GetSchemaFromSql(string sql, string dbServer = "SysDbContext")
{
List<SqlColumnSchemaInfo> list = new List<SqlColumnSchemaInfo>();
var connection = GetDbConnection(dbServer);
if (connection.State == ConnectionState.Closed)
{
connection.Open();
}
try
{
using var cmd = connection.CreateCommand();
cmd.CommandText = sql;
using var reader = cmd.ExecuteReader(CommandBehavior.SchemaOnly);
if (reader == null) return list;
var schemaTable = reader.GetSchemaTable();
if (schemaTable == null || schemaTable.Rows.Count == 0)
return list;
int ordinal = 0;
foreach (DataRow row in schemaTable.Rows)
{
var info = RowToColumnInfo(row, reader, ordinal++);
if (info != null)
list.Add(info);
}
return list;
}
catch (Exception ex)
{
throw new Exception(ex.Message, ex.InnerException);
}
finally
{
if (connection.State == ConnectionState.Open)
connection.Close();
}
}
private static SqlColumnSchemaInfo RowToColumnInfo(DataRow row, IDataReader reader, int ordinal)
{
var colName = GetRowValue<string>(row, "ColumnName")
?? GetRowValue<string>(row, "BaseColumnName");
var dataType = GetRowValue<Type>(row, "DataType");
var netTypeName = dataType?.Name ?? GetRowValue<string>(row, "DataTypeName") ?? string.Empty;
var dbDataType = string.Empty;
if (reader != null && ordinal >= 0 && ordinal < reader.FieldCount)
{
try { dbDataType = reader.GetDataTypeName(ordinal) ?? string.Empty; }
catch { }
}
if (string.IsNullOrEmpty(dbDataType))
dbDataType = GetRowValue<string>(row, "DataTypeName") ?? string.Empty;
var length = GetRowValue<int?>(row, "ColumnSize");
var precision = GetRowValue<int?>(row, "NumericPrecision");
var scale = GetRowValue<int?>(row, "NumericScale");
if (length.HasValue && length.Value <= 0)
length = null;
if (dataType != null && (netTypeName.Contains("Decimal", StringComparison.OrdinalIgnoreCase) || netTypeName.Contains("Numeric", StringComparison.OrdinalIgnoreCase)))
{
length = precision;
}
var allowNull = true;
if (row.Table.Columns.Contains("AllowDBNull"))
{
var allowNullVal = row["AllowDBNull"];
if (allowNullVal != null && allowNullVal != DBNull.Value)
allowNull = Convert.ToBoolean(allowNullVal);
}
var csharpTypeName = DbTypeToCSharpType(dbDataType, dataType, netTypeName);
return new SqlColumnSchemaInfo
{
ColumnName = colName ?? string.Empty,
Comment = string.Empty,
Length = length,
Scale = scale,
DataType = csharpTypeName,
DbDataType = dbDataType,
IsNullable = allowNull,
Ordinal = ordinal
};
}
/// <summary>
/// 将数据库字段类型转换为 C# 类型名称(如 int、string、DateTime、decimal 等)
/// 参照 Sys_TableInfoService 的 GetSqlServerStructure/GetMySqlStructure/GetPgSqlStructure 映射
/// </summary>
private static string DbTypeToCSharpType(string dbDataType, Type netType, string netTypeName)
{
var dbLower = (dbDataType ?? string.Empty).Trim().ToLowerInvariant();
if (!string.IsNullOrEmpty(dbLower))
{
// SQL Server、MySQL、PostgreSQL 等统一映射
switch (dbLower)
{
case "uniqueidentifier":
case "uuid":
return "guid";
case "bit":
case "bool":
case "boolean":
return "bool";
case "tinyint":
return "byte";
case "smallint":
case "int2":
return "short";
case "int":
case "integer":
case "mediumint":
case "int4":
case "year":
return "int";
case "bigint":
case "int8":
return "long";
case "real":
case "float4":
return "float";
case "float":
case "double":
case "float8":
return "double";
case "decimal":
case "numeric":
case "money":
case "smallmoney":
return "decimal";
case "time":
return "TimeSpan";
case "date":
case "datetime":
case "datetime2":
case "smalldatetime":
case "timestamp":
return "DateTime";
case "char":
case "varchar":
case "nvarchar":
case "nchar":
case "text":
case "ntext":
case "xml":
case "varbinary":
case "image":
case "tinytext":
case "mediumtext":
case "longtext":
case "tinyblob":
case "blob":
case "mediumblob":
case "longblob":
case "bytea":
return "string";
}
}
// 无 DbDataType 或未匹配时,按 .NET Type 映射
if (netType != null)
{
if (netType == typeof(bool) || netType == typeof(bool?)) return "bool";
if (netType == typeof(byte) || netType == typeof(byte?)) return "byte";
if (netType == typeof(short) || netType == typeof(short?)) return "short";
if (netType == typeof(int) || netType == typeof(int?)) return "int";
if (netType == typeof(long) || netType == typeof(long?)) return "long";
if (netType == typeof(float) || netType == typeof(float?)) return "float";
if (netType == typeof(double) || netType == typeof(double?)) return "double";
if (netType == typeof(decimal) || netType == typeof(decimal?)) return "decimal";
if (netType == typeof(DateTime) || netType == typeof(DateTime?)) return "DateTime";
if (netType == typeof(TimeSpan) || netType == typeof(TimeSpan?)) return "TimeSpan";
if (netType == typeof(Guid) || netType == typeof(Guid?)) return "guid";
if (netType == typeof(string)) return "string";
if (netType == typeof(byte[])) return "byte[]";
}
if (!string.IsNullOrEmpty(netTypeName))
{
if (netTypeName.IndexOf("Boolean", StringComparison.OrdinalIgnoreCase) >= 0) return "bool";
if (netTypeName.IndexOf("Byte", StringComparison.OrdinalIgnoreCase) >= 0) return "byte";
if (netTypeName.IndexOf("Int16", StringComparison.OrdinalIgnoreCase) >= 0) return "short";
if (netTypeName.IndexOf("Int32", StringComparison.OrdinalIgnoreCase) >= 0) return "int";
if (netTypeName.IndexOf("Int64", StringComparison.OrdinalIgnoreCase) >= 0) return "long";
if (netTypeName.IndexOf("Single", StringComparison.OrdinalIgnoreCase) >= 0) return "float";
if (netTypeName.IndexOf("Double", StringComparison.OrdinalIgnoreCase) >= 0) return "double";
if (netTypeName.IndexOf("Decimal", StringComparison.OrdinalIgnoreCase) >= 0 || netTypeName.IndexOf("Numeric", StringComparison.OrdinalIgnoreCase) >= 0) return "decimal";
if (netTypeName.IndexOf("DateTime", StringComparison.OrdinalIgnoreCase) >= 0) return "DateTime";
if (netTypeName.IndexOf("TimeSpan", StringComparison.OrdinalIgnoreCase) >= 0) return "TimeSpan";
if (netTypeName.IndexOf("Guid", StringComparison.OrdinalIgnoreCase) >= 0) return "guid";
if (netTypeName.IndexOf("String", StringComparison.OrdinalIgnoreCase) >= 0) return "string";
}
return "string";
}
private static T GetRowValue<T>(DataRow row, string columnName)
{
if (!row.Table.Columns.Contains(columnName))
return default;
var val = row[columnName];
if (val == null || val == DBNull.Value)
return default;
var targetType = typeof(T);
if (targetType == typeof(string))
return (T)(object)(val?.ToString() ?? string.Empty);
if (targetType == typeof(Type))
return val is Type t ? (T)(object)t : default;
if (targetType == typeof(int?) || targetType == typeof(int))
{
try { return (T)(object)Convert.ToInt32(val); }
catch { return default; }
}
try
{
var underlyingType = Nullable.GetUnderlyingType(targetType) ?? targetType;
return (T)Convert.ChangeType(val, underlyingType);
}
catch
{
return default;
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,22 @@
using VolPro.Builder.IRepositories;
using VolPro.Builder.IServices;
using VolPro.Core.BaseProvider;
using VolPro.Core.Extensions.AutofacManager;
using VolPro.Entity.DomainModels;
namespace VolPro.Builder.Services
{
public partial class Sys_TableInfoService : ServiceBase<Sys_TableInfo, ISys_TableInfoRepository>, ISys_TableInfoService, IDependency
{
public Sys_TableInfoService(ISys_TableInfoRepository repository)
: base(repository)
{
Init(repository);
}
public static ISys_TableInfoService Instance
{
get { return AutofacContainerModule.GetService<ISys_TableInfoService>(); }
}
}
}

View File

@@ -0,0 +1,12 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace VolPro.Builder.Services;
public class CreateTableRequest
{
[Required]
public string TableName { get; set; } = string.Empty;
public List<TableColumnDto> Columns { get; set; } = new();
}

View File

@@ -0,0 +1,26 @@
namespace VolPro.Builder.Services;
public class TableColumnDto
{
public string ColumnName { get; set; } = string.Empty;
public string DataType { get; set; } = "nvarchar";
public int? Length { get; set; }
// decimal/numeric 的小数位scale
public int? Scale { get; set; }
public bool IsNullable { get; set; } = true;
public bool IsPrimaryKey { get; set; } = false;
public bool IsIdentity { get; set; } = false;
public string Comment { get; set; }
public int Order { get; set; }
public string DefaultValue { get; set; } = string.Empty;
/// <summary>
/// 数据库字段true=来自数据库false=新增)
/// </summary>
public bool IsDbField { get; set; }
/// <summary>
/// 原始字段名(用于区分重命名与删除:重命名时 OriginalColumnName 为旧名)
/// </summary>
public string OriginalColumnName { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,9 @@
using System.Collections.Generic;
namespace VolPro.Builder.Services;
public class TableInfoDto
{
public string TableName { get; set; } = string.Empty;
public List<TableColumnDto> Columns { get; set; } = new();
}

View File

@@ -0,0 +1,12 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace VolPro.Builder.Services;
public class UpdateTableRequest
{
[Required]
public string TableName { get; set; } = string.Empty;
public List<TableColumnDto> Columns { get; set; } = new();
}

View File

@@ -0,0 +1,294 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Threading.Tasks;
using VolPro.Builder.IServices;
using VolPro.Core.EFDbContext;
namespace VolPro.Builder.Services;
/// <summary>
/// MySQL 数据库表结构操作实现。
/// </summary>
public class MySqlTableProvider : ITableDatabaseProvider
{
private readonly BaseDbContext _context;
public MySqlTableProvider(BaseDbContext context)
{
_context = context;
}
public async Task<bool> TableExistsAsync(string tableName)
{
const string mysqlSql = @"SELECT COUNT(*) as Value
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = @tableName;";
var res = await _context.SqlSugarClient.Ado.GetScalarAsync(mysqlSql, new { tableName });
return Convert.ToInt32(res) > 0;
}
public async Task CreateTableAsync(CreateTableRequest request)
{
await CreateTableMySqlAsync(request);
}
public async Task<List<string>> GetAllTablesAsync()
{
const string mysqlSql = @"
SELECT TABLE_NAME as Value
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_SCHEMA = DATABASE()
ORDER BY TABLE_NAME";
var rows = await _context.SqlSugarClient.Ado.SqlQueryAsync<string>(mysqlSql);
return rows ?? new List<string>();
}
public async Task<TableInfoDto> GetTableInfoAsync(string tableName)
{
return await GetTableInfoMySqlAsync(tableName);
}
public async Task UpdateTableAsync(UpdateTableRequest request)
{
await UpdateTableMySqlAsync(request);
}
public async Task DeleteTableAsync(string tableName)
{
var dropMySql = $"DROP TABLE `{tableName.Replace("`", "``")}`;";
await _context.SqlSugarClient.Ado.ExecuteCommandAsync(dropMySql);
}
private static string NormalizeMySqlDefault(string raw)
{
if (string.IsNullOrWhiteSpace(raw)) return null;
var s = raw.Trim();
// MySQL BIT 类型默认值格式为 b'1' 或 b'0',统一返回 1 或 0
if (s.StartsWith("b'", StringComparison.OrdinalIgnoreCase) && s.EndsWith("'") && s.Length >= 4)
{
var inner = s[2..^1];
return inner == "1" ? "1" : inner == "0" ? "0" : inner;
}
if (s.Length >= 2 && s.StartsWith("'") && s.EndsWith("'"))
return s[1..^1].Replace("''", "'");
return s;
}
private static string FormatDefaultForMySql(string value)
{
if (string.IsNullOrWhiteSpace(value)) return string.Empty;
var v = value.Trim();
var vLower = v.ToLower();
if (vLower is "current_timestamp" or "now()" or "now(6)")
return $" DEFAULT {v}";
if (long.TryParse(v, out _) || decimal.TryParse(v, out _))
return $" DEFAULT {v}";
if (vLower == "true" || vLower == "1") return " DEFAULT 1";
if (vLower == "false" || vLower == "0") return " DEFAULT 0";
return $" DEFAULT '{v.Replace("'", "''")}'";
}
// MySQL 类型映射:根据通用 DataType/Length/Scale 生成 MySQL 列类型
private static string BuildMySqlColumnType(TableColumnDto column)
{
var dt = (column.DataType ?? string.Empty).Trim();
var dtLower = dt.ToLower();
return dtLower switch
{
"nvarchar" or "varchar" or "character varying" => $"VARCHAR({(column.Length is > 0 and <= 65535 ? column.Length : 255)})",
"char" or "nchar" => $"CHAR({(column.Length is > 0 and <= 255 ? column.Length : 1)})",
"nvarchar(max)" or "varchar(max)" => "LONGTEXT",
"int" or "integer" => "INT",
"bigint" => "BIGINT",
"tinyint" => "TINYINT",
"bit" => "BIT",
"decimal" or "numeric" =>
$"DECIMAL({(column.Length ?? 18)},{(column.Scale ?? 0)})",
"double" or "float" => "DOUBLE",
"datetime" => "DATETIME",
"date" => "DATE",
"text" or "ntext" => "TEXT",
"longtext" => "LONGTEXT",
"uniqueidentifier" or "uuid" => "CHAR(36)",
_ => dt // 其他:假设是合法的 MySQL 类型
};
}
// MySQL 专用:建表
private async Task CreateTableMySqlAsync(CreateTableRequest request)
{
var columnDefs = new List<string>();
foreach (var column in request.Columns.OrderBy(c => c.Order))
{
var typeSql = BuildMySqlColumnType(column);
var isIdentity = column.IsIdentity;
var nullSql = isIdentity ? "NOT NULL" : (column.IsNullable ? "NULL" : "NOT NULL");
var identitySql = isIdentity ? " AUTO_INCREMENT" : string.Empty;
var commentSql = string.IsNullOrWhiteSpace(column.Comment)
? string.Empty
: $" COMMENT '{column.Comment.Replace("'", "''")}'";
var defaultSql = FormatDefaultForMySql(column.DefaultValue);
columnDefs.Add($"`{column.ColumnName}` {typeSql}{identitySql} {nullSql}{defaultSql}{commentSql}");
}
var pkCols = request.Columns.Where(c => c.IsPrimaryKey).OrderBy(c => c.Order).Select(c => $"`{c.ColumnName}`");
var pkSql = pkCols.Any() ? $", PRIMARY KEY ({string.Join(", ", pkCols)})" : string.Empty;
var createSql = $"CREATE TABLE `{request.TableName}` (\n {string.Join(",\n ", columnDefs)}{pkSql}\n) ENGINE=InnoDB DEFAULT CHARSET=utf8;";
await _context.SqlSugarClient.Ado.ExecuteCommandAsync(createSql);
}
// MySQL 专用:获取表结构
private async Task<TableInfoDto> GetTableInfoMySqlAsync(string tableName)
{
const string sql = @"
SELECT
c.COLUMN_NAME,
c.DATA_TYPE,
c.CHARACTER_MAXIMUM_LENGTH,
c.IS_NULLABLE,
c.ORDINAL_POSITION,
c.NUMERIC_SCALE,
c.NUMERIC_PRECISION,
CASE WHEN k.CONSTRAINT_NAME = 'PRIMARY' THEN 1 ELSE 0 END AS IS_PRIMARY_KEY,
CASE WHEN c.EXTRA LIKE '%auto_increment%' THEN 1 ELSE 0 END AS IS_IDENTITY,
c.COLUMN_COMMENT,
c.COLUMN_DEFAULT
FROM INFORMATION_SCHEMA.COLUMNS c
LEFT JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE k
ON k.TABLE_SCHEMA = c.TABLE_SCHEMA
AND k.TABLE_NAME = c.TABLE_NAME
AND k.COLUMN_NAME = c.COLUMN_NAME
WHERE c.TABLE_SCHEMA = DATABASE() AND c.TABLE_NAME = @tableName
ORDER BY c.ORDINAL_POSITION;";
var dt = await _context.SqlSugarClient.Ado.GetDataTableAsync(sql, new { tableName });
var columns = new List<TableColumnDto>();
foreach (DataRow row in dt.Rows)
{
var dataType = row[1]?.ToString() ?? "";
var isDecimal = dataType.Equals("decimal", StringComparison.OrdinalIgnoreCase);
int? charLen = null;
if (!dataType.Equals("text", StringComparison.OrdinalIgnoreCase) &&
!dataType.Equals("longtext", StringComparison.OrdinalIgnoreCase) &&
row[2] != DBNull.Value && row[2] != null)
{
var raw = Convert.ToInt64(row[2]);
if (raw > 0 && raw <= int.MaxValue) charLen = (int)raw;
}
var numericScale = row[5] == DBNull.Value || row[5] == null ? (int?)null : Convert.ToInt32(row[5]);
var numericPrecision = row[6] == DBNull.Value || row[6] == null ? (int?)null : Convert.ToInt32(row[6]);
var colDefault = row[10] == DBNull.Value || row[10] == null ? null : NormalizeMySqlDefault(row[10]?.ToString());
columns.Add(new TableColumnDto
{
ColumnName = row[0]?.ToString() ?? "",
DataType = dataType,
Length = isDecimal ? numericPrecision : charLen,
Scale = isDecimal ? numericScale : null,
IsNullable = string.Equals(row[3]?.ToString(), "YES", StringComparison.OrdinalIgnoreCase),
Order = Convert.ToInt32(row[4]),
IsPrimaryKey = row[7] != DBNull.Value && row[7] != null && Convert.ToInt32(row[7]) == 1,
IsIdentity = row[8] != DBNull.Value && row[8] != null && Convert.ToInt32(row[8]) == 1,
Comment = row[9]?.ToString(),
DefaultValue = colDefault ?? string.Empty
});
}
return new TableInfoDto { TableName = tableName, Columns = columns };
}
// MySQL 专用:更新表(仅支持新增列、修改类型/长度/小数位/可空、更新主键)
private async Task UpdateTableMySqlAsync(UpdateTableRequest request)
{
var existing = await GetTableInfoMySqlAsync(request.TableName) ?? throw new InvalidOperationException("Table not found.");
var existingByName = existing.Columns.ToDictionary(c => c.ColumnName, c => c, StringComparer.Ordinal);
var newNames = request.Columns.Select(c => c.ColumnName).ToHashSet(StringComparer.Ordinal);
// 1) 识别并执行重命名字段(区分大小写)
var renames = request.Columns
.Where(c => c.IsDbField && !string.IsNullOrWhiteSpace(c.OriginalColumnName) &&
c.OriginalColumnName != c.ColumnName &&
existingByName.Keys.Any(k => string.Equals(k, c.OriginalColumnName, StringComparison.OrdinalIgnoreCase)))
.ToList();
foreach (var r in renames)
{
var actualKey = existingByName.Keys.FirstOrDefault(k => string.Equals(k, r.OriginalColumnName, StringComparison.OrdinalIgnoreCase));
if (actualKey == null) continue;
var oldCol = existingByName[actualKey];
var typeSql = BuildMySqlColumnType(oldCol);
var isIdentity = oldCol.IsIdentity;
var nullSql = isIdentity ? "NOT NULL" : (oldCol.IsNullable ? "NULL" : "NOT NULL");
var identitySql = isIdentity ? " AUTO_INCREMENT" : string.Empty;
var commentSql = string.IsNullOrWhiteSpace(oldCol.Comment) ? string.Empty : $" COMMENT '{oldCol.Comment.Replace("'", "''")}'";
var defaultSql = FormatDefaultForMySql(oldCol.DefaultValue);
var renameSql = $"ALTER TABLE `{request.TableName}` CHANGE COLUMN `{actualKey.Replace("`", "``")}` `{r.ColumnName.Replace("`", "``")}` {typeSql}{identitySql} {nullSql}{defaultSql}{commentSql};";
await _context.SqlSugarClient.Ado.ExecuteCommandAsync(renameSql);
existingByName.Remove(actualKey);
existingByName[r.ColumnName] = new TableColumnDto { ColumnName = r.ColumnName, DataType = oldCol.DataType, Length = oldCol.Length, Scale = oldCol.Scale, IsNullable = oldCol.IsNullable, IsPrimaryKey = oldCol.IsPrimaryKey, IsIdentity = oldCol.IsIdentity, Comment = oldCol.Comment, DefaultValue = oldCol.DefaultValue ?? "", Order = oldCol.Order };
}
var existingNames = existingByName.Keys.ToHashSet(StringComparer.Ordinal);
var columnsToDrop = existingNames.Except(newNames, StringComparer.Ordinal).ToList();
foreach (var column in request.Columns.Where(c => !existingByName.ContainsKey(c.ColumnName)).OrderBy(c => c.Order))
{
var typeSql = BuildMySqlColumnType(column);
var isIdentity = column.IsIdentity;
var nullSql = isIdentity ? "NOT NULL" : (column.IsNullable ? "NULL" : "NOT NULL");
var identitySql = isIdentity ? " AUTO_INCREMENT" : string.Empty;
var commentSql = string.IsNullOrWhiteSpace(column.Comment) ? string.Empty : $" COMMENT '{column.Comment.Replace("'", "''")}'";
var defaultSql = FormatDefaultForMySql(column.DefaultValue);
var addColSql = $"ALTER TABLE `{request.TableName}` ADD COLUMN `{column.ColumnName}` {typeSql}{identitySql} {nullSql}{defaultSql}{commentSql};";
await _context.SqlSugarClient.Ado.ExecuteCommandAsync(addColSql);
}
foreach (var column in request.Columns.Where(c => existingByName.ContainsKey(c.ColumnName)))
{
var old = existingByName[column.ColumnName];
var typeSql = BuildMySqlColumnType(column);
var oldTypeSql = BuildMySqlColumnType(old);
var typeChanged = !string.Equals(oldTypeSql, typeSql, StringComparison.OrdinalIgnoreCase);
var nullableChanged = old.IsNullable != column.IsNullable;
var defaultChanged = (old.DefaultValue ?? "").Trim() != (column.DefaultValue ?? "").Trim();
var identityChanged = old.IsIdentity != column.IsIdentity;
var commentChanged = (old.Comment ?? "").Trim() != (column.Comment ?? "").Trim();
if (!typeChanged && !nullableChanged && !defaultChanged && !identityChanged && !commentChanged)
continue;
var isIdentity = column.IsIdentity;
var nullSql = isIdentity ? "NOT NULL" : (column.IsNullable ? "NULL" : "NOT NULL");
var identitySql = isIdentity ? " AUTO_INCREMENT" : string.Empty;
var commentSql = string.IsNullOrWhiteSpace(column.Comment) ? string.Empty : $" COMMENT '{column.Comment.Replace("'", "''")}'";
var defaultSql = FormatDefaultForMySql(column.DefaultValue);
var modifySql = $"ALTER TABLE `{request.TableName}` MODIFY COLUMN `{column.ColumnName}` {typeSql}{identitySql} {nullSql}{defaultSql}{commentSql};";
await _context.SqlSugarClient.Ado.ExecuteCommandAsync(modifySql);
}
foreach (var colName in columnsToDrop)
{
await _context.SqlSugarClient.Ado.ExecuteCommandAsync($"ALTER TABLE `{request.TableName}` DROP COLUMN `{colName}`;");
}
var newPkCols = request.Columns.Where(c => c.IsPrimaryKey).OrderBy(c => c.Order).Select(c => c.ColumnName).ToList();
var existingPkCols = existingByName.Values.Where(c => c.IsPrimaryKey).OrderBy(c => c.Order).Select(c => c.ColumnName).ToList();
var pkChanged = !newPkCols.SequenceEqual(existingPkCols);
if (pkChanged)
{
try { await _context.SqlSugarClient.Ado.ExecuteCommandAsync($"ALTER TABLE `{request.TableName}` DROP PRIMARY KEY;"); } catch { }
if (newPkCols.Any())
{
var pkColsSql = string.Join(", ", newPkCols.Select(c => $"`{c}`"));
await _context.SqlSugarClient.Ado.ExecuteCommandAsync($"ALTER TABLE `{request.TableName}` ADD PRIMARY KEY ({pkColsSql});");
}
}
}
}

View File

@@ -0,0 +1,369 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Threading.Tasks;
using Npgsql;
using VolPro.Builder.IServices;
using VolPro.Core.EFDbContext;
namespace VolPro.Builder.Services;
/// <summary>
/// PostgreSQL 数据库表结构操作实现。
/// </summary>
public class PgSqlTableProvider : ITableDatabaseProvider
{
private readonly BaseDbContext _context;
private readonly string _schema;
public PgSqlTableProvider(BaseDbContext context)
{
_context = context;
// 从 PG 连接字符串推断 schema优先 SearchPath / search_path其次 schema默认 public
var connStr = _context.SqlSugarClient.Ado.Connection?.ConnectionString;
var schema = "public";
if (!string.IsNullOrWhiteSpace(connStr))
{
try
{
var builder = new NpgsqlConnectionStringBuilder(connStr);
if (!string.IsNullOrWhiteSpace(builder.SearchPath))
{
schema = builder.SearchPath.Split(',')[0].Trim();
}
else if (builder.TryGetValue("schema", out var sVal) && sVal is string s && !string.IsNullOrWhiteSpace(s))
{
schema = s.Trim();
}
}
catch
{
// 解析失败时回退到 public
schema = "public";
}
}
_schema = schema;
}
public async Task<bool> TableExistsAsync(string tableName)
{
const string pgSql = "SELECT COUNT(*) as value FROM information_schema.tables WHERE table_schema = @schemaName AND table_name = @tableName;";
var res = await _context.SqlSugarClient.Ado.GetScalarAsync(pgSql, new { schemaName = _schema, tableName });
return Convert.ToInt32(res) > 0;
}
public async Task CreateTableAsync(CreateTableRequest request)
{
await CreateTablePgSqlAsync(request);
}
public async Task<List<string>> GetAllTablesAsync()
{
const string pgSql = "SELECT table_name as value FROM information_schema.tables WHERE table_schema = @schemaName ORDER BY table_name;";
var rows = await _context.SqlSugarClient.Ado.SqlQueryAsync<string>(pgSql, new { schemaName = _schema });
return rows ?? new List<string>();
}
public async Task<TableInfoDto> GetTableInfoAsync(string tableName)
{
return await GetTableInfoPgSqlAsync(tableName);
}
public async Task UpdateTableAsync(UpdateTableRequest request)
{
await UpdateTablePgSqlAsync(request);
}
public async Task DeleteTableAsync(string tableName)
{
var dropPg = $"DROP TABLE \"{tableName.Replace("\"", "\"\"")}\";";
await _context.SqlSugarClient.Ado.ExecuteCommandAsync(dropPg);
}
private static string NormalizePgSqlDefault(string raw)
{
if (string.IsNullOrWhiteSpace(raw)) return null;
var s = raw.Trim();
if (s.StartsWith("'") && s.Contains("'::"))
{
var end = s.IndexOf("'::", StringComparison.Ordinal);
if (end > 0)
{
var inner = s[1..end].Replace("''", "'");
return inner;
}
}
if (s.StartsWith("nextval(")) return null;
return s;
}
private static string FormatDefaultForPgSql(string value)
{
if (string.IsNullOrWhiteSpace(value)) return string.Empty;
var v = value.Trim();
var vLower = v.ToLower();
if (vLower is "current_timestamp" or "now()" or "localtimestamp")
return $" DEFAULT {v}";
if (long.TryParse(v, out _) || decimal.TryParse(v, out _))
return $" DEFAULT {v}";
if (vLower == "true" || vLower == "1") return " DEFAULT true";
if (vLower == "false" || vLower == "0") return " DEFAULT false";
return $" DEFAULT '{v.Replace("'", "''")}'";
}
// PGSql 类型映射
private static string BuildPgSqlColumnType(TableColumnDto column)
{
var dt = (column.DataType ?? string.Empty).Trim();
var dtLower = dt.ToLower();
return dtLower switch
{
"nvarchar" or "varchar" or "character varying" => $"VARCHAR({(column.Length is > 0 ? column.Length : 255)})",
"char" or "nchar" => $"CHAR({(column.Length is > 0 ? column.Length : 1)})",
"nvarchar(max)" or "varchar(max)" or "text" or "ntext" => "TEXT",
"int" or "integer" => "INTEGER",
"bigint" => "BIGINT",
"tinyint" or "bit" => "SMALLINT",
"decimal" or "numeric" =>
$"NUMERIC({(column.Length ?? 18)},{(column.Scale ?? 0)})",
"float" or "double" => "DOUBLE PRECISION",
"datetime" => "TIMESTAMP",
"date" => "DATE",
"uniqueidentifier" or "uuid" => "UUID",
_ => dt
};
}
// PGSql 专用:建表
private async Task CreateTablePgSqlAsync(CreateTableRequest request)
{
var columnDefs = new List<string>();
foreach (var column in request.Columns.OrderBy(c => c.Order))
{
var typeSql = BuildPgSqlColumnType(column);
var isIntLike = typeSql.StartsWith("INTEGER", StringComparison.OrdinalIgnoreCase) ||
typeSql.StartsWith("INT4", StringComparison.OrdinalIgnoreCase) ||
typeSql.StartsWith("INT8", StringComparison.OrdinalIgnoreCase) ||
typeSql.StartsWith("INT4", StringComparison.OrdinalIgnoreCase) ||
typeSql.StartsWith("INT2", StringComparison.OrdinalIgnoreCase) ||
typeSql.StartsWith("BIGINT", StringComparison.OrdinalIgnoreCase);
var wantIdentity = column.IsIdentity && column.IsPrimaryKey && isIntLike;
// PGSql自增主键使用 GENERATED BY DEFAULT AS IDENTITY
var identitySql = wantIdentity ? " GENERATED ALWAYS AS IDENTITY " : string.Empty;
var nullSql = column.IsNullable && !column.IsPrimaryKey
? "NULL"
: "NOT NULL";
var defaultSql = FormatDefaultForPgSql(column.DefaultValue);
columnDefs.Add($"\"{column.ColumnName}\" {typeSql}{identitySql} {nullSql}{defaultSql}");
}
var pkCols = request.Columns.Where(c => c.IsPrimaryKey)
.OrderBy(c => c.Order)
.Select(c => $"\"{c.ColumnName}\"");
var pkSql = pkCols.Any() ? $", CONSTRAINT \"PK_{request.TableName}\" PRIMARY KEY ({string.Join(", ", pkCols)})" : string.Empty;
var createSql = $"CREATE TABLE \"{request.TableName}\" (\n {string.Join(",\n ", columnDefs)}{pkSql}\n);";
await _context.SqlSugarClient.Ado.ExecuteCommandAsync(createSql);
foreach (var column in request.Columns.Where(c => !string.IsNullOrWhiteSpace(c.Comment)))
{
var commentSql = $"COMMENT ON COLUMN \"{request.TableName}\".\"{column.ColumnName}\" IS '{column.Comment!.Replace("'", "''")}';";
await _context.SqlSugarClient.Ado.ExecuteCommandAsync(commentSql);
}
}
// PGSql 专用:获取表结构
private async Task<TableInfoDto> GetTableInfoPgSqlAsync(string tableName)
{
const string sql = @"
SELECT c.column_name, c.data_type, c.character_maximum_length, c.is_nullable, c.ordinal_position,
c.numeric_scale, c.numeric_precision,
CASE WHEN pk.column_name IS NOT NULL THEN 1 ELSE 0 END AS is_primary_key,
CASE WHEN c.column_default LIKE 'nextval(%' THEN 1 ELSE 0 END AS is_identity,
col_description(format('%I.%I', c.table_schema, c.table_name)::regclass::oid, a.attnum) AS comment,
c.column_default
FROM information_schema.columns c
LEFT JOIN (SELECT kcu.table_schema, kcu.table_name, kcu.column_name
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
WHERE tc.constraint_type = 'PRIMARY KEY') pk
ON pk.table_schema = c.table_schema AND pk.table_name = c.table_name AND pk.column_name = c.column_name
JOIN pg_attribute a ON a.attname = c.column_name AND a.attrelid = format('%I.%I', c.table_schema, c.table_name)::regclass
WHERE c.table_schema = @schemaName AND c.table_name = @tableName ORDER BY c.ordinal_position;";
var dt = await _context.SqlSugarClient.Ado.GetDataTableAsync(sql, new { schemaName = _schema, tableName });
var columns = new List<TableColumnDto>();
foreach (DataRow row in dt.Rows)
{
var dataType = row[1]?.ToString() ?? "";
var isDecimal = dataType.Equals("numeric", StringComparison.OrdinalIgnoreCase) || dataType.Equals("decimal", StringComparison.OrdinalIgnoreCase);
int? charLen = null;
if (row[2] != DBNull.Value && row[2] != null)
{
var raw = Convert.ToInt64(row[2]);
if (raw > 0 && raw <= int.MaxValue) charLen = (int)raw;
}
var numericScale = row[5] == DBNull.Value || row[5] == null ? (int?)null : Convert.ToInt32(row[5]);
var numericPrecision = row[6] == DBNull.Value || row[6] == null ? (int?)null : Convert.ToInt32(row[6]);
var colDefault = row[10] == DBNull.Value || row[10] == null ? null : NormalizePgSqlDefault(row[10]?.ToString());
columns.Add(new TableColumnDto
{
ColumnName = row[0]?.ToString() ?? "",
DataType = dataType,
Length = isDecimal ? numericPrecision : charLen,
Scale = isDecimal ? numericScale : null,
IsNullable = string.Equals(row[3]?.ToString(), "YES", StringComparison.OrdinalIgnoreCase),
Order = Convert.ToInt32(row[4]),
IsPrimaryKey = row[7] != DBNull.Value && row[7] != null && Convert.ToInt32(row[7]) == 1,
IsIdentity = row[8] != DBNull.Value && row[8] != null && Convert.ToInt32(row[8]) == 1,
Comment = row[9]?.ToString(),
DefaultValue = colDefault ?? string.Empty
});
}
return new TableInfoDto { TableName = tableName, Columns = columns };
}
// PGSql 专用:更新表(新增/删除/修改列 + 主键 & 注释)
private async Task UpdateTablePgSqlAsync(UpdateTableRequest request)
{
var existing = await GetTableInfoPgSqlAsync(request.TableName) ?? throw new InvalidOperationException("Table not found.");
var existingByName = existing.Columns.ToDictionary(c => c.ColumnName, c => c, StringComparer.Ordinal);
var newNames = request.Columns.Select(c => c.ColumnName).ToHashSet(StringComparer.Ordinal);
// 1) 识别并执行重命名字段(区分大小写)
var renames = request.Columns
.Where(c => c.IsDbField && !string.IsNullOrWhiteSpace(c.OriginalColumnName) &&
c.OriginalColumnName != c.ColumnName &&
existingByName.Keys.Any(k => string.Equals(k, c.OriginalColumnName, StringComparison.OrdinalIgnoreCase)))
.ToList();
foreach (var r in renames)
{
var actualKey = existingByName.Keys.FirstOrDefault(k => string.Equals(k, r.OriginalColumnName, StringComparison.OrdinalIgnoreCase));
if (actualKey == null) continue;
var renameSql = $"ALTER TABLE \"{request.TableName}\" RENAME COLUMN \"{actualKey.Replace("\"", "\"\"")}\" TO \"{r.ColumnName.Replace("\"", "\"\"")}\";";
await _context.SqlSugarClient.Ado.ExecuteCommandAsync(renameSql);
var oldCol = existingByName[actualKey];
existingByName.Remove(actualKey);
existingByName[r.ColumnName] = new TableColumnDto { ColumnName = r.ColumnName, DataType = oldCol.DataType, Length = oldCol.Length, Scale = oldCol.Scale, IsNullable = oldCol.IsNullable, IsPrimaryKey = oldCol.IsPrimaryKey, IsIdentity = oldCol.IsIdentity, Comment = oldCol.Comment, DefaultValue = oldCol.DefaultValue ?? "", Order = oldCol.Order };
}
var existingNames = existingByName.Keys.ToHashSet(StringComparer.Ordinal);
var columnsToDrop = existingNames.Except(newNames, StringComparer.Ordinal).ToList();
// 2) 新增列
foreach (var column in request.Columns.Where(c => !existingByName.ContainsKey(c.ColumnName)).OrderBy(c => c.Order))
{
var typeSql = BuildPgSqlColumnType(column);
var isIdentity = column.IsIdentity &&
(typeSql.StartsWith("INTEGER", StringComparison.OrdinalIgnoreCase) ||
typeSql.StartsWith("BIGINT", StringComparison.OrdinalIgnoreCase));
var identitySql = isIdentity ? " GENERATED ALWAYS AS IDENTITY" : string.Empty;
var nullSql = column.IsNullable && !column.IsPrimaryKey
? "NULL"
: "NOT NULL";
var defaultSql = FormatDefaultForPgSql(column.DefaultValue);
var addColSql = $"ALTER TABLE \"{request.TableName}\" ADD COLUMN \"{column.ColumnName}\" {typeSql}{identitySql} {nullSql}{defaultSql};";
await _context.SqlSugarClient.Ado.ExecuteCommandAsync(addColSql);
if (!string.IsNullOrWhiteSpace(column.Comment))
{
var commentSql = $"COMMENT ON COLUMN \"{request.TableName}\".\"{column.ColumnName}\" IS '{column.Comment!.Replace("'", "''")}';";
await _context.SqlSugarClient.Ado.ExecuteCommandAsync(commentSql);
}
}
// 3) 修改已有列:仅当有变更时才执行 ALTER
foreach (var column in request.Columns.Where(c => existingByName.ContainsKey(c.ColumnName)))
{
var typeSql = BuildPgSqlColumnType(column);
var old = existingByName[column.ColumnName];
var isIntegerLike = typeSql.StartsWith("INTEGER", StringComparison.OrdinalIgnoreCase) ||
typeSql.StartsWith("BIGINT", StringComparison.OrdinalIgnoreCase);
var wantIdentity = column.IsIdentity && column.IsPrimaryKey && isIntegerLike;
var oldTypeSql = BuildPgSqlColumnType(old);
var typeChanged = !string.Equals(oldTypeSql, typeSql, StringComparison.OrdinalIgnoreCase);
var nullableChanged = old.IsNullable != column.IsNullable;
var defaultChanged = (old.DefaultValue ?? "").Trim() != (column.DefaultValue ?? "").Trim();
var identityChanged = isIntegerLike && old.IsIdentity != wantIdentity;
var commentChanged = (old.Comment ?? "").Trim() != (column.Comment ?? "").Trim();
if (typeChanged)
{
var alterTypeSql = $"ALTER TABLE \"{request.TableName}\" ALTER COLUMN \"{column.ColumnName}\" TYPE {typeSql};";
await _context.SqlSugarClient.Ado.ExecuteCommandAsync(alterTypeSql);
}
if (nullableChanged)
{
var nullSql = column.IsNullable && !column.IsPrimaryKey ? "DROP NOT NULL" : "SET NOT NULL";
await _context.SqlSugarClient.Ado.ExecuteCommandAsync($"ALTER TABLE \"{request.TableName}\" ALTER COLUMN \"{column.ColumnName}\" {nullSql};");
}
if (defaultChanged)
{
var defaultSql = FormatDefaultForPgSql(column.DefaultValue);
if (!string.IsNullOrWhiteSpace(defaultSql))
await _context.SqlSugarClient.Ado.ExecuteCommandAsync($"ALTER TABLE \"{request.TableName}\" ALTER COLUMN \"{column.ColumnName}\" SET {defaultSql.Trim()};");
else
try { await _context.SqlSugarClient.Ado.ExecuteCommandAsync($"ALTER TABLE \"{request.TableName}\" ALTER COLUMN \"{column.ColumnName}\" DROP DEFAULT;"); } catch { }
}
if (identityChanged)
{
if (wantIdentity && !old.IsIdentity)
await _context.SqlSugarClient.Ado.ExecuteCommandAsync($"ALTER TABLE \"{request.TableName}\" ALTER COLUMN \"{column.ColumnName}\" ADD GENERATED BY DEFAULT AS IDENTITY;");
else if (!wantIdentity && old.IsIdentity)
await _context.SqlSugarClient.Ado.ExecuteCommandAsync($"ALTER TABLE \"{request.TableName}\" ALTER COLUMN \"{column.ColumnName}\" DROP IDENTITY IF EXISTS;");
}
if (commentChanged)
{
var commentText = string.IsNullOrWhiteSpace(column.Comment) ? "NULL" : $"'{column.Comment!.Replace("'", "''")}'";
await _context.SqlSugarClient.Ado.ExecuteCommandAsync($"COMMENT ON COLUMN \"{request.TableName}\".\"{column.ColumnName}\" IS {commentText};");
}
}
foreach (var colName in columnsToDrop)
{
await _context.SqlSugarClient.Ado.ExecuteCommandAsync($"ALTER TABLE \"{request.TableName}\" DROP COLUMN \"{colName}\";");
}
var newPkCols = request.Columns.Where(c => c.IsPrimaryKey).OrderBy(c => c.Order).Select(c => c.ColumnName).ToList();
var existingPkCols = existingByName.Values.Where(c => c.IsPrimaryKey).OrderBy(c => c.Order).Select(c => c.ColumnName).ToList();
var pkChanged = !newPkCols.SequenceEqual(existingPkCols);
if (pkChanged)
{
const string findPkSql = "SELECT c.conname FROM pg_constraint c JOIN pg_class t ON c.conrelid = t.oid JOIN pg_namespace n ON n.oid = t.relnamespace WHERE c.contype = 'p' AND n.nspname = @schemaName AND t.relname = @tableName;";
var pkDt = await _context.SqlSugarClient.Ado.GetDataTableAsync(findPkSql, new { schemaName = _schema, tableName = request.TableName });
foreach (DataRow r in pkDt.Rows)
{
var pkName = r[0]?.ToString();
if (!string.IsNullOrEmpty(pkName))
await _context.SqlSugarClient.Ado.ExecuteCommandAsync($"ALTER TABLE \"{_schema}\".\"{request.TableName}\" DROP CONSTRAINT \"{pkName}\";");
}
if (newPkCols.Any())
{
var pkColsSql = string.Join(", ", newPkCols.Select(c => $"\"{c}\""));
await _context.SqlSugarClient.Ado.ExecuteCommandAsync($"ALTER TABLE \"{_schema}\".\"{request.TableName}\" ADD CONSTRAINT \"PK_{request.TableName}\" PRIMARY KEY ({pkColsSql});");
}
}
}
}

View File

@@ -0,0 +1,558 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Threading.Tasks;
using VolPro.Builder.IServices;
using VolPro.Core.EFDbContext;
namespace VolPro.Builder.Services;
public class SqlServerTableProvider : ITableDatabaseProvider
{
private readonly BaseDbContext _context;
public SqlServerTableProvider(BaseDbContext context)
{
_context = context;
}
private static string NormalizeSqlServerDefault(string raw)
{
if (string.IsNullOrWhiteSpace(raw)) return null;
var s = raw.Trim();
while (s.StartsWith("(") && s.EndsWith(")")) s = s[1..^1].Trim();
return string.IsNullOrWhiteSpace(s) ? null : s;
}
private static string FormatDefaultForSqlServer(string value)
{
if (string.IsNullOrWhiteSpace(value)) return string.Empty;
var v = value.Trim();
var vLower = v.ToLower();
if (vLower is "getdate()" or "newid()" or "sysdatetime()")
return $" DEFAULT {v}";
if (long.TryParse(v, out _) || decimal.TryParse(v, out _))
return $" DEFAULT {v}";
if (vLower == "true" || vLower == "1") return " DEFAULT 1";
if (vLower == "false" || vLower == "0") return " DEFAULT 0";
return $" DEFAULT N'{v.Replace("'", "''")}'";
}
private static string BuildColumnDefinition(TableColumnDto column)
{
var definition = $"[{column.ColumnName}] ";
var dtLower = (column.DataType ?? "").Trim().ToLower();
if (dtLower == "nvarchar(max)") { dtLower = "nvarchar"; }
if (dtLower == "varchar(max)") { dtLower = "varchar"; }
if (dtLower is "nvarchar" or "varchar")
{
var length = (column.DataType ?? "").Contains("max", StringComparison.OrdinalIgnoreCase) ? -1 : (column.Length ?? 255);
definition += $"{dtLower}({(length == -1 ? "MAX" : length.ToString())})";
}
else if (dtLower is "char" or "nchar")
{
var length = column.Length ?? 1;
definition += $"{column.DataType}({length})";
}
else if (dtLower is "decimal" or "numeric")
{
var precision = column.Length ?? 18;
var scale = column.Scale ?? 0;
definition += $"{column.DataType}({precision}, {scale})";
}
else
{
definition += column.DataType ?? "nvarchar";
}
if (column.IsIdentity) definition += " IDENTITY(1,1)";
if (!column.IsNullable) definition += " NOT NULL";
definition += FormatDefaultForSqlServer(column.DefaultValue);
return definition;
}
/// <summary>
/// SQL Server 不支持 ALTER COLUMN 修改 IDENTITY通过临时表重建实现
/// </summary>
private async Task RebuildTableForIdentityChangeAsync(UpdateTableRequest request, Dictionary<string, TableColumnDto> existingByName)
{
var tableName = request.TableName;
var tmpName = $"Tmp_{tableName}_{Guid.NewGuid().ToString("N")[..8]}";
var colDefs = request.Columns.OrderBy(c => c.Order).Select(BuildColumnDefinition).ToList();
var createTmpSql = $"CREATE TABLE [{tmpName}] (\n " + string.Join(",\n ", colDefs) + "\n)";
await _context.SqlSugarClient.Ado.ExecuteCommandAsync(createTmpSql);
var colsToCopy = request.Columns.Where(c => existingByName.ContainsKey(c.ColumnName)).OrderBy(c => c.Order).Select(c => c.ColumnName).ToList();
var hasIdentity = request.Columns.Any(c => c.IsIdentity);
var colList = string.Join(", ", colsToCopy.Select(c => $"[{c}]"));
// IDENTITY_INSERT 是会话级,必须与 INSERT 在同一连接/同一批中执行
if (colsToCopy.Count > 0)
{
var insertBatch = hasIdentity
? $"SET IDENTITY_INSERT [{tmpName}] ON; INSERT INTO [{tmpName}] ({colList}) SELECT {colList} FROM [{tableName}]; SET IDENTITY_INSERT [{tmpName}] OFF"
: $"INSERT INTO [{tmpName}] ({colList}) SELECT {colList} FROM [{tableName}]";
await _context.SqlSugarClient.Ado.ExecuteCommandAsync(insertBatch);
}
var dropPkSql = $@"
DECLARE @pk NVARCHAR(200);
SELECT @pk = name FROM sys.key_constraints WHERE type = 'PK' AND parent_object_id = OBJECT_ID('{tableName.Replace("'", "''")}');
IF @pk IS NOT NULL EXEC('ALTER TABLE [{tableName}] DROP CONSTRAINT [' + @pk + ']')";
await _context.SqlSugarClient.Ado.ExecuteCommandAsync(dropPkSql);
await _context.SqlSugarClient.Ado.ExecuteCommandAsync($"DROP TABLE [{tableName}]");
await _context.SqlSugarClient.Ado.ExecuteCommandAsync($"EXEC sp_rename 'dbo.[{tmpName}]', '{tableName.Replace("'", "''")}', 'OBJECT'");
if (request.Columns.Any(c => c.IsPrimaryKey))
{
var pkCols = request.Columns.Where(c => c.IsPrimaryKey).OrderBy(c => c.Order).Select(c => $"[{c.ColumnName}]");
var addPkSql = $"ALTER TABLE [{tableName}] ADD CONSTRAINT [PK_{tableName}] PRIMARY KEY ({string.Join(", ", pkCols)})";
await _context.SqlSugarClient.Ado.ExecuteCommandAsync(addPkSql);
}
foreach (var column in request.Columns.Where(c => !string.IsNullOrWhiteSpace(c.Comment)))
{
try
{
var commentSql = $@"
EXEC sp_addextendedproperty @name = N'MS_Description', @value = N'{column.Comment.Replace("'", "''")}',
@level0type = N'SCHEMA', @level0name = N'dbo', @level1type = N'TABLE', @level1name = N'{tableName.Replace("'", "''")}', @level2type = N'COLUMN', @level2name = N'{column.ColumnName.Replace("'", "''")}'";
await _context.SqlSugarClient.Ado.ExecuteCommandAsync(commentSql);
}
catch { /* 注释已存在时忽略 */ }
}
}
public async Task<bool> TableExistsAsync(string tableName)
{
const string sql = "SELECT COUNT(*) as Value FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = @tableName";
var res = await _context.SqlSugarClient.Ado.GetScalarAsync(sql, new { tableName });
return Convert.ToInt32(res) > 0;
}
public async Task CreateTableAsync(CreateTableRequest request)
{
var columnDefinitions = new List<string>();
foreach (var column in request.Columns.OrderBy(c => c.Order))
{
var definition = $"[{column.ColumnName}] ";
// Data type and length
var dtLower = column.DataType.ToLower();
if (dtLower == "nvarchar" || dtLower == "varchar")
{
var length = column.Length ?? 255;
definition += $"{column.DataType}({(length == -1 ? "MAX" : length.ToString())})";
}
else if (dtLower == "char" || dtLower == "nchar")
{
var length = column.Length ?? 1;
definition += $"{column.DataType}({length})";
}
else if (dtLower == "decimal" || dtLower == "numeric")
{
var precision = column.Length ?? 18;
var scale = column.Scale ?? 0;
definition += $"{column.DataType}({precision}, {scale})";
}
else
{
definition += column.DataType;
}
// Identity (Auto increment)
if (column.IsIdentity)
{
definition += " IDENTITY(1,1)";
}
// Nullable
if (!column.IsNullable)
{
definition += " NOT NULL";
}
// Default value
definition += FormatDefaultForSqlServer(column.DefaultValue);
columnDefinitions.Add(definition);
}
// Build CREATE TABLE statement
var createTableSql = $"CREATE TABLE [{request.TableName}] (\n " +
string.Join(",\n ", columnDefinitions) +
"\n)";
await _context.SqlSugarClient.Ado.ExecuteCommandAsync(createTableSql);
// Add primary key constraint
var pkColumns = request.Columns
.Where(c => c.IsPrimaryKey)
.OrderBy(c => c.Order)
.Select(c => $"[{c.ColumnName}]");
var pkConstraintName = $"PK_{request.TableName}";
var pkSql = $"ALTER TABLE [{request.TableName}] ADD CONSTRAINT [{pkConstraintName}] PRIMARY KEY ({string.Join(", ", pkColumns)})";
await _context.SqlSugarClient.Ado.ExecuteCommandAsync(pkSql);
// Add column comments (MS_Description)
foreach (var column in request.Columns.Where(c => !string.IsNullOrWhiteSpace(c.Comment)))
{
var commentSql = $@"
EXEC sp_addextendedproperty
@name = N'MS_Description',
@value = N'{column.Comment.Replace("'", "''")}',
@level0type = N'SCHEMA', @level0name = N'dbo',
@level1type = N'TABLE', @level1name = N'{request.TableName}',
@level2type = N'COLUMN', @level2name = N'{column.ColumnName}'";
await _context.SqlSugarClient.Ado.ExecuteCommandAsync(commentSql);
}
}
public async Task<List<string>> GetAllTablesAsync()
{
const string sql = "SELECT TABLE_NAME as Value FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE' ORDER BY TABLE_NAME";
var rows = await _context.SqlSugarClient.Ado.SqlQueryAsync<string>(sql);
return rows ?? new List<string>();
}
public async Task<TableInfoDto> GetTableInfoAsync(string tableName)
{
const string columnsSql = @"
DECLARE @objId INT = OBJECT_ID(QUOTENAME('dbo') + '.' + QUOTENAME(@tableName));
SELECT
c.COLUMN_NAME,
c.DATA_TYPE,
c.CHARACTER_MAXIMUM_LENGTH,
c.IS_NULLABLE,
c.ORDINAL_POSITION,
c.NUMERIC_SCALE,
c.NUMERIC_PRECISION,
COLUMNPROPERTY(@objId, c.COLUMN_NAME, 'IsIdentity') AS IS_IDENTITY,
CASE WHEN pk.COLUMN_NAME IS NOT NULL THEN 1 ELSE 0 END AS IS_PRIMARY_KEY,
CAST(ep.value AS NVARCHAR(4000)) AS COMMENT,
c.COLUMN_DEFAULT
FROM INFORMATION_SCHEMA.COLUMNS c
LEFT JOIN (
SELECT ku.TABLE_NAME, ku.COLUMN_NAME
FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc
INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE ku
ON tc.CONSTRAINT_TYPE = 'PRIMARY KEY' AND tc.CONSTRAINT_NAME = ku.CONSTRAINT_NAME
) pk ON c.TABLE_NAME = pk.TABLE_NAME AND c.COLUMN_NAME = pk.COLUMN_NAME
LEFT JOIN sys.columns sc ON sc.object_id = @objId AND sc.name = c.COLUMN_NAME
LEFT JOIN sys.extended_properties ep ON ep.major_id = sc.object_id AND ep.minor_id = sc.column_id AND ep.name = 'MS_Description'
WHERE c.TABLE_SCHEMA = 'dbo' AND c.TABLE_NAME = @tableName
ORDER BY c.ORDINAL_POSITION;";
var dt = await _context.SqlSugarClient.Ado.GetDataTableAsync(columnsSql, new { tableName });
var columnsWithComments = new List<TableColumnDto>();
foreach (DataRow row in dt.Rows)
{
var length = row[2] == DBNull.Value || row[2] == null ? (int?)null : Convert.ToInt32(row[2]);
var numericScale = row[5] == DBNull.Value || row[5] == null ? (int?)null : Convert.ToInt32(row[5]);
var numericPrecision = row[6] == DBNull.Value || row[6] == null ? (int?)null : Convert.ToInt32(row[6]);
var dataType = row[1]?.ToString() ?? "";
var isDecimal = dataType.Equals("decimal", StringComparison.OrdinalIgnoreCase) || dataType.Equals("numeric", StringComparison.OrdinalIgnoreCase);
var colDefault = row[10] == DBNull.Value || row[10] == null ? null : NormalizeSqlServerDefault(row[10]?.ToString());
columnsWithComments.Add(new TableColumnDto
{
ColumnName = row[0]?.ToString() ?? "",
DataType = dataType,
Length = isDecimal ? numericPrecision : (length == -1 ? -1 : length),
Scale = isDecimal ? numericScale : null,
IsNullable = row[3]?.ToString() == "YES",
Order = Convert.ToInt32(row[4]),
IsIdentity = row[7] != DBNull.Value && row[7] != null && Convert.ToInt32(row[7]) == 1,
IsPrimaryKey = row[8] != DBNull.Value && row[8] != null && Convert.ToInt32(row[8]) == 1,
Comment = row[9]?.ToString(),
DefaultValue = colDefault ?? string.Empty
});
}
return new TableInfoDto { TableName = tableName, Columns = columnsWithComments };
}
public async Task UpdateTableAsync(UpdateTableRequest request)
{
var existingTable = await GetTableInfoAsync(request.TableName);
if (existingTable == null)
{
throw new InvalidOperationException($"Could not retrieve table information for '{request.TableName}'.");
}
var existingByName = existingTable.Columns.ToDictionary(c => c.ColumnName, c => c, StringComparer.Ordinal);
var newColumnNames = request.Columns.Select(c => c.ColumnName).ToHashSet(StringComparer.Ordinal);
// 1) 识别并执行重命名字段(区分大小写)
var renames = request.Columns
.Where(c => c.IsDbField && !string.IsNullOrWhiteSpace(c.OriginalColumnName) &&
c.OriginalColumnName != c.ColumnName &&
existingByName.Keys.Any(k => string.Equals(k, c.OriginalColumnName, StringComparison.OrdinalIgnoreCase)))
.ToList();
foreach (var r in renames)
{
var actualKey = existingByName.Keys.FirstOrDefault(k => string.Equals(k, r.OriginalColumnName, StringComparison.OrdinalIgnoreCase));
if (actualKey == null) continue;
var renameSql = $"EXEC sp_rename 'dbo.[{request.TableName}].[{actualKey}]', '{r.ColumnName.Replace("'", "''")}', 'COLUMN';";
await _context.SqlSugarClient.Ado.ExecuteCommandAsync(renameSql);
var oldCol = existingByName[actualKey];
existingByName.Remove(actualKey);
existingByName[r.ColumnName] = new TableColumnDto { ColumnName = r.ColumnName, DataType = oldCol.DataType, Length = oldCol.Length, Scale = oldCol.Scale, IsNullable = oldCol.IsNullable, IsPrimaryKey = oldCol.IsPrimaryKey, IsIdentity = oldCol.IsIdentity, Comment = oldCol.Comment, DefaultValue = oldCol.DefaultValue ?? "", Order = oldCol.Order };
}
var existingColumnNames = existingByName.Keys.ToHashSet(StringComparer.Ordinal);
var columnsToDrop = existingColumnNames.Except(newColumnNames);
foreach (var columnName in columnsToDrop)
{
var dropColumnSql = $"ALTER TABLE [{request.TableName}] DROP COLUMN [{columnName}]";
await _context.SqlSugarClient.Ado.ExecuteCommandAsync(dropColumnSql);
}
// 2) 新增列
var columnsToAdd = request.Columns.Where(c => !existingColumnNames.Contains(c.ColumnName, StringComparer.Ordinal));
foreach (var column in columnsToAdd.OrderBy(c => c.Order))
{
var definition = $"[{column.ColumnName}] ";
var dtLowerAdd = column.DataType.ToLower();
if (dtLowerAdd == "nvarchar" || dtLowerAdd == "varchar")
{
var length = column.Length ?? 255;
definition += $"{column.DataType}({(length == -1 ? "MAX" : length.ToString())})";
}
else if (dtLowerAdd == "char" || dtLowerAdd == "nchar")
{
var length = column.Length ?? 1;
definition += $"{column.DataType}({length})";
}
else if (dtLowerAdd == "decimal" || dtLowerAdd == "numeric")
{
var precision = column.Length ?? 18;
var scale = column.Scale ?? 0;
definition += $"{column.DataType}({precision}, {scale})";
}
else
{
definition += column.DataType;
}
if (column.IsIdentity)
{
definition += " IDENTITY(1,1)";
}
if (!column.IsNullable)
{
definition += " NOT NULL";
}
definition += FormatDefaultForSqlServer(column.DefaultValue);
var addColumnSql = $"ALTER TABLE [{request.TableName}] ADD {definition}";
await _context.SqlSugarClient.Ado.ExecuteCommandAsync(addColumnSql);
// 新列注释
if (!string.IsNullOrWhiteSpace(column.Comment))
{
try
{
var commentSql = $@"
EXEC sp_addextendedproperty
@name = N'MS_Description',
@value = N'{column.Comment.Replace("'", "''")}',
@level0type = N'SCHEMA', @level0name = N'dbo',
@level1type = N'TABLE', @level1name = N'{request.TableName.Replace("'", "''")}',
@level2type = N'COLUMN', @level2name = N'{column.ColumnName.Replace("'", "''")}'";
await _context.SqlSugarClient.Ado.ExecuteCommandAsync(commentSql);
}
catch { /* 注释已存在时忽略 */ }
}
}
// 3) Alter existing columns: 类型/长度/小数位/可空existingByName 已在 rename 后更新)
var columnsToAlter = request.Columns
.Where(c => existingByName.ContainsKey(c.ColumnName))
.OrderBy(c => c.Order);
foreach (var column in columnsToAlter)
{
var existing = existingByName[column.ColumnName];
var defaultChanged = (existing.DefaultValue ?? "").Trim() != (column.DefaultValue ?? "").Trim();
var newTypeRaw = column.DataType?.Trim() ?? "";
var newTypeLower = newTypeRaw.ToLower();
var oldTypeLower = (existing.DataType ?? "").Trim().ToLower();
// 兼容传入 nvarchar(max)/varchar(max)
if (newTypeLower == "nvarchar(max)") { newTypeLower = "nvarchar"; column.Length = -1; }
if (newTypeLower == "varchar(max)") { newTypeLower = "varchar"; column.Length = -1; }
bool typeChanged = !string.Equals(oldTypeLower, newTypeLower, StringComparison.OrdinalIgnoreCase);
bool nullableChanged = existing.IsNullable != column.IsNullable;
bool lengthChanged = false;
bool scaleChanged = false;
if (newTypeLower is "nvarchar" or "varchar")
{
var oldLen = existing.Length ?? 255;
var newLen = column.Length ?? 255;
lengthChanged = oldLen != newLen;
}
else if (newTypeLower is "char" or "nchar")
{
var oldLen = existing.Length ?? 1;
var newLen = column.Length ?? 1;
lengthChanged = oldLen != newLen;
}
else if (newTypeLower is "decimal" or "numeric")
{
var oldPrec = existing.Length ?? 18;
var newPrec = column.Length ?? 18;
lengthChanged = oldPrec != newPrec;
var oldScale = existing.Scale ?? 0;
var newScale = column.Scale ?? 0;
scaleChanged = oldScale != newScale;
}
bool isIdentityChanged = existing.IsIdentity != column.IsIdentity;
// 仅当字段有实际修改时才执行更新脚本
if (!typeChanged && !nullableChanged && !lengthChanged && !scaleChanged && !defaultChanged && !isIdentityChanged)
continue;
// IsIdentity 变更SQL Server 不支持 ALTER COLUMN 修改 IDENTITY需通过临时表重建
if (isIdentityChanged)
{
await RebuildTableForIdentityChangeAsync(request, existingByName);
var refreshed = await GetTableInfoAsync(request.TableName);
foreach (var c in refreshed.Columns)
existingByName[c.ColumnName] = c;
break;
}
if (typeChanged || nullableChanged || lengthChanged || scaleChanged)
{
string typeSql;
if (newTypeLower is "nvarchar" or "varchar")
{
var len = column.Length ?? 255;
typeSql = $"{newTypeLower}({(len == -1 ? "MAX" : len.ToString())})";
}
else if (newTypeLower is "char" or "nchar")
{
var len = column.Length ?? 1;
typeSql = $"{newTypeLower}({len})";
}
else if (newTypeLower is "decimal" or "numeric")
{
var precision = column.Length ?? 18;
var scale = column.Scale ?? 0;
typeSql = $"{newTypeLower}({precision}, {scale})";
}
else
{
typeSql = newTypeRaw;
}
var nullSql = column.IsNullable ? "NULL" : "NOT NULL";
var alterSql = $"ALTER TABLE [{request.TableName}] ALTER COLUMN [{column.ColumnName}] {typeSql} {nullSql}";
await _context.SqlSugarClient.Ado.ExecuteCommandAsync(alterSql);
}
if (defaultChanged)
{
var dfName = $"DF_{request.TableName}_{column.ColumnName}";
var dropDfSql = $@"DECLARE @cn NVARCHAR(200);
SELECT @cn = dc.name FROM sys.default_constraints dc
INNER JOIN sys.columns c ON dc.parent_object_id = c.object_id AND dc.parent_column_id = c.column_id
WHERE c.object_id = OBJECT_ID('{request.TableName.Replace("'", "''")}') AND c.name = N'{column.ColumnName.Replace("'", "''")}';
IF @cn IS NOT NULL EXEC('ALTER TABLE [{request.TableName}] DROP CONSTRAINT [' + @cn + ']')";
await _context.SqlSugarClient.Ado.ExecuteCommandAsync(dropDfSql);
if (!string.IsNullOrWhiteSpace(column.DefaultValue))
{
var defVal = column.DefaultValue.Trim();
var vLower = defVal.ToLower();
var innerExpr = vLower is "getdate()" or "newid()" or "sysdatetime()" ? defVal
: long.TryParse(defVal, out _) || decimal.TryParse(defVal, out _) ? defVal
: vLower == "true" || vLower == "1" ? "1"
: vLower == "false" || vLower == "0" ? "0"
: $"N'{defVal.Replace("'", "''")}'";
var addDfSql = $"ALTER TABLE [{request.TableName}] ADD CONSTRAINT [{dfName}] DEFAULT ({innerExpr}) FOR [{column.ColumnName}]";
await _context.SqlSugarClient.Ado.ExecuteCommandAsync(addDfSql);
}
}
}
var existingPkCols = existingTable.Columns.Where(c => c.IsPrimaryKey).OrderBy(c => c.Order).Select(c => c.ColumnName).ToList();
var newPkCols = request.Columns.Where(c => c.IsPrimaryKey).OrderBy(c => c.Order).Select(c => c.ColumnName).ToList();
var pkChanged = !existingPkCols.SequenceEqual(newPkCols);
if (pkChanged)
{
// Drop existing PK constraint
var dropPkSql = $@"
DECLARE @pkConstraintName NVARCHAR(200);
SELECT @pkConstraintName = name
FROM sys.key_constraints
WHERE type = 'PK' AND parent_object_id = OBJECT_ID('{request.TableName}');
IF @pkConstraintName IS NOT NULL
EXEC('ALTER TABLE [{request.TableName}] DROP CONSTRAINT [' + @pkConstraintName + ']')";
await _context.SqlSugarClient.Ado.ExecuteCommandAsync(dropPkSql);
if (newPkCols.Any())
{
var pkConstraintName = $"PK_{request.TableName}";
var pkColumns = newPkCols.Select(c => $"[{c}]");
var addPkSql = $"ALTER TABLE [{request.TableName}] ADD CONSTRAINT [{pkConstraintName}] PRIMARY KEY ({string.Join(", ", pkColumns)})";
await _context.SqlSugarClient.Ado.ExecuteCommandAsync(addPkSql);
}
}
foreach (var column in request.Columns.Where(c => !string.IsNullOrWhiteSpace(c.Comment)))
{
if (existingByName.TryGetValue(column.ColumnName, out var ec) && (ec.Comment ?? "").Trim() == (column.Comment ?? "").Trim())
continue;
var escapedComment = column.Comment!.Replace("'", "''");
var tableNameEscaped = request.TableName.Replace("'", "''");
var columnNameEscaped = column.ColumnName.Replace("'", "''");
try
{
var updateCommentSql = $@"
EXEC sp_updateextendedproperty
@name = N'MS_Description',
@value = N'{escapedComment}',
@level0type = N'SCHEMA', @level0name = N'dbo',
@level1type = N'TABLE', @level1name = N'{tableNameEscaped}',
@level2type = N'COLUMN', @level2name = N'{columnNameEscaped}'";
await _context.SqlSugarClient.Ado.ExecuteCommandAsync(updateCommentSql);
}
catch
{
try
{
var addCommentSql = $@"
EXEC sp_addextendedproperty
@name = N'MS_Description',
@value = N'{escapedComment}',
@level0type = N'SCHEMA', @level0name = N'dbo',
@level1type = N'TABLE', @level1name = N'{tableNameEscaped}',
@level2type = N'COLUMN', @level2name = N'{columnNameEscaped}'";
await _context.SqlSugarClient.Ado.ExecuteCommandAsync(addCommentSql);
}
catch { /* 忽略 */ }
}
}
}
public async Task DeleteTableAsync(string tableName)
{
var dropTableSql = $"DROP TABLE [{tableName}]";
await _context.SqlSugarClient.Ado.ExecuteCommandAsync(dropTableSql);
}
}

View File

@@ -0,0 +1,41 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using System;
using VolPro.Builder.IServices;
using VolPro.Core.Const;
using VolPro.Core.DBManager;
using VolPro.Core.EFDbContext;
using VolPro.Core.Extensions.AutofacManager;
using VolPro.Core.Utilities;
namespace VolPro.Builder.Services;
public class TableProviderFactory : ITableProviderFactory, IDependency
{
private readonly IHttpContextAccessor _httpContextAccessor;
public TableProviderFactory(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public ITableDatabaseProvider GetProvider(string dbService)
{
var type = DbRelativeCache.GetDbContextType(dbService);
object dbObj = _httpContextAccessor.HttpContext.RequestServices.GetService(type);
BaseDbContext _context = dbObj as BaseDbContext;
string dbType = DbRelativeCache.GetDbType(dbService);
switch (dbType)
{
case "MySql":
return new MySqlTableProvider(_context);
case "PgSql":
return new PgSqlTableProvider(_context);
case "MsSql":
return new SqlServerTableProvider(_context);
default:
throw new Exception("数据库未支持");
}
}
}

View File

@@ -0,0 +1,106 @@
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using VolPro.Builder.IServices;
using VolPro.Builder.Services;
using VolPro.Core;
using VolPro.Core.Extensions.AutofacManager;
using VolPro.Core.UserManager;
using VolPro.Core.Utilities;
namespace VolPro.Builder.Services;
public class TableService : ITableService, IDependency
{
private readonly ITableProviderFactory _factory;
WebResponseContent webResponse = new WebResponseContent();
public TableService(ITableProviderFactory factory)
{
_factory = factory;
}
public async Task<bool> TableExistsAsync(string dbService, string tableName)
{
var provider = _factory.GetProvider(dbService);
return await provider.TableExistsAsync(tableName);
}
public async Task<WebResponseContent> CreateTableAsync(string dbService, CreateTableRequest request)
{
var provider = _factory.GetProvider(dbService);
if (await provider.TableExistsAsync(request.TableName))
{
return webResponse.Error($"{request.TableName}表已存在");
}
await provider.CreateTableAsync(request);
return webResponse.OK("创建成功");
}
public async Task<object> GetAllTablesAsync(string dbService)
{
var provider = _factory.GetProvider(dbService);
var tables = await provider.GetAllTablesAsync();
var tableInfo = TableColumnContext.TableInfo;
var list = tables.Select(s => new
{
table = s,
name = tableInfo.Where(x => x.TableName == s).Select(x => x.ColumnCNName).FirstOrDefault() ?? s
}).OrderBy(item => !IsStartWithLetter(item.name))
.ThenBy(item => item.name, StringComparer.OrdinalIgnoreCase)
.ToList();
return list;
}
private static bool IsStartWithLetter(string str)
{
if (string.IsNullOrEmpty(str)) return false;
char firstChar = str[0];
return (firstChar >= 'a' && firstChar <= 'z') ||
(firstChar >= 'A' && firstChar <= 'Z');
}
public async Task<TableInfoDto> GetTableInfoAsync(string dbService, string tableName)
{
var provider = _factory.GetProvider(dbService);
var res= await provider.GetTableInfoAsync(tableName);
foreach (var item in res.Columns)
{
item.IsDbField = true;
item.OriginalColumnName = item.ColumnName ?? string.Empty;
item.Comment = TableColumnContext.Data.Where(x => x.ColumnName == item.ColumnName).Select(s => s.ColumnCnName).FirstOrDefault() ?? item.Comment;
}
return res;
}
public async Task<WebResponseContent> UpdateTableAsync(string dbService, UpdateTableRequest request)
{
var provider = _factory.GetProvider(dbService);
if (!await provider.TableExistsAsync(request.TableName))
{
return webResponse.Error($"{request.TableName}不存在");
}
await provider.UpdateTableAsync(request);
return webResponse.OK("修改成功");
}
public async Task<WebResponseContent> DeleteTableAsync(string dbService, string tableName)
{
if (tableName.ToLower().StartsWith("sys"))
{
return webResponse.Error("系统表不能删除,请在数据库操作".Translator());
}
var provider = _factory.GetProvider(dbService);
if (!await provider.TableExistsAsync(tableName)) {
return webResponse.Error($"{tableName}不存在");
}
await provider.DeleteTableAsync(tableName);
return webResponse.OK("删除成功");
}
}