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,138 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Controllers;
using System.Linq;
using VolPro.Core.Controllers.Basic;
using VolPro.Core.Filters;
using VolPro.Core.UserManager;
using VolPro.Entity.DomainModels;
using VolPro.Core.ManageUser;
using VolPro.Core.Enums;
using VolPro.Core.Utilities;
using Microsoft.AspNetCore.Mvc.Authorization;
using System.Reflection;
using VolPro.Core.Extensions;
namespace VolPro.Core.Generic
{
[JWTAuthorize, ApiController]
public class GenericBaseController : VolController
{
public GenericBaseController() { }
public override void OnActionExecuting(ActionExecutingContext context)
{
GenericTableAsyncLocal.Clear();
string TableName = null;
if (context.ActionArguments?.Count > 0)
{
foreach (var argument in context.ActionArguments.Values.Where(argument => argument != null))
{
var argumentType = argument.GetType();
if (argumentType == typeof(PageDataOptions)|| argumentType == typeof(SaveModel))
{
var tableNameProperty = argumentType.GetProperty("TableName");
if (tableNameProperty != null)
{
TableName = tableNameProperty.GetValue(argument)?.ToString();
}
}
}
}
WebResponseContent webResponse = new();
if (string.IsNullOrEmpty(TableName))
{
TableName = HttpContext.Request.Query["tableName"];
}
if (string.IsNullOrEmpty(TableName))
{
context.Result = GetResult(context, "缺少参数table,请检查代码生成器生器Sys_TableInfo、Sys_TableColumn是否有当前表配置或菜单设置的表名是否正确");
return;
}
var list = TableColumnContext.TableInfo
.Where(x => x.TableName == TableName).ToList();
if (list.Count == 0)
{
context.Result = GetResult(context, $"未找到表【{TableName}】 配置信息,请检查代码生成器配置是否存在当前表");
context.Result = Json(webResponse);
return;
}
if (list.Count > 1)
{
context.Result = GetResult(context, $"表【{TableName}】 存在多个配置信息,请检查代码生成器配置是否重复");
return;
}
GenericTableAsyncLocal.CurrentTableName = TableName;
if (context.Filters.Any(item => item is IAllowAnonymousFilter))
{
base.OnActionExecuting(context);
return;
}
if (UserContext.Current.IsSuperAdmin)
{
base.OnActionExecuting(context);
return;
}
string[] currentActionPermissionNames = [];
if (!(context.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor))
{
base.OnActionExecuting(context);
return;
}
CustomAttributeData attrData = controllerActionDescriptor.MethodInfo
.CustomAttributes
.FirstOrDefault(a => a.AttributeType == typeof(ApiActionPermissionAttribute))
?? controllerActionDescriptor.ControllerTypeInfo
.CustomAttributes
.FirstOrDefault(a => a.AttributeType == typeof(ApiActionPermissionAttribute));
if (attrData == null)
{
base.OnActionExecuting(context);
return;
}
ActionPermissionOptions currentActionPermission = default;
foreach (var arg in attrData.ConstructorArguments)
{
if (arg.ArgumentType == typeof(ActionPermissionOptions) && arg.Value != null)
{
currentActionPermission = (ActionPermissionOptions)arg.Value;
break;
}
}
if (Equals(currentActionPermission, default(ActionPermissionOptions)))
{
base.OnActionExecuting(context);
return;
}
//ActionPermissionFilter.cs中统一验证权限
//var names = new List<string>();
//foreach (ActionPermissionOptions option in Enum.GetValues(typeof(ActionPermissionOptions)))
//{
// if (option == 0) continue;
// if (currentActionPermission.HasFlag(option))
// {
// names.Add(option.ToString());
// }
//}
//currentActionPermissionNames = names.ToArray();
//var hasActionAuth = UserContext.Current.Permissions
// .Where(x => x.TableName == TableName.ToLower())
// .Any(c => c.UserAuthArr != null && currentActionPermissionNames.Any(action => c.UserAuthArr.Contains(action)));
//if (!hasActionAuth)
//{
// context.Result = GetResult(context, "没有权限操作");
// return;
//}
base.OnActionExecuting(context);
}
private IActionResult GetResult(ActionExecutingContext context, string message)
{
return Json(new { status = false, message });
}
}
}

View File

@@ -0,0 +1,164 @@
using Dapper;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using SqlSugar;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using VolPro.Core.Controllers.Basic;
using VolPro.Core.Dapper;
using VolPro.Core.DBManager;
using VolPro.Core.DbSqlSugar;
using VolPro.Core.EFDbContext;
using VolPro.Core.Filters;
using VolPro.Core.UserManager;
using VolPro.Core.Utilities;
namespace VolPro.Core.Generic
{
public class GenericBaseService
{
public WebResponseContent WebResponse { get; set; }
public GenericBaseService()
{
WebResponse = new WebResponseContent();
}
public int DbParamsCount
{
get
{
return DbRelativeCache.GetDbType(TableInfo.DBServer) == "MsSql" ? 2000 : 100000;
}
}
private UserManager.TableInfo _tableInfo { get; set; }
public UserManager.TableInfo TableInfo
{
get
{
if (_tableInfo != null) return _tableInfo;
_tableInfo = TableColumnContext.TableInfo
.Where(x => x.TableName == GenericTableAsyncLocal.CurrentTableName)
.FirstOrDefault();
if (_tableInfo == null) throw new Exception($" 未找到表【{GenericTableAsyncLocal.CurrentTableName}】 配置信息");
if (string.IsNullOrEmpty(_tableInfo.DBServer))
{
_tableInfo.DBServer = typeof(SysDbContext).Name;
}
return _tableInfo;
}
}
public UserManager.TableInfo GetDetailTableInfo(string tableName = null)
{
if (string.IsNullOrEmpty(TableInfo.DetailName))
{
return null;
}
//主从表
if (string.IsNullOrEmpty(tableName))
{
return TableColumnContext.TableInfo.Where(x => x.TableName == TableInfo.DetailName).FirstOrDefault();
}
//一对多
return TableColumnContext.TableInfo.Where(x => x.TableName == tableName).FirstOrDefault();
}
public UserManager.TableInfo GetTableInfo(string tableName = null)
{
return TableColumnContext.TableInfo.Where(x => x.TableName == tableName).FirstOrDefault();
}
private BaseDbContext _dbContext { get; set; }
public BaseDbContext DbContext
{
get
{
if (_dbContext != null) return _dbContext;
string dbServer = _tableInfo.DBServer;
var type = DbRelativeCache.GetDbContextType(dbServer);
object dbObj = HttpContext.Current.RequestServices.GetService(type);
_dbContext = (BaseDbContext)dbObj;
return _dbContext;
}
}
private List<TableColumnField> _columns { get; set; }
public List<TableColumnField> Columns
{
get
{
if (_columns != null) return _columns;
_columns = TableColumnContext.Data
.Where(x => x.TableName == GenericTableAsyncLocal.CurrentTableName)
.ToList();
return _columns;
}
}
/// <summary>
/// 实际表字段
/// </summary>
private List<TableColumnField> _tableColumns { get; set; }
public List<TableColumnField> TableColumns
{
get
{
if (_tableColumns != null) return _tableColumns;
_tableColumns = Columns.Where(x => x.ReferenceField == 0).ToList();
return _tableColumns;
}
}
public List<TableColumnField> GetTableColumns(string table)
{
return TableColumnContext.Data
.Where(x => x.TableName == table)
.ToList();
}
private ISqlSugarClient _genericDbContext { get; set; }
public ISqlSugarClient GenericDbContext
{
get
{
if (_genericDbContext == null)
{
_genericDbContext = DbManger.GetSqlSugarClient(TableInfo.DBServer);
}
return _genericDbContext;
}
}
public async Task<List<dynamic>> QueryListAsync(string sql, List<SugarParameter> parameters)
{
return await GenericDbContext.Ado.SqlQueryAsync<dynamic>(sql, parameters);
}
public async Task<object> ExecuteScalarAsync(string sql, List<SugarParameter> parameters)
{
return await GenericDbContext.Ado.GetScalarAsync(sql, parameters);
}
public async Task<int> ExcuteNonQueryAsync(string sql, object parameters)
{
return await GenericDbContext.Ado.ExecuteCommandAsync(sql, parameters);
}
public async Task BeginTranAsync()
{
await GenericDbContext.Ado.BeginTranAsync();
}
public async Task CommitTranAsync()
{
await GenericDbContext.Ado.CommitTranAsync();
}
public async Task RollbackTranAsync()
{
await GenericDbContext.Ado.RollbackTranAsync();
}
}
}

View File

