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)";
}
}