Skip to content

后端service开发 ​

表与实体 ​

对于大型应用系统开发来说,我们依然坚持数据库优先,而不是代码优先(code first);

你的业务开发,从数据库表开始,比如,你有一个部门维护表:

名称类型长度小数点是否为空主键备注
idvarchar320True1
c_dept_codevarchar1000True部门编码
c_dept_namevarchar1000False部门名称
n_statusnumeric10False状态1启用,0停用
c_creatorvarchar1000False创建人
d_create_timetimestamp60False创建时间
c_last_modifiervarchar1000False最后更新人
d_last_modify_timetimestamp60False最后更新时间

虽然我们支持不同的主键形式,但是我们约定主键字段为ID,类型为32位字符串,否则你会遇到各种麻烦。

四个审计字段是固定的,可以有也可以没有,框架会自动检测并赋值。

c_creator、d_create_time、c_last_modifier、d_last_modify_time。

如果你希望有个乐观锁控制,字段固定为n_row_version,整形int。

下一步,准备实体,你可以手写,也可以自动生成:Hello.Service\Entities\Basic\TbbDept.cs

csharp
/// <summary>
/// 部门编码
/// </summary>
public partial class TbbDept : HmxEntity , IHmxFullAuditedEntity
{
    /// <summary>
    /// id
    /// </summary>
    [LDisplay("id")]
    public override string Id { get; set; }

    /// <summary>
    /// 部门编码
    /// </summary>
    [LDisplay("部门编码")]
    public virtual string CDeptCode { get; set; }

    /// <summary>
    /// 部门名称
    /// </summary>
    [LDisplay("部门名称")]
    public virtual string CDeptName { get; set; }

    /// <summary>
    /// 状态1启用,0停用
    /// </summary>
    [LDisplay("状态1启用,0停用")]
    public virtual decimal? NStatus { get; set; }

    /// <summary>
    /// 创建人
    /// </summary>
    [LDisplay("创建人")]
    public virtual string Creator { get; set; }

    /// <summary>
    /// 创建时间
    /// </summary>
    [LDisplay("创建时间")]
    public virtual DateTime? CreateTime { get; set; }

    /// <summary>
    /// 最后更新人
    /// </summary>
    [LDisplay("最后更新人")]
    public virtual string LastModifier { get; set; }

    /// <summary>
    /// 最后更新时间
    /// </summary>
    [LDisplay("最后更新时间")]
    public virtual DateTime? LastModifyTime { get; set; }

}

基于我们的ORM工具,你还需要准备映射文件:Hello.Service.Impl\Mappings\basic\TbbDeptMapping.cs

csharp
/// <summary>
/// 部门表映射文件
/// </summary>
public partial class TbbDeptMapping : ILinq2dbFluentEnityMapping
{
    /// <summary>
    /// 配置
    /// </summary>
    /// <param name="model"></param>
    public void Configure(FluentMappingBuilder model)
    {
        model.Entity<TbbDept>()
            .HasTableName("TBB_DEPT")
            .HasPrimaryKey(x => x.Id)
            .Property(x => x.Selected).IsNotColumn()
            .Property(x => x.Id).HasColumnName("ID")
            .Property(x => x.CDeptCode).HasColumnName("C_DEPT_CODE")
            .Property(x => x.CDeptName).HasColumnName("C_DEPT_NAME")
            .Property(x => x.NStatus).HasColumnName("N_STATUS")
            .Property(x => x.Creator).HasColumnName("c_creator")
            .Property(x => x.CreateTime).HasColumnName("d_create_time")
            .Property(x => x.LastModifier).HasColumnName("c_last_modifier")
            .Property(x => x.LastModifyTime).HasColumnName("d_last_modify_time");
    }
}

生成工具,web地址为:http://localhost:5225/admin/code(启动后端服务后访问)

当然winform也是有的;

新增服务 ​

⚠️ 重要警告:服务类和接口必须定义在正确的命名空间下,否则框架无法识别,会导致动态WebAPI生成失败,程序运行时报错!

记住一个服务需要契约层和实现层,也就是分别存放于Hello.Service和Hello.Service.Impl两个项目里面。

命名空间规范:

  • 契约层接口:{项目名}.Services.{模块名}
  • 实现层类:{项目名}.Services.{模块名}(与接口保持一致)

正确的命名空间示例:

csharp
// ✅ 正确:有明确的命名空间
namespace Hmx.Service.Admin.Services
{
    public interface IDeptAppService : IHmxAppService { }
    public class DeptAppService : HmxAppServiceBase<DeptAppService>, IDeptAppService { }
}

// ❌ 错误:缺少命名空间,会导致框架无法识别
public interface IDeptAppService : IHmxAppService { }
public class DeptAppService : HmxAppServiceBase<DeptAppService>, IDeptAppService { }

比如你可以定义一个部门管理的服务接口,存放在Hello.Service/Services/Basic/IDeptAppService.cs文件:

csharp
/// <summary>
/// 部门管理
/// </summary>
public interface IDeptAppService : IHmxAppService
{
    /// <summary>
    /// 获取部门列表
    /// </summary>
    /// <param name="input"></param>
    /// <returns></returns>
    Task<List<TbbDept>> GetDeptList(TbbDept input);

    /// <summary>
    /// 添加客户列表
    /// </summary>
    /// <returns></returns>
    Task AddDeptList(TbbDept tbbDept);

    /// <summary>
    /// 更新客户列表
    /// </summary>
    /// <returns></returns>
    Task UpdateDeptList(TbbDept tbbDept);
}

特别注意:命名约定【IXXXXXAppService】,且必须继承IhmxAppService是固定写法;

对应的,你必须有该接口的实现,存放于Hello.Service.Impl/Services/Basic/DeptAppService.cs:

csharp
/// <summary>
/// 部门服务
/// </summary>
public class DeptAppService : HmxAppServiceBase<DeptAppService>, IDeptAppService
{
    /// <summary>
    /// 添加部门列表
    /// </summary>
    public async Task AddDeptList(TbbDept tbbDept)
    {
        if (string.IsNullOrEmpty(tbbDept?.CDeptCode))
        {
            throw new UserFriendlyException("部门编码为空,请检查输入信息。");
        }

        tbbDept.Id = YitIdHelper.NextStrId();
        tbbDept.NStatus ??= 1;

        var db = GetDbContext();

        // 检查客户编码是否已存在
        var existingCustomer = await db.GetTable<TbbDept>()
            .Where(x => x.CDeptCode == tbbDept.CDeptCode)
            .FirstOrDefaultAsync();

        if (existingCustomer != null)
        {
            throw new UserFriendlyException("部门编码已存在,请检查输入信息。");
        }

        tbbDept.Id = YitIdHelper.NextStrId();
        await db.InsertAsync(tbbDept);
    }