@@ -0,0 +1,928 @@
using Microsoft.AspNetCore.Http;
using OfficeOpenXml.FormulaParsing.Excel.Functions.Math;
using OfficeOpenXml.FormulaParsing.Excel.Functions.Text;
using SqlSugar;
using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using VolPro.Core.DBManager;
using VolPro.Core.Enums;
using VolPro.Core.Extensions;
using VolPro.Core.Services;
using VolPro.Core.Tenancy;
using VolPro.Core.UserManager;
using VolPro.Core.Utilities;
using VolPro.Entity.DomainModels;
namespace VolPro.Core.Generic
{
/// <summary>
/// 通用数据库 CRUD 抽象基类,封装三种数据库公共逻辑
/// </summary>
public abstract class GenericDbProviderBase : GenericBaseService, IGenericDbProvider
{
/// <summary>
/// 不同数据库的左右引号符号MySql: ``, PgSql: "", SqlServer: []
/// </summary>
protected abstract string LeftQuote { get; }
protected abstract string RightQuote { get; }
protected GenericDbProviderBase() : base()
{
}
public virtual async Task<PageGridData<object>> GetPageDataAsync(PageDataOptions options, bool isDetail = false)
{
//租户过滤
options = options ?? new PageDataOptions();
if (!string.IsNullOrEmpty(options.Wheres) && (options.Filter == null || options.Filter.Count == 0))
{
options.Filter = options.Wheres.DeserializeObject<List<SearchParameters>>();
}
//获取逻辑删除过滤
string logicDelField = this.GetLogicDelField(Columns);
if (logicDelField != null)
{
options.Filter.Add(new SearchParameters() { Name = logicDelField, Value = "0" });
}
string dbTableName = string.IsNullOrEmpty(TableInfo.TableTrueName)
? TableInfo.TableName
: TableInfo.TableTrueName;
string selectColumns = string.Join(",", Columns.Select(c => $"{LeftQuote}{c.ColumnName}{RightQuote}"));
string baseSql = string.IsNullOrEmpty(TableInfo.DbSql)
? $"SELECT {selectColumns} FROM {LeftQuote}{dbTableName}{RightQuote}"
: $"SELECT * from ({TableInfo.DbSql}) TDbSQL ";
var whereList = new List<string>();
// SqlSugar 参数集合
var parameters = new List<SugarParameter>();
//执行自定义sql
baseSql = dbTableName.GetSearchSqlQuery(baseSql, options.Filter, parameters) ?? baseSql;
//过滤租户数据权限
baseSql = dbTableName.CreateTenancySqlFilter(baseSql, options.Filter, parameters);
this.BuildWhere(options.Filter, Columns, whereList, parameters, LeftQuote, RightQuote);
var baseWithWhere = new StringBuilder();
baseWithWhere.Append(baseSql);
if (whereList.Count > 0)
{
baseWithWhere.Append(" WHERE ").Append(string.Join(" AND ", whereList));
}
string sortField = options.Sort;
string sortOrder = string.IsNullOrEmpty(options.Order) ? "DESC" : options.Order.ToUpper();
if (string.IsNullOrEmpty(sortField))
{
sortField = Columns.FirstOrDefault(c => c.IsKey == 1)?.ColumnName
?? Columns.First().ColumnName;
}
string orderBy = $"ORDER BY {LeftQuote}{sortField}{RightQuote} {(sortOrder == "ASC" ? "ASC" : "DESC")}";
string countSql = $"SELECT COUNT(1) FROM ({baseWithWhere}) T";
//导出
if (options.Export)
{
options.Rows = 200000;
}
int page = options.Page <= 0 ? 1 : options.Page;
int rows = options.Rows <= 0 ? 30 : options.Rows;
string pageSql = BuildPageSql(baseWithWhere.ToString(), selectColumns, orderBy, page, rows);
return new PageGridData<object>()
{
rows = await QueryListAsync(pageSql, parameters),
total = options.Export ? 0 : (await ExecuteScalarAsync(countSql, parameters)).GetInt()
};
}
public virtual async Task<PageGridData<object>> GetDetailPageAsync(PageDataOptions options)
{
return await GetPageDataAsync(options, true);
}
/// <summary>
/// 构造分页 SQL默认使用 LIMIT/OFFSETMySql、PgSql
/// </summary>
protected virtual string BuildPageSql(string baseWithWhereSql, string selectColumns, string orderBy, int page, int rows)
{
int offset = (page - 1) * rows;
return $"{baseWithWhereSql} {orderBy} LIMIT {rows} OFFSET {offset}";
}
/// <summary>
/// 添加数据Add
/// </summary>
/// <param name="saveModel"></param>
/// <returns></returns>
public virtual async Task<WebResponseContent> AddAsync(SaveModel saveModel)
{
var response = WebResponseContent.Instance;
if (saveModel == null || saveModel.MainData == null || saveModel.MainData.Count == 0)
{
return response.Error("提交数据为空");
}
string dbTableName = string.IsNullOrEmpty(TableInfo.TableTrueName)
? TableInfo.TableName
: TableInfo.TableTrueName;
var tableColumns = TableColumns;
if (tableColumns == null || tableColumns.Count == 0)
{
return response.Error("未找到表字段配置信息");
}
var keyColumn = tableColumns.FirstOrDefault(c => c.IsKey == 1);
// 先设置创建人/创建时间默认值
saveModel.MainData.SetCreateDefaultVal();
//生成自增单据号
IdentitySqlCode.CreateCode(saveModel.MainData, TableInfo.TableName, tableColumns, LeftQuote, RightQuote);
// 设置逻辑删除字段0
this.SetLogicDelDefault(saveModel.MainData, tableColumns)
//审批字段默认值为0
.SetAuditDefault(saveModel.MainData, tableColumns)
// 按主键类型自动生成主键值
.SetPrimaryKey(saveModel.MainData, keyColumn);
// 根据字段配置校验主表必填、长度、类型
string validMsg = this.ValidateColumns(saveModel.MainData, tableColumns);
if (!string.IsNullOrEmpty(validMsg))
{
return response.Error(validMsg);
}
// 在主表校验通过后,提前校验所有明细表(忽略外键字段的必填)
validMsg = this.ValidateAllDetails(saveModel, tableColumns, keyColumn);
if (!string.IsNullOrEmpty(validMsg))
{
return response.Error(validMsg);
}
bool keyIsIdentity = this.IsIdentity(keyColumn.ColumnType);
// 需要忽略的字段(修改人、修改时间等)
var ignoreFields = this.GetModifyFieldsToIgnore();
var fieldNames = new List<string>();
var paramNames = new List<string>();
var parameters = new List<SugarParameter>();
// 只写入 TableColumns 中配置的真实表字段,且排除需要忽略的字段
foreach (var col in tableColumns)
{
if (ignoreFields.Contains(col.ColumnName)) continue;
if (!saveModel.MainData.TryGetValue(col.ColumnName, out object value)) continue;
//自增不写入主键
if (keyColumn.ColumnName == col.ColumnName && keyIsIdentity) continue;
// 对于允许为 null 的字段,需要支持显式写入 NULL。
// Dapper/Npgsql 不接受 System.DBNull 类型作为参数实体成员,这里统一将 DBNull 转成 null。
if (value is DBNull)
{
value = null;
}
value = CoerceDapperParam(value, col);
fieldNames.Add($"{LeftQuote}{col.ColumnName}{RightQuote}");
paramNames.Add("@" + col.ColumnName);
var par = new SugarParameter("@" + col.ColumnName, value);
var dbType = GenericDbValidationExtensions.EffectiveDapperDbType(value, col);
if (dbType != null)
{
par.DbType = (System.Data.DbType)dbType;
}
parameters.Add(par);
}
if (fieldNames.Count == 0) return response.Error("提交字段与表配置不匹配");
string insertSql = $"INSERT INTO {LeftQuote}{dbTableName}{RightQuote} ({string.Join(",", fieldNames)}) VALUES ({string.Join(",", paramNames)})";
try
{
await BeginTranAsync();
if (keyIsIdentity)
{
saveModel.MainData.Remove(keyColumn.ColumnName);
string identitySql = BuildIdentitySql(keyColumn);
object newId = await ExecuteInsertWithIdentityAsync(insertSql, identitySql, parameters, keyColumn);
saveModel.MainData[keyColumn.ColumnName] = long.Parse(newId.ToString());
}
else
{
await ExcuteNonQueryAsync(insertSql, parameters);
}
// 主表插入成功后,处理一对多明细
await InsertDetailsAsync(saveModel, keyColumn);
Logger.OK(LoggerType.Add, saveModel.Serialize());
response.OK(ResponseType.SaveSuccess);
response.Data = saveModel.MainData;
await CommitTranAsync();
return response;
}
catch (Exception ex)
{
await RollbackTranAsync();
throw new Exception($"add新建异常table:{TableInfo.TableName},参数:{saveModel.Serialize()},异常信息:{ex.Message + ex.StackTrace}");
}
}
/// <summary>
/// 编辑
/// </summary>
/// <param name="saveModel"></param>
/// <returns></returns>
public virtual async Task<WebResponseContent> UpdateAsync(SaveModel saveModel)
{
var response = WebResponseContent.Instance;
string dbTableName = string.IsNullOrEmpty(TableInfo.TableTrueName)
? TableInfo.TableName
: TableInfo.TableTrueName;
var keyColumn = TableColumns.FirstOrDefault(c => c.IsKey == 1);
if (keyColumn == null)
{
return response.Error("未配置主键,不能编辑");
}
var columns = TableColumns.Where(x => x.ReferenceField == 0).ToList();
// 1、saveModel.MainData 取出主键字段,如果没有值,提示缺少主键字段参数
if (!saveModel.MainData.TryGetValue(keyColumn.ColumnName, out object keyVal) || string.IsNullOrEmpty(keyVal.ToString()))
{
return response.Error(ResponseType.KeyError);
}
saveModel.MainData.SetModifyDefaultVal();
var data = saveModel.MainData
.Where(kv => !kv.Key.Equals(keyColumn.ColumnName, StringComparison.OrdinalIgnoreCase)
&& columns.Any(c => c.ColumnName.Equals(kv.Key, StringComparison.OrdinalIgnoreCase)))
.ToDictionary(k => k.Key, v => v.Value);
if (data.Count == 0)
{
return response.Error("没有需要更新的字段");
}
// 2、根据 TableColumns 中的字段校验 MainData 中【提交的字段】:
// IsNull、Maxlength、ColumnType只针对 data 里存在的字段)
var submitMainColumns = columns
.Where(c => data.ContainsKey(c.ColumnName))
.ToList();
string validMsg = this.ValidateColumns(saveModel.MainData, submitMainColumns);
if (!string.IsNullOrEmpty(validMsg))
{
return response.Error(validMsg);
}
// 3、明细表数据校验规则同 Add但编辑时只校验提交的字段并忽略外键必填
validMsg = this.ValidateAllDetailsForUpdate(saveModel, keyColumn);
if (!string.IsNullOrEmpty(validMsg))
{
return response.Error(validMsg);
}
try
{
var setList = new List<string>();
var parameters = new List<SugarParameter>();
// 5、6忽略 ReferenceField=1 的字段已经在 columns 过滤;这里再忽略 ModifyMember 对应字段
var ignoreFields = this.GetAddFieldsToIgnore();
foreach (var kv in data)
{
if (ignoreFields.Contains(kv.Key)) continue;
// 校验阶段可能已把 MainData 中的字符串转为 Guid 等data 为校验前快照,优先取 MainData
if (!saveModel.MainData.TryGetValue(kv.Key, out object value))
{
value = kv.Value;
}
if (value is DBNull)
{
value = null;
}
var col = columns.FirstOrDefault(c => c.ColumnName.Equals(kv.Key, StringComparison.OrdinalIgnoreCase));
if (col == null) continue;
value = CoerceDapperParam(value, col);
string paramName = kv.Key;
setList.Add($"{LeftQuote}{kv.Key}{RightQuote} = @{paramName}");
//parameters.Add(new SugarParameter("@" + paramName, value));
var par = new SugarParameter("@" + col.ColumnName, value);
var dbType = GenericDbValidationExtensions.EffectiveDapperDbType(value, col);
if (dbType != null)
{
par.DbType = (System.Data.DbType)dbType;
}
parameters.Add(par);
}
object pkParam = CoerceDapperParam(keyVal, keyColumn) ?? DBNull.Value;
var parPk = new SugarParameter("@pk", pkParam);
var dbTypePk = GenericDbValidationExtensions.EffectiveDapperDbType(pkParam, keyColumn);
if (dbTypePk != null)
{
parPk.DbType = (System.Data.DbType)dbTypePk;
}
parameters.Add(parPk);
await BeginTranAsync();
string sql = $"UPDATE {LeftQuote}{dbTableName}{RightQuote} SET {string.Join(",", setList)} WHERE {LeftQuote}{keyColumn.ColumnName}{RightQuote} = @pk";
int count = await ExcuteNonQueryAsync(sql, parameters);
if (count <= 0)
{
await RollbackTranAsync();
return response.Error("未找到更新的数据");
}
// 4、根据明细主键是否有值区分新建或编辑分别执行插入或更新
response = await UpdateDetailsAsync(saveModel, keyColumn, keyVal);
if (!response.Status)
{
await RollbackTranAsync();
return response;
}
await CommitTranAsync();
response.OK(ResponseType.EidtSuccess);
}
catch (Exception ex)
{
await RollbackTranAsync();
throw new Exception($"编辑异常table:{TableInfo.TableName},参数:{saveModel.Serialize()},异常信息:{ex.Message + ex.StackTrace}");
}
return response;
}
public virtual async Task<WebResponseContent> DelAsync(List<object> keys, bool delDetail = true)
{
try
{
await BeginTranAsync();
var res = await Del(keys, TableInfo.TableTrueName);
if (delDetail && !string.IsNullOrEmpty(TableInfo.DetailName))
{
var tables = TableInfo.DetailName.Split(",");
foreach (var table in tables)
{
res = await Del(keys, table);
}
}
await CommitTranAsync();
return WebResponse.OK("删除成功".Translator());
}
catch (Exception ex)
{
await RollbackTranAsync();
throw new Exception($"表删除异常table:{TableInfo.TableName},异常信息:{ex.Message + ex.StackTrace}");
}
}
private async Task<WebResponseContent> Del(List<object> keys, string table, TableColumnField keyColumn = null)
{
if (keys == null || keys.Count == 0) return WebResponse.OK("无数据");
string dbTableName = table;
keyColumn = keyColumn ?? GetTableColumns(table).FirstOrDefault(c => c.IsKey == 1);
if (keyColumn == null) return WebResponse.Error("未配置主键,不能删除");
keys = keys.Select(k => CoerceDapperParam(k, keyColumn)).ToList();
var parameters = new List<SugarParameter>();
string sql = $"DELETE FROM {LeftQuote}{dbTableName}{RightQuote} WHERE {LeftQuote}{keyColumn.ColumnName}{RightQuote} IN (@keys)";
var parKeys = new SugarParameter("@keys", keys);
var dbTypeKeys = keys != null && keys.Count > 0
? GenericDbValidationExtensions.EffectiveDapperDbType(keys[0], keyColumn)
: keyColumn?.GetDbType();
if (dbTypeKeys != null)
{
parKeys.DbType = (System.Data.DbType)dbTypeKeys;
}
parameters.Add(parKeys);
int count = await ExcuteNonQueryAsync(sql, parameters);
WebResponse.OK(ResponseType.DelSuccess);
return WebResponse;
}
public virtual async Task<WebResponseContent> UploadAsync(List<IFormFile> files)
{
if (files == null || files.Count == 0) return WebResponse.Error("请上传文件");
string date = DateTime.Now.ToString("yyyMMddHHmmsss");
string filePath = $"Upload/Generic/{TableInfo.TableName}/{date}/";
string fullPath = filePath.MapPath(true);
if (!Directory.Exists(fullPath)) Directory.CreateDirectory(fullPath);
for (int i = 0; i < files.Count; i++)
{
string fileName = Utilities.HttpContext.Current.Request("fileName");
if (string.IsNullOrEmpty(fileName))
{
fileName = files[i].FileName;
}
using var stream = new FileStream(fullPath + fileName, FileMode.Create);
await files[i].CopyToAsync(stream);
}
return WebResponse.OK("文件上传成功".Translator(), filePath);
}
/// <summary>
/// 下载导入Excel模板基于当前表配置动态生成
/// </summary>
/// <returns>Excel 文件字节数组</returns>
public byte[] DownLoadTemplateAsync()
{
var ignoreFields = this.GetAddAndModifyFieldsToIgnore();
var bytes = GenericExcelTemplateHelper.BuildTemplateBytes(TableInfo.ColumnCNName, TableColumns.Where(x => !ignoreFields.Contains(x.ColumnName)).ToList());
return bytes;
}
/// <summary>
/// 导入表数据Excel
/// </summary>
/// <param name="fileInput"></param>
/// <returns></returns>
public async Task<WebResponseContent> ImportAsync(List<IFormFile> fileInput)
{
if (fileInput == null || fileInput.Count == 0)
{
return WebResponse.Error("请选择上传的文件".Translator());
}
var ignoreFields = this.GetAddAndModifyFieldsToIgnore();
// TableColumns.Where(x => !ignoreFields.Contains(x.ColumnName)).ToList()
var formFile = fileInput[0];
List<Dictionary<string, object>> rows = null;
using (var stream = formFile.OpenReadStream())
{
var resp = GenericExcelImportHelper.ReadRowsByCellOptions(TableInfo.TableName, stream, ignoreFields: ignoreFields);
if (!resp.Status) return resp;
rows = ((List<Dictionary<string, object>>)resp.Data).Where(x => x.Count > 0).ToList();
}
if (rows == null || rows.Count == 0)
{
return WebResponse.Error("未读取到导入数据".Translator());
}
var keyColumn = TableColumns.FirstOrDefault(c => c.IsKey == 1);
string msg = this.ValidateDetailList(TableInfo.DetailName, rows, keyColumn);
if (!string.IsNullOrEmpty(msg))
{
return WebResponse.Error(msg);
}
foreach (var row in rows)
{
IdentitySqlCode.CreateCode(row, TableInfo.TableName, TableColumns, LeftQuote, RightQuote);
}
//明细表导入
string mainId = Utilities.HttpContext.Current.Request.Query["id"];
if (!string.IsNullOrEmpty(mainId))
{
string mainTable = Utilities.HttpContext.Current.Request.Query["mainTable"];
var keyName = GetTableInfo(mainTable)?.MainKeyField ??
GetTableColumns(mainTable).Where(x => x.IsKey == 1).Select(s => s.ColumnName).FirstOrDefault();
if (string.IsNullOrEmpty(keyName))
{
return WebResponse.Error("未找到明细表的主表配置信息,请检查代码生成器是否有主表配置".Translator());
}
foreach (var item in rows)
{
item[keyName] = mainId;
}
}
await InsertDetailListAsync(TableInfo.TableName, rows, null, null);
return WebResponse.OK("导入成功,共{$ts}条".TranslatorFormat(rows.Count), new { rows.Count });
}
/// <summary>
/// 导出文件(参照 ServiceBase.Export实现字典数据源转换、字段显示等
/// </summary>
/// <param name="loadData"></param>
/// <returns>Excel 文件字节数组</returns>
public async Task<byte[]> ExportAsync(PageDataOptions loadData)
{
loadData.Export = true;
var pageData = await GetPageDataAsync(loadData);
var dictRows = new List<Dictionary<string, object>>();
foreach (var item in pageData.rows)
{
var entry = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
if (item is IDictionary<string, object> genericDict)
{
foreach (var kv in genericDict)
{
entry[kv.Key] = kv.Value;
}
dictRows.Add(entry);
}
}
var exportFields = loadData.Columns ?? [];
var ignoreColumns = new List<string>();
var bytes = GenericExcelExportHelper.BuildExportBytes(TableInfo.TableName, dictRows, loadData.Columns ?? [], ignoreColumns);
return bytes;
}
public async Task<WebResponseContent> AuditAsync(object[] id, int? auditStatus, string auditReason)
{
string table = TableInfo.TableName;
await Task.CompletedTask;
return null;
}
/// </summary>
protected virtual string BuildIdentitySql(TableColumnField keyColumn, bool batch = false)
{
return null;
}
protected virtual async Task<object> ExecuteInsertWithIdentityAsync(string insertSql, string identitySql, List<SugarParameter> parameters, TableColumnField keyColumn)
{
return await ExecuteScalarAsync($"{insertSql} {identitySql}", parameters);
}
/// <summary>
/// 插入一对多明细数据
/// </summary>
protected virtual async Task InsertDetailsAsync(SaveModel saveModel, TableColumnField mainKeyColumn)
{
// 主键值
object mainKeyValue = saveModel.MainData[mainKeyColumn.ColumnName];
// 单明细表 List<Dictionary<string, object>>
if (saveModel.DetailData != null && saveModel.DetailData.Count > 0)
{
await InsertDetailListAsync(TableInfo.DetailName, saveModel.DetailData, mainKeyColumn, mainKeyValue);
saveModel.MainData[TableInfo.DetailName] = saveModel.DetailData;
}
// 新结构:多明细 List<DetailInfo>
if (saveModel.Details != null && saveModel.Details.Count > 0)
{
foreach (var item in saveModel.Details)
{
if (item?.Data == null || item.Data.Count == 0) continue;
await InsertDetailListAsync(item.Table, item.Data, mainKeyColumn, mainKeyValue);
saveModel.MainData[item.Table] = item.Data;
}
}
}
/// <summary>
/// Update 时,根据明细主键是否有值区分新建/编辑,分别执行插入或更新
/// </summary>
private async Task<WebResponseContent> UpdateDetailsAsync(SaveModel saveModel, TableColumnField mainKeyColumn, object mainKeyValue)
{
// 旧结构:单明细表
if (saveModel.DetailData != null && saveModel.DetailData.Count > 0 && !string.IsNullOrEmpty(TableInfo.DetailName))
{
await UpsertDetailListAsync(TableInfo.DetailName, saveModel.DetailData, mainKeyColumn, mainKeyValue);
WebResponse = await Del(saveModel.DelKeys, TableInfo.DetailName);
if (!WebResponse.Status)
{
return WebResponse;
}
}
// 新结构:多明细
if (saveModel.Details != null && saveModel.Details.Count > 0)
{
foreach (var item in saveModel.Details)
{
if (item?.Data == null || item.Data.Count == 0) continue;
await UpsertDetailListAsync(item.Table, item.Data, mainKeyColumn, mainKeyValue);
WebResponse = await Del(item.DelKeys, item.Table);
if (!WebResponse.Status)
{
return WebResponse;
}
}
}
return WebResponse.OK();
}
/// <summary>
/// 对指定明细表的数据做“有主键则更新,无主键则插入”的操作
/// </summary>
private async Task UpsertDetailListAsync(string detailTableName, List<Dictionary<string, object>> rows, TableColumnField mainKeyColumn, object mainKeyValue)
{
if (string.IsNullOrEmpty(detailTableName) || rows == null || rows.Count == 0) return;
var detailColumns = TableColumnContext.Data
.Where(x => x.TableName == detailTableName && x.ReferenceField == 0)
.ToList();
if (detailColumns == null || detailColumns.Count == 0) return;
var detailKeyCol = detailColumns.FirstOrDefault(c => c.IsKey == 1);
if (detailKeyCol == null) return;
var foreignCol = detailColumns.FirstOrDefault(c =>
c.ColumnName.Equals(mainKeyColumn.ColumnName, StringComparison.OrdinalIgnoreCase)
&& string.Equals(c.ColumnType, mainKeyColumn.ColumnType, StringComparison.OrdinalIgnoreCase));
var detailTableInfo = TableColumnContext.TableInfo
.FirstOrDefault(t => t.TableName == detailTableName);
string detailDbTableName = string.IsNullOrEmpty(detailTableInfo?.TableTrueName)
? detailTableName
: detailTableInfo.TableTrueName;
var ignoreModifyFields = this.GetModifyFieldsToIgnore();
var ignoreAddFields = this.GetAddFieldsToIgnore();
bool keyIsIdentity = this.IsIdentity(detailKeyCol.ColumnType);
// 新增明细行
var addRows = new List<Dictionary<string, object>>();
// 编辑明细行批量 UPDATE控制单次参数数量
int maxParams = DbParamsCount;
var updateSqlBuilder = new StringBuilder();
var updateParameters = new List<SugarParameter>();
int currentUpdateParamCount = 0;
int updateRowIndex = 0;
async Task FlushUpdateAsync()
{
if (updateSqlBuilder.Length == 0) return;
await ExcuteNonQueryAsync(updateSqlBuilder.ToString(), updateParameters);
updateSqlBuilder.Clear();
updateParameters = new List<SugarParameter>();
currentUpdateParamCount = 0;
updateRowIndex = 0;
}
foreach (var row in rows)
{
if (row == null) continue;
bool hasDetailKey = row.TryGetValue(detailKeyCol.ColumnName, out object detailKeyVal)
&& detailKeyVal != null
&& !string.IsNullOrEmpty(detailKeyVal.ToString())
&& !new string[] { "0", Guid.Empty.ToString() }.Contains(detailKeyVal?.ToString());
// 外键字段
if (foreignCol != null)
{
row[foreignCol.ColumnName] = mainKeyValue;
}
if (!hasDetailKey)
{
// 新建明细:仅加入待新增列表,真正的插入逻辑统一走 InsertDetailListAsync与 Add 保持一致)
addRows.Add(row);
}
else
{
// 编辑明细:只更新提交的字段
row.SetModifyDefaultVal();
var setList = new List<string>();
var localParams = new List<(string ParamName, object Value, TableColumnField Col)>();
foreach (var kv in row)
{
if (kv.Key.Equals(detailKeyCol.ColumnName, StringComparison.OrdinalIgnoreCase)) continue;
if (ignoreAddFields.Contains(kv.Key)) continue;
var col = detailColumns.FirstOrDefault(c => c.ColumnName.Equals(kv.Key, StringComparison.OrdinalIgnoreCase));
if (col == null) continue;
object value = kv.Value;
if (value is DBNull) value = null;
string baseParamName = kv.Key;
string paramName = $"{baseParamName}_u{updateRowIndex}";
setList.Add($"{LeftQuote}{kv.Key}{RightQuote} = @{paramName}");
localParams.Add((paramName, value, col));
}
if (setList.Count == 0) continue;
// 预估本行 UPDATE 需要的参数数量(字段数 + 主键)
int needed = localParams.Count + 1;
if (currentUpdateParamCount + needed > maxParams)
{
await FlushUpdateAsync();
}
foreach (var (ParamName, Value, Col) in localParams)
{
object v = CoerceDapperParam(Value, Col);
var par = new SugarParameter("@" + ParamName, v);
var dbType = GenericDbValidationExtensions.EffectiveDapperDbType(v, Col);
if (dbType != null)
{
par.DbType = (System.Data.DbType)dbType;
}
updateParameters.Add(par);
currentUpdateParamCount++;
}
string pkParamName = $"{detailKeyCol.ColumnName}_pk{updateRowIndex}";
object detailPk = CoerceDapperParam(detailKeyVal, detailKeyCol);
var parPk = new SugarParameter("@" + pkParamName, detailPk);
var dbTypePk = GenericDbValidationExtensions.EffectiveDapperDbType(detailPk, detailKeyCol);
if (dbTypePk != null)
{
parPk.DbType = (System.Data.DbType)dbTypePk;
}
updateParameters.Add(parPk);
currentUpdateParamCount++;
string updateSql =
$"UPDATE {LeftQuote}{detailDbTableName}{RightQuote} SET {string.Join(",", setList)} WHERE {LeftQuote}{detailKeyCol.ColumnName}{RightQuote} = @{pkParamName};";
updateSqlBuilder.AppendLine(updateSql);
updateRowIndex++;
}
}
if (addRows.Count > 0)
{
await InsertDetailListAsync(detailTableName, addRows, mainKeyColumn, mainKeyValue);
}
await FlushUpdateAsync();
}
/// <summary>
/// 按规则插入某一张明细表的数据列表(批量 SQL控制单次参数数量
/// </summary>
private async Task InsertDetailListAsync(string detailTableName, List<Dictionary<string, object>> rows, TableColumnField mainKeyColumn, object mainKeyValue)
{
if (string.IsNullOrEmpty(detailTableName) || rows == null || rows.Count == 0) return;
// 获取明细表字段配置
var detailColumns = TableColumnContext.Data
.Where(x => x.TableName == detailTableName && x.ReferenceField == 0)
.ToList();
if (detailColumns == null || detailColumns.Count == 0) return;
// 明细表主键
var detailKeyCol = detailColumns.FirstOrDefault(c => c.IsKey == 1);
if (detailKeyCol == null) return;
// 外键字段:与主表主键同名、同类型
TableColumnField foreignCol = null;
if (mainKeyColumn != null)
{
foreignCol = detailColumns.FirstOrDefault(c => c.ColumnName == mainKeyColumn.ColumnName);
if (foreignCol == null) return;
}
// 明细表真实库表名
var detailTableInfo = TableColumnContext.TableInfo
.FirstOrDefault(t => t.TableName == detailTableName);
string detailDbTableName = string.IsNullOrEmpty(detailTableInfo?.TableTrueName)
? detailTableName
: detailTableInfo.TableTrueName;
// 忽略字段(修改人/修改时间)
var ignoreFields = this.GetModifyFieldsToIgnore();
bool keyIsIdentity = this.IsIdentity(detailKeyCol.ColumnType);
// 参与插入的列:自增时排除主键;非自增时包含主键(由代码生成)
var insertColumns = detailColumns
.Where(col =>
{
if (ignoreFields.Contains(col.ColumnName)) return false;
if (keyIsIdentity && col.ColumnName.Equals(detailKeyCol.ColumnName, StringComparison.OrdinalIgnoreCase))
return false;
return true;
})
.ToList();
if (insertColumns.Count == 0) return;
string columnList = string.Join(",", insertColumns.Select(c => $"{LeftQuote}{c.ColumnName}{RightQuote}"));
// batch=true 时:自增返回 RETURNING/OUTPUT/;SELECT 等片段,非自增返回 null
string identitySqlPart = keyIsIdentity ? BuildIdentitySql(detailKeyCol, true) : string.Empty;
bool needReturnIds = !string.IsNullOrWhiteSpace(identitySqlPart);
int maxParams = DbParamsCount;
var parameters = new List<SugarParameter>();
var batchRows = new List<Dictionary<string, object>>();
var batchRowIndexes = new List<int>();
int currentParamCount = 0;
int globalRowIndex = 0;
async Task FlushAsync()
{
if (batchRows.Count == 0) return;
var valuesClauses = new List<string>();
for (int i = 0; i < batchRows.Count; i++)
{
int rowIdx = batchRowIndexes[i];
var valueParams = insertColumns.Select(c => "@" + $"{c.ColumnName}_{rowIdx}").ToList();
valuesClauses.Add($"({string.Join(",", valueParams)})");
}
string sql;
//sqlserver批量返回语法OUTPUT
if (needReturnIds && identitySqlPart.TrimStart().StartsWith("OUTPUT", StringComparison.OrdinalIgnoreCase))
sql = $"INSERT INTO {LeftQuote}{detailDbTableName}{RightQuote} ({columnList}) {identitySqlPart} VALUES {string.Join(",", valuesClauses)};";
else
sql = $"INSERT INTO {LeftQuote}{detailDbTableName}{RightQuote} ({columnList}) VALUES {string.Join(",", valuesClauses)}{identitySqlPart};";
if (needReturnIds)
{
var ids = (await QueryListAsync(sql, parameters))
.Serialize()
.DeserializeObject<List<Dictionary<string, long>>>()
.SelectMany(x => x.Values)
.ToList();
if (this is GenericMySqlProvider && ids != null && ids.Count == 1 && batchRows.Count > 0)
{
long firstId = Convert.ToInt64(ids[0]);
for (int i = 0; i < batchRows.Count; i++)
batchRows[i][detailKeyCol.ColumnName] = firstId + i;
}
else
{
for (int i = 0; i < batchRows.Count && i < ids.Count; i++)
batchRows[i][detailKeyCol.ColumnName] = ids[i];
}
}
else
{
await ExcuteNonQueryAsync(sql, parameters);
}
parameters = new List<SugarParameter>();
batchRows.Clear();
batchRowIndexes.Clear();
currentParamCount = 0;
}
foreach (var row in rows)
{
if (row == null) continue;
if (foreignCol != null)
{
row[foreignCol.ColumnName] = mainKeyValue;
}
row.SetCreateDefaultVal();
this.SetLogicDelDefault(row, detailColumns).SetAuditDefault(row, detailColumns);
if (!keyIsIdentity)
this.SetPrimaryKey(row, detailKeyCol);
int needed = insertColumns.Count;
if (currentParamCount + needed > maxParams)
await FlushAsync();
foreach (var col in insertColumns)
{
row.TryGetValue(col.ColumnName, out object value);
if (value is DBNull) value = null;
value = CoerceDapperParam(value, col);
var par = new SugarParameter("@" + $"{col.ColumnName}_{globalRowIndex}", value);
var dbType = GenericDbValidationExtensions.EffectiveDapperDbType(value, col);
if (dbType != null)
{
par.DbType = (System.Data.DbType)dbType;
}
parameters.Add(par);
currentParamCount++;
}
batchRows.Add(row);
batchRowIndexes.Add(globalRowIndex);
globalRowIndex++;
}
await FlushAsync();
}
/// <summary>
/// SqlServer / PostgreSQLGuid 列须把 string、JsonElement 转为 Guid。MySql 等保持原样。
/// PostgreSQL元数据为 varchar(Text) 且值为 Guid 时转为字符串以绑 Text。
/// </summary>
private object CoerceDapperParam(object value, TableColumnField col)
{
if (col == null) return value;
if (col.ColumnType == "long")
{
return Convert.ToInt64(value);
}
string db = DbRelativeCache.GetDbType(TableInfo.DBServer);
if (db == "PgSql" && !GenericDbValidationExtensions.IsGuidColumn(col) && col.GetDbType() ==System.Data.DbType.String && value is Guid pgGuid)
return pgGuid.ToString();
if (db != "MsSql" && db != "PgSql") return value;
if (!GenericDbValidationExtensions.IsGuidColumn(col)) return value;
if (value is Guid) return value;
if (value == null || value is DBNull) return null;
string s;
if (value is string str) s = str;
else if (value is JsonElement je && je.ValueKind == JsonValueKind.String) s = je.GetString();
else s = value?.ToString();
if (string.IsNullOrWhiteSpace(s)) return null;
return Guid.TryParse(s, out var g) ? (object)g : value;
}
}
}

