内置特性
# 后端插件开发
# 日志管理
详情
Web API以及网关服务调试日志输出统一通过/IoTCenterWeb/publish下logsetting.json文件配置管理,平台使用Serilog管理日志文件。

# 平台日志输出
详情
若需调试Web API正常启动前后相关服务以及插件详细日志输出,修改日志配置文件最小级别为Debug。

# 插件日志输出
详情
我们暴露了位于IoTCenter.Utilities命名空间里的ILoggingService日志接口,以便在实际业务中进行日志调试、业务日志和平台关键操作记录存储。

# 插件独享日志
详情
重载函数中包含上下文即(context)的标识参数可作为插件独享日志文件输出,我们约定规范context为插件程序集名称。

提示
context参数建议固化为插件程序集名称,勿给定类似随机值,底层会针对此参数不同而每次实例化日志配置且缓存。
# 关键操作日志
当对平台重要业务进行操作时,我们需要详细记录操作以便审计可追溯,例如远程访问的IP和端口,是什么账号,操作什么资源,做了什么事件,操作后的结果。我们通过日志接口中提供的审计方法(Audit)来进行详细记录。

# 常用帮助扩展
# 字符串扩展
我们提供位于IoTCenter.Utilities.Extensions命名空间里对于字符串的扩展方法。例如,空字符串判断(IsEmpty)、分隔字符串(SplitString)、分隔字符串并去重(SplitStringWithDistinct)、分隔字符串并转int数组(SplitStringToIntArray)、字符串转int(ToInt),字符串转decimal(ToDecimal)、字符串转DateTime(ToDateTime)、字符串转double(ToDouble)等。
# 日期扩展
我们提供位于IoTCenter.Utilities.Extensions命名空间里对于日期的扩展方法。例如将日期转换为时间戳(ToUnixTimestamp),反之时间戳转换为日期(FromUnixTimeStampMillisecond)、日期转换为秒(GetUnixTimeStampSeconds)、日期转换为毫秒(GetUnixTimeStampMilliseconds)等
# 序列化扩展
我们提供位于IoTCenter.Utilities命名空间里的序列化扩展方法。例如对象转JSON字符串(ToJson),反之JSON字符串反序列化为对象(FromJson)等。
# 枚举扩展
我们提供位于IoTCenter.Utilities命名空间里对于枚举的扩展方法。例如获取枚举的描述信息(GetDescription)、获取枚举的描述信息(GetDisplayName)等。
# 键值对对象
我们提供位于IoTCenter.Utilities命名空间里对于前后端交互或存储等时常涉及到的键值对属性数据结构。例如需要使用Id和Name属性的键值对(IdNameObject)、 Key和Value属性的键值对(KeyValueObject)、Name和Value属性的键值对(NameValueObject)。
# 会话信息
详情
我们提供位于System命名空间里的Session类且通过构造函数注入来获取每一请求上下文中用户相关信息,例如获取当前操作账号、角色名,是否为管理员,访问IP和端口信息。

注意
比如在Job等中获取会话信息必然抛空引用异常,这是极其错误的做法,因为会话基于请求上下文,勿随意滥用。
# 读取配置
详情
必须定义顶级根节点,建议以插件全名称或后缀名称作为顶级根节点,否则当加载多个插件,读取同名节点时将会被覆盖。比如读取如下根节点下Url值。

底层暴露IIotConfiguration接口,将其通过构造函数注入,使用方式与IConfiguration接口等同。

# 事件总线
详情
引入事件总线设施从而降低代码耦合或削峰等,当前版本支持本地事件总线和Kafka分布式事件总线。所有事件总线定义以及订阅处理规范约定放于IntegrationEvents目录,Events目录为事件定义,EventHandlers目录为事件处理程序即订阅事件,同时我们约定规范事件名称以点号隔开。

注意
事件总线订阅处理程序做好幂等性设计,所谓幂等性即执行一次或多次,其影响是相同的,换言之,重复执行也不影响实际业务。
# 本地事件总线
详情
模拟定义添加用户后给用户发送邮件事件

实现ILocalEventHandler本地事件处理接口,其泛型类为对应要接收的事件。

