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 { /// /// 通用数据库 CRUD 抽象基类,封装三种数据库公共逻辑 /// public abstract class GenericDbProviderBase : GenericBaseService, IGenericDbProvider { /// /// 不同数据库的左右引号符号(MySql: ``, PgSql: "", SqlServer: []) /// protected abstract string LeftQuote { get; } protected abstract string RightQuote { get; } protected GenericDbProviderBase() : base() { } public virtual async Task> 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>(); } //获取逻辑删除过滤 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(); // SqlSugar 参数集合 var parameters = new List(); //执行自定义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() { rows = await QueryListAsync(pageSql, parameters), total = options.Export ? 0 : (await ExecuteScalarAsync(countSql, parameters)).GetInt() }; } public virtual async Task> GetDetailPageAsync(PageDataOptions options) { return await GetPageDataAsync(options, true); } /// /// 构造分页 SQL,默认使用 LIMIT/OFFSET(MySql、PgSql) /// 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}"; } /// /// 添加数据Add /// /// /// public virtual async Task 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(); var paramNames = new List(); var parameters = new List(); // 只写入 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}"); } } /// /// 编辑 /// /// /// public virtual async Task 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(); var parameters = new List(); // 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 DelAsync(List 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 Del(List 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(); 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 UploadAsync(List 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); } /// /// 下载导入Excel模板(基于当前表配置动态生成) /// /// Excel 文件字节数组 public byte[] DownLoadTemplateAsync() { var ignoreFields = this.GetAddAndModifyFieldsToIgnore(); var bytes = GenericExcelTemplateHelper.BuildTemplateBytes(TableInfo.ColumnCNName, TableColumns.Where(x => !ignoreFields.Contains(x.ColumnName)).ToList()); return bytes; } /// /// 导入表数据Excel /// /// /// public async Task ImportAsync(List 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> rows = null; using (var stream = formFile.OpenReadStream()) { var resp = GenericExcelImportHelper.ReadRowsByCellOptions(TableInfo.TableName, stream, ignoreFields: ignoreFields); if (!resp.Status) return resp; rows = ((List>)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 }); } /// /// 导出文件(参照 ServiceBase.Export,实现字典数据源转换、字段显示等) /// /// /// Excel 文件字节数组 public async Task ExportAsync(PageDataOptions loadData) { loadData.Export = true; var pageData = await GetPageDataAsync(loadData); var dictRows = new List>(); foreach (var item in pageData.rows) { var entry = new Dictionary(StringComparer.OrdinalIgnoreCase); if (item is IDictionary genericDict) { foreach (var kv in genericDict) { entry[kv.Key] = kv.Value; } dictRows.Add(entry); } } var exportFields = loadData.Columns ?? []; var ignoreColumns = new List(); var bytes = GenericExcelExportHelper.BuildExportBytes(TableInfo.TableName, dictRows, loadData.Columns ?? [], ignoreColumns); return bytes; } public async Task AuditAsync(object[] id, int? auditStatus, string auditReason) { string table = TableInfo.TableName; await Task.CompletedTask; return null; } /// protected virtual string BuildIdentitySql(TableColumnField keyColumn, bool batch = false) { return null; } protected virtual async Task ExecuteInsertWithIdentityAsync(string insertSql, string identitySql, List parameters, TableColumnField keyColumn) { return await ExecuteScalarAsync($"{insertSql} {identitySql}", parameters); } /// /// 插入一对多明细数据 /// protected virtual async Task InsertDetailsAsync(SaveModel saveModel, TableColumnField mainKeyColumn) { // 主键值 object mainKeyValue = saveModel.MainData[mainKeyColumn.ColumnName]; // 单明细表 List> if (saveModel.DetailData != null && saveModel.DetailData.Count > 0) { await InsertDetailListAsync(TableInfo.DetailName, saveModel.DetailData, mainKeyColumn, mainKeyValue); saveModel.MainData[TableInfo.DetailName] = saveModel.DetailData; } // 新结构:多明细 List 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; } } } /// /// Update 时,根据明细主键是否有值区分新建/编辑,分别执行插入或更新 /// private async Task 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(); } /// /// 对指定明细表的数据做“有主键则更新,无主键则插入”的操作 /// private async Task UpsertDetailListAsync(string detailTableName, List> 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>(); // 编辑明细行批量 UPDATE,控制单次参数数量 int maxParams = DbParamsCount; var updateSqlBuilder = new StringBuilder(); var updateParameters = new List(); int currentUpdateParamCount = 0; int updateRowIndex = 0; async Task FlushUpdateAsync() { if (updateSqlBuilder.Length == 0) return; await ExcuteNonQueryAsync(updateSqlBuilder.ToString(), updateParameters); updateSqlBuilder.Clear(); updateParameters = new List(); 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(); 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(); } /// /// 按规则插入某一张明细表的数据列表(批量 SQL,控制单次参数数量) /// private async Task InsertDetailListAsync(string detailTableName, List> 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(); var batchRows = new List>(); var batchRowIndexes = new List(); int currentParamCount = 0; int globalRowIndex = 0; async Task FlushAsync() { if (batchRows.Count == 0) return; var valuesClauses = new List(); 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>>() .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(); 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(); } /// /// SqlServer / PostgreSQL:Guid 列须把 string、JsonElement 转为 Guid。MySql 等保持原样。 /// PostgreSQL:元数据为 varchar(Text) 且值为 Guid 时转为字符串以绑 Text。 /// 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; } } }