View File

@@ -0,0 +1,411 @@
using SqlSugar;
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using VolPro.Core.Configuration;
using VolPro.Core.UserManager;
using VolPro.Core.Utilities;
using VolPro.Entity.DomainModels;
namespace VolPro.Core.Generic
{
public static class GenericDbProviderExtensions
{
/// <summary>
/// 设置逻辑删除字段、审批字段默认值为0
/// </summary>
public static T SetLogicDelDefault<T>(this T provider,
Dictionary<string, object> mainData,
List<TableColumnField> columns)
where T : GenericDbProviderBase
{
string LogicDelField = provider.GetLogicDelField(columns);
if (LogicDelField != null)
{
mainData[LogicDelField] = 0;
}
return provider;
}
/// <summary>
/// 获取逻辑删除字段
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="columns"></param>
/// <returns></returns>
public static string GetLogicDelField<T>(this T provider, List<TableColumnField> columns)
where T : GenericDbProviderBase
{
if (!string.IsNullOrEmpty(AppSetting.LogicDelField))
{
var logicCol = columns.FirstOrDefault(c =>
c.ColumnName.Equals(AppSetting.LogicDelField, StringComparison.OrdinalIgnoreCase));
if (logicCol != null)
{
return logicCol.ColumnName;
}
}
return null;
}
public static T SetAuditDefault<T>(this T provider,
Dictionary<string, object> mainData,
List<TableColumnField> columns)
where T : GenericDbProviderBase
{
var auditCol = columns.FirstOrDefault(c =>
c.ColumnName.Equals("AuditStatus", StringComparison.OrdinalIgnoreCase));
if (auditCol != null)
{
if (!mainData.TryGetValue(auditCol.ColumnName, out object val)
|| val == null
|| (val is string s && string.IsNullOrWhiteSpace(s)))
{
mainData[auditCol.ColumnName] = 0;
}
}
return provider;
}
/// <summary>
/// 根据主键类型自动生成主键值雪花、Guid、string-Guid
/// </summary>
public static T SetPrimaryKey<T>(this T provider,
Dictionary<string, object> mainData,
TableColumnField keyColumn)
where T : GenericDbProviderBase
{
if (keyColumn == null) return provider;
string keyName = keyColumn.ColumnName;
string type = (keyColumn.ColumnType ?? "").Trim().ToLower();
mainData.TryGetValue(keyName, out object keyVal);
bool hasValue = !string.IsNullOrEmpty(keyVal?.ToString());
// bigint/long 雪花ID
if ((type == "long" || type == "bigint") && AppSetting.UseSnow)
{
var worker = new IdWorker();
long id = worker.NextId();
mainData[keyName] = id;
return provider;
}
// Guid 主键,string 主键,且未传值时生成 Guid 字符串
if (type == "guid" || (type == "string" && !hasValue))
{
mainData[keyName] = Guid.NewGuid();
return provider;
}
return provider;
}
private static List<string> ModifyFields { get; set; }
/// <summary>
/// 获取需要忽略修改人/修改时间字段
/// </summary>
public static List<string> GetModifyFieldsToIgnore<T>(this T provider)
where T : GenericDbProviderBase
{
if (ModifyFields != null)
{
return ModifyFields;
}
var ignore = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
if (!string.IsNullOrWhiteSpace(AppSetting.ModifyMember?.UserIdField))
{
ignore.Add(AppSetting.ModifyMember.UserIdField);
}
if (!string.IsNullOrWhiteSpace(AppSetting.ModifyMember?.UserNameField))
{
ignore.Add(AppSetting.ModifyMember.UserNameField);
}
if (!string.IsNullOrWhiteSpace(AppSetting.ModifyMember?.DateField))
{
ignore.Add(AppSetting.ModifyMember.DateField);
}
ModifyFields = ignore.ToList();
return ModifyFields;
}
private static HashSet<string> AddFields { get; set; }
/// <summary>
/// 获取需要忽略写入人
/// </summary>
public static HashSet<string> GetAddFieldsToIgnore<T>(this T provider)
where T : GenericDbProviderBase
{
if (AddFields != null)
{
return AddFields;
}
var ignore = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
if (!string.IsNullOrWhiteSpace(AppSetting.CreateMember?.UserIdField))
{
ignore.Add(AppSetting.CreateMember.UserIdField);
}
if (!string.IsNullOrWhiteSpace(AppSetting.CreateMember?.UserNameField))
{
ignore.Add(AppSetting.CreateMember.UserNameField);
}
if (!string.IsNullOrWhiteSpace(AppSetting.CreateMember?.DateField))
{
ignore.Add(AppSetting.CreateMember.DateField);
}
AddFields = ignore;
return AddFields;
}
private static HashSet<string> AddAndModifyFields { get; set; }
public static HashSet<string> GetAddAndModifyFieldsToIgnore<T>(this T provider)
where T : GenericDbProviderBase
{
if (AddAndModifyFields == null)
{
AddAndModifyFields = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
GetAddFieldsToIgnore(provider).ToList().ForEach(x => AddAndModifyFields.Add(x));
GetModifyFieldsToIgnore(provider).ToList().ForEach(x => AddAndModifyFields.Add(x));
}
return AddAndModifyFields;
}
/// <summary>
/// 判断字段类型是否为整型int/long/bigint
/// </summary>
public static bool IsIntOrLong<T>(this T provider, string columnType)
where T : GenericDbProviderBase
{
string type = (columnType ?? "").Trim().ToLower();
return type == "int" || type == "long" || type == "bigint";
}
/// <summary>
/// 是否自增主键
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="provider"></param>
/// <param name="columnType"></param>
/// <returns></returns>
public static bool IsIdentity<T>(this T provider, string columnType)
where T : GenericDbProviderBase
{
string type = (columnType ?? "").Trim().ToLower();
if (type == "int")
{
return true;
}
return (type == "long" || type == "bigint") && !AppSetting.UseSnow;
}
// GetDbType / IsZero 已移动到 GenericDbValidationExtensions.cs
private static void AddWhereSugarParameter(List<SugarParameter> parameters, string name, object value, TableColumnField col)
{
var par = new SugarParameter(name, value);
var dbType = GenericDbValidationExtensions.EffectiveDapperDbType(value, col);
if (dbType != null)
{
par.DbType = (System.Data.DbType)dbType;
}
parameters.Add(par);
}
/// <summary>
/// 构造查询条件 where 语句
/// </summary>
public static T BuildWhere<T>(this T provider,
List<SearchParameters> filters,
List<TableColumnField> columns,
List<string> whereList,
List<SugarParameter> parameters,
string LeftQuote,
string RightQuote)
where T : GenericDbProviderBase
{
if (filters == null || filters.Count == 0) return provider;
int index = 0;
foreach (var f in filters)
{
if (string.IsNullOrEmpty(f?.Name)) continue;
var col = columns.FirstOrDefault(c =>
c.ColumnName.Equals(f.Name, StringComparison.OrdinalIgnoreCase));
if (col == null) continue;
string op = (f.DisplayType ?? "").ToLower();
if (op == "isnull")
{
whereList.Add($"{LeftQuote}{col.ColumnName}{RightQuote} IS NULL");
continue;
}
if (op == "isnotnull")
{
whereList.Add($"{LeftQuote}{col.ColumnName}{RightQuote} IS NOT NULL");
continue;
}
if (string.IsNullOrEmpty(f.Value)) continue;
object ConvertValue(string val)
{
string type = (col.ColumnType ?? "").ToLower();
if (type == "long" || type == "bigint" || type == "int64")
{
if (long.TryParse(val, out var lv))
return lv;
return null;
}
if (type.Contains("int"))
{
if (int.TryParse(val,out var iv))
return iv;
return null;
}
if (type == "guid" || type == "uniqueidentifier")
{
if (Guid.TryParse(val, out var gv)) return gv;
return null;
}
if (type.Contains("decimal") || type.Contains("numeric") || type.Contains("money"))
{
if (decimal.TryParse(val, out var dv)) return dv;
return null;
}
if (type.Contains("float") || type.Contains("double"))
{
if (double.TryParse(val, out var fv)) return fv;
return null;
}
if (type.Contains("date") || type.Contains("time"))
{
if (DateTime.TryParse(val, out var dt)) return dt;
return null;
}
if (type.Contains("bit") || type.Contains("bool"))
{
if (val == "1" || val.Equals("true", StringComparison.OrdinalIgnoreCase)) return true;
if (val == "0" || val.Equals("false", StringComparison.OrdinalIgnoreCase)) return false;
}
return val;
}
bool isIn = op == "in" || op == "selectlist" || op == "checkbox" || op == "notin";
if (isIn)
{
var arr = f.Value.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(v => v.Trim())
.ToArray();
if (arr.Length == 0) continue;
var paramNames = new List<string>();
foreach (var v in arr)
{
var cv = ConvertValue(v);
if (cv == null) continue;
string name = $"@p{index++}";
string placeholder = name;
paramNames.Add(placeholder);
AddWhereSugarParameter(parameters, name, cv, col);
}
if (paramNames.Count == 0) continue;
string inSql = string.Join(",", paramNames);
whereList.Add($"{LeftQuote}{col.ColumnName}{RightQuote} {(op == "notin" ? "NOT IN" : "IN")} ({inSql})");
continue;
}
// LIKE / 模糊查询统一在这里处理保证三种数据库PgSql/MySql/SqlServer都能正常工作
bool isLikeOp = op == "like" || op == "contains"
|| op == "startwith" || op == "likestart"
|| op == "endwith" || op == "likeend";
if (isLikeOp)
{
string raw = f.Value;
string pattern;
switch (op)
{
case "startwith":
case "likestart":
pattern = raw + "%";
break;
case "endwith":
case "likeend":
pattern = "%" + raw;
break;
case "like":
case "contains":
default:
pattern = "%" + raw + "%";
break;
}
string nameLike = $"p{index++}";
string placeholderLike = $"@{nameLike}";
AddWhereSugarParameter(parameters, placeholderLike, pattern, col);
whereList.Add($"{LeftQuote}{col.ColumnName}{RightQuote} LIKE {placeholderLike}");
continue;
}
var valObj = ConvertValue(f.Value);
if (valObj == null) continue;
string name1 = $"p{index++}";
string placeholder1 = $"@{name1}";
AddWhereSugarParameter(parameters, placeholder1, valObj, col);
switch (op)
{
case "gt":
case ">":
whereList.Add($"{LeftQuote}{col.ColumnName}{RightQuote} > {placeholder1}");
break;
case ">=":
case "thanorequal":
whereList.Add($"{LeftQuote}{col.ColumnName}{RightQuote} >= {placeholder1}");
break;
case "lt":
case "<":
whereList.Add($"{LeftQuote}{col.ColumnName}{RightQuote} < {placeholder1}");
break;
case "<=":
case "lessorequal":
whereList.Add($"{LeftQuote}{col.ColumnName}{RightQuote} <= {placeholder1}");
break;
case "neq":
case "!=":
case "<>":
whereList.Add($"{LeftQuote}{col.ColumnName}{RightQuote} <> {placeholder1}");
break;
case "eq":
case "==":
case "=":
default:
whereList.Add($"{LeftQuote}{col.ColumnName}{RightQuote} = {placeholder1}");
break;
}
}
return provider;
}
/// <summary>
/// 列是否为 Guid / uniqueidentifier 语义
/// </summary>
internal static bool IsGuidColumn(TableColumnField col)
{
if (col == null) return false;
if (col.GetDbType() == System.Data.DbType.Guid) return true;
string t = (col.ColumnType ?? "").Trim().ToLowerInvariant();
return t == "guid" || t == "uniqueidentifier";
}
}
}

