262 lines
14 KiB
C#
262 lines
14 KiB
C#
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;
|
||
|
||
/// <summary>
|
||
/// 元数据控制器:提供表结构信息,前端据此自动生成表格列与表单控件
|
||
/// 路由:
|
||
/// GET api/meta/table/{table} 表字段元数据(含枚举选项/关联/编号标记)
|
||
/// GET api/meta/refs/{table} 关联表下拉数据([{value,label}])
|
||
/// POST api/meta/gencode/{table}/{field} 业务编号自动生成
|
||
/// </summary>
|
||
[ApiController]
|
||
[Route("api/meta")]
|
||
[Authorize]
|
||
public class MetaController : ControllerBase
|
||
{
|
||
/// <summary>系统自动维护字段(表单不展示、不可编辑)</summary>
|
||
private static readonly HashSet<string> SystemFields = new(StringComparer.OrdinalIgnoreCase)
|
||
{
|
||
"Flag", "AddTime", "Adder", "UpdateTime", "Updater",
|
||
// 批次来源入库单ID:由入库联动自动维护,前端不展示
|
||
"InStockId"
|
||
};
|
||
|
||
/// <summary>
|
||
/// 关联字段映射:字段名 → (关联表, 主显示字段, 过滤字段, 过滤值)
|
||
/// 过滤字段用于同表多类型下拉(如 BaseCommon_Partner 按 PartnerType 区分客户/供应商)
|
||
/// 前端根据 *Id 字段自动加载关联下拉
|
||
/// </summary>
|
||
private static readonly Dictionary<string, (string Table, string Label, string? FilterField, object? FilterValue)> 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)
|
||
};
|
||
|
||
/// <summary>
|
||
/// 业务编号生成规则:表名 → (编号字段, 前缀, 日期格式, 流水位数, 前缀-日期分隔符, 日期-流水分隔符)
|
||
/// </summary>
|
||
private static readonly Dictionary<string, (string Field, string Prefix, string DatePart, int SeqLen, string SepDate, string SepSeq)> 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;
|
||
}
|
||
|
||
/// <summary>获取指定表的字段元数据</summary>
|
||
[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<ColumnAttribute>();
|
||
var desc = p.GetCustomAttribute<DescriptionAttribute>()?.Description
|
||
?? p.GetCustomAttribute<DisplayNameAttribute>()?.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<DescriptionAttribute>()?.Description ?? table, fields });
|
||
}
|
||
|
||
/// <summary>关联表下拉数据:[{value,label}],label 取显示字段(无则用 Code/Name)
|
||
/// 支持 filterField/filterValue:同表多类型下拉过滤(如供应商=PartnerType=1、客户=PartnerType=0)</summary>
|
||
[HttpGet("refs/{table}")]
|
||
public async Task<ApiResult> 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<string> { "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<Dictionary<string, object>>(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);
|
||
}
|
||
|
||
/// <summary>业务编号自动生成(如 YLZ20260815-001)</summary>
|
||
[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 });
|
||
}
|
||
|
||
/// <summary>从字段描述解析枚举选项:如 "状态:0=收茧中 1=已入库 2=翻包中"</summary>
|
||
private static List<Dictionary<string, object>>? 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<Dictionary<string, object>>();
|
||
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<string, object> { ["value"] = v, ["label"] = part[(eq + 1)..].Trim() });
|
||
}
|
||
return list.Count >= 2 ? list : null;
|
||
}
|
||
|
||
/// <summary>C# 类型 → 数据库类型名(仅用于前端展示,非建表依据)</summary>
|
||
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)";
|
||
}
|
||
}
|