init: F9智慧缫丝系统 - 后端(server) + 主前端(web) + 打印设计器(openprint) 首次提交

This commit is contained in:
2026-08-15 15:37:25 +08:00
commit 7235265749
294 changed files with 59182 additions and 0 deletions
@@ -0,0 +1,66 @@
using F9MES.Application.Auth;
using F9MES.Common.Auth;
using F9MES.Common.Result;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace F9MES.Api.Controllers;
/// <summary>
/// 认证控制器:登录、当前用户信息、修改密码
/// </summary>
[ApiController]
[Route("api/auth")]
public class AuthController : ControllerBase
{
private readonly AuthService _auth;
private readonly CurrentUserService _currentUser;
private readonly MenuService _menuService;
public AuthController(AuthService auth, CurrentUserService currentUser, MenuService menuService)
{
_auth = auth;
_currentUser = currentUser;
_menuService = menuService;
}
/// <summary>登录(手机号+密码)</summary>
[HttpPost("login")]
[AllowAnonymous]
public async Task<ApiResult> Login([FromBody] LoginDto dto)
{
return await _auth.LoginAsync(dto.Phone ?? "", dto.Password ?? "");
}
/// <summary>当前用户信息 + 菜单树</summary>
[HttpGet("me")]
public async Task<ApiResult> Me()
{
var user = _currentUser.User;
if (user == null) return ApiResult.Error("未登录", 401);
var menus = await _menuService.GetMenuTreeAsync(user);
return ApiResult.Ok(new { user, menus });
}
/// <summary>修改密码</summary>
[HttpPost("changePassword")]
public async Task<ApiResult> ChangePassword([FromBody] ChangePasswordDto dto)
{
return await _auth.ChangePasswordAsync(_currentUser.UserId, dto.OldPassword ?? "", dto.NewPassword ?? "");
}
}
/// <summary>登录参数</summary>
public class LoginDto
{
public string? Phone { get; set; }
public string? Password { get; set; }
}
/// <summary>修改密码参数</summary>
public class ChangePasswordDto
{
public string? OldPassword { get; set; }
public string? NewPassword { get; set; }
}
@@ -0,0 +1,145 @@
using FreeSql;
using F9MES.Application.Auth;
using F9MES.Common.Result;
using F9MES.Domain.BaseCommon;
using F9MES.Domain.BaseSys;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace F9MES.Api.Controllers;
/// <summary>
/// 系统管理控制器:组织树/字典/用户管理
/// </summary>
[ApiController]
[Route("api/basesys")]
[Authorize]
public class BaseSysController : ControllerBase
{
private readonly IFreeSql _db;
private readonly AuthService _auth;
public BaseSysController(IFreeSql db, AuthService auth)
{
_db = db;
_auth = auth;
}
/// <summary>组织架构树</summary>
[HttpGet("org/tree")]
public async Task<ApiResult> OrgTree()
{
var orgs = await _db.Queryable<BaseSys_Org>()
.Where(a => a.Flag > 0 && a.IsEnable == 1)
.OrderBy(a => a.Sort).ToListAsync();
return ApiResult.Ok(BuildOrgTree(orgs, 0));
}
private static object BuildOrgTree(List<BaseSys_Org> all, long parentId)
{
return all.Where(o => o.ParentId == parentId).Select(o => new
{
o.Id, o.Name, o.Code, o.OrgType, o.Manager, o.Phone,
children = BuildOrgTree(all, o.Id)
}).ToList();
}
/// <summary>查询字典项(按字典类型编码)</summary>
[HttpGet("dict/{typeCode}")]
public async Task<ApiResult> GetDict(string typeCode)
{
var list = await _db.Queryable<BaseCommon_Dict>()
.Where(a => a.Flag > 0 && a.TypeCode == typeCode && a.IsEnable == 1)
.OrderBy(a => a.Sort)
.ToListAsync();
return ApiResult.Ok(list.Select(d => new { d.Id, d.DictKey, d.DictValue, d.ExtData }));
}
/// <summary>查询全部字典(按类型分组)</summary>
[HttpGet("dict/all")]
public async Task<ApiResult> GetAllDicts()
{
var types = await _db.Queryable<BaseCommon_DictType>()
.Where(a => a.Flag > 0).ToListAsync();
var dicts = await _db.Queryable<BaseCommon_Dict>()
.Where(a => a.Flag > 0 && a.IsEnable == 1)
.OrderBy(a => a.Sort).ToListAsync();
return ApiResult.Ok(types.Select(t => new
{
t.Id, t.Code, t.Name,
items = dicts.Where(d => d.TypeCode == t.Code).Select(d => new { d.Id, d.DictKey, d.DictValue, d.ExtData })
}));
}
/// <summary>创建用户(自动生成默认密码哈希)</summary>
[HttpPost("user/create")]
public async Task<ApiResult> CreateUser([FromBody] CreateUserDto dto)
{
if (string.IsNullOrWhiteSpace(dto.Phone) || dto.Phone.Length < 6)
return ApiResult.Error("手机号不合法");
var exists = await _db.Queryable<BaseSys_User>()
.Where(a => a.Phone == dto.Phone && a.Flag > 0).AnyAsync();
if (exists) return ApiResult.Error("该手机号已注册");
var password = string.IsNullOrWhiteSpace(dto.Password) ? "123456" : dto.Password!;
var user = new BaseSys_User
{
Phone = dto.Phone,
Password = AuthService.HashPassword(password),
Name = dto.Name ?? dto.Phone,
Gender = dto.Gender,
IsSuperAdmin = 0,
Status = dto.Status ?? 1,
AddTime = DateTime.Now,
UpdateTime = DateTime.Now
};
await _db.Insert(user).ExecuteAffrowsAsync();
return ApiResult.Ok(new { user.Id }, "创建成功,初始密码 " + password);
}
/// <summary>用户绑定角色</summary>
[HttpPost("user/{userId:long}/bindRoles")]
public async Task<ApiResult> BindRoles(long userId, [FromBody] List<long> roleIds)
{
await _db.Delete<BaseSys_UserRole>().Where(a => a.UserId == userId).ExecuteAffrowsAsync();
var binds = roleIds.Select(rid => new BaseSys_UserRole { UserId = userId, RoleId = rid }).ToList();
if (binds.Count > 0) await _db.Insert(binds).ExecuteAffrowsAsync();
return ApiResult.Ok(null, "绑定成功");
}
/// <summary>角色绑定菜单权限</summary>
[HttpPost("role/{roleId:long}/bindMenus")]
public async Task<ApiResult> BindMenus(long roleId, [FromBody] List<long> menuIds)
{
await _db.Delete<BaseSys_RoleMenu>().Where(a => a.RoleId == roleId).ExecuteAffrowsAsync();
var binds = menuIds.Select(mid => new BaseSys_RoleMenu { RoleId = roleId, MenuId = mid }).ToList();
if (binds.Count > 0) await _db.Insert(binds).ExecuteAffrowsAsync();
return ApiResult.Ok(null, "授权成功");
}
/// <summary>重置用户密码</summary>
[HttpPost("user/resetPassword")]
public async Task<ApiResult> ResetPassword([FromBody] ResetPasswordDto dto)
{
return await _auth.ResetPasswordAsync(dto.UserId, dto.NewPassword ?? "123456");
}
}
/// <summary>创建用户参数</summary>
public class CreateUserDto
{
public string? Phone { get; set; }
public string? Password { get; set; }
public string? Name { get; set; }
public int Gender { get; set; }
public int? Status { get; set; }
}
/// <summary>重置密码参数</summary>
public class ResetPasswordDto
{
public long UserId { get; set; }
public string? NewPassword { get; set; }
}
@@ -0,0 +1,310 @@
using System.Reflection;
using F9MES.Api.Common;
using F9MES.Application.Base;
using F9MES.Application.Biz;
using F9MES.Application.Dtos;
using F9MES.Common.Entities;
using F9MES.Common.Result;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace F9MES.Api.Controllers;
/// <summary>
/// 动态数据控制器:通过表名访问任意已注册实体的通用 CRUD
/// 路由:POST api/data/{table}/page | get | add | update | delete | all
/// 表名示例:RawMaterial_Zhuangkou / Process_Sheet / Fims_Batch ...
/// 出入库单据变更后自动联动库存(见 SyncStockAsync
/// </summary>
[ApiController]
[Route("api/data")]
[Authorize]
public class DataController : ControllerBase
{
/// <summary>需要库存联动的单据表:表名 → 联动类型(Raw=原料 / Fims=成品)</summary>
private static readonly Dictionary<string, string> StockLink = new(StringComparer.OrdinalIgnoreCase)
{
["RawMaterial_InStock"] = "Raw",
["RawMaterial_OutStock"] = "Raw",
["Fims_InStock"] = "Fims",
["Fims_OutStock"] = "Fims"
};
/// <summary>
/// 需要生产执行进度联动的单据表:表名 → 联动方式
/// RawZk=按原料庄口反查工艺庄口 / Pz=按工艺庄口 / SpoolCheck=丝片检查 / GradeResult=厂检定级
/// </summary>
private static readonly Dictionary<string, string> ProductionLink = new(StringComparer.OrdinalIgnoreCase)
{
["ProXuan_Daily"] = "RawZk",
["ProQian_CocoonBoiling"] = "Pz",
["ProQian_ThreadRecord"] = "Pz",
["ProHou_Daily"] = "Pz",
["ProHou_SpoolLedger"] = "Pz",
["ProHou_WeighBig"] = "Pz",
["ProHou_SpoolCheck"] = "SpoolCheck",
["ProPlan_WorkOrder"] = "Pz",
["Lims_GradeResult"] = "GradeResult"
};
private readonly IServiceProvider _sp;
public DataController(IServiceProvider sp)
{
_sp = sp;
}
/// <summary>获取所有已注册表名(供前端下拉/调试)</summary>
[HttpGet("tables")]
public ApiResult Tables()
{
return ApiResult.Ok(EntityCatalog.Names.OrderBy(k => k).ToList());
}
/// <summary>分页查询</summary>
[HttpPost("{table}/page")]
public async Task<ApiResult> Page(string table, [FromBody] QueryDto dto)
{
return await InvokeAsync(table, "GetPageAsync", new object?[] { dto });
}
/// <summary>按 ID 查询</summary>
[HttpGet("{table}/{id:long}")]
public async Task<ApiResult> Get(string table, long id)
{
return await InvokeAsync(table, "GetByIdAsync", new object?[] { id });
}
/// <summary>新增</summary>
[HttpPost("{table}/add")]
public async Task<ApiResult> Add(string table, [FromBody] object body)
{
var entity = ConvertBody(table, body);
if (entity == null) return ApiResult.Error("表不存在或数据格式错误");
var result = await InvokeEntityAsync(entity, "AddAsync");
await SyncStockAsync(table, entity);
await SyncProductionAsync(table, entity);
return ApiResult.Ok(result, "新增成功");
}
/// <summary>修改(部分字段更新:仅更新请求体中出现的字段,未提交字段保持原值)</summary>
[HttpPost("{table}/update")]
public async Task<ApiResult> Update(string table, [FromBody] object body)
{
var entity = ConvertBody(table, body);
if (entity == null) return ApiResult.Error("表不存在或数据格式错误");
var fields = GetBodyFieldNames(body);
var result = await InvokeAsync(table, "UpdateAsync", new object?[] { entity, fields });
await SyncStockAsync(table, entity);
await SyncProductionAsync(table, entity);
return result is ApiResult ar ? ar : ApiResult.Ok();
}
/// <summary>删除(软删除)</summary>
[HttpPost("{table}/delete/{id:long}")]
public async Task<ApiResult> Delete(string table, long id)
{
var getRes = await InvokeAsync(table, "GetByIdAsync", new object?[] { id });
var entity = getRes.Code == 0 ? getRes.Data : null;
var result = await InvokeAsync(table, "DeleteAsync", new object?[] { id });
if (result.Code == 0)
{
await SyncStockAsync(table, entity);
await SyncProductionAsync(table, entity);
}
return result;
}
/// <summary>批量删除</summary>
[HttpPost("{table}/deleteRange")]
public async Task<ApiResult> DeleteRange(string table, [FromBody] List<long> ids)
{
var entities = new List<object?>();
foreach (var id in ids)
{
var r = await InvokeAsync(table, "GetByIdAsync", new object?[] { id });
if (r.Code == 0) entities.Add(r.Data);
}
var result = await InvokeAsync(table, "DeleteRangeAsync", new object?[] { ids });
if (result.Code == 0)
{
foreach (var e in entities) await SyncStockAsync(table, e);
foreach (var e in entities) await SyncProductionAsync(table, e);
}
return result;
}
/// <summary>出入库单据变更后联动重算库存(先按 Id 取库中完整实体,避免前端只提交部分字段导致联动失效)</summary>
private async Task SyncStockAsync(string table, object? entity)
{
if (entity == null || !StockLink.TryGetValue(table, out var kind)) return;
entity = await LoadFullEntityAsync(table, entity);
if (entity == null) return;
var stock = _sp.GetRequiredService<StockService>();
if (kind == "Raw")
{
var zkId = ReadLong(entity, "ZhuangkouId");
if (zkId > 0) await stock.RecalcRawAsync(zkId);
}
else // Fims
{
var pzId = ReadLong(entity, "ProcessZhuangkouId");
if (pzId > 0)
{
// 入库单变更:先自动生成/对齐批次与包件,再重算库存
var fims = _sp.GetRequiredService<FimsService>();
await fims.SyncByInStockAsync(ReadLong(entity, "Id"));
await stock.RecalcFimsAsync(pzId);
}
else
{
// 出库单主表无庄口字段:从明细反查
var oid = ReadLong(entity, "Id");
foreach (var z in (await stock.GetFimsZhuangkouIdsByOutStockAsync(oid)).Distinct())
await stock.RecalcFimsAsync(z);
}
}
}
/// <summary>生产执行单据变更后联动重算工单/庄口进度(先按 Id 取库中完整实体)</summary>
private async Task SyncProductionAsync(string table, object? entity)
{
if (entity == null || !ProductionLink.TryGetValue(table, out var kind)) return;
entity = await LoadFullEntityAsync(table, entity);
if (entity == null) return;
var prod = _sp.GetRequiredService<ProductionService>();
switch (kind)
{
case "RawZk":
{
var zkId = ReadLong(entity, "ZhuangkouId");
if (zkId > 0) await prod.RecalcByRawZhuangkouAsync(zkId);
break;
}
case "SpoolCheck":
{
var id = ReadLong(entity, "Id");
await prod.SyncSpoolCheckAsync(id);
break;
}
case "GradeResult":
{
// 厂检定级生效 → 自动生成成品入库单草稿(联动批次/包件在入库确认时触发)
var id = ReadLong(entity, "Id");
var fims = _sp.GetRequiredService<FimsService>();
await fims.SyncByGradeResultAsync(id);
break;
}
default: // Pz
{
var pzId = ReadLong(entity, "ProcessZhuangkouId");
if (pzId > 0) await prod.RecalcByProcessZhuangkouAsync(pzId);
break;
}
}
}
/// <summary>联动前按 Id 从库中加载完整实体(前端部分更新时补齐缺失字段)</summary>
private async Task<object?> LoadFullEntityAsync(string table, object? entity)
{
var id = ReadLong(entity, "Id");
if (id <= 0) return entity;
var r = await InvokeAsync(table, "GetByIdAsync", new object?[] { id });
return r.Code == 0 ? r.Data : entity;
}
/// <summary>反射读取实体长整型属性值(属性名大小写不敏感)</summary>
private static long ReadLong(object entity, string propName)
{
var p = entity.GetType().GetProperty(propName,
BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
var v = p?.GetValue(entity);
return v == null ? 0 : Convert.ToInt64(v);
}
/// <summary>全部数据(下拉用)</summary>
[HttpGet("{table}/all")]
public async Task<ApiResult> All(string table)
{
var result = await InvokeAsync(table, "GetAllAsync", Array.Empty<object?>());
return ApiResult.Ok(result);
}
/// <summary>反射调用 CrudService&lt;T&gt; 的无参数/实体参数方法</summary>
private async Task<ApiResult> InvokeAsync(string table, string methodName, object?[] args)
{
if (!EntityCatalog.TryGet(table, out var entityType))
return ApiResult.Error($"表 {table} 不存在");
var crudType = typeof(CrudService<>).MakeGenericType(entityType);
var service = ActivatorUtilities.CreateInstance(_sp, crudType);
var method = FindMethod(crudType, methodName, args);
if (method == null) return ApiResult.Error($"方法 {methodName} 不存在");
var task = (Task)method.Invoke(service, args)!;
await task.ConfigureAwait(false);
var result = task.GetType().GetProperty("Result")?.GetValue(task);
if (result is ApiResult ar) return ar;
// 非 ApiResult 返回值(实体/分页/列表)统一包装为成功响应
return result == null ? ApiResult.Error("数据不存在") : ApiResult.Ok(result);
}
/// <summary>反射调用 CrudService&lt;T&gt; 的实体参数方法(返回实体本身)</summary>
private async Task<object?> InvokeEntityAsync(object entity, string methodName)
{
var entityType = entity.GetType();
var crudType = typeof(CrudService<>).MakeGenericType(entityType);
var service = ActivatorUtilities.CreateInstance(_sp, crudType);
var method = FindMethod(crudType, methodName, new[] { entity });
if (method == null) return null;
var task = (Task)method.Invoke(service, new[] { entity })!;
await task.ConfigureAwait(false);
return task.GetType().GetProperty("Result")?.GetValue(task);
}
/// <summary>
/// 按名称 + 参数类型精确查找方法(避免方法重载时 GetMethod(name) 抛出 AmbiguousMatchException
/// </summary>
private static MethodInfo? FindMethod(Type type, string name, object?[]? args)
{
if (args is { Length: > 0 } && args.All(a => a != null))
{
var paramTypes = args.Select(a => a!.GetType()).ToArray();
var m = type.GetMethod(name, paramTypes);
if (m != null) return m;
}
var matches = type.GetMethods()
.Where(m => m.Name == name && m.GetParameters().Length == (args?.Length ?? 0))
.ToList();
return matches.Count == 1 ? matches[0] : null;
}
/// <summary>将请求体 JSON 转换为目标实体对象</summary>
private object? ConvertBody(string table, object body)
{
if (!EntityCatalog.TryGet(table, out var entityType)) return null;
var json = System.Text.Json.JsonSerializer.Serialize(body,
new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true });
return System.Text.Json.JsonSerializer.Deserialize(json, entityType,
new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true });
}
/// <summary>提取请求体中出现的字段名(用于部分更新,属性名大小写不敏感)</summary>
private static List<string> GetBodyFieldNames(object body)
{
var json = System.Text.Json.JsonSerializer.Serialize(body);
try
{
using var doc = System.Text.Json.JsonDocument.Parse(json);
if (doc.RootElement.ValueKind != System.Text.Json.JsonValueKind.Object) return new List<string>();
return doc.RootElement.EnumerateObject().Select(p => p.Name).ToList();
}
catch
{
return new List<string>();
}
}
}
@@ -0,0 +1,258 @@
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)
};
/// <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, "", "-"),
["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)";
}
}
@@ -0,0 +1,107 @@
using F9MES.Application.Print;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace F9MES.Api.Controllers;
/// <summary>
/// 打印对接接口(OpenPrint 后端契约,见 openprint/src/repository/*
/// 注意:与 F9 其它接口不同,此处返回【裸 JSON】而非 ApiResult 信封:
/// - 列表:{ items, total }
/// - 详情/创建/更新:资源对象
/// - 删除:204 无 body
/// - 错误:{ code, message, detail, requestId }
/// 鉴权复用 F9 的 JWTopenprint 侧自动注入 Authorization: Bearer {token})。
/// </summary>
[ApiController]
[Authorize]
[Route("api/print")]
public class PrintApiController : ControllerBase
{
private readonly PrintService _print;
public PrintApiController(PrintService print)
{
_print = print;
}
// ==================== 模板 ====================
/// <summary>模板列表(不含 content</summary>
[HttpGet("templates")]
public async Task<IActionResult> GetTemplates()
{
var (items, total) = await _print.ListTemplatesAsync();
return Ok(new { items, total });
}
/// <summary>模板详情(含 contentTemplateJSON 字符串)</summary>
[HttpGet("templates/{id}")]
public async Task<IActionResult> GetTemplate(string id)
{
var row = await _print.GetTemplateAsync(id);
if (row == null) return Err(StatusCodes.Status404NotFound, "模板不存在", $"id={id}");
return Ok(row);
}
/// <summary>创建模板(201 + 含真实 id 的资源)</summary>
[HttpPost("templates")]
public async Task<IActionResult> CreateTemplate([FromBody] PrintTemplateInput input)
{
if (input == null || string.IsNullOrWhiteSpace(input.Name))
return Err(StatusCodes.Status400BadRequest, "模板名称不能为空");
var row = await _print.CreateTemplateAsync(input);
return StatusCode(StatusCodes.Status201Created, row);
}
/// <summary>更新模板(部分字段;openprint 发 name 和/或 content</summary>
[HttpPut("templates/{id}")]
public async Task<IActionResult> UpdateTemplate(string id, [FromBody] PrintTemplateInput input)
{
if (input == null)
return Err(StatusCodes.Status400BadRequest, "请求体不能为空");
var row = await _print.UpdateTemplateAsync(id, input);
if (row == null) return Err(StatusCodes.Status404NotFound, "模板不存在", $"id={id}");
return Ok(row);
}
/// <summary>物理删除模板(204 无 body</summary>
[HttpDelete("templates/{id}")]
public async Task<IActionResult> DeleteTemplate(string id)
{
var ok = await _print.DeleteTemplateAsync(id);
if (!ok) return Err(StatusCodes.Status404NotFound, "模板不存在", $"id={id}");
return NoContent();
}
// ==================== 数据源 ====================
/// <summary>数据源列表(含子表层级 tables</summary>
[HttpGet("data-sources")]
public IActionResult GetDataSources()
{
var items = _print.ListDataSources();
return Ok(new { items, total = items.Count });
}
/// <summary>数据源字段定义</summary>
[HttpGet("data-sources/{id}/fields")]
public IActionResult GetFields(string id)
{
var items = _print.ListFields(id);
return Ok(new { items, total = items.Count });
}
/// <summary>统一错误信封:{ code, message, detail, requestId }</summary>
private IActionResult Err(int status, string message, string? detail = null)
{
return new JsonResult(new
{
code = status,
message,
detail,
requestId = HttpContext.TraceIdentifier,
})
{ StatusCode = status };
}
}
@@ -0,0 +1,57 @@
using System.Text.Json;
using F9MES.Common.Result;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace F9MES.Api.Controllers;
/// <summary>
/// 天气服务控制器:集成 Open-Meteo 免费天气 API(无需 Key
/// 用于生产车间温湿度环境参考
/// </summary>
[ApiController]
[Route("api/weather")]
[Authorize]
public class WeatherController : ControllerBase
{
private readonly IHttpClientFactory _http;
public WeatherController(IHttpClientFactory http)
{
_http = http;
}
/// <summary>
/// 查询实时天气
/// </summary>
/// <param name="lat">纬度,默认嘉兴</param>
/// <param name="lon">经度,默认嘉兴</param>
[HttpGet("now")]
[AllowAnonymous]
public async Task<ApiResult> Now(double lat = 30.75, double lon = 120.75)
{
try
{
var client = _http.CreateClient("weather");
var url = $"https://api.open-meteo.com/v1/forecast?latitude={lat}&longitude={lon}&current=temperature_2m,relative_humidity_2m,apparent_temperature,precipitation,weather_code,wind_speed_10m&timezone=Asia%2FShanghai";
var json = await client.GetStringAsync(url);
var doc = JsonDocument.Parse(json);
var cur = doc.RootElement.GetProperty("current");
return ApiResult.Ok(new
{
temperature = cur.GetProperty("temperature_2m").GetDecimal(),
humidity = cur.GetProperty("relative_humidity_2m").GetDecimal(),
apparentTemp = cur.GetProperty("apparent_temperature").GetDecimal(),
precipitation = cur.GetProperty("precipitation").GetDecimal(),
weatherCode = cur.GetProperty("weather_code").GetInt32(),
windSpeed = cur.GetProperty("wind_speed_10m").GetDecimal(),
time = cur.GetProperty("time").GetString()
});
}
catch (Exception ex)
{
return ApiResult.Error("天气服务获取失败:" + ex.Message);
}
}
}
@@ -0,0 +1,172 @@
using F9MES.Common.Auth;
using F9MES.Common.Result;
using F9MES.Domain.Common;
using F9MES.Domain.Fims;
using F9MES.Domain.ProHou;
using F9MES.Domain.ProPlan;
using F9MES.Domain.ProQian;
using F9MES.Domain.Process;
using FreeSql;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace F9MES.Api.Controllers;
/// <summary>
/// 工作台聚合数据(真实业务统计)
/// 路由:GET api/workbench/summary
/// </summary>
[ApiController]
[Route("api/workbench")]
[Authorize]
public class WorkBenchController : ControllerBase
{
private readonly IFreeSql _db;
private readonly CurrentUserService _currentUser;
public WorkBenchController(IFreeSql db, CurrentUserService currentUser)
{
_db = db;
_currentUser = currentUser;
}
/// <summary>工作台首页聚合数据(统计卡 + 趋势 + 占比 + 待办 + 预警)</summary>
[HttpGet("summary")]
public async Task<ApiResult> Summary()
{
var today = DateTime.Today;
var start = today.AddDays(-29);
// ===== 统计卡 =====
// 今日产丝(返丝日报 OutputWeight 合计)
var todayOutput = await _db.Select<ProHou_Daily>()
.Where(d => d.Flag == 1 && d.DailyDate >= today && d.DailyDate < today.AddDays(1))
.SumAsync(d => d.OutputWeight);
// 在线机组(启用且运行中)
var machineTotal = await _db.Select<ProQian_Machine>()
.Where(m => m.Flag == 1 && m.IsEnable == 1).CountAsync();
var machineRunning = await _db.Select<ProQian_Machine>()
.Where(m => m.Flag == 1 && m.IsEnable == 1 && m.Status == 1).CountAsync();
// 待办工单(待下发/已下发/执行中/已暂停)
var workOrderPending = await _db.Select<ProPlan_WorkOrder>()
.Where(w => w.Flag == 1 && (w.Status == 0 || w.Status == 1 || w.Status == 2 || w.Status == 4))
.CountAsync();
// 工作流待办(当前用户)
var myFlowTodo = await _db.Select<Common_WorkflowTask>()
.Where(t => t.Flag == 1 && t.Status == 0 && t.UserId == _currentUser.UserId)
.CountAsync();
// 库存预警(低于下限)
var warnList = await _db.Select<Fims_Stock>()
.Where(s => s.Flag == 1 && s.WarnMin > 0 && s.StockWeight < s.WarnMin)
.ToListAsync();
// ===== 近30日产丝趋势 =====
var trendRaw = await _db.Select<ProHou_Daily>()
.Where(d => d.Flag == 1 && d.DailyDate >= start && d.DailyDate < today.AddDays(1))
.ToListAsync(d => new { d.DailyDate, d.OutputWeight, d.ProcessZhuangkouId });
var trend = new List<object>();
for (var i = 0; i < 30; i++)
{
var day = start.AddDays(i);
var sum = trendRaw.Where(x => x.DailyDate.Date == day.Date).Sum(x => x.OutputWeight);
trend.Add(new { date = day.ToString("MM-dd"), weight = sum });
}
// ===== 庄口产量占比(近30天,Top 8=====
var zkIds = trendRaw.Select(x => x.ProcessZhuangkouId).Distinct().ToList();
var zkNames = new List<Process_ProcessZhuangkou>();
if (zkIds.Count > 0)
{
zkNames = await _db.Select<Process_ProcessZhuangkou>()
.Where(z => z.Flag == 1 && zkIds.Contains(z.Id))
.ToListAsync();
}
var nameMap = zkNames.ToDictionary(x => x.Id, x => string.IsNullOrEmpty(x.Name) ? $"庄口{x.Id}" : x.Name);
var zhuangkou = trendRaw
.GroupBy(x => x.ProcessZhuangkouId)
.Select(g => new { name = nameMap.GetValueOrDefault(g.Key) ?? $"庄口{g.Key}", value = g.Sum(x => x.OutputWeight) })
.OrderByDescending(x => x.value)
.Take(8)
.Select(x => (object)x)
.ToList();
// ===== 待办列表(工单 + 工作流审批)=====
var pendingOrders = await _db.Select<ProPlan_WorkOrder>()
.Where(w => w.Flag == 1 && (w.Status == 0 || w.Status == 1 || w.Status == 2 || w.Status == 4))
.OrderByDescending(w => w.AddTime)
.Limit(8)
.ToListAsync(w => new { w.Id, w.OrderNo, w.ProcessZhuangkouId, w.Status, w.PlanStartDate });
var todos = new List<object>();
foreach (var o in pendingOrders)
{
var zkName = nameMap.GetValueOrDefault(o.ProcessZhuangkouId) ?? "";
todos.Add(new
{
id = o.Id,
title = $"{o.OrderNo}{zkName}",
type = "工单",
statusText = WorkOrderStatusText(o.Status),
time = o.PlanStartDate.ToString("MM-dd"),
url = "/proplan/workorder"
});
}
if (myFlowTodo > 0)
{
todos.Add(new
{
id = 0,
title = $"您有 {myFlowTodo} 条审批待办待处理",
type = "审批",
statusText = "待审批",
time = "",
url = "/common/workflowtask"
});
}
// ===== 预警列表 =====
var warnings = new List<object>();
foreach (var s in warnList.Take(8))
{
var zkName = nameMap.GetValueOrDefault(s.ProcessZhuangkouId) ?? "";
warnings.Add(new
{
id = s.Id,
title = $"库存告急:{zkName} {s.Grade}",
content = $"现存 {s.StockWeight}kg,低于预警下限 {s.WarnMin}kg" + (string.IsNullOrEmpty(s.Location) ? "" : $"{s.Location}"),
level = "danger",
time = s.UpdateTime.ToString("MM-dd HH:mm"),
url = "/fims/stock"
});
}
var cards = new
{
todayOutput,
machineTotal,
machineRunning,
workOrderPending,
flowTodo = myFlowTodo,
warningCount = warnList.Count
};
return ApiResult.Ok(new { cards, trend, zhuangkou, todos, warnings });
}
private static string WorkOrderStatusText(int status) => status switch
{
0 => "待下发",
1 => "已下发",
2 => "执行中",
3 => "已完成",
4 => "已暂停",
5 => "已取消",
_ => "未知"
};
}