- 移除 LegacyStatusNormalizationMigrator,仅保留 AreaLayoutModeDefaultMigrator - 删除 WmsDispatchStatus 枚举、常量和全局 using - 统一所有 DbContext 注入为 MiGuDbContext,移除 PlatformDbContext - 服务实体操作统一用 IEditableRepository<T>,移除本地实现 - 移除 WmsService 的 MigrateLegacyAsync 方法 - 精简接口模型,移除部分字段和请求体 - 前端保存容器时 status 字段取自已有数据,移除写死默认值 - 更新文档,去除旧说明,统一数据库初始化流程
177 lines
5.8 KiB
C#
177 lines
5.8 KiB
C#
using System.Text.Json;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using MiGu.Server.Auth;
|
|
using MiGu.Server.Persistence;
|
|
|
|
namespace MiGu.Server.Dashboard;
|
|
|
|
public sealed class DashboardShortcutService
|
|
{
|
|
private readonly MiGuDbContext _db;
|
|
private readonly RbacStore _rbac;
|
|
|
|
private static readonly JsonSerializerOptions JsonOpts = new()
|
|
{
|
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
|
};
|
|
|
|
public DashboardShortcutService(MiGuDbContext db, RbacStore rbac)
|
|
{
|
|
_db = db;
|
|
_rbac = rbac;
|
|
}
|
|
|
|
public sealed record QuickEntriesResult(IReadOnlyList<string> Keys, bool UsingDefaults);
|
|
|
|
public async Task<QuickEntriesResult> GetAsync(string userId, string scope, CancellationToken ct = default)
|
|
{
|
|
scope = NormalizeScope(scope);
|
|
var allowed = AllowedPages(userId, scope);
|
|
|
|
var row = await _db.UserDashboardShortcuts
|
|
.AsNoTracking()
|
|
.FirstOrDefaultAsync(x => x.UserId == userId && x.Scope == scope, ct);
|
|
|
|
if (row == null)
|
|
{
|
|
var defaults = EnsureMandatoryKeys(
|
|
FilterKeys(DashboardShortcutCatalog.DefaultKeysForScope(scope), scope, allowed),
|
|
scope, allowed);
|
|
return new QuickEntriesResult(
|
|
defaults.Take(DashboardShortcutCatalog.MaxKeysPerUser).ToList(),
|
|
UsingDefaults: true);
|
|
}
|
|
|
|
var keys = ParseKeys(row.KeysJson);
|
|
var filtered = EnsureMandatoryKeys(
|
|
FilterKeys(keys, scope, allowed), scope, allowed)
|
|
.Take(DashboardShortcutCatalog.MaxKeysPerUser)
|
|
.ToList();
|
|
return new QuickEntriesResult(filtered, UsingDefaults: false);
|
|
}
|
|
|
|
public async Task<QuickEntriesResult> SaveAsync(
|
|
string userId, string scope, IReadOnlyList<string>? keys, CancellationToken ct = default)
|
|
{
|
|
scope = NormalizeScope(scope);
|
|
var allowed = AllowedPages(userId, scope);
|
|
var sanitized = EnsureMandatoryKeys(
|
|
FilterKeys(Deduplicate(keys ?? []), scope, allowed), scope, allowed);
|
|
if (sanitized.Count > DashboardShortcutCatalog.MaxKeysPerUser)
|
|
sanitized = sanitized.Take(DashboardShortcutCatalog.MaxKeysPerUser).ToList();
|
|
|
|
var row = await _db.UserDashboardShortcuts
|
|
.FirstOrDefaultAsync(x => x.UserId == userId && x.Scope == scope, ct);
|
|
|
|
var json = JsonSerializer.Serialize(sanitized, JsonOpts);
|
|
var now = DateTimeOffset.UtcNow;
|
|
|
|
if (row == null)
|
|
{
|
|
_db.UserDashboardShortcuts.Add(new UserDashboardShortcut
|
|
{
|
|
UserId = userId,
|
|
Scope = scope,
|
|
KeysJson = json,
|
|
UpdatedAt = now
|
|
});
|
|
}
|
|
else
|
|
{
|
|
row.KeysJson = json;
|
|
row.UpdatedAt = now;
|
|
}
|
|
|
|
await _db.SaveChangesAsync(ct);
|
|
return new QuickEntriesResult(sanitized, UsingDefaults: false);
|
|
}
|
|
|
|
private HashSet<string> AllowedPages(string userId, string scope)
|
|
{
|
|
var user = _rbac.FindUserById(userId);
|
|
if (user == null || !user.Enabled)
|
|
return new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
var eff = _rbac.ComputeEffective(user, scope);
|
|
return eff.Pages.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
|
}
|
|
|
|
private static List<string> FilterKeys(
|
|
IEnumerable<string> keys, string scope, HashSet<string> allowedPages)
|
|
{
|
|
var outKeys = new List<string>();
|
|
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
foreach (var raw in keys)
|
|
{
|
|
var key = DashboardShortcutCatalog.NormalizeKey(raw ?? "");
|
|
if (string.IsNullOrEmpty(key)) continue;
|
|
if (!DashboardShortcutCatalog.IsValidKey(key)) continue;
|
|
if (!DashboardShortcutCatalog.KeyMatchesScope(key, scope)) continue;
|
|
if (seen.Contains(key)) continue;
|
|
|
|
var pageKey = DashboardShortcutCatalog.PageKeyFor(key);
|
|
if (!allowedPages.Contains(pageKey)) continue;
|
|
|
|
seen.Add(key);
|
|
outKeys.Add(key);
|
|
}
|
|
return outKeys;
|
|
}
|
|
|
|
private static List<string> EnsureMandatoryKeys(
|
|
List<string> keys, string scope, HashSet<string> allowedPages)
|
|
{
|
|
if (!string.Equals(scope, PageCatalog.ScopePlatform, StringComparison.OrdinalIgnoreCase))
|
|
return keys;
|
|
|
|
var result = new List<string>();
|
|
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
foreach (var raw in DashboardShortcutCatalog.MandatoryPlatformKeys)
|
|
{
|
|
var key = DashboardShortcutCatalog.NormalizeKey(raw);
|
|
if (!DashboardShortcutCatalog.IsValidKey(key)) continue;
|
|
var pageKey = DashboardShortcutCatalog.PageKeyFor(key);
|
|
if (!allowedPages.Contains(pageKey)) continue;
|
|
if (seen.Add(key)) result.Add(key);
|
|
}
|
|
|
|
foreach (var key in keys)
|
|
{
|
|
if (seen.Add(key)) result.Add(key);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private static List<string> Deduplicate(IReadOnlyList<string> keys)
|
|
{
|
|
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
var list = new List<string>();
|
|
foreach (var k in keys)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(k)) continue;
|
|
var t = k.Trim();
|
|
if (seen.Add(t)) list.Add(t);
|
|
}
|
|
return list;
|
|
}
|
|
|
|
private static List<string> ParseKeys(string json)
|
|
{
|
|
try
|
|
{
|
|
return JsonSerializer.Deserialize<List<string>>(json, JsonOpts) ?? [];
|
|
}
|
|
catch
|
|
{
|
|
return [];
|
|
}
|
|
}
|
|
|
|
private static string NormalizeScope(string scope) =>
|
|
string.Equals(scope, PageCatalog.ScopeMonitor, StringComparison.OrdinalIgnoreCase)
|
|
? PageCatalog.ScopeMonitor
|
|
: PageCatalog.ScopePlatform;
|
|
}
|