feat: 新增IM即时通讯(浮窗)、工作流、打印模块及工作台增强
- IM: 新增浮窗聊天(ImFloatWindow)、管理页(monitor/config/service/message)、SSE推送 - 工作流: 新增待办/我的流程页面及后端服务 - 打印: 新增打印模板、出库单打印(PrintPage)、模板种子脚本 - 工作台: 增强快捷入口与工作台数据 - 修复: TagsView页签关闭、CrudPage通用表格增强 - 移除导航菜单中的即时通讯入口,改为右下角浮窗
This commit is contained in:
@@ -11,4 +11,8 @@
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
using F9MES.Common.Result;
|
||||
using F9MES.Domain.BaseSys;
|
||||
using F9MES.Domain.Common;
|
||||
using F9MES.Domain.Im;
|
||||
using FreeSql;
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
|
||||
namespace F9MES.Application.Im;
|
||||
|
||||
/// <summary>IM 系统管理:性能监测 / 服务监测 / 配置管理 / 消息管理</summary>
|
||||
public class ImAdminService
|
||||
{
|
||||
private readonly IFreeSql _db;
|
||||
private readonly ImService _im;
|
||||
|
||||
public ImAdminService(IFreeSql db, ImService im)
|
||||
{
|
||||
_db = db;
|
||||
_im = im;
|
||||
}
|
||||
|
||||
// ==================== 性能监测 ====================
|
||||
|
||||
public async Task<object> PerformanceAsync()
|
||||
{
|
||||
var now = DateTime.Now;
|
||||
var todayStart = now.Date;
|
||||
var weekStart = todayStart.AddDays(-6);
|
||||
|
||||
var total = await _db.Select<Common_Message>().Where(m => m.Flag == 1).CountAsync();
|
||||
var today = await _db.Select<Common_Message>()
|
||||
.Where(m => m.Flag == 1 && m.SendTime >= todayStart).CountAsync();
|
||||
var unread = await _db.Select<Common_Message>()
|
||||
.Where(m => m.Flag == 1 && m.IsRead == 0).CountAsync();
|
||||
|
||||
// 近 7 天消息趋势
|
||||
var weekRows = await _db.Select<Common_Message>()
|
||||
.Where(m => m.Flag == 1 && m.SendTime >= weekStart)
|
||||
.GroupBy(m => m.SendTime.Date)
|
||||
.ToListAsync(g => new { Day = g.Key, Count = g.Count() });
|
||||
var trendMap = weekRows.ToDictionary(x => x.Day.Date, x => x.Count);
|
||||
var trend = new List<object>();
|
||||
for (var d = weekStart; d <= todayStart; d = d.AddDays(1))
|
||||
trend.Add(new { date = d.ToString("MM-dd"), count = trendMap.TryGetValue(d, out var c) ? c : 0 });
|
||||
|
||||
// 消息类型分布
|
||||
var typeRows = await _db.Select<Common_Message>()
|
||||
.Where(m => m.Flag == 1)
|
||||
.GroupBy(m => m.MsgType)
|
||||
.ToListAsync(g => new { Type = g.Key, Count = g.Count() });
|
||||
var typeNames = new Dictionary<int, string>
|
||||
{
|
||||
{ 0, "系统通知" }, { 1, "业务提醒" }, { 2, "审批通知" }, { 3, "预警" }, { 4, "单聊消息" }
|
||||
};
|
||||
var typeDist = typeRows.Select(x => new { type = x.Type, name = typeNames.GetValueOrDefault(x.Type, $"类型{x.Type}"), count = x.Count }).ToList();
|
||||
|
||||
// 会话数:去重 发送人+接收人 组合(系统群发 SenderId=0 不参与)
|
||||
var pairs = await _db.Select<Common_Message>()
|
||||
.Where(m => m.Flag == 1 && m.SenderId != 0)
|
||||
.ToListAsync(m => new { A = m.SenderId < m.UserId ? m.SenderId : m.UserId, B = m.SenderId < m.UserId ? m.UserId : m.SenderId });
|
||||
var sessionCount = pairs.Select(p => $"{p.A}-{p.B}").Distinct().Count();
|
||||
|
||||
// 平均消息长度
|
||||
var bodyLen = await _db.Select<Common_Message>()
|
||||
.Where(m => m.Flag == 1 && m.Content != null)
|
||||
.ToListAsync(m => m.Content ?? "");
|
||||
var avgLen = bodyLen.Count == 0 ? 0 : (int)Math.Round(bodyLen.Average(s => Encoding.UTF8.GetByteCount(s)));
|
||||
|
||||
return new
|
||||
{
|
||||
total,
|
||||
today,
|
||||
unread,
|
||||
online = _im.OnlineCount(),
|
||||
sessions = sessionCount,
|
||||
avgLen,
|
||||
trend,
|
||||
typeDist,
|
||||
updatedAt = now.ToString("yyyy-MM-dd HH:mm:ss"),
|
||||
};
|
||||
}
|
||||
|
||||
// ==================== 服务监测 ====================
|
||||
|
||||
public async Task<object> ServiceStatusAsync()
|
||||
{
|
||||
var dbOk = false;
|
||||
string dbDetail = "";
|
||||
try
|
||||
{
|
||||
await _db.Ado.ExecuteScalarAsync("SELECT 1");
|
||||
dbOk = true;
|
||||
dbDetail = _db.Ado.DataType.ToString();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
dbDetail = ex.Message;
|
||||
}
|
||||
|
||||
var lastMsg = await _db.Select<Common_Message>()
|
||||
.Where(m => m.Flag == 1)
|
||||
.OrderByDescending(m => m.SendTime)
|
||||
.FirstAsync(m => m.SendTime);
|
||||
var total = await _db.Select<Common_Message>().Where(m => m.Flag == 1).CountAsync();
|
||||
var startTime = Process.GetCurrentProcess().StartTime;
|
||||
var uptime = DateTime.Now - startTime;
|
||||
var tableCountObj = await _db.Ado.ExecuteScalarAsync(
|
||||
"SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE()");
|
||||
var tableCount = tableCountObj == null ? 0 : Convert.ToInt64(tableCountObj);
|
||||
|
||||
return new
|
||||
{
|
||||
apiStatus = "正常",
|
||||
apiDetail = "HTTP API 响应正常",
|
||||
dbStatus = dbOk ? "正常" : "异常",
|
||||
dbDetail,
|
||||
dbName = "f9web(MySQL)",
|
||||
startTime = startTime.ToString("yyyy-MM-dd HH:mm:ss"),
|
||||
uptime = $"{uptime.Days}天{uptime.Hours}小时{uptime.Minutes}分",
|
||||
lastMsgTime = lastMsg == default ? "" : lastMsg.ToString("yyyy-MM-dd HH:mm:ss"),
|
||||
totalMessages = total,
|
||||
online = _im.OnlineCount(),
|
||||
serverTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"),
|
||||
tableCount,
|
||||
};
|
||||
}
|
||||
|
||||
// ==================== 配置管理 ====================
|
||||
|
||||
private static List<ImConfigInput> DefaultConfigs() => new()
|
||||
{
|
||||
new() { Key = "pollInterval", Name = "轮询间隔(秒)", Value = "5", },
|
||||
new() { Key = "retainDays", Name = "消息保留天数", Value = "30", },
|
||||
new() { Key = "defaultTitle", Name = "默认公告标题", Value = "系统公告", },
|
||||
new() { Key = "maxBody", Name = "消息内容最大长度", Value = "500", },
|
||||
};
|
||||
|
||||
/// <summary>读取配置(含默认值,数据库已存的覆盖)</summary>
|
||||
public async Task<List<ImConfigInput>> GetConfigAsync()
|
||||
{
|
||||
var rows = await _db.Select<Im_Config>().ToListAsync();
|
||||
var map = rows.ToDictionary(r => r.Key, r => new ImConfigInput { Key = r.Key, Value = r.Value, Name = r.Name });
|
||||
var list = DefaultConfigs();
|
||||
foreach (var item in list)
|
||||
if (map.TryGetValue(item.Key, out var saved))
|
||||
item.Value = saved.Value;
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <summary>保存配置(按 Key 更新或插入)</summary>
|
||||
public async Task SaveConfigAsync(List<ImConfigInput> inputs)
|
||||
{
|
||||
if (inputs == null || inputs.Count == 0) return;
|
||||
foreach (var i in inputs)
|
||||
{
|
||||
var exists = await _db.Select<Im_Config>().Where(c => c.Key == i.Key).FirstAsync();
|
||||
if (exists == null)
|
||||
{
|
||||
await _db.Insert(new Im_Config { Key = i.Key, Value = i.Value, Name = i.Name, Flag = 1 }).ExecuteAffrowsAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
await _db.Update<Im_Config>()
|
||||
.Set(c => c.Value, i.Value)
|
||||
.Set(c => c.Name, i.Name)
|
||||
.Set(c => c.UpdateTime, DateTime.Now)
|
||||
.Where(c => c.Key == i.Key)
|
||||
.ExecuteAffrowsAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 消息管理 ====================
|
||||
|
||||
/// <summary>消息管理分页(msgType=-1 全部;kw 匹配标题/内容)</summary>
|
||||
public async Task<PageResult<ImAdminMessageDto>> GetMessagesAsync(string? kw, int msgType, int page, int size)
|
||||
{
|
||||
var q = _db.Select<Common_Message>().Where(m => m.Flag == 1);
|
||||
if (msgType >= 0) q = q.Where(m => m.MsgType == msgType);
|
||||
if (!string.IsNullOrWhiteSpace(kw))
|
||||
q = q.Where(m => m.Title.Contains(kw) || (m.Content != null && m.Content.Contains(kw)));
|
||||
q = q.OrderByDescending(m => m.SendTime);
|
||||
|
||||
var total = await q.CountAsync();
|
||||
var rows = await q.Skip((page - 1) * size).Take(size)
|
||||
.ToListAsync(m => new { m.Id, m.SenderId, m.SenderName, m.UserId, m.Title, m.Content, m.MsgType, m.IsRead, m.SendTime });
|
||||
|
||||
var userIds = rows.Select(r => r.UserId).Distinct().ToList();
|
||||
var users = await _db.Select<BaseSys_User>()
|
||||
.Where(u => userIds.Contains(u.Id))
|
||||
.ToListAsync(u => new { u.Id, u.Name });
|
||||
var nameMap = users.ToDictionary(u => u.Id, u => u.Name);
|
||||
|
||||
var items = rows.Select(r => new ImAdminMessageDto
|
||||
{
|
||||
Id = r.Id,
|
||||
SenderId = r.SenderId,
|
||||
SenderName = r.SenderId == 0 ? "系统" : (r.SenderName ?? $"用户{r.SenderId}"),
|
||||
ReceiverId = r.UserId,
|
||||
ReceiverName = nameMap.TryGetValue(r.UserId, out var n) ? n : $"用户{r.UserId}",
|
||||
Title = r.Title,
|
||||
Content = r.Content ?? "",
|
||||
MsgType = r.MsgType,
|
||||
IsRead = r.IsRead,
|
||||
SendTime = r.SendTime,
|
||||
}).ToList();
|
||||
return PageResult<ImAdminMessageDto>.From(page, size, total, items);
|
||||
}
|
||||
|
||||
/// <summary>删除消息(软删)</summary>
|
||||
public async Task<int> DeleteMessageAsync(long id)
|
||||
{
|
||||
return await _db.Update<Common_Message>()
|
||||
.Set(m => m.Flag, 0)
|
||||
.Where(m => m.Id == id)
|
||||
.ExecuteAffrowsAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
namespace F9MES.Application.Im;
|
||||
|
||||
/// <summary>发送单聊消息入参</summary>
|
||||
public class ImSendInput
|
||||
{
|
||||
/// <summary>接收人用户ID</summary>
|
||||
public long PeerId { get; set; }
|
||||
|
||||
/// <summary>消息内容</summary>
|
||||
public string Content { get; set; } = "";
|
||||
}
|
||||
|
||||
/// <summary>标记已读入参</summary>
|
||||
public class ImReadInput
|
||||
{
|
||||
/// <summary>对端用户ID(标记与该用户的所有未读)</summary>
|
||||
public long PeerId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>系统/业务通知入参(群发)</summary>
|
||||
public class ImNotifyInput
|
||||
{
|
||||
/// <summary>接收人用户ID列表(空=全部启用用户)</summary>
|
||||
public List<long>? UserIds { get; set; }
|
||||
|
||||
/// <summary>消息类型:0=系统通知 1=业务提醒 2=审批通知 3=预警</summary>
|
||||
public int MsgType { get; set; } = 0;
|
||||
|
||||
/// <summary>标题</summary>
|
||||
public string Title { get; set; } = "";
|
||||
|
||||
/// <summary>内容</summary>
|
||||
public string Content { get; set; } = "";
|
||||
}
|
||||
|
||||
/// <summary>会话条目</summary>
|
||||
public class ImSessionDto
|
||||
{
|
||||
public long PeerId { get; set; }
|
||||
public string PeerName { get; set; } = "";
|
||||
public string? LastContent { get; set; }
|
||||
public DateTime LastTime { get; set; }
|
||||
public int Unread { get; set; }
|
||||
public int MsgType { get; set; }
|
||||
/// <summary>是否系统通知/公告会话(PeerId=0)</summary>
|
||||
public bool IsSystem { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>消息条目</summary>
|
||||
public class ImMessageDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public long SenderId { get; set; }
|
||||
public string SenderName { get; set; } = "";
|
||||
public long ReceiverId { get; set; }
|
||||
public string Content { get; set; } = "";
|
||||
/// <summary>标题(系统通知/公告标题)</summary>
|
||||
public string? Title { get; set; }
|
||||
public int MsgType { get; set; }
|
||||
public DateTime SendTime { get; set; }
|
||||
/// <summary>是否我发出的</summary>
|
||||
public bool Mine { get; set; }
|
||||
/// <summary>是否系统通知/公告消息</summary>
|
||||
public bool IsSystem { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>IM 配置项(读写入参/返回)</summary>
|
||||
public class ImConfigInput
|
||||
{
|
||||
/// <summary>配置键</summary>
|
||||
public string Key { get; set; } = "";
|
||||
|
||||
/// <summary>配置值</summary>
|
||||
public string Value { get; set; } = "";
|
||||
|
||||
/// <summary>配置名称</summary>
|
||||
public string Name { get; set; } = "";
|
||||
}
|
||||
|
||||
/// <summary>IM 管理消息条目</summary>
|
||||
public class ImAdminMessageDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
/// <summary>发送人ID(0=系统)</summary>
|
||||
public long SenderId { get; set; }
|
||||
/// <summary>发送人姓名</summary>
|
||||
public string SenderName { get; set; } = "";
|
||||
/// <summary>接收人ID</summary>
|
||||
public long ReceiverId { get; set; }
|
||||
/// <summary>接收人姓名</summary>
|
||||
public string ReceiverName { get; set; } = "";
|
||||
/// <summary>标题</summary>
|
||||
public string Title { get; set; } = "";
|
||||
/// <summary>内容</summary>
|
||||
public string Content { get; set; } = "";
|
||||
/// <summary>消息类型:0=系统 1=业务 2=审批 3=预警 4=单聊</summary>
|
||||
public int MsgType { get; set; }
|
||||
/// <summary>是否已读</summary>
|
||||
public int IsRead { get; set; }
|
||||
/// <summary>发送时间</summary>
|
||||
public DateTime SendTime { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace F9MES.Application.Im;
|
||||
|
||||
/// <summary>
|
||||
/// IM SSE 长连接中枢(单例):
|
||||
/// 维护 userId -> 连接集合,向在线客户端实时推送新消息/通知。
|
||||
/// 客户端通过 GET /api/im/events 订阅,服务端在消息落库后推送。
|
||||
/// </summary>
|
||||
public class ImEventHub
|
||||
{
|
||||
private readonly ConcurrentDictionary<long, List<ImSseClient>> _clients = new();
|
||||
private static readonly JsonSerializerOptions JsonOpts = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
};
|
||||
|
||||
/// <summary>当前已建立 SSE 长连接的用户数</summary>
|
||||
public int OnlineCount => _clients.Count;
|
||||
|
||||
/// <summary>订阅:注册连接并保持到请求取消(客户端断开/服务端关闭)</summary>
|
||||
public async Task SubscribeAsync(long userId, HttpResponse response, CancellationToken ct)
|
||||
{
|
||||
var client = new ImSseClient(userId, response);
|
||||
var list = _clients.GetOrAdd(userId, _ => new List<ImSseClient>());
|
||||
lock (list) list.Add(client);
|
||||
try
|
||||
{
|
||||
// 握手事件:客户端可据此确认连接成功
|
||||
await client.WriteEventAsync("connected", "{}");
|
||||
// 心跳:每 20s 发注释行,防止代理/网关超时断开
|
||||
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(20));
|
||||
try
|
||||
{
|
||||
while (await timer.WaitForNextTickAsync(ct))
|
||||
await client.WriteCommentAsync("ping");
|
||||
}
|
||||
catch (OperationCanceledException) { /* 客户端断开 */ }
|
||||
}
|
||||
finally
|
||||
{
|
||||
lock (list) list.Remove(client);
|
||||
if (list.Count == 0) _clients.TryRemove(userId, out _);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>向指定用户推送事件</summary>
|
||||
public async Task PushToUserAsync(long userId, object data)
|
||||
{
|
||||
var payload = JsonSerializer.Serialize(data, JsonOpts);
|
||||
await PushRawAsync(userId, payload);
|
||||
}
|
||||
|
||||
/// <summary>向多个用户推送同一事件(按用户去重)</summary>
|
||||
public async Task PushToUsersAsync(IEnumerable<long> userIds, object data)
|
||||
{
|
||||
var payload = JsonSerializer.Serialize(data, JsonOpts);
|
||||
var done = new HashSet<long>();
|
||||
foreach (var uid in userIds)
|
||||
{
|
||||
if (done.Add(uid)) await PushRawAsync(uid, payload);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task PushRawAsync(long userId, string payload)
|
||||
{
|
||||
if (!_clients.TryGetValue(userId, out var list)) return;
|
||||
List<ImSseClient> snapshot;
|
||||
lock (list) snapshot = list.ToList();
|
||||
foreach (var c in snapshot)
|
||||
await c.WriteEventAsync("message", payload);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>单个 SSE 连接(写入失败即视为已断开,静默忽略)</summary>
|
||||
public class ImSseClient
|
||||
{
|
||||
public long UserId { get; }
|
||||
private readonly HttpResponse _response;
|
||||
|
||||
public ImSseClient(long userId, HttpResponse response)
|
||||
{
|
||||
UserId = userId;
|
||||
_response = response;
|
||||
}
|
||||
|
||||
public async Task WriteEventAsync(string eventName, string data)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _response.WriteAsync($"event: {eventName}\ndata: {data}\n\n");
|
||||
await _response.Body.FlushAsync();
|
||||
}
|
||||
catch { /* 连接已断开 */ }
|
||||
}
|
||||
|
||||
public async Task WriteCommentAsync(string text)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _response.WriteAsync($": {text}\n\n");
|
||||
await _response.Body.FlushAsync();
|
||||
}
|
||||
catch { /* 忽略 */ }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
using F9MES.Common.Auth;
|
||||
using F9MES.Common.Cache;
|
||||
using F9MES.Domain.Common;
|
||||
using F9MES.Domain.BaseSys;
|
||||
using FreeSql;
|
||||
|
||||
namespace F9MES.Application.Im;
|
||||
|
||||
/// <summary>轻量级 IM:单聊消息 + 系统通知</summary>
|
||||
public class ImService
|
||||
{
|
||||
private const string ActiveKeyPrefix = "im:active:";
|
||||
private readonly IFreeSql _db;
|
||||
private readonly CurrentUserService _currentUser;
|
||||
private readonly CacheService _cache;
|
||||
private readonly ImEventHub _hub;
|
||||
|
||||
public ImService(IFreeSql db, CurrentUserService currentUser, CacheService cache, ImEventHub hub)
|
||||
{
|
||||
_db = db;
|
||||
_currentUser = currentUser;
|
||||
_cache = cache;
|
||||
_hub = hub;
|
||||
}
|
||||
|
||||
/// <summary>记录当前用户活跃时间(SSE 订阅 / 接口调用时触发)</summary>
|
||||
public void TouchActive()
|
||||
{
|
||||
var me = _currentUser.UserId;
|
||||
if (me > 0) _cache.Set(ActiveKeyPrefix + me, DateTime.Now, 60);
|
||||
}
|
||||
|
||||
/// <summary>当前在线用户数(SSE 长连接数为准,缓存活跃估算兜底)</summary>
|
||||
public int OnlineCount() => Math.Max(_hub.OnlineCount, _cache.CountByPrefix(ActiveKeyPrefix));
|
||||
|
||||
/// <summary>会话列表:按对端聚合最后一条消息 + 未读数(系统通知/公告聚合为 peerId=0 会话并置顶)</summary>
|
||||
public async Task<List<ImSessionDto>> GetSessionsAsync()
|
||||
{
|
||||
TouchActive();
|
||||
var me = _currentUser.UserId;
|
||||
var msgs = await _db.Select<Common_Message>()
|
||||
.Where(m => m.Flag == 1 && (m.UserId == me || m.SenderId == me))
|
||||
.OrderByDescending(m => m.SendTime)
|
||||
.Limit(500)
|
||||
.ToListAsync(m => new { m.Id, m.SenderId, m.UserId, m.Content, m.Title, m.SendTime, m.MsgType, m.IsRead });
|
||||
|
||||
// 按对端聚合(系统通知 SenderId=0 统一聚合为 peerId=0 的"系统通知"会话)
|
||||
var dict = new Dictionary<long, ImSessionDto>();
|
||||
foreach (var m in msgs)
|
||||
{
|
||||
long peerId;
|
||||
bool isSystem;
|
||||
if (m.SenderId == 0)
|
||||
{
|
||||
if (m.UserId != me) continue; // 只看发给我的系统通知
|
||||
peerId = 0;
|
||||
isSystem = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
peerId = m.SenderId == me ? m.UserId : m.SenderId;
|
||||
if (peerId == me || peerId == 0) continue; // 跳过自聊
|
||||
isSystem = false;
|
||||
}
|
||||
|
||||
if (!dict.TryGetValue(peerId, out var s))
|
||||
{
|
||||
s = new ImSessionDto
|
||||
{
|
||||
PeerId = peerId,
|
||||
PeerName = isSystem ? "系统通知" : "",
|
||||
IsSystem = isSystem,
|
||||
};
|
||||
dict[peerId] = s;
|
||||
}
|
||||
if (s.LastTime < m.SendTime)
|
||||
{
|
||||
s.LastTime = m.SendTime;
|
||||
s.LastContent = isSystem && !string.IsNullOrEmpty(m.Title) ? m.Title : m.Content;
|
||||
s.MsgType = m.MsgType;
|
||||
}
|
||||
// 未读:接收者是我且未读
|
||||
if (m.UserId == me && m.IsRead == 0)
|
||||
s.Unread++;
|
||||
}
|
||||
|
||||
var sessions = dict.Values
|
||||
.OrderBy(s => s.IsSystem ? 0 : 1) // 系统通知/公告置顶
|
||||
.ThenByDescending(s => s.LastTime)
|
||||
.ToList();
|
||||
// 补全对端用户姓名(系统会话除外)
|
||||
var peerIds = sessions.Where(s => !s.IsSystem).Select(s => s.PeerId).Distinct().ToList();
|
||||
if (peerIds.Count > 0)
|
||||
{
|
||||
var users = await _db.Select<BaseSys_User>()
|
||||
.Where(u => peerIds.Contains(u.Id))
|
||||
.ToListAsync(u => new { u.Id, u.Name });
|
||||
var nameMap = users.ToDictionary(u => u.Id, u => u.Name);
|
||||
foreach (var s in sessions)
|
||||
if (!s.IsSystem)
|
||||
s.PeerName = nameMap.TryGetValue(s.PeerId, out var n) ? n : $"用户{s.PeerId}";
|
||||
}
|
||||
return sessions;
|
||||
}
|
||||
|
||||
/// <summary>聊天记录(分页;peerId=0 时查询系统通知/公告)</summary>
|
||||
public async Task<(List<ImMessageDto> Items, int Total)> GetMessagesAsync(long peerId, int page, int size)
|
||||
{
|
||||
var me = _currentUser.UserId;
|
||||
var query = _db.Select<Common_Message>().Where(m => m.Flag == 1);
|
||||
if (peerId == 0)
|
||||
{
|
||||
// 系统通知/公告:发给我的系统消息
|
||||
query = query.Where(m => m.SenderId == 0 && m.UserId == me);
|
||||
}
|
||||
else
|
||||
{
|
||||
query = query.Where(m =>
|
||||
(m.UserId == me && m.SenderId == peerId) || (m.UserId == peerId && m.SenderId == me));
|
||||
}
|
||||
query = query.OrderByDescending(m => m.SendTime);
|
||||
|
||||
var total = await query.CountAsync();
|
||||
var rows = await query.Skip((page - 1) * size).Take(size)
|
||||
.ToListAsync(m => new { m.Id, m.SenderId, m.UserId, m.Content, m.Title, m.SendTime, m.MsgType });
|
||||
|
||||
// 补全对端姓名(系统通知对端即"系统通知")
|
||||
string peerName = "系统通知";
|
||||
if (peerId != 0)
|
||||
{
|
||||
var peer = await _db.Select<BaseSys_User>().Where(u => u.Id == peerId).FirstAsync();
|
||||
peerName = string.IsNullOrEmpty(peer?.Name) ? $"用户{peerId}" : peer.Name;
|
||||
}
|
||||
|
||||
var items = rows.Select(m => new ImMessageDto
|
||||
{
|
||||
Id = m.Id,
|
||||
SenderId = m.SenderId,
|
||||
SenderName = m.SenderId == me ? "我" : peerName,
|
||||
ReceiverId = m.UserId,
|
||||
Content = m.Content ?? "",
|
||||
Title = m.Title,
|
||||
MsgType = m.MsgType,
|
||||
IsSystem = m.SenderId == 0,
|
||||
SendTime = m.SendTime,
|
||||
Mine = m.SenderId == me
|
||||
}).ToList();
|
||||
return (items, (int)total);
|
||||
}
|
||||
|
||||
/// <summary>发送单聊消息(落库后向接收方 SSE 实时推送)</summary>
|
||||
public async Task SendAsync(long peerId, string content)
|
||||
{
|
||||
var me = _currentUser.UserId;
|
||||
if (peerId == me) throw new InvalidOperationException("不能给自己发送消息");
|
||||
if (string.IsNullOrWhiteSpace(content)) throw new InvalidOperationException("消息内容不能为空");
|
||||
|
||||
var meName = _currentUser.User?.Name ?? "";
|
||||
var now = DateTime.Now;
|
||||
var id = await _db.Insert(new Common_Message
|
||||
{
|
||||
SenderId = me,
|
||||
SenderName = meName,
|
||||
UserId = peerId,
|
||||
Title = "单聊消息",
|
||||
Content = content,
|
||||
MsgType = 4,
|
||||
IsRead = 0,
|
||||
SendTime = now,
|
||||
Flag = 1,
|
||||
Adder = me,
|
||||
}).ExecuteIdentityAsync();
|
||||
|
||||
// 实时推送:仅推给接收方(发送方本地已刷新)
|
||||
await _hub.PushToUserAsync(peerId, new ImMessageDto
|
||||
{
|
||||
Id = id,
|
||||
SenderId = me,
|
||||
SenderName = meName,
|
||||
ReceiverId = peerId,
|
||||
Title = "单聊消息",
|
||||
Content = content,
|
||||
MsgType = 4,
|
||||
IsSystem = false,
|
||||
SendTime = now,
|
||||
Mine = false,
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>标记会话已读(peerId=0 时标记全部系统通知已读)</summary>
|
||||
public async Task ReadAsync(long peerId)
|
||||
{
|
||||
var me = _currentUser.UserId;
|
||||
var q = _db.Update<Common_Message>().Set(m => m.IsRead, 1);
|
||||
if (peerId == 0)
|
||||
q = q.Where(m => m.Flag == 1 && m.UserId == me && m.SenderId == 0 && m.IsRead == 0);
|
||||
else
|
||||
q = q.Where(m => m.Flag == 1 && m.UserId == me && m.SenderId == peerId && m.IsRead == 0);
|
||||
await q.ExecuteAffrowsAsync();
|
||||
}
|
||||
|
||||
/// <summary>未读消息总数(顶栏角标)</summary>
|
||||
public async Task<int> GetUnreadCountAsync()
|
||||
{
|
||||
TouchActive();
|
||||
var me = _currentUser.UserId;
|
||||
return (int)await _db.Select<Common_Message>()
|
||||
.Where(m => m.Flag == 1 && m.UserId == me && m.IsRead == 0)
|
||||
.CountAsync();
|
||||
}
|
||||
|
||||
/// <summary>系统/业务通知群发:给指定用户(空=全部启用用户)</summary>
|
||||
public async Task<int> NotifyAsync(ImNotifyInput input)
|
||||
{
|
||||
var me = _currentUser.UserId;
|
||||
var title = string.IsNullOrEmpty(input.Title) ? "系统通知" : input.Title;
|
||||
List<long> userIds;
|
||||
if (input.UserIds is { Count: > 0 })
|
||||
userIds = input.UserIds;
|
||||
else
|
||||
userIds = await _db.Select<BaseSys_User>()
|
||||
.Where(u => u.Flag == 1 && u.Status == 1)
|
||||
.ToListAsync(u => u.Id);
|
||||
|
||||
if (userIds.Count == 0) return 0;
|
||||
|
||||
var now = DateTime.Now;
|
||||
var msgs = userIds.Select(uid => new Common_Message
|
||||
{
|
||||
SenderId = 0,
|
||||
SenderName = "系统",
|
||||
UserId = uid,
|
||||
Title = title,
|
||||
Content = input.Content,
|
||||
MsgType = input.MsgType,
|
||||
IsRead = 0,
|
||||
SendTime = now,
|
||||
Flag = 1,
|
||||
Adder = me,
|
||||
}).ToList();
|
||||
await _db.Insert(msgs).ExecuteAffrowsAsync();
|
||||
|
||||
// 实时推送:给每个接收用户推送一条消息事件
|
||||
foreach (var uid in userIds)
|
||||
{
|
||||
await _hub.PushToUserAsync(uid, new ImMessageDto
|
||||
{
|
||||
Id = 0,
|
||||
SenderId = 0,
|
||||
SenderName = "系统",
|
||||
ReceiverId = uid,
|
||||
Title = title,
|
||||
Content = input.Content ?? "",
|
||||
MsgType = input.MsgType,
|
||||
IsSystem = true,
|
||||
SendTime = now,
|
||||
Mine = false,
|
||||
});
|
||||
}
|
||||
return msgs.Count;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace F9MES.Application.Workflow;
|
||||
|
||||
/// <summary>发起流程入参</summary>
|
||||
public class WorkflowStartInput
|
||||
{
|
||||
/// <summary>流程定义ID</summary>
|
||||
public long WorkflowId { get; set; }
|
||||
|
||||
/// <summary>关联业务类型(缺省用流程定义的类型)</summary>
|
||||
public string? BizType { get; set; }
|
||||
|
||||
/// <summary>关联业务记录ID</summary>
|
||||
public long BizId { get; set; }
|
||||
|
||||
/// <summary>关联业务单号</summary>
|
||||
public string? BillNo { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>审批/驳回入参</summary>
|
||||
public class WorkflowHandleInput
|
||||
{
|
||||
/// <summary>任务ID</summary>
|
||||
public long TaskId { get; set; }
|
||||
|
||||
/// <summary>处理意见</summary>
|
||||
public string? Comment { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>流程定义(含节点)</summary>
|
||||
public class WorkflowDefinitionDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string Name { get; set; } = "";
|
||||
public string? Code { get; set; }
|
||||
public string BizType { get; set; } = "";
|
||||
public List<WorkflowNodeDto> Nodes { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>流程节点</summary>
|
||||
public class WorkflowNodeDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string Name { get; set; } = "";
|
||||
/// <summary>0=开始 1=审批 2=抄送 3=条件 4=结束</summary>
|
||||
public int NodeType { get; set; }
|
||||
public string? ApproverJson { get; set; }
|
||||
public int Sort { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>待办项</summary>
|
||||
public class WorkflowTodoDto
|
||||
{
|
||||
public long TaskId { get; set; }
|
||||
public long InstanceId { get; set; }
|
||||
public long NodeId { get; set; }
|
||||
public string WorkflowName { get; set; } = "";
|
||||
public string NodeName { get; set; } = "";
|
||||
public string BizType { get; set; } = "";
|
||||
public string? BillNo { get; set; }
|
||||
public long BizId { get; set; }
|
||||
public string StartUserName { get; set; } = "";
|
||||
public DateTime StartTime { get; set; }
|
||||
public string? Comment { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>实例列表项</summary>
|
||||
public class WorkflowInstanceDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string WorkflowName { get; set; } = "";
|
||||
public string BizType { get; set; } = "";
|
||||
public string? BillNo { get; set; }
|
||||
public long BizId { get; set; }
|
||||
/// <summary>0=进行中 1=已通过 2=已驳回 3=已撤销</summary>
|
||||
public int Status { get; set; }
|
||||
/// <summary>当前节点名称</summary>
|
||||
public string NodeName { get; set; } = "";
|
||||
public DateTime StartTime { get; set; }
|
||||
public DateTime? EndTime { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>实例详情(含任务轨迹)</summary>
|
||||
public class WorkflowInstanceDetailDto : WorkflowInstanceDto
|
||||
{
|
||||
public List<WorkflowTaskDto> Tasks { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>任务轨迹项</summary>
|
||||
public class WorkflowTaskDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public long NodeId { get; set; }
|
||||
public string NodeName { get; set; } = "";
|
||||
public long UserId { get; set; }
|
||||
public string UserName { get; set; } = "";
|
||||
public string? Comment { get; set; }
|
||||
/// <summary>0=待处理 1=已同意 2=已驳回</summary>
|
||||
public int Status { get; set; }
|
||||
public DateTime? HandleTime { get; set; }
|
||||
public DateTime AddTime { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using FreeSql;
|
||||
using F9MES.Domain.BaseSys;
|
||||
using F9MES.Domain.Common;
|
||||
|
||||
namespace F9MES.Application.Workflow;
|
||||
|
||||
/// <summary>
|
||||
/// 简易工作流服务:流程定义查询、发起、待办、同意、驳回、实例查询
|
||||
/// 流程模型:Common_Workflow(定义) → Common_WorkflowNode(节点) → Common_WorkflowInstance(实例) → Common_WorkflowTask(任务)
|
||||
/// 节点流转:按 Sort 升序,发起后首个审批节点生成任务;同意后推进到下一审批节点,无则流程通过;驳回则流程终止。
|
||||
/// 审批人配置 ApproverJson:JSON 数组 ["1","2"](用户ID),v1 取首个用户处理。
|
||||
/// </summary>
|
||||
public class WorkflowService
|
||||
{
|
||||
private readonly IFreeSql _db;
|
||||
|
||||
public WorkflowService(IFreeSql db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
|
||||
/// <summary>已发布流程定义(含节点),可按业务类型过滤</summary>
|
||||
public async Task<List<WorkflowDefinitionDto>> GetDefinitionsAsync(string? bizType)
|
||||
{
|
||||
var flows = await _db.Select<Common_Workflow>()
|
||||
.Where(a => a.Flag > 0 && a.Status == 1)
|
||||
.WhereIf(!string.IsNullOrWhiteSpace(bizType), a => a.BizType == bizType)
|
||||
.OrderBy(a => a.Id)
|
||||
.ToListAsync();
|
||||
|
||||
var result = new List<WorkflowDefinitionDto>();
|
||||
foreach (var f in flows)
|
||||
{
|
||||
var nodes = await _db.Select<Common_WorkflowNode>()
|
||||
.Where(a => a.WorkflowId == f.Id && a.Flag > 0)
|
||||
.OrderBy(a => a.Sort)
|
||||
.ToListAsync();
|
||||
result.Add(new WorkflowDefinitionDto
|
||||
{
|
||||
Id = f.Id,
|
||||
Name = f.Name,
|
||||
Code = f.Code,
|
||||
BizType = f.BizType,
|
||||
Nodes = nodes.Select(n => new WorkflowNodeDto
|
||||
{
|
||||
Id = n.Id,
|
||||
Name = n.Name,
|
||||
NodeType = n.NodeType,
|
||||
ApproverJson = n.ApproverJson,
|
||||
Sort = n.Sort,
|
||||
}).ToList(),
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>发起流程:创建实例 + 首个审批任务</summary>
|
||||
public async Task<long> StartAsync(WorkflowStartInput input, long userId)
|
||||
{
|
||||
var flow = await _db.Select<Common_Workflow>()
|
||||
.Where(a => a.Id == input.WorkflowId && a.Flag > 0 && a.Status == 1)
|
||||
.FirstAsync();
|
||||
if (flow == null)
|
||||
throw new InvalidOperationException("流程不存在或未发布");
|
||||
|
||||
var nodes = await _db.Select<Common_WorkflowNode>()
|
||||
.Where(a => a.WorkflowId == flow.Id && a.Flag > 0)
|
||||
.OrderBy(a => a.Sort)
|
||||
.ToListAsync();
|
||||
var first = nodes.FirstOrDefault(a => a.NodeType == 1);
|
||||
if (first == null)
|
||||
throw new InvalidOperationException("流程未配置审批节点");
|
||||
|
||||
var inst = new Common_WorkflowInstance
|
||||
{
|
||||
WorkflowId = flow.Id,
|
||||
BillNo = input.BillNo,
|
||||
BizId = input.BizId,
|
||||
BizType = string.IsNullOrWhiteSpace(input.BizType) ? flow.BizType : input.BizType!,
|
||||
CurrentNodeId = first.Id,
|
||||
StartUserId = userId,
|
||||
Status = 0,
|
||||
StartTime = DateTime.Now,
|
||||
Flag = 1,
|
||||
Adder = userId,
|
||||
};
|
||||
// ExecuteIdentityAsync:返回自增主键并回填(ExecuteAffrows 不回填)
|
||||
inst.Id = await _db.Insert(inst).ExecuteIdentityAsync();
|
||||
|
||||
await CreateTaskAsync(inst.Id, first, userId);
|
||||
return inst.Id;
|
||||
}
|
||||
|
||||
/// <summary>同意:任务完成 → 推进下一审批节点,无则实例通过</summary>
|
||||
public async Task ApproveAsync(WorkflowHandleInput input, long userId)
|
||||
{
|
||||
var task = await _db.Select<Common_WorkflowTask>()
|
||||
.Where(a => a.Id == input.TaskId && a.Flag > 0 && a.Status == 0 && a.UserId == userId)
|
||||
.FirstAsync();
|
||||
if (task == null)
|
||||
throw new InvalidOperationException("任务不存在或无权处理");
|
||||
|
||||
task.Status = 1;
|
||||
task.Comment = input.Comment;
|
||||
task.HandleTime = DateTime.Now;
|
||||
task.Updater = userId;
|
||||
await _db.Update<Common_WorkflowTask>().SetSource(task).ExecuteAffrowsAsync();
|
||||
|
||||
var inst = await _db.Select<Common_WorkflowInstance>().Where(a => a.Id == task.InstanceId).FirstAsync();
|
||||
if (inst == null) return;
|
||||
|
||||
var nodes = await _db.Select<Common_WorkflowNode>()
|
||||
.Where(a => a.WorkflowId == inst.WorkflowId && a.Flag > 0)
|
||||
.OrderBy(a => a.Sort)
|
||||
.ToListAsync();
|
||||
var cur = nodes.FirstOrDefault(a => a.Id == task.NodeId);
|
||||
var next = cur == null ? null : nodes.FirstOrDefault(a => a.Sort > cur.Sort && a.NodeType == 1);
|
||||
|
||||
if (next == null)
|
||||
{
|
||||
inst.Status = 1; // 已通过
|
||||
inst.EndTime = DateTime.Now;
|
||||
inst.Updater = userId;
|
||||
await _db.Update<Common_WorkflowInstance>().SetSource(inst).ExecuteAffrowsAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
inst.CurrentNodeId = next.Id;
|
||||
inst.Updater = userId;
|
||||
await _db.Update<Common_WorkflowInstance>().SetSource(inst).ExecuteAffrowsAsync();
|
||||
await CreateTaskAsync(inst.Id, next, userId);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>驳回:任务驳回 → 实例终止</summary>
|
||||
public async Task RejectAsync(WorkflowHandleInput input, long userId)
|
||||
{
|
||||
var task = await _db.Select<Common_WorkflowTask>()
|
||||
.Where(a => a.Id == input.TaskId && a.Flag > 0 && a.Status == 0 && a.UserId == userId)
|
||||
.FirstAsync();
|
||||
if (task == null)
|
||||
throw new InvalidOperationException("任务不存在或无权处理");
|
||||
|
||||
task.Status = 2;
|
||||
task.Comment = input.Comment;
|
||||
task.HandleTime = DateTime.Now;
|
||||
task.Updater = userId;
|
||||
await _db.Update<Common_WorkflowTask>().SetSource(task).ExecuteAffrowsAsync();
|
||||
|
||||
var inst = await _db.Select<Common_WorkflowInstance>().Where(a => a.Id == task.InstanceId).FirstAsync();
|
||||
if (inst == null) return;
|
||||
|
||||
inst.Status = 2; // 已驳回
|
||||
inst.EndTime = DateTime.Now;
|
||||
inst.Updater = userId;
|
||||
await _db.Update<Common_WorkflowInstance>().SetSource(inst).ExecuteAffrowsAsync();
|
||||
}
|
||||
|
||||
/// <summary>我的待办(分页)</summary>
|
||||
public async Task<(List<WorkflowTodoDto> items, long total)> GetMyTodosAsync(long userId, int page, int size)
|
||||
{
|
||||
var query = _db.Select<Common_WorkflowTask, Common_WorkflowInstance, Common_Workflow, BaseSys_User>()
|
||||
.InnerJoin((t, i, f, u) => t.InstanceId == i.Id)
|
||||
.InnerJoin((t, i, f, u) => i.WorkflowId == f.Id)
|
||||
.LeftJoin((t, i, f, u) => i.StartUserId == u.Id)
|
||||
.Where((t, i, f, u) => t.Flag > 0 && t.UserId == userId && t.Status == 0 && i.Flag > 0);
|
||||
|
||||
var total = await query.CountAsync();
|
||||
var list = await query
|
||||
.OrderByDescending((t, i, f, u) => t.Id)
|
||||
.Skip((page - 1) * size)
|
||||
.Take(size)
|
||||
.ToListAsync((t, i, f, u) => new WorkflowTodoDto
|
||||
{
|
||||
TaskId = t.Id,
|
||||
InstanceId = i.Id,
|
||||
NodeId = t.NodeId,
|
||||
WorkflowName = f.Name,
|
||||
NodeName = "",
|
||||
BizType = i.BizType,
|
||||
BillNo = i.BillNo,
|
||||
BizId = i.BizId,
|
||||
StartUserName = u.Name,
|
||||
StartTime = i.StartTime,
|
||||
});
|
||||
|
||||
await FillNodeNamesAsync(list, a => a.NodeId, (a, n) => a.NodeName = n);
|
||||
return (list, total);
|
||||
}
|
||||
|
||||
/// <summary>我发起的实例(分页)</summary>
|
||||
public async Task<(List<WorkflowInstanceDto> items, long total)> GetMyInstancesAsync(long userId, int page, int size, int? status)
|
||||
{
|
||||
var query = _db.Select<Common_WorkflowInstance, Common_Workflow>()
|
||||
.InnerJoin((i, f) => i.WorkflowId == f.Id)
|
||||
.Where((i, f) => i.Flag > 0 && i.StartUserId == userId)
|
||||
.WhereIf(status.HasValue, (i, f) => i.Status == status);
|
||||
|
||||
var total = await query.CountAsync();
|
||||
var list = await query
|
||||
.OrderByDescending((i, f) => i.Id)
|
||||
.Skip((page - 1) * size)
|
||||
.Take(size)
|
||||
.ToListAsync((i, f) => new WorkflowInstanceDto
|
||||
{
|
||||
Id = i.Id,
|
||||
WorkflowName = f.Name,
|
||||
BizType = i.BizType,
|
||||
BillNo = i.BillNo,
|
||||
BizId = i.BizId,
|
||||
Status = i.Status,
|
||||
NodeName = "",
|
||||
StartTime = i.StartTime,
|
||||
EndTime = i.EndTime,
|
||||
});
|
||||
|
||||
// 当前节点名(实例 → 节点)
|
||||
var instRows = await _db.Select<Common_WorkflowInstance>()
|
||||
.Where(a => list.Select(x => x.Id).Contains(a.Id))
|
||||
.ToListAsync();
|
||||
var nodeIds = instRows.Where(a => a.CurrentNodeId > 0).Select(a => a.CurrentNodeId).Distinct().ToList();
|
||||
var nodes = nodeIds.Count > 0
|
||||
? await _db.Select<Common_WorkflowNode>().Where(a => nodeIds.Contains(a.Id)).ToListAsync()
|
||||
: new List<Common_WorkflowNode>();
|
||||
var nodeMap = nodes.ToDictionary(a => a.Id, a => a.Name);
|
||||
foreach (var item in list)
|
||||
{
|
||||
var row = instRows.FirstOrDefault(a => a.Id == item.Id);
|
||||
if (row != null) item.NodeName = nodeMap.GetValueOrDefault(row.CurrentNodeId, "");
|
||||
}
|
||||
return (list, total);
|
||||
}
|
||||
|
||||
/// <summary>实例详情(含任务轨迹)</summary>
|
||||
public async Task<WorkflowInstanceDetailDto?> GetInstanceDetailAsync(long id)
|
||||
{
|
||||
var inst = await _db.Select<Common_WorkflowInstance, Common_Workflow>()
|
||||
.InnerJoin((i, f) => i.WorkflowId == f.Id)
|
||||
.Where((i, f) => i.Id == id && i.Flag > 0)
|
||||
.ToOneAsync((i, f) => new WorkflowInstanceDetailDto
|
||||
{
|
||||
Id = i.Id,
|
||||
WorkflowName = f.Name,
|
||||
BizType = i.BizType,
|
||||
BillNo = i.BillNo,
|
||||
BizId = i.BizId,
|
||||
Status = i.Status,
|
||||
NodeName = "",
|
||||
StartTime = i.StartTime,
|
||||
EndTime = i.EndTime,
|
||||
});
|
||||
if (inst == null) return null;
|
||||
|
||||
var instRow = await _db.Select<Common_WorkflowInstance>().Where(a => a.Id == id).FirstAsync();
|
||||
if (instRow != null && instRow.CurrentNodeId > 0)
|
||||
{
|
||||
var curNode = await _db.Select<Common_WorkflowNode>().Where(a => a.Id == instRow.CurrentNodeId).FirstAsync();
|
||||
inst.NodeName = curNode?.Name ?? "";
|
||||
}
|
||||
|
||||
var tasks = await _db.Select<Common_WorkflowTask, BaseSys_User>()
|
||||
.InnerJoin((t, u) => t.UserId == u.Id)
|
||||
.Where((t, u) => t.InstanceId == id && t.Flag > 0)
|
||||
.OrderBy((t, u) => t.Id)
|
||||
.ToListAsync((t, u) => new WorkflowTaskDto
|
||||
{
|
||||
Id = t.Id,
|
||||
NodeId = t.NodeId,
|
||||
NodeName = "",
|
||||
UserId = t.UserId,
|
||||
UserName = u.Name,
|
||||
Comment = t.Comment,
|
||||
Status = t.Status,
|
||||
HandleTime = t.HandleTime,
|
||||
AddTime = t.AddTime,
|
||||
});
|
||||
|
||||
var nodeIds = tasks.Select(a => a.NodeId).Distinct().ToList();
|
||||
var nodes = nodeIds.Count > 0
|
||||
? await _db.Select<Common_WorkflowNode>().Where(a => nodeIds.Contains(a.Id)).ToListAsync()
|
||||
: new List<Common_WorkflowNode>();
|
||||
var nodeMap = nodes.ToDictionary(a => a.Id, a => a.Name);
|
||||
foreach (var t in tasks) t.NodeName = nodeMap.GetValueOrDefault(t.NodeId, "");
|
||||
inst.Tasks = tasks;
|
||||
return inst;
|
||||
}
|
||||
|
||||
/// <summary>创建审批任务(审批人取 ApproverJson 首个用户,缺省给操作人)</summary>
|
||||
private async Task CreateTaskAsync(long instanceId, Common_WorkflowNode node, long operatorId)
|
||||
{
|
||||
var approverId = ParseFirstApprover(node.ApproverJson) ?? operatorId;
|
||||
await _db.Insert(new Common_WorkflowTask
|
||||
{
|
||||
InstanceId = instanceId,
|
||||
NodeId = node.Id,
|
||||
UserId = approverId,
|
||||
Status = 0,
|
||||
Flag = 1,
|
||||
Adder = operatorId,
|
||||
}).ExecuteAffrowsAsync();
|
||||
}
|
||||
|
||||
/// <summary>解析 ApproverJson 首个审批人ID(JSON 数组 ["1","2"])</summary>
|
||||
private static long? ParseFirstApprover(string? json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json)) return null;
|
||||
try
|
||||
{
|
||||
var arr = JsonSerializer.Deserialize<List<long>>(json);
|
||||
return arr != null && arr.Count > 0 ? arr[0] : null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>为列表项填充节点名</summary>
|
||||
private async Task FillNodeNamesAsync<T>(List<T> items, Func<T, long> nodeIdGetter, Action<T, string> setter)
|
||||
{
|
||||
var nodeIds = items.Select(nodeIdGetter).Where(a => a > 0).Distinct().ToList();
|
||||
if (nodeIds.Count == 0) return;
|
||||
var nodes = await _db.Select<Common_WorkflowNode>().Where(a => nodeIds.Contains(a.Id)).ToListAsync();
|
||||
var nodeMap = nodes.ToDictionary(a => a.Id, a => a.Name);
|
||||
foreach (var item in items)
|
||||
{
|
||||
var n = nodeMap.GetValueOrDefault(nodeIdGetter(item), "");
|
||||
setter(item, n);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user