Skip to content

项目介绍

项目结构

Admin.Core 8.5.0

Admin.Core     
├── 01.sln        // 项目文件
│     └── Directory.Build.props               // 指定 Target Framework 和 Nuget 包版本
├── 02.build      // 打包构建
│     └── Directory.Build.props               // Nuget包属性
├── modules       // 模块项目
│     └── admin   // 权限管理
│           ├── ZhonTai.Host                  // 接口启动项目
│           │     ├── ConfigCenter            // 配置中心文件
│           │     ├── InitData                // 初始化数据
│           │     ├── appsettings.json        // 系统应用配置
│           │     └── nlog.config             // nlog日志配置
│           ├── ZhonTai.Admin                 // 权限接口项目
│           │     ├── Core                    // 接口核心
│           │     ├── Domain                  // 领域:实体、仓储接口、Dto定义
│           │     ├── Repositories            // 仓储实现
│           │     ├── Services                // 服务接口、服务实现、Dto定义
│           │     └── Tools                   // 辅助工具/第三方Nuget包封装
│           └── ZhonTai.Tests                 // 权限测试项目
│                 ├── Controllers             // 接口测试
│                 ├── Repositories            // 仓储测试
│                 └── Services                // 服务测试
└── platform      // 平台项目
      ├── ZhonTai.ApiUI                       // 接口文档库
      ├── ZhonTai.Common                      // 通用帮助库
      └── ZhonTai.DynamicApi                  // 动态接口库

Admin模板 8.5.0

用模板新建项目MyCompanyName.MyProjectName,文件结构如下:

MyCompanyName.MyProjectName
  ├── MyCompanyName.MyProjectName.Host            // 启动项目
  │     ├── ConfigCenter                          // 配置中心文件
  │     ├── InitData                              // 初始化数据
  │     ├── appsettings.json                      // 系统应用配置
  │     └── nlog.config                           // nlog日志配置
  ├── MMyCompanyName.MyProjectName.Api            // 接口项目
  │     ├── Core                                  // 接口核心
  │     ├── Domain                                // 领域:实体、仓储接口定义
  │     ├── Repositories                          // 仓储实现
  │     └── Services                              // 服务接口、服务实现、输入输出模型定义
  ├── MMyCompanyName.MyProjectName.Api.Contracts  // 契约项目
  │     ├── Resources                             // 国际化资源文件
  │     └── Services                              // 服务接口、输入输出模型定义
  └── MyCompanyName.MyProjectName.Tests           // 测试项目
        ├── Controllers                           // 接口测试
        ├── Repositories                          // 仓储测试
        └── Services                              // 服务测试

配置文件

appsettings.json 应用程序配置

