feat: 新增IM即时通讯(浮窗)、工作流、打印模块及工作台增强
- IM: 新增浮窗聊天(ImFloatWindow)、管理页(monitor/config/service/message)、SSE推送 - 工作流: 新增待办/我的流程页面及后端服务 - 打印: 新增打印模板、出库单打印(PrintPage)、模板种子脚本 - 工作台: 增强快捷入口与工作台数据 - 修复: TagsView页签关闭、CrudPage通用表格增强 - 移除导航菜单中的即时通讯入口,改为右下角浮窗
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
using System.Threading.Tasks;
|
||||
using F9MES.Application.Im;
|
||||
using F9MES.Common.Result;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace F9MES.Api.Controllers;
|
||||
|
||||
/// <summary>IM 系统管理:性能/服务监测、配置界面、系统消息与公告管理</summary>
|
||||
[ApiController]
|
||||
[Route("api/im/admin")]
|
||||
[Authorize]
|
||||
public class ImAdminController : ControllerBase
|
||||
{
|
||||
private readonly ImAdminService _svc;
|
||||
|
||||
public ImAdminController(ImAdminService svc)
|
||||
{
|
||||
_svc = svc;
|
||||
}
|
||||
|
||||
/// <summary>性能监测</summary>
|
||||
[HttpGet("performance")]
|
||||
public async Task<ActionResult> Performance()
|
||||
{
|
||||
return Ok(ApiResult.Ok(await _svc.PerformanceAsync()));
|
||||
}
|
||||
|
||||
/// <summary>服务监测</summary>
|
||||
[HttpGet("service")]
|
||||
public async Task<ActionResult> Service()
|
||||
{
|
||||
return Ok(ApiResult.Ok(await _svc.ServiceStatusAsync()));
|
||||
}
|
||||
|
||||
/// <summary>读取 IM 配置</summary>
|
||||
[HttpGet("config")]
|
||||
public async Task<ActionResult> Config()
|
||||
{
|
||||
return Ok(ApiResult.Ok(await _svc.GetConfigAsync()));
|
||||
}
|
||||
|
||||
/// <summary>保存 IM 配置</summary>
|
||||
[HttpPost("config")]
|
||||
public async Task<ActionResult> SaveConfig([FromBody] List<ImConfigInput> inputs)
|
||||
{
|
||||
await _svc.SaveConfigAsync(inputs ?? new());
|
||||
return Ok(ApiResult.Ok(true, "配置已保存"));
|
||||
}
|
||||
|
||||
/// <summary>系统消息管理分页(msgType=-1 全部)</summary>
|
||||
[HttpGet("messages")]
|
||||
public async Task<ActionResult> Messages([FromQuery] string? kw, [FromQuery] int msgType = -1, [FromQuery] int page = 1, [FromQuery] int size = 20)
|
||||
{
|
||||
return Ok(ApiResult.Ok(await _svc.GetMessagesAsync(kw, msgType, page, size)));
|
||||
}
|
||||
|
||||
/// <summary>删除消息</summary>
|
||||
[HttpDelete("messages/{id}")]
|
||||
public async Task<ActionResult> DeleteMessage(long id)
|
||||
{
|
||||
await _svc.DeleteMessageAsync(id);
|
||||
return Ok(ApiResult.Ok(true, "已删除"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
using System.Threading.Tasks;
|
||||
using F9MES.Application.Im;
|
||||
using F9MES.Common.Auth;
|
||||
using F9MES.Common.Result;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace F9MES.Api.Controllers;
|
||||
|
||||
/// <summary>轻量 IM:单聊 + 系统通知(SSE 长连接实时推送)</summary>
|
||||
[ApiController]
|
||||
[Route("api/im")]
|
||||
[Authorize]
|
||||
public class ImController : ControllerBase
|
||||
{
|
||||
private readonly ImService _im;
|
||||
private readonly ImEventHub _hub;
|
||||
private readonly CurrentUserService _currentUser;
|
||||
|
||||
public ImController(ImService im, ImEventHub hub, CurrentUserService currentUser)
|
||||
{
|
||||
_im = im;
|
||||
_hub = hub;
|
||||
_currentUser = currentUser;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SSE 长连接:订阅新消息/通知的实时推送。
|
||||
/// 客户端用 fetch 流式读取(EventSource 无法携带 Authorization header),
|
||||
/// 收到 message 事件后刷新会话/消息列表即可,无需再轮询。
|
||||
/// </summary>
|
||||
[HttpGet("events")]
|
||||
public async Task Events(CancellationToken ct)
|
||||
{
|
||||
var me = _currentUser.UserId;
|
||||
if (me <= 0)
|
||||
{
|
||||
Response.StatusCode = StatusCodes.Status401Unauthorized;
|
||||
return;
|
||||
}
|
||||
_im.TouchActive();
|
||||
Response.ContentType = "text/event-stream";
|
||||
Response.Headers.CacheControl = "no-cache";
|
||||
Response.Headers.Append("Connection", "keep-alive");
|
||||
await _hub.SubscribeAsync(me, Response, ct);
|
||||
}
|
||||
|
||||
/// <summary>会话列表</summary>
|
||||
[HttpGet("sessions")]
|
||||
public async Task<ActionResult> Sessions()
|
||||
{
|
||||
var list = await _im.GetSessionsAsync();
|
||||
return Ok(ApiResult.Ok(list));
|
||||
}
|
||||
|
||||
/// <summary>与某人的聊天记录</summary>
|
||||
[HttpGet("messages")]
|
||||
public async Task<ActionResult> Messages([FromQuery] long peerId, [FromQuery] int page = 1, [FromQuery] int size = 20)
|
||||
{
|
||||
var (items, total) = await _im.GetMessagesAsync(peerId, page, size);
|
||||
return Ok(ApiResult.Ok(new { items, total, page, size }));
|
||||
}
|
||||
|
||||
/// <summary>发送单聊消息</summary>
|
||||
[HttpPost("send")]
|
||||
public async Task<ActionResult> Send([FromBody] ImSendInput input)
|
||||
{
|
||||
await _im.SendAsync(input.PeerId, input.Content);
|
||||
return Ok(ApiResult.Ok(true, "发送成功"));
|
||||
}
|
||||
|
||||
/// <summary>标记与某人的会话已读</summary>
|
||||
[HttpPost("read")]
|
||||
public async Task<ActionResult> Read([FromBody] ImReadInput input)
|
||||
{
|
||||
await _im.ReadAsync(input.PeerId);
|
||||
return Ok(ApiResult.Ok(true));
|
||||
}
|
||||
|
||||
/// <summary>未读消息总数(顶栏角标)</summary>
|
||||
[HttpGet("unread-count")]
|
||||
public async Task<ActionResult> UnreadCount()
|
||||
{
|
||||
var count = await _im.GetUnreadCountAsync();
|
||||
return Ok(ApiResult.Ok(count));
|
||||
}
|
||||
|
||||
/// <summary>系统/业务通知群发(工单/工艺单变更等)</summary>
|
||||
[HttpPost("notify")]
|
||||
public async Task<ActionResult> Notify([FromBody] ImNotifyInput input)
|
||||
{
|
||||
var count = await _im.NotifyAsync(input);
|
||||
return Ok(ApiResult.Ok(count, $"已发送 {count} 条通知"));
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
using System.Reflection;
|
||||
using F9MES.Api.Common;
|
||||
using F9MES.Application.Base;
|
||||
using F9MES.Application.Print;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace F9MES.Api.Controllers;
|
||||
|
||||
@@ -18,11 +22,38 @@ namespace F9MES.Api.Controllers;
|
||||
[Route("api/print")]
|
||||
public class PrintApiController : ControllerBase
|
||||
{
|
||||
private readonly PrintService _print;
|
||||
/// <summary>
|
||||
/// 打印数据关联字段补全映射:主表 *Id 字段 → 关联表及取名字段。
|
||||
/// 出库单/入库单等打印时自动注入 {Xxx}Code / {Xxx}Name(如 ZhuangkouCode、OutOrgName)。
|
||||
/// </summary>
|
||||
private static readonly Dictionary<string, (string Table, string? CodeField, string? NameField)> RefMap =
|
||||
new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["ZhuangkouId"] = ("RawMaterial_Zhuangkou", "Code", "Name"),
|
||||
["OutOrgId"] = ("BaseSys_Org", null, "Name"),
|
||||
["SupplierId"] = ("BaseCommon_Partner", null, "Name"),
|
||||
["EmployeeId"] = ("HRS_Employee", null, "Name"),
|
||||
["TeamId"] = ("ProXuan_Team", null, "Name"),
|
||||
["MachineId"] = ("ProQian_Machine", "MachineNo", "Name"),
|
||||
["CustomerId"] = ("Sams_Customer", null, "Name"),
|
||||
};
|
||||
|
||||
public PrintApiController(PrintService print)
|
||||
/// <summary>枚举字段文本映射:打印时自动注入 {Prop}Text(如 OutTypeText = 领料/退货/报损)</summary>
|
||||
private static readonly Dictionary<string, Dictionary<int, string>> EnumMaps =
|
||||
new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["RawMaterial_OutStock.OutType"] = new() { [0] = "领料", [1] = "退货", [2] = "报损" },
|
||||
["RawMaterial_OutStock.Status"] = new() { [0] = "草稿", [1] = "已出库", [2] = "作废" },
|
||||
["RawMaterial_Instock.InstockType"] = new() { [0] = "采购", [1] = "退货" },
|
||||
};
|
||||
|
||||
private readonly PrintService _print;
|
||||
private readonly IServiceProvider _sp;
|
||||
|
||||
public PrintApiController(PrintService print, IServiceProvider sp)
|
||||
{
|
||||
_print = print;
|
||||
_sp = sp;
|
||||
}
|
||||
|
||||
// ==================== 模板 ====================
|
||||
@@ -92,6 +123,79 @@ public class PrintApiController : ControllerBase
|
||||
return Ok(new { items, total = items.Count });
|
||||
}
|
||||
|
||||
// ==================== 打印数据 ====================
|
||||
|
||||
/// <summary>
|
||||
/// 取单据行数据(供 openprint 外部打印页 / 设计器真实数据预览使用)。
|
||||
/// 返回裸 JSON 信封:{ "表名": { 字段... } },字段键与 PrintService.BuildFields 的 Path 前缀一致;
|
||||
/// 并按 RefMap 自动补全 *Id 关联的 Code/Name(如 ZhuangkouCode、OutOrgName)。
|
||||
/// </summary>
|
||||
[HttpGet("data/{table}/{id:long}")]
|
||||
public async Task<IActionResult> GetPrintData(string table, long id)
|
||||
{
|
||||
if (!EntityCatalog.TryGet(table, out var entityType))
|
||||
return Err(StatusCodes.Status404NotFound, "表不存在", $"table={table}");
|
||||
|
||||
var row = await GetRowAsync(entityType, id);
|
||||
if (row == null)
|
||||
return Err(StatusCodes.Status404NotFound, "数据不存在", $"table={table}&id={id}");
|
||||
|
||||
// 行对象 → 属性字典(保持 PascalCase 键,后续序列化为 camelCase,openprint 侧自动补齐别名)
|
||||
var props = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var p in row.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance))
|
||||
{
|
||||
if (p.GetIndexParameters().Length > 0) continue;
|
||||
props[p.Name] = p.GetValue(row);
|
||||
// 枚举文本注入:{Table}.{Prop} → {Prop}Text
|
||||
if (props[p.Name] is int ev &&
|
||||
EnumMaps.TryGetValue($"{table}.{p.Name}", out var map) &&
|
||||
map.TryGetValue(ev, out var txt))
|
||||
{
|
||||
props[$"{p.Name}Text"] = txt;
|
||||
}
|
||||
}
|
||||
|
||||
// 关联字段补全:*Id → {Xxx}Code / {Xxx}Name
|
||||
foreach (var kv in props.ToList())
|
||||
{
|
||||
if (!kv.Key.EndsWith("Id", StringComparison.Ordinal) || kv.Value is not long refId || refId <= 0) continue;
|
||||
if (!RefMap.TryGetValue(kv.Key, out var refInfo)) continue;
|
||||
if (!EntityCatalog.TryGet(refInfo.Table, out var refType)) continue;
|
||||
var refRow = await GetRowAsync(refType, refId);
|
||||
if (refRow == null) continue;
|
||||
|
||||
var baseName = kv.Key[..^2]; // 去掉 "Id" 后缀
|
||||
var code = GetRefField(refRow, refInfo.CodeField);
|
||||
var name = GetRefField(refRow, refInfo.NameField) ?? code;
|
||||
if (!string.IsNullOrEmpty(code)) props[$"{baseName}Code"] = code;
|
||||
if (!string.IsNullOrEmpty(name)) props[$"{baseName}Name"] = name;
|
||||
}
|
||||
|
||||
return Ok(new Dictionary<string, object?> { [table] = props });
|
||||
}
|
||||
|
||||
/// <summary>读取关联行指定字段值</summary>
|
||||
private static string? GetRefField(object row, string? field)
|
||||
{
|
||||
if (string.IsNullOrEmpty(field)) return null;
|
||||
var p = row.GetType().GetProperty(field,
|
||||
BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
|
||||
var v = p?.GetValue(row);
|
||||
return v == null ? null : v.ToString();
|
||||
}
|
||||
|
||||
/// <summary>按实体类型 + 主键动态查询单行(复用 CrudService 模板)</summary>
|
||||
private async Task<object?> GetRowAsync(Type entityType, long id)
|
||||
{
|
||||
var crudType = typeof(CrudService<>).MakeGenericType(entityType);
|
||||
var service = ActivatorUtilities.CreateInstance(_sp, crudType);
|
||||
var method = crudType.GetMethod("GetByIdAsync", new[] { typeof(long) });
|
||||
if (method == null) return null;
|
||||
var task = (Task)method.Invoke(service, new object[] { id })!;
|
||||
await task.ConfigureAwait(false);
|
||||
return task.GetType().GetProperty("Result")?.GetValue(task);
|
||||
}
|
||||
|
||||
/// <summary>统一错误信封:{ code, message, detail, requestId }</summary>
|
||||
private IActionResult Err(int status, string message, string? detail = null)
|
||||
{
|
||||
|
||||
@@ -6,6 +6,7 @@ using F9MES.Domain.ProHou;
|
||||
using F9MES.Domain.ProPlan;
|
||||
using F9MES.Domain.ProQian;
|
||||
using F9MES.Domain.Process;
|
||||
using F9MES.Domain.ProXuan;
|
||||
using FreeSql;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@@ -126,7 +127,7 @@ public class WorkBenchController : ControllerBase
|
||||
type = "审批",
|
||||
statusText = "待审批",
|
||||
time = "",
|
||||
url = "/common/workflowtask"
|
||||
url = "/workflow/todo"
|
||||
});
|
||||
}
|
||||
|
||||
@@ -159,6 +160,155 @@ public class WorkBenchController : ControllerBase
|
||||
return ApiResult.Ok(new { cards, trend, zhuangkou, todos, warnings });
|
||||
}
|
||||
|
||||
/// <summary>庄口生产分工段进度(选茧→煮茧→缫丝→复摇→秤大丝)</summary>
|
||||
[HttpGet("zhuangkou-progress")]
|
||||
public async Task<ApiResult> ZhuangkouProgress()
|
||||
{
|
||||
var pzs = await _db.Select<Process_ProcessZhuangkou>()
|
||||
.Where(z => z.Flag == 1 && z.Status != 4)
|
||||
.OrderBy(z => z.Status)
|
||||
.OrderByDescending(z => z.AddTime)
|
||||
.ToListAsync(z => new { z.Id, z.Code, z.Name, z.ZhuangkouId, z.Progress, z.Status });
|
||||
|
||||
var pzIds = pzs.Select(p => p.Id).ToList();
|
||||
var zkIds = pzs.Select(p => p.ZhuangkouId).Where(id => id > 0).Distinct().ToList();
|
||||
|
||||
// 选茧(ProXuan_Daily 按原料庄口 ZhuangkouId 关联)
|
||||
var xuanMap = new Dictionary<long, (decimal Input, decimal Good)>();
|
||||
if (zkIds.Count > 0)
|
||||
{
|
||||
var rows = await _db.Select<ProXuan_Daily>()
|
||||
.Where(a => a.Flag == 1 && zkIds.Contains(a.ZhuangkouId))
|
||||
.ToListAsync(a => new { a.ZhuangkouId, a.InputWeight, a.GoodWeight });
|
||||
xuanMap = rows
|
||||
.GroupBy(x => x.ZhuangkouId)
|
||||
.ToDictionary(g => g.Key, g => (Input: g.Sum(x => x.InputWeight), Good: g.Sum(x => x.GoodWeight)));
|
||||
}
|
||||
|
||||
// 煮茧(送茧量)
|
||||
var boilMap = new Dictionary<long, decimal>();
|
||||
if (pzIds.Count > 0)
|
||||
{
|
||||
var rows = await _db.Select<ProQian_CocoonBoiling>()
|
||||
.Where(a => a.Flag == 1 && pzIds.Contains(a.ProcessZhuangkouId))
|
||||
.ToListAsync(a => new { a.ProcessZhuangkouId, a.InputWeight });
|
||||
boilMap = rows.GroupBy(x => x.ProcessZhuangkouId).ToDictionary(g => g.Key, g => g.Sum(x => x.InputWeight));
|
||||
}
|
||||
|
||||
// 缫丝(下丝量)
|
||||
var threadMap = new Dictionary<long, decimal>();
|
||||
if (pzIds.Count > 0)
|
||||
{
|
||||
var rows = await _db.Select<ProQian_ThreadRecord>()
|
||||
.Where(a => a.Flag == 1 && pzIds.Contains(a.ProcessZhuangkouId))
|
||||
.ToListAsync(a => new { a.ProcessZhuangkouId, a.Weight });
|
||||
threadMap = rows.GroupBy(x => x.ProcessZhuangkouId).ToDictionary(g => g.Key, g => g.Sum(x => x.Weight));
|
||||
}
|
||||
|
||||
// 复摇(上机丝量 / 返丝产量)
|
||||
var houMap = new Dictionary<long, (decimal Input, decimal Output)>();
|
||||
if (pzIds.Count > 0)
|
||||
{
|
||||
var rows = await _db.Select<ProHou_Daily>()
|
||||
.Where(a => a.Flag == 1 && pzIds.Contains(a.ProcessZhuangkouId))
|
||||
.ToListAsync(a => new { a.ProcessZhuangkouId, a.InputWeight, a.OutputWeight });
|
||||
houMap = rows
|
||||
.GroupBy(x => x.ProcessZhuangkouId)
|
||||
.ToDictionary(g => g.Key, g => (Input: g.Sum(x => x.InputWeight), Output: g.Sum(x => x.OutputWeight)));
|
||||
}
|
||||
|
||||
// 秤大丝(总重量)
|
||||
var weighMap = new Dictionary<long, decimal>();
|
||||
if (pzIds.Count > 0)
|
||||
{
|
||||
var rows = await _db.Select<ProHou_WeighBig>()
|
||||
.Where(a => a.Flag == 1 && pzIds.Contains(a.ProcessZhuangkouId))
|
||||
.ToListAsync(a => new { a.ProcessZhuangkouId, a.TotalWeight });
|
||||
weighMap = rows.GroupBy(x => x.ProcessZhuangkouId).ToDictionary(g => g.Key, g => g.Sum(x => x.TotalWeight));
|
||||
}
|
||||
|
||||
// 工单计划(TargetProcess:0=选茧 1=前缫)
|
||||
var orderMap = new Dictionary<long, List<(int TargetProcess, decimal PlanOutput)>>();
|
||||
if (pzIds.Count > 0)
|
||||
{
|
||||
var rows = await _db.Select<ProPlan_WorkOrder>()
|
||||
.Where(a => a.Flag == 1 && pzIds.Contains(a.ProcessZhuangkouId))
|
||||
.ToListAsync(a => new { a.ProcessZhuangkouId, a.TargetProcess, a.PlanOutput });
|
||||
orderMap = rows
|
||||
.GroupBy(x => x.ProcessZhuangkouId)
|
||||
.ToDictionary(g => g.Key, g => g.Select(x => (x.TargetProcess, x.PlanOutput)).ToList());
|
||||
}
|
||||
|
||||
var items = new List<object>();
|
||||
foreach (var p in pzs)
|
||||
{
|
||||
var xuan = xuanMap.GetValueOrDefault(p.ZhuangkouId);
|
||||
var boil = boilMap.GetValueOrDefault(p.Id);
|
||||
var thread = threadMap.GetValueOrDefault(p.Id);
|
||||
var hou = houMap.GetValueOrDefault(p.Id);
|
||||
var weigh = weighMap.GetValueOrDefault(p.Id);
|
||||
var orders = orderMap.GetValueOrDefault(p.Id) ?? new List<(int, decimal)>();
|
||||
|
||||
// 计划产丝量(前缫工单计划产出合计)
|
||||
var planOutput = orders.Where(o => o.TargetProcess == 1).Sum(o => o.PlanOutput);
|
||||
|
||||
var stages = new List<object>
|
||||
{
|
||||
Stage("xuan", "选茧", "领料 → 上车茧", xuan.Good, xuan.Input),
|
||||
Stage("boil", "煮茧", "上车茧 → 送茧", boil, xuan.Good),
|
||||
Stage("thread", "缫丝", "送茧 → 下丝", thread, boil),
|
||||
Stage("reel", "复摇", "上机丝 → 返丝", hou.Output, hou.Input),
|
||||
Stage("weigh", "秤大丝", "返丝 → 成件称重", weigh, hou.Output)
|
||||
};
|
||||
|
||||
items.Add(new
|
||||
{
|
||||
id = p.Id,
|
||||
code = p.Code,
|
||||
name = string.IsNullOrEmpty(p.Name) ? p.Code : p.Name,
|
||||
status = p.Status,
|
||||
statusText = ZhuangkouStatusText(p.Status),
|
||||
progress = p.Progress,
|
||||
planOutput,
|
||||
stages
|
||||
});
|
||||
}
|
||||
|
||||
return ApiResult.Ok(new
|
||||
{
|
||||
total = pzs.Count,
|
||||
producing = pzs.Count(p => p.Status == 1),
|
||||
notStarted = pzs.Count(p => p.Status == 0),
|
||||
paused = pzs.Count(p => p.Status == 2),
|
||||
finished = pzs.Count(p => p.Status == 3),
|
||||
items
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>构建单个工段进度(rate = actual / base * 100,封顶 100)</summary>
|
||||
private static object Stage(string key, string name, string flow, decimal actual, decimal baseVal)
|
||||
{
|
||||
var rate = baseVal > 0 ? Math.Round(Math.Min(100m, actual / baseVal * 100m), 1) : 0m;
|
||||
return new
|
||||
{
|
||||
key,
|
||||
name,
|
||||
flow,
|
||||
actual = Math.Round(actual, 1),
|
||||
@base = Math.Round(baseVal, 1),
|
||||
rate
|
||||
};
|
||||
}
|
||||
|
||||
private static string ZhuangkouStatusText(int status) => status switch
|
||||
{
|
||||
0 => "未投产",
|
||||
1 => "生产中",
|
||||
2 => "已暂停",
|
||||
3 => "已完成",
|
||||
_ => "已关闭"
|
||||
};
|
||||
|
||||
private static string WorkOrderStatusText(int status) => status switch
|
||||
{
|
||||
0 => "待下发",
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
using System.Threading.Tasks;
|
||||
using F9MES.Application.Workflow;
|
||||
using F9MES.Common.Auth;
|
||||
using F9MES.Common.Result;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
/// <summary>
|
||||
/// 简易工作流 API:流程定义、发起、待办、审批、实例查询
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[Route("api/workflow")]
|
||||
public class WorkflowController : ControllerBase
|
||||
{
|
||||
private readonly WorkflowService _workflow;
|
||||
private readonly CurrentUserService _currentUser;
|
||||
|
||||
public WorkflowController(WorkflowService workflow, CurrentUserService currentUser)
|
||||
{
|
||||
_workflow = workflow;
|
||||
_currentUser = currentUser;
|
||||
}
|
||||
|
||||
/// <summary>已发布流程定义列表(可按业务类型过滤)</summary>
|
||||
[HttpGet("definitions")]
|
||||
public async Task<ActionResult> GetDefinitions([FromQuery] string? bizType)
|
||||
{
|
||||
var list = await _workflow.GetDefinitionsAsync(bizType);
|
||||
return Ok(ApiResult.Ok(list));
|
||||
}
|
||||
|
||||
/// <summary>发起流程</summary>
|
||||
[HttpPost("start")]
|
||||
public async Task<ActionResult> Start(WorkflowStartInput input)
|
||||
{
|
||||
var instanceId = await _workflow.StartAsync(input, _currentUser.UserId);
|
||||
return Ok(ApiResult.Ok(new { instanceId }, "流程发起成功"));
|
||||
}
|
||||
|
||||
/// <summary>我的待办(分页)</summary>
|
||||
[HttpGet("todos")]
|
||||
public async Task<ActionResult> GetMyTodos([FromQuery] int page = 1, [FromQuery] int size = 20)
|
||||
{
|
||||
var (items, total) = await _workflow.GetMyTodosAsync(_currentUser.UserId, page, size);
|
||||
return Ok(ApiResult.Ok(PageResult<WorkflowTodoDto>.From(page, size, total, items)));
|
||||
}
|
||||
|
||||
/// <summary>同意</summary>
|
||||
[HttpPost("approve")]
|
||||
public async Task<ActionResult> Approve(WorkflowHandleInput input)
|
||||
{
|
||||
await _workflow.ApproveAsync(input, _currentUser.UserId);
|
||||
return Ok(ApiResult.Ok(null, "已同意"));
|
||||
}
|
||||
|
||||
/// <summary>驳回</summary>
|
||||
[HttpPost("reject")]
|
||||
public async Task<ActionResult> Reject(WorkflowHandleInput input)
|
||||
{
|
||||
await _workflow.RejectAsync(input, _currentUser.UserId);
|
||||
return Ok(ApiResult.Ok(null, "已驳回"));
|
||||
}
|
||||
|
||||
/// <summary>我发起的实例(分页)</summary>
|
||||
[HttpGet("instances")]
|
||||
public async Task<ActionResult> GetMyInstances([FromQuery] int page = 1, [FromQuery] int size = 20, [FromQuery] int? status = null)
|
||||
{
|
||||
var (items, total) = await _workflow.GetMyInstancesAsync(_currentUser.UserId, page, size, status);
|
||||
return Ok(ApiResult.Ok(PageResult<WorkflowInstanceDto>.From(page, size, total, items)));
|
||||
}
|
||||
|
||||
/// <summary>实例详情(含任务轨迹)</summary>
|
||||
[HttpGet("instance/{id:long}")]
|
||||
public async Task<ActionResult> GetInstanceDetail([FromRoute] long id)
|
||||
{
|
||||
var detail = await _workflow.GetInstanceDetailAsync(id);
|
||||
if (detail == null) return Ok(ApiResult.Error("流程实例不存在"));
|
||||
return Ok(ApiResult.Ok(detail));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user