View File

@@ -0,0 +1,67 @@
using System;
using System.Linq;
using VolPro.Core.Configuration;
using VolPro.Core.Const;
using VolPro.Core.DBManager;
using VolPro.Core.EFDbContext;
using VolPro.Core.Extensions.AutofacManager;
using VolPro.Core.UserManager;
namespace VolPro.Core.Generic
{
/// <summary>
/// 通用数据库操作工厂,根据 DBServer/dbType 返回具体实现
/// </summary>
public interface IGenericDbProviderFactory
{
IGenericDbProvider GetProvider(string table = null);
IGenericDbProvider Provider { get; }
}
public class GenericDbProviderFactory : IGenericDbProviderFactory, IDependency
{
private readonly IServiceProvider _serviceProvider;
public IGenericDbProvider Provider
{
get
{
return GetProvider();
}
}
public GenericDbProviderFactory(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public IGenericDbProvider GetProvider(string table = null)
{
table ??= GenericTableAsyncLocal.CurrentTableName;
string dbServer = TableColumnContext.TableInfo
.Where(x => x.TableName == table).Select(s => s.DBServer)
.FirstOrDefault() ?? typeof(SysDbContext).Name;
string dbType = DbRelativeCache.GetDbType(dbServer) ?? DBType.Name;
IGenericDbProvider provider = null;
switch (dbType)
{
case "MySql":
provider = new GenericMySqlProvider();
break;
case "PgSql":
provider = new GenericPgSqlProvider();
break;
//case "Oracle":
// provider = new GenericOracleProvider();
// break;
case "MsSql":
provider = new GenericSqlServerProvider();
break;
default:
throw new Exception("数据库暂未支持");
}
return provider;
}
}
}

View File

@@ -0,0 +1,460 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using VolPro.Core.UserManager;
using VolPro.Entity.DomainModels;
namespace VolPro.Core.Generic
{
/// <summary>
/// 通用数据库字段与明细数据校验扩展
/// </summary>
public static class GenericDbValidationExtensions
{
/// <summary>
/// 字段校验:必填、长度、类型
/// </summary>
public static string ValidateColumns<T>(this T provider,
Dictionary<string, object> mainData,
List<TableColumnField> columns)
where T : GenericDbProviderBase
{
foreach (var col in columns)
{
string colName = col.ColumnName;
string displayName = string.IsNullOrEmpty(col.ColumnCnName) ? colName : col.ColumnCnName;
mainData.TryGetValue(colName, out object val);
bool hasValue = val != null && !(val is string s && string.IsNullOrWhiteSpace(s));
if (col.IsKey==1)
{
continue;
}
// 必填校验IsNull 为 0 时字段必填)
if ((col.IsNull ?? 1) == 0 && !hasValue)
{
return $"【{displayName}】不能为空";
}
if (!hasValue)
{
continue;
}
string type = (col.ColumnType ?? "").Trim().ToLower();
// 最大长度校验(只判断字符串类型)
if (type == "string" && (col.Maxlength ?? 0) > 0)
{
string str = val.ToString();
if (col.Maxlength.HasValue && str.Length > col.Maxlength.Value)
{
return $"【{displayName}】长度不能超过{col.Maxlength}个字符";
}
}
// 类型匹配校验
if (!provider.CheckAndConvertValue(mainData, colName, val, type, displayName, out string errorMsg))
{
return errorMsg;
}
}
return null;
}
/// <summary>
/// 处理字段类型转换与校验
/// </summary>
public static bool CheckAndConvertValue<T>(this T provider,
Dictionary<string, object> mainData,
string fieldName,
object val,
string type,
string displayName,
out string error)
where T : GenericDbProviderBase
{
error = null;
if (string.IsNullOrEmpty(type))
{
return true;
}
try
{
object converted = val;
string str = val as string ?? val.ToString();
switch (type)
{
case "int":
case "short":
case "byte":
if (!(val is int || val is short || val is byte))
{
if (!int.TryParse(str, out int iv))
{
error = $"【{displayName}】必须是整数";
return false;
}
converted = iv;
}
break;
case "long":
case "bigint":
if (!(val is long))
{
if (!long.TryParse(str, out long lv))
{
error = $"【{displayName}】必须是整数";
mainData[fieldName] = lv;
return false;
}
converted = lv;
}
break;
case "decimal":
if (!(val is decimal))
{
if (!decimal.TryParse(str, out decimal dv))
{
error = $"【{displayName}】必须是数字";
return false;
}
converted = dv;
}
break;
case "float":
if (!(val is float || val is double))
{
if (!double.TryParse(str, out double fv))
{
error = $"【{displayName}】必须是数字";
mainData[fieldName] = fv;
return false;
}
converted = fv;
}
break;
case "datetime":
case "date":
case "time":
if (!(val is DateTime))
{
if (!DateTime.TryParse(str, out DateTime dt))
{
error = $"【{displayName}】日期格式不正确";
return false;
}
converted = dt;
}
break;
case "bool":
if (!(val is bool))
{
string b = str.ToLower();
if (b == "1" || b == "true" || b == "是")
{
converted = true;
}
else if (b == "0" || b == "false" || b == "否")
{
converted = false;
}
else
{
error = $"【{displayName}】必须是布尔值";
return false;
}
}
break;
case "guid":
if (!(val is Guid))
{
if (!Guid.TryParse(str, out Guid g))
{
error = $"【{displayName}】不是有效的Guid";
return false;
}
converted = g;
}
break;
default:
break;
}
mainData[fieldName] = converted;
return true;
}
catch
{
error = $"【{displayName}】数据格式不正确";
return false;
}
}
/// <summary>
/// 根据表字段配置获取对应的 DbType避免 PgSql 等数据库将参数推断为 text 导致类型不匹配
/// </summary>
public static DbType? GetDbType(this TableColumnField col)
{
if (col?.ColumnType == null) return null;
string type = col.ColumnType.Trim().ToLower();
switch (type)
{
case "int":
case "short":
case "byte":
return DbType.Int32;
case "long":
case "bigint":
return DbType.Int64;
case "string":
return DbType.String;
case "guid":
case "uniqueidentifier":
return DbType.Guid;
case "datetime":
case "date":
case "time":
return DbType.DateTime;
case "decimal":
return DbType.Decimal;
case "float":
case "double":
return DbType.Double;
case "bool":
return DbType.Boolean;
default:
return null;
}
}
/// <summary>
/// 结合列元数据与运行时值确定参数 DbType与 SugarParameter / Dapper 一致)
/// </summary>
internal static DbType? EffectiveDapperDbType(object value, TableColumnField col)
{
if (IsGuidColumn(col) && value is Guid) return DbType.Guid;
var fromCol = col?.GetDbType();
if (fromCol != null) return fromCol;
return InferDbTypeFromClrValue(value);
}
private static DbType? InferDbTypeFromClrValue(object value)
{
if (value == null || value is DBNull) return null;
var tc = Type.GetTypeCode(value.GetType());
switch (tc)
{
case TypeCode.Boolean: return DbType.Boolean;
case TypeCode.Byte: return DbType.Byte;
case TypeCode.Int16: return DbType.Int16;
case TypeCode.Int32: return DbType.Int32;
case TypeCode.Int64: return DbType.Int64;
case TypeCode.Single: return DbType.Single;
case TypeCode.Double: return DbType.Double;
case TypeCode.Decimal: return DbType.Decimal;
case TypeCode.DateTime: return DbType.DateTime;
case TypeCode.String: return DbType.String;
default:
return value is Guid ? DbType.Guid : (DbType?)null;
}
}
/// <summary>
/// 列是否为 Guid / uniqueidentifier 语义
/// </summary>
internal static bool IsGuidColumn(TableColumnField col)
{
if (col == null) return false;
if (col.GetDbType() == DbType.Guid) return true;
string t = (col.ColumnType ?? "").Trim().ToLowerInvariant();
return t == "guid" || t == "uniqueidentifier";
}
/// <summary>
/// 判断值是否等于0用于判断主键是否有意义的值
/// </summary>
public static bool IsZero<T>(this T provider, object value)
where T : GenericDbProviderBase
{
if (value == null) return true;
switch (Type.GetTypeCode(value.GetType()))
{
case TypeCode.Byte:
case TypeCode.Int16:
case TypeCode.Int32:
case TypeCode.Int64:
case TypeCode.Decimal:
case TypeCode.Double:
case TypeCode.Single:
return Convert.ToDecimal(value) == 0;
case TypeCode.String:
if (decimal.TryParse((string)value, out decimal d))
{
return d == 0;
}
break;
}
return false;
}
/// <summary>
/// Add 时,对所有明细表做字段校验,且忽略外键字段(与主表主键同名字段)的必填要求
/// </summary>
public static string ValidateAllDetails<T>(this T provider, SaveModel saveModel, List<TableColumnField> mainTableColumns, TableColumnField mainKeyColumn)
where T : GenericDbProviderBase
{
// 单明细表
if (saveModel.DetailData != null && saveModel.DetailData.Count > 0)
{
string msg = provider.ValidateDetailList(provider.TableInfo.DetailName, saveModel.DetailData, mainKeyColumn);
if (!string.IsNullOrEmpty(msg)) return msg;
}
// 多明细
if (saveModel.Details != null && saveModel.Details.Count > 0)
{
foreach (var item in saveModel.Details)
{
if (item?.Data == null || item.Data.Count == 0) continue;
string msg = provider.ValidateDetailList(item.Table, item.Data, mainKeyColumn);
if (!string.IsNullOrEmpty(msg)) return msg;
}
}
return null;
}
/// <summary>
/// Add 时,校验明细表的所有数据行(忽略外键必填)
/// </summary>
public static string ValidateDetailList<T>(this T provider, string detailTableName, List<Dictionary<string, object>> rows, TableColumnField mainKeyColumn)
where T : GenericDbProviderBase
{
if (string.IsNullOrEmpty(detailTableName) || rows == null || rows.Count == 0) return null;
var detailColumns = TableColumnContext.Data
.Where(x => x.TableName == detailTableName && x.ReferenceField == 0 && x.IsKey != 1)
.ToList();
if (detailColumns == null || detailColumns.Count == 0) return null;
// 找到外键字段:与主表主键同名
var foreignCol = detailColumns.FirstOrDefault(c => c.ColumnName.Equals(mainKeyColumn.ColumnName, StringComparison.OrdinalIgnoreCase));
List<string> ingroCols = new List<string>();
if (foreignCol != null)
{
ingroCols.Add(foreignCol.ColumnName);
}
// 忽略外键必填,把外键字段的 IsNull 视为可空1即不参与必填校验
List<TableColumnField> validateColumns = detailColumns.Where(c => !ingroCols.Contains(c.ColumnName))
.ToList();
for (int i = 0; i < rows.Count; i++)
{
var row = rows[i];
string msg = provider.ValidateColumns(row, validateColumns);
if (!string.IsNullOrEmpty(msg))
{
return "第[{$ts}]行".TranslatorFormat(i + 1) + "," + msg;
}
}
return null;
}
/// <summary>
/// Update 时,对所有明细表做字段校验(只校验提交的字段,并忽略外键必填)
/// </summary>
public static string ValidateAllDetailsForUpdate<T>(this T provider, SaveModel saveModel, TableColumnField mainKeyColumn)
where T : GenericDbProviderBase
{
// 单明细表
if (saveModel.DetailData != null && saveModel.DetailData.Count > 0)
{
string msg = provider.ValidateDetailListForUpdate(provider.TableInfo.DetailName, saveModel.DetailData, mainKeyColumn);
if (!string.IsNullOrEmpty(msg)) return msg;
}
// 多明细
if (saveModel.Details != null && saveModel.Details.Count > 0)
{
foreach (var item in saveModel.Details)
{
if (item?.Data == null || item.Data.Count == 0) continue;
string msg = provider.ValidateDetailListForUpdate(item.Table, item.Data, mainKeyColumn);
if (!string.IsNullOrEmpty(msg)) return msg;
}
}
return null;
}
/// <summary>
/// Update 时,校验某一张明细表的所有数据行(只校验提交的字段,并忽略外键必填)
/// </summary>
public static string ValidateDetailListForUpdate<T>(this T provider, string detailTableName, List<Dictionary<string, object>> rows, TableColumnField mainKeyColumn)
where T : GenericDbProviderBase
{
if (string.IsNullOrEmpty(detailTableName) || rows == null || rows.Count == 0) return null;
var detailColumns = TableColumnContext.Data
.Where(x => x.TableName == detailTableName && x.ReferenceField == 0)
.ToList();
if (detailColumns == null || detailColumns.Count == 0) return null;
// 找到外键字段:与主表主键同名、同类型
var foreignCol = detailColumns.FirstOrDefault(c =>
c.ColumnName.Equals(mainKeyColumn.ColumnName, StringComparison.OrdinalIgnoreCase)
&& string.Equals(c.ColumnType, mainKeyColumn.ColumnType, StringComparison.OrdinalIgnoreCase));
foreach (var row in rows)
{
if (row == null) continue;
// 只校验提交的字段
var submitColumns = detailColumns
.Where(c => row.ContainsKey(c.ColumnName))
.ToList();
if (submitColumns.Count == 0) continue;
// 忽略外键必填:将外键列视为可空
if (foreignCol != null && submitColumns.Any(c => c.ColumnName.Equals(foreignCol.ColumnName, StringComparison.OrdinalIgnoreCase)))
{
submitColumns = submitColumns
.Select(c =>
{
if (!c.ColumnName.Equals(foreignCol.ColumnName, StringComparison.OrdinalIgnoreCase))
{
return c;
}
return new TableColumnField
{
ColumnName = c.ColumnName,
ColumnCnName = c.ColumnCnName,
ColumnType = c.ColumnType,
TableName = c.TableName,
IsDisplay = c.IsDisplay,
ReferenceField = c.ReferenceField,
Maxlength = c.Maxlength,
IsKey = c.IsKey,
IsNull = 1
};
})
.ToList();
}
string msg = provider.ValidateColumns(row, submitColumns);
if (!string.IsNullOrEmpty(msg)) return msg;
}
return null;
}
}
}

View File

@@ -0,0 +1,301 @@
using OfficeOpenXml;
using OfficeOpenXml.Style;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using VolPro.Core.Extensions;
using VolPro.Core.Utilities;
namespace VolPro.Core.Generic
{
/// <summary>
/// 通用导出 Excel 辅助类(参照 EPPlusHelper.Export 的行为,
/// 实现字典数据源转换、字段类型显示、语言翻译等操作),仅返回内存字节,不落地文件。
/// </summary>
internal static class GenericExcelExportHelper
{
public static byte[] BuildExportBytes(
string tableName,
List<Dictionary<string, object>> rows,
string[] exportColumns,
List<string> ignoreColumns)
{
// 1. 列配置CellOptions
var mi = typeof(EPPlusHelper).GetMethod("GetExportColumnInfo",
BindingFlags.NonPublic | BindingFlags.Static);
if (mi == null) return null;
var cellOptions = mi.Invoke(null, new object[] { tableName, false, true, exportColumns }) as List<CellOptions>;
if (cellOptions == null || cellOptions.Count == 0)
return Array.Empty<byte>();
// 2. 最终要导出的列
var exportCols = BuildExportColumns(cellOptions, ignoreColumns);
if (exportCols.Count == 0)
return [];
// 3. 字典列元数据
var dicNoKeys = cellOptions
.Where(x => !string.IsNullOrEmpty(x.DropNo) && x.KeyValues != null && x.KeyValues.Keys.Count > 0)
.Select(x => (x.ColumnName, x.SearchType, x.EditType))
.Distinct()
.ToList();
var multiSelectColumns = dicNoKeys
.Where(x => x.SearchType == "checkbox" || x.SearchType == "selectList" || x.SearchType == "treeSelect"
|| x.EditType == "checkbox" || x.EditType == "selectList" || x.EditType == "treeSelect")
.Select(x => x.ColumnName)
.ToArray();
using var package = new ExcelPackage();
var sheet = package.Workbook.Worksheets.Add("sheet1");
// 4. 表头
WriteHeaderRow(sheet, cellOptions, exportCols);
// 5. 数据行
WriteDataRows(sheet, rows, exportCols, cellOptions, dicNoKeys, multiSelectColumns);
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
// 6. 自动列宽
if (sheet.Dimension != null)
sheet.Cells[sheet.Dimension.Address].AutoFitColumns();
}
using var ms = new MemoryStream();
package.SaveAs(ms);
return ms.ToArray();
}
/// <summary>确定最终导出的列顺序,并应用忽略字段</summary>
private static List<string> BuildExportColumns(List<CellOptions> cellOptions, List<string> ignoreColumns)
{
var cols = cellOptions.Select(c => c.ColumnName).ToList();
if (ignoreColumns != null && ignoreColumns.Count > 0)
{
var ignoreSet = new HashSet<string>(ignoreColumns.Select(x => x.ToLower()));
cols = cols.Where(c => !ignoreSet.Contains(c.ToLower())).ToList();
}
return cols;
}
/// <summary>写表头行(颜色、宽度、翻译与 EPPlusHelper.Export 中模板=false 一致)</summary>
private static void WriteHeaderRow(
ExcelWorksheet sheet,
List<CellOptions> cellOptions,
List<string> exportCols)
{
for (int i = 0; i < exportCols.Count; i++)
{
string colName = exportCols[i];
using (var cell = sheet.Cells[1, i + 1])
{
cell.Style.Fill.PatternType = ExcelFillStyle.Solid;
cell.Style.Fill.BackgroundColor.SetColor(Color.Gray);
cell.Style.Font.Color.SetColor(Color.White);
}
var opt = cellOptions.FirstOrDefault(x => x.ColumnName == colName);
if (opt == null)
{
sheet.Column(i + 1).Width = 15;
sheet.Cells[1, i + 1].Value = colName.Translator();
continue;
}
sheet.Column(i + 1).Width = opt.ColumnWidth / 6.0;
var header = opt.ColumnCNName;
sheet.Cells[1, i + 1].Value = header.Translator();
}
}
/// <summary>写数据行,包括日期格式、字典转换、图片及 long 文本处理</summary>
private static void WriteDataRows(
ExcelWorksheet sheet,
List<Dictionary<string, object>> rows,
List<string> exportCols,
List<CellOptions> cellOptions,
List<(string ColumnName, string SearchType, string EditType)> dicNoKeys,
string[] multiSelectColumns)
{
if (rows == null || rows.Count == 0) return;
const long imageLimitBytes = 10 * 1024 * 1024;
long embeddedBytes = 0;
for (int r = 0; r < rows.Count; r++)
{
var row = rows[r];
for (int c = 0; c < exportCols.Count; c++)
{
string colName = exportCols[c];
row.TryGetValue(colName, out object value);
int? viewType = cellOptions
.Where(x => x.ColumnName == colName)
.Select(x => x.ViewType)
.FirstOrDefault();
// 日期格式
value = FormatDate(value, viewType);
// 字典转换
if (value != null && dicNoKeys.Any(x => x.ColumnName == colName))
value = TranslateDictionary(cellOptions, multiSelectColumns, colName, value);
// 图片
if (viewType == 1 && value != null)
{
embeddedBytes = HandleImageCell(sheet, r, c, value, embeddedBytes, imageLimitBytes);
continue;
}
// long 按文本导出
if (value is long || value is long?)
{
sheet.Cells[r + 2, c + 1].Style.Numberformat.Format = "@";
value = value?.ToString();
}
sheet.Cells[r + 2, c + 1].Value = value;
}
}
}
/// <summary>按 viewType/类型格式化日期</summary>
private static object FormatDate(object value, int? viewType)
{
if (value == null) return null;
// 6year年、5month年月、4date年月日
if (viewType == 6 || viewType == 5 || viewType == 4)
{
if (value is DateTime dt)
{
return viewType switch
{
6 => (object)dt.Year,
5 => dt.ToString("yyyy-MM"),
_ => dt.ToString("yyyy-MM-dd")
};
}
return value;
}
if (value is DateTime dt2)
return dt2.ToString("yyyy-MM-dd HH:mm:sss");
return value;
}
/// <summary>字典数据源转换(多选 / 单选)</summary>
private static object TranslateDictionary(
List<CellOptions> cellOptions,
string[] multiSelectColumns,
string colName,
object value)
{
if (value == null) return null;
if (multiSelectColumns.Contains(colName))
{
return string.Join(",", GetMultiDictValues(cellOptions, colName, value.ToString()));
}
var map = cellOptions
.Where(x => x.ColumnName == colName)
.Select(x => x.KeyValues)
.FirstOrDefault();
if (map == null) return value;
return map.TryGetValue(value.ToString(), out var show) ? (object)show : value;
}
/// <summary>多选字典列“1,2,3” => “名称1,名称2,名称3”</summary>
private static IEnumerable<string> GetMultiDictValues(
List<CellOptions> cellOptions,
string colName,
string raw)
{
var map = cellOptions
.Where(x => x.ColumnName == colName)
.Select(x => x.KeyValues)
.FirstOrDefault();
var parts = raw.Split(',');
if (map == null)
{
foreach (var p in parts) yield return p;
yield break;
}
foreach (var p in parts)
{
yield return map.TryGetValue(p, out var show) ? show : p;
}
}
/// <summary>图片导出:嵌入或超链接</summary>
private static long HandleImageCell(
ExcelWorksheet sheet,
int rowIndex,
int colIndex,
object value,
long embeddedBytes,
long imageLimitBytes)
{
string imgPath = value.ToString();
if (string.IsNullOrWhiteSpace(imgPath))
return embeddedBytes;
bool isHttp = imgPath.StartsWith("http", StringComparison.OrdinalIgnoreCase);
if (!isHttp)
{
imgPath = ("".MapPath(true) + "\\" + imgPath).ReplacePath();
if (!File.Exists(imgPath))
{
sheet.Cells[rowIndex + 2, colIndex + 1].Value = value;
return embeddedBytes;
}
var fi = new FileInfo(imgPath);
long size = fi.Length;
bool canEmbed = embeddedBytes + size <= imageLimitBytes;
if (canEmbed)
{
var pic = sheet.Drawings.AddPicture($"img_{rowIndex}_{colIndex}_{Guid.NewGuid():N}", fi);
pic.SetPosition(rowIndex + 1, 1, colIndex, 1);
pic.SetSize(80, 80);
if (sheet.Row(rowIndex + 2).Height < 60)
sheet.Row(rowIndex + 2).Height = 60;
if (sheet.Column(colIndex + 1).Width < 15)
sheet.Column(colIndex + 1).Width = 15;
sheet.Cells[rowIndex + 2, colIndex + 1].Value = null;
return embeddedBytes + size;
}
var fileUri = new Uri(fi.FullName);
sheet.Cells[rowIndex + 2, colIndex + 1].Hyperlink = fileUri;
sheet.Cells[rowIndex + 2, colIndex + 1].Value = Path.GetFileName(imgPath);
return embeddedBytes;
}
if (Uri.TryCreate(imgPath, UriKind.Absolute, out var uri))
{
sheet.Cells[rowIndex + 2, colIndex + 1].Hyperlink = uri;
sheet.Cells[rowIndex + 2, colIndex + 1].Value = Path.GetFileName(imgPath);
}
return embeddedBytes;
}
}
}