    /// <summary>
    /// 修改部门列表
    /// </summary>
    public async Task UpdateDeptList(TbbDept tbbDept)
    {
        var db = GetDbContext();
        await db.UpdateAsync(tbbDept);
    }

    /// <summary>
    /// 获取部门列表
    /// </summary>
    public async Task<List<TbbDept>> GetDeptList(TbbDept input)
    {
        var db = GetDbContext();
        var query = await db.GetTable<TbbDept>()
            .WhereIf(!string.IsNullOrEmpty(input?.CDeptName), x => x.CDeptName.Contains(input.CDeptName))
            .WhereIf(!string.IsNullOrEmpty(input?.CDeptCode), x => x.CDeptCode.Contains(input.CDeptCode))
            .Where(x => x.NStatus == 1)
            .OrderBy(x => x.CDeptCode)
            .ToListAsync();
        return query;
    }
}

特别注意:命名约定【XXXXXAppService】,且必须继承HmxAppServiceBase<DeptAppService>, IDeptAppService是固定写法;

当然我们不建议使用实体入TbbDept直接在服务的输入和输出操作。更好的,我们建议您使用DTO对象:

比如:\Hello.Service\Dtos\basic\InputZbsDto.cs

csharp
/// <summary>
/// 质保书查询输入参数
/// </summary>
public class InputZbsDto
{
    /// <summary>
    /// 证书号
    /// </summary>
    public virtual string CZsh { get; set; }

    /// <summary>
    /// 钢种
    /// </summary>
    public virtual string CSgCode { get; set; }

    /// <summary>
    /// 原客户名称
    /// </summary>
    public virtual string CCustName1 { get; set; }

    /// <summary>
    /// 开始时间
    /// </summary>
    public virtual string DBegin { get; set; }

    /// <summary>
    /// 截止时间
    /// </summary>
    public virtual string DEnd { get; set; }
}

写完了,你就可以运行程序了,然后访问http://192.168.132.68:5225/swagger/index.html,你会看到:

到这里,你的后端服务就成功了!

常见组件使用 ​

缓存组件 ​

在所有的服务基类HmxAppServiceBase<T>里面都有两个属性:(区别是本地缓存还是分布式缓存)

csharp
protected virtual IHmxDistributedCache DistributedCache
{
    get { return EngineContext.Current.Resolve<IHmxDistributedCache>(); }
}

protected virtual IHmxCache MemoryCache
{
    get { return EngineContext.Current.Resolve<IHmxCache>(); }
}

序列化组件 ​

二进制序列化 :var formatter = EngineContext.Current.Resolve<IHmxBinaryFormatter>();

  • object Deserialize(byte[] bytes, Type type);
  • byte[] Serialize(object value);

Json序列化直接使用微软的System.Text.Json 或者 newtonsoft.json ,请直接参考官方文档。

日志组件 ​

在所有的服务基类HmxAppServiceBase<T>里面都有两个属性:(区别是本地缓存还是分布式缓存)

csharp
protected virtual ILogger<T> Logger
{
    get
    {
        return EngineContext.Current.Resolve<ILogger<T>>();
    }
}

当然你也可以直接使用 static readonly ILogger _log = LogManager.GetDefaultLogger();来获取日志对象;

事务控制 ​

系统内默认一个请求就自动开启一个事务,实际上您完全不必要关心事务的控制。

如果你想了解细节,以及手动控制,您可以参考下面的代码片段:

csharp
using (var scop = Hmx.Http.Core.Data.Uow.UnitOfWorkManager.Begin(UowScopeOption.RequiresNew))//开启一个单独的事务
{
    var tsUser = new TsUser();

    tsUser.Id = Hmx.Service.Widgets.Utilities.SequenceUtility.GenerateID();
    tsUser.CUserName = "lizx";
    GetDbContext().InsertAsync(tsUser);
    GetDbContext().DeleteAsync(x => x.CUserName == "lizx");

    using (var uow2 = Hmx.Http.Core.Data.Uow.UnitOfWorkManager.Begin(UowScopeOption.Suppress))
    {
        //该部分代码脱离父级事务,表示不在事务范围内,该部分代码没有事务控制
    }

    scop.Complete();//一定要执行这句话,否则事务无法提交
}

数据库连接 ​

csharp
/// <summary>
/// 获取数据库时间
/// </summary>
public async Task<DateTime> GetDataBaseTime()
{
    var db = GetDbContext() as HmxLinq2dbDataRepository;
    var dtime = db.GetDataConnection().Select(() => Sql.CurrentTimestamp);//这里具备原生ADO.NET的访问
    return await Task.FromResult(dtime);
}

用户上下文 ​

你可以从 EngineContext.Current.User 获取当前的登录用户信息:

csharp
public interface IHmxSession
{
    string UserId { get; }
    string UserName { get; }
    string Token { get; }
    bool IsAuthenticated { get; }
    UserType UserType { get; }
}

序列号生成 ​

请使用Hmx.Core.Utils.YitIdHelper,他有两个调用方法:

  • public static long NextId()

  • public static string NextStrId()

参考用法:

csharp
var newPermissions = input.RescIds.Select(x => new HmxRolePermission
{
    Id = YitIdHelper.NextStrId(),
    CNsCode = input.GroupId,
    CRoleId = input.RoleId,
    CRescId = x,
}).ToList();

配置文件 ​

后端服务使用 appsettings.json 进行配置,支持多环境配置(如 appsettings.Development.json)。

配置文件位置 ​

项目配置文件路径
后端WebAPI服务admin/Hmx.Service.Startup/appsettings.json

完整配置结构 ​

json
{
  "AppSettings": {
    "loglevel": "debug",
    "logBasedir": "",
    "swaggerEnable": "true",
    "backgroudJob": "Enable",
    "uploadPath": "D:\\mes-apps\\upload",
    "ipAccessFilter": "true",
    "nacos_namespace": "DEFAULT",
    "nacos_config_dataid": "MES"
  },
  "ConnectionStrings": {
    "redis": "10.11.5.49:6379,password=redis123!",
    "rabbitmq": "amqp://user:password@10.11.5.57:5672/dev",
    "influxdb": "http://user:token@10.11.5.59:8086",
    "masterdb": "Oracle.Managed://User Id=gzmes;Password=gzmes,-123;Data Source=10.11.5.47/RMESCDB;"
  },
  "OpenAI": {
    "ModelId": "oc/deepseek-v4-flash-free",
    "Endpoint": "https://api.example.com/v1",
    "ApiKey": "sk-xxxxxxxxxxxx"
  },
  "Urls": "http://0.0.0.0:5225",
  "AllowedHosts": "*"
}

配置项详解 ​

1. AppSettings - 应用设置 ​

