back‐end‐CN - ganweisoft/WebPlugins GitHub Wiki

后端开发指南

1. 插件工程架构

1.1 工程化解决方案

1.1.1 下载模板引擎

1.1.2 命令行创建插件模板解决方案

1.1.3 VisualStudio创建插件模板解决方案

流程如下:

1.2 解决方案结构

1.2.1 Solution Items解决方案文件夹

1.2.1.1 Directory.Build.props

1.2.2 docs解决方案文件夹

该解决方案主要提供有关该插件接口、数据库脚本以及版本更新等等MarkDown记录,仅供参考

1.2.3 build解决方案文件夹

该解决方案主要提供统一包管理和升级。有关AspNetCore官方包则放在 Dependencies.AspNetCore.props文件中,其他包则放在Dependencies.props文件中

1.2.4 插件项目

  1. Controllers: 控制器接口
  2. Data: 实体模型
  3. Dto: 跨插件操作模型
  4. IntegrationEvents: 事件总线
  5. Mappers: AutoMapper映射配置
  6. Models: DTO(Data Transfer Object)
  7. Services: 服务接口
  8. ServicesImpl: 服务接口实现
  9. .json: 插件配置(约定配置名称和插件名称一致,请勿随意更改名称)
  10. Manifest: 插件程序集信息
  11. Startup: 注册接口、使用中间件等
  12. TestDbContext: EntityFramework Core上下文

2. 上下文管理规范

2.1 模型映射规范

  1. 禁止定义联合主键,也不推荐使用外键
  2. 鉴于不同数据库对于大小写敏感,强烈建议统一映射为小写
  3. 若无特殊情况,强烈建议使用EF Core注解方式即DataAnnotation

2.2 上下文命名规范

约定规范:插件名称后缀+DbContext,例如我们创建的Ganweisoft.IoTCenter.Module.UserDemo插件,则上下文名称定义为UserDemoDbContext。

2.3 上下文基本说明

  1. 请勿删除GwDbContext,保持默认继承GwDbContext,该上下文里面包含操作底层数据模型,比如产品表、设备表、遥测表、遥信等。
  2. 重写OnModelCreating方法,请勿删除base.OnModelCreating(modelBuilder)。

3. 接口定义及调用

3.1 服务接口定义

响应数据结构必须通过底层暴露的OperateResult包裹并返回,将操作实体进行DTO映射转换,DTO模型存放于Models目录,命名规范约定:[类名]+Model。

3.2 服务接口实现

通过构造函数注入的上下文直接操作数据库模型。

添加、删除、查询、更新常规基本操作,比如如下添加,AddAsync方法在基类中已有实现。

3.3 服务接口注册

Startup类文件中注册服务以及EntityFramework Core上下文。

3.4 控制器继承

IoT平台使用JWT或Cookie认证授权,所有定义控制器必须继承DefaultController,否则当访问接口时会抛异常或401,通过构造函数使用注册的服务接口,另外请求方式建议遵循RESTful风格。

:warning:1. 本地调试需要切换为非安全模式
:warning:2. 将生成的dll以项目名为文件夹(具体内容请参考plugins文件夹下的内容)放置到指定目录:./IoTCenterWeb/publish/plugins/
:warning:3. 重启服务使插件生效

3.5 本地调试接口

将插件附加到Web API服务进程(单网关启动需附加到GWHost1服务进程),然后通过UI界面操作即可断点调试具体接口

3.6 接口调试工具

通过正确的账号和密码成功登录IoTCenter平台,浏览器F12查看任意非匿名接口,然后复制请求头中的Cookie信息。

例如我们测试添加用户接口:

4. 日志管理

Web API以及网关服务调试日志输出统一通过/IoTCenterWeb/publish下logsetting.json文件配置管理,平台使用Serilog管理日志文件。

4.1 平台日志输出

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

4.2 插件日志输出

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

4.2.1 插件独享日志

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

:warning:context参数建议固化为插件程序集名称,勿给定类似随机值,底层会针对此参数不同而每次实例化日志配置且缓存。

4.2.2 关键操作日志

当对平台重要业务进行操作时,我们需要详细记录操作以便审计可追溯,例如远程访问的IP和端口,是什么账号,操作什么资源,做了什么事件,操作后的结果。我们通过日志接口中提供的审计方法(Audit)来进行详细记录。

5. 常用帮助扩展

5.1 字符串扩展

