NopCommerce源码架构详解-产品列表相关源码分析1

nop中最常用到的就是产品列表展示,我们可以指定排序方式、指定数据显示方式(List或Grid)和指定每页显示的记录数。

NopCommerce源码架构详解概述

文章目录

概述

NopCommerce源码架构详解-产品列表相关源码分析1。

内容

nop中最常用到的就是产品列表展示,我们可以指定排序方式、指定数据显示方式(List或Grid)和指定每页显示的记录数。

在浏览器查看:http://localhost:15536/software-games?pagesize=4&viewmode=list&orderby=15

会有如下图所示:

2020-04-23-20-48-48

这个分类的Url地址为:/software-games?pagesize=4&viewmode=list&orderby=15,可以看到Nop的所有产品的分类url都是通过用分类名这种利于seo的方式,而不是通过分类ID的访问。

这里主要用到了Nop.Web.Infrastructure.GenericUrlRouteProvider和Nop.Web.Framework.Seo.GenericPathRoute。

using System.Web.Routing;
using Nop.Web.Framework.Localization;
using Nop.Web.Framework.mvc.Routes;
using Nop.Web.Framework.Seo;

namespace Nop.Web.Infrastructure
{
    public partial class GenericUrlRouteProvider : IRouteProvider
    {
        public void RegisterRoutes(RouteCollection routes)
        {
            //generic URLs
            routes.MapGenericPathRoute("GenericUrl",
                                       "{generic_se_name}",
                                       new {controller = "Common", action = "GenericUrl"},
                                       new[] {"Nop.Web.Controllers"});

            //省略其它代码...
        }

        public int Priority
        {
            get
            {
                //it should be the last route
                //we do not set it to -int.MaxValue so it could be overriden (if required)
                return -1000000;
            }
        }
    }
}

MVC框架会根据路由里面变量generic_se_name值进行转换成具体真实Controller。

/software-games?pagesize=4&viewmode=list&orderby=15

上面的Url的对应generic_se_name为software-games。而我们看Nop的数据库有一个很关键的表:UrlRecord。

2020-04-23-20-48-53

这个表的字段Slug正好和generic_se_name相应对应。我们可以看到Slug值为software-games这条记录的EntityName为Category。在类Nop.Web.Framework.Seo.GenericPathRoute有一个GetRouteData方法会根据generic_se_name获取到一个对应的UrlRecord,然后根据这个UrlRecord的EntityName的值,把data.Values值赋值,转到正确的Controller。如下代码:

