新增分段管理
This commit is contained in:
@@ -40,6 +40,7 @@ public static class PageCatalog
|
||||
new("admin-processes", "进程管理", "设计与编排", ScopePlatform),
|
||||
new("admin-scripts", "脚本管理", "设计与编排", ScopePlatform),
|
||||
new("admin-task-templates", "任务编排", "设计与编排", ScopePlatform),
|
||||
new("admin-simple-fields", "字段管理", "设计与编排", ScopePlatform),
|
||||
|
||||
// ── 管理端 / Platform:平台配置中心(聚合页,每个 Key 对齐前端聚合路由 route.name) ──
|
||||
new("admin-config-strategy", "调度策略", "平台配置中心", ScopePlatform),
|
||||
|
||||
@@ -120,6 +120,12 @@ public sealed class RbacStore
|
||||
&& !r.Pages.Contains("admin-task-templates", StringComparer.OrdinalIgnoreCase)
|
||||
&& hasProcessAndScript)
|
||||
r.Pages.Add("admin-task-templates");
|
||||
|
||||
// Simple 字段管理:与任务编排同属设计与编排,有任务编排权限时自动补齐。
|
||||
if (!r.Pages.Contains(PageCatalog.Wildcard)
|
||||
&& !r.Pages.Contains("admin-simple-fields", StringComparer.OrdinalIgnoreCase)
|
||||
&& r.Pages.Contains("admin-task-templates", StringComparer.OrdinalIgnoreCase))
|
||||
r.Pages.Add("admin-simple-fields");
|
||||
}
|
||||
|
||||
private RbacSnapshot SeedDefault(IConfiguration config)
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace MiGu.Server.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 投影 API 占位:真实落地时由 YARP 反代到 SimpleLite WebAPI 的 /api/projection/* 路径。
|
||||
/// 本地 Mock 数据仅用于无 SimpleLite 运行时的开发联调。
|
||||
///
|
||||
/// AR-4: 全 class 加 [Authorize] —— 任何登录用户都能读 mock 投影数据;未登录直接 401。
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[Route("api/projection")]
|
||||
public class ProjectionController : ControllerBase
|
||||
{
|
||||
[HttpGet("sites")]
|
||||
public IActionResult Sites() => Ok(new[]
|
||||
{
|
||||
new { id = "S001", name = "A 区-入库点", x = 1000, y = 2000 },
|
||||
new { id = "S002", name = "A 区-出库点", x = 3000, y = 2000 },
|
||||
new { id = "S003", name = "B 区-缓存区", x = 5000, y = 2000 }
|
||||
});
|
||||
|
||||
[HttpGet("tracks")]
|
||||
public IActionResult Tracks() => Ok(new[]
|
||||
{
|
||||
new { id = "T001", kind = "line", fromSiteId = "S001", toSiteId = "S002" },
|
||||
new { id = "T002", kind = "line", fromSiteId = "S002", toSiteId = "S003" }
|
||||
});
|
||||
|
||||
[HttpGet("cars")]
|
||||
public IActionResult Cars() => Ok(new[]
|
||||
{
|
||||
new { id = "C01", name = "AGV-001", state = "running", batterySoc = 0.86 },
|
||||
new { id = "C02", name = "AGV-002", state = "idle", batterySoc = 0.42 }
|
||||
});
|
||||
|
||||
[HttpGet("missions")]
|
||||
public IActionResult Missions() => Ok(new[]
|
||||
{
|
||||
new { id = "M01", name = "A 区送料 #1", status = "running", priority = 50 },
|
||||
new { id = "M02", name = "A→B 缓存搬运", status = "queued", priority = 60 }
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using MiGu.Server.SimpleFields;
|
||||
|
||||
namespace MiGu.Server.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Simple 字段管理 API:按车型维护 site / track / plan / car 四类反射字段的默认值与多语言名称。
|
||||
/// 数据持久化于 <c>platform.db</c> 的 <c>simple_fields</c> 表。
|
||||
///
|
||||
/// 对应前端「字段管理」页(<c>/admin/simple-fields</c>,页面 key <c>admin-simple-fields</c>)。
|
||||
/// 前端主流程为「刷新」读库、「默认」从 SimpleLite 拉取模板、「保存」走 <see cref="SaveBatch"/> 全量替换。
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[TypeFilter(typeof(SimpleFieldExceptionFilter))]
|
||||
[Route("api/simple-fields")]
|
||||
public sealed class SimpleFieldController : ControllerBase
|
||||
{
|
||||
private readonly SimpleFieldService _service;
|
||||
|
||||
public SimpleFieldController(SimpleFieldService service) => _service = service;
|
||||
|
||||
/// <summary>
|
||||
/// 查询字段列表,支持按字段类型、车型与关键字过滤。
|
||||
/// 结果按 car_type → field_type → key 排序。
|
||||
/// </summary>
|
||||
/// <param name="fieldType">字段类型,如 <c>siteFields</c>、<c>carFields</c>。</param>
|
||||
/// <param name="carType">车型唯一标识:<c>assemblyName.shortName</c>。</param>
|
||||
/// <param name="q">关键字,匹配 key / car_type / 中英文名 / 其他语言 / 默认值。</param>
|
||||
[HttpGet]
|
||||
public Task<List<SimpleField>> List([FromQuery] string? fieldType, [FromQuery] string? carType, [FromQuery] string? q) => _service.ListAsync(fieldType, carType, q);
|
||||
|
||||
/// <summary>
|
||||
/// 新增单条字段;同车型 + 字段类型下 key 不可重复
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
public Task<SimpleField> Create([FromBody] SimpleFieldRequest req) => _service.SaveAsync(req);
|
||||
|
||||
/// <summary>
|
||||
/// 按 id 更新单条字段
|
||||
/// </summary>
|
||||
[HttpPut("{id:guid}")]
|
||||
public Task<SimpleField> Update(Guid id, [FromBody] SimpleFieldRequest req) => _service.SaveAsync(req with { Id = id });
|
||||
|
||||
/// <summary>
|
||||
/// 按 id 删除单条字段
|
||||
/// </summary>
|
||||
[HttpDelete("{id:guid}")]
|
||||
public async Task<IActionResult> Delete(Guid id)
|
||||
{
|
||||
await _service.DeleteAsync(id);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量保存字段
|
||||
/// <paramref name="req"/>.<see cref="SimpleFieldBatchRequest.ReplaceAll"/> 为 <c>true</c> 时先清空表再写入(前端「保存」使用此模式)。
|
||||
/// 返回实际写入条数 <c>{ count }</c>。
|
||||
/// </summary>
|
||||
[HttpPost("batch")]
|
||||
public async Task<IActionResult> SaveBatch([FromBody] SimpleFieldBatchRequest req)
|
||||
{
|
||||
var count = await _service.SaveBatchAsync(req);
|
||||
|
||||
return Ok(new { count });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 <see cref="SimpleFieldException"/> 转为 HTTP 400,响应体 <c>{ message }</c>
|
||||
/// </summary>
|
||||
public sealed class SimpleFieldExceptionFilter : IExceptionFilter
|
||||
{
|
||||
public void OnException(ExceptionContext context)
|
||||
{
|
||||
if (context.Exception is not SimpleFieldException ex) { return; }
|
||||
|
||||
context.Result = new BadRequestObjectResult(new { message = ex.Message });
|
||||
|
||||
context.ExceptionHandled = true;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using MiGu.Server.Wms;
|
||||
using MiGu.Server.SimpleFields;
|
||||
|
||||
namespace MiGu.Server.Persistence;
|
||||
|
||||
@@ -16,6 +17,7 @@ public sealed class PlatformDbContext : DbContext
|
||||
public DbSet<ContainerMaterial> ContainerMaterials => Set<ContainerMaterial>();
|
||||
public DbSet<ContainerLocationHistory> ContainerLocationHistories => Set<ContainerLocationHistory>();
|
||||
public DbSet<ContainerMaterialHistory> ContainerMaterialHistories => Set<ContainerMaterialHistory>();
|
||||
public DbSet<SimpleField> SimpleFields => Set<SimpleField>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
@@ -55,6 +57,31 @@ public sealed class PlatformDbContext : DbContext
|
||||
|
||||
modelBuilder.Entity<ContainerMaterial>().Property(x => x.Quantity).HasPrecision(18, 4);
|
||||
modelBuilder.Entity<ContainerMaterialHistory>().Property(x => x.QuantityDelta).HasPrecision(18, 4);
|
||||
|
||||
ConfigureSimpleField(modelBuilder);
|
||||
}
|
||||
|
||||
private static void ConfigureSimpleField(ModelBuilder modelBuilder)
|
||||
{
|
||||
var e = modelBuilder.Entity<SimpleField>();
|
||||
e.ToTable("simple_fields");
|
||||
e.HasKey(x => x.Id);
|
||||
e.Property(x => x.Id).HasColumnName("id");
|
||||
e.Property(x => x.CarType).HasColumnName("car_type").HasMaxLength(64);
|
||||
e.Property(x => x.FieldType).HasColumnName("field_type").HasMaxLength(64);
|
||||
e.Property(x => x.Key).HasColumnName("key").HasMaxLength(128);
|
||||
e.Property(x => x.Value).HasColumnName("value");
|
||||
e.Property(x => x.DataType).HasColumnName("data_type").HasMaxLength(128);
|
||||
e.Property(x => x.Chinese).HasColumnName("chinese").HasMaxLength(256).IsRequired(false);
|
||||
e.Property(x => x.English).HasColumnName("english").HasMaxLength(256).IsRequired(false);
|
||||
e.Property(x => x.Other).HasColumnName("other").HasMaxLength(512);
|
||||
e.Property(x => x.IsDefault).HasColumnName("is_default");
|
||||
var dateTime = new ValueConverter<DateTimeOffset, string>(
|
||||
v => SimpleFieldDateTime.ToStorage(v),
|
||||
v => SimpleFieldDateTime.FromStorage(v));
|
||||
e.Property(x => x.CreateTime).HasColumnName("create_time").HasConversion(dateTime).HasMaxLength(19);
|
||||
e.Property(x => x.UpdateTime).HasColumnName("update_time").HasConversion(dateTime).HasMaxLength(19);
|
||||
e.HasIndex(x => new { x.CarType, x.FieldType, x.Key }).IsUnique();
|
||||
}
|
||||
|
||||
public override int SaveChanges(bool acceptAllChangesOnSuccess)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using MiGu.Server.Wms;
|
||||
using MiGu.Server.SimpleFields;
|
||||
|
||||
namespace MiGu.Server.Persistence;
|
||||
|
||||
@@ -38,6 +40,7 @@ public static class PlatformPersistence
|
||||
|
||||
services.AddScoped<WmsReferenceValidator>();
|
||||
services.AddScoped<WmsService>();
|
||||
services.AddScoped<SimpleFieldService>();
|
||||
return services;
|
||||
}
|
||||
|
||||
@@ -46,6 +49,97 @@ public static class PlatformPersistence
|
||||
using var scope = services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<PlatformDbContext>();
|
||||
await db.Database.EnsureCreatedAsync();
|
||||
// EnsureCreated 只在「库文件不存在」时建表;已有 platform.db 时新增实体不会自动补表。
|
||||
await EnsureSimpleFieldsTableAsync(db);
|
||||
}
|
||||
|
||||
/// <summary>为已存在的数据库补建 simple_fields 表(幂等)。</summary>
|
||||
private static async Task EnsureSimpleFieldsTableAsync(PlatformDbContext db)
|
||||
{
|
||||
if (db.Database.IsSqlite())
|
||||
{
|
||||
await db.Database.ExecuteSqlRawAsync("""
|
||||
CREATE TABLE IF NOT EXISTS simple_fields (
|
||||
id TEXT NOT NULL CONSTRAINT PK_simple_fields PRIMARY KEY,
|
||||
car_type TEXT NOT NULL DEFAULT '',
|
||||
field_type TEXT NOT NULL,
|
||||
"key" TEXT NOT NULL,
|
||||
value TEXT NOT NULL DEFAULT '',
|
||||
data_type TEXT NOT NULL DEFAULT '',
|
||||
chinese TEXT,
|
||||
english TEXT,
|
||||
other TEXT NOT NULL DEFAULT '',
|
||||
is_default INTEGER NOT NULL,
|
||||
create_time TEXT NOT NULL,
|
||||
update_time TEXT NOT NULL
|
||||
);
|
||||
""");
|
||||
// 须先删旧索引 (field_type, other, key):把 other 清空为「其他语言」后会与旧唯一约束冲突。
|
||||
await db.Database.ExecuteSqlRawAsync("DROP INDEX IF EXISTS IX_simple_fields_field_type_other_key;");
|
||||
await db.Database.ExecuteSqlRawAsync("""
|
||||
UPDATE simple_fields SET car_type = other
|
||||
WHERE (car_type IS NULL OR car_type = '') AND other <> '';
|
||||
""");
|
||||
await db.Database.ExecuteSqlRawAsync("""
|
||||
UPDATE simple_fields SET other = ''
|
||||
WHERE other <> '' AND other = car_type;
|
||||
""");
|
||||
await db.Database.ExecuteSqlRawAsync("""
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS IX_simple_fields_car_type_field_type_key
|
||||
ON simple_fields (car_type, field_type, "key");
|
||||
""");
|
||||
return;
|
||||
}
|
||||
|
||||
// 非 SQLite:表不存在时尝试按当前模型创建(已有库不会走 EnsureCreated)。
|
||||
if (!await TableExistsAsync(db, "simple_fields"))
|
||||
{
|
||||
var creator = db.GetService<Microsoft.EntityFrameworkCore.Storage.IRelationalDatabaseCreator>();
|
||||
await creator.CreateTablesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查表是否存在
|
||||
/// </summary>
|
||||
/// <param name="db">数据库上下文</param>
|
||||
/// <param name="table">表名</param>
|
||||
/// <returns>表是否存在</returns>
|
||||
private static async Task<bool> TableExistsAsync(PlatformDbContext db, string table)
|
||||
{
|
||||
var conn = db.Database.GetDbConnection();
|
||||
if (conn.State != System.Data.ConnectionState.Open)
|
||||
await conn.OpenAsync();
|
||||
try
|
||||
{
|
||||
await using var cmd = conn.CreateCommand();
|
||||
if (db.Database.IsSqlServer())
|
||||
{
|
||||
cmd.CommandText = "SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = @t";
|
||||
var p = cmd.CreateParameter(); p.ParameterName = "@t"; p.Value = table; cmd.Parameters.Add(p);
|
||||
}
|
||||
else if (db.Database.IsNpgsql())
|
||||
{
|
||||
cmd.CommandText = "SELECT 1 FROM information_schema.tables WHERE table_name = @t";
|
||||
var p = cmd.CreateParameter(); p.ParameterName = "@t"; p.Value = table; cmd.Parameters.Add(p);
|
||||
}
|
||||
else if (db.Database.IsMySql())
|
||||
{
|
||||
cmd.CommandText = "SELECT 1 FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = @t";
|
||||
var p = cmd.CreateParameter(); p.ParameterName = "@t"; p.Value = table; cmd.Parameters.Add(p);
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var result = await cmd.ExecuteScalarAsync();
|
||||
return result != null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (conn.State == System.Data.ConnectionState.Open)
|
||||
await conn.CloseAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private static string ResolveConnectionString(IConfiguration configuration, IWebHostEnvironment env, string provider)
|
||||
|
||||
@@ -6,6 +6,7 @@ using Microsoft.OpenApi.Models;
|
||||
using MiGu.Server.Auth;
|
||||
using MiGu.Server.Configs;
|
||||
using MiGu.Server.Launcher;
|
||||
using MiGu.Server.OpenApi;
|
||||
using MiGu.Server.Persistence;
|
||||
using Yarp.ReverseProxy.Transforms;
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
using System.Globalization;
|
||||
|
||||
namespace MiGu.Server.SimpleFields;
|
||||
|
||||
public static class SimpleFieldDateTime
|
||||
{
|
||||
public const string StorageFormat = "yyyy-MM-dd HH:mm:ss";
|
||||
|
||||
public static DateTimeOffset Now => DateTimeOffset.Now;
|
||||
|
||||
public static string ToStorage(DateTimeOffset value) =>
|
||||
value.LocalDateTime.ToString(StorageFormat, CultureInfo.InvariantCulture);
|
||||
|
||||
public static DateTimeOffset FromStorage(string value)
|
||||
{
|
||||
if (DateTime.TryParseExact(value, StorageFormat, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out var local))
|
||||
return new DateTimeOffset(local);
|
||||
|
||||
return DateTimeOffset.Parse(value, CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace MiGu.Server.SimpleFields;
|
||||
|
||||
public sealed class SimpleField
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
|
||||
/// <summary>车型唯一标识:assemblyName.shortName,如 StandardScene.QrLidar.Forklift。</summary>
|
||||
[MaxLength(64)]
|
||||
public string CarType { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// 字段类型唯一标识:assemblyName.shortName,如 StandardScene.QrLidar.Forklift。
|
||||
/// </summary>
|
||||
[MaxLength(64)]
|
||||
public string FieldType { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// 字段唯一标识:key,如 Forklift.PositionX。
|
||||
/// </summary>
|
||||
[MaxLength(128)]
|
||||
public string Key { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// 字段值,如 10.0。
|
||||
/// </summary>
|
||||
public string Value { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// 数据类型,如 System.String。
|
||||
/// </summary>
|
||||
[MaxLength(128)]
|
||||
public string DataType { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// 中文名称,如 位置 X。
|
||||
/// </summary>
|
||||
[MaxLength(256)]
|
||||
public string? Chinese { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 英文名称,如 Position X。
|
||||
/// </summary>
|
||||
[MaxLength(256)]
|
||||
public string? English { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 其他语言名称,如 位置 X。
|
||||
/// </summary>
|
||||
[MaxLength(512)]
|
||||
public string Other { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// 是否内置默认字段,如 true。
|
||||
/// </summary>
|
||||
public bool IsDefault { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建时间,如 2021-01-01 12:00:00。
|
||||
/// </summary>
|
||||
public DateTimeOffset CreateTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 更新时间,如 2021-01-01 12:00:00。
|
||||
/// </summary>
|
||||
public DateTimeOffset UpdateTime { get; set; }
|
||||
}
|
||||
|
||||
public sealed record SimpleFieldRequest(
|
||||
Guid? Id,
|
||||
string CarType,
|
||||
string FieldType,
|
||||
string Key,
|
||||
string? Value,
|
||||
string? DataType,
|
||||
string? Chinese,
|
||||
string? English,
|
||||
string? Other,
|
||||
bool IsDefault);
|
||||
|
||||
public sealed record SimpleFieldBatchRequest(
|
||||
bool ReplaceAll,
|
||||
List<SimpleFieldRequest> Items);
|
||||
@@ -0,0 +1,132 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using MiGu.Server.Persistence;
|
||||
|
||||
namespace MiGu.Server.SimpleFields;
|
||||
|
||||
public sealed class SimpleFieldService
|
||||
{
|
||||
private readonly PlatformDbContext _db;
|
||||
|
||||
public SimpleFieldService(PlatformDbContext db) => _db = db;
|
||||
|
||||
public async Task<List<SimpleField>> ListAsync(string? fieldType = null, string? carType = null, string? q = null)
|
||||
{
|
||||
var query = _db.SimpleFields.AsNoTracking()
|
||||
.OrderBy(x => x.CarType).ThenBy(x => x.FieldType).ThenBy(x => x.Key)
|
||||
.AsQueryable();
|
||||
if (!string.IsNullOrWhiteSpace(fieldType))
|
||||
query = query.Where(x => x.FieldType == fieldType);
|
||||
if (!string.IsNullOrWhiteSpace(carType))
|
||||
query = query.Where(x => x.CarType == carType);
|
||||
if (!string.IsNullOrWhiteSpace(q))
|
||||
{
|
||||
var kw = q.Trim();
|
||||
query = query.Where(x =>
|
||||
x.Key.Contains(kw) ||
|
||||
x.CarType.Contains(kw) ||
|
||||
(x.Chinese != null && x.Chinese.Contains(kw)) ||
|
||||
(x.English != null && x.English.Contains(kw)) ||
|
||||
x.Other.Contains(kw) ||
|
||||
x.Value.Contains(kw));
|
||||
}
|
||||
return await query.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<SimpleField> SaveAsync(SimpleFieldRequest req)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(req.CarType)) throw new SimpleFieldException("car_type 不能为空");
|
||||
if (string.IsNullOrWhiteSpace(req.FieldType)) throw new SimpleFieldException("field_type 不能为空");
|
||||
if (string.IsNullOrWhiteSpace(req.Key)) throw new SimpleFieldException("key 不能为空");
|
||||
|
||||
var carType = req.CarType.Trim();
|
||||
var now = SimpleFieldDateTime.Now;
|
||||
SimpleField entity;
|
||||
if (req.Id is { } id && id != Guid.Empty)
|
||||
{
|
||||
entity = await _db.SimpleFields.FirstOrDefaultAsync(x => x.Id == id)
|
||||
?? throw new SimpleFieldException("记录不存在");
|
||||
}
|
||||
else
|
||||
{
|
||||
var dup = await _db.SimpleFields.AnyAsync(x =>
|
||||
x.CarType == carType &&
|
||||
x.FieldType == req.FieldType.Trim() &&
|
||||
x.Key == req.Key.Trim());
|
||||
if (dup) throw new SimpleFieldException("同车型与字段类型下 key 已存在");
|
||||
|
||||
entity = new SimpleField { Id = Guid.NewGuid(), CreateTime = now };
|
||||
_db.SimpleFields.Add(entity);
|
||||
}
|
||||
|
||||
entity.CarType = carType;
|
||||
entity.FieldType = req.FieldType.Trim();
|
||||
entity.Key = req.Key.Trim();
|
||||
entity.Value = req.Value?.Trim() ?? "";
|
||||
entity.DataType = req.DataType?.Trim() ?? "";
|
||||
entity.Chinese = req.Chinese is null ? null : req.Chinese.Trim();
|
||||
entity.English = req.English is null ? null : req.English.Trim();
|
||||
entity.Other = req.Other?.Trim() ?? "";
|
||||
entity.IsDefault = req.IsDefault;
|
||||
entity.UpdateTime = now;
|
||||
if (entity.CreateTime == default) entity.CreateTime = now;
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
return entity;
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(Guid id)
|
||||
{
|
||||
var entity = await _db.SimpleFields.FirstOrDefaultAsync(x => x.Id == id)
|
||||
?? throw new SimpleFieldException("记录不存在");
|
||||
_db.SimpleFields.Remove(entity);
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
/// <summary>批量保存全部字段;ReplaceAll=true 时清空表后写入。</summary>
|
||||
public async Task<int> SaveBatchAsync(SimpleFieldBatchRequest req)
|
||||
{
|
||||
var items = req.Items ?? new List<SimpleFieldRequest>();
|
||||
if (items.Count == 0) throw new SimpleFieldException("没有可保存的字段");
|
||||
|
||||
if (req.ReplaceAll)
|
||||
{
|
||||
var all = await _db.SimpleFields.ToListAsync();
|
||||
_db.SimpleFields.RemoveRange(all);
|
||||
}
|
||||
|
||||
var now = SimpleFieldDateTime.Now;
|
||||
var added = 0;
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(item.CarType) ||
|
||||
string.IsNullOrWhiteSpace(item.FieldType) ||
|
||||
string.IsNullOrWhiteSpace(item.Key))
|
||||
continue;
|
||||
|
||||
_db.SimpleFields.Add(new SimpleField
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
CarType = item.CarType.Trim(),
|
||||
FieldType = item.FieldType.Trim(),
|
||||
Key = item.Key.Trim(),
|
||||
Value = item.Value?.Trim() ?? "",
|
||||
DataType = item.DataType?.Trim() ?? "",
|
||||
Chinese = item.Chinese is null ? null : item.Chinese.Trim(),
|
||||
English = item.English is null ? null : item.English.Trim(),
|
||||
Other = item.Other?.Trim() ?? "",
|
||||
IsDefault = item.IsDefault,
|
||||
CreateTime = now,
|
||||
UpdateTime = now
|
||||
});
|
||||
added++;
|
||||
}
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
return added;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class SimpleFieldException : Exception
|
||||
{
|
||||
public SimpleFieldException(string message) : base(message) { }
|
||||
}
|
||||
Reference in New Issue
Block a user