我们提供位于IoTCenter.Utilities.Extensions命名空间里对于字符串的扩展方法。例如,空字符串判断(IsEmpty)、分隔字符串(SplitString)、分隔字符串并去重(SplitStringWithDistinct)、分隔字符串并转int数组(SplitStringToIntArray)、字符串转int(ToInt),字符串转decimal(ToDecimal)、字符串转DateTime(ToDateTime)、字符串转double(ToDouble)等。

5.2 日期扩展

我们提供位于IoTCenter.Utilities.Extensions命名空间里对于日期的扩展方法。例如将日期转换为时间戳(ToUnixTimestamp),反之时间戳转换为日期(FromUnixTimeStampMillisecond)、日期转换为秒(GetUnixTimeStampSeconds)、日期转换为毫秒(GetUnixTimeStampMilliseconds)等

5.3 序列化扩展

我们提供位于IoTCenter.Utilities命名空间里的序列化扩展方法。例如对象转JSON字符串(ToJson),反之JSON字符串反序列化为对象(FromJson)等。

5.4 枚举扩展

我们提供位于IoTCenter.Utilities命名空间里对于枚举的扩展方法。例如获取枚举的描述信息(GetDescription)、获取枚举的描述信息(GetDisplayName)等。

5.5 键值对对象

我们提供位于IoTCenter.Utilities命名空间里对于前后端交互或存储等时常涉及到的键值对属性数据结构。例如需要使用Id和Name属性的键值对(IdNameObject)、 Key和Value属性的键值对(KeyValueObject)、Name和Value属性的键值对(NameValueObject)。

6. 会话信息

我们提供位于System命名空间里的Session类且通过构造函数注入来获取每一请求上下文中用户相关信息,例如获取当前操作账号、角色名,是否为管理员,访问IP和端口信息。

:warning:比如在Job等中获取会话信息必然抛空引用异常,这是极其错误的做法,因为会话基于请求上下文,勿随意滥用。

7. 读取配置

必须定义顶级根节点,建议以插件全名称或后缀名称作为顶级根节点,否则当加载多个插件,读取同名节点时将会被覆盖。比如读取如下根节点下Url值。

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

8. 定时作业

利用https://github.com/atifaziz/NCrontab库实现计划作业表达式,包含秒级定义即支持6部分表达式定义(底层作业滚动周期为10s,所以自定义实现定时作业应>=10s)。

8.1 使用示例

底层暴露IBackgroundTask接口,通过BackgroundTask特性配置Corn表达式。

定时作业以单例形式注入

8.2 使用注意

在实际使用过程中,可能会出现定时任务仅运行一次或不运行情况,基于底层定时任务实现逻辑。列举如下三点使用注意事项:

  1. 插件定时任务以【插件程序集名称+定时任务类名】作为计划名称,所以各插件中计划名称不可重复。
  2. DoWorkAsync方法调用由底层根据滚动周期自动调用,切不可在其方法中再次使用While,否则会完全阻止其他任务调用。
  3. DoWorkAsync方法建议尽可能使用异步方法,否则若业务逻辑耗时太长,会阻塞其他任务调用。

:warning:若上述底层提供的作业无法满足实际业务要求,可使用Quartz代替。

9. 数据转换对象(DTO)

对数据进行DTO,存在各式各样包,基于此,底层统一规范使用AutoMapper来进行映射,关于AutoMapper这里不再做详细介绍,若不熟悉,请自行查阅相关资料并了解,为考虑到不同场景或简化等原因,底层提供多种映射使用方式。我们使用如下源目标和和映射目标作为示例演示:

9.1 映射配置

底层暴露IAutoMapperConfig接口,创建映射配置类实现该接口,在此类中全局配置插件库中所有映射,配置类名称推荐以【插件名 + AutoMapperConfig】命名。

AutoMapper对于属性忽略映射,使用如下: .ForMember(d => d.Id, o => o.Ignore())

为进一步简化书写,底层额外扩展封装Ignore方法,使用如下: .Ignore(d => d.Id)

例如,如下所示,我们仅配置映射属性Name,但忽略映射Id。

9.2 映射方式(一)

底层通过对外暴露IObjectMapping接口来进行对象映射,如下以构造函数注入使用。

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

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

9.3 映射方式(二)

上述为AutoMapper框架默认映射方式,但底层额外基于object提供了扩展方法,更加方便简洁化,所以我们不再通过IObjectMapping接口进行映射。

比如上述通过源对象CreateUserDTO映射到目标对象UserTest,导入命名空间【IoTCenterCore.AutoMapper】,然后调用MapTo扩展方法,我们可修改为如下:

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