using System.ComponentModel; using System.Reflection; using FreeSql.DataAnnotations; using F9MES.Api.Common; using F9MES.Common.Result; using F9MES.Common.Utils; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace F9MES.Api.Controllers; /// /// 元数据控制器:提供表结构信息,前端据此自动生成表格列与表单控件 /// 路由: /// GET api/meta/table/{table} 表字段元数据(含枚举选项/关联/编号标记) /// GET api/meta/refs/{table} 关联表下拉数据([{value,label}]) /// POST api/meta/gencode/{table}/{field} 业务编号自动生成 /// [ApiController] [Route("api/meta")] [Authorize] public class MetaController : ControllerBase { /// 系统自动维护字段(表单不展示、不可编辑) private static readonly HashSet SystemFields = new(StringComparer.OrdinalIgnoreCase) { "Flag", "AddTime", "Adder", "UpdateTime", "Updater", // 批次来源入库单ID:由入库联动自动维护,前端不展示 "InStockId" }; /// /// 关联字段映射:字段名 → (关联表, 主显示字段, 过滤字段, 过滤值) /// 过滤字段用于同表多类型下拉(如 BaseCommon_Partner 按 PartnerType 区分客户/供应商) /// 前端根据 *Id 字段自动加载关联下拉 /// private static readonly Dictionary RefMap = new(StringComparer.OrdinalIgnoreCase) { ["ZhuangkouId"] = ("RawMaterial_Zhuangkou", "Code", null, null), ["ProcessZhuangkouId"] = ("Process_ProcessZhuangkou", "Code", null, null), ["SupplierId"] = ("BaseCommon_Partner", "Name", "PartnerType", 1), ["CustomerId"] = ("BaseCommon_Partner", "Name", "PartnerType", 0), ["SpecId"] = ("BaseCommon_SilkSpec", "Name", null, null), ["MaterialId"] = ("BaseCommon_Material", "Name", null, null), ["TeamId"] = ("ProXuan_Team", "Name", null, null), ["WorkshopId"] = ("ProQian_Workshop", "Name", null, null), ["MachineId"] = ("ProQian_Machine", "MachineNo", null, null), ["EmployeeId"] = ("HRS_Employee", "Name", null, null), ["BatchId"] = ("Fims_Batch", "BatchNo", null, null), ["GradeResultId"] = ("Lims_GradeResult", "GradeNo", null, null), ["ContractId"] = ("Sams_Contract", "Code", null, null), ["SheetId"] = ("Process_Sheet", "SheetNo", null, null), ["SourceSheetId"] = ("Process_Sheet", "SheetNo", null, null), ["OutOrgId"] = ("BaseSys_Org", "Name", null, null), ["AbnormalId"] = ("Lims_Abnormal", "AbnormalNo", null, null), ["CheckId"] = ("Lims_Check", "CheckNo", null, null), ["PackageId"] = ("Fims_Package", "PackageNo", null, null), ["GradeId"] = ("BaseCommon_GradeStandard", "Name", null, null), ["RepackId"] = ("RawMaterial_Repack", "PlanNo", null, null), ["PurchaseId"] = ("RawMaterial_Purchase", "BillNo", null, null), ["WarehouseId"] = ("RawMaterial_Warehouse", "Name", null, null) }; /// /// 业务编号生成规则:表名 → (编号字段, 前缀, 日期格式, 流水位数, 前缀-日期分隔符, 日期-流水分隔符) /// private static readonly Dictionary CodeRules = new(StringComparer.OrdinalIgnoreCase) { ["RawMaterial_Zhuangkou"] = ("Code", "YLZ", "yyyyMMdd", 3, "", "-"), ["RawMaterial_InStock"] = ("BillNo", "YLR", "yyyyMMdd", 4, "", "-"), ["RawMaterial_Repack"] = ("PlanNo", "RZ", "yyyyMMdd", 3, "", "-"), ["RawMaterial_OutStock"] = ("BillNo", "YLC", "yyyyMMdd", 4, "", "-"), ["RawMaterial_Purchase"] = ("BillNo", "CG", "yyyyMMdd", 4, "", "-"), ["RawMaterial_Warehouse"] = ("Code", "CK", "yyyyMMdd", 3, "", "-"), ["RawMaterial_Inspect"] = ("Code", "YJ", "yyyyMMdd", 3, "", "-"), ["Process_Trial"] = ("BillNo", "SY", "yyyyMMdd", 3, "", "-"), ["Process_ProcessZhuangkou"] = ("Code", "GYZ", "yyyy", 3, "-", "-"), ["Process_Sheet"] = ("SheetNo", "GYD", "yyyyMMdd", 3, "", "-"), ["Lims_SpoolCheck"] = ("CheckNo", "JH", "yyyyMMdd", 3, "", "-"), ["Lims_BlackBoard"] = ("CheckNo", "HB", "yyyyMMdd", 3, "", "-"), ["Lims_Denier"] = ("CheckNo", "XD", "yyyyMMdd", 3, "", "-"), ["Lims_Moisture"] = ("CheckNo", "GL", "yyyyMMdd", 3, "", "-"), ["Lims_GradeResult"] = ("GradeNo", "FJ", "yyyyMMdd", 3, "", "-"), ["Lims_Abnormal"] = ("AbnormalNo", "YC", "yyyyMMdd", 3, "", "-"), ["Fims_Batch"] = ("BatchNo", "CP", "yyyyMMdd", 3, "", "-"), ["Fims_Package"] = ("PackageNo", "BZ", "yyyyMMdd", 3, "", "-"), ["Fims_InStock"] = ("BillNo", "CPR", "yyyyMMdd", 4, "", "-"), ["Fims_OutStock"] = ("BillNo", "CPC", "yyyyMMdd", 4, "", "-") }; private readonly IFreeSql _db; private readonly OrderNoGenerator _gen; public MetaController(IFreeSql db, OrderNoGenerator gen) { _db = db; _gen = gen; } /// 获取指定表的字段元数据 [HttpGet("table/{table}")] public ApiResult Table(string table) { if (!EntityCatalog.TryGet(table, out var type)) return ApiResult.Error($"表 {table} 不存在"); CodeRules.TryGetValue(table, out var codeRule); var fields = type.GetProperties(BindingFlags.Public | BindingFlags.Instance) .Where(p => p.GetIndexParameters().Length == 0) .OrderBy(p => p.MetadataToken) .Select(p => { var col = p.GetCustomAttribute(); var desc = p.GetCustomAttribute()?.Description ?? p.GetCustomAttribute()?.DisplayName ?? p.Name; var isPrimary = col?.IsPrimary == true; RefMap.TryGetValue(p.Name, out var refInfo); return new { name = p.Name, label = desc, dbType = col?.DbType ?? DbTypeName(p.PropertyType), isIdentity = col?.IsIdentity == true, isPrimary, isSystem = SystemFields.Contains(p.Name), propType = Nullable.GetUnderlyingType(p.PropertyType)?.Name ?? p.PropertyType.Name, // 智能配置 options = ParseOptions(desc), // 枚举选项 [{value,label}] refTable = refInfo.Table, // 关联表名 refLabel = refInfo.Label, // 关联显示字段 refFilterField = refInfo.FilterField, // 关联过滤字段(同表多类型下拉) refFilterValue = refInfo.FilterValue, // 关联过滤值 isCode = codeRule.Field != null && codeRule.Field.Equals(p.Name, StringComparison.OrdinalIgnoreCase) // 业务编号(新增时自动生成) }; }).ToList(); return ApiResult.Ok(new { table, title = type.GetCustomAttribute()?.Description ?? table, fields }); } /// 关联表下拉数据:[{value,label}],label 取显示字段(无则用 Code/Name) /// 支持 filterField/filterValue:同表多类型下拉过滤(如供应商=PartnerType=1、客户=PartnerType=0) [HttpGet("refs/{table}")] public async Task Refs(string table, string? keyword = null, int size = 500, string? filterField = null, string? filterValue = null) { if (!EntityCatalog.TryGet(table, out var type)) return ApiResult.Error($"表 {table} 不存在"); var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance) .Where(p => p.GetIndexParameters().Length == 0).ToList(); // 优先使用 RefMap 中配置的显示字段(如 MachineNo/BatchNo),其次 Name/Code/首个字符串字段 var refEntry = RefMap.Values.FirstOrDefault(r => r.Table.Equals(table, StringComparison.OrdinalIgnoreCase)); var labelProp = props.FirstOrDefault(p => refEntry.Label != null && p.Name.Equals(refEntry.Label, StringComparison.OrdinalIgnoreCase)) ?? props.FirstOrDefault(p => p.Name.Equals("Name", StringComparison.OrdinalIgnoreCase)) ?? props.FirstOrDefault(p => p.Name.Equals("Code", StringComparison.OrdinalIgnoreCase)) ?? props.FirstOrDefault(p => p.PropertyType == typeof(string)); var codeProp = props.FirstOrDefault(p => p.Name.Equals("Code", StringComparison.OrdinalIgnoreCase)); var nameProp = props.FirstOrDefault(p => p.Name.Equals("Name", StringComparison.OrdinalIgnoreCase)); if (labelProp == null) return ApiResult.Error($"表 {table} 无可展示字段"); var cols = new HashSet { "Id", labelProp.Name }; if (codeProp != null) cols.Add(codeProp.Name); if (nameProp != null) cols.Add(nameProp.Name); var sql = $"SELECT `{string.Join("`, `", cols)}` FROM `{table}` WHERE Flag > 0"; // 同表多类型过滤(字段名白名单校验,防注入) if (!string.IsNullOrWhiteSpace(filterField) && props.Any(p => p.Name.Equals(filterField, StringComparison.OrdinalIgnoreCase))) { var fv = (filterValue ?? "").Replace("'", "''"); sql += long.TryParse(fv, out var fnum) ? $" AND `{filterField}` = {fnum}" : $" AND `{filterField}` = '{fv}'"; } if (!string.IsNullOrWhiteSpace(keyword)) { var kw = keyword.Replace("'", "''"); sql += codeProp != null && codeProp.Name != labelProp.Name ? $" AND (`{labelProp.Name}` LIKE '%{kw}%' OR `{codeProp.Name}` LIKE '%{kw}%')" : $" AND `{labelProp.Name}` LIKE '%{kw}%'"; } sql += $" ORDER BY Id DESC LIMIT {Math.Clamp(size, 1, 2000)}"; var rows = await _db.Ado.QueryAsync>(sql); var items = rows.Select(r => { var getVal = (string col) => r.TryGetValue(col, out var v) ? v?.ToString() ?? "" : ""; // 首选显示字段值(RefMap 指定 / Name / Code / 首个字符串字段) var label = getVal(labelProp.Name); // label 为空时用 Code 兜底 if (label == "" && codeProp != null && codeProp.Name != labelProp.Name) label = getVal(codeProp.Name); // 追加名称字段(如 编号 + 名称),提升可辨识度 if (nameProp != null && nameProp.Name != labelProp.Name) { var nm = getVal(nameProp.Name); if (nm != "" && nm != label) label = $"{label} {nm}".Trim(); } return new { value = Convert.ToInt64(r["Id"]), label }; }).ToList(); return ApiResult.Ok(items); } /// 业务编号自动生成(如 YLZ20260815-001) [HttpPost("gencode/{table}/{field}")] public ApiResult Gencode(string table, string field) { if (!CodeRules.TryGetValue(table, out var rule)) return ApiResult.Error($"表 {table} 未配置自动编号规则"); if (!rule.Field.Equals(field, StringComparison.OrdinalIgnoreCase)) return ApiResult.Error($"字段 {field} 不是 {table} 的编号字段({rule.Field})"); var no = _gen.Generate2(rule.Prefix, rule.DatePart, rule.SeqLen, rule.SepDate, rule.SepSeq); return ApiResult.Ok(new { value = no }); } /// 从字段描述解析枚举选项:如 "状态:0=收茧中 1=已入库 2=翻包中" private static List>? ParseOptions(string desc) { if (string.IsNullOrWhiteSpace(desc)) return null; var idx = desc.IndexOfAny(new[] { ':', ':' }); if (idx < 0 || idx == desc.Length - 1) return null; var body = desc[(idx + 1)..].Trim(); if (body.Length == 0) return null; var parts = body.Split(new[] { ' ', ';', ';', '\n', '\t', '\r' }, StringSplitOptions.RemoveEmptyEntries); if (parts.Length < 2) return null; var list = new List>(); foreach (var part in parts) { var eq = part.IndexOf('='); if (eq <= 0 || eq == part.Length - 1) return null; if (!int.TryParse(part[..eq].Trim(), out var v)) return null; list.Add(new Dictionary { ["value"] = v, ["label"] = part[(eq + 1)..].Trim() }); } return list.Count >= 2 ? list : null; } /// C# 类型 → 数据库类型名(仅用于前端展示,非建表依据) private static string DbTypeName(Type t) { var ut = Nullable.GetUnderlyingType(t) ?? t; if (ut == typeof(string)) return "varchar(255)"; if (ut == typeof(int) || ut == typeof(short) || ut == typeof(byte)) return "int"; if (ut == typeof(long)) return "bigint"; if (ut == typeof(decimal)) return "decimal(18,2)"; if (ut == typeof(double) || ut == typeof(float)) return "decimal(18,4)"; if (ut == typeof(DateTime)) return "datetime"; if (ut == typeof(bool)) return "tinyint(1)"; if (ut.IsEnum) return "int"; return "varchar(255)"; } }