最后在Configure方法中通过底层暴露的本地事件总线接口,订阅当添加用户后,给用户发送邮件事件处理。
注意
非跨库订阅即发布和订阅事件处理程序在同一插件内,事件类可无需显式声明EventName事件名称特性,但前置条件需Web Api发布目录下IoTCenterCore.EventBus>=6.1.5
# 全局订阅
首次加载插件时,立即全局订阅事件处理。

# 局部订阅
详情
也可以在具体接口实现中,按需局部订阅事件,比如当添加用户成功后才去订阅事件处理。

当添加用户成功后,发布给用户发送邮件事件。

# 跨库订阅
详情
一般来讲,事件发布和订阅所属事件类型应为相同类型,但在插件化中存在事件发布和订阅处于不同库场景,此时发布事件和订阅事件类型不同,但属性一致,为避免代码耦合即解耦,底层设施额外扩展此场景。

场景假设:在插件库Ganweisoft.IoTCenter.Module.Test1发布触发发送用户邮件事件,而在Ganweisoft.IoTCenter.Module.Test2订阅用户发送邮件事件处理。

我们在Ganweisoft.IoTCenter.Module.Test1插件中发布用户邮件事件。

定义发布事件类,基础设施暴露EventName特性,通过EventName特性指定发布事件名称。

在Ganweisoft.IoTCenter.Module.Test2插件中订阅以及进行订阅用户邮件事件处理。

此插件中同样使用EventName特性指定相同订阅事件名称。
说明
上述发布和订阅示例皆为一对一关系,对于一对多关系即一处发布多处订阅同样支持。
# 模糊订阅
详情
订阅指定具体事件名称仍无法充分满足实际业务需求,我们针对本地事件总线提供三种模糊订阅策略增强式支持,前缀式订阅【显式指定(.*)】,后缀式订阅【显式指定(*.)】,全量式订阅【显式指定(*)】。
例如,将Ganweisoft.IoTCenter.Module.Test2插件中订阅事件名称显式指定为*,即可全量式订阅所有发布事件。

注意
前缀式和后缀式订阅同理,前缀式订阅具体指,比如订阅敢为云发布的邮件和短信服务通知,敢为云发布邮件和短信事件名称定义为ganweicloud.send.mail和ganweicloud.send.sms,可使用前缀式模糊订阅这2个事件,例如:ganweicloud.send.*。
# Kafka分布式事件总线
# 配置说明
详情
应用商店下载Kafka应用插件,常见使用场景:网关发布,Web API插件订阅;Web API插件发布和订阅;其他应用发布,Web API插件订阅等;
| 字段 | 说明 |
|---|---|
| BootstrapServers | Kafka地址 |
| GroupId | Kafka消费组Id |
示例1:使用默认配置连接到本地 kafka 服务器的最小化配置。
示例2:Default节点配置对应Kafka中的ClientConfig,比如指定套接字超时。

示例3:如果需要连接到本地主机以外的其他服务器,则需要配置连接属性,指定主机名(作为IP地址)。

# 使用说明
详情
发布和订阅处理程序与本地事件总线使用方式类似,只需将本地事件总线接口ILocalEventBus替换为分布式事件总线接口IDistributedEventBus,本地事件处理程序接口ILocalEventHandler替换为分布式事件总线处理程序接口IDistributedEventHandler。



我们发布如下分布式事件示例,在执行该示例接口前请确保Kafka服务处于正常运行状态,同时已安装Kafka订阅插件以及相关配置正确。