appsettings.json
json
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.AspNetCore": "Warning",
      "Microsoft.Hosting.Lifetime": "Information",
      "Microsoft.Extensions.Diagnostics.HealthChecks": "Warning"
    }
  },
  "AllowedHosts": "*",
  //应用配置
  "AppSettings": {
    //使用配置中心
    "UseConfigCenter": true,
    //配置中心路径
    "ConfigCenterPath": "ConfigCenter"
  },
  // 事件总线和分布式事务
  "CAP": {
    "RabbitMq": {
      "HostName": "",
      "Port": 5672,
      "UserName": "",
      "Password": ""
    }
  },
  /*
  //发送邮件验证码
  await AppInfo.GetRequiredService<ICapPublisher>().PublishAsync(SubscribeNames.EmailSingleSend,
  new EamilSingleSendEvent
  {
      ToEmail = new EamilSingleSendEvent.Models.EmailModel
      {
          Address = ""
      },
      Subject = "中台Admin账号邮件验证码",
      Body = code
  });
  */
  //邮件配置
  "Email": {
    //主机
    "Host": "smtp.qq.com",
    //端口 465、587、25
    "Port": 465,
    //是否使用SSL
    "UseSsl": true,
    //邮箱账号
    "UserName": "",
    //邮箱密码
    "Password": "",
    //发件人
    "FromEmail": {
      //名称
      "Name": "",
      //地址
      "Address": ""
    },
    //收件人
    "ToEmail": {
      //名称
      "Name": "",
      //地址
      "Address": ""
    }
  },
  //任务调度配置
  "TaskScheduler": {
    //进程启动信息
    "ProcessStartInfo": {
      "FileName": "C:/grpcurl_1.8.7/grpcurl",
      //工作目录
      "WorkingDirectory": ""
    },
    //告警邮件
    "AlerEmail": {
      "Enable": true,
      "Adress": ""
    }
  },
  // 滑动验证码
  "SlideCaptcha": {
    // 缓存过期时长
    "ExpirySeconds": 300, 
    // 缓存前缀
    "StoreageKeyPrefix": "app:captcha:",
    // 容错值(校验时用,缺口位置与实际滑动位置匹配容错范围)
    "Tolerant": 0.02,
    // 背景图配置
    "Backgrounds": [
      {
        "Type": "file",
        "Data": "wwwroot/captcha/jigsaw/backgrounds/1.jpg"
      },
      {
        "Type": "file",
        "Data": "wwwroot/captcha/jigsaw/backgrounds/2.jpg"
      },
      {
        "Type": "file",
        "Data": "wwwroot/captcha/jigsaw/backgrounds/3.jpg"
      },
      {
        "Type": "file",
        "Data": "wwwroot/captcha/jigsaw/backgrounds/4.jpg"
      },
      {
        "Type": "file",
        "Data": "wwwroot/captcha/jigsaw/backgrounds/5.jpg"
      }
    ],
    // Templates不配置,则使用默认模板
    "Templates": [
      //{
      //  "Slider": {
      //    "Type": "file",
      //    "Data": "wwwroot/captcha/jigsaw/templates/1/transparent.png"
      //  },
      //  "Hole": {
      //    "Type": "file",
      //    "Data": "wwwroot/captcha/jigsaw/templates/1/dark.png"
      //  }
      //}
    ]
  }
}

dbconfig.json 数据库配置