配置项类型默认值说明
loglevelstringdebug日志级别:Trace, Debug, Info, Warn, Error, Fatal
logBasedirstring${basedir}/logs日志文件存放目录,支持绝对路径
swaggerEnablestringtrue是否启用Swagger API文档,设为 false 可关闭
backgroudJobstring-后台任务调度,设为 Enable 启用Quartz调度器
uploadPathstring{parentDir}/upload文件上传目录,供上传下载使用
ipAccessFilterstringtrueIP访问过滤器,设为 false 可关闭
nacos_namespacestringDEFAULTNacos服务注册命名空间
nacos_config_dataidstringMESNacos配置中心数据ID

代码中读取配置:

csharp
// 方式1:通过EngineContext
var config = EngineContext.Current.GetConfig("AppSettings:backgroudJob");

// 方式2:通过IConfiguration
var config = EngineContext.Current.Resolve<IConfiguration>();
var value = config.GetSection("AppSettings:uploadPath").Get<string>();

2. ConnectionStrings - 连接字符串 ​

配置项必填说明
redis✅Redis连接字符串,用于缓存、SignalR、服务注册
rabbitmq否RabbitMQ连接字符串,用于事件总线(不配则使用Redis)
masterdb✅主数据库连接,用户、角色、资源等
influxdb否InfluxDB时序数据库连接(监控指标)
其他dbkey否自定义数据库连接(如 salesdb, stockdb)

Redis连接格式:

{host}:{port},password={password}
示例:10.11.5.49:6379,password=redis123!

RabbitMQ连接格式:

amqp://{user}:{password}@{host}:{port}/{vhost}
示例:amqp://mes:bjhhg@10.11.5.57:5672/dev

数据库连接格式:

{Provider}://{ConnectionString}

支持的Provider:

Provider名称数据库类型
Oracle.ManagedOracle(推荐)
OracleOracle(旧驱动)
PostgreSQLPostgreSQL
MySql.Data / MySqlConnectorMySQL
System.Data.SqlClientSQL Server
System.Data.SQLiteSQLite

Oracle连接示例:

Oracle.Managed://User Id=gzmes;Password=gzmes,-123;Data Source=10.11.5.47/RMESCDB;Pooling=true;Min Pool Size=1;Max Pool Size=10;

PostgreSQL连接示例:

PostgreSQL://Server=xxx.cn;Port=55432;Database=xxx;User Id=xxx;Password=xxx;

多数据库配置:

json
"ConnectionStrings": {
    "masterdb": "Oracle.Managed://User Id=c##rmes;Password=xxx;Data Source=10.0.0.1/ORCLCDB;",
    "salesdb": "Oracle.Managed://User Id=sales;Password=xxx;Data Source=10.0.0.2/ORCLCDB;",
    "stockdb": "PostgreSQL://Server=stock.db.cn;Port=5432;Database=stock;User Id=xxx;Password=xxx;"
}

代码中获取数据库连接:

csharp
// 获取默认masterdb连接
var db = GetDbContext();

// 获取自定义数据库连接
var salesDb = GetDbContext("salesdb");
var stockDb = GetDbContext("stockdb");

注意:连接字符串支持AES加密,框架会自动尝试解密。

3. OpenAI - AI助手配置 ​

用于AI智能助手服务(AIMind),配置大语言模型接入。

配置项说明
ApiKeyAPI密钥
ModelId模型ID,如 oc/deepseek-v4-flash-free
EndpointAPI端点地址
json
"OpenAI": {
    "ModelId": "oc/deepseek-v4-flash-free",
    "Endpoint": "https://mimo.rv.com.cn/v1",
    "ApiKey": "sk-5d3e3554632ebc31-vvb4zt-23d4e3f2"
}

4. 其他配置 ​

配置项说明
Urls服务监听地址,格式 http://0.0.0.0:{port}
AllowedHosts允许的主机,* 表示允许所有

环境配置 ​

支持通过环境变量或文件覆盖配置:

  • appsettings.Development.json - 开发环境
  • appsettings.Production.json - 生产环境

配置优先级 ​

环境变量 > 命令行参数 > 环境配置文件 > 默认 appsettings.json

ORM工具实战 ​

定义实体 ​

csharp
using Hmx.Core;
using Hmx.Core.Data;
using Hmx.Core.Enums;
using System;

namespace Hmx.Service.Admin.Entities
{
    /// <summary>
    /// 用户表
    /// </summary>
    public partial class HmxUser : HmxEntity, IHmxFullAuditedEntity
    {
        /// <summary>
        /// 用户Id
        /// </summary>
        [LDisplay("用户Id")]
        public override string Id { get; set; }

        /// <summary>
        /// 用户名
        /// </summary>
        [LDisplay("用户名")]
        public virtual string CUserName { get; set; }

        /// <summary>
        /// 密码
        /// </summary>
        [LDisplay("密码")]
        public virtual string CPassword { get; set; }

        /// <summary>
        /// 时间戳
        /// </summary>
        [LDisplay("时间戳")]
        public virtual DateTime CTimestamp { get; set; }

        /// <summary>
        /// 创建人
        /// </summary>
        [LDisplay("创建人")]
        public virtual string Creator { get; set; }

        /// <summary>
        /// 最后更新人
        /// </summary>
        [LDisplay("最后更新人")]
        public virtual string LastModifier { get; set; }

        /// <summary>
        /// 创建时间
        /// </summary>
        [LDisplay("创建时间")]
        public virtual DateTime? CreateTime { get; set; }

        /// <summary>
        /// 最后更新时间
        /// </summary>
        [LDisplay("最后更新时间")]
        public virtual DateTime? LastModifyTime { get; set; }
    }
}

几点重要说明:

  • 所有的实体必须继承HmxEntity
  • 如果你需要审计日志支持,可选继承IhmxFullAuditedEntity
  • 实体一般放置在接口协议层,可以和前端打交道,有必要的情况,可以放后端,交互使用DTO
  • Ldisplay用于表格列的自动描述展示

建立映射 ​

csharp
using Hmx.Http.Core.Data.Linq2db;
using Hmx.Service.Admin.Entities;
using LinqToDB.Mapping;

namespace Hmx.Service.Admin.Domain.Mappings
{
    public partial class HmxUserMapping : ILinq2dbFluentEnityMapping
    {
        public void Configure(FluentMappingBuilder model)
        {
            model.Entity<HmxUser>()
                .HasTableName("HMX_USER")
                .HasPrimaryKey(x => x.Id)
                .Property(x => x.Selected).IsNotColumn()
                .Property(x => x.Id).HasColumnName("ID")
                .Property(x => x.CPassword).HasColumnName("C_PASSWORD")
                .Property(x => x.CTimestamp).HasColumnName("C_TIMESTAMP")
                .Property(x => x.CUserName).HasColumnName("C_USER_NAME")
                .Property(x => x.Creator).HasColumnName("C_CREATOR")
                .Property(x => x.LastModifier).HasColumnName("C_LAST_MODIFIER")
                .Property(x => x.CreateTime).HasColumnName("D_CREATE_TIME")
                .Property(x => x.LastModifyTime).HasColumnName("D_LAST_MODIFY_TIME");
        }
    }
}