注意
分布式事件总线订阅不支持模糊订阅策略,仅支持一对一和一对多关系的显式指定具体事件名称订阅。
# 定时作业
插件定时作业统一集成 Quartz Job
# 1. 静态 Job(声明式)
详情
通过[PluginJob]特性声明 Job,框架在 Shell 激活时自动扫描并注册,无需手动调用任何 API。
# 1.1 基本用法
第一步:实现IPluginJob接口,通过构造函数注入依赖服务。
第二步:标注[PluginJob]特性,设置Name和调度规则。
using IoTCenterCore.Quartz.Abstractions;
using System.Threading;
using System.Threading.Tasks;
[PluginJob(Name = "device-status-check", IntervalSeconds = 300)]
public class DeviceStatusCheckJob : IPluginJob
{
private readonly IDeviceRepository _deviceRepository;
private readonly ILoggingService _logger;
public DeviceStatusCheckJob(IDeviceRepository deviceRepository, ILoggingService logger)
{
_deviceRepository = deviceRepository;
_logger = logger;
}
public async Task ExecuteAsync(CancellationToken cancellationToken)
{
var devices = await _deviceRepository.GetOnlineDevicesAsync(cancellationToken);
foreach (var device in devices)
{
cancellationToken.ThrowIfCancellationRequested();
await _deviceRepository.CheckStatusAsync(device.Id, cancellationToken);
}
_logger.Info($"设备状态检查完成,共处理 {devices.Count} 台设备");
}
}
第三步:在Startup.ConfigureServices中注册 Job 实现类:
services.TryAddTransient<DeviceStatusCheckJob>();
# 1.2 PluginJobAttribute 参数说明
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
Name | string | 是 | Job 唯一标识,整个应用程序内不可重复 |
IntervalSeconds | int | 三选一 | 固定间隔触发(秒) |
CronExpression | string | 三选一 | Quartz Cron 表达式(6 段:秒 分 时 日 月 周) |
RunOnce | bool | 三选一 | 为true时仅执行一次,执行后自动清理 |
DisallowConcurrentExecution | bool | 否 | 是否禁止并发执行,默认true |
StartTimeType | Type | 否 | 自定义首次触发时间,须实现IPluginJobStartTime接口 |
# 1.3 调度规则说明
三种调度模式互斥,优先级:RunOnce>IntervalSeconds>CronExpression。
固定间隔
// 每 60 秒执行一次
[PluginJob(Name = "cache-refresh", IntervalSeconds = 60)]
public class CacheRefreshJob : IPluginJob { /* ... */ }
Cron 表达式
// 每天凌晨 2 点执行(Quartz 6 段格式)
[PluginJob(Name = "daily-report", CronExpression = "0 0 2 * * ?")]
public class DailyReportJob : IPluginJob { /* ... */ }
// 每小时整点执行
[PluginJob(Name = "hourly-sync", CronExpression = "0 0 * * * ?")]
public class HourlySyncJob : IPluginJob { /* ... */ }
// 工作日(周一至周五)上午 9:30 执行
[PluginJob(Name = "workday-task", CronExpression = "0 30 9 ? * MON-FRI")]
public class WorkdayTaskJob : IPluginJob { /* ... */ }
一次性执行
// 应用启动后执行一次,完成后自动清理
[PluginJob(Name = "data-migration", RunOnce = true)]
public class DataMigrationJob : IPluginJob { /* ... */ }
# 1.4 自定义首次触发时间
实现IPluginJobStartTime接口,通过StartTimeType指定:
// 自定义首次触发时间:明天凌晨 3:00
public class Tomorrow3AMStartTime : IPluginJobStartTime
{
public DateTimeOffset? GetStartAt()
{
return DateTimeOffset.UtcNow.Date.AddDays(1).AddHours(3);
}
}
[PluginJob(Name = "scheduled-task", IntervalSeconds = 3600, StartTimeType = typeof(Tomorrow3AMStartTime))]
public class ScheduledTaskJob : IPluginJob { /* ... */ }
# 1.5 通过 IPluginJobDescriptor 声明
当需要精确控制 Job 元数据,或需要动态计算调度规则时,可通过IPluginJobDescriptor接口替代[PluginJob]特性。实现类须提供无参构造函数,框架在 Shell 激活时自动发现。
using IoTCenterCore.Quartz.Abstractions;
using System;
public class DeviceCheckJobDescriptor : IPluginJobDescriptor
{
public string JobKey => "device-status-check"; // Job 唯一标识
public Type JobType => typeof(DeviceStatusCheckJob); // Job 实现类型
public IJobSchedule DefaultSchedule => new IntervalJobSchedule(300); // 默认每 300 秒
public bool DisallowConcurrentExecution => true; // 禁止并发
}
注意
此时DeviceStatusCheckJob类上无需标注[PluginJob]特性。框架在 Shell 激活时同时扫描IPluginJobDescriptor实现类和[PluginJob]特性类。
# 2. 动态 Job(编程式)
详情
当 Job 的调度参数需要运行时决定(例如用户配置的定时任务),可通过IJobManager动态创建和管理。
# 2.1 创建动态 Job
注入IJobManager,调用CreateAsync动态注册 Job:
using IoTCenterCore.Quartz.Abstractions;
public class UserScheduledTaskService
{
private readonly IJobManager _jobManager;
public UserScheduledTaskService(IJobManager jobManager)
{
_jobManager = jobManager;
}
public async Task CreateUserTaskAsync(string taskId, string cronExpression, UserTaskParams taskParams)
{
// jobKey: 建议格式 "{业务类型}-{业务主键}"
var jobKey = $"user-task-{taskId}";
await _jobManager.CreateAsync(
jobKey: jobKey,
jobType: typeof(UserTaskExecutionJob),
schedule: new CronJobSchedule(cronExpression),
parameters: taskParams, // 业务参数,在 Job 内部通过 IJobParams<T> 接收
disallowConcurrentExecution: true
);
}
}
IJobManager.CreateAsync参数说明:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
jobKey | string | 是 | Job 唯一标识,建议格式"{业务类型}-{业务主键}" |
jobType | Type | 是 | Job 实现类型,须实现IPluginJob且已在当前插件中加载 |
schedule | IJobSchedule | 是 | 调度规则(IntervalJobSchedule/CronJobSchedule/OneTimeJobSchedule) |
parameters | object | 否 | 业务参数对象 |
disallowConcurrentExecution | bool | 否 | 是否禁止并发,默认true |
# 2.2 接收动态参数
动态创建的 Job 可通过注入IJobParams<T>获取调用方传入的业务参数:
public class UserTaskExecutionJob : IPluginJob
{
private readonly IJobParams<UserTaskParams> _params;
private readonly ITaskExecutor _executor;
public UserTaskExecutionJob(IJobParams<UserTaskParams> params, ITaskExecutor executor)
{
_params = params;
_executor = executor;
}
public async Task ExecuteAsync(CancellationToken cancellationToken)
{
var taskParams = _params.Value;
if (taskParams != null)
{
await _executor.ExecuteAsync(taskParams, cancellationToken);
}
}
}
// 参数类型
public class UserTaskParams
{
public string Action { get; set; }
public Dictionary<string, string> Options { get; set; }
}
注意
动态 Job 的实现类不应同时标注[PluginJob]特性。二者是互斥的使用路径——[PluginJob]由框架自动调度,CreateAsync由业务代码主动管理。同一类同时走两条路径会导致重复调度和参数缺失。
提示
IJobParams<T>也适用于静态 Job 场景。当静态 Job 被动态TriggerNowAsync触发时,可通过IJobParams<T>拿到调用方传入的参数。
# 2.3 运行时管理
IJobManager提供完整的 Job 生命周期管理方法:
public class JobManagementController
{
private readonly IJobManager _jobManager;
public JobManagementController(IJobManager jobManager)
{
_jobManager = jobManager;
}
// 查看所有 Job 状态
public async Task<IEnumerable<JobStatus>> GetAllJobsAsync()
{
return await _jobManager.GetAllAsync();
// JobStatus 包含: JobKey, TypeName, IsPaused, NextFireTime, PreviousFireTime
}
// 暂停指定 Job
public async Task PauseJobAsync(string jobKey)
{
await _jobManager.PauseAsync(jobKey, typeof(UserTaskExecutionJob));
// Job 仍保留在调度器中,只是不再触发
}
// 恢复已暂停的 Job
public async Task ResumeJobAsync(string jobKey)
{
await _jobManager.ResumeAsync(jobKey, typeof(UserTaskExecutionJob));
}
// 立即触发一次(不影响原调度计划)
public async Task TriggerJobNowAsync(string jobKey)
{
await _jobManager.TriggerNowAsync(jobKey, typeof(UserTaskExecutionJob));
}
// 修改调度规则(跨热加载保持)
public async Task ChangeScheduleAsync(string jobKey, int newIntervalSeconds)
{
await _jobManager.UpdateScheduleAsync(
jobKey,
typeof(UserTaskExecutionJob),
new IntervalJobSchedule(newIntervalSeconds));
}
// 修改为 Cron 表达式
public async Task ChangeToCronScheduleAsync(string jobKey, string cronExpression)
{
await _jobManager.UpdateScheduleAsync(
jobKey,
typeof(UserTaskExecutionJob),
new CronJobSchedule(cronExpression));
}
// 删除 Job
public async Task DeleteJobAsync(string jobKey)
{
await _jobManager.DeleteAsync(jobKey, typeof(UserTaskExecutionJob));
}
}
# 3. Job 正式启动唤醒特性
详情
插件 Job 运行时常需要操作数据库表,但表结构依赖于ConfigureAsync中调用UseAutoMigratorWithNotify<WorkOrderDbContext>()等自动迁移后才能就绪。Job 的调度恢复与迁移的完成时机割裂——Job 可能在表还不存在时就开始执行,导致运行时异常。
为此提供IPluginJobResume作为 Job 正式运行的前置条件:Job 被调度到 Quartz 后保持暂停状态,直到指定的条件就绪后才正式恢复执行。
提示
也可借助第 2 章动态 Job(编程式):将动态 Job 放入自动迁移回调逻辑里面
# 3.1 基本用法(单信号场景)
多数场景下,一个插件的所有 Job 等待同一个条件(如 DB 迁移完成),使用默认实现即可:
Step 1— Job 声明ResumeCondition:
[PluginJob(Name = "workorder-auto-create", IntervalSeconds = 5,
ResumeCondition = typeof(PluginJobResumeSignal))]
public class IoTWorkOrderAutoCreateJob : IPluginJob
{
public async Task ExecuteAsync(CancellationToken cancellationToken)
{
// 不再需要内部 guard check,Job 开始执行时表已存在
}
}
[PluginJob(Name = "workorder-timeout-notice", IntervalSeconds = 5,
ResumeCondition = typeof(PluginJobResumeSignal))]
public class WorkOrderTimeOutNoticeJob : IPluginJob { ... }
Step 2—Startup.cs中注册并发出信号:
public class Startup : StartupBase
{
public override void ConfigureServices(IServiceCollection services)
{
// ... 其他注册 ...
services.AddSingleton<PluginJobResumeSignal>();
}
public override async ValueTask ConfigureAsync(IApplicationBuilder builder, ...)
{
var signal = builder.ApplicationServices.GetRequiredService<PluginJobResumeSignal>();
builder.UseAutoMigratorWithNotify<WorkOrderDbContext>(sp =>
{
// 迁移完成 → 所有关联 Job 正式恢复执行
sp.GetRequiredService<PluginJobResumeSignal>().Signal();
return Task.CompletedTask;
});
// ... 其他配置 ...
}
}
# 3.2 多信号场景(不同 Job 等待不同条件)
当插件的不同 Job 依赖不同初始化条件时,通过空派生类区分:
// 空派生类仅用于 DI 类型区分
public class DbReadySignal : PluginJobResumeSignal { }
public class ConfigLoadedSignal : PluginJobResumeSignal { }
[PluginJob(ResumeCondition = typeof(DbReadySignal))]
public class JobA : IPluginJob { }
[PluginJob(ResumeCondition = typeof(ConfigLoadedSignal))]
public class JobB : IPluginJob { }
// Startup.cs
services.AddSingleton<DbReadySignal>();
services.AddSingleton<ConfigLoadedSignal>();
# 3.3 自定义唤醒逻辑
对于复杂场景(如等待多个异步条件同时满足),自行实现IPluginJobResume:
public class CustomResumeCondition : IPluginJobResume
{
private readonly IRequiredService _service;
public CustomResumeCondition(IRequiredService service)
{
_service = service;
}
public async Task WaitForResumeAsync(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
if (await _service.CheckReadyAsync())
return;
await Task.Delay(1000, cancellationToken);
}
}
}
# 4. 注意事项
详情
- 不兼容6.x版本:仅10.2版本可用,以便支持热加载以及避免各插件重复定义专注于业务。
- Job Name 全局唯一:不同插件中的
Name不可重复。框架内部使用{assemblyName}:{Name}做隔离,但建议Name本身保持全局唯一 - 构造函数注入:
IPluginJob实现类通过构造函数注入相关接口依赖,禁止使用底层Shell创建Scope - CancellationToken:长时间运行的 Job 应定期检查
cancellationToken.ThrowIfCancellationRequested(),以响应超时取消和 Shell 关闭信号 - 禁止并发:默认
DisallowConcurrentExecution = true,上一次执行未完成时新触发将在 Quartz 线程池中排队。对于幂等、无状态的 Job 可设为false允许并发 - 动态 Job 的 jobType:必须是已在当前插件中加载的类型,即
[PluginJob]或IPluginJobDescriptor所引用的IPluginJob实现类 - Cron 表达式格式:采用 Quartz 6 段格式(秒 分 时 日 月 周)
- 关注Job执行耗时:大量Job长时间未执行完成,会占用线程池线程,从而导致线程饥饿或者其他待执行Job延迟等待
# 数据转换对象(DTO)
详情
对数据进行DTO,存在各式各样包,基于此,底层统一规范使用AutoMapper来进行映射,关于AutoMapper这里不再做详细介绍,若不熟悉,请自行查阅相关资料并了解,为考虑到不同场景或简化等原因,底层提供多种映射使用方式
我们使用如下源目标和和映射目标作为示例演示