View File

@@ -0,0 +1,210 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using OfficeOpenXml;
using VolPro.Core.Extensions;
using VolPro.Core.UserManager;
using VolPro.Core.Utilities;
namespace VolPro.Core.Generic
{
internal static class GenericExcelImportHelper
{
/// <summary>
/// 从 Excel 流读取数据,按 CellOptions 校验表头和字典,
/// 返回 WebResponseContent成功时 Data 为 List&lt;Dictionary&lt;string,object&gt;&gt;
/// </summary>
public static WebResponseContent ReadRowsByCellOptions(string tableName, Stream excelStream, int importStartRowIndex = 1,
HashSet<string> ignoreFields=null)
{
var response = WebResponseContent.Instance;
var resultRows = new List<Dictionary<string, object>>();
if (excelStream == null || !excelStream.CanRead)
return response.Error("未能读取上传的文件".Translator());
var cellOptions = GetCellOptions(tableName);
if (cellOptions == null || cellOptions.Count == 0)
return response.Error($"未找到表【{tableName}】的导出配置".Translator());
using var package = new ExcelPackage(excelStream);
var sheet = package.Workbook.Worksheets.FirstOrDefault();
if (sheet?.Dimension == null || sheet.Dimension.End.Row <= importStartRowIndex)
return response.Error("导入文件中没有数据".Translator());
if (ignoreFields!=null)
{
cellOptions = cellOptions.Where(x => !ignoreFields.Contains(x.ColumnName)).ToList();
}
if (!BindHeaderIndexes(sheet, cellOptions, importStartRowIndex, out string headerError))
return response.Error(headerError);
var dicNoKeys = cellOptions
.Where(x => !string.IsNullOrEmpty(x.DropNo) && x.KeyValues != null && x.KeyValues.Keys.Count > 0)
.Select(x => (x.ColumnName, x.SearchType, x.EditType))
.Distinct()
.ToList();
var multiSelectColumns = dicNoKeys
.Where(x => x.SearchType == "checkbox" || x.SearchType == "selectList" || x.SearchType == "treeSelect"
|| x.EditType == "checkbox" || x.EditType == "selectList" || x.EditType == "treeSelect")
.Select(x => x.ColumnName)
.ToArray();
// 日期字段类型(用于 Excel 数字日期转换)
var dateFields = TableColumnContext.Data
.Where(x => x.TableName == tableName && (x.ColumnType == "Date" || x.ColumnType == "DateTime"))
.Select(s => s.ColumnName)
.ToHashSet(StringComparer.OrdinalIgnoreCase);
int rowStart = importStartRowIndex + 1;
int rowEnd = sheet.Dimension.End.Row;
int colStart = sheet.Dimension.Start.Column;
int colEnd = sheet.Dimension.End.Column;
for (int r = rowStart; r <= rowEnd; r++)
{
var rowDict = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
bool hasAnyValue = false;
for (int c = colStart; c <= colEnd; c++)
{
var opt = cellOptions.FirstOrDefault(x => x.Index == c);
if (opt == null) continue;
string raw = sheet.Cells[r, c].Value?.ToString();
raw = raw?.Trim();
if (string.IsNullOrEmpty(raw))
{
if (opt.Requierd)
{
string msg = "第[{$ts}]行,[{$ts}]验证未通过,不能为空"
.TranslatorFormat(r, opt.ColumnCNName);
return response.Error(msg);
}
rowDict[opt.ColumnName] = null;
continue;
}
hasAnyValue = true;
// 日期校验与转换(参考 EPPlusHelper.ReadToDataTable 中的日期处理):
// 如果当前字段在 dateFields 中,且值为长度 5 的数字Excel 序列号),则转换成 DateTime。
if (dateFields.Contains(opt.ColumnName)
&& raw.Length == 5
&& int.TryParse(raw, out int days))
{
var dt = new DateTime(1900, 1, 1).AddDays(days - 2);
rowDict[opt.ColumnName] = dt;
continue;
}
if (string.IsNullOrEmpty(opt.DropNo))
{
rowDict[opt.ColumnName] = raw;
continue;
}
if (opt.KeyValues == null)
{
return response.Error("[{$ts}]数据字典缺失".TranslatorFormat(opt.ColumnCNName));
}
string key = null;
if (multiSelectColumns.Contains(opt.ColumnName))
{
var cellValues = raw.Replace("", ",")
.Split(",", StringSplitOptions.RemoveEmptyEntries);
var keys = opt.KeyValues
.Where(x => cellValues.Contains(x.Value))
.Select(s => s.Key)
.ToArray();
if (cellValues.Length == keys.Length)
{
key = string.Join(",", keys);
}
}
else
{
key = opt.KeyValues
.Where(x => x.Value == raw)
.Select(s => s.Key)
.FirstOrDefault();
}
if (key == null)
{
string values = string.Join(",",
opt.KeyValues
.Take(300)
.Select(s => s.Value.Translator()));
string msg = "第[{$ts}]行,[{$ts}]验证未通过,只能填写[{$ts}]"
.TranslatorFormat(r, opt.ColumnCNName, values);
return response.Error(msg);
}
rowDict[opt.ColumnName] = key;
}
if (!hasAnyValue) continue;
resultRows.Add(rowDict);
}
return response.OK(null, resultRows);
}
private static List<CellOptions> GetCellOptions(string tableName)
{
var mi = typeof(EPPlusHelper).GetMethod(
"GetExportColumnInfo",
BindingFlags.NonPublic | BindingFlags.Static);
if (mi == null) return null;
return mi.Invoke(null, new object[] { tableName, false, false, null }) as List<CellOptions>;
}
private static bool BindHeaderIndexes(
ExcelWorksheet sheet,
List<CellOptions> cellOptions,
int headerRow,
out string error)
{
error = null;
for (int j = sheet.Dimension.Start.Column, k = sheet.Dimension.End.Column; j <= k; j++)
{
string columnCNName = sheet.Cells[headerRow, j].Value?.ToString()?.Trim();
if (string.IsNullOrEmpty(columnCNName)) continue;
var options = cellOptions
.FirstOrDefault(x => x.ColumnCNName.Translator() == columnCNName);
if (options == null)
{
error = "[{$ts}]不是模板中的列".TranslatorReplace(columnCNName, true);
return false;
}
if (options.Index > 0)
{
error = "[{$ts}]列名重复".TranslatorReplace(columnCNName, true);
return false;
}
options.Index = j;
}
if (cellOptions.Exists(x => x.Index == 0))
{
var errorOps = cellOptions
.Where(x => x.Index == 0)
.Select(s => s.ColumnCNName.Translator() + "," + s.ColumnName);
error = $"{"".Translator()}:{string.Join("; ", errorOps)}";
return false;
}
return true;
}
}
}