几点重要说明:

  • 没有采取attribute方式映射,而是采取mapping类的形式
  • 映射类必须继承ILinq2dbFluentEnityMapping 并且实现
  • 映射类必须放置在后端项目中,框架会自动扫描

获取连接 ​

csharp
/// <summary>
/// 基础后台服务示例
/// </summary>
public class DemoAppService : HmxAppServiceBase<DemoAppService>, IDemoAppService
{
    /// <summary>
    /// 获取数据库时间
    /// </summary>
    public async Task<DateTime> GetDataBaseTime()
    {
        var db = GetDbContext() as HmxLinq2dbDataRepository;
        var dtime = db.GetDataConnection().Select(() => Sql.CurrentTimestamp);

        return await Task.FromResult(dtime);
    }
}

几点重要说明:

  • 所有的数据库操作都在XXXAppService里面,定义的XXXAppService都需要继承HmxAppServiceBase<T>
  • 所有的XXXAppService 都必须继承一个接口,该接口供前端使用,名称为 IXXXAppService
  • 所有的数据库连接的获取从GetDbContext() 开始,这个方法有一个参数dbkey,和连接字符串对应
  • protected IHmxDataRepository GetDbContext(string dbkey = "masterdb")
json
"ConnectionStrings": {
    "redis": "10.11.5.52:6379",
    "rabbitmq": "amqp://mes:bjhhg@10.11.5.57:5672/dev",
    "influxdb": "http://ddh:222-33333334434343==@10.11.5.59:8086",
    "masterdb": "Oracle://User Id************2*",
    "salesdb": " Oracle://User Id************3*",
    "stockdb": " Oracle://User Id*************4"
}

按列查询 ​

大多数情况下,我们会从数据库中获取整行:

from p in db.Product where p.ProductID == 5 select p;

不过,有时候收集所有字段太浪费,所以我们只想用特定的字段,但仍然使用我们的POCOs;这对依赖对象跟踪的库来说是个挑战,比如LINQ转SQL的库。

from p in db.Product orderby p.Name descending

select new Product

csharp
{
    Name = p.Name
};

组合查询 ​

我们不必串接字符串,而是可以"组合"LINQ表达式。在下面的示例中,最终的 SQL 是真还是假,或者如果不是空,会有所不同。

csharp
public static Product[] GetProducts(bool onlyActive, string searchFor)
{
    using var db = new DbNorthwind();

    var products = from p in db.Product select p;

    if (onlyActive)
    {
        products = from p in products where !p.Discontinued select p;
    }

    if (searchFor != null)
    {
        products = from p in products where p.Name.Contains(searchFor) select p;
    }

    return products.ToArray();
}

分页查询 ​

很多时候,我们需要编写只返回整个数据集子集的代码。我们对之前的例子进行了扩展,展示产品搜索功能可能的样子。

请记住,下面的代码会对数据库进行二次查询。一次是查找记录总数,这是许多分页控制的要求,一次是返回实际数据。

csharp
public static List<Product> Search(string searchFor, int currentPage, int pageSize, out int totalRecords)
{
    using var db = new DbNorthwind();

    var products = from p in db.Product select p;

    if (searchFor != null)
    {
        products = from p in products where p.Name.Contains(searchFor) select p;
    }

    totalRecords = products.Count();

    return products.Skip((currentPage - 1) * pageSize).Take(pageSize).ToList();
}

关联查询 ​

这假设我们添加了一个类,就像我们对类所做的那样,定义了所有字段,并在数据访问类中定义了表访问性质。我们现在可以写一个类似这样的 INNER JOIN 查询:CategoryProductDbNorthwind

from p in db.Product

join c in db.Category on p.CategoryID equals c.CategoryID

select new Product

csharp
{
    Name = p.Name,
    Category = c
};

以及类似这样的LEFT JOIN查询:

from p in db.Product

from c in db.Category.Where(q => q.CategoryID == p.CategoryID).DefaultIfEmpty()

select new Product

csharp
{
    Name = p.Name,
    Category = c
};

自定义结果集 ​

在前面的例子中,我们把整个对象分配给我们的产品,但如果我们想要类中的所有字段,但又不想手动指定每个字段呢?遗憾的是,我们 无法 写出这样的内容:CategoryProduct

from p in db.Product

from c in db.Category.Where(q => q.CategoryID == p.CategoryID).DefaultIfEmpty()

select new Product(c);

上述查询假设 Product 类有一个构造子接受对象。上述查询无法正常工作,但 我们可以通过 以下查询来绕过:Category

from p in db.Product

from c in db.Category.Where(q => q.CategoryID == p.CategoryID).DefaultIfEmpty()

select Product.Build(p, c) ;

为了实现这个功能,我们需要类中有一个函数,看起来像这样:Product

csharp
public static Product Build(Product? product, Category category)
{
    if (product != null)
    {
        product.Category = category;
    }

    return product;
}

这种方法的一个注意事项是,如果你用它来处理组合查询(见上文示例),这个部分只能出现在最终选择中。

插入数据 ​

迟早我们需要向数据库添加新文件。一种方法是调用命名空间中的扩展方法;所以一定要导入。ProductInsertLinqToDB

csharp
using var db = new DbNorthwind();

db.Insert(product);

这会插入我们类中的所有列,但不会获取生成的身份值。为此,我们可以使用如下方法:ProductInsertWith*Identity

csharp
using var db = new DbNorthwind();

product.ProductID = db.InsertWithInt32Identity(product);

还有一种方法是如果数据库记录是通过主键找到的,则会更新,否则会添加记录。InsertOrReplace

如果你只需要插入某些字段,或者使用数据库生成的值,你可以写:

csharp
using var db = new DbNorthwind();

db.Product
    .Value(p => p.Name, product.Name)
    .Value(p => p.UnitPrice, 10.2m)
    .Value(p => p.Added, () => Sql.CurrentTimestamp)
    .Insert();

使用该方法还允许我们构建如下插入语句:

csharp
using var db = new DbNorthwind();

var statement = db.Product
    .Value(p => p.Name, product.Name)
    .Value(p => p.UnitPrice, 10.2m);

if (storeAdded) {
    statement.Value(p => p.Added, () => Sql.CurrentTimestamp);
}

statement.Insert();

更新数据 ​

更新记录的模式与 类似。我们有一个扩展方法,可以更新数据库中的所有列:Insert

csharp
using var db = new DbNorthwind();

db.Update(product);

我们还有一个更低级别的更新机制:

csharp
using var db = new DbNorthwind();

db.Product
    .Where(p => p.ProductID == product.ProductID)
    .Set(p => p.Name, product.Name)
    .Set(p => p.UnitPrice, product.UnitPrice)
    .Update();