# 映射配置
详情
底层暴露IAutoMapperConfig接口,创建映射配置类实现该接口,在此类中全局配置插件库中所有映射,配置类名称推荐以【插件名 + AutoMapperConfig】命名。
AutoMapper对于属性忽略映射,使用如下: .ForMember(d => d.Id, o => o.Ignore()) 为进一步简化书写,底层额外扩展封装Ignore方法,使用如下: .Ignore(d => d.Id)
例如,如下所示,我们仅配置映射属性Name,但忽略映射Id。

# 映射方式(一)
详情
底层通过对外暴露IObjectMapping接口来进行对象映射,如下以构造函数注入使用。

调用接口中的Map方法完成从源目标到映射目标的数据映射,此方式乃AutoMapper内置映射使用方式。

同样,通过Postman来模拟验证测试。


# 映射方式(二)
详情
上述为AutoMapper框架默认映射方式,但底层额外基于object提供了扩展方法,更加方便简洁化,所以我们不再通过IObjectMapping接口进行映射。
比如上述通过源对象CreateUserDTO映射到目标对象UserTest,导入命名空间【IoTCenterCore.AutoMapper】,然后调用MapTo扩展方法,我们可修改为如下:

调用MapToList方法将映射源集合映射到目标集合。

# 国际化语言
# 翻译文件说明
详情
前端页面内容和相关操作等提示都由后台统一返回,我们将翻译分为公共翻译和插件翻译两部分。Ganweisoft.IoTCenter.Module.Localization插件作为国际化支持入口,其Localization目录作为全局国际化公共翻译。
zh-CN文件:公共中文翻译 en-US文件:公共英文翻译