dbconfig.json
json
{
  "DbConfig": {
    //数据库注册键
    "key": "appdb",
    //程序集名称,自动获取实体表,为空则通过ConfigureFreeSql自定义配置
    "assemblyNames": [ "MyCompanyName.MyProjectName.Api" ],

    //监听所有操作
    "monitorCommand": false,
    //监听Curd操作
    "curd": true,
    //监听同步结构脚本
    "syncStructureSql": false,
    //监听同步数据Curd操作
    "syncDataCurd": false,

    //建库
    "createDb": false,
    //SqlServer,PostgreSQL,Oracle,OdbcOracle,OdbcSqlServer,OdbcMySql,OdbcPostgreSQL,Odbc,OdbcDameng,MsAccess
    //建库连接字符串
    //MySql "Server=localhost; Port=3306; Database=mysql; Uid=root; Pwd=pwd; Charset=utf8mb4;"
    //SqlServer "Data Source=.;User Id=sa;Password=pwd;Initial Catalog=master;TrustServerCertificate=true;Pooling=true;Min Pool Size=1"
    //PostgreSQL "Host=localhost;Port=5432;Username=postgres;Password=; Database=postgres;Pooling=true;Minimum Pool Size=1"
    //Oracle "user id=SYS;password=pwd; data source=//127.0.0.1:1521/XE;Pooling=true;Min Pool Size=1"
    "createDbConnectionString": "",
    //建库脚本,复杂建库脚本可放到createdbsql.txt中
    //MySql "CREATE DATABASE `appdb` CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_general_ci'"
    //SqlServer "CREATE DATABASE [appdb]"
    //PostgreSQL "CREATE DATABASE \"appdb\" WITH ENCODING = 'UTF8'"
    "createDbSql": "",

    //同步结构
    "syncStructure": true,
    //同步结构批次实体数
    "syncStructureEntityBatchSize": 1,
    //同步数据
    "syncData": true,
    //同步更新数据
    "sysUpdateData": true,
    //新增强制更新EntityUpdate数据
    "forceUpdate": false,
    //同步数据地址
    //"SyncDataPath": "InitData/Admin",
    //同步所有表["ad_dict_type", "ad_dict", "ad_user",  "ad_user_staff", "ad_org", "ad_role", "ad_api", "ad_view", "ad_permission", "ad_permission_api", "ad_user_role", "ad_user_org", "ad_role_permission", "ad_tenant", "ad_tenant_permission"]
    //同步指定表["ad_api", "ad_view", "ad_permission", "ad_permission_api"]
    //同步数据包含表,指定表同步,不填同步所有表
    "syncDataIncludeTables": [],
    //同步排除表["ad_user"]
    //同步数据排除表,指定表不同步
    "syncDataExcludeTables": [],
    //同步数据操作用户
    "syncDataUser": {
      "id": 161223411986501,
      "userName": "admin",
      "name": "管理员",
      "tenantId": 161223412138053
    },

    //项目初始化不开启生成数据,发布生产环境前,如果开发环境有配置数据需要更新数据包,可以开启生成数据包,使用完记得关闭
    //开启生成数据前先关闭syncStructure syncData createDb
    //生成数据
    "generateData": false,

    //数据库配置 https://github.com/dotnetcore/FreeSql/wiki/入门
    //连接字符串语法 https://www.connectionstrings.com
    //数据库类型 MySql = 0, SqlServer = 1, PostgreSQL = 2, Oracle = 3, Sqlite = 4, OdbcOracle = 5, OdbcSqlServer = 6, OdbcMySql = 7, OdbcPostgreSQL = 8, Odbc = 9, OdbcDameng = 10, MsAccess = 11, Dameng = 12, OdbcKingbaseES = 13, ShenTong = 14, KingbaseES = 15, Firebird = 16
    "type": "Sqlite",

    //连接字符串
    //MySql "Server=localhost; Port=3306; Database=appdb; Uid=root; Pwd=pwd; Charset=utf8mb4;"
    //SqlServer "Data Source=.;Integrated Security=True;Initial Catalog=appdb;Pooling=true;Min Pool Size=1"
    //PostgreSQL "Host=localhost;Port=5432;Username=postgres;Password=; Database=appdb;Pooling=true;Minimum Pool Size=1"
    //Sqlite "Data Source=|DataDirectory|\\appdb.db; Pooling=true;Min Pool Size=1"
    //"Oracle" "user id=SYS;password=pwd; data source=//127.0.0.1:1521/XE;Pooling=true;Min Pool Size=1",
    "connectionString": "Data Source=|DataDirectory|\\appdb.db; Pooling=true;Min Pool Size=1",

    //指定程序集
    //FreeSql.MySql.MySqlProvider`1,FreeSql.Provider.MySqlConnector
    "providerType": "",

    //读写分离从库列表
    "slaveList": [
      //{
      //  //权重
      //  "Weight": 1,
      //  //连接字符串
      //  "ConnectionString": "Data Source=|DataDirectory|\\appdb.db; Pooling=true;Min Pool Size=1"
      //}
    ],

    //空闲时间(分),设置idleTime=0则不自动回收, 设置1天不使用则自动回收
    "idleTime": 1440,

    //新增时强制更新EntityUpdate数据
    "forceUpdate": false,

    //多数据库
    //Core/Consts定义DbKeys枚举
    //使用仓储访问 public ModuleRepository(UnitOfWorkManagerCloud muowm) : base(DbKeys.AppDb, muowm)
    //使用FreeSqlCloud访问  freeSqlCloud.Use(DbKeys.AppDb);
    "dbs": [
      {
        //权限库
        "key": "admindb",
        //程序集名称,自动获取实体表
        "assemblyNames": [ "ZhonTai.Admin" ],

        //监听所有操作
        "monitorCommand": false,
        //监听Curd操作
        "curd": true,

        //建库
        "createDb": false,
        //SqlServer,PostgreSQL,Oracle,OdbcOracle,OdbcSqlServer,OdbcMySql,OdbcPostgreSQL,Odbc,OdbcDameng,MsAccess
        //建库连接字符串
        //MySql "Server=localhost; Port=3306; Database=mysql; Uid=root; Pwd=pwd; Charset=utf8mb4;"
        //SqlServer "Data Source=.;User Id=sa;Password=pwd;Initial Catalog=master;TrustServerCertificate=true;Pooling=true;Min Pool Size=1"
        //PostgreSQL "Host=localhost;Port=5432;Username=postgres;Password=; Database=postgres;Pooling=true;Minimum Pool Size=1"
        //Oracle "user id=SYS;password=pwd; data source=127.0.0.1:1521/XE;Pooling=true;Min Pool Size=1"
        "createDbConnectionString": "",
        //建库脚本,复杂建库脚本可放到createdbsql.txt中
        //MySql "CREATE DATABASE `admindb` CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_general_ci'"
        //SqlServer "CREATE DATABASE [admindb]"
        //PostgreSQL "CREATE DATABASE \"admindb\" WITH ENCODING = 'UTF8'"
        "createDbSql": "",

        //同步结构
        "syncStructure": true,
        //同步数据
        "syncData": true,

        //项目初始化不开启生成数据,发布生产环境前,如果开发环境有配置数据需要更新数据包,可以开启生成数据包,使用完记得关闭
        //开启生成数据前先关闭syncStructure syncData createDb
        //生成数据
        "generateData": false,

        //数据库配置 https:github.com/dotnetcore/FreeSql/wiki/入门
        //连接字符串语法 https:www.connectionstrings.com
        //数据库类型 MySql = 0, SqlServer = 1, PostgreSQL = 2, Oracle = 3, Sqlite = 4, OdbcOracle = 5, OdbcSqlServer = 6, OdbcMySql = 7, OdbcPostgreSQL = 8, Odbc = 9, OdbcDameng = 10, MsAccess = 11, Dameng = 12, OdbcKingbaseES = 13, ShenTong = 14, KingbaseES = 15, Firebird = 16
        "type": "Sqlite",

        //连接字符串
        //MySql "Server=localhost; Port=3306; Database=admindb; Uid=root; Pwd=pwd; Charset=utf8mb4;"
        //SqlServer "Data Source=.;Integrated Security=True;Initial Catalog=admindb;Pooling=true;Min Pool Size=1"
        //PostgreSQL "Host=localhost;Port=5432;Username=postgres;Password=; Database=admindb;Pooling=true;Minimum Pool Size=1"
        //Sqlite "Data Source=|DataDirectory|\\admindb.db; Pooling=true;Min Pool Size=1"
        //"Oracle" "user id=SYS;password=pwd; data source=127.0.0.1:1521/XE;Pooling=true;Min Pool Size=1",
        "connectionString": "Data Source=|DataDirectory|\\admindb.db; Pooling=true;Min Pool Size=1",

        //指定程序集
        //FreeSql.MySql.MySqlProvider`1,FreeSql.Provider.MySqlConnector
        "providerType": "",

        //读写分离从库列表
        "slaveList": [
          //{
          //  权重
          //  "Weight": 1,
          //  连接字符串
          //  "ConnectionString": "Data Source=|DataDirectory|\\admindb.db; Pooling=true;Min Pool Size=1"
          //}
        ]
      }
    ]
  }
}

