跳到主要内容
版本:Next

后端开发

1. 插件二开规范

  • 统一术语约定:

框架级别标识

​ -- Pedestal : 基座标识

​ -- StandardPlugin :标准化插件标识

​ -- CustomMadePlugin :定制化插件标识

​ -- ExternalPlugin :外部二开插件标识

​ 框架级别标识用途说明:通过框架级别标识 对CMS2.0软件系统的数据、项目信息进行标识,完成数据的隔离,保证软件的数据及信息安全;

  • CMS2.0软件系统需要遵循的开发规范

【强制】缓存的键命名需要遵循以下格式:

框架级别标识:插件标识:业务标识:项目标识【选填】:数据标识【选填】

【强制】插件打包必须要遵循以下的规范, 打包配置文件中的HotPlugId属性信息需要遵循以下格式:

框架级别标识_项目或客户标识【选填】_插件标识

【强制】如果插件与插件、插件与基座 的业务流程需要数据保证强一致性,需要使用基座提供分布式事务总线,确保跨模块/插件 间的信息可追溯,防止在出现最坏情况时可以人工介入恢复数据;

备注:由于基座尚未支持分布式事务总线,所以该规范暂时不用支持。

【重要不强制】如果插件需要对数据库进行初始化或者迁移的操作,需要继承基座提供的IProjectRuntimeMigrator(运行时迁移器)这个抽象类,并在子类实现中完成插件数据库迁移的操作;

【重要不强制】插件的业务数据库命名应遵循以下格式

项目标识_后缀标识

-- 项目标识:项目id

-- 后缀标识:推荐使用project

【重要不强制】插件的业务数据库表命名应遵循以下格式

插件前缀_业务表名

【重要不强制】如果使用ef core ORM框架的code-first方式,插件对应的数据迁移表应该使用__efmigrationshistory_equipmentmaintenance格式来命名迁移表,equipmentmaintenance为插件标识。这样命名可以防止当存在多个插件时出现迁移数据表数据错乱。

2. 插件开发环境

  • 技术栈: 微软.net技术栈;
  • 目标框架: .net6、asp.net core webapi
  • 开发IDE: Visual Studio 2022
  • 开发语言:C#
  • 安装CMS2.0基座
  • 数据库支持:mysql 8.0+; sql server 2016+
  • 插件项目模板安装

​ 打开powershell终端,执行以下命令,可以完成模板安装

dotnet new install CMS.Plugin.Template.Simple --nuget-source https://nexus.sycdev.com/repository/nuget-group/index.json

​ 模版卸载命令

dotnet new uninstall CMS.Plugin.Template.Simple

3. 快速入门

3.1 创建项目

3.1.1 根据模版创建插件项目(最佳实践)

模板模板示例说明:

1.实现了基座的插件接口,完成与基座集成;

2.提供了一个完整的增删改查功能,给二次开发提供指引;

3.提供简单、便捷、易于扩展的项目分层架构;

  • 打开PowerShell命令行终端,创建项目

​ 执行以下命令

dotnet new cms-plugin-sim -n Demo -p Sample

​ 创建出一个解决方案标识为Demo, 插件项目标识为Sample 的工程

参数选项说明:

-n : 解决方案标识

-p : 插件项目名称标识

  • 解决方案结构说明

框架依赖包:最少需要引用CMS2.0框架的以下依赖包:CMS.Framework.AspNetCore、CMS.Extensions.Variable、CMS.Extensions.Data、SYC.Plugin、CMS.Unit.Authority、CMS.Data.Stressing

接口控制器层:存放插件开发的接口控制器;

业务领域层:存放插件业务核心设计模型及代码实现,按业务模型及功能以文件夹进行分类,比如模型对象、数据库访问器、管理器、业务服务、适配器等等;

多语言翻译文件层:存放多语言文件;

数据库迁移文件:存放ef core code-first方式生成的数据库迁移文件;

辅助设施层:存放帮助类、工具类、公共类等;

框架接口实现层:CMSPluginEntry、CMSPluginProjectService、PluginDbContextMigrator 实现了基座提供的核心接口,实现插件和基座集成以及通信;

PluginEntry:插件入口抽象类,插件必须实现了该抽象类才完成与基座集成;

BaseProjectService:插件服务抽象类,插件实现了该抽象类,则该插件可以具备基座提供的协议基础能力,比如插件授权、可以在前端 UI 上控制服务启停;