指定插件国际化翻译目录如下:

# zh-CN目录
- Back:后台插件中文翻译
- Front:前端UI中文翻译
# en-US目录
- Back:后台插件英文翻译
- Front:前端UI英文翻译
注意
- JSON翻译文件命名约定:英文且小写。
- 前端文件命名约定【前端插件名.子菜单名.json】
- 后台插件命名约定【插件名.json】
# 使用示例
以设备列表英文翻译为例,作进一步详细说明
详情

- 前端UI

- 后端响应

翻译模板json数组内容:

msgid "填写翻译键" msgstr "填写翻译值" 使用.NET Core框架所提供【IStringLocalizer<T>】构造函数注入,进行翻译。
private readonly IStringLocalizer<T> _stringLocalizer;
# 公共翻译内容
详情
建议翻译键以【common_】作为前缀,其他单词通过下划线隔开。
若需响应参数不正确或参数为空或不能为空等,对应公共翻译键如_stringLocalizer["common_parameter_is_incorrect"]。
# 插件翻译内容
建议翻译键以【插件名简称_】作为前缀,其他单词通过下划线隔开。
比如翻译设备不存在,_stringLocalizer["equip_config_device_not_exist"]。
# 插件翻译配置
详情
旧版本后端JSON翻译文件格式(兼容但废弃,新开发插件请勿继续使用):msgid为键,msgtr为值