同样,如果需要,我们可以将更新查询拆分成多个部分:

csharp
using var db = new DbNorthwind();

var statement = db.Product
    .Where(p => p.ProductID == product.ProductID)
    .Set(p => p.Name, product.Name);

if (updatePrice) statement = statement.Set(p => p.UnitPrice, product.UnitPrice);

statement.Update();

你不局限于单一记录更新。例如,我们可以停产所有已停产的产品:

csharp
using var db = new DbNorthwind();

db.Product
    .Where(p => p.UnitsInStock == 0)
    .Set(p => p.Discontinued, true)
    .Update();

删除数据 ​

和更新记录类似,你也可以删除记录:

csharp
using var db = new DbNorthwind();

db.Product
    .Where(p => p.Discontinued)
    .Delete();

批量复制 ​

批量复制功能支持将大量数据从其他数据源传输到表中。

csharp
var list = new List<ProductTemp>();

// ... populate list ...

using var db = new DbNorthwind();

db.BulkCopy(list);

事务处理 ​

注意:下面的内容均为原理解释,实际上事务已经在框架中封装,开发无需感知。

你可以理解为每个请求就是一个事务范围,他会自动的提交事务或者回滚事务,当遇到异常的时候。

使用数据库事务非常简单。你只需调用你的 ,运行一个或多个查询,然后通过调用 提交更改。如果发生了什么需要回滚更改,你可以调用或抛出例外。BeginTransaction() DataConnectionCommitTransaction() RollbackTransaction()

csharp
using var db = new DbNorthwind();

db.BeginTransaction();

// or
// using var tr = db.BeginTransaction();

// ... select / insert / update / delete ...

if (somethingIsNotRight)
{
    db.RollbackTransaction();
    // or
    // tr.Rollback();
}
else
{
    db.CommitTransaction();
    // or
    // tr.Commit();
}

另外,你也可以用 .NET 内置的类:TransactionScope

csharp
using var transaction = new TransactionScope();

// or for async code
// using var transaction = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled);

using var db = new DbNorthwind();

// ...

transaction.Complete();

需要注意的是,你的"上下文"类有两个基类:和。它们之间的关键区别在于连接保持的行为。 打开连接,并保持连接直到 dispose 发生。

它的行为和你以前用Entity Framework时一样:每次查询都打开连接,查询完成后马上关闭连接。LinqToDB.Data.DataConnectionLinqToDB.DataContextDataConnectionDataContext

这种行为差异在与以下情况一起使用时尤为重要:TransactionScope

csharp
using var db = new LinqToDB.Data.DataConnection("provider name", "connection string");

var product = db.GetTable<Product>()
    .FirstOrDefault(); // connection opened here

var scope = new TransactionScope();

// this transaction was not attached to connection
// because it was opened earlier

product.Name = "Lollipop";

db.Update(product);

scope.Dispose();

// no transaction rollback happed, "Lollipop" has been saved

A在开启时即与环境交易挂钩。连接建立后生成的任何 s 都不会影响连接。

在之前代码中替换为 ,交易范围将如预期般工作:创建的记录将随交易一同丢弃。

DataConnectionTransactionScopeDataConnectionDataContext

虽然看起来是正确的职业,但强烈建议使用。其默认行为可以通过将属性设置为

DataContextDataConnectionCloseAfterUsetrue

csharp
public class DbNorthwind : LinqToDB.Data.DataConnection
{
    public DbNorthwind() : base("Northwind")
    {
        (this as IDataContext).CloseAfterUse = true;
    }
}

更多内容 ​

请参考 https://linq2db.github.io

发布与部署 ​

我们的后端基于 .NET 10,前端 WinForm 同样基于 .NET 10,后端服务器支持 Linux 和 Windows。由于跨平台特性,不依赖 IIS,完全独立地以 Windows 服务方式部署。

服务器目录结构 ​

假设远程服务器的目标文件夹为 D:\mes-apps,目录结构如下:

D:\mes-apps\
├── api\                          # 后端服务和前端 WinForm 更新
│   ├── Hmx.Service.Startup.exe
│   ├── appsettings.json          # 环境配置(编译后替换)
│   ├── version                   # 版本号文件
│   └── update\                   # 前端更新包(自动更新用)
│       ├── Hmx.Winforms.Startup.exe
│       ├── AutoUpdater.exe
│       └── ...
├── apks\                         # 移动端 APK 文件
├── logs\                         # 后端统一日志目录
├── nginx\                        # Nginx 服务
├── promtail\                     # 日志采集器
├── report\                       # 报表服务
├── upload\                       # 文件上传目录
└── web\                          # 前端静态文件

本章节主要讲解 api 目录的发布与部署。

手动部署 ​

1. 发布文件 ​

在开发工具中通过 Publish 分别完成前端和后端文件的发布:

2. 前后端集成 ​

将前端发布目录(output\windows\release\net10.0-windows\publish)复制到后端的 update 子目录中进行合并:

output\services\release\net10.0\publish\
├── Hmx.Service.Startup.exe       # 后端启动程序
├── appsettings.json
└── update\                       # 前端更新包(合并到此处)
    ├── Hmx.Winforms.Startup.exe
    └── ...

前后端集成在 update 目录是为了配合内置的自动更新服务。参考「自动更新」章节。

3. 部署到服务器 ​

将合并后的 publish 文件夹内容复制到服务器的 D:\mes-apps\api 目录:

4. 启动服务 ​

找到后端启动程序 Hmx.Service.Startup.exe,直接运行即可。

5. 注册为 Windows 服务 ​

使用 NSSM(Non-Sucking Service Manager)将应用注册为 Windows 服务,避免每次手动启动:

NSSM 的使用教程可参考网络资料。

CI/CD 流水线 ​

手动部署繁琐且容易出错,项目通过 Azure DevOps Pipeline 实现自动化构建和部署。

前提:需要先完成 Azure DevOps Server 的搭建,参考 Azure DevOps 服务器搭建。

流水线架构 ​

mermaid
flowchart LR
    subgraph Stage1["Stage 1: Build — 编译构建"]
        direction TB
        A1("1. Checkout 代码(含子模块)") --> A2("2. NuGet 还原")
        A2 --> A3("3. Publish 后端服务")
        A3 --> A4("4. Publish 前端 WinForms")
        A4 --> A5("5. 后处理\n环境配置·版本号·签名·清理")
        A5 --> A6("6. 发布构建产物")
    end

    Stage1 -->|"触发部署"| Stage2

    subgraph Stage2["Stage 2: Deploy — 部署"]
        direction TB
        B1("1. 下载构建产物") --> B2("2. 停止服务 NSSM")
        B2 --> B3("3. 传输文件\nrobocopy / boltgo")
        B3 --> B4("4. 启动服务 NSSM")
    end

    style Stage1 fill:#e8f4fd,stroke:#1a73e8,color:#1a1a1a
    style Stage2 fill:#e8f8e8,stroke:#2e7d32,color:#1a1a1a