public override RouteData GetRouteData(HttpContextBase httpContext)
{
    RouteData data = base.GetRouteData(httpContext);
    if (data != null && DataSettingsHelper.DatabaseIsInstalled())
    {
        var urlRecordService = EngineContext.Current.Resolve<IUrlRecordService>();
        var slug = data.Values["generic_se_name"] as string;
        //performance optimization.
        //we load a cached verion here. it reduces number of SQL requests for each page load
        var urlRecord = urlRecordService.GetBySlugCached(slug);
        //comment the line above and uncomment the line below in order to disable this performance "workaround"
        //var urlRecord = urlRecordService.GetBySlug(slug);
        if (urlRecord == null)
        {
            //no URL record found

            //var webHelper = EngineContext.Current.Resolve<IWebHelper>();
            //var response = httpContext.Response;
            //response.Status = "302 Found";
            //response.RedirectLocation = webHelper.GetStoreLocation(false);
            //response.End();
            //return null;

            data.Values["controller"] = "Common";
            data.Values["action"] = "PageNotFound";
            return data;
        }
        //ensre that URL record is active
        if (!urlRecord.IsActive)
        {
            //URL record is not active. let's find the latest one
            var activeSlug = urlRecordService.GetActiveSlug(urlRecord.EntityId, urlRecord.EntityName, urlRecord.LanguageId);
            if (!string.IsNullOrWhiteSpace(activeSlug))
            {
                //the active one is found
                var webHelper = EngineContext.Current.Resolve<IWebHelper>();
                var response = httpContext.Response;
                response.Status = "301 Moved Permanently";
                response.RedirectLocation = string.Format("{0}{1}", webHelper.GetStoreLocation(false), activeSlug);
                response.End();
                return null;
            }
            else
            {
                //no active slug found

                //var webHelper = EngineContext.Current.Resolve<IWebHelper>();
                //var response = httpContext.Response;
                //response.Status = "302 Found";
                //response.RedirectLocation = webHelper.GetStoreLocation(false);
                //response.End();
                //return null;

                data.Values["controller"] = "Common";
                data.Values["action"] = "PageNotFound";
                return data;
            }
        }

        //ensure that the slug is the same for the current language
        //otherwise, it can cause some issues when customers choose a new language but a slug stays the same
        var workContext = EngineContext.Current.Resolve<IWorkContext>();
        var slugForCurrentLanguage = SeoExtensions.GetSeName(urlRecord.EntityId, urlRecord.EntityName, workContext.WorkingLanguage.Id);
        if (!String.IsNullOrEmpty(slugForCurrentLanguage) && 
            !slugForCurrentLanguage.Equals(slug, StringComparison.InvariantCultureIgnoreCase))
        {
            //we should make not null or "" validation above because some entities does not have SeName for standard (ID=0) language (e.g. news, blog posts)
            var webHelper = EngineContext.Current.Resolve<IWebHelper>();
            var response = httpContext.Response;
            //response.Status = "302 Found";
            response.Status = "302 Moved Temporarily";
            response.RedirectLocation = string.Format("{0}{1}", webHelper.GetStoreLocation(false), slugForCurrentLanguage);
            response.End();
            return null;
        }

        //process URL
        switch (urlRecord.EntityName.ToLowerInvariant())
        {
            case "product":
                {
                    data.Values["controller"] = "Product";
                    data.Values["action"] = "ProductDetails";
                    data.Values["productid"] = urlRecord.EntityId;
                    data.Values["SeName"] = urlRecord.Slug;
                }
                break;
            case "category":
                {
                    data.Values["controller"] = "Catalog";
                    data.Values["action"] = "Category";
                    data.Values["categoryid"] = urlRecord.EntityId;
                    data.Values["SeName"] = urlRecord.Slug;
                }
                break;
            case "manufacturer":
                {
                    data.Values["controller"] = "Catalog";
                    data.Values["action"] = "Manufacturer";
                    data.Values["manufacturerid"] = urlRecord.EntityId;
                    data.Values["SeName"] = urlRecord.Slug;
                }
                break;
            case "vendor":
                {
                    data.Values["controller"] = "Catalog";
                    data.Values["action"] = "Vendor";
                    data.Values["vendorid"] = urlRecord.EntityId;
                    data.Values["SeName"] = urlRecord.Slug;
                }
                break;
            case "newsitem":
                {
                    data.Values["controller"] = "News";
                    data.Values["action"] = "NewsItem";
                    data.Values["newsItemId"] = urlRecord.EntityId;
                    data.Values["SeName"] = urlRecord.Slug;
                }
                break;
            case "blogpost":
                {
                    data.Values["controller"] = "Blog";
                    data.Values["action"] = "BlogPost";
                    data.Values["blogPostId"] = urlRecord.EntityId;
                    data.Values["SeName"] = urlRecord.Slug;
                }
                break;
            case "topic":
                {
                    data.Values["controller"] = "Topic";
                    data.Values["action"] = "TopicDetails";
                    data.Values["topicId"] = urlRecord.EntityId;
                    data.Values["SeName"] = urlRecord.Slug;
                }
                break;
            default:
                {
                    //no record found

                    //generate an event this way developers could insert their own types
                    EngineContext.Current.Resolve<IEventPublisher>()
                        .Publish(new CustomUrlRecordEntityNameRequested(data, urlRecord));
                }
                break;
        }
    }
    return data;
}

当请求一个分类的产品列表的时候会进入switch的category的分支,也就是controller:Catalog,action:Category。

下面我们再来看看产品分类相关表:

1、Category:分类信息表

2020-04-23-20-48-59

分类信息表包含了产品类别的基础信息以及一些配置信息,如seo相关信息、筛选页大小选项、是否在首页显示等等。

2、CategoryTemplate:分类对应视图、模版

2020-04-23-20-49-04

CategoryTemplate表示分类对应的视图路径。

Nop.Web.Controllers.CatalogController

下面我们来看看Action–Category的完整代码:

