Files
F9Web/server/src/F9MES.Api/Controllers/MetaController.cs
T

262 lines
14 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)";
}
}