View File

@@ -0,0 +1,71 @@
using OfficeOpenXml;
using OfficeOpenXml.FormulaParsing.Excel.Functions.Math;
using OfficeOpenXml.Style;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Runtime.InteropServices;
using VolPro.Core.UserManager;
using VolPro.Core.Utilities;
namespace VolPro.Core.Generic
{
/// <summary>
/// 通用导入模板 Excel 生成辅助类(基于 TableColumnField 元数据)
/// </summary>
internal static class GenericExcelTemplateHelper
{
/// <summary>
/// 根据表配置生成导入模板 Excel 的二进制内容
/// </summary>
/// <param name="tableDisplayName">表中文名/标题</param>
/// <param name="columns">表字段配置</param>
/// <returns>Excel 文件字节数组</returns>
public static byte[] BuildTemplateBytes(string tableDisplayName, List<TableColumnField> columns)
{
if (columns == null || columns.Count == 0)
{
return [];
}
//仅导出实际表字段
var exportColumns = columns
.Where(c => c.IsDisplay == 1 && c.ReferenceField == 0)
.ToList();
if (exportColumns.Count == 0)
{
return [];
}
using var package = new ExcelPackage();
string sheetName = string.IsNullOrWhiteSpace(tableDisplayName)
? (exportColumns.First().TableName ?? "sheet1")
: tableDisplayName;
var worksheet = package.Workbook.Worksheets.Add(sheetName);
int colIndex = 1;
foreach (var col in exportColumns)
{
worksheet.Cells[1, colIndex].Style.Fill.PatternType = ExcelFillStyle.Solid;
worksheet.Cells[1, colIndex].Style.Fill.BackgroundColor.SetColor(col.IsNull == 0 ? Color.Red : Color.White);
string columnName = col.ColumnName;
worksheet.Cells[1, colIndex].Value = string.IsNullOrWhiteSpace(col.ColumnCnName)
? columnName
: col.ColumnCnName.Translator();
colIndex++;
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
// 自动列宽
if (worksheet.Dimension != null)
{
worksheet.Cells[worksheet.Dimension.Address].AutoFitColumns();
}
}
return package.GetAsByteArray();
}
}
}