appconfig.json 自定义应用配置 8.5.0

appconfig.json
json
{
  "AppConfig": {
    //应用程序类型Controllers ControllersWithViews MVC
    "appType": "Controllers",
    //Api地址
    "urls": [ "http://*:8000" ],
    //跨域地址
    "corUrls": [],
    //程序集名称
    "assemblyNames": [ "MyCompanyName.MyProjectName.Api", "ZhonTai.Admin" ],
    //租户
    "tenant": true,
    //分布式事务唯一标识app,为空则不生成分布式事务表
    "distributeKey": "",
    //验证
    "validate": {
      //登录
      "login": true,
      //权限
      "permission": true,
      //数据权限
      "dataPermission": true,
      //接口数据权限
      "apiDataPermission": true
    },
    //Swagger接口文档,访问路径/swagger
    "swagger": {
      //启用
      "enable": true,
      //启用枚举架构过滤器
      "enableEnumSchemaFilter": true,
      //启用接口排序文档过滤器
      "enableOrderTagsDocumentFilter": true,
      //启用枚举属性名
      "enableJsonStringEnumConverter": false,
      //启用SchemaId命名空间
      "enableSchemaIdNamespace": false,
      //程序集列表,用于启用SchemaId命名空间
      "assemblyNameList": [],
      //路由前缀,如配置微服务文档地址:doc/module/swagger
      "routePrefix": "app/swagger",
      //地址
      "url": "http://localhost:8000",
      //项目列表
      "projects": [
        {
          "name": "MyCompanyName.MyProjectName",
          "code": "app",
          "version": "v8.3.0",
          "description": "MyCompanyName.MyProjectName"
        }
        ,
        {
          "name": "中台Admin",
          "code": "admin",
          "version": "v8.3.0",
          "description": "权限管理"
        }
      ]
    },
    //新版接口文档展示
    "apiUI": {
      //启用
      "enable": true,
      //路由前缀,如配置微服务文档地址:doc/module
      "routePrefix": "app",
      //页脚
      "footer": {
        "enable": false,
        "content": "Copyright<a-icon type=\"copyright\" /> 2022-<a target=\"_blank\" href=\"https://www.zhontai.net\">中台Admin</a>"
      }
    },
    //MiniProfiler性能分析器
    "miniProfiler": false,
    //统一认证授权服务器
    "identityServer": {
      //启用
      "enable": false,
      //地址,开发认证地址前往appconfig.Development.json修改
      "url": "https://localhost:5000",
      //启用Https
      "requireHttpsMetadata": true,
      //订阅者
      "audience": "admin.server.api"
    },
    //面向切面编程
    "aop": {
      //事物
      "transaction": true
    },
    //日志
    "log": {
      //操作日志
      "operation": true
    },
    //限流
    "rateLimit": false,
    //验证码
    "varifyCode": {
      //启用
      "enable": true,
      //字体列表
      "fonts": [ "Times New Roman", "Verdana", "Arial", "Gungsuh", "Impact" ]
    },
    //默认密码
    "defaultPassword": "123asd",
    //动态api
    "dynamicApi": {
      //结果格式化
      "formatResult": true
    },
    //实现标准标识密码哈希
    "passwordHasher": false,
    //Kestrel服务器配置
    "Kestrel": {
      //HTTP连接保活最长时间,单位秒,600 = 10 分钟
      "KeepAliveTimeout": 600,
      //发送请求头最长时间,单位秒,600 = 10 分钟
      "RequestHeadersTimeout": 600,
      //最大请求大小,单位 bytes, 设置 null 不限制大小,104857600 = 100 MB
      "maxRequestBodySize": 104857600
    },
    //健康检查
    "healthChecks": {
      //启用
      "enable": true,
      //访问路径
      "path": "/app/health"
    },
    //指定跨域访问时预检等待时间,以秒为单位,默认30分钟
    "PreflightMaxAge": 1800,
    //是否开启任务调度管理界面
    "TaskSchedulerUI": {
      //启用
      "enable": true,
      //访问路径
      "path": "/task"
    },
    //Id生成器
    "IdGenerator": {
      "DataCenterId": 0, // 数据中心ID(机房ID,默认0) 请确保全局唯一
      "WorkerId": 1, // 机器码,必须全局唯一(或相同 DataCenterId 内唯一),理论最大值 2^WorkerIdBitLength-1 = 63,最大值 524287
      "WorkerIdBitLength": 6, // 机器码位长,决定 WorkerId 的最大值,默认值6,取值范围 [1, 19]
      "SeqBitLength": 6, // 序列数位长,默认值6,取值范围 [3, 21],决定每毫秒基础生成的ID个数。如果每秒请求数不超过5W,保持默认值6即可;如果超过5W,不超过50W,建议赋值10或更大,以此类推。
      "CachePrefix": "zhontai:workerid" // 缓存前缀
    },
    //语言配置
    "Lang": {
      //启用Json配置
      "EnableJson": true,
      //默认语言
      "DefaultLang": "zh-CN",
      //语言列表
      "Langs": [ "zh-CN", "en" ],
      //语言请求解析列表
      "RequestCultureProviders": [] //["QueryString","Cookie","AcceptLanguageHeader"]
    },
    //IP地址定位
    "IP2Region": {
      //启用
      "Enable": true,
      //绝对数据库路径,为空则默认使用网站根目录/ip2region.xdb
      "DbPath": ""
    }
  }
}