​ IProjectRuntimeMigrator:项目运行时迁移器接口,插件如果需要进行数据迁移则需要实现该接口,并在实现的接口方法中完成插件数据迁移操作;

  • 调试配置

(1)对插件项目工程进行配置,将插件编译输出目录输出到基座所在的安装目录中,如下图:

(2)启动基座,配置服务端口及数据库

(3)编译插件项目,附加到基座启动的服务进程进行插件开发调试,vs选择 调试—>附加到进程 选择目标进程后可以对插件进行断点调试

(4)以swagger作为接口文档,运行基座后通过http://127.0.0.1:18000/swagger/index.html 地址访问swagger文档

​ (4.1)可以通过swagger对外提供接口文档;

​ (4.2)可以通过swagger发起接口调用,测试调试接口;进行接口调用时需要对接口进行一次授权,如下图:

配置完成后,可以通过swagger进行接口调用,可以对插件程序进行断点调试,如图:

  • 插件服务发布

通过插件模版创建的项目在根目录中,提供了发布脚本publish.ps1,鼠标选择publish.ps1 右键—>使用PowerShell运行,会对插件服务进行打包输出到插件根目录output\publish文件夹下,示例如图:

4. 重点的基础设施

  • 当前项目工程

  • 数据库连接获取

  • 当前用户

  • 插件数据库迁移或种子数据初始化

  • 插件配置

  • 业务/分布式事务

  • 文件系统

下文出现的 serviceProvider 是指.net6框架 IServiceProvider的类型对象,用于依赖注入需要的对象类型;

4.1 当前项目工程

CMS2.0在同一时刻只支持打开一个项目工程,当前运行的项目工程信息在系统中非常重要,与许多业务实现密切相关。CMS2.0系统会将当前项目运行信息抽象封装为IProjectRunner(项目运行者)模型,开发者可以通过IProjectRunner模型对象拿到关注的相关信息;

示例伪代码:获取当前项目工程id

//1.通过依赖注入方式注入 IProjectRunner
var projectRunner = serviceProvider.GetRequiredService<IProjectRunner>();

//2.获取当前项目工程的id
var currentProject = projectRunner.Current.Info.Id;

//3.相关业务处理

相关的信息可以通过转到定义根据代码实现进行查看获取

4.2 数据库连接获取

插件和基座需要使用相同的数据库服务,同时CMS2.0的数据需要以项目进行隔离。基座提供了根据数据库获取数据库连接字符串的封装。

示例伪代码:获取插件数据库的数据库连接字符串

//1.获取当前项目工程id;
var projectId = ...... ;

//2.将项目工程id和插件数据库后缀标识(_project)进行拼接,作为插件服务对应的数据库;
例如:项目工程id为3602188e08d04c43b734b359d24909b4 , 则插件服务对应的数据库为:3602188e08d04c43b734b359d24909b4_project

//3.通过依赖注入方式注入IDataSourceProvider
var dataSourceProvider = serviceProvider.GetRequiredService<IDataSourceProvider>();

//4.依赖基座提供的IDataSourceProvider对象,获取插件数据库连接字符串
var pluginDbConnectStr dataSourceProvider.CreateConnectionString($"{projectId}_{dataBaseName}");

示例代码:插件借助基座提供的接口进行封装,用于获取获取插件数据库连接字符串

using CMS.DataPersistence.Data;
using CMS.Project.Abstractions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace CMS.Plugin.sample.Utilities.Providers
{
/// <summary>
/// 项目信息提供者
/// </summary>
public class ProjectInfoProvider : IProjectInfoProvider
{
private readonly ILogger<ProjectInfoProvider> _logger;
private readonly IServiceScopeFactory _serviceScopeFactory;
private static bool _isPlugRunning;

/// <summary>
/// ProjectInfoProvider
/// </summary>
/// <param name="logger">ILogger</param>
/// <param name="serviceScopeFactory">IServiceScopeFactory</param>
public ProjectInfoProvider(
ILogger<ProjectInfoProvider> logger,
IServiceScopeFactory serviceScopeFactory
)
{
_serviceScopeFactory = serviceScopeFactory;
_logger = logger;
}

/// <inheritdoc />
public string GetProjectId()
{
var result = Guid.NewGuid().ToString();

var projectRunner = _serviceScopeFactory
.CreateScope()
.ServiceProvider.GetRequiredService<IProjectRunner>();

if (projectRunner?.Current == null)
{
_logger.LogWarning("ProjectRunner.Current 为空");
return result;
}

return projectRunner.Current.Info.Id;
}

/// <summary>
/// 根据数据库名称获取数据库连接字符串
/// </summary>
/// <param name="dataBaseName"></param>
/// <returns></returns>
public string GetConnectionString(string dataBaseName)
{
var projectId = GetProjectId();
var dataSourceProvider = _serviceScopeFactory
.CreateScope()
.ServiceProvider.GetRequiredService<IDataSourceProvider>();

if (dataSourceProvider == null)
{
_logger.LogWarning("DataSourceProvider 为空");
return string.Empty;
}

return dataSourceProvider.CreateConnectionString($"{projectId}_{dataBaseName}");
}

/// <inheritdoc />
public bool IsPlugsRunning { get { return _isPlugRunning; } set { _isPlugRunning = value; } }
}
}