流水线文件结构 ​

项目根目录/
├── scripts/
│   ├── pipelines-main-PRD.yaml              # 主流水线(PRD 环境)
│   ├── pipelines-main-DEV.yaml              # 主流水线(DEV 环境)
│   ├── pipelines-scripts-post-build-process.yml   # 编译后处理模板
│   ├── pipelines-scripts-deploy-service.yml       # 部署服务模板
│   ├── pipelines-scripts-robocopy-deploy.yml      # robocopy 传输模板
│   ├── pipelines-scripts-boltgo-deploy.yml        # boltgo 传输模板
│   ├── pipelines-scripts-winrm-exec.yml           # WinRM 远程执行模板
│   ├── app-services-PRD.config.json               # 后端 PRD 配置
│   ├── app-services-DEV.config.json               # 后端 DEV 配置
│   ├── app-winforms-PRD.config.json               # 前端 PRD 配置
│   └── AutoUpdater-signed.exe                     # 自动更新程序

主流水线配置 ​

DEV 环境(pipelines-main-DEV.yaml) ​
yaml
trigger: none

pool:
  name: default

variables:
  - group: deploy_secrets_tqmes_DEV

stages:
  # ==================== Stage 1: 编译构建 ====================
  - stage: Build
    displayName: 编译构建
    jobs:
      - job: BuildJob
        displayName: 编译 MES 项目(DEV)
        steps:
          - checkout: self
            submodules: true
            clean: true

          - task: PowerShell@2
            displayName: 'Nuget包还原'
            inputs:
              targetType: 'inline'
              script: |
                dotnet restore tqmes.startup.all.sln --configfile "D:/AzureAgent/_work/nuget.config"

          # Publish 后端服务项目
          - task: DotNetCoreCLI@2
            displayName: 'Publish Services'
            inputs:
              command: 'publish'
              projects: 'startups/DDH.TQMES.Service.Startup/DDH.TQMES.Service.Startup.csproj'
              publishWebProjects: false
              zipAfterPublish: false
              arguments: >
                --configuration Release --no-restore
                /p:PublishDir="$(Build.SourcesDirectory)\output\publish\services"

          # Publish 前端 WinForms 项目
          - task: DotNetCoreCLI@2
            displayName: 'Publish WinForms'
            inputs:
              command: 'publish'
              projects: 'startups/DDH.TQMES.Winform.Startup/DDH.TQMES.Winform.Startup.csproj'
              publishWebProjects: false
              zipAfterPublish: false
              arguments: >
                --configuration Release --no-restore
                /p:PublishDir="$(Build.SourcesDirectory)\output\publish\winforms"
                /p:UseAppHost=true

          # 调用统一的 post-build 处理模板
          - template: pipelines-scripts-post-build-process.yml
            parameters:
              cfgStartup: 'DDH.TQMES.Winform.Startup'
              cfgEnv: 'DEV'

          # 发布构建产物
          - task: PublishBuildArtifacts@1
            displayName: '发布构建产物'
            inputs:
              PathtoPublish: '$(Build.SourcesDirectory)\output\publish'
              ArtifactName: 'drop'
              publishLocation: 'Container'

  # ==================== Stage 2: 部署 ====================
  - stage: Deploy
    displayName: 部署测试服务器
    dependsOn: Build
    jobs:
      - job: DeployServer1Service
        displayName: 部署到测试服务器
        steps:
          - template: pipelines-scripts-deploy-service.yml
            parameters:
              serverIp: $(SERVER_A_IP)
              username: $(SERVER_A_USER)
              password: $(SERVER_A_PWD)
              serviceName: ddh-tqmes-app-api
PRD 环境(pipelines-main-PRD.yaml) ​

与 DEV 环境的差异主要在于:变量组、NuGet 源路径、部署到多台服务器。

yaml
trigger: none

pool:
  name: default

variables:
  - group: deploy_secrets_hgmes_PRD

stages:
  # ==================== Stage 1: 编译构建 ====================
  - stage: Build
    displayName: 编译构建
    jobs:
      - job: BuildJob
        displayName: 编译 MES-PRD 项目
        steps:
          - checkout: self
            submodules: true
            clean: true

          # 拉取依赖仓库
          - checkout: git://HgGzMes/Hmx_Admin
            path: Hmx.Admin

          - task: PowerShell@2
            displayName: "Nuget包还原"
            inputs:
              targetType: "inline"
              script: |
                dotnet restore "$(Build.SourcesDirectory)/HgGzMes/HgGzMes.slnx" \
                  --configfile "C:/AzureAgent/_work/nuget.config"

          - task: DotNetCoreCLI@2
            displayName: "Publish Services"
            inputs:
              command: "publish"
              workingDirectory: $(Build.SourcesDirectory)/HgGzMes
              projects: ".../HgGzMes.Service.Startup.csproj"
              arguments: >
                --configuration Release --no-restore
                /p:PublishDir=".../output/publish/services"

          - task: DotNetCoreCLI@2
            displayName: "Publish WinForms"
            inputs:
              command: "publish"
              workingDirectory: $(Build.SourcesDirectory)/HgGzMes
              projects: ".../HgGzMes.Winform.Startup.csproj"
              arguments: >
                --configuration Release --no-restore
                /p:PublishDir=".../output/publish/winforms"
                /p:UseAppHost=true

          - template: pipelines-scripts-post-build-process.yml
            parameters:
              cfgStartup: "HgGzMes.Winform.Startup"
              cfgEnv: "PRD"

          - task: PublishBuildArtifacts@1
            displayName: "发布构建产物"
            inputs:
              PathtoPublish: '.../output/publish'
              ArtifactName: "drop"

  # ==================== Stage 2: 部署 ====================
  - stage: Deploy
    displayName: 部署生产服务器
    dependsOn: Build
    jobs:
      - job: DeployServer1Service
        displayName: "部署到服务器A"
        steps:
          - template: pipelines-scripts-deploy-service.yml
            parameters:
              serverIp: $(SERVER_A_IP)
              username: $(SERVER_A_USER)
              password: $(SERVER_A_PWD)
              serviceName: hggzmesapi

      - job: DeployServer2Service
        displayName: "部署到服务器B"
        steps:
          - template: pipelines-scripts-deploy-service.yml
            parameters:
              serverIp: $(SERVER_B_IP)
              username: $(SERVER_B_USER)
              password: $(SERVER_B_PWD)
              serviceName: hggzmesapi

编译后处理模板 ​

文件:pipelines-scripts-post-build-process.yml

功能:写入版本号、删除调试文件、替换环境配置、复制自动更新程序、代码签名。