public ActionResult Category(int categoryId, CatalogPagingFilteringModel command)
{
    var category = _categoryService.GetCategoryById(categoryId);
    if (category == null || category.Deleted)
        return InvokeHttp404();

    //Check whether the current user has a "Manage catalog" permission
    //It allows him to preview a category before publishing
    if (!category.Published && !_permissionService.Authorize(StandardPermissionProvider.ManageCategories))
        return InvokeHttp404();

    //ACL (access control list)
    if (!_aclService.Authorize(category))
        return InvokeHttp404();

    //Store mapping
    if (!_storeMappingService.Authorize(category))
        return InvokeHttp404();

    //'Continue shopping' URL
    _genericAttributeService.SaveAttribute(_workContext.CurrentCustomer, 
        SystemCustomerAttributeNames.LastContinueShoppingPage, 
        _webHelper.GetThisPageUrl(false),
        _storeContext.CurrentStore.Id);

    var model = category.ToModel();

    //处理排序字段
    PrepareSortingOptions(model.PagingFilteringContext, command);
    //view mode
    PrepareViewModes(model.PagingFilteringContext, command);
    //page size
    PreparePageSizeOptions(model.PagingFilteringContext, command,
        category.AllowCustomersToSelectPageSize, 
        category.PageSizeOptions, 
        category.PageSize);

    //价格筛选
    model.PagingFilteringContext.PriceRangeFilter.LoadPriceRangeFilters(category.PriceRanges, _webHelper, _priceFormatter);
    var selectedPriceRange = model.PagingFilteringContext.PriceRangeFilter.GetSelectedPriceRange(_webHelper, category.PriceRanges);
    decimal? minPriceConverted = null;
    decimal? maxPriceConverted = null;
    if (selectedPriceRange != null)
    {
        if (selectedPriceRange.From.HasValue)
            minPriceConverted = _currencyService.ConvertToPrimaryStoreCurrency(selectedPriceRange.From.Value, _workContext.WorkingCurrency);

        if (selectedPriceRange.To.HasValue)
            maxPriceConverted = _currencyService.ConvertToPrimaryStoreCurrency(selectedPriceRange.To.Value, _workContext.WorkingCurrency);
    }

    //分类面包屑
    model.DisplayCategoryBreadcrumb = _catalogSettings.CategoryBreadcrumbEnabled;
    if (model.DisplayCategoryBreadcrumb)
    {
        foreach (var catBr in category.GetCategoryBreadCrumb(_categoryService, _aclService, _storeMappingService))
        {
            model.CategoryBreadcrumb.Add(new CategoryModel()
            {
                Id = catBr.Id,
                Name = catBr.GetLocalized(x => x.Name),
                SeName = catBr.GetSeName()
            });
        }
    }

    var customerRolesIds = _workContext.CurrentCustomer.CustomerRoles
        .Where(cr => cr.Active).Select(cr => cr.Id).ToList();

    //subcategories
    string subCategoriesCacheKey = string.Format(ModelCacheEventConsumer.CATEGORY_SUBCATEGORIES_KEY,
        categoryId,
        string.Join(",", customerRolesIds),
        _storeContext.CurrentStore.Id,
        _workContext.WorkingLanguage.Id,
        _webHelper.IsCurrentConnectionSecured());
    model.SubCategories = _cacheManager.Get(subCategoriesCacheKey, () =>
    {
        return _categoryService.GetAllCategoriesByParentCategoryId(categoryId)
        .Select(x =>
        {
            var subCatModel = new CategoryModel.SubCategoryModel()
            {
                Id = x.Id,
                Name = x.GetLocalized(y => y.Name),
                SeName = x.GetSeName(),
            };

            //prepare picture model
            int pictureSize = _mediaSettings.CategoryThumbPictureSize;
            var categoryPictureCacheKey = string.Format(ModelCacheEventConsumer.CATEGORY_PICTURE_MODEL_KEY, x.Id, pictureSize, true, _workContext.WorkingLanguage.Id, _webHelper.IsCurrentConnectionSecured(), _storeContext.CurrentStore.Id);
            subCatModel.PictureModel = _cacheManager.Get(categoryPictureCacheKey, () =>
            {
                var picture = _pictureService.GetPictureById(x.PictureId);
                var pictureModel = new PictureModel()
                {
                    FullSizeImageUrl = _pictureService.GetPictureUrl(picture),
                    ImageUrl = _pictureService.GetPictureUrl(picture, pictureSize),
                    Title = string.Format(_localizationService.GetResource("Media.Category.ImageLinkTitleFormat"), subCatModel.Name),
                    AlternateText = string.Format(_localizationService.GetResource("Media.Category.ImageAlternateTextFormat"), subCatModel.Name)
                };
                return pictureModel;
            });

            return subCatModel;
        })
        .ToList();
    }); 

    //featured products
    if (!_catalogSettings.IgnoreFeaturedProducts)
    {
        //We cache a value indicating whether we have featured products
        IPagedList<Product> featuredProducts = null;
        string cacheKey = string.Format(ModelCacheEventConsumer.CATEGORY_HAS_FEATURED_PRODUCTS_KEY, categoryId,
            string.Join(",", customerRolesIds), _storeContext.CurrentStore.Id);
        var hasFeaturedProductsCache = _cacheManager.Get<bool?>(cacheKey);
        if (!hasFeaturedProductsCache.HasValue)
        {
            //no value in the cache yet
            //let's load products and cache the result (true/false)
            featuredProducts = _productService.SearchProducts(
               categoryIds: new List<int>() { category.Id },
               storeId: _storeContext.CurrentStore.Id,
               visibleIndividuallyOnly: true,
               featuredProducts: true);
            hasFeaturedProductsCache = featuredProducts.TotalCount > 0;
            _cacheManager.Set(cacheKey, hasFeaturedProductsCache, 60);
        }
        if (hasFeaturedProductsCache.Value && featuredProducts == null)
        {
            //cache indicates that the category has featured products
            //let's load them
            featuredProducts = _productService.SearchProducts(
               categoryIds: new List<int>() { category.Id },
               storeId: _storeContext.CurrentStore.Id,
               visibleIndividuallyOnly: true,
               featuredProducts: true);
        }
        if (featuredProducts != null)
        {
            model.FeaturedProducts = PrepareProductOverviewModels(featuredProducts).ToList();
        }
    }

    var categoryIds = new List<int>();
    categoryIds.Add(category.Id);
    if (_catalogSettings.ShowProductsFromSubcategories)
    {
        //include subcategories
        categoryIds.AddRange(GetChildCategoryIds(category.Id));
    }

    //products
    IList<int> alreadyFilteredSpecOptionIds = model.PagingFilteringContext.SpecificationFilter.GetAlreadyFilteredSpecOptionIds(_webHelper);
    IList<int> filterableSpecificationAttributeOptionIds = null;
    var products = _productService.SearchProducts(out filterableSpecificationAttributeOptionIds, true,
        categoryIds: categoryIds,
        storeId: _storeContext.CurrentStore.Id,
        visibleIndividuallyOnly: true,
        featuredProducts:_catalogSettings.IncludeFeaturedProductsInNormalLists ? null : (bool?)false,
        priceMin:minPriceConverted, 
        priceMax:maxPriceConverted,
        filteredSpecs: alreadyFilteredSpecOptionIds,
        orderBy: (ProductSortingEnum)command.OrderBy,
        pageIndex: command.PageNumber - 1,
        pageSize: command.PageSize);
    model.Products = PrepareProductOverviewModels(products).ToList();

    model.PagingFilteringContext.LoadPagedList(products);

    //specs
    model.PagingFilteringContext.SpecificationFilter.PrepareSpecsFilters(alreadyFilteredSpecOptionIds,
        filterableSpecificationAttributeOptionIds, 
        _specificationAttributeService, _webHelper, _workContext);

    //template
    var templateCacheKey = string.Format(ModelCacheEventConsumer.CATEGORY_TEMPLATE_MODEL_KEY, category.CategoryTemplateId);
    var templateViewPath = _cacheManager.Get(templateCacheKey, () =>
        {
            var template = _categoryTemplateService.GetCategoryTemplateById(category.CategoryTemplateId);
            if (template == null)
                template = _categoryTemplateService.GetAllCategoryTemplates().FirstOrDefault();
            if (template == null)
                throw new Exception("No default template could be loaded");
            return template.ViewPath;
        });

    //activity log
    _customerActivityService.InsertActivity("PublicStore.ViewCategory", _localizationService.GetResource("ActivityLog.PublicStore.ViewCategory"), category.Name);

    return View(templateViewPath, model);
}
原文出处:蓝狐软件工作室【蓝狐】

原文链接:http://m.lanhusoft.com/Article/349.html

本文观点不代表 .Net中文网 立场,转载请联系原作者。

发表评论

登录后才能评论