View File

@@ -0,0 +1,26 @@
using Dapper;
using VolPro.Core.EFDbContext;
using VolPro.Core.UserManager;
using VolPro.Entity.DomainModels;
namespace VolPro.Core.Generic
{
/// <summary>
/// MySql 通用 CRUD 实现
/// </summary>
public class GenericMySqlProvider : GenericDbProviderBase
{
protected override string LeftQuote => "`";
protected override string RightQuote => "`";
public GenericMySqlProvider() : base()
{
}
protected override string BuildIdentitySql(TableColumnField keyColumn, bool batch = false)
{
// 单条插入和批量插入统一使用 LAST_INSERT_ID()
// 批量场景下返回的是本次批量的起始自增值,上层根据行数自行推算每条的 Id
return $" ;SELECT LAST_INSERT_ID();";
}
}
}

View File

@@ -0,0 +1,49 @@
using SqlSugar;
using System.Collections.Generic;
using System.Threading.Tasks;
using VolPro.Core.EFDbContext;
using VolPro.Core.Extensions.AutofacManager;
using VolPro.Core.UserManager;
using VolPro.Entity.DomainModels;
namespace VolPro.Core.Generic
{
/// <summary>
/// Oracle 通用 CRUD 实现
/// </summary>
public class GenericOracleProvider : GenericDbProviderBase
{
protected override string LeftQuote => "\"";
protected override string RightQuote => "\"";
public GenericOracleProvider() : base()
{
}
/// <summary>
/// Oracle 12c+ 使用 RETURNING col INTO :outParam 获取自增/序列主键
/// 批量插入时 Oracle 需 BULK COLLECT暂不支持返回多行 id返回 null
/// </summary>
protected override string BuildIdentitySql(TableColumnField keyColumn, bool batch = false)
{
if (batch)
{
return null;
}
return $"RETURNING {LeftQuote}{keyColumn.ColumnName}{RightQuote} INTO :newId";
}
/// <summary>
/// Oracle 使用输出参数获取 RETURNING 值,需 Execute 后读取参数
/// </summary>
protected override async Task<object> ExecuteInsertWithIdentityAsync(string insertSql, string identitySql, List<SugarParameter> parameters, TableColumnField keyColumn)
{
// SqlSugar 输出参数new SugarParameter(name, null, true)
var outParam = new SugarParameter(":newId", null, true);
parameters.Add(outParam);
await ExcuteNonQueryAsync($"{insertSql} {identitySql}", parameters);
return outParam.Value;
}
}
}