yaml
parameters:
  - name: cfgStartup       # 启动程序名(不含 .exe),例如 DDH.TQMES.Winform.Startup
    type: string
    default: 'DDH.TQMES.Winform.Startup'
  - name: cfgEnv           # 环境:PRD 或 DEV
    type: string
    default: 'PRD'

steps:
  - task: PowerShell@2
    displayName: '处理编译后文件'
    inputs:
      targetType: 'inline'
      script: |
        $cfgStartup = "${{ parameters.cfgStartup }}"
        $cfgEnv     = "${{ parameters.cfgEnv }}"
        $dropPath   = "$(Build.SourcesDirectory)\output\publish"
        $scriptPath = "$(Build.SourcesDirectory)\scripts"

        $servicesPath = Join-Path $dropPath "services"
        $winformsPath = Join-Path $dropPath "winforms"

        $startupExe       = "$cfgStartup.exe"
        $startupSignedExe = "$cfgStartup-signed.exe"
        $buildVersion = "Build_$(Get-Date -Format 'yyyyMMdd_HHmmss')"

        Write-Host "=== 开始编译后处理 ==="
        Write-Host "环境: $cfgEnv | 启动程序: $startupExe | 版本号: $buildVersion"

        # 写入版本文件
        $buildVersion > (Join-Path $servicesPath "version")
        $buildVersion > (Join-Path $winformsPath "version")

        # 删除调试文件和默认配置
        Get-ChildItem -Path $dropPath -Recurse `
          -Include *.pdb, *.xml, appsettings.json | Remove-Item -Force -ErrorAction SilentlyContinue

        # 复制环境配置文件
        Copy-Item "$scriptPath\app-services-$cfgEnv.config.json" `
          "$servicesPath\appsettings.json" -Force
        Copy-Item "$scriptPath\app-winforms-$cfgEnv.config.json" `
          "$winformsPath\appsettings.json" -Force

        # 复制自动更新程序
        Copy-Item "$scriptPath\startup\AutoUpdater-signed.exe" `
          "$winformsPath\AutoUpdater.exe" -Force
        Copy-Item "$scriptPath\startup\AutoUpdater-$cfgEnv.exe.config" `
          "$winformsPath\AutoUpdater.exe.config" -Force

        # 清理旧的签名临时文件
        Get-ChildItem -Path "$scriptPath\startup" `
          -Include *-signed.exe -Recurse | Remove-Item -Force -ErrorAction SilentlyContinue

        # 代码签名
        $inFile  = Join-Path $winformsPath $startupExe
        $outFile = Join-Path "$scriptPath\startup" $startupSignedExe

        Write-Host "正在签名: $inFile"
        C:\Windows\osslsigncode\osslsigncode sign `
          -pkcs12 "$scriptPath\startup\codesign.pfx" `
          -pass bjhhg `
          -in $inFile `
          -out $outFile

        Copy-Item -Path $outFile -Destination $inFile -Force

        Write-Host "=== 编译后处理完成 ==="

部署服务模板 ​

文件:pipelines-scripts-deploy-service.yml

功能:下载构建产物 → 停止 Windows 服务 → 传输文件到服务器 → 启动服务。

yaml
parameters:
  - name: serverIp
    type: string
  - name: username
    type: string
  - name: password
    type: string
  - name: serviceName
    type: string

steps:
  # 下载产物
  - task: DownloadBuildArtifacts@1
    displayName: '下载构建物到代理点'
    inputs:
      buildType: 'current'
      downloadType: 'single'
      artifactName: 'drop'
      downloadPath: '$(System.DefaultWorkingDirectory)/output'

  # 停止服务
  - template: pipelines-scripts-winrm-exec.yml
    parameters:
      serverInfo: "${{ parameters.serverIp }} ${{ parameters.username }} ${{ parameters.password }}"
      remoteCommand: |
        & `"nssm.exe`" stop ${{ parameters.serviceName }} > `$null 2>&1

  # 传输后端服务
  - template: pipelines-scripts-robocopy-deploy.yml
    parameters:
      sourcePath: '$(System.DefaultWorkingDirectory)/output/drop/services'
      targetPath: '$(MES_ROOT_PATH)/api'
      serverIp: '${{ parameters.serverIp }}'
      username: '${{ parameters.username }}'
      password: '${{ parameters.password }}'
      cleanTarget: true

  # 传输前端更新包
  - template: pipelines-scripts-robocopy-deploy.yml
    parameters:
      sourcePath: '$(System.DefaultWorkingDirectory)/output/drop/winforms'
      targetPath: '$(MES_ROOT_PATH)/api/update'
      serverIp: '${{ parameters.serverIp }}'
      username: '${{ parameters.username }}'
      password: '${{ parameters.password }}'
      cleanTarget: true

  # 启动服务
  - template: pipelines-scripts-winrm-exec.yml
    parameters:
      serverInfo: "${{ parameters.serverIp }} ${{ parameters.username }} ${{ parameters.password }}"
      remoteCommand: |
        & `"nssm.exe`" start ${{ parameters.serviceName }} > `$null 2>&1

文件传输模板 ​

项目提供两种文件传输方式,根据网络环境选择:

模板协议端口适用场景
robocopy(推荐)SMB/CIFS445445 端口可用,性能稳定
boltgoQUICUDP 7879445 端口被封锁

⚠️ 445 端口可用时优先使用 robocopy,配置更简单、性能更稳定。

robocopy 传输模板(推荐) ​

文件:pipelines-scripts-robocopy-deploy.yml

使用 Windows 内置的 robocopy 通过 SMB 协议传输文件,支持多线程(/MT:16)、断点续传、自动重试。

yaml
parameters:
  - name: sourcePath          # 本地源路径
    type: string
  - name: targetPath          # 远程目标路径(如 D:\mes-apps\api)
    type: string
  - name: serverIp            # 目标服务器 IP
    type: string
  - name: username            # 管理员用户名
    type: string
  - name: password            # 管理员密码
    type: string
  - name: cleanTarget         # 是否镜像清理(默认 true)
    type: boolean
    default: true