4.3 当前用户

在前后端分离架构的应用程序中检索有关已登录用户的信息是很常见的. 当前用户是与应用程序中的当前请求相关的活动用户。

CMS2.0用户身份标识会存放在token中,token是采用jwt协议,token是放在http的请求头Authorization 中。

解码后token信息如下:

示例代码:获取当前请求用户信息

using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using CMS.Unit.Authority.Services.Abstractions;
using IdGen;
using Microsoft.AspNetCore.Http;

namespace CMS.Plugin.ProductionManagement.Service.Utilities.User;

/// <summary>
/// 当前用户信息提供者
/// </summary>
public class HttpContextCurrentUserProvider : ICurrentUserProvider
{
/// <summary>
/// HttpContextCurrentUser
/// </summary>
/// <param name="httpContextAccessor">IHttpContextAccessor</param>
/// <param name="userService">IUserService</param>
public HttpContextCurrentUserProvider(IHttpContextAccessor httpContextAccessor, IUserService userService)
{
Name = httpContextAccessor.HttpContext.User.Identity?.Name;
var nameIdentifierClaim = httpContextAccessor.HttpContext.User.Claims.FirstOrDefault(claim => claim.Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier");
if (nameIdentifierClaim != null)
{
var nameIdentifierClaimValue = nameIdentifierClaim.Value;
if (!Guid.TryParse(nameIdentifierClaimValue, out var idGuid))
{
UserAccount = nameIdentifierClaimValue;
return;
}

var userDto = userService.GetUser(idGuid).Result;
Id = userDto?.Id.ToString();
UserAccount = userDto?.UserName;
}
}

/// <summary>
/// 用户id
/// </summary>
public string? Id { get; }

/// <summary>
/// 用户名称
/// </summary>
public string? Name { get; }

/// <summary>
/// 用户账号
/// </summary>
public string? UserAccount { get; set; }
}

4.4 数据库备份

基座提供了数据库备份的能力,如果插件数据库需要备份,需要遵循基座的数据库备份约定。

示例伪代码:插件实现数据库备份的核心要点

1.需要在插件注册服务的方法中 注册IDataPackageFactory
context.Services.AddScoped<IDataPackageFactory>(p =>
new DatabasePackageFactory(PluginConstants.PluginIdentity, PluginConstants.PluginName, DataTypes.Plugin, "_project", p));

new DatabasePackageFactory参数说明:
-- PluginConstants.PluginIdentity:插件的标识
-- PluginConstants.PluginName:插件名称
-- DataTypes.Plugin : 类别
-- "_project" : 插件数据库后缀


2.需要在插件加载启用时上报插件的信息
context.GetRequiredService<DataStoreManager>()
.Add(new DataStoreGroup(PluginConstants.PluginIdentity, PluginConstants.PluginName, DataTypes.Plugin));

new DataStoreGroup参数说明:
-- PluginConstants.PluginIdentity:插件的标识
-- PluginConstants.PluginName:插件名称
-- DataTypes.Plugin : 类别

3. 1和2的参数PluginConstants.PluginIdentity、PluginConstants.PluginName需要一致

示例代码

using CMS.Data.Stressing;
using CMS.Plugin.sample.Domains.Models.Consts;
using Microsoft.Extensions.DependencyInjection;
using Structing.Core;
using Structing.Web;
using SYC.Plugin;

namespace CMS.Plugin.sample
{
/// <summary>
/// 示例插件,使用<see cref="EnableApplicationPartAttribute"/>将导入Controller
/// </summary>
[EnableApplicationPart]
public class CMSPluginEntry : PluginEntry
{
/// <summary>
/// 注册服务和配置
/// </summary>
/// <param name="context"></param>
public override void Register(IRegisteContext context)
{
//注册插件的备份功能,通过基座备份的能力完成插件的数据备份
context.Services.AddScoped<IDataPackageFactory>(p =>
new DatabasePackageFactory(PluginConstants.PluginIdentity, PluginConstants.PluginName, DataTypes.Plugin, "_project", p));

base.Register(context);
}

/// <summary>
/// 配置管道和服务
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public override Task ReadyAsync(IReadyContext context)
{
//向基座提供插件备份的相关信息,用于插件数据备份
context.GetRequiredService<DataStoreManager>()
.Add(new DataStoreGroup(PluginConstants.PluginIdentity, PluginConstants.PluginName, DataTypes.Plugin));

return base.ReadyAsync(context);
}
}
}

5.5 插件数据迁移

数据库的结构及种子数据迁移是软件系统的常规操作。基于CMS2.0软件系统的架构设计,插件数据迁移需要遵循基座的数据迁移约定。需要继承基座提供的IProjectRuntimeMigrator接口并在约定方法中完成数据库的架构及种子数据迁移。

示例伪代码:插件数据执行数据迁移的核心要点

1.插件继承基座的IProjectRuntimeMigrator接口

2.在实现的public async Task UpgradeAsync(Project.Project project, IServiceProvider serviceProvider)方法中执行插件的数据迁移操作

示例代码:

using CMS.Plugin.sample.Domains.DataAccesses.DataSeeds;
using CMS.Project.Abstractions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace CMS.Plugin.Sample.Data
{
/// <summary>
/// 工程数据库迁移器,当<see cref="IProjectRunner"/>加载工程时此对象会被创建调用
/// </summary>
public class PluginDbContextMigrator : IProjectRuntimeMigrator
{
private readonly ILogger<PluginDbContextMigrator> _logger;

public PluginDbContextMigrator(ILogger<PluginDbContextMigrator> logger)
{
_logger = logger;
}

/// <summary>
/// 执行迁移
/// </summary>
/// <param name="project">当前要运行的工程</param>
/// <param name="serviceProvider">存在运行工程上下文的serviceProvider</param>
/// <returns></returns>
public async Task UpgradeAsync(Project.Project project, IServiceProvider serviceProvider)
{
var cmsPluginDataSeed = serviceProvider.GetRequiredService<ICmsPluginDataSeedService>();
await cmsPluginDataSeed.SeedAsync();
}
}
}

5.6 插件配置

待支持,请关注后续迭代

5.7 业务/分布式事件总线

待支持,请关注后续迭代。

5.8 文件系统

待支持,请关注后续迭代

6. 基座核心业务模块

基座提供了很多标准的、强大的核心业务模块并对外提供了接口,插件可以借助基座提供的业务模块能力,快速的完成业务开发。详细的业务模块说明及接口请详看API参考文档后端 | CMS (shengyc.com)

重点的业务模块如下