View File

@@ -0,0 +1,27 @@
using Dapper;
using VolPro.Core.EFDbContext;
using VolPro.Core.Extensions.AutofacManager;
using VolPro.Core.UserManager;
using VolPro.Entity.DomainModels;
namespace VolPro.Core.Generic
{
/// <summary>
/// PostgreSql 通用 CRUD 实现
/// </summary>
public class GenericPgSqlProvider : GenericDbProviderBase
{
protected override string LeftQuote => "\"";
protected override string RightQuote => "\"";
public GenericPgSqlProvider() : base()
{
}
protected override string BuildIdentitySql(TableColumnField keyColumn, bool batch = false)
{
// PostgreSQL 单条与批量均使用 RETURNING 语法
return $" RETURNING {LeftQuote}{keyColumn.ColumnName}{RightQuote}";
}
}
}

View File

@@ -0,0 +1,57 @@
using Dapper;
using VolPro.Core.Configuration;
using VolPro.Core.EFDbContext;
using VolPro.Core.Extensions.AutofacManager;
using VolPro.Core.UserManager;
using VolPro.Entity.DomainModels;
namespace VolPro.Core.Generic
{
public class GenericSqlServerProvider : GenericDbProviderBase
{
protected override string LeftQuote => "[";
protected override string RightQuote => "]";
public GenericSqlServerProvider() : base()
{
}
protected override string BuildPageSql(string baseWithWhereSql, string selectColumns, string orderBy, int page, int rows)
{
int offset = (page - 1) * rows;
int end = page * rows;
if (AppSetting.UseSqlserver2008)
{
return $@"
SELECT {selectColumns}
FROM (
SELECT ROW_NUMBER() OVER ({orderBy}) AS RowNum, {selectColumns}
FROM (
{baseWithWhereSql}
) AS S
) AS X
WHERE X.RowNum BETWEEN {offset + 1} AND {end}
ORDER BY X.RowNum";
}
return $@"
SELECT {selectColumns}
FROM (
{baseWithWhereSql}
) AS S
{orderBy}
OFFSET {offset} ROWS FETCH NEXT {rows} ROWS ONLY";
}
protected override string BuildIdentitySql(TableColumnField keyColumn, bool batch = false)
{
// 单条插入:使用 SCOPE_IDENTITY()
if (!batch)
{
return $";SELECT SCOPE_IDENTITY();";
}
// 批量插入:使用 OUTPUT INSERTED.[Id],由上层拼接在 VALUES 之前
return $" OUTPUT INSERTED.{LeftQuote}{keyColumn.ColumnName}{RightQuote}";
}
}
}

View File

@@ -0,0 +1,28 @@
using System.Threading;
namespace VolPro.Core.Generic
{
public static class GenericTableAsyncLocal
{
private static readonly AsyncLocal<string> _currentTableName = new AsyncLocal<string>();
/// <summary>
/// 当前请求对应的表名
/// </summary>
public static string CurrentTableName
{
get => _currentTableName.Value;
set {
if (_currentTableName.Value==null)
{
_currentTableName.Value = value;
}
}
}
public static void Clear()
{
_currentTableName.Value = null;
}
}
}

View File

@@ -0,0 +1,82 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Threading.Tasks;
using VolPro.Core.EFDbContext;
using VolPro.Core.Enums;
using VolPro.Core.Filters;
using VolPro.Core.Middleware;
using VolPro.Core.UserManager;
using VolPro.Core.Utilities;
using VolPro.Entity.DomainModels;
namespace VolPro.Core.Generic
{
/// <summary>
/// 不同数据库类型的通用 CRUD 提供接口
/// </summary>
public interface IGenericDbProvider
{
/// <summary>
/// 查询
/// </summary>
/// <param name="options"></param>
/// <param name="isDetail"></param>
/// <returns></returns>
Task<PageGridData<object>> GetPageDataAsync(PageDataOptions options, bool isDetail = false);
/// <summary>
/// 明细查询
/// </summary>
/// <param name="options"></param>
/// <returns></returns>
Task<PageGridData<object>> GetDetailPageAsync(PageDataOptions options);
/// <summary>
/// 添加
/// </summary>
/// <param name="saveModel"></param>
/// <returns></returns>
Task<WebResponseContent> AddAsync(SaveModel saveModel);
/// <summary>
/// 编辑
/// </summary>
/// <param name="saveModel"></param>
/// <returns></returns>
Task<WebResponseContent> UpdateAsync(SaveModel saveModel);
/// <summary>
/// 删除
/// </summary>
/// <param name="keys"></param>
/// <param name="delDetail"></param>
/// <returns></returns>
Task<WebResponseContent> DelAsync(List<object> keys, bool delDetail = true);
/// <summary>
/// 上传
/// </summary>
/// <param name="files"></param>
/// <returns></returns>
Task<WebResponseContent> UploadAsync(List<IFormFile> files);
/// <summary>
/// 下载导入Excel模板
/// </summary>
/// <returns></returns>
byte[] DownLoadTemplateAsync();
/// <summary>
/// 导入表数据Excel
/// </summary>
/// <param name="fileInput"></param>
/// <returns></returns>
Task<WebResponseContent> ImportAsync(List<IFormFile> fileInput);
/// <summary>
/// 导出文件
/// </summary>
/// <param name="loadData"></param>
/// <returns></returns>
Task<byte[]> ExportAsync(PageDataOptions loadData);
Task<WebResponseContent> AuditAsync(object[] id, int? auditStatus, string auditReason);
}
}