steps:
  - task: PowerShell@2
    displayName: ${{ parameters.displayName }}
    inputs:
      targetType: inline
      script: |
        # 自动把 D:\path 转成 \\IP\D$\path
        if ($dest -notmatch '^\\\\') {
            $drive = $dest.Split(':\')[0] + '$'
            $path  = $dest.Substring(3).Replace('\','\')
            $dest  = "\\$ip\$drive\$path"
        }

        # 建立 SMB 连接
        net use "\\$ip" $pass /user:$user >$null 2>&1

        # 执行 robocopy
        if ($clean -eq 'true') {
            robocopy "$src" "$dest" /MIR /MT:16 /R:3 /W:5 /NP /NFL
        } else {
            robocopy "$src" "$dest" /E /MT:16 /R:3 /W:5 /NP /NFL
        }

robocopy 参数说明:

参数说明
/MIR镜像模式(同步删除目标多余文件)
/E复制子目录(包括空目录)
/MT:1616 线程并行复制
/R:3失败重试 3 次
/W:5重试间隔 5 秒
/NP不显示进度百分比
/NFL不记录文件名日志
boltgo 传输模板 ​

文件:pipelines-scripts-boltgo-deploy.yml

boltgo 是基于 QUIC 协议的高速文件传输工具,适用于 SMB/445 端口被封锁的内网环境。

前置条件:

  1. 代理服务器和目标服务器均已将 boltgo.exe 加入 PATH
  2. 目标服务器开放 UDP 7879 端口
  3. 目标服务器 boltgo recv 常驻运行
yaml
parameters:
  - name: sourcePath          # 本地源路径
    type: string
  - name: targetPath          # 远程目标路径
    type: string
  - name: serverIp            # 目标服务器 IP
    type: string
  - name: username            # 管理员用户名
    type: string
  - name: password            # 管理员密码
    type: string
  - name: cleanTarget         # 是否清理目标目录(默认 true)
    type: boolean
    default: true

steps:
  - task: PowerShell@2
    displayName: ${{ parameters.displayName }}
    inputs:
      targetType: inline
      script: |
        # 1. 探测服务端 dest 目录
        $saveToBase = (boltgo probe "${ip}:7879").Trim()

        # 2. boltgo 传输到临时子目录
        $subDir = "deploy_$(Get-Random -SetSeed (Get-Date).Millisecond)"
        boltgo send "$src" "${ip}:7879" "$subDir" --debug

        # 3. WinRM 远程移动文件到最终目标
        Invoke-Command -Session $session -ScriptBlock {
          param($saveToBase, $subDir, $dest, $clean)
          $src = Join-Path $saveToBase $subDir
          $mode = if ($clean -eq "true") { "/MIR" } else { "/E" }
          robocopy $src $dest $mode /MT:16 /R:3 /W:5
        }

WinRM 远程执行模板 ​

文件:pipelines-scripts-winrm-exec.yml

通过 WinRM 远程执行命令(如启动/停止服务)。

前置条件:

powershell
# 客户端(Azure DevOps 代理)配置
winrm set winrm/config/client/auth '@{Basic="true"}'
winrm set winrm/config/client '@{AllowUnencrypted="true"}'
Set-Item WSMan:\localhost\Client\TrustedHosts -Value "*"

# 目标服务器配置
winrm set winrm/config/service/auth '@{Basic="true"}'
winrm set winrm/config/service '@{AllowUnencrypted="true"}'
yaml
parameters:
  - name: serverInfo
    type: string   # 格式: "serverIp username password"
  - name: remoteCommand
    type: string

steps:
  - task: PowerShell@2
    displayName: "远程执行命令"
    inputs:
      targetType: 'inline'
      script: |
        $parts = "${{ parameters.serverInfo }}" -split '\s+'
        $serverIp = $parts[0]
        $username = $parts[1]
        $password = $parts[2]

        $Pass = ConvertTo-SecureString $password -AsPlainText -Force
        $Creds = New-Object System.Management.Automation.PSCredential($username, $Pass)

        Invoke-Command -ComputerName $serverIp -Port 5985 -Credential $Creds `
          -Authentication Basic -UseSSL:$false -ScriptBlock {
            param($cmd)
            Invoke-Expression $cmd
          } -ArgumentList "${{ parameters.remoteCommand }}"

配置步骤 ​

Step 1: 创建项目和仓库 ​
  1. 在 Azure DevOps 中创建项目
  2. 导入 Git 仓库
  3. 配置子模块访问权限
Step 2: 配置代理池 ​
  1. 进入 Project Settings → Agent pools
  2. 创建或使用 default 代理池
  3. 确保代理服务器已安装:
    • .NET SDK 10.0
    • NuGet 配置文件
    • boltgo.exe(如使用 boltgo 传输)
    • nssm.exe(Windows 服务管理)
Step 3: 创建变量组 ​
  1. 进入 Pipelines → Library
  2. 创建变量组(如 deploy_secrets_hgmes_PRD)
  3. 添加以下变量:
变量名说明示例
SERVER_A_IP服务器 A 的 IP10.11.5.49
SERVER_A_USER服务器 A 的用户名admin
SERVER_A_PWD服务器 A 的密码***(标记为 Secret)
SERVER_B_IP服务器 B 的 IP10.11.5.50
SERVER_B_USER服务器 B 的用户名admin
SERVER_B_PWD服务器 B 的密码***(标记为 Secret)
MES_ROOT_PATHMES 安装根路径D:\mes-apps
Step 4: 配置环境配置文件 ​

在 scripts 目录下创建环境配置文件:

app-services-PRD.config.json:

json
{
  "AppSettings": {
    "loglevel": "info"
  },
  "ConnectionStrings": {
    "redis": "10.11.5.49:6379,password=xxx",
    "rabbitmq": "amqp://user:pass@10.11.5.57:5672/prd",
    "masterdb": "Oracle.Managed://User Id=xxx;Password=xxx;Data Source=xxx"
  },
  "Urls": "http://0.0.0.0:5225"
}

app-winforms-PRD.config.json:

json
{
  "AppSettings": {
    "loglevel": "info"
  },
  "ConnectionStrings": {
    "SignalR": "http://10.11.5.49:5225"
  }
}
Step 5: 创建流水线 ​
  1. 进入 Pipelines → New pipeline
  2. 选择 Existing Azure Pipelines YAML file
  3. 选择对应的流水线文件(如 scripts/pipelines-main-PRD.yaml)
  4. 保存并运行

常见问题 ​

1. NuGet 还原失败 ​

原因:nuget.config 路径错误或网络问题。

解决:

  • 确认 C:/AzureAgent/_work/nuget.config 存在
  • 检查 NuGet 源是否可达
2. boltgo 传输失败 ​

原因:UDP 端口未开放或 boltgo 服务未启动。

解决:

  • 检查目标服务器 7879 端口是否开放
  • 确认目标服务器 boltgo recv 正在运行
  • 测试连通性:boltgo probe 10.11.5.49:7879
3. WinRM 连接失败 ​

原因:WinRM 配置不正确。

解决:

powershell
# 在代理服务器上执行
winrm set winrm/config/client/auth '@{Basic="true"}'
winrm set winrm/config/client '@{AllowUnencrypted="true"}'
Set-Item WSMan:\localhost\Client\TrustedHosts -Value "*"

# 在目标服务器上执行
winrm set winrm/config/service/auth '@{Basic="true"}'
winrm set winrm/config/service '@{AllowUnencrypted="true"}'
4. 服务启动失败 ​

原因:NSSM 未安装或服务名错误。

解决:

  • 确认 nssm.exe 在目标服务器 PATH 中
  • 确认变量组中的 serviceName 与 NSSM 注册的服务名一致

HiMind 工业互联网平台 技术文档