cacheconfig.json 缓存配置

cacheconfig.json
json
{
  "CacheConfig": {
    //缓存类型 Memory = 0,Redis = 1
    "type": "Memory",
    //限流缓存类型 Memory = 0,Redis = 1
    "typeRateLimit": "Memory",
    //Redis配置
    "redis": {
      //连接字符串
      "connectionString": "127.0.0.1:6379,password=,defaultDatabase=0",
      //限流连接字符串
      "connectionStringRateLimit": "127.0.0.1:6379,password=,defaultDatabase=0"
    }
  }
}

jwtconfig.json Jwt配置

jwtconfig.json
json
{
  "JwtConfig": {
    //发行者
    "issuer": "admin.core",
    //订阅者
    "audience": "admin.core",
    //密钥
    "securityKey": "73c50ca4f5e411eda7095254008978e98978e9edada7095254070952540089",
    //有效期(分钟) 120 = 2小时
    "expires": 120,
    //刷新有效期(分钟) 1440 = 1天
    "refreshExpires": 1440
  }
}

ossconfig.json OSS文件上传配置

ossconfig.json
json
{
  "OssConfig": {
    //本地上传配置
    "LocalUploadConfig": {
      //上传目录
      "Directory": "upload",
      //日期目录
      "DateTimeDirectory": "yyyy/MM/dd",
      "Md5": false,
      //文件最大大小byte
      "MaxSize": 104857600,
      //包含文件拓展名
      "IncludeExtension": [],
      //排除文件拓展名
      "ExcludeExtension": [ ".exe", ".dll", ".jar" ]
    },
    //文件存储供应商
    "Provider": "Minio",
    //OSS配置列表
    "OSSConfigs": [
      //Minio
      {
        "Provider": "Minio",
        "Endpoint": "127.0.0.1:9006",
        "Region": "",
        "AccessKey": "minio",
        "SecretKey": "minio",
        "IsEnableHttps": false,
        "IsEnableCache": true,
        "BucketName": "admin",
        "Url": "", //文件外链
        "Md5": false,
        "Enable": false
      },
      //阿里云
      {
        "Provider": "Aliyun",
        "Endpoint": "oss-cn-shenzhen.aliyuncs.com",
        "Region": "",
        "AccessKey": "",
        "SecretKey": "",
        "IsEnableHttps": true,
        "IsEnableCache": true,
        "BucketName": "admin",
        "Url": "",
        "Md5": false,
        "Enable": false
      },
      //腾讯云
      {
        "Provider": "QCloud",
        "Endpoint": "", //AppId
        "Region": "",
        "AccessKey": "",
        "SecretKey": "",
        "IsEnableHttps": true,
        "IsEnableCache": true,
        "BucketName": "admin",
        "Url": "",
        "Md5": false,
        "Enable": false
      },
      //七牛
      {
        "Provider": "Qiniu",
        "Endpoint": "",
        "Region": "",
        "AccessKey": "",
        "SecretKey": "",
        "IsEnableHttps": true,
        "IsEnableCache": true,
        "BucketName": "admin",
        "Url": "",
        "Md5": false,
        "Enable": false
      },
      //华为云
      {
        "Provider": "HuaweiCloud",
        "Endpoint": "",
        "Region": "",
        "AccessKey": "",
        "SecretKey": "",
        "IsEnableHttps": true,
        "IsEnableCache": true,
        "BucketName": "admin",
        "Url": "",
        "Md5": false,
        "Enable": false
      }
    ]
  }
}

ratelimitconfig.json 限流配置

ratelimitconfig.json
json
{
  /*
  https://github.com/stefanprodan/AspNetCoreRateLimit/wiki/IpRateLimitMiddleware
  https://github.com/stefanprodan/AspNetCoreRateLimit/wiki/Using-Redis-as-a-distributed-counter-store
  */
  "IpRateLimiting": {
    "EnableEndpointRateLimiting": true,
    "StackBlockedRequests": false,
    "RealIpHeader": "X-Real-IP",
    "ClientIdHeader": "X-ClientId",
    "IpWhitelist": [], // "127.0.0.1"
    "EndpointWhitelist": [ "get:/api/admin/auth/refresh" ], // "get:/api/a", "*:/api/b"
    "ClientWhitelist": [],
    "HttpStatusCode": 429,
    "QuotaExceededResponse": {
      "Content": "{{\"code\":429,\"msg\":\"访问过于频繁!\"}}",
      "ContentType": "application/json",
      "StatusCode": 429
    },
    "GeneralRules": [
      {
        "Endpoint": "*",
        "Period": "1s",
        "Limit": 3
      },
      {
        "Endpoint": "*",
        "Period": "10m",
        "Limit": 200
      }
    ]
  }
}