  • 用户模块

用户模块提供了用户相关的数据接口,插件基座提供的接口获取响应的服务,完成自己的业务;

  • 变量模块

    提供了监听变量、访问变量、获取变量信息的接口,插件可以通过变量服务的接口实现与变量相关的业务;

  • 数据模块

    数据模块提供了强大的 数据归档,数据聚合的处理能力,通过数据模块及基座低代码能力结合,可以实现强大的、动态的数据分析功能。插件服务通过使用基座提供的数据模块接口即可实现插件业务相关的数据分析功能;

  • 告警模块

  • 其他模块

7. 完整插件打包

CMS2.0的插件是前后端分离的方式开发的,当前端及后端的功能已经开发完成后,需要将发布出来的程序包要放到插件打包工具中,插件打包工具会将发布包打包成完整的插件包。

如果需要插件打包工具请联系盛原成相关人员获取。

插件打包工具的使用说明,如图所示

配置文件中的Category属性包含的类别有:设备管理、生产管理、硬件集成、过程监控、数据获取、性能分析 ; 请根据自己所开发的插件业务选择合适的类型。

8. 示例

  • 数据库操作
  • 新建插件
  • 数据:如何实现在开发版数据模块显示自定义表格
  • 调试入口
  • Excel 导入导出
  • 多语言
  • 创建后台服务
  • 共享插件
  • 变量读写
  • 变量变化监听
  • 创建 webapi 服务

请联系相关人员获取示例代码

9. 问题及反馈

如果发现问题、想法可以向相关人员反馈。盛原成产品研发中心会对反馈进行评估,并进行相关的迭代;