Distributed Cache(分布式缓存)-SqlServer - 云霄宇霁 - 博客园

mikel阅读(295)

来源: Distributed Cache(分布式缓存)-SqlServer – 云霄宇霁 – 博客园

Net Core 缓存系列:

1、NetCore IMemoryCache 内存缓存

2、Distributed Cache(分布式缓存)-SqlServer

3、Distributed Cache(分布式缓存)-Redis

欢迎交流学习!!! GitHub源码

分布式缓存是由多个应用服务器共享的缓存,通常作为外部服务存储在单个应用服务器上,常用的有SQLServer,Redis,NCache。

分布式缓存可以提高ASP.NET Core应用程序的性能和可伸缩性,尤其是应用程序由云服务或服务器场托管时。

分布式缓存的特点:

  • 跨多个服务器请求,保证一致性。
  • 应用程序的服务器重启或部署时,缓存数据不丢失。
  • 不使用本地缓存(如果是多个应用服务器会出现不一致及数据丢失的风险)

SQL Server Distrubuted Cahce configure and application

1、Nuget下载安装包

2、使用sql-cache 工具创建缓存列表

Win+r 打开cmd命令,输入如下指令,创建缓存表,

dotnet sql-cache create "Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=DistCache;Integrated Security=True;" dbo TestCache

如遇到以下error,需要安装dotnet-SQL-cache,命令如下。(Note: NetCore 3.1 对应的dotnet-SQL-chche version 3.1.13)

dotnet tool install --global dotnet-sql-cache

如果看到如下提示,证明缓存表创建成功:

缓存表结构:

