init: F9智慧缫丝系统 - 后端(server) + 主前端(web) + 打印设计器(openprint) 首次提交
This commit is contained in:
@@ -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<T> 的无参数/实体参数方法</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<T> 的实体参数方法(返回实体本身)</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 的 JWT(openprint 侧自动注入 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>模板详情(含 content,TemplateJSON 字符串)</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}¤t=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 => "已取消",
|
||||
_ => "未知"
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user