但这样一来我们需要显式增加zh-CN(中文)翻译配置文件,同时对开发人员看代码时若翻译键模糊不清导致不明白具体翻译内容,面向研发体验不友好。 所以基于以上2点,我们针对翻译配置做出进一步优化:
- 数据结构:JSON数组调整为对象
- 翻译格式:键值对格式调整为对象格式,以中文为翻译键,值为翻译值
- 默认翻译:当未找到中文翻译键对应的值时,默认以中文翻译键作为值返回
新版本后端JSON翻译文件格式优化如下

# 文件统一管理
为便于统一管理文件以及屏蔽重复操作文件逻辑,平台开放式提供统一文件上传存储管理等功能
# 物理存储
物理存储目录可在./IoTCenter/data/AlarmCenter 目录中的AlarmCenterProperties.xml文件进行灵活配置
<StorageFile value="D:\ganwei\IoTCenter\StorageFile" />
# 数据库查询
我们可直接操作数据库表gwfilestorage查看上传的相关文件信息,表列详细说明如下
| 列名 | 列类型 | 备注 |
|---|---|---|
| id | string | 主键,32位guid |
| full_path | string | 磁盘实际物理路径 |
| upload_time | datetime | 上传时间 |
| file_name | string | 文件名称 |
| extension_name | string | 扩展名/文件类型 |
| subject | string | 主题 |
| access_path | string | 虚拟目录访问路径 |
注意
- 登录插件6.1.1.75版本支持文件上传限制参数。
- 在/IoTCenterWeb/publish文件夹中的appsettings.json可配置允许上传的类型。 ✳为不限制文件上传类型,配置为✳时请勿另外配置类型。
"FileStorageOptions": {
"FileTypeControl": [
"*"
], // 允许上传的文件类型,包含*时表示不限
"FileCountLimits": 10, //批量上传文件最大限制
"FileSizeLimits": 10240 //文件大小限制,单位kb
}
或
"FileStorageOptions": {
"FileTypeControl": [
"png",
"jpg",
"jpeg",
"zip",
"txt"
], // 允许上传的文件类型,包含*时表示不限
"FileCountLimits": 10, //批量上传文件最大限制
"FileSizeLimits": 10240 //文件大小限制,单位kb
}
# 文件上传
# 接口名称
/api/fileStorage/upload
# 请求方式
POST
# 请求参数说明
content-Type:multipart/form-data
表单提交
| 属性名 | 属性类型 | 说明 |
|---|---|---|
| formFile | file | 文件,可批量上传 |
| subject | string | 主题 |
响应参数说明 content-Type:application/json
| 属性名 | 属性类型 | 说明 |
|---|---|---|
| code | int | 200成功,其他失败 |
| succeeded | bool | 是否成功 |
| message | string | 说明 |
| data | List< objec> | 消息体对象集合 |
| data=>id | string | 图片id |
| extensionName | string | 文件扩展名/类型 |
| subject | string | 主题 |
| fileName | string | 文件名 |
| accessPath | string | 回显地址 |
data < object > 消息体对象示例
# 文件下载
# 接口名称
/api/fileStorage/download/{id}
# 请求方式
GET
# 请求参数说明
请求参数位置为 route参数
| 属性名 | 属性类型 | 说明 |
|---|---|---|
| id | string | 图片id |
响应参数说明 content-Type:application/octet-stream 二进制数据,无数据时,响应HTTP状态码为204
# 获取缩略图
# 接口名称
/api/fileStorage/getThumbnail/{id}
# 请求方式
GET
# 请求参数说明
route参数
| 属性名 | 属性类型 | 说明 |
|---|---|---|
| id | string | 图片id |
query参数
| 属性名 | 属性类型 | 说明 |
|---|---|---|
| width | int | 宽像素 |
| height | int | 高像素 |
响应参数说明
content-Type:image/图片文件扩展名 无数据时 HTTP状态码为204,尝试获取非图片格式文件时返回400 HTTP状态码
# 获取图片预览
# 接口名称
/api/fileStorage/preview/{id} 支持以下图片格式 { ".png", ".jpg", ".jpeg" }
# 请求方式
GET
# 请求参数说明
route参数
| 属性名 | 属性类型 | 说明 |
|---|---|---|
| id | string | 图片id |
响应参数说明 content-Type:image/图片文件扩展名 无数据时,HTTP状态码为204,尝试获取非图片格式文件时返回400 HTTP状态码
# 其他特性
# 插件热加载Hook回调
IPluginHotReloadHook 是插件热加载触发的扩展回调接口。业务插件可实现此接口并注册到 DI 容器,以便确保热加载时业务插件资源得到及时释放(比如发布的事件&订阅、非托管资源、启动进程任务等等)。
详情
- 接口定义
public interface IPluginHotReloadHook
{
/// <summary>
/// 【已存在插件】在旧 ALC 卸载前调用,业务插件可在此处清理事件订阅等资源。
/// 异常不影响其他钩子执行,也不阻止后续的 ALC 卸载流程。
/// </summary>
/// <param name="assemblyNames">本次热加载涉及的插件程序集名称集合。</param>
Task OnBeforeUnloadAsync(IReadOnlyCollection<string> assemblyNames)
{
return Task.CompletedTask;
}
/// <summary>
/// 【已存在插件】重建并完成管道构建后调用。
/// </summary>
/// <param name="assemblyNames">本次热加载涉及的插件程序集名称集合。</param>
Task OnHotReloadAsync(IReadOnlyCollection<string> assemblyNames);
/// <summary>
/// 【新增插件】构建插件管道之前调用。此时新程序集已加载,业务插件可在此处进行准备或清理工作。
/// 异常不影响其他钩子执行,也不阻止后续的 Shell 重建流程。
/// </summary>
/// <param name="assemblyName">本次新加载的插件程序集名称。</param>
Task OnBeforePluginLoadedAsync(string assemblyName)
{
return Task.CompletedTask;
}
/// <summary>
/// 【新增插件】的 ALC 加载并完成管道重建后调用。
/// </summary>
/// <param name="assemblyName">本次新加载的插件程序集名称。</param>
Task OnPluginLoadedAsync(string assemblyName)
{
return Task.CompletedTask;
}
/// <summary>
/// 【卸载插件】在插件永久卸载、Shell 管道重建之前调用,业务插件可在此处清理事件订阅等资源。
/// 异常不影响其他钩子执行,也不阻止后续的卸载流程。
/// </summary>
/// <param name="assemblyName">本次卸载的插件程序集名称。</param>
Task OnBeforePluginUnloadedAsync(string assemblyName)
{
return Task.CompletedTask;
}
/// <summary>
/// 【卸载插件】在插件永久卸载并完成管道移除后调用。
/// </summary>
/// <param name="assemblyName">本次卸载的插件程序集名称。</param>
Task OnPluginUnloadedAsync(string assemblyName)
{
return Task.CompletedTask;
}
}
注册自定义 HOOK 回调到 DI 容器
services.AddSingleton<IPluginHotReloadHook, YourPluginHotReloadHook>();
注意
IoTCenterCore.Module.Targets选择 10.0.5 及以上版本。
# 数据库表结构自动迁移
详情
通过NuGet下载IoTCenterCore.EntityFrameworkCore.AutoMigrator包最新版本。

在Startup类中构建管道方法(Configure)中配置如下一行
builder.UseAutoMigrator<TestDbContext>();

注意
- 仅支持自动创建表或自动创建列。
- 字段类型调整以及列的重命名、删除列等都涉及到对表进行破坏性变更,实际生产环境存在重大风险,所以忽略处理。
# 匿名安全扫描
详情
为加强平台匿名接口安全性,现增加自动化扫描所有插件匿名接口功能,启用该功能需具备如下两个条件:
Web Api运行目录下IoTCenterCore.dll文件版本:6.1.4+(含6.1.4版本)
Web Api运行目录下appsettings.json配置文件,将ScanAnonymousApi节点值修改为true
启动平台访问Web时将会在Web Api运行目录下生成名为anonymous_api_description.json文件,数据结构示例如下
