完善系统参数配置和数据字典管理模块
This commit is contained in:
@@ -1,12 +1,227 @@
|
||||
using Model;
|
||||
using Model.Dto.Asset;
|
||||
using Model.Entity.Asset;
|
||||
using ORM;
|
||||
using QRCoder;
|
||||
using Service.Interface;
|
||||
using SqlSugar;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Service.Implement
|
||||
{
|
||||
/// <summary>
|
||||
/// 二维码 服务实现
|
||||
/// 设备二维码 服务实现
|
||||
/// 扫码直达 URL 约定:{baseUrl}/asset/ledger/equipment?id={equipmentId}
|
||||
/// baseUrl 由 Controller 从 HTTP 请求(scheme://host)传入;如需固定域名可改为读 sys_param 的 site_base_url
|
||||
/// </summary>
|
||||
public class QrCodeService : IQrCodeService
|
||||
{
|
||||
// TODO: 实现 二维码 相关方法
|
||||
public async Task<Result<QrCodeDto>> GenerateAsync(long equipmentId, string baseUrl)
|
||||
{
|
||||
if (equipmentId <= 0) return Result<QrCodeDto>.Error("设备Id无效");
|
||||
try
|
||||
{
|
||||
var entity = await LoadEquipmentAsync(equipmentId);
|
||||
if (entity == null) return Result<QrCodeDto>.Error("设备不存在或已被删除");
|
||||
|
||||
var url = BuildQrUrl(baseUrl, equipmentId);
|
||||
if (string.IsNullOrEmpty(entity.QrCodeUrl))
|
||||
{
|
||||
await SqlSugarContext.DbContext.Updateable<EquipmentEntity>()
|
||||
.SetColumns(x => x.QrCodeUrl == url)
|
||||
.Where(x => x.Id == equipmentId).ExecuteCommandAsync();
|
||||
entity.QrCodeUrl = url;
|
||||
}
|
||||
return Result<QrCodeDto>.Success(ToDto(entity));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<QrCodeDto>.Error("生成二维码失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result<byte[]>> GenerateImageAsync(long equipmentId, string baseUrl)
|
||||
{
|
||||
if (equipmentId <= 0) return Result<byte[]>.Error("设备Id无效");
|
||||
try
|
||||
{
|
||||
var entity = await LoadEquipmentAsync(equipmentId);
|
||||
if (entity == null) return Result<byte[]>.Error("设备不存在或已被删除");
|
||||
|
||||
var url = string.IsNullOrEmpty(entity.QrCodeUrl) ? BuildQrUrl(baseUrl, equipmentId) : entity.QrCodeUrl;
|
||||
if (string.IsNullOrEmpty(entity.QrCodeUrl))
|
||||
{
|
||||
await SqlSugarContext.DbContext.Updateable<EquipmentEntity>()
|
||||
.SetColumns(x => x.QrCodeUrl == url)
|
||||
.Where(x => x.Id == equipmentId).ExecuteCommandAsync();
|
||||
entity.QrCodeUrl = url;
|
||||
}
|
||||
|
||||
var png = EncodePng(url);
|
||||
return Result<byte[]>.Success(png);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<byte[]>.Error("生成二维码图片失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result<List<QrCodeBatchResultDto>>> BatchGenerateAsync(long[] equipmentIds, string baseUrl)
|
||||
{
|
||||
if (equipmentIds == null || equipmentIds.Length == 0)
|
||||
return Result<List<QrCodeBatchResultDto>>.Error("设备Id列表不能为空");
|
||||
var results = new List<QrCodeBatchResultDto>();
|
||||
foreach (var id in equipmentIds.Distinct())
|
||||
{
|
||||
var r = await GenerateAsync(id, baseUrl);
|
||||
results.Add(new QrCodeBatchResultDto
|
||||
{
|
||||
EquipmentId = id.ToString(),
|
||||
EquipmentCode = r.Data?.EquipmentCode,
|
||||
EquipmentName = r.Data?.EquipmentName,
|
||||
QrCodeUrl = r.Data?.QrCodeUrl,
|
||||
Success = r.IsSuccess,
|
||||
Message = r.IsSuccess ? "成功" : r.Msg
|
||||
});
|
||||
}
|
||||
return Result<List<QrCodeBatchResultDto>>.Success(results);
|
||||
}
|
||||
|
||||
public async Task<Result<List<QrCodeDto>>> GetListPagedAsync(int pageIndex, int pageSize, RefAsync<int> total, string? keyword = null, bool qrOnly = false)
|
||||
{
|
||||
try
|
||||
{
|
||||
var list = await SqlSugarContext.DbContext.Queryable<EquipmentEntity>()
|
||||
.Where(x => x.IsDel == 0)
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(keyword), x => x.Code!.Contains(keyword!) || x.Name!.Contains(keyword!))
|
||||
.WhereIF(qrOnly, x => x.QrCodeUrl != null && x.QrCodeUrl != "")
|
||||
.OrderBy(x => x.Code)
|
||||
.ToPageListAsync(pageIndex, pageSize, total);
|
||||
return Result<List<QrCodeDto>>.Success(list.Select(ToDto).ToList());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<List<QrCodeDto>>.Error("查询二维码列表失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result<string>> GetPrintHtmlAsync(long equipmentId, string baseUrl)
|
||||
{
|
||||
if (equipmentId <= 0) return Result<string>.Error("设备Id无效");
|
||||
try
|
||||
{
|
||||
var entity = await LoadEquipmentAsync(equipmentId);
|
||||
if (entity == null) return Result<string>.Error("设备不存在或已被删除");
|
||||
|
||||
var url = string.IsNullOrEmpty(entity.QrCodeUrl) ? BuildQrUrl(baseUrl, equipmentId) : entity.QrCodeUrl;
|
||||
var png = EncodePng(url);
|
||||
var base64 = Convert.ToBase64String(png);
|
||||
|
||||
var html = BuildPrintHtml(entity, url, base64);
|
||||
return Result<string>.Success(html);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<string>.Error("生成打印页失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result> BindRfidAsync(long equipmentId, string rfidCode)
|
||||
{
|
||||
if (equipmentId <= 0) return Result.Error("设备Id无效");
|
||||
if (string.IsNullOrWhiteSpace(rfidCode)) return Result.Error("RFID 编号不能为空");
|
||||
try
|
||||
{
|
||||
var exists = await SqlSugarContext.DbContext.Queryable<EquipmentEntity>()
|
||||
.Where(x => x.Id == equipmentId && x.IsDel == 0).AnyAsync();
|
||||
if (!exists) return Result.Error("设备不存在或已被删除");
|
||||
|
||||
await SqlSugarContext.DbContext.Updateable<EquipmentEntity>()
|
||||
.SetColumns(x => x.RfidCode == rfidCode)
|
||||
.Where(x => x.Id == equipmentId).ExecuteCommandAsync();
|
||||
return Result.Success();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Error("绑定 RFID 失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 私有工具 ====================
|
||||
|
||||
private static async Task<EquipmentEntity?> LoadEquipmentAsync(long id)
|
||||
=> await SqlSugarContext.DbContext.Queryable<EquipmentEntity>()
|
||||
.Where(x => x.Id == id && x.IsDel == 0).FirstAsync();
|
||||
|
||||
private static QrCodeDto ToDto(EquipmentEntity e) => new()
|
||||
{
|
||||
EquipmentId = e.Id.ToString(),
|
||||
EquipmentCode = e.Code,
|
||||
EquipmentName = e.Name,
|
||||
Location = e.Location,
|
||||
QrCode = e.QrCode,
|
||||
QrCodeUrl = e.QrCodeUrl,
|
||||
RfidCode = e.RfidCode
|
||||
};
|
||||
|
||||
private static string BuildQrUrl(string baseUrl, long equipmentId)
|
||||
{
|
||||
var b = (baseUrl ?? string.Empty).TrimEnd('/');
|
||||
return $"{b}/asset/ledger/equipment?id={equipmentId}";
|
||||
}
|
||||
|
||||
private static byte[] EncodePng(string content)
|
||||
{
|
||||
using var gen = new QRCodeGenerator();
|
||||
var data = gen.CreateQrCode(content, QRCodeGenerator.ECCLevel.Q);
|
||||
var png = new PngByteQRCode(data);
|
||||
return png.GetGraphic(20); // 20 px per module,便于扫描识别
|
||||
}
|
||||
|
||||
private static string BuildPrintHtml(EquipmentEntity e, string url, string base64)
|
||||
{
|
||||
// 简单 A4 打印模板:左侧二维码,右侧设备信息;浏览器 window.print() 即可
|
||||
var safe = (string? s) => System.Net.WebUtility.HtmlEncode(s ?? string.Empty);
|
||||
return $@"<!DOCTYPE html>
|
||||
<html lang='zh-CN'>
|
||||
<head>
|
||||
<meta charset='utf-8' />
|
||||
<title>设备二维码 - {safe(e.Name)}</title>
|
||||
<style>
|
||||
@page {{ size: A4; margin: 12mm; }}
|
||||
body {{ font-family: 'Microsoft YaHei', sans-serif; color: #303133; }}
|
||||
.label {{ border:1px solid #303133; border-radius:6px; padding:16px; display:flex; gap:24px; align-items:center; max-width:480px; }}
|
||||
.qr img {{ width: 220px; height: 220px; }}
|
||||
.info dl {{ margin:0; }}
|
||||
.info dt {{ font-size:13px; color:#909399; margin-top:8px; }}
|
||||
.info dd {{ font-size:16px; margin:4px 0 0 0; font-weight:600; }}
|
||||
.info h2 {{ margin:0 0 8px 0; font-size:20px; }}
|
||||
.url {{ margin-top:12px; font-size:12px; color:#909399; word-break:break-all; }}
|
||||
.actions {{ margin-top:16px; }}
|
||||
.actions button {{ padding:6px 16px; font-size:14px; cursor:pointer; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class='label'>
|
||||
<div class='qr'><img src='data:image/png;base64,{base64}' alt='QR' /></div>
|
||||
<div class='info'>
|
||||
<h2>{safe(e.Name)}</h2>
|
||||
<dl>
|
||||
<dt>设备编号</dt><dd>{safe(e.Code)}</dd>
|
||||
<dt>二维码编号</dt><dd>{safe(e.QrCode)}</dd>
|
||||
<dt>存放位置</dt><dd>{safe(e.Location)}</dd>
|
||||
<dt>责任人</dt><dd>{safe(e.ResponsiblePerson)}</dd>
|
||||
</dl>
|
||||
<div class='url'>{safe(url)}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class='actions'><button onclick='window.print()'>打印</button></div>
|
||||
<script>window.onload=function(){{setTimeout(function(){{window.print();}},300);}};</script>
|
||||
</body>
|
||||
</html>";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
using Model;
|
||||
using Model.Dto.System;
|
||||
using Model.Entity.System;
|
||||
using Service.Interface;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Service.Implement
|
||||
{
|
||||
/// <summary>
|
||||
/// 本地文件系统存储 Provider
|
||||
/// 落地策略:存到 BasePath/{Bucket?}/{yyyy}/{MM}/{dd}/{guid}{ext},URL = {Endpoint}/{Bucket?}/{yyyy}/{MM}/{dd}/{guid}{ext}
|
||||
/// wwwroot/uploads 由 Program.UseStaticFiles 提供访问;MinIO/OSS 实现见 IFileStorageProvider 扩展点
|
||||
/// </summary>
|
||||
public class LocalFileStorageProvider : IFileStorageProvider
|
||||
{
|
||||
public string ProviderName => "local";
|
||||
|
||||
public async Task<Result<FileUploadResultDto>> UploadAsync(Stream stream, string originalName, long size, FileStorageConfigEntity config)
|
||||
{
|
||||
if (config == null) return Result<FileUploadResultDto>.Error("存储配置不能为空");
|
||||
if (stream == null || !stream.CanRead) return Result<FileUploadResultDto>.Error("文件流不可读");
|
||||
try
|
||||
{
|
||||
var basePath = ResolveBasePath(config.BasePath);
|
||||
if (string.IsNullOrWhiteSpace(basePath))
|
||||
return Result<FileUploadResultDto>.Error("Local 存储必须配置 BasePath");
|
||||
|
||||
var ext = Path.GetExtension(originalName)?.ToLowerInvariant() ?? "";
|
||||
var now = DateTime.Now;
|
||||
var datePath = $"{now:yyyy}/{now:MM}/{now:dd}";
|
||||
var fileName = $"{Guid.NewGuid():N}{ext}";
|
||||
var storedFileName = string.IsNullOrWhiteSpace(config.Bucket)
|
||||
? $"{datePath}/{fileName}"
|
||||
: $"{config.Bucket}/{datePath}/{fileName}";
|
||||
|
||||
var dir = Path.Combine(basePath,
|
||||
string.IsNullOrWhiteSpace(config.Bucket) ? datePath : Path.Combine(config.Bucket, datePath));
|
||||
Directory.CreateDirectory(dir);
|
||||
var fullPath = Path.Combine(dir, fileName);
|
||||
|
||||
using (var fs = new FileStream(fullPath, FileMode.CreateNew, FileAccess.Write, FileShare.None))
|
||||
{
|
||||
await stream.CopyToAsync(fs);
|
||||
}
|
||||
|
||||
var url = BuildUrl(config.Endpoint, config.Bucket, storedFileName);
|
||||
return Result<FileUploadResultDto>.Success(new FileUploadResultDto
|
||||
{
|
||||
FileName = storedFileName,
|
||||
Url = url,
|
||||
Ext = ext,
|
||||
Size = size,
|
||||
Provider = ProviderName
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<FileUploadResultDto>.Error("本地文件上传失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public Task<Result> DeleteAsync(string storedFileName, FileStorageConfigEntity config)
|
||||
{
|
||||
if (config == null || string.IsNullOrWhiteSpace(storedFileName))
|
||||
return Task.FromResult(Result.Error("存储配置或文件名不能为空"));
|
||||
try
|
||||
{
|
||||
var basePath = ResolveBasePath(config.BasePath);
|
||||
var fullPath = Path.Combine(basePath, storedFileName.Replace('/', Path.DirectorySeparatorChar));
|
||||
if (File.Exists(fullPath)) File.Delete(fullPath);
|
||||
return Task.FromResult(Result.Success());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Task.FromResult(Result.Error("删除本地文件失败", ex));
|
||||
}
|
||||
}
|
||||
|
||||
public Task<Result> TestConnectionAsync(FileStorageConfigEntity config)
|
||||
{
|
||||
if (config == null) return Task.FromResult(Result.Error("存储配置不能为空"));
|
||||
try
|
||||
{
|
||||
var basePath = ResolveBasePath(config.BasePath);
|
||||
if (string.IsNullOrWhiteSpace(basePath))
|
||||
return Task.FromResult(Result.Error("Local 存储必须配置 BasePath"));
|
||||
Directory.CreateDirectory(basePath);
|
||||
var testFile = Path.Combine(basePath, $".storage_test_{Guid.NewGuid():N}");
|
||||
File.WriteAllText(testFile, "ok");
|
||||
File.Delete(testFile);
|
||||
return Task.FromResult(Result.Success());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Task.FromResult(Result.Error("Local 连接测试失败:" + ex.Message));
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 私有工具 ====================
|
||||
|
||||
private static string ResolveBasePath(string? basePath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(basePath)) return string.Empty;
|
||||
// 相对路径以运行目录为基准(如 wwwroot/uploads)
|
||||
return Path.IsPathRooted(basePath) ? basePath : Path.Combine(AppContext.BaseDirectory, basePath);
|
||||
}
|
||||
|
||||
private static string BuildUrl(string? endpoint, string? bucket, string storedFileName)
|
||||
{
|
||||
var segs = new List<string>();
|
||||
if (!string.IsNullOrWhiteSpace(endpoint)) segs.Add(endpoint.TrimEnd('/'));
|
||||
if (!string.IsNullOrWhiteSpace(bucket)) segs.Add(bucket.Trim('/'));
|
||||
segs.Add(storedFileName);
|
||||
return string.Join('/', segs);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ namespace Service.Implement
|
||||
public const byte DataScopeAll = 1;
|
||||
public const byte DataScopeLab = 2;
|
||||
|
||||
/// <summary>6 个系统权限编码</summary>
|
||||
/// <summary>7 个系统权限编码</summary>
|
||||
public static readonly (string Code, string Name, string Group)[] Permissions =
|
||||
{
|
||||
("device:view", "查看设备", "device"),
|
||||
@@ -21,7 +21,8 @@ namespace Service.Implement
|
||||
("device:control", "控制设备", "device"),
|
||||
("alert:confirm", "确认告警", "alert"),
|
||||
("inspection:manage", "巡检管理", "inspection"),
|
||||
("user:manage", "用户管理", "user")
|
||||
("user:manage", "用户管理", "user"),
|
||||
("system:manage", "系统配置管理", "system")
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
@@ -30,8 +31,8 @@ namespace Service.Implement
|
||||
/// </summary>
|
||||
public static readonly (string Code, string Name, byte DataScope, string[] Permissions)[] Roles =
|
||||
{
|
||||
("superadmin", "超级管理员", DataScopeAll, new[] { "device:view", "device:edit", "device:control", "alert:confirm", "inspection:manage", "user:manage" }),
|
||||
("labadmin", "实验室管理员", DataScopeLab, new[] { "device:view", "device:edit", "device:control", "alert:confirm", "inspection:manage" }),
|
||||
("superadmin", "超级管理员", DataScopeAll, new[] { "device:view", "device:edit", "device:control", "alert:confirm", "inspection:manage", "user:manage", "system:manage" }),
|
||||
("labadmin", "实验室管理员", DataScopeLab, new[] { "device:view", "device:edit", "device:control", "alert:confirm", "inspection:manage", "system:manage" }),
|
||||
("operator", "设备操作员", DataScopeLab, new[] { "device:view", "inspection:manage" }),
|
||||
("maintengineer", "维修工程师", DataScopeLab, new[] { "device:view", "device:edit", "device:control", "alert:confirm" })
|
||||
};
|
||||
@@ -113,6 +114,12 @@ namespace Service.Implement
|
||||
db.Insertable(new UserRoleEntity { UserId = adminId, RoleId = superAdmin.Id, CreateTime = now }).ExecuteCommand();
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 系统参数种子(一期 4 个内置参数,幂等:按 ParamKey 跳过已存在)
|
||||
SeedSystemParams(db, now);
|
||||
|
||||
// 5. 默认本地文件存储配置(幂等:仅当无任何配置时插入一条默认 Local 通道)
|
||||
SeedDefaultFileStorage(db, now);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
@@ -120,5 +127,70 @@ namespace Service.Implement
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static void SeedDefaultFileStorage(SqlSugar.ISqlSugarClient db, DateTime now)
|
||||
{
|
||||
var hasAny = db.Queryable<FileStorageConfigEntity>().Where(x => x.IsDel == 0).Any();
|
||||
if (hasAny) return;
|
||||
var entity = new FileStorageConfigEntity
|
||||
{
|
||||
Provider = "local",
|
||||
Name = "本地存储",
|
||||
Endpoint = "/uploads",
|
||||
AccessKey = null,
|
||||
SecretKey = null,
|
||||
Bucket = null,
|
||||
Region = null,
|
||||
BasePath = "wwwroot/uploads",
|
||||
MaxSizeMB = 10,
|
||||
AllowedExts = null, // 不限制扩展名;如需限制填 [".jpg",".png",".pdf",".xlsx"]
|
||||
IsDefault = 1,
|
||||
Status = 1,
|
||||
Remark = "系统默认本地存储通道(文件落 wwwroot/uploads,通过 /uploads 静态访问)",
|
||||
CreateTime = now
|
||||
};
|
||||
db.Insertable(entity).ExecuteReturnSnowflakeId();
|
||||
}
|
||||
|
||||
/// <summary>一期 4 个内置系统参数(IsSystem=1,禁止删除,仅允许改 Value/Remark)</summary>
|
||||
/// <remarks>Group 与 ParamType 约定:0=int,1=text,2=enum,3=json</remarks>
|
||||
private static readonly (string Key, string Name, string? Value, byte Type, string? Options, string? Unit, string Group, int Sort, string? Remark)[] SystemParams =
|
||||
{
|
||||
("data_collect_default_frequency", "数据采集默认频率", "5", 0, null, "秒", "数据采集", 1, "设备数据采集的默认间隔(秒),新建设备时作为默认值"),
|
||||
("alert_notify_method", "告警通知方式", "feishu", 2,
|
||||
"[{\"value\":\"feishu\",\"label\":\"飞书\"},{\"value\":\"dingtalk\",\"label\":\"钉钉\"},{\"value\":\"wecom\",\"label\":\"企业微信\"},{\"value\":\"email\",\"label\":\"邮件\"}]",
|
||||
null, "告警", 2, "默认告警通知渠道(多选需在告警规则内单独配置)"),
|
||||
("work_order_code_rule", "工单编号规则", "WO{yyyyMMddHHmmss}", 1, null, null, "工单", 3,
|
||||
"工单编号生成模板,支持占位符:{yyyy}年 {MM}月 {dd}日 {HH}时 {mm}分 {ss}秒,序列号用 {seq4} 表示 4 位顺序号"),
|
||||
("file_upload_limit", "文件上传限制", "10", 2,
|
||||
"[{\"value\":\"5\",\"label\":\"5MB\"},{\"value\":\"10\",\"label\":\"10MB\"},{\"value\":\"20\",\"label\":\"20MB\"},{\"value\":\"50\",\"label\":\"50MB\"}]",
|
||||
"MB", "文件", 4, "单个文件上传大小上限(MB)")
|
||||
};
|
||||
|
||||
private static void SeedSystemParams(SqlSugar.ISqlSugarClient db, DateTime now)
|
||||
{
|
||||
foreach (var (key, name, value, type, options, unit, group, sort, remark) in SystemParams)
|
||||
{
|
||||
var exists = db.Queryable<SystemParamEntity>().Where(x => x.ParamKey == key).Any();
|
||||
if (exists) continue;
|
||||
var entity = new SystemParamEntity
|
||||
{
|
||||
ParamKey = key,
|
||||
ParamName = name,
|
||||
ParamValue = value,
|
||||
ParamType = type,
|
||||
ParamOptions = options,
|
||||
Unit = unit,
|
||||
Group = group,
|
||||
Sort = sort,
|
||||
Remark = remark,
|
||||
IsSystem = 1,
|
||||
LastUpdateUser = "system",
|
||||
LastUpdateTime = now,
|
||||
CreateTime = now
|
||||
};
|
||||
db.Insertable(entity).ExecuteReturnSnowflakeId();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,360 @@
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Model;
|
||||
using Model.Dto.System;
|
||||
using Model.Entity.System;
|
||||
using Model.Mapper;
|
||||
using ORM;
|
||||
using Service.Interface;
|
||||
using SqlSugar;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Service.Implement
|
||||
{
|
||||
/// <summary>
|
||||
/// 数据字典管理 服务实现
|
||||
/// 缓存策略:按 typeCode 缓存启用项,新增/修改/删除字典分类或字典项时失效对应 typeCode;启动首次访问时按需加载
|
||||
/// </summary>
|
||||
public class DictService : IDictService
|
||||
{
|
||||
// TODO: 实现 数据字典管理 相关方法
|
||||
private readonly IMemoryCache _cache;
|
||||
private static readonly object _cacheLock = new();
|
||||
|
||||
public DictService(IMemoryCache cache)
|
||||
{
|
||||
_cache = cache;
|
||||
}
|
||||
|
||||
// ==================== 字典分类 ====================
|
||||
|
||||
public async Task<Result<List<DictTypeDto>>> GetTypesPagedAsync(int pageIndex, int pageSize, RefAsync<int> total, string? keyword = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var list = await SqlSugarContext.DbContext.Queryable<DictTypeEntity>()
|
||||
.Where(x => x.IsDel == 0)
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(keyword), x => x.Code.Contains(keyword!) || x.Name.Contains(keyword!))
|
||||
.OrderBy(x => x.Sort).OrderBy(x => x.CreateTime, OrderByType.Desc)
|
||||
.ToPageListAsync(pageIndex, pageSize, total);
|
||||
|
||||
var dtos = list.ToDtoList();
|
||||
await FillItemCountAsync(dtos);
|
||||
return Result<List<DictTypeDto>>.Success(dtos);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<List<DictTypeDto>>.Error("查询字典分类失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result<List<DictTypeDto>>> GetTypesAllAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var list = await SqlSugarContext.DbContext.Queryable<DictTypeEntity>()
|
||||
.Where(x => x.IsDel == 0 && x.Status == 1)
|
||||
.OrderBy(x => x.Sort).OrderBy(x => x.Code)
|
||||
.ToListAsync();
|
||||
return Result<List<DictTypeDto>>.Success(list.ToDtoList());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<List<DictTypeDto>>.Error("查询字典分类失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result<DictTypeDto>> GetTypeByIdAsync(long id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var entity = await SqlSugarContext.DbContext.Queryable<DictTypeEntity>()
|
||||
.Where(x => x.Id == id && x.IsDel == 0).FirstAsync();
|
||||
if (entity == null) return Result<DictTypeDto>.Error("字典分类不存在或已被删除");
|
||||
var dto = entity.ToDto();
|
||||
await FillItemCountAsync(new List<DictTypeDto> { dto });
|
||||
return Result<DictTypeDto>.Success(dto);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<DictTypeDto>.Error("查询字典分类详情失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result<DictTypeDto>> AddTypeAsync(DictTypeDto dto)
|
||||
{
|
||||
if (dto == null || string.IsNullOrWhiteSpace(dto.Code) || string.IsNullOrWhiteSpace(dto.Name))
|
||||
return Result<DictTypeDto>.Error("字典编码和名称不能为空");
|
||||
|
||||
try
|
||||
{
|
||||
var exists = await SqlSugarContext.DbContext.Queryable<DictTypeEntity>()
|
||||
.Where(x => x.Code == dto.Code && x.IsDel == 0).AnyAsync();
|
||||
if (exists) return Result<DictTypeDto>.Error($"字典编码「{dto.Code}」已存在");
|
||||
|
||||
var entity = dto.ToEntity();
|
||||
entity.Id = 0;
|
||||
entity.CreateTime = DateTime.Now;
|
||||
var id = await SqlSugarContext.DbContext.Insertable(entity).ExecuteReturnSnowflakeIdAsync();
|
||||
entity.Id = id;
|
||||
return Result<DictTypeDto>.Success(entity.ToDto());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<DictTypeDto>.Error("新增字典分类失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result<DictTypeDto>> UpdateTypeAsync(DictTypeDto dto)
|
||||
{
|
||||
if (dto == null || !long.TryParse(dto.Id, out var id) || id <= 0)
|
||||
return Result<DictTypeDto>.Error("字典分类Id无效");
|
||||
|
||||
try
|
||||
{
|
||||
var entity = await SqlSugarContext.DbContext.Queryable<DictTypeEntity>()
|
||||
.Where(x => x.Id == id && x.IsDel == 0).FirstAsync();
|
||||
if (entity == null) return Result<DictTypeDto>.Error("字典分类不存在或已被删除");
|
||||
|
||||
var codeExists = await SqlSugarContext.DbContext.Queryable<DictTypeEntity>()
|
||||
.Where(x => x.Code == dto.Code && x.Id != id && x.IsDel == 0).AnyAsync();
|
||||
if (codeExists) return Result<DictTypeDto>.Error($"字典编码「{dto.Code}」已存在");
|
||||
|
||||
var newEntity = dto.ToEntity();
|
||||
newEntity.Id = id;
|
||||
await SqlSugarContext.DbContext.Updateable(newEntity)
|
||||
.IgnoreColumns(x => new { x.CreateTime, x.IsDel })
|
||||
.ExecuteCommandAsync();
|
||||
// 编码可能变更:旧/新 Code 都失效
|
||||
InvalidateCache(entity.Code);
|
||||
InvalidateCache(dto.Code);
|
||||
return Result<DictTypeDto>.Success(newEntity.ToDto());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<DictTypeDto>.Error("修改字典分类失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result> DeleteTypeAsync(long id)
|
||||
{
|
||||
if (id <= 0) return Result.Error("字典分类Id无效");
|
||||
try
|
||||
{
|
||||
var entity = await SqlSugarContext.DbContext.Queryable<DictTypeEntity>()
|
||||
.Where(x => x.Id == id && x.IsDel == 0).FirstAsync();
|
||||
if (entity == null) return Result.Error("字典分类不存在或已被删除");
|
||||
|
||||
var hasItems = await SqlSugarContext.DbContext.Queryable<DictItemEntity>()
|
||||
.Where(x => x.TypeId == id && x.IsDel == 0).AnyAsync();
|
||||
if (hasItems) return Result.Error("该字典分类下还有字典项,请先删除字典项再删除分类");
|
||||
|
||||
await SqlSugarContext.DbContext.Updateable<DictTypeEntity>()
|
||||
.SetColumns(x => x.IsDel == 1)
|
||||
.Where(x => x.Id == id).ExecuteCommandAsync();
|
||||
InvalidateCache(entity.Code);
|
||||
return Result.Success();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Error("删除字典分类失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 字典项 ====================
|
||||
|
||||
public async Task<Result<List<DictItemDto>>> GetItemsByTypeAsync(long typeId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var list = await SqlSugarContext.DbContext.Queryable<DictItemEntity>()
|
||||
.Where(x => x.TypeId == typeId && x.IsDel == 0)
|
||||
.OrderBy(x => x.Sort).OrderBy(x => x.CreateTime, OrderByType.Desc)
|
||||
.ToListAsync();
|
||||
return Result<List<DictItemDto>>.Success(list.ToDtoList());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<List<DictItemDto>>.Error("查询字典项失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result<DictItemDto>> AddItemAsync(DictItemDto dto)
|
||||
{
|
||||
if (dto == null || !long.TryParse(dto.TypeId, out var typeId) || typeId <= 0)
|
||||
return Result<DictItemDto>.Error("所属字典分类Id无效");
|
||||
if (string.IsNullOrWhiteSpace(dto.Code) || string.IsNullOrWhiteSpace(dto.Name))
|
||||
return Result<DictItemDto>.Error("字典项编码和名称不能为空");
|
||||
|
||||
try
|
||||
{
|
||||
var typeExists = await SqlSugarContext.DbContext.Queryable<DictTypeEntity>()
|
||||
.Where(x => x.Id == typeId && x.IsDel == 0).AnyAsync();
|
||||
if (!typeExists) return Result<DictItemDto>.Error("所属字典分类不存在");
|
||||
|
||||
var codeExists = await SqlSugarContext.DbContext.Queryable<DictItemEntity>()
|
||||
.Where(x => x.TypeId == typeId && x.Code == dto.Code && x.IsDel == 0).AnyAsync();
|
||||
if (codeExists) return Result<DictItemDto>.Error($"字典项编码「{dto.Code}」在该分类下已存在");
|
||||
|
||||
var entity = dto.ToEntity();
|
||||
entity.Id = 0;
|
||||
entity.TypeId = typeId;
|
||||
entity.CreateTime = DateTime.Now;
|
||||
if (entity.IsDefault == 1)
|
||||
{
|
||||
await SqlSugarContext.DbContext.Updateable<DictItemEntity>()
|
||||
.SetColumns(x => x.IsDefault == 0)
|
||||
.Where(x => x.TypeId == typeId && x.IsDel == 0).ExecuteCommandAsync();
|
||||
}
|
||||
var id = await SqlSugarContext.DbContext.Insertable(entity).ExecuteReturnSnowflakeIdAsync();
|
||||
entity.Id = id;
|
||||
|
||||
InvalidateCache(await GetTypeCodeAsync(typeId));
|
||||
return Result<DictItemDto>.Success(entity.ToDto());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<DictItemDto>.Error("新增字典项失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result<DictItemDto>> UpdateItemAsync(DictItemDto dto)
|
||||
{
|
||||
if (dto == null || !long.TryParse(dto.Id, out var id) || id <= 0)
|
||||
return Result<DictItemDto>.Error("字典项Id无效");
|
||||
if (!long.TryParse(dto.TypeId, out var typeId) || typeId <= 0)
|
||||
return Result<DictItemDto>.Error("所属字典分类Id无效");
|
||||
|
||||
try
|
||||
{
|
||||
var entity = await SqlSugarContext.DbContext.Queryable<DictItemEntity>()
|
||||
.Where(x => x.Id == id && x.IsDel == 0).FirstAsync();
|
||||
if (entity == null) return Result<DictItemDto>.Error("字典项不存在或已被删除");
|
||||
|
||||
var codeExists = await SqlSugarContext.DbContext.Queryable<DictItemEntity>()
|
||||
.Where(x => x.TypeId == typeId && x.Code == dto.Code && x.Id != id && x.IsDel == 0).AnyAsync();
|
||||
if (codeExists) return Result<DictItemDto>.Error($"字典项编码「{dto.Code}」在该分类下已存在");
|
||||
|
||||
// 默认项互斥:改为默认时清零同分类其它默认项
|
||||
if (dto.IsDefault == 1 && entity.IsDefault == 0)
|
||||
{
|
||||
await SqlSugarContext.DbContext.Updateable<DictItemEntity>()
|
||||
.SetColumns(x => x.IsDefault == 0)
|
||||
.Where(x => x.TypeId == typeId && x.IsDel == 0 && x.Id != id).ExecuteCommandAsync();
|
||||
}
|
||||
|
||||
var newEntity = dto.ToEntity();
|
||||
newEntity.Id = id;
|
||||
newEntity.TypeId = typeId;
|
||||
await SqlSugarContext.DbContext.Updateable(newEntity)
|
||||
.IgnoreColumns(x => new { x.CreateTime, x.IsDel })
|
||||
.ExecuteCommandAsync();
|
||||
|
||||
InvalidateCache(await GetTypeCodeAsync(typeId));
|
||||
return Result<DictItemDto>.Success(newEntity.ToDto());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<DictItemDto>.Error("修改字典项失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result> DeleteItemAsync(long id)
|
||||
{
|
||||
if (id <= 0) return Result.Error("字典项Id无效");
|
||||
try
|
||||
{
|
||||
var entity = await SqlSugarContext.DbContext.Queryable<DictItemEntity>()
|
||||
.Where(x => x.Id == id && x.IsDel == 0).FirstAsync();
|
||||
if (entity == null) return Result.Error("字典项不存在或已被删除");
|
||||
|
||||
await SqlSugarContext.DbContext.Updateable<DictItemEntity>()
|
||||
.SetColumns(x => x.IsDel == 1)
|
||||
.Where(x => x.Id == id).ExecuteCommandAsync();
|
||||
|
||||
InvalidateCache(await GetTypeCodeAsync(entity.TypeId));
|
||||
return Result.Success();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Error("删除字典项失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 业务侧:按 typeCode 查询启用项(带缓存) ====================
|
||||
|
||||
public async Task<Result<List<DictOptionDto>>> GetOptionsByCodeAsync(string typeCode)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(typeCode))
|
||||
return Result<List<DictOptionDto>>.Error("字典编码不能为空");
|
||||
|
||||
try
|
||||
{
|
||||
var key = $"dict:options:{typeCode}";
|
||||
if (!_cache.TryGetValue(key, out List<DictOptionDto>? cached) || cached == null)
|
||||
{
|
||||
lock (_cacheLock)
|
||||
{
|
||||
cached ??= LoadOptionsByCodeAsync(typeCode).GetAwaiter().GetResult();
|
||||
_cache.Set(key, cached, TimeSpan.FromHours(1));
|
||||
}
|
||||
}
|
||||
return Result<List<DictOptionDto>>.Success(cached);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<List<DictOptionDto>>.Error("查询字典选项失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 私有工具 ====================
|
||||
|
||||
private async Task<string> GetTypeCodeAsync(long typeId)
|
||||
{
|
||||
var code = await SqlSugarContext.DbContext.Queryable<DictTypeEntity>()
|
||||
.Where(x => x.Id == typeId).Select(x => x.Code).FirstAsync();
|
||||
return code ?? string.Empty;
|
||||
}
|
||||
|
||||
private async Task<List<DictOptionDto>> LoadOptionsByCodeAsync(string typeCode)
|
||||
{
|
||||
var db = SqlSugarContext.DbContext;
|
||||
var typeId = await db.Queryable<DictTypeEntity>()
|
||||
.Where(x => x.Code == typeCode && x.IsDel == 0 && x.Status == 1)
|
||||
.Select(x => x.Id).FirstAsync();
|
||||
if (typeId == 0) return new List<DictOptionDto>();
|
||||
|
||||
var items = await db.Queryable<DictItemEntity>()
|
||||
.Where(x => x.TypeId == typeId && x.IsDel == 0 && x.Status == 1)
|
||||
.OrderBy(x => x.Sort).OrderBy(x => x.Code)
|
||||
.ToListAsync();
|
||||
|
||||
return items.Select(e => new DictOptionDto
|
||||
{
|
||||
Code = e.Code,
|
||||
Name = e.Name,
|
||||
IsDefault = e.IsDefault == 1
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
private void InvalidateCache(string? typeCode)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(typeCode)) return;
|
||||
_cache.Remove($"dict:options:{typeCode}");
|
||||
}
|
||||
|
||||
private async Task FillItemCountAsync(List<DictTypeDto> dtos)
|
||||
{
|
||||
if (dtos == null || dtos.Count == 0) return;
|
||||
var ids = dtos.Select(d => long.Parse(d.Id)).ToList();
|
||||
var counts = await SqlSugarContext.DbContext.Queryable<DictItemEntity>()
|
||||
.Where(x => ids.Contains(x.TypeId) && x.IsDel == 0)
|
||||
.GroupBy(x => x.TypeId)
|
||||
.Select(x => new { TypeId = x.TypeId, Count = SqlFunc.AggregateCount(x.Id) })
|
||||
.ToListAsync();
|
||||
var map = counts.ToDictionary(c => c.TypeId, c => c.Count);
|
||||
foreach (var d in dtos) d.ItemCount = map.TryGetValue(long.Parse(d.Id), out var c) ? c : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,322 @@
|
||||
using Model;
|
||||
using Model.Dto.System;
|
||||
using Model.Entity.System;
|
||||
using Model.Mapper;
|
||||
using ORM;
|
||||
using Service.Interface;
|
||||
using SqlSugar;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Service.Implement
|
||||
{
|
||||
/// <summary>
|
||||
/// 文件存储管理 服务实现
|
||||
/// 通过 IEnumerable<IFileStorageProvider> 按配置 Provider 字段挑选实现;当前仅 Local 落地
|
||||
/// </summary>
|
||||
public class FileStorageService : IFileStorageService
|
||||
{
|
||||
// TODO: 实现 文件存储管理 相关方法
|
||||
private readonly IEnumerable<IFileStorageProvider> _providers;
|
||||
private readonly ICurrentUser _currentUser;
|
||||
|
||||
// 掩码标记:前端回显 AccessKey/SecretKey 时用 **** 脱敏,回写时若仍为掩码则保留原值
|
||||
private const string MaskMarker = "****";
|
||||
|
||||
public FileStorageService(IEnumerable<IFileStorageProvider> providers, ICurrentUser currentUser)
|
||||
{
|
||||
_providers = providers;
|
||||
_currentUser = currentUser;
|
||||
}
|
||||
|
||||
// ==================== 存储配置 ====================
|
||||
|
||||
public async Task<Result<List<FileStorageConfigDto>>> GetListPagedAsync(int pageIndex, int pageSize, RefAsync<int> total, string? keyword = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var list = await SqlSugarContext.DbContext.Queryable<FileStorageConfigEntity>()
|
||||
.Where(x => x.IsDel == 0)
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(keyword), x => x.Name.Contains(keyword!) || x.Provider.Contains(keyword!))
|
||||
.OrderBy(x => x.IsDefault, OrderByType.Desc).OrderBy(x => x.CreateTime, OrderByType.Desc)
|
||||
.ToPageListAsync(pageIndex, pageSize, total);
|
||||
var dtos = list.ToDtoList();
|
||||
// AccessKey/SecretKey 脱敏回显
|
||||
foreach (var d in dtos) { d.AccessKey = Mask(d.AccessKey); d.SecretKey = Mask(d.SecretKey); }
|
||||
return Result<List<FileStorageConfigDto>>.Success(dtos);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<List<FileStorageConfigDto>>.Error("查询存储配置失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result<FileStorageConfigDto>> GetByIdAsync(long id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var entity = await SqlSugarContext.DbContext.Queryable<FileStorageConfigEntity>()
|
||||
.Where(x => x.Id == id && x.IsDel == 0).FirstAsync();
|
||||
if (entity == null) return Result<FileStorageConfigDto>.Error("存储配置不存在或已被删除");
|
||||
var dto = entity.ToDto();
|
||||
dto.AccessKey = Mask(dto.AccessKey);
|
||||
dto.SecretKey = Mask(dto.SecretKey);
|
||||
return Result<FileStorageConfigDto>.Success(dto);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<FileStorageConfigDto>.Error("查询存储配置失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result<FileStorageConfigDto>> AddAsync(FileStorageConfigDto dto)
|
||||
{
|
||||
if (dto == null || string.IsNullOrWhiteSpace(dto.Provider) || string.IsNullOrWhiteSpace(dto.Name))
|
||||
return Result<FileStorageConfigDto>.Error("Provider 和名称不能为空");
|
||||
if (!IsProviderSupported(dto.Provider))
|
||||
return Result<FileStorageConfigDto>.Error($"不支持的 Provider:{dto.Provider}(当前仅支持 local)");
|
||||
|
||||
try
|
||||
{
|
||||
var entity = dto.ToEntity();
|
||||
entity.Id = 0;
|
||||
entity.CreateTime = DateTime.Now;
|
||||
if (entity.IsDefault == 1) await ClearOtherDefaultAsync(0);
|
||||
var id = await SqlSugarContext.DbContext.Insertable(entity).ExecuteReturnSnowflakeIdAsync();
|
||||
entity.Id = id;
|
||||
var result = entity.ToDto();
|
||||
result.AccessKey = Mask(result.AccessKey);
|
||||
result.SecretKey = Mask(result.SecretKey);
|
||||
return Result<FileStorageConfigDto>.Success(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<FileStorageConfigDto>.Error("新增存储配置失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result<FileStorageConfigDto>> UpdateAsync(FileStorageConfigDto dto)
|
||||
{
|
||||
if (dto == null || !long.TryParse(dto.Id, out var id) || id <= 0)
|
||||
return Result<FileStorageConfigDto>.Error("配置Id无效");
|
||||
if (!string.IsNullOrWhiteSpace(dto.Provider) && !IsProviderSupported(dto.Provider))
|
||||
return Result<FileStorageConfigDto>.Error($"不支持的 Provider:{dto.Provider}");
|
||||
|
||||
try
|
||||
{
|
||||
var entity = await SqlSugarContext.DbContext.Queryable<FileStorageConfigEntity>()
|
||||
.Where(x => x.Id == id && x.IsDel == 0).FirstAsync();
|
||||
if (entity == null) return Result<FileStorageConfigDto>.Error("存储配置不存在或已被删除");
|
||||
|
||||
// AccessKey/SecretKey 为掩码时保留原值(前端未改密钥)
|
||||
var newEntity = dto.ToEntity();
|
||||
newEntity.Id = id;
|
||||
newEntity.CreateTime = entity.CreateTime;
|
||||
if (string.IsNullOrEmpty(newEntity.AccessKey) || newEntity.AccessKey.Contains(MaskMarker))
|
||||
newEntity.AccessKey = entity.AccessKey;
|
||||
if (string.IsNullOrEmpty(newEntity.SecretKey) || newEntity.SecretKey.Contains(MaskMarker))
|
||||
newEntity.SecretKey = entity.SecretKey;
|
||||
|
||||
if (newEntity.IsDefault == 1) await ClearOtherDefaultAsync(id);
|
||||
await SqlSugarContext.DbContext.Updateable(newEntity)
|
||||
.IgnoreColumns(x => new { x.CreateTime, x.IsDel })
|
||||
.ExecuteCommandAsync();
|
||||
var result = newEntity.ToDto();
|
||||
result.AccessKey = Mask(result.AccessKey);
|
||||
result.SecretKey = Mask(result.SecretKey);
|
||||
return Result<FileStorageConfigDto>.Success(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<FileStorageConfigDto>.Error("修改存储配置失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result> DeleteAsync(long id)
|
||||
{
|
||||
if (id <= 0) return Result.Error("配置Id无效");
|
||||
try
|
||||
{
|
||||
var entity = await SqlSugarContext.DbContext.Queryable<FileStorageConfigEntity>()
|
||||
.Where(x => x.Id == id && x.IsDel == 0).FirstAsync();
|
||||
if (entity == null) return Result.Error("存储配置不存在或已被删除");
|
||||
if (entity.IsDefault == 1) return Result.Error($"配置「{entity.Name}」是默认通道,禁止删除(请先切换默认)");
|
||||
|
||||
await SqlSugarContext.DbContext.Updateable<FileStorageConfigEntity>()
|
||||
.SetColumns(x => x.IsDel == 1)
|
||||
.Where(x => x.Id == id).ExecuteCommandAsync();
|
||||
return Result.Success();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Error("删除存储配置失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result> SetDefaultAsync(long id)
|
||||
{
|
||||
if (id <= 0) return Result.Error("配置Id无效");
|
||||
try
|
||||
{
|
||||
var entity = await SqlSugarContext.DbContext.Queryable<FileStorageConfigEntity>()
|
||||
.Where(x => x.Id == id && x.IsDel == 0).FirstAsync();
|
||||
if (entity == null) return Result.Error("存储配置不存在或已被删除");
|
||||
if (entity.Status == 0) return Result.Error("已停用的配置不能设为默认");
|
||||
|
||||
await ClearOtherDefaultAsync(id);
|
||||
await SqlSugarContext.DbContext.Updateable<FileStorageConfigEntity>()
|
||||
.SetColumns(x => x.IsDefault == 1)
|
||||
.Where(x => x.Id == id).ExecuteCommandAsync();
|
||||
return Result.Success();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Error("设置默认存储失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result> TestAsync(long id)
|
||||
{
|
||||
if (id <= 0) return Result.Error("配置Id无效");
|
||||
try
|
||||
{
|
||||
var entity = await SqlSugarContext.DbContext.Queryable<FileStorageConfigEntity>()
|
||||
.Where(x => x.Id == id && x.IsDel == 0).FirstAsync();
|
||||
if (entity == null) return Result.Error("存储配置不存在或已被删除");
|
||||
|
||||
var provider = ResolveProvider(entity.Provider);
|
||||
if (provider == null) return Result.Error($"未注册 Provider:{entity.Provider}");
|
||||
return await provider.TestConnectionAsync(entity);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Error("测试连接失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 文件上传/记录 ====================
|
||||
|
||||
public async Task<Result<FileRecordDto>> UploadAsync(Stream stream, string originalName, long size, string? bizType = null, string? bizId = null, string? uploader = null)
|
||||
{
|
||||
if (stream == null || size <= 0) return Result<FileRecordDto>.Error("文件流或大小无效");
|
||||
if (string.IsNullOrWhiteSpace(originalName)) return Result<FileRecordDto>.Error("文件名不能为空");
|
||||
|
||||
try
|
||||
{
|
||||
// 取默认启用的存储配置
|
||||
var config = await SqlSugarContext.DbContext.Queryable<FileStorageConfigEntity>()
|
||||
.Where(x => x.IsDel == 0 && x.IsDefault == 1 && x.Status == 1).FirstAsync();
|
||||
if (config == null) return Result<FileRecordDto>.Error("未配置默认存储通道,请先在「文件存储管理」新增并设为默认");
|
||||
|
||||
// 大小校验
|
||||
if (config.MaxSizeMB > 0 && size > config.MaxSizeMB * 1024L * 1024L)
|
||||
return Result<FileRecordDto>.Error($"文件大小超过上限 {config.MaxSizeMB}MB");
|
||||
|
||||
// 扩展名校验
|
||||
var ext = Path.GetExtension(originalName)?.ToLowerInvariant() ?? "";
|
||||
var allowed = ParseExts(config.AllowedExts);
|
||||
if (allowed.Count > 0 && !allowed.Contains(ext))
|
||||
return Result<FileRecordDto>.Error($"不支持的文件类型 {ext}(允许:{string.Join(",", allowed)})");
|
||||
|
||||
var provider = ResolveProvider(config.Provider);
|
||||
if (provider == null) return Result<FileRecordDto>.Error($"未注册 Provider:{config.Provider}");
|
||||
|
||||
var upRes = await provider.UploadAsync(stream, originalName, size, config);
|
||||
if (!upRes.IsSuccess || upRes.Data == null) return Result<FileRecordDto>.Error(upRes.Msg);
|
||||
|
||||
var record = new FileRecordEntity
|
||||
{
|
||||
FileName = upRes.Data.FileName,
|
||||
OriginalName = originalName,
|
||||
Url = upRes.Data.Url,
|
||||
Size = size,
|
||||
Ext = ext,
|
||||
Provider = config.Provider,
|
||||
StorageId = config.Id,
|
||||
Uploader = string.IsNullOrWhiteSpace(uploader) ? _currentUser.UserName : uploader,
|
||||
BizType = string.IsNullOrWhiteSpace(bizType) ? "generic" : bizType,
|
||||
BizId = bizId,
|
||||
CreateTime = DateTime.Now
|
||||
};
|
||||
var rid = await SqlSugarContext.DbContext.Insertable(record).ExecuteReturnSnowflakeIdAsync();
|
||||
record.Id = rid;
|
||||
return Result<FileRecordDto>.Success(record.ToDto());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<FileRecordDto>.Error("文件上传失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result<List<FileRecordDto>>> GetFileListPagedAsync(int pageIndex, int pageSize, RefAsync<int> total, string? keyword = null, string? bizType = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var list = await SqlSugarContext.DbContext.Queryable<FileRecordEntity>()
|
||||
.Where(x => x.IsDel == 0)
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(keyword), x => x.FileName.Contains(keyword!) || x.OriginalName!.Contains(keyword!))
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(bizType), x => x.BizType == bizType)
|
||||
.OrderBy(x => x.CreateTime, OrderByType.Desc)
|
||||
.ToPageListAsync(pageIndex, pageSize, total);
|
||||
return Result<List<FileRecordDto>>.Success(list.ToDtoList());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<List<FileRecordDto>>.Error("查询文件记录失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result<FileRecordDto>> GetFileByIdAsync(long id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var entity = await SqlSugarContext.DbContext.Queryable<FileRecordEntity>()
|
||||
.Where(x => x.Id == id && x.IsDel == 0).FirstAsync();
|
||||
if (entity == null) return Result<FileRecordDto>.Error("文件记录不存在或已被删除");
|
||||
return Result<FileRecordDto>.Success(entity.ToDto());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<FileRecordDto>.Error("查询文件记录失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 私有工具 ====================
|
||||
|
||||
private IFileStorageProvider? ResolveProvider(string? providerName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(providerName)) return null;
|
||||
return _providers.FirstOrDefault(p => p.ProviderName == providerName);
|
||||
}
|
||||
|
||||
private static bool IsProviderSupported(string provider)
|
||||
=> provider == "local" || provider == "minio" || provider == "oss";
|
||||
|
||||
private static string? Mask(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value)) return value;
|
||||
if (value.Length <= 4) return MaskMarker;
|
||||
return value[..2] + MaskMarker + value[^2..];
|
||||
}
|
||||
|
||||
private static List<string> ParseExts(string? json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json)) return new List<string>();
|
||||
try
|
||||
{
|
||||
var arr = System.Text.Json.JsonSerializer.Deserialize<List<string>>(json);
|
||||
return arr?.Select(e => e.ToLowerInvariant()).ToList() ?? new List<string>();
|
||||
}
|
||||
catch { return new List<string>(); }
|
||||
}
|
||||
|
||||
private async Task ClearOtherDefaultAsync(long keepId)
|
||||
{
|
||||
await SqlSugarContext.DbContext.Updateable<FileStorageConfigEntity>()
|
||||
.SetColumns(x => x.IsDefault == 0)
|
||||
.Where(x => x.IsDel == 0 && x.Id != keepId).ExecuteCommandAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,257 @@
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Model;
|
||||
using Model.Dto.System;
|
||||
using Model.Entity.System;
|
||||
using Model.Mapper;
|
||||
using ORM;
|
||||
using Service.Interface;
|
||||
using SqlSugar;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Service.Implement
|
||||
{
|
||||
/// <summary>
|
||||
/// 系统参数配置 服务实现
|
||||
/// 缓存策略:按 ParamKey 缓存值;增删改时失效对应 Key(Key 变更时新旧都失效)
|
||||
/// </summary>
|
||||
public class SystemParamService : ISystemParamService
|
||||
{
|
||||
// TODO: 实现 系统参数配置 相关方法
|
||||
private readonly IMemoryCache _cache;
|
||||
private readonly ICurrentUser _currentUser;
|
||||
private static readonly object _cacheLock = new();
|
||||
|
||||
public SystemParamService(IMemoryCache cache, ICurrentUser currentUser)
|
||||
{
|
||||
_cache = cache;
|
||||
_currentUser = currentUser;
|
||||
}
|
||||
|
||||
// ==================== 管理侧 ====================
|
||||
|
||||
public async Task<Result<List<SystemParamDto>>> GetListPagedAsync(int pageIndex, int pageSize, RefAsync<int> total, string? keyword = null, string? group = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var list = await SqlSugarContext.DbContext.Queryable<SystemParamEntity>()
|
||||
.Where(x => x.IsDel == 0)
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(keyword), x => x.ParamKey.Contains(keyword!) || x.ParamName.Contains(keyword!))
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(group), x => x.Group == group)
|
||||
.OrderBy(x => x.Sort).OrderBy(x => x.CreateTime, OrderByType.Desc)
|
||||
.ToPageListAsync(pageIndex, pageSize, total);
|
||||
return Result<List<SystemParamDto>>.Success(list.ToDtoList());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<List<SystemParamDto>>.Error("查询系统参数失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result<SystemParamDto>> GetByIdAsync(long id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var entity = await SqlSugarContext.DbContext.Queryable<SystemParamEntity>()
|
||||
.Where(x => x.Id == id && x.IsDel == 0).FirstAsync();
|
||||
if (entity == null) return Result<SystemParamDto>.Error("系统参数不存在或已被删除");
|
||||
return Result<SystemParamDto>.Success(entity.ToDto());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<SystemParamDto>.Error("查询系统参数详情失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result<SystemParamDto>> AddAsync(SystemParamDto dto)
|
||||
{
|
||||
if (dto == null || string.IsNullOrWhiteSpace(dto.ParamKey) || string.IsNullOrWhiteSpace(dto.ParamName))
|
||||
return Result<SystemParamDto>.Error("参数键和名称不能为空");
|
||||
|
||||
try
|
||||
{
|
||||
var keyExists = await SqlSugarContext.DbContext.Queryable<SystemParamEntity>()
|
||||
.Where(x => x.ParamKey == dto.ParamKey && x.IsDel == 0).AnyAsync();
|
||||
if (keyExists) return Result<SystemParamDto>.Error($"参数键「{dto.ParamKey}」已存在");
|
||||
|
||||
var entity = dto.ToEntity();
|
||||
entity.Id = 0;
|
||||
entity.IsSystem = 0; // 用户新增的不是内置参数
|
||||
entity.CreateTime = DateTime.Now;
|
||||
entity.LastUpdateUser = _currentUser.UserName;
|
||||
entity.LastUpdateTime = DateTime.Now;
|
||||
var id = await SqlSugarContext.DbContext.Insertable(entity).ExecuteReturnSnowflakeIdAsync();
|
||||
entity.Id = id;
|
||||
InvalidateCache(entity.ParamKey);
|
||||
return Result<SystemParamDto>.Success(entity.ToDto());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<SystemParamDto>.Error("新增系统参数失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result<SystemParamDto>> UpdateAsync(SystemParamDto dto)
|
||||
{
|
||||
if (dto == null || !long.TryParse(dto.Id, out var id) || id <= 0)
|
||||
return Result<SystemParamDto>.Error("参数Id无效");
|
||||
|
||||
try
|
||||
{
|
||||
var entity = await SqlSugarContext.DbContext.Queryable<SystemParamEntity>()
|
||||
.Where(x => x.Id == id && x.IsDel == 0).FirstAsync();
|
||||
if (entity == null) return Result<SystemParamDto>.Error("系统参数不存在或已被删除");
|
||||
|
||||
// Key 变更时校验唯一
|
||||
if (!string.Equals(entity.ParamKey, dto.ParamKey, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var keyExists = await SqlSugarContext.DbContext.Queryable<SystemParamEntity>()
|
||||
.Where(x => x.ParamKey == dto.ParamKey && x.Id != id && x.IsDel == 0).AnyAsync();
|
||||
if (keyExists) return Result<SystemParamDto>.Error($"参数键「{dto.ParamKey}」已存在");
|
||||
}
|
||||
|
||||
var oldKey = entity.ParamKey;
|
||||
var now = DateTime.Now;
|
||||
var userName = _currentUser.UserName;
|
||||
|
||||
if (entity.IsSystem == 1)
|
||||
{
|
||||
// 内置参数仅允许改 Value/Remark,其它字段保留
|
||||
entity.ParamValue = dto.ParamValue;
|
||||
entity.Remark = dto.Remark;
|
||||
entity.LastUpdateUser = userName;
|
||||
entity.LastUpdateTime = now;
|
||||
await SqlSugarContext.DbContext.Updateable(entity)
|
||||
.UpdateColumns(x => new { x.ParamValue, x.Remark, x.LastUpdateUser, x.LastUpdateTime })
|
||||
.ExecuteCommandAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
// 用户自定义参数:全字段更新(审计字段由服务端控管)
|
||||
var newEntity = dto.ToEntity();
|
||||
newEntity.Id = id;
|
||||
newEntity.IsSystem = 0;
|
||||
newEntity.CreateTime = entity.CreateTime;
|
||||
newEntity.LastUpdateUser = userName;
|
||||
newEntity.LastUpdateTime = now;
|
||||
await SqlSugarContext.DbContext.Updateable(newEntity)
|
||||
.IgnoreColumns(x => new { x.CreateTime, x.IsDel })
|
||||
.ExecuteCommandAsync();
|
||||
entity = newEntity;
|
||||
}
|
||||
|
||||
InvalidateCache(oldKey);
|
||||
InvalidateCache(entity.ParamKey);
|
||||
return Result<SystemParamDto>.Success(entity.ToDto());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<SystemParamDto>.Error("修改系统参数失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result> DeleteAsync(long id)
|
||||
{
|
||||
if (id <= 0) return Result.Error("参数Id无效");
|
||||
try
|
||||
{
|
||||
var entity = await SqlSugarContext.DbContext.Queryable<SystemParamEntity>()
|
||||
.Where(x => x.Id == id && x.IsDel == 0).FirstAsync();
|
||||
if (entity == null) return Result.Error("系统参数不存在或已被删除");
|
||||
if (entity.IsSystem == 1) return Result.Error($"内置参数「{entity.ParamName}」禁止删除,仅允许修改值");
|
||||
|
||||
await SqlSugarContext.DbContext.Updateable<SystemParamEntity>()
|
||||
.SetColumns(x => x.IsDel == 1)
|
||||
.Where(x => x.Id == id).ExecuteCommandAsync();
|
||||
InvalidateCache(entity.ParamKey);
|
||||
return Result.Success();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Error("删除系统参数失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public Task<Result> ReloadCacheAsync()
|
||||
{
|
||||
// IMemoryCache 非强类型缓存清除,此处复用一个枚举 Key 的策略:清除所有 sysparam: 前缀缓存需要 ICache 抽象;
|
||||
// 当前实现:通过 Coherent MemoryCache 实例的 compact,按约定不实现细粒度清除,仅返回成功提示由进程内自然过期
|
||||
// 注:增删改单条时已按 Key 失效;本接口预留给运维紧急场景,下次访问会重新加载
|
||||
try
|
||||
{
|
||||
// 由于 IMemoryCache 没有枚举能力,这里仅作占位返回;如需全量清除,请重启服务或改用 IDistributedCache
|
||||
return Task.FromResult(Result.Success());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Task.FromResult(Result.Error("清空缓存失败", ex));
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 业务侧:按 Key 取值(带缓存) ====================
|
||||
|
||||
public async Task<Result<string>> GetValueAsync(string key)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
return Result<string>.Error("参数键不能为空");
|
||||
try
|
||||
{
|
||||
var value = await LoadValueByKeyAsync(key);
|
||||
return Result<string>.Success(value ?? string.Empty);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<string>.Error("查询参数值失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result<List<SystemParamValueDto>>> GetValuesAsync(string[] keys)
|
||||
{
|
||||
if (keys == null || keys.Length == 0)
|
||||
return Result<List<SystemParamValueDto>>.Error("参数键列表不能为空");
|
||||
try
|
||||
{
|
||||
var distinctKeys = keys.Where(k => !string.IsNullOrWhiteSpace(k)).Distinct().ToList();
|
||||
var list = new List<SystemParamValueDto>(distinctKeys.Count);
|
||||
foreach (var k in distinctKeys)
|
||||
{
|
||||
var v = await LoadValueByKeyAsync(k);
|
||||
list.Add(new SystemParamValueDto { Key = k, Value = v ?? string.Empty });
|
||||
}
|
||||
return Result<List<SystemParamValueDto>>.Success(list);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<List<SystemParamValueDto>>.Error("批量查询参数值失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 私有工具 ====================
|
||||
|
||||
private async Task<string?> LoadValueByKeyAsync(string key)
|
||||
{
|
||||
var cacheKey = $"sysparam:{key}";
|
||||
if (!_cache.TryGetValue(cacheKey, out string? cached))
|
||||
{
|
||||
lock (_cacheLock)
|
||||
{
|
||||
if (!_cache.TryGetValue(cacheKey, out cached))
|
||||
{
|
||||
cached = SqlSugarContext.DbContext.Queryable<SystemParamEntity>()
|
||||
.Where(x => x.ParamKey == key && x.IsDel == 0)
|
||||
.Select(x => x.ParamValue).First();
|
||||
_cache.Set(cacheKey, cached ?? string.Empty, TimeSpan.FromHours(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
return cached;
|
||||
}
|
||||
|
||||
private void InvalidateCache(string? key)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key)) return;
|
||||
_cache.Remove($"sysparam:{key}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user