3、在请求管道中添加DistributedCache(Note:这里建议ConnectionString,SchemaName,TableName最好写在配置文件里,在本章最后简单介绍下如果在NetCore console 里配置使用appsettings.json

 View Code

4、通过构造函数依赖注入IDistributedCache

复制代码
private readonly IDistributedCache _cacheService;

public SqlServerService(IDistributedCache cacheService)
{
      this._cacheService = cacheService;
}
复制代码

最简单的方式是直接使用IDistributedCache Extension方法,提供了String类型的cache value还是比较常用的。

以下是简单封装实现,根据需求更改即可。

复制代码
 public class SqlServerService: ISqlServerService
    {
        private readonly IDistributedCache _cacheService;

        public SqlServerService(IDistributedCache cacheService)
        {
            this._cacheService = cacheService;
        }

        public async Task SetAsync(string key, byte[] value, object expiration = null, bool isAbsoluteExpiration = false)
        {
            var options = this.BuildDistributedCacheEntryOptions(expiration, isAbsoluteExpiration);
            await _cacheService.SetAsync(key, value, options);
        }

        public async Task SetAsync(string key, string value, object expiration = null, bool isAbsoluteExpiration = false)
        {
            var options = this.BuildDistributedCacheEntryOptions(expiration, isAbsoluteExpiration);
            await _cacheService.SetStringAsync(key, value, options);
        }

        public async Task<byte[]> GetAsync(string key)
        {
            return await _cacheService.GetAsync(key);
        }

        public async Task<string> GetStringAsync(string key)
        {
            return await _cacheService.GetStringAsync(key);
        }

        public async Task RemoveAsync(string key)
        {
            await _cacheService.RemoveAsync(key);
        }

        public async Task RefreshAsync(string key)
        {
            await _cacheService.RefreshAsync(key);
        }

        private DistributedCacheEntryOptions BuildDistributedCacheEntryOptions(object expiration = null, bool isAbsoluteExpiration = false)
        {
            var options = new DistributedCacheEntryOptions();
            if (expiration != null)
            {
                if (expiration is TimeSpan)
                {
                    if (isAbsoluteExpiration)
                        options.SetAbsoluteExpiration((TimeSpan)expiration);
                    else
                        options.SetSlidingExpiration((TimeSpan)expiration);
                }
                else if (expiration is DateTimeOffset)
                {
                    options.SetAbsoluteExpiration((DateTimeOffset)expiration);
                }
                else
                {
                    throw new NotSupportedException("Not support current expiration object settings.");
                }
            }
            return options;
        }
    }
复制代码

这里主要说下DistributedCacheEntryOptions这个类,作用是设置缓存项的过期时间

复制代码
public class DistributedCacheEntryOptions
{
    public DistributedCacheEntryOptions()
    public DateTimeOffset? AbsoluteExpiration { get; set; }//绝对过期时间
    public TimeSpan? AbsoluteExpirationRelativeToNow { get; set; }//绝对过期时间
    public TimeSpan? SlidingExpiration { get; set; } //滑动过期时间(如果在滑动过期时间内刷新,将重新设置滑动过去时间,对应IDistributedCache中的Refresh方法)
}
复制代码

OK,Sql Server IDistributeCache 就介绍到这里。

 

附加:NetCore Console 中使用appsettings.json文件

1、Nuget下载安装packages

2、添加appsettings.json文件,修改property->copy always

"SqlServerDistributedCache": {
    "ConnectionString": "",
    "SchemaName": "",
    "TableName": ""
  }

3、构建Configuration对象并添加到请求管道

复制代码
public static IServiceCollection ConfigureServices(this IServiceCollection services)
{
            var configuration = BuildConfiguration();
            services.AddSingleton<IConfiguration>(configuration);
            services.AddDistributedSqlServerCache(options=> 
            {
                options.ConnectionString = configuration["SqlServerDistributedCache:ConnectionString"];
                options.SchemaName = configuration["SqlServerDistributedCache:SchemaName"];
                options.TableName = configuration["SqlServerDistributedCache:TableName"];
            });
            services.AddTransient<ISqlServerService, SqlServerService>();
            return services;
}

        private static IConfigurationRoot BuildConfiguration()
        {
            var env = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");

            var builder = new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile($"appsettings.json", true, true)
                .AddJsonFile($"appsettings.{env}.json", true, true)
                .AddEnvironmentVariables();
            return builder.Build();
        }
复制代码

这里是根据环境变量“ASPNETCORE_ENVIRONMENT”读取不同的appsettings文件,比如Development,Staging,Product

4、通过构造函数注入使用IConfiguration对象即可。

完整代码地址: Github

sqlserver清除缓存,记录查询时间 - 虎头 - 博客园

mikel阅读(355)

来源: sqlserver清除缓存,记录查询时间 – 虎头 – 博客园

--1. 将当前数据库的全部脏页写入磁盘。“脏页”是已输入缓存区高速缓存且已修改但尚未写入磁盘的数据页。
--   CHECKPOINT 可创建一个检查点,在该点保证全部脏页都已写入磁盘,从而在以后的恢复过程中节省时间。
CHECKPOINT
--2. 若要从缓冲池中删除清除缓冲区,请首先使用 CHECKPOINT 生成一个冷缓存。这可以强制将当前数据库的全部脏页写入磁盘,然后清除缓冲区。
--   完成此操作后,便可发出 DBCC DROPCLEANBUFFERS 命令来从缓冲池中删除所有缓冲区。
DBCC DROPCLEANBUFFERS
--3. 释放过程缓存将导致系统重新编译某些语句(例如,即席 SQL 语句),而不重用缓存中的语句。
DBCC FREEPROCCACHE
--4. 从所有缓存中释放所有未使用的缓存条目。SQL Server 2005 Database Engine 会事先在后台清理未使用的缓存条目,以使内存可用于当前条目。
--  但是,可以使用此命令从所有缓存中手动删除未使用的条目。
DBCC FREESYSTEMCACHE ( 'ALL' )
--5. 要接着执行你的查询,不然SQLServer会时刻的自动往缓存里读入最有可能需要的数据页.

 

1
2
3
4
5
6
7
CHECKPOINT;
DBCC DROPCLEANBUFFERS;
DBCC FREEPROCCACHE;
DBCC FREESYSTEMCACHE ('ALL');
SET STATISTICS TIME ON ;
--查询条件
SET STATISTICS TIME OFF;

基于ASP.NET ZERO,开发SaaS版供应链管理系统 - freed - 博客园

mikel阅读(201)

来源: 基于ASP.NET ZERO,开发SaaS版供应链管理系统 – freed – 博客园

前言

在园子吸收营养10多年,一直没有贡献,目前园子危机时刻,除了捐款+会员,也鼓起勇气,发篇文助力一下。

2018年下半年,公司决定开发一款SaaS版行业供应链管理系统,经过选型,确定采用ABP(ASP.NET Boilerplate)框架。为了加快开发效率,购买了商业版的 ASP.NET ZERO(以下简称ZERO),选择ASP.NET Core + Angular的SPA框架进行系统开发(ABP.IO届时刚刚起步,还很不成熟,因此没有选用)。

关于ABPZERO,园子里已经有诸多介绍,因此不再赘述。本文侧重介绍我们基于ZERO框架开发系统过程中进行的一些优化、调整、扩展部分的内容,方便有需要的园友们了解或者参考。

系统架构

系统在2020年7月发布上线(部署在阿里云上),目前有超过500家企业/个人注册体验(付费的很少),感兴趣的可以在此系统的着陆网站 scm.plus 注册一个免费账号体验一下,欢迎大家的批评指正。

系统架构图

ZERO框架总体上来说还是不错的,可以快速的上手,集成的通用功能(版本、租户、角色、用户、设置等)初期都可以直接使用,但还达不到直接发布使用的水准,需要经过诸多的优化调整扩展后才能发布上线。

A 后端(ASP.NET Core)部分

0、移除不需要的功能:Chat、SignalR、DynamicProperty、GraphQL、IdentityServer4。

基于系统功能定位,移除的这些不需要的功能,使系统尽可能的精简。

1、Migrations内移除Designer.cs。

在我们的开发环境内,经过测试与验证,使用mySQL数据库时候,可以安全移除add-migration时候生成的庞大的Designer.cs文件。移除Designer.cs文件时候,需要把该文件内的DbContext与Migration声明语句移到对应的migration.cs文件内:

[DbContext(typeof(SCMDbContext))]
[Migration("20230811015119_Upgraded_To_Abp_8_3")]
public partial class Upgraded_To_Abp_8_3 : Migration
{
   ...
}

2、替换必要的功能包,确保系统后端可以部署到linux环境:

  • 使用SkiaSharp替换System.Drawing.Common;
  • 使用EPPlus替换NPOI。

3、停用系统默认的外部登录( Facebook、Google、Microsoft、Twitter等),添加微信扫码与小程序登录。

4、停用系统默认的支付选项( Paypal、Stripe等),添加支付宝(Alipay)支付。

5、Excel文件上传,ZERO默认没有实现,需要自行添加Excel文件的上传与导入功能:

  • Excel文件上传后先缓存该文件;
  • 创建一个后台Job(HangFire)执行Excel文件的读取、处理等;
  • Job发送执行后的结果(消息通知)。
[HttpPost]
[AbpMvcAuthorize(AppPermissions.Pages_Txxxs_Excel_Import)]
public async Task<JsonResult> ImportFromExcel()
{
    try
    {
        var jobArgs = await DoImportFromExcelJobArgs(AbpSession.ToUserIdentifier());

        var queueState = new EnqueuedState(GetJobQueueName());
        IBackgroundJobClient hangFireClient = new BackgroundJobClient();
        hangFireClient.Create<ImportTxxxsToExcelJob>(x => x.ExecuteAsync(jobArgs), queueState);

        return Json(new AjaxResponse(new { }));
    }
    catch (Exception ex)
    {
        return Json(new AjaxResponse(new ErrorInfo(ex.Message)));
    }
}

6、图片与文件上传存储,ZERO的默认实现是保存上传的图片文件到数据库内,需要改造存储到OSS中:

  • 使用MD5哈希前缀,生成OSS文件对象的名称(含path),提高OSS并发性能:
private static string GetOssObjName(int? tenantId, Guid id, bool isThumbnail)
{
    string tid = (tenantId ?? 0).ToString();
    string ext = isThumbnail ? "thu" : "ori"; //thu - 缩略图、ori - 原图/原文件
    string hashStr = BitConverter.ToString(MD5.HashData(Encoding.UTF8.GetBytes(tid)), 0).Replace("-", string.Empty).ToLower();

    return $"{hashStr[..4]}/{tid}/{id}.{ext}";
}
  • 若OSS未启用或者上传失败,则直接存储到数据库中:
public async Task SaveAsync(BinaryObject file)
{
    if (file?.Bytes == null) { return; }

    //1、OSS上传,成功后直接返回
    if (OssPutObject(file.TenantId, file.Id, file.Bytes, isThumbnail: false)) { return; } 

    //2、若OSS未启用或者上传失败,则直接上传到数据库中
    await _binaryObjectRepository.InsertAsync(file);
}
  • 获取时候遵循一样的逻辑:若OSS未启用或者获取不到,则直接自数据库中获取;自数据库获取成功后要同步数据库中记录到OSS中。

7、Webhook功能,需要改造支持推送数据到第三方接口,如:企业微信群、钉钉群、聚水潭API等:

  • 重写WebhookManager的SignWebhookRequest方法;
  • 重写DefaultWebhookSender的CreateWebhookRequestMessage、AddAdditionalHeaders、SendHttpRequest方法;
  • 缓存Webhook Subscription:
private SCMWebhookCacheItem SetAndGetCache(int? tenantId, string keyName = "SubscriptionCount")
{
   int tid = tenantId ?? 0; var cacheKey = $"{keyName}-{tid}";

   return _cacheManager.GetSCMWebhookCache().Get(cacheKey, () =>
   {
        int count = 0;
        var names = new Dictionary<string, List<WebhookSubscription>>();

        UnitOfWorkManager.WithUnitOfWork(() =>
        {
            using (UnitOfWorkManager.Current.SetTenantId(tenantId))
            {
                if (_featureChecker.IsEnabled(tid, "SCM.H"))            //Feature 核查
                {
                    var items = _webhookSubscriptionRepository.GetAllList(e => e.TenantId == tenantId && e.IsActive == true);
                    count = items.Count;

                    foreach (var item in items)
                    {
                        if (string.IsNullOrWhiteSpace(item.Webhooks)) { continue; }
                        var whNames = JsonHelper.DeserializeObject<string[]>(item.Webhooks); if (whNames == null) { continue; }
                        foreach (string whName in whNames)
                        {
                            if (names.ContainsKey(whName))
                            {
                                names[whName].Add(item.ToWebhookSubscription());
                            }
                            else
                            {
                                names.Add(whName, new List<WebhookSubscription> { item.ToWebhookSubscription() });
                            }
                        }
                    }
                }
            }
        });

        return new SCMWebhookCacheItem(count, names);
    });
}

8、在WebHostModule中设定只有一台Server执行后台Work,避免多台Server重复执行:

public override void PostInitialize()
{
    ...

    string defaultEndsWith = _appConfiguration["Job:DefaultEndsWith"];
    if (string.IsNullOrWhiteSpace(defaultEndsWith)) { defaultEndsWith = "01"; }
    if (AppVersionHelper.MachineName.EndsWith(defaultEndsWith))
    {
        var workManager = IocManager.Resolve<IBackgroundWorkerManager>();

        workManager.Add(IocManager.Resolve<SubscriptionExpirationCheckWorker>());
        workManager.Add(IocManager.Resolve<SubscriptionExpireEmailNotifierWorker>());
        workManager.Add(IocManager.Resolve<SubscriptionPaymentsCheckWorker>());
        workManager.Add(IocManager.Resolve<ExpiredAuditLogDeleterWorker>());
        workManager.Add(IocManager.Resolve<PasswordExpirationBackgroundWorker>());
    }

    ...
}

9、限流功能,ZERO默认没有实现,通过添加AspNetCoreRateLimit中间件集成限流功能:

  • 采用客户端ID(ClientRateLimiting)进行设置;
  • 重写RateLimitConfigurationRegisterResolvers方法,添加定制化的ClientIpHeaderResolveContributor:存在客户端ID则优先获取,反之获取客户端的IP:
    public class RateLimitConfigurationExtensions : RateLimitConfiguration  
    {
        ...
        public override void RegisterResolvers()
        {
            ClientResolvers.Add(new ClientIpHeaderResolveContributor(SCMConsts.TenantIdCookieName));
        }
    }

    public class ClientIpHeaderResolveContributor : IClientResolveContributor
    {
        private readonly string _headerName;

        public ClientIpHeaderResolveContributor(string headerName)
        {
            _headerName = headerName;     
        }

        public Task<string> ResolveClientAsync(HttpContext httpContext)
        {
            IPAddress clientIp = null;

            var headers = httpContext?.Request?.Headers;
            if (headers != null && headers.Count > 0)
            {
                if (headers.ContainsKey(_headerName))                               //0 scm_tid
                {
                    string clientId = headers[_headerName].ToString();
                    if (!string.IsNullOrWhiteSpace(clientId))
                    {
                        return Task.FromResult(clientId);
                    }
                }

                try
                {
                    if (headers.ContainsKey("X-Real-IP"))                           //1 X-Real-IP
                    {
                        clientIp = IpAddressUtil.ParseIp(headers["X-Real-IP"].ToString());
                    }
                    
                    if (clientIp == null && headers.ContainsKey("X-Forwarded-For")) //2 X-Forwarded-For
                    {
                        clientIp = IpAddressUtil.ParseIp(headers["X-Forwarded-For"].ToString());
                    }
                }
                catch {}

                clientIp ??= httpContext?.Connection?.RemoteIpAddress;             //3 RemoteIpAddress
            }

            return Task.FromResult(clientIp?.ToString());
        }
    }

B 前端(Angular)部分

0、类似后端,移除不需要的功能:Chat、SignalR、DynamicProperty等。

1、拆分精简service-proxies.ts文件:

  • ZERO使用NSwag生成前端的TypeScript代码文件service-proxies.ts,全部模块的都生成到一个文件内,导致该文件非常庞大,最终编译生成的main.js接近4MB;
  • 按系统执行层次,拆分service-proxies.ts为多个文件,精简其中的共用代码,调整module的调用、拆分、懒加载等,最终大幅度减少了main.js的大小(目前是587KB)。

2、优化表格组件primeng table,实现客户端表格使用状态的本地存储:表格列宽、列顺序、列显示隐藏、列固定、分页设定等。

3、实现客户端的卡片视图功能。

4、集成ng-lazyload-image,实现图片展示的懒加载。

5、集成ngx-markdown,实现markdown格式的在线帮助。

6、业务组件设置为独立组件,ChangeDetectionStrateg设置为OnPush:

@Component({
    changeDetection: ChangeDetectionStrategy.OnPush,
    templateUrl: './txxxs.component.html',
    standalone: true,
    imports: [...]
})
export class TxxxsComponent extends AppComponentBase {
    ...
    constructor(
        injector: Injector,
        changeDetector: ChangeDetectorRef,
    ) {
        super(injector);
        setInterval(() => { changeDetector.markForCheck(); }, AppConsts.ChangeDetectorMS);
    }
    ...
}

7、仪表盘升级为工作台,除了可以添加图表外,也可以添加业务组件(独立组件)。

8、路由直接链接业务组件,实现懒加载:

import { Route } from '@angular/router';
export default [
    { path: 'p120303/t12030301s', loadComponent: () => import('./t12030301s.component').then(c => c.T12030301sComponent), ... },
    { path: 'p120405/t12040501s', loadComponent: () => import('./t12040501s.component').then(c => c.T12040501sComponent), ... },
    { path: 'p120405/t12040502s', loadComponent: () => import('./t12040502s.component').then(c => c.T12040502sComponent), ... },
] as Route[];

9、通过webpackInclude,减少打包后的文件数量;使用webpackChunkName设定打包后的文件名:

function registerLocales(
    resolve: (value?: boolean | Promise<boolean>) => void,
    reject: any,
    spinnerService: NgxSpinnerService
) {
    if (shouldLoadLocale()) {
        let angularLocale = convertAbpLocaleToAngularLocale(abp.localization.currentLanguage.name);
        import(
            /* webpackInclude: /(en|en-GB|zh|zh-Hans|zh-Hant)\.mjs$/ */
            /* webpackChunkName: "angular-common-locales" */
            `/node_modules/@angular/common/locales/${angularLocale}.mjs`).then((module) => {
                registerLocaleData(module.default);
                resolve(true);
                spinnerService.hide();
            }, reject);
    } else {
        resolve(true);
        spinnerService.hide();
    }
}

C 小程序(Vue3)部分

后端部分已经实现小程序集成微信登录,后端输出的语言文本与API等小程序都可以直接调用,因此小程序的开发实现就相对比较容易,只需要实现必要的UI界面即可。

  • 小程序采用 uni-app(vue3) 框架进行开发,整体效率较高。
  • 有部分代码可以基于前端 Angular 的代码复制后稍加调整后即可使用。
  • 目前只输出了微信小程序,方便同企业微信群内的消息推送一体化集成。
  • 后端部分实现的Webhook功能,可以直接推送消息到企业微信群内,用户可以单击消息卡片,直接打开微信小程序内对应的页面,查看数据或者进行其他的维护操作。
  • 小程序中需要在onLaunch中进行路由守卫(登录拦截),以处理通过分享单独页面或者企业微信群内通过消息卡片直接打开小程序页面的权限核查。

总结

若没有优秀的工具框架支持,开发SaaS化系统并不是一件容易的事。基于ABP框架,使用ZERO工具,极大的降低了开发SaaS化系统的门槛,也促成了这套系统的实践与发布。

本文简要介绍了我们实现这套系统中的一些要点,供有需要的人了解参考,就算是抛砖引玉吧!

asp.net中 使用Nginx 配置 IIS站点负载均衡 - 路人阿丙 - 博客园

mikel阅读(205)

来源: asp.net中 使用Nginx 配置 IIS站点负载均衡 – 路人阿丙 – 博客园

这是一偏初学者入门的内容,发现有问题的地方,欢迎留言,一起学习,一起进步

 

本文主要记录一下在Windows平台中,IIS站点如何使用Nginx 做一个简单的负载均衡 

一、 准备工作

官网下载安装包:https://nginx.org/en/download.html

 

 

 

这里框选的Windows平台下适用的版本,分别是在线版本、稳定版、和历史版本,可以根据自己的需求选择,如果你不知道选啥,那就选个稳定版吧

 

下载之后的文件加压出来长这样:

 

 

 

这里边的文件看名字就可以发现他们的用户,比如:conf里边是nginx的配置文件,html放一些通用的静态页面,logs就是日志里,这些在后边我们都大概会用到

 

 

二、 启动nginx的方式

可以两种方式直接运行 niginx ,也可以使用命令,做测试的话 建议使用命令,别问为啥,问了就是因为操作方便

方式一:双击nginx.exe

方式二:进入cmd 到该目录下,运行 start nginx

 

 

 

启动后如果闪退,进程中也找不到nginx的进程,说明启动失败了,去logs中查看一下错误日志,一般最常见的错误有2个

1、 Nginx监听端口已经在本机上被使用,默认80端口,这种情况下我们换一个端口就行了

打开conf文件夹中的nginx.conf文件,记事本打开,修改其中的监听端口,如图监听8010端口:

 

 

 

2、 Nginx所在的目录有中文或者特殊字符了,比如这样的提示:

2020/09/02 09:36:00 [emerg] 14236#24932: CreateFile() "E:\软件安装包\分布式\nginx-1.18.0\nginx-1.18.0/conf/nginx.conf" 
failed (1113: No mapping for the Unicode character exists in the target multi-byte code page)

解决方法很简单,别放中文目录,避免目录中的特殊字符就好了,比如我把nginx-1.18.0文件夹直接拷贝到我的D盘

最后再次启动nigix ,如果启动成功,可以在进程管理器中看到nginx的进程,并且在logs文件夹中会生成一个nginx.pid的文件,这个文件存放的其实就是nginx主进程的进程ID

启动成功之后 ,浏览器中访问http://127.0.0.1:8010/ ,看到Welcome 就说明启动已经可以了

 

 

 

 

三、简单配置负载均衡

  预期配置目标:使用nginx配置,实现对两个IIS站点的均衡访问。

       预期测试现象:如果通过8010端口可以均衡的看到8011和8012两个端口对应的IIS站点中的内容, 说明配置成功

  测试站点准备操作:

  1、创建一个Web站点,我这边是这样做的:

 

 

在视图中 我写了Stie1,生成这个项目。

2、将这个项目直接复制一个,并将视图中的Site1改为Site2,这样我就有两个路由资源完全相同的站点了文件了。

3、 在IIS中配置两个站点Site1和Site2,端口号 我分别设置为:8011、8012,文件地址分别是2步骤中的两个文件夹

 

 

4、分别通过127.0.0.1:8011和127.0.0.1:8012 先检测确认这两个站点没有问题,并且可以通过界面内容看出来 是两个站点

 

测试站点准备好了,接下来开始修改nginx的配置文件了

5、打开conf下的nginx.conf,找一下有没有upstream 配置,没有的话就按照下边的代码 复制粘贴一个改改,粘贴在server节点上边就行了,注意别放server里边了

    
  upstream my_web{
        server 127.0.0.1:8011 weight=1;
        server 127.0.0.1:8012 weight=2;
    }

这里边的my_web是我自己起的名字,起个有意义的名字,后边要用

这里边的 每一个server 都指定一个映射的地址,weight值你可以理解为,在轮询分配资源的时候分配的数量,如代码配置中的1和2,意思是 8011分配一个访问之后,8012开始分,8012分2个之后再继续给8011分,这个地方其实就是分配的权重值的,分配比例是自身权重n除以总权重值值和T ,也就是 n/T

6、修改conf下的nginx.conf中的server

 

 

图中的第一个空色框,前边有提到 是监听的端口;

server_name 就是上一个步骤中我们配置的 upstream 的名字my_web;

在server中的location / 中添加proxy_pass ,上边有整体截图,这里是location参考代码,

    location / {
            root   html;
            index  index.html index.htm;
            proxy_pass   http://my_web;#my_web很眼熟对不对?没错 就是你自己定义的名字
     }

7、OK 到这里 你就可以做简单的测试了

重启一下nginx就可以了,结束进程重启  或者 cmd中使用命令:nginx -s reload

重启完成之后在浏览器中访问:127.0.0.1:8010站点,集合加上你的路由哦。然后重复刷新查看效果:

 

 

 

 

看到内容了吗?和预期测试结果吻合,简单的配置完成了!!是不是很简单  好了去装13吧,

 

四、其他常用的配置

其实就是重点说一下upstream的配置了,先来一段有注释的配置

复制代码
#########-全局块-#########

#user administrator administrators;  #配置用户或者组
worker_processes  1; #允许生成的进程数,默认为1
#pid        logs/nginx.pid; #指定nginx进程运行文件存放地址
error_log   logs/error.log error;  #制定日志路径,级别:debug|info|notice|warn|error|crit|alert|emerg 

########-events块-########
events {
    accept_mutex on;   #设置网路连接序列化,防止惊群现象发生,默认为on
    multi_accept on;  #设置一个进程是否同时接受多个网络连接,默认为off
    #use epoll;      #事件驱动模型,select|poll|kqueue|epoll|resig|/dev/poll|eventport
    worker_connections  1024; #最大连接数
}

#########-http块-#########
http {
    include       mime.types;  #文件扩展名与文件类型映射表
    default_type  application/octet-stream;  #默认文件类型,默认为text/plain

    #access_log off; #取消服务日志
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"'; #自定义格式
    access_log logs/access.log main;

    sendfile        on; #允许sendfile方式传输文件
    #tcp_nopush     on;

    #keepalive_timeout  0;
    keepalive_timeout  65; #连接超时时间

    #gzip  on;

    upstream mysvr.com {
    server 127.0.0.1:8080 weight=8;
    server 127.0.0.1:8081 weight=9;
    }

    server {
        listen       80;  #监听端口
        server_name  127.0.0.1;   #监听地址

        #charset koi8-r;
        #access_log  logs/host.access.log  main;

        location / {
            #root   html; #根目录
            #index  index.html index.htm; #设置默认页
            random_index on;  #随机访问服务器
            #设置主机头和客户端真实地址,以便服务器获取客户端真实IP
            proxy_set_header    X-Real-IP           $remote_addr;
            proxy_set_header    X-Forwarded-For     $proxy_add_x_forwarded_for;
            proxy_set_header    Host                $http_host;
            proxy_set_header    X-NginX-Proxy       true;
            proxy_set_header    Connection          "";
            proxy_http_version  1.1;
            proxy_connect_timeout 1; 
            proxy_send_timeout 30; 
            proxy_read_timeout 60;
            client_max_body_size 50m;
            client_body_buffer_size 256k;
            proxy_pass  http://mysvr.com;  #请求转向mysvr 定义的服务器列表
        }

        #error_page  404              /404.html;  #错误页

        # redirect server error pages to the static page /50x.html
        #
        #error_page   500 502 503 504  /50x.html;
        #location = /50x.html {
        #    root   html;
        #}

    }

}
复制代码

#号代表注释,这里主要说明下server块和location块。

server块中 listen       80   顾名思义这是nginx启动后监听的端口,server_name就是监听IP地址,部署的时候要填写外网IP就可以了。

location块中 root是访问根目录,index是默认页,我们这里使用proxy_pass反向代理转发其他服务器地址就先注释掉了。
接下来说下,proxy_pass 配置
proxy_pass  http://mysvr.com;   mysvr.com是自定义的名字,通过上面定义的upstream块映射获取server地址访问。

upstream mysvr.com {
    server 192.168.1.10:8080;
    server 192.168.1.10:8081;
    }
    默认方式:依照轮询,方式进行负载,每一个请求按时间顺序逐一分配到不同的后端服务器。假设后端服务器down掉。能自己主动剔除。尽管这样的方式简便、成本低廉。但缺点是:可靠性低和负载分配不均衡。
upstream mysvr.com {
    server 192.168.1.10:8080 weight=8;
    server 192.168.1.10:8081 weight=9;
    }
    weight几率方式:指定轮询几率,weight和访问比率成正比,用于后端服务器性能不均的情况,如果后端服务器down掉,能自动剔除。

upstream mysvr.com {
    ip_hash;
    server 192.168.1.10 weight=8;
    server 192.168.2.10 weight=9;
    }
    ip_hash:每个请求按照发起客户端的ip的hash结果进行匹配,这样的算法下一个固定ip地址的客户端总会访问到同一个后端服务器,这也在一定程度上解决了集群部署环境下session共享的问题。

upstream mysvr.com{      
      server 192.168.1.10; 
      server 192.168.2.10; 
      fair; 
    }
    fair(第三方)按后端服务器的响应时间来分配请求,响应时间短的优先分配。与weight分配策略类似。

 upstream mysvr.com{ 
      server 192.168.1.10:8080; 
      server 192.168.1.10:8081; 
      hash $request_uri; 
      hash_method crc32; 
    }
    url_hash(第三方)按访问url的hash结果来分配请求,使每一个url定向到同一个后端服务器。后端服务器为缓存时比較有效。
    注意:在upstream中加入hash语句。server语句中不能写入weight等其他的參数,hash_method是使用的hash算法。


upstream还能够为每一个设备设置状态值,这些状态值的含义分别例如以下:

down 表示单前的server临时不參与负载.

weight 默觉得1.weight越大,负载的权重就越大。

max_fails :同意请求失败的次数默觉得1.当超过最大次数时,返回proxy_next_upstream 模块定义的错误.

fail_timeout : max_fails次失败后。暂停的时间。

backup: 其他全部的非backup机器down或者忙的时候,请求backup机器。所以这台机器压力会最轻。

upstream bakend{ #定义负载均衡设备的Ip及设备状态 
      ip_hash; 
      server 10.0.0.11:9090 down; 
      server 10.0.0.11:8080 weight=2; 
      server 10.0.0.11:6060; 
      server 10.0.0.11:7070 backup; 
}


都配置好后,就可以启动nginx了,双击nginx.exe运行也可以。
或者打开cmd窗口,进入nginx目录下运行start nginx 启动。
运行nginx.exe -s reload  重启。
nginx.exe -s stop 停止服务

最后自一段参考:https://www.cnblogs.com/han1982/p/9590342.html,如果你有一定经验,直接看最后一段 就能配置了

 

ABP(ASP.NET Boilerplate Project)快速入门 - xhznl - 博客园

mikel阅读(213)

来源: ABP(ASP.NET Boilerplate Project)快速入门 – xhznl – 博客园

前言

这两天看了一下ABP,做个简单的学习记录。记录主要有以下内容:

  1. 从官网创建并下载项目(.net core 3.x + vue)
  2. 项目在本地成功运行
  3. 新增实体并映射到数据库
  4. 完成对新增实体的基本增删改查

ABP官网:https://aspnetboilerplate.com/
Github:https://github.com/aspnetboilerplate

创建项目

进入官网

Get started,选择前后端技术栈,我这里就选.net core 3.x和vue。

填写自己的项目名称,邮箱,然后点create my project就可以下载项目了。

解压文件

运行项目

后端项目

首先运行后端项目,打开/aspnet-core/MyProject.sln

改一下MyProject.Web.Host项目下appsettings.json的数据库连接字符串,如果本地安装了msSQL,用windows身份认证,不改也行

数据库默认是使用msSQL的,当然也可以改其他数据库。

将MyProject.Web.Host项目设置为启动项,打开程序包管理器控制台,默认项目选择DbContext所在的项目,也就是MyProject.EntityFrameworkCore。执行update-database

数据库已成功创建:

Ctrl+F5,不出意外,浏览器就会看到这个界面:

前端项目

后端项目成功运行了,下面运行一下前端项目,先要确保本机有nodejs环境并安装了vue cli,这个就不介绍了。

/vue目录下打开cmd执行:npm install

install完成后执行:npm run serve

打开浏览器访问http://localhost:8080/,不出意外的话,会看到这个界面:

使用默认用户 admin/123qwe 登录系统:

至此,前后端项目都已成功运行。
那么基于abp的二次开发该从何下手呢,最简单的,比如要增加一个数据表,并且完成最基本CRUD该怎么做?

新增实体

实体类需要放在MyProject.Core项目下,我新建一个MyTest文件夹,并新增一个Simple类,随意给2个属性。

我这里继承了abp的Entity类,Entity类有主键ID属性,这个泛型int是指主键的类型,不写默认就是int。abp还有一个比较复杂的FullAuditedEntity类型,继承FullAuditedEntity的话就有创建时间,修改时间,创建人,修改人,软删除等字段。这个看实际情况。

public class Simple : Entity<int>
{
    public string Name { get; set; }

    public string Details { get; set; }
}

修改MyProject.EntityFrameworkCore项目的/EntityFrameworkCore/MyProjectDbContext:

public class MyProjectDbContext : AbpZeroDbContext<Tenant, Role, User, MyProjectDbContext>
{
    /* Define a DbSet for each entity of the application */

    public DbSet<Simple> Simples { get; set; }

    public MyProjectDbContext(DbContextOptions<MyProjectDbContext> options)
        : base(options)
    {
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.Entity<Simple>(p =>
        {
            p.ToTable("Simples", "test");
            p.Property(x => x.Name).IsRequired(true).HasMaxLength(20);
            p.Property(x => x.Details).HasMaxLength(100);
        });
    }
}

然后就可以迁移数据库了,程序包管理器控制台执行:add-migration mytest1update-database

刷新数据库,Simples表已生成:

实体的增删改查

进入MyProject.Application项目,新建一个MyTest文件夹

Dto

CreateSimpleDto,新增Simple数据的传输对象,比如ID,创建时间,创建人等字段,就可以省略

public class CreateSimpleDto
{
    public string Name { get; set; }

    public string Details { get; set; }
}

PagedSimpleResultRequestDto,分页查询对象

public class PagedSimpleResultRequestDto : PagedResultRequestDto
{
    /// <summary>
    /// 查询关键字
    /// </summary>
    public string Keyword { get; set; }
}

SimpleDto,这里跟CreateSimpleDto的区别就是继承了EntityDto,多了个ID属性

public class SimpleDto : EntityDto<int>
{
    public string Name { get; set; }

    public string Details { get; set; }
}

SimpleProfile,用来定义AutoMapper的映射关系清单

public class SimpleProfile : Profile
{
    public SimpleProfile()
    {
        CreateMap<Simple, SimpleDto>();
        CreateMap<SimpleDto, Simple>();
        CreateMap<CreateSimpleDto, Simple>();
    }
}

Service

注意,类名参考abp的规范去命名。

ISimpleAppService,Simple服务接口。我这里继承IAsyncCrudAppService,这个接口中包含了增删改查的基本定义,非常方便。如果不需要的话,也可以继承IApplicationService自己定义

public interface ISimpleAppService : IAsyncCrudAppService<SimpleDto, int, PagedSimpleResultRequestDto, CreateSimpleDto, SimpleDto>
{

}

SimpleAppService,Simple服务,继承包含了增删改查的AsyncCrudAppService类,如果有需要的话可以override这些增删改查方法。也可以继承MyProjectAppServiceBase,自己定义。

public class SimpleAppService : AsyncCrudAppService<Simple, SimpleDto, int, PagedSimpleResultRequestDto, CreateSimpleDto, SimpleDto>, ISimpleAppService
{
    public SimpleAppService(IRepository<Simple, int> repository) : base(repository)
    {

    }

    /// <summary>
    /// 条件过滤
    /// </summary>
    /// <param name="input"></param>
    /// <returns></returns>
    protected override IQueryable<Simple> CreateFilteredQuery(PagedSimpleResultRequestDto input)
    {
        return Repository.GetAll()
            .WhereIf(!input.Keyword.IsNullOrWhiteSpace(), a => a.Name.Contains(input.Keyword));
    }
}

接口测试

重新运行项目,不出意外的话,Swagger中就会多出Simple相关的接口。

  • Create


  • Get


  • GetAll


  • Update


  • Delete


总结

ABP是一个优秀的框架,基于ABP的二次开发肯定会非常高效,但前提是需要熟练掌握ABP,弄清楚他的设计理念以及他的一些实现原理。

以后有时间的话再深入学习一下。文中如果有不妥之处欢迎指正。

.NET6.0实现IOC容器 - 宣君 - 博客园

mikel阅读(271)

来源: .NET6.0实现IOC容器 – 宣君 – 博客园

1. 创建一个.NET6应用程序

这里使用.NET6.0 WebAPI 应用

2. 声明接口

public interface IAuthService
{
bool CheckToken();
}

3. 实现接口

class AuthServiceImpl : IAuthService
{
public bool CheckToken()
{
Console.WriteLine(“check token”);
return true;
}
}

4. 配置IOC容器

下面是在 program类中的代码

var services = new ServiceCollection();
services.AddSingleton<IAuthService, AuthServiceImpl>();

5. 获取服务

通过在 Controller的构造函数中注入IAuthService

private readonly IAuthService _service;
public WeatherForecastController(IAuthService service)
{
_service = service;
}
[HttpGet(Name = “test”)]
public bool Get()
{
return _service.CheckToken();
}

启动后,通过swagger发起请求,验证接口。

基本IOC容器流程已实现。但是这样存在一个弊端,每个接口和实现都要在program中手动注册一遍,还要在Controller构造函数中进行依赖注入,有没有能自动实现注册代替program中的手动注册?

接下来,对上述流程进行改良。

6. 改良思路

定义一个AutowiredAttribute标记,通过Atrribute标记的方式,在实现类上标记其要实现的接口服务,然后实现一个服务加载类ServiceLoader,在这个类中反射获取所有具备AutoIocAttribute的实现类,然后注册到ServiceCollection中。

6.1 定义特性标记AutowiredAttribute

[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public class AutowiredAttribute : Attribute
{
/// <summary>
/// 接口
/// </summary>
public Type Iface { get; set; }
/// <summary>
/// 实现类名
/// </summary>
public string ImplClassName { get; set; }
public AutowiredAttribute(Type iface, [CallerMemberName] string implClassName = “”)
{
Iface = iface;
ImplClassName = implClassName;
}
}

6.2 实现服务加载类

利用IServiceCollection作为服务容器

public class ServiceLoader
{
private static object _lock = new object();
private static AppRuntime _inst;
private readonly IServiceCollection _iocService = new ServiceCollection();
private readonly ICollection<Assembly> _iocAssembly = new HashSet<Assembly>();
private IServiceProvider _iocServiceProvider = null;
public static ServiceLoader Instance
{
get
{
if (_inst == null)
{
lock (_lock)
{
_inst = new ServiceLoader();
_inst.Startup(typeof(ServiceLoader).Assembly);
}
}
return _inst;
}
}
public T GetService<T>()
{
EnsureAutoIoc<T>();
return _iocServiceProvider.GetService<T>();
}
private void EnsureAutoIoc<T>()
{
Startup(typeof(T).Assembly);
}
public void Startup(Assembly ass)
{
if (_iocAssembly.Any(x => x == ass))
{
return;
}
_iocAssembly.Add(ass);
var types = ass.GetTypes().Where(x => x.GetCustomAttribute<AutowiredAttribute>() != null);
foreach (var item in types)
{
var autoIocAtt = item.GetCustomAttribute<AutowiredAttribute>();
AddTransient(autoIocAtt.Iface, item);
}
//_iocServiceProvider = _iocService.BuildServiceProvider();
Interlocked.Exchange(ref _iocServiceProvider, _iocService.BuildServiceProvider());
}
private void AddTransient(Type iface, Type impl)
{
_iocService.AddTransient(iface, impl);
}
}

6.3 在实现类加上标记

[Autowired(typeof(IAuthService))]
class AuthServiceImpl : IAuthService
{
public bool CheckToken()
{
Console.WriteLine(“check token”);
return true;
}
}

6.4 在 Controller 中调用

var svc = ServiceLoader.Instance.GetService<IAuthService>();
svc.CheckToken();

至此一个基本的完整IOC容器已实现。

Gpt进阶(二):训练部署自己的ChatGPT模型(羊驼 Alpaca-LoRA) - 知乎

mikel阅读(396)

来源: Gpt进阶(二):训练部署自己的ChatGPT模型(羊驼 Alpaca-LoRA) – 知乎

参考

1 Alpaca-LoRA(羊驼模型):
github.com/tloen/alpaca
2 Chinese-alpaca-lora(开源的中文数据集)
github.com/LC1332/Chine
3 lora(git和论文参考):github.com/microsoft/Lo

本文重要介绍使用羊驼模型,在消费级显卡中,几小时内就可以完成Alpaca的微调工作

我们目前使用的都是openai的模型接口,出于数据安全的考虑,我们有时候也需要部署一个自己的AI模型,但GPT并没有开源相关模型的代码,想训练自己的模型,只能找到一些开源算法模型微调后,训练属于自己的gpt模型

LLM的开源社区贡献了很多可以给我们自己训练的模型。比如Meta开源了对标GPT3模型的LLaMA模型,斯坦福在其基础上进行微调,得到了Alpaca模型,也就是目前比较火的羊驼

lora:大型语言模型的低秩适配器;简单来说就是微调模型的另一种方式,来调试模型在具体场景下的准确度;假设模型适应过程中的权重变化也具有较低的“内在秩”,从而提出了低秩适应(low – rank adaptation, LoRA)方法。论文地址(github):github.com/microsoft/Lo

环境准备:

1 python环境

#网址:https://conda.io/en/latest/miniconda.html
wget https://repo.anaconda.com/miniconda/Miniconda3-py310_23.1.0-1-Linux-x86_64.sh #下载脚本
sh Miniconda3-py39_4.12.0-Linux-x86_64.sh # 执行
~/miniconda3/bin/conda init #初始化Shell,以便直接运行conda
conda create --name alpaca python=3.9  #关启shell,创建虚拟环境
conda activate alpaca #激活 

2 下载羊驼代码

git clone https://github.com/tloen/alpaca-lora.git #下载源代码
cd alpaca-lora
pip install -r requirements.txt #安装依赖
#测试pytorch
import torch
torch.cuda.is_available()

3 准备数据集

构造指令数据集结构,类似于instruct的方法

可参考使用开源的中文数据集:Chinese-alpaca-lora,链接开头已经给出,下载后放到项目根目录下

4 下载LLaMA基础模型

huggingface.co/decapoda

下载完成后放到根目录下/llama-7b-hf

训练模型

python finetune.py \
    --base_model 'llama-7b-hf' \
    --data_path './trans_chinese_alpaca_data.json' \
    --output_dir './lora-alpaca-zh'

其他具体参数可以git链接

模型训练后, lora-alpaca-zh 下就有模型生成了

模型推理

Inference (generate.py)

python generate.py \
    --load_8bit \
    --base_model 'decapoda-research/llama-7b-hf' \
    --lora_weights 'tloen/alpaca-lora-7b'

云端部署

使用kaggle部署模型,访问web交互

地址:kaggle.com/models

当然具体也可以部署到自己的服务器上。。。

最后,加入我们

本地CPU+6G内存部署类ChatGPT模型(Vicuna 小羊驼) - 知乎

mikel阅读(435)

来源: 本地CPU+6G内存部署类ChatGPT模型(Vicuna 小羊驼) – 知乎

1.配置介绍

笔记本硬件配置为R7 4800U + 32g内存,但是实际上8g或者16g就够了

操作系统为Deepin 20,其他操作系统应该也不影响

2.环境配置

conda create -n llm python=3.9
conda activate llm
conda install pytorch==1.13.1 cpuonly -c pytorch
pip install llama-cpp-python

llama-cpp-python是llama.cpp的Python接口,具体介绍看下面链接

3. 模型下载

这里我们要下载4-bit量化的Vicuna模型,有7B和13B两个版本,每个版本里面还细分了几个版本,主要是速度和准确率之间的平衡,根据情况自己下载

4.模型执行

from llama_cpp import Llama

# 路径替换成你自己的模型路径
llm = Llama(model_path="./models/ggml-vicuna-13b-1.1-q4_2.bin")


while True:
    # Name the planets in the solar system?
    prompt = input("prompt: ")
    # 相关控制参数自己调节
    output = llm(f"Q:{prompt} A:", max_tokens=128, stop=[roles[0]], echo=False)
    print("Answer: ", output["choices"][0]["text"])

5.测试示例

(1) Implement a Python function to find the longest common subsequence of two input strings using dynamic programming.

Here is an example implementation of a Python function that finds the longest common subsequence of two input strings using dynamic programming:
```python
def lcs(s1, s2):
    n = len(s1)
    m = len(s2)

    # Initialize dp table with not available values
    dp = [[None] * (m+1) for _ in range(n+1)]

    # Fill dp table with starting value of 0
    for i in range(1, n+1):
        dp[i][0] = 0

    # Fill dp table with starting value of 0
    for j in range(1, m+1):
        dp[0][j] = 0

    # Iterate over all possible subsequences of s1 and s2
    for i in range(1, n+1):
        for j in range(1, m+1):
            if s1[i-1] == s2[j-1]:
                # If current characters match, increment dp[i][j] by 1
                dp[i][j] = max(dp[i][j], dp[i-1][j-1] + 1)
            else:
                # If current characters do not match, use the larger of the two options
                dp[i][j] = max(dp[i][j], max(dp[i-1][j], dp[i][j-1]))

    # Return the longest common subsequence
    return dp[n][m]
```
This function takes two input strings `s1` and `s2`, and returns the longest common subsequence of the two strings.

(2) 锅包肉怎么做

锅包肉是一道非常有名的广东菜,制作方法简单,口感鲜美。以下是一份锅包肉的做法:

所需材料:

* 猪里脊肉 500克
* 白蒜 3个
* 大葱 1根
* 洋葱 1根
* 红椒 1个
* 花椒 1个
* 生抽 适量
* 食用油 适量
* 盐、胡椒粉、蒜泥 适量

步骤:
1. 将猪里脊肉切成小块,用刀刻上竖直线,保持原有形状。
2. 將白蒜、大葱和洋葱切成顺序,再将红椒和花椒切成小段。
3. 在碗中加入生抽,用中火加热,放入肉片后盖上适量的食用油。
4. 当肉片发达香气时,将白蒜、大葱和洋葱一起加入碗中,用中火燒烂,直到肉片受烧后取出。

6. 总结

7B大小的Vicuna模型4-bit量化后,效果感觉不太好,尤其是中文,不如ChatGLM-6B

13B的的Vicuna模型4-bit量化后,效果还可以,英文可能达到了宣称的那样,中文感觉还略差于ChatGLM-6B一点

thinkphp:html中的if condition 嵌套写法、条件判断的各种情况(eq、neq、gt、lt、or、and)_

mikel阅读(241)

来源: thinkphp:html中的if condition 嵌套写法、条件判断的各种情况(eq、neq、gt、lt、or、and)_<if condition=-CSDN博客

伪代码 术语符号
大于 gt
小于 lt
等于 eq
不等于 neq
或者 or
并且 and
变量start_time代表的含义是活动的开始时间,变量end_time代表的含义是活动的结束时间。两个变量都是时间戳的格式。下面就将显示状态一列的数据,各种情况下的判断条件列举出来。

等于 eq

<td>
<if condition=”$vo[‘start_time’] eq $vo[‘end_time’] “>进行中
<else>已结束</else>
</if>
</td>

image.png

不等于 neq

<td>
<if condition=”$vo[‘start_time’] neq $vo[‘end_time’] “>进行中
<else>已结束</else>
</if>
</td>

image.png

大于 gt

<td>
<if condition=”$vo[‘start_time’] gt 0 “>进行中
<else>已结束</else>
</if>
</td>

image.png

小于 lt

<td>
<if condition=”$vo[‘start_time’] lt 0 “>进行中
<else>已结束</else>
</if>
</td>

image.png

或者 or

<td>
<if condition=”($vo[‘start_time’] – $time gt 0) OR ($time neq 666) “>
<else>已结束</else>
</if>
</td>

image.png

并且 and

<td>
<if condition=”($vo[‘start_time’] gt 0) AND ($time eq 666) “>进行中
<else>已结束</else>
</if>
</td>

image.png

各种if和else的嵌套
超过一个if else的写法

<div align=”center”>
<if condition=”$company_data.approval eq 0 “>
<!– 审核中 –>
<img id=”imgChange” src=”__TMPL__/public/assets/images/step2.png” alt=””>
<elseif condition=”$company_data.approval eq 1 “>
<!– 审核通过 –>
<img id=”imgChange” src=”__TMPL__/public/assets/images/step3.png” alt=””>
<elseif condition=”($company_data.approval eq 2) OR ($company_data.approval eq 99) “>
<!– 审核被拒或者未认证 –>
<img id=”imgChange” src=”__TMPL__/public/assets/images/step1.png” alt=””>
<else />
<!– 其余情况 –>
<img id=”imgChange” src=”__TMPL__/public/assets/images/step1.png” alt=””>
</if>
</div>
一个if else的写法

<if condition=”$company_data.approval eq 0 “>
<div align=”center” id=”step2ID” class=”step2_class” style=””>
<label class=”step2Class” style=”color:Red;font-size:30px;” id=”step2Label”>人工审核中,请等待1-2天
</label>
</div>
<else>
<div align=”center” id=”step2ID” class=”step2_class” style=”display:none;”>
<label class=”step2Class” style=”color:Red;font-size:30px;” id=”step2Label”>人工审核中,请等待1-2天
</label>
</div>
</else>

</if>

————————————————
版权声明:本文为CSDN博主「sdwflqzfx」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/sdwflqzfx/article/details/104200448

安装 Puppeteer 时跳过 Chromium 下载 | 自由行

mikel阅读(422)

来源: 安装 Puppeteer 时跳过 Chromium 下载 | 自由行

Puppeteer 包含的 Chromium 因为体积过大,我们在升级 Puppeteer 时,希望可以跳过 Chromium 重新安装,本文介绍这种方法。

下载Chromium

默认情况下,下载 puppeteer 的同时,执行

$ npm i puppeteer

会自动下载 Chromium,在命令行里会看到下面的日志:

Downloading Chromium r672088 – 108 Mb [========== ] 49% 25.7s
Chromium downloaded to /你的路径

看到这些,这说明 Chromium 已经下载完成。

另外,如果本地已经有了 Chrome/Chromium 或者 准备用远程的 Chrome/Chromium,可以只安装 Puppeteer的核心功能。运行下面的命令:

$ npm i puppeteer-core

这样安装的 puppeteer 将不包含 Chromium

二者关系

Puppeteer是在Chromium上层的脚本,以 CDP 协议控制 Chromium 的行为。二者关系大致如下图。

跳过下载Chromium

由于 Chromium 体积过大(>100M),有时候本地已经安装了Chromium,在后续升级 Puppeteer 时,不需要重新下载Chromium,这时候需要跳过 Chromium 的下载。

PUPPETEER_SKIP_CHROMIUM_DOWNLOAD

跳过的方法是:

$ PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true npm i puppeteer

其实,只要有 PUPPETEER_SKIP_CHROMIUM_DOWNLOAD 这个环境变量存在(无论其值是不是 true ),都不会下载 Chromium. 跳过的时候,会提示下面的文字

**INFO** Skipping Chromium download. "PUPPETEER_SKIP_CHROMIUM_DOWNLOAD" environment variable was found.

注意

如果第一次安装 Puppeteer 的时候,使用了 PUPPETEER_SKIP_CHROMIUM_DOWNLOAD 环境变量,那么安装的 Puppeteer 中不会包含 Chromium. 这是即使删除 PUPPETEER_SKIP_CHROMIUM_DOWNLOAD 变量(unset),再次执行 npm i puppeteer 命令也不会重新下载 Chromium了,需要删除 puppeteer 重新安装。